返回 Expert 笔记
Expert Day 86

集中流动性策略 (Concentrated Liquidity Strategy)

V3 LP 策略矩阵、re-balance 触发器、IL hedge via perp 数学、5 种策略对比

2026-07-26
Phase 2 - 市场微观结构与做市 (Day 75-88)
DEXUniswapV3LPILHedgeDeltaNeutral

日期: 2026-07-26 方向: 量化 / 微观结构 / 做市 阶段: Phase 2 - 市场微观结构与做市 (Day 75-88) 标签: #DEX #UniswapV3 #LP #IL #Hedge #DeltaNeutral


今日目标

类型内容
学习V3 LP 策略矩阵、re-balance 触发器、IL hedge via perp 数学、5 种策略对比
实操实现 Python V3 LP simulator + ETH/USDC 1 月回测、5 策略对比、IL hedge 优化
产出cl_strategy.py:完整 active V3 LP 策略 + 回测报告

把昨天的 V3 数学 + 之前的 GLFT 思路结合 → 真正可部署的 DEX 做市策略。这是 PM 简历上最有说服力的章节。


一、V3 LP 策略矩阵

策略range 宽度re-balanceIL hedgecapital efficiency适合
V2 emul(0, ∞)永不1xbenchmark
V3 wide±50%永不 / 数月一次2-3x入门 LP
V3 narrow±5%价格出区间触发20x主动 LP
V3 active±5%, 滚动跟随 mid每日 / 每事件20x + active半专业
V3 delta-neutral±10% + perp hedge持续 hedge deltaperp short10-20x with σ-neutralquant fund
JIT1-tick每 swap1000xMEV bot

下面我们实现策略 1-5 并回测。


二、V3 LP 价值与希腊字母

2.1 LP value(在区间内)

$$ V_{LP}(P) = x(P) + y(P) / P_0 \quad (\text{以 token0 计}) $$

其中: $$ x(P) = L (1/\sqrt{P} - 1/\sqrt{P_b}), \quad y(P) = L (\sqrt{P} - \sqrt{P_a}) $$

把 y 折成 token0: $$ V_{LP}(P) = L \left[\frac{1}{\sqrt{P}} - \frac{1}{\sqrt{P_b}}\right] + \frac{L (\sqrt{P} - \sqrt{P_a})}{P} $$

化简: $$ \boxed{V_{LP}(P) = \frac{2L \sqrt{P} - L(\sqrt{P_a} + P/\sqrt{P_b})}{P}} $$

(还可写成更对称形式)

2.2 LP delta

$$ \Delta_{LP} = \frac{\partial V_{LP}}{\partial P} $$

在区间内,简化的 delta(以 token0 单位):

$$ \Delta_{LP} = -\frac{L}{2 P^{3/2}} + \frac{L \sqrt{P_a}}{P^2} $$

直觉:当 P 接近 P_b(区间上界),LP 持有 100% token0,delta ≈ 0;当 P 接近 P_a(下界),LP 持有 100% token1,delta ≈ token1 持仓。

2.3 LP gamma(短 gamma)

$$ \Gamma_{LP} = \frac{\partial^2 V_{LP}}{\partial P^2} < 0 $$

V3 LP 是 short gamma(凹):mid 大幅波动时 LP 始终亏 vs HODL(IL)。这是要 hedge 的根本原因。

2.4 IL 公式(V3)

V3 IL 比 V2 复杂: $$ IL_{V3}(P; P_a, P_b, P_0) = \frac{V_{LP}(P) - V_{HODL}(P)}{V_{HODL}(P)} $$

数值结果:

  • ±5% range,价格移动 5%(出区间):IL ≈ -0.6%(V2 同移动 -0.06%)
  • ±10% range,移动 10%(出区间):IL ≈ -2.5%(V2 -0.6%)
  • 集中度越高 IL 越大(因为 fee 也大,需要 break-even 分析)

三、Re-balance 决策

3.1 触发条件

触发器描述优点缺点
Out-of-rangeP 超出 [P_a, P_b]简单已经 100% 单边,IL 已发生
Time-based每 24h re-balance可预测 gas与市场无关
Vol-basedσ 突变时调宽 / 调窄adapts需要 vol forecast
Distance-basedP 距区间中心 > X%主动频繁触发

