feat: Specialist Desks v2 — COT, Forward Curves, Surprise Index, Hawk/Dove scorer
- COT Positioning: CFTC disaggregated + financial futures (19 markets) via Socrata free API net MM position % OI + weekly change stored in cot_data table - Forward Curves: yfinance front-month vs +3M slope (8 commodities) contango/backwardation/flat stored in forward_curve_data table - Surprise Index: consensus_estimate + actual_value on specialist_reports auto-computes surprise_score = actual - consensus on save - Hawk/Dove Text Scorer: GPT-4o-mini endpoint for CB statements score -1..+1, label, summary, key_phrases (forex/bonds: hawk/dove; commodities: bull/bear) - AI context injection: COT net positioning, forward curve structure, surprise scores, upcoming consensus estimates injected into all desk blocks - Frontend: COT panel (net% bars), Forward Curves panel, SurpriseInput on report cards, Hawk/Dove scorer in forex/bonds config tab - auto_cycle.py: non-blocking COT + curve refresh before each cycle Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -109,10 +109,11 @@ li { margin-bottom: 4px; font-size: 9.5pt; }
|
||||
<span class="badge badge-p3">Phase 3 · Portfolio & Risk</span>
|
||||
<span class="badge badge-p4">Phase 4 · IA Probabiliste</span>
|
||||
<span class="badge" style="background:#d1fae5;color:#065f46;">Phase 5 · Cycle Context & Intelligence</span>
|
||||
<span class="badge" style="background:#fce7f3;color:#9d174d;">Phase 6 · Specialist Desks & Calibration</span>
|
||||
</div>
|
||||
<div class="meta">
|
||||
Version 5.0 · Juin 2026 · 5 Phases · FastAPI + React + SQLite + GPT-4o<br>
|
||||
19 pages · 50 signaux macro · IV Gate · Pattern Convergence · IBKR Ticket
|
||||
Version 6.0 · Juin 2026 · 6 Phases · FastAPI + React + SQLite + GPT-4o<br>
|
||||
23 pages · 50 signaux macro · IV Gate · Pattern Convergence · IBKR Ticket
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -180,6 +181,26 @@ li { margin-bottom: 4px; font-size: 9.5pt; }
|
||||
<li><a href="#s9">9. Guide Trader — Lire le Cockpit Page par Page</a></li>
|
||||
<li><a href="#s10">10. Décisions de Conception</a></li>
|
||||
<li><a href="#s11">11. Glossaire</a></li>
|
||||
<li><a href="#s2c">2c. Workflow Complet du Cycle — Graphe des Calculs</a></li>
|
||||
<ul class="toc-list toc-indent">
|
||||
<li><a href="#s2c-diagram">2c.1 Graphe Visuel du Pipeline</a></li>
|
||||
<li><a href="#s2c-formulas">2c.2 Démonstration des Calculs Étape par Étape</a></li>
|
||||
</ul>
|
||||
<li><a href="#s12">12. Specialist Desks — Configuration par Asset Class</a></li>
|
||||
<ul class="toc-list toc-indent">
|
||||
<li><a href="#s12-1">12.1 Architecture et tables DB</a></li>
|
||||
<li><a href="#s12-2">12.2 Injection dans les prompts IA</a></li>
|
||||
</ul>
|
||||
<li><a href="#s13">13. Bibliothèque Patterns — Find Similar & Merge</a></li>
|
||||
<ul class="toc-list toc-indent">
|
||||
<li><a href="#s13-1">13.1 Analyse de similarité GPT-4o-mini</a></li>
|
||||
<li><a href="#s13-2">13.2 Fusion et remapping des données</a></li>
|
||||
</ul>
|
||||
<li><a href="#s14">14. Calibration Progressive — IA vs Observé</a></li>
|
||||
<ul class="toc-list toc-indent">
|
||||
<li><a href="#s14-1">14.1 Formule de crédibilité et blending</a></li>
|
||||
<li><a href="#s14-2">14.2 Affichage sur les cards et rapport de cycle</a></li>
|
||||
</ul>
|
||||
</ul>
|
||||
|
||||
<div class="page-break"></div>
|
||||
@@ -188,7 +209,7 @@ li { margin-bottom: 4px; font-size: 9.5pt; }
|
||||
<h2 id="s1">1. Vue d'Ensemble du Système</h2>
|
||||
|
||||
<h3 id="s1-1">1.1 Ce que fait le système</h3>
|
||||
<p>GeoOptions Intelligence est un cockpit de trading d'options à usage personnel, conçu pour un trader individuel opérant sur Interactive Brokers avec des positions de l'ordre de 1 000 € par trade et un horizon moyen de 3 mois. Le système fusionne en temps quasi-réel trois flux d'information hétérogènes — données de marché financières (prix, volatilité implicite, open interest), signaux géopolitiques (flux RSS de presse internationale, scores de tension par région) et analyse macroéconomique (régime macro dominant parmi 4 états : Expansion, Stagflation, Récession, Stress Géopolitique) — pour générer des suggestions de patterns de trading options avec probabilités de succès, tailles de position ajustées au risque, et contre-thèses d'invalidation. L'ensemble tourne sur un VPS Linux sous FastAPI (backend) et React (frontend), avec SQLite comme base de données persistante et GPT-4o comme moteur de raisonnement.</p>
|
||||
<p>GeoOptions Intelligence est un cockpit de trading d'options à usage personnel, conçu pour un trader individuel opérant sur Interactive Brokers avec des positions de l'ordre de 1 000 € par trade et un horizon moyen de 3 mois. Le système fusionne en temps quasi-réel trois flux d'information hétérogènes — données de marché financières (prix, volatilité implicite, open interest), signaux géopolitiques (flux RSS de presse internationale, scores de tension par région) et analyse macroéconomique (régime macro dominant parmi 8 états : Goldilocks, Désinflation, Soft Landing, Reflation, Stagflation, Choc Inflationniste, Récession, Crise de Liquidité) — pour générer des suggestions de patterns de trading options avec probabilités de succès, tailles de position ajustées au risque, et contre-thèses d'invalidation. L'ensemble tourne sur un VPS Linux sous FastAPI (backend) et React (frontend), avec SQLite comme base de données persistante et GPT-4o comme moteur de raisonnement.</p>
|
||||
|
||||
<h3 id="s1-2">1.2 Les 3 Principes Fondamentaux</h3>
|
||||
|
||||
@@ -209,31 +230,153 @@ li { margin-bottom: 4px; font-size: 9.5pt; }
|
||||
|
||||
<h3 id="s1-3">1.3 Architecture Technique</h3>
|
||||
|
||||
<div class="arch-box">
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ FRONTEND (React + Vite) │
|
||||
│ Dashboard · Radar Géo · Marchés · Régime · Options Lab │
|
||||
│ Patterns · Portefeuille · Journal · Rapport IA · Super Ctx │
|
||||
│ Analytics · Analytics Avancées · Risk Dashboard · Config │
|
||||
└──────────────────────────┬──────────────────────────────────────┘
|
||||
│ HTTP / REST API
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ BACKEND (FastAPI / Python) │
|
||||
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────────────┐ │
|
||||
│ │ Scheduler │ │ AI Engine │ │ Risk Engine │ │
|
||||
│ │ APScheduler │ │ GPT-4o API │ │ Kelly · HHI · Cluster│ │
|
||||
│ │ 6h cycle │ │ 6 prompts │ │ Bayesian · K-Means │ │
|
||||
│ └──────┬───────┘ └──────┬───────┘ └──────────┬───────────┘ │
|
||||
│ │ │ │ │
|
||||
│ ┌──────▼─────────────────▼───────────────────────▼───────────┐ │
|
||||
│ │ SQLite Database (15+ tables) │ │
|
||||
│ └────────────────────────────────────────────────────────────┘ │
|
||||
└──────────────────────────────────────────────────────────────────┘
|
||||
│ │ │
|
||||
▼ ▼ ▼
|
||||
yfinance API OpenAI API RSS Feeds
|
||||
(prix, IV, OI) (GPT-4o) (Reuters, BBC, Al Jazeera)
|
||||
<style>
|
||||
.arch-diag { margin: 18px 0; font-family: 'Arial', sans-serif; font-size: 9pt; }
|
||||
.arch-layer { border-radius: 10px; padding: 14px 18px; margin-bottom: 0; }
|
||||
.arch-layer-fe { background: linear-gradient(135deg, #1e3a8a, #1d4ed8); color: white; border-radius: 10px 10px 0 0; }
|
||||
.arch-layer-be { background: linear-gradient(135deg, #1e1b4b, #3730a3); color: white; }
|
||||
.arch-layer-db { background: linear-gradient(135deg, #0c4a6e, #0284c7); color: white; border-radius: 0 0 10px 10px; }
|
||||
.arch-layer-title { font-size: 8pt; font-weight: 700; letter-spacing: 1.5px; text-transform: uppercase; opacity: 0.75; margin-bottom: 10px; }
|
||||
.arch-pages { display: flex; flex-wrap: wrap; gap: 5px; }
|
||||
.arch-page-chip { background: rgba(255,255,255,0.15); border: 1px solid rgba(255,255,255,0.25); border-radius: 20px; padding: 3px 10px; font-size: 7.5pt; white-space: nowrap; }
|
||||
.arch-page-chip.freq { background: rgba(255,255,255,0.28); border-color: rgba(255,255,255,0.5); font-weight: 700; }
|
||||
.arch-modules { display: grid; grid-template-columns: repeat(5, 1fr); gap: 8px; margin-top: 10px; }
|
||||
.arch-module { background: rgba(255,255,255,0.12); border: 1px solid rgba(255,255,255,0.2); border-radius: 8px; padding: 10px 8px; text-align: center; font-size: 8pt; }
|
||||
.arch-module-icon { font-size: 16pt; display: block; margin-bottom: 4px; }
|
||||
.arch-module-name { font-weight: 700; font-size: 8.5pt; }
|
||||
.arch-module-detail { font-size: 7pt; opacity: 0.7; margin-top: 2px; }
|
||||
.arch-http { text-align: center; padding: 8px; font-size: 8pt; font-weight: 700; color: #64748b; letter-spacing: 1px; border-left: 2px solid #e2e8f0; border-right: 2px solid #e2e8f0; display: flex; align-items: center; justify-content: center; gap: 8px; background: #f8fafc; }
|
||||
.arch-http::before, .arch-http::after { content: ''; flex: 1; height: 1px; background: #cbd5e1; }
|
||||
.arch-db-tables { display: flex; flex-wrap: wrap; gap: 5px; margin-top: 8px; }
|
||||
.arch-db-chip { background: rgba(255,255,255,0.15); border: 1px solid rgba(255,255,255,0.2); border-radius: 4px; padding: 2px 8px; font-size: 7pt; font-family: 'Courier New', monospace; }
|
||||
.arch-ext-row { display: grid; grid-template-columns: repeat(5, 1fr); gap: 8px; margin-top: 12px; }
|
||||
.arch-ext-card { background: #f8fafc; border: 1.5px solid #e2e8f0; border-radius: 8px; padding: 10px 8px; text-align: center; font-size: 8pt; }
|
||||
.arch-ext-icon { font-size: 18pt; display: block; margin-bottom: 4px; }
|
||||
.arch-ext-name { font-weight: 700; color: #1e3a8a; font-size: 8.5pt; }
|
||||
.arch-ext-detail { font-size: 7pt; color: #64748b; margin-top: 2px; }
|
||||
.arch-arrows-ext { display: flex; justify-content: space-around; padding: 4px 0; color: #94a3b8; font-size: 12pt; }
|
||||
</style>
|
||||
|
||||
<div class="arch-diag">
|
||||
|
||||
<!-- LAYER 1 — FRONTEND -->
|
||||
<div class="arch-layer arch-layer-fe">
|
||||
<div class="arch-layer-title">⬡ FRONTEND — React 18 · Vite · Tailwind CSS · Recharts</div>
|
||||
<div class="arch-pages">
|
||||
<span class="arch-page-chip freq">Dashboard</span>
|
||||
<span class="arch-page-chip freq">Radar Géo</span>
|
||||
<span class="arch-page-chip freq">Journal</span>
|
||||
<span class="arch-page-chip">Markets</span>
|
||||
<span class="arch-page-chip">Macro Regime</span>
|
||||
<span class="arch-page-chip">Options Lab</span>
|
||||
<span class="arch-page-chip">Patterns</span>
|
||||
<span class="arch-page-chip">Portfolio</span>
|
||||
<span class="arch-page-chip">Cycle Report</span>
|
||||
<span class="arch-page-chip">Super Context</span>
|
||||
<span class="arch-page-chip">Analytics</span>
|
||||
<span class="arch-page-chip">Adv. Analytics</span>
|
||||
<span class="arch-page-chip">Risk Dashboard</span>
|
||||
<span class="arch-page-chip">VaR Analysis</span>
|
||||
<span class="arch-page-chip">Position History</span>
|
||||
<span class="arch-page-chip">Backtest</span>
|
||||
<span class="arch-page-chip">Calendar</span>
|
||||
<span class="arch-page-chip">Specialist Desks</span>
|
||||
<span class="arch-page-chip">System Logs</span>
|
||||
<span class="arch-page-chip">Configuration</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- HTTP connector -->
|
||||
<div class="arch-http">HTTP / REST API</div>
|
||||
|
||||
<!-- LAYER 2 — BACKEND -->
|
||||
<div class="arch-layer arch-layer-be">
|
||||
<div class="arch-layer-title">⚙ BACKEND — FastAPI · Python 3.11 · APScheduler</div>
|
||||
<div class="arch-modules">
|
||||
<div class="arch-module">
|
||||
<span class="arch-module-icon">🕐</span>
|
||||
<div class="arch-module-name">Scheduler</div>
|
||||
<div class="arch-module-detail">APScheduler · cycle 6h · elapsed-time aware</div>
|
||||
</div>
|
||||
<div class="arch-module">
|
||||
<span class="arch-module-icon">🤖</span>
|
||||
<div class="arch-module-name">AI Engine</div>
|
||||
<div class="arch-module-detail">GPT-4o · 6 prompts · GPT-4o-mini résumés</div>
|
||||
</div>
|
||||
<div class="arch-module">
|
||||
<span class="arch-module-icon">⚖️</span>
|
||||
<div class="arch-module-name">Risk Engine</div>
|
||||
<div class="arch-module-detail">Kelly · HHI · K-Means · Bayésien · VaR</div>
|
||||
</div>
|
||||
<div class="arch-module">
|
||||
<span class="arch-module-icon">🔬</span>
|
||||
<div class="arch-module-name">Cycle Context</div>
|
||||
<div class="arch-module-detail">Decay · IVR · 50 macro · Tech · Price Discovery</div>
|
||||
</div>
|
||||
<div class="arch-module">
|
||||
<span class="arch-module-icon">🏦</span>
|
||||
<div class="arch-module-name">Specialist Desks</div>
|
||||
<div class="arch-module-detail">7 desks · rapports · injection IA duale</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- DB connector -->
|
||||
<div class="arch-http">SQLite / aiosqlite</div>
|
||||
|
||||
<!-- LAYER 3 — DATABASE -->
|
||||
<div class="arch-layer arch-layer-db">
|
||||
<div class="arch-layer-title">🗄 BASE DE DONNÉES — SQLite 18+ Tables</div>
|
||||
<div class="arch-db-tables">
|
||||
<span class="arch-db-chip">custom_patterns</span>
|
||||
<span class="arch-db-chip">trade_entry_prices</span>
|
||||
<span class="arch-db-chip">pattern_score_history</span>
|
||||
<span class="arch-db-chip">geo_news</span>
|
||||
<span class="arch-db-chip">macro_regime_history</span>
|
||||
<span class="arch-db-chip">system_logs</span>
|
||||
<span class="arch-db-chip">knowledge_base</span>
|
||||
<span class="arch-db-chip">iv_history</span>
|
||||
<span class="arch-db-chip">portfolio_snapshots</span>
|
||||
<span class="arch-db-chip">skipped_trades</span>
|
||||
<span class="arch-db-chip">pattern_embeddings</span>
|
||||
<span class="arch-db-chip">ai_reasoning_traces</span>
|
||||
<span class="arch-db-chip">specialist_reports</span>
|
||||
<span class="arch-db-chip">asset_class_configs</span>
|
||||
<span class="arch-db-chip">ai_call_logs</span>
|
||||
<span class="arch-db-chip">fred_data</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- External APIs -->
|
||||
<div class="arch-arrows-ext">↑ ↑ ↑ ↑ ↑</div>
|
||||
<div class="arch-ext-row">
|
||||
<div class="arch-ext-card">
|
||||
<span class="arch-ext-icon">📈</span>
|
||||
<div class="arch-ext-name">yfinance</div>
|
||||
<div class="arch-ext-detail">Prix spot · IV · OI · 50 macro signaux · bootstrap 45j</div>
|
||||
</div>
|
||||
<div class="arch-ext-card">
|
||||
<span class="arch-ext-icon">🧠</span>
|
||||
<div class="arch-ext-name">OpenAI API</div>
|
||||
<div class="arch-ext-detail">GPT-4o (scoring/suggest) · GPT-4o-mini (news/résumés/similarité)</div>
|
||||
</div>
|
||||
<div class="arch-ext-card">
|
||||
<span class="arch-ext-icon">📰</span>
|
||||
<div class="arch-ext-name">RSS Feeds</div>
|
||||
<div class="arch-ext-detail">Reuters · AP · Al Jazeera · FT · Bloomberg · 7 flux</div>
|
||||
</div>
|
||||
<div class="arch-ext-card">
|
||||
<span class="arch-ext-icon">🏛️</span>
|
||||
<div class="arch-ext-name">IBKR</div>
|
||||
<div class="arch-ext-detail">Portefeuille réel · Prix options live · Ticket auto-calculé</div>
|
||||
</div>
|
||||
<div class="arch-ext-card">
|
||||
<span class="arch-ext-icon">🏦</span>
|
||||
<div class="arch-ext-name">FRED API</div>
|
||||
<div class="arch-ext-detail">CPI · NFP · Taux Fed · PCE · Chômage</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="callout blue">
|
||||
@@ -431,6 +574,21 @@ final_relevance = clamp(base_relevance + ai_alignment, 0, 100)
|
||||
<p>Ces données sont encodées dans le méga-prompt de l'Étape 5.</p>
|
||||
</div>
|
||||
|
||||
<div class="step-block">
|
||||
<div class="step-num">Étape 4b — Chargement des Specialist Desks</div>
|
||||
<p><strong>Input :</strong> Table <code>asset_class_configs</code> (7 desks) + <code>specialist_reports</code> + <code>report_desk_links</code></p>
|
||||
<p><strong>Traitement :</strong> Pour chaque desk actif, le système charge les fondamentaux, la sensibilité macro par régime, les seuils de prix significatifs/extrêmes, et les 5 prochains rapports planifiés triés par date et importance.</p>
|
||||
<p><strong>Output :</strong> Un bloc de contexte structuré par desk, injecté dans <strong>deux prompts distincts</strong> :</p>
|
||||
<ul>
|
||||
<li><strong>Scoring (Étape 5) :</strong> seuls les desks couverts par les patterns scorés ce cycle sont injectés — contexte ciblé pour réduire les tokens</li>
|
||||
<li><strong>Suggestion (Étape 5) :</strong> les 7 desks sont systématiquement injectés — permet à l'IA de suggérer des patterns sur des desks non encore couverts</li>
|
||||
</ul>
|
||||
<div class="callout amber">
|
||||
<div class="label">Pourquoi deux chemins distincts ?</div>
|
||||
Lors du scoring, l'IA connaît déjà l'asset class des patterns. L'injection ciblée suffit et coûte moins cher. Lors de la suggestion, l'IA doit explorer librement tous les marchés — injecter les 7 desks garantit qu'aucune opportunité n'est manquée par méconnaissance du contexte desk.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="step-block">
|
||||
<div class="step-num">Étape 4 — Construction du Super Contexte</div>
|
||||
<p><strong>Input :</strong> Base de données Knowledge Base (patterns historiques), trades des 90 derniers jours, insights récents avec scores de fiabilité</p>
|
||||
@@ -1757,6 +1915,37 @@ Seuil : confidence > 0.75 → ALERTE INVALIDATION générée dans l'UI
|
||||
<p><strong>Onglet Options — Parameters :</strong> IV Gate thresholds (Alert / Extreme), exit defaults (target % / stop-loss %), indicateurs techniques à activer par horizon, skew threshold pour alertes directionnelles.</p>
|
||||
<p>Ne jamais modifier le modèle GPT en production sans tester sur un cycle complet. Ne jamais baisser le Kelly fraction en dessous de 1/4 (tailles trop petites pour couvrir les frais IBKR).</p>
|
||||
|
||||
<h4>Pattern Lab (Options Lab) — Flux de Travail Détaillé</h4>
|
||||
<p>La page <strong>Patterns</strong> est le cœur opérationnel de GeoOptions. Voici le flux de travail typique d'un trader après chaque cycle :</p>
|
||||
<div class="flow-mini">
|
||||
<div class="flow-step">1. Lire le<br>Cycle Report</div>
|
||||
<div class="flow-arrow">→</div>
|
||||
<div class="flow-step">2. Ouvrir<br>Patterns Explorer</div>
|
||||
<div class="flow-arrow">→</div>
|
||||
<div class="flow-step">3. Filtrer par<br>classe d'actif</div>
|
||||
<div class="flow-arrow">→</div>
|
||||
<div class="flow-step">4. Trier par<br>Score IA ↓</div>
|
||||
<div class="flow-arrow">→</div>
|
||||
<div class="flow-step">5. Vérifier<br>MaturityBadge</div>
|
||||
<div class="flow-arrow">→</div>
|
||||
<div class="flow-step">6. Find Similar<br>(si nouveau)</div>
|
||||
<div class="flow-arrow">→</div>
|
||||
<div class="flow-step">7. Log ou<br>Skip manuellement</div>
|
||||
</div>
|
||||
<table>
|
||||
<thead><tr><th>Fonctionnalité</th><th>Ce qu'elle fait</th><th>Quand l'utiliser</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td><strong>Filtres class d'actif</strong></td><td>Filtrage par forex/metals/agri/energy/indices/crypto/bonds</td><td>Tous les jours, pour ne voir que les patterns pertinents au marché actif</td></tr>
|
||||
<tr><td><strong>Tri Score IA / Date / Probabilité</strong></td><td>Reclasse les patterns par score du dernier cycle (desc), date de création, ou bayesian_win_rate</td><td>Après cycle — trier par Score IA pour voir quels patterns le système juge les plus opportuns aujourd'hui</td></tr>
|
||||
<tr><td><strong>MaturityBadge</strong></td><td>Affiche pour chaque pattern : source (Pure AI → Data-driven), poids calibration, WR bayésien, AI estimate vs calibré</td><td>Avant de décider de logger un trade — savoir si le G dans l'EV vient de l'IA ou de données réelles</td></tr>
|
||||
<tr><td><strong>Find Similar (ScanSearch)</strong></td><td>Appelle GPT-4o-mini pour comparer le pattern à tous les autres. Retourne : doublon à fusionner / contre-scénario / pattern unique</td><td>Après chaque cycle qui ajoute des nouveaux patterns — évite l'accumulation de quasi-doublons</td></tr>
|
||||
<tr><td><strong>Merge (Fusionner)</strong></td><td>Fusion transactionnelle : union historical_instances, somme backtest hits, remapping de 6 tables enfants</td><td>Quand Find Similar retourne "merge_as_instance" — ne jamais fusionner des directions opposées</td></tr>
|
||||
<tr><td><strong>Éditeur complet</strong></td><td>Modifier name, description, triggers, expected_move_pct, trades suggérés, historical_instances</td><td>Pour corriger un pattern généré par l'IA qui contient des erreurs de libellé ou de paramètres</td></tr>
|
||||
<tr><td><strong>Suggestion IA manuelle</strong></td><td>Déclenche le prompt suggest_patterns à la demande (hors cycle)</td><td>Quand le marché évolue significativement en intra-journée et qu'on veut de nouvelles idées immédiatement</td></tr>
|
||||
<tr><td><strong>Vue grille / liste</strong></td><td>Grille : aperçu rapide de nombreux patterns. Liste : détail complet avec toutes les métriques visibles</td><td>Grille pour le matin (scan rapide) · Liste pour la décision finale d'un trade</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="page-break"></div>
|
||||
|
||||
<!-- SECTION 10 -->
|
||||
@@ -1854,15 +2043,625 @@ Seuil : confidence > 0.75 → ALERTE INVALIDATION générée dans l'UI
|
||||
<tr><td><strong>Moniteur de Portefeuille IA</strong></td><td>Agent GPT-4o-mini (Step 7b du cycle) qui analyse l'état du portefeuille simulé après chaque log de trades. Produit un assessment global + liste d'actions (close / rebalance / monitor) + suggestion de rééquilibrage. Non-bloquant et persisté dans system_logs pour affichage dans le Journal.</td></tr>
|
||||
<tr><td><strong>Concentration de Classe</strong></td><td>Pourcentage d'une classe d'actif (energy, metals, agriculture, equities, indices, forex, rates) dans le portefeuille simulé total. Seuil d'alerte : > 35%. Affiché sous forme de barres horizontales colorées dans l'onglet Risque Sim.</td></tr>
|
||||
<tr><td><strong>Price Warning</strong></td><td>Badge ambre sur un trade dans l'onglet Ouverts : <code>NO DATA</code> (ni prix d'entrée ni prix actuel disponible), <code>NO ENTRY</code> (prix d'entrée absent), <code>NO LIVE</code> (prix actuel non récupéré). Signale que le monitoring MtM est impossible pour ce trade.</td></tr>
|
||||
<tr><td><strong>Specialist Desk</strong></td><td>Configuration par asset class dans GeoOptions : fondamentaux clés, sensibilité macro par régime, seuils de prix significatifs/extrêmes, et catalogue de rapports critiques (y compris non-calendrier). Injecté dans les deux prompts IA (scoring et suggestion) pour ancrer les recommandations dans la culture réelle de chaque desk.</td></tr>
|
||||
<tr><td><strong>Crédibilité Actuarielle</strong></td><td>Méthode de pondération w = n/(n+k) (k=5 dans GeoOptions) qui mesure la confiance accordée aux données observées vs l'estimation a priori. À n=5 trades : 50% observé. Inspiré de Bühlmann-Straub (actuariat). Utilisée pour blender l'estimated_move IA avec le rendement réellement observé.</td></tr>
|
||||
<tr><td><strong>Calibrated Expected Move</strong></td><td>Valeur blendée de l'expected_move_pct : (1−w)×ai_estimate + w×observed_avg_win_pct. Utilisée comme G dans l'EV dès que la crédibilité w > 10%. Permet de corriger progressivement le biais d'optimisme de l'IA avec les données réelles.</td></tr>
|
||||
<tr><td><strong>Find Similar</strong></td><td>Endpoint GPT-4o-mini qui compare un pattern de la bibliothèque contre tous les autres et retourne : merge_as_instance (doublon → fusionner), counter_scenario (direction opposée, même régime → conserver les deux), new_pattern (pas de match structurel).</td></tr>
|
||||
<tr><td><strong>Pattern Merge</strong></td><td>Fusion transactionnelle de deux patterns : union des historical_instances (dédup), somme des compteurs backtest, remapping de toutes les tables FK (scores, trades, traces IA, logs), suppression du pattern discardé. Irréversible — confirmation requise.</td></tr>
|
||||
<tr><td><strong>Barra de Maturité</strong></td><td>Badge visuel sur chaque card PatternExplorer montrant le degré de calibration : label source (Pure AI → Data-driven), barre dégradée indiquant le poids observé %, et comparaison chiffrée IA estimate vs Calibré.</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="page-break"></div>
|
||||
|
||||
<!-- SECTION 2c — WORKFLOW DIAGRAM REDESIGNED -->
|
||||
<h2 id="s2c">2c. Workflow Complet du Cycle — Pipeline de Calcul</h2>
|
||||
<p>Chaque étape du cycle automatique est documentée ici avec ses formules, données entrantes, et sorties — aucun calcul masqué, aucune boîte noire.</p>
|
||||
|
||||
<h3 id="s2c-diagram">2c.1 Vue Synthétique du Pipeline (8 Phases)</h3>
|
||||
|
||||
<style>
|
||||
/* ===== PIPELINE VISUAL ===== */
|
||||
.wf-pipeline { margin: 24px 0; font-family: 'Arial', sans-serif; }
|
||||
.wf-phase { display: flex; gap: 0; margin-bottom: 3px; align-items: stretch; }
|
||||
.wf-num { width: 36px; flex-shrink: 0; display: flex; align-items: center; justify-content: center; font-size: 11pt; font-weight: 900; color: white; border-radius: 8px 0 0 8px; }
|
||||
.wf-box { flex: 1; border-radius: 0 8px 8px 0; padding: 12px 16px; }
|
||||
.wf-box-title { font-size: 9pt; font-weight: 700; text-transform: uppercase; letter-spacing: 0.8px; margin-bottom: 6px; }
|
||||
.wf-box-sub { font-size: 8.5pt; line-height: 1.5; }
|
||||
.wf-chips { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 6px; }
|
||||
.wf-chip { border-radius: 20px; padding: 2px 10px; font-size: 7.5pt; font-weight: 600; border: 1px solid; white-space: nowrap; }
|
||||
|
||||
/* Phase colors */
|
||||
.wf-c1 .wf-num { background: #1d4ed8; }
|
||||
.wf-c1 .wf-box { background: linear-gradient(135deg, #dbeafe, #eff6ff); border: 1.5px solid #93c5fd; }
|
||||
.wf-c1 .wf-box-title { color: #1d4ed8; }
|
||||
.wf-c1 .wf-chip { background: #dbeafe; border-color: #93c5fd; color: #1e3a8a; }
|
||||
|
||||
.wf-c2 .wf-num { background: #7c3aed; }
|
||||
.wf-c2 .wf-box { background: linear-gradient(135deg, #ede9fe, #f5f3ff); border: 1.5px solid #a78bfa; }
|
||||
.wf-c2 .wf-box-title { color: #7c3aed; }
|
||||
.wf-c2 .wf-chip { background: #ede9fe; border-color: #a78bfa; color: #4c1d95; }
|
||||
|
||||
.wf-c3 .wf-num { background: #0891b2; }
|
||||
.wf-c3 .wf-box { background: linear-gradient(135deg, #cffafe, #ecfeff); border: 1.5px solid #67e8f9; }
|
||||
.wf-c3 .wf-box-title { color: #0891b2; }
|
||||
.wf-c3 .wf-chip { background: #cffafe; border-color: #67e8f9; color: #164e63; }
|
||||
|
||||
.wf-c4 .wf-num { background: #d97706; }
|
||||
.wf-c4 .wf-box { background: linear-gradient(135deg, #fef3c7, #fffbeb); border: 1.5px solid #fcd34d; }
|
||||
.wf-c4 .wf-box-title { color: #b45309; }
|
||||
.wf-c4 .wf-chip { background: #fef3c7; border-color: #fcd34d; color: #78350f; }
|
||||
|
||||
.wf-c5 .wf-num { background: #dc2626; }
|
||||
.wf-c5 .wf-box { background: linear-gradient(135deg, #fee2e2, #fff5f5); border: 1.5px solid #fca5a5; }
|
||||
.wf-c5 .wf-box-title { color: #dc2626; }
|
||||
.wf-c5 .wf-chip { background: #fee2e2; border-color: #fca5a5; color: #7f1d1d; }
|
||||
|
||||
.wf-c6 .wf-num { background: #16a34a; }
|
||||
.wf-c6 .wf-box { background: linear-gradient(135deg, #dcfce7, #f0fdf4); border: 1.5px solid #86efac; }
|
||||
.wf-c6 .wf-box-title { color: #15803d; }
|
||||
.wf-c6 .wf-chip { background: #dcfce7; border-color: #86efac; color: #14532d; }
|
||||
|
||||
.wf-c7 .wf-num { background: #0f766e; }
|
||||
.wf-c7 .wf-box { background: linear-gradient(135deg, #ccfbf1, #f0fdfa); border: 1.5px solid #5eead4; }
|
||||
.wf-c7 .wf-box-title { color: #0f766e; }
|
||||
.wf-c7 .wf-chip { background: #ccfbf1; border-color: #5eead4; color: #134e4a; }
|
||||
|
||||
.wf-arrow { text-align: center; color: #94a3b8; font-size: 13pt; height: 18px; display: flex; align-items: center; justify-content: center; margin: 2px 0; }
|
||||
|
||||
/* ===== FORMULA CARDS ===== */
|
||||
.wf-formulas { margin: 28px 0; }
|
||||
.wf-fcard { border: 2px solid; border-radius: 12px; overflow: hidden; margin: 14px 0; break-inside: avoid; }
|
||||
.wf-fcard-head { padding: 10px 18px; display: flex; align-items: center; gap: 10px; }
|
||||
.wf-fcard-head .icon { font-size: 20pt; }
|
||||
.wf-fcard-head .title { font-size: 10.5pt; font-weight: 700; }
|
||||
.wf-fcard-head .subtitle { font-size: 8pt; opacity: 0.75; margin-top: 2px; }
|
||||
.wf-fcard-body { padding: 14px 18px; background: white; }
|
||||
.wf-formula { font-family: 'Courier New', monospace; font-size: 9pt; background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 6px; padding: 10px 14px; margin: 8px 0; line-height: 1.7; }
|
||||
.wf-formula .highlight { background: #fef3c7; padding: 0 3px; border-radius: 3px; font-weight: 700; }
|
||||
.wf-formula .result { color: #15803d; font-weight: 700; }
|
||||
.wf-example-table { width: 100%; font-size: 8.5pt; margin: 8px 0; border-collapse: collapse; }
|
||||
.wf-example-table td { padding: 5px 10px; border-bottom: 1px solid #f1f5f9; vertical-align: middle; }
|
||||
.wf-example-table tr:last-child td { border-bottom: none; }
|
||||
.wf-example-table .param { font-family: 'Courier New', monospace; color: #1e3a8a; font-size: 8pt; }
|
||||
.wf-example-table .val { font-weight: 700; }
|
||||
.wf-example-table .val.g { color: #15803d; }
|
||||
.wf-example-table .val.r { color: #dc2626; }
|
||||
.wf-example-table .val.a { color: #b45309; }
|
||||
.wf-note { font-size: 8pt; color: #64748b; font-style: italic; margin-top: 6px; }
|
||||
.wf-separator { border: none; border-top: 2px dashed #e2e8f0; margin: 14px 0; }
|
||||
|
||||
/* color themes for formula cards */
|
||||
.wf-fcard.blue { border-color: #93c5fd; }
|
||||
.wf-fcard.blue .wf-fcard-head { background: linear-gradient(135deg, #1d4ed8, #2563eb); color: white; }
|
||||
.wf-fcard.purple { border-color: #a78bfa; }
|
||||
.wf-fcard.purple .wf-fcard-head { background: linear-gradient(135deg, #7c3aed, #8b5cf6); color: white; }
|
||||
.wf-fcard.amber { border-color: #fcd34d; }
|
||||
.wf-fcard.amber .wf-fcard-head { background: linear-gradient(135deg, #d97706, #f59e0b); color: white; }
|
||||
.wf-fcard.red { border-color: #fca5a5; }
|
||||
.wf-fcard.red .wf-fcard-head { background: linear-gradient(135deg, #dc2626, #ef4444); color: white; }
|
||||
.wf-fcard.green { border-color: #86efac; }
|
||||
.wf-fcard.green .wf-fcard-head { background: linear-gradient(135deg, #16a34a, #22c55e); color: white; }
|
||||
.wf-fcard.teal { border-color: #5eead4; }
|
||||
.wf-fcard.teal .wf-fcard-head { background: linear-gradient(135deg, #0f766e, #0d9488); color: white; }
|
||||
.wf-fcard.cyan { border-color: #67e8f9; }
|
||||
.wf-fcard.cyan .wf-fcard-head { background: linear-gradient(135deg, #0891b2, #06b6d4); color: white; }
|
||||
</style>
|
||||
|
||||
<div class="wf-pipeline">
|
||||
|
||||
<div class="wf-phase wf-c1">
|
||||
<div class="wf-num">①</div>
|
||||
<div class="wf-box">
|
||||
<div class="wf-box-title">Collecte Parallèle des Données</div>
|
||||
<div class="wf-box-sub">Fetch simultané sur 6 sources. ThreadPoolExecutor 20 workers pour les 50 tickers macro.</div>
|
||||
<div class="wf-chips">
|
||||
<span class="wf-chip">📰 RSS 7 flux</span>
|
||||
<span class="wf-chip">📈 yfinance 50 signaux</span>
|
||||
<span class="wf-chip">🏦 FRED (CPI/NFP/taux)</span>
|
||||
<span class="wf-chip">📅 Calendrier économique</span>
|
||||
<span class="wf-chip">📊 IV Watchlist + OI</span>
|
||||
<span class="wf-chip">🏛️ IBKR portefeuille réel</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="wf-arrow">↓</div>
|
||||
|
||||
<div class="wf-phase wf-c2">
|
||||
<div class="wf-num">②</div>
|
||||
<div class="wf-box">
|
||||
<div class="wf-box-title">Traitements & Calculs Intermédiaires</div>
|
||||
<div class="wf-box-sub">Formules déterministes — aucune IA impliquée à ce stade.</div>
|
||||
<div class="wf-chips">
|
||||
<span class="wf-chip">⏱ Decay news e^(−λt)</span>
|
||||
<span class="wf-chip">📐 IVR = (IV−min)/(max−min)</span>
|
||||
<span class="wf-chip">🎯 Régime macro 8 scénarios</span>
|
||||
<span class="wf-chip">📉 Signaux blendés 1j/5j/10j</span>
|
||||
<span class="wf-chip">🔍 Price Discovery multi-actifs</span>
|
||||
<span class="wf-chip">⚡ Tech: RSI · BB · MAs</span>
|
||||
<span class="wf-chip">🌐 Surface vol: SKEW/VVIX</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="wf-arrow">↓</div>
|
||||
|
||||
<div class="wf-phase wf-c3">
|
||||
<div class="wf-num">③</div>
|
||||
<div class="wf-box">
|
||||
<div class="wf-box-title">Chargement Specialist Desks (7 desks)</div>
|
||||
<div class="wf-box-sub">Fondamentaux + sensibilité macro + rapports à venir. Injecté dans les 2 prompts suivants.</div>
|
||||
<div class="wf-chips">
|
||||
<span class="wf-chip">💱 Forex</span>
|
||||
<span class="wf-chip">🥇 Metals</span>
|
||||
<span class="wf-chip">🌾 Agri</span>
|
||||
<span class="wf-chip">⚡ Energy</span>
|
||||
<span class="wf-chip">📊 Indices</span>
|
||||
<span class="wf-chip">₿ Crypto</span>
|
||||
<span class="wf-chip">📉 Bonds</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="wf-arrow">↓</div>
|
||||
|
||||
<div class="wf-phase wf-c4">
|
||||
<div class="wf-num">④</div>
|
||||
<div class="wf-box">
|
||||
<div class="wf-box-title">GPT-4o — Suggestion de Nouveaux Patterns</div>
|
||||
<div class="wf-box-sub">Méga-prompt injectant la totalité du contexte. Output : 4–6 patterns avec expected_move_pct et trades suggérés.</div>
|
||||
<div class="wf-chips">
|
||||
<span class="wf-chip">🤖 GPT-4o (1 appel)</span>
|
||||
<span class="wf-chip">macro + geo + news_decayed</span>
|
||||
<span class="wf-chip">7 desks injectés</span>
|
||||
<span class="wf-chip">reliability_map</span>
|
||||
<span class="wf-chip">trade_mandate €</span>
|
||||
<span class="wf-chip">portfolio_lessons</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="wf-arrow">↓</div>
|
||||
|
||||
<div class="wf-phase wf-c4">
|
||||
<div class="wf-num">⑤</div>
|
||||
<div class="wf-box">
|
||||
<div class="wf-box-title">GPT-4o — Scoring de Tous les Patterns (4 Piliers)</div>
|
||||
<div class="wf-box-sub">Score 0–100 = P1(News 30) + P2(Calendrier 20) + P3(Prix 35) + P4(R/R 15). Score delta par trade : eff_score = base + delta.</div>
|
||||
<div class="wf-chips">
|
||||
<span class="wf-chip">P1 — News & Géo (30pts)</span>
|
||||
<span class="wf-chip">P2 — Calendrier (20pts)</span>
|
||||
<span class="wf-chip">P3 — Prix multi-actifs (35pts)</span>
|
||||
<span class="wf-chip">P4 — Risk/Reward (15pts)</span>
|
||||
<span class="wf-chip">contra-signal check</span>
|
||||
<span class="wf-chip">macro_bias par asset class</span>
|
||||
<span class="wf-chip">desks ciblés injectés</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="wf-arrow">↓</div>
|
||||
|
||||
<div class="wf-phase wf-c5">
|
||||
<div class="wf-num">⑥</div>
|
||||
<div class="wf-box">
|
||||
<div class="wf-box-title">IV Gate — Filtre Dur Pré-Log</div>
|
||||
<div class="wf-box-sub">Règles déterministes sur IVR, skew, term structure. Verdict OK/WARN → continue. ALERT → skipped_trades (bloqué).</div>
|
||||
<div class="wf-chips">
|
||||
<span class="wf-chip">IVR < 30 → Longs autorisés</span>
|
||||
<span class="wf-chip">IVR 30–60 → Spreads préférés</span>
|
||||
<span class="wf-chip">IVR 60–80 → Spreads seulement</span>
|
||||
<span class="wf-chip">IVR > 80 → Vente vol uniquement</span>
|
||||
<span class="wf-chip">Skew > 8% → Bear Spread forcé</span>
|
||||
<span class="wf-chip">Backwardation → no vol sell</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="wf-arrow">↓</div>
|
||||
|
||||
<div class="wf-phase wf-c6">
|
||||
<div class="wf-num">⑦</div>
|
||||
<div class="wf-box">
|
||||
<div class="wf-box-title">Profil de Risque + Calcul EV + Log</div>
|
||||
<div class="wf-box-sub">eff_score ≥ min_score ET exp_move ≥ min_gain → profil retenu → EV calculé → trade_entry_prices.</div>
|
||||
<div class="wf-chips">
|
||||
<span class="wf-chip">p = eff_score / 100</span>
|
||||
<span class="wf-chip">G = calibrated_move / 100</span>
|
||||
<span class="wf-chip">ev_net = p×G − (1−p)</span>
|
||||
<span class="wf-chip">trade_score normalisé 0–100</span>
|
||||
<span class="wf-chip">matched_profile stocké</span>
|
||||
<span class="wf-chip">→ IBKR ticket auto-calculé</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="wf-arrow">↓</div>
|
||||
|
||||
<div class="wf-phase wf-c7">
|
||||
<div class="wf-num">⑧</div>
|
||||
<div class="wf-box">
|
||||
<div class="wf-box-title">Apprentissage Post-Cycle & Calibration</div>
|
||||
<div class="wf-box-sub">Mis à jour uniquement sur les trades "matures" (≥35% horizon écoulé) pour éviter de biaiser les stats avec des trades non résolus.</div>
|
||||
<div class="wf-chips">
|
||||
<span class="wf-chip">🎲 Bayésien Beta(1+w, 1+l)</span>
|
||||
<span class="wf-chip">📊 Reliability = WR×log(n+1)</span>
|
||||
<span class="wf-chip">🔬 Calibration w=n/(n+5)</span>
|
||||
<span class="wf-chip">📈 Brier Score calibration IA</span>
|
||||
<span class="wf-chip">🎯 K-Means clusters régimes</span>
|
||||
<span class="wf-chip">🧬 Embeddings sémantiques</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<h3 id="s2c-formulas">2c.2 Démonstration des Calculs — Simulation Complète</h3>
|
||||
<p>Simulation d'un trade <strong>Bull Call Spread WTI</strong> du cycle du 23 juin 2026. Chaque valeur est calculable à la main.</p>
|
||||
|
||||
<div class="wf-formulas">
|
||||
|
||||
<!-- CARD 1: NEWS DECAY -->
|
||||
<div class="wf-fcard blue">
|
||||
<div class="wf-fcard-head">
|
||||
<span class="icon">⏱</span>
|
||||
<div>
|
||||
<div class="title">① Decay Temporel des News</div>
|
||||
<div class="subtitle">Atténuation exponentielle — demi-vie 12h</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="wf-fcard-body">
|
||||
<div class="wf-formula">
|
||||
λ = ln(2) / 12 = <span class="highlight">0.0578 h⁻¹</span>
|
||||
decayed_score = impact_score × e<sup>−λ × age_heures</sup>
|
||||
|
||||
Article OPEC — age : 8h | impact brut : 0.78
|
||||
→ decayed = 0.78 × e<sup>−0.0578 × 8</sup> = 0.78 × 0.630 = <span class="result">0.492</span>
|
||||
</div>
|
||||
<table class="wf-example-table">
|
||||
<tr><td>Article âgé de 8h</td><td class="param">factor = 0.630</td><td class="val g">−37% vs brut</td></tr>
|
||||
<tr><td>Article âgé de 24h</td><td class="param">factor = 0.250</td><td class="val a">−75%</td></tr>
|
||||
<tr><td>Article âgé de 48h</td><td class="param">factor = 0.063</td><td class="val r">−94% (plancher 0.30 appliqué)</td></tr>
|
||||
</table>
|
||||
<div class="wf-note">Le plancher de 0.30 évite qu'un article majeur ne disparaisse complètement des 72h suivantes.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- CARD 2: REGIME MACRO -->
|
||||
<div class="wf-fcard purple">
|
||||
<div class="wf-fcard-head">
|
||||
<span class="icon">🎯</span>
|
||||
<div>
|
||||
<div class="title">② Régime Macro — 8 Scénarios (50 signaux)</div>
|
||||
<div class="subtitle">Scoring règle-basé + lissage Bayésien 25 jours</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="wf-fcard-body">
|
||||
<div class="wf-formula">
|
||||
Signal blend (anti-bruit) :
|
||||
signal_brent = 0.20 × Δ1j + 0.50 × Δ5j + 0.30 × Δ10j
|
||||
→ Ex : +3.2% | +0.8% | -0.4% → blend = <span class="result">+0.92%</span> (vs +3.2% brut)
|
||||
|
||||
Score régime brut (exemple Stagflation) :
|
||||
Brent↑↑ (+30) + Courbe plate (+10) + Or↑ (+15) + Cuivre↓ (+15) + Défensifs>Tech (+8) = <span class="result">78/100</span>
|
||||
|
||||
Lissage Bayésien (9 jours d'historique, w_prior=45%) :
|
||||
score_lissé = 0.55 × 78 + 0.45 × 65 = <span class="result">72.35</span>
|
||||
</div>
|
||||
<table class="wf-example-table">
|
||||
<tr><td><strong>Stagflation</strong></td><td class="val g">72 pts — DOMINANT ✓</td><td>Brent↑, Or↑, courbe plate, cuivre↓</td></tr>
|
||||
<tr><td>Choc Inflationniste</td><td class="val a">58 pts</td><td>Écart <15pts → résistance au switch</td></tr>
|
||||
<tr><td>Récession</td><td>43 pts</td><td>Courbe inversée mais VIX encore modéré</td></tr>
|
||||
<tr><td>Goldilocks</td><td class="val r">21 pts</td><td>S&P200j OK mais VIX élevé</td></tr>
|
||||
</table>
|
||||
<div class="wf-note">8 régimes : Goldilocks · Désinflation · Soft Landing · Reflation · Stagflation · Choc Inflationniste · Récession · Crise de Liquidité</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- CARD 3: IV RANK -->
|
||||
<div class="wf-fcard cyan">
|
||||
<div class="wf-fcard-head">
|
||||
<span class="icon">📐</span>
|
||||
<div>
|
||||
<div class="title">③ IV Rank & IV Gate</div>
|
||||
<div class="subtitle">Positionnement de la vol actuelle dans sa plage annuelle → stratégie optimale</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="wf-fcard-body">
|
||||
<div class="wf-formula">
|
||||
IVR = (IV_actuelle − IV_min_52s) / (IV_max_52s − IV_min_52s) × 100
|
||||
|
||||
WTI (OVX proxy) : IV=28%, min=18%, max=52%
|
||||
IVR = (28 − 18) / (52 − 18) × 100 = 10/34 × 100 = <span class="result">29.4%</span>
|
||||
|
||||
IV Gate → stratégie Bull Call Spread WTI :
|
||||
IVR = 29% → zone "spreads légèrement préférés" → <span class="result">OK ✓</span>
|
||||
(IVR > 60 → WARN | IVR > 80 → ALERT ✗ bloqué)
|
||||
</div>
|
||||
<div class="wf-note">L'IVR mesure à quelle hauteur est la vol actuelle dans sa plage 52 semaines — pas sa valeur absolue. Un IVR=80 sur XLE (IV=35%) est plus "cher" qu'un IVR=30 sur VIX (IV=28%).</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- CARD 4: SCORING 4 PILLARS -->
|
||||
<div class="wf-fcard amber">
|
||||
<div class="wf-fcard-head">
|
||||
<span class="icon">🎯</span>
|
||||
<div>
|
||||
<div class="title">④ Scoring GPT-4o — 4 Piliers (0–100)</div>
|
||||
<div class="subtitle">Décomposition sur le pattern "Choc Offre Pétrolier WTI"</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="wf-fcard-body">
|
||||
<table class="wf-example-table">
|
||||
<tr><td><strong>P1 — News & Géo (max 30)</strong></td><td></td></tr>
|
||||
<tr><td class="param" style="padding-left:20px">geo (max 12)</td><td class="val g">10 — 3 articles OPEC/Russie pertinents</td></tr>
|
||||
<tr><td class="param" style="padding-left:20px">eco/macro (max 10)</td><td class="val g">8 — Stagflation bullish+ énergie</td></tr>
|
||||
<tr><td class="param" style="padding-left:20px">volume/récence (max 8)</td><td class="val a">5 — 2 articles >6h (decay actif)</td></tr>
|
||||
<tr><td><strong>P1 Total → 23/30</strong></td><td class="val g">✓</td></tr>
|
||||
<tr><td colspan="2"><hr style="border:none;border-top:1px dashed #e2e8f0;margin:4px 0;"/></td></tr>
|
||||
<tr><td><strong>P2 — Calendrier (max 20)</strong></td><td></td></tr>
|
||||
<tr><td class="param" style="padding-left:20px">banques centrales (max 10)</td><td class="val a">6 — BCE dans 8j</td></tr>
|
||||
<tr><td class="param" style="padding-left:20px">macro releases (max 10)</td><td class="val g">7 — EIA dans 2j (catalyseur proche)</td></tr>
|
||||
<tr><td><strong>P2 Total → 13/20</strong></td><td class="val g">✓</td></tr>
|
||||
<tr><td colspan="2"><hr style="border:none;border-top:1px dashed #e2e8f0;margin:4px 0;"/></td></tr>
|
||||
<tr><td><strong>P3 — Prix multi-actifs (max 35)</strong></td><td></td></tr>
|
||||
<tr><td class="param" style="padding-left:20px">énergie/commod (max 7)</td><td class="val g">7 — WTI+2.3%, OVX=42 modéré</td></tr>
|
||||
<tr><td class="param" style="padding-left:20px">taux & bonds (max 7)</td><td>5 — TLT stable</td></tr>
|
||||
<tr><td class="param" style="padding-left:20px">forex (max 7)</td><td>4 — DXY neutre</td></tr>
|
||||
<tr><td class="param" style="padding-left:20px">indices (max 7)</td><td>4 — XLE+1.8%</td></tr>
|
||||
<tr><td class="param" style="padding-left:20px">VIX/IV (max 7)</td><td class="val a">5 — VIX=18, SKEW=122</td></tr>
|
||||
<tr><td><strong>P3 Total → 25/35</strong></td><td class="val g">✓</td></tr>
|
||||
<tr><td colspan="2"><hr style="border:none;border-top:1px dashed #e2e8f0;margin:4px 0;"/></td></tr>
|
||||
<tr><td><strong>P4 — Risk/Reward (max 15)</strong></td><td></td></tr>
|
||||
<tr><td class="param" style="padding-left:20px">asymétrie R/R (max 10)</td><td class="val g">8 — spread risque défini</td></tr>
|
||||
<tr><td class="param" style="padding-left:20px">timing (max 5)</td><td class="val g">4 — EIA dans 2j</td></tr>
|
||||
<tr><td><strong>P4 Total → 12/15</strong></td><td class="val g">✓</td></tr>
|
||||
</table>
|
||||
<div class="wf-formula">
|
||||
Score pattern = P1 + P2 + P3 + P4 = 23 + 13 + 25 + 12 = <span class="result">73/100</span>
|
||||
score_delta (Bull Call Spread) = <span class="highlight">+7</span> [meilleur trade du pattern, IVR=29 optimal spreads]
|
||||
eff_score = 73 + 7 = <span class="result">80/100</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- CARD 5: EV CALCULATION -->
|
||||
<div class="wf-fcard green">
|
||||
<div class="wf-fcard-head">
|
||||
<span class="icon">📊</span>
|
||||
<div>
|
||||
<div class="title">⑤ Calcul EV avec Calibration Progressive</div>
|
||||
<div class="subtitle">Pattern avec 3 trades matures (2 gagnants, avg win = +82%)</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="wf-fcard-body">
|
||||
<div class="wf-formula">
|
||||
<span class="highlight">CALIBRATION (crédibilité actuarielle)</span>
|
||||
k = 5, n = 3 trades matures
|
||||
w = 3 / (3+5) = <span class="result">0.375</span> [37.5% poids observé]
|
||||
|
||||
ai_estimate = 95% | observed_avg_win = 82%
|
||||
calibrated_move = (1−0.375)×95% + 0.375×82%
|
||||
= 59.4% + 30.8% = <span class="result">90.2%</span>
|
||||
|
||||
<span class="highlight">CALCUL EV</span>
|
||||
p = eff_score / 100 = 80/100 = 0.80
|
||||
G = calibrated_move / 100 = 0.902
|
||||
|
||||
ev_gross = p × G = 0.80 × 0.902 = <span class="result">0.722</span> (+72.2%)
|
||||
ev_net = p × G − (1−p) = 0.722 − 0.20 = <span class="result">+0.522</span> ✓ positif
|
||||
trade_score = 0.722/(0.722+0.20) × 100 = <span class="result">78.3 / 100</span>
|
||||
</div>
|
||||
<div class="wf-note">💡 Sans calibration (pattern nouveau, w=0) : G=0.95, ev_net = 0.80×0.95−0.20 = +0.56. Avec calibration : +0.52. La calibration a réduit l'optimisme de l'IA de 7% — ce qui est sain.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- CARD 6: BAYESIAN UPDATE -->
|
||||
<div class="wf-fcard teal">
|
||||
<div class="wf-fcard-head">
|
||||
<span class="icon">🎲</span>
|
||||
<div>
|
||||
<div class="title">⑥ Mise à Jour Bayésienne Post-Maturation</div>
|
||||
<div class="subtitle">Prior neutre de Laplace → les données prennent progressivement le dessus</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="wf-fcard-body">
|
||||
<div class="wf-formula">
|
||||
Distribution Beta : α = 1 + wins | β = 1 + losses
|
||||
|
||||
État actuel : 3 trades matures, 2 gagnants
|
||||
α = 1+2 = 3 | β = 1+1 = 2
|
||||
bayesian_win_rate = α/(α+β) = 3/5 = <span class="result">60%</span>
|
||||
IC95 ≈ ±0.196 → intervalle crédible [40.4% ; 79.6%] (large → encore peu de données)
|
||||
|
||||
Après 20 trades, 13 gagnants :
|
||||
α=14, β=8 → WR = 14/22 = <span class="result">63.6%</span>
|
||||
IC95 ≈ ±0.082 → [55.4% ; 71.8%] (serré → données consolidées)
|
||||
|
||||
reliability_score = win_rate × log(n+1)
|
||||
n=3, WR=60% → 0.60 × log(4) = <span class="result">0.83</span> (insuffisant, seuil fiable = 1.5)
|
||||
n=20, WR=64% → 0.64 × log(21) = <span class="result">1.95</span> (fiable ✓)
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="callout green">
|
||||
<div class="label">Zéro boîte noire — Traçabilité complète</div>
|
||||
Toutes les valeurs de ce workflow sont stockées en DB et consultables : le score 73/100 dans <code>pattern_score_history</code>, l'EV dans <code>trade_entry_prices</code>, les raisons du skipped dans <code>skipped_trades</code>, les traces GPT dans <code>ai_reasoning_traces</code>. La page <strong>Cycle Report</strong> restitue l'intégralité du raisonnement après chaque cycle.
|
||||
</div>
|
||||
|
||||
<div class="page-break"></div>
|
||||
|
||||
<!-- SECTION 12 -->
|
||||
<h2 id="s12">12. Specialist Desks — Configuration par Asset Class</h2>
|
||||
<p>Les Specialist Desks modélisent la réalité d'un hedge fund multi-stratégies où chaque desk (forex, metals, agri, énergie, indices, crypto, obligations) a une culture, des drivers et des rapports critiques spécifiques que le trader généraliste ne connaît pas par défaut.</p>
|
||||
|
||||
<h3 id="s12-1">12.1 Architecture et Tables DB</h3>
|
||||
<p>Trois tables nouvelles créées en Phase 6 :</p>
|
||||
|
||||
<table>
|
||||
<thead><tr><th>Table</th><th>Rôle</th><th>Colonnes clés</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td><code>asset_class_configs</code></td><td>Configuration par desk (7 desks)</td><td>asset_class, display_name, icon, fundamentals (TEXT libre), macro_sensitivity (JSON), price_delta_thresholds (JSON)</td></tr>
|
||||
<tr><td><code>specialist_reports</code></td><td>Catalogue de rapports (manuel + auto)</td><td>id, name, source, cadence, next_date, importance (1–3), notes</td></tr>
|
||||
<tr><td><code>report_desk_links</code></td><td>Liaison N:N reports ↔ desks</td><td>report_id, asset_class</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<p><strong>7 desks pré-configurés au démarrage :</strong></p>
|
||||
<table>
|
||||
<thead><tr><th>Desk</th><th>Drivers fondamentaux (exemples)</th><th>Rapports liés par défaut</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td>💱 Forex</td><td>Divergence politique monétaire, différentiel de taux, flux carry trade, balance des paiements</td><td>FOMC, ECB, BOJ, NFP, CPI</td></tr>
|
||||
<tr><td>🥇 Metals</td><td>Dollar index, tensions géopolitiques, demande EM, production minière, inflation réelle</td><td>COT, CPI</td></tr>
|
||||
<tr><td>🌾 Agri</td><td>Météo, USDA stocks, cycles de plantation, demande Chine, subventions gouvernementales</td><td>WASDE, COT, China PMI, USDA Export Inspections</td></tr>
|
||||
<tr><td>⚡ Energy</td><td>OPEC+, inventaires pétrole, demande transport, géopolitique Moyen-Orient, USD</td><td>EIA Petroleum, Baker Hughes Rig Count, COT</td></tr>
|
||||
<tr><td>📊 Indices</td><td>Earnings saison, Fed policy, rotation sectorielle, VIX, buybacks</td><td>FOMC, NFP, CPI, Earnings</td></tr>
|
||||
<tr><td>₿ Crypto</td><td>Régulation, adoption institutionnelle, flux ETF, halving BTC, risque on/off</td><td>VIX (proxy risk-off)</td></tr>
|
||||
<tr><td>📉 Bonds</td><td>Politique monétaire, inflation anticipée, pente de courbe, qualité de crédit</td><td>FOMC, ECB, BOJ, CPI, NFP</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="callout amber">
|
||||
<div class="label">Rapports non-calendrier</div>
|
||||
Contrairement à la page Institutional Reports (rapports auto-fetchés), les Specialist Desks accueillent des rapports <strong>entièrement manuels</strong> qui n'existent dans aucun calendrier économique standard. Exemple : le <em>Cocoa Grinding Report</em> (trimestriel, World Cocoa Foundation) n'est pas dans Bloomberg ni Reuters — il peut être ajouté manuellement avec une date prévisionnelle et lié au desk Agri.
|
||||
</div>
|
||||
|
||||
<h3 id="s12-2">12.2 Injection dans les Prompts IA</h3>
|
||||
<p>Le bloc Specialist Desks est injecté à <strong>deux endroits</strong> du pipeline :</p>
|
||||
|
||||
<table>
|
||||
<thead><tr><th>Prompt</th><th>Asset classes injectées</th><th>Contenu par desk</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td><code>score_patterns_with_context</code></td><td>Uniquement les asset classes présentes dans les patterns scorés ce cycle</td><td>Fondamentaux (≤300 chars), macro sensitivity (4 pairs régime→effet), price thresholds, 5 prochains rapports planifiés</td></tr>
|
||||
<tr><td><code>suggest_patterns_from_market_context</code></td><td><strong>Tous les 7 desks</strong> systématiquement</td><td>Identique — permet à l'IA de générer des suggestions sur tous les desks même si aucun pattern existant ne les couvre</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<p>Format du bloc injecté dans le prompt (exemple desk Agri) :</p>
|
||||
<div class="arch-box">## SPECIALIST DESK — AGRI 🌾
|
||||
Key drivers: Météo, USDA stocks, cycles de plantation, demande Chine...
|
||||
Regime sensitivity: Stagflation → bullish | Goldilocks → neutral | Récession → bearish | Disinflation → bearish
|
||||
Price thresholds: significant_week=3% extreme_week=8%
|
||||
Upcoming reports:
|
||||
• WASDE (USDA) — 2026-07-11 [monthly] ⭐×3
|
||||
• USDA Export Inspections (USDA) — 2026-06-30 [weekly]</div>
|
||||
|
||||
<div class="page-break"></div>
|
||||
|
||||
<!-- SECTION 13 -->
|
||||
<h2 id="s13">13. Bibliothèque Patterns — Find Similar & Merge</h2>
|
||||
<p>Après chaque cycle, l'IA peut ajouter des patterns qui recoupent des patterns existants. La feature Find Similar permet de détecter et fusionner ces doublons tout en préservant toutes les données associées.</p>
|
||||
|
||||
<h3 id="s13-1">13.1 Analyse de Similarité GPT-4o-mini</h3>
|
||||
<p>Endpoint : <code>POST /api/patterns/find-similar</code></p>
|
||||
|
||||
<table>
|
||||
<thead><tr><th>Recommandation</th><th>Condition</th><th>Action proposée</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td><code>merge_as_instance</code></td><td>Même régime + même instrument + <strong>même direction</strong></td><td>Bouton "Fusionner dans [X]" avec confirmation</td></tr>
|
||||
<tr><td><code>counter_scenario</code></td><td>Même régime + même instrument + <strong>direction opposée</strong></td><td>Appliquer le regime_tag commun — les deux patterns restent</td></tr>
|
||||
<tr><td><code>new_pattern</code></td><td>Régime ou instrument différent, pas de match structurel</td><td>Badge vert "Pattern unique" — rien à faire</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<p><strong>Règle critique :</strong> ne jamais fusionner des directions opposées (bullish WTI + bearish WTI = deux scénarios valides du même régime, pas un doublon).</p>
|
||||
|
||||
<h3 id="s13-2">13.2 Fusion et Remapping Complet des Données</h3>
|
||||
<p>Endpoint : <code>POST /api/patterns/merge</code> — transaction atomique SQLite :</p>
|
||||
|
||||
<div class="arch-box">1. Charger pattern_keep + pattern_discard
|
||||
2. Fusionner historical_instances[] → union + dédup par (date, event)
|
||||
3. merged_hits = keep.backtest_hits + discard.backtest_hits
|
||||
merged_runs = keep.backtest_runs_count + discard.backtest_runs_count
|
||||
4. merged_regime = keep.regime_tag OR discard.regime_tag
|
||||
5. UPDATE pattern_score_history SET pattern_id = keep_id WHERE pattern_id = discard_id
|
||||
UPDATE trade_entry_prices SET pattern_id = keep_id WHERE pattern_id = discard_id
|
||||
UPDATE ai_reasoning_traces SET pattern_id = keep_id WHERE pattern_id = discard_id
|
||||
UPDATE ai_call_logs SET pattern_id = keep_id WHERE pattern_id = discard_id
|
||||
UPDATE skipped_trades SET pattern_id = keep_id WHERE pattern_id = discard_id
|
||||
DELETE FROM pattern_embeddings WHERE pattern_id = discard_id (sera régénéré)
|
||||
6. UPDATE custom_patterns SET ... WHERE id = keep_id (données fusionnées)
|
||||
7. DELETE FROM custom_patterns WHERE id = discard_id</div>
|
||||
|
||||
<div class="callout green">
|
||||
<div class="label">Intégrité garantie</div>
|
||||
Toutes les tables liées par foreign key logicielle sont remappées avant la suppression. L'Analytics, la Calibration et le Journal continuent de voir les données du pattern discardé, maintenant consolidées sous le pattern conservé. La calibration recalculera le nouveau poids de crédibilité au prochain cycle.
|
||||
</div>
|
||||
|
||||
<div class="page-break"></div>
|
||||
|
||||
<!-- SECTION 14 -->
|
||||
<h2 id="s14">14. Calibration Progressive — IA vs Observé</h2>
|
||||
<p>L'<code>expected_move_pct</code> est initialement une estimation de GPT-4o du rendement option (levier inclus) si la thèse est confirmée. Ce n'est pas un calcul de pricing options (pas de Black-Scholes). La calibration progressive remplace progressivement cette estimation par le rendement réellement observé sur les trades matures.</p>
|
||||
|
||||
<h3 id="s14-1">14.1 Formule de Crédibilité et Blending</h3>
|
||||
<p>Modèle actuariel de crédibilité (Bühlmann-Straub simplifié) :</p>
|
||||
|
||||
<div class="formula-box">k = 5 [constante de crédibilité]
|
||||
n = nombre de trades matures (≥35% de l'horizon écoulé)
|
||||
|
||||
w (poids observé) = n / (n + k)
|
||||
|
||||
observed_avg_win_pct = moyenne(pnl_pct) sur les trades gagnants matures
|
||||
calibrated_expected_move = (1 − w) × ai_estimate + w × observed_avg_win_pct
|
||||
[seulement calculé si au moins 1 trade gagnant]</div>
|
||||
|
||||
<p><strong>Progression du poids observé :</strong></p>
|
||||
<table>
|
||||
<thead><tr><th>n trades matures</th><th>Poids observé (w)</th><th>Source label</th><th>Interprétation</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td>0</td><td>0%</td><td class="val-blue">Pure AI</td><td>100% estimation IA, aucune donnée réelle</td></tr>
|
||||
<tr><td>1</td><td>17%</td><td class="val-blue">Early data</td><td>Légère influence des données — encore majoritairement IA</td></tr>
|
||||
<tr><td>3</td><td>38%</td><td class="val-amber">Early data</td><td>Données commencent à corriger l'IA</td></tr>
|
||||
<tr><td>5</td><td>50%</td><td class="val-amber">Mixed</td><td>Équilibre parfait IA / observé</td></tr>
|
||||
<tr><td>10</td><td>67%</td><td class="val-amber">Mixed</td><td>Données majoritaires</td></tr>
|
||||
<tr><td>20</td><td>80%</td><td class="val-green">Data-driven</td><td>IA en fond de correction seulement</td></tr>
|
||||
<tr><td>50</td><td>91%</td><td class="val-green">Data-driven</td><td>Quasi-entièrement ancré dans le réel</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="callout blue">
|
||||
<div class="label">Rôle dans l'EV</div>
|
||||
Le <code>calibrated_expected_move</code> est le <strong>G</strong> dans <code>ev_net = p×G − (1−p)</code>. Un G trop optimiste (IA pure sur pattern nouveau) surévalue l'EV. Avec 10 trades gagnants observés à 65% en moyenne là où l'IA estimait 95%, le calibré vaut 0.67×65 + 0.33×95 ≈ 75% — l'EV recalculé est 11% plus conservateur. Cela peut faire passer un trade de "loggé" à "skipped" si le profil exige gain ≥ 80%.
|
||||
</div>
|
||||
|
||||
<h3 id="s14-2">14.2 Affichage et Rapport de Cycle</h3>
|
||||
|
||||
<p><strong>Card PatternExplorer — MaturityBadge :</strong></p>
|
||||
<ul>
|
||||
<li>Badge source coloré : <em>Pure AI</em> (gris) / <em>Early data</em> (bleu) / <em>Mixed</em> (violet) / <em>Data-driven</em> (vert)</li>
|
||||
<li>Barre dégradée slate→violet représentant le pourcentage de poids observé</li>
|
||||
<li>Grille 3 cases : Win rate Bayésien | Estimation IA (%) | Calibré (%)</li>
|
||||
</ul>
|
||||
|
||||
<p><strong>Section Calibration dans le Rapport de Cycle :</strong></p>
|
||||
<ul>
|
||||
<li>Barre globale avec le poids observé moyen sur tous les patterns actifs</li>
|
||||
<li>4 compteurs (Pure AI / Early / Mixed / Data-driven)</li>
|
||||
<li>Table per-pattern : n trades, win rate, AI estimate → Calibré, avec code couleur source</li>
|
||||
</ul>
|
||||
|
||||
<p><strong>Mise à jour automatique :</strong> <code>update_bayesian_posteriors()</code> est appelé à la fin de chaque cycle. Il recalcule simultanément les posteriors Beta(α,β) et les valeurs de calibration pour tous les patterns ayant des trades matures.</p>
|
||||
|
||||
<table>
|
||||
<thead><tr><th>Nouvelle colonne custom_patterns</th><th>Type</th><th>Description</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td><code>calibrated_expected_move</code></td><td>REAL</td><td>Valeur blendée utilisée comme G dans l'EV. NULL si aucun trade gagnant.</td></tr>
|
||||
<tr><td><code>calibration_weight</code></td><td>REAL 0–1</td><td>w = n/(n+5). Stocké pour affichage frontend sans recalcul.</td></tr>
|
||||
<tr><td><code>observed_avg_win_pct</code></td><td>REAL</td><td>Moyenne des pnl_pct > 0 sur trades matures. NULL si aucun gain.</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<hr>
|
||||
|
||||
<div class="footer-doc">
|
||||
<p>Document généré le 22 juin 2026 · GeoOptions Intelligence v5.0</p>
|
||||
<p>FastAPI + React + SQLite + GPT-4o · 19 pages · 5 phases · Usage interne trader</p>
|
||||
<p>Sections : Architecture · Cycle complet simulé · Phase 5 Cycle Context Engine · Phase 1 IV/Structure + IV Gate + Watchlist · Phase 2 Fiabilité · Phase 3 Risk Engine · Pattern Convergence + IBKR Ticket · Phase 4 IA Probabiliste · 7 Prompts IA · 20+ Tables DB · Guide 19 pages · 19 Décisions conception · Glossaire 50+ termes</p>
|
||||
<p>Document généré le 23 juin 2026 · GeoOptions Intelligence v6.0</p>
|
||||
<p>FastAPI + React + SQLite + GPT-4o · 23 pages · 6 phases · Usage interne trader</p>
|
||||
<p>Sections : Architecture · Cycle complet simulé · Phase 5 Cycle Context Engine · Phase 1 IV/Structure + IV Gate + Watchlist · Phase 2 Fiabilité · Phase 3 Risk Engine · Pattern Convergence + IBKR Ticket · Phase 4 IA Probabiliste · 7 Prompts IA · 20+ Tables DB · Guide 19 pages · 19 Décisions conception · Glossaire 50+ termes · Specialist Desks · Calibration Progressive · Find Similar+Merge · Workflow Calculs</p>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
|
||||
@@ -3,10 +3,13 @@ Specialist Desks — per asset-class fundamental configs + report catalogue.
|
||||
Prefix: /api/specialist-desks
|
||||
"""
|
||||
import uuid
|
||||
import logging
|
||||
from typing import Optional, List
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from pydantic import BaseModel
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter(prefix="/api/specialist-desks", tags=["specialist-desks"])
|
||||
|
||||
|
||||
@@ -45,6 +48,20 @@ class ReportUpsert(BaseModel):
|
||||
desks: List[str] = []
|
||||
|
||||
|
||||
class ReportResultUpdate(BaseModel):
|
||||
actual_value: Optional[float] = None
|
||||
consensus_estimate: Optional[float] = None
|
||||
text_sentiment_score: Optional[float] = None
|
||||
text_sentiment_label: Optional[str] = None
|
||||
text_sentiment_summary: Optional[str] = None
|
||||
|
||||
|
||||
class ScoreTextRequest(BaseModel):
|
||||
text: str
|
||||
report_name: str = ""
|
||||
desk: str = "forex"
|
||||
|
||||
|
||||
# ── Configs ────────────────────────────────────────────────────────────────────
|
||||
|
||||
@router.get("/configs")
|
||||
@@ -142,3 +159,79 @@ def unlink_from_desk(report_id: str, asset_class: str):
|
||||
from services.database import unlink_report_desk
|
||||
unlink_report_desk(report_id, asset_class)
|
||||
return {"unlinked": report_id, "desk": asset_class}
|
||||
|
||||
|
||||
# ── Surprise Index ─────────────────────────────────────────────────────────────
|
||||
|
||||
@router.put("/reports/{report_id}/result")
|
||||
def update_report_result(report_id: str, body: ReportResultUpdate):
|
||||
from services.database import update_report_result as _upd
|
||||
_upd(
|
||||
report_id=report_id,
|
||||
actual_value=body.actual_value,
|
||||
consensus_estimate=body.consensus_estimate,
|
||||
text_sentiment_score=body.text_sentiment_score,
|
||||
text_sentiment_label=body.text_sentiment_label,
|
||||
text_sentiment_summary=body.text_sentiment_summary,
|
||||
)
|
||||
return {"saved": report_id}
|
||||
|
||||
|
||||
# ── COT Data ───────────────────────────────────────────────────────────────────
|
||||
|
||||
@router.get("/cot")
|
||||
def get_cot_data():
|
||||
from services.database import get_latest_cot_data
|
||||
return get_latest_cot_data()
|
||||
|
||||
|
||||
@router.post("/cot/fetch")
|
||||
def fetch_cot():
|
||||
from services.cot_fetcher import fetch_all_cot
|
||||
from services.database import save_cot_data
|
||||
try:
|
||||
data = fetch_all_cot()
|
||||
if data:
|
||||
saved = save_cot_data(data)
|
||||
return {"fetched": len(data), "saved": saved}
|
||||
return {"fetched": 0, "saved": 0, "warning": "No COT data returned"}
|
||||
except Exception as e:
|
||||
logger.error(f"COT fetch error: {e}")
|
||||
raise HTTPException(500, str(e))
|
||||
|
||||
|
||||
# ── Forward Curves ─────────────────────────────────────────────────────────────
|
||||
|
||||
@router.get("/forward-curves")
|
||||
def get_forward_curves():
|
||||
from services.database import get_latest_forward_curves
|
||||
return get_latest_forward_curves()
|
||||
|
||||
|
||||
@router.post("/forward-curves/fetch")
|
||||
def fetch_forward_curves_endpoint():
|
||||
from services.forward_curve import fetch_forward_curves
|
||||
from services.database import save_forward_curves
|
||||
try:
|
||||
data = fetch_forward_curves()
|
||||
if data:
|
||||
saved = save_forward_curves(data)
|
||||
return {"fetched": len(data), "saved": saved}
|
||||
return {"fetched": 0, "saved": 0, "warning": "No curve data returned"}
|
||||
except Exception as e:
|
||||
logger.error(f"Forward curve fetch error: {e}")
|
||||
raise HTTPException(500, str(e))
|
||||
|
||||
|
||||
# ── Hawk / Dove Text Scorer ────────────────────────────────────────────────────
|
||||
|
||||
@router.post("/score-text")
|
||||
def score_text(body: ScoreTextRequest):
|
||||
"""Score a central bank / commodity report excerpt via GPT-4o-mini."""
|
||||
try:
|
||||
from services.ai_analyzer import score_report_text
|
||||
result = score_report_text(body.text, body.report_name, body.desk)
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.error(f"Text scoring error: {e}")
|
||||
raise HTTPException(500, str(e))
|
||||
|
||||
@@ -377,10 +377,21 @@ SCORE DISTRIBUTION RULE (MANDATORY):
|
||||
|
||||
|
||||
def _build_specialist_context_block(asset_classes: set) -> str:
|
||||
"""Build a SPECIALIST DESK CONTEXT block for the given asset classes."""
|
||||
"""Build SPECIALIST DESK CONTEXT block including COT, forward curves, surprise scores."""
|
||||
try:
|
||||
from services.database import get_asset_class_config, get_desk_reports
|
||||
from services.database import get_asset_class_config, get_desk_reports, get_latest_cot_data, get_latest_forward_curves
|
||||
from datetime import datetime as _dt
|
||||
|
||||
# Fetch COT and forward curve data once
|
||||
try:
|
||||
all_cot = get_latest_cot_data()
|
||||
except Exception:
|
||||
all_cot = []
|
||||
try:
|
||||
all_curves = get_latest_forward_curves()
|
||||
except Exception:
|
||||
all_curves = []
|
||||
|
||||
blocks = []
|
||||
for ac in sorted(asset_classes):
|
||||
cfg = get_asset_class_config(ac)
|
||||
@@ -388,16 +399,19 @@ def _build_specialist_context_block(asset_classes: set) -> str:
|
||||
continue
|
||||
reports = get_desk_reports(ac)
|
||||
lines = [f"\n## SPECIALIST DESK — {cfg['display_name'].upper()} {cfg.get('icon','')}"]
|
||||
|
||||
if cfg.get("fundamentals"):
|
||||
lines.append(f"Key drivers: {cfg['fundamentals'][:300]}")
|
||||
|
||||
# Macro sensitivity
|
||||
sensitivities = cfg.get("macro_sensitivity") or []
|
||||
if sensitivities:
|
||||
sens_str = " | ".join(
|
||||
f"{s.get('regime','?')} → {s.get('effect','?')}"
|
||||
f"{s.get('regime','?')} -> {s.get('effect','?')}"
|
||||
for s in sensitivities[:4]
|
||||
)
|
||||
lines.append(f"Regime sensitivity: {sens_str}")
|
||||
|
||||
# Price thresholds
|
||||
thresh = cfg.get("price_delta_thresholds") or {}
|
||||
if thresh:
|
||||
@@ -405,7 +419,30 @@ def _build_specialist_context_block(asset_classes: set) -> str:
|
||||
f"Price thresholds: significant_week={thresh.get('significant_week','?')}% "
|
||||
f"extreme_week={thresh.get('extreme_week','?')}%"
|
||||
)
|
||||
# Upcoming reports
|
||||
|
||||
# COT positioning for this asset class
|
||||
cot_ac = [c for c in all_cot if c.get("asset_class") == ac]
|
||||
if cot_ac:
|
||||
lines.append("COT Positioning (Money Managers net, % of OI):")
|
||||
for c in cot_ac[:5]:
|
||||
net_pct = c.get("net_pct_oi", 0)
|
||||
chg = c.get("change_net", 0)
|
||||
direction = "LONG" if net_pct > 5 else "SHORT" if net_pct < -5 else "NEUTRAL"
|
||||
chg_str = f" (chg {'+' if chg >= 0 else ''}{chg:,} wk)" if chg else ""
|
||||
lines.append(f" - {c['commodity']}: net={net_pct:+.1f}% OI [{direction}]{chg_str} -- {c.get('report_date','?')}")
|
||||
|
||||
# Forward curves for this asset class
|
||||
curves_ac = [c for c in all_curves if c.get("asset_class") == ac]
|
||||
if curves_ac:
|
||||
lines.append("Forward Curve Structure (spot vs +3M):")
|
||||
for curve in curves_ac[:5]:
|
||||
struct = curve.get("structure", "unknown")
|
||||
slope = curve.get("slope_pct")
|
||||
slope_str = f" ({slope:+.1f}%)" if slope is not None else ""
|
||||
struct_label = struct.upper()
|
||||
lines.append(f" - {curve['asset']}: {struct_label}{slope_str}")
|
||||
|
||||
# Recent reports with surprise scores
|
||||
today = _dt.utcnow().strftime("%Y-%m-%d")
|
||||
upcoming = sorted(
|
||||
[r for r in reports if r.get("next_date") and r["next_date"] >= today],
|
||||
@@ -414,16 +451,85 @@ def _build_specialist_context_block(asset_classes: set) -> str:
|
||||
if upcoming:
|
||||
lines.append("Upcoming reports:")
|
||||
for r in upcoming:
|
||||
lines.append(
|
||||
f" • {r['name']} ({r['source']}) — {r['next_date']} [{r['cadence']}]"
|
||||
+ (f" ⭐×{r['importance']}" if r.get("importance", 1) >= 3 else "")
|
||||
)
|
||||
line = f" - {r['name']} ({r['source']}) -- {r['next_date']} [{r['cadence']}]"
|
||||
if r.get("importance", 1) >= 3:
|
||||
line += " ***"
|
||||
if r.get("consensus_estimate") is not None:
|
||||
line += f" [consensus: {r['consensus_estimate']}]"
|
||||
lines.append(line)
|
||||
|
||||
# Recent releases with surprise scores
|
||||
recent_releases = sorted(
|
||||
[r for r in reports if r.get("last_date") and r.get("surprise_score") is not None],
|
||||
key=lambda r: r.get("last_date", ""),
|
||||
reverse=True
|
||||
)[:3]
|
||||
if recent_releases:
|
||||
lines.append("Recent releases (surprise index = actual - consensus):")
|
||||
for r in recent_releases:
|
||||
surprise = r["surprise_score"]
|
||||
label = r.get("text_sentiment_label", "")
|
||||
label_str = f" [{label}]" if label else ""
|
||||
sign = "+" if surprise >= 0 else ""
|
||||
lines.append(f" - {r['name']}: surprise={sign}{surprise:.2f}{label_str} -- {r.get('last_date','?')}")
|
||||
|
||||
blocks.append("\n".join(lines))
|
||||
return "\n".join(blocks) + "\n" if blocks else ""
|
||||
except Exception:
|
||||
return ""
|
||||
|
||||
|
||||
def score_report_text(text: str, report_name: str = "", desk: str = "forex") -> Dict:
|
||||
"""Score a report/statement excerpt. Returns score (-1 to +1), label, summary, key_phrases."""
|
||||
if desk in ("forex", "bonds"):
|
||||
dimension = "monetary policy stance: -1 = very dovish (rate cuts expected), 0 = neutral, +1 = very hawkish (rate hikes expected)"
|
||||
examples = "Hawkish: 'inflation remains persistent', 'further tightening may be appropriate'. Dovish: 'inflation returning to target', 'downside risks dominate', 'easing cycle beginning'."
|
||||
elif desk in ("energy", "metals", "agri"):
|
||||
dimension = "commodity market outlook: -1 = very bearish (oversupply, demand weakness), 0 = neutral, +1 = very bullish (deficit, demand surge)"
|
||||
examples = "Bullish: 'output cuts extended', 'crop failure', 'stockpiles at multi-year low'. Bearish: 'surplus expected', 'demand revision lower', 'record production'."
|
||||
else:
|
||||
dimension = "market sentiment: -1 = very bearish, 0 = neutral, +1 = very bullish"
|
||||
examples = ""
|
||||
|
||||
prompt = f"""You are a macro trading analyst. Analyze the following text and return ONLY valid JSON.
|
||||
|
||||
Report: {report_name or 'Unknown'}
|
||||
Desk: {desk}
|
||||
Score dimension: {dimension}
|
||||
{examples}
|
||||
|
||||
TEXT TO ANALYZE:
|
||||
{text[:3000]}
|
||||
|
||||
Return JSON with exactly these fields:
|
||||
{{
|
||||
"score": <float from -1.0 to +1.0>,
|
||||
"label": <"very_hawkish" | "hawkish" | "neutral" | "dovish" | "very_dovish"> (for forex/bonds)
|
||||
or <"very_bullish" | "bullish" | "neutral" | "bearish" | "very_bearish"> (for others),
|
||||
"summary": <1-2 sentence summary of the key signal>,
|
||||
"key_phrases": [<up to 5 most significant phrases that drove the score>],
|
||||
"confidence": <"high" | "medium" | "low">
|
||||
}}"""
|
||||
|
||||
try:
|
||||
import openai as _oai
|
||||
from services.database import get_config as _gc
|
||||
api_key = _gc("openai_api_key") or ""
|
||||
if not api_key:
|
||||
return {"score": 0, "label": "neutral", "summary": "No API key configured", "key_phrases": [], "confidence": "low"}
|
||||
client = _oai.OpenAI(api_key=api_key)
|
||||
response = client.chat.completions.create(
|
||||
model="gpt-4o-mini",
|
||||
messages=[{"role": "user", "content": prompt}],
|
||||
temperature=0.1,
|
||||
response_format={"type": "json_object"},
|
||||
)
|
||||
import json
|
||||
return json.loads(response.choices[0].message.content)
|
||||
except Exception as e:
|
||||
return {"score": 0, "label": "neutral", "summary": f"Error: {str(e)}", "key_phrases": [], "confidence": "low"}
|
||||
|
||||
|
||||
def score_patterns_with_context(
|
||||
patterns: List[Dict],
|
||||
recent_news: List[Dict],
|
||||
|
||||
@@ -594,6 +594,21 @@ def run_cycle_once(trigger: str = "auto") -> Dict[str, Any]:
|
||||
except Exception as _ibe:
|
||||
logger.warning(f"[Cycle] Institutional block failed (non-blocking): {_ibe}")
|
||||
|
||||
# ── COT + Forward Curves (background refresh, non-blocking) ───────────────
|
||||
try:
|
||||
from services.cot_fetcher import fetch_all_cot
|
||||
from services.forward_curve import fetch_forward_curves
|
||||
from services.database import save_cot_data, save_forward_curves as _sfc
|
||||
cot_data = fetch_all_cot()
|
||||
if cot_data:
|
||||
save_cot_data(cot_data)
|
||||
curve_data = fetch_forward_curves()
|
||||
if curve_data:
|
||||
_sfc(curve_data)
|
||||
except Exception as _e:
|
||||
import logging as _logging
|
||||
_logging.getLogger(__name__).warning(f"COT/Curve refresh failed (non-blocking): {_e}")
|
||||
|
||||
suggestions = suggest_patterns_from_market_context(
|
||||
news, quotes, calendar, macro_regime=macro_regime, geo_score=geo_score_obj,
|
||||
portfolio_lessons=portfolio_lessons,
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
"""
|
||||
CFTC Commitment of Traders (COT) weekly fetcher.
|
||||
Data from CFTC Socrata public API — no API key required.
|
||||
|
||||
Two modes:
|
||||
- fetch_cot_report(): legacy institutional_reports format (used by institutional_scheduler)
|
||||
- fetch_all_cot(): flat list of per-commodity/asset COT entries (used by specialist desks / cot_data table)
|
||||
"""
|
||||
import logging
|
||||
import math
|
||||
@@ -192,3 +196,162 @@ def fetch_cot_report() -> Optional[Dict]:
|
||||
**signals,
|
||||
"ai_summary": ai_summary,
|
||||
}
|
||||
|
||||
|
||||
# ── Specialist Desks flat COT feed (cot_data table) ──────────────────────────
|
||||
|
||||
_DISAGG_URL = "https://publicreporting.cftc.gov/resource/72hh-3qpy.json" # disaggregated commodities
|
||||
_LEGACY_FIN_URL = "https://publicreporting.cftc.gov/resource/gpe5-46if.json" # financial/forex
|
||||
|
||||
_DISAGG_MARKETS = [
|
||||
("CRUDE OIL, LIGHT SWEET - NYMEX", "WTI Crude", "energy"),
|
||||
("NATURAL GAS - NYMEX", "Natural Gas", "energy"),
|
||||
("GOLD - COMEX", "Gold", "metals"),
|
||||
("SILVER - COMEX", "Silver", "metals"),
|
||||
("COPPER- #1 - COMEX", "Copper", "metals"),
|
||||
("CORN - CBOT", "Corn", "agri"),
|
||||
("WHEAT - CBOT", "Wheat", "agri"),
|
||||
("SOYBEANS - CBOT", "Soybeans", "agri"),
|
||||
("SOYBEAN OIL - CBOT", "Soybean Oil", "agri"),
|
||||
("COCOA - ICE", "Cocoa", "agri"),
|
||||
("COFFEE C - ICE", "Coffee", "agri"),
|
||||
]
|
||||
|
||||
_FIN_MARKETS = [
|
||||
("EURO FX - CME", "Euro (EUR/USD)", "forex"),
|
||||
("JAPANESE YEN - CME", "Japanese Yen", "forex"),
|
||||
("BRITISH POUND STERLING - CME", "British Pound", "forex"),
|
||||
("SWISS FRANC - CME", "Swiss Franc", "forex"),
|
||||
("U.S. DOLLAR INDEX - ICE FUTURES U.S.", "DXY Index", "forex"),
|
||||
("30-DAY FEDERAL FUNDS - CBOT", "Fed Funds", "bonds"),
|
||||
("U.S. TREASURY BONDS - CBOT", "US T-Bonds", "bonds"),
|
||||
("10-YEAR U.S. TREASURY NOTES - CBOT", "10Y T-Notes", "bonds"),
|
||||
]
|
||||
|
||||
|
||||
def _parse_int_flat(v) -> int:
|
||||
try:
|
||||
return int(str(v).replace(",", "").strip())
|
||||
except Exception:
|
||||
return 0
|
||||
|
||||
|
||||
def _fetch_disaggregated_flat() -> List[Dict[str, Any]]:
|
||||
"""Fetch commodity COT (disaggregated) — MM long/short positions."""
|
||||
results = []
|
||||
market_names = [m[0] for m in _DISAGG_MARKETS]
|
||||
name_map = {m[0]: (m[1], m[2]) for m in _DISAGG_MARKETS}
|
||||
|
||||
quoted = ", ".join(f"'{n}'" for n in market_names)
|
||||
params = {
|
||||
"$select": "market_and_exchange_names,report_date_as_yyyy_mm_dd,m_money_positions_long_all,m_money_positions_short_all,open_interest_all,change_in_m_money_long_all,change_in_m_money_short_all",
|
||||
"$where": f"market_and_exchange_names in ({quoted})",
|
||||
"$order": "report_date_as_yyyy_mm_dd DESC",
|
||||
"$limit": str(len(market_names) * 2),
|
||||
}
|
||||
try:
|
||||
resp = requests.get(_DISAGG_URL, params=params, timeout=20)
|
||||
resp.raise_for_status()
|
||||
rows = resp.json()
|
||||
except Exception as e:
|
||||
logger.error(f"COT disaggregated fetch failed: {e}")
|
||||
return []
|
||||
|
||||
seen: set = set()
|
||||
for row in rows:
|
||||
mkt = row.get("market_and_exchange_names", "")
|
||||
if mkt not in name_map or mkt in seen:
|
||||
continue
|
||||
seen.add(mkt)
|
||||
label, ac = name_map[mkt]
|
||||
|
||||
mm_long = _parse_int_flat(row.get("m_money_positions_long_all", 0))
|
||||
mm_short = _parse_int_flat(row.get("m_money_positions_short_all", 0))
|
||||
oi = _parse_int_flat(row.get("open_interest_all", 0))
|
||||
net = mm_long - mm_short
|
||||
chg_long = _parse_int_flat(row.get("change_in_m_money_long_all", 0))
|
||||
chg_short = _parse_int_flat(row.get("change_in_m_money_short_all", 0))
|
||||
change_net = chg_long - chg_short
|
||||
net_pct_oi = round(net / oi * 100, 2) if oi else 0.0
|
||||
|
||||
results.append({
|
||||
"market_name": mkt,
|
||||
"commodity": label,
|
||||
"asset_class": ac,
|
||||
"report_date": row.get("report_date_as_yyyy_mm_dd", "")[:10],
|
||||
"mm_long": mm_long,
|
||||
"mm_short": mm_short,
|
||||
"open_interest": oi,
|
||||
"net_position": net,
|
||||
"net_pct_oi": net_pct_oi,
|
||||
"change_net": change_net,
|
||||
})
|
||||
|
||||
return results
|
||||
|
||||
|
||||
def _fetch_financial_flat() -> List[Dict[str, Any]]:
|
||||
"""Fetch financial/forex COT (legacy) — Non-commercial long/short."""
|
||||
results = []
|
||||
market_names = [m[0] for m in _FIN_MARKETS]
|
||||
name_map = {m[0]: (m[1], m[2]) for m in _FIN_MARKETS}
|
||||
|
||||
quoted = ", ".join(f"'{n}'" for n in market_names)
|
||||
params = {
|
||||
"$select": "market_and_exchange_names,report_date_as_yyyy_mm_dd,noncomm_positions_long_all,noncomm_positions_short_all,open_interest_all,change_in_noncomm_long_all,change_in_noncomm_short_all",
|
||||
"$where": f"market_and_exchange_names in ({quoted})",
|
||||
"$order": "report_date_as_yyyy_mm_dd DESC",
|
||||
"$limit": str(len(market_names) * 2),
|
||||
}
|
||||
try:
|
||||
resp = requests.get(_LEGACY_FIN_URL, params=params, timeout=20)
|
||||
resp.raise_for_status()
|
||||
rows = resp.json()
|
||||
except Exception as e:
|
||||
logger.error(f"COT financial fetch failed: {e}")
|
||||
return []
|
||||
|
||||
seen: set = set()
|
||||
for row in rows:
|
||||
mkt = row.get("market_and_exchange_names", "")
|
||||
if mkt not in name_map or mkt in seen:
|
||||
continue
|
||||
seen.add(mkt)
|
||||
label, ac = name_map[mkt]
|
||||
|
||||
nc_long = _parse_int_flat(row.get("noncomm_positions_long_all", 0))
|
||||
nc_short = _parse_int_flat(row.get("noncomm_positions_short_all", 0))
|
||||
oi = _parse_int_flat(row.get("open_interest_all", 0))
|
||||
net = nc_long - nc_short
|
||||
chg_long = _parse_int_flat(row.get("change_in_noncomm_long_all", 0))
|
||||
chg_short = _parse_int_flat(row.get("change_in_noncomm_short_all", 0))
|
||||
change_net = chg_long - chg_short
|
||||
net_pct_oi = round(net / oi * 100, 2) if oi else 0.0
|
||||
|
||||
results.append({
|
||||
"market_name": mkt,
|
||||
"commodity": label,
|
||||
"asset_class": ac,
|
||||
"report_date": row.get("report_date_as_yyyy_mm_dd", "")[:10],
|
||||
"mm_long": nc_long,
|
||||
"mm_short": nc_short,
|
||||
"open_interest": oi,
|
||||
"net_position": net,
|
||||
"net_pct_oi": net_pct_oi,
|
||||
"change_net": change_net,
|
||||
})
|
||||
|
||||
return results
|
||||
|
||||
|
||||
def fetch_all_cot() -> List[Dict[str, Any]]:
|
||||
"""Fetch both disaggregated (commodities) and financial (forex/bonds) COT data.
|
||||
|
||||
Returns a flat list of per-market dicts suitable for save_cot_data().
|
||||
"""
|
||||
logger.info("Fetching COT data from CFTC (specialist desks feed)...")
|
||||
all_data: List[Dict[str, Any]] = []
|
||||
all_data.extend(_fetch_disaggregated_flat())
|
||||
all_data.extend(_fetch_financial_flat())
|
||||
logger.info(f"COT: fetched {len(all_data)} markets")
|
||||
return all_data
|
||||
|
||||
@@ -108,6 +108,32 @@ def init_db():
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Specialist Reports — surprise index + text sentiment columns
|
||||
try:
|
||||
c.execute("ALTER TABLE specialist_reports ADD COLUMN consensus_estimate REAL")
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
c.execute("ALTER TABLE specialist_reports ADD COLUMN actual_value REAL")
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
c.execute("ALTER TABLE specialist_reports ADD COLUMN surprise_score REAL")
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
c.execute("ALTER TABLE specialist_reports ADD COLUMN text_sentiment_score REAL")
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
c.execute("ALTER TABLE specialist_reports ADD COLUMN text_sentiment_label TEXT")
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
c.execute("ALTER TABLE specialist_reports ADD COLUMN text_sentiment_summary TEXT")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Pattern Lab — historical backtest runs
|
||||
c.execute("""CREATE TABLE IF NOT EXISTS backtest_lab_runs (
|
||||
id TEXT PRIMARY KEY,
|
||||
@@ -677,6 +703,36 @@ def init_db():
|
||||
PRIMARY KEY (report_id, asset_class)
|
||||
)""")
|
||||
|
||||
# ── COT & Forward Curve data ──────────────────────────────────────────────
|
||||
c.execute("""CREATE TABLE IF NOT EXISTS cot_data (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
market_name TEXT NOT NULL,
|
||||
commodity TEXT NOT NULL,
|
||||
asset_class TEXT NOT NULL,
|
||||
report_date TEXT NOT NULL,
|
||||
mm_long INTEGER DEFAULT 0,
|
||||
mm_short INTEGER DEFAULT 0,
|
||||
open_interest INTEGER DEFAULT 0,
|
||||
net_position INTEGER DEFAULT 0,
|
||||
net_pct_oi REAL DEFAULT 0.0,
|
||||
change_net INTEGER DEFAULT 0,
|
||||
fetched_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
UNIQUE(commodity, report_date)
|
||||
)""")
|
||||
|
||||
c.execute("""CREATE TABLE IF NOT EXISTS forward_curve_data (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
asset TEXT NOT NULL,
|
||||
asset_class TEXT NOT NULL,
|
||||
front_price REAL,
|
||||
far_price REAL,
|
||||
slope_pct REAL,
|
||||
structure TEXT NOT NULL DEFAULT 'unknown',
|
||||
months_spread INTEGER DEFAULT 3,
|
||||
fetched_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
UNIQUE(asset, DATE(fetched_at))
|
||||
)""")
|
||||
|
||||
# Seed desk defaults (idempotent)
|
||||
_desk_count = c.execute("SELECT COUNT(*) FROM asset_class_configs").fetchone()[0]
|
||||
if _desk_count == 0:
|
||||
@@ -4268,3 +4324,109 @@ def upsert_asset_class_config(
|
||||
return True
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
# ── COT Data ──────────────────────────────────────────────────────────────────
|
||||
|
||||
def save_cot_data(entries: List[Dict[str, Any]]) -> int:
|
||||
conn = get_conn()
|
||||
try:
|
||||
saved = 0
|
||||
for e in entries:
|
||||
conn.execute("""INSERT INTO cot_data
|
||||
(market_name, commodity, asset_class, report_date, mm_long, mm_short,
|
||||
open_interest, net_position, net_pct_oi, change_net)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?)
|
||||
ON CONFLICT(commodity, report_date) DO UPDATE SET
|
||||
mm_long=excluded.mm_long, mm_short=excluded.mm_short,
|
||||
open_interest=excluded.open_interest, net_position=excluded.net_position,
|
||||
net_pct_oi=excluded.net_pct_oi, change_net=excluded.change_net,
|
||||
fetched_at=datetime('now')""",
|
||||
(e["market_name"], e["commodity"], e["asset_class"], e["report_date"],
|
||||
e.get("mm_long", 0), e.get("mm_short", 0), e.get("open_interest", 0),
|
||||
e.get("net_position", 0), e.get("net_pct_oi", 0.0), e.get("change_net", 0)))
|
||||
saved += 1
|
||||
conn.commit()
|
||||
return saved
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
def get_latest_cot_data() -> List[Dict[str, Any]]:
|
||||
conn = get_conn()
|
||||
try:
|
||||
rows = conn.execute("""
|
||||
SELECT * FROM cot_data
|
||||
WHERE (commodity, report_date) IN (
|
||||
SELECT commodity, MAX(report_date) FROM cot_data GROUP BY commodity
|
||||
)
|
||||
ORDER BY asset_class, net_pct_oi DESC""").fetchall()
|
||||
return [dict(r) for r in rows]
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
# ── Forward Curve Data ────────────────────────────────────────────────────────
|
||||
|
||||
def save_forward_curves(entries: List[Dict[str, Any]]) -> int:
|
||||
conn = get_conn()
|
||||
try:
|
||||
saved = 0
|
||||
for e in entries:
|
||||
conn.execute("""INSERT INTO forward_curve_data
|
||||
(asset, asset_class, front_price, far_price, slope_pct, structure, months_spread)
|
||||
VALUES (?,?,?,?,?,?,?)
|
||||
ON CONFLICT(asset, DATE(fetched_at)) DO UPDATE SET
|
||||
front_price=excluded.front_price, far_price=excluded.far_price,
|
||||
slope_pct=excluded.slope_pct, structure=excluded.structure,
|
||||
fetched_at=datetime('now')""",
|
||||
(e["asset"], e["asset_class"], e.get("front_price"), e.get("far_price"),
|
||||
e.get("slope_pct"), e.get("structure", "unknown"), e.get("months_spread", 3)))
|
||||
saved += 1
|
||||
conn.commit()
|
||||
return saved
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
def get_latest_forward_curves() -> List[Dict[str, Any]]:
|
||||
conn = get_conn()
|
||||
try:
|
||||
rows = conn.execute("""
|
||||
SELECT * FROM forward_curve_data
|
||||
WHERE (asset, DATE(fetched_at)) IN (
|
||||
SELECT asset, MAX(DATE(fetched_at)) FROM forward_curve_data GROUP BY asset
|
||||
)
|
||||
ORDER BY asset_class, asset""").fetchall()
|
||||
return [dict(r) for r in rows]
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
# ── Surprise Index ────────────────────────────────────────────────────────────
|
||||
|
||||
def update_report_result(
|
||||
report_id: str,
|
||||
actual_value: Optional[float],
|
||||
consensus_estimate: Optional[float],
|
||||
text_sentiment_score: Optional[float] = None,
|
||||
text_sentiment_label: Optional[str] = None,
|
||||
text_sentiment_summary: Optional[str] = None,
|
||||
) -> bool:
|
||||
conn = get_conn()
|
||||
try:
|
||||
surprise = None
|
||||
if actual_value is not None and consensus_estimate is not None:
|
||||
surprise = round(actual_value - consensus_estimate, 4)
|
||||
conn.execute("""UPDATE specialist_reports SET
|
||||
actual_value=?, consensus_estimate=?, surprise_score=?,
|
||||
text_sentiment_score=?, text_sentiment_label=?, text_sentiment_summary=?,
|
||||
updated_at=datetime('now')
|
||||
WHERE id=?""",
|
||||
(actual_value, consensus_estimate, surprise,
|
||||
text_sentiment_score, text_sentiment_label, text_sentiment_summary,
|
||||
report_id))
|
||||
conn.commit()
|
||||
return True
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
95
backend/services/forward_curve.py
Normal file
95
backend/services/forward_curve.py
Normal file
@@ -0,0 +1,95 @@
|
||||
"""
|
||||
Forward Curve Service — contango / backwardation per commodity.
|
||||
Uses yfinance to compare front-month vs near-dated futures contracts.
|
||||
"""
|
||||
import logging
|
||||
from datetime import datetime, timedelta
|
||||
from typing import List, Dict, Any, Optional
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Month codes for futures tickers (standard CME/NYMEX/CBOT convention)
|
||||
_MONTH_CODES = {1:'F',2:'G',3:'H',4:'J',5:'K',6:'M',7:'N',8:'Q',9:'U',10:'V',11:'X',12:'Z'}
|
||||
|
||||
# (base_ticker, label, asset_class, months_spread)
|
||||
_CURVE_SPECS = [
|
||||
("CL", "WTI Crude", "energy", 3),
|
||||
("NG", "Natural Gas", "energy", 3),
|
||||
("GC", "Gold", "metals", 3),
|
||||
("SI", "Silver", "metals", 3),
|
||||
("HG", "Copper", "metals", 3),
|
||||
("ZC", "Corn", "agri", 3),
|
||||
("ZW", "Wheat", "agri", 3),
|
||||
("ZS", "Soybeans", "agri", 3),
|
||||
]
|
||||
|
||||
|
||||
def _ticker_for_month(base: str, months_ahead: int) -> str:
|
||||
target = datetime.now() + timedelta(days=months_ahead * 30)
|
||||
code = _MONTH_CODES[target.month]
|
||||
year = str(target.year)[-2:]
|
||||
return f"{base}{code}{year}"
|
||||
|
||||
|
||||
def _get_price(ticker: str) -> Optional[float]:
|
||||
try:
|
||||
import yfinance as yf
|
||||
data = yf.download(ticker, period="3d", progress=False, auto_adjust=True)
|
||||
if data.empty:
|
||||
return None
|
||||
close = data["Close"].dropna()
|
||||
if close.empty:
|
||||
return None
|
||||
val = float(close.iloc[-1])
|
||||
# Handle multi-index DataFrames
|
||||
if hasattr(val, '__iter__'):
|
||||
val = list(val)[0]
|
||||
return round(val, 4) if val and val > 0 else None
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def fetch_forward_curves() -> List[Dict[str, Any]]:
|
||||
"""Fetch front-month and far-month prices to compute curve slope."""
|
||||
results = []
|
||||
for base, label, ac, spread in _CURVE_SPECS:
|
||||
front_ticker = f"{base}=F"
|
||||
front_price = _get_price(front_ticker)
|
||||
if not front_price:
|
||||
logger.warning(f"Forward curve: no front-month price for {label} ({front_ticker})")
|
||||
continue
|
||||
|
||||
# Try M+spread, M+spread-1, M+spread+1 (some contract months are illiquid)
|
||||
far_price = None
|
||||
for offset in [0, -1, 1, -2, 2]:
|
||||
far_ticker = _ticker_for_month(base, spread + offset)
|
||||
far_price = _get_price(far_ticker)
|
||||
if far_price:
|
||||
break
|
||||
|
||||
slope_pct = None
|
||||
structure = "unknown"
|
||||
if far_price:
|
||||
slope_pct = round((far_price - front_price) / front_price * 100, 2)
|
||||
if slope_pct > 0.15:
|
||||
structure = "contango"
|
||||
elif slope_pct < -0.15:
|
||||
structure = "backwardation"
|
||||
else:
|
||||
structure = "flat"
|
||||
|
||||
results.append({
|
||||
"asset": label,
|
||||
"asset_class": ac,
|
||||
"front_price": round(front_price, 2),
|
||||
"far_price": round(far_price, 2) if far_price else None,
|
||||
"slope_pct": slope_pct,
|
||||
"structure": structure,
|
||||
"months_spread": spread,
|
||||
})
|
||||
logger.info(
|
||||
f"Forward curve {label}: front={front_price:.2f} "
|
||||
f"far={far_price} slope={slope_pct}% -> {structure}"
|
||||
)
|
||||
|
||||
return results
|
||||
@@ -1252,3 +1252,107 @@ export const useUnlinkReportDesk = () => {
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// ── COT / Forward Curves / Surprise Index / Hawk-Dove — new types ─────────────
|
||||
|
||||
export interface CotEntry {
|
||||
id: number
|
||||
market_name: string
|
||||
commodity: string
|
||||
asset_class: string
|
||||
report_date: string
|
||||
mm_long: number
|
||||
mm_short: number
|
||||
open_interest: number
|
||||
net_position: number
|
||||
net_pct_oi: number
|
||||
change_net: number
|
||||
fetched_at: string
|
||||
}
|
||||
|
||||
export interface ForwardCurveEntry {
|
||||
id: number
|
||||
asset: string
|
||||
asset_class: string
|
||||
front_price: number | null
|
||||
far_price: number | null
|
||||
slope_pct: number | null
|
||||
structure: 'contango' | 'backwardation' | 'flat' | 'unknown'
|
||||
months_spread: number
|
||||
fetched_at: string
|
||||
}
|
||||
|
||||
export interface TextSentimentResult {
|
||||
score: number
|
||||
label: string
|
||||
summary: string
|
||||
key_phrases: string[]
|
||||
confidence: 'high' | 'medium' | 'low'
|
||||
}
|
||||
|
||||
export interface ReportResultUpdate {
|
||||
actual_value?: number | null
|
||||
consensus_estimate?: number | null
|
||||
text_sentiment_score?: number | null
|
||||
text_sentiment_label?: string | null
|
||||
text_sentiment_summary?: string | null
|
||||
}
|
||||
|
||||
// ── COT Data ──────────────────────────────────────────────────────────────────
|
||||
|
||||
export function useCotData() {
|
||||
return useQuery<CotEntry[]>({
|
||||
queryKey: ['specialist-desks', 'cot'],
|
||||
queryFn: () => api.get('/specialist-desks/cot').then(r => r.data),
|
||||
staleTime: 1000 * 60 * 60, // 1h
|
||||
})
|
||||
}
|
||||
|
||||
export function useFetchCot() {
|
||||
const qc = useQueryClient()
|
||||
return useMutation({
|
||||
mutationFn: () => api.post('/specialist-desks/cot/fetch').then(r => r.data),
|
||||
onSuccess: () => qc.invalidateQueries({ queryKey: ['specialist-desks', 'cot'] }),
|
||||
})
|
||||
}
|
||||
|
||||
// ── Forward Curves ────────────────────────────────────────────────────────────
|
||||
|
||||
export function useForwardCurves() {
|
||||
return useQuery<ForwardCurveEntry[]>({
|
||||
queryKey: ['specialist-desks', 'forward-curves'],
|
||||
queryFn: () => api.get('/specialist-desks/forward-curves').then(r => r.data),
|
||||
staleTime: 1000 * 60 * 60,
|
||||
})
|
||||
}
|
||||
|
||||
export function useFetchForwardCurves() {
|
||||
const qc = useQueryClient()
|
||||
return useMutation({
|
||||
mutationFn: () => api.post('/specialist-desks/forward-curves/fetch').then(r => r.data),
|
||||
onSuccess: () => qc.invalidateQueries({ queryKey: ['specialist-desks', 'forward-curves'] }),
|
||||
})
|
||||
}
|
||||
|
||||
// ── Surprise Index ────────────────────────────────────────────────────────────
|
||||
|
||||
export function useUpdateReportResult() {
|
||||
const qc = useQueryClient()
|
||||
return useMutation({
|
||||
mutationFn: ({ id, body }: { id: string; body: ReportResultUpdate }) =>
|
||||
api.put(`/specialist-desks/reports/${id}/result`, body).then(r => r.data),
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: ['specialist-reports'] })
|
||||
qc.invalidateQueries({ queryKey: ['desk-configs'] })
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// ── Hawk/Dove Text Scorer ─────────────────────────────────────────────────────
|
||||
|
||||
export function useScoreText() {
|
||||
return useMutation({
|
||||
mutationFn: (body: { text: string; report_name: string; desk: string }) =>
|
||||
api.post('/specialist-desks/score-text', body).then(r => r.data) as Promise<TextSentimentResult>,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -4,12 +4,17 @@ import {
|
||||
useUpdateDeskConfig, useCreateSpecialistReport,
|
||||
useUpdateSpecialistReport, useDeleteSpecialistReport,
|
||||
useLinkReportDesk, useUnlinkReportDesk,
|
||||
useCotData, useFetchCot, useForwardCurves, useFetchForwardCurves,
|
||||
useUpdateReportResult, useScoreText,
|
||||
DeskConfig, SpecialistReport, MacroSensitivity,
|
||||
TextSentimentResult, ReportResultUpdate,
|
||||
} from '../hooks/useApi'
|
||||
import {
|
||||
Users, FileText, Plus, Trash2, Save, X, Edit2,
|
||||
ChevronRight, Star, Link, ExternalLink, RefreshCw,
|
||||
AlertCircle, Calendar, BookOpen,
|
||||
TrendingUp, TrendingDown, Minus, BarChart2, FlaskConical,
|
||||
ArrowUp, ArrowDown, Activity,
|
||||
} from 'lucide-react'
|
||||
import clsx from 'clsx'
|
||||
|
||||
@@ -230,6 +235,79 @@ function MacroSensitivityEditor({
|
||||
)
|
||||
}
|
||||
|
||||
// ── Surprise Index inline component ───────────────────────────────────────────
|
||||
|
||||
function SurpriseInput({ report, onSave }: {
|
||||
report: any
|
||||
onSave: (data: ReportResultUpdate) => void
|
||||
}) {
|
||||
const [consensus, setConsensus] = useState<string>(report.consensus_estimate?.toString() ?? '')
|
||||
const [actual, setActual] = useState<string>(report.actual_value?.toString() ?? '')
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
const surprise = actual !== '' && consensus !== ''
|
||||
? parseFloat(actual) - parseFloat(consensus)
|
||||
: null
|
||||
|
||||
return (
|
||||
<div className="mt-1">
|
||||
{!open && (
|
||||
<button onClick={() => setOpen(true)} className="text-[10px] text-slate-600 hover:text-slate-400 transition-colors flex items-center gap-1">
|
||||
<FlaskConical className="w-2.5 h-2.5" />
|
||||
{report.surprise_score != null
|
||||
? <span className={clsx('font-mono', report.surprise_score > 0 ? 'text-emerald-500' : 'text-red-500')}>
|
||||
surprise: {report.surprise_score >= 0 ? '+' : ''}{report.surprise_score}
|
||||
</span>
|
||||
: 'Add consensus / actual'}
|
||||
</button>
|
||||
)}
|
||||
{open && (
|
||||
<div className="mt-2 space-y-1.5 bg-dark-700/60 rounded p-2 border border-slate-700/40">
|
||||
<div className="flex gap-2">
|
||||
<div className="flex-1">
|
||||
<div className="text-[9px] text-slate-600 mb-0.5">Consensus</div>
|
||||
<input value={consensus} onChange={e => setConsensus(e.target.value)} type="number" step="any"
|
||||
placeholder="0.0"
|
||||
className="w-full px-2 py-1 text-[10px] bg-dark-800 border border-slate-600/50 rounded text-slate-300 focus:outline-none focus:border-violet-500 font-mono" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="text-[9px] text-slate-600 mb-0.5">Actual</div>
|
||||
<input value={actual} onChange={e => setActual(e.target.value)} type="number" step="any"
|
||||
placeholder="0.0"
|
||||
className="w-full px-2 py-1 text-[10px] bg-dark-800 border border-slate-600/50 rounded text-slate-300 focus:outline-none focus:border-violet-500 font-mono" />
|
||||
</div>
|
||||
{surprise !== null && (
|
||||
<div className="flex-1">
|
||||
<div className="text-[9px] text-slate-600 mb-0.5">Surprise</div>
|
||||
<div className={clsx('px-2 py-1 text-[10px] font-mono font-bold rounded border',
|
||||
surprise > 0 ? 'text-emerald-400 border-emerald-800/40 bg-emerald-900/20' :
|
||||
surprise < 0 ? 'text-red-400 border-red-800/40 bg-red-900/20' :
|
||||
'text-slate-400 border-slate-700/40')}>
|
||||
{surprise >= 0 ? '+' : ''}{surprise.toFixed(3)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex gap-2 justify-end">
|
||||
<button onClick={() => setOpen(false)} className="text-[10px] text-slate-600 hover:text-slate-400 transition-colors">Cancel</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
onSave({
|
||||
consensus_estimate: consensus !== '' ? parseFloat(consensus) : null,
|
||||
actual_value: actual !== '' ? parseFloat(actual) : null,
|
||||
})
|
||||
setOpen(false)
|
||||
}}
|
||||
className="px-2 py-0.5 bg-violet-800 hover:bg-violet-700 text-white text-[10px] rounded transition-colors">
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ── Main page ──────────────────────────────────────────────────────────────────
|
||||
|
||||
export default function SpecialistDesks() {
|
||||
@@ -242,12 +320,22 @@ export default function SpecialistDesks() {
|
||||
const { mutate: linkReport } = useLinkReportDesk()
|
||||
const { mutate: unlinkReport } = useUnlinkReportDesk()
|
||||
|
||||
const { data: cotData = [], isLoading: cotLoading } = useCotData()
|
||||
const { data: curveData = [], isLoading: curvesLoading } = useForwardCurves()
|
||||
const { mutate: fetchCot, isPending: fetchingCot } = useFetchCot()
|
||||
const { mutate: fetchCurves, isPending: fetchingCurves } = useFetchForwardCurves()
|
||||
const { mutateAsync: updateReportResult } = useUpdateReportResult()
|
||||
const { mutateAsync: scoreText, isPending: scoringText } = useScoreText()
|
||||
const [scoreTextInput, setScoreTextInput] = useState('')
|
||||
const [scoreTextResult, setScoreTextResult] = useState<TextSentimentResult | null>(null)
|
||||
const [scoreReportName, setScoreReportName] = useState('')
|
||||
|
||||
const orderedDesks = DESK_ORDER
|
||||
.map(ac => desks.find(d => d.asset_class === ac))
|
||||
.filter(Boolean) as DeskConfig[]
|
||||
|
||||
const [activeDeskAc, setActiveDeskAc] = useState<string>(DESK_ORDER[0])
|
||||
const [activeTab, setActiveTab] = useState<'config' | 'reports' | 'all-reports'>('config')
|
||||
const [activeTab, setActiveTab] = useState<'config' | 'reports' | 'all-reports' | 'cot' | 'curves'>('config')
|
||||
|
||||
// Desk edit state
|
||||
const [editFund, setEditFund] = useState('')
|
||||
@@ -362,10 +450,225 @@ export default function SpecialistDesks() {
|
||||
<FileText className="w-3.5 h-3.5" />
|
||||
All Reports ({allReports.length})
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('cot')}
|
||||
className={clsx(
|
||||
'px-4 py-3 flex items-center gap-2 border-t border-slate-700/40 transition-colors text-xs',
|
||||
activeTab === 'cot'
|
||||
? 'bg-slate-700/50 text-slate-200'
|
||||
: 'text-slate-500 hover:text-slate-300'
|
||||
)}>
|
||||
<BarChart2 className="w-3.5 h-3.5" />
|
||||
COT Positioning
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('curves')}
|
||||
className={clsx(
|
||||
'px-4 py-3 flex items-center gap-2 border-t border-slate-700/40 transition-colors text-xs',
|
||||
activeTab === 'curves'
|
||||
? 'bg-slate-700/50 text-slate-200'
|
||||
: 'text-slate-500 hover:text-slate-300'
|
||||
)}>
|
||||
<Activity className="w-3.5 h-3.5" />
|
||||
Forward Curves
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* ── Right: desk detail / all reports ──────────────────────────────── */}
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
|
||||
{/* ══ COT VIEW ════════════════════════════════════════════ */}
|
||||
{activeTab === 'cot' && (
|
||||
<div className="p-6 space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-lg font-bold text-slate-100 flex items-center gap-2">
|
||||
<BarChart2 className="w-5 h-5 text-cyan-400" />
|
||||
COT Positioning
|
||||
</h1>
|
||||
<p className="text-xs text-slate-500 mt-0.5">CFTC Commitment of Traders — Money Manager net positions (% of Open Interest)</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => fetchCot()}
|
||||
disabled={fetchingCot}
|
||||
className="flex items-center gap-1.5 bg-cyan-800 hover:bg-cyan-700 disabled:opacity-40 text-white px-3 py-2 rounded text-xs font-semibold transition-colors">
|
||||
<RefreshCw className={clsx('w-3.5 h-3.5', fetchingCot && 'animate-spin')} />
|
||||
{fetchingCot ? 'Fetching…' : 'Refresh CFTC'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{cotLoading ? (
|
||||
<div className="text-slate-500 text-sm flex items-center gap-2">
|
||||
<RefreshCw className="w-4 h-4 animate-spin" /> Loading…
|
||||
</div>
|
||||
) : cotData.length === 0 ? (
|
||||
<div className="text-slate-500 text-sm p-8 text-center border border-dashed border-slate-700 rounded-lg">
|
||||
No COT data yet. Click "Refresh CFTC" to fetch the latest weekly report.
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{(['energy', 'metals', 'agri', 'forex', 'bonds'] as const).map(ac => {
|
||||
const entries = cotData.filter(c => c.asset_class === ac)
|
||||
if (!entries.length) return null
|
||||
const labels: Record<string, string> = { energy: '⚡ Energy', metals: '🥇 Metals', agri: '🌾 Agri & Softs', forex: '💱 Forex', bonds: '📉 Bonds' }
|
||||
const reportDate = entries[0]?.report_date
|
||||
return (
|
||||
<div key={ac} className="bg-dark-800 border border-slate-700/40 rounded-lg overflow-hidden">
|
||||
<div className="flex items-center justify-between px-4 py-2.5 border-b border-slate-700/40 bg-dark-700/50">
|
||||
<span className="text-xs font-bold text-slate-200">{labels[ac] || ac}</span>
|
||||
{reportDate && <span className="text-[10px] text-slate-500 font-mono">COT week of {reportDate}</span>}
|
||||
</div>
|
||||
<div className="divide-y divide-slate-700/30">
|
||||
{entries.map(entry => {
|
||||
const pct = entry.net_pct_oi
|
||||
const isLong = pct > 5
|
||||
const isShort = pct < -5
|
||||
const barWidth = Math.min(Math.abs(pct), 50) // cap at 50%
|
||||
const chg = entry.change_net
|
||||
return (
|
||||
<div key={entry.id} className="px-4 py-3 flex items-center gap-4">
|
||||
<div className="w-28 flex-shrink-0 text-xs font-medium text-slate-200">{entry.commodity}</div>
|
||||
{/* Net position bar */}
|
||||
<div className="flex-1 flex items-center gap-2">
|
||||
<div className="w-24 h-2 bg-slate-700/50 rounded-full overflow-hidden relative">
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<div className="w-px h-full bg-slate-600" />
|
||||
</div>
|
||||
{pct > 0 ? (
|
||||
<div className="absolute left-1/2 top-0 h-full bg-emerald-500 rounded-r"
|
||||
style={{ width: `${barWidth}%` }} />
|
||||
) : (
|
||||
<div className="absolute right-1/2 top-0 h-full bg-red-500 rounded-l"
|
||||
style={{ width: `${barWidth}%` }} />
|
||||
)}
|
||||
</div>
|
||||
<span className={clsx(
|
||||
'text-xs font-mono font-bold min-w-[50px]',
|
||||
isLong ? 'text-emerald-400' : isShort ? 'text-red-400' : 'text-slate-400'
|
||||
)}>
|
||||
{pct >= 0 ? '+' : ''}{pct.toFixed(1)}%
|
||||
</span>
|
||||
<span className={clsx('text-[10px] px-1.5 py-0.5 rounded font-bold',
|
||||
isLong ? 'bg-emerald-900/50 text-emerald-300' : isShort ? 'bg-red-900/50 text-red-300' : 'bg-slate-700/50 text-slate-400'
|
||||
)}>
|
||||
{isLong ? 'LONG' : isShort ? 'SHORT' : 'NEUTRAL'}
|
||||
</span>
|
||||
</div>
|
||||
{/* Weekly change */}
|
||||
{chg !== 0 && (
|
||||
<div className={clsx('flex items-center gap-0.5 text-[10px] font-mono',
|
||||
chg > 0 ? 'text-emerald-400' : 'text-red-400')}>
|
||||
{chg > 0 ? <ArrowUp className="w-2.5 h-2.5" /> : <ArrowDown className="w-2.5 h-2.5" />}
|
||||
{Math.abs(chg).toLocaleString()}
|
||||
</div>
|
||||
)}
|
||||
{/* Net position (contracts) */}
|
||||
<div className="text-[10px] font-mono text-slate-500 text-right w-28">
|
||||
{entry.net_position.toLocaleString()} net
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
<p className="text-[10px] text-slate-600 text-center">
|
||||
Source: CFTC Disaggregated Futures (commodities) + Legacy Financial (forex/bonds) — published weekly on Fridays
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ══ FORWARD CURVES VIEW ══════════════════════════════════ */}
|
||||
{activeTab === 'curves' && (
|
||||
<div className="p-6 space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-lg font-bold text-slate-100 flex items-center gap-2">
|
||||
<Activity className="w-5 h-5 text-amber-400" />
|
||||
Forward Curves
|
||||
</h1>
|
||||
<p className="text-xs text-slate-500 mt-0.5">Contango / Backwardation — spot vs +3M futures slope (yfinance)</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => fetchCurves()}
|
||||
disabled={fetchingCurves}
|
||||
className="flex items-center gap-1.5 bg-amber-800 hover:bg-amber-700 disabled:opacity-40 text-white px-3 py-2 rounded text-xs font-semibold transition-colors">
|
||||
<RefreshCw className={clsx('w-3.5 h-3.5', fetchingCurves && 'animate-spin')} />
|
||||
{fetchingCurves ? 'Fetching…' : 'Refresh Curves'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{curvesLoading ? (
|
||||
<div className="text-slate-500 text-sm flex items-center gap-2">
|
||||
<RefreshCw className="w-4 h-4 animate-spin" /> Loading…
|
||||
</div>
|
||||
) : curveData.length === 0 ? (
|
||||
<div className="text-slate-500 text-sm p-8 text-center border border-dashed border-slate-700 rounded-lg">
|
||||
No forward curve data yet. Click "Refresh Curves" to fetch.
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{(['energy', 'metals', 'agri'] as const).map(ac => {
|
||||
const entries = curveData.filter(c => c.asset_class === ac)
|
||||
if (!entries.length) return null
|
||||
const labels: Record<string, string> = { energy: '⚡ Energy', metals: '🥇 Metals', agri: '🌾 Agri & Softs' }
|
||||
return (
|
||||
<div key={ac} className="bg-dark-800 border border-slate-700/40 rounded-lg overflow-hidden">
|
||||
<div className="px-4 py-2.5 border-b border-slate-700/40 bg-dark-700/50">
|
||||
<span className="text-xs font-bold text-slate-200">{labels[ac]}</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-0 divide-y divide-slate-700/30">
|
||||
{entries.map(e => {
|
||||
const isContango = e.structure === 'contango'
|
||||
const isBackward = e.structure === 'backwardation'
|
||||
const slope = e.slope_pct
|
||||
return (
|
||||
<div key={e.id} className="px-4 py-3 flex items-center gap-3 border-r border-slate-700/30 last:border-r-0">
|
||||
<div className="flex-1">
|
||||
<div className="text-xs font-medium text-slate-200">{e.asset}</div>
|
||||
<div className="text-[10px] text-slate-500 font-mono mt-0.5">
|
||||
Front: {e.front_price?.toFixed(2) ?? '—'}
|
||||
{e.far_price && ` | +3M: ${e.far_price.toFixed(2)}`}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right flex-shrink-0">
|
||||
<div className={clsx('flex items-center gap-1 text-xs font-bold',
|
||||
isContango ? 'text-sky-400' : isBackward ? 'text-orange-400' : 'text-slate-400'
|
||||
)}>
|
||||
{isContango ? <TrendingUp className="w-3 h-3" />
|
||||
: isBackward ? <TrendingDown className="w-3 h-3" />
|
||||
: <Minus className="w-3 h-3" />}
|
||||
{e.structure.toUpperCase()}
|
||||
</div>
|
||||
{slope !== null && (
|
||||
<div className={clsx('text-[10px] font-mono mt-0.5',
|
||||
slope > 0 ? 'text-sky-500' : slope < 0 ? 'text-orange-500' : 'text-slate-500'
|
||||
)}>
|
||||
{slope >= 0 ? '+' : ''}{slope.toFixed(2)}%
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
<div className="bg-dark-800 border border-slate-700/40 rounded-lg p-4">
|
||||
<div className="text-[10px] text-slate-500 space-y-1">
|
||||
<div><span className="text-sky-400 font-bold">CONTANGO</span> = futures prix > spot → marché normal (coût de stockage) → <em>pénalise les positions longues au rollover</em></div>
|
||||
<div><span className="text-orange-400 font-bold">BACKWARDATION</span> = futures prix < spot → pénurie physique immédiate → <em>profite aux positions longues au rollover</em></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'all-reports' ? (
|
||||
/* ══ ALL REPORTS VIEW ════════════════════════════════════════════ */
|
||||
<div className="p-6 space-y-4">
|
||||
@@ -560,6 +863,87 @@ export default function SpecialistDesks() {
|
||||
className="w-full px-3 py-2 text-xs bg-dark-700 border border-slate-600/50 rounded text-slate-300 focus:outline-none focus:border-violet-500 resize-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* ── Hawk/Dove Text Scorer (Forex & Bonds desks only) ── */}
|
||||
{(activeDeskAc === 'forex' || activeDeskAc === 'bonds') && (
|
||||
<div className="mt-6 border-t border-slate-700/40 pt-5">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<FlaskConical className="w-4 h-4 text-violet-400" />
|
||||
<span className="text-sm font-semibold text-slate-200">Hawk/Dove Text Scorer</span>
|
||||
<span className="text-[10px] text-slate-500">— Paste a central bank statement to get an IA sentiment score</span>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<input
|
||||
value={scoreReportName}
|
||||
onChange={e => setScoreReportName(e.target.value)}
|
||||
placeholder="Report name (e.g. FOMC Minutes June 2026)"
|
||||
className="w-full px-3 py-2 text-xs bg-dark-700 border border-slate-600/50 rounded text-slate-200 focus:outline-none focus:border-violet-500"
|
||||
/>
|
||||
<textarea
|
||||
value={scoreTextInput}
|
||||
onChange={e => setScoreTextInput(e.target.value)}
|
||||
rows={5}
|
||||
placeholder="Paste the FOMC minutes, ECB statement, BoJ press release… (up to 3000 chars used)"
|
||||
className="w-full px-3 py-2 text-xs bg-dark-700 border border-slate-600/50 rounded text-slate-300 focus:outline-none focus:border-violet-500 resize-none font-mono"
|
||||
/>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={async () => {
|
||||
if (!scoreTextInput.trim()) return
|
||||
const result = await scoreText({ text: scoreTextInput, report_name: scoreReportName, desk: activeDeskAc })
|
||||
setScoreTextResult(result)
|
||||
}}
|
||||
disabled={scoringText || !scoreTextInput.trim()}
|
||||
className="flex items-center gap-1.5 px-4 py-2 bg-violet-700 hover:bg-violet-600 disabled:opacity-40 text-white text-xs font-semibold rounded transition-colors">
|
||||
<FlaskConical className="w-3.5 h-3.5" />
|
||||
{scoringText ? 'Analyzing…' : 'Score Text'}
|
||||
</button>
|
||||
{scoreTextResult && (
|
||||
<button onClick={() => { setScoreTextResult(null); setScoreTextInput(''); setScoreReportName('') }}
|
||||
className="text-xs text-slate-500 hover:text-slate-300 transition-colors">Clear</button>
|
||||
)}
|
||||
</div>
|
||||
{scoreTextResult && (
|
||||
<div className={clsx('rounded-lg border p-4 space-y-3',
|
||||
scoreTextResult.score > 0.3 ? 'border-red-700/40 bg-red-900/10' :
|
||||
scoreTextResult.score < -0.3 ? 'border-sky-700/40 bg-sky-900/10' :
|
||||
'border-slate-700/40 bg-dark-700/30'
|
||||
)}>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className={clsx('text-sm font-bold px-2 py-0.5 rounded',
|
||||
scoreTextResult.score > 0.3 ? 'bg-red-800/50 text-red-200' :
|
||||
scoreTextResult.score < -0.3 ? 'bg-sky-800/50 text-sky-200' :
|
||||
'bg-slate-700/50 text-slate-300'
|
||||
)}>
|
||||
{scoreTextResult.label.replace(/_/g, ' ').toUpperCase()}
|
||||
</span>
|
||||
<span className="text-[10px] text-slate-500">confidence: {scoreTextResult.confidence}</span>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div className="text-lg font-mono font-bold" style={{
|
||||
color: scoreTextResult.score > 0 ? '#f87171' : scoreTextResult.score < 0 ? '#38bdf8' : '#94a3b8'
|
||||
}}>
|
||||
{scoreTextResult.score >= 0 ? '+' : ''}{scoreTextResult.score.toFixed(2)}
|
||||
</div>
|
||||
<div className="text-[10px] text-slate-500">score (−1=dovish, +1=hawkish)</div>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-xs text-slate-300 leading-relaxed">{scoreTextResult.summary}</p>
|
||||
{scoreTextResult.key_phrases?.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{scoreTextResult.key_phrases.map((phrase, i) => (
|
||||
<span key={i} className="px-2 py-0.5 bg-slate-700/50 border border-slate-600/40 rounded text-[10px] text-slate-400 italic">
|
||||
"{phrase}"
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -607,6 +991,10 @@ export default function SpecialistDesks() {
|
||||
)}
|
||||
</div>
|
||||
{r.notes && <p className="text-[10px] text-slate-600 mt-0.5 italic">{r.notes}</p>}
|
||||
<SurpriseInput
|
||||
report={r}
|
||||
onSave={(data) => updateReportResult({ id: r.id, body: data })}
|
||||
/>
|
||||
</div>
|
||||
<Stars n={r.importance} />
|
||||
<div className="flex items-center gap-1.5">
|
||||
|
||||
Reference in New Issue
Block a user