feat: Phase 4+5 — price discovery status + replay historique

Phase 4 — Price Discovery Status (la pièce maîtresse) :
- price_discovery.py (nouveau) : capture_price_snapshots() sauve les prix des tickers
  liés à chaque news scorée (energy→BZ=F/NG=F, metals→GC=F/HG=F, indices→^GSPC/IWM)
- compute_absorptions() mesure combien du mouvement attendu s'est déjà produit
  (status: not_yet_priced <30% / partially_priced 30-80% / fully_priced >80%)
- build_price_discovery_block() → bloc prompt avec opportunités classées
- database.py : table news_price_snapshots + save/get/purge fonctions
- auto_cycle.py : capture après ai_score_news_batch, compute avant suggestions,
  block injecté dans suggestion + scoring prompts + context snapshot
- ai_analyzer.py : param price_discovery_block dans suggest + score

Phase 5 — Replay historique :
- cycle.py : POST /api/cycle/contexts/{run_id}/replay — recharge le snapshot historique
  et relance suggest_patterns_from_market_context avec le contexte original
- useApi.ts : hook useReplayCycle
- SystemLogs.tsx : bouton "Rejouer ce cycle" dans onglet Contexte IA avec champ
  notes, résultats inline (liste des patterns générés), section price_discovery
  ouverte par défaut en rouge

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
OpenSquared
2026-06-20 16:58:57 +02:00
parent 9c0ebbd138
commit a21699805b
7 changed files with 499 additions and 15 deletions

View File

@@ -98,3 +98,96 @@ def get_context_snapshot(run_id: str):
if not snap:
raise HTTPException(404, "Snapshot non trouvé pour ce cycle")
return snap
class ReplayRequest(BaseModel):
override_notes: Optional[str] = None # optional annotation added to the replay
@router.post("/contexts/{run_id}/replay")
def replay_cycle(run_id: str, req: ReplayRequest):
"""
Phase 5 — Re-run AI suggestion with the historical context from a saved snapshot.
Returns new pattern suggestions based on the original context data.
"""
snap = get_cycle_context_snapshot(run_id)
if not snap:
raise HTTPException(404, "Snapshot non trouvé pour ce cycle")
ctx = snap.get("context", {})
if not ctx:
raise HTTPException(400, "Snapshot vide — impossible de rejouer")
try:
from services.ai_analyzer import suggest_patterns_from_market_context, apply_news_decay, partition_news_by_age
import json
# Reconstruct minimal inputs from the snapshot
# news: merge inter_cycle + recent_24h + older from partitioned snapshot
news_part = ctx.get("news_partitioned", {})
news_flat = (
news_part.get("inter_cycle", [])
+ news_part.get("recent_24h", [])
+ news_part.get("older", [])
)
# quotes: reconstruct from quotes_summary
quotes_by_class = ctx.get("quotes_summary", {})
# calendar
calendar = ctx.get("calendar", [])
# macro regime (simplified for replay)
macro_regime = None
if ctx.get("macro_regime"):
macro_regime = {"scenarios": {"dominant": ctx["macro_regime"].get("dominant"), "scores": ctx["macro_regime"].get("scores"), "asset_bias": {}, "reasons": []}, "gauges": {}}
# geo score
geo_score = ctx.get("geo_score", {"score": 50, "level": "medium", "top_risks": []})
# preserved blocks
tech_block = ctx.get("tech_indicators_block", "")
iv_context = ctx.get("iv_context_preview", "")
cycle_meta = ctx.get("cycle_meta", {})
# Rebuild FRED block from saved releases
fred_block = ""
if ctx.get("fred_releases"):
from services.fred_fetcher import build_fred_context_block
fred_block = build_fred_context_block(ctx["fred_releases"])
# Rebuild price discovery block from saved absorptions
pd_block = ""
if ctx.get("price_discovery"):
from services.price_discovery import build_price_discovery_block
pd_block = build_price_discovery_block(ctx["price_discovery"])
# Add replay note to cycle_meta
if req.override_notes:
cycle_meta = {**cycle_meta, "replay_notes": req.override_notes}
cycle_meta["is_replay"] = True
cycle_meta["replayed_at"] = __import__("datetime").datetime.utcnow().isoformat()
suggestions = suggest_patterns_from_market_context(
news=news_flat,
quotes_by_class=quotes_by_class,
calendar=calendar,
macro_regime=macro_regime,
geo_score=geo_score,
iv_context=iv_context,
cycle_meta=cycle_meta,
tech_indicators_block=tech_block,
fred_block=fred_block,
price_discovery_block=pd_block,
)
return {
"run_id": run_id,
"original_ts": snap["ts"],
"replayed_at": cycle_meta["replayed_at"],
"override_notes": req.override_notes,
"suggestions_count": len(suggestions),
"suggestions": suggestions,
}
except Exception as e:
raise HTTPException(500, f"Replay failed: {str(e)}")