added calcs

This commit is contained in:
Bojan Kucera 2025-06-03 14:54:02 -04:00
parent ef12c9d308
commit 7886b7cfa5
10 changed files with 4331 additions and 0 deletions

View file

@ -0,0 +1,504 @@
/**
* Options Pricing Models
* Implementation of various options pricing models and Greeks calculations
*/
export interface OptionParameters {
spotPrice: number;
strikePrice: number;
timeToExpiry: number; // in years
riskFreeRate: number;
volatility: number;
dividendYield?: number;
}
export interface OptionPricing {
callPrice: number;
putPrice: number;
intrinsicValueCall: number;
intrinsicValuePut: number;
timeValueCall: number;
timeValuePut: number;
}
export interface GreeksCalculation {
delta: number;
gamma: number;
theta: number;
vega: number;
rho: number;
}
export interface ImpliedVolatilityResult {
impliedVolatility: number;
iterations: number;
converged: boolean;
}
/**
* Black-Scholes option pricing model
*/
export function blackScholes(params: OptionParameters): OptionPricing {
const { spotPrice, strikePrice, timeToExpiry, riskFreeRate, volatility, dividendYield = 0 } = params;
if (timeToExpiry <= 0) {
const intrinsicValueCall = Math.max(spotPrice - strikePrice, 0);
const intrinsicValuePut = Math.max(strikePrice - spotPrice, 0);
return {
callPrice: intrinsicValueCall,
putPrice: intrinsicValuePut,
intrinsicValueCall,
intrinsicValuePut,
timeValueCall: 0,
timeValuePut: 0
};
}
const d1 = (Math.log(spotPrice / strikePrice) + (riskFreeRate - dividendYield + 0.5 * volatility * volatility) * timeToExpiry) /
(volatility * Math.sqrt(timeToExpiry));
const d2 = d1 - volatility * Math.sqrt(timeToExpiry);
const nd1 = normalCDF(d1);
const nd2 = normalCDF(d2);
const nMinusd1 = normalCDF(-d1);
const nMinusd2 = normalCDF(-d2);
const callPrice = spotPrice * Math.exp(-dividendYield * timeToExpiry) * nd1 -
strikePrice * Math.exp(-riskFreeRate * timeToExpiry) * nd2;
const putPrice = strikePrice * Math.exp(-riskFreeRate * timeToExpiry) * nMinusd2 -
spotPrice * Math.exp(-dividendYield * timeToExpiry) * nMinusd1;
const intrinsicValueCall = Math.max(spotPrice - strikePrice, 0);
const intrinsicValuePut = Math.max(strikePrice - spotPrice, 0);
const timeValueCall = callPrice - intrinsicValueCall;
const timeValuePut = putPrice - intrinsicValuePut;
return {
callPrice,
putPrice,
intrinsicValueCall,
intrinsicValuePut,
timeValueCall,
timeValuePut
};
}
/**
* Calculate option Greeks using Black-Scholes model
*/
export function calculateGreeks(params: OptionParameters, optionType: 'call' | 'put' = 'call'): GreeksCalculation {
const { spotPrice, strikePrice, timeToExpiry, riskFreeRate, volatility, dividendYield = 0 } = params;
if (timeToExpiry <= 0) {
return {
delta: optionType === 'call' ? (spotPrice > strikePrice ? 1 : 0) : (spotPrice < strikePrice ? -1 : 0),
gamma: 0,
theta: 0,
vega: 0,
rho: 0
};
}
const d1 = (Math.log(spotPrice / strikePrice) + (riskFreeRate - dividendYield + 0.5 * volatility * volatility) * timeToExpiry) /
(volatility * Math.sqrt(timeToExpiry));
const d2 = d1 - volatility * Math.sqrt(timeToExpiry);
const nd1 = normalCDF(d1);
const nd2 = normalCDF(d2);
const npd1 = normalPDF(d1);
// Delta
const callDelta = Math.exp(-dividendYield * timeToExpiry) * nd1;
const putDelta = Math.exp(-dividendYield * timeToExpiry) * (nd1 - 1);
const delta = optionType === 'call' ? callDelta : putDelta;
// Gamma (same for calls and puts)
const gamma = Math.exp(-dividendYield * timeToExpiry) * npd1 /
(spotPrice * volatility * Math.sqrt(timeToExpiry));
// Theta
const term1 = -(spotPrice * npd1 * volatility * Math.exp(-dividendYield * timeToExpiry)) /
(2 * Math.sqrt(timeToExpiry));
const term2Call = riskFreeRate * strikePrice * Math.exp(-riskFreeRate * timeToExpiry) * nd2;
const term2Put = -riskFreeRate * strikePrice * Math.exp(-riskFreeRate * timeToExpiry) * normalCDF(-d2);
const term3 = dividendYield * spotPrice * Math.exp(-dividendYield * timeToExpiry) *
(optionType === 'call' ? nd1 : normalCDF(-d1));
const theta = optionType === 'call' ?
(term1 - term2Call + term3) / 365 :
(term1 + term2Put + term3) / 365;
// Vega (same for calls and puts)
const vega = spotPrice * Math.exp(-dividendYield * timeToExpiry) * npd1 * Math.sqrt(timeToExpiry) / 100;
// Rho
const callRho = strikePrice * timeToExpiry * Math.exp(-riskFreeRate * timeToExpiry) * nd2 / 100;
const putRho = -strikePrice * timeToExpiry * Math.exp(-riskFreeRate * timeToExpiry) * normalCDF(-d2) / 100;
const rho = optionType === 'call' ? callRho : putRho;
return {
delta,
gamma,
theta,
vega,
rho
};
}
/**
* Calculate implied volatility using Newton-Raphson method
*/
export function calculateImpliedVolatility(
marketPrice: number,
spotPrice: number,
strikePrice: number,
timeToExpiry: number,
riskFreeRate: number,
optionType: 'call' | 'put' = 'call',
dividendYield: number = 0,
initialGuess: number = 0.2,
tolerance: number = 1e-6,
maxIterations: number = 100
): ImpliedVolatilityResult {
let volatility = initialGuess;
let iterations = 0;
let converged = false;
for (let i = 0; i < maxIterations; i++) {
iterations = i + 1;
const params: OptionParameters = {
spotPrice,
strikePrice,
timeToExpiry,
riskFreeRate,
volatility,
dividendYield
};
const pricing = blackScholes(params);
const theoreticalPrice = optionType === 'call' ? pricing.callPrice : pricing.putPrice;
const priceDiff = theoreticalPrice - marketPrice;
if (Math.abs(priceDiff) < tolerance) {
converged = true;
break;
}
// Calculate vega for Newton-Raphson
const greeks = calculateGreeks(params, optionType);
const vega = greeks.vega * 100; // Convert back from percentage
if (Math.abs(vega) < 1e-10) {
break; // Avoid division by zero
}
volatility = volatility - priceDiff / vega;
// Keep volatility within reasonable bounds
volatility = Math.max(0.001, Math.min(volatility, 10));
}
return {
impliedVolatility: volatility,
iterations,
converged
};
}
/**
* Binomial option pricing model
*/
export function binomialOptionPricing(
params: OptionParameters,
optionType: 'call' | 'put' = 'call',
americanStyle: boolean = false,
steps: number = 100
): OptionPricing {
const { spotPrice, strikePrice, timeToExpiry, riskFreeRate, volatility, dividendYield = 0 } = params;
const dt = timeToExpiry / steps;
const u = Math.exp(volatility * Math.sqrt(dt));
const d = 1 / u;
const p = (Math.exp((riskFreeRate - dividendYield) * dt) - d) / (u - d);
const discount = Math.exp(-riskFreeRate * dt);
// Create price tree
const stockPrices: number[][] = [];
for (let i = 0; i <= steps; i++) {
stockPrices[i] = [];
for (let j = 0; j <= i; j++) {
stockPrices[i][j] = spotPrice * Math.pow(u, i - j) * Math.pow(d, j);
}
}
// Calculate option values at expiration
const optionValues: number[][] = [];
for (let i = 0; i <= steps; i++) {
optionValues[i] = [];
}
for (let j = 0; j <= steps; j++) {
if (optionType === 'call') {
optionValues[steps][j] = Math.max(stockPrices[steps][j] - strikePrice, 0);
} else {
optionValues[steps][j] = Math.max(strikePrice - stockPrices[steps][j], 0);
}
}
// Work backwards through the tree
for (let i = steps - 1; i >= 0; i--) {
for (let j = 0; j <= i; j++) {
// European option value
const holdValue = discount * (p * optionValues[i + 1][j] + (1 - p) * optionValues[i + 1][j + 1]);
if (americanStyle) {
// American option - can exercise early
const exerciseValue = optionType === 'call' ?
Math.max(stockPrices[i][j] - strikePrice, 0) :
Math.max(strikePrice - stockPrices[i][j], 0);
optionValues[i][j] = Math.max(holdValue, exerciseValue);
} else {
optionValues[i][j] = holdValue;
}
}
}
const price = optionValues[0][0];
const intrinsicValue = optionType === 'call' ?
Math.max(spotPrice - strikePrice, 0) :
Math.max(strikePrice - spotPrice, 0);
const timeValue = price - intrinsicValue;
if (optionType === 'call') {
return {
callPrice: price,
putPrice: 0, // Not calculated
intrinsicValueCall: intrinsicValue,
intrinsicValuePut: 0,
timeValueCall: timeValue,
timeValuePut: 0
};
} else {
return {
callPrice: 0, // Not calculated
putPrice: price,
intrinsicValueCall: 0,
intrinsicValuePut: intrinsicValue,
timeValueCall: 0,
timeValuePut: timeValue
};
}
}
/**
* Monte Carlo option pricing
*/
export function monteCarloOptionPricing(
params: OptionParameters,
optionType: 'call' | 'put' = 'call',
numSimulations: number = 100000
): OptionPricing {
const { spotPrice, strikePrice, timeToExpiry, riskFreeRate, volatility, dividendYield = 0 } = params;
let totalPayoff = 0;
for (let i = 0; i < numSimulations; i++) {
// Generate random price path
const z = boxMullerTransform();
const finalPrice = spotPrice * Math.exp(
(riskFreeRate - dividendYield - 0.5 * volatility * volatility) * timeToExpiry +
volatility * Math.sqrt(timeToExpiry) * z
);
// Calculate payoff
const payoff = optionType === 'call' ?
Math.max(finalPrice - strikePrice, 0) :
Math.max(strikePrice - finalPrice, 0);
totalPayoff += payoff;
}
const averagePayoff = totalPayoff / numSimulations;
const price = averagePayoff * Math.exp(-riskFreeRate * timeToExpiry);
const intrinsicValue = optionType === 'call' ?
Math.max(spotPrice - strikePrice, 0) :
Math.max(strikePrice - spotPrice, 0);
const timeValue = price - intrinsicValue;
if (optionType === 'call') {
return {
callPrice: price,
putPrice: 0,
intrinsicValueCall: intrinsicValue,
intrinsicValuePut: 0,
timeValueCall: timeValue,
timeValuePut: 0
};
} else {
return {
callPrice: 0,
putPrice: price,
intrinsicValueCall: 0,
intrinsicValuePut: intrinsicValue,
timeValueCall: 0,
timeValuePut: timeValue
};
}
}
/**
* Calculate option portfolio risk metrics
*/
export function calculateOptionPortfolioRisk(
positions: Array<{
optionType: 'call' | 'put';
quantity: number;
params: OptionParameters;
}>
): {
totalDelta: number;
totalGamma: number;
totalTheta: number;
totalVega: number;
totalRho: number;
portfolioValue: number;
} {
let totalDelta = 0;
let totalGamma = 0;
let totalTheta = 0;
let totalVega = 0;
let totalRho = 0;
let portfolioValue = 0;
for (const position of positions) {
const greeks = calculateGreeks(position.params, position.optionType);
const pricing = blackScholes(position.params);
const optionPrice = position.optionType === 'call' ? pricing.callPrice : pricing.putPrice;
totalDelta += greeks.delta * position.quantity;
totalGamma += greeks.gamma * position.quantity;
totalTheta += greeks.theta * position.quantity;
totalVega += greeks.vega * position.quantity;
totalRho += greeks.rho * position.quantity;
portfolioValue += optionPrice * position.quantity;
}
return {
totalDelta,
totalGamma,
totalTheta,
totalVega,
totalRho,
portfolioValue
};
}
/**
* Volatility surface interpolation
*/
export function interpolateVolatilitySurface(
strikes: number[],
expiries: number[],
volatilities: number[][],
targetStrike: number,
targetExpiry: number
): number {
// Simplified bilinear interpolation
// In production, use more sophisticated interpolation methods
// Find surrounding points
let strikeIndex = 0;
let expiryIndex = 0;
for (let i = 0; i < strikes.length - 1; i++) {
if (targetStrike >= strikes[i] && targetStrike <= strikes[i + 1]) {
strikeIndex = i;
break;
}
}
for (let i = 0; i < expiries.length - 1; i++) {
if (targetExpiry >= expiries[i] && targetExpiry <= expiries[i + 1]) {
expiryIndex = i;
break;
}
}
// Bilinear interpolation
const x1 = strikes[strikeIndex];
const x2 = strikes[strikeIndex + 1];
const y1 = expiries[expiryIndex];
const y2 = expiries[expiryIndex + 1];
const q11 = volatilities[expiryIndex][strikeIndex];
const q12 = volatilities[expiryIndex + 1][strikeIndex];
const q21 = volatilities[expiryIndex][strikeIndex + 1];
const q22 = volatilities[expiryIndex + 1][strikeIndex + 1];
const wx = (targetStrike - x1) / (x2 - x1);
const wy = (targetExpiry - y1) / (y2 - y1);
return q11 * (1 - wx) * (1 - wy) +
q21 * wx * (1 - wy) +
q12 * (1 - wx) * wy +
q22 * wx * wy;
}
// Helper functions
/**
* Normal cumulative distribution function
*/
function normalCDF(x: number): number {
return 0.5 * (1 + erf(x / Math.sqrt(2)));
}
/**
* Normal probability density function
*/
function normalPDF(x: number): number {
return Math.exp(-0.5 * x * x) / Math.sqrt(2 * Math.PI);
}
/**
* Error function approximation
*/
function erf(x: number): number {
// Abramowitz and Stegun approximation
const a1 = 0.254829592;
const a2 = -0.284496736;
const a3 = 1.421413741;
const a4 = -1.453152027;
const a5 = 1.061405429;
const p = 0.3275911;
const sign = x >= 0 ? 1 : -1;
x = Math.abs(x);
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;
}
/**
* Box-Muller transformation for normal random numbers
*/
function boxMullerTransform(): number {
let u1 = Math.random();
let u2 = Math.random();
// Ensure u1 is not zero
while (u1 === 0) {
u1 = Math.random();
}
return Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);
}