feat: Phase 1 — IV Rank, Term Structure, Skew, Options Flow (Sprint 1.1/1.2/1.3)

Backend:
- iv_engine.py: ATM IV, term structure (30/60/90/180j), put/call skew,
  options flow (P/C OI ratio, unusual strikes, gamma bias), proxy map for futures→ETFs
- database.py: iv_history table + save_iv_snapshot, get_iv_rank_percentile, get_iv_history
- routers/options_vol.py: /api/options-vol/ endpoints (snapshot, batch, watchlist, history)
- auto_cycle.py: inject IV context string into scoring prompt (step 3.5)
- ai_analyzer.py: score_patterns_with_context accepts iv_context param
- main.py: register options_vol router

Frontend:
- pages/OptionsLab.tsx: full IV dashboard (watchlist by IVR, term structure, skew, flow, sparkline)
- pages/JournalDeBord.tsx: IvRankCell component + IV Rank column per trade
- hooks/useApi.ts: useIvSnapshot, useIvWatchlist, useIvBatch, useIvHistory, useIvForTrade

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
OpenSquared
2026-06-17 16:29:33 +02:00
parent 07c1a74704
commit 9a6b6f70b1
12 changed files with 3886 additions and 329 deletions

View File

@@ -0,0 +1,971 @@
<!DOCTYPE html>
<html lang="fr"><head>
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>GeoOptions Intelligence — Documentation Système</title>
<style>
/* ── Base ── */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: 'Georgia', 'Times New Roman', serif;
font-size: 11pt;
line-height: 1.7;
color: #1a1a2e;
background: #fff;
max-width: 900px;
margin: 0 auto;
padding: 40px 48px;
}
/* ── Typography ── */
h1 { font-size: 24pt; font-weight: 700; color: #0f2d6b; margin-bottom: 6px; letter-spacing: -0.5px; }
h2 { font-size: 15pt; font-weight: 700; color: #0f2d6b; margin: 36px 0 12px; border-bottom: 2.5px solid #0f2d6b; padding-bottom: 6px; }
h3 { font-size: 12pt; font-weight: 700; color: #1a3a7c; margin: 24px 0 8px; }
h4 { font-size: 10.5pt; font-weight: 700; color: #2d5a9e; margin: 16px 0 6px; }
p { margin-bottom: 10px; }
/* ── Cover ── */
.cover {
text-align: center;
padding: 60px 0 48px;
border-bottom: 3px solid #0f2d6b;
margin-bottom: 48px;
}
.cover .subtitle { font-size: 13pt; color: #4a6fa5; margin-top: 8px; font-style: italic; }
.cover .meta { font-size: 9.5pt; color: #7a8aaa; margin-top: 24px; font-family: 'Courier New', monospace; }
.cover .logo { font-size: 36pt; margin-bottom: 16px; }
/* ── Callout boxes ── */
.callout {
border-left: 4px solid;
padding: 12px 16px;
margin: 18px 0;
border-radius: 0 6px 6px 0;
}
.callout.blue { border-color: #2d6be4; background: #eef4ff; }
.callout.green { border-color: #16a34a; background: #f0fdf4; }
.callout.amber { border-color: #d97706; background: #fffbeb; }
.callout.red { border-color: #dc2626; background: #fff5f5; }
.callout .label { font-weight: 700; font-size: 9pt; text-transform: uppercase; letter-spacing: 1px; margin-bottom: 4px; }
.callout.blue .label { color: #2d6be4; }
.callout.green .label { color: #16a34a; }
.callout.amber .label { color: #d97706; }
.callout.red .label { color: #dc2626; }
/* ── Tables ── */
table { width: 100%; border-collapse: collapse; margin: 16px 0 24px; font-size: 10pt; }
th { background: #0f2d6b; color: #fff; padding: 9px 12px; text-align: left; font-size: 9pt; text-transform: uppercase; letter-spacing: 0.5px; }
td { padding: 8px 12px; border-bottom: 1px solid #e2e8f0; vertical-align: top; }
tr:nth-child(even) td { background: #f8fafc; }
tr:hover td { background: #eef2ff; }
/* ── Flow diagram ── */
.flow { display: flex; flex-direction: column; gap: 2px; margin: 20px 0; }
.flow-step {
display: flex;
align-items: flex-start;
gap: 14px;
padding: 12px 16px;
border-radius: 6px;
}
.flow-step.ai { background: #eef4ff; border: 1px solid #bfd1f5; }
.flow-step.data { background: #f0fdf4; border: 1px solid #bbf7d0; }
.flow-step.db { background: #fafafa; border: 1px solid #e2e8f0; }
.flow-step.bg { background: #fefce8; border: 1px solid #fde68a; }
.flow-num {
width: 26px; height: 26px; border-radius: 50%;
display: flex; align-items: center; justify-content: center;
font-size: 9pt; font-weight: 700; flex-shrink: 0; margin-top: 2px;
}
.flow-step.ai .flow-num { background: #2d6be4; color: #fff; }
.flow-step.data .flow-num { background: #16a34a; color: #fff; }
.flow-step.db .flow-num { background: #64748b; color: #fff; }
.flow-step.bg .flow-num { background: #d97706; color: #fff; }
.flow-title { font-weight: 700; font-size: 10.5pt; margin-bottom: 2px; }
.flow-step.ai .flow-title { color: #1e3a8a; }
.flow-step.data .flow-title { color: #14532d; }
.flow-step.db .flow-title { color: #334155; }
.flow-step.bg .flow-title { color: #92400e; }
.flow-desc { font-size: 9.5pt; color: #475569; }
.flow-arrow { text-align: center; color: #94a3b8; font-size: 14pt; line-height: 1; }
.badge {
display: inline-block;
padding: 1px 8px;
border-radius: 20px;
font-size: 8.5pt;
font-weight: 700;
font-family: 'Courier New', monospace;
}
.badge-ai { background: #dbeafe; color: #1e40af; }
.badge-mini { background: #ede9fe; color: #5b21b6; }
.badge-db { background: #f1f5f9; color: #475569; border: 1px solid #cbd5e1; }
.badge-bg { background: #fef9c3; color: #854d0e; }
ul { margin: 8px 0 12px 20px; }
li { margin-bottom: 4px; }
li::marker { color: #2d6be4; }
code {
font-family: 'Courier New', monospace;
font-size: 9pt;
background: #f1f5f9;
padding: 1px 5px;
border-radius: 3px;
color: #1e3a8a;
}
.section-intro { font-size: 10.5pt; color: #475569; font-style: italic; margin-bottom: 20px; }
/* ── Prompt anatomy cards ── */
.prompt-card {
border: 1.5px solid #bfd1f5;
border-radius: 8px;
margin: 20px 0;
overflow: hidden;
}
.prompt-card-header {
background: #0f2d6b;
color: #fff;
padding: 10px 16px;
font-weight: 700;
font-size: 10pt;
display: flex;
align-items: center;
justify-content: space-between;
}
.prompt-card-body { padding: 14px 16px; }
.prompt-section { margin-bottom: 12px; }
.prompt-section-title { font-weight: 700; font-size: 9pt; text-transform: uppercase; letter-spacing: 0.8px; color: #2d6be4; margin-bottom: 4px; }
.included { color: #15803d; }
.excluded { color: #dc2626; }
.check::before { content: "✓ "; font-weight: 700; }
.cross::before { content: "✗ "; font-weight: 700; }
/* ── Maturity viz ── */
.maturity-bar { display: flex; height: 24px; border-radius: 4px; overflow: hidden; margin: 12px 0 6px; }
.mat-seg { display: flex; align-items: center; justify-content: center; font-size: 8pt; font-weight: 700; color: #fff; }
.mat-trop-tot { background: #94a3b8; flex: 10; }
.mat-debut { background: #f59e0b; flex: 25; }
.mat-mature { background: #22c55e; flex: 40; }
.mat-fin { background: #3b82f6; flex: 25; }
.mat-legend { display: flex; gap: 16px; flex-wrap: wrap; font-size: 8.5pt; margin-top: 4px; }
.mat-legend span { display: flex; align-items: center; gap: 5px; }
.mat-dot { width: 10px; height: 10px; border-radius: 50%; display: inline-block; flex-shrink: 0; }
.page-break { page-break-before: always; }
hr { border: none; border-top: 1px solid #e2e8f0; margin: 32px 0; }
/* ── Print ── */
@media print {
body { padding: 20px 28px; font-size: 10pt; }
h1 { font-size: 20pt; }
h2 { font-size: 13pt; }
.cover { padding: 30px 0 30px; }
.callout, .prompt-card { break-inside: avoid; }
.flow-step { break-inside: avoid; }
a { text-decoration: none; color: inherit; }
@page { margin: 1.5cm 1.8cm; size: A4; }
}
</style>
</head>
<body>
<!-- ══════════════════════════════════════════════════════════════════════════
COVER
══════════════════════════════════════════════════════════════════════════ -->
<div class="cover">
<div class="logo"></div>
<h1>GeoOptions Intelligence</h1>
<div class="subtitle">Architecture du Système &amp; Logique des Prompts IA</div>
<div class="meta">
Version 2.0 &nbsp;·&nbsp; Juin 2026 &nbsp;·&nbsp; FastAPI + React + GPT-4o
</div>
</div>
<!-- ══════════════════════════════════════════════════════════════════════════
1. CONCEPT GÉNÉRAL
══════════════════════════════════════════════════════════════════════════ -->
<h2>1. Concept Général</h2>
<p class="section-intro">
GeoOptions Intelligence est un cockpit de trading d'options dont
l'intelligence centrale repose sur un cycle itératif : collecter les
signaux macro-géopolitiques, identifier les patterns d'opportunité,
scorer leur pertinence en temps réel, puis apprendre des performances
passées pour affiner les cycles suivants.
</p>
<h3>1.1 Philosophie fondamentale</h3>
<p>
Le système est construit autour d'une idée simple : <strong>les options sur dérivés géopolitiques suivent des patterns répétables</strong>.
Une escalade tarifaire US-Chine génère systématiquement de la
volatilité sur les semi-conducteurs. Un choc pétrolier bénéficie aux
stratégies long sur l'énergie. Le défi n'est pas de <em>créer</em> ces patterns, mais de les <em>détecter</em> assez tôt pour agir avant le consensus.
</p>
<p>
L'IA (GPT-4o) joue le rôle d'un <strong>stratège senior externe</strong>
qui lit les news, connaît les régimes macro, et enrichit la base de
patterns — mais ce sont des règles humaines explicites qui décident si
un trade est logué, si une analyse est valide, et si les leçons passées
sont fiables.
</p>
<div class="callout blue">
<div class="label">Principe directeur</div>
L'IA propose, les règles quantitatives filtrent, l'historique de performance valide. Le cycle répète et apprend.
</div>
<h3>1.2 Les trois mémoires du système</h3>
<table>
<tbody><tr>
<th>Mémoire</th>
<th>Contenu</th>
<th>Durée de vie</th>
<th>Usage</th>
</tr>
<tr>
<td><strong>Rapport Portfolio</strong></td>
<td>Analyse post-mortem des trades matures : gagnants, perdants, leçons clés</td>
<td>~730 jours (jusqu'au prochain rapport)</td>
<td>Injecté dans les prompts de suggestion et scoring du cycle suivant</td>
</tr>
<tr>
<td><strong>Super Contexte</strong></td>
<td>Synthèse évolutive : régimes, patterns récurrents, erreurs, corrélations macro</td>
<td>Permanent — s'enrichit à chaque cycle (max 1×/6h)</td>
<td>Contexte de fond pour tout le raisonnement IA</td>
</tr>
<tr>
<td><strong>Base de Connaissances (KB)</strong></td>
<td>Entrées structurées par catégorie (régimes, patterns, erreurs) avec score de confiance</td>
<td>Permanent — géré manuellement ou auto-ajouté par synthèse</td>
<td>Consulté lors de la synthèse du Super Contexte</td>
</tr>
</tbody></table>
<!-- ══════════════════════════════════════════════════════════════════════════
2. WORKFLOW D'UN CYCLE
══════════════════════════════════════════════════════════════════════════ -->
<div class="page-break"></div>
<h2>2. Workflow d'un Cycle Complet</h2>
<p class="section-intro">
Un cycle se déclenche automatiquement (toutes les N heures) ou
manuellement. La durée typique est de 2 à 5 minutes pour les étapes
synchrones, plus 1 à 3 minutes en background selon la disponibilité des
données.
</p>
<div class="flow">
<div class="flow-step db">
<div class="flow-num">0</div>
<div>
<div class="flow-title">Chargement du contexte mémoriel <span class="badge badge-db">DB</span></div>
<div class="flow-desc">Charge le dernier Rapport Portfolio et le Super Contexte depuis la base de données. Ces données enrichiront <em>tous</em> les prompts IA du cycle courant.</div>
</div>
</div>
<div class="flow-arrow"></div>
<div class="flow-step data">
<div class="flow-num">1</div>
<div>
<div class="flow-title">Collecte des données de marché <span class="badge badge-mini">GPT-4o-mini</span></div>
<div class="flow-desc">
Récupère les news géopolitiques (RSS, GDELT), les prix de marché
(yfinance), les indicateurs macro (VIX, pente de courbe, DXY, Brent,
or, cuivre, S&amp;P vs moyenne 200j) et le calendrier économique.
<br><strong>Appel IA :</strong> GPT-4o-mini score chaque news (impact 0100, classification géopolitique) en batch.
</div>
</div>
</div>
<div class="flow-arrow"></div>
<div class="flow-step ai">
<div class="flow-num">2</div>
<div>
<div class="flow-title">Suggestion de nouveaux patterns <span class="badge badge-ai">GPT-4o</span></div>
<div class="flow-desc">
GPT-4o reçoit le contexte complet (news + prix + macro +
calendrier + Super Contexte + leçons portfolio) et propose des nouvelles
thèses de trading structurées.
<br>Retourne : nom, description, thèse macro, trades suggérés, keywords, probabilité, horizon cible.
</div>
</div>
</div>
<div class="flow-arrow"></div>
<div class="flow-step db">
<div class="flow-num">3</div>
<div>
<div class="flow-title">Filtre anti-doublons (Jaccard) <span class="badge badge-db">Règle</span></div>
<div class="flow-desc">
Chaque pattern suggéré est comparé aux patterns existants par
similarité de keywords (distance de Jaccard). Si la similarité dépasse
le seuil configuré (défaut : 30%), le pattern est rejeté comme doublon.
Seuls les patterns vraiment nouveaux sont sauvegardés.
</div>
</div>
</div>
<div class="flow-arrow"></div>
<div class="flow-step ai">
<div class="flow-num">4</div>
<div>
<div class="flow-title">Scoring de TOUS les patterns <span class="badge badge-ai">GPT-4o</span></div>
<div class="flow-desc">
GPT-4o score l'ensemble des patterns (nouveaux + existants) en
les confrontant au contexte marché actuel. Retourne pour chaque pattern :
score de pertinence (0100), niveau de confiance, catalyseur clé,
résumé, trade recommandé avec stratégie (call spread, put, straddle...).
</div>
</div>
</div>
<div class="flow-arrow"></div>
<div class="flow-step db">
<div class="flow-num">5</div>
<div>
<div class="flow-title">Logging et journal <span class="badge badge-db">DB</span></div>
<div class="flow-desc">
Sauvegarde les scores en historique, log les prix d'entrée des
trades passant les filtres EV/score, enregistre l'alerte géo et le
régime macro. Traces de raisonnement (input/output de chaque appel IA)
persistées pour audit.
</div>
</div>
</div>
<div class="flow-arrow"></div>
<div class="flow-step ai">
<div class="flow-num">6</div>
<div>
<div class="flow-title">Commentaire de cycle <span class="badge badge-ai">GPT-4o</span></div>
<div class="flow-desc">
Synthèse narrative courte (46 phrases) expliquant pourquoi les
patterns scorent ainsi aujourd'hui, quel est le risque principal, et
quelle recommandation tactique pour le prochain cycle. Affiché dans
l'interface.
</div>
</div>
</div>
<div class="flow-arrow"><em style="font-size:9pt;color:#94a3b8">Thread background (non-bloquant)</em></div>
<div class="flow-step bg">
<div class="flow-num">7</div>
<div>
<div class="flow-title">Rapport Portfolio automatique <span class="badge badge-ai">GPT-4o</span> <span class="badge badge-bg">Background</span></div>
<div class="flow-desc">
Déclenché seulement si ≥ 2 trades matures avec |P&amp;L| &gt;
0.1% sont disponibles. GPT-4o analyse les performances réelles des
trades matures et génère un rapport structuré (headline, analyse
gagnants/perdants, leçons clés, angles morts). Ce rapport sera injecté
au cycle suivant.
</div>
</div>
</div>
<div class="flow-arrow"></div>
<div class="flow-step bg">
<div class="flow-num">8</div>
<div>
<div class="flow-title">Mise à jour du Super Contexte <span class="badge badge-ai">GPT-4o</span> <span class="badge badge-bg">Background</span></div>
<div class="flow-desc">
Déclenché seulement si le dernier Super Contexte a plus de 6
heures. GPT-4o agrège tous les rapports portfolio, tous les trades (90j)
et la base de connaissances pour produire une synthèse évolutive riche.
Nouvelles entrées KB auto-ajoutées.
</div>
</div>
</div>
</div>
<div class="callout green">
<div class="label">Résumé des appels IA par cycle</div>
<strong>Synchrones (systématiques) :</strong> 1× GPT-4o-mini (scoring news) + 3× GPT-4o (suggestion patterns, scoring patterns, commentaire)<br>
<strong>Background (conditionnels) :</strong> 02× GPT-4o selon la disponibilité de trades matures et la fraîcheur du Super Contexte
</div>
<!-- ══════════════════════════════════════════════════════════════════════════
3. ANATOMIE DES PROMPTS
══════════════════════════════════════════════════════════════════════════ -->
<div class="page-break"></div>
<h2>3. Anatomie des Prompts IA — Ce qu'on Inclut et Pourquoi</h2>
<p class="section-intro">
Chaque prompt est construit pour donner à GPT-4o exactement ce dont il
a besoin pour être pertinent — sans surcharge inutile qui diluerait le
signal ou augmenterait le coût.
</p>
<!-- ── 3.1 Scoring des news ── -->
<h3>3.1 Scoring des news (GPT-4o-mini)</h3>
<div class="prompt-card">
<div class="prompt-card-header">
<span>Prompt : Classification et scoring d'impact des news</span>
<span class="badge badge-mini">GPT-4o-mini</span>
</div>
<div class="prompt-card-body">
<div class="prompt-section">
<div class="prompt-section-title included">✓ Ce qui est inclus</div>
<ul>
<li><strong>Titre + source + date de chaque news</strong> — le minimum nécessaire pour scorer l'impact</li>
<li><strong>Liste des catégories cibles</strong> (énergie, métaux, indices, forex, agriculture) — ancre le scoring sur nos actifs réels</li>
<li><strong>Schéma de sortie strict</strong> (score 0100, catégorie, keywords) — pour être parsé de manière fiable</li>
</ul>
</div>
<div class="prompt-section">
<div class="prompt-section-title excluded">✗ Ce qui est exclu</div>
<ul>
<li><strong>Corps complet des articles</strong> — le titre suffit pour scorer l'impact géopolitique ; le corps alourdit le contexte sans gain mesurable</li>
<li><strong>Historique des scores passés</strong> — scorer une news est une tâche stateless ; l'historique n'apporterait rien</li>
<li><strong>Données de marché</strong> — inutiles pour déterminer l'impact géopolitique d'une news</li>
</ul>
</div>
<div class="callout amber">
<div class="label">Pourquoi GPT-4o-mini ici ?</div>
Le scoring des news est une tâche de classification répétitive sur
des centaines d'items. GPT-4o-mini est 10× moins cher que GPT-4o pour
une qualité de classification identique sur des tâches structurées
courtes. On garde GPT-4o pour le raisonnement stratégique complexe.
</div>
</div>
</div>
<!-- ── 3.2 Suggestion de patterns ── -->
<h3>3.2 Suggestion de nouveaux patterns (GPT-4o)</h3>
<div class="prompt-card">
<div class="prompt-card-header">
<span>Prompt : Identification de nouvelles thèses de trading</span>
<span class="badge badge-ai">GPT-4o</span>
</div>
<div class="prompt-card-body">
<div class="prompt-section">
<div class="prompt-section-title included">✓ Ce qui est inclus</div>
<ul>
<li><strong>Top N news scorées avec leur impact</strong> — les signaux géopolitiques qui motivent les thèses</li>
<li><strong>Régime macro dominant</strong> (stagflation, crise géo, expansion...) + scores de tous les scénarios — ancre les suggestions dans la réalité macro du moment</li>
<li><strong>Prix clés</strong> (VIX, DXY, Brent, or, cuivre) — oriente vers des actifs avec momentum</li>
<li><strong>Calendrier économique à venir</strong> — évite de suggérer des thèses juste avant un catalyseur qui les invaliderait</li>
<li><strong>Super Contexte</strong> (narrative + priorités stratégiques + erreurs récurrentes) — le "cerveau" accumulé qui guide le style de suggestions</li>
<li><strong>Leçons du dernier rapport portfolio</strong> (headline + leçons clés) — ancre dans ce qui a marché ou échoué récemment</li>
<li><strong>Schéma de sortie complet</strong> (nom, description, thèse macro, keywords, trades suggérés, horizon, probabilité)</li>
</ul>
</div>
<div class="prompt-section">
<div class="prompt-section-title excluded">✗ Ce qui est exclu</div>
<ul>
<li><strong>Liste des patterns existants</strong> — le filtre
Jaccard (étape 3) gère les doublons algorithmiquement ; l'inclure dans
le prompt créerait du bruit et biaiserait GPT-4o vers des variations des
patterns actuels</li>
<li><strong>Historique complet des scores</strong> — on injecte seulement le résumé des leçons (rapport portfolio) pas les données brutes ; plus compact, plus signal</li>
<li><strong>Données de trades individuels</strong> — trop granulaires pour la suggestion ; les leçons agrégées suffisent</li>
<li><strong>Prix d'entrée et P&amp;L des positions ouvertes</strong> — crée un biais d'ancrage ; on veut des suggestions basées sur le marché actuel, pas sur nos positions</li>
</ul>
</div>
</div>
</div>
<!-- ── 3.3 Scoring des patterns ── -->
<h3>3.3 Scoring des patterns (GPT-4o)</h3>
<div class="prompt-card">
<div class="prompt-card-header">
<span>Prompt : Évaluation de pertinence de chaque pattern dans le contexte actuel</span>
<span class="badge badge-ai">GPT-4o</span>
</div>
<div class="prompt-card-body">
<div class="prompt-section">
<div class="prompt-section-title included">✓ Ce qui est inclus</div>
<ul>
<li><strong>Tous les patterns</strong> (nom, description, thèse,
keywords, trades suggérés) — GPT-4o doit évaluer chacun dans le même
appel pour maintenir une cohérence relative des scores</li>
<li><strong>Contexte marché complet</strong> (régime macro, prix, news) — le même contexte que la suggestion, pour cohérence</li>
<li><strong>Super Contexte</strong> — les erreurs récurrentes et
priorités stratégiques permettent de pénaliser les patterns qui
répètent des patterns d'échec connus</li>
<li><strong>Biais par classe d'actif</strong> (calculé depuis le régime dominant) — oriente les scores vers les classes qui profitent du régime</li>
<li><strong>Score de risque géopolitique global</strong> — affecte les scores des patterns directionnels risqués</li>
<li><strong>Schéma de sortie structuré</strong> (score, confiance, buckets d'évaluation, trade recommandé avec détails option)</li>
</ul>
</div>
<div class="prompt-section">
<div class="prompt-section-title excluded">✗ Ce qui est exclu</div>
<ul>
<li><strong>Scores historiques de chaque pattern</strong>
créerait un biais d'ancrage ; un pattern qui scorait 80 il y a 3
semaines ne devrait pas nécessairement scorer 80 aujourd'hui si le
régime a changé</li>
<li><strong>P&amp;L réel des trades logués</strong> — même raison : GPT-4o doit évaluer le marché aujourd'hui, pas être influencé par nos performances passées</li>
<li><strong>Identité des traders</strong> — non pertinent pour l'évaluation macro</li>
</ul>
</div>
</div>
</div>
<!-- ── 3.4 Commentaire de cycle ── -->
<h3>3.4 Commentaire de cycle (GPT-4o)</h3>
<div class="prompt-card">
<div class="prompt-card-header">
<span>Prompt : Narration du cycle pour le trader</span>
<span class="badge badge-ai">GPT-4o</span>
</div>
<div class="prompt-card-body">
<div class="prompt-section">
<div class="prompt-section-title included">✓ Ce qui est inclus</div>
<ul>
<li><strong>Top 5 patterns scorés</strong> (nom, score, résumé) — ce que le cycle a retenu comme pertinent maintenant</li>
<li><strong>Régime dominant + scores macro</strong> — pour expliquer pourquoi ces patterns scorent ainsi</li>
<li><strong>Indicateurs macro clés</strong> (VIX, pente de courbe, DXY, Brent, or, cuivre) — les chiffres bruts pour ancrer la narration</li>
<li><strong>Top 5 news à fort impact</strong> — les catalyseurs qui expliquent le contexte du cycle</li>
<li><strong>Trades logués ces 7 derniers jours</strong> (pattern, underlying, stratégie) — pour commenter l'alignement positions/contexte</li>
</ul>
</div>
<div class="prompt-section">
<div class="prompt-section-title excluded">✗ Ce qui est exclu</div>
<ul>
<li><strong>P&amp;L des trades</strong> — le commentaire doit rester sur le <em>contexte marché</em>, pas sur les performances (risque de biais confirmationiste)</li>
<li><strong>Toutes les news</strong> — seules les top 5 à fort impact sont pertinentes pour une synthèse narrative</li>
<li><strong>Super Contexte complet</strong> — le commentaire est une analyse ponctuelle du cycle actuel, pas une synthèse mémorielle</li>
</ul>
</div>
</div>
</div>
<!-- ── 3.5 Rapport portfolio ── -->
<h3>3.5 Rapport Portfolio automatique (GPT-4o)</h3>
<div class="prompt-card">
<div class="prompt-card-header">
<span>Prompt : Post-mortem de performance sur trades matures</span>
<span class="badge badge-ai">GPT-4o</span>
</div>
<div class="prompt-card-body">
<div class="prompt-section">
<div class="prompt-section-title included">✓ Ce qui est inclus</div>
<ul>
<li><strong>Uniquement les trades MATURES</strong> avec P&amp;L réel — seule source fiable d'apprentissage (voir section 4)</li>
<li><strong>Classification explicite</strong> (matures gagnants / matures perdants / immatures à ne pas juger)</li>
<li><strong>Score d'entrée de chaque trade</strong> — permet de corréler "le système a bien scoré ET le trade a marché" vs les faux positifs</li>
<li><strong>Indicateur de maturité</strong> de chaque trade (% de l'horizon écoulé) — contexte pour interpréter le P&amp;L</li>
<li><strong>Règle de timing explicite dans le system prompt</strong> — interdit de tirer des conclusions sur les trades immatures</li>
</ul>
</div>
<div class="prompt-section">
<div class="prompt-section-title excluded">✗ Ce qui est exclu</div>
<ul>
<li><strong>Trades immatures comme base d'apprentissage</strong> — un trade ouvert depuis 2 jours sur un horizon de 60 jours ne dit rien de fiable sur la qualité de la thèse</li>
<li><strong>Contexte macro actuel</strong> — le rapport analyse le passé (pourquoi les matures ont marché), pas le présent</li>
<li><strong>Données de marché en temps réel</strong> — hors sujet pour un post-mortem</li>
</ul>
</div>
</div>
</div>
<!-- ── 3.6 Super Contexte ── -->
<h3>3.6 Synthèse Super Contexte (GPT-4o)</h3>
<div class="prompt-card">
<div class="prompt-card-header">
<span>Prompt : Mise à jour de la base de raisonnement évolutive</span>
<span class="badge badge-ai">GPT-4o</span>
</div>
<div class="prompt-card-body">
<div class="prompt-section">
<div class="prompt-section-title included">✓ Ce qui est inclus</div>
<ul>
<li><strong>10 derniers rapports portfolio</strong> (headline, stats, analyse gagnants/perdants, leçons) — la matière première de l'apprentissage</li>
<li><strong>Tous les trades (90j)</strong> classés par maturité avec P&amp;L — la base empirique</li>
<li><strong>Base de connaissances existante</strong> (par catégorie : régimes, patterns, erreurs) — évite de repartir de zéro et préserve les insights validés</li>
<li><strong>Règle de timing dans le system prompt</strong> — même garde-fou que pour le rapport portfolio</li>
<li><strong>Instruction de conservation des insights existants</strong> si peu de matures disponibles — évite que la synthèse "oublie" les leçons validées à cause d'un manque de données récentes</li>
</ul>
</div>
<div class="prompt-section">
<div class="prompt-section-title excluded">✗ Ce qui est exclu</div>
<ul>
<li><strong>News du jour</strong> — le Super Contexte est une mémoire long terme, pas une analyse quotidienne</li>
<li><strong>Prix de marché actuels</strong> — même raison ; les régimes et corrélations sont déduits des performances, pas des prix instantanés</li>
<li><strong>Plus de 10 rapports</strong> — au-delà de 10
rapports, le prompt dépasserait la fenêtre utile ; les rapports les plus
anciens ont aussi moins de pertinence dans un marché qui évolue</li>
</ul>
</div>
</div>
</div>
<!-- ══════════════════════════════════════════════════════════════════════════
4. LA MATURITÉ DES TRADES
══════════════════════════════════════════════════════════════════════════ -->
<div class="page-break"></div>
<h2>4. La Maturité des Trades — Pourquoi C'est Central</h2>
<p class="section-intro">
C'est l'une des décisions d'architecture les plus importantes du
système. Un P&amp;L observé sur une option après 3 jours est du bruit
statistique. Après 70% de son horizon, c'est un signal. Mélanger les
deux dans les prompts IA pollue l'apprentissage.
</p>
<h3>4.1 Définition des stades</h3>
<div class="maturity-bar">
<div class="mat-seg mat-trop-tot">010%</div>
<div class="mat-seg mat-debut">1035%</div>
<div class="mat-seg mat-mature">3575%</div>
<div class="mat-seg mat-fin">&gt;75%</div>
</div>
<div class="mat-legend">
<span><span class="mat-dot" style="background:#94a3b8"></span><strong>Trop tôt</strong> (010%) — Signal nul, ne pas analyser</span>
<span><span class="mat-dot" style="background:#f59e0b"></span><strong>Début</strong> (1035%) — Signal faible, surveiller</span>
<span><span class="mat-dot" style="background:#22c55e"></span><strong>Mature</strong> (3575%) — Signal fiable, apprendre</span>
<span><span class="mat-dot" style="background:#3b82f6"></span><strong>Fin d'horizon</strong> (&gt;75%) — Signal fort, leçons définitives</span>
</div>
<h3>4.2 Impact sur chaque étape du système</h3>
<table>
<tbody><tr>
<th>Composant</th>
<th>Traitement des immatures</th>
<th>Traitement des matures</th>
</tr>
<tr>
<td><strong>Rapport Portfolio</strong></td>
<td>Listés pour transparence, étiquetés "ne pas juger", pas dans les calculs P&amp;L</td>
<td>Base exclusive des analyses, leçons et statistiques</td>
</tr>
<tr>
<td><strong>Super Contexte</strong></td>
<td>Listés avec étiquette, exclus des insights de performance</td>
<td>Matures gagnants et perdants analysés pour régimes et patterns</td>
</tr>
<tr>
<td><strong>Journal de Bord</strong></td>
<td>Badge gris/orange + barre de progression courte</td>
<td>Badge vert/bleu + P&amp;L affiché comme signal fiable</td>
</tr>
<tr>
<td><strong>Gate rapport portfolio</strong></td>
<td colspan="2">Au moins 2 trades matures avec |P&amp;L| &gt; 0,1% requis pour déclencher la génération GPT-4o</td>
</tr>
</tbody></table>
<div class="callout red">
<div class="label">Risque évité</div>
Sans ce filtre de maturité, GPT-4o pourrait tirer des leçons d'un
trade ouvert depuis 2 heures qui montre 5% (normal sur une option). Il
conclurait "la stratégie straddle est mauvaise sur le secteur énergie"
et dégraderait les cycles suivants. Le filtre de maturité protège
l'intégrité de l'apprentissage.
</div>
<!-- ══════════════════════════════════════════════════════════════════════════
5. RÈGLES DE FILTRAGE DES TRADES LOGUÉS
══════════════════════════════════════════════════════════════════════════ -->
<h2>5. Règles de Filtrage des Trades Logués</h2>
<p class="section-intro">
Tous les patterns scorés ne génèrent pas un trade dans le Journal de Bord. Deux filtres quantitatifs contrôlent l'entrée.
</p>
<h3>5.1 Les deux filtres</h3>
<table>
<tbody><tr>
<th>Filtre</th>
<th>Formule</th>
<th>Paramètre</th>
<th>Logique</th>
</tr>
<tr>
<td><strong>Score minimum</strong></td>
<td><code>score ≥ min_score</code></td>
<td>Configurable (défaut : 0)</td>
<td>Rejette les patterns jugés peu pertinents par GPT-4o dans le contexte actuel</td>
</tr>
<tr>
<td><strong>EV nette minimum</strong></td>
<td><code>p × G (1p) ≥ min_ev</code></td>
<td>Configurable (défaut : 0.0)</td>
<td>Filtre les trades à espérance mathématique négative (p = probabilité, G = gain/perte max)</td>
</tr>
</tbody></table>
<h3>5.2 Les profils de risque</h3>
<p>
Les profils de risque définissent des <em>frontières d'acceptation</em> nommées (Conservateur, Standard, Agressif...). Un trade est logué s'il passe <strong>au moins un</strong>
profil activé. Chaque profil définit un score minimum et un gain
minimum attendu, et affiche la formule EV nette à la frontière pour
vérifier la cohérence mathématique.
</p>
<div class="callout green">
<div class="label">Intérêt de l'approche multi-profil</div>
Un même pattern peut être rejeté par le profil Conservateur (score 60,
gain 50% requis) mais accepté par le profil Agressif (score 40, gain
200% requis). Cela permet de logger des opportunités à fort potentiel
sans abaisser la barre globale.
</div>
<!-- ══════════════════════════════════════════════════════════════════════════
6. GATE 6H DU SUPER CONTEXTE
══════════════════════════════════════════════════════════════════════════ -->
<h2>6. La Gate de 6 Heures du Super Contexte</h2>
<p>
Le Super Contexte est une synthèse mémorielle fondée sur des
performances de trades à 3090 jours. Le relancer toutes les 3 heures (à
chaque cycle) serait non seulement coûteux, mais contre-productif : un
nouveau trade immature ne devrait jamais remettre en cause des
conclusions issues de mois d'observations.
</p>
<table>
<tbody><tr>
<th>Scénario</th>
<th>Comportement</th>
<th>Raison</th>
</tr>
<tr>
<td>Dernier Super Contexte &lt; 6h</td>
<td>Synthèse ignorée, le SC existant est utilisé</td>
<td>Préserve les insights solides, évite le bruit des trades immatures</td>
</tr>
<tr>
<td>Dernier Super Contexte &gt; 6h</td>
<td>Nouvelle synthèse GPT-4o déclenchée</td>
<td>Intègre les nouvelles données de performance accumulées</td>
</tr>
<tr>
<td>Trigger manuel avec <code>?force=true</code></td>
<td>Synthèse forcée quelle que soit l'ancienneté</td>
<td>Permet une mise à jour immédiate si l'utilisateur le décide</td>
</tr>
</tbody></table>
<div class="callout amber">
<div class="label">Analogie</div>
Le Super Contexte est comme un livre de règles qui s'enrichit après
chaque trimestre d'opérations. On ne réécrit pas le livre parce qu'un
trade ouvert ce matin montre 2%.
</div>
<!-- ══════════════════════════════════════════════════════════════════════════
7. DÉDUPLICATION PAR JACCARD
══════════════════════════════════════════════════════════════════════════ -->
<h2>7. Filtre Anti-doublon par Distance de Jaccard</h2>
<p>
GPT-4o, s'il reçoit les mêmes news deux cycles de suite, risque de
suggérer des patterns très proches. La distance de Jaccard mesure le
chevauchement entre les <em>keywords</em> de deux patterns.
</p>
<div class="callout blue">
<div class="label">Formule</div>
<code>Jaccard(A, B) = |A ∩ B| / |A B|</code>
<br>Où A et B sont les ensembles de keywords (en minuscules) de deux patterns.
<br><br>
Si la similarité maximale entre le candidat et <em>n'importe quel</em> pattern existant dépasse le seuil (défaut 30%), le candidat est rejeté.
</div>
<table>
<tbody><tr>
<th>Seuil configuré</th>
<th>Comportement</th>
<th>Usage recommandé</th>
</tr>
<tr>
<td>10% (strict)</td>
<td>Presque tout passe, peu de déduplications</td>
<td>Base de données de patterns vide ou très petite</td>
</tr>
<tr>
<td>30% (défaut)</td>
<td>Équilibre entre nouveauté et redondance</td>
<td>Usage normal avec &gt;20 patterns existants</td>
</tr>
<tr>
<td>6080% (permissif)</td>
<td>Seuls les quasi-doublons exacts sont rejetés</td>
<td>Veut maximiser le volume de patterns générés</td>
</tr>
</tbody></table>
<!-- ══════════════════════════════════════════════════════════════════════════
8. ARCHITECTURE DES DONNÉES
══════════════════════════════════════════════════════════════════════════ -->
<div class="page-break"></div>
<h2>8. Architecture des Données</h2>
<table>
<tbody><tr>
<th>Table / Collection</th>
<th>Contenu</th>
<th>Consulté par</th>
</tr>
<tr>
<td><code>custom_patterns</code></td>
<td>Patterns créés manuellement ou par l'IA (nom, thèse, keywords, trades suggérés)</td>
<td>Scoring, suggestion (filtre Jaccard)</td>
</tr>
<tr>
<td><code>pattern_scores</code></td>
<td>Historique de chaque scoring run (score, confiance, catalyseur, trade recommandé)</td>
<td>Cockpit, Rapport IA, Journal</td>
</tr>
<tr>
<td><code>trade_entry_prices</code></td>
<td>Un enregistrement par trade logué : prix d'entrée, score à l'entrée, P&amp;L courant</td>
<td>Journal de Bord, Rapport Portfolio, Super Contexte</td>
</tr>
<tr>
<td><code>ai_reports</code></td>
<td>Rapports portfolio générés par GPT-4o (JSON + stats + winners/losers)</td>
<td>Super Contexte (10 derniers), Rapport IA page</td>
</tr>
<tr>
<td><code>reasoning_states</code></td>
<td>Versions successives du Super Contexte (narrative + synthesis JSON)</td>
<td>Cycle (contexte mémoriel), Super Contexte page</td>
</tr>
<tr>
<td><code>kb_entries</code></td>
<td>Base de connaissances : entrées par catégorie avec confiance (0100)</td>
<td>Synthèse Super Contexte, Super Contexte page</td>
</tr>
<tr>
<td><code>reasoning_traces</code></td>
<td>Input/output de chaque appel IA (suggestion + scoring) — audit complet</td>
<td>Débogage, traçabilité des décisions</td>
</tr>
<tr>
<td><code>cycle_runs</code></td>
<td>Métadonnées de chaque cycle (statut, patterns added/scored, commentary, régime)</td>
<td>Page Config, historique cycles</td>
</tr>
<tr>
<td><code>config</code></td>
<td>Clés API, paramètres auto-cycle, seuils EV/score</td>
<td>Startup, chaque cycle</td>
</tr>
</tbody></table>
<!-- ══════════════════════════════════════════════════════════════════════════
9. SYNTHÈSE DES CHOIX DE CONCEPTION
══════════════════════════════════════════════════════════════════════════ -->
<h2>9. Synthèse des Choix de Conception</h2>
<table>
<tbody><tr>
<th>Choix</th>
<th>Alternative rejetée</th>
<th>Raison</th>
</tr>
<tr>
<td>GPT-4o-mini pour le scoring des news</td>
<td>GPT-4o pour tout</td>
<td>Classification répétitive → coût 10× réduit sans perte qualité</td>
</tr>
<tr>
<td>Filtre Jaccard pour déduplier (pas GPT-4o)</td>
<td>Demander à GPT-4o si un pattern est un doublon</td>
<td>Algorithmique = déterministe, gratuit, rapide</td>
</tr>
<tr>
<td>Gate 6h sur le Super Contexte</td>
<td>Resynthétiser à chaque cycle</td>
<td>Protège l'intégrité mémorielle du bruit court terme</td>
</tr>
<tr>
<td>Rapport portfolio en background thread</td>
<td>Bloquer le cycle jusqu'à la fin du rapport</td>
<td>Le rapport sert au cycle <em>suivant</em> — pas besoin qu'il soit prêt maintenant</td>
</tr>
<tr>
<td>Filtres EV et score configurables par profil</td>
<td>Un seul seuil global fixe</td>
<td>Permet plusieurs appétits de risque sans modifier le code</td>
</tr>
<tr>
<td>Injecter Super Contexte dans la suggestion</td>
<td>Laisser GPT-4o repartir de zéro</td>
<td>Le SC contient des mois d'apprentissage validé ; en partir sans le contexte serait un gaspillage</td>
</tr>
<tr>
<td>Exclure les P&amp;L du prompt de scoring</td>
<td>Donner à GPT-4o les performances passées pour scorer</td>
<td>Évite le biais d'ancrage et la circularité (un pattern qui a
bien scoré dans le passé ne mérite pas nécessairement un bon score
aujourd'hui)</td>
</tr>
<tr>
<td>Cleanup des cycles "running" au démarrage</td>
<td>Laisser l'état incohérent</td>
<td>Un kill du processus laisse la DB en état "running" pour toujours ; le cleanup au startup garantit la cohérence</td>
</tr>
</tbody></table>
<hr>
<div class="callout blue">
<div class="label">En résumé</div>
Le système applique un principe de <strong>séparation claire entre signal et bruit</strong>
: l'IA reçoit ce qui lui permet de raisonner (contexte actuel + mémoire
validée), pas ce qui peut la biaiser (performances récentes non
matures, historique de scores bruts). Les règles quantitatives (Jaccard,
maturité, EV, gate 6h) protègent la qualité des outputs IA en amont et
en aval de chaque appel.
</div>
<p style="margin-top:40px; font-size:9pt; color:#94a3b8; text-align:center;">
GeoOptions Intelligence v2.0 &nbsp;·&nbsp; Document généré le 17 juin 2026 &nbsp;·&nbsp; FastAPI + React + SQLite + GPT-4o
</p>
</body></html>

View File

@@ -0,0 +1,888 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>GeoOptions Intelligence — Documentation Système</title>
<style>
/* ── Base ── */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: 'Georgia', 'Times New Roman', serif;
font-size: 11pt;
line-height: 1.7;
color: #1a1a2e;
background: #fff;
max-width: 900px;
margin: 0 auto;
padding: 40px 48px;
}
/* ── Typography ── */
h1 { font-size: 24pt; font-weight: 700; color: #0f2d6b; margin-bottom: 6px; letter-spacing: -0.5px; }
h2 { font-size: 15pt; font-weight: 700; color: #0f2d6b; margin: 36px 0 12px; border-bottom: 2.5px solid #0f2d6b; padding-bottom: 6px; }
h3 { font-size: 12pt; font-weight: 700; color: #1a3a7c; margin: 24px 0 8px; }
h4 { font-size: 10.5pt; font-weight: 700; color: #2d5a9e; margin: 16px 0 6px; }
p { margin-bottom: 10px; }
/* ── Cover ── */
.cover {
text-align: center;
padding: 60px 0 48px;
border-bottom: 3px solid #0f2d6b;
margin-bottom: 48px;
}
.cover .subtitle { font-size: 13pt; color: #4a6fa5; margin-top: 8px; font-style: italic; }
.cover .meta { font-size: 9.5pt; color: #7a8aaa; margin-top: 24px; font-family: 'Courier New', monospace; }
.cover .logo { font-size: 36pt; margin-bottom: 16px; }
/* ── Callout boxes ── */
.callout {
border-left: 4px solid;
padding: 12px 16px;
margin: 18px 0;
border-radius: 0 6px 6px 0;
}
.callout.blue { border-color: #2d6be4; background: #eef4ff; }
.callout.green { border-color: #16a34a; background: #f0fdf4; }
.callout.amber { border-color: #d97706; background: #fffbeb; }
.callout.red { border-color: #dc2626; background: #fff5f5; }
.callout .label { font-weight: 700; font-size: 9pt; text-transform: uppercase; letter-spacing: 1px; margin-bottom: 4px; }
.callout.blue .label { color: #2d6be4; }
.callout.green .label { color: #16a34a; }
.callout.amber .label { color: #d97706; }
.callout.red .label { color: #dc2626; }
/* ── Tables ── */
table { width: 100%; border-collapse: collapse; margin: 16px 0 24px; font-size: 10pt; }
th { background: #0f2d6b; color: #fff; padding: 9px 12px; text-align: left; font-size: 9pt; text-transform: uppercase; letter-spacing: 0.5px; }
td { padding: 8px 12px; border-bottom: 1px solid #e2e8f0; vertical-align: top; }
tr:nth-child(even) td { background: #f8fafc; }
tr:hover td { background: #eef2ff; }
/* ── Flow diagram ── */
.flow { display: flex; flex-direction: column; gap: 2px; margin: 20px 0; }
.flow-step {
display: flex;
align-items: flex-start;
gap: 14px;
padding: 12px 16px;
border-radius: 6px;
}
.flow-step.ai { background: #eef4ff; border: 1px solid #bfd1f5; }
.flow-step.data { background: #f0fdf4; border: 1px solid #bbf7d0; }
.flow-step.db { background: #fafafa; border: 1px solid #e2e8f0; }
.flow-step.bg { background: #fefce8; border: 1px solid #fde68a; }
.flow-num {
width: 26px; height: 26px; border-radius: 50%;
display: flex; align-items: center; justify-content: center;
font-size: 9pt; font-weight: 700; flex-shrink: 0; margin-top: 2px;
}
.flow-step.ai .flow-num { background: #2d6be4; color: #fff; }
.flow-step.data .flow-num { background: #16a34a; color: #fff; }
.flow-step.db .flow-num { background: #64748b; color: #fff; }
.flow-step.bg .flow-num { background: #d97706; color: #fff; }
.flow-title { font-weight: 700; font-size: 10.5pt; margin-bottom: 2px; }
.flow-step.ai .flow-title { color: #1e3a8a; }
.flow-step.data .flow-title { color: #14532d; }
.flow-step.db .flow-title { color: #334155; }
.flow-step.bg .flow-title { color: #92400e; }
.flow-desc { font-size: 9.5pt; color: #475569; }
.flow-arrow { text-align: center; color: #94a3b8; font-size: 14pt; line-height: 1; }
.badge {
display: inline-block;
padding: 1px 8px;
border-radius: 20px;
font-size: 8.5pt;
font-weight: 700;
font-family: 'Courier New', monospace;
}
.badge-ai { background: #dbeafe; color: #1e40af; }
.badge-mini { background: #ede9fe; color: #5b21b6; }
.badge-db { background: #f1f5f9; color: #475569; border: 1px solid #cbd5e1; }
.badge-bg { background: #fef9c3; color: #854d0e; }
ul { margin: 8px 0 12px 20px; }
li { margin-bottom: 4px; }
li::marker { color: #2d6be4; }
code {
font-family: 'Courier New', monospace;
font-size: 9pt;
background: #f1f5f9;
padding: 1px 5px;
border-radius: 3px;
color: #1e3a8a;
}
.section-intro { font-size: 10.5pt; color: #475569; font-style: italic; margin-bottom: 20px; }
/* ── Prompt anatomy cards ── */
.prompt-card {
border: 1.5px solid #bfd1f5;
border-radius: 8px;
margin: 20px 0;
overflow: hidden;
}
.prompt-card-header {
background: #0f2d6b;
color: #fff;
padding: 10px 16px;
font-weight: 700;
font-size: 10pt;
display: flex;
align-items: center;
justify-content: space-between;
}
.prompt-card-body { padding: 14px 16px; }
.prompt-section { margin-bottom: 12px; }
.prompt-section-title { font-weight: 700; font-size: 9pt; text-transform: uppercase; letter-spacing: 0.8px; color: #2d6be4; margin-bottom: 4px; }
.included { color: #15803d; }
.excluded { color: #dc2626; }
.check::before { content: "✓ "; font-weight: 700; }
.cross::before { content: "✗ "; font-weight: 700; }
/* ── Maturity viz ── */
.maturity-bar { display: flex; height: 24px; border-radius: 4px; overflow: hidden; margin: 12px 0 6px; }
.mat-seg { display: flex; align-items: center; justify-content: center; font-size: 8pt; font-weight: 700; color: #fff; }
.mat-trop-tot { background: #94a3b8; flex: 10; }
.mat-debut { background: #f59e0b; flex: 25; }
.mat-mature { background: #22c55e; flex: 40; }
.mat-fin { background: #3b82f6; flex: 25; }
.mat-legend { display: flex; gap: 16px; flex-wrap: wrap; font-size: 8.5pt; margin-top: 4px; }
.mat-legend span { display: flex; align-items: center; gap: 5px; }
.mat-dot { width: 10px; height: 10px; border-radius: 50%; display: inline-block; flex-shrink: 0; }
.page-break { page-break-before: always; }
hr { border: none; border-top: 1px solid #e2e8f0; margin: 32px 0; }
/* ── Print ── */
@media print {
body { padding: 20px 28px; font-size: 10pt; }
h1 { font-size: 20pt; }
h2 { font-size: 13pt; }
.cover { padding: 30px 0 30px; }
.callout, .prompt-card { break-inside: avoid; }
.flow-step { break-inside: avoid; }
a { text-decoration: none; color: inherit; }
@page { margin: 1.5cm 1.8cm; size: A4; }
}
</style>
</head>
<body>
<!-- ══════════════════════════════════════════════════════════════════════════
COVER
══════════════════════════════════════════════════════════════════════════ -->
<div class="cover">
<div class="logo"></div>
<h1>GeoOptions Intelligence</h1>
<div class="subtitle">Architecture du Système &amp; Logique des Prompts IA</div>
<div class="meta">
Version 2.0 &nbsp;·&nbsp; Juin 2026 &nbsp;·&nbsp; FastAPI + React + GPT-4o
</div>
</div>
<!-- ══════════════════════════════════════════════════════════════════════════
1. CONCEPT GÉNÉRAL
══════════════════════════════════════════════════════════════════════════ -->
<h2>1. Concept Général</h2>
<p class="section-intro">
GeoOptions Intelligence est un cockpit de trading d'options dont l'intelligence centrale repose sur un cycle itératif : collecter les signaux macro-géopolitiques, identifier les patterns d'opportunité, scorer leur pertinence en temps réel, puis apprendre des performances passées pour affiner les cycles suivants.
</p>
<h3>1.1 Philosophie fondamentale</h3>
<p>
Le système est construit autour d'une idée simple : <strong>les options sur dérivés géopolitiques suivent des patterns répétables</strong>. Une escalade tarifaire US-Chine génère systématiquement de la volatilité sur les semi-conducteurs. Un choc pétrolier bénéficie aux stratégies long sur l'énergie. Le défi n'est pas de <em>créer</em> ces patterns, mais de les <em>détecter</em> assez tôt pour agir avant le consensus.
</p>
<p>
L'IA (GPT-4o) joue le rôle d'un <strong>stratège senior externe</strong> qui lit les news, connaît les régimes macro, et enrichit la base de patterns — mais ce sont des règles humaines explicites qui décident si un trade est logué, si une analyse est valide, et si les leçons passées sont fiables.
</p>
<div class="callout blue">
<div class="label">Principe directeur</div>
L'IA propose, les règles quantitatives filtrent, l'historique de performance valide. Le cycle répète et apprend.
</div>
<h3>1.2 Les trois mémoires du système</h3>
<table>
<tr>
<th>Mémoire</th>
<th>Contenu</th>
<th>Durée de vie</th>
<th>Usage</th>
</tr>
<tr>
<td><strong>Rapport Portfolio</strong></td>
<td>Analyse post-mortem des trades matures : gagnants, perdants, leçons clés</td>
<td>~730 jours (jusqu'au prochain rapport)</td>
<td>Injecté dans les prompts de suggestion et scoring du cycle suivant</td>
</tr>
<tr>
<td><strong>Super Contexte</strong></td>
<td>Synthèse évolutive : régimes, patterns récurrents, erreurs, corrélations macro</td>
<td>Permanent — s'enrichit à chaque cycle (max 1×/6h)</td>
<td>Contexte de fond pour tout le raisonnement IA</td>
</tr>
<tr>
<td><strong>Base de Connaissances (KB)</strong></td>
<td>Entrées structurées par catégorie (régimes, patterns, erreurs) avec score de confiance</td>
<td>Permanent — géré manuellement ou auto-ajouté par synthèse</td>
<td>Consulté lors de la synthèse du Super Contexte</td>
</tr>
</table>
<!-- ══════════════════════════════════════════════════════════════════════════
2. WORKFLOW D'UN CYCLE
══════════════════════════════════════════════════════════════════════════ -->
<div class="page-break"></div>
<h2>2. Workflow d'un Cycle Complet</h2>
<p class="section-intro">
Un cycle se déclenche automatiquement (toutes les N heures) ou manuellement. La durée typique est de 2 à 5 minutes pour les étapes synchrones, plus 1 à 3 minutes en background selon la disponibilité des données.
</p>
<div class="flow">
<div class="flow-step db">
<div class="flow-num">0</div>
<div>
<div class="flow-title">Chargement du contexte mémoriel <span class="badge badge-db">DB</span></div>
<div class="flow-desc">Charge le dernier Rapport Portfolio et le Super Contexte depuis la base de données. Ces données enrichiront <em>tous</em> les prompts IA du cycle courant.</div>
</div>
</div>
<div class="flow-arrow"></div>
<div class="flow-step data">
<div class="flow-num">1</div>
<div>
<div class="flow-title">Collecte des données de marché <span class="badge badge-mini">GPT-4o-mini</span></div>
<div class="flow-desc">
Récupère les news géopolitiques (RSS, GDELT), les prix de marché (yfinance), les indicateurs macro (VIX, pente de courbe, DXY, Brent, or, cuivre, S&amp;P vs moyenne 200j) et le calendrier économique.
<br><strong>Appel IA :</strong> GPT-4o-mini score chaque news (impact 0100, classification géopolitique) en batch.
</div>
</div>
</div>
<div class="flow-arrow"></div>
<div class="flow-step ai">
<div class="flow-num">2</div>
<div>
<div class="flow-title">Suggestion de nouveaux patterns <span class="badge badge-ai">GPT-4o</span></div>
<div class="flow-desc">
GPT-4o reçoit le contexte complet (news + prix + macro + calendrier + Super Contexte + leçons portfolio) et propose des nouvelles thèses de trading structurées.
<br>Retourne : nom, description, thèse macro, trades suggérés, keywords, probabilité, horizon cible.
</div>
</div>
</div>
<div class="flow-arrow"></div>
<div class="flow-step db">
<div class="flow-num">3</div>
<div>
<div class="flow-title">Filtre anti-doublons (Jaccard) <span class="badge badge-db">Règle</span></div>
<div class="flow-desc">
Chaque pattern suggéré est comparé aux patterns existants par similarité de keywords (distance de Jaccard). Si la similarité dépasse le seuil configuré (défaut : 30%), le pattern est rejeté comme doublon. Seuls les patterns vraiment nouveaux sont sauvegardés.
</div>
</div>
</div>
<div class="flow-arrow"></div>
<div class="flow-step ai">
<div class="flow-num">4</div>
<div>
<div class="flow-title">Scoring de TOUS les patterns <span class="badge badge-ai">GPT-4o</span></div>
<div class="flow-desc">
GPT-4o score l'ensemble des patterns (nouveaux + existants) en les confrontant au contexte marché actuel. Retourne pour chaque pattern : score de pertinence (0100), niveau de confiance, catalyseur clé, résumé, trade recommandé avec stratégie (call spread, put, straddle...).
</div>
</div>
</div>
<div class="flow-arrow"></div>
<div class="flow-step db">
<div class="flow-num">5</div>
<div>
<div class="flow-title">Logging et journal <span class="badge badge-db">DB</span></div>
<div class="flow-desc">
Sauvegarde les scores en historique, log les prix d'entrée des trades passant les filtres EV/score, enregistre l'alerte géo et le régime macro. Traces de raisonnement (input/output de chaque appel IA) persistées pour audit.
</div>
</div>
</div>
<div class="flow-arrow"></div>
<div class="flow-step ai">
<div class="flow-num">6</div>
<div>
<div class="flow-title">Commentaire de cycle <span class="badge badge-ai">GPT-4o</span></div>
<div class="flow-desc">
Synthèse narrative courte (46 phrases) expliquant pourquoi les patterns scorent ainsi aujourd'hui, quel est le risque principal, et quelle recommandation tactique pour le prochain cycle. Affiché dans l'interface.
</div>
</div>
</div>
<div class="flow-arrow"><em style="font-size:9pt;color:#94a3b8">Thread background (non-bloquant)</em></div>
<div class="flow-step bg">
<div class="flow-num">7</div>
<div>
<div class="flow-title">Rapport Portfolio automatique <span class="badge badge-ai">GPT-4o</span> <span class="badge badge-bg">Background</span></div>
<div class="flow-desc">
Déclenché seulement si ≥ 2 trades matures avec |P&amp;L| &gt; 0.1% sont disponibles. GPT-4o analyse les performances réelles des trades matures et génère un rapport structuré (headline, analyse gagnants/perdants, leçons clés, angles morts). Ce rapport sera injecté au cycle suivant.
</div>
</div>
</div>
<div class="flow-arrow"></div>
<div class="flow-step bg">
<div class="flow-num">8</div>
<div>
<div class="flow-title">Mise à jour du Super Contexte <span class="badge badge-ai">GPT-4o</span> <span class="badge badge-bg">Background</span></div>
<div class="flow-desc">
Déclenché seulement si le dernier Super Contexte a plus de 6 heures. GPT-4o agrège tous les rapports portfolio, tous les trades (90j) et la base de connaissances pour produire une synthèse évolutive riche. Nouvelles entrées KB auto-ajoutées.
</div>
</div>
</div>
</div>
<div class="callout green">
<div class="label">Résumé des appels IA par cycle</div>
<strong>Synchrones (systématiques) :</strong> 1× GPT-4o-mini (scoring news) + 3× GPT-4o (suggestion patterns, scoring patterns, commentaire)<br>
<strong>Background (conditionnels) :</strong> 02× GPT-4o selon la disponibilité de trades matures et la fraîcheur du Super Contexte
</div>
<!-- ══════════════════════════════════════════════════════════════════════════
3. ANATOMIE DES PROMPTS
══════════════════════════════════════════════════════════════════════════ -->
<div class="page-break"></div>
<h2>3. Anatomie des Prompts IA — Ce qu'on Inclut et Pourquoi</h2>
<p class="section-intro">
Chaque prompt est construit pour donner à GPT-4o exactement ce dont il a besoin pour être pertinent — sans surcharge inutile qui diluerait le signal ou augmenterait le coût.
</p>
<!-- ── 3.1 Scoring des news ── -->
<h3>3.1 Scoring des news (GPT-4o-mini)</h3>
<div class="prompt-card">
<div class="prompt-card-header">
<span>Prompt : Classification et scoring d'impact des news</span>
<span class="badge badge-mini">GPT-4o-mini</span>
</div>
<div class="prompt-card-body">
<div class="prompt-section">
<div class="prompt-section-title included">✓ Ce qui est inclus</div>
<ul>
<li><strong>Titre + source + date de chaque news</strong> — le minimum nécessaire pour scorer l'impact</li>
<li><strong>Liste des catégories cibles</strong> (énergie, métaux, indices, forex, agriculture) — ancre le scoring sur nos actifs réels</li>
<li><strong>Schéma de sortie strict</strong> (score 0100, catégorie, keywords) — pour être parsé de manière fiable</li>
</ul>
</div>
<div class="prompt-section">
<div class="prompt-section-title excluded">✗ Ce qui est exclu</div>
<ul>
<li><strong>Corps complet des articles</strong> — le titre suffit pour scorer l'impact géopolitique ; le corps alourdit le contexte sans gain mesurable</li>
<li><strong>Historique des scores passés</strong> — scorer une news est une tâche stateless ; l'historique n'apporterait rien</li>
<li><strong>Données de marché</strong> — inutiles pour déterminer l'impact géopolitique d'une news</li>
</ul>
</div>
<div class="callout amber">
<div class="label">Pourquoi GPT-4o-mini ici ?</div>
Le scoring des news est une tâche de classification répétitive sur des centaines d'items. GPT-4o-mini est 10× moins cher que GPT-4o pour une qualité de classification identique sur des tâches structurées courtes. On garde GPT-4o pour le raisonnement stratégique complexe.
</div>
</div>
</div>
<!-- ── 3.2 Suggestion de patterns ── -->
<h3>3.2 Suggestion de nouveaux patterns (GPT-4o)</h3>
<div class="prompt-card">
<div class="prompt-card-header">
<span>Prompt : Identification de nouvelles thèses de trading</span>
<span class="badge badge-ai">GPT-4o</span>
</div>
<div class="prompt-card-body">
<div class="prompt-section">
<div class="prompt-section-title included">✓ Ce qui est inclus</div>
<ul>
<li><strong>Top N news scorées avec leur impact</strong> — les signaux géopolitiques qui motivent les thèses</li>
<li><strong>Régime macro dominant</strong> (stagflation, crise géo, expansion...) + scores de tous les scénarios — ancre les suggestions dans la réalité macro du moment</li>
<li><strong>Prix clés</strong> (VIX, DXY, Brent, or, cuivre) — oriente vers des actifs avec momentum</li>
<li><strong>Calendrier économique à venir</strong> — évite de suggérer des thèses juste avant un catalyseur qui les invaliderait</li>
<li><strong>Super Contexte</strong> (narrative + priorités stratégiques + erreurs récurrentes) — le "cerveau" accumulé qui guide le style de suggestions</li>
<li><strong>Leçons du dernier rapport portfolio</strong> (headline + leçons clés) — ancre dans ce qui a marché ou échoué récemment</li>
<li><strong>Schéma de sortie complet</strong> (nom, description, thèse macro, keywords, trades suggérés, horizon, probabilité)</li>
</ul>
</div>
<div class="prompt-section">
<div class="prompt-section-title excluded">✗ Ce qui est exclu</div>
<ul>
<li><strong>Liste des patterns existants</strong> — le filtre Jaccard (étape 3) gère les doublons algorithmiquement ; l'inclure dans le prompt créerait du bruit et biaiserait GPT-4o vers des variations des patterns actuels</li>
<li><strong>Historique complet des scores</strong> — on injecte seulement le résumé des leçons (rapport portfolio) pas les données brutes ; plus compact, plus signal</li>
<li><strong>Données de trades individuels</strong> — trop granulaires pour la suggestion ; les leçons agrégées suffisent</li>
<li><strong>Prix d'entrée et P&amp;L des positions ouvertes</strong> — crée un biais d'ancrage ; on veut des suggestions basées sur le marché actuel, pas sur nos positions</li>
</ul>
</div>
</div>
</div>
<!-- ── 3.3 Scoring des patterns ── -->
<h3>3.3 Scoring des patterns (GPT-4o)</h3>
<div class="prompt-card">
<div class="prompt-card-header">
<span>Prompt : Évaluation de pertinence de chaque pattern dans le contexte actuel</span>
<span class="badge badge-ai">GPT-4o</span>
</div>
<div class="prompt-card-body">
<div class="prompt-section">
<div class="prompt-section-title included">✓ Ce qui est inclus</div>
<ul>
<li><strong>Tous les patterns</strong> (nom, description, thèse, keywords, trades suggérés) — GPT-4o doit évaluer chacun dans le même appel pour maintenir une cohérence relative des scores</li>
<li><strong>Contexte marché complet</strong> (régime macro, prix, news) — le même contexte que la suggestion, pour cohérence</li>
<li><strong>Super Contexte</strong> — les erreurs récurrentes et priorités stratégiques permettent de pénaliser les patterns qui répètent des patterns d'échec connus</li>
<li><strong>Biais par classe d'actif</strong> (calculé depuis le régime dominant) — oriente les scores vers les classes qui profitent du régime</li>
<li><strong>Score de risque géopolitique global</strong> — affecte les scores des patterns directionnels risqués</li>
<li><strong>Schéma de sortie structuré</strong> (score, confiance, buckets d'évaluation, trade recommandé avec détails option)</li>
</ul>
</div>
<div class="prompt-section">
<div class="prompt-section-title excluded">✗ Ce qui est exclu</div>
<ul>
<li><strong>Scores historiques de chaque pattern</strong> — créerait un biais d'ancrage ; un pattern qui scorait 80 il y a 3 semaines ne devrait pas nécessairement scorer 80 aujourd'hui si le régime a changé</li>
<li><strong>P&amp;L réel des trades logués</strong> — même raison : GPT-4o doit évaluer le marché aujourd'hui, pas être influencé par nos performances passées</li>
<li><strong>Identité des traders</strong> — non pertinent pour l'évaluation macro</li>
</ul>
</div>
</div>
</div>
<!-- ── 3.4 Commentaire de cycle ── -->
<h3>3.4 Commentaire de cycle (GPT-4o)</h3>
<div class="prompt-card">
<div class="prompt-card-header">
<span>Prompt : Narration du cycle pour le trader</span>
<span class="badge badge-ai">GPT-4o</span>
</div>
<div class="prompt-card-body">
<div class="prompt-section">
<div class="prompt-section-title included">✓ Ce qui est inclus</div>
<ul>
<li><strong>Top 5 patterns scorés</strong> (nom, score, résumé) — ce que le cycle a retenu comme pertinent maintenant</li>
<li><strong>Régime dominant + scores macro</strong> — pour expliquer pourquoi ces patterns scorent ainsi</li>
<li><strong>Indicateurs macro clés</strong> (VIX, pente de courbe, DXY, Brent, or, cuivre) — les chiffres bruts pour ancrer la narration</li>
<li><strong>Top 5 news à fort impact</strong> — les catalyseurs qui expliquent le contexte du cycle</li>
<li><strong>Trades logués ces 7 derniers jours</strong> (pattern, underlying, stratégie) — pour commenter l'alignement positions/contexte</li>
</ul>
</div>
<div class="prompt-section">
<div class="prompt-section-title excluded">✗ Ce qui est exclu</div>
<ul>
<li><strong>P&amp;L des trades</strong> — le commentaire doit rester sur le <em>contexte marché</em>, pas sur les performances (risque de biais confirmationiste)</li>
<li><strong>Toutes les news</strong> — seules les top 5 à fort impact sont pertinentes pour une synthèse narrative</li>
<li><strong>Super Contexte complet</strong> — le commentaire est une analyse ponctuelle du cycle actuel, pas une synthèse mémorielle</li>
</ul>
</div>
</div>
</div>
<!-- ── 3.5 Rapport portfolio ── -->
<h3>3.5 Rapport Portfolio automatique (GPT-4o)</h3>
<div class="prompt-card">
<div class="prompt-card-header">
<span>Prompt : Post-mortem de performance sur trades matures</span>
<span class="badge badge-ai">GPT-4o</span>
</div>
<div class="prompt-card-body">
<div class="prompt-section">
<div class="prompt-section-title included">✓ Ce qui est inclus</div>
<ul>
<li><strong>Uniquement les trades MATURES</strong> avec P&amp;L réel — seule source fiable d'apprentissage (voir section 4)</li>
<li><strong>Classification explicite</strong> (matures gagnants / matures perdants / immatures à ne pas juger)</li>
<li><strong>Score d'entrée de chaque trade</strong> — permet de corréler "le système a bien scoré ET le trade a marché" vs les faux positifs</li>
<li><strong>Indicateur de maturité</strong> de chaque trade (% de l'horizon écoulé) — contexte pour interpréter le P&amp;L</li>
<li><strong>Règle de timing explicite dans le system prompt</strong> — interdit de tirer des conclusions sur les trades immatures</li>
</ul>
</div>
<div class="prompt-section">
<div class="prompt-section-title excluded">✗ Ce qui est exclu</div>
<ul>
<li><strong>Trades immatures comme base d'apprentissage</strong> — un trade ouvert depuis 2 jours sur un horizon de 60 jours ne dit rien de fiable sur la qualité de la thèse</li>
<li><strong>Contexte macro actuel</strong> — le rapport analyse le passé (pourquoi les matures ont marché), pas le présent</li>
<li><strong>Données de marché en temps réel</strong> — hors sujet pour un post-mortem</li>
</ul>
</div>
</div>
</div>
<!-- ── 3.6 Super Contexte ── -->
<h3>3.6 Synthèse Super Contexte (GPT-4o)</h3>
<div class="prompt-card">
<div class="prompt-card-header">
<span>Prompt : Mise à jour de la base de raisonnement évolutive</span>
<span class="badge badge-ai">GPT-4o</span>
</div>
<div class="prompt-card-body">
<div class="prompt-section">
<div class="prompt-section-title included">✓ Ce qui est inclus</div>
<ul>
<li><strong>10 derniers rapports portfolio</strong> (headline, stats, analyse gagnants/perdants, leçons) — la matière première de l'apprentissage</li>
<li><strong>Tous les trades (90j)</strong> classés par maturité avec P&amp;L — la base empirique</li>
<li><strong>Base de connaissances existante</strong> (par catégorie : régimes, patterns, erreurs) — évite de repartir de zéro et préserve les insights validés</li>
<li><strong>Règle de timing dans le system prompt</strong> — même garde-fou que pour le rapport portfolio</li>
<li><strong>Instruction de conservation des insights existants</strong> si peu de matures disponibles — évite que la synthèse "oublie" les leçons validées à cause d'un manque de données récentes</li>
</ul>
</div>
<div class="prompt-section">
<div class="prompt-section-title excluded">✗ Ce qui est exclu</div>
<ul>
<li><strong>News du jour</strong> — le Super Contexte est une mémoire long terme, pas une analyse quotidienne</li>
<li><strong>Prix de marché actuels</strong> — même raison ; les régimes et corrélations sont déduits des performances, pas des prix instantanés</li>
<li><strong>Plus de 10 rapports</strong> — au-delà de 10 rapports, le prompt dépasserait la fenêtre utile ; les rapports les plus anciens ont aussi moins de pertinence dans un marché qui évolue</li>
</ul>
</div>
</div>
</div>
<!-- ══════════════════════════════════════════════════════════════════════════
4. LA MATURITÉ DES TRADES
══════════════════════════════════════════════════════════════════════════ -->
<div class="page-break"></div>
<h2>4. La Maturité des Trades — Pourquoi C'est Central</h2>
<p class="section-intro">
C'est l'une des décisions d'architecture les plus importantes du système. Un P&amp;L observé sur une option après 3 jours est du bruit statistique. Après 70% de son horizon, c'est un signal. Mélanger les deux dans les prompts IA pollue l'apprentissage.
</p>
<h3>4.1 Définition des stades</h3>
<div class="maturity-bar">
<div class="mat-seg mat-trop-tot">010%</div>
<div class="mat-seg mat-debut">1035%</div>
<div class="mat-seg mat-mature">3575%</div>
<div class="mat-seg mat-fin">&gt;75%</div>
</div>
<div class="mat-legend">
<span><span class="mat-dot" style="background:#94a3b8"></span><strong>Trop tôt</strong> (010%) — Signal nul, ne pas analyser</span>
<span><span class="mat-dot" style="background:#f59e0b"></span><strong>Début</strong> (1035%) — Signal faible, surveiller</span>
<span><span class="mat-dot" style="background:#22c55e"></span><strong>Mature</strong> (3575%) — Signal fiable, apprendre</span>
<span><span class="mat-dot" style="background:#3b82f6"></span><strong>Fin d'horizon</strong> (&gt;75%) — Signal fort, leçons définitives</span>
</div>
<h3>4.2 Impact sur chaque étape du système</h3>
<table>
<tr>
<th>Composant</th>
<th>Traitement des immatures</th>
<th>Traitement des matures</th>
</tr>
<tr>
<td><strong>Rapport Portfolio</strong></td>
<td>Listés pour transparence, étiquetés "ne pas juger", pas dans les calculs P&amp;L</td>
<td>Base exclusive des analyses, leçons et statistiques</td>
</tr>
<tr>
<td><strong>Super Contexte</strong></td>
<td>Listés avec étiquette, exclus des insights de performance</td>
<td>Matures gagnants et perdants analysés pour régimes et patterns</td>
</tr>
<tr>
<td><strong>Journal de Bord</strong></td>
<td>Badge gris/orange + barre de progression courte</td>
<td>Badge vert/bleu + P&amp;L affiché comme signal fiable</td>
</tr>
<tr>
<td><strong>Gate rapport portfolio</strong></td>
<td colspan="2">Au moins 2 trades matures avec |P&amp;L| &gt; 0,1% requis pour déclencher la génération GPT-4o</td>
</tr>
</table>
<div class="callout red">
<div class="label">Risque évité</div>
Sans ce filtre de maturité, GPT-4o pourrait tirer des leçons d'un trade ouvert depuis 2 heures qui montre 5% (normal sur une option). Il conclurait "la stratégie straddle est mauvaise sur le secteur énergie" et dégraderait les cycles suivants. Le filtre de maturité protège l'intégrité de l'apprentissage.
</div>
<!-- ══════════════════════════════════════════════════════════════════════════
5. RÈGLES DE FILTRAGE DES TRADES LOGUÉS
══════════════════════════════════════════════════════════════════════════ -->
<h2>5. Règles de Filtrage des Trades Logués</h2>
<p class="section-intro">
Tous les patterns scorés ne génèrent pas un trade dans le Journal de Bord. Deux filtres quantitatifs contrôlent l'entrée.
</p>
<h3>5.1 Les deux filtres</h3>
<table>
<tr>
<th>Filtre</th>
<th>Formule</th>
<th>Paramètre</th>
<th>Logique</th>
</tr>
<tr>
<td><strong>Score minimum</strong></td>
<td><code>score ≥ min_score</code></td>
<td>Configurable (défaut : 0)</td>
<td>Rejette les patterns jugés peu pertinents par GPT-4o dans le contexte actuel</td>
</tr>
<tr>
<td><strong>EV nette minimum</strong></td>
<td><code>p × G (1p) ≥ min_ev</code></td>
<td>Configurable (défaut : 0.0)</td>
<td>Filtre les trades à espérance mathématique négative (p = probabilité, G = gain/perte max)</td>
</tr>
</table>
<h3>5.2 Les profils de risque</h3>
<p>
Les profils de risque définissent des <em>frontières d'acceptation</em> nommées (Conservateur, Standard, Agressif...). Un trade est logué s'il passe <strong>au moins un</strong> profil activé. Chaque profil définit un score minimum et un gain minimum attendu, et affiche la formule EV nette à la frontière pour vérifier la cohérence mathématique.
</p>
<div class="callout green">
<div class="label">Intérêt de l'approche multi-profil</div>
Un même pattern peut être rejeté par le profil Conservateur (score 60, gain 50% requis) mais accepté par le profil Agressif (score 40, gain 200% requis). Cela permet de logger des opportunités à fort potentiel sans abaisser la barre globale.
</div>
<!-- ══════════════════════════════════════════════════════════════════════════
6. GATE 6H DU SUPER CONTEXTE
══════════════════════════════════════════════════════════════════════════ -->
<h2>6. La Gate de 6 Heures du Super Contexte</h2>
<p>
Le Super Contexte est une synthèse mémorielle fondée sur des performances de trades à 3090 jours. Le relancer toutes les 3 heures (à chaque cycle) serait non seulement coûteux, mais contre-productif : un nouveau trade immature ne devrait jamais remettre en cause des conclusions issues de mois d'observations.
</p>
<table>
<tr>
<th>Scénario</th>
<th>Comportement</th>
<th>Raison</th>
</tr>
<tr>
<td>Dernier Super Contexte &lt; 6h</td>
<td>Synthèse ignorée, le SC existant est utilisé</td>
<td>Préserve les insights solides, évite le bruit des trades immatures</td>
</tr>
<tr>
<td>Dernier Super Contexte &gt; 6h</td>
<td>Nouvelle synthèse GPT-4o déclenchée</td>
<td>Intègre les nouvelles données de performance accumulées</td>
</tr>
<tr>
<td>Trigger manuel avec <code>?force=true</code></td>
<td>Synthèse forcée quelle que soit l'ancienneté</td>
<td>Permet une mise à jour immédiate si l'utilisateur le décide</td>
</tr>
</table>
<div class="callout amber">
<div class="label">Analogie</div>
Le Super Contexte est comme un livre de règles qui s'enrichit après chaque trimestre d'opérations. On ne réécrit pas le livre parce qu'un trade ouvert ce matin montre 2%.
</div>
<!-- ══════════════════════════════════════════════════════════════════════════
7. DÉDUPLICATION PAR JACCARD
══════════════════════════════════════════════════════════════════════════ -->
<h2>7. Filtre Anti-doublon par Distance de Jaccard</h2>
<p>
GPT-4o, s'il reçoit les mêmes news deux cycles de suite, risque de suggérer des patterns très proches. La distance de Jaccard mesure le chevauchement entre les <em>keywords</em> de deux patterns.
</p>
<div class="callout blue">
<div class="label">Formule</div>
<code>Jaccard(A, B) = |A ∩ B| / |A B|</code>
<br>Où A et B sont les ensembles de keywords (en minuscules) de deux patterns.
<br><br>
Si la similarité maximale entre le candidat et <em>n'importe quel</em> pattern existant dépasse le seuil (défaut 30%), le candidat est rejeté.
</div>
<table>
<tr>
<th>Seuil configuré</th>
<th>Comportement</th>
<th>Usage recommandé</th>
</tr>
<tr>
<td>10% (strict)</td>
<td>Presque tout passe, peu de déduplications</td>
<td>Base de données de patterns vide ou très petite</td>
</tr>
<tr>
<td>30% (défaut)</td>
<td>Équilibre entre nouveauté et redondance</td>
<td>Usage normal avec &gt;20 patterns existants</td>
</tr>
<tr>
<td>6080% (permissif)</td>
<td>Seuls les quasi-doublons exacts sont rejetés</td>
<td>Veut maximiser le volume de patterns générés</td>
</tr>
</table>
<!-- ══════════════════════════════════════════════════════════════════════════
8. ARCHITECTURE DES DONNÉES
══════════════════════════════════════════════════════════════════════════ -->
<div class="page-break"></div>
<h2>8. Architecture des Données</h2>
<table>
<tr>
<th>Table / Collection</th>
<th>Contenu</th>
<th>Consulté par</th>
</tr>
<tr>
<td><code>custom_patterns</code></td>
<td>Patterns créés manuellement ou par l'IA (nom, thèse, keywords, trades suggérés)</td>
<td>Scoring, suggestion (filtre Jaccard)</td>
</tr>
<tr>
<td><code>pattern_scores</code></td>
<td>Historique de chaque scoring run (score, confiance, catalyseur, trade recommandé)</td>
<td>Cockpit, Rapport IA, Journal</td>
</tr>
<tr>
<td><code>trade_entry_prices</code></td>
<td>Un enregistrement par trade logué : prix d'entrée, score à l'entrée, P&amp;L courant</td>
<td>Journal de Bord, Rapport Portfolio, Super Contexte</td>
</tr>
<tr>
<td><code>ai_reports</code></td>
<td>Rapports portfolio générés par GPT-4o (JSON + stats + winners/losers)</td>
<td>Super Contexte (10 derniers), Rapport IA page</td>
</tr>
<tr>
<td><code>reasoning_states</code></td>
<td>Versions successives du Super Contexte (narrative + synthesis JSON)</td>
<td>Cycle (contexte mémoriel), Super Contexte page</td>
</tr>
<tr>
<td><code>kb_entries</code></td>
<td>Base de connaissances : entrées par catégorie avec confiance (0100)</td>
<td>Synthèse Super Contexte, Super Contexte page</td>
</tr>
<tr>
<td><code>reasoning_traces</code></td>
<td>Input/output de chaque appel IA (suggestion + scoring) — audit complet</td>
<td>Débogage, traçabilité des décisions</td>
</tr>
<tr>
<td><code>cycle_runs</code></td>
<td>Métadonnées de chaque cycle (statut, patterns added/scored, commentary, régime)</td>
<td>Page Config, historique cycles</td>
</tr>
<tr>
<td><code>config</code></td>
<td>Clés API, paramètres auto-cycle, seuils EV/score</td>
<td>Startup, chaque cycle</td>
</tr>
</table>
<!-- ══════════════════════════════════════════════════════════════════════════
9. SYNTHÈSE DES CHOIX DE CONCEPTION
══════════════════════════════════════════════════════════════════════════ -->
<h2>9. Synthèse des Choix de Conception</h2>
<table>
<tr>
<th>Choix</th>
<th>Alternative rejetée</th>
<th>Raison</th>
</tr>
<tr>
<td>GPT-4o-mini pour le scoring des news</td>
<td>GPT-4o pour tout</td>
<td>Classification répétitive → coût 10× réduit sans perte qualité</td>
</tr>
<tr>
<td>Filtre Jaccard pour déduplier (pas GPT-4o)</td>
<td>Demander à GPT-4o si un pattern est un doublon</td>
<td>Algorithmique = déterministe, gratuit, rapide</td>
</tr>
<tr>
<td>Gate 6h sur le Super Contexte</td>
<td>Resynthétiser à chaque cycle</td>
<td>Protège l'intégrité mémorielle du bruit court terme</td>
</tr>
<tr>
<td>Rapport portfolio en background thread</td>
<td>Bloquer le cycle jusqu'à la fin du rapport</td>
<td>Le rapport sert au cycle <em>suivant</em> — pas besoin qu'il soit prêt maintenant</td>
</tr>
<tr>
<td>Filtres EV et score configurables par profil</td>
<td>Un seul seuil global fixe</td>
<td>Permet plusieurs appétits de risque sans modifier le code</td>
</tr>
<tr>
<td>Injecter Super Contexte dans la suggestion</td>
<td>Laisser GPT-4o repartir de zéro</td>
<td>Le SC contient des mois d'apprentissage validé ; en partir sans le contexte serait un gaspillage</td>
</tr>
<tr>
<td>Exclure les P&amp;L du prompt de scoring</td>
<td>Donner à GPT-4o les performances passées pour scorer</td>
<td>Évite le biais d'ancrage et la circularité (un pattern qui a bien scoré dans le passé ne mérite pas nécessairement un bon score aujourd'hui)</td>
</tr>
<tr>
<td>Cleanup des cycles "running" au démarrage</td>
<td>Laisser l'état incohérent</td>
<td>Un kill du processus laisse la DB en état "running" pour toujours ; le cleanup au startup garantit la cohérence</td>
</tr>
</table>
<hr>
<div class="callout blue">
<div class="label">En résumé</div>
Le système applique un principe de <strong>séparation claire entre signal et bruit</strong> : l'IA reçoit ce qui lui permet de raisonner (contexte actuel + mémoire validée), pas ce qui peut la biaiser (performances récentes non matures, historique de scores bruts). Les règles quantitatives (Jaccard, maturité, EV, gate 6h) protègent la qualité des outputs IA en amont et en aval de chaque appel.
</div>
<p style="margin-top:40px; font-size:9pt; color:#94a3b8; text-align:center;">
GeoOptions Intelligence v2.0 &nbsp;·&nbsp; Document généré le 17 juin 2026 &nbsp;·&nbsp; FastAPI + React + SQLite + GPT-4o
</p>
</body>
</html>

900
GeoOptions_Roadmap_Dev.html Normal file
View File

@@ -0,0 +1,900 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>GeoOptions Intelligence — Roadmap de Développement</title>
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: 'Georgia', 'Times New Roman', serif;
font-size: 10.5pt;
line-height: 1.65;
color: #1a1a2e;
background: #fff;
max-width: 940px;
margin: 0 auto;
padding: 40px 48px;
}
h1 { font-size: 22pt; font-weight: 700; color: #0f2d6b; margin-bottom: 6px; }
h2 { font-size: 14pt; font-weight: 700; color: #0f2d6b; margin: 36px 0 12px; border-bottom: 2.5px solid #0f2d6b; padding-bottom: 6px; }
h3 { font-size: 11pt; font-weight: 700; color: #1a3a7c; margin: 22px 0 8px; }
h4 { font-size: 10pt; font-weight: 700; color: #2d5a9e; margin: 14px 0 5px; }
p { margin-bottom: 9px; }
.cover { text-align: center; padding: 50px 0 40px; border-bottom: 3px solid #0f2d6b; margin-bottom: 44px; }
.cover .subtitle { font-size: 12pt; color: #4a6fa5; margin-top: 8px; font-style: italic; }
.cover .meta { font-size: 9pt; color: #7a8aaa; margin-top: 20px; font-family: 'Courier New', monospace; }
.cover .logo { font-size: 32pt; margin-bottom: 14px; }
/* ── Phase blocks ── */
.phase {
border: 1.5px solid;
border-radius: 10px;
margin: 28px 0;
overflow: hidden;
}
.phase-header {
padding: 14px 20px;
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: 8px;
}
.phase-title { font-size: 12pt; font-weight: 700; display: flex; align-items: center; gap: 10px; }
.phase-body { padding: 0 20px 20px; }
.phase.p1 { border-color: #dc2626; }
.phase.p1 .phase-header { background: #dc2626; color: #fff; }
.phase.p2 { border-color: #d97706; }
.phase.p2 .phase-header { background: #d97706; color: #fff; }
.phase.p3 { border-color: #2563eb; }
.phase.p3 .phase-header { background: #2563eb; color: #fff; }
.phase.p4 { border-color: #7c3aed; }
.phase.p4 .phase-header { background: #7c3aed; color: #fff; }
.phase-meta { font-size: 9pt; opacity: 0.88; font-family: 'Courier New', monospace; }
/* ── Sprint cards ── */
.sprint {
background: #f8fafc;
border: 1px solid #e2e8f0;
border-radius: 8px;
margin: 14px 0;
overflow: hidden;
}
.sprint-header {
padding: 10px 16px;
background: #f1f5f9;
border-bottom: 1px solid #e2e8f0;
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
flex-wrap: wrap;
}
.sprint-num { font-size: 8.5pt; font-weight: 700; font-family: 'Courier New', monospace; color: #64748b; }
.sprint-name { font-size: 10.5pt; font-weight: 700; color: #1e3a8a; }
.sprint-duration { font-size: 8.5pt; background: #dbeafe; color: #1e40af; padding: 2px 8px; border-radius: 20px; font-family: 'Courier New', monospace; font-weight: 700; }
.sprint-body { padding: 14px 16px; display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
@media (max-width: 700px) { .sprint-body { grid-template-columns: 1fr; } }
.sprint-section-title { font-size: 8pt; font-weight: 700; text-transform: uppercase; letter-spacing: 0.8px; color: #64748b; margin-bottom: 6px; }
ul.deliverables { list-style: none; margin: 0; padding: 0; }
ul.deliverables li { padding: 3px 0 3px 16px; position: relative; font-size: 9.5pt; }
ul.deliverables li::before { content: "→"; position: absolute; left: 0; color: #2563eb; font-weight: 700; }
ul.why { list-style: none; margin: 0; padding: 0; }
ul.why li { padding: 3px 0 3px 16px; position: relative; font-size: 9.5pt; color: #475569; }
ul.why li::before { content: "✓"; position: absolute; left: 0; color: #16a34a; font-weight: 700; }
ul.blockers { list-style: none; margin: 0; padding: 0; }
ul.blockers li { padding: 3px 0 3px 16px; position: relative; font-size: 9.5pt; color: #92400e; }
ul.blockers li::before { content: "⚠"; position: absolute; left: 0; }
.sprint-full { grid-column: 1 / -1; }
/* ── Score table ── */
table { width: 100%; border-collapse: collapse; margin: 14px 0 20px; font-size: 9.5pt; }
th { background: #0f2d6b; color: #fff; padding: 8px 12px; text-align: left; font-size: 8.5pt; text-transform: uppercase; letter-spacing: 0.5px; }
td { padding: 7px 12px; border-bottom: 1px solid #e2e8f0; vertical-align: top; }
tr:nth-child(even) td { background: #f8fafc; }
.score-bar { display: inline-block; height: 8px; border-radius: 4px; vertical-align: middle; margin-right: 6px; }
.score-now { background: #94a3b8; }
.score-then { background: #22c55e; }
/* ── Callout ── */
.callout { border-left: 4px solid; padding: 11px 15px; margin: 16px 0; border-radius: 0 6px 6px 0; font-size: 9.5pt; }
.callout.blue { border-color: #2d6be4; background: #eef4ff; }
.callout.green { border-color: #16a34a; background: #f0fdf4; }
.callout.amber { border-color: #d97706; background: #fffbeb; }
.callout.red { border-color: #dc2626; background: #fff5f5; }
.callout .label { font-weight: 700; font-size: 8.5pt; text-transform: uppercase; letter-spacing: 0.8px; margin-bottom: 3px; }
.callout.blue .label { color: #2d6be4; }
.callout.green .label { color: #16a34a; }
.callout.amber .label { color: #d97706; }
.callout.red .label { color: #dc2626; }
code { font-family: 'Courier New', monospace; font-size: 8.5pt; background: #f1f5f9; padding: 1px 5px; border-radius: 3px; color: #1e3a8a; }
/* ── Timeline ── */
.timeline { display: flex; gap: 0; margin: 24px 0; overflow-x: auto; }
.tl-item { flex: 1; min-width: 90px; text-align: center; position: relative; }
.tl-item::before { content: ''; position: absolute; top: 16px; left: 50%; right: -50%; height: 2px; background: #e2e8f0; z-index: 0; }
.tl-item:last-child::before { display: none; }
.tl-dot { width: 32px; height: 32px; border-radius: 50%; margin: 0 auto 6px; display: flex; align-items: center; justify-content: center; font-size: 9pt; font-weight: 700; color: #fff; position: relative; z-index: 1; }
.tl-item.p1 .tl-dot { background: #dc2626; }
.tl-item.p2 .tl-dot { background: #d97706; }
.tl-item.p3 .tl-dot { background: #2563eb; }
.tl-item.p4 .tl-dot { background: #7c3aed; }
.tl-label { font-size: 8pt; font-weight: 700; line-height: 1.3; color: #334155; }
.tl-weeks { font-size: 7.5pt; color: #94a3b8; font-family: 'Courier New', monospace; }
.badge { display: inline-block; padding: 1px 8px; border-radius: 20px; font-size: 8pt; font-weight: 700; font-family: 'Courier New', monospace; }
.badge-crit { background: #fee2e2; color: #991b1b; }
.badge-high { background: #fef3c7; color: #92400e; }
.badge-med { background: #dbeafe; color: #1e40af; }
.badge-low { background: #ede9fe; color: #5b21b6; }
.badge-data { background: #f0fdf4; color: #14532d; border: 1px solid #bbf7d0; }
.badge-back { background: #f1f5f9; color: #475569; border: 1px solid #cbd5e1; }
.badge-front { background: #fdf4ff; color: #7e22ce; border: 1px solid #e9d5ff; }
.badge-ai { background: #eff6ff; color: #1d4ed8; border: 1px solid #bfdbfe; }
.page-break { page-break-before: always; }
hr { border: none; border-top: 1px solid #e2e8f0; margin: 28px 0; }
.score-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin: 16px 0; }
.score-item { background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 6px; padding: 10px 14px; }
.score-item .domain { font-size: 8.5pt; color: #64748b; margin-bottom: 4px; }
.score-item .now-val { font-size: 14pt; font-weight: 700; }
.score-item .target { font-size: 8.5pt; color: #16a34a; font-weight: 700; }
.score-item.p1 .now-val { color: #dc2626; }
.score-item.p2 .now-val { color: #d97706; }
.score-item.p3 .now-val { color: #2563eb; }
@media print {
body { padding: 18px 24px; font-size: 9.5pt; }
.phase { break-inside: avoid; }
.sprint { break-inside: avoid; }
@page { margin: 1.5cm 1.8cm; size: A4; }
}
</style>
</head>
<body>
<!-- COVER -->
<div class="cover">
<div class="logo">🗺️</div>
<h1>GeoOptions Intelligence</h1>
<div class="subtitle">Roadmap de Développement — Vers le Niveau Hedge Fund</div>
<div class="meta">
Basé sur l'audit expert · Juin 2026 · 4 Phases · ~6 mois
</div>
</div>
<!-- ÉTAT ACTUEL -->
<h2>État actuel vs Cibles</h2>
<p>L'audit positionne le système à <strong>6,5/10 au niveau "fonds professionnel"</strong> avec des notes excellentes sur l'architecture IA et la discipline de recherche, mais des lacunes importantes sur le côté purement options (volatilité implicite, structure de marché, sizing). Ce roadmap adresse chacune.</p>
<div class="score-grid">
<div class="score-item p1"><div class="domain">Trading d'Options</div><div class="now-val">5/10</div><div class="target">→ Cible : 8/10 (Phase 1 + 2)</div></div>
<div class="score-item p1"><div class="domain">Gestion du risque</div><div class="now-val">6/10</div><div class="target">→ Cible : 8.5/10 (Phase 3)</div></div>
<div class="score-item p2"><div class="domain">Niveau Fonds Pro</div><div class="now-val">6.5/10</div><div class="target">→ Cible : 8.5/10 (Phase 1→3)</div></div>
<div class="score-item p3"><div class="domain">Architecture IA</div><div class="now-val">8.5/10</div><div class="target">→ Cible : 9.5/10 (Phase 2 + 4)</div></div>
</div>
<!-- TIMELINE -->
<h2>Vue d'ensemble — 6 mois</h2>
<div class="timeline">
<div class="tl-item p1">
<div class="tl-dot">1</div>
<div class="tl-label">Phase 1<br>IV & Vol</div>
<div class="tl-weeks">S1S6</div>
</div>
<div class="tl-item p2">
<div class="tl-dot">2</div>
<div class="tl-label">Phase 2<br>Fiabilité & Thèses</div>
<div class="tl-weeks">S7S12</div>
</div>
<div class="tl-item p3">
<div class="tl-dot">3</div>
<div class="tl-label">Phase 3<br>Portefeuille & Risk</div>
<div class="tl-weeks">S13S20</div>
</div>
<div class="tl-item p4">
<div class="tl-dot">4</div>
<div class="tl-label">Phase 4<br>Moteur Quantitatif</div>
<div class="tl-weeks">S21S26</div>
</div>
</div>
<table>
<tr>
<th>Phase</th><th>Thème</th><th>Durée</th><th>Lacune adressée</th><th>Impact note globale</th>
</tr>
<tr>
<td><strong>1</strong></td>
<td>Volatilité implicite &amp; structure du marché options</td>
<td>6 semaines</td>
<td>Trading options 5/10 → 7.5/10</td>
<td>+1.5 pts niveau pro</td>
</tr>
<tr>
<td><strong>2</strong></td>
<td>Fiabilité historique, contre-thèses &amp; calibration</td>
<td>6 semaines</td>
<td>Architecture IA 8.5/10 → 9.5/10</td>
<td>+0.8 pts niveau pro</td>
</tr>
<tr>
<td><strong>3</strong></td>
<td>Portefeuille, corrélations &amp; position sizing</td>
<td>8 semaines</td>
<td>Gestion risque 6/10 → 8.5/10</td>
<td>+1.2 pts niveau pro</td>
</tr>
<tr>
<td><strong>4</strong></td>
<td>Moteur probabiliste &amp; apprentissage automatique</td>
<td>6 semaines</td>
<td>Calibration statistique / ML</td>
<td>Niveau recherche quant</td>
</tr>
</table>
<!-- ═══════════════════════════════════════════════════════════════
PHASE 1
═══════════════════════════════════════════════════════════════ -->
<div class="page-break"></div>
<div class="phase p1">
<div class="phase-header">
<div class="phase-title">🔴 Phase 1 — Volatilité Implicite &amp; Structure Marché Options</div>
<div class="phase-meta">Semaines 16 · PRIORITÉ CRITIQUE · Lacune la plus impactante</div>
</div>
<div class="phase-body">
<div class="callout red">
<div class="label">Pourquoi c'est la priorité absolue</div>
"Tu trades des options. IV Rank, term structure, skew sont les paramètres fondamentaux de valorisation d'une option. Sans eux, une bonne thèse géopolitique peut quand même produire un mauvais trade : si l'IV est déjà au 95e percentile, tu paies 3× trop cher pour la convexité." — Audit expert
</div>
<!-- Sprint 1 -->
<div class="sprint">
<div class="sprint-header">
<div>
<div class="sprint-num">Sprint 1.1</div>
<div class="sprint-name">IV Rank &amp; IV Percentile par underlying</div>
</div>
<div style="display:flex;gap:6px;align-items:center;flex-wrap:wrap;">
<span class="badge badge-data">data</span>
<span class="badge badge-back">backend</span>
<span class="badge badge-front">frontend</span>
<span class="sprint-duration">S1 S2</span>
</div>
</div>
<div class="sprint-body">
<div>
<div class="sprint-section-title">Livrables</div>
<ul class="deliverables">
<li>Endpoint <code>GET /api/options/iv/{ticker}</code> retournant IV actuelle, IV Rank, IV Percentile (52 semaines)</li>
<li>Service <code>iv_engine.py</code> : récupère les options chains yfinance, calcule IV par Black-Scholes inverse (ou utilise la mid-price des ATM options)</li>
<li>Table DB <code>iv_history</code> : stockage journalier de l'IV par underlying (pour calculer Rank/Percentile)</li>
<li>Affichage dans Journal de Bord : badge "IV Rank 78%" coloré (vert &lt;25%, orange 2575%, rouge &gt;75%)</li>
<li>Affichage dans Cockpit : section "Options Atmosphere" avec IV des actifs clés</li>
</ul>
</div>
<div>
<div class="sprint-section-title">Pourquoi ces choix</div>
<ul class="why">
<li>IV Rank = (IV actuelle IV min 52s) / (IV max 52s IV min 52s) → signal cheap/cher universel</li>
<li>IV Percentile = % jours où IV était inférieure → plus robuste aux pics extrêmes</li>
<li>yfinance donne accès aux options chains gratuitement sur les actions US et ETFs</li>
<li>Stockage journalier nécessaire : sans historique, pas de Rank/Percentile</li>
</ul>
<div class="sprint-section-title" style="margin-top:10px">Contraintes</div>
<ul class="blockers">
<li>yfinance : données options limitées aux actions US/ETF (pas les futures)</li>
<li>Calcul BS inverse peut être instable sur les options très OTM — utiliser uniquement les ATM proches</li>
</ul>
</div>
</div>
</div>
<!-- Sprint 2 -->
<div class="sprint">
<div class="sprint-header">
<div>
<div class="sprint-num">Sprint 1.2</div>
<div class="sprint-name">Term Structure &amp; Skew</div>
</div>
<div style="display:flex;gap:6px;align-items:center;flex-wrap:wrap;">
<span class="badge badge-data">data</span>
<span class="badge badge-back">backend</span>
<span class="badge badge-front">frontend</span>
<span class="sprint-duration">S3 S4</span>
</div>
</div>
<div class="sprint-body">
<div>
<div class="sprint-section-title">Livrables</div>
<ul class="deliverables">
<li>Term structure : IV à 30j, 60j, 90j, 180j — affiché comme courbe dans une nouvelle page "Options Lab"</li>
<li>Indicateur contango/backwardation : <code>structure = "contango" | "backwardation" | "flat"</code></li>
<li>Put/Call skew : IV put 25-delta IV call 25-delta (proxy de protection de marché)</li>
<li>Injection dans le prompt de scoring : "IV Rank=78%, structure=backwardation, skew=+4pts → IV élevée, marché achète des puts"</li>
<li>Règle dans le prompt : si IV Rank &gt; 80%, pénaliser les stratégies acheteuses de vol (long straddle, long call)</li>
</ul>
</div>
<div>
<div class="sprint-section-title">Pourquoi ces choix</div>
<ul class="why">
<li>Contango (IV court terme &lt; long terme) = marché calme, bon pour vendre de la vol courte</li>
<li>Backwardation (IV court terme &gt; long terme) = stress, bon pour acheter de la protection courte</li>
<li>Skew positif (puts plus chers que calls) = marché se protège → biais baissier implicite</li>
<li>Injecter dans le scoring change concrètement les recommandations de stratégie d'option</li>
</ul>
<div class="sprint-section-title" style="margin-top:10px">Lié à</div>
<ul class="why">
<li>Sprint 1.1 (infrastructure IV)</li>
<li>Modifie le prompt de scoring (étape 4 du cycle)</li>
</ul>
</div>
</div>
</div>
<!-- Sprint 3 -->
<div class="sprint">
<div class="sprint-header">
<div>
<div class="sprint-num">Sprint 1.3</div>
<div class="sprint-name">Options Flow (proxy Open Interest)</div>
</div>
<div style="display:flex;gap:6px;align-items:center;flex-wrap:wrap;">
<span class="badge badge-data">data</span>
<span class="badge badge-back">backend</span>
<span class="sprint-duration">S5 S6</span>
</div>
</div>
<div class="sprint-body">
<div>
<div class="sprint-section-title">Livrables</div>
<ul class="deliverables">
<li>Calcul du Put/Call Open Interest Ratio par underlying (via yfinance options chains)</li>
<li>Détection des strikes avec OI inhabituel (OI &gt; 2σ de la moyenne sur 20j)</li>
<li>Indicateur "Dealer Gamma" simplifié : net gamma des market makers estimé depuis l'OI puts/calls ATM</li>
<li>Signal injecté dans le prompt : "OI puts/calls = 1.8 → smart money défensif" ou "gros OI call sur strike 550 → mur de résistance potentiel"</li>
</ul>
</div>
<div>
<div class="sprint-section-title">Pourquoi ces choix</div>
<ul class="why">
<li>Options flow = ce que font réellement les institutionnels, pas ce qu'ils disent</li>
<li>Gamma exposure élevée des dealers → ils vont hedger → amortit les mouvements de prix</li>
<li>yfinance permet d'accéder à l'OI sans abonnement premium (limité aux US)</li>
</ul>
<div class="sprint-section-title" style="margin-top:10px">Contraintes</div>
<ul class="blockers">
<li>Gamma dealer exact nécessite des données tick-by-tick (abonnement SpotGamma ~$50/mois si souhaité plus tard)</li>
<li>Proxy OI est une approximation — le labeler "estimation" dans l'UI</li>
</ul>
</div>
</div>
</div>
</div>
</div>
<!-- ═══════════════════════════════════════════════════════════════
PHASE 2
═══════════════════════════════════════════════════════════════ -->
<div class="page-break"></div>
<div class="phase p2">
<div class="phase-header">
<div class="phase-title">🟠 Phase 2 — Fiabilité Historique, Contre-Thèses &amp; Calibration</div>
<div class="phase-meta">Semaines 712 · PRIORITÉ HAUTE · Renforce l'intégrité de l'apprentissage</div>
</div>
<div class="phase-body">
<div class="callout amber">
<div class="label">Enjeu</div>
"Tous les patterns sont traités pareil. Or un pattern avec 28 trades à 68% de win rate mérite beaucoup plus de crédit qu'un pattern avec 3 trades à 100% (biais de petit échantillon)." — Audit expert
</div>
<!-- Sprint 2.1 -->
<div class="sprint">
<div class="sprint-header">
<div>
<div class="sprint-num">Sprint 2.1</div>
<div class="sprint-name">Pattern Reliability Score</div>
</div>
<div style="display:flex;gap:6px;align-items:center;flex-wrap:wrap;">
<span class="badge badge-back">backend</span>
<span class="badge badge-front">frontend</span>
<span class="badge badge-ai">prompt</span>
<span class="sprint-duration">S7 S8</span>
</div>
</div>
<div class="sprint-body">
<div>
<div class="sprint-section-title">Livrables</div>
<ul class="deliverables">
<li>Vue DB <code>pattern_reliability</code> : pour chaque pattern, calcule trades_count, win_rate, avg_pnl_pct, max_pnl, max_loss sur les trades <em>matures uniquement</em></li>
<li>Score de fiabilité composite = win_rate × log(trades_count + 1) (pénalise les petits échantillons)</li>
<li>Endpoint <code>GET /api/patterns/reliability</code></li>
<li>Affichage dans la page Patterns : tableau avec colonnes Trades / Win Rate / Avg P&amp;L / Fiabilité</li>
<li>Badge dans Journal de Bord : "⭐ 68% sur 28 trades" ou "🔬 2 trades — données insuffisantes"</li>
<li>Injection dans le prompt de suggestion : tableau de fiabilité des 10 meilleurs patterns → GPT-4o ne suggère pas de variantes d'un pattern déjà fiable si le contexte n'a pas changé</li>
</ul>
</div>
<div>
<div class="sprint-section-title">Pourquoi ces choix</div>
<ul class="why">
<li>Un pattern nouveau n'a aucune track record → sa "fiabilité" = 0 jusqu'à accumulation de données</li>
<li>Le multiplicateur log(n+1) évite qu'un pattern à 100% sur 2 trades écrase un pattern à 65% sur 30 trades</li>
<li>Injecter dans le prompt de suggestion : GPT-4o peut adapter ses thèses en connaissant ce qui a empiriquement fonctionné</li>
<li>Critique = base uniquement sur trades MATURES pour éviter le bruit (cohérence avec le principe fondamental)</li>
</ul>
</div>
</div>
</div>
<!-- Sprint 2.2 -->
<div class="sprint">
<div class="sprint-header">
<div>
<div class="sprint-num">Sprint 2.2</div>
<div class="sprint-name">Contre-Thèses &amp; Événements Invalidants</div>
</div>
<div style="display:flex;gap:6px;align-items:center;flex-wrap:wrap;">
<span class="badge badge-back">backend</span>
<span class="badge badge-front">frontend</span>
<span class="badge badge-ai">prompt</span>
<span class="sprint-duration">S9 S10</span>
</div>
</div>
<div class="sprint-body">
<div>
<div class="sprint-section-title">Livrables</div>
<ul class="deliverables">
<li>Champs <code>counter_thesis</code>, <code>invalidation_trigger</code>, <code>invalidation_probability</code> ajoutés à la table <code>custom_patterns</code></li>
<li>Modification du prompt de suggestion : GPT-4o doit retourner pour chaque pattern {"thesis": ..., "counter_thesis": ..., "invalidation_trigger": ..., "invalidation_prob": 0-100}</li>
<li>Affichage dans la page Patterns : onglet "Thèse / Contre-thèse / Invalidation" avec couleur de probabilité d'invalidation</li>
<li>Système de surveillance : si une news matche le <code>invalidation_trigger</code> d'un trade ouvert → alerte dans le Journal de Bord</li>
<li>Intégration dans le scoring : patterns avec forte probabilité d'invalidation imminente → score pénalisé</li>
</ul>
</div>
<div>
<div class="sprint-section-title">Pourquoi ces choix</div>
<ul class="why">
<li>Force GPT-4o à raisonner en "steel man" de la position opposée → réduit le biais de confirmation</li>
<li>Le trigger d'invalidation permet une gestion dynamique : si le trigger se réalise, sortir du trade</li>
<li>Exemple : thèse = escalade Taiwan; contre-thèse = bluff diplomatique; invalidation = annonce négociations US-Chine</li>
</ul>
<div class="sprint-section-title" style="margin-top:10px">Impact Super Contexte</div>
<ul class="why">
<li>Le rapport portfolio pourra distinguer "thèse correcte mais invalidation s'est réalisée" vs "thèse incorrecte dès le départ"</li>
</ul>
</div>
</div>
</div>
<!-- Sprint 2.3 -->
<div class="sprint">
<div class="sprint-header">
<div>
<div class="sprint-num">Sprint 2.3</div>
<div class="sprint-name">Calibration Probabiliste &amp; Demi-Vie des Insights</div>
</div>
<div style="display:flex;gap:6px;align-items:center;flex-wrap:wrap;">
<span class="badge badge-back">backend</span>
<span class="badge badge-front">frontend</span>
<span class="sprint-duration">S11 S12</span>
</div>
</div>
<div class="sprint-body">
<div>
<div class="sprint-section-title">Livrables</div>
<ul class="deliverables">
<li>Stockage de la probabilité prédite (0100) dans <code>pattern_scores</code> — issue du champ <code>probability</code> du pattern à la date de scoring</li>
<li>À maturité du trade : comparaison probabilité prédite vs résultat binaire (P&amp;L &gt; 0)</li>
<li>Courbe de calibration : "quand je prédis 75%, le taux de réussite réel est X%"</li>
<li>Score de Brier affiché dans une page Analytics</li>
<li>Demi-vie des insights KB : champ <code>expires_at</code> + <code>confidence_decay_days</code> sur les entrées KB. Les insights de plus de 90j sans confirmation voient leur confiance baisser automatiquement</li>
</ul>
</div>
<div>
<div class="sprint-section-title">Pourquoi ces choix</div>
<ul class="why">
<li>Si le système annonce "80%" et n'a raison qu'à 52% → il est surconfiant → recalibrer le prompt</li>
<li>Protège contre la dérive du Super Contexte : une leçon apprise en 2024 peut ne plus être valide en 2026 si le régime a changé</li>
<li>Score de Brier = mesure standard de calibration probabiliste (0 = parfait, 1 = nul, 0.25 = aléatoire)</li>
</ul>
<div class="sprint-section-title" style="margin-top:10px">Adresse le risque expert</div>
<ul class="why">
<li>Protège la boucle SC contre l'auto-confirmation : les insights expirent s'ils ne sont pas reconfirmés empiriquement</li>
</ul>
</div>
</div>
</div>
</div>
</div>
<!-- ═══════════════════════════════════════════════════════════════
PHASE 3
═══════════════════════════════════════════════════════════════ -->
<div class="page-break"></div>
<div class="phase p3">
<div class="phase-header">
<div class="phase-title">🔵 Phase 3 — Portefeuille, Corrélations &amp; Position Sizing</div>
<div class="phase-meta">Semaines 1320 · PRIORITÉ HAUTE · Raisonnement portefeuille vs trade par trade</div>
</div>
<div class="phase-body">
<div class="callout blue">
<div class="label">Enjeu</div>
"Tu peux avoir 3 trades différents (pétrole, défense, short Europe) qui dépendent tous d'une seule variable : guerre au Moyen-Orient. En réalité, tu n'as qu'un seul risque concentré. Un Risk Engine révèle ça." — Audit expert
</div>
<!-- Sprint 3.1 -->
<div class="sprint">
<div class="sprint-header">
<div>
<div class="sprint-num">Sprint 3.1</div>
<div class="sprint-name">Vue Portefeuille Consolidée</div>
</div>
<div style="display:flex;gap:6px;align-items:center;flex-wrap:wrap;">
<span class="badge badge-back">backend</span>
<span class="badge badge-front">frontend</span>
<span class="sprint-duration">S13 S14</span>
</div>
</div>
<div class="sprint-body">
<div>
<div class="sprint-section-title">Livrables</div>
<ul class="deliverables">
<li>Nouvelle page "Portefeuille Global" : vue consolidée de toutes les positions ouvertes par classe d'actif</li>
<li>Exposition nette par classe d'actif (énergie, métaux, indices US, Europe, Asie, forex)</li>
<li>P&amp;L agrégé et décomposé (par classe, par régime d'entrée, par pattern)</li>
<li>Graphique d'évolution du P&amp;L portefeuille dans le temps</li>
<li>Indicateur de concentration : si &gt;40% du portefeuille dans une classe → alerte</li>
</ul>
</div>
<div>
<div class="sprint-section-title">Pourquoi ces choix</div>
<ul class="why">
<li>La vue par trade masque les concentrations : 5 trades en énergie = 1 gros pari sur le pétrole</li>
<li>Décomposer par régime d'entrée permet de voir "ai-je sur-tradé en régime stagflation ?"</li>
</ul>
</div>
</div>
</div>
<!-- Sprint 3.2 -->
<div class="sprint">
<div class="sprint-header">
<div>
<div class="sprint-num">Sprint 3.2</div>
<div class="sprint-name">Risk Cluster Engine</div>
</div>
<div style="display:flex;gap:6px;align-items:center;flex-wrap:wrap;">
<span class="badge badge-back">backend</span>
<span class="badge badge-front">frontend</span>
<span class="badge badge-ai">prompt</span>
<span class="sprint-duration">S15 S16</span>
</div>
</div>
<div class="sprint-body">
<div>
<div class="sprint-section-title">Livrables</div>
<ul class="deliverables">
<li>Classification des trades ouverts par "facteur de risque" : géopolitique, inflation, récession, liquidité, dollar</li>
<li>Calcul de l'exposition nette par facteur (en nb de trades ET en P&amp;L potentiel)</li>
<li>Heatmap des corrélations entre patterns (corrélation P&amp;L historique entre paires de patterns)</li>
<li>Alerte "Concentration Risque" : si exposition &gt; seuil sur un facteur → alerte dans le Cockpit</li>
<li>Injection dans le prompt de scoring : "Attention : 60% du portefeuille est déjà exposé au risque géopolitique Asie — pénaliser les nouveaux patterns dépendants de ce facteur"</li>
</ul>
</div>
<div>
<div class="sprint-section-title">Pourquoi ces choix</div>
<ul class="why">
<li>Le Risk Cluster révèle le risque "caché" commun à des trades apparemment indépendants</li>
<li>Injecter dans le scoring modifie concrètement les recommandations selon l'état du portefeuille</li>
<li>Corrélation P&amp;L historique : deux patterns corrélés à 0.8 ne sont pas deux opportunités indépendantes</li>
</ul>
</div>
</div>
</div>
<!-- Sprint 3.3 -->
<div class="sprint">
<div class="sprint-header">
<div>
<div class="sprint-num">Sprint 3.3</div>
<div class="sprint-name">Position Sizing — Kelly Fractionnel</div>
</div>
<div style="display:flex;gap:6px;align-items:center;flex-wrap:wrap;">
<span class="badge badge-back">backend</span>
<span class="badge badge-front">frontend</span>
<span class="sprint-duration">S17 S18</span>
</div>
</div>
<div class="sprint-body">
<div>
<div class="sprint-section-title">Livrables</div>
<ul class="deliverables">
<li>Calcul du Kelly fractionnel par trade : <code>f* = (p × G (1p)) / G</code> où p = probabilité estimée, G = gain max</li>
<li>Kelly fractionnel : recommander 2550% du Kelly théorique (standard institutionnel)</li>
<li>Ajustement par Risk Cluster : si le facteur de risque est déjà saturé → diviser le sizing par 2</li>
<li>Volatility targeting : ajuster la taille pour maintenir une contribution constante au risque portefeuille (ex : chaque trade cible 1% de VaR)</li>
<li>Affichage dans Journal de Bord : colonne "Sizing suggéré" avec explication (Kelly brut / ajusté)</li>
</ul>
</div>
<div>
<div class="sprint-section-title">Pourquoi ces choix</div>
<ul class="why">
<li>"Un signal moyen avec un sizing exceptionnel gagne de l'argent. L'inverse non." — Audit expert</li>
<li>Kelly fractionnel (2550%) est la norme : le Kelly complet sur-optimise et produit des drawdowns brutaux</li>
<li>VaR contribution constante = technique de risk parity adaptée au sizing d'options</li>
</ul>
</div>
</div>
</div>
<!-- Sprint 3.4 -->
<div class="sprint">
<div class="sprint-header">
<div>
<div class="sprint-num">Sprint 3.4</div>
<div class="sprint-name">Tableau de Bord Risque Global</div>
</div>
<div style="display:flex;gap:6px;align-items:center;flex-wrap:wrap;">
<span class="badge badge-front">frontend</span>
<span class="sprint-duration">S19 S20</span>
</div>
</div>
<div class="sprint-body">
<div>
<div class="sprint-section-title">Livrables</div>
<ul class="deliverables">
<li>Page "Risk Dashboard" : vue unique de tout le risque portefeuille</li>
<li>Jauge de concentration par facteur (géo, inflation, récession, liquidité)</li>
<li>Expected Drawdown du portefeuille estimé (basé sur corrélations historiques)</li>
<li>Score de "diversification effective" : 1 (positions toutes corrélées) → N (positions parfaitement indépendantes)</li>
<li>Recommandation automatique : "Portefeuille sur-concentré sur risque géo Asie — les 2 prochains trades devraient être sur d'autres facteurs"</li>
</ul>
</div>
<div>
<div class="sprint-section-title">Pourquoi ces choix</div>
<ul class="why">
<li>Un Risk Committee sérieux regarde ce tableau avant chaque trade</li>
<li>La recommandation automatique ferme la boucle : le Risk Engine alimente directement les suggestions du prochain cycle</li>
</ul>
</div>
</div>
</div>
</div>
</div>
<!-- ═══════════════════════════════════════════════════════════════
PHASE 4
═══════════════════════════════════════════════════════════════ -->
<div class="page-break"></div>
<div class="phase p4">
<div class="phase-header">
<div class="phase-title">🟣 Phase 4 — Moteur Probabiliste &amp; Apprentissage Automatique</div>
<div class="phase-meta">Semaines 2126 · NIVEAU RECHERCHE · Vers le niveau quant fund</div>
</div>
<div class="phase-body">
<div class="callout blue">
<div class="label">Enjeu</div>
"Un fonds institutionnel voudrait : IA → hypothèse → Modèle statistique → validation. Aujourd'hui tu as LLM → score. Il manque la validation quantitative indépendante de GPT." — Audit expert
</div>
<!-- Sprint 4.1 -->
<div class="sprint">
<div class="sprint-header">
<div>
<div class="sprint-num">Sprint 4.1</div>
<div class="sprint-name">Bayesian Updating des Scores</div>
</div>
<div style="display:flex;gap:6px;align-items:center;flex-wrap:wrap;">
<span class="badge badge-back">backend</span>
<span class="badge badge-front">frontend</span>
<span class="sprint-duration">S21 S22</span>
</div>
</div>
<div class="sprint-body">
<div>
<div class="sprint-section-title">Livrables</div>
<ul class="deliverables">
<li>Chaque pattern a un prior bayésien (distribution Beta sur le win rate) mis à jour à chaque trade mature</li>
<li>Score composite = α × score_GPT + (1α) × score_bayesien où α décroît avec le nombre de trades (confiance croissante dans l'historique)</li>
<li>Intervalles de confiance affichés : "Win rate estimé : 65% ± 12%" (beta distribution)</li>
<li>Recalibration automatique du facteur alpha selon le score de Brier du système (Phase 2.3)</li>
</ul>
</div>
<div>
<div class="sprint-section-title">Pourquoi ces choix</div>
<ul class="why">
<li>Beta(α,β) est le prior conjugué naturel pour un taux de succès → mise à jour simple et analytique</li>
<li>Plus le pattern a de trades → plus l'historique prime sur l'opinion de GPT-4o</li>
<li>Adresse directement le manque de calibration statistique identifié par l'expert</li>
</ul>
</div>
</div>
</div>
<!-- Sprint 4.2 -->
<div class="sprint">
<div class="sprint-header">
<div>
<div class="sprint-num">Sprint 4.2</div>
<div class="sprint-name">Détection Automatique de Régimes (Clustering)</div>
</div>
<div style="display:flex;gap:6px;align-items:center;flex-wrap:wrap;">
<span class="badge badge-back">backend</span>
<span class="badge badge-front">frontend</span>
<span class="sprint-duration">S23 S24</span>
</div>
</div>
<div class="sprint-body">
<div>
<div class="sprint-section-title">Livrables</div>
<ul class="deliverables">
<li>Clustering non supervisé (K-Means ou DBSCAN) sur les vecteurs macro journaliers (VIX, pente, DXY, Brent, or) pour auto-détecter les régimes latents</li>
<li>Comparaison régime détecté vs régime nommé (stagflation, expansion...) → mesure d'alignement</li>
<li>Auto-découverte de sous-régimes : "stagflation avec stress dollar" vs "stagflation avec fuite vers l'or"</li>
<li>Affichage dans Régime Macro : "Cluster 3 (similaire à Oct 2022, Nov 2023)" avec performance historique des patterns dans ce cluster</li>
</ul>
</div>
<div>
<div class="sprint-section-title">Pourquoi ces choix</div>
<ul class="why">
<li>Les régimes nommés à la main sont une approximation — le clustering découvre les régimes réels dans les données</li>
<li>"Les marchés ne sont pas stationnaires" — le clustering aide à identifier quand le régime a changé</li>
<li>Librairie sklearn (déjà dans l'environnement Python probable) — pas de dépendance lourde</li>
</ul>
</div>
</div>
</div>
<!-- Sprint 4.3 -->
<div class="sprint">
<div class="sprint-header">
<div>
<div class="sprint-num">Sprint 4.3</div>
<div class="sprint-name">Embeddings de Patterns &amp; Similarité Sémantique</div>
</div>
<div style="display:flex;gap:6px;align-items:center;flex-wrap:wrap;">
<span class="badge badge-back">backend</span>
<span class="badge badge-ai">API</span>
<span class="sprint-duration">S25</span>
</div>
</div>
<div class="sprint-body">
<div>
<div class="sprint-section-title">Livrables</div>
<ul class="deliverables">
<li>Embedding de chaque pattern via l'API OpenAI (<code>text-embedding-3-small</code>) stocké en DB</li>
<li>Remplacement du filtre Jaccard par similarité cosinus sur embeddings → détection de doublons sémantiques, pas juste des mots identiques</li>
<li>Recherche sémantique : "trouver les patterns similaires à cette thèse" dans la page Patterns</li>
</ul>
</div>
<div>
<div class="sprint-section-title">Pourquoi ces choix</div>
<ul class="why">
<li>Jaccard rate "Taiwan Tensions" et "Détroit de Taiwan Risk" comme différents — embeddings les détecte comme proches</li>
<li><code>text-embedding-3-small</code> = $0.002/1M tokens, quasi gratuit</li>
</ul>
</div>
</div>
</div>
<!-- Sprint 4.4 -->
<div class="sprint">
<div class="sprint-header">
<div>
<div class="sprint-num">Sprint 4.4</div>
<div class="sprint-name">Tableau de Bord Analytique Complet</div>
</div>
<div style="display:flex;gap:6px;align-items:center;flex-wrap:wrap;">
<span class="badge badge-front">frontend</span>
<span class="sprint-duration">S26</span>
</div>
</div>
<div class="sprint-body">
<div class="sprint-full">
<div class="sprint-section-title">Livrables</div>
<ul class="deliverables">
<li>Page Analytics avec : courbe de calibration, Brier score, win rate par régime, performance par pattern, drawdown historique</li>
<li>Comparaison score GPT vs résultat réel sur 6 mois : "le modèle sur-performe dans les régimes de crise, sous-performe en régime d'expansion"</li>
<li>Export CSV de toutes les métriques pour analyse externe</li>
</ul>
</div>
</div>
</div>
</div>
</div>
<!-- RÉCAPITULATIF -->
<div class="page-break"></div>
<h2>Récapitulatif Exécutif</h2>
<table>
<tr>
<th>Sprint</th><th>Feature</th><th>Durée</th><th>Dépendances</th><th>Impact</th>
</tr>
<tr><td>1.1</td><td>IV Rank &amp; Percentile</td><td>2s</td><td></td><td>★★★★★</td></tr>
<tr><td>1.2</td><td>Term Structure &amp; Skew</td><td>2s</td><td>1.1</td><td>★★★★★</td></tr>
<tr><td>1.3</td><td>Options Flow (proxy OI)</td><td>2s</td><td>1.1</td><td>★★★★☆</td></tr>
<tr><td>2.1</td><td>Pattern Reliability Score</td><td>2s</td><td>historique trades</td><td>★★★★★</td></tr>
<tr><td>2.2</td><td>Contre-thèses &amp; Invalidation</td><td>2s</td><td></td><td>★★★★☆</td></tr>
<tr><td>2.3</td><td>Calibration + Demi-vie KB</td><td>2s</td><td>2.1</td><td>★★★★☆</td></tr>
<tr><td>3.1</td><td>Vue Portefeuille Consolidée</td><td>2s</td><td></td><td>★★★★☆</td></tr>
<tr><td>3.2</td><td>Risk Cluster Engine</td><td>2s</td><td>3.1</td><td>★★★★★</td></tr>
<tr><td>3.3</td><td>Position Sizing (Kelly)</td><td>2s</td><td>2.1, 3.2</td><td>★★★★★</td></tr>
<tr><td>3.4</td><td>Risk Dashboard</td><td>2s</td><td>3.1, 3.2, 3.3</td><td>★★★☆☆</td></tr>
<tr><td>4.1</td><td>Bayesian Updating</td><td>2s</td><td>2.1, 2.3</td><td>★★★★☆</td></tr>
<tr><td>4.2</td><td>Clustering de Régimes</td><td>2s</td><td>données macro</td><td>★★★☆☆</td></tr>
<tr><td>4.3</td><td>Embeddings Patterns</td><td>1s</td><td></td><td>★★★☆☆</td></tr>
<tr><td>4.4</td><td>Analytics Dashboard</td><td>1s</td><td>toutes phases</td><td>★★★☆☆</td></tr>
</table>
<h3>Décisions de démarrage immédiates</h3>
<table>
<tr><th>Question</th><th>Recommandation</th></tr>
<tr>
<td>Source des données IV ?</td>
<td>yfinance pour les sous-jacents US/ETF (gratuit). Pour les futures (Brent, or), explorer Yahoo Finance "BZ=F", "GC=F" — options disponibles mais limitées.</td>
</tr>
<tr>
<td>Stocker l'IV en DB ou calculer à la volée ?</td>
<td>Stocker journalièrement. IV Rank et Percentile ont besoin de 52 semaines d'historique — impossible à calculer à la volée sur yfinance sans cet historique.</td>
</tr>
<tr>
<td>Kelly sur quel capital de référence ?</td>
<td>Laisser l'utilisateur définir un capital de référence dans la Config (ex : 50 000€) — le sizing est exprimé en % et en € notionnel.</td>
</tr>
<tr>
<td>Embeddings : OpenAI ou local ?</td>
<td>OpenAI <code>text-embedding-3-small</code> pour démarrer ($0.002/1M tokens, négligeable). Migration vers un modèle local (sentence-transformers) possible en Phase 4 si nécessaire.</td>
</tr>
</table>
<div class="callout green">
<div class="label">Projection des notes post-roadmap</div>
<strong>Trading d'Options :</strong> 5/10 → <strong>8.5/10</strong> (Phase 1 + 2)<br>
<strong>Gestion du Risque :</strong> 6/10 → <strong>8.5/10</strong> (Phase 3)<br>
<strong>Niveau Fonds Pro :</strong> 6.5/10 → <strong>8.5/10</strong> (Phase 1 → 3)<br>
<strong>Architecture IA :</strong> 8.5/10 → <strong>9.5/10</strong> (Phase 2 + 4)
</div>
<p style="margin-top:40px; font-size:8.5pt; color:#94a3b8; text-align:center;">
GeoOptions Intelligence v2.0 — Roadmap de développement · Basé sur audit expert · Juin 2026
</p>
</body>
</html>

View File

@@ -1,6 +1,6 @@
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from routers import market_data, geopolitical, options, backtest, ai, portfolio, config, patterns, journal, cycle as cycle_router, profiles as profiles_router, reasoning as reasoning_router, knowledge as knowledge_router
from routers import market_data, geopolitical, options, backtest, ai, portfolio, config, patterns, journal, cycle as cycle_router, profiles as profiles_router, reasoning as reasoning_router, knowledge as knowledge_router, options_vol as options_vol_router
from services.database import init_db, get_config, cleanup_stale_running_cycles
import os
import uvicorn
@@ -71,6 +71,7 @@ app.include_router(cycle_router.router)
app.include_router(profiles_router.router)
app.include_router(reasoning_router.router)
app.include_router(knowledge_router.router)
app.include_router(options_vol_router.router)
@app.get("/")

View File

@@ -0,0 +1,157 @@
"""
Options volatility endpoints — IV Rank, Term Structure, Skew, Options Flow.
"""
from fastapi import APIRouter, BackgroundTasks, HTTPException
from typing import List, Optional
import logging
router = APIRouter(prefix="/api/options-vol", tags=["options-vol"])
logger = logging.getLogger(__name__)
# Simple in-memory cache to avoid hammering yfinance on every page load
_iv_cache: dict = {}
_CACHE_TTL_SECONDS = 3600 # 1h
def _is_stale(key: str) -> bool:
import time
entry = _iv_cache.get(key)
if not entry:
return True
return (time.time() - entry["ts"]) > _CACHE_TTL_SECONDS
def _set_cache(key: str, data: dict):
import time
_iv_cache[key] = {"data": data, "ts": time.time()}
@router.get("/snapshot/{ticker}")
def get_iv_snapshot(ticker: str, force: bool = False):
"""Full IV snapshot for one ticker: IV, Rank, Percentile, Term Structure, Skew, Flow."""
key = f"snapshot:{ticker.upper()}"
if not force and not _is_stale(key):
return _iv_cache[key]["data"]
from services.iv_engine import get_full_iv_snapshot
data = get_full_iv_snapshot(ticker)
_set_cache(key, data)
return data
@router.get("/batch")
def get_iv_batch(tickers: str = "SPY,QQQ,GLD,USO,UNG,XLE,TLT"):
"""
IV snapshot for multiple tickers (comma-separated).
Returns a dict {ticker: snapshot}.
Cached 1h per ticker.
"""
ticker_list = [t.strip().upper() for t in tickers.split(",") if t.strip()][:12]
result = {}
from services.iv_engine import get_full_iv_snapshot
for ticker in ticker_list:
key = f"snapshot:{ticker}"
if not _is_stale(key):
result[ticker] = _iv_cache[key]["data"]
else:
snap = get_full_iv_snapshot(ticker)
_set_cache(key, snap)
result[ticker] = snap
return {"snapshots": result, "count": len(result)}
@router.get("/watchlist")
def get_iv_watchlist():
"""
IV for the core IV watchlist (portfolio-relevant tickers).
Returns a summary list sorted by IV Rank descending.
"""
from services.iv_engine import IV_WATCHLIST, get_atm_iv, _resolve_ticker
from services.database import get_iv_rank_percentile, save_iv_snapshot
from datetime import date
results = []
today = date.today().isoformat()
for ticker in IV_WATCHLIST:
key = f"watchlist:{ticker}"
if not _is_stale(key):
results.append(_iv_cache[key]["data"])
continue
iv = get_atm_iv(ticker, 30)
if not iv:
continue
proxy = _resolve_ticker(ticker)
save_iv_snapshot(proxy, today, iv)
rank_data = get_iv_rank_percentile(proxy, iv)
item = {
"ticker": ticker,
"iv_current_pct": round(iv * 100, 1),
"iv_rank": rank_data.get("iv_rank"),
"iv_percentile": rank_data.get("iv_percentile"),
"history_days": rank_data.get("history_days", 0),
"signal": (
"sell_vol" if (rank_data.get("iv_rank") or 0) > 80
else "buy_vol" if (rank_data.get("iv_rank") or 100) < 20
else "neutral"
),
}
_set_cache(key, item)
results.append(item)
results.sort(key=lambda x: -(x.get("iv_rank") or 0))
return {"items": results, "count": len(results)}
@router.get("/history/{ticker}")
def get_iv_history_endpoint(ticker: str, days: int = 90):
"""Historical IV for a ticker (used to render IV chart)."""
from services.database import get_iv_history
from services.iv_engine import _resolve_ticker
proxy = _resolve_ticker(ticker)
history = get_iv_history(proxy, days)
return {"ticker": ticker, "proxy": proxy, "history": history, "count": len(history)}
@router.post("/refresh-watchlist")
def refresh_watchlist(background_tasks: BackgroundTasks):
"""
Trigger a background refresh of IV data for all watchlist tickers.
Call this once daily to build up the 52-week IV history.
"""
def _refresh():
from services.iv_engine import IV_WATCHLIST, get_full_iv_snapshot
logger.info(f"[IVRefresh] Refreshing IV for {len(IV_WATCHLIST)} tickers")
for ticker in IV_WATCHLIST:
try:
snap = get_full_iv_snapshot(ticker)
key = f"snapshot:{ticker}"
import time
_iv_cache[key] = {"data": snap, "ts": time.time()}
logger.debug(f"[IVRefresh] {ticker}: IV={snap.get('iv_current_pct')}% IVR={snap.get('iv_rank')}")
except Exception as e:
logger.warning(f"[IVRefresh] {ticker} failed: {e}")
logger.info("[IVRefresh] Done")
background_tasks.add_task(_refresh)
return {"status": "refresh started", "tickers": len(__import__("services.iv_engine", fromlist=["IV_WATCHLIST"]).IV_WATCHLIST)}
@router.get("/for-trade/{underlying}")
def get_iv_for_trade(underlying: str):
"""
IV snapshot specifically for a trade's underlying.
Used by JournalDeBord to show the IV context at trade time.
"""
from services.iv_engine import get_full_iv_snapshot
key = f"snapshot:{underlying.upper()}"
if not _is_stale(key):
return _iv_cache[key]["data"]
data = get_full_iv_snapshot(underlying)
_set_cache(key, data)
return data

View File

@@ -326,6 +326,7 @@ def score_patterns_with_context(
category_filter: str = None,
macro_regime: Optional[Dict] = None,
portfolio_lessons: Optional[Dict] = None,
iv_context: str = "",
) -> List[Dict[str, Any]]:
"""Score all patterns with rich context (news, prices, IV) using GPT-4o."""
if not get_client():
@@ -607,7 +608,7 @@ Leçons : {' | '.join(str(l)[:80] for l in lessons[:3])}
prompt_header = f"""CONTEXTE GLOBAL:
- Score risque géopolitique: {geo_score.get('score', 50)}/100 ({geo_score.get('level', 'medium')})
- Top risques: {geo_score.get('top_risks', [])}
{macro_section}{lessons_header}
{macro_section}{lessons_header}{iv_context}
TEMPLATE DE NOTATION:
{scoring_template}

View File

@@ -231,6 +231,25 @@ def run_cycle_once(trigger: str = "auto") -> Dict[str, Any]:
summary["patterns_added"] = added_count
# ── Step 3.5: Collect IV context ─────────────────────────────────────
iv_context = ""
try:
from services.iv_engine import get_iv_context_for_prompt, IV_WATCHLIST
# Collect underlyings from current trade journal + default watchlist
from services.database import get_mtm_trades_with_traces
_mtm = get_mtm_trades_with_traces(days=90)
_trade_tickers = list({
(t.get("underlying") or "").upper()
for t in _mtm.get("all_trades", [])
if t.get("underlying")
})
_iv_tickers = (_trade_tickers + IV_WATCHLIST[:6])[:10]
iv_context = get_iv_context_for_prompt(_iv_tickers)
if iv_context:
logger.info(f"[Cycle {run_id[:16]}] IV context collected for {len(_iv_tickers)} tickers")
except Exception as _e:
logger.warning(f"[Cycle] IV context collection failed (non-blocking): {_e}")
# ── Step 4: Score ALL patterns ────────────────────────────────────────
# Verify all patterns have IDs before scoring (guard against stale data)
patterns_with_id = [p for p in existing if p.get("id")]
@@ -251,6 +270,7 @@ def run_cycle_once(trigger: str = "auto") -> Dict[str, Any]:
category_filter=None,
macro_regime=macro_regime,
portfolio_lessons=portfolio_lessons,
iv_context=iv_context,
)
scored_with_id = [s for s in scored if s.get("pattern_id")]
scored_without_id = [s for s in scored if not s.get("pattern_id")]

View File

@@ -284,9 +284,21 @@ def init_db():
trades_analyzed INTEGER DEFAULT 0,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
)""")
c.execute("""CREATE TABLE IF NOT EXISTS iv_history (
id INTEGER PRIMARY KEY AUTOINCREMENT,
ticker TEXT NOT NULL,
recorded_date TEXT NOT NULL,
iv_current REAL,
iv_30d REAL,
iv_60d REAL,
iv_90d REAL,
created_at TEXT DEFAULT (datetime('now'))
)""")
try:
c.execute("CREATE INDEX IF NOT EXISTS idx_kb_category ON knowledge_base(category, status)")
c.execute("CREATE INDEX IF NOT EXISTS idx_rs_version ON reasoning_state(version DESC)")
c.execute("CREATE UNIQUE INDEX IF NOT EXISTS idx_iv_history_ticker_date ON iv_history(ticker, recorded_date)")
except Exception:
pass
@@ -1501,3 +1513,63 @@ def delete_kb_entry(entry_id: int) -> bool:
conn.commit()
conn.close()
return cur.rowcount > 0
# ── IV History ────────────────────────────────────────────────────────────────
def save_iv_snapshot(ticker: str, recorded_date: str, iv_current: float,
iv_30d=None, iv_60d=None, iv_90d=None) -> None:
conn = get_conn()
conn.execute(
"""INSERT OR REPLACE INTO iv_history
(ticker, recorded_date, iv_current, iv_30d, iv_60d, iv_90d)
VALUES (?, ?, ?, ?, ?, ?)""",
(ticker.upper(), recorded_date, iv_current, iv_30d, iv_60d, iv_90d),
)
conn.commit()
conn.close()
def get_iv_rank_percentile(ticker: str, current_iv: float, days: int = 252) -> Dict[str, Any]:
conn = get_conn()
rows = conn.execute(
"""SELECT iv_current FROM iv_history
WHERE ticker=? AND iv_current IS NOT NULL AND iv_current > 0
ORDER BY recorded_date DESC LIMIT ?""",
(ticker.upper(), days),
).fetchall()
conn.close()
if not rows:
return {"iv_rank": None, "iv_percentile": None, "history_days": 0}
hist = [r["iv_current"] for r in rows]
iv_min = min(hist)
iv_max = max(hist)
iv_rank = (
round((current_iv - iv_min) / (iv_max - iv_min) * 100, 1)
if iv_max > iv_min else 50.0
)
iv_percentile = round(sum(1 for v in hist if v < current_iv) / len(hist) * 100, 1)
return {
"iv_rank": iv_rank,
"iv_percentile": iv_percentile,
"history_days": len(hist),
"iv_min_52w": round(iv_min * 100, 1),
"iv_max_52w": round(iv_max * 100, 1),
}
def get_iv_history(ticker: str, days: int = 90) -> List[Dict]:
conn = get_conn()
rows = conn.execute(
"""SELECT recorded_date, iv_current, iv_30d, iv_60d, iv_90d
FROM iv_history WHERE ticker=? AND iv_current IS NOT NULL
ORDER BY recorded_date DESC LIMIT ?""",
(ticker.upper(), days),
).fetchall()
conn.close()
return [dict(r) for r in rows]

View File

@@ -0,0 +1,467 @@
"""
Implied Volatility engine — IV Rank, IV Percentile, Term Structure, Skew, Options Flow.
Supports ETFs and US equities via yfinance options chains.
Futures (CL=F, GC=F, etc.) don't have options in yfinance → mapped to ETF proxies.
"""
import logging
from datetime import date, datetime, timedelta
from typing import Dict, List, Optional, Any
import yfinance as yf
logger = logging.getLogger(__name__)
# ── Ticker mapping: futures/indices → optionable ETF/stock proxies ────────────
# Futures don't have listed options in yfinance; map to liquid ETF equivalents.
_PROXY: Dict[str, str] = {
"CL=F": "USO", # WTI oil → US Oil Fund
"BZ=F": "BNO", # Brent → US Brent Oil Fund
"NG=F": "UNG", # Natural Gas ETF
"GC=F": "GLD", # Gold → SPDR Gold
"SI=F": "SLV", # Silver ETF
"HG=F": "COPX", # Copper miners ETF
"PL=F": "PPLT", # Platinum ETF
"ZC=F": "CORN", # Corn ETF
"ZW=F": "WEAT", # Wheat ETF
"ZS=F": "SOYB", # Soybean ETF
"KC=F": "JO", # Coffee ETF
"SB=F": "CANE", # Sugar ETF
"^GSPC": "SPY", # S&P 500 → SPY
"^NDX": "QQQ", # NASDAQ 100 → QQQ
"^DJI": "DIA", # Dow → DIA
"^STOXX50E": "FEZ", # Euro Stoxx → FEZ
"^N225": "EWJ", # Nikkei → iShares Japan
"^VIX": "VIXY", # VIX → VIXY (approximate)
"EURUSD=X": "FXE", # EUR/USD ETF
"USDJPY=X": "FXY", # Yen ETF
"GBP=X": "FXB", # GBP ETF
"USDCHF=X": "FXF", # CHF ETF
"UUP": "UUP", # Already an ETF
}
# Core optionable tickers to track IV for (portfolio-relevant)
IV_WATCHLIST = [
"SPY", "QQQ", "GLD", "SLV", "USO", "BNO", "UNG",
"XLE", "UUP", "TLT", "GDX", "EWJ", "FEZ",
"XOM", "CVX", "LMT", "RTX", "BA",
]
def _resolve_ticker(ticker: str) -> str:
"""Return the optionable proxy ticker for a given symbol."""
return _PROXY.get(ticker.upper(), ticker.upper())
def _get_current_price(t: yf.Ticker) -> Optional[float]:
try:
info = t.fast_info
price = getattr(info, "last_price", None) or getattr(info, "regular_market_price", None)
if price:
return float(price)
except Exception:
pass
try:
hist = t.history(period="2d", interval="1d")
if not hist.empty:
return float(hist["Close"].dropna().iloc[-1])
except Exception:
pass
return None
def get_atm_iv(ticker: str, target_days: int = 30) -> Optional[float]:
"""
Get the ATM implied volatility for a ticker at the given target DTE.
Returns IV as a decimal (0.25 = 25%). Returns None if unavailable.
"""
proxy = _resolve_ticker(ticker)
try:
t = yf.Ticker(proxy)
price = _get_current_price(t)
if not price:
return None
expirations = t.options
if not expirations:
return None
today = date.today()
target_date = today + timedelta(days=target_days)
# Find expiration closest to target_days
best_exp = min(
expirations,
key=lambda e: abs((datetime.strptime(e, "%Y-%m-%d").date() - target_date).days),
)
chain = t.option_chain(best_exp)
calls = chain.calls[chain.calls["impliedVolatility"] > 0]
puts = chain.puts[chain.puts["impliedVolatility"] > 0]
if calls.empty and puts.empty:
return None
# ATM = strike closest to current price
all_strikes = sorted(set(calls["strike"].tolist() + puts["strike"].tolist()))
if not all_strikes:
return None
atm_strike = min(all_strikes, key=lambda s: abs(s - price))
ivs = []
c = calls[calls["strike"] == atm_strike]["impliedVolatility"]
p = puts[puts["strike"] == atm_strike]["impliedVolatility"]
if not c.empty and float(c.iloc[0]) > 0.01:
ivs.append(float(c.iloc[0]))
if not p.empty and float(p.iloc[0]) > 0.01:
ivs.append(float(p.iloc[0]))
if not ivs:
return None
return round(sum(ivs) / len(ivs), 4)
except Exception as e:
logger.debug(f"[IV] {proxy}: {e}")
return None
def get_term_structure(ticker: str) -> Dict[str, Any]:
"""
Get IV at 30d, 60d, 90d, 180d expirations.
Returns structure type: contango | backwardation | flat.
"""
proxy = _resolve_ticker(ticker)
result: Dict[str, Any] = {
"ticker": ticker,
"proxy": proxy,
"iv_30d": None,
"iv_60d": None,
"iv_90d": None,
"iv_180d": None,
"structure": None,
}
targets = {"iv_30d": 30, "iv_60d": 60, "iv_90d": 90, "iv_180d": 180}
try:
t = yf.Ticker(proxy)
price = _get_current_price(t)
if not price:
return result
expirations = t.options
if not expirations:
return result
today = date.today()
for field, days in targets.items():
target_date = today + timedelta(days=days)
best_exp = min(
expirations,
key=lambda e: abs((datetime.strptime(e, "%Y-%m-%d").date() - target_date).days),
)
# Only use if within ±20 days of target
actual_days = abs((datetime.strptime(best_exp, "%Y-%m-%d").date() - target_date).days)
if actual_days > 20:
continue
try:
chain = t.option_chain(best_exp)
calls = chain.calls[chain.calls["impliedVolatility"] > 0.01]
puts = chain.puts[chain.puts["impliedVolatility"] > 0.01]
all_strikes = sorted(set(calls["strike"].tolist() + puts["strike"].tolist()))
if not all_strikes:
continue
atm = min(all_strikes, key=lambda s: abs(s - price))
ivs = []
c = calls[calls["strike"] == atm]["impliedVolatility"]
p = puts[puts["strike"] == atm]["impliedVolatility"]
if not c.empty:
ivs.append(float(c.iloc[0]))
if not p.empty:
ivs.append(float(p.iloc[0]))
if ivs:
result[field] = round(sum(ivs) / len(ivs), 4)
except Exception:
continue
# Determine structure from 30d vs 90d
iv30 = result.get("iv_30d")
iv90 = result.get("iv_90d")
if iv30 and iv90:
diff = iv90 - iv30
if diff > 0.015:
result["structure"] = "contango" # vol increases with time → calm
elif diff < -0.015:
result["structure"] = "backwardation" # vol decreases with time → stress
else:
result["structure"] = "flat"
except Exception as e:
logger.debug(f"[TermStructure] {proxy}: {e}")
return result
def get_skew(ticker: str, target_days: int = 30) -> Dict[str, Any]:
"""
Put/Call skew: IV of 25-delta put minus IV of 25-delta call.
Positive skew = market buying put protection (bearish hedging).
"""
proxy = _resolve_ticker(ticker)
result: Dict[str, Any] = {
"ticker": ticker,
"proxy": proxy,
"put_skew": None,
"skew_pct": None,
"interpretation": None,
}
try:
t = yf.Ticker(proxy)
price = _get_current_price(t)
if not price:
return result
expirations = t.options
if not expirations:
return result
today = date.today()
target_date = today + timedelta(days=target_days)
best_exp = min(
expirations,
key=lambda e: abs((datetime.strptime(e, "%Y-%m-%d").date() - target_date).days),
)
chain = t.option_chain(best_exp)
calls = chain.calls[chain.calls["impliedVolatility"] > 0.01].copy()
puts = chain.puts[chain.puts["impliedVolatility"] > 0.01].copy()
if calls.empty or puts.empty:
return result
# Approximate 25-delta strikes: ~8590% of price for puts, ~110115% for calls
otm_put_strike = price * 0.90
otm_call_strike = price * 1.10
put_row = puts.iloc[(puts["strike"] - otm_put_strike).abs().argsort()[:1]]
call_row = calls.iloc[(calls["strike"] - otm_call_strike).abs().argsort()[:1]]
if put_row.empty or call_row.empty:
return result
iv_put = float(put_row["impliedVolatility"].iloc[0])
iv_call = float(call_row["impliedVolatility"].iloc[0])
put_skew = iv_put - iv_call
result["put_skew"] = round(put_skew, 4)
result["skew_pct"] = round(put_skew * 100, 1)
result["iv_put_25d"] = round(iv_put * 100, 1)
result["iv_call_25d"] = round(iv_call * 100, 1)
if put_skew > 0.03:
result["interpretation"] = "Marché achète des puts — protection baissière élevée"
elif put_skew < -0.02:
result["interpretation"] = "Marché achète des calls — biais haussier spéculatif"
else:
result["interpretation"] = "Skew équilibré — pas de biais directionnel fort"
except Exception as e:
logger.debug(f"[Skew] {proxy}: {e}")
return result
def get_options_flow(ticker: str) -> Dict[str, Any]:
"""
Estimate options flow from open interest changes.
Returns Put/Call OI ratio and unusual strike detection.
"""
proxy = _resolve_ticker(ticker)
result: Dict[str, Any] = {
"ticker": ticker,
"proxy": proxy,
"pc_oi_ratio": None,
"total_call_oi": None,
"total_put_oi": None,
"unusual_strikes": [],
"flow_bias": None,
"gamma_bias": None,
}
try:
t = yf.Ticker(proxy)
price = _get_current_price(t)
if not price:
return result
expirations = t.options
if not expirations:
return result
today = date.today()
# Aggregate OI across the next 90 days of expirations
total_call_oi = 0
total_put_oi = 0
strike_oi: Dict[float, Dict[str, int]] = {}
for exp in expirations[:6]: # limit to first 6 expirations
exp_date = datetime.strptime(exp, "%Y-%m-%d").date()
if (exp_date - today).days > 90:
break
try:
chain = t.option_chain(exp)
total_call_oi += int(chain.calls["openInterest"].fillna(0).sum())
total_put_oi += int(chain.puts["openInterest"].fillna(0).sum())
# Aggregate per strike
for _, row in chain.calls.iterrows():
s = float(row["strike"])
strike_oi.setdefault(s, {"call": 0, "put": 0})
strike_oi[s]["call"] += int(row.get("openInterest") or 0)
for _, row in chain.puts.iterrows():
s = float(row["strike"])
strike_oi.setdefault(s, {"call": 0, "put": 0})
strike_oi[s]["put"] += int(row.get("openInterest") or 0)
except Exception:
continue
if total_call_oi + total_put_oi == 0:
return result
result["total_call_oi"] = total_call_oi
result["total_put_oi"] = total_put_oi
pc_ratio = total_put_oi / total_call_oi if total_call_oi > 0 else None
result["pc_oi_ratio"] = round(pc_ratio, 2) if pc_ratio else None
# Detect unusual strikes (OI > 1.5× median strike OI)
all_oi = [v["call"] + v["put"] for v in strike_oi.values() if v["call"] + v["put"] > 0]
if all_oi:
import statistics
median_oi = statistics.median(all_oi)
threshold = median_oi * 3
unusual = [
{
"strike": round(s, 2),
"call_oi": d["call"],
"put_oi": d["put"],
"total_oi": d["call"] + d["put"],
"type": "call" if d["call"] > d["put"] else "put",
"pct_otm": round((s - price) / price * 100, 1),
}
for s, d in strike_oi.items()
if d["call"] + d["put"] > threshold
]
result["unusual_strikes"] = sorted(unusual, key=lambda x: -x["total_oi"])[:5]
# Net gamma bias: are dealers long or short gamma at current price?
atm_calls_oi = sum(d["call"] for s, d in strike_oi.items() if abs(s - price) / price < 0.03)
atm_puts_oi = sum(d["put"] for s, d in strike_oi.items() if abs(s - price) / price < 0.03)
if atm_calls_oi + atm_puts_oi > 0:
# Dealers are typically short what they sell (opposite of OI)
# More call OI = dealers are short calls = short gamma (amplifies moves)
# More put OI = dealers are short puts = long gamma (dampens moves)
if atm_puts_oi > atm_calls_oi * 1.3:
result["gamma_bias"] = "Dealers long gamma (amortit les mouvements)"
elif atm_calls_oi > atm_puts_oi * 1.3:
result["gamma_bias"] = "Dealers short gamma (amplifie les mouvements)"
else:
result["gamma_bias"] = "Gamma neutre"
if pc_ratio:
if pc_ratio > 1.3:
result["flow_bias"] = "Défensif — smart money achète des puts"
elif pc_ratio < 0.7:
result["flow_bias"] = "Spéculatif — OI calls dominant"
else:
result["flow_bias"] = "Équilibré"
except Exception as e:
logger.debug(f"[OptionsFlow] {proxy}: {e}")
return result
def get_full_iv_snapshot(ticker: str) -> Dict[str, Any]:
"""
Full IV snapshot for a ticker: current IV, rank/percentile, term structure, skew, flow.
Calls DB for historical rank/percentile.
"""
from services.database import get_iv_rank_percentile, save_iv_snapshot
proxy = _resolve_ticker(ticker)
today = date.today().isoformat()
iv_current = get_atm_iv(ticker, target_days=30)
term = get_term_structure(ticker)
skew = get_skew(ticker, target_days=30)
flow = get_options_flow(ticker)
rank_data: Dict[str, Any] = {}
if iv_current:
# Save to history first, then calculate rank
save_iv_snapshot(proxy, today, iv_current, term.get("iv_30d"), term.get("iv_60d"), term.get("iv_90d"))
rank_data = get_iv_rank_percentile(proxy, iv_current)
return {
"ticker": ticker,
"proxy": proxy,
"iv_current_pct": round(iv_current * 100, 1) if iv_current else None,
"iv_rank": rank_data.get("iv_rank"),
"iv_percentile": rank_data.get("iv_percentile"),
"history_days": rank_data.get("history_days", 0),
"iv_min_52w_pct": rank_data.get("iv_min_52w"),
"iv_max_52w_pct": rank_data.get("iv_max_52w"),
"term_structure": term,
"skew": skew,
"options_flow": flow,
"fetched_at": datetime.utcnow().isoformat(),
}
def get_iv_context_for_prompt(tickers: List[str]) -> str:
"""
Build a compact IV context string to inject into GPT-4o scoring prompts.
"""
lines = []
for ticker in tickers[:8]:
proxy = _resolve_ticker(ticker)
iv = get_atm_iv(ticker, 30)
if not iv:
continue
from services.database import get_iv_rank_percentile, save_iv_snapshot
today = date.today().isoformat()
save_iv_snapshot(proxy, today, iv, None, None, None)
rank = get_iv_rank_percentile(proxy, iv)
iv_rank = rank.get("iv_rank")
hist_days = rank.get("history_days", 0)
skew_data = get_skew(ticker, 30)
skew_pct = skew_data.get("skew_pct")
term = get_term_structure(ticker)
structure = term.get("structure")
rank_str = f"IVR={iv_rank:.0f}%" if iv_rank is not None and hist_days >= 30 else "IVR=N/A (historique insuffisant)"
skew_str = f"skew={skew_pct:+.1f}pts" if skew_pct is not None else ""
term_str = f"structure={structure}" if structure else ""
flag = ""
if iv_rank and iv_rank > 80:
flag = " ⚠️ IV ÉLEVÉE — préférer vendre de la vol"
elif iv_rank and iv_rank < 20:
flag = " ✓ IV BASSE — bon moment pour acheter de la convexité"
parts = [f"{ticker}/{proxy}", f"IV={iv*100:.1f}%", rank_str]
if skew_str:
parts.append(skew_str)
if term_str:
parts.append(term_str)
if flag:
parts.append(flag)
lines.append(" " + " | ".join(parts))
if not lines:
return ""
return "=== VOLATILITÉ IMPLICITE ===\n" + "\n".join(lines) + "\n⚠️ RÈGLE: Si IVR>80%, pénaliser les stratégies acheteuses de vol (long call/put, straddle). Si IVR<20%, favoriser l'achat de convexité.\n"

View File

@@ -573,3 +573,43 @@ export const useDeleteKbEntry = () => {
onSuccess: () => qc.invalidateQueries({ queryKey: ['knowledge-entries'] }),
})
}
// ── Options Volatility ────────────────────────────────────────────────────────
export const useIvSnapshot = (ticker: string) =>
useQuery({
queryKey: ['iv-snapshot', ticker],
queryFn: () => api.get(`/options-vol/snapshot/${encodeURIComponent(ticker)}`).then(r => r.data),
enabled: !!ticker,
staleTime: 60 * 60_000, // 1h — IV doesn't change that fast
})
export const useIvWatchlist = () =>
useQuery({
queryKey: ['iv-watchlist'],
queryFn: () => api.get('/options-vol/watchlist').then(r => r.data),
staleTime: 60 * 60_000,
})
export const useIvBatch = (tickers: string) =>
useQuery({
queryKey: ['iv-batch', tickers],
queryFn: () => api.get('/options-vol/batch', { params: { tickers } }).then(r => r.data),
enabled: !!tickers,
staleTime: 60 * 60_000,
})
export const useIvHistory = (ticker: string, days = 90) =>
useQuery({
queryKey: ['iv-history', ticker, days],
queryFn: () => api.get(`/options-vol/history/${encodeURIComponent(ticker)}`, { params: { days } }).then(r => r.data),
enabled: !!ticker,
staleTime: 60 * 60_000,
})
export const useIvForTrade = (underlying: string) =>
useQuery({
queryKey: ['iv-for-trade', underlying],
queryFn: () => api.get(`/options-vol/for-trade/${encodeURIComponent(underlying)}`).then(r => r.data),
enabled: !!underlying,
staleTime: 60 * 60_000,
})

View File

@@ -1,7 +1,7 @@
import { useState, useEffect, useRef } from 'react'
import { BookOpen, TrendingUp, TrendingDown, Activity, AlertTriangle, RefreshCw, Zap, CheckCircle, XCircle, Brain, Trash2, Search, X, ChevronDown, ChevronUp } from 'lucide-react'
import clsx from 'clsx'
import { useJournalSummary, useMacroHistory, useGeoHistory, useTradeMtm, useCycleHistory, useCycleStatus, useTriggerCycle, useTradePostmortem, useAnalyzePostmortem, api } from '../hooks/useApi'
import { useJournalSummary, useMacroHistory, useGeoHistory, useTradeMtm, useCycleHistory, useCycleStatus, useTriggerCycle, useTradePostmortem, useAnalyzePostmortem, useIvForTrade, api } from '../hooks/useApi'
import { useQueryClient } from '@tanstack/react-query'
const SCENARIO_META: Record<string, { label: string; color: string; emoji: string }> = {
@@ -61,6 +61,27 @@ function ScoreDelta({ entry, latest }: { entry: number | null; latest: number |
// ── Section 1 : Historique des régimes macro ──────────────────────────────────
function IvRankCell({ underlying }: { underlying: string }) {
const { data, isLoading } = useIvForTrade(underlying)
if (isLoading) return <span className="text-slate-700 text-[10px]"></span>
const rank = data?.iv_rank
const iv = data?.iv_current_pct
if (rank == null) return <span className="text-slate-700 text-[10px]"></span>
const color = rank >= 80 ? 'text-red-400' : rank >= 50 ? 'text-amber-400' : rank >= 20 ? 'text-emerald-400' : 'text-blue-400'
const signal = rank >= 80 ? '↓vol' : rank < 20 ? '↑vol' : ''
return (
<div className="flex flex-col items-end gap-0.5">
<span className={clsx('text-[10px] font-bold font-mono', color)}>
{rank}%
</span>
<span className="text-[8px] text-slate-600">{iv != null ? `IV ${iv}%` : ''}</span>
{signal && <span className={clsx('text-[8px] font-semibold', color)}>{signal}</span>}
</div>
)
}
function MacroHistorySection({ days }: { days: number }) {
const { data, isLoading, refetch, isFetching } = useMacroHistory(days)
const history: any[] = (data as any)?.history ?? []
@@ -395,6 +416,7 @@ function TradeMtmSection({ days }: { days: number }) {
<th className="text-right px-3 py-2 font-medium">Prix entrée</th>
<th className="text-right px-3 py-2 font-medium">Prix actuel</th>
<th className="text-right px-3 py-2 font-medium">Maturité</th>
<th className="text-right px-3 py-2 font-medium">IV Rank</th>
<th className="text-right px-3 py-2 font-medium">P&L th.</th>
<th className="px-3 py-2 font-medium w-8"></th>
</tr>
@@ -483,6 +505,9 @@ function TradeMtmSection({ days }: { days: number }) {
</span>
)}
</td>
<td className="px-3 py-2 text-right">
<IvRankCell underlying={t.underlying} />
</td>
<td className="px-3 py-2 text-right">
<PnlBadge pnl={t.pnl_pct} />
</td>

View File

@@ -1,341 +1,356 @@
import { useState } from 'react'
import { usePnlCurve } from '../hooks/useApi'
import axios from 'axios'
import { useIvWatchlist, useIvSnapshot, useIvHistory } from '../hooks/useApi'
import { Activity, TrendingUp, TrendingDown, Minus, RefreshCw, ChevronDown, ChevronUp } from 'lucide-react'
import clsx from 'clsx'
import {
LineChart, Line, XAxis, YAxis, Tooltip, ResponsiveContainer,
CartesianGrid, ReferenceLine, Legend,
} from 'recharts'
import { TrendingUp, FlaskConical, Calculator } from 'lucide-react'
import type { PnLPoint } from '../types'
const STRATEGIES = [
{ key: 'long_call', label: 'Long Call', desc: 'Pari haussier, gain illimité, perte limitée à la prime' },
{ key: 'long_put', label: 'Long Put', desc: 'Pari baissier, gain important, perte limitée à la prime' },
{ key: 'bull_call_spread', label: 'Bull Call Spread', desc: 'Haussier modéré, coût réduit, gain plafonné' },
{ key: 'bear_put_spread', label: 'Bear Put Spread', desc: 'Baissier modéré, coût réduit, gain plafonné' },
{ key: 'straddle', label: 'Long Straddle', desc: 'Pari sur la volatilité, direction neutre' },
]
// ── Helpers ───────────────────────────────────────────────────────────────────
const WATCHLIST_QUICK = [
'GLD', 'USO', 'WEAT', 'UNG', 'SPY', 'QQQ', 'GDX', 'COPX',
'XLE', 'FXE', 'UUP', 'XOM', 'LMT',
]
interface Greeks {
price: number; delta: number; gamma: number; theta: number; vega: number; rho: number
underlying_price: number; sigma: number
function ivRankColor(rank: number | null | undefined): string {
if (rank == null) return 'text-slate-500'
if (rank >= 80) return 'text-red-400'
if (rank >= 50) return 'text-amber-400'
if (rank >= 20) return 'text-emerald-400'
return 'text-blue-400'
}
interface SpreadResult {
strategy: string; net_debit: number; max_loss: number; max_gain: number | null
breakeven?: number; breakevens?: number[]; legs: Array<{type: string; strike: number; premium: number}>
underlying_price: number; sigma: number
function ivRankBg(rank: number | null | undefined): string {
if (rank == null) return 'bg-dark-700/30 border-slate-700/30'
if (rank >= 80) return 'bg-red-900/20 border-red-700/30'
if (rank >= 50) return 'bg-amber-900/10 border-amber-700/20'
if (rank >= 20) return 'bg-emerald-900/10 border-emerald-700/20'
return 'bg-blue-900/20 border-blue-700/30'
}
export default function OptionsLab() {
const [strategy, setStrategy] = useState('long_call')
const [symbol, setSymbol] = useState('GLD')
const [strike, setStrike] = useState(200)
const [strikeHigh, setStrikeHigh] = useState(210)
const [expiry, setExpiry] = useState(90)
const [optionType, setOptionType] = useState('call')
const [quantity, setQuantity] = useState(1)
const [result, setResult] = useState<Greeks | SpreadResult | null>(null)
const [pnlData, setPnlData] = useState<PnLPoint[]>([])
const [loading, setLoading] = useState(false)
function ivSignalLabel(rank: number | null | undefined) {
if (rank == null) return null
if (rank >= 80) return { text: 'Vendre de la vol', icon: <TrendingDown className="w-3 h-3" />, cls: 'text-red-400' }
if (rank < 20) return { text: 'Acheter de la vol', icon: <TrendingUp className="w-3 h-3" />, cls: 'text-blue-400' }
return { text: 'Neutre', icon: <Minus className="w-3 h-3" />, cls: 'text-slate-500' }
}
const compute = async () => {
setLoading(true)
try {
let res: Greeks | SpreadResult
if (strategy === 'long_call' || strategy === 'long_put') {
const type = strategy === 'long_call' ? 'call' : 'put'
const r = await axios.get('/api/options/price', {
params: { symbol, strike, expiry_days: expiry, option_type: type }
})
res = r.data as Greeks
const pnl = await axios.get('/api/options/pnl-curve', {
params: { symbol, strike, expiry_days: expiry, option_type: type, quantity, premium_paid: res.price }
})
setPnlData(pnl.data as PnLPoint[])
} else if (strategy === 'bull_call_spread') {
const r = await axios.get('/api/options/strategy/bull-call-spread', {
params: { symbol, strike_low: strike, strike_high: strikeHigh, expiry_days: expiry }
})
res = r.data as SpreadResult
setPnlData([])
} else if (strategy === 'bear_put_spread') {
const r = await axios.get('/api/options/strategy/bear-put-spread', {
params: { symbol, strike_high: strikeHigh, strike_low: strike, expiry_days: expiry }
})
res = r.data as SpreadResult
setPnlData([])
} else {
const r = await axios.get('/api/options/strategy/straddle', {
params: { symbol, strike, expiry_days: expiry }
})
res = r.data as SpreadResult
setPnlData([])
}
setResult(res)
} catch (e) {
console.error(e)
}
setLoading(false)
function StructureBadge({ structure }: { structure: string | null | undefined }) {
if (!structure) return null
const map: Record<string, { label: string; cls: string }> = {
contango: { label: 'Contango ↗', cls: 'text-emerald-400 bg-emerald-900/20 border-emerald-700/30' },
backwardation: { label: 'Backwardation ↘', cls: 'text-red-400 bg-red-900/20 border-red-700/30' },
flat: { label: 'Flat →', cls: 'text-slate-400 bg-slate-800 border-slate-700/30' },
}
const s = map[structure] || { label: structure, cls: 'text-slate-400 bg-slate-800 border-slate-700' }
return <span className={clsx('text-[9px] font-semibold px-1.5 py-0.5 rounded border', s.cls)}>{s.label}</span>
}
const isGreeks = result && 'delta' in result
const isSpread = result && 'net_debit' in result
function IvBar({ rank }: { rank: number | null | undefined }) {
if (rank == null) return <div className="h-1.5 w-full bg-slate-800 rounded-full" />
const color = rank >= 80 ? 'bg-red-500' : rank >= 50 ? 'bg-amber-500' : rank >= 20 ? 'bg-emerald-500' : 'bg-blue-500'
return (
<div className="p-6 space-y-5">
<div>
<h1 className="text-xl font-bold text-white flex items-center gap-2">
<FlaskConical className="w-5 h-5 text-blue-400" /> Options Lab
</h1>
<p className="text-xs text-slate-500 mt-0.5">
Pricer Black-Scholes · Greeks · Stratégies · Courbe P&L
</p>
</div>
<div className="grid grid-cols-3 gap-5">
{/* Left: strategy builder */}
<div className="col-span-1 space-y-4">
{/* Strategy select */}
<div className="card">
<div className="section-title">Stratégie</div>
<div className="space-y-1.5">
{STRATEGIES.map(s => (
<button
key={s.key}
onClick={() => setStrategy(s.key)}
className={clsx('w-full text-left px-3 py-2 rounded border text-xs transition-all', {
'bg-blue-600/20 border-blue-500/60 text-blue-300': strategy === s.key,
'border-slate-700/40 text-slate-400 hover:border-slate-600': strategy !== s.key,
})}
>
<div className="font-semibold">{s.label}</div>
<div className="text-slate-500 mt-0.5">{s.desc}</div>
</button>
))}
</div>
</div>
{/* Parameters */}
<div className="card">
<div className="section-title flex items-center gap-1"><Calculator className="w-3 h-3" /> Paramètres</div>
<div className="space-y-3">
<div>
<label className="text-xs text-slate-500 mb-1 block">Sous-jacent</label>
<div className="flex gap-1 flex-wrap mb-1">
{WATCHLIST_QUICK.map(s => (
<button
key={s}
onClick={() => setSymbol(s)}
className={clsx('px-2 py-0.5 rounded text-xs border', {
'bg-blue-600 border-blue-500 text-white': symbol === s,
'border-slate-700 text-slate-500 hover:border-slate-500': symbol !== s,
})}
>
{s}
</button>
))}
</div>
<input
type="text"
value={symbol}
onChange={e => setSymbol(e.target.value.toUpperCase())}
className="w-full bg-dark-700 border border-slate-700 rounded px-2 py-1.5 text-sm text-white focus:outline-none focus:border-blue-500"
placeholder="Ex: GLD, USO, SPY"
/>
</div>
<div>
<label className="text-xs text-slate-500 mb-1 block">
Strike {strategy === 'bull_call_spread' || strategy === 'bear_put_spread' ? 'bas' : ''}
</label>
<input
type="number"
value={strike}
onChange={e => setStrike(Number(e.target.value))}
className="w-full bg-dark-700 border border-slate-700 rounded px-2 py-1.5 text-sm text-white focus:outline-none focus:border-blue-500"
/>
</div>
{(strategy === 'bull_call_spread' || strategy === 'bear_put_spread') && (
<div>
<label className="text-xs text-slate-500 mb-1 block">Strike haut</label>
<input
type="number"
value={strikeHigh}
onChange={e => setStrikeHigh(Number(e.target.value))}
className="w-full bg-dark-700 border border-slate-700 rounded px-2 py-1.5 text-sm text-white focus:outline-none focus:border-blue-500"
/>
</div>
)}
<div>
<label className="text-xs text-slate-500 mb-1 block">Expiration (jours)</label>
<div className="flex gap-1 mb-1">
{[30, 60, 90, 180].map(d => (
<button
key={d}
onClick={() => setExpiry(d)}
className={clsx('px-2 py-0.5 rounded text-xs border', {
'bg-blue-600 border-blue-500 text-white': expiry === d,
'border-slate-700 text-slate-500 hover:border-slate-500': expiry !== d,
})}
>
{d}j
</button>
))}
</div>
<input
type="number"
value={expiry}
onChange={e => setExpiry(Number(e.target.value))}
className="w-full bg-dark-700 border border-slate-700 rounded px-2 py-1.5 text-sm text-white focus:outline-none focus:border-blue-500"
/>
</div>
{(strategy === 'long_call' || strategy === 'long_put') && (
<div>
<label className="text-xs text-slate-500 mb-1 block">Nb contrats</label>
<input
type="number"
value={quantity}
onChange={e => setQuantity(Number(e.target.value))}
min={1}
className="w-full bg-dark-700 border border-slate-700 rounded px-2 py-1.5 text-sm text-white focus:outline-none focus:border-blue-500"
/>
</div>
)}
<button
onClick={compute}
disabled={loading}
className="w-full bg-blue-600 hover:bg-blue-500 disabled:opacity-50 text-white rounded py-2 text-sm font-semibold transition-colors"
>
{loading ? 'Calcul...' : '⚡ Calculer'}
</button>
</div>
</div>
</div>
{/* Right: results */}
<div className="col-span-2 space-y-4">
{/* Greeks / Spread summary */}
{isGreeks && (
<div className="card">
<div className="section-title">Prix & Greeks {symbol} {strike} {strategy === 'long_call' ? 'Call' : 'Put'} {expiry}j</div>
<div className="grid grid-cols-4 gap-3 mb-4">
{[
{ label: 'Prime', value: `$${result.price.toFixed(4)}`, highlight: true },
{ label: 'Coût (1 contrat)', value: `$${(result.price * 100).toFixed(2)}` },
{ label: 'Spot sous-jacent', value: `$${result.underlying_price?.toFixed(2)}` },
{ label: 'Vol. Réalisée', value: `${((result.sigma ?? 0) * 100).toFixed(1)}%` },
].map(({ label, value, highlight }) => (
<div key={label} className="card-sm text-center">
<div className="stat-label">{label}</div>
<div className={clsx('stat-value text-lg mt-1', highlight && 'text-blue-400')}>{value}</div>
</div>
))}
</div>
<div className="grid grid-cols-5 gap-2">
{[
{ label: 'Delta (Δ)', value: result.delta, desc: 'Sensibilité au prix' },
{ label: 'Gamma (Γ)', value: result.gamma, desc: 'Variation du delta' },
{ label: 'Theta (Θ)', value: result.theta, desc: 'Déclin temporel/j' },
{ label: 'Vega (ν)', value: result.vega, desc: 'Sensibilité à IV' },
{ label: 'Rho (ρ)', value: result.rho, desc: 'Sensibilité aux taux' },
].map(({ label, value, desc }) => (
<div key={label} className="card-sm text-center">
<div className="text-xs text-slate-500">{label}</div>
<div className={clsx('text-base font-bold mt-0.5', {
'text-emerald-400': value > 0,
'text-red-400': value < 0,
'text-slate-400': value === 0,
})}>
{value?.toFixed(4)}
</div>
<div className="text-xs text-slate-600 mt-0.5">{desc}</div>
</div>
))}
</div>
</div>
)}
{isSpread && (
<div className="card">
<div className="section-title">Résumé stratégie {result.strategy}</div>
<div className="grid grid-cols-4 gap-3 mb-4">
{[
{ label: 'Débit net', value: `$${result.net_debit.toFixed(4)}`, color: 'text-red-400' },
{ label: 'Perte max', value: `$${result.max_loss.toFixed(2)}`, color: 'text-red-400' },
{ label: 'Gain max', value: result.max_gain != null ? `$${result.max_gain.toFixed(2)}` : '∞', color: 'text-emerald-400' },
{ label: 'Seuil renta.', value: `$${(result.breakeven ?? (result.breakevens?.[0]) ?? 0).toFixed(2)}`, color: 'text-yellow-400' },
].map(({ label, value, color }) => (
<div key={label} className="card-sm text-center">
<div className="stat-label">{label}</div>
<div className={clsx('text-lg font-bold mt-1', color)}>{value}</div>
</div>
))}
</div>
<div>
<div className="text-xs text-slate-500 mb-2">Jambes de la stratégie</div>
{result.legs.map((leg, i) => (
<div key={i} className="flex items-center gap-3 text-xs py-1.5 border-b border-slate-700/30 last:border-0">
<span className={clsx('badge', leg.type.includes('long') ? 'badge-green' : 'badge-red')}>
{leg.type.toUpperCase()}
</span>
<span className="text-white">Strike: ${leg.strike}</span>
<span className="text-slate-400">Prime: ${leg.premium.toFixed(4)}</span>
</div>
))}
</div>
</div>
)}
{/* P&L Chart */}
{pnlData.length > 0 && (
<div className="card">
<div className="section-title">Courbe P&L à l'expiration</div>
<ResponsiveContainer width="100%" height={250}>
<LineChart data={pnlData}>
<CartesianGrid strokeDasharray="3 3" stroke="#1e2d4d" />
<XAxis dataKey="underlying" tick={{ fill: '#475569', fontSize: 9 }}
tickFormatter={v => `$${v.toFixed(0)}`} />
<YAxis tick={{ fill: '#475569', fontSize: 9 }} tickLine={false} axisLine={false}
tickFormatter={v => `$${v.toFixed(0)}`} />
<Tooltip
contentStyle={{ background: '#0f1623', border: '1px solid #1e2d4d', fontSize: 11 }}
formatter={(v: number) => [`$${v.toFixed(2)}`, 'P&L']}
labelFormatter={v => `Prix sous-jacent: $${Number(v).toFixed(2)}`}
/>
<ReferenceLine y={0} stroke="#475569" strokeDasharray="4 4" />
<ReferenceLine x={strike} stroke="#f59e0b" strokeDasharray="4 4" label={{ value: 'Strike', fill: '#f59e0b', fontSize: 9 }} />
<Line
type="monotone" dataKey="pnl"
stroke="#3b82f6" strokeWidth={2} dot={false}
activeDot={{ r: 4, fill: '#3b82f6' }}
/>
</LineChart>
</ResponsiveContainer>
</div>
)}
{!result && !loading && (
<div className="card h-80 flex items-center justify-center text-slate-600 text-sm">
<div className="text-center">
<FlaskConical className="w-10 h-10 mx-auto mb-3 opacity-20" />
<div>Configurer et calculer une stratégie</div>
<div className="text-xs mt-1">Les résultats apparaîtront ici</div>
</div>
</div>
)}
</div>
</div>
<div className="h-1.5 w-full bg-slate-800 rounded-full overflow-hidden">
<div className={clsx('h-full rounded-full', color)} style={{ width: `${Math.min(rank, 100)}%` }} />
</div>
)
}
function IvSparkline({ history }: { history: { recorded_date: string; iv_current: number }[] }) {
if (!history || history.length < 3) return null
const vals = [...history].reverse().map(h => h.iv_current * 100)
const max = Math.max(...vals)
const min = Math.min(...vals)
const range = max - min || 1
const W = 100, H = 28
const points = vals.map((v, i) => {
const x = (i / (vals.length - 1)) * W
const y = H - ((v - min) / range) * (H - 2) - 1
return `${x.toFixed(1)},${y.toFixed(1)}`
}).join(' ')
return (
<svg width={W} height={H} className="opacity-70">
<polyline points={points} fill="none" stroke="#60a5fa" strokeWidth="1.5" strokeLinejoin="round" />
<circle
cx={W} cy={H - ((vals[vals.length - 1] - min) / range) * (H - 2) - 1}
r="2" fill="#60a5fa" />
</svg>
)
}
// ── Expanded detail panel for one ticker ─────────────────────────────────────
function TickerDetail({ ticker }: { ticker: string }) {
const { data: snap, isLoading } = useIvSnapshot(ticker)
const { data: histData } = useIvHistory(ticker, 90)
if (isLoading) return <div className="h-20 mt-3 animate-pulse bg-dark-700 rounded" />
if (!snap) return null
const ts = (snap.term_structure || {}) as any
const skew = (snap.skew || {}) as any
const flow = (snap.options_flow || {}) as any
const history: any[] = histData?.history || []
return (
<div className="mt-3 border-t border-slate-700/30 pt-3 grid grid-cols-3 gap-4">
{/* Term Structure */}
<div>
<div className="text-[9px] text-slate-500 uppercase tracking-wide font-semibold mb-2">Term Structure</div>
<div className="space-y-1.5">
{([['30j', ts.iv_30d], ['60j', ts.iv_60d], ['90j', ts.iv_90d], ['180j', ts.iv_180d]] as [string, number | undefined][]).map(([label, iv]) =>
iv ? (
<div key={label} className="flex items-center gap-2">
<span className="text-[9px] text-slate-600 w-7 shrink-0">{label}</span>
<div className="flex-1 h-1 bg-slate-800 rounded-full overflow-hidden">
<div className="h-full bg-blue-500/50 rounded-full" style={{ width: `${Math.min(iv * 300, 100)}%` }} />
</div>
<span className="text-[9px] text-slate-300 font-mono w-9 text-right">{(iv * 100).toFixed(1)}%</span>
</div>
) : null
)}
{ts.structure && <div className="mt-1"><StructureBadge structure={ts.structure} /></div>}
</div>
</div>
{/* Skew */}
<div>
<div className="text-[9px] text-slate-500 uppercase tracking-wide font-semibold mb-2">Skew Put/Call</div>
{skew.put_skew != null ? (
<div className="space-y-1">
<div className="flex justify-between text-[9px]">
<span className="text-slate-600">Put 25Δ</span>
<span className="text-slate-300 font-mono">{skew.iv_put_25d}%</span>
</div>
<div className="flex justify-between text-[9px]">
<span className="text-slate-600">Call 25Δ</span>
<span className="text-slate-300 font-mono">{skew.iv_call_25d}%</span>
</div>
<div className={clsx('text-[10px] font-bold mt-1',
skew.skew_pct > 3 ? 'text-red-400' : skew.skew_pct < -2 ? 'text-blue-400' : 'text-slate-400')}>
{skew.skew_pct > 0 ? '+' : ''}{skew.skew_pct} pts
</div>
{skew.interpretation && (
<div className="text-[9px] text-slate-600 italic leading-tight">{skew.interpretation}</div>
)}
</div>
) : (
<div className="text-[9px] text-slate-700 italic">Données insuffisantes</div>
)}
</div>
{/* Options Flow */}
<div>
<div className="text-[9px] text-slate-500 uppercase tracking-wide font-semibold mb-2">Options Flow</div>
{flow.pc_oi_ratio != null ? (
<div className="space-y-1">
<div className="flex justify-between text-[9px]">
<span className="text-slate-600">P/C OI ratio</span>
<span className={clsx('font-mono font-bold',
flow.pc_oi_ratio > 1.3 ? 'text-red-400' : flow.pc_oi_ratio < 0.7 ? 'text-blue-400' : 'text-slate-300')}>
{flow.pc_oi_ratio}
</span>
</div>
{flow.flow_bias && <div className="text-[9px] text-slate-500">{flow.flow_bias}</div>}
{flow.gamma_bias && <div className="text-[9px] text-amber-500/70">{flow.gamma_bias}</div>}
{flow.unusual_strikes?.length > 0 && (
<div className="mt-1 pt-1 border-t border-slate-700/30">
<div className="text-[8px] text-slate-600 mb-0.5">Strikes inhabituels</div>
{flow.unusual_strikes.slice(0, 2).map((s: any) => (
<div key={s.strike} className="text-[8px] font-mono text-slate-500">
{s.strike} {s.type.toUpperCase()} OI={s.total_oi.toLocaleString()}
<span className="text-slate-700"> ({s.pct_otm > 0 ? '+' : ''}{s.pct_otm}%)</span>
</div>
))}
</div>
)}
</div>
) : (
<div className="text-[9px] text-slate-700 italic">Pas d'OI disponible</div>
)}
</div>
{/* History sparkline */}
{history.length > 4 && (
<div className="col-span-3 flex items-center gap-3 pt-1 border-t border-slate-700/20">
<span className="text-[9px] text-slate-600 shrink-0">IV 90j</span>
<IvSparkline history={history} />
<div className="text-[9px] text-slate-600">
min <span className="text-slate-400">{snap.iv_min_52w_pct}%</span>
{' · '}max <span className="text-slate-400">{snap.iv_max_52w_pct}%</span>
{' · '}{snap.history_days}j d'historique
</div>
</div>
)}
</div>
)
}
// ── Watchlist Row ─────────────────────────────────────────────────────────────
function WatchlistRow({ item }: { item: any }) {
const [expanded, setExpanded] = useState(false)
const signal = ivSignalLabel(item.iv_rank)
return (
<div className={clsx('rounded-lg border transition-all', ivRankBg(item.iv_rank))}>
<div
className="flex items-center gap-3 px-3 py-2.5 cursor-pointer"
onClick={() => setExpanded(!expanded)}
>
<div className="w-14 shrink-0">
<div className="text-sm font-bold text-slate-200">{item.ticker}</div>
{item.proxy && item.proxy !== item.ticker && (
<div className="text-[8px] text-slate-700"> {item.proxy}</div>
)}
</div>
<div className="w-16 shrink-0">
<span className={clsx('text-sm font-bold font-mono', ivRankColor(item.iv_rank))}>
{item.iv_current_pct != null ? `${item.iv_current_pct}%` : '—'}
</span>
<div className="text-[8px] text-slate-600">IV actuelle</div>
</div>
<div className="flex-1 min-w-0 space-y-1">
<div className="flex items-center justify-between">
<span className="text-[9px] text-slate-600">IV Rank</span>
<span className={clsx('text-[10px] font-bold font-mono', ivRankColor(item.iv_rank))}>
{item.iv_rank != null ? `${item.iv_rank}%` : 'N/A'}
</span>
</div>
<IvBar rank={item.iv_rank} />
</div>
<div className="w-16 text-right shrink-0">
<div className="text-[8px] text-slate-600">Pctile</div>
<div className={clsx('text-[10px] font-mono font-bold', ivRankColor(item.iv_rank))}>
{item.iv_percentile != null ? `${item.iv_percentile}%` : '—'}
</div>
</div>
<div className="w-32 text-right shrink-0">
{signal && (
<div className={clsx('flex items-center justify-end gap-1 text-[10px] font-semibold', signal.cls)}>
{signal.icon} {signal.text}
</div>
)}
{item.history_days > 0 && (
<div className="text-[8px] text-slate-700">{item.history_days}j historique</div>
)}
</div>
<div className="text-slate-700">
{expanded ? <ChevronUp className="w-3.5 h-3.5" /> : <ChevronDown className="w-3.5 h-3.5" />}
</div>
</div>
{expanded && (
<div className="px-3 pb-3">
<TickerDetail ticker={item.proxy || item.ticker} />
</div>
)}
</div>
)
}
// ── Main page ─────────────────────────────────────────────────────────────────
export default function OptionsLab() {
const { data, isLoading, refetch, isFetching } = useIvWatchlist()
const items: any[] = data?.items || []
const sellVol = items.filter(i => (i.iv_rank ?? 50) >= 80)
const buyVol = items.filter(i => i.iv_rank != null && i.iv_rank < 20)
const neutral = items.filter(i => i.iv_rank != null && i.iv_rank >= 20 && i.iv_rank < 80)
const noData = items.filter(i => i.iv_rank == null)
return (
<div className="p-6 space-y-5">
<div className="flex items-center justify-between">
<div>
<h1 className="text-xl font-bold text-white flex items-center gap-2">
<Activity className="w-5 h-5 text-blue-400" /> Options Lab
</h1>
<p className="text-xs text-slate-500 mt-0.5">
IV Rank · IV Percentile · Term Structure · Skew · Options Flow
</p>
</div>
<button
onClick={() => refetch()}
disabled={isFetching}
className="flex items-center gap-1.5 text-xs border border-slate-600 text-slate-400 hover:text-slate-200 hover:border-slate-500 px-3 py-1.5 rounded transition-all disabled:opacity-50"
>
<RefreshCw className={clsx('w-3.5 h-3.5', isFetching && 'animate-spin')} />
{isFetching ? 'Chargement...' : 'Actualiser'}
</button>
</div>
{/* Légende */}
<div className="grid grid-cols-2 gap-3">
<div className="card bg-red-900/10 border-red-700/20 text-xs">
<div className="flex items-center gap-2 mb-1">
<TrendingDown className="w-3.5 h-3.5 text-red-400" />
<span className="font-semibold text-red-400">IV Rank &gt; 80% Vendre de la vol</span>
</div>
<div className="text-slate-500 leading-snug">Credit spreads, iron condors, covered calls. La prime est élevée avantage au vendeur.</div>
</div>
<div className="card bg-blue-900/10 border-blue-700/20 text-xs">
<div className="flex items-center gap-2 mb-1">
<TrendingUp className="w-3.5 h-3.5 text-blue-400" />
<span className="font-semibold text-blue-400">IV Rank &lt; 20% Acheter de la convexité</span>
</div>
<div className="text-slate-500 leading-snug">Long calls, long puts, straddles, strangles. La prime est cheap bon moment pour acheter du gamma.</div>
</div>
</div>
<div className="text-[10px] text-slate-600 -mt-2 px-1">
<strong className="text-slate-500">Contango</strong> = IV courte &lt; longue (marché calme, bon pour vendre du court terme) ·
<strong className="text-slate-500"> Backwardation</strong> = IV courte &gt; longue (stress, bon pour acheter de la protection courte) ·
<strong className="text-slate-500"> Skew +</strong> = puts plus chers que calls (biais baissier institutionnel)
</div>
{isLoading ? (
<div className="space-y-2">
{[...Array(6)].map((_, i) => <div key={i} className="h-12 animate-pulse bg-dark-700 rounded-lg" />)}
</div>
) : items.length === 0 ? (
<div className="card text-center py-12 text-slate-600">
<Activity className="w-8 h-8 mx-auto mb-3 opacity-30" />
<div className="font-semibold mb-1 text-slate-500">Aucune donnée IV disponible</div>
<div className="text-xs text-slate-600">
Les ETFs US sont requis (SPY, QQQ, GLD...).
<br />L'IV Rank nécessite de l'historique les données s'accumulent à chaque actualisation.
</div>
<button onClick={() => refetch()} disabled={isFetching}
className="mt-4 text-xs bg-blue-600 hover:bg-blue-500 text-white px-4 py-1.5 rounded disabled:opacity-50">
Lancer la collecte
</button>
</div>
) : (
<div className="space-y-5">
{sellVol.length > 0 && (
<div>
<div className="text-xs font-semibold text-red-400 uppercase tracking-wide mb-2 flex items-center gap-1.5">
<TrendingDown className="w-3.5 h-3.5" /> Vendre de la vol IVR &gt; 80% ({sellVol.length})
</div>
<div className="space-y-1.5">{sellVol.map(item => <WatchlistRow key={item.ticker} item={item} />)}</div>
</div>
)}
{buyVol.length > 0 && (
<div>
<div className="text-xs font-semibold text-blue-400 uppercase tracking-wide mb-2 flex items-center gap-1.5">
<TrendingUp className="w-3.5 h-3.5" /> Acheter de la vol IVR &lt; 20% ({buyVol.length})
</div>
<div className="space-y-1.5">{buyVol.map(item => <WatchlistRow key={item.ticker} item={item} />)}</div>
</div>
)}
{neutral.length > 0 && (
<div>
<div className="text-xs font-semibold text-slate-500 uppercase tracking-wide mb-2 flex items-center gap-1.5">
<Minus className="w-3.5 h-3.5" /> Zone neutre IVR 2080% ({neutral.length})
</div>
<div className="space-y-1.5">{neutral.map(item => <WatchlistRow key={item.ticker} item={item} />)}</div>
</div>
)}
{noData.length > 0 && (
<div>
<div className="text-xs text-slate-700 uppercase tracking-wide mb-2">Sans historique ({noData.length})</div>
<div className="space-y-1.5">{noData.map(item => <WatchlistRow key={item.ticker} item={item} />)}</div>
</div>
)}
</div>
)}
</div>
)
}