Skip to content

Position Analysis

Position Analysis primitives decompose a live LP position into its drivers — impermanent loss, fee income, and net PnL — so an LLM (or a human) can answer the question why is this position making or losing money? The category contains three primitives, one per protocol family: Uniswap V2/V3, Balancer (2-asset weighted), and Stableswap (2-asset).

All primitives in the Agentic Primitives section follow the same contract: stateless construction, computation at .apply(), typed dataclass return.

Build three twins from the canonical MockProvider recipes — one per protocol — and apply a small swap to push the stableswap pool slightly off peg so its analysis isn’t trivially zero.

from defipy.twin import MockProvider, StateTwinBuilder
from stableswappy.process.swap import Swap as StableswapSwap
provider = MockProvider()
builder = StateTwinBuilder()
lp_v2 = builder.build(provider.snapshot("eth_dai_v2"))
lp_bal = builder.build(provider.snapshot("eth_dai_balancer_50_50"))
lp_sts = builder.build(provider.snapshot("usdc_dai_stableswap_A10"))
# Push the stableswap pool slightly off peg with a 5,000 USDC -> DAI swap.
sts_tokens = lp_sts.factory.token_from_exchange[lp_sts.name]
StableswapSwap().apply(lp_sts, sts_tokens["USDC"], sts_tokens["DAI"], "agent", 5000.0)
{'tkn_out_amt': 4976.831425047311, 'tkn_in_nm': 'USDC', 'tkn_in_fee': 0.49773291579631074}

Purpose. Decompose an LP position into impermanent loss, fee income, and net PnL for Uniswap V2 and V3 pools.

Signature.

AnalyzePosition().apply(
lp, lp_init_amt, entry_x_amt, entry_y_amt,
lwr_tick=None, upr_tick=None, holding_period_days=None,
) -> PositionAnalysis

The caller supplies entry token amounts explicitly — the natural framing for an LP (“I deposited X ETH and Y DAI”). The primitive combines entry amounts with the current pool state to compute current value, hold value, and the IL/fee decomposition. lwr_tick / upr_tick are V3-only; omit for V2. holding_period_days enables real_apr annualization.

The example below models a position that entered at 80 DAI/ETH and is being analyzed at the current spot of 100 DAI/ETH — i.e. ETH appreciated 25% since entry.

from defipy import AnalyzePosition
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

Purpose. Decompose a 2-asset Balancer LP position into IL, fees, and net PnL. Sibling to AnalyzePosition adapted to the weighted-AMM IL formula where the weight (not just the price ratio) drives IL.

Signature.

AnalyzeBalancerPosition().apply(
lp, lp_init_amt, entry_base_amt, entry_opp_amt,
holding_period_days=None,
) -> BalancerPositionAnalysis

Base / opp follow the pool’s token insertion order. v1 has no fee attribution — fee_income is always 0.0 and il_with_fees == il_percentage. All values are denominated in opp-token units (Balancer’s natural numeraire). N>2 pools raise ValueError.

Same scenario as the V2 example: the position entered when ETH was priced at 80 DAI.

from defipy import AnalyzeBalancerPosition
result = AnalyzeBalancerPosition().apply(
lp_bal,
lp_init_amt = 100.0,
entry_base_amt = 1000.0,
entry_opp_amt = 80000.0,
holding_period_days = 30.0,
)
print(f"base / opp: {result.base_tkn_name} / {result.opp_tkn_name}")
print(f"base_weight: {result.base_weight}")
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"net_pnl: {result.net_pnl:.4f}")
print(f"alpha: {result.alpha:.6f}")
print(f"diagnosis: {result.diagnosis}")
base / opp: ETH / DAI base_weight: 0.5 current_value: 200000.0000 hold_value: 180000.0000 il_percentage: -0.006192 net_pnl: 20000.0000 alpha: 1.250000 diagnosis: net_positive

Purpose. Decompose a 2-asset Stableswap LP position. The flat-curve regime around peg means small price deviations can produce surprisingly large IL at high A.

Signature.

AnalyzeStableswapPosition().apply(
lp, lp_init_amt, entry_amounts, holding_period_days=None,
) -> StableswapPositionAnalysis

entry_amounts is a list [x0, x1] in the pool’s insertion order (covers single-sided deposits, not just balanced ones). Values are denominated at peg — tokens summed 1:1, stableswap’s natural numeraire.

Reachability. When the pool’s implied alpha is unreachable (high A, large |1−α|), the result returns None for il_percentage and net_pnl, with diagnosis = "unreachable_alpha". When the pool is at peg the result short-circuits to il_percentage = 0.0 and diagnosis = "at_peg".

The setup cell pushed this pool slightly off peg via a 5,000 USDC → DAI swap, so the analysis below reflects a real (small) IL number.

from defipy import AnalyzeStableswapPosition
result = AnalyzeStableswapPosition().apply(
lp_sts,
lp_init_amt = 100.0,
entry_amounts = [100000.0, 100000.0],
holding_period_days = 30.0,
)
print(f"token_names: {result.token_names}")
print(f"A: {result.A}")
print(f"alpha: {result.alpha:.6f}")
print(f"current_value: {result.current_value:.6f}")
print(f"hold_value: {result.hold_value:.4f}")
print(f"il_percentage: {result.il_percentage:.8f}")
print(f"net_pnl: {result.net_pnl:.6f}")
print(f"diagnosis: {result.diagnosis}")
token_names: ['USDC', 'DAI'] A: 10 alpha: 0.990934 current_value: 199966.455457 hold_value: 200000.0000 il_percentage: -0.00016772 net_pnl: -33.544543 diagnosis: il_dominant
ProtocolSupportedNotes
Uniswap V2AnalyzePosition — full IL + fee decomposition
Uniswap V3AnalyzePosition with lwr_tick / upr_tick for concentrated positions
BalancerAnalyzeBalancerPosition — 2-asset only; no fee attribution in v1
Stableswap⚠️AnalyzeStableswapPosition — 2-asset only; unreachable-alpha regime returns None for IL/PnL

All three primitives in this category are surfaced as MCP tools in the curated set of 10:

  • AnalyzePosition
  • AnalyzeBalancerPosition
  • AnalyzeStableswapPosition

Position analysis is the highest-traffic agent question (“how is my position doing?”), so all three primitives — one per protocol family — are exposed directly rather than left to the LLM to compose.