feat: IBKR ticket in Dashboard + Journal MtM (Strike, DTE, legs)

Adds full Interactive Brokers order ticket to both the Dashboard cockpit
and the Journal de Bord MtM expanded rows. Each ticket shows the
underlying, computed strike in dollars, estimated expiry date (nearest
Friday), per-leg BUY/SELL CALL/PUT breakdown, order type LIMIT, budget
and target.

Also adds Strike and DTE columns to the MtM table and persists
strike_guidance + expiry_days_at_entry in trade_entry_prices.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
OpenSquared
2026-06-18 13:54:29 +02:00
parent 05a475fb04
commit 18b3ae6f91
3 changed files with 386 additions and 59 deletions

View File

@@ -207,6 +207,8 @@ def init_db():
("last_seen_at", "TEXT"), ("last_seen_at", "TEXT"),
("pnl_pct", "REAL"), ("pnl_pct", "REAL"),
("capital_invested", "REAL"), ("capital_invested", "REAL"),
("strike_guidance", "TEXT"),
("expiry_days_at_entry", "INTEGER"),
]: ]:
try: try:
c.execute(f"ALTER TABLE trade_entry_prices ADD COLUMN {col} {definition}") 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 _orig.get("horizon_days") or
90 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( existing_row = conn.execute(
"SELECT id FROM trade_entry_prices WHERE pattern_id=? AND underlying=? AND strategy=?", "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, (run_id, pattern_id, pattern_name, underlying, strategy,
entry_price, entry_date, score_at_entry, latest_score, entry_price, entry_date, score_at_entry, latest_score,
expected_move_pct, horizon_days, ev_at_entry, ev_net, expected_move_pct, horizon_days, ev_at_entry, ev_net,
trade_score, matched_profile, last_seen_at) trade_score, matched_profile, last_seen_at,
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""", ( strike_guidance, expiry_days_at_entry)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""", (
run_id, pid, pattern_name, ticker_key, strategy, run_id, pid, pattern_name, ticker_key, strategy,
entry_price, today, eff_score, eff_score, entry_price, today, eff_score, eff_score,
exp_move, horizon, ev_gross, ev_net, exp_move, horizon, ev_gross, ev_net,
trade_score, matched, now_ts, trade_score, matched, now_ts,
strike_guidance, expiry_days_entry,
)) ))
inserted_count += 1 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}") _log.info(f"[TradeLog] NEW trade: pattern='{pattern_name}' {underlying} {strategy} score={eff_score} gain={exp_move:.0f}% profile='{matched}' price={entry_price}")

View File

