// SPDX-License-Identifier: MIT pragma solidity ^0.8.19; import "forge-std/Script.sol"; import "forge-std/console2.sol"; /// @notice Data for a single strategy's backtest result struct StrategyReport { string name; uint256 initialValueWeth; // wei uint256 finalValueWeth; // wei (position value + fees) uint256 feesEarnedWeth; // WETH-denominated fees (wei) uint256 feesEarnedToken; // Token-denominated fees (token base units) uint256 rebalanceCount; int256 ilBps; // Impermanent loss in basis points (0 or negative vs HODL) int256 netPnlBps; // Net P&L in basis points vs initial capital uint256 timeInRangeBps; // 0–10000 (10000 = 100%; ignored when hasTimeInRange=false) bool hasTimeInRange; // false for HODL } /// @notice Backtest configuration metadata written to the report struct BacktestConfig { address pool; uint256 startBlock; uint256 endBlock; uint256 startTimestamp; uint256 endTimestamp; uint256 initialCapitalWei; uint256 recenterInterval; } /// @title Reporter /// @notice Abstract mixin that generates Markdown and JSON backtest reports. /// Inherit alongside Script to get access to vm and file I/O. abstract contract Reporter is Script { // ── Public entry point ──────────────────────────────────────────────────── /// @notice Write markdown + JSON reports to script/backtesting/reports/. function _writeReport(StrategyReport[] memory reports, BacktestConfig memory config) internal { string memory ts = vm.toString(block.timestamp); string memory mdPath = string(abi.encodePacked("script/backtesting/reports/report-", ts, ".md")); string memory jsonPath = string(abi.encodePacked("script/backtesting/reports/report-", ts, ".json")); vm.writeFile(mdPath, _buildMarkdown(reports, config)); vm.writeFile(jsonPath, _buildJson(reports, config)); console2.log("\n=== Backtest Report ==="); console2.log("Markdown:", mdPath); console2.log(" JSON:", jsonPath); } // ── Markdown builder ────────────────────────────────────────────────────── function _buildMarkdown(StrategyReport[] memory reports, BacktestConfig memory config) internal view returns (string memory out) { out = "# Backtest Report: AERO/WETH 1%, 7 days\n\n"; // Table header out = string( abi.encodePacked( out, "| Strategy | Final Value | Fees Earned | Rebalances | IL (%) | Net P&L (%) | Time in Range |\n", "|---|---|---|---|---|---|---|\n" ) ); // Table rows for (uint256 i = 0; i < reports.length; i++) { out = string(abi.encodePacked(out, _markdownRow(reports[i]), "\n")); } // Configuration section out = string( abi.encodePacked( out, "\n## Configuration\n", "- Pool: `", vm.toString(config.pool), "`\n", "- Period: block ", vm.toString(config.startBlock), " to block ", vm.toString(config.endBlock), " (7 days simulated)\n", "- Initial capital: 10 ETH equivalent\n", "- Recenter interval: ", vm.toString(config.recenterInterval), " blocks\n" ) ); } function _markdownRow(StrategyReport memory r) internal view returns (string memory) { string memory tir = r.hasTimeInRange ? _fmtPct(r.timeInRangeBps) : "N/A"; return string( abi.encodePacked( "| ", r.name, " | ", _fmtEth(r.finalValueWeth), " | ", _fmtEth(r.feesEarnedWeth), " | ", vm.toString(r.rebalanceCount), " | ", _fmtSignedPct(r.ilBps), " | ", _fmtSignedPct(r.netPnlBps), " | ", tir, " |" ) ); } // ── JSON builder ────────────────────────────────────────────────────────── function _buildJson(StrategyReport[] memory reports, BacktestConfig memory config) internal view returns (string memory) { string memory strategiesArr = "["; for (uint256 i = 0; i < reports.length; i++) { if (i > 0) strategiesArr = string(abi.encodePacked(strategiesArr, ",")); strategiesArr = string(abi.encodePacked(strategiesArr, _strategyJson(reports[i]))); } strategiesArr = string(abi.encodePacked(strategiesArr, "]")); return string( abi.encodePacked( '{"report":{"timestamp":', vm.toString(block.timestamp), ',"strategies":', strategiesArr, '},"config":{"pool":"', vm.toString(config.pool), '","startBlock":', vm.toString(config.startBlock), ',"endBlock":', vm.toString(config.endBlock), ',"durationDays":7,"initialCapitalWei":"', vm.toString(config.initialCapitalWei), '","recenterInterval":', vm.toString(config.recenterInterval), "}}" ) ); } function _strategyJson(StrategyReport memory r) internal view returns (string memory) { string memory tir = r.hasTimeInRange ? vm.toString(r.timeInRangeBps) : '"N/A"'; return string( abi.encodePacked( '{"name":"', r.name, '","initialValueWei":"', vm.toString(r.initialValueWeth), '","finalValueWei":"', vm.toString(r.finalValueWeth), '","feesEarnedWei":"', vm.toString(r.feesEarnedWeth), '","rebalances":', vm.toString(r.rebalanceCount), ',"ilBps":', _intStr(r.ilBps), ',"netPnlBps":', _intStr(r.netPnlBps), ',"timeInRangeBps":', tir, "}" ) ); } // ── Formatting helpers ──────────────────────────────────────────────────── /// @dev Format wei amount as "X.XXXX ETH" function _fmtEth(uint256 weiAmt) internal view returns (string memory) { uint256 whole = weiAmt / 1e18; uint256 frac = (weiAmt % 1e18) * 10_000 / 1e18; return string(abi.encodePacked(vm.toString(whole), ".", _pad4(frac), " ETH")); } /// @dev Format basis points as signed percentage "+X.XX%" or "-X.XX%" function _fmtSignedPct(int256 bps) internal view returns (string memory) { if (bps < 0) { uint256 abs = uint256(-bps); return string(abi.encodePacked("-", vm.toString(abs / 100), ".", _pad2(abs % 100), "%")); } uint256 u = uint256(bps); return string(abi.encodePacked("+", vm.toString(u / 100), ".", _pad2(u % 100), "%")); } /// @dev Format basis points as percentage "X.XX%" function _fmtPct(uint256 bps) internal view returns (string memory) { return string(abi.encodePacked(vm.toString(bps / 100), ".", _pad2(bps % 100), "%")); } /// @dev Convert int256 to decimal string (no leading sign for positive) function _intStr(int256 n) internal view returns (string memory) { if (n < 0) { return string(abi.encodePacked("-", vm.toString(uint256(-n)))); } return vm.toString(uint256(n)); } /// @dev Pad to 4 decimal digits with leading zeros function _pad4(uint256 n) internal pure returns (string memory) { if (n >= 1000) return _numStr(n); if (n >= 100) return string(abi.encodePacked("0", _numStr(n))); if (n >= 10) return string(abi.encodePacked("00", _numStr(n))); return string(abi.encodePacked("000", _numStr(n))); } /// @dev Pad to 2 decimal digits with leading zeros function _pad2(uint256 n) internal pure returns (string memory) { if (n >= 10) return _numStr(n); return string(abi.encodePacked("0", _numStr(n))); } /// @dev Pure uint-to-decimal-string (avoids vm dependency for small helpers) function _numStr(uint256 n) internal pure returns (string memory) { if (n == 0) return "0"; uint256 len; uint256 tmp = n; while (tmp > 0) { len++; tmp /= 10; } bytes memory buf = new bytes(len); for (uint256 i = len; i > 0; i--) { buf[i - 1] = bytes1(uint8(48 + (n % 10))); n /= 10; } return string(buf); } }