Skip to content

Commit 5ff8565

Browse files
authored
Feat: cache PrimaryExchange for Equities (#185)
* feat: PrimaryExchangeService test:feat: test primary exchange service * fix: if primary exchange is null * test:feat: LeanPrimaryExchangeShouldMachIB * remove: not used cache in EquityPrimaryExchangeService * remove: primaryExchange service * refactor: GetPrimaryExchange simplify * refactor: GetPrimaryExchange + xml description
1 parent b9921a3 commit 5ff8565

File tree

2 files changed

+87
-7
lines changed

2 files changed

+87
-7
lines changed

QuantConnect.InteractiveBrokersBrokerage.Tests/InteractiveBrokersBrokerageAdditionalTests.cs

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
*/
1515

1616
using System;
17+
using System.Text;
1718
using System.Collections.Generic;
1819
using System.Diagnostics;
1920
using System.Globalization;
@@ -1175,6 +1176,66 @@ private Contract CreateContract(Symbol symbol)
11751176
return contract;
11761177
}
11771178

1179+
[Test, Explicit("Long-running test (~10 minutes). Compares LEAN and IB API primary exchanges for up to 1000 equity symbols.")]
1180+
public void GetEquityPrimaryExchangeShouldMatchBetweenLeanAndIB()
1181+
{
1182+
using var ib = new InteractiveBrokersBrokerage(new QCAlgorithm(), new OrderProvider(), new SecurityProvider());
1183+
ib.Connect();
1184+
1185+
var tickers = QuantConnect.Algorithm.CSharp.StressSymbols.StockSymbols.ToList();
1186+
1187+
var totalCount = default(int);
1188+
var equalCount = default(int);
1189+
1190+
var logBuilder = new StringBuilder($"***** GetEquityPrimaryExchange Test ({tickers.Count} tickers) *****");
1191+
var logBuilder2 = new StringBuilder("***** MissMatched Symbols *****");
1192+
1193+
foreach (var ticker in tickers)
1194+
{
1195+
totalCount++;
1196+
1197+
var symbol = Symbol.Create(ticker, SecurityType.Equity, Market.USA);
1198+
1199+
var contract = CreateContract(symbol);
1200+
1201+
var leanPrimaryExchange = ib.GetPrimaryExchange(contract, symbol);
1202+
1203+
var ibPrimaryExchange = ib.GetContractDetails(contract, symbol.Value)?.Contract.PrimaryExch;
1204+
1205+
if (ibPrimaryExchange == null)
1206+
{
1207+
totalCount--;
1208+
logBuilder.AppendLine($"[SKIP] Contract not found for {symbol}");
1209+
continue;
1210+
}
1211+
1212+
if (totalCount == 1000)
1213+
{
1214+
logBuilder.AppendLine("Symbol processing limit reached (1000). Stopping test.");
1215+
break;
1216+
}
1217+
1218+
Assert.IsNotNull(leanPrimaryExchange);
1219+
1220+
bool isEqual = string.Equals(leanPrimaryExchange, ibPrimaryExchange, StringComparison.InvariantCultureIgnoreCase);
1221+
if (isEqual)
1222+
{
1223+
logBuilder.AppendLine($"[RESULT] {symbol.Value} | LEAN = {leanPrimaryExchange} | IB API = {ibPrimaryExchange} | Match = {isEqual}");
1224+
equalCount++;
1225+
}
1226+
else
1227+
{
1228+
logBuilder2.AppendLine($"[RESULT] {symbol.Value} | LEAN = {leanPrimaryExchange} | IB API = {ibPrimaryExchange} | Match = {isEqual}");
1229+
}
1230+
}
1231+
1232+
logBuilder.AppendLine("----- Test Summary -----");
1233+
logBuilder.AppendLine($"Processed: {totalCount} | Matches: {equalCount} | Mismatches: {totalCount - equalCount}");
1234+
1235+
Log.Trace(logBuilder.ToString());
1236+
Log.Trace(logBuilder2.ToString());
1237+
}
1238+
11781239
private List<BaseData> GetHistory(
11791240
Symbol symbol,
11801241
Resolution resolution,

QuantConnect.InteractiveBrokersBrokerage/InteractiveBrokersBrokerage.cs

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,11 @@ public sealed class InteractiveBrokersBrokerage : Brokerage, IDataQueueHandler,
207207

208208
private static readonly SymbolPropertiesDatabase _symbolPropertiesDatabase = SymbolPropertiesDatabase.FromDataFolder();
209209

210+
/// <summary>
211+
/// Provides primary exchange data based on LEAN map files.
212+
/// </summary>
213+
private MapFilePrimaryExchangeProvider _exchangeProvider;
214+
210215
// exchange time zones by symbol
211216
private readonly Dictionary<Symbol, DateTimeZone> _symbolExchangeTimeZones = new Dictionary<Symbol, DateTimeZone>();
212217

@@ -1374,6 +1379,7 @@ private void Initialize(
13741379

13751380
_symbolMapper = new InteractiveBrokersSymbolMapper(_mapFileProvider);
13761381
_contractSpecificationService = new(GetContractDetails);
1382+
_exchangeProvider = new MapFilePrimaryExchangeProvider(_mapFileProvider);
13771383

13781384
_subscriptionManager = new EventBasedDataQueueHandlerSubscriptionManager();
13791385
_subscriptionManager.SubscribeImpl += (s, t) => Subscribe(s);
@@ -1615,24 +1621,37 @@ public static string GetContractDescription(Contract contract)
16151621
return $"{contract} {contract.PrimaryExch ?? string.Empty} {contract.LastTradeDateOrContractMonth.ToStringInvariant()} {contract.Strike.ToStringInvariant()} {contract.Right}";
16161622
}
16171623

1618-
private string GetPrimaryExchange(Contract contract, Symbol symbol)
1624+
/// <summary>
1625+
/// Retrieves the primary exchange for a given symbol.
1626+
/// </summary>
1627+
/// <param name="contract">The IB <see cref="Contract"/> object containing contract details.</param>
1628+
/// <param name="symbol">The Lean <see cref="Symbol"/> object representing the security.</param>
1629+
/// <returns>
1630+
/// The name of the primary exchange as a string.
1631+
/// If the market is USA and Lean provides an exchange, that value is returned.
1632+
/// Otherwise, falls back to the IB contract's <c>PrimaryExch</c> field.
1633+
/// Returns <c>null</c> if no exchange information is available.
1634+
/// </returns>
1635+
internal string GetPrimaryExchange(Contract contract, Symbol symbol)
16191636
{
1620-
var details = GetContractDetails(contract, symbol.Value);
1621-
if (details == null)
1637+
if (symbol.ID.Market.Equals(Market.USA, StringComparison.InvariantCultureIgnoreCase))
16221638
{
1623-
// we were unable to find the contract details
1624-
return null;
1639+
var leanExchange = _exchangeProvider.GetPrimaryExchange(symbol.ID)?.Name;
1640+
if (!string.IsNullOrEmpty(leanExchange))
1641+
{
1642+
return leanExchange;
1643+
}
16251644
}
16261645

1627-
return details.Contract.PrimaryExch;
1646+
return GetContractDetails(contract, symbol.Value)?.Contract.PrimaryExch;
16281647
}
16291648

16301649
/// <summary>
16311650
/// Will return and cache the IB contract details for the requested contract
16321651
/// </summary>
16331652
/// <param name="contract">The target contract</param>
16341653
/// <param name="ticker">The associated Lean ticker. Just used for logging, can be provided empty</param>
1635-
private ContractDetails GetContractDetails(Contract contract, string ticker, bool failIfNotFound = true)
1654+
internal ContractDetails GetContractDetails(Contract contract, string ticker, bool failIfNotFound = true)
16361655
{
16371656
if (contract.SecType != null && _contractDetails.TryGetValue(GetUniqueKey(contract), out var details))
16381657
{

0 commit comments

Comments
 (0)