@ -227,42 +227,53 @@ def cmd_run_all(args: argparse.Namespace) -> None:
print_run_summary ( summary )
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 :
def cmd_status ( args : argparse . Namespace ) - > None :
""" Show session status. """
""" Show session status. """
state = _get_state_manager ( args . db )
state = _get_state_manager ( args . db )
session = _resolve_session ( state , args . session )
broker = _get_broker ( )
broker = _get_broker ( )
from apps . paper_trader . reporter import print_status
from apps . paper_trader . reporter import print_status
print_status ( session , broker , state )
for session in _resolve_sessions ( state , args . session ) :
print_status ( session , broker , state )
def cmd_positions ( args : argparse . Namespace ) - > None :
def cmd_positions ( args : argparse . Namespace ) - > None :
""" Show current positions (live from Alpaca). """
""" Show current positions (live from Alpaca). """
state = _get_state_manager ( args . db )
state = _get_state_manager ( args . db )
session = _resolve_session ( state , args . session )
broker = _get_broker ( )
broker = _get_broker ( )
from apps . paper_trader . reporter import print_positions
from apps . paper_trader . reporter import print_positions
print_positions ( session , broker , state )
for session in _resolve_sessions ( state , args . session ) :
print_positions ( session , broker , state )
def cmd_trades ( args : argparse . Namespace ) - > None :
def cmd_trades ( args : argparse . Namespace ) - > None :
""" Show trade history. """
""" Show trade history. """
state = _get_state_manager ( args . db )
state = _get_state_manager ( args . db )
session = _resolve_session ( state , args . session )
from apps . paper_trader . reporter import print_trades
from apps . paper_trader . reporter import print_trades
print_trades ( session , state , last = args . last )
for session in _resolve_sessions ( state , args . session ) :
print_trades ( session , state , last = args . last )
def cmd_equity ( args : argparse . Namespace ) - > None :
def cmd_equity ( args : argparse . Namespace ) - > None :
""" Show equity curve. """
""" Show equity curve. """
state = _get_state_manager ( args . db )
state = _get_state_manager ( args . db )
session = _resolve_session ( state , args . session )
from apps . paper_trader . reporter import print_equity
from apps . paper_trader . reporter import print_equity
print_equity ( session , state )
for session in _resolve_sessions ( state , args . session ) :
print_equity ( session , state )
def cmd_sessions ( args : argparse . Namespace ) - > None :
def cmd_sessions ( args : argparse . Namespace ) - > None :
@ -296,18 +307,130 @@ def cmd_resume(args: argparse.Namespace) -> None:
_console . print ( f " [green]Session ' { session . session_name } ' resumed.[/] " )
_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 :
def cmd_backtest ( args : argparse . Namespace ) - > None :
""" Run historical backtest simulation using paper trading engine. """
""" Run historical backtest simulation using paper trading engine. """
import datetime as dt
import datetime as dt
for cfg in args . configs :
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 ( ) :
if not Path ( cfg ) . exists ( ) :
_console . print ( f " [red]ERROR: Config not found: { cfg } [/] " )
_console . print ( f " [red]ERROR: Config not found: { cfg } [/] " )
sys . exit ( 1 )
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 :
try :
start_date = dt . date . fromisoformat ( args . start )
start_date = _parse_date ( args . start )
end_date = dt . date . fromisoformat ( args . end )
end_date = _parse_date( args . end , is_end = True ) if args . end else _latest_backtest_date ( )
except ValueError as exc :
except ValueError as exc :
_console . print ( f " [red]ERROR: Invalid date: { exc } [/] " )
_console . print ( f " [red]ERROR: Invalid date: { exc } [/] " )
sys . exit ( 1 )
sys . exit ( 1 )
@ -320,13 +443,14 @@ def cmd_backtest(args: argparse.Namespace) -> None:
sys . exit ( 1 )
sys . exit ( 1 )
_console . print ( f " [bold cyan]Backtest:[/] { start_date } → { end_date } capital=$ { args . capital : ,.0f } " )
_console . print ( f " [bold cyan]Backtest:[/] { start_date } → { end_date } capital=$ { args . capital : ,.0f } " )
_console . print ( f " Strategies: { ' , ' . join ( args . configs ) } " )
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 . backtest_sim import run_backtest
from apps . paper_trader . reporter import print_backtest_results
from apps . paper_trader . reporter import print_backtest_results
results = run_backtest (
results = run_backtest (
configs = args. configs,
configs = configs,
capital = args . capital ,
capital = args . capital ,
start_date = start_date ,
start_date = start_date ,
end_date = end_date ,
end_date = end_date ,
@ -334,7 +458,7 @@ def cmd_backtest(args: argparse.Namespace) -> None:
oracle_url = oracle_url ,
oracle_url = oracle_url ,
console = _console ,
console = _console ,
)
)
print_backtest_results ( results , output_dir = args . output )
print_backtest_results ( results , output_dir = args . output , show_trades = not args . no_trades )
def cmd_auto ( args : argparse . Namespace ) - > None :
def cmd_auto ( args : argparse . Namespace ) - > None :
@ -381,8 +505,9 @@ def cmd_close(args: argparse.Namespace) -> None:
for ss in state . get_open_strategy_states ( session . session_id ) :
for ss in state . get_open_strategy_states ( session . session_id ) :
state . close_strategy_state ( session . session_id , ss . symbol )
state . close_strategy_state ( session . session_id , ss . symbol )
state . set_session_status ( session . session_id , " closed " )
# Delete session and all related data
_console . print ( f " [red]Session ' { session . session_name } ' closed.[/] " )
state . delete_session ( session . session_id )
_console . print ( f " [red]Session ' { session . session_name } ' closed and deleted.[/] " )
# ------------------------------------------------------------------ #
# ------------------------------------------------------------------ #
@ -407,7 +532,7 @@ def _print_help() -> None:
tbl . add_column ( " Description " )
tbl . add_column ( " Description " )
tbl . add_column ( " Key Options " , style = " dim " )
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]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 ( " [bold cyan]auto[/] " , " 자동 데몬 — 스케줄에 맞게 파이프라인+매매 자동 실행 " , " [--session NAME] [--dry-run] " )
tbl . add_row ( " " , " " , " " )
tbl . add_row ( " " , " " , " " )
tbl . add_row ( " start " , " 새 세션 생성 " , " --config PATH --capital FLOAT --name STR " )
tbl . add_row ( " start " , " 새 세션 생성 " , " --config PATH --capital FLOAT --name STR " )
@ -462,17 +587,27 @@ def main() -> None:
# backtest
# backtest
p = sub . add_parser ( " backtest " , help = " Run historical backtest simulation using paper trading engine " )
p = sub . add_parser ( " backtest " , help = " Run historical backtest simulation using paper trading engine " )
p . add_argument ( " --config " , " -c " , action = " append " , required = True ,
p . add_argument ( " --config " , " -c " , action = " append " ,
dest = " configs " , metavar = " PATH " ,
dest = " configs " , metavar = " PATH " ,
help = " Config path (repeat for multiple strategies) " )
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 ,
p . add_argument ( " --capital " , " -k " , type = float , default = 10000.0 ,
help = " Per-session capital (default: 10000) " )
help = " Per-session capital (default: 10000) " )
p . add_argument ( " --start " , required = True , metavar = " YYYY-MM-DD " ,
p . add_argument ( " --start " , default = None , metavar = " YYYY[-MM-DD] " ,
help = " Backtest start date " )
help = " Backtest start date (YYYY-MM-DD or YYYY) " )
p . add_argument ( " --end " , required = True , metavar = " YYYY-MM-DD " ,
p . add_argument ( " --end " , default = None , metavar = " YYYY[-MM-DD] " ,
help = " Backtest end date " )
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 ,
p . add_argument ( " --output " , " -o " , default = None ,
help = " Directory to save results CSV (optional) " )
help = " Directory to save results CSV (optional) " )
p . add_argument ( " --no-trades " , action = " store_true " , default = False ,
help = " Hide per-strategy trade log " )
# auto
# auto
p = sub . add_parser ( " auto " , help = " 자동 데몬 — ET 장 스케줄에 맞게 파이프라인+매매 자동 실행 " )
p = sub . add_parser ( " auto " , help = " 자동 데몬 — ET 장 스케줄에 맞게 파이프라인+매매 자동 실행 " )
@ -523,23 +658,23 @@ def main() -> None:
# status
# status
p = sub . add_parser ( " status " , help = " Show session status " )
p = sub . add_parser ( " status " , help = " Show session status " )
p . add_argument ( " --db " , * * db_kwargs )
p . add_argument ( " --db " , * * db_kwargs )
p . add_argument ( " --session " , " -s " , required= Tru e, help = " Session name or ID " )
p . add_argument ( " --session " , " -s " , default= Non e, help = " Session name or ID (omit for all) " )
# positions
# positions
p = sub . add_parser ( " positions " , help = " Show current positions " )
p = sub . add_parser ( " positions " , help = " Show current positions " )
p . add_argument ( " --db " , * * db_kwargs )
p . add_argument ( " --db " , * * db_kwargs )
p . add_argument ( " --session " , " -s " , required= Tru e, help = " Session name or ID " )
p . add_argument ( " --session " , " -s " , default= Non e, help = " Session name or ID (omit for all) " )
# trades
# trades
p = sub . add_parser ( " trades " , help = " Show trade history " )
p = sub . add_parser ( " trades " , help = " Show trade history " )
p . add_argument ( " --db " , * * db_kwargs )
p . add_argument ( " --db " , * * db_kwargs )
p . add_argument ( " --session " , " -s " , required= Tru e, help = " Session name or ID " )
p . add_argument ( " --session " , " -s " , default= Non e, help = " Session name or ID (omit for all) " )
p . add_argument ( " --last " , " -n " , type = int , default = None , help = " Show last N trades " )
p . add_argument ( " --last " , " -n " , type = int , default = None , help = " Show last N trades " )
# equity
# equity
p = sub . add_parser ( " equity " , help = " Show equity curve " )
p = sub . add_parser ( " equity " , help = " Show equity curve " )
p . add_argument ( " --db " , * * db_kwargs )
p . add_argument ( " --db " , * * db_kwargs )
p . add_argument ( " --session " , " -s " , required= Tru e, help = " Session name or ID " )
p . add_argument ( " --session " , " -s " , default= Non e, help = " Session name or ID (omit for all) " )
# sessions
# sessions
p = sub . add_parser ( " sessions " , help = " List all sessions " )
p = sub . add_parser ( " sessions " , help = " List all sessions " )