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 <noreply@anthropic.com>
This commit is contained in:
OpenSquared
2026-06-24 22:48:10 +02:00
parent 418d03254d
commit aa81598278
4 changed files with 635 additions and 579 deletions

View File

@@ -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."
}
]
}

View File

@@ -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)}

View File

@@ -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:

View File

@@ -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<string, LinePoint[]>
regime: {
current: string; confidence: number; scores: Record<string, number>
signals: RegimeSignals
}
regime: { current: string; confidence: number; scores: Record<string, number>; 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<LevelKey, { bg: string; border: string; text: string; label: string }> = {
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<LevelKey, number> = { 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 (
<div className="bg-dark-900/40 rounded-xl border border-slate-700/40 px-4 py-3">
<div className="text-xs text-slate-500 uppercase tracking-wide mb-2.5">Événements sur la période</div>
<div className="space-y-1.5">
{LEVEL_STRIP.map(level => {
const evs = events.filter(e => e.level === level)
const cols = STRIP_COLORS[level]
return (
<div key={level} className="flex items-center gap-2 h-6">
<span className={clsx('text-xs font-bold w-5 shrink-0', cols.label)}>{STRIP_LABELS[level]}</span>
{/* paddingRight aligns with lightweight-charts right price scale (~60px) */}
<div className="relative flex-1 h-full" style={{ paddingRight: 62 }}>
{evs.map((ev, i) => {
const left = leftPct(ev.date)
const width = widthPct(ev.date, ev.end_date, level)
return (
<div
key={i}
title={`${ev.title}\n${ev.date}${ev.end_date ? ' → ' + ev.end_date : ''}`}
className={clsx('absolute top-0 h-full rounded border overflow-hidden', cols.bg, cols.border)}
style={{ left: `${left}%`, width: `${width}%` }}
>
{width > 3 && (
<span className={clsx('absolute inset-0 flex items-center px-1 truncate pointer-events-none', cols.text)}
style={{ fontSize: 9 }}>
{ev.title}
</span>
)}
</div>
)
})}
{evs.length === 0 && (
<div className="absolute inset-0 flex items-center">
<div className="w-full border-t border-dashed border-slate-700/40" />
</div>
)}
</div>
</div>
)
})}
</div>
</div>
)
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<string, string> = {
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<string, string> = {
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<string, string> = {
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 (
<div className={clsx('rounded-xl border p-4 space-y-3', colorMap[col])}>
<div className={clsx('rounded-xl border p-4 space-y-3', borderMap[col])}>
{/* Header */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<BarChart2 className={clsx('w-4 h-4', `text-${col}-400`)} />
<span className="text-xs font-semibold text-slate-400 uppercase tracking-wide">Régime</span>
<span className="text-xs font-semibold text-slate-400 uppercase tracking-wide">Régime Détecté</span>
</div>
<span className="text-xs text-slate-600">{dateLabel}</span>
</div>
<div>
<div className={clsx('text-lg font-bold leading-tight', `text-${col}-400`)}>{regime.current}</div>
<div className="text-xs text-slate-500 mt-0.5">Confiance {Math.round(regime.confidence * 100)}%</div>
{/* Regime label + confidence */}
<div className={clsx('rounded-lg px-3 py-2', `bg-${col}-900/20`)}>
<div className={clsx('text-base font-bold leading-tight', `text-${col}-300`)}>{regime.current}</div>
<div className="flex items-center gap-2 mt-1">
<div className="flex-1 h-1.5 bg-slate-800 rounded-full overflow-hidden">
<div className={clsx('h-full rounded-full', `bg-${col}-500`)}
style={{ width: `${Math.round(regime.confidence * 100)}%` }} />
</div>
<span className="text-xs text-slate-500">{Math.round(regime.confidence * 100)}%</span>
</div>
</div>
<div className="space-y-1.5">
{Object.entries(regime.scores).sort(([, a], [, b]) => b - a).map(([label, score]) => {
const c2 = regimeColor(label)
return (
<div key={label} className="space-y-0.5">
<div className="flex items-center justify-between text-xs">
<span className={label === regime.current ? `font-semibold text-${c2}-400` : 'text-slate-500'}>{label}</span>
<span className="text-slate-500">{Math.round(score * 100)}%</span>
</div>
<div className="h-1 bg-slate-800 rounded-full overflow-hidden">
<div className={clsx('h-full rounded-full transition-all', barColorMap[c2] || 'bg-blue-500')}
style={{ width: `${Math.round(score * 100)}%` }} />
</div>
</div>
)
})}
</div>
<div className="border-t border-slate-700/30 pt-3 space-y-1.5">
<div className="text-xs text-slate-600 uppercase tracking-wide mb-1">Signaux {dateLabel}</div>
{sigItems.map(s => (
<div key={s.label} className="flex items-center justify-between text-xs">
<span className="text-slate-500">{s.label}</span>
<span className={s.color}>{s.value}</span>
{/* 6 signal metrics — 2 column grid */}
<div className="grid grid-cols-2 gap-2">
{metrics.map(m => (
<div key={m.label} className="rounded-lg bg-dark-700/50 px-2.5 py-2">
<div className="text-xs text-slate-600 mb-0.5 truncate">{m.label}</div>
<div className={clsx('text-sm font-bold', m.color)}>{m.value}</div>
{m.sub && <div className="text-xs text-slate-600 mt-0.5">{m.sub}</div>}
</div>
))}
</div>
@@ -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 (
<div className="rounded-xl border border-slate-700/40 bg-dark-800/60 p-4 space-y-3">
@@ -285,7 +233,6 @@ function TrendCard({ trend, dateLabel }: { trend: TrendMetrics; dateLabel: strin
<span className="text-xs text-slate-600">{dateLabel}</span>
</div>
{/* Prix au snapshot */}
<div className="flex items-center justify-between bg-dark-700/40 rounded-lg px-3 py-1.5">
<span className="text-xs text-slate-500">Prix</span>
<span className="text-sm font-bold text-white">
@@ -300,8 +247,7 @@ function TrendCard({ trend, dateLabel }: { trend: TrendMetrics; dateLabel: strin
<div key={row.label} className="flex items-center justify-between text-xs py-0.5">
<span className="text-slate-500">{row.label}</span>
<span className={clsx('flex items-center gap-1', (row as any).bold ? 'font-semibold' : '', pctColor(row.arrow ?? 0))}>
<Arrow v={row.arrow ?? 0} />
{row.value}
<Arrow v={row.arrow ?? 0} />{row.value}
</span>
</div>
))}
@@ -334,8 +280,7 @@ function TrendCard({ trend, dateLabel }: { trend: TrendMetrics; dateLabel: strin
<div className="absolute top-0 h-full bg-blue-500/60 rounded-full" style={{ width: `${pct52w}%` }} />
</div>
<div className="flex justify-between text-xs text-slate-600 mt-0.5">
<span>{trend.low_52w?.toFixed(2)}</span>
<span>{trend.high_52w?.toFixed(2)}</span>
<span>{trend.low_52w?.toFixed(2)}</span><span>{trend.high_52w?.toFixed(2)}</span>
</div>
</div>
)}
@@ -368,7 +313,7 @@ function EventsCard({ events }: { events: SnapshotEvent[] }) {
{events.length === 0 ? (
<div className="text-xs text-slate-600 italic py-2">Aucun événement lié trouvé</div>
) : (
<div className="space-y-2 max-h-[360px] overflow-y-auto pr-1">
<div className="space-y-2 max-h-[380px] overflow-y-auto pr-1">
{events.map((ev, i) => (
<div key={i}
className="rounded-lg border border-slate-700/30 bg-dark-700/40 p-2 cursor-pointer hover:bg-dark-700/70 transition-colors"
@@ -396,9 +341,9 @@ function EventsCard({ events }: { events: SnapshotEvent[] }) {
// ── NarrativeCard ─────────────────────────────────────────────────────────────
function NarrativeCard({
narrative, loading, onLoad, instrument,
}: { narrative: string; loading: boolean; onLoad: () => void; instrument: InstrumentConfig }) {
function NarrativeCard({ narrative, loading, onLoad, instrument }: {
narrative: string; loading: boolean; onLoad: () => void; instrument: InstrumentConfig
}) {
return (
<div className="rounded-xl border border-slate-700/40 bg-dark-800/60 p-4">
<div className="flex items-center justify-between mb-3">
@@ -421,36 +366,222 @@ function NarrativeCard({
</div>
) : (
<div className="text-sm text-slate-600 italic">
Cliquez "Générer" pour obtenir une analyse IA pour {instrument.name}.
Cliquez "Générer" pour une analyse IA pour {instrument.name}.
</div>
)}
</div>
)
}
// ── 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 (
<div className="bg-dark-900/40 rounded-xl border border-slate-700/40 px-4 py-3">
<div className="text-xs text-slate-500 uppercase tracking-wide mb-2.5">Événements par driver</div>
<div className="space-y-1.5">
{topDrivers.map((driver, di) => {
const cols = DRIVER_PALETTE[di % DRIVER_PALETTE.length]
const matchedEvs = events.filter(ev => eventMatchesDriver(ev, driver.keywords ?? []))
return (
<div key={driver.key} className="flex items-center gap-2 h-6">
<span className={clsx('text-xs font-semibold shrink-0 truncate w-28', cols.label)}
style={{ fontSize: 10 }}
title={driver.label}
>
{driver.label}
</span>
{/* paddingRight aligns with chart right price scale (~60px) */}
<div className="relative flex-1 h-full" style={{ paddingRight: 62 }}>
{matchedEvs.map((ev, i) => {
const left = leftPct(ev.date)
const width = widthPct(ev.date, ev.end_date)
return (
<div key={i}
title={`${ev.title}\n${ev.date}${ev.end_date ? ' → ' + ev.end_date : ''}\n${ev.description ?? ''}`}
className={clsx('absolute top-0 h-full rounded border overflow-hidden', cols.bg, cols.border)}
style={{ left: `${left}%`, width: `${width}%` }}
>
{width > 3 && (
<span className={clsx('absolute inset-0 flex items-center px-1 truncate pointer-events-none', cols.text)}
style={{ fontSize: 9 }}>
{ev.title}
</span>
)}
</div>
)
})}
{matchedEvs.length === 0 && (
<div className="absolute inset-0 flex items-center">
<div className="w-full border-t border-dashed border-slate-700/30" />
</div>
)}
</div>
</div>
)
})}
</div>
</div>
)
}
// ── DriversPanel — édition inline ────────────────────────────────────────────
function DriversPanel({ instrumentId, drivers, onSave, onClose }: {
instrumentId: string
drivers: Driver[]
onSave: (drivers: Driver[]) => void
onClose: () => void
}) {
const [local, setLocal] = useState<Driver[]>(() => 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 (
<div className="rounded-xl border border-blue-700/40 bg-dark-800/80 p-4 space-y-4">
<div className="flex items-center justify-between">
<span className="text-sm font-semibold text-white">Éditer les drivers {instrumentId}</span>
<div className="flex items-center gap-2">
<button onClick={addDriver}
className="flex items-center gap-1 text-xs px-2.5 py-1 bg-blue-800/30 hover:bg-blue-800/50 text-blue-300 border border-blue-700/40 rounded-lg transition-colors">
<Plus className="w-3 h-3" /> Ajouter
</button>
<button onClick={save} disabled={saving}
className="flex items-center gap-1.5 text-xs px-3 py-1 bg-emerald-800/30 hover:bg-emerald-800/50 text-emerald-300 border border-emerald-700/40 rounded-lg transition-colors disabled:opacity-40">
{saving ? <RefreshCw className="w-3 h-3 animate-spin" /> : <Save className="w-3 h-3" />}
{saving ? 'Sauvegarde...' : 'Sauvegarder'}
</button>
<button onClick={onClose} className="p-1 text-slate-500 hover:text-white transition-colors">
<X className="w-4 h-4" />
</button>
</div>
</div>
{error && <div className="text-xs text-red-400 bg-red-900/20 border border-red-700/30 rounded-lg px-3 py-1.5">{error}</div>}
<div className="space-y-2 max-h-[420px] overflow-y-auto pr-1">
{local.map((d, i) => (
<div key={i} className="rounded-lg border border-slate-700/30 bg-dark-700/40 p-3 space-y-2">
<div className="flex items-center gap-2">
<input
className="flex-1 text-xs bg-dark-600 border border-slate-700/40 rounded px-2 py-1 text-white placeholder-slate-600"
placeholder="Label"
value={d.label}
onChange={e => updateDriver(i, 'label', e.target.value)}
/>
<div className="flex items-center gap-1.5">
<span className="text-xs text-slate-500">Poids</span>
<input
type="number" min="0" max="1" step="0.05"
className="w-16 text-xs bg-dark-600 border border-slate-700/40 rounded px-2 py-1 text-white"
value={d.weight}
onChange={e => updateDriver(i, 'weight', parseFloat(e.target.value))}
/>
</div>
<button onClick={() => removeDriver(i)} className="p-1 text-slate-600 hover:text-red-400 transition-colors">
<Trash2 className="w-3.5 h-3.5" />
</button>
</div>
<div>
<label className="text-xs text-slate-600 mb-0.5 block">Mots-clés (séparés par virgule)</label>
<input
className="w-full text-xs bg-dark-600 border border-slate-700/40 rounded px-2 py-1 text-slate-300 placeholder-slate-600"
placeholder="Fed, FOMC, pivot, QE, taux directeur..."
value={(d.keywords ?? []).join(', ')}
onChange={e => updateKeywords(i, e.target.value)}
/>
</div>
</div>
))}
</div>
</div>
)
}
// ── 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<InstrumentConfig[]>([])
const [snapshot, setSnapshot] = useState<Snapshot | null>(null)
const [narrative, setNarrative] = useState('')
const [loading, setLoading] = useState(false)
const [snapshot, setSnapshot] = useState<Snapshot | null>(null)
const [narrative, setNarrative] = useState('')
const [loading, setLoading] = useState(false)
const [loadingNarr, setLoadingNarr] = useState(false)
const [selectorOpen, setSelectorOpen] = useState(false)
const [selectedDate, setSelectedDate] = useState<string | null>(null)
const [editDrivers, setEditDrivers] = useState(false)
// Local drivers (updated optimistically after save)
const [localDrivers, setLocalDrivers] = useState<Driver[] | null>(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<string, PriceCandle>,
indMap: {} as Record<string, Record<string, number>>,
sortedDates: [] as string[],
dateIndex: {} as Record<string, number>,
}
if (!snapshot) return { priceMap: {} as Record<string, PriceCandle>, indMap: {} as Record<string, Record<string, number>>, sortedDates: [] as string[], dateIndex: {} as Record<string, number> }
const priceMap: Record<string, PriceCandle> = {}
const indMap: Record<string, Record<string, number>> = {}
const sortedDates: string[] = []
const dateIndex: Record<string, number> = {}
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 (
<div className="p-6 max-w-screen-xl mx-auto space-y-4">
@@ -668,31 +750,45 @@ export default function InstrumentDashboard() {
{displayPrice.toLocaleString('fr-FR', { maximumFractionDigits: 4 })}
</span>
{isLastDate && snapshot && (
<span className={clsx('text-sm font-semibold', (displayPricePct ?? 0) >= 0 ? 'text-emerald-400' : 'text-red-400')}>
{(displayPricePct ?? 0) >= 0 ? '+' : ''}{snapshot.change_abs?.toFixed(2)} ({(displayPricePct ?? 0) >= 0 ? '+' : ''}{displayPricePct?.toFixed(2)}%)
<span className={clsx('text-sm font-semibold', (snapshot.change_pct ?? 0) >= 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)}%)
</span>
)}
</div>
)}
<div className="ml-auto flex items-center gap-1 bg-dark-800 border border-slate-700/40 rounded-xl p-1">
{PERIODS.map(p => (
<button key={p.key} onClick={() => setPeriod(p.key)}
className={clsx('px-3 py-1 text-xs rounded-lg transition-colors',
period === p.key ? 'bg-blue-700/50 text-blue-300' : 'text-slate-400 hover:text-white hover:bg-dark-700'
<div className="ml-auto flex items-center gap-2">
{/* Drivers edit button */}
{snapshot && (
<button onClick={() => setEditDrivers(e => !e)}
className={clsx('flex items-center gap-1.5 text-xs px-3 py-1.5 border rounded-lg transition-colors',
editDrivers
? 'bg-blue-800/40 text-blue-300 border-blue-700/40'
: 'bg-dark-800 text-slate-400 border-slate-700/40 hover:text-white hover:bg-dark-700'
)}
>
{p.label}
<Pencil className="w-3 h-3" />
Drivers
</button>
))}
)}
{/* Period selector */}
<div className="flex items-center gap-1 bg-dark-800 border border-slate-700/40 rounded-xl p-1">
{PERIODS.map(p => (
<button key={p.key} onClick={() => setPeriod(p.key)}
className={clsx('px-3 py-1 text-xs rounded-lg transition-colors',
period === p.key ? 'bg-blue-700/50 text-blue-300' : 'text-slate-400 hover:text-white hover:bg-dark-700'
)}
>
{p.label}
</button>
))}
</div>
</div>
{selected && (
<p className="w-full text-xs text-slate-500">{selected.description}</p>
)}
{selected && <p className="w-full text-xs text-slate-500">{selected.description}</p>}
</div>
{/* ── Loading skeleton ── */}
{/* ── Loading ── */}
{loading && (
<div className="space-y-4">
<div className="bg-dark-800/60 rounded-xl border border-slate-700/40 animate-pulse" style={{ height: 420 }} />
@@ -706,7 +802,6 @@ export default function InstrumentDashboard() {
{/* ── Content ── */}
{!loading && snapshot && (
<>
{/* Chart */}
<InstrumentChart
priceData={snapshot.price_data}
indicators={snapshot.indicators}
@@ -715,10 +810,13 @@ export default function InstrumentDashboard() {
onDateHover={handleDateHover}
/>
{/* Event timeline strip */}
<EventTimelineStrip events={snapshot.events} priceData={snapshot.price_data} />
<EventTimelineStrip
events={snapshot.events}
priceData={snapshot.price_data}
drivers={activeDrivers}
/>
{/* Snapshot date badge */}
{/* Date badge */}
<div className="flex items-center gap-2">
<div className={clsx(
'flex items-center gap-1.5 px-3 py-1 rounded-full text-xs font-medium border',
@@ -736,7 +834,6 @@ export default function InstrumentDashboard() {
<div className="grid grid-cols-3 gap-4">
<RegimeCard
regime={snapshot.regime}
config={snapshot.instrument}
signalsAt={dateSignals}
dateLabel={dateLabel}
/>
@@ -747,6 +844,16 @@ export default function InstrumentDashboard() {
<EventsCard events={snapshot.events} />
</div>
{/* Drivers edit panel */}
{editDrivers && (
<DriversPanel
instrumentId={instrumentId}
drivers={activeDrivers}
onSave={saved => { setLocalDrivers(saved); setEditDrivers(false) }}
onClose={() => setEditDrivers(false)}
/>
)}
{/* AI Narrative */}
<NarrativeCard
narrative={narrative}