You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

826 lines
35 KiB
Python

"""CLI for paper trading with Alpaca API.
Usage:
fithia2 paper <command> [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
def _resolve_config_path(id_or_path: str) -> str:
"""Resolve an experiment ID or name to a config file path.
Accepts:
- Numeric ID: '228' → looks up name from index → 'configs/experiments/NAME.json'
- Experiment name: 'return_max_long_v6new.362''configs/experiments/NAME.json'
- Existing file path: 'configs/experiments/foo.json' → unchanged
"""
if Path(id_or_path).exists():
return id_or_path
try:
from libs.backtest.experiments import resolve_experiment_name
name = resolve_experiment_name(id_or_path)
return f"configs/experiments/{name}.json"
except (KeyError, Exception):
# Fall back to treating as-is (original behaviour)
return id_or_path
# ------------------------------------------------------------------ #
# Commands
# ------------------------------------------------------------------ #
def cmd_start(args: argparse.Namespace) -> None:
"""Create a new paper trading session."""
config_path = _resolve_config_path(args.config)
if not Path(config_path).exists():
_console.print(f"[red]ERROR: Config not found: {config_path}[/]")
sys.exit(1)
if (
getattr(args, "parking", None)
or getattr(args, "idle_alpha", None)
or getattr(args, "form4_sleeve", None)
or getattr(args, "ownership_sleeve", None)
):
from libs.backtest.domain import (
FORM4_CAPTURE_SLEEVE_PRESETS,
IDLE_ALPHA_SLEEVE_PRESETS,
OWNERSHIP_CAPTURE_SLEEVE_PRESETS,
PARKING_PRESETS,
)
if getattr(args, "parking", None) and args.parking not in PARKING_PRESETS:
_console.print(f"[red]ERROR: Unknown parking preset: {args.parking}[/]")
sys.exit(1)
if getattr(args, "idle_alpha", None) and args.idle_alpha not in IDLE_ALPHA_SLEEVE_PRESETS:
_console.print(f"[red]ERROR: Unknown idle alpha preset: {args.idle_alpha}[/]")
sys.exit(1)
if getattr(args, "form4_sleeve", None) and args.form4_sleeve not in FORM4_CAPTURE_SLEEVE_PRESETS:
_console.print(f"[red]ERROR: Unknown Form 4 sleeve preset: {args.form4_sleeve}[/]")
sys.exit(1)
if getattr(args, "ownership_sleeve", None) and args.ownership_sleeve not in OWNERSHIP_CAPTURE_SLEEVE_PRESETS:
_console.print(f"[red]ERROR: Unknown ownership sleeve preset: {args.ownership_sleeve}[/]")
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,
parking_preset=getattr(args, "parking", None),
idle_alpha_preset=getattr(args, "idle_alpha", None),
form4_sleeve_preset=getattr(args, "form4_sleeve", None),
ownership_sleeve_preset=getattr(args, "ownership_sleeve", None),
)
_console.print(f"[green]Session created:[/] {args.name} id={session_id} capital=${args.capital:,.0f}")
_console.print(f"[dim]Config: {config_path}[/]")
if getattr(args, "parking", None):
_console.print(f"[dim]Cash parking: {args.parking}[/]")
if getattr(args, "idle_alpha", None):
_console.print(f"[dim]Idle alpha: {args.idle_alpha}[/]")
if getattr(args, "form4_sleeve", None):
_console.print(f"[dim]Form 4 sleeve: {args.form4_sleeve}[/]")
if getattr(args, "ownership_sleeve", None):
_console.print(f"[dim]Ownership sleeve: {args.ownership_sleeve}[/]")
_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
from apps.backtester.run import load_manifest, resolve_config
from apps.paper_trader.backtest_sim import load_snapshot_store_for_session
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)
manifest = load_manifest(session.config_path)
config = resolve_config(manifest)
snapshot_store = load_snapshot_store_for_session(config, oracle_url, db_dsn)
return PaperTradingEngine(
session=session, broker=broker, state=state, event_detector=detector,
snapshot_store=snapshot_store,
)
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.
"""
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] = []
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:
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)
# 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 = [_resolve_config_path(c) for c in (args.configs or [])]
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, --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)
if (
getattr(args, "parking", None)
or getattr(args, "idle_alpha", None)
or getattr(args, "form4_sleeve", None)
or getattr(args, "ownership_sleeve", None)
):
from libs.backtest.domain import (
FORM4_CAPTURE_SLEEVE_PRESETS,
IDLE_ALPHA_SLEEVE_PRESETS,
OWNERSHIP_CAPTURE_SLEEVE_PRESETS,
PARKING_PRESETS,
)
if getattr(args, "parking", None) and args.parking not in PARKING_PRESETS:
_console.print(f"[red]ERROR: Unknown parking preset: {args.parking}[/]")
sys.exit(1)
if getattr(args, "idle_alpha", None) and args.idle_alpha not in IDLE_ALPHA_SLEEVE_PRESETS:
_console.print(f"[red]ERROR: Unknown idle alpha preset: {args.idle_alpha}[/]")
sys.exit(1)
if getattr(args, "form4_sleeve", None) and args.form4_sleeve not in FORM4_CAPTURE_SLEEVE_PRESETS:
_console.print(f"[red]ERROR: Unknown Form 4 sleeve preset: {args.form4_sleeve}[/]")
sys.exit(1)
if getattr(args, "ownership_sleeve", None) and args.ownership_sleeve not in OWNERSHIP_CAPTURE_SLEEVE_PRESETS:
_console.print(f"[red]ERROR: Unknown ownership sleeve preset: {args.ownership_sleeve}[/]")
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)
if getattr(args, "non_core_allocator_v2", False) and not getattr(args, "non_core_allocator_v2_mode", None):
args.non_core_allocator_v2_mode = "shadow"
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,
parking_preset=getattr(args, 'parking', None),
idle_alpha_preset=getattr(args, 'idle_alpha', None),
form4_sleeve_preset=getattr(args, 'form4_sleeve', None),
ownership_sleeve_preset=getattr(args, 'ownership_sleeve', None),
risk_off_alpha_sleeve_preset=getattr(args, 'risk_off_sleeve', None),
non_core_allocator_v2_mode=getattr(args, 'non_core_allocator_v2_mode', None),
snapshot_id_override=getattr(args, 'snapshot_id', None),
auto_refresh=not getattr(args, 'no_refresh', False),
)
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] [--parking P] [--idle-alpha P] [--form4-sleeve P] [--ownership-sleeve P]")
tbl.add_row("[bold cyan]auto[/]", "자동 데몬 — 스케줄에 맞게 파이프라인+매매 자동 실행", "[--session NAME] [--dry-run]")
tbl.add_row("", "", "")
tbl.add_row("start", "새 세션 생성", "--config PATH --capital FLOAT --name STR [--parking P] [--idle-alpha P] [--form4-sleeve P] [--ownership-sleeve P]")
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("--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")
p.add_argument("--parking", default=None, metavar="PRESET",
help="Apply cash parking preset (e.g. vm_24_m20, vol_20_24, sgov). Overrides config.")
p.add_argument("--idle-alpha", default=None, metavar="PRESET",
help="Apply idle alpha sleeve preset (e.g. micro_event_alpha). Overrides config.")
p.add_argument("--form4-sleeve", default=None, metavar="PRESET",
help="Apply Form 4 sleeve preset (e.g. reserve_form4_cluster_patient). Overrides config.")
p.add_argument("--ownership-sleeve", default=None, metavar="PRESET",
help="Apply 13D/13G ownership sleeve preset (e.g. ownership_13d_raise_reserve_plus_strict). Overrides config.")
p.add_argument("--risk-off-sleeve", default=None, metavar="PRESET",
help="Apply risk-off alpha sleeve preset. Overrides config.")
p.add_argument("--non-core-allocator-v2", action="store_true", default=False,
help="Enable non-core allocator v2.")
p.add_argument("--non-core-allocator-v2-mode", default=None, choices=["shadow", "live"],
help="Run non-core allocator v2 in shadow or live mode.")
p.add_argument("--snapshot-id", default=None, metavar="SNAPSHOT_ID",
help="Override dataset_snapshot_id (e.g. midlarge-liquid-long-v1-oot-2020-2021)")
p.add_argument("--no-refresh", action="store_true", default=False,
help="Skip automatic snapshot refresh (use existing data as-is)")
# 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)")
p.add_argument("--parking", default=None, metavar="PRESET", help="Cash parking preset name")
p.add_argument("--idle-alpha", default=None, metavar="PRESET", help="Idle alpha sleeve preset name")
p.add_argument("--form4-sleeve", default=None, metavar="PRESET", help="Form 4 sleeve preset name")
p.add_argument("--ownership-sleeve", default=None, metavar="PRESET", help="13D/13G ownership sleeve preset name")
# 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()