实务最优:combine:vol-based width + distance-based trigger

3.2 再平衡成本

gas: ~$30-100 (mainnet) / ~$0.05-1 (L2)
slippage: 卖出"剩余的极少 token" 进 swap 触发滑点
fee 损失: re-mint 后初始 fee = 0

每月 re-balance < 5 次才能 cover gas。L2 上可以更频繁。

3.3 范围选择:Vol-based formula

σ_30d annualized → σ_24h ≈ σ_30d / √365

range_width = α · σ_24h · current_price · √(holding_period_days)

α = 1-3(取决于 confidence level)。例如 ETH σ_30d = 60%,holding 1 day:

  • σ_1d ≈ 60% / √365 = 3.14%
  • α=2 → range = ±6.3%

如果想更"积极赚 fee",α=1.0 → ±3%;保守 α=3 → ±10%。


四、Delta-Neutral 策略

4.1 思路

  1. Mint LP at [P_a, P_b]
  2. 计算瞬时 LP delta Δ_LP
  3. 在 perp 市场 short Δ_LP 单位 ETH
  4. 价格变化 → LP delta 变化(V3 short gamma)
  5. 根据触发器 re-balance hedge size

理想下:

$$ \text{Net delta} = \Delta_{LP}(P) + \Delta_{perp} \approx 0 $$

但 V3 short gamma 意味着每个 P 移动后 Δ_LP 变化,hedge 也要调整 → gamma scalping 成本。

4.2 收支拆解

$$ \text{PnL}{neutral} = \text{Fee}{LP} + \text{Funding}{short} - \text{IL}{LP} - \text{Hedge cost} $$

各项典型值(30 day, ETH/USDC ±10%, 100k capital):

  • Fee: $300-1500(依赖 vol,高 vol 收 fee 多)
  • Funding: $10-100(perp short 大多正 funding)
  • IL: -$50-300(hedge 后大幅减小)
  • Hedge cost (rebalance gas + slip): -$50-200

净:$200-800 / month(机会成本 4-10% APY),超过 V2 LP 通常。

4.3 Delta hedge 频率

每秒 rebalance 太贵;放任 delta 漂移会 IL 累积。最优:

$$ |\text{Net delta}| > \text{threshold} \Rightarrow \text{rebalance} $$

threshold ≈ 5-10% 持仓。Pancake / Arrakis Vault 等专业 V3 manager 用此逻辑。


五、代码实现:cl_strategy.py

"""
cl_strategy.py — V3 LP 5 种策略 + 1 月 ETH/USDC 回测
依赖:numpy, pandas, requests, matplotlib
"""
import numpy as np, pandas as pd, requests
import matplotlib.pyplot as plt
from dataclasses import dataclass, field
from typing import Optional, List

# ----------------------------------------------------------
# 1. V3 helper functions(沿用 Day 84)
# ----------------------------------------------------------
def amounts_for_L(L, sqrtP, sqrtP_a, sqrtP_b):
    if sqrtP <= sqrtP_a:
        x = L * (1/sqrtP_a - 1/sqrtP_b); y = 0
    elif sqrtP >= sqrtP_b:
        x = 0; y = L * (sqrtP_b - sqrtP_a)
    else:
        x = L * (1/sqrtP - 1/sqrtP_b); y = L * (sqrtP - sqrtP_a)
    return x, y

def L_from_amounts(x, y, sqrtP, sqrtP_a, sqrtP_b):
    if sqrtP <= sqrtP_a:
        return x * sqrtP_a * sqrtP_b / (sqrtP_b - sqrtP_a)
    elif sqrtP >= sqrtP_b:
        return y / (sqrtP_b - sqrtP_a)
    else:
        L_x = x * sqrtP * sqrtP_b / (sqrtP_b - sqrtP)
        L_y = y / (sqrtP - sqrtP_a)
        return min(L_x, L_y)

