"""模拟盘行情统一接口(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