FutureFillModel fills a resting market order at the midnight extended-hours start on the previous session's quote bar, without stale wait or warning, when the subscription has no extended hours

Abierto
#9,909 5 comentarios 0 reacciones 0 asignados Ver en GitHub

Los mantenedores suelen responder en 1 día

Nadie ha tomado este issue todavía.

  • #9911 de @abhi-byte62 — cerrado sin fusionar

Evaluación

Dificultad
4/5
Tiempo estimado
3-5 días
Aptitud para principiantes
48/100
Tipo de issue
Error
Claridad
Bien especificado
Estado de actividad
Activo
Stack tecnológico
csharp

Línea de trabajo

Read Common/Orders/Fills/FutureFillModel.cs, especially MarketFill and the existing StopMarketFill hours check, then trace the stale-price logic in FillModel.cs. Reproduce with a futures subscription where extendedMarketHours is false and an order rests over a session close. Done when the order waits for fresh data or is marked stale, with a regression test covering the reported case.

Escrito por el modelo de indexación a partir del texto del issue.

Descripción

Expected Behavior

A futures market order that rests across a session close waits for fresh data for its contract before filling, or at least fills with the stale-price warning. In particular it should not fill at a time of day when the subscription cannot receive any data (an extended-hours segment on a subscription with extendedMarketHours: false), on a quote bar from the previous session.

Actual Behavior

FutureFillModel.MarketFill hardcodes IsExchangeOpen(asset, true) (Common/Orders/Fills/FutureFillModel.cs:45), so a resting market order on a future subscribed without extended market hours is filled at the first time step inside an extended-hours segment: for CBOT grains the premarket segment starts at 00:00 Chicago, so the fill lands at midnight exchange time. The data used is the last cached bar, the previous session's 13:17-13:18 quote bar (the regular session closes 13:20 and the subscription delivers nothing after it), and the fill price is that bar's bid/ask close.

No stale-price wait and no stale-price warning apply, because the stale check compares the data end time with the order submission time, not with the fill time (FutureFillModel.cs:57-65, FillModel.cs:304-312, FillModel.cs:1093): the bar ended 2 minutes before the order was placed, so pricesEndTimeUtc + StalePriceTimeSpan < order.Time is false even though at the moment of the fill the bar is 10 h 40 m old (58 h over a weekend).

For a thin deferred contract whose last quote bar of the day carries a wide resting book, the fill is the far side of that stale book (e.g. ask 301.9 against a 273.1 bid, or ask 292.0 against a 270.2 bid in a run over 2010) while the market at the time of the fill is elsewhere (bid 284.9 / ask 285.0 on 2010-05-03 00:00 Chicago for the second example).

FutureFillModel.StopMarketFill (FutureFillModel.cs:124) already gates on GetSubscriptionDataConfigs(asset).IsExtendedMarketHours(); MarketFill does not, which is the inconsistency.

Potential Solution

Either or both:

  1. In FutureFillModel.MarketFill, gate on the subscription's setting like StopMarketFill does: IsExchangeOpen(asset, GetSubscriptionDataConfigs(asset).IsExtendedMarketHours()). A regular-hours-only subscription then rests the order until the next regular session, where the first bar is fresh (verified: with extendedMarketHours: true the same order fills at 19:01 Chicago on the fresh evening bar, see below).
  2. Make the stale check in InternalMarketFill / FutureFillModel.MarketFill consider the age of the data at the fill time when the order has been resting (utcTime of the fill event rather than order.Time), so an order that rested across a close waits for fresh data per ShouldWaitForFreshDataOnStale, or at least carries FilledAtStalePrice.
Reproducing the Problem

QuantConnect Cloud, LEAN 2.5.0.0.18184 (also matches master a54e9cc for the lines cited). Backtest c2b01ac9c18f5436ebf3459d405b77f8 (project 37925270): fill Monday 2009-01-05 00:00 Chicago at the Friday 13:17 ask. Backtest 774bc1b258957ee80d4dfd691fd9b780 (project 37925312, same code with extended_market_hours=True): fill Sunday 2009-01-04 19:01 Chicago at the fresh bar's ask.

from AlgorithmImports import *


