diff --git a/backend/services/database.py b/backend/services/database.py index 7e2da92..98a546b 100644 --- a/backend/services/database.py +++ b/backend/services/database.py @@ -207,6 +207,8 @@ def init_db(): ("last_seen_at", "TEXT"), ("pnl_pct", "REAL"), ("capital_invested", "REAL"), + ("strike_guidance", "TEXT"), + ("expiry_days_at_entry", "INTEGER"), ]: try: c.execute(f"ALTER TABLE trade_entry_prices ADD COLUMN {col} {definition}") @@ -1008,6 +1010,16 @@ def log_trade_entries(run_id: str, scored_patterns: List[Dict[str, Any]], quotes _orig.get("horizon_days") or 90 ) + strike_guidance = ( + trade.get("strike_guidance") or + sp.get("recommended_trade", {}).get("strike_guidance") or + None + ) + expiry_days_entry = int( + trade.get("expiry_days") or + sp.get("recommended_trade", {}).get("expiry_days") or + horizon + ) existing_row = conn.execute( "SELECT id FROM trade_entry_prices WHERE pattern_id=? AND underlying=? AND strategy=?", @@ -1025,12 +1037,14 @@ def log_trade_entries(run_id: str, scored_patterns: List[Dict[str, Any]], quotes (run_id, pattern_id, pattern_name, underlying, strategy, entry_price, entry_date, score_at_entry, latest_score, expected_move_pct, horizon_days, ev_at_entry, ev_net, - trade_score, matched_profile, last_seen_at) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""", ( + trade_score, matched_profile, last_seen_at, + strike_guidance, expiry_days_at_entry) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""", ( run_id, pid, pattern_name, ticker_key, strategy, entry_price, today, eff_score, eff_score, exp_move, horizon, ev_gross, ev_net, trade_score, matched, now_ts, + strike_guidance, expiry_days_entry, )) inserted_count += 1 _log.info(f"[TradeLog] NEW trade: pattern='{pattern_name}' {underlying} {strategy} score={eff_score} gain={exp_move:.0f}% profile='{matched}' price={entry_price}") diff --git a/frontend/src/pages/Dashboard.tsx b/frontend/src/pages/Dashboard.tsx index 26051c1..5893819 100644 --- a/frontend/src/pages/Dashboard.tsx +++ b/frontend/src/pages/Dashboard.tsx @@ -5,7 +5,7 @@ import { useScorePatterns, useLastScores, useAllPatterns, useMacroRegime, usePortfolioPositions, useTradeMtm, useRiskProfiles, useRiskDashboard, } from '../hooks/useApi' -import { Target, Clock, Brain, Globe, Plus, RefreshCw, ChevronDown, ChevronUp, CheckCircle2, ShieldAlert, LayoutGrid, List } from 'lucide-react' +import { Target, Clock, Brain, Globe, Plus, RefreshCw, ChevronDown, ChevronUp, CheckCircle2, ShieldAlert, LayoutGrid, List, Terminal } from 'lucide-react' import clsx from 'clsx' import type { Quote } from '../types' import { format } from 'date-fns' @@ -165,11 +165,159 @@ const BIAS_DISPLAY: Record = { 'defensive':{ label: '⚠ Défensif', color: '#f59e0b' }, } +// ── IBKR Ticket ────────────────────────────────────────────────────────────── + +function computeStrikeDollars(price: number, guidance: string, strategy: string): number { + const g = guidance.toUpperCase() + const match = g.match(/(\d+)\s*%\s*OTM/) + if (!match) return price // ATM + const pct = parseInt(match[1]) / 100 + const isBearish = /put|bear/i.test(strategy) + const raw = isBearish ? price * (1 - pct) : price * (1 + pct) + // Round to sensible increment + const inc = price > 1000 ? 10 : price > 200 ? 5 : price > 50 ? 1 : 0.5 + return Math.round(raw / inc) * inc +} + +function estimateExpiryDate(horizonDays: number): Date { + const d = new Date() + d.setDate(d.getDate() + horizonDays) + // Advance to nearest Friday (IBKR standard option expiry) + const dow = d.getDay() + if (dow !== 5) d.setDate(d.getDate() + ((5 - dow + 7) % 7)) + return d +} + +function buildLegs(strategy: string, strike: number | null, price: number | null) { + const s = strategy.toLowerCase() + const atm = price ? (() => { + const inc = price > 1000 ? 10 : price > 200 ? 5 : price > 50 ? 1 : 0.5 + return Math.round(price / inc) * inc + })() : null + if (s.includes('bull call spread')) return [ + { action: 'BUY', type: 'CALL', strike: atm }, + { action: 'SELL', type: 'CALL', strike }, + ] + if (s.includes('bear put spread')) return [ + { action: 'BUY', type: 'PUT', strike: atm }, + { action: 'SELL', type: 'PUT', strike }, + ] + if (s.includes('straddle')) return [ + { action: 'BUY', type: 'CALL', strike: atm ?? strike }, + { action: 'BUY', type: 'PUT', strike: atm ?? strike }, + ] + if (s.includes('strangle')) return [ + { action: 'BUY', type: 'CALL', strike: price ? Math.round(price * 1.05) : strike }, + { action: 'BUY', type: 'PUT', strike: price ? Math.round(price * 0.95) : strike }, + ] + if (s.includes('call')) return [{ action: 'BUY', type: 'CALL', strike }] + if (s.includes('put')) return [{ action: 'BUY', type: 'PUT', strike }] + return [{ action: 'BUY', type: strategy.toUpperCase(), strike }] +} + +function IBKRTicket({ strategy, underlying, strikeGuidance, expiryDays, entryPrice, maxLoss, targetGain, timing, invalidation }: { + strategy: string + underlying: string + strikeGuidance?: string | null + expiryDays?: number | null + entryPrice?: number | null + maxLoss?: number | null + targetGain?: number | null + timing?: string | null + invalidation?: string | null +}) { + const strike = entryPrice && strikeGuidance + ? computeStrikeDollars(entryPrice, strikeGuidance, strategy) + : (entryPrice ?? null) + const expiryDate = expiryDays ? estimateExpiryDate(expiryDays) : null + const legs = buildLegs(strategy, strike, entryPrice ?? null) + const fmtStrike = (s: number | null | undefined) => + s == null ? '—' : s >= 100 ? `$${s.toFixed(0)}` : `$${s.toFixed(2)}` + + return ( +
+
+ Ticket IBKR + {!entryPrice && Prix non disponible — vérifier dans IBKR} +
+ + {/* Header: ticker + type */} +
+
+
Sous-jacent
+
{underlying}
+
Security Type: OPT
+
+
+
Stratégie
+
{strategy}
+ {entryPrice &&
Sous-jacent: ${entryPrice.toFixed(2)}
} +
+
+
Expiration
+
+ {expiryDate ? format(expiryDate, 'd MMM yyyy', { locale: fr }) : expiryDays ? `~${expiryDays}j` : '—'} +
+
{expiryDays ? `${expiryDays}j DTE` : ''}
+
+
+ + {/* Legs */} +
+ {legs.map((leg, i) => ( +
+ {leg.action} + 1 contrat + {leg.type} + Strike + {fmtStrike(leg.strike)} + {leg.strike === null && strikeGuidance && ( + ({strikeGuidance}) + )} +
+ ))} +
+ + {/* Order params */} +
+
+
Type d'ordre
+
LIMIT
+
Prix = prime dans IBKR
+
+
+
Budget max
+
{maxLoss ? `${Math.abs(maxLoss).toFixed(0)}€` : '~1 000€'}
+
Perte max estimée
+
+
+
Cible
+
{targetGain ? `+${targetGain.toFixed(0)}€` : '—'}
+
+
+ + {/* Timing / invalidation */} + {timing && ( +
+ + {timing} +
+ )} + {invalidation && ( +
+ + {invalidation} +
+ )} +
+ ) +} + function TradeCard({ item, onAdd, macroInfo, addedInfo, profiles }: { item: TradeItem onAdd: (item: TradeItem) => void macroInfo?: { dominant: string; label: string; color: string; emoji: string; assetBias: Record } | null - addedInfo?: { entry_date: string; expiry_days?: number } | null + addedInfo?: { entry_date: string; expiry_days?: number; entry_price?: number; strike_guidance?: string } | null profiles?: any[] }) { const [expanded, setExpanded] = useState(false) @@ -392,7 +540,7 @@ function TradeRow({ item, onAdd, macroInfo, addedInfo, profiles, rank }: { item: TradeItem onAdd: (item: TradeItem) => void macroInfo?: { dominant: string; label: string; color: string; emoji: string; assetBias: Record } | null - addedInfo?: { entry_date: string; expiry_days?: number } | null + addedInfo?: { entry_date: string; expiry_days?: number; entry_price?: number; strike_guidance?: string } | null profiles?: any[] rank: number }) { @@ -529,56 +677,72 @@ function TradeRow({ item, onAdd, macroInfo, addedInfo, profiles, rank }: { {expanded && ( -
- {/* Col 1: score justification */} -
- {scoreInfo?.key_catalyst && ( -
- 🔑 -

{scoreInfo.key_catalyst}

-
- )} - {rankRationale &&

{rankRationale}

} - {scoreInfo?.summary &&

{scoreInfo.summary}

} - {scoreInfo?.buckets?.length > 0 && ( - - )} -
- {/* Col 2: actions + contra + timing */} -
- {scoreInfo?.contra_signals?.length > 0 && ( -
-
Contra signals
- {scoreInfo.contra_signals.slice(0, 3).map((cs: any, i: number) => ( -
{cs.title ?? cs}
- ))} -
- )} - {macroInfo && macroInfo.dominant !== 'incertain' && ( -
- {macroInfo.emoji} {macroInfo.label} - · - {bd.label} -
- )} - {addedInfo && ( -
- - Ajouté le {format(new Date(addedInfo.entry_date), "d MMM yyyy", { locale: fr })} -
- )} - +
+
+ {/* Col 1: score justification */} +
+ {scoreInfo?.key_catalyst && ( +
+ 🔑 +

{scoreInfo.key_catalyst}

+
+ )} + {rankRationale &&

{rankRationale}

} + {scoreInfo?.summary &&

{scoreInfo.summary}

} + {scoreInfo?.buckets?.length > 0 && ( + + )} +
+ {/* Col 2: contra + macro + add button */} +
+ {scoreInfo?.contra_signals?.length > 0 && ( +
+
Contra signals
+ {scoreInfo.contra_signals.slice(0, 3).map((cs: any, i: number) => ( +
{cs.title ?? cs}
+ ))} +
+ )} + {macroInfo && macroInfo.dominant !== 'incertain' && ( +
+ {macroInfo.emoji} {macroInfo.label} + · + {bd.label} +
+ )} + {addedInfo && ( +
+ + Ajouté le {format(new Date(addedInfo.entry_date), "d MMM yyyy", { locale: fr })} +
+ )} + +
+ {/* IBKR Ticket — full width below the analysis */} + {trade.strategy && trade.underlying && ( + + )}
@@ -653,13 +817,16 @@ export default function Dashboard() { // Fallback map from cycle-auto-logged trades (trade_entry_prices), keyed by pattern_id const mtmMap = useMemo(() => { - const map: Record = {} + const map: Record = {} for (const t of ((tradeMtmData as any)?.trades ?? [])) { const pid = t.pattern_id as string if (!pid) continue const entry_date = (t.entry_date as string) ?? '' - const expiry_days = (t.horizon_days as number) ?? undefined - if (!map[pid] || entry_date > map[pid].entry_date) map[pid] = { entry_date, expiry_days } + const expiry_days = (t.expiry_days_at_entry ?? t.horizon_days) as number | undefined + const entry_price = (t.entry_price as number) ?? undefined + const strike_guidance = (t.strike_guidance as string) ?? undefined + if (!map[pid] || entry_date > map[pid].entry_date) + map[pid] = { entry_date, expiry_days, entry_price, strike_guidance } } return map }, [tradeMtmData]) diff --git a/frontend/src/pages/JournalDeBord.tsx b/frontend/src/pages/JournalDeBord.tsx index 0d3d7f8..14c9380 100644 --- a/frontend/src/pages/JournalDeBord.tsx +++ b/frontend/src/pages/JournalDeBord.tsx @@ -1,8 +1,10 @@ import { useState, useEffect, useRef, Fragment } from 'react' -import { BookOpen, TrendingUp, TrendingDown, Activity, AlertTriangle, RefreshCw, Zap, CheckCircle, XCircle, Brain, Trash2, Search, X, ChevronDown, ChevronUp } from 'lucide-react' +import { BookOpen, TrendingUp, TrendingDown, Activity, AlertTriangle, RefreshCw, Zap, CheckCircle, XCircle, Brain, Trash2, Search, X, ChevronDown, ChevronUp, Terminal } from 'lucide-react' import clsx from 'clsx' import { useJournalSummary, useMacroHistory, useGeoHistory, useTradeMtm, useCycleHistory, useCycleStatus, useTriggerCycle, useTradePostmortem, useAnalyzePostmortem, useIvForTrade, useKellySizing, api } from '../hooks/useApi' import { useQueryClient } from '@tanstack/react-query' +import { format } from 'date-fns' +import { fr } from 'date-fns/locale' const SCENARIO_META: Record = { goldilocks: { label: 'Goldilocks', color: '#22c55e', emoji: '🌟' }, @@ -99,6 +101,133 @@ function KellyCell({ patternId, capital = 10000 }: { patternId: string; capital? ) } +// ── IBKR Ticket helpers ─────────────────────────────────────────────────────── + +function computeStrikeDollars(price: number, guidance: string, strategy: string): number { + const g = guidance.toUpperCase() + const match = g.match(/(\d+)\s*%\s*OTM/) + if (!match) return price + const pct = parseInt(match[1]) / 100 + const isBearish = /put|bear/i.test(strategy) + const raw = isBearish ? price * (1 - pct) : price * (1 + pct) + const inc = price > 1000 ? 10 : price > 200 ? 5 : price > 50 ? 1 : 0.5 + return Math.round(raw / inc) * inc +} + +function estimateExpiryDate(horizonDays: number): Date { + const d = new Date() + d.setDate(d.getDate() + horizonDays) + const dow = d.getDay() + if (dow !== 5) d.setDate(d.getDate() + ((5 - dow + 7) % 7)) + return d +} + +function buildLegs(strategy: string, strike: number | null, price: number | null) { + const s = strategy.toLowerCase() + const atm = price ? (() => { + const inc = price > 1000 ? 10 : price > 200 ? 5 : price > 50 ? 1 : 0.5 + return Math.round(price / inc) * inc + })() : null + if (s.includes('bull call spread')) return [ + { action: 'BUY', type: 'CALL', strike: atm }, + { action: 'SELL', type: 'CALL', strike }, + ] + if (s.includes('bear put spread')) return [ + { action: 'BUY', type: 'PUT', strike: atm }, + { action: 'SELL', type: 'PUT', strike }, + ] + if (s.includes('straddle')) return [ + { action: 'BUY', type: 'CALL', strike: atm ?? strike }, + { action: 'BUY', type: 'PUT', strike: atm ?? strike }, + ] + if (s.includes('strangle')) return [ + { action: 'BUY', type: 'CALL', strike: price ? Math.round(price * 1.05) : strike }, + { action: 'BUY', type: 'PUT', strike: price ? Math.round(price * 0.95) : strike }, + ] + if (s.includes('call')) return [{ action: 'BUY', type: 'CALL', strike }] + if (s.includes('put')) return [{ action: 'BUY', type: 'PUT', strike }] + return [{ action: 'BUY', type: strategy.toUpperCase(), strike }] +} + +function IBKRTicket({ strategy, underlying, strikeGuidance, expiryDays, entryPrice, maxLoss, targetGain }: { + strategy: string + underlying: string + strikeGuidance?: string | null + expiryDays?: number | null + entryPrice?: number | null + maxLoss?: number | null + targetGain?: number | null +}) { + const strike = entryPrice && strikeGuidance + ? computeStrikeDollars(entryPrice, strikeGuidance, strategy) + : (entryPrice ?? null) + const expiryDate = expiryDays ? estimateExpiryDate(expiryDays) : null + const legs = buildLegs(strategy, strike, entryPrice ?? null) + const fmtStrike = (s: number | null | undefined) => + s == null ? '—' : s >= 100 ? `$${s.toFixed(0)}` : `$${s.toFixed(2)}` + + return ( +
+
+ Ticket IBKR + {!entryPrice && Prix non disponible — vérifier dans IBKR} +
+ +
+
+
Sous-jacent
+
{underlying}
+
Security Type: OPT
+
+
+
Stratégie
+
{strategy}
+ {entryPrice &&
Sous-jacent: ${entryPrice.toFixed(2)}
} +
+
+
Expiration
+
+ {expiryDate ? format(expiryDate, 'd MMM yyyy', { locale: fr }) : expiryDays ? `~${expiryDays}j` : '—'} +
+
{expiryDays ? `${expiryDays}j DTE` : ''}
+
+
+ +
+ {legs.map((leg, i) => ( +
+ {leg.action} + 1 contrat + {leg.type} + Strike + {fmtStrike(leg.strike)} + {leg.strike === null && strikeGuidance && ( + ({strikeGuidance}) + )} +
+ ))} +
+ +
+
+
Type d'ordre
+
LIMIT
+
Prix = prime dans IBKR
+
+
+
Budget max
+
{maxLoss ? `${Math.abs(maxLoss).toFixed(0)}€` : '~1 000€'}
+
Perte max estimée
+
+
+
Cible
+
{targetGain ? `+${targetGain.toFixed(0)}€` : '—'}
+
+
+
+ ) +} + function MacroHistorySection({ days }: { days: number }) { const { data, isLoading, refetch, isFetching } = useMacroHistory(days) const history: any[] = (data as any)?.history ?? [] @@ -424,6 +553,8 @@ function TradeMtmSection({ days }: { days: number }) { Pattern Profil Stratégie + Strike + DTE Ticker Score Trade Score @@ -464,6 +595,12 @@ function TradeMtmSection({ days }: { days: number }) { {t.direction === 'bearish' ? '🐻' : '🐂'} {t.strategy || '—'} + + {t.strike_guidance ?? '—'} + + + {t.expiry_days_at_entry != null ? `${t.expiry_days_at_entry}j` : t.horizon_days != null ? `${t.horizon_days}j` : '—'} + {t.underlying} @@ -554,7 +691,16 @@ function TradeMtmSection({ days }: { days: number }) { {isExpanded && ( - + + setSelectedTradeId(null)} />