Guide

Uniswap V3 Concentrated Liquidity: A Complete Guide for AI Agents

How AI agents provide concentrated liquidity on Uniswap V3 — tick range selection, fee tier optimization, impermanent loss modeling, auto-rebalancing, and gamma hedging with perpetual futures.

March 6, 2026 24 min read Purple Flea Research
4,000x
Peak capital efficiency vs V2
4
Fee tiers: 0.01% / 0.05% / 0.3% / 1%
~$2B
Daily Uniswap V3 volume (ETH mainnet)

1. V3 vs V2: The LP Math Revolution

Uniswap V2 deployed liquidity uniformly across the entire price curve from 0 to infinity, meaning 99%+ of capital sat idle at any given moment. V3 introduced concentrated liquidity — LPs now specify a price range [Pa, Pb] within which their capital is active. When price moves outside the range, the LP's position converts entirely to one asset and earns no fees.

The V2 invariant is the classic xy = k constant product formula. V3 generalizes this with a virtual liquidity concept:

# V2: x * y = k  (uniform, infinite range)
# V3: (x + L/sqrt(P_b)) * (y + L*sqrt(P_a)) = L^2

# Where:
# L = liquidity (the fundamental unit in V3)
# P_a = lower price tick (sqrt form: sqrtPa)
# P_b = upper price tick (sqrt form: sqrtPb)
# P   = current price

# Real reserves given L and current price P:
import math

def real_reserves(L, P, P_a, P_b):
    """Returns (x_real, y_real) for a V3 position."""
    sqrtP  = math.sqrt(P)
    sqrtPa = math.sqrt(P_a)
    sqrtPb = math.sqrt(P_b)

    if P <= P_a:
        # Out of range (below): all token0
        x = L * (1/sqrtPa - 1/sqrtPb)
        y = 0
    elif P >= P_b:
        # Out of range (above): all token1
        x = 0
        y = L * (sqrtPb - sqrtPa)
    else:
        # In range: both tokens
        x = L * (1/sqrtP - 1/sqrtPb)
        y = L * (sqrtP - sqrtPa)
    return x, y

The key insight: a V3 position between Pa and Pb is economically equivalent to a V2 position of infinite range, but using only the capital actually needed for that range. This is the source of V3's capital efficiency gains.

Capital Efficiency Formula: A position covering a 2x price range (e.g., 1000–2000 USDC/ETH) uses approximately 5.8x less capital than the equivalent V2 position. A 1.1x range delivers ~40x efficiency. The tighter the range, the higher the efficiency — and the higher the rebalancing risk.

2. Tick Spacing and Range Selection

V3 prices are discretized into ticks. Each tick corresponds to a 0.01% price increment: price at tick i = 1.0001^i. Tick spacing (the minimum gap between initialized ticks) depends on the fee tier:

Fee TierTick SpacingMin Range WidthBest For
0.01%10.01%Stablecoin pairs (USDC/USDT)
0.05%100.1%Correlated pairs (WBTC/ETH, stETH/ETH)
0.30%600.6%Standard pairs (ETH/USDC)
1.00%2002.0%Exotic pairs, low-liquidity tokens

When selecting a range, agents must balance two opposing forces: fee capture (tighter range = more fees per dollar when in range) and durability (wider range = more time in range = fewer rebalances). The optimal range width is a function of price volatility:

import numpy as np

def optimal_range(current_price, daily_vol, days_until_rebalance, fee_tier):
    """
    Estimate optimal range width based on price volatility.
    daily_vol: annualized volatility / sqrt(252)
    days_until_rebalance: how long we want to stay in range
    Returns: (lower_price, upper_price)
    """
    # Use 1-sigma move over horizon as range half-width
    sigma = daily_vol * math.sqrt(days_until_rebalance)

    # Conservative agents use 2-sigma (95% confidence)
    multiplier = 2.0

    lower = current_price * math.exp(-multiplier * sigma)
    upper = current_price * math.exp(multiplier * sigma)

    # Round to valid tick boundaries
    def price_to_tick(p):
        return math.floor(math.log(p) / math.log(1.0001))

    def tick_to_price(t, spacing):
        aligned = math.floor(t / spacing) * spacing
        return 1.0001 ** aligned

    spacing = {0.0001: 1, 0.0005: 10, 0.003: 60, 0.01: 200}[fee_tier]

    lower_tick  = price_to_tick(lower)
    upper_tick  = price_to_tick(upper)
    lower_price = tick_to_price(lower_tick, spacing)
    upper_price = tick_to_price(upper_tick, spacing)

    return lower_price, upper_price

