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
63 lines
2.2 KiB
Python
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"]
|