feat: 50-signal macro engine — vol surface, sectors, EM, carry, long bonds

data_fetcher.py
- MACRO_GAUGE_CONFIG: 15 → 29 tickers (+silver, vvix, skew, ovx, gvz,
  usdjpy, xlk, xlf, xlp, xlu, eem, emb, fxi, tlt)
- 5 new derived metrics: silver_gold_ratio, xlk_xlp_momentum, xlf_spx_ratio,
  eem_spx_ratio, vol_surface_regime (composite classification)
- ThreadPoolExecutor max_workers raised to 20
- score_macro_scenarios: +15 new variables; each of 8 scenarios enriched
  with vol-surface (SKEW, VVIX), sector rotation (XLK, XLF, XLP, XLU),
  EM/carry (EEM, EMB, USDJPY), long bonds (TLT), silver signals

ai_analyzer.py
- macro_ctx: 5 → 21 fields per pattern (vol surface, sectors, EM, carry,
  long bonds, silver/gold ratio — all with interpretation comments)
- macro_section in scoring prompt: describes surface de vol regime, sector
  rotation, global/carry signals with explicit GPT instructions for pilier 3e
- DEFAULT_ANALYSIS_TEMPLATE: pilier 3e expanded with SKEW/VVIX/OVX/GVZ guidance

SIGNALS_FUTURES.md: reference document listing 30+ signals not yet
available (FRED, CFTC COT, EIA, Baltic Dry, LME, credit spreads,
hedge fund positioning, central bank balance sheets) with implementation
priority and cost estimate.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
OpenSquared
2026-06-19 08:34:48 +02:00
parent 18b3ae6f91
commit d8c0334feb
3 changed files with 312 additions and 9 deletions

View File