# Example: ETH/USDC at $2000, 80% ann. vol, 3-day horizon
eth_price = 2000
ann_vol   = 0.80
daily_vol = ann_vol / math.sqrt(252)
lo, hi = optimal_range(eth_price, daily_vol, 3, 0.003)
print(f"Suggested range: ${lo:.0f} – ${hi:.0f}")
# Suggested range: $1632 – $2449

3. Capital Efficiency Formula

The capital efficiency ratio (CER) of a V3 position vs V2 is derived from the ratio of virtual to real liquidity:

def capital_efficiency_ratio(P, P_a, P_b):
    """
    How much more capital-efficient is this V3 position vs V2?
    Returns a multiplier (e.g., 10 means 10x more efficient).
    """
    sqrtP  = math.sqrt(P)
    sqrtPa = math.sqrt(P_a)
    sqrtPb = math.sqrt(P_b)

    # Fraction of the price range that the position covers
    # (in sqrt-price space)
    concentration = sqrtP / sqrtPa

    cer = (sqrtPb - sqrtPa) / (sqrtPb / sqrtP - 1 + sqrtP / sqrtPb - 1)
    # Simplified:
    cer_simple = sqrtPb * sqrtP / (sqrtPb - sqrtP) + sqrtP / (sqrtP - sqrtPa)

    return abs(1 / (1 - sqrtPa/sqrtP) - 1)

# Capital efficiency for various range widths at current price P
scenarios = [
    ("Full range (V2)", 0, float('inf')),
    ("10x range",       P/10, P*10),
    ("4x range",        P/4,  P*4),
    ("2x range",        P/2,  P*2),
    ("1.5x range",      P/1.5, P*1.5),
    ("1.1x range",      P/1.1, P*1.1),
]

P = 2000
print("Range Width | Efficiency vs V2")
for label, pa, pb in scenarios[1:]:
    sqrtP  = math.sqrt(P)
    sqrtPa = math.sqrt(pa)
    sqrtPb = math.sqrt(pb)
    eff = (sqrtPb - sqrtPa) ** 2 / ((sqrtP - sqrtPa) * (sqrtPb - sqrtP)) if pa < P < pb else float('inf')
    print(f"{label:20s} | {eff:.1f}x")

Fee Revenue Projection

Expected daily fee revenue for a V3 position depends on volume, fee tier, and the fraction of total pool liquidity the position represents:

def expected_daily_fees(position_liquidity, pool_total_liquidity,
                         daily_volume, fee_tier, price_in_range_fraction):
    """
    position_liquidity: L value of our position
    pool_total_liquidity: total L in active tick
    daily_volume: USD volume per day
    price_in_range_fraction: fraction of day price spends in our range
    """
    fee_revenue = (position_liquidity / pool_total_liquidity) * \
                  daily_volume * fee_tier * price_in_range_fraction
    return fee_revenue

# Example: 1% of pool liquidity, 0.3% fee, $10M daily volume, 60% time in range
fees = expected_daily_fees(1.0, 100.0, 10_000_000, 0.003, 0.60)
print(f"Expected daily fees: ${fees:,.0f}")  # ~$180/day

4. Fee Tier Comparison and Selection

Choosing the wrong fee tier is one of the most common LP mistakes. The rule of thumb: match fee tier to pair correlation. Highly correlated pairs generate thin margins on high volume, so use low fee tiers. Exotic pairs with high spread demand higher fees to compensate for IL risk.

