Comparison

Comparison primitives answer “which is better for my position?” across choices that are otherwise apples-to-apples-ish.

Two primitives: - CompareFeeTiers — V3-only: compare candidate fee tiers for the same token pair - CompareProtocols — cross-protocol: IL and slippage for the same trade across two AMM types

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

Setup

[1]:
from defipy.twin import MockProvider, StateTwinBuilder
from defipy.utils.data import FeeTierCandidate

provider = MockProvider()
builder = StateTwinBuilder()
lp_v2 = builder.build(provider.snapshot("eth_dai_v2"))
lp_v3 = builder.build(provider.snapshot("eth_dai_v3"))
lp_bal = builder.build(provider.snapshot("eth_dai_balancer_50_50"))
v2_tokens = lp_v2.factory.token_from_exchange[lp_v2.name]

CompareFeeTiers

Purpose. Compare V3 fee tiers for the same token pair. Mismatched token pairs raise ValueError; non-V3 pools raise ValueError.

Signature.

CompareFeeTiers().apply(candidates) -> FeeTierComparison

candidates is a list of FeeTierCandidate(lp, position_size_lp, lwr_tick, upr_tick, name=None). observed_fee_yield is None when the pool has no fees recorded / zero spot / zero TVL — None-yield candidates sort last in ranking_by_observed_fee_yield. position_size_lp is currently an echo (not used in v1 ranking).

Note: the canonical MockProvider exposes a single V3 ETH/DAI recipe (3000 bps fee), so the example below shows the API shape with one candidate. In practice you’d pass 2-3 candidates from different fee tiers (500 / 3000 / 10000) of the same pair to drive the ranking.

[2]:
from defipy import CompareFeeTiers

candidates = [
    FeeTierCandidate(
        lp = lp_v3,
        position_size_lp = 10000.0,
        lwr_tick = 45000,
        upr_tick = 47000,
        name = "3000bps_narrow",
    ),
]

result = CompareFeeTiers().apply(candidates)
[3]:
print(f"numeraire:                       {result.numeraire}")
print(f"pair:                            {result.pair}")
print(f"ranking_by_observed_fee_yield:   {result.ranking_by_observed_fee_yield}")
print(f"ranking_by_tvl:                  {result.ranking_by_tvl}")
print()
for t in result.tiers:
    print(f"  {t.name}: fee_tier_bps={t.fee_tier_bps}, tvl={t.pool_tvl_in_token0:.4f}, "
          f"yield={t.observed_fee_yield}, in_range={t.in_range}, width={t.range_width_pct:.4f}")
numeraire:                       ETH
pair:                            ETH/DAI
ranking_by_observed_fee_yield:   ['3000bps_narrow']
ranking_by_tvl:                  ['3000bps_narrow']

  3000bps_narrow: fee_tier_bps=30, tvl=2000.0000, yield=None, in_range=True, width=0.1992

CompareProtocols

Purpose. Compare IL at a price shock + slippage at a trade size between two pools. Pools may be different protocols.

Signature.

CompareProtocols(price_shock=0.10, v3_range_pct=0.10).apply(
    lp_a, lp_b, amount, token_in=None,
) -> ProtocolComparison

token_in defaults to lp_a’s token0 — both pools must contain it. V3 il_at_shock is None when price_shock > v3_range_pct (out-of-range regime). Stableswap il_at_shock is None on DepegUnreachableError. Balancer/Stableswap slippage_at_amount is None (CalculateSlippage is Uniswap-only). Advantage labels: "pool_a", "pool_b", "tied", or None (when either side is None).

[4]:
from defipy import CompareProtocols

# Compare V2 against Balancer on the same ETH/DAI pair, 10 ETH trade.
result = CompareProtocols().apply(
    lp_v2,
    lp_bal,
    amount = 10.0,
    token_in = v2_tokens["ETH"],
)
[5]:
print(f"price_shock used:    {result.price_shock}")
print(f"amount / token_in:   {result.amount} {result.token_in_name}")
print(f"il_advantage:        {result.il_advantage}")
print(f"slippage_advantage:  {result.slippage_advantage}")
print()
print(f"  pool_a ({result.pool_a.protocol}):  il={result.pool_a.il_at_shock}, "
      f"slippage={result.pool_a.slippage_at_amount}, tvl={result.pool_a.tvl_in_token_in}")
print(f"  pool_b ({result.pool_b.protocol}):  il={result.pool_b.il_at_shock}, "
      f"slippage={result.pool_b.slippage_at_amount}, tvl={result.pool_b.tvl_in_token_in}")
print()
print(f"notes: {result.notes}")
price_shock used:    0.1
amount / token_in:   10.0 ETH
il_advantage:        tied
slippage_advantage:  None

  pool_a (uniswap_v2):  il=0.0012602161831160605, slippage=0.012841965602938644, tvl=2000.0
  pool_b (balancer):  il=0.0012602161831160605, slippage=None, tvl=2000.0

notes: ['pool_b: Balancer slippage not computed in v1 (CalculateSlippage is Uniswap-only)']

Protocol coverage

MCP tool exposure

Neither primitive is in the curated 10. Comparison answers (“which fee tier?”, “which protocol?”) are second-order decisions an agent makes after a position-level analysis — they’re better composed LLM-side over the leaf primitives. The category exists for when an agent wants the structured comparison instead of building it from scratch.