Skip to content

AnalyzePosition

AnalyzePosition is the read-only, stateless primitive that decomposes a Uniswap V2 or V3 LP position into impermanent loss, fee income, net PnL, and a categorical diagnosis. It composes UniswapImpLoss for the closed-form IL math.

Sibling primitives handle the other AMM families:

ProtocolRequired call shape
Uniswap V2AnalyzePosition().apply(lp, lp_init_amt, entry_x_amt, entry_y_amt, holding_period_days=None)
Uniswap V3AnalyzePosition().apply(lp, lp_init_amt, entry_x_amt, entry_y_amt, lwr_tick, upr_tick, holding_period_days=None)
Balancer❌ Use AnalyzeBalancerPosition
Stableswap❌ Use AnalyzeStableswapPosition
ParameterTypeDescription
lpUniswapExchangeV2 or V3 LP exchange at current pool state.
lp_init_amtfloatLP tokens held by the position, in human units.
entry_x_amtfloatAmount of token0 originally deposited at entry, human units.
entry_y_amtfloatAmount of token1 originally deposited at entry, human units.
lwr_tick, upr_tickint (V3 only)Position’s tick range. Required for V3, omit for V2.
holding_period_daysfloat (optional)Holding period in days. When provided, real_apr is annualized from net_pnl; otherwise real_apr = None.

V2/V3 closed-form IL via UniswapImpLoss.calc_iloss(α) where

IL(α)=2α1+α1,α=pcurrentpentry\mathrm{IL}(\alpha) = \frac{2\sqrt{\alpha}}{1 + \alpha} - 1, \qquad \alpha = \frac{p_{current}}{p_{entry}}

with p measured as token1-per-token0 (matches lp.get_price(token0)).

Numeraire: token0 (e.g. ETH for an ETH/DAI pool). All values are token0-denominated.

V3 scaling. For V3, the IL formula picks up a range factor r = P_upper / P_lower, and calc_iloss accepts r as a second argument. Outside the range the position behaves like a 100% allocation to one side; inside the range the closed form scales accordingly.

Paper-share entry vs settlement-share. UniswapImpLoss(lp, lp_init_amt) exposes x_tkn_init / y_tkn_init — the linear share of current reserves entitled to lp_init_amt. This is the “paper value” — what the position is worth if it could be redeemed without moving the pool. Settlement value (running an internal RebaseIndexToken swap) is V2-only and crashes on V3 at 100% pool ownership with ZeroDivisionError; the primitive uses paper value to stay V2/V3-uniform.

Fee income isolation. fee_income = (il_with_fees − il_raw) · hold_value. The gap between realized IL (from current vs hold values) and pure-price IL (from the closed form) is the fee contribution.

Diagnosis enum. Three values:

  • "net_positive"net_pnl > 0
  • "fee_compensated" — fees recovered >50% of the IL drag ((1 − il_with_fees / il_raw) > 0.5)
  • "il_dominant" — IL drag dominates; fees aren’t keeping up
from defipy import AnalyzePosition
from defipy.twin import MockProvider, StateTwinBuilder
provider = MockProvider()
builder = StateTwinBuilder()
lp_v2 = builder.build(provider.snapshot("eth_dai_v2"))
# Position entered at 80 DAI/ETH; current spot is 100 DAI/ETH (ETH +25%).
result = AnalyzePosition().apply(
lp_v2,
lp_init_amt = 10000.0,
entry_x_amt = 1000.0,
entry_y_amt = 80000.0,
holding_period_days = 30.0,
)
print(f"current_value: {result.current_value:.4f}")
print(f"hold_value: {result.hold_value:.4f}")
print(f"il_percentage: {result.il_percentage:.6f}")
print(f"fee_income: {result.fee_income:.4f}")
print(f"net_pnl: {result.net_pnl:.4f}")
print(f"diagnosis: {result.diagnosis}")
current_value: 2000.0000 hold_value: 1800.0000 il_percentage: -0.006192 fee_income: 211.1456 net_pnl: 200.0000 diagnosis: net_positive
  • Composes UniswapImpLoss for both x_tkn_init/y_tkn_init (linear-share denominators) and calc_iloss (closed-form IL formula).
  • Composed into by AggregatePortfolio — the V2/V3 entry point for portfolio-level analysis.
  • Composed into by FindBreakEvenTime for the current IL-drag and fee-rate inputs.