Fee TierPool ExamplesTypical Daily VolIL RiskAgent Strategy
0.01%USDC/USDT, DAI/USDC$500M+Near zeroUltra-tight range, high rebalance frequency
0.05%ETH/USDC, WBTC/USDC$200M–$1BMedium2–7 day range, volatility-adjusted width
0.30%WBTC/ETH, LINK/ETH$50M–$200MMedium-HighWider ranges, less frequent rebalance
1.00%New tokens, exotic pairs$1M–$50MHighVery wide range or active monitoring

An autonomous agent should evaluate multiple fee tiers for the same pair and select the one with the highest fee APR relative to IL risk. The metric to optimize:

def fee_tier_score(fee_apr, il_estimate, rebalance_cost):
    """Score a fee tier for a given position horizon."""
    net_return = fee_apr - il_estimate - rebalance_cost
    return net_return

# Agent evaluates all available tiers
def select_best_tier(pair_data, position_size, horizon_days):
    scores = {}
    for tier, data in pair_data.items():
        fee_apr    = estimate_fee_apr(data, position_size)
        il_est     = estimate_il(data['vol'], tier, horizon_days)
        rebal_cost = estimate_rebalance_cost(data['vol'], tier)
        scores[tier] = fee_tier_score(fee_apr, il_est, rebal_cost)
    return max(scores, key=scores.get)

5. Impermanent Loss in Concentrated Ranges

IL in V3 is significantly amplified compared to V2 because the position is more concentrated. When price exits the range, the LP holds 100% of the less valuable asset. The IL formula for V3 is:

def v3_impermanent_loss(P_initial, P_final, P_a, P_b):
    """
    Calculate impermanent loss for a V3 position.
    Returns IL as a fraction (negative = loss).
    """
    def position_value(P, Pa, Pb, L=1.0):
        """Value of position at price P, normalized."""
        sqrtP  = math.sqrt(max(P, Pa))
        sqrtPa = math.sqrt(Pa)
        sqrtPb = math.sqrt(Pb)
        sqrtPc = math.sqrt(min(P, Pb))

        x = L * (1/sqrtPc - 1/sqrtPb)  # token0 amount
        y = L * (sqrtPc - sqrtPa)        # token1 amount
        return x * P + y  # total value in token1 units

    P_a = max(P_a, 1e-10)
    L = 1.0

    V_hold  = (0.5 * P_final/P_initial + 0.5)  # 50/50 HODL
    V_lp    = position_value(P_final, P_a, P_b)
    V_lp_0  = position_value(P_initial, P_a, P_b)

    il = (V_lp / V_lp_0) / V_hold - 1
    return il

# IL comparison: V3 2x range vs V2 full range
import numpy as np
price_ratios = np.linspace(0.5, 2.0, 100)
P0, Pa, Pb = 2000, 1000, 4000

print("Price Ratio | V3 IL (2x range) | V2 IL (full range)")
for r in [0.5, 0.75, 1.0, 1.25, 1.5, 2.0]:
    Pf = P0 * r
    v3_il = v3_impermanent_loss(P0, Pf, Pa, Pb)
    v2_il = 2 * math.sqrt(r) / (1 + r) - 1  # standard V2 IL formula
    print(f"  {r:.2f}x    |    {v3_il*100:+.2f}%         |  {v2_il*100:+.2f}%")
Key Risk: When price exits a concentrated range, IL becomes "realized" in the sense that the position becomes single-sided and earns zero fees. The agent must decide: wait for price to return, or close and redeploy. This decision should be governed by a clear policy based on drift speed and rebalancing cost.

6. Rebalancing Triggers and Gas Optimization

Rebalancing a V3 position involves removing liquidity, swapping to re-balance token ratios, and minting a new position. Each rebalance costs gas (~$5–$30 on Ethereum mainnet, <$0.10 on L2s). Agents must implement smart rebalancing logic to avoid over-trading.

from dataclasses import dataclass
from typing import Optional
import time

