From 529f782c7d235de667f6bd857aa1b75a6ee0dc6b Mon Sep 17 00:00:00 2001 From: matvt-cell <282639098+matvt-cell@users.noreply.github.com> Date: Sun, 30 Aug 2026 22:06:51 +0100 Subject: [PATCH 1/4] Fix MOO slippage reference price --- Common/Orders/Fills/EquityFillModel.cs | 7 +- .../Orders/Slippage/ConstantSlippageModel.cs | 13 ++++ Common/Orders/Slippage/ISlippageModel.cs | 15 ++++- .../Orders/Fills/EquityFillModelTests.cs | 64 +++++++++++++++++++ .../Orders/Slippage/SlippageModelsTests.cs | 17 +++++ 5 files changed, 110 insertions(+), 6 deletions(-) diff --git a/Common/Orders/Fills/EquityFillModel.cs b/Common/Orders/Fills/EquityFillModel.cs index a1c766b70d4f..d347fd4096e9 100644 --- a/Common/Orders/Fills/EquityFillModel.cs +++ b/Common/Orders/Fills/EquityFillModel.cs @@ -563,9 +563,6 @@ public override OrderEvent MarketOnOpenFill(Security asset, MarketOnOpenOrder or fill.FillQuantity = order.Quantity; fill.Status = OrderStatus.Filled; - //Calculate the model slippage: e.g. 0.01c - var slip = asset.SlippageModel.GetSlippageApproximation(asset, order); - var bestEffortMessage = ""; // If there is no trade information, get the bid or ask, then apply the slippage @@ -578,7 +575,7 @@ public override OrderEvent MarketOnOpenFill(Security asset, MarketOnOpenOrder or fill.Message += bestEffortMessage; } - fill.FillPrice += slip; + fill.FillPrice += asset.SlippageModel.GetSlippageApproximation(asset, order, fill.FillPrice); break; case OrderDirection.Sell: if (fill.FillPrice == 0) @@ -587,7 +584,7 @@ public override OrderEvent MarketOnOpenFill(Security asset, MarketOnOpenOrder or fill.Message += bestEffortMessage; } - fill.FillPrice -= slip; + fill.FillPrice -= asset.SlippageModel.GetSlippageApproximation(asset, order, fill.FillPrice); break; } diff --git a/Common/Orders/Slippage/ConstantSlippageModel.cs b/Common/Orders/Slippage/ConstantSlippageModel.cs index 43e348060b68..c8991e403d19 100644 --- a/Common/Orders/Slippage/ConstantSlippageModel.cs +++ b/Common/Orders/Slippage/ConstantSlippageModel.cs @@ -43,5 +43,18 @@ public decimal GetSlippageApproximation(Security asset, Order order) return lastData.Value*_slippagePercent; } + + /// + /// Slippage Model. Return a decimal cash slippage approximation on the order + /// using the provided reference price. + /// + /// The security matching the order + /// The order to compute slippage for + /// The price used as the reference for the slippage calculation + /// The slippage approximation + public decimal GetSlippageApproximation(Security asset, Order order, decimal referencePrice) + { + return referencePrice * _slippagePercent; + } } } diff --git a/Common/Orders/Slippage/ISlippageModel.cs b/Common/Orders/Slippage/ISlippageModel.cs index c0826f2560ab..589dabd9c7d6 100644 --- a/Common/Orders/Slippage/ISlippageModel.cs +++ b/Common/Orders/Slippage/ISlippageModel.cs @@ -26,5 +26,18 @@ public interface ISlippageModel /// Slippage Model. Return a decimal cash slippage approximation on the order. /// decimal GetSlippageApproximation(Security asset, Order order); + + /// + /// Slippage Model. Return a decimal cash slippage approximation on the order + /// using the provided reference price. + /// + /// The security matching the order + /// The order to compute slippage for + /// The price used as the reference for the slippage calculation + /// The slippage approximation + decimal GetSlippageApproximation(Security asset, Order order, decimal referencePrice) + { + return GetSlippageApproximation(asset, order); + } } -} \ No newline at end of file +} diff --git a/Tests/Common/Orders/Fills/EquityFillModelTests.cs b/Tests/Common/Orders/Fills/EquityFillModelTests.cs index 8dc4c81bbe7a..9cec4fe8fb0a 100644 --- a/Tests/Common/Orders/Fills/EquityFillModelTests.cs +++ b/Tests/Common/Orders/Fills/EquityFillModelTests.cs @@ -22,6 +22,7 @@ using QuantConnect.Indicators; using QuantConnect.Orders; using QuantConnect.Orders.Fills; +using QuantConnect.Orders.Slippage; using QuantConnect.Securities; using QuantConnect.Securities.Forex; using QuantConnect.Tests.Common.Data; @@ -446,6 +447,69 @@ public void PerformsMarketOnOpenUsingOpenPriceWithMinuteSubscription(int quantit Assert.AreEqual(expected, fill.FillPrice); } + [TestCase(-100, 103.896)] + [TestCase(100, 104.104)] + public void PerformsMarketOnOpenUsingOpenPriceForConstantSlippageWithDailySubscription(int quantity, decimal expected) + { + const decimal open = 104m; + const decimal baselineClose = 105m; + const decimal mutatedClose = 103.5m; + const decimal slippagePercent = 0.001m; + + var reference = new DateTime(2015, 06, 05, 12, 0, 0); + var config = CreateTradeBarConfig(Symbols.SPY, Resolution.Daily); + + var baselineEquity = CreateEquity(config); + var mutatedEquity = CreateEquity(config); + + baselineEquity.SetSlippageModel(new ConstantSlippageModel(slippagePercent)); + mutatedEquity.SetSlippageModel(new ConstantSlippageModel(slippagePercent)); + + var time = baselineEquity.Exchange.Hours.GetNextMarketOpen(reference, false); + TimeKeeper.SetUtcDateTime(time.ConvertToUtc(TimeZones.NewYork)); + + TradeBar GetTradeBar(decimal close) => new TradeBar( + time.RoundDown(Time.OneDay), + Symbols.SPY, + open, + 106m, + 100m, + close, + 100, + Time.OneDay); + + baselineEquity.SetMarketPrice(GetTradeBar(baselineClose)); + mutatedEquity.SetMarketPrice(GetTradeBar(mutatedClose)); + + var baselineOrder = new MarketOnOpenOrder(Symbols.SPY, quantity, reference); + var mutatedOrder = new MarketOnOpenOrder(Symbols.SPY, quantity, reference); + + var configProvider = new MockSubscriptionDataConfigProvider(config); + + var baselineFill = ((EquityFillModel)baselineEquity.FillModel) + .Fill(new FillModelParameters( + baselineEquity, + baselineOrder, + configProvider, + Time.OneHour, + null)) + .Single(); + + var mutatedFill = ((EquityFillModel)mutatedEquity.FillModel) + .Fill(new FillModelParameters( + mutatedEquity, + mutatedOrder, + configProvider, + Time.OneHour, + null)) + .Single(); + + Assert.AreEqual(quantity, baselineFill.FillQuantity); + Assert.AreEqual(quantity, mutatedFill.FillQuantity); + Assert.AreEqual(expected, baselineFill.FillPrice); + Assert.AreEqual(baselineFill.FillPrice, mutatedFill.FillPrice); + } + [TestCase(-100)] [TestCase(100)] public void PerformsMarketOnOpenUsingOpenPriceWithDailySubscription(int quantity) diff --git a/Tests/Common/Orders/Slippage/SlippageModelsTests.cs b/Tests/Common/Orders/Slippage/SlippageModelsTests.cs index b37dda8e0ccf..0a468e0dc31a 100644 --- a/Tests/Common/Orders/Slippage/SlippageModelsTests.cs +++ b/Tests/Common/Orders/Slippage/SlippageModelsTests.cs @@ -161,5 +161,22 @@ public void AlphaStreamsSlippageModel_ForexTest() var actual = model.GetSlippageApproximation(_forex, _forexBuyOrder); Assert.AreEqual(expected, actual); } + [Test] + public void SlippageModelReferencePriceOverloadIsBackwardsCompatible() + { + ISlippageModel model = new LegacySlippageModel(); + + var actual = model.GetSlippageApproximation(_equity, _equityBuyOrder, 123m); + + Assert.AreEqual(42m, actual); + } + + private sealed class LegacySlippageModel : ISlippageModel + { + public decimal GetSlippageApproximation(Security asset, Order order) + { + return 42m; + } + } } } From 66ed461b449712fcb3a5b2aa81af474befd5ddca Mon Sep 17 00:00:00 2001 From: Martin Molinero Date: Thu, 10 Sep 2026 12:47:25 -0300 Subject: [PATCH 2/4] Reference the bar open for market on open slippage Market on open orders fill at the bar open, but the slippage models scaled the slippage by the last data value, which for a bar is its close. With daily data that leaks the fill-day close into the fill price. Fix it inside the slippage models instead of extending ISlippageModel: when the order is a MarketOnOpenOrder and the last data is a bar, the models use its open as the reference price. Ticks keep using the price. Applies to the constant, volume share (C# and Python), alpha streams and market impact models, and to every fill model path since they all call the same method. Drops the interface overload and fill model changes and extends the tests to cover all models, data types, resolutions and the Python port. Fixes #9753 Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01ELsDPabTSwU5kQF7f2ARTE --- Common/Orders/Fills/EquityFillModel.cs | 7 +- .../Slippage/AlphaStreamsSlippageModel.cs | 9 +- .../Orders/Slippage/ConstantSlippageModel.cs | 15 +-- Common/Orders/Slippage/ISlippageModel.cs | 15 +-- .../Slippage/MarketImpactSlippageModel.cs | 5 +- .../Slippage/VolumeShareSlippageModel.cs | 5 +- .../Slippage/VolumeShareSlippageModel.py | 5 +- .../Orders/Fills/EquityFillModelTests.cs | 18 ++-- .../Slippage/MarketImpactSlippageModelTest.cs | 17 +++ .../Orders/Slippage/SlippageModelsTests.cs | 102 ++++++++++++++++-- 10 files changed, 152 insertions(+), 46 deletions(-) diff --git a/Common/Orders/Fills/EquityFillModel.cs b/Common/Orders/Fills/EquityFillModel.cs index d347fd4096e9..a1c766b70d4f 100644 --- a/Common/Orders/Fills/EquityFillModel.cs +++ b/Common/Orders/Fills/EquityFillModel.cs @@ -563,6 +563,9 @@ public override OrderEvent MarketOnOpenFill(Security asset, MarketOnOpenOrder or fill.FillQuantity = order.Quantity; fill.Status = OrderStatus.Filled; + //Calculate the model slippage: e.g. 0.01c + var slip = asset.SlippageModel.GetSlippageApproximation(asset, order); + var bestEffortMessage = ""; // If there is no trade information, get the bid or ask, then apply the slippage @@ -575,7 +578,7 @@ public override OrderEvent MarketOnOpenFill(Security asset, MarketOnOpenOrder or fill.Message += bestEffortMessage; } - fill.FillPrice += asset.SlippageModel.GetSlippageApproximation(asset, order, fill.FillPrice); + fill.FillPrice += slip; break; case OrderDirection.Sell: if (fill.FillPrice == 0) @@ -584,7 +587,7 @@ public override OrderEvent MarketOnOpenFill(Security asset, MarketOnOpenOrder or fill.Message += bestEffortMessage; } - fill.FillPrice -= asset.SlippageModel.GetSlippageApproximation(asset, order, fill.FillPrice); + fill.FillPrice -= slip; break; } diff --git a/Common/Orders/Slippage/AlphaStreamsSlippageModel.cs b/Common/Orders/Slippage/AlphaStreamsSlippageModel.cs index 5158247cee7e..4dd98e3a6f0a 100644 --- a/Common/Orders/Slippage/AlphaStreamsSlippageModel.cs +++ b/Common/Orders/Slippage/AlphaStreamsSlippageModel.cs @@ -13,6 +13,7 @@ * limitations under the License. */ +using QuantConnect.Data.Market; using QuantConnect.Securities; using System.Collections.Generic; @@ -40,7 +41,13 @@ public decimal GetSlippageApproximation(Security asset, Order order) return 0; } - return _slippagePercent * asset.GetLastData()?.Value ?? 0; + var lastData = asset.GetLastData(); + if (lastData == null) return 0; + + // Market on open orders fill at the bar open, which is the price we have to reference, not the bar close + var referencePrice = order.Type == OrderType.MarketOnOpen && lastData is IBar bar ? bar.Open : lastData.Value; + + return _slippagePercent * referencePrice; } } } \ No newline at end of file diff --git a/Common/Orders/Slippage/ConstantSlippageModel.cs b/Common/Orders/Slippage/ConstantSlippageModel.cs index c8991e403d19..ab77d7728784 100644 --- a/Common/Orders/Slippage/ConstantSlippageModel.cs +++ b/Common/Orders/Slippage/ConstantSlippageModel.cs @@ -14,6 +14,7 @@ */ using QuantConnect.Data; +using QuantConnect.Data.Market; using QuantConnect.Securities; namespace QuantConnect.Orders.Slippage @@ -41,19 +42,9 @@ public decimal GetSlippageApproximation(Security asset, Order order) var lastData = asset.GetLastData(); if (lastData == null) return 0; - return lastData.Value*_slippagePercent; - } + // Market on open orders fill at the bar open, which is the price we have to reference, not the bar close + var referencePrice = order.Type == OrderType.MarketOnOpen && lastData is IBar bar ? bar.Open : lastData.Value; - /// - /// Slippage Model. Return a decimal cash slippage approximation on the order - /// using the provided reference price. - /// - /// The security matching the order - /// The order to compute slippage for - /// The price used as the reference for the slippage calculation - /// The slippage approximation - public decimal GetSlippageApproximation(Security asset, Order order, decimal referencePrice) - { return referencePrice * _slippagePercent; } } diff --git a/Common/Orders/Slippage/ISlippageModel.cs b/Common/Orders/Slippage/ISlippageModel.cs index 589dabd9c7d6..c0826f2560ab 100644 --- a/Common/Orders/Slippage/ISlippageModel.cs +++ b/Common/Orders/Slippage/ISlippageModel.cs @@ -26,18 +26,5 @@ public interface ISlippageModel /// Slippage Model. Return a decimal cash slippage approximation on the order. /// decimal GetSlippageApproximation(Security asset, Order order); - - /// - /// Slippage Model. Return a decimal cash slippage approximation on the order - /// using the provided reference price. - /// - /// The security matching the order - /// The order to compute slippage for - /// The price used as the reference for the slippage calculation - /// The slippage approximation - decimal GetSlippageApproximation(Security asset, Order order, decimal referencePrice) - { - return GetSlippageApproximation(asset, order); - } } -} +} \ No newline at end of file diff --git a/Common/Orders/Slippage/MarketImpactSlippageModel.cs b/Common/Orders/Slippage/MarketImpactSlippageModel.cs index 394e05a69486..7de6a5de472e 100644 --- a/Common/Orders/Slippage/MarketImpactSlippageModel.cs +++ b/Common/Orders/Slippage/MarketImpactSlippageModel.cs @@ -127,8 +127,11 @@ public decimal GetSlippageApproximation(Security asset, Order order) // realized market impact var realizedImpact = temporaryImpact + permanentImpact * 0.5d; + // Market on open orders fill at the bar open, which is the price we have to reference, not the bar close + var referencePrice = order.Type == OrderType.MarketOnOpen && asset.GetLastData() is IBar bar ? bar.Open : asset.Price; + // estimate the slippage by temporary impact - return SlippageFromImpactEstimation(realizedImpact) * asset.Price; + return SlippageFromImpactEstimation(realizedImpact) * referencePrice; } /// diff --git a/Common/Orders/Slippage/VolumeShareSlippageModel.cs b/Common/Orders/Slippage/VolumeShareSlippageModel.cs index 2841182f8bfa..ff6552174f69 100644 --- a/Common/Orders/Slippage/VolumeShareSlippageModel.cs +++ b/Common/Orders/Slippage/VolumeShareSlippageModel.cs @@ -86,7 +86,10 @@ public decimal GetSlippageApproximation(Security asset, Order order) slippagePercent = volumeShare * volumeShare * _priceImpact; } - return slippagePercent * lastData.Value; + // Market on open orders fill at the bar open, which is the price we have to reference, not the bar close + var referencePrice = order.Type == OrderType.MarketOnOpen ? ((IBar)lastData).Open : lastData.Value; + + return slippagePercent * referencePrice; } } } diff --git a/Common/Orders/Slippage/VolumeShareSlippageModel.py b/Common/Orders/Slippage/VolumeShareSlippageModel.py index 3921c414a741..8cd3edb25fe8 100644 --- a/Common/Orders/Slippage/VolumeShareSlippageModel.py +++ b/Common/Orders/Slippage/VolumeShareSlippageModel.py @@ -58,4 +58,7 @@ def get_slippage_approximation(self, asset: Security, order: Order) -> float: slippage_percent = volume_share * volume_share * self.price_impact - return slippage_percent * last_data.Value; + # Market on open orders fill at the bar open, which is the price we have to reference, not the bar close + reference_price = last_data.open if order.type == OrderType.MARKET_ON_OPEN else last_data.value + + return slippage_percent * reference_price diff --git a/Tests/Common/Orders/Fills/EquityFillModelTests.cs b/Tests/Common/Orders/Fills/EquityFillModelTests.cs index 9cec4fe8fb0a..b7ce196777ee 100644 --- a/Tests/Common/Orders/Fills/EquityFillModelTests.cs +++ b/Tests/Common/Orders/Fills/EquityFillModelTests.cs @@ -447,9 +447,14 @@ public void PerformsMarketOnOpenUsingOpenPriceWithMinuteSubscription(int quantit Assert.AreEqual(expected, fill.FillPrice); } - [TestCase(-100, 103.896)] - [TestCase(100, 104.104)] - public void PerformsMarketOnOpenUsingOpenPriceForConstantSlippageWithDailySubscription(int quantity, decimal expected) + // The slippage is referenced to the bar open, so the fill price does not depend on the bar close + [TestCase(-100, 103.896, Resolution.Daily)] + [TestCase(100, 104.104, Resolution.Daily)] + [TestCase(-100, 103.896, Resolution.Hour)] + [TestCase(100, 104.104, Resolution.Hour)] + [TestCase(-100, 103.896, Resolution.Minute)] + [TestCase(100, 104.104, Resolution.Minute)] + public void PerformsMarketOnOpenUsingOpenPriceForConstantSlippage(int quantity, decimal expected, Resolution resolution) { const decimal open = 104m; const decimal baselineClose = 105m; @@ -457,7 +462,7 @@ public void PerformsMarketOnOpenUsingOpenPriceForConstantSlippageWithDailySubscr const decimal slippagePercent = 0.001m; var reference = new DateTime(2015, 06, 05, 12, 0, 0); - var config = CreateTradeBarConfig(Symbols.SPY, Resolution.Daily); + var config = CreateTradeBarConfig(Symbols.SPY, resolution); var baselineEquity = CreateEquity(config); var mutatedEquity = CreateEquity(config); @@ -468,15 +473,16 @@ public void PerformsMarketOnOpenUsingOpenPriceForConstantSlippageWithDailySubscr var time = baselineEquity.Exchange.Hours.GetNextMarketOpen(reference, false); TimeKeeper.SetUtcDateTime(time.ConvertToUtc(TimeZones.NewYork)); + var period = resolution.ToTimeSpan(); TradeBar GetTradeBar(decimal close) => new TradeBar( - time.RoundDown(Time.OneDay), + time.RoundDown(period), Symbols.SPY, open, 106m, 100m, close, 100, - Time.OneDay); + period); baselineEquity.SetMarketPrice(GetTradeBar(baselineClose)); mutatedEquity.SetMarketPrice(GetTradeBar(mutatedClose)); diff --git a/Tests/Common/Orders/Slippage/MarketImpactSlippageModelTest.cs b/Tests/Common/Orders/Slippage/MarketImpactSlippageModelTest.cs index 6a931d6a4688..81a69ca8bb0d 100644 --- a/Tests/Common/Orders/Slippage/MarketImpactSlippageModelTest.cs +++ b/Tests/Common/Orders/Slippage/MarketImpactSlippageModelTest.cs @@ -162,6 +162,23 @@ public void SlippageExpectationTests(decimal orderQuantity, int index, double ex Assert.AreEqual(expected, (double)slippage, 0.005d); } + // Market on open orders fill at the bar open, so the slippage is referenced to it instead of the close + [TestCase(10000)] + [TestCase(-10000)] + public void MarketOnOpenOrdersReferenceTheOpenPrice(decimal orderQuantity) + { + var asset = _securities[0]; + asset.SetMarketPrice(new TradeBar(_algorithm.Time, asset.Symbol, 90m, 110m, 80m, 100m, 1)); + var time = new DateTime(2015, 6, 10, 14, 00, 0); + + // fresh models so both use the same noise sequence + var marketSlippage = new MarketImpactSlippageModel(_algorithm).GetSlippageApproximation(asset, new MarketOrder(asset.Symbol, orderQuantity, time)); + var marketOnOpenSlippage = new MarketImpactSlippageModel(_algorithm).GetSlippageApproximation(asset, new MarketOnOpenOrder(asset.Symbol, orderQuantity, time)); + + Assert.AreEqual(0.5075d, (double)marketSlippage, 0.005d); + Assert.AreEqual((double)marketSlippage * 0.9d, (double)marketOnOpenSlippage, 0.0001d); + } + // Test on buy & sell orders [TestCase(1)] [TestCase(-1)] diff --git a/Tests/Common/Orders/Slippage/SlippageModelsTests.cs b/Tests/Common/Orders/Slippage/SlippageModelsTests.cs index 0a468e0dc31a..a15f5e4725a5 100644 --- a/Tests/Common/Orders/Slippage/SlippageModelsTests.cs +++ b/Tests/Common/Orders/Slippage/SlippageModelsTests.cs @@ -14,13 +14,17 @@ */ using NUnit.Framework; +using Python.Runtime; +using QuantConnect.Data; using QuantConnect.Data.Market; using QuantConnect.Orders; using QuantConnect.Orders.Slippage; +using QuantConnect.Python; using QuantConnect.Securities; using QuantConnect.Securities.Equity; using QuantConnect.Securities.Forex; using System; +using System.IO; namespace QuantConnect.Tests.Common.Orders.Slippage { @@ -161,22 +165,104 @@ public void AlphaStreamsSlippageModel_ForexTest() var actual = model.GetSlippageApproximation(_forex, _forexBuyOrder); Assert.AreEqual(expected, actual); } - [Test] - public void SlippageModelReferencePriceOverloadIsBackwardsCompatible() + + // Market on open orders fill at the bar open, so the slippage is referenced to it instead of the close. + // Any other order type keeps using the last price. Ticks have no open so the price is used + [TestCase(MarketDataType.TradeBar, OrderType.Market, 100)] + [TestCase(MarketDataType.TradeBar, OrderType.MarketOnOpen, 90)] + [TestCase(MarketDataType.TradeBar, OrderType.MarketOnClose, 100)] + [TestCase(MarketDataType.TradeBar, OrderType.Limit, 100)] + [TestCase(MarketDataType.QuoteBar, OrderType.Market, 100)] + [TestCase(MarketDataType.QuoteBar, OrderType.MarketOnOpen, 90)] + [TestCase(MarketDataType.Tick, OrderType.Market, 100)] + [TestCase(MarketDataType.Tick, OrderType.MarketOnOpen, 100)] + public void SlippageModelsReferenceTheOpenPriceForMarketOnOpenOrders(MarketDataType dataType, OrderType orderType, decimal expectedReferencePrice) { - ISlippageModel model = new LegacySlippageModel(); + var security = CreateSecurityWithData(dataType); + var order = CreateOrder(orderType, security.Symbol); + + Assert.AreEqual(expectedReferencePrice * 0.5m, new ConstantSlippageModel(0.5m).GetSlippageApproximation(security, order)); - var actual = model.GetSlippageApproximation(_equity, _equityBuyOrder, 123m); + if (security.Type == SecurityType.Equity) + { + Assert.AreEqual(expectedReferencePrice * 0.0001m, new AlphaStreamsSlippageModel().GetSlippageApproximation(security, order)); + } - Assert.AreEqual(42m, actual); + var volumeShareModel = new VolumeShareSlippageModel(); + if (dataType == MarketDataType.Tick) + { + Assert.Throws(() => volumeShareModel.GetSlippageApproximation(security, order)); + } + else + { + // order quantity is 1 and the bar volume is 100, below the volume limit + var volumeShare = 1m / 100m; + Assert.AreEqual(expectedReferencePrice * volumeShare * volumeShare * 0.1m, volumeShareModel.GetSlippageApproximation(security, order)); + } } - private sealed class LegacySlippageModel : ISlippageModel + [TestCase(MarketDataType.TradeBar, OrderType.Market, 100)] + [TestCase(MarketDataType.TradeBar, OrderType.MarketOnOpen, 90)] + [TestCase(MarketDataType.QuoteBar, OrderType.Market, 100)] + [TestCase(MarketDataType.QuoteBar, OrderType.MarketOnOpen, 90)] + public void PythonVolumeShareSlippageModelReferencesTheOpenPriceForMarketOnOpenOrders(MarketDataType dataType, OrderType orderType, decimal expectedReferencePrice) { - public decimal GetSlippageApproximation(Security asset, Order order) + var security = CreateSecurityWithData(dataType); + var order = CreateOrder(orderType, security.Symbol); + + ISlippageModel model; + using (Py.GIL()) { - return 42m; + var module = PyModule.FromString("VolumeShareSlippageModelTest", + File.ReadAllText("../../../Common/Orders/Slippage/VolumeShareSlippageModel.py")); + model = new SlippageModelPythonWrapper(module.GetAttr("VolumeShareSlippageModel").Invoke()); } + + // order quantity is 1 and the bar volume is 100, below the volume limit + var volumeShare = 1m / 100m; + var expected = expectedReferencePrice * volumeShare * volumeShare * 0.1m; + Assert.AreEqual((double)expected, (double)model.GetSlippageApproximation(security, order), 1e-12); + } + + private static Order CreateOrder(OrderType orderType, Symbol symbol) + { + var time = new DateTime(2015, 6, 10, 9, 0, 0); + return orderType switch + { + OrderType.Market => new MarketOrder(symbol, 1, time), + OrderType.MarketOnOpen => new MarketOnOpenOrder(symbol, 1, time), + OrderType.MarketOnClose => new MarketOnCloseOrder(symbol, 1, time), + OrderType.Limit => new LimitOrder(symbol, 1, 100, time), + _ => throw new ArgumentOutOfRangeException(nameof(orderType)) + }; + } + + /// + /// Sets data whose open is 90 and close is 100 as the last data of a security and returns it. + /// Quote bars are not the default data type for equities, so forex is used for them + /// + private Security CreateSecurityWithData(MarketDataType dataType) + { + var time = new DateTime(2015, 6, 10, 9, 30, 0); + BaseData data; + switch (dataType) + { + case MarketDataType.TradeBar: + data = new TradeBar(time, Symbols.SPY, 90m, 110m, 80m, 100m, 100); + break; + case MarketDataType.QuoteBar: + data = new QuoteBar(time, Symbols.EURUSD, new Bar(89m, 109m, 79m, 99m), 100, new Bar(91m, 111m, 81m, 101m), 100); + break; + case MarketDataType.Tick: + data = new Tick(time, Symbols.SPY, 100m, 100m) { TickType = TickType.Trade, Quantity = 100 }; + break; + default: + throw new ArgumentOutOfRangeException(nameof(dataType)); + } + + Security security = data.Symbol == Symbols.SPY ? _equity : _forex; + security.SetMarketPrice(data); + return security; } } } From 994ebbf46f86ab9d67bd59be15fa1ca7fc396d28 Mon Sep 17 00:00:00 2001 From: Martin Molinero Date: Thu, 10 Sep 2026 13:24:51 -0300 Subject: [PATCH 3/4] Add market on open slippage regression algorithm Daily SPY with a constant slippage model, alternating market on open buys and sells, asserting every fill is the bar open plus or minus slippage on that same open. Fails without the slippage model fix for GH 9753. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01ELsDPabTSwU5kQF7f2ARTE --- ...tOnOpenOrderSlippageRegressionAlgorithm.cs | 150 ++++++++++++++++++ 1 file changed, 150 insertions(+) create mode 100644 Algorithm.CSharp/MarketOnOpenOrderSlippageRegressionAlgorithm.cs diff --git a/Algorithm.CSharp/MarketOnOpenOrderSlippageRegressionAlgorithm.cs b/Algorithm.CSharp/MarketOnOpenOrderSlippageRegressionAlgorithm.cs new file mode 100644 index 000000000000..00131740aaa0 --- /dev/null +++ b/Algorithm.CSharp/MarketOnOpenOrderSlippageRegressionAlgorithm.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.Data.Market; +using QuantConnect.Interfaces; +using QuantConnect.Orders; +using QuantConnect.Orders.Slippage; + +namespace QuantConnect.Algorithm.CSharp +{ + /// + /// Regression algorithm asserting that market on open orders using daily data are filled at the bar open + /// with the slippage referenced to that same open price, not to the bar close which is not known at the open. + /// See GH issue 9753 + /// + public class MarketOnOpenOrderSlippageRegressionAlgorithm : QCAlgorithm, IRegressionAlgorithmDefinition + { + private const decimal SlippagePercent = 0.01m; + private Symbol _symbol; + private int _fills; + + public override void Initialize() + { + SetStartDate(2013, 10, 07); + SetEndDate(2013, 10, 11); + SetCash(100000); + + var security = AddEquity("SPY", Resolution.Daily); + security.SetSlippageModel(new ConstantSlippageModel(SlippagePercent)); + _symbol = security.Symbol; + } + + public override void OnData(Slice slice) + { + if (!slice.Bars.ContainsKey(_symbol) || Transactions.GetOpenOrders(_symbol).Count > 0) + { + return; + } + + // alternate buys and sells so both directions are checked + MarketOnOpenOrder(_symbol, Portfolio[_symbol].Invested ? -100 : 100); + } + + public override void OnOrderEvent(OrderEvent orderEvent) + { + if (orderEvent.Status != OrderStatus.Filled) + { + return; + } + + // the fill happens when the daily bar arrives, so this is the bar the order was filled with + var bar = Securities[_symbol].Cache.GetData(); + if (bar.Open == bar.Close) + { + throw new RegressionTestException($"Expected the fill bar open and close to differ so the slippage reference can be asserted: {bar}"); + } + + var slippage = bar.Open * SlippagePercent; + var expectedFillPrice = orderEvent.Direction == OrderDirection.Buy ? bar.Open + slippage : bar.Open - slippage; + if (orderEvent.FillPrice != expectedFillPrice) + { + throw new RegressionTestException($"Expected {orderEvent.Direction} fill price {expectedFillPrice} (open {bar.Open} +/- {SlippagePercent:P} slippage) but was {orderEvent.FillPrice}. Bar: {bar}"); + } + + _fills++; + } + + public override void OnEndOfAlgorithm() + { + if (_fills < 2) + { + throw new RegressionTestException($"Expected at least a buy and a sell fill but got {_fills}"); + } + } + + /// + /// 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 }; + + /// + /// Data Points count of all timeslices of algorithm + /// + public long DataPoints => 48; + + /// + /// 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", "5"}, + {"Average Win", "0%"}, + {"Average Loss", "-0.29%"}, + {"Compounding Annual Return", "-36.910%"}, + {"Drawdown", "0.600%"}, + {"Expectancy", "-1"}, + {"Start Equity", "100000"}, + {"End Equity", "99412.82"}, + {"Net Profit", "-0.587%"}, + {"Sharpe Ratio", "-14.31"}, + {"Sortino Ratio", "-19.441"}, + {"Probabilistic Sharpe Ratio", "1.568%"}, + {"Loss Rate", "100%"}, + {"Win Rate", "0%"}, + {"Profit-Loss Ratio", "0"}, + {"Alpha", "-0.502"}, + {"Beta", "0.093"}, + {"Annual Standard Deviation", "0.022"}, + {"Annual Variance", "0"}, + {"Information Ratio", "-11.354"}, + {"Tracking Error", "0.202"}, + {"Treynor Ratio", "-3.402"}, + {"Total Fees", "$4.00"}, + {"Estimated Strategy Capacity", "$1300000000.00"}, + {"Lowest Capacity Asset", "SPY R735QTJ8XC9X"}, + {"Portfolio Turnover", "11.63%"}, + {"Drawdown Recovery", "0"}, + {"OrderListHash", "4bbdfd7aaf0f2e4fa6cc9226fbf3d9e8"} + }; + } +} From c8ff879d7430c8f35b345a25b6d79d671f324721 Mon Sep 17 00:00:00 2001 From: Martin Molinero Date: Thu, 10 Sep 2026 13:54:05 -0300 Subject: [PATCH 4/4] Update market impact slippage regression statistics The algorithm submits market orders on daily data while the exchange is closed, so they become market on open orders and their slippage is now referenced to the bar open. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01ELsDPabTSwU5kQF7f2ARTE --- ...tImpactSlippageModelRegressionAlgorithm.cs | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/Algorithm.CSharp/MarketImpactSlippageModelRegressionAlgorithm.cs b/Algorithm.CSharp/MarketImpactSlippageModelRegressionAlgorithm.cs index f67f2fd48245..b4148a2489b9 100644 --- a/Algorithm.CSharp/MarketImpactSlippageModelRegressionAlgorithm.cs +++ b/Algorithm.CSharp/MarketImpactSlippageModelRegressionAlgorithm.cs @@ -95,31 +95,31 @@ public override void OnOrderEvent(OrderEvent orderEvent) {"Total Orders", "9"}, {"Average Win", "0%"}, {"Average Loss", "-0.04%"}, - {"Compounding Annual Return", "-93.847%"}, - {"Drawdown", "4.200%"}, + {"Compounding Annual Return", "-94.178%"}, + {"Drawdown", "4.300%"}, {"Expectancy", "-1"}, {"Start Equity", "10000000"}, - {"End Equity", "9649801.20"}, - {"Net Profit", "-3.502%"}, - {"Sharpe Ratio", "-2.93"}, - {"Sortino Ratio", "-2.869"}, - {"Probabilistic Sharpe Ratio", "7.243%"}, + {"End Equity", "9642964.36"}, + {"Net Profit", "-3.570%"}, + {"Sharpe Ratio", "-2.896"}, + {"Sortino Ratio", "-2.829"}, + {"Probabilistic Sharpe Ratio", "7.047%"}, {"Loss Rate", "100%"}, {"Win Rate", "0%"}, {"Profit-Loss Ratio", "0"}, - {"Alpha", "-3.355"}, - {"Beta", "1.244"}, - {"Annual Standard Deviation", "0.306"}, - {"Annual Variance", "0.094"}, - {"Information Ratio", "-20.203"}, - {"Tracking Error", "0.142"}, - {"Treynor Ratio", "-0.722"}, - {"Total Fees", "$1859.00"}, + {"Alpha", "-3.395"}, + {"Beta", "1.262"}, + {"Annual Standard Deviation", "0.312"}, + {"Annual Variance", "0.097"}, + {"Information Ratio", "-19.58"}, + {"Tracking Error", "0.147"}, + {"Treynor Ratio", "-0.715"}, + {"Total Fees", "$1860.21"}, {"Estimated Strategy Capacity", "$470000000.00"}, {"Lowest Capacity Asset", "AAPL R735QTJ8XC9X"}, - {"Portfolio Turnover", "21.04%"}, + {"Portfolio Turnover", "21.06%"}, {"Drawdown Recovery", "0"}, - {"OrderListHash", "fc0626f660981cb698f6a9a5d5d1389a"} + {"OrderListHash", "6a2a541fbde8de8454e9ae3a20d1ed69"} }; } }