Merge pull request #78 from blockworks-foundation/pan/candles

Add basic perp ohlcv feed
This commit is contained in:
tylersssss 2023-02-10 11:11:28 -05:00 committed by GitHub
commit d0e937b559
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 403 additions and 24 deletions

View File

@ -157,6 +157,7 @@ export default {
pricescale: 100,
has_intraday: true,
has_weekly_and_monthly: false,
has_empty_bars: true,
supported_resolutions: configurationData.supported_resolutions as any,
intraday_multipliers: configurationData.intraday_multipliers,
volume_precision: 2,

246
apis/mngo/datafeed.ts Normal file
View File

@ -0,0 +1,246 @@
import { makeApiRequest, parseResolution } from './helpers'
import { subscribeOnStream, unsubscribeFromStream } from './streaming'
import mangoStore from '@store/mangoStore'
import {
DatafeedConfiguration,
LibrarySymbolInfo,
ResolutionString,
SearchSymbolResultItem,
} from '@public/charting_library'
export const SUPPORTED_RESOLUTIONS = [
'1',
'3',
'5',
'15',
'30',
'60',
'120',
'240',
'1D',
] as const
type BaseBar = {
low: number
high: number
open: number
close: number
}
type KlineBar = BaseBar & {
volume: number
timestamp: number
}
type TradingViewBar = BaseBar & {
time: number
}
type Bar = KlineBar & TradingViewBar
type SymbolInfo = LibrarySymbolInfo & {
address: string
}
const lastBarsCache = new Map()
const configurationData = {
supported_resolutions: SUPPORTED_RESOLUTIONS,
intraday_multipliers: [
'1',
'3',
'5',
'15',
'30',
'45',
'60',
'120',
'240',
'1440',
],
exchanges: [],
}
// async function getAllSymbols() {
// const data = await makeApiRequest(
// 'public/tokenlist?sort_by=v24hUSD&sort_type=desc&offset=0&limit=-1'
// )
// return data.data.tokens
// }
export const queryBars = async (
tokenAddress: string,
resolution: typeof SUPPORTED_RESOLUTIONS[number],
periodParams: {
firstDataRequest: boolean
from: number
to: number
}
): Promise<Bar[]> => {
const { from, to } = periodParams
const urlParameters = {
'perp-market': tokenAddress,
resolution: parseResolution(resolution),
start_datetime: new Date(from * 1000).toISOString(),
end_datetime: new Date(to * 1000).toISOString(),
}
const query = Object.keys(urlParameters)
.map((name: string) => `${name}=${(urlParameters as any)[name]}`)
.join('&')
const data = await makeApiRequest(`/stats/candles-perp?${query}`)
if (!data || !data.length) {
return []
}
let bars: Bar[] = []
for (const bar of data) {
const timestamp = new Date(bar.candle_start).getTime()
if (timestamp >= from * 1000 && timestamp < to * 1000) {
bars = [
...bars,
{
time: timestamp,
low: bar.low,
high: bar.high,
open: bar.open,
close: bar.close,
volume: bar.volume,
timestamp,
},
]
}
}
return bars
}
export default {
onReady: (callback: (configuration: DatafeedConfiguration) => void) => {
setTimeout(() => callback(configurationData as any))
},
searchSymbols: async (
_userInput: string,
_exchange: string,
_symbolType: string,
_onResultReadyCallback: (items: SearchSymbolResultItem[]) => void
) => {
return
},
resolveSymbol: async (
symbolAddress: string,
onSymbolResolvedCallback: (symbolInfo: SymbolInfo) => void
// _onResolveErrorCallback: any,
// _extension: any
) => {
let symbolItem:
| {
address: string
type: string
symbol: string
}
| undefined
if (!symbolItem) {
symbolItem = {
address: symbolAddress,
type: 'pair',
symbol: '',
}
}
const ticker = mangoStore.getState().selectedMarket.name
const symbolInfo: SymbolInfo = {
address: symbolItem.address,
ticker: symbolItem.address,
name: symbolItem.symbol || symbolItem.address,
description: ticker || symbolItem.address,
type: symbolItem.type,
session: '24x7',
timezone: 'Etc/UTC',
minmov: 1,
pricescale: 100,
has_intraday: true,
has_weekly_and_monthly: false,
has_empty_bars: true,
supported_resolutions: configurationData.supported_resolutions as any,
intraday_multipliers: configurationData.intraday_multipliers,
volume_precision: 2,
data_status: 'streaming',
full_name: '',
exchange: '',
listed_exchange: '',
format: 'price',
}
onSymbolResolvedCallback(symbolInfo)
},
getBars: async (
symbolInfo: SymbolInfo,
resolution: ResolutionString,
periodParams: {
countBack: number
firstDataRequest: boolean
from: number
to: number
},
onHistoryCallback: (
bars: Bar[],
t: {
noData: boolean
}
) => void,
onErrorCallback: (e: any) => void
) => {
try {
const { firstDataRequest } = periodParams
const bars = await queryBars(
symbolInfo.address,
resolution as any,
periodParams
)
if (!bars || bars.length === 0) {
// "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,
})
} catch (error) {
console.warn('[getBars]: Get error', error)
onErrorCallback(error)
}
},
subscribeBars: (
symbolInfo: SymbolInfo,
resolution: string,
onRealtimeCallback: (data: any) => void,
subscriberUID: string,
onResetCacheNeededCallback: () => void
) => {
subscribeOnStream(
symbolInfo,
resolution,
onRealtimeCallback,
subscriberUID,
onResetCacheNeededCallback,
lastBarsCache.get(symbolInfo.address)
)
},
unsubscribeBars: () => {
console.warn('[unsubscribeBars]')
unsubscribeFromStream()
},
}