@dataclass
class RebalancePolicy:
    # Trigger conditions
    out_of_range: bool = True          # Always rebalance if price exits range
    drift_threshold: float = 0.15     # Rebalance if price drifted >15% inside range
    fee_collection_interval: int = 7  # Collect fees every 7 days minimum
    min_fee_to_rebalance: float = 20  # Min $20 in accrued fees before rebalancing

    # Cost parameters
    gas_estimate_usd: float = 15.0    # Estimated gas cost per rebalance
    slippage_bps: int = 30            # Expected swap slippage in basis points

class UniswapV3PositionManager:
    def __init__(self, web3, position_id, policy: RebalancePolicy):
        self.w3 = web3
        self.position_id = position_id
        self.policy = policy
        self.last_rebalance = time.time()

    def check_rebalance_needed(self, current_price, position) -> Optional[str]:
        """Returns reason for rebalance or None if not needed."""
        P, Pa, Pb = current_price, position.tick_lower_price, position.tick_upper_price

        # 1. Price out of range
        if P <= Pa or P >= Pb:
            return "price_out_of_range"

        # 2. Price drifted significantly inside range
        range_width  = Pb - Pa
        center_price = (Pa + Pb) / 2
        drift = abs(P - center_price) / range_width
        if drift > self.policy.drift_threshold:
            return f"drift_{drift:.2%}"

        # 3. Fee collection interval
        days_since = (time.time() - self.last_rebalance) / 86400
        if days_since >= self.policy.fee_collection_interval:
            fees_usd = self.estimate_accrued_fees(position)
            if fees_usd >= self.policy.min_fee_to_rebalance:
                return f"fee_collection_{fees_usd:.2f}usd"

        return None

    def estimate_accrued_fees(self, position) -> float:
        """Query pending fees from the NonfungiblePositionManager contract."""
        # Call collect() with recipient=self to estimate (staticcall)
        # Returns (amount0, amount1) in token units
        pass

    def execute_rebalance(self, current_price, position, new_range_width_multiplier=2.0):
        """Remove, swap, and redeploy with new range centered on current price."""
        reason = self.check_rebalance_needed(current_price, position)
        if not reason:
            return {"status": "no_rebalance_needed"}

        print(f"Rebalancing: {reason}")

        # 1. Remove all liquidity + collect fees
        # 2. Compute new range
        new_lower = current_price / new_range_width_multiplier
        new_upper = current_price * new_range_width_multiplier

        # 3. Swap if needed to 50/50 value for new range
        # 4. Mint new position

        self.last_rebalance = time.time()
        return {"status": "rebalanced", "reason": reason, "new_range": (new_lower, new_upper)}

Gas Cost Amortization

On Ethereum mainnet, rebalancing only makes sense when accrued fees exceed gas costs by a comfortable margin. Rule of thumb: only rebalance when accrued fees are at least 3x the expected gas cost. On L2s (Arbitrum, Base, Optimism), this threshold drops to 1.2x.

7. Gamma Hedging with Perpetual Futures

A V3 LP position has negative gamma — it is concave, meaning large price moves hurt the position more than proportionally. This is the mathematical essence of impermanent loss. Agents can hedge this negative gamma by holding a long gamma position in options or, more practically, by dynamically trading perpetual futures.

The LP's delta (price sensitivity) changes as price moves. At any price P within the range, the effective delta of the LP position is approximately:

def lp_delta(P, P_a, P_b, L=1.0):
    """
    First derivative of LP position value with respect to price.
    This is the delta to hedge.
    """
    sqrtP  = math.sqrt(P)
    sqrtPa = math.sqrt(P_a)
    sqrtPb = math.sqrt(P_b)

    if P <= P_a:
        # All token0, delta = full token0 amount
        return L * (1/sqrtPa - 1/sqrtPb)
    elif P >= P_b:
        # All token1, delta = 0 (in token0 terms)
        return 0
    else:
        # In range: delta is between 0 and 1
        # dV/dP = L * (0.5/sqrtP - 0.5*sqrtP/P_b) in simplified form
        return L * (0.5/sqrtP)  # approximate for mid-range

def lp_gamma(P, P_a, P_b, L=1.0):
    """Second derivative — this is the negative gamma we want to hedge."""
    sqrtP = math.sqrt(P)
    if P_a < P < P_b:
        return -L * 0.25 / (P ** 1.5)  # negative!
    return 0

class GammaHedger:
    """
    Hedges the negative gamma of a V3 LP position using perpetual futures.
    Strategy: maintain delta-neutral by adjusting perp position as price moves.
    """
    def __init__(self, pf_api_key, position):
        self.pf   = PurpleFleasAPI(pf_api_key)  # Purple Flea perp API
        self.pos  = position
        self.hedge_position = 0  # current perp position (+ = long, - = short)

    def compute_target_hedge(self, current_price):
        """Target perp size to achieve delta neutrality."""
        lp_delta_val = lp_delta(current_price, self.pos.Pa, self.pos.Pb, self.pos.L)
        # LP is long token0 with this delta; short perp to hedge
        return -lp_delta_val  # negative = short

    def rehedge(self, current_price, threshold=0.02):
        """Adjust hedge if delta drifted more than threshold."""
        target = self.compute_target_hedge(current_price)
        delta_drift = abs(target - self.hedge_position)

        if delta_drift > threshold:
            adjustment = target - self.hedge_position
            self.pf.adjust_position(
                market="ETH-PERP",
                size_delta=adjustment,
                reduce_only=(adjustment * self.hedge_position < 0)
            )
            self.hedge_position = target
            return {"hedged": True, "adjustment": adjustment}
        return {"hedged": False}
Purple Flea Integration: Purple Flea's perpetual futures API (275+ markets) is ideal for gamma hedging V3 LP positions. The sub-second execution and low maker fees mean delta rehedging stays cost-effective even for smaller positions. See the trading docs.

8. Complete UniswapV3Agent Implementation

The following is a production-ready autonomous LP agent that integrates range selection, monitoring, rebalancing, and gamma hedging into a single event loop:

"""
UniswapV3Agent — Autonomous concentrated liquidity provider
Integrates with Purple Flea perpetual futures for gamma hedging.
"""

import asyncio
import math
import time
import logging
from dataclasses import dataclass, field
from typing import Optional, Tuple
from web3 import Web3

logging.basicConfig(level=logging.INFO, format='%(asctime)s %(levelname)s %(message)s')
log = logging.getLogger("UniswapV3Agent")

@dataclass
class V3Position:
    pool_address: str
    token0: str
    token1: str
    fee_tier: int
    tick_lower: int
    tick_upper: int
    liquidity: int
    position_id: Optional[int] = None

    @property
    def Pa(self): return 1.0001 ** self.tick_lower
    @property
    def Pb(self): return 1.0001 ** self.tick_upper

@dataclass
class AgentConfig:
    rpc_url: str
    private_key: str
    pf_api_key: str
    pair: str = "ETH/USDC"
    capital_usdc: float = 10_000
    fee_tier: int = 500          # 0.05%
    range_sigma: float = 2.0    # 2-sigma range
    horizon_days: int = 3
    enable_hedging: bool = True
    rebalance_gas_limit: float = 25.0  # Max gas cost in USD

