feat: pattern convergence engine — categories, signal_direction, conviction scores
Phase 1 — Catégorisation: - database.py: ADD COLUMN category + signal_direction on custom_patterns (migration); save_custom_pattern persists category/signal_direction; new helpers: get_unclassified_patterns(), update_pattern_classification(), get_patterns_with_last_score() - ai_analyzer.py: PATTERN_CATEGORIES dict (8 categories: géopolitique, macro_monétaire, technique, commodités_supply, risk_off, flux_saisonnier, géo_économique, crédit_stress); classify_patterns_batch() → GPT-4o-mini batch classification - suggest schema: added category + signal_direction fields so new patterns are classified from birth - auto_cycle.py: Step 3.1 classifies all unclassified patterns after each suggestion Phase 2 — Convergence layer (post-scoring, no extra AI call): - ai_analyzer.py: _compute_convergence() groups scored patterns by (underlying, signal_direction); conviction_bonus = min(20, +5 per additional agreeing pattern); adds conviction_score, conviction_bonus, convergence_count, convergence_underlying, convergence_partners to each result; called at end of score_patterns_with_context(), re-sorts by conviction_score - auto_cycle.py: logs convergence summary after scoring; propagates category/signal_direction to scored results for display Phase optionnelle — Convergence in suggestion prompt: - ai_analyzer.py: suggest_patterns_from_market_context() accepts convergence_block param; injected into prompt so AI knows which underlyings have multi-pattern agreement - auto_cycle.py: before suggestion, loads last-cycle scores via get_patterns_with_last_score(), calls _compute_convergence() to build convergence block, passes to suggester Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -531,6 +531,26 @@ def run_cycle_once(trigger: str = "auto") -> Dict[str, Any]:
|
||||
except Exception as _snap_e:
|
||||
logger.warning(f"[Cycle] Context snapshot save failed (non-blocking): {_snap_e}")
|
||||
|
||||
# ── Build convergence block from previous cycle's scored patterns ──
|
||||
_convergence_block_for_suggest = ""
|
||||
try:
|
||||
from services.database import get_patterns_with_last_score
|
||||
from services.ai_analyzer import _compute_convergence
|
||||
_prev_scored = get_patterns_with_last_score()
|
||||
_prev_with_scores = [
|
||||
{"pattern_id": p.get("id"), "score": p.get("last_score") or 0,
|
||||
"recommended_trade": {"underlying": (p.get("suggested_trades") or [{}])[0].get("underlying", "")} if p.get("suggested_trades") else {},
|
||||
"trade_rankings": [{"underlying": t.get("underlying", "")} for t in (p.get("suggested_trades") or [])],
|
||||
"geo_trigger": p.get("name", "")}
|
||||
for p in _prev_scored if p.get("last_score") is not None
|
||||
]
|
||||
if _prev_with_scores:
|
||||
_, _convergence_block_for_suggest = _compute_convergence(_prev_with_scores, _prev_scored)
|
||||
if _convergence_block_for_suggest:
|
||||
logger.info(f"[Cycle {run_id[:16]}] Convergence block built from {len(_prev_with_scores)} previously scored patterns")
|
||||
except Exception as _cbe:
|
||||
logger.warning(f"[Cycle] Convergence block build failed (non-blocking): {_cbe}")
|
||||
|
||||
suggestions = suggest_patterns_from_market_context(
|
||||
news, quotes, calendar, macro_regime=macro_regime, geo_score=geo_score_obj,
|
||||
portfolio_lessons=portfolio_lessons,
|
||||
@@ -541,6 +561,7 @@ def run_cycle_once(trigger: str = "auto") -> Dict[str, Any]:
|
||||
fred_block=_fred_block,
|
||||
price_discovery_block=_price_discovery_block,
|
||||
portfolio_context_block=_portfolio_block,
|
||||
convergence_block=_convergence_block_for_suggest,
|
||||
run_id=run_id,
|
||||
)
|
||||
except Exception as e:
|
||||
@@ -599,6 +620,21 @@ def run_cycle_once(trigger: str = "auto") -> Dict[str, Any]:
|
||||
|
||||
summary["patterns_added"] = added_count
|
||||
|
||||
# ── Step 3.1: Classify unclassified patterns (category + signal_direction) ─
|
||||
try:
|
||||
from services.database import get_unclassified_patterns, update_pattern_classification
|
||||
from services.ai_analyzer import classify_patterns_batch
|
||||
_unclassified = get_unclassified_patterns()
|
||||
if _unclassified:
|
||||
logger.info(f"[Cycle {run_id[:16]}] Classifying {len(_unclassified)} unclassified patterns")
|
||||
_classifications = classify_patterns_batch(_unclassified)
|
||||
for _cl in _classifications:
|
||||
if _cl.get("id") and _cl.get("category"):
|
||||
update_pattern_classification(_cl["id"], _cl["category"], _cl.get("signal_direction", "neutral"))
|
||||
logger.info(f"[Cycle {run_id[:16]}] Classified {len(_classifications)} patterns")
|
||||
except Exception as _cle:
|
||||
logger.warning(f"[Cycle] Pattern classification failed (non-blocking): {_cle}")
|
||||
|
||||
# ── Step 3.5: Collect risk cluster context ───────────────────────────
|
||||
risk_cluster_context = ""
|
||||
try:
|
||||
@@ -694,6 +730,22 @@ def run_cycle_once(trigger: str = "auto") -> Dict[str, Any]:
|
||||
logger.debug(f"[Cycle] Enriched '{orig.get('name')}' expected_move_pct={orig['expected_move_pct']}")
|
||||
if not s.get("trade_rankings") and not s.get("suggested_trades"):
|
||||
s["suggested_trades"] = orig.get("suggested_trades", [])
|
||||
# Propagate category + signal_direction to scored result for display
|
||||
if orig.get("category") and not s.get("category"):
|
||||
s["category"] = orig["category"]
|
||||
if orig.get("signal_direction") and not s.get("signal_direction"):
|
||||
s["signal_direction"] = orig["signal_direction"]
|
||||
|
||||
# ── Log convergence summary ───────────────────────────────────────────
|
||||
try:
|
||||
_top_conv = [s for s in scored if s.get("convergence_count", 0) > 0]
|
||||
if _top_conv:
|
||||
logger.info(
|
||||
f"[Cycle {run_id[:16]}] Convergence: {len(_top_conv)} patterns with cross-pattern boost "
|
||||
f"— top: {_top_conv[0].get('convergence_underlying','?')} ×{_top_conv[0].get('convergence_count',0)+1}"
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# ── Step 5: Log everything ────────────────────────────────────────────
|
||||
logger.info(f"[Cycle {run_id[:16]}] Step 5: logging")
|
||||
|
||||
Reference in New Issue
Block a user