# 数据平台每日增量更新 — 详细设计文档

**项目**: sanguo_vnpy 数据平台  
**作者**: 赵云（数据总管）  
**日期**: 2026-05-06  
**版本**: v2.0  
**状态**: 待评审（重大架构变更）

---

## 一、背景与目标

### 1.1 现状

经过 P1（日线导入）和 P3（15分钟线下载导入），数据平台已建成：

| 数据类型 | 存储 | 覆盖范围 | 数据量 |
|---------|------|---------|--------|
| 日线行情 | NAS Parquet (`/Volumes/stock/A股数据/日线数据/daily/{year}/`) | 2010~2026 全市场 | ~5000只/年 |
| 15min分钟线 | NAS Parquet (`/Volumes/stock/minute_kline/15min/`) | 2025-09~2026-04 全市场 | 5193只 |
| vnpy主库 | NAS SQLite (`/Volumes/stock/sanguo_vnpy/data/quant_trading.db`) | 同上 | 1.4GB, 1281万行 |
| vnpy DB备份 | NAS (`.bak`) | 2026-05-02 | 330MB |

**问题**：数据是静态快照，没有自动更新机制。每次更新需手动执行脚本。

### 1.2 目标

1. **每日自动增量更新**：交易日收盘后自动更新日线+15min数据
2. **多数据源整合**：保留所有数据源访问方式，取各源最优数据合并
3. **数据最大化**：历史数据尽量完整，增量数据每日累积
4. **部署集成**：最终整合到 sanguo_vnpy 项目统一部署（待实现）

---

## 二、数据源调研

### 2.1 已验证的数据源

| 源 | 接口 | 可用性 | 历史深度 | 限频 | 适用场景 |
|---|---|---|---|---|---|
| **新浪财经** | `quotes.sina.cn/.../getKLineData` | ✅ Mac可用 | 15min: 800条(~3个月), 日线: 800条(~3年), 60min: 800条(~10月) | 0.3s/请求无封禁 | 15min增量、日线增量 |
| **腾讯财经** | `web.ifzq.gtimg.cn/.../fqkline` | ⚠️ 偶尔连接重置 | 日线: 按日期范围查询，可获取多年 | 无明显限制 | 日线增量（主源） |
| **东方财富** | `push2his.eastmoney.com/.../kline` | ❌ Mac直连被拒 | 理论上可指定任意日期范围 | 未知 | 历史回补（需Windows环境） |
| **akshare** | `stock_zh_a_hist_min_em` / `stock_zh_a_hist` | ⚠️ 走东方财富，受代理影响 | 理论完整 | 有代理污染问题 | 备用（需网络正常时） |
| **腾讯 minute/query** | `web.ifzq.gtimg.cn/.../minute/query` | ⚠️ 仅当天1min数据 | 仅当天 | 未知 | 当天1min→聚合15min（备源） |

### 2.2 数据源限制详情

**新浪财经 K线API**：
- URL: `https://quotes.sina.cn/cn/api/jsonp_v2.php/var%20=min15_{symbol}=/CN_MarketDataService.getKLineData?symbol={symbol}&scale={period}&ma=no&datalen={count}`
- `datalen` 参数最大有效值: **800**（超过返回null）
- `scale` 支持: 5, 15, 30, 60, 240（日线）
- 字段: day, open, high, low, close, volume, amount
- amount为真实成交额
- 时间戳为end-of-bar格式
- 返回JSONP，需正则提取JSON数组

**腾讯财经 fqkline API**：
- URL: `https://web.ifzq.gtimg.cn/appstock/app/fqkline/get?param={symbol},{period},{start},,{days},`
- 支持按日期范围查询
- 返回格式: `[date, open, close, high, low, volume]` 或 7列含amount
- amount有时为0（不完整）

**东方财富 K线API**：
- URL: `http://push2his.eastmoney.com/api/qt/stock/kline/get?secid={market}.{code}&klt={period}&fqt=1&beg={start}&end={end}`
- Mac环境直连被拒绝（Connection reset / 502）
- 可能与IP/地区/UA有关
- **Windows Node（192.168.2.33）待验证**：Node当前离线

### 2.3 多数据源策略