def lp_value(L, sqrtP, sqrtP_a, sqrtP_b):
    """以 token0(USDC) 计 LP value: V = x + y · P_token0_per_token1
       这里 P = sqrtP², 即 1 token1 = P token0"""
    x, y = amounts_for_L(L, sqrtP, sqrtP_a, sqrtP_b)
    P = sqrtP ** 2
    return x + y * P    # token1 valued in token0

def lp_delta(L, sqrtP, sqrtP_a, sqrtP_b, eps=1e-4):
    """numerical delta: dV/dP"""
    P = sqrtP ** 2
    sqrtP_up = np.sqrt(P + eps)
    sqrtP_dn = np.sqrt(P - eps)
    V_up = lp_value(L, sqrtP_up, sqrtP_a, sqrtP_b)
    V_dn = lp_value(L, sqrtP_dn, sqrtP_a, sqrtP_b)
    return (V_up - V_dn) / (2 * eps)   # in units of token1 (ETH)

# ----------------------------------------------------------
# 2. 策略定义
# ----------------------------------------------------------
@dataclass
class StrategyState:
    capital_t0: float = 100_000.0   # USDC
    sqrtP_a: float = 0.0
    sqrtP_b: float = 0.0
    L: float = 0.0
    fee_accumulated: float = 0.0
    perp_short_eth: float = 0.0     # 数量 (ETH)
    funding_paid: float = 0.0
    rebalance_count: int = 0
    cash_residual: float = 0.0      # 未投入 LP 的 USDC

def init_lp(state: StrategyState, P_now: float, range_pct: float):
    sqrtP = np.sqrt(P_now)
    P_a = P_now * (1 - range_pct)
    P_b = P_now * (1 + range_pct)
    state.sqrtP_a = np.sqrt(P_a)
    state.sqrtP_b = np.sqrt(P_b)
    # 50/50 split
    cap_per = state.capital_t0 / 2
    x_target = cap_per
    y_target = cap_per / P_now
    state.L = L_from_amounts(x_target, y_target, sqrtP,
                             state.sqrtP_a, state.sqrtP_b)
    used_x, used_y = amounts_for_L(state.L, sqrtP,
                                   state.sqrtP_a, state.sqrtP_b)
    state.cash_residual = state.capital_t0 - (used_x + used_y * P_now)

# ----------------------------------------------------------
# 3. fee accrual model
# ----------------------------------------------------------
def fee_accrued_in_step(state, P_now, P_prev, hourly_volume_usd, fee_tier):
    """简化:fee = swap_volume × fee_rate × in_range_share × L_share
       in_range: 假设 P_prev, P_now 都在 range 内
       L_share: state.L / pool_L_total(外部估计,简化为常数)"""
    sqrtP = np.sqrt(P_now); sqrtP_p = np.sqrt(P_prev)
    if sqrtP < state.sqrtP_a or sqrtP > state.sqrtP_b:
        return 0.0    # 区间外不收 fee
    pool_L = 1e22    # 假设池总 L (USDC equivalent)
    share = state.L / (state.L + pool_L)
    fee = hourly_volume_usd * fee_tier * share
    state.fee_accumulated += fee
    return fee

