"""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 _resolve_sessions(state, name_or_id: str | None) -> list: """Return a single session if specified, or all sessions if None.""" if name_or_id: 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] sessions = state.list_sessions() if not sessions: _console.print("[dim]No sessions found.[/]") sys.exit(0) return sessions def cmd_status(args: argparse.Namespace) -> None: """Show session status.""" state = _get_state_manager(args.db) broker = _get_broker() from apps.paper_trader.reporter import print_status for session in _resolve_sessions(state, args.session): print_status(session, broker, state) def cmd_positions(args: argparse.Namespace) -> None: """Show current positions (live from Alpaca).""" state = _get_state_manager(args.db) broker = _get_broker() from apps.paper_trader.reporter import print_positions for session in _resolve_sessions(state, args.session): print_positions(session, broker, state) def cmd_trades(args: argparse.Namespace) -> None: """Show trade history.""" state = _get_state_manager(args.db) from apps.paper_trader.reporter import print_trades for session in _resolve_sessions(state, args.session): print_trades(session, state, last=args.last) def cmd_equity(args: argparse.Namespace) -> None: """Show equity curve.""" state = _get_state_manager(args.db) from apps.paper_trader.reporter import print_equity for session in _resolve_sessions(state, args.session): 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 _resolve_rank_configs(start: int, end: int) -> list[str]: """Load strategies ranked start..end from leaderboard by SQS score. start/end are 1-based inclusive. e.g. (1, 5) = top 5, (20, 40) = rank 20-40. Overlays are excluded; use --overlay to run them explicitly. """ import json registry_path = Path("journal/experiment_registry.json") if not registry_path.exists(): _console.print("[red]ERROR: journal/experiment_registry.json not found. Run `fithia2 lb` first.[/]") sys.exit(1) registry = json.loads(registry_path.read_text()) ranked: list[str] = [] skipped_overlays: list[str] = [] entries = sorted( ( e for e in registry.get("entries", []) if e.get("sqs_score") is not None and not e.get("is_retired", False) ), key=lambda e: e["sqs_score"], reverse=True, ) for e in entries: is_overlay = e.get("strategy_family") == "overlay" or e.get("overlay_common_window_summary") is not None if is_overlay: skipped_overlays.append(e["experiment_name"]) continue if e.get("trade_count", 0) <= 0 or e.get("valid_trade_count", 0) <= 0: continue name = e["experiment_name"] cfg_path = e.get("config_path") or f"configs/experiments/{name}.json" if Path(cfg_path).exists(): ranked.append(cfg_path) if skipped_overlays: labels = ", ".join(skipped_overlays[:5]) if len(skipped_overlays) > 5: labels += ", ..." _console.print( "[yellow]Skipping overlay leaderboard entries for `--top/--rank` " f"(use `--overlay` to run them explicitly): {labels}[/]" ) # 1-based inclusive slice return ranked[start - 1 : end] def cmd_backtest(args: argparse.Namespace) -> None: """Run historical backtest simulation using paper trading engine.""" import datetime as dt configs = args.configs or [] if args.overlays: configs.extend(args.overlays) if args.top: configs = _resolve_rank_configs(1, args.top) + configs if args.rank: parts = args.rank.split("-") if len(parts) == 1 and parts[0].isdigit(): n = int(parts[0]) configs = _resolve_rank_configs(n, n) + configs elif len(parts) == 2 and parts[0].isdigit() and parts[1].isdigit(): configs = _resolve_rank_configs(int(parts[0]), int(parts[1])) + configs else: _console.print("[red]ERROR: --rank format: N or START-END (e.g. 5 or 20-40)[/]") sys.exit(1) if not configs: _console.print("[red]ERROR: Specify --config, --overlay, --top, or --rank[/]") sys.exit(1) for cfg in configs: if not Path(cfg).exists(): _console.print(f"[red]ERROR: Config not found: {cfg}[/]") sys.exit(1) import calendar def _latest_backtest_date() -> dt.date: """Return today if market is closed (after 4 PM ET or non-trading day), else yesterday.""" from libs.common.time_utils import is_trading_day, to_eastern, utc_now now_et = to_eastern(utc_now()) today = now_et.date() if not is_trading_day(today) or now_et.hour >= 16: return today return today - dt.timedelta(days=1) def _parse_date(val: str, is_end: bool = False) -> dt.date: """Parse YYYY-MM-DD, YYYY-MM, or YYYY. Clamp end dates to latest available.""" latest = _latest_backtest_date() parts = val.split("-") if len(parts) == 1 and len(val) == 4 and val.isdigit(): # YYYY year = int(val) if is_end: return min(dt.date(year, 12, 31), latest) return dt.date(year, 1, 1) if len(parts) == 2: # YYYY-MM year, month = int(parts[0]), int(parts[1]) if is_end: last_day = calendar.monthrange(year, month)[1] return min(dt.date(year, month, last_day), latest) return dt.date(year, month, 1) return dt.date.fromisoformat(val) # Resolve --year shorthand if args.year: if args.start or args.end: _console.print("[red]ERROR: --year cannot be combined with --start/--end[/]") sys.exit(1) args.start = args.year args.end = args.year if not args.start: _console.print("[red]ERROR: Specify --start (and optionally --end), or --year[/]") sys.exit(1) try: start_date = _parse_date(args.start) end_date = _parse_date(args.end, is_end=True) if args.end else _latest_backtest_date() 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}") names = [Path(c).stem for c in configs] _console.print(f"Strategies ({len(configs)}): {', '.join(names)}") from apps.paper_trader.backtest_sim import run_backtest from apps.paper_trader.reporter import print_backtest_results results = run_backtest( configs=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, show_trades=not args.no_trades) 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) # Delete session and all related data state.delete_session(session.session_id) _console.print(f"[red]Session '{session.session_name}' closed and deleted.[/]") # ------------------------------------------------------------------ # # 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 | --top N --year YYYY | --start DATE --end DATE [--capital N] [--no-trades]") 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", dest="configs", metavar="PATH", help="Config path (repeat for multiple strategies)") p.add_argument("--overlay", action="append", dest="overlays", metavar="PATH", help="Overlay config path (repeat for multiple)") p.add_argument("--top", "-t", type=int, default=None, metavar="N", help="Use top N strategies from leaderboard (by SQS score)") p.add_argument("--rank", default=None, metavar="START-END", help="Use strategies ranked START to END (e.g. 20-40)") p.add_argument("--capital", "-k", type=float, default=10000.0, help="Per-session capital (default: 10000)") p.add_argument("--start", default=None, metavar="YYYY[-MM-DD]", help="Backtest start date (YYYY-MM-DD or YYYY)") p.add_argument("--end", default=None, metavar="YYYY[-MM-DD]", help="Backtest end date (YYYY-MM-DD or YYYY)") p.add_argument("--year", "-y", default=None, metavar="YYYY", help="Shorthand for --start YYYY --end YYYY") p.add_argument("--output", "-o", default=None, help="Directory to save results CSV (optional)") p.add_argument("--no-trades", action="store_true", default=False, help="Hide per-strategy trade log") # 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", default=None, help="Session name or ID (omit for all)") # positions p = sub.add_parser("positions", help="Show current positions") p.add_argument("--db", **db_kwargs) p.add_argument("--session", "-s", default=None, help="Session name or ID (omit for all)") # trades p = sub.add_parser("trades", help="Show trade history") p.add_argument("--db", **db_kwargs) p.add_argument("--session", "-s", default=None, help="Session name or ID (omit for all)") 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", default=None, help="Session name or ID (omit for all)") # 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()