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 — tests unitaires du module de calcul pur3 * Author : Simon-Pierre Boucher — contact@spboucher.ai4 * ----------------------------------------------------------------------------5 * node --test tests/6 * ==========================================================================*/7'use strict';8const { test } = require('node:test');9const assert = require('node:assert/strict');10const M = require('../web/js/options-math.js');1112const G0 = { delta: 0, gamma: 0, theta: 0, vega: 0, rho: 0 };13const leg = (kind, strike, qty, price, greeks) =>14 ({ kind, strike, qty, price, greeks: greeks || G0 });1516test('long call — payoff à l’échéance', () => {17 const legs = [leg('c', 100, 1, 5)];18 assert.equal(M.payoffAt(legs, 90), -500); // OTM : perte = prime19 assert.equal(M.payoffAt(legs, 100), -500); // ATM20 assert.equal(M.payoffAt(legs, 105), 0); // breakeven21 assert.equal(M.payoffAt(legs, 120), 1500); // ITM22 assert.deepEqual(M.breakevens(legs), [105]);23 const ex = M.extremes(legs);24 assert.equal(ex.maxProfit, Infinity);25 assert.equal(ex.maxLoss, -500);26 assert.equal(M.netCost(legs), 500); // débit27});2829test('short put — payoff, crédit et perte max bornée à S=0', () => {30 const legs = [leg('p', 50, -1, 2)];31 assert.equal(M.netCost(legs), -200); // crédit32 assert.equal(M.payoffAt(legs, 60), 200); // OTM : on garde la prime33 assert.equal(M.payoffAt(legs, 48), 0); // breakeven 4834 assert.equal(M.payoffAt(legs, 0), -4800); // pire cas35 assert.deepEqual(M.breakevens(legs), [48]);36 const ex = M.extremes(legs);37 assert.equal(ex.maxProfit, 200);38 assert.equal(ex.maxLoss, -4800);39});4041test('bull call spread — bornes et breakeven exacts', () => {42 const legs = [leg('c', 100, 1, 6), leg('c', 110, -1, 2)];43 assert.equal(M.netCost(legs), 400);44 assert.equal(M.payoffAt(legs, 95), -400);45 assert.equal(M.payoffAt(legs, 104), 0); // BE = 100 + 446 assert.equal(M.payoffAt(legs, 110), 600);47 assert.equal(M.payoffAt(legs, 150), 600); // plafonné48 assert.deepEqual(M.breakevens(legs), [104]);49 const ex = M.extremes(legs);50 assert.equal(ex.maxProfit, 600);51 assert.equal(ex.maxLoss, -400);52});5354test('straddle long — deux breakevens', () => {55 const legs = [leg('c', 100, 1, 4), leg('p', 100, 1, 3)];56 assert.deepEqual(M.breakevens(legs), [93, 107]);57 const ex = M.extremes(legs);58 assert.equal(ex.maxProfit, Infinity); // côté call59 assert.equal(ex.maxLoss, -700); // pile sur le strike60 assert.equal(M.payoffAt(legs, 100), -700);61});6263test('iron condor — quatre jambes, profit = crédit net, perte bornée', () => {64 // put 90 short / put 85 long / call 110 short / call 115 long, crédit net 2 $65 const legs = [66 leg('p', 90, -1, 1.8), leg('p', 85, 1, 0.8),67 leg('c', 110, -1, 1.6), leg('c', 115, 1, 0.6),68 ];69 assert.equal(+M.netCost(legs).toFixed(2), -200); // crédit 2.0070 assert.equal(+M.payoffAt(legs, 100).toFixed(6), 200); // au centre : on garde tout71 assert.equal(+M.payoffAt(legs, 85).toFixed(6), -300); // aile basse : largeur 5 − crédit 272 assert.equal(+M.payoffAt(legs, 120).toFixed(6), -300); // aile haute73 const be = M.breakevens(legs);74 assert.deepEqual(be, [88, 112]); // 90−2 et 110+275 const ex = M.extremes(legs);76 assert.equal(+ex.maxProfit.toFixed(6), 200);77 assert.equal(+ex.maxLoss.toFixed(6), -300);78});7980test('covered call — jambe action + call short', () => {81 const legs = [82 { kind: 's', strike: 0, qty: 100, price: 95, mult: 1, greeks: G0 },83 leg('c', 100, -1, 3),84 ];85 assert.equal(M.payoffAt(legs, 100), 800); // +500 action, +300 prime86 assert.equal(M.payoffAt(legs, 120), 800); // plafonné au-dessus du strike87 assert.equal(M.payoffAt(legs, 92), 0); // BE = 95 − 388 assert.deepEqual(M.breakevens(legs), [92]);89 const ex = M.extremes(legs);90 assert.equal(ex.maxProfit, 800);91 assert.equal(ex.maxLoss, -9200); // action à 0, prime gardée92});9394test('netGreeks — agrégation signée ×100, action delta 1', () => {95 const legs = [96 leg('c', 100, 2, 4, { delta: 0.55, gamma: 0.02, theta: -0.05, vega: 0.11, rho: 0.03 }),97 leg('p', 95, -1, 2, { delta: -0.30, gamma: 0.015, theta: -0.04, vega: 0.09, rho: -0.02 }),98 { kind: 's', strike: 0, qty: 50, price: 98, mult: 1, greeks: G0 },99 ];100 const g = M.netGreeks(legs);101 assert.equal(+g.delta.toFixed(4), 2 * 100 * 0.55 - 100 * -0.30 + 50); // 190102 assert.equal(+g.gamma.toFixed(4), +(200 * 0.02 - 100 * 0.015).toFixed(4));103 assert.equal(+g.theta.toFixed(4), +(200 * -0.05 - 100 * -0.04).toFixed(4));104 assert.equal(+g.vega.toFixed(4), +(200 * 0.11 - 100 * 0.09).toFixed(4));105});106107test('pnlToday — Taylor : delta, gamma, theta', () => {108 const legs = [leg('c', 100, 1, 5, { delta: 0.5, gamma: 0.04, theta: -0.08, vega: 0.1, rho: 0 })];109 // dS = +2 : 100·(0.5·2 + 0.5·0.04·4) = 108110 assert.equal(+M.pnlToday(legs, 102, 100).toFixed(4), 108);111 // 3 jours plus tard sans bouger : 100·(−0.08·3) = −24112 assert.equal(+M.pnlToday(legs, 100, 100, { days: 3 }).toFixed(4), -24);113 // jambe action : delta pur114 const stock = [{ kind: 's', strike: 0, qty: 100, price: 95, mult: 1, greeks: G0 }];115 assert.equal(M.pnlToday(stock, 97, 95), 200);116});117118test('payoffCurve — inclut les strikes comme points exacts', () => {119 const legs = [leg('c', 100, 1, 6), leg('c', 110, -1, 2)];120 const curve = M.payoffCurve(legs, 80, 130, 25);121 assert.ok(curve.some(p => p.s === 100));122 assert.ok(curve.some(p => p.s === 110));123 assert.ok(curve.length >= 26);124 const last = curve[curve.length - 1];125 assert.equal(last.pnl, 600);126});127128test('refPrice — mid, fallback last, illiquide', () => {129 assert.deepEqual(M.refPrice({ bid: 1.0, ask: 1.2, last_price: 1.4 }), { price: 1.1, source: 'mid' });130 assert.deepEqual(M.refPrice({ bid: 0, ask: 0, last_price: 0.9 }), { price: 0.9, source: 'last' });131 assert.deepEqual(M.refPrice({ bid: 0, ask: 0.01, last_price: 0 }), { price: 0.01, source: 'mid' });132 assert.deepEqual(M.refPrice({ bid: 0, ask: 0, last_price: 0 }), { price: 0, source: null });133});134