0810259911
- _resolve_dir_key: 15min支持raw/qfq双源(移除raw15min抛错) - _check_adjust_cfg: 按interval查dir(minute_15_raw_dir/qfq_dir) - read_parquet_15min: datetime列优先(baostock时分,旧date兼容) - config: 加 minute_15_qfq_dir/minute_15_raw_dir - test: 更新raw15min路由断言(6 passed) 实测容器: 600000 15min qfq 336bars close6.1977 / raw 336bars close6.6200, datetime时分正确(09:45:00). 15min双源分期项落地.
88 lines
3.3 KiB
Python
88 lines
3.3 KiB
Python
"""模拟盘行情统一接口(raw 真实价 / qfq 前复权 双源,spec §3.3 / §5)。
|
||
|
||
双源(分红除权准确方案):
|
||
- raw:撮合/涨跌停/成交价用真实价(adjustflag=3 / akshare adjust=""),cfg.data_paths['raw_dir']
|
||
- qfq:策略信号用前复权(无除权缺口,MA 信号准),cfg.data_paths['qfq_dir']
|
||
- daily_dir 是历史 mixed(hfq+raw),仅 backtest 兼容,模拟盘不用
|
||
|
||
_read_fn 内 lazy import datareader,避免模块级依赖 vnpy 链(tzlocal 等),
|
||
本机无 vnpy 完整依赖时仍可 import + 单测(mock _read_fn)。
|
||
"""
|
||
import logging
|
||
from collections.abc import Iterator
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
def _read_fn(interval: str):
|
||
"""按 interval 返回读取函数(lazy import)。"""
|
||
if interval == "d":
|
||
from sanguo_data.datareader import read_parquet_daily
|
||
return read_parquet_daily
|
||
if interval == "15m":
|
||
from sanguo_data.datareader import read_parquet_15min
|
||
return read_parquet_15min
|
||
raise ValueError(f"不支持的 interval: {interval}(首版仅 d / 15m)")
|
||
|
||
|
||
def _resolve_dir_key(adjust: str, interval: str) -> str:
|
||
"""adjust+interval → cfg.data_paths 目录 key。日线/15min 均支持 raw/qfq 双源。"""
|
||
if interval == "d":
|
||
if adjust == "raw":
|
||
return "raw_dir"
|
||
if adjust == "qfq":
|
||
return "qfq_dir" # 干净 qfq(daily_dir 是 mixed,仅 backtest 兼容)
|
||
return "daily_dir"
|
||
if interval in ("15m", "15min"):
|
||
if adjust == "raw":
|
||
return "minute_15_raw_dir"
|
||
if adjust == "qfq":
|
||
return "minute_15_qfq_dir"
|
||
return "minute_15_dir"
|
||
raise ValueError(f"不支持的 interval: {interval}(仅 d / 15m)")
|
||
|
||
|
||
def _check_adjust_cfg(adjust: str, cfg, interval: str = "d") -> None:
|
||
"""raw/qfq 需对应 dir 配置,缺失明确报错(不静默 fallback,避免混源)。"""
|
||
need = {
|
||
("raw", "d"): "raw_dir", ("qfq", "d"): "qfq_dir",
|
||
("raw", "15m"): "minute_15_raw_dir", ("qfq", "15m"): "minute_15_qfq_dir",
|
||
}.get((adjust, interval))
|
||
if need and cfg and need not in getattr(cfg, "data_paths", {}):
|
||
raise ValueError(
|
||
f"{adjust} 模式需 cfg.data_paths['{need}'](未配置;"
|
||
f"先用对应下载脚本生成 parquet)"
|
||
)
|
||
|
||
|
||
def iter_bars(
|
||
symbols: list[str],
|
||
start: str,
|
||
end: str,
|
||
interval: str,
|
||
adjust: str = "qfq",
|
||
cfg=None,
|
||
) -> Iterator[tuple]:
|
||
"""按日期 cross-section yield (date, {symbol: BarData})。"""
|
||
dir_key = _resolve_dir_key(adjust, interval)
|
||
_check_adjust_cfg(adjust, cfg, interval)
|
||
read_fn = _read_fn(interval)
|
||
by_date: dict = {}
|
||
for sym in symbols:
|
||
for bar in read_fn(sym, start, end, cfg, dir_key):
|
||
dt = bar.datetime
|
||
key = dt.date() if hasattr(dt, "date") else dt
|
||
by_date.setdefault(key, {})[sym] = bar
|
||
for date in sorted(by_date.keys()):
|
||
yield date, by_date[date]
|
||
|
||
|
||
def fetch_day(symbol: str, date: str, interval: str,
|
||
adjust: str = "qfq", cfg=None):
|
||
"""实走模式拉当日 bar(C-S3 用)。"""
|
||
dir_key = _resolve_dir_key(adjust, interval)
|
||
_check_adjust_cfg(adjust, cfg, interval)
|
||
read_fn = _read_fn(interval)
|
||
bars = read_fn(symbol, date, date, cfg, dir_key)
|
||
return bars[-1] if bars else None
|