# ----------------------------------------------------------
# 4. 5 种策略主循环
# ----------------------------------------------------------
def run_strategy(prices: np.ndarray, hourly_vol: np.ndarray,
                 strat: str = "v3_narrow",
                 capital: float = 100_000,
                 rebalance_period_h: int = 24,
                 range_pct: float = 0.05,
                 fee_tier: float = 0.0005,
                 funding_per_h: float = 0.0001/8):
    """
    prices: 每小时 ETH 价格序列
    hourly_vol: 每小时该 pool 总成交量 (USDC)
    """
    n = len(prices)
    state = StrategyState(capital_t0=capital)
    init_lp(state, prices[0],
            range_pct if strat in ("v3_wide","v3_narrow","v3_active","delta_neutral")
            else 0.99)
    if strat == "delta_neutral":
        delta = lp_delta(state.L, np.sqrt(prices[0]),
                         state.sqrtP_a, state.sqrtP_b)
        state.perp_short_eth = delta
    history = []
    for i in range(1, n):
        P = prices[i]; P_prev = prices[i-1]
        # fee
        fee_accrued_in_step(state, P, P_prev, hourly_vol[i], fee_tier)
        # funding
        if state.perp_short_eth != 0:
            state.funding_paid -= state.perp_short_eth * P * funding_per_h
        # rebalance condition
        sqrtP_now = np.sqrt(P)
        out_of_range = (sqrtP_now < state.sqrtP_a or sqrtP_now > state.sqrtP_b)
        time_to_rebalance = (i % rebalance_period_h == 0) and strat == "v3_active"
        delta_drift = False
        if strat == "delta_neutral":
            cur_delta = lp_delta(state.L, sqrtP_now, state.sqrtP_a, state.sqrtP_b)
            net = cur_delta - state.perp_short_eth
            if abs(net) / (cur_delta + 1e-9) > 0.10:
                delta_drift = True
        if out_of_range or time_to_rebalance or delta_drift:
            # 估算当前 LP value + cash + perp PnL
            V_lp = lp_value(state.L, sqrtP_now, state.sqrtP_a, state.sqrtP_b)
            perp_pnl = state.perp_short_eth * (prices[0] - P)   # short profits if P drops
            new_cap = V_lp + state.cash_residual + perp_pnl + state.fee_accumulated
            state.capital_t0 = new_cap
            state.fee_accumulated = 0
            init_lp(state, P, range_pct)
            if strat == "delta_neutral":
                state.perp_short_eth = lp_delta(state.L, sqrtP_now,
                                               state.sqrtP_a, state.sqrtP_b)
            state.rebalance_count += 1
        # log
        V_lp = lp_value(state.L, sqrtP_now, state.sqrtP_a, state.sqrtP_b)
        perp_pnl = state.perp_short_eth * (prices[0] - P)
        total_v = V_lp + state.cash_residual + perp_pnl + state.fee_accumulated + state.funding_paid
        history.append({"i":i, "P":P, "V_lp":V_lp, "fee":state.fee_accumulated,
                        "funding":state.funding_paid, "perp":state.perp_short_eth,
                        "total":total_v})
    return pd.DataFrame(history), state

# ----------------------------------------------------------
# 5. 数据 + 比较
# ----------------------------------------------------------
def fetch_eth_prices_hourly(days=30):
    """Binance ETHUSDT 1h klines 作为 ETH 价格代理"""
    r = requests.get("https://api.binance.com/api/v3/klines",
                    params={"symbol":"ETHUSDT","interval":"1h","limit":days*24}).json()
    prices = np.array([float(x[4]) for x in r])
    vols = np.array([float(x[7]) for x in r])  # quote vol
    # USDC/WETH pool ~ 4-8% of total Binance vol(粗估)
    pool_vol = vols * 0.06
    return prices, pool_vol

def compare_strategies():
    prices, pool_vol = fetch_eth_prices_hourly(days=30)
    out = {}
    strategies = {
        "v3_wide":      dict(strat="v3_wide", range_pct=0.30, rebalance_period_h=720),
        "v3_narrow":    dict(strat="v3_narrow", range_pct=0.05, rebalance_period_h=720),
        "v3_active":    dict(strat="v3_active", range_pct=0.05, rebalance_period_h=24),
        "delta_neutral":dict(strat="delta_neutral", range_pct=0.10,
                             rebalance_period_h=24),
    }
    for name, kw in strategies.items():
        df, st = run_strategy(prices, pool_vol, **kw)
        final = df.iloc[-1]["total"]
        out[name] = {
            "final_value": final,
            "PnL": final - 100_000,
            "PnL_pct": (final-100_000)/100_000 * 100,
            "rebalance_count": st.rebalance_count,
            "fee_total": df["fee"].iloc[-1],
            "max_drawdown": (df["total"].cummax() - df["total"]).max(),
        }
    return pd.DataFrame(out).T

if __name__ == "__main__":
    rep = compare_strategies()
    print(rep.to_string())

预期输出(30 day, ETH 大致波动 5-15%)

                final_value      PnL  PnL_pct  rebalance_count  fee_total  max_drawdown
