UNPKG

quant-zero

Version:

Node-Quant is a powerful Node.js package for developing and testing quantitative trading strategies in cryptocurrency markets, offering tools for backtesting and performance analysis.

215 lines (180 loc) 5.71 kB
import fs from 'fs' import path from 'path' import { BacktestResults, DataSourceType, SimulationOptions, StrategyOptions, TimeFrame, TRADE_KEY, } from '@/types' import { OHLCV } from 'ccxt' import downloadPairData from '@/utils/dataInstaller' import { CandleSticks } from '@/charting' import { TradeManager } from '@/managers/TradeManager' import { calcAlpha, calcBeta, calcCovariance, calcPL, calcSharpeE, calcStddev, calcVariance, } from '@/utils/maths' import getAvgMarketReturn from '@/utils/getAvgMarketReturn' export class Strategy { public name: string; private data: OHLCV[] = [] public readonly strategyOptions: Required<StrategyOptions> public tradeManager: TradeManager public indicators: Map<string, any> = new Map() public pairDataPath: string = '' constructor(strategyOptions: StrategyOptions) { const { name, dataLength = 100, timeFrame = TimeFrame.MINUTE, chartType = new CandleSticks(), indicators = [], simulationOptions, pair, } = strategyOptions this.name = strategyOptions.name; this.strategyOptions = { name, dataLength, timeFrame, chartType, indicators, simulationOptions: simulationOptions ?? { pair }, pair, } this.tradeManager = new TradeManager(this) } public async loadData(): Promise<void> { const { pair, timeFrame, dataLength } = this.strategyOptions const dataFolderPath = path.join(process.cwd(), 'data') if (!fs.existsSync(dataFolderPath)) { fs.mkdirSync(dataFolderPath) } const { path: pairDataPath, data } = await downloadPairData( pair, timeFrame, dataLength, dataFolderPath, ) this.pairDataPath = pairDataPath this.data = data } private provideAllIndicators(): void { this.strategyOptions.indicators.forEach((indicator) => indicator.provide(this.data), ) } private feedAllIndicators(data: OHLCV): void { this.strategyOptions.indicators.forEach((indicator) => indicator.feed(data)) } public async backtest({}: SimulationOptions): Promise<BacktestResults> { const results: BacktestResults = { alpha: 0, beta: 0, maxDrawdown: 0, maxProfit: 0, percentageProfitable: 0, return: 0, profitFactor: 0, sharpeE: 0, tradeCount: 0, reportData: { data: [], trades: [], }, } await Promise.all([this.internalStart(), this.onStart(this.data)]) for (const update of this.data) { await Promise.all([ this.internalUpdate(update, this.data), this.onUpdate(update, this.data), ]) } const tradeHistory = this.tradeManager.getTradeHistory() results.tradeCount = tradeHistory.length results.reportData.trades = tradeHistory if (tradeHistory.length === 0) { return results } const returns = tradeHistory.map((trade) => { const { [TRADE_KEY.PL]: pl = 0, [TRADE_KEY.leverage]: leverage = 1 } = trade.getData() return pl * leverage }) const totalPL = returns.reduce((sum, r) => sum + r, 0) const profitableTrades = returns.filter((r) => r > 0) const lossTrades = returns.filter((r) => r < 0) results.return = totalPL results.percentageProfitable = (profitableTrades.length / returns.length) * 100 results.maxDrawdown = Math.min(...returns) results.maxProfit = Math.max(...returns) const grossProfit = profitableTrades.reduce((sum, r) => sum + r, 0) const grossLoss = Math.abs(lossTrades.reduce((sum, r) => sum + r, 0)) results.profitFactor = grossLoss !== 0 ? grossProfit / grossLoss : grossProfit const averageReturn = totalPL / returns.length const riskFreeRate = 0 const marketReturn = await getAvgMarketReturn( this.strategyOptions.pair, TimeFrame.MONTH, 60, // 5 years * 12 months ) const covariance = calcCovariance(returns, marketReturn) || 0 const variance = calcVariance(returns) || 0 const standardDeviation = calcStddev(returns) results.beta = calcBeta(covariance, variance) results.alpha = calcAlpha( averageReturn, riskFreeRate, results.beta, marketReturn, ) results.sharpeE = calcSharpeE( averageReturn, results.return, riskFreeRate, standardDeviation, ) results.reportData.data.push({ name: `${this.strategyOptions.pair.replaceAll('/', '_')}_${ this.strategyOptions.timeFrame }`, type: DataSourceType.PRICE, data: this.data, }) this.indicators.forEach((indicatorData, indicatorKey) => { results.reportData.data.push({ name: indicatorKey, type: DataSourceType.FACTOR, data: indicatorData, }) }) return results } private internalStart() { // this.provideAllIndicators(); this.strategyOptions.indicators.forEach((indicator) => { indicator.clear() }) } private async internalUpdate(update: OHLCV, updates: OHLCV[]): Promise<void> { this.feedAllIndicators(update) await Promise.all( this.strategyOptions.indicators.map(async (indicator) => { this.indicators.set(indicator.key, await indicator.generate()) }), ) this.tradeManager.onUpdate(update, updates) } protected live() {} protected onStart(updates: OHLCV[]): void {} protected onUpdate(update: OHLCV, updates: OHLCV[]): void {} }