import asyncio import sys from decimal import Decimal from pathlib import Path import click # Add service source paths so we can import strategy-engine and backtester _ROOT = Path(__file__).resolve().parents[5] sys.path.insert(0, str(_ROOT / "services" / "strategy-engine" / "src")) sys.path.insert(0, str(_ROOT / "services" / "strategy-engine")) sys.path.insert(0, str(_ROOT / "services" / "backtester" / "src")) @click.group() def backtest(): """Backtesting commands.""" pass @backtest.command() @click.option("--strategy", required=True, help="Strategy name to backtest") @click.option("--symbol", required=True, help="Trading symbol (e.g. AAPL)") @click.option("--timeframe", default="1h", show_default=True, help="Candle timeframe") @click.option("--balance", default=10000.0, show_default=True, help="Initial balance in USD") @click.option( "--output", "output_format", type=click.Choice(["text", "csv", "json"]), default="text", show_default=True, help="Output format", ) @click.option("--file", "file_path", default=None, help="Save output to file") def run(strategy, symbol, timeframe, balance, output_format, file_path): """Run a backtest for a strategy.""" try: from strategy_engine.plugin_loader import load_strategies from backtester.engine import BacktestEngine from backtester.reporter import format_report, export_csv, export_json from shared.db import Database from shared.config import Settings from shared.models import Candle except ImportError as e: click.echo(f"Error: Could not import required modules: {e}", err=True) sys.exit(1) strategies_dir = _ROOT / "services" / "strategy-engine" / "strategies" strategies = load_strategies(strategies_dir) matched = [s for s in strategies if s.name == strategy] if not matched: available = [s.name for s in strategies] click.echo(f"Error: Strategy '{strategy}' not found. Available: {available}", err=True) sys.exit(1) strat = matched[0] async def _run(): settings = Settings() db = Database(settings.database_url.get_secret_value()) await db.connect() try: candle_rows = await db.get_candles(symbol, timeframe, limit=500) if not candle_rows: click.echo(f"Error: No candles found for {symbol} {timeframe}", err=True) sys.exit(1) candles = [] for row in reversed(candle_rows): # get_candles returns DESC, we need ASC candles.append( Candle( symbol=row["symbol"], timeframe=row["timeframe"], open_time=row["open_time"], open=row["open"], high=row["high"], low=row["low"], close=row["close"], volume=row["volume"], ) ) engine = BacktestEngine(strat, Decimal(str(balance))) result = engine.run(candles) if output_format == "csv": output = export_csv(result) elif output_format == "json": output = export_json(result) else: output = format_report(result) if file_path: Path(file_path).write_text(output) click.echo(f"Report saved to {file_path}") else: click.echo(output) finally: await db.close() asyncio.run(_run()) @backtest.command("walk-forward") @click.option("--strategy", required=True, help="Strategy name") @click.option("--symbol", required=True, help="Trading symbol") @click.option("--timeframe", default="1h", show_default=True, help="Candle timeframe") @click.option("--balance", default=10000.0, show_default=True, help="Initial balance") @click.option("--windows", default=5, show_default=True, help="Number of walk-forward windows") def walk_forward(strategy, symbol, timeframe, balance, windows): """Run walk-forward analysis to detect overfitting.""" try: from strategy_engine.plugin_loader import load_strategies from backtester.walk_forward import WalkForwardEngine from shared.db import Database from shared.config import Settings from shared.models import Candle except ImportError as e: click.echo(f"Error: Could not import required modules: {e}", err=True) sys.exit(1) strategies_dir = _ROOT / "services" / "strategy-engine" / "strategies" strategies = load_strategies(strategies_dir) matched = [s for s in strategies if s.name == strategy] if not matched: click.echo(f"Error: Strategy '{strategy}' not found.", err=True) sys.exit(1) strategy_cls = type(matched[0]) async def _run(): settings = Settings() db = Database(settings.database_url.get_secret_value()) await db.connect() try: rows = await db.get_candles(symbol, timeframe, limit=2000) if not rows: click.echo(f"Error: No candles found for {symbol} {timeframe}", err=True) sys.exit(1) candles = [ Candle( symbol=r["symbol"], timeframe=r["timeframe"], open_time=r["open_time"], open=r["open"], high=r["high"], low=r["low"], close=r["close"], volume=r["volume"], ) for r in reversed(rows) ] param_grid = [{}] # Default params — extend as needed engine = WalkForwardEngine( strategy_factory=strategy_cls, param_grid=param_grid, initial_balance=Decimal(str(balance)), num_windows=windows, ) result = engine.run(candles) click.echo(f"Walk-Forward Analysis: {result.strategy_name}") click.echo(f"Windows: {result.num_windows}") click.echo(f"In-sample profit: {result.in_sample_profit_pct:.2f}%") click.echo(f"Out-of-sample profit: {result.out_of_sample_profit_pct:.2f}%") click.echo(f"Efficiency ratio: {result.efficiency_ratio:.2f}") if result.efficiency_ratio > 0.5: click.echo("-> Strategy appears robust (ratio > 0.5)") else: click.echo("-> WARNING: Possible overfitting (ratio < 0.5)") finally: await db.close() asyncio.run(_run())