From b4624c77de2ea615a65c04a39d657a38ff2a7c95 Mon Sep 17 00:00:00 2001 From: TheSiahxyz <164138827+TheSiahxyz@users.noreply.github.com> Date: Wed, 1 Apr 2026 17:04:38 +0900 Subject: feat(cli): implement backtest, strategy, portfolio, and data commands --- cli/src/trading_cli/commands/backtest.py | 96 +++++++++++++++++++++++++++++--- 1 file changed, 89 insertions(+), 7 deletions(-) (limited to 'cli/src/trading_cli/commands/backtest.py') diff --git a/cli/src/trading_cli/commands/backtest.py b/cli/src/trading_cli/commands/backtest.py index 0f0cdbe..b9e3c1b 100644 --- a/cli/src/trading_cli/commands/backtest.py +++ b/cli/src/trading_cli/commands/backtest.py @@ -1,5 +1,16 @@ +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(): @@ -10,15 +21,85 @@ def backtest(): @backtest.command() @click.option("--strategy", required=True, help="Strategy name to backtest") @click.option("--symbol", required=True, help="Trading symbol (e.g. BTCUSDT)") -@click.option("--from", "from_date", required=True, help="Start date (ISO format)") -@click.option("--to", "to_date", default=None, help="End date (ISO format, defaults to now)") +@click.option("--timeframe", default="1h", show_default=True, help="Candle timeframe") @click.option("--balance", default=10000.0, show_default=True, help="Initial balance in USDT") -def run(strategy, symbol, from_date, to_date, balance): +@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.""" - to_label = to_date or "now" - click.echo( - f"Running backtest: strategy={strategy}, symbol={symbol}, {from_date} → {to_label}, balance={balance}..." - ) + 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) + 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() @@ -26,3 +107,4 @@ def run(strategy, symbol, from_date, to_date, balance): def report(backtest_id): """Show a backtest report by ID.""" click.echo(f"Showing backtest report for ID: {backtest_id}...") + click.echo("(Not yet implemented - requires stored backtest results)") -- cgit v1.2.3