spb/hfchart Public
HFChart — la référence des charts haute fréquence : 14 types rendus canvas from scratch (zéro dépendance), données HF Market Data — www.hfchart.io
JavaScript 82.1%
CSS 10%
HTML 5.8%
Python 2.1%
1/* ============================================================================2 * HFChart Options Lab — module de calcul PUR (payoff, breakevens, Greeks)3 * Author : Simon-Pierre Boucher — contact@spboucher.ai4 * ----------------------------------------------------------------------------5 * Aucune dépendance, aucun accès DOM/réseau : utilisable dans le navigateur6 * (window.HF.optmath) et sous Node pour les tests unitaires (module.exports).7 *8 * Une jambe (leg) :9 * {10 * kind : 'c' | 'p' | 's', // call, put, ou action (stock)11 * strike : number, // 0 / ignoré pour 's'12 * qty : number, // signé : + long, − short (contrats ; actions pour 's')13 * price : number, // prime par action (prix d'achat pour 's')14 * mult : number, // 100 par défaut, 1 pour 's'15 * greeks : {delta,gamma,theta,vega,rho} // par action, côté long (API HF Market Data)16 * }17 * Conventions : P&L en $ pour la position ; débit net positif = on paie.18 * ==========================================================================*/19(function (root) {20 'use strict';2122 const multOf = leg => (leg.mult != null ? leg.mult : (leg.kind === 's' ? 1 : 100));2324 /* Valeur intrinsèque par action à l'échéance. */25 function intrinsic(kind, strike, S) {26 if (kind === 'c') return Math.max(S - strike, 0);27 if (kind === 'p') return Math.max(strike - S, 0);28 return S; // 's' : l'action vaut S29 }3031 /* P&L de la position à l'échéance pour un spot S. */32 function payoffAt(legs, S) {33 let pnl = 0;34 for (const leg of legs) {35 pnl += leg.qty * multOf(leg) * (intrinsic(leg.kind, leg.strike, S) - leg.price);36 }37 return pnl;38 }3940 /* Coût net d'ouverture (débit > 0, crédit < 0). */41 function netCost(legs) {42 let c = 0;43 for (const leg of legs) c += leg.qty * multOf(leg) * leg.price;44 return c;45 }4647 /* Greeks agrégés de la position ($ par unité de facteur). */48 function netGreeks(legs) {49 const out = { delta: 0, gamma: 0, theta: 0, vega: 0, rho: 0 };50 for (const leg of legs) {51 const g = leg.greeks || {};52 const k = leg.qty * multOf(leg);53 out.delta += k * (leg.kind === 's' ? 1 : (g.delta || 0));54 out.gamma += k * (g.gamma || 0);55 out.theta += k * (g.theta || 0);56 out.vega += k * (g.vega || 0);57 out.rho += k * (g.rho || 0);58 }59 return out;60 }6162 /* Points de cassure du payoff : les strikes des jambes optionnelles. */63 function kinks(legs) {64 const ks = [...new Set(legs.filter(l => l.kind !== 's').map(l => l.strike))];65 ks.sort((a, b) => a - b);66 return ks;67 }6869 /* Points d'évaluation exacts : 0, chaque strike, et un point au-delà du70 * dernier strike (le payoff est linéaire par morceaux entre ces points). */71 function evalPoints(legs) {72 const ks = kinks(legs);73 const far = ks.length ? ks[ks.length - 1] * 1.5 + 100 : 200;74 return [0, ...ks, far];75 }7677 /* Pente du payoff au-delà du dernier strike ($ par $ de spot). */78 function slopeRight(legs) {79 let s = 0;80 for (const leg of legs) {81 if (leg.kind === 'c' || leg.kind === 's') s += leg.qty * multOf(leg);82 }83 return s;84 }8586 /* Breakevens exacts (zéros du payoff à l'échéance). */87 function breakevens(legs) {88 const pts = evalPoints(legs);89 const out = [];90 for (let i = 1; i < pts.length; i++) {91 const x0 = pts[i - 1], x1 = pts[i];92 const y0 = payoffAt(legs, x0), y1 = payoffAt(legs, x1);93 if (y0 === 0 && (i === 1 || payoffAt(legs, pts[i - 2]) !== 0)) out.push(x0);94 if ((y0 < 0 && y1 > 0) || (y0 > 0 && y1 < 0)) {95 out.push(x0 + (x1 - x0) * (-y0) / (y1 - y0));96 } else if (y1 === 0 && i === pts.length - 1) {97 out.push(x1);98 }99 }100 // au-delà du dernier point, la pente est constante : un zéro éventuel est déjà couvert101 return [...new Set(out.map(v => +v.toFixed(6)))].sort((a, b) => a - b);102 }103104 /* Profit max / perte max à l'échéance. Infinity si non borné à droite. */105 function extremes(legs) {106 const pts = evalPoints(legs);107 let maxP = -Infinity, minP = Infinity;108 for (const p of pts) {109 const y = payoffAt(legs, p);110 if (y > maxP) maxP = y;111 if (y < minP) minP = y;112 }113 const s = slopeRight(legs);114 if (s > 1e-9) maxP = Infinity;115 if (s < -1e-9) minP = -Infinity;116 return { maxProfit: maxP, maxLoss: minP };117 }118119 /* Courbe de payoff à l'échéance sur [sMin, sMax] — insère les strikes pour120 * que les cassures soient exactes. */121 function payoffCurve(legs, sMin, sMax, n) {122 n = n || 160;123 const xs = new Set();124 for (let i = 0; i <= n; i++) xs.add(sMin + (i / n) * (sMax - sMin));125 for (const k of kinks(legs)) if (k > sMin && k < sMax) xs.add(k);126 const sorted = [...xs].sort((a, b) => a - b);127 return sorted.map(s => ({ s, pnl: payoffAt(legs, s) }));128 }129130 /* P&L « aujourd'hui » estimé par développement de Taylor sur les Greeks :131 * ΔP&L ≈ Σ qty·mult·( Δ·dS + ½Γ·dS² + Θ·jours + ν·dIV )132 * S0 = spot de référence (jour de construction), days = jours écoulés,133 * dIV = variation d'IV en points (ex. +0.02). Estimation locale : fiable134 * près de S0, indicative loin de S0. */135 function pnlToday(legs, S, S0, opts) {136 const days = (opts && opts.days) || 0;137 const dIV = (opts && opts.dIV) || 0;138 const dS = S - S0;139 let pnl = 0;140 for (const leg of legs) {141 const g = leg.greeks || {};142 const k = leg.qty * multOf(leg);143 if (leg.kind === 's') { pnl += k * dS; continue; }144 pnl += k * ((g.delta || 0) * dS + 0.5 * (g.gamma || 0) * dS * dS +145 (g.theta || 0) * days + (g.vega || 0) * dIV * 100);146 }147 return pnl;148 }149150 function pnlTodayCurve(legs, sMin, sMax, S0, opts, n) {151 n = n || 160;152 const out = [];153 for (let i = 0; i <= n; i++) {154 const s = sMin + (i / n) * (sMax - sMin);155 out.push({ s, pnl: pnlToday(legs, s, S0, opts) });156 }157 return out;158 }159160 /* Prix de référence d'un contrat : mid (bid+ask)/2, fallback last.161 * Retourne {price, source: 'mid'|'last'|null}. */162 function refPrice(rec) {163 const bid = +rec.bid || 0, ask = +rec.ask || 0, last = +rec.last_price || 0;164 if (bid > 0 && ask > 0 && ask >= bid) return { price: (bid + ask) / 2, source: 'mid' };165 if (ask > 0 && bid === 0) return { price: last > 0 ? last : ask, source: last > 0 ? 'last' : 'mid' };166 if (last > 0) return { price: last, source: 'last' };167 return { price: 0, source: null };168 }169170 const M = {171 intrinsic, payoffAt, netCost, netGreeks, kinks, breakevens, extremes,172 payoffCurve, pnlToday, pnlTodayCurve, slopeRight, refPrice, multOf,173 };174175 if (typeof module !== 'undefined' && module.exports) module.exports = M;176 else (root.HF = root.HF || {}).optmath = M;177})(typeof window !== 'undefined' ? window : globalThis);178