feat: temporal params (lag/diffusion/decay) sur aretes causales

- CausalEdge: lag_min, diffusion_min, decay_days
- SVG: labels +Xm (jaune, source) et decay (gris, cible) sur les aretes
- Editeur: champs lag/diffusion/decay dans add-edge et edit-edge
- Bibliotheque: section Parametres temporels avec grille editable + sauvegarde PATCH
- Backend: _drift_metrics utilise lag_min pour decaler fenetre intraday 5min
- analyze: calcule effective_lag depuis aretes → market_asset, retourne dans resultat

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
OpenSquared
2026-06-28 11:47:04 +02:00
parent cb33fa0dd7
commit 98d6be8212
2 changed files with 192 additions and 28 deletions

View File

@@ -134,24 +134,25 @@ def _fetch_prices(event_date_str: str, instruments: list[str]) -> dict:
return out
def _drift_metrics(prices: dict, event_date_str: str, inst: str) -> dict:
def _drift_metrics(prices: dict, event_date_str: str, inst: str, lag_min: int = 0) -> dict:
series = prices.get(inst, [])
mode = prices.get("mode", "none")
edate = event_date_str[:10]
empty = {"pre_pips": None, "post_pips": None, "drift_ratio": None, "leak": "unknown"}
empty = {"pre_pips": None, "post_pips": None, "drift_ratio": None, "leak": "unknown", "lag_min": lag_min}
if not series:
return empty
mult = 10000 if inst in ("EURUSD",) else 10
if mode == "intraday_5m":
n = len(series)
if n < 8:
return empty
mid = n // 2
# Convertir en pips (×10000 pour FX, ×10 pour or/pétrole/SP500)
mult = 10000 if inst in ("EURUSD",) else 10
pre_pips = round((series[mid - 1]["c"] - series[0]["c"]) * mult)
post_pips = round((series[-1]["c"] - series[mid]["c"]) * mult)
# Décale le mid en avant selon le lag (chaque barre = 5 min)
lag_bars = max(0, round(lag_min / 5))
mid = min(n // 2 + lag_bars, n - 2)
pre_pips = round((series[mid - 1]["c"] - series[0]["c"]) * mult)
post_pips = round((series[-1]["c"] - series[mid]["c"]) * mult)
else:
pre = [b for b in series if b["t"] < edate]
same = [b for b in series if b["t"] == edate]
@@ -760,6 +761,15 @@ def analyze_event(body: AnalyzeRequest):
# Évaluation du graphe
node_values = evaluate_graph(graph, inputs, body.coef_overrides or {})
# Lag effectif : max des lag_min sur toutes les arêtes menant à un market_asset
edges = graph.get("edges", [])
nodes_map = {n["id"]: n for n in graph.get("nodes", [])}
output_ids = {n["id"] for n in graph.get("nodes", []) if n.get("type") in ("market_asset", "output")}
effective_lag = max(
(e.get("lag_min", 0) or 0 for e in edges if e.get("to") in output_ids),
default=0,
)
# Prix réels
instruments = list({body.instrument} | set(tmpl.get("instruments", [body.instrument])))
prices = _fetch_prices(event["start_date"], instruments)
@@ -768,7 +778,7 @@ def analyze_event(body: AnalyzeRequest):
actual_moves: dict = {}
drift_by_inst: dict = {}
for inst in instruments:
drift = _drift_metrics(prices, event["start_date"], inst)
drift = _drift_metrics(prices, event["start_date"], inst, lag_min=effective_lag)
drift_by_inst[inst] = drift
if drift.get("post_pips") is not None:
actual_moves[inst] = drift["post_pips"]
@@ -786,18 +796,19 @@ def analyze_event(body: AnalyzeRequest):
analyzed_at = datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ")
result = {
"event": event,
"template_id": body.template_id,
"template_name": tmpl["name"],
"instrument": body.instrument,
"inputs": inputs,
"node_values": node_values,
"actual_moves": actual_moves,
"yields": yields,
"activation": activation,
"drift": drift_by_inst.get(body.instrument, {}),
"prices_mode": prices.get("mode", "none"),
"analyzed_at": analyzed_at,
"event": event,
"template_id": body.template_id,
"template_name": tmpl["name"],
"instrument": body.instrument,
"inputs": inputs,
"node_values": node_values,
"actual_moves": actual_moves,
"yields": yields,
"activation": activation,
"drift": drift_by_inst.get(body.instrument, {}),
"prices_mode": prices.get("mode", "none"),
"effective_lag_min": effective_lag,
"analyzed_at": analyzed_at,
}
# Persistance