add web worker

This commit is contained in:
MuslemRahimi 2025-04-09 13:16:28 +02:00
parent 87f78615d6
commit cf7bc59f4d
3 changed files with 537 additions and 757 deletions

View File

@ -21,6 +21,8 @@
let shouldUpdate = false; let shouldUpdate = false;
let config: any = null; let config: any = null;
let downloadWorker: Worker | undefined; let downloadWorker: Worker | undefined;
let plotWorker: Worker | undefined;
let strategyWorker: Worker | undefined;
// Strategy selection // Strategy selection
let selectedStrategy = "Long Call"; let selectedStrategy = "Long Call";
@ -139,460 +141,18 @@
await loadData(); await loadData();
}; };
async function changeStrategy(strategy) { const handlePlotMessage = async (event) => {
selectedStrategy = strategy?.name; const output = event?.data?.output;
description = strategy?.description; metrics = output?.metrics;
config = output?.options;
breakEvenPrice = output?.breakEvenPrice;
totalPremium = output?.totalPremium;
// Set appropriate option type and action based on strategy const xMax = output?.xMax;
switch (selectedStrategy) { const xMin = output?.xMin;
case "Long Call": const dataPoints = output?.dataPoints;
selectedOptionType = "Call";
selectedAction = "Buy";
break;
case "Short Call":
selectedOptionType = "Call";
selectedAction = "Sell";
break;
case "Long Put":
selectedOptionType = "Put";
selectedAction = "Buy";
break;
case "Short Put":
selectedOptionType = "Put";
selectedAction = "Sell";
break;
case "Cash Secured Put":
selectedOptionType = "Put";
selectedAction = "Sell";
break;
default:
break;
}
if ("Bull Call Spread" === selectedStrategy) {
// Find the lower strike first (for the Buy leg)
const lowerStrike = selectedStrike;
// Find a higher strike in the available strikeList for the Sell leg config = {
// First, calculate the target strike (40% higher)
const targetHigherStrike = lowerStrike * 1.4;
// Find the closest available strike price that is higher than the lower strike
let higherStrike;
if (strikeList && strikeList.length > 0) {
// Filter strikes that are higher than the lower strike
const higherStrikes = strikeList?.filter(
(strike) => strike > lowerStrike,
);
if (higherStrikes.length > 0) {
// Find the strike closest to our target from the available higher strikes
higherStrike = higherStrikes?.reduce((closest, strike) => {
return Math.abs(strike - targetHigherStrike) <
Math.abs(closest - targetHigherStrike)
? strike
: closest;
}, higherStrikes[0]);
} else {
// If no higher strikes available, use the highest available strike
higherStrike = Math.max(...strikeList);
}
} else {
// Fallback if strikeList is empty
higherStrike = lowerStrike * 1.4;
}
userStrategy = [
{
strike: lowerStrike,
optionType: "Call",
date: selectedDate,
optionPrice: 0, // This will be updated when loadData() is called
quantity: 1,
action: "Buy",
},
{
strike: higherStrike,
optionType: "Call",
optionPrice: 0, // This will be updated when loadData() is called
date: selectedDate,
quantity: 1,
action: "Sell",
},
];
} else if (["Bull Put Spread"].includes(selectedStrategy)) {
// Find the lower strike first (for the Buy leg)
const lowerStrike = selectedStrike;
// Find a higher strike in the available strikeList for the Sell leg
// First, calculate the target strike (40% higher)
const targetHigherStrike = lowerStrike * 1.4;
// Find the closest available strike price that is higher than the lower strike
let higherStrike;
if (strikeList && strikeList.length > 0) {
// Filter strikes that are higher than the lower strike
const higherStrikes = strikeList?.filter(
(strike) => strike > lowerStrike,
);
if (higherStrikes.length > 0) {
// Find the strike closest to our target from the available higher strikes
higherStrike = higherStrikes?.reduce((closest, strike) => {
return Math.abs(strike - targetHigherStrike) <
Math.abs(closest - targetHigherStrike)
? strike
: closest;
}, higherStrikes[0]);
} else {
// If no higher strikes available, use the highest available strike
higherStrike = Math.max(...strikeList);
}
} else {
// Fallback if strikeList is empty
higherStrike = lowerStrike * 1.4;
}
userStrategy = [
{
strike: higherStrike,
optionType: "Put",
date: selectedDate,
optionPrice: 0, // This will be updated when loadData() is called
quantity: 1,
action: "Sell",
},
{
strike: lowerStrike,
optionType: "Put",
optionPrice: 0, // This will be updated when loadData() is called
date: selectedDate,
quantity: 1,
action: "Buy",
},
];
} else if (["Bear Call Spread"].includes(selectedStrategy)) {
// Find the lower strike first (for the Buy leg)
const lowerStrike = selectedStrike;
// Find a higher strike in the available strikeList for the Sell leg
// First, calculate the target strike (40% higher)
const targetHigherStrike = lowerStrike * 1.4;
// Find the closest available strike price that is higher than the lower strike
let higherStrike;
if (strikeList && strikeList.length > 0) {
// Filter strikes that are higher than the lower strike
const higherStrikes = strikeList?.filter(
(strike) => strike > lowerStrike,
);
if (higherStrikes.length > 0) {
// Find the strike closest to our target from the available higher strikes
higherStrike = higherStrikes?.reduce((closest, strike) => {
return Math.abs(strike - targetHigherStrike) <
Math.abs(closest - targetHigherStrike)
? strike
: closest;
}, higherStrikes[0]);
} else {
// If no higher strikes available, use the highest available strike
higherStrike = Math.max(...strikeList);
}
} else {
// Fallback if strikeList is empty
higherStrike = lowerStrike * 1.4;
}
userStrategy = [
{
strike: lowerStrike,
optionType: "Call",
date: selectedDate,
optionPrice: 0, // This will be updated when loadData() is called
quantity: 1,
action: "Sell",
},
{
strike: higherStrike,
optionType: "Call",
optionPrice: 0, // This will be updated when loadData() is called
date: selectedDate,
quantity: 1,
action: "Buy",
},
];
} else if ("Bear Put Spread" === selectedStrategy) {
// Find the lower strike first (for the Buy leg)
const lowerStrike = selectedStrike;
// Find a higher strike in the available strikeList for the Sell leg
// First, calculate the target strike (40% higher)
const targetHigherStrike = lowerStrike * 1.4;
// Find the closest available strike price that is higher than the lower strike
let higherStrike;
if (strikeList && strikeList.length > 0) {
// Filter strikes that are higher than the lower strike
const higherStrikes = strikeList?.filter(
(strike) => strike > lowerStrike,
);
if (higherStrikes.length > 0) {
// Find the strike closest to our target from the available higher strikes
higherStrike = higherStrikes?.reduce((closest, strike) => {
return Math.abs(strike - targetHigherStrike) <
Math.abs(closest - targetHigherStrike)
? strike
: closest;
}, higherStrikes[0]);
} else {
// If no higher strikes available, use the highest available strike
higherStrike = Math.max(...strikeList);
}
} else {
// Fallback if strikeList is empty
higherStrike = lowerStrike * 1.4;
}
userStrategy = [
{
strike: higherStrike,
optionType: "Put",
date: selectedDate,
optionPrice: 0, // This will be updated when loadData() is called
quantity: 1,
action: "Buy",
},
{
strike: lowerStrike,
optionType: "Put",
optionPrice: 0, // This will be updated when loadData() is called
date: selectedDate,
quantity: 1,
action: "Sell",
},
];
} else if ("Long Straddle" === selectedStrategy) {
userStrategy = [
{
strike: selectedStrike,
optionType: "Call",
date: selectedDate,
optionPrice: 0,
quantity: 1,
action: "Buy",
},
{
strike: selectedStrike,
optionType: "Put",
optionPrice: 0,
date: selectedDate,
quantity: 1,
action: "Buy",
},
];
} else if ("Short Straddle" === selectedStrategy) {
userStrategy = [
{
strike: selectedStrike,
optionType: "Call",
date: selectedDate,
optionPrice: 0,
quantity: 1,
action: "Sell",
},
{
strike: selectedStrike,
optionType: "Put",
optionPrice: 0,
date: selectedDate,
quantity: 1,
action: "Sell",
},
];
} else if ("Long Call Butterfly" === selectedStrategy) {
const lowerStrike = selectedStrike;
// Define target values relative to the lower strike
const targetMidStrike = lowerStrike * 1.1;
const targetHigherStrike = lowerStrike * 1.2;
// Initialize the strike values that will be used in the strategy
let higherStrike;
let midStrike;
if (strikeList && strikeList.length > 0) {
// Filter strikes that are higher than the lower strike
const higherStrikes = strikeList.filter(
(strike) => strike > lowerStrike,
);
// Determine the higher strike leg:
if (higherStrikes.length > 0) {
// Choose the strike closest to our targetHigherStrike
higherStrike = higherStrikes.reduce((closest, strike) => {
return Math.abs(strike - targetHigherStrike) <
Math.abs(closest - targetHigherStrike)
? strike
: closest;
}, higherStrikes[0]);
} else {
// If no higher strikes are available, fallback to using the highest strike from the list
higherStrike = Math.max(...strikeList);
}
// For the mid strike, filter strikes that lie between the lowerStrike and the higherStrike
const midStrikes = strikeList.filter(
(strike) => strike > lowerStrike && strike < higherStrike,
);
// Determine the mid strike leg:
if (midStrikes.length > 0) {
// Choose the strike closest to our targetMidStrike
midStrike = midStrikes.reduce((closest, strike) => {
return Math.abs(strike - targetMidStrike) <
Math.abs(closest - targetMidStrike)
? strike
: closest;
}, midStrikes[0]);
} else {
// Fallback if no strike exists in between: you could use the target or any other logic.
midStrike = lowerStrike * 1.1;
}
} else {
// Fallback if strikeList is empty
higherStrike = lowerStrike * 1.2;
midStrike = lowerStrike * 1.1;
}
// Build the trading strategy for a Long Call Butterfly
userStrategy = [
{
strike: higherStrike,
optionType: "Call",
date: selectedDate,
optionPrice: 0,
quantity: 1,
action: "Buy",
},
{
strike: midStrike,
optionType: "Call",
date: selectedDate,
optionPrice: 0,
quantity: 2,
action: "Sell",
},
{
strike: lowerStrike,
optionType: "Call",
date: selectedDate,
optionPrice: 0,
quantity: 1,
action: "Buy",
},
];
} else {
userStrategy = [
{
strike: selectedStrike,
optionType: selectedOptionType,
date: selectedDate,
optionPrice: selectedOptionPrice,
quantity: selectedQuantity,
action: selectedAction,
},
];
}
userStrategy = [...userStrategy];
await loadData();
shouldUpdate = true;
}
const payoffFunctions = {
"Buy Call": (
s: number,
strike: number,
premium: number,
quantity: number,
) => (s < strike ? -premium : (s - strike) * 100 * quantity - premium),
"Sell Call": (
s: number,
strike: number,
premium: number,
quantity: number,
) => (s < strike ? premium : premium - (s - strike) * 100 * quantity),
"Buy Put": (
s: number,
strike: number,
premium: number,
quantity: number,
) => (s > strike ? -premium : (strike - s) * 100 * quantity - premium),
"Sell Put": (
s: number,
strike: number,
premium: number,
quantity: number,
) => (s > strike ? premium : premium - (strike - s) * 100 * quantity),
};
const formatDate = (dateString: string) => {
const date = new Date(dateString);
return date.toLocaleDateString("en-US", {
month: "short",
day: "2-digit",
year: "numeric",
timeZone: "America/New_York",
});
};
function plotData() {
// Determine x-axis range based on current stock price and max leg strike
if (!userStrategy || userStrategy.length === 0) {
return null;
}
const maxLegStrike = Math.max(...userStrategy.map((leg) => leg.strike));
const xMin = 0;
const xMax = Math.floor(Math.max(currentStockPrice, maxLegStrike) * 3);
const step = 10;
// Calculate the total premium across all legs
totalPremium = userStrategy.reduce((sum, leg) => {
return sum + leg.optionPrice * 100 * leg.quantity;
}, 0);
// Compute the aggregated payoff at each underlying price
const dataPoints = [];
for (let s = xMin; s <= xMax; s += step) {
let aggregatedPayoff = 0;
userStrategy.forEach((leg) => {
const legPremium = leg.optionPrice * 100 * leg.quantity;
const scenarioKey = `${leg.action} ${leg.optionType}`;
if (payoffFunctions[scenarioKey]) {
aggregatedPayoff += payoffFunctions[scenarioKey](
s,
leg.strike,
legPremium,
leg.quantity,
);
} else {
console.error(
"Payoff function not implemented for scenario:",
scenarioKey,
);
}
});
dataPoints.push([s, aggregatedPayoff]);
}
metrics = calculateMetrics();
calculateBreakevenPrice(dataPoints);
//console.log(userStrategy);
const options = {
credits: { enabled: false }, credits: { enabled: false },
chart: { chart: {
type: "area", type: "area",
@ -637,7 +197,7 @@
label: { label: {
text: `<span class="hidden sm:block text-black dark:text-white text-sm">Breakeven $${ text: `<span class="hidden sm:block text-black dark:text-white text-sm">Breakeven $${
typeof breakEvenPrice === "number" typeof breakEvenPrice === "number"
? breakEvenPrice.toFixed(2) ? breakEvenPrice?.toFixed(2)
: "" : ""
}</span>`, }</span>`,
style: { color: $mode === "light" ? "black" : "white" }, style: { color: $mode === "light" ? "black" : "white" },
@ -645,7 +205,7 @@
zIndex: 5, zIndex: 5,
} }
: null, : null,
].filter((line) => line !== null), ]?.filter((line) => line !== null),
}, },
yAxis: { yAxis: {
title: { title: {
@ -721,316 +281,56 @@
}, },
], ],
}; };
return options;
}
function calculateMetrics() {
const multiplier = 100;
// 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 handleStrategyMessage = async (event) => {
const consolidatedLegs = []; userStrategy = event?.data?.output;
const strikeMap = new Map();
// Group legs by strike and option type userStrategy = [...userStrategy];
allLegs.forEach((leg) => { await loadData();
const key = `${leg.strike}-${leg.optionType}`; shouldUpdate = true;
if (!strikeMap.has(key)) { };
strikeMap.set(key, []);
}
strikeMap.get(key).push(leg);
});
// Consolidate legs with same strike/option type into net positions async function changeStrategy(strategy) {
strikeMap.forEach((legs, key) => { selectedStrategy = strategy?.name;
let netQuantity = 0; description = strategy?.description;
let netCost = 0;
legs.forEach((leg) => { strategyWorker.postMessage({
const quantity = leg.quantity || 1; userStrategy,
if (leg.action === "Buy") { strikeList,
netQuantity += quantity; selectedStrategy,
netCost += leg.optionPrice * quantity; selectedAction,
} else { selectedDate,
netQuantity -= quantity; selectedOptionPrice,
netCost -= leg.optionPrice * quantity; selectedOptionType,
} selectedQuantity,
}); selectedStrike,
// 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",
}); });
} }
const formatDate = (dateString: string) => {
const date = new Date(dateString);
return date.toLocaleDateString("en-US", {
month: "short",
day: "2-digit",
year: "numeric",
timeZone: "America/New_York",
}); });
};
// Separate the legs by type and action function plotData() {
const buyCalls = consolidatedLegs.filter( // Determine x-axis range based on current stock price and max leg strike
(leg) => leg.action === "Buy" && leg.optionType === "Call", if (!userStrategy || userStrategy.length === 0) {
); return null;
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;
} }
try {
plotWorker.postMessage({
userStrategy: userStrategy,
currentStockPrice: currentStockPrice,
}); });
} catch (error) {
// --- VERTICAL SPREAD HANDLING (UPDATED) --- console.error("Error fetching stock data:", error);
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) {
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;
}
}
}
// Helper function for currency formatting
function formatCurrency(value: number): string {
return Math.abs(value)?.toLocaleString("en-US", {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
});
} }
const getContractHistory = async (contractId: string) => { const getContractHistory = async (contractId: string) => {
@ -1411,6 +711,18 @@
downloadWorker.onmessage = handleDownloadMessage; downloadWorker.onmessage = handleDownloadMessage;
} }
if (!plotWorker) {
const PlotWorker = await import("./workers/plotWorker?worker");
plotWorker = new PlotWorker.default();
plotWorker.onmessage = handlePlotMessage;
}
if (!strategyWorker) {
const StrategyWorker = await import("./workers/strategyWorker?worker");
strategyWorker = new StrategyWorker.default();
strategyWorker.onmessage = handleStrategyMessage;
}
await getStockData(); await getStockData();
shouldUpdate = true; shouldUpdate = true;

