Optimization
Optimization primitives answer “what’s the best version of this action?” — the optimal split for a single-sided deposit, the cost of a rebalance, the tradeoff across V3 tick ranges.
Three primitives:
OptimalDepositSplit— V2-only: optimal swap fraction for a single-sided zap-inEvaluateRebalance— V2-only: cost of cycling a position (withdraw → swap → re-zap)EvaluateTickRanges— V3-only: capital-efficiency vs IL-exposure vs fee-capture tradeoff across N candidate tick ranges
All primitives in the Agentic Primitives section follow the same contract: stateless construction, computation at .apply(), typed dataclass return.
from defipy.twin import MockProvider, StateTwinBuilderfrom defipy.utils.data import TickRangeCandidate
provider = MockProvider()builder = StateTwinBuilder()lp_v2 = builder.build(provider.snapshot("eth_dai_v2"))lp_v3 = builder.build(provider.snapshot("eth_dai_v3"))v2_tokens = lp_v2.factory.token_from_exchange[lp_v2.name]OptimalDepositSplit
Section titled “OptimalDepositSplit”Purpose. Compute the optimal swap fraction for a single-sided V2 deposit. Closed-form solution — no search, just the algebraic optimum.
Signature.
OptimalDepositSplit().apply(lp, token_in, amount_in) -> DepositSplitResultNon-mutating projection (delegates to SwapDeposit._calc_univ2_deposit_portion). V3 raises ValueError. Slippage is reported in token-in units.
from defipy import OptimalDepositSplit
result = OptimalDepositSplit().apply( lp_v2, token_in = v2_tokens["ETH"], amount_in = 100.0,)print(f"token_in: {result.token_in_name}")print(f"amount_in: {result.amount_in}")print(f"optimal_fraction: {result.optimal_fraction:.6f}")print(f"swap_amount_in: {result.swap_amount_in:.4f}")print(f"swap_amount_out: {result.swap_amount_out:.4f}")print(f"deposit_amount_in: {result.deposit_amount_in:.4f}")print(f"deposit_amount_out: {result.deposit_amount_out:.4f}")print(f"expected_lp_tokens: {result.expected_lp_tokens:.4f}")print(f"slippage_cost: {result.slippage_cost:.4f}")print(f"slippage_pct: {result.slippage_pct:.6f}")EvaluateRebalance
Section titled “EvaluateRebalance”Purpose. Report the all-in cost of cycling a V2 LP position — withdraw, swap to a single side, re-zap. Reports cost only; the rebalance verdict is left to the caller (or a downstream LLM).
Signature.
EvaluateRebalance().apply(lp, token_out, position_size_lp) -> RebalanceCostReportV3 raises ValueError. Refuses to operate when the caller owns >99.9% of the pool (no counterparty for the swap leg).
from defipy import EvaluateRebalance
result = EvaluateRebalance().apply( lp_v2, token_out = v2_tokens["DAI"], position_size_lp = 1000.0,)print(f"token_out: {result.token_out_name}")print(f"current_value: {result.current_value:.4f}")print(f"withdrawal_total_out: {result.withdrawal_total_out:.4f}")print(f"withdrawal_slippage_pct: {result.withdrawal_slippage_pct:.6f}")print(f"redeposit_slippage_pct: {result.redeposit_slippage_pct:.6f}")print(f"total_slippage_cost: {result.total_slippage_cost:.4f}")print(f"total_slippage_pct: {result.total_slippage_pct:.6f}")print(f"expected_lp_tokens_after: {result.expected_lp_tokens_after:.4f}")print(f"lp_delta: {result.lp_delta:.4f}")EvaluateTickRanges
Section titled “EvaluateTickRanges”Purpose. Quantify 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.
Signature.
EvaluateTickRanges(price_shock=0.10).apply( lp, candidates, split_comparison=None,) -> TickRangeEvaluationcandidates is a list of TickRangeCandidate(lwr_tick, upr_tick, name=None). optimal_range is the highest fee_capture_pct / il_exposure (with a 1e-9 floor to avoid divide-by-zero). All candidates must be in-range vs the current tick; out-of-range candidates raise ValueError.
from defipy import EvaluateTickRanges
# 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}")print()print(f"{'name':>12} {'cap_eff':>10} {'il_exposure':>12} {'fee_pct':>10} {'width_pct':>14}")for r in result.ranges: print(f"{r.name:>12} {r.capital_efficiency:>10.4f} {r.il_exposure:>12.6f} " f"{r.fee_capture_pct:>10.6f} {r.range_width_pct:>14.4f}")print()print(f"optimal_range: {result.optimal_range.name}")Protocol coverage
Section titled “Protocol coverage”| Protocol | Supported | Notes |
|---|---|---|
| Uniswap V2 | ✅ | OptimalDepositSplit, EvaluateRebalance |
| Uniswap V3 | ✅ | EvaluateTickRanges (V2/V3 zap variants pending UniV3Helper backlog for the others) |
| Balancer | ❌ | Multi-asset deposit semantics differ; not yet ported |
| Stableswap | ❌ | Single-sided deposits use a different objective; not yet ported |
MCP tool exposure
Section titled “MCP tool exposure”None of these are in the curated 10. Optimization primitives require multi-step reasoning — “is this rebalance worth it?”, “should I narrow my range?” — that’s better composed LLM-side over the leaf cost/scenario primitives. The category exists for when an agent needs the building blocks; the verdict is the LLM’s job.