fix: Backtesting #5: Position tracking + P&L metrics (#319)

- Add PositionTracker.sol: tracks position lifecycle (open/close per
  recenter), records tick ranges, liquidity, entry/exit blocks/timestamps,
  token amounts (via LiquidityAmounts math), fees (proportional to
  liquidity share), IL (LP exit value − HODL value at exit price), and
  net P&L per position. Aggregates total fees, cumulative IL, net P&L,
  rebalance count, Anchor time-in-range, and capital efficiency accumulators.
  Logs with [TRACKER][TYPE] prefix; emits cumulative P&L every 500 blocks.

- Modify StrategyExecutor.sol: add IUniswapV3Pool + token0isWeth to
  constructor (creates PositionTracker internally), call
  tracker.notifyBlock() on every block for time-in-range, and call
  tracker.recordRecenter() on each successful recenter. logSummary()
  now delegates to tracker.logFinalSummary().

- Modify BacktestRunner.s.sol: pass sp.pool and token0isWeth to
  StrategyExecutor constructor; log tracker address.

- forge fmt: reformat all backtesting scripts and affected src/test files
  to project style (number_underscore=thousands, multiline_func_header=all).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
openhands 2026-02-27 11:23:18 +00:00
parent a491ecb7d9
commit cfcf750084
12 changed files with 666 additions and 244 deletions

View file

