1
0
Files
2026-01-27 18:30:41 +08:00

281 lines
10 KiB
Markdown
Raw Permalink 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.
# Spec: Data Fetching
## ADDED Requirements
### Requirement: 数据库连接配置
系统 SHALL 通过硬编码常量管理数据库连接参数(开发环境)。
#### Scenario: 使用硬编码常量
- **WHEN** 系统在 backtest.py 中定义数据库配置
- **THEN** 系统定义 DB_HOST, DB_NAME, DB_USER, DB_PASSWORD 常量
- **THEN** DB_HOST 值 SHALL 为数据库主机地址(如 '81.71.3.24'
- **THEN** DB_NAME 值 SHALL 为数据库名称(如 'leopard_dev'
- **THEN** DB_USER 值 SHALL 为数据库用户名
- **THEN** DB_PASSWORD 值 SHALL 为数据库密码
#### Scenario: 构建连接字符串
- **WHEN** 系统创建 SQLAlchemy 连接
- **THEN** 系统使用硬编码的常量构建连接字符串
- **THEN** 连接字符串格式 SHALL 为 `postgresql://{user}:{password}@{host}/{database}`
- **THEN** 不从环境变量读取任何凭证
#### Scenario: 修改数据库凭证
- **WHEN** 开发人员需要更换数据库或凭证
- **THEN** 开发人员直接修改 backtest.py 中的常量值
- **THEN** 修改后脚本使用新凭证连接数据库
---
### Requirement: 数据库连接建立
系统 SHALL 使用 SQLAlchemy 创建 PostgreSQL 数据库连接。
#### Scenario: 成功建立连接
- **WHEN** 凭证正确且数据库可访问
- **THEN** 系统使用 `sqlalchemy.create_engine(conn_str)` 创建引擎
- **THEN** 连接字符串格式 SHALL 为 `postgresql://{user}:{password}@{host}/{database}`
- **THEN** 系统成功创建引擎对象
- **THEN** 系统可用于执行查询
#### Scenario: 连接字符串构建
- **WHEN** 系统构建 PostgreSQL 连接字符串
- **THEN** 连接字符串 SHALL 正确编码特殊字符(密码中的 @, : 等)
- **THEN** 连接字符串 SHALL 使用标准 URI 格式
- **THEN** 连接字符串 SHALL 不包含额外选项(仅基础连接参数)
#### Scenario: 数据库连接失败
- **WHEN** 凭证错误或数据库不可达
- **THEN** SQLAlchemy 抛出 `sqlalchemy.exc.OperationalError`
- **THEN** 主流程捕获异常
- **THEN** 系统输出错误信息:"数据库连接失败: {error}"
- **THEN** 系统退出并返回状态码 2
#### Scenario: 连接池管理
- **WHEN** 系统创建引擎对象
- **THEN** SQLAlchemy SHALL 自动管理连接池
- **THEN** 查询后连接 SHALL 自动返回池中
- **THEN** 系统 SHALL 在查询完成后调用 `engine.dispose()` 清理
---
### Requirement: SQL 查询构建
系统 SHALL 构建参数化的 SQL 查询以获取股票历史数据。
#### Scenario: 基础查询结构
- **WHEN** 系统构建查询
- **THEN** 查询 SHALL 选择 trade_date, Open, High, Low, Close, Volume, factor
- **THEN** 查询 SHALL 连接 leopard_daily 和 leopard_stock 表
- **THEN** 查询 SHALL 按 stock.code 过滤
- **THEN** 查询 SHALL 按 trade_date 范围过滤
- **THEN** 查询 SHALL 按 trade_date 升序排序
#### Scenario: 复权价格计算
- **WHEN** 系统计算复权价格
- **THEN** Open SHALL 计算为 `open * factor`
- **THEN** Close SHALL 计算为 `close * factor`
- **THEN** High SHALL 计算为 `high * factor`
- **THEN** Low SHALL 计算为 `low * factor`
- **THEN** Volume SHALL 直接使用原始值(不复权)
- **THEN** factor SHALL 使用 `COALESCE(factor, 1.0)` 处理 NULL 值
#### Scenario: 参数化股票代码
- **WHEN** 用户指定股票代码(如 '000001.SZ'
- **THEN** 查询 WHERE 子句 SHALL 使用 `stock.code = '{code}'`
- **THEN** 代码 SHALL 精确匹配(不使用 LIKE
- **THEN** 查询 SHALL 返回匹配股票的所有日线数据
#### Scenario: 参数化日期范围
- **WHEN** 用户指定开始日期 '2024-01-01' 和结束日期 '2025-12-31'
- **THEN** 查询 WHERE 子句 SHALL 使用 `BETWEEN '{start_date} 00:00:00' AND '{end_date} 23:59:59'`
- **THEN** 00:00:00 和 23:59:59 SHALL 覆盖全天
- **THEN** 日期格式 SHALL 为 YYYY-MM-DD HH:MM:SS
#### Scenario: 完整 SQL 查询
- **WHEN** 系统执行数据加载
- **THEN** 查询 SHALL 为:
```sql
SELECT
trade_date,
open * factor AS Open,
close * factor AS Close,
high * factor AS High,
low * factor AS Low,
volume AS Volume,
COALESCE(factor, 1.0) AS factor
FROM leopard_daily daily
LEFT JOIN leopard_stock stock ON stock.id = daily.stock_id
WHERE stock.code = '{code}'
AND daily.trade_date BETWEEN '{start_date} 00:00:00'
AND '{end_date} 23:59:59'
ORDER BY daily.trade_date
```
---
### Requirement: 数据查询执行
系统 SHALL 使用 pandas 的 `read_sql` 函数执行 SQL 查询并返回 DataFrame。
#### Scenario: 成功执行查询
- **WHEN** SQL 查询有效且数据存在
- **THEN** 系统调用 `pd.read_sql(query, engine)`
- **THEN** 系统返回 DataFrame 对象
- **THEN** DataFrame SHALL 包含查询结果的所有列
- **THEN** DataFrame 行数 SHALL 匹配数据库返回的记录数
#### Scenario: 数据类型处理
- **WHEN** pandas 读取 SQL 结果
- **THEN** trade_date SHALL 自动转换为 datetime 类型
- **THEN** Open, High, Low, Close, Volume SHALL 为 float 类型
- **THEN** factor SHALL 为 float 类型
- **THEN** 系统不需要手动类型转换(除日期索引设置)
#### Scenario: 查询返回空结果
- **WHEN** 指定股票代码或日期范围无数据
- **THEN** `read_sql` 返回空 DataFrame0 行)
- **THEN** 系统检查 `len(df) == 0`
- **THEN** 系统抛出 ValueError: "未找到股票 {code} 在指定时间范围内的数据"
#### Scenario: SQL 语法错误
- **WHEN** SQL 查询包含语法错误
- **THEN** SQLAlchemy 抛出 `sqlalchemy.exc.ProgrammingError`
- **THEN** 主流程捕获异常
- **THEN** 系统输出错误信息:"SQL 查询错误: {error}"
- **THEN** 系统退出并返回状态码 2
---
### Requirement: 数据格式转换
系统 SHALL 将查询结果转换为 backtesting 库要求的格式。
#### Scenario: 设置日期索引
- **WHEN** DataFrame 加载完成
- **THEN** 系统调用 `df.set_index('trade_date', inplace=True)`
- **THEN** DataFrame 的索引 SHALL 为 DatetimeIndex
- **THEN** 索引 SHALL 不再是数值索引
- **THEN** backtesting 库 SHALL 能正确处理日期范围
#### Scenario: 列名格式化
- **WHEN** DataFrame 加载完成
- **THEN** 列名 SHALL 为 ['Open', 'High', 'Low', 'Close', 'Volume', 'factor']
- **THEN** 列名 SHALL 遵循 backtesting 库要求(首字母大写)
- **THEN** 列名 SHALL 与 SQL 查询中的别名一致
#### Scenario: 数据验证
- **WHEN** 系统准备返回 DataFrame
- **THEN** 系统验证 DataFrame 包含必需列
- **THEN** 系统验证 'Open', 'High', 'Low', 'Close', 'Volume' 列存在
- **THEN** 系统验证索引为 DatetimeIndex
- **WHEN** 验证失败
- **THEN** 系统抛出 ValueError: "数据格式不符合要求"
---
### Requirement: 数据清理
系统 SHALL 清理数据以确保回测质量。
#### Scenario: 删除 NULL 值行
- **WHEN** DataFrame 包含 NULL 或 NaN 值
- **THEN** 系统调用 `df.dropna()` 删除
- **THEN** 任何包含 NaN 的行 SHALL 被删除
- **THEN** 返回的 DataFrame SHALL 不包含 NULL 值
#### Scenario: 数据完整性检查
- **WHEN** DataFrame 加载完成
- **THEN** 系统检查 trade_date 连续性
- **THEN** 系统检查无重复日期
- **WHEN** 发现异常
- **THEN** 系统输出警告:"数据存在异常: {detail}"
#### Scenario: 最小数据量验证
- **WHEN** DataFrame 行数少于 10
- **THEN** 系统输出错误:"数据不足,至少需要 10 天数据"
- **THEN** 系统抛出 ValueError
- **THEN** 主流程捕获并退出
---
### Requirement: 资源管理
系统 SHALL 正确管理数据库连接和内存资源。
#### Scenario: 引擎创建和清理
- **WHEN** 系统开始数据加载
- **THEN** 系统创建 SQLAlchemy 引擎对象
- **THEN** 系统使用引擎执行查询
- **WHEN** 查询完成
- **THEN** 系统调用 `engine.dispose()` 关闭连接池
- **THEN** 系统释放所有数据库连接
#### Scenario: 异常情况下的资源清理
- **WHEN** 查询过程中抛出异常
- **THEN** 系统在 finally 块中调用 `engine.dispose()`
- **THEN** 所有连接 SHALL 被正确关闭
- **THEN** 系统不会泄漏数据库连接
---
### Requirement: 错误处理和日志
系统 SHALL 提供清晰的错误信息和调试支持。
#### Scenario: 连接错误信息
- **WHEN** 数据库连接失败
- **THEN** 错误信息 SHALL 包含数据库主机和端口
- **THEN** 错误信息 SHALL 区分网络错误和认证错误
- **THEN** 系统提示用户检查凭证和网络连接
#### Scenario: 查询错误信息
- **WHEN** SQL 查询失败
- **THEN** 错误信息 SHALL 包含失败的 SQL 语句
- **THEN** 错误信息 SHALL 包含数据库返回的错误详情
- **THEN** 系统提示用户检查表结构和数据
#### Scenario: 数据格式错误信息
- **WHEN** 返回的 DataFrame 不符合要求
- **THEN** 错误信息 SHALL 列出缺失的列
- **THEN** 错误信息 SHALL 提示期望的格式
- **THEN** 系统建议用户检查数据库表结构
---
### Requirement: 函数接口
`load_data_from_db` 函数 SHALL 提供清晰的调用接口。
#### Scenario: 函数签名
- **WHEN** 主流程调用 `load_data_from_db(code, start_date, end_date)`
- **THEN** 函数接收三个字符串参数
- **THEN** `code` 为股票代码(如 '000001.SZ'
- **THEN** `start_date` 为开始日期(如 '2024-01-01'
- **THEN** `end_date` 为结束日期(如 '2025-12-31'
#### Scenario: 返回值
- **WHEN** 数据加载成功
- **THEN** 函数返回 pandas.DataFrame
- **THEN** DataFrame 索引为 DatetimeIndextrade_date
- **THEN** DataFrame 包含 ['Open', 'High', 'Low', 'Close', 'Volume', 'factor'] 列
#### Scenario: 异常抛出
- **WHEN** 数据加载失败
- **THEN** 函数 SHALL 抛出异常(不捕获)
- **THEN** 异常类型 SHALL 为 ValueError业务逻辑错误
- **THEN** 主流程负责捕获和处理异常
---
### Requirement: 性能考虑
系统 SHALL 优化数据加载性能以支持大数据集。
#### Scenario: 使用 pandas 向量化操作
- **WHEN** 执行复权计算
- **THEN** 计算 SHALL 使用 pandas 向量化操作
- **THEN** 不使用循环逐行计算
- **THEN** 10 年数据(约 2500 行) SHALL 在 1 秒内加载
#### Scenario: 索引优化
- **WHEN** 设置 DataFrame 索引
- **THEN** `set_index()` 操作 SHALL 高效(使用底层数组拷贝)
- **THEN** 日期索引 SHALL 支持快速范围查询
#### Scenario: 内存管理
- **WHEN** 加载大数据集
- **THEN** 系统 SHALL 及时调用 `engine.dispose()` 释放连接
- **THEN** DataFrame SHALL 使用 pandas 内部优化存储
- **THEN** 内存占用 SHALL 合理10 年数据约几 MB