Skip to content

EvaluateTickRanges

EvaluateTickRanges is the read-only, stateless primitive that quantifies the capital-efficiency vs IL-exposure vs fee-capture tradeoff across N V3 candidate tick ranges. Optionally compares one wide range against a split of N narrow ranges.

V3 only.

ProtocolRequired call shape
Uniswap V2❌ V2 has no concept of tick ranges
Uniswap V3EvaluateTickRanges(price_shock=0.10).apply(lp, candidates, split_comparison=None)
Balancer❌ Not applicable
Stableswap❌ Not applicable
ParameterTypeDescription
price_shockfloat (default 0.10)Symmetric price shock used to evaluate IL exposure per candidate.
ParameterTypeDescription
lpUniswapV3ExchangeV3 LP at current state.
candidateslist[TickRangeCandidate]Each carries lwr_tick, upr_tick, and optional name. All candidates must be in-range vs the current tick — out-of-range candidates raise ValueError.
split_comparisontuple (optional)Optional one-vs-N split comparison. Format details on the result dataclass.

Per-candidate metrics:

  • capital_efficiency — relative to a full-range position (full range = 1.0; tighter ranges = higher number)
  • il_exposure — IL at the constructor’s price_shock evaluated against the candidate’s range factor
  • fee_capture_pct — heuristic share of pool fees the candidate would capture (function of range tightness and current tick position within the range)
  • range_width_pct — percentage range width

optimal_range. Highest fee_capture_pct / il_exposure ratio (with a 1e-9 floor to avoid divide-by-zero on zero-IL candidates). The intuition: best return per unit of IL risk.

fee_capture_pct is a heuristic, not a measured quantity — the primitive estimates capture as a function of range tightness and current-tick position, not a measured fee history. Useful for relative ranking; less useful for absolute fee predictions.

All-candidates-in-range constraint. If any candidate’s range doesn’t include the current tick, the primitive raises ValueError. Out-of-range candidates have undefined il_exposure (the IL formula assumes the position is participating in the swap), so the primitive refuses rather than silently returning misleading numbers.

from defipy import EvaluateTickRanges
from defipy.utils.data import TickRangeCandidate
from defipy.twin import MockProvider, StateTwinBuilder
provider = MockProvider()
builder = StateTwinBuilder()
lp_v3 = builder.build(provider.snapshot("eth_dai_v3"))
# Current ETH/DAI tick ≈ 46054. Three candidate ranges around it:
candidates = [
TickRangeCandidate(lwr_tick=45000, upr_tick=47000, name="narrow"),
TickRangeCandidate(lwr_tick=44000, upr_tick=48000, name="medium"),
TickRangeCandidate(lwr_tick=-887220, upr_tick=887220, name="full_range"),
]
result = EvaluateTickRanges().apply(lp_v3, candidates)
print(f"price_shock used: {result.price_shock}")
for r in result.ranges:
print(f" {r.name:>12} cap_eff={r.capital_efficiency:.4f} "
f"il_exposure={r.il_exposure:.6f} fee_pct={r.fee_capture_pct:.6f}")
print(f"optimal_range: {result.optimal_range.name}")
price_shock used: 0.1 narrow cap_eff=10.5088 il_exposure=6.162096 fee_pct=0.001050 medium cap_eff=5.5169 il_exposure=0.174863 fee_pct=0.000551 full_range cap_eff=1.0000 il_exposure=0.001260 fee_pct=0.000100 optimal_range: full_range

The narrow range has 10× the capital efficiency but 6× the IL exposure at the 10% shock — the ratio favors full_range for this scenario. Different price_shock values produce different optimal_range selections; tune the shock to the volatility regime you care about.

  • Independent leaf primitive — does not depend on other agentic primitives.
  • Composed into by CompareFeeTiers for the per-tier range evaluation when ranges differ across candidates.