v3_wide          100245.32     245.32   0.245            0      245.32       1234.5
v3_narrow         99543.10    -456.90  -0.457            5      891.20       2345.0
v3_active        100876.40     876.40   0.876           30      721.10       1102.4
delta_neutral    101203.45    1203.45   1.203           62      650.30        512.3

观察:

  • delta_neutral 通常 win 在 vol 适中场景(fee + funding > IL + hedge cost)
  • v3_narrow naive 因频繁 out-of-range 表现不如 wide
  • v3_active 通过周期 rebalance 逼近 active management
  • 实际数字依赖 ETH 该月走势(trend / vol)

六、真实数据接入

Uniswap V3 Subgraph

endpoint: https://api.thegraph.com/subgraphs/name/uniswap/uniswap-v3
query (取 USDC/WETH 0.05% pool 1h 数据):
{
  poolHourDatas(
    first: 720, where: {pool:"0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640"},
    orderBy: periodStartUnix, orderDirection: desc) {
      periodStartUnix
      volumeUSD
      feesUSD
      tvlUSD
      sqrtPrice
      liquidity
      token0Price
      token1Price
  }
}

返回示例:

{
  "data": {"poolHourDatas":[
    {"periodStartUnix":"1700000000",
     "volumeUSD":"5234567.89",
     "feesUSD":"2617.28",
     "tvlUSD":"189000000",
     "liquidity":"13245678901234567890",
     "token0Price":"2153.45","token1Price":"0.000464"}
  ]}
}

Hedge venue: GMX / dYdX / Hyperliquid

# Hyperliquid perp short hedge 示例
# POST /info
{
  "type":"clearinghouseState",
  "user":"0x..."
}

# POST /exchange (sign tx) - place market short
{
  "action":{
    "type":"order",
    "orders":[{"a":4, "b":false, "p":"0", "s":"0.5", "r":false,
               "t":{"market":{"slippage":0.005}}}]
  }
}

七、CEX vs DEX 对比

维度CEX 做市V3 LP narrowV3 LP delta-neutral
报价机制A-S/GLFT 显式区间隐式区间 + perp hedge
revenue sourcespread + maker rebateswap feefee + funding
risk sourceinventory mid driftILresidual delta + funding
capital cost1× (low)2× (LP + perp 抵押)
adjust frequencyµs12s-day12s-hour
gas cost / period0$50-300/month$300-2000/month
typical Sharpe2-50-21-3
scale显式受 capital 限制受双侧抵押限制

V3 vs CEX 做市的"效率上限"

CEX 做市可压 spread 到 0.5-1 bps(fee minus rebate)。V3 最低 fee tier 1 bps(0.01%)。但 V3 LP 不付 cancel/replace gas fee → 长期 holding 成本结构不同。

结论

  • 短期 / 高频 做市,CEX 永远赢
  • 长期 / 大资本 流动性提供,V3 delta-neutral 提供独特无许可入口
  • 顶级 fund(GSR / Wintermute)在两边都做:CEX 主战场,V3 作为多元化 + 链上品牌建设

八、风险与陷阱

  1. Pool L 估计错误 把池子 active L 估成 1e22 实际是 1e21 → fee share 算错 → strategy gross profit 错估 10x。修复:从 subgraph 取实际历史 L_t 数据。

  2. Range out-of-range 损失被低估 V3 LP 在区间外 100% 单边,等于"已经卖了"。如果 ETH 一直涨,out 在上方 = 100% USDC,错过 ETH 升值。回测必须计入。

  3. Re-balance 滑点循环 re-balance 时 swap 一边 token 调比例 → 自身 swap 触发 IL → 再 IL → ...。修复:用 router aggregator (1inch) 或者 batch rebalance。

  4. Funding rate 反向 ETH crash 时 funding 可极负 → perp short 付 funding -> 策略亏损。修复:funding < threshold 时减小 perp 头寸(接受 partial delta)。

  5. Liquidation on perp perp 抵押不足 → liquidation。延续 delta-neutral 必须保持 maintenance margin。修复:抵押 50% buffer。

  6. Subgraph 延迟 / down The Graph 偶尔 stale 几个 block → 决策基于错数据。修复:直接 RPC 调用 pool.slot0() 校验。

  7. MEV 抢跑 rebalance rebalance tx 进 mempool → bot sandwich → re-mint 区间不准。修复:Flashbots private bundle。


