"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.UniswapV3Backtest = void 0;
const tslib_1 = require("tslib");
// Uniswap V3 Contracts
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 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 QuoterV2_json_1 = (0, tslib_1.__importDefault)(require("@uniswap/v3-periphery/artifacts/contracts/lens/QuoterV2.sol/QuoterV2.json"));
const axios_1 = (0, tslib_1.__importDefault)(require("axios"));
const cli_progress_1 = (0, tslib_1.__importDefault)(require("cli-progress"));
const cli_ux_1 = (0, tslib_1.__importDefault)(require("cli-ux"));
const cli_table3_1 = (0, tslib_1.__importDefault)(require("cli-table3"));
const color_1 = (0, tslib_1.__importDefault)(require("@oclif/color"));
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");
const events_1 = require("../utils/events");
const network_1 = require("../utils/network");
const constants_1 = require("./../utils/constants");
const types_1 = require("./types");
const Uniswapv3_1 = require("./Uniswapv3");
const kill = require('kill-port');
const uniswap_position_quoter_1 = require("@steerprotocol/uniswap-position-quoter");
const replay_1 = require("./replay");
const change_case_1 = require("change-case");
// 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('Fetching top holders for t0...');
            // Largest holder lookup
            const { data: token0TopHolders } = await axios_1.default.get(`https://api.bloxy.info/token/token_holders_list?token=${txEventsArray[0].token0.id}&limit=5&key=ACCtm4ZHsPBqS&format=structure`);
            console.log('Fetching top holders for t1...');
            const { data: token1TopHolders } = await axios_1.default.get(`https://api.bloxy.info/token/token_holders_list?token=${txEventsArray[0].token1.id}&limit=5&key=ACCtm4ZHsPBqS&format=structure`);
            // console.log(token0TopHolders)
            // Parse top holders
            const token0TopHolder = await this.getTopHolder(token0TopHolders, txEventsArray[0].token0.id, 'Wallet', provider);
            const token1TopHolder = await this.getTopHolder(token1TopHolders, txEventsArray[0].token1.id, 'Wallet', provider);
            // fund top holder wallets for token0 and token1
            await provider.send('hardhat_setBalance', [
                token0TopHolder.address,
                (0, utils_1.parseEther)('1000').toHexString(),
            ]);
            await provider.send('hardhat_setBalance', [
                token1TopHolder.address,
                (0, utils_1.parseEther)('1000').toHexString(),
            ]);
            // Setup an impersonation account for the top token0 holder
            const [mainActor, mainSigner] = await getImpersonationSigner(token0TopHolder.address, provider);
            // 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(token0Address, constants_1.ERC20_ABI, mainSigner);
            const token1 = new ethers_1.Contract(token1Address, constants_1.ERC20_ABI, mainSigner);
            // Move all the assets from the token 0 top holder into the token 1
            // top holder, that way we can use one account for all simulations
            const t1HolderActor = new network_1.Impersonator(token1TopHolder.address);
            const t1Signer = await t1HolderActor.impersonate(provider);
            const t1TransferBal = (await token1.balanceOf(t1Signer.address)).sub(1);
            const trt1tx = await token1
                .connect(t1Signer)
                .transfer(mainSigner.address, t1TransferBal);
            await trt1tx.wait();
            await t1HolderActor.stopImpersonate(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('SteerBacktest', 'StrBT', token0Address, token1Address, poolFee, ethers_1.BigNumber.from(10).pow(36));
            // 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
            0, initParams);
            // Wait for the tx to be mined
            await txV3.wait();
            const fee = await (await poolContract.fee()).toString();
            const txSetPool = await DeployedManager.setPool(UniswapV3Factory, fee);
            const tx = await txSetPool.wait();
            // set up poor account to act as sole investor of the strategy
            // arbitrary address, should have no tokens however
            const poorAddress = '0x0000000000000000133700000000000000000000';
            // 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', [
                poorAddress,
                (0, utils_1.parseEther)('1000').toHexString(),
            ]);
            // 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);
            // Transfer the tokens from the combined top token0 address to the poor actor
            await token0.connect(mainSigner).transfer(poorAddress, t0initialNative);
            await token1.connect(mainSigner).transfer(poorAddress, t1initialNative);
            // Next, we should impersonate the poor actor and approve for the
            // uni liq vault
            const [, poorSigner] = await getImpersonationSigner(poorAddress, provider);
            // 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, poorAddress);
            // Wait for the tx to be mined
            await depTx.wait();
            // Track actual deposit amounts based on what
            // was taken from the poor actor's wallet
            const initDept0 = Number(t0initialNative.sub(await token0.balanceOf(poorAddress)).toString());
            const initDept1 = Number(t1initialNative.sub(await token1.balanceOf(poorAddress)).toString());
            const currentSlot = await poolContract.slot0();
            // ratio * 1e18
            const X192 = 2 ** 192;
            const initPrice = Number(currentSlot.sqrtPriceX96.pow(2).toString()) / (X192);
            const initToken0In1 = initDept0 * (initPrice);
            const initTot1 = initDept1 + initToken0In1;
            // 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 initVal = (initTot1 / (10 ** token1Decimals)) * initT1toUSD;
            // 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 = [];
                    // console.log('about to tend')
                    // eslint-disable-next-line @typescript-eslint/ban-ts-comment
                    // @ts-ignore
                    const output = await strategy.reloadExecute({
                        currentTime: event.timestamp,
                    });
                    const newPositions = [];
                    // eslint-disable-next-line eqeqeq, no-eq-null
                    if (output == null || output.bins == null) {
                        console.log('Bin failure, skipping tend');
                        console.log(output);
                        nextEpochTrigger += epochLength;
                        continue;
                    }
                    for (const bin of output.bins) {
                        const temp = { lowerTick: Number(bin.lowerBound), upperTick: Number(bin.upperBound), weight: Number(bin.weight) };
                        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),
                        });
                    }
                    const swapResult = await PositionQuoter.getSwapAmount(poolObj, pos, balt0, balt1, token0Address, token1Address, poolFee, 20);
                    if (!swapResult) {
                        console.log('🚨 🚨 Skipping tend due to swap result failure');
                        continue;
                    }
                    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;
                    try {
                        const tendTx = await DeployedManager.connect(mainSigner).tend(LiqPos, (_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, {
                            gasLimit: 30000000,
                        });
                        _tendTx = await tendTx.wait();
                    }
                    catch (error) {
                        console.log('🚨 Tend Failure! Make sure your strategy is not realizing increased losses!');
                        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) {
                    const swapParams = ethers_1.BigNumber.from(Math.round(Number(event.amount0) * (10 ** token0Decimals))).lt(0) ?
                        {
                            tokenIn: token1Address,
                            tokenOut: token0Address,
                            fee: poolFee,
                            recipient: mainSigner.address,
                            deadline: 1920170954,
                            amountIn: ethers_1.BigNumber.from((Math.round(Number(event.amount1) * (10 ** token1Decimals))).toLocaleString('fullwide', { useGrouping: false })),
                            amountOutMinimum: 0,
                            sqrtPriceLimitX96: ethers_1.BigNumber.from('0'),
                        } :
                        {
                            // token0 in 1 out
                            tokenIn: token0Address,
                            tokenOut: token1Address,
                            fee: poolFee,
                            recipient: mainSigner.address,
                            deadline: 1920170954,
                            amountIn: ethers_1.BigNumber.from((Math.round(Number(event.amount0) * (10 ** token0Decimals))).toLocaleString('fullwide', { useGrouping: false })),
                            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(poorAddress);
            // 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, poorAddress);
            // The difference of how much principle we started with vs how much was deposited
            const t0offset = t0initialNative.sub(ethers_1.BigNumber.from(initDept0.toString()));
            const t1offset = t1initialNative.sub(ethers_1.BigNumber.from(initDept1.toString()));
            // Final balances
            const finalBal0 = (await token0.balanceOf(poorAddress)).sub(t0offset);
            const finalBal1 = (await token1.balanceOf(poorAddress)).sub(t1offset);
            // This helps us get some acceptable precision with various decimal lengths
            const highPoint0 = finalBal0.gt(ethers_1.BigNumber.from(initDept0.toString())) ? finalBal0 : ethers_1.BigNumber.from(initDept0.toString());
            const highPoint1 = finalBal1.gt(ethers_1.BigNumber.from(initDept1.toString())) ? finalBal1 : ethers_1.BigNumber.from(initDept1.toString());
            // Here we take (endingBal - Deposit) * precision * 10000 / precision / Deposit / 100 giving us a the percentage change, same for next line
            const fint0change = (finalBal0
                .sub(ethers_1.BigNumber.from(initDept0.toString()))
                .mul(highPoint0.mul(10000))
                .div(highPoint0)
                .div(ethers_1.BigNumber.from(initDept0.toString()))
                .toNumber() / 100).toFixed(2);
            const fint1change = (finalBal1
                .sub(initDept1.toString())
                .mul(highPoint1.mul(10000))
                .div(highPoint1)
                .div(initDept1.toString())
                .toNumber() / 100)
                .toFixed(2);
            // Calculate the current ratio
            const finPrice = Number(currentSlot.sqrtPriceX96.pow(2).toString()) / (X192);
            const finToken0In1 = Number(finalBal0.toString()) * (finPrice);
            const finTot1 = Number(finalBal1.toString()) + finToken0In1;
            const periodChange = ((finTot1 - initTot1) / initTot1);
            // 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 = ethFinalUSDCG.data.market_data.current_price.usd;
            const finalVal = (finTot1 / (10 ** token1Decimals)) * finT1toUSD;
            // 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 = (initTot1 / (10 ** token1Decimals)) * finT1toUSD;
            // 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;
            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 Native Balance T0', 'Initial Native Balance T1', 'Total Principle in Native T1', 'Initial Value in USD'],
                colWidths: [25, 25, 35, 25],
            });
            _table2.push([initDept0.toString(), initDept1.toString(), initTot1.toString(), '$' + 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 Native Balance T0', 'Change in T0 Holdings', 'Final Native Balance T1', 'Change in T1 Holdings', 'Final Total Native T1', 'Final Value in USD'],
                colWidths: [25, 25, 25, 30, 25, 25],
            });
            _table4.push([finalBal0.toString(), fint0change + '%', finalBal1.toString(), fint1change + '%', finTot1, '$' + 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 = (LPName, LPSymbol, token0, token1, fee, maxTotalSupply, totalFee, steerFractionOfFee, _maxTickChange, _twapInterval) => {
    // Handle optional params
    if (maxTotalSupply === undefined) {
        maxTotalSupply = ethers_1.BigNumber.from(10).pow(36);
    }
    if (totalFee === undefined) {
        totalFee = 1500;
    }
    if (steerFractionOfFee === undefined) {
        steerFractionOfFee = 6667;
    }
    if (typeof _maxTickChange === 'undefined' || _maxTickChange === null) {
        _maxTickChange = 250;
    }
    if (typeof _twapInterval === 'undefined') {
        _twapInterval = 45;
    }
    return ethers_1.utils.defaultAbiCoder.encode([
        'address',
        'address',
        'uint24',
        'uint256',
        'uint16',
        'uint16',
        'int24',
        'int32',
    ], [
        token0,
        token1,
        fee,
        maxTotalSupply,
        totalFee,
        steerFractionOfFee,
        _maxTickChange,
        _twapInterval, // How much time
    ]);
};
