- 删除 src/data/api_wrappers/api_daily.py (240行) - 更新 6 个文档文件,将 daily 表引用替换为 pro_bar - 同步 README.md 中的因子框架和训练模块示例 BREAKING CHANGE: api_daily 模块已移除,请使用 api_pro_bar 替代
22 KiB
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 中导出:
from src.data.api_wrappers.api_{data_type} import get_{data_type}
__all__ = [
# ... 其他导出 ...
"get_{data_type}",
]
4. 接口设计规范
4.1 数据获取函数签名要求
函数必须返回 pd.DataFrame,参数必须包含以下之一:
4.1.1 按日期获取的接口(优先)
适用于:涨跌停、龙虎榜、筹码分布、每日指标等。
函数签名要求:
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 按股票获取的接口
适用于:日线行情、资金流向等。
函数签名要求:
def get_{data_type}(
ts_code: str,
start_date: Optional[str] = None,
end_date: Optional[str] = None,
# 其他可选参数...
) -> pd.DataFrame:
要求:
ts_code为必选参数- 需要遍历所有股票获取全市场数据
4.2 文档字符串要求
函数必须包含 Google 风格的完整文档字符串,包含:
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:
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 返回字段名保持一致
禁止做法(以下代码是不允许的):
# 错误:将 Tushare 的原始字段名改为自定义名称
column_mapping = {
"turnover_rate": "tor", # 不要这样做
"volume_ratio": "vr", # 不要这样做
}
data = data.rename(columns=column_mapping)
正确做法(直接使用原始字段名):
# 正确:保留 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,
导致:
- 代码与 Tushare 文档不一致,增加学习成本
- 数据库字段名与 API 字段名不一致,造成混淆
- 需要额外的数据迁移脚本修复历史数据
4.6 令牌桶限速要求
所有 API 调用必须通过 TushareClient,自动满足令牌桶限速要求。
4.6.1 基本用法(单线程场景)
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 参数):
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):
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,限流器正常工作
...
关键规则:
- 所有按股票获取的接口必须接受
client: Optional[TushareClient] = None参数 - Sync 类在
__init__中创建self.client = TushareClient() - Sync 类的同步方法必须将
self.client传递给数据获取函数 - 数据获取函数使用
client = client or TushareClient()模式
所有 API 调用必须通过 TushareClient,自动满足令牌桶限速要求:
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 表:
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 存储写入策略
批量写入模式(推荐用于多线程场景):
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()
直接写入模式(适用于简单场景):
from src.data.storage import Storage
storage = Storage()
storage.save("{data_type}", data, mode="append")
5.4 数据类型映射
标准字段类型映射(DEFAULT_TYPE_MAPPING):
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 模块提供高级同步功能:
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 自动处理增量更新:
- 检查本地最新日期: 从 DuckDB 获取
MAX(trade_date) - 获取交易日历: 从
api_trade_cal获取交易日范围 - 计算需要同步的日期: 本地最新日期 + 1 到最新交易日
- 批量获取数据: 按日期或按股票获取
- 批量写入: 使用
ThreadSafeStorage队列写入
6.3 便捷函数
每个接口必须提供顶层便捷函数:
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 按日期获取接口模板
"""{数据描述} 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,
) -> 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)
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 = 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 按股票获取接口模板
"""{数据描述} 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,
) -> 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)
Returns:
pd.DataFrame with {数据描述} data
"""
client = 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 函数模板
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 测试覆盖要求
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 准备工作
- 在
api.md中定义接口信息,包含:- 接口名称和描述
- 输入参数(名称、类型、必选、描述)
- 输出参数(名称、类型、描述)
9.2 调用 Skill
告知 Claude 要封装的接口名称:
"帮我封装 {data_type} 接口"
"为 {data_type} 接口生成代码"
9.3 自动生成内容
Skill 会自动:
- 解析
api.md中的接口定义 - 生成
src/data/api_wrappers/api_{data_type}.py - 更新
src/data/api_wrappers/__init__.py导出 - 生成
tests/test_{data_type}.py测试文件 - 提供
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 - 函数包含完整的 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-02-23
版本: v2.0 - 更新 DuckDB 存储规范,添加 Skill 自动化说明