diff --git a/QuantConnect.InteractiveBrokersBrokerage.Tests/InteractiveBrokersBrokerageAdditionalTests.cs b/QuantConnect.InteractiveBrokersBrokerage.Tests/InteractiveBrokersBrokerageAdditionalTests.cs index 16c1979..e5ae93b 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("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), @@ -766,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))] @@ -908,7 +910,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(TradeBar) : typeof(QuoteBar), symbol, resolution, SecurityExchangeHours.AlwaysOpen(exchangeTimeZone), @@ -917,7 +919,7 @@ private List GetHistory( includeExtendedMarketHours, false, DataNormalizationMode.Raw, - TickType.Trade); + symbol.SecurityType != SecurityType.Cfd && symbol.SecurityType != SecurityType.Forex ? TickType.Trade : TickType.Quote); var start = DateTime.UtcNow; var history = brokerage.GetHistory(request).ToList(); @@ -950,6 +952,17 @@ 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("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 + var metalCfdSymbol1 = Symbol.Create("XAUUSD", SecurityType.Cfd, Market.InteractiveBrokers); + // Londong Silver + var metalCfdSymbol2 = Symbol.Create("XAGUSD", SecurityType.Cfd, Market.InteractiveBrokers); + return new[] { // 30 min RTH today + 60 min RTH yesterday @@ -983,9 +996,76 @@ 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), + + // 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 c9b666c..e4597be 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,166 @@ public void GetsTickDataAfterDisconnectionConnectionCycle() } } + 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[] { "IBUS500", "IBAU200", "IBUS30", "IBUST100", "IBGB100", "IBEU50", "IBFR40", "IBHK50", "IBJP225" }; + 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(); + } + + [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); + + 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(); + + foreach (var ticker in tickers) + { + var symbol = Symbol.Create(ticker, SecurityType.Cfd, Market.InteractiveBrokers); + var config = resolution switch + { + Resolution.Tick => GetSubscriptionDataConfig(symbol, resolution), + _ => tickType == TickType.Trade + ? GetSubscriptionDataConfig(symbol, resolution) + : GetSubscriptionDataConfig(symbol, resolution) + }; + + 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(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.InteractiveBrokers); + + 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() + { + using var ib = new InteractiveBrokersBrokerage(new QCAlgorithm(), new OrderProvider(), new SecurityProvider()); + ib.Connect(); + + var usSpx500Cfd = Symbol.Create("IBUS500", 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.Tests/InteractiveBrokersCfdOrderTests.cs b/QuantConnect.InteractiveBrokersBrokerage.Tests/InteractiveBrokersCfdOrderTests.cs new file mode 100644 index 0000000..43e034f --- /dev/null +++ b/QuantConnect.InteractiveBrokersBrokerage.Tests/InteractiveBrokersCfdOrderTests.cs @@ -0,0 +1,294 @@ +/* + * 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("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); + + 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)), + }; + } + + 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) + { + base.CancelOrders(parameters); + } + + [Test, TestCaseSource(nameof(IndexCfdOrderTest))] + public void LongFromZeroIndexCfd(OrderTestParameters parameters) + { + base.LongFromZero(parameters); + } + + [Test, TestCaseSource(nameof(IndexCfdOrderTest))] + public void CloseFromLongIndexCfd(OrderTestParameters parameters) + { + base.CloseFromLong(parameters); + } + + [Test, TestCaseSource(nameof(IndexCfdOrderTest))] + public void ShortFromZeroIndexCfd(OrderTestParameters parameters) + { + base.ShortFromZero(parameters); + } + + [Test, TestCaseSource(nameof(IndexCfdOrderTest))] + public void CloseFromShortIndexCfd(OrderTestParameters parameters) + { + base.CloseFromShort(parameters); + } + + [Test, TestCaseSource(nameof(IndexCfdOrderTest))] + public void ShortFromLongIndexCfd(OrderTestParameters parameters) + { + base.ShortFromLong(parameters); + } + + [Test, TestCaseSource(nameof(IndexCfdOrderTest))] + public void LongFromShortIndexCfd(OrderTestParameters parameters) + { + base.LongFromShort(parameters); + } + + #endregion + + #region Equity CFDs + + [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); + } + + #endregion + + #region Forex CFDs + + [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); + } + + #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() + { + 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/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/InteractiveBrokers/IB-symbol-map.json b/QuantConnect.InteractiveBrokersBrokerage/InteractiveBrokers/IB-symbol-map.json index 1ca7228..f8b56a3 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", diff --git a/QuantConnect.InteractiveBrokersBrokerage/InteractiveBrokersBrokerage.cs b/QuantConnect.InteractiveBrokersBrokerage/InteractiveBrokersBrokerage.cs index d19d443..b42c48f 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 { @@ -75,8 +76,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 @@ -194,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(); @@ -222,6 +223,7 @@ public sealed class InteractiveBrokersBrokerage : Brokerage, IDataQueueHandler, private bool _historyDelistedAssetWarning; private bool _historyExpiredAssetWarning; private bool _historyOpenInterestWarning; + private bool _historyCfdTradeWarning; private bool _historyInvalidPeriodWarning; /// @@ -1285,6 +1287,7 @@ private void Initialize( _client.TickPrice += HandleTickPrice; _client.TickSize += HandleTickSize; _client.CurrentTimeUtc += HandleBrokerTime; + _client.ReRouteMarketDataRequest += HandleMarketDataReRoute; // we need to wait until we receive the next valid id from the server _client.NextValidId += (sender, e) => @@ -1412,7 +1415,7 @@ private void IBPlaceOrder(Order order, bool needsNewId, string exchange = null) { if (noSubmissionOrderTypes) { - if(!_submissionOrdersWarningSent) + if (!_submissionOrdersWarningSent) { _submissionOrdersWarningSent = true; OnMessage(new BrokerageMessageEvent(BrokerageMessageType.Warning, @@ -1446,6 +1449,15 @@ private void IBPlaceOrder(Order order, bool needsNewId, string exchange = null) private static string GetUniqueKey(Contract contract) { + var leanSecurityType = ConvertSecurityType(contract); + if (leanSecurityType == SecurityType.Equity || + leanSecurityType == SecurityType.Forex || + leanSecurityType == SecurityType.Cfd || + leanSecurityType == SecurityType.Index) + { + 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}"; } @@ -1516,13 +1528,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); } /// @@ -1530,7 +1542,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 @@ -1543,7 +1555,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})" }; @@ -1817,21 +1829,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; } } @@ -2178,7 +2201,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; @@ -2319,7 +2342,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; } @@ -2328,7 +2351,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); @@ -2571,7 +2594,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); } @@ -2604,7 +2627,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); @@ -2846,6 +2869,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 +2877,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, failIfNotFound: false); - if (symbol.ID.SecurityType == SecurityType.Equity) + // 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 (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 +2947,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 @@ -3178,6 +3220,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."); } @@ -3186,7 +3231,7 @@ private static string ConvertSecurityType(SecurityType type) /// /// Maps SecurityType enum /// - private SecurityType ConvertSecurityType(Contract contract) + private static SecurityType ConvertSecurityType(Contract contract) { switch (contract.SecType) { @@ -3210,6 +3255,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)}. " + @@ -3329,7 +3377,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) + { + // If this is a forex CFD, we need to compose the symbol like we do for forex + var potentialCurrencyPair = contract.TradingClass.Replace(".", ""); + if (CurrencyPairUtil.IsForexDecomposable(potentialCurrencyPair)) + { + Forex.DecomposeCurrencyPair(potentialCurrencyPair, 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; @@ -3498,6 +3564,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 /// @@ -3546,15 +3646,7 @@ private bool Subscribe(IEnumerable symbols) // track subscription time for minimum delay in unsubscribe _subscriptionTimes[id] = DateTime.UtcNow; - 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()); + RequestMarketData(contract, id); _subscribedSymbols[symbol] = id; _subscribedTickers[id] = new SubscriptionEntry { Symbol = subscribeSymbol, PriceMagnifier = priceMagnifier }; @@ -3677,7 +3769,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.InteractiveBrokers); } /// @@ -4018,7 +4111,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(); @@ -4079,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) @@ -4118,7 +4221,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)); @@ -4146,6 +4249,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; @@ -4341,9 +4462,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; @@ -4408,7 +4529,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; } @@ -4452,7 +4573,7 @@ private void CheckRateLimiting() private void CheckHighResolutionHistoryRateLimiting(Resolution resolution) { - if(resolution != Resolution.Tick && resolution != Resolution.Second) + if (resolution != Resolution.Tick && resolution != Resolution.Second) { return; } @@ -4608,7 +4729,7 @@ private void StartGatewayWeeklyRestartTask() if (restart) { - Log.Trace($"InteractiveBrokersBrokerage.StartGatewayWeeklyRestartTask(): triggering weekly restart manually"); + Log.Trace($"InteractiveBrokersBrokerage.StartGatewayWeeklyRestartTask(): triggering weekly restart manually"); try { @@ -4706,9 +4827,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() @@ -5059,6 +5180,7 @@ private enum RequestType ContractDetails, History, Executions, + SoftContractDetails, // Don't fail if we can't find the contract } private class RequestInformation diff --git a/QuantConnect.InteractiveBrokersBrokerage/InteractiveBrokersSymbolMapper.cs b/QuantConnect.InteractiveBrokersBrokerage/InteractiveBrokersSymbolMapper.cs index 8777a4b..63a9eed 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: @@ -147,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 @@ -158,7 +169,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);