Strategies are event driven. The runtime dispatches events to handlers on your class, applies the actions you return, and advances the simulation clock. Your code never calls the exchange, the broker, or the data feed directly; it only receives events and returns actions. Symbol reference lives in Strategy SDK.
The class shape
Your strategy must be a class named Strategy that extends
StrategyBase. Its constructor signature is fixed.
class Strategy(StrategyBase):
def __init__(
self,
data_coordinator: DataCoordinator,
config: dict[str, JSONValue],
) -> None:
validate_config(config)
# read parameters, initialize state
Store the data_coordinator on self if you need to query
positions or contract symbols later. Always call
validate_config(config) first; it raises with a precise error
if the config does not match your schema, instead of failing deeper with a
confusing stack trace.
Lifecycle hooks
In the order they fire.
on_start (required)
async def on_start(self, as_of_time: datetime) -> list[Action]:
...
Called once before any other events. Return the initial actions:
typically an InsertCashAction to fund the portfolio and one
or more RegisterSecurityActions to subscribe to market data.
You can also register a timer with RegisterTimeStepAction.
on_bar
async def on_bar(self, event: BarEvent) -> list[Action]:
...
Called for each bar event on a registered symbol and timeframe. Update
your indicators and state, then return any orders or other actions. This
is the primary decision loop for most strategies. Events may arrive for
multiple symbols or timeframes, so filter by
event.symbol and event.asset_type.
on_timer
async def on_timer(self, event: TimerEvent) -> list[Action]:
...
Called for each timer tick you registered with
RegisterTimeStepAction. The resulting timers have
timer_id="time_step".
on_expiration
async def on_expiration(self, event: ExpirationEvent) -> list[Action]:
... Called when a security you registered expires. The framework automatically unsubscribes every bar and quote registration for that security, so this handler is where you resolve the new front month contract and register it.
on_order_update
async def on_order_update(self, event: OrderUpdateEvent) -> list[Action]:
...
Called on every state change for orders you placed. The event carries
order_state, cumulative_quantity_filled, and
cumulative_net_money. Correlate updates by
order_id; there is no side field.
A common pattern: set self._order_pending = True when you
place an order, clear it here when the order reaches a terminal state
(FILLED, CANCELLED, REJECTED,
EXPIRED), and check that flag before placing another order.
This prevents duplicate submissions across repeated bar events.
on_stop
async def on_stop(self, reason: str) -> None:
...
Called once at shutdown. Cleanup only; it returns None, not
a list of actions. You cannot place orders from here.
Simulation time
self.simulation_time is the current simulation clock,
updated by the framework before each handler runs. Use it whenever you
need a timestamp. In backtest it is the historical time; in paper or
live it tracks real time.
Return value rules
-
on_start,on_bar,on_quote,on_timer,on_expiration, andon_order_updatemust all returnlist[Action]. An empty list is fine. -
on_stopreturnsNone. - Do not block. No long sleeps, no synchronous I/O that stalls the event loop.
Quote subscriptions are disabled
The runtime currently ignores quote subscriptions. Do not use
QuoteRegistrationRequest or
UnregisterQuoteAction. Use bar subscriptions only. This
applies to all modes, not just backtest.
Runtime environment
Strategy code runs in a sandboxed container with a read only filesystem, no network access, and no subprocess capability. The framework handles everything outside your logic: event loop, data subscriptions, order routing, portfolio, and persistence. Python 3.13+.