Causal lab v2
This commit is contained in:
@@ -85,6 +85,15 @@ def startup():
|
||||
from services.geo_analyzer import GEO_PATTERNS
|
||||
from services.database import seed_builtin_patterns
|
||||
seed_builtin_patterns(GEO_PATTERNS)
|
||||
# Seed causal graph templates
|
||||
try:
|
||||
from services.database import get_conn as _get_conn
|
||||
from services.causal_graphs import init_tables as _cg_init, seed_templates as _cg_seed
|
||||
_cg_conn = _get_conn()
|
||||
_cg_init(_cg_conn); _cg_seed(_cg_conn); _cg_conn.close()
|
||||
_log.info("[Startup] Causal graph templates seeded")
|
||||
except Exception as _e:
|
||||
_log.warning(f"[Startup] Causal graph seed failed: {_e}")
|
||||
# Auto-bootstrap désactivé — utiliser les boutons dans Cycle Actions / Timeline
|
||||
# Start auto-cycle scheduler if enabled
|
||||
from services.auto_cycle import start_scheduler
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
687
backend/services/causal_graphs.py
Normal file
687
backend/services/causal_graphs.py
Normal file
@@ -0,0 +1,687 @@
|
||||
"""
|
||||
Bibliothèque de graphes causaux — templates pré-peuplés et helpers.
|
||||
|
||||
Chaque template définit une chaîne causale entre un type d'événement de marché
|
||||
et un ou plusieurs instruments (EURUSD, XAUUSD, SP500, BRENT...).
|
||||
|
||||
Structure graph_json :
|
||||
nodes[] — nœuds avec position (x,y) pour le rendu SVG
|
||||
edges[] — arêtes directionnelles entre nœuds
|
||||
coefficients — valeurs heuristiques + calibrées
|
||||
instruments[] — instruments cibles du template
|
||||
input_mapping — comment extraire les inputs depuis un market_event
|
||||
"""
|
||||
import ast
|
||||
import json
|
||||
import logging
|
||||
import operator
|
||||
from typing import Optional
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# ── Helpers de construction ────────────────────────────────────────────────────
|
||||
|
||||
def _n(id_, label, type_, x, y, formula=None, unit="", instrument=None, description=""):
|
||||
n = {"id": id_, "label": label, "type": type_, "x": x, "y": y}
|
||||
if formula: n["formula"] = formula
|
||||
if unit: n["unit"] = unit
|
||||
if instrument: n["instrument"] = instrument
|
||||
if description: n["description"] = description
|
||||
return n
|
||||
|
||||
def _e(from_, to_, style="solid", type_="causal", label=""):
|
||||
e = {"from": from_, "to": to_, "style": style, "type": type_}
|
||||
if label: e["label"] = label
|
||||
return e
|
||||
|
||||
def _c(value, description=""):
|
||||
return {"value": value, "calibrated": None, "description": description}
|
||||
|
||||
|
||||
# ── Templates pré-peuplés ─────────────────────────────────────────────────────
|
||||
|
||||
BUILT_IN_TEMPLATES = [
|
||||
|
||||
# ── 1. CPI US surprise ──────────────────────────────────────────────────────
|
||||
{
|
||||
"name": "CPI US — surprise inflationniste",
|
||||
"category": "macro_us",
|
||||
"sub_type": "CPI",
|
||||
"instruments": ["EURUSD", "XAUUSD"],
|
||||
"description": "Surprise inflationniste US → anticipations FED → taux longs → EUR/USD & Or",
|
||||
"ai_rationale": (
|
||||
"Un CPI US au-dessus du consensus renforce les anticipations de hausse FED, "
|
||||
"remonte le 10Y américain, élargit le spread US-EU et apprécie le dollar. "
|
||||
"L'or est pénalisé par la hausse des taux réels attendus."
|
||||
),
|
||||
"graph_json": {
|
||||
"nodes": [
|
||||
_n("cpi_surprise", "CPI US surprise", "input", 200, 45, unit="%"),
|
||||
_n("fed_fwd", "Signal FED anticipations", "intermediate", 200, 155, formula="cpi_surprise / 0.1 * {{coef_cpi_fwd}}"),
|
||||
_n("us_10y", "Δ US 10Y", "intermediate", 200, 265, formula="fed_fwd * {{coef_fwd_10y}}", unit="%"),
|
||||
_n("eurusd", "EUR/USD", "output", 110, 380, formula="-us_10y * {{coef_10y_eurusd}}", unit="pips", instrument="EURUSD"),
|
||||
_n("xauusd", "Or (XAU/USD)", "output", 290, 380, formula="-fed_fwd * {{coef_fwd_gold}}", unit="$/oz", instrument="XAUUSD"),
|
||||
],
|
||||
"edges": [
|
||||
_e("cpi_surprise", "fed_fwd", "dashed", "fwd_guidance", "anticipations hawkish"),
|
||||
_e("fed_fwd", "us_10y", "dashed", "expectations"),
|
||||
_e("us_10y", "eurusd", "solid", "rate_diff"),
|
||||
_e("fed_fwd", "xauusd", "dashed", "real_yield"),
|
||||
],
|
||||
"coefficients": {
|
||||
"coef_cpi_fwd": _c(0.50, "Impact de 0.1% de surprise CPI sur signal FED"),
|
||||
"coef_fwd_10y": _c(0.070, "Signal FED → Δ rendement US 10Y (%)"),
|
||||
"coef_10y_eurusd": _c(200, "1% de spread → EUR/USD (pips)"),
|
||||
"coef_fwd_gold": _c(15.0, "Unité signal FED → Or ($/oz)"),
|
||||
},
|
||||
"instruments": ["EURUSD", "XAUUSD"],
|
||||
"input_mapping": {
|
||||
"cpi_surprise": {"source": "surprise", "unit": "%", "description": "actual − consensus (%)"}
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
# ── 2. NFP surprise ─────────────────────────────────────────────────────────
|
||||
{
|
||||
"name": "NFP — surprise emploi non-agricole",
|
||||
"category": "macro_us",
|
||||
"sub_type": "NFP",
|
||||
"instruments": ["EURUSD", "SP500"],
|
||||
"description": "Surprise créations d'emploi → signal FED → 2Y + 10Y → EUR/USD & S&P",
|
||||
"ai_rationale": (
|
||||
"Un NFP supérieur aux attentes signale une économie robuste et renforce "
|
||||
"les anticipations restrictives FED. Le dollar s'apprécie via le canal des taux courts. "
|
||||
"L'impact sur le S&P500 est négatif net en contexte hawkish (taux > croissance)."
|
||||
),
|
||||
"graph_json": {
|
||||
"nodes": [
|
||||
_n("nfp_surprise", "NFP surprise", "input", 200, 45, unit="k"),
|
||||
_n("fed_fwd", "Signal FED anticipations", "intermediate", 200, 155, formula="nfp_surprise / 100 * {{coef_nfp_fwd}}"),
|
||||
_n("us_2y", "Δ US 2Y", "intermediate", 110, 265, formula="fed_fwd * {{coef_fwd_2y}}", unit="%"),
|
||||
_n("us_10y", "Δ US 10Y", "intermediate", 290, 265, formula="fed_fwd * {{coef_fwd_10y}}", unit="%"),
|
||||
_n("eurusd", "EUR/USD", "output", 110, 380, formula="-(us_2y * {{coef_2y_fx}} + us_10y * {{coef_10y_fx}})", unit="pips", instrument="EURUSD"),
|
||||
_n("sp500", "S&P 500", "output", 290, 380, formula="-fed_fwd * {{coef_fwd_sp}}", unit="pts", instrument="SP500"),
|
||||
],
|
||||
"edges": [
|
||||
_e("nfp_surprise", "fed_fwd", "dashed", "fwd_guidance"),
|
||||
_e("fed_fwd", "us_2y", "solid", "rate_channel"),
|
||||
_e("fed_fwd", "us_10y", "dashed", "expectations"),
|
||||
_e("us_2y", "eurusd", "solid", "rate_diff"),
|
||||
_e("us_10y", "eurusd", "dashed", "rate_diff"),
|
||||
_e("fed_fwd", "sp500", "dashed", "risk_asset"),
|
||||
],
|
||||
"coefficients": {
|
||||
"coef_nfp_fwd": _c(0.35, "100k NFP → signal FED"),
|
||||
"coef_fwd_2y": _c(0.030, "Signal FED → Δ US 2Y (%)"),
|
||||
"coef_fwd_10y": _c(0.070, "Signal FED → Δ US 10Y (%)"),
|
||||
"coef_2y_fx": _c(500, "1% spread 2Y → EUR/USD (pips)"),
|
||||
"coef_10y_fx": _c(200, "1% spread 10Y → EUR/USD (pips)"),
|
||||
"coef_fwd_sp": _c(20.0, "Unité signal FED hawkish → S&P500 (pts, négatif)"),
|
||||
},
|
||||
"instruments": ["EURUSD", "SP500"],
|
||||
"input_mapping": {
|
||||
"nfp_surprise": {"source": "surprise", "unit": "k", "description": "actual − consensus (milliers)"}
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
# ── 3. FOMC décision de taux ─────────────────────────────────────────────────
|
||||
{
|
||||
"name": "FOMC — décision de taux + ton",
|
||||
"category": "macro_us",
|
||||
"sub_type": "FOMC",
|
||||
"instruments": ["EURUSD", "XAUUSD", "SP500"],
|
||||
"description": "Décision FED : surprise taux (bps) + ton du communiqué → 2 canaux → FX, Or, Actions",
|
||||
"ai_rationale": (
|
||||
"La décision FOMC active deux canaux distincts : le canal taux (fait accompli → ancre 2Y) "
|
||||
"et le canal ton/forward guidance (discours → anticipe trajectoire future → 10Y). "
|
||||
"L'or réagit principalement au canal réel (taux nominaux − inflation attendue)."
|
||||
),
|
||||
"graph_json": {
|
||||
"nodes": [
|
||||
_n("rate_surprise_bps", "Surprise taux (bps)", "input", 110, 45, unit="bps", description="actual − expected en points de base"),
|
||||
_n("tone_score", "Ton du communiqué", "input", 290, 45, unit="score", description="−3 très dovish … +3 très hawkish"),
|
||||
_n("fed_rate_p", "Pression taux directeur", "intermediate", 110, 165, formula="rate_surprise_bps / 25 * {{coef_rate_mult}}"),
|
||||
_n("fed_fwd", "Signal anticipations FED", "intermediate", 290, 165, formula="tone_score * {{coef_tone}}"),
|
||||
_n("us_2y", "Δ US 2Y", "intermediate", 110, 285, formula="fed_rate_p * 0.085 + fed_fwd * 0.030", unit="%"),
|
||||
_n("us_10y", "Δ US 10Y", "intermediate", 290, 285, formula="fed_rate_p * 0.035 + fed_fwd * {{coef_fwd_10y}}", unit="%"),
|
||||
_n("eurusd", "EUR/USD", "output", 110, 400, formula="-(us_2y * 500 + us_10y * 200)", unit="pips", instrument="EURUSD"),
|
||||
_n("xauusd", "Or (XAU/USD)", "output", 290, 400, formula="-(us_10y * {{coef_10y_gold}} + fed_fwd * {{coef_tone_gold}})", unit="$/oz", instrument="XAUUSD"),
|
||||
],
|
||||
"edges": [
|
||||
_e("rate_surprise_bps", "fed_rate_p", "solid", "rate_channel"),
|
||||
_e("tone_score", "fed_fwd", "dashed", "fwd_guidance"),
|
||||
_e("fed_rate_p", "us_2y", "solid", "rate_anchor", "canal taux → 2Y"),
|
||||
_e("fed_fwd", "us_2y", "dashed", "bleed"),
|
||||
_e("fed_rate_p", "us_10y", "solid", "level_shift"),
|
||||
_e("fed_fwd", "us_10y", "dashed", "expectations", "canal ton → 10Y"),
|
||||
_e("us_2y", "eurusd", "solid", "rate_diff"),
|
||||
_e("us_10y", "eurusd", "dashed", "rate_diff"),
|
||||
_e("us_10y", "xauusd", "solid", "real_yield"),
|
||||
_e("fed_fwd", "xauusd", "dashed", "expectations"),
|
||||
],
|
||||
"coefficients": {
|
||||
"coef_rate_mult": _c(1.0, "Multiplicateur surprise taux"),
|
||||
"coef_tone": _c(1.2, "Ton → signal anticipations"),
|
||||
"coef_fwd_10y": _c(0.070,"Signal ton → Δ US 10Y (%)"),
|
||||
"coef_10y_gold": _c(30.0, "1% hausse US 10Y → Or ($/oz, négatif)"),
|
||||
"coef_tone_gold": _c(10.0, "Signal ton hawkish → Or ($/oz, négatif)"),
|
||||
},
|
||||
"instruments": ["EURUSD", "XAUUSD", "SP500"],
|
||||
"input_mapping": {
|
||||
"rate_surprise_bps": {"source": "surprise_bps", "unit": "bps"},
|
||||
"tone_score": {"source": "user_input", "range": [-3, 3]},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
# ── 4. ECB décision de taux ──────────────────────────────────────────────────
|
||||
{
|
||||
"name": "ECB — décision de taux + ton",
|
||||
"category": "macro_eu",
|
||||
"sub_type": "ECB",
|
||||
"instruments": ["EURUSD"],
|
||||
"description": "Décision BCE : surprise taux + ton → taux EU → spread → EUR/USD",
|
||||
"ai_rationale": (
|
||||
"La BCE active le même schéma dual-canal que la FED mais dans le sens inverse pour EUR/USD. "
|
||||
"Une BCE hawkish surprise élargit les spreads courts EU, réduit le spread US-EU et apprécie l'euro. "
|
||||
"L'effet sur le 10Y Bund est amplifié par le canal forward guidance."
|
||||
),
|
||||
"graph_json": {
|
||||
"nodes": [
|
||||
_n("ecb_rate_bps", "Surprise taux ECB (bps)", "input", 110, 45, unit="bps"),
|
||||
_n("ecb_tone", "Ton du communiqué BCE", "input", 290, 45, unit="score", description="−3 dovish … +3 hawkish"),
|
||||
_n("ecb_rate_p", "Pression taux BCE", "intermediate", 110, 165, formula="ecb_rate_bps / 25 * {{coef_ecb_mult}}"),
|
||||
_n("ecb_fwd", "Signal anticipations BCE", "intermediate", 290, 165, formula="ecb_tone * {{coef_ecb_tone}}"),
|
||||
_n("eu_2y", "Δ Bund 2Y", "intermediate", 110, 285, formula="ecb_rate_p * 0.080 + ecb_fwd * 0.025", unit="%"),
|
||||
_n("eu_10y", "Δ Bund 10Y", "intermediate", 290, 285, formula="ecb_rate_p * 0.030 + ecb_fwd * {{coef_ecb_fwd_10y}}", unit="%"),
|
||||
_n("eurusd", "EUR/USD", "output", 200, 400, formula="eu_2y * 500 + eu_10y * 200", unit="pips", instrument="EURUSD"),
|
||||
],
|
||||
"edges": [
|
||||
_e("ecb_rate_bps", "ecb_rate_p", "solid", "rate_channel"),
|
||||
_e("ecb_tone", "ecb_fwd", "dashed", "fwd_guidance"),
|
||||
_e("ecb_rate_p", "eu_2y", "solid", "rate_anchor"),
|
||||
_e("ecb_fwd", "eu_2y", "dashed", "bleed"),
|
||||
_e("ecb_rate_p", "eu_10y", "solid", "level_shift"),
|
||||
_e("ecb_fwd", "eu_10y", "dashed", "expectations"),
|
||||
_e("eu_2y", "eurusd", "solid", "spread_narrow"),
|
||||
_e("eu_10y", "eurusd", "dashed", "spread_narrow"),
|
||||
],
|
||||
"coefficients": {
|
||||
"coef_ecb_mult": _c(1.0, "Multiplicateur surprise taux BCE"),
|
||||
"coef_ecb_tone": _c(1.2, "Ton BCE → signal anticipations"),
|
||||
"coef_ecb_fwd_10y": _c(0.060,"Signal ton BCE → Δ Bund 10Y (%)"),
|
||||
},
|
||||
"instruments": ["EURUSD"],
|
||||
"input_mapping": {
|
||||
"ecb_rate_bps": {"source": "surprise_bps", "unit": "bps"},
|
||||
"ecb_tone": {"source": "user_input", "range": [-3, 3]},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
# ── 5. Escalade géopolitique Europe ──────────────────────────────────────────
|
||||
{
|
||||
"name": "Escalade géopolitique — Europe",
|
||||
"category": "geopolitical",
|
||||
"sub_type": "Geopolitical",
|
||||
"instruments": ["EURUSD", "XAUUSD", "BRENT"],
|
||||
"description": "Conflit / escalade en Europe → risk-off + choc énergie → EUR/USD, Or, Pétrole",
|
||||
"ai_rationale": (
|
||||
"Un événement géopolitique européen active deux canaux simultanés : "
|
||||
"l'aversion au risque (VIX → USD safe haven → EUR baisse) et le choc énergétique "
|
||||
"(disruption approvisionnement → pétrole monte → or monte, coût importations EU → EUR faiblit)."
|
||||
),
|
||||
"graph_json": {
|
||||
"nodes": [
|
||||
_n("severity", "Sévérité (1−10)", "input", 200, 45, unit="score", description="1=faible, 10=crise majeure"),
|
||||
_n("risk_aversion", "Aversion au risque", "intermediate", 110, 155, formula="severity * {{coef_sev_risk}}"),
|
||||
_n("energy_shock", "Choc énergie EU", "intermediate", 290, 155, formula="severity * {{coef_sev_energy}}"),
|
||||
_n("vix_delta", "Δ VIX", "intermediate", 110, 265, formula="risk_aversion * {{coef_risk_vix}}"),
|
||||
_n("oil_delta", "Δ Pétrole (%)", "intermediate", 290, 265, formula="energy_shock * {{coef_energy_oil}}"),
|
||||
_n("eurusd", "EUR/USD", "output", 80, 390, formula="-(vix_delta * {{coef_vix_fx}} + energy_shock * {{coef_energy_fx}})", unit="pips", instrument="EURUSD"),
|
||||
_n("xauusd", "Or (XAU/USD)", "output", 200, 390, formula="vix_delta * {{coef_vix_gold}} + oil_delta * {{coef_oil_gold}}", unit="$/oz", instrument="XAUUSD"),
|
||||
_n("brent", "Brent (USD/bbl)", "output", 320, 390, formula="oil_delta * {{coef_oil_brent}}", unit="$/bbl", instrument="BRENT"),
|
||||
],
|
||||
"edges": [
|
||||
_e("severity", "risk_aversion", "solid", "transmission", "perception risque"),
|
||||
_e("severity", "energy_shock", "solid", "supply_chain", "disruption énergie"),
|
||||
_e("risk_aversion", "vix_delta", "solid", "fear_gauge"),
|
||||
_e("energy_shock", "oil_delta", "solid", "supply_shock"),
|
||||
_e("vix_delta", "eurusd", "solid", "safe_haven", "fuite vers USD"),
|
||||
_e("energy_shock", "eurusd", "solid", "trade_balance", "coût imports EU"),
|
||||
_e("vix_delta", "xauusd", "dashed", "safe_haven", "fuite vers or"),
|
||||
_e("oil_delta", "xauusd", "dashed", "inflation_hedge"),
|
||||
_e("oil_delta", "brent", "solid", "direct"),
|
||||
],
|
||||
"coefficients": {
|
||||
"coef_sev_risk": _c(1.5, "Sévérité → aversion au risque"),
|
||||
"coef_sev_energy": _c(0.8, "Sévérité → choc énergie"),
|
||||
"coef_risk_vix": _c(3.0, "Aversion risque → Δ VIX points"),
|
||||
"coef_energy_oil": _c(2.0, "Choc énergie → % hausse pétrole"),
|
||||
"coef_vix_fx": _c(4.0, "1 pt VIX → EUR/USD pips (négatif)"),
|
||||
"coef_energy_fx": _c(8.0, "Choc énergie → EUR/USD pips (négatif)"),
|
||||
"coef_vix_gold": _c(5.0, "1 pt VIX → Or $/oz (positif)"),
|
||||
"coef_oil_gold": _c(3.0, "1% oil → Or $/oz"),
|
||||
"coef_oil_brent": _c(5.0, "Choc énergie → Brent $/bbl"),
|
||||
},
|
||||
"instruments": ["EURUSD", "XAUUSD", "BRENT"],
|
||||
"input_mapping": {
|
||||
"severity": {"source": "impact_score_scaled", "unit": "score", "range": [1, 10]}
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
# ── 6. Choc offre pétrole ────────────────────────────────────────────────────
|
||||
{
|
||||
"name": "Choc offre pétrole — OPEC / disruption",
|
||||
"category": "commodity",
|
||||
"sub_type": "Oil",
|
||||
"instruments": ["BRENT", "EURUSD", "XAUUSD"],
|
||||
"description": "Choc d'offre pétrolière → prix → inflation EU → trade balance → EUR",
|
||||
"ai_rationale": (
|
||||
"Un choc d'offre haussier sur le pétrole impacte directement le Brent. "
|
||||
"L'Europe, importateur net, voit ses coûts d'importation augmenter (trade balance → EUR négatif), "
|
||||
"et ses anticipations d'inflation croître (BCE contrainte entre lutte anti-inflation et croissance)."
|
||||
),
|
||||
"graph_json": {
|
||||
"nodes": [
|
||||
_n("oil_change_pct", "Choc prix pétrole (%)", "input", 200, 45, unit="%"),
|
||||
_n("brent_move", "Δ Brent ($/bbl)", "intermediate", 110, 155, formula="oil_change_pct * {{coef_oil_brent}}"),
|
||||
_n("eu_import_cost", "Coût imports EU", "intermediate", 290, 155, formula="oil_change_pct * {{coef_oil_import}}"),
|
||||
_n("eu_inflation", "Anticipations inflation EU","intermediate",290, 265, formula="eu_import_cost * {{coef_import_inf}}"),
|
||||
_n("eurusd", "EUR/USD", "output", 110, 380, formula="-(eu_import_cost * {{coef_import_fx}} + eu_inflation * {{coef_inf_fx}})", unit="pips", instrument="EURUSD"),
|
||||
_n("xauusd", "Or (XAU/USD)", "output", 290, 380, formula="brent_move * {{coef_oil_gold}}", unit="$/oz", instrument="XAUUSD"),
|
||||
],
|
||||
"edges": [
|
||||
_e("oil_change_pct", "brent_move", "solid", "direct"),
|
||||
_e("oil_change_pct", "eu_import_cost","solid", "trade_balance"),
|
||||
_e("eu_import_cost", "eu_inflation", "dashed", "cost_push"),
|
||||
_e("eu_import_cost", "eurusd", "solid", "trade_balance", "déficit commercial EU"),
|
||||
_e("eu_inflation", "eurusd", "dashed", "policy_ambiguity","BCE coincé"),
|
||||
_e("brent_move", "xauusd", "dashed", "inflation_hedge"),
|
||||
],
|
||||
"coefficients": {
|
||||
"coef_oil_brent": _c(1.0, "% hausse oil → Δ Brent $/bbl (linéaire à calibrer)"),
|
||||
"coef_oil_import": _c(0.5, "% hausse oil → pression imports EU (score)"),
|
||||
"coef_import_inf": _c(0.3, "Pression imports → anticipations inflation EU"),
|
||||
"coef_import_fx": _c(10.0, "Pression imports → EUR/USD pips (négatif)"),
|
||||
"coef_inf_fx": _c(5.0, "Inflation EU → EUR/USD (ambiguë, négatif net)"),
|
||||
"coef_oil_gold": _c(0.8, "1$/bbl Brent → Or $/oz"),
|
||||
},
|
||||
"instruments": ["BRENT", "EURUSD", "XAUUSD"],
|
||||
"input_mapping": {
|
||||
"oil_change_pct": {"source": "impact_score_pct", "unit": "%"}
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
# ── 7. COT repositionnement institutionnel ───────────────────────────────────
|
||||
{
|
||||
"name": "COT — repositionnement institutionnel EUR",
|
||||
"category": "report",
|
||||
"sub_type": "COT",
|
||||
"instruments": ["EURUSD"],
|
||||
"description": "Variation nette des positions institutionnelles EUR → signal momentum → EUR/USD",
|
||||
"ai_rationale": (
|
||||
"Le rapport COT (Commitment of Traders) révèle les positions nettes des fonds spéculatifs sur EUR. "
|
||||
"Un changement significatif des positions nettes agit comme signal de momentum, "
|
||||
"les marchés interprétant un repositionnement institutionnel comme un signal directionnel."
|
||||
),
|
||||
"graph_json": {
|
||||
"nodes": [
|
||||
_n("net_longs_delta", "Δ Positions nettes EUR (k contrats)", "input", 200, 45, unit="k contracts"),
|
||||
_n("momentum_signal", "Signal momentum institutionnel", "intermediate", 200, 185, formula="net_longs_delta * {{coef_cot_momentum}}"),
|
||||
_n("eurusd", "EUR/USD", "output", 200, 330, formula="momentum_signal * {{coef_momentum_fx}}", unit="pips", instrument="EURUSD"),
|
||||
],
|
||||
"edges": [
|
||||
_e("net_longs_delta", "momentum_signal", "solid", "positioning"),
|
||||
_e("momentum_signal", "eurusd", "solid", "momentum"),
|
||||
],
|
||||
"coefficients": {
|
||||
"coef_cot_momentum": _c(0.5, "k contrats → signal momentum"),
|
||||
"coef_momentum_fx": _c(2.0, "Signal momentum → EUR/USD pips"),
|
||||
},
|
||||
"instruments": ["EURUSD"],
|
||||
"input_mapping": {
|
||||
"net_longs_delta": {"source": "actual_value", "unit": "k contracts"}
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
# ── 8. EIA stocks pétrole ────────────────────────────────────────────────────
|
||||
{
|
||||
"name": "EIA — stocks pétroliers hebdomadaires",
|
||||
"category": "report",
|
||||
"sub_type": "EIA",
|
||||
"instruments": ["BRENT", "EURUSD"],
|
||||
"description": "Surprise stocks EIA → prix pétrole → canal inflation / risk appetite → EUR/USD",
|
||||
"ai_rationale": (
|
||||
"Une surprise négative sur les stocks EIA (stocks < consensus) signale une demande forte "
|
||||
"ou une offre réduite, ce qui fait monter le pétrole. Impact EUR/USD indirect via "
|
||||
"le canal inflation EU (coût imports) et le risk appetite."
|
||||
),
|
||||
"graph_json": {
|
||||
"nodes": [
|
||||
_n("eia_surprise_mb", "Surprise EIA (Mb vs consensus)", "input", 200, 45, unit="Mb", description="négatif = stocks inférieurs aux attentes"),
|
||||
_n("oil_reaction", "Réaction pétrole (%)", "intermediate", 200, 165, formula="-eia_surprise_mb * {{coef_eia_oil}}"),
|
||||
_n("brent", "Δ Brent ($/bbl)", "output", 110, 290, formula="oil_reaction * {{coef_oil_dollar}}", unit="$/bbl", instrument="BRENT"),
|
||||
_n("eurusd", "EUR/USD", "output", 290, 290, formula="-oil_reaction * {{coef_oil_eurusd}}", unit="pips", instrument="EURUSD"),
|
||||
],
|
||||
"edges": [
|
||||
_e("eia_surprise_mb", "oil_reaction", "solid", "supply_demand", "stocks bas → oil monte"),
|
||||
_e("oil_reaction", "brent", "solid", "direct"),
|
||||
_e("oil_reaction", "eurusd", "dashed", "trade_balance", "coût imports EU"),
|
||||
],
|
||||
"coefficients": {
|
||||
"coef_eia_oil": _c(0.5, "1 Mb sous consensus → % hausse pétrole"),
|
||||
"coef_oil_dollar": _c(1.0, "% hausse oil → Δ Brent $/bbl"),
|
||||
"coef_oil_eurusd": _c(3.0, "% hausse oil → EUR/USD pips (négatif pour EUR)"),
|
||||
},
|
||||
"instruments": ["BRENT", "EURUSD"],
|
||||
"input_mapping": {
|
||||
"eia_surprise_mb": {"source": "surprise", "unit": "Mb"}
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
# ── 9. Risk-off global ───────────────────────────────────────────────────────
|
||||
{
|
||||
"name": "Risk-off global — fuite vers la sécurité",
|
||||
"category": "sentiment",
|
||||
"sub_type": "Risk",
|
||||
"instruments": ["EURUSD", "XAUUSD", "SP500"],
|
||||
"description": "Choc d'aversion au risque → flight-to-safety → USD & Or montent, actions baissent",
|
||||
"ai_rationale": (
|
||||
"Un épisode de risk-off global (crash, crise financière, choc exogène) provoque "
|
||||
"une fuite vers les actifs refuge : USD (liquidity premium), Or (store of value), "
|
||||
"et une sortie des actifs risqués (actions, EM, EUR). Le VIX s'envole et amplifie la réaction."
|
||||
),
|
||||
"graph_json": {
|
||||
"nodes": [
|
||||
_n("risk_score", "Score risk-off (1−10)", "input", 200, 45, unit="score", description="1=légère tension, 10=crise majeure"),
|
||||
_n("vix_spike", "Δ VIX", "intermediate", 110, 165, formula="risk_score * {{coef_risk_vix}}"),
|
||||
_n("gold_demand", "Demande actifs refuge", "intermediate", 290, 165, formula="risk_score * {{coef_risk_refuge}}"),
|
||||
_n("usd_safe", "USD safe-haven", "intermediate", 110, 285, formula="vix_spike * {{coef_vix_usd}}"),
|
||||
_n("eurusd", "EUR/USD", "output", 80, 400, formula="-usd_safe * {{coef_usd_eurusd}}", unit="pips", instrument="EURUSD"),
|
||||
_n("xauusd", "Or (XAU/USD)", "output", 200, 400, formula="gold_demand * {{coef_refuge_gold}}", unit="$/oz", instrument="XAUUSD"),
|
||||
_n("sp500", "S&P 500", "output", 320, 400, formula="-vix_spike * {{coef_vix_sp}}", unit="pts", instrument="SP500"),
|
||||
],
|
||||
"edges": [
|
||||
_e("risk_score", "vix_spike", "solid", "fear_gauge"),
|
||||
_e("risk_score", "gold_demand", "solid", "safe_haven"),
|
||||
_e("vix_spike", "usd_safe", "solid", "liquidity", "fuite liquidité USD"),
|
||||
_e("usd_safe", "eurusd", "solid", "fx_safe_haven"),
|
||||
_e("gold_demand", "xauusd", "solid", "store_value"),
|
||||
_e("vix_spike", "sp500", "solid", "risk_asset", "risk premium"),
|
||||
],
|
||||
"coefficients": {
|
||||
"coef_risk_vix": _c(4.0, "Score risk → Δ VIX pts"),
|
||||
"coef_risk_refuge": _c(2.0, "Score risk → demande refuges (score)"),
|
||||
"coef_vix_usd": _c(0.8, "Δ VIX → signal USD safe-haven"),
|
||||
"coef_usd_eurusd": _c(20.0, "Signal USD → EUR/USD pips (négatif)"),
|
||||
"coef_refuge_gold": _c(15.0, "Demande refuge → Or $/oz"),
|
||||
"coef_vix_sp": _c(12.0, "1 pt VIX → S&P 500 pts (négatif)"),
|
||||
},
|
||||
"instruments": ["EURUSD", "XAUUSD", "SP500"],
|
||||
"input_mapping": {
|
||||
"risk_score": {"source": "impact_score_scaled", "unit": "score", "range": [1, 10]}
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
# ── 10. Tensions commerciales / tarifs ──────────────────────────────────────
|
||||
{
|
||||
"name": "Tarifs douaniers — tensions commerciales",
|
||||
"category": "geopolitical",
|
||||
"sub_type": "Trade",
|
||||
"instruments": ["EURUSD", "SP500"],
|
||||
"description": "Annonce tarifs → choc exportations EU + risk-off → EUR/USD & actions",
|
||||
"ai_rationale": (
|
||||
"Les tarifs douaniers impactent l'EUR/USD via deux canaux : "
|
||||
"le choc direct sur les exportations européennes (BCE plus dovish = EUR baisse) "
|
||||
"et le canal risk-off global (incertitude → fuite USD → EUR baisse, S&P corrige)."
|
||||
),
|
||||
"graph_json": {
|
||||
"nodes": [
|
||||
_n("tariff_pct", "Tarifs annoncés (%)", "input", 200, 45, unit="%"),
|
||||
_n("eu_export_shock","Choc exportations EU", "intermediate", 110, 165, formula="tariff_pct * {{coef_tariff_export}}"),
|
||||
_n("risk_off_signal","Signal risk-off", "intermediate", 290, 165, formula="tariff_pct * {{coef_tariff_risk}}"),
|
||||
_n("ecb_dovish", "Pression BCE dovish", "intermediate", 110, 285, formula="eu_export_shock * {{coef_export_ecb}}"),
|
||||
_n("eurusd", "EUR/USD", "output", 110, 400, formula="-(ecb_dovish * {{coef_ecb_fx}} + risk_off_signal * {{coef_risk_fx}})", unit="pips", instrument="EURUSD"),
|
||||
_n("sp500", "S&P 500", "output", 290, 400, formula="-risk_off_signal * {{coef_risk_sp}}", unit="pts", instrument="SP500"),
|
||||
],
|
||||
"edges": [
|
||||
_e("tariff_pct", "eu_export_shock", "solid", "trade_impact"),
|
||||
_e("tariff_pct", "risk_off_signal", "dashed", "uncertainty"),
|
||||
_e("eu_export_shock", "ecb_dovish", "solid", "growth_slowdown"),
|
||||
_e("ecb_dovish", "eurusd", "solid", "rate_differential"),
|
||||
_e("risk_off_signal", "eurusd", "dashed", "safe_haven"),
|
||||
_e("risk_off_signal", "sp500", "solid", "risk_asset"),
|
||||
],
|
||||
"coefficients": {
|
||||
"coef_tariff_export": _c(1.5, "% tarif → pression exports EU"),
|
||||
"coef_tariff_risk": _c(1.0, "% tarif → signal risk-off"),
|
||||
"coef_export_ecb": _c(0.5, "Choc exports → pression dovish BCE"),
|
||||
"coef_ecb_fx": _c(15.0, "Pression BCE dovish → EUR/USD pips (négatif)"),
|
||||
"coef_risk_fx": _c(10.0, "Risk-off → EUR/USD pips (négatif)"),
|
||||
"coef_risk_sp": _c(25.0, "Risk-off → S&P 500 pts (négatif)"),
|
||||
},
|
||||
"instruments": ["EURUSD", "SP500"],
|
||||
"input_mapping": {
|
||||
"tariff_pct": {"source": "impact_score_pct", "unit": "%"}
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
# ── DB Helpers ─────────────────────────────────────────────────────────────────
|
||||
|
||||
def init_tables(conn):
|
||||
conn.execute("""
|
||||
CREATE TABLE IF NOT EXISTS causal_graph_templates (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL UNIQUE,
|
||||
category TEXT NOT NULL,
|
||||
sub_type TEXT,
|
||||
instruments TEXT NOT NULL DEFAULT '[]',
|
||||
description TEXT DEFAULT '',
|
||||
graph_json TEXT NOT NULL,
|
||||
ai_rationale TEXT DEFAULT '',
|
||||
calibration_json TEXT DEFAULT '{}',
|
||||
created_by TEXT DEFAULT 'system',
|
||||
heuristic_ver INTEGER DEFAULT 1,
|
||||
created_at TEXT DEFAULT (datetime('now')),
|
||||
updated_at TEXT DEFAULT (datetime('now'))
|
||||
)
|
||||
""")
|
||||
conn.execute("""
|
||||
CREATE TABLE IF NOT EXISTS causal_event_analyses (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
market_event_id INTEGER,
|
||||
template_id INTEGER,
|
||||
instrument TEXT NOT NULL DEFAULT 'EURUSD',
|
||||
inputs_json TEXT DEFAULT '{}',
|
||||
override_params TEXT DEFAULT '{}',
|
||||
prediction_json TEXT,
|
||||
actual_json TEXT,
|
||||
activation_score REAL,
|
||||
drift_json TEXT,
|
||||
ai_recommendation TEXT,
|
||||
analyzed_at TEXT DEFAULT (datetime('now')),
|
||||
FOREIGN KEY (template_id) REFERENCES causal_graph_templates(id)
|
||||
)
|
||||
""")
|
||||
conn.commit()
|
||||
|
||||
|
||||
def seed_templates(conn):
|
||||
"""Insère les templates built-in s'ils n'existent pas encore."""
|
||||
for t in BUILT_IN_TEMPLATES:
|
||||
existing = conn.execute(
|
||||
"SELECT id FROM causal_graph_templates WHERE name = ?", (t["name"],)
|
||||
).fetchone()
|
||||
if existing:
|
||||
continue
|
||||
conn.execute("""
|
||||
INSERT INTO causal_graph_templates
|
||||
(name, category, sub_type, instruments, description, graph_json, ai_rationale, created_by)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, 'system')
|
||||
""", (
|
||||
t["name"],
|
||||
t["category"],
|
||||
t.get("sub_type", ""),
|
||||
json.dumps(t.get("instruments", [])),
|
||||
t.get("description", ""),
|
||||
json.dumps(t["graph_json"]),
|
||||
t.get("ai_rationale", ""),
|
||||
))
|
||||
conn.commit()
|
||||
|
||||
|
||||
def get_templates(conn, category: str = "") -> list:
|
||||
where = f"WHERE category = '{category}'" if category else ""
|
||||
rows = conn.execute(
|
||||
f"SELECT * FROM causal_graph_templates {where} ORDER BY category, name"
|
||||
).fetchall()
|
||||
result = []
|
||||
for r in rows:
|
||||
d = dict(r)
|
||||
d["graph_json"] = json.loads(d["graph_json"] or "{}")
|
||||
d["instruments"] = json.loads(d["instruments"] or "[]")
|
||||
d["calibration_json"] = json.loads(d["calibration_json"] or "{}")
|
||||
result.append(d)
|
||||
return result
|
||||
|
||||
|
||||
def get_template(conn, template_id: int) -> Optional[dict]:
|
||||
row = conn.execute(
|
||||
"SELECT * FROM causal_graph_templates WHERE id = ?", (template_id,)
|
||||
).fetchone()
|
||||
if not row:
|
||||
return None
|
||||
d = dict(row)
|
||||
d["graph_json"] = json.loads(d["graph_json"] or "{}")
|
||||
d["instruments"] = json.loads(d["instruments"] or "[]")
|
||||
d["calibration_json"] = json.loads(d["calibration_json"] or "{}")
|
||||
return d
|
||||
|
||||
|
||||
def update_coefficients(conn, template_id: int, coef_updates: dict):
|
||||
tmpl = get_template(conn, template_id)
|
||||
if not tmpl:
|
||||
return False
|
||||
graph = tmpl["graph_json"]
|
||||
for k, v in coef_updates.items():
|
||||
if k in graph.get("coefficients", {}):
|
||||
graph["coefficients"][k]["value"] = float(v)
|
||||
conn.execute(
|
||||
"UPDATE causal_graph_templates SET graph_json = ?, updated_at = datetime('now') WHERE id = ?",
|
||||
(json.dumps(graph), template_id)
|
||||
)
|
||||
conn.commit()
|
||||
return True
|
||||
|
||||
|
||||
def update_calibration(conn, template_id: int, analysis_result: dict):
|
||||
"""Met à jour les stats de calibration après une nouvelle analyse."""
|
||||
tmpl = get_template(conn, template_id)
|
||||
if not tmpl:
|
||||
return
|
||||
calib = tmpl.get("calibration_json") or {}
|
||||
n = calib.get("n_events", 0) + 1
|
||||
# Running average of activation score
|
||||
prev_act = calib.get("avg_activation", 0) or 0
|
||||
new_act = analysis_result.get("activation_score") or 0
|
||||
# Running average of pred / actual pips
|
||||
instrument = analysis_result.get("instrument", "EURUSD")
|
||||
pred_pips = analysis_result.get("pred_pips", 0) or 0
|
||||
act_pips = analysis_result.get("actual_pips", 0) or 0
|
||||
|
||||
calib["n_events"] = n
|
||||
calib["avg_activation"] = round((prev_act * (n - 1) + new_act) / n, 3)
|
||||
calib["last_analyzed"] = analysis_result.get("analyzed_at", "")
|
||||
|
||||
if instrument not in calib:
|
||||
calib[instrument] = {"n": 0, "avg_pred": 0, "avg_actual": 0}
|
||||
ci = calib[instrument]
|
||||
ni = ci["n"] + 1
|
||||
ci["n"] = ni
|
||||
ci["avg_pred"] = round((ci["avg_pred"] * (ni - 1) + pred_pips) / ni, 1)
|
||||
ci["avg_actual"] = round((ci["avg_actual"] * (ni - 1) + act_pips) / ni, 1)
|
||||
if ci["avg_pred"] != 0:
|
||||
ci["coef_ratio"] = round(ci["avg_actual"] / ci["avg_pred"], 2)
|
||||
|
||||
conn.execute(
|
||||
"UPDATE causal_graph_templates SET calibration_json = ?, updated_at = datetime('now') WHERE id = ?",
|
||||
(json.dumps(calib), template_id)
|
||||
)
|
||||
conn.commit()
|
||||
|
||||
|
||||
# ── Évaluateur de graphe ───────────────────────────────────────────────────────
|
||||
|
||||
def _safe_eval(expr: str, context: dict) -> float:
|
||||
"""Évalue une expression arithmétique simple avec des variables."""
|
||||
_ops = {
|
||||
ast.Add: operator.add,
|
||||
ast.Sub: operator.sub,
|
||||
ast.Mult: operator.mul,
|
||||
ast.Div: operator.truediv,
|
||||
ast.USub: operator.neg,
|
||||
}
|
||||
|
||||
def _eval(node):
|
||||
if isinstance(node, ast.Constant):
|
||||
return float(node.value)
|
||||
if isinstance(node, ast.Name):
|
||||
if node.id not in context:
|
||||
raise ValueError(f"Variable inconnue : {node.id}")
|
||||
return float(context[node.id])
|
||||
if isinstance(node, ast.BinOp):
|
||||
op = _ops.get(type(node.op))
|
||||
if op is None:
|
||||
raise ValueError(f"Opérateur non supporté : {type(node.op)}")
|
||||
return op(_eval(node.left), _eval(node.right))
|
||||
if isinstance(node, ast.UnaryOp) and isinstance(node.op, ast.USub):
|
||||
return -_eval(node.operand)
|
||||
raise ValueError(f"Nœud AST non supporté : {type(node)}")
|
||||
|
||||
tree = ast.parse(expr.strip(), mode="eval")
|
||||
return _eval(tree.body)
|
||||
|
||||
|
||||
def evaluate_graph(graph_json: dict, inputs: dict, coef_overrides: Optional[dict] = None) -> dict:
|
||||
"""
|
||||
Évalue un graphe causal depuis les inputs, retourne dict {node_id: valeur}.
|
||||
Les {{coef_xxx}} sont substitués depuis graph_json["coefficients"] (ou overrides).
|
||||
"""
|
||||
coefs = {k: v["value"] for k, v in graph_json.get("coefficients", {}).items()}
|
||||
if coef_overrides:
|
||||
coefs.update({k: float(v) for k, v in coef_overrides.items()})
|
||||
|
||||
def sub(formula: str) -> str:
|
||||
for k, v in coefs.items():
|
||||
formula = formula.replace(f"{{{{{k}}}}}", str(v))
|
||||
return formula
|
||||
|
||||
values = dict(inputs)
|
||||
nodes = graph_json.get("nodes", [])
|
||||
# Évaluation en ordre topologique (max N passes)
|
||||
for _ in range(len(nodes) + 2):
|
||||
changed = False
|
||||
for node in nodes:
|
||||
if node["id"] in values or not node.get("formula"):
|
||||
continue
|
||||
try:
|
||||
result = _safe_eval(sub(node["formula"]), values)
|
||||
values[node["id"]] = round(result, 4)
|
||||
changed = True
|
||||
except Exception:
|
||||
pass
|
||||
if not changed:
|
||||
break
|
||||
|
||||
return values
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user