fix: Address AI review findings for backtesting baseline strategies (#320)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
5205ea6f4a
commit
9061f8e8f6
2 changed files with 87 additions and 7 deletions
|
|
@ -4,6 +4,7 @@ pragma solidity ^0.8.19;
|
|||
import { IERC20 } from "@openzeppelin/token/ERC20/IERC20.sol";
|
||||
import { IUniswapV3Pool } from "@uniswap-v3-core/interfaces/IUniswapV3Pool.sol";
|
||||
import { TickMath } from "@aperture/uni-v3-lib/TickMath.sol";
|
||||
import { LiquidityAmounts } from "@aperture/uni-v3-lib/LiquidityAmounts.sol";
|
||||
|
||||
import { Kraiken } from "../../src/Kraiken.sol";
|
||||
import { LiquidityManager } from "../../src/LiquidityManager.sol";
|
||||
|
|
@ -66,6 +67,11 @@ contract BacktestRunner is Reporter {
|
|||
uint256 internal kraikenBlocksInRange;
|
||||
uint256 internal kraikenTotalBlocks;
|
||||
|
||||
// Snapshot of LM position composition at simulation start (for IL calculation)
|
||||
uint256 internal kraikenInitialValue; // total WETH-equivalent value at start
|
||||
uint256 internal kraikenInitialWeth; // raw WETH portion at start (positions + balance)
|
||||
uint256 internal kraikenInitialToken; // KRK portion at start (positions + balance)
|
||||
|
||||
// ── Simulation bookkeeping ────────────────────────────────────────────────
|
||||
|
||||
uint256 internal simStartBlock;
|
||||
|
|
@ -104,7 +110,9 @@ contract BacktestRunner is Reporter {
|
|||
|
||||
// First recenter establishes the three-position structure
|
||||
vm.prank(feesDest);
|
||||
try lm.recenter() { } catch { }
|
||||
try lm.recenter() { } catch {
|
||||
console2.log("WARNING: initial recenter failed -- LM positions may be uninitialised");
|
||||
}
|
||||
|
||||
console2.log("Pool:", address(pool));
|
||||
console2.log("token0isWeth:", token0isWeth);
|
||||
|
|
@ -163,6 +171,10 @@ contract BacktestRunner is Reporter {
|
|||
simStartBlock = block.number;
|
||||
simStartTimestamp = block.timestamp;
|
||||
|
||||
// Snapshot LM position value at simulation start for fair P&L and IL measurement
|
||||
(uint160 sqrtInit,,,,,, ) = pool.slot0();
|
||||
(kraikenInitialValue, kraikenInitialWeth, kraikenInitialToken) = _getLmPositionStats(sqrtInit);
|
||||
|
||||
console2.log("Strategies initialised.");
|
||||
console2.log(" Full-Range LP: WETH", INITIAL_CAPITAL / 2 / 1e18, "| KRK", krkPerStrategy / 1e18);
|
||||
console2.log(" Fixed-Width LP: WETH", INITIAL_CAPITAL / 2 / 1e18, "| KRK", krkPerStrategy / 1e18);
|
||||
|
|
@ -270,11 +282,31 @@ contract BacktestRunner is Reporter {
|
|||
uint256 fwTimeInRangeBps = fixedWidthLp.totalBlocks() > 0 ? fixedWidthLp.blocksInRange() * 10_000 / fixedWidthLp.totalBlocks() : 0;
|
||||
|
||||
// ── KrAIken 3-position strategy ────────────────────────────────────
|
||||
// feesDest is the configured feeDestination for this LM; _scrapePositions() transfers
|
||||
// fee0 and fee1 directly to feeDestination on every recenter, so these balances are
|
||||
// the actual LP trading fees distributed to the protocol over the simulation period.
|
||||
uint256 kraikenFeesWeth = weth.balanceOf(feesDest);
|
||||
uint256 kraikenFeesToken = kraiken.balanceOf(feesDest);
|
||||
uint256 kraikenFeesValue = kraikenFeesWeth + (kraikenFeesToken * krkPrice / 1e18);
|
||||
uint256 kraikenFinalValue = INITIAL_CAPITAL + kraikenFeesValue;
|
||||
int256 kraikenPnlBps = int256(kraikenFeesValue) * 10_000 / int256(INITIAL_CAPITAL);
|
||||
|
||||
// Measure the current market value of the LM's live positions plus its raw balances.
|
||||
// The LM's positions remain open at end of simulation (no finalize() call), so we
|
||||
// must compute their value using tick math rather than burning them.
|
||||
(uint256 kraikenEndPositionValue, , ) = _getLmPositionStats(sqrtFinal);
|
||||
|
||||
// Final value = open position value + fees already distributed to feesDest
|
||||
uint256 kraikenFinalValue = kraikenEndPositionValue + kraikenFeesValue;
|
||||
|
||||
// P&L relative to measured start value (not a hardcoded constant)
|
||||
int256 kraikenPnlBps = (int256(kraikenFinalValue) - int256(kraikenInitialValue)) * 10_000
|
||||
/ int256(kraikenInitialValue > 0 ? kraikenInitialValue : 1);
|
||||
|
||||
// IL: compare open position value (fees already excluded — they left to feesDest) to
|
||||
// HODL of the same initial token split at final price.
|
||||
uint256 kraikenHodlFinalValue = kraikenInitialWeth + (kraikenInitialToken * krkPrice / 1e18);
|
||||
int256 kraikenIlBps = (int256(kraikenEndPositionValue) - int256(kraikenHodlFinalValue)) * 10_000
|
||||
/ int256(kraikenHodlFinalValue > 0 ? kraikenHodlFinalValue : 1);
|
||||
|
||||
uint256 kraikenTimeInRangeBps = kraikenTotalBlocks > 0 ? kraikenBlocksInRange * 10_000 / kraikenTotalBlocks : 0;
|
||||
|
||||
// ── Assemble reports ───────────────────────────────────────────────
|
||||
|
|
@ -282,12 +314,12 @@ contract BacktestRunner is Reporter {
|
|||
|
||||
reports[0] = StrategyReport({
|
||||
name: "KrAIken 3-Pos",
|
||||
initialValueWeth: INITIAL_CAPITAL,
|
||||
initialValueWeth: kraikenInitialValue,
|
||||
finalValueWeth: kraikenFinalValue,
|
||||
feesEarnedWeth: kraikenFeesWeth,
|
||||
feesEarnedToken: kraikenFeesToken,
|
||||
rebalanceCount: kraikenRecenterCount,
|
||||
ilBps: 0, // KrAIken floor mechanism is designed to prevent IL below VWAP
|
||||
ilBps: kraikenIlBps,
|
||||
netPnlBps: kraikenPnlBps,
|
||||
timeInRangeBps: kraikenTimeInRangeBps,
|
||||
hasTimeInRange: true
|
||||
|
|
@ -356,6 +388,51 @@ contract BacktestRunner is Reporter {
|
|||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
// KrAIken position valuation
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// @notice Compute the total WETH-equivalent value of all three LM positions
|
||||
/// plus the LM's uninvested WETH balance, at the given sqrtPrice.
|
||||
/// @return totalValueWeth All holdings valued in WETH (wei)
|
||||
/// @return totalWeth Raw WETH in positions + LM balance (wei)
|
||||
/// @return totalKrk Raw KRK in positions + LM balance (base units)
|
||||
function _getLmPositionStats(uint160 sqrtPrice)
|
||||
internal
|
||||
view
|
||||
returns (uint256 totalValueWeth, uint256 totalWeth, uint256 totalKrk)
|
||||
{
|
||||
ThreePositionStrategy.Stage[3] memory stages = [
|
||||
ThreePositionStrategy.Stage.FLOOR,
|
||||
ThreePositionStrategy.Stage.ANCHOR,
|
||||
ThreePositionStrategy.Stage.DISCOVERY
|
||||
];
|
||||
|
||||
for (uint256 i = 0; i < 3; i++) {
|
||||
(uint128 liquidity, int24 tickLower, int24 tickUpper) = lm.positions(stages[i]);
|
||||
if (liquidity == 0) continue;
|
||||
|
||||
uint160 sqrtA = TickMath.getSqrtRatioAtTick(tickLower);
|
||||
uint160 sqrtB = TickMath.getSqrtRatioAtTick(tickUpper);
|
||||
(uint256 amount0, uint256 amount1) = LiquidityAmounts.getAmountsForLiquidity(sqrtPrice, sqrtA, sqrtB, liquidity);
|
||||
|
||||
if (token0isWeth) {
|
||||
totalWeth += amount0;
|
||||
totalKrk += amount1;
|
||||
} else {
|
||||
totalKrk += amount0;
|
||||
totalWeth += amount1;
|
||||
}
|
||||
}
|
||||
|
||||
// Include uninvested balances held directly by the LM
|
||||
totalWeth += weth.balanceOf(address(lm));
|
||||
totalKrk += kraiken.balanceOf(address(lm));
|
||||
|
||||
uint256 krkPrice = _krkPriceScaled(sqrtPrice, token0isWeth);
|
||||
totalValueWeth = totalWeth + (totalKrk * krkPrice / 1e18);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
// Swap helpers (direct pool interaction)
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
|
|
@ -422,7 +499,10 @@ contract BacktestRunner is Reporter {
|
|||
if (t0isWeth) {
|
||||
// price = KRK base units / WETH base units = sq^2 / 2^192
|
||||
// krkAmount = wethAmount × sq^2 / 2^192
|
||||
// Safe: p = sq >> 48 eliminates 2^48 from each factor before squaring
|
||||
// Approximation: shifting sq right by 48 before squaring avoids overflow at typical
|
||||
// pool prices (sqrtPriceX96 well below 2^128). At extreme tick boundaries
|
||||
// (sqrtPriceX96 near MAX_SQRT_RATIO ≈ 2^128) p would be ~2^80 and overflow uint256.
|
||||
// Acceptable for this simulation where the pool is initialised near 1 ETH : 240k KRK.
|
||||
uint256 p = sq >> 48;
|
||||
krkAmount = wethAmount * p * p >> 96;
|
||||
} else {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue