feat: strategy builder
This commit is contained in:
@@ -69,7 +69,7 @@ def get_iv_watchlist():
|
||||
Returns a summary list sorted by IV Rank descending.
|
||||
"""
|
||||
from services.iv_engine import get_atm_iv, _resolve_ticker
|
||||
from services.database import get_iv_rank_percentile, save_iv_snapshot, get_iv_history, get_watchlist_tickers
|
||||
from services.database import get_iv_rank_percentile, save_iv_snapshot, get_iv_history, get_watchlist_tickers, get_iv_change_1d
|
||||
from datetime import date
|
||||
|
||||
results = []
|
||||
@@ -97,10 +97,12 @@ def get_iv_watchlist():
|
||||
if live_iv:
|
||||
save_iv_snapshot(proxy, today, iv)
|
||||
rank_data = get_iv_rank_percentile(proxy, iv)
|
||||
iv_change_1d = get_iv_change_1d(proxy, iv)
|
||||
|
||||
item = {
|
||||
"ticker": ticker,
|
||||
"iv_current_pct": round(iv * 100, 1),
|
||||
"iv_change_1d_pct": round(iv_change_1d * 100, 1) if iv_change_1d is not None else None,
|
||||
"iv_rank": rank_data.get("iv_rank"),
|
||||
"iv_percentile": rank_data.get("iv_percentile"),
|
||||
"history_days": rank_data.get("history_days", 0),
|
||||
|
||||
@@ -3159,6 +3159,22 @@ def get_iv_history(ticker: str, days: int = 90) -> List[Dict]:
|
||||
return [dict(r) for r in rows]
|
||||
|
||||
|
||||
def get_iv_change_1d(ticker: str, current_iv: float) -> Optional[float]:
|
||||
"""Day-over-day IV delta (decimal, e.g. 0.012 = +1.2 vol points) — current_iv minus
|
||||
the most recent prior trading day's recorded iv_current. None if no prior day exists."""
|
||||
conn = get_conn()
|
||||
row = conn.execute(
|
||||
"""SELECT iv_current FROM iv_history
|
||||
WHERE ticker=? AND iv_current IS NOT NULL AND recorded_date < date('now')
|
||||
ORDER BY recorded_date DESC LIMIT 1""",
|
||||
(ticker.upper(),),
|
||||
).fetchone()
|
||||
conn.close()
|
||||
if not row or row["iv_current"] is None:
|
||||
return None
|
||||
return current_iv - row["iv_current"]
|
||||
|
||||
|
||||
# ── IV Watchlist ──────────────────────────────────────────────────────────────
|
||||
|
||||
def get_watchlist_tickers() -> List[str]:
|
||||
|
||||
@@ -404,7 +404,7 @@ 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
|
||||
from services.database import get_iv_rank_percentile, save_iv_snapshot, get_iv_change_1d
|
||||
|
||||
proxy = _resolve_ticker(ticker)
|
||||
today = date.today().isoformat()
|
||||
@@ -423,16 +423,19 @@ def get_full_iv_snapshot(ticker: str) -> Dict[str, Any]:
|
||||
iv_current = recent[0]["iv_current"]
|
||||
|
||||
rank_data: Dict[str, Any] = {}
|
||||
iv_change_1d = None
|
||||
if iv_current:
|
||||
if live_iv and date.today().weekday() < 5:
|
||||
# Only persist weekday IV — weekend premium inflates IV and corrupts history
|
||||
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)
|
||||
iv_change_1d = get_iv_change_1d(proxy, iv_current)
|
||||
|
||||
return {
|
||||
"ticker": ticker,
|
||||
"proxy": proxy,
|
||||
"iv_current_pct": round(iv_current * 100, 1) if iv_current else None,
|
||||
"iv_change_1d_pct": round(iv_change_1d * 100, 1) if iv_change_1d is not None else None,
|
||||
"iv_rank": rank_data.get("iv_rank"),
|
||||
"iv_percentile": rank_data.get("iv_percentile"),
|
||||
"history_days": rank_data.get("history_days", 0),
|
||||
|
||||
@@ -28,6 +28,7 @@ def get_chain_slice(symbol: str, target_days: int = 8, n_expiries: int = 3) -> D
|
||||
)
|
||||
|
||||
spot = next((r["spot"] for r in flat_rows if r.get("spot") is not None), None)
|
||||
as_of = max((r["created_at"] for r in flat_rows if r.get("created_at")), default=None)
|
||||
today = date.today()
|
||||
|
||||
by_expiry: Dict[str, List[Dict[str, Any]]] = {}
|
||||
@@ -77,6 +78,7 @@ def get_chain_slice(symbol: str, target_days: int = 8, n_expiries: int = 3) -> D
|
||||
"symbol": symbol.upper(),
|
||||
"proxy": symbol.upper(),
|
||||
"spot": round(float(spot), 6) if spot is not None else None,
|
||||
"as_of": as_of,
|
||||
"expiries": expiries_out,
|
||||
}
|
||||
|
||||
|
||||
@@ -1520,7 +1520,7 @@ export const useRejectAiTradeProposal = () => {
|
||||
|
||||
export type ChainRow = { strike: number; bid: number; ask: number; mid: number; last: number; iv: number; open_interest: number; volume: number }
|
||||
export type ChainExpiry = { expiry_date: string; days_to_expiry: number; calls: ChainRow[]; puts: ChainRow[] }
|
||||
export type ChainSlice = { symbol: string; proxy: string; spot: number; expiries: ChainExpiry[] }
|
||||
export type ChainSlice = { symbol: string; proxy: string; spot: number; as_of?: string | null; expiries: ChainExpiry[] }
|
||||
|
||||
export type ManualGridCell = { days_to_expiry: number; strike_pct: number; iv: number | null }
|
||||
|
||||
|
||||
26
frontend/src/lib/format.ts
Normal file
26
frontend/src/lib/format.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* Shared price/rate formatter. Raw quotes (spot, strike, bid/ask) always show a
|
||||
* meaningful number of decimals — 4 for FX-scale instruments (<10), 2 above that
|
||||
* (equities/indices), matching standard quoting convention for each magnitude.
|
||||
* NOT for post-nominal dollar amounts (P&L, entry cost) — those stay at 2 decimals
|
||||
* via each page's own fmtMoney.
|
||||
*/
|
||||
export function fmtPrice(v: number | null | undefined, decimals?: number): string {
|
||||
if (v == null || Number.isNaN(v)) return '—'
|
||||
const d = decimals ?? (Math.abs(v) < 10 ? 4 : 2)
|
||||
return v.toFixed(d)
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats a backend UTC timestamp as a compact local "as of" label, e.g. "18/07 14:32".
|
||||
* Backend timestamps come in two shapes, both naive UTC: Python's `datetime.isoformat()`
|
||||
* ("2026-07-18T14:32:01.123") and sqlite's `datetime('now')` ("2026-07-18 14:32:01").
|
||||
*/
|
||||
export function fmtAsOf(raw: string | null | undefined): string | null {
|
||||
if (!raw) return null
|
||||
let iso = raw.includes('T') ? raw : raw.replace(' ', 'T')
|
||||
if (!/[zZ]|[+-]\d{2}:?\d{2}$/.test(iso)) iso += 'Z'
|
||||
const d = new Date(iso)
|
||||
if (Number.isNaN(d.getTime())) return null
|
||||
return d.toLocaleString('fr-FR', { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' })
|
||||
}
|
||||
@@ -464,7 +464,16 @@ function SaxoConnectionCard() {
|
||||
setInput('')
|
||||
}
|
||||
|
||||
// Guarded by isPending everywhere it's called from the UI: each call captures `symbols`
|
||||
// from the current render, so firing a second mutation before the first one's
|
||||
// invalidateQueries/refetch completes reads a stale list and undoes the first removal
|
||||
// once it resolves — confirmed as the cause of rapid multi-click removals "coming back".
|
||||
const removeSymbol = (sym: string) => updateWatchlist.mutate(symbols.filter(s => s !== sym))
|
||||
const clearAllSymbols = () => {
|
||||
if (symbols.length === 0) return
|
||||
if (!window.confirm(`Effacer les ${symbols.length} ticker(s) de la watchlist Saxo ?`)) return
|
||||
updateWatchlist.mutate([])
|
||||
}
|
||||
|
||||
const handleExpandWatchlist = async () => {
|
||||
const kw = expandKeyword.trim()
|
||||
@@ -608,7 +617,7 @@ function SaxoConnectionCard() {
|
||||
<option key={m.uic} value={m.symbol}>{m.description} ({m.asset_type})</option>
|
||||
))}
|
||||
</datalist>
|
||||
<button onClick={addSymbol} disabled={!input.trim()}
|
||||
<button onClick={addSymbol} disabled={!input.trim() || updateWatchlist.isPending}
|
||||
className="flex items-center gap-1 px-3 py-1.5 rounded text-xs bg-blue-600 text-white hover:bg-blue-500 disabled:opacity-40">
|
||||
<Plus className="w-3.5 h-3.5" /> Ajouter
|
||||
</button>
|
||||
@@ -631,6 +640,15 @@ function SaxoConnectionCard() {
|
||||
</button>
|
||||
</div>
|
||||
{expandMsg && <div className="text-[10px] text-slate-500 mb-2">{expandMsg}</div>}
|
||||
{symbols.length > 0 && (
|
||||
<div className="flex justify-end mb-1.5">
|
||||
<button onClick={clearAllSymbols} disabled={updateWatchlist.isPending}
|
||||
title="Retire tous les tickers de la watchlist en un seul appel"
|
||||
className="flex items-center gap-1 px-2 py-1 rounded text-[10px] border border-red-700/40 text-red-400 hover:bg-red-900/20 disabled:opacity-40">
|
||||
<Trash2 className="w-3 h-3" /> Tout effacer ({symbols.length})
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{symbols.map(sym => {
|
||||
const check = validation?.find(v => v.symbol === sym.toUpperCase())
|
||||
@@ -646,7 +664,9 @@ function SaxoConnectionCard() {
|
||||
<button onClick={() => handleSnapshotNow(sym)} title="Snapshot maintenant" className="hover:text-white">
|
||||
<Camera className="w-3 h-3" />
|
||||
</button>
|
||||
<button onClick={() => removeSymbol(sym)}><X className="w-3 h-3" /></button>
|
||||
<button onClick={() => removeSymbol(sym)} disabled={updateWatchlist.isPending} className="disabled:opacity-40">
|
||||
<X className="w-3 h-3" />
|
||||
</button>
|
||||
</span>
|
||||
)
|
||||
})}
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
import { scoreColor } from '../components/TradeIdeas'
|
||||
import { ASSET_CLASS_COLORS } from '../constants/assetColors'
|
||||
import TradeRankList from '../components/TradeRankList'
|
||||
import { fmtPrice, fmtAsOf } from '../lib/format'
|
||||
|
||||
const riskGauge = (score: number) => {
|
||||
if (score < 25) return { color: 'text-emerald-400', bg: 'bg-emerald-500', label: 'LOW' }
|
||||
@@ -113,7 +114,7 @@ function QuoteRow({ q }: { q: Quote }) {
|
||||
<div className="flex items-center justify-between py-1.5 border-b border-slate-700/20 last:border-0">
|
||||
<span className="text-xs text-white truncate max-w-[140px]">{q.name || q.symbol}</span>
|
||||
<div className="text-right ml-2">
|
||||
<div className="text-xs text-white font-mono">{q.price.toFixed(2)}</div>
|
||||
<div className="text-xs text-white font-mono">{fmtPrice(q.price)}</div>
|
||||
<div className={clsx('text-xs font-mono', pos ? 'positive' : 'negative')}>
|
||||
{pos ? '+' : ''}{q.change_pct.toFixed(2)}%
|
||||
</div>
|
||||
@@ -403,7 +404,7 @@ export default function Dashboard() {
|
||||
<div key={it.ticker} className="flex items-center justify-between text-[10px]">
|
||||
<span className="text-slate-400 font-mono">{it.ticker}</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-slate-500 font-mono">{it.price != null ? it.price.toFixed(2) : '—'}</span>
|
||||
<span className="text-slate-500 font-mono">{fmtPrice(it.price)}</span>
|
||||
<span className={clsx('font-mono font-bold w-12 text-right', (it.change_pct ?? 0) >= 0 ? 'text-emerald-400' : 'text-red-400')}>
|
||||
{it.change_pct != null ? `${it.change_pct >= 0 ? '+' : ''}${it.change_pct.toFixed(2)}%` : '—'}
|
||||
</span>
|
||||
@@ -923,13 +924,17 @@ export default function Dashboard() {
|
||||
.map((w: any) => snapshots[w.ticker])
|
||||
.filter(Boolean)
|
||||
.sort((a: any, b: any) => Math.abs((b.iv_rank ?? 50) - 50) - Math.abs((a.iv_rank ?? 50) - 50))
|
||||
const asOf = fmtAsOf(highlights.map((h: any) => h.fetched_at).filter(Boolean).sort().pop())
|
||||
|
||||
return (
|
||||
<Link to="/options" ref={optionsLabCardRef} className="card flex flex-col hover:border-slate-600/60 transition-all cursor-pointer">
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<span className="section-title mb-0 text-[10px]">🧪 Options Lab</span>
|
||||
<div className="flex items-center gap-1.5">
|
||||
{asOf && <span className="text-[9px] text-slate-600" title="Données IV mises à jour à">MAJ {asOf}</span>}
|
||||
<ArrowUpRight className="w-3 h-3 text-slate-600" />
|
||||
</div>
|
||||
</div>
|
||||
{watchlist.length === 0 ? (
|
||||
<div className="text-[10px] text-slate-600 mt-1">
|
||||
Add instruments to your watchlist (Config) to see IV highlights
|
||||
@@ -940,6 +945,7 @@ export default function Dashboard() {
|
||||
const rank = h.iv_rank
|
||||
const skewPct = h.skew?.skew_pct
|
||||
const ivCur = h.iv_current_pct
|
||||
const ivChg = h.iv_change_1d_pct
|
||||
return (
|
||||
<div key={h.ticker} className="pb-1 border-b border-slate-700/20 last:border-0">
|
||||
<div className="flex items-center gap-1.5 text-[10px]">
|
||||
@@ -947,6 +953,11 @@ export default function Dashboard() {
|
||||
<span className="font-mono text-slate-200 font-bold shrink-0">{h.ticker}</span>
|
||||
<span className="text-slate-500">IVR {rank != null ? rank.toFixed(0) : '—'}</span>
|
||||
{ivCur != null && <span className="text-slate-600">· IV {ivCur.toFixed(1)}%</span>}
|
||||
{ivChg != null && Math.abs(ivChg) >= 0.1 && (
|
||||
<span className={clsx('font-mono', ivChg > 0 ? 'text-orange-400' : 'text-blue-400')}>
|
||||
{ivChg >= 0 ? '+' : ''}{ivChg.toFixed(1)}pt
|
||||
</span>
|
||||
)}
|
||||
{skewPct != null && Math.abs(skewPct) > 3 && (
|
||||
<span className={clsx('ml-auto text-[9px]', skewPct > 0 ? 'text-orange-400' : 'text-blue-400')}>
|
||||
skew {skewPct >= 0 ? '+' : ''}{skewPct.toFixed(1)}
|
||||
|
||||
@@ -8,6 +8,7 @@ import axios from 'axios'
|
||||
import clsx from 'clsx'
|
||||
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts'
|
||||
import InstrumentChart, { TheoPoint } from '../components/InstrumentChart'
|
||||
import { fmtPrice } from '../lib/format'
|
||||
|
||||
const api = axios.create({ baseURL: '/api' })
|
||||
|
||||
@@ -448,7 +449,7 @@ function TrendCard({ trend, dateLabel }: { trend: TrendMetrics; dateLabel: strin
|
||||
<div className="flex items-center justify-between bg-dark-700/40 rounded-lg px-3 py-1.5">
|
||||
<span className="text-xs text-slate-500">Prix</span>
|
||||
<span className="text-sm font-bold text-white">
|
||||
{trend.current_price?.toLocaleString('fr-FR', { maximumFractionDigits: 4 })}
|
||||
{fmtPrice(trend.current_price)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -492,7 +493,7 @@ function TrendCard({ trend, dateLabel }: { trend: TrendMetrics; dateLabel: strin
|
||||
<div className="absolute top-0 h-full bg-blue-500/60 rounded-full" style={{ width: `${pct52w}%` }} />
|
||||
</div>
|
||||
<div className="flex justify-between text-xs text-slate-600 mt-0.5">
|
||||
<span>{trend.low_52w?.toFixed(2)}</span><span>{trend.high_52w?.toFixed(2)}</span>
|
||||
<span>{fmtPrice(trend.low_52w)}</span><span>{fmtPrice(trend.high_52w)}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -1906,11 +1907,11 @@ export default function InstrumentDashboard({ instrumentIdProp, isVisible }: { i
|
||||
{displayPrice !== undefined && (
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-xl font-bold text-white">
|
||||
{displayPrice.toLocaleString('fr-FR', { maximumFractionDigits: 4 })}
|
||||
{fmtPrice(displayPrice)}
|
||||
</span>
|
||||
{isLastDate && snapshot && (
|
||||
<span className={clsx('text-sm font-semibold', (snapshot.change_pct ?? 0) >= 0 ? 'text-emerald-400' : 'text-red-400')}>
|
||||
{(snapshot.change_pct ?? 0) >= 0 ? '+' : ''}{snapshot.change_abs?.toFixed(2)} ({(snapshot.change_pct ?? 0) >= 0 ? '+' : ''}{snapshot.change_pct?.toFixed(2)}%)
|
||||
{(snapshot.change_pct ?? 0) >= 0 ? '+' : ''}{fmtPrice(snapshot.change_abs)} ({(snapshot.change_pct ?? 0) >= 0 ? '+' : ''}{snapshot.change_pct?.toFixed(2)}%)
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -174,6 +174,14 @@ function TickerDetail({ ticker }: { ticker: string }) {
|
||||
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}d of history
|
||||
{snap.iv_change_1d_pct != null && (
|
||||
<>
|
||||
{' · '}vs veille{' '}
|
||||
<span className={snap.iv_change_1d_pct > 0 ? 'text-orange-400' : snap.iv_change_1d_pct < 0 ? 'text-blue-400' : 'text-slate-400'}>
|
||||
{snap.iv_change_1d_pct >= 0 ? '+' : ''}{snap.iv_change_1d_pct}pt
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -203,6 +211,11 @@ function WatchlistRow({ item }: { item: any }) {
|
||||
<span className={clsx('text-sm font-bold font-mono', ivRankColor(item.iv_rank))}>
|
||||
{item.iv_current_pct != null ? `${item.iv_source === 'history' ? '~' : ''}${item.iv_current_pct}%` : '—'}
|
||||
</span>
|
||||
{item.iv_change_1d_pct != null && Math.abs(item.iv_change_1d_pct) >= 0.1 && (
|
||||
<span className={clsx('ml-1 text-[9px] font-mono', item.iv_change_1d_pct > 0 ? 'text-orange-400' : 'text-blue-400')}>
|
||||
{item.iv_change_1d_pct >= 0 ? '+' : ''}{item.iv_change_1d_pct.toFixed(1)}
|
||||
</span>
|
||||
)}
|
||||
<div className="text-[8px] text-slate-600">{item.iv_source === 'history' ? 'Estimated IV' : 'Current IV'}</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -8,10 +8,11 @@ import {
|
||||
useOptionChainSlice, usePriceStrategy, useOptimizeStrategy,
|
||||
useScenarios, useSaveScenario, useDeleteScenario,
|
||||
useSavedStrategies, useSaveStrategyRecord, useDeleteSavedStrategy,
|
||||
useWatchlistTickers, useSaxoCatalog,
|
||||
useWatchlistTickers, useSaxoCatalog, useIvForTrade,
|
||||
type StrategyLeg, type StrategyScenario, type PriceCombo, type StrategyCandidate,
|
||||
type OptimizeConstraints, type SavedScenario,
|
||||
} from '../hooks/useApi'
|
||||
import { fmtPrice, fmtAsOf } from '../lib/format'
|
||||
|
||||
type WatchlistEntry = { ticker: string; is_active: number | boolean; added_by: string }
|
||||
|
||||
@@ -321,7 +322,7 @@ function VolSurfaceHeatmap({ chain, spot }: { chain: any; spot: number }) {
|
||||
const { bg, fg } = ivCellColor(cell.iv, min, max)
|
||||
return (
|
||||
<td key={pct} className="text-center rounded font-semibold" style={{ background: bg, color: fg }}
|
||||
title={`${exp.expiry_date} · ${pct}% (${(spot * pct / 100).toFixed(2)}) · IV ${(cell.iv * 100).toFixed(1)}%`}>
|
||||
title={`${exp.expiry_date} · ${pct}% (${fmtPrice(spot * pct / 100)}) · IV ${(cell.iv * 100).toFixed(1)}%`}>
|
||||
{(cell.iv * 100).toFixed(1)}
|
||||
</td>
|
||||
)
|
||||
@@ -375,7 +376,7 @@ function LegRow({
|
||||
onChange={(e) => onChange({ ...leg, strike: parseFloat(e.target.value) })}
|
||||
>
|
||||
{rows.map((r: any) => (
|
||||
<option key={r.strike} value={r.strike}>{r.strike} (bid {r.bid} / ask {r.ask})</option>
|
||||
<option key={r.strike} value={r.strike}>{fmtPrice(r.strike)} (bid {fmtPrice(r.bid)} / ask {fmtPrice(r.ask)})</option>
|
||||
))}
|
||||
</select>
|
||||
<select
|
||||
@@ -609,6 +610,7 @@ export default function StrategyBuilder() {
|
||||
|
||||
const { data: chain, isLoading: chainLoading, isError: chainError, error: chainErrorObj, refetch: refetchChain, isFetching } =
|
||||
useOptionChainSlice(debouncedSymbol, horizonDays, 3)
|
||||
const { data: ivForTrade } = useIvForTrade(debouncedSymbol)
|
||||
|
||||
useEffect(() => {
|
||||
setScenario(s => ({ ...s, symbol: debouncedSymbol, horizon_days: horizonDays }))
|
||||
@@ -722,7 +724,24 @@ export default function StrategyBuilder() {
|
||||
{chain && (
|
||||
<div className="card space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="stat-label">Jambes (1-4) — Spot {chain.spot}</div>
|
||||
<div className="stat-label flex items-center gap-2 flex-wrap">
|
||||
<span>Jambes (1-4) — Spot {fmtPrice(chain.spot)}</span>
|
||||
{fmtAsOf(chain.as_of) && (
|
||||
<span className="text-slate-600 font-normal text-[10px]" title="Date de capture de la chaîne d'options (dernier snapshot Saxo)">
|
||||
· données du {fmtAsOf(chain.as_of)}
|
||||
</span>
|
||||
)}
|
||||
{ivForTrade?.iv_current_pct != null && (
|
||||
<span className="text-slate-500 font-normal text-[10px]">
|
||||
· IV ATM {ivForTrade.iv_current_pct.toFixed(1)}%
|
||||
{ivForTrade.iv_change_1d_pct != null && Math.abs(ivForTrade.iv_change_1d_pct) >= 0.1 && (
|
||||
<span className={ivForTrade.iv_change_1d_pct > 0 ? 'text-orange-400' : 'text-blue-400'}>
|
||||
{' '}({ivForTrade.iv_change_1d_pct >= 0 ? '+' : ''}{ivForTrade.iv_change_1d_pct.toFixed(1)}pt vs veille)
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<label className="flex items-center gap-1.5 text-xs text-slate-400" title="Notionnel par contrat (ex. 100 000 = 1 lot standard EURUSD). S'applique à chaque jambe, multiplié par sa quantité.">
|
||||
Nominal (USD)
|
||||
|
||||
Reference in New Issue
Block a user