From 8ba7a1c613ed5b33ff013b94afbb16f5b357f9fa Mon Sep 17 00:00:00 2001 From: mnsrulz Date: Thu, 22 Aug 2024 01:47:03 +0000 Subject: [PATCH] few enhancements 1. only trades screen is protected 2. better ui experience --- package-lock.json | 17 +++ package.json | 1 + src/app/api/symbols/[symbol]/summary/route.ts | 11 +- src/app/layout.tsx | 9 +- src/app/routes.tsx | 22 ++-- src/components/DeltaGammaHedging.tsx | 124 +++--------------- src/components/add-trade.tsx | 4 +- src/components/mystocks.tsx | 2 +- src/components/stock-options-view.tsx | 29 +++- src/lib/stockService.ts | 22 ++++ src/middleware.ts | 3 +- 11 files changed, 110 insertions(+), 134 deletions(-) create mode 100644 src/lib/stockService.ts diff --git a/package-lock.json b/package-lock.json index b4dd975..cde3672 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,6 +25,7 @@ "match-sorter": "^6.3.4", "next": "14.2.2", "next-auth": "^5.0.0-beta.16", + "nuqs": "^1.17.8", "react": "^18", "react-dom": "^18", "react-hook-form": "^7.51.2", @@ -4089,6 +4090,11 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==" + }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -4211,6 +4217,17 @@ } } }, + "node_modules/nuqs": { + "version": "1.17.8", + "resolved": "https://registry.npmjs.org/nuqs/-/nuqs-1.17.8.tgz", + "integrity": "sha512-JqsnzO+hJyjJE7ebuhpHMLA2iGY48e2xr0oJQFhj7kjUmDABL2XOup47rxF5TL/5b9jEsmU2t0lAKin1VdK1/A==", + "dependencies": { + "mitt": "^3.0.1" + }, + "peerDependencies": { + "next": ">=13.4 <14.0.2 || ^14.0.3" + } + }, "node_modules/oauth4webapi": { "version": "2.10.4", "resolved": "https://registry.npmjs.org/oauth4webapi/-/oauth4webapi-2.10.4.tgz", diff --git a/package.json b/package.json index 3350b37..b800d1a 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "match-sorter": "^6.3.4", "next": "14.2.2", "next-auth": "^5.0.0-beta.16", + "nuqs": "^1.17.8", "react": "^18", "react-dom": "^18", "react-hook-form": "^7.51.2", diff --git a/src/app/api/symbols/[symbol]/summary/route.ts b/src/app/api/symbols/[symbol]/summary/route.ts index 84e94d9..764d147 100644 --- a/src/app/api/symbols/[symbol]/summary/route.ts +++ b/src/app/api/symbols/[symbol]/summary/route.ts @@ -1,17 +1,16 @@ +import { getCurrentPrice } from "@/lib/stockService"; import { StockPriceData } from "@/lib/types"; import ky from "ky"; import { NextResponse } from "next/server"; //export const runtime = 'edge'; //This specifies the runtime environment that the middleware function will be executed in. export async function GET(request: Request, p: { params: { symbol: string } }) { - const priceresp = await ky(`https://www.optionsprofitcalculator.com/ajax/getStockPrice?stock=${p.params.symbol}&reqId=${new Date().getTime()}`, { - retry: { - limit: 3 - } - }).json<{ price: { last: number | undefined } }>(); + const resp = await getCurrentPrice(p.params.symbol) return NextResponse.json({ quoteSummary: { - price: { regularMarketPrice: priceresp.price.last as number } + price: { + regularMarketPrice: resp.price.last + } } } as StockPriceData); } diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 90d587e..54c7fec 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -3,6 +3,7 @@ import { Inter } from "next/font/google"; import "./globals.css"; import TabsRouter from "./routes"; import { CssBaseline } from "@mui/material"; +import { SessionProvider } from "next-auth/react"; const inter = Inter({ subsets: ["latin"] }); @@ -18,10 +19,12 @@ export default function RootLayout({ }>) { return ( - + - - {children} + + + + {children} ); diff --git a/src/app/routes.tsx b/src/app/routes.tsx index 404bd92..4634d81 100644 --- a/src/app/routes.tsx +++ b/src/app/routes.tsx @@ -17,7 +17,7 @@ import { useState } from 'react'; import MenuIcon from '@mui/icons-material/Menu'; import AdbIcon from '@mui/icons-material/Adb'; import AccountCircle from '@mui/icons-material/AccountCircle'; -import { signOut } from 'next-auth/react' +import { signIn, signOut, useSession } from 'next-auth/react' /* @@ -96,6 +96,7 @@ function CurrentRoute() { export default function TabsRouter() { const [anchorElNav, setAnchorElNav] = useState(null); const [anchorElUser, setAnchorElUser] = useState(null); + const session = useSession(); const handleOpenNavMenu = (event: React.MouseEvent) => { setAnchorElNav(event.currentTarget); @@ -231,7 +232,7 @@ export default function TabsRouter() { - + @@ -252,14 +253,17 @@ export default function TabsRouter() { open={Boolean(anchorElUser)} onClose={handleCloseUserMenu} > - {/* {settings.map((setting) => ( - - {setting} + {session.status == 'authenticated' ? ( + <> + + Sign Out + + + ) : ( + signIn()}> + Sign In - ))} */} - - Sign Out - + )} diff --git a/src/components/DeltaGammaHedging.tsx b/src/components/DeltaGammaHedging.tsx index 4f777d3..ea2807e 100644 --- a/src/components/DeltaGammaHedging.tsx +++ b/src/components/DeltaGammaHedging.tsx @@ -1,108 +1,23 @@ -import { Dialog, DialogContent, DialogActions, Button, Typography, LinearProgress, FormControl, InputLabel, MenuItem, Select, Tab, Tabs, Paper, CircularProgress, Box } from "@mui/material"; +import { Dialog, DialogContent, DialogActions, Button, Typography, LinearProgress, FormControl, InputLabel, MenuItem, Select, Tab, Tabs, Paper, Box } from "@mui/material"; import { BarChart } from '@mui/x-charts/BarChart'; -import { axisClasses, ChartsReferenceLine } from '@mui/x-charts'; +import { ChartsReferenceLine } from '@mui/x-charts'; import { OptionsHedgingData, useDeltaGammaHedging } from "@/lib/socket"; -import { useState } from "react"; import { getColorPallete } from "@/lib/color"; -import { currencyFormatter, fixedCurrencyFormatter, humanAbsCurrencyFormatter } from "@/lib/formatters"; +import { humanAbsCurrencyFormatter } from "@/lib/formatters"; +import { useQueryState, parseAsInteger, parseAsStringEnum } from "nuqs"; interface ITickerProps { symbol: string, onClose: () => void } -// const data = [ -// { strike: 67.5, exposure: 200 }, -// { strike: 69.0, exposure: -300 }, -// { strike: 70.5, exposure: 400 }, -// { strike: 72.0, exposure: -150 }, -// { strike: 73.5, exposure: 350 }, -// { strike: 75.0, exposure: -250 }, -// { strike: 76.5, exposure: 100 }, -// { strike: 78.0, exposure: -50 }, -// { strike: 79.5, exposure: 450 }, -// { strike: 81.0, exposure: -100 }, -// { strike: 82.5, exposure: 200 }, -// ]; - -// const seriesA = { -// data: [2, 3, 1, 4, 5], -// label: 'Series A', -// }; -// const seriesB = { -// data: [3, 1, 4, 2, 1], -// label: 'Series B', -// }; -// const seriesC = { -// data: [3, 2, 4, 5, 1], -// label: 'Series C', -// }; - -// const valueFormatter = (value: number | null) => `${value}`; -// const chartSetting = { -// yAxis: [ -// { -// label: 'Open interest', -// }, -// ], -// // width: 500, -// colors: ['red', 'green'], -// height: 500, -// sx: { -// [`.${axisClasses.left} .${axisClasses.label}`]: { -// //transform: 'translate(-20px, 0)', -// }, -// }, -// }; - - -/* - - - - - - - - - - - - -*/ - -// const colorCodes = [ -// '#a32020', -// '#e0301e', -// '#eb8c00', -// '#dc6900', - -// '#005073', -// '#107dac', -// '#189ad3', -// '#1ebbd7', -// '#71c7ec', - -// '#234d20', -// '#36802d', -// '#77ab59', -// '#c9df8a', -// '#f0f7da', - -// '#bf8bff', -// '#dabcff' -// ]; - -//deepOrange -// const colorCodes = [ -// "#A52A2A", "#FF3333", "#FF5733", "#FF8C33", "#FFA07A", -// "#228B22", "#33FF33", "#8CFF33", "#ADFF33", "#98FB98", -// "#4169E1", "#3333FF", "#3357FF", "#338CFF", "#87CEFA", -// "#B8860B", "#FFD700", "#FFEB3B", "#FFFACD", "#FFFFE0" -// ] - const colorCodes = getColorPallete(); +enum DexGexType { + 'DEX' = 'DEX', + 'GEX' = 'GEX' +} + interface IExpo { data: OptionsHedgingData, exposure: 'dex' | 'gex', @@ -194,14 +109,11 @@ const Expo = (props: IExpo) => { export const DeltaGammaHedging = (props: ITickerProps) => { const { onClose } = props; - const [dte, setDte] = useState(50); - const [strikeCounts, setStrikesCount] = useState(30); - const { data, isLoading } = useDeltaGammaHedging(props.symbol, dte, strikeCounts); - const [value, setValue] = useState(0); + const [dte, setDte] = useQueryState('dte', parseAsInteger.withDefault(50)); + const [strikeCounts, setStrikesCount] = useQueryState('sc', parseAsInteger.withDefault(30)); - const handleChange = (event: React.SyntheticEvent, newValue: number) => { - setValue(newValue); - }; + const { data, isLoading } = useDeltaGammaHedging(props.symbol, dte, strikeCounts); + const [gexTab, setGexTab] = useQueryState('dgextab', parseAsStringEnum(Object.values(DexGexType)).withDefault(DexGexType.DEX)); // if (isLoading) return ; // if (!data) return
No data to show!!!
; @@ -242,19 +154,19 @@ export const DeltaGammaHedging = (props: ITickerProps) => { setGexTab(v)} indicatorColor="secondary" textColor="inherit" variant="fullWidth" aria-label="full width tabs example" > - - + + { - isLoading ? : data ? :
no data...
+ isLoading ? : data ? :
no data...
} diff --git a/src/components/add-trade.tsx b/src/components/add-trade.tsx index c0d1730..67b4039 100644 --- a/src/components/add-trade.tsx +++ b/src/components/add-trade.tsx @@ -57,7 +57,7 @@ import ky from 'ky'; export const AddTradeDialog = (props: ITickerProps) => { const { onClose, open, ticker } = props; const theme = useTheme(); - const matches = useMediaQuery(theme.breakpoints.down('sm')); + const showFullScreenDialog = useMediaQuery(theme.breakpoints.down('sm')); if (!open) return
const handleSubmit = async (data: any) => { @@ -84,7 +84,7 @@ export const AddTradeDialog = (props: ITickerProps) => { return diff --git a/src/components/mystocks.tsx b/src/components/mystocks.tsx index 61254e2..3407f62 100644 --- a/src/components/mystocks.tsx +++ b/src/components/mystocks.tsx @@ -88,7 +88,7 @@ export const MyStockList = (props: ITickerProps) => { const handleCloseAddTrade = () => { setOpenAddTrade(false); }; const descriptionElementRef = React.useRef(null); return
-

my stocks

+

Watchlist

{/*
    {mytickers.map(m =>
  • {m.name} -- {m.symbol}
  • )}
*/} diff --git a/src/components/stock-options-view.tsx b/src/components/stock-options-view.tsx index 3be677c..fdf0e16 100644 --- a/src/components/stock-options-view.tsx +++ b/src/components/stock-options-view.tsx @@ -11,6 +11,8 @@ import { Pcr } from './pcr'; import { IOptionsGrid, NumberRange, OptionsInnerData } from '@/lib/types'; import { StrikePriceSlider } from './StrikePriceSlider'; import { DeltaGammaHedging } from './DeltaGammaHedging'; +import { useRouter, useSearchParams } from 'next/navigation'; +import { useQueryState, parseAsStringEnum, parseAsBoolean } from 'nuqs'; interface ITickerProps { symbol: string @@ -21,18 +23,33 @@ export type IStrikePriceSliderPorps = { allStrikePricesValues: number[], onChang type PriceModeType = 'LAST_PRICE' | 'BID_PRICE' | 'ASK_PRICE' | 'AVG_PRICE' type ValueModeType = 'PRICE' | 'ANNUAL_RETURN' | 'TOTAL_RETURN' | 'PCR' +// type PutCallType = 'PUT' | 'CALL'; +enum PutCallType { + 'PUT' = 'PUT', + 'CALL' = 'CALL' +} const numberFormatter = (v: string) => v && Number(v); const todaysDate = dayjs().format('YYYY-MM-DD'); export const StockOptionsView = (props: ITickerProps) => { const { data, isLoading, strikePriceRange, setStrikePriceRange, targetPrice, setTargetPrice } = useOptionTracker(props.symbol); - const [putCallTabValue, setPutCallTabValue] = useState<'PUT' | 'CALL'>('PUT'); + //const router = useRouter(); + //const query = useSearchParams(); + //const currentTab = query.get('tab') as PutCallType || 'PUT'; + // const handleCallTabValue = (v: PutCallType) => { + // setPutCallTabValue(v); + // const newSearchParams = new URLSearchParams(query); + // newSearchParams.set('tab', v); + // router.push(`?${newSearchParams.toString()}`); + // }; + + const [putCallTabValue, handleCallTabValue] = useQueryState('tab', parseAsStringEnum(Object.values(PutCallType)).withDefault(PutCallType.PUT)); const [priceMode, setPriceMode] = useState('AVG_PRICE'); const [valueMode, setValueMode] = useState('PRICE'); const [pcrSelectedData, setPcrSelectedData] = useState(); const [pcrOpen, setPcrOpen] = useState(false); - const [deltaHedgingOpen, setDeltaHedgingOpen] = useState(false); + const [deltaHedgingOpen, setDeltaHedgingOpen] = useQueryState('showDexGex', parseAsBoolean); function handlePcrSelection(v: string) { const fss = data?.options[v]; @@ -72,7 +89,7 @@ export const StockOptionsView = (props: ITickerProps) => { } const numberofdays = dayjs(d).diff(todaysDate, 'days') + 1; workingStrikePrices.forEach(s => { - const po = putCallTabValue == 'CALL' ? data.options[d].c[s.strikePrice] : data.options[d].p[s.strikePrice]; + const po = putCallTabValue == PutCallType.CALL ? data.options[d].c[s.strikePrice] : data.options[d].p[s.strikePrice]; const price = (() => { switch (priceMode) { case 'LAST_PRICE': @@ -89,13 +106,13 @@ export const StockOptionsView = (props: ITickerProps) => { (o as any)[s.strikePrice] = price && (() => { switch (valueMode) { case 'TOTAL_RETURN': - if (putCallTabValue == 'PUT') { + if (putCallTabValue == PutCallType.PUT) { return (targetPrice > s.value ? price : (price - (s.value - targetPrice))) / (s.value); } else { return (price / targetPrice); } case 'ANNUAL_RETURN': - if (putCallTabValue == 'PUT') { + if (putCallTabValue == PutCallType.PUT) { const sellCost = (targetPrice > s.value ? price : (price - (s.value - targetPrice))); const risk = s.value; return (sellCost / risk) * (365 / numberofdays); @@ -147,7 +164,7 @@ export const StockOptionsView = (props: ITickerProps) => { setTargetPrice(Number(v.target.value))} type='number' /> - setPutCallTabValue(v)} variant="fullWidth" indicatorColor="secondary" + handleCallTabValue(v)} variant="fullWidth" indicatorColor="secondary" textColor="secondary"> diff --git a/src/lib/stockService.ts b/src/lib/stockService.ts new file mode 100644 index 0000000..cab21b2 --- /dev/null +++ b/src/lib/stockService.ts @@ -0,0 +1,22 @@ +import ky from 'ky'; +import yf from 'yahoo-finance2'; + +export const getCurrentPrice = async (symbol: string) => { + try { + const quote = await yf.quoteCombine(symbol); + if (!quote.regularMarketPrice) throw new Error('null price received!'); + return { + price: { + last: quote.regularMarketPrice + } + } + } catch (error) { + console.log(`error occurred while fetching the price using yahoo finance api. falling back to optionprofitcalculator`); + const priceresp = await ky(`https://www.optionsprofitcalculator.com/ajax/getStockPrice?stock=${symbol}&reqId=${new Date().getTime()}`, { + retry: { + limit: 3 + } + }).json<{ price: { last: number } }>(); + return priceresp; + } +} \ No newline at end of file diff --git a/src/middleware.ts b/src/middleware.ts index 58b7237..8bdf57b 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -17,7 +17,8 @@ export default NextAuth(authOptions).auth; // }) export const config = { - matcher: ['/((?!api/auth|_next/static|_next/image|.*\\.png$|.*\\.ico$).*)'] + //matcher: ['/((?!api/auth|_next/static|_next/image|.*\\.png$|.*\\.ico$).*)'] + matcher: ['/api/trades/:path*', '/trades/:path*'] }; // async function isAuthenticated(req: NextRequest) {