trader.decisions module

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.

class trader.decisions.DecisionMemory(*args, **kwargs)[source]

Bases: Protocol

The most recent decision a strategy recorded for a symbol.

latest_decision(strategy_id, ticker)[source]

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.

Parameters:
  • strategy_id (str)

  • ticker (str)

Return type:

StoredDecision | None

class trader.decisions.StoredDecision(decision_id, decided_at, action, reasoning, inputs)[source]

Bases: object

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.

Parameters:
  • decision_id (int)

  • decided_at (datetime)

  • action (str)

  • reasoning (str)

  • inputs (dict[str, object])

decision_id: int
decided_at: datetime
action: str
reasoning: str
inputs: dict[str, object]