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.
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 Tier | Tick Spacing | Min Range Width | Best For |
|---|---|---|---|
| 0.01% | 1 | 0.01% | Stablecoin pairs (USDC/USDT) |
| 0.05% | 10 | 0.1% | Correlated pairs (WBTC/ETH, stETH/ETH) |
| 0.30% | 60 | 0.6% | Standard pairs (ETH/USDC) |
| 1.00% | 200 | 2.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 Tier | Pool Examples | Typical Daily Vol | IL Risk | Agent Strategy |
|---|---|---|---|---|
| 0.01% | USDC/USDT, DAI/USDC | $500M+ | Near zero | Ultra-tight range, high rebalance frequency |
| 0.05% | ETH/USDC, WBTC/USDC | $200M–$1B | Medium | 2–7 day range, volatility-adjusted width |
| 0.30% | WBTC/ETH, LINK/ETH | $50M–$200M | Medium-High | Wider ranges, less frequent rebalance |
| 1.00% | New tokens, exotic pairs | $1M–$50M | High | Very 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}%")
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}
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:
- Access to the mempool (or a block builder relationship) to detect pending large swaps
- Sufficient capital to provide meaningful liquidity in a single tick
- Extremely narrow range centered on the current price for maximum fee capture
- Atomic mint + swap + burn in a single transaction or two consecutive ones
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 Docs10. 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}%")