diff --git a/Algorithm.CSharp/ConsolidateHourBarsIntoDailyBarsRegressionAlgorithm.cs b/Algorithm.CSharp/ConsolidateHourBarsIntoDailyBarsRegressionAlgorithm.cs new file mode 100644 index 000000000000..2b6fdea31d1b --- /dev/null +++ b/Algorithm.CSharp/ConsolidateHourBarsIntoDailyBarsRegressionAlgorithm.cs @@ -0,0 +1,166 @@ +/* + * QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals. + * Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. +*/ + +using QuantConnect.Data; +using QuantConnect.Indicators; +using QuantConnect.Interfaces; +using System; +using System.Collections.Generic; +using QuantConnect.Data.Market; + +namespace QuantConnect.Algorithm.CSharp +{ + /// + /// This regression algorithm asserts the consolidated US equity daily bars from the hour bars exactly matches + /// the daily bars returned from the database + /// + public class ConsolidateHourBarsIntoDailyBarsRegressionAlgorithm : QCAlgorithm, IRegressionAlgorithmDefinition + { + private Symbol _spy; + private RelativeStrengthIndex _rsi; + private RelativeStrengthIndex _rsiTimeDelta; + private Dictionary _values = new(); + private int _count; + private bool _indicatorsCompared; + + public override void Initialize() + { + SetStartDate(2020, 5, 1); + SetEndDate(2020, 6, 5); + + _spy = AddEquity("SPY", Resolution.Hour).Symbol; + + // We will use these two indicators to compare the daily consolidated bars equals + // the ones returned from the database. We use this specific type of indicator as + // it depends on its previous values. Thus, if at some point the bars received by + // the indicators differ, so will their final values + _rsi = new RelativeStrengthIndex("FIRST", 15, MovingAverageType.Wilders); + RegisterIndicator(_spy, _rsi, Resolution.Daily, selector: (bar) => + { + var tradeBar = (TradeBar)bar; + return (tradeBar.Close + tradeBar.Open) / 2; + }); + + // We won't register this indicator as we will update it manually at the end of the + // month, so that we can compare the values of the indicator that received consolidated + // bars and the values of this one + _rsiTimeDelta = new RelativeStrengthIndex("SECOND" ,15, MovingAverageType.Wilders); + } + + public override void OnData(Slice slice) + { + if (IsWarmingUp) return; + + if (slice.ContainsKey(_spy) && slice[_spy] != null) + { + if (Time.Month == EndDate.Month) + { + var history = History(_spy, _count, Resolution.Daily); + foreach (var bar in history) + { + var time = bar.EndTime.Date; + var average = (bar.Close + bar.Open) / 2; + _rsiTimeDelta.Update(bar.EndTime, average); + if (_rsiTimeDelta.Current.Value != _values[time]) + { + throw new RegressionTestException($"Both {_rsi.Name} and {_rsiTimeDelta.Name} should have the same values, but they differ. {_rsi.Name}: {_values[time]} | {_rsiTimeDelta.Name}: {_rsiTimeDelta.Current.Value}"); + } + } + _indicatorsCompared = true; + Quit(); + } + else + { + _values[Time.Date] = _rsi.Current.Value; + + // Since the symbol resolution is hour and the symbol is equity, we know the last bar received in a day will + // be at the market close, this is 16h. We need to count how many daily bars were consolidated in order to know + // how many we need to request from the history + if (Time.Hour == 16) + { + _count++; + } + } + } + } + + public override void OnEndOfAlgorithm() + { + if (!_indicatorsCompared) + { + throw new RegressionTestException($"Indicators {_rsi.Name} and {_rsiTimeDelta.Name} should have been compared, but they were not. Please make sure the indicators are getting SPY data"); + } + } + + /// + /// This is used by the regression test system to indicate if the open source Lean repository has the required data to run this algorithm. + /// + public bool CanRunLocally { get; } = true; + + /// + /// This is used by the regression test system to indicate which languages this algorithm is written in. + /// + public List Languages { get; } = new() { Language.CSharp, Language.Python }; + + /// + /// Data Points count of all timeslices of algorithm + /// + public long DataPoints => 290; + + /// + /// Data Points count of the algorithm history + /// + public int AlgorithmHistoryDataPoints => 20; + + /// + /// Final status of the algorithm + /// + public AlgorithmStatus AlgorithmStatus => AlgorithmStatus.Completed; + + /// + /// This is used by the regression test system to indicate what the expected statistics are from running the algorithm + /// + public Dictionary ExpectedStatistics => new Dictionary + { + {"Total Orders", "0"}, + {"Average Win", "0%"}, + {"Average Loss", "0%"}, + {"Compounding Annual Return", "0%"}, + {"Drawdown", "0%"}, + {"Expectancy", "0"}, + {"Start Equity", "100000"}, + {"End Equity", "100000"}, + {"Net Profit", "0%"}, + {"Sharpe Ratio", "0"}, + {"Sortino Ratio", "0"}, + {"Probabilistic Sharpe Ratio", "0%"}, + {"Loss Rate", "0%"}, + {"Win Rate", "0%"}, + {"Profit-Loss Ratio", "0"}, + {"Alpha", "0"}, + {"Beta", "0"}, + {"Annual Standard Deviation", "0"}, + {"Annual Variance", "0"}, + {"Information Ratio", "-5.215"}, + {"Tracking Error", "0.159"}, + {"Treynor Ratio", "0"}, + {"Total Fees", "$0.00"}, + {"Estimated Strategy Capacity", "$0"}, + {"Lowest Capacity Asset", ""}, + {"Portfolio Turnover", "0%"}, + {"OrderListHash", "d41d8cd98f00b204e9800998ecf8427e"} + }; + } +} diff --git a/Algorithm.CSharp/VolumeShareSlippageModelAlgorithm.cs b/Algorithm.CSharp/VolumeShareSlippageModelAlgorithm.cs new file mode 100644 index 000000000000..5f6861c6b935 --- /dev/null +++ b/Algorithm.CSharp/VolumeShareSlippageModelAlgorithm.cs @@ -0,0 +1,135 @@ +/* + * 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.Collections.Generic; +using System.Linq; +using QuantConnect.Algorithm.Framework.Portfolio; +using QuantConnect.Data; +using QuantConnect.Data.UniverseSelection; +using QuantConnect.Interfaces; +using QuantConnect.Orders.Slippage; +using QuantConnect.Securities; + +namespace QuantConnect.Algorithm.CSharp +{ + /// + /// Example algorithm implementing VolumeShareSlippageModel. + /// + public class VolumeShareSlippageModelAlgorithm : QCAlgorithm, IRegressionAlgorithmDefinition + { + private List _longs = new(); + private List _shorts = new(); + + public override void Initialize() + { + SetStartDate(2020, 11, 29); + SetEndDate(2020, 12, 2); + // To set the slippage model to limit to fill only 30% volume of the historical volume, with 5% slippage impact. + SetSecurityInitializer((security) => security.SetSlippageModel(new VolumeShareSlippageModel(0.3m, 0.05m))); + + // Create SPY symbol to explore its constituents. + var spy = QuantConnect.Symbol.Create("SPY", SecurityType.Equity, Market.USA); + + UniverseSettings.Resolution = Resolution.Daily; + // Add universe to trade on the most and least weighted stocks among SPY constituents. + AddUniverse(Universe.ETF(spy, universeFilterFunc: Selection)); + } + + private IEnumerable Selection(IEnumerable constituents) + { + var sortedByDollarVolume = constituents.OrderBy(x => x.Weight).ToList(); + // Add the 10 most weighted stocks to the universe to long later. + _longs = sortedByDollarVolume.TakeLast(10) + .Select(x => x.Symbol) + .ToList(); + // Add the 10 least weighted stocks to the universe to short later. + _shorts = sortedByDollarVolume.Take(10) + .Select(x => x.Symbol) + .ToList(); + + return _longs.Union(_shorts); + } + + public override void OnData(Slice slice) + { + // Equally invest into the selected stocks to evenly dissipate capital risk. + // Dollar neutral of long and short stocks to eliminate systematic risk, only capitalize the popularity gap. + var targets = _longs.Select(symbol => new PortfolioTarget(symbol, 0.05m)).ToList(); + targets.AddRange(_shorts.Select(symbol => new PortfolioTarget(symbol, -0.05m)).ToList()); + + // Liquidate the ones not being the most and least popularity stocks to release fund for higher expected return trades. + SetHoldings(targets, liquidateExistingHoldings: true); + } + + /// + /// This is used by the regression test system to indicate if the open source Lean repository has the required data to run this algorithm. + /// + public bool CanRunLocally { get; } = true; + + /// + /// This is used by the regression test system to indicate which languages this algorithm is written in. + /// + public List Languages { get; } = new() { Language.CSharp, Language.Python }; + + /// + /// Data Points count of all timeslices of algorithm + /// + public long DataPoints => 1035; + + /// + /// Data Points count of the algorithm history + /// + public int AlgorithmHistoryDataPoints => 0; + + /// + /// Final status of the algorithm + /// + public AlgorithmStatus AlgorithmStatus => AlgorithmStatus.Completed; + + /// + /// This is used by the regression test system to indicate what the expected statistics are from running the algorithm + /// + public Dictionary ExpectedStatistics => new Dictionary + { + {"Total Orders", "4"}, + {"Average Win", "0%"}, + {"Average Loss", "0%"}, + {"Compounding Annual Return", "20.900%"}, + {"Drawdown", "0%"}, + {"Expectancy", "0"}, + {"Start Equity", "100000"}, + {"End Equity", "100190.84"}, + {"Net Profit", "0.191%"}, + {"Sharpe Ratio", "9.794"}, + {"Sortino Ratio", "0"}, + {"Probabilistic Sharpe Ratio", "0%"}, + {"Loss Rate", "0%"}, + {"Win Rate", "0%"}, + {"Profit-Loss Ratio", "0"}, + {"Alpha", "0.297"}, + {"Beta", "-0.064"}, + {"Annual Standard Deviation", "0.017"}, + {"Annual Variance", "0"}, + {"Information Ratio", "-18.213"}, + {"Tracking Error", "0.099"}, + {"Treynor Ratio", "-2.695"}, + {"Total Fees", "$4.00"}, + {"Estimated Strategy Capacity", "$4400000000.00"}, + {"Lowest Capacity Asset", "GOOCV VP83T1ZUHROL"}, + {"Portfolio Turnover", "4.22%"}, + {"OrderListHash", "9d2bd0df7c094c393e77f72b7739bfa0"} + }; + } +} diff --git a/Algorithm.Python/ConsolidateHourBarsIntoDailyBarsRegressionAlgorithm.py b/Algorithm.Python/ConsolidateHourBarsIntoDailyBarsRegressionAlgorithm.py new file mode 100644 index 000000000000..f9eedfedb999 --- /dev/null +++ b/Algorithm.Python/ConsolidateHourBarsIntoDailyBarsRegressionAlgorithm.py @@ -0,0 +1,69 @@ +# 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. + +from AlgorithmImports import * + +### +### This regression algorithm asserts the consolidated US equity daily bars from the hour bars exactly matches +### the daily bars returned from the database +### +class ConsolidateHourBarsIntoDailyBarsRegressionAlgorithm(QCAlgorithm): + def initialize(self): + self.set_start_date(2020, 5, 1) + self.set_end_date(2020, 6, 5) + + self.spy = self.add_equity("SPY", Resolution.HOUR).symbol + + # We will use these two indicators to compare the daily consolidated bars equals + # the ones returned from the database. We use this specific type of indicator as + # it depends on its previous values. Thus, if at some point the bars received by + # the indicators differ, so will their final values + self._rsi = RelativeStrengthIndex("First", 15, MovingAverageType.WILDERS) + self.register_indicator(self.spy, self._rsi, Resolution.DAILY, selector= lambda bar: (bar.close + bar.open) / 2) + + # We won't register this indicator as we will update it manually at the end of the + # month, so that we can compare the values of the indicator that received consolidated + # bars and the values of this one + self._rsi_timedelta = RelativeStrengthIndex("Second", 15, MovingAverageType.WILDERS) + self._values = {} + self.count = 0; + self._indicators_compared = False; + + def on_data(self, data: Slice): + if self.is_warming_up: + return + + if data.contains_key(self.spy) and data[self.spy] != None: + if self.time.month == self.end_date.month: + history = self.history[TradeBar](self.spy, self.count, Resolution.DAILY) + for bar in history: + time = bar.end_time.strftime('%Y-%m-%d') + average = (bar.close + bar.open) / 2 + self._rsi_timedelta.update(bar.end_time, average) + if self._rsi_timedelta.current.value != self._values[time]: + raise Exception(f"Both {self._rsi.name} and {self._rsi_timedelta.name} should have the same values, but they differ. {self._rsi.name}: {self._values[time]} | {self._rsi_timedelta.name}: {self._rsi_timedelta.current.value}") + self._indicators_compared = True + self.quit() + else: + time = self.time.strftime('%Y-%m-%d') + self._values[time] = self._rsi.current.value + + # Since the symbol resolution is hour and the symbol is equity, we know the last bar received in a day will + # be at the market close, this is 16h. We need to count how many daily bars were consolidated in order to know + # how many we need to request from the history + if self.time.hour == 16: + self.count += 1 + + def on_end_of_algorithm(self): + if not self._indicators_compared: + raise Exception(f"Indicators {self._rsi.name} and {self._rsi_timedelta.name} should have been compared, but they were not. Please make sure the indicators are getting SPY data") diff --git a/Algorithm.Python/VolumeShareSlippageModelAlgorithm.py b/Algorithm.Python/VolumeShareSlippageModelAlgorithm.py new file mode 100644 index 000000000000..e585ec109c3e --- /dev/null +++ b/Algorithm.Python/VolumeShareSlippageModelAlgorithm.py @@ -0,0 +1,53 @@ +# 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. + +from AlgorithmImports import * +from Orders.Slippage.VolumeShareSlippageModel import VolumeShareSlippageModel + +### +### Example algorithm implementing VolumeShareSlippageModel. +### +class VolumeShareSlippageModelAlgorithm(QCAlgorithm): + longs = [] + shorts = [] + + def initialize(self) -> None: + self.set_start_date(2020, 11, 29) + self.set_end_date(2020, 12, 2) + # To set the slippage model to limit to fill only 30% volume of the historical volume, with 5% slippage impact. + self.set_security_initializer(lambda security: security.set_slippage_model(VolumeShareSlippageModel(0.3, 0.05))) + + # Create SPY symbol to explore its constituents. + spy = Symbol.create("SPY", SecurityType.EQUITY, Market.USA) + + self.universe_settings.resolution = Resolution.DAILY + # Add universe to trade on the most and least weighted stocks among SPY constituents. + self.add_universe(self.universe.etf(spy, universe_filter_func=self.selection)) + + def selection(self, constituents: List[ETFConstituentUniverse]) -> List[Symbol]: + sorted_by_weight = sorted(constituents, key=lambda c: c.weight) + # Add the 10 most weighted stocks to the universe to long later. + self.longs = [c.symbol for c in sorted_by_weight[-10:]] + # Add the 10 least weighted stocks to the universe to short later. + self.shorts = [c.symbol for c in sorted_by_weight[:10]] + + return self.longs + self.shorts + + def on_data(self, slice: Slice) -> None: + # Equally invest into the selected stocks to evenly dissipate capital risk. + # Dollar neutral of long and short stocks to eliminate systematic risk, only capitalize the popularity gap. + targets = [PortfolioTarget(symbol, 0.05) for symbol in self.longs] + targets += [PortfolioTarget(symbol, -0.05) for symbol in self.shorts] + + # Liquidate the ones not being the most and least popularity stocks to release fund for higher expected return trades. + self.set_holdings(targets, liquidate_existing_holdings=True) diff --git a/Common/Data/Consolidators/MarketHourAwareConsolidator.cs b/Common/Data/Consolidators/MarketHourAwareConsolidator.cs index 8fa437885378..2b9565f5cb24 100644 --- a/Common/Data/Consolidators/MarketHourAwareConsolidator.cs +++ b/Common/Data/Consolidators/MarketHourAwareConsolidator.cs @@ -132,7 +132,12 @@ public virtual void Update(IBaseData data) { Initialize(data); - if (_extendedMarketHours || ExchangeHours.IsOpen(data.Time, false)) + // US equity hour data from the database starts at 9am but the exchange opens at 9:30am. Thus, we need to handle + // this case specifically to avoid skipping the first hourly bar. To avoid this, we assert the period is daily, + // the data resolution is hour and the exchange opens at any point in time over the data.Time to data.EndTime interval + if (_extendedMarketHours || + ExchangeHours.IsOpen(data.Time, false) || + (Period == Time.OneDay && (data.EndTime - data.Time == Time.OneHour) && ExchangeHours.IsOpen(data.Time, data.EndTime, false))) { Consolidator.Update(data); } diff --git a/Common/Data/Consolidators/PeriodCountConsolidatorBase.cs b/Common/Data/Consolidators/PeriodCountConsolidatorBase.cs index 386d6a0ad310..d4f94e4ea3de 100644 --- a/Common/Data/Consolidators/PeriodCountConsolidatorBase.cs +++ b/Common/Data/Consolidators/PeriodCountConsolidatorBase.cs @@ -322,10 +322,21 @@ protected DateTime GetRoundedBarTime(DateTime time) protected DateTime GetRoundedBarTime(IBaseData inputData) { var potentialStartTime = GetRoundedBarTime(inputData.Time); - if(_period.HasValue && potentialStartTime + _period < inputData.EndTime) + if (_period.HasValue && potentialStartTime + _period < inputData.EndTime) { - // whops! the end time we were giving is beyond our potential end time, so let's use the giving bars star time instead - potentialStartTime = inputData.Time; + // US equity hour bars from the database starts at 9am but the exchange opens at 9:30am. Thus, the method + // GetRoundedBarTime(inputData.Time) returns the market open of the previous day, which is not consistent + // with the given end time. For that reason we need to handle this case specifically, by calling + // GetRoundedBarTime(inputData.EndTime) as it will return our expected start time: 9:30am + if (inputData.EndTime - inputData.Time == Time.OneHour && potentialStartTime.Date < inputData.Time.Date) + { + potentialStartTime = GetRoundedBarTime(inputData.EndTime); + } + else + { + // whops! the end time we were giving is beyond our potential end time, so let's use the giving bars star time instead + potentialStartTime = inputData.Time; + } } return potentialStartTime; diff --git a/Common/Indicators/RollingWindow.cs b/Common/Indicators/RollingWindow.cs index d2f7ed526813..b1172752499f 100644 --- a/Common/Indicators/RollingWindow.cs +++ b/Common/Indicators/RollingWindow.cs @@ -16,6 +16,7 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Runtime.CompilerServices; using System.Threading; namespace QuantConnect.Indicators @@ -174,7 +175,7 @@ public T this [int i] return default; } - return _list[(_list.Count + _tail - i - 1) % _list.Count]; + return _list[GetListIndex(i, _list.Count, _tail)]; } finally { @@ -206,7 +207,7 @@ public T this [int i] } } - _list[(_list.Count + _tail - i - 1) % _list.Count] = value; + _list[GetListIndex(i, _list.Count, _tail)] = value; } finally { @@ -244,18 +245,20 @@ public bool IsReady /// 1 public IEnumerator GetEnumerator() { - // we make a copy on purpose so the enumerator isn't tied - // to a mutable object, well it is still mutable but out of scope - var temp = new List(_list.Count); try { _listLock.EnterReadLock(); - for (int i = 0; i < _list.Count; i++) + // we make a copy on purpose so the enumerator isn't tied + // to a mutable object, well it is still mutable but out of scope + var count = _list.Count; + var temp = new T[count]; + for (int i = 0; i < count; i++) { - temp.Add(this[i]); + temp[i] = _list[GetListIndex(i, count, _tail)]; } - return temp.GetEnumerator(); + + return ((IEnumerable) temp).GetEnumerator(); } finally { @@ -325,6 +328,12 @@ public void Reset() } } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int GetListIndex(int index, int listCount, int tail) + { + return (listCount + tail - index - 1) % listCount; + } + private void Resize(int size) { try diff --git a/Common/Orders/Slippage/VolumeShareSlippageModel.py b/Common/Orders/Slippage/VolumeShareSlippageModel.py new file mode 100644 index 000000000000..3921c414a741 --- /dev/null +++ b/Common/Orders/Slippage/VolumeShareSlippageModel.py @@ -0,0 +1,61 @@ +# 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. + +from AlgorithmImports import * + +class VolumeShareSlippageModel: + '''Represents a slippage model that is calculated by multiplying the price impact constant by the square of the ratio of the order to the total volume.''' + + def __init__(self, volume_limit: float = 0.025, price_impact: float = 0.1) -> None: + '''Initializes a new instance of the "VolumeShareSlippageModel" class + Args: + volume_limit: + price_impact: Defines how large of an impact the order will have on the price calculation''' + self.volume_limit = volume_limit + self.price_impact = price_impact + + def get_slippage_approximation(self, asset: Security, order: Order) -> float: + '''Slippage Model. Return a decimal cash slippage approximation on the order. + Args: + asset: The Security instance of the security of the order. + order: The Order instance being filled.''' + last_data = asset.get_last_data() + if not last_data: + return 0 + + bar_volume = 0 + slippage_percent = self.volume_limit * self.volume_limit * self.price_impact + + if last_data.data_type == MarketDataType.TRADE_BAR: + bar_volume = last_data.volume + elif last_data.data_type == MarketDataType.QUOTE_BAR: + bar_volume = last_data.last_bid_size if order.direction == OrderDirection.BUY else last_data.last_ask_size + else: + raise InvalidOperationException(Messages.VolumeShareSlippageModel.invalid_market_data_type(last_data)) + + # If volume is zero or negative, we use the maximum slippage percentage since the impact of any quantity is infinite + # In FX/CFD case, we issue a warning and return zero slippage + if bar_volume <= 0: + security_type = asset.symbol.id.security_type + if security_type == SecurityType.CFD or security_type == SecurityType.FOREX or security_type == SecurityType.CRYPTO: + Log.error(Messages.VolumeShareSlippageModel.volume_not_reported_for_market_data_type(security_type)) + return 0 + + Log.error(Messages.VolumeShareSlippageModel.negative_or_zero_bar_volume(bar_volume, slippage_percent)) + else: + # Ratio of the order to the total volume + volume_share = min(order.absolute_quantity / bar_volume, self.volume_limit) + + slippage_percent = volume_share * volume_share * self.price_impact + + return slippage_percent * last_data.Value; diff --git a/Common/QuantConnect.csproj b/Common/QuantConnect.csproj index 489d5b15c1bb..c438ad020749 100644 --- a/Common/QuantConnect.csproj +++ b/Common/QuantConnect.csproj @@ -67,5 +67,8 @@ PreserveNewest true + + PreserveNewest + diff --git a/Tests/Common/Data/MarketHourAwareConsolidatorTests.cs b/Tests/Common/Data/MarketHourAwareConsolidatorTests.cs index 14dc97c84367..f025f3dac927 100644 --- a/Tests/Common/Data/MarketHourAwareConsolidatorTests.cs +++ b/Tests/Common/Data/MarketHourAwareConsolidatorTests.cs @@ -117,6 +117,71 @@ public void Daily(bool strictEndTime) Assert.AreEqual(1, latestBar.Low); } + [Test] + public void BarIsSkippedWhenDataResolutionIsNotHourAndMarketIsClose() + { + var symbol = Symbols.SPY; + using var consolidator = new MarketHourAwareConsolidator(true, Resolution.Daily, typeof(TradeBar), TickType.Trade, false); + var consolidatedBarsCount = 0; + TradeBar latestBar = null; + + consolidator.DataConsolidated += (sender, bar) => + { + latestBar = (TradeBar)bar; + consolidatedBarsCount++; + }; + + var time = new DateTime(2020, 05, 01, 09, 30, 0); + // this bar will be ignored because it's during market closed hours and the bar resolution is not Hour + consolidator.Update(new TradeBar() { Time = time.Subtract(Time.OneMinute), Period = Time.OneMinute, Symbol = symbol, Open = 1 }); + Assert.IsNull(latestBar); + Assert.AreEqual(0, consolidatedBarsCount); + } + + [Test] + public void DailyBarCanBeConsolidatedFromHourData() + { + var symbol = Symbols.SPY; + using var consolidator = new MarketHourAwareConsolidator(true, Resolution.Daily, typeof(TradeBar), TickType.Trade, false); + var consolidatedBarsCount = 0; + TradeBar latestBar = null; + + consolidator.DataConsolidated += (sender, bar) => + { + latestBar = (TradeBar)bar; + consolidatedBarsCount++; + }; + + var time = new DateTime(2020, 05, 01, 09, 0, 0); + var hourBars = new List() + { + new TradeBar() { Time = time, Period = Time.OneHour, Symbol = symbol, Open = 2 }, + new TradeBar() { Time = time.AddHours(1), Period = Time.OneHour, Symbol = symbol, High = 200 }, + new TradeBar() { Time = time.AddHours(2), Period = Time.OneHour, Symbol = symbol, Low = 0.02m }, + new TradeBar() { Time = time.AddHours(3), Period = Time.OneHour, Symbol = symbol, Close = 20 }, + new TradeBar() { Time = time.AddHours(4), Period = Time.OneHour, Symbol = symbol, Open = 3 }, + new TradeBar() { Time = time.AddHours(5), Period = Time.OneHour, Symbol = symbol, High = 300 }, + new TradeBar() { Time = time.AddHours(6), Period = Time.OneHour, Symbol = symbol, Low = 0.03m, Close = 30 }, + }; + + foreach (var bar in hourBars) + { + consolidator.Update(bar); + } + + consolidator.Scan(time.AddHours(7)); + + // Assert that the bar emitted + Assert.IsNotNull(latestBar); + Assert.AreEqual(time.AddHours(7), latestBar.EndTime); + Assert.AreEqual(time.AddMinutes(30), latestBar.Time); + Assert.AreEqual(1, consolidatedBarsCount); + Assert.AreEqual(2, latestBar.Open); + Assert.AreEqual(300, latestBar.High); + Assert.AreEqual(0.02, latestBar.Low); + Assert.AreEqual(30, latestBar.Close); + } + [TestCase(true)] [TestCase(false)] public void DailyExtendedMarketHours(bool strictEndTime) diff --git a/Tests/Indicators/RollingWindowTests.cs b/Tests/Indicators/RollingWindowTests.cs index c451e8838b71..5c7001c50c6f 100644 --- a/Tests/Indicators/RollingWindowTests.cs +++ b/Tests/Indicators/RollingWindowTests.cs @@ -114,6 +114,12 @@ public void EnumeratesAsExpected() Assert.AreEqual(2, inOrder[0]); Assert.AreEqual(1, inOrder[1]); Assert.AreEqual(0, inOrder[2]); + + window.Add(3); + var inOrder2 = window.ToList(); + Assert.AreEqual(3, inOrder2[0]); + Assert.AreEqual(2, inOrder2[1]); + Assert.AreEqual(1, inOrder2[2]); } [Test]