UNPKG

@backtest/framework

Version:

Backtesting trading strategies in TypeScript / JavaScript

391 lines 21.2 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.run = run; const orders_1 = require("./orders"); const parse_1 = require("./parse"); const prisma_historical_data_1 = require("./prisma-historical-data"); const strategies_1 = require("./strategies"); const error_1 = require("./error"); const common_1 = require("../core/common"); const logger = __importStar(require("./logger")); async function run(runParams) { var _a, _b, _c, _d, _e; if (!runParams) { throw new error_1.BacktestError('No options specified', error_1.ErrorCode.MissingInput); } if (!runParams.strategyName) { throw new error_1.BacktestError('Strategy name must be specified', error_1.ErrorCode.MissingInput); } if (!((_a = runParams.historicalData) === null || _a === void 0 ? void 0 : _a.length)) { throw new error_1.BacktestError('Historical data names must be specified', error_1.ErrorCode.MissingInput); } const strategyFilePath = (0, strategies_1.getStrategy)(runParams.strategyName, runParams.rootPath); if (!strategyFilePath) { throw new error_1.BacktestError(`Strategy file ${runParams.strategyName}.ts not found.`, error_1.ErrorCode.StrategyNotFound); } runParams.alwaysFreshLoad && delete require.cache[require.resolve(strategyFilePath)]; const strategy = await Promise.resolve(`${strategyFilePath}`).then(s => __importStar(require(s))); const runStartTime = new Date().getTime(); if ((strategy === null || strategy === void 0 ? void 0 : strategy.runStrategy) === undefined) { throw new error_1.BacktestError(`${runParams.strategyName} file does not have a function with the name of runStrategy.\nIt is mandatory to export a function with this name:\n\nexport async function runStrategy(bth: BTH) {}`, error_1.ErrorCode.StrategyError); } let multiSymbol = runParams.historicalData.length > 1; let multiParams = false; let permutations = [{}]; let permutationReturn = []; if (Object.keys(runParams.params).length !== 0) { for (const key in runParams.params) { const paramsKey = runParams.params[key]; if ((typeof paramsKey === 'string' && paramsKey.includes(',')) || (Array.isArray(paramsKey) && paramsKey.length > 1)) { logger.trace(`Found multiple values for ${key}`); multiParams = true; permutations = (0, parse_1.generatePermutations)(runParams.params); break; } } } const supportCandles = []; const supportCandlesByInterval = {}; const historicalNames = [...new Set(runParams.historicalData)]; const supportHistoricalNames = [...new Set(runParams.supportHistoricalData)]; let basePair = undefined; for (let supportCount = 0; supportCount < supportHistoricalNames.length; supportCount++) { const candlesRequest = await (0, prisma_historical_data_1.getCandles)(supportHistoricalNames[supportCount]); if (!candlesRequest) { throw new error_1.BacktestError(`Candles for ${supportHistoricalNames[supportCount]} not found`, error_1.ErrorCode.NotFound); } const candles = candlesRequest.candles; const metaCandle = (_c = (_b = candlesRequest.metaCandles) === null || _b === void 0 ? void 0 : _b[0]) !== null && _c !== void 0 ? _c : null; if (!metaCandle) { throw new error_1.BacktestError(`Historical data for ${supportHistoricalNames[supportCount]} not found`, error_1.ErrorCode.NotFound); } if (!basePair) { basePair = metaCandle.symbol; } else if (metaCandle.symbol !== basePair) { throw new error_1.BacktestError(`All symbols must have the same base pair. ${metaCandle.symbol} does not match ${basePair}`, error_1.ErrorCode.InvalidInput); } supportCandlesByInterval[metaCandle.interval] = candles; supportCandles.push(...candles); } for (let symbolCount = 0; symbolCount < historicalNames.length; symbolCount++) { const historicalName = historicalNames[symbolCount]; const candlesRequest = await (0, prisma_historical_data_1.getCandles)(historicalName); if (!candlesRequest) { throw new error_1.BacktestError(`Candles for ${historicalName} not found`, error_1.ErrorCode.NotFound); } const candles = candlesRequest.candles; const historicalData = (_e = (_d = candlesRequest.metaCandles) === null || _d === void 0 ? void 0 : _d[0]) !== null && _e !== void 0 ? _e : null; if (!historicalData) { throw new error_1.BacktestError(`Historical data for ${historicalName} not found`, error_1.ErrorCode.NotFound); } if (!!basePair && basePair != historicalData.symbol) { throw new error_1.BacktestError(`All symbols must have the same base pair. ${historicalData.symbol} does not match ${basePair}`, error_1.ErrorCode.InvalidInput); } if ((strategy === null || strategy === void 0 ? void 0 : strategy.startCallback) !== undefined) { await (strategy === null || strategy === void 0 ? void 0 : strategy.startCallback(historicalName)); } const tradingInterval = historicalData.interval; for (let permutationCount = 0; permutationCount < permutations.length; permutationCount++) { if (multiParams) { runParams.params = permutations[permutationCount]; } await _resetOrders(runParams); const allWorths = []; const tradableCandles = candles.filter((c) => c.closeTime >= runParams.startTime && c.closeTime <= runParams.endTime); const numberOfCandles = tradableCandles.length; const firstCandle = tradableCandles[0]; const lastCandle = tradableCandles[numberOfCandles - 1]; const runMeta = _initializeRunMetaData(runParams, firstCandle, lastCandle, numberOfCandles); const allHistoricalData = Object.assign({}, { [historicalData.interval]: candles }, supportCandlesByInterval); const allCandles = _filterAndSortCandles(runParams, tradingInterval, candles, supportCandles); for (const currentCandle of allCandles) { let canBuySell = true; const tradingCandle = currentCandle.interval === tradingInterval; async function buy(buyParams) { return _buy(runParams, tradingCandle, canBuySell, currentCandle, buyParams); } async function sell(buyParams) { return _sell(runParams, tradingCandle, canBuySell, currentCandle, buyParams); } async function getCandles(type, start, end) { return _getCandles(allHistoricalData, canBuySell, currentCandle, type, start, end); } if (tradingCandle) { const { open, high, low, close, closeTime } = currentCandle; if (orders_1.orderBook.stopLoss > 0) { if (orders_1.orderBook.baseAmount > 0 && low <= orders_1.orderBook.stopLoss) { await sell({ price: orders_1.orderBook.stopLoss }); } else if (orders_1.orderBook.borrowedBaseAmount > 0 && high >= orders_1.orderBook.stopLoss) { await sell({ price: orders_1.orderBook.stopLoss }); } } if (orders_1.orderBook.takeProfit > 0) { if (orders_1.orderBook.baseAmount > 0 && high >= orders_1.orderBook.takeProfit) { await sell({ price: orders_1.orderBook.takeProfit }); } else if (orders_1.orderBook.borrowedBaseAmount > 0 && low <= orders_1.orderBook.takeProfit) { await sell({ price: orders_1.orderBook.takeProfit }); } } const worth = await (0, orders_1.getCurrentWorth)(close, high, low, open); if (worth.low <= 0) { throw new error_1.BacktestError(`Your worth in this candle dropped to zero or below. It's recommended to manage shorts with stop losses. Lowest worth this candle: ${worth.low}, Date: ${(0, parse_1.dateToString)(closeTime)}`, error_1.ErrorCode.StrategyError); } allWorths.push({ close: worth.close, high: worth.high, low: worth.low, open: worth.open, time: closeTime }); if (high > runMeta.highestAssetAmount) { runMeta.highestAssetAmount = high; runMeta.highestAssetAmountDate = closeTime; } if (low < runMeta.lowestAssetAmount) { runMeta.lowestAssetAmount = low; runMeta.lowestAssetAmountDate = closeTime; } if (worth.high > runMeta.highestAmount) { runMeta.highestAmount = worth.high; runMeta.highestAmountDate = closeTime; } if (worth.low < runMeta.lowestAmount) { runMeta.lowestAmount = worth.low; runMeta.lowestAmountDate = closeTime; if (runMeta.highestAmount - worth.low > runMeta.maxDrawdownAmount) { runMeta.maxDrawdownAmount = (0, parse_1.round)(runMeta.highestAmount - worth.low); runMeta.maxDrawdownAmountDates = `${(0, parse_1.dateToString)(runMeta.highestAmountDate)} - ${(0, parse_1.dateToString)(closeTime)} : ${(0, parse_1.getDiffInDays)(runMeta.highestAmountDate, closeTime)}`; } const drawdownPercent = ((runMeta.highestAmount - worth.low) / runMeta.highestAmount) * 100; if (drawdownPercent > runMeta.maxDrawdownPercent) { runMeta.maxDrawdownPercent = (0, parse_1.round)(drawdownPercent); runMeta.maxDrawdownPercentDates = `${(0, parse_1.dateToString)(runMeta.highestAmountDate)} - ${(0, parse_1.dateToString)(closeTime)} : ${(0, parse_1.getDiffInDays)(runMeta.highestAmountDate, closeTime)}`; } } } try { await strategy.runStrategy({ tradingInterval, tradingCandle, currentCandle, params: runParams.params, orderBook: orders_1.orderBook, allOrders: orders_1.allOrders, buy, sell, getCandles }); } catch (error) { logger.error(error); throw new error_1.BacktestError(`Ran into an error running the strategy with error ${error.message || error}`, error_1.ErrorCode.StrategyError); } if (tradingCandle) { if (orders_1.orderBook.bought) { runMeta.numberOfCandlesInvested++; const currentCandles = allHistoricalData[currentCandle.interval]; const lastCandle = currentCandles === null || currentCandles === void 0 ? void 0 : currentCandles.slice(-1)[0]; if (lastCandle && lastCandle.closeTime === currentCandle.closeTime) { await sell(); } } } } if ((strategy === null || strategy === void 0 ? void 0 : strategy.finishCallback) !== undefined) { await (strategy === null || strategy === void 0 ? void 0 : strategy.finishCallback(historicalName)); } runMeta.sharpeRatio = (0, parse_1.calculateSharpeRatio)(allWorths); logger.debug(`Strategy ${runParams.strategyName} completed in ${Date.now() - runStartTime} ms`); if (multiParams || multiSymbol) { const assetAmounts = {}; assetAmounts.startingAssetAmount = runMeta.startingAssetAmount; assetAmounts.endingAssetAmount = runMeta.endingAssetAmount; assetAmounts.highestAssetAmount = runMeta.highestAssetAmount; assetAmounts.highestAssetAmountDate = runMeta.highestAssetAmountDate; assetAmounts.lowestAssetAmount = runMeta.lowestAssetAmount; assetAmounts.lowestAssetAmountDate = runMeta.lowestAssetAmountDate; assetAmounts.numberOfCandles = numberOfCandles; if (historicalData) { permutationReturn.push({ ...runParams.params, symbol: historicalData.symbol, interval: historicalData.interval, endAmount: allWorths[allWorths.length - 1].close, maxDrawdownAmount: runMeta.maxDrawdownAmount, maxDrawdownPercent: runMeta.maxDrawdownPercent, numberOfCandlesInvested: runMeta.numberOfCandlesInvested, sharpeRatio: runMeta.sharpeRatio, assetAmounts }); } } else { return { allOrders: orders_1.allOrders, runMetaData: runMeta, allWorths, allCandles: tradableCandles }; } } } return permutationReturn; } async function _resetOrders(runParams) { orders_1.orderBook.bought = false; orders_1.orderBook.boughtLong = false; orders_1.orderBook.boughtShort = false; orders_1.orderBook.baseAmount = 0; orders_1.orderBook.quoteAmount = runParams.startingAmount; orders_1.orderBook.borrowedBaseAmount = 0; orders_1.orderBook.fakeQuoteAmount = runParams.startingAmount; orders_1.orderBook.preBoughtQuoteAmount = runParams.startingAmount; orders_1.orderBook.stopLoss = 0; orders_1.orderBook.takeProfit = 0; await (0, orders_1.clearOrders)(); } function _initializeRunMetaData(runParams, firstCandle, lastCandle, numberOfCandles) { return { highestAmount: runParams.startingAmount, highestAmountDate: firstCandle.closeTime, lowestAmount: runParams.startingAmount, lowestAmountDate: firstCandle.closeTime, maxDrawdownAmount: 0, maxDrawdownAmountDates: '', maxDrawdownPercent: 0, maxDrawdownPercentDates: '', startingAssetAmount: firstCandle.close, startingAssetAmountDate: firstCandle.closeTime, endingAssetAmount: lastCandle.close, endingAssetAmountDate: lastCandle.closeTime, highestAssetAmount: firstCandle.high, highestAssetAmountDate: firstCandle.closeTime, lowestAssetAmount: firstCandle.low, lowestAssetAmountDate: firstCandle.closeTime, numberOfCandles: numberOfCandles, numberOfCandlesInvested: 0, sharpeRatio: 0 }; } function _filterAndSortCandles(runParams, tradingInterval, candles, supportCandles) { const allCandles = [...candles, ...supportCandles.filter((c) => c.interval !== tradingInterval)].filter((c) => c.closeTime >= runParams.startTime && c.closeTime <= runParams.endTime); const intervalOrder = (0, common_1.getIntervals)(); allCandles.sort((a, b) => { const byTime = a.closeTime - b.closeTime; if (byTime !== 0) return byTime; return intervalOrder.indexOf(a.interval) - intervalOrder.indexOf(b.interval); }); return allCandles; } async function _buy(runParams, tradingCandle, canBuySell, currentCandle, buyParams) { if (!tradingCandle) { throw new error_1.BacktestError('Cannot buy on a support candle', error_1.ErrorCode.ActionFailed); } if (!canBuySell) { logger.trace('Buy blocked until highest needed candles are met'); } else { buyParams = buyParams || {}; buyParams.price = buyParams.price || currentCandle.close; const buyParamsReal = { currentClose: currentCandle.close, percentFee: runParams.percentFee, percentSlippage: runParams.percentSlippage, date: currentCandle.closeTime, ...buyParams }; if (orders_1.orderBook.borrowedBaseAmount > 0 && orders_1.orderBook.baseAmount > 0) { if (buyParams.stopLoss && buyParams.stopLoss > 0) { throw new error_1.BacktestError('Cannot define a stop loss if in a long and a short', error_1.ErrorCode.ActionFailed); } if (buyParams.takeProfit && buyParams.takeProfit > 0) { throw new error_1.BacktestError('Cannot define a take profit if in a long and a short', error_1.ErrorCode.ActionFailed); } } if (buyParams.stopLoss && buyParams.stopLoss > 0) orders_1.orderBook.stopLoss = buyParams.stopLoss; if (buyParams.takeProfit && buyParams.takeProfit > 0) orders_1.orderBook.takeProfit = buyParams.takeProfit; const buyResults = await (0, orders_1.realBuy)(buyParamsReal); if (buyResults) { logger.trace(`Real buy performed`); } } } async function _sell(runParams, tradingCandle, canBuySell, currentCandle, sellParams) { if (!tradingCandle) { throw new error_1.BacktestError('Cannot sell on a support candle', error_1.ErrorCode.ActionFailed); } if (!canBuySell) { logger.trace('Sell blocked until highest needed candles are met'); } else { sellParams = sellParams || {}; sellParams.price = sellParams.price || currentCandle.close; const sellParamsReal = { currentClose: currentCandle.close, percentFee: runParams.percentFee, percentSlippage: runParams.percentSlippage, date: currentCandle.closeTime, ...sellParams }; const sellResults = await (0, orders_1.realSell)(sellParamsReal); if (sellResults) { logger.trace(`Real sell performed`); } } } async function _getCandles(allHistoricalData, canBuySell, currentCandle, type, start, end) { const allCurrentCandles = allHistoricalData[currentCandle.interval]; const isEndNull = end == null; const currentCandleIndex = allCurrentCandles.findIndex((c) => c.closeTime === currentCandle.closeTime); if (currentCandleIndex < 0) { canBuySell = false; throw new error_1.BacktestError('Impossible to found current candle', error_1.ErrorCode.ActionFailed); } const currentCandles = allCurrentCandles.slice(0, currentCandleIndex + 1); const currentCandleCount = currentCandles.length; const fromIndex = currentCandleCount - start; const toIndex = isEndNull ? fromIndex + 1 : currentCandleCount - end; if (currentCandleCount === 0 || fromIndex < 0 || toIndex < 0 || fromIndex >= currentCandleCount) { canBuySell = false; throw new error_1.BacktestError('Insufficient candles, choose another date or adjust the quantity', error_1.ErrorCode.ActionFailed); } const slicedCandles = currentCandles.slice(fromIndex, toIndex); const filteredCandles = type === 'candle' ? slicedCandles : slicedCandles.map((c) => c[type]); return isEndNull ? filteredCandles[0] : filteredCandles; } //# sourceMappingURL=run-strategy.js.map