diff --git a/src/charts/chart-misc.js b/src/charts/chart-misc.js
index 7c7bd73..bf91c8a 100644
--- a/src/charts/chart-misc.js
+++ b/src/charts/chart-misc.js
@@ -1,17 +1,24 @@
import {useChartOrderStore} from "@/orderbuild.js";
-const OHLC_START = new Date(1231027200*1000) // Sunday January 4th, 2009 just before Bitcoin Genesis
+// Sunday January 4th, 2009 just before Bitcoin Genesis
+const OHLC_START = 1231027200
-export function nearestOhlcStart(time, periodSeconds=null) {
+export function ohlcStart(timestamp, periodSeconds=null) {
if (periodSeconds===null)
periodSeconds = useChartOrderStore().intervalSecs
- return Math.round((time-OHLC_START) / periodSeconds) * periodSeconds + OHLC_START
+ return Math.floor((timestamp-OHLC_START) / periodSeconds) * periodSeconds + OHLC_START
}
-export function pointsToOhlcStart(points) {
+export function nearestOhlcStart(timestamp, periodSeconds=null) {
+ if (periodSeconds===null)
+ periodSeconds = useChartOrderStore().intervalSecs
+ return Math.round((timestamp-OHLC_START) / periodSeconds) * periodSeconds + OHLC_START
+}
+
+export function pointsToTvOhlcStart(points) {
return points === null ? null : points.map((p) => {
- return {time: nearestOhlcStart(p.time), price: p.price}
+ return {time: nearestOhlcStart(p.time/1000)*1000, price: p.price}
})
}
diff --git a/src/charts/chart.js b/src/charts/chart.js
index c056d7d..1fbf5ea 100644
--- a/src/charts/chart.js
+++ b/src/charts/chart.js
@@ -36,6 +36,7 @@ function changeSymbol(symbol) {
function changeInterval(interval, _timeframe) {
co.intervalSecs = intervalToSeconds(interval)
+ DataFeed.intervalChanged(co.intervalSecs)
}
diff --git a/src/charts/datafeed.js b/src/charts/datafeed.js
index 6f45a6c..0298f75 100644
--- a/src/charts/datafeed.js
+++ b/src/charts/datafeed.js
@@ -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: ``,
}
).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; iolder<= 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 = {}
diff --git a/src/charts/ohlc.js b/src/charts/ohlc.js
index 71b4e51..01311b7 100644
--- a/src/charts/ohlc.js
+++ b/src/charts/ohlc.js
@@ -1,5 +1,8 @@
import {useStore} from "@/store/store.js";
-import {nearestOhlcStart} from "@/charts/chart-misc.js";
+import {ohlcStart} from "@/charts/chart-misc.js";
+
+
+// support for Dexorder OHLC data files
function dailyFile(resName) {
@@ -61,27 +64,32 @@ function never(_timestamp) {
}
+// noinspection PointlessArithmeticExpressionJS
const resolutions = [
- { period: 1, tvRes: '1', filename: dailyFile( '1m'), nextStart: nextDay, },
- { period: 3, tvRes: '3', filename: dailyFile( '3m'), nextStart: nextDay, },
- { period: 5, tvRes: '5', filename: dailyFile( '5m'), nextStart: nextDay, },
- { period: 10, tvRes: '10', filename: dailyFile('10m'), nextStart: nextDay, },
- { period: 15, tvRes: '15', filename: dailyFile('15m'), nextStart: nextDay, },
- { period: 30, tvRes: '30', filename: dailyFile('30m'), nextStart: nextDay, },
- { period: 60, tvRes: '60', filename: monthlyFile( '1H'), nextStart: nextMonth, },
- { period: 120, tvRes: '120', filename: monthlyFile( '2H'), nextStart: nextMonth, },
- { period: 240, tvRes: '240', filename: monthlyFile( '4H'), nextStart: nextMonth, },
- { period: 480, tvRes: '480', filename: monthlyFile( '8H'), nextStart: nextMonth, },
- { period: 720, tvRes: '720', filename: monthlyFile('12H'), nextStart: nextMonth, },
- { period: 1440, tvRes: '1D', filename: yearlyFile( '1D'), nextStart: nextYear, },
- { period: 2880, tvRes: '2D', filename: yearlyFile( '2D'), nextStart: nextYear, },
- { period: 4320, tvRes: '3D', filename: yearlyFile( '3D'), nextStart: nextYear, },
- { period: 10080, tvRes: '1W', filename: singleFile( '1W'), nextStart: never, },
+ { seconds: 1 * 60, name: '1m', tvRes: '1', filename: dailyFile( '1m'), nextStart: nextDay, },
+ { seconds: 3 * 60, name: '3m', tvRes: '3', filename: dailyFile( '3m'), nextStart: nextDay, },
+ { seconds: 5 * 60, name: '5m', tvRes: '5', filename: dailyFile( '5m'), nextStart: nextDay, },
+ { seconds: 10 * 60, name: '10m', tvRes: '10', filename: dailyFile('10m'), nextStart: nextDay, },
+ { seconds: 15 * 60, name: '15m', tvRes: '15', filename: dailyFile('15m'), nextStart: nextDay, },
+ { seconds: 30 * 60, name: '30m', tvRes: '30', filename: dailyFile('30m'), nextStart: nextDay, },
+ { seconds: 60 * 60, name: '1H', tvRes: '60', filename: monthlyFile( '1H'), nextStart: nextMonth, },
+ { seconds: 120 * 60, name: '2H', tvRes: '120', filename: monthlyFile( '2H'), nextStart: nextMonth, },
+ { seconds: 240 * 60, name: '4H', tvRes: '240', filename: monthlyFile( '4H'), nextStart: nextMonth, },
+ { seconds: 480 * 60, name: '8H', tvRes: '480', filename: monthlyFile( '8H'), nextStart: nextMonth, },
+ { seconds: 720 * 60, name: '12H', tvRes: '720', filename: monthlyFile('12H'), nextStart: nextMonth, },
+ { seconds: 1440 * 60, name: '1D', tvRes: '1D', filename: yearlyFile( '1D'), nextStart: nextYear, },
+ { seconds: 2880 * 60, name: '2D', tvRes: '2D', filename: yearlyFile( '2D'), nextStart: nextYear, },
+ { seconds: 4320 * 60, name: '3D', tvRes: '3D', filename: yearlyFile( '3D'), nextStart: nextYear, },
+ { seconds: 10080 * 60, name: '1W', tvRes: '1W', filename: singleFile( '1W'), nextStart: never, },
]
-const resMap = {}
+const tvResMap = {}
for (const res of resolutions)
- resMap[res.tvRes] = res
+ tvResMap[res.tvRes] = res
+
+const dxoResMap = {}
+for (const res of resolutions)
+ dxoResMap[res.name] = res
const seriesStarts = {}
@@ -107,15 +115,19 @@ export async function loadOHLC (symbol, contract, from, to, tvRes) {
// console.log('loadOHLC', tvRes, new Date(1000*from), new Date(1000*to), symbol, contract);
let chainId
let bars = [];
- let inverted = symbol.inverted;
+ let inverted = false;
let baseURL
let latest = null // latest time, price
function fill(end, period) {
if (latest===null) return
const [start, price] = latest
- for (let now=nearestOhlcStart(start, period*60); now < end; now += period )
+ const periodSecs = period * 60
+ end = ohlcStart(end, periodSecs)
+ for (let now=ohlcStart(start+periodSecs, periodSecs); now < end; now += periodSecs ) {
bars.push({time:now * 1000, open:price, high:price, low:price, close:price})
+ latest = [now, price]
+ }
}
if (symbol.x?.data) {
@@ -130,7 +142,7 @@ export async function loadOHLC (symbol, contract, from, to, tvRes) {
}
baseURL += `${chainId}/${contract}/`
- const res = resMap[tvRes]
+ const res = tvResMap[tvRes]
const fetches = []
let start = from
if (!(baseURL in seriesStarts)) {
@@ -164,7 +176,6 @@ export async function loadOHLC (symbol, contract, from, to, tvRes) {
let lineNum = 0
response.split('\n').forEach((line) => {
lineNum++
- // console.log(`processing line ${lineNum}`, line)
const row = line.split(',')
let time, open, high, low, close=null
switch (row.length) {
@@ -204,8 +215,9 @@ export async function loadOHLC (symbol, contract, from, to, tvRes) {
close = parseFloat(row[4])
if (inverted) {
open = 1/open
- high = 1/high
- low = 1/low
+ const h = high
+ high = 1/low
+ low = 1/h
close = 1/close
}
break
@@ -214,10 +226,8 @@ export async function loadOHLC (symbol, contract, from, to, tvRes) {
break
}
if (close!==null) {
- // console.log(`filling up to ${time}`)
- fill(time, res.period)
+ fill(time, res.seconds)
const bar = {time:time*1000, open, high, low, close};
- // console.log('pushing bar', bar)
bars.push(bar)
latest = [time, close]
}
@@ -226,20 +236,21 @@ export async function loadOHLC (symbol, contract, from, to, tvRes) {
}
// else { console.log('response was empty') }
}
-
- fill(to, res.period)
-
- // 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?
-
- let noData = bars.length === 0;
- // if (noData) console.log("noData == true!");
- // console.log('bars', bars)
- return [bars, {noData}];
+ fill(to, res.seconds)
+ return bars
+}
+
+
+export function tvResolutionToPeriodString(res) {
+ return tvResMap[res].name
+}
+
+
+export function convertTvResolution(res) {
+ return tvResMap[res]
+}
+
+
+export function resForName(name) {
+ return dxoResMap[name]
}
diff --git a/src/charts/shape.js b/src/charts/shape.js
index c40bfc4..88a225d 100644
--- a/src/charts/shape.js
+++ b/src/charts/shape.js
@@ -5,7 +5,7 @@ import {chart, createShape, deleteShapeId, dragging, draggingShapeIds, drawShape
import {unique} from "@/misc.js";
import {allocationText} from "@/orderbuild.js";
import Color from "color";
-import {pointsToOhlcStart} from "@/charts/chart-misc.js";
+import {pointsToTvOhlcStart} from "@/charts/chart-misc.js";
//
@@ -202,7 +202,7 @@ export class Shape {
// createShape(this.type, this.points, {overrides:this.props}, new ShapeTVCallbacks(this))
options = {...options}
options['overrides'] = props
- this.tvPoints = pointsToOhlcStart(points)
+ this.tvPoints = pointsToTvOhlcStart(points)
this.tvCallbacks = new ShapeTVCallbacks(this);
const id = createShape(this.type, this.tvPoints, options, this.tvCallbacks)
// todo set id?
@@ -247,7 +247,7 @@ export class Shape {
if (this.id === null)
this.create()
else {
- points = pointsToOhlcStart(points)
+ points = pointsToTvOhlcStart(points)
if (dirtyPoints(this.tvPoints, points)) {
const s = this.tvShape();
const lbe = s._model._lineBeingEdited
diff --git a/src/charts/streaming.js b/src/charts/streaming.js
deleted file mode 100644
index 6aaccef..0000000
--- a/src/charts/streaming.js
+++ /dev/null
@@ -1,172 +0,0 @@
-// import { parseFullSymbol } from './helpers.js';
-// import { subPrices } from '@/blockchain/prices.js';
-
-// const socket = io('wss://streamer.cryptocompare.com');
-// const channelToSubscription = new Map();
-
-// socket.on('connect', () => {
-// console.log('[socket] Connected');
-// });
-
-// socket.on('disconnect', (reason) => {
-// console.log('[socket] Disconnected:', reason);
-// });
-
-// socket.on('error', (error) => {
-// console.log('[socket] Error:', error);
-// });
-
-// socket.on('m', data => {
-// console.log('[socket] Message:', data);
-// const [
-// eventTypeStr,
-// exchange,
-// fromSymbol,
-// toSymbol,
-// ,
-// ,
-// tradeTimeStr,
-// ,
-// tradePriceStr,
-// ] = data.split('~');
-
-// if (parseInt(eventTypeStr) !== 0) {
-// // Skip all non-trading events
-// return;
-// }
-// const tradePrice = parseFloat(tradePriceStr);
-// const tradeTime = parseInt(tradeTimeStr);
-// const channelString = `0~${exchange}~${fromSymbol}~${toSymbol}`;
-// const subscriptionItem = channelToSubscription.get(channelString);
-// if (subscriptionItem === undefined) {
-// return;
-// }
-// const lastDailyBar = subscriptionItem.lastDailyBar;
-// const nextDailyBarTime = getNextDailyBarTime(lastDailyBar.time, subscriptionItem.resolution);
-
-// console.log("tradeTime ", tradeTime, new Date(tradeTime))
-// console.log("lastDailyBar.time", lastDailyBar.time, new Date(lastDailyBar.time))
-// console.log("nextDailyBarTime ", nextDailyBarTime, new Date(nextDailyBarTime))
-
-// let bar;
-// if (tradeTime >= nextDailyBarTime) {
-// bar = {
-// time: nextDailyBarTime,
-// open: tradePrice,
-// high: tradePrice,
-// low: tradePrice,
-// close: tradePrice,
-// };
-// console.log('[socket] Generate new bar', bar);
-// console.log("time:", bar.time.toString(), new Date(bar.time).toUTCString())
-// } else {
-// bar = {
-// ...lastDailyBar,
-// high: Math.max(lastDailyBar.high, tradePrice),
-// low: Math.min(lastDailyBar.low, tradePrice),
-// close: tradePrice,
-// };
-// console.log('[socket] Update the latest bar by price', tradePrice);
-// }
-// subscriptionItem.lastDailyBar = bar;
-
-// // Send data to every subscriber of that symbol
-// subscriptionItem.handlers.forEach(handler => handler.callback(bar));
-// });
-
-// function getNextDailyBarTime(barTime, res) {
-// const date = new Date(barTime);
-// const resDigits = res.slice(0, -1)
-// if (res.endsWith("W")) {
-// date.setDate(date.getDate() + parseInt(resDigits)*7);
-// } else if (res.endsWith("D")) {
-// date.setDate(date.getDate() + parseInt(resDigits));
-// } else {
-// date.setMinutes(date.getMinutes() + parseInt(res))
-// }
-// return date.getTime();
-// }
-
-// export function subscribeOnStream(
-// symbolInfo,
-// resolution,
-// onRealtimeCallback,
-// subscriberUID,
-// onResetCacheNeededCallback,
-// lastDailyBar,
-// ) {
-// // return;
-// const parsedSymbol = parseFullSymbol(symbolInfo.full_name);
-// const channelString = `0~${parsedSymbol.exchange}~${parsedSymbol.fromSymbol}~${parsedSymbol.toSymbol}`;
-// const handler = {
-// id: subscriberUID,
-// callback: onRealtimeCallback,
-// };
-// let subscriptionItem = channelToSubscription.get(channelString);
-// if (subscriptionItem) {
-// // Already subscribed to the channel, use the existing subscription
-// subscriptionItem.handlers.push(handler);
-// return;
-// }
-// subscriptionItem = {
-// subscriberUID,
-// resolution,
-// lastDailyBar,
-// handlers: [handler],
-// };
-// channelToSubscription.set(channelString, subscriptionItem);
-// console.log('[subscribeBars]: Subscribe to streaming. Channel:', channelString);
-// socket.emit('SubAdd', { subs: [channelString] });
-// }
-
-// export function unsubscribeFromStream(subscriberUID) {
-// // return;
-// // Find a subscription with id === subscriberUID
-// for (const channelString of channelToSubscription.keys()) {
-// const subscriptionItem = channelToSubscription.get(channelString);
-// const handlerIndex = subscriptionItem.handlers
-// .findIndex(handler => handler.id === subscriberUID);
-
-// if (handlerIndex !== -1) {
-// // Remove from handlers
-// subscriptionItem.handlers.splice(handlerIndex, 1);
-
-// if (subscriptionItem.handlers.length === 0) {
-// // Unsubscribe from the channel if it was the last handler
-// console.log('[unsubscribeBars]: Unsubscribe from streaming. Channel:', channelString);
-// socket.emit('SubRemove', { subs: [channelString] });
-// channelToSubscription.delete(channelString);
-// break;
-// }
-// }
-// }
-// }
-
-// function sim() {
-// // Assuming these variables hold the data you extracted earlier
-// const eventTypeStr = "0";
-// const exchange = "Uniswap";
-// const fromSymbol = "WETH";
-// const toSymbol = "USD";
-// const tradeTimeStr = (Date.now()).toString();
-// const tradePriceStr = (55+Date.now()%23).toString();
-
-// // Constructing the original string
-// const data = [
-// eventTypeStr,
-// exchange,
-// fromSymbol,
-// toSymbol,
-// '', // Placeholder for the fifth element
-// '', // Placeholder for the sixth element
-// tradeTimeStr,
-// '', // Placeholder for the eighth element
-// tradePriceStr,
-// ].join('~');
-// socket._callbacks['$m'][0](data);
-// }
-
-// window.sim = sim;
-// socket._callbacks['$connect'][0]();
-// setInterval(sim, 10*1000);
-;
diff --git a/src/socket.js b/src/socket.js
index dd73dc3..e9ef064 100644
--- a/src/socket.js
+++ b/src/socket.js
@@ -15,7 +15,7 @@ socket.on('disconnect', () => {
})
socket.on('p', async (chainId, pool, price) => {
- // console.log('pool price from message', chainId, pool, price)
+ console.log('pool price from message', chainId, pool, price)
const s = useStore()
if( s.chainId !== chainId )
return
@@ -27,7 +27,7 @@ socket.on('ohlc', async (chainId, poolPeriod, ohlcs) => {
if (ohlcs && ohlcs.length) {
const split = poolPeriod.indexOf('|')
const pool = poolPeriod.slice(0,split)
- useStore().poolPrices[[chainId, pool]] = ohlcs[ohlcs.length - 1][4] // closing price
+ useStore().poolPrices[[chainId, pool]] = parseFloat(ohlcs[ohlcs.length - 1][4]) // closing price
}
DataFeed.poolCallback(chainId, poolPeriod, ohlcs)
})