From c1e6f17477afdeeae862bd69fbb168959234ee14 Mon Sep 17 00:00:00 2001 From: mnsrulz Date: Wed, 4 Sep 2024 01:15:18 +0000 Subject: [PATCH 1/2] add tradingview dialog --- .../api/symbols/[symbol]/historical/route.ts | 14 ++++ src/components/TradingViewWidgetDialog.tsx | 74 +++++++++++++++++++ src/components/Watchlist.tsx | 18 ++++- 3 files changed, 104 insertions(+), 2 deletions(-) create mode 100644 src/app/api/symbols/[symbol]/historical/route.ts create mode 100644 src/components/TradingViewWidgetDialog.tsx diff --git a/src/app/api/symbols/[symbol]/historical/route.ts b/src/app/api/symbols/[symbol]/historical/route.ts new file mode 100644 index 0000000..3679164 --- /dev/null +++ b/src/app/api/symbols/[symbol]/historical/route.ts @@ -0,0 +1,14 @@ +import { NextResponse } from "next/server"; +import yf from 'yahoo-finance2'; + +export async function GET(request: Request, p: { params: { symbol: string } }) { + const resp = await yf.historical(p.params.symbol, { + interval: '1d', + period1: new Date(2023, 0, 1), + period2: new Date() + }); + return NextResponse.json({ + data: resp, + symbol: p.params.symbol + }); +} diff --git a/src/components/TradingViewWidgetDialog.tsx b/src/components/TradingViewWidgetDialog.tsx new file mode 100644 index 0000000..edd7aed --- /dev/null +++ b/src/components/TradingViewWidgetDialog.tsx @@ -0,0 +1,74 @@ +import { Dialog, DialogContent, DialogActions, Button } from "@mui/material"; +import { useRef, useEffect } from "react"; + +interface ITickerProps { + symbol: string, + onClose: () => void +} + +const TradingViewWidget = (props: { symbol: string }) => { + const container = useRef(null); + useEffect( + () => { + const script = document.createElement("script"); + script.src = "https://s3.tradingview.com/external-embedding/embed-widget-advanced-chart.js"; + script.type = "text/javascript"; + script.async = true; + script.innerHTML = ` + { + "container_id": "technical-analysis-chart-demo", + "width": "100%", + "height": "100%", + "autosize": true, + "symbol": "${props.symbol}", + "interval": "D", + "timezone": "exchange", + "theme": "light", + "style": "1", + "withdateranges": true, + "hide_side_toolbar": false, + "allow_symbol_change": true, + "save_image": false, + "studies": [ + "ROC@tv-basicstudies", + "StochasticRSI@tv-basicstudies", + "MASimple@tv-basicstudies" + ], + "show_popup_button": true, + "popup_width": "1000", + "popup_height": "650", + "support_host": "https://www.tradingview.com" + }`; + const c = (container.current as unknown as HTMLDivElement); + c?.appendChild(script); + + return () => { + script.remove(); //removes self. In dev mode react somehow adding two nodes, so need to remove it explicitly. + } + }, [props.symbol]); + + return ( +
+
+
Track all markets on TradingView
+
+ ); +} + + +export const TradingViewWidgetDialog = (props: ITickerProps) => { + const { onClose } = props; + + return ( + + + + + + + + + ); +}; \ No newline at end of file diff --git a/src/components/Watchlist.tsx b/src/components/Watchlist.tsx index 99c2c0d..0b70188 100644 --- a/src/components/Watchlist.tsx +++ b/src/components/Watchlist.tsx @@ -11,6 +11,7 @@ import { AddTradeDialog } from './AddTradeDialog'; import { GridLinkAction } from './GridLinkAction'; import { SearchTickerItem } from '@/lib/types'; import { TickerSearch } from './TickerSearch'; +import { TradingViewWidgetDialog } from './TradingViewWidgetDialog'; interface IWatchlistProps { @@ -73,16 +74,27 @@ export const Watchlist = (props: IWatchlistProps) => { setCurrentStock(row); setOpenAddTrade(true); }} + />, + } + label="Show in Trading view" + showInMenu + onClick={() => { + setCurrentStock(row); + setOpenTradingViewDialog(true); + }} /> ] } } ]; - + const [openAddTrade, setOpenAddTrade] = useState(false); + const [openTradingViewDialog, setOpenTradingViewDialog] = useState(false); const [openAddToWatchlist, setOpenAddToWatchlist] = useState(false); - + const handleCloseAddTrade = () => { setOpenAddTrade(false); }; const handleAddToWatchlist = (item: SearchTickerItem) => { addToWatchlist(item); setOpenAddToWatchlist(false); } return
@@ -121,5 +133,7 @@ export const Watchlist = (props: IWatchlistProps) => { + + {openTradingViewDialog && currentStock?.symbol && { setOpenTradingViewDialog(false) }} />}
} \ No newline at end of file From 374c3d5577c51c06d0cd55676a4f5125bba22ad3 Mon Sep 17 00:00:00 2001 From: mnsrulz Date: Wed, 4 Sep 2024 05:54:14 +0000 Subject: [PATCH 2/2] allow archive data --- .../[symbol]/options/analyze/tradier/route.ts | 101 +----------------- src/components/DeltaGammaHedging.tsx | 24 ++++- src/lib/dgHedgingHelper.ts | 89 +++++++++++++++ src/lib/socket.ts | 74 +++++++++++-- src/lib/types.ts | 15 +++ 5 files changed, 193 insertions(+), 110 deletions(-) create mode 100644 src/lib/dgHedgingHelper.ts diff --git a/src/app/api/symbols/[symbol]/options/analyze/tradier/route.ts b/src/app/api/symbols/[symbol]/options/analyze/tradier/route.ts index ab04a8e..37cff0a 100644 --- a/src/app/api/symbols/[symbol]/options/analyze/tradier/route.ts +++ b/src/app/api/symbols/[symbol]/options/analyze/tradier/route.ts @@ -2,6 +2,8 @@ import ky from "ky"; import dayjs from 'dayjs'; import { NextResponse } from "next/server"; import { OptionsHedgingDataset } from "@/lib/socket"; +import { TradierOptionData } from "@/lib/types"; +import { calculateHedging, getCalculatedStrikes } from "@/lib/dgHedgingHelper"; const tradierBaseUri = process.env.TRADIER_BASE_URI || 'https://sandbox.tradier.com/'; const optionsChain = `${tradierBaseUri}v1/markets/options/chains`; const optionsExpiration = `${tradierBaseUri}v1/markets/options/expirations`; @@ -36,79 +38,7 @@ export async function GET(request: Request, p: { params: { symbol: string } }) { const allOptionChains = await Promise.all(allDates.map(d => getOptionData(symbol, d))); const allStrikes = getCalculatedStrikes(currentPrice, strikeCountValue, [...new Set(allOptionChains.flatMap(j => j.options.option.map(s => s.strike)))]); - - const allOp = allOptionChains.flatMap(j => j.options.option.map(s => s)); - - //console.log(`Rendering with dates: ${allDates} and strikes: ${allStrikes}`); - // const model: Record = {}; - const dmodel: OptionsHedgingDataset[] = []; - const gmodel: OptionsHedgingDataset[] = []; - let maxPosition = 0, gmaxPosition = 0; - for (const sp of allStrikes) { - const deltaExposure: OptionsHedgingDataset = { strike: sp }; - const gammaExposure: OptionsHedgingDataset = { strike: sp }; - let sumOfPv = 0, sumOfCv = 0; - let sumOfGPv = 0, sumOfGCv = 0; - dmodel.push(deltaExposure); - gmodel.push(gammaExposure); - // model[sp] = { - // calls: [], - // puts: [], - // data: [] - // } - for (const dt of allDates) { - const cv_o = allOp.find(j => j.strike == sp && j.expiration_date == dt && j.option_type == 'call'); - const pv_o = allOp.find(j => j.strike == sp && j.expiration_date == dt && j.option_type == 'put'); - - const cv = (cv_o?.open_interest || 0) * (cv_o?.greeks?.delta || 0) * 100 * currentPrice; - const pv = (pv_o?.open_interest || 0) * (pv_o?.greeks?.delta || 0) * 100 * currentPrice; - - const gcv = (cv_o?.open_interest || 0) * (cv_o?.greeks?.gamma || 0) * 100 * currentPrice; - const gpv = (pv_o?.open_interest || 0) * (pv_o?.greeks?.gamma || 0) * 100 * currentPrice; - // model[sp].calls.push(cv); - // model[sp].puts.push(pv); - // model[sp].data.push(-cv, pv); - - deltaExposure[`${dt}-call`] = -cv; - deltaExposure[`${dt}-put`] = -pv; - - const gv = gcv - gpv; - - if (gv > 0) { - gammaExposure[`${dt}-call`] = -gv; - gammaExposure[`${dt}-put`] = 0; - } else { - gammaExposure[`${dt}-call`] = 0; - gammaExposure[`${dt}-put`] = -gv; - } - - sumOfPv = sumOfPv + Math.abs(pv); - sumOfCv = sumOfCv + Math.abs(cv); - - sumOfGPv = sumOfGPv + Math.abs(gpv); - sumOfGCv = sumOfGCv + Math.abs(gcv); - } - maxPosition = Math.max(maxPosition, sumOfPv, sumOfCv); - gmaxPosition = Math.max(gmaxPosition, sumOfGPv, sumOfGCv); - } - - const finalResponse = { - exposureData: { - strikes: allStrikes, - expirations: allDates, - currentPrice, - maxPosition, - deltaDataset: { - dataset: dmodel, - maxPosition - }, - gammaDataset: { - dataset: gmodel, - maxPosition: gmaxPosition - } - }, - raw: allOptionChains - } + const finalResponse = calculateHedging(allOptionChains, allStrikes, allDates, currentPrice) return NextResponse.json(finalResponse); } @@ -119,20 +49,7 @@ function getOptionData(symbol: string, expiration: string) { expiration, 'greeks': 'true' } - }).json<{ - options: { - option: { - strike: number, - open_interest: number, - expiration_date: string, - option_type: 'put' | 'call', - greeks: { - delta: number, - gamma: number - } - }[] - } - }>(); + }).json(); } async function getCurrentPrice(symbol: string) { @@ -153,13 +70,3 @@ async function getCurrentPrice(symbol: string) { .last; } -///responsible for returning the strikes which we have to return in response. -function getCalculatedStrikes(currentPrice: number, maxStrikes: number, strikes: number[]) { - const currentOrAboveStrikes = strikes.filter(j => j >= currentPrice).sort((a, b) => a - b).reverse(); - const belowCurrentStrikes = strikes.filter(j => j < currentPrice).sort((a, b) => a - b); - let result = []; - while (result.length < maxStrikes && (currentOrAboveStrikes.length > 0 || belowCurrentStrikes.length > 0)) { - result.push(...[currentOrAboveStrikes.pop(), belowCurrentStrikes.pop()].filter(j => j)); - } - return result.map(Number).sort((a, b) => a - b); -} \ No newline at end of file diff --git a/src/components/DeltaGammaHedging.tsx b/src/components/DeltaGammaHedging.tsx index a2d7010..cebffea 100644 --- a/src/components/DeltaGammaHedging.tsx +++ b/src/components/DeltaGammaHedging.tsx @@ -1,10 +1,11 @@ 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 { ChartsReferenceLine } from '@mui/x-charts'; -import { OptionsHedgingData, useDeltaGammaHedging } from "@/lib/socket"; +import { OptionsHedgingData, useCachedDates, useDeltaGammaHedging } from "@/lib/socket"; import { getColorPallete } from "@/lib/color"; import { humanAbsCurrencyFormatter } from "@/lib/formatters"; import { useQueryState, parseAsInteger, parseAsStringEnum } from "nuqs"; +import { useState } from "react"; interface ITickerProps { symbol: string, @@ -112,8 +113,9 @@ export const DeltaGammaHedging = (props: ITickerProps) => { const { onClose } = props; const [dte, setDte] = useQueryState('dte', parseAsInteger.withDefault(50)); const [strikeCounts, setStrikesCount] = useQueryState('sc', parseAsInteger.withDefault(30)); - - const { data, isLoading } = useDeltaGammaHedging(props.symbol, dte, strikeCounts); + const { cachedDates } = useCachedDates(props.symbol); + const [dataMode, setDataMode] = useState('Live'); + const { data, isLoading } = useDeltaGammaHedging(props.symbol, dte, strikeCounts, dataMode); const [gexTab, setGexTab] = useQueryState('dgextab', parseAsStringEnum(Object.values(DexGexType)).withDefault(DexGexType.DEX)); // if (isLoading) return ; @@ -153,6 +155,22 @@ export const DeltaGammaHedging = (props: ITickerProps) => { 100 + + Data Mode + + { + const currentOrAboveStrikes = strikes.filter(j => j >= currentPrice).sort((a, b) => a - b).reverse(); + const belowCurrentStrikes = strikes.filter(j => j < currentPrice).sort((a, b) => a - b); + let result = []; + while (result.length < maxStrikes && (currentOrAboveStrikes.length > 0 || belowCurrentStrikes.length > 0)) { + result.push(...[currentOrAboveStrikes.pop(), belowCurrentStrikes.pop()].filter(j => j)); + } + return result.map(Number).sort((a, b) => a - b); +} + +export const calculateHedging = (allOptionChains: TradierOptionData[], allStrikes: number[], allDates: string[], currentPrice: number) => { + const allOp = allOptionChains.flatMap(j => j.options.option.map(s => s)); + //console.log(`Rendering with dates: ${allDates} and strikes: ${allStrikes}`); + // const model: Record = {}; + const dmodel: OptionsHedgingDataset[] = []; + const gmodel: OptionsHedgingDataset[] = []; + let maxPosition = 0, gmaxPosition = 0; + for (const sp of allStrikes) { + const deltaExposure: OptionsHedgingDataset = { strike: sp }; + const gammaExposure: OptionsHedgingDataset = { strike: sp }; + let sumOfPv = 0, sumOfCv = 0; + let sumOfGPv = 0, sumOfGCv = 0; + dmodel.push(deltaExposure); + gmodel.push(gammaExposure); + // model[sp] = { + // calls: [], + // puts: [], + // data: [] + // } + for (const dt of allDates) { + const cv_o = allOp.find(j => j.strike == sp && j.expiration_date == dt && j.option_type == 'call'); + const pv_o = allOp.find(j => j.strike == sp && j.expiration_date == dt && j.option_type == 'put'); + + const cv = (cv_o?.open_interest || 0) * (cv_o?.greeks?.delta || 0) * 100 * currentPrice; + const pv = (pv_o?.open_interest || 0) * (pv_o?.greeks?.delta || 0) * 100 * currentPrice; + + const gcv = (cv_o?.open_interest || 0) * (cv_o?.greeks?.gamma || 0) * 100 * currentPrice; + const gpv = (pv_o?.open_interest || 0) * (pv_o?.greeks?.gamma || 0) * 100 * currentPrice; + // model[sp].calls.push(cv); + // model[sp].puts.push(pv); + // model[sp].data.push(-cv, pv); + + deltaExposure[`${dt}-call`] = -cv; + deltaExposure[`${dt}-put`] = -pv; + + const gv = gcv - gpv; + + if (gv > 0) { + gammaExposure[`${dt}-call`] = -gv; + gammaExposure[`${dt}-put`] = 0; + } else { + gammaExposure[`${dt}-call`] = 0; + gammaExposure[`${dt}-put`] = -gv; + } + + sumOfPv = sumOfPv + Math.abs(pv); + sumOfCv = sumOfCv + Math.abs(cv); + + sumOfGPv = sumOfGPv + Math.abs(gpv); + sumOfGCv = sumOfGCv + Math.abs(gcv); + } + maxPosition = Math.max(maxPosition, sumOfPv, sumOfCv); + gmaxPosition = Math.max(gmaxPosition, sumOfGPv, sumOfGCv); + } + + const finalResponse = { + exposureData: { + expirations: allDates, + strikes: allStrikes, + currentPrice, + maxPosition, + deltaDataset: { + dataset: dmodel, + maxPosition + }, + gammaDataset: { + dataset: gmodel, + maxPosition: gmaxPosition + } + }, + raw: allOptionChains + } + + return finalResponse; +} \ No newline at end of file diff --git a/src/lib/socket.ts b/src/lib/socket.ts index 9773a66..0fe1464 100644 --- a/src/lib/socket.ts +++ b/src/lib/socket.ts @@ -2,7 +2,8 @@ import { useCallback, useEffect, useState } from 'react'; import { io } from 'socket.io-client'; import ky from 'ky'; -import { NumberRange, OptionsInnerData, SearchTickerItem, StockPriceData } from './types'; +import { NumberRange, OptionsInnerData, SearchTickerItem, StockPriceData, TradierOptionData } from './types'; +import { calculateHedging, getCalculatedStrikes } from './dgHedgingHelper'; const WatchlistUpdateFrequency = parseInt(process.env.WATCHLIST_UPDATE_FREQUENCY_MS || '1000'); const URL = `https://mztrading-socket.deno.dev` @@ -105,10 +106,14 @@ type GammaDeltaDatasetType = { maxPosition: number } +export type CachedOptionSummaryType = { + symbol: string, dt: string +} + export type OptionsHedgingData = { expirations: string[], strikes: number[], - data: { puts: number[], calls: number[], data: number[] }, + //data: { puts: number[], calls: number[], data: number[] }, currentPrice: number, deltaDataset: GammaDeltaDatasetType, gammaDataset: GammaDeltaDatasetType @@ -143,23 +148,72 @@ export const useOptionTracker = (symbol: string) => { return { data, isLoading, strikePriceRange, setStrikePriceRange, targetPrice, setTargetPrice }; } +export const useCachedDatesData = (symbol: string, dt: string) => { + const [data, setOd] = useState([]); + const [isLoading, setIsLoading] = useState(true); + useEffect(() => { + setIsLoading(true); + ky(`https://mztrading-data.deno.dev/data`, { + searchParams: { + s: symbol, + dt + } + }).json().then(r => { + setOd(r); + }).finally(() => setIsLoading(false)); + }, [symbol]); -export const useDeltaGammaHedging = (symbol: string, dte: number, sc: number) => { - const [data, setOd] = useState(); + return { cachedDatesData: data, isLoadingCachedDatesData: isLoading }; +} + +export const useCachedDates = (symbol: string) => { + const [data, setOd] = useState([]); const [isLoading, setIsLoading] = useState(true); useEffect(() => { setIsLoading(true); - ky(`/api/symbols/${symbol}/options/analyze/tradier`, { + ky(`https://mztrading-data.deno.dev/summary`, { searchParams: { - dte, - sc + s: symbol } - }).json<{ exposureData: OptionsHedgingData }>().then(r => { - setOd(r.exposureData); + }).json<{ symbol: string, dt: string }[]>().then(r => { + setOd(r); }).finally(() => setIsLoading(false)); - }, [symbol, dte, sc]); + }, [symbol]); + + return { cachedDates: data, isLoadingCachedDates: isLoading }; +} + +export const useDeltaGammaHedging = (symbol: string, dte: number, sc: number, dataMode: string) => { + const [data, setOd] = useState(); + const [isLoading, setIsLoading] = useState(true); + + useEffect(() => { + setIsLoading(true); + if (dataMode == 'Live') { + ky(`/api/symbols/${symbol}/options/analyze/tradier`, { + searchParams: { + dte, + sc + } + }).json<{ exposureData: OptionsHedgingData }>().then(r => { + setOd(r.exposureData); + }).finally(() => setIsLoading(false)); + } else { + ky(`https://mztrading-data.deno.dev/data`, { + searchParams: { + s: symbol, + dt: dataMode + } + }).json<{ data: TradierOptionData[], currentPrice: number }>().then(r => { + const allDates = [...new Set(r.data.flatMap(j => j.options.option.map(s => s.expiration_date)))]; + const allStrikes = getCalculatedStrikes(r.currentPrice, sc, [...new Set(r.data.flatMap(j => j.options.option.map(s => s.strike)))]); + const finalResponse = calculateHedging(r.data, allStrikes, allDates, r.currentPrice); + setOd(finalResponse.exposureData); + }).finally(() => setIsLoading(false)); + } + }, [symbol, dte, sc, dataMode]); return { data, isLoading }; } diff --git a/src/lib/types.ts b/src/lib/types.ts index a9636c5..15569e8 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -33,6 +33,21 @@ export type StockPriceData = { } } +export type TradierOptionData = { + options: { + option: { + strike: number, + open_interest: number, + expiration_date: string, + option_type: 'put' | 'call', + greeks: { + delta: number, + gamma: number + } + }[] + } + } + export type IOptionsGrid = { id: string }