class FutureRestingMarketOrderFill(QCAlgorithm):
    """Place one market order on a deferred CBOT ZM contract at 14:20 New York (13:20 Chicago, the day-session
    close) and log when and on which cached bar it fills."""

    def initialize(self) -> None:
        self.set_start_date(2009, 1, 2)
        self.set_end_date(2009, 1, 6)
        self.set_cash(100000)
        future = self.add_future("ZM", Resolution.MINUTE, extended_market_hours=False)
        future.set_filter(0, 200)
        self._target = Symbol.create_future("ZM", Market.CBOT, datetime(2009, 5, 14))
        self._sent = False

    def on_data(self, slice: Slice) -> None:
        if self._sent or self._target not in self.securities:
            return
        if self.time.hour == 14 and self.time.minute == 20:
            sec = self.securities[self._target]
            qb = sec.cache.get_data[QuoteBar]()
            self.log(f"SUBMIT {self.time} local={sec.local_time} "
                     f"open_regular={sec.exchange.hours.is_open(sec.local_time, False)} "
                     f"open_extended={sec.exchange.hours.is_open(sec.local_time, True)} "
                     f"last_quote_bar={qb.time}->{qb.end_time} bid={qb.bid.close} ask={qb.ask.close} price={sec.price}")
            self.market_order(self._target, 1)
            self._sent = True

    def on_order_event(self, order_event: OrderEvent) -> None:
        if order_event.status != OrderStatus.FILLED:
            return
        sec = self.securities[order_event.symbol]
        last = sec.cache.get_data()
        qb = sec.cache.get_data[QuoteBar]()
        self.log(f"FILL {self.time} local={sec.local_time} fill_price={order_event.fill_price} "
                 f"message='{order_event.message}' "
                 f"open_regular={sec.exchange.hours.is_open(sec.local_time, False)} "
                 f"open_extended={sec.exchange.hours.is_open(sec.local_time, True)} "
                 f"last_data={type(last).__name__} {last.time}->{last.end_time} fill_forward={last.is_fill_forward} "
                 f"bid={qb.bid.close} ask={qb.ask.close}")

Output with extended_market_hours=False (algorithm time is New York):

2009-01-02 14:20:00 SUBMIT 2009-01-02 14:20:00 local=2009-01-02 13:20:00 open_regular=False open_extended=False last_quote_bar=2009-01-02 13:17:00->2009-01-02 13:18:00 bid=273.1 ask=301.9 price=287.5
2009-01-05 01:00:00 FILL 2009-01-05 01:00:00 local=2009-01-05 00:00:00 fill_price=301.9 message='' open_regular=False open_extended=True last_data=QuoteBar 2009-01-02 13:17:00->2009-01-02 13:18:00 fill_forward=False bid=273.1 ask=301.9

Output with extended_market_hours=True:

2009-01-02 14:20:00 SUBMIT 2009-01-02 14:20:00 local=2009-01-02 13:20:00 open_regular=False open_extended=False last_quote_bar=2009-01-02 13:17:00->2009-01-02 13:18:00 bid=273.1 ask=301.9 price=287.5
2009-01-04 20:01:00 FILL 2009-01-04 20:01:00 local=2009-01-04 19:01:00 fill_price=299.4 message='' open_regular=False open_extended=True last_data=QuoteBar 2009-01-04 19:00:00->2009-01-04 19:01:00 fill_forward=False bid=298.9 ask=299.4

The same pattern appears on every entry of a two-year probe that places market orders at 14:20 New York on ZL and ZM contracts without extended hours: all of them fill at 01:00 New York (00:00 Chicago) the next trading day on the previous session's last quote bar, none with a stale-price message.

Reported through support conversation 215476421928703.

Checklist
  • I have completely filled out this template
  • I have confirmed that this issue exists on the current master branch
  • I have confirmed that this is not a duplicate issue by searching issues
  • I have provided detailed steps to reproduce the issue
Lenguaje dominante
C#
Estrellas
22.5k
Forks
5.4k
Merge medio
1 d 2 min
PR fusionados (30 d)
71

Preparar el entorno

Abrir en Codespaces

Inicia el contenedor de desarrollo del proyecto en tu navegador, con tu propia cuenta de GitHub.

Primeros pasos

  1. Lee el issue completo y luego la guía de contribución del proyecto.
  2. Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
  3. Haz un fork del repositorio y trabaja en una rama.
  4. Abre un pull request que haga referencia al número del issue.

Más de QuantConnect/Lean

Todos los issues de QuantConnect/Lean

Issues similares

Más issues de C#

Recibe los nuevos issues en tu correo

Un resumen breve de issues de GitHub para principiantes.