Skip to content

Commit dc22878

Browse files
authored
Add evaluation/backtest harness + decision confidence
Add evaluation/backtest harness + decision confidence
2 parents 520df20 + 38a3d43 commit dc22878

7 files changed

Lines changed: 817 additions & 15 deletions

File tree

cli/main.py

Lines changed: 139 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
12651390
if __name__ == "__main__":
12661391
app()

0 commit comments

Comments
 (0)