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, StateTwinBuilderfrom 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)AnalyzePosition
Section titled “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,) -> PositionAnalysisThe 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}")AnalyzeBalancerPosition
Section titled “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,) -> BalancerPositionAnalysisBase / 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}")AnalyzeStableswapPosition
Section titled “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,) -> StableswapPositionAnalysisentry_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}")Protocol coverage
Section titled “Protocol coverage”| Protocol | Supported | Notes |
|---|---|---|
| Uniswap V2 | ✅ | AnalyzePosition — full IL + fee decomposition |
| Uniswap V3 | ✅ | AnalyzePosition with lwr_tick / upr_tick for concentrated positions |
| Balancer | ✅ | AnalyzeBalancerPosition — 2-asset only; no fee attribution in v1 |
| Stableswap | ⚠️ | AnalyzeStableswapPosition — 2-asset only; unreachable-alpha regime returns None for IL/PnL |
MCP tool exposure
Section titled “MCP tool exposure”All three primitives in this category are surfaced as MCP tools in the curated set of 10:
AnalyzePositionAnalyzeBalancerPositionAnalyzeStableswapPosition
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.