summaryrefslogtreecommitdiff
path: root/frontend/lib/blackScholes.ts
blob: b4c831a2b5073bbe2278c5bd2e4d249e8f44cf64 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
export type OptionType = 'C' | 'P';

export interface Greeks {
  delta: number;
  gamma: number;
  vega: number;
  theta: number;
  rho: number;
}

function erf(x: number): number {
  const sign = x < 0 ? -1 : 1;
  x = Math.abs(x);
  const a1 = 0.254829592, a2 = -0.284496736, a3 = 1.421413741,
        a4 = -1.453152027, a5 = 1.061405429, p = 0.3275911;
  const t = 1.0 / (1.0 + p * x);
  const y = 1.0 - (((((a5 * t + a4) * t) + a3) * t + a2) * t + a1) * t * Math.exp(-x * x);
  return sign * y;
}

function normCdf(x: number): number {
  return 0.5 * (1 + erf(x / Math.SQRT2));
}

function normPdf(x: number): number {
  return Math.exp(-0.5 * x * x) / Math.sqrt(2 * Math.PI);
}

function d1d2(S: number, K: number, T: number, r: number, q: number, sigma: number): [number, number] {
  const vt = sigma * Math.sqrt(T);
  const d1 = (Math.log(S / K) + (r - q + 0.5 * sigma * sigma) * T) / vt;
  return [d1, d1 - vt];
}

export function bsPrice(S: number, K: number, T: number, r: number, q: number, sigma: number, type: OptionType): number {
  if (T <= 0 || sigma <= 0) return type === 'C' ? Math.max(S - K, 0) : Math.max(K - S, 0);
  const [d1, d2] = d1d2(S, K, T, r, q, sigma);
  const eqt = Math.exp(-q * T), ert = Math.exp(-r * T);
  if (type === 'C') return S * eqt * normCdf(d1) - K * ert * normCdf(d2);
  return K * ert * normCdf(-d2) - S * eqt * normCdf(-d1);
}

export function bsGreeks(S: number, K: number, T: number, r: number, q: number, sigma: number, type: OptionType): Greeks {
  const safeT = Math.max(T, 1e-6), safeS = Math.max(sigma, 1e-6);
  const [d1, d2] = d1d2(S, K, safeT, r, q, safeS);
  const eqt = Math.exp(-q * safeT), ert = Math.exp(-r * safeT);
  const pdf1 = normPdf(d1);
  const delta = type === 'C' ? eqt * normCdf(d1) : -eqt * normCdf(-d1);
  const gamma = eqt * pdf1 / (S * safeS * Math.sqrt(safeT));
  const vega = S * eqt * pdf1 * Math.sqrt(safeT) * 0.01;
  const thetaYr = type === 'C'
    ? (-S * pdf1 * safeS * eqt / (2 * Math.sqrt(safeT)) - r * K * ert * normCdf(d2) + q * S * eqt * normCdf(d1))
    : (-S * pdf1 * safeS * eqt / (2 * Math.sqrt(safeT)) + r * K * ert * normCdf(-d2) - q * S * eqt * normCdf(-d1));
  const theta = thetaYr / 365;
  const rho = type === 'C'
    ? K * safeT * ert * normCdf(d2) * 0.01
    : -K * safeT * ert * normCdf(-d2) * 0.01;
  return { delta, gamma, vega, theta, rho };
}

export function bsImpliedVol(S: number, K: number, T: number, r: number, q: number, mktPrice: number, type: OptionType): number {
  if (T <= 0) return NaN;
  let lo = 0.0001, hi = 5.0;
  if (mktPrice < bsPrice(S, K, T, r, q, lo, type) || mktPrice > bsPrice(S, K, T, r, q, hi, type)) return NaN;
  for (let i = 0; i < 100; i++) {
    const mid = (lo + hi) / 2;
    const pMid = bsPrice(S, K, T, r, q, mid, type);
    if (Math.abs(pMid - mktPrice) < 1e-6) return mid;
    if (pMid < mktPrice) lo = mid; else hi = mid;
  }
  return (lo + hi) / 2;
}

export function bsSynthIV(S: number, K: number, T: number, atmSigma: number): number {
  const logM = Math.log(K / S);
  const t = Math.max(T, 1 / 365);
  const termLift = 0.014 * (Math.sqrt(t) - Math.sqrt(30 / 365));
  const skew = -0.085 * logM / Math.sqrt(t * 4);
  const smile = 0.32 * (logM * logM) / Math.sqrt(t);
  return Math.max(0.04, Math.min(1.5, atmSigma + termLift + skew + smile));
}