mango-v4-ui/apis/datafeed.ts

409 lines
9.5 KiB
TypeScript
Raw Normal View History

/* eslint-disable @typescript-eslint/no-explicit-any */
2023-03-04 21:48:09 -08:00
import { makeApiRequest, parseResolution } from './birdeye/helpers'
2023-03-04 12:42:35 -08:00
import {
makeApiRequest as makePerpApiRequest,
parseResolution as parsePerpResolution,
2023-03-04 21:48:09 -08:00
} from './mngo/helpers'
import {
closeSocket,
2023-03-04 12:42:35 -08:00
// isOpen,
subscribeOnStream as subscribeOnSpotStream,
2023-03-04 21:48:09 -08:00
} from './birdeye/streaming'
2023-03-04 12:42:35 -08:00
import {
closeSocket as closePerpSocket,
// isOpen as isPerpOpen,
subscribeOnStream as subscribeOnPerpStream,
unsubscribeFromStream as unsubscribeFromPerpStream,
2023-03-04 21:48:09 -08:00
} from './mngo/streaming'
2022-12-19 17:09:33 -08:00
import mangoStore from '@store/mangoStore'
2023-01-02 05:30:12 -08:00
import {
2023-01-02 05:41:22 -08:00
DatafeedConfiguration,
2023-01-02 05:30:12 -08:00
LibrarySymbolInfo,
ResolutionString,
2023-01-02 05:41:22 -08:00
SearchSymbolResultItem,
2023-01-28 11:21:45 -08:00
} from '@public/charting_library'
2023-05-04 12:41:33 -07:00
import { PublicKey } from '@solana/web3.js'
2023-08-06 18:54:41 -07:00
import {
Group,
PerpMarket,
Serum3Market,
} from '@blockworks-foundation/mango-v4'
2023-01-02 05:30:12 -08:00
2023-01-02 11:50:03 -08:00
export const SUPPORTED_RESOLUTIONS = [
'1',
'3',
'5',
'15',
'30',
'60',
'120',
'240',
'1D',
] as const
type BaseBar = {
2023-01-02 05:41:22 -08:00
low: number
high: number
open: number
close: number
}
2023-01-02 11:50:03 -08:00
type KlineBar = BaseBar & {
volume: number
timestamp: number
}
type TradingViewBar = BaseBar & {
time: number
}
type Bar = KlineBar & TradingViewBar
2023-02-27 23:20:11 -08:00
export type SymbolInfo = LibrarySymbolInfo & {
2023-01-02 05:30:12 -08:00
address: string
2023-08-06 18:54:41 -07:00
quote_token: string
base_token: string
2023-01-02 05:30:12 -08:00
}
2022-12-15 15:33:31 -08:00
const lastBarsCache = new Map()
const subscriptionIds = new Map()
2022-12-15 15:33:31 -08:00
const configurationData = {
2023-01-02 11:50:03 -08:00
supported_resolutions: SUPPORTED_RESOLUTIONS,
2022-12-15 15:33:31 -08:00
intraday_multipliers: ['1', '3', '5', '15', '30', '60', '120', '240'],
exchanges: [],
}
2023-08-06 18:54:41 -07:00
const getMktFromMktAddress = (
2023-05-05 11:13:43 -07:00
group: Group,
2023-07-21 11:47:53 -07:00
symbolAddress: string,
2023-08-06 18:54:41 -07:00
): Serum3Market | PerpMarket | null => {
2023-05-05 11:13:43 -07:00
try {
const serumMkt = group.getSerum3MarketByExternalMarket(
2023-07-21 11:47:53 -07:00
new PublicKey(symbolAddress),
2023-05-05 11:13:43 -07:00
)
if (serumMkt) {
2023-08-06 18:54:41 -07:00
return serumMkt
2023-05-05 11:13:43 -07:00
}
} catch {
console.log('Address is not a serum market')
}
const perpMarkets = Array.from(group.perpMarketsMapByMarketIndex.values())
const perpMkt = perpMarkets.find(
2023-07-21 11:47:53 -07:00
(perpMarket) => perpMarket.publicKey.toString() === symbolAddress,
2023-05-05 11:13:43 -07:00
)
2022-12-15 15:33:31 -08:00
2023-05-05 11:13:43 -07:00
if (perpMkt) {
2023-08-06 18:54:41 -07:00
return perpMkt
2023-05-05 11:13:43 -07:00
}
return null
}
2022-12-15 15:33:31 -08:00
2023-03-04 12:42:35 -08:00
let marketType: 'spot' | 'perp'
export const queryPerpBars = async (
tokenAddress: string,
2023-07-22 13:44:43 -07:00
resolution: (typeof SUPPORTED_RESOLUTIONS)[number],
2023-03-04 12:42:35 -08:00
periodParams: {
firstDataRequest: boolean
from: number
to: number
2023-07-21 11:47:53 -07:00
},
2023-03-04 12:42:35 -08:00
): Promise<Bar[]> => {
const { from, to } = periodParams
2023-09-05 20:59:59 -07:00
if (tokenAddress === 'Loading') return []
2023-03-04 12:42:35 -08:00
const urlParameters = {
'perp-market': tokenAddress,
resolution: parsePerpResolution(resolution),
2023-09-05 20:59:59 -07:00
start_datetime: new Date((from - 3_000_000) * 1000).toISOString(),
2023-03-04 12:42:35 -08:00
end_datetime: new Date(to * 1000).toISOString(),
}
const query = Object.keys(urlParameters)
.map((name: string) => `${name}=${(urlParameters as any)[name]}`)
.join('&')
const data = await makePerpApiRequest(`/stats/candles-perp?${query}`)
if (!data || !data.length) {
return []
}
let bars: Bar[] = []
let previousBar: Bar | undefined = undefined
2023-03-04 12:42:35 -08:00
for (const bar of data) {
const timestamp = new Date(bar.candle_start).getTime()
2023-09-05 20:59:59 -07:00
bars = [
...bars,
{
time: timestamp,
low: bar.low,
high: bar.high,
open: previousBar ? previousBar.close : bar.open,
close: bar.close,
volume: bar.volume,
timestamp,
},
]
previousBar = bar
2023-03-04 12:42:35 -08:00
}
return bars
}
export const queryBirdeyeBars = async (
2023-01-02 11:17:27 -08:00
tokenAddress: string,
2023-07-22 13:44:43 -07:00
resolution: (typeof SUPPORTED_RESOLUTIONS)[number],
2023-01-02 11:17:27 -08:00
periodParams: {
firstDataRequest: boolean
from: number
to: number
2023-07-21 11:47:53 -07:00
},
2023-08-06 18:54:41 -07:00
quote_token: string,
market: string,
2023-01-02 11:17:27 -08:00
): Promise<Bar[]> => {
2023-12-25 03:43:24 -08:00
try {
const { from, to } = periodParams
const urlParameters = {
base_address: tokenAddress,
quote_address: quote_token,
type: parseResolution(resolution),
time_from: from,
time_to: to,
merge_market_id: market,
}
2023-08-06 18:54:41 -07:00
2023-12-25 03:43:24 -08:00
const query = Object.keys(urlParameters)
.map(
(name: string) =>
`${name}=${encodeURIComponent((urlParameters as any)[name])}`,
)
.join('&')
2023-01-02 11:17:27 -08:00
2023-12-25 03:43:24 -08:00
const data = await makeApiRequest(`defi/ohlcv/base_quote_merge?${query}`)
if (!data.success || data.data.items.length === 0) {
return []
}
2023-12-25 03:43:24 -08:00
let bars: Bar[] = []
for (const bar of data.data.items) {
if (bar.unixTime >= from && bar.unixTime < to) {
const timestamp = bar.unixTime * 1000
if (bar.h >= 223111) continue
bars = [
...bars,
{
time: timestamp,
low: bar.l,
high: bar.h,
open: bar.o,
close: bar.c,
volume: bar.vQuote,
timestamp,
},
]
}
2023-01-02 11:17:27 -08:00
}
2023-12-25 03:43:24 -08:00
return bars
} catch (e) {
console.log('failed to query birdeye bars', e)
return []
2023-01-02 11:17:27 -08:00
}
}
2023-08-19 03:36:15 -07:00
const datafeed = {
2023-01-02 05:41:22 -08:00
onReady: (callback: (configuration: DatafeedConfiguration) => void) => {
2023-01-02 11:50:03 -08:00
setTimeout(() => callback(configurationData as any))
2022-12-15 15:33:31 -08:00
},
searchSymbols: async (
2023-01-02 05:41:22 -08:00
_userInput: string,
_exchange: string,
_symbolType: string,
2023-07-21 11:47:53 -07:00
_onResultReadyCallback: (items: SearchSymbolResultItem[]) => void,
2022-12-15 15:33:31 -08:00
) => {
return
},
resolveSymbol: async (
2023-01-02 05:30:12 -08:00
symbolAddress: string,
2023-07-21 11:47:53 -07:00
onSymbolResolvedCallback: (symbolInfo: SymbolInfo) => void,
2023-01-02 05:30:12 -08:00
// _onResolveErrorCallback: any,
// _extension: any
2022-12-15 15:33:31 -08:00
) => {
2023-01-02 05:30:12 -08:00
let symbolItem:
| {
address: string
type: string
symbol: string
}
| undefined
2022-12-15 15:33:31 -08:00
if (!symbolItem) {
2022-12-15 22:13:00 -08:00
symbolItem = {
address: symbolAddress,
type: 'pair',
2023-01-02 05:30:12 -08:00
symbol: '',
2022-12-15 22:13:00 -08:00
}
2022-12-15 15:33:31 -08:00
}
2023-05-05 11:13:43 -07:00
const mangoStoreState = mangoStore.getState()
const group = mangoStoreState.group
let ticker = mangoStoreState.selectedMarket.name
2023-08-06 18:54:41 -07:00
let quote_token = ''
let base_token = ''
2023-05-05 11:13:43 -07:00
2023-05-04 12:41:33 -07:00
if (group && symbolAddress) {
2023-08-06 18:54:41 -07:00
const market = getMktFromMktAddress(group, symbolAddress)
if (market) {
ticker = market.name
if (market instanceof Serum3Market) {
base_token = group
.getFirstBankByTokenIndex(market.baseTokenIndex)
.mint.toString()
quote_token = group
.getFirstBankByTokenIndex(market.quoteTokenIndex)
.mint.toString()
}
2023-05-04 18:11:37 -07:00
}
2023-05-04 12:41:33 -07:00
}
2022-12-15 15:33:31 -08:00
2023-01-02 05:30:12 -08:00
const symbolInfo: SymbolInfo = {
2023-08-06 18:54:41 -07:00
quote_token,
base_token,
2022-12-15 15:33:31 -08:00
address: symbolItem.address,
ticker: symbolItem.address,
2023-05-04 18:11:37 -07:00
name: ticker || symbolItem.address,
2022-12-19 17:09:33 -08:00
description: ticker || symbolItem.address,
2022-12-15 15:33:31 -08:00
type: symbolItem.type,
session: '24x7',
timezone: 'Etc/UTC',
minmov: 1,
pricescale: 10 ** 16,
2022-12-15 15:33:31 -08:00
has_intraday: true,
has_weekly_and_monthly: false,
2023-02-01 18:56:07 -08:00
has_empty_bars: true,
2023-01-02 11:50:03 -08:00
supported_resolutions: configurationData.supported_resolutions as any,
2022-12-15 15:33:31 -08:00
intraday_multipliers: configurationData.intraday_multipliers,
volume_precision: 2,
data_status: 'streaming',
2023-01-02 05:41:22 -08:00
full_name: '',
exchange: '',
2023-01-02 05:30:12 -08:00
listed_exchange: '',
format: 'price',
2022-12-15 15:33:31 -08:00
}
onSymbolResolvedCallback(symbolInfo)
},
getBars: async (
2023-01-02 05:30:12 -08:00
symbolInfo: SymbolInfo,
2023-01-02 11:50:03 -08:00
resolution: ResolutionString,
2023-01-02 05:30:12 -08:00
periodParams: {
countBack: number
firstDataRequest: boolean
from: number
to: number
},
onHistoryCallback: (
2023-01-02 05:41:22 -08:00
bars: Bar[],
2023-01-02 05:30:12 -08:00
t: {
noData: boolean
2023-07-21 11:47:53 -07:00
},
2023-01-02 05:30:12 -08:00
) => void,
2023-07-21 11:47:53 -07:00
onErrorCallback: (e: any) => void,
2022-12-15 15:33:31 -08:00
) => {
try {
2023-01-02 11:17:27 -08:00
const { firstDataRequest } = periodParams
2023-03-04 12:42:35 -08:00
let bars
2023-08-06 19:16:51 -07:00
if (symbolInfo.description?.includes('PERP') && symbolInfo.address) {
2023-03-04 12:42:35 -08:00
marketType = 'perp'
bars = await queryPerpBars(
symbolInfo.address,
resolution as any,
2023-07-21 11:47:53 -07:00
periodParams,
2023-03-04 12:42:35 -08:00
)
2023-08-06 19:16:51 -07:00
} else if (symbolInfo.address) {
2023-03-04 12:42:35 -08:00
marketType = 'spot'
bars = await queryBirdeyeBars(
2023-08-06 18:54:41 -07:00
symbolInfo.base_token,
2023-03-04 12:42:35 -08:00
resolution as any,
2023-07-21 11:47:53 -07:00
periodParams,
2023-08-06 18:54:41 -07:00
symbolInfo.quote_token,
symbolInfo.address,
2023-03-04 12:42:35 -08:00
)
}
2023-01-02 11:17:27 -08:00
if (!bars || bars.length === 0) {
2022-12-15 15:33:31 -08:00
// "noData" should be set if there is no data in the requested period.
onHistoryCallback([], {
noData: true,
})
return
}
if (firstDataRequest) {
lastBarsCache.set(symbolInfo.address, {
...bars[bars.length - 1],
})
}
onHistoryCallback(bars, {
noData: false,
})
return bars
2022-12-15 15:33:31 -08:00
} catch (error) {
2023-01-06 11:41:03 -08:00
console.warn('[getBars]: Get error', error)
2022-12-15 15:33:31 -08:00
onErrorCallback(error)
}
},
subscribeBars: (
2023-01-02 05:41:22 -08:00
symbolInfo: SymbolInfo,
resolution: string,
onRealtimeCallback: (data: any) => void,
subscriberUID: string,
2023-07-21 11:47:53 -07:00
onResetCacheNeededCallback: () => void,
2022-12-15 15:33:31 -08:00
) => {
subscriptionIds.set(subscriberUID, symbolInfo.address)
2023-03-04 12:42:35 -08:00
if (symbolInfo.description?.includes('PERP')) {
subscribeOnPerpStream(
symbolInfo,
resolution,
onRealtimeCallback,
subscriberUID,
onResetCacheNeededCallback,
2023-07-21 11:47:53 -07:00
lastBarsCache.get(symbolInfo.address),
2023-03-04 12:42:35 -08:00
)
} else {
subscribeOnSpotStream(
symbolInfo,
resolution,
onRealtimeCallback,
subscriberUID,
onResetCacheNeededCallback,
2023-07-21 11:47:53 -07:00
lastBarsCache.get(symbolInfo.address),
2023-03-04 12:42:35 -08:00
)
}
2022-12-15 15:33:31 -08:00
},
unsubscribeBars: (subscriberUID: string) => {
2023-03-04 12:42:35 -08:00
if (marketType === 'perp') {
unsubscribeFromPerpStream(subscriberUID)
2023-03-04 12:42:35 -08:00
} else {
2023-08-19 03:36:15 -07:00
// unsubscribeFromStream()
2023-03-04 12:42:35 -08:00
}
2022-12-15 15:33:31 -08:00
},
2023-03-04 12:42:35 -08:00
closeSocket: () => {
2023-03-04 12:42:35 -08:00
if (marketType === 'spot') {
closeSocket()
} else {
closePerpSocket()
}
},
2023-03-04 12:42:35 -08:00
name: 'birdeye',
2023-03-04 12:42:35 -08:00
// isSocketOpen: marketType === 'spot' ? isOpen : isPerpOpen,
2022-12-15 15:33:31 -08:00
}
2023-08-19 03:36:15 -07:00
export default datafeed