"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 = (0, tslib_1.__importStar)(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 types_1 = require("../../utils/types");
const Uniswapv3_1 = require("../../utils/Uniswapv3");
const webserver_1 = require("../../utils/webserver");
const Strategy_1 = require("../../utils/Strategy");
// 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();
    }
    async run() {
        console.log('\n   ░██████╗████████╗███████╗███████╗██████╗░\n   ██╔════╝╚══██╔══╝██╔════╝██╔════╝██╔══██╗\n   ╚█████╗░░░░██║░░░█████╗░░█████╗░░██████╔╝\n   ░╚═══██╗░░░██║░░░██╔══╝░░██╔══╝░░██╔══██╗\n   ██████╔╝░░░██║░░░███████╗███████╗██║░░██║\n   ╚═════╝░░░░╚═╝░░░╚══════╝╚══════╝╚═╝░░╚═╝\n');
        const { args, flags } = await this.parse(AppBacktest);
        // Parse command flags
        let poolId = flags.pool;
        // Dataset placeholder
        let dataset;
        this.startWebserver(this.stats);
        this.stats.status.active = true;
        this.stats.status.state = types_1.BacktestState.Configuring;
        // Setup data directory location for saved scrapes
        const dataDirectory = node_path_1.default.join(this.config.dataDir, 'data');
        // Read all the files from the data directory
        const savedDatasets = (await (0, promises_1.readdir)(dataDirectory)).filter(file => !file.endsWith('.DS_Store'));
        // Instance of the Uniswap Utility Class, provides historical data requests
        const Uni3Adapter = new Uniswapv3_1.UniswapV3();
        // If there is not a poolId, prompt the user for a token for a pool
        if (poolId === undefined) {
            const tokenAnswers = await this.getPoolId(Uni3Adapter);
            // 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 Uni3Adapter.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 Uni3Adapter.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 Uni3Adapter.findPoolsByToken0(tokenData.id ||
                                '0x0000000000000000000000000000000000000000', tokens))) === null || _a === void 0 ? void 0 : _a.data) === null || _b === void 0 ? void 0 : _b.pools :
                            await Uni3Adapter.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
            poolId = this.parsePoolId(poolAnswers.poolId);
        }
        // If the pool is still undefined, then we have a problem
        // Log the error and exit
        if (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
            const details = await Uni3Adapter.getPoolDetails(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 (!details) {
                this.log('Unable to retrieve pool information. Please try again. Exiting...');
                this.exit(1);
            }
            this.stats.poolDetails = details;
            // One we have the details, we can display them in a table to the user
            Uni3Adapter.displayTargetEntityDetails(details);
            // 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(poolId))) {
                usingPreviousDataset = await this.shouldUsePreviousDataset(usingPreviousDataset);
            }
            const historicalDataLocation = dataDirectory;
            // We will store all of our mint, burns, and swap events in a single array
            let data = [];
            // 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(details.createdAtTimestamp))
                        .toDate(),
                    validate: t => {
                        return (dayjs
                            .unix(t / 1000)
                            .isAfter(dayjs
                            .unix(Number(details.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
                data = await Uni3Adapter.getHistoricalData(details.id, start, end, flags.sleep ? Number(flags.sleep) : 0);
                dataset = `${poolId}_${start.getTime()}_${end.getTime()}_raw.json`;
                // Now that we have data, we need to sort it by timestamp
                // and save it to disk
                data = await this.saveDataset(data, `${historicalDataLocation}/${poolId}_${start.getTime()}_${end.getTime()}_raw.json`);
            }
            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(poolId)),
                    },
                ];
                const { fileLoc } = await inquirer.prompt(chooseDataset);
                dataset = fileLoc;
                // Read the file contents of the chosen file and pull the data out of it
                try {
                    if (fileLoc) {
                        const fileContents = await promises_1.default.readFile(node_path_1.default.resolve(historicalDataLocation + '/' + fileLoc), 'utf8');
                        data = this.sortDataset(JSON.parse(fileContents));
                    }
                }
                catch {
                    this.log('Unable to read dataset. Please try again. Exiting...');
                    this.exit(1);
                }
                // const availableData: Array<string> = fs.readdir("../../../data")
            }
            // Setup historical data for stats storage
            this.stats.historicalData = data;
            // We now can create a token object for token0
            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 = data.filter((d) => d.type === Uniswapv3_1.TransactionType.SWAP);
            // Convert all the swaps to to a timestamp and price for token0 to token1
            const swapData = swaps.map((d) => {
                return {
                    index: dayjs.unix(d.timestamp).toDate().getTime(),
                    value: (Number(new sdk_core_1.Price(Token0, Token1, jsbi_1.default.BigInt(exports.Q192), jsbi_1.default.multiply(jsbi_1.default.BigInt(d.sqrtPriceX96), jsbi_1.default.BigInt(d.sqrtPriceX96))).toFixed(26)) * (10 ** (Token1.decimals - Token0.decimals))).toString(),
                };
            });
            // 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.';
                    },
                },
            ];
            const { candleSize } = await inquirer.prompt(candleSizeQuestions);
            const numCandlesQuestions = [
                {
                    type: 'input',
                    name: 'numCandles',
                    default: '14',
                    message: 'How many candles should be provided to the execution bundle for each tend use?',
                },
            ];
            const { numCandles } = await inquirer.prompt(numCandlesQuestions);
            // Post pretest data gathering to setup the candles
            const candles = await Uni3Adapter.getCandles(swapData, candleSize);
            // Setup candles for stats storage
            this.stats.candles = candles;
            // 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
            // const candles = await Uni3Adapter.getCandles(swapData) // @NOTE: moved down for pretest data inclusion
            // Now we can create the strategy class for the wasm execution module
            const strategy = new Strategy_1.Strategy(args.file);
            // We need to initialize the wasm module by loading it from the disk
            await strategy.load();
            // Now that we have a strategy class, we should add the candles to it
            strategy.setCandles(candles);
            strategy.setNumCandles(Number(numCandles));
            // Begin to retrieve the strategy's parameters
            const strategyInputConfig = await strategy.getConfig();
            this.log('📝 Strategy Input Config:');
            let strategyParams;
            // 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...
            while (!strategyParams) {
                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] = strategy.validateParams(strategyParamsAnswers);
                if (!isValidStrategyParams && paramErrors) {
                    console.log('Input Errors:');
                    // eslint-disable-next-line unicorn/no-for-loop
                    for (let i = 0; i < paramErrors.length; i++) {
                        console.log('\tVariable ' + paramErrors[i].instancePath.slice(1) + ' ' + paramErrors[i].message);
                    }
                }
                else {
                    strategyParams = strategyParamsAnswers;
                }
            }
            // Save all the config data to the strategy class
            strategy.setConfigParams(strategyParams);
            // Setup params for stats storage
            this.stats.params = strategyParams;
            // 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);
            // Calculate the duration based on the users input
            // eslint-disable-next-line @typescript-eslint/ban-ts-comment
            // @ts-ignore
            const epochDuration = dayjs.default.duration(...(epochLength.match(/(\d+)([dhmw])/).slice(1, 3))).asSeconds();
            // INSERT
            // Get the pretest data for the first epoch
            // const durat = new Duration(candleSize)
            // console.log(data[0])
            // console.log(strategyParams.period)
            // console.log(durat.valueOf() / 1000)
            // console.log(Number(data[0].timestamp) - ((durat.valueOf() / 1000) * Number(strategyParams.period)))
            // console.log(data[0].timestamp)
            // const pretestData = await Uni3Adapter.getPreTestData(
            //   details.id,
            //   dayjs.unix(data[0].timestamp - ((durat.valueOf() / 1000) * Number(strategyParams.period))).toDate(),
            //   dayjs.unix(data[0].timestamp).toDate(),
            //   0,
            // )
            // console.log('prepre', pretestData)
            // const preSwapData = pretestData.map((d: any) => {
            //   return {
            //     index: dayjs
            //     .unix(d.timestamp)
            //     .toDate()
            //     .getTime(),
            //     value: new Price(
            //       Token0,
            //       Token1,
            //       Q192,
            //       JSBI.multiply(
            //         JSBI.BigInt(d.sqrtPriceX96),
            //         JSBI.BigInt(d.sqrtPriceX96),
            //       ),
            //     ).toFixed(10),
            //   }
            // })
            // console.log('pre', preSwapData)
            // console.log('swap', swapData)
            // eslint-disable-next-line unicorn/prefer-spread
            // const totCandles = preSwapData.concat(swapData)
            // END INSERT
            // We sorted the data variable, so lets look at the first one
            // and determine the starting block number
            const diffBlockNum = data[0].transaction.blockNumber - 1;
            // 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 ${details.token0.symbol} in the vault:`,
                    validate: (value) => {
                        try {
                            ethers_1.utils.parseUnits(String(value), details.token0.decimals);
                            return true;
                        }
                        catch {
                            return 'Please enter a valid number. Example: 1.23';
                        }
                    },
                },
                {
                    type: 'input',
                    name: 'token1InitialAmount',
                    message: `Starting amount of ${details.token1.symbol} in the vault:`,
                    validate: (value) => {
                        try {
                            ethers_1.utils.parseUnits(String(value), details.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);
            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), details.token0.decimals);
                            return true;
                        }
                        catch {
                            return 'Please enter a valid number. Example: 1.23';
                        }
                    },
                },
            ];
            const { gasPrice } = await inquirer.prompt(gasPriceQuestions);
            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);
            // 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)(`npx hardhat node --fork https://eth-mainnet.alchemyapi.io/v2/Q115S9wBEp4Z2CZPmW8RTfwXCIjevA19 --fork-block-number ${diffBlockNum}`, { cwd: this.config.dataDir, stdio: null }, function (err, _stdout, _stderr) {
                    if (err) {
                        // console.log('Error while attempting to fork mainnet:', 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:', error)
                    }
                    await new Promise(resolve => {
                        setTimeout(resolve, 5000);
                    });
                }
            });
            // Now that we are started we can create a Uniswap backtest class
            const backtest = new backtest_1.UniswapV3Backtest();
            // Lets run the backtest
            try {
                await backtest.run({
                    token0InitialAmount,
                    token1InitialAmount,
                    location: node_path_1.default.resolve(historicalDataLocation + '/' + dataset),
                    epochLength: String(epochDuration),
                    poolId: poolId,
                }, strategy, this.stats, gasPrice);
                this.stats.status.state = types_1.BacktestState.Finished;
            }
            catch (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 });
            }
        }
    }
    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);
        // Once it's sorted, we can write the contents to the data directory
        await promises_1.default.writeFile(node_path_1.default.resolve(historicalDataLocation), JSON.stringify(sortedHistoricalData, null, 2), '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 tokensBySymbol = await Uni3Adapter.findTokensBySymbol(input || 'Steer');
                    const tokensByName = await Uni3Adapter.findTokensByName(input || 'Steer');
                    return [
                        ...tokensBySymbol.data.tokens,
                        ...tokensByName.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;
    }
    startWebserver(stats) {
        this.server = new webserver_1.WebServer(8012, 'localhost', '/');
        this.server.start();
        this.server.addRoute('/status', (req, res) => {
            res.send(stats.status);
        });
        this.server.addRoute('/candles', (req, res) => {
            res.send(stats.candles);
        });
        this.server.addRoute('/rebalances', (req, res) => {
            res.send(stats.rebalances);
        });
        this.server.addRoute('/poolStates', (req, res) => {
            res.send(stats.poolStates);
        });
        this.server.addRoute('/pool', (req, res) => {
            res.send(stats.poolDetails || {});
        });
        this.server.addRoute('/params', (req, res) => {
            res.send(stats.params);
        });
        this.server.addRoute('/historical-data', (req, res) => {
            res.send(stats.historicalData);
        });
        this.server.addRoute('/results', (req, res) => {
            res.send(stats.results);
        });
    }
}
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.",
    }),
};
AppBacktest.args = [
    { name: 'file', required: true, description: 'File to load strategy from' },
];
