18 Commits

Author SHA1 Message Date
gao daef609e8b feat: QMT Bridge 设为主数据源,新增指数日K支持及数据源文档
- QmtBridgeSource: provides 增加 index_daily,新增 fetch_index_daily 方法
- task_kline_index: 优先级改为 qmt_bridge → mairui → sina 三级降级
- task_kline_5min: 优先用 qmt_bridge,不可用时降级 mairui
- task_kline_daily: 优先级加入 qmt_bridge(首位)
- config: 新增 qmt_bridge_url 配置项
- registry: qmt_bridge 注册信息同步更新
- docs: 新增 DATASETS_AND_SOURCES.md,完整说明数据集与数据源依赖关系
- AGENTS.md: 同步更新指数数据源描述
2026-07-23 23:39:59 +08:00
gao 4516f6d75a udpate 2026-07-21 22:09:40 +08:00
gao 8f016f25df chore: baseline — ORM migration + systemd deploy + MCP server + daily check
- ORM migration: models/ops 重构
- deploy: 标准化 systemd timer/service 部署体系
- MCP server: 5 个只读工具(任务/状态/数据集)
- daily check: 数据一致性巡检
- watch: task_watch 后台监控
- kline_daily: 麦蕊优先,时间门禁15:00
- 删除: task_industry_sector, task_sector_features
2026-07-21 16:37:00 +08:00
gao 91e83a3b0b feat(mcp): MCP server (5 tools) — 任务/状态/数据集/详情/今日汇总
新增 app/mcp_server.py,让 LLM (Claude Desktop / Claude Code) 通过
MCP stdio 协议查询 market_sync 的同步情况:

工具列表:
  1. list_sync_tasks        - 列出 13 个 sync task + 当前状态 + 7d 运行统计
  2. get_task_status        - 单个 task 的状态机 + 最近 N 天所有 run
  3. list_datasets          - 17 张 PG 表的行数 + 日期范围
  4. get_dataset_info       - 单表的 schema/索引/PK + 3 行抽样(白名单防注入)
  5. get_today_sync_summary - 今日 KPI + 每个 task 的最新 run

传输: stdio (本地启动 Claude Desktop 直连)
启动: python -m app.mcp_server
依赖: mcp[cli]>=1.0.0 (已加入 requirements.txt)

复用层: app.core.db.ops + app.core.sync.registry 的 helper,
不绕过 ORM session (与 dashboard API 一致)。

验证:
  - initialize 握手 OK
  - tools/list 返回 5 个工具的完整 schema
  - 各 tool 在 stdio 协议下正常返回 JSON
2026-07-07 17:59:05 +08:00
gao 5cb64f181a feat(api): dashboard 看板 + config 配置页
Dashboard (M3):
  - 路由 /api/dashboard/{daily,history,today,datasets,stats}/
  - daily_summary: 按日期聚合 ok/warning/error/blocked
  - history: 同步历史详情(按 task / 日期过滤)
  - today: 今日 KPI + 任务列表
  - datasets: dataset_registry + 最近 7d run 合并
  - stats: 各表行数 + 日期范围(17 张表)

  HTML SPA: app/api/static/dashboard.html
    - 5 张状态卡片 + 7 天趋势表 + 任务状态表 + 数据规模表 + 历史详情
    - 自动 30s 刷新, 纯 vanilla JS (无框架依赖)

Config (M5):
  - 路由 /api/config/{GET list, GET key, PUT upsert, DELETE}/
  - 凭证类自动脱敏 (MAIRUI_LICENCE / XUEQIU_TOKEN / MAIRUI_RPS_LIMIT)
  - POST /api/config/reload: 把 .env / 环境变量已知项回填到 config 表
  - 分类过滤 + key 搜索 + 凭证提示

  HTML SPA: app/api/static/config.html
    - 列表 + 编辑弹窗 + 删除确认
    - 凭证类编辑时强制重新输入完整值(避免显示脱敏误以为是新值)
    - 一键从环境变量回填

FastAPI 主入口挂载:
  - include_router(dashboard, config)
  - mount('/static', StaticFiles) 提供 HTML
2026-07-07 17:54:05 +08:00
gao d217553d79 feat(sync): sync_history 表 + base.py 自动记录每次同步
新增 23 张表 sync_history:append-only,每次 task.run() 完成追加一行。

字段:
  dataset_id / run_date / status / trigger_source / started_at / finished_at /
  elapsed_sec / rows_written / message / error / stats (JSONB) / triggered_by
索引:
  (dataset_id, started_at) / (started_at) / (status)

