bugfixing options calculator

This commit is contained in:
MuslemRahimi 2025-04-08 15:50:40 +02:00
parent c66a06f873
commit 22661562ab

View File

@ -279,7 +279,6 @@
// Calculate break-even and metrics for single-leg strategies // Calculate break-even and metrics for single-leg strategies
calculateMetrics(); calculateMetrics();
calculateBreakevenPrice(dataPoints); calculateBreakevenPrice(dataPoints);
console.log(userStrategy);
// Build the chart options // Build the chart options
const options = { const options = {
credits: { enabled: false }, credits: { enabled: false },
@ -429,17 +428,60 @@
return metrics; return metrics;
} }
// Classify legs by type // First, consolidate identical strikes with opposite actions (buy/sell)
const buyCalls = allLegs.filter( const consolidatedLegs = [];
const strikeMap = new Map();
// Group legs by strike price and option type
allLegs.forEach((leg) => {
const key = `${leg.strike}-${leg.optionType}`;
if (!strikeMap.has(key)) {
strikeMap.set(key, []);
}
strikeMap.get(key).push(leg);
});
// Consolidate legs with the same strike and option type
strikeMap.forEach((legs, key) => {
let netQuantity = 0;
let netCost = 0;
legs.forEach((leg) => {
const quantity = leg.quantity || 1;
if (leg.action === "Buy") {
netQuantity += quantity;
netCost += leg.optionPrice * quantity;
} else {
// Sell
netQuantity -= quantity;
netCost -= leg.optionPrice * quantity;
}
});
// Only add non-zero net positions
if (netQuantity !== 0) {
const [strike, optionType] = key.split("-");
consolidatedLegs.push({
strike: parseFloat(strike),
optionType,
optionPrice: Math.abs(netCost / netQuantity),
quantity: Math.abs(netQuantity),
action: netQuantity > 0 ? "Buy" : "Sell",
});
}
});
// Now work with consolidated legs
const buyCalls = consolidatedLegs.filter(
(leg) => leg.action === "Buy" && leg.optionType === "Call", (leg) => leg.action === "Buy" && leg.optionType === "Call",
); );
const sellCalls = allLegs.filter( const sellCalls = consolidatedLegs.filter(
(leg) => leg.action === "Sell" && leg.optionType === "Call", (leg) => leg.action === "Sell" && leg.optionType === "Call",
); );
const buyPuts = allLegs.filter( const buyPuts = consolidatedLegs.filter(
(leg) => leg.action === "Buy" && leg.optionType === "Put", (leg) => leg.action === "Buy" && leg.optionType === "Put",
); );
const sellPuts = allLegs.filter( const sellPuts = consolidatedLegs.filter(
(leg) => leg.action === "Sell" && leg.optionType === "Put", (leg) => leg.action === "Sell" && leg.optionType === "Put",
); );
@ -455,180 +497,231 @@
} }
}); });
// Check for standard vertical spreads first // Check if any positions have unlimited profit or loss
const isSimpleCallVertical = let hasUnlimitedProfit = false;
buyCalls.length === 1 && let hasUnlimitedLoss = false;
sellCalls.length === 1 &&
buyPuts.length === 0 &&
sellPuts.length === 0;
const isSimplePutVertical =
buyPuts.length === 1 &&
sellPuts.length === 1 &&
buyCalls.length === 0 &&
sellCalls.length === 0;
// Handle standard vertical spreads // Check for unlimited profit (net long calls)
if (isSimpleCallVertical) { if (buyCalls.length > 0) {
// Call vertical spread // Sort by strike price ascending
const buyCall = buyCalls[0]; const sortedBuyCalls = [...buyCalls].sort((a, b) => a.strike - b.strike);
const sellCall = sellCalls[0]; const sortedSellCalls = [...sellCalls].sort(
const buyQuantity = buyCall.quantity || 1; (a, b) => a.strike - b.strike,
const sellQuantity = sellCall.quantity || 1; );
// Only use vertical spread calculation if quantities match // If highest long call strike is higher than all short calls, or there are no short calls
if (buyQuantity === sellQuantity) { if (
if (buyCall.strike < sellCall.strike) { sellCalls.length === 0 ||
// Bull call spread (buy lower, sell higher) sortedBuyCalls[sortedBuyCalls.length - 1].strike >
const strikeDiff = sortedSellCalls[sortedSellCalls.length - 1].strike
(sellCall.strike - buyCall.strike) * multiplier * buyQuantity; ) {
const maxProfit = strikeDiff + netPremium; hasUnlimitedProfit = true;
const maxLoss = -netPremium; // Net debit is the max loss
metrics = {
maxProfit: `$${formatCurrency(maxProfit)}`,
maxLoss: `$${formatCurrency(maxLoss)}`,
};
return metrics;
} else {
// Bear call spread (buy higher, sell lower)
const strikeDiff =
(buyCall.strike - sellCall.strike) * multiplier * buyQuantity;
const maxProfit = netPremium; // Net credit is the max profit
const maxLoss = strikeDiff - netPremium; // Strike diff minus net credit
metrics = {
maxProfit: `$${formatCurrency(maxProfit)}`,
maxLoss: `$${formatCurrency(maxLoss)}`,
};
return metrics;
}
} }
} else if (isSimplePutVertical) {
// Put vertical spread
const buyPut = buyPuts[0];
const sellPut = sellPuts[0];
const buyQuantity = buyPut.quantity || 1;
const sellQuantity = sellPut.quantity || 1;
// Only use vertical spread calculation if quantities match // Also check quantities - if buy quantity > sell quantity
if (buyQuantity === sellQuantity) { const totalBuyCallQuantity = sortedBuyCalls.reduce(
if (buyPut.strike > sellPut.strike) { (sum, leg) => sum + (leg.quantity || 1),
// Bear put spread (buy higher, sell lower) 0,
const strikeDiff = );
(buyPut.strike - sellPut.strike) * multiplier * buyQuantity; const totalSellCallQuantity = sortedSellCalls.reduce(
const maxProfit = strikeDiff + netPremium; (sum, leg) => sum + (leg.quantity || 1),
const maxLoss = -netPremium; // Net debit is the max loss 0,
);
metrics = { if (totalBuyCallQuantity > totalSellCallQuantity) {
maxProfit: `$${formatCurrency(maxProfit)}`, hasUnlimitedProfit = true;
maxLoss: `$${formatCurrency(maxLoss)}`,
};
return metrics;
} else {
// Bull put spread (buy lower, sell higher)
const strikeDiff =
(sellPut.strike - buyPut.strike) * multiplier * buyQuantity;
const maxProfit = netPremium; // Net credit is the max profit
const maxLoss = strikeDiff - netPremium; // Strike diff minus net credit
metrics = {
maxProfit: `$${formatCurrency(maxProfit)}`,
maxLoss: `$${formatCurrency(maxLoss)}`,
};
return metrics;
}
} }
} }
// For complex or custom strategies, calculate P/L at different price points // Check for unlimited loss (net short calls)
if (sellCalls.length > 0) {
// Sort by strike price ascending
const sortedBuyCalls = [...buyCalls].sort((a, b) => a.strike - b.strike);
const sortedSellCalls = [...sellCalls].sort(
(a, b) => a.strike - b.strike,
);
// Generate an array of price points to evaluate // If highest short call strike is higher than all long calls, or there are no long calls
const strikes = allLegs.map((leg) => leg.strike); if (
const minStrike = Math.min(...strikes); buyCalls.length === 0 ||
const maxStrike = Math.max(...strikes); sortedSellCalls[sortedSellCalls.length - 1].strike >
sortedBuyCalls[sortedBuyCalls.length - 1].strike
) {
hasUnlimitedLoss = true;
}
// Include price points at 0, below the lowest strike, at each strike, and above the highest strike // Also check quantities - if sell quantity > buy quantity
const pricePoints = [0, minStrike * 0.5]; const totalBuyCallQuantity = sortedBuyCalls.reduce(
strikes.forEach((strike) => pricePoints.push(strike)); (sum, leg) => sum + (leg.quantity || 1),
pricePoints.push(maxStrike * 1.5, maxStrike * 2); 0,
);
const totalSellCallQuantity = sortedSellCalls.reduce(
(sum, leg) => sum + (leg.quantity || 1),
0,
);
// Add special price points for large moves if (totalSellCallQuantity > totalBuyCallQuantity) {
const hasShortCall = sellCalls.length > 0; hasUnlimitedLoss = true;
const hasLongPut = buyPuts.length > 0; }
const hasShortPut = sellPuts.length > 0; }
const hasLongCall = buyCalls.length > 0;
// Check for unlimited profit/loss scenarios // Calculate maximum loss
let unlimitedProfit = hasLongCall && !hasShortCall; let maxLoss = -netPremium; // Start with net premium paid
let unlimitedLoss = hasShortCall && !hasLongCall;
// Calculate P/L at each price point // For your specific strategy (buy lower strike, sell and buy same higher strike),
let maxProfit = -Infinity; // the max loss is the net premium paid
let maxLoss = -Infinity;
pricePoints.forEach((price) => { // Add logic for specific strategy patterns
let profitLoss = netPremium; // Start with net premium if (buyCalls.length > 0 && sellCalls.length > 0) {
// This is a complex strategy with both long and short calls
// For this specific pattern, max loss is typically the net premium
maxLoss = -netPremium;
}
// Calculate P/L contribution from each leg at this price point // Check for special case: Call Ratio Spread with lower strike bought
allLegs.forEach((leg) => { if (
const quantity = leg.quantity || 1; buyCalls.length === 1 &&
const strike = leg.strike; sellCalls.length === 1 &&
buyCalls[0].strike < sellCalls[0].strike &&
sellCalls[0].quantity > buyCalls[0].quantity
) {
// Call ratio spread with more short calls than long calls
const spreadWidth =
(sellCalls[0].strike - buyCalls[0].strike) * multiplier;
const buyQuantity = buyCalls[0].quantity || 1;
const sellQuantity = sellCalls[0].quantity || 1;
if (leg.optionType === "Call") { // Max loss can be unlimited if ratio is > 1
if (price > strike) { if (sellQuantity > buyQuantity) {
// Call is in the money hasUnlimitedLoss = true;
const intrinsicValue = (price - strike) * multiplier * quantity; }
if (leg.action === "Buy") {
profitLoss += intrinsicValue;
} else {
profitLoss -= intrinsicValue;
}
}
} else if (leg.optionType === "Put") {
if (price < strike) {
// Put is in the money
const intrinsicValue = (strike - price) * multiplier * quantity;
if (leg.action === "Buy") {
profitLoss += intrinsicValue;
} else {
profitLoss -= intrinsicValue;
}
}
}
});
// Update max profit and max loss // Max profit is at the short strike
maxProfit = Math.max(maxProfit, profitLoss); const maxProfit = spreadWidth * buyQuantity + netPremium;
maxLoss = Math.min(maxLoss, profitLoss);
});
maxLoss = Math.abs(maxLoss); metrics = {
maxProfit: `$${formatCurrency(maxProfit)}`,
maxLoss: hasUnlimitedLoss
? "Unlimited"
: `$${formatCurrency(Math.abs(maxLoss))}`,
};
return metrics;
}
// Handle unlimited scenarios // Adjust based on unlimited profit/loss conditions
if (unlimitedProfit) { if (hasUnlimitedProfit && !hasUnlimitedLoss) {
// Unlimited profit, limited loss
metrics = { metrics = {
maxProfit: "Unlimited", maxProfit: "Unlimited",
maxLoss: `$${formatCurrency(maxLoss)}`, maxLoss: `$${formatCurrency(Math.abs(maxLoss))}`,
}; };
} else if (unlimitedLoss) { } else if (!hasUnlimitedProfit && hasUnlimitedLoss) {
// Limited profit, unlimited loss
// Need to calculate max profit at various price points
const strikes = allLegs.map((leg) => leg.strike);
const minStrike = Math.min(...strikes);
const maxStrike = Math.max(...strikes);
// Calculate potential profit at each strike price
let maxProfit = netPremium;
strikes.forEach((price) => {
let profitAtPrice = netPremium;
allLegs.forEach((leg) => {
const quantity = leg.quantity || 1;
if (leg.optionType === "Call") {
if (price > leg.strike) {
// Call is in-the-money
const intrinsicValue =
(price - leg.strike) * multiplier * quantity;
if (leg.action === "Buy") {
profitAtPrice += intrinsicValue;
} else {
profitAtPrice -= intrinsicValue;
}
}
} else if (leg.optionType === "Put") {
if (price < leg.strike) {
// Put is in-the-money
const intrinsicValue =
(leg.strike - price) * multiplier * quantity;
if (leg.action === "Buy") {
profitAtPrice += intrinsicValue;
} else {
profitAtPrice -= intrinsicValue;
}
}
}
});
maxProfit = Math.max(maxProfit, profitAtPrice);
});
metrics = { metrics = {
maxProfit: `$${formatCurrency(maxProfit)}`, maxProfit: `$${formatCurrency(maxProfit)}`,
maxLoss: "Unlimited", maxLoss: "Unlimited",
}; };
} else if (hasUnlimitedProfit && hasUnlimitedLoss) {
// Both unlimited profit and loss - unusual case
metrics = {
maxProfit: "Unlimited",
maxLoss: "Unlimited",
};
} else { } else {
// Finite profit and loss // Both limited profit and limited loss
// Need to calculate at various price points
const strikes = allLegs.map((leg) => leg.strike);
const minStrike = Math.min(...strikes);
const maxStrike = Math.max(...strikes);
// Calculate at various price points
const pricePoints = [0, minStrike / 2, ...strikes, maxStrike * 1.5];
let maxProfit = -Infinity;
maxLoss = -netPremium; // Start with premium paid
pricePoints.forEach((price) => {
let profitAtPrice = netPremium;
allLegs.forEach((leg) => {
const quantity = leg.quantity || 1;
if (leg.optionType === "Call") {
if (price > leg.strike) {
// Call is in-the-money
const intrinsicValue =
(price - leg.strike) * multiplier * quantity;
if (leg.action === "Buy") {
profitAtPrice += intrinsicValue;
} else {
profitAtPrice -= intrinsicValue;
}
}
} else if (leg.optionType === "Put") {
if (price < leg.strike) {
// Put is in-the-money
const intrinsicValue =
(leg.strike - price) * multiplier * quantity;
if (leg.action === "Buy") {
profitAtPrice += intrinsicValue;
} else {
profitAtPrice -= intrinsicValue;
}
}
}
});
maxProfit = Math.max(maxProfit, profitAtPrice);
if (profitAtPrice < 0) {
maxLoss = Math.min(maxLoss, profitAtPrice);
}
});
metrics = { metrics = {
maxProfit: `$${formatCurrency(maxProfit)}`, maxProfit: `$${formatCurrency(maxProfit)}`,
maxLoss: `$${formatCurrency(maxLoss)}`, maxLoss: `$${formatCurrency(Math.abs(maxLoss))}`,
}; };
} }
// Special case: if maxLoss is negative, it's actually a profit floor
if (maxLoss < 0) {
metrics.maxLoss = "$0"; // Can't lose money
metrics.maxProfit = `$${formatCurrency(Math.max(maxProfit, Math.abs(maxLoss)))}`;
}
return metrics; return metrics;
} }
@ -656,7 +749,7 @@
// Helper function for currency formatting // Helper function for currency formatting
function formatCurrency(value: number): string { function formatCurrency(value: number): string {
return value?.toLocaleString("en-US", { return Math.abs(value)?.toLocaleString("en-US", {
minimumFractionDigits: 2, minimumFractionDigits: 2,
maximumFractionDigits: 2, maximumFractionDigits: 2,
}); });