285 lines
9.5 KiB
TypeScript
285 lines
9.5 KiB
TypeScript
#!/usr/bin/env bun
|
|
/**
|
|
* Strategy Service CLI
|
|
* Command-line interface for running backtests and managing strategies
|
|
*/
|
|
import { program } from 'commander';
|
|
import { createEventBus } from '@stock-bot/event-bus';
|
|
import { getLogger } from '@stock-bot/logger';
|
|
import { EventMode } from '../backtesting/modes/event-mode';
|
|
import HybridMode from '../backtesting/modes/hybrid-mode';
|
|
import { LiveMode } from '../backtesting/modes/live-mode';
|
|
import VectorizedMode from '../backtesting/modes/vectorized-mode';
|
|
import { BacktestContext } from '../framework/execution-mode';
|
|
|
|
const logger = getLogger('strategy-cli');
|
|
|
|
interface CLIBacktestConfig {
|
|
strategy: string;
|
|
strategies: string;
|
|
symbol: string;
|
|
startDate: string;
|
|
endDate: string;
|
|
mode: 'live' | 'event' | 'vectorized' | 'hybrid';
|
|
initialCapital?: number;
|
|
config?: string;
|
|
output?: string;
|
|
verbose?: boolean;
|
|
}
|
|
|
|
async function runBacktest(options: CLIBacktestConfig): Promise<void> {
|
|
logger.info('Starting backtest from CLI', { options });
|
|
|
|
try {
|
|
// Initialize event bus
|
|
const eventBus = createEventBus({
|
|
serviceName: 'strategy-cli',
|
|
enablePersistence: false, // Disable Redis for CLI
|
|
});
|
|
|
|
// Create backtest context
|
|
const context: BacktestContext = {
|
|
backtestId: `cli_${Date.now()}`,
|
|
strategy: {
|
|
id: options.strategy,
|
|
name: options.strategy,
|
|
type: options.strategy,
|
|
code: options.strategy,
|
|
parameters: {},
|
|
},
|
|
symbol: options.symbol,
|
|
startDate: options.startDate,
|
|
endDate: options.endDate,
|
|
initialCapital: options.initialCapital || 10000,
|
|
mode: options.mode,
|
|
};
|
|
|
|
// Load additional config if provided
|
|
if (options.config) {
|
|
const configData = await loadConfig(options.config);
|
|
context.strategy.parameters = { ...context.strategy.parameters, ...configData };
|
|
}
|
|
|
|
// Create and execute the appropriate mode
|
|
let executionMode;
|
|
|
|
switch (options.mode) {
|
|
case 'live':
|
|
executionMode = new LiveMode(context, eventBus);
|
|
break;
|
|
case 'event':
|
|
executionMode = new EventMode(context, eventBus);
|
|
break;
|
|
case 'vectorized':
|
|
executionMode = new VectorizedMode(context, eventBus);
|
|
break;
|
|
case 'hybrid':
|
|
executionMode = new HybridMode(context, eventBus);
|
|
break;
|
|
default:
|
|
throw new Error(`Unknown execution mode: ${options.mode}`);
|
|
}
|
|
|
|
// Subscribe to progress updates
|
|
eventBus.subscribe('backtest.update', message => {
|
|
const { backtestId: _backtestId, progress, ...data } = message.data;
|
|
console.log(`Progress: ${progress}%`, data);
|
|
});
|
|
|
|
await executionMode.initialize();
|
|
const result = await executionMode.execute();
|
|
await executionMode.cleanup();
|
|
|
|
// Display results
|
|
displayResults(result);
|
|
|
|
// Save results if output specified
|
|
if (options.output) {
|
|
await saveResults(result, options.output);
|
|
}
|
|
|
|
await eventBus.close();
|
|
} catch (error) {
|
|
logger.error('Backtest failed', error);
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
async function loadConfig(configPath: string): Promise<any> {
|
|
try {
|
|
if (configPath.endsWith('.json')) {
|
|
const file = Bun.file(configPath);
|
|
return await file.json();
|
|
} else {
|
|
// Assume it's a JavaScript/TypeScript module
|
|
return await import(configPath);
|
|
}
|
|
} catch (error) {
|
|
logger.error('Failed to load config', { configPath, error });
|
|
throw new Error(`Failed to load config from ${configPath}: ${(error as Error).message}`);
|
|
}
|
|
}
|
|
|
|
function displayResults(result: any): void {
|
|
console.log('\n=== Backtest Results ===');
|
|
console.log(`Strategy: ${result.strategy.name}`);
|
|
console.log(`Symbol: ${result.symbol}`);
|
|
console.log(`Period: ${result.startDate} to ${result.endDate}`);
|
|
console.log(`Mode: ${result.mode}`);
|
|
console.log(`Duration: ${result.duration}ms`);
|
|
|
|
console.log('\n--- Performance ---');
|
|
console.log(`Total Return: ${(result.performance.totalReturn * 100).toFixed(2)}%`);
|
|
console.log(`Sharpe Ratio: ${result.performance.sharpeRatio.toFixed(3)}`);
|
|
console.log(`Max Drawdown: ${(result.performance.maxDrawdown * 100).toFixed(2)}%`);
|
|
console.log(`Win Rate: ${(result.performance.winRate * 100).toFixed(1)}%`);
|
|
console.log(`Profit Factor: ${result.performance.profitFactor.toFixed(2)}`);
|
|
|
|
console.log('\n--- Trading Stats ---');
|
|
console.log(`Total Trades: ${result.performance.totalTrades}`);
|
|
console.log(`Winning Trades: ${result.performance.winningTrades}`);
|
|
console.log(`Losing Trades: ${result.performance.losingTrades}`);
|
|
console.log(`Average Trade: ${result.performance.avgTrade.toFixed(2)}`);
|
|
console.log(`Average Win: ${result.performance.avgWin.toFixed(2)}`);
|
|
console.log(`Average Loss: ${result.performance.avgLoss.toFixed(2)}`);
|
|
console.log(`Largest Win: ${result.performance.largestWin.toFixed(2)}`);
|
|
console.log(`Largest Loss: ${result.performance.largestLoss.toFixed(2)}`);
|
|
|
|
if (result.metadata) {
|
|
console.log('\n--- Metadata ---');
|
|
Object.entries(result.metadata).forEach(([key, value]) => {
|
|
console.log(`${key}: ${Array.isArray(value) ? value.join(', ') : value}`);
|
|
});
|
|
}
|
|
}
|
|
|
|
async function saveResults(result: any, outputPath: string): Promise<void> {
|
|
try {
|
|
if (outputPath.endsWith('.json')) {
|
|
await Bun.write(outputPath, JSON.stringify(result, null, 2));
|
|
} else if (outputPath.endsWith('.csv')) {
|
|
const csv = convertTradesToCSV(result.trades);
|
|
await Bun.write(outputPath, csv);
|
|
} else {
|
|
// Default to JSON
|
|
await Bun.write(outputPath + '.json', JSON.stringify(result, null, 2));
|
|
}
|
|
|
|
logger.info(`\nResults saved to: ${outputPath}`);
|
|
} catch (error) {
|
|
logger.error('Failed to save results', { outputPath, error });
|
|
}
|
|
}
|
|
|
|
function convertTradesToCSV(trades: any[]): string {
|
|
if (trades.length === 0) {
|
|
return 'No trades executed\n';
|
|
}
|
|
|
|
const headers = Object.keys(trades[0]).join(',');
|
|
const rows = trades.map(trade =>
|
|
Object.values(trade)
|
|
.map(value => (typeof value === 'string' ? `"${value}"` : value))
|
|
.join(',')
|
|
);
|
|
|
|
return [headers, ...rows].join('\n');
|
|
}
|
|
|
|
async function listStrategies(): Promise<void> {
|
|
console.log('Available strategies:');
|
|
console.log(' sma_crossover - Simple Moving Average Crossover');
|
|
console.log(' ema_crossover - Exponential Moving Average Crossover');
|
|
console.log(' rsi_mean_reversion - RSI Mean Reversion');
|
|
console.log(' macd_trend - MACD Trend Following');
|
|
console.log(' bollinger_bands - Bollinger Bands Strategy');
|
|
// Add more as they're implemented
|
|
}
|
|
|
|
async function validateStrategy(strategy: string): Promise<void> {
|
|
console.log(`Validating strategy: ${strategy}`);
|
|
|
|
// TODO: Add strategy validation logic
|
|
// This could check if the strategy exists, has valid parameters, etc.
|
|
|
|
const validStrategies = [
|
|
'sma_crossover',
|
|
'ema_crossover',
|
|
'rsi_mean_reversion',
|
|
'macd_trend',
|
|
'bollinger_bands',
|
|
];
|
|
|
|
if (!validStrategies.includes(strategy)) {
|
|
console.warn(`Warning: Strategy '${strategy}' is not in the list of known strategies`);
|
|
console.log('Use --list-strategies to see available strategies');
|
|
} else {
|
|
console.log(`✓ Strategy '${strategy}' is valid`);
|
|
}
|
|
}
|
|
|
|
// CLI Commands
|
|
program.name('strategy-cli').description('Stock Trading Bot Strategy CLI').version('1.0.0');
|
|
|
|
program
|
|
.command('backtest')
|
|
.description('Run a backtest')
|
|
.requiredOption('-s, --strategy <strategy>', 'Strategy to test')
|
|
.requiredOption('--symbol <symbol>', 'Symbol to trade')
|
|
.requiredOption('--start-date <date>', 'Start date (YYYY-MM-DD)')
|
|
.requiredOption('--end-date <date>', 'End date (YYYY-MM-DD)')
|
|
.option('-m, --mode <mode>', 'Execution mode', 'vectorized')
|
|
.option('-c, --initial-capital <amount>', 'Initial capital', '10000')
|
|
.option('--config <path>', 'Configuration file path')
|
|
.option('-o, --output <path>', 'Output file path')
|
|
.option('-v, --verbose', 'Verbose output')
|
|
.action(async (options: CLIBacktestConfig) => {
|
|
await runBacktest(options);
|
|
});
|
|
|
|
program.command('list-strategies').description('List available strategies').action(listStrategies);
|
|
|
|
program
|
|
.command('validate')
|
|
.description('Validate a strategy')
|
|
.requiredOption('-s, --strategy <strategy>', 'Strategy to validate')
|
|
.action(async (options: CLIBacktestConfig) => {
|
|
await validateStrategy(options.strategy);
|
|
});
|
|
|
|
program
|
|
.command('compare')
|
|
.description('Compare multiple strategies')
|
|
.requiredOption('--strategies <strategies>', 'Comma-separated list of strategies')
|
|
.requiredOption('--symbol <symbol>', 'Symbol to trade')
|
|
.requiredOption('--start-date <date>', 'Start date (YYYY-MM-DD)')
|
|
.requiredOption('--end-date <date>', 'End date (YYYY-MM-DD)')
|
|
.option('-m, --mode <mode>', 'Execution mode', 'vectorized')
|
|
.option('-c, --initial-capital <amount>', 'Initial capital', '10000')
|
|
.option('-o, --output <path>', 'Output directory')
|
|
.action(async (options: CLIBacktestConfig) => {
|
|
const strategies = options.strategies.split(',').map((s: string) => s.trim());
|
|
console.log(`Comparing strategies: ${strategies.join(', ')}`);
|
|
|
|
const _results: any[] = [];
|
|
|
|
for (const strategy of strategies) {
|
|
console.log(`\nRunning ${strategy}...`);
|
|
try {
|
|
await runBacktest({
|
|
...options,
|
|
strategy,
|
|
output: options.output ? `${options.output}/${strategy}.json` : undefined,
|
|
});
|
|
} catch (error) {
|
|
console.error(`Failed to run ${strategy}:`, (error as Error).message);
|
|
}
|
|
}
|
|
|
|
console.log('\nComparison completed!');
|
|
});
|
|
|
|
// Parse command line arguments
|
|
program.parse();
|
|
|
|
export { listStrategies, runBacktest, validateStrategy };
|