strategy.base¶
The lower-level Strategy protocol and StrategyContext — the seam that lets one class run against the simulator or the live gateway.
base
¶
The write-once Strategy base class and its injected context.
A strategy subclasses :class:Strategy and talks ONLY to self.ctx —
protocol-typed edges (OrderApi/PositionApi/HistoryApi/Clock).
The SAME subclass runs in backtest (wired to a SimBroker) and live (wired
to topstep_sdk.AsyncTopstepClient), because both satisfy the identical
structural protocols. Never import a concrete broker or fill model here.
StrategyContext
dataclass
¶
StrategyContext(orders: OrderApi, positions: PositionApi, history: HistoryApi, clock: Clock, account_id: int, instruments: Mapping[str, InstrumentSpec] = dict[str, InstrumentSpec]())
Everything a strategy may touch. Protocol-typed: sim and live inject
different concretions behind the same names.
instruments
class-attribute
instance-attribute
¶
instruments: Mapping[str, InstrumentSpec] = field(default_factory=dict[str, InstrumentSpec])
from_broker
classmethod
¶
from_broker(broker: Broker, *, clock: Clock, account_id: int, instruments: Mapping[str, InstrumentSpec] | None = None) -> StrategyContext
Source code in src/topstep_backtest/strategy/base.py
instrument
¶
instrument(contract_id: str) -> InstrumentSpec
Spec for a contract id (registered mapping first, table fallback).
Source code in src/topstep_backtest/strategy/base.py
Strategy
¶
Base strategy. Override the hooks you need; all are optional.
Lifecycle: on_start -> (on_bar / on_order / on_fill /
on_position)* -> on_stop. Order-state changes arrive via
on_order; executions via on_fill (the parity-safe pattern in both
sim and live — never busy-poll wait_for_fill in a backtest).
Drivers (the BacktestEngine, the future live runner) deliver events
through handle_* — pure pass-throughs here. User strategies override
on_*; framework base classes interpose in handle_*, so overriding
a user hook can never sever framework bookkeeping.
bind
¶
bind(ctx: StrategyContext) -> None
note
¶
Attach a free-text breadcrumb to the current bar's replay frame.
This is how a strategy explains WHY, which no recorder can infer from
the order flow: self.note(f"cross up, {self.fast.value} > ...").
A pure sink — with no recorder attached (Backtest(record=False),
the default) it is a no-op, and with one attached it writes to the
recording only. It can never influence the run: results are
byte-identical with and without notes (pinned by a golden test).
Source code in src/topstep_backtest/strategy/base.py
on_start
¶
on_order
async
¶
on_fill
async
¶
on_position
async
¶
on_stop
¶
handle_bar
async
¶
handle_bar(bar: Bar) -> None
Driver entry point for a completed bar (drivers call handle_*,
never on_*); the default simply awaits the user hook.