Files
momentum-system/tests/unit/storage/test_trade_analysis_projection.py
T
Claude 618950ce9c Add AI trade analysis (advisory): armed/weak verdict, post-mortems, EOD note
Extends the local-LLM layer from news to TRADES, reusing the proven enrichment
pattern (structured JSON, Postgres cache, off-hot-path scheduler, graceful
degradation). Advisory only — never gates a trade.

strategy/evaluation/trade_analysis.py: four analyses keyed by analysis_type —
  * armed      — does a loaded setup still make sense to pursue? (pursue/avoid/
                 monitor + confidence + concerns), weighing numbers + catalyst
  * weak       — why a too-soft setup failed and whether a thesis remains
  * postmortem — closed-trade lesson (uses r_multiple / exit_reason)
  * eod        — once-a-day session narrative
Schema-constrained decoding (ANALYSIS_SCHEMA), tolerant _parse, and a
context_hash so an unchanged board isn't re-sent to the GPU. run_* helpers
upsert into ai_trade_analysis_cache; trade_analysis_map / query_trade_analysis
read it back. NEVER raise.

Wiring:
- ai_trade_analysis_cache table (storage/pg_schema.sql)
- OllamaConfig.trade_analysis_{enabled,interval_seconds,batch_limit} + env
- run_live_paper step_analyze on the Scheduler, gated by ollama.enabled AND
  TRADE_ANALYSIS_ENABLED (ships OFF); reads the in-memory trigger book + day P&L
- query_trade_analysis projection; api snapshots() attaches the verdict to armed
  triggers + approval rows and exposes trade_analysis
- dashboard: per-name AI verdict chip (pursue=green/avoid=red/monitor=amber) +
  an "AI session note" panel
- .env.example documents the toggles

Tests: parse/enum/clamp, prompt builders, persistence + context_hash dedupe,
closed-trade + EOD paths, config from_env, and the dashboard projection. 230 pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01M5HTMkaadGDnAPtXe2hWwg
2026-06-29 22:00:03 +00:00

63 lines
2.2 KiB
Python

"""Tests for the AI trade-analysis dashboard projection."""
import json
from datetime import date
import pytest
from storage.event_store import EventStore
from storage.projections import query_trade_analysis
@pytest.fixture
def store():
s = EventStore(":memory:")
yield s
s.close()
SESS = date(2026, 6, 29)
def _seed(store, atype, symbol, decision, confidence, summary, concerns, sess=SESS):
store.con.execute(
"INSERT INTO ai_trade_analysis_cache (analysis_type, symbol, session_date, "
"context_hash, decision, confidence, summary, concerns, detail, model) "
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
[atype, symbol, sess, "h", decision, confidence, summary,
json.dumps(concerns), "{}", "test-model"],
)
def test_empty_when_no_rows(store):
assert query_trade_analysis(store) == {}
def test_groups_by_type_and_symbol(store):
_seed(store, "armed", "AAA", "pursue", 0.8, "clean gap", ["thin float"])
_seed(store, "weak", "BBB", "monitor", 0.4, "soft", [])
_seed(store, "postmortem", "AAA", "none", 0.0, "stopped out", ["entered late"])
_seed(store, "eod", "", "none", 0.0, "decent day", ["press winners"])
ta = query_trade_analysis(store)
assert ta["armed"]["AAA"]["decision"] == "pursue"
assert ta["armed"]["AAA"]["concerns"] == ["thin float"]
assert ta["weak"]["BBB"]["decision"] == "monitor"
assert ta["postmortem"]["AAA"]["summary"] == "stopped out"
# the EOD note lands under the '' symbol key
assert ta["eod"][""]["summary"] == "decent day"
def test_scopes_to_latest_session(store):
_seed(store, "armed", "OLD", "avoid", 0.5, "yesterday", [], sess=date(2026, 6, 26))
_seed(store, "armed", "NEW", "pursue", 0.9, "today", [], sess=SESS)
ta = query_trade_analysis(store) # MAX(session_date) -> only the latest day
assert "NEW" in ta["armed"] and "OLD" not in ta["armed"]
def test_for_date_selects_past_session(store):
_seed(store, "armed", "OLD", "avoid", 0.5, "yesterday", [], sess=date(2026, 6, 26))
_seed(store, "armed", "NEW", "pursue", 0.9, "today", [], sess=SESS)
ta = query_trade_analysis(store, for_date="2026-06-26")
assert "OLD" in ta["armed"] and "NEW" not in ta["armed"]