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:
@@ -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}")
|
||||
|
||||
@@ -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<string, { label: string; color: string }> = {
|
||||
'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 }: {
|
||||
item: TradeItem
|
||||
onAdd: (item: TradeItem) => void
|
||||
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[]
|
||||
}) {
|
||||
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<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[]
|
||||
rank: number
|
||||
}) {
|
||||
@@ -529,7 +677,8 @@ function TradeRow({ item, onAdd, macroInfo, addedInfo, profiles, rank }: {
|
||||
{expanded && (
|
||||
<tr className="bg-dark-900/60">
|
||||
<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">
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
{/* Col 1: score justification */}
|
||||
<div className="col-span-2">
|
||||
{scoreInfo?.key_catalyst && (
|
||||
@@ -544,7 +693,7 @@ function TradeRow({ item, onAdd, macroInfo, addedInfo, profiles, rank }: {
|
||||
<ScoreJustification buckets={scoreInfo.buckets} keyCatalyst={undefined} />
|
||||
)}
|
||||
</div>
|
||||
{/* Col 2: actions + contra + timing */}
|
||||
{/* Col 2: contra + macro + add button */}
|
||||
<div className="space-y-3">
|
||||
{scoreInfo?.contra_signals?.length > 0 && (
|
||||
<div className="bg-orange-900/10 border border-orange-700/20 rounded p-2 space-y-1">
|
||||
@@ -580,6 +729,21 @@ function TradeRow({ item, onAdd, macroInfo, addedInfo, profiles, rank }: {
|
||||
</button>
|
||||
</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>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
@@ -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<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 ?? [])) {
|
||||
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])
|
||||
|
||||
@@ -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<string, { label: string; color: string; emoji: string }> = {
|
||||
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 }) {
|
||||
const { data, isLoading, refetch, isFetching } = useMacroHistory(days)
|
||||
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">Profil</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-right px-3 py-2 font-medium">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 || '—'}
|
||||
</span>
|
||||
</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 text-right">
|
||||
<ScoreDelta entry={t.score_at_entry} latest={t.latest_score ?? t.score_at_entry} />
|
||||
@@ -554,7 +691,16 @@ function TradeMtmSection({ days }: { days: number }) {
|
||||
</tr>
|
||||
{isExpanded && (
|
||||
<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)} />
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
Reference in New Issue
Block a user