Download algotrader/cli.py from ParallelLLC/algorithmic_trading: direct link, hf CLI and curl.
- Browser
- Download file 12.7 kB
-
https://huggingface.co/ParallelLLC/algorithmic_trading/resolve/main/algotrader/cli.py
- Command line
-
hf download hf://ParallelLLC/algorithmic_trading/algotrader/cli.py
-
curl -L -o cli.py https://huggingface.co/ParallelLLC/algorithmic_trading/resolve/main/algotrader/cli.py
12.7 kB
| """Command-line interface. | |
| python -m algotrader.cli lab --symbol SPY --strategy sma_cross | |
| python -m algotrader.cli arena --symbol BTC-USD --start 2018-01-01 | |
| python -m algotrader.cli strategies | |
| """ | |
| from __future__ import annotations | |
| import argparse | |
| import json | |
| import sys | |
| from typing import Dict, List | |
| from . import __version__ | |
| from .lab import LabConfig, run_arena, run_lab | |
| from .strategies import REGISTRY, get_strategy | |
| def _parse_params(pairs: List[str] | None) -> Dict[str, float]: | |
| params: Dict[str, float] = {} | |
| for pair in pairs or []: | |
| if "=" not in pair: | |
| raise SystemExit(f"--param expects name=value, got '{pair}'") | |
| name, _, value = pair.partition("=") | |
| params[name.strip()] = float(value) | |
| return params | |
| def _add_common(parser: argparse.ArgumentParser) -> None: | |
| parser.add_argument("--symbol", default="SPY") | |
| parser.add_argument("--start", default="2015-01-01") | |
| parser.add_argument("--end", default=None) | |
| parser.add_argument("--interval", default="1d") | |
| parser.add_argument( | |
| "--source", default="yahoo", choices=["yahoo", "live", "auto", "cache", "synthetic"], | |
| help="'yahoo' requires a Yahoo download. 'synthetic' is offline tests only. 'auto' falls back to the simulator.", | |
| ) | |
| parser.add_argument("--commission-bps", type=float, default=1.0) | |
| parser.add_argument("--slippage-bps", type=float, default=2.0) | |
| parser.add_argument("--no-short", action="store_true", help="Long/flat only.") | |
| def _config_from(args: argparse.Namespace, **overrides) -> LabConfig: | |
| return LabConfig( | |
| symbol=args.symbol, | |
| start=args.start, | |
| end=args.end, | |
| interval=args.interval, | |
| source=args.source, | |
| commission_bps=args.commission_bps, | |
| slippage_bps=args.slippage_bps, | |
| allow_short=not args.no_short, | |
| **overrides, | |
| ) | |
| def _cmd_lab(args: argparse.Namespace) -> int: | |
| cfg = _config_from( | |
| args, | |
| strategy=args.strategy, | |
| params=_parse_params(args.param), | |
| n_permutations=args.permutations, | |
| permutation_method=args.null, | |
| wf_folds=args.folds, | |
| ) | |
| progress = None if args.quiet else (lambda f, m: print(f" [{f:5.0%}] {m}", file=sys.stderr)) | |
| report = run_lab(cfg, progress=progress) | |
| if args.json: | |
| payload = { | |
| "symbol": report.market.symbol, | |
| "source": report.market.source, | |
| "strategy": report.strategy.key, | |
| "params": report.params, | |
| "metrics": report.backtest.metrics, | |
| "benchmark_metrics": report.backtest.benchmark_metrics, | |
| "p_value": report.permutation.p_value if report.permutation else None, | |
| "deflated_sharpe": report.dsr.get("dsr"), | |
| "pbo": report.pbo.get("pbo"), | |
| "walkforward_efficiency": report.walkforward.get("efficiency"), | |
| "cost_stress": report.cost_stress, | |
| "verdict": {k: v for k, v in report.verdict.items()}, | |
| } | |
| print(json.dumps(payload, indent=2, default=str)) | |
| return 0 | |
| v, m, b = report.verdict, report.backtest.metrics, report.backtest.benchmark_metrics | |
| bar = "=" * 66 | |
| print(f"\n{bar}") | |
| print(f" {report.strategy.name} on {report.market.symbol} [{report.market.source} data]") | |
| print(f" {report.market.start.date()} to {report.market.end.date()} · {len(report.market.df):,} bars") | |
| print(bar) | |
| print(f" REALITY SCORE {v['score']:.1f} / 100 GRADE {v['grade']}") | |
| print(f" {v['headline']}") | |
| print(bar) | |
| print(f" Total return {m['total_return']:>9.1%} buy & hold {b['total_return']:>8.1%}") | |
| print(f" CAGR {m['cagr']:>9.1%} buy & hold {b['cagr']:>8.1%}") | |
| print(f" Sharpe {m['sharpe']:>9.2f} buy & hold {b['sharpe']:>8.2f}") | |
| print(f" Max drawdown {m['max_drawdown']:>9.1%}") | |
| print(f" Trades {int(m.get('n_trades', 0)):>9,}") | |
| print(bar) | |
| if report.permutation: | |
| print(f" Permutation p {report.permutation.p_value:>9.3f} ({report.permutation.n_permutations} shuffled markets)") | |
| print(f" Deflated Sharpe {report.dsr.get('dsr', 0):>9.2f} (after {report.trials.get('n', 1)} variants)") | |
| pbo = report.pbo.get("pbo") | |
| print(f" Overfit prob. {pbo:>9.2f}" if pbo == pbo else " Overfit prob. n/a") | |
| print(f" Walk-forward eff. {report.walkforward.get('efficiency', 0):>9.2f}") | |
| print(f" Sharpe at 3x cost {report.cost_stress.get('sharpe_3x', 0):>9.2f}") | |
| print(bar) | |
| for flag in v["flags"]: | |
| print(f" ! {flag}") | |
| if v["flags"]: | |
| print(bar) | |
| print(f" {v['verdict']}\n") | |
| return 0 | |
| def _cmd_arena(args: argparse.Namespace) -> int: | |
| cfg = _config_from(args) | |
| progress = None if args.quiet else (lambda f, m: print(f" [{f:5.0%}] {m}", file=sys.stderr)) | |
| table, market, _ = run_arena(cfg, n_permutations=args.permutations, progress=progress) | |
| if args.json: | |
| print(table.to_json(orient="records", indent=2)) | |
| return 0 | |
| print(f"\n {market.symbol} [{market.source} data] " | |
| f"{market.start.date()} to {market.end.date()}\n") | |
| display = table.drop(columns=["key"]).copy() | |
| for col in ("Return", "CAGR", "MaxDD"): | |
| display[col] = display[col].map("{:.1%}".format) | |
| for col in ("Sharpe", "DSR", "Evidence"): | |
| display[col] = display[col].map("{:.2f}".format) | |
| display["p-value"] = display["p-value"].map(lambda v: "—" if v != v else f"{v:.3f}") | |
| print(display.to_string(index=False)) | |
| print("\n Ranked by evidence = (1 - p) x deflated Sharpe, not by return.\n") | |
| return 0 | |
| def _cmd_portfolio(args: argparse.Namespace) -> int: | |
| from .portfolio_lab import DEFAULT_UNIVERSE, PortfolioLabConfig, run_portfolio_lab | |
| symbols = [s.strip() for s in args.symbols.split(",") if s.strip()] if args.symbols else DEFAULT_UNIVERSE | |
| cfg = PortfolioLabConfig( | |
| symbols=symbols, | |
| start=args.start, | |
| end=args.end, | |
| interval=args.interval, | |
| source=args.source, | |
| strategy=args.strategy, | |
| params=_parse_params(args.param), | |
| commission_bps=args.commission_bps, | |
| slippage_bps=args.slippage_bps, | |
| allow_short=not args.no_short, | |
| rebalance=args.rebalance, | |
| n_permutations=args.permutations, | |
| wf_folds=args.folds, | |
| ) | |
| progress = None if args.quiet else (lambda f, m: print(f" [{f:5.0%}] {m}", file=sys.stderr)) | |
| report = run_portfolio_lab(cfg, progress=progress) | |
| if args.json: | |
| print(json.dumps({ | |
| "symbols": report.panel.symbols, | |
| "strategy": report.strategy.key, | |
| "params": report.params, | |
| "metrics": report.backtest.metrics, | |
| "p_value": report.permutation.p_value if report.permutation else None, | |
| "deflated_sharpe": report.dsr.get("dsr"), | |
| "pbo": report.pbo.get("pbo"), | |
| "walkforward_efficiency": report.walkforward.get("efficiency"), | |
| "attribution": report.attribution, | |
| "survivorship": report.survivorship.__dict__, | |
| "verdict": dict(report.verdict), | |
| }, indent=2, default=str)) | |
| return 0 | |
| v, m, b = report.verdict, report.backtest.metrics, report.backtest.benchmark_metrics | |
| bar = "=" * 72 | |
| print(f"\n{bar}") | |
| print(f" {report.strategy.name} on {len(report.panel.symbols)} symbols [{report.panel.interval}]") | |
| print(f" {report.panel.index[0].date()} to {report.panel.index[-1].date()} · " | |
| f"{len(report.panel):,} bars · rebalance {report.config.rebalance}") | |
| print(bar) | |
| print(f" REALITY SCORE {v['score']:.1f} / 100 GRADE {v['grade']}") | |
| print(f" {v['headline']}") | |
| print(bar) | |
| print(f" Total return {m['total_return']:>9.1%} equal weight {b['total_return']:>8.1%}") | |
| print(f" CAGR {m['cagr']:>9.1%} equal weight {b['cagr']:>8.1%}") | |
| print(f" Sharpe {m['sharpe']:>9.2f} equal weight {b['sharpe']:>8.2f}") | |
| print(f" Max drawdown {m['max_drawdown']:>9.1%}") | |
| print(f" Gross / net exp. {m.get('gross_exposure', 0):>9.2f} / {m.get('net_exposure', 0):.2f}") | |
| print(f" Avg positions {m.get('avg_positions', 0):>9.1f} turnover {m.get('turnover_ann', 0):.1f}x/yr") | |
| print(bar) | |
| if report.permutation: | |
| print(f" Name-shuffle p {report.permutation.p_value:>9.3f} " | |
| f"({report.permutation.n_permutations} shuffles of which names got which weights)") | |
| print(f" Deflated Sharpe {report.dsr.get('dsr', 0):>9.2f} (after {report.trials.get('n', 1)} variants)") | |
| pbo = report.pbo.get("pbo") | |
| print(f" Overfit prob. {pbo:>9.2f}" if pbo == pbo else " Overfit prob. n/a") | |
| print(f" Walk-forward eff. {report.walkforward.get('efficiency', 0):>9.2f}") | |
| if report.attribution.get("available"): | |
| print(f" Style alpha {report.attribution['alpha_annual']:>9.1%} " | |
| f"t = {report.attribution['alpha_t_stat']:.2f}, R² = {report.attribution['r_squared']:.2f}") | |
| print(f" Survivorship {report.survivorship.survival_rate:>9.0%} " | |
| f"({report.survivorship.n_delisted} of {report.survivorship.n_symbols} stopped trading)") | |
| print(bar) | |
| for flag in v["flags"]: | |
| print(f" ! {flag}") | |
| if v["flags"]: | |
| print(bar) | |
| print(f" {v['verdict']}\n") | |
| return 0 | |
| def _cmd_strategies(args: argparse.Namespace) -> int: | |
| from .cross_sectional import XS_REGISTRY | |
| for title, registry in (("Single asset", REGISTRY), ("Cross-sectional", XS_REGISTRY)): | |
| print(f"\n {title}\n {'-' * len(title)}") | |
| for key, strategy in registry.items(): | |
| params = ", ".join(f"{p.name}={p.default:g}" for p in strategy.params) or "no parameters" | |
| print(f" {key:<22} {strategy.name:<28} [{strategy.family}]") | |
| print(f" {'':<22} {strategy.description}") | |
| print(f" {'':<22} defaults: {params}\n") | |
| return 0 | |
| def main(argv: List[str] | None = None) -> int: | |
| parser = argparse.ArgumentParser( | |
| prog="algotrader", | |
| description="Backtest a trading rule, then try to prove the result was luck.", | |
| ) | |
| parser.add_argument("--version", action="version", version=f"algotrader {__version__}") | |
| sub = parser.add_subparsers(dest="command", required=True) | |
| lab = sub.add_parser("lab", help="Full reality check for one strategy.") | |
| _add_common(lab) | |
| lab.add_argument("--strategy", default="sma_cross", choices=sorted(REGISTRY)) | |
| lab.add_argument("--param", action="append", metavar="NAME=VALUE", | |
| help="Override a strategy parameter. Repeatable.") | |
| lab.add_argument("--permutations", type=int, default=250) | |
| lab.add_argument("--null", default="permute", choices=["permute", "block"]) | |
| lab.add_argument("--folds", type=int, default=5) | |
| lab.add_argument("--json", action="store_true") | |
| lab.add_argument("--quiet", "-q", action="store_true") | |
| lab.set_defaults(func=_cmd_lab) | |
| arena = sub.add_parser("arena", help="Race every strategy on one market.") | |
| _add_common(arena) | |
| arena.add_argument("--permutations", type=int, default=120) | |
| arena.add_argument("--json", action="store_true") | |
| arena.add_argument("--quiet", "-q", action="store_true") | |
| arena.set_defaults(func=_cmd_arena) | |
| from .cross_sectional import XS_REGISTRY | |
| portfolio = sub.add_parser( | |
| "portfolio", help="Reality check for a cross-sectional (multi-asset) strategy." | |
| ) | |
| _add_common(portfolio) | |
| portfolio.add_argument( | |
| "--symbols", default=None, | |
| help="Comma-separated universe, e.g. SPY,QQQ,AAPL. Defaults to a 12-name universe.", | |
| ) | |
| portfolio.add_argument("--strategy", default="xs_momentum", choices=sorted(XS_REGISTRY)) | |
| portfolio.add_argument("--param", action="append", metavar="NAME=VALUE") | |
| portfolio.add_argument("--rebalance", default="M", help="D, W, M, Q, or a number of bars.") | |
| portfolio.add_argument("--permutations", type=int, default=150) | |
| portfolio.add_argument("--folds", type=int, default=4) | |
| portfolio.add_argument("--json", action="store_true") | |
| portfolio.add_argument("--quiet", "-q", action="store_true") | |
| portfolio.set_defaults(func=_cmd_portfolio) | |
| listing = sub.add_parser("strategies", help="List the strategy zoo.") | |
| listing.set_defaults(func=_cmd_strategies) | |
| args = parser.parse_args(argv) | |
| try: | |
| return args.func(args) | |
| except KeyboardInterrupt: | |
| return 130 | |
| except Exception as exc: # noqa: BLE001 | |
| print(f"error: {exc}", file=sys.stderr) | |
| return 1 | |
| if __name__ == "__main__": | |
| raise SystemExit(main()) | |