Skip to content

Commit

Permalink
Merge pull request #100 from jhonabreul/feature-10-cfd-support
Browse files Browse the repository at this point in the history
Add support for CFDs
  • Loading branch information
jhonabreul authored Mar 26, 2024
2 parents 3ca8063 + bec32b4 commit b133b55
Show file tree
Hide file tree
Showing 8 changed files with 828 additions and 61 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand All @@ -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))]
Expand Down Expand Up @@ -908,7 +910,7 @@ private List<BaseData> 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),
Expand All @@ -917,7 +919,7 @@ private List<BaseData> 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();
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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),
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down Expand Up @@ -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<string> 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<Symbol>();
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<Tick>(symbol, resolution),
_ => tickType == TickType.Trade
? GetSubscriptionDataConfig<TradeBar>(symbol, resolution)
: GetSubscriptionDataConfig<QuoteBar>(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<Symbol>();
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<Tick>(underlyingSymbol, resolution),
_ => tickType == TickType.Trade
? GetSubscriptionDataConfig<TradeBar>(underlyingSymbol, resolution)
: GetSubscriptionDataConfig<QuoteBar>(underlyingSymbol, resolution)
};
var cfdConfig = resolution switch
{
Resolution.Tick => GetSubscriptionDataConfig<Tick>(cfdSymbol, resolution),
_ => tickType == TickType.Trade
? GetSubscriptionDataConfig<TradeBar>(cfdSymbol, resolution)
: GetSubscriptionDataConfig<QuoteBar>(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<QuoteBar>(usSpx500Cfd, Resolution.Second);

var enumerator = ib.Subscribe(config, (s, e) => { });

Assert.IsNull(enumerator);
}

protected SubscriptionDataConfig GetSubscriptionDataConfig<T>(Symbol symbol, Resolution resolution)
{
var entry = MarketHoursDatabase.FromDataFolder().GetEntry(symbol.ID.Market, symbol, symbol.SecurityType);
Expand Down
Loading

0 comments on commit b133b55

Please sign in to comment.