This commit is contained in:
Bojan Kucera 2025-06-04 22:46:01 -04:00
parent 7993148a95
commit 528be93804
38 changed files with 4617 additions and 1081 deletions

View file

@ -0,0 +1,300 @@
#!/usr/bin/env bun
/**
* Strategy Service CLI
* Command-line interface for running backtests and managing strategies
*/
import { program } from 'commander';
import { createLogger } from '@stock-bot/logger';
import { createEventBus } from '@stock-bot/event-bus';
import { BacktestContext } from '../framework/execution-mode';
import LiveMode from '../backtesting/modes/live-mode';
import EventMode from '../backtesting/modes/event-mode';
import VectorizedMode from '../backtesting/modes/vectorized-mode';
import HybridMode from '../backtesting/modes/hybrid-mode';
const logger = createLogger('strategy-cli');
interface CLIBacktestConfig {
strategy: 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, 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 });
console.error('Backtest failed:', error.message);
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.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));
}
console.log(`\nResults saved to: ${outputPath}`);
} catch (error) {
logger.error('Failed to save results', { outputPath, error });
console.error(`Failed to save results: ${error.message}`);
}
}
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) => {
await runBacktest({
strategy: options.strategy,
symbol: options.symbol,
startDate: options.startDate,
endDate: options.endDate,
mode: options.mode,
initialCapital: parseFloat(options.initialCapital),
config: options.config,
output: options.output,
verbose: options.verbose
});
});
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) => {
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) => {
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({
strategy,
symbol: options.symbol,
startDate: options.startDate,
endDate: options.endDate,
mode: options.mode,
initialCapital: parseFloat(options.initialCapital),
output: options.output ? `${options.output}/${strategy}.json` : undefined
});
} catch (error) {
console.error(`Failed to run ${strategy}:`, error.message);
}
}
console.log('\nComparison completed!');
});
// Parse command line arguments
program.parse();
export { runBacktest, listStrategies, validateStrategy };