10 KiB
10 KiB
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 为:
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返回空 DataFrame(0 行) - 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 索引为 DatetimeIndex(trade_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)