@@ -5,7 +5,7 @@ import {
useScorePatterns, useLastScores, useAllPatterns, useMacroRegime, useScorePatterns, useLastScores, useAllPatterns, useMacroRegime,
usePortfolioPositions, useTradeMtm, useRiskProfiles, useRiskDashboard, usePortfolioPositions, useTradeMtm, useRiskProfiles, useRiskDashboard,
} from '../hooks/useApi' } 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 clsx from 'clsx'
import type { Quote } from '../types' import type { Quote } from '../types'
import { format } from 'date-fns' import { format } from 'date-fns'
@@ -165,11 +165,159 @@ const BIAS_DISPLAY: Record<string, { label: string; color: string }> = {
'defensive':{ label: '⚠ Défensif', color: '#f59e0b' }, '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 (
<div className="bg-blue-950/20 border border-blue-700/30 rounded p-3 space-y-2.5">
<div className="text-[10px] font-bold text-blue-300 uppercase tracking-wide flex items-center gap-1.5">
<Terminal className="w-3 h-3" /> Ticket IBKR
{!entryPrice && <span className="ml-auto text-slate-600 font-normal normal-case">Prix non disponible vérifier dans IBKR</span>}
</div>
{/* Header: ticker + type */}
<div className="grid grid-cols-3 gap-3 text-[10px]">
<div>
<div className="text-slate-600 mb-0.5">Sous-jacent</div>
<div className="text-white font-mono font-bold text-sm">{underlying}</div>
<div className="text-slate-600 text-[9px]">Security Type: OPT</div>
</div>
<div>
<div className="text-slate-600 mb-0.5">Stratégie</div>
<div className="text-slate-200 font-semibold">{strategy}</div>
{entryPrice && <div className="text-slate-600 text-[9px]">Sous-jacent: ${entryPrice.toFixed(2)}</div>}
</div>
<div>
<div className="text-slate-600 mb-0.5">Expiration</div>
<div className="text-slate-200 font-mono">
{expiryDate ? format(expiryDate, 'd MMM yyyy', { locale: fr }) : expiryDays ? `~${expiryDays}j` : '—'}
</div>
<div className="text-slate-600 text-[9px]">{expiryDays ? `${expiryDays}j DTE` : ''}</div>
</div>
</div>
{/* Legs */}
<div className="space-y-1">
{legs.map((leg, i) => (
<div key={i} className="flex items-center gap-2 bg-slate-900/70 rounded px-2.5 py-1.5 font-mono text-[10px]">
<span className={clsx('w-8 font-bold', leg.action === 'BUY' ? 'text-emerald-400' : 'text-red-400')}>{leg.action}</span>
<span className="text-slate-400">1 contrat</span>
<span className={clsx('font-bold w-10 text-center', leg.type === 'CALL' ? 'text-blue-300' : leg.type === 'PUT' ? 'text-orange-300' : 'text-slate-300')}>{leg.type}</span>
<span className="text-slate-600">Strike</span>
<span className="text-yellow-300 font-bold">{fmtStrike(leg.strike)}</span>
{leg.strike === null && strikeGuidance && (
<span className="text-slate-500">({strikeGuidance})</span>
)}
</div>
))}
</div>
{/* Order params */}
<div className="grid grid-cols-3 gap-3 text-[10px]">
<div>
<div className="text-slate-600 mb-0.5">Type d'ordre</div>
<div className="text-slate-300 font-semibold">LIMIT</div>
<div className="text-slate-600 text-[9px]">Prix = prime dans IBKR</div>
</div>
<div>
<div className="text-slate-600 mb-0.5">Budget max</div>
<div className="text-slate-300 font-mono">{maxLoss ? `${Math.abs(maxLoss).toFixed(0)}€` : '~1 000'}</div>
<div className="text-slate-600 text-[9px]">Perte max estimée</div>
</div>
<div>
<div className="text-slate-600 mb-0.5">Cible</div>
<div className="text-emerald-400 font-mono">{targetGain ? `+${targetGain.toFixed(0)}€` : ''}</div>
</div>
</div>
{/* Timing / invalidation */}
{timing && (
<div className="flex gap-1.5 text-[10px] bg-yellow-900/10 border border-yellow-700/20 rounded px-2 py-1.5">
<span className="text-yellow-400 shrink-0">⏱</span>
<span className="text-yellow-200/70 leading-snug">{timing}</span>
</div>
)}
{invalidation && (
<div className="flex gap-1.5 text-[10px] bg-red-900/10 border border-red-700/20 rounded px-2 py-1.5">
<span className="text-red-400 shrink-0">✗</span>
<span className="text-red-200/70 leading-snug">{invalidation}</span>
</div>
)}
</div>
)
}
function TradeCard({ item, onAdd, macroInfo, addedInfo, profiles }: { function TradeCard({ item, onAdd, macroInfo, addedInfo, profiles }: {
item: TradeItem item: TradeItem
onAdd: (item: TradeItem) => void onAdd: (item: TradeItem) => void
macroInfo?: { dominant: string; label: string; color: string; emoji: string; assetBias: Record<string, string> } | null macroInfo?: { dominant: string; label: string; color: string; emoji: string; assetBias: Record<string, string> } | 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[] profiles?: any[]
}) { }) {
const [expanded, setExpanded] = useState(false) const [expanded, setExpanded] = useState(false)
@@ -392,7 +540,7 @@ function TradeRow({ item, onAdd, macroInfo, addedInfo, profiles, rank }: {
item: TradeItem item: TradeItem
onAdd: (item: TradeItem) => void onAdd: (item: TradeItem) => void
macroInfo?: { dominant: string; label: string; color: string; emoji: string; assetBias: Record<string, string> } | null macroInfo?: { dominant: string; label: string; color: string; emoji: string; assetBias: Record<string, string> } | 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[] profiles?: any[]
rank: number rank: number
}) { }) {
@@ -529,56 +677,72 @@ function TradeRow({ item, onAdd, macroInfo, addedInfo, profiles, rank }: {
{expanded && ( {expanded && (
<tr className="bg-dark-900/60"> <tr className="bg-dark-900/60">
<td colSpan={13} className="px-0 py-0 border-b border-blue-700/20"> <td colSpan={13} className="px-0 py-0 border-b border-blue-700/20">
<div className="p-4 grid grid-cols-3 gap-4 text-xs"> <div className="p-4 space-y-3 text-xs">
{/* Col 1: score justification */} <div className="grid grid-cols-3 gap-4">
<div className="col-span-2"> {/* Col 1: score justification */}
{scoreInfo?.key_catalyst && ( <div className="col-span-2">
<div className="flex gap-1.5 bg-yellow-400/5 border border-yellow-400/20 rounded px-2 py-1.5 mb-3"> {scoreInfo?.key_catalyst && (
<span className="text-yellow-400 shrink-0">🔑</span> <div className="flex gap-1.5 bg-yellow-400/5 border border-yellow-400/20 rounded px-2 py-1.5 mb-3">
<p className="text-yellow-200/80 leading-snug">{scoreInfo.key_catalyst}</p> <span className="text-yellow-400 shrink-0">🔑</span>
</div> <p className="text-yellow-200/80 leading-snug">{scoreInfo.key_catalyst}</p>
)} </div>
{rankRationale && <p className="text-slate-500 italic mb-2">{rankRationale}</p>} )}
{scoreInfo?.summary && <p className="text-slate-400 mb-3">{scoreInfo.summary}</p>} {rankRationale && <p className="text-slate-500 italic mb-2">{rankRationale}</p>}
{scoreInfo?.buckets?.length > 0 && ( {scoreInfo?.summary && <p className="text-slate-400 mb-3">{scoreInfo.summary}</p>}
<ScoreJustification buckets={scoreInfo.buckets} keyCatalyst={undefined} /> {scoreInfo?.buckets?.length > 0 && (
)} <ScoreJustification buckets={scoreInfo.buckets} keyCatalyst={undefined} />
</div> )}
{/* Col 2: actions + contra + timing */} </div>
<div className="space-y-3"> {/* Col 2: contra + macro + add button */}
{scoreInfo?.contra_signals?.length > 0 && ( <div className="space-y-3">
<div className="bg-orange-900/10 border border-orange-700/20 rounded p-2 space-y-1"> {scoreInfo?.contra_signals?.length > 0 && (
<div className="text-orange-400 font-semibold uppercase text-[9px] tracking-wide">Contra signals</div> <div className="bg-orange-900/10 border border-orange-700/20 rounded p-2 space-y-1">
{scoreInfo.contra_signals.slice(0, 3).map((cs: any, i: number) => ( <div className="text-orange-400 font-semibold uppercase text-[9px] tracking-wide">Contra signals</div>
<div key={i} className="text-orange-300/70 text-[10px] leading-snug">{cs.title ?? cs}</div> {scoreInfo.contra_signals.slice(0, 3).map((cs: any, i: number) => (
))} <div key={i} className="text-orange-300/70 text-[10px] leading-snug">{cs.title ?? cs}</div>
</div> ))}
)} </div>
{macroInfo && macroInfo.dominant !== 'incertain' && ( )}
<div className="text-[11px]"> {macroInfo && macroInfo.dominant !== 'incertain' && (
<span style={{ color: macroInfo.color }}>{macroInfo.emoji} {macroInfo.label}</span> <div className="text-[11px]">
<span className="text-slate-600 mx-1">·</span> <span style={{ color: macroInfo.color }}>{macroInfo.emoji} {macroInfo.label}</span>
<span style={{ color: bd.color }}>{bd.label}</span> <span className="text-slate-600 mx-1">·</span>
</div> <span style={{ color: bd.color }}>{bd.label}</span>
)} </div>
{addedInfo && ( )}
<div className="flex items-center gap-1.5 text-[10px] text-emerald-400 bg-emerald-900/20 border border-emerald-700/30 rounded px-2 py-1"> {addedInfo && (
<CheckCircle2 className="w-3 h-3 shrink-0" /> <div className="flex items-center gap-1.5 text-[10px] text-emerald-400 bg-emerald-900/20 border border-emerald-700/30 rounded px-2 py-1">
Ajouté le {format(new Date(addedInfo.entry_date), "d MMM yyyy", { locale: fr })} <CheckCircle2 className="w-3 h-3 shrink-0" />
</div> Ajouté le {format(new Date(addedInfo.entry_date), "d MMM yyyy", { locale: fr })}
)} </div>
<button )}
onClick={e => { e.stopPropagation(); onAdd(item) }} <button
className={clsx( onClick={e => { e.stopPropagation(); onAdd(item) }}
'w-full flex items-center justify-center gap-1 text-xs rounded py-1.5 transition-all border', className={clsx(
addedInfo 'w-full flex items-center justify-center gap-1 text-xs rounded py-1.5 transition-all border',
? 'text-slate-500 border-slate-700/30 hover:text-slate-300 hover:border-slate-600' addedInfo
: 'text-blue-400 hover:text-white hover:bg-blue-600 border-blue-500/30 hover:border-blue-500' ? 'text-slate-500 border-slate-700/30 hover:text-slate-300 hover:border-slate-600'
)}> : 'text-blue-400 hover:text-white hover:bg-blue-600 border-blue-500/30 hover:border-blue-500'
<Plus className="w-3 h-3" /> )}>
{addedInfo ? 'Ajouter à nouveau' : 'Ajouter au portefeuille'} <Plus className="w-3 h-3" />
</button> {addedInfo ? 'Ajouter à nouveau' : 'Ajouter au portefeuille'}
</button>
</div>
</div> </div>
{/* IBKR Ticket — full width below the analysis */}
{trade.strategy && trade.underlying && (
<IBKRTicket
strategy={trade.strategy}
underlying={trade.underlying}
strikeGuidance={trade.strike_guidance ?? scoreInfo?.recommended_trade?.strike_guidance ?? addedInfo?.strike_guidance}
expiryDays={scoreInfo?.recommended_trade?.expiry_days ?? trade.expiry_days ?? trade.horizon_days ?? addedInfo?.expiry_days}
entryPrice={addedInfo?.entry_price ?? null}
maxLoss={trade.max_loss_eur ?? scoreInfo?.recommended_trade?.max_loss_eur}
targetGain={trade.target_gain_eur ?? scoreInfo?.recommended_trade?.target_gain_eur}
timing={scoreInfo?.recommended_trade?.timing_note}
invalidation={scoreInfo?.recommended_trade?.invalidation}
/>
)}
</div> </div>
</td> </td>
</tr> </tr>
@@ -653,13 +817,16 @@ export default function Dashboard() {
// Fallback map from cycle-auto-logged trades (trade_entry_prices), keyed by pattern_id // Fallback map from cycle-auto-logged trades (trade_entry_prices), keyed by pattern_id
const mtmMap = useMemo(() => { const mtmMap = useMemo(() => {
const map: Record<string, { entry_date: string; expiry_days?: number }> = {} const map: Record<string, { entry_date: string; expiry_days?: number; entry_price?: number; strike_guidance?: string }> = {}
for (const t of ((tradeMtmData as any)?.trades ?? [])) { for (const t of ((tradeMtmData as any)?.trades ?? [])) {
const pid = t.pattern_id as string const pid = t.pattern_id as string
if (!pid) continue if (!pid) continue
const entry_date = (t.entry_date as string) ?? '' const entry_date = (t.entry_date as string) ?? ''
const expiry_days = (t.horizon_days as number) ?? undefined const expiry_days = (t.expiry_days_at_entry ?? t.horizon_days) as number | undefined
if (!map[pid] || entry_date > map[pid].entry_date) map[pid] = { entry_date, expiry_days } 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 return map
}, [tradeMtmData]) }, [tradeMtmData])

View File

@@ -1,8 +1,10 @@
import { useState, useEffect, useRef, Fragment } from 'react' 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 clsx from 'clsx'
import { useJournalSummary, useMacroHistory, useGeoHistory, useTradeMtm, useCycleHistory, useCycleStatus, useTriggerCycle, useTradePostmortem, useAnalyzePostmortem, useIvForTrade, useKellySizing, api } from '../hooks/useApi' import { useJournalSummary, useMacroHistory, useGeoHistory, useTradeMtm, useCycleHistory, useCycleStatus, useTriggerCycle, useTradePostmortem, useAnalyzePostmortem, useIvForTrade, useKellySizing, api } from '../hooks/useApi'
import { useQueryClient } from '@tanstack/react-query' import { useQueryClient } from '@tanstack/react-query'
import { format } from 'date-fns'
import { fr } from 'date-fns/locale'
const SCENARIO_META: Record<string, { label: string; color: string; emoji: string }> = { const SCENARIO_META: Record<string, { label: string; color: string; emoji: string }> = {
goldilocks: { label: 'Goldilocks', color: '#22c55e', emoji: '🌟' }, 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 (
<div className="bg-blue-950/20 border border-blue-700/30 rounded p-3 space-y-2.5">
<div className="text-[10px] font-bold text-blue-300 uppercase tracking-wide flex items-center gap-1.5">
<Terminal className="w-3 h-3" /> Ticket IBKR
{!entryPrice && <span className="ml-auto text-slate-600 font-normal normal-case">Prix non disponible vérifier dans IBKR</span>}
</div>
<div className="grid grid-cols-3 gap-3 text-[10px]">
<div>
<div className="text-slate-600 mb-0.5">Sous-jacent</div>
<div className="text-white font-mono font-bold text-sm">{underlying}</div>
<div className="text-slate-600 text-[9px]">Security Type: OPT</div>
</div>
<div>
<div className="text-slate-600 mb-0.5">Stratégie</div>
<div className="text-slate-200 font-semibold">{strategy}</div>
{entryPrice && <div className="text-slate-600 text-[9px]">Sous-jacent: ${entryPrice.toFixed(2)}</div>}
</div>
<div>
<div className="text-slate-600 mb-0.5">Expiration</div>
<div className="text-slate-200 font-mono">
{expiryDate ? format(expiryDate, 'd MMM yyyy', { locale: fr }) : expiryDays ? `~${expiryDays}j` : '—'}
</div>
<div className="text-slate-600 text-[9px]">{expiryDays ? `${expiryDays}j DTE` : ''}</div>
</div>
</div>
<div className="space-y-1">
{legs.map((leg, i) => (
<div key={i} className="flex items-center gap-2 bg-slate-900/70 rounded px-2.5 py-1.5 font-mono text-[10px]">
<span className={clsx('w-8 font-bold', leg.action === 'BUY' ? 'text-emerald-400' : 'text-red-400')}>{leg.action}</span>
<span className="text-slate-400">1 contrat</span>
<span className={clsx('font-bold w-10 text-center', leg.type === 'CALL' ? 'text-blue-300' : leg.type === 'PUT' ? 'text-orange-300' : 'text-slate-300')}>{leg.type}</span>
<span className="text-slate-600">Strike</span>
<span className="text-yellow-300 font-bold">{fmtStrike(leg.strike)}</span>
{leg.strike === null && strikeGuidance && (
<span className="text-slate-500">({strikeGuidance})</span>
)}
</div>
))}
</div>
<div className="grid grid-cols-3 gap-3 text-[10px]">
<div>
<div className="text-slate-600 mb-0.5">Type d'ordre</div>
<div className="text-slate-300 font-semibold">LIMIT</div>
<div className="text-slate-600 text-[9px]">Prix = prime dans IBKR</div>
</div>
<div>
<div className="text-slate-600 mb-0.5">Budget max</div>
<div className="text-slate-300 font-mono">{maxLoss ? `${Math.abs(maxLoss).toFixed(0)}€` : '~1 000'}</div>
<div className="text-slate-600 text-[9px]">Perte max estimée</div>
</div>
<div>
<div className="text-slate-600 mb-0.5">Cible</div>
<div className="text-emerald-400 font-mono">{targetGain ? `+${targetGain.toFixed(0)}€` : ''}</div>
</div>
</div>
</div>
)
}
function MacroHistorySection({ days }: { days: number }) { function MacroHistorySection({ days }: { days: number }) {
const { data, isLoading, refetch, isFetching } = useMacroHistory(days) const { data, isLoading, refetch, isFetching } = useMacroHistory(days)
const history: any[] = (data as any)?.history ?? [] const history: any[] = (data as any)?.history ?? []
@@ -424,6 +553,8 @@ function TradeMtmSection({ days }: { days: number }) {
<th className="text-left px-3 py-2 font-medium">Pattern</th> <th className="text-left px-3 py-2 font-medium">Pattern</th>
<th className="text-left px-3 py-2 font-medium">Profil</th> <th className="text-left px-3 py-2 font-medium">Profil</th>
<th className="text-left px-3 py-2 font-medium">Stratégie</th> <th className="text-left px-3 py-2 font-medium">Stratégie</th>
<th className="text-right px-3 py-2 font-medium">Strike</th>
<th className="text-right px-3 py-2 font-medium">DTE</th>
<th className="text-left px-3 py-2 font-medium">Ticker</th> <th className="text-left px-3 py-2 font-medium">Ticker</th>
<th className="text-right px-3 py-2 font-medium">Score</th> <th className="text-right px-3 py-2 font-medium">Score</th>
<th className="text-right px-3 py-2 font-medium">Trade Score</th> <th className="text-right px-3 py-2 font-medium">Trade Score</th>
@@ -464,6 +595,12 @@ function TradeMtmSection({ days }: { days: number }) {
{t.direction === 'bearish' ? '🐻' : '🐂'} {t.strategy || ''} {t.direction === 'bearish' ? '🐻' : '🐂'} {t.strategy || ''}
</span> </span>
</td> </td>
<td className="px-3 py-2 text-right font-mono text-[11px] text-slate-400">
{t.strike_guidance ?? ''}
</td>
<td className="px-3 py-2 text-right font-mono text-[11px] text-slate-400">
{t.expiry_days_at_entry != null ? `${t.expiry_days_at_entry}j` : t.horizon_days != null ? `${t.horizon_days}j` : ''}
</td>
<td className="px-3 py-2 font-mono text-slate-300">{t.underlying}</td> <td className="px-3 py-2 font-mono text-slate-300">{t.underlying}</td>
<td className="px-3 py-2 text-right"> <td className="px-3 py-2 text-right">
<ScoreDelta entry={t.score_at_entry} latest={t.latest_score ?? t.score_at_entry} /> <ScoreDelta entry={t.score_at_entry} latest={t.latest_score ?? t.score_at_entry} />
@@ -554,7 +691,16 @@ function TradeMtmSection({ days }: { days: number }) {
</tr> </tr>
{isExpanded && ( {isExpanded && (
<tr className="bg-dark-900/60"> <tr className="bg-dark-900/60">
<td colSpan={16} className="px-0 py-0 border-b border-blue-700/20"> <td colSpan={18} className="px-4 py-3 border-b border-blue-700/20 space-y-3">
<IBKRTicket
strategy={t.strategy ?? ''}
underlying={t.underlying}
strikeGuidance={t.strike_guidance}
expiryDays={t.expiry_days_at_entry ?? t.horizon_days}
entryPrice={t.entry_price}
maxLoss={t.max_loss_eur}
targetGain={t.target_gain_eur}
/>
<PostmortemPanel tradeId={t.id} onClose={() => setSelectedTradeId(null)} /> <PostmortemPanel tradeId={t.id} onClose={() => setSelectedTradeId(null)} />
</td> </td>
</tr> </tr>