Skip to content

protocols

The structural types both the simulator and the live SDK satisfy. Bar lives here, including what its two timestamps mean.

protocols

THE canonical interface module — every subsystem imports from here.

This file is the frozen contract that resolves cross-subsystem interface drift (the #1 composition risk identified at design time). It defines:

  • the time contract (Clock, TimeEvent)
  • the data contract (Bar, BarType, DataFeed — bars stamped at CLOSE; feeds yield in non-decreasing ts_init order)
  • the parity seam (OrderApi / PositionApi / HistoryApi / Broker — method surfaces copied verbatim from the topstep-sdk resources, so AsyncTopstepClient satisfies Broker structurally and SimBroker implements the identical protocol)
  • the fill contract (MarketContext, PricePath, WorkingOrder, Fill, FillModel, FeeModel — the only realism component that changes across data tiers; strategies never import it)

Conventions (binding): - Timestamps are int nanoseconds since the UTC epoch. Every event carries ts_event (venue occurrence) and ts_init (engine ingest); dispatch order is strictly non-decreasing ts_init. - A Bar's ts_init equals its CLOSE time — a strategy physically cannot act on an unfinished bar. - All prices are Decimal on the instrument's tick grid. - Backtest data feeds are synchronous iterators (deterministic pull); live feeds adapt the SDK market hub separately.

PricePath module-attribute

PricePath = tuple[PathPoint, ...]

The deterministic intrabar price path: OPEN -> first extreme -> second extreme -> CLOSE. Pessimistic ordering: with an open position, the ADVERSE extreme comes first (long -> low first, short -> high first); flat defaults to the extreme nearer the open. Built once per bar by fills.path.build_path and shared by the fill model AND the rule engine's breach check so their relative ordering within a bar is decided by ONE walk, never two opinions.

TimeEvent

Bases: Struct

A named timer/alert firing at ts_ns.

name instance-attribute

name: str

ts_ns instance-attribute

ts_ns: int

Clock

Bases: Protocol

Swappable time source. ALL time-based logic flows through this —

never datetime.now(). TestClock advances only as the engine drains events; LiveClock is wall time. Identical API in both, so time-driven strategy/rule logic is parity-safe.

now_ns

now_ns() -> int
Source code in src/topstep_backtest/protocols.py
def now_ns(self) -> int: ...

now

now() -> datetime

Current time as a tz-aware UTC datetime.

Source code in src/topstep_backtest/protocols.py
def now(self) -> datetime:
    """Current time as a tz-aware UTC datetime."""
    ...

set_time_alert

set_time_alert(name: str, at_ns: int, cb: Callable[[TimeEvent], None]) -> None
Source code in src/topstep_backtest/protocols.py
def set_time_alert(self, name: str, at_ns: int, cb: Callable[[TimeEvent], None]) -> None: ...

set_timer

set_timer(name: str, interval_ns: int, cb: Callable[[TimeEvent], None]) -> None
Source code in src/topstep_backtest/protocols.py
def set_timer(self, name: str, interval_ns: int, cb: Callable[[TimeEvent], None]) -> None: ...

cancel_timer

cancel_timer(name: str) -> None
Source code in src/topstep_backtest/protocols.py
def cancel_timer(self, name: str) -> None: ...

BarType

Bases: Struct

Identifies a bar stream: instrument + step + unit.

contract_id is the gateway contract id (e.g. "CON.F.US.MNQ.U26").

contract_id instance-attribute

contract_id: str

unit instance-attribute

unit: AggregateBarUnit

unit_number instance-attribute

unit_number: int

Bar

Bases: Struct

An OHLCV bar. ts_init == bar CLOSE time (the no-look-ahead anchor);

ts_event == bar OPEN time (when the bar's window began at the venue).

bar_type instance-attribute

bar_type: BarType

ts_event instance-attribute

ts_event: int

ts_init instance-attribute

ts_init: int

open instance-attribute

open: Decimal

high instance-attribute

high: Decimal

low instance-attribute

low: Decimal

close instance-attribute

close: Decimal

volume instance-attribute

volume: int

DataFeed

Bases: Protocol

A time-ordered source of bars for backtests.

MUST yield bars in non-decreasing ts_init order across ALL instruments (one merged stream). The engine asserts this invariant on dispatch.

instruments

instruments() -> Sequence[str]

The contract ids this feed emits.

Source code in src/topstep_backtest/protocols.py
def instruments(self) -> Sequence[str]:
    """The contract ids this feed emits."""
    ...

OrderApi

Bases: Protocol

Order surface — matches topstep_sdk.resources.order.OrderResource.

place async

place(account_id: int, contract_id: str, *, side: OrderSide | int, type: OrderType | int, size: int, limit_price: float | Decimal | None = None, stop_price: float | Decimal | None = None, trail_price: float | Decimal | None = None, custom_tag: str | None = None, stop_loss_bracket: PlaceOrderBracket | dict[str, int] | None = None, take_profit_bracket: PlaceOrderBracket | dict[str, int] | None = None, stop_loss_ticks: int | None = None, take_profit_ticks: int | None = None) -> int
Source code in src/topstep_backtest/protocols.py
async def place(
    self,
    account_id: int,
    contract_id: str,
    *,
    side: OrderSide | int,
    type: OrderType | int,
    size: int,
    limit_price: float | Decimal | None = None,
    stop_price: float | Decimal | None = None,
    trail_price: float | Decimal | None = None,
    custom_tag: str | None = None,
    stop_loss_bracket: PlaceOrderBracket | dict[str, int] | None = None,
    take_profit_bracket: PlaceOrderBracket | dict[str, int] | None = None,
    stop_loss_ticks: int | None = None,
    take_profit_ticks: int | None = None,
) -> int: ...

buy async

buy(account_id: int, contract_id: str, size: int, *, type: OrderType | int = MARKET, limit_price: float | Decimal | None = None, stop_price: float | Decimal | None = None, trail_price: float | Decimal | None = None, custom_tag: str | None = None, stop_loss_bracket: PlaceOrderBracket | dict[str, int] | None = None, take_profit_bracket: PlaceOrderBracket | dict[str, int] | None = None, stop_loss_ticks: int | None = None, take_profit_ticks: int | None = None) -> int
Source code in src/topstep_backtest/protocols.py
async def buy(
    self,
    account_id: int,
    contract_id: str,
    size: int,
    *,
    type: OrderType | int = OrderType.MARKET,
    limit_price: float | Decimal | None = None,
    stop_price: float | Decimal | None = None,
    trail_price: float | Decimal | None = None,
    custom_tag: str | None = None,
    stop_loss_bracket: PlaceOrderBracket | dict[str, int] | None = None,
    take_profit_bracket: PlaceOrderBracket | dict[str, int] | None = None,
    stop_loss_ticks: int | None = None,
    take_profit_ticks: int | None = None,
) -> int: ...

sell async

sell(account_id: int, contract_id: str, size: int, *, type: OrderType | int = MARKET, limit_price: float | Decimal | None = None, stop_price: float | Decimal | None = None, trail_price: float | Decimal | None = None, custom_tag: str | None = None, stop_loss_bracket: PlaceOrderBracket | dict[str, int] | None = None, take_profit_bracket: PlaceOrderBracket | dict[str, int] | None = None, stop_loss_ticks: int | None = None, take_profit_ticks: int | None = None) -> int
Source code in src/topstep_backtest/protocols.py
async def sell(
    self,
    account_id: int,
    contract_id: str,
    size: int,
    *,
    type: OrderType | int = OrderType.MARKET,
    limit_price: float | Decimal | None = None,
    stop_price: float | Decimal | None = None,
    trail_price: float | Decimal | None = None,
    custom_tag: str | None = None,
    stop_loss_bracket: PlaceOrderBracket | dict[str, int] | None = None,
    take_profit_bracket: PlaceOrderBracket | dict[str, int] | None = None,
    stop_loss_ticks: int | None = None,
    take_profit_ticks: int | None = None,
) -> int: ...

modify async

modify(account_id: int, order_id: int, *, size: int | None = None, limit_price: float | Decimal | None = None, stop_price: float | Decimal | None = None, trail_price: float | Decimal | None = None) -> None
Source code in src/topstep_backtest/protocols.py
async def modify(
    self,
    account_id: int,
    order_id: int,
    *,
    size: int | None = None,
    limit_price: float | Decimal | None = None,
    stop_price: float | Decimal | None = None,
    trail_price: float | Decimal | None = None,
) -> None: ...

cancel async

cancel(account_id: int, order_id: int) -> None
Source code in src/topstep_backtest/protocols.py
async def cancel(self, account_id: int, order_id: int) -> None: ...

cancel_all async

cancel_all(account_id: int) -> list[int]
Source code in src/topstep_backtest/protocols.py
async def cancel_all(self, account_id: int) -> list[int]: ...

search_open async

search_open(account_id: int) -> list[OrderModel]
Source code in src/topstep_backtest/protocols.py
async def search_open(self, account_id: int) -> list[OrderModel]: ...

get async

get(account_id: int, order_id: int) -> OrderModel | None
Source code in src/topstep_backtest/protocols.py
async def get(self, account_id: int, order_id: int) -> OrderModel | None: ...

wait_for_fill async

wait_for_fill(account_id: int, order_id: int, *, timeout: float = 30.0, poll_interval: float = 1.0) -> OrderModel
Source code in src/topstep_backtest/protocols.py
async def wait_for_fill(
    self,
    account_id: int,
    order_id: int,
    *,
    timeout: float = 30.0,  # noqa: ASYNC109 - mirrors the SDK signature
    poll_interval: float = 1.0,
) -> OrderModel: ...

PositionApi

Bases: Protocol

Position surface — matches topstep_sdk.resources.position.PositionResource.

search_open async

search_open(account_id: int) -> list[PositionModel]
Source code in src/topstep_backtest/protocols.py
async def search_open(self, account_id: int) -> list[PositionModel]: ...

close async

close(account_id: int, contract_id: str) -> None
Source code in src/topstep_backtest/protocols.py
async def close(self, account_id: int, contract_id: str) -> None: ...

partial_close async

partial_close(account_id: int, contract_id: str, size: int) -> None
Source code in src/topstep_backtest/protocols.py
async def partial_close(self, account_id: int, contract_id: str, size: int) -> None: ...

close_all async

close_all(account_id: int) -> list[str]
Source code in src/topstep_backtest/protocols.py
async def close_all(self, account_id: int) -> list[str]: ...

HistoryApi

Bases: Protocol

History surface — matches topstep_sdk.resources.history.HistoryResource.

retrieve_bars async

retrieve_bars(contract_id: str, *, unit: AggregateBarUnit | int, unit_number: int, start_time: datetime | str, end_time: datetime | str, limit: int = 1000, live: bool = False, include_partial_bar: bool = False) -> list[AggregateBarModel]
Source code in src/topstep_backtest/protocols.py
async def retrieve_bars(
    self,
    contract_id: str,
    *,
    unit: AggregateBarUnit | int,
    unit_number: int,
    start_time: datetime | str,
    end_time: datetime | str,
    limit: int = 1000,
    live: bool = False,
    include_partial_bar: bool = False,
) -> list[AggregateBarModel]: ...

Broker

Bases: Protocol

The write-once seam: AsyncTopstepClient satisfies this structurally

(its .orders/.positions/.history resources match the protocols above verbatim) and SimBroker implements the identical surface — swapping sim <-> live is pure wiring, with zero strategy change.

orders property

orders: OrderApi

positions property

positions: PositionApi

history property

history: HistoryApi

Liquidity

Bases: IntEnum

MAKER class-attribute instance-attribute

MAKER = 0

TAKER class-attribute instance-attribute

TAKER = 1

PointKind

Bases: IntEnum

Kind of a point on the deterministic intrabar price path.

OPEN class-attribute instance-attribute

OPEN = 0

EXTREME_FIRST class-attribute instance-attribute

EXTREME_FIRST = 1

EXTREME_SECOND class-attribute instance-attribute

EXTREME_SECOND = 2

CLOSE class-attribute instance-attribute

CLOSE = 3

PathPoint

Bases: Struct

One waypoint of the intrabar path; consecutive points bound a monotonic

price segment. seq orders all intrabar happenings (fills, rule-breach liquidations) deterministically along the path.

seq instance-attribute

seq: int

kind instance-attribute

kind: PointKind

price instance-attribute

price: Decimal

MarketContext

Bases: Struct

Tier-polymorphic market slice the engine feeds a FillModel.

Exactly one of the tier fields is populated per event (Tier 0: bar). A fill model may read only data at/before ts_event — never ahead.

ts_event instance-attribute

ts_event: int

ts_init instance-attribute

ts_init: int

instrument instance-attribute

instrument: InstrumentSpec

bar class-attribute instance-attribute

bar: Bar | None = None

quote class-attribute instance-attribute

quote: QuoteData | None = None

last_trade class-attribute instance-attribute

last_trade: MarketTradeData | None = None

prev_close class-attribute instance-attribute

prev_close: Decimal | None = None

WorkingOrder

WorkingOrder(*, order_id: int, account_id: int, contract_id: str, side: OrderSide, type: OrderType, size: int, accepted_ts: int, limit_price: Decimal | None = None, stop_price: Decimal | None = None, trail_stop_price: Decimal | None = None, trail_distance_ticks: int | None = None, custom_tag: str | None = None, parent_order_id: int | None = None, linked_order_id: int | None = None, reduce_only: bool = False)

Mutable engine-owned order lifecycle state (NOT part of any message).

accepted_ts is the no-look-ahead firewall: an order participates in a bar only if accepted_ts <= bar.ts_event (i.e. it existed at or before the bar's open) — a close-signal order can never fill inside its own bar.

Source code in src/topstep_backtest/protocols.py
def __init__(
    self,
    *,
    order_id: int,
    account_id: int,
    contract_id: str,
    side: OrderSide,
    type: OrderType,
    size: int,
    accepted_ts: int,
    limit_price: Decimal | None = None,
    stop_price: Decimal | None = None,
    trail_stop_price: Decimal | None = None,
    trail_distance_ticks: int | None = None,
    custom_tag: str | None = None,
    parent_order_id: int | None = None,
    linked_order_id: int | None = None,
    reduce_only: bool = False,
) -> None:
    from topstep_sdk import OrderStatus  # local import avoids cycle at module load

    self.order_id = order_id
    self.account_id = account_id
    self.contract_id = contract_id
    self.side = side
    self.type = type
    self.size = size
    self.accepted_ts = accepted_ts
    self.limit_price = limit_price
    self.stop_price = stop_price
    self.trail_stop_price = trail_stop_price
    self.trail_distance_ticks = trail_distance_ticks
    self.custom_tag = custom_tag
    self.parent_order_id = parent_order_id
    self.linked_order_id = linked_order_id
    self.reduce_only = reduce_only
    self.filled_qty = 0
    self.avg_fill_price: Decimal | None = None
    self.status = OrderStatus.OPEN

order_id instance-attribute

order_id = order_id

account_id instance-attribute

account_id = account_id

contract_id instance-attribute

contract_id = contract_id

side instance-attribute

side = side

type instance-attribute

type = type

size instance-attribute

size = size

accepted_ts instance-attribute

accepted_ts = accepted_ts

limit_price instance-attribute

limit_price = limit_price

stop_price instance-attribute

stop_price = stop_price

trail_stop_price instance-attribute

trail_stop_price = trail_stop_price

trail_distance_ticks instance-attribute

trail_distance_ticks = trail_distance_ticks

custom_tag instance-attribute

custom_tag = custom_tag

parent_order_id instance-attribute

parent_order_id = parent_order_id

linked_order_id instance-attribute

linked_order_id = linked_order_id

reduce_only instance-attribute

reduce_only = reduce_only

filled_qty instance-attribute

filled_qty = 0

avg_fill_price instance-attribute

avg_fill_price: Decimal | None = None

status instance-attribute

status = OrderStatus.OPEN

remaining property

remaining: int

Fill

Bases: Struct

One execution produced by a fill model.

seq is the path point starting the segment where the fill triggers; trigger_price is the LEVEL that was touched (stop/limit level, or the open) — the broker orders intrabar events by (seq, |trigger - seg_start|), NOT by the slippage-adjusted fill price. Ties resolve by order id.

order_id instance-attribute

order_id: int

price instance-attribute

price: Decimal

qty instance-attribute

qty: int

ts_event instance-attribute

ts_event: int

seq instance-attribute

seq: int

liquidity instance-attribute

liquidity: Liquidity

trigger_price class-attribute instance-attribute

trigger_price: Decimal | None = None

note class-attribute instance-attribute

note: str = ''

FillModel

Bases: Protocol

The ONLY component that changes across data tiers (bar -> L1 -> L2 -> MBO).

Given one order and the current market slice + shared intrabar path, decide whether/where it fills. Must be deterministic (any randomness seeded) and must never read past ctx.ts_event.

try_fill

try_fill(order: WorkingOrder, ctx: MarketContext, path: PricePath) -> list[Fill]
Source code in src/topstep_backtest/protocols.py
def try_fill(
    self,
    order: WorkingOrder,
    ctx: MarketContext,
    path: PricePath,
) -> list[Fill]: ...

FeeModel

Bases: Protocol

Per-side, per-instrument cost, charged on entry AND exit.

Returns (exchange_and_nfa_fees, broker_commission) per the SDK's HalfTradeModel split (fees vs commissions).

fee

fee(instrument: InstrumentSpec, side: OrderSide, qty: int, liquidity: Liquidity) -> tuple[Decimal, Decimal]
Source code in src/topstep_backtest/protocols.py
def fee(
    self,
    instrument: InstrumentSpec,
    side: OrderSide,
    qty: int,
    liquidity: Liquidity,
) -> tuple[Decimal, Decimal]: ...