# K线数据与指标数据存储方案分析

## 当前问题

### 数据特征
- **K线数据**（价格+成交量）：daily/weekly/monthly 三个周期，可以增量更新
- **指标数据**（PE/PB/换手率等）：只有 daily 周期，需要前向填充到 weekly/monthly
- **数据依赖**：daily_basic 数据为空时，整个股票的所有周期数据都无法保存

### 当前问题
1. **0值问题**：指标数据在历史日期（如2008-2019年）为0，导致：
   - K线不能完全 incremental（因为有0值）
   - 或者 K线 incremental 但需要对 daily_basic 做 rolling（确保指标数据覆盖）

2. **数据冗余**：指标数据是日线的，但周线和月线也缓存了一份

3. **更新策略冲突**：
   - K线适合 incremental（每天更新）
   - 指标可能需要 rolling（滚动刷新最近N天，因为某些指标可能滞后更新）

---

## 方案对比

### 方案1：当前实现（合并handler，同表存储）

**实现方式**：
- 一个 handler 请求 3 个 term 的 K 线 + daily_basic 指标
- 合并后存储到 `stock_kline` 表
- 指标数据前向填充到周线和月线

**优点**：
- ✅ **数据联动**：一次查询能拿到 K 线的所有价格数据和常用指标数据
- ✅ **日期连贯**：要么都有数据，要么都没有数据，不会出现有 K 线没指标的情况
- ✅ **查询性能**：不用 JOIN，读更快
- ✅ **常用指标预计算**：指标数据在获取时自动计算在周线和月线周期上，取的时候不用计算
- ✅ **API 调用优化**：daily_basic 只调用一次，所有周期共享

**缺点**：
- ❌ **0值问题**：历史日期的指标为0，导致 K 线不能完全 incremental
- ❌ **数据冗余**：指标数据在周线和月线也缓存了一份（虽然是日线数据）
- ❌ **更新策略冲突**：K 线适合 incremental，但指标可能需要 rolling
- ❌ **依赖性强**：daily_basic 失败会导致整个股票的所有周期数据无法保存

**适用场景**：
- 查询时总是需要同时使用价格和指标
- 指标数据更新频率与 K 线一致
- 可以接受历史日期的指标为0

---

### 方案2：拆分handler，同表存储

**实现方式**：
- `kline_handler`：只处理 K 线数据（daily/weekly/monthly）
- `daily_basic_handler`：只处理指标数据（daily）
- 两个 handler 都写入 `stock_kline` 表
- 查询时自动 JOIN 或合并

**优点**：
- ✅ **更新策略独立**：K 线可以用 incremental，指标可以用 rolling
- ✅ **数据联动保留**：仍然在同一张表，查询时不需要 JOIN
- ✅ **日期连贯**：通过 upsert 机制，确保数据一致性
- ✅ **解耦**：daily_basic 失败不影响 K 线数据保存

**缺点**：
- ⚠️ **写入冲突**：两个 handler 同时写入同一张表，需要处理并发和冲突
- ⚠️ **数据一致性**：可能出现有 K 线没指标，或有指标没 K 线的情况（需要处理）
- ⚠️ **查询逻辑复杂**：需要判断哪些字段来自哪个 handler，可能需要合并逻辑
- ⚠️ **0值问题仍然存在**：历史日期的指标仍然为0

**实现复杂度**：⭐⭐⭐⭐（高）
- 需要处理并发写入
- 需要处理数据合并逻辑
- 需要处理字段映射

---

### 方案3：分表存储

**实现方式**：
- `stock_kline` 表：只存储 K 线数据（价格+成交量）
- `stock_daily_basic` 表：只存储指标数据（PE/PB/换手率等）
- 两个独立的 handler，各自管理自己的表
- 查询时需要 JOIN

**优点**：
- ✅ **完全解耦**：K 线和指标完全独立，互不影响
- ✅ **更新策略独立**：K 线可以用 incremental，指标可以用 rolling
- ✅ **无0值问题**：K 线表不存指标，可以完全 incremental
- ✅ **无数据冗余**：指标只存日线，周线和月线需要时再计算
- ✅ **灵活性高**：可以独立扩展指标字段，不影响 K 线表结构

**缺点**：
- ❌ **查询性能**：需要 JOIN，读更慢
- ❌ **数据不一致**：可能出现有 K 线没指标，或有指标没 K 线的情况
- ❌ **计算开销**：周线和月线的指标需要实时计算（从日线聚合）
- ❌ **查询逻辑复杂**：需要处理 JOIN 和 NULL 值
- ❌ **日期不连贯**：可能出现有 K 线没指标的情况

**实现复杂度**：⭐⭐⭐⭐⭐（很高）
- 需要创建新表
- 需要修改所有查询逻辑（添加 JOIN）
- 需要处理数据不一致的情况
- 需要实现周线/月线指标的计算逻辑

