diff --git a/Algorithm.CSharp/AlgorithmSlippageModelRegressionAlgorithm.cs b/Algorithm.CSharp/AlgorithmSlippageModelRegressionAlgorithm.cs new file mode 100644 index 000000000000..b1bc4de5150d --- /dev/null +++ b/Algorithm.CSharp/AlgorithmSlippageModelRegressionAlgorithm.cs @@ -0,0 +1,150 @@ +/* + * 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 QuantConnect.Data; +using QuantConnect.Interfaces; +using QuantConnect.Orders; +using QuantConnect.Orders.Slippage; +using QuantConnect.Securities; + +namespace QuantConnect.Algorithm.CSharp +{ + /// + /// Regression algorithm asserting that the algorithm-level + /// applies a custom subclass to all securities, + /// with per-security models set afterwards taking precedence + /// + public class AlgorithmSlippageModelRegressionAlgorithm : QCAlgorithm, IRegressionAlgorithmDefinition + { + private CustomSlippageModel _slippageModel; + private Symbol _spy; + private Symbol _ibm; + + public override void Initialize() + { + SetStartDate(2013, 10, 07); + SetEndDate(2013, 10, 11); + SetCash(100000); + + SetSecurityInitializer(new BrokerageModelSecurityInitializer(BrokerageModel, new FuncSecuritySeeder(GetLastKnownPrices))); + + _slippageModel = new CustomSlippageModel(); + SetSlippageModel(_slippageModel); + + _spy = AddEquity("SPY", Resolution.Minute).Symbol; + var ibm = AddEquity("IBM", Resolution.Minute); + // per-security models set after the algorithm-level model take precedence for that security + ibm.SetSlippageModel(NullSlippageModel.Instance); + _ibm = ibm.Symbol; + } + + public override void OnData(Slice slice) + { + if (!Portfolio.Invested) + { + SetHoldings(_spy, 0.5m); + SetHoldings(_ibm, 0.5m); + } + } + + public override void OnEndOfAlgorithm() + { + if (Securities[_spy].SlippageModel != _slippageModel) + { + throw new RegressionTestException("Expected SPY to use the algorithm-level slippage model"); + } + if (Securities[_ibm].SlippageModel != NullSlippageModel.Instance) + { + throw new RegressionTestException("Expected the per-security slippage model to take precedence for IBM"); + } + if (_slippageModel.CallCount == 0) + { + throw new RegressionTestException("Expected the algorithm-level slippage model to have been used"); + } + } + + private class CustomSlippageModel : SlippageModel + { + public int CallCount { get; private set; } + + public override decimal GetSlippageApproximation(Security asset, Order order) + { + CallCount++; + return 0.05m; + } + } + + /// + /// 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 => 7843; + + /// + /// 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", "2"}, + {"Average Win", "0%"}, + {"Average Loss", "0%"}, + {"Compounding Annual Return", "343.438%"}, + {"Drawdown", "2.100%"}, + {"Expectancy", "0"}, + {"Start Equity", "100000"}, + {"End Equity", "101922.49"}, + {"Net Profit", "1.922%"}, + {"Sharpe Ratio", "10.891"}, + {"Sortino Ratio", "0"}, + {"Probabilistic Sharpe Ratio", "66.279%"}, + {"Loss Rate", "0%"}, + {"Win Rate", "0%"}, + {"Profit-Loss Ratio", "0"}, + {"Alpha", "0.565"}, + {"Beta", "0.993"}, + {"Annual Standard Deviation", "0.232"}, + {"Annual Variance", "0.054"}, + {"Information Ratio", "7.794"}, + {"Tracking Error", "0.071"}, + {"Treynor Ratio", "2.545"}, + {"Total Fees", "$3.55"}, + {"Estimated Strategy Capacity", "$16000000.00"}, + {"Lowest Capacity Asset", "IBM R735QTJ8XC9X"}, + {"Portfolio Turnover", "19.93%"}, + {"Drawdown Recovery", "3"}, + {"OrderListHash", "c4766cde15ad208b5f6c12c6a0af59b9"} + }; + } +} diff --git a/Algorithm.Python/AlgorithmSlippageModelRegressionAlgorithm.py b/Algorithm.Python/AlgorithmSlippageModelRegressionAlgorithm.py new file mode 100644 index 000000000000..737ac4b8c67b --- /dev/null +++ b/Algorithm.Python/AlgorithmSlippageModelRegressionAlgorithm.py @@ -0,0 +1,71 @@ +# 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 * + +### +### Regression algorithm asserting that the algorithm-level set_slippage_model() applies a custom +### SlippageModel subclass to all securities, with per-security models set afterwards taking precedence. +### It also asserts that BrokerageModelSecurityInitializer accepts a plain callable as security seeder +### and that None is accepted by the framework model setters as the null model. +### +class AlgorithmSlippageModelRegressionAlgorithm(QCAlgorithm): + + def initialize(self): + self.set_start_date(2013, 10, 7) + self.set_end_date(2013, 10, 11) + self.set_cash(100000) + + # the security seeder can be a plain callable, it gets wrapped into a FuncSecuritySeeder + self.set_security_initializer(BrokerageModelSecurityInitializer(self.brokerage_model, self.get_last_known_prices)) + + # None is accepted by the framework model setters as the null model + self.set_risk_management(None) + self.set_execution(None) + + self._slippage_model = CustomSlippageModel() + self.set_slippage_model(self._slippage_model) + + self._spy = self.add_equity("SPY", Resolution.MINUTE).symbol + ibm = self.add_equity("IBM", Resolution.MINUTE) + # per-security models set after the algorithm-level model take precedence for that security + ibm.set_slippage_model(NullSlippageModel.INSTANCE) + self._ibm = ibm.symbol + + def on_data(self, data): + if not self.portfolio.invested: + self.set_holdings(self._spy, 0.5) + self.set_holdings(self._ibm, 0.5) + + def on_end_of_algorithm(self): + if not isinstance(self.securities[self._ibm].slippage_model, NullSlippageModel): + raise AssertionError("Expected the per-security slippage model to take precedence for IBM") + if self._slippage_model.call_count == 0: + raise AssertionError("Expected the algorithm-level slippage model to have been used") + if not isinstance(self.risk_management, NullRiskManagementModel): + raise AssertionError("Expected set_risk_management(None) to set the null risk management model") + if not isinstance(self.execution, NullExecutionModel): + raise AssertionError("Expected set_execution(None) to set the null execution model") + +### +### Custom slippage model derived from the C# SlippageModel base class. +### The ISlippageModel interface cannot be used as a Python base class. +### +class CustomSlippageModel(SlippageModel): + def __init__(self): + super().__init__() + self.call_count = 0 + + def get_slippage_approximation(self, asset, order): + self.call_count += 1 + return 0.05 diff --git a/Algorithm/QCAlgorithm.Framework.Python.cs b/Algorithm/QCAlgorithm.Framework.Python.cs index 2848b5ce32dc..5bcb8e6a84df 100644 --- a/Algorithm/QCAlgorithm.Framework.Python.cs +++ b/Algorithm/QCAlgorithm.Framework.Python.cs @@ -32,6 +32,13 @@ public partial class QCAlgorithm [DocumentationAttribute(AlgorithmFramework)] public void SetAlpha(PyObject alpha) { + // None is accepted as the null model, e.g. to disable a model a template set up + if (alpha is null || alpha.IsNone()) + { + SetAlpha(new NullAlphaModel()); + return; + } + Alpha = PythonUtil.CreateInstanceOrWrapper( alpha, py => new AlphaModelPythonWrapper(py) @@ -60,6 +67,13 @@ public void AddAlpha(PyObject alpha) [DocumentationAttribute(TradingAndOrders)] public void SetExecution(PyObject execution) { + // None is accepted as the null model, e.g. to disable a model a template set up + if (execution is null || execution.IsNone()) + { + SetExecution(new NullExecutionModel()); + return; + } + Execution = PythonUtil.CreateInstanceOrWrapper( execution, py => new ExecutionModelPythonWrapper(py) @@ -74,6 +88,13 @@ public void SetExecution(PyObject execution) [DocumentationAttribute(TradingAndOrders)] public void SetPortfolioConstruction(PyObject portfolioConstruction) { + // None is accepted as the null model, e.g. to disable a model a template set up + if (portfolioConstruction is null || portfolioConstruction.IsNone()) + { + SetPortfolioConstruction(new NullPortfolioConstructionModel()); + return; + } + PortfolioConstruction = PythonUtil.CreateInstanceOrWrapper( portfolioConstruction, py => new PortfolioConstructionModelPythonWrapper(py) @@ -88,6 +109,13 @@ public void SetPortfolioConstruction(PyObject portfolioConstruction) [DocumentationAttribute(Universes)] public void SetUniverseSelection(PyObject universeSelection) { + // None is accepted as the null model, e.g. to disable a model a template set up + if (universeSelection is null || universeSelection.IsNone()) + { + SetUniverseSelection(new NullUniverseSelectionModel()); + return; + } + UniverseSelection = PythonUtil.CreateInstanceOrWrapper( universeSelection, py => new UniverseSelectionModelPythonWrapper(py) @@ -117,6 +145,15 @@ public void AddUniverseSelection(PyObject universeSelection) [DocumentationAttribute(TradingAndOrders)] public void SetRiskManagement(PyObject riskManagement) { + // None is accepted as the null model, e.g. to disable a model a template set up. + // Without this, set_risk_management(None) failed with + // "IRiskManagementModel must be fully implemented. Please implement these missing methods on NoneType: ManageRisk" + if (riskManagement is null || riskManagement.IsNone()) + { + SetRiskManagement(new NullRiskManagementModel()); + return; + } + RiskManagement = PythonUtil.CreateInstanceOrWrapper( riskManagement, py => new RiskManagementModelPythonWrapper(py) diff --git a/Algorithm/QCAlgorithm.Python.cs b/Algorithm/QCAlgorithm.Python.cs index 9859cbc4094b..5f993ad6992e 100644 --- a/Algorithm/QCAlgorithm.Python.cs +++ b/Algorithm/QCAlgorithm.Python.cs @@ -32,6 +32,7 @@ using QuantConnect.Util; using QuantConnect.Interfaces; using QuantConnect.Orders; +using QuantConnect.Orders.Slippage; using QuantConnect.Commands; using QuantConnect.Api; @@ -1437,6 +1438,23 @@ public void SetRiskFreeInterestRateModel(PyObject model) SetRiskFreeInterestRateModel(riskFreeInterestRateModel); } + /// + /// Sets the slippage model for all securities in the algorithm, including securities added afterwards, + /// for instance through universe selection + /// + /// Individual securities can override this model by calling + /// after this method, e.g. from + /// for securities added by universe selection + /// The slippage model to use + [DocumentationAttribute(Modeling)] + public void SetSlippageModel(PyObject slippageModel) + { + SetSlippageModel(PythonUtil.CreateInstanceOrWrapper( + slippageModel, + py => new SlippageModelPythonWrapper(py) + )); + } + /// /// Sets the security initializer function, used to initialize/configure securities after creation /// diff --git a/Algorithm/QCAlgorithm.cs b/Algorithm/QCAlgorithm.cs index daf195a7a1e1..ff13d221ff00 100644 --- a/Algorithm/QCAlgorithm.cs +++ b/Algorithm/QCAlgorithm.cs @@ -27,6 +27,8 @@ using QuantConnect.Interfaces; using QuantConnect.Notifications; using QuantConnect.Orders; +using QuantConnect.Orders.Slippage; +using System.Collections.Specialized; using QuantConnect.Parameters; using QuantConnect.Scheduling; using QuantConnect.Securities; @@ -146,6 +148,7 @@ public partial class QCAlgorithm : MarshalByRefObject, IAlgorithm // flips to true when the user private bool _userSetSecurityInitializer; + private ISlippageModel _slippageModel; // warmup resolution variables private TimeSpan? _warmupTimeSpan; @@ -1407,10 +1410,56 @@ public void SetBrokerageModel(IBrokerageModel model) // restore the saved leverage security.SetLeverage(leverage); + + // restore the algorithm-level slippage model, if set, over the brokerage default + if (_slippageModel != null) + { + security.SetSlippageModel(_slippageModel); + } } } } + /// + /// Sets the slippage model for all securities in the algorithm, including securities added afterwards, + /// for instance through universe selection + /// + /// Individual securities can override this model by calling + /// after this method, e.g. from + /// for securities added by universe selection + /// The slippage model to use + [DocumentationAttribute(Modeling)] + public void SetSlippageModel(ISlippageModel slippageModel) + { + if (slippageModel == null) + { + throw new ArgumentNullException(nameof(slippageModel)); + } + + if (_slippageModel == null) + { + // First call: subscribe so that securities added later also get the model. + // The add event fires after the security initializer (e.g. BrokerageModelSecurityInitializer) + // has run, so this model takes precedence over the brokerage default + Securities.CollectionChanged += (sender, changedEventArgs) => + { + if (changedEventArgs.Action == NotifyCollectionChangedAction.Add) + { + foreach (Security security in changedEventArgs.NewItems) + { + security.SetSlippageModel(_slippageModel); + } + } + }; + } + _slippageModel = slippageModel; + + foreach (var kvp in Securities) + { + kvp.Value.SetSlippageModel(slippageModel); + } + } + /// /// Sets the implementation used to handle messages from the brokerage. /// The default implementation will forward messages to debug or error diff --git a/Common/Orders/Slippage/AlphaStreamsSlippageModel.cs b/Common/Orders/Slippage/AlphaStreamsSlippageModel.cs index 5158247cee7e..9f58201b9cc4 100644 --- a/Common/Orders/Slippage/AlphaStreamsSlippageModel.cs +++ b/Common/Orders/Slippage/AlphaStreamsSlippageModel.cs @@ -21,7 +21,7 @@ namespace QuantConnect.Orders.Slippage /// /// Represents a slippage model that uses a constant percentage of slip /// - public class AlphaStreamsSlippageModel : ISlippageModel + public class AlphaStreamsSlippageModel : SlippageModel { private const decimal _slippagePercent = 0.0001m; @@ -33,7 +33,7 @@ public AlphaStreamsSlippageModel() { } /// /// Return a decimal cash slippage approximation on the order. /// - public decimal GetSlippageApproximation(Security asset, Order order) + public override decimal GetSlippageApproximation(Security asset, Order order) { if (asset.Type != SecurityType.Equity) { diff --git a/Common/Orders/Slippage/ConstantSlippageModel.cs b/Common/Orders/Slippage/ConstantSlippageModel.cs index 43e348060b68..ed838187dbb3 100644 --- a/Common/Orders/Slippage/ConstantSlippageModel.cs +++ b/Common/Orders/Slippage/ConstantSlippageModel.cs @@ -21,7 +21,7 @@ namespace QuantConnect.Orders.Slippage /// /// Represents a slippage model that uses a constant percentage of slip /// - public class ConstantSlippageModel : ISlippageModel + public class ConstantSlippageModel : SlippageModel { private readonly decimal _slippagePercent; /// @@ -36,7 +36,7 @@ public ConstantSlippageModel(decimal slippagePercent) /// /// Slippage Model. Return a decimal cash slippage approximation on the order. /// - public decimal GetSlippageApproximation(Security asset, Order order) + public override decimal GetSlippageApproximation(Security asset, Order order) { var lastData = asset.GetLastData(); if (lastData == null) return 0; diff --git a/Common/Orders/Slippage/MarketImpactSlippageModel.cs b/Common/Orders/Slippage/MarketImpactSlippageModel.cs index 394e05a69486..79f69cabf1c7 100644 --- a/Common/Orders/Slippage/MarketImpactSlippageModel.cs +++ b/Common/Orders/Slippage/MarketImpactSlippageModel.cs @@ -38,7 +38,7 @@ namespace QuantConnect.Orders.Slippage /// the market regime is not taken into account, /// and the market environment does not have many market makers at that time, /// so it is recommend to recalibrate with reference to the original paper. - public class MarketImpactSlippageModel : ISlippageModel + public class MarketImpactSlippageModel : SlippageModel { private readonly IAlgorithm _algorithm; private readonly bool _nonNegative; @@ -94,7 +94,7 @@ public MarketImpactSlippageModel(IAlgorithm algorithm, bool nonNegative = true, /// /// Slippage Model. Return a decimal cash slippage approximation on the order. /// - public decimal GetSlippageApproximation(Security asset, Order order) + public override decimal GetSlippageApproximation(Security asset, Order order) { if (asset.Type == SecurityType.Forex || asset.Type == SecurityType.Cfd) { diff --git a/Common/Orders/Slippage/NullSlippageModel.cs b/Common/Orders/Slippage/NullSlippageModel.cs index d447f2beadcb..f0ef8d8579d5 100644 --- a/Common/Orders/Slippage/NullSlippageModel.cs +++ b/Common/Orders/Slippage/NullSlippageModel.cs @@ -20,7 +20,7 @@ namespace QuantConnect.Orders.Slippage /// /// Null slippage model, which provider no slippage /// - public sealed class NullSlippageModel : ISlippageModel + public sealed class NullSlippageModel : SlippageModel { /// /// The null slippage model instance @@ -30,7 +30,7 @@ public sealed class NullSlippageModel : ISlippageModel /// /// Will return no slippage /// - public decimal GetSlippageApproximation(Security asset, Order order) + public override decimal GetSlippageApproximation(Security asset, Order order) { return 0; } diff --git a/Common/Orders/Slippage/SlippageModel.cs b/Common/Orders/Slippage/SlippageModel.cs new file mode 100644 index 000000000000..2f1db4c36265 --- /dev/null +++ b/Common/Orders/Slippage/SlippageModel.cs @@ -0,0 +1,41 @@ +/* + * 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.Securities; + +namespace QuantConnect.Orders.Slippage +{ + /// + /// Base class for any slippage model. Returns no slippage by default + /// + /// Please use as the base class for + /// any implementations of . Python algorithms + /// must derive from this class (or implement a plain class with a + /// get_slippage_approximation method) instead of the + /// interface, which cannot be used as a Python base class + public class SlippageModel : ISlippageModel + { + /// + /// Slippage Model. Return a decimal cash slippage approximation on the order. + /// + /// The security being traded + /// The order being filled + /// The slippage of the order in units of the account currency + public virtual decimal GetSlippageApproximation(Security asset, Order order) + { + return 0; + } + } +} diff --git a/Common/Orders/Slippage/VolumeShareSlippageModel.cs b/Common/Orders/Slippage/VolumeShareSlippageModel.cs index 2841182f8bfa..bc91dd769bd1 100644 --- a/Common/Orders/Slippage/VolumeShareSlippageModel.cs +++ b/Common/Orders/Slippage/VolumeShareSlippageModel.cs @@ -24,7 +24,7 @@ namespace QuantConnect.Orders.Slippage /// 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. /// - public class VolumeShareSlippageModel : ISlippageModel + public class VolumeShareSlippageModel : SlippageModel { private readonly decimal _priceImpact; private readonly decimal _volumeLimit; @@ -43,7 +43,7 @@ public VolumeShareSlippageModel(decimal volumeLimit = 0.025m, decimal priceImpac /// /// Slippage Model. Return a decimal cash slippage approximation on the order. /// - public decimal GetSlippageApproximation(Security asset, Order order) + public override decimal GetSlippageApproximation(Security asset, Order order) { var lastData = asset.GetLastData(); if (lastData == null) return 0; diff --git a/Common/Python/PythonWrapper.cs b/Common/Python/PythonWrapper.cs index 127f52ab13b7..bb1fa0801708 100644 --- a/Common/Python/PythonWrapper.cs +++ b/Common/Python/PythonWrapper.cs @@ -60,7 +60,11 @@ public static PyObject ValidateImplementationOf(this PyObject model) continue; } } - missingMembers.Add(member.Name); + // Render the expected python signature (snake-cased name and parameter names) + // so the user knows exactly what to implement, e.g. "get_slippage_approximation(asset, order)" + missingMembers.Add(method != null + ? $"{method.Name.ToSnakeCase()}({string.Join(", ", method.GetParameters().Select(parameter => parameter.Name.ToSnakeCase()))})" + : member.Name.ToSnakeCase()); } } diff --git a/Common/Securities/BrokerageModelSecurityInitializer.cs b/Common/Securities/BrokerageModelSecurityInitializer.cs index b64ac0bcae7d..d2b4ee74b17a 100644 --- a/Common/Securities/BrokerageModelSecurityInitializer.cs +++ b/Common/Securities/BrokerageModelSecurityInitializer.cs @@ -14,6 +14,8 @@ * */ +using System; +using Python.Runtime; using QuantConnect.Brokerages; namespace QuantConnect.Securities @@ -49,6 +51,43 @@ public BrokerageModelSecurityInitializer(IBrokerageModel brokerageModel, ISecuri _securitySeeder = securitySeeder; } + /// + /// Initializes a new instance of the class + /// for the specified algorithm + /// + /// The brokerage model used to initialize the security models + /// An instance or a Python callable, like + /// the algorithm's get_last_known_price method, used to seed the initial price of the security. + /// It can also be None, in which case no seeding is performed + public BrokerageModelSecurityInitializer(IBrokerageModel brokerageModel, PyObject securitySeeder) + { + _brokerageModel = brokerageModel; + using (Py.GIL()) + { + if (securitySeeder is null || securitySeeder.IsNone()) + { + _securitySeeder = SecuritySeeder.Null; + } + else if (securitySeeder.TryConvert(out var seeder)) + { + _securitySeeder = seeder; + } + else if (securitySeeder.IsCallable()) + { + // Wrap python callables, like a get_last_known_price method reference or a lambda, + // the same way QCAlgorithm.SetSecurityInitializer accepts a function + _securitySeeder = new FuncSecuritySeeder(securitySeeder); + } + else + { + throw new ArgumentException( + $"BrokerageModelSecurityInitializer(): unsupported security seeder '{securitySeeder.GetPythonType().Name}'. " + + "Please provide an ISecuritySeeder instance (e.g. FuncSecuritySeeder), a callable taking a Security " + + "and returning its seed data (e.g. self.get_last_known_price), or None to skip seeding."); + } + } + } + /// /// Initializes the specified security by setting up the models /// diff --git a/Tests/Algorithm/AlgorithmModelsTests.cs b/Tests/Algorithm/AlgorithmModelsTests.cs new file mode 100644 index 000000000000..f87147bacd59 --- /dev/null +++ b/Tests/Algorithm/AlgorithmModelsTests.cs @@ -0,0 +1,171 @@ +/* + * 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; +using NUnit.Framework; +using Python.Runtime; +using QuantConnect.Algorithm; +using QuantConnect.Brokerages; +using QuantConnect.Algorithm.Framework.Alphas; +using QuantConnect.Algorithm.Framework.Execution; +using QuantConnect.Algorithm.Framework.Portfolio; +using QuantConnect.Algorithm.Framework.Risk; +using QuantConnect.Algorithm.Framework.Selection; +using QuantConnect.Orders.Slippage; +using QuantConnect.Python; +using QuantConnect.Tests.Engine.DataFeeds; + +namespace QuantConnect.Tests.Algorithm +{ + [TestFixture] + public class AlgorithmModelsTests + { + private QCAlgorithm _algorithm; + + [SetUp] + public void SetUp() + { + _algorithm = new QCAlgorithm(); + _algorithm.SubscriptionManager.SetDataManager(new DataManagerStub(_algorithm)); + } + + [Test] + public void NoneFrameworkModelsAreAcceptedAsNullModels() + { + // Reproduces 'self.set_risk_management(None)' raising + // "IRiskManagementModel must be fully implemented. Please implement these missing methods on NoneType: ManageRisk" + using (Py.GIL()) + { + var module = PyModule.FromString(nameof(NoneFrameworkModelsAreAcceptedAsNullModels), @" +def set_none_models(algo): + algo.set_alpha(None) + algo.set_execution(None) + algo.set_portfolio_construction(None) + algo.set_risk_management(None) + algo.set_universe_selection(None) +"); + Assert.DoesNotThrow(() => module.GetAttr("set_none_models").Invoke(_algorithm.ToPython())); + } + + Assert.IsInstanceOf(_algorithm.Alpha); + Assert.IsInstanceOf(_algorithm.Execution); + Assert.IsInstanceOf(_algorithm.PortfolioConstruction); + Assert.IsInstanceOf(_algorithm.RiskManagement); + Assert.IsInstanceOf(_algorithm.UniverseSelection); + } + + [Test] + public void AlgorithmSlippageModelIsAppliedToExistingAndFutureSecurities() + { + var spy = _algorithm.AddEquity("SPY", Resolution.Daily); + var model = new ConstantSlippageModel(0.5m); + _algorithm.SetSlippageModel(model); + Assert.AreSame(model, spy.SlippageModel); + + var ibm = _algorithm.AddEquity("IBM", Resolution.Daily); + Assert.AreSame(model, ibm.SlippageModel); + } + + [Test] + public void PerSecuritySlippageModelOverridesAlgorithmLevelModel() + { + var model = new ConstantSlippageModel(0.5m); + _algorithm.SetSlippageModel(model); + var spy = _algorithm.AddEquity("SPY", Resolution.Daily); + var ibm = _algorithm.AddEquity("IBM", Resolution.Daily); + + // per-security models set after the algorithm-level model take precedence for that security + ibm.SetSlippageModel(NullSlippageModel.Instance); + Assert.AreSame(model, spy.SlippageModel); + Assert.AreSame(NullSlippageModel.Instance, ibm.SlippageModel); + + // a new algorithm-level model is applied to all securities again + var newModel = new ConstantSlippageModel(0.1m); + _algorithm.SetSlippageModel(newModel); + Assert.AreSame(newModel, spy.SlippageModel); + Assert.AreSame(newModel, ibm.SlippageModel); + } + + [Test] + public void AlgorithmSlippageModelSurvivesSetBrokerageModel() + { + var spy = _algorithm.AddEquity("SPY", Resolution.Daily); + var model = new ConstantSlippageModel(0.5m); + _algorithm.SetSlippageModel(model); + + // SetBrokerageModel re-initializes existing securities with the brokerage default models, + // the algorithm-level slippage model must survive it + _algorithm.SetBrokerageModel(BrokerageName.InteractiveBrokersBrokerage); + Assert.AreSame(model, spy.SlippageModel); + } + + [Test] + public void AlgorithmSlippageModelRejectsNull() + { + Assert.Throws(() => _algorithm.SetSlippageModel((ISlippageModel)null)); + } + + [Test] + public void AlgorithmSlippageModelIsAppliedToAllSecurities_Python() + { + // Reproduces "'MyAlgorithm' object has no attribute 'set_slippage_model'" + var spy = _algorithm.AddEquity("SPY", Resolution.Daily); + + using (Py.GIL()) + { + var module = PyModule.FromString(nameof(AlgorithmSlippageModelIsAppliedToAllSecurities_Python), @" +from AlgorithmImports import * + +def set_model(algo): + algo.set_slippage_model(ConstantSlippageModel(0.5)) +"); + Assert.DoesNotThrow(() => module.GetAttr("set_model").Invoke(_algorithm.ToPython())); + } + + Assert.IsInstanceOf(spy.SlippageModel); + + // securities added after the algorithm-level model is set also get it + var ibm = _algorithm.AddEquity("IBM", Resolution.Daily); + Assert.IsInstanceOf(ibm.SlippageModel); + } + + [Test] + public void PythonCustomAlgorithmSlippageModelIsAppliedToAllSecurities() + { + var spy = _algorithm.AddEquity("SPY", Resolution.Daily); + + using (Py.GIL()) + { + var module = PyModule.FromString(nameof(PythonCustomAlgorithmSlippageModelIsAppliedToAllSecurities), @" +from AlgorithmImports import * + +class CustomSlippageModel: + def get_slippage_approximation(self, asset, order): + return 0.25 + +def set_model(algo): + algo.set_slippage_model(CustomSlippageModel()) +"); + Assert.DoesNotThrow(() => module.GetAttr("set_model").Invoke(_algorithm.ToPython())); + } + + var ibm = _algorithm.AddEquity("IBM", Resolution.Daily); + foreach (var security in new[] { spy, ibm }) + { + Assert.IsInstanceOf(security.SlippageModel); + } + } + } +} diff --git a/Tests/Common/Securities/BrokerageModelSecurityInitializerTests.cs b/Tests/Common/Securities/BrokerageModelSecurityInitializerTests.cs index 086ae160cfde..80e2dc5c71d9 100644 --- a/Tests/Common/Securities/BrokerageModelSecurityInitializerTests.cs +++ b/Tests/Common/Securities/BrokerageModelSecurityInitializerTests.cs @@ -17,6 +17,7 @@ using System; using NodaTime; using NUnit.Framework; +using Python.Runtime; using QuantConnect.Algorithm; using QuantConnect.Brokerages; using QuantConnect.Data; @@ -161,6 +162,92 @@ public void BrokerageModelSecurityInitializer_CannotSetPrice_ForNonExistentHisto Assert.IsTrue(_tradeBarSecurity.Price == 0); } + [Test] + public void PythonConstructorAcceptsBoundMethodSeeder() + { + // Reproduces BrokerageModelSecurityInitializer(self.brokerage_model, self._seed_function) + // failed with "No method matches given arguments for .ctor: (InteractiveBrokersBrokerageModel, )" + using (Py.GIL()) + { + var module = PyModule.FromString(nameof(PythonConstructorAcceptsBoundMethodSeeder), @" +from AlgorithmImports import * + +class SeederHolder: + def __init__(self): + self.called = False + + def seed(self, security): + self.called = True + return TradeBar(datetime(2013, 10, 10), security.symbol, 10, 10, 10, 10, 100) + +def create_initializer(brokerage_model, holder): + return BrokerageModelSecurityInitializer(brokerage_model, holder.seed) +"); + var holder = module.GetAttr("SeederHolder").Invoke(); + using var initializer = module.GetAttr("create_initializer") + .Invoke(new DefaultBrokerageModel().ToPython(), holder); + initializer.As().Initialize(_tradeBarSecurity); + + Assert.IsTrue(holder.GetAttr("called").As()); + Assert.AreEqual(10m, _tradeBarSecurity.Price); + } + } + + [Test] + public void PythonConstructorAcceptsSecuritySeederInstance() + { + using (Py.GIL()) + { + var module = PyModule.FromString(nameof(PythonConstructorAcceptsSecuritySeederInstance), @" +from AlgorithmImports import * + +def create_initializer(brokerage_model, seeder): + return BrokerageModelSecurityInitializer(brokerage_model, seeder) +"); + using var initializer = module.GetAttr("create_initializer") + .Invoke(new DefaultBrokerageModel().ToPython(), SecuritySeeder.Null.ToPython()); + Assert.DoesNotThrow(() => initializer.As().Initialize(_tradeBarSecurity)); + Assert.AreEqual(0m, _tradeBarSecurity.Price); + } + } + + [Test] + public void PythonConstructorAcceptsNoneSeeder() + { + using (Py.GIL()) + { + var module = PyModule.FromString(nameof(PythonConstructorAcceptsNoneSeeder), @" +from AlgorithmImports import * + +def create_initializer(brokerage_model): + return BrokerageModelSecurityInitializer(brokerage_model, None) +"); + using var initializer = module.GetAttr("create_initializer").Invoke(new DefaultBrokerageModel().ToPython()); + Assert.DoesNotThrow(() => initializer.As().Initialize(_tradeBarSecurity)); + Assert.AreEqual(0m, _tradeBarSecurity.Price); + } + } + + [Test] + public void PythonConstructorRejectsNonCallableSeeder() + { + using (Py.GIL()) + { + var module = PyModule.FromString(nameof(PythonConstructorRejectsNonCallableSeeder), @" +from AlgorithmImports import * + +class NotASeeder: + pass + +def create_initializer(brokerage_model): + return BrokerageModelSecurityInitializer(brokerage_model, NotASeeder()) +"); + var exception = Assert.Catch( + () => module.GetAttr("create_initializer").Invoke(new DefaultBrokerageModel().ToPython())); + Assert.That(exception.Message, Does.Contain("unsupported security seeder").And.Contain("ISecuritySeeder")); + } + } + [Test] public void BrokerageModelSecurityInitializer_SetLeverageForBuyingPowerModel_Successfully() { diff --git a/Tests/Python/PythonWrapperTests.cs b/Tests/Python/PythonWrapperTests.cs index 88ffb01cf84e..8d8871b1df57 100644 --- a/Tests/Python/PythonWrapperTests.cs +++ b/Tests/Python/PythonWrapperTests.cs @@ -28,8 +28,9 @@ public static class PythonWrapperTests [TestFixture] public class ValidateImplementationOf { - [TestCase(nameof(MissingMethodOne), "ModelMissingMethodOne", "MethodOne")] - [TestCase(nameof(MissingProperty), "ModelMissingProperty", "PropertyOne")] + [TestCase(nameof(MissingMethodOne), "ModelMissingMethodOne", "method_one()")] + [TestCase(nameof(MissingMethodTwo), "ModelMissingMethodTwo", "method_two(parameter_one, parameter_two)")] + [TestCase(nameof(MissingProperty), "ModelMissingProperty", "property_one")] public void ThrowsOnMissingMember(string moduleName, string className, string missingMemberName) { using (Py.GIL()) @@ -301,6 +302,21 @@ def PropertyOne(self): return 'value' "; + private const string MissingMethodTwo = + @" +from clr import AddReference +AddReference('QuantConnect.Tests') + +from QuantConnect.Tests.Python import * + +class ModelMissingMethodTwo: + def MethodOne(): + pass + @property + def PropertyOne(self): + return 'value' +"; + private const string MissingProperty = @" from clr import AddReference @@ -319,7 +335,7 @@ interface IModel { string PropertyOne { get; set; } void MethodOne(); - void MethodTwo(); + void MethodTwo(string parameterOne, int parameterTwo); } public class Model : IModel @@ -330,7 +346,7 @@ public void MethodOne() { } - public void MethodTwo() + public void MethodTwo(string parameterOne, int parameterTwo) { } } diff --git a/Tests/Python/SecurityCustomModelTests.cs b/Tests/Python/SecurityCustomModelTests.cs index a3cbc8d5ecaa..c02c8037a597 100644 --- a/Tests/Python/SecurityCustomModelTests.cs +++ b/Tests/Python/SecurityCustomModelTests.cs @@ -19,6 +19,7 @@ using QuantConnect.Algorithm; using QuantConnect.Data; using QuantConnect.Data.Market; +using QuantConnect.Orders; using QuantConnect.Python; using QuantConnect.Securities; using QuantConnect.Securities.Equity; @@ -73,6 +74,71 @@ public void SetBuyingPowerModelFails() Assert.Throws(() => spy.SetBuyingPowerModel(pyObject)); } + [Test] + [TestCase(true)] + [TestCase(false)] + public void SetSlippageModelSuccess(bool isChild) + { + var spy = GetSecurity(Symbols.SPY, Resolution.Daily); + var time = new DateTime(2018, 8, 20, 15, 0, 0); + spy.SetMarketPrice(new Tick(time, Symbols.SPY, 100m, 100m)); + + // Test two custom slippage models. + // The first inherits from the C# SlippageModel base class and the other is 100% python. + // Subclassing the ISlippageModel interface is not supported by pythonnet + // (fails with "interface takes exactly one argument" at instantiation), + // the concrete SlippageModel base class is the supported base. + var code = isChild + ? CreateCustomSlippageModelFromSlippageModelCode() + : CreateCustomSlippageModelCode(); + + using (Py.GIL()) + { + var module = PyModule.FromString("CustomSlippageModel", code); + spy.SetSlippageModel(module.GetAttr("CustomSlippageModel").Invoke()); + } + + Assert.IsAssignableFrom(spy.SlippageModel); + + var order = new MarketOrder(spy.Symbol, 1, time); + Assert.AreEqual(0.12m, spy.SlippageModel.GetSlippageApproximation(spy, order)); + } + + [Test] + public void SetSlippageModelChildWithoutOverrideUsesBaseImplementation() + { + var spy = GetSecurity(Symbols.SPY, Resolution.Daily); + var time = new DateTime(2018, 8, 20, 15, 0, 0); + spy.SetMarketPrice(new Tick(time, Symbols.SPY, 100m, 100m)); + + using (Py.GIL()) + { + var module = PyModule.FromString("CustomSlippageModelNoOverride", @" +from AlgorithmImports import * + +class CustomSlippageModel(SlippageModel): + pass"); + spy.SetSlippageModel(module.GetAttr("CustomSlippageModel").Invoke()); + } + + var order = new MarketOrder(spy.Symbol, 1, time); + Assert.AreEqual(0m, spy.SlippageModel.GetSlippageApproximation(spy, order)); + } + + private string CreateCustomSlippageModelCode() => @" +from AlgorithmImports import * + +class CustomSlippageModel: + def get_slippage_approximation(self, asset, order): + return 0.12"; + + private string CreateCustomSlippageModelFromSlippageModelCode() => @" +from AlgorithmImports import * + +class CustomSlippageModel(SlippageModel): + def get_slippage_approximation(self, asset, order): + return 0.12"; + private PyObject CreateCustomBuyingPowerModel(string code) { using (Py.GIL())