# 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` 返回空 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)