"""data_source 测试(mock _read_fn,不依赖 vnpy 链,spec §5/§3.3)。 read_parquet 的真测试依赖 vnpy BarData + NAS parquet,本机无完整依赖, 在容器内冒烟(spec §17);本文件测 iter_bars 调度逻辑(adjust → dir_key 路由)。 """ import logging from datetime import datetime from types import SimpleNamespace import pytest from sanguo_trader.data_source import iter_bars, fetch_day def _mock_bar(sym: str, date_str: str, close: float): return SimpleNamespace( symbol=sym, datetime=datetime.strptime(date_str, "%Y-%m-%d"), close_price=close, ) def _cfg(paths): return SimpleNamespace(data_paths=paths) def test_iter_bars_cross_section_multi_symbol(monkeypatch): seen = [] def mock_read(sym, start, end, cfg, dir_key): seen.append(dir_key) return [_mock_bar(sym, "2024-01-02", 10.5 if sym == "600000" else 15.5)] monkeypatch.setattr("sanguo_trader.data_source._read_fn", lambda iv: mock_read) sections = list(iter_bars(["600000", "000001"], "2024-01-01", "2024-01-31", "d")) assert len(sections) == 1 _date, d = sections[0] assert d["600000"].close_price == 10.5 assert all(k == "qfq_dir" for k in seen) # 默认 qfq → qfq_dir(双源) def test_iter_bars_raw_uses_raw_dir(monkeypatch, caplog): """raw 模式路由到 raw_dir,不再 fallback qfq(task #79)。""" seen = [] def mock_read(sym, start, end, cfg, dir_key): seen.append(dir_key) return [_mock_bar(sym, "2024-01-02", 10.01)] monkeypatch.setattr("sanguo_trader.data_source._read_fn", lambda iv: mock_read) with caplog.at_level(logging.WARNING): sections = list(iter_bars( ["600000"], "2024-01-01", "2024-01-31", "d", adjust="raw", cfg=_cfg({"raw_dir": "/x/raw"}), )) assert seen == ["raw_dir"] # raw → raw_dir assert "fallback" not in caplog.text.lower() assert sections[0][1]["600000"].close_price == 10.01 def test_iter_bars_raw_missing_dir_raises(monkeypatch): """raw 缺 raw_dir 配置 → 明确报错(不静默 fallback,防混源)。""" monkeypatch.setattr( "sanguo_trader.data_source._read_fn", lambda iv: lambda s, st, e, c, dir_key: [], ) with pytest.raises(ValueError, match="raw_dir"): list(iter_bars(["600000"], "2024-01-01", "2024-01-31", "d", adjust="raw", cfg=_cfg({}))) def test_iter_bars_raw_15min_routes_dedicated_dir(monkeypatch): """raw 15min 路由 minute_15_raw_dir(baostock 双源,分期项已落地)。""" seen = [] monkeypatch.setattr( "sanguo_trader.data_source._read_fn", lambda iv: lambda s, st, e, c, dir_key: seen.append(dir_key) or [], ) list(iter_bars(["600000"], "2024-01-01", "2024-01-31", "15m", adjust="raw", cfg=_cfg({"minute_15_raw_dir": "/x"}))) assert seen == ["minute_15_raw_dir"] # 缺 minute_15_raw_dir 配置 → 明确报错(不 fallback,防混源) with pytest.raises(ValueError, match="minute_15_raw_dir"): list(iter_bars(["600000"], "2024-01-01", "2024-01-31", "15m", adjust="raw", cfg=_cfg({}))) def test_unsupported_interval_rejected(): with pytest.raises(ValueError): # _read_fn 在生成器首次 next 时才执行,用 list 触发 list(iter_bars(["600000"], "2024-01-01", "2024-01-31", "5m")) def test_fetch_day_returns_last_bar(monkeypatch): seen = [] def mock_read(sym, start, end, cfg, dir_key): seen.append(dir_key) return [_mock_bar(sym, "2024-01-02", 10.0), _mock_bar(sym, "2024-01-02", 10.5)] monkeypatch.setattr("sanguo_trader.data_source._read_fn", lambda iv: mock_read) bar = fetch_day("600000", "2024-01-02", "d") assert bar.close_price == 10.5 # 取最后一个 assert seen == ["qfq_dir"] # 默认 qfq → qfq_dir(双源)