From 6cfc43b60ab75c2fc96b0fa39381f7d532c29b83 Mon Sep 17 00:00:00 2001 From: Jhonathan Abreu Date: Thu, 14 Mar 2024 12:47:18 -0400 Subject: [PATCH 01/16] Add support for CFD data source --- ...iveBrokersBrokerageDataQueueHandlerTest.cs | 69 ++++++++++++++++++- .../InteractiveBrokers/IB-symbol-map.json | 18 ++++- .../InteractiveBrokersBrokerage.cs | 9 ++- .../InteractiveBrokersSymbolMapper.cs | 18 ++++- 4 files changed, 107 insertions(+), 7 deletions(-) diff --git a/QuantConnect.InteractiveBrokersBrokerage.Tests/InteractiveBrokersBrokerageDataQueueHandlerTest.cs b/QuantConnect.InteractiveBrokersBrokerage.Tests/InteractiveBrokersBrokerageDataQueueHandlerTest.cs index c9b666c..4efb44c 100644 --- a/QuantConnect.InteractiveBrokersBrokerage.Tests/InteractiveBrokersBrokerageDataQueueHandlerTest.cs +++ b/QuantConnect.InteractiveBrokersBrokerage.Tests/InteractiveBrokersBrokerageDataQueueHandlerTest.cs @@ -22,11 +22,9 @@ using QuantConnect.Algorithm; using QuantConnect.Brokerages.InteractiveBrokers; using QuantConnect.Data; -using QuantConnect.Data.Auxiliary; using QuantConnect.Data.Market; -using QuantConnect.Lean.Engine.DataFeeds; +using QuantConnect.Lean.Engine.DataFeeds.Enumerators; using QuantConnect.Securities; -using QuantConnect.Tests.Engine.DataFeeds; namespace QuantConnect.Tests.Brokerages.InteractiveBrokers { @@ -140,6 +138,71 @@ public void GetsTickDataAfterDisconnectionConnectionCycle() } } + [Test] + public void CanSubscribeToCFD() + { + using var ib = new InteractiveBrokersBrokerage(new QCAlgorithm(), new OrderProvider(), new SecurityProvider()); + ib.Connect(); + + var cancelationToken = new CancellationTokenSource(); + + var symbolsWithData = new HashSet(); + + var equityCfds = new[] { "AAPL", "SPY", "GOOG" }; + var forexCfds = new[] { "AUDUSD", "NZDUSD", "USDCAD", "USDCHF" }; + var indexCfds = new[] { "SPX500USD", "AU200AUD", "US30USD", "NAS100USD", "UK100GBP", "EU50EUR", "DE40EUR", "FR40EUR", "ES35EUR", + "NL25EUR", "CH20CHF", "JP225USD", "HK50HKD" }; + + var tickers = equityCfds.Concat(forexCfds).Concat(indexCfds); + + foreach (var ticker in tickers) + { + var symbol = Symbol.Create(ticker, SecurityType.Cfd, Market.Oanda); + var configs = new[] + { + GetSubscriptionDataConfig(symbol, Resolution.Second), + GetSubscriptionDataConfig(symbol, Resolution.Second), + }; + + foreach (var config in configs) + { + ProcessFeed( + ib.Subscribe(config, (s, e) => + { + symbolsWithData.Add(((NewDataAvailableEventArgs)e).DataPoint.Symbol); + }), + cancelationToken, + (tick) => Log(tick)); + } + } + + Thread.Sleep(10 * 1000); + cancelationToken.Cancel(); + cancelationToken.Dispose(); + + Assert.IsNotEmpty(symbolsWithData); + + // IB does not stream data for equities and Forex CFDs: https://ibkrcampus.com/ibkr-api-page/trader-workstation-api/#re-route-cfds + Assert.IsFalse(equityCfds.Any(x => symbolsWithData.Any(symbol => symbol.Value == x))); + Assert.IsFalse(forexCfds.Any(x => symbolsWithData.Any(symbol => symbol.Value == x))); + + Console.WriteLine(string.Join(", ", symbolsWithData.Select(s => s.Value))); + } + + [Test] + public void CannotSubscribeToCFDWithUnsupportedMarket() + { + using var ib = new InteractiveBrokersBrokerage(new QCAlgorithm(), new OrderProvider(), new SecurityProvider()); + ib.Connect(); + + var usSpx500Cfd = Symbol.Create("SPX500USD", SecurityType.Cfd, Market.FXCM); + var config = GetSubscriptionDataConfig(usSpx500Cfd, Resolution.Second); + + var enumerator = ib.Subscribe(config, (s, e) => { }); + + Assert.IsNull(enumerator); + } + protected SubscriptionDataConfig GetSubscriptionDataConfig(Symbol symbol, Resolution resolution) { var entry = MarketHoursDatabase.FromDataFolder().GetEntry(symbol.ID.Market, symbol, symbol.SecurityType); diff --git a/QuantConnect.InteractiveBrokersBrokerage/InteractiveBrokers/IB-symbol-map.json b/QuantConnect.InteractiveBrokersBrokerage/InteractiveBrokers/IB-symbol-map.json index 1ca7228..b585a06 100644 --- a/QuantConnect.InteractiveBrokersBrokerage/InteractiveBrokers/IB-symbol-map.json +++ b/QuantConnect.InteractiveBrokersBrokerage/InteractiveBrokers/IB-symbol-map.json @@ -1,5 +1,6 @@ /* 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", @@ -16,5 +17,20 @@ "ZAR": "6Z", "BRR": "BTC", "DA": "DC", - "BQX": "BIO" + "BQX": "BIO", + + // CFDs + "IBUS500": "SPX500USD", + "IBUS30": "US30USD", + "IBUST100": "NAS100USD", + "IBGB100": "UK100GBP", + "IBEU50": "EU50EUR", + "IBDE40": "DE40EUR", + "IBFR40": "FR40EUR", + "IBES35": "ES35EUR", + "IBNL25": "NL25EUR", + "IBCH20": "CH20CHF", + "IBJP225": "JP225USD", + "IBHK50": "HK50HKD", + "IBAU200": "AU200AUD" } diff --git a/QuantConnect.InteractiveBrokersBrokerage/InteractiveBrokersBrokerage.cs b/QuantConnect.InteractiveBrokersBrokerage/InteractiveBrokersBrokerage.cs index d19d443..8bd2433 100644 --- a/QuantConnect.InteractiveBrokersBrokerage/InteractiveBrokersBrokerage.cs +++ b/QuantConnect.InteractiveBrokersBrokerage/InteractiveBrokersBrokerage.cs @@ -3178,6 +3178,9 @@ private static string ConvertSecurityType(SecurityType type) case SecurityType.Future: return IB.SecurityType.Future; + case SecurityType.Cfd: + return IB.SecurityType.ContractForDifference; + default: throw new ArgumentException($"The {type} security type is not currently supported."); } @@ -3210,6 +3213,9 @@ private SecurityType ConvertSecurityType(Contract contract) case IB.SecurityType.Future: return SecurityType.Future; + case IB.SecurityType.ContractForDifference: + return SecurityType.Cfd; + default: throw new NotSupportedException( $"An existing position or open order for an unsupported security type was found: {GetContractDescription(contract)}. " + @@ -3677,7 +3683,8 @@ private static bool CanSubscribe(Symbol symbol) (securityType == SecurityType.IndexOption && market == Market.USA) || (securityType == SecurityType.Index && market == Market.USA) || (securityType == SecurityType.FutureOption) || - (securityType == SecurityType.Future); + (securityType == SecurityType.Future) || + (securityType == SecurityType.Cfd && market == Market.Oanda); } /// diff --git a/QuantConnect.InteractiveBrokersBrokerage/InteractiveBrokersSymbolMapper.cs b/QuantConnect.InteractiveBrokersBrokerage/InteractiveBrokersSymbolMapper.cs index 8777a4b..ebea31a 100644 --- a/QuantConnect.InteractiveBrokersBrokerage/InteractiveBrokersSymbolMapper.cs +++ b/QuantConnect.InteractiveBrokersBrokerage/InteractiveBrokersSymbolMapper.cs @@ -80,12 +80,16 @@ public InteractiveBrokersSymbolMapper(string ibNameMapFullName) public string GetBrokerageSymbol(Symbol symbol) { if (string.IsNullOrWhiteSpace(symbol?.Value)) + { 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 && @@ -93,11 +97,16 @@ public string GetBrokerageSymbol(Symbol symbol) symbol.ID.SecurityType != SecurityType.Option && symbol.ID.SecurityType != SecurityType.IndexOption && symbol.ID.SecurityType != SecurityType.FutureOption && - symbol.ID.SecurityType != SecurityType.Future) + symbol.ID.SecurityType != SecurityType.Future && + symbol.ID.SecurityType != SecurityType.Cfd) + { throw new ArgumentException("Invalid security type: " + symbol.ID.SecurityType); + } if (symbol.ID.SecurityType == SecurityType.Forex && ticker.Length != 6) + { throw new ArgumentException("Forex symbol length must be equal to 6: " + symbol.Value); + } switch (symbol.ID.SecurityType) { @@ -114,6 +123,7 @@ public string GetBrokerageSymbol(Symbol symbol) return GetBrokerageSymbol(symbol.Underlying); case SecurityType.Future: + case SecurityType.Cfd: return GetBrokerageRootSymbol(symbol.ID.Symbol); case SecurityType.Equity: @@ -158,7 +168,7 @@ public string GetBrokerageSymbol(Symbol symbol) return Symbol.CreateFuture(GetLeanRootSymbol(brokerageSymbol), market, expirationDate); case SecurityType.Option: - // See SecurityType.Equity case. The equity underlying may include a space, e.g. BRK B. + // 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); @@ -199,6 +209,10 @@ public string GetBrokerageSymbol(Symbol symbol) case SecurityType.Equity: brokerageSymbol = brokerageSymbol.Replace(" ", "."); break; + + case SecurityType.Cfd: + brokerageSymbol = GetLeanRootSymbol(brokerageSymbol); + break; } return Symbol.Create(brokerageSymbol, securityType, market); From ff53b12373b5b3d01b96e4096b351f195bcd35df Mon Sep 17 00:00:00 2001 From: Jhonathan Abreu Date: Thu, 14 Mar 2024 14:58:41 -0400 Subject: [PATCH 02/16] Minor unit test improvements --- ...iveBrokersBrokerageDataQueueHandlerTest.cs | 34 ++++++++++++------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/QuantConnect.InteractiveBrokersBrokerage.Tests/InteractiveBrokersBrokerageDataQueueHandlerTest.cs b/QuantConnect.InteractiveBrokersBrokerage.Tests/InteractiveBrokersBrokerageDataQueueHandlerTest.cs index 4efb44c..904255e 100644 --- a/QuantConnect.InteractiveBrokersBrokerage.Tests/InteractiveBrokersBrokerageDataQueueHandlerTest.cs +++ b/QuantConnect.InteractiveBrokersBrokerage.Tests/InteractiveBrokersBrokerageDataQueueHandlerTest.cs @@ -138,15 +138,21 @@ public void GetsTickDataAfterDisconnectionConnectionCycle() } } - [Test] - public void CanSubscribeToCFD() + [TestCase(TickType.Trade, Resolution.Tick)] + [TestCase(TickType.Quote, Resolution.Tick)] + [TestCase(TickType.Quote, Resolution.Second)] + public void CanSubscribeToCFD(TickType tickType, Resolution resolution) { + // 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 cancelationToken = new CancellationTokenSource(); var symbolsWithData = new HashSet(); + var locker = new object(); var equityCfds = new[] { "AAPL", "SPY", "GOOG" }; var forexCfds = new[] { "AUDUSD", "NZDUSD", "USDCAD", "USDCHF" }; @@ -158,22 +164,24 @@ public void CanSubscribeToCFD() foreach (var ticker in tickers) { var symbol = Symbol.Create(ticker, SecurityType.Cfd, Market.Oanda); - var configs = new[] + var config = resolution switch { - GetSubscriptionDataConfig(symbol, Resolution.Second), - GetSubscriptionDataConfig(symbol, Resolution.Second), + Resolution.Tick => GetSubscriptionDataConfig(symbol, resolution), + _ => tickType == TickType.Trade + ? GetSubscriptionDataConfig(symbol, resolution) + : GetSubscriptionDataConfig(symbol, resolution) }; - foreach (var config in configs) - { - ProcessFeed( - ib.Subscribe(config, (s, e) => + ProcessFeed( + ib.Subscribe(config, (s, e) => + { + lock (locker) { symbolsWithData.Add(((NewDataAvailableEventArgs)e).DataPoint.Symbol); - }), - cancelationToken, - (tick) => Log(tick)); - } + } + }), + cancelationToken, + (tick) => Log(tick)); } Thread.Sleep(10 * 1000); From 648fc20ce9f05affa8e50b60f4e0f908ed78c651 Mon Sep 17 00:00:00 2001 From: Jhonathan Abreu Date: Fri, 15 Mar 2024 10:07:22 -0400 Subject: [PATCH 03/16] Re-route CFD data request to underlying when signaled by IB --- ...iveBrokersBrokerageDataQueueHandlerTest.cs | 42 +- .../Client/InteractiveBrokersClient.cs | 50 ++ .../RerouteMarketDataRequestEventArgs.cs | 51 ++ .../InteractiveBrokersBrokerage.cs | 453 +++++++++++------- 4 files changed, 402 insertions(+), 194 deletions(-) create mode 100644 QuantConnect.InteractiveBrokersBrokerage/Client/RerouteMarketDataRequestEventArgs.cs diff --git a/QuantConnect.InteractiveBrokersBrokerage.Tests/InteractiveBrokersBrokerageDataQueueHandlerTest.cs b/QuantConnect.InteractiveBrokersBrokerage.Tests/InteractiveBrokersBrokerageDataQueueHandlerTest.cs index 904255e..81abc06 100644 --- a/QuantConnect.InteractiveBrokersBrokerage.Tests/InteractiveBrokersBrokerageDataQueueHandlerTest.cs +++ b/QuantConnect.InteractiveBrokersBrokerage.Tests/InteractiveBrokersBrokerageDataQueueHandlerTest.cs @@ -138,10 +138,29 @@ public void GetsTickDataAfterDisconnectionConnectionCycle() } } - [TestCase(TickType.Trade, Resolution.Tick)] - [TestCase(TickType.Quote, Resolution.Tick)] - [TestCase(TickType.Quote, Resolution.Second)] - public void CanSubscribeToCFD(TickType tickType, Resolution resolution) + private static TestCaseData[] GetCFDSubscriptionTestCases() + { + var baseTestCases = new[] + { + new { TickType = TickType.Trade, Resolution = Resolution.Tick }, + new { TickType = TickType.Quote, Resolution = Resolution.Tick }, + new { TickType = TickType.Quote, Resolution = Resolution.Second } + }; + + var equityCfds = new[] { "AAPL", "SPY", "GOOG" }; + var indexCfds = new[] { "SPX500USD", "AU200AUD", "US30USD", "NAS100USD", "UK100GBP", "DE30EUR", "FR40EUR", "HK50HKD", "JP225" }; + var forexCfds = new[] { "AUDUSD", "NZDUSD", "USDCAD", "USDCHF" }; + + return baseTestCases.SelectMany(testCase => new[] + { + new TestCaseData(equityCfds, testCase.TickType, testCase.Resolution), + new TestCaseData(indexCfds, testCase.TickType, testCase.Resolution), + new TestCaseData(forexCfds, testCase.TickType, testCase.Resolution), + }).ToArray(); + } + + [TestCaseSource(nameof(GetCFDSubscriptionTestCases))] + public void CanSubscribeToCFD(IEnumerable tickers, TickType tickType, Resolution resolution) { // Wait a bit to make sure previous tests already disconnected from IB Thread.Sleep(2000); @@ -154,13 +173,6 @@ public void CanSubscribeToCFD(TickType tickType, Resolution resolution) var symbolsWithData = new HashSet(); var locker = new object(); - var equityCfds = new[] { "AAPL", "SPY", "GOOG" }; - var forexCfds = new[] { "AUDUSD", "NZDUSD", "USDCAD", "USDCHF" }; - var indexCfds = new[] { "SPX500USD", "AU200AUD", "US30USD", "NAS100USD", "UK100GBP", "EU50EUR", "DE40EUR", "FR40EUR", "ES35EUR", - "NL25EUR", "CH20CHF", "JP225USD", "HK50HKD" }; - - var tickers = equityCfds.Concat(forexCfds).Concat(indexCfds); - foreach (var ticker in tickers) { var symbol = Symbol.Create(ticker, SecurityType.Cfd, Market.Oanda); @@ -188,13 +200,9 @@ public void CanSubscribeToCFD(TickType tickType, Resolution resolution) cancelationToken.Cancel(); cancelationToken.Dispose(); - Assert.IsNotEmpty(symbolsWithData); - - // IB does not stream data for equities and Forex CFDs: https://ibkrcampus.com/ibkr-api-page/trader-workstation-api/#re-route-cfds - Assert.IsFalse(equityCfds.Any(x => symbolsWithData.Any(symbol => symbol.Value == x))); - Assert.IsFalse(forexCfds.Any(x => symbolsWithData.Any(symbol => symbol.Value == x))); - Console.WriteLine(string.Join(", ", symbolsWithData.Select(s => s.Value))); + + Assert.IsTrue(tickers.Any(x => symbolsWithData.Any(symbol => symbol.Value == x))); } [Test] diff --git a/QuantConnect.InteractiveBrokersBrokerage/Client/InteractiveBrokersClient.cs b/QuantConnect.InteractiveBrokersBrokerage/Client/InteractiveBrokersClient.cs index d0fac98..250ef38 100644 --- a/QuantConnect.InteractiveBrokersBrokerage/Client/InteractiveBrokersClient.cs +++ b/QuantConnect.InteractiveBrokersBrokerage/Client/InteractiveBrokersClient.cs @@ -160,6 +160,16 @@ public class InteractiveBrokersClient : DefaultEWrapper, IDisposable /// public event EventHandler FamilyCodes; + /// + /// ReRouteMarketDataRequest event handler + /// + public event EventHandler ReRouteMarketDataRequest; + + /// + /// ReRouteMarketDataDepthRequest event handler + /// + public event EventHandler ReRouteMarketDataDepthRequest; + #endregion /// @@ -503,6 +513,30 @@ public override void familyCodes(FamilyCode[] familyCodes) OnFamilyCodes(new FamilyCodesEventArgs(familyCodes)); } + /// + /// Callback that gets the parameters to re-route a Level 1 market data request. + /// Stock and Forex CFDs requests are re-routed to their underlyings. + /// + /// The request ID + /// The contract ID of the underlying + /// The underlying primary exchange + public override void rerouteMktDataReq(int reqId, int conId, string exchange) + { + OnReRouteMarketDataRequest(new RerouteMarketDataRequestEventArgs(reqId, conId, exchange)); + } + + /// + /// Callback that gets the parameters to re-route a Level 2market data request. + /// Stock and Forex CFDs requests are re-routed to their underlyings. + /// + /// The request ID + /// The contract ID of the underlying + /// The underlying primary exchange + public override void rerouteMktDepthReq(int reqId, int conId, string exchange) + { + OnReRouteMarketDepthRequest(new RerouteMarketDataRequestEventArgs(reqId, conId, exchange)); + } + #endregion #region Event Invocators @@ -723,6 +757,22 @@ protected virtual void OnFamilyCodes(FamilyCodesEventArgs e) FamilyCodes?.Invoke(this, e); } + /// + /// ReRouteMarketDataRequest event invocator + /// + private void OnReRouteMarketDataRequest(RerouteMarketDataRequestEventArgs args) + { + ReRouteMarketDataRequest?.Invoke(this, args); + } + + /// + /// ReRouteMarketDepthRequest event invocator + /// + private void OnReRouteMarketDepthRequest(RerouteMarketDataRequestEventArgs args) + { + ReRouteMarketDataDepthRequest?.Invoke(this, args); + } + #endregion } } diff --git a/QuantConnect.InteractiveBrokersBrokerage/Client/RerouteMarketDataRequestEventArgs.cs b/QuantConnect.InteractiveBrokersBrokerage/Client/RerouteMarketDataRequestEventArgs.cs new file mode 100644 index 0000000..5fa2a80 --- /dev/null +++ b/QuantConnect.InteractiveBrokersBrokerage/Client/RerouteMarketDataRequestEventArgs.cs @@ -0,0 +1,51 @@ +/* + * 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 System; + +namespace QuantConnect.Brokerages.InteractiveBrokers.Client +{ + /// + /// Event arguments class for the + /// and events. + /// + public class RerouteMarketDataRequestEventArgs : EventArgs + { + /// + /// The request ID used to track data + /// + public int RequestId { get; } + + /// + /// The underlying contract ID for the new request + /// + public int ContractId { get; } + + /// + /// The Underlying's primary exchange + /// + public string UnderlyingPrimaryExchange { get; } + + /// + /// Initializes a new instance of the class + /// + public RerouteMarketDataRequestEventArgs(int requestId, int contractId, string underlyingPrimaryExchange) + { + RequestId = requestId; + ContractId = contractId; + UnderlyingPrimaryExchange = underlyingPrimaryExchange; + } + } +} \ No newline at end of file diff --git a/QuantConnect.InteractiveBrokersBrokerage/InteractiveBrokersBrokerage.cs b/QuantConnect.InteractiveBrokersBrokerage/InteractiveBrokersBrokerage.cs index 8bd2433..554261e 100644 --- a/QuantConnect.InteractiveBrokersBrokerage/InteractiveBrokersBrokerage.cs +++ b/QuantConnect.InteractiveBrokersBrokerage/InteractiveBrokersBrokerage.cs @@ -75,8 +75,8 @@ public sealed class InteractiveBrokersBrokerage : Brokerage, IDataQueueHandler, private static bool _submissionOrdersWarningSent; private readonly HashSet _noSubmissionOrderTypes = new(new[] { - OrderType.MarketOnOpen - ,OrderType.ComboLegLimit, + OrderType.MarketOnOpen, + OrderType.ComboLegLimit, // combo market & limit do not send submission event when trading only options OrderType.ComboMarket, OrderType.ComboLimit @@ -2846,6 +2846,7 @@ private Contract CreateContract(Symbol symbol, bool includeExpired, List SecType = securityType, Currency = symbolProperties.QuoteCurrency }; + if (symbol.ID.SecurityType == SecurityType.Forex) { // forex is special, so rewrite some of the properties to make it work @@ -2853,19 +2854,37 @@ private Contract CreateContract(Symbol symbol, bool includeExpired, List contract.Symbol = ibSymbol.Substring(0, 3); contract.Currency = ibSymbol.Substring(3); } + else if (symbol.ID.SecurityType == SecurityType.Cfd) + { + // Let's try getting the contract details in order to get the type of CFD (stock, index or forex) + var details = GetContractDetails(contract, symbol.Value); + + // if null, it might be a forex CFD, we need to split the symbol just like we do for forex + if (details == null) + { + contract.Exchange = "SMART"; + contract.Symbol = ibSymbol.Substring(0, 3); + contract.Currency = ibSymbol.Substring(3); + + details = GetContractDetails(contract, symbol.Value); - if (symbol.ID.SecurityType == SecurityType.Equity) + if (details == null || details.UnderSecType != IB.SecurityType.Cash) + { + Log.Error("InteractiveBrokersBrokerage.CreateContract(): Unable to resolve CFD symbol: " + symbol.Value); + return null; + } + } + } + else if (symbol.ID.SecurityType == SecurityType.Equity) { contract.PrimaryExch = GetPrimaryExchange(contract, symbol); } - // Indexes requires that the exchange be specified exactly - if (symbol.ID.SecurityType == SecurityType.Index) + else if (symbol.ID.SecurityType == SecurityType.Index) { contract.Exchange = IndexSymbol.GetIndexExchange(symbol); } - - if (symbol.ID.SecurityType.IsOption()) + else if (symbol.ID.SecurityType.IsOption()) { // Subtract a day from Index Options, since their last trading date // is on the day before the expiry. @@ -2905,7 +2924,7 @@ private Contract CreateContract(Symbol symbol, bool includeExpired, List contract.TradingClass = GetTradingClass(contract, symbol); contract.IncludeExpired = includeExpired; } - if (symbol.ID.SecurityType == SecurityType.Future) + else if (symbol.ID.SecurityType == SecurityType.Future) { // we convert Market.* markets into IB exchanges if we have them in our map @@ -3552,18 +3571,80 @@ private bool Subscribe(IEnumerable symbols) // track subscription time for minimum delay in unsubscribe _subscriptionTimes[id] = DateTime.UtcNow; - if (_enableDelayedStreamingData) + var requestData = (Contract contract) => { - // Switch to delayed market data if the user does not have the necessary real time data subscription. - // If live data is available, it will always be returned instead of delayed data. - Client.ClientSocket.reqMarketDataType(3); - } + if (_enableDelayedStreamingData) + { + // Switch to delayed market data if the user does not have the necessary real time data subscription. + // If live data is available, it will always be returned instead of delayed data. + Client.ClientSocket.reqMarketDataType(3); + } + + // we would like to receive OI (101) + Client.ClientSocket.reqMktData(id, contract, "101", false, false, new List()); + }; + + requestData(contract); + + if (symbol.ID.SecurityType == SecurityType.Cfd) + { + // we need to listen for market data request re-routings since IB does not have data for Equity and Forex CFDs + // Reference: https://ibkrcampus.com/ibkr-api-page/trader-workstation-api/#re-route-cfds + + var rerouted = new ManualResetEventSlim(false); + + EventHandler handleRerouteEvent = (_, e) => + { + if (e.RequestId == id) + { + Log.Trace($"InteractiveBrokersBrokerage.Subscribe(): Re-routing {symbol} CFD data request to underlying"); + rerouted.Set(); + + // re-route the request to the underlying + var underlyingContract = new Contract + { + ConId = e.ContractId, + Exchange = e.UnderlyingPrimaryExchange, + }; + + requestData(underlyingContract); + + // TODO: What happens if the underlying is already subscribed? + // TODO: What happens if the underlying is subscribed after the CFD? + } + }; + + EventHandler handleData = (_, e) => + { + if (e.TickerId == id) + { + // received data, no need for re-routing + rerouted.Set(); + } + }; + + Client.ReRouteMarketDataRequest += handleRerouteEvent; + Client.ReRouteMarketDataDepthRequest += handleRerouteEvent; + Client.TickSize += handleData; - // we would like to receive OI (101) - Client.ClientSocket.reqMktData(id, contract, "101", false, false, new List()); + // TODO: What to do if timeout? + rerouted.Wait(TimeSpan.FromSeconds(5)); + + Client.ReRouteMarketDataRequest -= handleRerouteEvent; + Client.ReRouteMarketDataDepthRequest -= handleRerouteEvent; + Client.TickSize -= handleData; + } _subscribedSymbols[symbol] = id; - _subscribedTickers[id] = new SubscriptionEntry { Symbol = subscribeSymbol, PriceMagnifier = priceMagnifier }; + var subscriptionEntry = new SubscriptionEntry { Symbol = subscribeSymbol, PriceMagnifier = priceMagnifier }; + lock (_subscribedTickers) + { + if (!_subscribedTickers.TryGetValue(id, out var subscriptionEntries)) + { + _subscribedTickers[id] = subscriptionEntries = new List(); + } + subscriptionEntries.Add(subscriptionEntry); + } Log.Trace($"InteractiveBrokersBrokerage.Subscribe(): Subscribe Processed: {symbol.Value} ({GetContractDescription(contract)}) # {id}. SubscribedSymbols.Count: {_subscribedSymbols.Count}"); } @@ -3643,8 +3724,22 @@ private bool Unsubscribe(IEnumerable symbols) Client.ClientSocket.cancelMktData(id); - SubscriptionEntry entry; - return _subscribedTickers.TryRemove(id, out entry); + lock (_subscribedTickers) + { + if (!_subscribedTickers.TryGetValue(id, out var subscriptionEntries)) + { + return false; + } + + var removed = subscriptionEntries.RemoveAll(x => x.Symbol == symbol); + + if (subscriptionEntries.Count == 0) + { + _subscribedTickers.Remove(id, out _); + } + + return removed > 0; + } } } } @@ -3710,220 +3805,224 @@ private void HandleTickPrice(object sender, IB.TickPriceEventArgs e) // tickPrice events are always followed by tickSize events, // so we save off the bid/ask/last prices and only emit ticks in the tickSize event handler. - SubscriptionEntry entry; - if (!_subscribedTickers.TryGetValue(e.TickerId, out entry)) + if (!_subscribedTickers.TryGetValue(e.TickerId, out var entries)) { return; } - var symbol = entry.Symbol; + foreach (var entry in entries) + { + var symbol = entry.Symbol; - // negative price (-1) means no price available, normalize to zero - var price = e.Price < 0 ? 0 : Convert.ToDecimal(e.Price) / entry.PriceMagnifier; + // negative price (-1) means no price available, normalize to zero + var price = e.Price < 0 ? 0 : Convert.ToDecimal(e.Price) / entry.PriceMagnifier; - switch (e.Field) - { - case IBApi.TickType.BID: - case IBApi.TickType.DELAYED_BID: + switch (e.Field) + { + case IBApi.TickType.BID: + case IBApi.TickType.DELAYED_BID: - if (entry.LastQuoteTick == null) - { - entry.LastQuoteTick = new Tick + if (entry.LastQuoteTick == null) { - // in the event of a symbol change this will break since we'll be assigning the - // new symbol to the permtick which won't be known by the algorithm - Symbol = symbol, - TickType = TickType.Quote - }; - } + entry.LastQuoteTick = new Tick + { + // in the event of a symbol change this will break since we'll be assigning the + // new symbol to the permtick which won't be known by the algorithm + Symbol = symbol, + TickType = TickType.Quote + }; + } - // set the last bid price - entry.LastQuoteTick.BidPrice = price; - break; + // set the last bid price + entry.LastQuoteTick.BidPrice = price; + break; - case IBApi.TickType.ASK: - case IBApi.TickType.DELAYED_ASK: + case IBApi.TickType.ASK: + case IBApi.TickType.DELAYED_ASK: - if (entry.LastQuoteTick == null) - { - entry.LastQuoteTick = new Tick + if (entry.LastQuoteTick == null) { - // in the event of a symbol change this will break since we'll be assigning the - // new symbol to the permtick which won't be known by the algorithm - Symbol = symbol, - TickType = TickType.Quote - }; - } + entry.LastQuoteTick = new Tick + { + // in the event of a symbol change this will break since we'll be assigning the + // new symbol to the permtick which won't be known by the algorithm + Symbol = symbol, + TickType = TickType.Quote + }; + } - // set the last ask price - entry.LastQuoteTick.AskPrice = price; - break; + // set the last ask price + entry.LastQuoteTick.AskPrice = price; + break; - case IBApi.TickType.LAST: - case IBApi.TickType.DELAYED_LAST: + case IBApi.TickType.LAST: + case IBApi.TickType.DELAYED_LAST: - if (entry.LastTradeTick == null) - { - entry.LastTradeTick = new Tick + if (entry.LastTradeTick == null) { - // in the event of a symbol change this will break since we'll be assigning the - // new symbol to the permtick which won't be known by the algorithm - Symbol = symbol, - TickType = TickType.Trade - }; - } + entry.LastTradeTick = new Tick + { + // in the event of a symbol change this will break since we'll be assigning the + // new symbol to the permtick which won't be known by the algorithm + Symbol = symbol, + TickType = TickType.Trade + }; + } - // set the last traded price - entry.LastTradeTick.Value = price; - break; + // set the last traded price + entry.LastTradeTick.Value = price; + break; - default: - return; + default: + return; + } } } private void HandleTickSize(object sender, IB.TickSizeEventArgs e) { - SubscriptionEntry entry; - if (!_subscribedTickers.TryGetValue(e.TickerId, out entry)) + if (!_subscribedTickers.TryGetValue(e.TickerId, out var entries)) { return; } - var symbol = entry.Symbol; + foreach (var entry in entries) + { + var symbol = entry.Symbol; - // negative size (-1) means no quantity available, normalize to zero - var quantity = e.Size < 0 ? 0 : e.Size; + // negative size (-1) means no quantity available, normalize to zero + var quantity = e.Size < 0 ? 0 : e.Size; - if (quantity == decimal.MaxValue) - { - // we've seen this with SPX index bid size, not valid, expected for indexes - quantity = 0; - } + if (quantity == decimal.MaxValue) + { + // we've seen this with SPX index bid size, not valid, expected for indexes + quantity = 0; + } - Tick tick; - switch (e.Field) - { - case IBApi.TickType.BID_SIZE: - case IBApi.TickType.DELAYED_BID_SIZE: + Tick tick; + switch (e.Field) + { + case IBApi.TickType.BID_SIZE: + case IBApi.TickType.DELAYED_BID_SIZE: - tick = entry.LastQuoteTick; + tick = entry.LastQuoteTick; - if (tick == null) - { - // tick size message must be preceded by a tick price message - return; - } + if (tick == null) + { + // tick size message must be preceded by a tick price message + return; + } - tick.BidSize = quantity; + tick.BidSize = quantity; - if (tick.BidPrice == 0) - { - // no bid price, do not emit tick - return; - } + if (tick.BidPrice == 0) + { + // no bid price, do not emit tick + return; + } - if (tick.BidPrice > 0 && tick.AskPrice > 0 && tick.BidPrice >= tick.AskPrice) - { - // new bid price jumped at or above previous ask price, wait for new ask price - return; - } + if (tick.BidPrice > 0 && tick.AskPrice > 0 && tick.BidPrice >= tick.AskPrice) + { + // new bid price jumped at or above previous ask price, wait for new ask price + return; + } - if (tick.AskPrice == 0) - { - // we have a bid price but no ask price, use bid price as value - tick.Value = tick.BidPrice; - } - else - { - // we have both bid price and ask price, use mid price as value - tick.Value = (tick.BidPrice + tick.AskPrice) / 2; - } - break; + if (tick.AskPrice == 0) + { + // we have a bid price but no ask price, use bid price as value + tick.Value = tick.BidPrice; + } + else + { + // we have both bid price and ask price, use mid price as value + tick.Value = (tick.BidPrice + tick.AskPrice) / 2; + } + break; - case IBApi.TickType.ASK_SIZE: - case IBApi.TickType.DELAYED_ASK_SIZE: + case IBApi.TickType.ASK_SIZE: + case IBApi.TickType.DELAYED_ASK_SIZE: - tick = entry.LastQuoteTick; + tick = entry.LastQuoteTick; - if (tick == null) - { - // tick size message must be preceded by a tick price message - return; - } + if (tick == null) + { + // tick size message must be preceded by a tick price message + return; + } - tick.AskSize = quantity; + tick.AskSize = quantity; - if (tick.AskPrice == 0) - { - // no ask price, do not emit tick - return; - } + if (tick.AskPrice == 0) + { + // no ask price, do not emit tick + return; + } - if (tick.BidPrice > 0 && tick.AskPrice > 0 && tick.BidPrice >= tick.AskPrice) - { - // new ask price jumped at or below previous bid price, wait for new bid price - return; - } + if (tick.BidPrice > 0 && tick.AskPrice > 0 && tick.BidPrice >= tick.AskPrice) + { + // new ask price jumped at or below previous bid price, wait for new bid price + return; + } - if (tick.BidPrice == 0) - { - // we have an ask price but no bid price, use ask price as value - tick.Value = tick.AskPrice; - } - else - { - // we have both bid price and ask price, use mid price as value - tick.Value = (tick.BidPrice + tick.AskPrice) / 2; - } - break; + if (tick.BidPrice == 0) + { + // we have an ask price but no bid price, use ask price as value + tick.Value = tick.AskPrice; + } + else + { + // we have both bid price and ask price, use mid price as value + tick.Value = (tick.BidPrice + tick.AskPrice) / 2; + } + break; - case IBApi.TickType.LAST_SIZE: - case IBApi.TickType.DELAYED_LAST_SIZE: + case IBApi.TickType.LAST_SIZE: + case IBApi.TickType.DELAYED_LAST_SIZE: - tick = entry.LastTradeTick; + tick = entry.LastTradeTick; - if (tick == null) - { - // tick size message must be preceded by a tick price message - return; - } + if (tick == null) + { + // tick size message must be preceded by a tick price message + return; + } - // set the traded quantity - tick.Quantity = quantity; - break; + // set the traded quantity + tick.Quantity = quantity; + break; - case IBApi.TickType.OPEN_INTEREST: - case IBApi.TickType.OPTION_CALL_OPEN_INTEREST: - case IBApi.TickType.OPTION_PUT_OPEN_INTEREST: + case IBApi.TickType.OPEN_INTEREST: + case IBApi.TickType.OPTION_CALL_OPEN_INTEREST: + case IBApi.TickType.OPTION_PUT_OPEN_INTEREST: - if (!symbol.ID.SecurityType.IsOption() && symbol.ID.SecurityType != SecurityType.Future) - { - return; - } + if (!symbol.ID.SecurityType.IsOption() && symbol.ID.SecurityType != SecurityType.Future) + { + return; + } - if (entry.LastOpenInterestTick == null) - { - entry.LastOpenInterestTick = new Tick { Symbol = symbol, TickType = TickType.OpenInterest }; - } + if (entry.LastOpenInterestTick == null) + { + entry.LastOpenInterestTick = new Tick { Symbol = symbol, TickType = TickType.OpenInterest }; + } - tick = entry.LastOpenInterestTick; + tick = entry.LastOpenInterestTick; - tick.Value = quantity; - break; + tick.Value = quantity; + break; - default: - return; - } + default: + return; + } - if (tick.IsValid()) - { - tick = new Tick(tick) + if (tick.IsValid()) { - Time = GetRealTimeTickTime(symbol) - }; + tick = new Tick(tick) + { + Time = GetRealTimeTickTime(symbol) + }; - _aggregator.Update(tick); + _aggregator.Update(tick); + } } } @@ -4867,7 +4966,7 @@ private void AddGuaranteedTag(IBApi.Order ibOrder, bool nonGuaranteed) private bool _maxSubscribedSymbolsReached = false; private readonly ConcurrentDictionary _subscribedSymbols = new ConcurrentDictionary(); - private readonly ConcurrentDictionary _subscribedTickers = new ConcurrentDictionary(); + private readonly ConcurrentDictionary> _subscribedTickers = new ConcurrentDictionary>(); private class SubscriptionEntry { From 5f99295723c1f8b6a1949763a8209b3ced1ecce3 Mon Sep 17 00:00:00 2001 From: Jhonathan Abreu Date: Fri, 15 Mar 2024 16:30:27 -0400 Subject: [PATCH 04/16] Add CFDs orders support --- .../InteractiveBrokersCfdOrderTests.cs | 223 ++++++++++++++++++ .../InteractiveBrokersBrokerage.cs | 20 +- .../InteractiveBrokersSymbolMapper.cs | 3 +- 3 files changed, 244 insertions(+), 2 deletions(-) create mode 100644 QuantConnect.InteractiveBrokersBrokerage.Tests/InteractiveBrokersCfdOrderTests.cs diff --git a/QuantConnect.InteractiveBrokersBrokerage.Tests/InteractiveBrokersCfdOrderTests.cs b/QuantConnect.InteractiveBrokersBrokerage.Tests/InteractiveBrokersCfdOrderTests.cs new file mode 100644 index 0000000..adb4ee3 --- /dev/null +++ b/QuantConnect.InteractiveBrokersBrokerage.Tests/InteractiveBrokersCfdOrderTests.cs @@ -0,0 +1,223 @@ +/* + * 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; + +namespace QuantConnect.Tests.Brokerages.InteractiveBrokers +{ + [TestFixture] + [Explicit("These tests require the IBGateway to be installed.")] + public class InteractiveBrokersCfdOrderTests : BrokerageTests + { + private static Symbol IndexCfdSymbol = Symbol.Create("SPX500USD", SecurityType.Cfd, Market.Oanda); + private static Symbol EquityCfdSymbol = Symbol.Create("AAPL", SecurityType.Cfd, Market.Oanda); + private static Symbol ForexCfdSymbol = Symbol.Create("AUDUSD", SecurityType.Cfd, Market.Oanda); + + protected override Symbol Symbol => IndexCfdSymbol; + protected override SecurityType SecurityType => SecurityType.Cfd; + + private static TestCaseData[] IndexCfdOrderTest() + { + return new[] + { + new TestCaseData(new MarketOrderTestParameters(IndexCfdSymbol)), + new TestCaseData(new LimitOrderTestParameters(IndexCfdSymbol, 10000m, 0.01m)), + new TestCaseData(new StopMarketOrderTestParameters(IndexCfdSymbol, 10000m, 0.01m)), + new TestCaseData(new StopLimitOrderTestParameters(IndexCfdSymbol, 10000m, 0.01m)), + new TestCaseData(new LimitIfTouchedOrderTestParameters(IndexCfdSymbol, 10000m, 0.01m)), + }; + } + + private static TestCaseData[] EquityCfdOrderTest() + { + return new[] + { + new TestCaseData(new MarketOrderTestParameters(EquityCfdSymbol)), + new TestCaseData(new LimitOrderTestParameters(EquityCfdSymbol, 10000m, 0.01m)), + new TestCaseData(new StopMarketOrderTestParameters(EquityCfdSymbol, 10000m, 0.01m)), + new TestCaseData(new StopLimitOrderTestParameters(EquityCfdSymbol, 10000m, 0.01m)), + new TestCaseData(new LimitIfTouchedOrderTestParameters(EquityCfdSymbol, 10000m, 0.01m)), + }; + } + + private static TestCaseData[] ForexCfdOrderTest() + { + return new[] + { + new TestCaseData(new MarketOrderTestParameters(ForexCfdSymbol)), + new TestCaseData(new LimitOrderTestParameters(ForexCfdSymbol, 10000m, 0.01m)), + new TestCaseData(new StopMarketOrderTestParameters(ForexCfdSymbol, 10000m, 0.01m)), + new TestCaseData(new StopLimitOrderTestParameters(ForexCfdSymbol, 10000m, 0.01m)), + new TestCaseData(new LimitIfTouchedOrderTestParameters(ForexCfdSymbol, 10000m, 0.01m)), + }; + } + + [Test, TestCaseSource(nameof(IndexCfdOrderTest))] + public override void CancelOrders(OrderTestParameters parameters) + { + base.CancelOrders(parameters); + } + + [Test, TestCaseSource(nameof(IndexCfdOrderTest))] + public override void LongFromZero(OrderTestParameters parameters) + { + base.LongFromZero(parameters); + } + + [Test, TestCaseSource(nameof(IndexCfdOrderTest))] + public override void CloseFromLong(OrderTestParameters parameters) + { + base.CloseFromLong(parameters); + } + + [Test, TestCaseSource(nameof(IndexCfdOrderTest))] + public override void ShortFromZero(OrderTestParameters parameters) + { + base.ShortFromZero(parameters); + } + + [Test, TestCaseSource(nameof(IndexCfdOrderTest))] + public override void CloseFromShort(OrderTestParameters parameters) + { + base.CloseFromShort(parameters); + } + + [Test, TestCaseSource(nameof(IndexCfdOrderTest))] + public override void ShortFromLong(OrderTestParameters parameters) + { + base.ShortFromLong(parameters); + } + + [Test, TestCaseSource(nameof(IndexCfdOrderTest))] + public override void LongFromShort(OrderTestParameters parameters) + { + base.LongFromShort(parameters); + } + + [Test, TestCaseSource(nameof(EquityCfdOrderTest))] + public void CancelOrdersEquityCfd(OrderTestParameters parameters) + { + base.CancelOrders(parameters); + } + + [Test, TestCaseSource(nameof(EquityCfdOrderTest))] + public void LongFromZeroEquityCfd(OrderTestParameters parameters) + { + base.LongFromZero(parameters); + } + + [Test, TestCaseSource(nameof(EquityCfdOrderTest))] + public void CloseFromLongEquityCfd(OrderTestParameters parameters) + { + base.CloseFromLong(parameters); + } + + [Test, TestCaseSource(nameof(EquityCfdOrderTest))] + public void ShortFromZeroEquityCfd(OrderTestParameters parameters) + { + base.ShortFromZero(parameters); + } + + [Test, TestCaseSource(nameof(EquityCfdOrderTest))] + public void CloseFromShortEquityCfd(OrderTestParameters parameters) + { + base.CloseFromShort(parameters); + } + + [Test, TestCaseSource(nameof(EquityCfdOrderTest))] + public void ShortFromLongEquityCfd(OrderTestParameters parameters) + { + base.ShortFromLong(parameters); + } + + [Test, TestCaseSource(nameof(EquityCfdOrderTest))] + public void LongFromShortEquityCfd(OrderTestParameters parameters) + { + base.LongFromShort(parameters); + } + + [Test, TestCaseSource(nameof(ForexCfdOrderTest))] + public void CancelOrdersForexCfd(OrderTestParameters parameters) + { + base.CancelOrders(parameters); + } + + [Test, TestCaseSource(nameof(ForexCfdOrderTest))] + public void LongFromZeroForexCfd(OrderTestParameters parameters) + { + base.LongFromZero(parameters); + } + + [Test, TestCaseSource(nameof(ForexCfdOrderTest))] + public void CloseFromLongForexCfd(OrderTestParameters parameters) + { + base.CloseFromLong(parameters); + } + + [Test, TestCaseSource(nameof(ForexCfdOrderTest))] + public void ShortFromZeroForexCfd(OrderTestParameters parameters) + { + base.ShortFromZero(parameters); + } + + [Test, TestCaseSource(nameof(ForexCfdOrderTest))] + public void CloseFromShortForexCfd(OrderTestParameters parameters) + { + base.CloseFromShort(parameters); + } + + [Test, TestCaseSource(nameof(ForexCfdOrderTest))] + public void ShortFromLongForexCfd(OrderTestParameters parameters) + { + base.ShortFromLong(parameters); + } + + [Test, TestCaseSource(nameof(ForexCfdOrderTest))] + public void LongFromShortForexCfd(OrderTestParameters parameters) + { + base.LongFromShort(parameters); + } + + // TODO: Add tests to get holdings after placing orders + + 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 554261e..4d35a16 100644 --- a/QuantConnect.InteractiveBrokersBrokerage/InteractiveBrokersBrokerage.cs +++ b/QuantConnect.InteractiveBrokersBrokerage/InteractiveBrokersBrokerage.cs @@ -3354,7 +3354,25 @@ private Symbol MapSymbol(Contract contract) try { var securityType = ConvertSecurityType(contract); - var ibSymbol = securityType == SecurityType.Forex ? contract.Symbol + contract.Currency : contract.Symbol; + + var ibSymbol = contract.Symbol; + if (securityType == SecurityType.Forex) + { + ibSymbol += contract.Currency; + } + else if (securityType == SecurityType.Cfd) + { + var potentialCurrencyPair = contract.TradingClass.Replace(".", ""); + var potentialForexSymbol = Symbol.Create(potentialCurrencyPair, SecurityType.Forex, Market.Oanda); + if (CurrencyPairUtil.IsDecomposable(potentialForexSymbol)) + { + CurrencyPairUtil.DecomposeCurrencyPair(potentialForexSymbol, out var baseCurrency, out var quoteCurrency); + if (baseCurrency == contract.Symbol && quoteCurrency == contract.Currency) + { + ibSymbol += contract.Currency; + } + } + } var market = InteractiveBrokersBrokerageModel.DefaultMarketMap[securityType]; var isFutureOption = contract.SecType == IB.SecurityType.FutureOption; diff --git a/QuantConnect.InteractiveBrokersBrokerage/InteractiveBrokersSymbolMapper.cs b/QuantConnect.InteractiveBrokersBrokerage/InteractiveBrokersSymbolMapper.cs index ebea31a..e8fdd81 100644 --- a/QuantConnect.InteractiveBrokersBrokerage/InteractiveBrokersSymbolMapper.cs +++ b/QuantConnect.InteractiveBrokersBrokerage/InteractiveBrokersSymbolMapper.cs @@ -157,7 +157,8 @@ public string GetBrokerageSymbol(Symbol symbol) securityType != SecurityType.Option && securityType != SecurityType.IndexOption && securityType != SecurityType.Future && - securityType != SecurityType.FutureOption) + securityType != SecurityType.FutureOption && + securityType != SecurityType.Cfd) throw new ArgumentException("Invalid security type: " + securityType); try From 6ea46ed0334a99bbefebda7d2326b3dbb981bb15 Mon Sep 17 00:00:00 2001 From: Jhonathan Abreu Date: Mon, 18 Mar 2024 12:39:17 -0400 Subject: [PATCH 05/16] Minor change --- .../InteractiveBrokersCfdOrderTests.cs | 14 +++++++------- .../InteractiveBrokersBrokerage.cs | 7 ++++--- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/QuantConnect.InteractiveBrokersBrokerage.Tests/InteractiveBrokersCfdOrderTests.cs b/QuantConnect.InteractiveBrokersBrokerage.Tests/InteractiveBrokersCfdOrderTests.cs index adb4ee3..6eae80d 100644 --- a/QuantConnect.InteractiveBrokersBrokerage.Tests/InteractiveBrokersCfdOrderTests.cs +++ b/QuantConnect.InteractiveBrokersBrokerage.Tests/InteractiveBrokersCfdOrderTests.cs @@ -69,43 +69,43 @@ private static TestCaseData[] ForexCfdOrderTest() } [Test, TestCaseSource(nameof(IndexCfdOrderTest))] - public override void CancelOrders(OrderTestParameters parameters) + public void CancelOrdersIndexCfd(OrderTestParameters parameters) { base.CancelOrders(parameters); } [Test, TestCaseSource(nameof(IndexCfdOrderTest))] - public override void LongFromZero(OrderTestParameters parameters) + public void LongFromZeroIndexCfd(OrderTestParameters parameters) { base.LongFromZero(parameters); } [Test, TestCaseSource(nameof(IndexCfdOrderTest))] - public override void CloseFromLong(OrderTestParameters parameters) + public void CloseFromLongIndexCfd(OrderTestParameters parameters) { base.CloseFromLong(parameters); } [Test, TestCaseSource(nameof(IndexCfdOrderTest))] - public override void ShortFromZero(OrderTestParameters parameters) + public void ShortFromZeroIndexCfd(OrderTestParameters parameters) { base.ShortFromZero(parameters); } [Test, TestCaseSource(nameof(IndexCfdOrderTest))] - public override void CloseFromShort(OrderTestParameters parameters) + public void CloseFromShortIndexCfd(OrderTestParameters parameters) { base.CloseFromShort(parameters); } [Test, TestCaseSource(nameof(IndexCfdOrderTest))] - public override void ShortFromLong(OrderTestParameters parameters) + public void ShortFromLongIndexCfd(OrderTestParameters parameters) { base.ShortFromLong(parameters); } [Test, TestCaseSource(nameof(IndexCfdOrderTest))] - public override void LongFromShort(OrderTestParameters parameters) + public void LongFromShortIndexCfd(OrderTestParameters parameters) { base.LongFromShort(parameters); } diff --git a/QuantConnect.InteractiveBrokersBrokerage/InteractiveBrokersBrokerage.cs b/QuantConnect.InteractiveBrokersBrokerage/InteractiveBrokersBrokerage.cs index 4d35a16..7229c5b 100644 --- a/QuantConnect.InteractiveBrokersBrokerage/InteractiveBrokersBrokerage.cs +++ b/QuantConnect.InteractiveBrokersBrokerage/InteractiveBrokersBrokerage.cs @@ -53,6 +53,7 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; using QuantConnect.Data.Auxiliary; +using QuantConnect.Securities.Forex; namespace QuantConnect.Brokerages.InteractiveBrokers { @@ -3362,11 +3363,11 @@ private Symbol MapSymbol(Contract contract) } else if (securityType == SecurityType.Cfd) { + // If this is a forex CFD, we need to compose the symbol like we do for forex var potentialCurrencyPair = contract.TradingClass.Replace(".", ""); - var potentialForexSymbol = Symbol.Create(potentialCurrencyPair, SecurityType.Forex, Market.Oanda); - if (CurrencyPairUtil.IsDecomposable(potentialForexSymbol)) + if (CurrencyPairUtil.IsForexDecomposable(potentialCurrencyPair)) { - CurrencyPairUtil.DecomposeCurrencyPair(potentialForexSymbol, out var baseCurrency, out var quoteCurrency); + Forex.DecomposeCurrencyPair(potentialCurrencyPair, out var baseCurrency, out var quoteCurrency); if (baseCurrency == contract.Symbol && quoteCurrency == contract.Currency) { ibSymbol += contract.Currency; From 48fbbd61d5c8696808df74cec7fcef65f4debee2 Mon Sep 17 00:00:00 2001 From: Jhonathan Abreu Date: Tue, 19 Mar 2024 12:51:19 -0400 Subject: [PATCH 06/16] Add CFD history support --- ...eractiveBrokersBrokerageAdditionalTests.cs | 57 ++++++++++++++++++- .../InteractiveBrokersBrokerage.cs | 26 ++++++++- 2 files changed, 80 insertions(+), 3 deletions(-) diff --git a/QuantConnect.InteractiveBrokersBrokerage.Tests/InteractiveBrokersBrokerageAdditionalTests.cs b/QuantConnect.InteractiveBrokersBrokerage.Tests/InteractiveBrokersBrokerageAdditionalTests.cs index 16c1979..95e3a3b 100644 --- a/QuantConnect.InteractiveBrokersBrokerage.Tests/InteractiveBrokersBrokerageAdditionalTests.cs +++ b/QuantConnect.InteractiveBrokersBrokerage.Tests/InteractiveBrokersBrokerageAdditionalTests.cs @@ -754,6 +754,7 @@ public void GetHistoryDoesNotThrowError504WhenDisconnected() new TestCaseData(Symbol.CreateOption(Symbols.SPY, Market.India, OptionStyle.American, OptionRight.Call, 100m, new DateTime(2024, 12, 12)), Resolution.Daily, TickType.Trade), new TestCaseData(Symbol.CreateOption(Symbols.SPX, Market.India, OptionStyle.American, OptionRight.Call, 100m, new DateTime(2024, 12, 12)), Resolution.Daily, TickType.Trade), new TestCaseData(Symbol.Create("SPX", SecurityType.Index, Market.India), Resolution.Daily, TickType.Trade), + new TestCaseData(Symbol.Create("SPX500USD", SecurityType.Cfd, Market.FXCM), Resolution.Daily, TickType.Trade), // Unsupported resolution new TestCaseData(Symbols.SPY, Resolution.Tick, TickType.Trade), new TestCaseData(Symbols.SPY_C_192_Feb19_2016, Resolution.Tick, TickType.Trade), @@ -908,7 +909,7 @@ private List GetHistory( var request = new HistoryRequest( endTimeInExchangeTimeZone.ConvertToUtc(exchangeTimeZone).Subtract(historyTimeSpan), endTimeInExchangeTimeZone.ConvertToUtc(exchangeTimeZone), - typeof(TradeBar), + symbol.SecurityType != SecurityType.Cfd && symbol.SecurityType != SecurityType.Forex ? typeof(QuoteBar) : typeof(QuoteBar), symbol, resolution, SecurityExchangeHours.AlwaysOpen(exchangeTimeZone), @@ -917,7 +918,7 @@ private List GetHistory( includeExtendedMarketHours, false, DataNormalizationMode.Raw, - TickType.Trade); + symbol.SecurityType != SecurityType.Cfd && symbol.SecurityType != SecurityType.Forex ? TickType.Quote : TickType.Quote); var start = DateTime.UtcNow; var history = brokerage.GetHistory(request).ToList(); @@ -950,6 +951,13 @@ private static TestCaseData[] HistoryData() var optionSymbol = Symbol.CreateOption("AAPL", Market.USA, OptionStyle.American, OptionRight.Call, 145, new DateTime(2021, 8, 20)); var delistedEquity = Symbol.Create("AAA.1", SecurityType.Equity, Market.USA); + + var forexSymbol = Symbol.Create("EURUSD", SecurityType.Forex, Market.Oanda); + + var indexCfdSymbol = Symbol.Create("SPX500USD", SecurityType.Cfd, Market.Oanda); + var equityCfdSymbol = Symbol.Create("SPY", SecurityType.Cfd, Market.Oanda); + var forexCfdSymbol = Symbol.Create("EURUSD", SecurityType.Cfd, Market.Oanda); + return new[] { // 30 min RTH today + 60 min RTH yesterday @@ -983,9 +991,54 @@ private static TestCaseData[] HistoryData() new TestCaseData(optionSymbol, Resolution.Second, TimeZones.NewYork, TimeZones.NewYork, new DateTime(2021, 8, 6, 10, 0, 0), TimeSpan.FromHours(19), true, 5400), + new TestCaseData(forexSymbol, Resolution.Daily, TimeZones.NewYork, TimeZones.NewYork, + new DateTime(2023, 12, 21, 0, 0, 0), TimeSpan.FromDays(10), true, 8), + // delisted asset new TestCaseData(delistedEquity, Resolution.Second, TimeZones.NewYork, TimeZones.NewYork, new DateTime(2021, 8, 6, 10, 0, 0), TimeSpan.FromHours(19), false, 0), + + // Index Cfd: + // daily + new TestCaseData(indexCfdSymbol, Resolution.Daily, TimeZones.NewYork, TimeZones.NewYork, + new DateTime(2023, 12, 21, 0, 0, 0), TimeSpan.FromDays(10), true, 7), + // hourly + new TestCaseData(indexCfdSymbol, Resolution.Hour, TimeZones.NewYork, TimeZones.NewYork, + new DateTime(2023, 12, 21, 0, 0, 0), TimeSpan.FromDays(5), false, 75), + // minute + new TestCaseData(indexCfdSymbol, Resolution.Minute, TimeZones.NewYork, TimeZones.NewYork, + new DateTime(2023, 12, 21, 0, 0, 0), TimeSpan.FromMinutes(60 * 8), false, 420), + // second + new TestCaseData(indexCfdSymbol, Resolution.Second, TimeZones.NewYork, TimeZones.NewYork, + new DateTime(2023, 12, 21, 22, 0, 0), TimeSpan.FromMinutes(60), false, 3600), + + // Equity Cfd: + // daily + new TestCaseData(equityCfdSymbol, Resolution.Daily, TimeZones.NewYork, TimeZones.NewYork, + new DateTime(2023, 12, 21, 0, 0, 0), TimeSpan.FromDays(10), true, 8), + // hourly + new TestCaseData(equityCfdSymbol, Resolution.Hour, TimeZones.NewYork, TimeZones.NewYork, + new DateTime(2023, 12, 21, 0, 0, 0), TimeSpan.FromDays(5), false, 48), + // minute + new TestCaseData(equityCfdSymbol, Resolution.Minute, TimeZones.NewYork, TimeZones.NewYork, + new DateTime(2023, 12, 21, 0, 0, 0), TimeSpan.FromMinutes(60 * 8), false, 240), + // second: only 1 RTH from 19 to 20 + new TestCaseData(equityCfdSymbol, Resolution.Second, TimeZones.NewYork, TimeZones.NewYork, + new DateTime(2023, 12, 21, 22, 0, 0), TimeSpan.FromMinutes(3 * 60), false, 3600), + + // Forex Cfd: + // daily + new TestCaseData(forexCfdSymbol, Resolution.Daily, TimeZones.NewYork, TimeZones.NewYork, + new DateTime(2023, 12, 21, 0, 0, 0), TimeSpan.FromDays(10), true, 8), + // hourly + new TestCaseData(forexCfdSymbol, Resolution.Hour, TimeZones.NewYork, TimeZones.NewYork, + new DateTime(2023, 12, 21, 0, 0, 0), TimeSpan.FromDays(5), false, 79), + // minute + new TestCaseData(forexCfdSymbol, Resolution.Minute, TimeZones.NewYork, TimeZones.NewYork, + new DateTime(2023, 12, 21, 0, 0, 0), TimeSpan.FromMinutes(60 * 8), false, 465), + // second + new TestCaseData(forexCfdSymbol, Resolution.Second, TimeZones.NewYork, TimeZones.NewYork, + new DateTime(2023, 12, 21, 22, 0, 0), TimeSpan.FromMinutes(60), false, 3600), }; } diff --git a/QuantConnect.InteractiveBrokersBrokerage/InteractiveBrokersBrokerage.cs b/QuantConnect.InteractiveBrokersBrokerage/InteractiveBrokersBrokerage.cs index 7229c5b..0378cab 100644 --- a/QuantConnect.InteractiveBrokersBrokerage/InteractiveBrokersBrokerage.cs +++ b/QuantConnect.InteractiveBrokersBrokerage/InteractiveBrokersBrokerage.cs @@ -1447,10 +1447,16 @@ private void IBPlaceOrder(Order order, bool needsNewId, string exchange = null) private static string GetUniqueKey(Contract contract) { + var leanSecurityType = ConvertSecurityType(contract); + if (leanSecurityType.IsOption()) + { // for IB trading class can be different depending on the contract flavor, e.g. index options SPX & SPXW return $"{contract.ToString().ToUpperInvariant()} {contract.LastTradeDateOrContractMonth.ToStringInvariant()} {contract.Strike.ToStringInvariant()} {contract.Right} {contract.TradingClass}"; } + return contract.ToString().ToUpperInvariant(); + } + /// /// Get Contract Description /// @@ -3209,7 +3215,7 @@ private static string ConvertSecurityType(SecurityType type) /// /// Maps SecurityType enum /// - private SecurityType ConvertSecurityType(Contract contract) + private static SecurityType ConvertSecurityType(Contract contract) { switch (contract.SecType) { @@ -4271,6 +4277,24 @@ public override IEnumerable GetHistory(HistoryRequest request) // preparing the data for IB request var contract = CreateContract(request.Symbol, includeExpired: true); + var contractDetails = GetContractDetails(contract, request.Symbol.Value); + if (contract.SecType == IB.SecurityType.ContractForDifference) + { + // IB does not have data for equity and forex CFDs, we need to use the underlying security + var underlyingSecurityType = contractDetails.UnderSecType switch + { + IB.SecurityType.Stock => SecurityType.Equity, + IB.SecurityType.Cash => SecurityType.Forex, + _ => (SecurityType?)null + }; + + if (underlyingSecurityType.HasValue) + { + var underlyingSymbol = Symbol.Create(request.Symbol.Value, underlyingSecurityType.Value, request.Symbol.ID.Market); + contract = CreateContract(underlyingSymbol, includeExpired: true); + } + } + var resolution = ConvertResolution(request.Resolution); var startTime = request.Resolution == Resolution.Daily ? request.StartTimeUtc.Date : request.StartTimeUtc; From 9f3afacf191eaad511b908ead4e0d8c8d2652a84 Mon Sep 17 00:00:00 2001 From: Jhonathan Abreu Date: Tue, 19 Mar 2024 15:10:31 -0400 Subject: [PATCH 07/16] Add metal cfds unit tests --- ...eractiveBrokersBrokerageAdditionalTests.cs | 26 +++++++ ...iveBrokersBrokerageDataQueueHandlerTest.cs | 2 + .../InteractiveBrokersCfdOrderTests.cs | 71 +++++++++++++++++++ 3 files changed, 99 insertions(+) diff --git a/QuantConnect.InteractiveBrokersBrokerage.Tests/InteractiveBrokersBrokerageAdditionalTests.cs b/QuantConnect.InteractiveBrokersBrokerage.Tests/InteractiveBrokersBrokerageAdditionalTests.cs index 95e3a3b..742daa9 100644 --- a/QuantConnect.InteractiveBrokersBrokerage.Tests/InteractiveBrokersBrokerageAdditionalTests.cs +++ b/QuantConnect.InteractiveBrokersBrokerage.Tests/InteractiveBrokersBrokerageAdditionalTests.cs @@ -957,6 +957,10 @@ private static TestCaseData[] HistoryData() var indexCfdSymbol = Symbol.Create("SPX500USD", SecurityType.Cfd, Market.Oanda); var equityCfdSymbol = Symbol.Create("SPY", SecurityType.Cfd, Market.Oanda); var forexCfdSymbol = Symbol.Create("EURUSD", SecurityType.Cfd, Market.Oanda); + // Londong Gold + var metalCfdSymbol1 = Symbol.Create("XAUUSD", SecurityType.Cfd, Market.Oanda); + // Londong Silver + var metalCfdSymbol2 = Symbol.Create("XAGUSD", SecurityType.Cfd, Market.Oanda); return new[] { @@ -1039,6 +1043,28 @@ private static TestCaseData[] HistoryData() // second new TestCaseData(forexCfdSymbol, Resolution.Second, TimeZones.NewYork, TimeZones.NewYork, new DateTime(2023, 12, 21, 22, 0, 0), TimeSpan.FromMinutes(60), false, 3600), + + // Metal Cfd: + // daily + new TestCaseData(metalCfdSymbol1, Resolution.Daily, TimeZones.NewYork, TimeZones.NewYork, + new DateTime(2023, 12, 21, 0, 0, 0), TimeSpan.FromDays(10), true, 8), + new TestCaseData(metalCfdSymbol2, Resolution.Daily, TimeZones.NewYork, TimeZones.NewYork, + new DateTime(2023, 12, 21, 0, 0, 0), TimeSpan.FromDays(10), true, 8), + // hourly + new TestCaseData(metalCfdSymbol1, Resolution.Hour, TimeZones.NewYork, TimeZones.NewYork, + new DateTime(2023, 12, 21, 0, 0, 0), TimeSpan.FromDays(5), false, 75), + new TestCaseData(metalCfdSymbol2, Resolution.Hour, TimeZones.NewYork, TimeZones.NewYork, + new DateTime(2023, 12, 21, 0, 0, 0), TimeSpan.FromDays(5), false, 75), + // minute + new TestCaseData(metalCfdSymbol1, Resolution.Minute, TimeZones.NewYork, TimeZones.NewYork, + new DateTime(2023, 12, 21, 0, 0, 0), TimeSpan.FromMinutes(60 * 8), false, 420), + new TestCaseData(metalCfdSymbol2, Resolution.Minute, TimeZones.NewYork, TimeZones.NewYork, + new DateTime(2023, 12, 21, 0, 0, 0), TimeSpan.FromMinutes(60 * 8), false, 420), + // second + new TestCaseData(metalCfdSymbol1, Resolution.Second, TimeZones.NewYork, TimeZones.NewYork, + new DateTime(2023, 12, 21, 22, 0, 0), TimeSpan.FromMinutes(60), false, 3600), + new TestCaseData(metalCfdSymbol2, Resolution.Second, TimeZones.NewYork, TimeZones.NewYork, + new DateTime(2023, 12, 21, 22, 0, 0), TimeSpan.FromMinutes(60), false, 3600), }; } diff --git a/QuantConnect.InteractiveBrokersBrokerage.Tests/InteractiveBrokersBrokerageDataQueueHandlerTest.cs b/QuantConnect.InteractiveBrokersBrokerage.Tests/InteractiveBrokersBrokerageDataQueueHandlerTest.cs index 81abc06..bdce2a7 100644 --- a/QuantConnect.InteractiveBrokersBrokerage.Tests/InteractiveBrokersBrokerageDataQueueHandlerTest.cs +++ b/QuantConnect.InteractiveBrokersBrokerage.Tests/InteractiveBrokersBrokerageDataQueueHandlerTest.cs @@ -150,12 +150,14 @@ private static TestCaseData[] GetCFDSubscriptionTestCases() var equityCfds = new[] { "AAPL", "SPY", "GOOG" }; var indexCfds = new[] { "SPX500USD", "AU200AUD", "US30USD", "NAS100USD", "UK100GBP", "DE30EUR", "FR40EUR", "HK50HKD", "JP225" }; var forexCfds = new[] { "AUDUSD", "NZDUSD", "USDCAD", "USDCHF" }; + var metalCfds = new[] { "XAUUSD", "XAGUSD" }; return baseTestCases.SelectMany(testCase => new[] { new TestCaseData(equityCfds, testCase.TickType, testCase.Resolution), new TestCaseData(indexCfds, testCase.TickType, testCase.Resolution), new TestCaseData(forexCfds, testCase.TickType, testCase.Resolution), + new TestCaseData(metalCfds, testCase.TickType, testCase.Resolution), }).ToArray(); } diff --git a/QuantConnect.InteractiveBrokersBrokerage.Tests/InteractiveBrokersCfdOrderTests.cs b/QuantConnect.InteractiveBrokersBrokerage.Tests/InteractiveBrokersCfdOrderTests.cs index 6eae80d..bf6c20d 100644 --- a/QuantConnect.InteractiveBrokersBrokerage.Tests/InteractiveBrokersCfdOrderTests.cs +++ b/QuantConnect.InteractiveBrokersBrokerage.Tests/InteractiveBrokersCfdOrderTests.cs @@ -28,6 +28,7 @@ public class InteractiveBrokersCfdOrderTests : BrokerageTests private static Symbol IndexCfdSymbol = Symbol.Create("SPX500USD", SecurityType.Cfd, Market.Oanda); private static Symbol EquityCfdSymbol = Symbol.Create("AAPL", SecurityType.Cfd, Market.Oanda); private static Symbol ForexCfdSymbol = Symbol.Create("AUDUSD", SecurityType.Cfd, Market.Oanda); + private static Symbol MetalCfdSymbol = Symbol.Create("XAUUSD", SecurityType.Cfd, Market.Oanda); protected override Symbol Symbol => IndexCfdSymbol; protected override SecurityType SecurityType => SecurityType.Cfd; @@ -68,6 +69,20 @@ private static TestCaseData[] ForexCfdOrderTest() }; } + private static TestCaseData[] MetalCfdOrderTest() + { + return new[] + { + new TestCaseData(new MarketOrderTestParameters(MetalCfdSymbol)), + new TestCaseData(new LimitOrderTestParameters(MetalCfdSymbol, 10000m, 0.01m)), + new TestCaseData(new StopMarketOrderTestParameters(MetalCfdSymbol, 10000m, 0.01m)), + new TestCaseData(new StopLimitOrderTestParameters(MetalCfdSymbol, 10000m, 0.01m)), + new TestCaseData(new LimitIfTouchedOrderTestParameters(MetalCfdSymbol, 10000m, 0.01m)), + }; + } + + #region Index CFDs + [Test, TestCaseSource(nameof(IndexCfdOrderTest))] public void CancelOrdersIndexCfd(OrderTestParameters parameters) { @@ -110,6 +125,10 @@ public void LongFromShortIndexCfd(OrderTestParameters parameters) base.LongFromShort(parameters); } + #endregion + + #region Equity CFDs + [Test, TestCaseSource(nameof(EquityCfdOrderTest))] public void CancelOrdersEquityCfd(OrderTestParameters parameters) { @@ -152,6 +171,10 @@ public void LongFromShortEquityCfd(OrderTestParameters parameters) base.LongFromShort(parameters); } + #endregion + + #region Forex CFDs + [Test, TestCaseSource(nameof(ForexCfdOrderTest))] public void CancelOrdersForexCfd(OrderTestParameters parameters) { @@ -194,6 +217,54 @@ public void LongFromShortForexCfd(OrderTestParameters parameters) base.LongFromShort(parameters); } + #endregion + + #region Metal CFDs + + [Test, TestCaseSource(nameof(MetalCfdOrderTest))] + public void CancelOrdersMetalCfd(OrderTestParameters parameters) + { + base.CancelOrders(parameters); + } + + [Test, TestCaseSource(nameof(MetalCfdOrderTest))] + public void LongFromZeroMetalCfd(OrderTestParameters parameters) + { + base.LongFromZero(parameters); + } + + [Test, TestCaseSource(nameof(MetalCfdOrderTest))] + public void CloseFromLongMetalCfd(OrderTestParameters parameters) + { + base.CloseFromLong(parameters); + } + + [Test, TestCaseSource(nameof(MetalCfdOrderTest))] + public void ShortFromZeroMetalCfd(OrderTestParameters parameters) + { + base.ShortFromZero(parameters); + } + + [Test, TestCaseSource(nameof(MetalCfdOrderTest))] + public void CloseFromShortMetalCfd(OrderTestParameters parameters) + { + base.CloseFromShort(parameters); + } + + [Test, TestCaseSource(nameof(MetalCfdOrderTest))] + public void ShortFromLongMetalCfd(OrderTestParameters parameters) + { + base.ShortFromLong(parameters); + } + + [Test, TestCaseSource(nameof(MetalCfdOrderTest))] + public void LongFromShortMetalCfd(OrderTestParameters parameters) + { + base.LongFromShort(parameters); + } + + #endregion + // TODO: Add tests to get holdings after placing orders protected override bool IsAsync() From 10ef0cf61a1c3d4968724e7e708557bf3e008779 Mon Sep 17 00:00:00 2001 From: Jhonathan Abreu Date: Tue, 19 Mar 2024 16:15:43 -0400 Subject: [PATCH 08/16] Cleanup --- .../InteractiveBrokersBrokerage.cs | 46 ++++++++++--------- 1 file changed, 24 insertions(+), 22 deletions(-) diff --git a/QuantConnect.InteractiveBrokersBrokerage/InteractiveBrokersBrokerage.cs b/QuantConnect.InteractiveBrokersBrokerage/InteractiveBrokersBrokerage.cs index 0378cab..dda6cc6 100644 --- a/QuantConnect.InteractiveBrokersBrokerage/InteractiveBrokersBrokerage.cs +++ b/QuantConnect.InteractiveBrokersBrokerage/InteractiveBrokersBrokerage.cs @@ -195,9 +195,9 @@ public sealed class InteractiveBrokersBrokerage : Brokerage, IDataQueueHandler, // See https://interactivebrokers.github.io/tws-api/historical_limitations.html // Making more than 60 requests within any ten minute period will cause a pacing violation for Small Bars (30 secs or less) - private readonly RateGate _historyHighResolutionRateLimiter = new (58, TimeSpan.FromMinutes(10)); + private readonly RateGate _historyHighResolutionRateLimiter = new(58, TimeSpan.FromMinutes(10)); // The maximum number of simultaneous open historical data requests from the API is 50, we limit the count further so we can server them as best as possible - private readonly SemaphoreSlim _concurrentHistoryRequests = new (20); + private readonly SemaphoreSlim _concurrentHistoryRequests = new(20); // additional IB request information, will be matched with errors in the handler, for better error reporting private readonly ConcurrentDictionary _requestInformation = new(); @@ -1413,7 +1413,7 @@ private void IBPlaceOrder(Order order, bool needsNewId, string exchange = null) { if (noSubmissionOrderTypes) { - if(!_submissionOrdersWarningSent) + if (!_submissionOrdersWarningSent) { _submissionOrdersWarningSent = true; OnMessage(new BrokerageMessageEvent(BrokerageMessageType.Warning, @@ -1450,9 +1450,9 @@ private static string GetUniqueKey(Contract contract) var leanSecurityType = ConvertSecurityType(contract); if (leanSecurityType.IsOption()) { - // for IB trading class can be different depending on the contract flavor, e.g. index options SPX & SPXW - return $"{contract.ToString().ToUpperInvariant()} {contract.LastTradeDateOrContractMonth.ToStringInvariant()} {contract.Strike.ToStringInvariant()} {contract.Right} {contract.TradingClass}"; - } + // for IB trading class can be different depending on the contract flavor, e.g. index options SPX & SPXW + return $"{contract.ToString().ToUpperInvariant()} {contract.LastTradeDateOrContractMonth.ToStringInvariant()} {contract.Strike.ToStringInvariant()} {contract.Right} {contract.TradingClass}"; + } return contract.ToString().ToUpperInvariant(); } @@ -1830,7 +1830,7 @@ private void HandleError(object sender, IB.ErrorEventArgs e) MapFile mapFile = null; if (requestInfo.AssociatedSymbol.RequiresMapping()) { - var resolver = _mapFileProvider.Get(AuxiliaryDataKey.Create(requestInfo.AssociatedSymbol)); + var resolver = _mapFileProvider.Get(AuxiliaryDataKey.Create(requestInfo.AssociatedSymbol)); mapFile = resolver.ResolveMapFile(requestInfo.AssociatedSymbol); } var historicalLimitDate = requestInfo.AssociatedSymbol.GetDelistingDate(mapFile).AddDays(1) @@ -2185,7 +2185,7 @@ private void HandleExecutionDetails(object sender, IB.ExecutionDetailsEventArgs return; } - if(executionDetails.Contract.SecType == IB.SecurityType.Bag) + if (executionDetails.Contract.SecType == IB.SecurityType.Bag) { // for combo order we get an initial event but later we get a global event for each leg in the combo return; @@ -2326,7 +2326,7 @@ private bool CanEmitFill(Order order, Execution execution) private Order TryGetOrderForFilling(int orderId) { - if(_pendingGroupOrdersForFilling.TryGetValue(orderId, out var fillingParameters)) + if (_pendingGroupOrdersForFilling.TryGetValue(orderId, out var fillingParameters)) { return fillingParameters.First().Order; } @@ -2335,7 +2335,7 @@ private Order TryGetOrderForFilling(int orderId) private List RemoveCachedOrdersForFilling(Order order) { - if(order.GroupOrderManager == null) + if (order.GroupOrderManager == null) { // not a combo order _pendingGroupOrdersForFilling.TryGetValue(order.Id, out var details); @@ -2578,7 +2578,7 @@ private IBApi.Order ConvertOrder(List orders, Contract contract, int ibOr }); } } - else if(limitOrder != null) + else if (limitOrder != null) { ibOrder.LmtPrice = NormalizePriceToBrokerage(limitOrder.LimitPrice, contract, order.Symbol); } @@ -2611,7 +2611,7 @@ private IBApi.Order ConvertOrder(List orders, Contract contract, int ibOr ibOrder.LmtPrice = NormalizePriceToBrokerage(limitIfTouchedOrder.LimitPrice, contract, order.Symbol, minTick); ibOrder.AuxPrice = NormalizePriceToBrokerage(limitIfTouchedOrder.TriggerPrice, contract, order.Symbol, minTick); } - else if(comboLimitOrder != null) + else if (comboLimitOrder != null) { AddGuaranteedTag(ibOrder, false); var baseContract = CreateContract(order.Symbol, includeExpired: false); @@ -4149,7 +4149,7 @@ public IEnumerable LookupSymbols(Symbol symbol, bool includeExpired, str private static string GetContractMultiplier(decimal contractMultiplier) { - if(contractMultiplier >= 1) + if (contractMultiplier >= 1) { // IB doesn't like 5000.0 return Convert.ToInt32(contractMultiplier).ToStringInvariant(); @@ -4249,7 +4249,7 @@ public override IEnumerable GetHistory(HistoryRequest request) return Enumerable.Empty(); } } - else if(request.Symbol.ID.SecurityType == SecurityType.Equity) + else if (request.Symbol.ID.SecurityType == SecurityType.Equity) { var localNow = DateTime.UtcNow.ConvertFromUtc(request.ExchangeHours.TimeZone); var resolver = _mapFileProvider.Get(AuxiliaryDataKey.Create(request.Symbol)); @@ -4461,6 +4461,8 @@ private IEnumerable GetHistory( { if (args.Id == historicalTicker) { + Console.WriteLine($"History request error: {args.Code} {args.Message}..."); + if (args.Code == 162 && args.Message.Contains("pacing violation")) { // pacing violation happened @@ -4490,9 +4492,9 @@ private IEnumerable GetHistory( { waitResult = WaitHandle.WaitAny(new WaitHandle[] { dataDownloaded }, timeOut * 1000); - if(waitResult == WaitHandle.WaitTimeout) + if (waitResult == WaitHandle.WaitTimeout) { - if(Interlocked.Exchange(ref dataDownloadedCount, 0) != 0) + if (Interlocked.Exchange(ref dataDownloadedCount, 0) != 0) { // timeout but data is being downloaded, so we are good, let's wait again but clear the data download count waitResult = 0; @@ -4557,7 +4559,7 @@ private string GetSymbolExchange(SecurityType securityType, string market, strin // Futures options share the same market as the underlying Symbol case SecurityType.FutureOption: case SecurityType.Future: - if(_futuresExchanges.TryGetValue(market, out var result)) + if (_futuresExchanges.TryGetValue(market, out var result)) { return result; } @@ -4601,7 +4603,7 @@ private void CheckRateLimiting() private void CheckHighResolutionHistoryRateLimiting(Resolution resolution) { - if(resolution != Resolution.Tick && resolution != Resolution.Second) + if (resolution != Resolution.Tick && resolution != Resolution.Second) { return; } @@ -4757,7 +4759,7 @@ private void StartGatewayWeeklyRestartTask() if (restart) { - Log.Trace($"InteractiveBrokersBrokerage.StartGatewayWeeklyRestartTask(): triggering weekly restart manually"); + Log.Trace($"InteractiveBrokersBrokerage.StartGatewayWeeklyRestartTask(): triggering weekly restart manually"); try { @@ -4855,9 +4857,9 @@ private void OnIbAutomaterExited(object sender, ExitedEventArgs e) private TimeSpan GetRestartDelay() { // during weekends wait until one hour before FX market open before restarting IBAutomater - return _ibAutomater.IsWithinWeekendServerResetTimes() - ? GetNextWeekendReconnectionTimeUtc() - DateTime.UtcNow - : _defaultRestartDelay; + return _ibAutomater.IsWithinWeekendServerResetTimes() + ? GetNextWeekendReconnectionTimeUtc() - DateTime.UtcNow + : _defaultRestartDelay; } private TimeSpan GetWeeklyRestartDelay() From c3b87351df3db2389485fbbbe60ecdb53c69e43e Mon Sep 17 00:00:00 2001 From: Jhonathan Abreu Date: Tue, 19 Mar 2024 16:39:48 -0400 Subject: [PATCH 09/16] Minor fix --- .../InteractiveBrokersBrokerage.cs | 44 ++++++++++++------- 1 file changed, 28 insertions(+), 16 deletions(-) diff --git a/QuantConnect.InteractiveBrokersBrokerage/InteractiveBrokersBrokerage.cs b/QuantConnect.InteractiveBrokersBrokerage/InteractiveBrokersBrokerage.cs index dda6cc6..67e4385 100644 --- a/QuantConnect.InteractiveBrokersBrokerage/InteractiveBrokersBrokerage.cs +++ b/QuantConnect.InteractiveBrokersBrokerage/InteractiveBrokersBrokerage.cs @@ -1523,13 +1523,13 @@ private decimal GetMinTick(Contract contract, string ticker) /// /// The target contract /// The associated Lean ticker. Just used for logging, can be provided empty - private ContractDetails GetContractDetails(Contract contract, string ticker) + private ContractDetails GetContractDetails(Contract contract, string ticker, bool failIfNotFound = true) { if (_contractDetails.TryGetValue(GetUniqueKey(contract), out var details)) { return details; } - return GetContractDetailsImpl(contract, ticker); + return GetContractDetailsImpl(contract, ticker, failIfNotFound); } /// @@ -1537,7 +1537,7 @@ private ContractDetails GetContractDetails(Contract contract, string ticker) /// /// The target contract /// The associated Lean ticker. Just used for logging, can be provided empty - private ContractDetails GetContractDetailsImpl(Contract contract, string ticker) + private ContractDetails GetContractDetailsImpl(Contract contract, string ticker, bool failIfNotFound = true) { const int timeout = 60; // sec @@ -1550,7 +1550,7 @@ private ContractDetails GetContractDetailsImpl(Contract contract, string ticker) _requestInformation[requestId] = new RequestInformation { RequestId = requestId, - RequestType = RequestType.ContractDetails, + RequestType = failIfNotFound ? RequestType.ContractDetails : RequestType.SoftContractDetails, Message = $"[Id={requestId}] GetContractDetails: {ticker} ({contract})" }; @@ -1824,21 +1824,32 @@ private void HandleError(object sender, IB.ErrorEventArgs e) else if (errorCode == 200) { // No security definition has been found for the request - // This is a common error when requesting historical data for expired contracts, in which case can ignore it - if (requestInfo is not null && requestInfo.RequestType == RequestType.History) + if (requestInfo is not null) { - MapFile mapFile = null; - if (requestInfo.AssociatedSymbol.RequiresMapping()) + // This is a common error when requesting historical data for expired contracts, in which case can ignore it + if (requestInfo.RequestType == RequestType.History) { - var resolver = _mapFileProvider.Get(AuxiliaryDataKey.Create(requestInfo.AssociatedSymbol)); - mapFile = resolver.ResolveMapFile(requestInfo.AssociatedSymbol); - } - var historicalLimitDate = requestInfo.AssociatedSymbol.GetDelistingDate(mapFile).AddDays(1) - .ConvertToUtc(requestInfo.HistoryRequest.ExchangeHours.TimeZone); + MapFile mapFile = null; + if (requestInfo.AssociatedSymbol.RequiresMapping()) + { + var resolver = _mapFileProvider.Get(AuxiliaryDataKey.Create(requestInfo.AssociatedSymbol)); + mapFile = resolver.ResolveMapFile(requestInfo.AssociatedSymbol); + } + var historicalLimitDate = requestInfo.AssociatedSymbol.GetDelistingDate(mapFile).AddDays(1) + .ConvertToUtc(requestInfo.HistoryRequest.ExchangeHours.TimeZone); - if (DateTime.UtcNow.Date > historicalLimitDate) + if (DateTime.UtcNow.Date > historicalLimitDate) + { + Log.Trace($"InteractiveBrokersBrokerage.HandleError(): Expired contract historical data request, ignoring error. ErrorCode: {errorCode} - {errorMsg}"); + return; + } + } + // If the request is marked as a soft contract details reques, we won't exit if not found. + // This can happen when checking whether a cfd if a forex cfd to create a contract, + // the first contract details request might return this error if it's in fact a forex CFD + // since the whole symbol (e.g. EURUSD) will be used first. We can ignore it + else if (requestInfo.RequestType == RequestType.SoftContractDetails) { - Log.Trace($"InteractiveBrokersBrokerage.HandleError(): Expired contract historical data request, ignoring error. ErrorCode: {errorCode} - {errorMsg}"); return; } } @@ -2864,7 +2875,7 @@ private Contract CreateContract(Symbol symbol, bool includeExpired, List else if (symbol.ID.SecurityType == SecurityType.Cfd) { // Let's try getting the contract details in order to get the type of CFD (stock, index or forex) - var details = GetContractDetails(contract, symbol.Value); + var details = GetContractDetails(contract, symbol.Value, failIfNotFound: false); // if null, it might be a forex CFD, we need to split the symbol just like we do for forex if (details == null) @@ -5210,6 +5221,7 @@ private enum RequestType ContractDetails, History, Executions, + SoftContractDetails, // Don't fail if we can't find the contract } private class RequestInformation From c994799db0acaeb898c7a82fd676ded11d179e39 Mon Sep 17 00:00:00 2001 From: Jhonathan Abreu Date: Tue, 19 Mar 2024 16:49:36 -0400 Subject: [PATCH 10/16] Minor cleanup --- .../InteractiveBrokersBrokerage.cs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/QuantConnect.InteractiveBrokersBrokerage/InteractiveBrokersBrokerage.cs b/QuantConnect.InteractiveBrokersBrokerage/InteractiveBrokersBrokerage.cs index 67e4385..bb7426a 100644 --- a/QuantConnect.InteractiveBrokersBrokerage/InteractiveBrokersBrokerage.cs +++ b/QuantConnect.InteractiveBrokersBrokerage/InteractiveBrokersBrokerage.cs @@ -3644,9 +3644,6 @@ private bool Subscribe(IEnumerable symbols) }; requestData(underlyingContract); - - // TODO: What happens if the underlying is already subscribed? - // TODO: What happens if the underlying is subscribed after the CFD? } }; @@ -3663,8 +3660,7 @@ private bool Subscribe(IEnumerable symbols) Client.ReRouteMarketDataDepthRequest += handleRerouteEvent; Client.TickSize += handleData; - // TODO: What to do if timeout? - rerouted.Wait(TimeSpan.FromSeconds(5)); + rerouted.Wait(TimeSpan.FromSeconds(10)); Client.ReRouteMarketDataRequest -= handleRerouteEvent; Client.ReRouteMarketDataDepthRequest -= handleRerouteEvent; From 21fee4d87141e36cbb5ba0388078ca818a6caa7b Mon Sep 17 00:00:00 2001 From: Jhonathan Abreu Date: Wed, 20 Mar 2024 09:33:05 -0400 Subject: [PATCH 11/16] Not waiting for event in market data request re-routing --- ...eractiveBrokersBrokerageAdditionalTests.cs | 4 +- .../InteractiveBrokersBrokerage.cs | 96 +++++++------------ 2 files changed, 39 insertions(+), 61 deletions(-) diff --git a/QuantConnect.InteractiveBrokersBrokerage.Tests/InteractiveBrokersBrokerageAdditionalTests.cs b/QuantConnect.InteractiveBrokersBrokerage.Tests/InteractiveBrokersBrokerageAdditionalTests.cs index 742daa9..2a3e0d4 100644 --- a/QuantConnect.InteractiveBrokersBrokerage.Tests/InteractiveBrokersBrokerageAdditionalTests.cs +++ b/QuantConnect.InteractiveBrokersBrokerage.Tests/InteractiveBrokersBrokerageAdditionalTests.cs @@ -909,7 +909,7 @@ private List GetHistory( var request = new HistoryRequest( endTimeInExchangeTimeZone.ConvertToUtc(exchangeTimeZone).Subtract(historyTimeSpan), endTimeInExchangeTimeZone.ConvertToUtc(exchangeTimeZone), - symbol.SecurityType != SecurityType.Cfd && symbol.SecurityType != SecurityType.Forex ? typeof(QuoteBar) : typeof(QuoteBar), + symbol.SecurityType != SecurityType.Cfd && symbol.SecurityType != SecurityType.Forex ? typeof(TradeBar) : typeof(QuoteBar), symbol, resolution, SecurityExchangeHours.AlwaysOpen(exchangeTimeZone), @@ -918,7 +918,7 @@ private List GetHistory( includeExtendedMarketHours, false, DataNormalizationMode.Raw, - symbol.SecurityType != SecurityType.Cfd && symbol.SecurityType != SecurityType.Forex ? TickType.Quote : TickType.Quote); + symbol.SecurityType != SecurityType.Cfd && symbol.SecurityType != SecurityType.Forex ? TickType.Trade : TickType.Quote); var start = DateTime.UtcNow; var history = brokerage.GetHistory(request).ToList(); diff --git a/QuantConnect.InteractiveBrokersBrokerage/InteractiveBrokersBrokerage.cs b/QuantConnect.InteractiveBrokersBrokerage/InteractiveBrokersBrokerage.cs index bb7426a..3a84b84 100644 --- a/QuantConnect.InteractiveBrokersBrokerage/InteractiveBrokersBrokerage.cs +++ b/QuantConnect.InteractiveBrokersBrokerage/InteractiveBrokersBrokerage.cs @@ -1286,6 +1286,8 @@ private void Initialize( _client.TickPrice += HandleTickPrice; _client.TickSize += HandleTickSize; _client.CurrentTimeUtc += HandleBrokerTime; + _client.ReRouteMarketDataRequest += HandleMarketDataReRoute; + _client.ReRouteMarketDataDepthRequest += HandleMarketDataReRoute; // we need to wait until we receive the next valid id from the server _client.NextValidId += (sender, e) => @@ -3559,6 +3561,40 @@ public IEnumerator Subscribe(SubscriptionDataConfig dataConfig, EventH return enumerator; } + /// + /// Submits a market data request (a subscription) for a given contract to IB. + /// + private void RequestMarketData(Contract contract, int requestId) + { + if (_enableDelayedStreamingData) + { + // Switch to delayed market data if the user does not have the necessary real time data subscription. + // If live data is available, it will always be returned instead of delayed data. + Client.ClientSocket.reqMarketDataType(3); + } + + // we would like to receive OI (101) + Client.ClientSocket.reqMktData(requestId, contract, "101", false, false, new List()); + } + + /// + /// Handles the re-rout market data request event issued by the IB server + /// + private void HandleMarketDataReRoute(object sender, IB.RerouteMarketDataRequestEventArgs e) + { + var requestInformation = _requestInformation.GetValueOrDefault(e.RequestId); + Log.Trace($"InteractiveBrokersBrokerage.Subscribe(): Re-routing {requestInformation?.AssociatedSymbol} CFD data request to underlying"); + + // re-route the request to the underlying + var underlyingContract = new Contract + { + ConId = e.ContractId, + Exchange = e.UnderlyingPrimaryExchange, + }; + + RequestMarketData(underlyingContract, e.RequestId); + } + /// /// Adds the specified symbols to the subscription /// @@ -3607,65 +3643,7 @@ private bool Subscribe(IEnumerable symbols) // track subscription time for minimum delay in unsubscribe _subscriptionTimes[id] = DateTime.UtcNow; - var requestData = (Contract contract) => - { - if (_enableDelayedStreamingData) - { - // Switch to delayed market data if the user does not have the necessary real time data subscription. - // If live data is available, it will always be returned instead of delayed data. - Client.ClientSocket.reqMarketDataType(3); - } - - // we would like to receive OI (101) - Client.ClientSocket.reqMktData(id, contract, "101", false, false, new List()); - }; - - requestData(contract); - - if (symbol.ID.SecurityType == SecurityType.Cfd) - { - // we need to listen for market data request re-routings since IB does not have data for Equity and Forex CFDs - // Reference: https://ibkrcampus.com/ibkr-api-page/trader-workstation-api/#re-route-cfds - - var rerouted = new ManualResetEventSlim(false); - - EventHandler handleRerouteEvent = (_, e) => - { - if (e.RequestId == id) - { - Log.Trace($"InteractiveBrokersBrokerage.Subscribe(): Re-routing {symbol} CFD data request to underlying"); - rerouted.Set(); - - // re-route the request to the underlying - var underlyingContract = new Contract - { - ConId = e.ContractId, - Exchange = e.UnderlyingPrimaryExchange, - }; - - requestData(underlyingContract); - } - }; - - EventHandler handleData = (_, e) => - { - if (e.TickerId == id) - { - // received data, no need for re-routing - rerouted.Set(); - } - }; - - Client.ReRouteMarketDataRequest += handleRerouteEvent; - Client.ReRouteMarketDataDepthRequest += handleRerouteEvent; - Client.TickSize += handleData; - - rerouted.Wait(TimeSpan.FromSeconds(10)); - - Client.ReRouteMarketDataRequest -= handleRerouteEvent; - Client.ReRouteMarketDataDepthRequest -= handleRerouteEvent; - Client.TickSize -= handleData; - } + RequestMarketData(contract, id); _subscribedSymbols[symbol] = id; var subscriptionEntry = new SubscriptionEntry { Symbol = subscribeSymbol, PriceMagnifier = priceMagnifier }; From 0edc7b31506a2381ec155c8ce089abd77b65674e Mon Sep 17 00:00:00 2001 From: Jhonathan Abreu Date: Wed, 20 Mar 2024 11:51:52 -0400 Subject: [PATCH 12/16] Address peer review --- .../InteractiveBrokersBrokerageAdditionalTests.cs | 10 +++++----- ...ractiveBrokersBrokerageDataQueueHandlerTest.cs | 2 +- .../InteractiveBrokersCfdOrderTests.cs | 8 ++++---- .../InteractiveBrokersBrokerage.cs | 15 ++++++++------- 4 files changed, 18 insertions(+), 17 deletions(-) diff --git a/QuantConnect.InteractiveBrokersBrokerage.Tests/InteractiveBrokersBrokerageAdditionalTests.cs b/QuantConnect.InteractiveBrokersBrokerage.Tests/InteractiveBrokersBrokerageAdditionalTests.cs index 2a3e0d4..1f4e398 100644 --- a/QuantConnect.InteractiveBrokersBrokerage.Tests/InteractiveBrokersBrokerageAdditionalTests.cs +++ b/QuantConnect.InteractiveBrokersBrokerage.Tests/InteractiveBrokersBrokerageAdditionalTests.cs @@ -954,13 +954,13 @@ private static TestCaseData[] HistoryData() var forexSymbol = Symbol.Create("EURUSD", SecurityType.Forex, Market.Oanda); - var indexCfdSymbol = Symbol.Create("SPX500USD", SecurityType.Cfd, Market.Oanda); - var equityCfdSymbol = Symbol.Create("SPY", SecurityType.Cfd, Market.Oanda); - var forexCfdSymbol = Symbol.Create("EURUSD", SecurityType.Cfd, Market.Oanda); + var indexCfdSymbol = Symbol.Create("SPX500USD", SecurityType.Cfd, Market.USA); + var equityCfdSymbol = Symbol.Create("SPY", SecurityType.Cfd, Market.USA); + var forexCfdSymbol = Symbol.Create("EURUSD", SecurityType.Cfd, Market.USA); // Londong Gold - var metalCfdSymbol1 = Symbol.Create("XAUUSD", SecurityType.Cfd, Market.Oanda); + var metalCfdSymbol1 = Symbol.Create("XAUUSD", SecurityType.Cfd, Market.USA); // Londong Silver - var metalCfdSymbol2 = Symbol.Create("XAGUSD", SecurityType.Cfd, Market.Oanda); + var metalCfdSymbol2 = Symbol.Create("XAGUSD", SecurityType.Cfd, Market.USA); return new[] { diff --git a/QuantConnect.InteractiveBrokersBrokerage.Tests/InteractiveBrokersBrokerageDataQueueHandlerTest.cs b/QuantConnect.InteractiveBrokersBrokerage.Tests/InteractiveBrokersBrokerageDataQueueHandlerTest.cs index bdce2a7..fb9515a 100644 --- a/QuantConnect.InteractiveBrokersBrokerage.Tests/InteractiveBrokersBrokerageDataQueueHandlerTest.cs +++ b/QuantConnect.InteractiveBrokersBrokerage.Tests/InteractiveBrokersBrokerageDataQueueHandlerTest.cs @@ -177,7 +177,7 @@ public void CanSubscribeToCFD(IEnumerable tickers, TickType tickType, Re foreach (var ticker in tickers) { - var symbol = Symbol.Create(ticker, SecurityType.Cfd, Market.Oanda); + var symbol = Symbol.Create(ticker, SecurityType.Cfd, Market.USA); var config = resolution switch { Resolution.Tick => GetSubscriptionDataConfig(symbol, resolution), diff --git a/QuantConnect.InteractiveBrokersBrokerage.Tests/InteractiveBrokersCfdOrderTests.cs b/QuantConnect.InteractiveBrokersBrokerage.Tests/InteractiveBrokersCfdOrderTests.cs index bf6c20d..ac9f4c2 100644 --- a/QuantConnect.InteractiveBrokersBrokerage.Tests/InteractiveBrokersCfdOrderTests.cs +++ b/QuantConnect.InteractiveBrokersBrokerage.Tests/InteractiveBrokersCfdOrderTests.cs @@ -25,10 +25,10 @@ namespace QuantConnect.Tests.Brokerages.InteractiveBrokers [Explicit("These tests require the IBGateway to be installed.")] public class InteractiveBrokersCfdOrderTests : BrokerageTests { - private static Symbol IndexCfdSymbol = Symbol.Create("SPX500USD", SecurityType.Cfd, Market.Oanda); - private static Symbol EquityCfdSymbol = Symbol.Create("AAPL", SecurityType.Cfd, Market.Oanda); - private static Symbol ForexCfdSymbol = Symbol.Create("AUDUSD", SecurityType.Cfd, Market.Oanda); - private static Symbol MetalCfdSymbol = Symbol.Create("XAUUSD", SecurityType.Cfd, Market.Oanda); + private static Symbol IndexCfdSymbol = Symbol.Create("SPX500USD", SecurityType.Cfd, Market.USA); + private static Symbol EquityCfdSymbol = Symbol.Create("AAPL", SecurityType.Cfd, Market.USA); + private static Symbol ForexCfdSymbol = Symbol.Create("AUDUSD", SecurityType.Cfd, Market.USA); + private static Symbol MetalCfdSymbol = Symbol.Create("XAUUSD", SecurityType.Cfd, Market.USA); protected override Symbol Symbol => IndexCfdSymbol; protected override SecurityType SecurityType => SecurityType.Cfd; diff --git a/QuantConnect.InteractiveBrokersBrokerage/InteractiveBrokersBrokerage.cs b/QuantConnect.InteractiveBrokersBrokerage/InteractiveBrokersBrokerage.cs index 3a84b84..c2bf5c6 100644 --- a/QuantConnect.InteractiveBrokersBrokerage/InteractiveBrokersBrokerage.cs +++ b/QuantConnect.InteractiveBrokersBrokerage/InteractiveBrokersBrokerage.cs @@ -1450,13 +1450,16 @@ private void IBPlaceOrder(Order order, bool needsNewId, string exchange = null) private static string GetUniqueKey(Contract contract) { var leanSecurityType = ConvertSecurityType(contract); - if (leanSecurityType.IsOption()) + if (leanSecurityType == SecurityType.Equity || + leanSecurityType == SecurityType.Forex || + leanSecurityType == SecurityType.Cfd || + leanSecurityType == SecurityType.Index) { - // for IB trading class can be different depending on the contract flavor, e.g. index options SPX & SPXW - return $"{contract.ToString().ToUpperInvariant()} {contract.LastTradeDateOrContractMonth.ToStringInvariant()} {contract.Strike.ToStringInvariant()} {contract.Right} {contract.TradingClass}"; + return contract.ToString().ToUpperInvariant(); } - return contract.ToString().ToUpperInvariant(); + // for IB trading class can be different depending on the contract flavor, e.g. index options SPX & SPXW + return $"{contract.ToString().ToUpperInvariant()} {contract.LastTradeDateOrContractMonth.ToStringInvariant()} {contract.Strike.ToStringInvariant()} {contract.Right} {contract.TradingClass}"; } /// @@ -3789,7 +3792,7 @@ private static bool CanSubscribe(Symbol symbol) (securityType == SecurityType.Index && market == Market.USA) || (securityType == SecurityType.FutureOption) || (securityType == SecurityType.Future) || - (securityType == SecurityType.Cfd && market == Market.Oanda); + (securityType == SecurityType.Cfd && market == Market.USA); } /// @@ -4446,8 +4449,6 @@ private IEnumerable GetHistory( { if (args.Id == historicalTicker) { - Console.WriteLine($"History request error: {args.Code} {args.Message}..."); - if (args.Code == 162 && args.Message.Contains("pacing violation")) { // pacing violation happened From 7377e24b5184626b8a1d20e0d13c35d288abbfa4 Mon Sep 17 00:00:00 2001 From: Jhonathan Abreu Date: Wed, 20 Mar 2024 12:33:27 -0400 Subject: [PATCH 13/16] Minor changes and add unit tests --- ...iveBrokersBrokerageDataQueueHandlerTest.cs | 81 +++- .../InteractiveBrokersBrokerage.cs | 350 ++++++++---------- 2 files changed, 241 insertions(+), 190 deletions(-) diff --git a/QuantConnect.InteractiveBrokersBrokerage.Tests/InteractiveBrokersBrokerageDataQueueHandlerTest.cs b/QuantConnect.InteractiveBrokersBrokerage.Tests/InteractiveBrokersBrokerageDataQueueHandlerTest.cs index fb9515a..617d7d0 100644 --- a/QuantConnect.InteractiveBrokersBrokerage.Tests/InteractiveBrokersBrokerageDataQueueHandlerTest.cs +++ b/QuantConnect.InteractiveBrokersBrokerage.Tests/InteractiveBrokersBrokerageDataQueueHandlerTest.cs @@ -202,11 +202,88 @@ public void CanSubscribeToCFD(IEnumerable tickers, TickType tickType, Re cancelationToken.Cancel(); cancelationToken.Dispose(); - Console.WriteLine(string.Join(", ", symbolsWithData.Select(s => s.Value))); - Assert.IsTrue(tickers.Any(x => symbolsWithData.Any(symbol => symbol.Value == x))); } + private static TestCaseData[] GetCFDAndUnderlyingSubscriptionTestCases() + { + var baseTestCases = new[] + { + new { TickType = TickType.Trade, Resolution = Resolution.Tick }, + new { TickType = TickType.Quote, Resolution = Resolution.Tick }, + new { TickType = TickType.Quote, Resolution = Resolution.Second } + }; + + var equityCfd = "AAPL"; + var forexCfd = "AUDUSD"; + + return baseTestCases.SelectMany(testCase => new[] + { + new TestCaseData(equityCfd, SecurityType.Equity, Market.USA, testCase.TickType, testCase.Resolution, true), + new TestCaseData(equityCfd, SecurityType.Equity, Market.USA, testCase.TickType, testCase.Resolution, false), + new TestCaseData(forexCfd, SecurityType.Forex, Market.Oanda, testCase.TickType, testCase.Resolution, true), + new TestCaseData(forexCfd, SecurityType.Forex, Market.Oanda, testCase.TickType, testCase.Resolution, false), + }).ToArray(); + } + + [TestCaseSource(nameof(GetCFDAndUnderlyingSubscriptionTestCases))] + public void CanSubscribeToCFDAndUnderlying(string ticker, SecurityType underlyingSecurityType, string underlyingMarket, + TickType tickType, Resolution resolution, bool underlyingFirst) + { + // 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 cancelationToken = new CancellationTokenSource(); + + var symbolsWithData = new HashSet(); + var locker = new object(); + + var underlyingSymbol = Symbol.Create(ticker, underlyingSecurityType, underlyingMarket); + var cfdSymbol = Symbol.Create(ticker, SecurityType.Cfd, Market.USA); + + var underlyingConfig = resolution switch + { + Resolution.Tick => GetSubscriptionDataConfig(underlyingSymbol, resolution), + _ => tickType == TickType.Trade + ? GetSubscriptionDataConfig(underlyingSymbol, resolution) + : GetSubscriptionDataConfig(underlyingSymbol, resolution) + }; + var cfdConfig = resolution switch + { + Resolution.Tick => GetSubscriptionDataConfig(cfdSymbol, resolution), + _ => tickType == TickType.Trade + ? GetSubscriptionDataConfig(cfdSymbol, resolution) + : GetSubscriptionDataConfig(cfdSymbol, resolution) + }; + var configs = underlyingFirst + ? new[] { underlyingConfig, cfdConfig } + : new[] { cfdConfig, underlyingConfig }; + + foreach (var config in configs) + { + ProcessFeed( + ib.Subscribe(config, (s, e) => + { + lock (locker) + { + symbolsWithData.Add(((NewDataAvailableEventArgs)e).DataPoint.Symbol); + } + }), + cancelationToken, + (tick) => Log(tick)); + } + + Thread.Sleep(10 * 1000); + cancelationToken.Cancel(); + cancelationToken.Dispose(); + + Assert.IsTrue(symbolsWithData.Contains(cfdSymbol)); + Assert.IsTrue(symbolsWithData.Contains(underlyingSymbol)); + } + [Test] public void CannotSubscribeToCFDWithUnsupportedMarket() { diff --git a/QuantConnect.InteractiveBrokersBrokerage/InteractiveBrokersBrokerage.cs b/QuantConnect.InteractiveBrokersBrokerage/InteractiveBrokersBrokerage.cs index c2bf5c6..8dfcff8 100644 --- a/QuantConnect.InteractiveBrokersBrokerage/InteractiveBrokersBrokerage.cs +++ b/QuantConnect.InteractiveBrokersBrokerage/InteractiveBrokersBrokerage.cs @@ -3649,15 +3649,7 @@ private bool Subscribe(IEnumerable symbols) RequestMarketData(contract, id); _subscribedSymbols[symbol] = id; - var subscriptionEntry = new SubscriptionEntry { Symbol = subscribeSymbol, PriceMagnifier = priceMagnifier }; - lock (_subscribedTickers) - { - if (!_subscribedTickers.TryGetValue(id, out var subscriptionEntries)) - { - _subscribedTickers[id] = subscriptionEntries = new List(); - } - subscriptionEntries.Add(subscriptionEntry); - } + _subscribedTickers[id] = new SubscriptionEntry { Symbol = subscribeSymbol, PriceMagnifier = priceMagnifier }; Log.Trace($"InteractiveBrokersBrokerage.Subscribe(): Subscribe Processed: {symbol.Value} ({GetContractDescription(contract)}) # {id}. SubscribedSymbols.Count: {_subscribedSymbols.Count}"); } @@ -3737,22 +3729,8 @@ private bool Unsubscribe(IEnumerable symbols) Client.ClientSocket.cancelMktData(id); - lock (_subscribedTickers) - { - if (!_subscribedTickers.TryGetValue(id, out var subscriptionEntries)) - { - return false; - } - - var removed = subscriptionEntries.RemoveAll(x => x.Symbol == symbol); - - if (subscriptionEntries.Count == 0) - { - _subscribedTickers.Remove(id, out _); - } - - return removed > 0; - } + SubscriptionEntry entry; + return _subscribedTickers.TryRemove(id, out entry); } } } @@ -3818,224 +3796,220 @@ private void HandleTickPrice(object sender, IB.TickPriceEventArgs e) // tickPrice events are always followed by tickSize events, // so we save off the bid/ask/last prices and only emit ticks in the tickSize event handler. - if (!_subscribedTickers.TryGetValue(e.TickerId, out var entries)) + SubscriptionEntry entry; + if (!_subscribedTickers.TryGetValue(e.TickerId, out entry)) { return; } - foreach (var entry in entries) - { - var symbol = entry.Symbol; + var symbol = entry.Symbol; - // negative price (-1) means no price available, normalize to zero - var price = e.Price < 0 ? 0 : Convert.ToDecimal(e.Price) / entry.PriceMagnifier; + // negative price (-1) means no price available, normalize to zero + var price = e.Price < 0 ? 0 : Convert.ToDecimal(e.Price) / entry.PriceMagnifier; - switch (e.Field) - { - case IBApi.TickType.BID: - case IBApi.TickType.DELAYED_BID: + switch (e.Field) + { + case IBApi.TickType.BID: + case IBApi.TickType.DELAYED_BID: - if (entry.LastQuoteTick == null) + if (entry.LastQuoteTick == null) + { + entry.LastQuoteTick = new Tick { - entry.LastQuoteTick = new Tick - { - // in the event of a symbol change this will break since we'll be assigning the - // new symbol to the permtick which won't be known by the algorithm - Symbol = symbol, - TickType = TickType.Quote - }; - } + // in the event of a symbol change this will break since we'll be assigning the + // new symbol to the permtick which won't be known by the algorithm + Symbol = symbol, + TickType = TickType.Quote + }; + } - // set the last bid price - entry.LastQuoteTick.BidPrice = price; - break; + // set the last bid price + entry.LastQuoteTick.BidPrice = price; + break; - case IBApi.TickType.ASK: - case IBApi.TickType.DELAYED_ASK: + case IBApi.TickType.ASK: + case IBApi.TickType.DELAYED_ASK: - if (entry.LastQuoteTick == null) + if (entry.LastQuoteTick == null) + { + entry.LastQuoteTick = new Tick { - entry.LastQuoteTick = new Tick - { - // in the event of a symbol change this will break since we'll be assigning the - // new symbol to the permtick which won't be known by the algorithm - Symbol = symbol, - TickType = TickType.Quote - }; - } + // in the event of a symbol change this will break since we'll be assigning the + // new symbol to the permtick which won't be known by the algorithm + Symbol = symbol, + TickType = TickType.Quote + }; + } - // set the last ask price - entry.LastQuoteTick.AskPrice = price; - break; + // set the last ask price + entry.LastQuoteTick.AskPrice = price; + break; - case IBApi.TickType.LAST: - case IBApi.TickType.DELAYED_LAST: + case IBApi.TickType.LAST: + case IBApi.TickType.DELAYED_LAST: - if (entry.LastTradeTick == null) + if (entry.LastTradeTick == null) + { + entry.LastTradeTick = new Tick { - entry.LastTradeTick = new Tick - { - // in the event of a symbol change this will break since we'll be assigning the - // new symbol to the permtick which won't be known by the algorithm - Symbol = symbol, - TickType = TickType.Trade - }; - } + // in the event of a symbol change this will break since we'll be assigning the + // new symbol to the permtick which won't be known by the algorithm + Symbol = symbol, + TickType = TickType.Trade + }; + } - // set the last traded price - entry.LastTradeTick.Value = price; - break; + // set the last traded price + entry.LastTradeTick.Value = price; + break; - default: - return; - } + default: + return; } } private void HandleTickSize(object sender, IB.TickSizeEventArgs e) { - if (!_subscribedTickers.TryGetValue(e.TickerId, out var entries)) + SubscriptionEntry entry; + if (!_subscribedTickers.TryGetValue(e.TickerId, out entry)) { return; } - foreach (var entry in entries) - { - var symbol = entry.Symbol; + var symbol = entry.Symbol; - // negative size (-1) means no quantity available, normalize to zero - var quantity = e.Size < 0 ? 0 : e.Size; + // negative size (-1) means no quantity available, normalize to zero + var quantity = e.Size < 0 ? 0 : e.Size; - if (quantity == decimal.MaxValue) - { - // we've seen this with SPX index bid size, not valid, expected for indexes - quantity = 0; - } + if (quantity == decimal.MaxValue) + { + // we've seen this with SPX index bid size, not valid, expected for indexes + quantity = 0; + } - Tick tick; - switch (e.Field) - { - case IBApi.TickType.BID_SIZE: - case IBApi.TickType.DELAYED_BID_SIZE: + Tick tick; + switch (e.Field) + { + case IBApi.TickType.BID_SIZE: + case IBApi.TickType.DELAYED_BID_SIZE: - tick = entry.LastQuoteTick; + tick = entry.LastQuoteTick; - if (tick == null) - { - // tick size message must be preceded by a tick price message - return; - } + if (tick == null) + { + // tick size message must be preceded by a tick price message + return; + } - tick.BidSize = quantity; + tick.BidSize = quantity; - if (tick.BidPrice == 0) - { - // no bid price, do not emit tick - return; - } + if (tick.BidPrice == 0) + { + // no bid price, do not emit tick + return; + } - if (tick.BidPrice > 0 && tick.AskPrice > 0 && tick.BidPrice >= tick.AskPrice) - { - // new bid price jumped at or above previous ask price, wait for new ask price - return; - } + if (tick.BidPrice > 0 && tick.AskPrice > 0 && tick.BidPrice >= tick.AskPrice) + { + // new bid price jumped at or above previous ask price, wait for new ask price + return; + } - if (tick.AskPrice == 0) - { - // we have a bid price but no ask price, use bid price as value - tick.Value = tick.BidPrice; - } - else - { - // we have both bid price and ask price, use mid price as value - tick.Value = (tick.BidPrice + tick.AskPrice) / 2; - } - break; + if (tick.AskPrice == 0) + { + // we have a bid price but no ask price, use bid price as value + tick.Value = tick.BidPrice; + } + else + { + // we have both bid price and ask price, use mid price as value + tick.Value = (tick.BidPrice + tick.AskPrice) / 2; + } + break; - case IBApi.TickType.ASK_SIZE: - case IBApi.TickType.DELAYED_ASK_SIZE: + case IBApi.TickType.ASK_SIZE: + case IBApi.TickType.DELAYED_ASK_SIZE: - tick = entry.LastQuoteTick; + tick = entry.LastQuoteTick; - if (tick == null) - { - // tick size message must be preceded by a tick price message - return; - } + if (tick == null) + { + // tick size message must be preceded by a tick price message + return; + } - tick.AskSize = quantity; + tick.AskSize = quantity; - if (tick.AskPrice == 0) - { - // no ask price, do not emit tick - return; - } + if (tick.AskPrice == 0) + { + // no ask price, do not emit tick + return; + } - if (tick.BidPrice > 0 && tick.AskPrice > 0 && tick.BidPrice >= tick.AskPrice) - { - // new ask price jumped at or below previous bid price, wait for new bid price - return; - } + if (tick.BidPrice > 0 && tick.AskPrice > 0 && tick.BidPrice >= tick.AskPrice) + { + // new ask price jumped at or below previous bid price, wait for new bid price + return; + } - if (tick.BidPrice == 0) - { - // we have an ask price but no bid price, use ask price as value - tick.Value = tick.AskPrice; - } - else - { - // we have both bid price and ask price, use mid price as value - tick.Value = (tick.BidPrice + tick.AskPrice) / 2; - } - break; + if (tick.BidPrice == 0) + { + // we have an ask price but no bid price, use ask price as value + tick.Value = tick.AskPrice; + } + else + { + // we have both bid price and ask price, use mid price as value + tick.Value = (tick.BidPrice + tick.AskPrice) / 2; + } + break; - case IBApi.TickType.LAST_SIZE: - case IBApi.TickType.DELAYED_LAST_SIZE: + case IBApi.TickType.LAST_SIZE: + case IBApi.TickType.DELAYED_LAST_SIZE: - tick = entry.LastTradeTick; + tick = entry.LastTradeTick; - if (tick == null) - { - // tick size message must be preceded by a tick price message - return; - } + if (tick == null) + { + // tick size message must be preceded by a tick price message + return; + } - // set the traded quantity - tick.Quantity = quantity; - break; + // set the traded quantity + tick.Quantity = quantity; + break; - case IBApi.TickType.OPEN_INTEREST: - case IBApi.TickType.OPTION_CALL_OPEN_INTEREST: - case IBApi.TickType.OPTION_PUT_OPEN_INTEREST: + case IBApi.TickType.OPEN_INTEREST: + case IBApi.TickType.OPTION_CALL_OPEN_INTEREST: + case IBApi.TickType.OPTION_PUT_OPEN_INTEREST: - if (!symbol.ID.SecurityType.IsOption() && symbol.ID.SecurityType != SecurityType.Future) - { - return; - } + if (!symbol.ID.SecurityType.IsOption() && symbol.ID.SecurityType != SecurityType.Future) + { + return; + } - if (entry.LastOpenInterestTick == null) - { - entry.LastOpenInterestTick = new Tick { Symbol = symbol, TickType = TickType.OpenInterest }; - } + if (entry.LastOpenInterestTick == null) + { + entry.LastOpenInterestTick = new Tick { Symbol = symbol, TickType = TickType.OpenInterest }; + } - tick = entry.LastOpenInterestTick; + tick = entry.LastOpenInterestTick; - tick.Value = quantity; - break; + tick.Value = quantity; + break; - default: - return; - } + default: + return; + } - if (tick.IsValid()) + if (tick.IsValid()) + { + tick = new Tick(tick) { - tick = new Tick(tick) - { - Time = GetRealTimeTickTime(symbol) - }; + Time = GetRealTimeTickTime(symbol) + }; - _aggregator.Update(tick); - } + _aggregator.Update(tick); } } @@ -4997,7 +4971,7 @@ private void AddGuaranteedTag(IBApi.Order ibOrder, bool nonGuaranteed) private bool _maxSubscribedSymbolsReached = false; private readonly ConcurrentDictionary _subscribedSymbols = new ConcurrentDictionary(); - private readonly ConcurrentDictionary> _subscribedTickers = new ConcurrentDictionary>(); + private readonly ConcurrentDictionary _subscribedTickers = new ConcurrentDictionary(); private class SubscriptionEntry { From 7d6e3e99ea971c34e46701fcd4411391a72d7c67 Mon Sep 17 00:00:00 2001 From: Jhonathan Abreu Date: Mon, 25 Mar 2024 12:44:37 -0400 Subject: [PATCH 14/16] Use new Lean Martket InteractiveBrokers for Cfds --- .../InteractiveBrokersBrokerageAdditionalTests.cs | 10 +++++----- .../InteractiveBrokersBrokerageDataQueueHandlerTest.cs | 4 ++-- .../InteractiveBrokersCfdOrderTests.cs | 8 ++++---- .../InteractiveBrokersBrokerage.cs | 3 +-- 4 files changed, 12 insertions(+), 13 deletions(-) diff --git a/QuantConnect.InteractiveBrokersBrokerage.Tests/InteractiveBrokersBrokerageAdditionalTests.cs b/QuantConnect.InteractiveBrokersBrokerage.Tests/InteractiveBrokersBrokerageAdditionalTests.cs index 1f4e398..60806cd 100644 --- a/QuantConnect.InteractiveBrokersBrokerage.Tests/InteractiveBrokersBrokerageAdditionalTests.cs +++ b/QuantConnect.InteractiveBrokersBrokerage.Tests/InteractiveBrokersBrokerageAdditionalTests.cs @@ -954,13 +954,13 @@ private static TestCaseData[] HistoryData() var forexSymbol = Symbol.Create("EURUSD", SecurityType.Forex, Market.Oanda); - var indexCfdSymbol = Symbol.Create("SPX500USD", SecurityType.Cfd, Market.USA); - var equityCfdSymbol = Symbol.Create("SPY", SecurityType.Cfd, Market.USA); - var forexCfdSymbol = Symbol.Create("EURUSD", SecurityType.Cfd, Market.USA); + var indexCfdSymbol = Symbol.Create("SPX500USD", SecurityType.Cfd, Market.InteractiveBrokers); + var equityCfdSymbol = Symbol.Create("SPY", SecurityType.Cfd, Market.InteractiveBrokers); + var forexCfdSymbol = Symbol.Create("EURUSD", SecurityType.Cfd, Market.InteractiveBrokers); // Londong Gold - var metalCfdSymbol1 = Symbol.Create("XAUUSD", SecurityType.Cfd, Market.USA); + var metalCfdSymbol1 = Symbol.Create("XAUUSD", SecurityType.Cfd, Market.InteractiveBrokers); // Londong Silver - var metalCfdSymbol2 = Symbol.Create("XAGUSD", SecurityType.Cfd, Market.USA); + var metalCfdSymbol2 = Symbol.Create("XAGUSD", SecurityType.Cfd, Market.InteractiveBrokers); return new[] { diff --git a/QuantConnect.InteractiveBrokersBrokerage.Tests/InteractiveBrokersBrokerageDataQueueHandlerTest.cs b/QuantConnect.InteractiveBrokersBrokerage.Tests/InteractiveBrokersBrokerageDataQueueHandlerTest.cs index 617d7d0..d811bc6 100644 --- a/QuantConnect.InteractiveBrokersBrokerage.Tests/InteractiveBrokersBrokerageDataQueueHandlerTest.cs +++ b/QuantConnect.InteractiveBrokersBrokerage.Tests/InteractiveBrokersBrokerageDataQueueHandlerTest.cs @@ -177,7 +177,7 @@ public void CanSubscribeToCFD(IEnumerable tickers, TickType tickType, Re foreach (var ticker in tickers) { - var symbol = Symbol.Create(ticker, SecurityType.Cfd, Market.USA); + var symbol = Symbol.Create(ticker, SecurityType.Cfd, Market.InteractiveBrokers); var config = resolution switch { Resolution.Tick => GetSubscriptionDataConfig(symbol, resolution), @@ -242,7 +242,7 @@ public void CanSubscribeToCFDAndUnderlying(string ticker, SecurityType underlyin var locker = new object(); var underlyingSymbol = Symbol.Create(ticker, underlyingSecurityType, underlyingMarket); - var cfdSymbol = Symbol.Create(ticker, SecurityType.Cfd, Market.USA); + var cfdSymbol = Symbol.Create(ticker, SecurityType.Cfd, Market.InteractiveBrokers); var underlyingConfig = resolution switch { diff --git a/QuantConnect.InteractiveBrokersBrokerage.Tests/InteractiveBrokersCfdOrderTests.cs b/QuantConnect.InteractiveBrokersBrokerage.Tests/InteractiveBrokersCfdOrderTests.cs index ac9f4c2..05f3f68 100644 --- a/QuantConnect.InteractiveBrokersBrokerage.Tests/InteractiveBrokersCfdOrderTests.cs +++ b/QuantConnect.InteractiveBrokersBrokerage.Tests/InteractiveBrokersCfdOrderTests.cs @@ -25,10 +25,10 @@ namespace QuantConnect.Tests.Brokerages.InteractiveBrokers [Explicit("These tests require the IBGateway to be installed.")] public class InteractiveBrokersCfdOrderTests : BrokerageTests { - private static Symbol IndexCfdSymbol = Symbol.Create("SPX500USD", SecurityType.Cfd, Market.USA); - private static Symbol EquityCfdSymbol = Symbol.Create("AAPL", SecurityType.Cfd, Market.USA); - private static Symbol ForexCfdSymbol = Symbol.Create("AUDUSD", SecurityType.Cfd, Market.USA); - private static Symbol MetalCfdSymbol = Symbol.Create("XAUUSD", SecurityType.Cfd, Market.USA); + private static Symbol IndexCfdSymbol = Symbol.Create("SPX500USD", SecurityType.Cfd, Market.InteractiveBrokers); + private static Symbol EquityCfdSymbol = Symbol.Create("AAPL", SecurityType.Cfd, Market.InteractiveBrokers); + private static Symbol ForexCfdSymbol = Symbol.Create("AUDUSD", SecurityType.Cfd, Market.InteractiveBrokers); + private static Symbol MetalCfdSymbol = Symbol.Create("XAUUSD", SecurityType.Cfd, Market.InteractiveBrokers); protected override Symbol Symbol => IndexCfdSymbol; protected override SecurityType SecurityType => SecurityType.Cfd; diff --git a/QuantConnect.InteractiveBrokersBrokerage/InteractiveBrokersBrokerage.cs b/QuantConnect.InteractiveBrokersBrokerage/InteractiveBrokersBrokerage.cs index 8dfcff8..d02d684 100644 --- a/QuantConnect.InteractiveBrokersBrokerage/InteractiveBrokersBrokerage.cs +++ b/QuantConnect.InteractiveBrokersBrokerage/InteractiveBrokersBrokerage.cs @@ -1287,7 +1287,6 @@ private void Initialize( _client.TickSize += HandleTickSize; _client.CurrentTimeUtc += HandleBrokerTime; _client.ReRouteMarketDataRequest += HandleMarketDataReRoute; - _client.ReRouteMarketDataDepthRequest += HandleMarketDataReRoute; // we need to wait until we receive the next valid id from the server _client.NextValidId += (sender, e) => @@ -3770,7 +3769,7 @@ private static bool CanSubscribe(Symbol symbol) (securityType == SecurityType.Index && market == Market.USA) || (securityType == SecurityType.FutureOption) || (securityType == SecurityType.Future) || - (securityType == SecurityType.Cfd && market == Market.USA); + (securityType == SecurityType.Cfd && market == Market.InteractiveBrokers); } /// From beaa9fa9004abb27e4101318647bb5831fa5eb28 Mon Sep 17 00:00:00 2001 From: Jhonathan Abreu Date: Mon, 25 Mar 2024 18:21:11 -0400 Subject: [PATCH 15/16] Ditch index cfd symbol map from map file --- ...nteractiveBrokersBrokerageAdditionalTests.cs | 5 +++-- ...ctiveBrokersBrokerageDataQueueHandlerTest.cs | 4 ++-- .../InteractiveBrokers/IB-symbol-map.json | 17 +---------------- .../InteractiveBrokersBrokerage.cs | 11 +++++++++++ .../InteractiveBrokersSymbolMapper.cs | 4 ---- 5 files changed, 17 insertions(+), 24 deletions(-) diff --git a/QuantConnect.InteractiveBrokersBrokerage.Tests/InteractiveBrokersBrokerageAdditionalTests.cs b/QuantConnect.InteractiveBrokersBrokerage.Tests/InteractiveBrokersBrokerageAdditionalTests.cs index 60806cd..e5ae93b 100644 --- a/QuantConnect.InteractiveBrokersBrokerage.Tests/InteractiveBrokersBrokerageAdditionalTests.cs +++ b/QuantConnect.InteractiveBrokersBrokerage.Tests/InteractiveBrokersBrokerageAdditionalTests.cs @@ -754,7 +754,7 @@ public void GetHistoryDoesNotThrowError504WhenDisconnected() new TestCaseData(Symbol.CreateOption(Symbols.SPY, Market.India, OptionStyle.American, OptionRight.Call, 100m, new DateTime(2024, 12, 12)), Resolution.Daily, TickType.Trade), new TestCaseData(Symbol.CreateOption(Symbols.SPX, Market.India, OptionStyle.American, OptionRight.Call, 100m, new DateTime(2024, 12, 12)), Resolution.Daily, TickType.Trade), new TestCaseData(Symbol.Create("SPX", SecurityType.Index, Market.India), Resolution.Daily, TickType.Trade), - new TestCaseData(Symbol.Create("SPX500USD", SecurityType.Cfd, Market.FXCM), Resolution.Daily, TickType.Trade), + new TestCaseData(Symbol.Create("IBUS500", SecurityType.Cfd, Market.FXCM), Resolution.Daily, TickType.Trade), // Unsupported resolution new TestCaseData(Symbols.SPY, Resolution.Tick, TickType.Trade), new TestCaseData(Symbols.SPY_C_192_Feb19_2016, Resolution.Tick, TickType.Trade), @@ -767,6 +767,7 @@ public void GetHistoryDoesNotThrowError504WhenDisconnected() new TestCaseData(Symbols.USDJPY, Resolution.Tick, TickType.OpenInterest), new TestCaseData(Symbols.SPX, Resolution.Tick, TickType.OpenInterest), new TestCaseData(Symbols.Future_ESZ18_Dec2018, Resolution.Tick, TickType.OpenInterest), + new TestCaseData(Symbol.Create("IBUS500", SecurityType.Cfd, Market.InteractiveBrokers), Resolution.Daily, TickType.Trade), }; [TestCaseSource(nameof(UnsupportedHistoryTestCases))] @@ -954,7 +955,7 @@ private static TestCaseData[] HistoryData() var forexSymbol = Symbol.Create("EURUSD", SecurityType.Forex, Market.Oanda); - var indexCfdSymbol = Symbol.Create("SPX500USD", SecurityType.Cfd, Market.InteractiveBrokers); + var indexCfdSymbol = Symbol.Create("IBUS500", SecurityType.Cfd, Market.InteractiveBrokers); var equityCfdSymbol = Symbol.Create("SPY", SecurityType.Cfd, Market.InteractiveBrokers); var forexCfdSymbol = Symbol.Create("EURUSD", SecurityType.Cfd, Market.InteractiveBrokers); // Londong Gold diff --git a/QuantConnect.InteractiveBrokersBrokerage.Tests/InteractiveBrokersBrokerageDataQueueHandlerTest.cs b/QuantConnect.InteractiveBrokersBrokerage.Tests/InteractiveBrokersBrokerageDataQueueHandlerTest.cs index d811bc6..e4597be 100644 --- a/QuantConnect.InteractiveBrokersBrokerage.Tests/InteractiveBrokersBrokerageDataQueueHandlerTest.cs +++ b/QuantConnect.InteractiveBrokersBrokerage.Tests/InteractiveBrokersBrokerageDataQueueHandlerTest.cs @@ -148,7 +148,7 @@ private static TestCaseData[] GetCFDSubscriptionTestCases() }; var equityCfds = new[] { "AAPL", "SPY", "GOOG" }; - var indexCfds = new[] { "SPX500USD", "AU200AUD", "US30USD", "NAS100USD", "UK100GBP", "DE30EUR", "FR40EUR", "HK50HKD", "JP225" }; + var indexCfds = new[] { "IBUS500", "IBAU200", "IBUS30", "IBUST100", "IBGB100", "IBEU50", "IBFR40", "IBHK50", "IBJP225" }; var forexCfds = new[] { "AUDUSD", "NZDUSD", "USDCAD", "USDCHF" }; var metalCfds = new[] { "XAUUSD", "XAGUSD" }; @@ -290,7 +290,7 @@ public void CannotSubscribeToCFDWithUnsupportedMarket() using var ib = new InteractiveBrokersBrokerage(new QCAlgorithm(), new OrderProvider(), new SecurityProvider()); ib.Connect(); - var usSpx500Cfd = Symbol.Create("SPX500USD", SecurityType.Cfd, Market.FXCM); + var usSpx500Cfd = Symbol.Create("IBUS500", SecurityType.Cfd, Market.FXCM); var config = GetSubscriptionDataConfig(usSpx500Cfd, Resolution.Second); var enumerator = ib.Subscribe(config, (s, e) => { }); diff --git a/QuantConnect.InteractiveBrokersBrokerage/InteractiveBrokers/IB-symbol-map.json b/QuantConnect.InteractiveBrokersBrokerage/InteractiveBrokers/IB-symbol-map.json index b585a06..f8b56a3 100644 --- a/QuantConnect.InteractiveBrokersBrokerage/InteractiveBrokers/IB-symbol-map.json +++ b/QuantConnect.InteractiveBrokersBrokerage/InteractiveBrokers/IB-symbol-map.json @@ -17,20 +17,5 @@ "ZAR": "6Z", "BRR": "BTC", "DA": "DC", - "BQX": "BIO", - - // CFDs - "IBUS500": "SPX500USD", - "IBUS30": "US30USD", - "IBUST100": "NAS100USD", - "IBGB100": "UK100GBP", - "IBEU50": "EU50EUR", - "IBDE40": "DE40EUR", - "IBFR40": "FR40EUR", - "IBES35": "ES35EUR", - "IBNL25": "NL25EUR", - "IBCH20": "CH20CHF", - "IBJP225": "JP225USD", - "IBHK50": "HK50HKD", - "IBAU200": "AU200AUD" + "BQX": "BIO" } diff --git a/QuantConnect.InteractiveBrokersBrokerage/InteractiveBrokersBrokerage.cs b/QuantConnect.InteractiveBrokersBrokerage/InteractiveBrokersBrokerage.cs index d02d684..b42c48f 100644 --- a/QuantConnect.InteractiveBrokersBrokerage/InteractiveBrokersBrokerage.cs +++ b/QuantConnect.InteractiveBrokersBrokerage/InteractiveBrokersBrokerage.cs @@ -223,6 +223,7 @@ public sealed class InteractiveBrokersBrokerage : Brokerage, IDataQueueHandler, private bool _historyDelistedAssetWarning; private bool _historyExpiredAssetWarning; private bool _historyOpenInterestWarning; + private bool _historyCfdTradeWarning; private bool _historyInvalidPeriodWarning; /// @@ -4171,6 +4172,16 @@ public override IEnumerable GetHistory(HistoryRequest request) return null; } + if (request.Symbol.SecurityType == SecurityType.Cfd && request.TickType == TickType.Trade) + { + if (!_historyCfdTradeWarning) + { + _historyCfdTradeWarning = true; + OnMessage(new BrokerageMessageEvent(BrokerageMessageType.Warning, "GetHistoryCfdTrade", "IB does not provide CFD trade historical data")); + } + return null; + } + if (request.EndTimeUtc < request.StartTimeUtc) { if (!_historyInvalidPeriodWarning) diff --git a/QuantConnect.InteractiveBrokersBrokerage/InteractiveBrokersSymbolMapper.cs b/QuantConnect.InteractiveBrokersBrokerage/InteractiveBrokersSymbolMapper.cs index e8fdd81..63a9eed 100644 --- a/QuantConnect.InteractiveBrokersBrokerage/InteractiveBrokersSymbolMapper.cs +++ b/QuantConnect.InteractiveBrokersBrokerage/InteractiveBrokersSymbolMapper.cs @@ -210,10 +210,6 @@ public string GetBrokerageSymbol(Symbol symbol) case SecurityType.Equity: brokerageSymbol = brokerageSymbol.Replace(" ", "."); break; - - case SecurityType.Cfd: - brokerageSymbol = GetLeanRootSymbol(brokerageSymbol); - break; } return Symbol.Create(brokerageSymbol, securityType, market); From bec32b4540a2fd818f5f3c38138a9520519829f6 Mon Sep 17 00:00:00 2001 From: Jhonathan Abreu Date: Mon, 25 Mar 2024 18:21:25 -0400 Subject: [PATCH 16/16] Minor change --- .../InteractiveBrokersCfdOrderTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/QuantConnect.InteractiveBrokersBrokerage.Tests/InteractiveBrokersCfdOrderTests.cs b/QuantConnect.InteractiveBrokersBrokerage.Tests/InteractiveBrokersCfdOrderTests.cs index 05f3f68..43e034f 100644 --- a/QuantConnect.InteractiveBrokersBrokerage.Tests/InteractiveBrokersCfdOrderTests.cs +++ b/QuantConnect.InteractiveBrokersBrokerage.Tests/InteractiveBrokersCfdOrderTests.cs @@ -25,7 +25,7 @@ namespace QuantConnect.Tests.Brokerages.InteractiveBrokers [Explicit("These tests require the IBGateway to be installed.")] public class InteractiveBrokersCfdOrderTests : BrokerageTests { - private static Symbol IndexCfdSymbol = Symbol.Create("SPX500USD", SecurityType.Cfd, Market.InteractiveBrokers); + private static Symbol IndexCfdSymbol = Symbol.Create("IBUS500", SecurityType.Cfd, Market.InteractiveBrokers); private static Symbol EquityCfdSymbol = Symbol.Create("AAPL", SecurityType.Cfd, Market.InteractiveBrokers); private static Symbol ForexCfdSymbol = Symbol.Create("AUDUSD", SecurityType.Cfd, Market.InteractiveBrokers); private static Symbol MetalCfdSymbol = Symbol.Create("XAUUSD", SecurityType.Cfd, Market.InteractiveBrokers);