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>
194 lines
7.4 KiB
Python
194 lines
7.4 KiB
Python
from fastapi import APIRouter, HTTPException
|
|
from pydantic import BaseModel
|
|
from typing import Optional
|
|
from services.database import get_cycle_runs, get_cycle_run, set_config, get_config, list_cycle_context_snapshots, get_cycle_context_snapshot
|
|
from services.auto_cycle import get_status, trigger_manual, restart_scheduler
|
|
|
|
router = APIRouter(prefix="/api/cycle", tags=["cycle"])
|
|
|
|
|
|
@router.get("/status")
|
|
def cycle_status():
|
|
"""Current scheduler state + last cycle summary."""
|
|
return get_status()
|
|
|
|
|
|
@router.get("/history")
|
|
def cycle_history(limit: int = 20):
|
|
"""List recent cycle runs."""
|
|
runs = get_cycle_runs(limit=limit)
|
|
# Parse commentary JSON for frontend
|
|
import json
|
|
for r in runs:
|
|
if r.get("commentary"):
|
|
try:
|
|
r["commentary_parsed"] = json.loads(r["commentary"])
|
|
except Exception:
|
|
r["commentary_parsed"] = {"commentary": r["commentary"]}
|
|
return {"runs": runs, "count": len(runs)}
|
|
|
|
|
|
@router.post("/trigger")
|
|
def trigger_cycle():
|
|
"""Manually trigger one cycle immediately (non-blocking)."""
|
|
key = get_config("openai_api_key") or ""
|
|
if not key:
|
|
raise HTTPException(400, "Clé OpenAI non configurée")
|
|
trigger_manual()
|
|
return {"triggered": True, "message": "Cycle lancé en arrière-plan"}
|
|
|
|
|
|
class CycleConfigRequest(BaseModel):
|
|
enabled: Optional[bool] = None
|
|
interval_hours: Optional[float] = None
|
|
similarity_threshold: Optional[float] = None
|
|
min_ev_threshold: Optional[float] = None
|
|
min_score_threshold: Optional[int] = None
|
|
journal_retention_days: Optional[int] = None
|
|
maturity_threshold_pct: Optional[int] = None
|
|
|
|
|
|
@router.post("/config")
|
|
def update_cycle_config(req: CycleConfigRequest):
|
|
"""Update auto-cycle + filter configuration and restart scheduler."""
|
|
if req.enabled is not None:
|
|
set_config("auto_cycle_enabled", "true" if req.enabled else "false")
|
|
if req.interval_hours is not None:
|
|
if not (0.5 <= req.interval_hours <= 24):
|
|
raise HTTPException(400, "interval_hours must be between 0.5 and 24")
|
|
set_config("auto_cycle_hours", str(req.interval_hours))
|
|
if req.similarity_threshold is not None:
|
|
if not (0.0 <= req.similarity_threshold <= 1.0):
|
|
raise HTTPException(400, "similarity_threshold must be between 0 and 1")
|
|
set_config("auto_cycle_similarity_threshold", str(req.similarity_threshold))
|
|
if req.min_ev_threshold is not None:
|
|
if not (0.0 <= req.min_ev_threshold <= 1.0):
|
|
raise HTTPException(400, "min_ev_threshold must be between 0 and 1")
|
|
set_config("min_ev_threshold", str(req.min_ev_threshold))
|
|
if req.min_score_threshold is not None:
|
|
if not (0 <= req.min_score_threshold <= 100):
|
|
raise HTTPException(400, "min_score_threshold must be between 0 and 100")
|
|
set_config("min_score_threshold", str(req.min_score_threshold))
|
|
if req.journal_retention_days is not None:
|
|
if not (7 <= req.journal_retention_days <= 365):
|
|
raise HTTPException(400, "journal_retention_days must be between 7 and 365")
|
|
set_config("journal_retention_days", str(req.journal_retention_days))
|
|
if req.maturity_threshold_pct is not None:
|
|
if not (5 <= req.maturity_threshold_pct <= 75):
|
|
raise HTTPException(400, "maturity_threshold_pct must be between 5 and 75")
|
|
set_config("maturity_threshold_pct", str(req.maturity_threshold_pct))
|
|
|
|
# Restart scheduler to pick up changes
|
|
restart_scheduler()
|
|
|
|
return get_status()
|
|
|
|
|
|
|
|
@router.get("/contexts")
|
|
def list_context_snapshots(limit: int = 30):
|
|
"""List cycle context snapshots (most recent first)."""
|
|
return {"snapshots": list_cycle_context_snapshots(limit=limit)}
|
|
|
|
|
|
@router.get("/contexts/{run_id}")
|
|
def get_context_snapshot(run_id: str):
|
|
"""Return the full context snapshot for a given cycle run_id."""
|
|
snap = get_cycle_context_snapshot(run_id)
|
|
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)}")
|