feat(pipeline): VPS 价格源适配器——桥分钟bar+dbbardata日线(偏差日报三价链锚点) [vps]
This commit is contained in:
@@ -0,0 +1,138 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""VPS 价格源适配器(偏差日报三价链锚点;Mac 不可跑——桥/dbbardata 仅 VPS)。
|
||||
|
||||
分钟 close:桥 xtdata shim(PYTHONPATH 前置 C:\\sanguo_bigqmt\\xtquant_bridge +
|
||||
env BIGQMT_ACCOUNT_ID/BIGQMT_REDIS_PASSWORD——口令运行时读 C:\\redis\\redis.conf,
|
||||
永不落盘/回显)。日线 OHLC/prev_close:quant_trading.db dbbardata(datareader
|
||||
同款列名)。symbol 规范化:日志/台账四种形态 → xtdata 的 XXXXXX.SH/SZ。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import re
|
||||
import sqlite3
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
_BRIDGE_PYROOT = os.environ.get(
|
||||
"SANGUO_BRIDGE_PYROOT", r"C:\sanguo_bigqmt\xtquant_bridge")
|
||||
_ACCOUNT = os.environ.get("BIGQMT_ACCOUNT_ID", "66639661")
|
||||
_DAILY_DB = os.environ.get(
|
||||
"SANGUO_DAILY_DB", r"C:\sanguo_vnpy_v2\data\quant_trading.db")
|
||||
|
||||
|
||||
def normalize_symbol(sym: str) -> str:
|
||||
"""600276.SH/600276/sh600276/600276.XSHG → 600276.SH(SZ 对称)。"""
|
||||
s = sym.strip().upper()
|
||||
m = re.match(r"^(?:SH|SZ)?(\d{6})(?:\.(?:SH|SZ|XSHG|XSHE))?$", s)
|
||||
if not m:
|
||||
return s
|
||||
code = m.group(1)
|
||||
exch = "SH" if s.startswith("SH") or s.endswith(("SH", "XSHG")) \
|
||||
or code.startswith(("6", "9", "5")) else "SZ"
|
||||
return f"{code}.{exch}"
|
||||
|
||||
|
||||
def _read_redis_password() -> str:
|
||||
with open(r"C:\redis\redis.conf", "r", errors="replace") as f:
|
||||
for line in f:
|
||||
m = re.match(r"^requirepass\s+(\S+)", line)
|
||||
if m:
|
||||
return m.group(1).strip()
|
||||
raise SystemExit("NO_REQUIREPASS_IN_CONF")
|
||||
|
||||
|
||||
def _ensure_bridge_env() -> None:
|
||||
parts = [p for p in os.environ.get("PYTHONPATH", "").split(os.pathsep)
|
||||
if p]
|
||||
if _BRIDGE_PYROOT not in parts:
|
||||
os.environ["PYTHONPATH"] = os.pathsep.join([_BRIDGE_PYROOT] + parts)
|
||||
os.environ.setdefault("BIGQMT_ACCOUNT_ID", _ACCOUNT)
|
||||
if "BIGQMT_REDIS_PASSWORD" not in os.environ:
|
||||
os.environ["BIGQMT_REDIS_PASSWORD"] = _read_redis_password()
|
||||
|
||||
|
||||
def _minute_closes(symbols: list[str], day: str) -> dict[tuple[str, str], float]:
|
||||
"""桥拉一日分钟 close → {(symbol, 'HH:MM'): close}(当日锚点一次拉齐)。"""
|
||||
_ensure_bridge_env()
|
||||
from xtquant import xtdata # noqa: WPS433(shim 需 env 先就位,延迟导入)
|
||||
start = f"{day} 09:00:00"
|
||||
end = f"{day} 15:30:00"
|
||||
out: dict[tuple[str, str], float] = {}
|
||||
codes = sorted({normalize_symbol(s) for s in symbols})
|
||||
for i in range(0, len(codes), 50):
|
||||
df = xtdata.get_market_data(
|
||||
field_list=["close"], stock_list=codes[i:i + 50], period="1m",
|
||||
start_time=start, end_time=end)
|
||||
if df is None or len(df) == 0:
|
||||
continue
|
||||
closes = df["close"] if hasattr(df, "keys") and "close" in df else df
|
||||
for code in codes[i:i + 50]:
|
||||
try:
|
||||
sub = closes[code]
|
||||
except (KeyError, TypeError, IndexError):
|
||||
continue
|
||||
for idx, v in sub.items() if hasattr(sub, "items") else []:
|
||||
minute = str(idx)[-8:-3] if len(str(idx)) >= 8 else str(idx)[-5:]
|
||||
if v == v: # NaN 剔除
|
||||
out[(code, minute)] = float(v)
|
||||
return out
|
||||
|
||||
|
||||
def _daily_bars(symbol: str, day: str) -> dict | None:
|
||||
"""dbbardata 当日+前一日日线 → {open/high/low/close/prev_close}。
|
||||
|
||||
exchange 映射(infra 核修):dbbardata 用 vt 约定 SSE/SZSE(datareader.py:158
|
||||
同款),非 SH/SZ——照抄 split 结果会静默查空、锚点全 None。
|
||||
"""
|
||||
code = normalize_symbol(symbol)
|
||||
bare, exch = code.split(".")
|
||||
exch = {"SH": "SSE", "SZ": "SZSE"}[exch]
|
||||
prev = (datetime.strptime(day, "%Y-%m-%d") - timedelta(days=10)
|
||||
).strftime("%Y-%m-%d")
|
||||
end_excl = (datetime.strptime(day, "%Y-%m-%d") + timedelta(days=1)
|
||||
).strftime("%Y-%m-%d")
|
||||
with sqlite3.connect(_DAILY_DB) as conn:
|
||||
rows = conn.execute(
|
||||
"SELECT datetime, open_price, high_price, low_price, close_price "
|
||||
"FROM dbbardata WHERE symbol=? AND exchange=? AND interval='d' "
|
||||
"AND datetime>=? AND datetime<? ORDER BY datetime",
|
||||
(bare, exch, prev, end_excl)).fetchall()
|
||||
rows = [r for r in rows if str(r[0])[:10] <= day]
|
||||
if not rows:
|
||||
return None
|
||||
cur = rows[-1]
|
||||
return {"open": cur[1], "high": cur[2], "low": cur[3], "close": cur[4],
|
||||
"prev_close": rows[-2][4] if len(rows) >= 2 else cur[1]}
|
||||
|
||||
|
||||
def make_price_fn():
|
||||
"""PriceFn:minute≠None 走桥分钟缓存;None 走 dbbardata 日 close。"""
|
||||
cache: dict[str, dict[tuple[str, str], float]] = {}
|
||||
|
||||
def fn(symbol: str, day: str, minute: str | None):
|
||||
code = normalize_symbol(symbol)
|
||||
if minute is None:
|
||||
bars = _daily_bars(code, day)
|
||||
return bars and bars["close"]
|
||||
if day not in cache:
|
||||
cache[day] = _minute_closes([code], day)
|
||||
return cache[day].get((code, minute))
|
||||
return fn
|
||||
|
||||
|
||||
def make_ohlc_fn():
|
||||
def fn(symbol: str, day: str):
|
||||
return _daily_bars(normalize_symbol(symbol), day)
|
||||
return fn
|
||||
|
||||
|
||||
def probe(day: str, symbol: str = "600276.SH") -> None:
|
||||
"""fail-loud 自检(infra 核修):已知 symbol 查不到日线/分钟→直接 raise,
|
||||
绝不静默空转(VPS 首跑/部署后冒烟必调;测试不依赖)。"""
|
||||
bars = _daily_bars(symbol, day)
|
||||
if not bars or not bars.get("close"):
|
||||
raise RuntimeError(f"probe: dbbardata 无 {symbol}@{day} 日线(exchange 映射/库路径错?)")
|
||||
mins = _minute_closes([symbol], day)
|
||||
if not mins:
|
||||
raise RuntimeError(f"probe: 桥分钟 close 空 {symbol}@{day}(shim env/形态错?)")
|
||||
print(f"probe ok: close={bars['close']} minutes={len(mins)}")
|
||||
Reference in New Issue
Block a user