Files
market_sync/bin/market_sync_run.sh
T
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

92 lines
3.7 KiB
Bash
Executable File
Raw 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.
#!/bin/bash
# systemd 调度的入口:跑一次全量同步 + 配套结构化 watch
#
# 设计:
# - 后台启动 runall_once.py(输出重定向到时间戳 log)
# - 同时后台启动 structured_watch.sh 盯进程(13 checkpoint × 递增间隔)
# - wrapper 自身 fast-poll runall PID(每 5s 一次)
# - runall 一退出就 SIGTERM watch(不靠 watch 自己的 checkpoint 轮询,避开 30min 长 sleep
# - watch 自然结束后整个脚本退出
# - 退出码透传 runall_once.py 的 exit codesystemd 据此判断成功 / 失败
#
# 为什么不只用 structured_watch
# - 看 [[background-watcher-pitfalls]] 记忆:先承诺后验证是三大坑之一
# - structured_watch.sh 的 schedule 在后期是 30min / 60min 间隔
# - runall 死后,watch 最坏要等 60min 才发现进程没了
# - wrapper fast-poll 让响应时间 < 5s
#
# 为什么不让 systemd TimeoutStartSec= 自动收尾:
# - 超时是 SIGTERM → SIGKILL,但 SIGKILL 不给 runall / watch 写日志的机会
# - 自己 fast-poll 能优雅收尾(先 SIGTERM watch 留出写日志时间,再 SIGKILL 兜底)
set -u
PROJECT_ROOT="/home/gao/Development/quant_home/market_sync"
LOG_DIR="$PROJECT_ROOT/logs"
mkdir -p "$LOG_DIR"
TS=$(date +%Y%m%d_%H%M%S)
RUNALL_LOG="$LOG_DIR/runall_${TS}.log"
WATCH_LOG="$LOG_DIR/watch_${TS}.log"
echo "[market-sync-run] start ts=$TS"
echo "[market-sync-run] runall_log=$RUNALL_LOG"
echo "[market-sync-run] watch_log=$WATCH_LOG"
# 注:不做法定节假日过滤。理由(fail-open 原则):
# - 节假日配置(TRADING_HOLIDAYS / config 表)一旦出错(误把今天加进去、
# 漏更新本年度、笔误等)= 直接漏一天数据,且没声音
# - 大不了非交易日跑出来 mairui/baostock 返回空数据,task 报 warning 但不丢历史
# - 周末过滤由 systemd timer `OnCalendar=Mon..Fri` 完成,足够
# ── 1) 后台启动 runall_once.py ──
cd "$PROJECT_ROOT"
.venv/bin/python bin/runall_once.py > "$RUNALL_LOG" 2>&1 &
RUNALL_PID=$!
echo "[market-sync-run] runall pid=$RUNALL_PID"
# ── 2) 后台启动 structured_watch.sh(独立进程,不阻塞 wrapper)
# PATTERN 不含 "python" → 触发 watch 内部 case 第二分支,前缀 [p]ython.* 拼出
"$PROJECT_ROOT/bin/structured_watch.sh" \
"runall_once" \
"$RUNALL_LOG" \
"$WATCH_LOG" \
"market-sync-full" > /dev/null 2>&1 &
WATCH_PID=$!
echo "[market-sync-run] watch pid=$WATCH_PID"
# ── 3) wrapper 自身 fast-poll runall PID(每 5s
# 这是关键修复:runall 死后 < 5s 内就触发 watch 退出
RUNALL_EXIT_CODE=1
while kill -0 "$RUNALL_PID" 2>/dev/null; do
sleep 5
done
# runall 已退出,取 exit code
wait "$RUNALL_PID" 2>/dev/null
RUNALL_EXIT_CODE=$?
echo "[market-sync-run] runall 已退出 exit_code=$RUNALL_EXIT_CODE"
# ── 4) 通知 watch 退出(如果还在跑)──
if kill -0 "$WATCH_PID" 2>/dev/null; then
echo "[market-sync-run] 通知 watch 退出(pid=$WATCH_PID"
kill -TERM "$WATCH_PID" 2>/dev/null || true
# 给 watch 30s 自然退出(不要立即 KILL,留它写最后日志)
for _ in 1 2 3 4 5 6; do
sleep 5
kill -0 "$WATCH_PID" 2>/dev/null || break
done
if kill -0 "$WATCH_PID" 2>/dev/null; then
echo "[market-sync-run] ⚠️ watch 30s 内未退出,强制 kill -9"
kill -KILL "$WATCH_PID" 2>/dev/null || true
fi
fi
# ── 5) 输出 summary ──
SUMMARY="$LOG_DIR/runall_summary.json"
if [ -f "$SUMMARY" ]; then
echo "[market-sync-run] === summary ==="
cat "$SUMMARY"
echo "[market-sync-run] === /summary ==="
fi
echo "[market-sync-run] done exit_code=$RUNALL_EXIT_CODE"
exit "$RUNALL_EXIT_CODE"