55
apis/mngo/helpers.ts Normal file
View File

@ -0,0 +1,55 @@
import { MANGO_DATA_API_URL } from 'utils/constants'
// Make requests to mngo.cloud API
export async function makeApiRequest(path: string) {
try {
const response = await fetch(`${MANGO_DATA_API_URL}${path}`)
return response.json()
} catch (error: any) {
throw new Error(`mngo.cloud request error: ${error.status}`)
}
}
const RESOLUTION_MAPPING: Record<string, string> = {
'1': '1',
'3': '3',
'5': '5',
'15': '15',
'30': '30',
'45': '45',
'60': '60',
'120': '120',
'240': '240',
'1D': '1440',
'1W': '10080',
}
export function parseResolution(resolution: string) {
if (!resolution || !RESOLUTION_MAPPING[resolution])
return RESOLUTION_MAPPING[0]
return RESOLUTION_MAPPING[resolution]
}
export function getNextBarTime(lastBar: any, resolution = '1D') {
if (!lastBar) return
const lastCharacter = resolution.slice(-1)
let nextBarTime
switch (true) {
case lastCharacter === 'W':
nextBarTime = 7 * 24 * 60 * 60 * 1000 + lastBar.time
break
case lastCharacter === 'D':
nextBarTime = 1 * 24 * 60 * 60 * 1000 + lastBar.time
break
default:
nextBarTime = 1 * 60 * 1000 + lastBar.time
break
}
return nextBarTime
}

80
apis/mngo/streaming.ts Normal file
View File

@ -0,0 +1,80 @@
import { parseResolution, getNextBarTime } from './helpers'
let subscriptionItem: any = {}
// Create WebSocket connection.
const socket = new WebSocket(`wss://api.mngo.cloud/fills/v1/`)
// Connection opened
socket.addEventListener('open', (_event) => {
console.log('[socket] Connected')
})
// Listen for messages
socket.addEventListener('message', (msg) => {
const data = JSON.parse(msg.data)
if (!data.event) return console.warn(data)
if (data.event.maker) return
const currTime = new Date(data.event.timestamp).getTime()
const lastBar = subscriptionItem.lastBar
const resolution = subscriptionItem.resolution
const nextBarTime = getNextBarTime(lastBar, resolution)
const price = data.event.price
const size = data.event.quantity
let bar
if (currTime >= nextBarTime) {
bar = {
time: nextBarTime,
open: price,
high: price,
low: price,
close: price,
volume: size,
}
} else {
bar = {
...lastBar,
high: Math.max(lastBar.high, price),
low: Math.min(lastBar.low, price),
close: price,
volume: lastBar.volume + size,
}
}
subscriptionItem.lastBar = bar
subscriptionItem.callback(bar)
})
export function subscribeOnStream(
symbolInfo: any,
resolution: any,
onRealtimeCallback: any,
subscriberUID: any,
onResetCacheNeededCallback: any,
lastBar: any
) {
subscriptionItem = {
resolution,
lastBar,
callback: onRealtimeCallback,
}
const msg = {
command: 'subscribe',
marketId: 'HwhVGkfsSQ9JSQeQYu2CbkRCLvsh3qRZxG6m4oMVwZpN',
}
socket.send(JSON.stringify(msg))
}
export function unsubscribeFromStream() {
const msg = {
command: 'unsubscribe',
marketId: 'HwhVGkfsSQ9JSQeQYu2CbkRCLvsh3qRZxG6m4oMVwZpN',
}
socket.send(JSON.stringify(msg))
}

