Files
claude_dev 0810259911 feat(data): 15min双源集成(data_source路由 + baostock格式适配)
- _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双源分期项落地.
2026-07-09 19:34:43 +08:00

88 lines
3.3 KiB
Python
Raw Permalink 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.
"""模拟盘行情统一接口(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 是历史 mixedhfq+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" # 干净 qfqdaily_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