feat(portfolio): get_security_info_batch + get_value_metrics_batch 批量接口
策略层提速第三轮(filters 通病 + 策略01): - get_security_info_batch: 2 条 SQL(symbol IN chunk + GROUP BY sym,exc 拿 min/max; constituent_unified 拿 name)替 N×2 逐只。filters.py filter_st_stock/ filter_new_stock 自动探测批量(hasattr + isinstance dict 回退逐只, 向后兼容)。 三策略 ST/次新过滤通病: 万次查询压成 2 条。 - get_value_metrics_batch: ThreadPool 并发逐只委托 lpp.get_value_metrics (多期 ROE/FCF/流动比率逻辑不变, 只并发)。策略01 价值精选提速。 - 接口3 get_ticks_batch 不做: 实证 get_current_tick 无 last_price/paused 字段 → 回测涨跌停/停牌 filter 恒不过滤(死代码), 批量化无意义; 真问题是回测 涨跌停检测失效(策略层另修)。 Mac TDD parity 测试全绿(batch==逐只); 93 回归通过。
This commit is contained in:
@@ -546,6 +546,83 @@ class TestGetFundamentalsFields:
|
||||
assert pd.isna(df.loc["000001.XSHE", "eps"]) # 缺失股票 eps NaN
|
||||
|
||||
|
||||
# ======================== Task 3c: get_security_info_batch / get_value_metrics_batch ========================
|
||||
def _make_security_info_fixture(tmp_path):
|
||||
"""dbbardata(3 只日线, 含碰撞 SSE/SZSE) + constituent_unified(含 1 ST) 供 batch 测试。"""
|
||||
db = tmp_path / "s.db"
|
||||
c = sqlite3.connect(str(db))
|
||||
c.execute(
|
||||
"CREATE TABLE dbbardata(symbol TEXT, exchange TEXT, datetime TEXT, "
|
||||
"interval TEXT, close_price REAL)"
|
||||
)
|
||||
c.executemany("INSERT INTO dbbardata VALUES(?,?,?,?,?)", [
|
||||
("600519", "SSE", "2024-06-18 00:00:00", "d", 1500.0),
|
||||
("600519", "SSE", "2024-06-20 00:00:00", "d", 1510.0),
|
||||
("000001", "SZSE", "2023-01-03 00:00:00", "d", 12.0),
|
||||
("000001", "SZSE", "2024-06-20 00:00:00", "d", 11.0),
|
||||
("000002", "SZSE", "2024-06-19 00:00:00", "d", 8.0),
|
||||
])
|
||||
c.execute("CREATE TABLE constituent_unified(code TEXT, code_name TEXT)")
|
||||
c.executemany("INSERT INTO constituent_unified VALUES(?,?)", [
|
||||
("600519", "贵州茅台"), ("000001", "平安银行"), ("000002", "*ST某某"),
|
||||
])
|
||||
c.commit()
|
||||
c.close()
|
||||
return db
|
||||
|
||||
|
||||
class TestGetSecurityInfoBatch:
|
||||
"""get_security_info_batch: 2 条 SQL 替 N×2 逐只(filters ST/次新通病)。"""
|
||||
|
||||
def test_batch_matches_per_stock(self, tmp_path):
|
||||
db = _make_security_info_fixture(tmp_path)
|
||||
p = LocalUnifiedProvider({"db_path": str(db), "data_dir": str(tmp_path)})
|
||||
codes = ["600519.XSHG", "000001.XSHE", "000002.XSHE", "999999.XSHG"]
|
||||
batch = p.get_security_info_batch(codes)
|
||||
assert set(batch.keys()) == set(codes)
|
||||
for code in codes: # 核心回归: batch == 逐只
|
||||
assert batch[code] == p.get_security_info(code), f"mismatch {code}"
|
||||
assert batch["600519.XSHG"]["start_date"] == "2024-06-18"
|
||||
assert batch["600519.XSHG"]["end_date"] == "2024-06-20"
|
||||
assert "*ST" in batch["000002.XSHE"]["display_name"] # 名字从 constituent_unified
|
||||
assert batch["999999.XSHG"]["start_date"] is None # 缺数据
|
||||
|
||||
def test_batch_empty(self, tmp_path):
|
||||
db = _make_security_info_fixture(tmp_path)
|
||||
p = LocalUnifiedProvider({"db_path": str(db), "data_dir": str(tmp_path)})
|
||||
assert p.get_security_info_batch([]) == {}
|
||||
|
||||
|
||||
class TestGetValueMetricsBatch:
|
||||
"""get_value_metrics_batch: ThreadPool 并发逐只(策略01 价值精选), 与逐只一致。"""
|
||||
|
||||
@staticmethod
|
||||
def _norm(d):
|
||||
"""NaN 容错规范化(NaN!=NaN 会让 dict==False; 用 'NaN' 占位)。"""
|
||||
if d is None:
|
||||
return None
|
||||
out = {}
|
||||
for k, v in d.items():
|
||||
if isinstance(v, list):
|
||||
out[k] = ["NaN" if (isinstance(x, float) and x != x) else x for x in v]
|
||||
elif isinstance(v, float) and v != v:
|
||||
out[k] = "NaN"
|
||||
else:
|
||||
out[k] = v
|
||||
return out
|
||||
|
||||
def test_batch_matches_per_stock(self, tmp_path):
|
||||
db = _make_fundamentals_fixture(tmp_path)
|
||||
p = LocalUnifiedProvider({"db_path": str(db), "data_dir": str(tmp_path)})
|
||||
codes = ["600519.XSHG", "999999.XSHG"]
|
||||
batch = p.get_value_metrics_batch(codes, date="2024-09-30")
|
||||
assert set(batch.keys()) == set(codes)
|
||||
for code in codes: # 核心回归: batch == 逐只(多期 dict, NaN 容错)
|
||||
assert self._norm(batch[code]) == self._norm(
|
||||
p.get_value_metrics(code, date="2024-09-30")
|
||||
), code
|
||||
|
||||
|
||||
# ======================== Task 4: 辅助方法 ========================
|
||||
class TestAuxMethods:
|
||||
def test_get_trade_days_from_dbbardata(self, unified_provider):
|
||||
|
||||
Reference in New Issue
Block a user