feat: market event

This commit is contained in:
OpenSquared
2026-06-29 21:16:29 +02:00
parent 5e819b5b67
commit b223f50f8b
2 changed files with 98 additions and 9 deletions

View File

@@ -1127,9 +1127,9 @@ def _build_graph_json_from_spec(spec: dict) -> dict:
def _run_auto_analysis(event: dict, template_id: int) -> bool:
"""
Lightweight causal analysis for the auto_template pipeline.
Builds inputs from event fields (no yfinance), evaluates the graph,
and upserts into causal_event_analyses so the instrument frise can read it.
Causal analysis for the auto_template pipeline.
Builds inputs from event fields, evaluates the graph, fetches actual prices,
and upserts into causal_event_analyses so the instrument frise can score it.
"""
try:
from services.database import get_conn
@@ -1220,9 +1220,27 @@ def _run_auto_analysis(event: dict, template_id: int) -> bool:
analyzed_at = datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ")
event_id = event["id"]
# Effective lag from template edges (for price window and chip width)
edges = graph.get("edges", [])
effective_lag_days = max((e.get("lag_days", 0) or 0 for e in edges), default=0)
# Fetch actual prices to compute actual pips for scoring
actual_moves: dict = {}
drift_by_inst: dict = {}
try:
prices = _fetch_prices(event["start_date"], instruments, lag_days=effective_lag_days)
for inst in instruments:
drift = _drift_metrics(prices, event["start_date"], inst,
lag_min=0, lag_days=effective_lag_days)
drift_by_inst[inst] = drift
if drift.get("post_pips") is not None:
actual_moves[inst] = drift["post_pips"]
except Exception as _pe:
logger.warning(f"[auto_analysis] price fetch failed for event #{event_id}: {_pe}")
logger.info(
f"[auto_analysis] event #{event_id} → tmpl #{template_id} | "
f"inputs={inputs} | instruments={all_instruments_csv}"
f"inputs={inputs} | instruments={all_instruments_csv} | actual_moves={actual_moves}"
)
existing = conn.execute(
@@ -1238,8 +1256,8 @@ def _run_auto_analysis(event: dict, template_id: int) -> bool:
WHERE market_event_id=? AND template_id=?
""", (
all_instruments_csv, json.dumps(inputs), json.dumps({}),
json.dumps(node_values), json.dumps({}), None,
json.dumps({}), analyzed_at,
json.dumps(node_values), json.dumps(actual_moves), None,
json.dumps(drift_by_inst), analyzed_at,
event_id, template_id,
))
else:
@@ -1252,12 +1270,12 @@ def _run_auto_analysis(event: dict, template_id: int) -> bool:
""", (
event_id, template_id, all_instruments_csv,
json.dumps(inputs), json.dumps({}),
json.dumps(node_values), json.dumps({}),
None, json.dumps({}), analyzed_at,
json.dumps(node_values), json.dumps(actual_moves),
None, json.dumps(drift_by_inst), analyzed_at,
))
conn.commit()
conn.close()
logger.info(f"[auto_analysis] causal_event_analyses upserted event #{event_id}, tmpl #{template_id}")
logger.info(f"[auto_analysis] upserted event #{event_id}, tmpl #{template_id}, actual_moves={actual_moves}")
return True
except Exception as e:
@@ -1369,6 +1387,49 @@ Règles : coef intermédiaire ∈ [-5,5] ; coef output en pips, |val| ∈ [30,20
return {"error": str(e)}
@router.post("/api/causal-lab/auto-analyze/refresh")
def refresh_auto_analyses():
"""
Re-run _run_auto_analysis for all causal_event_analyses rows with empty actual_json.
Fills in real price data so comprehension scores can be computed.
"""
try:
from services.database import get_conn
conn = get_conn()
rows = conn.execute("""
SELECT a.id AS cea_id, a.market_event_id, a.template_id,
a.actual_json
FROM causal_event_analyses a
WHERE a.actual_json IS NULL OR a.actual_json = '{}' OR a.actual_json = 'null'
""").fetchall()
conn.close()
refreshed, failed = 0, 0
for row in rows:
try:
conn2 = get_conn()
ev_row = conn2.execute(
"SELECT * FROM market_events WHERE id = ?", (row["market_event_id"],)
).fetchone()
conn2.close()
if not ev_row:
continue
ok = _run_auto_analysis(dict(ev_row), row["template_id"])
if ok:
refreshed += 1
else:
failed += 1
except Exception as _e:
logger.warning(f"[refresh_auto_analyses] cea#{row['cea_id']}: {_e}")
failed += 1
logger.info(f"[refresh_auto_analyses] done: {refreshed} refreshed, {failed} failed")
return {"refreshed": refreshed, "failed": failed, "total": len(rows)}
except Exception as e:
logger.error(f"[refresh_auto_analyses] {e}")
raise HTTPException(500, str(e))
@router.post("/api/causal-lab/create-from-event")
def create_template_from_event(body: CreateFromEventRequest):
"""GPT-4o génère et enregistre un template causal adapté à l'événement."""

View File

@@ -1020,6 +1020,9 @@ function ExplanationScore({
templates: CausalTemplate[]
causalInsts: string[]
}) {
const [refreshing, setRefreshing] = useState(false)
const [refreshDone, setRefreshDone] = useState(false)
// For each linked event, compute the comprehension score and collect scored ones
const scored: number[] = []
const totalLinked: number[] = []
@@ -1051,6 +1054,19 @@ function ExplanationScore({
const barCls = globalScore == null ? 'bg-slate-600'
: globalScore >= 70 ? 'bg-emerald-500' : globalScore >= 40 ? 'bg-amber-500' : 'bg-red-500'
const handleRefresh = async () => {
setRefreshing(true)
try {
await api.post('/causal-lab/auto-analyze/refresh')
setRefreshDone(true)
setTimeout(() => setRefreshDone(false), 3000)
} finally {
setRefreshing(false)
}
}
const needsRefresh = totalLinked.length > 0 && scored.length === 0 && !refreshDone
return (
<div className="flex items-center gap-3 px-4 py-2.5 rounded-xl border border-slate-700/30 bg-dark-800/50">
<span className="text-xs text-slate-500 whitespace-nowrap">Note globale</span>
@@ -1063,6 +1079,18 @@ function ExplanationScore({
<span className="text-[10px] text-slate-600 whitespace-nowrap">
{scored.length}/{totalLinked.length} graphes
</span>
{needsRefresh && (
<button
onClick={handleRefresh}
disabled={refreshing}
className="text-[10px] text-violet-400 hover:text-violet-200 bg-violet-900/20 hover:bg-violet-900/40 border border-violet-800/40 rounded px-2 py-1 whitespace-nowrap disabled:opacity-50 transition-colors"
>
{refreshing ? '…' : 'Recalculer'}
</button>
)}
{refreshDone && (
<span className="text-[10px] text-emerald-500 whitespace-nowrap"> Rechargez la page</span>
)}
</div>
)
}