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,14 +1,15 @@
// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity ^0.8.19;
import { IUniswapV3MintCallback } from "@uniswap-v3-core/interfaces/callback/IUniswapV3MintCallback.sol";
import { IUniswapV3SwapCallback } from "@uniswap-v3-core/interfaces/callback/IUniswapV3SwapCallback.sol";
import { IUniswapV3Pool } from "@uniswap-v3-core/interfaces/IUniswapV3Pool.sol";
import { TickMath } from "@aperture/uni-v3-lib/TickMath.sol";
import { Vm } from "forge-std/Vm.sol";
import { console2 } from "forge-std/console2.sol";
import { MockToken } from "./MockToken.sol";
import { StrategyExecutor } from "./StrategyExecutor.sol";
import { TickMath } from "@aperture/uni-v3-lib/TickMath.sol";
import { IUniswapV3Pool } from "@uniswap-v3-core/interfaces/IUniswapV3Pool.sol";
import { IUniswapV3MintCallback } from "@uniswap-v3-core/interfaces/callback/IUniswapV3MintCallback.sol";
import { IUniswapV3SwapCallback } from "@uniswap-v3-core/interfaces/callback/IUniswapV3SwapCallback.sol";
import { Vm } from "forge-std/Vm.sol";
import { console2 } from "forge-std/console2.sol";
/**
* @title EventReplayer
@ -107,13 +108,7 @@ contract EventReplayer is IUniswapV3MintCallback, IUniswapV3SwapCallback {
_replayWithStrategy(eventsFile, totalEvents, strategyExecutor);
}
function _replayWithStrategy(
string memory eventsFile,
uint256 totalEvents,
StrategyExecutor strategyExecutor
)
internal
{
function _replayWithStrategy(string memory eventsFile, uint256 totalEvents, StrategyExecutor strategyExecutor) internal {
uint256 idx = 0;
// Track the last Swap event's expected state for drift measurement.
@ -178,9 +173,8 @@ contract EventReplayer is IUniswapV3MintCallback, IUniswapV3SwapCallback {
if (absDrift > maxDrift) maxDrift = absDrift;
// Log sqrtPrice deviation when it exceeds ~0.01% (filters rounding noise).
if (finalSqrtPrice != lastExpectedSqrtPrice) {
uint256 priceDelta = finalSqrtPrice > lastExpectedSqrtPrice
? uint256(finalSqrtPrice - lastExpectedSqrtPrice)
: uint256(lastExpectedSqrtPrice - finalSqrtPrice);
uint256 priceDelta =
finalSqrtPrice > lastExpectedSqrtPrice ? uint256(finalSqrtPrice - lastExpectedSqrtPrice) : uint256(lastExpectedSqrtPrice - finalSqrtPrice);
if (lastExpectedSqrtPrice > 0 && priceDelta * 10_000 > uint256(lastExpectedSqrtPrice)) {
console2.log(" final sqrtPrice divergence:", priceDelta);
}
@ -392,14 +386,7 @@ contract EventReplayer is IUniswapV3MintCallback, IUniswapV3SwapCallback {
/**
* @notice Emit a progress line and accumulate drift statistics for one checkpoint.
*/
function _logCheckpoint(
uint256 idx,
uint256 totalEvents,
int24 expectedTick,
uint160 expectedSqrtPrice
)
internal
{
function _logCheckpoint(uint256 idx, uint256 totalEvents, int24 expectedTick, uint160 expectedSqrtPrice) internal {
(uint160 currentSqrtPrice, int24 currentTick,,,,,) = pool.slot0();
int256 diff = int256(currentTick) - int256(expectedTick);
uint256 absDrift = diff >= 0 ? uint256(diff) : uint256(-diff);
@ -425,9 +412,8 @@ contract EventReplayer is IUniswapV3MintCallback, IUniswapV3SwapCallback {
// Log sqrtPrice deviation when it exceeds ~0.01% (filters rounding noise).
if (currentSqrtPrice != expectedSqrtPrice) {
uint256 priceDelta = currentSqrtPrice > expectedSqrtPrice
? uint256(currentSqrtPrice - expectedSqrtPrice)
: uint256(expectedSqrtPrice - currentSqrtPrice);
uint256 priceDelta =
currentSqrtPrice > expectedSqrtPrice ? uint256(currentSqrtPrice - expectedSqrtPrice) : uint256(expectedSqrtPrice - currentSqrtPrice);
if (expectedSqrtPrice > 0 && priceDelta * 10_000 > uint256(expectedSqrtPrice)) {
console2.log(" sqrtPrice divergence:", priceDelta);
}