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
78 lines
2.6 KiB
Python
78 lines
2.6 KiB
Python
"""Deterministic strategy evaluation tests (Milestone 1)."""
|
|
|
|
from datetime import datetime
|
|
|
|
from strategy.evaluation.setup_evaluator import evaluate_setup
|
|
from tests.synthetic import bull_flag_bars, fading_bars, tiny_bars
|
|
|
|
EVAL_TIME = datetime(2026, 6, 11, 9, 45)
|
|
|
|
|
|
def test_bull_flag_is_ready():
|
|
result = evaluate_setup(
|
|
bull_flag_bars(),
|
|
previous_close=10.0,
|
|
avg_daily_volume=500_000,
|
|
evaluation_time=EVAL_TIME,
|
|
)
|
|
assert result.status == "ready"
|
|
assert result.success_score_pct >= 60.0
|
|
setup = result.setups[0]
|
|
assert setup["entry_price"] > 0
|
|
assert 0 < setup["stop_loss_price"] < setup["entry_price"]
|
|
assert 0 <= setup["quality_score"] <= 1
|
|
|
|
|
|
def test_fading_tape_is_blocked():
|
|
result = evaluate_setup(
|
|
fading_bars(),
|
|
previous_close=10.0,
|
|
avg_daily_volume=500_000,
|
|
evaluation_time=EVAL_TIME,
|
|
)
|
|
assert result.status == "blocked"
|
|
assert result.success_score_pct < 60.0
|
|
|
|
|
|
def test_insufficient_data_is_blocked():
|
|
result = evaluate_setup(tiny_bars(), previous_close=5.0, evaluation_time=EVAL_TIME)
|
|
assert result.status == "blocked"
|
|
assert "insufficient_data" in (result.reason or "")
|
|
|
|
|
|
def test_after_cutoff_is_late():
|
|
result = evaluate_setup(
|
|
bull_flag_bars(),
|
|
previous_close=10.0,
|
|
avg_daily_volume=500_000,
|
|
evaluation_time=datetime(2026, 6, 11, 15, 30),
|
|
)
|
|
assert result.status == "late"
|
|
|
|
|
|
def test_criteria_results_cover_all_weights():
|
|
result = evaluate_setup(
|
|
bull_flag_bars(),
|
|
previous_close=10.0,
|
|
avg_daily_volume=500_000,
|
|
evaluation_time=EVAL_TIME,
|
|
)
|
|
expected = {
|
|
"sufficient_data", "gap", "relative_volume", "impulse", "pullback",
|
|
"pullback_volume", "vwap", "candle_quality", "breakout",
|
|
}
|
|
evaluated = set(result.criteria_names_passed) | set(result.criteria_names_failed)
|
|
assert expected <= evaluated
|
|
|
|
|
|
def test_catalyst_score_is_optional_passthrough():
|
|
"""The Phase 2 catalyst_score is a pure pass-through: None == legacy, and a
|
|
strong catalyst lifts the setup's quality score without changing the gate."""
|
|
kw = dict(previous_close=10.0, avg_daily_volume=500_000, evaluation_time=EVAL_TIME)
|
|
legacy = evaluate_setup(bull_flag_bars(), **kw)
|
|
explicit_none = evaluate_setup(bull_flag_bars(), catalyst_score=None, **kw)
|
|
boosted = evaluate_setup(bull_flag_bars(), catalyst_score=1.0, **kw)
|
|
|
|
assert legacy.setups[0]["quality_score"] == explicit_none.setups[0]["quality_score"]
|
|
assert boosted.setups[0]["quality_score"] > legacy.setups[0]["quality_score"]
|