You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何通过Uniswap V3子图计算指定池各tick单代币TVL及占比

Uniswap V3流动性分布参考

Uniswap V3 单tick TVL计算方案与偏差修复

核心需求

通过Uniswap V3 Subgraph API计算指定交易池每个tick中两种代币各自的总锁仓价值(TVL),以及各tick TVL占全池总TVL的比例。

现有实现进展

当前通过GraphQL分页拉取指定池的所有tick数据及对应liquidityNet字段,查询语句如下:

var query = "{ticks(first:1000,orderBy:tickIdx,orderDirection:asc,where:{tickIdx_gt:" + lastTickIdx +",poolAddress:\""+id+"\"}){tickIdx liquidityNet}}";

其中lastTickIdx为已拉取的最大tickIdx,作为分页游标使用。

初始计算思路为:从当前tick(currentTick)对应的池全局流动性出发,向价格高低两个方向遍历所有tick,逐tick调整流动性数值,再通过价格公式计算每个tick锁定的代币数量。使用的基础计算公式如下,其中p0、p1为相邻tick对应的价格点,d0、d1为两种代币的精度:

// 计算tick锁定的token0数量
tvl00 = (Math.Sqrt(p0) - Math.Sqrt(p1)) * loopLiquidity * Math.Pow(10, 0 - d0);
// 计算tick锁定的token1数量
tvl11 = tvl00 * (Math.Pow(1.0001, curr)) * Math.Pow(10, d0 - d1);

以currentTick=5为例,初始设计的流动性迭代规则为:

2  -> loopLiquidity = tick3处的loopLiquidity  - tick2的LiquidityNet 
3  -> loopLiquidity = tick4处的loopLiquidity  - tick3的LiquidityNet
4  -> loopLiquidity = tick5处的loopLiquidity  - tick4的LiquidityNet
currentTick 5  -> loopLiquidity = 池全局流动性
6  -> loopLiquidity = tick5处的loopLiquidity  + tick6的LiquidityNet 
7  -> loopLiquidity = tick6处的loopLiquidity  + tick7的LiquidityNet

现存问题

当前tick附近的单tick锁仓值计算结果和Uniswap官方流动性图表展示数值匹配,但全池累加得到的token0、token1总TVL明显低于子图返回的官方TVL,存在固定偏差。

当前使用的实现代码如下:

// 池当前tick
double currTick = Convert.ToDouble(currTickx);
// 池费率等级
double feeTier = Convert.ToDouble(feeTierx);
// token0精度
double d0 = Convert.ToDouble(d0x);
// token1精度
double d1 = Convert.ToDouble(d1x);
// 存储tickIdx与liquidityNet映射关系的字典
Dictionary<double, double> dict = new Dictionary<double, double>();
foreach (var tick in extractedTicks)
{
    dict[tick.tickIdx] = Convert.ToDouble(tick.liquidityNet);
  
}
// tickSpacing(相邻两个有效tick的间隔)
double tickSpacing = feeTier / 50;

// 当前tick的前一个有效tick
double upperTick = currTick - ((currTick % tickSpacing)+tickSpacing)%tickSpacing;

// 当前tick的后一个有效tick
double lowerTick = upperTick + tickSpacing;


// 获取最小、最大tick索引
double minTick = extractedTicks[0].tickIdx;

double maxTick = extractedTicks[extractedTicks.Length - 1].tickIdx;

// total0为全池token0总TVL,total1为全池token1总TVL
double total0 = 0, total1 = 0;

// 指定池的全局总流动性
double loopLiquidity = Convert.ToDouble(poolLiquidity);
//从lowerTick遍历到maxTick -> 计算锁定的token0价值
for(double curr = lowerTick; curr <= maxTick; curr += tickSpacing)
{
   
    if (dict.ContainsKey(curr) == true) { loopLiquidity += dict[curr]; loopLiquidity1 += dict1[curr];
        
    }
    double p0 = Math.Pow(1.0001, curr-tickSpacing);
    p0 = 1 / p0;
    double p1 = Math.Pow(1.0001, curr);
    p1 = 1 / p1;
    //该tick中token0的锁仓价值
    var tvl00 = (Math.Sqrt(p0) - Math.Sqrt(p1)) * loopLiquidity * Math.Pow(10, 0 - d0);

    // 该tick中token1的锁仓价值
    var tvl11 = tvl00 * (Math.Pow(1.0001, curr)) * Math.Pow(10, d0 - d1);
   
   total0 += tvl00;
    
}

//从upperTick遍历到minTick -> 计算锁定的token1价值
loopLiquidity = Convert.ToDouble(poolLiquidity);

