"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.UniswapV3Backtest = void 0;
const tslib_1 = require("tslib");
// Uniswap V3 Contracts
const color_1 = (0, tslib_1.__importDefault)(require("@oclif/color"));
const UniswapV3Factory_json_1 = (0, tslib_1.__importDefault)(require("@uniswap/v3-core/artifacts/contracts/UniswapV3Factory.sol/UniswapV3Factory.json"));
const UniswapV3Pool_json_1 = (0, tslib_1.__importDefault)(require("@uniswap/v3-core/artifacts/contracts/UniswapV3Pool.sol/UniswapV3Pool.json"));
const QuoterV2_json_1 = (0, tslib_1.__importDefault)(require("@uniswap/v3-periphery/artifacts/contracts/lens/QuoterV2.sol/QuoterV2.json"));
const NFTDescriptor_json_1 = (0, tslib_1.__importDefault)(require("@uniswap/v3-periphery/artifacts/contracts/libraries/NFTDescriptor.sol/NFTDescriptor.json"));
const NonfungiblePositionManager_json_1 = (0, tslib_1.__importDefault)(require("@uniswap/v3-periphery/artifacts/contracts/NonfungiblePositionManager.sol/NonfungiblePositionManager.json"));
const NonfungibleTokenPositionDescriptor_json_1 = (0, tslib_1.__importDefault)(require("@uniswap/v3-periphery/artifacts/contracts/NonfungibleTokenPositionDescriptor.sol/NonfungibleTokenPositionDescriptor.json"));
const SwapRouter_json_1 = (0, tslib_1.__importDefault)(require("@uniswap/v3-periphery/artifacts/contracts/SwapRouter.sol/SwapRouter.json"));
const cli_progress_1 = (0, tslib_1.__importDefault)(require("cli-progress"));
const cli_table3_1 = (0, tslib_1.__importDefault)(require("cli-table3"));
const cli_ux_1 = (0, tslib_1.__importDefault)(require("cli-ux"));
const coingecko_api_1 = (0, tslib_1.__importDefault)(require("coingecko-api"));
const ethers_1 = require("ethers");
const utils_1 = require("ethers/lib/utils");
const promises_1 = (0, tslib_1.__importDefault)(require("node:fs/promises"));
const UniLiquidityManager_json_1 = require("../artifacts/contracts/UniLiquidityManager.sol/UniLiquidityManager.json");
// eslint-disable-next-line camelcase
const uniswap_position_quoter_1 = require("@steerprotocol/uniswap-position-quoter");
const change_case_1 = require("change-case");
const events_1 = require("../utils/events");
const network_1 = require("../utils/network");
const constants_1 = require("./../utils/constants");
const replay_1 = require("./replay");
const types_1 = require("./types");
const uniswapv3_1 = require("./uniswapv3");
const kill = require('kill-port');
const POOR_ADDRESS = '0x0000000000000000133700000000000000000000';
const RICH_ADDRESS = '0x0000000000000000133800000000000000000000';
// Uniswap v3 artifacts
const artifacts = {
    UniswapV3Factory: UniswapV3Factory_json_1.default,
    SwapRouter: SwapRouter_json_1.default,
    NFTDescriptor: NFTDescriptor_json_1.default,
    NonfungibleTokenPositionDescriptor: NonfungibleTokenPositionDescriptor_json_1.default,
    NonfungiblePositionManager: NonfungiblePositionManager_json_1.default,
    UniswapV3Pool: UniswapV3Pool_json_1.default,
};
class UniswapV3Backtest {
    // eslint-disable-next-line complexity
    async run(config, strategy, stats, gasPrice) {
        var _a;
        const progressBar = new cli_progress_1.default.SingleBar({}, cli_progress_1.default.Presets.shades_classic);
        stats.status.state = types_1.BacktestState.Running;
        stats.status.progress = 0;
        try {
            // Setup a local RPC Provider for ethers
            const provider = new ethers_1.ethers.providers.JsonRpcProvider('http://localhost:8545');
            // Parse pool id
            const poolAddress = config.poolId;
            // Setup a coingecko client
            const CoinGeckoClient = new coingecko_api_1.default();
            // Uniswap Contract Addresses
            const swapRouterAddress = '0xE592427A0AEce92De3Edee1F18E0157C05861564';
            const NFTManagerAddress = '0xC36442b4a4522E871399CD717aBDD847Ab11FE88';
            const UniswapV3Factory = '0x1F98431c8aD98523631AE4a59f267346ea31F984';
            const WETH = '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2';
            // const quoterAddress = '0x61fFE014bA17989E743c5F6cB21bF9697530B21e' // V3 '0xb27308f9F90D607463bb33eA1BeBb41C27CE5AB6'
            // Setup swap router contract
            const swapRouterContract = new ethers_1.Contract(swapRouterAddress, new utils_1.Interface(artifacts.SwapRouter.abi)).connect(provider.getSigner(0));
            // Setup NFT Manager contract
            const nftManagerContract = new ethers_1.Contract(NFTManagerAddress, new utils_1.Interface(artifacts.NonfungiblePositionManager.abi)).connect(provider.getSigner(0));
            // Setup pool contract
            const poolContract = new ethers_1.Contract(poolAddress, new utils_1.Interface(artifacts.UniswapV3Pool.abi)).connect(provider.getSigner(0));
            // Parse historical data
            const historicalData = JSON.parse(await promises_1.default.readFile(config.location, 'utf-8'));
            // Copy the historical data, so we can mutate the data
            let txEventsArray = [...historicalData];
            // Sort the events by timestamp so we replay them in proper order
            txEventsArray = txEventsArray.sort(function (a, b) {
                return Number(a.timestamp) - Number(b.timestamp);
            });
            // Load in token info
            const token0Decimals = txEventsArray[0].token0.decimals;
            const token1Decimals = txEventsArray[0].token1.decimals;
            const token0Address = txEventsArray[0].token0.id;
            const token1Address = txEventsArray[0].token1.id;
            // Pull the pool fee for the current pool we are working with
            // This is using for mint, burn, and setup of the associated vault contract
            const poolFee = await poolContract.fee();
            console.log('Acquiring assets for backtest...');
            const { mainActor, mainSigner, token0, token1 } = (await provideTokensToAccounts([txEventsArray[0].token0.id, txEventsArray[0].token1.id], poolAddress, Number(txEventsArray[0].transaction.blockNumber), provider));
            // Process approvals for the swap router and the nft manager
            // (which mints and burns liquidity)
            await token0
                .connect(mainSigner)
                .approve(swapRouterAddress, (0, utils_1.parseUnits)('999999999999', token0Decimals));
            await token1
                .connect(mainSigner)
                .approve(swapRouterAddress, (0, utils_1.parseUnits)('999999999999', token1Decimals));
            await token0
                .connect(mainSigner)
                .approve(NFTManagerAddress, (0, utils_1.parseUnits)('999999999999', token0Decimals));
            await token1
                .connect(mainSigner)
                .approve(NFTManagerAddress, (0, utils_1.parseUnits)('999999999999', token1Decimals));
            // Create contract factory for the UniLiquidityManger
            // eslint-disable-next-line camelcase
            const ManagerFactory = new ethers_1.ethers.ContractFactory(UniLiquidityManager_json_1.abi, UniLiquidityManager_json_1.bytecode);
            const QuoterFactory = new ethers_1.ethers.ContractFactory(QuoterV2_json_1.default.abi, QuoterV2_json_1.default.bytecode);
            const DeployedQuoter = await QuoterFactory.connect(mainSigner).deploy(UniswapV3Factory, WETH);
            const DeployedQuoterAddress = DeployedQuoter.address;
            const quoterContract = new ethers_1.Contract(DeployedQuoterAddress, new utils_1.Interface(QuoterV2_json_1.default.abi)).connect(provider.getSigner(0));
            const PositionQuoter = new uniswap_position_quoter_1.CustomRouter(quoterContract);
            // Deploy a new instance of the UniLiquidityManager
            const DeployedManager = await ManagerFactory.connect(mainSigner).deploy();
            // Next we need to encode the input params of the vault, this is abi encoded as different
            // vault will have different params
            const initParams = encodeInitUniV3VaultParams(token0Address, token1Address, poolFee, ethers_1.BigNumber.from(10).pow(36), 1000, 45);
            // Now that we have encoded the vault params, we can initialize the vault
            const txV3 = await DeployedManager.connect(mainSigner).initialize(mainActor.address, // Orchestrator
            mainActor.address, // Steer Address
            mainActor.address, // Governance
            0, initParams);
            // Wait for the tx to be mined
            await txV3.wait();
            const fee = await (await poolContract.fee()).toLocaleString('fullwide', { useGrouping: false });
            const txSetPool = await DeployedManager.setPool(UniswapV3Factory, fee);
            await txSetPool.wait();
            // Now that we have a clean wallet to work from we need
            // to setup some tokens so it can begin trading. First,
            // we provide it ETH
            await provider.send('hardhat_setBalance', [
                POOR_ADDRESS,
                (0, utils_1.parseEther)('1000').toHexString(),
            ]);
            // Next, we should impersonate the poor actor and approve for the
            // uni liq vault
            const [, poorSigner] = await getImpersonationSigner(POOR_ADDRESS, provider);
            // Then we convert the initial amounts the user provided into their native values
            const t0initialNative = (0, utils_1.parseUnits)(config.token0InitialAmount, token0Decimals);
            const t1initialNative = (0, utils_1.parseUnits)(config.token1InitialAmount, token1Decimals);
            // We dont want to have tokens yet, dump them
            const t0PreTransferBalance = await token0.balanceOf(POOR_ADDRESS);
            const t1PreTransferBalance = await token1.balanceOf(POOR_ADDRESS);
            // Send existing balance back to rich
            await token0.connect(poorSigner).transfer(mainSigner.address, t0PreTransferBalance);
            await token1.connect(poorSigner).transfer(mainSigner.address, t1PreTransferBalance);
            // Transfer the tokens from the combined top token0 address to the poor actor
            await token0.connect(mainSigner).transfer(POOR_ADDRESS, t0initialNative);
            await token1.connect(mainSigner).transfer(POOR_ADDRESS, t1initialNative);
            const t0PostTransferBalance = await token0.balanceOf(POOR_ADDRESS);
            const t1PostTransferBalance = await token1.balanceOf(POOR_ADDRESS);
            // Approve tokens
            await token0
                .connect(poorSigner)
                .approve(DeployedManager.address, (0, utils_1.parseEther)('999999999999').toHexString());
            await token1
                .connect(poorSigner)
                .approve(DeployedManager.address, (0, utils_1.parseEther)('999999999999').toHexString());
            // Next we want to bootstrap the vault by supplying tokens from the
            // poor actor to the uni liq vault
            const depTx = await DeployedManager.connect(poorSigner).deposit(t0initialNative, t1initialNative, 0, 0, POOR_ADDRESS);
            // Wait for the tx to be mined
            await depTx.wait();
            const t0PostDepositBal = await token0.balanceOf(POOR_ADDRESS);
            const t1PostDepositBal = await token1.balanceOf(POOR_ADDRESS);
            // Track actual deposit amounts based on what
            // was taken from the poor actor's wallet
            const initDept0 = t0PostTransferBalance.sub(t0PostDepositBal);
            const initDept1 = t1PostTransferBalance.sub(t1PostDepositBal);
            const currentSlot = await poolContract.slot0();
            // ratio * 1e18
            const X192 = ethers_1.BigNumber.from(2).pow(192);
            const initPrice = (currentSlot.sqrtPriceX96.pow(2));
            const initToken0In1 = initDept0.mul(initPrice);
            const initTot1 = initDept1.add(initToken0In1.div(X192));
            // eslint-disable-next-line @typescript-eslint/ban-ts-comment
            // @ts-ignore
            const t1data = await CoinGeckoClient.coins.fetchCoinContractInfo(token1.address);
            // process.env.DEBUG && console.log(t1data.data.id);
            const unixStartTime = txEventsArray[0].timestamp;
            let dateMS = new Date(unixStartTime * 1000);
            // let options = {day:'2-digit',month:'2-digit'}
            let dateEN = dateMS
                .toLocaleDateString('en-GB', {
                day: '2-digit',
                month: '2-digit',
                year: 'numeric',
            })
                .replace('/', '-')
                .replace('/', '-');
            const t1DataInitialUSD = await CoinGeckoClient.coins.fetchHistory(t1data.data.id, { date: dateEN });
            const initT1toUSD = t1DataInitialUSD.data.market_data.current_price.usd;
            // Adjust for decimals and multiply by nonNative usd price
            const _usdInitValPrecBN = ethers_1.BigNumber.from(Math.round(initT1toUSD * 1000000).toString());
            const initVal = Number(initTot1.mul(_usdInitValPrecBN).div(ethers_1.BigNumber.from(10).pow(token1Decimals)).toString()) / 1000000;
            // process.env.DEBUG && console.log(
            //   'Total value deposited measured in USD (accurate to day): $' +
            //     initVal.toFixed(2),
            // )
            // Get the current nft tokenId
            const mintParams = {
                token0: token0Address,
                token1: token1Address,
                fee: poolFee,
                tickLower: 0,
                tickUpper: 600,
                amount0Desired: ethers_1.BigNumber.from(5),
                amount1Desired: ethers_1.BigNumber.from(5),
                amount0Min: 0,
                amount1Min: 0,
                recipient: mainSigner.address,
                deadline: 1920170954,
            };
            const tokenidMintTx = await nftManagerContract.connect(mainSigner).mint(mintParams);
            const tokenidMintRec = await tokenidMintTx.wait();
            const events = (0, events_1.filterEvents)(tokenidMintRec, 'IncreaseLiquidity');
            const eventResult = (0, events_1.decodeEvents)(nftManagerContract, events || []);
            // Any tokenID > tokenID_start will have occured post fork
            const tokenIDStart = Number(eventResult[0][0]);
            // this is how we will track our own mints
            let tokenIDCounter = tokenIDStart + 1;
            // Create a counter for failed transactions
            let txsFailed = 0;
            let cumulativeGas = ethers_1.BigNumber.from(0);
            // This mapping links the ticks [number,number] to tokenID <number>
            const positionMap = new Map();
            let nextEpochTrigger = txEventsArray[0].timestamp - 1;
            const epochLength = Number(config.epochLength);
            const backtestDataArray = [];
            const priceMovementDataArray = [];
            // Time the backtesting
            const startTime = Date.now();
            const poolObj = new ethers_1.Contract(poolAddress, UniswapV3Pool_json_1.default.abi, mainSigner);
            progressBar.start(100, 0);
            // -----Main event loop-----
            for (const event of txEventsArray) {
                // console.log(event.type, event.timestamp)
                const percentChange = (txEventsArray.indexOf(event) / txEventsArray.length) * 100;
                // Update progress bar
                const percent = Number(Number(percentChange).toFixed(2));
                progressBar.update(percent);
                stats.status.progress = percent;
                const currentTick = (await poolContract.slot0()).tick;
                // EPOCH STRATEGY
                // check if this next strategy is past the epoch trigger
                if (event.timestamp >= nextEpochTrigger) {
                    // let results = [];
                    let output;
                    try {
                        // console.log('about to tend')
                        // eslint-disable-next-line @typescript-eslint/ban-ts-comment
                        // @ts-ignore
                        output = await strategy.reloadExecute({
                            currentTime: event.timestamp,
                        });
                    }
                    catch (error) {
                        console.log('Strategy execution bundle error:', error);
                        console.log('Make sure your bundle is working properly and accepts candles as data input.');
                    }
                    const newPositions = [];
                    // Output will be parsed result
                    // eslint-disable-next-line eqeqeq, no-eq-null
                    if (output == null || output.functionName == null) {
                        console.log('Bin failure, skipping tend (possbile problem with input params)');
                        // console.log(output)
                        nextEpochTrigger += epochLength;
                        continue;
                    }
                    // Make position array
                    for (let i = 0; i < output.valuesArray[1][0].length; i++) {
                        const temp = {
                            lowerTick: Number(output.valuesArray[1][0][i]),
                            upperTick: Number(output.valuesArray[1][1][i]),
                            weight: Number(output.valuesArray[1][2][i]),
                        };
                        newPositions.push(temp);
                    }
                    // console.log(newPositions)
                    // Strategy returns array of start end weight
                    if (newPositions[0].weight === 0) {
                        // console.log('no positions, skipping epoch')
                        nextEpochTrigger += epochLength;
                        continue;
                    }
                    await DeployedManager.poke();
                    const [balt0, balt1,] = await DeployedManager.getTotalAmounts();
                    const pos = [];
                    for (const newPos of newPositions) {
                        pos.push({
                            lowerTick: Number(newPos.lowerTick),
                            upperTick: Number(newPos.upperTick),
                            weight: Number(newPos.weight),
                        });
                    }
                    // Here we tend to take everything out so the calculations dont use our liquidity when reaching exact precision
                    const _LiqPos = {
                        lowerTick: [0],
                        upperTick: [600],
                        relativeWeight: [0],
                    };
                    const encoder = new utils_1.AbiCoder();
                    const _timeSensitive = encoder.encode(['int256', 'uint160'], [0, ethers_1.BigNumber.from(0)]);
                    await (0, network_1.setNextBlockTimestamp)(120, provider);
                    try {
                        const tendTx = await DeployedManager.connect(mainSigner).tend(0, _LiqPos, _timeSensitive, {
                            gasLimit: 30000000,
                        });
                        await tendTx.wait();
                    }
                    catch (error) {
                        console.log('🚨 Tend Failure!');
                        throw error;
                    }
                    let swapResult;
                    try {
                        swapResult = await PositionQuoter.getSwapAmount(poolObj, pos, balt0, balt1, token0Address, token1Address, poolFee, 20);
                    }
                    catch (error) {
                        console.log('Swap estimate error:', error);
                    }
                    if (!swapResult) {
                        console.log('🚨 🚨 Skipping tend due to swap result failure');
                        continue;
                    }
                    // Slippage check ~ can be configured, but not necessarily a concern with assumptions
                    const sqrtPrice = swapResult.zeroForOne ? ethers_1.BigNumber.from('50').mul(currentSlot.sqrtPriceX96).div('100') : ethers_1.BigNumber.from('150').mul(currentSlot.sqrtPriceX96).div('100');
                    // convert pos into LiquidityPositionsStruct
                    const lowerTick = [];
                    const upperTick = [];
                    const relativeWeight = [];
                    for (const isoPos of newPositions) {
                        lowerTick.push(Number(isoPos.lowerTick));
                        upperTick.push(Number(isoPos.upperTick));
                        relativeWeight.push(Number(isoPos.weight));
                    }
                    const LiqPos = {
                        lowerTick: lowerTick,
                        upperTick: upperTick,
                        relativeWeight: relativeWeight,
                    };
                    await (0, network_1.setNextBlockTimestamp)(120, provider);
                    // eslint-disable-next-line @typescript-eslint/ban-ts-comment
                    // @ts-ignore
                    await (0, replay_1.mineBlock)(provider);
                    let _tendTx;
                    const totalWeight = output.valuesArray[0];
                    const timeSensitive = encoder.encode(['int256', 'uint160'], [(_a = swapResult === null || swapResult === void 0 ? void 0 : swapResult.amountToSwap) === null || _a === void 0 ? void 0 : _a.mul((swapResult === null || swapResult === void 0 ? void 0 : swapResult.zeroForOne) ? 1 : -1), sqrtPrice]);
                    try {
                        const tendTx = await DeployedManager.connect(mainSigner).tend(totalWeight, LiqPos, timeSensitive, {
                            gasLimit: 30000000,
                        });
                        _tendTx = await tendTx.wait();
                    }
                    catch (error) {
                        console.log('🚨 Tend Failure! Make sure your strategy is making positions on initializable ticks (Relative to pool fee)');
                        throw error;
                    }
                    cumulativeGas = cumulativeGas.add(_tendTx.gasUsed);
                    const rebalance = {
                        timestamp: nextEpochTrigger,
                        positions: newPositions,
                        currentTick,
                        balt0: balt0.toString(),
                        balt1: balt1.toString(),
                        gasUsed: _tendTx.gasUsed.toString(),
                    };
                    // add data
                    backtestDataArray.push(rebalance);
                    stats.rebalances.push(rebalance);
                    // set next trigger
                    nextEpochTrigger += epochLength;
                }
                if (event.type === uniswapv3_1.TransactionType.SWAP) {
                    // Number might be negative here, so we check for '-'
                    const swapParams = (event.amount0[0] === '-') ?
                        {
                            tokenIn: token1Address,
                            tokenOut: token0Address,
                            fee: poolFee,
                            recipient: mainSigner.address,
                            deadline: 1920170954,
                            amountIn: (0, utils_1.parseUnits)(event.amount1, token1Decimals),
                            amountOutMinimum: 0,
                            sqrtPriceLimitX96: ethers_1.BigNumber.from('0'),
                        } :
                        {
                            // token0 in 1 out
                            tokenIn: token0Address,
                            tokenOut: token1Address,
                            fee: poolFee,
                            recipient: mainSigner.address,
                            deadline: 1920170954,
                            amountIn: (0, utils_1.parseUnits)(event.amount0, token0Decimals),
                            amountOutMinimum: 0,
                            sqrtPriceLimitX96: ethers_1.BigNumber.from('0'),
                        };
                    try {
                        const swapTx = await swapRouterContract
                            .connect(mainSigner)
                            .exactInputSingle(swapParams, { gasLimit: 30000000 });
                        await swapTx.wait();
                        // tx10 = await tx9.wait() as ContractReceipt;
                    }
                    catch (error) {
                        process.env.DEBUG && console.log(error);
                        txsFailed += 1;
                    }
                    const priceMovement = {
                        timestamp: event.timestamp,
                        tick: currentTick,
                    };
                    priceMovementDataArray.push(priceMovement);
                    stats.poolStates.push(priceMovement);
                }
                if (event.type === uniswapv3_1.TransactionType.MINT) {
                    if (String(event.amount0).includes('.') || String(event.amount1).includes('.')) {
                        continue;
                    }
                    const mintParams = {
                        token0: token0Address,
                        token1: token1Address,
                        fee: poolFee,
                        tickLower: event.tickLower,
                        tickUpper: event.tickUpper,
                        amount0Desired: ethers_1.BigNumber.from(event.amount0),
                        amount1Desired: ethers_1.BigNumber.from(event.amount1),
                        amount0Min: 0,
                        amount1Min: 0,
                        recipient: mainSigner.address,
                        deadline: 1920170954,
                    };
                    try {
                        const mintTx = await nftManagerContract.connect(mainSigner).mint(mintParams);
                        await mintTx.wait();
                        // Add position to our mapping of ticks - id
                        positionMap.set([Number(event.tickLower), Number(event.tickUpper)], tokenIDCounter);
                        tokenIDCounter++;
                    }
                    catch (error) {
                        txsFailed += 1;
                        process.env.DEBUG && console.log(error);
                    }
                }
                if (event.type === uniswapv3_1.TransactionType.BURN) {
                    process.env.DEBUG && console.log('🔥');
                    // See if the token was made by us, or if we should imitate
                    let decreaseParams;
                    if (Number(event.tokenID) < tokenIDStart) {
                        // Imitate
                        const [, tempSigner] = await getImpersonationSigner(event.position.owner, provider);
                        // Ensure the address has Eth
                        await provider.send('hardhat_setBalance', [
                            event.position.owner,
                            '0x999999999000000000000000000',
                        ]);
                        // Burn position
                        decreaseParams = {
                            tokenId: event.tokenID,
                            liquidity: ethers_1.BigNumber.from(event.amount),
                            amount0Min: 0,
                            amount1Min: 0,
                            deadline: 1920170954,
                        };
                        try {
                            const txDec = await nftManagerContract
                                .connect(tempSigner)
                                .decreaseLiquidity(decreaseParams, { gasLimit: 30000000 });
                            await txDec.wait();
                        }
                        catch (error) {
                            txsFailed += 1;
                            process.env.DEBUG && console.log(error);
                        }
                    }
                    else {
                        // Decrease from our mapping
                        // Burn position
                        const currentToken = positionMap.get([
                            Number(event.tickLower),
                            Number(event.tickUpper),
                        ]);
                        if (typeof currentToken !== 'number') {
                            txsFailed += 1;
                            continue;
                        }
                        decreaseParams = {
                            tokenId: ethers_1.BigNumber.from(currentToken),
                            liquidity: ethers_1.BigNumber.from(event.amount),
                            amount0Min: 0,
                            amount1Min: 0,
                            deadline: 1920170954,
                        };
                        try {
                            const txDec = await nftManagerContract
                                .connect(mainSigner)
                                .decreaseLiquidity(decreaseParams, { gasLimit: 30000000 });
                            await txDec.wait();
                        }
                        catch (error) {
                            txsFailed += 1;
                            process.env.DEBUG && console.log(error);
                        }
                    }
                }
            } //  --END event loop--
            const endTime = Date.now();
            process.env.DEBUG && console.log('🥇');
            const txLength = txEventsArray.length;
            const successPercent = Math.round(((txLength - txsFailed) * 10000) / txLength) / 100;
            const simRunTime = (endTime - startTime) / (1000 * 60);
            // console.log('Simulation ran in', simRunTime.toFixed(2), 'minutes')
            const simulatedTime = txEventsArray[txEventsArray.length - 1].timestamp -
                txEventsArray[0].timestamp;
            const YEARInSeconds = 31536000;
            // const compoundsYearly = YEARInSeconds / epochLength
            // @todo withdraw funds, compare and analyze
            const finShareBal = await DeployedManager.balanceOf(POOR_ADDRESS);
            // console.log('ending share bal:', finShareBal.toString())
            await (0, network_1.setNextBlockTimestamp)(5, provider);
            console.log('\n__________');
            await DeployedManager.connect(poorSigner).withdraw(finShareBal, 0, 0, POOR_ADDRESS);
            // The difference of how much principle we started with vs how much was deposited
            // const t0offset = initDept0.gt(t0initialNative) ? BigNumber.from(0) : t0initialNative.sub(initDept0)
            // const t1offset = initDept1.gt(t1initialNative) ? BigNumber.from(0) : t1initialNative.sub(initDept1)
            // Final balances compared to amount deposited (initDept0)
            const finalBal0 = (await token0.balanceOf(POOR_ADDRESS)).sub(t0PostDepositBal);
            const finalBal1 = (await token1.balanceOf(POOR_ADDRESS)).sub(t1PostDepositBal);
            // This helps us get some acceptable precision with various decimal lengths
            const highPoint0 = finalBal0.gt(initDept0) ? finalBal0 : initDept0;
            const highPoint1 = finalBal1.gt(initDept1) ? finalBal1 : initDept1;
            // Here we take (endingBal - Deposit) * precision * 10000 / precision / Deposit / 100 giving us a the percentage change, same for next line Number(finalBal1.toString())
            const fint0change = (finalBal0
                .sub(initDept0)
                .mul(highPoint0.mul(10000))
                .div(highPoint0)
                .div(initDept0)
                .toNumber() / 100).toFixed(2);
            const fint1change = (finalBal1
                .sub(initDept1)
                .mul(highPoint1.mul(10000))
                .div(highPoint1)
                .div(initDept1)
                .toNumber() / 100)
                .toFixed(2);
            // Calculate the current ratio
            // This value might be quite high or very 0<n<1
            const finPrice = (currentSlot.sqrtPriceX96.pow(2));
            const finToken0In1 = (finalBal0).mul(finPrice);
            const finTot1 = (finalBal1).add(finToken0In1.div(X192));
            // Here we can abandon precision, these estimates will only be precise up to 17 digits
            const difference = (finTot1.sub(initTot1)).mul(10000);
            const numDiff = difference.div(initTot1);
            const periodChange = numDiff.toNumber() / 10000;
            // Here we get the day the test ended to call the price of ETH and t1 that day
            dateMS = new Date(txEventsArray[txEventsArray.length - 1].timestamp * 1000);
            dateEN = dateMS
                .toLocaleDateString('en-GB', {
                day: '2-digit',
                month: '2-digit',
                year: 'numeric',
            })
                .replace('/', '-')
                .replace('/', '-');
            // eslint-disable-next-line @typescript-eslint/no-unused-vars
            const t1DataFinalUSD = await CoinGeckoClient.coins.fetchHistory(t1data.data.id, { date: dateEN });
            const ethFinalUSDCG = await CoinGeckoClient.coins.fetchHistory('ethereum', {
                date: dateEN,
            });
            const ethFinalUSD = ethFinalUSDCG.data.market_data.current_price.usd;
            const finT1toUSD = t1DataFinalUSD.data.market_data.current_price.usd;
            const _usdFinValPrecBN = ethers_1.BigNumber.from(Math.round(finT1toUSD * 100000000).toString());
            const finalVal = Number((finTot1.mul(_usdFinValPrecBN).div(ethers_1.BigNumber.from(10).pow(token1Decimals))).toString()) / 100000000;
            // gains over time * times run in year, doesn't factor compunding
            const staticAPY = (YEARInSeconds / simulatedTime) * periodChange * 100;
            // console.log('Linearized APY (less accurate):', staticAPY.toString() + '%')
            // See how many times the funds were compounded in this time period
            const numEpochs = simulatedTime / epochLength;
            const epochPYear = (YEARInSeconds / simulatedTime);
            const APY = ((1 + periodChange) ** epochPYear) - 1;
            // Note true compounding (by epoch length) was retired because it smooths results too much to give unrealistic apys
            // Calculate what the return would have been with compounding each epoch
            // const avgEpochReturn = numEpochs * (((periodChange + 1) ** (1 / numEpochs)) - 1)
            // Take this rate and apply it for a whole year
            // const compoundedAPY = ((1 + avgEpochReturn) ** compoundsYearly) - 1
            // Vs hodl
            const finHodlComparison = Number(initTot1.mul(_usdFinValPrecBN).div(ethers_1.BigNumber.from(10).pow(token1Decimals)).toString()) / 100000000;
            // Gas and epoch info
            const cumGasAdj = ethers_1.ethers.utils.formatEther(cumulativeGas.mul(10 ** 9).mul(Number(gasPrice).toFixed(0)));
            const gasUSD = Number(cumGasAdj) * ethFinalUSD;
            // Token symbols
            const t0Symbol = await token0.symbol();
            const t1Symbol = await token1.symbol();
            console.log('** note USD values are from 18:00 UTC from the start & end dates of the backtest period **');
            console.log(color_1.default.yellow('\nGeneral Run Info'));
            const _table1 = new cli_table3_1.default({
                head: ['Backtest Runtime', 'TX Count', 'Failed TXs', 'Success Rate'],
                colWidths: [20, 10, 20, 20],
            });
            _table1.push([(simRunTime.toFixed(2) + ' minutes'), txLength, txsFailed, successPercent + '%']);
            const runInfo = _table1.options.head.reduce((acc, _curr, index) => {
                acc = {
                    ...acc,
                    [(0, change_case_1.camelCase)(_table1.options.head[index])]: _table1[index],
                };
                return acc;
            }, {});
            console.log(_table1.toString());
            console.log(color_1.default.green('Initial Balance Info'));
            const _table2 = new cli_table3_1.default({
                head: ['Initial Balance ' + t0Symbol, 'Initial Balance ' + t1Symbol, 'Total Principle in ' + t1Symbol, 'Initial Value in USD'],
                colWidths: [25, 25, 35, 25],
            });
            _table2.push([ethers_1.ethers.utils.formatUnits(initDept0, token0Decimals), ethers_1.ethers.utils.formatUnits(initDept1, token1Decimals), ethers_1.ethers.utils.formatUnits(initTot1, token1Decimals), '$' + initVal.toFixed(2)]);
            console.log(_table2.toString());
            const initialBalanceInfo = _table2.options.head.reduce((acc, _curr, index) => {
                acc = {
                    ...acc,
                    [(0, change_case_1.camelCase)(_table2.options.head[index])]: _table2[index],
                };
                return acc;
            }, {});
            console.log(color_1.default.blue('Strategy Info'));
            const _table3 = new cli_table3_1.default({
                head: ['Strategy Runs', 'Gas Spent', 'ETH spent on gas', 'Gas Spent (USD) - end of period price'],
                colWidths: [20, 20, 20, 45],
            });
            _table3.push([Math.floor(numEpochs).toString(), cumulativeGas.toString(), cumGasAdj.toString(), '$' + gasUSD.toFixed(2)]);
            console.log(_table3.toString());
            const strategyInfo = _table3.options.head.reduce((acc, _curr, index) => {
                acc = {
                    ...acc,
                    [(0, change_case_1.camelCase)(_table3.options.head[index])]: _table3[index],
                };
                return acc;
            }, {});
            console.log(color_1.default.green('Final Balance Info'));
            const _table4 = new cli_table3_1.default({
                head: ['Final Balance ' + t0Symbol, 'Change in ' + t0Symbol + ' Holdings', 'Final Balance ' + t1Symbol, 'Change in ' + t1Symbol + ' Holdings', 'Final Total in ' + t1Symbol, 'Final Value in USD'],
                colWidths: [25, 25, 25, 30, 25, 25],
            });
            _table4.push([ethers_1.ethers.utils.formatUnits(finalBal0, token0Decimals), fint0change + '%', ethers_1.ethers.utils.formatUnits(finalBal1, token1Decimals), fint1change + '%', ethers_1.ethers.utils.formatUnits(finTot1, token1Decimals), '$' + finalVal.toFixed(2)]);
            console.log(_table4.toString());
            const finalBalanceInfo = _table4.options.head.reduce((acc, _curr, index) => {
                acc = {
                    ...acc,
                    [(0, change_case_1.camelCase)(_table4.options.head[index])]: _table4[index],
                };
                return acc;
            }, {});
            console.log(color_1.default.magenta('Results'));
            const _table5 = new cli_table3_1.default({
                head: ['Return from Backtest', 'Net USD Adjusted for Gas', 'Compounded APY', 'Linearized APY', 'Hold Change in Value'],
                colWidths: [25, 30, 20, 20],
            });
            _table5.push([(periodChange * 100).toFixed(4) + '%', '$' + (finalVal - initVal - gasUSD).toFixed(2), (100 * APY).toFixed(2) + '%', staticAPY.toFixed(4) + '%', (((finHodlComparison / initVal) - 1) * 100).toFixed(2) + '%']);
            console.log(_table5.toString());
            const results = _table5.options.head.reduce((acc, _curr, index) => {
                acc = {
                    ...acc,
                    [(0, change_case_1.camelCase)(_table5.options.head[index])]: _table5[index],
                };
                return acc;
            }, {});
            stats.results = {
                runInfo,
                initialBalanceInfo,
                strategyInfo,
                finalBalanceInfo,
                results,
            };
        }
        catch (error) {
            progressBar.stop();
            await kill(8545);
            setTimeout(() => {
                throw error;
            }, 10000);
        }
        // ----END TEST----
    }
    getTopHolder(holders, tokenAddress, type, provider) {
        return holders.find(async (holder) => {
            const swapInToken = new ethers_1.Contract(tokenAddress, constants_1.ERC20_ABI, provider);
            const balance = await swapInToken.balanceOf(holder.address);
            return holder.address_type === type && balance.gt(0);
        });
    }
    displayTargetEntityDetails(poolDetails) {
        cli_ux_1.default.table([poolDetails], {
            id: {
                header: 'Address',
                get: (row) => row.id,
            },
            pair: {
                header: 'Pair',
                get: (row) => `${row.token0.name}/${row.token1.name}`,
            },
            feeTier: {
                header: 'Feed Tier',
                get: (row) => `${Number(row.feeTier) / 10000}%`,
            },
            createdAtTimestamp: {
                header: 'Created At',
                get: (row) => new Date(Number(row.createdAtTimestamp) * 1000).toDateString(),
            },
            Liquidity: {
                header: 'Liquidity',
                get: (row) => Number(row.totalValueLockedUSD).toFixed(2),
            },
        });
    }
}
exports.UniswapV3Backtest = UniswapV3Backtest;
const getImpersonationSigner = async (address, provider) => {
    const actor = new network_1.Impersonator(address);
    const signer = await actor.impersonate(provider);
    return [actor, signer];
};
const encodeInitUniV3VaultParams = (token0, token1, fee, maxTotalSupply, _maxTickChange, _twapInterval) => {
    // Handle optional params
    if (maxTotalSupply === undefined) {
        maxTotalSupply = ethers_1.BigNumber.from(10).pow(36);
    }
    if (typeof _maxTickChange === 'undefined' || _maxTickChange === null) {
        _maxTickChange = 250;
    }
    if (typeof _twapInterval === 'undefined') {
        _twapInterval = 45;
    }
    return ethers_1.utils.defaultAbiCoder.encode([
        'address',
        'address',
        'uint24',
        'uint256',
        'int24',
        'int32',
    ], [
        token0,
        token1,
        fee,
        maxTotalSupply,
        _maxTickChange,
        _twapInterval, // How much time
    ]);
};
// The goal is to find the largest holders and compile all of their assets into a single address
// this will allow us to test the vault with a single address that has a large amount of assets
// and replay the transactions on mainnet to see how the vault performs
const provideTokensToAccounts = async (tokens, poolAddress, blockNumber, provider) => {
    // Get the top 20 holders of each token
    const graphCaller = new uniswapv3_1.UniswapV3();
    const holdersT0 = await graphCaller.requestBQTopHolders(tokens[0], blockNumber, 20);
    const holdersT1 = await graphCaller.requestBQTopHolders(tokens[1], blockNumber, 20);
    if (holdersT0.length === 0 || holdersT1.length === 0) {
        const unavailable = holdersT0.length === 0 ? tokens[0] : tokens[1];
        console.log('Could not successfully acquire assets and top token holders for token: ' + unavailable);
        console.log('Try a different token or reach out for further options.');
        // eslint-disable-next-line no-process-exit, unicorn/no-process-exit
        process.exit(1);
    }
    // Parse top holders
    // filter out any zero addresses and any addresses which might be the current pool
    const token0TopHolders = holdersT0.filter(holder => {
        return holder[0] !== poolAddress || holder[0] !== '0x0000000000000000000000000000000000000000';
    });
    const token1TopHolders = holdersT1.filter(holder => {
        return holder[0] !== poolAddress || holder[0] !== '0x0000000000000000000000000000000000000000';
    });
    // Setup an impersonation account for the first holder
    // in the service provider response
    const [mainActor, mainSigner] = await getImpersonationSigner(RICH_ADDRESS, provider);
    await provider.send('hardhat_setBalance', [
        mainActor.address,
        (0, utils_1.parseEther)('1000').toHexString(),
    ]);
    // Once we have the op holders, we can setup the tokens
    // and connect their signers for token0 and token1
    const token0 = new ethers_1.Contract(tokens[0], constants_1.ERC20_ABI, mainSigner);
    const token1 = new ethers_1.Contract(tokens[1], constants_1.ERC20_ABI, mainSigner);
    // Now we need condense all the funds into the main account
    for (const holder of [...token0TopHolders, ...token1TopHolders]) {
        // To do so, we need to make sure all the transfers have enough gas in the various accounts
        await provider.send('hardhat_setBalance', [
            holder,
            (0, utils_1.parseEther)('1000').toHexString(),
        ]);
        const isToken0Holder = token0TopHolders.includes(holder);
        const holdingToken = (isToken0Holder ? token0 : token1);
        // Now we can transfer the funds to the main account
        // We need to impersonate the holder
        const holderActor = new network_1.Impersonator(holder);
        const holderSigner = await holderActor.impersonate(provider);
        // We subtract one to make sure we have a successful transfer
        const amount = await holdingToken.balanceOf(holderSigner.address);
        if (amount.isZero()) {
            continue;
        }
        const transferAmount = amount.sub(1);
        if (holderSigner.address === mainSigner.address) {
            continue;
        }
        else {
            const transferTx = await holdingToken.connect(holderSigner).transfer(mainSigner.address, transferAmount);
            await transferTx.wait();
        }
        await holderActor.stopImpersonate(provider);
    }
    return { mainActor, mainSigner, token0, token1 };
};
