Strategy

Strategy base class and @strategy decorator

Strategy base class

python
from horizon import Strategy, Signal
from horizon.asset_classes import Equity
from horizon.features import Zscore

class MyStrategy(Strategy):
    name = "my_strategy"                          # optional, defaults to class name
    asset_classes: list[AssetClass] = [Equity]    # required
    features: dict[str, Feature] = {"z": Zscore(20)}  # optional

    def evaluate(
        self,
        features: FeatureNamespace,
        universe: list[Market],
    ) -> list[Signal]:
        ...

Class attributes

  • name: str: strategy identifier. Tagged onto every signal and trade for attribution. Defaults to the class name.
  • asset_classes: list[AssetClass]: which asset classes this strategy trades. Used by the engine to filter the universe each tick. Defaults to [].
  • features: dict[str, Feature]: feature requirements. Each entry is a named Feature instance. The engine computes them once per tick and passes the result as f to evaluate().

Required method

python
def evaluate(self, features, universe) -> list[Signal]:
    ...
  • features: a FeatureNamespace giving per-market access by name: features.z[market.id].
  • universe: the list of Market objects eligible for this strategy (already filtered by asset class).

Returns a list of Signal objects. Zero signals is fine.

Optional third argument

python
def evaluate(self, features, universe, ctx: Context) -> list[Signal]:
    if ctx.portfolio.drawdown_pct > 0.05:
        return []
    ...

The engine detects the third argument’s presence automatically and injects the Context only when requested.

Optional hooks

python
def on_start(self, ctx: Context) -> None:
    """Called once when the run begins."""

def on_stop(self, ctx: Context) -> None:
    """Called once when the run ends."""

def eligible_markets(self, universe: list[Market]) -> list[Market]:
    """Narrow the universe further than asset-class filtering."""
    return [m for m in universe if m.metadata.get("market_cap", 0) > 1e9]

State

Strategies can hold stateful fields declared as class attributes. State persists across ticks within one backtest run:

python
@dataclass
class KalmanState:
    mean: float = 100.0
    variance: float = 1.0

class KalmanMR(Strategy):
    state: KalmanState = KalmanState()

    def evaluate(self, f, universe):
        self.state.mean = ...  # update
        return [...]

@strategy decorator

Function form for one-off strategies:

python
from horizon import strategy, Signal
from horizon.asset_classes import Equity
from horizon.features import Zscore

@strategy(
    name="simple_mr",
    asset_classes=[Equity],
    features={"z": Zscore(20)},
)
def simple_mr(f, universe):
    return [
        Signal.from_score(m, score=-f.z[m.id], edge_per_stdev=15)
        for m in universe
        if abs(f.z[m.id]) < -2
    ]

The decorator returns a class (not an instance) that implements the Strategy protocol. Drops straight into hz.run(strategies=[simple_mr]).

Decorator signature

python
@strategy(
    name: str | None = None,
    asset_classes: list[AssetClass] | None = None,
    features: dict[str, Feature] | None = None,
)
def my_strategy(f, universe):
    ...

The wrapped function can accept (f, universe) or (f, universe, ctx): the decorator detects the signature the same way as class strategies.

Signature detection

The engine looks at evaluate’s parameter list to decide whether to inject ctx:

python
def evaluate(self, f, universe):          # no ctx injection
def evaluate(self, f, universe, ctx):     # ctx injected

Detected via inspect.signature(evaluate_fn). No flags, no metadata.

Exception safety

If evaluate() raises an exception, the run loop catches it, logs it, and continues with the next strategy. Other strategies in the same run still get to evaluate their signals. See tests/test_behavioral_audit.py::test_loop_survives_bad_strategy_exception.

What the engine does for you

  • Filters the universe by asset_classes: you only see markets you care about
  • Computes features once per tick and passes them as a typed namespace
  • Tags signals with strategy_id automatically before depositing in the signal store
  • Detects ctx presence in evaluate() and injects it only if requested
  • Catches exceptions so one broken strategy can’t kill the whole run
  • Routes trades through the right asset-class executor based on market asset class

Next