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-decreasingts_initorder) - the parity seam (
OrderApi/PositionApi/HistoryApi/Broker— method surfaces copied verbatim from the topstep-sdk resources, soAsyncTopstepClientsatisfiesBrokerstructurally andSimBrokerimplements 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
¶
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
¶
Source code in src/topstep_backtest/protocols.py
now
¶
set_time_alert
¶
set_time_alert(name: str, at_ns: int, cb: Callable[[TimeEvent], None]) -> None
Source code in src/topstep_backtest/protocols.py
BarType
¶
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).
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.
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
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
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
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
cancel
async
¶
Source code in src/topstep_backtest/protocols.py
cancel_all
async
¶
Source code in src/topstep_backtest/protocols.py
search_open
async
¶
Source code in src/topstep_backtest/protocols.py
get
async
¶
Source code in src/topstep_backtest/protocols.py
wait_for_fill
async
¶
PositionApi
¶
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
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.
Liquidity
¶
PointKind
¶
Bases: IntEnum
Kind of a point on the deterministic intrabar price path.
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.
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.
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
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.
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.
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]