vs dataset_registry 的区别:
  - dataset_registry 只保留「最后一次状态」便于快查
  - sync_history 保留完整历史,支持看板按天/按 task 维度聚合 + 失败回溯

自动落库:
  - SyncTask.run() 成功后调 _record_history()
  - 异常路径也写(避免排查时漏掉崩溃的 run)
  - 落库失败仅 warning 不阻塞主流程

新增 db_ops helpers:
  - insert_sync_history()
  - list_sync_history(dataset_id?, days, limit) 看板/API 用
  - daily_sync_summary(days) 按日期聚合 ok/warning/error/blocked

_ensure_schema 改造:
  从 no-op 改为用 superuser 连接跑 create_all(),SA 2.x IF NOT EXISTS 幂等。
  这样新增 model 不用手动跑 pg_bootstrap。生产首次部署仍建议跑 pg_bootstrap
  (它还要建 role / schema)。

测试: tests/test_schema_models.py 22 → 23 张表
2026-07-07 16:55:51 +08:00
gao fbc3cc84f0 feat(service): dockerize — Dockerfile + docker-compose + 统一入口
把项目打包成可部署的数据同步服务:

- Dockerfile (multi-stage builder/runtime, slim base, 含 healthcheck)
- docker-compose.yml (postgres:16-alpine + market_sync, 持久化 pgdata + logs)
- .dockerignore (减 build context)
- bin/service_run.sh (单进程跑 uvicorn + scheduler 线程)
- app/entrypoints/worker.py 抽 start_scheduler_thread() (幂等)

部署:
  docker compose up -d
  # 然后访问 http://localhost:8100/docs 看 FastAPI

设计选择:
  - 单容器一服务(uvicorn 主 + scheduler daemon thread),不用 supervisord
  - 原 systemd timer 由进程内 scheduler 取代(配置已存在于 config 表)
  - PG 用 docker volume 持久化;同步日志单独 volume
2026-07-07 16:30:56 +08:00
gao a8ea132924 fix(stock_node+utils+cli): review findings P0/P1/P2 一轮修复
medium effort code-review 暴露 8 个 finding,按 P0/P1/P2 优先级修复:

P0 (数据正确性 / 跑得起来):
  #1 replace_all_node_categories 改 scope-aware delete
     之前无条件 delete(NodeCategory) + 插入 runtime filter 后的子集,
     导致 --type2 0 跑把其它 6 个 category 误删。改为只 delete 本批
     category_key 集合,其它 type2 不动。

  #2 replace_all_stock_node_map 对称化
     之前空 rows 时 early-return 留下 stale,导致「categories 新
     但 mappings 旧」不一致。改为 scope-aware delete 同 #1。

  #3 stock_node systemd --workers 20 → 8
     20 workers × 默认 10 RPS = 200 RPS,撞穿 mairui 钻石档 100 RPS
     上限,触发风控。改为 8 × 10 = 80 RPS,留 20% buffer。

P1 (静默错):
  #4 is_a_share_code 接 hermes 格式
     之前硬性要求 len(c)==6,iter_stock_codes 改返 hermes 时 5 个
     task (moneyflow/share_snapshot/kline_5min/tick_trade/kline_daily)
     会静默过滤成空 list。剥前缀再判断,兼容 'SH600519'。

  #5 mairui tz 契约钉在 source 层
     tz_localize 改为「已带 tz 就保留,没有再标 Asia/Shanghai」,
     避免 mairui 改格式时抛 TypeError。task_tick_trade 删掉
     strftime fallback + 冗长注释(契约已在 source 层 docstring)。

  #6 CLI inspect.signature 过滤 kwargs
     --type2 / --backfill 加在共享 p_sync parser 上,任何 task 通过
     **kwargs 静默吞掉。改为 dispatcher 按 task._run 签名过滤,
     不支持的参数打 warning。

P2 (维护):
  #7 task_stock_node --type2 输入校验
     之前空字符串 / 「concept」 / 99 都 silently collapse 成空 set,
     走「无匹配叶子节点」warning 分支(被 daily_check 当正常)。
     CLI 不再 silent-drop,任务层加 strict 校验,typo 返 status=error。

  #8 code6_to_exchange 删 3 个死分支
     4 个 code6_to_* wrapper 都先 _strip_hermes,3 个 hermes if 分支
     走不到。删除后 function 简化,所有 caller 行为不变。

附带:
  - tests/test_smoke.py: 11 → 13 (含 mairui_ma_daily + stock_node)
  - tests/test_schema_models.py: 21 → 22 (含 kline_stock_ma_daily +
    node_categories/nodes/stock_node_map)
  - bin/market_sync_stock_node_run.sh 注释补充 RPS 计算

