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
text/typescript
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 {}
}