/* ============================================================================ * HFChart Options Lab — tests unitaires du module de calcul pur * Author : Simon-Pierre Boucher — contact@spboucher.ai * ---------------------------------------------------------------------------- * node --test tests/ * ==========================================================================*/ 'use strict'; const { test } = require('node:test'); const assert = require('node:assert/strict'); const M = require('../web/js/options-math.js'); const G0 = { delta: 0, gamma: 0, theta: 0, vega: 0, rho: 0 }; const leg = (kind, strike, qty, price, greeks) => ({ kind, strike, qty, price, greeks: greeks || G0 }); test('long call — payoff à l’échéance', () => { const legs = [leg('c', 100, 1, 5)]; assert.equal(M.payoffAt(legs, 90), -500); // OTM : perte = prime assert.equal(M.payoffAt(legs, 100), -500); // ATM assert.equal(M.payoffAt(legs, 105), 0); // breakeven assert.equal(M.payoffAt(legs, 120), 1500); // ITM assert.deepEqual(M.breakevens(legs), [105]); const ex = M.extremes(legs); assert.equal(ex.maxProfit, Infinity); assert.equal(ex.maxLoss, -500); assert.equal(M.netCost(legs), 500); // débit }); test('short put — payoff, crédit et perte max bornée à S=0', () => { const legs = [leg('p', 50, -1, 2)]; assert.equal(M.netCost(legs), -200); // crédit assert.equal(M.payoffAt(legs, 60), 200); // OTM : on garde la prime assert.equal(M.payoffAt(legs, 48), 0); // breakeven 48 assert.equal(M.payoffAt(legs, 0), -4800); // pire cas assert.deepEqual(M.breakevens(legs), [48]); const ex = M.extremes(legs); assert.equal(ex.maxProfit, 200); assert.equal(ex.maxLoss, -4800); }); test('bull call spread — bornes et breakeven exacts', () => { const legs = [leg('c', 100, 1, 6), leg('c', 110, -1, 2)]; assert.equal(M.netCost(legs), 400); assert.equal(M.payoffAt(legs, 95), -400); assert.equal(M.payoffAt(legs, 104), 0); // BE = 100 + 4 assert.equal(M.payoffAt(legs, 110), 600); assert.equal(M.payoffAt(legs, 150), 600); // plafonné assert.deepEqual(M.breakevens(legs), [104]); const ex = M.extremes(legs); assert.equal(ex.maxProfit, 600); assert.equal(ex.maxLoss, -400); }); test('straddle long — deux breakevens', () => { const legs = [leg('c', 100, 1, 4), leg('p', 100, 1, 3)]; assert.deepEqual(M.breakevens(legs), [93, 107]); const ex = M.extremes(legs); assert.equal(ex.maxProfit, Infinity); // côté call assert.equal(ex.maxLoss, -700); // pile sur le strike assert.equal(M.payoffAt(legs, 100), -700); }); test('iron condor — quatre jambes, profit = crédit net, perte bornée', () => { // put 90 short / put 85 long / call 110 short / call 115 long, crédit net 2 $ const legs = [ leg('p', 90, -1, 1.8), leg('p', 85, 1, 0.8), leg('c', 110, -1, 1.6), leg('c', 115, 1, 0.6), ]; assert.equal(+M.netCost(legs).toFixed(2), -200); // crédit 2.00 assert.equal(+M.payoffAt(legs, 100).toFixed(6), 200); // au centre : on garde tout assert.equal(+M.payoffAt(legs, 85).toFixed(6), -300); // aile basse : largeur 5 − crédit 2 assert.equal(+M.payoffAt(legs, 120).toFixed(6), -300); // aile haute const be = M.breakevens(legs); assert.deepEqual(be, [88, 112]); // 90−2 et 110+2 const ex = M.extremes(legs); assert.equal(+ex.maxProfit.toFixed(6), 200); assert.equal(+ex.maxLoss.toFixed(6), -300); }); test('covered call — jambe action + call short', () => { const legs = [ { kind: 's', strike: 0, qty: 100, price: 95, mult: 1, greeks: G0 }, leg('c', 100, -1, 3), ]; assert.equal(M.payoffAt(legs, 100), 800); // +500 action, +300 prime assert.equal(M.payoffAt(legs, 120), 800); // plafonné au-dessus du strike assert.equal(M.payoffAt(legs, 92), 0); // BE = 95 − 3 assert.deepEqual(M.breakevens(legs), [92]); const ex = M.extremes(legs); assert.equal(ex.maxProfit, 800); assert.equal(ex.maxLoss, -9200); // action à 0, prime gardée }); test('netGreeks — agrégation signée ×100, action delta 1', () => { const legs = [ leg('c', 100, 2, 4, { delta: 0.55, gamma: 0.02, theta: -0.05, vega: 0.11, rho: 0.03 }), leg('p', 95, -1, 2, { delta: -0.30, gamma: 0.015, theta: -0.04, vega: 0.09, rho: -0.02 }), { kind: 's', strike: 0, qty: 50, price: 98, mult: 1, greeks: G0 }, ]; const g = M.netGreeks(legs); assert.equal(+g.delta.toFixed(4), 2 * 100 * 0.55 - 100 * -0.30 + 50); // 190 assert.equal(+g.gamma.toFixed(4), +(200 * 0.02 - 100 * 0.015).toFixed(4)); assert.equal(+g.theta.toFixed(4), +(200 * -0.05 - 100 * -0.04).toFixed(4)); assert.equal(+g.vega.toFixed(4), +(200 * 0.11 - 100 * 0.09).toFixed(4)); }); test('pnlToday — Taylor : delta, gamma, theta', () => { const legs = [leg('c', 100, 1, 5, { delta: 0.5, gamma: 0.04, theta: -0.08, vega: 0.1, rho: 0 })]; // dS = +2 : 100·(0.5·2 + 0.5·0.04·4) = 108 assert.equal(+M.pnlToday(legs, 102, 100).toFixed(4), 108); // 3 jours plus tard sans bouger : 100·(−0.08·3) = −24 assert.equal(+M.pnlToday(legs, 100, 100, { days: 3 }).toFixed(4), -24); // jambe action : delta pur const stock = [{ kind: 's', strike: 0, qty: 100, price: 95, mult: 1, greeks: G0 }]; assert.equal(M.pnlToday(stock, 97, 95), 200); }); test('payoffCurve — inclut les strikes comme points exacts', () => { const legs = [leg('c', 100, 1, 6), leg('c', 110, -1, 2)]; const curve = M.payoffCurve(legs, 80, 130, 25); assert.ok(curve.some(p => p.s === 100)); assert.ok(curve.some(p => p.s === 110)); assert.ok(curve.length >= 26); const last = curve[curve.length - 1]; assert.equal(last.pnl, 600); }); test('refPrice — mid, fallback last, illiquide', () => { assert.deepEqual(M.refPrice({ bid: 1.0, ask: 1.2, last_price: 1.4 }), { price: 1.1, source: 'mid' }); assert.deepEqual(M.refPrice({ bid: 0, ask: 0, last_price: 0.9 }), { price: 0.9, source: 'last' }); assert.deepEqual(M.refPrice({ bid: 0, ask: 0.01, last_price: 0 }), { price: 0.01, source: 'mid' }); assert.deepEqual(M.refPrice({ bid: 0, ask: 0, last_price: 0 }), { price: 0, source: null }); });