Files
sanguo_vnpy_v2/scripts/data_platform/xt_eod.py
T
claude_dev 114a69e997 fix(data): dbbardata 日线双行根治(统一纯日期+helper,留ROWID max去重)
根因: dbbardata UNIQUE(symbol,exchange,interval,datetime) 按字符串字面比较,
多写入路径混用 'YYYY-MM-DD' 与 'YYYY-MM-DD 00:00:00' -> 同一交易日双行,
INSERT OR REPLACE 不去重 -> 回测交易日翻倍/pivot duplicate/信号异常。

方案A (统一纯日期, 详见 Main Agent 诊断):
- 新增 scripts/data_platform/dbbardata_utils.py: normalize_daily_dt(s)
  取前 10 字符, None/短串安全
- 4 个日线写入脚本写入前调 helper:
  - bs_eod.py (sanguo-bs-eod 个股日线 baostock)
  - migrate_daily_baostock.py (历史迁移)
  - xt_eod.py (sanguo-xt-eod ETF/基金 xtata)
  - import_vnpy_daily_fast.py (NAS 日线 parquet 导入, 加防御)
- TDD: tests/data_platform/test_dbbardata_utils.py 9 cases 全过
- 回归: tests/data_platform + tests/portfolio 199 passed 12 skipped

peewee DateTimeField formats 含 '%Y-%m-%d' (阶段0 VPS 实测确认),
读纯日期不崩, 方案A 前提成立。

15min 干净, 不动 (分钟必须带时分)。只改日线 interval='d'。

数据层根治, 不在 provider 适配兜底 (用户铁律)。
2026-07-23 12:18:41 +08:00

135 lines
4.7 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.
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""xt_eod.py — sanguo-xt-eod (方案A schtask 18:40): xtata ETF/基金 EOD 增量 -> dbbardata('d')。
baostock 只取 type=1 股票, 不覆盖 ETF/基金 -> xtata 独占 (spec §14)。
个股日线由 bs_eod (baostock) 灌, 此处不碰个股, 避免 dbbardata 两源冲突。
- universe = 沪深ETF 沪深基金
- download_history_data2 批量 paced -> 本地缓存
- get_market_data_ex raw(dividend_type=none) -> dbbardata('d') INSERT OR REPLACE
- volume 手->股 (×100, 与 daily_update_xtdata 同口径)
- 无限流, 单进程 download 不并发
用法: python xt_eod.py [--limit N] [--dry-run]
"""
import argparse
import datetime as dt
import sqlite3
import time
from xtquant import xtdata as xd
import pandas as pd
from dbbardata_utils import normalize_daily_dt
DB = r"C:\sanguo_vnpy_v2\data\quant_trading.db"
LOOKBACK = int(__import__("os").environ.get("LOOKBACK_DAYS", "30"))
T0 = time.time()
def log(m):
print(f"[XT-EOD {time.time()-T0:.0f}s] {m}", flush=True)
def prefix_of(sym):
return "sh" if sym[:2] in ("51", "56", "58", "50") else ("sh" if sym[:2] == "60" else "sz")
def exc_of(sym):
return "SSE" if sym[:2] in ("51", "56", "58", "50", "60", "68") else "SZSE"
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--limit", type=int, default=0)
ap.add_argument("--dry-run", action="store_true")
args = ap.parse_args()
end = dt.datetime.now().strftime("%Y%m%d")
start = (dt.datetime.now() - dt.timedelta(days=LOOKBACK)).strftime("%Y%m%d")
log(f"start window={start}~{end}")
u = list(set(
(xd.get_stock_list_in_sector("沪深ETF") or []) +
(xd.get_stock_list_in_sector("沪深基金") or [])
))
if not u:
log("FATAL empty universe (miniQMT 未连?)")
return 2
if args.limit:
u = u[:args.limit]
log(f"universe ETF+基金: {len(u)}")
# download paced
BATCH = 200
for i in range(0, len(u), BATCH):
try:
xd.download_history_data2(u[i:i+BATCH], "1d", start, end, lambda d, p: None)
except Exception as e:
log(f"dl batch@{i} err: {e}")
time.sleep(1.0)
log("download done")
conn = sqlite3.connect(DB, timeout=60)
conn.execute("PRAGMA busy_timeout = 60000")
conn.execute("PRAGMA journal_mode = WAL")
ok = fail = empty = rows = 0
conn.execute("BEGIN")
try:
for i, code in enumerate(u):
sym = code.split(".")[0]
try:
r = xd.get_market_data_ex([], [code], period="1d", start_time=start,
end_time=end, dividend_type="none")
df = r.get(code) if r else None
if df is None or not len(df):
empty += 1
continue
db = pd.DataFrame({
"symbol": sym,
"exchange": exc_of(sym),
# datetime 归一纯日期 (dbbardata 双行根治方案A)
"datetime": [normalize_daily_dt(
f"{str(idx)[:4]}-{str(idx)[4:6]}-{str(idx)[6:8]}")
for idx in df.index],
"interval": "d",
"volume": (df["volume"].astype(float).values * 100),
"turnover": df["amount"].astype(float).values,
"open_interest": 0.0,
"open_price": df["open"].astype(float).values,
"high_price": df["high"].astype(float).values,
"low_price": df["low"].astype(float).values,
"close_price": df["close"].astype(float).values,
})
if not args.dry_run:
conn.executemany(
"INSERT OR REPLACE INTO dbbardata "
"(symbol,exchange,datetime,interval,volume,turnover,open_interest,"
"open_price,high_price,low_price,close_price) VALUES (?,?,?,?,?,?,?,?,?,?,?)",
db.itertuples(index=False, name=None))
rows += len(db)
ok += 1
except Exception as e:
fail += 1
if fail <= 5:
log(f"{code} err: {e}")
if (i+1) % 200 == 0:
log(f"进度 {i+1}/{len(u)} ok={ok} empty={empty} fail={fail} rows={rows}")
conn.execute("COMMIT")
except Exception as e:
conn.execute("ROLLBACK")
log(f"FATAL rollback: {e}")
conn.close()
return 1
conn.close()
log(f"DONE ok={ok} empty={empty} fail={fail} rows={rows}"
f"{' [DRY-RUN]' if args.dry_run else ''}")
return 0
if __name__ == "__main__":
raise SystemExit(main())