Skip to content

Market orders wait for fresh data instead of filling on stale prices#9535

Open
Martin-Molinero wants to merge 6 commits into
QuantConnect:masterfrom
Martin-Molinero:bug-market-fill-stale-data
Open

Market orders wait for fresh data instead of filling on stale prices#9535
Martin-Molinero wants to merge 6 commits into
QuantConnect:masterfrom
Martin-Molinero:bug-market-fill-stale-data

Conversation

@Martin-Molinero

@Martin-Molinero Martin-Molinero commented Jun 16, 2026

Copy link
Copy Markdown
Member

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), EquityFillModel and FutureFillModel return an unfilled order event when the best available price is older than StalePriceTimeSpan and the asset is subscribed only at hour/daily resolution, so the order stays pending and fills on the next bar (via the shared ShouldWaitForFreshData helper).
  • For minute / second / tick subscriptions the previous behavior is kept (fill on the stale price with a warning), since stale data at those resolutions is a genuine gap rather than a bar still forming.

StalePriceTimeSpan keeps 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 shared FillModel.GetMarketFillPrice helper used by the base FillModel and FutureFillModel. Equity fills are untouched: a resting equity order is already converted to MarketOnOpen by QCAlgorithm.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?

  • Added 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).
  • Added 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).
  • Updated FillOutsideHoursDailyResolutionAlgorithm — the daily order now waits for the next close instead of filling on the stale 23:00 data.
  • FutureOptionDailyRegressionAlgorithm now 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.
  • Regenerated statistics for the hour/daily algorithms whose fills change (BasicTemplateIndexHourly, BasicTemplateIndexOptionsHourly, BasicTemplateFuturesDaily, HSIFutureDaily, FutureOption{Daily,Hourly}, AddBetaIndicatorNewAssets, CoinbaseCryptoYearMarketTrading). Minute/second/tick algorithms are unaffected.
  • All fill-model unit tests pass.

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)

Checklist

  • My code follows the code style of this project.
  • I have added tests to cover my changes.
  • All new and existing tests passed.

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 / OrderListHash change 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.

Algorithm Total Orders (before -> after) Why it changed
BasicTemplateIndexHourly, BasicTemplateIndexOptionsHourly 81 -> 19 Ping-pong buy/liquidate of an SPX option every hourly bar. The SPX hourly sample data is sparse and fill-forwarded, so most bars repeat an older real bar; the orders used to fill on that stale price every hour and now fill only when a fresh bar arrives.
BasicTemplateFuturesHourly 718 -> 66 Buys the front contract when flat / liquidates when invested. On hourly data that is mostly fill-forwarded, both legs used to fill on every fill-forwarded bar; they now fill only on genuinely fresh bars — hence the large drop.
BasicTemplateFuturesWithExtendedMarketHourly 1992 -> 170 As above, including the (also fill-forwarded) extended-hours bars.
HSIFutureHour 57 -> 44 As above (HSI future, hourly).
BasicTemplateFuturesDaily 38 -> 22 Buys the front contract when flat (this fills at the real daily close) and liquidates when invested. The liquidation only fires on the overnight pulse: the continuous-contract roll / delisting auxiliary data is timestamped at midnight, when the futures exchange is open, and at that moment the only price available is the stale fill-forwarded daily bar. Those liquidations (and re-buys on fill-forwarded bars) used to fill on the stale price; they now wait for fresh data, so fewer round-trips complete.
BasicTemplateFuturesWithExtendedMarketDaily 36 -> 22 As BasicTemplateFuturesDaily.
HSIFutureDaily 15 -> 9 As BasicTemplateFuturesDaily (HSI future, daily).
AddBetaIndicatorNewAssets 436 -> 327 Buys BTCUSD when flat on each daily bar and liquidates when `
CoinbaseCryptoYearMarketTrading 388 -> 388 Submits one market order every daily bar via an unconditional buy/liquidate toggle, so the count is fixed at the number of daily bars (unchanged). On the days whose Coinbase daily bar is fill-forwarded, the order now waits for the next fresh bar, so several fills land at different prices — changing End Equity / Net Profit / OrderListHash but not the count.
FutureOptionDaily 2 -> 2 (restructured) It bought and liquidated on the same day; on daily data both legs filled at the same stale close (a degenerate ~0 P&L round trip). A same-day buy can no longer fill on stale data, so it now buys one day and liquidates the next (StartDate moved to 1/6, DataPoints 27 -> 36), a genuine two-day round trip. Still 2 orders, but real fill prices and a new hash.
FutureOptionHourly unchanged No statistics change. Hourly data fills within the day, so the original same-day buy/liquidate still works; it only overrides StartDate/EndDate back to 1/7-1/8 so it does not inherit the daily base's new wider window.
FillOutsideHoursDailyResolution 1 -> 1 A daily market order submitted at 23:00 used to fill immediately on the prior 16:00 close (~7h stale); it now waits for the next daily close (different fill price/hash).
FillOutsideHoursMinuteResolution (assertion only) Shared base-class assertion updated so that no resolution fills on the stale outside-hours bar at submission. Minute/second/tick still fill at the next open exactly as before, so there is no statistics change.

🤖 Generated with Claude Code

@Martin-Molinero Martin-Molinero force-pushed the bug-market-fill-stale-data branch 3 times, most recently from 4349cd8 to d38d92a Compare June 16, 2026 22:03
Martin-Molinero and others added 4 commits June 17, 2026 17:38
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>
@Martin-Molinero Martin-Molinero force-pushed the bug-market-fill-stale-data branch from e3a144f to bafd5dc Compare June 17, 2026 20:45
Martin-Molinero and others added 2 commits June 17, 2026 17:53
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant