@backtest/framework
Version:
Backtesting trading strategies in TypeScript / JavaScript
394 lines • 23.3 kB
JavaScript
;
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;
};
})();
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
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"));
function run(runParams) {
return __awaiter(this, void 0, void 0, function* () {
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 = yield 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 = yield (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 = yield (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) {
yield (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];
}
yield _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;
function buy(buyParams) {
return __awaiter(this, void 0, void 0, function* () {
return _buy(runParams, tradingCandle, canBuySell, currentCandle, buyParams);
});
}
function sell(buyParams) {
return __awaiter(this, void 0, void 0, function* () {
return _sell(runParams, tradingCandle, canBuySell, currentCandle, buyParams);
});
}
function getCandles(type, start, end) {
return __awaiter(this, void 0, void 0, function* () {
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) {
yield sell({ price: orders_1.orderBook.stopLoss });
}
else if (orders_1.orderBook.borrowedBaseAmount > 0 && high >= orders_1.orderBook.stopLoss) {
yield sell({ price: orders_1.orderBook.stopLoss });
}
}
if (orders_1.orderBook.takeProfit > 0) {
if (orders_1.orderBook.baseAmount > 0 && high >= orders_1.orderBook.takeProfit) {
yield sell({ price: orders_1.orderBook.takeProfit });
}
else if (orders_1.orderBook.borrowedBaseAmount > 0 && low <= orders_1.orderBook.takeProfit) {
yield sell({ price: orders_1.orderBook.takeProfit });
}
}
const worth = yield (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 {
yield 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) {
yield sell();
}
}
}
}
if ((strategy === null || strategy === void 0 ? void 0 : strategy.finishCallback) !== undefined) {
yield (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(Object.assign(Object.assign({}, 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;
});
}
function _resetOrders(runParams) {
return __awaiter(this, void 0, void 0, function* () {
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;
yield (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;
}
function _buy(runParams, tradingCandle, canBuySell, currentCandle, buyParams) {
return __awaiter(this, void 0, void 0, function* () {
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 = Object.assign({ 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 = yield (0, orders_1.realBuy)(buyParamsReal);
if (buyResults) {
logger.trace(`Real buy performed`);
}
}
});
}
function _sell(runParams, tradingCandle, canBuySell, currentCandle, sellParams) {
return __awaiter(this, void 0, void 0, function* () {
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 = Object.assign({ currentClose: currentCandle.close, percentFee: runParams.percentFee, percentSlippage: runParams.percentSlippage, date: currentCandle.closeTime }, sellParams);
const sellResults = yield (0, orders_1.realSell)(sellParamsReal);
if (sellResults) {
logger.trace(`Real sell performed`);
}
}
});
}
function _getCandles(allHistoricalData, canBuySell, currentCandle, type, start, end) {
return __awaiter(this, void 0, void 0, function* () {
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