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:
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:
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--verbosemode. - Funding ticks fire at HL's hourly cadence. Your
on_fundinghook receives them withasset,time,rate,premium. - Position math runs through
Portfolioautomatically. You return orders, not position deltas.