From 9865ce9815809faeb29132b16dddf9535f525ca5 Mon Sep 17 00:00:00 2001 From: Louis Szeto <56447733+LouisSzeto@users.noreply.github.com> Date: Tue, 10 Dec 2024 02:37:35 +0800 Subject: [PATCH] Add example on volume share slippage model (#8437) * add csharp example * add python example * regression * add pythom implementation as example model * peer review * dependencies * fix regression test --- .../VolumeShareSlippageModelAlgorithm.cs | 135 ++++++++++++++++++ .../VolumeShareSlippageModelAlgorithm.py | 53 +++++++ .../Slippage/VolumeShareSlippageModel.py | 61 ++++++++ Common/QuantConnect.csproj | 3 + 4 files changed, 252 insertions(+) create mode 100644 Algorithm.CSharp/VolumeShareSlippageModelAlgorithm.cs create mode 100644 Algorithm.Python/VolumeShareSlippageModelAlgorithm.py create mode 100644 Common/Orders/Slippage/VolumeShareSlippageModel.py 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/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/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 e9fac89bc4a2..088dfd992381 100644 --- a/Common/QuantConnect.csproj +++ b/Common/QuantConnect.csproj @@ -63,5 +63,8 @@ PreserveNewest true + + PreserveNewest +