diff --git a/src/routes/options-calculator/+page.svelte b/src/routes/options-calculator/+page.svelte index b5c8f15b..72967f10 100644 --- a/src/routes/options-calculator/+page.svelte +++ b/src/routes/options-calculator/+page.svelte @@ -37,14 +37,14 @@ let optionSymbol; let breakEvenPrice; - let premium; + let totalPremium; let limits = {}; let rawData = {}; let searchBarData = []; let timeoutId; - let inputValue = selectedTicker; + let inputValue = ""; let touchedInput = false; let prebuiltStrategy = [ { @@ -160,101 +160,123 @@ }; function plotData() { - // total premium paid for 1 contract (premium is calculated per share times 100 shares) - premium = selectedOptionPrice * 100 * selectedQuantity; + userStrategy = [ + { + action: selectedAction, + quantity: selectedQuantity, + date: selectedDate, + strike: selectedStrike, + optionType: selectedOptionType, + optionPrice: selectedOptionPrice, + }, + ]; - // Create a key from the selected action and option type - const scenarioKey = `${selectedAction} ${selectedOptionType}`; - - // Calculate break-even price per share using the mapping above. - // Note: For display, we assume optionPrice is per share. - breakEvenPrice; - if (breakEvenCalculators[scenarioKey]) { - breakEvenPrice = breakEvenCalculators[scenarioKey]( - selectedStrike, - selectedOptionPrice, - ); - } else { - console.error("Break-even scenario not implemented:", scenarioKey); - breakEvenPrice = selectedStrike; // default fallback - } - - limits = {}; - if (scenarioKey === "Buy Call") { - limits = { - maxProfit: "Unlimited", - maxLoss: `-$${premium?.toLocaleString("en-US", { - minimumFractionDigits: 2, - maximumFractionDigits: 2, - })}`, - }; - } else if (scenarioKey === "Sell Call") { - limits = { - maxProfit: `$${premium?.toLocaleString("en-US", { - minimumFractionDigits: 2, - maximumFractionDigits: 2, - })}`, - maxLoss: "Unlimited", - }; - } else if (scenarioKey === "Buy Put") { - limits = { - // Maximum profit when underlying goes to 0 - maxProfit: `$${(selectedStrike * 100 - premium)?.toLocaleString( - "en-US", - { - minimumFractionDigits: 2, - maximumFractionDigits: 2, - }, - )}`, - maxLoss: `-$${premium?.toLocaleString("en-US", { - minimumFractionDigits: 2, - maximumFractionDigits: 2, - })}`, - }; - } else if (scenarioKey === "Sell Put") { - limits = { - maxProfit: `$${premium?.toLocaleString("en-US", { - minimumFractionDigits: 2, - maximumFractionDigits: 2, - })}`, - // Maximum loss when underlying goes to 0 - maxLoss: `-$${(selectedStrike * 100 - premium)?.toLocaleString( - "en-US", - { - minimumFractionDigits: 2, - maximumFractionDigits: 2, - }, - )}`, - }; - } else { - console.error("Limits not defined for scenario:", scenarioKey); - limits = { maxProfit: "n/a", maxLoss: "n/a" }; - } - - const dataPoints = []; + // Determine x-axis range based on current stock price and max leg strike + const maxLegStrike = Math.max(...userStrategy?.map((leg) => leg.strike)); const xMin = 0; - const xMax = Math.floor(Math.max(currentStockPrice, selectedStrike) * 3); + const xMax = Math.floor(Math.max(currentStockPrice, maxLegStrike) * 3); const step = 10; - if (payoffFunctions[scenarioKey]) { - for (let s = xMin; s <= xMax; s += step) { - // For each price point, calculate payoff based on the scenario. - const payoff = payoffFunctions[scenarioKey](s, selectedStrike, premium); - dataPoints.push([s, payoff]); - } - } else { - console.error( - "Payoff function not implemented for scenario:", - scenarioKey, - ); + // 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, + ); + } else { + console.error( + "Payoff function not implemented for scenario:", + scenarioKey, + ); + } + }); + dataPoints.push([s, aggregatedPayoff]); } + if (userStrategy.length === 1) { + const leg = userStrategy[0]; + const scenarioKey = `${leg?.action} ${leg?.optionType}`; + if (breakEvenCalculators[scenarioKey]) { + breakEvenPrice = breakEvenCalculators[scenarioKey]( + leg.strike, + leg.optionPrice, + ); + } + if (scenarioKey === "Buy Call") { + limits = { + maxProfit: "Unlimited", + maxLoss: `-$${totalPremium.toLocaleString("en-US", { + minimumFractionDigits: 2, + maximumFractionDigits: 2, + })}`, + }; + } else if (scenarioKey === "Sell Call") { + limits = { + maxProfit: `$${totalPremium.toLocaleString("en-US", { + minimumFractionDigits: 2, + maximumFractionDigits: 2, + })}`, + maxLoss: "Unlimited", + }; + } else if (scenarioKey === "Buy Put") { + limits = { + maxProfit: `$${(leg.strike * 100 - totalPremium).toLocaleString( + "en-US", + { + minimumFractionDigits: 2, + maximumFractionDigits: 2, + }, + )}`, + maxLoss: `-$${totalPremium.toLocaleString("en-US", { + minimumFractionDigits: 2, + maximumFractionDigits: 2, + })}`, + }; + } else if (scenarioKey === "Sell Put") { + limits = { + maxProfit: `$${totalPremium.toLocaleString("en-US", { + minimumFractionDigits: 2, + maximumFractionDigits: 2, + })}`, + maxLoss: `-$${(leg.strike * 100 - totalPremium).toLocaleString( + "en-US", + { + minimumFractionDigits: 2, + maximumFractionDigits: 2, + }, + )}`, + }; + } else { + console.error("Limits not defined for scenario:", scenarioKey); + limits = { maxProfit: "n/a", maxLoss: "n/a" }; + } + } else { + // For multiple legs, simply display the aggregated premium info + limits = { + info: `Aggregated Premium: $${totalPremium.toLocaleString("en-US", { + minimumFractionDigits: 2, + maximumFractionDigits: 2, + })}`, + }; + } + + // Build the chart options (using the first leg's ticker for the title) const options = { - credits: { - enabled: false, - }, + credits: { enabled: false }, chart: { - type: "area", // or "line" + type: "area", backgroundColor: $mode === "light" ? "#fff" : "#09090B", plotBackgroundColor: $mode === "light" ? "#fff" : "#09090B", height: 400, @@ -272,9 +294,7 @@ text: `${selectedTicker} Price at Expiration ($)`, style: { color: $mode === "light" ? "#545454" : "white" }, }, - labels: { - style: { color: $mode === "light" ? "#545454" : "white" }, - }, + labels: { style: { color: $mode === "light" ? "#545454" : "white" } }, plotLines: [ // Underlying Price line { @@ -288,22 +308,27 @@ }, zIndex: 5, }, - { - value: breakEvenPrice, - color: "#10B981", - dashStyle: "Dash", - width: $screenWidth < 640 ? 0 : 1.5, - label: { - text: ``, - style: { color: $mode === "light" ? "black" : "white" }, - }, - zIndex: 5, - }, - ], + // Only add a breakeven line if there is a single leg + breakEvenPrice !== null + ? { + value: breakEvenPrice, + color: "#10B981", + dashStyle: "Dash", + width: $screenWidth < 640 ? 0 : 1.5, + label: { + text: ``, + style: { color: $mode === "light" ? "black" : "white" }, + }, + zIndex: 5, + } + : null, + ].filter((line) => line !== null), }, yAxis: { title: { - text: "Expected Profit/Loss ($)", + text: "", style: { color: $mode === "light" ? "#545454" : "white" }, }, gridLineWidth: 1, @@ -318,67 +343,55 @@ tooltip: { shared: true, useHTML: true, - backgroundColor: "rgba(0, 0, 0, 0.8)", // Semi-transparent black - borderColor: "rgba(255, 255, 255, 0.2)", // Slightly visible white border + backgroundColor: "rgba(0, 0, 0, 0.8)", + borderColor: "rgba(255, 255, 255, 0.2)", borderWidth: 1, - style: { - color: "#fff", - fontSize: "16px", - padding: "10px", - }, + style: { color: "#fff", fontSize: "16px", padding: "10px" }, borderRadius: 2, formatter: function () { const underlyingPrice = this.x; const profitLoss = this.y; - // Calculate percentage change for underlying price relative to currentStockPrice const underlyingPctChange = ((underlyingPrice - currentStockPrice) / currentStockPrice) * 100; - // Calculate profit/loss percentage relative to the total premium paid - const profitLossPctChange = (profitLoss / premium) * 100; - + const profitLossPctChange = (profitLoss / totalPremium) * 100; return ` -
-
- Underlying Price: - $${underlyingPrice} - (${underlyingPctChange.toFixed(2)}%) -
-
- Profit or Loss: - ${profitLoss < 0 ? "-$" : "$"}${Math.abs(profitLoss).toLocaleString("en-US")} - (${profitLossPctChange.toFixed(2)}%) -
-
- `; +
+
+ Underlying Price: + $${underlyingPrice} + (${underlyingPctChange.toFixed(2)}%) +
+
+ Profit or Loss: + ${profitLoss < 0 ? "-$" : "$"}${Math.abs(profitLoss).toLocaleString("en-US")} + (${profitLossPctChange.toFixed(2)}%) +
+
+ `; }, }, - plotOptions: { area: { fillOpacity: 0.2, - marker: { - enabled: false, - }, + marker: { enabled: false }, animation: false, }, series: { zoneAxis: "y", zones: [ { - value: 0, // below $0 -> red + value: 0, color: "#E02424", fillColor: "rgba(224,36,36,0.5)", }, { - color: "#10B981", // above $0 -> green + color: "#10B981", fillColor: "rgba(16,185,129,0.5)", }, ], }, }, - legend: { - enabled: false, - }, + legend: { enabled: false }, series: [ { name: "Payoff", @@ -432,8 +445,6 @@ } else { selectedAction = "Buy"; } - - config = plotData(); shouldUpdate = true; } @@ -449,7 +460,6 @@ // Set a new debounce timeout (1 second) debounceTimeout = setTimeout(() => { config = plotData(); - shouldUpdate = true; }, 500); } @@ -466,7 +476,6 @@ // Set a new debounce timeout (1 second) debounceTimeout = setTimeout(() => { config = plotData(); - shouldUpdate = true; }, 500); } @@ -519,10 +528,7 @@ const output = await getContractHistory(optionSymbol); selectedOptionPrice = output?.history?.at(-1)?.mark; - - config = plotData(); shouldUpdate = true; - isLoaded = true; } async function getStockData() { @@ -557,10 +563,12 @@ await getStockData(); await loadData("default"); + inputValue = ""; } onMount(async () => { await getStockData(); await loadData("default"); + shouldUpdate = true; }); onDestroy(() => { @@ -570,19 +578,10 @@ $: { if (shouldUpdate) { shouldUpdate = false; - userStrategy = [ - { - ticker: selectedTicker, - action: selectedAction, - quantity: selectedQuantity, - date: selectedDate, - strike: selectedStrike, - optionType: selectedOptionType, - optionPrice: selectedOptionPrice, - }, - ]; - console.log("yes"); + config = plotData(); + + isLoaded = true; } } @@ -656,6 +655,51 @@
{#if isLoaded && config} + +
+ +
+ {#if inputValue?.length !== 0 && inputValue !== selectedTicker} + + {#each searchBarData as searchItem} + changeTicker(searchItem)} + > +
+ {searchItem?.symbol} + {searchItem?.name} +
+
+ {:else} + + No results found + + {/each} +
+ {/if} +
+
- -
- -
- {#if inputValue?.length !== 0 && inputValue !== item?.ticker} - - {#each searchBarData as searchItem} - changeTicker(searchItem)} - > -
- {searchItem?.symbol} - {searchItem?.name} -
-
- {:else} - - No results found - - {/each} -
- {/if} -
+ {selectedTicker}
@@ -999,7 +1010,7 @@
${premium?.toLocaleString("en-US", { + >${totalPremium?.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2, })} @@ -1043,398 +1054,6 @@
- {:else}