diff --git a/package-lock.json b/package-lock.json index c020ba1..ac5f9fc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -33,6 +33,7 @@ "match-sorter": "^6.3.4", "next": "14.2.2", "next-auth": "^5.0.0-beta.16", + "numeral": "^2.0.6", "nuqs": "^1.17.8", "react": "^18", "react-dom": "^18", @@ -52,6 +53,7 @@ "@types/he": "^1.2.3", "@types/lodash.chunk": "^4.2.9", "@types/node": "^20", + "@types/numeral": "^2.0.5", "@types/react": "^18", "@types/react-dom": "^18", "eslint": "^8", @@ -1476,6 +1478,13 @@ "undici-types": "~5.26.4" } }, + "node_modules/@types/numeral": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/numeral/-/numeral-2.0.5.tgz", + "integrity": "sha512-kH8I7OSSwQu9DS9JYdFWbuvhVzvFRoCPCkGxNwoGgaPeDfEPJlcxNvEOypZhQ3XXHsGbfIuYcxcJxKUfJHnRfw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/parse-json": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", @@ -4434,6 +4443,15 @@ } } }, + "node_modules/numeral": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/numeral/-/numeral-2.0.6.tgz", + "integrity": "sha512-qaKRmtYPZ5qdw4jWJD6bxEf1FJEqllJrwxCLIm0sQU/A7v2/czigzOb+C2uSiFsa9lBUzeH7M1oK+Q+OLxL3kA==", + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/nuqs": { "version": "1.17.8", "resolved": "https://registry.npmjs.org/nuqs/-/nuqs-1.17.8.tgz", diff --git a/package.json b/package.json index b2ca41e..3b68755 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "match-sorter": "^6.3.4", "next": "14.2.2", "next-auth": "^5.0.0-beta.16", + "numeral": "^2.0.6", "nuqs": "^1.17.8", "react": "^18", "react-dom": "^18", @@ -57,6 +58,7 @@ "@types/he": "^1.2.3", "@types/lodash.chunk": "^4.2.9", "@types/node": "^20", + "@types/numeral": "^2.0.5", "@types/react": "^18", "@types/react-dom": "^18", "eslint": "^8", diff --git a/src/app/options/analyze/[symbol]/page.tsx b/src/app/options/analyze/[symbol]/page.tsx index 09d60eb..78d802e 100644 --- a/src/app/options/analyze/[symbol]/page.tsx +++ b/src/app/options/analyze/[symbol]/page.tsx @@ -1,25 +1,16 @@ -import { getDexGexAnalysisView } from "@/lib/tradierService"; -import { C } from "./C"; import { DexGexType } from "@/lib/types"; import { Suspense } from "react"; import { LinearProgress } from "@mui/material"; +import { OptionsAnalysisChartWrapper } from "@/components/OptionsAnalysisChartWrapper"; -const ChartWrapper = async (props: { symbol: string, dte: number, tab: DexGexType, strikeCountValue: number }) => { - const { symbol, dte, tab, strikeCountValue } = props; - const cachedDates: string[] = [];//await getCachedSummaryDatesBySymbol(symbol); - const { currentPrice, mappedOptions } = await getDexGexAnalysisView(symbol); - const dataMode = 'Live' - return -} - -export default async function Page({ params, searchParams }: { params: { symbol: string }, searchParams: { [key: string]: string | number } }) { +export default async function OptionsAnalysisPage({ params, searchParams }: { params: { symbol: string }, searchParams: { [key: string]: string | number } }) { const search = searchParams; const { symbol } = params; - const strikeCountValue = (search['sc'] || 30) as number; - const dte = (search['dte'] || 50) as number; - const tab = (search['tab'] || 'DEX') as DexGexType; + const strikeCount = (search['sc'] || 30) as number; + const daysToExpiration = (search['dte'] || 50) as number; + const analysisType = (search['tab'] || 'DEX') as DexGexType; return }> - + } \ No newline at end of file diff --git a/src/app/options/analyze/page.tsx b/src/app/options/analyze/page.tsx index ad07e7e..56b9ceb 100644 --- a/src/app/options/analyze/page.tsx +++ b/src/app/options/analyze/page.tsx @@ -1,6 +1,5 @@ 'use client'; import { TickerSearch } from "@/components/TickerSearch"; -import { Container } from "@mui/material"; import { useRouter } from 'next/navigation' export default function Page() { diff --git a/src/components/Expo.tsx b/src/components/Expo.tsx index 48779ba..690cd3e 100644 --- a/src/components/Expo.tsx +++ b/src/components/Expo.tsx @@ -33,6 +33,13 @@ const calculateLeftMargin = (maxStrikeValue: number) => { return 64 } +const xAxixFormatter = (datasetType: OptionsDatasetType, v: number) => { + if (datasetType == 'gex' && v > 0) { + return `-${humanAbsCurrencyFormatter(v)}`; + } + return humanAbsCurrencyFormatter(v); +} + export const Expo = (props: IExpo) => { const { data, dte, symbol, skipAnimation } = props; // const height = (data.strikes.length < 10 ? 100 : 0) + data.strikes.length * 15; @@ -114,7 +121,7 @@ export const Expo = (props: IExpo) => { scaleType: 'linear', min: -maxPosition * 1.05, //5% extra to allow some spacing max: maxPosition * 1.05, - valueFormatter: (v) => (isGex && v > 0) ? `-${humanAbsCurrencyFormatter(v)}` : humanAbsCurrencyFormatter(v) //in case of net gex, the values on right should have negative ticks + valueFormatter: (v: number) => xAxixFormatter(props.exposure, v)// (v) => (isGex && v > 0) ? `-${humanAbsCurrencyFormatter(v)}` : humanAbsCurrencyFormatter(v) //in case of net gex, the values on right should have negative ticks } ] } diff --git a/src/components/OptionsAnalysisChartWrapper.tsx b/src/components/OptionsAnalysisChartWrapper.tsx new file mode 100644 index 0000000..0e227d7 --- /dev/null +++ b/src/components/OptionsAnalysisChartWrapper.tsx @@ -0,0 +1,15 @@ +import { getDexGexAnalysisView } from "@/lib/tradierService"; +import { DexGexType } from "@/lib/types"; +import { getDexGexAnalysisViewCboe } from "@/lib/cboeService"; +import { OptionsAnalysisComponent } from "@/components/OptionsAnalysisComponent"; + +export const OptionsAnalysisChartWrapper = async (props: { symbol: string, daysToExpiration: number, analysisType: DexGexType, strikeCount: number }) => { + const { symbol, daysToExpiration, analysisType, strikeCount } = props; + const cachedDates: string[] = [];//await getCachedSummaryDatesBySymbol(symbol); + const { currentPrice, mappedOptions } = await getDexGexAnalysisView(symbol); + const dataMode = 'Live'; + + return +} \ No newline at end of file diff --git a/src/app/options/analyze/[symbol]/C.tsx b/src/components/OptionsAnalysisComponent.tsx similarity index 96% rename from src/app/options/analyze/[symbol]/C.tsx rename to src/components/OptionsAnalysisComponent.tsx index e13e776..43bb0fc 100644 --- a/src/app/options/analyze/[symbol]/C.tsx +++ b/src/components/OptionsAnalysisComponent.tsx @@ -5,7 +5,6 @@ import { calculateHedgingV2, getCalculatedStrikes } from "@/lib/dgHedgingHelper" import { DexGexType, MiniOptionContract } from "@/lib/types"; import { FormControl, InputLabel, Select, MenuItem, Box, Tab, Tabs, Paper, Container, Typography } from "@mui/material"; import dayjs from "dayjs"; -import { useRouter } from "next/navigation"; import { parseAsInteger, parseAsString, parseAsStringEnum, useQueryState } from "nuqs"; import { useMemo } from "react"; @@ -23,8 +22,7 @@ const stikeOptions = [20, 100, 150, 200] - -export const C = (props: { symbol: string, cachedDates: string[], dte: number, sc: number, dataMode: string, data: MiniOptionContract[], price: number, tab: DexGexType }) => { +export const OptionsAnalysisComponent = (props: { symbol: string, cachedDates: string[], dte: number, sc: number, dataMode: string, data: MiniOptionContract[], price: number, tab: DexGexType }) => { const { cachedDates, data, symbol, price, sc } = props; const [dte, setDte] = useQueryState('dte', parseAsInteger.withDefault(props.dte)); const [strikeCounts, setStrikesCount] = useQueryState('sc', parseAsInteger.withDefault(props.sc)); diff --git a/src/lib/cboeService.ts b/src/lib/cboeService.ts new file mode 100644 index 0000000..0439dd1 --- /dev/null +++ b/src/lib/cboeService.ts @@ -0,0 +1,46 @@ +import ky from "ky"; + +const client = ky.create({ + headers: { + 'Accept': 'application/json' + }, + cache: 'no-cache' +}); + +//JUST FOR REF.. PLANNING TO USE THE MZTRADING-DATA SERVICE FOR THIS +export const getDexGexAnalysisViewCboe = async (symbol: string) => { + const optionChain = await client(`https://cdn.cboe.com/api/global/delayed_quotes/options/${symbol}.json`).json<{ + data: { + options: { + option: string, + open_interest: number, + delta: number, + volume: number, + gamma: number, + }[], + close: number + } + }>(); + const currentPrice = optionChain.data.close; //TODO: is this the close price which remains same if the market is open?? + + console.time(`getDexGexAnalysisViewCboe-mapping-${symbol}`) + const mappedOptions = optionChain.data.options.map(({ option, open_interest,volume, delta, gamma }) => { + //implement mem cache for regex match?? + const rxMatch = /(\w+)(\d{6})([CP])(\d+)/.exec(option); + if(!rxMatch) throw new Error('error parsing option') + + return { + strike: Number(`${rxMatch[4]}`)/1000, + expiration_date: `20${rxMatch[2].substring(0,2)}-${rxMatch[2].substring(2,4)}-${rxMatch[2].substring(4,6)}`, + open_interest, + option_type: (rxMatch[3] == 'C' ? 'call': 'put') as 'call' | 'put', + volume, + greeks: { + delta: delta || 0, + gamma: gamma || 0, + } + } + }); + console.timeEnd(`getDexGexAnalysisViewCboe-mapping-${symbol}`) + return { mappedOptions, currentPrice } +} \ No newline at end of file diff --git a/src/lib/formatters.ts b/src/lib/formatters.ts index 7157cdc..1606125 100644 --- a/src/lib/formatters.ts +++ b/src/lib/formatters.ts @@ -1,17 +1,11 @@ +import numeral from 'numeral'; + export const percentageFormatter = (v: number) => v && Number(v).toLocaleString(undefined, { style: 'percent', minimumFractionDigits: 2 }) || ''; export const currencyFormatter = new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', }).format; export const numberFormatter = new Intl.NumberFormat('en-US', { style: 'decimal', maximumFractionDigits: 2 }).format; export const positiveNegativeNumberFormatter = new Intl.NumberFormat('en-US', { style: 'decimal', minimumFractionDigits: 2, maximumFractionDigits: 2, signDisplay: 'always' }).format; export const fixedCurrencyFormatter = new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', maximumFractionDigits: 0 }).format; -export const humanAbsCurrencyFormatter = (tick: number) => { - tick = Math.abs(tick); - if (tick >= 1000000000) { - return `${(tick / 1000000000).toFixed(1)}B`; // Billions - } else if (tick >= 1000000) { - return `${(tick / 1000000).toFixed(1)}M`; // Millions - } else if (tick >= 1000) { - return `${(tick / 1000).toFixed(1)}K`; // Thousands - } - return `${tick}`; +export const humanAbsCurrencyFormatter = (tick: number) => { + return numeral(Math.abs(tick)).format('0.[0]a').toUpperCase(); } \ No newline at end of file