How LPSignal backtests concentrated liquidity
Net APR = fee APR − impermanent loss: what one unit of liquidity in a given price range actually earned, after the value it lost against simply holding the two tokens. LPSignal measures it every hour, for every covered pool and a set of ranges, from on-chain data only.
In short
- Exact fees. Read from each pool's fee-growth counters at the last block of every hour, dynamic fees included — never estimated from trading volume for a signal.
- Impermanent loss included. At the end of each window the position is compared with holding the tokens it was opened with.
- Specific ranges. Three or four range widths per pool type, aligned to the pool's tick spacing, over the last 1 hour, 24 hours, 7 days and 30 days.
- Scored in public. Every opportunity signal is backtested again 7 days later on the exact ticks it recommended; losing signals stay on the track record.
- Coverage. Uniswap v3 and v4, PancakeSwap v3, Aerodrome and Velodrome Slipstream on Ethereum, BNB Chain, Base, Arbitrum, Optimism and Polygon; pools of established tokens only.
Why the APR on DEX sites is not what an LP earns
DEX and analytics sites usually show a pool-level fee APR: the last day's fees divided by all the liquidity in the pool, annualised. For a concentrated-liquidity pool that number answers none of an LP's questions:
- It averages over every position, including those out of range that earn nothing. A tight range that stays in range earns several times the average; one that the price leaves earns zero.
- It ignores impermanent loss. On a volatile pair, a range can pay a high fee APR and still be worth less than holding the tokens.
- It does not say which range to use.
LPSignal answers the question directly: for this pool and this range, what would a position have returned, after impermanent loss, over the last week?
Step 1: exact fees from fee growth
Every Uniswap v3-style pool keeps cumulative counters of the fees earned per unit of liquidity (feeGrowthGlobal0X128 and feeGrowthGlobal1X128). LPSignal reads them at the last block of every hour; the difference between two hours, divided by 2128, is exactly what one unit of in-range liquidity earned in that hour — after the protocol's cut, with dynamic fees included.
Estimating fees from Swap events (volume × fee rate) is close on plain Uniswap and PancakeSwap pools — within about 1% in our measurements on Base — but was off by up to 1.8× on Aerodrome Slipstream, where the fee is dynamic, part of the liquidity is staked and swaps cross ticks. That is why only windows with exact fee data can fire a signal.
Step 2: ranges and windows
Each pool is tested on range widths that suit its type. A range of ±5% covers prices from 5% below to 5% above the price at entry, rounded outwards to the pool's tick spacing; a signal names the exact ticks.
| Pool type | Examples | Ranges tested |
|---|---|---|
| Stable | USDT/USDC | ±0.05% · ±0.1% · ±0.5% |
| Correlated | wstETH/WETH, cbBTC/WETH | ±0.5% · ±1% · ±3% |
| Volatile | WETH/USDC, WBNB/USDT | ±5% · ±10% · ±20% · full range |
Every range is backtested over the last 1 hour, 24 hours, 7 days and 30 days, recomputed every hour.
Step 3: impermanent loss and time in range
The backtest opens one unit of liquidity in the range at the window's opening price. Every hour whose prices stayed inside the range is credited that hour's exact fees; hours outside it earn nothing and still count in the average, and an hour whose price crossed an edge of the range is not credited (when such hours carry too much of the fees, the result is marked as estimated). At the end of the window the position is valued at the closing price and compared with holding the tokens it was opened with: the difference is the impermanent loss. Both are annualised over the window: net APR = fee APR − impermanent loss.
Step 4: from backtest to signal
A pool fires an opportunity signal when one of its ranges meets all of these at the top of an hour, on exact data only:
| Condition | Stable | Correlated | Volatile |
|---|---|---|---|
| Net APR over the last 7 days | ≥ 8% | ≥ 10% | ≥ 30% |
| Net APR over the last 24 hours | ≥ 8% | ≥ 10% | ≥ 30% |
| Time in range over 7 days | ≥ 80% | ||
After a signal, the same pool stays quiet for 72 hours unless its 7-day net APR climbs to 1.5 times the figure at the last signal. A chain more than 2 hours behind fires nothing, so no signal is ever about stale data.
Step 5: every signal is scored in public
Seven days after an opportunity signal fires, LPSignal backtests exactly the ticks it recommended over those seven days and publishes the realised net APR next to the figure at the signal — for every signal, the losing ones included. A signal whose week lacks exact fee data is shown as unscored and left out of the averages, so the track record is never padded with estimates. See the track record.
What a backtest does not include
- The future. A range that paid well last week can lose money next week if the price breaks out of it.
- Gas and the cost of swapping into the position.
- Aerodrome and Velodrome gauge emissions: figures are for unstaked liquidity; the staking rate is shown for reference only.
- Uniswap v4 pools with hooks or dynamic fees (not covered).
Questions
What is net APR for a liquidity provider?
The annualised return of a liquidity position after impermanent loss: the fees it earned minus the value it lost against simply holding the two tokens. LPSignal computes it per pool and per price range from hourly backtests.
How do you backtest a Uniswap v3 position?
Take the pool's hourly history, open one unit of liquidity in the chosen range at the window's opening price, credit it the exact fees from the pool's fee-growth counters for every hour the price stayed inside the range, and at the end compare its value with holding the tokens it was opened with. LPSignal does this every hour for every covered pool.
Why is fee APR misleading for concentrated liquidity?
Pool-level fee APR divides fees by all the liquidity in the pool, in range or not, and ignores impermanent loss. A specific range can earn far more or nothing at all, and on a volatile pair the loss can exceed the fees.
Can net APR be negative?
Yes. When the price moves far enough, impermanent loss exceeds the fees earned and the position is worth less than holding the tokens. LPSignal shows those ranges too, and keeps losing signals on its track record.