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.

Setup

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.

[1]:
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)
[1]:
{'tkn_out_amt': 4976.831425047311,
 'tkn_in_nm': 'USDC',
 'tkn_in_fee': 0.49773291579631074}

AnalyzePosition

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.

[2]:
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,
)
[3]:
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

AnalyzeBalancerPosition

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.

[4]:
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,
)
[5]:
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

AnalyzeStableswapPosition

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.

[6]:
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,
)
[7]:
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

Protocol coverage

MCP tool exposure

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.