```
数据源选择优先级（按数据质量排序）：

日线增量更新:
  主源: 腾讯 fqkline（支持日期范围，amount有时为0）
  备源: 新浪 getKLineData scale=240（固定800条，amount真实）

15min增量更新:
  主源: 新浪 getKLineData scale=15（固定800条，amount真实，稳定可靠）
  备源: 腾讯 minute/query → 聚合15min（仅当天数据）

历史回补（15min更早的历史）:
  首选: 东方财富（需Windows环境，可指定日期范围）
  备选: akshare stock_zh_a_hist_min_em（依赖东方财富，需网络正常）
```

---

## 三、系统设计

### 3.1 整体架构

```
┌──────────────────────────────────────────────────────┐
│                  定时调度层 (OpenClaw Cron)            │
│  每交易日 15:35 触发 daily_update_all.sh               │
└───────────────────┬──────────────────────────────────┘
                    │
                    ▼
┌──────────────────────────────────────────────────────┐
│              daily_all_update.py (主脚本)              │
│                                                       │
│  ┌─────────────────┐    ┌──────────────────────┐      │
│  │  日线增量更新     │    │  15min增量更新        │      │
│  │  腾讯fqkline(主) │    │  新浪API(主)          │      │
│  │  新浪(备)        │    │  腾讯聚合(备)         │      │
│  └────────┬────────┘    └──────────┬───────────┘      │
│           │                        │                   │
│           ▼                        ▼                   │
│  ┌─────────────────────────────────────────────┐      │
│  │           数据校验层                          │      │
│  │  价格>0 | OHLC一致性 | 去重 | 类型兼容       │      │
│  └─────────────────────┬───────────────────────┘      │
│                        │                               │
│           ┌────────────┴────────────┐                  │
│           ▼                         ▼                  │
│  ┌─────────────────┐    ┌──────────────────────┐      │
│  │  Parquet写入     │    │  vnpy DB写入          │      │
│  │  原子写入(.tmp)  │    │  本地tmp→ATTACH导入   │      │
│  │  增量合并        │    │  NAS SQLite           │      │
│  └─────────────────┘    └──────────────────────┘      │
└──────────────────────────────────────────────────────┘
```

### 3.2 文件结构

```
~/.openclaw/sanguo_projects/sanguo_vnpy/data_platform/
├── daily_all_update.py          # 主脚本：全市场增量更新（日线+15min）
├── daily_update_all.sh          # Shell wrapper，由cron调用
├── download_minute.py           # 15min全量/批量下载脚本（保留）
├── import_vnpy_minute.py        # 分钟线导入vnpy DB（保留）
├── import_vnpy_daily_fast.py    # 日线全量导入脚本（保留，首次用）
├── updater.py                   # 旧版日线更新脚本（保留）
├── daily_update.sh              # 旧版wrapper（保留）
├── fallback.py                  # 降级工具（保留）
├── validator.py                 # 数据验证工具（保留）
└── logs/                        # 日志目录
```

### 3.3 核心流程

#### 3.3.1 日线增量更新

```
1. 扫描全市场股票列表（从 stock_basic_info CSV）
2. 对每只股票:
   a. 获取Parquet中最后日期
   b. 计算需要补充的日期范围（last_date+1 ~ today）
   c. 如果已是最新，跳过
   d. 调用腾讯fqkline API获取增量数据
   e. 数据校验（价格>0, 类型一致）
   f. 增量合并到年度Parquet文件（原子写入）
   g. 收集vnpy DB写入数据
3. 批量写入vnpy DB（本地tmp → ATTACH导入NAS DB）
```

#### 3.3.2 15分钟线增量更新

```
1. 扫描全市场股票列表
2. 对每只股票:
   a. 调用新浪API获取最近800条15min数据
   b. 数据校验（价格>0, OHLC一致性）
   c. 与已有Parquet增量合并（drop_duplicates keep='last'）
   d. 原子写入Parquet
   e. 计算新增行数，收集vnpy DB写入数据
3. 批量写入vnpy DB
```

#### 3.3.3 vnpy DB写入策略（解决SMB性能问题）

**问题**：NAS通过SMB挂载在Mac上，直接对1.4GB SQLite文件进行频繁读写：
- 查询超时（>20秒无响应）
- 写入可能触发SIGKILL（进程被系统终止）
- SMB文件锁与SQLite锁冲突风险

**方案：本地临时DB → ATTACH导入**

```
1. 在 /tmp/ 创建本地SQLite DB，写入增量数据
2. ATTACH NAS DB
3. INSERT OR REPLACE ... SELECT 从本地导入NAS DB
4. 更新 dbbaroverview 表
5. DETACH，删除本地临时文件
```

