/** * 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 { // …Newton–Raphson 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); }