// import {subscribeOnStream, unsubscribeFromStream,} from './streaming.js'; import {jBars, tvResolutionToPeriodString} from './jBars.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"; // disable debug messages logging let console = { log: function() {} } let feeDropdown = null let widget = null // Python code to generate VARIABLE_TICK_SIZE: // from decimal import Decimal // K=12 # powers above and below to include // D=5 # number of significant digits to display // ' '.join(f'{Decimal(10)**(k-D):f} {Decimal(10)**k:f}' for k in range(-K,K+1))+f' {Decimal(10)**(K+1-D)}' const VARIABLE_TICK_SIZE = '0.00000000000000001 0.000000000001 0.0000000000000001 0.00000000001 0.000000000000001 0.0000000001 0.00000000000001 0.000000001 0.0000000000001 0.00000001 0.000000000001 0.0000001 0.00000000001 0.000001 0.0000000001 0.00001 0.000000001 0.0001 0.00000001 0.001 0.0000001 0.01 0.000001 0.1 0.00001 1 0.0001 10 0.001 100 0.01 1000 0.1 10000 1 100000 10 1000000 100 10000000 1000 100000000 10000 1000000000 100000 10000000000 1000000 100000000000 10000000 1000000000000 100000000' export function initFeeDropdown(w) { widget = w widget.createDropdown( { title: 'Fees', tooltip: 'Choose Fee Tier', items: [/*{title: 'Automatic Fee Selection', onSelect: () => {console.log('autofees')}}*/], icon: ``, } ).then(dropdown => {feeDropdown = dropdown; updateFeeDropdown()}) } function updateFeeDropdown() { if (feeDropdown===null) return const symbolItem = useChartOrderStore().selectedSymbol const feeOpts = { items: symbolItem.pools.map((p)=> { return { title: (p[1]/10000).toFixed(2)+'%', onSelect: ()=>selectPool(p), } }) } feeDropdown.applyOptions(feeOpts) } function selectPool(p) { const co = useChartOrderStore(); if ( co.selectedPool === null || co.selectedPool[0] !== p[0] || co.selectedPool[1] !== p[0]) { co.selectedPool = p } } const lastBarsCache = new Map(); // DatafeedConfiguration implementation const configurationData = { // Represents the resolutions for bars supported by your datafeed supported_resolutions: ['1', '3', '5', '10', '15', '30', '60', '120', '240', '480', '720', '1D', '2D', '3D', '1W'], // The `exchanges` arguments are used for the `searchSymbols` method if a user selects the exchange exchanges: [ // { // value: 'UNIv2', // name: 'Uniswap v2', // desc: 'Uniswap v2', // }, { value: 'UNIv3', name: 'Uniswap v3', desc: 'Uniswap v3', logo: 'https://upload.wikimedia.org/wikipedia/commons/e/e7/Uniswap_Logo.svg', }, ], // The `symbols_types` arguments are used for the `searchSymbols` method if a user selects this symbol type symbols_types: [ {name: 'swap', value: 'swap',}, ], }; const tokenMap = {} const poolMap = {} let _symbols = null 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() charset: {split: /\W+/}, tokenize: 'forward', }) const indexes = {} const symbolsSeen = {} // keyed by (base,quote) so we only list one pool per pair even if there are many fee tiers function addSymbol(p, base, quote, inverted) { 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) 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]) indexes[key].as.push(p.a) return } symbolsSeen[key] = true const longExchange = ['Uniswap v2', 'Uniswap v3',][p.e] const description = `${base.n} / ${quote.n}` const type = 'swap' const pools = [[p.a, p.f]] const decimals = p.d _symbols[key] = { ticker: key, full_name, symbol, description, exchange, type, inverted, base, quote, pools, decimals, x:p.x } if (defaultSymbol===null) defaultSymbol = _symbols[key] console.log('new symbol', key, _symbols[key]) indexes[key] = { // key id: key, // addresses as: [p.a], // multiple pool addrs for each fee tier b: p.b, q: p.q, // symbols fn: full_name, bs: base.s, qs: quote.s, e: exchange + ' ' + longExchange, d: description, } } // function formatFee(fee) { // let str = (fee / 10000).toFixed(2); // if (str.startsWith('0')) // start with the decimal point not a zero // str = str.slice(1) // return str // } async function getAllSymbols() { if (_symbols===null) { const chainId = useStore().chainId; const md = metadata[chainId] if(!md) { console.log('could not get metadata for chain', chainId) return [] } _symbols = {} for (const t of md.t) tokenMap[t.a] = t md.p.forEach((p)=>{ poolMap[p.a] = p const base = tokenMap[p.b]; const quote = tokenMap[p.q]; if (!base) { console.log(`No token ${p.b} found`) return } if (!quote) { console.log(`No token ${p.q} found`) return } addSymbol(p, base, quote, false); addSymbol(p, quote, base, true); }) console.log('indexes', indexes) Object.values(indexes).forEach(indexer.add.bind(indexer)) } console.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 } function checkBar(bar, msg) { // Everything should be positive let o_l = bar.open - bar.low let c_l = bar.close - bar.low let h_o = bar.high - bar.open let h_c = bar.high - bar.close 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) } 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) } } export const DataFeed = { onReady: (callback) => { console.log('[onReady]: Method call'); setTimeout(() => callback(configurationData)); }, searchSymbols: async ( userInput, exchange, symbolType, onResultReadyCallback, ) => { console.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) const result = [] for (const f of found) for (const key of f.result) result.push(_symbols[key]) onResultReadyCallback(result); }, resolveSymbol: async ( symbolName, onSymbolResolvedCallback, onResolveErrorCallback, extension ) => { console.log('[resolveSymbol]: Method call', symbolName); const symbols = await getAllSymbols(); const symbolItem = symbolName === 'default' ? defaultSymbol : symbols[symbolName] console.log('symbol resolved?', symbolItem) if (!symbolItem) { console.log('[resolveSymbol]: Cannot resolve symbol', symbolName); onResolveErrorCallback('cannot resolve symbol'); return; } const co = useChartOrderStore(); co.selectedSymbol = symbolItem const pool = symbolItem.pools[0]; // choose the first-listed pool. server will adjust metadata accordingly. // noinspection JSValidateTypes co.selectedPool = pool updateFeeDropdown() const priceScale = '100' // LibrarySymbolInfo // https://www.tradingview.com/charting-library-docs/latest/api/interfaces/Charting_Library.LibrarySymbolInfo const symbolInfo = { ticker: symbolItem.ticker, name: symbolItem.symbol, pro_name: symbolItem.full_name, // we add the fee to the description after the specific pool has been resolved description: symbolItem.description + ` ${(pool[1] / 10000).toFixed(2)}%`, type: symbolItem.type, session: '24x7', timezone: 'Etc/UTC', exchange: symbolItem.exchange, minmov: 0.000000000000000001, pricescale: null, variable_tick_size: VARIABLE_TICK_SIZE, has_intraday: true, // Added to allow less than one day to work visible_plots_set: 'ohlc', has_weekly_and_monthly: true, // Added to allow greater than one day to work supported_resolutions: configurationData.supported_resolutions, // volume_precision: 2, data_status: 'streaming', }; console.log('[resolveSymbol]: Symbol resolved', symbolName); onSymbolResolvedCallback(symbolInfo); }, getBars: async (symbolInfo, resolution, periodParams, onHistoryCallback, onErrorCallback) => { const { from, to, firstDataRequest } = periodParams; console.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 jBars(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); } catch (error) { console.log('[getBars]: Get error', error); onErrorCallback(error); } }, subscribeBarsOnRealtimeCallback: null, subscribeBars: ( symbolInfo, resolution, onRealtimeCallback, 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 key = `${chainId}|${poolPeriod}`; const subscriptionUIDs = subscriptionCallbacks[key]; if (subscriptionUIDs===undefined) { console.log('unsubbing abandoned subscription', poolPeriod) socket.emit('unsubOHLCs', chainId, [poolPeriod]) return } function onRealtimeCallback() { for (const subId of subscriptionUIDs) { const [_chainId, _poolAddr, _period, _onRealtimeCallback, _onResetCacheNeededCallback] = subscriptions[subId] _onRealtimeCallback(...arguments) } } function onResetCacheNeededCallback() { for (const subId of subscriptionUIDs) { const [_chainId, _poolAddr, _period, _onRealtimeCallback, _onResetCacheNeededCallback] = subscriptions[subId] _onResetCacheNeededCallback(...arguments) } } let ohlc = ohlcs.at(-1); console.log("poolCallBack ohlc:", new Date(Number(ohlc[0])*1000).toGMTString(), ohlc) for (let i = 0; i