Source code for trader.strategies.base

"""The strategy interface every trading rule implements.

Strategies emit only a direction. Position sizing, stops, and capital live
outside them, so comparing two strategies compares their signals rather than
their money management.
"""

from collections.abc import Sequence
from dataclasses import dataclass, field
from decimal import Decimal
from enum import StrEnum
from typing import Protocol, runtime_checkable

from trader.domain import Bar, Position

__all__ = ["Action", "Signal", "Strategy"]


[docs] class Action(StrEnum): """Long-only action set.""" BUY = "buy" # open a long SELL = "sell" # close the long, go flat HOLD = "hold" # do nothing
[docs] @dataclass(frozen=True, slots=True) class Signal: """A strategy's decision, with enough context to explain it later. `reason` and `indicators` are not decoration: they are what gets written to the decisions table, and what a later LLM reads when asked why a trade happened. """ action: Action reason: str indicators: dict[str, Decimal | float] = field(default_factory=dict)
[docs] @runtime_checkable class Strategy(Protocol): """A trading rule over a bar series.""" id: str #: Whether `BacktestEvaluator` may gate this strategy. #: #: `run_backtest` calls `evaluate()` once per bar, which is free for a rule #: strategy and ruinous for one that makes a network call: a 400-bar window #: would be 400 model calls per symbol per cycle, and the historical news #: such a replay would need does not exist. Derived from the strategy #: rather than configured, so it cannot be misconfigured. backtestable: bool = True
[docs] def warmup_bars(self) -> int: """Bars needed before this strategy can produce a defined signal.""" ...
[docs] def evaluate(self, bars: Sequence[Bar], position: Position | None) -> Signal: """Decide, given bars up to and including the decision bar. The caller guarantees `bars` contains nothing after the decision bar, so a strategy cannot look ahead even by accident. `position` is the holding itself rather than a flag, because a strategy deciding whether to *exit* needs to know what it is exiting: entry price, size and unrealized P/L. A bool made every sell decision blind to whether the position was up or down. Rule strategies that only care whether a position exists ask `position is not None`. Beware `False is not None` — it is `True`. That is why the bool was removed rather than kept alongside this parameter: one representation of "is there a position", so a caller cannot pass the wrong kind of truth. """ ...