Market orders wait for fresh data instead of filling on stale prices#9535
Open
Martin-Molinero wants to merge 6 commits into
Open
Market orders wait for fresh data instead of filling on stale prices#9535Martin-Molinero wants to merge 6 commits into
Martin-Molinero wants to merge 6 commits into
Conversation
4349cd8 to
d38d92a
Compare
A market order would previously fill immediately on the most recent available data even when that data was older than StalePriceTimeSpan (default one hour), only attaching a warning. This is unrealistic for a coarse resolution asset (hour/daily) where the latest bar is the stale previous close when the order is placed mid-bar or via an intraday scheduled event. The default fill models (FillModel, EquityFillModel, FutureFillModel) now wait for fresh data instead of filling on a stale price, but only for hour and daily resolutions; the order fills when the next bar closes. For minute/second/tick subscriptions the previous behavior is kept (fill on the stale price with a warning), since stale data there is a genuine gap rather than a bar still forming. Adds HourResolutionMarketOrderStalePriceRegressionAlgorithm, updates the FillOutsideHours daily expectation, and regenerates statistics for the hour/daily algorithms whose fills change. FutureOptionDaily buys and liquidates a day apart now (a same-day buy + liquidate cannot fill on daily data once stale fills are disabled). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The interface and class docs now match and reflect the actual behavior: the wait-for-fresh-data only applies to hour/daily resolutions, while minute/second/tick subscriptions still fill on stale data with a warning. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A hour/daily market order that was resting before the current bar opened (it predates the bar - placed after the previous close or while waiting for fresh data) now fills at the bar open, the price when trading resumed (like a MarketOnOpen), instead of the bar close. Orders placed during the bar still fill at the current/close price, so intraday mid-bar fills are unchanged. Equity fills are unchanged (resting equity orders are already converted to MarketOnOpen by QCAlgorithm.MarketOrder). Adds the shared FillModel.GetMarketFillPrice helper used by the base FillModel and FutureFillModel, a unit test, and regenerates statistics for the affected daily/hour futures, index and crypto regression algorithms (order counts unchanged, only fill prices). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…bar open RestingMarketOrderFillsAtBarOpenRegressionAlgorithm buys a daily future on the bar that delivers it (fills at that bar's close) and submits a liquidation while the market is closed (overnight pulse, no fresh bar). The liquidation rests and fills on a later bar at the bar open, not its close - asserting the new GetMarketFillPrice behavior. The in-bar buy is asserted to fill at the close, for contrast. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
e3a144f to
bafd5dc
Compare
Add Prices.Time (the bar start, mirroring BaseData.Time/EndTime), populated from the source bar/tick in every GetPrices path. GetMarketFillPrice now uses prices.Time directly instead of a second asset.Cache.GetData() lookup. Behavior is unchanged (prices.Time equals the previously read cache time). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… latest close HourMarketOrderFillsAtBarCloseRegressionAlgorithm submits an hour resolution market order mid-bar (via an intraday scheduled event) while the market is open, using the default one hour StalePriceTimeSpan. It asserts the order fills immediately at the latest available bar's close - not waiting and not at the bar open - since the latest bar is within the stale window. Guards the resting-order open-fill behavior against affecting ordinary in-session fills. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
A market order would previously fill immediately on the most recent available data even when that data was older than
StalePriceTimeSpan(default one hour), only attaching a warning message. That is unrealistic when a market order is placed mid-bar for a coarse resolution asset (hour/daily) or through an intraday scheduled event, where the latest bar is the stale previous close.The default fill models now wait for fresh data instead of filling on a stale price — but only for hour and daily resolutions (the order fills when the next bar closes):
FillModel(base),EquityFillModelandFutureFillModelreturn an unfilled order event when the best available price is older thanStalePriceTimeSpanand the asset is subscribed only at hour/daily resolution, so the order stays pending and fills on the next bar (via the sharedShouldWaitForFreshDatahelper).StalePriceTimeSpankeeps its one hour default; tighten it (e.g. to one minute) to make hour/daily orders wait for the next bar more aggressively.When a waiting (or otherwise resting) market order does fill, it fills at the open of the bar trading resumes on - the price when the market reopened, like a
MarketOnOpen- rather than that bar's close, when the order predates the bar (it was placed before the bar opened, e.g. after the previous close or while waiting). A market order placed during a bar still fills at the current/close price, so ordinary intraday mid-bar fills are unchanged. This is implemented by the sharedFillModel.GetMarketFillPricehelper used by the baseFillModelandFutureFillModel. Equity fills are untouched: a resting equity order is already converted toMarketOnOpenbyQCAlgorithm.MarketOrder, and the equity orders that reach the fill model are mid-bar.Related Issue
Mixing daily/hour resolution assets with intraday scheduled events or lower resolution assets, where market orders fill at the stale previous close.
Motivation and Context
Filling at a stale, already past price silently mis-prices fills. Waiting for the next bar produces a realistic fill (e.g. an hour-resolution order placed at minute 55 fills on the next hour bar instead of the 55-minute-old previous bar), while leaving high resolution fills untouched.
How Has This Been Tested?
RestingMarketOrderFillsAtBarOpenRegressionAlgorithm(a daily future: the in-bar buy fills at the bar close; a liquidation submitted while the market is closed rests and fills on a later bar at the bar open, asserting the resting-order open-fill).HourResolutionMarketOrderStalePriceRegressionAlgorithm(opts into a one minute stale window; asserts a mid-bar hour order fills on the next hour bar, not the stale previous bar).FillOutsideHoursDailyResolutionAlgorithm— the daily order now waits for the next close instead of filling on the stale 23:00 data.FutureOptionDailyRegressionAlgorithmnow buys and liquidates a day apart (a same-day buy + liquidate cannot fill on daily data once stale fills are disabled); the hourly variant is unchanged.BasicTemplateIndexHourly,BasicTemplateIndexOptionsHourly,BasicTemplateFuturesDaily,HSIFutureDaily,FutureOption{Daily,Hourly},AddBetaIndicatorNewAssets,CoinbaseCryptoYearMarketTrading). Minute/second/tick algorithms are unaffected.Types of changes
Checklist
Affected regression algorithms — why each changed
Shared root cause: in the open-source sample data, the hour/daily subscriptions used by these algorithms are sparse and heavily fill-forwarded, so most hour/daily bars just repeat an older real bar (a stale price). These algorithms place market orders on (nearly) every bar. Previously each order filled immediately on whatever was cached — including fill-forwarded bars and the stale previous daily close. Now, on hour/daily subscriptions, a market order waits for genuinely fresh data, so fills land only on real bars. Where many fills were happening on fill-forwarded bars the order count drops; where the count is fixed by the algorithm's structure, the fill prices /
OrderListHashchange instead. Minute/second/tick algorithms are unaffected.Note: for the daily/hour futures, index and crypto algorithms below, a second effect now also applies - a resting order (one placed before the bar it fills on) fills at that bar's open instead of its close. This changes fill prices and
OrderListHash(and PnL) but not the order counts shown.Note: delisting-triggered liquidations are not affected — the engine force-liquidates a delisted holding directly at the last price (
BrokerageTransactionHandler.HandleDelistingNotification, which bypasses the fill model), so a waiting order never strands a position past delisting.BasicTemplateIndexHourly,BasicTemplateIndexOptionsHourlyBasicTemplateFuturesHourlyBasicTemplateFuturesWithExtendedMarketHourlyHSIFutureHourBasicTemplateFuturesDailyBasicTemplateFuturesWithExtendedMarketDailyBasicTemplateFuturesDaily.HSIFutureDailyBasicTemplateFuturesDaily(HSI future, daily).AddBetaIndicatorNewAssetsCoinbaseCryptoYearMarketTradingOrderListHashbut not the count.FutureOptionDailyDataPoints27 -> 36), a genuine two-day round trip. Still 2 orders, but real fill prices and a new hash.FutureOptionHourlyStartDate/EndDateback to 1/7-1/8 so it does not inherit the daily base's new wider window.FillOutsideHoursDailyResolutionFillOutsideHoursMinuteResolution🤖 Generated with Claude Code