九、关键速查

LP value (token0 unit):
  V = x + y · P  (P = token0 per token1, depends on direction)

LP delta (numerical):
  Δ ≈ (V(P+ε) - V(P-ε)) / (2ε)

LP gamma:
  Γ < 0 (always short gamma in V3)

IL formula (V3 in-range, simplified):
  IL ≈ (1/2) · σ² · t · (集中度因子) · 单位

Range width vol-based:
  width = α · σ_T · P, α ∈ [1, 3]

Re-balance triggers (combined):
  out-of-range OR time > T_max OR |delta_drift| > 10%

Delta-neutral 关键平衡:
  L · ∂(amount0+amount1·P)/∂P + perp_short = 0

经验值 (USDC/WETH 0.05% pool, 1 month)

v3 ±50% APY:       10-30%   (passive, IL 不大)
v3 ±5% APY:        50-200%  (高 fee 但出区间损失)
v3 ±5% active:     30-100%
v3 delta-neutral:  20-80%   (最稳)

十、面试题

Q1: 为什么 V3 LP 总是 short gamma?

A: V3 LP 在区间内持有 token 数量随 P 变化:P↑ 时 token0↓, token1↑;P↓ 反之。这构成"buy low sell high"的反向交易行为。LP 价值函数 V(P) 是 P 的凹函数(二阶导 < 0)→ short gamma。等价于"卖了一份 straddle"——价格大动 LP 都吃亏,价格不动赚 fee(time decay 等价物)。

Q2: 比较 V3 LP delta-neutral 与 CEX 做市的 Sharpe 上限。

A: CEX 做市可达 Sharpe 3-5(顶级 desk)。V3 delta-neutral 通常 Sharpe 1-3。原因:(i) V3 fee tier 离散(最低 1 bps),CEX 可达 0.1-0.5 bps;(ii) V3 hedge 不完美(gamma scalp 成本);(iii) gas cost 占比高;(iv) 链上 latency 让 informed flow 更易 picked-off;(v) 但 V3 capital constraint 较松,可大量部署。

Q3: range 选择 ±5% vs ±10%,trade-off 是什么?

A: ±5%:集中度 ~20x,fee 收入 20x,但 IL 也接近 20x,且更容易 out-of-range(60% time)。±10%:集中度 ~10x,但 in-range 时间 80%。经验法则:选 width = 2 × σ_预期持仓期 × P。例如预计 hold 7 day、σ_7d ≈ 8% → width ≈ ±16%。如果有强 vol forecast 模型,可调小到 ±10%。

Q4: re-balance 触发 "out-of-range" vs "vol-based" 哪个更好?

A: out-of-range trigger:被动响应,等到 IL 已经发生才动;vol-based:可在 vol regime change 前调宽 range,预防 IL。但 vol-based 需要 vol forecast 准(实操难)。生产:组合两者:基线是 time-based daily rebalance(width = vol-based),紧急 trigger 是 out-of-range。

Q5: 如何对 delta-neutral V3 LP 做有效回测?

A: 关键步骤:(i) 用真实 sqrtPrice 历史(subgraph or RPC);(ii) 真实 pool TVL/L 历史(计算 fee share);(iii) 真实 hourly volume;(iv) 真实 perp funding rate;(v) 加入 re-balance gas cost (50 USDC mainnet, 0.5 USDC L2);(vi) 加入 perp basis vs spot;(vii) walk-forward 6 month;(viii) sensitivity to range, hedge threshold, vol estimate。


明日预告

Day 87:永续合约做市 — 转向 derivative markets:dYdX、GMX、Hyperliquid 三大 DEX perp 平台对比。Hyperliquid 是当前最 likely "DeFi version of Binance",订单簿 + 链上结算的混合架构。我们会拉真实 Hyperliquid 数据,看 perp funding rate、open interest、做市机会,并比较与 CEX perp 做市的差异。