stock-bot/apps/strategy-service/src/cli/index.ts
2025-06-11 11:11:47 -04:00

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 };