2023-02-25 16:22:39 -08:00
|
|
|
/* 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'
|
2023-02-15 14:17:58 -08:00
|
|
|
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()
|
|
|
|
|
2023-03-10 07:49:06 -08:00
|
|
|
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[] = []
|
2023-03-06 12:15:21 -08:00
|
|
|
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,
|
2023-08-18 15:34:00 -07:00
|
|
|
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-02-08 17:40:34 -08:00
|
|
|
|
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,
|
2023-03-31 13:29:06 -07:00
|
|
|
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,
|
2023-08-18 15:34:00 -07:00
|
|
|
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,
|
|
|
|
})
|
2023-02-15 14:17:58 -08:00
|
|
|
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
|
|
|
) => {
|
2023-03-10 07:49:06 -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
|
|
|
},
|
|
|
|
|
2023-03-10 07:49:06 -08:00
|
|
|
unsubscribeBars: (subscriberUID: string) => {
|
2023-03-04 12:42:35 -08:00
|
|
|
if (marketType === 'perp') {
|
2023-04-25 13:03:09 -07:00
|
|
|
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
|
|
|
|
2023-02-15 14:17:58 -08:00
|
|
|
closeSocket: () => {
|
2023-03-04 12:42:35 -08:00
|
|
|
if (marketType === 'spot') {
|
|
|
|
closeSocket()
|
|
|
|
} else {
|
|
|
|
closePerpSocket()
|
|
|
|
}
|
2023-02-15 14:17:58 -08:00
|
|
|
},
|
2023-03-04 12:42:35 -08:00
|
|
|
|
2023-02-15 14:17:58 -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
|