---

## 使用场景分析

### 查询模式分析

**从代码中看到的使用模式**：

1. **策略查询**（`StrategyWorkerDataManager`）：
   - 主要使用价格数据（open, close, high, low, volume）
   - 偶尔使用指标（PE、PB），但频率不高
   - 查询模式：按股票+日期范围查询

2. **标签查询**（`TagWorkerDataManager`）：
   - 主要使用价格数据
   - 指标使用频率低

3. **跨表查询**（`StockService.load_with_latest_price`）：
   - 需要同时获取价格和指标（PE、PB、turnover_rate）
   - 使用 JOIN 查询

4. **策略示例**（`Waly`）：
   - 使用 `pe` 字段进行筛选
   - 但使用频率不高

**结论**：
- 价格数据是**高频使用**（每次查询都需要）
- 指标数据是**低频使用**（偶尔需要 PE、PB 等）

---

## 推荐方案

### 推荐：方案2（拆分handler，同表存储）⭐⭐⭐⭐

**理由**：

1. **平衡了灵活性和性能**：
   - 保留了同表存储的优势（查询性能、数据联动）
   - 解决了更新策略冲突问题（K 线 incremental，指标 rolling）

2. **解决了主要问题**：
   - ✅ 更新策略独立：K 线可以用 incremental，指标可以用 rolling
   - ✅ 解耦：daily_basic 失败不影响 K 线数据保存
   - ✅ 保留了查询性能优势

3. **实现复杂度可控**：
   - 不需要创建新表
   - 不需要修改查询逻辑
   - 只需要处理并发写入和数据合并

4. **向后兼容**：
   - 表结构不变
   - 查询逻辑不变
   - 只是数据来源分离

**实现要点**：

1. **写入策略**：
   - K 线 handler：写入价格字段（open, close, high, low, volume, amount 等）
   - Daily_basic handler：只更新指标字段（pe, pb, turnover_rate 等）
   - 使用 upsert 机制，按 `(id, date, term)` 主键合并

2. **数据一致性**：
   - K 线数据可以独立存在（指标字段为 NULL 或 0）
   - 指标数据可以独立存在（但需要对应的 K 线数据才有意义）
   - 查询时使用 LEFT JOIN 或合并逻辑

3. **更新策略**：
   - K 线：incremental（每天更新）
   - Daily_basic：rolling（滚动刷新最近30天，因为指标可能滞后更新）

4. **0值处理**：
   - Schema 中指标字段改为 `isRequired: false`
   - 查询时处理 NULL 值（使用默认值或跳过）

---

### 不推荐：方案3（分表存储）❌

**理由**：
- 查询性能下降明显（需要 JOIN）
- 实现复杂度很高（需要修改所有查询逻辑）
- 数据不一致问题难以处理
- 周线/月线指标计算开销大

**适用场景**：
- 如果未来指标数据量非常大（如数百个指标）
- 如果指标更新频率与 K 线差异很大（如指标每周更新一次）
- 如果查询时很少同时需要价格和指标

---

### 保留：方案1（当前实现）⚠️

**适用场景**：
- 如果指标更新频率与 K 线一致（都是每天更新）
- 如果可以接受历史日期的指标为0
- 如果查询时总是需要同时使用价格和指标

**改进建议**：
- 将指标字段改为 `isRequired: false`，允许 NULL
- 优化 daily_basic 的更新策略（使用 rolling 确保覆盖）

---

## 最终建议

**推荐方案2（拆分handler，同表存储）**，理由：

1. **解决了核心问题**：
   - 更新策略独立（K 线 incremental，指标 rolling）
   - 解耦（daily_basic 失败不影响 K 线）

2. **保留了优势**：
   - 查询性能（同表，无需 JOIN）
   - 数据联动（一次查询获取所有数据）

3. **实现复杂度可控**：
   - 不需要创建新表
   - 不需要修改查询逻辑
   - 只需要处理并发写入

4. **向后兼容**：
   - 表结构不变
   - 查询逻辑不变

**实现步骤**：

1. **创建 `daily_basic_handler`**：
   - 只处理 daily_basic API
   - 只更新指标字段
   - 使用 rolling 更新策略（滚动刷新最近30天）

2. **简化 `kline_handler`**：
   - 移除 daily_basic 相关逻辑
   - 只处理 K 线数据（daily/weekly/monthly）
   - 使用 incremental 更新策略

3. **处理并发写入**：
   - 使用 upsert 机制
   - 按 `(id, date, term)` 主键合并
   - K 线 handler 写入价格字段，daily_basic handler 写入指标字段

4. **Schema 调整**：
   - 指标字段改为 `isRequired: false`
   - 允许 NULL 值

5. **查询逻辑**：
   - 保持不变（因为同表存储）
   - 查询时自动获取所有字段（包括可能为 NULL 的指标字段）
