像中心化交易所一样,拉取uniswap v3的市场行情(市场深度)

  • 刘军
  • 更新于 2024-02-01 13:00
  • 阅读 1395

@uniswap/v3-sdk里面的pool对象,提供了getOutputAmount函数,但是要求pool里面包含tick,这样我们想知道给定的输入能活得多少输出,就不需要提交到节点执行了,自己本机的nodejs就能运算出来。大大降低节点的压力。设池中有xy两种币。问题1:用a量的x能换来b量

@uniswap/v3-sdk里面的pool对象,提供了getOutputAmount函数,但是要求pool里面包含tick,这样我们想知道给定的输入能获得多少输出,就不需要提交到节点执行了,自己本机的nodejs就能运算出来。大大降低节点的压力。况且,QuoterV2.sol的询价,只能给定一个输入并获得一个输出。不能在本次询价导致价格改变的基础上,再提交下一个询价

设池中有xy两种币。 问题1:用a量的x能换来b量的y(a和b的单位是聪或者伟),请问b是多少? 答案是:如果没有手续费,就是b=ay/(a+x); 如果有手续费f,就是b=(1-f)ay/(x+(1-f)a)

问题2:用x币换y币,会导致x的价格下降,请问用多少x换y,才能导致x的执行价下降比例是r?也就是给定r,求a(单位是聪或者伟). 备注:x的初始价p0= y/x, x的执行价p1= b/a= (1-f)y/(x+(1-f)a),执行后的价格p2= (y-b)/(a+x)= y/(x+a) - ay/(a+x)^2 【注意:计算出来的a,只是能保证当前价跟执行价之间的关系是r,不能保证这个执行价,跟下一个执行价之间的关系还是r。但是各个执行价之间的差距,没必要都是r,所以就用本次计算出来的a作为每次的inputAmount. 因为双曲线斜率越来越小,所以这会导致r越来越小。要想r恒定,则a需要越来越大】 答案是:由r= 1- p1/p0,得出 a= x[1/(1-r) - 1/(1-f)]. 因为a>0 ,所以r>f

问题3: 假设要拉取一百个买单和一百个卖单,需要获取多少个tick数据? 分析:如果挂单价格递增0.1%, 100个挂单会引起10.5%的价格波动。如果挂单价格递增0.3%,100个挂单会引起35%的价格波动。结合问题1、问题2来看,如果费率f=0.3%, r=f+ 0.2% = 0.5%,一百个挂单会引起65%的价格波动 . 所以我们最多处理65%的价格波动就行。那么65%的价格波动,涉及到多少个tick呢?解方程1.0001**n = 1.65,得n=log1.0001(1.65)= log(1.65)/log(1.0001)= 4984. TickLen.getPopulatedTicksInWord函数,一次最多能返回一个字节(256个)的“被填充过的有效tick”(只返回被填充过的,而不是全部有效tick。有效tick是指tickNumber % TICK_SPACING =0的)。那么:

  • 当FeeAmount=100时,费率=0.01%,TICK_SPACING=1,对该函数调用19.46次就能返回4984个有效tick.
  • 当FeeAmount=500时,费率=0.05%,TICK_SPACING=10,对该函数调用1.94次就能返回498.4个有效tick(涉及4984个).
  • 当FeeAmount=3000时,费率=0.3%,TICK_SPACING=60,对该函数调用0.46次就能返回83个有效tick(涉及4984个).
  • 当FeeAmount=10000时,费率=1%,TICK_SPACING=200,对该函数调用0.097次就能返回25个有效tick(涉及4984个).

因为我们不会使用0.01%费率的池子,也就不会出现TICK_SPACING=1的情况。所以除了获取当前字节,还要获取左边2字节和右边2字节。共调用TickLen.getPopulatedTicksInWord函数的次数:1+2+2=5次。每调用一次,返回的数据量有点大。 *所以答案就是:最多5字节的tick,也就是`5256=1280`个tick**.

整个程序代码如下:

export async function bookProduct(coinPair, marketOrderSize, orderStepRatio, poolFee) {

    const [goods, money] = coinPair.toLowerCase().split("-")
    const [goodsToken, moneyToken] = [tokens[goods].wrapped, tokens[money].wrapped]
    assert(goodsToken && moneyToken, "token 不存在:" + [goods, money])
    let bids, asks;
    try {
        let pool = await creatPoolWithticksFromPool(await getPool(provider, goodsToken, moneyToken, poolFee, false))

        //用卖的办法(输入goods),模拟出市场买单。然后我可以提交卖单吃掉这些市场买单。
        bids = await createMarketOrder(pool, goodsToken, moneyToken, marketOrderSize, orderStepRatio, goodsToken, poolFee)
        //用买的办法(输入money),模拟出市场卖单。然后我可以提交买单吃掉这些市场卖单。
        asks = await createMarketOrder(pool, moneyToken, goodsToken, marketOrderSize, orderStepRatio, goodsToken, poolFee)
    } catch (e) {
        console.error(new Date().toLocaleString() + ' bookProduct异常:', e.stack || e)
        throw e
    }
    return {asks, bids}
}

/**
 * 辅助方法。利用v2的恒定乘积原理,生成市场挂单。
 * 注意:涉及到 x * y=k, x * sqrt(PriceX) =L, pool.liquidity, pool.sqrtRatioX96这些内容的,数据单位一律是聪、伟这些最小单位。
 * @param pool {Pool}
 * @param inputToken {Token}
 * @param outputToken {Token}
 * @param marketOrderSize
 * @param r {Number} 价格下降比例。也就是orderStepRatio.
 * @param goodsToken {Token}
 * @param poolFee {Number} 整数表示的费率。 500表示百万分之500,也就是0.0005,也就是0.05%.
 * @return orderArr {[[string,string]]}
 */

async function createMarketOrder(pool, inputToken, outputToken, marketOrderSize, r, goodsToken, poolFee) {
    //模拟生成市场挂单
    //计算输入的x币的数量a (a是Big类型,单位是聪或者伟。)
    let f = poolFeeToNumber(poolFee)
    assert(r > f, 'r必须大于手续费f. a= x/(1-r) - x/(1-f).  因为a>0 ,所以r>f')

    let outputAmountArr = []
    let inputAmountArr = []
    for (let i = 0, tmpPool = pool; i < marketOrderSize; i++) {
        let inputAmount = getInputAmount(tmpPool, inputToken, r, f)
        if (Number(inputAmount.toFixed()) === 0) {//如果市场深度太小
            break;
        }
        ;[outputAmountArr[i], tmpPool] = await tmpPool.getOutputAmount(inputAmount)
        inputAmountArr[i] = inputAmount
    }
    //开始计算市场挂单
    let orderArr = [] //存储模拟出来的市场挂单
    let isSell = goodsToken.equals(inputToken)//如果是用商品换钱(卖单)
    for (let i in outputAmountArr) {
        let [goodsAmount, moneyAmount] = isSell ? [inputAmountArr[i], outputAmountArr[i]] : [outputAmountArr[i], inputAmountArr[i]]

        let price = new Price({baseAmount: goodsAmount, quoteAmount: moneyAmount}).toFixed(9)
        let amount = goodsAmount.toFixed(goodsAmount.currency.decimals)
        orderArr.push([price, amount])
    }
    return orderArr
}

/**
 * 辅助createMarketOrder方法,计算inputAmount
 * @param pool {Pool}
 * @param inputToken {Token}
 * @param r {Number} 价格下降比例
 * @param f {Number} 手续费费率
 * @return {CurrencyAmount<*>} inputAmount
 */
function getInputAmount(pool, inputToken, r, f) {
    //x币的总数量(Big类型,单位是聪、伟等最小单元)
    let inputTokenTotalRawAmount = getTokenAmount(pool.liquidity, pool.sqrtRatioX96, pool.token0.equals(inputToken))
    //a是Big类型,单位是聪或者伟等最小单元
    const bigA = inputTokenTotalRawAmount.times(1 / (1 - r) - 1 / (1 - f))
    return CurrencyAmount.fromRawAmount(inputToken, bigA.toFixed(0))
}

/**
 * 根据流动性、价格开方,计算资金池中某种币的数量(单位是聪、伟等最小单元).
 * x数量= L / sqrtP1,  y数量= L/sqrtP2 = L * sqrtP1
 * @param liquidity {JSBI} 流动性
 * @param sqrtPriceX96 {JSBI} token0的价格开根号
 * @param isToken0Amount {boolean} 是计算token0的数量吗。
 * @return {Big} 币数量,单位是聪、伟等最小单元
 */
export function getTokenAmount(liquidity, sqrtPriceX96, isToken0Amount) {
    let sqrtPrice = X96ToBig(sqrtPriceX96)
    //是计算token0的数量吗。如果不是,就用乘法.因为:x数量= L / sqrtP1,  y数量= L/sqrtP2 = L * sqrtP1
    if (isToken0Amount) {
        return Big(liquidity.toString()).div(sqrtPrice)
    } else {
        return Big(liquidity.toString()).times(sqrtPrice)
    }
}

