Files
easy_tdx_max/src/easy_tdx/backtest/cli.py
T
Justin Gu e374a0da28 release: v1.32.6 — 两周改动深度审查全面修复(回测口径三件套/LLM 安全加固/涨停价舍入/时区统一/缓存与竞态等 58 处)
对 v1.21→v1.32.5 的 249 文件 4.2 万行改动做六路专项审查,本轮落地全部发现:

回测正确性:组合收益 fillna(0) 虚增、轮动停牌日过期价成交、单标的 WF 逐窗指标
被预热区稀释(三件套均带先红后绿回归);worst_drawdown 方向、grading 容错、
组合体检品种费率、寻优端点费率透传。

安全:LLM api_url 仅 http/https 且禁 userinfo(封死 file:// 读取与 Key 外送链)、
错误响应不回显原始 body、响应体 2MB 上限、配置原子写、坏配置字段级防御。

数据:涨跌停价整数分币舍入(67/318/90 个价位错 1 分漏判清零)、交易时段/采样/
provisional 统一沪时区、warehouse 增量缺口自动全量重拉、provisional 定点转正、
baostock 真故障抛错 + W/M 去 tradestatus(实测服务端报错,周月兜底此前从未工作)
+ 指数 vol 股→手(实测锚定)、ccpm 结构变更抛错。

Web API:缓存键补 count/vipdoc、NaN 清洗先于缓存、count>800 分页取全量、
submit 透传真实状态、pending 不再被淘汰成幽灵、watchlist/server 入参约束。

公式:FILTER 去副作用、0-1 值域误判收严、递归深度上限、REF 负移位显式禁止。

前端:4 处请求竞态序号守卫、Sparkline viewBox、北交所 market=2 映射、
空数据缓存死角、AI 弹窗卸载中止轮询、量能/资金日历口径修正。

CLI/CI:warehouse sync 失败 exit 1、参数校验干净报错、release 真实发布 SHA256、
CI 超时与缓存、spec 补 baostock 前提。

约 60 条回归测试先红后绿;pytest 1820 全过,ruff/mypy/vue-tsc/node --test 全绿。
2026-09-06 22:16:48 +08:00

879 lines
30 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""回测 CLI 命令。"""
from __future__ import annotations
import importlib.util
from pathlib import Path
from typing import Any
import click
@click.command()
@click.argument("market")
@click.argument("code")
@click.option("--strategy", "strategy_str", default=None, help="DSL 策略表达式 (P1)")
@click.option("--strategy-file", "strategy_file", default=None, help="Python 策略文件路径")
@click.option(
"--combo-strategies",
"combo_strategies",
default=None,
help="多因子组合:逗号分隔的策略文件路径(如 strats/a.py,strats/b.py,strats/c.py",
)
@click.option(
"--combo-mode",
"combo_mode",
default="MAJORITY",
type=click.Choice(["AND", "OR", "MAJORITY"], case_sensitive=False),
help="多因子信号合并模式(默认 MAJORITY",
)
@click.option("--cash", default=100000.0, type=float, help="初始资金")
@click.option("--commission", default=0.0003, type=float, help="佣金率")
@click.option(
"--auto-fees",
"auto_fees",
is_flag=True,
help="按标的品种自动解析费率(ETF/可转债免印花税等;显式 --commission 优先)",
)
@click.option(
"--execution",
default="next_open",
type=click.Choice(["next_open", "next_close"]),
help="成交价规则",
)
@click.option("--period", default="DAILY", help="K线周期")
@click.option("--adjust", default="NONE", help="复权: NONE/QFQ/HFQ")
@click.option("--count", default=500, type=int, help="K线数量")
@click.option("--indicators", default=None, help="预计算指标(逗号分隔)")
@click.option(
"--chanlun-level",
"chanlun_level",
default=None,
help="自动计算缠论分析并注入策略(如 DAILY/30MIN",
)
@click.option("--table", "use_table", is_flag=True, help="表格输出")
@click.option("--output", "output_fmt", type=click.Choice(["json", "table", "csv"]), default="json")
@click.option(
"--wf",
"walk_forward",
is_flag=True,
help="附加 Walk-Forward 样本外验证(默认 7 窗,每窗独立开仓)",
)
@click.option("--wf-windows", "wf_windows", default=7, type=int, help="Walk-Forward 窗口数")
@click.option(
"--evaluate",
"full_evaluate",
is_flag=True,
help="一条龙评估:回测+WF+适配性+综合评分+S-D评级+买入持有基准对比(覆盖常规输出)",
)
def backtest(
market: str,
code: str,
strategy_str: str | None,
strategy_file: str | None,
combo_strategies: str | None,
combo_mode: str,
cash: float,
commission: float,
auto_fees: bool,
execution: str,
period: str,
adjust: str,
count: int,
indicators: str | None,
chanlun_level: str | None,
use_table: bool,
output_fmt: str,
walk_forward: bool,
wf_windows: int,
full_evaluate: bool,
) -> None:
"""回测引擎:执行策略并返回绩效报告。
示例:
easy-tdx backtest SZ 000001 --strategy-file my_strategy.py
easy-tdx backtest SH 600519 --strategy-file ma_cross.py --table
easy-tdx backtest SZ 000001 --strategy-file my_strategy.py --indicators MACD,KDJ
easy-tdx backtest SZ 000001 --strategy-file chanlun_strategy.py --chanlun-level DAILY
easy-tdx backtest SZ 300308 --strategy-file ma_cross.py --wf --wf-windows 7
easy-tdx backtest SZ 300308 --strategy-file ma_cross.py --evaluate
easy-tdx backtest SZ 000001 \
--combo-strategies strategies/macd_cross.py,strategies/rsi_reversal.py \
--combo-mode MAJORITY --table
"""
from ..backtest.engine import BacktestEngine
from ..cli.conn import get_mac_client
from ..cli.parsers import parse_adjust, parse_market, parse_period
from ..indicator import compute_indicators
# 1. 加载策略(单策略 or 多因子组合)
is_combo = combo_strategies is not None
# 组合模式暂不支持的分析旗标:显式告警而非静默吞掉(审查修复)
if is_combo and walk_forward:
click.echo(
"警告: --wfWalk-Forward 样本外验证)暂不支持 --combo-strategies 组合模式,已忽略",
err=True,
)
if is_combo and full_evaluate:
click.echo(
"警告: --evaluate(一条龙评估)暂不支持 --combo-strategies 组合模式,已忽略", err=True
)
if is_combo:
assert combo_strategies is not None # narrowed by is_combo
combo_classes = _load_combo_strategies(combo_strategies)
else:
strategy_cls = _load_strategy(strategy_str, strategy_file)
if strategy_cls is None:
click.echo("错误: 必须指定 --strategy-file / --combo-strategies / --strategy", err=True)
raise SystemExit(1)
# 2. 获取数据
mkt = parse_market(market)
with get_mac_client() as client:
df = client.get_stock_kline(
mkt,
code,
period=parse_period(period),
start=0,
count=count,
adjust=parse_adjust(adjust),
)
# 3. 预计算指标
if indicators:
indicator_list = [ind.strip() for ind in indicators.split(",")]
df = compute_indicators(df, indicator_list)
# 4. 创建引擎并运行
if is_combo:
from ..backtest.combo import CombinationRunner
runner = CombinationRunner(
strategy_classes=combo_classes,
df=df,
cash=cash,
commission=commission,
execution=execution,
)
result = runner.run_combination(
indices=list(range(len(combo_classes))),
mode=combo_mode.upper(),
)
else:
assert strategy_cls is not None # guarded above by SystemExit
# 一条龙评估:覆盖常规输出(含回测本身,无需重复跑)
if full_evaluate:
import json as _json
from ..backtest.benchmark import evaluate_strategy
report = evaluate_strategy(
strategy=strategy_cls,
df=df,
cash=cash,
commission=commission,
execution=execution,
symbol=f"{market}:{code}",
auto_fees=auto_fees,
n_windows=wf_windows,
)
click.echo(_json.dumps(report, ensure_ascii=False, default=str))
return
engine = BacktestEngine(
strategy=strategy_cls,
cash=cash,
commission=commission,
execution=execution,
chanlun_level=chanlun_level,
symbol=f"{market}:{code}",
auto_fees=auto_fees,
)
result = engine.run(df)
# 5. 输出结果
fmt = "table" if use_table else output_fmt
if fmt == "json":
click.echo(result.to_json())
elif fmt == "table":
_print_table(result)
else:
click.echo(result.to_json())
# 6. 附加 Walk-Forward 样本外验证(--wf
if walk_forward and not is_combo:
import json as _json
from ..backtest.walkforward import WalkForwardEngine
assert strategy_cls is not None
wf = WalkForwardEngine(
strategy=strategy_cls,
n_windows=wf_windows,
cash=cash,
commission=commission,
execution=execution,
symbol=f"{market}:{code}",
auto_fees=auto_fees,
)
wf_report = {"walkforward": wf.run(df).to_dict()}
click.echo(_json.dumps(wf_report, ensure_ascii=False, default=str))
def _load_strategy(strategy_str: str | None, strategy_file: str | None) -> type | None:
"""加载策略类。
优先从 Python 文件加载,其次从 DSL 表达式加载(未实现)。
Args:
strategy_str: DSL 策略表达式
strategy_file: Python 策略文件路径
Returns:
Strategy 子类
"""
if strategy_file:
return _load_strategy_from_file(strategy_file)
if strategy_str:
click.echo("错误: DSL 策略表达式尚未实现", err=True)
return None
return None
def _load_strategy_from_file(path: str) -> type:
"""从 Python 文件加载 Strategy 子类。
Args:
path: Python 文件路径
Returns:
Strategy 子类
"""
from ..backtest.strategy import Strategy
file_path = Path(path)
if not file_path.exists():
click.echo(f"错误: 文件不存在: {path}", err=True)
raise SystemExit(1)
spec = importlib.util.spec_from_file_location("strategy_module", file_path)
if spec is None or spec.loader is None:
click.echo(f"错误: 无法加载文件: {path}", err=True)
raise SystemExit(1)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
# 查找 Strategy 子类
strategy_classes = []
for name in dir(module):
obj = getattr(module, name)
try:
if isinstance(obj, type) and issubclass(obj, Strategy) and obj is not Strategy:
strategy_classes.append(obj)
except TypeError:
pass
if not strategy_classes:
click.echo(f"错误: 文件中未找到 Strategy 子类: {path}", err=True)
raise SystemExit(1)
if len(strategy_classes) > 1:
click.echo(f"警告: 文件包含多个 Strategy 子类,使用第一个: {path}", err=True)
return strategy_classes[0]
def _load_combo_strategies(combo_strategies: str) -> list[type]:
"""从逗号分隔的路径列表加载多个策略类。
Args:
combo_strategies: 逗号分隔的策略文件路径
Returns:
Strategy 子类列表
"""
paths = [p.strip() for p in combo_strategies.split(",") if p.strip()]
if len(paths) < 2:
click.echo("错误: --combo-strategies 至少需要 2 个策略文件", err=True)
raise SystemExit(1)
classes: list[type] = []
for p in paths:
cls = _load_strategy_from_file(p)
classes.append(cls)
names = [c.__name__ for c in classes]
click.echo(f"[*] 多因子组合 ({len(classes)} 因子): {' + '.join(names)}")
return classes
def _print_table(result: Any) -> None:
"""以表格形式输出回测结果。"""
perf = result.performance
config = result.config
click.echo("=== 回测绩效概要 ===")
click.echo(f"总收益率: {perf.get('total_return', 0):.2%}")
click.echo(f"年化收益: {perf.get('annual_return', 0):.2%}")
click.echo(f"最大回撤: {perf.get('max_drawdown', 0):.2%}")
click.echo(f"夏普比率: {perf.get('sharpe', 0):.2f}")
click.echo(f"胜率: {perf.get('win_rate', 0):.2%}")
click.echo(f"交易次数: {perf.get('total_trades', 0)}")
# 深度风险指标(v1.28 新增;老结果缺键时跳过,不输出 0 假值)
if perf.get("ulcer_index") is not None:
click.echo(f"Ulcer 指数: {perf.get('ulcer_index', 0):.4f}")
click.echo(f"日 VaR(95%): {perf.get('var_95', 0):.2%}")
click.echo(f"日 CVaR(95%): {perf.get('cvar_95', 0):.2%}")
click.echo(f"SQN 系统质量: {perf.get('sqn', 0):.2f}")
click.echo(
f"最大连胜/连亏: {perf.get('max_consecutive_wins', 0)} / "
f"{perf.get('max_consecutive_losses', 0)}"
)
click.echo()
if getattr(result, "diagnostic", None):
click.echo(f"⚠ 诊断: {result.diagnostic}")
click.echo()
click.echo("=== 配置参数 ===")
click.echo(f"初始资金: {config.get('cash', 0):.2f}")
click.echo(f"佣金率: {config.get('commission', 0):.4f}")
click.echo(f"成交规则: {config.get('execution', 'next_open')}")
if config.get("chanlun_level"):
click.echo(f"缠论级别: {config.get('chanlun_level')}")
click.echo()
if config.get("future_leak_warning"):
click.echo("!!! 警告: 策略可能存在未来函数(使用未来数据)")
click.echo()
if not result.trades.empty:
click.echo("=== 最近交易记录 ===")
recent_trades = result.trades.tail(10)
for idx, trade in recent_trades.iterrows():
direction = "买入" if trade["direction"] == "BUY" else "卖出"
status = "拒绝" if trade["rejected"] else "成交"
click.echo(
f" [{trade['datetime']}] {direction} "
f"数量={trade['size']:.0f} 价格={trade['price']:.2f} "
f"盈亏={trade['pnl']:.2f} [{status}]"
)
else:
click.echo("无交易记录")
# ── portfolio 多标的组合回测命令 ─────────────────────────────────────────────
@click.command()
@click.option(
"--stocks",
required=True,
help="股票列表:逗号分隔的 市场:代码(如 SZ:000001,SH:600519,SH:600036",
)
@click.option("--strategy-file", "strategy_file", required=True, help="Python 策略文件路径")
@click.option("--cash", default=200_000.0, type=float, help="总资金(默认 20 万)")
@click.option("--commission", default=0.0003, type=float, help="佣金率")
@click.option(
"--execution",
default="next_open",
type=click.Choice(["next_open", "next_close"]),
help="成交价规则",
)
@click.option("--period", default="DAILY", help="K线周期")
@click.option("--adjust", default="NONE", help="复权: NONE/QFQ/HFQ")
@click.option("--count", default=500, type=int, help="K线数量")
@click.option(
"--allocation",
default="equal",
type=click.Choice(["equal"], case_sensitive=False),
help="资金分配方式(默认 equal 均等分配)",
)
@click.option(
"--chanlun-level",
"chanlun_level",
default=None,
help="自动计算缠论分析并注入策略(如 DAILY/30MIN",
)
@click.option(
"--auto-fees",
"auto_fees",
is_flag=True,
help="按标的品种自动解析费率(ETF/可转债免印花税等;显式 --commission 优先)",
)
@click.option("--wf", "walk_forward", is_flag=True, help="附加组合级 Walk-Forward 样本外验证")
@click.option("--wf-windows", "wf_windows", default=7, type=int, help="Walk-Forward 窗口数")
@click.option(
"--evaluate",
"full_evaluate",
is_flag=True,
help="组合级一条龙评估:组合回测+组合WF+适配性+综合评分+组合评级+等权买入持有基准对比(覆盖常规输出)",
)
@click.option("--table", "use_table", is_flag=True, help="表格输出")
@click.option("--output", "output_fmt", type=click.Choice(["json", "table", "csv"]), default="json")
def portfolio(
stocks: str,
strategy_file: str,
cash: float,
commission: float,
execution: str,
period: str,
adjust: str,
count: int,
allocation: str,
chanlun_level: str | None,
auto_fees: bool,
walk_forward: bool,
wf_windows: int,
full_evaluate: bool,
use_table: bool,
output_fmt: str,
) -> None:
"""多标的组合回测:共享资金池,独立产生信号,统一管理仓位。
对多只股票同时回测,按均等比例分配资金,汇总组合整体绩效。
示例:
easy-tdx portfolio --stocks SZ:000001,SH:600519 --strategy-file ma_cross.py
easy-tdx portfolio --stocks SZ:000001,SH:600519,SH:600036 \\
--strategy-file my_strategy.py --cash 500000 --table
easy-tdx portfolio --stocks SZ:000001,SH:600519 \\
--strategy-file chanlun_strat.py --chanlun-level DAILY
easy-tdx portfolio --stocks SZ:000001,SH:600519 \\
--strategy-file ma_cross.py --wf --wf-windows 7
easy-tdx portfolio --stocks SZ:000001,SH:600519 \\
--strategy-file ma_cross.py --evaluate
"""
import json
from ..cli.conn import get_mac_client
from ..cli.parsers import parse_adjust, parse_market, parse_period
from .portfolio_engine import PortfolioBacktestEngine, StockData
# 1. 加载策略
strategy_cls = _load_strategy_from_file(strategy_file)
strategy_name = strategy_cls.__name__
# 2. 解析股票列表
stock_list = []
for item in stocks.split(","):
item = item.strip()
if ":" not in item:
click.echo(f"错误: 股票格式应为 市场:代码,如 SZ:000001,收到: {item}", err=True)
raise SystemExit(1)
mkt_str, code = item.split(":", 1)
stock_list.append((mkt_str.strip().upper(), code.strip()))
if not stock_list:
click.echo("错误: 未指定股票", err=True)
raise SystemExit(1)
click.echo(f"策略: {strategy_name} | 标的: {len(stock_list)} 只 | 资金: {cash:,.0f}", err=True)
# 3. 获取数据
stock_data_list: list[StockData] = []
with get_mac_client() as client:
for mkt_str, code in stock_list:
mkt = parse_market(mkt_str)
df = client.get_stock_kline(
mkt,
code,
period=parse_period(period),
start=0,
count=count,
adjust=parse_adjust(adjust),
)
stock_data_list.append(StockData(code=code, market=mkt_str, df=df))
# 4. 组合级一条龙评估:覆盖常规输出(含组合回测本身,无需重复跑)
if full_evaluate:
from .benchmark import evaluate_portfolio
report = evaluate_portfolio(
strategy=strategy_cls,
stocks=stock_data_list,
total_cash=cash,
commission=commission,
execution=execution,
chanlun_level=chanlun_level,
auto_fees=auto_fees,
n_windows=wf_windows,
)
click.echo(json.dumps(report, ensure_ascii=False, default=str))
return
# 5. 组合级 Walk-Forward 样本外验证(--wf
if walk_forward:
from .walkforward import PortfolioWalkForwardEngine
wf = PortfolioWalkForwardEngine(
strategy=strategy_cls,
stocks=stock_data_list,
n_windows=wf_windows,
total_cash=cash,
commission=commission,
execution=execution,
chanlun_level=chanlun_level,
auto_fees=auto_fees,
)
click.echo(json.dumps({"walkforward": wf.run().to_dict()}, ensure_ascii=False, default=str))
return
# 6. 常规组合回测
engine = PortfolioBacktestEngine(
strategy=strategy_cls,
stocks=stock_data_list,
total_cash=cash,
allocation=allocation,
commission=commission,
execution=execution,
chanlun_level=chanlun_level,
auto_fees=auto_fees,
)
result = engine.run()
# 7. 输出结果
fmt = "table" if use_table else output_fmt
if fmt == "table":
_print_portfolio_table(result)
else:
click.echo(json.dumps(result.to_dict(), ensure_ascii=False, indent=2))
def _print_portfolio_table(result: Any) -> None:
"""以表格形式输出组合回测结果。"""
perf = result.total_performance
click.echo("=== 组合回测绩效概要 ===")
click.echo(f"标的数量: {perf.get('total_stocks', 0)}")
click.echo(f"总资金: {perf.get('total_cash', 0):,.0f}")
click.echo(f"组合收益率: {perf.get('total_return', 0):.2%}")
click.echo(f"组合年化: {perf.get('annual_return', 0):.2%}")
click.echo()
click.echo("── 各标的详情 ──")
for key, stock_result in result.individual_results.items():
sp = stock_result.performance
alloc = result.equity_allocation.get(key, 0)
click.echo(
f" {key}: 收益={sp.get('total_return', 0):.2%} "
f"夏普={sp.get('sharpe', 0):.2f} "
f"回撤={sp.get('max_drawdown', 0):.2%} "
f"分配={alloc:.0%} "
f"交易={sp.get('total_trades', 0)}"
)
click.echo()
# ── strategies 内置策略列表命令 ──────────────────────────────────────────────
@click.command("strategies")
@click.option("--output", "output_fmt", type=click.Choice(["json", "table"]), default="table")
def strategies(output_fmt: str) -> None:
"""列出内置策略注册表:名称、参数定义与预设寻优网格。
策略名可直接用于 optimize --strategy / Web API /backtest/run 的 strategy 字段。
示例:
easy-tdx strategies
easy-tdx strategies --output json
"""
import json
from .strategies import get_registry
entries = get_registry().all()
if output_fmt == "json":
click.echo(json.dumps([e.to_schema() for e in entries], ensure_ascii=False, indent=2))
return
click.echo(f"=== 内置策略({len(entries)} 个)===\n")
for entry in entries:
schema = entry.to_schema()
params_desc = ", ".join(f"{p['name']}={p['default']}" for p in schema["params"])
grid = schema.get("preset_grid") or {}
points = 1
for vals in grid.values():
points *= len(vals)
grid_desc = " × ".join(f"{k}:{len(v)}" for k, v in grid.items()) if grid else "无"
click.echo(f" {entry.name}{entry.label}")
click.echo(f" 参数: {params_desc or '无'}")
click.echo(f" 预设网格: {grid_desc}{points} 点)")
if entry.description:
click.echo(f" 说明: {entry.description}")
click.echo()
# ── optimize 参数网格寻优命令 ────────────────────────────────────────────────
def _coerce_param_value(raw: str) -> Any:
"""把字符串参数值尽量转为 int/float,失败保留字符串。"""
try:
return int(raw)
except ValueError:
pass
try:
return float(raw)
except ValueError:
return raw
def _parse_param_grid(pairs: tuple[str, ...]) -> dict[str, list[Any]]:
"""解析 --param fast=5,10,15 形式的自定义网格。"""
grid: dict[str, list[Any]] = {}
for item in pairs:
name, sep, raw = item.partition("=")
values = [_coerce_param_value(v.strip()) for v in raw.split(",") if v.strip()]
if not sep or not name.strip() or not values:
click.echo(f"错误: --param 格式应为 参数名=值1,值2,收到: {item}", err=True)
raise SystemExit(1)
grid[name.strip()] = values
return grid
@click.command()
@click.argument("market")
@click.argument("code")
@click.option(
"--strategy",
"strategy_name",
default=None,
help="注册表策略名(见 strategies 命令;网格取 --param 或该策略预设)",
)
@click.option(
"--all",
"optimize_all",
is_flag=True,
help="一键寻优所有内置策略:逐策略按预设网格寻优,输出全局排名",
)
@click.option(
"--param",
"param_pairs",
multiple=True,
help="自定义参数网格,如 --param fast=5,10,15 --param slow=20,60(覆盖预设)",
)
@click.option("--cash", default=1_000_000.0, type=float, help="初始资金")
@click.option("--commission", default=0.0003, type=float, help="佣金率")
@click.option("--slippage", default=0.0, type=float, help="滑点")
@click.option(
"--execution",
default="next_open",
type=click.Choice(["next_open", "next_close"]),
help="成交价规则",
)
@click.option(
"--workers",
default=1,
type=int,
help="并行进程数:1=串行+指标缓存(默认);2+=进程级并行",
)
@click.option("--period", default="DAILY", help="K线周期")
@click.option("--adjust", default="NONE", help="复权: NONE/QFQ/HFQ")
@click.option("--count", default=500, type=int, help="K线数量")
@click.option("--top", default=15, type=int, help="表格输出显示前 N 行")
@click.option("--table", "use_table", is_flag=True, help="表格输出")
@click.option("--output", "output_fmt", type=click.Choice(["json", "table"]), default="json")
def optimize(
market: str,
code: str,
strategy_name: str | None,
optimize_all: bool,
param_pairs: tuple[str, ...],
cash: float,
commission: float,
slippage: float,
execution: str,
workers: int,
period: str,
adjust: str,
count: int,
top: int,
use_table: bool,
output_fmt: str,
) -> None:
"""参数网格寻优:单策略网格搜索,或 --all 一键寻优所有内置策略。
示例:
easy-tdx optimize SZ 000001 --strategy ma_cross
easy-tdx optimize SZ 000001 --strategy ma_cross \\
--param fast=5,10,15 --param slow=20,60
easy-tdx optimize SZ 000001 --all --workers 4 --table
"""
from ..cli.conn import get_mac_client
from ..cli.parsers import parse_adjust, parse_market, parse_period
from .strategies import get_registry
from .strategies.presets import get_preset
registry = get_registry()
# 1. 校验模式与策略(联网取数之前,快速失败)
if optimize_all == (strategy_name is not None):
click.echo("错误: --all 与 --strategy 二选一", err=True)
raise SystemExit(1)
custom_grid = _parse_param_grid(param_pairs) if param_pairs else None
if optimize_all and custom_grid is not None:
# --all 逐策略使用各自预设网格,--param 无处安放:显式告警而非静默忽略(审查修复)
click.echo(
"警告: --param 在 --all 模式下被忽略(一键寻优逐策略使用各自预设网格;"
"如需自定义网格请指定 --strategy 单策略寻优)",
err=True,
)
if not optimize_all:
assert strategy_name is not None
try:
entry = registry.get(strategy_name)
except KeyError as exc:
click.echo(f"错误: {exc}", err=True)
raise SystemExit(1) from None
if custom_grid is not None:
declared = {p.name for p in entry.params}
unknown = set(custom_grid) - declared
if unknown:
click.echo(
f"错误: 未知参数 {sorted(unknown)}"
f"'{strategy_name}' 可用参数: {sorted(declared)}",
err=True,
)
raise SystemExit(1)
else:
custom_grid = get_preset(strategy_name)
if not custom_grid:
first_param = entry.params[0].name if entry.params else "参数名"
click.echo(
f"错误: 策略 '{strategy_name}' 未登记预设网格,请用 --param 指定(如 "
f"--param {first_param}=5,10,20",
err=True,
)
raise SystemExit(1)
# 2. 获取数据
mkt = parse_market(market)
with get_mac_client() as client:
df = client.get_stock_kline(
mkt,
code,
period=parse_period(period),
start=0,
count=count,
adjust=parse_adjust(adjust),
)
# 3. 寻优
fmt = "table" if use_table else output_fmt
import json
if optimize_all:
from .optimizer import optimize_all_strategies
report = optimize_all_strategies(
df,
cash=cash,
commission=commission,
slippage=slippage,
execution=execution,
workers=workers,
)
if fmt == "table":
_print_optimize_all_table(report, top)
else:
click.echo(json.dumps(report, ensure_ascii=False, default=str))
return
from .optimizer import ParamGridOptimizer
assert strategy_name is not None and custom_grid is not None
optimizer = ParamGridOptimizer(
strategy_name=strategy_name,
param_grid=custom_grid,
df=df,
cash=cash,
commission=commission,
slippage=slippage,
execution=execution,
workers=workers,
)
result = optimizer.run()
if fmt == "table":
_print_optimize_table(result.to_dict(), top)
else:
click.echo(json.dumps(result.to_dict(), ensure_ascii=False, default=str))
def _fmt_grid_point(params: dict[str, Any]) -> str:
"""把参数字典格式化为 fast=10, slow=20 形式。"""
return ", ".join(f"{k}={v}" for k, v in params.items())
def _print_optimize_table(report: dict[str, Any], top: int) -> None:
"""以表格形式输出单策略寻优结果。"""
results = report.get("results") or []
best = report.get("best")
click.echo(f"=== 参数寻优: {report.get('strategy')}{len(results)} 个有效网格点)===\n")
click.echo(
f"{'排名':<4} {'参数':<28} {'总收益率':>8} {'夏普':>6} {'最大回撤':>8} "
f"{'交易':>4} {'胜率':>6}"
)
for i, r in enumerate(results[:top], 1):
click.echo(
f"{i:<4} {_fmt_grid_point(r['params']):<28} {r['total_return']:>8.2%} "
f"{r['sharpe']:>6.2f} {r['max_drawdown']:>8.2%} "
f"{r['total_trades']:>4} {r['win_rate']:>6.1%}"
)
if best:
click.echo(f"\n最佳参数: {_fmt_grid_point(best['params'])}")
if len(results) > top:
click.echo(f"(仅显示前 {top} 行,完整结果用 --output json")
def _print_optimize_all_table(report: dict[str, Any], top: int) -> None:
"""以表格形式输出 --all 全策略寻优排名。"""
ranking = report.get("ranking") or []
click.echo(
f"=== 一键寻优所有策略({len(ranking)} 个策略,"
f"共 {report.get('total_grid_points', 0)} 网格点)===\n"
)
click.echo(
f"{'排名':<4} {'策略':<20} {'最佳参数':<28} {'总收益率':>8} {'夏普':>6} "
f"{'最大回撤':>8} {'交易':>4} {'胜率':>6}"
)
for i, r in enumerate(ranking[:top], 1):
label = f"{r['strategy']} {r.get('strategy_label', '')}"
click.echo(
f"{i:<4} {label:<20} {_fmt_grid_point(r['params']):<28} "
f"{r['total_return']:>8.2%} {r['sharpe']:>6.2f} "
f"{r['max_drawdown']:>8.2%} {r['total_trades']:>4} {r['win_rate']:>6.1%}"
)
if report.get("skipped"):
click.echo(f"\n跳过(未注册): {', '.join(report['skipped'])}")
if ranking:
click.echo(f"\n全局最优: {ranking[0]['strategy']} {_fmt_grid_point(ranking[0]['params'])}")
if len(ranking) > top:
click.echo(f"(仅显示前 {top} 行,完整结果用 --output json")