dict[upperTick + tickSpacing] = 0;
for (double curr = upperTick; curr>=minTick; curr -= tickSpacing)
{
    
    
    double k = curr + tickSpacing;
    if (dict.ContainsKey(curr + tickSpacing) == true) { loopLiquidity -= dict[curr + tickSpacing];}
    double p0 = Math.Pow(1.0001, curr-tickSpacing);
    p0 = 1 / p0;
    double p1 = Math.Pow(1.0001, curr );
    p1 = 1 / p1;

    //该tick中token0的锁仓价值
    var tvl00 = (Math.Sqrt(p0) - Math.Sqrt(p1)) * loopLiquidity * Math.Pow(10, 0 - d0);
    
    //该tick中token1的锁仓价值
    var tvl11 = tvl00 * (Math.Pow(1.0001, curr)) * Math.Pow(10, d0 - d1);

    total1 += tvl11;
    
}
// totalValuelocked0、totalValuelocked1为直接从子图查询到的池官方TVL数值
Console.WriteLine("token0 from lower half => " + total0 + "  " + totalValuelocked0);
Console.WriteLine("token1 from upper half => " + total1 + "  " + totalValuelocked1);
// 计算目标为获取Uniswap页面悬浮提示框中展示的每个tick对应的锁仓价值

偏差原因与修正方法

总TVL计算偏低由4个核心错误导致:

  • 遍历逻辑漏算活跃区间持仓:把遍历拆成高于currentTick、低于currentTick两段,仅在高价段计算token0、低价段计算token1,完全漏掉了upperTick到lowerTick之间(即当前价格所处的活跃区间)的持仓,这个区间内是同时持有两种代币的。
  • 价格计算逻辑颠倒:Uniswap V3的价格公式为P = 1.0001^tick,代码中对p0、p1都做了取倒数操作,颠倒了价格上下限,近tick误差不明显,但跨大量tick累加后偏差会被显著放大。
  • 代币计算未区分区间场景:每个tick区间的持仓分三种情况:区间完全在当前价格上方时仅持有token0,完全在当前价格下方时仅持有token1,包含当前价格时同时持有两种代币。原有逻辑在非活跃区间同时计算两种代币导致重复累加,在活跃区间又漏算对应代币。
  • 代码笔误:代码中出现了未定义的loopLiquidity1、dict1变量,会直接导致流动性迭代结果错误。

修正后核心计算逻辑

不要拆分上下段遍历,从最小tick到最大tick单次遍历即可,计算时按区间和当前价格的位置关系分别计算代币量,同时用decimal类型替代double避免浮点精度误差:

// 计算当前价格的平方根
decimal sqrtPriceCurrent = (decimal)Math.Sqrt(Math.Pow(1.0001, currTick));
decimal loopLiquidity = Convert.ToDecimal(poolLiquidity);
// 存储每个tick的TVL
Dictionary<decimal, (decimal tvl0, decimal tvl1)> tickTvlMap = new();
decimal totalTvl0 = 0, totalTvl1 = 0;

// 从最小tick单次遍历到最大tick
for (decimal curr = minTick; curr <= maxTick; curr += (decimal)tickSpacing)
{
    // 跨过tick下界时更新流动性
    if (dict.ContainsKey((double)curr))
    {
        loopLiquidity += (decimal)dict[(double)curr];
    }
    decimal sqrtPriceLower = (decimal)Math.Sqrt(Math.Pow(1.0001, (double)curr));
    decimal sqrtPriceUpper = (decimal)Math.Sqrt(Math.Pow(1.0001, (double)(curr + tickSpacing)));
    
    decimal amount0 = 0;
    decimal amount1 = 0;
    
    // 分场景计算当前区间的代币数量
    if (sqrtPriceCurrent <= sqrtPriceLower)
    {
        // 区间在当前价上方,全为token0
        amount0 = loopLiquidity * (sqrtPriceUpper - sqrtPriceLower) / (sqrtPriceLower * sqrtPriceUpper);
    }
    else if (sqrtPriceCurrent >= sqrtPriceUpper)
    {
        // 区间在当前价下方,全为token1
        amount1 = loopLiquidity * (sqrtPriceUpper - sqrtPriceLower);
    }
    else
    {
        // 区间包含当前价,同时持有两种代币
        amount0 = loopLiquidity * (sqrtPriceUpper - sqrtPriceCurrent) / (sqrtPriceCurrent * sqrtPriceUpper);
        amount1 = loopLiquidity * (sqrtPriceCurrent - sqrtPriceLower);
    }
    // 按代币精度转换为实际数量
    amount0 /= (decimal)Math.Pow(10, d0);
    amount1 /= (decimal)Math.Pow(10, d1);
    
    tickTvlMap[curr] = (amount0, amount1);
    totalTvl0 += amount0;
    totalTvl1 += amount1;
}

额外注意事项

  • 拉取tick数据时要确保拉取到全池所有初始化过(liquidityNet != 0)的tick,漏拉tick会导致流动性迭代错误。
  • 子图返回的官方TVL是按代币当前美元价格折算的总价值,计算得到的代币数量需要乘以当前币价再和官方TVL做对比,不要直接拿数量和价值做比较。
  • 计算单tick TVL占比时,用单tick的两种代币价值之和除以全池总TVL即可。

内容的提问来源于stack exchange,提问作者Nishant Goel

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.28 05:57:12