Periscøpe
Docs menu Strategy Protocol

Strategy Protocol

How a strategy behaves at runtime: lifecycle hooks, registration, events, and actions.

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, and on_order_update must all return list[Action]. An empty list is fine.
  • on_stop returns None.
  • 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+.