diff --git a/backend/services/ai_analyzer.py b/backend/services/ai_analyzer.py index 27c7b8a..7eb9d3f 100644 --- a/backend/services/ai_analyzer.py +++ b/backend/services/ai_analyzer.py @@ -95,169 +95,169 @@ def _chat( # ── News / Article Analysis ─────────────────────────────────────────────────── -SYSTEM_NEWS = """Tu es un analyste financier géopolitique senior spécialisé en options. -Tu analyses des actualités et identifies leur impact potentiel sur les marchés financiers. -Réponds UNIQUEMENT en JSON selon le schéma demandé. Sois précis et concis.""" +SYSTEM_NEWS = """You are a senior geopolitical financial analyst specializing in options. +You analyze news and identify their potential impact on financial markets. +Respond ONLY in JSON according to the requested schema. Be precise and concise.""" def analyze_news_item(title: str, summary: str) -> Dict[str, Any]: """Classify and score a single news item with GPT.""" - user = f"""Analyse cet article géopolitique/économique: -Titre: {title} -Résumé: {summary} + user = f"""Analyze this geopolitical/economic article: +Title: {title} +Summary: {summary} -Retourne ce JSON: +Return this JSON: {{ "category": "military|sanctions|elections|natural_disaster|health_crisis|resource_scarcity|trade_war|energy|political_speech|financial_crisis|general", "impact_score": , "direction": "bullish|bearish|neutral|volatile", "affected_assets": {{ - "energy": , - "metals": , - "agriculture": , - "indices": , - "forex": + "energy": , + "metals": , + "agriculture": , + "indices": , + "forex": }}, - "key_entities": [], + "key_entities": [], "horizon": "immediate|days|weeks|months", - "reasoning": "<1 phrase expliquant l'impact>" + "reasoning": "<1 sentence explaining the impact>" }}""" result = _chat(SYSTEM_NEWS, user) if not result: return {"category": "general", "impact_score": 0.1, "direction": "neutral", - "affected_assets": {}, "key_entities": [], "horizon": "days", "reasoning": "AI non disponible"} + "affected_assets": {}, "key_entities": [], "horizon": "days", "reasoning": "AI unavailable"} return result # ── Speech / Text Analysis (Trump, Powell, etc.) ───────────────────────────── -SYSTEM_SPEECH = """Tu es un analyste quantitatif géopolitique. Tu décodes les discours et déclarations -de personnalités politiques/économiques pour identifier des opportunités de trading en options. -Tu te spécialises dans: discours Trump (tarifs, énergie, dollar), Powell/Fed (taux), -leaders géopolitiques (sanctions, guerres, ressources). Réponds en JSON uniquement.""" +SYSTEM_SPEECH = """You are a quantitative geopolitical analyst. You decode speeches and statements +from political/economic figures to identify options trading opportunities. +You specialize in: Trump speeches (tariffs, energy, dollar), Powell/Fed (rates), +geopolitical leaders (sanctions, wars, resources). Respond in JSON only.""" def analyze_speech(text: str, speaker: str = "") -> Dict[str, Any]: """Deep analysis of a speech/statement for trading signals.""" - user = f"""Analyse cette déclaration{'de ' + speaker if speaker else ''} pour des signaux de trading: + user = f"""Analyze this statement{'from ' + speaker if speaker else ''} for trading signals: --- {text[:3000]} --- -Retourne ce JSON: +Return this JSON: {{ - "speaker_identified": "", + "speaker_identified": "", "tone": "hawkish|dovish|aggressive|conciliatory|ambiguous", - "key_statements": [], + "key_statements": [], "market_signals": [ {{ - "asset": "", + "asset": "", "direction": "up|down|volatile", "magnitude": "low|medium|high|extreme", - "reasoning": "", - "timeframe": "" + "reasoning": "", + "timeframe": "" }} ], "options_opportunities": [ {{ - "underlying": "", + "underlying": "", "strategy": "Long Call|Long Put|Bull Call Spread|Bear Put Spread|Long Straddle", "strike_guidance": "", - "expiry_guidance": "<30j|60j|90j>", - "rationale": "", + "expiry_guidance": "<30d|60d|90d>", + "rationale": "", "confidence": , - "capital_1000eur": "" + "capital_1000eur": "" }} ], "risk_level": "low|medium|high|extreme", - "geo_pattern_triggered": "", - "summary": "<2-3 phrases de synthèse pour un trader>" + "geo_pattern_triggered": "", + "summary": "<2-3 sentence summary for a trader>" }}""" result = _chat(SYSTEM_SPEECH, user, model="gpt-4o") if not result: - return {"error": "OpenAI non disponible — vérifier la clé API"} + return {"error": "OpenAI unavailable — check API key"} return result # ── Pattern Evaluation & Creation ──────────────────────────────────────────── -SYSTEM_PATTERN = """Tu es un expert en analyse géopolitique quantitative et en trading d'options. -Tu évalues et améliores des patterns géopolitiques pour un système de trading algorithmique. -Tes évaluations se basent sur des faits historiques vérifiables. Réponds en JSON.""" +SYSTEM_PATTERN = """You are an expert in quantitative geopolitical analysis and options trading. +You evaluate and improve geopolitical patterns for an algorithmic trading system. +Your evaluations are based on verifiable historical facts. Respond in JSON.""" def evaluate_pattern(pattern: Dict[str, Any]) -> Dict[str, Any]: """AI evaluation of a user-defined pattern.""" - user = f"""Évalue ce pattern géopolitique de trading: + user = f"""Evaluate this geopolitical trading pattern: {json.dumps(pattern, ensure_ascii=False, indent=2)} -Retourne ce JSON: +Return this JSON: {{ "quality_score": , "validity": "excellent|good|fair|poor", - "strengths": [], - "weaknesses": [], + "strengths": [], + "weaknesses": [], "suggested_improvements": {{ - "additional_keywords": [], - "additional_triggers": [], - "probability_estimate": , - "expected_move_revision": , - "horizon_revision": + "additional_keywords": [], + "additional_triggers": [], + "probability_estimate": , + "expected_move_revision": , + "horizon_revision": }}, "historical_validation": [ {{ "date": "", - "event": "<événement réel qui confirme le pattern>", - "outcome": "" + "event": "", + "outcome": "" }} ], - "counter_scenarios": [<2-3 scénarios qui invalideraient ce pattern>], - "overall_recommendation": "", - "risk_warnings": [] + "counter_scenarios": [<2-3 scenarios that would invalidate this pattern>], + "overall_recommendation": "", + "risk_warnings": [] }}""" result = _chat(SYSTEM_PATTERN, user, model="gpt-4o") if not result: - return {"error": "OpenAI non disponible", "quality_score": 0} + return {"error": "OpenAI unavailable", "quality_score": 0} return result def suggest_pattern_from_context(context: str) -> Dict[str, Any]: """AI creates a pattern structure from a free-text context description.""" - user = f"""Un trader décrit ce contexte géopolitique et veut créer un pattern de trading: + user = f"""A trader describes this geopolitical context and wants to create a trading pattern: "{context}" -Génère un pattern complet en JSON: +Generate a complete pattern in JSON: {{ - "id": "P_USER_<3 lettres aléatoires>", - "name": "", - "description": "", - "triggers": [], - "keywords": [<10-15 mots-clés anglais pour détecter ce pattern dans les news>], + "id": "P_USER_<3 random letters>", + "name": "", + "description": "", + "triggers": [], + "keywords": [<10-15 English keywords to detect this pattern in news>], "historical_instances": [ - {{"date": "", "event": "<événement réel>", "outcome": ""}} + {{"date": "", "event": "", "outcome": ""}} ], "suggested_trades": [ - {{"strategy": "", "underlying": "", "rationale": ""}} + {{"strategy": "", "underlying": "", "rationale": ""}} ], - "asset_class": "", + "asset_class": "
", "expected_move_pct": , "probability": , "horizon_days": , "confidence_in_pattern": , - "caveats": [] + "caveats": [] }}""" result = _chat(SYSTEM_PATTERN, user, model="gpt-4o") if not result: - return {"error": "OpenAI non disponible"} + return {"error": "OpenAI unavailable"} return result # ── Top 10 Trade Ideas Ranking ──────────────────────────────────────────────── -SYSTEM_RANKING = """Tu es un gestionnaire de portefeuille spécialisé en options. -Tu dois sélectionner et classer les 10 meilleures opportunités de trading options -pour un capital de ~1000€ avec horizon 3 mois, en intégrant le contexte géopolitique actuel. -Privilégie: risque/rendement optimal, liquidité des options, clarté du catalyseur. Réponds en JSON.""" +SYSTEM_RANKING = """You are a portfolio manager specializing in options. +You must select and rank the 10 best options trading opportunities +for a capital of ~€1000 with a 3-month horizon, integrating the current geopolitical context. +Prioritize: optimal risk/reward, options liquidity, clarity of catalyst. Respond in JSON.""" def rank_trade_ideas( pattern_matches: List[Dict], @@ -282,23 +282,23 @@ def rank_trade_ideas( ], } - user = f"""Contexte géopolitique et marché actuel: + user = f"""Current geopolitical and market context: {json.dumps(context, ensure_ascii=False, indent=2)} -Génère les 10 meilleures idées de trades en options pour 1000€ / horizon 3 mois. -Diversifie les classes d'actifs. Inclus au moins: 2 énergie, 1 métal, 1 agri, 2 indices/actions, 1 forex. +Generate the 10 best options trade ideas for €1000 / 3-month horizon. +Diversify across asset classes. Include at least: 2 energy, 1 metal, 1 agri, 2 indices/equities, 1 forex. -Retourne ce JSON: +Return this JSON: {{ "ideas": [ {{ "rank": <1-10>, - "title": "", - "underlying": "", + "title": "", + "underlying": "", "strategy": "Long Call|Long Put|Bull Call Spread|Bear Put Spread|Long Straddle|Long Strangle", - "asset_class": "", - "rationale": "", - "geo_trigger": "", + "asset_class": "", + "rationale": "", + "geo_trigger": "", "strike_guidance": "", "expiry_days": , "expected_move_pct": , @@ -306,13 +306,13 @@ Retourne ce JSON: "target_gain_eur": , "confidence": , "risk_level": "low|medium|high|extreme", - "timing": "", - "invalidation": "" + "timing": "", + "invalidation": "" }} ], - "portfolio_note": "", + "portfolio_note": "", "current_bias": "bullish|bearish|neutral|volatile", - "key_risk": "" + "key_risk": "
" }}""" result = _chat(SYSTEM_RANKING, user, model="gpt-4o") @@ -323,57 +323,57 @@ Retourne ce JSON: # ── Pattern Scoring with Rich Context ──────────────────────────────────────── -DEFAULT_ANALYSIS_TEMPLATE = """Pour chaque pattern, note chaque sous-pilier ET fournis un commentaire 1-2 phrases en français. -Score total = somme exacte des 4 piliers (0-100). +DEFAULT_ANALYSIS_TEMPLATE = """For each pattern, score each sub-pillar AND provide a 1-2 sentence comment in English. +Total score = exact sum of the 4 pillars (0-100). -PILIER 1 — ACTUALITÉS & GÉO-CONTEXTE (30 pts max) - 1a. News géopolitiques (0-12): pertinence des événements récents vs keywords/triggers du pattern - 1b. News macro/économiques (0-10): données macro, publications éco, politiques monétaires/fiscales - 1c. Volume & récence signal (0-8) : nb de sources indépendantes, fraîcheur (<48h = max), cohérence +PILLAR 1 — NEWS & GEO-CONTEXT (30 pts max) + 1a. Geopolitical news (0-12): relevance of recent events vs pattern keywords/triggers + 1b. Macro/economic news (0-10): macro data, economic releases, monetary/fiscal policies + 1c. Signal volume & recency (0-8) : number of independent sources, freshness (<48h = max), consistency -PILIER 2 — CALENDRIER ÉCONOMIQUE (20 pts max) - 2a. Banques centrales (0-10): décisions FOMC/BCE/BoJ/BoE à venir, minutes, discours membres - 2b. Publications macro (0-10): CPI, NFP, PIB, PMI, rapport OPEC — alignement avec le pattern +PILLAR 2 — ECONOMIC CALENDAR (20 pts max) + 2a. Central banks (0-10): upcoming FOMC/ECB/BoJ/BoE decisions, minutes, member speeches + 2b. Macro releases (0-10): CPI, NFP, GDP, PMI, OPEC report — alignment with pattern -PILIER 3 — SIGNAUX DE PRIX (35 pts max) - 3a. Taux & Obligations (0-7): mouvements yields, courbe de taux, spreads crédit - 3b. Énergie & Matières prem. (0-7): or, pétrole, gaz, cuivre, blé — direction et momentum - 3c. Forex (0-7): USD index, EUR/USD, paires émergentes — cohérence avec pattern - 3d. Actions & Indices (0-7): SPX, NDX, rotation sectorielle, breadth, sentiment - 3e. Volatilité & Surface de vol (0-7): - - Régime VIX (niveau absolu et tendance) - - SKEW Index: si >135 → queues chères → PRÉFÉRER spreads (budget = max loss limité) - si <115 → vol bon marché → envisager straddles/strangles si catalyseur binaire - - VVIX: si >100 → straddles/strangles trop chers → préférer directionnels - - OVX (>40) / GVZ (>22): vol sectorielle élevée sur le sous-jacent → primes gonflées, spreads - - Régime surface: contango_calm (idéal vendre vol) vs backwardation_panic (primes explosées) +PILLAR 3 — PRICE SIGNALS (35 pts max) + 3a. Rates & Bonds (0-7): yield moves, yield curve, credit spreads + 3b. Energy & Commodities (0-7): gold, oil, gas, copper, wheat — direction and momentum + 3c. Forex (0-7): USD index, EUR/USD, emerging pairs — consistency with pattern + 3d. Equities & Indices (0-7): SPX, NDX, sector rotation, breadth, sentiment + 3e. Volatility & Vol Surface (0-7): + - VIX regime (absolute level and trend) + - SKEW Index: if >135 → expensive tails → PREFER spreads (budget = limited max loss) + if <115 → cheap vol → consider straddles/strangles if binary catalyst + - VVIX: if >100 → straddles/strangles too expensive → prefer directionals + - OVX (>40) / GVZ (>22): elevated sector vol on the underlying → inflated premiums, use spreads + - Surface regime: contango_calm (ideal to sell vol) vs backwardation_panic (exploded premiums) -PILIER 4 — RISQUE / RÉCOMPENSE (15 pts max) - 4a. Asymétrie R/R (0-10): ratio gain potentiel / prime payée / perte max pour ~1000€ - 4b. Timing d'entrée (0-5) : qualité du point d'entrée vs analogues historiques du pattern +PILLAR 4 — RISK / REWARD (15 pts max) + 4a. R/R Asymmetry (0-10): potential gain / premium paid / max loss ratio for ~€1000 + 4b. Entry timing (0-5) : quality of entry point vs historical analogues of the pattern -Règles: score total = 1a+1b+1c+2a+2b+3a+3b+3c+3d+3e+4a+4b; ne pas dépasser les max; commenter chaque sous-pilier. +Rules: total score = 1a+1b+1c+2a+2b+3a+3b+3c+3d+3e+4a+4b; do not exceed maxima; comment each sub-pillar. -⚠️ RÈGLE ANTI-BIAIS DIRECTIONNELLE (IMPÉRATIVE): -- "expected_direction" indique si le pattern attend une hausse ou une baisse. -- "contra_signals" liste les news AI-scorées qui CONTREDISENT cette direction. -- "has_strong_contra": true = le contexte actuel ANNULE ou INVERSE la thèse du pattern. -→ Si has_strong_contra=true: score total ≤ 40/100 ; sous-pilier 1a geo ≤ 3/12. -→ Si resolution=true dans contra_signals (accord/cessez-le-feu résolvant le conflit trigger): 1a geo = 0-2/12. -→ Indique toujours dans "summary" si le signal est [SUPPORTING], [NEUTRAL] ou [CONTRA].""" +⚠️ ANTI-DIRECTIONAL BIAS RULE (MANDATORY): +- "expected_direction" indicates whether the pattern expects a rise or fall. +- "contra_signals" lists AI-scored news that CONTRADICTS this direction. +- "has_strong_contra": true = current context CANCELS or REVERSES the pattern thesis. +→ If has_strong_contra=true: total score ≤ 40/100; sub-pillar 1a geo ≤ 3/12. +→ If resolution=true in contra_signals (agreement/ceasefire resolving the trigger conflict): 1a geo = 0-2/12. +→ Always indicate in "summary" whether the signal is [SUPPORTING], [NEUTRAL] or [CONTRA].""" -SYSTEM_SCORER = """Tu es un gestionnaire de portefeuille senior spécialisé en options géopolitiques. -Tu analyses des patterns géopolitiques avec leur contexte marché enrichi (news, prix, IV) pour identifier -les meilleures opportunités de trading options (~1000€, horizon 3 mois). -Tu es rigoureux, quantitatif et pragmatique. Réponds UNIQUEMENT en JSON valide. +SYSTEM_SCORER = """You are a senior portfolio manager specializing in geopolitical options. +You analyze geopolitical patterns with their enriched market context (news, prices, IV) to identify +the best options trading opportunities (~€1000, 3-month horizon). +You are rigorous, quantitative and pragmatic. Respond ONLY in valid JSON. -RÈGLE DE DISTRIBUTION DES SCORES (IMPÉRATIVE): -- Les scores doivent refléter une vraie distribution : certains patterns sont forts (70+), d'autres faibles (30-) -- Il est INTERDIT de noter tous les patterns au même score ou proche de 50 -- Base tes scores sur : relevant_news_count (catalyseurs actifs), macro_regime.asset_class_bias, has_strong_contra, horizon vs catalyseurs imminents -- Un pattern sans aucune news récente pertinente (relevant_news_count=0) ne peut pas dépasser 35/100 -- Un pattern avec has_strong_contra=true ne dépasse pas 40/100 -- Un pattern avec 3+ news pertinentes ET macro favorable peut atteindre 70-85/100""" +SCORE DISTRIBUTION RULE (MANDATORY): +- Scores must reflect a real distribution: some patterns are strong (70+), others weak (30-) +- It is FORBIDDEN to score all patterns the same or close to 50 +- Base your scores on: relevant_news_count (active catalysts), macro_regime.asset_class_bias, has_strong_contra, horizon vs imminent catalysts +- A pattern with no relevant recent news (relevant_news_count=0) cannot exceed 35/100 +- A pattern with has_strong_contra=true cannot exceed 40/100 +- A pattern with 3+ relevant news AND favorable macro can reach 70-85/100""" def score_patterns_with_context( @@ -532,7 +532,7 @@ def score_patterns_with_context( "historical_instances": pat.get("historical_instances", [])[:2], "suggested_trades": pat.get("suggested_trades", []), "expected_move_pct": pat.get("expected_move_pct", 0), - "expected_direction": "hausse" if expected_up else "baisse", + "expected_direction": "up" if expected_up else "down", "horizon_days": pat.get("horizon_days", 90), "relevant_news_count": len(relevant_news), "relevant_news": relevant_news, @@ -567,31 +567,31 @@ def score_patterns_with_context( return f"{v:.{decimals}f}{unit}" if v is not None else "n/d" macro_section = f""" -RÉGIME MACRO ACTUEL (50 compteurs — 29 tickers + 9 métriques dérivées): -- Scénario dominant: {dom.upper()} | Scores: {json.dumps(sc_scores, ensure_ascii=False)} +CURRENT MACRO REGIME (50 counters — 29 tickers + 9 derived metrics): +- Dominant scenario: {dom.upper()} | Scores: {json.dumps(sc_scores, ensure_ascii=False)} -SURFACE DE VOLATILITÉ (pilier 3e — impact direct stratégie options): -- SKEW Index: {_fmt(_skew, '', 0)} | Régime vol: {_vol_r} | VVIX: {_fmt(_vvix, '', 0)} | OVX: {_fmt(_ovx, '', 0)} - → SKEW >135 = queues chères → SPREADS (pas de straddles). VVIX >100 = primes gonflées → directionnels. - → {_vol_r} = {"contango calme: idéal spreads bon marché" if _vol_r == "contango_calm" else "backwardation/panique: options très chères, primes à vendre" if _vol_r == "backwardation_panic" else "tail risk élevé: protection queues = faveur spreads définis" if _vol_r == "tail_risk_elevated" else "environnement normal"} +VOLATILITY SURFACE (pillar 3e — direct impact on options strategy): +- SKEW Index: {_fmt(_skew, '', 0)} | Vol regime: {_vol_r} | VVIX: {_fmt(_vvix, '', 0)} | OVX: {_fmt(_ovx, '', 0)} + → SKEW >135 = expensive tails → SPREADS (no straddles). VVIX >100 = inflated premiums → directionals. + → {_vol_r} = {"contango calm: ideal for cheap spreads" if _vol_r == "contango_calm" else "backwardation/panic: very expensive options, premiums to sell" if _vol_r == "backwardation_panic" else "elevated tail risk: tail protection = favor defined spreads" if _vol_r == "tail_risk_elevated" else "normal environment"} -ROTATION SECTORIELLE & RISQUE (pilier 3d + 3e): -- Tech vs Défensifs (XLK-XLP): {_fmt(_tech_st, '%pts')} | Financières vs S&P: {_fmt(_xlf_spx, '%pts')} - → {"Risk-on sectoriel fort" if _tech_st and _tech_st > 0.5 else "Rotation défensive ⚠️" if _tech_st and _tech_st < -0.5 else "Neutre sectoriel"} - → {"Banques saines = pas de récession" if _xlf_spx and _xlf_spx > 0.3 else "Stress bancaire anticipé ⚠️" if _xlf_spx and _xlf_spx < -0.3 else ""} +SECTOR ROTATION & RISK (pillars 3d + 3e): +- Tech vs Defensives (XLK-XLP): {_fmt(_tech_st, '%pts')} | Financials vs S&P: {_fmt(_xlf_spx, '%pts')} + → {"Strong sector risk-on" if _tech_st and _tech_st > 0.5 else "Defensive rotation ⚠️" if _tech_st and _tech_st < -0.5 else "Sector neutral"} + → {"Healthy banks = no recession" if _xlf_spx and _xlf_spx > 0.3 else "Anticipated banking stress ⚠️" if _xlf_spx and _xlf_spx < -0.3 else ""} -GLOBAL / CARRY / QUALITÉ (pilier 3a + 3d): -- EM Actions J+1: {_fmt(_eem_c, '%')} | USD/JPY J+1: {_fmt(_usdjpy_c, '%')} | TLT (20Y bonds) J+1: {_fmt(_tlt_c, '%')} -- Ratio Ag/Or: {_fmt(_sgr, '', 5)} - → {"EM surperforme = croissance globale" if _eem_c and _eem_c > 0.5 else "EM stress = fuite vers US ⚠️" if _eem_c and _eem_c < -1.0 else ""} - → {"JPY s'apprécie = carry unwind = risk-off ⚠️" if _usdjpy_c and _usdjpy_c < -0.8 else "JPY faible = risk-on carry actif" if _usdjpy_c and _usdjpy_c > 0.5 else ""} - → {"TLT monte = flight to quality = bonds longs demandés" if _tlt_c and _tlt_c > 0.3 else "TLT baisse = taux longs remontent = inflation/risk-on" if _tlt_c and _tlt_c < -0.3 else ""} +GLOBAL / CARRY / QUALITY (pillars 3a + 3d): +- EM Equities D+1: {_fmt(_eem_c, '%')} | USD/JPY D+1: {_fmt(_usdjpy_c, '%')} | TLT (20Y bonds) D+1: {_fmt(_tlt_c, '%')} +- Ag/Au Ratio: {_fmt(_sgr, '', 5)} + → {"EM outperforms = global growth" if _eem_c and _eem_c > 0.5 else "EM stress = flight to US ⚠️" if _eem_c and _eem_c < -1.0 else ""} + → {"JPY appreciating = carry unwind = risk-off ⚠️" if _usdjpy_c and _usdjpy_c < -0.8 else "Weak JPY = risk-on carry active" if _usdjpy_c and _usdjpy_c > 0.5 else ""} + → {"TLT rising = flight to quality = long bonds in demand" if _tlt_c and _tlt_c > 0.3 else "TLT falling = long rates rising = inflation/risk-on" if _tlt_c and _tlt_c < -0.3 else ""} -Instructions de notation: -- Intègre le régime dans 3a (taux: slope+TLT), 3b (énergie+OVX), 3d (indices+XLF+EM), 3e (SKEW+VVIX+régime) -- "macro_regime.asset_class_bias" dans chaque pattern → majore/minore 3b ou 3d -- "macro_regime.skew_index" + "vol_surface_regime" → influence directe sur le choix de stratégie dans "recommended_trade" -- Indique dans "summary": [GOLDILOCKS|STAGFLATION|RÉCESSION|DÉSINFLATION|CRISE] + [SUPPORTING|NEUTRAL|CONTRA] +Scoring instructions: +- Integrate regime into 3a (rates: slope+TLT), 3b (energy+OVX), 3d (indices+XLF+EM), 3e (SKEW+VVIX+regime) +- "macro_regime.asset_class_bias" in each pattern → increase/decrease 3b or 3d +- "macro_regime.skew_index" + "vol_surface_regime" → direct influence on strategy choice in "recommended_trade" +- Indicate in "summary": [GOLDILOCKS|STAGFLATION|RECESSION|DISINFLATION|CRISIS] + [SUPPORTING|NEUTRAL|CONTRA] """ # Temporal context block for scoring @@ -605,11 +605,11 @@ Instructions de notation: _partitioned_sc = partition_news_by_age(news_decayed_sc, _delta_min_sc) _inter_sc = _partitioned_sc["inter_cycle"] temporal_section_sc = ( - f"\nCONTEXTE TEMPOREL DU CYCLE:\n" - f"- Dernier cycle il y a : {_delta_min_sc:.0f} minutes | {_calib_sc}\n" - f"- NEWS INTER-CYCLE (non encore pricées, {len(_inter_sc)} articles) : " + f"\nCYCLE TEMPORAL CONTEXT:\n" + f"- Last cycle: {_delta_min_sc:.0f} minutes ago | {_calib_sc}\n" + f"- INTER-CYCLE NEWS (not yet priced, {len(_inter_sc)} articles): " + " | ".join(n.get("title", "")[:60] for n in _inter_sc[:3]) + "\n" - f"⚠️ Tiens compte du délai depuis le dernier cycle pour évaluer si les news sont déjà intégrées.\n" + f"⚠️ Account for the delay since the last cycle to assess whether news is already priced in.\n" ) _tech_sc_section = f"\n{tech_indicators_block}\n" if tech_indicators_block else "" @@ -617,111 +617,111 @@ Instructions de notation: _pd_sc_section = f"\n{price_discovery_block}\n" if price_discovery_block else "" _portfolio_sc_section = f"\n{portfolio_context_block}\n" if portfolio_context_block else "" - user = f"""CONTEXTE GLOBAL: -- Score risque géopolitique: {geo_score.get('score', 50)}/100 ({geo_score.get('level', 'medium')}) -- Top risques: {geo_score.get('top_risks', [])} + user = f"""GLOBAL CONTEXT: +- Geopolitical risk score: {geo_score.get('score', 50)}/100 ({geo_score.get('level', 'medium')}) +- Top risks: {geo_score.get('top_risks', [])} {temporal_section_sc} {macro_section} {_fred_sc_section} {_pd_sc_section} {_tech_sc_section} {_portfolio_sc_section} -TEMPLATE DE NOTATION: +SCORING TEMPLATE: {scoring_template} -PATTERNS À SCORER ({len(pattern_blocks)} patterns): +PATTERNS TO SCORE ({len(pattern_blocks)} patterns): {json.dumps(pattern_blocks, ensure_ascii=False, indent=2)} -Pour chacun des {len(pattern_blocks)} patterns, score chaque sous-pilier + commente en français. -Le champ "score" = somme exacte de tous les sous-piliers. +For each of the {len(pattern_blocks)} patterns, score each sub-pillar + add a comment in English. +The "score" field = exact sum of all sub-pillars. -⚠️ TRADE RANKINGS (OBLIGATOIRE): Un pattern peut avoir plusieurs suggested_trades (ex: Long Call WTI + Bull Spread XLE). -Ces trades ne méritent PAS tous le même score. Pour chaque pattern, remplis "trade_rankings" en: -- classant les trades du meilleur (rank 1) au moins bon -- assignant un "score_delta" entre -20 et +20 (ex: +10 pour le meilleur, 0 pour la moyenne, -8 pour le moins bon) -- expliquant en 1 phrase pourquoi chaque trade est au-dessus/en-dessous de la moyenne du pattern -- la somme des score_delta doit être ≈ 0 (les trades se compensent par rapport au score pattern) +⚠️ TRADE RANKINGS (MANDATORY): A pattern can have multiple suggested_trades (e.g.: Long Call WTI + Bull Spread XLE). +These trades do NOT all deserve the same score. For each pattern, fill "trade_rankings" by: +- ranking trades from best (rank 1) to worst +- assigning a "score_delta" between -20 and +20 (e.g.: +10 for the best, 0 for average, -8 for the worst) +- explaining in 1 sentence why each trade is above/below the pattern average +- the sum of score_delta should be ≈ 0 (trades offset each other relative to the pattern score) -Retourne UNIQUEMENT ce JSON valide: +Return ONLY this valid JSON: {{ "scored_patterns": [ {{ "pattern_id": "", - "score": , + "score": , "confidence": , "buckets": [ {{ "id": "actualites", - "label": "Actualités & Géo-contexte", + "label": "News & Geo-context", "score": <0-30>, "max": 30, - "comment": "", + "comment": "<1-2 sentence summary>", "subs": [ - {{"id": "geo", "label": "News géopolitiques", "score": <0-12>, "max": 12, "comment": "<1-2 phrases>"}}, - {{"id": "eco", "label": "News macro/éco", "score": <0-10>, "max": 10, "comment": "<1-2 phrases>"}}, - {{"id": "flux", "label": "Volume & récence", "score": <0-8>, "max": 8, "comment": "<1-2 phrases>"}} + {{"id": "geo", "label": "Geopolitical news", "score": <0-12>, "max": 12, "comment": "<1-2 sentences>"}}, + {{"id": "eco", "label": "Macro/economic news", "score": <0-10>, "max": 10, "comment": "<1-2 sentences>"}}, + {{"id": "flux", "label": "Volume & recency", "score": <0-8>, "max": 8, "comment": "<1-2 sentences>"}} ] }}, {{ "id": "calendrier", - "label": "Calendrier économique", + "label": "Economic Calendar", "score": <0-20>, "max": 20, - "comment": "", + "comment": "", "subs": [ - {{"id": "banques", "label": "Banques centrales", "score": <0-10>, "max": 10, "comment": "<1-2 phrases>"}}, - {{"id": "macro_cal", "label": "Publications macro", "score": <0-10>, "max": 10, "comment": "<1-2 phrases>"}} + {{"id": "banques", "label": "Central banks", "score": <0-10>, "max": 10, "comment": "<1-2 sentences>"}}, + {{"id": "macro_cal", "label": "Macro releases", "score": <0-10>, "max": 10, "comment": "<1-2 sentences>"}} ] }}, {{ "id": "prix", - "label": "Signaux de prix", + "label": "Price Signals", "score": <0-35>, "max": 35, - "comment": "", + "comment": "", "subs": [ - {{"id": "taux", "label": "Taux & Obligations", "score": <0-7>, "max": 7, "comment": "<1-2 phrases>"}}, - {{"id": "energie", "label": "Énergie & Matières", "score": <0-7>, "max": 7, "comment": "<1-2 phrases>"}}, - {{"id": "forex_sig", "label": "Forex", "score": <0-7>, "max": 7, "comment": "<1-2 phrases>"}}, - {{"id": "actions", "label": "Actions & Indices", "score": <0-7>, "max": 7, "comment": "<1-2 phrases>"}}, - {{"id": "vix", "label": "Volatilité (VIX/IV)", "score": <0-7>, "max": 7, "comment": "<1-2 phrases>"}} + {{"id": "taux", "label": "Rates & Bonds", "score": <0-7>, "max": 7, "comment": "<1-2 sentences>"}}, + {{"id": "energie", "label": "Energy & Commodities", "score": <0-7>, "max": 7, "comment": "<1-2 sentences>"}}, + {{"id": "forex_sig", "label": "Forex", "score": <0-7>, "max": 7, "comment": "<1-2 sentences>"}}, + {{"id": "actions", "label": "Equities & Indices", "score": <0-7>, "max": 7, "comment": "<1-2 sentences>"}}, + {{"id": "vix", "label": "Volatility (VIX/IV)", "score": <0-7>, "max": 7, "comment": "<1-2 sentences>"}} ] }}, {{ "id": "rr", - "label": "Risque / Récompense", + "label": "Risk / Reward", "score": <0-15>, "max": 15, - "comment": "", + "comment": "", "subs": [ - {{"id": "asymetrie", "label": "Asymétrie R/R", "score": <0-10>, "max": 10, "comment": "<1-2 phrases>"}}, - {{"id": "timing_rr", "label": "Timing d'entrée", "score": <0-5>, "max": 5, "comment": "<1-2 phrases>"}} + {{"id": "asymetrie", "label": "R/R Asymmetry", "score": <0-10>, "max": 10, "comment": "<1-2 sentences>"}}, + {{"id": "timing_rr", "label": "Entry timing", "score": <0-5>, "max": 5, "comment": "<1-2 sentences>"}} ] }} ], - "key_catalyst": "", + "key_catalyst": "
", "recommended_trade": {{ - "underlying": "", + "underlying": "", "strategy": "", "strike_guidance": "", "expiry_days": , - "rationale": "", + "rationale": "", "target_gain_eur": , "max_loss_eur": , - "timing_note": "", - "invalidation": "" + "timing_note": "", + "invalidation": "" }}, - "asset_class": "", + "asset_class": "", "geo_trigger": "", - "summary": "", + "summary": "<1 sentence summary>", "trade_rankings": [ {{ "underlying": "", - "strategy": "", + "strategy": "", "rank": <1-N>, - "score_delta": , - "rationale": "<1 phrase: pourquoi ce trade mérite plus/moins que les autres du même pattern>", - "expected_move_pct": + "score_delta": , + "rationale": "<1 sentence: why this trade deserves more/less than others in the same pattern>", + "expected_move_pct": }} ] }} @@ -729,7 +729,7 @@ Retourne UNIQUEMENT ce JSON valide: "analysis_meta": {{ "patterns_analyzed": , "top_bias": "bullish|bearish|neutral|volatile", - "key_risk": "" + "key_risk": "
" }} }}""" @@ -754,22 +754,22 @@ Erreurs à éviter: {' | '.join(str(m) for m in mistakes[:2])} """ lessons_header = f""" {super_scoring_block} -RETOUR DE PERFORMANCE (rapport du {portfolio_lessons.get('created_at','?')[:10]}) : -Bilan global : {portfolio_lessons.get('headline', '')[:150]} -Angles morts détectés : {portfolio_lessons.get('blind_spots', '')[:150]} -Priorités : {portfolio_lessons.get('next_cycle_priorities', '')[:150]} -Leçons : {' | '.join(str(l)[:80] for l in lessons[:3])} -⚠️ Tiens compte du Super Contexte et de ces leçons pour ajuster les scores et les commentaires par pilier. +PERFORMANCE FEEDBACK (report from {portfolio_lessons.get('created_at','?')[:10]}): +Overall summary: {portfolio_lessons.get('headline', '')[:150]} +Blind spots detected: {portfolio_lessons.get('blind_spots', '')[:150]} +Priorities: {portfolio_lessons.get('next_cycle_priorities', '')[:150]} +Lessons: {' | '.join(str(l)[:80] for l in lessons[:3])} +⚠️ Take the Super Context and these lessons into account to adjust scores and pillar comments. """ # Build the per-batch prompt template (static parts) - prompt_header = f"""CONTEXTE GLOBAL: -- Score risque géopolitique: {geo_score.get('score', 50)}/100 ({geo_score.get('level', 'medium')}) -- Top risques: {geo_score.get('top_risks', [])} + prompt_header = f"""GLOBAL CONTEXT: +- Geopolitical risk score: {geo_score.get('score', 50)}/100 ({geo_score.get('level', 'medium')}) +- Top risks: {geo_score.get('top_risks', [])} {macro_section}{lessons_header}{iv_context} {risk_context} -TEMPLATE DE NOTATION: +SCORING TEMPLATE: {scoring_template} """ @@ -782,26 +782,26 @@ TEMPLATE DE NOTATION: _scorer_log.info(f"[Scorer] Batch of {len(batch)} patterns: {ids}") batch_user = ( prompt_header - + f"PATTERNS À SCORER ({len(batch)} patterns):\n" + + f"PATTERNS TO SCORE ({len(batch)} patterns):\n" + json.dumps(batch, ensure_ascii=False, indent=2) - + f"\n\n⚠️ OBLIGATOIRE: Tu dois retourner EXACTEMENT {len(batch)} objets dans scored_patterns — un pour CHAQUE pattern de la liste, SANS EXCEPTION. Même si un pattern a score=0 (non pertinent actuellement), il doit figurer dans la liste.\n\n" - + f"Pour chacun des {len(batch)} patterns, score chaque sous-pilier + commente en français.\n" - + "Le champ \"score\" = somme exacte de tous les sous-piliers.\n\n" - + "🎯 CALIBRATION OBLIGATOIRE — Les scores DOIVENT être différenciés, jamais tous identiques:\n" - + " score 75-100 = catalyseur actif fort, signal clair, timing excellent\n" - + " score 55-74 = signal modéré, contexte favorable mais incertain\n" - + " score 35-54 = neutre, peu de signal, attente\n" - + " score 15-34 = contra-signal, régime défavorable, risque élevé\n" - + " score 0-14 = aucune pertinence dans le contexte actuel\n" - + "⛔ Ne pas mettre tous les patterns à 50 — c'est un échec de calibration.\n" - + " Utilise les relevant_news_count, macro_regime.asset_class_bias, et contra_signals pour vraiment différencier.\n\n" - + "⚠️ TRADE RANKINGS (OBLIGATOIRE): Un pattern peut avoir plusieurs suggested_trades (ex: Long Call WTI + Bull Spread XLE).\n" - + "Ces trades ne méritent PAS tous le même score. Pour chaque pattern, remplis \"trade_rankings\" en:\n" - + "- classant les trades du meilleur (rank 1) au moins bon\n" - + "- assignant un \"score_delta\" entre -20 et +20 (ex: +10 pour le meilleur, 0 pour la moyenne, -8 pour le moins bon)\n" - + "- expliquant en 1 phrase pourquoi chaque trade est au-dessus/en-dessous de la moyenne du pattern\n" - + "- la somme des score_delta doit être ≈ 0 (les trades se compensent par rapport au score pattern)\n\n" - + "Retourne UNIQUEMENT ce JSON valide:\n" + + f"\n\n⚠️ MANDATORY: You must return EXACTLY {len(batch)} objects in scored_patterns — one for EACH pattern in the list, WITHOUT EXCEPTION. Even if a pattern has score=0 (currently irrelevant), it must appear in the list.\n\n" + + f"For each of the {len(batch)} patterns, score each sub-pillar + add a comment in English.\n" + + "The \"score\" field = exact sum of all sub-pillars.\n\n" + + "🎯 MANDATORY CALIBRATION — Scores MUST be differentiated, never all the same:\n" + + " score 75-100 = strong active catalyst, clear signal, excellent timing\n" + + " score 55-74 = moderate signal, favorable but uncertain context\n" + + " score 35-54 = neutral, little signal, waiting\n" + + " score 15-34 = contra-signal, unfavorable regime, high risk\n" + + " score 0-14 = no relevance in current context\n" + + "⛔ Do not score all patterns at 50 — that is a calibration failure.\n" + + " Use relevant_news_count, macro_regime.asset_class_bias, and contra_signals to truly differentiate.\n\n" + + "⚠️ TRADE RANKINGS (MANDATORY): A pattern can have multiple suggested_trades (e.g.: Long Call WTI + Bull Spread XLE).\n" + + "These trades do NOT all deserve the same score. For each pattern, fill \"trade_rankings\" by:\n" + + "- ranking trades from best (rank 1) to worst\n" + + "- assigning a \"score_delta\" between -20 and +20 (e.g.: +10 for the best, 0 for average, -8 for the worst)\n" + + "- explaining in 1 sentence why each trade is above/below the pattern average\n" + + "- the sum of score_delta should be ≈ 0 (trades offset each other relative to the pattern score)\n\n" + + "Return ONLY this valid JSON:\n" + _return_schema ) try: @@ -824,11 +824,11 @@ TEMPLATE DE NOTATION: "score": 0, "confidence": 0, "buckets": [], - "key_catalyst": "Non pertinent dans le contexte actuel", + "key_catalyst": "Not relevant in current context", "recommended_trade": {}, "asset_class": p.get("asset_class", ""), "geo_trigger": p.get("name", ""), - "summary": "[CONTRA] Pattern non pertinent dans le contexte actuel.", + "summary": "[CONTRA] Pattern not relevant in current context.", "trade_rankings": [], }) return scored @@ -891,14 +891,14 @@ TEMPLATE DE NOTATION: # ── Pattern convergence ─────────────────────────────────────────────────────── PATTERN_CATEGORIES = { - "géopolitique": "Conflits armés, sanctions, élections, alliances militaires, tensions régionales", - "macro_monétaire": "Inflation, taux directeurs Fed/BCE, dollar index, yield curve, QE/QT", - "technique": "RSI, Bollinger, momentum, cassures de support/résistance, patterns chartistes", - "commodités_supply":"OPEC, stocks pétrole/gaz, récoltes, disruptions mines, logistique supply chain", - "risk_off": "Catastrophes naturelles, crises bancaires, pandémies, chocs de volatilité", - "flux_saisonnier": "COT flows, saisonnalité, rééquilibrages fin trimestre, options expiry", - "géo_économique": "Chaînes d'approvisionnement, protectionnisme, dédollarisation, reshoring", - "crédit_stress": "Spreads HY, CDS souverains, stress bancaire, risque de défaut", + "géopolitique": "Armed conflicts, sanctions, elections, military alliances, regional tensions", + "macro_monétaire": "Inflation, Fed/ECB policy rates, dollar index, yield curve, QE/QT", + "technique": "RSI, Bollinger, momentum, support/resistance breakouts, chart patterns", + "commodités_supply":"OPEC, oil/gas inventories, harvests, mine disruptions, supply chain logistics", + "risk_off": "Natural disasters, banking crises, pandemics, volatility shocks", + "flux_saisonnier": "COT flows, seasonality, end-of-quarter rebalancing, options expiry", + "géo_économique": "Supply chains, protectionism, de-dollarization, reshoring", + "crédit_stress": "HY spreads, sovereign CDS, banking stress, default risk", } @@ -918,21 +918,21 @@ def classify_patterns_batch(patterns: List[Dict]) -> List[Dict]: for p in patterns ] cats_desc = "\n".join(f"- {c}: {d}" for c, d in PATTERN_CATEGORIES.items()) - system = "Tu es un classificateur de patterns financiers géopolitiques. Réponds UNIQUEMENT en JSON valide." - user = f"""Classifie chaque pattern selon UNE de ces catégories: + system = "You are a geopolitical financial pattern classifier. Respond ONLY in valid JSON." + user = f"""Classify each pattern according to ONE of these categories: {cats_desc} signal_direction: -- "bullish": anticipe une hausse du sous-jacent -- "bearish": anticipe une baisse du sous-jacent -- "volatility": anticipe un mouvement sans direction claire (straddle, vol play) -- "neutral": direction incertaine / stratégie de range +- "bullish": anticipates a rise in the underlying +- "bearish": anticipates a fall in the underlying +- "volatility": anticipates a move without clear direction (straddle, vol play) +- "neutral": uncertain direction / range strategy -Patterns à classifier: +Patterns to classify: {json.dumps(compact, ensure_ascii=False)} -Retourne UNIQUEMENT ce JSON: -{{"classifications": [{{"id": "", "category": "", "signal_direction": ""}}]}}""" +Return ONLY this JSON: +{{"classifications": [{{"id": "", "category": "", "signal_direction": ""}}]}}""" try: res = _chat(system, user, model="gpt-4o-mini", json_mode=True, max_tokens=2000) @@ -1028,18 +1028,18 @@ def _compute_convergence( else: _dir_sym = {"bullish": "▲", "bearish": "▼", "volatility": "⟷", "neutral": "↔"} lines = [ - "\n## 🎯 CONVERGENCE SIGNAUX — Underlyings à forte conviction inter-patterns", - "Ces sous-jacents sont ciblés par plusieurs patterns actifs dans la MÊME direction :", + "\n## 🎯 SIGNAL CONVERGENCE — High-conviction underlyings across patterns", + "These underlyings are targeted by multiple active patterns in the SAME direction:", ] for count, avg_sc, und, direction, pats, cats in conv_lines[:8]: sym = _dir_sym.get(direction, "↔") pat_names = " | ".join(p["name"][:30] for p in pats[:4]) - lines.append(f" {sym} {und} {direction.upper()}: {count} patterns convergents (score moy. {avg_sc:.0f}/100)") - lines.append(f" Catégories: {cats or 'N/A'} | Patterns: {pat_names}") + lines.append(f" {sym} {und} {direction.upper()}: {count} converging patterns (avg score {avg_sc:.0f}/100)") + lines.append(f" Categories: {cats or 'N/A'} | Patterns: {pat_names}") lines += [ "", - "⚠️ Ces convergences signalent une FORTE conviction collective — privilégie des patterns", - "complémentaires sur ces underlyings ou signale un contra-signal si le contexte l'inverse.", + "⚠️ These convergences signal STRONG collective conviction — favor complementary patterns", + "on these underlyings or flag a contra-signal if context reverses.", ] conv_block = "\n".join(lines) @@ -1050,11 +1050,11 @@ def _compute_convergence( # ── Temporal news utilities ─────────────────────────────────────────────────── -# Halflife par catégorie de news (heures) — après combien de temps l'impact est réduit de moitié +# Halflife by news category (hours) — after how long the impact is halved _NEWS_HALFLIFE_HOURS: Dict[str, float] = { - "economic": 4.0, # CPI, NFP, PIB, FOMC → pricés très vite - "geopolitical": 48.0, # conflit, sanction, accord → digestion lente - "corporate": 12.0, # earnings, deal, fusion → digestion moyenne + "economic": 4.0, # CPI, NFP, GDP, FOMC → priced in very fast + "geopolitical": 48.0, # conflict, sanction, deal → slow digestion + "corporate": 12.0, # earnings, deal, merger → medium digestion "default": 24.0, } @@ -1109,9 +1109,9 @@ def apply_news_decay(news: List[Dict]) -> List[Dict]: def partition_news_by_age(news: List[Dict], delta_minutes: float) -> Dict[str, List[Dict]]: """ Split news into temporal buckets relative to cycle delta: - - inter_cycle : published within delta_minutes → potentiellement PAS ENCORE dans les prix - - recent_24h : published 24h → delta_minutes ago → partiellement pricés - - older : > 24h → considérés comme pricés par le marché + - inter_cycle : published within delta_minutes → potentially NOT YET priced in + - recent_24h : published 24h → delta_minutes ago → partially priced in + - older : > 24h → considered already priced in by the market """ delta_hours = delta_minutes / 60.0 inter, recent, older = [], [], [] @@ -1148,9 +1148,9 @@ def _build_temporal_news_block(partitioned: Dict[str, List], cycle_meta: Dict) - age = n.get("age_hours", "?") lines.append( f" • [{n.get('source','')}] {n.get('title','')[:110]}" - f" (impact={score:.2f}, âge={age:.0f}h)" + f" (impact={score:.2f}, age={age:.0f}h)" ) - return "\n".join(lines) if lines else " (aucune)" + return "\n".join(lines) if lines else " (none)" inter = partitioned.get("inter_cycle", []) recent = partitioned.get("recent_24h", []) @@ -1159,36 +1159,36 @@ def _build_temporal_news_block(partitioned: Dict[str, List], cycle_meta: Dict) - # Market session banner if is_weekend: session_banner = ( - f"\n## 📅 STATUT DES MARCHÉS — {day_of_week.upper()}\n" - f"⚠️ WEEKEND — Marchés FERMÉS. {market_note}\n" - "→ Les patterns peuvent être identifiés mais NE PEUVENT PAS être exécutés avant lundi matin.\n" - "→ Utilise ce cycle pour préparer des ordres à cours limité pour l'ouverture de lundi.\n" - "→ Les prix affichés sont ceux de la CLÔTURE DE VENDREDI — ne pas interpréter les mouvements intraday.\n" - "→ La volatilité implicite des options est artificiellement élevée ce weekend (prime weekend des market makers) — les IVR affichés sont surestimés.\n" + f"\n## 📅 MARKET STATUS — {day_of_week.upper()}\n" + f"⚠️ WEEKEND — Markets CLOSED. {market_note}\n" + "→ Patterns can be identified but CANNOT be executed before Monday morning.\n" + "→ Use this cycle to prepare limit orders for Monday's open.\n" + "→ Prices shown are FRIDAY CLOSE prices — do not interpret intraday moves.\n" + "→ Implied volatility is artificially elevated this weekend (market maker weekend premium) — IVR displayed is overstated.\n" ) elif market_session in ("pre_market", "after_hours", "overnight"): session_banner = ( - f"\n## 📅 STATUT DES MARCHÉS — {day_of_week} {market_session.upper()}\n" + f"\n## 📅 MARKET STATUS — {day_of_week} {market_session.upper()}\n" f"{market_note}\n" - "→ Les prix peuvent ne pas refléter la session régulière — liquidité réduite.\n" + "→ Prices may not reflect the regular session — reduced liquidity.\n" ) else: - session_banner = f"\n## 📅 STATUT DES MARCHÉS — {day_of_week} | {market_session.upper()}\n{market_note}\n" if day_of_week else "" + session_banner = f"\n## 📅 MARKET STATUS — {day_of_week} | {market_session.upper()}\n{market_note}\n" if day_of_week else "" block = f""" {session_banner} -## ⏱ CONTEXTE TEMPOREL DU CYCLE -- Dernier cycle il y a : {delta_min:.0f} minutes -- Calibration : {calib} +## ⏱ CYCLE TEMPORAL CONTEXT +- Last cycle: {delta_min:.0f} minutes ago +- Calibration: {calib} -## 📍 NEWS INTER-CYCLE — dernières {delta_min:.0f}min (POTENTIELLEMENT PAS ENCORE dans les prix) +## 📍 INTER-CYCLE NEWS — last {delta_min:.0f}min (POTENTIALLY NOT YET priced in) {_fmt_news(inter, 6)} -*→ Ce sont les nouvelles les plus susceptibles de créer des opportunités non-pricées.* +*→ These are the news most likely to create unpriced opportunities.* -## 📰 NEWS RÉCENTES — 24h (partiellement pricées, decay appliqué) +## 📰 RECENT NEWS — 24h (partially priced, decay applied) {_fmt_news(recent, 5)} -## 📦 NEWS PLUS ANCIENNES — >24h (considérées comme déjà intégrées par le marché) +## 📦 OLDER NEWS — >24h (considered already priced in by the market) {_fmt_news(older, 3)} """ return block @@ -1258,22 +1258,22 @@ def suggest_patterns_from_market_context( bias_lines = "\n".join([f" - {cls}: {b}" for cls, b in asset_bias.items()]) brent_str = f"{brent_chg:+.2f}%" if brent_chg is not None else "N/A" macro_block = f""" -## Régime macro actuel (30 compteurs institutionnels) -- Scénario dominant: {dominant.upper()} | Scores: {json.dumps(scores, ensure_ascii=False)} -- Signaux clés: {', '.join(reasons[:4])} -- Compteurs: VIX={vix} | Pente 10Y-3M={slope}% | Or/Cuivre={gold_cu} | SPX vs 200j={spx_200}% | Brent J-1={brent_str} -- Biais par classe d'actif (scénario {dominant.upper()}): +## Current macro regime (30 institutional counters) +- Dominant scenario: {dominant.upper()} | Scores: {json.dumps(scores, ensure_ascii=False)} +- Key signals: {', '.join(reasons[:4])} +- Counters: VIX={vix} | Slope 10Y-3M={slope}% | Gold/Copper={gold_cu} | SPX vs 200d={spx_200}% | Brent D-1={brent_str} +- Bias by asset class (scenario {dominant.upper()}): {bias_lines} -⚠️ CONTRAINTE: Les patterns proposés doivent être COHÉRENTS avec ce régime macro. -- Favorise les patterns dont l'asset_class a un biais "bullish" ou "bullish+" dans le régime actuel. -- Évite les patterns haussiers sur des classes "bearish" ou "bearish+" sauf si un catalyseur géopolitique exceptionnel le justifie. -- Chaque pattern doit expliquer dans "macro_fit" pourquoi il est compatible (ou en tension) avec le régime {dominant.upper()}. +⚠️ CONSTRAINT: Proposed patterns must be CONSISTENT with this macro regime. +- Favor patterns whose asset_class has a "bullish" or "bullish+" bias in the current regime. +- Avoid bullish patterns on "bearish" or "bearish+" classes unless an exceptional geopolitical catalyst justifies it. +- Each pattern must explain in "macro_fit" why it is compatible (or in tension) with the {dominant.upper()} regime. """ geo_block = "" if geo_score: - geo_block = f"\n## Risque géopolitique global\n- Score: {geo_score.get('score', 50)}/100 ({geo_score.get('level', 'medium')})\n- Top risques: {', '.join(str(r) for r in geo_score.get('top_risks', [])[:3])}\n" + geo_block = f"\n## Global geopolitical risk\n- Score: {geo_score.get('score', 50)}/100 ({geo_score.get('level', 'medium')})\n- Top risks: {', '.join(str(r) for r in geo_score.get('top_risks', [])[:3])}\n" lessons_block = "" if portfolio_lessons: @@ -1284,24 +1284,24 @@ def suggest_patterns_from_market_context( super_block = "" if super_ctx: super_block = f""" -## 🧠 SUPER CONTEXTE — Base de raisonnement accumulée +## 🧠 SUPER CONTEXT — Accumulated reasoning base {super_ctx[:600]} -Priorités stratégiques: {' | '.join(str(p) for p in priorities[:3])} -Erreurs récurrentes à éviter: {' | '.join(str(m) for m in mistakes[:3])} +Strategic priorities: {' | '.join(str(p) for p in priorities[:3])} +Recurring mistakes to avoid: {' | '.join(str(m) for m in mistakes[:3])} """ lessons_block = f""" {super_block} -## ⚡ RETOUR DE PERFORMANCE — cycles précédents (rapport du {portfolio_lessons.get('created_at','?')[:10]}) -Performance globale : {portfolio_lessons.get('headline', '')} -Pourquoi les gains : {portfolio_lessons.get('winners_analysis', '')[:200]} -Pourquoi les pertes : {portfolio_lessons.get('losers_analysis', '')[:200]} -Angles morts détectés : {portfolio_lessons.get('blind_spots', '')[:150]} -Priorités identifiées : {portfolio_lessons.get('next_cycle_priorities', '')[:200]} -Leçons clés : +## ⚡ PERFORMANCE FEEDBACK — previous cycles (report from {portfolio_lessons.get('created_at','?')[:10]}) +Overall performance: {portfolio_lessons.get('headline', '')} +Why gains: {portfolio_lessons.get('winners_analysis', '')[:200]} +Why losses: {portfolio_lessons.get('losers_analysis', '')[:200]} +Blind spots detected: {portfolio_lessons.get('blind_spots', '')[:150]} +Priorities identified: {portfolio_lessons.get('next_cycle_priorities', '')[:200]} +Key lessons: {chr(10).join(f' - {l}' for l in lessons[:4])} -⚠️ CONSIGNE : Tiens compte de ce retour de performance et du Super Contexte pour proposer des patterns MIEUX CIBLÉS. -Évite les erreurs identifiées dans les pertes. Privilégie les types de thèses qui ont fonctionné. +⚠️ INSTRUCTION: Take this performance feedback and the Super Context into account to propose BETTER TARGETED patterns. +Avoid mistakes identified in losses. Favor types of theses that have worked. """ reliability_block = "" @@ -1321,9 +1321,9 @@ Leçons clés : ) if lines: reliability_block = ( - "\n## 📊 FIABILITÉ HISTORIQUE DES PATTERNS (trades matures uniquement)\n" + "\n## 📊 HISTORICAL PATTERN RELIABILITY (mature trades only)\n" + "\n".join(lines) - + "\n⚠️ Inspire-toi des patterns fiables. Évite de reproduire les patterns en bas de liste.\n" + + "\n⚠️ Draw inspiration from reliable patterns. Avoid reproducing the bottom-ranked patterns.\n" ) iv_block = "" @@ -1331,28 +1331,28 @@ Leçons clés : iv_block = f""" {iv_context} -## ⚠️ RÈGLES STRICTES IV → STRATÉGIE (OBLIGATOIRE pour chaque suggested_trade) -Le choix de stratégie doit tenir compte du COÛT de la volatilité implicite (IVR = IV Rank 52 semaines): +## ⚠️ STRICT IV → STRATEGY RULES (MANDATORY for each suggested_trade) +Strategy choice must account for the COST of implied volatility (IVR = IV Rank 52 weeks): -| IVR | Stratégie AUTORISÉE | Stratégie INTERDITE | +| IVR | ALLOWED strategy | FORBIDDEN strategy | |--------------|----------------------------------------------------------|------------------------------| | < 30% (cheap)| Long Call, Long Put, Long Straddle | Iron Condor, Short Strangle | -| 30–60% (mod) | Bull Call Spread, Bear Put Spread | Long Straddle, Strangle naked| -| 60–80% (cher)| Bull/Bear Spread, Cash-Secured Put | Long Call/Put naked | -| > 80% (pic) | Iron Condor, Short Strangle, Covered Call, Cash-Secured Put | TOUTE option long naked | +| 30–60% (mod) | Bull Call Spread, Bear Put Spread | Long Straddle, naked Strangle| +| 60–80% (exp) | Bull/Bear Spread, Cash-Secured Put | naked Long Call/Put | +| > 80% (peak) | Iron Condor, Short Strangle, Covered Call, Cash-Secured Put | ANY naked long option | -Règles supplémentaires: -- Skew put élevé (> 5 pts) → Long Put trop cher → préférer Bear Put Spread -- Term structure backwardation → ne pas vendre la vol (vendeur piégé) -- Si IVR inconnu → utiliser spreads débiteurs par défaut (neutre au coût de vol) -- Long Straddle UNIQUEMENT si IVR < 25% ET catalyseur clairement identifié +Additional rules: +- High put skew (> 5 pts) → Long Put too expensive → prefer Bear Put Spread +- Term structure backwardation → do not sell vol (seller trapped) +- If IVR unknown → use debit spreads by default (vol cost-neutral) +- Long Straddle ONLY if IVR < 25% AND catalyst clearly identified -⚠️ INTERDICTION: Ne jamais suggérer "Long Call" ou "Long Put" (naked) si IVR > 60% sur ce ticker. +⚠️ PROHIBITION: Never suggest "Long Call" or "Long Put" (naked) if IVR > 60% on this ticker. """ # Build temporal news block (partitioned by cycle delta) temporal_news_block = _build_temporal_news_block(_partitioned, _cycle_meta) if _cycle_meta else ( - "## Actualités géopolitiques du moment (triées par impact)\n" + news_block + "## Current geopolitical news (sorted by impact)\n" + news_block ) tech_block_section = f"\n{tech_indicators_block}\n" if tech_indicators_block else "" @@ -1361,66 +1361,66 @@ Règles supplémentaires: portfolio_section = f"\n{portfolio_context_block}\n" if portfolio_context_block else "" convergence_section = f"\n{convergence_block}\n" if convergence_block else "" - user = f"""Tu es un stratège géopolitique et financier senior, expert en options. + user = f"""You are a senior geopolitical and financial strategist, expert in options. {macro_block}{geo_block}{lessons_block}{reliability_block}{iv_block} {temporal_news_block} -## Prix des marchés (variation J-1) +## Market prices (D-1 change) {market_block} {fred_section} {pd_section} {tech_block_section} {portfolio_section} {convergence_section} -## Calendrier économique à venir +## Upcoming economic calendar {cal_block} -En analysant ce panorama, propose 4 à 6 NOUVEAUX patterns géopolitiques qui sont en train d'émerger RIGHT NOW et qui méritent d'être surveillés pour des opportunités d'options. +Analyzing this landscape, propose 4 to 6 NEW geopolitical patterns that are emerging RIGHT NOW and deserve monitoring for options opportunities. -Ne reprend pas les patterns classiques connus (Middle East Oil Spike, Gold Flight to Safety, etc.) — propose des patterns SPÉCIFIQUES au contexte actuel, cohérents avec le régime macro. +Do not repeat well-known classic patterns (Middle East Oil Spike, Gold Flight to Safety, etc.) — propose patterns SPECIFIC to the current context, consistent with the macro regime. -IMPORTANT — STRATÉGIE: Respecte ABSOLUMENT les règles IV→stratégie définies ci-dessus si l'IVR est connu. +IMPORTANT — STRATEGY: Strictly follow the IV→strategy rules defined above if IVR is known. -IMPORTANT — CHAMP expected_move_pct: -Ce champ représente le RENDEMENT OPTION ATTENDU en % (levier inclus), PAS le mouvement du sous-jacent. -Raisonne: si le sous-jacent bouge de X% dans la direction attendue, combien gagne l'option en %? -- Long Call ATM (delta ~0.5, 30-90j): sous-jacent +5% → option +60 à +150% -- Long Call OTM (delta ~0.25): sous-jacent +8% → option +100 à +300% -- Bull Call Spread: sous-jacent +5% → spread +50 à +120% (plafonné) -- Long Straddle: mouvement ±10% → option +80 à +200% -Exemples réalistes: Long Call énergie sur catalyseur fort → 80-200%. Spread défensif → 40-100%. +IMPORTANT — expected_move_pct FIELD: +This field represents the EXPECTED OPTION RETURN in % (leverage included), NOT the move in the underlying. +Reason: if the underlying moves X% in the expected direction, how much does the option gain in %? +- Long Call ATM (delta ~0.5, 30-90d): underlying +5% → option +60 to +150% +- Long Call OTM (delta ~0.25): underlying +8% → option +100 to +300% +- Bull Call Spread: underlying +5% → spread +50 to +120% (capped) +- Long Straddle: ±10% move → option +80 to +200% +Realistic examples: Long Call energy on strong catalyst → 80-200%. Defensive spread → 40-100%. -Retourne UNIQUEMENT ce JSON: +Return ONLY this JSON: {{ "patterns": [ {{ - "name": "", - "description": "", - "macro_fit": "<1-2 phrases: pourquoi ce pattern est cohérent ou en tension avec le régime macro actuel, et quel catalyseur géopolitique le justifie>", + "name": "", + "description": "", + "macro_fit": "<1-2 sentences: why this pattern is consistent or in tension with the current macro regime, and what geopolitical catalyst justifies it>", "triggers": ["", ""], "keywords": ["", "", ""], "asset_class": "", "category": "", "signal_direction": "", - "expected_move_pct": , + "expected_move_pct": , "probability": , "horizon_days": , - "counter_thesis": "<1-2 phrases: principal scénario adverse qui invaliderait ce pattern — sois spécifique (ex: accord de paix inattendu, données CPI sous 3%, etc.)>", - "invalidation_trigger": "<événement précis et mesurable à surveiller — ex: 'prix pétrole < 70$/b 3j consécutifs', 'FOMC hawkish surprise', 'cessez-le-feu Russie-Ukraine'>", - "invalidation_probability": , + "counter_thesis": "<1-2 sentences: main adverse scenario that would invalidate this pattern — be specific (e.g.: unexpected peace deal, CPI data below 3%, etc.)>", + "invalidation_trigger": "", + "invalidation_probability": , "suggested_trades": [ {{ - "strategy": "", - "underlying": "", - "rationale": "", + "strategy": "", + "underlying": "", + "rationale": "", "asset_class": "", - "expected_move_pct": + "expected_move_pct": }}, {{ - "strategy": "", - "underlying": "", + "strategy": "", + "underlying": "", "rationale": "", "asset_class": "", - "expected_move_pct": + "expected_move_pct": }} ] }} @@ -1468,7 +1468,7 @@ For each item return: - dir_metals: "bullish"|"bearish"|"neutral" (for gold/silver/copper) - dir_indices: "bullish"|"bearish"|"neutral" (risk-on vs risk-off) - resolution: true if this is a de-escalation/peace/deal that REDUCES a prior conflict -- insight: "<1 short French sentence on main market effect>" +- insight: "<1 short English sentence on main market effect>" JSON: {{"items": [{{"i":,"impact_score":,"dir_energy":"...","dir_metals":"...","dir_indices":"...","resolution":,"insight":"..."}}]}}""" diff --git a/backend/services/auto_cycle.py b/backend/services/auto_cycle.py index ef632e1..cedb841 100644 --- a/backend/services/auto_cycle.py +++ b/backend/services/auto_cycle.py @@ -49,7 +49,7 @@ def _max_similarity_vs_existing(candidate_kws: List[str], existing: List[Dict]) return max((_jaccard(candidate_kws, p.get("keywords") or []) for p in existing), default=0.0) -_EMBED_SIM_THRESHOLD = 0.75 # seuil cosinus pour considérer deux patterns comme doublons +_EMBED_SIM_THRESHOLD = 0.75 # cosine threshold to consider two patterns as duplicates def _is_duplicate_pattern( @@ -59,8 +59,8 @@ def _is_duplicate_pattern( jaccard_threshold: float = 0.30, ) -> bool: """ - Retourne True si le candidat est trop similaire à un pattern existant. - Essaie les embeddings cosinus (Sprint 4.3) avec fallback Jaccard. + Returns True if the candidate is too similar to an existing pattern. + Tries cosine embeddings (Sprint 4.3) with Jaccard fallback. """ if not existing: return False @@ -77,7 +77,7 @@ def _is_duplicate_pattern( api_key=api_key, candidate_id=candidate.get("id"), ) - if sim > 0: # embedding a fonctionné + if sim > 0: # embedding worked return sim >= _EMBED_SIM_THRESHOLD except Exception as _emb_err: logger.debug(f"[Cycle] Embedding failed, fallback Jaccard: {_emb_err}") @@ -104,13 +104,13 @@ def _run_portfolio_monitor(risk: dict, cycle_id: str) -> dict: n_alerts = len(risk.get("alerts", [])) system_prompt = ( - "Tu es un risk manager spécialisé dans les portefeuilles d'options géopolitiques. " - "Analyse l'état du portefeuille simulé ci-dessous et génère des recommandations concrètes. " - "Réponds en JSON avec ce schéma exact: " - '{"assessment": "<2 phrases max sur l\'état global>", ' + "You are a risk manager specializing in geopolitical options portfolios. " + "Analyze the state of the simulated portfolio below and generate concrete recommendations. " + "Respond in JSON with this exact schema: " + '{"assessment": "", ' '"actions": [{"priority": "high|medium", "type": "close_trade|rebalance|monitor", ' - '"trade_id": , "underlying": "", "reason": ""}], ' - '"rebalance_suggestion": ""}' + '"trade_id": , "underlying": "", "reason": ""}], ' + '"rebalance_suggestion": ""}' ) user_prompt = context diff --git a/backend/services/portfolio_context.py b/backend/services/portfolio_context.py index b173c31..d7a393f 100644 --- a/backend/services/portfolio_context.py +++ b/backend/services/portfolio_context.py @@ -99,13 +99,13 @@ def get_portfolio_concentration(open_trades: List[Dict]) -> Dict[str, int]: def build_portfolio_context_block(open_trades: List[Dict], concentration: Dict[str, int]) -> str: """Build a prompt section describing current portfolio for injection into AI prompts.""" if not open_trades: - return "\n## PORTEFEUILLE ACTUEL\nAucun trade en cours — portefeuille vide.\n" + return "\n## CURRENT PORTFOLIO\nNo open trades — empty portfolio.\n" total = len(open_trades) conc_sorted = sorted(concentration.items(), key=lambda x: -x[1]) conc_str = " | ".join(f"{cls.upper()}: {n}" for cls, n in conc_sorted) - lines: List[str] = [f"Total: {total} trade(s) ouvert(s) | Concentration: {conc_str}", ""] + lines: List[str] = [f"Total: {total} open trade(s) | Concentration: {conc_str}", ""] for t in open_trades: sym = t["underlying"] @@ -118,27 +118,27 @@ def build_portfolio_context_block(open_trades: List[Dict], concentration: Dict[s pat = t.get("pattern_name", "") entry = t.get("entry_price") cur = t.get("current_price") - ep_str = f"entrée {entry:.2f} → actuel {cur:.2f}" if entry and cur else "" + ep_str = f"entry {entry:.2f} → current {cur:.2f}" if entry and cur else "" - line = f" • {sym} | {strat} [{cls}] | {held}j tenu / {rem}j restants | J-1: {m1d_str} | J-5: {m5d_str}" + line = f" • {sym} | {strat} [{cls}] | {held}d held / {rem}d remaining | D-1: {m1d_str} | D-5: {m5d_str}" if ep_str: line += f" | {ep_str}" if pat: - line += f" | thèse: «{pat}»" + line += f" | thesis: «{pat}»" lines.append(line) # Identify overweight classes overweight = [cls for cls, n in conc_sorted if n >= 3] - ow_str = ", ".join(overweight) if overweight else "aucune" + ow_str = ", ".join(overweight) if overweight else "none" block = ( - "\n## PORTEFEUILLE ACTUEL — POSITIONS OUVERTES\n" + "\n## CURRENT PORTFOLIO — OPEN POSITIONS\n" + "\n".join(lines) - + f"\n\nClasses surpondérées (≥3 trades): {ow_str}\n" - + "\n⚠️ CONSIGNES IMPÉRATIVES (non négociables):\n" - + "1. NE PAS suggérer un nouveau trade sur un sous-jacent déjà en portefeuille — doublement interdit.\n" - + "2. Signaler explicitement dans 'rationale' si une suggestion CONTREDIT une position ouverte (signal de clôture potentiel).\n" - + "3. Éviter d'alourdir une classe surpondérée (≥3 trades) sauf catalyseur exceptionnel justifié.\n" - + "4. Un signal opposé à une position ouverte = opportunité de SORTIE à documenter, pas d'entrée inversée.\n" + + f"\n\nOverweight classes (≥3 trades): {ow_str}\n" + + "\n⚠️ MANDATORY RULES (non-negotiable):\n" + + "1. DO NOT suggest a new trade on an underlying already in the portfolio — strictly forbidden.\n" + + "2. Explicitly flag in 'rationale' if a suggestion CONTRADICTS an open position (potential exit signal).\n" + + "3. Avoid adding to an overweight class (≥3 trades) unless an exceptional justified catalyst exists.\n" + + "4. A signal opposing an open position = EXIT opportunity to document, not a reverse entry.\n" ) return block diff --git a/frontend/src/components/TradeIdeas.tsx b/frontend/src/components/TradeIdeas.tsx index b60c5ea..6f35d56 100644 --- a/frontend/src/components/TradeIdeas.tsx +++ b/frontend/src/components/TradeIdeas.tsx @@ -33,25 +33,25 @@ const BUCKET_ICONS: Record = { } export const CATEGORIES = [ - { key: 'all', label: 'Tous' }, - { key: 'energy', label: '⛽ Énergie' }, - { key: 'metals', label: '🥇 Métaux' }, + { key: 'all', label: 'All' }, + { key: 'energy', label: '⛽ Energy' }, + { key: 'metals', label: '🥇 Metals' }, { key: 'agriculture', label: '🌾 Agri' }, { key: 'indices', label: '📊 Indices' }, - { key: 'equities', label: '📈 Actions' }, + { key: 'equities', label: '📈 Equities' }, { key: 'forex', label: '💱 Forex' }, ] export const THEMATIC_CATEGORIES = [ - { key: 'all', label: 'Toutes' }, - { key: 'géopolitique', label: '⚔️ Géopo' }, + { key: 'all', label: 'All' }, + { key: 'géopolitique', label: '⚔️ Geopo' }, { key: 'macro_monétaire', label: '🏦 Macro' }, - { key: 'technique', label: '📐 Technique' }, + { key: 'technique', label: '📐 Technical' }, { key: 'commodités_supply',label: '🛢️ Supply' }, { key: 'risk_off', label: '🌩️ Risk-off' }, - { key: 'flux_saisonnier', label: '📅 Flux' }, - { key: 'géo_économique', label: '🌐 Géo-éco' }, - { key: 'crédit_stress', label: '💳 Crédit' }, + { key: 'flux_saisonnier', label: '📅 Flows' }, + { key: 'géo_économique', label: '🌐 Geo-eco' }, + { key: 'crédit_stress', label: '💳 Credit' }, ] const SIGNAL_DIR: Record = { @@ -80,12 +80,12 @@ export interface TradeItem { } const BIAS_DISPLAY: Record = { - 'bullish+': { label: '★★ Compatible', color: '#10b981' }, + 'bullish+': { label: '★★ Aligned', color: '#10b981' }, 'bullish': { label: '★ Compatible', color: '#34d399' }, - 'neutral': { label: '→ Neutre', color: '#64748b' }, - 'bearish': { label: '✗ Défavorable', color: '#f97316' }, + 'neutral': { label: '→ Neutral', color: '#64748b' }, + 'bearish': { label: '✗ Unfavorable', color: '#f97316' }, 'bearish+': { label: '✗✗ Contra', color: '#ef4444' }, - 'defensive':{ label: '⚠ Défensif', color: '#f59e0b' }, + 'defensive':{ label: '⚠ Defensive', color: '#f59e0b' }, } // ── IBKR helpers ────────────────────────────────────────────────────────────── @@ -234,33 +234,33 @@ function IBKRTicket({ strategy, underlying, strikeGuidance, expiryDays, entryPri return (
- Ticket IBKR - {!entryPrice && Prix non disponible — vérifier dans IBKR} + IBKR Ticket + {!entryPrice && Price unavailable — check in IBKR}
-
Sous-jacent
+
Underlying
{underlying}
Security Type: OPT
-
Stratégie
+
Strategy
{strategy}
- {entryPrice &&
Sous-jacent: ${entryPrice.toFixed(2)}
} + {entryPrice &&
Underlying: ${entryPrice.toFixed(2)}
}
-
Expiration
+
Expiry
- {expiryDate ? format(expiryDate, 'd MMM yyyy', { locale: fr }) : expiryDays ? `~${expiryDays}j` : '—'} + {expiryDate ? format(expiryDate, 'd MMM yyyy', { locale: fr }) : expiryDays ? `~${expiryDays}d` : '—'}
-
{expiryDays ? `${expiryDays}j DTE` : ''}
+
{expiryDays ? `${expiryDays}d DTE` : ''}
{legs.map((leg, i) => (
{leg.action} - 1 contrat + 1 contract {leg.type} Strike {fmtStrike(leg.strike)} @@ -270,17 +270,17 @@ function IBKRTicket({ strategy, underlying, strikeGuidance, expiryDays, entryPri
-
Type d'ordre
+
Order type
LIMIT
-
Prix = prime dans IBKR
+
Price = premium in IBKR
-
Budget max
-
{maxLoss ? `${Math.abs(maxLoss).toFixed(0)}€` : '~1 000€'}
-
Perte max estimée
+
Max budget
+
{maxLoss ? `${Math.abs(maxLoss).toFixed(0)}€` : '~1,000€'}
+
Estimated max loss
-
Cible
+
Target
{targetGain ? `+${targetGain.toFixed(0)}€` : '—'}
@@ -373,7 +373,7 @@ export function TradeCard({ item, onAdd, macroInfo, addedInfo, profiles }: { )}
) : ( - à scorer + to score )}
{convergenceCount > 0 && ( @@ -404,7 +404,7 @@ export function TradeCard({ item, onAdd, macroInfo, addedInfo, profiles }: { <>· {matchedProfile ? ● {matchedProfile.name} - : ✗ {gainPct === 0 ? 'Gain non défini' : 'Aucun profil'} + : ✗ {gainPct === 0 ? 'Gain undefined' : 'No profile'} } )} @@ -435,7 +435,7 @@ export function TradeCard({ item, onAdd, macroInfo, addedInfo, profiles }: { {expanded && (
@@ -463,7 +463,7 @@ export function TradeCard({ item, onAdd, macroInfo, addedInfo, profiles }: { {addedInfo && (
- Ajouté le {format(new Date(addedInfo.entry_date), "d MMM yyyy", { locale: fr })} + Added on {format(new Date(addedInfo.entry_date), "d MMM yyyy", { locale: fr })}
)}
) @@ -560,7 +560,7 @@ export function TradeRow({ item, onAdd, macroInfo, addedInfo, profiles, rank }: )} - ) : à scorer} + ) : to score} {effectiveConviction !== null && ( @@ -646,7 +646,7 @@ export function TradeRow({ item, onAdd, macroInfo, addedInfo, profiles, rank }: {addedInfo && (
- Ajouté le {format(new Date(addedInfo.entry_date), "d MMM yyyy", { locale: fr })} + Added on {format(new Date(addedInfo.entry_date), "d MMM yyyy", { locale: fr })}
)} @@ -826,7 +826,7 @@ export function TradeIdeasTab() { }, { onSuccess: () => { refetchPositions() - setToast({ title: 'Ajouté au portefeuille', sub: `${t.strategy ?? ''} ${t.underlying ?? ''} · ${item.patternName}`.trim() }) + setToast({ title: 'Added to portfolio', sub: `${t.strategy ?? ''} ${t.underlying ?? ''} · ${item.patternName}`.trim() }) }, }) } @@ -844,13 +844,13 @@ export function TradeIdeasTab() {

- Idées de trade + Trade Ideas

{scoredAt && ( - — scoré le {format(new Date(scoredAt), "d MMM à HH:mm", { locale: fr })} + — scored on {format(new Date(scoredAt), "d MMM HH:mm", { locale: fr })} )} {topScored.length > 0 && ( - {topScored.length} scoré{topScored.length > 1 ? 's' : ''} · {allUnscored.length} à scorer + {topScored.length} scored · {allUnscored.length} to score )}
@@ -886,7 +886,7 @@ export function TradeIdeasTab() { 'bg-slate-600 text-white': topN === n, 'text-slate-400 hover:text-slate-200': topN !== n, })}> - {n === 0 ? 'Tous' : `Top ${n}`} + {n === 0 ? 'All' : `Top ${n}`} ))}
@@ -904,10 +904,10 @@ export function TradeIdeasTab() { ) : ( - Clé OpenAI requise + OpenAI key required )}
@@ -927,16 +927,16 @@ export function TradeIdeasTab() { # Pattern / Ticker - Stratégie + Strategy Score EV - Gain - Max/Cible - Profil + Move + Max/Target + Profile Macro - Entrée - Durée + Entry + Duration @@ -956,7 +956,7 @@ export function TradeIdeasTab() { {topScored.length > 0 && (
- {allUnscored.length} trade{allUnscored.length > 1 ? 's' : ''} à scorer + {allUnscored.length} trade{allUnscored.length > 1 ? 's' : ''} to score
)} @@ -982,7 +982,7 @@ export function TradeIdeasTab() { {allPatterns.length === 0 && (
- Démarrer le backend — patterns en cours de chargement… + Start the backend — patterns loading…
)} diff --git a/frontend/src/components/layout/Sidebar.tsx b/frontend/src/components/layout/Sidebar.tsx index 81a8d3a..26cc0b3 100644 --- a/frontend/src/components/layout/Sidebar.tsx +++ b/frontend/src/components/layout/Sidebar.tsx @@ -8,23 +8,23 @@ import clsx from 'clsx' const nav = [ { to: '/', icon: LayoutDashboard, label: 'Cockpit' }, - { to: '/geo', icon: Globe, label: 'Radar Géopolitique' }, - { to: '/markets', icon: BarChart2, label: 'Marchés & Prix' }, - { to: '/macro', icon: Activity, label: 'Régime Macro' }, + { to: '/geo', icon: Globe, label: 'Geopolitical Radar' }, + { to: '/markets', icon: BarChart2, label: 'Markets & Prices' }, + { to: '/macro', icon: Activity, label: 'Macro Regime' }, { to: '/options', icon: TrendingUp, label: 'Options Lab' }, { to: '/patterns', icon: Zap, label: 'Patterns' }, - { to: '/portfolio', icon: DollarSign, label: 'Portefeuille' }, - { to: '/journal', icon: BookOpen, label: 'Journal de Bord' }, - { to: '/rapport', icon: FileBarChart, label: 'Rapport de Cycle' }, - { to: '/super-contexte', icon: Brain, label: 'Super Contexte' }, + { to: '/portfolio', icon: DollarSign, label: 'Portfolio' }, + { to: '/journal', icon: BookOpen, label: 'Trading Journal' }, + { to: '/rapport', icon: FileBarChart, label: 'Cycle Report' }, + { to: '/super-contexte', icon: Brain, label: 'Super Context' }, { to: '/analytics', icon: FlaskConical, label: 'Analytics' }, - { to: '/analytics-advanced', icon: Microscope, label: 'Analytics Avancées' }, + { to: '/analytics-advanced', icon: Microscope, label: 'Advanced Analytics' }, { to: '/risk', icon: ShieldAlert, label: 'Risk Dashboard' }, - { to: '/var', icon: Gauge, label: 'VaR Analyse' }, - { to: '/position-history', icon: GitCompare, label: 'Historique Positions' }, + { to: '/var', icon: Gauge, label: 'VaR Analysis' }, + { to: '/position-history', icon: GitCompare, label: 'Position History' }, { to: '/backtest', icon: History, label: 'Backtest' }, - { to: '/calendar', icon: Calendar, label: 'Calendrier' }, - { to: '/logs', icon: ScrollText, label: 'Logs Système' }, + { to: '/calendar', icon: Calendar, label: 'Calendar' }, + { to: '/logs', icon: ScrollText, label: 'System Logs' }, { to: '/config', icon: Settings, label: 'Configuration' }, ] @@ -56,7 +56,7 @@ export default function Sidebar() { {/* Geo risk indicator */} {riskScore && (
-
Risque Géo
+
Geo Risk
{riskScore.score}/100
{riskScore.level}
@@ -65,7 +65,7 @@ export default function Sidebar() { {/* Portfolio mini summary */} {summary && (summary.open_positions > 0 || summary.unrealized_pnl !== 0) && (
-
Portefeuille
+
Portfolio
{summary.open_positions} positions = 0 ? 'text-emerald-400' : 'text-red-400')}> @@ -97,7 +97,7 @@ export default function Sidebar() { 'bg-dark-700 text-slate-600': !aiStatus?.enabled, })}> - {aiStatus?.enabled ? 'IA GPT-4o active' : 'IA non configurée'} + {aiStatus?.enabled ? 'AI GPT-4o active' : 'AI not configured'}
diff --git a/frontend/src/pages/Analytics.tsx b/frontend/src/pages/Analytics.tsx index 7094520..b5193ed 100644 --- a/frontend/src/pages/Analytics.tsx +++ b/frontend/src/pages/Analytics.tsx @@ -8,8 +8,8 @@ function ReliabilityTable({ data }: { data: any[] }) { return (
-
Aucune donnée de fiabilité
-
Les données apparaissent après 3+ trades matures (≥35% de l'horizon écoulé) par pattern
+
No reliability data
+
Data appears after 3+ mature trades (≥35% of horizon elapsed) per pattern
) } @@ -22,10 +22,10 @@ function ReliabilityTable({ data }: { data: any[] }) { Pattern Trades Win Rate - PnL moyen + Avg PnL Max gain - Max perte - Score fiabilité + Max loss + Reliability score @@ -69,8 +69,8 @@ function CalibrationSection({ data }: { data: any }) { return (
-
Pas encore de données de calibration
-
Nécessite des trades matures avec probabilité stockée vs résultat réalisé
+
No calibration data yet
+
Requires mature trades with stored probability vs realized outcome
) } @@ -84,31 +84,31 @@ function CalibrationSection({ data }: { data: any }) {
{brier_score?.toFixed(3) ?? '—'}
Brier Score
-
(0 = parfait, 1 = nul)
+
(0 = perfect, 1 = null)
{sample_size}
-
Trades analysés
+
Trades analyzed
{interpretation ?? '—'}
-
Interprétation
+
Interpretation
{/* Calibration buckets */} {buckets && buckets.length > 0 && (
-
Calibration par décile (prédit vs réalisé)
+
Calibration by decile (predicted vs realized)
- - - + + + - + @@ -142,7 +142,7 @@ function CalibrationSection({ data }: { data: any }) {
Prob. préditeTaux réelBiaisPredicted prob.Actual rateBias TradesBarreBar
- Barre grise = prob. prédite · Barre colorée = taux réalisé · Vert si réel ≥ prédit, rouge sinon + Grey bar = predicted prob. · Colored bar = realized rate · Green if actual ≥ predicted, red otherwise
)} @@ -166,7 +166,7 @@ export default function Analytics() { Analytics & Calibration

- Fiabilité historique des patterns · Calibration probabiliste · Brier score + Historical pattern reliability · Probabilistic calibration · Brier score

@@ -175,18 +175,18 @@ export default function Analytics() {
{reliability.length}
-
Patterns avec historique
+
Patterns with history
{reliability.reduce((s: number, r: any) => s + r.trade_count, 0)}
-
Trades matures analysés
+
Mature trades analyzed
{topPatterns[0] && (
- Meilleur pattern + Best pattern
{topPatterns[0].pattern_name}
{topPatterns[0].win_rate_pct}% WR
@@ -195,7 +195,7 @@ export default function Analytics() { {bottomPatterns[0] && (
- À éviter + To avoid
{bottomPatterns[0].pattern_name}
{bottomPatterns[0].win_rate_pct}% WR
@@ -207,8 +207,8 @@ export default function Analytics() { {/* Reliability table */}
- Fiabilité par pattern - (trades ≥35% de l'horizon uniquement) + Reliability by pattern + (trades ≥35% of horizon only)
{loadingR ? (
{[1,2,3].map(i =>
)}
@@ -221,17 +221,17 @@ export default function Analytics() {
- Calibration probabiliste + Probabilistic calibration
{loadingC ? ( diff --git a/frontend/src/pages/AnalyticsAdvanced.tsx b/frontend/src/pages/AnalyticsAdvanced.tsx index d0e2c5b..c48783d 100644 --- a/frontend/src/pages/AnalyticsAdvanced.tsx +++ b/frontend/src/pages/AnalyticsAdvanced.tsx @@ -11,8 +11,8 @@ function BayesianTable({ data }: { data: any[] }) { return (
-
Pas encore de données bayésiennes
-
Les posteriors se calculent après des trades matures (≥35% de l'horizon)
+
No Bayesian data yet
+
Posteriors are computed after mature trades (≥35% of the horizon)
) } @@ -29,11 +29,11 @@ function BayesianTable({ data }: { data: any[] }) { Pattern Prior GPT - WR Bayésien - IC 95% - Trades matures - Dérive - Confiance + Bayesian WR + CI 95% + Mature trades + Drift + Confidence @@ -78,7 +78,7 @@ function BayesianTable({ data }: { data: any[] }) { )} {withoutData.length > 0 && (
- {withoutData.length} pattern(s) sans trades matures — prior GPT uniquement + {withoutData.length} pattern(s) without mature trades — GPT prior only
)}
@@ -92,8 +92,8 @@ function TransitionMatrix({ data }: { data: any }) { return (
-
Pas encore de transitions détectées
-
Les clusters se remplissent au fil des cycles
+
No transitions detected yet
+
Clusters fill up as cycles run
) } @@ -104,13 +104,13 @@ function TransitionMatrix({ data }: { data: any }) { return (
- Probabilité de transition d'un régime à l'autre (lignes = source, colonnes = destination) - · {data.n_transitions} transitions totales + Transition probability from one regime to another (rows = source, columns = destination) + · {data.n_transitions} total transitions
- + {ids.map(dst => (
De ↓ / Vers →From ↓ / To →
C{dst}
@@ -145,7 +145,7 @@ function TransitionMatrix({ data }: { data: any }) {
- Diagonal = reste dans le même cluster · Vert = transition dominante · Bleu = auto-transition + Diagonal = stays in same cluster · Green = dominant transition · Blue = self-transition
) @@ -165,7 +165,7 @@ function ClusterTimeline({ data }: { data: any[] }) { if (!data || data.length === 0) { return (
- Aucun cluster détecté encore — lancez un cycle pour démarrer + No cluster detected yet — run a cycle to get started
) } @@ -187,7 +187,7 @@ function ClusterTimeline({ data }: { data: any[] }) { {recent.map((r: any, i: number) => (
- Chaque carré = 1 snapshot · Blanc cerclé = anomalie détectée · {data.length} entrées au total + Each square = 1 snapshot · White ring = anomaly detected · {data.length} total entries
{/* Latest cluster detail */} {data[0] && (
-
Cluster actuel
+
Current cluster
{data[0].cluster_label} ({data[0].dominant_regime}) {data[0].anomaly_flag ? ( - Anomalie + Anomaly ) : null}
@@ -228,21 +228,21 @@ function EmbeddingsSummary({ data }: { data: any[] }) { return (
- Aucun embedding disponible — ils se génèrent automatiquement lors du filtrage des patterns + No embeddings available — they are generated automatically during pattern filtering
) } return (
-
{data.length} patterns vectorisés
+
{data.length} vectorized patterns
- - - + + + @@ -301,10 +301,10 @@ export default function AnalyticsAdvanced() {

- Analytics Avancées — Phase 4 + Advanced Analytics — Phase 4

- Bayesian updating · Clustering de régimes · Embeddings sémantiques + Bayesian updating · Regime clustering · Semantic embeddings

@@ -322,7 +322,7 @@ export default function AnalyticsAdvanced() { className="flex items-center gap-1.5 px-3 py-1.5 text-xs bg-blue-600/20 hover:bg-blue-600/30 border border-blue-500/30 text-blue-300 rounded transition-colors disabled:opacity-50" > - Détecter régime + Detect regime
@@ -333,25 +333,25 @@ export default function AnalyticsAdvanced() {
{patternsWithData.length}
-
Patterns bayésiens actifs
+
Active Bayesian patterns
{avgBayesWR !== null ? `${avgBayesWR}%` : '—'}
-
Win rate bayésien moyen
+
Average Bayesian win rate
{latestCluster ? `C${latestCluster.cluster_id}` : '—'}
- {latestCluster?.cluster_label ?? 'Aucun cluster'} + {latestCluster?.cluster_label ?? 'No cluster'}
{embeddings.length}
-
Patterns vectorisés
+
Vectorized patterns
@@ -360,8 +360,8 @@ export default function AnalyticsAdvanced() {
- Posteriors bayésiens par pattern - Beta(α,β) · IC 95% + Bayesian posteriors by pattern + Beta(α,β) · CI 95%
{bayesUpdate.data && ( {bayesUpdate.data.message} @@ -374,9 +374,9 @@ export default function AnalyticsAdvanced() { )}
- Lecture : Prior GPT = probabilité annoncée par GPT-4o · - WR Bayésien = win rate observé avec lissage Laplace (β a priori faible) · - Dérive = écart posterior − prior · IC 95% basé sur la distribution Beta + Reading: GPT Prior = probability stated by GPT-4o · + Bayesian WR = observed win rate with Laplace smoothing (weak β prior) · + Drift = posterior − prior gap · CI 95% based on Beta distribution
@@ -387,16 +387,16 @@ export default function AnalyticsAdvanced() {
- Historique des clusters + Cluster history
{loadingClusters ? ( @@ -410,8 +410,8 @@ export default function AnalyticsAdvanced() {
- Matrice de transition - P(régime j | régime i) + Transition matrix + P(regime j | regime i)
{loadingTrans ? (
@@ -425,9 +425,9 @@ export default function AnalyticsAdvanced() {
- Embeddings sémantiques + Semantic embeddings - text-embedding-3-small · Similarité cosinus remplace Jaccard + text-embedding-3-small · Cosine similarity replaces Jaccard
{loadingEmb ? ( @@ -436,9 +436,9 @@ export default function AnalyticsAdvanced() { )}
- Comment ça marche : Chaque nouveau pattern suggéré est vectorisé et comparé - aux patterns existants via similarité cosinus (seuil : 0.75). Si la similarité est > 0.75, - le pattern est rejeté comme doublon sémantique — plus précis que Jaccard qui ne voit que les mots communs. + How it works: Each newly suggested pattern is vectorized and compared + to existing patterns via cosine similarity (threshold: 0.75). If similarity is > 0.75, + the pattern is rejected as a semantic duplicate — more precise than Jaccard which only sees common words.
diff --git a/frontend/src/pages/Backtest.tsx b/frontend/src/pages/Backtest.tsx index bd272da..3bbac9c 100644 --- a/frontend/src/pages/Backtest.tsx +++ b/frontend/src/pages/Backtest.tsx @@ -63,7 +63,7 @@ export default function Backtest() { Backtest & Simulation

- Testez vos stratégies options sur des données historiques réelles + Test your options strategies on real historical data

@@ -74,7 +74,7 @@ export default function Backtest() {
Configuration
- +
{SYMBOLS.slice(0, 7).map(s => ( ))}
- + - {isPending ? 'Calcul...' : 'Lancer le backtest'} + {isPending ? 'Computing...' : 'Run backtest'}
@@ -196,12 +196,12 @@ export default function Backtest() { {/* KPIs */}
= 0 ? '+' : ''}${typed.total_return_pct.toFixed(2)}%`} positive={typed.total_return_pct >= 0} /> = 50} @@ -221,9 +221,9 @@ export default function Backtest() { {/* Equity curve */}
-
Courbe d'équité — {form.symbol} {STRATEGIES.find(s => s.key === form.strategy)?.label}
+
Equity curve — {form.symbol} {STRATEGIES.find(s => s.key === form.strategy)?.label}
- Capital final: {typed.final_capital.toFixed(2)}€ + Final capital: {typed.final_capital.toFixed(2)}€ {' '}(initial: {form.capital}€ · P&L: {typed.total_pnl >= 0 ? '+' : ''}{typed.total_pnl.toFixed(2)}€)
@@ -254,17 +254,17 @@ export default function Backtest() { {/* Last trades */}
-
Derniers trades exécutés
+
Last executed trades
PatternClasseModèleMis à jourClassModelUpdated
- - - + + + - - + + @@ -298,8 +298,8 @@ export default function Backtest() {
-
Configurer et lancer un backtest
-
Données yfinance — historique complet disponible
+
Configure and run a backtest
+
yfinance data — full history available
)} diff --git a/frontend/src/pages/CalendarPage.tsx b/frontend/src/pages/CalendarPage.tsx index 89ffcf0..4f89d8f 100644 --- a/frontend/src/pages/CalendarPage.tsx +++ b/frontend/src/pages/CalendarPage.tsx @@ -3,7 +3,6 @@ import clsx from 'clsx' import { Calendar, Clock, Globe, AlertTriangle } from 'lucide-react' import type { EconomicEvent, AssetClass } from '../types' import { format, parseISO, isAfter, isBefore, addDays } from 'date-fns' -import { fr } from 'date-fns/locale' const ASSET_EMOJIS: Record = { energy: '⛽', metals: '🥇', agriculture: '🌾', equities: '📈', @@ -16,9 +15,9 @@ const COUNTRY_FLAGS: Record = { } const IMPORTANCE_CONFIG: Record = { - high: { color: 'text-red-400 border-red-700/40', label: 'Majeur', dots: 3 }, - medium: { color: 'text-yellow-400 border-yellow-700/40', label: 'Modéré', dots: 2 }, - low: { color: 'text-slate-400 border-slate-700/40', label: 'Mineur', dots: 1 }, + high: { color: 'text-red-400 border-red-700/40', label: 'Major', dots: 3 }, + medium: { color: 'text-yellow-400 border-yellow-700/40', label: 'Moderate', dots: 2 }, + low: { color: 'text-slate-400 border-slate-700/40', label: 'Minor', dots: 1 }, } function EventCard({ ev }: { ev: EconomicEvent }) { @@ -42,19 +41,19 @@ function EventCard({ ev }: { ev: EconomicEvent }) { {'●'.repeat(cfg.dots)} - {isToday && AUJOURD'HUI} - {isSoon && !isToday && BIENTÔT} + {isToday && TODAY} + {isSoon && !isToday && SOON}
{ev.title}
- {format(evDate, "EEEE d MMM yyyy", { locale: fr })} · {ev.country} + {format(evDate, "EEEE, MMM d yyyy")} · {ev.country}
{cfg.label}
- {ev.previous &&
Préc: {ev.previous}
} - {ev.forecast &&
Prév: {ev.forecast}
} - {ev.actual &&
Réel: {ev.actual}
} + {ev.previous &&
Prev: {ev.previous}
} + {ev.forecast &&
Fcst: {ev.forecast}
} + {ev.actual &&
Actual: {ev.actual}
}
{ev.asset_impact && ev.asset_impact.length > 0 && ( @@ -84,25 +83,25 @@ export default function CalendarPage() {

- Calendrier Économique & Géopolitique + Economic & Geopolitical Calendar

- Événements macro, catalyseurs géopolitiques, dates clés + Macro events, geopolitical catalysts, key dates

{/* Legend */}
-
●●● Majeur (forte volatilité attendue)
-
●● Modéré
-
Mineur
+
●●● Major (high volatility expected)
+
●● Moderate
+
Minor
{/* Economic calendar */}
- Événements à venir ({upcoming.length}) + Upcoming events ({upcoming.length})
{isLoading ? ( [1,2,3].map(i =>
) @@ -110,14 +109,14 @@ export default function CalendarPage() { upcoming.map((ev, i) => ) ) : (
- Démarrer le backend pour charger le calendrier + Start the backend to load the calendar
)} {past.length > 0 && ( <>
- Événements passés ({past.length}) + Past events ({past.length})
{past.map((ev, i) => )} @@ -128,7 +127,7 @@ export default function CalendarPage() {
- Alertes géopolitiques + Geopolitical alerts
{highImpactNews.length > 0 ? (
@@ -145,20 +144,20 @@ export default function CalendarPage() { ))}
) : ( -
Charger les actualités géopolitiques
+
Load geopolitical news
)}
{/* Trade opportunity windows */}
-
Fenêtres d'opportunité
+
Opportunity windows
{[ - { window: 'Pré-FOMC (-3j)', strategy: 'Straddle sur SPY', rationale: 'IV monte avant décision' }, - { window: 'Pré-NFP (-2j)', strategy: 'Straddle sur indices', rationale: 'Directional uncertainty' }, - { window: 'Post-OPEC', strategy: 'Bull Call Spread USO', rationale: 'Cut → oil spike probable' }, - { window: 'Pré-USDA Crop', strategy: 'Long Call WEAT', rationale: 'Supply news catalyst' }, - { window: 'Élections US approche', strategy: 'Long VIX Call', rationale: 'Vol expansion garantie' }, + { window: 'Pre-FOMC (-3d)', strategy: 'Straddle on SPY', rationale: 'IV rises ahead of decision' }, + { window: 'Pre-NFP (-2d)', strategy: 'Straddle on indices', rationale: 'Directional uncertainty' }, + { window: 'Post-OPEC', strategy: 'Bull Call Spread USO', rationale: 'Cut → oil spike likely' }, + { window: 'Pre-USDA Crop', strategy: 'Long Call WEAT', rationale: 'Supply news catalyst' }, + { window: 'US Election approaching', strategy: 'Long VIX Call', rationale: 'Vol expansion guaranteed' }, ].map((op, i) => (
{op.window}
@@ -172,16 +171,16 @@ export default function CalendarPage() { {/* Geo-event impact guide */}
- Guide d'impact + Impact guide
{[ - { event: 'Conflit Moyen-Orient', impact: 'Oil +10-20%', cls: 'energy' }, - { event: 'Sanctions Russie', impact: 'Gaz +15-40%', cls: 'energy' }, - { event: 'Tarifs US-Chine', impact: 'Soja -8%', cls: 'agriculture' }, - { event: 'Crise sanitaire', impact: 'Or +7-12%', cls: 'metals' }, - { event: 'Hausses Fed', impact: 'USD +2-4%', cls: 'forex' }, - { event: 'Guerre Ukraine', impact: 'Blé +15-50%', cls: 'agriculture' }, + { event: 'Middle East conflict', impact: 'Oil +10-20%', cls: 'energy' }, + { event: 'Russia sanctions', impact: 'Gas +15-40%', cls: 'energy' }, + { event: 'US-China tariffs', impact: 'Soy -8%', cls: 'agriculture' }, + { event: 'Health crisis', impact: 'Gold +7-12%', cls: 'metals' }, + { event: 'Fed hikes', impact: 'USD +2-4%', cls: 'forex' }, + { event: 'Ukraine war', impact: 'Wheat +15-50%', cls: 'agriculture' }, ].map((g, i) => (
{g.event} diff --git a/frontend/src/pages/Config.tsx b/frontend/src/pages/Config.tsx index a2db1a3..e1761e2 100644 --- a/frontend/src/pages/Config.tsx +++ b/frontend/src/pages/Config.tsx @@ -7,39 +7,39 @@ import clsx from 'clsx' const API = '' const SOURCE_CATEGORIES = { - 'Flux RSS actifs': ['reuters_world', 'reuters_business', 'reuters_energy', 'ap_top', 'aljazeera', 'ft', 'bloomberg'], - 'Données économiques': ['newsapi', 'gdelt', 'eia', 'fred', 'usda'], - 'Santé & Catastrophes': ['who', 'emdat'], - 'Réseaux sociaux': ['twitter_trump'], + 'Active RSS Feeds': ['reuters_world', 'reuters_business', 'reuters_energy', 'ap_top', 'aljazeera', 'ft', 'bloomberg'], + 'Economic Data': ['newsapi', 'gdelt', 'eia', 'fred', 'usda'], + 'Health & Disasters': ['who', 'emdat'], + 'Social Media': ['twitter_trump'], } const SOURCE_DOCS: Record = { - reuters_world: { description: 'Actualités mondiales Reuters', cost: 'Gratuit' }, - reuters_business: { description: 'Business et marchés Reuters', cost: 'Gratuit' }, - reuters_energy: { description: 'Énergie et commodités Reuters', cost: 'Gratuit' }, - ap_top: { description: 'Associated Press — Top Stories', cost: 'Gratuit' }, - aljazeera: { description: 'Al Jazeera — couverture Moyen-Orient/Monde', cost: 'Gratuit' }, - ft: { description: 'Financial Times — finance internationale', cost: 'Gratuit' }, - bloomberg: { description: 'Bloomberg Markets RSS', cost: 'Gratuit' }, - newsapi: { description: '100 req/jour gratuit — actualités mondiales multi-sources', link: 'https://newsapi.org', cost: '100 req/j gratuit' }, - gdelt: { description: 'Base de données géopolitique mondiale — 300K events/jour', link: 'https://gdeltproject.org', cost: 'Gratuit total' }, - eia: { description: 'US Energy Information Administration — données pétrole/gaz hebdo', link: 'https://www.eia.gov/opendata', cost: 'Gratuit avec clé' }, - fred: { description: 'Federal Reserve St. Louis — macro US (PIB, inflation, emploi)', link: 'https://fred.stlouisfed.org/docs/api/fred/', cost: 'Gratuit avec clé' }, - usda: { description: 'USDA — rapports agricoles officiels US', cost: 'Gratuit' }, - who: { description: 'OMS — alertes sanitaires mondiales RSS', cost: 'Gratuit' }, - emdat: { description: 'EM-DAT — base de données catastrophes naturelles', link: 'https://www.emdat.be', cost: 'Inscription gratuite' }, - twitter_trump: { description: 'Flux X/Twitter Trump — discours, annonces tarifs', cost: 'API payante ($100/mois+)' }, + reuters_world: { description: 'Reuters World News', cost: 'Free' }, + reuters_business: { description: 'Reuters Business & Markets', cost: 'Free' }, + reuters_energy: { description: 'Reuters Energy & Commodities', cost: 'Free' }, + ap_top: { description: 'Associated Press — Top Stories', cost: 'Free' }, + aljazeera: { description: 'Al Jazeera — Middle East/World coverage', cost: 'Free' }, + ft: { description: 'Financial Times — international finance', cost: 'Free' }, + bloomberg: { description: 'Bloomberg Markets RSS', cost: 'Free' }, + newsapi: { description: '100 req/day free — multi-source world news', link: 'https://newsapi.org', cost: '100 req/d free' }, + gdelt: { description: 'Global geopolitical database — 300K events/day', link: 'https://gdeltproject.org', cost: 'Totally free' }, + eia: { description: 'US Energy Information Administration — weekly oil/gas data', link: 'https://www.eia.gov/opendata', cost: 'Free with key' }, + fred: { description: 'Federal Reserve St. Louis — US macro (GDP, inflation, employment)', link: 'https://fred.stlouisfed.org/docs/api/fred/', cost: 'Free with key' }, + usda: { description: 'USDA — official US agricultural reports', cost: 'Free' }, + who: { description: 'WHO — global health alerts RSS', cost: 'Free' }, + emdat: { description: 'EM-DAT — natural disasters database', link: 'https://www.emdat.be', cost: 'Free registration' }, + twitter_trump: { description: 'X/Twitter Trump feed — speeches, tariff announcements', cost: 'Paid API ($100/mo+)' }, } // ── Risk Profiles Component ─────────────────────────────────────────────────── const PROFILE_COLORS = [ - { value: '#22c55e', label: 'Vert' }, - { value: '#3b82f6', label: 'Bleu' }, + { value: '#22c55e', label: 'Green' }, + { value: '#3b82f6', label: 'Blue' }, { value: '#f97316', label: 'Orange' }, - { value: '#ef4444', label: 'Rouge' }, - { value: '#8b5cf6', label: 'Violet' }, - { value: '#eab308', label: 'Jaune' }, + { value: '#ef4444', label: 'Red' }, + { value: '#8b5cf6', label: 'Purple' }, + { value: '#eab308', label: 'Yellow' }, ] function NextRunCountdown({ nextRunAt }: { nextRunAt: string }) { @@ -64,16 +64,16 @@ function NextRunCountdown({ nextRunAt }: { nextRunAt: string }) { return (
- Prochain cycle dans {remaining} - ({absTime} heure locale) + Next cycle in {remaining} + ({absTime} local time)
) } function evNetLabel(evNet: number): { text: string; cls: string } { - if (evNet > 0.05) return { text: `EV nette +${(evNet * 100).toFixed(0)}%`, cls: 'text-emerald-400' } - if (evNet >= -0.01) return { text: 'EV nette ≈ 0', cls: 'text-yellow-400' } - return { text: `EV nette ${(evNet * 100).toFixed(0)}%`, cls: 'text-slate-600' } + if (evNet > 0.05) return { text: `Net EV +${(evNet * 100).toFixed(0)}%`, cls: 'text-emerald-400' } + if (evNet >= -0.01) return { text: 'Net EV ≈ 0', cls: 'text-yellow-400' } + return { text: `Net EV ${(evNet * 100).toFixed(0)}%`, cls: 'text-slate-600' } } function ProfileRow({ @@ -113,7 +113,7 @@ function ProfileRow({
{profile.name} - {!enabled && désactivé} + {!enabled && disabled} Score ≥ {profile.min_score} Gain ≥ {profile.min_gain_pct}% {fev} @@ -138,7 +138,7 @@ function ProfileRow({
- + setName(e.target.value)} className="w-full bg-dark-800 border border-slate-700 rounded px-2 py-1 text-sm text-white" />
@@ -158,7 +158,7 @@ function ProfileRow({ {/* Live EV preview */}
- À la frontière (score={score}, gain={gain}%) + At the frontier (score={score}, gain={gain}%) {evText} Trade Score = {trade_score.toFixed(1)}/100 @@ -179,14 +179,14 @@ function ProfileRow({
+ className="text-xs text-slate-500 hover:text-slate-300 px-2 py-1">Cancel
@@ -227,22 +227,22 @@ function RiskProfilesCard() {
-
Profils de risque
+
Risk Profiles
- Un trade est loggé dans le journal s'il passe au moins un profil activé - · Formule : EV nette = (score/100 × gain/100) − (1 − score/100) + A trade is logged in the journal if it passes at least one enabled profile + · Formula: Net EV = (score/100 × gain/100) − (1 − score/100)
{isLoading ? (
) : profiles.length === 0 ? ( -
Aucun profil — tous les trades seront ignorés
+
No profiles — all trades will be ignored
) : (
{profiles.map((p: any) => ( @@ -257,13 +257,13 @@ function RiskProfilesCard() { {showNew && (
- Nouveau profil + New profile
- - setNewName(e.target.value)} placeholder="ex: Ultra-risqué" + + setNewName(e.target.value)} placeholder="e.g. Ultra-aggressive" className="w-full bg-dark-800 border border-slate-700 rounded px-2 py-1 text-sm text-white placeholder:text-slate-700" />
@@ -282,7 +282,7 @@ function RiskProfilesCard() {
{evNetLabel(nEvNet).text} Trade Score = {nTradeScore.toFixed(1)}/100 - Score min pour EV=0 avec gain {newGain}% : {nMinScore}/100 + Min score for EV=0 with gain {newGain}% : {nMinScore}/100
@@ -295,10 +295,10 @@ function RiskProfilesCard() { ))}
- +
@@ -308,7 +308,7 @@ function RiskProfilesCard() { {/* Frontier visualization */} {profiles.length > 0 && (
-
Frontière d'acceptation — chaque point représente le minimum requis par profil
+
Acceptance frontier — each point represents the minimum required per profile
{profiles.filter((p: any) => p.enabled).map((p: any) => { const { text: ev, cls } = evNetLabel(p.ev_net_at_frontier ?? 0) @@ -412,15 +412,15 @@ export default function Config() { mutationFn: (cfg: any) => fetch(`${API}/api/var/scheduler/config`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(cfg), }).then(r => r.json()), - onSuccess: () => { refetchSched(); setSavedMsg('Schedulers sauvegardés'); setTimeout(() => setSavedMsg(''), 2000) }, + onSuccess: () => { refetchSched(); setSavedMsg('Schedulers saved'); setTimeout(() => setSavedMsg(''), 2000) }, }) const { mutate: triggerVarNow, isPending: triggeringVar } = useMutation({ mutationFn: () => fetch(`${API}/api/var/run-now`, { method: 'POST' }).then(r => r.json()), - onSuccess: () => { setSavedMsg('Snapshot VaR sauvegardé'); setTimeout(() => setSavedMsg(''), 2000) }, + onSuccess: () => { setSavedMsg('VaR snapshot saved'); setTimeout(() => setSavedMsg(''), 2000) }, }) const { mutate: triggerPnlNow, isPending: triggeringPnl } = useMutation({ mutationFn: () => fetch(`${API}/api/var/pnl/run-now`, { method: 'POST' }).then(r => r.json()), - onSuccess: () => { setSavedMsg('Snapshot PnL sauvegardé'); setTimeout(() => setSavedMsg(''), 2000) }, + onSuccess: () => { setSavedMsg('PnL snapshot saved'); setTimeout(() => setSavedMsg(''), 2000) }, }) // Auto-cycle local state @@ -486,7 +486,7 @@ export default function Config() { const saveSources = () => { updateSources(displaySources, { - onSuccess: () => { setSavedMsg('Sources sauvegardées'); setTimeout(() => setSavedMsg(''), 2000) } + onSuccess: () => { setSavedMsg('Sources saved'); setTimeout(() => setSavedMsg(''), 2000) } }) } @@ -498,7 +498,7 @@ export default function Config() { if (fredKey) keys.fred_api_key = fredKey updateApiKeys(keys, { onSuccess: () => { - setSavedMsg('Clés API sauvegardées') + setSavedMsg('API keys saved') setTimeout(() => setSavedMsg(''), 2000) setOpenaiKey(''); setNewsapiKey(''); setEiaKey(''); setFredKey('') } @@ -506,11 +506,11 @@ export default function Config() { } const CONFIG_TABS = [ - { key: 'ia', label: 'IA & Analyse', icon: Brain }, + { key: 'ia', label: 'AI & Analysis', icon: Brain }, { key: 'cycle', label: 'Auto-Cycle & Logging', icon: Zap }, { key: 'sources', label: 'Sources', icon: Globe }, - { key: 'options', label: 'Options — Paramètres', icon: TrendingUp }, - { key: 'profiles', label: 'Profils de risque', icon: SlidersHorizontal }, + { key: 'options', label: 'Options — Settings', icon: TrendingUp }, + { key: 'profiles', label: 'Risk Profiles', icon: SlidersHorizontal }, ] as const return ( @@ -520,7 +520,7 @@ export default function Config() {

Configuration

-

Clés API, sources, paramètres IA & cycle

+

API keys, sources, AI & cycle settings

{savedMsg && (
@@ -549,30 +549,30 @@ export default function Config() {
{/* AI Status */}
-
Statut IA
+
AI Status
{aiStatus?.enabled ? ( <> -
OpenAI Connecté
+
OpenAI Connected
GPT-4o + GPT-4o-mini
) : ( <> -
OpenAI Non configuré
-
Entrer la clé ci-dessous
+
OpenAI Not configured
+
Enter key below
)}
-
• Analyse de discours (Trump, Powell...)
-
• Classification IA des actualités
-
• Évaluation de patterns
-
• Top 10 idées contextualisées
+
• Speech analysis (Trump, Powell...)
+
• AI news classification
+
• Pattern evaluation
+
• Top 10 contextualized ideas
{/* OpenAI API Key */}
-
Clé OpenAI
+
OpenAI Key
@@ -582,7 +582,7 @@ export default function Config() {
- {aiStatus?.enabled ? '✓ Actif' : '○ Inactif'} + {aiStatus?.enabled ? '✓ Active' : '○ Inactive'}
- {savingKeys ? 'Sauvegarde...' : 'Sauvegarder la clé'} + {savingKeys ? 'Saving...' : 'Save key'}
@@ -608,11 +608,11 @@ export default function Config() { {/* Right: Analysis params */}

- Paramètres d'analyse IA + AI Analysis Settings

- +
{[5, 10, 15, 20].map(n => (
- +
- +
EntréeSortiePrix entréeEntryExitEntry price StrikePrimePrix sortiePremiumExit price P&L Capital