symbol rework; fee % switching; symbol search improvements

This commit is contained in:
tim
2024-10-11 00:26:29 -04:00
parent 6d19adb130
commit 5b23864c2e
11 changed files with 233 additions and 178 deletions

View File

@@ -32,7 +32,6 @@ const quoteSymbols = [
'WETH',
]
let feeDropdown = null
let widget = null
// Python code to generate VARIABLE_TICK_SIZE:
@@ -42,46 +41,6 @@ let widget = null
// ' '.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: () => {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
let items
if (symbolItem===null)
items = [{title: '0.00%'}]
else {
items = symbolItem.pools.map((p)=> {
return {
title: (p[1]/10000).toFixed(2)+'%',
onSelect: ()=>selectPool(p),
}
})
}
feeDropdown.applyOptions({items})
}
function selectPool(p) {
const co = useChartOrderStore();
if ( co.selectedPool === null || co.selectedPool[0] !== p[0] || co.selectedPool[1] !== p[1]) {
co.selectedPool = p
}
}
const lastBarsCache = new Map();
// DatafeedConfiguration implementation
const configurationData = {
@@ -110,8 +69,8 @@ const configurationData = {
};
const tokenMap = {}
const poolMap = {}
const tokenMap = {} // todo needs chainId
const poolMap = {} // todo needs chainId
let _symbols = null // keyed by the concatenated hex addrs of the token pair e.g. '0xf3Ed85D882b5d9A67fC10dBf8f9AA991212983aA' + '0x6cdC5106DC100115E6C310539Fe44a61b3EEa6C4'
const indexer = new FlexSearch.Document({
@@ -121,53 +80,90 @@ 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
const feeGroups = {} // keyed by ticker without the final fee field. values are a list of pools [[addr,fee],...]
// todo add chainIds to all these keys
function addSymbol(p, base, quote, inverted) {
// The symbol key is the chain and base/quote: basically a "pair." It is only used by dexorder. The TradingView
// symbol is keyed by the `ticker` which is defined as 'chain_id|pool_addr' for absolute uniqueness.
export function tickerForOrder(chainId, order) {
const [exchange, fee] = order.route
return tickerKey(chainId, exchange, order.tokenIn, order.tokenOut, fee, true)
}
export function tickerKey(chainId, exchange, tokenAddrA, tokenAddrB, fee, chooseInversion=false ) {
if (chooseInversion && invertedDefault(tokenAddrA, tokenAddrB) )
[tokenAddrA, tokenAddrB] = [tokenAddrB, tokenAddrA]
// NOTE: the ticker key specifies a base and quote ordering, so there are two tickers per pool
return `${chainId}|${exchange}|${tokenAddrA}|${tokenAddrB}|${fee}`
}
export function feelessTickerKey(ticker) {
return ticker.split('|').slice(0, -1).join('|');
}
function addSymbol(chainId, p, base, quote, inverted) {
const symbol = base.s + quote.s
const exchange = ['UNIv2', 'UNIv3'][p.e]
const full_name = exchange + ':' + symbol // + '%' + formatFee(fee)
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])
log('integrated symbol', symbolInfo)
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 ticker = tickerKey(chainId, p.e, base.a, quote.a, p.f)
// add the search index only if this is the natural, noninverted base/quote pair
log('addSymbol', p, base, quote, inverted, ticker)
const description = `${base.n} / ${quote.n} ${(p.f/10000).toFixed(2)}%`
const type = 'swap'
const pools = [[p.a, p.f]]
const decimals = inverted ? -p.d : p.d
_symbols[key] = {
ticker: key, full_name, symbol, description,
exchange, type, inverted, base, quote, pools, decimals, x:p.x
const symbolInfo = {
key: ticker, ticker,
chainId, address: p.a, exchangeId: p.e,
full_name, symbol, description, exchange, type, inverted, base, quote, decimals, x:p.x, fee:p.f,
};
_symbols[ticker] = symbolInfo
const feelessKey = feelessTickerKey(ticker)
if (feelessKey in feeGroups) {
feeGroups[feelessKey].push([symbolInfo.address, symbolInfo.fee])
feeGroups[feelessKey].sort((a,b)=>a[1]-b[1])
}
if (defaultSymbol===null)
defaultSymbol = _symbols[key]
log('new symbol', key, _symbols[key])
indexes[key] = {
// key
id: key,
else
feeGroups[feelessKey] = [[symbolInfo.address, symbolInfo.fee]]
symbolInfo.feeGroup = feeGroups[feelessKey]
if (defaultSymbol===null && !invertedDefault(symbolInfo.base.a, symbolInfo.quote.a))
defaultSymbol = _symbols[ticker]
log('new symbol', ticker, _symbols[ticker])
}
// 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 buildSymbolIndex() {
for (const symbol of Object.values(_symbols)) {
if (invertedDefault(symbol.base.a, symbol.quote.a))
continue // don't search "upside down" pairs
const feelessKey = feelessTickerKey(symbol.ticker)
const feeGroup = feeGroups[feelessKey]
const [_addr, medianFee] = feeGroup[Math.floor((feeGroup.length-1)/2)]
if (symbol.fee !== medianFee)
continue // show the pool with the median fee by default
const ticker = symbol.ticker
const longExchange = ['Uniswap v2', 'Uniswap v3',][symbol.exchangeId]
if (ticker in indexes) {
indexes[ticker].as.push(symbol.address) // add the pool address index
} else {
indexes[ticker] = {
// key
id: ticker,
// addresses
a: symbol.address,
b: symbol.base.a,
q: symbol.quote.a,
// symbols
fn: symbol.full_name,
bs: symbol.base.s,
qs: symbol.quote.s,
e: symbol.exchange + ' ' + longExchange,
d: symbol.description,
}
}
}
}
@@ -178,7 +174,29 @@ function addSymbol(p, base, quote, inverted) {
// return str
// }
function getAllSymbols() {
function invertedDefault(tokenAddrA, tokenAddrB) {
// lower priority is more important (earlier in the list)
const a = tokenMap[tokenAddrA];
const b = tokenMap[tokenAddrB];
if (!a) {
log(`No token ${tokenAddrA} found`)
return
}
if (!b) {
log(`No token ${tokenAddrB} found`)
return
}
let basePriority = quoteSymbols.indexOf(a.s)
let quotePriority = quoteSymbols.indexOf(b.s)
if (basePriority === -1)
basePriority = Number.MAX_SAFE_INTEGER
if (quotePriority === -1)
quotePriority = Number.MAX_SAFE_INTEGER
return basePriority < quotePriority
}
export function getAllSymbols() {
if (_symbols===null) {
const chainId = useStore().chainId;
const md = metadata[chainId]
@@ -201,19 +219,10 @@ function getAllSymbols() {
log(`No token ${p.q} found`)
return
}
// 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);
addSymbol(chainId, p, quote, base, true);
addSymbol(chainId, p, base, quote, false);
})
buildSymbolIndex()
log('indexes', indexes)
Object.values(indexes).forEach(indexer.add.bind(indexer))
}
@@ -221,18 +230,24 @@ function getAllSymbols() {
return _symbols
}
export function lookupSymbol(key) { // lookup by ticker which is "0xbaseAddress/0xquoteAddress"
function invertTicker(ticker) {
const [chainId, exchange, base, quote, fee] = ticker.split('|')
return tickerKey(chainId, exchange, quote, base, fee)
}
export function lookupSymbol(ticker) { // lookup by ticker which is "0xbaseAddress/0xquoteAddress"
// todo tim lookup default base/quote pool
const symbols = getAllSymbols();
if (!(key in symbols)) {
if (!(ticker in symbols)) {
// check the inverted symbol
const [base,quote] = key.split('/')
key = quote+'/'+base
if (!(key in symbols)) {
console.error('no symbol found for key', key, symbols)
const orig = ticker
ticker = invertTicker(ticker);
if (!(ticker in symbols)) {
console.error('no symbol found for ticker', orig, symbols)
return null
}
}
return symbols[key]
return symbols[ticker]
}
function checkBar(bar, msg) {
@@ -322,9 +337,13 @@ export const DataFeed = {
const found = indexer.search(userInput, 10)
log('found', found)
const result = []
const seen = {}
for (const f of found)
for (const key of f.result)
result.push(_symbols[key])
for (const ticker of f.result)
if (!(ticker in seen)) {
result.push(_symbols[ticker])
seen[ticker] = true
}
onResultReadyCallback(result);
},
@@ -348,7 +367,6 @@ export const DataFeed = {
log('[resolveSymbol]: Method call', symbolName);
const symbols = getAllSymbols();
const symbolItem = symbolName === 'default' ? defaultSymbol : symbols[symbolName]
log('symbol resolved?', symbolItem)
if (!symbolItem) {
log('[resolveSymbol]: Cannot resolve symbol', symbolName);
onResolveErrorCallback('cannot resolve symbol');
@@ -356,19 +374,19 @@ export const DataFeed = {
}
const co = useChartOrderStore();
co.selectedSymbol = symbolItem
const pool = symbolItem.pools[0]; // choose the first-listed pool. server will adjust metadata accordingly.
const feelessKey = feelessTickerKey(symbolItem.ticker)
const symbolsByFee = feeGroups[feelessKey]
symbolsByFee.sort((a,b)=>a.fee-b.fee)
const pool = symbolsByFee[Math.floor((symbolsByFee.length - 1)/2)] // median rounded down
// noinspection JSValidateTypes
co.selectedPool = pool
updateFeeDropdown()
const priceScale = '100'
co.selectedPool = pool // todo remove
// 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)}%`,
description: symbolItem.description,
type: symbolItem.type,
session: '24x7',
timezone: 'Etc/UTC',
@@ -392,9 +410,9 @@ export const DataFeed = {
log('[getBars]: Method call', symbolInfo, resolution, from, to);
try {
// todo need to consider the selected fee tier
const [poolAddr, poolFee] = useChartOrderStore().selectedPool;
await getAllSymbols()
const symbol = lookupSymbol(symbolInfo.ticker);
const poolAddr = symbol.address
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
@@ -436,30 +454,12 @@ export const DataFeed = {
}
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
}
const symbol = getAllSymbols()[symbolInfo.ticker]
const poolAddr = symbol.address
const chainId = useStore().chainId;
const res = convertTvResolution(resolution)
const period = res.name
console.log('subscription symbol', symbol, getAllSymbols())
log('subscription symbol', symbol, chainId, poolAddr, res)
const sub = new RealtimeSubscription(chainId, poolAddr, res, symbol, subscriberUID, onRealtimeCallback, onResetCacheNeededCallback)
log('sub', sub.key)
subOHLC(chainId, poolAddr, period)