Skip to content

Commit 9bf2836

Browse files
matvt-cellMartin-Molineroclaude
authored
Fix MOO slippage reference price (#9763)
* Fix MOO slippage reference price * 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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ELsDPabTSwU5kQF7f2ARTE * 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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ELsDPabTSwU5kQF7f2ARTE * 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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ELsDPabTSwU5kQF7f2ARTE --------- Co-authored-by: matvt-cell <282639098+matvt-cell@users.noreply.github.com> Co-authored-by: Martin Molinero <martin.molinero1@gmail.com> Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
1 parent 2a0030a commit 9bf2836

10 files changed

Lines changed: 382 additions & 22 deletions

Algorithm.CSharp/MarketImpactSlippageModelRegressionAlgorithm.cs

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -95,31 +95,31 @@ public override void OnOrderEvent(OrderEvent orderEvent)
9595
{"Total Orders", "9"},
9696
{"Average Win", "0%"},
9797
{"Average Loss", "-0.04%"},
98-
{"Compounding Annual Return", "-93.847%"},
99-
{"Drawdown", "4.200%"},
98+
{"Compounding Annual Return", "-94.178%"},
99+
{"Drawdown", "4.300%"},
100100
{"Expectancy", "-1"},
101101
{"Start Equity", "10000000"},
102-
{"End Equity", "9649801.20"},
103-
{"Net Profit", "-3.502%"},
104-
{"Sharpe Ratio", "-2.93"},
105-
{"Sortino Ratio", "-2.869"},
106-
{"Probabilistic Sharpe Ratio", "7.243%"},
102+
{"End Equity", "9642964.36"},
103+
{"Net Profit", "-3.570%"},
104+
{"Sharpe Ratio", "-2.896"},
105+
{"Sortino Ratio", "-2.829"},
106+
{"Probabilistic Sharpe Ratio", "7.047%"},
107107
{"Loss Rate", "100%"},
108108
{"Win Rate", "0%"},
109109
{"Profit-Loss Ratio", "0"},
110-
{"Alpha", "-3.355"},
111-
{"Beta", "1.244"},
112-
{"Annual Standard Deviation", "0.306"},
113-
{"Annual Variance", "0.094"},
114-
{"Information Ratio", "-20.203"},
115-
{"Tracking Error", "0.142"},
116-
{"Treynor Ratio", "-0.722"},
117-
{"Total Fees", "$1859.00"},
110+
{"Alpha", "-3.395"},
111+
{"Beta", "1.262"},
112+
{"Annual Standard Deviation", "0.312"},
113+
{"Annual Variance", "0.097"},
114+
{"Information Ratio", "-19.58"},
115+
{"Tracking Error", "0.147"},
116+
{"Treynor Ratio", "-0.715"},
117+
{"Total Fees", "$1860.21"},
118118
{"Estimated Strategy Capacity", "$470000000.00"},
119119
{"Lowest Capacity Asset", "AAPL R735QTJ8XC9X"},
120-
{"Portfolio Turnover", "21.04%"},
120+
{"Portfolio Turnover", "21.06%"},
121121
{"Drawdown Recovery", "0"},
122-
{"OrderListHash", "fc0626f660981cb698f6a9a5d5d1389a"}
122+
{"OrderListHash", "6a2a541fbde8de8454e9ae3a20d1ed69"}
123123
};
124124
}
125125
}
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
/*
2+
* QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
3+
* Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation.
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software
10+
* distributed under the License is distributed on an "AS IS" BASIS,
11+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
* See the License for the specific language governing permissions and
13+
* limitations under the License.
14+
*/
15+
16+
using System.Collections.Generic;
17+
using QuantConnect.Data;
18+
using QuantConnect.Data.Market;
19+
using QuantConnect.Interfaces;
20+
using QuantConnect.Orders;
21+
using QuantConnect.Orders.Slippage;
22+
23+
namespace QuantConnect.Algorithm.CSharp
24+
{
25+
/// <summary>
26+
/// Regression algorithm asserting that market on open orders using daily data are filled at the bar open
27+
/// with the slippage referenced to that same open price, not to the bar close which is not known at the open.
28+
/// See GH issue 9753
29+
/// </summary>
30+
public class MarketOnOpenOrderSlippageRegressionAlgorithm : QCAlgorithm, IRegressionAlgorithmDefinition
31+
{
32+
private const decimal SlippagePercent = 0.01m;
33+
private Symbol _symbol;
34+
private int _fills;
35+
36+
public override void Initialize()
37+
{
38+
SetStartDate(2013, 10, 07);
39+
SetEndDate(2013, 10, 11);
40+
SetCash(100000);
41+
42+
var security = AddEquity("SPY", Resolution.Daily);
43+
security.SetSlippageModel(new ConstantSlippageModel(SlippagePercent));
44+
_symbol = security.Symbol;
45+
}
46+
47+
public override void OnData(Slice slice)
48+
{
49+
if (!slice.Bars.ContainsKey(_symbol) || Transactions.GetOpenOrders(_symbol).Count > 0)
50+
{
51+
return;
52+
}
53+
54+
// alternate buys and sells so both directions are checked
55+
MarketOnOpenOrder(_symbol, Portfolio[_symbol].Invested ? -100 : 100);
56+
}
57+
58+
public override void OnOrderEvent(OrderEvent orderEvent)
59+
{
60+
if (orderEvent.Status != OrderStatus.Filled)
61+
{
62+
return;
63+
}
64+
65+
// the fill happens when the daily bar arrives, so this is the bar the order was filled with
66+
var bar = Securities[_symbol].Cache.GetData<TradeBar>();
67+
if (bar.Open == bar.Close)
68+
{
69+
throw new RegressionTestException($"Expected the fill bar open and close to differ so the slippage reference can be asserted: {bar}");
70+
}
71+
72+
var slippage = bar.Open * SlippagePercent;
73+
var expectedFillPrice = orderEvent.Direction == OrderDirection.Buy ? bar.Open + slippage : bar.Open - slippage;
74+
if (orderEvent.FillPrice != expectedFillPrice)
75+
{
76+
throw new RegressionTestException($"Expected {orderEvent.Direction} fill price {expectedFillPrice} (open {bar.Open} +/- {SlippagePercent:P} slippage) but was {orderEvent.FillPrice}. Bar: {bar}");
77+
}
78+
79+
_fills++;
80+
}
81+
82+
public override void OnEndOfAlgorithm()
83+
{
84+
if (_fills < 2)
85+
{
86+
throw new RegressionTestException($"Expected at least a buy and a sell fill but got {_fills}");
87+
}
88+
}
89+
90+
/// <summary>
91+
/// This is used by the regression test system to indicate if the open source Lean repository has the required data to run this algorithm.
92+
/// </summary>
93+
public bool CanRunLocally { get; } = true;
94+
95+
/// <summary>
96+
/// This is used by the regression test system to indicate which languages this algorithm is written in.
97+
/// </summary>
98+
public List<Language> Languages { get; } = new() { Language.CSharp };
99+
100+
/// <summary>
101+
/// Data Points count of all timeslices of algorithm
102+
/// </summary>
103+
public long DataPoints => 48;
104+
105+
/// <summary>
106+
/// Data Points count of the algorithm history
107+
/// </summary>
108+
public int AlgorithmHistoryDataPoints => 0;
109+
110+
/// <summary>
111+
/// Final status of the algorithm
112+
/// </summary>
113+
public AlgorithmStatus AlgorithmStatus => AlgorithmStatus.Completed;
114+
115+
/// <summary>
116+
/// This is used by the regression test system to indicate what the expected statistics are from running the algorithm
117+
/// </summary>
118+
public Dictionary<string, string> ExpectedStatistics => new Dictionary<string, string>
119+
{
120+
{"Total Orders", "5"},
121+
{"Average Win", "0%"},
122+
{"Average Loss", "-0.29%"},
123+
{"Compounding Annual Return", "-36.910%"},
124+
{"Drawdown", "0.600%"},
125+
{"Expectancy", "-1"},
126+
{"Start Equity", "100000"},
127+
{"End Equity", "99412.82"},
128+
{"Net Profit", "-0.587%"},
129+
{"Sharpe Ratio", "-14.31"},
130+
{"Sortino Ratio", "-19.441"},
131+
{"Probabilistic Sharpe Ratio", "1.568%"},
132+
{"Loss Rate", "100%"},
133+
{"Win Rate", "0%"},
134+
{"Profit-Loss Ratio", "0"},
135+
{"Alpha", "-0.502"},
136+
{"Beta", "0.093"},
137+
{"Annual Standard Deviation", "0.022"},
138+
{"Annual Variance", "0"},
139+
{"Information Ratio", "-11.354"},
140+
{"Tracking Error", "0.202"},
141+
{"Treynor Ratio", "-3.402"},
142+
{"Total Fees", "$4.00"},
143+
{"Estimated Strategy Capacity", "$1300000000.00"},
144+
{"Lowest Capacity Asset", "SPY R735QTJ8XC9X"},
145+
{"Portfolio Turnover", "11.63%"},
146+
{"Drawdown Recovery", "0"},
147+
{"OrderListHash", "4bbdfd7aaf0f2e4fa6cc9226fbf3d9e8"}
148+
};
149+
}
150+
}

