Stateful Strategies

Strategies that carry state across ticks. Kalman filters, online learners, regime trackers

Strategies that need memory across ticks (Kalman filters, online learning models, rolling feature caches) use Horizon’s state mechanism. Declare a mutable attribute on the class; the engine preserves it across calls to evaluate().

Pattern

python
from dataclasses import dataclass, field
from horizon import Strategy, Signal
from horizon.asset_classes import Equity
from horizon.features import Price


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


class KalmanMeanReversion(Strategy):
    asset_classes = [Equity]
    features = {"price": Price()}
    state: KalmanState = field(default_factory=KalmanState)

    def evaluate(self, f, universe):
        signals = []
        for m in universe:
            current = f.price[m.id]
            # Kalman update
            kalman_gain = self.state.variance / (self.state.variance + 1.0)
            self.state.mean += kalman_gain * (current - self.state.mean)
            self.state.variance = (1 - kalman_gain) * self.state.variance + 0.01

            # Compare to running mean
            deviation = (current - self.state.mean) / max(self.state.mean, 0.01)
            if deviation < -0.03:
                signals.append(Signal.increase(m, edge_bps=40, reason=f"kalman dev={deviation:+.3f}"))
            elif deviation > 0.03:
                signals.append(Signal.decrease(m, edge_bps=40, reason=f"kalman dev={deviation:+.3f}"))
        return signals

Per-market state

For per-market state (one Kalman filter per ticker), use a dict keyed by market id:

python
class PerMarketKalman(Strategy):
    asset_classes = [Equity]
    features = {"price": Price()}
    state: dict = field(default_factory=dict)

    def evaluate(self, f, universe):
        for m in universe:
            if m.id not in self.state:
                self.state[m.id] = KalmanState(mean=f.price[m.id], variance=1.0)
            # ...

Online ML

python
from river.linear_model import LogisticRegression

class OnlineML(Strategy):
    asset_classes = [Equity]
    features = {...}

    def __init__(self):
        super().__init__()
        self.models = {}   # per-market online model

    def evaluate(self, f, universe, ctx):
        # Update model with realized outcome (requires storing past predictions)
        # Then predict for current tick
        ...

See river for online learning models. Install via horizon[ml].

Determinism

Stateful strategies are still deterministic as long as:

  1. Their state updates are pure functions of the previous state + current inputs
  2. They don’t use random.random() without a seed

To preserve determinism across backtests, initialize state deterministically in __init__ or as a default dataclass.field.

Next