Files
market_sync/app/tasks/task_share_snapshot.py
T
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

145 lines
5.8 KiB
Python
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.
"""同步任务:股本快照(雪球 quote_detail)。
写入两张表:
- stocks.total_share / float_share / share_updated_at(基础信息表)
- share(独立股本时序表)
"""
from __future__ import annotations
import os
import time
from concurrent.futures import ThreadPoolExecutor, as_completed
from typing import Any
from app.core.config import settings
from app.core.db import ops as db_ops
from app.core.datasource.base import registry as ds_registry
from app.core.datasource.registry import is_source_ready
from app.core.datasource.utils import is_a_share_code, to_code6
from app.core.sync.base import SyncTask
from app.core.sync.registry import mark_sync_blocked
from app.core.utils.logging import get_logger
logger = get_logger("sync.share")
class SyncShareSnapshot(SyncTask):
dataset_id = "share_snapshot"
def _run(
self,
*,
trigger_source: str = "manual",
codes: list[str] | None = None,
max_workers: int = 10,
**kwargs,
) -> dict[str, Any]:
if not settings.xueqiu_token:
mark_sync_blocked(self.dataset_id, message="未配置 XUEQIU_TOKEN")
return {"status": "blocked", "message": "未配置 XUEQIU_TOKEN"}
xq = ds_registry.get("datasource_xueqiu")
if xq is None:
return {"status": "error", "message": "雪球数据源未注册"}
ok, reason = is_source_ready(xq.key)
if not ok:
mark_sync_blocked(self.dataset_id, message=f"雪球未就绪: {reason}")
return {"status": "blocked", "message": f"雪球未就绪: {reason}"}
if codes:
stock_codes = [to_code6(c) for c in codes]
else:
stock_codes = [c for c in db_ops.iter_stock_codes(active_only=True) if is_a_share_code(c)]
limit_raw = os.environ.get("MARKET_DATA_STOCK_LIMIT", "").strip()
if limit_raw.isdigit() and int(limit_raw) > 0:
stock_codes = stock_codes[: int(limit_raw)]
if not stock_codes:
return {"status": "error", "message": "无股票代码"}
# 跳过今日已刷过的
today = time.strftime("%Y-%m-%d")
existing = {s["code"]: s for s in db_ops.fetch_all_stocks()}
todo = []
for c6 in stock_codes:
old = existing.get(c6, {})
if str(old.get("share_updated_at", "") or "") == today and old.get("total_share", 0) > 0:
continue
todo.append(c6)
skip = len(stock_codes) - len(todo)
if not todo:
return {"status": "ok", "message": f"全部 {skip} 只已最新,跳过"}
total = len(todo)
ok_cnt = fail_cnt = 0
rows_to_share_table: list[dict] = []
t0 = time.time()
with ThreadPoolExecutor(max_workers=max_workers) as pool:
futures = {pool.submit(xq.fetch_share_snapshot, c6): c6 for c6 in todo}
for i, future in enumerate(as_completed(futures), 1):
c6 = futures[future]
try:
r = future.result()
except Exception as e:
fail_cnt += 1
logger.warning(f"[share {c6}] 异常: {e}")
continue
if r is None:
fail_cnt += 1
continue
# 写 stocks + share 表 —— 两张表 stock_code 都用 hermes(带 SH/SZ 前缀)
# 与 stocks.code 保持一致,方便 JOIN。
# 历史 bug:之前 share 表写的是裸 6 位 c6,导致 share JOIN stocks 失败。
# 一次性 UPDATE2026-07-01)已把存量 6.3 万行加上前缀。
# trade_date 必须用源数据返回的交易日(数据源层负责"最近已完成交易日"
# 兜底),不允许用 today 兜底 — 7月9日 早盘跑任务时 today=7月9日,
# 但股本快照实际对应 7月8日 收盘。早期代码用 today 会让 share 表里
# 同一股本被重复写多行,统计/回溯出错。
trade_date = r.get("trade_date")
if not trade_date:
fail_cnt += 1
logger.warning(f"[share {c6}] 源数据未返回 trade_date,跳过(避免用运行日兜底)")
continue
hermes = self._to_hermes(c6)
db_ops.update_stock_share_snapshot(
code=hermes,
total_share=r["total_share"],
float_share=r["float_share"],
trade_date=trade_date,
)
rows_to_share_table.append({
"stock_code": hermes,
"trade_date": trade_date,
"total_share": r["total_share"],
"float_share": r["float_share"],
})
ok_cnt += 1
if i % 50 == 0 or i == total:
self._progress(
message=f"股本 {i}/{total} OK:{ok_cnt} FAIL:{fail_cnt}",
current=i, total=total, current_step=c6,
)
# 批量写 share 表
if rows_to_share_table:
try:
db_ops.upsert_share(rows_to_share_table)
except Exception as e:
logger.warning(f"写 share 表失败: {e}")
elapsed = round(time.time() - t0, 1)
msg = f"股本 {ok_cnt}{fail_cnt}{skip}跳, {elapsed}s"
return {
"status": "ok" if fail_cnt == 0 else "warning",
"message": msg,
"ok": ok_cnt, "fail": fail_cnt, "skip": skip,
"elapsed_sec": elapsed,
}
@staticmethod
def _to_hermes(code6: str) -> str:
if code6.startswith(("5", "6", "9")):
return f"SH{code6}"
if code6.startswith(("4", "8")):
return f"BJ{code6}"
return f"SZ{code6}"