add worker
This commit is contained in:
parent
7a8ca4aa10
commit
87f78615d6
@ -20,6 +20,7 @@
|
||||
let isLoaded = false;
|
||||
let shouldUpdate = false;
|
||||
let config: any = null;
|
||||
let downloadWorker: Worker | undefined;
|
||||
|
||||
// Strategy selection
|
||||
let selectedStrategy = "Long Call";
|
||||
@ -131,6 +132,13 @@
|
||||
let userStrategy = [];
|
||||
let description = prebuiltStrategy[0]?.description;
|
||||
|
||||
const handleDownloadMessage = async (event) => {
|
||||
rawData = event?.data?.output;
|
||||
|
||||
currentStockPrice = rawData?.getStockQuote?.price || 0;
|
||||
await loadData();
|
||||
};
|
||||
|
||||
async function changeStrategy(strategy) {
|
||||
selectedStrategy = strategy?.name;
|
||||
description = strategy?.description;
|
||||
@ -1171,21 +1179,9 @@
|
||||
|
||||
async function getStockData() {
|
||||
try {
|
||||
const postData = { ticker: selectedTicker };
|
||||
const response = await fetch("/api/options-calculator", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(postData),
|
||||
downloadWorker.postMessage({
|
||||
ticker: selectedTicker,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch stock data: ${response.statusText}`);
|
||||
}
|
||||
|
||||
rawData = (await response.json()) || {};
|
||||
currentStockPrice = rawData?.getStockQuote?.price || 0;
|
||||
} catch (error) {
|
||||
console.error("Error fetching stock data:", error);
|
||||
}
|
||||
@ -1409,8 +1405,13 @@
|
||||
}
|
||||
}
|
||||
|
||||
if (!downloadWorker) {
|
||||
const DownloadWorker = await import("./workers/downloadWorker?worker");
|
||||
downloadWorker = new DownloadWorker.default();
|
||||
downloadWorker.onmessage = handleDownloadMessage;
|
||||
}
|
||||
|
||||
await getStockData();
|
||||
await loadData();
|
||||
|
||||
shouldUpdate = true;
|
||||
});
|
||||
|
||||
38
src/routes/options-calculator/workers/downloadWorker.ts
Normal file
38
src/routes/options-calculator/workers/downloadWorker.ts
Normal file
@ -0,0 +1,38 @@
|
||||
// Cache to store previous requests
|
||||
let cache = new Map();
|
||||
|
||||
const getStockData = async (ticker) => {
|
||||
|
||||
|
||||
// Check if data for this rule set is already in the cache
|
||||
if (cache.has(ticker)) {
|
||||
console.log("Returning cached data");
|
||||
return cache.get(ticker);
|
||||
}
|
||||
|
||||
const postData = { ticker: ticker };
|
||||
const response = await fetch("/api/options-calculator", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(postData),
|
||||
});
|
||||
|
||||
|
||||
const output = (await response.json()) || {};
|
||||
|
||||
cache.set(ticker, output);
|
||||
|
||||
return output;
|
||||
};
|
||||
|
||||
onmessage = async (event) => {
|
||||
const { ticker } = event.data;
|
||||
const output = await getStockData(ticker);
|
||||
|
||||
|
||||
postMessage({ message: "success", output });
|
||||
};
|
||||
|
||||
export {};
|
||||
311
src/routes/options-calculator/workers/plotWorker.ts
Normal file
311
src/routes/options-calculator/workers/plotWorker.ts
Normal file
@ -0,0 +1,311 @@
|
||||
// Helper function for currency formatting
|
||||
function formatCurrency(value: number): string {
|
||||
return Math.abs(value)?.toLocaleString("en-US", {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2,
|
||||
});
|
||||
}
|
||||
|
||||
function calculateMetrics(userStrategy) {
|
||||
const multiplier = 100;
|
||||
let metrics = {};
|
||||
|
||||
// Get all legs in the strategy
|
||||
const allLegs = [...userStrategy];
|
||||
|
||||
// Check if strategy is empty
|
||||
if (allLegs.length === 0) {
|
||||
metrics = {
|
||||
maxProfit: "$0",
|
||||
maxLoss: "$0",
|
||||
};
|
||||
return metrics;
|
||||
}
|
||||
|
||||
// Consolidate identical strikes with opposite actions (Buy/Sell)
|
||||
const consolidatedLegs = [];
|
||||
const strikeMap = new Map();
|
||||
|
||||
// Group legs by strike 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 same strike/option type into net positions
|
||||
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 {
|
||||
netQuantity -= quantity;
|
||||
netCost -= leg.optionPrice * quantity;
|
||||
}
|
||||
});
|
||||
// Only include legs with nonzero 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",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Separate the legs by type and action
|
||||
const buyCalls = consolidatedLegs.filter(
|
||||
(leg) => leg.action === "Buy" && leg.optionType === "Call",
|
||||
);
|
||||
const sellCalls = consolidatedLegs.filter(
|
||||
(leg) => leg.action === "Sell" && leg.optionType === "Call",
|
||||
);
|
||||
const buyPuts = consolidatedLegs.filter(
|
||||
(leg) => leg.action === "Buy" && leg.optionType === "Put",
|
||||
);
|
||||
const sellPuts = consolidatedLegs.filter(
|
||||
(leg) => leg.action === "Sell" && leg.optionType === "Put",
|
||||
);
|
||||
|
||||
// Calculate net premium for the entire strategy.
|
||||
let netPremium = 0;
|
||||
allLegs.forEach((leg) => {
|
||||
const quantity = leg.quantity || 1;
|
||||
const premium = leg.optionPrice * multiplier * quantity;
|
||||
if (leg.action === "Buy") {
|
||||
netPremium -= premium;
|
||||
} else {
|
||||
netPremium += premium;
|
||||
}
|
||||
});
|
||||
|
||||
// --- VERTICAL SPREAD HANDLING (UPDATED) ---
|
||||
if (buyCalls.length === 1 && sellCalls.length === 1) {
|
||||
const buyCall = buyCalls[0];
|
||||
const sellCall = sellCalls[0];
|
||||
const spreadWidth =
|
||||
Math.abs(buyCall.strike - sellCall.strike) * multiplier;
|
||||
|
||||
if (buyCall.strike < sellCall.strike) {
|
||||
// Bull call spread: max loss is net debit, max profit is spread width - net debit
|
||||
const maxLoss = -netPremium; // Net debit is negative, convert to positive
|
||||
const maxProfit = spreadWidth - maxLoss;
|
||||
metrics = {
|
||||
maxProfit: `$${formatCurrency(maxProfit)}`,
|
||||
maxLoss: `$${formatCurrency(maxLoss)}`,
|
||||
};
|
||||
} else {
|
||||
// Bear call spread: max profit is net credit, max loss is spread width - net credit
|
||||
const maxProfit = netPremium;
|
||||
const maxLoss = spreadWidth - maxProfit;
|
||||
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 hasUnlimitedLoss = false;
|
||||
if (buyCalls.length > 0) {
|
||||
const sortedBuyCalls = [...buyCalls].sort((a, b) => a.strike - b.strike);
|
||||
const sortedSellCalls = [...sellCalls].sort(
|
||||
(a, b) => a.strike - b.strike,
|
||||
);
|
||||
if (
|
||||
sellCalls.length === 0 ||
|
||||
sortedBuyCalls[sortedBuyCalls.length - 1].strike >
|
||||
sortedSellCalls[sortedSellCalls.length - 1].strike
|
||||
) {
|
||||
hasUnlimitedProfit = true;
|
||||
}
|
||||
const totalBuyCallQuantity = sortedBuyCalls.reduce(
|
||||
(sum, leg) => sum + (leg.quantity || 1),
|
||||
0,
|
||||
);
|
||||
const totalSellCallQuantity = sortedSellCalls.reduce(
|
||||
(sum, leg) => sum + (leg.quantity || 1),
|
||||
0,
|
||||
);
|
||||
if (totalBuyCallQuantity > totalSellCallQuantity) {
|
||||
hasUnlimitedProfit = true;
|
||||
}
|
||||
}
|
||||
if (sellCalls.length > 0) {
|
||||
const sortedBuyCalls = [...buyCalls].sort((a, b) => a.strike - b.strike);
|
||||
const sortedSellCalls = [...sellCalls].sort(
|
||||
(a, b) => a.strike - b.strike,
|
||||
);
|
||||
if (
|
||||
buyCalls.length === 0 ||
|
||||
sortedSellCalls[sortedSellCalls.length - 1].strike >
|
||||
sortedBuyCalls[sortedBuyCalls.length - 1].strike
|
||||
) {
|
||||
hasUnlimitedLoss = true;
|
||||
}
|
||||
const totalBuyCallQuantity = sortedBuyCalls.reduce(
|
||||
(sum, leg) => sum + (leg.quantity || 1),
|
||||
0,
|
||||
);
|
||||
const totalSellCallQuantity = sortedSellCalls.reduce(
|
||||
(sum, leg) => sum + (leg.quantity || 1),
|
||||
0,
|
||||
);
|
||||
if (totalSellCallQuantity > totalBuyCallQuantity) {
|
||||
hasUnlimitedLoss = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if exactly one put is bought and one sold (vertical spread)
|
||||
if (buyPuts.length === 1 && sellPuts.length === 1) {
|
||||
const buyStrike = buyPuts[0].strike;
|
||||
const sellStrike = sellPuts[0].strike;
|
||||
const spreadWidth = Math.abs(buyStrike - sellStrike) * multiplier;
|
||||
|
||||
// Bull Put Spread (sell higher strike, buy lower strike)
|
||||
if (sellStrike > buyStrike) {
|
||||
const maxProfit = netPremium; // Net credit received
|
||||
const maxLoss = spreadWidth - maxProfit;
|
||||
metrics = {
|
||||
maxProfit: `$${formatCurrency(maxProfit)}`,
|
||||
maxLoss: `$${formatCurrency(maxLoss)}`,
|
||||
};
|
||||
return metrics;
|
||||
}
|
||||
// Bear Put Spread (buy higher strike, sell lower strike)
|
||||
else if (buyStrike > sellStrike) {
|
||||
const maxProfit = spreadWidth - Math.abs(netPremium);
|
||||
const maxLoss = Math.abs(netPremium); // Net debit paid
|
||||
metrics = {
|
||||
maxProfit: `$${formatCurrency(maxProfit)}`,
|
||||
maxLoss: `$${formatCurrency(maxLoss)}`,
|
||||
};
|
||||
return metrics;
|
||||
}
|
||||
}
|
||||
|
||||
// --- 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) {
|
||||
metrics = {
|
||||
maxProfit: "Unlimited",
|
||||
maxLoss: `$${formatCurrency(Math.abs(computedMaxLoss))}`,
|
||||
};
|
||||
} else if (!hasUnlimitedProfit && hasUnlimitedLoss) {
|
||||
metrics = {
|
||||
maxProfit: `$${formatCurrency(computedMaxProfit)}`,
|
||||
maxLoss: "Unlimited",
|
||||
};
|
||||
} else if (hasUnlimitedProfit && hasUnlimitedLoss) {
|
||||
metrics = {
|
||||
maxProfit: "Unlimited",
|
||||
maxLoss: "Unlimited",
|
||||
};
|
||||
} else {
|
||||
metrics = {
|
||||
maxProfit: `$${formatCurrency(computedMaxProfit)}`,
|
||||
maxLoss: `$${formatCurrency(Math.abs(computedMaxLoss))}`,
|
||||
};
|
||||
}
|
||||
|
||||
return metrics;
|
||||
}
|
||||
|
||||
|
||||
function calculateBreakevenPrice(dataPoints) {
|
||||
let breakEvenPrice = null;
|
||||
// Loop over the dataPoints to find a sign change from loss to profit or vice versa
|
||||
for (let i = 1; i < dataPoints.length; i++) {
|
||||
const [prevPrice, prevProfitLoss] = dataPoints[i - 1];
|
||||
const [currPrice, currProfitLoss] = dataPoints[i];
|
||||
|
||||
// Check if there is a sign change between consecutive points
|
||||
if (
|
||||
(prevProfitLoss < 0 && currProfitLoss >= 0) ||
|
||||
(prevProfitLoss > 0 && currProfitLoss <= 0)
|
||||
) {
|
||||
// Linear interpolation to estimate the exact crossing point
|
||||
const priceDiff = currPrice - prevPrice;
|
||||
const profitDiff = currProfitLoss - prevProfitLoss;
|
||||
const ratio = Math.abs(prevProfitLoss) / Math.abs(profitDiff);
|
||||
breakEvenPrice = prevPrice + ratio * priceDiff;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return breakEvenPrice;
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user