class UniswapV3Agent:
    UNIV3_QUOTER = "0x61fFE014bA17989E743c5F6cB21bF9697530B21e"
    NPM_ADDRESS  = "0xC36442b4a4522E871399CD717aBDD847Ab11FE88"

    def __init__(self, config: AgentConfig):
        self.cfg    = config
        self.w3     = Web3(Web3.HTTPProvider(config.rpc_url))
        self.wallet = self.w3.eth.account.from_key(config.private_key)
        self.position: Optional[V3Position] = None
        self.hedger = None
        self.stats  = {"rebalances": 0, "fees_collected": 0.0, "il_total": 0.0}

    async def get_current_price(self) -> float:
        """Fetch current ETH/USDC price from Uniswap V3 pool slot0."""
        # In production: read slot0 from pool contract
        # slot0 returns sqrtPriceX96; price = (sqrtPriceX96 / 2**96) ** 2
        # ... (simplified here)
        return 2000.0  # Placeholder

    async def compute_optimal_range(self, current_price: float) -> Tuple[int, int]:
        """Compute tick bounds for optimal range given current vol."""
        daily_vol = await self.estimate_realised_vol(window_days=14)
        sigma = daily_vol * math.sqrt(self.cfg.horizon_days)

        lower = current_price * math.exp(-self.cfg.range_sigma * sigma)
        upper = current_price * math.exp(+self.cfg.range_sigma * sigma)

        spacing = {100: 1, 500: 10, 3000: 60, 10000: 200}[self.cfg.fee_tier]

        def price_to_aligned_tick(p, spacing, round_down=True):
            raw = math.log(p) / math.log(1.0001)
            if round_down:
                return math.floor(raw / spacing) * spacing
            return math.ceil(raw / spacing) * spacing

        tick_lower = price_to_aligned_tick(lower, spacing, round_down=True)
        tick_upper = price_to_aligned_tick(upper, spacing, round_down=False)
        return tick_lower, tick_upper

    async def estimate_realised_vol(self, window_days: int) -> float:
        """Estimate realised daily vol from historical price data."""
        # In production: fetch OHLC from oracle or DEX subgraph
        return 0.05  # 5% daily vol placeholder

    async def open_position(self, tick_lower: int, tick_upper: int):
        """Mint a new V3 LP position."""
        log.info(f"Opening V3 position: ticks [{tick_lower}, {tick_upper}]")
        # 1. Approve token0 and token1 to NPM
        # 2. Call NPM.mint() with params
        # 3. Store returned tokenId
        self.position = V3Position(
            pool_address="0x...",
            token0="USDC", token1="WETH",
            fee_tier=self.cfg.fee_tier,
            tick_lower=tick_lower, tick_upper=tick_upper,
            liquidity=0, position_id=12345
        )
        if self.cfg.enable_hedging:
            self.hedger = GammaHedger(self.cfg.pf_api_key, self.position)
        log.info("Position opened successfully")

    async def close_position(self):
        """Remove liquidity and collect all fees."""
        if not self.position:
            return
        log.info(f"Closing position {self.position.position_id}")
        # 1. Call NPM.decreaseLiquidity(tokenId, 100% of liquidity)
        # 2. Call NPM.collect(tokenId, type(uint128).max, type(uint128).max)
        # 3. Optionally burn NFT
        self.position = None

    async def run(self):
        """Main agent loop."""
        log.info(f"UniswapV3Agent starting | capital: ${self.cfg.capital_usdc:,.0f}")

        # Initial position
        price = await self.get_current_price()
        tl, tu = await self.compute_optimal_range(price)
        await self.open_position(tl, tu)
        entry_price = price

        while True:
            try:
                price = await self.get_current_price()
                pos   = self.position

                if pos is None:
                    await asyncio.sleep(60)
                    continue

                Pa, Pb = pos.Pa, pos.Pb

                # Check if rebalance needed
                out_of_range = price <= Pa or price >= Pb
                center = (Pa + Pb) / 2
                drift  = abs(price - center) / (Pb - Pa)

                should_rebalance = out_of_range or drift > 0.20

                if should_rebalance:
                    log.info(f"Rebalancing at P={price:.2f} (range: {Pa:.2f}-{Pb:.2f})")
                    fees = await self.collect_fees_estimate()

                    if fees >= self.cfg.rebalance_gas_limit * 1.5:
                        await self.close_position()
                        tl, tu = await self.compute_optimal_range(price)
                        await self.open_position(tl, tu)
                        self.stats["rebalances"] += 1
                        self.stats["fees_collected"] += fees

                # Hedge if enabled
                if self.hedger:
                    hedge_result = self.hedger.rehedge(price)
                    if hedge_result["hedged"]:
                        log.info(f"Hedge adjusted: {hedge_result['adjustment']:.4f} ETH")

                log.info(
                    f"P={price:.2f} | Range=[{Pa:.0f},{Pb:.0f}] | "
                    f"Rebalances={self.stats['rebalances']} | "
                    f"Fees=${self.stats['fees_collected']:.2f}"
                )

            except Exception as e:
                log.error(f"Error in main loop: {e}")

            await asyncio.sleep(60)  # Check every minute

    async def collect_fees_estimate(self) -> float:
        """Estimate pending fees via staticcall."""
        return 45.0  # Placeholder