验证:
  - pytest tests/ 11 passed
  - is_a_share_code 8 个 hermes 边界 case 全过
  - cli sync kline_daily --type2 2,3 → warning 已打印
  - cli sync stock_node --type2 concept → status=error
  - cli sync stock_node --type2 99 → status=error
  - cli sync stock_node --type2 0 → 仅 0:0 行被刷新,其它 6 类不动
  - fetch_tick_trade('600519') 返回 tz=Asia/Shanghai +08:00
2026-07-07 16:04:47 +08:00
gao e8e35b9716 fix(mairui): 修正 _BASE_URL 子域名(api → a)
mairui 把资金流接口 transaction 路由到 a.mairuiapi.com 子域名,
主域名 api.mairuiapi.com 调用 transaction 返回 HTTP 410 endpoint_unavailable。
其他接口(kline/5min/tick)在两个子域名均正常。

症状:moneyflow 同步 5205 全失败,0 行写入;manual --force 重跑同样 0 行。
原因:项目 _BASE_URL 写成主域名,transaction 接口请求被服务端永久拒绝。

修复:改为文档示例用的 a.mairuiapi.com 子域名。
验证:health_check 通过;moneyflow --force 5204 成 1 败,102731 行入库。
2026-07-07 10:56:28 +08:00
gao f7a2cbe5a8 fix(daily_check): 期望窗口未到不应判 missed
work #07 (2026-07-03):daily_check 在日内运行时(如 09:00/12:00/15:00)
误把'还没到时间'的任务标 missed。SCHEDULE 里有 window_end 但 _classify_task
没用到——直接判 last_success vs today_start。

修复:判定 missed 之前先比较 now vs window_end;未到则返回 not_yet
(新状态,不进 alerts),已过但今日未成功走原 missed 路径。解析失败
回落旧逻辑不引入风险。

验证 4 个用例:12:00 kline_daily→not_yet, 18:00 kline_daily→missed,
18:00 longhubang→not_yet, 22:30 longhubang→missed。全部正确。

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-03 18:25:50 +08:00
gao b351bd7595 feat: 新增 mairui 历史分时 MA 日 K 级别 sync task
work #06 (2026-07-03):user 请求加 mairui /hsdata 历史分时 MA 同步(日 K 级别)。

mairui 端点探测:/d/ma, /d/ma5/10/20, /15/ma, /30/ma, /60/ma 端点结构存在
但当前免费 licence 返 数据不存在;基础 K 线 (/d/n, /15/n, /30/n, /60/n) 正常。

策略:本地从 kline_stock 计算(pandas per-stock rolling),写新表
kline_stock_ma_daily,source=local_kline_proxy 标识本地派生。
mairui URL 留作未来升级 licence 后切 API 用。

变更:
- app/core/db/models.py: KlineStockMADaily ORM model
- app/core/db/ops.py: upsert_kline_stock_ma_daily_rows (批量 5000/批)
- app/tasks/task_mairui_ma_daily.py: SyncMairuiMADaily (全量重算)
- app/tasks/__init__.py: 注册到 TASKS dict
- app/core/sync/registry.py: SYNC_DEFINITION (sort_order=90, dep=kline_daily)
- app/core/scheduler/scheduler.py: schedule_mairui_ma_daily @ 16:30 + register_sync_jobs
- bin/daily_sync_check.py: SCHEDULE entry (window_end=17:00)

烟测:11,684,592 行, 5510 只, 1370s (23min),MA5/10/20/60 全部计算。
SH600519 样本:ma5=1194.18 ma10=1191.03 ma20=1211.94 ma60=1296.20 (2026-07-03)

未来优化(不在本 work):
- 增量模式(每日只算最近 1-2 天)→ 23min → 30s
- 升级 mairui licence 切到 /d/maN API
- 加 EMA / BOLL / KDJ 等其他指标

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-03 17:36:49 +08:00
gao 46ddf8e171 feat: bin/systemd_deploy.sh 一键部署脚本
work #05 (2026-07-03):Stop hook 反馈 work #04 结尾'目标达成'是错的
预测,不是现状。timer/service 没真的跑 = 数据没真的进 PG。

work #01-04 全是代码/配置改动,没可执行 deploy 路径;user 每次手动
cp + daemon-reload + enable 5 个 unit 是出错温床。

修复:写 bin/systemd_deploy.sh,必须 sudo 跑,幂等,显示 plan → 等
用户确认 → 执行(拷贝 + 删孤儿 + reload + enable 新 timer + 验证)。

