Files
easy_tdx_max/src/easy_tdx/cli/cmd_ccpm.py
T
GitHub 99397a12e3 feat(ccpm): 中金所成交持仓排名采集——CLI/API/WebUI 三端 + 新手科普
- 新增 easy_tdx.ccpm 模块(独立数据源,标准库 urllib 零依赖):
  官网 /sj/ccpm/{YYYYMM}/{DD}/{品种}.xml,交易日约 16:15 发布,
  单文件含全部合约 × 三类排名(datatypeid 0=成交量/1=持买单/2=持卖单)× 前 20 名会员
- 协议要点:?id= 为 0~99 随机防缓存参数可省略;非交易日 302→error_404,
  禁用 urllib 自动重定向并翻译为 CcpmNoDataError(区别于网络错误 CcpmError);
  仅 http 可用;历史可回溯至 2012 年
- 8 品种:IF/IH/IC/IM 股指 + TS/TF/T/TL 国债;latest_rank() 自动回溯最近交易日;
  按日不可变 → ~/.easy_tdx/cache/ccpm/ 落盘缓存,历史二次查询零网络
- CLI:easy-tdx ccpm IF [--date] [--table] [--refresh] [--no-cache],all=全品种
- API:GET /ccpm/products(品种科普元数据)+ GET /ccpm/rank?product&date
  (404=非交易日/未发布,refresh 强制重抓)
- WebUI「期货持仓排名」页(行情组):品种下拉+日期+自动回溯开关+一键采集,
  合约页签自动标注主力,前 20 合计多/空/净持仓概览,三组排名并排表格
- 三段新手科普:「这是什么数据」「品种一览」「多单空单加减仓怎么看」
  (强调排名看不出套保还是投机,空单多 ≠ 看空市场)
- 测试 20 例(mock HTTP 零网络):解析/缓存/302 语义/回溯/路由/CLI
2026-09-02 20:54:59 +08:00

99 lines
2.9 KiB
Python

"""中金所成交持仓排名命令(独立数据源,无需 TDX 服务器)。"""
from __future__ import annotations
from typing import TYPE_CHECKING
import click
if TYPE_CHECKING:
import pandas as pd
#: 表格模式下的中文列名(JSON/CSV 保持英文机器友好列名)
_COLUMN_LABELS = {
"trading_day": "交易日",
"product": "品种",
"instrument": "合约",
"rank": "排名",
"vol_member": "成交量·会员",
"vol": "成交量(手)",
"vol_chg": "增减",
"long_member": "持买单·会员",
"long_pos": "持买单量(手)",
"long_chg": "增减2",
"short_member": "持卖单·会员",
"short_pos": "持卖单量(手)",
"short_chg": "增减3",
}
@click.command("ccpm")
@click.argument("product", default="IF")
@click.option(
"--date",
"trade_date",
default=None,
help="交易日 YYYY-MM-DD(缺省自动回溯到最近有数据的交易日)",
)
@click.option("--table", "use_table", is_flag=True, help="表格输出")
@click.option("--output", "output_fmt", type=click.Choice(["json", "table", "csv"]), default="json")
@click.option("--refresh", is_flag=True, help="忽略本地缓存,强制重新抓取")
@click.option("--no-cache", is_flag=True, help="本次不读也不写本地缓存")
def ccpm(
product: str,
trade_date: str | None,
use_table: bool,
output_fmt: str,
refresh: bool,
no_cache: bool,
) -> None:
"""获取中金所成交持仓排名(官网每日收盘后约 16:15 发布,前 20 名会员)。
\b
品种代码:
IF 沪深300 IH 上证50 IC 中证500 IM 中证1000
TS 2年国债 TF 5年国债 T 10年国债 TL 30年国债
all = 一次抓取全部 8 个品种
\b
示例:
easy-tdx ccpm IF --table
easy-tdx ccpm IF --date 2026-09-02
easy-tdx ccpm all --date 2026-08-28 --table
"""
from ..ccpm import PRODUCT_CODES, CcpmClient, CcpmError
from .output import print_error, print_output
products = PRODUCT_CODES if product.strip().lower() == "all" else [product.strip().upper()]
client = CcpmClient(use_cache=not no_cache)
frames = []
try:
for p in products:
if trade_date:
frames.append(client.get_rank(p, trade_date, refresh=refresh))
else:
frames.append(client.latest_rank(p, refresh=refresh))
except (CcpmError, ValueError) as e:
print_error(str(e))
raise SystemExit(1) from e
import pandas as pd
df = pd.concat(frames, ignore_index=True) if len(frames) > 1 else frames[0]
fmt = "table" if use_table else output_fmt
if fmt == "table":
click.echo(_render_table(df))
else:
print_output(df, fmt)
def _render_table(df: pd.DataFrame) -> str:
"""中文表头 + 不截断会员名的表格渲染。"""
from .output import _render_table_full
return _render_table_full(df.rename(columns=_COLUMN_LABELS))