mirror of
https://ghfast.top/https://github.com/aeroxw/easy-tdx.git
synced 2026-09-12 15:44:15 +08:00
fix(types): 修复 CI mypy strict + ruff format 失败
mypy (13 errors → 0): - portfolio/optimizer: register_optimizer 返回类型改为 Callable 装饰器签名 (原标注 type[WeightOptimizer] 导致 4 个子类 Too many arguments) - factor/engine: _datetime_to_int 用 isinstance 收窄替代 object→int 强转 - factor/analysis: 删多余 type:ignore(改由 mypy override 统一处理 scipy) - backtest/orders, execution: np.sqrt 表达式用 float() 包裹消除 no-any-return - MyTT.pyi: MACD 签名删除错误的 LOW/HIGH 参数(与 MyTT.py 实际签名对齐) - pyproject: 新增 scipy mypy override (ignore_missing_imports) ruff format: 8 个 test 文件格式化 验证: 564 passed, mypy 192 文件零错误, ruff check/format 全绿
This commit is contained in:
@@ -37,6 +37,10 @@ module = "easy_tdx.MyTT"
|
||||
module = ["fastapi.*", "uvicorn.*", "pydantic.*"]
|
||||
ignore_missing_imports = true
|
||||
|
||||
[[tool.mypy.overrides]]
|
||||
module = ["scipy", "scipy.*"]
|
||||
ignore_missing_imports = true
|
||||
|
||||
[tool.ruff]
|
||||
target-version = "py310"
|
||||
line-length = 100
|
||||
|
||||
@@ -22,8 +22,6 @@ def DMA(S: npt.ArrayLike, A: npt.ArrayLike) -> NDArray: ...
|
||||
|
||||
def MACD(
|
||||
CLOSE: npt.ArrayLike,
|
||||
LOW: npt.ArrayLike,
|
||||
HIGH: npt.ArrayLike,
|
||||
SHORT: int = ...,
|
||||
LONG: int = ...,
|
||||
M: int = ...,
|
||||
|
||||
@@ -79,7 +79,7 @@ class ExecutionModel(ABC):
|
||||
returns = np.diff(close) / close[:-1]
|
||||
if len(returns) < 2:
|
||||
return 0.0
|
||||
return float(np.std(returns)) * np.sqrt(252)
|
||||
return float(float(np.std(returns)) * np.sqrt(252))
|
||||
|
||||
def _calc_buy_size(
|
||||
self,
|
||||
|
||||
@@ -326,7 +326,7 @@ class OrderSimulator:
|
||||
if len(returns) < 2:
|
||||
return 0.0
|
||||
daily_vol = float(np.std(returns))
|
||||
return daily_vol * np.sqrt(252)
|
||||
return float(daily_vol * np.sqrt(252))
|
||||
|
||||
def _execute_buy(
|
||||
self,
|
||||
|
||||
@@ -57,7 +57,7 @@ class FactorAnalyzer:
|
||||
continue
|
||||
if method == "spearman":
|
||||
try:
|
||||
import scipy # type: ignore[import-untyped] # noqa: F401 # pandas spearman lazy import
|
||||
import scipy # noqa: F401 # pandas spearman lazy import
|
||||
except ImportError as e:
|
||||
raise ImportError(
|
||||
"Rank IC (spearman) 需要 scipy,请执行 `pip install easy-tdx[science]`"
|
||||
|
||||
@@ -22,8 +22,11 @@ def _resolve_factor(f: str | Factor) -> Factor:
|
||||
def _datetime_to_int(dt_val: object) -> int:
|
||||
"""将 datetime 值转为 YYYYMMDD 整数。"""
|
||||
if hasattr(dt_val, "strftime"):
|
||||
return int(dt_val.strftime("%Y%m%d")) # type: ignore[union-attr]
|
||||
return int(dt_val)
|
||||
strftime = getattr(dt_val, "strftime")
|
||||
return int(strftime("%Y%m%d"))
|
||||
if isinstance(dt_val, (int, float)):
|
||||
return int(dt_val)
|
||||
return 0
|
||||
|
||||
|
||||
_ALL_DATES: object = object()
|
||||
|
||||
@@ -3,10 +3,14 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Callable
|
||||
|
||||
|
||||
class WeightOptimizer(ABC):
|
||||
"""权重优化器基类。"""
|
||||
@@ -25,7 +29,7 @@ class WeightOptimizer(ABC):
|
||||
_OPTIMIZER_REGISTRY: dict[str, type[WeightOptimizer]] = {}
|
||||
|
||||
|
||||
def register_optimizer(name: str) -> type[WeightOptimizer]:
|
||||
def register_optimizer(name: str) -> Callable[[type[WeightOptimizer]], type[WeightOptimizer]]:
|
||||
"""注册优化器。"""
|
||||
|
||||
def wrapper(cls: type[WeightOptimizer]) -> type[WeightOptimizer]:
|
||||
@@ -127,6 +131,7 @@ class MeanVarianceOptimizer(WeightOptimizer):
|
||||
from scipy.optimize import minimize # noqa: F401
|
||||
|
||||
return self._optimize_with_scipy(factor_scores, n_stocks)
|
||||
|
||||
except ImportError:
|
||||
fallback = EqualWeightOptimizer()
|
||||
return fallback.optimize(factor_scores, n_stocks)
|
||||
@@ -136,7 +141,7 @@ class MeanVarianceOptimizer(WeightOptimizer):
|
||||
factor_scores: pd.DataFrame,
|
||||
n_stocks: int,
|
||||
) -> dict[str, float]:
|
||||
from scipy.optimize import minimize as _minimize # type: ignore[import]
|
||||
from scipy.optimize import minimize as _minimize
|
||||
|
||||
top = factor_scores.nlargest(n_stocks, "score")
|
||||
if len(top) == 0:
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
# tests/unit/test_factor_analysis.py
|
||||
"""Test FactorAnalyzer and FactorReport."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
# tests/unit/test_factor_base.py
|
||||
"""Test Factor base class and registry."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pandas as pd
|
||||
@@ -97,6 +98,7 @@ class TestRegistry:
|
||||
return df["close"]
|
||||
|
||||
with pytest.raises(ValueError, match="已注册"):
|
||||
|
||||
@register_factor
|
||||
class Dup2(Factor):
|
||||
name = "dup_test_factor"
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
"""Test built-in factor computation correctness."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
@@ -22,15 +23,17 @@ def _make_df(n: int = 120, seed: int = 42) -> pd.DataFrame:
|
||||
amount = close * vol
|
||||
|
||||
dates = pd.date_range("2024-01-01", periods=n, freq="D")
|
||||
return pd.DataFrame({
|
||||
"datetime": dates,
|
||||
"open": open_,
|
||||
"high": high,
|
||||
"low": low,
|
||||
"close": close,
|
||||
"vol": vol,
|
||||
"amount": amount,
|
||||
})
|
||||
return pd.DataFrame(
|
||||
{
|
||||
"datetime": dates,
|
||||
"open": open_,
|
||||
"high": high,
|
||||
"low": low,
|
||||
"close": close,
|
||||
"vol": vol,
|
||||
"amount": amount,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
# ── Auto-registration ──────────────────────────────────────────────
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
# tests/unit/test_factor_engine.py
|
||||
"""Test FactorEngine."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
@@ -23,15 +24,17 @@ def _make_df(n: int = 60, seed: int = 42) -> pd.DataFrame:
|
||||
amount = close * vol
|
||||
|
||||
dates = pd.date_range("2024-01-01", periods=n, freq="D")
|
||||
return pd.DataFrame({
|
||||
"datetime": dates,
|
||||
"open": open_,
|
||||
"high": high,
|
||||
"low": low,
|
||||
"close": close,
|
||||
"vol": vol,
|
||||
"amount": amount,
|
||||
})
|
||||
return pd.DataFrame(
|
||||
{
|
||||
"datetime": dates,
|
||||
"open": open_,
|
||||
"high": high,
|
||||
"low": low,
|
||||
"close": close,
|
||||
"vol": vol,
|
||||
"amount": amount,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class _SimpleMomentum(Factor):
|
||||
@@ -196,6 +199,7 @@ class TestFactorEngineWithBuiltins:
|
||||
df = _make_df(200)
|
||||
|
||||
from easy_tdx.factor.builtin import list_factors
|
||||
|
||||
for f_info in list_factors():
|
||||
name = f_info["name"]
|
||||
result = engine.compute_single(df, [name])
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
# tests/unit/test_factor_transform.py
|
||||
"""Test factor preprocessing functions."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
@@ -20,12 +21,14 @@ def _make_cross_section(n_dates: int = 20, n_stocks: int = 30, seed: int = 42) -
|
||||
rows = []
|
||||
for d in range(n_dates):
|
||||
for s in range(n_stocks):
|
||||
rows.append({
|
||||
"date": 20240101 + d,
|
||||
"code": f"{s:06d}",
|
||||
"momentum_20d": rng.normal(0.02, 0.05),
|
||||
"volatility_20d": abs(rng.normal(0.02, 0.01)),
|
||||
})
|
||||
rows.append(
|
||||
{
|
||||
"date": 20240101 + d,
|
||||
"code": f"{s:06d}",
|
||||
"momentum_20d": rng.normal(0.02, 0.05),
|
||||
"volatility_20d": abs(rng.normal(0.02, 0.01)),
|
||||
}
|
||||
)
|
||||
df = pd.DataFrame(rows)
|
||||
df.loc[0, "momentum_20d"] = 10.0
|
||||
df.loc[1, "momentum_20d"] = -10.0
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
"""Test portfolio optimizers."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
@@ -15,10 +16,12 @@ from easy_tdx.portfolio.optimizer import (
|
||||
|
||||
def _make_scores(n: int = 20, seed: int = 42) -> pd.DataFrame:
|
||||
rng = np.random.default_rng(seed)
|
||||
return pd.DataFrame({
|
||||
"code": [f"{i:06d}" for i in range(n)],
|
||||
"score": rng.normal(0.02, 0.05, n),
|
||||
})
|
||||
return pd.DataFrame(
|
||||
{
|
||||
"code": [f"{i:06d}" for i in range(n)],
|
||||
"score": rng.normal(0.02, 0.05, n),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class TestEqualWeight:
|
||||
@@ -57,11 +60,13 @@ class TestRiskParity:
|
||||
assert abs(sum(w.values()) - 1.0) < 1e-6
|
||||
|
||||
def test_with_volatility_column(self):
|
||||
scores = pd.DataFrame({
|
||||
"code": ["A", "B", "C"],
|
||||
"score": [1.0, 1.0, 1.0],
|
||||
"volatility": [0.1, 0.2, 0.4],
|
||||
})
|
||||
scores = pd.DataFrame(
|
||||
{
|
||||
"code": ["A", "B", "C"],
|
||||
"score": [1.0, 1.0, 1.0],
|
||||
"volatility": [0.1, 0.2, 0.4],
|
||||
}
|
||||
)
|
||||
w = RiskParityOptimizer().optimize(scores, n_stocks=3)
|
||||
assert w["A"] > w["C"]
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
"""Test RebalanceEngine."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
@@ -14,13 +15,17 @@ def _make_market(n_stocks: int = 10, n_days: int = 120, seed: int = 42) -> dict[
|
||||
for i in range(n_stocks):
|
||||
close = 10.0 + np.cumsum(rng.normal(0.01, 0.5, n_days))
|
||||
close = np.maximum(close, 1.0)
|
||||
data[f"{i:06d}"] = pd.DataFrame({
|
||||
"datetime": pd.date_range("2024-01-01", periods=n_days, freq="D"),
|
||||
"open": close, "high": close + 0.3, "low": close - 0.3,
|
||||
"close": close,
|
||||
"vol": rng.integers(1e5, 1e7, n_days).astype(float),
|
||||
"amount": close * 1e6,
|
||||
})
|
||||
data[f"{i:06d}"] = pd.DataFrame(
|
||||
{
|
||||
"datetime": pd.date_range("2024-01-01", periods=n_days, freq="D"),
|
||||
"open": close,
|
||||
"high": close + 0.3,
|
||||
"low": close - 0.3,
|
||||
"close": close,
|
||||
"vol": rng.integers(1e5, 1e7, n_days).astype(float),
|
||||
"amount": close * 1e6,
|
||||
}
|
||||
)
|
||||
return data
|
||||
|
||||
|
||||
@@ -28,8 +33,10 @@ class TestRebalanceEngine:
|
||||
def test_basic_run(self):
|
||||
engine = RebalanceEngine(
|
||||
optimizer=EqualWeightOptimizer(),
|
||||
factor_name="momentum_20d", n_stocks=5,
|
||||
rebalance_freq="M", cash=1_000_000,
|
||||
factor_name="momentum_20d",
|
||||
n_stocks=5,
|
||||
rebalance_freq="M",
|
||||
cash=1_000_000,
|
||||
)
|
||||
result = engine.run(_make_market(), start_date=20240101, end_date=20240430)
|
||||
assert len(result.states) > 0
|
||||
@@ -40,7 +47,8 @@ class TestRebalanceEngine:
|
||||
def test_with_factor_weighted(self):
|
||||
engine = RebalanceEngine(
|
||||
optimizer=FactorWeightedOptimizer(),
|
||||
factor_name="momentum_20d", n_stocks=5,
|
||||
factor_name="momentum_20d",
|
||||
n_stocks=5,
|
||||
)
|
||||
result = engine.run(_make_market(), start_date=20240101, end_date=20240430)
|
||||
assert len(result.states) > 0
|
||||
@@ -51,14 +59,17 @@ class TestRebalanceEngine:
|
||||
|
||||
def test_equity_curve_dates_sorted(self):
|
||||
result = RebalanceEngine(
|
||||
optimizer=EqualWeightOptimizer(), rebalance_freq="M",
|
||||
optimizer=EqualWeightOptimizer(),
|
||||
rebalance_freq="M",
|
||||
).run(_make_market(), start_date=20240101, end_date=20240430)
|
||||
dates = result.equity_curve["datetime"].tolist()
|
||||
assert dates == sorted(dates)
|
||||
|
||||
def test_trades_recorded(self):
|
||||
engine = RebalanceEngine(
|
||||
optimizer=EqualWeightOptimizer(), n_stocks=3, rebalance_freq="M",
|
||||
optimizer=EqualWeightOptimizer(),
|
||||
n_stocks=3,
|
||||
rebalance_freq="M",
|
||||
)
|
||||
result = engine.run(_make_market(), start_date=20240101, end_date=20240430)
|
||||
assert len(result.trades) > 0
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
"""Test RiskModel."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
|
||||
Reference in New Issue
Block a user