feat: Phase 4 — Moteur Probabiliste & Apprentissage Automatique
Sprint 4.1 — Bayesian Updating - database.py: update_bayesian_posteriors() — Beta(α,β) posteriors sur trades matures - database.py: get_bayesian_posteriors() — posteriors + IC 95% + dérive prior GPT vs posterior - Colonnes Bayésiennes ajoutées : bayesian_alpha, bayesian_beta, bayesian_win_rate, bayesian_sample_size - auto_cycle.py: appel update_bayesian_posteriors() en Step 5.5 (après scoring) Sprint 4.2 — Détection Automatique de Régimes (K-Means numpy pur) - database.py: detect_and_save_regime_clusters() — K-Means sur 7 gauges macro (VIX, slope, DXY…) - database.py: get_regime_cluster_history() — timeline des clusters - database.py: get_regime_transition_matrix() — P(cluster j | cluster i) sur N transitions - Table regime_clusters avec anomaly_flag (points > 3σ) - auto_cycle.py: appel detect_and_save_regime_clusters() en Step 5.6 Sprint 4.3 — Embeddings Sémantiques (remplace Jaccard) - database.py: get_or_create_pattern_embedding() — OpenAI text-embedding-3-small, stocké en DB - database.py: max_cosine_similarity_vs_existing() — similarité cosinus vs patterns existants - Table pattern_embeddings avec vecteur JSON + model_version - auto_cycle.py: _is_duplicate_pattern() — cosinus seuil 0.75 avec fallback Jaccard automatique Sprint 4.4 — Tableau de Bord Analytique Avancé - AnalyticsAdvanced.tsx: nouvelle page /analytics-advanced • BayesianTable : prior GPT vs WR bayésien ± IC 95%, dérive, niveau de confiance • ClusterTimeline : timeline colorée des clusters + anomalies • TransitionMatrix : heatmap P(j|i) avec diagonale auto-transition • EmbeddingsSummary : liste des patterns vectorisés • Boutons "Bayesian update" et "Détecter régime" avec mutation React Query - analytics.py router : 5 nouveaux endpoints (bayesian, regime-clusters, transitions, detect, embeddings) - useApi.ts : 4 nouveaux hooks (useBayesianPosteriors, useRegimeClusters, useRegimeTransitions, usePatternEmbeddings) - App.tsx + Sidebar.tsx : route /analytics-advanced + entrée menu Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -49,6 +49,44 @@ def _max_similarity_vs_existing(candidate_kws: List[str], existing: List[Dict])
|
||||
return max((_jaccard(candidate_kws, p.get("keywords") or []) for p in existing), default=0.0)
|
||||
|
||||
|
||||
_EMBED_SIM_THRESHOLD = 0.75 # seuil cosinus pour considérer deux patterns comme doublons
|
||||
|
||||
|
||||
def _is_duplicate_pattern(
|
||||
candidate: Dict,
|
||||
existing: List[Dict],
|
||||
api_key: str,
|
||||
jaccard_threshold: float = 0.30,
|
||||
) -> bool:
|
||||
"""
|
||||
Retourne True si le candidat est trop similaire à un pattern existant.
|
||||
Essaie les embeddings cosinus (Sprint 4.3) avec fallback Jaccard.
|
||||
"""
|
||||
if not existing:
|
||||
return False
|
||||
|
||||
# Tentative embedding cosinus
|
||||
if api_key:
|
||||
text = ((candidate.get("name") or "") + " " + (candidate.get("description") or "")).strip()
|
||||
if text:
|
||||
try:
|
||||
from services.database import max_cosine_similarity_vs_existing
|
||||
sim = max_cosine_similarity_vs_existing(
|
||||
candidate_text=text,
|
||||
existing_patterns=existing,
|
||||
api_key=api_key,
|
||||
candidate_id=candidate.get("id"),
|
||||
)
|
||||
if sim > 0: # embedding a fonctionné
|
||||
return sim >= _EMBED_SIM_THRESHOLD
|
||||
except Exception as _emb_err:
|
||||
logger.debug(f"[Cycle] Embedding failed, fallback Jaccard: {_emb_err}")
|
||||
|
||||
# Fallback Jaccard
|
||||
sim = _max_similarity_vs_existing(candidate.get("keywords") or [], existing)
|
||||
return sim >= jaccard_threshold
|
||||
|
||||
|
||||
# ── Core cycle logic ──────────────────────────────────────────────────────────
|
||||
|
||||
def run_cycle_once(trigger: str = "auto") -> Dict[str, Any]:
|
||||
@@ -230,20 +268,19 @@ def run_cycle_once(trigger: str = "auto") -> Dict[str, Any]:
|
||||
summary["patterns_suggested"] = len(suggestions)
|
||||
logger.info(f"[Cycle {run_id[:16]}] Suggested {len(suggestions)} patterns from AI")
|
||||
|
||||
# ── Step 3: Filter by similarity ──────────────────────────────────────
|
||||
# ── Step 3: Filter by similarity (Embedding cosinus ou Jaccard fallback) ─
|
||||
existing = get_custom_patterns()
|
||||
logger.info(f"[Cycle {run_id[:16]}] Step 3: {len(existing)} existing patterns, threshold={sim_threshold}")
|
||||
added_count = 0
|
||||
for s in suggestions:
|
||||
kws = s.get("keywords") or []
|
||||
sim = _max_similarity_vs_existing(kws, existing)
|
||||
if sim < sim_threshold:
|
||||
if not _is_duplicate_pattern(s, existing, ai_key, jaccard_threshold=sim_threshold):
|
||||
|
||||
# Capture returned ID so the pattern has a valid id for scoring
|
||||
assigned_id = save_custom_pattern(s)
|
||||
s["id"] = assigned_id
|
||||
existing.append(s)
|
||||
added_count += 1
|
||||
logger.info(f"[Cycle] Added pattern '{s.get('name')}' id={assigned_id} (sim={sim:.2f})")
|
||||
logger.info(f"[Cycle] Added pattern '{s.get('name')}' id={assigned_id}")
|
||||
# ── Save suggestion reasoning trace ───────────────────────────
|
||||
_top_news_ctx = [
|
||||
{"title": n.get("title", "")[:120], "impact": round(float(n.get("impact_score") or 0), 2), "source": n.get("source", "")}
|
||||
@@ -276,7 +313,7 @@ def run_cycle_once(trigger: str = "auto") -> Dict[str, Any]:
|
||||
macro_dominant=dominant,
|
||||
)
|
||||
else:
|
||||
logger.debug(f"[Cycle] Filtered '{s.get('name')}' — sim={sim:.2f} >= {sim_threshold}")
|
||||
logger.debug(f"[Cycle] Filtered '{s.get('name')}' — doublon détecté")
|
||||
|
||||
summary["patterns_added"] = added_count
|
||||
|
||||
@@ -434,6 +471,28 @@ def run_cycle_once(trigger: str = "auto") -> Dict[str, Any]:
|
||||
"fetched_at": datetime.utcnow().isoformat(), "cached": False}
|
||||
_macro_cache["ts"] = _dt.datetime.utcnow()
|
||||
|
||||
# ── Step 5.5: Bayesian posterior update (Sprint 4.1) ─────────────────
|
||||
try:
|
||||
from services.database import update_bayesian_posteriors
|
||||
_n_updated = update_bayesian_posteriors()
|
||||
if _n_updated:
|
||||
logger.info(f"[Cycle {run_id[:16]}] Bayesian posteriors mis à jour : {_n_updated} patterns")
|
||||
except Exception as _be:
|
||||
logger.warning(f"[Cycle] Bayesian update failed (non-blocking): {_be}")
|
||||
|
||||
# ── Step 5.6: Régime clustering (Sprint 4.2) ──────────────────────────
|
||||
try:
|
||||
from services.database import detect_and_save_regime_clusters
|
||||
_cluster_result = detect_and_save_regime_clusters(n_clusters=4, days=180)
|
||||
if "current_cluster" in _cluster_result:
|
||||
logger.info(
|
||||
f"[Cycle {run_id[:16]}] Régime cluster : {_cluster_result.get('current_label')} "
|
||||
f"(cluster {_cluster_result.get('current_cluster')}, "
|
||||
f"anomalie={_cluster_result.get('current_anomaly')})"
|
||||
)
|
||||
except Exception as _ce:
|
||||
logger.warning(f"[Cycle] Régime clustering failed (non-blocking): {_ce}")
|
||||
|
||||
# ── Step 6: GPT-4o cycle commentary ──────────────────────────────────
|
||||
logger.info(f"[Cycle {run_id[:16]}] Step 6: generating commentary")
|
||||
commentary = _generate_cycle_commentary(
|
||||
|
||||
Reference in New Issue
Block a user