Files
ProStock/docs/api/API_INTERFACE_SPEC.md
liaozhaorun d4e0e2a0b6 feat(data): 添加每日筹码及胜率数据接口 (cyq_perf)
- 新增 api_cyq_perf 模块,支持筹码分布数据获取和同步
- 在 sync_registry 中注册 cyq_perf 同步器
2026-03-26 22:22:43 +08:00

805 lines
23 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# ProStock 数据接口封装规范
## 1. 概述
本文档定义了新增 Tushare API 接口封装的标准规范。所有非特殊接口必须遵循此规范,确保:
- 代码风格统一
- 自动 sync 支持
- 增量更新逻辑一致
- 减少存储写入压力
- 类型安全(强制类型提示)
### 1.1 技术栈
- **存储层**: DuckDB高性能嵌入式 OLAP 数据库)
- **数据格式**: Pandas DataFrame / Polars DataFrame
- **速率限制**: 令牌桶算法TokenBucketRateLimiter
- **并发**: ThreadPoolExecutor 多线程
- **类型系统**: Python 3.10+ 类型提示
### 1.2 自动化支持
项目提供 `prostock-api-interface` Skill 来自动化接口封装流程。在 `api.md` 中定义接口后,调用该 Skill 可自动生成:
- 数据模块文件(`src/data/api_wrappers/api_{data_type}.py`
- 数据库表管理配置
- 测试文件(`tests/test_{data_type}.py`
## 2. 接口分类
### 2.1 特殊接口(不参与统一 sync
以下接口有独立的同步逻辑,不参与自动 sync 机制:
| 接口类型 | 文件名 | 说明 |
|---------|--------|------|
| 交易日历 | `api_trade_cal.py` | 全局数据,按日期范围获取,使用 HDF5 缓存 |
| 股票基础信息 | `api_stock_basic.py` | 一次性全量获取CSV 存储 |
| 辅助数据 | `api_industry`, `api_concept` | 低频更新,独立管理 |
### 2.2 标准接口(必须遵循本规范)
所有按股票或按日期获取的因子数据、行情数据、财务数据等,必须遵循本规范:
- 按日期获取:**优先选择**,支持全市场批量获取
- 按股票获取:仅当 API 不支持按日期获取时使用
## 3. 文件结构要求
### 3.1 文件命名
```
api_{data_type}.py
```
- 示例:`api_daily.py``api_moneyflow.py``api_limit_list.py`
- **必须**以 `api_` 前缀开头
- 使用小写字母和下划线
### 3.2 文件位置
所有接口文件必须位于 `src/data/api_wrappers/` 目录下。
### 3.3 导出要求
新接口必须在 `src/data/api_wrappers/__init__.py` 中导出:
```python
from src.data.api_wrappers.api_{data_type} import get_{data_type}
__all__ = [
# ... 其他导出 ...
"get_{data_type}",
]
```
## 4. 接口设计规范
### 4.1 数据获取函数签名要求
函数必须返回 `pd.DataFrame`,参数必须包含以下之一:
#### 4.1.1 按日期获取的接口(优先)
适用于:涨跌停、龙虎榜、筹码分布、每日指标等。
**函数签名要求**
```python
def get_{data_type}(
trade_date: Optional[str] = None,
start_date: Optional[str] = None,
end_date: Optional[str] = None,
ts_code: Optional[str] = None,
# 其他可选参数...
) -> pd.DataFrame:
```
**要求**
- 优先使用 `trade_date` 获取单日全市场数据
- 支持 `start_date + end_date` 获取区间数据
- `ts_code` 作为可选过滤参数
- **性能优势**: 单日全市场数据一次 API 调用即可完成
#### 4.1.2 按股票获取的接口
适用于:日线行情、资金流向等。
**函数签名要求**
```python
def get_{data_type}(
ts_code: str,
start_date: Optional[str] = None,
end_date: Optional[str] = None,
# 其他可选参数...
) -> pd.DataFrame:
```
**要求**
- `ts_code` 为必选参数
- 需要遍历所有股票获取全市场数据
### 4.2 文档字符串要求
函数必须包含 **Google 风格**的完整文档字符串,包含:
```python
def get_{data_type}(...) -> pd.DataFrame:
"""Fetch {数据描述} from Tushare.
This interface retrieves {详细描述}.
Args:
ts_code: Stock code (e.g., '000001.SZ', '600000.SH')
trade_date: Specific trade date (YYYYMMDD format)
start_date: Start date (YYYYMMDD format)
end_date: End date (YYYYMMDD format)
# 其他参数...
Returns:
pd.DataFrame with columns:
- ts_code: Stock code
- trade_date: Trade date (YYYYMMDD)
- {其他字段}: {字段描述}
Example:
>>> # Get single date data for all stocks
>>> data = get_{data_type}(trade_date='20240101')
>>>
>>> # Get date range data
>>> data = get_{data_type}(start_date='20240101', end_date='20240131')
>>>
>>> # Get specific stock data
>>> data = get_{data_type}(ts_code='000001.SZ', trade_date='20240101')
"""
```
### 4.3 日期格式要求
- 所有日期参数使用 **YYYYMMDD** 字符串格式
- 统一使用 `trade_date` 作为日期字段名
- 如果 API 返回其他日期字段名(如 `date``end_date`),必须在返回前重命名为 `trade_date`
```python
if "date" in data.columns:
data = data.rename(columns={"date": "trade_date"})
```
### 4.4 股票代码要求
- 统一使用 `ts_code` 作为股票代码字段名
- 格式:`{code}.{exchange}`,如 `000001.SZ``600000.SH`
- 确保返回的 DataFrame 包含 `ts_code`
### 4.5 字段名规范(重要)
**必须使用 Tushare API 返回的原始字段名,禁止进行不必要的重命名。**
这是为了确保:
- 代码可读性:使用 API 文档中的标准字段名
- 维护简单性:避免因字段名映射导致的混淆和错误
- 数据一致性:数据库字段名与 API 返回字段名保持一致
**禁止做法**(以下代码是不允许的):
```python
# 错误:将 Tushare 的原始字段名改为自定义名称
column_mapping = {
"turnover_rate": "tor", # 不要这样做
"volume_ratio": "vr", # 不要这样做
}
data = data.rename(columns=column_mapping)
```
**正确做法**(直接使用原始字段名):
```python
# 正确:保留 Tushare 返回的原始字段名
# Tushare 返回 'turnover_rate',就直接使用 'turnover_rate'
# Tushare 返回 'volume_ratio',就直接使用 'volume_ratio'
# 表结构定义应使用原始字段名
TABLE_SCHEMA = {
"ts_code": "VARCHAR(16) NOT NULL",
"trade_date": "DATE NOT NULL",
"turnover_rate": "DOUBLE", # 使用原始字段名
"volume_ratio": "DOUBLE", # 使用原始字段名
# ... 其他字段
}
```
**例外情况**(允许重命名):
- 日期字段:如果 API 返回 `date`,应重命名为 `trade_date` 以符合项目规范
- 必须重命名的情况:如果两个不同 API 返回相同含义但不同名称的字段,需要统一命名
**教训**(真实案例):
`api_pro_bar.py` 早期版本将 `turnover_rate` 重命名为 `tor``volume_ratio` 重命名为 `vr`
导致:
1. 代码与 Tushare 文档不一致,增加学习成本
2. 数据库字段名与 API 字段名不一致,造成混淆
3. 需要额外的数据迁移脚本修复历史数据
### 4.6 令牌桶限速要求
所有 API 调用必须通过 `TushareClient`,自动满足令牌桶限速要求。
#### 4.6.1 基本用法(单线程场景)
```python
from src.data.client import TushareClient
def get_{data_type}(...) -> pd.DataFrame:
client = TushareClient()
# Build parameters
params = {}
if trade_date:
params["trade_date"] = trade_date
if ts_code:
params["ts_code"] = ts_code
# ...
# Fetch data (rate limiting handled automatically)
data = client.query("{api_name}", **params)
return data
```
**注意**: `TushareClient` 自动处理:
- 令牌桶速率限制
- API 重试逻辑(指数退避)
- 配置加载
#### 4.6.2 多线程/并发场景(重要)
**问题**: 多线程并发调用时,如果每个线程创建独立的 `TushareClient` 实例,每个实例会有独立的限流器,导致实际并发请求数 = 线程数 × 单个限流器速率,**限流失效**。
**解决方案**: 数据获取函数必须接受可选的 `client` 参数Sync 类传递共享的客户端实例。
**数据获取函数签名**(必须支持 client 参数):
```python
from src.data.client import TushareClient
from typing import Optional
def get_{data_type}(
ts_code: str,
start_date: Optional[str] = None,
end_date: Optional[str] = None,
client: Optional[TushareClient] = None, # 新增:可选客户端参数
) -> pd.DataFrame:
"""Fetch {数据描述} from Tushare.
Args:
ts_code: Stock code
start_date: Start date (YYYYMMDD)
end_date: End date (YYYYMMDD)
client: Optional TushareClient instance for shared rate limiting.
If None, creates a new client. For concurrent sync operations,
pass a shared client to ensure proper rate limiting.
Returns:
pd.DataFrame with data
"""
client = client or TushareClient() # 如果没有提供则创建新实例
params = {"ts_code": ts_code}
if start_date:
params["start_date"] = start_date
if end_date:
params["end_date"] = end_date
data = client.query("{api_name}", **params)
return data
```
**Sync 类实现**(必须传递共享 client
```python
from concurrent.futures import ThreadPoolExecutor
from src.data.client import TushareClient
from src.data.storage import ThreadSafeStorage
class {DataType}Sync:
def __init__(self, max_workers: Optional[int] = None):
self.storage = ThreadSafeStorage()
self.client = TushareClient() # 共享客户端实例
self.max_workers = max_workers or 10
def sync_single_stock(
self,
ts_code: str,
start_date: str,
end_date: str,
) -> pd.DataFrame:
"""同步单只股票的数据。"""
# 传递共享 client 以确保多线程下的限流生效
data = get_{data_type}(
ts_code=ts_code,
start_date=start_date,
end_date=end_date,
client=self.client, # 关键:传递共享客户端
)
return data
def sync_all(self, ...):
# 使用 ThreadPoolExecutor 并发执行
with ThreadPoolExecutor(max_workers=self.max_workers) as executor:
# 所有线程共享 self.client限流器正常工作
...
```
**关键规则**:
1. 所有按股票获取的接口必须接受 `client: Optional[TushareClient] = None` 参数
2. Sync 类在 `__init__` 中创建 `self.client = TushareClient()`
3. Sync 类的同步方法必须将 `self.client` 传递给数据获取函数
4. 数据获取函数使用 `client = client or TushareClient()` 模式
所有 API 调用必须通过 `TushareClient`,自动满足令牌桶限速要求:
```python
from src.data.client import TushareClient
def get_{data_type}(...) -> pd.DataFrame:
client = TushareClient()
# Build parameters
params = {}
if trade_date:
params["trade_date"] = trade_date
if ts_code:
params["ts_code"] = ts_code
# ...
# Fetch data (rate limiting handled automatically)
data = client.query("{api_name}", **params)
return data
```
**注意**: `TushareClient` 自动处理:
- 令牌桶速率限制
- API 重试逻辑(指数退避)
- 配置加载
## 5. DuckDB 存储规范
### 5.0 强制落库要求(关键)
**所有封装的 API 接口必须将数据落库到 DuckDB。**
这是数据同步的核心原则,确保:
- 数据持久化:避免重复调用 API节省 token
- 增量更新:基于本地数据状态进行智能同步
- 数据一致性:所有数据都有统一的存储和访问方式
- 离线可用:数据可以在没有网络的情况下查询
**落库检查清单**
- [ ]`storage.py``_init_db()` 方法中创建对应的表
- [ ] 表结构必须包含 `ts_code``trade_date` 作为主键
- [ ] 实现 `sync_{data_type}()` 函数,使用 `Storage``ThreadSafeStorage` 保存数据
- [ ] 确保同步逻辑正确处理增量更新
**反例警示**`api_pro_bar.py` 早期版本虽然实现了 `sync_pro_bar()` 函数,但忘记在 `storage.py` 中创建 `pro_bar` 表,导致同步的数据无法落库,造成 token 浪费和数据丢失。
### 5.1 存储架构
### 5.1 存储架构
项目使用 **DuckDB** 作为持久化存储:
- **单例模式**: `Storage` 类确保单一数据库连接
- **线程安全**: `ThreadSafeStorage` 提供并发写入支持
- **UPSERT 支持**: `INSERT OR REPLACE` 自动处理重复数据
- **查询下推**: WHERE 条件在数据库层过滤
### 5.2 表结构设计
每个数据类型对应一个 DuckDB 表:
```sql
CREATE TABLE {data_type} (
ts_code VARCHAR(16) NOT NULL,
trade_date DATE NOT NULL,
# ...
PRIMARY KEY (ts_code, trade_date)
);
CREATE INDEX idx_{data_type}_date_code ON {data_type}(trade_date, ts_code);
```
**主键要求**:
- 必须包含 `ts_code``trade_date`
- 使用 UPSERT 确保幂等性
### 5.3 存储写入策略
**批量写入模式**(推荐用于多线程场景):
```python
from src.data.storage import ThreadSafeStorage
def sync_{data_type}(self, ...):
storage = ThreadSafeStorage()
# 收集数据到队列(不立即写入)
for data_chunk in data_generator:
storage.queue_save("{data_type}", data_chunk)
# 批量写入所有数据
storage.flush()
```
**直接写入模式**(适用于简单场景):
```python
from src.data.storage import Storage
storage = Storage()
storage.save("{data_type}", data, mode="append")
```
### 5.4 数据类型映射
标准字段类型映射(`DEFAULT_TYPE_MAPPING`
```python
DEFAULT_TYPE_MAPPING = {
"ts_code": "VARCHAR(16)",
"trade_date": "DATE",
"open": "DOUBLE",
"high": "DOUBLE",
"low": "DOUBLE",
"close": "DOUBLE",
"vol": "DOUBLE",
"amount": "DOUBLE",
# ... 其他字段
}
```
## 6. Sync 集成规范
### 6.1 使用 db_manager 进行同步
项目使用 `db_manager` 模块提供高级同步功能:
```python
from src.data.db_manager import SyncManager, ensure_table
def sync_{data_type}(force_full: bool = False) -> pd.DataFrame:
"""Sync {数据描述} to DuckDB."""
manager = SyncManager()
# 确保表存在
ensure_table("{data_type}", schema={
"ts_code": "VARCHAR(16)",
"trade_date": "DATE",
# ... 其他字段
})
# 执行同步
result = manager.sync(
table_name="{data_type}",
fetch_func=get_{data_type},
start_date=start_date,
end_date=end_date,
force_full=force_full,
)
return result
```
### 6.2 增量更新逻辑
`SyncManager` 自动处理增量更新:
1. **检查本地最新日期**: 从 DuckDB 获取 `MAX(trade_date)`
2. **获取交易日历**: 从 `api_trade_cal` 获取交易日范围
3. **计算需要同步的日期**: 本地最新日期 + 1 到最新交易日
4. **批量获取数据**: 按日期或按股票获取
5. **批量写入**: 使用 `ThreadSafeStorage` 队列写入
### 6.3 便捷函数
每个接口必须提供顶层便捷函数:
```python
def sync_{data_type}(force_full: bool = False) -> pd.DataFrame:
"""Sync {数据描述} to local storage.
Args:
force_full: If True, force full reload from 20180101
Returns:
DataFrame with synced data
"""
# Implementation...
```
## 7. 代码模板
### 7.1 按日期获取接口模板
```python
"""{数据描述} interface.
Fetch {数据描述} data from Tushare.
"""
import pandas as pd
from typing import Optional
from src.data.client import TushareClient
def get_{data_type}(
trade_date: Optional[str] = None,
start_date: Optional[str] = None,
end_date: Optional[str] = None,
ts_code: Optional[str] = None,
client: Optional[TushareClient] = None, # 关键:可选客户端参数,用于共享速率限制
) -> pd.DataFrame:
"""Fetch {数据描述} from Tushare.
This interface retrieves {详细描述}.
Args:
trade_date: Specific trade date (YYYYMMDD format)
start_date: Start date (YYYYMMDD format)
end_date: End date (YYYYMMDD format)
ts_code: Stock code filter (optional)
client: Optional TushareClient instance for shared rate limiting.
If None, creates a new client. For concurrent sync operations,
pass a shared client to ensure proper rate limiting.
Returns:
pd.DataFrame with columns:
- ts_code: Stock code
- trade_date: Trade date (YYYYMMDD)
- {字段1}: {描述}
- {字段2}: {描述}
Example:
>>> # Get all stocks for a single date
>>> data = get_{data_type}(trade_date='20240101')
>>>
>>> # Get date range data
>>> data = get_{data_type}(start_date='20240101', end_date='20240131')
"""
client = client or TushareClient() # 如果没有提供则创建新实例
# Build parameters
params = {}
if trade_date:
params["trade_date"] = trade_date
if start_date:
params["start_date"] = start_date
if end_date:
params["end_date"] = end_date
if ts_code:
params["ts_code"] = ts_code
# Fetch data
data = client.query("{tushare_api_name}", **params)
# Rename date column if needed
if "date" in data.columns:
data = data.rename(columns={"date": "trade_date"})
return data
```
### 7.2 按股票获取接口模板
```python
"""{数据描述} interface.
Fetch {数据描述} data from Tushare (per stock).
"""
import pandas as pd
from typing import Optional
from src.data.client import TushareClient
def get_{data_type}(
ts_code: str,
start_date: Optional[str] = None,
end_date: Optional[str] = None,
client: Optional[TushareClient] = None, # 关键:可选客户端参数,用于共享速率限制
) -> pd.DataFrame:
"""Fetch {数据描述} for a specific stock.
Args:
ts_code: Stock code (e.g., '000001.SZ')
start_date: Start date (YYYYMMDD format)
end_date: End date (YYYYMMDD format)
client: Optional TushareClient instance for shared rate limiting.
If None, creates a new client. For concurrent sync operations,
pass a shared client to ensure proper rate limiting.
Returns:
pd.DataFrame with {数据描述} data
"""
client = client or TushareClient() # 如果没有提供则创建新实例
params = {"ts_code": ts_code}
if start_date:
params["start_date"] = start_date
if end_date:
params["end_date"] = end_date
data = client.query("{tushare_api_name}", **params)
return data
```
### 7.3 Sync 函数模板
```python
from src.data.db_manager import SyncManager, ensure_table
from src.data.api_wrappers import get_{data_type}
def sync_{data_type}(force_full: bool = False) -> pd.DataFrame:
"""Sync {数据描述} to local DuckDB storage.
Args:
force_full: If True, force full reload from 20180101
Returns:
DataFrame with synced data
"""
manager = SyncManager()
# Ensure table exists with proper schema
ensure_table("{data_type}", schema={
"ts_code": "VARCHAR(16)",
"trade_date": "DATE",
# Add other fields...
})
# Perform sync
result = manager.sync(
table_name="{data_type}",
fetch_func=get_{data_type},
force_full=force_full,
)
return result
```
## 8. 测试规范
### 8.1 测试文件要求
必须创建对应的测试文件:`tests/test_{data_type}.py`
### 8.2 测试覆盖要求
```python
import pytest
import pandas as pd
from unittest.mock import patch, MagicMock
from src.data.api_wrappers.api_{data_type} import get_{data_type}
class Test{DataType}:
"""Test suite for {data_type} API wrapper."""
@patch("src.data.api_wrappers.api_{data_type}.TushareClient")
def test_get_by_date(self, mock_client_class):
"""Test fetching data by date."""
# Setup mock
mock_client = MagicMock()
mock_client_class.return_value = mock_client
mock_client.query.return_value = pd.DataFrame({
"ts_code": ["000001.SZ"],
"trade_date": ["20240101"],
# ... other columns
})
# Test
result = get_{data_type}(trade_date="20240101")
# Assert
assert not result.empty
assert "ts_code" in result.columns
assert "trade_date" in result.columns
mock_client.query.assert_called_once()
@patch("src.data.api_wrappers.api_{data_type}.TushareClient")
def test_get_by_stock(self, mock_client_class):
"""Test fetching data by stock code."""
# Similar setup...
pass
@patch("src.data.api_wrappers.api_{data_type}.TushareClient")
def test_empty_response(self, mock_client_class):
"""Test handling empty response."""
mock_client = MagicMock()
mock_client_class.return_value = mock_client
mock_client.query.return_value = pd.DataFrame()
result = get_{data_type}(trade_date="20240101")
assert result.empty
```
### 8.3 Mock 规范
- 在导入位置打补丁:`patch('src.data.api_wrappers.api_{data_type}.TushareClient')`
- 测试正常和异常情况
- 验证参数传递正确
## 9. 使用 Skill 自动生成
### 9.1 准备工作
1.`api.md` 中定义接口信息,包含:
- 接口名称和描述
- 输入参数(名称、类型、必选、描述)
- 输出参数(名称、类型、描述)
### 9.2 调用 Skill
告知 Claude 要封装的接口名称:
> "帮我封装 {data_type} 接口"
> "为 {data_type} 接口生成代码"
### 9.3 自动生成内容
Skill 会自动:
1. 解析 `api.md` 中的接口定义
2. 生成 `src/data/api_wrappers/api_{data_type}.py`
3. 更新 `src/data/api_wrappers/__init__.py` 导出
4. 生成 `tests/test_{data_type}.py` 测试文件
5. 提供 `sync_{data_type}()` 函数模板
## 10. 检查清单
### 10.1 文件结构
- [ ] 文件位于 `src/data/api_wrappers/api_{data_type}.py`
- [ ] 已更新 `src/data/api_wrappers/__init__.py` 导出公共接口
- [ ] 已创建 `tests/test_{data_type}.py` 测试文件
### 10.2 接口实现
- [ ] 数据获取函数使用 `TushareClient`
- [ ] **关键**:数据获取函数接受 `client: Optional[TushareClient] = None` 参数用于共享速率限制
- [ ] **关键**Sync 类在 `fetch_single_date()` / `fetch_single_stock()` 中传递 `self.client`
- [ ] 函数包含完整的 Google 风格文档字符串
- [ ] 日期参数使用 `YYYYMMDD` 格式
- [ ] 返回的 DataFrame 包含 `ts_code``trade_date` 字段
- [ ] 优先实现按日期获取的接口(如果 API 支持)
- [ ] 参数传递前检查是否为 None
### 10.3 存储集成
- [ ] 使用 `Storage``ThreadSafeStorage` 进行数据存储
- [ ] 表结构包含 `ts_code``trade_date` 作为主键
- [ ] 使用 UPSERT 模式(`INSERT OR REPLACE`
- [ ] 多线程场景使用 `queue_save()` + `flush()` 模式
### 10.4 Sync 集成
- [ ] 使用 `db_manager` 模块进行同步管理
- [ ] 实现 `sync_{data_type}()` 便捷函数
- [ ] 支持 `force_full` 参数
- [ ] 增量更新逻辑正确
### 10.5 测试
- [ ] 已编写单元测试
- [ ] 已 mock `TushareClient`
- [ ] 测试覆盖按日期和按股票获取
- [ ] 测试覆盖正常和异常情况
## 11. 示例参考
### 11.1 完整示例api_pro_bar.py
参见 `src/data/api_wrappers/api_pro_bar.py` - 按股票获取 Pro Bar 行情数据的完整实现(主力行情表)。
### 11.2 完整示例api_trade_cal.py
参见 `src/data/api_wrappers/api_trade_cal.py` - 特殊接口(交易日历)的实现,包含 HDF5 缓存逻辑。
### 11.3 完整示例api_stock_basic.py
参见 `src/data/api_wrappers/api_stock_basic.py` - 特殊接口(股票基础信息)的实现,包含 CSV 存储逻辑。
---
**最后更新**: 2026-03-26
**版本**: v2.1 - 更新速率限制规范,强调多线程场景下 client 参数传递