Common/Orders/Slippage/AlphaStreamsSlippageModel.cs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
* limitations under the License.
1414
*/
1515

16+
using QuantConnect.Data.Market;
1617
using QuantConnect.Securities;
1718
using System.Collections.Generic;
1819

@@ -40,7 +41,13 @@ public decimal GetSlippageApproximation(Security asset, Order order)
4041
return 0;
4142
}
4243

43-
return _slippagePercent * asset.GetLastData()?.Value ?? 0;
44+
var lastData = asset.GetLastData();
45+
if (lastData == null) return 0;
46+
47+
// Market on open orders fill at the bar open, which is the price we have to reference, not the bar close
48+
var referencePrice = order.Type == OrderType.MarketOnOpen && lastData is IBar bar ? bar.Open : lastData.Value;
49+
50+
return _slippagePercent * referencePrice;
4451
}
4552
}
4653
}

Common/Orders/Slippage/ConstantSlippageModel.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
*/
1515

1616
using QuantConnect.Data;
17+
using QuantConnect.Data.Market;
1718
using QuantConnect.Securities;
1819

1920
namespace QuantConnect.Orders.Slippage
@@ -41,7 +42,10 @@ public decimal GetSlippageApproximation(Security asset, Order order)
4142
var lastData = asset.GetLastData();
4243
if (lastData == null) return 0;
4344

