# 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）
