feat: AI-enriched news scoring + directional alignment on pattern matching
Hook ai_score_news_batch() into /api/geo/news so every news fetch is enriched by GPT-4o-mini: corrected impact_score, ai_dir_energy/metals/indices, ai_resolution (ceasefire/peace deal flag), ai_insight (1 French sentence). Gracefully no-ops when OpenAI is not configured. Add _compute_ai_alignment() in geo_analyzer: for each pattern compares the news AI directional signals against the pattern's expected_move direction and produces a -25..+25 bonus injected into similarity/relevance scores. Contra-signals (e.g. peace deal → oil bearish while pattern expects oil spike) are flagged. Frontend GeoRadar: PatternRelevanceCard shows AI alignment badge (green = aligned, red = contra-signal) + base relevance diff + AI insights. NewsCard shows ai_insight, directional arrows per asset class (⛽↑ 🥇↓) and resolution badge when expanded. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -17,6 +17,14 @@ def geo_news(force_refresh: bool = False):
|
||||
if not force_refresh and _news_cache["data"] and (now - _news_cache["ts"]) < 3600:
|
||||
return _news_cache["data"]
|
||||
news = fetch_geo_news()
|
||||
# Enrich with AI: corrects impact_score, adds ai_dir_energy/metals/indices,
|
||||
# ai_resolution (ceasefire/peace deal), ai_insight (1 French sentence).
|
||||
# Gracefully skips if OpenAI not configured.
|
||||
try:
|
||||
from services.ai_analyzer import ai_score_news_batch
|
||||
news = ai_score_news_batch(news)
|
||||
except Exception:
|
||||
pass
|
||||
_news_cache["data"] = news
|
||||
_news_cache["ts"] = now
|
||||
return news
|
||||
|
||||
@@ -217,6 +217,57 @@ def compute_geo_risk_score(events: List[Dict[str, Any]]) -> Dict[str, Any]:
|
||||
}
|
||||
|
||||
|
||||
# Map asset_class → AI directional field produced by ai_score_news_batch()
|
||||
_ASSET_AI_DIR: Dict[str, str] = {
|
||||
"energy": "ai_dir_energy",
|
||||
"metals": "ai_dir_metals",
|
||||
"indices": "ai_dir_indices",
|
||||
"equities": "ai_dir_indices",
|
||||
}
|
||||
|
||||
|
||||
def _compute_ai_alignment(events: List[Dict[str, Any]], pattern: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Return AI directional alignment between news signals and pattern expected direction.
|
||||
Uses ai_dir_* fields added by ai_score_news_batch().
|
||||
Returns alignment bonus (-25..+25) and metadata for display.
|
||||
"""
|
||||
asset_class = pattern.get("asset_class", "")
|
||||
expected_positive = (pattern.get("expected_move_pct") or 0) > 0
|
||||
ai_field = _ASSET_AI_DIR.get(asset_class)
|
||||
|
||||
ai_news = [e for e in events if e.get("ai_scored")]
|
||||
empty = {"ai_alignment": 0, "ai_contra_signal": False, "ai_insights": [], "ai_scored_count": 0}
|
||||
if not ai_news or not ai_field:
|
||||
return empty
|
||||
|
||||
bullish = sum(1 for e in ai_news if e.get(ai_field) == "bullish")
|
||||
bearish = sum(1 for e in ai_news if e.get(ai_field) == "bearish")
|
||||
resolutions = sum(1 for e in ai_news if e.get("ai_resolution"))
|
||||
total = len(ai_news)
|
||||
|
||||
# Positive alignment = news confirms pattern direction
|
||||
if expected_positive:
|
||||
raw = (bullish - bearish) / total
|
||||
contra = bearish > bullish or (resolutions > 0 and asset_class in ("energy", "metals"))
|
||||
else:
|
||||
raw = (bearish - bullish) / total
|
||||
contra = bullish > bearish
|
||||
|
||||
bonus = int(round(max(-25.0, min(25.0, raw * 25))))
|
||||
|
||||
insights = [
|
||||
e["ai_insight"] for e in ai_news
|
||||
if e.get("ai_insight") and e.get(ai_field, "neutral") != "neutral"
|
||||
][:3]
|
||||
|
||||
return {
|
||||
"ai_alignment": bonus,
|
||||
"ai_contra_signal": bool(contra),
|
||||
"ai_insights": insights,
|
||||
"ai_scored_count": len(ai_news),
|
||||
}
|
||||
|
||||
|
||||
def match_patterns(events: List[Dict[str, Any]], patterns: Optional[List[Dict[str, Any]]] = None) -> List[Dict[str, Any]]:
|
||||
"""Find which historical geo-patterns best match current event feed."""
|
||||
if not events:
|
||||
@@ -237,17 +288,21 @@ def match_patterns(events: List[Dict[str, Any]], patterns: Optional[List[Dict[st
|
||||
similarity = round((trigger_match * 0.5 + keyword_match * 0.5) * 100, 1)
|
||||
|
||||
if similarity > 10:
|
||||
ai = _compute_ai_alignment(events, pattern)
|
||||
adjusted = max(0, min(100, similarity + ai["ai_alignment"]))
|
||||
matches.append({
|
||||
"pattern_id": pattern["id"],
|
||||
"name": pattern["name"],
|
||||
"description": pattern["description"],
|
||||
"similarity": similarity,
|
||||
"similarity": round(adjusted, 1),
|
||||
"base_similarity": similarity,
|
||||
"suggested_trades": pattern["suggested_trades"],
|
||||
"asset_class": pattern["asset_class"],
|
||||
"expected_move_pct": pattern["expected_move_pct"],
|
||||
"probability": pattern["probability"],
|
||||
"horizon_days": pattern["horizon_days"],
|
||||
"historical_instances": pattern["historical_instances"],
|
||||
**ai,
|
||||
})
|
||||
|
||||
return sorted(matches, key=lambda x: x["similarity"], reverse=True)[:5]
|
||||
@@ -324,12 +379,16 @@ def compute_pattern_relevance(
|
||||
})
|
||||
matching_news.sort(key=lambda x: x["impact"], reverse=True)
|
||||
|
||||
ai = _compute_ai_alignment(events, pattern)
|
||||
adjusted_relevance = max(0, min(100, relevance + ai["ai_alignment"]))
|
||||
|
||||
result.append({
|
||||
"pattern_id": pattern.get("id", ""),
|
||||
"name": pattern.get("name", ""),
|
||||
"description": pattern.get("description", ""),
|
||||
"asset_class": pattern.get("asset_class", ""),
|
||||
"relevance": relevance,
|
||||
"relevance": round(adjusted_relevance, 1),
|
||||
"base_relevance": relevance,
|
||||
"keyword_hits": len(kw_hits),
|
||||
"keyword_total": len(keywords_list),
|
||||
"matched_keywords": kw_hits,
|
||||
@@ -338,6 +397,7 @@ def compute_pattern_relevance(
|
||||
"expected_move_pct": pattern.get("expected_move_pct", 0),
|
||||
"probability": pattern.get("probability", 0),
|
||||
"horizon_days": pattern.get("horizon_days", 0),
|
||||
**ai,
|
||||
})
|
||||
|
||||
result.sort(key=lambda x: x["relevance"], reverse=True)
|
||||
|
||||
Reference in New Issue
Block a user