Leverage a local Ollama model to interpret news catalysts for the small-cap momentum strategy — distinguishing bullish catalysts (FDA approval, M&A) from dilutive offerings the technicals alone can't see. Advisory-first, then automate. Phase 1 (advisory; no change to trade behavior): - research/ingestion/news_enrichment.py: stdlib urllib call to Ollama with structured JSON output, classifying each headline into a catalyst type, sentiment, conviction and dilution flag. Graceful degradation (None on any failure) so trading is never blocked. - news_catalyst_cache table + populate the previously-dead news_events.sentiment. - Runs on the existing interval Scheduler (off the hot path), gated by OllamaConfig; deduped by the raw_news_items payload hash. - Surface the advisory on the dashboard approval queue (projection + api + UI). Phase 2 (automation; ships gated OFF): - Optional catalyst_score blended into calculate_setup_quality (None reproduces the exact legacy weights); pure pass-through via evaluate_setup. - Dilution veto in TradingExecutionService (slow + fast paths) emitting a catalyst_dilution_veto risk event, behind NEWS_DILUTION_VETO_ENABLED. Config (OllamaConfig.from_env), .env.example docs, and tests for parsing, persistence/dedupe, the projection, the score blend, the veto, and an end-to-end enrich->advisory flow (Ollama mocked). Default model moves to qwen2.5:7b-instruct (best small-model JSON, fits a 12GB GPU). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M5HTMkaadGDnAPtXe2hWwg
45 lines
1.4 KiB
Python
45 lines
1.4 KiB
Python
"""Tests for the catalyst-advisory dashboard projection."""
|
|
|
|
import pytest
|
|
|
|
from storage.event_store import EventStore
|
|
from storage.projections import query_catalyst_advisory
|
|
|
|
|
|
@pytest.fixture
|
|
def store():
|
|
s = EventStore(":memory:")
|
|
yield s
|
|
s.close()
|
|
|
|
|
|
def _seed(store, headline_hash, symbol, ctype, sentiment, conviction, dilutive):
|
|
store.con.execute(
|
|
"INSERT INTO news_catalyst_cache (headline_hash, symbol, headline, source, "
|
|
"catalyst_type, sentiment, conviction, is_dilutive, rationale, model) "
|
|
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
|
[headline_hash, symbol, f"{symbol} news", "rss", ctype, sentiment,
|
|
conviction, dilutive, "why", "test-model"],
|
|
)
|
|
|
|
|
|
def test_advisory_empty_when_no_rows(store):
|
|
assert query_catalyst_advisory(store) == {}
|
|
|
|
|
|
def test_advisory_returns_per_symbol(store):
|
|
_seed(store, "h1", "BIO", "fda_approval", 0.9, 0.85, False)
|
|
_seed(store, "h2", "SHEL", "offering_dilution", -0.7, 0.8, True)
|
|
|
|
adv = query_catalyst_advisory(store)
|
|
assert adv["BIO"]["catalyst_type"] == "fda_approval"
|
|
assert adv["BIO"]["is_dilutive"] is False
|
|
assert adv["BIO"]["sentiment"] == 0.9
|
|
assert adv["SHEL"]["is_dilutive"] is True
|
|
|
|
|
|
def test_advisory_skips_empty_symbol_marker(store):
|
|
# enrichment writes a sentinel row with symbol='' for ticker-less headlines
|
|
_seed(store, "h3", "", "other", 0.0, 0.2, False)
|
|
assert query_catalyst_advisory(store) == {}
|