Source code for trader.decisions

"""Reading an earlier decision back, for a strategy that may reuse it.

A Protocol for the same reason `BrokerAdapter`, `LlmProvider` and
`NewsProvider` are ones: the thing that *needs* a previous decision is
`LlmStrategy`, and a strategy must not depend on SQLAlchemy, a session factory
or the `decisions` table's column names. `DecisionRepository` satisfies this
shape; a test satisfies it with a dict.

**`StoredDecision.inputs` is deliberately an opaque `dict`, and no code in this
module knows a single key inside it.** `LlmStrategy` is the only thing that
writes `decisions.inputs_json` for an LLM strategy, so it is the only thing
that should read it: one owner for the key names. A repository that unpacked
`news_fingerprint`, `signal_action` and `bar_summary` into typed fields would
be a second definition of that schema, free to drift from the first — the
`bar_width(interval)` failure CLAUDE.md already records twice. The price is
that this module cannot type-check the contents; the alternative price is a
silent disagreement about what a stored decision said, which is worse.
"""

from dataclasses import dataclass
from datetime import datetime
from typing import Protocol, runtime_checkable

__all__ = ["DecisionMemory", "StoredDecision"]


[docs] @dataclass(frozen=True, slots=True) class StoredDecision: """One row of the `decisions` table, as a value object. `action` is what the *pipeline* did, which is not always what the strategy said — a veto can overwrite it (`veto_class`), and `inputs["signal_action"]` is the strategy's own opinion. A reader deciding whether an earlier answer may be carried forward wants the latter; both are reachable from here, and conflating them is how a `rejected` row would read as a refusal to trade. """ decision_id: int decided_at: datetime action: str reasoning: str inputs: dict[str, object]
[docs] @runtime_checkable class DecisionMemory(Protocol): """The most recent decision a strategy recorded for a symbol."""
[docs] def latest_decision(self, strategy_id: str, ticker: str) -> StoredDecision | None: """The newest recorded decision for this `(strategy_id, ticker)` pair. `None` when there is none — including when `inputs_json` is absent or unreadable, because a decision whose inputs cannot be read is not evidence of anything a caller could act on. Must never raise on ordinary data. A caller reads this to decide whether it can *avoid* work; a failure here has to degrade into doing the work, never into aborting a cycle. """ ...