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:
@@ -78,6 +78,9 @@ def init_db():
|
||||
"ALTER TABLE custom_patterns ADD COLUMN bayesian_win_rate REAL",
|
||||
"ALTER TABLE custom_patterns ADD COLUMN bayesian_updated_at TEXT",
|
||||
"ALTER TABLE custom_patterns ADD COLUMN bayesian_sample_size INTEGER DEFAULT 0",
|
||||
# Convergence Phase 1 — thematic classification
|
||||
"ALTER TABLE custom_patterns ADD COLUMN category TEXT",
|
||||
"ALTER TABLE custom_patterns ADD COLUMN signal_direction TEXT",
|
||||
]:
|
||||
try:
|
||||
c.execute(_sql)
|
||||
@@ -863,8 +866,9 @@ def save_custom_pattern(pattern: Dict[str, Any]) -> str:
|
||||
suggested_trades, asset_class, expected_move_pct, probability,
|
||||
horizon_days, ai_quality_score, ai_evaluation, source,
|
||||
counter_thesis, invalidation_trigger, invalidation_probability,
|
||||
category, signal_direction,
|
||||
is_active, updated_at
|
||||
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,1,datetime('now'))""", (
|
||||
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,1,datetime('now'))""", (
|
||||
pat_id,
|
||||
pattern.get("name", ""),
|
||||
pattern.get("description", ""),
|
||||
@@ -882,6 +886,8 @@ def save_custom_pattern(pattern: Dict[str, Any]) -> str:
|
||||
pattern.get("counter_thesis"),
|
||||
pattern.get("invalidation_trigger"),
|
||||
pattern.get("invalidation_probability"),
|
||||
pattern.get("category"),
|
||||
pattern.get("signal_direction"),
|
||||
))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
@@ -903,6 +909,57 @@ def get_custom_patterns() -> List[Dict[str, Any]]:
|
||||
return result
|
||||
|
||||
|
||||
def get_unclassified_patterns() -> List[Dict[str, Any]]:
|
||||
"""Return active patterns where category IS NULL (need AI classification)."""
|
||||
conn = get_conn()
|
||||
rows = conn.execute(
|
||||
"SELECT * FROM custom_patterns WHERE is_active=1 AND (category IS NULL OR category='')"
|
||||
).fetchall()
|
||||
conn.close()
|
||||
result = []
|
||||
for r in rows:
|
||||
d = dict(r)
|
||||
for f in ["triggers", "keywords", "historical_instances", "suggested_trades", "ai_evaluation"]:
|
||||
d[f] = json.loads(d.get(f) or "[]")
|
||||
result.append(d)
|
||||
return result
|
||||
|
||||
|
||||
def update_pattern_classification(pat_id: str, category: str, signal_direction: str) -> None:
|
||||
"""Persist category + signal_direction for a pattern."""
|
||||
conn = get_conn()
|
||||
conn.execute(
|
||||
"UPDATE custom_patterns SET category=?, signal_direction=?, updated_at=datetime('now') WHERE id=?",
|
||||
(category, signal_direction, pat_id),
|
||||
)
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
|
||||
def get_patterns_with_last_score() -> List[Dict[str, Any]]:
|
||||
"""Each active pattern + its most recent score from pattern_score_history."""
|
||||
conn = get_conn()
|
||||
rows = conn.execute("""
|
||||
SELECT cp.*, psh.score AS last_score, psh.summary AS last_summary, psh.scored_at AS last_scored_at
|
||||
FROM custom_patterns cp
|
||||
LEFT JOIN (
|
||||
SELECT pattern_id, score, summary, scored_at,
|
||||
ROW_NUMBER() OVER (PARTITION BY pattern_id ORDER BY scored_at DESC) AS rn
|
||||
FROM pattern_score_history
|
||||
) psh ON psh.pattern_id = cp.id AND psh.rn = 1
|
||||
WHERE cp.is_active=1
|
||||
ORDER BY cp.created_at DESC
|
||||
""").fetchall()
|
||||
conn.close()
|
||||
result = []
|
||||
for r in rows:
|
||||
d = dict(r)
|
||||
for f in ["triggers", "keywords", "historical_instances", "suggested_trades", "ai_evaluation"]:
|
||||
d[f] = json.loads(d.get(f) or "[]")
|
||||
result.append(d)
|
||||
return result
|
||||
|
||||
|
||||
def toggle_pattern_active(pat_id: str) -> bool:
|
||||
"""Toggle is_active for a pattern. Returns the new state."""
|
||||
conn = get_conn()
|
||||
|
||||
Reference in New Issue
Block a user