/* ============================================================================ * HFChart Options Lab — module de calcul PUR (payoff, breakevens, Greeks) * Author : Simon-Pierre Boucher — contact@spboucher.ai * ---------------------------------------------------------------------------- * Aucune dépendance, aucun accès DOM/réseau : utilisable dans le navigateur * (window.HF.optmath) et sous Node pour les tests unitaires (module.exports). * * Une jambe (leg) : * { * kind : 'c' | 'p' | 's', // call, put, ou action (stock) * strike : number, // 0 / ignoré pour 's' * qty : number, // signé : + long, − short (contrats ; actions pour 's') * price : number, // prime par action (prix d'achat pour 's') * mult : number, // 100 par défaut, 1 pour 's' * greeks : {delta,gamma,theta,vega,rho} // par action, côté long (API HF Market Data) * } * Conventions : P&L en $ pour la position ; débit net positif = on paie. * ==========================================================================*/ (function (root) { 'use strict'; const multOf = leg => (leg.mult != null ? leg.mult : (leg.kind === 's' ? 1 : 100)); /* Valeur intrinsèque par action à l'échéance. */ function intrinsic(kind, strike, S) { if (kind === 'c') return Math.max(S - strike, 0); if (kind === 'p') return Math.max(strike - S, 0); return S; // 's' : l'action vaut S } /* P&L de la position à l'échéance pour un spot S. */ function payoffAt(legs, S) { let pnl = 0; for (const leg of legs) { pnl += leg.qty * multOf(leg) * (intrinsic(leg.kind, leg.strike, S) - leg.price); } return pnl; } /* Coût net d'ouverture (débit > 0, crédit < 0). */ function netCost(legs) { let c = 0; for (const leg of legs) c += leg.qty * multOf(leg) * leg.price; return c; } /* Greeks agrégés de la position ($ par unité de facteur). */ function netGreeks(legs) { const out = { delta: 0, gamma: 0, theta: 0, vega: 0, rho: 0 }; for (const leg of legs) { const g = leg.greeks || {}; const k = leg.qty * multOf(leg); out.delta += k * (leg.kind === 's' ? 1 : (g.delta || 0)); out.gamma += k * (g.gamma || 0); out.theta += k * (g.theta || 0); out.vega += k * (g.vega || 0); out.rho += k * (g.rho || 0); } return out; } /* Points de cassure du payoff : les strikes des jambes optionnelles. */ function kinks(legs) { const ks = [...new Set(legs.filter(l => l.kind !== 's').map(l => l.strike))]; ks.sort((a, b) => a - b); return ks; } /* Points d'évaluation exacts : 0, chaque strike, et un point au-delà du * dernier strike (le payoff est linéaire par morceaux entre ces points). */ function evalPoints(legs) { const ks = kinks(legs); const far = ks.length ? ks[ks.length - 1] * 1.5 + 100 : 200; return [0, ...ks, far]; } /* Pente du payoff au-delà du dernier strike ($ par $ de spot). */ function slopeRight(legs) { let s = 0; for (const leg of legs) { if (leg.kind === 'c' || leg.kind === 's') s += leg.qty * multOf(leg); } return s; } /* Breakevens exacts (zéros du payoff à l'échéance). */ function breakevens(legs) { const pts = evalPoints(legs); const out = []; for (let i = 1; i < pts.length; i++) { const x0 = pts[i - 1], x1 = pts[i]; const y0 = payoffAt(legs, x0), y1 = payoffAt(legs, x1); if (y0 === 0 && (i === 1 || payoffAt(legs, pts[i - 2]) !== 0)) out.push(x0); if ((y0 < 0 && y1 > 0) || (y0 > 0 && y1 < 0)) { out.push(x0 + (x1 - x0) * (-y0) / (y1 - y0)); } else if (y1 === 0 && i === pts.length - 1) { out.push(x1); } } // au-delà du dernier point, la pente est constante : un zéro éventuel est déjà couvert return [...new Set(out.map(v => +v.toFixed(6)))].sort((a, b) => a - b); } /* Profit max / perte max à l'échéance. Infinity si non borné à droite. */ function extremes(legs) { const pts = evalPoints(legs); let maxP = -Infinity, minP = Infinity; for (const p of pts) { const y = payoffAt(legs, p); if (y > maxP) maxP = y; if (y < minP) minP = y; } const s = slopeRight(legs); if (s > 1e-9) maxP = Infinity; if (s < -1e-9) minP = -Infinity; return { maxProfit: maxP, maxLoss: minP }; } /* Courbe de payoff à l'échéance sur [sMin, sMax] — insère les strikes pour * que les cassures soient exactes. */ function payoffCurve(legs, sMin, sMax, n) { n = n || 160; const xs = new Set(); for (let i = 0; i <= n; i++) xs.add(sMin + (i / n) * (sMax - sMin)); for (const k of kinks(legs)) if (k > sMin && k < sMax) xs.add(k); const sorted = [...xs].sort((a, b) => a - b); return sorted.map(s => ({ s, pnl: payoffAt(legs, s) })); } /* P&L « aujourd'hui » estimé par développement de Taylor sur les Greeks : * ΔP&L ≈ Σ qty·mult·( Δ·dS + ½Γ·dS² + Θ·jours + ν·dIV ) * S0 = spot de référence (jour de construction), days = jours écoulés, * dIV = variation d'IV en points (ex. +0.02). Estimation locale : fiable * près de S0, indicative loin de S0. */ function pnlToday(legs, S, S0, opts) { const days = (opts && opts.days) || 0; const dIV = (opts && opts.dIV) || 0; const dS = S - S0; let pnl = 0; for (const leg of legs) { const g = leg.greeks || {}; const k = leg.qty * multOf(leg); if (leg.kind === 's') { pnl += k * dS; continue; } pnl += k * ((g.delta || 0) * dS + 0.5 * (g.gamma || 0) * dS * dS + (g.theta || 0) * days + (g.vega || 0) * dIV * 100); } return pnl; } function pnlTodayCurve(legs, sMin, sMax, S0, opts, n) { n = n || 160; const out = []; for (let i = 0; i <= n; i++) { const s = sMin + (i / n) * (sMax - sMin); out.push({ s, pnl: pnlToday(legs, s, S0, opts) }); } return out; } /* Prix de référence d'un contrat : mid (bid+ask)/2, fallback last. * Retourne {price, source: 'mid'|'last'|null}. */ function refPrice(rec) { const bid = +rec.bid || 0, ask = +rec.ask || 0, last = +rec.last_price || 0; if (bid > 0 && ask > 0 && ask >= bid) return { price: (bid + ask) / 2, source: 'mid' }; if (ask > 0 && bid === 0) return { price: last > 0 ? last : ask, source: last > 0 ? 'last' : 'mid' }; if (last > 0) return { price: last, source: 'last' }; return { price: 0, source: null }; } const M = { intrinsic, payoffAt, netCost, netGreeks, kinks, breakevens, extremes, payoffCurve, pnlToday, pnlTodayCurve, slopeRight, refPrice, multOf, }; if (typeof module !== 'undefined' && module.exports) module.exports = M; else (root.HF = root.HF || {}).optmath = M; })(typeof window !== 'undefined' ? window : globalThis);