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

78
SIGNALS_FUTURES.md Normal file
View File

@@ -0,0 +1,78 @@
# Signaux non disponibles actuellement — Feuille de route
Ce document liste les signaux macro et de marché que nous ne pouvons pas encore
récupérer gratuitement via yfinance, et les sources qui les rendraient accessibles.
---
## A. Disponibles gratuitement mais non encore intégrés
Ces sources sont accessibles sans abonnement — implémentation possible.
| Signal | Source | Délai | Intérêt pour le trade |
|--------|---------|-------|----------------------|
| **CFTC Commitments of Traders (COT)** | cftc.gov (CSV hebdomadaire) | 3 jours ouvrés | Positionnement net des spéculateurs sur or, pétrole, blé, forex — signal de retournement contrarian fort |
| **Baltic Dry Index (BDI)** | Scrapage web (Investing.com) | J+1 | Leading indicator du commerce mondial — baisse = ralentissement industriel global |
| **Fed H.4.1 — Bilan de la Fed (weekly)** | federalreserve.gov (CSV) | 1 semaine | Expansion/contraction du bilan Fed = signal quantitative easing/tightening |
| **US Money Supply M2** | FRED API (gratuit) | ~6 semaines | Croissance M2 → inflation future ; contraction M2 → récession (2022-2023 signal) |
| **CNN Fear & Greed Index** | Scrapage CNN | J | Sentiment extrême (0 = peur max, 100 = avidité max) — contrarian signal timing |
| **AAII Investor Sentiment Survey** | aaii.com (hebdo) | 1 semaine | % bulls/bears retail — extrêmes signalent retournement |
| **FRED Economic Data (40+ séries)** | api.stlouisfed.org (gratuit) | Variable | CPI, PCE, taux chômage U6, leading indicators Conference Board, etc. |
| **US 2Y Treasury yield** | yfinance `^IRX` proxy ou FRED | J | Segment le plus Fed-sensitive — différentiel 2Y/10Y = meilleur signal récession |
| **Breakeven inflation 5Y (TIPS)** | FRED: T5YIE | J+1 | Taux réels implicites = ce que le marché anticipe pour l'inflation |
| **Indice PMI manufacturier (ISM)** | Scrapage ou FRED | Mensuel | PMI < 50 = contraction manufacturière ; PMI > 55 = expansion forte |
| **Google Trends (mots-clés macro)** | pytrends (gratuit) | J | "Recession", "unemployment", "inflation" — sentiment populaire précède souvent les données |
| **Données OPEC production** | opec.org (PDF/RSS) | Mensuel | Production réelle vs quota = signal énergie avant les chiffres EIA |
| **EIA Crude Oil Inventories** | eia.gov (CSV) | Hebdo (mercredi) | Stocks pétrole US = driver court terme du WTI |
---
## B. Nécessitent un abonnement payant (prix modéré)
| Signal | Source | Coût estimé | Intérêt |
|--------|---------|-------------|---------|
| **CBOE Put/Call Ratio (temps réel)** | CBOE DataShop | ~$50/mois | Sentiment options en temps réel — P/C > 1.2 = panique = timing long |
| **Gamma Exposure (GEX) SPX** | SpotGamma / SqueezeMetrics | ~$50-150/mois | Exposition gamma des market makers = niveaux magnétiques prix, vol des options |
| **Options Flow (dark pools)** | Unusual Whales / FlowAlgo | ~$50-200/mois | Gros blocs institutionnels sur options = signal directionnel fort |
| **Short Interest Ratio** | Ortex / S3 Partners | ~$100/mois | % float vendu à découvert = squeeze potential ou bearish conviction institutionnel |
| **Credit Default Swaps (CDS IG/HY)** | Bloomberg / Markit | $500+/mois | Spread CDS direct = meilleur indicateur crédit (vs HYG qui est proxy) |
| **VIX Term Structure (VX Futures)** | CBOE / IVolatility | ~$30/mois | Contango/backwardation exact de la courbe VIX futures — clé pour stratégies vol |
| **Skew par sous-jacent (GLD, USO, etc.)** | IVolatility / OptionMetrics | ~$50/mois | Skew spécifique par ETF = évalue si les puts/calls sont chers sur TON sous-jacent |
| **Insider Trading en temps réel** | OpenInsider (partiellement gratuit) | ~$50/mois pour alertes | Achats/ventes dirigeants = signal fondamental avant résultats |
| **Earnings Revision Score** | Factset / Bloomberg | $500+/mois | Révisions de bénéfices = meilleur driver des actions sur 3-6 mois |
---
## C. Signaux structurellement difficiles à obtenir
Ces données sont soit OTC, soit publiées avec un délai important, soit agrégées uniquement
par des institutions qui ne les partagent pas publiquement.
| Signal | Problème d'accès | Intérêt si disponible |
|--------|-----------------|----------------------|
| **Réserves d'or des banques centrales** | FMI publie avec 2-3 mois de délai ; achats réels souvent cachés | Demande physique institutionnelle = pression prix or moyen terme |
| **Bilans détaillés BCE/PBoC/BoJ** | Données disponibles mais complexes à parser (formats hétérogènes, langues) | Expansion monétaire mondiale = signal reflation/inflation globale |
| **Positionnement hedge funds (13F)** | SEC 13F = 45 jours de délai après fin du trimestre | Trop tardif pour le trading — anecdotique |
| **Dettes souveraines EM (libellées en USD)** | Banque Mondiale, FMI — trimestriels/annuels | Signal de crise de dette EM avant que ça se voit sur EEM/EMB |
| **Prix physiques vs prix futures (basis)** | OTC / brokers spécialisés matières premières | Basis pétrole = signal supply/demand physique réel vs spéculatif |
| **Dark pool prints (actions)** | FINRA publie avec délai ; temps réel = ~$500+/mois | Flow institutionnel avant les mouvements visibles |
| **Inventaires métaux LME (Cu, Al, Zn, Ni)** | LME.com — partiellement gratuit | Stocks physiques cuivre/aluminium = confirmation Dr Copper signal |
| **Taux SOFR / repo overnight** | NY Fed publie J+1 — utilisable | Stress du marché repo = signal crise liquidité avant VIX |
| **Global PMI composite (JP Morgan)** | IHS Markit / S&P — abonnement | PMI monde = leading indicator croissance globale avant EEM |
| **Inflation swap rates (5Y5Y forward)** | Bloomberg uniquement | Anticipations inflation long terme des pros — différent du breakeven TIPS |
---
## D. Priorités d'implémentation suggérées
Par ordre de valeur/effort :
1. **FRED API (gratuit)** — M2, breakeven 5Y, US 2Y yield, leading indicators → ±2h de dev
2. **CFTC COT (gratuit, hebdo)** — COT or + pétrole + blé → signal contrarian puissant → ±4h de dev
3. **EIA inventories pétrole (gratuit)** — driver court terme WTI → ±2h de dev
4. **CNN Fear & Greed (scrapage)** — sentiment composite → ±1h de dev
5. **Baltic Dry Index (scrapage)** — commerce mondial → ±1h de dev
6. **LME stocks cuivre** — confirmation signal cuivre → ±2h de dev
7. **SpotGamma GEX** (~$50/mois) — niveaux gamma SPX → impact direct stratégie
8. **CBOE P/C ratio** (~$50/mois) — timing options → impact direct
9. **VIX term structure** (~$30/mois) → améliore pilier 3e

