Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
6526928b67
commit
5205ea6f4a
4 changed files with 915 additions and 0 deletions
229
onchain/script/backtesting/BaselineStrategies.sol
Normal file
229
onchain/script/backtesting/BaselineStrategies.sol
Normal file
|
|
@ -0,0 +1,229 @@
|
|||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.19;
|
||||
|
||||
import { IERC20 } from "@openzeppelin/token/ERC20/IERC20.sol";
|
||||
import { IUniswapV3Pool } from "@uniswap-v3-core/interfaces/IUniswapV3Pool.sol";
|
||||
import { IUniswapV3MintCallback } from "@uniswap-v3-core/interfaces/callback/IUniswapV3MintCallback.sol";
|
||||
import { LiquidityAmounts } from "@aperture/uni-v3-lib/LiquidityAmounts.sol";
|
||||
import { TickMath } from "@aperture/uni-v3-lib/TickMath.sol";
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Full-Range LP Strategy
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// @title FullRangeLPStrategy
|
||||
/// @notice Single LP position covering the entire valid tick range (MIN_TICK to MAX_TICK).
|
||||
/// Position is set once at initialization and never rebalanced, giving 100% time-in-range.
|
||||
contract FullRangeLPStrategy is IUniswapV3MintCallback {
|
||||
/// @dev Tick boundaries aligned to 200-spacing for the 1% fee pool
|
||||
int24 public constant TICK_LOWER = -887_200;
|
||||
int24 public constant TICK_UPPER = 887_200;
|
||||
|
||||
IUniswapV3Pool public immutable pool;
|
||||
IERC20 public immutable token0;
|
||||
IERC20 public immutable token1;
|
||||
bool public immutable token0isWeth;
|
||||
|
||||
uint128 public liquidity;
|
||||
uint256 public feesCollected0;
|
||||
uint256 public feesCollected1;
|
||||
uint256 public totalBlocks;
|
||||
bool public initialized;
|
||||
|
||||
constructor(IUniswapV3Pool _pool, IERC20 _weth, IERC20 _token, bool _token0isWeth) {
|
||||
pool = _pool;
|
||||
token0isWeth = _token0isWeth;
|
||||
token0 = _token0isWeth ? _weth : _token;
|
||||
token1 = _token0isWeth ? _token : _weth;
|
||||
}
|
||||
|
||||
/// @notice Initialize: use current token balances to mint a full-range position.
|
||||
/// Caller must transfer tokens to this contract before calling.
|
||||
function initialize() external {
|
||||
require(!initialized, "already initialized");
|
||||
initialized = true;
|
||||
|
||||
uint256 bal0 = token0.balanceOf(address(this));
|
||||
uint256 bal1 = token1.balanceOf(address(this));
|
||||
(, int24 currentTick,,,,,) = pool.slot0();
|
||||
uint160 sqrtCurrent = TickMath.getSqrtRatioAtTick(currentTick);
|
||||
uint160 sqrtLower = TickMath.getSqrtRatioAtTick(TICK_LOWER);
|
||||
uint160 sqrtUpper = TickMath.getSqrtRatioAtTick(TICK_UPPER);
|
||||
|
||||
liquidity = LiquidityAmounts.getLiquidityForAmounts(sqrtCurrent, sqrtLower, sqrtUpper, bal0, bal1);
|
||||
if (liquidity > 0) {
|
||||
pool.mint(address(this), TICK_LOWER, TICK_UPPER, liquidity, "");
|
||||
}
|
||||
}
|
||||
|
||||
/// @notice Record one simulation block. Full-range is always in-range, so this just increments totalBlocks.
|
||||
function recordBlock() external {
|
||||
totalBlocks++;
|
||||
}
|
||||
|
||||
/// @notice Collect accrued fees without changing liquidity.
|
||||
function collectFees() external {
|
||||
if (liquidity > 0) {
|
||||
pool.burn(TICK_LOWER, TICK_UPPER, 0);
|
||||
}
|
||||
(uint256 f0, uint256 f1) = pool.collect(address(this), TICK_LOWER, TICK_UPPER, type(uint128).max, type(uint128).max);
|
||||
feesCollected0 += f0;
|
||||
feesCollected1 += f1;
|
||||
}
|
||||
|
||||
/// @notice Burn entire position and collect all tokens + fees.
|
||||
function finalize() external {
|
||||
if (liquidity > 0) {
|
||||
pool.burn(TICK_LOWER, TICK_UPPER, liquidity);
|
||||
(uint256 f0, uint256 f1) = pool.collect(address(this), TICK_LOWER, TICK_UPPER, type(uint128).max, type(uint128).max);
|
||||
feesCollected0 += f0;
|
||||
feesCollected1 += f1;
|
||||
liquidity = 0;
|
||||
}
|
||||
}
|
||||
|
||||
function getWethBalance() external view returns (uint256) {
|
||||
return token0isWeth ? token0.balanceOf(address(this)) : token1.balanceOf(address(this));
|
||||
}
|
||||
|
||||
function getTokenBalance() external view returns (uint256) {
|
||||
return token0isWeth ? token1.balanceOf(address(this)) : token0.balanceOf(address(this));
|
||||
}
|
||||
|
||||
/// @inheritdoc IUniswapV3MintCallback
|
||||
function uniswapV3MintCallback(uint256 amount0Owed, uint256 amount1Owed, bytes calldata) external override {
|
||||
require(msg.sender == address(pool), "not pool");
|
||||
if (amount0Owed > 0) token0.transfer(msg.sender, amount0Owed);
|
||||
if (amount1Owed > 0) token1.transfer(msg.sender, amount1Owed);
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Fixed-Width LP Strategy
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// @title FixedWidthLPStrategy
|
||||
/// @notice LP position ±2000 ticks around current price (200-tick spacing).
|
||||
/// Rebalances (removes + remints) when price exits the range.
|
||||
contract FixedWidthLPStrategy is IUniswapV3MintCallback {
|
||||
int24 public constant HALF_WIDTH = 2_000; // ±2000 ticks from center
|
||||
int24 public constant TICK_SPACING = 200;
|
||||
int24 public constant MIN_TICK = -887_200;
|
||||
int24 public constant MAX_TICK = 887_200;
|
||||
|
||||
IUniswapV3Pool public immutable pool;
|
||||
IERC20 public immutable token0;
|
||||
IERC20 public immutable token1;
|
||||
bool public immutable token0isWeth;
|
||||
|
||||
int24 public tickLower;
|
||||
int24 public tickUpper;
|
||||
uint128 public liquidity;
|
||||
|
||||
uint256 public rebalanceCount;
|
||||
uint256 public feesCollected0;
|
||||
uint256 public feesCollected1;
|
||||
uint256 public blocksInRange;
|
||||
uint256 public totalBlocks;
|
||||
bool public initialized;
|
||||
|
||||
constructor(IUniswapV3Pool _pool, IERC20 _weth, IERC20 _token, bool _token0isWeth) {
|
||||
pool = _pool;
|
||||
token0isWeth = _token0isWeth;
|
||||
token0 = _token0isWeth ? _weth : _token;
|
||||
token1 = _token0isWeth ? _token : _weth;
|
||||
}
|
||||
|
||||
/// @notice Initialize around the current pool price.
|
||||
/// Caller must transfer tokens to this contract before calling.
|
||||
function initialize() external {
|
||||
require(!initialized, "already initialized");
|
||||
initialized = true;
|
||||
(, int24 currentTick,,,,,) = pool.slot0();
|
||||
_createPosition(currentTick);
|
||||
}
|
||||
|
||||
/// @notice Record one simulation block; tracks time-in-range.
|
||||
function recordBlock() external {
|
||||
totalBlocks++;
|
||||
(, int24 currentTick,,,,,) = pool.slot0();
|
||||
if (currentTick >= tickLower && currentTick < tickUpper) {
|
||||
blocksInRange++;
|
||||
}
|
||||
}
|
||||
|
||||
/// @notice Rebalance if current price is outside the position range.
|
||||
/// @return didRebalance true if a rebalance occurred.
|
||||
function maybeRebalance() external returns (bool didRebalance) {
|
||||
(, int24 currentTick,,,,,) = pool.slot0();
|
||||
if (currentTick >= tickLower && currentTick < tickUpper) return false;
|
||||
|
||||
_removePosition();
|
||||
_createPosition(currentTick);
|
||||
rebalanceCount++;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// @notice Collect accrued fees without changing liquidity.
|
||||
function collectFees() external {
|
||||
if (liquidity > 0) {
|
||||
pool.burn(tickLower, tickUpper, 0);
|
||||
}
|
||||
(uint256 f0, uint256 f1) = pool.collect(address(this), tickLower, tickUpper, type(uint128).max, type(uint128).max);
|
||||
feesCollected0 += f0;
|
||||
feesCollected1 += f1;
|
||||
}
|
||||
|
||||
/// @notice Burn current position and collect everything.
|
||||
function finalize() external {
|
||||
_removePosition();
|
||||
}
|
||||
|
||||
function getWethBalance() external view returns (uint256) {
|
||||
return token0isWeth ? token0.balanceOf(address(this)) : token1.balanceOf(address(this));
|
||||
}
|
||||
|
||||
function getTokenBalance() external view returns (uint256) {
|
||||
return token0isWeth ? token1.balanceOf(address(this)) : token0.balanceOf(address(this));
|
||||
}
|
||||
|
||||
// ── Internal ──────────────────────────────────────────────────────────────
|
||||
|
||||
function _createPosition(int24 currentTick) internal {
|
||||
int24 center = (currentTick / TICK_SPACING) * TICK_SPACING;
|
||||
tickLower = center - HALF_WIDTH;
|
||||
tickUpper = center + HALF_WIDTH;
|
||||
|
||||
// Clamp to pool boundaries
|
||||
if (tickLower < MIN_TICK) tickLower = ((MIN_TICK / TICK_SPACING) + 1) * TICK_SPACING;
|
||||
if (tickUpper > MAX_TICK) tickUpper = (MAX_TICK / TICK_SPACING) * TICK_SPACING;
|
||||
|
||||
uint256 bal0 = token0.balanceOf(address(this));
|
||||
uint256 bal1 = token1.balanceOf(address(this));
|
||||
uint160 sqrtCurrent = TickMath.getSqrtRatioAtTick(currentTick);
|
||||
uint160 sqrtLow = TickMath.getSqrtRatioAtTick(tickLower);
|
||||
uint160 sqrtHigh = TickMath.getSqrtRatioAtTick(tickUpper);
|
||||
|
||||
liquidity = LiquidityAmounts.getLiquidityForAmounts(sqrtCurrent, sqrtLow, sqrtHigh, bal0, bal1);
|
||||
if (liquidity > 0) {
|
||||
pool.mint(address(this), tickLower, tickUpper, liquidity, "");
|
||||
}
|
||||
}
|
||||
|
||||
function _removePosition() internal {
|
||||
if (liquidity > 0) {
|
||||
pool.burn(tickLower, tickUpper, liquidity);
|
||||
(uint256 f0, uint256 f1) = pool.collect(address(this), tickLower, tickUpper, type(uint128).max, type(uint128).max);
|
||||
feesCollected0 += f0;
|
||||
feesCollected1 += f1;
|
||||
liquidity = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// @inheritdoc IUniswapV3MintCallback
|
||||
function uniswapV3MintCallback(uint256 amount0Owed, uint256 amount1Owed, bytes calldata) external override {
|
||||
require(msg.sender == address(pool), "not pool");
|
||||
if (amount0Owed > 0) token0.transfer(msg.sender, amount0Owed);
|
||||
if (amount1Owed > 0) token1.transfer(msg.sender, amount1Owed);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue