Skip to content

Strategy Callbacks

on_bar (required)

Called with every completed bar. Return an Order to emit a signal, or None.

from replaybt import Strategy, MarketOrder, Side, Bar
from typing import Dict, Any, List

class MyStrategy(Strategy):
    def on_bar(
        self,
        bar: Bar,               # completed 1m bar
        indicators: Dict[str, Any],  # current indicator values
        positions: List,         # open positions
    ):
        ema = indicators.get("ema_fast")
        if ema is None:
            return None

        if not positions and bar.close > ema:
            return MarketOrder(
                side=Side.LONG,
                take_profit_pct=0.05,
                stop_loss_pct=0.03,
            )
        return None

Arguments:

Arg Type Description
bar Bar Current completed 1m bar with timestamp, OHLCV, symbol
indicators Dict[str, Any] Values from configured indicators. None if not ready
positions List[Position] Currently open positions for this symbol

Returns: Order, List[Order], or None

configure

Called once before the first bar. Use it to initialize strategy state.

class MyStrategy(Strategy):
    def configure(self, config: dict):
        self._prev_fast = None
        self._prev_slow = None
        self._trade_count = 0

The config dict is the same dict passed to the engine constructor.

on_fill

Called after an entry fill (or a merge fill for scale-in). Can return a LimitOrder for scale-in or a MarketOrder.

from replaybt import LimitOrder

class ScaleInStrategy(Strategy):
    def on_fill(self, fill):
        if fill.is_entry:
            # Place a limit order to scale in at -0.5% from entry
            dip_price = fill.price * (1 - 0.005) if fill.side == Side.LONG else fill.price * (1 + 0.005)
            return LimitOrder(
                side=fill.side,
                limit_price=dip_price,
                merge_position=True,  # merge into existing position
                timeout_bars=120,
            )
        return None

Arguments:

Arg Type Description
fill Fill The fill event with price, side, size, fees, is_entry flag

Returns: Order or None

on_exit

Called after a position closes. Can return an Order for immediate re-entry, a CancelPendingLimitsOrder to clean up, or None.

from replaybt import CancelPendingLimitsOrder

class FlipStrategy(Strategy):
    def on_exit(self, fill, trade):
        # Cancel any pending scale-in limit orders
        if trade.reason == "TAKE_PROFIT":
            return CancelPendingLimitsOrder()
        return None

Arguments:

Arg Type Description
fill Fill The exit fill
trade Trade Completed trade with entry/exit times, PnL, reason

Returns: Order, CancelPendingLimitsOrder, or None

check_exits

Called before on_bar() on every bar where positions are open. Lets you implement custom exit logic (e.g., time-based exits, indicator-based exits).

class TimedExitStrategy(Strategy):
    def check_exits(self, bar, positions):
        exits = []
        for i, pos in enumerate(positions):
            # Close after 100 bars
            hours_held = (bar.timestamp - pos.entry_time).total_seconds() / 3600
            if hours_held > 100:
                exits.append((i, bar.close, "TIME_EXIT"))
        return exits

Return format: list of tuples:

# Full close:
(position_index, exit_price, reason_string)

# Partial close:
(position_index, exit_price, reason_string, close_fraction)
# close_fraction: 0.5 = close 50% of position

Note

If check_exits() returns any exits, on_bar() is skipped for that bar (same behavior as engine-level SL/TP exits).

Warmup

Indicators need a warmup period before producing valid values. During warmup, indicators.get("name") returns None.

def on_bar(self, bar, indicators, positions):
    ema = indicators.get("ema_fast")
    if ema is None:
        return None  # still warming up
    # ... rest of logic

The number of warmup bars depends on the indicator and its period. For an EMA with period 35 on 30m timeframe, you need at least 35 * 30 = 1,050 1m bars before it's ready.