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.
"""
...