Files
gao 05635b76b9 chore: 重构前基线 — 9 个 sync task 全部 ok + akshare 移除 + mairui 资金流接入
状态:
- 9 个 sync task(stock_basic / kline_daily / kline_index / kline_5min /
  moneyflow / industry_sector / sector_features / share_snapshot / market_regime)
- 数据源:baostock + mairui + 雪球(pysnowball) + 新浪(4 个)
- 项目级约束:永远不用 akshare(已落实)
- kline_5min 改用 DB 快照统一全量/增量逻辑
- 零后端 Chrome 扩展 xueqiu_sync(独立项目)
2026-06-15 15:36:09 +08:00

203 lines
9.4 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Market Data Sync — 架构说明
> A 股市场数据定时同步框架。多个数据源 → 交易日感知调度器 → MySQL。
## 1. 目录结构
```
market_data_sync/
├── app/ # 业务代码(唯一 Python 包)
│ ├── __init__.py
│ ├── core/ # 核心抽象层(与业务无关的基础设施)
│ │ ├── config.py # 统一配置(pydantic-settings + .env
│ │ ├── datasource/ # 数据源抽象
│ │ │ ├── base.py # DataSource / FetchResult / registry
│ │ │ └── registry.py # 健康监控 + 自动降级 + seed
│ │ ├── sync/ # 同步任务抽象
│ │ │ ├── base.py # SyncTask 基类(自动状态机)
│ │ │ └── registry.py # dataset_registry 状态机封装
│ │ ├── scheduler/ # 轻量调度器
│ │ │ └── scheduler.py # 交易日感知 + config 表持久化
│ │ ├── db/ # 数据库访问层
│ │ │ ├── connection.py # thread-local pymysql
│ │ │ ├── schema.py # 13 张表 DDL
│ │ │ └── ops.py # CRUD + 批量 upsert
│ │ └── utils/
│ │ └── logging.py # RotatingFileHandler
│ ├── sources/ # 数据源实现(5 个)
│ │ ├── sina.py # 新浪(免费 K 线)
│ │ ├── baostock.py # Baostock(基础信息+行业)
│ │ ├── mairui.py # 麦蕊智数(需 MAIRUI_LICENCE
│ │ ├── akshare.py # akshare/东方财富(5min+资金流)
│ │ └── xueqiu.py # 雪球(股本快照)
│ ├── tasks/ # 同步任务实现(8 个)
│ │ ├── __init__.py # TASKS 注册表 + get_task()
│ │ ├── task_stocks_basic.py
│ │ ├── task_kline_daily.py
│ │ ├── task_kline_index.py
│ │ ├── task_kline_5min.py
│ │ ├── task_moneyflow.py
│ │ ├── task_industry_sector.py
│ │ ├── task_share_snapshot.py
│ │ └── task_market_regime.py
│ ├── api/ # FastAPI 管理接口(端口 8100
│ │ ├── main.py # 入口 + 生命周期钩子
│ │ └── routes/
│ │ ├── health.py
│ │ ├── sync.py # 手动触发 / 状态查询
│ │ ├── schedule.py # 计划任务
│ │ └── datasources.py # 数据源健康度
│ └── entrypoints/ # 进程入口
│ ├── cli.py # CLIpython -m app.entrypoints.cli ...
│ └── worker.py # 纯调度模式(python -m app.entrypoints.worker
├── bin/ # 一次性脚本
│ └── runall_once.py # 一键串行跑全部任务
├── scripts/ # 旧 shell 脚本(保留作 fallback
├── tests/ # 单元测试(pytest
├── docs/ # 文档
├── data/ # 临时数据(一般为空)
├── logs/ # 运行日志(RotatingFileHandler
├── .env / .env.example # 配置
├── requirements.txt
├── README.md
└── start.sh # 统一启动入口
```
## 2. 模块依赖
```
┌────────────────────────────────────────┐
│ entrypoints/cli │ 手动/CLI
│ entrypoints/worker │ 调度模式
│ │ │
│ ▼ │
│ ┌─────────────────────────────┐ │
│ │ api (FastAPI) │ │ HTTP 管理
│ └──────────────┬──────────────┘ │
│ │ │
▼ ▼ │
┌──────────┐ ┌─────────────┐ │
│ tasks │◄───│ core │ │
│ (8 个) │ │ ├ config │ │
└────┬─────┘ │ ├ db │ │
│ │ ├ sync │ │
│ │ ├ scheduler│ │
│ │ └ datasource│ │
▼ └──────┬──────┘ │
┌──────────┐ │ │
│ sources │◄──────────┘ │
│ (5 个) │ │
└──────────┘ │
│ │
▼ │
数据源外部 API │
```
依赖方向单向:上层 → 下层。`core` 是稳定抽象,`sources` / `tasks` 是可插拔实现。
## 3. 核心抽象
### 3.1 DataSource (`app/core/datasource/base.py`)
```python
class DataSource:
key: str # "datasource_xxx" 全局唯一
name: str # 显示名
provides: list[str] # 能力列表 ["kline_daily", "moneyflow", ...]
requires_credential: bool
def is_available(self) -> tuple[bool, str]: ...
def health_check(self) -> dict: ...
def fetch_xxx(self, ...) -> pd.DataFrame: ...
```
- `registry` 是全局单例 dict[str, DataSource]
- 启动时 `build_default_registry()` 把 5 个源注册进去
- `pick_source(capability)``provides` 找最匹配的源
### 3.2 SyncTask (`app/core/sync/base.py`)
```python
class SyncTask:
dataset_id: str # 子类必须设置
def run(self, *, trigger_source="manual", **kwargs) -> dict:
mark_sync_running(...)
try:
result = self._run(...)
mark_sync_success/failed(...)
except Exception:
mark_sync_failed(...)
```
子类只需实现 `_run()`,状态机会自动包装。
### 3.3 Scheduler (`app/core/scheduler/scheduler.py`)
- 5s tick 扫描 config 表
- 读取 `trading_calendar_holidays` 决定是否交易日
- 时间匹配(HH:MM 精确触发)或 interval(间隔秒数)触发
- 启动时 `recover_interrupted_syncs()` 把卡死的 `running` 任务改 `failed`
## 4. 数据流(以 kline_daily 为例)
```
scheduler tick
└─ 时间到 (16:00)
└─ ScheduleJob.run()
└─ tasks.get_task("kline_daily")()
└─ SyncKlineDaily.run()
├─ mark_sync_running(...) ← 状态机
├─ _ensure_health_checked() ← 拉一次数据源健康
├─ pick_source("kline_daily") ← 选雪球(主) > 新浪 > baostock
├─ 多线程 fetch (10 workers) ← XUEQIU_TOKEN 限速
├─ 批量 upsert (CHUNK=500) ← pymysql executemany
└─ mark_sync_success(...) ← 状态机
```
## 5. 添加新数据源
1.`app/sources/` 新建 `my_source.py`
2. 继承 `DataSource`,设置 `key` / `name` / `provides` / `requires_credential`
3. 实现 `is_available()` / `health_check()` / 业务方法
4.`app/core/datasource/registry.py``build_default_registry()` 注册
## 6. 添加新同步任务
1.`app/tasks/` 新建 `task_xxx.py`
2. 继承 `SyncTask`,设置 `dataset_id`
3. 实现 `_run()`,返回 `{"status": "ok", "message": "..."}``{"status": "warning/blocked", ...}`
4.`app/tasks/__init__.py``TASKS` 字典中加一行
5. (可选)在 `app/core/sync/registry.py``SYNC_DEFINITIONS` 加条目
## 7. 数据库表
13 张表在 `app/core/db/schema.py`
| 表 | 用途 |
|---|---|
| `stocks` | 股票基础信息 |
| `kline_stock` | 日 K 线 |
| `kline_index` | 指数日 K |
| `kline_5min` | 5min K 线 |
| `moneyflow` | 资金流 |
| `industry` / `sectors` / `stock_sector_map` | 行业映射 |
| `share` | 股本快照 |
| `market_regime_daily` | 市场情绪 |
| `dataset_registry` | 同步任务状态机 |
| `config` | 通用配置(调度、节假日、token 等) |
| `indices` | 指数字典 |
## 8. 配置(.env
| 变量 | 必填 | 说明 |
|---|---|---|
| `MYSQL_HOST/PORT/USER/PASSWORD/DATABASE` | ✓ | 主库 |
| `MYSQL_DATABASE_BUSINESS` | | 业务库(默认 grid_seeker_model_base |
| `XUEQIU_TOKEN` | | 雪球 tokenK线主源 + 股本) |
| `MAIRUI_LICENCE` | | 麦蕊智数 licence |
| `HOLIDAYS_LIST` | | A 股休市日(逗号分隔 YYYY-MM-DD |
| `SCHEDULER_AUTO_SEED` | | 启动时 seed 默认计划任务 |
| `LOG_DIR` | | 日志目录(默认 logs) |
| `LOG_LEVEL` | | 日志级别(默认 INFO) |