Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
227 changes: 227 additions & 0 deletions data/feed/tushare_capabilities.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
"""Tushare endpoint capability registry — stable metadata + usage intent.

This is a HUMAN/PLANNING reference, NOT a runtime permission gate. It records,
per endpoint, what the data means to THIS project (phase, date field, PIT
timing, cache relevance, batch viability) so we can reason about which sources
are safe/useful to wire in.

Hard rules (do not violate):
* NO token, NO account points, NO secret of any kind lives here.
* Production code MUST NOT branch on ``observed_status`` to auto-skip an
endpoint — whether a call is authorized is decided by the live API response,
and permissions/points change over time. ``observed_status`` is a dated,
non-secret snapshot for auditing only (see docs/data/tushare_permissions.md
and the gitignored artifacts/permissions/ probe JSON).

``observed_status`` enum: authorized | empty_authorized | rate_limited |
no_permission | unknown. ``observed_at`` is the probe date for that snapshot.
"""

from __future__ import annotations

from dataclasses import dataclass

OBSERVED_AT = "2026-06-13"

# pit_timing vocabulary (when a value becomes known, for look-ahead safety):
# eod -> known after T's close (e.g. daily bar)
# eod_postclose -> published only in T's evening (use no earlier than T+1)
# same_day_publish -> published same day, PIT-safe to use on T (e.g. pe/pb)
# ann_date_disclosure -> usable only on/after its ann_date (financial reports)
# asof_snapshot -> latest snapshot on-or-before the date (index_weight)
# asof_interval -> in/out interval membership (PIT SW industry)
# dimension_snapshot -> slow-moving descriptor (list_date / name history)
# event_date -> tied to an event date (survey date)
# intraday -> within-day data (minute / auction)

# batch_viability_note vocabulary:
# proven_in_project_runs -> pulled at scale in real P0..P4-2 runs
# authorized_rate_unverified -> single call OK; per-batch rate NOT tested
# not_batch_viable_1_per_hour -> authorized but throttled to 1 call/hour
# no_access -> no permission (40203)


@dataclass(frozen=True)
class EndpointCapability:
"""Stable, non-secret metadata + this project's intent for one endpoint."""

name: str
category: str
phase_or_usage: str
date_field: str
key_fields: tuple[str, ...]
pit_timing: str
cache_relevance: str
batch_viability_note: str
observed_status: str = "unknown"
observed_at: str = OBSERVED_AT


_CAPS: tuple[EndpointCapability, ...] = (
# -- market bars (P4-1 cached) ---------------------------------------- #
EndpointCapability(
"daily", "market", "P0 raw OHLCV (qfq downstream)", "trade_date",
("ts_code", "trade_date"), "eod", "p4_1_cached",
"proven_in_project_runs", "authorized",
),
EndpointCapability(
"adj_factor", "market", "P1 front-adjust factor", "trade_date",
("ts_code", "trade_date"), "eod", "p4_1_cached",
"proven_in_project_runs", "authorized",
),
# -- universe / tradability (P4-2 cached) ----------------------------- #
EndpointCapability(
"index_weight", "universe", "P1 PIT index membership", "trade_date",
("index_code", "trade_date", "con_code"), "asof_snapshot", "p4_2_cached",
"proven_in_project_runs", "authorized",
),
EndpointCapability(
"suspend_d", "tradability", "P2-2 suspension flag", "trade_date",
("ts_code", "trade_date", "suspend_type"), "eod", "p4_2_cached",
"proven_in_project_runs", "empty_authorized",
),
EndpointCapability(
"namechange", "tradability", "P2-2 ST name intervals", "start_date",
("ts_code", "start_date", "end_date", "name"), "dimension_snapshot",
"p4_2_cached", "proven_in_project_runs", "authorized",
),
EndpointCapability(
"stock_basic", "universe", "P2-2 list_date (min_listing_days)", "list_date",
("ts_code", "list_date"), "dimension_snapshot", "p4_2_cached",
"proven_in_project_runs", "authorized",
),
EndpointCapability(
"stk_limit", "tradability", "P2-2 raw price limits", "trade_date",
("ts_code", "trade_date", "up_limit", "down_limit"), "eod", "p4_2_cached",
"proven_in_project_runs", "authorized",
),
# -- covariates / financials (P4-3 planned cache) --------------------- #
EndpointCapability(
"daily_basic", "covariate", "P2-3/P3-5 pe/pb/total_mv", "trade_date",
("ts_code", "trade_date"), "same_day_publish", "p4_3_planned",
"proven_in_project_runs", "authorized",
),
EndpointCapability(
"fina_indicator", "financial", "P3-1 roe/np_yoy (ann_date as-of)", "end_date",
("ts_code", "end_date", "ann_date"), "ann_date_disclosure", "p4_3_planned",
"proven_in_project_runs", "authorized",
),
EndpointCapability(
"income", "financial", "financial source (ann_date)", "end_date",
("ts_code", "end_date", "ann_date"), "ann_date_disclosure", "p4_3_planned",
"proven_in_project_runs", "authorized",
),
EndpointCapability(
"index_member_all", "covariate", "P2-3 PIT SW industry", "in_date",
("ts_code", "in_date", "out_date", "l1_name"), "asof_interval", "p4_3_planned",
"proven_in_project_runs", "authorized",
),
# -- EXPLORATORY candidate factor sources (authorized; rate UNVERIFIED) #
EndpointCapability(
"stk_factor", "special_factor", "EXPLORATORY technical factors", "trade_date",
("ts_code", "trade_date"), "eod", "candidate_uncached",
"authorized_rate_unverified", "authorized",
),
EndpointCapability(
"stk_factor_pro", "special_factor", "EXPLORATORY technical factors pro",
"trade_date", ("ts_code", "trade_date"), "eod", "candidate_uncached",
"authorized_rate_unverified", "authorized",
),
EndpointCapability(
"cyq_perf", "special_factor", "EXPLORATORY chip cost / winner rate",
"trade_date", ("ts_code", "trade_date"), "eod_postclose", "candidate_uncached",
"authorized_rate_unverified", "authorized",
),
EndpointCapability(
"cyq_chips", "special_factor", "EXPLORATORY chip distribution", "trade_date",
("ts_code", "trade_date", "price", "percent"), "eod_postclose",
"candidate_uncached", "authorized_rate_unverified", "authorized",
),
EndpointCapability(
"moneyflow", "moneyflow", "EXPLORATORY money flow (lg/sm)", "trade_date",
("ts_code", "trade_date"), "eod_postclose", "candidate_uncached",
"authorized_rate_unverified", "authorized",
),
EndpointCapability(
"moneyflow_dc", "moneyflow", "EXPLORATORY money flow (DC)", "trade_date",
("ts_code", "trade_date"), "eod_postclose", "candidate_uncached",
"authorized_rate_unverified", "authorized",
),
EndpointCapability(
"moneyflow_ths", "moneyflow", "EXPLORATORY money flow (THS)", "trade_date",
("ts_code", "trade_date"), "eod_postclose", "candidate_uncached",
"authorized_rate_unverified", "empty_authorized",
),
EndpointCapability(
"moneyflow_hsgt", "moneyflow", "EXPLORATORY north/south bound flow",
"trade_date", ("trade_date", "north_money"), "eod_postclose",
"candidate_uncached", "authorized_rate_unverified", "authorized",
),
EndpointCapability(
"limit_list_d", "sentiment", "EXPLORATORY limit-up/down stats", "trade_date",
("trade_date", "ts_code"), "eod_postclose", "candidate_uncached",
"authorized_rate_unverified", "authorized",
),
EndpointCapability(
"top_list", "sentiment", "EXPLORATORY dragon-tiger board", "trade_date",
("trade_date", "ts_code"), "eod_postclose", "candidate_uncached",
"authorized_rate_unverified", "empty_authorized",
),
EndpointCapability(
"block_trade", "reference", "block trades", "trade_date",
("ts_code", "trade_date"), "eod_postclose", "candidate_uncached",
"authorized_rate_unverified", "empty_authorized",
),
EndpointCapability(
"margin_detail", "reference", "margin trading detail", "trade_date",
("ts_code", "trade_date"), "eod_postclose", "candidate_uncached",
"authorized_rate_unverified", "authorized",
),
EndpointCapability(
"stk_surv", "alt_data", "institutional research records", "surv_date",
("ts_code", "surv_date"), "event_date", "candidate_uncached",
"authorized_rate_unverified", "authorized",
),
# -- minute bars: the 1-call/hour cap was LIFTED (re-probe 2026-06-13,
# 3/3 calls in ~12s) -> now authorized + batch-plausible; the exact rate
# ceiling was NOT stress-tested (no brute force).
EndpointCapability(
"stk_mins", "market_intraday", "EXPLORATORY minute bars (roadmap)",
"trade_time", ("ts_code", "trade_time"), "intraday", "candidate_uncached",
"authorized_rate_unverified", "authorized",
),
# -- throttled to 1 call/hour (NOT batch-viable) ---------------------- #
EndpointCapability(
"hm_detail", "sentiment", "EXPLORATORY hot-money detail", "trade_date",
("trade_date", "ts_code", "hm_name"), "eod_postclose", "candidate_uncached",
"not_batch_viable_1_per_hour", "rate_limited",
),
# -- no permission (40203) -------------------------------------------- #
EndpointCapability(
"stk_auction_o", "auction", "open call-auction", "trade_date",
("ts_code", "trade_date"), "intraday", "not_applicable",
"no_access", "no_permission",
),
EndpointCapability(
"stk_auction_c", "auction", "close call-auction", "trade_date",
("ts_code", "trade_date"), "intraday", "not_applicable",
"no_access", "no_permission",
),
)

CAPABILITIES: dict[str, EndpointCapability] = {c.name: c for c in _CAPS}


def get(name: str) -> EndpointCapability | None:
"""Return the capability record for ``name`` (or None if unknown)."""
return CAPABILITIES.get(name)


def names() -> tuple[str, ...]:
"""All registered endpoint names, in declaration order."""
return tuple(c.name for c in _CAPS)


def by_category(category: str) -> tuple[EndpointCapability, ...]:
"""All capabilities in ``category`` (declaration order)."""
return tuple(c for c in _CAPS if c.category == category)
102 changes: 102 additions & 0 deletions docs/data/tushare_permissions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
# Tushare 接口权限 / 限频实测

> 目的:区分 **有没有权限** 与 **能不能批量用**。MCP 单发探测只能证明权限(调一次通),
> 证明不了限频;**本表以 Python SDK 实测为准**,MCP 仅作辅助参考。

## 探测元信息

| 项 | 值 |
|---|---|
| 探测日期 | **2026-06-13** |
| Token 来源 | 外部 config:`financial_projects/.config.json`(key `tushare.token`)——**值绝不入库/不打印** |
| SDK | `tushare` **1.4.29**(`pro.query`,conda env `data_fetch`)|
| 方法 | 每接口 **1 次最小查询**(单只 `000001.SZ` + 单日/单报告期;`index_weight` 用单月区间)。**不暴力压测限频**;限频以 SDK 实际返回的「X 次/单位时间」为准 |
| 原始结果 | `artifacts/permissions/tushare_probe_20260613.json`(gitignored,无 token)|

> **更新 2026-06-13(同日复测)**:`stk_mins` 的「1 次/小时」限频**已放开**——SDK 连续 3 次小查询
> (不同交易日窗口、间隔 ~4s)**3/3 全通、约 12s 内完成**。判定从 `rate_limited` 改为
> `authorized`(exact 每分钟上限未压测)。分钟级数据现在**数据层可行**(路线图 architecture.html §11)。
> `hm_detail` 本轮**未复测**,其「1 次/小时」结论维持原状。

### 状态枚举

| status | 含义 |
|---|---|
| `authorized` | 有权限,本次返回了数据 |
| `empty_authorized` | **有权限**,但该样本(这只票/这天)无行——不是无权限 |
| `rate_limited` | 有权限,但触发限频(**关键:能调≠能批量**)|
| `no_permission` | 无权限(40203),需单独/更高档开通 |
| `transient_error` | 网络等瞬时错误(本次无)|

⚠️ **限频未逐个压测**:返回数据的接口只证明「这一次能调」,**未测每分钟/每天上限**。首轮探测时
`stk_mins`、`hm_detail` 因本会话先前 MCP 探测已各消耗掉额度,SDK 复测暴露 `1 次/小时`——
但 **`stk_mins` 同日再复测 3/3、限制已放开**(见上方更新说明),`hm_detail` 未复测、维持 1 次/小时。
其余 `authorized` 接口的批量额度**尚未核实**,接入前需各自做限频体检。

## 总表(SDK 实测)

### 项目数据层(P0–P4-2 已在用,且真实 run 已大规模拉取 = 批量可用已证)

| 接口 | 用途 / 项目相关 | probe params | SDK 结果 | 权限 | 批量可用 | PIT timing | 备注 |
|---|---|---|---|---|---|---|---|
| `daily` | P0 OHLCV(raw,下游 qfq) | ts+d | authorized (1 行) | ✅ | **已证**(真实 run) | T 日收盘后可得 T | P4-1 已缓存 |
| `adj_factor` | P1 复权因子 | ts+d | authorized (1) | ✅ | **已证** | T 日 | P4-1 已缓存 |
| `index_weight` | P1 PIT 成分 | 300+月区间 | authorized (600) | ✅ | **已证** | as-of 快照(latest ≤ date)| P4-2 已缓存;90 天分页 |
| `suspend_d` | P2-2 停牌 | ts+月区间 | empty_authorized (0) | ✅ | **已证** | T 日 flag | 样本无停牌≠无权限;P4-2 已缓存 |
| `namechange` | P2-2 ST 区间 | ts | authorized (8) | ✅ | **已证** | dimension(in/out 区间)| P4-2 已缓存 |
| `stock_basic` | P2-2 list_date(min_listing_days)| ts | authorized (1) | ✅ | **已证** | dimension(全局快照)| P4-2 已缓存 |
| `stk_limit` | P2-2 **raw** 涨跌停价 | ts+d | authorized (1) | ✅ | **已证** | T 日 **raw 价**(front-adjust 前比对)| P4-2 已缓存 |
| `daily_basic` | P2-3/P3-5 pe/pb/total_mv | ts+d | authorized (1) | ✅ | **已证** | **当日发布,same-day PIT-safe** | P4-3 待缓存 |
| `fina_indicator` | P3-1 roe/np_yoy(ann_date as-of)| ts+period | authorized (1) | ✅ | **已证** | **按 ann_date 披露后才可用** | 单次最多 100 行;P4-3 待缓存 |
| `index_member_all` | P2-3 PIT SW 行业 | ts | authorized (1) | ✅ | **已证** | in/out 区间 as-of | P4-3 待缓存 |
| `income` | 财务源(ann_date)| ts+period | authorized (1) | ✅ | 同 fina | 按 ann_date 披露 | 目前未进因子,留作财务源 |

### EXPLORATORY 候选源(权限有,但**批量额度未核实**——接入前各自限频体检)

| 接口 | 用途 / 项目相关 | probe params | SDK 结果 | 权限 | 批量可用 | PIT timing | 备注 |
|---|---|---|---|---|---|---|---|
| `stk_factor` | 技术因子(MACD/KDJ/RSI/BOLL…)| ts+d | authorized (1) | ✅ | **未压测** | T 日(由 EOD 计算)| 候选 |
| `stk_factor_pro` | 技术因子专业版(数百列,bfq/qfq/hfq)| ts+d | authorized (1) | ✅ | **未压测** | T 日 | 候选 ⭐ |
| `cyq_perf` | 每日筹码及胜率 | ts+d | authorized (1) | ✅ | **未压测** | ⚠️ **盘后 18–19 点更新 → T 日盘中/收盘不可用,T+1 才 PIT-safe** | 候选 ⭐ |
| `cyq_chips` | 每日筹码分布 | ts+d | authorized (104) | ✅ | **未压测** | ⚠️ **盘后 18–19 点** | 候选 ⭐ |
| `moneyflow` | 个股资金流(大小单)| ts+d | authorized (1) | ✅ | **未压测** | ⚠️ **盘后** | 候选 |
| `moneyflow_dc` | 东财个股资金流 | ts+d | authorized (1) | ✅ | **未压测** | ⚠️ **盘后**(数据起 2023-09-11)| 候选 ⭐ |
| `moneyflow_ths` | 同花顺个股资金流 | ts+d | empty_authorized (0) | ✅ | **未压测** | ⚠️ **盘后** | 样本空≠无权限 |
| `moneyflow_hsgt` | 北向/南向资金 | d | authorized (1) | ✅ | **未压测** | ⚠️ **盘后** | 候选 |
| `limit_list_d` | 涨跌停/炸板统计 | d+U | authorized (52) | ✅ | **未压测** | ⚠️ **盘后**(数据起 2020)| 候选 ⭐ |
| `top_list` | 龙虎榜明细 | ts+d | empty_authorized (0) | ✅ | **未压测** | ⚠️ **盘后** | 样本空(000001 当日未上榜)|
| `block_trade` | 大宗交易 | ts+d | empty_authorized (0) | ✅ | **未压测** | ⚠️ **盘后** | 样本空 |
| `margin_detail` | 两融明细 | ts+d | authorized (1) | ✅ | **未压测** | ⚠️ **盘后** | 参考 |
| `stk_surv` | 机构调研记录 | ts+月区间 | authorized (1) | ✅ | **未压测** | 按 surv_date(事件日)| 另类数据 |
| `stk_mins` | **分钟线** 1/5/15/30/60min | ts+小窗 | authorized (6) — **复测 3/3** | ✅ | **限频已放开**(每分钟上限未压测)| intraday(无 PIT 滞后)| ⭐ 路线图分钟级现在数据层可行;接 ETL 仍需缓存层+throttle |

### 限频到不可批量 / 无权限

| 接口 | 用途 | SDK 结果 | 判定 | 说明 |
|---|---|---|---|---|
| `hm_detail` | 游资交易每日明细 | `频率超限(1次/小时)` | ⚠️ **callable but NOT batch-viable** | 限 **1 次/小时**;本轮未复测 |
| `stk_auction_o` | 开盘集合竞价 | `没有接口访问权限(40203)` | ❌ **no_permission** | 集合竞价家族需**单独开通**,5000 仍不够 |
| `stk_auction_c` | 收盘集合竞价 | `没有接口访问权限(40203)` | ❌ **no_permission** | 同上 |

## PIT 安全提醒(接候选源前必读)

- **盘后数据**(`cyq_*` / `moneyflow*` / `limit_list_d` / `top_list` / `hm_detail` / `block_trade` /
`margin_detail`):T 日数值 **T 日收盘后**(部分晚到 18–19 点)才有。**绝不能用 T 日的该值参与 T 日
收盘选股**——会引入未来函数。只能当作「T 日已实现」、在 **T+1 调仓**时使用。接入时按 `factors` 层
不变量(factor 不碰未来收益)+ 现有 `ann_date` as-of 那套纪律处理。
- **当日发布且 PIT-safe**:`daily_basic`(pe/pb 基于当日收盘,已按 same-day 使用)。
- **披露日对齐**:`fina_indicator` / `income` 按 `ann_date` 才可用(已实现)。
- 任何候选源进 P3 矩阵仍走 **EXPLORATORY + 独立样本** 那套(POST-HOC 披露、跨 cell 复检)。

## 结论摘要

- **5000 积分基本打通全部「特色数据」权限档**(筹码、技术因子 pro、资金流、龙虎榜、涨跌停统计、
机构调研…),相比 3000 解锁一大批候选源——但**权限 ≠ 批量可用**。
- **需购买 / 无权限**:`stk_auction_o`、`stk_auction_c`(集合竞价家族)。
- **分钟线已放开**:`stk_mins` 同日复测 3/3,「1 次/小时」限制已解除——分钟级研究数据层现在可行
(每分钟上限未压测;接 ETL 仍要走缓存层 + throttle)。
- **仍限 1 次/小时、不可批量**:`hm_detail`(游资明细,本轮未复测)。
- **未压测限频(authorized 但批量额度未知)**:全部 EXPLORATORY 候选源(含已放开的 `stk_mins`)——接入前各做一次小批量限频体检
(项目已有 throttle+retry,正好测每接口 `X 次/分`)。
- **不要让生产代码依赖本表自动跳过**:本表与 `data/feed/tushare_capabilities.py` 仅供人审阅 / 规划;
运行时是否有权限由实际 API 返回决定,权限/积分会变。