Skip to content

Writing a strategy#

A strategy is a Python file that defines one subclass of hl_research.backtest.strategy.Strategy. The CLI loads the file via importlib, instantiates the class, and drives candle and funding events through it.

Minimum viable strategy#

# my_strategy.py
from hl_research.backtest.strategy import Order, Strategy

class BuyTheDip(Strategy):
    def on_candle(self, candle, ctx):
        if candle.close < candle.open * 0.95:
            return [Order(asset=candle.asset, side="buy", size=0.1, kind="market")]
        return []

Run it:

hlr backtest run my_strategy.py --asset BTC

Hooks#

The base class defines five hooks. Override what you need.

class Strategy:
    def on_start(self, ctx: BacktestContext) -> None: ...
    def on_candle(self, candle: Candle, ctx: BacktestContext) -> list[Order]: ...
    def on_funding(self, tick: FundingTick, ctx: BacktestContext) -> list[Order]: ...
    def on_fill(self, fill: Fill, ctx: BacktestContext) -> None: ...
    def on_end(self, ctx: BacktestContext) -> None: ...

The engine drives events in time order. Orders returned from on_candle or on_funding are simulated against the next candle's range.

Candle event#

@dataclass(frozen=True)
class Candle:
    asset: str
    interval: Interval
    open_time: int   # ms
    open: float
    high: float
    low: float
    close: float
    volume: float

Order#

@dataclass(frozen=True)
class Order:
    asset: str
    side: Literal["buy", "sell"]
    size: float
    kind: Literal["market", "limit", "stop"]
    price: float | None = None       # required for limit and stop
    reduce_only: bool = False

Context#

ctx.cash, ctx.equity, and ctx.positions reflect current state. ctx.params is the parameter dict passed by the optimizer.

Parameters#

Reference ctx.params in your hooks to make the strategy tunable:

class MA(Strategy):
    def on_candle(self, candle, ctx):
        lookback = int(ctx.params.get("lookback", 20))
        ...

Sweep with the CLI:

hlr backtest optimize my_strategy.py --param "lookback:5..50:5"

Fill simulation#

The v0 fill model is intentionally simple:

  • Market — fills at the next candle's open
  • Limit — fills if the next candle's range crosses the limit price; fill price equals the limit price
  • Stop — same as market but only triggered after the stop price is crossed
  • No partial fills, no queue-position modeling, no book-impact modeling

Document this assumption in any writeup that uses backtest results.

Authoring tips#

  • One Strategy subclass per file. The loader rejects multiples.
  • Strategies are stateless wrappers; state lives in ctx.
  • Use ctx.logger.info(...) for diagnostic output that appears in --verbose mode.
  • Funding ticks fire at HL's hourly cadence. Your on_funding hook receives them with asset, time, rate, premium.
  • Position math runs through Portfolio automatically. You return orders, not position deltas.