Team Ai
Modelpublic

ParallelLLC/algorithmic_trading

sourceHugging Faceapache-2.0updated 2mo agoView on Hugging Face
27likes32downloads
cli.py291 linesDownload Raw Back to algotrader
1"""Command-line interface.2 3    python -m algotrader.cli lab --symbol SPY --strategy sma_cross4    python -m algotrader.cli arena --symbol BTC-USD --start 2018-01-015    python -m algotrader.cli strategies6"""7 8from __future__ import annotations9 10import argparse11import json12import sys13from typing import Dict, List14 15from . import __version__16from .lab import LabConfig, run_arena, run_lab17from .strategies import REGISTRY, get_strategy18 19 20def _parse_params(pairs: List[str] | None) -> Dict[str, float]:21    params: Dict[str, float] = {}22    for pair in pairs or []:23        if "=" not in pair:24            raise SystemExit(f"--param expects name=value, got '{pair}'")25        name, _, value = pair.partition("=")26        params[name.strip()] = float(value)27    return params28 29 30def _add_common(parser: argparse.ArgumentParser) -> None:31    parser.add_argument("--symbol", default="SPY")32    parser.add_argument("--start", default="2015-01-01")33    parser.add_argument("--end", default=None)34    parser.add_argument("--interval", default="1d")35    parser.add_argument(36        "--source", default="yahoo", choices=["yahoo", "live", "auto", "cache", "synthetic"],37        help="'yahoo' requires a Yahoo download. 'synthetic' is offline tests only. 'auto' falls back to the simulator.",38    )39    parser.add_argument("--commission-bps", type=float, default=1.0)40    parser.add_argument("--slippage-bps", type=float, default=2.0)41    parser.add_argument("--no-short", action="store_true", help="Long/flat only.")42 43 44def _config_from(args: argparse.Namespace, **overrides) -> LabConfig:45    return LabConfig(46        symbol=args.symbol,47        start=args.start,48        end=args.end,49        interval=args.interval,50        source=args.source,51        commission_bps=args.commission_bps,52        slippage_bps=args.slippage_bps,53        allow_short=not args.no_short,54        **overrides,55    )56 57 58def _cmd_lab(args: argparse.Namespace) -> int:59    cfg = _config_from(60        args,61        strategy=args.strategy,62        params=_parse_params(args.param),63        n_permutations=args.permutations,64        permutation_method=args.null,65        wf_folds=args.folds,66    )67    progress = None if args.quiet else (lambda f, m: print(f"  [{f:5.0%}] {m}", file=sys.stderr))68    report = run_lab(cfg, progress=progress)69 70    if args.json:71        payload = {72            "symbol": report.market.symbol,73            "source": report.market.source,74            "strategy": report.strategy.key,75            "params": report.params,76            "metrics": report.backtest.metrics,77            "benchmark_metrics": report.backtest.benchmark_metrics,78            "p_value": report.permutation.p_value if report.permutation else None,79            "deflated_sharpe": report.dsr.get("dsr"),80            "pbo": report.pbo.get("pbo"),81            "walkforward_efficiency": report.walkforward.get("efficiency"),82            "cost_stress": report.cost_stress,83            "verdict": {k: v for k, v in report.verdict.items()},84        }85        print(json.dumps(payload, indent=2, default=str))86        return 087 88    v, m, b = report.verdict, report.backtest.metrics, report.backtest.benchmark_metrics89    bar = "=" * 6690    print(f"\n{bar}")91    print(f"  {report.strategy.name} on {report.market.symbol}   [{report.market.source} data]")92    print(f"  {report.market.start.date()} to {report.market.end.date()}  ·  {len(report.market.df):,} bars")93    print(bar)94    print(f"  REALITY SCORE   {v['score']:.1f} / 100      GRADE  {v['grade']}")95    print(f"  {v['headline']}")96    print(bar)97    print(f"  Total return      {m['total_return']:>9.1%}    buy & hold {b['total_return']:>8.1%}")98    print(f"  CAGR              {m['cagr']:>9.1%}    buy & hold {b['cagr']:>8.1%}")99    print(f"  Sharpe            {m['sharpe']:>9.2f}    buy & hold {b['sharpe']:>8.2f}")100    print(f"  Max drawdown      {m['max_drawdown']:>9.1%}")101    print(f"  Trades            {int(m.get('n_trades', 0)):>9,}")102    print(bar)103    if report.permutation:104        print(f"  Permutation p     {report.permutation.p_value:>9.3f}    ({report.permutation.n_permutations} shuffled markets)")105    print(f"  Deflated Sharpe   {report.dsr.get('dsr', 0):>9.2f}    (after {report.trials.get('n', 1)} variants)")106    pbo = report.pbo.get("pbo")107    print(f"  Overfit prob.     {pbo:>9.2f}" if pbo == pbo else "  Overfit prob.           n/a")108    print(f"  Walk-forward eff. {report.walkforward.get('efficiency', 0):>9.2f}")109    print(f"  Sharpe at 3x cost {report.cost_stress.get('sharpe_3x', 0):>9.2f}")110    print(bar)111    for flag in v["flags"]:112        print(f"  ! {flag}")113    if v["flags"]:114        print(bar)115    print(f"  {v['verdict']}\n")116    return 0117 118 119def _cmd_arena(args: argparse.Namespace) -> int:120    cfg = _config_from(args)121    progress = None if args.quiet else (lambda f, m: print(f"  [{f:5.0%}] {m}", file=sys.stderr))122    table, market, _ = run_arena(cfg, n_permutations=args.permutations, progress=progress)123 124    if args.json:125        print(table.to_json(orient="records", indent=2))126        return 0127 128    print(f"\n  {market.symbol}  [{market.source} data]  "129          f"{market.start.date()} to {market.end.date()}\n")130    display = table.drop(columns=["key"]).copy()131    for col in ("Return", "CAGR", "MaxDD"):132        display[col] = display[col].map("{:.1%}".format)133    for col in ("Sharpe", "DSR", "Evidence"):134        display[col] = display[col].map("{:.2f}".format)135    display["p-value"] = display["p-value"].map(lambda v: "—" if v != v else f"{v:.3f}")136    print(display.to_string(index=False))137    print("\n  Ranked by evidence = (1 - p) x deflated Sharpe, not by return.\n")138    return 0139 140 141def _cmd_portfolio(args: argparse.Namespace) -> int:142    from .portfolio_lab import DEFAULT_UNIVERSE, PortfolioLabConfig, run_portfolio_lab143 144    symbols = [s.strip() for s in args.symbols.split(",") if s.strip()] if args.symbols else DEFAULT_UNIVERSE145    cfg = PortfolioLabConfig(146        symbols=symbols,147        start=args.start,148        end=args.end,149        interval=args.interval,150        source=args.source,151        strategy=args.strategy,152        params=_parse_params(args.param),153        commission_bps=args.commission_bps,154        slippage_bps=args.slippage_bps,155        allow_short=not args.no_short,156        rebalance=args.rebalance,157        n_permutations=args.permutations,158        wf_folds=args.folds,159    )160    progress = None if args.quiet else (lambda f, m: print(f"  [{f:5.0%}] {m}", file=sys.stderr))161    report = run_portfolio_lab(cfg, progress=progress)162 163    if args.json:164        print(json.dumps({165            "symbols": report.panel.symbols,166            "strategy": report.strategy.key,167            "params": report.params,168            "metrics": report.backtest.metrics,169            "p_value": report.permutation.p_value if report.permutation else None,170            "deflated_sharpe": report.dsr.get("dsr"),171            "pbo": report.pbo.get("pbo"),172            "walkforward_efficiency": report.walkforward.get("efficiency"),173            "attribution": report.attribution,174            "survivorship": report.survivorship.__dict__,175            "verdict": dict(report.verdict),176        }, indent=2, default=str))177        return 0178 179    v, m, b = report.verdict, report.backtest.metrics, report.backtest.benchmark_metrics180    bar = "=" * 72181    print(f"\n{bar}")182    print(f"  {report.strategy.name} on {len(report.panel.symbols)} symbols   [{report.panel.interval}]")183    print(f"  {report.panel.index[0].date()} to {report.panel.index[-1].date()}  ·  "184          f"{len(report.panel):,} bars  ·  rebalance {report.config.rebalance}")185    print(bar)186    print(f"  REALITY SCORE   {v['score']:.1f} / 100      GRADE  {v['grade']}")187    print(f"  {v['headline']}")188    print(bar)189    print(f"  Total return      {m['total_return']:>9.1%}    equal weight {b['total_return']:>8.1%}")190    print(f"  CAGR              {m['cagr']:>9.1%}    equal weight {b['cagr']:>8.1%}")191    print(f"  Sharpe            {m['sharpe']:>9.2f}    equal weight {b['sharpe']:>8.2f}")192    print(f"  Max drawdown      {m['max_drawdown']:>9.1%}")193    print(f"  Gross / net exp.  {m.get('gross_exposure', 0):>9.2f} / {m.get('net_exposure', 0):.2f}")194    print(f"  Avg positions     {m.get('avg_positions', 0):>9.1f}    turnover {m.get('turnover_ann', 0):.1f}x/yr")195    print(bar)196    if report.permutation:197        print(f"  Name-shuffle p    {report.permutation.p_value:>9.3f}    "198              f"({report.permutation.n_permutations} shuffles of which names got which weights)")199    print(f"  Deflated Sharpe   {report.dsr.get('dsr', 0):>9.2f}    (after {report.trials.get('n', 1)} variants)")200    pbo = report.pbo.get("pbo")201    print(f"  Overfit prob.     {pbo:>9.2f}" if pbo == pbo else "  Overfit prob.           n/a")202    print(f"  Walk-forward eff. {report.walkforward.get('efficiency', 0):>9.2f}")203    if report.attribution.get("available"):204        print(f"  Style alpha       {report.attribution['alpha_annual']:>9.1%}    "205              f"t = {report.attribution['alpha_t_stat']:.2f}, R² = {report.attribution['r_squared']:.2f}")206    print(f"  Survivorship      {report.survivorship.survival_rate:>9.0%}    "207          f"({report.survivorship.n_delisted} of {report.survivorship.n_symbols} stopped trading)")208    print(bar)209    for flag in v["flags"]:210        print(f"  ! {flag}")211    if v["flags"]:212        print(bar)213    print(f"  {v['verdict']}\n")214    return 0215 216 217def _cmd_strategies(args: argparse.Namespace) -> int:218    from .cross_sectional import XS_REGISTRY219 220    for title, registry in (("Single asset", REGISTRY), ("Cross-sectional", XS_REGISTRY)):221        print(f"\n  {title}\n  {'-' * len(title)}")222        for key, strategy in registry.items():223            params = ", ".join(f"{p.name}={p.default:g}" for p in strategy.params) or "no parameters"224            print(f"  {key:<22} {strategy.name:<28} [{strategy.family}]")225            print(f"  {'':<22} {strategy.description}")226            print(f"  {'':<22} defaults: {params}\n")227    return 0228 229 230def main(argv: List[str] | None = None) -> int:231    parser = argparse.ArgumentParser(232        prog="algotrader",233        description="Backtest a trading rule, then try to prove the result was luck.",234    )235    parser.add_argument("--version", action="version", version=f"algotrader {__version__}")236    sub = parser.add_subparsers(dest="command", required=True)237 238    lab = sub.add_parser("lab", help="Full reality check for one strategy.")239    _add_common(lab)240    lab.add_argument("--strategy", default="sma_cross", choices=sorted(REGISTRY))241    lab.add_argument("--param", action="append", metavar="NAME=VALUE",242                     help="Override a strategy parameter. Repeatable.")243    lab.add_argument("--permutations", type=int, default=250)244    lab.add_argument("--null", default="permute", choices=["permute", "block"])245    lab.add_argument("--folds", type=int, default=5)246    lab.add_argument("--json", action="store_true")247    lab.add_argument("--quiet", "-q", action="store_true")248    lab.set_defaults(func=_cmd_lab)249 250    arena = sub.add_parser("arena", help="Race every strategy on one market.")251    _add_common(arena)252    arena.add_argument("--permutations", type=int, default=120)253    arena.add_argument("--json", action="store_true")254    arena.add_argument("--quiet", "-q", action="store_true")255    arena.set_defaults(func=_cmd_arena)256 257    from .cross_sectional import XS_REGISTRY258 259    portfolio = sub.add_parser(260        "portfolio", help="Reality check for a cross-sectional (multi-asset) strategy."261    )262    _add_common(portfolio)263    portfolio.add_argument(264        "--symbols", default=None,265        help="Comma-separated universe, e.g. SPY,QQQ,AAPL. Defaults to a 12-name universe.",266    )267    portfolio.add_argument("--strategy", default="xs_momentum", choices=sorted(XS_REGISTRY))268    portfolio.add_argument("--param", action="append", metavar="NAME=VALUE")269    portfolio.add_argument("--rebalance", default="M", help="D, W, M, Q, or a number of bars.")270    portfolio.add_argument("--permutations", type=int, default=150)271    portfolio.add_argument("--folds", type=int, default=4)272    portfolio.add_argument("--json", action="store_true")273    portfolio.add_argument("--quiet", "-q", action="store_true")274    portfolio.set_defaults(func=_cmd_portfolio)275 276    listing = sub.add_parser("strategies", help="List the strategy zoo.")277    listing.set_defaults(func=_cmd_strategies)278 279    args = parser.parse_args(argv)280    try:281        return args.func(args)282    except KeyboardInterrupt:283        return 130284    except Exception as exc:  # noqa: BLE001285        print(f"error: {exc}", file=sys.stderr)286        return 1287 288 289if __name__ == "__main__":290    raise SystemExit(main())291