From 0134476632eb35abeae0489383ad98af10d82f86 Mon Sep 17 00:00:00 2001 From: I Luk Kim Date: Tue, 24 Mar 2026 10:56:51 -0700 Subject: [PATCH] =?UTF-8?q?Fix=20async/sync=20conflict=20in=20paper=20back?= =?UTF-8?q?test=20=E2=80=94=20nested=20event=20loop=20error?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit run_backtest changed from async to sync function. Pipeline refresh (async) runs via asyncio.run() before the sync BacktestRunner, avoiding nested event loop when SnapshotStore.load() calls asyncio.run(). CLI updated to call run_backtest() directly (no asyncio.run wrapper). Tested: `fithia2 paper backtest --config v6new.24 --start 2025-03-23 --end 2026-03-23` works. Co-Authored-By: Claude Opus 4.6 (1M context) --- apps/paper_trader/backtest_sim.py | 13 +- apps/paper_trader/cli.py | 588 ++++++++++++++++++++++++++++++ 2 files changed, 597 insertions(+), 4 deletions(-) create mode 100644 apps/paper_trader/cli.py diff --git a/apps/paper_trader/backtest_sim.py b/apps/paper_trader/backtest_sim.py index ad141a5..14ec09f 100644 --- a/apps/paper_trader/backtest_sim.py +++ b/apps/paper_trader/backtest_sim.py @@ -301,7 +301,7 @@ async def _refresh_snapshot( raise -async def run_backtest( +def run_backtest( configs: list[str], capital: float, start_date: dt.date, @@ -314,7 +314,11 @@ async def run_backtest( Automatically refreshes the Parquet snapshot if it doesn't cover the requested end_date (runs pipeline + re-export). + + This is a SYNC function — runs async pipeline steps via asyncio.run() + before the sync BacktestRunner, avoiding nested event loop issues. """ + import asyncio from libs.common.time_utils import is_trading_day from libs.common.logging import configure_logging @@ -331,7 +335,7 @@ async def run_backtest( console.print(f"[bold]Trading days:[/] {trading_days[0]} → {trading_days[-1]} ({len(trading_days)} days)") console.print("[bold]Engine:[/] BacktestRunner (identical to research backtester)") - # Check if snapshots need refresh for each config + # Check if snapshots need refresh (async pipeline, run before sync backtest) for config_path in configs: from apps.backtester.run import load_manifest, resolve_config manifest = load_manifest(config_path) @@ -339,7 +343,6 @@ async def run_backtest( snapshot_id = config.dataset_snapshot_id if _snapshot_needs_refresh(snapshot_id, end_date): - # Determine universe_profile from snapshot export config universe_profile = None if "midlarge" in snapshot_id: universe_profile = "midlarge-liquid-long-v1" @@ -348,7 +351,9 @@ async def run_backtest( elif "smallcap" in snapshot_id: universe_profile = "smallcap-liquid-long-v1" - await _refresh_snapshot(snapshot_id, universe_profile, console=console) + if console: + console.print(f"\n[bold yellow]Snapshot '{snapshot_id}' is stale — refreshing...[/]") + asyncio.run(_refresh_snapshot(snapshot_id, universe_profile, console=console)) configure_logging("WARNING") diff --git a/apps/paper_trader/cli.py b/apps/paper_trader/cli.py new file mode 100644 index 0000000..2dfed8f --- /dev/null +++ b/apps/paper_trader/cli.py @@ -0,0 +1,588 @@ +"""CLI for paper trading with Alpaca API. + +Usage: + fithia2 paper [options] + +Commands: + auto 자동 데몬 — 매일 파이프라인 + 매매를 스케줄에 맞게 자동 실행 + start Create a new paper trading session + run Run daily processing for a session + run-close 장 마감 직전: same-day 이벤트 → MOC 매수 + run-open 장 시작 직후: 전날 exit + after-close 이벤트 → 시장가 매수 + run-all Run daily processing for all active sessions + monitor 장중 실시간 stop/target 모니터링 + status Show session status + positions Show current positions (live from Alpaca) + trades Show trade history + equity Show equity curve + sessions List all sessions + pause Pause a session + resume Resume a paused session + close Close session (liquidates all positions) +""" +from __future__ import annotations + +import argparse +import asyncio +import os +import sys +from pathlib import Path + +from rich.console import Console + +_console = Console(width=140) + +# Default path for the paper trading SQLite DB +_DEFAULT_DB = os.environ.get("PAPER_TRADER_DB", "paper_trading.db") + + +def _get_state_manager(db_path: str): + from apps.paper_trader.state import StateManager + return StateManager(db_path) + + +def _get_broker(): + from apps.paper_trader.alpaca_broker import AlpacaBroker + try: + return AlpacaBroker.from_env() + except (ValueError, ImportError) as exc: + _console.print(f"[red]ERROR: {exc}[/]") + sys.exit(1) + + +def _resolve_session(state, name_or_id: str): + session = state.get_session(name_or_id) + if session is None: + _console.print(f"[red]ERROR: Session not found: '{name_or_id}'[/]") + sys.exit(1) + return session + + +# ------------------------------------------------------------------ # +# Commands +# ------------------------------------------------------------------ # + +def cmd_start(args: argparse.Namespace) -> None: + """Create a new paper trading session.""" + config_path = args.config + if not Path(config_path).exists(): + _console.print(f"[red]ERROR: Config not found: {config_path}[/]") + sys.exit(1) + + state = _get_state_manager(args.db) + + # Check for duplicate name + existing = state.get_session(args.name) + if existing is not None: + _console.print(f"[red]ERROR: Session '{args.name}' already exists (id={existing.session_id}).[/]") + _console.print("Use a different name or close the existing session first.") + sys.exit(1) + + session_id = state.create_session( + session_name=args.name, + config_path=config_path, + initial_equity=args.capital, + ) + _console.print(f"[green]Session created:[/] {args.name} id={session_id} capital=${args.capital:,.0f}") + _console.print(f"[dim]Config: {config_path}[/]") + _console.print(f"\nRun daily processing with:\n fithia2 paper run --session {args.name}") + + +def cmd_run(args: argparse.Namespace) -> None: + """Run daily processing for a specific session.""" + state = _get_state_manager(args.db) + session = _resolve_session(state, args.session) + + if session.status == "paused": + _console.print(f"[yellow]Session '{session.session_name}' is paused. Use 'resume' first.[/]") + sys.exit(1) + if session.status == "closed": + _console.print(f"[red]Session '{session.session_name}' is closed.[/]") + sys.exit(1) + + broker = _get_broker() + + from apps.paper_trader.event_detector import EventDetector + from apps.paper_trader.engine import PaperTradingEngine + from apps.paper_trader.reporter import print_run_summary + + oracle_url = os.environ.get("ORACLE_URL") or os.environ.get("STOCK_ORACLE_URL", "http://localhost:8000") + db_dsn = os.environ.get("DB_DSN") or os.environ.get("POSTGRES_DSN", "") + + detector = EventDetector(db_dsn=db_dsn, oracle_url=oracle_url) + engine = PaperTradingEngine(session=session, broker=broker, state=state, event_detector=detector) + + target_date = None + if args.date: + import datetime as dt + try: + target_date = dt.date.fromisoformat(args.date) + except ValueError: + _console.print(f"[red]ERROR: Invalid date format: {args.date}. Use YYYY-MM-DD.[/]") + sys.exit(1) + + summary = asyncio.run(engine.run_daily(target_date=target_date, force=getattr(args, "force", False))) + print_run_summary(summary) + + +def _make_engine(session, args): + broker = _get_broker() + from apps.paper_trader.event_detector import EventDetector + from apps.paper_trader.engine import PaperTradingEngine + oracle_url = os.environ.get("ORACLE_URL") or os.environ.get("STOCK_ORACLE_URL", "http://localhost:8000") + db_dsn = os.environ.get("DB_DSN") or os.environ.get("POSTGRES_DSN", "") + detector = EventDetector(db_dsn=db_dsn, oracle_url=oracle_url) + state = _get_state_manager(args.db) + return PaperTradingEngine(session=session, broker=broker, state=state, event_detector=detector) + + +def cmd_run_close(args: argparse.Namespace) -> None: + """장 마감 직전: same-day 이벤트 → MOC 매수.""" + state = _get_state_manager(args.db) + session = _resolve_session(state, args.session) + + if session.status != "active": + _console.print(f"[red]Session '{session.session_name}' is not active.[/]") + sys.exit(1) + + import datetime as dt + target_date = None + if args.date: + try: + target_date = dt.date.fromisoformat(args.date) + except ValueError: + _console.print(f"[red]ERROR: Invalid date: {args.date}[/]") + sys.exit(1) + + engine = _make_engine(session, args) + from apps.paper_trader.reporter import print_run_summary + summary = asyncio.run(engine.run_reaction_close(target_date=target_date, force=getattr(args, "force", False))) + print_run_summary(summary) + + +def cmd_run_open(args: argparse.Namespace) -> None: + """장 시작 직후: 전날 exit 판단 + after-close 이벤트 → 시장가 매수.""" + state = _get_state_manager(args.db) + session = _resolve_session(state, args.session) + + if session.status != "active": + _console.print(f"[red]Session '{session.session_name}' is not active.[/]") + sys.exit(1) + + import datetime as dt + target_date = None + if args.date: + try: + target_date = dt.date.fromisoformat(args.date) + except ValueError: + _console.print(f"[red]ERROR: Invalid date: {args.date}[/]") + sys.exit(1) + + engine = _make_engine(session, args) + from apps.paper_trader.reporter import print_run_summary + summary = asyncio.run(engine.run_next_open(target_date=target_date, force=getattr(args, "force", False))) + print_run_summary(summary) + + +def cmd_monitor(args: argparse.Namespace) -> None: + """장중 실시간 stop/target 모니터링 (Ctrl+C로 종료).""" + state = _get_state_manager(args.db) + session = _resolve_session(state, args.session) + + if session.status != "active": + _console.print(f"[red]Session '{session.session_name}' is not active.[/]") + sys.exit(1) + + _console.print(f"[cyan]Monitoring session '{session.session_name}' every {args.interval}s (Ctrl+C to stop)...[/]") + engine = _make_engine(session, args) + try: + asyncio.run(engine.run_monitor(interval_sec=args.interval)) + except KeyboardInterrupt: + _console.print("\n[yellow]Monitor stopped.[/]") + + +def cmd_run_all(args: argparse.Namespace) -> None: + """Run daily processing for all active sessions.""" + state = _get_state_manager(args.db) + sessions = [s for s in state.list_sessions() if s.status == "active"] + + if not sessions: + _console.print("[dim]No active sessions found.[/]") + return + + broker = _get_broker() + + from apps.paper_trader.event_detector import EventDetector + from apps.paper_trader.engine import PaperTradingEngine + from apps.paper_trader.reporter import print_run_summary + + oracle_url = os.environ.get("ORACLE_URL") or os.environ.get("STOCK_ORACLE_URL", "http://localhost:8000") + db_dsn = os.environ.get("DB_DSN") or os.environ.get("POSTGRES_DSN", "") + + for session in sessions: + _console.print(f"\n[bold]Running session: {session.session_name}[/]") + detector = EventDetector(db_dsn=db_dsn, oracle_url=oracle_url) + engine = PaperTradingEngine(session=session, broker=broker, state=state, event_detector=detector) + summary = asyncio.run(engine.run_daily()) + print_run_summary(summary) + + +def cmd_status(args: argparse.Namespace) -> None: + """Show session status.""" + state = _get_state_manager(args.db) + session = _resolve_session(state, args.session) + broker = _get_broker() + + from apps.paper_trader.reporter import print_status + print_status(session, broker, state) + + +def cmd_positions(args: argparse.Namespace) -> None: + """Show current positions (live from Alpaca).""" + state = _get_state_manager(args.db) + session = _resolve_session(state, args.session) + broker = _get_broker() + + from apps.paper_trader.reporter import print_positions + print_positions(session, broker, state) + + +def cmd_trades(args: argparse.Namespace) -> None: + """Show trade history.""" + state = _get_state_manager(args.db) + session = _resolve_session(state, args.session) + + from apps.paper_trader.reporter import print_trades + print_trades(session, state, last=args.last) + + +def cmd_equity(args: argparse.Namespace) -> None: + """Show equity curve.""" + state = _get_state_manager(args.db) + session = _resolve_session(state, args.session) + + from apps.paper_trader.reporter import print_equity + print_equity(session, state) + + +def cmd_sessions(args: argparse.Namespace) -> None: + """List all sessions.""" + state = _get_state_manager(args.db) + sessions = state.list_sessions() + + from apps.paper_trader.reporter import print_sessions + print_sessions(sessions) + + +def cmd_pause(args: argparse.Namespace) -> None: + """Pause a session (stops daily processing).""" + state = _get_state_manager(args.db) + session = _resolve_session(state, args.session) + if session.status != "active": + _console.print(f"[yellow]Session '{session.session_name}' is already {session.status}.[/]") + return + state.set_session_status(session.session_id, "paused") + _console.print(f"[yellow]Session '{session.session_name}' paused.[/]") + + +def cmd_resume(args: argparse.Namespace) -> None: + """Resume a paused session.""" + state = _get_state_manager(args.db) + session = _resolve_session(state, args.session) + if session.status != "paused": + _console.print(f"[yellow]Session '{session.session_name}' is not paused (status={session.status}).[/]") + return + state.set_session_status(session.session_id, "active") + _console.print(f"[green]Session '{session.session_name}' resumed.[/]") + + +def cmd_backtest(args: argparse.Namespace) -> None: + """Run historical backtest simulation using paper trading engine.""" + import datetime as dt + + for cfg in args.configs: + if not Path(cfg).exists(): + _console.print(f"[red]ERROR: Config not found: {cfg}[/]") + sys.exit(1) + + try: + start_date = dt.date.fromisoformat(args.start) + end_date = dt.date.fromisoformat(args.end) + except ValueError as exc: + _console.print(f"[red]ERROR: Invalid date: {exc}[/]") + sys.exit(1) + + oracle_url = os.environ.get("ORACLE_URL") or os.environ.get("STOCK_ORACLE_URL", "http://localhost:8000") + db_dsn = os.environ.get("DB_DSN") or os.environ.get("POSTGRES_DSN", "") + + if not db_dsn: + _console.print("[red]ERROR: POSTGRES_DSN (or DB_DSN) env var not set.[/]") + sys.exit(1) + + _console.print(f"[bold cyan]Backtest:[/] {start_date} → {end_date} capital=${args.capital:,.0f}") + _console.print(f"Strategies: {', '.join(args.configs)}") + + from apps.paper_trader.backtest_sim import run_backtest + from apps.paper_trader.reporter import print_backtest_results + + results = run_backtest( + configs=args.configs, + capital=args.capital, + start_date=start_date, + end_date=end_date, + db_dsn=db_dsn, + oracle_url=oracle_url, + console=_console, + ) + print_backtest_results(results, output_dir=args.output) + + +def cmd_auto(args: argparse.Namespace) -> None: + """자동 데몬 — Phoenix(MST) 기준 ET 장 스케줄에 맞게 파이프라인 + 매매 자동 실행.""" + from apps.paper_trader.auto import run_auto + run_auto( + sessions=args.session or [], + db=args.db, + dry_run=args.dry_run, + ) + + +def cmd_close(args: argparse.Namespace) -> None: + """Close a session and liquidate all positions.""" + state = _get_state_manager(args.db) + session = _resolve_session(state, args.session) + + if session.status == "closed": + _console.print(f"[dim]Session '{session.session_name}' is already closed.[/]") + return + + broker = _get_broker() + + # Confirm + if not args.yes: + positions = broker.list_positions() + if positions: + _console.print(f"[yellow]This will liquidate {len(positions)} open position(s):[/]") + for p in positions: + _console.print(f" {p.symbol}: {p.qty} shares") + confirm = input(f"Close session '{session.session_name}'? [y/N] ") + if confirm.lower() != "y": + _console.print("Cancelled.") + return + + # Liquidate + try: + orders = broker.close_all_positions() + _console.print(f"[yellow]Liquidated {len(orders)} position(s).[/]") + except Exception as exc: + _console.print(f"[red]WARNING: Failed to close positions: {exc}[/]") + + # Mark all open strategy states as closed + for ss in state.get_open_strategy_states(session.session_id): + state.close_strategy_state(session.session_id, ss.symbol) + + state.set_session_status(session.session_id, "closed") + _console.print(f"[red]Session '{session.session_name}' closed.[/]") + + +# ------------------------------------------------------------------ # +# Main +# ------------------------------------------------------------------ # + +def _print_help() -> None: + from rich import box as rbox + from rich.panel import Panel + from rich.table import Table + + _console.print() + _console.print(Panel( + "[bold cyan]fithia2 paper-trader[/] — Alpaca Paper Trading CLI\n" + "[dim]전략 백테스트 로직으로 판단, Alpaca Paper Trading API로 주문 실행[/]", + border_style="cyan", + padding=(0, 2), + )) + + tbl = Table(box=rbox.SIMPLE, show_header=True, header_style="bold yellow", padding=(0, 2)) + tbl.add_column("Command", style="bold green", no_wrap=True) + tbl.add_column("Description") + tbl.add_column("Key Options", style="dim") + + tbl.add_row("[bold cyan]backtest[/]", "과거 기간 시뮬레이션 (복수 전략 비교)", "--config PATH [--config PATH] --start DATE --end DATE [--capital N] [--output DIR]") + tbl.add_row("[bold cyan]auto[/]", "자동 데몬 — 스케줄에 맞게 파이프라인+매매 자동 실행", "[--session NAME] [--dry-run]") + tbl.add_row("", "", "") + tbl.add_row("start", "새 세션 생성", "--config PATH --capital FLOAT --name STR") + tbl.add_row("run", "일일 처리 실행 (특정 세션)", "--session NAME [--date YYYY-MM-DD]") + tbl.add_row("run-close", "장 마감 직전: same-day 이벤트 → MOC 매수", "--session NAME [--date]") + tbl.add_row("run-open", "장 시작 직후: exit + after-close 신규진입", "--session NAME [--date]") + tbl.add_row("run-all", "전체 활성 세션 일일 처리", "") + tbl.add_row("monitor", "장중 실시간 stop/target 모니터링", "--session NAME [--interval N]") + tbl.add_row("", "", "") + tbl.add_row("status", "세션 상태 조회", "--session NAME") + tbl.add_row("positions", "보유 포지션 조회 (Alpaca 실시간)", "--session NAME") + tbl.add_row("trades", "거래 내역", "--session NAME [--last N]") + tbl.add_row("equity", "Equity curve", "--session NAME") + tbl.add_row("sessions", "세션 목록", "") + tbl.add_row("pause", "세션 일시정지", "--session NAME") + tbl.add_row("resume", "세션 재개", "--session NAME") + tbl.add_row("close", "세션 종료 (전 포지션 청산)", "--session NAME [--yes]") + + _console.print(tbl) + _console.print( + " [dim]공통 옵션:[/] [bold]--db[/] [dim](기본: paper_trading.db)[/]\n" + " [dim]환경변수:[/] ALPACA_API_KEY ALPACA_SECRET_KEY ALPACA_PAPER=true " + "ORACLE_URL DB_DSN\n" + ) + _console.print(" [bold]예시[/]") + _console.print(" [green]fithia2 paper auto --session my_session[/] [dim]← 추천: 데몬으로 모든 것을 자동 처리[/]") + _console.print(" [green]fithia2 paper start --config configs/experiments/return_max_long_v504.json --name my_session[/]") + _console.print(" [green]fithia2 paper positions --session my_session[/]") + _console.print(" [green]fithia2 paper trades --session my_session --last 20[/]") + _console.print() + + +def main() -> None: + # Auto-load .env file (python-dotenv) + try: + from dotenv import load_dotenv + load_dotenv() + except ImportError: + pass + + if len(sys.argv) == 1: + _print_help() + sys.exit(0) + + parser = argparse.ArgumentParser( + description="fithia2 Paper Trader — Alpaca Paper Trading CLI", + add_help=True, + ) + sub = parser.add_subparsers(dest="command", required=True) + + db_kwargs = {"default": _DEFAULT_DB, "help": f"SQLite DB path (default: {_DEFAULT_DB})"} + + # backtest + p = sub.add_parser("backtest", help="Run historical backtest simulation using paper trading engine") + p.add_argument("--config", "-c", action="append", required=True, + dest="configs", metavar="PATH", + help="Config path (repeat for multiple strategies)") + p.add_argument("--capital", "-k", type=float, default=10000.0, + help="Per-session capital (default: 10000)") + p.add_argument("--start", required=True, metavar="YYYY-MM-DD", + help="Backtest start date") + p.add_argument("--end", required=True, metavar="YYYY-MM-DD", + help="Backtest end date") + p.add_argument("--output", "-o", default=None, + help="Directory to save results CSV (optional)") + + # auto + p = sub.add_parser("auto", help="자동 데몬 — ET 장 스케줄에 맞게 파이프라인+매매 자동 실행") + p.add_argument("--db", **db_kwargs) + p.add_argument("--session", "-s", nargs="*", default=[], metavar="NAME", + help="Session name(s). Default: all active sessions.") + p.add_argument("--dry-run", action="store_true", + help="Print commands without executing.") + + # start + p = sub.add_parser("start", help="Create a new paper trading session") + p.add_argument("--db", **db_kwargs) + p.add_argument("--config", "-c", required=True, help="Path to experiment manifest or config JSON") + p.add_argument("--capital", "-k", type=float, default=10000.0, help="Initial capital (default: 10000)") + p.add_argument("--name", "-n", required=True, help="Session name (unique)") + + # run + p = sub.add_parser("run", help="Run daily processing for a session") + p.add_argument("--db", **db_kwargs) + p.add_argument("--session", "-s", required=True, help="Session name or ID") + p.add_argument("--date", "-d", help="Target date YYYY-MM-DD (default: today)") + p.add_argument("--force", "-f", action="store_true", help="Re-run even if date already processed") + + # run-close + p = sub.add_parser("run-close", help="장 마감 직전: same-day 이벤트 → MOC 매수") + p.add_argument("--db", **db_kwargs) + p.add_argument("--session", "-s", required=True, help="Session name or ID") + p.add_argument("--date", "-d", help="Target date YYYY-MM-DD (default: today)") + p.add_argument("--force", "-f", action="store_true", help="Re-run even if already processed") + + # run-open + p = sub.add_parser("run-open", help="장 시작 직후: 전날 exit + after-close 이벤트 → 시장가 매수") + p.add_argument("--db", **db_kwargs) + p.add_argument("--session", "-s", required=True, help="Session name or ID") + p.add_argument("--date", "-d", help="Target date YYYY-MM-DD (default: today)") + p.add_argument("--force", "-f", action="store_true", help="Re-run even if already processed") + + # monitor + p = sub.add_parser("monitor", help="장중 실시간 stop/target 모니터링") + p.add_argument("--db", **db_kwargs) + p.add_argument("--session", "-s", required=True, help="Session name or ID") + p.add_argument("--interval", "-i", type=int, default=60, help="Check interval in seconds (default: 60)") + + # run-all + p = sub.add_parser("run-all", help="Run daily processing for all active sessions") + p.add_argument("--db", **db_kwargs) + + # status + p = sub.add_parser("status", help="Show session status") + p.add_argument("--db", **db_kwargs) + p.add_argument("--session", "-s", required=True, help="Session name or ID") + + # positions + p = sub.add_parser("positions", help="Show current positions") + p.add_argument("--db", **db_kwargs) + p.add_argument("--session", "-s", required=True, help="Session name or ID") + + # trades + p = sub.add_parser("trades", help="Show trade history") + p.add_argument("--db", **db_kwargs) + p.add_argument("--session", "-s", required=True, help="Session name or ID") + p.add_argument("--last", "-n", type=int, default=None, help="Show last N trades") + + # equity + p = sub.add_parser("equity", help="Show equity curve") + p.add_argument("--db", **db_kwargs) + p.add_argument("--session", "-s", required=True, help="Session name or ID") + + # sessions + p = sub.add_parser("sessions", help="List all sessions") + p.add_argument("--db", **db_kwargs) + + # pause + p = sub.add_parser("pause", help="Pause a session") + p.add_argument("--db", **db_kwargs) + p.add_argument("--session", "-s", required=True, help="Session name or ID") + + # resume + p = sub.add_parser("resume", help="Resume a paused session") + p.add_argument("--db", **db_kwargs) + p.add_argument("--session", "-s", required=True, help="Session name or ID") + + # close + p = sub.add_parser("close", help="Close session (liquidates all positions)") + p.add_argument("--db", **db_kwargs) + p.add_argument("--session", "-s", required=True, help="Session name or ID") + p.add_argument("--yes", "-y", action="store_true", help="Skip confirmation prompt") + + args = parser.parse_args() + + dispatch = { + "backtest": cmd_backtest, + "auto": cmd_auto, + "start": cmd_start, + "run": cmd_run, + "run-close": cmd_run_close, + "run-open": cmd_run_open, + "monitor": cmd_monitor, + "run-all": cmd_run_all, + "status": cmd_status, + "positions": cmd_positions, + "trades": cmd_trades, + "equity": cmd_equity, + "sessions": cmd_sessions, + "pause": cmd_pause, + "resume": cmd_resume, + "close": cmd_close, + } + dispatch[args.command](args) + + +if __name__ == "__main__": + main()