**优点**：
- 增量数据先在本地SSD写完，与NAS交互只有一次批量INSERT
- 减少SMB文件操作次数
- INSERT OR REPLACE保证幂等性

**风险与缓解**：
| 风险 | 缓解措施 |
|------|---------|
| ATTACH时NAS DB被锁定 | timeout=120秒等待 |
| 中途失败导致DB不一致 | WAL模式 + INSERT OR REPLACE幂等 |
| overview表更新慢 | 只在全部数据导入后执行一次 |

### 3.4 数据校验规则

| 规则 | 说明 | 实现 |
|------|------|------|
| 价格>0 | close/open ≤ 0 的行丢弃 | `(df[["close","open"]] <= 0).any(axis=1)` |
| OHLC一致性 | high < max(open,close) 或 low > min(open,close) 的行丢弃 | 逐行比较 |
| 去重 | 相同day/date保留最新 | `drop_duplicates(subset=["day"], keep="last")` |
| 类型兼容 | volume/amount保持object与已有Parquet一致 | `.astype(str)` |
| NaN处理 | 价格NaN行丢弃，volume/amount NaN填0 | `fillna(0)` + `dropna` |
| 日期格式 | 日线: YYYY-MM-DD(str), 15min: YYYY-MM-DD HH:MM:SS(str) | 统一astype(str)避免混合类型 |

### 3.5 断点续传

- 15min更新：进度文件 `/Volumes/stock/logs/daily_update/progress/15min_progress.json`
  - 记录已完成的股票代码列表
  - 中断后重启自动跳过已完成的
- 日线更新：通过检查Parquet最后日期判断，天然幂等
- 日志：`/Volumes/stock/logs/daily_update/update_{timestamp}.log`
- 报告：`/Volumes/stock/logs/daily_update/report_{date}.json`

### 3.6 限频与容错

| 参数 | 值 | 说明 |
|------|-----|------|
| 请求间隔 | 0.3秒 | 避免触发源站限频 |
| 单股重试 | 3次 | 失败后重试，间隔1秒 |
| 连续失败暂停 | 10次连续失败后暂停60秒 | 防止批量封禁 |
| 超时 | 15秒/请求 | 单次请求超时 |
| DB写入超时 | 120秒 | SMB写入等待 |

---

## 四、vnpy DB Schema 参考

```sql
-- 主数据表
CREATE TABLE dbbardata (
    symbol VARCHAR(32),
    exchange VARCHAR(32),
    datetime VARCHAR(64),
    interval VARCHAR(8),
    volume FLOAT,
    turnover FLOAT,
    open_interest FLOAT,
    open_price FLOAT,
    high_price FLOAT,
    low_price FLOAT,
    close_price FLOAT,
    PRIMARY KEY (symbol, exchange, interval, datetime)
);

-- 概览表
CREATE TABLE dbbaroverview (
    symbol VARCHAR(32),
    exchange VARCHAR(32),
    interval VARCHAR(8),
    count INT,
    start VARCHAR(64),
    end VARCHAR(64),
    PRIMARY KEY (symbol, exchange, interval)
);
```

**interval值说明**：
- `d` = 日线
- `15m` = 15分钟线（v1.1修正：与司马懿确认，采用方案B）

**方案B实现**（2026-05-03 司马懿评审确认）：
1. vnpy Interval枚举加 `MINUTE_15 = "15m"`（monkey patch方式注入，不依赖vnpy版本）
2. executor INTERVAL_MAP 改 `"15m" = Interval.MINUTE_15`
3. DB迁移：`UPDATE dbbardata SET interval="15m" WHERE interval="1m" AND ...`
4. DB中现有"1m"数据需要一次迁移（迁移前备份）

---

## 五、多数据源保留策略

### 5.1 当前实现

| 数据源 | 代码文件 | 状态 |
|--------|---------|------|
| 新浪财经 | `daily_all_update.py` 中的 `try_sina_15min()` | ✅ 在用 |
| 腾讯fqkline | `daily_all_update.py` 中的 `fetch_tencent_daily()` | ✅ 在用 |
| 腾讯minute/query | `download_minute.py` 中的 `try_minute_query_aggregate()` | ✅ 已实现，作为备源 |
| 东方财富 | 未实现 | ❌ 待开发（需Windows环境） |
| akshare | `daily_all_update.py` 外部依赖 | ⚠️ 受代理影响 |

