summaryrefslogtreecommitdiff
path: root/cli/src/trading_cli/commands/backtest.py
diff options
context:
space:
mode:
authorTheSiahxyz <164138827+TheSiahxyz@users.noreply.github.com>2026-04-01 17:04:38 +0900
committerTheSiahxyz <164138827+TheSiahxyz@users.noreply.github.com>2026-04-01 17:04:38 +0900
commitb4624c77de2ea615a65c04a39d657a38ff2a7c95 (patch)
tree918be60fb38d95267f85aa5fe71aa972dc886652 /cli/src/trading_cli/commands/backtest.py
parent32bc579c3f15694308992690f4457524e8842f99 (diff)
feat(cli): implement backtest, strategy, portfolio, and data commands
Diffstat (limited to 'cli/src/trading_cli/commands/backtest.py')
-rw-r--r--cli/src/trading_cli/commands/backtest.py96
1 files changed, 89 insertions, 7 deletions
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)")