Strategy¶
Base class for Codon event-driven strategies. Mirrors C++ lrvx::Strategy.
Class: Strategy¶
Constructor¶
Parameters:
symbols-- List of symbol IDs to subscribe tostrategy_id-- Subscriber id, default1registry-- Symbol-registry handle. When omitted, the underlying handle is created empty andRunner.add_strategyrebuilds it once a registry is present. Pass the strategy into aRunnerto give it an event source; on its own it has none.
How a callback reaches your subclass¶
Your overrides are called through a table of C function pointers that the engine holds. Codon fills that table when the strategy is handed to a runner, not when it is constructed, and this is not an implementation detail you can ignore: a strategy that is never attached never receives anything.
strat = MyStrategy([btc])
runner.add_strategy(strat) # installs the callbacks
# or
backtest.set_strategy(strat) # same, for BacktestRunner
The reason for the timing is that the trampolines are generated against your
concrete subclass. Codon resolves method calls statically, so a trampoline
written against Strategy would call the empty base method rather than your
override. add_strategy and set_strategy are generic in the strategy's
type, which is the last point where that type is still visible -- inside
Strategy.__init__ a subclass has already been erased by super().
Two consequences worth knowing:
- Attaching one strategy to a runner is what makes it live. Constructing it is not enough, and a strategy with no callbacks installed raises rather than running silently through a backtest that would finish and print a result your logic took no part in.
- An exception raised inside a callback is caught at the boundary and reported on stderr, naming the callback. It is not allowed to unwind into the C++ frame that called it, because that is undefined behaviour and the engine cannot recover from it.
Overridable Callbacks¶
on_trade(ctx, trade)¶
Called on each trade event for subscribed symbols.
def on_trade(self, ctx: SymbolContext, trade: TradeData):
price = trade.price.to_double()
# strategy logic here
on_book_update(ctx)¶
Called on each order book update for subscribed symbols.
on_bar(ctx, bar)¶
Called on each closed OHLC bar.
def on_bar(self, ctx: SymbolContext, bar: BarData):
# bar.open, bar.high, bar.low, bar.close, bar.volume, ...
if bar.close > bar.open and self.position() == 0.0:
self.market_buy(0.01)
on_start() / on_stop()¶
Lifecycle callbacks.
on_fill(ctx, ev)¶
Called on each fill (status PARTIALLY_FILLED or FILLED) for orders this strategy emitted. ev is
an OrderEventData, described under Types; it carries order_id, side, fill_qty,
fill_price, exchange_ts_ns.
on_order_update(ctx, ev)¶
Called on every order-lifecycle status change for orders this strategy emitted: NEW, ACCEPTED,
CANCELED, REJECTED, REPLACED, TRIGGERED, TRAILING_UPDATED. Fills come through here too —
override on_fill instead if you only want fills.
on_queue_position_change(ctx, ev)¶
A resting limit order's queue position moved with no other lifecycle transition. ev.queue_ahead and
ev.queue_total carry the current snapshot. Backtest only.
on_market_position_change(ctx, ev)¶
A resting limit order's categorical market position transitioned (best, behind_best, mid_spread,
level_empty, crossed). ev.market_position is the new state; ev.distance_to_best_ticks is signed
ticks from best on our side. Backtest only.
Signal Emission¶
emit_market_buy(symbol, qty) -> int¶
Submit a market buy order. Returns order ID.
emit_market_sell(symbol, qty) -> int¶
Submit a market sell order. Returns order ID.
emit_limit_buy(symbol, price, qty) -> int¶
Submit a limit buy order. Returns order ID.
emit_limit_sell(symbol, price, qty) -> int¶
Submit a limit sell order. Returns order ID.
emit_cancel(order_id)¶
Cancel an order by ID.
emit_cancel_all(symbol)¶
Cancel all orders for a symbol.
emit_modify(order_id, new_price, new_qty)¶
Modify an existing order's price and quantity.
emit_stop_market(symbol, side, trigger, qty) -> int¶
Submit a stop market order. side: 0=BUY, 1=SELL.
emit_stop_limit(symbol, side, trigger, limit_price, qty) -> int¶
Submit a stop limit order.
emit_take_profit_market(symbol, side, trigger, qty) -> int¶
Submit a take profit market order.
emit_take_profit_limit(symbol, side, trigger, limit_price, qty) -> int¶
Submit a take profit limit order.
emit_trailing_stop(symbol, side, offset, qty) -> int¶
Submit a trailing stop order.
emit_trailing_stop_percent(symbol, side, callback_bps, qty) -> int¶
Submit a trailing stop with percentage callback. callback_bps: 100 = 1%.
emit_limit_buy_tif(symbol, price, qty, tif) -> int¶
Submit a limit buy with TimeInForce. tif: 0=GTC, 1=IOC, 2=FOK, 4=POST_ONLY.
emit_limit_sell_tif(symbol, price, qty, tif) -> int¶
Submit a limit sell with TimeInForce.
emit_close_position(symbol) -> int¶
Close entire position with a reduce-only market order.
Context Queries¶
position(symbol=None) -> float¶
Current position quantity. If symbol is None, uses the first subscribed symbol.
ctx(symbol=None) -> SymbolContext¶
Get a SymbolContext for querying per-symbol state.
get_order_status(order_id) -> int¶
Get order status. Returns -1 if not found.
String-Symbol Convenience API¶
These take an optional symbol name and resolve it through the strategy's own name map, falling
back to the primary symbol when omitted. Prices and quantities are plain float.
| Method | Returns |
|---|---|
market_buy(qty, symbol=None) |
int order id |
market_sell(qty, symbol=None) |
int order id |
limit_buy(price, qty, symbol=None, tif="gtc") |
int order id |
limit_sell(price, qty, symbol=None, tif="gtc") |
int order id |
stop_market(side, trigger, qty, symbol=None) |
int order id |
stop_limit(side, trigger, limit_price, qty, symbol=None) |
int order id |
take_profit_market(side, trigger, qty, symbol=None) |
int order id |
take_profit_limit(side, trigger, limit_price, qty, symbol=None) |
int order id |
trailing_stop(side, offset, qty, symbol=None) |
int order id |
trailing_stop_percent(side, callback_bps, qty, symbol=None) |
int order id |
close_position(symbol=None) |
int order id |
cancel_order(order_id) |
None |
cancel_all_orders(symbol=None) |
None |
modify_order(order_id, new_price, new_qty) |
None |
pos(symbol=None) |
float position |
last_price(symbol=None) |
float |
best_bid(symbol=None) |
Optional[float], None when the bid side has no level; 0.0 is a price |
best_ask(symbol=None) |
Optional[float], None when the ask side has no level |
mid_price(symbol=None) |
Optional[float], None unless both sides have a level |
order_status(order_id) |
int |
side and tif are lowercase strings here ('buy' / 'sell', 'gtc' / 'ioc' / ...), unlike the
emit_* methods which take the integer constants from lrvx.types. An unrecognised
side falls back to buy and an unrecognised tif to GTC. An unknown symbol name raises
ValueError.
Multi-Timeframe Bar Ring¶
| Method | Description |
|---|---|
last_closed_bar(symbol, bar_type, param) -> Optional[BarData] |
Most recent closed bar for that (symbol, timeframe), or None before one has closed |
last_n_closed_bars(symbol, bar_type, param, n) -> List[BarData] |
Most recent n bars, oldest first |
bar_ring_capacity() -> int |
Bars retained per (symbol, timeframe) |
set_bar_ring_capacity(n) |
Set the retention count |
param is nanoseconds for time bars, a count for tick bars, a threshold for volume bars.
Properties¶
primary_symbol -> int¶
Returns the first symbol in the subscription list.
primary_symbol_name -> str¶
The name of the primary symbol, as resolved through the registry.
Example¶
from lrvx.strategy import Strategy
from lrvx.context import SymbolContext
from lrvx.types import TradeData
from lrvx.indicators import EMA
class EmaCrossover(Strategy):
fast_ema: EMA
slow_ema: EMA
def __init__(self, symbols: List[int]):
super().__init__(symbols)
self.fast_ema = EMA(12)
self.slow_ema = EMA(26)
def on_trade(self, ctx: SymbolContext, trade: TradeData):
price = trade.price.to_double()
fast = self.fast_ema.update(price)
slow = self.slow_ema.update(price)
if not self.slow_ema.ready:
return
sym = self.primary_symbol
if fast > slow and ctx.is_flat():
self.emit_market_buy(sym, 1.0)
elif fast < slow and ctx.is_long():
self.emit_close_position(sym)