@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的)。那么:
因为我们不会使用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)
}
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!