### 5.2 设计原则

1. **所有数据源接口统一保留**，不删除任何已有的数据源访问代码
2. **数据合并策略**：同一股票同一周期从多个源获取时，按优先级选择：
   - amount（成交额）：优先有真实值的源（新浪 > 腾讯）
   - 数据长度：优先历史更长的源
   - 数据时效：优先更新的源
3. **源降级链**：主源失败自动尝试备源，不丢数据
4. **源标记**：Parquet文件可选增加 `_source` 列标记数据来源（待讨论）

### 5.3 未来扩展点

- 东方财富API集成（需Windows Node）
- akshare作为备用日线源（网络恢复后）
- 1min/5min/30min/60min等其他周期
- 北交所920xxx数据（需新数据源）

---

## 六、SMB/NAS 性能问题与方案

### 6.1 已知问题

| 问题 | 现象 | 影响 |
|------|------|------|
| SMB读大文件慢 | 1.4GB SQLite查询超时(>20s) | 无法直接在Mac上操作NAS DB |
| SMB写大文件卡死 | 进程被SIGKILL | 全量导入必须用本地中转 |
| SMB文件锁冲突 | SQLite WAL模式可能异常 | 并发写入风险 |
| Parquet小文件延迟 | 5300个parquet文件，SMB逐个读写 | 全量更新约30分钟 |

### 6.2 当前方案

```
写入流程（NAS DB）:
  本地/tmp写SQLite → ATTACH NAS DB → INSERT OR REPLACE → DETACH → 删除临时文件

写入流程（Parquet）:
  内存中合并 → 写本地.tmp → rename到NAS路径
```

### 6.3 待讨论：是否直接在NAS本地执行

NAS (192.168.2.154) 上运行的是 Linux，如果能SSH执行Python脚本：
- SQLite直接本地读写，无SMB延迟
- Parquet直接本地写入
- 速度提升10倍以上

**方案A（当前）**：Mac上跑脚本，SMB读写NAS
- 优点：无需SSH，利用Mac环境
- 缺点：SMB性能瓶颈

**方案B（建议）**：NAS上直接跑脚本（需姜维配合SSH/容器环境）
- 优点：无SMB瓶颈，速度快
- 缺点：需要NAS上有Python环境

**方案C（折中）**：Parquet写NAS（小文件SMB可接受），SQLite写Docker容器内（通过HTTP API）
- 优点：各取所长
- 缺点：需要开发写入API

> 📌 **待与司马懿讨论**：NAS性能问题的最终解决方案

---

## 七、定时任务配置

### 7.1 当前方案（OpenClaw Cron）

| 配置项 | 值 |
|--------|-----|
| 调度 | 每交易日（周一到周五）15:35 |
| 时区 | Asia/Shanghai |
| 执行方式 | isolated session（不消耗主session token） |
| 超时 | 3600秒（1小时） |
| 通知 | 完成后飞书通知 |

### 7.2 Cron表达式

```
35 15 * * 1-5    # 周一到周五 15:35
```

### 7.3 注意事项

- 非交易日也会触发，但脚本会检测无新数据后快速退出（所有股票都skipped）
- 未来可增加交易日历判断（如使用akshare获取交易日历）

---

## 八、部署方案（待实现）

### 8.1 当前部署状态

- 脚本路径：`~/.openclaw/sanguo_projects/sanguo_vnpy/data_platform/`
- 运行环境：Mac mini（楚锋的Mac mini），Python 3.9
- 调度：OpenClaw Cron
- 数据存储：NAS SMB挂载 `/Volumes/stock/`

### 8.2 目标部署（整合到sanguo_vnpy项目）

**待实现，设计如下**：

```
sanguo_vnpy/
├── deploy/
│   ├── docker-compose.yml        # 包含数据更新服务
│   └── data-updater/
│       ├── Dockerfile            # 数据更新容器
│       ├── crontab               # 容器内crontab
│       └── entrypoint.sh
├── src/
│   └── data_platform/            # 数据平台代码（从data_platform/迁移）
│       ├── daily_all_update.py
│       ├── download_minute.py
│       ├── import_vnpy_daily_fast.py
│       ├── import_vnpy_minute.py
│       └── ...
├── docs/
│   └── data-platform/
│       └── daily-update-design.md  # 本文档
└── config/
    └── data_platform.yaml        # 配置文件（路径、限频参数等）
```

