310 lines
8.7 KiB
JavaScript
310 lines
8.7 KiB
JavaScript
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 {useOrderStore, useStore} from "@/store/store.js";
|
|
import {subOHLC, unsubOHLC} from "@/blockchain/ohlcs.js";
|
|
|
|
let feeDropdown = null
|
|
let widget = null
|
|
|
|
export function initFeeDropdown(w) {
|
|
widget = w
|
|
widget.createDropdown(
|
|
{
|
|
title: 'Fees',
|
|
tooltip: 'Choose Fee Tier',
|
|
items: [/*{title: 'Automatic Fee Selection', onSelect: () => {console.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()})
|
|
}
|
|
|
|
|
|
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) {
|
|
// console.log('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}`
|
|
let found = symbolsSeen[key]
|
|
if (!found) {
|
|
key = `${quote.a}${base.a}`
|
|
found = symbolsSeen[key]
|
|
}
|
|
if (found) {
|
|
// 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])
|
|
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]]
|
|
_symbols[key] = {
|
|
full_name: key, symbol, ticker: full_name, description,
|
|
exchange, type, inverted, base, quote, pools, x:p.x
|
|
}
|
|
if (defaultSymbol===null)
|
|
defaultSymbol = _symbols[key]
|
|
console.log('added 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,
|
|
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) {
|
|
_symbols = {}
|
|
for (const t of metadata.t)
|
|
tokenMap[t.a] = t
|
|
metadata.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) {
|
|
return _symbols[key]
|
|
}
|
|
|
|
export function lookupBaseQuote(baseAddr, quoteAddr) {
|
|
|
|
return _symbols[key]
|
|
}
|
|
|
|
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();
|
|
const os = useOrderStore()
|
|
co.selectedSymbol = symbolItem
|
|
const pool = symbolItem.pools[Math.trunc(symbolItem.pools.length/2)];
|
|
// noinspection JSValidateTypes
|
|
co.selectedPool = pool
|
|
updateFeeDropdown()
|
|
// Symbol information object
|
|
const symbolInfo = {
|
|
ticker: symbolItem.full_name,
|
|
name: symbolItem.symbol,
|
|
description: symbolItem.description + ` ${(pool[1]/10000).toFixed(2)}%`,
|
|
type: symbolItem.type,
|
|
session: '24x7',
|
|
timezone: 'Etc/UTC',
|
|
exchange: symbolItem.exchange,
|
|
minmov: 1,
|
|
pricescale: 100,
|
|
has_intraday: true, // Added to allow less than one day to work
|
|
// has_no_volume: true, // deprecated
|
|
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(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(bars);
|
|
onHistoryCallback(bars, metadata);
|
|
} catch (error) {
|
|
console.log('[getBars]: Get error', error);
|
|
onErrorCallback(error);
|
|
}
|
|
},
|
|
|
|
subscribeBars: (
|
|
symbolInfo,
|
|
resolution,
|
|
onRealtimeCallback,
|
|
subscriberUID,
|
|
onResetCacheNeededCallback,
|
|
) => {
|
|
console.log('[subscribeBars]: Method call with subscriberUID:', subscriberUID);
|
|
const chainId = useStore().chainId;
|
|
const poolAddr = useChartOrderStore().selectedPool[0];
|
|
const period = tvResolutionToPeriodString(resolution);
|
|
subscriptions[subscriberUID] = [chainId, poolAddr, period]
|
|
subOHLC(chainId, poolAddr, period)
|
|
return; // disable
|
|
subscribeOnStream(
|
|
symbolInfo,
|
|
resolution,
|
|
onRealtimeCallback,
|
|
subscriberUID,
|
|
onResetCacheNeededCallback,
|
|
lastBarsCache.get(symbolInfo.full_name),
|
|
);
|
|
},
|
|
|
|
unsubscribeBars: (subscriberUID) => {
|
|
console.log('[unsubscribeBars]: Method call with subscriberUID:', subscriberUID);
|
|
const [chainId, poolAddr, period] = subscriptions[subscriberUID]
|
|
delete subscriptions[subscriberUID]
|
|
unsubOHLC(chainId, poolAddr, period)
|
|
return; // disable
|
|
unsubscribeFromStream(subscriberUID);
|
|
},
|
|
};
|
|
|
|
let defaultSymbol = null
|
|
const subscriptions = {}
|