tv DataFeed rework

This commit is contained in:
tim
2024-08-28 18:57:58 -04:00
parent 55397c2b1c
commit 79a6822fd5
7 changed files with 468 additions and 403 deletions

View File

@@ -1,15 +1,36 @@
// import {subscribeOnStream, unsubscribeFromStream,} from './streaming.js';
import {loadOHLC} from './ohlc.js';
import {convertTvResolution, loadOHLC} from './ohlc.js';
import {metadata} from "@/version.js";
import FlexSearch from "flexsearch";
import {useChartOrderStore} from "@/orderbuild.js";
import {useStore} from "@/store/store.js";
import {subOHLC, unsubOHLC} from "@/blockchain/ohlcs.js";
import {socket} from "@/socket.js";
import {ohlcStart} from "@/charts/chart-misc.js";
import {timestamp} from "@/misc.js";
// disable debug messages logging
let console = { log: function() {} }
const DEBUG_LOGGING = false
const log = DEBUG_LOGGING ? console.log : ()=>{}
// this file manages connecting data to TradingView using their DataFeed API.
// https://www.tradingview.com/charting-library-docs/latest/connecting_data/Datafeed-API/#integrate-datafeed-api
// see ohlc.js for fetching dexorder ohlc files
// in order of priority
const quoteSymbols = [
'USDT',
'USDC',
'TUSD',
'GUSD',
'BUSD',
'MUSD',
'DAI',
'CRVUSD',
'EURC',
'EURS',
'EURI',
'WBTC',
'WETH',
]
let feeDropdown = null
let widget = null
@@ -27,7 +48,7 @@ export function initFeeDropdown(w) {
{
title: 'Fees',
tooltip: 'Choose Fee Tier',
items: [/*{title: 'Automatic Fee Selection', onSelect: () => {console.log('autofees')}}*/],
items: [/*{title: 'Automatic Fee Selection', onSelect: () => {log('autofees')}}*/],
icon: `<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28"><g fill="none" stroke="currentColor"><circle cx="10" cy="10" r="2.5"/><circle cx="18" cy="18" r="2.5"/><path stroke-linecap="square" d="M17.5 7.5l-7 13"/></g></svg>`,
}
).then(dropdown => {feeDropdown = dropdown; updateFeeDropdown()})
@@ -54,7 +75,7 @@ function updateFeeDropdown() {
function selectPool(p) {
const co = useChartOrderStore();
if ( co.selectedPool === null || co.selectedPool[0] !== p[0] || co.selectedPool[1] !== p[0]) {
if ( co.selectedPool === null || co.selectedPool[0] !== p[0] || co.selectedPool[1] !== p[1]) {
co.selectedPool = p
}
}
@@ -91,7 +112,7 @@ const configurationData = {
const tokenMap = {}
const poolMap = {}
let _symbols = null
let _symbols = null // keyed by the concatenated hex addrs of the token pair e.g. '0xf3Ed85D882b5d9A67fC10dBf8f9AA991212983aA' + '0x6cdC5106DC100115E6C310539Fe44a61b3EEa6C4'
const indexer = new FlexSearch.Document({
document: {id: 'id', index: ['fn', 'as[]', 'b', 'q', 'bs', 'qs', 'e', 'd']}, // this must match what is generated for the index object in addSymbol()
@@ -102,18 +123,19 @@ const indexer = new FlexSearch.Document({
const indexes = {}
const symbolsSeen = {} // keyed by (base,quote) so we only list one pool per pair even if there are many fee tiers
// todo add chainIds to all these keys
function addSymbol(p, base, quote, inverted) {
const symbol = base.s + '/' + quote.s
const symbol = base.s + quote.s
const exchange = ['UNIv2', 'UNIv3'][p.e]
const full_name = exchange + ':' + symbol // + '%' + formatFee(fee)
let key = `${base.a}${quote.a}`
console.log('addSymbol', p, base, quote, inverted, key)
const key = `${base.a}/${quote.a}`
log('addSymbol', p, base, quote, inverted, key)
if (symbolsSeen[key]) {
// add this pool's address to the existing symbol as an additional fee tier
const symbolInfo = _symbols[key];
symbolInfo.pools.push([p.a, p.f])
symbolInfo.pools.sort((a,b)=>a[1]-b[1])
console.log('integrated symbol', _symbols[key])
log('integrated symbol', symbolInfo)
indexes[key].as.push(p.a)
return
}
@@ -129,7 +151,7 @@ function addSymbol(p, base, quote, inverted) {
}
if (defaultSymbol===null)
defaultSymbol = _symbols[key]
console.log('new symbol', key, _symbols[key])
log('new symbol', key, _symbols[key])
indexes[key] = {
// key
id: key,
@@ -156,12 +178,12 @@ function addSymbol(p, base, quote, inverted) {
// return str
// }
async function getAllSymbols() {
function getAllSymbols() {
if (_symbols===null) {
const chainId = useStore().chainId;
const md = metadata[chainId]
if(!md) {
console.log('could not get metadata for chain', chainId)
log('could not get metadata for chain', chainId)
return []
}
_symbols = {}
@@ -172,47 +194,35 @@ async function getAllSymbols() {
const base = tokenMap[p.b];
const quote = tokenMap[p.q];
if (!base) {
console.log(`No token ${p.b} found`)
log(`No token ${p.b} found`)
return
}
if (!quote) {
console.log(`No token ${p.q} found`)
log(`No token ${p.q} found`)
return
}
addSymbol(p, base, quote, false);
addSymbol(p, quote, base, true);
// todo check quotes symbol list for inversion hint
let basePriority = quoteSymbols.indexOf(base.s)
let quotePriority = quoteSymbols.indexOf(quote.s)
if (basePriority === -1)
basePriority = Number.MAX_SAFE_INTEGER
if (quotePriority === -1)
quotePriority = Number.MAX_SAFE_INTEGER
const showInverted = basePriority < quotePriority
if (showInverted)
addSymbol(p, quote, base, true);
else
addSymbol(p, base, quote, false);
})
console.log('indexes', indexes)
log('indexes', indexes)
Object.values(indexes).forEach(indexer.add.bind(indexer))
}
console.log('symbols', _symbols)
log('symbols', _symbols)
return _symbols
}
export function lookupSymbol(key) { // lookup by fullname
return _symbols[key]
}
export function lookupBaseQuote(baseAddr, quoteAddr) {
return _symbols[`${baseAddr}${quoteAddr}`]
}
function poolIsInverted() {
return useChartOrderStore().selectedSymbol.inverted
}
export function maybeInvertBar (bar) {
if (poolIsInverted()) {
bar.open = 1/bar.open
let high = bar.high
bar.high = 1/bar.low
bar.low = 1/high
bar.close = 1/bar.close
console.log("bar inverted")
} else {
console.log("bar NOT inverted")
}
return bar
return getAllSymbols()[key]
}
function checkBar(bar, msg) {
@@ -226,39 +236,81 @@ function checkBar(bar, msg) {
let h_l = bar.high - bar.low
if (o_l<0||c_l<0||h_o<0||h_c<0||h_l<0) {
console.log(msg, "bar.high/low inconsistent:", bar)
if (o_l<0) console.log("bar inconsistent: open-low: ", o_l)
if (c_l<0) console.log("bar inconsistent: close-low: ", c_l)
if (h_o<0) console.log("bar inconsistent: high-open: ", h_o)
if (h_c<0) console.log("bar inconsistent: high-close:", h_c)
if (h_l<0) console.log("bar inconsistent: high-low: ", h_l)
log(msg, "bar.high/low inconsistent:", bar)
if (o_l<0) log("bar inconsistent: open-low: ", o_l)
if (c_l<0) log("bar inconsistent: close-low: ", c_l)
if (h_o<0) log("bar inconsistent: high-open: ", h_o)
if (h_c<0) log("bar inconsistent: high-close:", h_c)
if (h_l<0) log("bar inconsistent: high-low: ", h_l)
} else {
console.log(msg, "bar diffs:", bar)
console.log("bar diff: open-low: ", o_l)
console.log("bar diff: close-low: ", c_l)
console.log("bar diff: high-open: ", h_o)
console.log("bar diff: high-close:", h_c)
console.log("bar diff: high-low: ", h_l)
log(msg, "bar diffs:", bar)
log("bar diff: open-low: ", o_l)
log("bar diff: close-low: ", c_l)
log("bar diff: high-open: ", h_o)
log("bar diff: high-close:", h_c)
log("bar diff: high-low: ", h_l)
}
}
function invertOhlcs(bars) {
const result = []
for (const bar of bars) {
const h = bar.high
result.push({
time: bar.time,
open: 1/bar.open,
high: 1/bar.low,
low: 1/h,
close: 1/bar.close,
})
}
return result
}
const subByTvSubId = {}
const subByKey = {}
class RealtimeSubscription {
constructor(chainId, poolAddr, res, symbol, tvSubId, onRealtimeCb, onResetCacheCb ) {
this.chainId = chainId
this.poolAddr = poolAddr
this.res = res
this.symbol = symbol
this.tvSubId = tvSubId
this.onRealtimeCb = onRealtimeCb
this.onResetCacheCb = onResetCacheCb
this.key = `${chainId}|${poolAddr}|${res.name}`
subByTvSubId[this.tvSubId] = this
subByKey[this.key] = this
}
close() {
delete subByTvSubId[this.tvSubId]
delete subByKey[this.key]
}
}
export const DataFeed = {
onReady: (callback) => {
console.log('[onReady]: Method call');
onReady(callback) {
log('[onReady]: Method call');
setTimeout(() => callback(configurationData));
},
searchSymbols: async (
async searchSymbols(
userInput,
exchange,
symbolType,
onResultReadyCallback,
) => {
console.log('[searchSymbols]: Method call');
) {
log('[searchSymbols]: Method call');
// todo limit results by exchange. use a separate indexer per exchange?
const found = indexer.search(userInput, 10)
console.log('found', found)
log('found', found)
const result = []
for (const f of found)
for (const key of f.result)
@@ -266,18 +318,29 @@ export const DataFeed = {
onResultReadyCallback(result);
},
resolveSymbol: async (
async resolveSymbol(
symbolName,
onSymbolResolvedCallback,
onResolveErrorCallback,
extension
) => {
console.log('[resolveSymbol]: Method call', symbolName);
const symbols = await getAllSymbols();
) {
setTimeout(async ()=>
await this.doResolveSymbol(symbolName,onSymbolResolvedCallback,onResolveErrorCallback,extension),
0)
},
async doResolveSymbol(
symbolName,
onSymbolResolvedCallback,
onResolveErrorCallback,
extension
) {
log('[resolveSymbol]: Method call', symbolName);
const symbols = getAllSymbols();
const symbolItem = symbolName === 'default' ? defaultSymbol : symbols[symbolName]
console.log('symbol resolved?', symbolItem)
log('symbol resolved?', symbolItem)
if (!symbolItem) {
console.log('[resolveSymbol]: Cannot resolve symbol', symbolName);
log('[resolveSymbol]: Cannot resolve symbol', symbolName);
onResolveErrorCallback('cannot resolve symbol');
return;
}
@@ -310,143 +373,298 @@ export const DataFeed = {
// volume_precision: 2,
data_status: 'streaming',
};
console.log('[resolveSymbol]: Symbol resolved', symbolName);
onSymbolResolvedCallback(symbolInfo);
log('[resolveSymbol]: Symbol resolved', symbolName);
onSymbolResolvedCallback(symbolInfo)
},
getBars: async (symbolInfo, resolution, periodParams, onHistoryCallback, onErrorCallback) => {
async getBars(symbolInfo, resolution, periodParams, onHistoryCallback, onErrorCallback) {
const { from, to, firstDataRequest } = periodParams;
console.log('[getBars]: Method call', symbolInfo, resolution, from, to);
log('[getBars]: Method call', symbolInfo, resolution, from, to);
try {
// todo need to consider the selected fee tier
let bars, metadata;
const pool = useChartOrderStore().selectedPool;
[bars, metadata] = await loadOHLC(lookupSymbol(symbolInfo.ticker), pool[0], from, to, resolution); // This is the one that does all the work
if (firstDataRequest) {
lastBarsCache.set(symbolInfo.full_name, {
...bars[bars.length - 1],
});
}
console.log(`[getBars]: returned ${bars.length} bar(s), and metadata ${metadata}`);
console.log('[getBars]: bars=', bars);
onHistoryCallback(bars, metadata);
const [poolAddr, poolFee] = useChartOrderStore().selectedPool;
await getAllSymbols()
const symbol = lookupSymbol(symbolInfo.ticker);
const res = convertTvResolution(resolution)
const key = `${useStore().chainId}|${poolAddr}|${res.name}`
let bars = await loadOHLC(symbol, poolAddr, from, to, resolution); // This is the one that does all the work
this.updatePushedCache(key, bars)
if (symbol.inverted)
bars = invertOhlcs(bars)
log(`[getBars]: returned ${bars.length} bar(s), and metadata ${metadata}`);
log('[getBars]: bars=', bars);
// noData should be set only if no bars are in the requested period and earlier.
// In our case, we are guaranteed to have contiguous samples.
// So we only return no bars (bars.length==0) if:
// 1. period is entirely before first data available.
// 2. period is entirely after last data available.
// Returning noData based on bars.length works perfectly assuming that TV never asks for case 2.
// This is probably not a safe assumption. The alternative would be to search
// backward to find beginning of history. How far to search?
const noData = bars.length === 0;
onHistoryCallback(bars, {noData});
} catch (error) {
console.log('[getBars]: Get error', error);
log('[getBars]: Get error', error);
onErrorCallback(error);
}
},
subscribeBarsOnRealtimeCallback: null,
subscribeBars: (
subscribeBars(
symbolInfo,
resolution,
onRealtimeCallback,
onRealtimeCallback2,
subscriberUID,
onResetCacheNeededCallback,
) => {
console.log('[subscribeBars]', symbolInfo, resolution, subscriberUID);
const chainId = useStore().chainId;
const poolAddr = useChartOrderStore().selectedPool[0];
const period = tvResolutionToPeriodString(resolution);
subscriptions[subscriberUID] = [chainId, poolAddr, period, onRealtimeCallback, onResetCacheNeededCallback]
const key = `${chainId}|${poolAddr}|${period}`
if (key in subscriptionCallbacks)
subscriptionCallbacks[key].push(subscriberUID)
else
subscriptionCallbacks[key] = [subscriberUID]
console.log('sub', key)
subOHLC(chainId, poolAddr, period)
},
) {
unsubscribeBars: (subscriberUID) => {
console.log('[unsubscribeBars]', subscriberUID);
const [chainId, poolAddr, period] = subscriptions[subscriberUID]
unsubOHLC(chainId, poolAddr, period)
delete subscriptions[subscriberUID]
const key = `${chainId}|${poolAddr}|${period}`;
console.log('unsub',key)
const remainingSubs = subscriptionCallbacks[key].filter((v)=>v!==subscriberUID)
if (remainingSubs.length===0)
delete subscriptionCallbacks[key]
else
subscriptionCallbacks[key] = remainingSubs
},
poolCallbackState : {lastBar:{'chain|pool|period':null}},
poolCallback(chainId, poolPeriod, ohlcs) {
console.log("poolCallback: chainId, pool, ohlcs:", chainId, poolPeriod, ohlcs)
if (ohlcs == null) {
console.log("poolCallback: ohlcs == null, nothing to do.")
return;
const oldCb = onRealtimeCallback2
function onRealtimeCallback() {
log('push bar', ...arguments)
oldCb(...arguments)
}
const key = `${chainId}|${poolPeriod}`;
const subscriptionUIDs = subscriptionCallbacks[key];
if (subscriptionUIDs===undefined) {
console.log('unsubbing abandoned subscription', poolPeriod)
socket.emit('unsubOHLCs', chainId, [poolPeriod])
log('[subscribeBars]', symbolInfo, resolution, subscriberUID);
const symbol = getAllSymbols()[symbolInfo.full_name]
const co = useChartOrderStore()
// todo redo symbolInfo: the full_name passed to TV should just be the pool addr. when searching a symbol, go ahead and select the first/most liquid automatically
let poolAddr = null
const selectedFee = co.selectedPool[1];
for (const [addr, fee] of symbol.pools) {
if (fee === selectedFee) {
poolAddr = addr
break
}
}
if (poolAddr===null) {
console.error(`Could not find pool for fee ${selectedFee}: ${symbol.pools}`)
return
}
function onRealtimeCallback() {
for (const subId of subscriptionUIDs) {
const [_chainId, _poolAddr, _period, _onRealtimeCallback, _onResetCacheNeededCallback] = subscriptions[subId]
_onRealtimeCallback(...arguments)
const chainId = useStore().chainId;
const res = convertTvResolution(resolution)
const period = res.name
console.log('subscription symbol', symbol, getAllSymbols())
const sub = new RealtimeSubscription(chainId, poolAddr, res, symbol, subscriberUID, onRealtimeCallback, onResetCacheNeededCallback)
log('sub', sub.key)
subOHLC(chainId, poolAddr, period)
this.startOHLCBumper()
},
unsubscribeBars(subscriberUID) {
log('[unsubscribeBars]', subscriberUID);
const sub = subByTvSubId[subscriberUID]
if (sub===undefined) {
console.log(`warning: no subscription found for tvSubId ${subscriberUID}`)
return
}
unsubOHLC(sub.chainId, sub.poolAddr, sub.res.name)
log('unsub',sub.key)
sub.close()
delete this.pushedBars[sub.key]
delete this.recentBars[sub.key]
},
// key-value format:
// 'chain|pool|period': [[javatime, open, high, low, close], ...]
pushedBars: {}, // bars actually sent to TradingView
recentBars:{}, // bars received from server notifications
pushRecentBars(key, recent) {
this.recentBars[key] = recent
const historical = this.pushedBars[key]
if (historical!==undefined)
this.overlapAndPush(key, recent)
},
overlapAndPush(key, ohlcs) {
log('overlapAndPush',key,ohlcs)
if (!ohlcs || ohlcs.length === 0) return
const sub = subByKey[key]
// do not check reorgs on mock symbols because the dev environment price will be different from the main chain
// price and reorgs would happen every time.
const checkReorg = sub.symbol.x?.data === undefined
const res = sub.res
const period = res.seconds * 1000
const bars = [] // to push
const pushed = this.pushedBars[key]
log('got pushed bars', pushed)
let pi = 0 // pushed index
let time
let price
if (pushed === undefined) {
time = ohlcs[0].time
price = ohlcs[0].close
}
else {
const last = pushed.length - 1
time = pushed[last].time
price = pushed[last].close
}
log('last time/price', time, price)
for( const ohlc of ohlcs ) {
log('handle ohlc', ohlc)
// forward the pi index to at least the current ohlc time
while( pi < pushed.length && pushed[pi].time < ohlc.time ) pi++
if (pi < pushed.length-1) {
// finalized bars must match the previous push exactly or else there was a reorg and we need to reset
const p = pushed[pi]
log('check reorg', checkReorg, pi, p, ohlc)
if (checkReorg &&
(p.time !== ohlc.time || p.open !== ohlc.open || p.high !== ohlc.high ||
p.low !== ohlc.low || p.close !== ohlc.close) )
{
console.log('RESET TV CACHE')
return this.resetCache(key)
}
}
else {
// the last pushed bar and anything after it can be sent to TV
while (time + period < ohlc.time) {
// fill gap
time += period
const bar = {time, open: price, high: price, low: price, close: price};
log('fill', bar)
bars.push(bar)
}
bars.push(ohlc)
time = ohlc.time
price = ohlc.close
}
}
return this.pushToTV(key, bars)
},
function onResetCacheNeededCallback() {
for (const subId of subscriptionUIDs) {
const [_chainId, _poolAddr, _period, _onRealtimeCallback, _onResetCacheNeededCallback] = subscriptions[subId]
_onResetCacheNeededCallback(...arguments)
}
resetCache(key) {
log('resetting TV data cache')
const sub = subByKey[key]
if (sub===undefined) {
console.log(`warning: no subscription found for dexorder key ${key}`)
return
}
sub.onResetCacheCb()
},
let ohlc = ohlcs.at(-1);
console.log("poolCallBack ohlc:", new Date(Number(ohlc[0])*1000).toGMTString(), ohlc)
for (let i = 0; i<ohlc.length; i++) if (ohlc[i]!=null) ohlc[i] = Number(ohlc[i])
// for (const ohlc of ohlcs) {
let date = new Date(ohlc[0]*1000)
let close = ohlc[4] // close
let bar = {
time: date.getTime(),
open: ohlc[1] ?? close, // open
high: ohlc[2] ?? close, // high
low: ohlc[3] ?? close, // low
updatePushedCache(key, ohlcs) {
log('updatePushedCache', key, ohlcs)
if (!(key in this.pushedBars))
this.pushedBars[key] = ohlcs
else {
const prev = this.pushedBars[key]
log('prev pushed', prev)
const endTime = prev[prev.length - 1].time;
if (endTime === ohlcs[0].time)
// the new ohlc's overlap the old time, so exclude the most recent historical item, which gets replaced
this.pushedBars[key] = [...prev.slice(0, -1), ...ohlcs]
else if (endTime <= ohlcs[ohlcs.length-1].time) {
// no overlap of any bars. full append.
this.pushedBars[key] = [...prev, ...ohlcs]
}
// otherwise the ohlc's being pushed are =>older<= than the ones in the pushedBars cache. This happens
// because TV can request data pages (using getBars()) out of chronological order.
}
},
pushToTV(key, ohlcs) {
if (ohlcs.length===0) return
log('pushing bars to tv', ohlcs)
this.updatePushedCache(key, ohlcs); // we cache the raw bars before inversion so they match dexorder data sources
const sub = subByKey[key]
if (sub===undefined) {
console.log(`warning: could not find subscription for dexorder sub key ${key}`)
return
}
if (sub.symbol.inverted)
ohlcs = invertOhlcs(ohlcs)
for (const ohlc of ohlcs)
sub.onRealtimeCb(ohlc)
},
poolCallback(chainId, poolPeriod, ohlcs) {
const key = `${chainId}|${poolPeriod}`;
const bars = []
for (const ohlc of ohlcs) {
let close = parseFloat(ohlc[4]) // close
const bar = {
time: ohlc[0] * 1000,
open: ohlc[1] ? parseFloat(ohlc[1]) : close, // open
high: ohlc[2] ? parseFloat(ohlc[2]) : close, // high
low: ohlc[3] ? parseFloat(ohlc[3]) : close, // low
close: close,
}
checkBar(bar, "poolCallback, before inversion:")
bar = maybeInvertBar(bar)
checkBar(bar, "poolCallback, after inversion:")
console.log('DataFeed.poolCallback', date.toGMTString(), ohlcs, bar)
let lastBar = DataFeed.poolCallbackState.lastBar[key]
// No last bar then initialize bar
if (lastBar===undefined) {
console.log('DataFeed.poolCallback', new Date(bar.time).toGMTString(), 'lastBar=', bar)
onRealtimeCallback(bar)
DataFeed.poolCallbackState.lastBar[key] = bar
bars.push(bar)
}
return this.pushRecentBars(key, bars)
},
intervalChanged(seconds) {
// rollover bumper
// this timer function creates a new bar when the period rolls over
this.startOHLCBumper()
},
// The OHLC bumper advances bars at the end of the period. If the price doesn't change, no new data will arrive, so the
// new bar is implicit and must be generated dynamically.
startOHLCBumper() {
const co = useChartOrderStore()
if (_rolloverBumper !== null)
clearTimeout(_rolloverBumper)
const period = co.intervalSecs;
if (period === 0)
return
const now = Date.now()
const nextRollover = ohlcStart(now/1000 + period)*1000 + 2000 // two second delay to wait for server data
const delay = nextRollover - now
_rolloverBumper = setTimeout(this.bumpOHLC.bind(this), delay)
},
bumpOHLC() {
log('bumpOHLC')
_rolloverBumper = null
const secs = useChartOrderStore().intervalSecs;
for (const sub of Object.values(subByKey)) {
log('check bump', sub.res.seconds, secs)
if (sub.res.seconds === secs) {
const pushed = this.pushedBars[sub.key]
log('check pushed', pushed)
if (pushed !== undefined && pushed.length > 0) {
const lastBar = pushed[pushed.length - 1]
const price = lastBar.close
const now = timestamp() * 1000
const period = sub.res.seconds * 1000
const fills = []
for( let time=lastBar.time + period; time < now; time += period ) {
log('pushing bump', time, price)
const bar = {time, open: price, high: price, low: price, close: price}
fills.push(bar)
}
this.pushToTV(sub.key, fills)
}
else {
log('warning: bumpOHLC() found no previous bars')
}
}
// bar time is less than last bar then ignore
else if (bar.time < lastBar.time ) {
}
// bar time equal to last bar then replace last bar
else if (bar.time === lastBar.time ) {
console.log('DataFeed.poolCallback', new Date(bar.time).toGMTString(), 'lastBar=', bar)
if (bar.high < lastBar.high) console.log("bar.high < lastBar.high (lastbar=)")
if (bar.low > lastBar.low) console.log("bar.low > lastBar.low (lastbar=)")
onRealtimeCallback(bar)
DataFeed.poolCallbackState.lastBar[key] = bar
}
// new bar, then render last and replace last bar
else {
console.log('DataFeed.poolCallback', new Date(bar.time).toGMTString(), 'lastBar=', bar)
onRealtimeCallback(bar)
DataFeed.poolCallbackState.lastBar[key] = bar
}
// }
}
}
this.startOHLCBumper()
},
}
let _rolloverBumper = null
let defaultSymbol = null
const subscriptions = {}
const subscriptionCallbacks = {}