diff --git a/src/routes/etf/[tickerID]/options/+layout.svelte b/src/routes/etf/[tickerID]/options/+layout.svelte index 0b1cbe53..a86a1e93 100644 --- a/src/routes/etf/[tickerID]/options/+layout.svelte +++ b/src/routes/etf/[tickerID]/options/+layout.svelte @@ -1,8 +1,45 @@
@@ -11,8 +48,36 @@
-
- +
+ +
+ +
- Build your Stock Screener to find profitable stocks. + Build your Stock Screener to find profitable etf. diff --git a/src/routes/etf/[tickerID]/options/+page.server.ts b/src/routes/etf/[tickerID]/options/+page.server.ts index bca8524c..59788669 100644 --- a/src/routes/etf/[tickerID]/options/+page.server.ts +++ b/src/routes/etf/[tickerID]/options/+page.server.ts @@ -25,24 +25,7 @@ export const load = async ({ locals, params }) => { return output; }; - const getOptionsPlotData = async () => { - const postData = { - ticker: params.tickerID, - }; - const response = await fetch(apiURL + "/options-plot-ticker", { - method: "POST", - headers: { - "Content-Type": "application/json", - "X-API-KEY": apiKey, - }, - body: JSON.stringify(postData), - }); - - const output = await response.json(); - - return output; - }; const getOptionsHistoricalData = async () => { const postData = { @@ -64,53 +47,12 @@ export const load = async ({ locals, params }) => { return output; }; - const getOptionsChainData = async () => { - const postData = { - ticker: params.tickerID, - }; - // make the POST request to the endpoint - const response = await fetch(apiURL + "/options-chain-data-ticker", { - method: "POST", - headers: { - "Content-Type": "application/json", - "X-API-KEY": apiKey, - }, - body: JSON.stringify(postData), - }); - - const output = await response.json(); - - return output; - }; - - const getOptionsGexData = async () => { - const postData = { - ticker: params.tickerID, - }; - - // make the POST request to the endpoint - const response = await fetch(apiURL + "/options-gex-ticker", { - 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 { getDailyStats: await getDailyStats(), - getOptionsPlotData: await getOptionsPlotData(), getOptionsHistoricalData: await getOptionsHistoricalData(), - getOptionsChainData: await getOptionsChainData(), - getOptionsGexData: await getOptionsGexData(), }; }; diff --git a/src/routes/etf/[tickerID]/options/+page.svelte b/src/routes/etf/[tickerID]/options/+page.svelte index fbf26817..a08a0c67 100644 --- a/src/routes/etf/[tickerID]/options/+page.svelte +++ b/src/routes/etf/[tickerID]/options/+page.svelte @@ -4,13 +4,14 @@ displayCompanyName, screenWidth, etfTicker, - setCache, - getCache, } from "$lib/store"; import DailyStats from "$lib/components/Options/DailyStats.svelte"; import { Chart } from "svelte-echarts"; - import { abbreviateNumber } from "$lib/utils"; - import InfoModal from "$lib/components/InfoModal.svelte"; + import { + abbreviateNumber, + abbreviateNumberWithColor, + monthNames, + } from "$lib/utils"; import { onMount } from "svelte"; import UpgradeToPro from "$lib/components/UpgradeToPro.svelte"; import { init, use } from "echarts/core"; @@ -18,83 +19,28 @@ import { GridComponent, TooltipComponent } from "echarts/components"; import { CanvasRenderer } from "echarts/renderers"; import Infobox from "$lib/components/Infobox.svelte"; + import * as HoverCard from "$lib/components/shadcn/hover-card/index.js"; + use([BarChart, LineChart, GridComponent, TooltipComponent, CanvasRenderer]); export let data; - let isLoaded = false; - let activeEX = 0; - let activeIdx = 0; let dailyStats = data?.getDailyStats; - const getDailyTransactions = async (transactionId) => { - let output; - const cachedData = getCache(transactionId, "getDailyTransactions"); - if (cachedData) { - output = cachedData; - } else { - const postData = { - transactionId: transactionId, - }; - // make the POST request to the endpoint - const response = await fetch("/api/options-daily-transactions", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(postData), - }); - - output = await response.json(); - - setCache(transactionId, output, "getDailyTransactions"); - } - - return output; - }; - - let rawPlotData = data?.getOptionsPlotData; let filteredList = []; - const monthNames = [ - "Jan", - "Feb", - "Mar", - "Apr", - "May", - "Jun", - "Jul", - "Aug", - "Sep", - "Oct", - "Nov", - "Dec", - ]; - let optionsPlotData = data?.getOptionsPlotData?.plot; let displayData = "volume"; let options; - let optionsEX; let rawData = data?.getOptionsHistoricalData; let optionList = rawData?.slice(0, 30); - let optionChainList = data?.getOptionsChainData?.at(0)?.chain || []; - let totalVolume; //= data?.getOptionsPlotData?.totalVolume; - - let totalOpenInterest; //= data?.getOptionsPlotData?.totalOpenInterest; - - // Computing the put-call ratio for open interest - let putCallOpenInterestRatio; //= data?.getOptionsPlotData?.putCallOpenInterestRatio; - let putCallRatio; - let displayTotalVolume; //= new Intl.NumberFormat("en", {minimumFractionDigits: 0, maximumFractionDigits: 0})?.format(totalVolume); - let displayTotalOpenInterest; //= new Intl.NumberFormat("en", {minimumFractionDigits: 0, maximumFractionDigits: 0})?.format(totalOpenInterest); - let displayTotalPutCall; let dateList; //= data?.getOptionsPlotData?.dateList; let callVolumeList; //= data?.getOptionsPlotData?.callVolumeList; let putVolumeList; //= data?.getOptionsPlotData?.putVolumeList; let callOpenInterestList; //= data?.getOptionsPlotData?.callOpenInterestList; let putOpenInterestList; //= data?.getOptionsPlotData?.putOpenInterestList; - let iv60List; + let priceList; let displayTimePeriod = "threeMonths"; @@ -114,27 +60,11 @@ month = (month < 10 ? "0" : "") + month; day = (day < 10 ? "0" : "") + day; - var formattedDate = month + "/" + day + "/" + year; + var formattedDate = day + "/" + year; return formattedDate; } - function formatTime(timeString) { - // Split the time string into components - const [hours, minutes, seconds] = timeString.split(":").map(Number); - - // Determine AM or PM - const period = hours >= 12 ? "PM" : "AM"; - - // Convert hours from 24-hour to 12-hour format - const formattedHours = hours % 12 || 12; // Converts 0 to 12 for midnight - - // Format the time string - const formattedTimeString = `${formattedHours.toString().padStart(2, "0")}:${minutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")} ${period}`; - - return formattedTimeString; - } - function changeTimePeriod(event) { displayTimePeriod = event.target.value; } @@ -143,13 +73,53 @@ displayData = event.target.value; } - function plotData(callData, putData, ivData) { + function plotData(callData, putData, priceList) { const options = { animation: false, tooltip: { trigger: "axis", + hideDelay: 100, + borderColor: "#969696", // Black border color + borderWidth: 1, // Border width of 1px + backgroundColor: "#313131", // Optional: Set background color for contrast + textStyle: { + color: "#fff", // Optional: Text color for better visibility + }, + formatter: function (params) { + // Get the timestamp from the first parameter + const timestamp = params[0].axisValue; + + // Initialize result with timestamp + let result = timestamp + "
"; + + // Sort params to ensure Vol appears last + params.sort((a, b) => { + if (a.seriesName === "Vol") return 1; + if (b.seriesName === "Vol") return -1; + return 0; + }); + + // Add each series data + params?.forEach((param) => { + const marker = + ''; + result += + marker + + param.seriesName + + ": " + + abbreviateNumber(param.value) + + "
"; + }); + + return result; + }, axisPointer: { - type: "shadow", + lineStyle: { + color: "#fff", + }, }, }, silent: true, @@ -224,10 +194,10 @@ }, }, { - name: "IV60", // Name for the line chart + name: "Price", // Name for the line chart type: "line", // Type of the chart (line) yAxisIndex: 1, // Use the second y-axis on the right - data: ivData, // iv60Data (assumed to be passed as ivData) + data: priceList, // iv60Data (assumed to be passed as priceList) itemStyle: { color: "#fff", // Choose a color for the line (gold in this case) }, @@ -242,95 +212,6 @@ return options; } - function getEXPlot(state) { - let dates = []; - let valueList = []; - let priceList = []; - - data?.getOptionsGexData?.forEach((item) => { - dates?.push(item?.date); - valueList?.push(item[state]); - priceList?.push(item?.close); - }); - - const option = { - silent: true, - animation: false, - tooltip: { - trigger: "axis", - hideDelay: 100, // Set the delay in milliseconds - }, - grid: { - left: "0%", - right: "0%", - bottom: "0%", - top: "5%", - containLabel: true, - }, - xAxis: { - type: "category", - boundaryGap: false, - data: dates, - axisLabel: { - color: "#fff", - formatter: function (value) { - // Assuming dates are in the format 'yyyy-mm-dd' - // Extract the month and day from the date string and convert the month to its abbreviated name - const dateParts = value.split("-"); - const day = dateParts[2].substring(0); // Extracting the last two digits of the year - const monthIndex = parseInt(dateParts[1]) - 1; // Months are zero-indexed in JavaScript Date objects - return `${day} ${monthNames[monthIndex]}`; - }, - }, - }, - yAxis: [ - { - type: "value", - splitLine: { - show: false, // Disable x-axis grid lines - }, - - axisLabel: { - show: false, // Hide y-axis labels - }, - }, - { - type: "value", - splitLine: { - show: false, // Disable x-axis grid lines - }, - position: "right", - axisLabel: { - show: false, // Hide y-axis labels - }, - }, - ], - series: [ - { - name: "Price", - data: priceList, - type: "line", - yAxisIndex: 0, - itemStyle: { - color: "#fff", - }, - showSymbol: false, - }, - { - name: state?.toUpperCase(), - data: valueList, - type: "bar", - yAxisIndex: 1, - itemStyle: { - color: "#8e53f4", // Change bar color to white - }, - }, - ], - }; - - return option; - } - function filterDate(filteredList, displayTimePeriod) { const now = Date.now(); let cutoffDate; @@ -365,52 +246,25 @@ } function processPlotData(filteredList: any[]) { - const totals = filteredList?.reduce( - (acc, obj) => { - acc.callVolume += obj?.CALL?.volume; - acc.putVolume += obj?.PUT?.volume; - acc.callOpenInterest += obj?.CALL?.open_interest; - acc.putOpenInterest += obj?.PUT?.open_interest; - return acc; - }, - { callVolume: 0, putVolume: 0, callOpenInterest: 0, putOpenInterest: 0 }, - ); - - putCallRatio = (totals.putVolume / totals.callVolume)?.toFixed(2); - totalVolume = totals.callVolume + totals.putVolume; - totalOpenInterest = totals.callOpenInterest + totals.putOpenInterest; - putCallOpenInterestRatio = ( - totals.putOpenInterest / totals.callOpenInterest - )?.toFixed(2); + filteredList = Array.isArray(filteredList) + ? filteredList.sort((a, b) => new Date(a?.date) - new Date(b?.date)) + : []; dateList = filteredList?.map((item) => item.date); - callVolumeList = filteredList?.map((item) => item?.CALL?.volume); - putVolumeList = filteredList?.map((item) => item?.PUT?.volume); - iv60List = filteredList?.map((item) => item?.iv60); + callVolumeList = filteredList?.map((item) => item?.call_volume); + putVolumeList = filteredList?.map((item) => item?.put_volume); + priceList = filteredList?.map((item) => item?.price); callOpenInterestList = filteredList?.map( - (item) => item?.CALL?.open_interest, + (item) => item?.call_open_interest, ); - putOpenInterestList = filteredList?.map((item) => item?.PUT?.open_interest); - - displayTotalVolume = new Intl.NumberFormat("en", { - minimumFractionDigits: 0, - maximumFractionDigits: 0, - }).format(totalVolume); - displayTotalPutCall = new Intl.NumberFormat("en", { - minimumFractionDigits: 0, - maximumFractionDigits: 0, - }).format(putCallRatio); - displayTotalOpenInterest = new Intl.NumberFormat("en", { - minimumFractionDigits: 0, - maximumFractionDigits: 0, - }).format(totalOpenInterest); + putOpenInterestList = filteredList?.map((item) => item?.put_open_interest); // Determine the type of plot data to generate based on displayData if (displayData === "volume") { - options = plotData(callVolumeList, putVolumeList, iv60List); + options = plotData(callVolumeList, putVolumeList, priceList); } else if (displayData === "openInterest") { - options = plotData(callOpenInterestList, putOpenInterestList, iv60List); + options = plotData(callOpenInterestList, putOpenInterestList, priceList); } } @@ -425,10 +279,6 @@ } onMount(async () => { - if (data?.getOptionsGexData?.length !== 0) { - optionsEX = getEXPlot("gex"); - } - if (data?.user?.tier === "Pro") { window.addEventListener("scroll", handleScroll); return () => { @@ -437,97 +287,10 @@ } }); - function daysLeft(targetDate) { - const targetTime = new Date(targetDate).getTime(); - const currentTime = new Date().getTime(); - const difference = targetTime - currentTime; - - const millisecondsPerDay = 1000 * 60 * 60 * 24; - const daysLeft = Math?.ceil(difference / millisecondsPerDay); - - return daysLeft; - } - - let optionHistoryList = []; - - let container; - let rawDataHistory = []; - - function getScroll() { - const scrollThreshold = container.scrollHeight * 0.8; // 80% of the container height - - // Check if the user has scrolled to the bottom based on the threshold - const isBottom = - container.scrollTop + container.clientHeight >= scrollThreshold; - - // Only load more data if at the bottom and there is still data to load - if (isBottom && optionHistoryList?.length !== rawDataHistory?.length) { - const nextIndex = optionHistoryList.length; // Ensure optionHistoryList is defined - const filteredNewResults = rawDataHistory.slice( - nextIndex, - nextIndex + 25, - ); // Ensure rawData is defined - optionHistoryList = [...optionHistoryList, ...filteredNewResults]; - } - } - - async function handleViewData(date: string) { - isLoaded = false; - optionDetailsDesktopModal?.showModal(); - - rawDataHistory = await getDailyTransactions($etfTicker + "-" + date); - - rawDataHistory?.forEach((item) => { - item.dte = daysLeft(item?.date_expiration); - }); - - optionHistoryList = rawDataHistory?.slice(0, 20); - isLoaded = true; - } - - function handleMode(i) { - activeIdx = i; - } - - const tabs = [ - { - title: "Historical Data", - }, - { - title: "Chain Data", - }, - ]; - - function changeStatement(event) { - optionChainList = - data?.getOptionsChainData - ?.filter((item) => item?.date_expiration === event.target.value) - ?.at(0)?.chain || []; - } - - const tabEX = [ - { - title: "GEX", - }, - { - title: "DEX", - }, - ]; - - function handleEXMode(i) { - activeEX = i; - optionsEX = getEXPlot(activeEX === 0 ? "gex" : "dex"); - } - $: { - if ( - (displayTimePeriod || displayData) && - rawPlotData?.length !== 0 && - typeof window !== "undefined" - ) { + if ((displayTimePeriod || displayData) && typeof window !== "undefined") { // Filter the raw plot data based on the selected time period - filteredList = filterDate(rawPlotData, displayTimePeriod); - + filteredList = filterDate(rawData, displayTimePeriod); // Process the filtered list to generate the plot data processPlotData(filteredList); } @@ -577,7 +340,7 @@ class="w-full relative flex justify-center items-center overflow-hidden" >
- {#if Object?.keys(dailyStats)?.length === 0 && rawPlotData?.length === 0} + {#if Object?.keys(dailyStats)?.length === 0 && rawData?.length === 0} {/if} @@ -587,90 +350,7 @@
{/if} - {#if rawPlotData?.length > 0} -
-
- - -
- {displayTotalVolume} -
-
-
- -
- {displayTotalOpenInterest} -
-
-
- -
- {putCallRatio !== "Infinity" ? putCallRatio : "> 1"} -
-
-
- - -
- {putCallOpenInterestRatio !== "Infinity" - ? putCallOpenInterestRatio - : "> 1"} -
-
-
- + {#if rawData?.length > 0}
- - {#each data?.getOptionsChainData as item, index} - - {/each} - -
- {/if} -
- {#if activeIdx === 0} - - - - - - - - - - - - - - - - {#each data?.user?.tier === "Pro" ? optionList : optionList?.slice(0, 3) as item, index} - handleViewData(item?.date)} - on:mouseover={() => - getDailyTransactions($etfTicker + "+" + item?.date)} - class="cursor-pointer sm:hover:bg-[#245073] sm:hover:bg-opacity-[0.2] odd:bg-odd border-b border-gray-800 {index + - 1 === - optionList?.slice(0, 3)?.length && - data?.user?.tier !== 'Pro' - ? 'opacity-[0.1]' - : ''}" - > - +
Date% ChangeP/CBear/BullBid/Ask Vol% OTMTotal VolumeTotal OITotal Prem
- {formatDate(item?.date)} -
+ + + + + + + + + + + + + + + + + + {#each data?.user?.tier === "Pro" ? optionList : optionList?.slice(0, 3) as item, index} + + - - - - - - - - - - - - - - - - - {/each} - -
Date% ChangeP/CVolumeC VolumeP VolumeVol/30D🐻/🐂 PremTotal OI% OI ChangeNet PremTotal Prem
+ {formatDate(item?.date)} + - {#if item?.changesPercentage >= 0} - +{item?.changesPercentage >= 1000 - ? abbreviateNumber(item?.changesPercentage) - : item?.changesPercentage?.toFixed(2)}% - {:else} - {item?.changesPercentage <= -1000 - ? abbreviateNumber(item?.changesPercentage) - : item?.changesPercentage?.toFixed(2)}% - - {/if} - - {item?.c_vol !== 0 - ? (item?.p_vol / item?.c_vol)?.toFixed(1) - : "-"} - - {#if item?.bear_ratio > (item?.neutral_ratio ?? 0) && item?.bear_ratio > (item?.bull_ratio ?? 0)} -
- {item?.bear_ratio?.toFixed(0)}% Bearish -
- {:else if item?.bull_ratio > (item?.neutral_ratio ?? 0) && item?.bull_ratio > (item?.bear_ratio ?? 0)} -
- {item?.bull_ratio?.toFixed(0)}% Bullish -
- {:else if item?.neutral_ratio > (item?.bull_ratio ?? 0) && item?.neutral_ratio > (item?.bear_ratio ?? 0)} -
- {item?.neutral_ratio?.toFixed(0)}% Neutral -
- {:else if item?.bear_ratio === item?.bull_ratio && item?.bear_ratio > (item?.neutral_ratio ?? 0)} -
- {item?.bear_ratio?.toFixed(0)}% Bear/Bull Tie -
- {:else if item?.bear_ratio === item?.neutral_ratio && item?.bear_ratio > (item?.bull_ratio ?? 0)} -
- {item?.bear_ratio?.toFixed(0)}% Bear/Neutral Tie -
- {:else if item?.bull_ratio === item?.neutral_ratio && item?.bull_ratio > (item?.bear_ratio ?? 0)} -
- {item?.bull_ratio?.toFixed(0)}% Bull/Neutral Tie -
- {:else} -
- Equal Distribution -
- {/if} -
- {#if item?.bid_ratio > (item?.midpoint_ratio ?? 0) && item?.bid_ratio > (item?.ask_ratio ?? 0)} -
- {item?.bid_ratio?.toFixed(0)}% Bid -
- {:else if item?.ask_ratio > (item?.midpoint_ratio ?? 0) && item?.ask_ratio > (item?.bid_ratio ?? 0)} -
- {item?.ask_ratio?.toFixed(0)}% Ask -
- {:else if item?.midpoint_ratio > (item?.ask_ratio ?? 0) && item?.midpoint_ratio > (item?.bid_ratio ?? 0)} -
- {item?.midpoint_ratio?.toFixed(0)}% Midpoint -
- {:else if item?.bid_ratio === item?.ask_ratio && item?.bid_ratio > (item?.midpoint_ratio ?? 0)} -
- {item?.bid_ratio?.toFixed(0)}% Bid/Ask Tie -
- {:else if item?.bid_ratio === item?.midpoint_ratio && item?.bid_ratio > (item?.ask_ratio ?? 0)} -
- {item?.bid_ratio?.toFixed(0)}% Bid/Neutral Tie -
- {:else if item?.ask_ratio === item?.midpoint_ratio && item?.ask_ratio > (item?.bid_ratio ?? 0)} -
- {item?.ask_ratio?.toFixed(0)}% Ask/Neutral Tie -
- {:else} -
- Equal Distribution -
- {/if} -
- {item?.otm_ratio?.toFixed(0)}% - - {@html abbreviateNumber( - item?.total_volume, - false, - true, - )} - - {@html abbreviateNumber(item?.total_oi, false, true)} - - {@html abbreviateNumber( - item?.total_bull_prem + - item?.total_bear_prem + - item?.total_neutral_prem, - false, - true, - )} -
- {:else} - - - - - - - - - - - - - - {#each data?.user?.tier === "Pro" ? optionChainList : optionChainList?.slice(0, 3) as item, index} - - - - - - - - + {:else} + {item?.changesPercentage <= -1000 + ? abbreviateNumberWithColor( + item?.changesPercentage, + ) + : item?.changesPercentage?.toFixed(2)}% + + {/if} + - + - + - - - {/each} - -
Call PremCall OICall VolumeStrike PricePut VolumePut OIPut Prem
- {@html abbreviateNumber( - item?.total_premium_call, - false, - true, - )} - - {@html abbreviateNumber( - item?.total_open_interest_call, - false, - true, - )} - - {@html abbreviateNumber( - item?.total_volume_call, - false, - true, - )} - -
+ {#if item?.changesPercentage >= 0} + +{item?.changesPercentage >= 1000 + ? abbreviateNumberWithColor( + item?.changesPercentage, + ) + : item?.changesPercentage?.toFixed(2)}% - {item?.strike_price} -
-
- {@html abbreviateNumber( - item?.total_volume_put, - false, - true, - )} - + {item?.putCallRatio} + - {@html abbreviateNumber( - item?.total_open_interest_put, - false, - true, - )} - + {item?.volume?.toLocaleString("en-US")} + - {@html abbreviateNumber( - item?.total_premium_put, - false, - true, - )} -
- {/if} + + {item?.call_volume?.toLocaleString("en-US")} + + + + {item?.put_volume?.toLocaleString("en-US")} + + + + {item?.avgVolumeRatio?.toFixed(2)} + + + + + +
+ +
+ +
+ + +
+ + +
+
+
+
+ +
+
+
+ Bearish: {@html abbreviateNumberWithColor( + item?.premium_ratio[0], + false, + true, + )} +
+
+ Neutral: {@html abbreviateNumberWithColor( + item?.premium_ratio[1], + false, + true, + )} +
+
+ Bullish: {@html abbreviateNumberWithColor( + item?.premium_ratio[2], + false, + true, + )} +
+
+
+
+
+ + + + {@html abbreviateNumberWithColor( + item?.total_open_interest, + false, + true, + )} + + + + {#if item?.changesPercentageOI >= 0} + +{item?.changesPercentageOI >= 1000 + ? abbreviateNumberWithColor( + item?.changesPercentageOI, + ) + : item?.changesPercentageOI?.toFixed(2)}% + {:else if item?.changesPercentageOI < 0} + {item?.changesPercentageOI <= -1000 + ? abbreviateNumberWithColor( + item?.changesPercentageOI, + ) + : item?.changesPercentageOI?.toFixed(2)}% + + {:else} + n/a + {/if} + + + + {@html abbreviateNumberWithColor( + item?.net_premium, + false, + true, + )} + + + + {@html abbreviateNumberWithColor( + item?.total_premium, + false, + true, + )} + + + {/each} + +
@@ -1137,191 +643,6 @@ - - - - - - - - -