"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Q192 = exports.Q96 = void 0;
const tslib_1 = require("tslib");
/* eslint-disable no-negated-condition */
const core_1 = require("@oclif/core");
const sdk_core_1 = require("@uniswap/sdk-core");
const dayjs = (0, tslib_1.__importStar)(require("dayjs"));
const duration_1 = (0, tslib_1.__importDefault)(require("dayjs/plugin/duration"));
const ethers_1 = require("ethers");
const exec_sh_1 = (0, tslib_1.__importDefault)(require("exec-sh"));
const inquirer = (0, tslib_1.__importStar)(require("inquirer"));
const inquirer_autocomplete_prompt_1 = (0, tslib_1.__importDefault)(require("inquirer-autocomplete-prompt"));
const inquirer_date_prompt_1 = (0, tslib_1.__importDefault)(require("inquirer-date-prompt"));
const jsbi_1 = (0, tslib_1.__importDefault)(require("jsbi"));
const promises_1 = require("node:fs/promises");
const node_path_1 = (0, tslib_1.__importDefault)(require("node:path"));
const backtest_1 = require("../../utils/backtest");
const stats_1 = require("../../utils/stats");
const Strategy_1 = require("../../utils/Strategy");
const chains_1 = require("../../utils/chains");
const types_1 = require("../../utils/types");
const uniswapv3_1 = require("../../utils/uniswapv3");
const webserver_1 = require("../../utils/webserver");
let runCount = 0;
const supportedChains = [
    chains_1.Chain.Mainnet,
    chains_1.Chain.Polygon,
    chains_1.Chain.Arbitrum,
    // Chain.Optimism,
    chains_1.Chain.BSC,
];
// eslint-disable-next-line unicorn/prefer-module
const kill = require('kill-port');
// eslint-disable-next-line unicorn/prefer-module
const axios = require('axios').default;
dayjs.extend(duration_1.default);
inquirer.registerPrompt('autocomplete', inquirer_autocomplete_prompt_1.default);
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
inquirer.registerPrompt('date', inquirer_date_prompt_1.default);
exports.Q96 = jsbi_1.default.exponentiate(jsbi_1.default.BigInt(2), jsbi_1.default.BigInt(96));
exports.Q192 = jsbi_1.default.exponentiate(exports.Q96, jsbi_1.default.BigInt(2));
class AppBacktest extends core_1.Command {
    constructor() {
        super(...arguments);
        this.server = undefined;
        this.stats = new stats_1.Stats();
        this.file = undefined;
        this.poolId = undefined;
        // Setup data directory location for saved scrapes
        this.dataDirectory = node_path_1.default.join(this.config.dataDir, 'data');
        this.dataset = undefined;
        this.candleSize = undefined;
        this.candles = [];
        this.strategyParams = undefined;
        this.datasetData = [];
        this.poolDetails = undefined;
        this.strategy = undefined;
        this.token0StartAmount = undefined;
        this.token1StartAmount = undefined;
        this.gasPrice = undefined;
        this.epochDuration = undefined;
        this.chain = undefined;
        this.useInterface = false;
        this.keepAlive = true;
        this.shouldProceed = false;
        // Instance of the Uniswap Utility Class, provides historical data requests
        this.UniV3Adapter = new uniswapv3_1.UniswapV3(chains_1.Chain.Mainnet);
        this.ingestionStatus = {
            active: false,
            error: null,
        };
        this.swapData = [];
        this.backtestStarted = false;
    }
    // eslint-disable-next-line complexity
    async run() {
        console.log('\n   ░██████╗████████╗███████╗███████╗██████╗░\n   ██╔════╝╚══██╔══╝██╔════╝██╔════╝██╔══██╗\n   ╚█████╗░░░░██║░░░█████╗░░█████╗░░██████╔╝\n   ░╚═══██╗░░░██║░░░██╔══╝░░██╔══╝░░██╔══██╗\n   ██████╔╝░░░██║░░░███████╗███████╗██║░░██║\n   ╚═════╝░░░░╚═╝░░░╚══════╝╚══════╝╚═╝░░╚═╝\n');
        const { args, flags } = await this.parse(AppBacktest);
        // Parse command flags
        this.poolId = flags.pool;
        this.useInterface = flags.interface;
        this.file = args.file;
        this.startWebserver();
        while (this.keepAlive) {
            await this.getTargetChain();
            this.stats.status.active = true;
            this.stats.status.state = types_1.BacktestState.Configuring;
            // Read all the files from the data directory
            const savedDatasets = await this.getDatasets();
            // If there is not a poolId, prompt the user for a token for a pool
            await this.getPool();
            // If the pool is still undefined, then we have a problem
            // Log the error and exit
            if (this.poolId === undefined) {
                this.log('Unable to determine pool for simulation. Please try again. Exiting...');
                this.exit(1);
            }
            else {
                // Once we have a pool, we now can pull the pool details
                // with the UniswapV3 utility class
                this.poolDetails = await this.UniV3Adapter.getPoolDetails(this.poolId);
                // Users can pass in a pool address and therefore we need to check
                // to make sure the detials were pull aka the pool is valid
                if (!this.poolDetails) {
                    this.log('Unable to retrieve pool information. Please try again. Exiting...');
                    this.exit(1);
                }
                this.stats.poolDetails = this.poolDetails;
                // One we have the details, we can display them in a table to the user
                this.UniV3Adapter.displayTargetEntityDetails(this.poolDetails);
                // We will store all of our mint, burns, and swap events in a single array
                this.datasetData = [];
                if (!this.useInterface) {
                    // Lets assume we don't use a previous dataset
                    let usingPreviousDataset = false;
                    // If the data directory has a dataset for the current pool, lets ask the users if they want to use it
                    if (savedDatasets.some(name => name.includes(this.poolId))) {
                        usingPreviousDataset = await this.shouldUsePreviousDataset(usingPreviousDataset);
                    }
                    // If we are are not used a previously saved data est then we need to fetch it
                    if (usingPreviousDataset === false) {
                        // Before we pull the data, we need to know the start and end dates
                        const start = dayjs
                            .default((await inquirer.prompt({
                            type: 'date',
                            name: 'Start Date',
                            message: 'Start backtesting from which date?',
                            default: dayjs
                                .unix(Number(this.poolDetails.createdAtTimestamp))
                                .toDate(),
                            validate: t => {
                                return (dayjs
                                    .unix(t / 1000)
                                    .isAfter(dayjs
                                    .unix(Number(this.poolDetails.createdAtTimestamp))
                                    .subtract(1, 'minute')
                                    .toDate()) || 'Start time must be at or after pool creation time');
                            },
                        }))['Start Date'])
                            .toDate();
                        const end = dayjs
                            .default((await inquirer.prompt({
                            type: 'date',
                            name: 'End Date',
                            message: 'End backtesting from which date?',
                            default: dayjs.default(start).add(14, 'day').toDate(),
                            validate: t => {
                                return (dayjs
                                    .unix(t / 1000)
                                    .isAfter(dayjs.default(start).subtract(1, 'minute')) ||
                                    'End time must be after start time');
                            },
                        }))['End Date'])
                            .toDate();
                        this.log('📥 Pulling historical data from the data connector...');
                        // Run historical data ingestion
                        const [newData] = await this.pullAndSaveUniswapPoolData(this.poolDetails.id, start, end, flags.sleep ? Number(flags.sleep) : 0);
                        this.datasetData = newData;
                    }
                    else {
                        // IF we are using a previously saved dataset, we need to get the file the user would like to use
                        const chooseDataset = [
                            {
                                type: 'list',
                                name: 'fileLoc',
                                message: 'Select dataset to use',
                                choices: savedDatasets.filter(name => name.includes(this.poolId)),
                            },
                        ];
                        const { fileLoc } = await inquirer.prompt(chooseDataset);
                        this.dataset = fileLoc;
                    }
                }
                else {
                    // wait for the user to select a dataset
                    console.log('waiting for dataset');
                    await new Promise(resolve => {
                        const interval = setInterval(() => {
                            if (this.dataset !== undefined) {
                                clearInterval(interval);
                                resolve(true);
                            }
                        }, 500);
                    });
                }
                // Read the file contents of the chosen file and pull the data out of it
                if (this.dataset) {
                    await this.changeDataset(this.dataset);
                }
                // Setup historical data for stats storage
                this.stats.historicalData = this.datasetData;
                console.log('📈 Processing historical data... completed ✅');
                // We now can create a token object for token0
                const swapData = this.parseSwapsFromDataset(this.poolDetails);
                this.swapData = swapData;
                // TODO: add handling for bundles with no candles, or no related params
                const candleSizeQuestions = [
                    {
                        type: 'input',
                        name: 'candleSize',
                        default: '1d',
                        message: 'What is the desired candle size? (Example: 30m,4h,3d):',
                        validate: (value) => {
                            return /\d+[dhms]/.test(value) ?
                                true :
                                'Please use the correct format.';
                        },
                    },
                ];
                // Once we have price data from swap data, we can build candles
                // during this process we will also ask the users for the candle size
                if (!this.useInterface) {
                    const { candleSize } = await inquirer.prompt(candleSizeQuestions);
                    this.candleSize = candleSize;
                }
                else {
                    this.candleSize = '30m';
                    // wait for the user to select a candle size
                    // console.log('waiting for candleSize')
                    // await new Promise(resolve => {
                    //   const interval = setInterval(() => {
                    //     if (this.candleSize !== undefined) {
                    //       clearInterval(interval)
                    //       resolve(true)
                    //     }
                    //   }, 500)
                    // })
                }
                // Post pretest data gathering to setup the candles
                await this.setDatasetCandles(swapData);
                // Setup candles for stats storage
                this.stats.candles = this.candles;
                // Now we can create the strategy class for the wasm execution module
                await this.loadStrategy(args.file);
                if (!this.strategy) {
                    throw new Error('Unable to load strategy');
                }
                // Now that we have a strategy class, we should add the candles to it
                this.strategy.setCandles(this.candles);
                // Begin to retrieve the strategy's parameters
                const strategyInputConfig = await this.strategy.getConfig();
                // Create the question corresponding to the strategy's parameters
                // then ask the user to answer them
                // then validate, if they are not valid, ask them again
                // once complete, continue...
                this.strategyParams = await this.getStrategyParams(strategyInputConfig);
                // Save all the config data to the strategy class
                this.strategy.setConfigParams(this.strategyParams);
                // Setup params for stats storage
                this.stats.params = this.strategyParams;
                if (!this.useInterface) {
                    // Last but not least, we need to get the strategy's epoch distance
                    // aka, how often should we run the strategy
                    const epochQuestions = [
                        {
                            type: 'input',
                            name: 'epochLength',
                            default: '1w',
                            message: 'How often should the strategy run? (i.e. 4h,1d,1w):',
                            validate: (value) => {
                                return (/(\d+)([dhmw])/.test(value) ||
                                    'Please enter a valid whole number.');
                            },
                        },
                    ];
                    const { epochLength } = await inquirer.prompt(epochQuestions);
                    this.epochDuration = epochLength;
                }
                else {
                    console.log('waiting for epochDuration');
                    await new Promise(resolve => {
                        const interval = setInterval(() => {
                            if (this.epochDuration !== undefined) {
                                clearInterval(interval);
                                resolve(true);
                            }
                        }, 500);
                    });
                }
                // We sorted the data variable, so lets look at the first one
                // and determine the starting block number
                const swap = this.datasetData.filter(({ type }) => type === uniswapv3_1.TransactionType.SWAP);
                // sort ascending order
                const sortSwap = swap.sort((a, b) => a.transaction.blockNumber - b.transaction.blockNumber);
                const diffBlockNum = sortSwap[0].transaction.blockNumber - 1;
                if (!this.useInterface) {
                    // The next information we need to gather is the amounts of liquidity for the tokens
                    const vaultQuestions = [
                        {
                            type: 'input',
                            name: 'token0InitialAmount',
                            message: `Starting amount of ${this.poolDetails.token0.symbol} in the vault:`,
                            validate: (value) => {
                                try {
                                    ethers_1.utils.parseUnits(String(value), this.poolDetails.token0.decimals);
                                    return true;
                                }
                                catch {
                                    return 'Please enter a valid number. Example: 1.23';
                                }
                            },
                        },
                        {
                            type: 'input',
                            name: 'token1InitialAmount',
                            message: `Starting amount of ${this.poolDetails.token1.symbol} in the vault:`,
                            validate: (value) => {
                                try {
                                    ethers_1.utils.parseUnits(String(value), this.poolDetails.token1.decimals);
                                    return true;
                                }
                                catch {
                                    return 'Please enter a valid number. Example: 1.23';
                                }
                            },
                        },
                    ];
                    console.log('⚠️ Significant token amounts relevant to the pool will negatively effect the pool simulation');
                    // We parse the user responses
                    const { token0InitialAmount, token1InitialAmount } = await inquirer.prompt(vaultQuestions);
                    this.token0StartAmount = token0InitialAmount;
                    this.token1StartAmount = token1InitialAmount;
                    const gasPriceQuestions = [
                        {
                            type: 'input',
                            name: 'gasPrice',
                            default: '21',
                            message: 'What gas price do you want to assume? (measured in gwei):',
                            validate: (value) => {
                                try {
                                    ethers_1.utils.parseUnits(String(value), this.poolDetails.token0.decimals);
                                    return true;
                                }
                                catch {
                                    return 'Please enter a valid number. Example: 1.23';
                                }
                            },
                        },
                    ];
                    const { gasPrice } = await inquirer.prompt(gasPriceQuestions);
                    this.gasPrice = gasPrice;
                }
                else {
                    await new Promise(resolve => {
                        const interval = setInterval(() => {
                            if (this.token0StartAmount !== undefined &&
                                this.token1StartAmount !== undefined &&
                                this.gasPrice !== undefined) {
                                clearInterval(interval);
                                resolve(true);
                            }
                        }, 500);
                    });
                }
                this.log('⏳ Prepping environment for testing...');
                this.stats.status.state = types_1.BacktestState.Warmup;
                // To prepare the environment for testing, we need to
                // make sure nothing is running on the default hardhat port
                await kill(8545);
                const getChainKey = () => {
                    switch (this.chain) {
                        case chains_1.Chain.Arbitrum:
                            return 'https://arb-mainnet.g.alchemy.com/v2/k4pN3WdBEFdAf2tsqMb1fe1QE6aG0s4_';
                        case chains_1.Chain.Polygon:
                            return 'https://polygon-mainnet.g.alchemy.com/v2/jgxsKkj5LsrTmRWxNgUAu6xoUQYlTKkU';
                        case chains_1.Chain.Mainnet:
                            return 'https://eth-mainnet.alchemyapi.io/v2/Q115S9wBEp4Z2CZPmW8RTfwXCIjevA19';
                        case chains_1.Chain.Optimism:
                            return 'https://opt-mainnet.g.alchemy.com/v2/c0gJGJWNTp71q8WdiX3BODATdJhkDaFf';
                        case chains_1.Chain.BSC:
                            return 'https://morning-special-haze.bsc.discover.quiknode.pro/fc2455f967f4632b03f83b212ca5c524d06fabdc/';
                    }
                };
                const forkCommand = `npx hardhat node --fork ${getChainKey()} --fork-block-number ${diffBlockNum} --verbose`;
                // If there is nothing on the hardhat port, we will start up a hardhat node headless
                // and fork at the given block numbers based on the datasets first block number
                // eslint-disable-next-line no-new
                new Promise((resolve, reject) => {
                    (0, exec_sh_1.default)(forkCommand, { cwd: this.config.dataDir, stdio: null }, function (err, _stdout, _stderr) {
                        if (err) {
                            console.log('Error with local fork:', err);
                            reject();
                        }
                        resolve(null);
                    });
                });
                // Hardhat takes time to start, so we must wait
                // eslint-disable-next-line no-async-promise-executor
                await new Promise(async (resolve) => {
                    let isOnline = false;
                    while (!isOnline) {
                        try {
                            await axios.get('http://localhost:8545');
                            this.log('Local node ready. Starting simulation...');
                            isOnline = true;
                            resolve(true);
                        }
                        catch {
                            // console.log('Error while attempting to connect to node:', e);
                        }
                        await new Promise(resolve => {
                            setTimeout(resolve, 5000);
                        });
                    }
                });
                // Now that we are started we can create a Uniswap backtest class
                if (this.useInterface) {
                    await new Promise(resolve => {
                        const interval = setInterval(() => {
                            if (this.backtestStarted) {
                                clearInterval(interval);
                                resolve(true);
                            }
                        }, 500);
                    });
                }
                this.startBacktest();
            }
            if (this.useInterface) {
                await new Promise(resolve => {
                    const interval = setInterval(() => {
                        if (this.shouldProceed) {
                            clearInterval(interval);
                            resolve(true);
                        }
                    }, 500);
                });
            }
            if (runCount > 0) {
                this.resetState();
            }
            runCount += 1;
        } // end while
    }
    async getStrategyParams(strategyInputConfig) {
        if (!this.useInterface && this.strategy) {
            while (!this.strategyParams) {
                // eslint-disable-next-line max-depth
                if (!this.useInterface) {
                    this.log('📝 Strategy Input Config:');
                    const strategyInputConfigQuestions = Object.keys(strategyInputConfig.properties).map(key => {
                        const obj = {
                            name: key,
                            message: `${key} (${strategyInputConfig.properties[key].description}):`,
                        };
                        if (strategyInputConfig.properties[key].enum !== undefined) {
                            obj.type = 'list';
                            if (strategyInputConfig.properties[key].enumNames !== undefined) {
                                // create obj list
                                const enumList = [];
                                for (let i = 0; i < strategyInputConfig.properties[key].enum.length; i++) {
                                    enumList.push({
                                        name: strategyInputConfig.properties[key].enumNames[i],
                                        value: strategyInputConfig.properties[key].enum[i],
                                    });
                                }
                                obj.choices = enumList;
                            }
                            else {
                                obj.choices = strategyInputConfig.properties[key].enum;
                            }
                        }
                        if (strategyInputConfig.properties[key].default !== undefined) {
                            obj.default = strategyInputConfig.properties[key].default;
                        }
                        if (strategyInputConfig.properties[key].type === 'number' ||
                            strategyInputConfig.properties[key].type === 'integer') {
                            obj.type = 'number';
                        }
                        if (strategyInputConfig.properties[key].type === 'input') {
                            obj.type = 'input';
                        }
                        return obj;
                    });
                    const strategyParamsAnswers = await inquirer.prompt(strategyInputConfigQuestions);
                    // Validate input params
                    const [isValidStrategyParams, paramErrors] = this.strategy.validateParams(strategyParamsAnswers);
                    // eslint-disable-next-line max-depth
                    if (!isValidStrategyParams && paramErrors) {
                        console.log('Input Errors:');
                        // eslint-disable-next-line unicorn/no-for-loop, max-depth
                        for (let i = 0; i < paramErrors.length; i++) {
                            console.log('\tVariable ' +
                                paramErrors[i].instancePath.slice(1) +
                                ' ' +
                                paramErrors[i].message);
                        }
                    }
                    else {
                        this.strategyParams = strategyParamsAnswers;
                    }
                }
            }
        }
        else {
            await new Promise(resolve => {
                const interval = setInterval(() => {
                    if (this.strategyParams !== undefined) {
                        clearInterval(interval);
                        resolve(true);
                    }
                }, 500);
            });
        }
        if (this.strategyParams === undefined) {
            throw new Error('Unable to parse strategy params');
        }
        return this.strategyParams;
    }
    async getPool() {
        if (this.poolId === undefined && !this.useInterface) {
            const tokenAnswers = await this.getPoolId(this.UniV3Adapter);
            // We need to parse the pool address so we can look up it's information
            // Then we parse the tokens associated with the pool
            const { data: poolTokens } = await this.UniV3Adapter.findTokensByAddress(this.parsePoolId(tokenAnswers.poolId));
            // We then parse out the first token
            // this provides the data we need to query linked pools
            const tokenData = poolTokens === null || poolTokens === void 0 ? void 0 : poolTokens.tokens[0];
            // We now pull pools with this above token on both side of the market
            // (aka token0 and token1)
            const poolQuestions = [
                {
                    type: 'autocomplete',
                    name: 'poolId',
                    message: 'Please pick a pool backtesting against or filter further with providing a token symbol',
                    source: async (answersSoFar, input) => {
                        var _a, _b;
                        const { data: { tokens: tokensBySymbol }, } = await this.UniV3Adapter.findTokensBySymbol(input || 'Steer', true);
                        const tokens = tokensBySymbol === null || tokensBySymbol === void 0 ? void 0 : tokensBySymbol.map((token) => {
                            return token.id;
                        });
                        const poolsByTokenAddress = tokens.length > 0 ?
                            (_b = (_a = (await this.UniV3Adapter.findPoolsByToken0(tokenData.id ||
                                '0x0000000000000000000000000000000000000000', tokens))) === null || _a === void 0 ? void 0 : _a.data) === null || _b === void 0 ? void 0 : _b.pools :
                            await this.UniV3Adapter.findPoolsByToken(tokenData.id || '0x0000000000000000000000000000000000000000');
                        return (poolsByTokenAddress || []).map(pool => `${pool.token0.name}/${pool.token1.name} - ${Number(pool.feeTier) / 10000}% - ${pool.id}`);
                    },
                },
            ];
            // The final pool chosen by the user
            const poolAnswers = await inquirer.prompt(poolQuestions);
            // parsed the pool id
            this.poolId = this.parsePoolId(poolAnswers.poolId);
        }
        if (this.useInterface) {
            // wait for the user to select a pool
            console.log('waiting for pool selection');
            await new Promise(resolve => {
                const interval = setInterval(() => {
                    if (this.poolId !== undefined) {
                        clearInterval(interval);
                        resolve(true);
                    }
                }, 500);
            });
        }
    }
    async getTargetChain() {
        if (!this.useInterface) {
            const answer = await inquirer.prompt([{
                    type: 'list',
                    name: 'chain',
                    message: 'Select chain',
                    choices: supportedChains.map((chain) => chains_1.Chain[chain]),
                }]);
            this.chain = answer.chain;
        }
        else {
            console.log('waiting for chain selection');
            await new Promise(resolve => {
                const interval = setInterval(() => {
                    if (this.chain !== undefined) {
                        clearInterval(interval);
                        resolve(true);
                    }
                }, 500);
            });
        }
        if (this.chain) {
            this.UniV3Adapter = new uniswapv3_1.UniswapV3(this.chain);
        }
    }
    async startBacktest() {
        const backtest = new backtest_1.UniswapV3Backtest(this.UniV3Adapter);
        console.log('running backtest with the following parameters:', [
            {
                token0InitialAmount: String(this.token0StartAmount),
                token1InitialAmount: String(this.token1StartAmount),
                location: node_path_1.default.resolve(this.dataDirectory + '/' + this.dataset),
                epochLength: String(this.getEpochDurationInSeconds(this.epochDuration)),
                poolId: this.poolId,
            },
            this.strategy,
            this.stats,
            this.gasPrice,
        ]);
        // Lets run the backtest
        try {
            if (this.strategy) {
                await backtest.run({
                    token0InitialAmount: String(this.token0StartAmount),
                    token1InitialAmount: String(this.token1StartAmount),
                    location: node_path_1.default.resolve(this.dataDirectory + '/' + this.dataset),
                    epochLength: String(this.getEpochDurationInSeconds(this.epochDuration)),
                    poolId: this.poolId,
                    gasPrice: this.gasPrice,
                    candleSize: this.candleSize || '30m',
                }, this.strategy, this.stats);
            }
            this.stats.status.state = types_1.BacktestState.Finished;
        }
        catch (error) {
            console.log('🧙‍♂️ 🔎 -> ~ file: backtest.ts:671 ~ AppBacktest ~ run ~ error', error);
            this.stats.status.active = false;
            this.stats.status.error = error.message;
            this.stats.status.state = types_1.BacktestState.Stopped;
            this.error('🚨 Simulation has failed. Please try again.', { exit: 1 });
        }
    }
    async loadStrategy(file) {
        this.strategy = new Strategy_1.Strategy(file);
        // We need to initialize the wasm module by loading it from the disk
        await this.strategy.load();
        return this.strategy;
    }
    getEpochDurationInSeconds(epochLength) {
        if (!epochLength) {
            return 0;
        }
        // Calculate the duration based on the users input
        // eslint-disable-next-line @typescript-eslint/ban-ts-comment
        // @ts-ignore
        // prettier-ignore
        return dayjs.default.duration(...(epochLength.match(/(\d+)([dhmw])/).slice(1, 3))).asSeconds();
    }
    parseSwapsFromDataset(details) {
        const Token0 = new sdk_core_1.Token(1, details.token0.id, Number(details.token0.decimals), details.token0.symbol, details.token0.name);
        // and we can create a token object for token1
        const Token1 = new sdk_core_1.Token(1, details.token1.id, Number(details.token1.decimals), details.token1.symbol, details.token1.name);
        // Pull only swap types from the data
        // We will use this to create the OHCL data
        const swaps = this.datasetData.filter((d) => d.type === uniswapv3_1.TransactionType.SWAP);
        const swapData = [];
        let i = 0;
        // Convert all the swaps to to a timestamp and price for token0 to token1
        // while loops are faster than maps and ++i is better for memory than i++
        // this decrease the loop time by 1ms, not a big change but good to have.
        while (i < swaps.length) {
            const swapPrice = new sdk_core_1.Price(Token0, Token1, jsbi_1.default.BigInt(exports.Q192), jsbi_1.default.multiply(jsbi_1.default.BigInt(swaps[i].sqrtPriceX96), jsbi_1.default.BigInt(swaps[i].sqrtPriceX96)))
                .quote(sdk_core_1.CurrencyAmount.fromRawAmount(Token0, ethers_1.utils.parseUnits('1', Token0.decimals).toString()));
            swapData.push({
                index: dayjs.unix(swaps[i].timestamp).toDate().getTime(),
                // eslint-disable-next-line unicorn/require-number-to-fixed-digits-argument
                value: swapPrice.toFixed(),
            });
            ++i;
        }
        return swapData;
    }
    async changeDataset(dataset) {
        this.dataset = dataset;
        try {
            if (this.dataset) {
                const fileContents = await (0, promises_1.readFile)(node_path_1.default.resolve(this.dataDirectory + '/' + this.dataset), 'utf8');
                this.datasetData = this.sortDataset(JSON.parse(fileContents));
            }
            else {
                this.datasetData = [];
            }
            return this.datasetData;
        }
        catch (error) {
            console.error(error);
            this.log('Unable to read dataset. Please try again. Exiting...');
            this.exit(1);
        }
    }
    async reloadStrategy() {
        if (this.file) {
            const strategy = await this.loadStrategy(this.file);
            if (this.strategyParams) {
                strategy.setConfigParams(this.strategyParams);
                strategy.getConfig();
            }
        }
    }
    async setDatasetCandles(swapData) {
        if (this.candleSize) {
            this.candles = this.UniV3Adapter.getCandles(swapData, this.candleSize);
            this.stats.candles = this.candles;
            return this.candles;
        }
        return this.candles;
    }
    async pullAndSaveUniswapPoolData(pool, start, end, sleep) {
        this.ingestionStatus.active = true;
        let data = [];
        try {
            data = await this.UniV3Adapter.getHistoricalData(pool, start, end, sleep);
        }
        catch (error) {
            console.log('Backtest failed to pull data from Uniswap', error);
            this.ingestionStatus.active = false;
            this.ingestionStatus.error = error.message;
        }
        const datasetName = `${pool}_${start.getTime()}_${end.getTime()}_raw.json`;
        // Now that we have data, we need to sort it by timestamp
        // and save it to disk
        const candles = await this.saveDataset(data, `${this.dataDirectory}/${datasetName}`);
        this.ingestionStatus.active = false;
        this.dataset = datasetName;
        return [candles, this.dataset];
    }
    async getDatasets(pool) {
        return (await (0, promises_1.readdir)(this.dataDirectory))
            .filter(file => !file.endsWith('.DS_Store'))
            .filter(file => (pool ? file.startsWith(pool) : true));
    }
    sortDataset(data) {
        // Now that we have data, we need to sort it by timestamp
        return data.sort(function (a, b) {
            return Number(a.timestamp) - Number(b.timestamp);
        });
    }
    async saveDataset(data, historicalDataLocation) {
        const sortedHistoricalData = this.sortDataset(data);
        const filePath = node_path_1.default.resolve(historicalDataLocation);
        // Once it's sorted, we can write the contents to the data directory
        await (0, promises_1.writeFile)(filePath, JSON.stringify(sortedHistoricalData), 'utf8');
        return sortedHistoricalData;
    }
    async shouldUsePreviousDataset(_usingOldData) {
        const useExistingQuestions = [
            {
                type: 'confirm',
                name: 'usingOldData',
                message: 'Would you like to use previous saved dataset?',
                default: false,
            },
        ];
        const { usingOldData } = await inquirer.prompt(useExistingQuestions);
        return usingOldData;
    }
    async getPoolId(Uni3Adapter) {
        const tokenQuestions = [
            {
                type: 'autocomplete',
                name: 'poolId',
                message: 'Provide a token name or symbol which you would like to backtest against',
                source: async (answersSoFar, input) => {
                    const tokens = await Uni3Adapter.findTokens(input || 'Steer');
                    return tokens.data.tokens.map((token) => `${token.name} - ${token.symbol} - ${token.id}`);
                },
            },
        ];
        const tokenAnswers = await inquirer.prompt(tokenQuestions);
        return tokenAnswers;
    }
    parsePoolId(poolId) {
        const id = poolId.split(' - ')[2];
        return id;
    }
    groupCandlesByEpoch(candles, epochDuration) {
        var _a;
        const groupedCandles = new Map();
        for (const candle of candles) {
            const epoch = Math.floor(candle.timestamp / epochDuration);
            if (!groupedCandles.has(epoch)) {
                groupedCandles.set(epoch, []);
            }
            (_a = groupedCandles.get(epoch)) === null || _a === void 0 ? void 0 : _a.push(candle);
        }
        return groupedCandles;
    }
    combineCandlesUpToEpoch(candles, epochDuration, upToEpoch) {
        const groupedCandles = this.groupCandlesByEpoch(candles, epochDuration);
        const combinedCandles = [];
        for (const [epoch, epochCandles] of groupedCandles) {
            if (epoch <= upToEpoch) {
                for (const candle of epochCandles) {
                    combinedCandles.push(candle);
                }
            }
            else {
                break;
            }
        }
        return combinedCandles;
    }
    epochToTimestamp(epoch, epochDuration) {
        return epoch * epochDuration;
    }
    timestampToEpoch(timestamp, epochDuration) {
        return Math.floor(timestamp / epochDuration);
    }
    determineFirstEpoch(candles, epochDuration) {
        let firstEpoch = Number.POSITIVE_INFINITY;
        for (const candle of candles) {
            const epoch = this.timestampToEpoch(candle.timestamp, epochDuration);
            if (epoch < firstEpoch) {
                firstEpoch = epoch;
            }
        }
        return firstEpoch;
    }
    determineLastEpoch(candles, epochDuration) {
        let lastEpoch = 0;
        for (const candle of candles) {
            const epoch = this.timestampToEpoch(candle.timestamp, epochDuration);
            if (epoch > lastEpoch) {
                lastEpoch = epoch;
            }
        }
        return lastEpoch;
    }
    async previewRebalances() {
        if (this.candles && this.epochDuration) {
            const firstEpoch = this.determineFirstEpoch(this.candles, this.getEpochDurationInSeconds(this.epochDuration) * 1000);
            const lastEpoch = this.determineLastEpoch(this.candles, this.getEpochDurationInSeconds(this.epochDuration) * 1000);
            const epochResults = [];
            if (this.strategy) {
                for (let epoch = firstEpoch; epoch <= lastEpoch; epoch++) {
                    // Create an array of all the candles up to the current epoch
                    const epochData = this.combineCandlesUpToEpoch(this.candles, this.getEpochDurationInSeconds(this.epochDuration) * 1000, epoch);
                    // Execute the strategy on the candles
                    try {
                        const result = (await this.strategy.execute(epochData));
                        // Add the results to the epochResults array
                        epochResults.push({
                            ...result,
                            epoch,
                            timestamp: this.epochToTimestamp(epoch, this.getEpochDurationInSeconds(this.epochDuration) * 1000),
                        });
                    }
                    catch (error) {
                        console.log('Strategy failed:', error);
                    }
                }
            }
            const rebalancePreviews = epochResults.map(output => {
                const positions = [];
                const boundsAndWeight = output.valuesArray[1];
                for (let i = 0; i < boundsAndWeight[0].length; i++) {
                    positions.push({
                        lowerTick: boundsAndWeight[0][i],
                        upperTick: boundsAndWeight[1][i],
                        weight: boundsAndWeight[2][i],
                    });
                }
                return {
                    timestamp: output.timestamp,
                    positions,
                };
            });
            return rebalancePreviews;
        }
        return [];
    }
    resetState() {
        this.candles = [];
        this.epochDuration = undefined;
        this.strategy = undefined;
        this.poolId = undefined;
        this.candleSize = undefined;
        this.backtestStarted = false;
        this.shouldProceed = false;
        this.poolDetails = undefined;
        this.stats = new stats_1.Stats();
    }
    resetSimulation() {
        this.resetState();
        this.stats.reset();
    }
    startWebserver() {
        this.server = new webserver_1.WebServer(8012, 'localhost', '/');
        this.server.start();
        this.server.addRoute('/status', (req, res) => {
            res.send(this.stats.status);
        });
        this.server.addRoute('/candles', (req, res) => {
            res.send(this.candles);
        });
        this.server.addRoute('/candles/interval', (req, res) => {
            res.send(this.candleSize);
        });
        this.server.addRoute('/rebalances', (req, res) => {
            res.send(this.stats.rebalances);
        });
        this.server.addRoute('/rebalances/preview', async (req, res) => {
            try {
                const results = await this.previewRebalances();
                res.send({ status: 'success', data: results });
            }
            catch (error) {
                res.send({ status: 'error', message: error.message });
            }
        });
        this.server.addRoute('/poolStates', (req, res) => {
            res.send(this.stats.poolStates);
        });
        this.server.addRoute('/pool', (req, res) => {
            res.send(this.stats.poolDetails || {});
        });
        this.server.addRoute('/params', (req, res) => {
            res.send(this.stats.params);
        });
        this.server.addRoute('/historical-data', (req, res) => {
            res.send(this.stats.historicalData);
        });
        this.server.addRoute('/results', (req, res) => {
            res.send(this.stats.results);
        });
        this.server.addRoute('/app/config', async (req, res) => {
            if (this.file) {
                const strategy = new Strategy_1.Strategy(this.file);
                // We need to initialize the wasm module by loading it from the disk
                await strategy.load();
                res.send(await strategy.getConfig());
            }
            else {
                res.send({
                    status: 'error',
                    error: 'No strategy file provided',
                });
            }
        });
        this.server.addRoute('/uniswap/datasets', async (req, res) => {
            // pull query params
            const { pool } = req.query;
            res.send(await this.getDatasets(pool === null || pool === void 0 ? void 0 : pool.toString()));
        });
        this.server.addRoute('/uniswap/dataset/current', async (req, res) => {
            res.send(this.dataset);
        });
        this.server.addRoute('/uniswap/dataset/change', async (req, res) => {
            const { dataset } = req.body;
            await this.changeDataset(dataset);
            res.send({
                status: 'ok',
            });
        }, 'post');
        this.server.addRoute('/uniswap/dataset/ingest', async (req, res) => {
            var _a, _b, _c;
            // pull query params
            const { pool, start, end, sleep } = req.body;
            console.log('🧙‍♂️ 🔎 -> ~ file: backtest.ts:877 ~ AppBacktest ~ this.server.addRoute ~ {pool, start, end, sleep}', { pool, start, end, sleep });
            const startTime = new Date(start);
            const endTime = new Date(end);
            if (((_a = String(pool)) === null || _a === void 0 ? void 0 : _a.length) > 0 &&
                ((_b = String(start)) === null || _b === void 0 ? void 0 : _b.length) > 0 &&
                ((_c = String(end)) === null || _c === void 0 ? void 0 : _c.length) > 0) {
                try {
                    this.pullAndSaveUniswapPoolData(pool, startTime, endTime, Number(sleep || 0));
                    res.send({
                        status: 'ok',
                    });
                }
                catch (error) {
                    res.send({
                        status: 'error',
                        error: error.message,
                    });
                }
            }
            else {
                res.send({
                    status: 'error',
                    error: 'Invalid params provided',
                });
            }
        }, 'post');
        this.server.addRoute('/uniswap/dataset/ingest/status', async (req, res) => {
            // pull query params
            res.send(this.ingestionStatus);
        });
        // POST routes
        this.server.addRoute('/uniswap/setPool', async (req, res) => {
            try {
                // Get the pool address from the request body
                const { pool } = req.body;
                if (!this.chain) {
                    res.send({
                        status: 'error',
                        error: 'Target chain must be set before setting pool',
                    });
                }
                else {
                    // Create a new instance of the UniswapV3 class
                    const uniswap = new uniswapv3_1.UniswapV3(this.chain);
                    // Get the pool details from the UniswapV3 class
                    const poolDetailsResponse = await uniswap.getPoolDetails(pool);
                    // If the pool details response is invalid, return an error
                    if (!poolDetailsResponse) {
                        res.send({
                            status: 'error',
                            error: 'Invalid pool provided',
                        });
                    }
                    // Set the pool details
                    this.stats.poolDetails = poolDetailsResponse;
                    this.poolId = poolDetailsResponse.id;
                    this.candles = [];
                    this.datasetData = [];
                    this.dataset = undefined;
                    // Send a successful response
                    res.send({
                        status: 'ok',
                    });
                }
            }
            catch (error) {
                // Send a response with an error
                res.send({
                    status: 'error',
                    error: error,
                });
            }
        }, 'post');
        this.server.addRoute('/uniswap/pool/details', async (req, res) => {
            // pull query params
            const swap = (this.datasetData.reverse().find(val => val.type === uniswapv3_1.TransactionType.SWAP));
            res.send(swap);
        });
        this.server.addRoute('/app/config', async (req, res) => {
            var _a;
            try {
                // Get the pool address from the request body
                const { config } = req.body;
                // Set the pool details
                this.stats.params = config;
                this.strategyParams = config;
                (_a = this.strategy) === null || _a === void 0 ? void 0 : _a.setConfigParams(config);
                // Send a successful response
                res.send({
                    status: 'ok',
                });
            }
            catch (error) {
                // Send a response with an error
                res.send({
                    status: 'error',
                    error: error,
                });
            }
        }, 'post');
        this.server.addRoute('/app/chain', async (req, res) => {
            try {
                // Get the pool address from the request body
                const { chain } = req.body;
                const parsedChain = chain;
                if (parsedChain === undefined) {
                    res.send({
                        status: 'error',
                        error: 'Invalid chain provided',
                    });
                }
                this.chain = chain;
                if (this.chain) {
                    this.UniV3Adapter.chain = this.chain;
                }
                // Send a successful response
                res.send({
                    status: 'ok',
                });
            }
            catch (error) {
                // Send a response with an error
                res.send({
                    status: 'error',
                    error: error,
                });
            }
        }, 'post');
        this.server.addRoute('/app/chain', (_req, res) => {
            res.send({ chain: this.chain });
        });
        this.server.addRoute('/app/restart', async (req, res) => {
            try {
                this.shouldProceed = true;
                // Send a successful response
                res.send({
                    status: 'ok',
                });
            }
            catch (error) {
                // Send a response with an error
                res.send({
                    status: 'error',
                    error: error,
                });
            }
        }, 'post');
        this.server.addRoute('/candles/change', async (req, res) => {
            try {
                // Get the pool address from the request body
                const { candleSize } = req.body;
                this.candleSize = candleSize;
                if (this.poolDetails) {
                    this.setDatasetCandles(this.swapData);
                }
                // Send a successful response
                res.send({
                    status: 'ok',
                });
            }
            catch (error) {
                // Send a response with an error
                res.send({
                    status: 'error',
                    error: error,
                });
            }
        }, 'post');
        this.server.addRoute('/uniswap/settings', async (req, res) => {
            try {
                // Get the pool address from the request body
                const { token0StartAmount, token1StartAmount, gasPrice, epochDuration, } = req.body;
                this.token0StartAmount = token0StartAmount;
                this.token1StartAmount = token1StartAmount;
                this.gasPrice = gasPrice;
                this.epochDuration = epochDuration;
                // Send a successful response
                res.send({
                    status: 'ok',
                });
            }
            catch (error) {
                // Send a response with an error
                res.send({
                    status: 'error',
                    error: error,
                });
            }
        }, 'post');
        this.server.addRoute('/simulation/start', async (_req, res) => {
            this.backtestStarted = true;
            res.send({
                status: 'ok',
            });
        }, 'post');
        this.server.addRoute('/app/reload', async (_req, res) => {
            try {
                await this.reloadStrategy();
                res.send({
                    status: 'ok',
                });
            }
            catch (error) {
                res.send({
                    status: 'error',
                    error: error,
                });
            }
        }, 'post');
        this.server.addRoute('/uniswap/settings', async (req, res) => {
            try {
                // Send a successful response
                res.send({
                    token0StartAmount: typeof this.token0StartAmount === 'undefined' ? this.token0StartAmount : `${this.token0StartAmount}`,
                    token1StartAmount: typeof this.token1StartAmount === 'undefined' ? this.token1StartAmount : `${this.token1StartAmount}`,
                    gasPrice: this.gasPrice,
                    epochDuration: this.epochDuration,
                });
            }
            catch (error) {
                // Send a response with an error
                res.send({
                    status: 'error',
                    error: error,
                });
            }
        });
    }
}
exports.default = AppBacktest;
AppBacktest.description = 'Backtest a strategy against a given data connector';
AppBacktest.examples = ['<%= config.bin %> <%= command.id %>'];
AppBacktest.flags = {
    // flag with a value (-n, --name=VALUE)
    pool: core_1.Flags.string({
        char: 'p',
        description: 'Pool which the strategy will be tested against.',
    }),
    sleep: core_1.Flags.string({
        char: 's',
        description: "Sleep setting for pulling historical data in seconds. Useful if pulling large datasets or you don't want to be rate limited.",
    }),
    interface: core_1.Flags.boolean({
        char: 'i',
        default: false,
        description: 'Use interface to run the backtest. Useful if you want to run the backtest in a browser.',
    }),
};
AppBacktest.args = [
    { name: 'file', required: true, description: 'File to load strategy from' },
];