View File

@ -11,7 +11,8 @@ import { useViewport } from 'hooks/useViewport'
import { CHART_DATA_FEED, DEFAULT_MARKET_NAME } from 'utils/constants'
import { breakpoints } from 'utils/theme'
import { COLORS } from 'styles/colors'
import Datafeed from 'apis/birdeye/datafeed'
import SpotDatafeed from 'apis/birdeye/datafeed'
import PerpDatafeed from 'apis/mngo/datafeed'
export interface ChartContainerProps {
container: ChartingLibraryWidgetOptions['container']
@ -93,20 +94,26 @@ const TradingViewChart = () => {
}
})
const selectedMarket = useMemo(() => {
const group = mangoStore.getState().group
if (!group || !selectedMarketName)
return '8BnEgHoWFysVcuFFX7QztDmzuH8r5ZFvyP3sYwn1XTh6'
if (!selectedMarketName.toLowerCase().includes('perp')) {
return group
.getSerum3MarketByName(selectedMarketName)
.serumMarketExternal.toString()
} else {
return group.getPerpMarketByName(selectedMarketName).publicKey.toString()
}
}, [selectedMarketName])
useEffect(() => {
const group = mangoStore.getState().group
if (tvWidgetRef.current && chartReady && selectedMarketName && group) {
if (tvWidgetRef.current && chartReady && selectedMarket && group) {
try {
let symbolName
if (!selectedMarketName.toLowerCase().includes('PERP')) {
symbolName = group
.getSerum3MarketByName(selectedMarketName)
.serumMarketExternal.toString()
} else {
symbolName = selectedMarketName
}
tvWidgetRef.current.setSymbol(
symbolName,
selectedMarket,
tvWidgetRef.current.activeChart().resolution(),
() => {
return
@ -116,7 +123,7 @@ const TradingViewChart = () => {
console.warn('Trading View change symbol error: ', e)
}
}
}, [selectedMarketName, chartReady])
}, [selectedMarket, chartReady])
useEffect(() => {
if (
@ -134,22 +141,12 @@ const TradingViewChart = () => {
useEffect(() => {
if (window) {
// const tempBtcDatafeedUrl = 'https://dex-pyth-price-mainnet.zeta.markets/tv/history?symbol=BTC-USDC&resolution=5&from=1674427748&to=1674430748&countback=2'
const tempBtcDatafeedUrl =
'https://redirect-origin.mangomarkets.workers.dev'
const btcDatafeed = new (window as any).Datafeeds.UDFCompatibleDatafeed(
tempBtcDatafeedUrl
)
const widgetOptions: ChartingLibraryWidgetOptions = {
// debug: true,
symbol:
spotOrPerp === 'spot'
? '8BnEgHoWFysVcuFFX7QztDmzuH8r5ZFvyP3sYwn1XTh6'
: 'BTC-USDC',
symbol: selectedMarket,
// BEWARE: no trailing slash is expected in feed URL
// tslint:disable-next-line:no-any
datafeed: spotOrPerp === 'spot' ? Datafeed : btcDatafeed,
datafeed: spotOrPerp === 'spot' ? SpotDatafeed : PerpDatafeed,
interval:
defaultProps.interval as ChartingLibraryWidgetOptions['interval'],
container: