**回测可视化 Web UI**(v1.17 新增)——Vue3 + ECharts 单页应用,浏览器里选标的、挑策略、调参数,K 线买卖点、净值回撤、25 项绩效指标一目了然。支持组合回测、参数网格寻优、多策略结果对比,**还能把好策略存进策略库(SQLite 持久化),勾选多个策略做资金分仓组合回测看综合表现**,全程零代码。v1.27 起新增「附加分析」开关:勾选后随回测自动跑 Walk-Forward 逐窗柱状图与一条龙评估报告(评分分项 / 高适配徽标 / 买入持有对比)。v1.28.1 起新手与 AI 辅助两连击:三个报告框内置**名词解释折叠帮助**(33 个词条讲清每项指标是什么、怎么算、怎么看,默认收起点击展开,重点粗体、阈值橙色、细节细体);回测报告一键导出 **AI 解读 Prompt**——把配置 + 25 项指标 + WF 逐窗 + 一条龙评估 + 评级 + 成交摘要组装成结构化提示词,复制发给 ChatGPT / Claude / DeepSeek / 豆包,即可获得「老手朋友」口吻的通俗解读、改进建议与 **0-10 信心分**(附「该不该执行」行动刻度)。
@@ -176,6 +178,21 @@ easy-tdx announcement 601088 --count 5 --download 5 --download-dir ./pdfs
> 独立数据源(巨潮资讯网),无需连接 TDX 行情服务器即可使用。
> 返回的 ``url`` 含 4 参数可直接打开,``pdf_url`` 为 PDF 直链。
+### 中金所成交持仓排名(v1.29.1)
+
+```bash
+easy-tdx ccpm IF --table # 最近有数据的交易日(缺省自动回溯)
+easy-tdx ccpm IF --date 2026-09-02 # 指定交易日
+easy-tdx ccpm all --date 2026-08-28 --table # 全部 8 个品种一次抓取
+easy-tdx ccpm TL --refresh # 忽略本地缓存,强制重新抓取
+```
+
+> 独立数据源(中金所官网),无需连接 TDX 行情服务器。品种:IF 沪深300 / IH 上证50 / IC 中证500 / IM 中证1000 股指期货,TS/TF/T/TL 为 2/5/10/30 年期国债期货。
+> 每个交易日收盘后约 16:15 发布,含该品种**全部合约 × 三类排名(成交量 / 持买单量·多单 / 持卖单量·空单)× 各前 20 名会员**;
+> 数据发布后不可变,按日缓存到 `~/.easy_tdx/cache/ccpm/`,历史二次查询零网络。
+> JSON 输出为英文列名(vol/long_pos/short_pos 等,机器友好),`--table` 自动切换中文表头。
+> WebUI 对应「期货持仓排名」页(含新手科普),API 为 `GET /api/v1/ccpm/rank`。
+
### 技术指标
```bash
@@ -518,6 +535,7 @@ Web UI 包含两大模块:
- **行情终端(v1.23 新增)**——侧边栏专业终端布局:
- **市场看板**:五大指数实时行情(SSE 推送)、全市场涨跌统计(涨/跌/平/涨停/跌停 + 堆叠条)、行业/概念板块热度榜、涨幅榜/跌幅榜、两市异动雷达(加速拉升/封涨停板/大单托盘等),点击个股打开五档盘口 + 分时/日K 对话框;
- **自选行情**:输入 6 位代码一键加自选(SQLite 持久化),全表实时刷新(SSE),行内迷你分时图,点击行看个股详情;
+ - **期货持仓排名**(v1.29.1):中金所每日成交/持仓前 20 名会员一键采集(品种下拉 + 日期选择 + 自动回溯最近交易日开关),合约页签自动标注主力,前 20 名合计多单/空单/净持仓概览,三组排名并排表格;附「品种一览」「多单空单加减仓怎么看」新手科普(重点:排名看不出套保还是投机,空单多 ≠ 看空市场);
- **实时推送架构**:后端单条轮询循环 fan-out 到所有 SSE 连接(交易时段 ~8s 一拍,盘外降频 60s,无人订阅自动休眠),前端指数退避重连。
- **回测工作台(v1.17 起)**——浏览器里选标的、挑策略、调参数,K 线买卖点、净值回撤、25 项绩效指标一目了然。支持组合回测、参数网格寻优、多策略结果对比,还能把好策略存进策略库(SQLite 持久化),勾选多个策略做资金分仓组合回测看综合表现,全程零代码。
diff --git a/src/easy_tdx/ccpm/__init__.py b/src/easy_tdx/ccpm/__init__.py
new file mode 100644
index 0000000..b478b39
--- /dev/null
+++ b/src/easy_tdx/ccpm/__init__.py
@@ -0,0 +1,37 @@
+"""中金所成交持仓排名(ccpm)——官网每日成交/持仓前 20 名会员数据。
+
+独立于 TDX 协议的 HTTP 数据源(中金所官网),无需连接行情服务器。
+
+用法::
+
+ from easy_tdx.ccpm import CcpmClient
+
+ client = CcpmClient()
+ df = client.get_rank("IF", "2026-09-02") # 指定交易日
+ df = client.latest_rank("IF") # 自动回溯最近有数据的交易日
+"""
+
+from .client import WIDE_COLUMNS, CcpmClient, normalize_date, parse_xml
+from .models import (
+ PRODUCT_CODES,
+ PRODUCTS,
+ CcpmError,
+ CcpmNoDataError,
+ ProductMeta,
+ list_products,
+ normalize_product,
+)
+
+__all__ = [
+ "CcpmClient",
+ "CcpmError",
+ "CcpmNoDataError",
+ "PRODUCTS",
+ "PRODUCT_CODES",
+ "ProductMeta",
+ "WIDE_COLUMNS",
+ "list_products",
+ "normalize_date",
+ "normalize_product",
+ "parse_xml",
+]
diff --git a/src/easy_tdx/ccpm/client.py b/src/easy_tdx/ccpm/client.py
new file mode 100644
index 0000000..4f41f8b
--- /dev/null
+++ b/src/easy_tdx/ccpm/client.py
@@ -0,0 +1,314 @@
+"""中金所成交持仓排名(ccpm)客户端。
+
+数据源:中国金融期货交易所官网「成交持仓排名」页
+http://www.cffex.com.cn/cn/ccpm.html
+
+实际数据文件(每个交易日收盘后约 16:10 北京时间批量生成)::
+
+ http://www.cffex.com.cn/sj/ccpm/{YYYYMM}/{DD}/{品种}.xml
+
+协议要点(2026-09 实测):
+
+- 官网 JS 在 URL 上拼的 ``?id=<随机数>`` 仅为防浏览器缓存参数,无语义,可省略。
+- XML(UTF-8)包含该品种当日**所有合约** × 三类排名 × 各前 20 名会员:
+ ``datatypeid`` 0=成交量 / 1=持买单量(多单)/ 2=持卖单量(空单)。
+- 非交易日或未发布时官网返回 302 → ``error_404.html``:HTTP 客户端若
+ 自动跟随重定向会拿到 200 的错误页,必须禁用重定向并把 302/404 识别为
+ 「无数据」(:class:`CcpmNoDataError`)。
+- 仅支持 http(https 证书握手失败),无鉴权/无频控。
+- 每个交易日的数据发布后不可变 → 按日落盘缓存
+ ``~/.easy_tdx/cache/ccpm/{YYYYMMDD}/{品种}.json``(随
+ ``EASY_TDX_CONFIG_DIR``),历史日期二次查询零网络请求。
+"""
+
+from __future__ import annotations
+
+import json
+import logging
+import os
+import re
+from datetime import date, datetime, timedelta
+from pathlib import Path
+from typing import Any
+from urllib import request as urlrequest
+from urllib.error import HTTPError
+from xml.etree import ElementTree as ET
+from zoneinfo import ZoneInfo
+
+import pandas as pd
+
+from .models import CcpmError, CcpmNoDataError, normalize_product
+
+logger = logging.getLogger(__name__)
+
+_UA = (
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
+ "(KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
+)
+_BASE_URL = "http://www.cffex.com.cn/sj/ccpm/{yyyymm}/{dd}/{product}.xml"
+_SHANGHAI_TZ = ZoneInfo("Asia/Shanghai")
+
+#: datatypeid → 宽表列前缀(0=成交量 / 1=持买单量 / 2=持卖单量,官网 ccpm.js 语义)
+_TYPE_KEYS = {0: "vol", 1: "long", 2: "short"}
+
+#: 宽表列(合约 × 排名 对齐三类排名,与官网 CSV 同构)
+WIDE_COLUMNS = [
+ "trading_day",
+ "product",
+ "instrument",
+ "rank",
+ "vol_member",
+ "vol",
+ "vol_chg",
+ "long_member",
+ "long_pos",
+ "long_chg",
+ "short_member",
+ "short_pos",
+ "short_chg",
+]
+
+
+class _NoRedirectHandler(urlrequest.HTTPRedirectHandler):
+ """禁止跟随重定向:非交易日的 302 → error_404.html 不能被当成数据页。"""
+
+ def redirect_request(
+ self,
+ req: urlrequest.Request,
+ fp: Any,
+ code: int,
+ msg: str,
+ headers: Any,
+ newurl: str,
+ ) -> urlrequest.Request | None:
+ return None
+
+
+_OPENER = urlrequest.build_opener(_NoRedirectHandler)
+
+
+def _fetch_xml(url: str, timeout: float) -> str:
+ """GET 原始 XML 文本(stdlib urllib,monkeypatch 点)。
+
+ 302/404 → :class:`CcpmNoDataError`(非交易日/未发布);其他 HTTP 错误 →
+ :class:`CcpmError`。
+ """
+ req = urlrequest.Request(url, headers={"User-Agent": _UA})
+ try:
+ with _OPENER.open(req, timeout=timeout) as resp:
+ return str(resp.read(), encoding="utf-8")
+ except HTTPError as e:
+ if e.code in (301, 302, 303, 307, 308, 404):
+ raise CcpmNoDataError(f"该日期非交易日或数据尚未发布: {url}") from e
+ raise CcpmError(f"中金所返回 HTTP {e.code}: {url}") from e
+
+
+def _today_shanghai() -> date:
+ return datetime.now(_SHANGHAI_TZ).date()
+
+
+def normalize_date(value: str | date | datetime | None) -> date:
+ """日期归一化:接受 ``date``/``datetime``/``YYYY-MM-DD``/``YYYYMMDD``。"""
+ if value is None:
+ return _today_shanghai()
+ if isinstance(value, datetime):
+ return value.date()
+ if isinstance(value, date):
+ return value
+ s = str(value).strip().replace("-", "").replace("/", "")
+ if not re.fullmatch(r"\d{8}", s):
+ raise ValueError(f"日期格式应为 YYYY-MM-DD 或 YYYYMMDD: {value}")
+ return datetime.strptime(s, "%Y%m%d").date()
+
+
+def _to_int(v: str | None) -> int | None:
+ if v is None:
+ return None
+ v = v.strip()
+ if not v:
+ return None
+ try:
+ return int(v)
+ except ValueError:
+ return None
+
+
+def parse_xml(text: str) -> list[dict[str, Any]]:
+ """解析 positionRank XML → 宽表行列表(合约 × 排名 对齐三类排名)。
+
+ XML 长表结构(每条记录一个 ```` 节点)::
+
+ + 数据来自中国金融期货交易所官网「成交持仓排名」,每个交易日收盘后约 16:15 + 发布:按品种按合约,统计成交量 / 持买单量(多单)/ 持卖单量(空单)各前 20 + 名期货公司会员。首次采集会实时抓取官网并缓存到本地,同一天再次查看不再联网。 +
+ + +| 排名 | +成交量排名 | +持买单量(多单)排名 | +持卖单量(空单)排名 | +||||||
|---|---|---|---|---|---|---|---|---|---|
| 会员简称 | 手数 | 增减 | +会员简称 | 手数 | 增减 | +会员简称 | 手数 | 增减 | +|
| {{ r.rank }} | +{{ r.vol_member ?? '—' }} | +{{ fmt(r.vol) }} | +{{ fmtChg(r.vol_chg) }} | +{{ r.long_member ?? '—' }} | +{{ fmt(r.long_pos) }} | +{{ fmtChg(r.long_chg) }} | +{{ r.short_member ?? '—' }} | +{{ fmt(r.short_pos) }} | +{{ fmtChg(r.short_chg) }} | +
| 合计 | +前 20 名 | +{{ fmt(totals.vol) }} | +{{ fmtChg(totals.volChg) }} | +前 20 名 | +{{ fmt(totals.long) }} | +{{ fmtChg(totals.longChg) }} | +前 20 名 | +{{ fmt(totals.short) }} | +{{ fmtChg(totals.shortChg) }} | +
+ 数据来源:中国金融期货交易所(中金所)官网每个交易日收盘后发布的 + 「成交持仓排名」。这里的每一行不是某个人,而是一家期货公司会员名下全部客户的合计 + ——名称后面的「(代客)」= 代理客户,即该公司经纪业务客户的汇总, + 不是期货公司自己的自营盘。 +
++ 三组排名的含义: +
++ 「增减」列= 相比上一个交易日的变化:正数 = 加仓(新开仓多于平仓), + 负数 = 减仓(平仓多于新开仓)。例如某会员空单 −800 = 其客户合计平掉了 800 手空单。 +
++ 注意口径:只统计前 20 名会员(通常约占全市场六到八成持仓), + 不是全部;同一会员名下的客户里套保、投机、套利混在一起,无法从这张表区分。 +
++ 中金所的期货分两大类:股指期货(跟踪股票指数,用来交易「大盘涨跌」) + 和国债期货(跟踪利率,用来交易「利率涨跌」)。合约代码后四位是到期月, + 如 IF2609 = 2026 年 9 月到期的沪深300股指期货。 +
+| 代码 | 名称 | 跟踪什么 | 1 手规模 | 代表市场哪一块 |
|---|---|---|---|---|
| {{ p.code }} | +{{ p.name }} | +{{ p.underlying }} | +{{ p.unit }} | +{{ p.intro }} | +
+ 国债期货补充:它不跟踪某只指数,标的是「名义标准国债」, + 本质是利率期货——价格与市场利率反向: + 国债期货涨价 ≈ 市场预期利率下行(债券牛市);跌价 ≈ 预期利率上行。 + 四个品种对应 2 / 5 / 10 / 30 年期限,期限越长对利率越敏感(TL 波动最大)。 +
+先理解期货:期货是「约定未来按某个价格买卖」的合约。今天买入 = 认为未来会涨(多头);今天卖出 = 认为未来会跌(空头)。不持有合约也可以先卖(这是期货和股票最大的不同)。
+多单(持买单量):已经买入、还没平仓的合约。持有的人分两种——① 看涨投机:赌指数上涨赚差价;② 多头套保:未来要买入一篮子股票,先买期货锁定成本。
+空单(持卖单量):已经卖出、还没平仓的合约。持有的人也分两种——① 看跌投机:赌指数下跌;② 套保空单(最常见!):机构手里已经拿着股票现货,卖出股指期货来对冲大盘下跌风险。量化「中性策略」就是典型:买一篮子股票 + 卖空等值股指期货,赚选股超额收益、剥离大盘涨跌。
++ ★ 最重要的一点:这张表看不出「对冲」还是「纯做空」! + 排名只披露期货公司客户合计持仓,不区分目的。股指期货的空单大头通常是机构套保盘, + 「空单多 / 空单增加」≠ 看空市场,很多情况下反而说明机构持有大量现货股票。 + 把空单直接读成利空,是新手最常见的误读。 +
+加仓 / 减仓(增减列):正数 = 加仓,负数 = 减仓。常见组合的含义(仅供参考,非预测):
+净持仓:本页顶部「净持仓(多−空)」= 前 20 名多单合计减空单合计,粗略衡量「头部席位」的多空力量对比:正 = 偏多,负 = 偏空。但它只覆盖前 20 名、且混合了各类目的的仓位,只能作为情绪参考,不能单独当作涨跌预测。
+为什么全市场多空永远相等?期货是零和合约——每有一张多单,必然对应一张空单(你买到的合约就是别人卖出的)。所以看「全市场谁多谁空」没有意义,排名表真正告诉你的是:仓位集中在哪些期货公司的客户手里、它们在加码还是撤退。
++ 本页面数据来自中金所官网公开披露,仅供量化研究与学习参考。持仓排名仅反映前 + 20 名会员客户的仓位分布,不区分套保/投机/套利目的,不构成任何投资建议, + 不能预测市场涨跌。期货交易带杠杆,亏损可能超过本金,入市需谨慎, + 据此操作风险自负。 +
+