View File

@@ -286,7 +286,13 @@ PILIER 3 — SIGNAUX DE PRIX (35 pts max)
3b. Énergie & Matières prem. (0-7): or, pétrole, gaz, cuivre, blé — direction et momentum 3b. Énergie & Matières prem. (0-7): or, pétrole, gaz, cuivre, blé — direction et momentum
3c. Forex (0-7): USD index, EUR/USD, paires émergentes — cohérence avec pattern 3c. Forex (0-7): USD index, EUR/USD, paires émergentes — cohérence avec pattern
3d. Actions & Indices (0-7): SPX, NDX, rotation sectorielle, breadth, sentiment 3d. Actions & Indices (0-7): SPX, NDX, rotation sectorielle, breadth, sentiment
3e. Volatilité (VIX/IV) (0-7): régime de vol, coût options, skew — favorable à la stratégie ? 3e. Volatilité & Surface de vol (0-7):
- Régime VIX (niveau absolu et tendance)
- SKEW Index: si >135 → queues chères → PRÉFÉRER spreads (budget = max loss limité)
si <115 → vol bon marché → envisager straddles/strangles si catalyseur binaire
- VVIX: si >100 → straddles/strangles trop chers → préférer directionnels
- OVX (>40) / GVZ (>22): vol sectorielle élevée sur le sous-jacent → primes gonflées, spreads
- Régime surface: contango_calm (idéal vendre vol) vs backwardation_panic (primes explosées)
PILIER 4 — RISQUE / RÉCOMPENSE (15 pts max) PILIER 4 — RISQUE / RÉCOMPENSE (15 pts max)
4a. Asymétrie R/R (0-10): ratio gain potentiel / prime payée / perte max pour ~1000€ 4a. Asymétrie R/R (0-10): ratio gain potentiel / prime payée / perte max pour ~1000€
@@ -419,11 +425,42 @@ def score_patterns_with_context(
"dominant_scenario": dominant, "dominant_scenario": dominant,
"scenario_scores": scenarios.get("scores", {}), "scenario_scores": scenarios.get("scores", {}),
"asset_class_bias": bias_for_class, "asset_class_bias": bias_for_class,
# ── Signaux historiques (phase 1) ─────────────────────────────
"vix": gauges.get("vix", {}).get("value"), "vix": gauges.get("vix", {}).get("value"),
"yield_slope_pct": gauges.get("slope_10y3m", {}).get("value"), "yield_slope_pct": gauges.get("slope_10y3m", {}).get("value"),
"gold_copper_ratio": gauges.get("gold_copper_ratio", {}).get("value"), "gold_copper_ratio": gauges.get("gold_copper_ratio", {}).get("value"),
"brent_1d_pct": gauges.get("brent", {}).get("change_pct"), "brent_1d_pct": gauges.get("brent", {}).get("change_pct"),
"spx_vs_200d_pct": gauges.get("spx_vs_200d", {}).get("value"), "spx_vs_200d_pct": gauges.get("spx_vs_200d", {}).get("value"),
# ── Surface de volatilité (phase 2) ──────────────────────────
# SKEW >135 = queues chères → spreads; <115 = vol bon marché → straddles
"skew_index": gauges.get("skew", {}).get("value"),
# VVIX >100 = straddles/strangles trop chers → préférer directionnels
"vvix": gauges.get("vvix", {}).get("value"),
# Vol sectorielle spécifique au sous-jacent
"oil_vol_ovx": gauges.get("ovx", {}).get("value"),
"gold_vol_gvz": gauges.get("gvz", {}).get("value"),
# Régime composite: contango_calm|normal|tail_risk_elevated|backwardation_panic|complacency_hedged
"vol_surface_regime": gauges.get("vol_surface_regime", {}).get("note"),
# ── Rotation sectorielle (phase 2) ────────────────────────────
# Positif = tech > défensifs = risk-on; négatif = rotation défensive
"tech_vs_staples_pct": gauges.get("xlk_xlp_momentum", {}).get("value"),
# Négatif = banques < marché = stress économique anticipé
"financials_vs_spx_pct": gauges.get("xlf_spx_ratio", {}).get("value"),
# ── Global / Marchés émergents (phase 2) ─────────────────────
"em_equity_1d_pct": gauges.get("eem", {}).get("change_pct"),
"em_bonds_1d_pct": gauges.get("emb", {}).get("change_pct"),
"china_equity_1d_pct": gauges.get("fxi", {}).get("change_pct"),
# Ratio EM vs SPX: positif = croissance globale; négatif = fuite vers US
"em_vs_us_divergence_pct": gauges.get("eem_spx_ratio", {}).get("value"),
# ── Carry / Risk-off (phase 2) ────────────────────────────────
# Négatif = JPY s'apprécie = carry unwind = risk-off global
"usdjpy_1d_pct": gauges.get("usdjpy", {}).get("change_pct"),
# ── Long bonds / Qualité (phase 2) ────────────────────────────
# Positif = flight to quality = risk-off; négatif = taux longs remontent
"long_bond_tlt_1d_pct": gauges.get("tlt", {}).get("change_pct"),
# ── Métaux (phase 2) ──────────────────────────────────────────
# Ratio Ag/Au: >0.016 = argent > or = risk-on industriel; <0.012 = defensive metals
"silver_gold_ratio": gauges.get("silver_gold_ratio", {}).get("value"),
} }
pattern_blocks.append({ pattern_blocks.append({
@@ -451,17 +488,50 @@ def score_patterns_with_context(
macro_section = "" macro_section = ""
if macro_regime: if macro_regime:
sc = macro_regime.get("scenarios", {}) sc = macro_regime.get("scenarios", {})
gauges_g = macro_regime.get("gauges", {})
dom = sc.get("dominant", "incertain") dom = sc.get("dominant", "incertain")
sc_scores = sc.get("scores", {}) sc_scores = sc.get("scores", {})
# Extract key new signals for global macro summary
_skew = gauges_g.get("skew", {}).get("value")
_vvix = gauges_g.get("vvix", {}).get("value")
_ovx = gauges_g.get("ovx", {}).get("value")
_vol_r = gauges_g.get("vol_surface_regime", {}).get("note", "normal")
_tech_st = gauges_g.get("xlk_xlp_momentum", {}).get("value")
_xlf_spx = gauges_g.get("xlf_spx_ratio", {}).get("value")
_eem_c = gauges_g.get("eem", {}).get("change_pct")
_usdjpy_c = gauges_g.get("usdjpy", {}).get("change_pct")
_tlt_c = gauges_g.get("tlt", {}).get("change_pct")
_sgr = gauges_g.get("silver_gold_ratio", {}).get("value")
def _fmt(v, unit="", decimals=1):
return f"{v:.{decimals}f}{unit}" if v is not None else "n/d"
macro_section = f""" macro_section = f"""
RÉGIME MACRO ACTUEL (30 compteurs agrégés): RÉGIME MACRO ACTUEL (50 compteurs — 29 tickers + 9 métriques dérivées):
- Scénario dominant: {dom.upper()} | Scores: {json.dumps(sc_scores, ensure_ascii=False)} - Scénario dominant: {dom.upper()} | Scores: {json.dumps(sc_scores, ensure_ascii=False)}
- Instruction: Intègre ce régime dans les piliers prix (3a taux, 3b énergie, 3d indices, 3e VIX).
Chaque pattern reçoit un champ "macro_regime.asset_class_bias" indiquant la compatibilité SURFACE DE VOLATILITÉ (pilier 3e — impact direct stratégie options):
(bullish+/bullish/neutral/bearish/bearish+/defensive) de sa classe d'actif avec le scénario dominant. - SKEW Index: {_fmt(_skew, '', 0)} | Régime vol: {_vol_r} | VVIX: {_fmt(_vvix, '', 0)} | OVX: {_fmt(_ovx, '', 0)}
"bullish+" = conditions très favorables pour ce pattern → majore 3b ou 3d selon la classe SKEW >135 = queues chères → SPREADS (pas de straddles). VVIX >100 = primes gonflées → directionnels.
"bearish" ou "bearish+" = conditions défavorables → minore 3b ou 3d {_vol_r} = {"contango calme: idéal spreads bon marché" if _vol_r == "contango_calm" else "backwardation/panique: options très chères, primes à vendre" if _vol_r == "backwardation_panic" else "tail risk élevé: protection queues = faveur spreads définis" if _vol_r == "tail_risk_elevated" else "environnement normal"}
Indique dans "summary": [GOLDILOCKS|STAGFLATION|RÉCESSION|DÉSINFLATION|CRISE] + [SUPPORTING|NEUTRAL|CONTRA]
ROTATION SECTORIELLE & RISQUE (pilier 3d + 3e):
- Tech vs Défensifs (XLK-XLP): {_fmt(_tech_st, '%pts')} | Financières vs S&P: {_fmt(_xlf_spx, '%pts')}
{"Risk-on sectoriel fort" if _tech_st and _tech_st > 0.5 else "Rotation défensive ⚠️" if _tech_st and _tech_st < -0.5 else "Neutre sectoriel"}
{"Banques saines = pas de récession" if _xlf_spx and _xlf_spx > 0.3 else "Stress bancaire anticipé ⚠️" if _xlf_spx and _xlf_spx < -0.3 else ""}
GLOBAL / CARRY / QUALITÉ (pilier 3a + 3d):
- EM Actions J+1: {_fmt(_eem_c, '%')} | USD/JPY J+1: {_fmt(_usdjpy_c, '%')} | TLT (20Y bonds) J+1: {_fmt(_tlt_c, '%')}
- Ratio Ag/Or: {_fmt(_sgr, '', 5)}
{"EM surperforme = croissance globale" if _eem_c and _eem_c > 0.5 else "EM stress = fuite vers US ⚠️" if _eem_c and _eem_c < -1.0 else ""}
{"JPY s'apprécie = carry unwind = risk-off ⚠️" if _usdjpy_c and _usdjpy_c < -0.8 else "JPY faible = risk-on carry actif" if _usdjpy_c and _usdjpy_c > 0.5 else ""}
{"TLT monte = flight to quality = bonds longs demandés" if _tlt_c and _tlt_c > 0.3 else "TLT baisse = taux longs remontent = inflation/risk-on" if _tlt_c and _tlt_c < -0.3 else ""}
Instructions de notation:
- Intègre le régime dans 3a (taux: slope+TLT), 3b (énergie+OVX), 3d (indices+XLF+EM), 3e (SKEW+VVIX+régime)
- "macro_regime.asset_class_bias" dans chaque pattern → majore/minore 3b ou 3d
- "macro_regime.skew_index" + "vol_surface_regime" → influence directe sur le choix de stratégie dans "recommended_trade"
- Indique dans "summary": [GOLDILOCKS|STAGFLATION|RÉCESSION|DÉSINFLATION|CRISE] + [SUPPORTING|NEUTRAL|CONTRA]
""" """
user = f"""CONTEXTE GLOBAL: user = f"""CONTEXTE GLOBAL:

View File

@@ -292,21 +292,44 @@ def get_economic_calendar() -> List[Dict[str, Any]]:
MACRO_GAUGE_CONFIG = [ MACRO_GAUGE_CONFIG = [
# (id, label, ticker, unit, bloc) # (id, label, ticker, unit, bloc)
# ── Liquidité / Taux ─────────────────────────────────────────────────────
("dxy", "Dollar DXY", "DX-Y.NYB", "index", "liquidite"), ("dxy", "Dollar DXY", "DX-Y.NYB", "index", "liquidite"),
("us10y", "UST 10Y", "^TNX", "%", "liquidite"), ("us10y", "UST 10Y", "^TNX", "%", "liquidite"),
("us3m", "UST 3M", "^IRX", "%", "liquidite"), ("us3m", "UST 3M", "^IRX", "%", "liquidite"),
("tips", "TIPS ETF", "TIP", "$", "liquidite"), ("tips", "TIPS ETF", "TIP", "$", "liquidite"),
("tlt", "Obligations 20Y+ (TLT)", "TLT", "$", "liquidite"),
# ── Crédit ───────────────────────────────────────────────────────────────
("vix", "VIX", "^VIX", "pts", "credit"), ("vix", "VIX", "^VIX", "pts", "credit"),
("hyg", "HY Bonds (HYG)", "HYG", "$", "credit"), ("hyg", "HY Bonds (HYG)", "HYG", "$", "credit"),
("lqd", "IG Bonds (LQD)", "LQD", "$", "credit"), ("lqd", "IG Bonds (LQD)", "LQD", "$", "credit"),
("ief", "Trésor 7-10Y (IEF)", "IEF", "$", "credit"), ("ief", "Trésor 7-10Y (IEF)", "IEF", "$", "credit"),
# ── Énergie ──────────────────────────────────────────────────────────────
("brent", "Brent", "BZ=F", "$", "energie"), ("brent", "Brent", "BZ=F", "$", "energie"),
("ng", "Gaz naturel", "NG=F", "$", "energie"), ("ng", "Gaz naturel", "NG=F", "$", "energie"),
# ── Métaux ───────────────────────────────────────────────────────────────
("gold", "Or", "GC=F", "$", "metaux"), ("gold", "Or", "GC=F", "$", "metaux"),
("silver", "Argent (Silver)", "SI=F", "$/oz", "metaux"),
("copper", "Cuivre", "HG=F", "$/lb", "metaux"), ("copper", "Cuivre", "HG=F", "$/lb", "metaux"),
# ── Croissance US ─────────────────────────────────────────────────────────
("spx", "S&P 500", "^GSPC", "pts", "croissance"), ("spx", "S&P 500", "^GSPC", "pts", "croissance"),
("iwm", "Russell 2000", "IWM", "$", "croissance"), ("iwm", "Russell 2000", "IWM", "$", "croissance"),
("xli", "Industriels XLI", "XLI", "$", "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 = { SCENARIO_META = {
@@ -337,7 +360,7 @@ def get_macro_gauges() -> Dict[str, Any]:
from concurrent.futures import ThreadPoolExecutor, as_completed from concurrent.futures import ThreadPoolExecutor, as_completed
raw: Dict[str, Any] = {} 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 = { futures = {
exe.submit(get_quote, ticker): (gid, label, ticker, unit, bloc) exe.submit(get_quote, ticker): (gid, label, ticker, unit, bloc)
for gid, label, ticker, unit, bloc in MACRO_GAUGE_CONFIG 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)")), 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) return _sanitize_floats(raw)
@@ -449,6 +538,22 @@ def score_macro_scenarios(gauges: Dict[str, Any]) -> Dict[str, Any]:
iwm_c = gc("iwm") iwm_c = gc("iwm")
xli_c = gc("xli") xli_c = gc("xli")
rel_perf = gv("iwm_spx_ratio") or 0.0 # Russell vs S&P relative perf 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] = {} scores: Dict[str, int] = {}
reasons: Dict[str, List[str]] = {} 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") if vs200 > 5: s += 15; r.append(f"S&P+{vs200}% vs 200j")
elif vs200 > 0: s += 7 elif vs200 > 0: s += 7
if copper_c > 0.5: s += 10; r.append("Cuivre↑") 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 scores["goldilocks"] = min(100, s); reasons["goldilocks"] = r
# DÉSINFLATION / BAISSE DE TAUX # 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 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 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 ↓)") 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 scores["desinflation"] = min(100, s); reasons["desinflation"] = r
# STAGFLATION — inflation + croissance faible # 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 gold_c > 0.5: s += 15; r.append("Or↑ (protection inflation)")
if copper_c < 0: s += 15; r.append("Cuivre↓ (demande faible)") if copper_c < 0: s += 15; r.append("Cuivre↓ (demande faible)")
if vix > 18: s += 10; r.append("VIX élevé") 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 scores["stagflation"] = min(100, s); reasons["stagflation"] = r
# RÉCESSION # 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)") if hyg_c < -0.5: s += 15; r.append("HYG↓ (spreads s'écartent)")
elif hyg_c < 0: s += 5 elif hyg_c < 0: s += 5
if gold_c > 0.3: s += 10; r.append("Or↑ (refuge)") 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 scores["recession"] = min(100, s); reasons["recession"] = r
# CRISE DE LIQUIDITÉ # 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 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 dxy_c > 1.0: s += 15; r.append("Dollar↑↑")
if ief_c > 0.5: s += 10; r.append("Obligations souveraines↑↑") 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 scores["crise_liquidite"] = min(100, s); reasons["crise_liquidite"] = r
# REFLATION — croissance accélère + inflation remonte (cuivre, énergie, small caps explosent) # 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)") if rel_perf > 0.3: s += 10; r.append("Small caps > large (risk-on large)")
elif rel_perf > 0: s += 4 elif rel_perf > 0: s += 4
if vix < 18: s += 5 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 scores["reflation"] = min(100, s); reasons["reflation"] = r
# SOFT LANDING — croissance positive + inflation en repli, pas encore basse # 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 xli_c > 0: s += 8; r.append("Industriels positifs")
if copper_c > 0: s += 5; r.append("Cuivre stable") 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") 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 scores["soft_landing"] = min(100, s); reasons["soft_landing"] = r
# CHOC INFLATIONNISTE — spike énergie/supply soudain (guerre, OPEC, sécheresse) # 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 elif vix > 18: s += 5
if copper_c < -0.5: s += 8; r.append("Cuivre↓ (demand destruction)") 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)") 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 scores["inflation_shock"] = min(100, s); reasons["inflation_shock"] = r
ranked = sorted(scores.items(), key=lambda x: x[1], reverse=True) ranked = sorted(scores.items(), key=lambda x: x[1], reverse=True)