reworked calcs lib

This commit is contained in:
Boki 2025-06-19 10:35:38 -04:00
parent da75979574
commit 0397796cfb
11 changed files with 2648 additions and 2359 deletions

View file

@ -0,0 +1,810 @@
/**
* 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,
};
}
export function impliedVolatility(
price: number,
S: number,
K: number,
T: number,
r: number,
isCall = true
): number {
// …NewtonRaphson on σ to match blackScholesPrice
let sigma = 0.2; // Initial guess for volatility
const tolerance = 1e-6;
const maxIterations = 100;
let iteration = 0;
let priceDiff = 1; // Initialize to a non-zero value
while (Math.abs(priceDiff) > tolerance && iteration < maxIterations) {
const params: OptionParameters = {
spotPrice: S,
strikePrice: K,
timeToExpiry: T,
riskFreeRate: r,
volatility: sigma,
};
const calculatedPrice = isCall ? blackScholes(params).callPrice : blackScholes(params).putPrice;
priceDiff = calculatedPrice - price;
// Calculate Vega
const greeks = calculateGreeks(params, isCall ? 'call' : 'put');
const vega = greeks.vega * 100; // Convert from percentage to absolute
if (vega === 0) {
break; // Avoid division by zero
}
sigma -= priceDiff / vega; // Update volatility estimate
iteration++;
}
if (iteration === maxIterations) {
console.warn('Implied volatility calculation did not converge');
}
if (sigma < 0) {
console.warn('Calculated implied volatility is negative, returning 0');
return 0;
}
if (sigma > 10) {
console.warn('Calculated implied volatility is too high, returning 10');
return 10; // Cap at a reasonable maximum
}
if (isNaN(sigma)) {
console.warn('Calculated implied volatility is NaN, returning 0');
return 0;
}
return sigma;
}
/**
* 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();
const 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);
}
/**
* Prices a straddle option strategy
*/
export function straddle(params: OptionParameters): {
callPrice: number;
putPrice: number;
strategyCost: number;
} {
const callOption = blackScholes(params);
const putOption = blackScholes(params);
const strategyCost = callOption.callPrice + putOption.putPrice;
return {
callPrice: callOption.callPrice,
putPrice: putOption.putPrice,
strategyCost: strategyCost,
};
}
/**
* Prices a strangle option strategy
*/
export function strangle(
callParams: OptionParameters,
putParams: OptionParameters
): { callPrice: number; putPrice: number; strategyCost: number } {
const callOption = blackScholes(callParams);
const putOption = blackScholes(putParams);
const strategyCost = callOption.callPrice + putOption.putPrice;
return {
callPrice: callOption.callPrice,
putPrice: putOption.putPrice,
strategyCost: strategyCost,
};
}
/**
* Prices a butterfly option strategy
*/
export function butterfly(
lowerStrikeParams: OptionParameters,
middleStrikeParams: OptionParameters,
upperStrikeParams: OptionParameters
): {
lowerCallPrice: number;
middleCallPrice: number;
upperCallPrice: number;
strategyCost: number;
} {
const lowerCall = blackScholes(lowerStrikeParams);
const middleCall = blackScholes(middleStrikeParams);
const upperCall = blackScholes(upperStrikeParams);
const strategyCost = lowerCall.callPrice - 2 * middleCall.callPrice + upperCall.callPrice;
return {
lowerCallPrice: lowerCall.callPrice,
middleCallPrice: middleCall.callPrice,
upperCallPrice: upperCall.callPrice,
strategyCost: strategyCost,
};
}
/**
* Prices a condor option strategy
*/
export function condor(
lowerStrikeParams: OptionParameters,
middleLowerStrikeParams: OptionParameters,
middleUpperStrikeParams: OptionParameters,
upperStrikeParams: OptionParameters
): {
lowerCallPrice: number;
middleLowerCallPrice: number;
middleUpperCallPrice: number;
upperCallPrice: number;
strategyCost: number;
} {
const lowerCall = blackScholes(lowerStrikeParams);
const middleLowerCall = blackScholes(middleLowerStrikeParams);
const middleUpperCall = blackScholes(middleUpperStrikeParams);
const upperCall = blackScholes(upperStrikeParams);
const strategyCost =
lowerCall.callPrice -
middleLowerCall.callPrice -
middleUpperCall.callPrice +
upperCall.callPrice;
return {
lowerCallPrice: lowerCall.callPrice,
middleLowerCallPrice: middleLowerCall.callPrice,
middleUpperCallPrice: middleUpperCall.callPrice,
upperCallPrice: upperCall.callPrice,
strategyCost: strategyCost,
};
}
/**
* Calculates combined Greeks for an option strategy
*/
export function calculateStrategyGreeks(
positions: Array<{
optionType: 'call' | 'put';
quantity: number;
params: OptionParameters;
}>
): GreeksCalculation {
let totalDelta = 0;
let totalGamma = 0;
let totalTheta = 0;
let totalVega = 0;
let totalRho = 0;
for (const position of positions) {
const greeks = calculateGreeks(position.params, position.optionType);
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;
}
return {
delta: totalDelta,
gamma: totalGamma,
theta: totalTheta,
vega: totalVega,
rho: totalRho,
};
}
/**
* Black-Scholes option pricing model with greeks
*/
export function blackScholesWithGreeks(
params: OptionParameters,
optionType: 'call' | 'put' = 'call'
): { pricing: OptionPricing; greeks: GreeksCalculation } {
const pricing = blackScholes(params);
const greeks = calculateGreeks(params, optionType);
return { pricing, greeks };
}
/**
* Calculates the breakeven point for a call option at expiration
*/
export function callBreakeven(strikePrice: number, callPrice: number): number {
return strikePrice + callPrice;
}
/**
* Calculates the breakeven point for a put option at expiration
*/
export function putBreakeven(strikePrice: number, putPrice: number): number {
return strikePrice - putPrice;
}
/**
* Estimates the probability of profit for a call option at expiration
*/
export function callProbabilityOfProfit(
spotPrice: number,
strikePrice: number,
timeToExpiry: number,
riskFreeRate: number,
volatility: number
): number {
const d1 =
(Math.log(spotPrice / strikePrice) +
(riskFreeRate + 0.5 * volatility * volatility) * timeToExpiry) /
(volatility * Math.sqrt(timeToExpiry));
return normalCDF(d1);
}
/**
* Estimates the probability of profit for a put option at expiration
*/
export function putProbabilityOfProfit(
spotPrice: number,
strikePrice: number,
timeToExpiry: number,
riskFreeRate: number,
volatility: number
): number {
const d1 =
(Math.log(spotPrice / strikePrice) +
(riskFreeRate + 0.5 * volatility * volatility) * timeToExpiry) /
(volatility * Math.sqrt(timeToExpiry));
return 1 - normalCDF(d1);
}