Skip to content

Strategy

Base class for Codon event-driven strategies. Mirrors C++ flox::Strategy.

Class: Strategy

Constructor

Strategy(symbols: List[int], strategy_id: int = 1, registry: cobj = cobj())

Parameters:

  • symbols -- List of symbol IDs to subscribe to
  • strategy_id -- Subscriber id, default 1
  • registry -- Symbol-registry handle. When omitted, the underlying handle is created empty and Runner.add_strategy rebuilds it once a registry is present. Pass the strategy into a Runner to give it an event source; on its own it has none.

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.

def on_book_update(self, ctx: SymbolContext):
    spread = ctx.book_spread()
    # strategy logic here

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 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) float
best_ask(symbol=None) float
mid_price(symbol=None) float
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 flox.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 flox.strategy import Strategy
from flox.context import SymbolContext
from flox.types import TradeData
from flox.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)