bugfixing options calculator

This commit is contained in:
MuslemRahimi 2025-04-08 02:02:39 +02:00
parent bfed732d72
commit f027ebe0da

View File

@ -64,7 +64,7 @@
let optionSymbol: string; let optionSymbol: string;
let breakEvenPrice: number | null = null; let breakEvenPrice: number | null = null;
let totalPremium: number; let totalPremium: number;
let limits: Record<string, string> = {}; let metrics: Record<string, string> = {};
let rawData: Record<string, any> = {}; let rawData: Record<string, any> = {};
// Search variables // Search variables
@ -102,7 +102,7 @@
// Other strategies commented out in original code // Other strategies commented out in original code
]; ];
let userStrategy: OptionLeg[] = []; let userStrategy = [];
let description = prebuiltStrategy[0]?.description; let description = prebuiltStrategy[0]?.description;
// STRATEGY FUNCTIONS // STRATEGY FUNCTIONS
@ -134,33 +134,49 @@
selectedOptionType = null; selectedOptionType = null;
selectedAction = null; selectedAction = null;
} }
userStrategy = [
await loadData("default"); {
strike: selectedStrike,
optionType: selectedOptionType,
date: selectedDate,
optionPrice: selectedOptionPrice,
quantity: selectedQuantity,
action: selectedAction,
},
];
shouldUpdate = true; shouldUpdate = true;
} }
// PAYOFF CALCULATION FUNCTIONS // PAYOFF CALCULATION FUNCTIONS
const payoffFunctions = { const payoffFunctions = {
"Buy Call": (s: number, strike: number, premium: number) => "Buy Call": (
s < strike ? -premium : (s - strike) * 100 * selectedQuantity - premium, s: number,
strike: number,
premium: number,
quantity: number,
) => (s < strike ? -premium : (s - strike) * 100 * quantity - premium),
"Sell Call": (s: number, strike: number, premium: number) => "Sell Call": (
s < strike ? premium : premium - (s - strike) * 100 * selectedQuantity, s: number,
strike: number,
premium: number,
quantity: number,
) => (s < strike ? premium : premium - (s - strike) * 100 * quantity),
"Buy Put": (s: number, strike: number, premium: number) => "Buy Put": (
s > strike ? -premium : (strike - s) * 100 * selectedQuantity - premium, s: number,
strike: number,
premium: number,
quantity: number,
) => (s > strike ? -premium : (strike - s) * 100 * quantity - premium),
"Sell Put": (s: number, strike: number, premium: number) => "Sell Put": (
s > strike ? premium : premium - (strike - s) * 100 * selectedQuantity, s: number,
}; strike: number,
premium: number,
// Define break-even calculators for each scenario (using per-share price) quantity: number,
const breakEvenCalculators = { ) => (s > strike ? premium : premium - (strike - s) * 100 * quantity),
"Buy Call": (strike: number, optionPrice: number) => strike + optionPrice,
"Sell Call": (strike: number, optionPrice: number) => strike + optionPrice,
"Buy Put": (strike: number, optionPrice: number) => strike - optionPrice,
"Sell Put": (strike: number, optionPrice: number) => strike - optionPrice,
}; };
const formatDate = (dateString: string) => { const formatDate = (dateString: string) => {
@ -200,6 +216,7 @@
s, s,
leg.strike, leg.strike,
legPremium, legPremium,
leg.quantity,
); );
} else { } else {
console.error( console.error(
@ -211,9 +228,9 @@
dataPoints.push([s, aggregatedPayoff]); dataPoints.push([s, aggregatedPayoff]);
} }
// Calculate break-even and limits for single-leg strategies // Calculate break-even and metrics for single-leg strategies
calculateBreakEvenAndLimits(); calculateMetrics();
calculateBreakevenPrice(dataPoints);
// Build the chart options // Build the chart options
const options = { const options = {
credits: { enabled: false }, credits: { enabled: false },
@ -258,9 +275,11 @@
dashStyle: "Dash", dashStyle: "Dash",
width: $screenWidth < 640 ? 0 : 1.5, width: $screenWidth < 640 ? 0 : 1.5,
label: { label: {
text: `<span class="hidden sm:block text-black dark:text-white text-sm">Breakeven $${breakEvenPrice.toFixed( text: `<span class="hidden sm:block text-black dark:text-white text-sm">Breakeven $${
2, typeof breakEvenPrice === "number"
)}</span>`, ? breakEvenPrice.toFixed(2)
: ""
}</span>`,
style: { color: $mode === "light" ? "black" : "white" }, style: { color: $mode === "light" ? "black" : "white" },
}, },
zIndex: 5, zIndex: 5,
@ -346,49 +365,55 @@
return options; return options;
} }
function calculateBreakEvenAndLimits() { function calculateMetrics() {
const multiplier = 100;
let totalPremium = 0; let totalPremium = 0;
let overallMaxProfit = 0; let overallMaxProfit = 0;
let overallMaxLoss = 0; let overallMaxLoss = 0;
let unlimitedProfit = false; let unlimitedProfit = false;
let unlimitedLoss = false; let unlimitedLoss = false;
// Loop through each leg in the strategy // Loop through each leg in the strategy.
for (let i = 0; i < userStrategy.length; i++) { for (let i = 0; i < userStrategy.length; i++) {
const leg = userStrategy[i]; const leg = userStrategy[i];
totalPremium += leg.optionPrice; // accumulate total premium const quantity = leg?.quantity || 1; // Default to 1 if quantity is missing.
const scenarioKey = `${leg?.action} ${leg?.optionType}`;
// Multiply the premium by the contract multiplier and quantity.
totalPremium += leg.optionPrice * multiplier * quantity;
const scenarioKey = `${leg?.action} ${leg?.optionType}`;
let legProfit = 0; let legProfit = 0;
let legLoss = 0; let legLoss = 0;
// Determine limits for each leg based on its scenario // Determine metrics for each leg based on its scenario.
if (scenarioKey === "Buy Call") { if (scenarioKey === "Buy Call") {
// Profit is unlimited for a long call, loss limited to the premium paid. // Long call: unlimited profit, limited loss (the premium paid).
legProfit = Infinity; legProfit = Infinity;
legLoss = -leg.optionPrice; legLoss = leg.optionPrice * multiplier * quantity;
unlimitedProfit = true; unlimitedProfit = true;
} else if (scenarioKey === "Sell Call") { } else if (scenarioKey === "Sell Call") {
// Profit is limited to the premium received; loss is unlimited. // Short call: limited profit (premium received), unlimited loss.
legProfit = leg.optionPrice; legProfit = leg.optionPrice * multiplier * quantity;
legLoss = -Infinity; legLoss = -Infinity;
unlimitedLoss = true; unlimitedLoss = true;
} else if (scenarioKey === "Buy Put") { } else if (scenarioKey === "Buy Put") {
// For a long put, profit is (strike * 100) minus the premium, loss is the premium paid. // Long put: profit is (strike * multiplier minus premium) and loss is the premium paid.
legProfit = leg.strike * 100 - leg.optionPrice; legProfit =
legLoss = -leg.optionPrice; (leg.strike * multiplier - leg.optionPrice * multiplier) * quantity;
legLoss = leg.optionPrice * multiplier * quantity;
} else if (scenarioKey === "Sell Put") { } else if (scenarioKey === "Sell Put") {
// For a short put, profit is the premium received, loss is (strike * 100 - premium). // Short put: profit is the premium received;
legProfit = leg.optionPrice; // Maximum loss is the difference between strike * multiplier and the premium, scaled by quantity.
legLoss = -(leg.strike * 100 - leg.optionPrice); legProfit = leg.optionPrice * multiplier * quantity;
legLoss =
(leg.strike * multiplier - leg.optionPrice * multiplier) * quantity;
} else { } else {
console.error("Limits not defined for scenario:", scenarioKey); console.error("Metrics not defined for scenario:", scenarioKey);
// Defaulting to zero contribution if unknown
legProfit = 0; legProfit = 0;
legLoss = 0; legLoss = 0;
} }
// Sum only the finite numbers. If any leg is unlimited, the corresponding flag is set. // Sum only the finite numbers.
if (isFinite(legProfit)) { if (isFinite(legProfit)) {
overallMaxProfit += legProfit; overallMaxProfit += legProfit;
} }
@ -397,8 +422,8 @@
} }
} }
// Format the aggregated limits: if any leg was unlimited, set overall accordingly. // Format the aggregated metrics.
limits = { metrics = {
maxProfit: unlimitedProfit maxProfit: unlimitedProfit
? "Unlimited" ? "Unlimited"
: `$${formatCurrency(overallMaxProfit)}`, : `$${formatCurrency(overallMaxProfit)}`,
@ -406,30 +431,38 @@
? "Unlimited" ? "Unlimited"
: `$${formatCurrency(overallMaxLoss)}`, : `$${formatCurrency(overallMaxLoss)}`,
}; };
}
// For break-even price, if there's exactly one leg, compute it; otherwise, set it to null. function calculateBreakevenPrice(dataPoints) {
if (userStrategy.length === 1) { breakEvenPrice = null;
const leg = userStrategy[0]; // Loop over the dataPoints to find a sign change from loss to profit or vice versa
const scenarioKey = `${leg?.action} ${leg?.optionType}`; for (let i = 1; i < dataPoints.length; i++) {
if (breakEvenCalculators[scenarioKey]) { const [prevPrice, prevProfitLoss] = dataPoints[i - 1];
breakEvenPrice = breakEvenCalculators[scenarioKey]( const [currPrice, currProfitLoss] = dataPoints[i];
leg.strike,
leg.optionPrice, // 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 // Helper function for currency formatting
function formatCurrency(value: number): string { function formatCurrency(value: number): string {
return value.toLocaleString("en-US", { return value?.toLocaleString("en-US", {
minimumFractionDigits: 2, minimumFractionDigits: 2,
maximumFractionDigits: 2, maximumFractionDigits: 2,
}); });
} }
// DATA LOADING FUNCTIONS
const getContractHistory = async (contractId: string) => { const getContractHistory = async (contractId: string) => {
const cacheKey = contractId; const cacheKey = contractId;
const cachedData = getCache(cacheKey, "getContractHistory"); const cachedData = getCache(cacheKey, "getContractHistory");
@ -473,8 +506,6 @@
return; return;
} }
isLoaded = false;
try { try {
if (userStrategy?.length > 0) { if (userStrategy?.length > 0) {
for (const item of userStrategy) { for (const item of userStrategy) {
@ -490,7 +521,7 @@
item.date = selectedDate; item.date = selectedDate;
} }
strikeList = optionData[selectedDate] || []; strikeList = optionData[item.date] || [];
item.strikeList = strikeList; item.strikeList = strikeList;
// Find closest strike to current stock price // Find closest strike to current stock price
@ -508,9 +539,9 @@
// Get option price // Get option price
optionSymbol = buildOptionSymbol( optionSymbol = buildOptionSymbol(
selectedTicker, selectedTicker,
selectedDate, item?.date,
selectedOptionType, item?.optionType,
selectedStrike, item?.strike,
); );
const output = await getContractHistory(optionSymbol); const output = await getContractHistory(optionSymbol);
@ -591,27 +622,6 @@
rawData = (await response.json()) || {}; rawData = (await response.json()) || {};
currentStockPrice = rawData?.getStockQuote?.price || 0; currentStockPrice = rawData?.getStockQuote?.price || 0;
// Initialize option data
if (rawData?.getData) {
optionData = rawData.getData[selectedOptionType] || {};
dateList = Object.keys(optionData);
if (dateList.length > 0) {
selectedDate = dateList[0];
strikeList = optionData[selectedDate] || [];
// Select strike closest to current stock price
if (strikeList.length > 0) {
selectedStrike = strikeList.reduce((closest, strike) => {
return Math.abs(strike - currentStockPrice) <
Math.abs(closest - currentStockPrice)
? strike
: closest;
}, strikeList[0]);
}
}
}
} catch (error) { } catch (error) {
console.error("Error fetching stock data:", error); console.error("Error fetching stock data:", error);
} }
@ -689,6 +699,7 @@
const updatedStrategy = [...userStrategy]; const updatedStrategy = [...userStrategy];
updatedStrategy[index].date = selectedDate; updatedStrategy[index].date = selectedDate;
userStrategy = updatedStrategy; userStrategy = updatedStrategy;
await loadData("default");
shouldUpdate = true; shouldUpdate = true;
} }
} }
@ -703,23 +714,20 @@
} }
} }
function handleOptionPriceInput(event: Event) { function handleOptionPriceInput(event: Event, index) {
const value = (event.target as HTMLInputElement).value; const value = (event.target as HTMLInputElement).value;
selectedOptionPrice = value === "" ? null : +value; selectedOptionPrice = value === "" ? null : +value;
const updatedStrategy = [...userStrategy];
updatedStrategy[index].optionPrice = selectedOptionPrice;
userStrategy = updatedStrategy;
// Clear any existing debounce timeout // Clear any existing debounce timeout
if (debounceTimeout) clearTimeout(debounceTimeout); if (debounceTimeout) clearTimeout(debounceTimeout);
// Set a new debounce timeout // Set a new debounce timeout
debounceTimeout = setTimeout(() => { debounceTimeout = setTimeout(() => {
if (userStrategy.length > 0) { shouldUpdate = true;
userStrategy = userStrategy.map((leg) => ({
...leg,
optionPrice: selectedOptionPrice,
}));
shouldUpdate = true;
}
}, 300); }, 300);
} }
@ -729,18 +737,15 @@
selectedQuantity = value === "" ? null : +value; selectedQuantity = value === "" ? null : +value;
const updatedStrategy = [...userStrategy]; const updatedStrategy = [...userStrategy];
updatedStrategy[index].quantity = updatedStrategy[index].quantity = updatedStrategy[index].quantity = selectedQuantity;
selectedQuantity;
userStrategy = updatedStrategy; userStrategy = updatedStrategy;
if (debounceTimeout) clearTimeout(debounceTimeout);
// Set a new debounce timeout
debounceTimeout = setTimeout(() => {
shouldUpdate = true;
}, 300);
} }
// Clear any existing debounce timeout
if (debounceTimeout) clearTimeout(debounceTimeout);
// Set a new debounce timeout
debounceTimeout = setTimeout(() => {
shouldUpdate = true;
}, 300);
} }
async function search() { async function search() {
@ -779,6 +784,7 @@
await getStockData(); await getStockData();
await loadData("default"); await loadData("default");
inputValue = ""; inputValue = "";
shouldUpdate = true;
} }
// LIFECYCLE FUNCTIONS // LIFECYCLE FUNCTIONS
@ -800,7 +806,10 @@
$: { $: {
if (shouldUpdate) { if (shouldUpdate) {
shouldUpdate = false; shouldUpdate = false;
config = plotData(); config = plotData();
userStrategy = [...userStrategy];
isLoaded = true; isLoaded = true;
} }
} }
@ -1120,7 +1129,7 @@
step="0.1" step="0.1"
min="0" min="0"
value={userStrategy[index]?.optionPrice} value={userStrategy[index]?.optionPrice}
on:input={handleOptionPriceInput} on:input={(e) => handleOptionPriceInput(e, index)}
class="border border-gray-300 dark:border-gray-500 rounded px-2 py-1 w-24 focus:outline-none focus:ring-1 focus:ring-blue-500" class="border border-gray-300 dark:border-gray-500 rounded px-2 py-1 w-24 focus:outline-none focus:ring-1 focus:ring-blue-500"
/> />
</td> </td>
@ -1239,7 +1248,9 @@
</div> </div>
<div class="flex items-baseline"> <div class="flex items-baseline">
<span class="text-lg font-semibold" <span class="text-lg font-semibold"
>${breakEvenPrice?.toFixed(2)}</span >{typeof breakEvenPrice === "number"
? "$" + breakEvenPrice?.toFixed(2)
: "n/a"}</span
> >
</div> </div>
</div> </div>
@ -1289,7 +1300,7 @@
<div <div
class="text-lg font-semibold text-green-800 dark:text-green-400" class="text-lg font-semibold text-green-800 dark:text-green-400"
> >
{limits?.maxProfit} {metrics?.maxProfit}
</div> </div>
</div> </div>
@ -1307,7 +1318,7 @@
<div <div
class="text-lg font-semibold text-red-600 dark:text-red-400" class="text-lg font-semibold text-red-600 dark:text-red-400"
> >
{limits?.maxLoss} {metrics?.maxLoss}
</div> </div>
</div> </div>
</div> </div>