clean up
This commit is contained in:
parent
7993148a95
commit
528be93804
38 changed files with 4617 additions and 1081 deletions
300
apps/strategy-service/src/cli/index.ts
Normal file
300
apps/strategy-service/src/cli/index.ts
Normal 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 };
|
||||
Loading…
Add table
Add a link
Reference in a new issue