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())