44-
return lastData.Value*_slippagePercent;
45+
// Market on open orders fill at the bar open, which is the price we have to reference, not the bar close
46+
var referencePrice = order.Type == OrderType.MarketOnOpen && lastData is IBar bar ? bar.Open : lastData.Value;
47+
48+
return referencePrice * _slippagePercent;
4549
}
4650
}
4751
}

Common/Orders/Slippage/MarketImpactSlippageModel.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,8 +127,11 @@ public decimal GetSlippageApproximation(Security asset, Order order)
127127
// realized market impact
128128
var realizedImpact = temporaryImpact + permanentImpact * 0.5d;
129129

130+
// Market on open orders fill at the bar open, which is the price we have to reference, not the bar close
131+
var referencePrice = order.Type == OrderType.MarketOnOpen && asset.GetLastData() is IBar bar ? bar.Open : asset.Price;
132+
130133
// estimate the slippage by temporary impact
131-
return SlippageFromImpactEstimation(realizedImpact) * asset.Price;
134+
return SlippageFromImpactEstimation(realizedImpact) * referencePrice;
132135
}
133136

134137
/// <summary>

Common/Orders/Slippage/VolumeShareSlippageModel.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,10 @@ public decimal GetSlippageApproximation(Security asset, Order order)
8686
slippagePercent = volumeShare * volumeShare * _priceImpact;
8787
}
8888

89-
return slippagePercent * lastData.Value;
89+
// Market on open orders fill at the bar open, which is the price we have to reference, not the bar close
90+
var referencePrice = order.Type == OrderType.MarketOnOpen ? ((IBar)lastData).Open : lastData.Value;
91+
92+
return slippagePercent * referencePrice;
9093
}
9194
}
9295
}

Common/Orders/Slippage/VolumeShareSlippageModel.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,4 +58,7 @@ def get_slippage_approximation(self, asset: Security, order: Order) -> float:
5858

5959
slippage_percent = volume_share * volume_share * self.price_impact
6060

61-
return slippage_percent * last_data.Value;
61+
# Market on open orders fill at the bar open, which is the price we have to reference, not the bar close
62+
reference_price = last_data.open if order.type == OrderType.MARKET_ON_OPEN else last_data.value
63+
64+
return slippage_percent * reference_price

Tests/Common/Orders/Fills/EquityFillModelTests.cs

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
using QuantConnect.Indicators;
2323
using QuantConnect.Orders;
2424
using QuantConnect.Orders.Fills;
25+
using QuantConnect.Orders.Slippage;
2526
using QuantConnect.Securities;
2627
using QuantConnect.Securities.Forex;
2728
using QuantConnect.Tests.Common.Data;
@@ -446,6 +447,75 @@ public void PerformsMarketOnOpenUsingOpenPriceWithMinuteSubscription(int quantit
446447
Assert.AreEqual(expected, fill.FillPrice);
447448
}
448449

