From aa81598278310553bccd4d06a0d9fdc1b7d15c63 Mon Sep 17 00:00:00 2001 From: OpenSquared Date: Wed, 24 Jun 2026 22:48:10 +0200 Subject: [PATCH] feat: driver-based timeline strip, regime signal metrics, drivers editor - instruments.json: add keywords array to every driver across 20 instruments (Fed, BCE, BOJ, OPEC, CPI, AI, EIA, etc.) for event-to-driver matching - instrument_service.py: add update_instrument_drivers() persisting changes to JSON and refreshing in-memory cache - instruments.py: add PUT /api/instruments/{id}/drivers endpoint (DriverUpdate model) - InstrumentDashboard: * RegimeCard: replace regime score bars with 6-metric signal grid (MA50/MA200 position, MA50 slope, MA200 slope, momentum 20j, dist MA200, ATR vol ratio) with colour-coded values and contextual sub-labels (Golden cross, Surextension, etc.) * EventTimelineStrip: rows now keyed by top-4 instrument drivers (by weight) instead of LT/MT/CT; events matched via case-insensitive keyword scan against title + description + category; fallback dashed line when no events match * DriversPanel: inline edit panel (toggle via Drivers button in header); edit label, weight, keywords (comma-separated) per driver; add/remove drivers; saves via PUT /api/instruments/{id}/drivers; optimistic local state update Co-Authored-By: Claude Sonnet 4.6 --- backend/config/instruments.json | 520 +++++++---------- backend/routers/instruments.py | 22 + backend/services/instrument_service.py | 27 + frontend/src/pages/InstrumentDashboard.tsx | 645 ++++++++++++--------- 4 files changed, 635 insertions(+), 579 deletions(-) diff --git a/backend/config/instruments.json b/backend/config/instruments.json index 86a8f58..8d6d124 100644 --- a/backend/config/instruments.json +++ b/backend/config/instruments.json @@ -8,25 +8,20 @@ "exchange": "CBOE", "category": "equity_index", "currency": "USD", - "description": "US large-cap equity benchmark", - "chart": { - "ma_periods": [20, 50, 200], - "bollinger_period": 20, - "bollinger_std": 2, - "show_volume": true - }, + "description": "US large-cap equity benchmark — 500 plus grandes capitalisations américaines", + "chart": { "ma_periods": [20, 50, 200], "bollinger_period": 20, "bollinger_std": 2, "show_volume": true }, "drivers": [ - {"key": "fed_policy", "label": "Fed Policy", "weight": 0.9}, - {"key": "earnings_growth", "label": "Earnings Growth", "weight": 0.85}, - {"key": "real_rates", "label": "Real Rates", "weight": 0.75}, - {"key": "liquidity", "label": "Global Liquidity", "weight": 0.7}, - {"key": "credit_spreads", "label": "Credit Spreads HY", "weight": 0.6} + {"key": "fed_policy", "label": "Fed Policy", "weight": 0.9, "keywords": ["FOMC","Fed","Federal Reserve","Powell","taux directeur","pivot","dot plot","QT","QE","taux Fed"]}, + {"key": "earnings_growth", "label": "Earnings Growth", "weight": 0.85, "keywords": ["Earnings","EPS","bénéfices","résultats","Earnings Season","reporting","profits"]}, + {"key": "real_rates", "label": "Real Rates", "weight": 0.75, "keywords": ["taux réels","TIPS","real yields","breakeven","inflation","CPI","PCE"]}, + {"key": "liquidity", "label": "Global Liquidity", "weight": 0.7, "keywords": ["liquidité","liquidity","QE","QT","bilan Fed","repo","M2","conditions financières"]}, + {"key": "vix", "label": "VIX / Fear", "weight": 0.6, "keywords": ["VIX","volatilité","vol spike","hedging","protection","skew","put","peur","fear"]} ], "regime_labels": ["Risk-On Bull", "Risk-Off Bear", "Stagflation", "Rate Shock", "Recovery"], - "event_keywords": ["FOMC", "CPI", "NFP", "GDP", "Earnings Season", "Fed Chair"], - "related_assets": ["SPX", "SPY", "QQQ", "ES"], - "correlation_instruments": ["QQQ", "TLT", "HYG", "VXX"], - "ai_context": "SPY tracks the S&P 500 (US large-cap benchmark). Bullish on: dovish Fed, strong earnings, liquidity expansion. Bearish on: rate hikes, recession, credit events. Watch: VIX, HYG spreads, earnings." + "event_keywords": ["FOMC","CPI","NFP","GDP","Earnings Season","Fed Chair","VIX"], + "related_assets": ["SPX","SPY","QQQ","ES"], + "correlation_instruments": ["QQQ","TLT","HYG","VXX"], + "ai_context": "SPY tracks the S&P 500. Bullish: dovish Fed, strong earnings, liquidity expansion. Bearish: rate hikes, recession, credit events. Watch: VIX, HYG spreads, FOMC." }, { "id": "QQQ", @@ -36,25 +31,20 @@ "exchange": "NASDAQ", "category": "equity_index", "currency": "USD", - "description": "US large-cap tech and growth benchmark", - "chart": { - "ma_periods": [20, 50, 200], - "bollinger_period": 20, - "bollinger_std": 2, - "show_volume": true - }, + "description": "US large-cap tech and growth benchmark — 100 plus grandes valeurs Nasdaq", + "chart": { "ma_periods": [20, 50, 200], "bollinger_period": 20, "bollinger_std": 2, "show_volume": true }, "drivers": [ - {"key": "fed_policy", "label": "Fed Policy", "weight": 0.9}, - {"key": "earnings_tech", "label": "Tech Earnings", "weight": 0.85}, - {"key": "real_rates", "label": "Real Rates", "weight": 0.8}, - {"key": "ai_capex", "label": "AI Capex", "weight": 0.8}, - {"key": "dollar", "label": "Dollar Index", "weight": 0.5} + {"key": "real_rates", "label": "Real Rates", "weight": 0.9, "keywords": ["taux réels","TIPS","real yields","FOMC","Fed","CPI","breakeven"]}, + {"key": "ai_capex", "label": "AI Capex", "weight": 0.85, "keywords": ["IA","AI","Nvidia","Microsoft","capex tech","data center","GPU","intelligence artificielle","ChatGPT"]}, + {"key": "megacaps", "label": "Mégacaps GAFAM", "weight": 0.8, "keywords": ["GAFAM","Apple","Meta","Alphabet","Amazon","Microsoft","earnings tech","Big Tech","résultats tech"]}, + {"key": "growth", "label": "Croissance US", "weight": 0.75, "keywords": ["croissance","GDP","récession","NFP","emploi","PMI","croissance économique"]}, + {"key": "dollar", "label": "Dollar Index", "weight": 0.5, "keywords": ["Dollar","DXY","dollar index","EUR/USD","devise"]} ], "regime_labels": ["Tech Bull (AI-Driven)", "Tech Bear", "Rate Compression", "Earnings Crash", "Recovery"], - "event_keywords": ["FOMC", "Big Tech Earnings", "CPI", "AI", "Rate"], - "related_assets": ["QQQ", "NDX", "NQ"], - "correlation_instruments": ["SPY", "TLT", "NVDA", "AAPL"], - "ai_context": "QQQ tracks the Nasdaq 100 (tech/growth benchmark). Bullish on: dovish Fed, AI capex boom, strong Big Tech earnings. Bearish on: rate hikes, earnings misses, AI regulation. Watch: real yields, mega-cap earnings reports." + "event_keywords": ["FOMC","Big Tech Earnings","CPI","AI","Rate","NVDA","AAPL"], + "related_assets": ["QQQ","NDX","NQ"], + "correlation_instruments": ["SPY","TLT","NVDA","AAPL"], + "ai_context": "QQQ tracks Nasdaq 100. Bullish: dovish Fed, AI capex boom, Big Tech earnings. Bearish: rate hikes, earnings misses, AI regulation. Watch: real yields, mega-cap earnings." }, { "id": "IWM", @@ -64,24 +54,19 @@ "exchange": "NYSE", "category": "equity_index", "currency": "USD", - "description": "US small-cap equity benchmark", - "chart": { - "ma_periods": [20, 50, 200], - "bollinger_period": 20, - "bollinger_std": 2, - "show_volume": true - }, + "description": "US small-cap equity benchmark — 2000 petites capitalisations américaines", + "chart": { "ma_periods": [20, 50, 200], "bollinger_period": 20, "bollinger_std": 2, "show_volume": true }, "drivers": [ - {"key": "credit_conditions", "label": "Credit Conditions", "weight": 0.85}, - {"key": "fed_policy", "label": "Fed Policy", "weight": 0.8}, - {"key": "gdp_growth", "label": "GDP Growth", "weight": 0.75}, - {"key": "dollar", "label": "Dollar Index", "weight": 0.6} + {"key": "gdp_growth", "label": "Croissance US", "weight": 0.85, "keywords": ["GDP","PIB","croissance US","NFP","emploi","récession","PMI","domestic growth"]}, + {"key": "credit", "label": "Crédit & Spreads", "weight": 0.8, "keywords": ["crédit","credit","HY","high yield","spreads","LBO","conditions de crédit","credit crunch"]}, + {"key": "regional_banks", "label": "Banques Régionales","weight": 0.75, "keywords": ["banques régionales","SVB","FDIC","banque","banking","bank failure","crédit bancaire"]}, + {"key": "dollar", "label": "Dollar Index", "weight": 0.6, "keywords": ["Dollar","DXY","dollar fort","USD strength"]} ], "regime_labels": ["Small Cap Expansion", "Credit Crunch Squeeze", "Risk-Off", "Recovery", "Neutral"], - "event_keywords": ["FOMC", "Credit", "GDP", "NFP", "Rates"], - "related_assets": ["IWM", "RTY", "RUT"], - "correlation_instruments": ["SPY", "HYG", "TLT"], - "ai_context": "IWM tracks the Russell 2000 (US small-cap). Heavily credit-sensitive — bullish when credit conditions loosen and domestic growth accelerates. Bearish on credit tightening, recession fears, dollar strength (imported cost pressure). Watch: HYG spreads, regional bank health." + "event_keywords": ["FOMC","Credit","GDP","NFP","Rates","Regional Banks"], + "related_assets": ["IWM","RTY","RUT"], + "correlation_instruments": ["SPY","HYG","TLT"], + "ai_context": "IWM tracks Russell 2000. Heavily credit-sensitive. Bullish: credit loosening, domestic growth. Bearish: credit tightening, recession, dollar strength. Watch: HYG spreads, regional bank health." }, { "id": "EEM", @@ -91,24 +76,19 @@ "exchange": "NYSE", "category": "equity_intl", "currency": "USD", - "description": "MSCI Emerging Markets large-cap benchmark", - "chart": { - "ma_periods": [20, 50, 200], - "bollinger_period": 20, - "bollinger_std": 2, - "show_volume": true - }, + "description": "MSCI Emerging Markets — grandes capitalisations des marchés émergents", + "chart": { "ma_periods": [20, 50, 200], "bollinger_period": 20, "bollinger_std": 2, "show_volume": true }, "drivers": [ - {"key": "dollar_index", "label": "Dollar Index", "weight": 0.9}, - {"key": "china_growth", "label": "China Growth", "weight": 0.85}, - {"key": "em_flows", "label": "EM Flows", "weight": 0.75}, - {"key": "commodities", "label": "Commodities", "weight": 0.7} + {"key": "dollar_index", "label": "Dollar Index", "weight": 0.9, "keywords": ["Dollar","DXY","USD","dollar index","dollar fort","faible dollar"]}, + {"key": "china_growth", "label": "Chine / PBoC", "weight": 0.85, "keywords": ["China","Chine","PBOC","PBoC","PIB chinois","Xi","immobilier chinois","stimulus Chine","relance Chine"]}, + {"key": "em_flows", "label": "Flux EM", "weight": 0.75, "keywords": ["EM","émergents","emerging","flows","flux","capitaux émergents"]}, + {"key": "commodities", "label": "Matières Premières","weight": 0.7, "keywords": ["matières premières","commodities","minerai","pétrole","métaux","commodité"]} ], "regime_labels": ["EM Growth Rally", "Dollar Squeeze", "China Risk-Off", "Commodity Lift", "Neutral"], - "event_keywords": ["China GDP", "Dollar", "EM", "PBOC", "Commodities"], - "related_assets": ["EEM", "EM", "CNY", "China"], - "correlation_instruments": ["GLD", "USO", "EURUSD=X"], - "ai_context": "EEM tracks MSCI Emerging Markets. Dollar weakness and China growth are the dominant drivers. Bullish on: PBOC stimulus, commodities rally, weak USD. Bearish on: Fed tightening, China slowdown, geopolitical trade tensions. Watch: USDCNY, China PMI, EM bond flows." + "event_keywords": ["China GDP","Dollar","EM","PBOC","Commodities","stimulus Chine"], + "related_assets": ["EEM","EM","CNY","China"], + "correlation_instruments": ["GLD","USO","EURUSD=X"], + "ai_context": "EEM tracks MSCI Emerging Markets. Bullish: PBOC stimulus, commodities rally, weak USD. Bearish: Fed tightening, China slowdown, trade tensions. Watch: USDCNY, China PMI, EM bond flows." }, { "id": "EFA", @@ -118,24 +98,19 @@ "exchange": "NYSE", "category": "equity_intl", "currency": "USD", - "description": "MSCI EAFE (Europe, Australasia, Far East) benchmark", - "chart": { - "ma_periods": [20, 50, 200], - "bollinger_period": 20, - "bollinger_std": 2, - "show_volume": true - }, + "description": "MSCI EAFE — Europe, Australasie, Extrême-Orient (hors US/Canada)", + "chart": { "ma_periods": [20, 50, 200], "bollinger_period": 20, "bollinger_std": 2, "show_volume": true }, "drivers": [ - {"key": "ecb_policy", "label": "ECB Policy", "weight": 0.85}, - {"key": "dollar", "label": "Dollar Index", "weight": 0.8}, - {"key": "european_growth", "label": "European Growth", "weight": 0.75}, - {"key": "global_trade", "label": "Global Trade", "weight": 0.6} + {"key": "ecb_policy", "label": "BCE / ECB", "weight": 0.85, "keywords": ["BCE","ECB","Lagarde","taux BCE","zone euro","politique BCE","ECB rate"]}, + {"key": "energy", "label": "Énergie Europe", "weight": 0.8, "keywords": ["énergie","gaz européen","pétrole","LNG","crisis énergétique","energy","energy price"]}, + {"key": "eurusd", "label": "EUR/USD", "weight": 0.75, "keywords": ["EUR/USD","euro dollar","eurusd","parité euro","euro fort","euro faible"]}, + {"key": "european_growth", "label": "Croissance Européenne", "weight": 0.7, "keywords": ["Europe","zone euro","Allemagne","PIB européen","PMI Europe","récession Europe","croissance européenne"]} ], "regime_labels": ["Global Risk-On", "USD Strength Drag", "Europe Recession", "Recovery", "Neutral"], - "event_keywords": ["ECB", "EU GDP", "Europe", "Germany", "BOJ", "Japan"], - "related_assets": ["EFA", "EAFE", "EUR", "EZU"], - "correlation_instruments": ["EURUSD=X", "EEM", "TLT"], - "ai_context": "EFA tracks developed international markets (Europe, Japan, Australia). Bullish on: EUR/JPY strength, ECB/BOJ dovishness, global reflation. Bearish on: USD strength (translation drag), European recession, geopolitical disruption. Watch: EUR/USD, German Bund yields, BOJ policy." + "event_keywords": ["ECB","EU GDP","Europe","Germany","BOJ","Japan","EUR"], + "related_assets": ["EFA","EAFE","EUR","EZU","SX5E"], + "correlation_instruments": ["EURUSD=X","EEM","TLT"], + "ai_context": "EFA tracks EAFE (Europe, Japan, Australia). Bullish: EUR/JPY strength, ECB/BOJ dovishness, global reflation. Bearish: USD strength, European recession, geopolitical disruption." }, { "id": "GLD", @@ -145,25 +120,20 @@ "exchange": "NYSE", "category": "metal", "currency": "USD", - "description": "SPDR Gold Shares — physical gold proxy", - "chart": { - "ma_periods": [50, 100, 200], - "bollinger_period": 50, - "bollinger_std": 2, - "show_volume": true - }, + "description": "SPDR Gold Shares — proxy physique de l'or", + "chart": { "ma_periods": [50, 100, 200], "bollinger_period": 50, "bollinger_std": 2, "show_volume": true }, "drivers": [ - {"key": "real_rates_us", "label": "Real Rates US", "weight": 0.95}, - {"key": "dollar_index", "label": "Dollar Index", "weight": 0.85}, - {"key": "inflation_expectations", "label": "Inflation Expectations", "weight": 0.75}, - {"key": "geopolitical_risk", "label": "Geopolitical Risk", "weight": 0.7}, - {"key": "cb_buying", "label": "Central Bank Buying", "weight": 0.5} + {"key": "real_rates_us", "label": "Taux Réels US", "weight": 0.95, "keywords": ["taux réels","TIPS","real yields","Fed","FOMC","CPI","breakeven","inflation réelle"]}, + {"key": "dollar_index", "label": "Dollar Index", "weight": 0.85, "keywords": ["Dollar","DXY","dollar index","USD","faible dollar","dollar fort"]}, + {"key": "inflation_expectations", "label": "Anticipations Inflation", "weight": 0.75, "keywords": ["inflation","CPI","PCE","breakeven","attentes d'inflation","stagflation"]}, + {"key": "geopolitical_risk", "label": "Risque Géopolitique", "weight": 0.7, "keywords": ["géopolitique","guerre","Moyen-Orient","Iran","Russie","conflit","Chine","tension","crise","war"]}, + {"key": "cb_buying", "label": "Achats Banques Centrales","weight": 0.5, "keywords": ["banque centrale","central bank","réserves","gold reserves","achats d'or","PBOC gold","dédollarisation"]} ], "regime_labels": ["Real Rates Falling", "Safe Haven Rally", "Inflation Hedge", "Dollar Weakness", "Range"], - "event_keywords": ["CPI", "Fed", "FOMC", "Dollar", "Geopolitical", "Iran", "Gold"], - "related_assets": ["GLD", "GC", "Gold", "XAU"], - "correlation_instruments": ["TLT", "SLV", "EURUSD=X", "USDJPY=X"], - "ai_context": "GLD tracks physical gold. Primary driver: real interest rates (inverse). Bullish on: falling real yields, dollar weakness, central bank buying, geopolitical crises. Bearish on: rising real yields, strong dollar, risk-on environment. Watch: TIPS yields, DXY, CB reserves." + "event_keywords": ["CPI","Fed","FOMC","Dollar","Geopolitical","Iran","Gold","guerre","inflation"], + "related_assets": ["GLD","GC","Gold","XAU"], + "correlation_instruments": ["TLT","SLV","EURUSD=X","USDJPY=X"], + "ai_context": "GLD tracks physical gold. Primary driver: real rates (inverse). Bullish: falling real yields, dollar weakness, CB buying, geopolitical crises. Bearish: rising real yields, strong dollar." }, { "id": "SLV", @@ -173,25 +143,20 @@ "exchange": "NYSE", "category": "metal", "currency": "USD", - "description": "iShares Silver Trust — physical silver proxy", - "chart": { - "ma_periods": [20, 50, 200], - "bollinger_period": 20, - "bollinger_std": 2, - "show_volume": true - }, + "description": "iShares Silver Trust — proxy physique de l'argent métal", + "chart": { "ma_periods": [20, 50, 200], "bollinger_period": 20, "bollinger_std": 2, "show_volume": true }, "drivers": [ - {"key": "industrial_demand", "label": "Industrial Demand", "weight": 0.8}, - {"key": "real_rates", "label": "Real Rates", "weight": 0.75}, - {"key": "dollar", "label": "Dollar Index", "weight": 0.7}, - {"key": "solar_ev_demand", "label": "Solar/EV Demand", "weight": 0.65}, - {"key": "gold_ratio", "label": "Gold/Silver Ratio", "weight": 0.6} + {"key": "industrial_demand", "label": "Demande Industrielle", "weight": 0.8, "keywords": ["industrie","manufacturing","PMI","production industrielle","fabrique","industry"]}, + {"key": "real_rates", "label": "Taux Réels", "weight": 0.75, "keywords": ["taux réels","TIPS","Fed","FOMC","real yields"]}, + {"key": "dollar", "label": "Dollar Index", "weight": 0.7, "keywords": ["Dollar","DXY","USD"]}, + {"key": "solar_ev_demand", "label": "Solaire / Véhicule EV","weight": 0.65, "keywords": ["solaire","solar","EV","véhicule électrique","énergie verte","green energy","photovoltaïque","battery"]}, + {"key": "gold_ratio", "label": "Ratio Or/Argent", "weight": 0.6, "keywords": ["Gold","or","ratio Or/Argent","gold silver","argent","silver"]} ], "regime_labels": ["Industrial Demand", "Safe Haven Flow", "Dollar Weakness", "Risk-Off", "Range"], - "event_keywords": ["Manufacturing", "PMI", "CPI", "Dollar", "Silver", "EV", "Solar"], - "related_assets": ["SLV", "SI", "Silver"], - "correlation_instruments": ["GLD", "USO", "EEM"], - "ai_context": "SLV tracks silver. Dual nature: monetary metal (like gold) + industrial metal (solar panels, EVs, electronics). Bullish on: green energy demand, falling real rates, weak dollar. Bearish on: manufacturing slowdown, strong dollar, risk-off. Watch: gold/silver ratio, PMI data." + "event_keywords": ["Manufacturing","PMI","CPI","Dollar","Silver","EV","Solar","taux réels"], + "related_assets": ["SLV","SI","Silver"], + "correlation_instruments": ["GLD","USO","EEM"], + "ai_context": "SLV tracks silver. Dual nature: monetary (like gold) + industrial (solar, EVs). Bullish: green energy demand, falling real rates, weak dollar. Bearish: manufacturing slowdown, strong dollar." }, { "id": "USO", @@ -201,25 +166,20 @@ "exchange": "NYSE", "category": "energy", "currency": "USD", - "description": "United States Oil Fund — WTI crude proxy", - "chart": { - "ma_periods": [20, 50, 200], - "bollinger_period": 20, - "bollinger_std": 2, - "show_volume": true - }, + "description": "United States Oil Fund — proxy WTI (pétrole brut américain)", + "chart": { "ma_periods": [20, 50, 200], "bollinger_period": 20, "bollinger_std": 2, "show_volume": true }, "drivers": [ - {"key": "opec_policy", "label": "OPEC Policy", "weight": 0.9}, - {"key": "inventories", "label": "EIA Inventories", "weight": 0.8}, - {"key": "geopolitical_risk", "label": "Geopolitical Risk", "weight": 0.75}, - {"key": "global_demand", "label": "Global Demand", "weight": 0.85}, - {"key": "dollar", "label": "Dollar Index", "weight": 0.7} + {"key": "opec_policy", "label": "OPEP+ / Production", "weight": 0.9, "keywords": ["OPEC","OPEP","production pétrolière","quotas","Arabie Saoudite","Saudi","réduction","coupes","OPEC meeting"]}, + {"key": "inventories", "label": "Stocks EIA", "weight": 0.8, "keywords": ["EIA","stocks pétroliers","inventaires","inventories","crude stocks","rapport EIA","crude supply"]}, + {"key": "geopolitical_risk","label": "Risque Géopolitique","weight": 0.75, "keywords": ["géopolitique","Moyen-Orient","Iran","Russie","conflit","guerre","war","Golfe Persique","tension"]}, + {"key": "global_demand", "label": "Demande Mondiale", "weight": 0.85, "keywords": ["demande mondiale","China demand","IEA","croissance mondiale","global demand","reprise économique"]}, + {"key": "dollar", "label": "Dollar Index", "weight": 0.7, "keywords": ["Dollar","DXY","USD"]} ], "regime_labels": ["Supply Shock", "Demand Recovery", "Inventory Draw", "Geopolitical Premium", "Neutral"], - "event_keywords": ["OPEC", "EIA", "Inventories", "Brent", "WTI", "Oil", "Middle East"], - "related_assets": ["USO", "CL", "WTI", "Oil", "Brent"], - "correlation_instruments": ["XOM", "EEM", "GLD", "UNG"], - "ai_context": "USO tracks WTI crude oil. Bullish on: OPEC+ cuts, geopolitical supply disruption, global demand recovery, weak dollar. Bearish on: OPEC+ output increases, demand recession, strong dollar, inventory builds. Watch: EIA weekly reports, OPEC meetings, Middle East tensions." + "event_keywords": ["OPEC","EIA","Inventories","Brent","WTI","Oil","Middle East","Saudi"], + "related_assets": ["USO","CL","WTI","Oil","Brent"], + "correlation_instruments": ["XOM","EEM","GLD","UNG"], + "ai_context": "USO tracks WTI crude. Bullish: OPEC+ cuts, geopolitical disruption, demand recovery, weak dollar. Bearish: OPEC output increases, demand recession, inventory builds." }, { "id": "UNG", @@ -229,24 +189,19 @@ "exchange": "NYSE", "category": "energy", "currency": "USD", - "description": "United States Natural Gas Fund — Henry Hub proxy", - "chart": { - "ma_periods": [20, 50, 200], - "bollinger_period": 20, - "bollinger_std": 2, - "show_volume": true - }, + "description": "United States Natural Gas Fund — proxy Henry Hub (gaz naturel)", + "chart": { "ma_periods": [20, 50, 200], "bollinger_period": 20, "bollinger_std": 2, "show_volume": true }, "drivers": [ - {"key": "weather_seasonality", "label": "Weather/Seasonality", "weight": 0.9}, - {"key": "storage", "label": "EIA Gas Storage", "weight": 0.85}, - {"key": "lng_exports", "label": "LNG Exports", "weight": 0.75}, - {"key": "production", "label": "US Production", "weight": 0.7} + {"key": "weather_seasonality", "label": "Météo / Saisonnalité","weight": 0.9, "keywords": ["météo","weather","hiver","été","froid","chaleur","El Niño","La Niña","saison","vague de froid","heat wave"]}, + {"key": "storage", "label": "Stockage EIA Gaz", "weight": 0.85, "keywords": ["EIA Gas Storage","stockage gaz","gas storage","inventaires gaz","rapport stockage gaz"]}, + {"key": "lng_exports", "label": "Exports LNG", "weight": 0.75, "keywords": ["LNG","GNL","exportations gaz","terminal LNG","gazoduc","liquefied natural gas"]}, + {"key": "production", "label": "Production US", "weight": 0.7, "keywords": ["production gaz","shale gas","fracking","puits gaz","Marcellus","Appalachian","US gas output"]} ], "regime_labels": ["Winter Storage Fill", "Supply Glut", "Demand Surge", "Export LNG Lift", "Neutral"], - "event_keywords": ["EIA Gas Storage", "LNG", "Gas", "Natural Gas", "Weather"], - "related_assets": ["UNG", "NG", "Natgas"], - "correlation_instruments": ["USO", "XOM"], - "ai_context": "UNG tracks natural gas (Henry Hub). Highly seasonal and weather-driven. Bullish on: cold winter demand, low storage levels, LNG export growth, production cuts. Bearish on: warm weather, record storage builds, weak LNG prices. Watch: weekly EIA storage report, weather forecasts." + "event_keywords": ["EIA Gas Storage","LNG","Gas","Natural Gas","Weather","stockage gaz"], + "related_assets": ["UNG","NG","Natgas"], + "correlation_instruments": ["USO","XOM"], + "ai_context": "UNG tracks natural gas (Henry Hub). Highly seasonal and weather-driven. Bullish: cold winter, low storage, LNG export growth. Bearish: warm weather, record storage builds." }, { "id": "TLT", @@ -256,25 +211,20 @@ "exchange": "NASDAQ", "category": "bond", "currency": "USD", - "description": "iShares 20+ Year Treasury Bond ETF", - "chart": { - "ma_periods": [20, 50, 200], - "bollinger_period": 20, - "bollinger_std": 2, - "show_volume": true - }, + "description": "iShares 20+ Year Treasury — proxy du rendement US 10Y/30Y", + "chart": { "ma_periods": [20, 50, 200], "bollinger_period": 20, "bollinger_std": 2, "show_volume": true }, "drivers": [ - {"key": "fed_path", "label": "Fed Path", "weight": 0.9}, - {"key": "inflation_expectations", "label": "Inflation Expectations", "weight": 0.85}, - {"key": "growth", "label": "Economic Growth", "weight": 0.75}, - {"key": "deficit_supply", "label": "Deficit/Supply", "weight": 0.7}, - {"key": "risk_aversion", "label": "Risk Aversion", "weight": 0.65} + {"key": "fed_path", "label": "Trajectoire Fed", "weight": 0.9, "keywords": ["FOMC","Fed","taux directeur","Fed hike","Fed cut","pivot","dot plot","Federal Reserve"]}, + {"key": "inflation_expectations", "label": "Inflation / CPI", "weight": 0.85, "keywords": ["CPI","PCE","inflation","déflation","attentes","breakeven","inflation US"]}, + {"key": "growth", "label": "Croissance / NFP", "weight": 0.75, "keywords": ["GDP","NFP","récession","croissance","emploi","PMI","chômage","payrolls"]}, + {"key": "deficit_supply", "label": "Déficit / Offre Treasury","weight": 0.7, "keywords": ["déficit","dette publique","Treasury auction","enchères","budget","fiscal","dette fédérale"]}, + {"key": "risk_aversion", "label": "Fuite Vers Qualité", "weight": 0.65, "keywords": ["risk-off","crise","fuite vers la qualité","safe haven","vol spike","contagion","crise bancaire"]} ], "regime_labels": ["Safe Haven Bid", "Rate Shock Selloff", "QE Expectations", "Inflation Selloff", "Range"], - "event_keywords": ["FOMC", "CPI", "Fed", "Treasury", "Yield", "Deficit"], - "related_assets": ["TLT", "TNX", "UST", "Bonds"], - "correlation_instruments": ["SPY", "GLD", "HYG", "USDJPY=X"], - "ai_context": "TLT tracks 20+ year US Treasuries. Bullish on: recession fears, Fed rate cuts, falling inflation, risk-off flows, QE expectations. Bearish on: rising inflation, Fed hikes, fiscal deficit expansion, strong growth. Watch: 10Y yield, breakevens, FOMC dots." + "event_keywords": ["FOMC","CPI","Fed","Treasury","Yield","Deficit","NFP"], + "related_assets": ["TLT","TNX","UST","Bonds"], + "correlation_instruments": ["SPY","GLD","HYG","USDJPY=X"], + "ai_context": "TLT tracks 20Y US Treasuries. Bullish: recession fears, Fed cuts, falling inflation, risk-off. Bearish: rising inflation, Fed hikes, fiscal deficit expansion, strong growth." }, { "id": "HYG", @@ -284,24 +234,19 @@ "exchange": "NYSE", "category": "credit", "currency": "USD", - "description": "iShares iBoxx USD High Yield Corporate Bond ETF", - "chart": { - "ma_periods": [20, 50, 200], - "bollinger_period": 20, - "bollinger_std": 2, - "show_volume": true - }, + "description": "iShares iBoxx USD High Yield Corporate Bond — baromètre du crédit HY", + "chart": { "ma_periods": [20, 50, 200], "bollinger_period": 20, "bollinger_std": 2, "show_volume": true }, "drivers": [ - {"key": "default_rate", "label": "Default Rate", "weight": 0.85}, - {"key": "growth", "label": "Economic Growth", "weight": 0.8}, - {"key": "liquidity", "label": "Liquidity Conditions", "weight": 0.8}, - {"key": "risk_appetite", "label": "Risk Appetite", "weight": 0.75} + {"key": "default_rate", "label": "Taux de Défaut", "weight": 0.85, "keywords": ["défauts","default","faillite","bankruptcy","spreads HY","taux de défaut","HY spreads"]}, + {"key": "growth", "label": "Croissance Économique", "weight": 0.8, "keywords": ["GDP","récession","croissance","PMI","emploi","payrolls","activité"]}, + {"key": "liquidity", "label": "Conditions de Liquidité","weight": 0.8, "keywords": ["liquidité","crédit","QT","QE","conditions financières","FCO","financial conditions"]}, + {"key": "risk_appetite", "label": "Appétit pour le Risque", "weight": 0.75, "keywords": ["risk-on","risk-off","appétit pour le risque","flux crédit","sentiment marché"]} ], "regime_labels": ["Credit Expansion", "Credit Crunch", "Spread Compression", "Recession Fear", "Neutral"], - "event_keywords": ["Credit", "Default", "GDP", "FOMC", "Recession", "HY"], - "related_assets": ["HYG", "LQD", "Credit", "HY"], - "correlation_instruments": ["SPY", "IWM", "TLT"], - "ai_context": "HYG tracks US high-yield corporate bonds. A key credit stress barometer. Bullish on: strong growth, low defaults, liquidity expansion, risk appetite. Bearish on: recession, default cycle, credit crunch, liquidity withdrawal. Watch: OAS spreads vs Treasuries, default rates, IWM correlation." + "event_keywords": ["Credit","Default","GDP","FOMC","Recession","HY","spreads"], + "related_assets": ["HYG","LQD","Credit","HY"], + "correlation_instruments": ["SPY","IWM","TLT"], + "ai_context": "HYG tracks US high-yield corporate bonds. Key credit stress barometer. Bullish: strong growth, low defaults, liquidity expansion. Bearish: recession, default cycle, credit crunch." }, { "id": "EURUSD=X", @@ -311,24 +256,19 @@ "exchange": "FX", "category": "fx", "currency": "USD", - "description": "Euro vs US Dollar spot rate", - "chart": { - "ma_periods": [20, 50, 200], - "bollinger_period": 20, - "bollinger_std": 2, - "show_volume": false - }, + "description": "Taux spot Euro / Dollar américain", + "chart": { "ma_periods": [20, 50, 200], "bollinger_period": 20, "bollinger_std": 2, "show_volume": false }, "drivers": [ - {"key": "fed_ecb_differential", "label": "Fed vs ECB Differential", "weight": 0.95}, - {"key": "rate_differential_2y", "label": "Rate Differential 2Y", "weight": 0.85}, - {"key": "growth_differential", "label": "Growth Differential", "weight": 0.75}, - {"key": "risk_appetite", "label": "Risk Appetite", "weight": 0.7} + {"key": "fed_ecb_differential", "label": "Différentiel Fed/BCE", "weight": 0.95, "keywords": ["BCE","ECB","Fed","FOMC","différentiel","taux directeurs","politique monétaire","rate differential"]}, + {"key": "rate_differential_2y", "label": "Taux 2 ans US-EUR", "weight": 0.85, "keywords": ["taux 2 ans","Bund 2Y","Treasury 2Y","swap","différentiel de taux","2Y spread"]}, + {"key": "inflation_differential", "label": "Inflation US vs EUR", "weight": 0.75, "keywords": ["CPI","inflation","PCE","HICP","zone euro","inflation différentiel"]}, + {"key": "dxy", "label": "Dollar Index DXY", "weight": 0.7, "keywords": ["Dollar","DXY","dollar index","USD","dollar fort","faible dollar"]} ], "regime_labels": ["EUR Strength (ECB Hawkish)", "USD Strength (Fed Hawkish)", "Risk-On EUR Rally", "Risk-Off USD Safe", "Range"], - "event_keywords": ["ECB", "FOMC", "CPI", "EUR", "Dollar", "Fed", "Eurozone"], - "related_assets": ["EUR", "USD", "EURUSD", "6E"], - "correlation_instruments": ["GLD", "EFA", "TLT"], - "ai_context": "EUR/USD is driven by the Fed-ECB rate differential. EUR bullish on: ECB hawkishness relative to Fed, European growth surprise, risk-on flows. EUR bearish on: Fed hawkishness, European recession, risk-off USD demand. Watch: 2Y Bund-Treasury spread, ECB/Fed meeting dates." + "event_keywords": ["ECB","FOMC","CPI","EUR","Dollar","Fed","Eurozone","BCE"], + "related_assets": ["EUR","USD","EURUSD","6E"], + "correlation_instruments": ["GLD","EFA","TLT"], + "ai_context": "EUR/USD driven by Fed-ECB rate differential. EUR bullish: ECB hawkishness, European growth surprise, risk-on. EUR bearish: Fed hawkishness, European recession, risk-off USD demand." }, { "id": "USDJPY=X", @@ -338,24 +278,19 @@ "exchange": "FX", "category": "fx", "currency": "JPY", - "description": "US Dollar vs Japanese Yen spot rate", - "chart": { - "ma_periods": [20, 50, 200], - "bollinger_period": 20, - "bollinger_std": 2, - "show_volume": false - }, + "description": "Taux spot Dollar américain / Yen japonais", + "chart": { "ma_periods": [20, 50, 200], "bollinger_period": 20, "bollinger_std": 2, "show_volume": false }, "drivers": [ - {"key": "boj_policy", "label": "BOJ Policy", "weight": 0.95}, - {"key": "rate_diff_us_jp", "label": "Rate Differential US-JP", "weight": 0.9}, - {"key": "fed_policy", "label": "Fed Policy", "weight": 0.85}, - {"key": "risk_appetite", "label": "Risk Appetite", "weight": 0.7} + {"key": "boj_policy", "label": "BOJ Policy", "weight": 0.95, "keywords": ["BOJ","Banque du Japon","Ueda","YCC","yield curve control","politique monétaire japonaise","BOJ rate","JGB"]}, + {"key": "rate_diff_us_jp","label": "Différentiel US-JP","weight": 0.9, "keywords": ["différentiel US-Japon","JGB","UST","rendements US-JP","US Japan spread","10Y spread"]}, + {"key": "fed_policy", "label": "Fed Policy", "weight": 0.85, "keywords": ["Fed","FOMC","taux directeur US","hawkish Fed","Fed hike","Federal Reserve"]}, + {"key": "risk_appetite", "label": "Risk Appetite / Carry","weight": 0.7, "keywords": ["risk-off","carry trade","yen safe haven","crise","carry unwind","yen refuge"]} ], "regime_labels": ["Carry Trade On", "BOJ Tightening Surprise", "Risk-Off JPY Safe Haven", "Dollar Bull", "YCC Adjustment"], - "event_keywords": ["BOJ", "Fed", "Japan", "YCC", "Carry", "JPY", "Yen"], - "related_assets": ["JPY", "USDJPY", "6J", "Yen"], - "correlation_instruments": ["TLT", "GLD", "VXX"], - "ai_context": "USD/JPY is primarily driven by the US-Japan rate differential and BOJ yield curve control. USD bullish on: Fed hikes, BOJ ultra-dovish, risk-on carry. JPY bullish on: BOJ tightening surprise, risk-off crisis, US yield decline. Watch: BOJ meetings, 10Y UST-JGB spread, carry unwind signals." + "event_keywords": ["BOJ","Fed","Japan","YCC","Carry","JPY","Yen","taux Japon"], + "related_assets": ["JPY","USDJPY","6J","Yen"], + "correlation_instruments": ["TLT","GLD","VXX"], + "ai_context": "USD/JPY driven by US-Japan rate differential and BOJ yield curve control. USD bullish: Fed hikes, BOJ ultra-dovish. JPY bullish: BOJ tightening surprise, risk-off. Watch: BOJ meetings, 10Y UST-JGB spread." }, { "id": "GBPUSD=X", @@ -365,24 +300,19 @@ "exchange": "FX", "category": "fx", "currency": "USD", - "description": "British Pound vs US Dollar spot rate", - "chart": { - "ma_periods": [20, 50, 200], - "bollinger_period": 20, - "bollinger_std": 2, - "show_volume": false - }, + "description": "Taux spot Livre Sterling / Dollar américain", + "chart": { "ma_periods": [20, 50, 200], "bollinger_period": 20, "bollinger_std": 2, "show_volume": false }, "drivers": [ - {"key": "boe_policy", "label": "BOE Policy", "weight": 0.9}, - {"key": "fed_policy", "label": "Fed Policy", "weight": 0.85}, - {"key": "uk_growth", "label": "UK Growth", "weight": 0.75}, - {"key": "political_risk", "label": "Political Risk UK", "weight": 0.65} + {"key": "boe_policy", "label": "BOE Policy", "weight": 0.9, "keywords": ["BOE","Bank of England","Bailey","taux UK","Royaume-Uni","Angleterre","UK rate","BOE meeting"]}, + {"key": "uk_inflation", "label": "Inflation UK", "weight": 0.8, "keywords": ["inflation UK","CPI UK","CPIH","RPI","inflation Angleterre"]}, + {"key": "uk_growth", "label": "Croissance UK", "weight": 0.75, "keywords": ["GDP UK","PMI UK","croissance UK","récession UK","UK economy","UK GDP"]}, + {"key": "political_risk","label": "Risque Politique UK","weight": 0.65, "keywords": ["politique UK","budget UK","Labour","Tories","fiscal UK","gilt","UK budget","Reeves"]} ], "regime_labels": ["GBP Strength (BOE Hawkish)", "USD Dominance", "Political Risk GBP", "UK Recession Pressure", "Range"], - "event_keywords": ["BOE", "Fed", "UK", "Britain", "Brexit", "GBP", "Pound"], - "related_assets": ["GBP", "GBPUSD", "6B", "Pound"], - "correlation_instruments": ["EURUSD=X", "TLT"], - "ai_context": "GBP/USD reflects the BOE vs Fed rate differential plus UK political risk premium. GBP bullish on: BOE hawkishness, strong UK data, political stability. GBP bearish on: UK recession, political uncertainty, Fed dominance, risk-off. Watch: UK CPI, BOE meetings, UK gilt yields." + "event_keywords": ["BOE","Fed","UK","Britain","GBP","Pound","UK CPI"], + "related_assets": ["GBP","GBPUSD","6B","Pound"], + "correlation_instruments": ["EURUSD=X","TLT"], + "ai_context": "GBP/USD reflects BOE vs Fed differential plus UK political risk premium. GBP bullish: BOE hawkishness, strong UK data, political stability. GBP bearish: UK recession, political uncertainty." }, { "id": "VXX", @@ -392,24 +322,19 @@ "exchange": "CBOE", "category": "volatility", "currency": "USD", - "description": "iPath Series B S&P 500 VIX Short-Term Futures ETN", - "chart": { - "ma_periods": [10, 20, 50], - "bollinger_period": 20, - "bollinger_std": 2, - "show_volume": true - }, + "description": "iPath VIX Short-Term Futures ETN — proxy de la peur et de la volatilité implicite", + "chart": { "ma_periods": [10, 20, 50], "bollinger_period": 20, "bollinger_std": 2, "show_volume": true }, "drivers": [ - {"key": "market_stress", "label": "Market Stress", "weight": 0.9}, - {"key": "risk_appetite", "label": "Risk Appetite", "weight": 0.85}, - {"key": "options_demand", "label": "Options Demand", "weight": 0.75}, - {"key": "macro_uncertainty", "label": "Macro Uncertainty", "weight": 0.7} + {"key": "market_stress", "label": "Stress de Marché", "weight": 0.9, "keywords": ["krach","crash","crise","choc","panic","volatilité","vol spike","selloff","capitulation","contagion"]}, + {"key": "risk_appetite", "label": "Peur / Risk-Off", "weight": 0.85, "keywords": ["risk-off","risk-on","sentiment","fear","peur","greed","appétit risque","VIX spike"]}, + {"key": "options_demand", "label": "Demande Options (Puts)", "weight": 0.75, "keywords": ["options","puts","hedging","protection","skew","IV","couverture","implied vol","volatilité implicite"]}, + {"key": "macro_uncertainty", "label": "Incertitude Macro", "weight": 0.7, "keywords": ["FOMC","surprise","incertitude","géopolitique","macro","inflation choc","data surprise"]} ], "regime_labels": ["Low Vol Complacency", "Vol Spike Crisis", "Vol Expansion", "Vol Compression", "Neutral"], - "event_keywords": ["VIX", "Volatility", "Crisis", "FOMC", "Market Stress"], - "related_assets": ["VXX", "VIX", "UVXY", "SVXY"], - "correlation_instruments": ["SPY", "TLT", "GLD"], - "ai_context": "VXX tracks short-term VIX futures (fear gauge). Spikes on: market crashes, geopolitical shocks, FOMC surprises, credit events. Mean-reverts in calm markets due to futures roll decay. Useful for hedging equity drawdowns. Watch: VIX term structure, SPX put skew, macro event calendar." + "event_keywords": ["VIX","Volatility","Crisis","FOMC","Market Stress","peur","vol spike"], + "related_assets": ["VXX","VIX","UVXY","SVXY"], + "correlation_instruments": ["SPY","TLT","GLD"], + "ai_context": "VXX tracks short-term VIX futures. Spikes on: market crashes, geopolitical shocks, FOMC surprises. Mean-reverts due to futures roll decay. Watch: VIX term structure, SPX put skew." }, { "id": "AAPL", @@ -419,25 +344,20 @@ "exchange": "NASDAQ", "category": "stock", "currency": "USD", - "description": "Apple Inc. — consumer tech, services ecosystem", - "chart": { - "ma_periods": [20, 50, 200], - "bollinger_period": 20, - "bollinger_std": 2, - "show_volume": true - }, + "description": "Apple Inc. — ecosystème consommateur tech et services", + "chart": { "ma_periods": [20, 50, 200], "bollinger_period": 20, "bollinger_std": 2, "show_volume": true }, "drivers": [ - {"key": "iphone_cycle", "label": "iPhone Cycle", "weight": 0.85}, - {"key": "china_sales", "label": "China Sales", "weight": 0.8}, - {"key": "fed_policy", "label": "Fed Policy", "weight": 0.8}, - {"key": "ai_integration", "label": "AI Integration", "weight": 0.75}, - {"key": "margins", "label": "Services Margins", "weight": 0.7} + {"key": "iphone_cycle", "label": "Cycle iPhone", "weight": 0.85, "keywords": ["iPhone","Apple","AAPL earnings","supercycle","launch","iPhone 16","hardware"]}, + {"key": "china_sales", "label": "Ventes Chine", "weight": 0.8, "keywords": ["Chine","China","Huawei","ban Apple","restrictions Apple","China sales","marché chinois"]}, + {"key": "ai_integration","label": "Apple Intelligence / IA","weight": 0.75, "keywords": ["Apple Intelligence","AI","IA","Siri","machine learning","IA Apple","generative AI"]}, + {"key": "margins", "label": "Services & Marges", "weight": 0.7, "keywords": ["services","App Store","iCloud","marge Apple","services revenue","abonnements"]}, + {"key": "fed_policy", "label": "Valorisation / Taux", "weight": 0.65, "keywords": ["Fed","taux","valorisation","taux d'intérêt","discount rate"]} ], "regime_labels": ["Growth Bull", "Rate Squeeze", "Earnings Beat", "Tech Selloff", "Neutral"], - "event_keywords": ["Apple Earnings", "iPhone", "Services", "China", "Fed", "AI", "AAPL"], - "related_assets": ["AAPL", "Apple"], - "correlation_instruments": ["QQQ", "NVDA", "SPY"], - "ai_context": "AAPL is the world's largest company by market cap. Key drivers: iPhone supercycle, Services revenue growth (highest-margin), China exposure. Bullish on: new product launches, AI integration (Apple Intelligence), services acceleration. Bearish on: China ban risk, rate hikes, iPhone demand slowdown. Watch: quarterly earnings, China sales data." + "event_keywords": ["Apple Earnings","iPhone","Services","China","Fed","AI","AAPL"], + "related_assets": ["AAPL","Apple"], + "correlation_instruments": ["QQQ","NVDA","SPY"], + "ai_context": "AAPL is world's largest market cap. Key drivers: iPhone cycle, Services revenue growth, China exposure, AI integration. Watch: quarterly earnings, China sales, Apple Intelligence traction." }, { "id": "NVDA", @@ -447,25 +367,20 @@ "exchange": "NASDAQ", "category": "stock", "currency": "USD", - "description": "NVIDIA Corporation — AI chips and data center GPU leader", - "chart": { - "ma_periods": [20, 50, 200], - "bollinger_period": 20, - "bollinger_std": 2, - "show_volume": true - }, + "description": "NVIDIA Corporation — leader mondial des GPU pour IA et data centers", + "chart": { "ma_periods": [20, 50, 200], "bollinger_period": 20, "bollinger_std": 2, "show_volume": true }, "drivers": [ - {"key": "ai_capex_cycle", "label": "AI Capex Cycle", "weight": 0.95}, - {"key": "earnings_growth", "label": "Earnings Growth", "weight": 0.9}, - {"key": "data_center_demand", "label": "Data Center Demand", "weight": 0.85}, - {"key": "export_restrictions", "label": "Export Restrictions", "weight": 0.7}, - {"key": "rates", "label": "Interest Rates", "weight": 0.6} + {"key": "ai_capex_cycle", "label": "Capex IA / Hyperscalers","weight": 0.95, "keywords": ["IA","AI","data center","capex","Nvidia","GPU","H100","Blackwell","NVLink","hyperscalers","infrastructure IA"]}, + {"key": "earnings_growth", "label": "Croissance Bénéfices", "weight": 0.9, "keywords": ["NVDA earnings","résultats Nvidia","revenues","data center revenue","beat","guidance"]}, + {"key": "data_center_demand","label": "Demande Data Center", "weight": 0.85, "keywords": ["Amazon AWS","Microsoft Azure","Google Cloud","data center","cloud","computing","GPU demand"]}, + {"key": "export_restrictions","label": "Restrictions Export", "weight": 0.7, "keywords": ["export control","restrictions export","Chine","China ban","BIS","CISA","H20","chip ban"]}, + {"key": "rates", "label": "Taux / Valorisation", "weight": 0.6, "keywords": ["taux","valorisation","growth stock","multiples","discount rate","Fed"]} ], "regime_labels": ["AI Bull Run", "AI Hype Correction", "Earnings Catalyst", "Rate Compression", "Consolidation"], - "event_keywords": ["NVIDIA Earnings", "AI", "Data Center", "Export", "GPU", "NVDA"], - "related_assets": ["NVDA", "NVIDIA", "AI"], - "correlation_instruments": ["QQQ", "AAPL", "SPY"], - "ai_context": "NVDA is the dominant AI infrastructure supplier (H100/H200/Blackwell GPUs). Bullish on: AI capex spending by hyperscalers, earnings beats, new GPU architecture launches. Bearish on: US-China export restrictions, AI spending slowdown, competitive pressure from AMD/Intel. Watch: quarterly data center revenue, export control updates." + "event_keywords": ["NVIDIA Earnings","AI","Data Center","Export","GPU","NVDA","Blackwell"], + "related_assets": ["NVDA","NVIDIA","AI"], + "correlation_instruments": ["QQQ","AAPL","SPY"], + "ai_context": "NVDA dominates AI infrastructure (H100/Blackwell GPUs). Bullish: AI capex spending, earnings beats, new GPU launches. Bearish: US-China export restrictions, AI spending slowdown." }, { "id": "GS", @@ -475,24 +390,19 @@ "exchange": "NYSE", "category": "stock", "currency": "USD", - "description": "The Goldman Sachs Group — global investment bank", - "chart": { - "ma_periods": [20, 50, 200], - "bollinger_period": 20, - "bollinger_std": 2, - "show_volume": true - }, + "description": "The Goldman Sachs Group — banque d'investissement globale", + "chart": { "ma_periods": [20, 50, 200], "bollinger_period": 20, "bollinger_std": 2, "show_volume": true }, "drivers": [ - {"key": "yield_curve", "label": "Yield Curve", "weight": 0.85}, - {"key": "ma_activity", "label": "M&A Activity", "weight": 0.8}, - {"key": "rates", "label": "Interest Rates", "weight": 0.8}, - {"key": "credit_conditions", "label": "Credit Conditions", "weight": 0.75} + {"key": "yield_curve", "label": "Courbe des Taux", "weight": 0.85, "keywords": ["courbe des taux","2Y-10Y","steepening","inversion","yield curve","10Y-2Y","pente"]}, + {"key": "ma_activity", "label": "M&A / Activité Deals", "weight": 0.8, "keywords": ["M&A","fusion-acquisition","LBO","IPO","deal","advisory","boutique","transaction"]}, + {"key": "rates", "label": "Taux d'Intérêt", "weight": 0.8, "keywords": ["taux","Fed","intérêts","NIM","net interest margin","Fed rate","interest rates"]}, + {"key": "credit_conditions","label": "Conditions de Crédit", "weight": 0.75, "keywords": ["crédit","spreads","conditions financières","leverage loans","credit tightening"]} ], "regime_labels": ["Finance Bull", "Recession Fear", "Yield Curve Steepen", "M&A Boom", "Range"], - "event_keywords": ["Goldman", "Financials", "Yield Curve", "M&A", "Banking", "Fed", "Credit"], - "related_assets": ["GS", "Goldman", "XLF", "Financials"], - "correlation_instruments": ["SPY", "TLT", "HYG"], - "ai_context": "Goldman Sachs benefits from: steep yield curve (NIM expansion), M&A advisory boom, robust fixed income trading. Bullish on: rising rates + yield curve steepening, IPO/M&A surge, strong trading volumes. Bearish on: yield curve inversion, recession (deal freeze), credit crisis. Watch: 2Y-10Y spread, IPO pipeline, trading revenues." + "event_keywords": ["Goldman","Financials","Yield Curve","M&A","Banking","Fed","Credit"], + "related_assets": ["GS","Goldman","XLF","Financials"], + "correlation_instruments": ["SPY","TLT","HYG"], + "ai_context": "Goldman benefits from: steep yield curve, M&A advisory boom, robust trading. Bullish: rate curve steepening, IPO/M&A surge. Bearish: yield curve inversion, recession, credit crisis." }, { "id": "XOM", @@ -502,25 +412,20 @@ "exchange": "NYSE", "category": "stock", "currency": "USD", - "description": "Exxon Mobil Corporation — global integrated energy major", - "chart": { - "ma_periods": [20, 50, 200], - "bollinger_period": 20, - "bollinger_std": 2, - "show_volume": true - }, + "description": "Exxon Mobil Corporation — major pétrolier intégré mondial", + "chart": { "ma_periods": [20, 50, 200], "bollinger_period": 20, "bollinger_std": 2, "show_volume": true }, "drivers": [ - {"key": "oil_price", "label": "Oil Price", "weight": 0.9}, - {"key": "global_demand", "label": "Global Demand", "weight": 0.8}, - {"key": "opec_policy", "label": "OPEC Policy", "weight": 0.75}, - {"key": "capex", "label": "Capital Expenditure", "weight": 0.6}, - {"key": "dollar", "label": "Dollar Index", "weight": 0.6} + {"key": "oil_price", "label": "Prix du Pétrole", "weight": 0.9, "keywords": ["WTI","Brent","pétrole","crude","oil price","prix pétrole","cours pétrole"]}, + {"key": "global_demand", "label": "Demande Mondiale", "weight": 0.8, "keywords": ["demande mondiale","China demand","IEA","croissance","global demand","reprise"]}, + {"key": "opec_policy", "label": "OPEP+ Production", "weight": 0.75, "keywords": ["OPEC","OPEP","quotas","production","Saudi","Arabie Saoudite","réduction"]}, + {"key": "capex", "label": "Capex / Investissement","weight": 0.6, "keywords": ["capex","investissement","forage","upstream","exploration","rig count"]}, + {"key": "dollar", "label": "Dollar Index", "weight": 0.6, "keywords": ["Dollar","DXY","USD"]} ], "regime_labels": ["Energy Bull", "Demand Slowdown", "OPEC Benefit", "Green Transition Drag", "Range"], - "event_keywords": ["Oil", "OPEC", "Energy", "XOM", "EIA", "Crude", "Exxon"], - "related_assets": ["XOM", "Exxon", "CL", "Oil", "Energy"], - "correlation_instruments": ["USO", "UNG", "EEM"], - "ai_context": "XOM is the largest US oil major. Revenues are highly correlated to oil and gas prices. Bullish on: OPEC cuts, geopolitical supply risk, refining margin expansion. Bearish on: oil price collapse, global recession, accelerating energy transition. Watch: WTI/Brent prices, OPEC+ decisions, quarterly earnings." + "event_keywords": ["Oil","OPEC","Energy","XOM","EIA","Crude","Exxon","WTI"], + "related_assets": ["XOM","Exxon","CL","Oil","Energy"], + "correlation_instruments": ["USO","UNG","EEM"], + "ai_context": "XOM revenues highly correlated to oil/gas prices. Bullish: OPEC cuts, geopolitical supply risk, refining margins. Bearish: oil price collapse, global recession, energy transition." }, { "id": "BTC-USD", @@ -530,25 +435,20 @@ "exchange": "Crypto", "category": "crypto", "currency": "USD", - "description": "Bitcoin — leading decentralized digital asset", - "chart": { - "ma_periods": [20, 50, 200], - "bollinger_period": 20, - "bollinger_std": 2, - "show_volume": true - }, + "description": "Bitcoin — premier actif décentralisé, indicateur de liquidité et Risk-On", + "chart": { "ma_periods": [20, 50, 200], "bollinger_period": 20, "bollinger_std": 2, "show_volume": true }, "drivers": [ - {"key": "global_liquidity", "label": "Global Liquidity", "weight": 0.85}, - {"key": "etf_flows", "label": "ETF Flows", "weight": 0.85}, - {"key": "risk_appetite", "label": "Risk Appetite", "weight": 0.8}, - {"key": "regulatory", "label": "Regulatory Environment", "weight": 0.7}, - {"key": "dollar", "label": "Dollar Index", "weight": 0.65} + {"key": "global_liquidity","label": "Liquidité Mondiale", "weight": 0.85, "keywords": ["liquidité mondiale","M2","Fed","QE","QT","conditions financières","liquidity","money supply"]}, + {"key": "etf_flows", "label": "Flux ETF Bitcoin", "weight": 0.85, "keywords": ["ETF Bitcoin","spot BTC ETF","BlackRock","Fidelity","flux ETF","ETF approval","IBIT","FBTC"]}, + {"key": "risk_appetite", "label": "Sentiment Risk-On", "weight": 0.8, "keywords": ["risk-on","risk-off","crypto","sentiment","appétit pour le risque","halving","cycle crypto"]}, + {"key": "regulatory", "label": "Réglementation Crypto", "weight": 0.7, "keywords": ["réglementation crypto","SEC","CFTC","crypto ban","ETF approval","régulation","MiCA"]}, + {"key": "dollar", "label": "Dollar Index", "weight": 0.65, "keywords": ["Dollar","DXY","USD","dédollarisation"]} ], "regime_labels": ["Crypto Bull (ETF Inflows)", "Risk-Off Crypto Selloff", "Liquidity Expansion", "Regulatory Fear", "Consolidation"], - "event_keywords": ["Bitcoin", "BTC", "Crypto", "ETF", "Fed", "Liquidity", "Regulation"], - "related_assets": ["BTC", "Bitcoin", "Crypto"], - "correlation_instruments": ["SPY", "QQQ", "GLD"], - "ai_context": "Bitcoin is the dominant crypto asset. Acts as a high-beta risk asset and an emerging macro hedge. Bullish on: Fed liquidity expansion, spot ETF inflows, halving cycle, institutional adoption. Bearish on: regulatory crackdown, risk-off markets, Fed tightening, crypto-specific contagion. Watch: spot BTC ETF flows, M2 money supply, halving cycle timing." + "event_keywords": ["Bitcoin","BTC","Crypto","ETF","Fed","Liquidity","Regulation","halving"], + "related_assets": ["BTC","Bitcoin","Crypto"], + "correlation_instruments": ["SPY","QQQ","GLD"], + "ai_context": "Bitcoin: high-beta risk asset + emerging macro hedge. Bullish: Fed liquidity expansion, spot ETF inflows, halving cycle. Bearish: regulatory crackdown, risk-off, Fed tightening." } ] } diff --git a/backend/routers/instruments.py b/backend/routers/instruments.py index 41a31dc..f1c3b9e 100644 --- a/backend/routers/instruments.py +++ b/backend/routers/instruments.py @@ -3,6 +3,7 @@ Instrument Dashboard Router. Exposes per-instrument snapshot (price, indicators, regime, trend, events) and AI narrative. """ from fastapi import APIRouter, HTTPException, Query +from pydantic import BaseModel from typing import List, Dict, Any, Optional from services.instrument_service import ( @@ -10,8 +11,12 @@ from services.instrument_service import ( get_instrument, get_snapshot, get_narrative, + update_instrument_drivers, ) +class DriverUpdate(BaseModel): + drivers: List[Dict[str, Any]] + router = APIRouter(prefix="/api/instruments", tags=["instruments"]) @@ -63,3 +68,20 @@ async def generate_narrative( "instrument_name": config.get("name", instrument_id), "narrative": narrative, } + + +@router.put("/{instrument_id}/drivers") +def update_drivers(instrument_id: str, body: DriverUpdate) -> Dict[str, Any]: + """ + Persist updated drivers (label, weight, keywords) for an instrument to instruments.json. + """ + config = get_instrument(instrument_id) + if not config: + raise HTTPException(status_code=404, detail=f"Instrument '{instrument_id}' not found") + + try: + update_instrument_drivers(instrument_id, body.drivers) + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + + return {"ok": True, "instrument_id": instrument_id.upper(), "drivers_count": len(body.drivers)} diff --git a/backend/services/instrument_service.py b/backend/services/instrument_service.py index 9527b7a..f8ec360 100644 --- a/backend/services/instrument_service.py +++ b/backend/services/instrument_service.py @@ -42,6 +42,33 @@ def get_instrument(instrument_id: str) -> Optional[Dict]: return _configs.get(instrument_id.upper()) +def update_instrument_drivers(instrument_id: str, drivers: List[Dict]) -> None: + """Persist updated drivers to instruments.json and refresh in-memory config.""" + global _configs + if _configs is None: + _load_configs() + + uid = instrument_id.upper() + if uid not in _configs: + raise ValueError(f"Instrument {uid} not found") + + # Load raw JSON, update the matching instrument, save back + with open(CONFIG_PATH, "r", encoding="utf-8") as f: + raw = json.load(f) + + for inst in raw["instruments"]: + if inst["id"] == uid: + inst["drivers"] = drivers + break + + with open(CONFIG_PATH, "w", encoding="utf-8") as f: + json.dump(raw, f, ensure_ascii=False, indent=2) + + # Refresh in-memory cache + _configs[uid]["drivers"] = drivers + logger.info(f"[instrument_service] Updated drivers for {uid} ({len(drivers)} drivers)") + + # ── DataFrame helpers ────────────────────────────────────────────────────────── def _ohlcv_to_df(records: List[Dict]) -> pd.DataFrame: diff --git a/frontend/src/pages/InstrumentDashboard.tsx b/frontend/src/pages/InstrumentDashboard.tsx index 94bf833..bc534ca 100644 --- a/frontend/src/pages/InstrumentDashboard.tsx +++ b/frontend/src/pages/InstrumentDashboard.tsx @@ -2,7 +2,7 @@ import { useState, useEffect, useCallback, useMemo } from 'react' import { useParams, useNavigate } from 'react-router-dom' import { Sparkles, RefreshCw, ChevronDown, TrendingUp, TrendingDown, - Minus, BarChart2, Clock, Calendar, AlertCircle, + Minus, BarChart2, Clock, Calendar, AlertCircle, Pencil, Save, X, Plus, Trash2, } from 'lucide-react' import axios from 'axios' import clsx from 'clsx' @@ -12,10 +12,13 @@ const api = axios.create({ baseURL: '/api' }) // ── Types ───────────────────────────────────────────────────────────────────── +interface Driver { + key: string; label: string; weight: number; keywords: string[] +} + interface InstrumentConfig { id: string; name: string; yf_ticker: string; category: string; currency: string - description: string - drivers: { key: string; label: string; weight: number }[] + description: string; drivers: Driver[] regime_labels: string[] chart: { ma_periods: number[]; show_volume: boolean } correlation_instruments: string[] @@ -45,10 +48,7 @@ interface Snapshot { instrument: InstrumentConfig price_data: PriceCandle[] indicators: Record - regime: { - current: string; confidence: number; scores: Record - signals: RegimeSignals - } + regime: { current: string; confidence: number; scores: Record; signals: RegimeSignals } trend: TrendMetrics events: SnapshotEvent[] current_price: number; change_pct: number; change_abs: number; period: string @@ -98,148 +98,101 @@ function fmtDateFR(s: string | null): string { function dateToMs(s: string) { return new Date(s).getTime() } -// ── EventTimelineStrip ──────────────────────────────────────────────────────── - -const LEVEL_STRIP = ['long', 'medium', 'short'] as const -type LevelKey = typeof LEVEL_STRIP[number] - -const STRIP_COLORS: Record = { - long: { bg: 'bg-violet-800/60', border: 'border-violet-700/60', text: 'text-violet-100', label: 'text-violet-400' }, - medium: { bg: 'bg-blue-800/60', border: 'border-blue-700/60', text: 'text-blue-100', label: 'text-blue-400' }, - short: { bg: 'bg-emerald-800/60',border: 'border-emerald-700/60',text: 'text-emerald-100', label: 'text-emerald-400'}, -} -const STRIP_LABELS = { long: 'LT', medium: 'MT', short: 'CT' } -const DEFAULT_DAYS: Record = { long: 60, medium: 21, short: 10 } - -function EventTimelineStrip({ events, priceData }: { events: SnapshotEvent[]; priceData: PriceCandle[] }) { - if (priceData.length < 2) return null - - const chartStart = dateToMs(priceData[0].time) - const chartEnd = dateToMs(priceData[priceData.length - 1].time) - const totalMs = chartEnd - chartStart - if (totalMs <= 0) return null - - function leftPct(dateStr: string): number { - return Math.max(0, Math.min(99, ((dateToMs(dateStr) - chartStart) / totalMs) * 100)) - } - - function widthPct(startStr: string, endStr: string | null, level: LevelKey): number { - const endMs = endStr - ? dateToMs(endStr) - : dateToMs(startStr) + DEFAULT_DAYS[level] * 86400000 - const w = ((endMs - dateToMs(startStr)) / totalMs) * 100 - return Math.min(100, Math.max(0.4, w)) - } - - return ( -
-
Événements sur la période
-
- {LEVEL_STRIP.map(level => { - const evs = events.filter(e => e.level === level) - const cols = STRIP_COLORS[level] - return ( -
- {STRIP_LABELS[level]} - {/* paddingRight aligns with lightweight-charts right price scale (~60px) */} -
- {evs.map((ev, i) => { - const left = leftPct(ev.date) - const width = widthPct(ev.date, ev.end_date, level) - return ( -
- {width > 3 && ( - - {ev.title} - - )} -
- ) - })} - {evs.length === 0 && ( -
-
-
- )} -
-
- ) - })} -
-
- ) +function pctN(a: number | undefined, b: number | undefined): number { + if (a === undefined || b === undefined || b === 0) return 0 + return ((a - b) / b) * 100 } -// ── RegimeCard ──────────────────────────────────────────────────────────────── +// ── RegimeCard (métriques de signaux) ───────────────────────────────────────── function RegimeCard({ - regime, config, signalsAt, dateLabel, + regime, signalsAt, dateLabel, }: { - regime: Snapshot['regime']; config: InstrumentConfig - signalsAt: RegimeSignals | null; dateLabel: string + regime: Snapshot['regime'] + signalsAt: RegimeSignals | null + dateLabel: string }) { const signals = signalsAt ?? regime.signals const col = regimeColor(regime.current) - const colorMap: Record = { - emerald: 'text-emerald-400 border-emerald-700/40 bg-emerald-950/30', - red: 'text-red-400 border-red-700/40 bg-red-950/30', - orange: 'text-orange-400 border-orange-700/40 bg-orange-950/30', - slate: 'text-slate-400 border-slate-700/40 bg-slate-900/30', - blue: 'text-blue-400 border-blue-700/40 bg-blue-950/30', + + const borderMap: Record = { + emerald: 'border-emerald-700/40 bg-emerald-950/30', + red: 'border-red-700/40 bg-red-950/30', + orange: 'border-orange-700/40 bg-orange-950/30', + slate: 'border-slate-700/40 bg-slate-900/30', + blue: 'border-blue-700/40 bg-blue-950/30', } - const barColorMap: Record = { - emerald: 'bg-emerald-500', red: 'bg-red-500', orange: 'bg-orange-500', slate: 'bg-slate-500', blue: 'bg-blue-500', - } - const sigItems = [ - { label: 'MA50 vs MA200', value: signals.ma50_above_ma200 === true ? 'Au-dessus ↑' : signals.ma50_above_ma200 === false ? 'En-dessous ↓' : '—', color: signals.ma50_above_ma200 ? 'text-emerald-400' : 'text-red-400' }, - { label: 'Slope MA50 (10j)', value: fmt(signals.ma50_slope_pct) + '%', color: pctColor(signals.ma50_slope_pct) }, - { label: 'Momentum 20j', value: fmt(signals.momentum_20d_pct) + '%', color: pctColor(signals.momentum_20d_pct) }, - { label: 'Distance MA200', value: fmt(signals.dist_ma200_pct) + '%', color: pctColor(signals.dist_ma200_pct) }, - { label: 'Volatilité (ATR%)', value: (signals.vol_ratio_pct ?? 0).toFixed(1) + '%', color: signals.vol_ratio_pct > 130 ? 'text-orange-400' : 'text-slate-400' }, + + // 6 signal metrics displayed as a compact grid + const ma50Color = signals.ma50_above_ma200 ? 'text-emerald-400' : 'text-red-400' + const volColor = (signals.vol_ratio_pct ?? 0) > 130 ? 'text-orange-400' : (signals.vol_ratio_pct ?? 0) < 70 ? 'text-cyan-400' : 'text-slate-300' + + const metrics: { label: string; value: string; sub?: string; color: string }[] = [ + { + label: 'MA50 / MA200', + value: signals.ma50_above_ma200 === true ? 'Au-dessus' : signals.ma50_above_ma200 === false ? 'En-dessous' : '—', + sub: signals.ma50_above_ma200 === true ? '↑ Golden cross' : signals.ma50_above_ma200 === false ? '↓ Death cross' : '', + color: ma50Color, + }, + { + label: 'Slope MA50 (10j)', + value: fmt(signals.ma50_slope_pct) + '%', + color: pctColor(signals.ma50_slope_pct), + }, + { + label: 'Slope MA200 (10j)', + value: fmt(signals.ma200_slope_pct) + '%', + color: pctColor(signals.ma200_slope_pct), + }, + { + label: 'Momentum 20j', + value: fmt(signals.momentum_20d_pct) + '%', + color: pctColor(signals.momentum_20d_pct), + }, + { + label: 'Distance MA200', + value: fmt(signals.dist_ma200_pct) + '%', + sub: signals.dist_ma200_pct > 10 ? 'Surextension' : signals.dist_ma200_pct < -10 ? 'Survendu' : 'Neutre', + color: pctColor(signals.dist_ma200_pct), + }, + { + label: 'Volatilité ATR', + value: (signals.vol_ratio_pct ?? 0).toFixed(0) + '%', + sub: (signals.vol_ratio_pct ?? 0) > 130 ? 'Élevée' : (signals.vol_ratio_pct ?? 0) < 70 ? 'Comprimée' : 'Normale', + color: volColor, + }, ] return ( -
+
+ {/* Header */}
- Régime + Régime Détecté
{dateLabel}
-
-
{regime.current}
-
Confiance {Math.round(regime.confidence * 100)}%
+ + {/* Regime label + confidence */} +
+
{regime.current}
+
+
+
+
+ {Math.round(regime.confidence * 100)}% +
-
- {Object.entries(regime.scores).sort(([, a], [, b]) => b - a).map(([label, score]) => { - const c2 = regimeColor(label) - return ( -
-
- {label} - {Math.round(score * 100)}% -
-
-
-
-
- ) - })} -
-
-
Signaux — {dateLabel}
- {sigItems.map(s => ( -
- {s.label} - {s.value} + + {/* 6 signal metrics — 2 column grid */} +
+ {metrics.map(m => ( +
+
{m.label}
+
{m.value}
+ {m.sub &&
{m.sub}
}
))}
@@ -255,25 +208,20 @@ function TrendCard({ trend, dateLabel }: { trend: TrendMetrics; dateLabel: strin const rsiZone = rsi > 70 ? 'Suracheté' : rsi < 30 ? 'Survendu' : 'Neutre' const items = [ - { - group: 'Tendance', rows: [ - { label: 'Slope MA50 (5j)', value: fmt(trend.ma50_slope_5d) + '%', arrow: trend.ma50_slope_5d }, - { label: 'Slope MA200 (20j)',value: fmt(trend.ma200_slope_20d) + '%', arrow: trend.ma200_slope_20d }, - { label: 'Distance MA50', value: trend.dist_ma50_pct !== null ? fmt(trend.dist_ma50_pct) + '%' : '—', arrow: trend.dist_ma50_pct ?? 0 }, - { label: 'Distance MA200', value: trend.dist_ma200_pct !== null ? fmt(trend.dist_ma200_pct) + '%' : '—', arrow: trend.dist_ma200_pct ?? 0, bold: true }, - ] - }, - { - group: 'Momentum', rows: [ - { label: 'Momentum 1M', value: fmt(trend.momentum_1m_pct) + '%', arrow: trend.momentum_1m_pct }, - { label: 'Momentum 3M', value: fmt(trend.momentum_3m_pct) + '%', arrow: trend.momentum_3m_pct, bold: true }, - ] - }, + { group: 'Tendance', rows: [ + { label: 'Slope MA50 (5j)', value: fmt(trend.ma50_slope_5d) + '%', arrow: trend.ma50_slope_5d }, + { label: 'Slope MA200 (20j)', value: fmt(trend.ma200_slope_20d) + '%', arrow: trend.ma200_slope_20d }, + { label: 'Distance MA50', value: trend.dist_ma50_pct != null ? fmt(trend.dist_ma50_pct) + '%' : '—', arrow: trend.dist_ma50_pct ?? 0 }, + { label: 'Distance MA200', value: trend.dist_ma200_pct != null ? fmt(trend.dist_ma200_pct) + '%' : '—', arrow: trend.dist_ma200_pct ?? 0, bold: true }, + ]}, + { group: 'Momentum', rows: [ + { label: 'Momentum 1M', value: fmt(trend.momentum_1m_pct) + '%', arrow: trend.momentum_1m_pct }, + { label: 'Momentum 3M', value: fmt(trend.momentum_3m_pct) + '%', arrow: trend.momentum_3m_pct, bold: true }, + ]}, ] const pct52w = trend.high_52w && trend.low_52w && (trend.high_52w - trend.low_52w) > 0 - ? ((trend.current_price - trend.low_52w) / (trend.high_52w - trend.low_52w)) * 100 - : null + ? ((trend.current_price - trend.low_52w) / (trend.high_52w - trend.low_52w)) * 100 : null return (
@@ -285,7 +233,6 @@ function TrendCard({ trend, dateLabel }: { trend: TrendMetrics; dateLabel: strin {dateLabel}
- {/* Prix au snapshot */}
Prix @@ -300,8 +247,7 @@ function TrendCard({ trend, dateLabel }: { trend: TrendMetrics; dateLabel: strin
{row.label} - - {row.value} + {row.value}
))} @@ -334,8 +280,7 @@ function TrendCard({ trend, dateLabel }: { trend: TrendMetrics; dateLabel: strin
- {trend.low_52w?.toFixed(2)} - {trend.high_52w?.toFixed(2)} + {trend.low_52w?.toFixed(2)}{trend.high_52w?.toFixed(2)}
)} @@ -368,7 +313,7 @@ function EventsCard({ events }: { events: SnapshotEvent[] }) { {events.length === 0 ? (
Aucun événement lié trouvé
) : ( -
+
{events.map((ev, i) => (
void; instrument: InstrumentConfig }) { +function NarrativeCard({ narrative, loading, onLoad, instrument }: { + narrative: string; loading: boolean; onLoad: () => void; instrument: InstrumentConfig +}) { return (
@@ -421,36 +366,222 @@ function NarrativeCard({
) : (
- Cliquez "Générer" pour obtenir une analyse IA pour {instrument.name}. + Cliquez "Générer" pour une analyse IA pour {instrument.name}.
)}
) } +// ── EventTimelineStrip — by drivers ────────────────────────────────────────── + +const DRIVER_PALETTE = [ + { bg: 'bg-violet-800/60', border: 'border-violet-700/60', text: 'text-violet-100', label: 'text-violet-400' }, + { bg: 'bg-blue-800/60', border: 'border-blue-700/60', text: 'text-blue-100', label: 'text-blue-400' }, + { bg: 'bg-emerald-800/60', border: 'border-emerald-700/60', text: 'text-emerald-100', label: 'text-emerald-400' }, + { bg: 'bg-amber-800/60', border: 'border-amber-700/60', text: 'text-amber-100', label: 'text-amber-400' }, +] + +function eventMatchesDriver(ev: SnapshotEvent, keywords: string[]): boolean { + const text = `${ev.title} ${ev.description ?? ''} ${ev.category ?? ''}`.toLowerCase() + return keywords.some(kw => text.includes(kw.toLowerCase())) +} + +function EventTimelineStrip({ + events, priceData, drivers, +}: { + events: SnapshotEvent[]; priceData: PriceCandle[]; drivers: Driver[] +}) { + if (priceData.length < 2) return null + + const chartStart = dateToMs(priceData[0].time) + const chartEnd = dateToMs(priceData[priceData.length - 1].time) + const totalMs = chartEnd - chartStart + if (totalMs <= 0) return null + + function leftPct(d: string) { return Math.max(0, Math.min(99, ((dateToMs(d) - chartStart) / totalMs) * 100)) } + function widthPct(start: string, end: string | null): number { + // Default width = 3% of period if no end date + const endMs = end ? dateToMs(end) : dateToMs(start) + totalMs * 0.03 + return Math.min(100, Math.max(0.4, ((endMs - dateToMs(start)) / totalMs) * 100)) + } + + // Top 4 drivers by weight + const topDrivers = [...drivers].sort((a, b) => b.weight - a.weight).slice(0, 4) + + return ( +
+
Événements par driver
+
+ {topDrivers.map((driver, di) => { + const cols = DRIVER_PALETTE[di % DRIVER_PALETTE.length] + const matchedEvs = events.filter(ev => eventMatchesDriver(ev, driver.keywords ?? [])) + return ( +
+ + {driver.label} + + {/* paddingRight aligns with chart right price scale (~60px) */} +
+ {matchedEvs.map((ev, i) => { + const left = leftPct(ev.date) + const width = widthPct(ev.date, ev.end_date) + return ( +
+ {width > 3 && ( + + {ev.title} + + )} +
+ ) + })} + {matchedEvs.length === 0 && ( +
+
+
+ )} +
+
+ ) + })} +
+
+ ) +} + +// ── DriversPanel — édition inline ──────────────────────────────────────────── + +function DriversPanel({ instrumentId, drivers, onSave, onClose }: { + instrumentId: string + drivers: Driver[] + onSave: (drivers: Driver[]) => void + onClose: () => void +}) { + const [local, setLocal] = useState(() => drivers.map(d => ({ ...d, keywords: [...(d.keywords ?? [])] }))) + const [saving, setSaving] = useState(false) + const [error, setError] = useState('') + + function updateDriver(i: number, field: keyof Driver, val: any) { + setLocal(prev => prev.map((d, idx) => idx === i ? { ...d, [field]: val } : d)) + } + + function updateKeywords(i: number, raw: string) { + const kws = raw.split(',').map(s => s.trim()).filter(Boolean) + updateDriver(i, 'keywords', kws) + } + + function addDriver() { + setLocal(prev => [...prev, { key: `driver_${Date.now()}`, label: 'Nouveau driver', weight: 0.5, keywords: [] }]) + } + + function removeDriver(i: number) { + setLocal(prev => prev.filter((_, idx) => idx !== i)) + } + + async function save() { + setSaving(true); setError('') + try { + await api.put(`/instruments/${instrumentId}/drivers`, { drivers: local }) + onSave(local) + onClose() + } catch (e: any) { + setError(e?.response?.data?.detail ?? 'Erreur de sauvegarde') + } finally { + setSaving(false) + } + } + + return ( +
+
+ Éditer les drivers — {instrumentId} +
+ + + +
+
+ + {error &&
{error}
} + +
+ {local.map((d, i) => ( +
+
+ updateDriver(i, 'label', e.target.value)} + /> +
+ Poids + updateDriver(i, 'weight', parseFloat(e.target.value))} + /> +
+ +
+
+ + updateKeywords(i, e.target.value)} + /> +
+
+ ))} +
+
+ ) +} + // ── Main page ───────────────────────────────────────────────────────────────── const PERIODS = [ { key: '3mo', label: '3M' }, { key: '6mo', label: '6M' }, - { key: '1y', label: '1Y' }, { key: '2y', label: '2Y' }, { key: '5y', label: '5Y' }, + { key: '1y', label: '1Y' }, { key: '2y', label: '2Y' }, { key: '5y', label: '5Y' }, ] -function pctN(a: number | undefined, b: number | undefined): number { - if (a === undefined || b === undefined || b === 0) return 0 - return ((a - b) / b) * 100 -} - export default function InstrumentDashboard() { const { id = 'SPY' } = useParams<{ id: string }>() const navigate = useNavigate() - const [period, setPeriod] = useState('1y') + const [period, setPeriod] = useState('1y') const [instruments, setInstruments] = useState([]) - const [snapshot, setSnapshot] = useState(null) - const [narrative, setNarrative] = useState('') - const [loading, setLoading] = useState(false) + const [snapshot, setSnapshot] = useState(null) + const [narrative, setNarrative] = useState('') + const [loading, setLoading] = useState(false) const [loadingNarr, setLoadingNarr] = useState(false) const [selectorOpen, setSelectorOpen] = useState(false) const [selectedDate, setSelectedDate] = useState(null) + const [editDrivers, setEditDrivers] = useState(false) + // Local drivers (updated optimistically after save) + const [localDrivers, setLocalDrivers] = useState(null) const instrumentId = id.toUpperCase() @@ -463,6 +594,8 @@ export default function InstrumentDashboard() { setSnapshot(null) setNarrative('') setSelectedDate(null) + setLocalDrivers(null) + setEditDrivers(false) api.get(`/instruments/${instrumentId}/snapshot?period=${period}`) .then(r => { setSnapshot(r.data) @@ -483,138 +616,87 @@ export default function InstrumentDashboard() { const handleDateHover = useCallback((date: string | null) => { if (date) setSelectedDate(date) - // null = mouse left chart area → keep last hovered date }, []) - // ── Lookup maps (rebuilt only when snapshot changes) ────────────────────── + // ── Lookup maps ─────────────────────────────────────────────────────────── const { priceMap, indMap, sortedDates, dateIndex } = useMemo(() => { - if (!snapshot) return { - priceMap: {} as Record, - indMap: {} as Record>, - sortedDates: [] as string[], - dateIndex: {} as Record, - } + if (!snapshot) return { priceMap: {} as Record, indMap: {} as Record>, sortedDates: [] as string[], dateIndex: {} as Record } const priceMap: Record = {} const indMap: Record> = {} const sortedDates: string[] = [] const dateIndex: Record = {} - - for (const c of snapshot.price_data) { - priceMap[c.time] = c - sortedDates.push(c.time) - } + for (const c of snapshot.price_data) { priceMap[c.time] = c; sortedDates.push(c.time) } sortedDates.forEach((d, i) => { dateIndex[d] = i }) - for (const [key, pts] of Object.entries(snapshot.indicators)) { - for (const pt of pts) { - if (!indMap[pt.time]) indMap[pt.time] = {} - indMap[pt.time][key] = pt.value - } + for (const pt of pts) { if (!indMap[pt.time]) indMap[pt.time] = {}; indMap[pt.time][key] = pt.value } } return { priceMap, indMap, sortedDates, dateIndex } }, [snapshot]) - // Resolve effective date (fall back to last if selectedDate not in index) const effectiveDate = useMemo(() => { if (selectedDate && dateIndex[selectedDate] !== undefined) return selectedDate return sortedDates[sortedDates.length - 1] ?? null }, [selectedDate, sortedDates, dateIndex]) - // ── Compute trend metrics at selectedDate ───────────────────────────────── const dateTrend = useMemo((): TrendMetrics | null => { if (!effectiveDate || !snapshot) return null - const candle = priceMap[effectiveDate] - if (!candle) return null - - const idx = dateIndex[effectiveDate] - const price = candle.close - const ind = indMap[effectiveDate] ?? {} - const ma50 = ind.ma50 - const ma200 = ind.ma200 - + const candle = priceMap[effectiveDate]; if (!candle) return null + const idx = dateIndex[effectiveDate], price = candle.close, ind = indMap[effectiveDate] ?? {} + const ma50 = ind.ma50, ma200 = ind.ma200, atr14 = ind.atr14 ?? 0 const d5 = idx >= 5 ? sortedDates[idx - 5] : null const d20 = idx >= 20 ? sortedDates[idx - 20] : null const d21 = idx >= 21 ? sortedDates[idx - 21] : null const d63 = idx >= 63 ? sortedDates[idx - 63] : null - - const ma50_slope_5d = pctN(ma50, d5 ? indMap[d5]?.ma50 : undefined) - const ma200_slope_20d = pctN(ma200, d20 ? indMap[d20]?.ma200 : undefined) - const momentum_1m_pct = d21 && priceMap[d21] ? pctN(price, priceMap[d21].close) : 0 - const momentum_3m_pct = d63 && priceMap[d63] ? pctN(price, priceMap[d63].close) : 0 - const dist_ma50_pct = ma50 ? pctN(price, ma50) : null - const dist_ma200_pct = ma200 ? pctN(price, ma200) : null - - const atr14 = ind.atr14 ?? 0 let atrSum = 0, atrCnt = 0 - for (let i = Math.max(0, idx - 65); i <= idx; i++) { - const v = indMap[sortedDates[i]]?.atr14 - if (v) { atrSum += v; atrCnt++ } - } - const atr_vs_3m_avg_pct = atrCnt > 0 && atr14 ? (atr14 / (atrSum / atrCnt)) * 100 : 100 - + for (let i = Math.max(0, idx - 65); i <= idx; i++) { const v = indMap[sortedDates[i]]?.atr14; if (v) { atrSum += v; atrCnt++ } } let high52 = candle.high, low52 = candle.low - for (let i = Math.max(0, idx - 252); i <= idx; i++) { - const c = priceMap[sortedDates[i]] - if (c) { if (c.high > high52) high52 = c.high; if (c.low < low52) low52 = c.low } - } - + for (let i = Math.max(0, idx - 252); i <= idx; i++) { const c = priceMap[sortedDates[i]]; if (c) { if (c.high > high52) high52 = c.high; if (c.low < low52) low52 = c.low } } return { - ma50_slope_5d, ma200_slope_20d, - rsi14_current: ind.rsi14 ?? 50, - atr14_current: atr14, - atr_vs_3m_avg_pct, momentum_1m_pct, momentum_3m_pct, - dist_ma50_pct, dist_ma200_pct, + ma50_slope_5d: pctN(ma50, d5 ? indMap[d5]?.ma50 : undefined), + ma200_slope_20d: pctN(ma200, d20 ? indMap[d20]?.ma200 : undefined), + rsi14_current: ind.rsi14 ?? 50, + atr14_current: atr14, + atr_vs_3m_avg_pct: atrCnt > 0 && atr14 ? (atr14 / (atrSum / atrCnt)) * 100 : 100, + momentum_1m_pct: d21 && priceMap[d21] ? pctN(price, priceMap[d21].close) : 0, + momentum_3m_pct: d63 && priceMap[d63] ? pctN(price, priceMap[d63].close) : 0, + dist_ma50_pct: ma50 ? pctN(price, ma50) : null, + dist_ma200_pct: ma200 ? pctN(price, ma200) : null, current_price: price, high_52w: high52, low_52w: low52, } }, [effectiveDate, priceMap, indMap, sortedDates, dateIndex, snapshot]) - // ── Compute regime signals at selectedDate ──────────────────────────────── const dateSignals = useMemo((): RegimeSignals | null => { if (!effectiveDate || !snapshot) return null - const candle = priceMap[effectiveDate] - if (!candle) return null - - const idx = dateIndex[effectiveDate] - const price = candle.close - const ind = indMap[effectiveDate] ?? {} - const ma50 = ind.ma50 - const ma200 = ind.ma200 - const atr14 = ind.atr14 - + const candle = priceMap[effectiveDate]; if (!candle) return null + const idx = dateIndex[effectiveDate], price = candle.close, ind = indMap[effectiveDate] ?? {} + const ma50 = ind.ma50, ma200 = ind.ma200, atr14 = ind.atr14 const d10 = idx >= 10 ? sortedDates[idx - 10] : null const d20 = idx >= 20 ? sortedDates[idx - 20] : null - - const ma50_slope_pct = pctN(ma50, d10 ? indMap[d10]?.ma50 : undefined) - const ma200_slope_pct = pctN(ma200, d10 ? indMap[d10]?.ma200 : undefined) - const momentum_20d_pct = d20 && priceMap[d20] ? pctN(price, priceMap[d20].close) : 0 - const dist_ma200_pct = ma200 ? pctN(price, ma200) : 0 - let atrSum = 0, atrCnt = 0 - for (let i = Math.max(0, idx - 65); i <= idx; i++) { - const v = indMap[sortedDates[i]]?.atr14 - if (v) { atrSum += v; atrCnt++ } - } - const vol_ratio_pct = atrCnt > 0 && atr14 ? (atr14 / (atrSum / atrCnt)) * 100 : 0 - + for (let i = Math.max(0, idx - 65); i <= idx; i++) { const v = indMap[sortedDates[i]]?.atr14; if (v) { atrSum += v; atrCnt++ } } return { ma50_above_ma200: ma50 !== undefined && ma200 !== undefined ? ma50 > ma200 : null, - ma50_slope_pct, ma200_slope_pct, momentum_20d_pct, dist_ma200_pct, vol_ratio_pct, + ma50_slope_pct: pctN(ma50, d10 ? indMap[d10]?.ma50 : undefined), + ma200_slope_pct: pctN(ma200, d10 ? indMap[d10]?.ma200 : undefined), + momentum_20d_pct: d20 && priceMap[d20] ? pctN(price, priceMap[d20].close) : 0, + dist_ma200_pct: ma200 ? pctN(price, ma200) : 0, + vol_ratio_pct: atrCnt > 0 && atr14 ? (atr14 / (atrSum / atrCnt)) * 100 : 0, } }, [effectiveDate, priceMap, indMap, sortedDates, dateIndex, snapshot]) - // ── UI helpers ──────────────────────────────────────────────────────────── + // ── UI ──────────────────────────────────────────────────────────────────── const grouped = CATEGORY_ORDER.map(cat => ({ cat, label: CATEGORY_LABELS[cat] ?? cat, items: instruments.filter(i => i.category === cat), })).filter(g => g.items.length > 0) - const selected = instruments.find(i => i.id === instrumentId) - const isLastDate = effectiveDate === sortedDates[sortedDates.length - 1] - const dateLabel = effectiveDate ? fmtDateFR(effectiveDate) : '—' - - // Header price: from computed trend (crosshair date) or snapshot + const selected = instruments.find(i => i.id === instrumentId) + const isLastDate = effectiveDate === sortedDates[sortedDates.length - 1] + const dateLabel = effectiveDate ? fmtDateFR(effectiveDate) : '—' const displayPrice = dateTrend?.current_price ?? snapshot?.current_price - const displayPricePct = snapshot?.change_pct + + // Drivers to use (local override after edit, else from snapshot) + const activeDrivers = localDrivers ?? snapshot?.instrument?.drivers ?? [] return (
@@ -668,31 +750,45 @@ export default function InstrumentDashboard() { {displayPrice.toLocaleString('fr-FR', { maximumFractionDigits: 4 })} {isLastDate && snapshot && ( - = 0 ? 'text-emerald-400' : 'text-red-400')}> - {(displayPricePct ?? 0) >= 0 ? '+' : ''}{snapshot.change_abs?.toFixed(2)} ({(displayPricePct ?? 0) >= 0 ? '+' : ''}{displayPricePct?.toFixed(2)}%) + = 0 ? 'text-emerald-400' : 'text-red-400')}> + {(snapshot.change_pct ?? 0) >= 0 ? '+' : ''}{snapshot.change_abs?.toFixed(2)} ({(snapshot.change_pct ?? 0) >= 0 ? '+' : ''}{snapshot.change_pct?.toFixed(2)}%) )}
)} -
- {PERIODS.map(p => ( - - ))} + )} + {/* Period selector */} +
+ {PERIODS.map(p => ( + + ))} +
- {selected && ( -

{selected.description}

- )} + {selected &&

{selected.description}

}
- {/* ── Loading skeleton ── */} + {/* ── Loading ── */} {loading && (
@@ -706,7 +802,6 @@ export default function InstrumentDashboard() { {/* ── Content ── */} {!loading && snapshot && ( <> - {/* Chart */} - {/* Event timeline strip */} - + - {/* Snapshot date badge */} + {/* Date badge */}
@@ -747,6 +844,16 @@ export default function InstrumentDashboard() {
+ {/* Drivers edit panel */} + {editDrivers && ( + { setLocalDrivers(saved); setEditDrivers(false) }} + onClose={() => setEditDrivers(false)} + /> + )} + {/* AI Narrative */}