### 8.3 部署步骤（草案）

1. 代码从 `~/.openclaw/sanguo_projects/` 迁移到 `sanguo_vnpy/src/data_platform/`
2. 配置外置为YAML文件
3. Docker容器内置crontab + Python脚本
4. 容器挂载NAS数据目录
5. 与现有vnpy回测服务docker-compose整合

---

## 九、测试

### 9.1 已完成的测试

| 测试项 | 结果 | 日期 |
|--------|------|------|
| 日线增量更新3只（000001/600519/300750） | ✅ 3只skipped（已是最新） | 2026-05-03 |
| 15min增量更新3只 | ✅ 3只ok，0 failed | 2026-05-03 |
| 全市场15min下载（5193只） | ✅ 完成，107只北交所失败（源不支持） | 2026-05-02 |
| vnpy DB日线全量导入（1281万行） | ✅ 回测验证通过 | 2026-05-02 |
| vnpy DB 15min导入（单只验证） | ✅ 1970行，16个时间点正确 | 2026-05-02 |

### 9.2 待测试项

| 测试项 | 方法 | 优先级 |
|--------|------|------|
| 全市场增量更新完整流程 | cron触发后检查report | P0 |
| NAS离线时脚本行为 | umount后运行，验证优雅退出 | P0 |
| DB写入并发安全 | 两个脚本同时写DB | P1 |
| 东方财富API（Windows） | Windows Node上线后测试 | P2 |
| 非交易日执行 | 周末运行，验证全部skipped | P1 |
| 30天连续运行稳定性 | 观察一个月的report | P1 |

---

## 十、Q&A — 讨论过的问题汇总

### Q1: Parquet双写是什么意思？还需要吗？

**讨论**：原TODO #4提到Parquet作为真相源（source of truth）与vnpy DB双写。  
**结论**：当前架构中 Parquet 是下载的**原始产出**，vnpy DB 是**导入产物**。Parquet本身就是备份。不需要额外的双写机制。真正需要的是**vnpy DB的定时备份**（当前.bak只备份一次）。

### Q2: 新浪API只能拿800条，怎么获取更长的历史？

**讨论**：新浪 `datalen=800` 是硬限制，超过800返回null。实测15min=3个月，日线=3年。  
**结论**：
- 增量更新场景：每日800条足够覆盖最新数据，历史在Parquet中累积
- 历史回补：需要东方财富API（可指定日期范围），但Mac被拒，需Windows环境
- 另一条路：如果之前有更长的CSV数据（如84只深市老数据有1970行），合并进Parquet

### Q3: vnpy DB的interval为什么是"1m"而不是"15m"？

**讨论**：vnpy 4.x的Interval枚举只有 `MINUTE="1m"`，没有 `MINUTE_15`。Docker用的是原始vnpy。  
**结论**：DB中15分钟线用 `interval="1m"` 存储，与BacktestingEngine `load_data(interval="1m")` 匹配。如果未来引入真正的1分钟线，需要重新设计interval值。

### Q4: 北交所107只股票怎么办？

**讨论**：新浪行情源不支持920xxx代码。  
**结论**：当前不影响（HS300无北交所），后续如需支持需引入新数据源（如东方财富）。

### Q5: 为什么不直接在NAS上跑脚本？

**讨论**：Mac通过SMB访问NAS，大文件操作慢且不稳定（SIGKILL）。  
**结论**：当前用本地tmp中转方案缓解。长期建议在NAS本地执行（需SSH/容器环境），或通过Docker容器HTTP API写入。

### Q6: amount（成交额）数据准确性？

**讨论**：腾讯fqkline的amount有时返回0，新浪API的amount是真实值。  
**结论**：
- 15min：用新浪（amount真实）
- 日线：用腾讯（amount可能为0，但支持日期范围查询更重要）
- 未来可考虑用新浪的amount覆盖腾讯的0值

### Q7: 每日增量更新多长时间？

**预估**：
- 日线：5300只 × 0.3s ≈ 26分钟（大部分skipped更快）
- 15min：5300只 × 0.3s ≈ 26分钟
- DB写入：取决于增量数据量，通常几百条
- **总计约30-50分钟**

### Q8: 如何处理节假日/非交易日？

**当前方案**：非交易日执行时，所有股票都检测到"已是最新"被skipped，快速退出（<1分钟）。  
**改进方向**：可增加交易日历判断，非交易日直接不执行（节省一次扫描）。