450+
// The slippage is referenced to the bar open, so the fill price does not depend on the bar close
451+
[TestCase(-100, 103.896, Resolution.Daily)]
452+
[TestCase(100, 104.104, Resolution.Daily)]
453+
[TestCase(-100, 103.896, Resolution.Hour)]
454+
[TestCase(100, 104.104, Resolution.Hour)]
455+
[TestCase(-100, 103.896, Resolution.Minute)]
456+
[TestCase(100, 104.104, Resolution.Minute)]
457+
public void PerformsMarketOnOpenUsingOpenPriceForConstantSlippage(int quantity, decimal expected, Resolution resolution)
458+
{
459+
const decimal open = 104m;
460+
const decimal baselineClose = 105m;
461+
const decimal mutatedClose = 103.5m;
462+
const decimal slippagePercent = 0.001m;
463+
464+
var reference = new DateTime(2015, 06, 05, 12, 0, 0);
465+
var config = CreateTradeBarConfig(Symbols.SPY, resolution);
466+
467+
var baselineEquity = CreateEquity(config);
468+
var mutatedEquity = CreateEquity(config);
469+
470+
baselineEquity.SetSlippageModel(new ConstantSlippageModel(slippagePercent));
471+
mutatedEquity.SetSlippageModel(new ConstantSlippageModel(slippagePercent));
472+
473+
var time = baselineEquity.Exchange.Hours.GetNextMarketOpen(reference, false);
474+
TimeKeeper.SetUtcDateTime(time.ConvertToUtc(TimeZones.NewYork));
475+
476+
var period = resolution.ToTimeSpan();
477+
TradeBar GetTradeBar(decimal close) => new TradeBar(
478+
time.RoundDown(period),
479+
Symbols.SPY,
480+
open,
481+
106m,
482+
100m,
483+
close,
484+
100,
485+
period);
486+
487+
baselineEquity.SetMarketPrice(GetTradeBar(baselineClose));
488+
mutatedEquity.SetMarketPrice(GetTradeBar(mutatedClose));
489+
490+
var baselineOrder = new MarketOnOpenOrder(Symbols.SPY, quantity, reference);
491+
var mutatedOrder = new MarketOnOpenOrder(Symbols.SPY, quantity, reference);
492+
493+
var configProvider = new MockSubscriptionDataConfigProvider(config);
494+
495+
var baselineFill = ((EquityFillModel)baselineEquity.FillModel)
496+
.Fill(new FillModelParameters(
497+
baselineEquity,
498+
baselineOrder,
499+
configProvider,
500+
Time.OneHour,
501+
null))
502+
.Single();
503+
504+
var mutatedFill = ((EquityFillModel)mutatedEquity.FillModel)
505+
.Fill(new FillModelParameters(
506+
mutatedEquity,
507+
mutatedOrder,
508+
configProvider,
509+
Time.OneHour,
510+
null))
511+
.Single();
512+
513+
Assert.AreEqual(quantity, baselineFill.FillQuantity);
514+
Assert.AreEqual(quantity, mutatedFill.FillQuantity);
515+
Assert.AreEqual(expected, baselineFill.FillPrice);
516+
Assert.AreEqual(baselineFill.FillPrice, mutatedFill.FillPrice);
517+
}
518+
449519
[TestCase(-100)]
450520
[TestCase(100)]
451521
public void PerformsMarketOnOpenUsingOpenPriceWithDailySubscription(int quantity)

Tests/Common/Orders/Slippage/MarketImpactSlippageModelTest.cs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,23 @@ public void SlippageExpectationTests(decimal orderQuantity, int index, double ex
162162
Assert.AreEqual(expected, (double)slippage, 0.005d);
163163
}
164164

165+
// Market on open orders fill at the bar open, so the slippage is referenced to it instead of the close
166+
[TestCase(10000)]
167+
[TestCase(-10000)]
168+
public void MarketOnOpenOrdersReferenceTheOpenPrice(decimal orderQuantity)
169+
{
170+
var asset = _securities[0];
171+
asset.SetMarketPrice(new TradeBar(_algorithm.Time, asset.Symbol, 90m, 110m, 80m, 100m, 1));
172+
var time = new DateTime(2015, 6, 10, 14, 00, 0);
173+
174+
// fresh models so both use the same noise sequence
175+
var marketSlippage = new MarketImpactSlippageModel(_algorithm).GetSlippageApproximation(asset, new MarketOrder(asset.Symbol, orderQuantity, time));
176+
var marketOnOpenSlippage = new MarketImpactSlippageModel(_algorithm).GetSlippageApproximation(asset, new MarketOnOpenOrder(asset.Symbol, orderQuantity, time));
177+
178+
Assert.AreEqual(0.5075d, (double)marketSlippage, 0.005d);
179+
Assert.AreEqual((double)marketSlippage * 0.9d, (double)marketOnOpenSlippage, 0.0001d);
180+
}
181+
165182
// Test on buy & sell orders
166183
[TestCase(1)]
167184
[TestCase(-1)]

0 commit comments

Comments
 (0)