Files
ProStock/docs/api/API_INTERFACE_SPEC.md
liaozhaorun a22bc2d282 refactor(data): 移除 api_daily 模块并更新文档
- 删除 src/data/api_wrappers/api_daily.py (240行)
- 更新 6 个文档文件,将 daily 表引用替换为 pro_bar
- 同步 README.md 中的因子框架和训练模块示例

BREAKING CHANGE: api_daily 模块已移除,请使用 api_pro_bar 替代
2026-03-14 01:48:56 +08:00

22 KiB
Raw Blame History

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.pyapi_moneyflow.pyapi_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 返回其他日期字段名(如 dateend_date),必须在返回前重命名为 trade_date
if "date" in data.columns:
    data = data.rename(columns={"date": "trade_date"})

4.4 股票代码要求

  • 统一使用 ts_code 作为股票代码字段名
  • 格式:{code}.{exchange},如 000001.SZ600000.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 重命名为 torvolume_ratio 重命名为 vr 导致:

  1. 代码与 Tushare 文档不一致,增加学习成本
  2. 数据库字段名与 API 字段名不一致,造成混淆
  3. 需要额外的数据迁移脚本修复历史数据

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限流器正常工作
            ...

关键规则:

  1. 所有按股票获取的接口必须接受 client: Optional[TushareClient] = None 参数
  2. Sync 类在 __init__ 中创建 self.client = TushareClient()
  3. Sync 类的同步方法必须将 self.client 传递给数据获取函数
  4. 数据获取函数使用 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_codetrade_date 作为主键
  • 实现 sync_{data_type}() 函数,使用 StorageThreadSafeStorage 保存数据
  • 确保同步逻辑正确处理增量更新

反例警示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_codetrade_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 自动处理增量更新:

  1. 检查本地最新日期: 从 DuckDB 获取 MAX(trade_date)
  2. 获取交易日历: 从 api_trade_cal 获取交易日范围
  3. 计算需要同步的日期: 本地最新日期 + 1 到最新交易日
  4. 批量获取数据: 按日期或按股票获取
  5. 批量写入: 使用 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 准备工作

  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
  • 函数包含完整的 Google 风格文档字符串
  • 日期参数使用 YYYYMMDD 格式
  • 返回的 DataFrame 包含 ts_codetrade_date 字段
  • 优先实现按日期获取的接口(如果 API 支持)
  • 参数传递前检查是否为 None

10.3 存储集成

  • 使用 StorageThreadSafeStorage 进行数据存储
  • 表结构包含 ts_codetrade_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 自动化说明