bugfixing

This commit is contained in:
MuslemRahimi 2025-04-08 21:05:49 +02:00
parent b1f2ce6c37
commit c2db43f5ed

View File

@ -160,7 +160,7 @@
default: default:
break; break;
} }
if (["Bull Call Spread"].includes(selectedStrategy)) { if ("Bull Call Spread" === selectedStrategy) {
// Find the lower strike first (for the Buy leg) // Find the lower strike first (for the Buy leg)
const lowerStrike = selectedStrike; const lowerStrike = selectedStrike;
@ -581,9 +581,9 @@
} }
// Calculate break-even and metrics for single-leg strategies // Calculate break-even and metrics for single-leg strategies
calculateMetrics(); metrics = calculateMetrics();
calculateBreakevenPrice(dataPoints); calculateBreakevenPrice(dataPoints);
// Build the chart options console.log(userStrategy);
const options = { const options = {
credits: { enabled: false }, credits: { enabled: false },
chart: { chart: {
@ -719,6 +719,7 @@
function calculateMetrics() { function calculateMetrics() {
const multiplier = 100; const multiplier = 100;
let metrics = {};
// Get all legs in the strategy // Get all legs in the strategy
const allLegs = [...userStrategy]; const allLegs = [...userStrategy];
@ -732,11 +733,11 @@
return metrics; return metrics;
} }
// First, consolidate identical strikes with opposite actions (buy/sell) // Consolidate identical strikes with opposite actions (Buy/Sell)
const consolidatedLegs = []; const consolidatedLegs = [];
const strikeMap = new Map(); const strikeMap = new Map();
// Group legs by strike price and option type // Group legs by strike and option type
allLegs.forEach((leg) => { allLegs.forEach((leg) => {
const key = `${leg.strike}-${leg.optionType}`; const key = `${leg.strike}-${leg.optionType}`;
if (!strikeMap.has(key)) { if (!strikeMap.has(key)) {
@ -745,24 +746,21 @@
strikeMap.get(key).push(leg); strikeMap.get(key).push(leg);
}); });
// Consolidate legs with the same strike and option type // Consolidate legs with same strike/option type into net positions
strikeMap.forEach((legs, key) => { strikeMap.forEach((legs, key) => {
let netQuantity = 0; let netQuantity = 0;
let netCost = 0; let netCost = 0;
legs.forEach((leg) => { legs.forEach((leg) => {
const quantity = leg.quantity || 1; const quantity = leg.quantity || 1;
if (leg.action === "Buy") { if (leg.action === "Buy") {
netQuantity += quantity; netQuantity += quantity;
netCost += leg.optionPrice * quantity; netCost += leg.optionPrice * quantity;
} else { } else {
// Sell
netQuantity -= quantity; netQuantity -= quantity;
netCost -= leg.optionPrice * quantity; netCost -= leg.optionPrice * quantity;
} }
}); });
// Only include legs with nonzero positions
// Only add non-zero net positions
if (netQuantity !== 0) { if (netQuantity !== 0) {
const [strike, optionType] = key.split("-"); const [strike, optionType] = key.split("-");
consolidatedLegs.push({ consolidatedLegs.push({
@ -775,7 +773,7 @@
} }
}); });
// Now work with consolidated legs // Separate the legs by type and action
const buyCalls = consolidatedLegs.filter( const buyCalls = consolidatedLegs.filter(
(leg) => leg.action === "Buy" && leg.optionType === "Call", (leg) => leg.action === "Buy" && leg.optionType === "Call",
); );
@ -789,31 +787,46 @@
(leg) => leg.action === "Sell" && leg.optionType === "Put", (leg) => leg.action === "Sell" && leg.optionType === "Put",
); );
// Calculate net premium for the entire strategy // Calculate net premium for the entire strategy.
// For each leg, buying incurs a debit and selling generates a credit.
let netPremium = 0; let netPremium = 0;
allLegs.forEach((leg) => { allLegs?.forEach((leg) => {
const quantity = leg.quantity || 1; const quantity = leg.quantity || 1;
const premium = leg.optionPrice * multiplier * quantity; const premium = leg.optionPrice * multiplier * quantity;
if (leg.action === "Buy") { if (leg.action === "Buy") {
netPremium -= premium; // Buying costs money (debit) netPremium -= premium;
} else { } else {
netPremium += premium; // Selling receives money (credit) netPremium += premium;
} }
}); });
// Check if any positions have unlimited profit or loss if (
buyCalls.length === 1 &&
sellCalls.length === 1 &&
sellCalls[0].strike < buyCalls[0].strike
) {
const spreadWidth =
(buyCalls[0].strike - sellCalls[0].strike) * multiplier;
// In a vertical spread, the max profit is the net premium (credit received)
// and the maximum loss equals the spread width minus the net premium.
const maxProfit = netPremium;
const maxLoss = spreadWidth - netPremium;
metrics = {
maxProfit: `$${formatCurrency(maxProfit)}`,
maxLoss: `$${formatCurrency(maxLoss)}`,
};
return metrics;
}
// --- END VERTICAL SPREAD HANDLING ---
// Determine unlimited profit/loss flags based on calls only.
let hasUnlimitedProfit = false; let hasUnlimitedProfit = false;
let hasUnlimitedLoss = false; let hasUnlimitedLoss = false;
// Check for unlimited profit (net long calls)
if (buyCalls.length > 0) { if (buyCalls.length > 0) {
// Sort by strike price ascending
const sortedBuyCalls = [...buyCalls].sort((a, b) => a.strike - b.strike); const sortedBuyCalls = [...buyCalls].sort((a, b) => a.strike - b.strike);
const sortedSellCalls = [...sellCalls].sort( const sortedSellCalls = [...sellCalls].sort(
(a, b) => a.strike - b.strike, (a, b) => a.strike - b.strike,
); );
// If highest long call strike is higher than all short calls, or there are no short calls
if ( if (
sellCalls.length === 0 || sellCalls.length === 0 ||
sortedBuyCalls[sortedBuyCalls.length - 1].strike > sortedBuyCalls[sortedBuyCalls.length - 1].strike >
@ -821,8 +834,6 @@
) { ) {
hasUnlimitedProfit = true; hasUnlimitedProfit = true;
} }
// Also check quantities - if buy quantity > sell quantity
const totalBuyCallQuantity = sortedBuyCalls.reduce( const totalBuyCallQuantity = sortedBuyCalls.reduce(
(sum, leg) => sum + (leg.quantity || 1), (sum, leg) => sum + (leg.quantity || 1),
0, 0,
@ -831,21 +842,15 @@
(sum, leg) => sum + (leg.quantity || 1), (sum, leg) => sum + (leg.quantity || 1),
0, 0,
); );
if (totalBuyCallQuantity > totalSellCallQuantity) { if (totalBuyCallQuantity > totalSellCallQuantity) {
hasUnlimitedProfit = true; hasUnlimitedProfit = true;
} }
} }
// Check for unlimited loss (net short calls)
if (sellCalls.length > 0) { if (sellCalls.length > 0) {
// Sort by strike price ascending
const sortedBuyCalls = [...buyCalls].sort((a, b) => a.strike - b.strike); const sortedBuyCalls = [...buyCalls].sort((a, b) => a.strike - b.strike);
const sortedSellCalls = [...sellCalls].sort( const sortedSellCalls = [...sellCalls].sort(
(a, b) => a.strike - b.strike, (a, b) => a.strike - b.strike,
); );
// If highest short call strike is higher than all long calls, or there are no long calls
if ( if (
buyCalls.length === 0 || buyCalls.length === 0 ||
sortedSellCalls[sortedSellCalls.length - 1].strike > sortedSellCalls[sortedSellCalls.length - 1].strike >
@ -853,8 +858,6 @@
) { ) {
hasUnlimitedLoss = true; hasUnlimitedLoss = true;
} }
// Also check quantities - if sell quantity > buy quantity
const totalBuyCallQuantity = sortedBuyCalls.reduce( const totalBuyCallQuantity = sortedBuyCalls.reduce(
(sum, leg) => sum + (leg.quantity || 1), (sum, leg) => sum + (leg.quantity || 1),
0, 0,
@ -863,166 +866,122 @@
(sum, leg) => sum + (leg.quantity || 1), (sum, leg) => sum + (leg.quantity || 1),
0, 0,
); );
if (totalSellCallQuantity > totalBuyCallQuantity) { if (totalSellCallQuantity > totalBuyCallQuantity) {
hasUnlimitedLoss = true; hasUnlimitedLoss = true;
} }
} }
// Calculate maximum loss // Check if exactly one put is bought and one sold (vertical spread)
let maxLoss = -netPremium; // Start with net premium paid if (buyPuts.length === 1 && sellPuts.length === 1) {
const buyStrike = buyPuts[0].strike;
const sellStrike = sellPuts[0].strike;
const spreadWidth = Math.abs(buyStrike - sellStrike) * multiplier;
// For your specific strategy (buy lower strike, sell and buy same higher strike), // Bull Put Spread (sell higher strike, buy lower strike)
// the max loss is the net premium paid if (sellStrike > buyStrike) {
const maxProfit = netPremium; // Net credit received
// Add logic for specific strategy patterns const maxLoss = spreadWidth - maxProfit;
if (buyCalls.length > 0 && sellCalls.length > 0) { metrics = {
// This is a complex strategy with both long and short calls maxProfit: `$${formatCurrency(maxProfit)}`,
// For this specific pattern, max loss is typically the net premium maxLoss: `$${formatCurrency(maxLoss)}`,
maxLoss = -netPremium; };
} return metrics;
}
// Check for special case: Call Ratio Spread with lower strike bought // Bear Put Spread (buy higher strike, sell lower strike)
if ( else if (buyStrike > sellStrike) {
buyCalls.length === 1 && const maxProfit = spreadWidth - Math.abs(netPremium);
sellCalls.length === 1 && const maxLoss = Math.abs(netPremium); // Net debit paid
buyCalls[0].strike < sellCalls[0].strike && metrics = {
sellCalls[0].quantity > buyCalls[0].quantity maxProfit: `$${formatCurrency(maxProfit)}`,
) { maxLoss: `$${formatCurrency(maxLoss)}`,
// Call ratio spread with more short calls than long calls };
const spreadWidth = return metrics;
(sellCalls[0].strike - buyCalls[0].strike) * multiplier;
const buyQuantity = buyCalls[0].quantity || 1;
const sellQuantity = sellCalls[0].quantity || 1;
// Max loss can be unlimited if ratio is > 1
if (sellQuantity > buyQuantity) {
hasUnlimitedLoss = true;
} }
// Max profit is at the short strike
const maxProfit = spreadWidth * buyQuantity + netPremium;
metrics = {
maxProfit: `$${formatCurrency(maxProfit)}`,
maxLoss: hasUnlimitedLoss
? "Unlimited"
: `$${formatCurrency(Math.abs(maxLoss))}`,
};
return metrics;
} }
// Adjust based on unlimited profit/loss conditions // --- RATIO SPREAD HANDLING ---
// Detect a pattern where two (or more) long calls bracket the short call(s) with balanced quantities.
if (buyCalls.length >= 2 && sellCalls.length >= 1) {
const buyStrikes = buyCalls.map((leg) => leg.strike);
const sellStrikes = sellCalls.map((leg) => leg.strike);
const lowerBuy = Math.min(...buyStrikes);
const higherBuy = Math.max(...buyStrikes);
const minSell = Math.min(...sellStrikes);
const maxSell = Math.max(...sellStrikes);
const totalBuyCallQuantity = buyCalls.reduce(
(sum, leg) => sum + leg.quantity,
0,
);
const totalSellCallQuantity = sellCalls.reduce(
(sum, leg) => sum + leg.quantity,
0,
);
if (
lowerBuy < minSell &&
higherBuy > maxSell &&
totalBuyCallQuantity === totalSellCallQuantity
) {
hasUnlimitedProfit = false;
hasUnlimitedLoss = false;
}
}
// --- END RATIO SPREAD HANDLING ---
// If we haven't returned earlier via a specific branch, then compute profit and loss
// by simulating across various price points.
const strikes = allLegs.map((leg) => leg.strike);
const minStrike = Math.min(...strikes);
const maxStrike = Math.max(...strikes);
const pricePoints = [0, minStrike / 2, ...strikes, maxStrike * 1.5];
let computedMaxProfit = -Infinity;
let computedMaxLoss = -netPremium; // starting point: net premium paid
pricePoints.forEach((price) => {
let profitAtPrice = netPremium;
allLegs.forEach((leg) => {
const quantity = leg.quantity || 1;
if (leg.optionType === "Call") {
if (price > leg.strike) {
const intrinsicValue = (price - leg.strike) * multiplier * quantity;
profitAtPrice +=
leg.action === "Buy" ? intrinsicValue : -intrinsicValue;
}
} else if (leg.optionType === "Put") {
if (price < leg.strike) {
const intrinsicValue = (leg.strike - price) * multiplier * quantity;
profitAtPrice +=
leg.action === "Buy" ? intrinsicValue : -intrinsicValue;
}
}
});
computedMaxProfit = Math.max(computedMaxProfit, profitAtPrice);
if (profitAtPrice < 0) {
computedMaxLoss = Math.min(computedMaxLoss, profitAtPrice);
}
});
// Adjust final metrics based on unlimited flags:
if (hasUnlimitedProfit && !hasUnlimitedLoss) { if (hasUnlimitedProfit && !hasUnlimitedLoss) {
// Unlimited profit, limited loss
metrics = { metrics = {
maxProfit: "Unlimited", maxProfit: "Unlimited",
maxLoss: `$${formatCurrency(Math.abs(maxLoss))}`, maxLoss: `$${formatCurrency(Math.abs(computedMaxLoss))}`,
}; };
} else if (!hasUnlimitedProfit && hasUnlimitedLoss) { } 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(computedMaxProfit)}`,
maxLoss: "Unlimited", maxLoss: "Unlimited",
}; };
} else if (hasUnlimitedProfit && hasUnlimitedLoss) { } else if (hasUnlimitedProfit && hasUnlimitedLoss) {
// Both unlimited profit and loss - unusual case
metrics = { metrics = {
maxProfit: "Unlimited", maxProfit: "Unlimited",
maxLoss: "Unlimited", maxLoss: "Unlimited",
}; };
} else { } else {
// 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(computedMaxProfit)}`,
maxLoss: `$${formatCurrency(Math.abs(maxLoss))}`, maxLoss: `$${formatCurrency(Math.abs(computedMaxLoss))}`,
}; };
} }