View File

@ -1,3 +1,5 @@
// Helper function for currency formatting // Helper function for currency formatting
function formatCurrency(value: number): string { function formatCurrency(value: number): string {
return Math.abs(value)?.toLocaleString("en-US", { return Math.abs(value)?.toLocaleString("en-US", {
@ -309,3 +311,92 @@ for (let i = 1; i < dataPoints.length; i++) {
return breakEvenPrice; return breakEvenPrice;
} }
const payoffFunctions = {
"Buy Call": (
s: number,
strike: number,
premium: number,
quantity: number,
) => (s < strike ? -premium : (s - strike) * 100 * quantity - premium),
"Sell Call": (
s: number,
strike: number,
premium: number,
quantity: number,
) => (s < strike ? premium : premium - (s - strike) * 100 * quantity),
"Buy Put": (
s: number,
strike: number,
premium: number,
quantity: number,
) => (s > strike ? -premium : (strike - s) * 100 * quantity - premium),
"Sell Put": (
s: number,
strike: number,
premium: number,
quantity: number,
) => (s > strike ? premium : premium - (strike - s) * 100 * quantity),
};
function plotData(userStrategy, currentStockPrice) {
// Determine x-axis range based on current stock price and max leg strike
if (!userStrategy || userStrategy.length === 0) {
return null;
}
const maxLegStrike = Math.max(...userStrategy.map((leg) => leg.strike));
const xMin = 0;
const xMax = Math.floor(Math.max(currentStockPrice, maxLegStrike) * 3);
const step = 10;
// Calculate the total premium across all legs
let totalPremium = userStrategy.reduce((sum, leg) => {
return sum + leg.optionPrice * 100 * leg.quantity;
}, 0);
// Compute the aggregated payoff at each underlying price
const dataPoints = [];
for (let s = xMin; s <= xMax; s += step) {
let aggregatedPayoff = 0;
userStrategy.forEach((leg) => {
const legPremium = leg.optionPrice * 100 * leg.quantity;
const scenarioKey = `${leg.action} ${leg.optionType}`;
if (payoffFunctions[scenarioKey]) {
aggregatedPayoff += payoffFunctions[scenarioKey](
s,
leg.strike,
legPremium,
leg.quantity,
);
} else {
console.error(
"Payoff function not implemented for scenario:",
scenarioKey,
);
}
});
dataPoints.push([s, aggregatedPayoff]);
}
const metrics = calculateMetrics(userStrategy);
let breakEvenPrice = calculateBreakevenPrice(dataPoints);
return {metrics, breakEvenPrice, totalPremium, dataPoints, xMin, xMax};
}
onmessage = async (event) => {
const { userStrategy, currentStockPrice } = event.data;
const output = plotData(userStrategy, currentStockPrice);
postMessage({ message: "success", output });
};
export {};

View File

@ -0,0 +1,377 @@
const getStrategy = (userStrategy, strikeList, selectedStrategy, selectedAction, selectedDate, selectedOptionPrice, selectedOptionType, selectedQuantity, selectedStrike) => {
// Set appropriate option type and action based on strategy
switch (selectedStrategy) {
case "Long Call":
selectedOptionType = "Call";
selectedAction = "Buy";
break;
case "Short Call":
selectedOptionType = "Call";
selectedAction = "Sell";
break;
case "Long Put":
selectedOptionType = "Put";
selectedAction = "Buy";
break;
case "Short Put":
selectedOptionType = "Put";
selectedAction = "Sell";
break;
case "Cash Secured Put":
selectedOptionType = "Put";
selectedAction = "Sell";
break;
default:
break;
}
if ("Bull Call Spread" === selectedStrategy) {
// Find the lower strike first (for the Buy leg)
const lowerStrike = selectedStrike;
// Find a higher strike in the available strikeList for the Sell leg
// First, calculate the target strike (40% higher)
const targetHigherStrike = lowerStrike * 1.4;
// Find the closest available strike price that is higher than the lower strike
let higherStrike;
if (strikeList && strikeList.length > 0) {
// Filter strikes that are higher than the lower strike
const higherStrikes = strikeList?.filter(
(strike) => strike > lowerStrike,
);
if (higherStrikes.length > 0) {
// Find the strike closest to our target from the available higher strikes
higherStrike = higherStrikes?.reduce((closest, strike) => {
return Math.abs(strike - targetHigherStrike) <
Math.abs(closest - targetHigherStrike)
? strike
: closest;
}, higherStrikes[0]);
} else {
// If no higher strikes available, use the highest available strike
higherStrike = Math.max(...strikeList);
}
} else {
// Fallback if strikeList is empty
higherStrike = lowerStrike * 1.4;
}
userStrategy = [
{
strike: lowerStrike,
optionType: "Call",
date: selectedDate,
optionPrice: 0, // This will be updated when loadData() is called
quantity: 1,
action: "Buy",
},
{
strike: higherStrike,
optionType: "Call",
optionPrice: 0, // This will be updated when loadData() is called
date: selectedDate,
quantity: 1,
action: "Sell",
},
];
} else if (["Bull Put Spread"].includes(selectedStrategy)) {
// Find the lower strike first (for the Buy leg)
const lowerStrike = selectedStrike;
// Find a higher strike in the available strikeList for the Sell leg
// First, calculate the target strike (40% higher)
const targetHigherStrike = lowerStrike * 1.4;
// Find the closest available strike price that is higher than the lower strike
let higherStrike;
if (strikeList && strikeList.length > 0) {
// Filter strikes that are higher than the lower strike
const higherStrikes = strikeList?.filter(
(strike) => strike > lowerStrike,
);
if (higherStrikes.length > 0) {
// Find the strike closest to our target from the available higher strikes
higherStrike = higherStrikes?.reduce((closest, strike) => {
return Math.abs(strike - targetHigherStrike) <
Math.abs(closest - targetHigherStrike)
? strike
: closest;
}, higherStrikes[0]);
} else {
// If no higher strikes available, use the highest available strike
higherStrike = Math.max(...strikeList);
}
} else {
// Fallback if strikeList is empty
higherStrike = lowerStrike * 1.4;
}
userStrategy = [
{
strike: higherStrike,
optionType: "Put",
date: selectedDate,
optionPrice: 0, // This will be updated when loadData() is called
quantity: 1,
action: "Sell",
},
{
strike: lowerStrike,
optionType: "Put",
optionPrice: 0, // This will be updated when loadData() is called
date: selectedDate,
quantity: 1,
action: "Buy",
},
];
} else if (["Bear Call Spread"].includes(selectedStrategy)) {
// Find the lower strike first (for the Buy leg)
const lowerStrike = selectedStrike;
// Find a higher strike in the available strikeList for the Sell leg
// First, calculate the target strike (40% higher)
const targetHigherStrike = lowerStrike * 1.4;
// Find the closest available strike price that is higher than the lower strike
let higherStrike;
if (strikeList && strikeList.length > 0) {
// Filter strikes that are higher than the lower strike
const higherStrikes = strikeList?.filter(
(strike) => strike > lowerStrike,
);
if (higherStrikes.length > 0) {
// Find the strike closest to our target from the available higher strikes
higherStrike = higherStrikes?.reduce((closest, strike) => {
return Math.abs(strike - targetHigherStrike) <
Math.abs(closest - targetHigherStrike)
? strike
: closest;
}, higherStrikes[0]);
} else {
// If no higher strikes available, use the highest available strike
higherStrike = Math.max(...strikeList);
}
} else {
// Fallback if strikeList is empty
higherStrike = lowerStrike * 1.4;
}
userStrategy = [
{
strike: lowerStrike,
optionType: "Call",
date: selectedDate,
optionPrice: 0, // This will be updated when loadData() is called
quantity: 1,
action: "Sell",
},
{
strike: higherStrike,
optionType: "Call",
optionPrice: 0, // This will be updated when loadData() is called
date: selectedDate,
quantity: 1,
action: "Buy",
},
];
} else if ("Bear Put Spread" === selectedStrategy) {
// Find the lower strike first (for the Buy leg)
const lowerStrike = selectedStrike;
// Find a higher strike in the available strikeList for the Sell leg
// First, calculate the target strike (40% higher)
const targetHigherStrike = lowerStrike * 1.4;
// Find the closest available strike price that is higher than the lower strike
let higherStrike;
if (strikeList && strikeList.length > 0) {
// Filter strikes that are higher than the lower strike
const higherStrikes = strikeList?.filter(
(strike) => strike > lowerStrike,
);
if (higherStrikes.length > 0) {
// Find the strike closest to our target from the available higher strikes
higherStrike = higherStrikes?.reduce((closest, strike) => {
return Math.abs(strike - targetHigherStrike) <
Math.abs(closest - targetHigherStrike)
? strike
: closest;
}, higherStrikes[0]);
} else {
// If no higher strikes available, use the highest available strike
higherStrike = Math.max(...strikeList);
}
} else {
// Fallback if strikeList is empty
higherStrike = lowerStrike * 1.4;
}
userStrategy = [
{
strike: higherStrike,
optionType: "Put",
date: selectedDate,
optionPrice: 0, // This will be updated when loadData() is called
quantity: 1,
action: "Buy",
},
{
strike: lowerStrike,
optionType: "Put",
optionPrice: 0, // This will be updated when loadData() is called
date: selectedDate,
quantity: 1,
action: "Sell",
},
];
} else if ("Long Straddle" === selectedStrategy) {
userStrategy = [
{
strike: selectedStrike,
optionType: "Call",
date: selectedDate,
optionPrice: 0,
quantity: 1,
action: "Buy",
},
{
strike: selectedStrike,
optionType: "Put",
optionPrice: 0,
date: selectedDate,
quantity: 1,
action: "Buy",
},
];
} else if ("Short Straddle" === selectedStrategy) {
userStrategy = [
{
strike: selectedStrike,
optionType: "Call",
date: selectedDate,
optionPrice: 0,
quantity: 1,
action: "Sell",
},
{
strike: selectedStrike,
optionType: "Put",
optionPrice: 0,
date: selectedDate,
quantity: 1,
action: "Sell",
},
];
} else if ("Long Call Butterfly" === selectedStrategy) {
const lowerStrike = selectedStrike;
// Define target values relative to the lower strike
const targetMidStrike = lowerStrike * 1.1;
const targetHigherStrike = lowerStrike * 1.2;
// Initialize the strike values that will be used in the strategy
let higherStrike;
let midStrike;
if (strikeList && strikeList.length > 0) {
// Filter strikes that are higher than the lower strike
const higherStrikes = strikeList.filter(
(strike) => strike > lowerStrike,
);
// Determine the higher strike leg:
if (higherStrikes.length > 0) {
// Choose the strike closest to our targetHigherStrike
higherStrike = higherStrikes.reduce((closest, strike) => {
return Math.abs(strike - targetHigherStrike) <
Math.abs(closest - targetHigherStrike)
? strike
: closest;
}, higherStrikes[0]);
} else {
// If no higher strikes are available, fallback to using the highest strike from the list
higherStrike = Math.max(...strikeList);
}
// For the mid strike, filter strikes that lie between the lowerStrike and the higherStrike
const midStrikes = strikeList.filter(
(strike) => strike > lowerStrike && strike < higherStrike,
);
// Determine the mid strike leg:
if (midStrikes.length > 0) {
// Choose the strike closest to our targetMidStrike
midStrike = midStrikes.reduce((closest, strike) => {
return Math.abs(strike - targetMidStrike) <
Math.abs(closest - targetMidStrike)
? strike
: closest;
}, midStrikes[0]);
} else {
// Fallback if no strike exists in between: you could use the target or any other logic.
midStrike = lowerStrike * 1.1;
}
} else {
// Fallback if strikeList is empty
higherStrike = lowerStrike * 1.2;
midStrike = lowerStrike * 1.1;
}
// Build the trading strategy for a Long Call Butterfly
userStrategy = [
{
strike: higherStrike,
optionType: "Call",
date: selectedDate,
optionPrice: 0,
quantity: 1,
action: "Buy",
},
{
strike: midStrike,
optionType: "Call",
date: selectedDate,
optionPrice: 0,
quantity: 2,
action: "Sell",
},
{
strike: lowerStrike,
optionType: "Call",
date: selectedDate,
optionPrice: 0,
quantity: 1,
action: "Buy",
},
];
} else {
userStrategy = [
{
strike: selectedStrike,
optionType: selectedOptionType,
date: selectedDate,
optionPrice: selectedOptionPrice,
quantity: selectedQuantity,
action: selectedAction,
},
];
}
return userStrategy;
};
onmessage = async (event) => {
const { userStrategy, strikeList, selectedStrategy, selectedAction, selectedDate, selectedOptionPrice, selectedOptionType, selectedQuantity, selectedStrike } = event.data;
const output = getStrategy(userStrategy, strikeList, selectedStrategy, selectedAction, selectedDate, selectedOptionPrice, selectedOptionType, selectedQuantity, selectedStrike);
postMessage({ message: "success", output });
};
export {};