### Q9: 数据更新和回测服务会冲突吗？

**风险**：更新脚本和回测服务同时读写同一个vnpy DB。  
**缓解**：回测服务在Docker容器内操作自己的DB副本（`/home/vnpy/.vntrader/database.db`），与NAS上的DB是不同文件。NAS DB更新后需要同步到Docker（目前手动wget）。  
**待改进**：自动化DB同步机制（cron或文件监控）。

### Q10: 代码部署为什么要和sanguo_vnpy整合？

**理由**：
1. 数据平台是为vnpy回测服务的，放一起管理方便
2. Docker统一部署，减少环境依赖
3. 配置集中管理（NAS路径、限频参数等）

---

## 十一、文件清单

| 文件 | 路径 | 说明 |
|------|------|------|
| `daily_all_update.py` | `sanguo_vnpy/data_platform/` | 主脚本：全市场增量更新 |
| `daily_update_all.sh` | `sanguo_vnpy/data_platform/` | Shell wrapper |
| `download_minute.py` | `sanguo_vnpy/data_platform/` | 15min全量下载（保留） |
| `import_vnpy_minute.py` | `sanguo_vnpy/data_platform/` | 分钟线导入DB（保留） |
| `import_vnpy_daily_fast.py` | `sanguo_vnpy/data_platform/` | 日线全量导入（保留） |
| `updater.py` | `sanguo_vnpy/data_platform/` | 旧版日线更新（保留） |
| `daily_update.sh` | `sanguo_vnpy/data_platform/` | 旧版wrapper（保留） |

---

## 十二、变更记录

| 日期 | 版本 | 变更 | 作者 |
|------|------|------|------|
| 2026-05-03 | v1.0-draft | 初始版本 | 赵云 |
| 2026-05-03 | v1.1 | 司马懿评审后修改：interval→15m, 严格增量追加, 日线进度文件, 全局源检测, DB轮转备份, 失败率告警 | 赵云 |
| 2026-05-05 | v1.2 | 东方财富集成：日线主源切换为东方财富(amount真实,反爬策略4s/请求+随机抖动), 腾讯降为备源 | 赵云 |
| 2026-05-06 | v2.0 | **重大架构变更**：BaoStock替代所有主源（无反爬、全量历史、amount真实）；15min interval改为1m；vnpy DB写入改为本地构建+rsync；新浪API已挂移除；多源fallback机制重构 | 赵云 |

---

## 十三、评审结果（2026-05-03 司马懿评审）

### v1.1 评审结论：有条件通过（已完成）

**2个阻塞项（已解决）**：
1. ✅ interval="1m" → "15m"：采用方案B（vnpy加MINUTE_15枚举 + monkey patch）
2. ✅ 15min增量合并：改为严格按日期追加，不再用drop_duplicates keep=last

**4个硬伤（已修复）**：
1. ✅ 日线增加进度文件
2. ✅ 全局源不可用检测（连续30只首次失败→终止）
3. ✅ DB轮转备份（保留7天，`quant_trading_{YYYYMMDD}.db.bak`）
4. ✅ 失败率>5%告警标记 + 源终止告警

---

## 十四、v2.0 重大架构变更（2026-05-06）

### 14.1 变更背景

v1.2运行暴露了5个根本性问题：

| # | 问题 | 根因 | 影响 |
|---|------|------|------|
| 1 | vnpy DB写入报`no such table: dbbardata` | SMB文件锁与SQLite ATTACH不兼容 | 日线+15min数据不入DB |
| 2 | 新浪15min API已失效 | 返回Error 0，所有请求失败 | 15min无法增量更新 |
| 3 | BaoStock 15min回补已完成但未入库 | 原脚本interval=`15m`与vnpy不兼容 | 5193只×8992条数据闲置 |
| 4 | 日线跨年写入bug | `year=datetime.now().year`硬编码 | 年初数据会写错目录 |
| 5 | overview全表聚合 | 1.4G DB上GROUP BY全表扫描 | NAS上可能超时/锁死 |

### 14.2 数据源重新调研

#### 数据源实测对比

