395 lines
11 KiB
TypeScript
395 lines
11 KiB
TypeScript
import { DataFrame } from '@stock-bot/data-frame';
|
|
import { getLogger } from '@stock-bot/logger';
|
|
import { atr, bollingerBands, ema, macd, rsi, sma } from '@stock-bot/utils';
|
|
|
|
// Vector operations interface
|
|
export interface VectorOperation {
|
|
name: string;
|
|
inputs: string[];
|
|
output: string;
|
|
operation: (inputs: number[][]) => number[];
|
|
}
|
|
|
|
// Vectorized strategy context
|
|
export interface VectorizedContext {
|
|
data: DataFrame;
|
|
lookback: number;
|
|
indicators: Record<string, number[]>;
|
|
signals: Record<string, number[]>;
|
|
}
|
|
|
|
// Performance metrics for vectorized backtesting
|
|
export interface VectorizedMetrics {
|
|
totalReturns: number;
|
|
sharpeRatio: number;
|
|
maxDrawdown: number;
|
|
winRate: number;
|
|
profitFactor: number;
|
|
totalTrades: number;
|
|
avgTrade: number;
|
|
returns: number[];
|
|
drawdown: number[];
|
|
equity: number[];
|
|
}
|
|
|
|
// Vectorized backtest result
|
|
export interface VectorizedBacktestResult {
|
|
metrics: VectorizedMetrics;
|
|
trades: VectorizedTrade[];
|
|
equity: number[];
|
|
timestamps: number[];
|
|
signals: Record<string, number[]>;
|
|
}
|
|
|
|
export interface VectorizedTrade {
|
|
entryIndex: number;
|
|
exitIndex: number;
|
|
entryPrice: number;
|
|
exitPrice: number;
|
|
quantity: number;
|
|
side: 'LONG' | 'SHORT';
|
|
pnl: number;
|
|
return: number;
|
|
duration: number;
|
|
}
|
|
|
|
// Vectorized strategy engine
|
|
export class VectorEngine {
|
|
private logger = getLogger('vector-engine');
|
|
private operations: Map<string, VectorOperation> = new Map();
|
|
|
|
constructor() {
|
|
this.registerDefaultOperations();
|
|
}
|
|
|
|
private registerDefaultOperations(): void {
|
|
// Register common mathematical operations
|
|
this.registerOperation({
|
|
name: 'add',
|
|
inputs: ['a', 'b'],
|
|
output: 'result',
|
|
operation: ([a, b]) => a.map((val, i) => val + b[i]),
|
|
});
|
|
|
|
this.registerOperation({
|
|
name: 'subtract',
|
|
inputs: ['a', 'b'],
|
|
output: 'result',
|
|
operation: ([a, b]) => a.map((val, i) => val - b[i]),
|
|
});
|
|
|
|
this.registerOperation({
|
|
name: 'multiply',
|
|
inputs: ['a', 'b'],
|
|
output: 'result',
|
|
operation: ([a, b]) => a.map((val, i) => val * b[i]),
|
|
});
|
|
|
|
this.registerOperation({
|
|
name: 'divide',
|
|
inputs: ['a', 'b'],
|
|
output: 'result',
|
|
operation: ([a, b]) => a.map((val, i) => (b[i] !== 0 ? val / b[i] : NaN)),
|
|
});
|
|
|
|
// Register comparison operations
|
|
this.registerOperation({
|
|
name: 'greater_than',
|
|
inputs: ['a', 'b'],
|
|
output: 'result',
|
|
operation: ([a, b]) => a.map((val, i) => (val > b[i] ? 1 : 0)),
|
|
});
|
|
|
|
this.registerOperation({
|
|
name: 'less_than',
|
|
inputs: ['a', 'b'],
|
|
output: 'result',
|
|
operation: ([a, b]) => a.map((val, i) => (val < b[i] ? 1 : 0)),
|
|
});
|
|
|
|
this.registerOperation({
|
|
name: 'crossover',
|
|
inputs: ['a', 'b'],
|
|
output: 'result',
|
|
operation: ([a, b]) => {
|
|
const result = new Array(a.length).fill(0);
|
|
for (let i = 1; i < a.length; i++) {
|
|
if (a[i] > b[i] && a[i - 1] <= b[i - 1]) {
|
|
result[i] = 1;
|
|
}
|
|
}
|
|
return result;
|
|
},
|
|
});
|
|
|
|
this.registerOperation({
|
|
name: 'crossunder',
|
|
inputs: ['a', 'b'],
|
|
output: 'result',
|
|
operation: ([a, b]) => {
|
|
const result = new Array(a.length).fill(0);
|
|
for (let i = 1; i < a.length; i++) {
|
|
if (a[i] < b[i] && a[i - 1] >= b[i - 1]) {
|
|
result[i] = 1;
|
|
}
|
|
}
|
|
return result;
|
|
},
|
|
});
|
|
}
|
|
|
|
registerOperation(operation: VectorOperation): void {
|
|
this.operations.set(operation.name, operation);
|
|
this.logger.debug(`Registered operation: ${operation.name}`);
|
|
}
|
|
|
|
// Execute vectorized strategy
|
|
async executeVectorizedStrategy(
|
|
data: DataFrame,
|
|
strategyCode: string
|
|
): Promise<VectorizedBacktestResult> {
|
|
try {
|
|
const context = this.prepareContext(data);
|
|
const signals = this.executeStrategy(context, strategyCode);
|
|
const trades = this.generateTrades(data, signals);
|
|
const metrics = this.calculateMetrics(data, trades);
|
|
|
|
return {
|
|
metrics,
|
|
trades,
|
|
equity: metrics.equity,
|
|
timestamps: data.getColumn('timestamp'),
|
|
signals,
|
|
};
|
|
} catch (error) {
|
|
this.logger.error('Vectorized strategy execution failed', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
private prepareContext(data: DataFrame): VectorizedContext {
|
|
const close = data.getColumn('close');
|
|
const high = data.getColumn('high');
|
|
const low = data.getColumn('low');
|
|
const volume = data.getColumn('volume');
|
|
|
|
// Calculate common indicators
|
|
const indicators: Record<string, number[]> = {
|
|
sma_20: sma(close, 20),
|
|
sma_50: sma(close, 50),
|
|
ema_12: ema(close, 12),
|
|
ema_26: ema(close, 26),
|
|
rsi: rsi(close),
|
|
};
|
|
|
|
const m = macd(close);
|
|
indicators.macd = m.macd;
|
|
indicators.macd_signal = m.signal;
|
|
indicators.macd_histogram = m.histogram;
|
|
|
|
const bb = bollingerBands(close);
|
|
indicators.bb_upper = bb.upper;
|
|
indicators.bb_middle = bb.middle;
|
|
indicators.bb_lower = bb.lower;
|
|
|
|
return {
|
|
data,
|
|
lookback: 100,
|
|
indicators,
|
|
signals: {},
|
|
};
|
|
}
|
|
|
|
private executeStrategy(
|
|
context: VectorizedContext,
|
|
strategyCode: string
|
|
): Record<string, number[]> {
|
|
// This is a simplified strategy execution
|
|
// In production, you'd want a more sophisticated strategy compiler/interpreter
|
|
const signals: Record<string, number[]> = {
|
|
buy: new Array(context.data.length).fill(0),
|
|
sell: new Array(context.data.length).fill(0),
|
|
};
|
|
|
|
// Example: Simple moving average crossover strategy
|
|
if (strategyCode.includes('sma_crossover')) {
|
|
const sma20 = context.indicators.sma_20;
|
|
const sma50 = context.indicators.sma_50;
|
|
|
|
for (let i = 1; i < sma20.length; i++) {
|
|
// Buy signal: SMA20 crosses above SMA50
|
|
if (!isNaN(sma20[i]) && !isNaN(sma50[i]) && !isNaN(sma20[i - 1]) && !isNaN(sma50[i - 1])) {
|
|
if (sma20[i] > sma50[i] && sma20[i - 1] <= sma50[i - 1]) {
|
|
signals.buy[i] = 1;
|
|
}
|
|
// Sell signal: SMA20 crosses below SMA50
|
|
else if (sma20[i] < sma50[i] && sma20[i - 1] >= sma50[i - 1]) {
|
|
signals.sell[i] = 1;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return signals;
|
|
}
|
|
|
|
private generateTrades(data: DataFrame, signals: Record<string, number[]>): VectorizedTrade[] {
|
|
const trades: VectorizedTrade[] = [];
|
|
const close = data.getColumn('close');
|
|
const timestamps = data.getColumn('timestamp');
|
|
|
|
let position: { index: number; price: number; side: 'LONG' | 'SHORT' } | null = null;
|
|
|
|
for (let i = 0; i < close.length; i++) {
|
|
if (signals.buy[i] === 1 && !position) {
|
|
// Open long position
|
|
position = {
|
|
index: i,
|
|
price: close[i],
|
|
side: 'LONG',
|
|
};
|
|
} else if (signals.sell[i] === 1) {
|
|
if (position && position.side === 'LONG') {
|
|
// Close long position
|
|
const trade: VectorizedTrade = {
|
|
entryIndex: position.index,
|
|
exitIndex: i,
|
|
entryPrice: position.price,
|
|
exitPrice: close[i],
|
|
quantity: 1, // Simplified: always trade 1 unit
|
|
side: 'LONG',
|
|
pnl: close[i] - position.price,
|
|
return: (close[i] - position.price) / position.price,
|
|
duration: timestamps[i] - timestamps[position.index],
|
|
};
|
|
trades.push(trade);
|
|
position = null;
|
|
} else if (!position) {
|
|
// Open short position
|
|
position = {
|
|
index: i,
|
|
price: close[i],
|
|
side: 'SHORT',
|
|
};
|
|
}
|
|
} else if (signals.buy[i] === 1 && position && position.side === 'SHORT') {
|
|
// Close short position
|
|
const trade: VectorizedTrade = {
|
|
entryIndex: position.index,
|
|
exitIndex: i,
|
|
entryPrice: position.price,
|
|
exitPrice: close[i],
|
|
quantity: 1,
|
|
side: 'SHORT',
|
|
pnl: position.price - close[i],
|
|
return: (position.price - close[i]) / position.price,
|
|
duration: timestamps[i] - timestamps[position.index],
|
|
};
|
|
trades.push(trade);
|
|
position = null;
|
|
}
|
|
}
|
|
|
|
return trades;
|
|
}
|
|
|
|
private calculateMetrics(data: DataFrame, trades: VectorizedTrade[]): VectorizedMetrics {
|
|
if (trades.length === 0) {
|
|
return {
|
|
totalReturns: 0,
|
|
sharpeRatio: 0,
|
|
maxDrawdown: 0,
|
|
winRate: 0,
|
|
profitFactor: 0,
|
|
totalTrades: 0,
|
|
avgTrade: 0,
|
|
returns: [],
|
|
drawdown: [],
|
|
equity: [],
|
|
};
|
|
}
|
|
|
|
const returns = trades.map(t => t.return);
|
|
const pnls = trades.map(t => t.pnl);
|
|
|
|
// Calculate equity curve
|
|
const equity: number[] = [10000]; // Starting capital
|
|
let currentEquity = 10000;
|
|
|
|
for (const trade of trades) {
|
|
currentEquity += trade.pnl;
|
|
equity.push(currentEquity);
|
|
}
|
|
|
|
// Calculate drawdown
|
|
const drawdown: number[] = [];
|
|
let peak = equity[0];
|
|
|
|
for (const eq of equity) {
|
|
if (eq > peak) {peak = eq;}
|
|
drawdown.push((peak - eq) / peak);
|
|
}
|
|
|
|
const totalReturns = (equity[equity.length - 1] - equity[0]) / equity[0];
|
|
const avgReturn = returns.reduce((sum, r) => sum + r, 0) / returns.length;
|
|
const returnStd = Math.sqrt(
|
|
returns.reduce((sum, r) => sum + Math.pow(r - avgReturn, 2), 0) / returns.length
|
|
);
|
|
|
|
const winningTrades = trades.filter(t => t.pnl > 0);
|
|
const losingTrades = trades.filter(t => t.pnl < 0);
|
|
|
|
const grossProfit = winningTrades.reduce((sum, t) => sum + t.pnl, 0);
|
|
const grossLoss = Math.abs(losingTrades.reduce((sum, t) => sum + t.pnl, 0));
|
|
|
|
return {
|
|
totalReturns,
|
|
sharpeRatio: returnStd !== 0 ? (avgReturn / returnStd) * Math.sqrt(252) : 0,
|
|
maxDrawdown: Math.max(...drawdown),
|
|
winRate: winningTrades.length / trades.length,
|
|
profitFactor: grossLoss !== 0 ? grossProfit / grossLoss : Infinity,
|
|
totalTrades: trades.length,
|
|
avgTrade: pnls.reduce((sum, pnl) => sum + pnl, 0) / trades.length,
|
|
returns,
|
|
drawdown,
|
|
equity,
|
|
};
|
|
}
|
|
|
|
// Utility methods for vectorized operations
|
|
applyOperation(operationName: string, inputs: Record<string, number[]>): number[] {
|
|
const operation = this.operations.get(operationName);
|
|
if (!operation) {
|
|
throw new Error(`Operation '${operationName}' not found`);
|
|
}
|
|
|
|
const inputArrays = operation.inputs.map(inputName => {
|
|
if (!inputs[inputName]) {
|
|
throw new Error(`Input '${inputName}' not provided for operation '${operationName}'`);
|
|
}
|
|
return inputs[inputName];
|
|
});
|
|
|
|
return operation.operation(inputArrays);
|
|
}
|
|
|
|
// Batch processing for multiple strategies
|
|
async batchBacktest(
|
|
data: DataFrame,
|
|
strategies: Array<{ id: string; code: string }>
|
|
): Promise<Record<string, VectorizedBacktestResult>> {
|
|
const results: Record<string, VectorizedBacktestResult> = {};
|
|
|
|
for (const strategy of strategies) {
|
|
try {
|
|
this.logger.info(`Running vectorized backtest for strategy: ${strategy.id}`);
|
|
results[strategy.id] = await this.executeVectorizedStrategy(data, strategy.code);
|
|
} catch (error) {
|
|
this.logger.error(`Backtest failed for strategy: ${strategy.id}`, error);
|
|
// Continue with other strategies
|
|
}
|
|
}
|
|
|
|
return results;
|
|
}
|
|
}
|