# Run the agent
if __name__ == "__main__":
    config = AgentConfig(
        rpc_url="https://mainnet.infura.io/v3/YOUR_KEY",
        private_key="0x...",
        pf_api_key="pf_live_your_key_here",
        capital_usdc=10_000,
        enable_hedging=True
    )
    agent = UniswapV3Agent(config)
    asyncio.run(agent.run())

9. Advanced Strategies: JIT Liquidity and MEV

Just-In-Time (JIT) liquidity is a MEV strategy where an agent adds concentrated liquidity in the block immediately before a large swap and removes it in the block after, capturing the fee without bearing any sustained IL risk. This requires:

JIT is highly competitive and increasingly filtered by aggregators, but remains viable on L2s and with private order flow relationships. An agent implementing JIT needs to solve the optimization:

def jit_profit(swap_size_usd, fee_tier, pool_total_liq, jit_liq):
    """
    Profit from a JIT operation.
    All liquidity in active tick at the moment of swap earns fees proportionally.
    """
    jit_share = jit_liq / (pool_total_liq + jit_liq)
    fee_earned = swap_size_usd * fee_tier * jit_share
    gas_cost   = 0.10  # On L2, gas for mint + burn
    return fee_earned - gas_cost

# JIT is profitable when:
# swap_size * fee * jit_share > gas_cost
# Minimum viable swap size:
min_swap = 0.10 / (0.003 * 0.01)  # 10% of pool, 0.3% fee, $0.10 gas
print(f"Min profitable swap: ${min_swap:,.0f}")  # ~$333

Trade Perpetuals to Hedge Your V3 Positions

Purple Flea offers 275+ perpetual futures markets with sub-second execution — ideal for delta hedging and gamma hedging V3 LP positions.

Start Trading View Perp Docs

10. Performance Tracking and Attribution

Measuring V3 LP performance requires careful accounting. The naive approach of comparing portfolio value at T1 to T0 conflates IL, fee income, and market movement. Proper attribution separates these components:

from dataclasses import dataclass

@dataclass
class LPPerformanceReport:
    period_days: float
    entry_price: float
    exit_price: float
    entry_value_usd: float
    exit_value_usd: float
    fees_collected_usd: float
    gas_costs_usd: float
    hedge_pnl_usd: float

    @property
    def hodl_return(self):
        """What a 50/50 HODL would have returned."""
        price_change = self.exit_price / self.entry_price
        return 0.5 * price_change + 0.5 - 1

    @property
    def lp_return(self):
        """Pure LP return before fees."""
        return (self.exit_value_usd - self.fees_collected_usd) / self.entry_value_usd - 1

    @property
    def impermanent_loss(self):
        return self.lp_return - self.hodl_return

    @property
    def net_return(self):
        """Total return including fees, gas, and hedge."""
        return (self.exit_value_usd + self.fees_collected_usd +
                self.hedge_pnl_usd - self.gas_costs_usd) / self.entry_value_usd - 1

    @property
    def fee_apy(self):
        return self.fees_collected_usd / self.entry_value_usd / self.period_days * 365

    def summary(self):
        print(f"=== LP Performance Report ===")
        print(f"Period:          {self.period_days:.1f} days")
        print(f"Price change:    {(self.exit_price/self.entry_price-1)*100:+.2f}%")
        print(f"HODL return:     {self.hodl_return*100:+.2f}%")
        print(f"LP return:       {self.lp_return*100:+.2f}%")
        print(f"Imperm. loss:    {self.impermanent_loss*100:+.2f}%")
        print(f"Fee APY:         {self.fee_apy*100:.1f}%")
        print(f"Net return:      {self.net_return*100:+.2f}%")