@@ -1241,26 +1241,151 @@ def wrapper(section_name, content):
12411241 display_complete_report (final_state )
12421242
12431243
1244- @app .command ()
1245- def analyze (
1246- checkpoint : bool | None = typer .Option (
1247- None ,
1248- "--checkpoint/--no-checkpoint" ,
1249- help = "Enable/disable checkpoint-resume (save state after each node so a "
1250- "crashed run can resume). Omit to honor TRADINGAGENTS_CHECKPOINT_ENABLED." ,
1251- ),
1252- clear_checkpoints : bool = typer .Option (
1253- False ,
1254- "--clear-checkpoints" ,
1255- help = "Delete all saved checkpoints before running (force fresh start)." ,
1256- ),
1257- ):
1244+ # Shared so the bare-invocation callback and the explicit `analyze` command
1245+ # expose identical flags without duplicating the help text.
1246+ _CHECKPOINT_OPT = typer .Option (
1247+ None ,
1248+ "--checkpoint/--no-checkpoint" ,
1249+ help = "Enable/disable checkpoint-resume (save state after each node so a "
1250+ "crashed run can resume). Omit to honor TRADINGAGENTS_CHECKPOINT_ENABLED." ,
1251+ )
1252+ _CLEAR_CHECKPOINTS_OPT = typer .Option (
1253+ False ,
1254+ "--clear-checkpoints" ,
1255+ help = "Delete all saved checkpoints before running (force fresh start)." ,
1256+ )
1257+ # Module-level singleton so the variadic argument default doesn't trip ruff B008
1258+ # (function call in argument default); same reason as the option singletons above.
1259+ _TICKERS_ARG = typer .Argument (..., help = "One or more tickers, e.g. AAPL MSFT 0700.HK" )
1260+
1261+
1262+ def _run_analyze (checkpoint : bool | None , clear_checkpoints : bool ) -> None :
12581263 if clear_checkpoints :
12591264 from tradingagents .graph .checkpointer import clear_all_checkpoints
12601265 n = clear_all_checkpoints (DEFAULT_CONFIG ["data_cache_dir" ])
12611266 console .print (f"[yellow]Cleared { n } checkpoint(s).[/yellow]" )
12621267 run_analysis (checkpoint = checkpoint )
12631268
12641269
1270+ @app .callback (invoke_without_command = True )
1271+ def main (
1272+ ctx : typer .Context ,
1273+ checkpoint : bool | None = _CHECKPOINT_OPT ,
1274+ clear_checkpoints : bool = _CLEAR_CHECKPOINTS_OPT ,
1275+ ):
1276+ """TradingAgents CLI. Run with no command to start the interactive analysis."""
1277+ # Only run the interactive flow for bare `tradingagents`; named subcommands
1278+ # (analyze, evaluate) handle themselves.
1279+ if ctx .invoked_subcommand is None :
1280+ _run_analyze (checkpoint , clear_checkpoints )
1281+
1282+
1283+ @app .command ()
1284+ def analyze (
1285+ checkpoint : bool | None = _CHECKPOINT_OPT ,
1286+ clear_checkpoints : bool = _CLEAR_CHECKPOINTS_OPT ,
1287+ ):
1288+ """Interactive analysis (same as running `tradingagents` with no command)."""
1289+ _run_analyze (checkpoint , clear_checkpoints )
1290+
1291+
1292+ @app .command ()
1293+ def evaluate (
1294+ tickers : list [str ] = _TICKERS_ARG ,
1295+ from_date : str = typer .Option (..., "--from" , help = "Backtest start date, YYYY-MM-DD." ),
1296+ to_date : str = typer .Option (..., "--to" , help = "Backtest end date, YYYY-MM-DD." ),
1297+ cadence_days : int = typer .Option (
1298+ 21 , "--cadence-days" ,
1299+ help = "Business days between sampled trade dates (~21 = monthly)." ,
1300+ ),
1301+ holding_days : int = typer .Option (
1302+ 5 , "--holding-days" ,
1303+ help = "Forward window (trading days) for the realized return/alpha outcome." ,
1304+ ),
1305+ analysts : str = typer .Option (
1306+ "market,news,fundamentals" , "--analysts" ,
1307+ help = "Comma-separated analysts. Default excludes 'social' to avoid "
1308+ "look-ahead leakage from StockTwits/Reddit." ,
1309+ ),
1310+ include_social : bool = typer .Option (
1311+ False , "--include-social" ,
1312+ help = "Add the social/sentiment analyst (WARNING: leaks 'now' social data "
1313+ "into historical dates)." ,
1314+ ),
1315+ out : str | None = typer .Option (
1316+ None , "--out" ,
1317+ help = "Output directory (default ~/.tradingagents/backtests/<stamp>/)." ,
1318+ ),
1319+ yes : bool = typer .Option (False , "--yes" , "-y" , help = "Skip the cost-confirmation prompt." ),
1320+ ):
1321+ """Backtest the analysis: score ratings vs forward alpha over a date range."""
1322+ from datetime import datetime
1323+ from pathlib import Path
1324+
1325+ from tradingagents .backtest import (
1326+ render_report ,
1327+ run_backtest ,
1328+ sample_dates ,
1329+ summarize ,
1330+ )
1331+
1332+ selected = [a .strip ().lower () for a in analysts .split ("," ) if a .strip ()]
1333+ if include_social and "social" not in selected :
1334+ selected .append ("social" )
1335+ if "social" in selected :
1336+ console .print (
1337+ "[yellow]WARNING:[/yellow] the social analyst reads 'now' (StockTwits/"
1338+ "Reddit), which leaks future information into historical backtest dates."
1339+ )
1340+
1341+ dates = sample_dates (from_date , to_date , cadence_days )
1342+ if not dates :
1343+ console .print ("[red]No business days in the given range.[/red]" )
1344+ raise typer .Exit (1 )
1345+ n_points = len (tickers ) * len (dates )
1346+
1347+ if out :
1348+ out_dir = Path (out )
1349+ else :
1350+ stamp = datetime .now ().strftime ("%Y%m%d_%H%M%S" )
1351+ out_dir = Path .home () / ".tradingagents" / "backtests" / stamp
1352+ results_path = out_dir / "results.jsonl"
1353+
1354+ console .print (
1355+ f"[bold]{ n_points } [/bold] evaluation points "
1356+ f"({ len (tickers )} ticker(s) × { len (dates )} dates) — analysts: { ', ' .join (selected )} ."
1357+ )
1358+ console .print (
1359+ "[yellow]Each point is a full multi-agent LLM run (minutes and $ each).[/yellow]"
1360+ )
1361+ if not yes and not typer .confirm ("Proceed?" , default = False ):
1362+ raise typer .Abort ()
1363+
1364+ rows = run_backtest (
1365+ tickers , from_date , to_date ,
1366+ cadence_days = cadence_days ,
1367+ holding_days = holding_days ,
1368+ selected_analysts = selected ,
1369+ config = DEFAULT_CONFIG .copy (),
1370+ results_path = results_path ,
1371+ )
1372+
1373+ report = render_report (
1374+ summarize (rows ),
1375+ meta = {
1376+ "tickers" : ", " .join (tickers ),
1377+ "range" : f"{ from_date } -> { to_date } " ,
1378+ "cadence_days" : cadence_days ,
1379+ "holding_days" : holding_days ,
1380+ "analysts" : ", " .join (selected ),
1381+ },
1382+ )
1383+ out_dir .mkdir (parents = True , exist_ok = True )
1384+ (out_dir / "report.md" ).write_text (report , encoding = "utf-8" )
1385+ console .print ()
1386+ console .print (report )
1387+ console .print (f"\n [green]Saved:[/green] { results_path } | { out_dir / 'report.md' } " )
1388+
1389+
12651390if __name__ == "__main__" :
12661391 app ()
0 commit comments