#!/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 { 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, 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 { 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 { 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 { 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 { 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 to test') .requiredOption('--symbol ', 'Symbol to trade') .requiredOption('--start-date ', 'Start date (YYYY-MM-DD)') .requiredOption('--end-date ', 'End date (YYYY-MM-DD)') .option('-m, --mode ', 'Execution mode', 'vectorized') .option('-c, --initial-capital ', 'Initial capital', '10000') .option('--config ', 'Configuration file path') .option('-o, --output ', '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 to validate') .action(async (options: CLIBacktestConfig) => { await validateStrategy(options.strategy); }); program .command('compare') .description('Compare multiple strategies') .requiredOption('--strategies ', 'Comma-separated list of strategies') .requiredOption('--symbol ', 'Symbol to trade') .requiredOption('--start-date ', 'Start date (YYYY-MM-DD)') .requiredOption('--end-date ', 'End date (YYYY-MM-DD)') .option('-m, --mode ', 'Execution mode', 'vectorized') .option('-c, --initial-capital ', 'Initial capital', '10000') .option('-o, --output ', '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 { runBacktest, listStrategies, validateStrategy };