b5aa18cf8d
三案皆 VPS 真桥只读探针实证(09-26 冒烟): - is_price_source_vps:minute_rows_to_map 形态归一(扁 DataFrame[index,close] 实测形态为准,兼容遗留嵌套 dict);_minute_closes 改逐票调桥(扁形态无 code 列,批量切片无法按票归属);_ensure_bridge_env 补 DEFAULT_DATA_ PROVIDER=miniqmt + PYTHONPATH 全链(bridge/vnpy 根/vnpy_v4.4.0/vnpy_qmt_ v0.3.3,SANGUO_VNPY_ROOT 可覆)——缺任一 xtdata import 即断。 - order_log_parser._t_raw_ts:epoch sanity floor=2000-01-01(生产实见 t_raw=0 退化单 3565 条→1970 幽灵日;退化/负/垃圾回退行首时间戳), 毫秒分支移入 floor 内评估。 - 测试 +3(扁/嵌套形态×2、epoch 退化回退×1)。 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
184 lines
7.6 KiB
Python
184 lines
7.6 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""VPS 价格源适配器(偏差日报三价链锚点;Mac 不可跑——桥/dbbardata 仅 VPS)。
|
||
|
||
分钟 close:桥 xtdata shim(PYTHONPATH 全链 bridge+vnpy 根+vnpy_v4.4.0+
|
||
vnpy_qmt_v0.3.3,env DEFAULT_DATA_PROVIDER/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
|
||
from typing import Any
|
||
|
||
_BRIDGE_PYROOT = os.environ.get(
|
||
"SANGUO_BRIDGE_PYROOT", r"C:\sanguo_bigqmt\xtquant_bridge")
|
||
_VNPY_ROOT = os.environ.get("SANGUO_VNPY_ROOT", r"C:\sanguo_vnpy_v2")
|
||
_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:
|
||
"""桥 env 配方(冒烟 09-26 实证):DEFAULT_DATA_PROVIDER=miniqmt +
|
||
PYTHONPATH 全链(bridge/vnpy 根/vnpy_v4.4.0/vnpy_qmt_v0.3.3)——缺任一项
|
||
xtdata import 即报 无法连接xtquant服务。逐项 append,已有项不重复。"""
|
||
parts = [p for p in os.environ.get("PYTHONPATH", "").split(os.pathsep)
|
||
if p]
|
||
for p in (_BRIDGE_PYROOT, _VNPY_ROOT,
|
||
os.path.join(_VNPY_ROOT, "vnpy_v4.4.0"),
|
||
os.path.join(_VNPY_ROOT, "vnpy_qmt_v0.3.3")):
|
||
if p not in parts:
|
||
parts.append(p)
|
||
os.environ["PYTHONPATH"] = os.pathsep.join(parts)
|
||
os.environ.setdefault("DEFAULT_DATA_PROVIDER", "miniqmt")
|
||
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_rows_to_map(obj: Any, code: str) -> dict[tuple[str, str], float]:
|
||
"""桥 get_market_data 返回 → {(code,'HH:MM'): close}。
|
||
|
||
实测返回=扁 DataFrame[index, close](冒烟 09-26 实证;时间在 index 列
|
||
或行索引皆出值);兼容遗留嵌套 dict {code:{ts:close}} 形态。分钟键=
|
||
digits 解析("20260924 14:59:00" 与 "2026-09-24 14:59:00" 皆出 "14:59")。
|
||
"""
|
||
out: dict[tuple[str, str], float] = {}
|
||
if hasattr(obj, "iterrows"): # 扁 DataFrame
|
||
for idx, row in obj.iterrows():
|
||
v = row.get("close") if hasattr(row, "get") else row["close"]
|
||
if v is None or v != v:
|
||
continue
|
||
ts_src = row.get("index", idx) if hasattr(row, "get") else idx
|
||
digits = re.sub(r"\D", "", str(ts_src))
|
||
if len(digits) >= 12:
|
||
minute = f"{digits[8:10]}:{digits[10:12]}"
|
||
else:
|
||
continue
|
||
out[(code, minute)] = float(v)
|
||
return out
|
||
sub = obj.get(code) if hasattr(obj, "get") else None # 遗留嵌套形态
|
||
if sub is None:
|
||
return out
|
||
for idx, v in (sub.items() if hasattr(sub, "items") else []):
|
||
if v is None or v != v:
|
||
continue
|
||
digits = re.sub(r"\D", "", str(idx))
|
||
if len(digits) >= 12:
|
||
out[(code, f"{digits[8:10]}:{digits[10:12]}")] = float(v)
|
||
return out
|
||
|
||
|
||
def _minute_closes(symbols: list[str], day: str) -> dict[tuple[str, str], float]:
|
||
"""桥拉一日分钟 close → {(symbol, 'HH:MM'): close}(当日锚点一次拉齐)。
|
||
|
||
逐票调桥(冒烟 09-26 实证:返回扁 DataFrame 无 code 列,批量切片无法
|
||
按票归属);形态解析归一走 minute_rows_to_map。
|
||
"""
|
||
_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 code in codes:
|
||
result = xtdata.get_market_data(
|
||
field_list=["close"], stock_list=[code], period="1m",
|
||
start_time=start, end_time=end)
|
||
out.update(minute_rows_to_map(result, code))
|
||
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=20)
|
||
).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。
|
||
|
||
缓存键=(day, code)——只按 day 键会首票之后全票静默拿 None 锚(终审核修)。
|
||
"""
|
||
cache: dict[tuple[str, 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"]
|
||
key = (day, code)
|
||
if key not in cache:
|
||
cache[key] = _minute_closes([code], day)
|
||
return cache[key].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/形态错?)")
|
||
if not any(re.match(r"^\d{2}:\d{2}$", k[1]) for k in mins):
|
||
raise RuntimeError(
|
||
f"probe: 分钟键形态异常 {symbol}@{day} keys={list(mins)[:3]}"
|
||
"(索引形态与解析不符)")
|
||
print(f"probe ok: close={bars['close']} minutes={len(mins)}")
|