diff --git a/pyproject.toml b/pyproject.toml index 7322f53..a75824e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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 diff --git a/src/easy_tdx/MyTT.pyi b/src/easy_tdx/MyTT.pyi index 6b2925f..4f85dad 100644 --- a/src/easy_tdx/MyTT.pyi +++ b/src/easy_tdx/MyTT.pyi @@ -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 = ..., diff --git a/src/easy_tdx/backtest/execution.py b/src/easy_tdx/backtest/execution.py index f0e4e57..bc1ba57 100644 --- a/src/easy_tdx/backtest/execution.py +++ b/src/easy_tdx/backtest/execution.py @@ -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, diff --git a/src/easy_tdx/backtest/orders.py b/src/easy_tdx/backtest/orders.py index 6333283..b6a03bb 100644 --- a/src/easy_tdx/backtest/orders.py +++ b/src/easy_tdx/backtest/orders.py @@ -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, diff --git a/src/easy_tdx/factor/analysis.py b/src/easy_tdx/factor/analysis.py index bc7e608..7b2e54f 100644 --- a/src/easy_tdx/factor/analysis.py +++ b/src/easy_tdx/factor/analysis.py @@ -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]`" diff --git a/src/easy_tdx/factor/engine.py b/src/easy_tdx/factor/engine.py index 0ff1ab6..db1e8a8 100644 --- a/src/easy_tdx/factor/engine.py +++ b/src/easy_tdx/factor/engine.py @@ -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() diff --git a/src/easy_tdx/portfolio/optimizer.py b/src/easy_tdx/portfolio/optimizer.py index b2bbed5..e397a63 100644 --- a/src/easy_tdx/portfolio/optimizer.py +++ b/src/easy_tdx/portfolio/optimizer.py @@ -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: diff --git a/tests/unit/test_factor_analysis.py b/tests/unit/test_factor_analysis.py index e8eed25..826e99b 100644 --- a/tests/unit/test_factor_analysis.py +++ b/tests/unit/test_factor_analysis.py @@ -1,5 +1,6 @@ # tests/unit/test_factor_analysis.py """Test FactorAnalyzer and FactorReport.""" + from __future__ import annotations import numpy as np diff --git a/tests/unit/test_factor_base.py b/tests/unit/test_factor_base.py index 5afff3a..298dad3 100644 --- a/tests/unit/test_factor_base.py +++ b/tests/unit/test_factor_base.py @@ -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" diff --git a/tests/unit/test_factor_builtin.py b/tests/unit/test_factor_builtin.py index c7e0d25..fc93f97 100644 --- a/tests/unit/test_factor_builtin.py +++ b/tests/unit/test_factor_builtin.py @@ -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 ────────────────────────────────────────────── diff --git a/tests/unit/test_factor_engine.py b/tests/unit/test_factor_engine.py index 0ba4471..01eede6 100644 --- a/tests/unit/test_factor_engine.py +++ b/tests/unit/test_factor_engine.py @@ -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]) diff --git a/tests/unit/test_factor_transform.py b/tests/unit/test_factor_transform.py index 37d1fe7..e0deb6d 100644 --- a/tests/unit/test_factor_transform.py +++ b/tests/unit/test_factor_transform.py @@ -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 diff --git a/tests/unit/test_portfolio_optimizer.py b/tests/unit/test_portfolio_optimizer.py index b5f48d5..2a64f53 100644 --- a/tests/unit/test_portfolio_optimizer.py +++ b/tests/unit/test_portfolio_optimizer.py @@ -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"] diff --git a/tests/unit/test_portfolio_rebalance.py b/tests/unit/test_portfolio_rebalance.py index e58f229..1987fdb 100644 --- a/tests/unit/test_portfolio_rebalance.py +++ b/tests/unit/test_portfolio_rebalance.py @@ -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 diff --git a/tests/unit/test_portfolio_risk.py b/tests/unit/test_portfolio_risk.py index 778f5ba..1bb2bef 100644 --- a/tests/unit/test_portfolio_risk.py +++ b/tests/unit/test_portfolio_risk.py @@ -1,4 +1,5 @@ """Test RiskModel.""" + from __future__ import annotations import numpy as np