/**
 * 把x96形式的定点数转化成浮点数Big,例如X96, X128.
 * 转化的方式为:(x96/ 2**96)
 * @param x96Value {JSBI} x96形式的值
 * @param k {Number} 小数部分占多少位?通常有 96或128
 * @return {Big} 浮点数.
 */
export function X96ToBig(x96Value, k = 96) {

    return Big(x96Value.toString()).div(Big(2).pow(k))
}

最关键的是函数是creatPoolWithticksFromPool,它能向你给定的pool填充足够的tick, 代码如下:

//查询tick,并创建新pool对象
let queryTicksCount = 0 //查询Ticks多少次了。每查询10次,才真正的查询一次。其它时候,直接获取缓存
let tickDataProviderCache = null
let bytesCache = 0 //除了获取当前字节,还应该左右各获取多少字节?
let addressCache = ''
let currentByteIndexCache = 0

/**
 * 根据旧pool,创建新pool,并填充足够数量的tick。
 * tick在bitmap中是按从小到大的顺序排列的。但是TickLen.getPopulatedTicksInWord返回的却是倒序的,需要再颠倒过来。
 * @param pool {Pool} 现有的pool
 * @return {Promise<Pool>} 包含足够ticks的新pool
 */
export async function creatPoolWithticksFromPool(pool) {
    let bytes = pool.fee === 500 ? 2 : 1
    /*
    每查询10次tick,才真正的从网上查询一次,其它时候直接获取缓存。
    缓存会失效吗?【会的】。1.记住currentTick所在字节,如果接下来它变到其它字节,就应该让缓存失效。
    2.做市商对自己position的改动,也会导致缓存失效。假设30秒内,所有的position不会有改动,那么就能认为这期间缓存是有效的。
    */
    const space = 3 //每3秒调用一次本函数,每9秒真的去网上查一次,平时就用缓存应付。因为tick数据就算不能实时同步,也只是影响到市场挂单量,而不会影响到挂单价格
    let address = Pool.getAddress(pool.token0, pool.token1, pool.fee, null, uniswapV3Factory)
    let currentByteIndex = (pool.tickCurrent / pool.tickSpacing) >> 8 //currentTick所在字节

    //如果cache失效(space能被除尽,或者价格变动导致currentByteIndex发生变化,或者pool地址发生变化)
    if (queryTicksCount++ % space === 0 || currentByteIndex !== currentByteIndexCache || addressCache !== address) {
        let promiseArr = []
        promiseArr.push(getPopulatedTicksInWord(address, currentByteIndex))
        for (let i = 0; i < bytes; i++) {
            promiseArr.push(getPopulatedTicksInWord(address, currentByteIndex + i + 1), getPopulatedTicksInWord(address, currentByteIndex - i - 1))
        }//end for
        let sortedTickArr = (await Promise.all(promiseArr)).flat().sort((tick1, tick2) => tick1.tick - tick2.tick)
        //智能合约返回的tick格式是 {tick,liquidityNet,liquidityGross},因此要转换成v3-sdk里面的Tick格式 {index,liquidityNet,liquidityGross}
        let tickArr = []
        sortedTickArr.forEach(tick => tickArr.push(new Tick({
            index: tick.tick,
            liquidityGross: tick.liquidityGross,
            liquidityNet: tick.liquidityNet
        })))
        console.log(new Date().toLocaleString() + `: call tickLensContract ${promiseArr.length} times, tick总量${tickArr.length}`)
        tickDataProviderCache = new MyTickListDataProvider(tickArr, pool.tickSpacing)
        bytesCache = bytes
        addressCache = address
        currentByteIndexCache = currentByteIndex
    }
    return new Pool(pool.token0, pool.token1, pool.fee, pool.sqrtRatioX96, pool.liquidity, pool.tickCurrent, tickDataProviderCache)
}

/**
 * 调用tickLens合约。返回的tick数组是倒序的
 * @param address pool的地址
 * @param byteIndex 字节编号
 * @return {Promise<{tick,liquidityNet,liquidityGross}[]>} ticks
 */
async function getPopulatedTicksInWord(address, byteIndex) {
    const tickLensContract = new Contract(tickLens, TickLensABI.abi, provider)
    return await tickLensContract.getPopulatedTicksInWord(address, byteIndex)
}
点赞 2
收藏 4
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

7 条评论

请先 登录 后评论
刘军
刘军
0x8e24...21f5
自由职业者