@@ -292,21 +292,44 @@ def get_economic_calendar() -> List[Dict[str, Any]]:
MACRO_GAUGE_CONFIG = [
# (id, label, ticker, unit, bloc)
# ── Liquidité / Taux ─────────────────────────────────────────────────────
("dxy", "Dollar DXY", "DX-Y.NYB", "index", "liquidite"),
("us10y", "UST 10Y", "^TNX", "%", "liquidite"),
("us3m", "UST 3M", "^IRX", "%", "liquidite"),
("tips", "TIPS ETF", "TIP", "$", "liquidite"),
("tlt", "Obligations 20Y+ (TLT)", "TLT", "$", "liquidite"),
# ── Crédit ───────────────────────────────────────────────────────────────
("vix", "VIX", "^VIX", "pts", "credit"),
("hyg", "HY Bonds (HYG)", "HYG", "$", "credit"),
("lqd", "IG Bonds (LQD)", "LQD", "$", "credit"),
("ief", "Trésor 7-10Y (IEF)", "IEF", "$", "credit"),
# ── Énergie ──────────────────────────────────────────────────────────────
("brent", "Brent", "BZ=F", "$", "energie"),
("ng", "Gaz naturel", "NG=F", "$", "energie"),
# ── Métaux ───────────────────────────────────────────────────────────────
("gold", "Or", "GC=F", "$", "metaux"),
("silver", "Argent (Silver)", "SI=F", "$/oz", "metaux"),
("copper", "Cuivre", "HG=F", "$/lb", "metaux"),
# ── Croissance US ─────────────────────────────────────────────────────────
("spx", "S&P 500", "^GSPC", "pts", "croissance"),
("iwm", "Russell 2000", "IWM", "$", "croissance"),
("xli", "Industriels XLI", "XLI", "$", "croissance"),
# ── Secteurs US (rotation) ────────────────────────────────────────────────
("xlk", "Tech (XLK)", "XLK", "$", "secteurs"),
("xlf", "Financières (XLF)", "XLF", "$", "secteurs"),
("xlp", "Conso. défensif (XLP)", "XLP", "$", "secteurs"),
("xlu", "Utilities (XLU)", "XLU", "$", "secteurs"),
# ── Volatilité de surface ─────────────────────────────────────────────────
("vvix", "VVIX (vol-of-vol)", "^VVIX", "pts", "volatilite"),
("skew", "CBOE SKEW", "^SKEW", "pts", "volatilite"),
("ovx", "Pétrole Vol (OVX)", "^OVX", "pts", "volatilite"),
("gvz", "Or Vol (GVZ)", "^GVZ", "pts", "volatilite"),
# ── Global / EM ───────────────────────────────────────────────────────────
("eem", "EM Actions (EEM)", "EEM", "$", "global"),
("emb", "EM Bonds (EMB)", "EMB", "$", "global"),
("fxi", "Chine Large Cap (FXI)", "FXI", "$", "global"),
# ── Forex risk-off ────────────────────────────────────────────────────────
("usdjpy", "USD/JPY", "USDJPY=X", "pts", "forex_ro"),
]
SCENARIO_META = {
@@ -337,7 +360,7 @@ def get_macro_gauges() -> Dict[str, Any]:
from concurrent.futures import ThreadPoolExecutor, as_completed
raw: Dict[str, Any] = {}
with ThreadPoolExecutor(max_workers=min(len(MACRO_GAUGE_CONFIG), 12)) as exe:
with ThreadPoolExecutor(max_workers=min(len(MACRO_GAUGE_CONFIG), 20)) as exe:
futures = {
exe.submit(get_quote, ticker): (gid, label, ticker, unit, bloc)
for gid, label, ticker, unit, bloc in MACRO_GAUGE_CONFIG
@@ -414,6 +437,72 @@ def get_macro_gauges() -> Dict[str, Any]:
else ("parité" if rel_perf > -0.2 else "large caps dominants (défensif)")),
}
# Derived: Silver / Gold ratio (Silver oz / Gold oz — proxy risk-on metals)
# <0.012 = or surperforme = risk-off; >0.016 = argent surperforme = risk-on industrie
sv = raw.get("silver", {}).get("value")
gv_ = raw.get("gold", {}).get("value")
sgr = round(sv / gv_, 5) if (sv and gv_) else None
raw["silver_gold_ratio"] = {
"id": "silver_gold_ratio", "label": "Ratio Argent/Or",
"ticker": None, "value": sgr, "change_pct": None, "unit": "ratio", "bloc": "derive",
"note": ("argent surperforme (risk-on industriel)" if sgr and sgr > 0.016
else ("neutre" if sgr and sgr > 0.012 else ("or surperforme (risk-off)" if sgr else None))),
}
# Derived: Tech vs Consumer Staples relative performance (XLK vs XLP)
# Positive = tech > défensifs = risk-on; negative = rotation défensive = ralentissement
xlk_c = raw.get("xlk", {}).get("change_pct") or 0.0
xlp_c = raw.get("xlp", {}).get("change_pct") or 0.0
tech_vs_staples = round(xlk_c - xlp_c, 2)
raw["xlk_xlp_momentum"] = {
"id": "xlk_xlp_momentum", "label": "Tech vs Défensifs (XLK-XLP)",
"ticker": None, "value": tech_vs_staples, "change_pct": None, "unit": "pts%", "bloc": "derive",
"note": ("tech > défensifs (risk-on fort)" if tech_vs_staples > 0.5
else ("parité" if tech_vs_staples > -0.5 else "rotation défensive ⚠️")),
}
# Derived: Financials vs S&P relative performance (XLF vs SPX)
# XLF is a leading indicator — underperformance signals credit/economic stress ahead
xlf_c = raw.get("xlf", {}).get("change_pct") or 0.0
xlf_vs_spx = round(xlf_c - spx_c_val, 2)
raw["xlf_spx_ratio"] = {
"id": "xlf_spx_ratio", "label": "Financières vs S&P (XLF-SPX)",
"ticker": None, "value": xlf_vs_spx, "change_pct": None, "unit": "pts%", "bloc": "derive",
"note": ("banques > marché (expansion crédit)" if xlf_vs_spx > 0.3
else ("parité" if xlf_vs_spx > -0.3 else "banques < marché ⚠️ (stress crédit)")),
}
# Derived: EM vs US equity relative performance (EEM vs SPX)
# Positive = global risk-on; negative = fuite vers US (dollar strength, EM stress)
eem_c = raw.get("eem", {}).get("change_pct") or 0.0
eem_vs_spx = round(eem_c - spx_c_val, 2)
raw["eem_spx_ratio"] = {
"id": "eem_spx_ratio", "label": "EM vs S&P (EEM-SPX)",
"ticker": None, "value": eem_vs_spx, "change_pct": None, "unit": "pts%", "bloc": "derive",
"note": ("EM > US (croissance globale)" if eem_vs_spx > 0.3
else ("parité" if eem_vs_spx > -0.5 else "fuite vers US ⚠️ (EM stress/dollar fort)")),
}
# Derived: Vol surface regime — composite classification from VIX + VVIX + SKEW
vix_v = raw.get("vix", {}).get("value") or 20.0
vvix_v = raw.get("vvix", {}).get("value") or 85.0
skew_v = raw.get("skew", {}).get("value") or 115.0
if vix_v < 15 and vvix_v < 90 and skew_v < 120:
vol_regime = "contango_calm" # ideal pour vendre vol ou spreads bon marché
elif vix_v > 30 or vvix_v > 110:
vol_regime = "backwardation_panic" # stress aigu — options chères, spreads larges
elif skew_v > 140 and vix_v > 20:
vol_regime = "tail_risk_elevated" # marché achète protection queue = méfiance
elif vix_v < 20 and skew_v > 130:
vol_regime = "complacency_hedged" # calme apparent mais queues protégées
else:
vol_regime = "normal"
raw["vol_surface_regime"] = {
"id": "vol_surface_regime", "label": "Régime Surface de Vol",
"ticker": None, "value": None, "change_pct": None, "unit": "regime", "bloc": "derive",
"note": vol_regime,
}
return _sanitize_floats(raw)
@@ -449,6 +538,22 @@ def score_macro_scenarios(gauges: Dict[str, Any]) -> Dict[str, Any]:
iwm_c = gc("iwm")
xli_c = gc("xli")
rel_perf = gv("iwm_spx_ratio") or 0.0 # Russell vs S&P relative perf
# ── Nouveaux signaux (phase 2 — 50 compteurs) ────────────────────────────
skew_v = gv("skew") or 115.0 # CBOE SKEW: normal ~115, élevé >130, extrême >145
vvix_v = gv("vvix") or 85.0 # Vol-of-vol: normal ~85, élevé >100, panique >115
ovx_v = gv("ovx") or 25.0 # Oil vol implicite
gvz_v = gv("gvz") or 17.0 # Gold vol implicite
tlt_c = gc("tlt") # Long bonds 20Y+ (hausse = flight to quality)
xlk_c = gc("xlk") # Tech (hausse = risk-on sectoriel)
xlf_c = gc("xlf") # Financières (baisse = stress crédit)
xlp_c = gc("xlp") # Conso. défensif (hausse = rotation défensive)
xlu_c = gc("xlu") # Utilities (hausse = rotation défensive)
eem_c = gc("eem") # EM actions (hausse = croissance globale)
emb_c = gc("emb") # EM bonds (baisse = crise liquidité EM)
fxi_c = gc("fxi") # Chine equities
usdjpy_c = gc("usdjpy") # USD/JPY (baisse = JPY s'apprécie = risk-off/panique carry)
silver_c = gc("silver") # Argent (surperf or = risk-on industriel)
tech_vs_staples = gv("xlk_xlp_momentum") or 0.0
scores: Dict[str, int] = {}
reasons: Dict[str, List[str]] = {}
@@ -470,6 +575,12 @@ def score_macro_scenarios(gauges: Dict[str, Any]) -> Dict[str, Any]:
if vs200 > 5: s += 15; r.append(f"S&P+{vs200}% vs 200j")
elif vs200 > 0: s += 7
if copper_c > 0.5: s += 10; r.append("Cuivre↑")
# Signaux phase 2
if skew_v < 115: s += 6; r.append(f"SKEW {skew_v:.0f} (no tail hedge = complacency)")
if vvix_v < 85: s += 5; r.append(f"VVIX {vvix_v:.0f} (vol stable)")
if tech_vs_staples > 0.5: s += 7; r.append("Tech > Défensifs (risk-on sectoriel)")
if eem_c > 0.3: s += 6; r.append("EM↑ (croissance globale)")
if usdjpy_c > 0.2: s += 4; r.append("JPY↓ (carry actif = risk-on)")
scores["goldilocks"] = min(100, s); reasons["goldilocks"] = r
# DÉSINFLATION / BAISSE DE TAUX
@@ -483,6 +594,10 @@ def score_macro_scenarios(gauges: Dict[str, Any]) -> Dict[str, Any]:
if vs200 is not None and vs200 > 0: s += 20; r.append("S&P au-dessus 200j")
if hyg_c > 0: s += 10; r.append("HYG↑")
if gold_c > 0 and brent_c < 0: s += 10; r.append("Or↑+Brent↓ (taux réels ↓)")
# Signaux phase 2
if tlt_c > 0.5: s += 12; r.append("TLT↑↑ (taux 20Y baissent = désinflation confirmée)")
elif tlt_c > 0.2: s += 6; r.append("TLT↑ (bonds longs soutiennent)")
if xlf_c > 0: s += 5; r.append("XLF↑ (banques = anticipent baisse taux)")
scores["desinflation"] = min(100, s); reasons["desinflation"] = r
# STAGFLATION — inflation + croissance faible
@@ -497,6 +612,11 @@ def score_macro_scenarios(gauges: Dict[str, Any]) -> Dict[str, Any]:
if gold_c > 0.5: s += 15; r.append("Or↑ (protection inflation)")
if copper_c < 0: s += 15; r.append("Cuivre↓ (demande faible)")
if vix > 18: s += 10; r.append("VIX élevé")
# Signaux phase 2
if xlp_c > xlk_c + 0.5: s += 8; r.append("Défensifs > Tech (rotation stagflationniste)")
if xlu_c > 0.4: s += 6; r.append("Utilities↑ (rotation vers revenus stables)")
if skew_v > 130: s += 5; r.append(f"SKEW {skew_v:.0f} (tail risk croissant)")
if tlt_c < -0.3: s += 5; r.append("TLT↓ (taux longs remontent = inflation persistante)")
scores["stagflation"] = min(100, s); reasons["stagflation"] = r
# RÉCESSION
@@ -514,6 +634,15 @@ def score_macro_scenarios(gauges: Dict[str, Any]) -> Dict[str, Any]:
if hyg_c < -0.5: s += 15; r.append("HYG↓ (spreads s'écartent)")
elif hyg_c < 0: s += 5
if gold_c > 0.3: s += 10; r.append("Or↑ (refuge)")
# Signaux phase 2
if tlt_c > 0.5: s += 15; r.append("TLT↑↑ (fuite vers bonds longs = signal recessionnaire fort)")
elif tlt_c > 0.2: s += 7; r.append("TLT↑ (obligations soutenues)")
if xlf_c < -1.0: s += 12; r.append("Financières↓↓ (banques = leading indicator récession)")
elif xlf_c < -0.3: s += 5
if eem_c < -1.0: s += 8; r.append("EM↓ (global slowdown)")
if usdjpy_c < -1.0: s += 10; r.append("JPY↑↑ (carry unwind = risk-off global)")
elif usdjpy_c < -0.5: s += 5
if skew_v > 135: s += 8; r.append(f"SKEW {skew_v:.0f} (tail risk extrême)")
scores["recession"] = min(100, s); reasons["recession"] = r
# CRISE DE LIQUIDITÉ
@@ -530,6 +659,16 @@ def score_macro_scenarios(gauges: Dict[str, Any]) -> Dict[str, Any]:
if gold_c > 1.0 and copper_c < -1.0: s += 20; r.append("Or↑+Cuivre↓ (fuite sécurité)")
if dxy_c > 1.0: s += 15; r.append("Dollar↑↑")
if ief_c > 0.5: s += 10; r.append("Obligations souveraines↑↑")
# Signaux phase 2 — les meilleurs indicateurs de crise liquide
if skew_v > 145: s += 15; r.append(f"SKEW {skew_v:.0f} — extrême tail risk (panique protection)")
elif skew_v > 135: s += 8
if vvix_v > 115: s += 15; r.append(f"VVIX {vvix_v:.0f} — vol-of-vol panique")
elif vvix_v > 100: s += 8; r.append(f"VVIX {vvix_v:.0f} — vol élevée")
if usdjpy_c < -1.5: s += 15; r.append("JPY↑↑↑ (carry trade unwind = panique globale)")
elif usdjpy_c < -0.8: s += 7; r.append("JPY↑ (risk-off carry)")
if xlf_c < -2.0: s += 15; r.append("Banques↓↓ (stress bancaire systémique)")
elif xlf_c < -1.0: s += 7
if emb_c < -1.0: s += 10; r.append("EM Bonds↓ (fuite liquidité EM)")
scores["crise_liquidite"] = min(100, s); reasons["crise_liquidite"] = r
# REFLATION — croissance accélère + inflation remonte (cuivre, énergie, small caps explosent)
@@ -547,6 +686,12 @@ def score_macro_scenarios(gauges: Dict[str, Any]) -> Dict[str, Any]:
if rel_perf > 0.3: s += 10; r.append("Small caps > large (risk-on large)")
elif rel_perf > 0: s += 4
if vix < 18: s += 5
# Signaux phase 2
if eem_c > 1.0: s += 10; r.append("EM↑↑ (croissance mondiale = reflation globale)")
elif eem_c > 0.3: s += 5; r.append("EM↑ (global risk-on)")
if xlk_c > 1.0: s += 8; r.append("Tech↑↑ (croissance+momentum)")
if silver_c > 1.5: s += 8; r.append("Argent↑↑ (industrial metals = reflation industrielle)")
if usdjpy_c > 0.5: s += 6; r.append("JPY↓ (carry trades actifs = risk-on global)")
scores["reflation"] = min(100, s); reasons["reflation"] = r
# SOFT LANDING — croissance positive + inflation en repli, pas encore basse
@@ -562,6 +707,10 @@ def score_macro_scenarios(gauges: Dict[str, Any]) -> Dict[str, Any]:
if xli_c > 0: s += 8; r.append("Industriels positifs")
if copper_c > 0: s += 5; r.append("Cuivre stable")
if ief_c > 0 and brent_c < 0: s += 7; r.append("Taux baissent + énergie recule")
# Signaux phase 2
if xlf_c > 0: s += 8; r.append("Financières↑ (banques = économie saine, no recession)")
if eem_c > 0: s += 5; r.append("EM stable (croissance globale intacte)")
if skew_v < 130: s += 4; r.append(f"SKEW {skew_v:.0f} (tail risk non-extrême)")
scores["soft_landing"] = min(100, s); reasons["soft_landing"] = r
# CHOC INFLATIONNISTE — spike énergie/supply soudain (guerre, OPEC, sécheresse)
@@ -578,6 +727,12 @@ def score_macro_scenarios(gauges: Dict[str, Any]) -> Dict[str, Any]:
elif vix > 18: s += 5
if copper_c < -0.5: s += 8; r.append("Cuivre↓ (demand destruction)")
if ief_c < -0.2: s += 8; r.append("Trésor↓ (taux longs remontent)")
# Signaux phase 2
if ovx_v > 45: s += 15; r.append(f"OVX {ovx_v:.0f} — vol pétrole extreme (choc supply aigu)")
elif ovx_v > 35: s += 8; r.append(f"OVX {ovx_v:.0f} — vol pétrole élevée")
if gvz_v > 22: s += 8; r.append(f"GVZ {gvz_v:.0f} — vol or élevée (inflation/géo)")
if xlp_c > 0.5: s += 6; r.append("Défensifs↑ (rotation anti-inflation)")
if tlt_c < -0.5: s += 8; r.append("TLT↓↓ (taux 20Y montent = anticipation inflation)")
scores["inflation_shock"] = min(100, s); reasons["inflation_shock"] = r
ranked = sorted(scores.items(), key=lambda x: x[1], reverse=True)