From 88638e82ad8a722ec8c6981aa1e812801a4e8438 Mon Sep 17 00:00:00 2001 From: Justin Gu <97915@qq.com> Date: Sat, 13 Jun 2026 21:10:35 +0800 Subject: [PATCH] =?UTF-8?q?release:=20v1.11.5=20=E2=80=94=20scipy=20?= =?UTF-8?q?=E4=BE=9D=E8=B5=96=E5=A3=B0=E6=98=8E=20+=20=E6=96=87=E6=A1=A3?= =?UTF-8?q?=E5=90=8C=E6=AD=A5=20+=20=E5=B7=A5=E7=A8=8B=E5=8C=96=E6=B8=85?= =?UTF-8?q?=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - pyproject: 新增 science 可选依赖组(scipy),dev 纳入 scipy;版本号 1.11.5 - factor/analysis: spearman 分支加 scipy 软依赖保护,缺失时友好报错 - mac/client: 板块排行吞异常处补 logger.debug - CLAUDE.md: 架构章节补全 mac/ex/unified/portfolio/factor/offline/screen 子包 - .gitignore: 补全 .coverage、signals.json - README: 补 1.11.5 changelog --- .gitignore | 4 +++ CLAUDE.md | 43 ++++++++++++++++++++++++++------- README.md | 18 ++++++++++++++ pyproject.toml | 5 ++-- src/easy_tdx/factor/analysis.py | 6 +++++ src/easy_tdx/mac/client.py | 5 ++++ 6 files changed, 70 insertions(+), 11 deletions(-) diff --git a/.gitignore b/.gitignore index 7d6c3d1..6d1c31a 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,7 @@ venv/ uv.lock docs/_build/ FK/ + +# 测试覆盖率与运行时产物 +.coverage +signals.json diff --git a/CLAUDE.md b/CLAUDE.md index a532ee0..ad5bbe0 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -23,7 +23,18 @@ ruff format --check src/ tests/ ``` src/easy_tdx/ -├── client.py # TdxClient / AsyncTdxClient(高层 API) +├── client.py # TdxClient / AsyncTdxClient(Windows 通达信协议高层 API) +├── mac/ # macOS 通达信协议(独立命令/编解码/握手) +│ ├── client.py # MacClient / AsyncMacClient(mac 行情高层 API) +│ ├── commands/ # mac 协议命令(SymbolQuotesCmd / BoardMembersQuotesCmd 等) +│ ├── enums.py # Category / SortType / FilterType / BoardType / Period / Adjust +│ └── models.py # MacQuoteField / MacTickChart 等 dataclass +├── ex/ # 扩展行情协议(ExTdx)+ macOS 扩展(MacEx) +│ ├── client.py # ExTdxClient / AsyncExTdxClient +│ ├── mac_client.py # MacExClient / AsyncMacExClient(goods_* 系列方法) +│ ├── commands/ # 扩展协议命令 +│ └── models.py # ExMarketInfo / ExInstrumentInfo 等 +├── unified.py # UnifiedTdxClient / AsyncUnifiedTdxClient(聚合 mac + mac_ex 的门面) ├── chanlun/ # 缠论技术分析模块(独立于 transport,纯计算) │ ├── analyser.py # ChanlunAnalyser 主入口(接收 DataFrame) │ ├── types.py # 数据结构(Kline/CLKline/FX/BI/XD/ZS/MMD/BC) @@ -39,27 +50,41 @@ src/easy_tdx/ │ ├── beichi.py # 背驰判断(笔/盘整/趋势) │ └── multi_level.py # 多级别联立分析 ├── transport/ -│ ├── sync.py # TdxConnection(socket)+ ping_host / ping_all +│ ├── sync.py # TdxConnection(socket)+ ping_host / ping_all / ping_mac_all │ └── async_.py # AsyncTdxConnection(asyncio) ├── commands/ # 每条命令:build_request() + parse_response(),无 IO -├── codec/ # price / volume / datetime / frame 编解码 +├── codec/ # price / volume / datetime / frame / financial / bitmap 编解码 ├── backtest/ # 回测引擎 │ ├── engine.py # BacktestEngine(止损/止盈执行/缠论自动桥接) │ ├── strategy.py # Strategy 基类(向量化 datetime) │ ├── orders.py # OrderSimulator(SL/TP 同 bar 执行) +│ ├── execution.py # 可插拔执行模型(Immediate/TWAP/VWAP/Limit) +│ ├── slippage.py # 滑点模型(Fixed/Percent/Volume/Volatility) │ ├── performance.py # PerformanceAnalyzer(FIFO 真实持仓天数) -│ ├── portfolio_engine.py # 多标的组合回测 -│ └── combo.py # 多因子组合回测 +│ ├── portfolio.py / portfolio_engine.py # 多标的组合回测 +│ ├── combo.py # 多因子组合回测 +│ ├── attribution.py # Brinson / 因子 / 成本归因 +│ └── dsl.py # 策略 DSL 解析 +├── portfolio/ # 组合层 +│ ├── optimizer.py # 权重优化(mean_variance 可选 scipy,缺失回退等权) +│ └── ... # risk / rebalance 等 +├── factor/ # 因子分析 +│ ├── analysis.py # FactorAnalyzer(compute_ic 用 spearman,可选 scipy → pip install easy-tdx[science]) +│ ├── engine.py / base.py / transform.py +│ └── builtin/ # 内置因子 ├── screen/ -│ └── scanner.py # SignalScanner(并发扫描/增量缓存) +│ ├── scanner.py # SignalScanner(并发扫描/增量缓存) +│ └── ranker.py # 排序/打分 ├── realtime/ │ └── engine.py # EventBus + RealtimeStrategy(asyncio 事件驱动) -├── cli/ -│ └── cmd_chanlun.py # easy-tdx chanlun CLI 命令 +├── offline/ # 离线数据读写(daily_bar / min_bar / gbbq / history_financial 等) +├── indicator.py / MyTT.py # 技术指标(MyTT 为第三方库移植,命名沿袭惯例) +├── config.py # 主机/端口/超时配置 + best_host 持久化 +├── cli/ # easy-tdx CLI(click);cmd_chanlun / cmd_backtest / cmd_offline / cmd_factor 等 └── models/ # 纯 dataclass,无业务逻辑 ``` -commands 层不依赖 transport,可独立单测。修改 codec 或 commands 时不需要网络。 +四套 client 关系:`client.py`(Windows 标准)、`mac/client.py`(macOS)、`ex/client.py`(扩展)、`ex/mac_client.py`(macOS 扩展);每套均提供 sync + async 镜像。`unified.py` 是 mac + mac_ex 的门面。commands 层不依赖 transport,可独立单测。修改 codec 或 commands 时不需要网络。 ## 协议编解码注意事项 diff --git a/README.md b/README.md index 312eb82..c1cbdd3 100644 --- a/README.md +++ b/README.md @@ -1511,6 +1511,24 @@ ruff format --check src/ tests/ # format check ## Changelog +### 1.11.5 (2026-06-13) + +**稳定性与代码质量修复** — 全项目代码审计 + 一个潜伏的 ping 崩溃 bug 修复。 + +**Bug 修复**: +- 修复 `easy-tdx ping` 在非交易时间(服务器握手阶段关闭连接)整个命令崩溃的问题。根因:`ping_host` 仅捕获 `OSError`,但握手期 `_recv_exact_sock` 抛出的 `TdxConnectionError`(继承自 `TdxError(Exception)` 而非 `OSError`)逃出捕获,经 `ping_all` 的 `fut.result()` 重新抛出,导致单台服务器不可用就拖垮整条测速命令。修复后符合 docstring 承诺"不可达服务器不包含在结果中",并加防御层让 `ping_all` 对异常 future 容错跳过。 +- 修复回测 `OrderSimulator._find_bar_index` 把 DataFrame 的 index label 当位置索引用的隐患。当传入 df 的 index 非默认 RangeIndex 时,`idxmax()` 返回的 label 与 `iloc[]` 期望的位置不一致,可能导致撮合取错 K 线。改用 `to_numpy().argmax()` 取真实位置。 + +**依赖与工程化**: +- scipy 隐式硬依赖声明:`factor/analysis.py` 的 Rank IC(spearman)通过 pandas lazy import scipy,干净环境必报 `ModuleNotFoundError`。新增 `science` 可选依赖组(`pip install easy-tdx[science]`),并在 spearman 分支加 try-import 友好报错(复用 `optimizer.py` 现有模式)。 +- `mac/client.py` 板块 N 日涨跌幅排行中静默吞异常的 `except Exception: continue` 补上 `logger.debug` 日志,便于排查。 +- `.gitignore` 补全 `.coverage`、`signals.json`。 + +**文档**: +- `CLAUDE.md` 架构章节更新:补全 `mac/`、`ex/`、`unified.py`、`portfolio/`、`factor/`、`offline/`、`screen/` 等子包,说明四套 client(Windows/macOS/扩展/macOS扩展)的 sync+async 镜像关系。 + +**测试**:564 passed, 0 failed(+8 新增:2 ping 容错回归、2 非连续 index 回归、4 既有覆盖增强) + ### 1.11.1 (2026-06-12) **量化因子引擎 + 组合管理 + 高级回测增强** — 三大新模块,补齐从因子研究到组合执行的完整量化链路。 diff --git a/pyproject.toml b/pyproject.toml index 09f4f8f..7322f53 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "easy-tdx" -version = "1.11.1" +version = "1.11.5" description = "通达信 TCP 协议行情数据客户端,支持在线行情、离线数据读取与写入同步" readme = "README.md" requires-python = ">=3.10" @@ -14,7 +14,8 @@ dependencies = ["pandas>=2.0", "tzdata>=2024.1", "click>=8.0"] easy-tdx = "easy_tdx.cli:cli" # cli/__init__.py exposes the click group [project.optional-dependencies] -dev = ["pytest>=8.0", "pytest-asyncio>=0.23", "pytest-cov", "mypy>=1.9", "ruff>=0.4"] +dev = ["pytest>=8.0", "pytest-asyncio>=0.23", "pytest-cov", "mypy>=1.9", "ruff>=0.4", "scipy>=1.10"] +science = ["scipy>=1.10"] web = ["fastapi>=0.110", "uvicorn[standard]>=0.29"] [tool.hatch.build.targets.wheel] diff --git a/src/easy_tdx/factor/analysis.py b/src/easy_tdx/factor/analysis.py index fdc5357..bc7e608 100644 --- a/src/easy_tdx/factor/analysis.py +++ b/src/easy_tdx/factor/analysis.py @@ -56,6 +56,12 @@ class FactorAnalyzer: ic_values.append(np.nan) continue if method == "spearman": + try: + import scipy # type: ignore[import-untyped] # noqa: F401 # pandas spearman lazy import + except ImportError as e: + raise ImportError( + "Rank IC (spearman) 需要 scipy,请执行 `pip install easy-tdx[science]`" + ) from e corr = valid[self._factor_col].corr(valid[self._return_col], method="spearman") else: corr = valid[self._factor_col].corr(valid[self._return_col], method="pearson") diff --git a/src/easy_tdx/mac/client.py b/src/easy_tdx/mac/client.py index 86d8ed6..256f502 100644 --- a/src/easy_tdx/mac/client.py +++ b/src/easy_tdx/mac/client.py @@ -3,6 +3,7 @@ from __future__ import annotations import asyncio +import logging import time from dataclasses import asdict from types import TracebackType @@ -48,6 +49,8 @@ _RETRY_DELAYS = (0.1, 0.5, 1.0, 2.0) _KLINE_PAGE_SIZE = 700 _BOARD_MEMBERS_PAGE_SIZE = 80 +_logger = logging.getLogger(__name__) + def _convert_board_code(board_symbol: str) -> int: """将用户可见的板块代码转换为服务器协议代码。 @@ -815,6 +818,7 @@ class MacClient: adjust=Adjust.NONE, ) except Exception: + _logger.debug("板块 %s K线获取失败,跳过", board_code, exc_info=True) continue if kline_df.empty or len(kline_df) < 2: @@ -1642,6 +1646,7 @@ class AsyncMacClient: adjust=Adjust.NONE, ) except Exception: + _logger.debug("板块 %s K线获取失败,跳过", board_code, exc_info=True) continue if kline_df.empty or len(kline_df) < 2: