mirror of
https://ghfast.top/https://github.com/aeroxw/easy_tdx_max.git
synced 2026-09-12 21:34:21 +08:00
fix(types): v1.20.2 修复 v1.20.1 引入的 CI mypy 失败
v1.20.1 把 BacktestResult.performance 扩大为 dict[str, float | str] (为塞 diagnostic_warning 字符串),破坏 6 处下游消费方类型契约, CI mypy job 转红。重构为更干净的设计: - 诊断信息走独立的 BacktestResult.diagnostic 字段(performance.py PerformanceAnalyzer.diagnostic 属性 → engine 透出 → to_dict 含该字段 → CLI 显示),performance 字典回归 dict[str, float](保留别名键)。 - optimizer.py 补 npt.NDArray 标注,消除 3 个既有 ndarray type-arg 错误。 测试:932 passed;mypy backtest/+portfolio/ 零错误;ruff 全绿。
This commit is contained in:
@@ -256,8 +256,8 @@ def _print_table(result: Any) -> None:
|
||||
click.echo(f"交易次数: {perf.get('total_trades', 0)}")
|
||||
click.echo()
|
||||
|
||||
if perf.get("diagnostic_warning"):
|
||||
click.echo(f"⚠ 诊断: {perf['diagnostic_warning']}")
|
||||
if getattr(result, "diagnostic", None):
|
||||
click.echo(f"⚠ 诊断: {result.diagnostic}")
|
||||
click.echo()
|
||||
|
||||
click.echo("=== 配置参数 ===")
|
||||
|
||||
@@ -166,11 +166,12 @@ class BacktestEngine:
|
||||
|
||||
# Step 4: Performance analysis
|
||||
trades_df = self._trades_to_df(trades)
|
||||
performance = PerformanceAnalyzer(
|
||||
analyzer = PerformanceAnalyzer(
|
||||
tracker.equity_curve,
|
||||
trades_df,
|
||||
risk_free_rate=0.03,
|
||||
).compute()
|
||||
)
|
||||
performance = analyzer.compute()
|
||||
|
||||
# Config snapshot
|
||||
config = {
|
||||
@@ -187,6 +188,7 @@ class BacktestEngine:
|
||||
trades=trades_df,
|
||||
positions=tracker.positions,
|
||||
config=config,
|
||||
diagnostic=analyzer.diagnostic,
|
||||
)
|
||||
|
||||
def _execute_with_model(self, signals: list[Signal], df: pd.DataFrame) -> list[Trade]:
|
||||
@@ -503,10 +505,11 @@ class BacktestEngine:
|
||||
Returns:
|
||||
BacktestResult with empty DataFrames
|
||||
"""
|
||||
perf = PerformanceAnalyzer(
|
||||
analyzer = PerformanceAnalyzer(
|
||||
pd.DataFrame(columns=["total", "drawdown", "drawdown_pct"]),
|
||||
pd.DataFrame(columns=["direction", "pnl", "rejected"]),
|
||||
).compute()
|
||||
)
|
||||
perf = analyzer.compute()
|
||||
|
||||
return BacktestResult(
|
||||
performance=perf,
|
||||
@@ -529,4 +532,5 @@ class BacktestEngine:
|
||||
columns=["datetime", "size", "avg_price", "market_value", "unrealized_pnl"]
|
||||
),
|
||||
config={},
|
||||
diagnostic=analyzer.diagnostic,
|
||||
)
|
||||
|
||||
@@ -48,8 +48,10 @@ class PerformanceAnalyzer:
|
||||
self._equity_curve = equity_curve
|
||||
self._trades = trades
|
||||
self._risk_free_rate = risk_free_rate
|
||||
# 数据异常诊断(资金曲线不足/恒定时填充),供上层透出给用户
|
||||
self.diagnostic: str | None = None
|
||||
|
||||
def compute(self) -> dict[str, float | str]:
|
||||
def compute(self) -> dict[str, float]:
|
||||
"""计算绩效指标。
|
||||
|
||||
Returns:
|
||||
@@ -76,7 +78,8 @@ class PerformanceAnalyzer:
|
||||
"""
|
||||
# 边界检查
|
||||
if len(self._equity_curve) < 2:
|
||||
return self._empty_metrics("资金曲线不足 2 根,无法计算绩效(数据可能为空或不全)")
|
||||
self.diagnostic = "资金曲线不足 2 根,无法计算绩效(数据可能为空或不全)"
|
||||
return self._empty_metrics()
|
||||
|
||||
total = self._equity_curve["total"].to_numpy()
|
||||
drawdown = self._equity_curve["drawdown"].to_numpy()
|
||||
@@ -89,10 +92,11 @@ class PerformanceAnalyzer:
|
||||
|
||||
# 日收益率数量太少时返回空指标
|
||||
if len(daily_ret) < 2:
|
||||
return self._empty_metrics(
|
||||
self.diagnostic = (
|
||||
"有效日收益率不足 2 个,绩效全 0(资金曲线可能恒定,常因数据不全或"
|
||||
"交易未生效;建议 easy-tdx ping 切换服务器后重试)"
|
||||
)
|
||||
return self._empty_metrics()
|
||||
|
||||
# 1. 总收益率(首根净值为 0 时无法定义,记为 0.0)
|
||||
total_return = (total[-1] / total[0]) - 1 if total[0] != 0 else 0.0
|
||||
@@ -337,19 +341,17 @@ class PerformanceAnalyzer:
|
||||
|
||||
return int(max_dd_idx - peak_idx)
|
||||
|
||||
def _empty_metrics(self, diagnostic: str | None = None) -> dict[str, float | str]:
|
||||
"""返回全零指标字典。
|
||||
def _empty_metrics(self) -> dict[str, float]:
|
||||
"""返回全零指标字典(数据不足时的默认返回值)。
|
||||
|
||||
用于数据不足时的默认返回值。``diagnostic`` 非空时一并返回,便于
|
||||
上层(CLI/UI)提示用户绩效全 0 的原因(典型为数据不全)。
|
||||
|
||||
Args:
|
||||
diagnostic: 可选的诊断说明,写入返回字典的 ``diagnostic_warning`` 键。
|
||||
数据异常的诊断说明通过 ``self.diagnostic`` 暴露,由上层(CLI/引擎)
|
||||
读取后透出给用户,不污染数值型 performance 字典(保持
|
||||
``dict[str, float]`` 类型,避免下游算术/比较类型报错)。
|
||||
|
||||
Returns:
|
||||
全零的绩效指标字典(可选含 ``diagnostic_warning``)。
|
||||
全零的绩效指标字典(含别名键 sharpe_ratio/start_cash/end_value)。
|
||||
"""
|
||||
metrics: dict[str, float | str] = {
|
||||
return {
|
||||
"total_return": 0.0,
|
||||
"annual_return": 0.0,
|
||||
"max_drawdown": 0.0,
|
||||
@@ -373,6 +375,3 @@ class PerformanceAnalyzer:
|
||||
"start_cash": 0.0,
|
||||
"end_value": 0.0,
|
||||
}
|
||||
if diagnostic is not None:
|
||||
metrics["diagnostic_warning"] = diagnostic
|
||||
return metrics
|
||||
|
||||
@@ -106,13 +106,15 @@ class BacktestResult:
|
||||
trades: 成交记录 DataFrame
|
||||
positions: 持仓快照 DataFrame
|
||||
config: 配置参数字典
|
||||
diagnostic: 数据异常诊断(资金曲线不足/恒定时的提示),无异常时为 None
|
||||
"""
|
||||
|
||||
performance: dict[str, float | str]
|
||||
performance: dict[str, float]
|
||||
equity_curve: pd.DataFrame
|
||||
trades: pd.DataFrame
|
||||
positions: pd.DataFrame
|
||||
config: dict[str, Any]
|
||||
diagnostic: str | None = None
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
"""将结果转换为 JSON 兼容字典。
|
||||
@@ -125,6 +127,7 @@ class BacktestResult:
|
||||
"trades": self.trades.to_dict(orient="records"),
|
||||
"positions": self.positions.to_dict(orient="records"),
|
||||
"config": self.config,
|
||||
"diagnostic": self.diagnostic,
|
||||
}
|
||||
|
||||
def to_json(self) -> str:
|
||||
|
||||
@@ -126,7 +126,7 @@ class FactorWeightedOptimizer(WeightOptimizer):
|
||||
top = factor_scores.nlargest(n_stocks, "score")
|
||||
if len(top) == 0:
|
||||
return {}
|
||||
scores = top["score"].to_numpy(dtype=np.float64)
|
||||
scores: npt.NDArray[np.float64] = top["score"].to_numpy(dtype=np.float64)
|
||||
if len(scores) > 10:
|
||||
q95 = np.percentile(scores, 95)
|
||||
q05 = np.percentile(scores, 5)
|
||||
@@ -160,10 +160,10 @@ class RiskParityOptimizer(WeightOptimizer):
|
||||
if len(top) == 0:
|
||||
return {}
|
||||
if "volatility" in top.columns:
|
||||
vol = top["volatility"].to_numpy(dtype=np.float64)
|
||||
vol: npt.NDArray[np.float64] = top["volatility"].to_numpy(dtype=np.float64)
|
||||
else:
|
||||
scores = top["score"].abs().to_numpy(dtype=np.float64)
|
||||
vol = 1.0 / (scores + 1e-8)
|
||||
scores: npt.NDArray[np.float64] = top["score"].abs().to_numpy(dtype=np.float64)
|
||||
vol = (1.0 / (scores + 1e-8)).astype(np.float64)
|
||||
vol = np.maximum(vol, 1e-8)
|
||||
inv_vol = 1.0 / vol
|
||||
total = inv_vol.sum()
|
||||
@@ -205,7 +205,7 @@ class MeanVarianceOptimizer(WeightOptimizer):
|
||||
variances = 1.0 / (np.abs(scores) + 1e-8) ** 2
|
||||
cov = np.diag(variances)
|
||||
|
||||
def objective(w: np.ndarray) -> float:
|
||||
def objective(w: npt.NDArray[np.float64]) -> float:
|
||||
return float(w @ cov @ w)
|
||||
|
||||
constraints = {"type": "eq", "fun": lambda w: float(np.sum(w) - 1.0)}
|
||||
|
||||
Reference in New Issue
Block a user