下一步:user 跑 sudo bash bin/systemd_deploy.sh 真把 unit 部署到
/etc,daily_check drift 才能归零,sync 目标才进入可观察达成路径。

重要:脚本就绪 ≠ 目标达成;脚本是必要非充分条件。

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-03 11:44:33 +08:00
gao cf45f98a8a feat(daily_check): systemd unit drift 自动检测
work #04 (2026-07-03):user 重申 sync 目标强调 timer/service/依赖/编码格式
全链路审计,发现 5 处 systemd unit drift:work #01 (4h timeout) 和 work #02
(morning.timer) 代码改了但没 cp 到 /etc/systemd/system/,等于没修。

根因:deployment 与 repo 各自演进无任何告警机制。

修复:在 daily_sync_check 加 _check_systemd_drift() 段,对比
bin/systemd/ 与 /etc/systemd/system/ 下所有 market-sync* unit:
- missing_in_etc (未部署) → status=not_deployed
- drifted (两边有但内容不一致, SHA256 比对) → status=drifted
- orphan_in_etc (/etc 有 repo 没维护, 捕获僵尸 unit) → status=orphan

drift 合入 alerts 让 webhook 也带;report schema_version 1→2。

立即验证:跑 daily_check 抓到 5 处 drift(4 未部署 + 1 内容不一致),
status=warning 让 webhook 自动发出——以后任何 deployment drift 都逃不掉。

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-03 11:37:21 +08:00
gao b1ab10b952 chore: 清理 dataset_registry 中 6 个 MySQL/SQLite-era 旧 task 行
work #03 (2026-07-03):daily_check 报告 18 行里有 6 个 disabled 旧 task
(stock_info/kline/hs300/industry/sector/scoring),占 1/3 噪音,干扰
对真正 missed 任务的注意力。

