feat: market event
This commit is contained in:
@@ -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."""
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user