| 数据源 | 15min可获取量 | 日线可获取量 | amount | 反爬 | 频率 | 当前状态 |
|--------|-------------|-------------|--------|------|------|----------|
| **BaoStock** | 无限制（按日期） | 无限制（按日期） | 真实(5.54亿) | **无** | 0.12s/只, 100只0错误 | ✅ 稳定 |
| **东方财富** | ~496条(7周) | 多年(~1046条) | 真实(5.54亿) | 4-5s/请求+UA+Referer | 中 | ✅ 可用 |
| **腾讯** | Connection reset | 按日期范围 | 有时为0 | 无 | 快 | ⚠️ 不稳定 |
| **新浪** | Error/2条 | Error/2条 | - | - | - | ❌ 已挂 |

**关键结论**：BaoStock在所有维度都最优（无反爬、全量历史、amount真实、速度快），应作为首选源。

#### v1.2 BaoStock压力测试

```
15min: 100只连续请求, 总耗时11.9s, 平均0.12s/只, 0错误
日线:  10只连续请求, 总耗时1.6s, 平均0.16s/只
全历史: sh.600000 2010-2026日线 3963条, 0.68s
```

#### v1.2 SQLite本地写入性能

```
100万条INSERT OR REPLACE: 2.0s
预估4600万条(15min全量): ~91s ≈ 1.5分钟
```

### 14.3 v2.0 核心架构变更

#### 变更1：数据源降级链重构

**设计原则**：按数据质量排序，质量最好的源排第一。每个源封装独立函数，统一返回DataFrame。主循环挨个尝试，成功即用，失败试下一个。

```
v1.x（旧）:
  日线: 腾讯(主) → 新浪(备)
  15min: 新浪(主) → 无备源

v2.0（新）:
  日线: BaoStock(主) → 东方财富(备) → 腾讯(三备)
  15min: BaoStock(主) → 东方财富(备) → 新浪(三备，当前已挂)
```

**Fallback机制**：
```python
SOURCES_DAILY = [
    ("baostock", fetch_baostock_daily),    # 最优：全量历史+无反爬+amount真实
    ("eastmoney", fetch_eastmoney_daily),  # 备用：多年历史+amount真实+4s限频
    ("tencent", fetch_tencent_daily),      # 三备：amount有时为0
]
SOURCES_15MIN = [
    ("baostock", fetch_baostock_15min),    # 最优
    ("eastmoney", fetch_eastmoney_15min),  # 备用：7周
    ("sina", try_sina_15min),              # 三备：当前已挂
]

def fetch_with_fallback(sources, code, start, end):
    for name, fetch_fn in sources:
        try:
            data = fetch_fn(code, start, end)
            if data is not None and len(data) > 0:
                return data, name
        except Exception:
            continue
    return None, None
```

#### 变更2：vnpy DB写入策略改为本地构建+rsync

**v1.x方案（ATTACH via SMB）**：直接在Mac上ATTACH NAS DB → SMB文件锁导致失败

**v2.0方案（本地构建+rsync）**：
1. 每日更新时，从NAS cp当前DB到本地`/tmp/`
2. 所有增量数据写入本地DB
3. 验证完整性后，rsync覆盖NAS DB
4. 备份旧DB（轮转7天）

```python
def sync_db_to_nas():
    # 备份
    backup = f"quant_trading_{today}.db.bak"
    shutil.copy2(str(VNPY_DB_PATH), str(VNPY_DB_PATH.parent / backup))
    
    # rsync本地→NAS
    os.system(f"rsync -av --progress {LOCAL_DB_PATH} {VNPY_DB_PATH}")
```

**性能预估**：
- cp NAS DB到本地：~15秒（1.4G）
- 增量写入本地DB：<1秒（日线）
- rsync覆盖NAS：~15秒
- 全量15min导入（首次）：~1.5分钟（4600万条）

#### 变更3：15min interval统一用`1m`

**v1.x**：interval=`15m`（与vnpy 4.x不兼容）
**v2.0**：interval=`1m`（姜维确认vnpy 4.x Interval.MINUTE.value=`1m`）

**数据格式（姜维确认）**：
- symbol: `000001`（纯代码）
- exchange: `SSE` / `SZSE`
- interval日线: `d`
- interval分钟线: `1m`

#### 变更4：日线跨年写入修复

**v1.x bug**：`year = datetime.now().year`，年初数据写错目录
**v2.0**：按数据日期分目录

```python
def update_daily_parquet(code, new_data):
    for yr in new_data["date"].str[:4].unique():
        year_data = new_data[new_data["date"].str[:4] == yr]
        parquet_path = DAILY_DIR / yr / f"{prefix}{clean}_daily.parquet"
        # 合并写入...
```