根因:PG 迁移(commit 7a985dd)时把它们 enabled=0 标记禁用但没删行。
旧 sync_script 全部指向 dashboard/api/services/sync/*.py(sibling 项目
已下线),storage_uri 指向 dashboard/data_uat/ 下的 SQLite/Parquet。

变更:
- bin/archive/clean_legacy_registry_rows.py: 一次性清理脚本,保守策略
  (enabled=0 + 旧 storage_layer 或非 app.tasks. 路径),默认 dry-run,
  显式 --apply 才 DELETE
- 实际删除 6 行:stock_info / kline / hs300 / industry / sector / scoring
- docs/works/2026-07-03-03-clean-legacy-registry.md: 完整 work 记录
  (含保守原则说明:enabled=0 但 storage_layer='pg' 的行不在清理范围)

验证:daily_check counts 从 18 行 → 12 行,disabled 段清零。

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-03 10:54:06 +08:00
gao d140fee9e7 fix: 早盘前 09:00 stock_basic + industry_sector 没触发
work #02 (2026-07-03):daily_check 复跑仍 10 missed,细看时间窗口发现
新问题——09:00 / 09:30 该跑的 2 个早盘前 task today 没触发。

根因:worker.py 进程内 scheduler 是预期的主调度(含 09:00 / 09:30),
但 worker.py 没人拉起;systemd timers 又只覆盖 15:30 / 21:xx,缺早盘档。
昨 18h 删 monitor 同时没补 worker.py → 早盘缺口暴露。

变更:
- bin/market_sync_morning_run.sh: 轻量 morning wrapper,只跑 stock_basic
  + industry_sector 串行(~25min),不复用 runall_once.py 的 8 task
- bin/systemd/market-sync-morning.{service,timer}: Mon..Fri 09:00 daily
  触发,TimeoutStartSec=3600 (1h)
- docs/works/2026-07-03-02-morning-no-trigger.md: 完整 work 记录

不在本 work 范围(更大重构):把 worker.py 拉起作为 long-running,
与 systemd timer 收敛到单一调度器——见 dual-scheduler-overlap memory。

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-03 10:50:25 +08:00
gao 295177027f fix: market-sync 2h timeout 截断 + 加 daily-check 巡检
work #01 (2026-07-03):昨天 15:30 runall 中 kline_5min 全量 69min 把 2h
窗口吃满,share_snapshot / market_regime 被 systemd SIGTERM 截断;
今天又叠加 worker.py 未长驻,导致 10 个 task missed。

变更:
- bin/systemd/market-sync.service: TimeoutStartSec 7200 → 14400 (4h)
- bin/daily_sync_check.py: 新增每日巡检,读 PG dataset_registry,
  对比 SCHEDULE 判定 ok/missed/failed/stuck/never_run,写 JSON 报告,
  失败时 POST HMAC webhook
- bin/systemd/market-sync-daily-check.{service,timer}: 23:00 daily 触发
  daily_check(替代 session-only 的 /loop 方案)
- config/daily_check_secrets.env.template: webhook 配置模板
- docs/works/2026-07-03-01-market-sync-timeout.md: 完整 work 记录
  (根因 + 中期/长期改进建议)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-03 10:48:20 +08:00
gao 7a985dddd5 refactor: PG-only 迁移 + 龙虎榜/tick/moneyflow 同步 + stock_code 统一 + mairui 编码修复
## 1. PG-only 重构
- 删 app/core/db/connection.py + schema.py (MySQL 路径)
- 新 app/core/db/{orm,models,pg_bootstrap}.py — SQLAlchemy 2.x ORM 一键建表
- 16 张业务表全在 market_data schema,原生 TIMESTAMPTZ / JSONB / Float / TEXT
- requirements.txt 删 PyMySQL 路径,加 psycopg2

## 2. 同步任务扩展(3 个新 task)
- **tick_trade** (mairui hsrl/zbjy):当天逐笔交易,21:00 发布
- **moneyflow** (mairui hsstock/history/transaction):个股资金流,21:30 发布
- **longhubang** (akshare):龙虎榜聚合层 + 席位层(2 张新表)
  - 长虎榜放宽 akshare 政策:仅"无替代源 + 烟测通过"场景允许
  - data_eastmoney 私有 API 不需要(akshare 烟测通过)
- 3-timer 设计:
  - 15:30 market-sync.service (8 base tasks via runall_once)
  - 21:05 market-sync-tick.service (tick_trade)
  - 21:35 market-sync-moneyflow.service (moneyflow)
  - 22:00 market-sync-lhb.service (longhubang,新加)
- bin/systemd/ 新增 tick / moneyflow / lhb 各 1 对 service+timer
- bin/market_sync_*_run.sh wrapper 脚本(不做法定节假日过滤,fail-open)

## 3. stock_code 统一为带 SH/SZ/BJ 前缀
- 历史 bug:stocks.code 用 SH600519,但 kline/moneyflow/tick_trade/kline_5min
  /stock_sector_map/industry 6 张表用纯 6 位 600519,跨表 JOIN 全部 0 行
- 新增 to_hermes() 工具:6位 / 9位(mairui `000001.SZ` 格式)→ 统一 SH000001
- 5 个 task 改写:用 to_hermes(code6) 写入 stock_code
- 一次性迁移 6 张表存量 154M 行(CASE WHEN 探测 + 去重 + 加前缀)
- ORM: stock_sector_map.stock_code / industry.code String(6)→String(10)

## 4. Bug 修复
- **share table stock_code 格式**:之前写 6 位不带前缀,与 stocks 不一致
  → 修 task_share_snapshot + 一次性 UPDATE 63,417 行加前缀
- **share_snapshot warning 状态错填 last_error**:
  → 加 mark_sync_warning() 走专用路径,不写 last_failure_at / last_error
- **schedule config lastRun 不同步**:
  → 加 update_job_status_for_dataset(),SyncTask.run() 完成后自动镜像
  → cli/runall 触发的 task 也能更新 schedule config

## 5. mairui UTF-8 编码修复
- 历史 bug:mairui.py:_fetch 用 latin-1 兜底解码,把所有 UTF-8 中文名
  double-encoded 写入 stocks.name(如 `歌华有线` 变成 `æ\xad\x8cå\x8d\x8e...`)
- 加 _decode_response():UTF-8 → GBK → latin-1 兜底
- 一次性修复 stocks.name 5,213 行:
  - 4,370 行 (encode('latin-1').decode('utf-8') 反向解码)
  - 616 行 (含 fullwidth A,宽松 printable 检查)
  - 820 行 (mid-character 截断,重新从 mairui 拉)

## 6. 测试
- tests/test_smoke.py: TASKS 10→11, SYNC_DEFINITIONS 10→11
- tests/test_schema_models.py: 16→18 张表,新增 longhubang_daily/seat
- pytest 11/11 passed

## 验证
- 6 张表 0 残留无前缀行
- stocks JOIN kline_stock / kline_5min / moneyflow / tick_trade / stock_sector_map:88-100% 命中
- 5,213 stocks.name 全部正确 UTF-8 中文
- pytest 11/11 passed
2026-07-01 22:00:58 +08:00
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