Expected Behavior
For an equity MarketOnOpen order using Daily TradeBar data:
- The base fill price may be derived from the fill-day
Open.
- Any slippage applied to that simulated opening fill should depend only on information available at or before the market open.
- If the
Open and all prior inputs remain unchanged, changing only the fill-day Close should not change the MOO fill price.
Actual Behavior
With ConstantSlippageModel(0.001), the observed fill is:
BUY fill = fill-day Open + fill-day Close × 0.001
SELL fill = fill-day Open - fill-day Close × 0.001
Therefore, an economically opening-auction fill depends on the same day's closing price, which is not available at the market open. This introduces look-ahead bias into the simulated execution price.
Although Daily resolution emits the order event when the completed daily bar becomes available, MarketOnOpenFill prices the order from that bar's Open. Using the same bar's Close as the slippage reference makes the simulated opening execution non-causal.
Controlled Reproduction
The experiment used:
- Explicit
MarketOnOpen orders
- Equity Daily
TradeBar data
ConstantSlippageModel(0.001)
- Raw normalization
fill_forward=False
extended_market_hours=False
daily_precise_end_time=True
- Fixed synthetic data
- Fixed LEAN image:
docker.io/quantconnect/lean:18024
sha256:8d35b77bf74bf551f5bbfbaa9d2904c762ea5d1cc8147c18ea322631d5b578e8
linux/amd64
Three runs were performed. Each mutation changed exactly one fill-day Close; the corresponding Open, strategy, configuration, image, order metadata, quantities, fees, and all other data remained unchanged.
Buy control
Baseline fill-day bar:
Date: 2024-01-04
Open: 104
Close: 105
BUY fill: 104.105
Mutation:
Date: 2024-01-04
Open: 104 (unchanged)
Close: 103.5 (only changed field)
BUY fill: 104.1035
Changing only the fill-day Close from 105 to 103.5 changed the BUY fill from 104.105 to 104.1035.
The SELL fill, order type, submission time, fill time, quantity, and fees were unchanged.
Sell control
Baseline fill-day bar:
Date: 2024-01-09
Open: 98
Close: 97
SELL fill: 97.903
Mutation:
Date: 2024-01-09
Open: 98 (unchanged)
Close: 98.5 (only changed field)
SELL fill: 97.9015
Changing only the fill-day Close from 97 to 98.5 changed the SELL fill from 97.903 to 97.9015.
The BUY fill, order type, submission time, fill time, quantity, and fees were unchanged.
Relevant Source Path
The behavior appears to result from the interaction of three components:
EquityFillModel.MarketOnOpenFill uses the TradeBar.Open as the base MOO fill price and then applies the configured slippage model.
ConstantSlippageModel calculates slippage from asset.GetLastData().Value.
- For a Daily
TradeBar, Value represents the bar's Close.
This produces the empirically observed Open ± Close × slippagePercent result.
Minimal Algorithm
from AlgorithmImports import *
from datetime import date
class DailyMooSlippageCausalityAlgorithm(QCAlgorithm):
def initialize(self):
self.set_start_date(2024, 1, 2)
self.set_end_date(2024, 1, 11)
self.set_cash(10000)
self.settings.daily_precise_end_time = True
self.symbol = self.add_equity(
"SYN",
Resolution.DAILY,
fill_forward=False,
leverage=1,
extended_market_hours=False,
data_normalization_mode=DataNormalizationMode.RAW,
).symbol
security = self.securities[self.symbol]
security.set_slippage_model(ConstantSlippageModel(0.001))
security.set_fee_model(ConstantFeeModel(1))
def on_data(self, data: Slice):
if self.symbol not in data.bars:
return
if self.time.date() == date(2024, 1, 3):
self.market_on_open_order(self.symbol, 10)
elif self.time.date() == date(2024, 1, 8):
self.market_on_open_order(self.symbol, -10)
Run this algorithm against three otherwise identical synthetic datasets:
- Baseline.
- Change only the 2024-01-04
Close from 105 to 103.5.
- Change only the 2024-01-09
Close from 97 to 98.5.
Potential Solution
The important semantic requirement is that slippage for an opening fill must not depend on information unavailable at the open.
Possible approaches include:
- Letting the fill model provide the pre-slippage reference price to the slippage model.
- Adding execution context to the slippage-model interface.
- Handling MOO slippage using a causal opening-price reference.
I do not want to prescribe a specific implementation, but documenting the current behavior alone would not remove the look-ahead bias.
Evidence
The machine-readable comparison file is attached.
comparison.json
SHA-256:
052D8B21DD40638BDDB046B1C6FEC3D416D1924249BE4B9BF927CF145D8AD6C1
Related Issues
This appears related to, but not duplicated by:
Those issues concern MOO accuracy or stale/non-causal fills, but they do not describe the specific fill-day Close dependency caused by Daily TradeBar + MarketOnOpen + ConstantSlippageModel.
Checklist
Expected Behavior
For an equity
MarketOnOpenorder using DailyTradeBardata:Open.Openand all prior inputs remain unchanged, changing only the fill-dayCloseshould not change the MOO fill price.Actual Behavior
With
ConstantSlippageModel(0.001), the observed fill is:Therefore, an economically opening-auction fill depends on the same day's closing price, which is not available at the market open. This introduces look-ahead bias into the simulated execution price.
Although Daily resolution emits the order event when the completed daily bar becomes available,
MarketOnOpenFillprices the order from that bar'sOpen. Using the same bar'sCloseas the slippage reference makes the simulated opening execution non-causal.Controlled Reproduction
The experiment used:
MarketOnOpenordersTradeBardataConstantSlippageModel(0.001)fill_forward=Falseextended_market_hours=Falsedaily_precise_end_time=TrueThree runs were performed. Each mutation changed exactly one fill-day
Close; the correspondingOpen, strategy, configuration, image, order metadata, quantities, fees, and all other data remained unchanged.Buy control
Baseline fill-day bar:
Mutation:
Changing only the fill-day
Closefrom105to103.5changed the BUY fill from104.105to104.1035.The SELL fill, order type, submission time, fill time, quantity, and fees were unchanged.
Sell control
Baseline fill-day bar:
Mutation:
Changing only the fill-day
Closefrom97to98.5changed the SELL fill from97.903to97.9015.The BUY fill, order type, submission time, fill time, quantity, and fees were unchanged.
Relevant Source Path
The behavior appears to result from the interaction of three components:
EquityFillModel.MarketOnOpenFilluses theTradeBar.Openas the base MOO fill price and then applies the configured slippage model.ConstantSlippageModelcalculates slippage fromasset.GetLastData().Value.TradeBar,Valuerepresents the bar'sClose.This produces the empirically observed
Open ± Close × slippagePercentresult.Minimal Algorithm
Run this algorithm against three otherwise identical synthetic datasets:
Closefrom105to103.5.Closefrom97to98.5.Potential Solution
The important semantic requirement is that slippage for an opening fill must not depend on information unavailable at the open.
Possible approaches include:
I do not want to prescribe a specific implementation, but documenting the current behavior alone would not remove the look-ahead bias.
Evidence
The machine-readable comparison file is attached.
Related Issues
This appears related to, but not duplicated by:
Those issues concern MOO accuracy or stale/non-causal fills, but they do not describe the specific fill-day
Closedependency caused byDaily TradeBar + MarketOnOpen + ConstantSlippageModel.Checklist
Closeas the only changed input.