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,15 +1,16 @@
// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity ^0.8.19;
import { BacktestKraiken } from "./BacktestKraiken.sol";
import { EventReplayer } from "./EventReplayer.sol";
import { KrAIkenDeployer, KrAIkenSystem } from "./KrAIkenDeployer.sol";
import { MockToken } from "./MockToken.sol";
import { ShadowPool, ShadowPoolDeployer } from "./ShadowPoolDeployer.sol";
import { StrategyExecutor } from "./StrategyExecutor.sol";
import { IERC20 } from "@openzeppelin/token/ERC20/IERC20.sol";
import { Script } from "forge-std/Script.sol";
import { console2 } from "forge-std/console2.sol";
import { IERC20 } from "@openzeppelin/token/ERC20/IERC20.sol";
import { MockToken } from "./MockToken.sol";
import { BacktestKraiken } from "./BacktestKraiken.sol";
import { ShadowPool, ShadowPoolDeployer } from "./ShadowPoolDeployer.sol";
import { EventReplayer } from "./EventReplayer.sol";
import { KrAIkenSystem, KrAIkenDeployer } from "./KrAIkenDeployer.sol";
import { StrategyExecutor } from "./StrategyExecutor.sol";
/**
* @title BacktestRunner
@ -77,14 +78,14 @@ contract BacktestRunner is Script {
// 1. Explicit env override.
try vm.envUint("INITIAL_SQRT_PRICE_X96") returns (uint256 val) {
if (val != 0) return uint160(val);
} catch {}
} catch { }
// 2. First Swap event in the events cache.
try vm.envString("BACKTEST_EVENTS_FILE") returns (string memory eventsFile) {
try this._parseSqrtPriceFromFile(eventsFile) returns (uint160 val) {
if (val != 0) return val;
} catch {}
} catch {}
} catch { }
} catch { }
// 3. Fallback default.
return DEFAULT_SQRT_PRICE_X96;
@ -101,12 +102,12 @@ contract BacktestRunner is Script {
uint256 recenterInterval = DEFAULT_RECENTER_INTERVAL;
try vm.envUint("RECENTER_INTERVAL") returns (uint256 v) {
if (v > 0) recenterInterval = v;
} catch {}
} catch { }
uint256 initialCapital = KrAIkenDeployer.DEFAULT_INITIAL_CAPITAL;
try vm.envUint("INITIAL_CAPITAL_WETH") returns (uint256 v) {
if (v > 0) initialCapital = v;
} catch {}
} catch { }
vm.startBroadcast();
@ -139,15 +140,15 @@ contract BacktestRunner is Script {
// 4. Set feeDestination = sender.
// 5. Mint initialCapital mock WETH to LM.
// ------------------------------------------------------------------
KrAIkenSystem memory sys =
KrAIkenDeployer.deploy(address(sp.factory), address(mockWeth), address(krk), sender, initialCapital);
KrAIkenSystem memory sys = KrAIkenDeployer.deploy(address(sp.factory), address(mockWeth), address(krk), sender, initialCapital);
// Deploy StrategyExecutor and grant it recenter access on the LM.
// recenterAccess bypasses TWAP stability check and cooldown correct
// for simulation where vm.warp drives time, not a real oracle.
// sender == feeDestination, so the onlyFeeDestination guard is satisfied.
bool token0isWeth = sp.token0 == address(mockWeth);
StrategyExecutor executor =
new StrategyExecutor(sys.lm, IERC20(address(mockWeth)), IERC20(address(krk)), sender, recenterInterval);
new StrategyExecutor(sys.lm, IERC20(address(mockWeth)), IERC20(address(krk)), sender, recenterInterval, sp.pool, token0isWeth);
sys.lm.setRecenterAccess(address(executor));
vm.stopBroadcast();
@ -185,9 +186,10 @@ contract BacktestRunner is Script {
console2.log("Optimizer: ", address(sys.optimizer));
console2.log("LiquidityMgr: ", address(sys.lm));
console2.log("StrategyExec: ", address(executor));
console2.log("PositionTracker:", address(executor.tracker()));
console2.log("Recenter intv: ", recenterInterval, " blocks");
console2.log("Initial capital:", initialCapital, " (mock WETH wei)");
console2.log("token0isWeth: ", sp.token0 == address(mockWeth));
console2.log("token0isWeth: ", token0isWeth);
// -----------------------------------------------------------------------
// Event replay (runs as local simulation no broadcast required).
@ -199,7 +201,7 @@ contract BacktestRunner is Script {
try vm.envString("BACKTEST_EVENTS_FILE") returns (string memory eventsFile) {
if (bytes(eventsFile).length > 0) {
// Reset file position _resolveSqrtPrice() may have consumed line 1.
try vm.closeFile(eventsFile) {} catch {}
try vm.closeFile(eventsFile) { } catch { }
// Pre-count events so replay() can show "[N/total]" progress lines.
uint256 totalEvents = 0;
@ -221,6 +223,6 @@ contract BacktestRunner is Script {
// Print final KrAIken strategy summary.
executor.logSummary();
}
} catch {}
} catch { }
}
}