From ef9fdca0b65c19b0f2251dbb8ab3c74ef459a00a Mon Sep 17 00:00:00 2001 From: MuslemRahimi Date: Sun, 6 Apr 2025 20:45:51 +0200 Subject: [PATCH] update calculator --- src/routes/api/options-calculator/+server.ts | 38 ++ src/routes/options-calculator/+page.server.ts | 49 --- src/routes/options-calculator/+page.svelte | 376 +++++++++++++----- 3 files changed, 310 insertions(+), 153 deletions(-) create mode 100644 src/routes/api/options-calculator/+server.ts delete mode 100644 src/routes/options-calculator/+page.server.ts diff --git a/src/routes/api/options-calculator/+server.ts b/src/routes/api/options-calculator/+server.ts new file mode 100644 index 00000000..16fd5365 --- /dev/null +++ b/src/routes/api/options-calculator/+server.ts @@ -0,0 +1,38 @@ +import type { RequestHandler } from "./$types"; + +export const POST: RequestHandler = async ({ request, locals }) => { + const data = await request.json(); + const { apiURL, apiKey } = locals; + + const postData = { ticker: data?.ticker }; + + // First API call: contract lookup summary + const contractResponse = await fetch(apiURL + "/contract-lookup-summary", { + method: "POST", + headers: { + "Content-Type": "application/json", + "X-API-KEY": apiKey, + }, + body: JSON.stringify(postData), + }); + + const contractOutput = await contractResponse.json(); + + // Second API call: stock quote + const stockResponse = await fetch(apiURL + "/stock-quote", { + method: "POST", + headers: { + "Content-Type": "application/json", + "X-API-KEY": apiKey, + }, + body: JSON.stringify(postData), + }); + + const stockOutput = await stockResponse.json(); + + // Combine both outputs into a single object + const output = { getData: contractOutput, getStockQuote: stockOutput }; + + + return new Response(JSON.stringify(output)); +}; diff --git a/src/routes/options-calculator/+page.server.ts b/src/routes/options-calculator/+page.server.ts deleted file mode 100644 index 74f33fdf..00000000 --- a/src/routes/options-calculator/+page.server.ts +++ /dev/null @@ -1,49 +0,0 @@ -export const load = async ({ locals }) => { - const { apiKey, apiURL } = locals; - - const getData = async () => { - const postData = { - ticker: 'TSLA', - }; - - const response = await fetch(apiURL + "/contract-lookup-summary", { - method: "POST", - headers: { - "Content-Type": "application/json", - "X-API-KEY": apiKey, - }, - body: JSON.stringify(postData), - }); - - const output = await response.json(); - - - return output; - }; - - - const getStockQuote = async () => { - const postData = { ticker: 'TSLA' }; - const response = await fetch(apiURL + "/stock-quote", { - method: "POST", - headers: { - "Content-Type": "application/json", - "X-API-KEY": apiKey, - }, - body: JSON.stringify(postData), - }); - - const output = await response.json(); - return output; - }; - - - - // Make sure to return a promise - return { - getData: await getData(), - getStockQuote: await getStockQuote(), - }; -}; - - diff --git a/src/routes/options-calculator/+page.svelte b/src/routes/options-calculator/+page.svelte index cbc3e7f2..ae863b4d 100644 --- a/src/routes/options-calculator/+page.svelte +++ b/src/routes/options-calculator/+page.svelte @@ -4,7 +4,8 @@ import SEO from "$lib/components/SEO.svelte"; import { onMount, onDestroy } from "svelte"; import { abbreviateNumber, buildOptionSymbol } from "$lib/utils"; - import { setCache, getCache } from "$lib/store"; + import { setCache, getCache, screenWidth } from "$lib/store"; + import { Combobox } from "bits-ui"; import { mode } from "mode-watcher"; import highcharts from "$lib/highcharts.ts"; @@ -21,23 +22,25 @@ let selectedQuantity = 1; let debounceTimeout; - let currentStockPrice = data?.getStockQuote?.price; + let currentStockPrice; - let optionData = data?.getData[selectedOptionType]; - let dateList = Object?.keys(optionData); - let selectedDate = Object?.keys(optionData)[0]; - let strikeList = optionData[selectedDate] || []; - let selectedStrike = strikeList.reduce((closest, strike) => { - return Math.abs(strike - currentStockPrice) < - Math.abs(closest - currentStockPrice) - ? strike - : closest; - }, strikeList[0]); + let optionData = {}; + let dateList = []; + let selectedDate; + let strikeList = []; + let selectedStrike; let optionSymbol; let breakEvenPrice; let premium; let limits = {}; + let rawData = {}; + + let searchBarData = []; + let timeoutId; + + let inputValue = selectedTicker; + let touchedInput = false; let strategies = [ { @@ -173,24 +176,48 @@ if (scenarioKey === "Buy Call") { limits = { maxProfit: "Unlimited", - maxLoss: `-$${premium?.toLocaleString("en-US")}`, + maxLoss: `-$${premium?.toLocaleString("en-US", { + minimumFractionDigits: 2, + maximumFractionDigits: 2, + })}`, }; } else if (scenarioKey === "Sell Call") { limits = { - maxProfit: `+$${premium?.toLocaleString("en-US")}`, + 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")}`, - maxLoss: `-$${premium?.toLocaleString("en-US")}`, + 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")}`, + maxProfit: `$${premium?.toLocaleString("en-US", { + minimumFractionDigits: 2, + maximumFractionDigits: 2, + })}`, // Maximum loss when underlying goes to 0 - maxLoss: `-$${(selectedStrike * 100 - premium)?.toLocaleString("en-US")}`, + maxLoss: `-$${(selectedStrike * 100 - premium)?.toLocaleString( + "en-US", + { + minimumFractionDigits: 2, + maximumFractionDigits: 2, + }, + )}`, }; } else { console.error("Limits not defined for scenario:", scenarioKey); @@ -245,22 +272,23 @@ // Underlying Price line { value: currentStockPrice, - color: "black", + color: $mode === "light" ? "black" : "white", dashStyle: "Dash", width: 1.2, label: { text: `Underlying Price $${currentStockPrice}`, + style: { color: $mode === "light" ? "black" : "white" }, }, zIndex: 5, }, - // Break-Even line { value: breakEvenPrice, color: "#10B981", dashStyle: "Dash", - width: 1.2, + width: $screenWidth < 640 ? 0 : 1.2, label: { - text: `Breakeven $${breakEvenPrice.toFixed(2)}`, + text: ``, + style: { color: $mode === "light" ? "black" : "white" }, }, zIndex: 5, }, @@ -282,11 +310,16 @@ }, tooltip: { shared: true, - backgroundColor: $mode === "light" ? "#f9fafb" : "#1f2937", - borderColor: "#6b7280", + useHTML: true, + backgroundColor: "rgba(0, 0, 0, 0.8)", // Semi-transparent black + borderColor: "rgba(255, 255, 255, 0.2)", // Slightly visible white border + borderWidth: 1, style: { - color: $mode === "light" ? "black" : "white", + color: "#fff", + fontSize: "16px", + padding: "10px", }, + borderRadius: 2, formatter: function () { const underlyingPrice = this.x; const profitLoss = this.y; @@ -297,17 +330,18 @@ const profitLossPctChange = (profitLoss / premium) * 100; return ` -
+
- Underlying Price: + Underlying Price: $${underlyingPrice} (${underlyingPctChange.toFixed(2)}%)
-
- Profit or Loss: - $${profitLoss.toLocaleString("en-US")} - (${profitLossPctChange.toFixed(2)}%) -
+
+
+ Profit or Loss: + ${profitLoss < 0 ? "-$" : "$"}${Math.abs(profitLoss).toLocaleString("en-US")} + (${profitLossPctChange.toFixed(2)}%) +
`; }, @@ -377,38 +411,6 @@ return output; }; - async function loadData(state: string) { - isLoaded = false; - optionData = data?.getData[selectedOptionType]; - - dateList = [...Object?.keys(optionData)]; - - strikeList = [...optionData[selectedDate]]; - - if (!strikeList?.includes(selectedStrike)) { - selectedStrike = strikeList.reduce((closest, strike) => { - return Math.abs(strike - currentStockPrice) < - Math.abs(closest - currentStockPrice) - ? strike - : closest; - }, strikeList[0]); - } - - optionSymbol = buildOptionSymbol( - selectedTicker, - selectedDate, - selectedOptionType, - selectedStrike, - ); - const output = await getContractHistory(optionSymbol); - - selectedOptionPrice = output?.history?.at(-1)?.mark; - - config = plotData(); - - isLoaded = true; - } - async function handleOptionType() { if (selectedOptionType === "Call") { selectedOptionType = "Put"; @@ -449,13 +451,106 @@ }, 500); } + async function search() { + clearTimeout(timeoutId); // Clear any existing timeout + + if (!inputValue.trim()) { + // Skip if query is empty or just whitespace + searchBarData = []; // Clear previous results + return; + } + + timeoutId = setTimeout(async () => { + const response = await fetch( + `/api/searchbar?query=${encodeURIComponent(inputValue)}&limit=10`, + ); + searchBarData = await response?.json(); + }, 50); // delay + } + + async function loadData(state: string) { + if (!rawData || !rawData.getData) { + console.error("rawData is undefined or invalid in loadData"); + return; + } + + isLoaded = false; + + optionData = rawData?.getData[selectedOptionType]; + + dateList = [...Object?.keys(optionData)]; + + strikeList = [...optionData[selectedDate]]; + + if (!strikeList?.includes(selectedStrike)) { + selectedStrike = strikeList.reduce((closest, strike) => { + return Math.abs(strike - currentStockPrice) < + Math.abs(closest - currentStockPrice) + ? strike + : closest; + }, strikeList[0]); + } + + optionSymbol = buildOptionSymbol( + selectedTicker, + selectedDate, + selectedOptionType, + selectedStrike, + ); + const output = await getContractHistory(optionSymbol); + + selectedOptionPrice = output?.history?.at(-1)?.mark; + + config = plotData(); + + isLoaded = true; + } + + async function getStockData() { + const postData = { ticker: selectedTicker }; + const response = await fetch("/api/options-calculator", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(postData), + }); + + rawData = (await response.json()) || {}; + + currentStockPrice = rawData?.getStockQuote?.price; + + optionData = rawData?.getData[selectedOptionType]; + dateList = Object?.keys(optionData); + selectedDate = Object?.keys(optionData)[0]; + strikeList = optionData[selectedDate] || []; + selectedStrike = strikeList.reduce((closest, strike) => { + return Math.abs(strike - currentStockPrice) < + Math.abs(closest - currentStockPrice) + ? strike + : closest; + }, strikeList[0]); + } + + async function changeTicker(symbol) { + selectedTicker = symbol; + await getStockData(); + await loadData("default"); + } onMount(async () => { + await getStockData(); await loadData("default"); }); onDestroy(() => { if (debounceTimeout) clearTimeout(debounceTimeout); }); + + $: { + if ($mode) { + config = plotData(); + } + } changeStrategy(strategy)} class="{selectedStrategy === strategy?.name - ? 'bg-blue-100' - : ''} select-none flex items-center space-x-2 border rounded-full px-3 py-1 text-sm font-medium border border-gray-300 cursor-pointer sm:hover:bg-blue-100" + ? 'bg-blue-100 dark:bg-primary text-muted' + : ''} text-sm elect-none flex items-center space-x-2 border border-gray-300 dark:border-gray-600 rounded-full px-3 py-1 text-blue-700 dark:text-white dark:sm:hover:text-white sm:hover:text-muted cursor-pointer" > {strategy.name} {#if strategy?.sentiment} {strategy.sentiment}{strategy.sentiment} {/if}
@@ -522,10 +618,14 @@ -
- +
+
- + + @@ -595,7 +740,7 @@ min="1" bind:value={selectedQuantity} on:input={handleQuantityInput} - class="border border-gray-300 rounded px-2 py-1 w-20 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-20 focus:outline-none focus:ring-1 focus:ring-blue-500" /> @@ -704,7 +849,7 @@ step="0.1" bind:value={selectedOptionPrice} on:input={handleOptionPriceInput} - class="border border-gray-300 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" /> @@ -755,7 +900,9 @@ {/if}
-

+

Trade Information

@@ -764,7 +911,7 @@ class="border border-gray-300 dark:border-gray-800 rounded-lg p-4 mb-6 shadow-sm max-w-sm" >
{selectedStrategy}
-
+
{selectedAction?.toUpperCase()} +{selectedQuantity} {selectedTicker} {formatDate(selectedDate)} @@ -774,10 +921,14 @@
-

Stock

+

+ Stock +

-
+
{selectedTicker} Current Price
@@ -788,7 +939,9 @@
-
+
{selectedTicker} Breakeven Price -

+

Trade Details

-
+
Cost of Trade
${premium?.toLocaleString("en-US")} + >${premium?.toLocaleString("en-US", { + minimumFractionDigits: 2, + maximumFractionDigits: 2, + })} +
-
+
Maximum Profit
-
+
{limits?.maxProfit}
-
+
Maximum Loss
-
+
{limits?.maxLoss}
-
- {selectedTicker} + +
+ +
+ {#if inputValue?.length !== 0 && inputValue !== selectedTicker} + + {#each searchBarData as item} + changeTicker(item?.symbol)} + > +
+ {item?.symbol} + {item?.name} +
+
+ {:else} + + No results found + + {/each} +
+ {/if} +
@@ -694,7 +839,7 @@