stock-bot/libs/utils/src/calculations/options-pricing.ts

718 lines
No EOL
20 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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();
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);
}
/**
* 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);
}