@ -1,10 +1,12 @@
// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity ^0.8.19;
import { console2 } from "forge-std/console2.sol";
import { IERC20 } from "@openzeppelin/token/ERC20/IERC20.sol";
import { LiquidityManager } from "../../src/LiquidityManager.sol";
import { ThreePositionStrategy } from "../../src/abstracts/ThreePositionStrategy.sol";
import { PositionTracker } from "./PositionTracker.sol";
import { IERC20 } from "@openzeppelin/token/ERC20/IERC20.sol";
import { IUniswapV3Pool } from "@uniswap-v3-core/interfaces/IUniswapV3Pool.sol";
import { console2 } from "forge-std/console2.sol";
/**
* @title StrategyExecutor
@ -17,6 +19,10 @@ import { ThreePositionStrategy } from "../../src/abstracts/ThreePositionStrategy
* - Fees collected (WETH + KRK) as the delta of feeDestination's balances
* - Revert reason on failure (logged and skipped replay never halts)
*
* Position tracking and P&L metrics are delegated to PositionTracker, which is
* notified on every block (for time-in-range) and on each successful recenter
* (for position lifecycle and fee/IL accounting).
*
* Access model: StrategyExecutor must be set as recenterAccess on the LM so that
* the cooldown and TWAP price-stability checks are bypassed in the simulation
* (vm.warp advances simulated time, not real oracle state).
@ -37,6 +43,8 @@ contract StrategyExecutor {
address public immutable feeDestination;
/// @notice Minimum block gap between recenter attempts.
uint256 public immutable recenterInterval;
/// @notice Position tracker records lifecycle, fees, and P&L for each position.
PositionTracker public immutable tracker;
// -------------------------------------------------------------------------
// Runtime state
@ -55,13 +63,16 @@ contract StrategyExecutor {
IERC20 _wethToken,
IERC20 _krkToken,
address _feeDestination,
uint256 _recenterInterval
uint256 _recenterInterval,
IUniswapV3Pool _pool,
bool _token0isWeth
) {
lm = _lm;
wethToken = _wethToken;
krkToken = _krkToken;
feeDestination = _feeDestination;
recenterInterval = _recenterInterval;
tracker = new PositionTracker(_pool, _token0isWeth);
}
// -------------------------------------------------------------------------
@ -70,9 +81,13 @@ contract StrategyExecutor {
/**
* @notice Attempt a recenter if enough blocks have elapsed since the last one.
* Always notifies the tracker of the current block for time-in-range accounting.
* @param blockNum Current block number (as advanced by EventReplayer via vm.roll).
*/
function maybeRecenter(uint256 blockNum) external {
// Always notify the tracker so time-in-range is counted for every observed block.
tracker.notifyBlock(blockNum);
if (blockNum - lastRecenterBlock < recenterInterval) return;
// Snapshot pre-recenter positions.
@ -103,9 +118,7 @@ contract StrategyExecutor {
}
if (!success) {
console2.log(
string.concat("[recenter SKIP @ block ", _str(blockNum), "] reason: ", failReason)
);
console2.log(string.concat("[recenter SKIP @ block ", _str(blockNum), "] reason: ", failReason));
return;
}
@ -117,14 +130,56 @@ contract StrategyExecutor {
uint256 feesWeth = wethToken.balanceOf(feeDestination) - wethPre;
uint256 feesKrk = krkToken.balanceOf(feeDestination) - krkPre;
// Record recenter in position tracker.
tracker.recordRecenter(
PositionTracker.PositionSnapshot({
floorLiq: fLiqPre,
floorLo: fLoPre,
floorHi: fHiPre,
anchorLiq: aLiqPre,
anchorLo: aLoPre,
anchorHi: aHiPre,
discLiq: dLiqPre,
discLo: dLoPre,
discHi: dHiPre
}),
PositionTracker.PositionSnapshot({
floorLiq: fLiqPost,
floorLo: fLoPost,
floorHi: fHiPost,
anchorLiq: aLiqPost,
anchorLo: aLoPost,
anchorHi: aHiPost,
discLiq: dLiqPost,
discLo: dLoPost,
discHi: dHiPost
}),
feesWeth,
feesKrk,
blockNum,
block.timestamp
);
_logRecenter(
blockNum,
fLiqPre, fLoPre, fHiPre,
aLiqPre, aLoPre, aHiPre,
dLiqPre, dLoPre, dHiPre,
fLiqPost, fLoPost, fHiPost,
aLiqPost, aLoPost, aHiPost,
dLiqPost, dLoPost, dHiPost,
fLiqPre,
fLoPre,
fHiPre,
aLiqPre,
aLoPre,
aHiPre,
dLiqPre,
dLoPre,
dHiPre,
fLiqPost,
fLoPost,
fHiPost,
aLiqPost,
aLoPost,
aHiPost,
dLiqPost,
dLoPost,
dHiPost,
feesWeth,
feesKrk
);
@ -146,36 +201,11 @@ contract StrategyExecutor {
console2.log("Total recenters: ", totalRecenters);
console2.log("Failed recenters: ", failedRecenters);
console2.log("Recenter interval:", recenterInterval, "blocks");
console2.log(
string.concat(
"Final Floor: tick [",
_istr(fLo),
", ",
_istr(fHi),
"] liq=",
_str(uint256(fLiq))
)
);
console2.log(
string.concat(
"Final Anchor: tick [",
_istr(aLo),
", ",
_istr(aHi),
"] liq=",
_str(uint256(aLiq))
)
);
console2.log(
string.concat(
"Final Discovery: tick [",
_istr(dLo),
", ",
_istr(dHi),
"] liq=",
_str(uint256(dLiq))
)
);
console2.log(string.concat("Final Floor: tick [", _istr(fLo), ", ", _istr(fHi), "] liq=", _str(uint256(fLiq))));
console2.log(string.concat("Final Anchor: tick [", _istr(aLo), ", ", _istr(aHi), "] liq=", _str(uint256(aLiq))));
console2.log(string.concat("Final Discovery: tick [", _istr(dLo), ", ", _istr(dHi), "] liq=", _str(uint256(dLiq))));
tracker.logFinalSummary(lastRecenterBlock);
}
// -------------------------------------------------------------------------
@ -184,84 +214,38 @@ contract StrategyExecutor {
function _logRecenter(
uint256 blockNum,
uint128 fLiqPre, int24 fLoPre, int24 fHiPre,
uint128 aLiqPre, int24 aLoPre, int24 aHiPre,
uint128 dLiqPre, int24 dLoPre, int24 dHiPre,
uint128 fLiqPost, int24 fLoPost, int24 fHiPost,
uint128 aLiqPost, int24 aLoPost, int24 aHiPost,
uint128 dLiqPost, int24 dLoPost, int24 dHiPost,
uint128 fLiqPre,
int24 fLoPre,
int24 fHiPre,
uint128 aLiqPre,
int24 aLoPre,
int24 aHiPre,
uint128 dLiqPre,
int24 dLoPre,
int24 dHiPre,
uint128 fLiqPost,
int24 fLoPost,
int24 fHiPost,
uint128 aLiqPost,
int24 aLoPost,
int24 aHiPost,
uint128 dLiqPost,
int24 dLoPost,
int24 dHiPost,
uint256 feesWeth,
uint256 feesKrk
)
internal
view
{
console2.log(
string.concat("=== Recenter #", _str(totalRecenters), " @ block ", _str(blockNum), " ===")
);
console2.log(
string.concat(
" Floor pre: tick [",
_istr(fLoPre),
", ",
_istr(fHiPre),
"] liq=",
_str(uint256(fLiqPre))
)
);
console2.log(
string.concat(
" Anchor pre: tick [",
_istr(aLoPre),
", ",
_istr(aHiPre),
"] liq=",
_str(uint256(aLiqPre))
)
);
console2.log(
string.concat(
" Disc pre: tick [",
_istr(dLoPre),
", ",
_istr(dHiPre),
"] liq=",
_str(uint256(dLiqPre))
)
);
console2.log(
string.concat(
" Floor post: tick [",
_istr(fLoPost),
", ",
_istr(fHiPost),
"] liq=",
_str(uint256(fLiqPost))
)
);
console2.log(
string.concat(
" Anchor post: tick [",
_istr(aLoPost),
", ",
_istr(aHiPost),
"] liq=",
_str(uint256(aLiqPost))
)
);
console2.log(
string.concat(
" Disc post: tick [",
_istr(dLoPost),
", ",
_istr(dHiPost),
"] liq=",
_str(uint256(dLiqPost))
)
);
console2.log(
string.concat(" Fees WETH: ", _str(feesWeth), " Fees KRK: ", _str(feesKrk))
);
console2.log(string.concat("=== Recenter #", _str(totalRecenters), " @ block ", _str(blockNum), " ==="));
console2.log(string.concat(" Floor pre: tick [", _istr(fLoPre), ", ", _istr(fHiPre), "] liq=", _str(uint256(fLiqPre))));
console2.log(string.concat(" Anchor pre: tick [", _istr(aLoPre), ", ", _istr(aHiPre), "] liq=", _str(uint256(aLiqPre))));
console2.log(string.concat(" Disc pre: tick [", _istr(dLoPre), ", ", _istr(dHiPre), "] liq=", _str(uint256(dLiqPre))));
console2.log(string.concat(" Floor post: tick [", _istr(fLoPost), ", ", _istr(fHiPost), "] liq=", _str(uint256(fLiqPost))));
console2.log(string.concat(" Anchor post: tick [", _istr(aLoPost), ", ", _istr(aHiPost), "] liq=", _str(uint256(aLiqPost))));
console2.log(string.concat(" Disc post: tick [", _istr(dLoPost), ", ", _istr(dHiPost), "] liq=", _str(uint256(dLiqPost))));
console2.log(string.concat(" Fees WETH: ", _str(feesWeth), " Fees KRK: ", _str(feesKrk)));
}
// -------------------------------------------------------------------------