feat: option lab

This commit is contained in:
OpenSquared
2026-07-28 11:14:31 +02:00
parent 568414ca0c
commit d2c393b8e5
11 changed files with 757 additions and 29 deletions

View File

@@ -326,6 +326,31 @@ export const usePositionPayoff = (posId: string, enabled: boolean) =>
enabled,
})
// "What would have been optimal, in hindsight?" — reprices the position's real legs and
// runs the Strategy Builder optimizer against the REAL historical Saxo chain + the REALIZED
// spot/IV move since entry (not a guessed scenario). Expensive (full optimizer run), so
// on-demand only — not auto-fetched, call refetch() from a button.
export type RetrospectiveCandidate = {
template_name: string; legs: StrategyLeg[]; score: number; return_on_capital_pct: number | null
net_pnl: number; max_gain: number | null; max_loss: number | null; net_delta_now: number
}
export type RetrospectiveComparison = {
available: boolean; reason?: string
underlying?: string; entry_date?: string; as_of?: string; horizon_days?: number
spot_entry?: number; spot_realized?: number; realized_spot_shock_pct?: number
iv_entry?: number; iv_realized?: number; realized_iv_shift?: number
actual_return_pct?: number | null
optimal_candidates?: RetrospectiveCandidate[]
}
export const useRetrospectiveOptimal = (posId: string, asOf?: string) =>
useQuery<RetrospectiveComparison>({
queryKey: ['portfolio-retrospective-optimal', posId, asOf],
queryFn: () => api.get(`/portfolio/positions/${posId}/retrospective-optimal`, { params: asOf ? { as_of: asOf } : {} }).then(r => r.data),
enabled: false,
staleTime: 5 * 60_000,
})
// Reprices every open position under a handful of named macro scenarios (Risk-Off,
// Risk-On, inflation persistante, dollar fort, baisse des matières premières) to surface
// when several differently-named positions are really the same underlying bet.
@@ -1048,6 +1073,36 @@ export const useSaxoIvSnapshot = (symbol: string) =>
staleTime: 5 * 60_000,
})
// Options Lab — "was this option well priced between two dates?" Picks the strike closest
// to the underlying's realized outcome at date_b (hindsight), reprices at both dates from
// real Saxo history, decomposes the move into Delta/Theta/Vega + a residual.
export type PricingCheckLeg = {
available: boolean
price_a?: number; price_b?: number; actual_change?: number
iv_a?: number | null; iv_b?: number | null
intrinsic_a?: number; time_value_a?: number; intrinsic_b?: number; time_value_b?: number
greeks_a?: { delta: number; gamma: number; theta: number; vega: number }
attribution?: { delta_pnl: number; theta_pnl: number; vega_pnl: number; explained: number; residual: number }
}
export type PricingCheckResult = {
available: boolean; reason?: string
ticker?: string; date_a?: string; date_b?: string; elapsed_days?: number; target_dte_used?: number
expiry_date?: string; expired_by_date_b?: boolean
spot_a?: number; spot_b?: number; spot_change_pct?: number; chosen_strike?: number
iv_a?: number | null; realized_vol?: number | null; vol_risk_premium?: number | null
legs?: { call: PricingCheckLeg; put: PricingCheckLeg }
}
// targetDte omitted/undefined -> backend defaults to the expiry closest to dateB (hindsight,
// same principle as the strike selection) — only pass it to force a different expiry.
export const usePricingCheck = (ticker: string, dateA: string, dateB: string, targetDte: number | undefined, enabled: boolean) =>
useQuery<PricingCheckResult>({
queryKey: ['options-pricing-check', ticker, dateA, dateB, targetDte],
queryFn: () => api.get('/saxo/pricing-check', { params: { ticker, date_a: dateA, date_b: dateB, target_dte: targetDte } }).then(r => r.data),
enabled: enabled && !!ticker && !!dateA && !!dateB,
staleTime: 5 * 60_000,
})
export const useSaxoIvHistory = (symbol: string, days = 90) =>
useQuery({
queryKey: ['saxo-iv-history', symbol, days],