diff --git a/package-lock.json b/package-lock.json index cde3672..e95ba90 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,7 @@ "@mui/x-data-grid": "7.3", "@mui/x-date-pickers": "^7.3.2", "@prisma/client": "^5.12.1", + "@uidotdev/usehooks": "^2.4.1", "dayjs": "^1.11.10", "human-format": "^1.2.0", "ky": "^1.2.3", @@ -1467,6 +1468,18 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@uidotdev/usehooks": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@uidotdev/usehooks/-/usehooks-2.4.1.tgz", + "integrity": "sha512-1I+RwWyS+kdv3Mv0Vmc+p0dPYH0DTRAo04HLyXReYBL9AeseDWUJyi4THuksBJcu9F0Pih69Ak150VDnqbVnXg==", + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, "node_modules/@ungap/structured-clone": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", diff --git a/package.json b/package.json index b800d1a..f1384c3 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "@mui/x-data-grid": "7.3", "@mui/x-date-pickers": "^7.3.2", "@prisma/client": "^5.12.1", + "@uidotdev/usehooks": "^2.4.1", "dayjs": "^1.11.10", "human-format": "^1.2.0", "ky": "^1.2.3", 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 37cff0a..9879db5 100644 --- a/src/app/api/symbols/[symbol]/options/analyze/tradier/route.ts +++ b/src/app/api/symbols/[symbol]/options/analyze/tradier/route.ts @@ -1,21 +1,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`; -const getQuotes = `${tradierBaseUri}v1/markets/quotes`; - -const client = ky.create({ - headers: { - 'Authorization': `Bearer ${process.env.TRADIER_TOKEN}`, - 'Accept': 'application/json' - }, - cache: 'no-cache' -}); +import { getCurrentPrice, getOptionData, getOptionExpirations } from '@/lib/tradierService'; export async function GET(request: Request, p: { params: { symbol: string } }) { const { searchParams } = new URL(request.url); @@ -26,11 +13,7 @@ export async function GET(request: Request, p: { params: { symbol: string } }) { const currentPrice = await getCurrentPrice(symbol); if (!currentPrice) throw new Error('Unable to evaluate current price') - const expresp = await client(optionsExpiration, { - searchParams: { - symbol - } - }).json<{ expirations: { date: string[] } }>(); + const expresp = await getOptionExpirations(symbol); const tillDate = dayjs().add(dteValue, 'days'); console.log(`all expirations: ${expresp.expirations.date}`); @@ -40,33 +23,4 @@ export async function GET(request: Request, p: { params: { symbol: string } }) { const allStrikes = getCalculatedStrikes(currentPrice, strikeCountValue, [...new Set(allOptionChains.flatMap(j => j.options.option.map(s => s.strike)))]); const finalResponse = calculateHedging(allOptionChains, allStrikes, allDates, currentPrice) return NextResponse.json(finalResponse); -} - -function getOptionData(symbol: string, expiration: string) { - return client(optionsChain, { - searchParams: { - symbol, - expiration, - 'greeks': 'true' - } - }).json(); -} - -async function getCurrentPrice(symbol: string) { - const cp = await client(getQuotes, { - searchParams: { - symbols: symbol - } - }).json<{ - quotes: { - quote: { - symbol: string, - last: number - } - } - }>(); - return cp.quotes.quote - //.find(x => x.symbol === symbol)? - .last; -} - +} \ No newline at end of file diff --git a/src/app/api/symbols/search/route.ts b/src/app/api/symbols/search/route.ts index e0ab0c6..6f3a43f 100644 --- a/src/app/api/symbols/search/route.ts +++ b/src/app/api/symbols/search/route.ts @@ -1,13 +1,27 @@ +import { lookupSymbol } from "@/lib/tradierService"; import { NextRequest, NextResponse } from "next/server"; -import yf from 'yahoo-finance2'; -export async function GET(request: NextRequest, p: { params: { symbol: string } }) { +export async function GET(request: NextRequest) { const q = request.nextUrl.searchParams.get('q'); if (!q) return NextResponse.json({ 'error': 'q parameter is not provided in the request.' }, { status: 400 }); - const resp = await yf.search(q); + + const res = await lookupSymbol(q) + if (!res.securities) return NextResponse.json({ items: [] }); + const { security } = res.securities + const t1 = Array.isArray(security) ? security : [security]; return NextResponse.json({ - items: resp + items: t1.map(j => { + return { + symbol: j.symbol, + name: j.description + }; + }) }); + + // const resp = await yf.search(q); + // return NextResponse.json({ + // items: resp + // }); } \ No newline at end of file diff --git a/src/components/TickerSearch.tsx b/src/components/TickerSearch.tsx index 3661658..2091414 100644 --- a/src/components/TickerSearch.tsx +++ b/src/components/TickerSearch.tsx @@ -1,38 +1,24 @@ -import { Autocomplete, CircularProgress, TextField, debounce } from '@mui/material'; +import { Autocomplete, CircularProgress, TextField } from '@mui/material'; +import { useDebounce } from "@uidotdev/usehooks"; import * as React from 'react'; -import { searchTicker } from '../lib/socket'; -import { useEffect, useMemo, useState } from 'react'; +import { useTickerSearch } from '../lib/socket'; import { SearchTickerItem } from '@/lib/types'; + interface ITickerProps { onChange: (value: SearchTickerItem) => void, label?: string } export const TickerSearch = (props: ITickerProps) => { - const [options, setOptions] = useState([]); - const [loading, setLoading] = useState(false); - const onInputChange = (ev: any, value: any, reason: any) => { - if (value) { - getData(value); - } else { - setOptions([]); - } - }; + const [searchTerm, setSearchTerm] = React.useState(''); + //const [options, setOptions] = useState([]); - const debouncedEventHandler = useMemo( - () => debounce(onInputChange, 300) - , []); - - const getData = async (searchTerm: string) => { - setLoading(true); - const result = await searchTicker(searchTerm); - setOptions(result); - setLoading(false); - }; + const debouncedSearchTerm = useDebounce(searchTerm, 300); + const { options, loading } = useTickerSearch(debouncedSearchTerm); return x} - onInputChange={debouncedEventHandler} + onInputChange={(ev, v) => setSearchTerm(v)} size='small' onChange={(ev, value) => value && props.onChange(value)} renderInput={(params) => ( @@ -54,6 +40,6 @@ export const TickerSearch = (props: ITickerProps) => { )} options={options} getOptionLabel={(option: SearchTickerItem) => `${option.symbol} - ${option.name}`} - fullWidth + fullWidth /> } \ No newline at end of file diff --git a/src/lib/socket.ts b/src/lib/socket.ts index 0fe1464..fd1e9a0 100644 --- a/src/lib/socket.ts +++ b/src/lib/socket.ts @@ -20,25 +20,21 @@ socket.on("connect", () => { }) -export const searchTicker = async (searchTerm: string) => { +export const searchTicker = async (searchTerm: string, signal?: AbortSignal) => { const { items } = await ky('/api/symbols/search', { searchParams: { q: searchTerm - } + }, + signal: signal }).json<{ - items: { - quotes: { - symbol: string, - longname: string, - isYahooFinance: boolean - }[] - } + items: SearchTickerItem[] }>(); - return items.quotes.filter(f => f.isYahooFinance).map(r => ({ - symbol: r.symbol, - name: r.longname - })); + return items; + // return items.quotes.filter(f => f.isYahooFinance).map(r => ({ + // symbol: r.symbol, + // name: r.longname + // })); // return new Promise((res, rej) => { // socket.emit('stock-list-request', { // q: searchTerm @@ -50,6 +46,32 @@ export const searchTicker = async (searchTerm: string) => { // }); } +export const useTickerSearch = (v: string) => { + const [options, setOptions] = useState([]); + const [loading, setLoading] = useState(false); + useEffect(() => { + if (!v) { + setOptions([]); + return; + } + const ab = new AbortController(); + const getData = async () => { + setLoading(true); + try { + const result = await searchTicker(v, ab.signal); + setOptions(result); + } catch (error) { + //do nothing + } + setLoading(false); + }; + getData(); + return () => { ab.abort(); } + }, [v]); + + return { options, loading }; +} + export const AddTickerToMyList = (item: SearchTickerItem) => { socket.emit('mystocks-add-request', item); } diff --git a/src/lib/tradierService.ts b/src/lib/tradierService.ts new file mode 100644 index 0000000..9615f6c --- /dev/null +++ b/src/lib/tradierService.ts @@ -0,0 +1,71 @@ +import ky from "ky"; +import { TradierOptionData } from "./types"; +const tradierBaseUri = process.env.TRADIER_BASE_URI || 'https://sandbox.tradier.com/'; +const optionsChain = `${tradierBaseUri}v1/markets/options/chains`; +const lookup = `${tradierBaseUri}v1/markets/lookup`; +const optionsExpiration = `${tradierBaseUri}v1/markets/options/expirations`; +const getQuotes = `${tradierBaseUri}v1/markets/quotes`; + +const client = ky.create({ + headers: { + 'Authorization': `Bearer ${process.env.TRADIER_TOKEN}`, + 'Accept': 'application/json' + }, + cache: 'no-cache' +}); + +export const getOptionExpirations = (symbol: string) => { + return client(optionsExpiration, { + searchParams: { + symbol + } + }).json<{ expirations: { date: string[] } }>(); + +} + +export const getOptionData = (symbol: string, expiration: string) => { + return client(optionsChain, { + searchParams: { + symbol, + expiration, + 'greeks': 'true' + } + }).json(); +} + +export const getCurrentPrice = async (symbol: string) => { + const cp = await client(getQuotes, { + searchParams: { + symbols: symbol + } + }).json<{ + quotes: { + quote: { + symbol: string, + last: number + } + } + }>(); + return cp.quotes.quote + //.find(x => x.symbol === symbol)? + .last; +} + +type Symbol = { + symbol: string, + description: string +} + +type LookupSymbolResponse = { securities: { + security: Symbol | Symbol[] +} } + +export const lookupSymbol = (q: string) => { + return client(lookup, { + searchParams: { + q, + //'types': 'stock, etf, index' + } + }).json(); + +} \ No newline at end of file