Skip to content

Commit

Permalink
Fix Python Consolidate API to respect strict daily times for Daily …
Browse files Browse the repository at this point in the history
…resolution (#8373)

* Fix ConsolidateRegressionAlgorithm

- Fix Bitcoin custom data time range. Historical data goes from April 2014
- Fix expected consolidated bar counts
- Fix python Consolidate implementations to respect daily strict times for Daily resolution

* Housekeeping
  • Loading branch information
jhonabreul authored Oct 16, 2024
1 parent 8ef0a49 commit 159fa5a
Show file tree
Hide file tree
Showing 3 changed files with 151 additions and 92 deletions.
140 changes: 83 additions & 57 deletions Algorithm.CSharp/ConsolidateRegressionAlgorithm.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
using QuantConnect.Indicators;
using QuantConnect.Interfaces;
using QuantConnect.Securities;
using QuantConnect.Securities.Future;

namespace QuantConnect.Algorithm.CSharp
{
Expand All @@ -31,69 +32,83 @@ namespace QuantConnect.Algorithm.CSharp
public class ConsolidateRegressionAlgorithm : QCAlgorithm, IRegressionAlgorithmDefinition
{
private List<int> _consolidationCounts;
private List<int> _expectedConsolidationCounts;
private List<SimpleMovingAverage> _smas;
private List<DateTime> _lastSmaUpdates;
private int _expectedConsolidations;
private int _customDataConsolidator;
private Symbol _symbol;
private int _customDataConsolidatorCount;
private Future _future;

/// <summary>
/// Initialise the data and resolution required, as well as the cash and start-end dates for your algorithm. All algorithms must initialized.
/// </summary>
public override void Initialize()
{
SetStartDate(2013, 10, 08);
SetEndDate(2013, 10, 20);
SetStartDate(2020, 01, 05);
SetEndDate(2020, 01, 20);

var SP500 = QuantConnect.Symbol.Create(Futures.Indices.SP500EMini, SecurityType.Future, Market.CME);
_symbol = FutureChainProvider.GetFutureContractList(SP500, StartDate).First();
var security = AddFutureContract(_symbol);
var symbol = FutureChainProvider.GetFutureContractList(SP500, StartDate).First();
_future = AddFutureContract(symbol);

_consolidationCounts = Enumerable.Repeat(0, 9).ToList();
_smas = _consolidationCounts.Select(_ => new SimpleMovingAverage(10)).ToList();
_lastSmaUpdates = _consolidationCounts.Select(_ => DateTime.MinValue).ToList();
var tradableDatesCount = QuantConnect.Time.EachTradeableDayInTimeZone(_future.Exchange.Hours,
StartDate,
EndDate,
_future.Exchange.TimeZone,
false).Count();
_expectedConsolidationCounts = new(10);

Consolidate<QuoteBar>(_symbol, time => new CalendarInfo(time.RoundDown(TimeSpan.FromDays(1)), TimeSpan.FromDays(1)),
Consolidate<QuoteBar>(symbol, time => new CalendarInfo(time.RoundDown(TimeSpan.FromDays(1)), TimeSpan.FromDays(1)),
bar => UpdateQuoteBar(bar, 0));
// The consolidator will respect the full 1 day bar span and will not consolidate the last tradable date,
// since scan will not be called at 202/01/21 12am
_expectedConsolidationCounts.Add(tradableDatesCount - 1);

Consolidate<QuoteBar>(_symbol, time => new CalendarInfo(time.RoundDown(TimeSpan.FromDays(1)), TimeSpan.FromDays(1)),
Consolidate<QuoteBar>(symbol, time => new CalendarInfo(time.RoundDown(TimeSpan.FromDays(1)), TimeSpan.FromDays(1)),
TickType.Quote, bar => UpdateQuoteBar(bar, 1));
_expectedConsolidationCounts.Add(tradableDatesCount - 1);

Consolidate<QuoteBar>(symbol, TimeSpan.FromDays(1), bar => UpdateQuoteBar(bar, 2));
_expectedConsolidationCounts.Add(tradableDatesCount - 1);

Consolidate(symbol, Resolution.Daily, TickType.Quote, (Action<QuoteBar>)(bar => UpdateQuoteBar(bar, 3)));
_expectedConsolidationCounts.Add(tradableDatesCount);

Consolidate<QuoteBar>(_symbol, TimeSpan.FromDays(1), bar => UpdateQuoteBar(bar, 2));
Consolidate(symbol, TimeSpan.FromDays(1), bar => UpdateTradeBar(bar, 4));
_expectedConsolidationCounts.Add(tradableDatesCount - 1);

Consolidate(_symbol, Resolution.Daily, TickType.Quote, (Action<QuoteBar>)(bar => UpdateQuoteBar(bar, 3)));
Consolidate<TradeBar>(symbol, TimeSpan.FromDays(1), bar => UpdateTradeBar(bar, 5));
_expectedConsolidationCounts.Add(tradableDatesCount - 1);

Consolidate(_symbol, TimeSpan.FromDays(1), bar => UpdateTradeBar(bar, 4));
// Test using abstract T types, through defining a 'BaseData' handler

Consolidate(symbol, Resolution.Daily, null, (Action<BaseData>)(bar => UpdateBar(bar, 6)));
_expectedConsolidationCounts.Add(tradableDatesCount);

Consolidate<TradeBar>(_symbol, TimeSpan.FromDays(1), bar => UpdateTradeBar(bar, 5));
Consolidate(symbol, TimeSpan.FromDays(1), null, (Action<BaseData>)(bar => UpdateBar(bar, 7)));
_expectedConsolidationCounts.Add(tradableDatesCount - 1);

Consolidate(symbol, TimeSpan.FromDays(1), (Action<BaseData>)(bar => UpdateBar(bar, 8)));
_expectedConsolidationCounts.Add(tradableDatesCount - 1);

_consolidationCounts = Enumerable.Repeat(0, _expectedConsolidationCounts.Count).ToList();
_smas = _consolidationCounts.Select(_ => new SimpleMovingAverage(10)).ToList();
_lastSmaUpdates = _consolidationCounts.Select(_ => DateTime.MinValue).ToList();

// custom data
var symbol = AddData<CustomDataRegressionAlgorithm.Bitcoin>("BTC", Resolution.Minute).Symbol;
Consolidate<TradeBar>(symbol, TimeSpan.FromDays(1), bar => _customDataConsolidator++);
var customSecurity = AddData<CustomDataRegressionAlgorithm.Bitcoin>("BTC", Resolution.Minute);
Consolidate<TradeBar>(customSecurity.Symbol, TimeSpan.FromDays(1), bar => _customDataConsolidatorCount++);

try
{
Consolidate<QuoteBar>(symbol, TimeSpan.FromDays(1), bar => { UpdateQuoteBar(bar, -1); });
Consolidate<QuoteBar>(customSecurity.Symbol, TimeSpan.FromDays(1), bar => { UpdateQuoteBar(bar, -1); });
throw new RegressionTestException($"Expected {nameof(ArgumentException)} to be thrown");
}
catch (ArgumentException)
{
// will try to use BaseDataConsolidator for which input is TradeBars not QuoteBars
}

// Test using abstract T types, through defining a 'BaseData' handler
Consolidate(_symbol, Resolution.Daily, null, (Action<BaseData>)(bar => UpdateBar(bar, 6)));

Consolidate(_symbol, TimeSpan.FromDays(1), null, (Action<BaseData>)(bar => UpdateBar(bar, 7)));

Consolidate(_symbol, TimeSpan.FromDays(1), (Action<BaseData>)(bar => UpdateBar(bar, 8)));

_expectedConsolidations = QuantConnect.Time.EachTradeableDayInTimeZone(security.Exchange.Hours,
StartDate,
EndDate,
security.Exchange.TimeZone,
false).Count();
}

private void UpdateBar(BaseData tradeBar, int position)
{
if (!(tradeBar is TradeBar))
Expand All @@ -119,16 +134,27 @@ private void UpdateQuoteBar(QuoteBar quoteBar, int position)

public override void OnEndOfAlgorithm()
{
if (_consolidationCounts.Any(i => i != _expectedConsolidations) || _customDataConsolidator == 0)
for (var i = 0; i < _consolidationCounts.Count; i++)
{
var consolidationCount = _consolidationCounts[i];
var expectedConsolidationCount = _expectedConsolidationCounts[i];

if (consolidationCount != expectedConsolidationCount)
{
throw new RegressionTestException($"Expected {expectedConsolidationCount} consolidations for consolidator {i} but received {consolidationCount}");
}
}

if (_customDataConsolidatorCount == 0)
{
throw new RegressionTestException("Unexpected consolidation count");
throw new RegressionTestException($"Unexpected custom data consolidation count: {_customDataConsolidatorCount}");
}

for (var i = 0; i < _smas.Count; i++)
{
if (_smas[i].Samples != _expectedConsolidations)
if (_smas[i].Samples != _expectedConsolidationCounts[i])
{
throw new RegressionTestException($"Expected {_expectedConsolidations} samples in each SMA but found {_smas[i].Samples} in SMA in index {i}");
throw new RegressionTestException($"Expected {_expectedConsolidationCounts} samples in each SMA but found {_smas[i].Samples} in SMA in index {i}");
}

if (_smas[i].Current.Time != _lastSmaUpdates[i])
Expand All @@ -144,9 +170,9 @@ public override void OnEndOfAlgorithm()
/// <param name="data">Slice object keyed by symbol containing the stock data</param>
public override void OnData(Slice slice)
{
if (!Portfolio.Invested)
if (!Portfolio.Invested && _future.HasData)
{
SetHoldings(_symbol, 0.5);
SetHoldings(_future.Symbol, 0.5);
}
}

Expand All @@ -163,7 +189,7 @@ public override void OnData(Slice slice)
/// <summary>
/// Data Points count of all timeslices of algorithm
/// </summary>
public long DataPoints => 12244;
public long DataPoints => 14227;

/// <summary>
/// Data Points count of the algorithm history
Expand All @@ -183,30 +209,30 @@ public override void OnData(Slice slice)
{"Total Orders", "1"},
{"Average Win", "0%"},
{"Average Loss", "0%"},
{"Compounding Annual Return", "6636.699%"},
{"Drawdown", "15.900%"},
{"Compounding Annual Return", "665.524%"},
{"Drawdown", "1.500%"},
{"Expectancy", "0"},
{"Start Equity", "100000"},
{"End Equity", "116177.7"},
{"Net Profit", "16.178%"},
{"Sharpe Ratio", "640.313"},
{"End Equity", "109332.4"},
{"Net Profit", "9.332%"},
{"Sharpe Ratio", "9.805"},
{"Sortino Ratio", "0"},
{"Probabilistic Sharpe Ratio", "99.824%"},
{"Probabilistic Sharpe Ratio", "93.474%"},
{"Loss Rate", "0%"},
{"Win Rate", "0%"},
{"Profit-Loss Ratio", "0"},
{"Alpha", "636.164"},
{"Beta", "5.924"},
{"Annual Standard Deviation", "1.012"},
{"Annual Variance", "1.024"},
{"Information Ratio", "696.123"},
{"Tracking Error", "0.928"},
{"Treynor Ratio", "109.404"},
{"Total Fees", "$23.65"},
{"Estimated Strategy Capacity", "$210000000.00"},
{"Lowest Capacity Asset", "ES VMKLFZIH2MTD"},
{"Portfolio Turnover", "81.19%"},
{"OrderListHash", "dfd9a280d3c6470b305c03e0b72c234e"}
{"Alpha", "3.164"},
{"Beta", "0.957"},
{"Annual Standard Deviation", "0.383"},
{"Annual Variance", "0.146"},
{"Information Ratio", "8.29"},
{"Tracking Error", "0.379"},
{"Treynor Ratio", "3.917"},
{"Total Fees", "$15.05"},
{"Estimated Strategy Capacity", "$2100000000.00"},
{"Lowest Capacity Asset", "ES XCZJLC9NOB29"},
{"Portfolio Turnover", "64.34%"},
{"OrderListHash", "d814db6d5a9c97ee6de477ea06cd3834"}
};
}
}
86 changes: 53 additions & 33 deletions Algorithm.Python/ConsolidateRegressionAlgorithm.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,42 +21,55 @@ class ConsolidateRegressionAlgorithm(QCAlgorithm):

# Initialise the data and resolution required, as well as the cash and start-end dates for your algorithm. All algorithms must initialized.
def initialize(self):
self.set_start_date(2013, 10, 8)
self.set_end_date(2013, 10, 20)
self.set_start_date(2020, 1, 5)
self.set_end_date(2020, 1, 20)

SP500 = Symbol.create(Futures.Indices.SP_500_E_MINI, SecurityType.FUTURE, Market.CME)
self._symbol = _symbol = self.future_chain_provider.get_future_contract_list(SP500, self.start_date)[0]
self.add_future_contract(_symbol)
symbol = self.future_chain_provider.get_future_contract_list(SP500, self.start_date)[0]
self._future = self.add_future_contract(symbol)

self._consolidation_counts = [0] * 6
self._smas = [SimpleMovingAverage(10) for x in self._consolidation_counts]
self._last_sma_updates = [datetime.min for x in self._consolidation_counts]
self._monthly_consolidator_sma = SimpleMovingAverage(10)
self._monthly_consolidation_count = 0
self._weekly_consolidator_sma = SimpleMovingAverage(10)
self._weekly_consolidation_count = 0
self._last_weekly_sma_update = datetime.min
tradable_dates_count = len(list(Time.each_tradeable_day_in_time_zone(self._future.exchange.hours,
self.start_date,
self.end_date,
self._future.exchange.time_zone,
False)));
self._expected_consolidation_counts = [];

self.consolidate(_symbol, Calendar.MONTHLY, lambda bar: self.update_monthly_consolidator(bar, -1)) # shouldn't consolidate
self.consolidate(symbol, Calendar.MONTHLY, lambda bar: self.update_monthly_consolidator(bar, -1)) # shouldn't consolidate

self.consolidate(_symbol, Calendar.WEEKLY, TickType.TRADE, lambda bar: self.update_weekly_consolidator(bar))
self.consolidate(symbol, Calendar.WEEKLY, TickType.TRADE, lambda bar: self.update_weekly_consolidator(bar))

self.consolidate(_symbol, Resolution.DAILY, lambda bar: self.update_trade_bar(bar, 0))
self.consolidate(symbol, Resolution.DAILY, lambda bar: self.update_trade_bar(bar, 0))
self._expected_consolidation_counts.append(tradable_dates_count)

self.consolidate(_symbol, Resolution.DAILY, TickType.QUOTE, lambda bar: self.update_quote_bar(bar, 1))
self.consolidate(symbol, Resolution.DAILY, TickType.QUOTE, lambda bar: self.update_quote_bar(bar, 1))
self._expected_consolidation_counts.append(tradable_dates_count)

self.consolidate(_symbol, timedelta(1), lambda bar: self.update_trade_bar(bar, 2))
self.consolidate(symbol, timedelta(1), lambda bar: self.update_trade_bar(bar, 2))
self._expected_consolidation_counts.append(tradable_dates_count - 1)

self.consolidate(_symbol, timedelta(1), TickType.QUOTE, lambda bar: self.update_quote_bar(bar, 3))
self.consolidate(symbol, timedelta(1), TickType.QUOTE, lambda bar: self.update_quote_bar(bar, 3))
self._expected_consolidation_counts.append(tradable_dates_count - 1)

# sending None tick type
self.consolidate(_symbol, timedelta(1), None, lambda bar: self.update_trade_bar(bar, 4))
self.consolidate(symbol, timedelta(1), None, lambda bar: self.update_trade_bar(bar, 4))
self._expected_consolidation_counts.append(tradable_dates_count - 1)

self.consolidate(_symbol, Resolution.DAILY, None, lambda bar: self.update_trade_bar(bar, 5))
self.consolidate(symbol, Resolution.DAILY, None, lambda bar: self.update_trade_bar(bar, 5))
self._expected_consolidation_counts.append(tradable_dates_count)

self._consolidation_counts = [0] * len(self._expected_consolidation_counts)
self._smas = [SimpleMovingAverage(10) for x in self._consolidation_counts]
self._last_sma_updates = [datetime.min for x in self._consolidation_counts]
self._monthly_consolidator_sma = SimpleMovingAverage(10)
self._monthly_consolidation_count = 0
self._weekly_consolidator_sma = SimpleMovingAverage(10)
self._weekly_consolidation_count = 0
self._last_weekly_sma_update = datetime.min

# custom data
self._custom_data_consolidator = 0
custom_symbol = self.add_data(Bitcoin, "BTC", Resolution.MINUTE).symbol
custom_symbol = self.add_data(Bitcoin, "BTC", Resolution.DAILY).symbol
self.consolidate(custom_symbol, timedelta(1), lambda bar: self.increment_counter(1))

self._custom_data_consolidator2 = 0
Expand Down Expand Up @@ -87,18 +100,25 @@ def update_weekly_consolidator(self, bar):
self._last_weekly_sma_update = bar.end_time
self._weekly_consolidation_count += 1

def OnEndOfAlgorithm(self):
expected_consolidations = 9
expected_weekly_consolidations = 1
if (any(i != expected_consolidations for i in self._consolidation_counts) or
self._weekly_consolidation_count != expected_weekly_consolidations or
self._custom_data_consolidator == 0 or
self._custom_data_consolidator2 == 0):
raise ValueError("Unexpected consolidation count")
def on_end_of_algorithm(self):
for i, expected_consolidation_count in enumerate(self._expected_consolidation_counts):
consolidation_count = self._consolidation_counts[i]
if consolidation_count != expected_consolidation_count:
raise ValueError(f"Unexpected consolidation count for index {i}: expected {expected_consolidation_count} but was {consolidation_count}")

expected_weekly_consolidations = (self.end_date - self.start_date).days // 7
if self._weekly_consolidation_count != expected_weekly_consolidations:
raise ValueError(f"Expected {expected_weekly_consolidations} weekly consolidations but found {self._weekly_consolidation_count}")

if self._custom_data_consolidator == 0:
raise ValueError("Custom data consolidator did not consolidate any data")

if self._custom_data_consolidator2 == 0:
raise ValueError("Custom data consolidator 2 did not consolidate any data")

for i, sma in enumerate(self._smas):
if sma.samples != expected_consolidations:
raise Exception(f"Expected {expected_consolidations} samples in each SMA but found {sma.samples} in SMA in index {i}")
if sma.samples != self._expected_consolidation_counts[i]:
raise Exception(f"Expected {self._expected_consolidation_counts[i]} samples in each SMA but found {sma.samples} in SMA in index {i}")

last_update = self._last_sma_updates[i]
if sma.current.time != last_update:
Expand All @@ -115,5 +135,5 @@ def OnEndOfAlgorithm(self):

# on_data event is the primary entry point for your algorithm. Each new data point will be pumped in here.
def on_data(self, data):
if not self.portfolio.invested:
self.set_holdings(self._symbol, 0.5)
if not self.portfolio.invested and self._future.has_data:
self.set_holdings(self._future.symbol, 0.5)
17 changes: 15 additions & 2 deletions Algorithm/QCAlgorithm.Python.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1392,7 +1392,7 @@ public void Quit(PyObject message)
[DocumentationAttribute(ConsolidatingData)]
public IDataConsolidator Consolidate(Symbol symbol, Resolution period, PyObject handler)
{
return Consolidate(symbol, period.ToTimeSpan(), null, handler);
return Consolidate(symbol, period, null, handler);
}

/// <summary>
Expand All @@ -1406,7 +1406,20 @@ public IDataConsolidator Consolidate(Symbol symbol, Resolution period, PyObject
[DocumentationAttribute(ConsolidatingData)]
public IDataConsolidator Consolidate(Symbol symbol, Resolution period, TickType? tickType, PyObject handler)
{
return Consolidate(symbol, period.ToTimeSpan(), tickType, handler);
// resolve consolidator input subscription
var type = GetSubscription(symbol, tickType).Type;

if (type == typeof(TradeBar))
{
return Consolidate(symbol, period, tickType, handler.ConvertToDelegate<Action<TradeBar>>());
}

if (type == typeof(QuoteBar))
{
return Consolidate(symbol, period, tickType, handler.ConvertToDelegate<Action<QuoteBar>>());
}

return Consolidate(symbol, period, tickType, handler.ConvertToDelegate<Action<BaseData>>());
}

/// <summary>
Expand Down

0 comments on commit 159fa5a

Please sign in to comment.