Skip to content

fills.bar_fill

The Tier-0 fill model and its one pessimistic intrabar path — the assumption every fill price in a report rests on.

bar_fill

Tier-0 bar-based fill model — the pessimistic execution realism layer.

Anti-optimism is the whole point: whenever a bar leaves the fill ambiguous, this model resolves it AGAINST the trader. Concretely:

  • Participation firewall (no look-ahead): an order interacts with a bar only if it was accepted at/before the bar's OPEN (accepted_ts <= bar.ts_event). An order created on a bar's close signal therefore first participates in the NEXT bar — the classic backtest look-ahead bug is structurally impossible.
  • Limits need trade-THROUGH: by default a resting limit does not fill on an exact touch of its level — the bar must trade at least one tick past it (an exact-touch extreme is exactly where real queues don't get filled). This applies at the OPEN too: a bar opening exactly at the level is a touch, not price improvement, so it does not fill at the open under the default; the order can still fill at its level if the bar later trades through. fill_limit_on_touch=True relaxes both for sensitivity analysis.
  • Stops always pay slippage: a triggered stop fills stop_slippage_ticks ADVERSE of its trigger, and a gap through the stop fills at the (worse) open plus slippage — never at the stop price itself. The slipped price MAY exceed the bar's high/low: real stop slippage escapes the bar's printed range, and clamping it back inside would be optimistic. That is intentional.
  • Marketable-at-open limits DO take the improvement: an open STRICTLY better than the level is genuine price improvement, not optimism — a resting buy limit above the open would fill at the open in reality too.
  • Honest timestamps: open-instant fills are stamped with the bar's OPEN time (bar.ts_event); every intrabar fill is stamped with the bar's CLOSE time (bar.ts_init) — the honest upper bound for an unknown intrabar time.
  • Trigger levels: every fill carries trigger_price — the LEVEL that triggered it (stop level, limit level, or the open), never the slippage-adjusted price. The broker orders intrabar events by trigger.

All prices are produced by integer-tick arithmetic on the instrument grid; a fill price can never land off-grid. Fills are for the FULL remaining quantity (Tier-0 models no partials).

BarFillConfig

Bases: Struct

Knobs of the Tier-0 pessimism model (all default to the conservative side).

stop_slippage_ticks class-attribute instance-attribute

stop_slippage_ticks: int = 1

Adverse ticks added to EVERY triggered stop fill (gap or intrabar).

market_slippage_ticks class-attribute instance-attribute

market_slippage_ticks: int = 0

Adverse ticks on market fills (0: the modeled micros are liquid enough).

fill_limit_on_touch class-attribute instance-attribute

fill_limit_on_touch: bool = False

False (default) = a limit fills only if the bar trades >= 1 tick THROUGH its level; True = an exact touch of the level suffices.

BarFillModel

BarFillModel(config: BarFillConfig | None = None)

Tier-0 FillModel: decides fills from OHLC bars + the shared path.

Conforms structurally to protocols.FillModel. Deterministic — no randomness at all at this tier.

Source code in src/topstep_backtest/fills/bar_fill.py
def __init__(self, config: BarFillConfig | None = None) -> None:
    self._config = config if config is not None else BarFillConfig()

config property

config: BarFillConfig

try_fill

try_fill(order: WorkingOrder, ctx: MarketContext, path: PricePath) -> list[Fill]
Source code in src/topstep_backtest/fills/bar_fill.py
def try_fill(
    self,
    order: WorkingOrder,
    ctx: MarketContext,
    path: PricePath,
) -> list[Fill]:
    bar = ctx.bar
    if bar is None:  # Tier-0 always sets it; refuse to guess otherwise
        return []
    if order.status != OrderStatus.OPEN or order.remaining <= 0:
        return []
    # Participation firewall: the order must have existed at/before this
    # bar's OPEN. An order accepted at a bar's close (ts_init) first
    # participates in the NEXT bar.
    if order.accepted_ts > bar.ts_event:
        return []

    tick = ctx.instrument.tick_size
    otype = OrderType(order.type)
    if otype == OrderType.MARKET:
        execution = self._market(order, bar, tick)
    elif otype == OrderType.LIMIT:
        execution = self._limit(order, bar, path, tick)
    elif otype == OrderType.STOP:
        if order.stop_price is None:
            return []
        execution = self._stop(order, bar, path, tick, order.stop_price)
    elif otype == OrderType.TRAILING_STOP:
        # The broker maintains trail_stop_price; without it there is no
        # trigger to evaluate.
        if order.trail_stop_price is None:
            return []
        execution = self._stop(order, bar, path, tick, order.trail_stop_price)
    else:
        # STOP_LIMIT (and any other type) is unsupported at Tier-0; the
        # broker rejects placement, and we never fill it here.
        return []

    if execution is None:
        return []

    liquidity = Liquidity.MAKER if otype == OrderType.LIMIT else Liquidity.TAKER
    # Open-instant fills happen at the bar's OPEN time; an intrabar fill's
    # true time is unknown, so it is stamped at the bar's CLOSE (ts_init) —
    # the honest upper bound. Never ctx.ts_event: the broker sets that to
    # the bar OPEN, which would make every fill look like an open fill.
    return [
        Fill(
            order_id=order.order_id,
            price=execution.price,
            qty=order.remaining,  # Tier-0: always the full remaining qty
            ts_event=bar.ts_event if execution.at_open else bar.ts_init,
            seq=execution.seq,
            liquidity=liquidity,
            trigger_price=execution.trigger,
        )
    ]