feat: causal lab
This commit is contained in:
@@ -134,12 +134,12 @@ def _fetch_prices(event_date_str: str, instruments: list[str]) -> dict:
|
||||
return out
|
||||
|
||||
|
||||
def _drift_metrics(prices: dict, event_date_str: str, inst: str, lag_min: int = 0) -> dict:
|
||||
def _drift_metrics(prices: dict, event_date_str: str, inst: str, lag_min: int = 0, lag_days: 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", "lag_min": lag_min}
|
||||
empty = {"pre_pips": None, "post_pips": None, "drift_ratio": None, "leak": "unknown", "lag_min": lag_min, "lag_days": lag_days}
|
||||
if not series:
|
||||
return empty
|
||||
|
||||
@@ -154,13 +154,18 @@ def _drift_metrics(prices: dict, event_date_str: str, inst: str, lag_min: int =
|
||||
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]
|
||||
if not pre or not same:
|
||||
# En mode journalier, lag_days décale la date de référence "post"
|
||||
if lag_days > 0:
|
||||
post_date = (datetime.strptime(edate, "%Y-%m-%d") + timedelta(days=lag_days)).strftime("%Y-%m-%d")
|
||||
pre = [b for b in series if b["t"] <= edate]
|
||||
post = [b for b in series if b["t"] >= post_date]
|
||||
else:
|
||||
pre = [b for b in series if b["t"] < edate]
|
||||
post = [b for b in series if b["t"] == edate]
|
||||
if not pre or not post:
|
||||
return empty
|
||||
mult = 10000 if inst in ("EURUSD",) else 10
|
||||
pre_pips = None
|
||||
post_pips = round((same[0]["c"] - pre[-1]["c"]) * mult)
|
||||
post_pips = round((post[0]["c"] - pre[-1]["c"]) * mult)
|
||||
|
||||
ratio: Optional[float] = None
|
||||
if pre_pips is not None and post_pips and post_pips != 0:
|
||||
@@ -866,7 +871,7 @@ 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
|
||||
# Lag effectif : max des lag_min/lag_days 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")}
|
||||
@@ -874,6 +879,10 @@ def analyze_event(body: AnalyzeRequest):
|
||||
(e.get("lag_min", 0) or 0 for e in edges if e.get("to") in output_ids),
|
||||
default=0,
|
||||
)
|
||||
effective_lag_days = max(
|
||||
(e.get("lag_days", 0) or 0 for e in edges if e.get("to") in output_ids),
|
||||
default=0,
|
||||
)
|
||||
|
||||
# Instruments : utiliser tous ceux du template si aucun spécifié
|
||||
if body.instrument:
|
||||
@@ -888,7 +897,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, lag_min=effective_lag)
|
||||
drift = _drift_metrics(prices, event["start_date"], inst, lag_min=effective_lag, lag_days=effective_lag_days)
|
||||
drift_by_inst[inst] = drift
|
||||
if drift.get("post_pips") is not None:
|
||||
actual_moves[inst] = drift["post_pips"]
|
||||
@@ -919,6 +928,7 @@ def analyze_event(body: AnalyzeRequest):
|
||||
"drift": drift_by_inst.get(primary_inst, {}),
|
||||
"prices_mode": prices.get("mode", "none"),
|
||||
"effective_lag_min": effective_lag,
|
||||
"effective_lag_days": effective_lag_days,
|
||||
"analyzed_at": analyzed_at,
|
||||
"graph_json": graph, # structure complète pour visualisation frontend
|
||||
}
|
||||
|
||||
@@ -22,7 +22,8 @@ export interface CausalEdge {
|
||||
strength?: 1 | 2 | 3
|
||||
sign?: 'positive' | 'negative' | 'neutral'
|
||||
label?: string
|
||||
lag_min?: number // délai avant onset (minutes, 0 = immédiat)
|
||||
lag_min?: number // délai avant onset (minutes) — mode intraday 5m
|
||||
lag_days?: number // délai avant onset (jours) — mode journalier
|
||||
diffusion_min?: number // durée jusqu'à absorption complète (minutes)
|
||||
decay_days?: number | null // demi-vie (jours), null = shift permanent
|
||||
}
|
||||
@@ -110,11 +111,10 @@ const edgeColor = (sign?: string): string => ({
|
||||
/** Épaisseur selon strength */
|
||||
const edgeWidth = (s?: number) => s === 1 ? 1 : s === 3 ? 3.5 : 2
|
||||
|
||||
/** Format lag minutes → "+5m" / "+2h" / "+3j" */
|
||||
/** Format lag minutes → "+5m" / "+2h" */
|
||||
function fmtLag(min: number): string {
|
||||
if (min < 60) return `+${min}m`
|
||||
if (min < 1440) return `+${(min / 60).toFixed(0)}h`
|
||||
return `+${(min / 1440).toFixed(0)}j`
|
||||
return `+${(min / 60).toFixed(0)}h`
|
||||
}
|
||||
/** Format decay days → "↩3j" */
|
||||
const fmtDecay = (d: number) => `↩${d}j`
|
||||
@@ -248,7 +248,9 @@ export function GraphSVG({
|
||||
const lpos = e.label ? edgeLabelPos(e) : null
|
||||
const labelText = e.label ?? ''
|
||||
const labelW = labelText.length * 5
|
||||
const lagTxt = (!compact && e.lag_min) ? fmtLag(e.lag_min) : null
|
||||
const lagTxtMin = (!compact && e.lag_min) ? fmtLag(e.lag_min) : null
|
||||
const lagTxtDay = (!compact && e.lag_days) ? `+${e.lag_days}j` : null
|
||||
const lagTxt = lagTxtMin && lagTxtDay ? `${lagTxtMin}/${lagTxtDay}` : (lagTxtMin ?? lagTxtDay)
|
||||
const decayTxt = (!compact && e.decay_days != null) ? fmtDecay(e.decay_days) : null
|
||||
const lagPos = lagTxt ? bezierPt(e, 0.1) : null
|
||||
const decayPos = decayTxt ? bezierPt(e, 0.88) : null
|
||||
@@ -466,7 +468,7 @@ function TabLibrary({ initialTemplateId }: { initialTemplateId?: number | null }
|
||||
} finally { setSavingTheory(false) }
|
||||
}
|
||||
|
||||
function updateEdgeLag(i: number, field: 'lag_min' | 'diffusion_min' | 'decay_days', val: string) {
|
||||
function updateEdgeLag(i: number, field: 'lag_min' | 'lag_days' | 'diffusion_min' | 'decay_days', val: string) {
|
||||
setEdgesLag(prev => prev.map((e, idx) => idx !== i ? e : {
|
||||
...e,
|
||||
[field]: val === '' ? (field === 'decay_days' ? null : undefined)
|
||||
@@ -532,7 +534,7 @@ function TabLibrary({ initialTemplateId }: { initialTemplateId?: number | null }
|
||||
</div>
|
||||
|
||||
{selected ? (
|
||||
<div className="flex-1 overflow-y-auto space-y-4 min-w-0">
|
||||
<div className="flex-1 overflow-y-auto space-y-4 min-w-0 max-h-[calc(100vh-230px)]">
|
||||
<div className="bg-dark-700 rounded-lg p-4 border border-slate-700/40">
|
||||
<div className="flex items-start justify-between mb-2">
|
||||
<div>
|
||||
@@ -604,28 +606,38 @@ function TabLibrary({ initialTemplateId }: { initialTemplateId?: number | null }
|
||||
<Zap className="w-3.5 h-3.5 text-yellow-400" /> Paramètres temporels
|
||||
</h4>
|
||||
<div className="space-y-2">
|
||||
<div className="grid grid-cols-[1fr_64px_64px_64px] gap-2 text-xs text-slate-500 pb-1 border-b border-slate-700/30">
|
||||
<span>Arête</span><span className="text-center">Lag (m)</span>
|
||||
<span className="text-center">Diff. (m)</span><span className="text-center">Decay (j)</span>
|
||||
<div className="grid grid-cols-[1fr_52px_52px_52px_52px] gap-1.5 text-xs text-slate-500 pb-1 border-b border-slate-700/30">
|
||||
<span>Arête</span>
|
||||
<span className="text-center" title="Délai intraday (mode 5m)">Lag 5m</span>
|
||||
<span className="text-center" title="Délai journalier (mode daily)">Lag j</span>
|
||||
<span className="text-center">Diff. m</span>
|
||||
<span className="text-center">Decay j</span>
|
||||
</div>
|
||||
{editEdgesLag.map((e, i) => (
|
||||
<div key={i} className="grid grid-cols-[1fr_64px_64px_64px] gap-2 items-center">
|
||||
<div key={i} className="grid grid-cols-[1fr_52px_52px_52px_52px] gap-1.5 items-center">
|
||||
<span className="text-xs font-mono text-slate-400 truncate">{e.from} → {e.to}</span>
|
||||
<input type="number" min={0} step={5}
|
||||
value={e.lag_min ?? ''}
|
||||
onChange={ev => updateEdgeLag(i, 'lag_min', ev.target.value)}
|
||||
placeholder="0"
|
||||
className="w-full bg-dark-800 border border-slate-600 rounded px-1.5 py-1 text-xs text-slate-200 text-center" />
|
||||
title="Délai en minutes (intraday 5m)"
|
||||
className="w-full bg-dark-800 border border-slate-600 rounded px-1 py-1 text-xs text-slate-200 text-center" />
|
||||
<input type="number" min={0} step={1}
|
||||
value={e.lag_days ?? ''}
|
||||
onChange={ev => updateEdgeLag(i, 'lag_days', ev.target.value)}
|
||||
placeholder="0"
|
||||
title="Délai en jours (mode journalier)"
|
||||
className="w-full bg-dark-800 border border-amber-600/40 rounded px-1 py-1 text-xs text-amber-200 text-center" />
|
||||
<input type="number" min={0} step={15}
|
||||
value={e.diffusion_min ?? ''}
|
||||
onChange={ev => updateEdgeLag(i, 'diffusion_min', ev.target.value)}
|
||||
placeholder="60"
|
||||
className="w-full bg-dark-800 border border-slate-600 rounded px-1.5 py-1 text-xs text-slate-200 text-center" />
|
||||
className="w-full bg-dark-800 border border-slate-600 rounded px-1 py-1 text-xs text-slate-200 text-center" />
|
||||
<input type="number" min={0} step={1}
|
||||
value={e.decay_days ?? ''}
|
||||
onChange={ev => updateEdgeLag(i, 'decay_days', ev.target.value)}
|
||||
placeholder="∞"
|
||||
className="w-full bg-dark-800 border border-slate-600 rounded px-1.5 py-1 text-xs text-slate-200 text-center" />
|
||||
className="w-full bg-dark-800 border border-slate-600 rounded px-1 py-1 text-xs text-slate-200 text-center" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -1216,23 +1228,30 @@ function TabEditor() {
|
||||
value={newEdge.label ?? ''} onChange={e => setNewEdge(p => ({ ...p, label: e.target.value }))}
|
||||
className="w-full bg-dark-700 border border-slate-600 rounded px-1.5 py-1 text-slate-200 text-xs mt-0.5" />
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-1">
|
||||
<div className="grid grid-cols-4 gap-1">
|
||||
<div>
|
||||
<label className="text-slate-500">Lag (min)</label>
|
||||
<label className="text-slate-500" title="Délai en minutes (intraday 5m)">Lag (min)</label>
|
||||
<input type="number" min={0} step={5} placeholder="0"
|
||||
value={newEdge.lag_min ?? ''}
|
||||
onChange={e => setNewEdge(p => ({ ...p, lag_min: e.target.value === '' ? undefined : parseInt(e.target.value) }))}
|
||||
className="w-full bg-dark-700 border border-slate-600 rounded px-1.5 py-1 text-slate-200 text-xs mt-0.5" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-slate-500">Diffusion (min)</label>
|
||||
<label className="text-amber-400" title="Délai en jours (mode journalier)">Lag (j)</label>
|
||||
<input type="number" min={0} step={1} placeholder="0"
|
||||
value={newEdge.lag_days ?? ''}
|
||||
onChange={e => setNewEdge(p => ({ ...p, lag_days: e.target.value === '' ? undefined : parseInt(e.target.value) }))}
|
||||
className="w-full bg-dark-700 border border-amber-600/40 rounded px-1.5 py-1 text-amber-200 text-xs mt-0.5" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-slate-500">Diff. (min)</label>
|
||||
<input type="number" min={0} step={15} placeholder="60"
|
||||
value={newEdge.diffusion_min ?? ''}
|
||||
onChange={e => setNewEdge(p => ({ ...p, diffusion_min: e.target.value === '' ? undefined : parseInt(e.target.value) }))}
|
||||
className="w-full bg-dark-700 border border-slate-600 rounded px-1.5 py-1 text-slate-200 text-xs mt-0.5" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-slate-500">Decay (jours)</label>
|
||||
<label className="text-slate-500">Decay (j)</label>
|
||||
<input type="number" min={0} step={1} placeholder="∞"
|
||||
value={newEdge.decay_days ?? ''}
|
||||
onChange={e => setNewEdge(p => ({ ...p, decay_days: e.target.value === '' ? null : parseFloat(e.target.value) }))}
|
||||
@@ -1304,16 +1323,23 @@ function TabEditor() {
|
||||
onChange={ev => setEditEdgeVal(p => ({ ...p, label: ev.target.value }))}
|
||||
className="w-full bg-dark-700 border border-slate-600 rounded px-1.5 py-1 text-slate-200 text-xs mt-0.5" />
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-1">
|
||||
<div className="grid grid-cols-4 gap-1">
|
||||
<div>
|
||||
<label className="text-slate-500">Lag (min)</label>
|
||||
<label className="text-slate-500" title="Délai en minutes (intraday 5m)">Lag (min)</label>
|
||||
<input type="number" min={0} step={5}
|
||||
value={editEdge.lag_min ?? e.lag_min ?? ''}
|
||||
onChange={ev => setEditEdgeVal(p => ({ ...p, lag_min: ev.target.value === '' ? undefined : parseInt(ev.target.value) }))}
|
||||
className="w-full bg-dark-700 border border-slate-600 rounded px-1.5 py-1 text-slate-200 text-xs mt-0.5" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-slate-500">Diffusion (min)</label>
|
||||
<label className="text-amber-400" title="Délai en jours (mode journalier)">Lag (j)</label>
|
||||
<input type="number" min={0} step={1}
|
||||
value={editEdge.lag_days ?? e.lag_days ?? ''}
|
||||
onChange={ev => setEditEdgeVal(p => ({ ...p, lag_days: ev.target.value === '' ? undefined : parseInt(ev.target.value) }))}
|
||||
className="w-full bg-dark-700 border border-amber-600/40 rounded px-1.5 py-1 text-amber-200 text-xs mt-0.5" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-slate-500">Diff. (min)</label>
|
||||
<input type="number" min={0} step={15}
|
||||
value={editEdge.diffusion_min ?? e.diffusion_min ?? ''}
|
||||
onChange={ev => setEditEdgeVal(p => ({ ...p, diffusion_min: ev.target.value === '' ? undefined : parseInt(ev.target.value) }))}
|
||||
|
||||
Reference in New Issue
Block a user