#### 变更5：overview增量更新

**v1.x**：`SELECT ... FROM dbbardata GROUP BY` 全表扫描（1.4G DB上很慢）
**v2.0**：只更新本次涉及的symbol

```python
for sym, exc, ivl in affected_keys:
    c.execute("""INSERT OR REPLACE INTO dbbaroverview 
    SELECT ?,?,?,COUNT(*),MIN(datetime),MAX(datetime)
    FROM dbbardata WHERE symbol=? AND exchange=? AND interval=?""",
    (sym, exc, ivl, sym, exc, ivl))
```

#### 变更6：进度文件加日期

**v1.x**：进度文件不区分日期，跨天可能跳过
**v2.0**：`daily_20260506_progress.json`，每次运行独立进度

#### 变更7：Cron fallback模型

**v1.x**：只用默认模型，配额用完则任务失败
**v2.0**：设置fallback模型（zhipu/glm-5.1），配额不足时自动降级

### 14.4 执行计划

#### 第1步：灌入现有数据到本地vnpy DB

```
1. cp NAS quant_trading.db → /tmp/quant_trading_import.db
2. import_vnpy_daily_fast.py --start-year 2026  # 补3/28~今天的日线增量
3. import_vnpy_minute.py --scope all            # 全量导入5193只15min
4. 验证数据完整性
5. rsync本地DB → NAS
```

#### 第2步：重构daily_all_update.py

按14.3的7个变更点重构代码。

#### 第3步：Cron更新+测试

- 更新cron任务配置
- 手动触发一次全量更新验证
- 确认日志无错误

### 14.5 与v1.x的兼容性

| 变更 | 向后兼容 | 影响 |
|------|---------|------|
| interval 15m→1m | ❌ 需要DB迁移 | 现有15m数据需UPDATE为1m |
| DB写入策略 | ✅ 无影响 | Parquet不受影响 |
| 数据源顺序 | ✅ 无影响 | 只是重试顺序变化 |
| 跨年写入 | ✅ 修正bug | 未来数据不再错 |
| overview增量 | ✅ 无影响 | 只是优化 |

> ⚠️ **DB迁移注意**：v1.x如果有`interval='15m'`的记录，需要一次性UPDATE为`'1m'`。当前DB中实际无15min数据（v1.x的写入全部失败），所以无需迁移。

---

## 十五、v2.0 评审待确认项

| # | 问题 | 建议方案 | 待确认 |
|---|------|---------|--------|
| 1 | BaoStock作为全主源是否合适？ | 无反爬+全量+amount真实，建议通过 | 司马懿 |
| 2 | 本地构建+rsync替代ATTACH | 姜维确认推荐，比ATTACH稳定 | 司马懿 |
| 3 | interval=1m而非15m | 姜维确认vnpy 4.x规范 | 司马懿 |
| 4 | 是否需要DB迁移脚本？ | 当前无15m数据，无需迁移 | 司马懿 |
| 5 | Fallback顺序是否合理？ | BaoStock→东方财富→腾讯/新浪 | 司马懿 |
| 6 | 日常更新全市场耗时预估？ | BaoStock: ~10min(15min)+~8min(日线)+rsync | 司马懿 |
| 7 | 是否需要额外反爬措施？ | BaoStock无需，备源保留原有措施 | 司马懿 |

### 15.6 v2.0 评审结论（2026-05-06 司马懿）

**结论：全部通过，可以部署**

C1 interval=1m：姜维翻源码确认vnpy硬约束，接受。**附加前提：代码里所有写interval='1m'的地方必须加注释，说明这是vnpy 4.x Interval.MINUTE硬约束，实际存储15分钟线。**

C2 rsync原子性：改为写新文件+mv原子重命名。

M1 BaoStock T+1延迟：已验证确认。日常增量改为东方财富(当天实时) → BaoStock(T+1补全) → 腾讯。

M2 失败暂停：改为失败率检测（最近100只>80%切换源）。

**最终Fallback顺序（含T+1调整）：**
```
日常增量（当天15:35触发）：
  日线：东方财富(实时) → BaoStock(T+1) → 腾讯
  15min：东方财富(实时7周) → BaoStock(T+1) → 新浪

历史回补：
  日线+15min：BaoStock（全量历史，无反爬）
```
