feat: causal lab - delete template + observable mapping + edge editing
- TabLibrary: bouton Supprimer (templates user uniquement) - TabEditor: inputMapping dans EditorState, panneau mapping visible quand noeud observable selectionne - TabEditor: edition d aretes (cliquer sur arête pour modifier style/force/signe/label) - Backend: GET /api/causal-lab/data-sources (prices/macro_series/ff_events) - analyze: auto-fetch market_watchlist+economic_events+ff_calendar depuis input_mapping - TabAnalyze: affiche inputs auto-recuperes vs manuels dans les resultats Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -500,6 +500,69 @@ def update_template(template_id: int, body: UpdateCoefRequest):
|
||||
raise HTTPException(500, str(e))
|
||||
|
||||
|
||||
@router.get("/api/causal-lab/data-sources")
|
||||
def get_data_sources():
|
||||
"""
|
||||
Retourne toutes les sources de données disponibles pour le mapping des nœuds observables :
|
||||
- prices : tickers yfinance (market_watchlist + defaults)
|
||||
- macro_series : series_id distincts depuis economic_events
|
||||
- ff_events : événements récurrents depuis ff_calendar
|
||||
"""
|
||||
PRICE_DEFAULTS = [
|
||||
{"key": "EURUSD", "label": "EUR/USD"},
|
||||
{"key": "XAUUSD", "label": "Or (Gold)"},
|
||||
{"key": "SP500", "label": "S&P 500"},
|
||||
{"key": "BRENT", "label": "Brent Crude"},
|
||||
{"key": "US2Y", "label": "US 2Y Yield"},
|
||||
{"key": "US10Y", "label": "US 10Y Yield"},
|
||||
{"key": "EU10Y", "label": "EU 10Y Yield"},
|
||||
]
|
||||
try:
|
||||
from services.database import get_conn
|
||||
conn = get_conn()
|
||||
|
||||
wl = conn.execute(
|
||||
"SELECT ticker, name FROM market_watchlist ORDER BY ticker"
|
||||
).fetchall()
|
||||
known = {x["key"] for x in PRICE_DEFAULTS}
|
||||
extra = [
|
||||
{"key": r["ticker"], "label": r["name"] or r["ticker"]}
|
||||
for r in wl if r["ticker"] not in known
|
||||
]
|
||||
|
||||
macro = conn.execute(
|
||||
"""SELECT DISTINCT series_id, event_name
|
||||
FROM economic_events
|
||||
WHERE series_id IS NOT NULL AND series_id != ''
|
||||
ORDER BY series_id LIMIT 200"""
|
||||
).fetchall()
|
||||
|
||||
ff = conn.execute(
|
||||
"""SELECT event_name, currency, COUNT(*) as cnt
|
||||
FROM ff_calendar
|
||||
WHERE event_name IS NOT NULL AND event_name != ''
|
||||
GROUP BY event_name, currency
|
||||
HAVING cnt >= 2
|
||||
ORDER BY cnt DESC LIMIT 100"""
|
||||
).fetchall()
|
||||
|
||||
conn.close()
|
||||
return {
|
||||
"prices": PRICE_DEFAULTS + extra,
|
||||
"macro_series": [
|
||||
{"key": r["series_id"], "label": r["event_name"] or r["series_id"]}
|
||||
for r in macro
|
||||
],
|
||||
"ff_events": [
|
||||
{"key": r["event_name"], "label": f"{r['event_name']} ({r['currency']})"}
|
||||
for r in ff
|
||||
],
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"[causal_lab] data_sources: {e}")
|
||||
raise HTTPException(500, str(e))
|
||||
|
||||
|
||||
@router.get("/api/causal-lab/market-events")
|
||||
def list_market_events(
|
||||
limit: int = Query(200, le=500),
|
||||
@@ -604,21 +667,94 @@ def analyze_event(body: AnalyzeRequest):
|
||||
|
||||
graph = tmpl["graph_json"]
|
||||
|
||||
# Inputs de base depuis l'événement
|
||||
# Inputs de base depuis l'événement + mappings DB/yfinance
|
||||
inputs = {}
|
||||
mapping = graph.get("input_mapping", {})
|
||||
YFINANCE_MAP = {
|
||||
"EURUSD": "EURUSD=X", "XAUUSD": "GC=F", "SP500": "^GSPC",
|
||||
"BRENT": "BZ=F", "US2Y": "US2YT=RR", "US10Y": "^TNX", "EU10Y": "GE10YT=RR",
|
||||
}
|
||||
edate_str = event["start_date"][:10]
|
||||
for input_id, cfg in mapping.items():
|
||||
src = cfg.get("source", "")
|
||||
if src == "surprise" and event.get("surprise_pct") is not None:
|
||||
src = cfg.get("source", "")
|
||||
key = cfg.get("key", "")
|
||||
field = cfg.get("field", "close")
|
||||
|
||||
if src == "market_watchlist" and key:
|
||||
try:
|
||||
import yfinance as yf
|
||||
from datetime import datetime as _dt, timedelta as _td
|
||||
event_dt = _dt.strptime(edate_str, "%Y-%m-%d")
|
||||
start = (event_dt - _td(days=5)).strftime("%Y-%m-%d")
|
||||
end = (event_dt + _td(days=2)).strftime("%Y-%m-%d")
|
||||
sym = YFINANCE_MAP.get(key, key)
|
||||
df = yf.download(sym, start=start, end=end,
|
||||
interval="1d", progress=False, auto_adjust=True)
|
||||
if df is not None and len(df) > 0:
|
||||
if hasattr(df.columns, "levels"):
|
||||
df.columns = df.columns.get_level_values(0)
|
||||
rows_yf = [
|
||||
(str(idx.date()), float(row["Close"]))
|
||||
for idx, row in df.iterrows()
|
||||
if float(row["Close"]) == float(row["Close"])
|
||||
]
|
||||
if field == "change_pct" and len(rows_yf) >= 2:
|
||||
pre = next((c for d, c in rows_yf if d < edate_str), None)
|
||||
cur = next((c for d, c in rows_yf if d == edate_str), None)
|
||||
if pre and cur and pre != 0:
|
||||
inputs[input_id] = round((cur - pre) / pre * 100, 4)
|
||||
else:
|
||||
val = next((c for d, c in rows_yf if d == edate_str), None)
|
||||
if val is None:
|
||||
val = next((c for d, c in reversed(rows_yf) if d <= edate_str), None)
|
||||
if val is not None:
|
||||
inputs[input_id] = round(val, 5)
|
||||
except Exception as _e:
|
||||
logger.debug(f"[causal_lab] market_watchlist fetch {key}: {_e}")
|
||||
|
||||
elif src == "economic_events" and key:
|
||||
row_e = conn.execute(
|
||||
"""SELECT actual_value, forecast_value FROM economic_events
|
||||
WHERE series_id = ? AND event_date <= ? AND actual_value IS NOT NULL
|
||||
ORDER BY event_date DESC LIMIT 1""",
|
||||
(key, edate_str)
|
||||
).fetchone()
|
||||
if row_e:
|
||||
if field == "surprise" and row_e["forecast_value"] is not None:
|
||||
inputs[input_id] = round(
|
||||
float(row_e["actual_value"]) - float(row_e["forecast_value"]), 4
|
||||
)
|
||||
else:
|
||||
inputs[input_id] = float(row_e["actual_value"])
|
||||
|
||||
elif src == "ff_calendar" and key:
|
||||
row_f = conn.execute(
|
||||
"""SELECT actual_value, forecast_value FROM ff_calendar
|
||||
WHERE event_name = ? AND event_date <= ? AND actual_value IS NOT NULL
|
||||
ORDER BY event_date DESC LIMIT 1""",
|
||||
(key, edate_str)
|
||||
).fetchone()
|
||||
if row_f and row_f["actual_value"]:
|
||||
try:
|
||||
actual = float(row_f["actual_value"])
|
||||
if field == "surprise" and row_f["forecast_value"]:
|
||||
inputs[input_id] = round(actual - float(row_f["forecast_value"]), 4)
|
||||
else:
|
||||
inputs[input_id] = actual
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
|
||||
# Sources legacy
|
||||
elif src == "surprise" and event.get("surprise_pct") is not None:
|
||||
inputs[input_id] = float(event["surprise_pct"])
|
||||
elif src == "impact_score_scaled" and event.get("impact_score") is not None:
|
||||
inputs[input_id] = float(event["impact_score"]) / 10.0
|
||||
elif src == "impact_score_pct" and event.get("impact_score") is not None:
|
||||
inputs[input_id] = float(event["impact_score"])
|
||||
elif src == "actual_value" and event.get("actual_value") is not None:
|
||||
elif src == "actual_value" and event.get("actual_value") is not None:
|
||||
inputs[input_id] = float(event["actual_value"])
|
||||
|
||||
# Inputs manuels (ex: tone_score depuis le frontend)
|
||||
# Inputs manuels (saisie frontend — priorité sur auto-fetch)
|
||||
inputs.update(body.inputs)
|
||||
|
||||
# Évaluation du graphe
|
||||
|
||||
Reference in New Issue
Block a user