├── .vscode └── settings.json ├── .DS_Store ├── .gitignore ├── README.md ├── test.js ├── lpmath.js ├── importswaps.js ├── LiqudityAmounts.sol └── rangetester.js /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "terminal.integrated.fontSize": 11 3 | } -------------------------------------------------------------------------------- /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reuptaken/rangetester/HEAD/.DS_Store -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /MKR2.txt 2 | /MKRperc.txt 3 | /RARI2.txt 4 | /USCD2.txt 5 | /describe.js 6 | /getyield.js 7 | /lptest.js 8 | /notes.md 9 | /notes.txt 10 | /report1.md 11 | /results.md 12 | /results.txt 13 | /snx1.txt 14 | /snx2.txt 15 | /data -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Simple backtester for Uniswap v3 concentrated liquidity vs Uniswap v2 data. 2 | 3 | Usage: `node rangetester.js SYMBOL` (eg. `node rangetester.js MKR`) – the input data for each pair has to be downloaded first using `node importswaps.js POOL_ADDRESS` to `./data/` directory (please create it first!). 4 | 5 | Then define some parameters for test directly in script (see comments.) 6 | 7 | It works like this: 8 | 9 | 1. When "week" starts capital is allocated in middle of the range defined by `top` and `bottom` functions. Amplifier is calculated based on range. 10 | 2. During the "week" fees are collected according to historical trades data. Only trades that fall into range are considered. Fees are calculated according to pool share and multiplied by amplifier. 11 | 3. At the end of the "week" both tokens are taken out of pool (it will be a capital for the next week). Fees collected during week are added to the capital. Those rather cryptic numbers, eg. `(-0.23%/-2.25%/1.78%)` are: impermanent loss for the week (always a negative number) / week profit/loss excluding fees / week profit/loss including fees. 12 | 13 | At the end of the backtest there are some numbers presented. Most of them are self-explanatory, but some are not. All numbers are in ETH not USD (unless it's explicitly specified) 14 | 15 | `VIR` – Volume In Range (how much of total volume (`V`) was in range and generated fees) 16 | `CVIR` – it's `VIR * amplifier` 17 | `VIR/V` – how much % of volume was captured 18 | 19 | `Strategy impact` – what was the impact of providing liquidity (this allows you to compare between pools disregarding change of token/ETH price) -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | const { default: NormalDistribution } = require('normal-distribution') 2 | const lpMath = require('./lpmath.js') 3 | require("normal-distribution") 4 | 5 | nd = new NormalDistribution(0, 1) 6 | 7 | const capital = 100 8 | const change = 2/3 9 | const yield = 10 * 0.003 10 | let capture = 1 - 0.682 11 | 12 | for (let i = 2; i <= 2000; i = i * 2) { 13 | const price = Math.sqrt(i) 14 | const changedPrice = Math.sqrt(i) * change 15 | let liquidity = lpMath.getLiquidityForAmounts(Math.sqrt(Math.sqrt(i)), Math.sqrt(1), Math.sqrt(i), (capital / 2) / price, capital / 2) 16 | let token0 = lpMath.getAmountsForLiquidity(Math.sqrt(changedPrice), Math.sqrt(1), Math.sqrt(i), liquidity)[0] 17 | let token1 = lpMath.getAmountsForLiquidity(Math.sqrt(changedPrice), Math.sqrt(1), Math.sqrt(i), liquidity)[1] 18 | const nowValue = token0 * changedPrice + token1 19 | const holdValue = ((capital / 2) / price) * changedPrice + (capital / 2) 20 | console.log(`${i}`) 21 | console.log(` boost=${(compression(1, i)).toFixed(2)} * yield=${yield} * capture=${(1 - capture).toFixed(2)} => ${(100 * (compression(1, i) * yield * (1 - capture))).toFixed(2)}%`) 22 | console.log(` range: 1..${i} ${price.toFixed(2)} -> ${changedPrice.toFixed(2)}, IL = ${(100 * ((nowValue - holdValue) / holdValue)).toFixed(2)}%`) 23 | console.log(` ${((nowValue - holdValue) / holdValue) / (compression(1, i) * yield * (1 - capture)).toFixed(2)}`) 24 | capture = (capture + capture / Math.E) / Math.E 25 | } 26 | 27 | function compression (bottom, top) { 28 | return 1 / (1 - (bottom / top) ** (1 / 4)) 29 | } 30 | 31 | 32 | function stdNormalDistribution (x) { 33 | return Math.pow(Math.E, -Math.pow(x,2) / 2) / Math.sqrt(2*Math.PI); 34 | } -------------------------------------------------------------------------------- /lpmath.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Q96 = 1 4 | 5 | function mulDiv (a, b, multiplier) { 6 | return a * b / multiplier 7 | } 8 | 9 | function getLiquidityForAmount0 (sqrtRatioAX96, sqrtRatioBX96, amount0) { 10 | if (sqrtRatioAX96 > sqrtRatioBX96) [sqrtRatioAX96, sqrtRatioBX96] = [sqrtRatioBX96, sqrtRatioAX96] 11 | const intermediate = mulDiv(sqrtRatioAX96, sqrtRatioBX96, Q96) 12 | return mulDiv(amount0, intermediate, sqrtRatioBX96 - sqrtRatioAX96) 13 | } 14 | 15 | function getLiquidityForAmount1 (sqrtRatioAX96, sqrtRatioBX96, amount1) { 16 | if (sqrtRatioAX96 > sqrtRatioBX96) [sqrtRatioAX96, sqrtRatioBX96] = [sqrtRatioBX96, sqrtRatioAX96] 17 | return mulDiv(amount1, Q96, sqrtRatioBX96 - sqrtRatioAX96) 18 | } 19 | 20 | function getLiquidityForAmounts (sqrtRatioX96, sqrtRatioAX96, sqrtRatioBX96, amount0, amount1) { 21 | let liquidity 22 | if (sqrtRatioAX96 > sqrtRatioBX96) [sqrtRatioAX96, sqrtRatioBX96] = [sqrtRatioBX96, sqrtRatioAX96] 23 | if (sqrtRatioX96 <= sqrtRatioAX96) { 24 | liquidity = getLiquidityForAmount0(sqrtRatioAX96, sqrtRatioBX96, amount0) 25 | } else { 26 | if (sqrtRatioX96 < sqrtRatioBX96) { 27 | const liquidity0 = getLiquidityForAmount0(sqrtRatioX96, sqrtRatioBX96, amount0) 28 | const liquidity1 = getLiquidityForAmount1(sqrtRatioAX96, sqrtRatioX96, amount1) 29 | liquidity = liquidity0 < liquidity1 ? liquidity0 : liquidity1 30 | } else { 31 | liquidity = getLiquidityForAmount1(sqrtRatioAX96, sqrtRatioBX96, amount1) 32 | } 33 | } 34 | return liquidity 35 | } 36 | 37 | function getAmount0ForLiquidity (sqrtRatioAX96, sqrtRatioBX96, liquidity) { 38 | if (sqrtRatioAX96 > sqrtRatioBX96) [sqrtRatioAX96, sqrtRatioBX96] = [sqrtRatioBX96, sqrtRatioAX96] 39 | return mulDiv(liquidity, sqrtRatioBX96 - sqrtRatioAX96, sqrtRatioBX96) / sqrtRatioAX96 40 | } 41 | 42 | function getAmount1ForLiquidity (sqrtRatioAX96, sqrtRatioBX96, liquidity) { 43 | if (sqrtRatioAX96 > sqrtRatioBX96) [sqrtRatioAX96, sqrtRatioBX96] = [sqrtRatioBX96, sqrtRatioAX96] 44 | return mulDiv(liquidity, sqrtRatioBX96 - sqrtRatioAX96, Q96) 45 | } 46 | 47 | function getAmountsForLiquidity (sqrtRatioX96, sqrtRatioAX96, sqrtRatioBX96, liquidity) { 48 | let amount0 49 | let amount1 50 | if (sqrtRatioAX96 > sqrtRatioBX96) [sqrtRatioAX96, sqrtRatioBX96] = [sqrtRatioBX96, sqrtRatioAX96] 51 | if (sqrtRatioX96 <= sqrtRatioAX96) { 52 | amount0 = getAmount0ForLiquidity(sqrtRatioAX96, sqrtRatioBX96, liquidity) 53 | amount1 = 0 54 | } else if (sqrtRatioX96 < sqrtRatioBX96) { 55 | amount0 = getAmount0ForLiquidity(sqrtRatioX96, sqrtRatioBX96, liquidity) 56 | amount1 = getAmount1ForLiquidity(sqrtRatioAX96, sqrtRatioX96, liquidity) 57 | } else { 58 | amount0 = 0 59 | amount1 = getAmount1ForLiquidity(sqrtRatioAX96, sqrtRatioBX96, liquidity) 60 | } 61 | return [amount0, amount1] 62 | } 63 | 64 | exports.getLiquidityForAmounts = getLiquidityForAmounts 65 | exports.getAmountsForLiquidity = getAmountsForLiquidity 66 | -------------------------------------------------------------------------------- /importswaps.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const fs = require('fs') 4 | const axios = require('axios') 5 | 6 | // const GRAPH = 'https://api.thegraph.com/subgraphs/name/benesjan/uniswap-v2' 7 | const GRAPH = 'https://api.thegraph.com/subgraphs/name/uniswap/uniswap-v2' 8 | 9 | const pair = process.argv[2] 10 | if (!pair) die('Contract address not provided') 11 | 12 | let lastTimestamp = '0' 13 | const step = 1000 14 | var reverse 15 | let allSwaps = [] 16 | let swaps 17 | let pairTokens 18 | let token0, token1 19 | let retry = false 20 | 21 | ;(async () => { 22 | const pairQuery = `{ 23 | pair(id: "${pair}"){ 24 | token0 { 25 | symbol 26 | name 27 | } 28 | token1 { 29 | symbol 30 | name 31 | } 32 | } 33 | } 34 | ` 35 | try { 36 | const rawPair = await axios.post(GRAPH, { query: pairQuery }) 37 | pairTokens = (rawPair.data.data.pair) 38 | token0 = pairTokens.token0.symbol 39 | reverse = (token0 === 'WETH') 40 | token1 = pairTokens.token1.symbol 41 | console.log(`Pair: ${token0}-${token1} ${reverse ? 'reverse' : ''}`) 42 | } catch (error) { 43 | die(error) 44 | } 45 | 46 | do { 47 | const swapsQuery = `{ 48 | swaps(first: ${step}, orderBy: timestamp, orderDirection: asc, where: { pair: "${pair}", timestamp_gt: "${lastTimestamp}"}) { 49 | transaction { 50 | blockNumber 51 | } 52 | timestamp 53 | amount0In 54 | amount0Out 55 | amount1In 56 | amount1Out 57 | amountUSD 58 | } 59 | } 60 | ` 61 | try { 62 | console.log(`${lastTimestamp}`) 63 | const rawSwaps = await axios.post(GRAPH, { query: swapsQuery }) 64 | if (rawSwaps.status !== 200) { 65 | die(rawSwaps) 66 | } 67 | if (rawSwaps.data.data) { 68 | swaps = rawSwaps.data.data.swaps 69 | if (!reverse) { 70 | swaps = swaps.map(swap => { 71 | return ({ amount0In: parseFloat(swap.amount0In), amount0Out: parseFloat(swap.amount0Out), amount1In: parseFloat(swap.amount1In), amount1Out: parseFloat(swap.amount1Out), timestamp: parseInt(swap.timestamp), blockNumber: parseInt(swap.transaction.blockNumber) }) 72 | }) 73 | } else { 74 | swaps = swaps.map(swap => { 75 | return ({ amount0In: parseFloat(swap.amount1In), amount0Out: parseFloat(swap.amount1Out), amount1In: parseFloat(swap.amount0In), amount1Out: parseFloat(swap.amount0Out), timestamp: parseInt(swap.timestamp), blockNumber: parseInt(swap.transaction.blockNumber) }) 76 | }) 77 | } 78 | if (rawSwaps.data.data.swaps.length) { 79 | allSwaps = [...allSwaps, ...swaps] 80 | lastTimestamp = swaps[swaps.length - 1].timestamp 81 | } 82 | retry = false 83 | } else { 84 | if (allSwaps[allSwaps.length - 1].timestamp < (Date.now() / 1000) - 60 * 60) { 85 | console.log('Retrying...') 86 | retry = true 87 | } else { 88 | swaps = [] 89 | retry = false 90 | } 91 | } 92 | } catch (error) { 93 | die(error) 94 | } 95 | } 96 | while (swaps.length || retry) 97 | console.log('Saving data') 98 | const output = {} 99 | output.address = pair 100 | output.token0 = reverse ? token1 : token0 101 | output.token1 = reverse ? token0 : token1 102 | output.swaps = allSwaps 103 | fs.writeFileSync(`./data/${output.token0}.json`, JSON.stringify(output, undefined, ' ')) 104 | console.log('Done') 105 | })() 106 | 107 | function die (why) { 108 | console.error(why) 109 | process.exit(1) 110 | } 111 | -------------------------------------------------------------------------------- /LiqudityAmounts.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity >=0.5.0; 3 | 4 | import '@uniswap/v3-core/contracts/libraries/FullMath.sol'; 5 | import '@uniswap/v3-core/contracts/libraries/FixedPoint96.sol'; 6 | 7 | /// @title Liquidity amount functions 8 | /// @notice Provides functions for computing liquidity amounts from token amounts and prices 9 | library LiquidityAmounts { 10 | /// @notice Downcasts uint256 to uint128 11 | /// @param x The uint258 to be downcasted 12 | /// @return y The passed value, downcasted to uint128 13 | function toUint128(uint256 x) private pure returns (uint128 y) { 14 | require((y = uint128(x)) == x); 15 | } 16 | 17 | /// @notice Computes the amount of liquidity received for a given amount of token0 and price range 18 | /// @dev Calculates amount0 * (sqrt(upper) * sqrt(lower)) / (sqrt(upper) - sqrt(lower)) 19 | /// @param sqrtRatioAX96 A sqrt price representing the first tick boundary 20 | /// @param sqrtRatioBX96 A sqrt price representing the second tick boundary 21 | /// @param amount0 The amount0 being sent in 22 | /// @return liquidity The amount of returned liquidity 23 | function getLiquidityForAmount0( 24 | uint160 sqrtRatioAX96, 25 | uint160 sqrtRatioBX96, 26 | uint256 amount0 27 | ) internal pure returns (uint128 liquidity) { 28 | if (sqrtRatioAX96 > sqrtRatioBX96) (sqrtRatioAX96, sqrtRatioBX96) = (sqrtRatioBX96, sqrtRatioAX96); 29 | uint256 intermediate = FullMath.mulDiv(sqrtRatioAX96, sqrtRatioBX96, FixedPoint96.Q96); 30 | return toUint128(FullMath.mulDiv(amount0, intermediate, sqrtRatioBX96 - sqrtRatioAX96)); 31 | } 32 | 33 | /// @notice Computes the amount of liquidity received for a given amount of token1 and price range 34 | /// @dev Calculates amount1 / (sqrt(upper) - sqrt(lower)). 35 | /// @param sqrtRatioAX96 A sqrt price representing the first tick boundary 36 | /// @param sqrtRatioBX96 A sqrt price representing the second tick boundary 37 | /// @param amount1 The amount1 being sent in 38 | /// @return liquidity The amount of returned liquidity 39 | function getLiquidityForAmount1( 40 | uint160 sqrtRatioAX96, 41 | uint160 sqrtRatioBX96, 42 | uint256 amount1 43 | ) internal pure returns (uint128 liquidity) { 44 | if (sqrtRatioAX96 > sqrtRatioBX96) (sqrtRatioAX96, sqrtRatioBX96) = (sqrtRatioBX96, sqrtRatioAX96); 45 | return toUint128(FullMath.mulDiv(amount1, FixedPoint96.Q96, sqrtRatioBX96 - sqrtRatioAX96)); 46 | } 47 | 48 | /// @notice Computes the maximum amount of liquidity received for a given amount of token0, token1, the current 49 | /// pool prices and the prices at the tick boundaries 50 | /// @param sqrtRatioX96 A sqrt price representing the current pool prices 51 | /// @param sqrtRatioAX96 A sqrt price representing the first tick boundary 52 | /// @param sqrtRatioBX96 A sqrt price representing the second tick boundary 53 | /// @param amount0 The amount of token0 being sent in 54 | /// @param amount1 The amount of token1 being sent in 55 | /// @return liquidity The maximum amount of liquidity received 56 | function getLiquidityForAmounts( 57 | uint160 sqrtRatioX96, 58 | uint160 sqrtRatioAX96, 59 | uint160 sqrtRatioBX96, 60 | uint256 amount0, 61 | uint256 amount1 62 | ) internal pure returns (uint128 liquidity) { 63 | if (sqrtRatioAX96 > sqrtRatioBX96) (sqrtRatioAX96, sqrtRatioBX96) = (sqrtRatioBX96, sqrtRatioAX96); 64 | 65 | if (sqrtRatioX96 <= sqrtRatioAX96) { 66 | liquidity = getLiquidityForAmount0(sqrtRatioAX96, sqrtRatioBX96, amount0); 67 | } else if (sqrtRatioX96 < sqrtRatioBX96) { 68 | uint128 liquidity0 = getLiquidityForAmount0(sqrtRatioX96, sqrtRatioBX96, amount0); 69 | uint128 liquidity1 = getLiquidityForAmount1(sqrtRatioAX96, sqrtRatioX96, amount1); 70 | 71 | liquidity = liquidity0 < liquidity1 ? liquidity0 : liquidity1; 72 | } else { 73 | liquidity = getLiquidityForAmount1(sqrtRatioAX96, sqrtRatioBX96, amount1); 74 | } 75 | } 76 | 77 | /// @notice Computes the amount of token0 for a given amount of liquidity and a price range 78 | /// @param sqrtRatioAX96 A sqrt price representing the first tick boundary 79 | /// @param sqrtRatioBX96 A sqrt price representing the second tick boundary 80 | /// @param liquidity The liquidity being valued 81 | /// @return amount0 The amount of token0 82 | function getAmount0ForLiquidity( 83 | uint160 sqrtRatioAX96, 84 | uint160 sqrtRatioBX96, 85 | uint128 liquidity 86 | ) internal pure returns (uint256 amount0) { 87 | if (sqrtRatioAX96 > sqrtRatioBX96) (sqrtRatioAX96, sqrtRatioBX96) = (sqrtRatioBX96, sqrtRatioAX96); 88 | 89 | return 90 | FullMath.mulDiv( 91 | uint256(liquidity) << FixedPoint96.RESOLUTION, 92 | sqrtRatioBX96 - sqrtRatioAX96, 93 | sqrtRatioBX96 94 | ) / sqrtRatioAX96; 95 | } 96 | 97 | /// @notice Computes the amount of token1 for a given amount of liquidity and a price range 98 | /// @param sqrtRatioAX96 A sqrt price representing the first tick boundary 99 | /// @param sqrtRatioBX96 A sqrt price representing the second tick boundary 100 | /// @param liquidity The liquidity being valued 101 | /// @return amount1 The amount of token1 102 | function getAmount1ForLiquidity( 103 | uint160 sqrtRatioAX96, 104 | uint160 sqrtRatioBX96, 105 | uint128 liquidity 106 | ) internal pure returns (uint256 amount1) { 107 | if (sqrtRatioAX96 > sqrtRatioBX96) (sqrtRatioAX96, sqrtRatioBX96) = (sqrtRatioBX96, sqrtRatioAX96); 108 | 109 | return FullMath.mulDiv(liquidity, sqrtRatioBX96 - sqrtRatioAX96, FixedPoint96.Q96); 110 | } 111 | 112 | /// @notice Computes the token0 and token1 value for a given amount of liquidity, the current 113 | /// pool prices and the prices at the tick boundaries 114 | /// @param sqrtRatioX96 A sqrt price representing the current pool prices 115 | /// @param sqrtRatioAX96 A sqrt price representing the first tick boundary 116 | /// @param sqrtRatioBX96 A sqrt price representing the second tick boundary 117 | /// @param liquidity The liquidity being valued 118 | /// @return amount0 The amount of token0 119 | /// @return amount1 The amount of token1 120 | function getAmountsForLiquidity( 121 | uint160 sqrtRatioX96, 122 | uint160 sqrtRatioAX96, 123 | uint160 sqrtRatioBX96, 124 | uint128 liquidity 125 | ) internal pure returns (uint256 amount0, uint256 amount1) { 126 | if (sqrtRatioAX96 > sqrtRatioBX96) (sqrtRatioAX96, sqrtRatioBX96) = (sqrtRatioBX96, sqrtRatioAX96); 127 | 128 | if (sqrtRatioX96 <= sqrtRatioAX96) { 129 | amount0 = getAmount0ForLiquidity(sqrtRatioAX96, sqrtRatioBX96, liquidity); 130 | } else if (sqrtRatioX96 < sqrtRatioBX96) { 131 | amount0 = getAmount0ForLiquidity(sqrtRatioX96, sqrtRatioBX96, liquidity); 132 | amount1 = getAmount1ForLiquidity(sqrtRatioAX96, sqrtRatioX96, liquidity); 133 | } else { 134 | amount1 = getAmount1ForLiquidity(sqrtRatioAX96, sqrtRatioBX96, liquidity); 135 | } 136 | } 137 | } -------------------------------------------------------------------------------- /rangetester.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const lpMath = require('./lpmath.js') 3 | 4 | if (!process.argv[2]) die('Data file not provided') 5 | const dataFile = './data/' + process.argv[2] + '.json' 6 | 7 | const WEEK = 604800 // it's called WEEK but actually can be arbitrary timeframe 8 | const skipWeeks = 2 // how many timeframes should be skipped (sometimes first day of trading has some big volatity, and it's better to ignore it) 9 | 10 | const weekBars = [] 11 | 12 | const swaps = require(dataFile).swaps 13 | const token0Symbol = require(dataFile).token0 === 'WETH' ? 'ETH' : require(dataFile).token0 14 | const token1Symbol = require(dataFile).token1 === 'WETH' ? 'ETH' : require(dataFile).token1 15 | const reverse = require(dataFile).token0 === 'ETH' 16 | console.log(' ') 17 | console.log(`Backtest for: ${token0Symbol}/${token1Symbol}`) 18 | 19 | // verbosity level 20 | const logRebalance = true 21 | const logWeekChange = false 22 | const logSwaps = false 23 | 24 | let currWeek = 0 25 | let prevSwap 26 | 27 | let dayLow = Infinity 28 | let weekLow = Infinity 29 | let dayHigh = 0 30 | let weekHigh = 0 31 | 32 | let dayVolume = 0 33 | let weekVolume = 0 34 | let volumeIn = 0 35 | let compressedVolumeIn = 0 36 | let totalVolume = 0 37 | let totalFees = 0 38 | let totalCapturedFees = 0 39 | let weekFees = 0 40 | let totalAmp = 0 41 | 42 | let totalPureIL = 1 43 | let totalIL = 1 44 | 45 | const feeLevel = 0.003 46 | 47 | const multi = 1.221 // this is used to multiple range width by some amount 48 | const initCapital = 135 // the capital which is needed to achieve some poolShare, in ETH, both sides (token + ETH) 49 | const poolShare = 0.01 // share in pool (v2 style), corresponding to some amount of capital defined by initCapital 50 | 51 | let capital = initCapital 52 | let netCapital = initCapital 53 | let liquidity 54 | 55 | let prevWeekOpenPrice = price(swaps[0]) 56 | let start = swaps[0].timestamp 57 | 58 | let lastPrice 59 | let firstToken0Investment = 0 60 | 61 | //const top = previousWeekATRPercentTop 62 | //const bottom = previousWeekATRPercentBottom 63 | const top = percentTop // function that defines top of the trading range 64 | const bottom = percentBottom // function that defines bottom of the trading range 65 | 66 | let rangeTop 67 | let rangeBottom 68 | 69 | let prevInCapital 70 | let token0, token1 71 | for (const swap of swaps) { 72 | const currPrice = price(swap) 73 | if (currPrice === Infinity) continue 74 | const now = new Date(swap.timestamp * 1000) 75 | 76 | if (prevSwap) if (Math.floor((swap.timestamp - start) / WEEK) !== Math.floor((prevSwap.timestamp - start) / WEEK)) newWeek(swap) 77 | 78 | if (currWeek >= skipWeeks) { 79 | // skipping 2 first weeks, first, because it has no data to compute the ranged, second, because the H/L data from first week can be flawed 80 | if (rangeDefined(rangeTop, rangeBottom)) { 81 | totalVolume = totalVolume + volume(swap) 82 | totalFees = totalFees + volume(swap) * poolShare * feeLevel 83 | if (inRange(currPrice, rangeTop, rangeBottom)) { 84 | if (logSwaps) console.log(`${now.toISOString()}: [${currWeek}] ${rangeBottom} > ${currPrice} < ${rangeTop}, wH: ${weekHigh}`) 85 | volumeIn = volumeIn + volume(swap) 86 | compressedVolumeIn = compressedVolumeIn + volume(swap) * compression(rangeBottom, rangeTop) 87 | totalCapturedFees = totalCapturedFees + volume(swap) * poolShare * feeLevel * compression(rangeBottom, rangeTop) 88 | weekFees = weekFees + volume(swap) * poolShare * feeLevel * compression(rangeBottom, rangeTop) 89 | } else { 90 | if (logSwaps) console.log(`${now.toISOString()}: [${currWeek}] ${rangeBottom} ~ ${currPrice} ~ ${rangeTop}, wH: ${weekHigh}`) 91 | } 92 | } else { 93 | console.log('*** Range undefined') 94 | } 95 | } else { 96 | // console.log(`*** Not counting volume in week: ${currWeek}`) 97 | } 98 | 99 | dayHigh = Math.max(currPrice, dayHigh) 100 | weekHigh = Math.max(currPrice, weekHigh) 101 | dayLow = Math.min(currPrice, dayLow) 102 | weekLow = Math.min(currPrice, weekLow) 103 | dayVolume = dayVolume + volume(swap) 104 | weekVolume = weekVolume + volume(swap) 105 | prevSwap = swap 106 | lastPrice = currPrice 107 | } 108 | console.log('---') 109 | console.log(`Weeks: ${currWeek}`) 110 | console.log(`Initial capital ${initCapital} ETH, pool share: ${poolShare * 100}%`) 111 | console.log(`Multiplier: ${multi}`) 112 | console.log(`VIR: ${volumeIn.toFixed(0)}, CVIR ${compressedVolumeIn.toFixed(0)}, V: ${totalVolume.toFixed(0)}`) 113 | console.log(`VIR/V: ${((volumeIn / totalVolume) * 100).toFixed(2)}%, CVIR/V: ${((compressedVolumeIn / totalVolume) * 100).toFixed(2)}%`) 114 | console.log(`Fees: ${totalFees.toFixed(2)} ${token1Symbol}, fees per week/capital: ${((totalFees / (currWeek - skipWeeks)) / capital).toFixed(4)} ETH`) 115 | console.log(`Avg amplifier: ${(totalAmp / currWeek).toFixed(2)}x`) 116 | console.log(`Captured fees: ${totalCapturedFees.toFixed(2)} ${token1Symbol}, captured fees per week/capital: ${((totalCapturedFees / (currWeek - skipWeeks)) / capital).toFixed(4)} ETH`) 117 | const holdPL = ((firstToken0Investment * lastPrice + initCapital / 2) - initCapital) / initCapital 118 | console.log(`Hodl P/L: ${(100 * holdPL).toFixed(2)}%`) 119 | const finalPL = (capital - initCapital) / initCapital 120 | console.log(`End capital: ${capital.toFixed(2)} ${token1Symbol}, P/L: ${(100 * finalPL).toFixed(2)}%`) 121 | const strategyImpact = (capital - (firstToken0Investment * lastPrice + initCapital / 2)) / (firstToken0Investment * lastPrice + initCapital / 2) 122 | console.log(`Strategy impact: ${(100 * strategyImpact).toFixed(2)}%`) 123 | 124 | function rebalance () { 125 | if (logRebalance) console.log(`Rebalance for week: ${currWeek}`) 126 | const closePrice = price(prevSwap) 127 | const openPrice = prevWeekOpenPrice 128 | if (liquidity) { 129 | const prevWeekCapital = token0 * closePrice + token1 130 | 131 | token0 = lpMath.getAmountsForLiquidity(Math.sqrt(closePrice), Math.sqrt(rangeBottom), Math.sqrt(rangeTop), liquidity)[0] 132 | token1 = lpMath.getAmountsForLiquidity(Math.sqrt(closePrice), Math.sqrt(rangeBottom), Math.sqrt(rangeTop), liquidity)[1] 133 | 134 | capital = token0 * closePrice + token1 + weekFees 135 | netCapital = token0 * closePrice + token1 136 | 137 | const weekIL = (netCapital / prevWeekCapital) - 1 138 | const weekPL = (capital / prevInCapital) - 1 139 | const netWeekPL = (netCapital / prevInCapital) - 1 140 | const weekYield = weekFees / capital 141 | if (logRebalance) console.log(` out: ${token0.toFixed(2)} ${token0Symbol} + ${token1.toFixed(2)} ${token1Symbol} = ${capital.toFixed(2)} ${token1Symbol} vs HODL: ${prevInCapital.toFixed(2)} ${token1Symbol}, fees: ${weekFees.toFixed(2)}, yield: ${(weekYield * 100).toFixed(2)}% (${(weekIL * 100).toFixed(2)}%/${(netWeekPL * 100).toFixed(2)}%/${(weekPL * 100).toFixed(2)}%)`) 142 | totalIL = totalIL * (1 + weekPL) 143 | totalPureIL = totalPureIL * (1 + netWeekPL) 144 | } 145 | rangeTop = top(currWeek, prevWeekOpenPrice, multi) 146 | rangeBottom = bottom(currWeek, prevWeekOpenPrice, multi) 147 | rangeBottom = rangeBottom < 0 ? 0 : rangeBottom 148 | 149 | liquidity = lpMath.getLiquidityForAmounts(Math.sqrt(prevWeekOpenPrice), Math.sqrt(rangeBottom), Math.sqrt(rangeTop), (capital / 2) / prevWeekOpenPrice, capital / 2) 150 | token0 = lpMath.getAmountsForLiquidity(Math.sqrt(prevWeekOpenPrice), Math.sqrt(rangeBottom), Math.sqrt(rangeTop), liquidity)[0] 151 | token1 = lpMath.getAmountsForLiquidity(Math.sqrt(prevWeekOpenPrice), Math.sqrt(rangeBottom), Math.sqrt(rangeTop), liquidity)[1] 152 | prevInCapital = token0 * prevWeekOpenPrice + token1 153 | firstToken0Investment = firstToken0Investment === 0 ? token0 : firstToken0Investment 154 | totalAmp = totalAmp + compression(rangeBottom, rangeTop) 155 | if (logRebalance) console.log(` in ${prevWeekOpenPrice}: ${token0.toFixed(2)} ${token0Symbol} + ${token1.toFixed(2)} ${token1Symbol} = ${prevInCapital.toFixed(2)} ${token1Symbol} in ${rangeBottom.toFixed(6)}..${rangeTop.toFixed(6)} range -> amplifier: ${compression(rangeBottom, rangeTop).toFixed(2)}x`) 156 | } 157 | 158 | function newWeek (swap) { 159 | const closePrice = price(prevSwap) 160 | const openPrice = prevWeekOpenPrice 161 | 162 | if (logWeekChange) console.log(`Creating bars for week: ${currWeek}, price: ${closePrice.toFixed(6)} ${token0Symbol}/${token1Symbol}`) 163 | 164 | if (Math.max(openPrice, weekLow, closePrice) > weekHigh) die('week too high') 165 | if (Math.min(openPrice, weekHigh, closePrice) < weekLow) die('week too low') 166 | weekBars.push({ n: currWeek, O: openPrice, H: weekHigh, L: weekLow, C: closePrice, volume: weekVolume }) 167 | // console.log(`Bar: n: ${currWeek}, O: ${openPrice}, H: ${weekHigh}, L: ${weekLow}, C: ${closePrice}, volume: ${weekVolume}`) 168 | currWeek++ 169 | prevWeekOpenPrice = price(swap) 170 | 171 | if (currWeek >= skipWeeks) { 172 | rebalance() 173 | } else { 174 | if (logRebalance) console.log(`Skipping rebalance: ${currWeek}`) 175 | } 176 | 177 | weekLow = Infinity 178 | weekHigh = 0 179 | weekVolume = 0 180 | weekFees = 0 181 | 182 | if (logWeekChange) console.log(`Next week: ${currWeek}, price: ${prevWeekOpenPrice.toFixed(6)} ${token0Symbol}/${token1Symbol}`) 183 | } 184 | 185 | function compression (bottom, top) { 186 | return 1 / (1 - (bottom / top) ** (1 / 4)) 187 | } 188 | 189 | // range based on previous week high and low 190 | function previousWeekHigh (week) { 191 | return week > 0 ? weekBars[week - 1].H : undefined 192 | } 193 | function previousWeekLow (week) { 194 | return week > 0 ? weekBars[week - 1].L : undefined 195 | } 196 | 197 | // range based on previous week volatility * multiplier 198 | function previousWeekATRPercentTop (week, basePrice, multiplier = 1) { 199 | if (week < 1) return undefined 200 | const TR = weekBars[week - 1].H - weekBars[week - 1].L 201 | return basePrice + TR / (2 / multiplier) 202 | } 203 | function previousWeekATRPercentBottom (week, basePrice, multiplier = 1) { 204 | if (week < 1) return undefined 205 | const TR = weekBars[week - 1].H - weekBars[week - 1].L 206 | const rangeTop = basePrice + TR / (2 / multiplier) 207 | const ratio = rangeTop / basePrice 208 | // console.log(basePrice, TR, rangeTop, basePrice / ratio) 209 | return basePrice / ratio 210 | } 211 | 212 | // range based on current price multiplied/divided by some multiplier 213 | 214 | function percentTop (week, basePrice, multiplier = 1) { 215 | return basePrice * multiplier 216 | } 217 | function percentBottom (week, basePrice, multiplier = 1) { 218 | return basePrice / multiplier 219 | } 220 | 221 | function rangeDefined (rangeTop, rangeBottom) { 222 | return (rangeTop && rangeBottom) 223 | } 224 | 225 | function inRange (price, rangeTop, rangeBottom) { 226 | return ((price <= rangeTop) && (price >= rangeBottom)) 227 | } 228 | 229 | function volume (swap) { 230 | if (!reverse) { 231 | return Math.max(swap.amount1In, swap.amount1Out) 232 | } else { 233 | return Math.max(swap.amount0In, swap.amount0Out) 234 | } 235 | } 236 | 237 | function price (swap) { 238 | if (!reverse) { 239 | return swap.amount0In ? swap.amount1Out / swap.amount0In : swap.amount1In / swap.amount0Out 240 | } else { 241 | return swap.amount0In ? swap.amount0In / swap.amount1Out : swap.amount0Out / swap.amount1In 242 | } 243 | } 244 | function die (why) { 245 | console.error(why) 246 | process.exit(1) 247 | } 248 | --------------------------------------------------------------------------------