Files
sanguo_vnpy_v2/tests/portfolio/test_local_unified_provider.py
T
claude_dev 41cc6d13bf feat(portfolio): LocalUnifiedProvider spec §6 使用层落地 + VPS E2E(Task6)
spec §6 使用层 provider — 读方案A 权威数据层, 零 online, 治幸存者偏差:
- get_price: dbbardata('d') raw + bs_adjust_factor 前复权(asof, qfq[t]=raw[t]*factor[t])
- get_index_stocks/get_constituent: constituent_unified 并集治偏差(300=940含被踢, 无date时点)
- get_fundamentals_df: baostock pe/pb/ps/pcf + akshare 市值 + 三表委托 LocalParquetProvider
- 辅助: trade_days/security_info/current_tick/split_dividend/all_securities

VPS E2E 实证修复(Mac fixture 盲区):
- dbbardata datetime 混合格式("2024-09-26" vs "2024-09-26 00:00:00")
  → pd.to_datetime format='mixed' + SQL substr(datetime,1,10) 比日期(字符串比漏边界)
- 补 TestMixedDatetimeFormat 单测覆盖

验证: VPS 真数据 E2E 全通过(600519在市raw/qfq复权/000005退市治偏差/510300ETF/
fundamentals市值+pe+eps全字段/辅助方法); Mac 37单测+149回归绿

交付: 使用说明 docs/portfolio_local_unified_provider.md(其他 session 直用)+
plan+probe+E2E 脚本
2026-07-23 08:25:44 +08:00

564 lines
22 KiB
Python

"""LocalUnifiedProvider 单元测试(spec §6 使用层)。
Mac 本地 TDD: sqlite tmp_path + tmp parquet fixture,零 VPS 依赖,零网络。
覆盖:
- ``jq_to_dbbardata`` / ``dbbardata_to_jq`` / ``_jq_to_bs_code`` 代码转换
- ``_build_qfq_factor`` 复权因子构造(asof)
- ``get_price`` dbbardata('d') raw + fq='qfq' 前复权 + panel=False 长表
- ``get_index_stocks`` constituent_unified 并集(治偏差,无 date 时点)
- ``get_fundamentals_df`` pe/pb/ps/pcf←valuation_baostock + 市值←static akshare + 三表委托 LocalParquetProvider
- 辅助方法 get_trade_days / get_all_securities / get_security_info / get_current_tick / get_split_dividend
"""
from __future__ import annotations
import sqlite3
from typing import Any, Dict, List
import pandas as pd
import pytest
from sanguo_portfolio.providers.local_unified_provider import (
jq_to_dbbardata,
dbbardata_to_jq,
_jq_to_bs_code,
_build_qfq_factor,
LocalUnifiedProvider,
)
# ======================== Task 0: 代码转换 ========================
class TestCodeFormat:
def test_jq_to_dbbardata_sh(self):
assert jq_to_dbbardata("600519.XSHG") == ("600519", "SSE")
def test_jq_to_dbbardata_sz(self):
assert jq_to_dbbardata("000001.XSHE") == ("000001", "SZSE")
def test_jq_to_dbbardata_pure_digit_sh(self):
# 6 开头 → SSE
assert jq_to_dbbardata("600519") == ("600519", "SSE")
def test_jq_to_dbbardata_pure_digit_sz(self):
# 0/3 开头 → SZSE
assert jq_to_dbbardata("000001") == ("000001", "SZSE")
assert jq_to_dbbardata("300001") == ("300001", "SZSE")
def test_dbbardata_to_jq_sh(self):
assert dbbardata_to_jq("600519", "SSE") == "600519.XSHG"
def test_dbbardata_to_jq_sz(self):
assert dbbardata_to_jq("000001", "SZSE") == "000001.XSHE"
def test_round_trip_jq_to_dbbardata_to_jq(self):
original = "600519.XSHG"
sym, exc = jq_to_dbbardata(original)
assert dbbardata_to_jq(sym, exc) == original
def test_jq_to_bs_code_sh(self):
# 600519.XSHG → 'sh.600519'(bs_adjust_factor.code 格式)
assert _jq_to_bs_code("600519.XSHG") == "sh.600519"
def test_jq_to_bs_code_sz(self):
assert _jq_to_bs_code("000001.XSHE") == "sz.000001"
# ======================== Task 0: 复权因子 asof ========================
class TestBuildQfqFactor:
def test_asof_before_all_events_uses_earliest(self, tmp_path):
# 2 除权事件, 最新=1.0
db = tmp_path / "t.db"
c = sqlite3.connect(str(db))
c.execute(
"CREATE TABLE bs_adjust_factor(code TEXT, dividOperateDate TEXT, "
"foreAdjustFactor REAL, backAdjustFactor REAL, adjustFactor REAL)"
)
c.executemany(
"INSERT INTO bs_adjust_factor VALUES(?,?,?,?,?)",
[
("sh.600519", "2024-06-19", 0.90, 0, 0),
("sh.600519", "2025-06-19", 1.00, 0, 0),
],
)
c.commit()
c.close()
# 2023 早于所有事件 → 用最早 factor=0.90
dates = pd.to_datetime(["2023-01-01"])
f = _build_qfq_factor("sh.600519", sqlite3.connect(str(db)), dates)
assert abs(f.iloc[0] - 0.90) < 1e-6
def test_asof_between_events(self, tmp_path):
db = tmp_path / "t.db"
c = sqlite3.connect(str(db))
c.execute(
"CREATE TABLE bs_adjust_factor(code TEXT, dividOperateDate TEXT, "
"foreAdjustFactor REAL, backAdjustFactor REAL, adjustFactor REAL)"
)
c.executemany(
"INSERT INTO bs_adjust_factor VALUES(?,?,?,?,?)",
[
("sh.600519", "2024-06-19", 0.90, 0, 0),
("sh.600519", "2025-06-19", 1.00, 0, 0),
],
)
c.commit()
c.close()
# 2024-07 在两事件之间 → ≤ 的最大事件是 2024-06-19, factor=0.90
dates = pd.to_datetime(["2024-07-01"])
f = _build_qfq_factor("sh.600519", sqlite3.connect(str(db)), dates)
assert abs(f.iloc[0] - 0.90) < 1e-6
def test_asof_after_all_events_uses_latest(self, tmp_path):
db = tmp_path / "t.db"
c = sqlite3.connect(str(db))
c.execute(
"CREATE TABLE bs_adjust_factor(code TEXT, dividOperateDate TEXT, "
"foreAdjustFactor REAL, backAdjustFactor REAL, adjustFactor REAL)"
)
c.executemany(
"INSERT INTO bs_adjust_factor VALUES(?,?,?,?,?)",
[
("sh.600519", "2024-06-19", 0.90, 0, 0),
("sh.600519", "2025-06-19", 1.00, 0, 0),
],
)
c.commit()
c.close()
# 2025-07 晚于所有事件 → 最新 factor=1.00
dates = pd.to_datetime(["2025-07-01"])
f = _build_qfq_factor("sh.600519", sqlite3.connect(str(db)), dates)
assert abs(f.iloc[0] - 1.00) < 1e-6
def test_no_events_returns_ones(self, tmp_path):
db = tmp_path / "t.db"
c = sqlite3.connect(str(db))
c.execute(
"CREATE TABLE bs_adjust_factor(code TEXT, dividOperateDate TEXT, "
"foreAdjustFactor REAL, backAdjustFactor REAL, adjustFactor REAL)"
)
c.commit()
c.close()
# 无事件 → 全 1.0
dates = pd.to_datetime(["2024-01-01", "2024-06-01"])
f = _build_qfq_factor("sh.600519", sqlite3.connect(str(db)), dates)
assert len(f) == 2
assert abs(f.iloc[0] - 1.0) < 1e-6
assert abs(f.iloc[1] - 1.0) < 1e-6
# ======================== Task 1: get_price fixture ========================
@pytest.fixture
def unified_provider(tmp_path):
"""造小样本 sqlite fixture: dbbardata 日线 + bs_adjust_factor。"""
db = tmp_path / "quant_trading.db"
c = sqlite3.connect(str(db))
c.execute(
"CREATE TABLE dbbardata(symbol TEXT, exchange TEXT, datetime TEXT, "
"interval TEXT, volume REAL, turnover REAL, open_interest REAL, "
"open_price REAL, high_price REAL, low_price REAL, close_price REAL)"
)
# 600519: 除权日 2024-06-19 raw close 1000 → 900 跳水
rows = [
("600519", "SSE", "2024-06-18 00:00:00", "d", 1000, 1e6, 0,
1000.0, 1010.0, 990.0, 1000.0),
("600519", "SSE", "2024-06-19 00:00:00", "d", 1000, 1e6, 0,
900.0, 910.0, 890.0, 900.0),
("600519", "SSE", "2024-06-20 00:00:00", "d", 1000, 1e6, 0,
910.0, 920.0, 900.0, 910.0),
]
c.executemany("INSERT INTO dbbardata VALUES(?,?,?,?,?,?,?,?,?,?,?)", rows)
# 复权因子: 2024-06-19 起除权, factor=0.9
c.execute(
"CREATE TABLE bs_adjust_factor(code TEXT, dividOperateDate TEXT, "
"foreAdjustFactor REAL, backAdjustFactor REAL, adjustFactor REAL)"
)
c.execute(
"INSERT INTO bs_adjust_factor VALUES('sh.600519','2024-06-19',0.9,0,0)"
)
c.commit()
c.close()
return LocalUnifiedProvider({"db_path": str(db), "data_dir": str(tmp_path)})
# ======================== Task 1: get_price daily ========================
class TestGetPrice:
def test_raw_keeps_original_prices(self, unified_provider):
# raw: 除权日 close=900 跳水(原值)
df = unified_provider.get_price(
"600519.XSHG",
start_date="2024-06-18",
end_date="2024-06-20",
fq="raw",
)
assert len(df) == 3
assert abs(df.loc["2024-06-19", "close"] - 900.0) < 1e-6
def test_qfq_earlier_date_uses_earliest_factor(self, unified_provider):
# 2024-06-18 早于除权日 06-19 → factor=0.9 → 1000*0.9=900
df = unified_provider.get_price(
"600519.XSHG",
start_date="2024-06-18",
end_date="2024-06-20",
fq="qfq",
)
assert abs(df.loc["2024-06-18", "close"] - 900.0) < 1e-6
def test_qfq_after_event_uses_event_factor(self, unified_provider):
# 2024-06-19/20 ≥ 除权日 → factor=0.9 → 900*0.9=810, 910*0.9=819
df = unified_provider.get_price(
"600519.XSHG",
start_date="2024-06-19",
end_date="2024-06-20",
fq="qfq",
)
assert abs(df.loc["2024-06-19", "close"] - 810.0) < 1e-6
assert abs(df.loc["2024-06-20", "close"] - 819.0) < 1e-6
def test_panel_false_returns_long_table(self, unified_provider):
# panel=False → 长表含 time + code 列
df = unified_provider.get_price(
"600519.XSHG",
end_date="2024-06-20",
count=2,
panel=False,
fields=["close"],
)
assert "code" in df.columns
assert "time" in df.columns
assert len(df) == 2
assert "600519.XSHG" in set(df["code"])
def test_fields_with_missing_column_fills_nan(self, unified_provider):
# high_limit 不在 dbbardata → NaN 降级(策略 prepare_stock_list 涨停识别降级)
df = unified_provider.get_price(
"600519.XSHG",
end_date="2024-06-20",
count=1,
panel=False,
fields=["close", "high_limit"],
)
assert "high_limit" in df.columns
# high_limit NaN(不崩)
assert pd.isna(df.iloc[0]["high_limit"]) or df.iloc[0]["high_limit"] != df.iloc[0]["high_limit"]
def test_minute_frequency_returns_empty(self, unified_provider):
# 1m 频率无数据 → 返空 DataFrame
df = unified_provider.get_price(
"600519.XSHG",
end_date="2024-06-20",
frequency="1m",
count=1,
panel=False,
)
assert isinstance(df, pd.DataFrame)
assert df.empty
def test_multi_stocks_panel_false(self, tmp_path):
# 多股 panel=False → 长表含 code 列区分
db = tmp_path / "t.db"
c = sqlite3.connect(str(db))
c.execute(
"CREATE TABLE dbbardata(symbol TEXT, exchange TEXT, datetime TEXT, "
"interval TEXT, volume REAL, turnover REAL, open_interest REAL, "
"open_price REAL, high_price REAL, low_price REAL, close_price REAL)"
)
rows = [
("600519", "SSE", "2024-06-19 00:00:00", "d", 1000, 1e6, 0,
900.0, 910.0, 890.0, 900.0),
("000001", "SZSE", "2024-06-19 00:00:00", "d", 1000, 1e6, 0,
10.0, 10.5, 9.8, 10.2),
]
c.executemany("INSERT INTO dbbardata VALUES(?,?,?,?,?,?,?,?,?,?,?)", rows)
c.commit()
c.close()
p = LocalUnifiedProvider({"db_path": str(db), "data_dir": str(tmp_path)})
df = p.get_price(
["600519.XSHG", "000001.XSHE"],
end_date="2024-06-19",
count=1,
panel=False,
fields=["close"],
)
assert len(df) == 2
assert set(df["code"]) == {"600519.XSHG", "000001.XSHE"}
# ======================== Task 2: get_index_stocks ========================
def _make_constituent_db(tmp_path):
db = tmp_path / "t.db"
c = sqlite3.connect(str(db))
c.execute(
"CREATE TABLE constituent_unified(index_code TEXT, code TEXT, code_name TEXT, "
"source TEXT, in_current INT, was_removed INT)"
)
c.executemany(
"INSERT INTO constituent_unified VALUES(?,?,?,?,?,?)",
[
("000300", "600519", "贵州茅台", "baostock", 1, 0),
("000300", "000001", "平安银行", "baostock", 1, 0),
("000300", "600811", "退市股", "baostock", 0, 1), # 被踢
],
)
c.commit()
c.close()
return db
class TestGetIndexStocks:
def test_union_of_current_and_removed(self, tmp_path):
db = _make_constituent_db(tmp_path)
p = LocalUnifiedProvider({"db_path": str(db), "data_dir": str(tmp_path)})
stocks = p.get_index_stocks("000300.XSHG", "2020-01-01")
# 并集含被踢(was_removed=1)
assert "600519.XSHG" in stocks
assert "000001.XSHE" in stocks
assert "600811.XSHG" in stocks # 6 开头 → SSE
assert len(stocks) == 3
def test_date_param_ignored_union_model(self, tmp_path):
# 并集模型 — date 参数不报错不过滤
db = _make_constituent_db(tmp_path)
p = LocalUnifiedProvider({"db_path": str(db), "data_dir": str(tmp_path)})
s1 = p.get_index_stocks("000300", "2010-01-01")
s2 = p.get_index_stocks("000300", "2024-12-31")
assert set(s1) == set(s2)
def test_get_constituent_is_alias(self, tmp_path):
db = _make_constituent_db(tmp_path)
p = LocalUnifiedProvider({"db_path": str(db), "data_dir": str(tmp_path)})
a = p.get_index_stocks("000300.XSHG", "2020-01-01")
b = p.get_constituent("000300", "2020-01-01")
assert a == b
def test_index_not_found_returns_empty(self, tmp_path):
db = _make_constituent_db(tmp_path)
p = LocalUnifiedProvider({"db_path": str(db), "data_dir": str(tmp_path)})
assert p.get_index_stocks("999999.XSHG", "2024-01-01") == []
def test_pure_digit_index_code(self, tmp_path):
# 纯数字 index_symbol 也能查
db = _make_constituent_db(tmp_path)
p = LocalUnifiedProvider({"db_path": str(db), "data_dir": str(tmp_path)})
stocks = p.get_index_stocks("000300", "2020-01-01")
assert len(stocks) == 3
# ======================== Task 3: get_fundamentals_df fixture ========================
def _make_fundamentals_fixture(tmp_path):
"""造 valuation_baostock + static/valuation + static/income + static/balance 样本。"""
# 1. valuation_baostock/2024.parquet(baostock 权威: pe/pb/ps/pcf)
vdir = tmp_path / "valuation_baostock"
vdir.mkdir()
pd.DataFrame({
"symbol": ["600519"],
"exchange": ["SH"],
"date": ["2024-09-30"],
"peTTM": [25.0],
"psTTM": [15.0],
"pcfNcfTTM": [20.0],
"pbMRQ": [7.5],
"turn": [0.1],
"pctChg": [1.0],
"isST": [0],
}).to_parquet(vdir / "2024.parquet")
# 2. static/valuation akshare(市值/股本)
sdir = tmp_path / "static" / "valuation"
sdir.mkdir(parents=True)
pd.DataFrame({
"数据日期": ["2024-09-30"],
"总市值": [2e12],
"流通市值": [1.5e12],
"总股本": [1.256e9],
"PE(TTM)": [25.0],
"市净率": [7.5],
}).to_parquet(sdir / "600519.SH_valuation.parquet")
# 3. static/income akshare(eps + yoy + net_profit)
idir = tmp_path / "static" / "income"
idir.mkdir(parents=True)
pd.DataFrame({
"SECUCODE": ["600519.SH"],
"REPORT_DATE": ["2024-09-30"],
"REPORT_TYPE": ["Q3"],
"BASIC_EPS": [41.0],
"OPERATE_INCOME": [3.7e10],
"PARENT_NETPROFIT": [9.5e9],
"OPERATE_INCOME_YOY": [15.0],
"OPERATE_PROFIT_YOY": [14.0],
}).to_parquet(idir / "600519.SH_income.parquet")
# 4. static/balance akshare(资产/负债/权益)
bdir = tmp_path / "static" / "balance"
bdir.mkdir(parents=True)
pd.DataFrame({
"SECUCODE": ["600519.SH"],
"REPORT_DATE": ["2024-09-30"],
"REPORT_TYPE": ["Q3"],
"TOTAL_ASSETS": [2.5e11],
"TOTAL_LIABILITIES": [5.4e10],
"TOTAL_PARENT_EQUITY": [2.2e11],
"SURPLUS_RESERVE": [8e10],
"UNASSIGN_RPOFIT": [7e10],
}).to_parquet(bdir / "600519.SH_balance.parquet")
db = tmp_path / "t.db"
c = sqlite3.connect(str(db))
c.execute("CREATE TABLE dbbardata(symbol TEXT, exchange TEXT, datetime TEXT, interval TEXT)")
c.execute("INSERT INTO dbbardata VALUES('600519','SSE','2024-09-30 00:00:00','d')")
c.commit()
c.close()
return db
# ======================== Task 3: get_fundamentals_df ========================
class TestGetFundamentals:
def test_pe_pb_from_baostock(self, tmp_path):
db = _make_fundamentals_fixture(tmp_path)
p = LocalUnifiedProvider({"db_path": str(db), "data_dir": str(tmp_path)})
df = p.get_fundamentals_df(["600519.XSHG"], date="2024-09-30")
assert not df.empty
# baostock 权威: peTTM=25 / pbMRQ=7.5
assert abs(df.loc["600519.XSHG", "pe_ratio"] - 25.0) < 1e-6
assert abs(df.loc["600519.XSHG", "pb_ratio"] - 7.5) < 1e-6
assert abs(df.loc["600519.XSHG", "ps_ratio"] - 15.0) < 1e-6
assert abs(df.loc["600519.XSHG", "pcf_ratio"] - 20.0) < 1e-6
def test_market_cap_from_akshare(self, tmp_path):
# baostock valuation 无市值列 → 从 static/valuation akshare 补
db = _make_fundamentals_fixture(tmp_path)
p = LocalUnifiedProvider({"db_path": str(db), "data_dir": str(tmp_path)})
df = p.get_fundamentals_df(["600519.XSHG"], date="2024-09-30")
# 2e12 元 → 2e4 亿
assert abs(df.loc["600519.XSHG", "market_cap"] - 2e4) < 1
# 1.5e12 元 → 1.5e4 亿
assert abs(df.loc["600519.XSHG", "circulating_market_cap"] - 1.5e4) < 1
def test_three_tables_delegated(self, tmp_path):
# 三表(income/balance)从 static akshare 读(委托 LocalParquetProvider)
db = _make_fundamentals_fixture(tmp_path)
p = LocalUnifiedProvider({"db_path": str(db), "data_dir": str(tmp_path)})
df = p.get_fundamentals_df(["600519.XSHG"], date="2024-09-30")
row = df.loc["600519.XSHG"]
# eps 来自 income.BASIC_EPS
assert abs(row["eps"] - 41.0) < 1e-6
# 总负债 5.4e10 元 → 540 亿
assert abs(row["total_liability"] - 540.0) < 1
# 归母权益 2.2e11 元 → 2200 亿
assert abs(row["total_sheet_owner_equities"] - 2200.0) < 1
# 留存收益 = 盈余公积 8e10 + 未分配利润 7e10 = 1.5e11 元 → 1500 亿
assert abs(row["retained_profit"] - 1500.0) < 1
# OPERATE_INCOME_YOY 15.0% → 0.15
assert abs(row["inc_revenue_year_on_year"] - 0.15) < 1e-6
def test_required_columns_present(self, tmp_path):
db = _make_fundamentals_fixture(tmp_path)
p = LocalUnifiedProvider({"db_path": str(db), "data_dir": str(tmp_path)})
df = p.get_fundamentals_df(["600519.XSHG"], date="2024-09-30")
# _FUNDAMENTAL_COLUMNS(对齐策略 all_weather)
expected = [
"code", "market_cap", "circulating_market_cap",
"pe_ratio", "pb_ratio", "ps_ratio", "pcf_ratio",
"roe", "roa", "eps", "gross_profit_margin", "net_profit_margin",
"inc_revenue_year_on_year", "inc_operation_profit_year_on_year",
"inc_total_revenue_year_on_year",
"total_liability", "total_sheet_owner_equities", "retained_profit",
"roic",
]
for col in expected:
assert col in df.columns, f"missing col: {col}"
def test_empty_stocks_returns_empty(self, tmp_path):
db = _make_fundamentals_fixture(tmp_path)
p = LocalUnifiedProvider({"db_path": str(db), "data_dir": str(tmp_path)})
df = p.get_fundamentals_df([], date="2024-09-30")
assert df.empty
# ======================== Task 4: 辅助方法 ========================
class TestAuxMethods:
def test_get_trade_days_from_dbbardata(self, unified_provider):
# dbbardata 600519 三根日线 → 3 个交易日
days = unified_provider.get_trade_days(
start_date="2024-06-18", end_date="2024-06-20"
)
assert len(days) == 3
assert all(hasattr(d, "year") for d in days)
def test_get_trade_days_count(self, unified_provider):
days = unified_provider.get_trade_days(count=2)
assert len(days) == 2
def test_get_security_info(self, unified_provider):
info = unified_provider.get_security_info("600519.XSHG")
assert info["code"] == "600519.XSHG"
assert "start_date" in info
assert "end_date" in info
def test_get_current_tick_high_limit(self, unified_provider):
# dbbardata 最近 close=910 → high_limit=910*1.1=1001
tick = unified_provider.get_current_tick("600519.XSHG")
assert tick is not None
assert abs(tick["close"] - 910.0) < 1e-6
assert abs(tick["high_limit"] - 910.0 * 1.1) < 1e-2
def test_get_split_dividend_from_adjust_factor(self, unified_provider):
# bs_adjust_factor 有 1 个事件 → 1 条记录
events = unified_provider.get_split_dividend(
"600519.XSHG", start_date="2024-01-01", end_date="2024-12-31"
)
assert len(events) >= 1
assert "date" in events[0]
def test_get_all_securities_from_dbbardata(self, tmp_path):
# dbbardata distinct symbol → DataFrame
db = tmp_path / "t.db"
c = sqlite3.connect(str(db))
c.execute(
"CREATE TABLE dbbardata(symbol TEXT, exchange TEXT, datetime TEXT, interval TEXT)"
)
c.executemany(
"INSERT INTO dbbardata VALUES(?,?,?,?)",
[
("600519", "SSE", "2024-06-19 00:00:00", "d"),
("000001", "SZSE", "2024-06-19 00:00:00", "d"),
],
)
c.commit()
c.close()
p = LocalUnifiedProvider({"db_path": str(db), "data_dir": str(tmp_path)})
df = p.get_all_securities()
assert len(df) == 2
assert "code" in df.columns
assert "600519.XSHG" in set(df["code"])
# ======================== 混合 datetime 格式(VPS 真实数据特性) ========================
class TestMixedDatetimeFormat:
"""dbbardata datetime 列混合格式(有只日期有带时间,不同 schtask/迁移写入)。
VPS E2E 实证: ``"2024-09-26"`` 与 ``"2024-09-26 00:00:00"`` 混存。
pandas 2.3 严格模式要 ``format='mixed'``(Mac 统一格式 fixture 盲区)。
"""
def test_get_price_mixed_datetime_format(self, tmp_path):
db = tmp_path / "t.db"
c = sqlite3.connect(str(db))
c.execute(
"CREATE TABLE dbbardata(symbol TEXT, exchange TEXT, datetime TEXT, interval TEXT, "
"volume REAL, turnover REAL, open_interest REAL, "
"open_price REAL, high_price REAL, low_price REAL, close_price REAL)"
)
c.executemany(
"INSERT INTO dbbardata VALUES(?,?,?,?,?,?,?,?,?,?,?)",
[
("600519", "SSE", "2024-09-25", "d", 1000, 1e6, 0, 1000.0, 1010.0, 990.0, 1000.0),
("600519", "SSE", "2024-09-26 00:00:00", "d", 1100, 1.1e6, 0, 1005.0, 1015.0, 995.0, 1010.0),
("600519", "SSE", "2024-09-27", "d", 1200, 1.2e6, 0, 1010.0, 1020.0, 1000.0, 1015.0),
],
)
c.commit()
c.close()
p = LocalUnifiedProvider({"db_path": str(db), "data_dir": str(tmp_path)})
df = p.get_price("600519.XSHG", start_date="2024-09-25", end_date="2024-09-27", fq="raw")
assert len(df) == 3 # 不崩 + 返 3 行(混合格式解析 OK)