added calcs
This commit is contained in:
parent
ef12c9d308
commit
7886b7cfa5
10 changed files with 4331 additions and 0 deletions
504
libs/utils/src/calculations/options-pricing.ts
Normal file
504
libs/utils/src/calculations/options-pricing.ts
Normal 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);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue