From 3f28f277bafbed244ffd1b71ac6b383f10fa38ca Mon Sep 17 00:00:00 2001 From: Jhonathan Abreu Date: Fri, 6 Sep 2024 15:18:45 -0400 Subject: [PATCH 1/5] Initial support for EUREX EuroStoxx50 futures and index --- ...iveBrokersBrokerageDataQueueHandlerTest.cs | 58 +++++++++++++++++++ .../InteractiveBrokersFuturesTests.cs | 57 ++++++++++-------- .../InteractiveBrokersSymbolMapperTests.cs | 33 +++++++++++ .../TestUtils.cs | 41 +++++++++++++ .../InteractiveBrokers/IB-symbol-map.json | 43 ++++++++------ .../InteractiveBrokersBrokerage.cs | 7 ++- .../InteractiveBrokersSymbolMapper.cs | 58 +++++++++++-------- 7 files changed, 228 insertions(+), 69 deletions(-) create mode 100644 QuantConnect.InteractiveBrokersBrokerage.Tests/TestUtils.cs diff --git a/QuantConnect.InteractiveBrokersBrokerage.Tests/InteractiveBrokersBrokerageDataQueueHandlerTest.cs b/QuantConnect.InteractiveBrokersBrokerage.Tests/InteractiveBrokersBrokerageDataQueueHandlerTest.cs index e4597be..8924116 100644 --- a/QuantConnect.InteractiveBrokersBrokerage.Tests/InteractiveBrokersBrokerageDataQueueHandlerTest.cs +++ b/QuantConnect.InteractiveBrokersBrokerage.Tests/InteractiveBrokersBrokerageDataQueueHandlerTest.cs @@ -25,6 +25,7 @@ using QuantConnect.Data.Market; using QuantConnect.Lean.Engine.DataFeeds.Enumerators; using QuantConnect.Securities; +using QuantConnect.Securities.Future; namespace QuantConnect.Tests.Brokerages.InteractiveBrokers { @@ -298,6 +299,63 @@ public void CannotSubscribeToCFDWithUnsupportedMarket() Assert.IsNull(enumerator); } + [Test] + public void CanSubscribeToEurexFutures() + { + // Wait a bit to make sure previous tests already disconnected from IB + Thread.Sleep(2000); + + using var ib = new InteractiveBrokersBrokerage(new QCAlgorithm(), new OrderProvider(), new SecurityProvider()); + ib.Connect(); + + var canonicalFuture = Symbol.Create("FESX", SecurityType.Future, Market.EUREX); + var contracts = TestUtils.GetFutureContracts(canonicalFuture, 3).ToList(); + Assert.AreEqual(3, contracts.Count); + + var resolutions = new[] { Resolution.Tick, Resolution.Second }; + var configs = contracts.SelectMany(symbol => resolutions.SelectMany(resolution => + { + return resolution switch + { + Resolution.Tick => new[] { GetSubscriptionDataConfig(symbol, resolution) }, + _ => new[] + { + GetSubscriptionDataConfig(symbol, resolution), + GetSubscriptionDataConfig(symbol, resolution) + } + }; + })); + + var cancelationToken = new CancellationTokenSource(); + var data = new List(); + + foreach (var config in configs) + { + ProcessFeed( + ib.Subscribe(config, (s, e) => + { + var dataPoint = ((NewDataAvailableEventArgs)e).DataPoint; + lock (data) + { + data.Add(dataPoint); + } + }), + cancelationToken, + (tick) => Log(tick)); + } + + Thread.Sleep(10 * 1000); + cancelationToken.Cancel(); + cancelationToken.Dispose(); + + var symbolsWithData = data.Select(tick => tick.Symbol).Distinct().ToList(); + CollectionAssert.AreEquivalent(contracts, symbolsWithData); + + var dataTypesWithData = data.Select(tick => tick.GetType()).Distinct().ToList(); + var expectedDataTypes = configs.Select(config => config.Type).Distinct().ToList(); + Assert.AreEqual(expectedDataTypes.Count, dataTypesWithData.Count); + } + protected SubscriptionDataConfig GetSubscriptionDataConfig(Symbol symbol, Resolution resolution) { var entry = MarketHoursDatabase.FromDataFolder().GetEntry(symbol.ID.Market, symbol, symbol.SecurityType); diff --git a/QuantConnect.InteractiveBrokersBrokerage.Tests/InteractiveBrokersFuturesTests.cs b/QuantConnect.InteractiveBrokersBrokerage.Tests/InteractiveBrokersFuturesTests.cs index 7346e25..ad9f984 100644 --- a/QuantConnect.InteractiveBrokersBrokerage.Tests/InteractiveBrokersFuturesTests.cs +++ b/QuantConnect.InteractiveBrokersBrokerage.Tests/InteractiveBrokersFuturesTests.cs @@ -13,14 +13,13 @@ * limitations under the License. */ +using System; using System.Collections.Generic; using System.Linq; using IBApi; using NUnit.Framework; using QuantConnect.Algorithm; using QuantConnect.Brokerages.InteractiveBrokers; -using QuantConnect.Data.Auxiliary; -using QuantConnect.Lean.Engine.DataFeeds; using QuantConnect.Logging; namespace QuantConnect.Tests.Brokerages.InteractiveBrokers @@ -287,7 +286,7 @@ public void CreatesExpectedFuturesContracts() { var contract = new Contract { - Symbol = symbolMapper.GetBrokerageRootSymbol(ticker), + Symbol = symbolMapper.GetBrokerageRootSymbol(ticker, SecurityType.Future), Currency = Currencies.USD, Exchange = null, SecType = "FUT" @@ -317,18 +316,18 @@ public void CreateExpectedFutureContractsWithDifferentCurrencies() var tickersByMarket = new Dictionary { - { - Market.HKFE, - new[] - { - "HSI" - } - }, + //{ + // Market.HKFE, + // new[] + // { + // "HSI" + // } + //}, { Market.CME, new[] { - "ACD", + //"ACD", "AJY", "ANE" } @@ -340,26 +339,38 @@ public void CreateExpectedFutureContractsWithDifferentCurrencies() { "ZC" } - } + }, + { + Market.EUREX, + new[] + { + "FESX" + } + }, }; - foreach (var kvp in tickersByMarket) + Assert.Multiple(() => { - var market = kvp.Key; - var tickers = kvp.Value; - - foreach (var ticker in tickers) + foreach (var kvp in tickersByMarket) { - var currentSymbol = Symbol.Create(ticker, SecurityType.Future, market); - var symbolsFound = ib.LookupSymbols(currentSymbol, false); - Assert.IsNotEmpty(symbolsFound); + var market = kvp.Key; + var tickers = kvp.Value; - foreach (var symbol in symbolsFound) + foreach (var ticker in tickers) { - Log.Trace($"Symbol found in IB: {symbol}"); + Log.Trace($"Market: {market} - Future Ticker: {ticker}"); + + var currentSymbol = Symbol.Create(ticker, SecurityType.Future, market); + var symbolsFound = ib.LookupSymbols(currentSymbol, false); + Assert.IsNotEmpty(symbolsFound, $"No contracts found for Market: {market} - Future Ticker: {ticker}"); + + foreach (var symbol in symbolsFound) + { + Log.Trace($" - Symbol found in IB: {symbol} :: {symbol.Value} :: {symbol.ID.Date}"); + } } } - } + }); } } } diff --git a/QuantConnect.InteractiveBrokersBrokerage.Tests/InteractiveBrokersSymbolMapperTests.cs b/QuantConnect.InteractiveBrokersBrokerage.Tests/InteractiveBrokersSymbolMapperTests.cs index a691b04..d01f356 100644 --- a/QuantConnect.InteractiveBrokersBrokerage.Tests/InteractiveBrokersSymbolMapperTests.cs +++ b/QuantConnect.InteractiveBrokersBrokerage.Tests/InteractiveBrokersSymbolMapperTests.cs @@ -204,5 +204,38 @@ public void FuturesOptionsWithUnderlyingContractMonthMappedByRuleResolvesUnderly Assert.AreEqual(expectedUnderlyingSymbol, futureOption.Underlying); } + + private static TestCaseData[] LeanIbSymbolMappingTestCases = new TestCaseData[] + { + new("6B", "GBP", SecurityType.Future), + new("AW", "AIGCI", SecurityType.Future), + new("FESX", "ESTX50", SecurityType.Future), + new("SX5E", "ESTX50", SecurityType.Index), + }; + + [TestCaseSource(nameof(LeanIbSymbolMappingTestCases))] + public void MapsLeanToIBSymbolDependingOnSecurityType(string leanTicker, string ibTicker, SecurityType securityType) + { + var mapper = new InteractiveBrokersSymbolMapper(TestGlobals.MapFileProvider); + + var leanSymbol = Symbol.Create(leanTicker, securityType, Market.InteractiveBrokers); + var resultIbTicker = mapper.GetBrokerageSymbol(leanSymbol); + + Assert.AreEqual(ibTicker, resultIbTicker); + } + + [TestCaseSource(nameof(LeanIbSymbolMappingTestCases))] + public void MapsIBToLeanSymbolDependingOnSecurityType(string leanTicker, string ibTicker, SecurityType securityType) + { + var mapper = new InteractiveBrokersSymbolMapper(TestGlobals.MapFileProvider); + + var expiry = new DateTime(2024, 09, 20); + var expectedLeanSymbol = securityType == SecurityType.Future + ? Symbol.CreateFuture(leanTicker, Market.InteractiveBrokers, expiry) + : Symbol.Create(leanTicker, securityType, Market.InteractiveBrokers); + var resultLeanSymbol = mapper.GetLeanSymbol(ibTicker, securityType, expectedLeanSymbol.ID.Market, expiry); + + Assert.AreEqual(expectedLeanSymbol, resultLeanSymbol); + } } } diff --git a/QuantConnect.InteractiveBrokersBrokerage.Tests/TestUtils.cs b/QuantConnect.InteractiveBrokersBrokerage.Tests/TestUtils.cs new file mode 100644 index 0000000..b1ef9fa --- /dev/null +++ b/QuantConnect.InteractiveBrokersBrokerage.Tests/TestUtils.cs @@ -0,0 +1,41 @@ +/* + * QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals. + * Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. +*/ + +using QuantConnect.Securities.Future; +using System.Collections.Generic; +using System; + +namespace QuantConnect.Tests.Brokerages.InteractiveBrokers +{ + public class TestUtils + { + public static IEnumerable GetFutureContracts(Symbol canonical, int count) + { + var expiryFunction = FuturesExpiryFunctions.FuturesExpiryFunction(canonical); + var lastExpiryMonth = DateTime.UtcNow; + + // Get the next N contracts + for (var i = 0; i < count; i++) + { + var expiry = expiryFunction(lastExpiryMonth); + var contract = Symbol.CreateFuture(canonical.ID.Symbol, canonical.ID.Market, expiry); + + yield return contract; + + lastExpiryMonth = expiry.AddMonths(1); + } + } + } +} diff --git a/QuantConnect.InteractiveBrokersBrokerage/InteractiveBrokers/IB-symbol-map.json b/QuantConnect.InteractiveBrokersBrokerage/InteractiveBrokers/IB-symbol-map.json index dfe2fef..aef885f 100644 --- a/QuantConnect.InteractiveBrokersBrokerage/InteractiveBrokers/IB-symbol-map.json +++ b/QuantConnect.InteractiveBrokersBrokerage/InteractiveBrokers/IB-symbol-map.json @@ -1,22 +1,27 @@ /* This is a manually created file that contains mappings from IB own naming to original symbols defined by respective exchanges. */ { - // Futures - "GBP": "6B", - "CAD": "6C", - "JPY": "6J", - "CHF": "6S", - "EUR": "6E", - "AUD": "6A", - "NZD": "6N", - "VIX": "VX", - "AIGCI": "AW", - "AC": "EH", - "BRE": "6L", - "MXP": "6M", - "RUR": "6R", - "ZAR": "6Z", - "BRR": "BTC", - "ETHUSDRR": "ETH", - "DA": "DC", - "BQX": "BIO" + "Future": { + "GBP": "6B", + "CAD": "6C", + "JPY": "6J", + "CHF": "6S", + "EUR": "6E", + "AUD": "6A", + "NZD": "6N", + "VIX": "VX", + "AIGCI": "AW", + "AC": "EH", + "BRE": "6L", + "MXP": "6M", + "RUR": "6R", + "ZAR": "6Z", + "BRR": "BTC", + "ETHUSDRR": "ETH", + "DA": "DC", + "BQX": "BIO", + "ESTX50": "FESX" + }, + "Index": { + "ESTX50": "SX5E" + } } diff --git a/QuantConnect.InteractiveBrokersBrokerage/InteractiveBrokersBrokerage.cs b/QuantConnect.InteractiveBrokersBrokerage/InteractiveBrokersBrokerage.cs index 72ae609..f057ece 100644 --- a/QuantConnect.InteractiveBrokersBrokerage/InteractiveBrokersBrokerage.cs +++ b/QuantConnect.InteractiveBrokersBrokerage/InteractiveBrokersBrokerage.cs @@ -188,7 +188,8 @@ public sealed class InteractiveBrokersBrokerage : Brokerage, IDataQueueHandler, { Market.CBOT, "CBOT" }, { Market.ICE, "NYBOT" }, { Market.CFE, "CFE" }, - { Market.NYSELIFFE, "NYSELIFFE" } + { Market.NYSELIFFE, "NYSELIFFE" }, + { Market.EUREX, "EUREX" } }; private readonly SymbolPropertiesDatabase _symbolPropertiesDatabase = SymbolPropertiesDatabase.FromDataFolder(); @@ -3426,7 +3427,7 @@ private Symbol MapSymbol(Contract contract) // Handle future options as a Future, up until we actually return the future. if (isFutureOption || securityType == SecurityType.Future) { - var leanSymbol = _symbolMapper.GetLeanRootSymbol(ibSymbol); + var leanSymbol = _symbolMapper.GetLeanRootSymbol(ibSymbol, securityType); var defaultMarket = market; if (!_symbolPropertiesDatabase.TryGetMarket(leanSymbol, SecurityType.Future, out market)) @@ -4067,7 +4068,7 @@ public IEnumerable LookupSymbols(Symbol symbol, bool includeExpired, str // setting up lookup request var contract = new Contract { - Symbol = _symbolMapper.GetBrokerageRootSymbol(lookupName), + Symbol = _symbolMapper.GetBrokerageRootSymbol(lookupName, symbol.SecurityType), Currency = securityCurrency ?? symbolProperties.QuoteCurrency, Exchange = exchangeSpecifier, SecType = ConvertSecurityType(symbol.SecurityType), diff --git a/QuantConnect.InteractiveBrokersBrokerage/InteractiveBrokersSymbolMapper.cs b/QuantConnect.InteractiveBrokersBrokerage/InteractiveBrokersSymbolMapper.cs index 63a9eed..87308ae 100644 --- a/QuantConnect.InteractiveBrokersBrokerage/InteractiveBrokersSymbolMapper.cs +++ b/QuantConnect.InteractiveBrokersBrokerage/InteractiveBrokersSymbolMapper.cs @@ -39,9 +39,9 @@ public class InteractiveBrokersSymbolMapper : ISymbolMapper { private readonly IMapFileProvider _mapFileProvider; - // we have a special treatment of futures, because IB renamed several exchange tickers (like GBP instead of 6B). We fix this: - // We map those tickers back to their original names using the map below - private readonly Dictionary _ibNameMap = new Dictionary(); + // we have a special treatment of futures and other security types, because IB renamed several exchange tickers (like GBP instead of 6B). + // We fix this: We map those tickers back to their original names using the map below + private readonly Dictionary> _ibNameMap = new Dictionary>(); /// /// Constructs InteractiveBrokersSymbolMapper. Default parameters are used. @@ -56,7 +56,7 @@ public InteractiveBrokersSymbolMapper(IMapFileProvider mapFileProvider) : /// Constructs InteractiveBrokersSymbolMapper /// /// New names map (IB -> LEAN) - public InteractiveBrokersSymbolMapper(Dictionary ibNameMap) + public InteractiveBrokersSymbolMapper(Dictionary> ibNameMap) { _ibNameMap = ibNameMap; } @@ -69,7 +69,7 @@ public InteractiveBrokersSymbolMapper(string ibNameMapFullName) { if (File.Exists(ibNameMapFullName)) { - _ibNameMap = JsonConvert.DeserializeObject>(File.ReadAllText(ibNameMapFullName)); + _ibNameMap = JsonConvert.DeserializeObject>>(File.ReadAllText(ibNameMapFullName)); } } /// @@ -124,16 +124,15 @@ public string GetBrokerageSymbol(Symbol symbol) case SecurityType.Future: case SecurityType.Cfd: - return GetBrokerageRootSymbol(symbol.ID.Symbol); + ticker = symbol.ID.Symbol; + break; case SecurityType.Equity: - return ticker.Replace(".", " "); - - case SecurityType.Index: - return ticker; + ticker = ticker.Replace(".", " "); + break; } - return ticker; + return GetBrokerageRootSymbol(ticker, symbol.SecurityType); } /// @@ -163,31 +162,35 @@ public string GetBrokerageSymbol(Symbol symbol) try { + var ticker = GetLeanRootSymbol(brokerageSymbol, securityType); switch (securityType) { case SecurityType.Future: - return Symbol.CreateFuture(GetLeanRootSymbol(brokerageSymbol), market, expirationDate); + return Symbol.CreateFuture(ticker, market, expirationDate); case SecurityType.Option: // See SecurityType.Equity case. The equity underlying may include a space, e.g. BRK B. brokerageSymbol = brokerageSymbol.Replace(" ", "."); - return Symbol.CreateOption(brokerageSymbol, market, OptionStyle.American, optionRight, strike, expirationDate); + ticker = GetLeanRootSymbol(brokerageSymbol, securityType); + return Symbol.CreateOption(ticker, market, OptionStyle.American, optionRight, strike, expirationDate); case SecurityType.IndexOption: // Index Options have their expiry offset from their last trading date by one day. We add one day // to get the expected expiration date. + ticker = GetLeanRootSymbol(brokerageSymbol, securityType); return Symbol.CreateOption( - Symbol.Create(IndexOptionSymbol.MapToUnderlying(brokerageSymbol), SecurityType.Index, market), - brokerageSymbol, + Symbol.Create(IndexOptionSymbol.MapToUnderlying(ticker), SecurityType.Index, market), + ticker, market, securityType.DefaultOptionStyle(), optionRight, strike, - IndexOptionSymbol.GetExpiryDate(brokerageSymbol, expirationDate)); + IndexOptionSymbol.GetExpiryDate(ticker, expirationDate)); case SecurityType.FutureOption: + ticker = GetLeanRootSymbol(brokerageSymbol, securityType); var future = FuturesOptionsUnderlyingMapper.GetUnderlyingFutureFromFutureOption( - GetLeanRootSymbol(brokerageSymbol), + GetLeanRootSymbol(ticker, securityType), market, expirationDate, DateTime.Now); @@ -209,10 +212,11 @@ public string GetBrokerageSymbol(Symbol symbol) case SecurityType.Equity: brokerageSymbol = brokerageSymbol.Replace(" ", "."); + ticker = GetLeanRootSymbol(brokerageSymbol, securityType); break; } - return Symbol.Create(brokerageSymbol, securityType, market); + return Symbol.Create(ticker, securityType, market); } catch (Exception exception) { @@ -226,11 +230,15 @@ public string GetBrokerageSymbol(Symbol symbol) /// /// LEAN root symbol /// - public string GetBrokerageRootSymbol(string rootSymbol) + public string GetBrokerageRootSymbol(string rootSymbol, SecurityType securityType) { - var brokerageSymbol = _ibNameMap.FirstOrDefault(kv => kv.Value == rootSymbol); + var brokerageSymbol = rootSymbol; + if (_ibNameMap.TryGetValue(securityType, out var symbolMap)) + { + brokerageSymbol = symbolMap.FirstOrDefault(kv => kv.Value == rootSymbol).Key; + } - return brokerageSymbol.Key ?? rootSymbol; + return brokerageSymbol ?? rootSymbol; } /// @@ -238,9 +246,11 @@ public string GetBrokerageRootSymbol(string rootSymbol) /// /// IB Brokerage root symbol /// - public string GetLeanRootSymbol(string brokerageRootSymbol) + public string GetLeanRootSymbol(string brokerageRootSymbol, SecurityType securityType) { - return _ibNameMap.ContainsKey(brokerageRootSymbol) ? _ibNameMap[brokerageRootSymbol] : brokerageRootSymbol; + return _ibNameMap.TryGetValue(securityType, out var symbolMap) && symbolMap.TryGetValue(brokerageRootSymbol, out var rootSymbol) + ? rootSymbol + : brokerageRootSymbol; } /// @@ -271,7 +281,7 @@ public Contract ParseMalformedContractFutureSymbol(Contract malformedContract, S var contractMonthExpiration = DateTime.ParseExact(match[2].Value, "MMM", CultureInfo.CurrentCulture).Month; var contractYearExpiration = match[3].Value; - var leanSymbol = GetLeanRootSymbol(contractSymbol); + var leanSymbol = GetLeanRootSymbol(contractSymbol, SecurityType.Future); string market; if (!symbolPropertiesDatabase.TryGetMarket(leanSymbol, SecurityType.Future, out market)) { From f884d2a2effa73ad3537b12287fad211395d6e2f Mon Sep 17 00:00:00 2001 From: Jhonathan Abreu Date: Mon, 9 Sep 2024 10:28:03 -0400 Subject: [PATCH 2/5] EUREX futures trading tests --- ...iveBrokersBrokerageDataQueueHandlerTest.cs | 48 +++++++- .../InteractiveBrokersEurexOrderTests.cs | 116 ++++++++++++++++++ .../InteractiveBrokersBrokerage.cs | 2 +- 3 files changed, 164 insertions(+), 2 deletions(-) create mode 100644 QuantConnect.InteractiveBrokersBrokerage.Tests/InteractiveBrokersEurexOrderTests.cs diff --git a/QuantConnect.InteractiveBrokersBrokerage.Tests/InteractiveBrokersBrokerageDataQueueHandlerTest.cs b/QuantConnect.InteractiveBrokersBrokerage.Tests/InteractiveBrokersBrokerageDataQueueHandlerTest.cs index 8924116..b9794fb 100644 --- a/QuantConnect.InteractiveBrokersBrokerage.Tests/InteractiveBrokersBrokerageDataQueueHandlerTest.cs +++ b/QuantConnect.InteractiveBrokersBrokerage.Tests/InteractiveBrokersBrokerageDataQueueHandlerTest.cs @@ -25,7 +25,6 @@ using QuantConnect.Data.Market; using QuantConnect.Lean.Engine.DataFeeds.Enumerators; using QuantConnect.Securities; -using QuantConnect.Securities.Future; namespace QuantConnect.Tests.Brokerages.InteractiveBrokers { @@ -356,6 +355,53 @@ public void CanSubscribeToEurexFutures() Assert.AreEqual(expectedDataTypes.Count, dataTypesWithData.Count); } + [Test] + public void CanSubscribeToEurexIndex() + { + // Wait a bit to make sure previous tests already disconnected from IB + Thread.Sleep(2000); + + using var ib = new InteractiveBrokersBrokerage(new QCAlgorithm(), new OrderProvider(), new SecurityProvider()); + ib.Connect(); + + var index = Symbol.Create("SX5E", SecurityType.Index, Market.EUREX); + + var resolutions = new[] { Resolution.Tick, Resolution.Second }; + var configs = resolutions.Select(resolution => resolution == Resolution.Tick + ? GetSubscriptionDataConfig(index, resolution) + : GetSubscriptionDataConfig(index, resolution)); + + var cancelationToken = new CancellationTokenSource(); + var data = new List(); + + foreach (var config in configs) + { + ProcessFeed( + ib.Subscribe(config, (s, e) => + { + var dataPoint = ((NewDataAvailableEventArgs)e).DataPoint; + lock (data) + { + data.Add(dataPoint); + } + }), + cancelationToken, + (tick) => Log(tick)); + } + + Thread.Sleep(20 * 1000); + cancelationToken.Cancel(); + cancelationToken.Dispose(); + + var symbolsWithData = data.Select(tick => tick.Symbol).Distinct().ToList(); + Assert.AreEqual(1, symbolsWithData.Count); + Assert.AreEqual(index, symbolsWithData[0]); + + var dataTypesWithData = data.Select(tick => tick.GetType()).Distinct().ToList(); + var expectedDataTypes = configs.Select(config => config.Type).Distinct().ToList(); + Assert.AreEqual(expectedDataTypes.Count, dataTypesWithData.Count); + } + protected SubscriptionDataConfig GetSubscriptionDataConfig(Symbol symbol, Resolution resolution) { var entry = MarketHoursDatabase.FromDataFolder().GetEntry(symbol.ID.Market, symbol, symbol.SecurityType); diff --git a/QuantConnect.InteractiveBrokersBrokerage.Tests/InteractiveBrokersEurexOrderTests.cs b/QuantConnect.InteractiveBrokersBrokerage.Tests/InteractiveBrokersEurexOrderTests.cs new file mode 100644 index 0000000..2555c1a --- /dev/null +++ b/QuantConnect.InteractiveBrokersBrokerage.Tests/InteractiveBrokersEurexOrderTests.cs @@ -0,0 +1,116 @@ +/* + * QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals. + * Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. +*/ + +using NUnit.Framework; +using QuantConnect.Algorithm; +using QuantConnect.Brokerages.InteractiveBrokers; +using QuantConnect.Interfaces; +using QuantConnect.Securities; +using System.Linq; + +namespace QuantConnect.Tests.Brokerages.InteractiveBrokers +{ + [TestFixture] + [Explicit("These tests require the IBGateway to be installed.")] + public class InteractiveBrokersEurexOrderTests : BrokerageTests + { + private static Symbol EuroStoxx50FutureSymbol = TestUtils.GetFutureContracts(Symbol.Create("FESX", SecurityType.Future, Market.EUREX), 1).Single(); + + protected override Symbol Symbol => EuroStoxx50FutureSymbol; + protected override SecurityType SecurityType => Symbol.SecurityType; + + private static TestCaseData[] EuroStoxx50FuturesOrderTest() + { + return new[] + { + new TestCaseData(new MarketOrderTestParameters(EuroStoxx50FutureSymbol)), + new TestCaseData(new LimitOrderTestParameters(EuroStoxx50FutureSymbol, 10000m, 10m)), + new TestCaseData(new StopMarketOrderTestParameters(EuroStoxx50FutureSymbol, 10000m, 10m)), + new TestCaseData(new StopLimitOrderTestParameters(EuroStoxx50FutureSymbol, 10000m, 10m)), + new TestCaseData(new LimitIfTouchedOrderTestParameters(EuroStoxx50FutureSymbol, 10000m, 10m)), + }; + } + + #region EuroStoxx50 Futures + + [Test, TestCaseSource(nameof(EuroStoxx50FuturesOrderTest))] + public override void CancelOrders(OrderTestParameters parameters) + { + base.CancelOrders(parameters); + } + + [Test, TestCaseSource(nameof(EuroStoxx50FuturesOrderTest))] + public override void LongFromZero(OrderTestParameters parameters) + { + base.LongFromZero(parameters); + } + + [Test, TestCaseSource(nameof(EuroStoxx50FuturesOrderTest))] + public override void CloseFromLong(OrderTestParameters parameters) + { + base.CloseFromLong(parameters); + } + + [Test, TestCaseSource(nameof(EuroStoxx50FuturesOrderTest))] + public override void ShortFromZero(OrderTestParameters parameters) + { + base.ShortFromZero(parameters); + } + + [Test, TestCaseSource(nameof(EuroStoxx50FuturesOrderTest))] + public override void CloseFromShort(OrderTestParameters parameters) + { + base.CloseFromShort(parameters); + } + + [Test, TestCaseSource(nameof(EuroStoxx50FuturesOrderTest))] + public override void ShortFromLong(OrderTestParameters parameters) + { + base.ShortFromLong(parameters); + } + + [Test, TestCaseSource(nameof(EuroStoxx50FuturesOrderTest))] + public override void LongFromShort(OrderTestParameters parameters) + { + base.LongFromShort(parameters); + } + + #endregion + + protected override bool IsAsync() + { + return true; + } + + protected override decimal GetAskPrice(Symbol symbol) + { + return 1m; + } + + protected override IBrokerage CreateBrokerage(IOrderProvider orderProvider, ISecurityProvider securityProvider) + { + return new InteractiveBrokersBrokerage(new QCAlgorithm(), orderProvider, securityProvider); + } + + protected override void DisposeBrokerage(IBrokerage brokerage) + { + if (brokerage != null) + { + brokerage.Disconnect(); + brokerage.Dispose(); + } + } + } +} diff --git a/QuantConnect.InteractiveBrokersBrokerage/InteractiveBrokersBrokerage.cs b/QuantConnect.InteractiveBrokersBrokerage/InteractiveBrokersBrokerage.cs index f057ece..1cc1142 100644 --- a/QuantConnect.InteractiveBrokersBrokerage/InteractiveBrokersBrokerage.cs +++ b/QuantConnect.InteractiveBrokersBrokerage/InteractiveBrokersBrokerage.cs @@ -3783,7 +3783,7 @@ private static bool CanSubscribe(Symbol symbol) (securityType == SecurityType.Forex && market == Market.Oanda) || (securityType == SecurityType.Option && market == Market.USA) || (securityType == SecurityType.IndexOption && market == Market.USA) || - (securityType == SecurityType.Index && market == Market.USA) || + (securityType == SecurityType.Index && (market == Market.USA || market == Market.EUREX)) || (securityType == SecurityType.FutureOption) || (securityType == SecurityType.Future) || (securityType == SecurityType.Cfd && market == Market.InteractiveBrokers); From b2e21eead60cf770d519488b8470152d406f9350 Mon Sep 17 00:00:00 2001 From: Jhonathan Abreu Date: Thu, 12 Sep 2024 09:59:57 -0400 Subject: [PATCH 3/5] Cleanup for symbol mapper --- .../InteractiveBrokersSymbolMapperTests.cs | 4 ++ .../InteractiveBrokersSymbolMapper.cs | 65 +++++++------------ 2 files changed, 27 insertions(+), 42 deletions(-) diff --git a/QuantConnect.InteractiveBrokersBrokerage.Tests/InteractiveBrokersSymbolMapperTests.cs b/QuantConnect.InteractiveBrokersBrokerage.Tests/InteractiveBrokersSymbolMapperTests.cs index d01f356..d445b72 100644 --- a/QuantConnect.InteractiveBrokersBrokerage.Tests/InteractiveBrokersSymbolMapperTests.cs +++ b/QuantConnect.InteractiveBrokersBrokerage.Tests/InteractiveBrokersSymbolMapperTests.cs @@ -82,6 +82,10 @@ public void ReturnsCorrectBrokerageSymbol() symbol = Symbol.Create("BRK.B", SecurityType.Equity, Market.USA); brokerageSymbol = mapper.GetBrokerageSymbol(symbol); Assert.AreEqual("BRK B", brokerageSymbol); + + symbol = Symbol.CreateCanonicalOption(symbol); + brokerageSymbol = mapper.GetBrokerageSymbol(symbol); + Assert.AreEqual("BRK B", brokerageSymbol); } [TestCase("AAPL", "AAPL")] diff --git a/QuantConnect.InteractiveBrokersBrokerage/InteractiveBrokersSymbolMapper.cs b/QuantConnect.InteractiveBrokersBrokerage/InteractiveBrokersSymbolMapper.cs index 87308ae..563bf6c 100644 --- a/QuantConnect.InteractiveBrokersBrokerage/InteractiveBrokersSymbolMapper.cs +++ b/QuantConnect.InteractiveBrokersBrokerage/InteractiveBrokersSymbolMapper.cs @@ -84,13 +84,6 @@ public string GetBrokerageSymbol(Symbol symbol) throw new ArgumentException("Invalid symbol: " + (symbol == null ? "null" : symbol.ToString())); } - var ticker = GetMappedTicker(symbol); - - if (string.IsNullOrWhiteSpace(ticker)) - { - throw new ArgumentException("Invalid symbol: " + symbol.ToString()); - } - if (symbol.ID.SecurityType != SecurityType.Forex && symbol.ID.SecurityType != SecurityType.Equity && symbol.ID.SecurityType != SecurityType.Index && @@ -103,33 +96,24 @@ public string GetBrokerageSymbol(Symbol symbol) throw new ArgumentException("Invalid security type: " + symbol.ID.SecurityType); } - if (symbol.ID.SecurityType == SecurityType.Forex && ticker.Length != 6) + if (symbol.ID.SecurityType == SecurityType.FutureOption) { - throw new ArgumentException("Forex symbol length must be equal to 6: " + symbol.Value); + // We use the underlying Future Symbol since IB doesn't use + // the Futures Options' ticker, but rather uses the underlying's + // Symbol, mapped to the brokerage. + return GetBrokerageSymbol(symbol.Underlying); } - switch (symbol.ID.SecurityType) + var ticker = GetMappedTicker(symbol); + + if (string.IsNullOrWhiteSpace(ticker)) { - case SecurityType.Option: - case SecurityType.IndexOption: - // Final case is for equities. We use the mapped value to select - // the equity we want to trade. We skip mapping for index options. - return GetMappedTicker(symbol.Underlying); - - case SecurityType.FutureOption: - // We use the underlying Future Symbol since IB doesn't use - // the Futures Options' ticker, but rather uses the underlying's - // Symbol, mapped to the brokerage. - return GetBrokerageSymbol(symbol.Underlying); - - case SecurityType.Future: - case SecurityType.Cfd: - ticker = symbol.ID.Symbol; - break; - - case SecurityType.Equity: - ticker = ticker.Replace(".", " "); - break; + throw new ArgumentException("Invalid symbol: " + symbol.ToString()); + } + + if (symbol.ID.SecurityType == SecurityType.Forex && ticker.Length != 6) + { + throw new ArgumentException("Forex symbol length must be equal to 6: " + symbol.Value); } return GetBrokerageRootSymbol(ticker, symbol.SecurityType); @@ -169,15 +153,11 @@ public string GetBrokerageSymbol(Symbol symbol) return Symbol.CreateFuture(ticker, market, expirationDate); case SecurityType.Option: - // See SecurityType.Equity case. The equity underlying may include a space, e.g. BRK B. - brokerageSymbol = brokerageSymbol.Replace(" ", "."); - ticker = GetLeanRootSymbol(brokerageSymbol, securityType); return Symbol.CreateOption(ticker, market, OptionStyle.American, optionRight, strike, expirationDate); case SecurityType.IndexOption: // Index Options have their expiry offset from their last trading date by one day. We add one day // to get the expected expiration date. - ticker = GetLeanRootSymbol(brokerageSymbol, securityType); return Symbol.CreateOption( Symbol.Create(IndexOptionSymbol.MapToUnderlying(ticker), SecurityType.Index, market), ticker, @@ -188,7 +168,6 @@ public string GetBrokerageSymbol(Symbol symbol) IndexOptionSymbol.GetExpiryDate(ticker, expirationDate)); case SecurityType.FutureOption: - ticker = GetLeanRootSymbol(brokerageSymbol, securityType); var future = FuturesOptionsUnderlyingMapper.GetUnderlyingFutureFromFutureOption( GetLeanRootSymbol(ticker, securityType), market, @@ -209,11 +188,6 @@ public string GetBrokerageSymbol(Symbol symbol) optionRight, strike, expirationDate); - - case SecurityType.Equity: - brokerageSymbol = brokerageSymbol.Replace(" ", "."); - ticker = GetLeanRootSymbol(brokerageSymbol, securityType); - break; } return Symbol.Create(ticker, securityType, market); @@ -248,9 +222,11 @@ public string GetBrokerageRootSymbol(string rootSymbol, SecurityType securityTyp /// public string GetLeanRootSymbol(string brokerageRootSymbol, SecurityType securityType) { - return _ibNameMap.TryGetValue(securityType, out var symbolMap) && symbolMap.TryGetValue(brokerageRootSymbol, out var rootSymbol) + var ticker = _ibNameMap.TryGetValue(securityType, out var symbolMap) && symbolMap.TryGetValue(brokerageRootSymbol, out var rootSymbol) ? rootSymbol : brokerageRootSymbol; + + return ticker.Replace(" ", "."); } /// @@ -306,6 +282,11 @@ public Contract ParseMalformedContractFutureSymbol(Contract malformedContract, S private string GetMappedTicker(Symbol symbol) { + if (symbol.ID.SecurityType == SecurityType.Option || symbol.ID.SecurityType == SecurityType.IndexOption) + { + return GetMappedTicker(symbol.Underlying); + } + var ticker = symbol.ID.Symbol; if (symbol.ID.SecurityType == SecurityType.Equity) { @@ -313,7 +294,7 @@ private string GetMappedTicker(Symbol symbol) ticker = mapFile.GetMappedSymbol(DateTime.UtcNow, symbol.Value); } - return ticker; + return ticker.Replace(".", " "); } /// From e96708de62f951cf624406ac60f3773e111122bd Mon Sep 17 00:00:00 2001 From: Jhonathan Abreu Date: Thu, 12 Sep 2024 10:46:02 -0400 Subject: [PATCH 4/5] Minor symbol mapper changes --- .../InteractiveBrokersSymbolMapper.cs | 23 ++++++++----------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/QuantConnect.InteractiveBrokersBrokerage/InteractiveBrokersSymbolMapper.cs b/QuantConnect.InteractiveBrokersBrokerage/InteractiveBrokersSymbolMapper.cs index 563bf6c..b171d0a 100644 --- a/QuantConnect.InteractiveBrokersBrokerage/InteractiveBrokersSymbolMapper.cs +++ b/QuantConnect.InteractiveBrokersBrokerage/InteractiveBrokersSymbolMapper.cs @@ -96,14 +96,6 @@ public string GetBrokerageSymbol(Symbol symbol) throw new ArgumentException("Invalid security type: " + symbol.ID.SecurityType); } - if (symbol.ID.SecurityType == SecurityType.FutureOption) - { - // We use the underlying Future Symbol since IB doesn't use - // the Futures Options' ticker, but rather uses the underlying's - // Symbol, mapped to the brokerage. - return GetBrokerageSymbol(symbol.Underlying); - } - var ticker = GetMappedTicker(symbol); if (string.IsNullOrWhiteSpace(ticker)) @@ -226,7 +218,12 @@ public string GetLeanRootSymbol(string brokerageRootSymbol, SecurityType securit ? rootSymbol : brokerageRootSymbol; - return ticker.Replace(" ", "."); + if (securityType == SecurityType.Equity || securityType == SecurityType.Option) + { + ticker = ticker.Replace(" ", "."); + } + + return ticker; } /// @@ -282,19 +279,19 @@ public Contract ParseMalformedContractFutureSymbol(Contract malformedContract, S private string GetMappedTicker(Symbol symbol) { - if (symbol.ID.SecurityType == SecurityType.Option || symbol.ID.SecurityType == SecurityType.IndexOption) + if (symbol.ID.SecurityType.IsOption()) { - return GetMappedTicker(symbol.Underlying); + symbol = symbol.Underlying; } var ticker = symbol.ID.Symbol; if (symbol.ID.SecurityType == SecurityType.Equity) { var mapFile = _mapFileProvider.Get(AuxiliaryDataKey.Create(symbol)).ResolveMapFile(symbol); - ticker = mapFile.GetMappedSymbol(DateTime.UtcNow, symbol.Value); + ticker = mapFile.GetMappedSymbol(DateTime.UtcNow, symbol.Value).Replace(".", " "); } - return ticker.Replace(".", " "); + return ticker; } /// From 5f745b50c774c99abebd8f01efe609e79e458ef1 Mon Sep 17 00:00:00 2001 From: Jhonathan Abreu Date: Thu, 12 Sep 2024 13:28:47 -0400 Subject: [PATCH 5/5] Unit tests cleanup --- .../InteractiveBrokersFuturesTests.cs | 8 -------- 1 file changed, 8 deletions(-) diff --git a/QuantConnect.InteractiveBrokersBrokerage.Tests/InteractiveBrokersFuturesTests.cs b/QuantConnect.InteractiveBrokersBrokerage.Tests/InteractiveBrokersFuturesTests.cs index ad9f984..73c7d8e 100644 --- a/QuantConnect.InteractiveBrokersBrokerage.Tests/InteractiveBrokersFuturesTests.cs +++ b/QuantConnect.InteractiveBrokersBrokerage.Tests/InteractiveBrokersFuturesTests.cs @@ -316,18 +316,10 @@ public void CreateExpectedFutureContractsWithDifferentCurrencies() var tickersByMarket = new Dictionary { - //{ - // Market.HKFE, - // new[] - // { - // "HSI" - // } - //}, { Market.CME, new[] { - //"ACD", "AJY", "ANE" }