Files
tick-stock-panel/backend/tests/test_custom_financial_provider.py
shy3130 b63c17e9ec feat(custom): 自定义数据源支持五档盘口与财务数据接入
后端拉取/路由/能力增广链路此前已就绪, 本次补齐缺口:

- 编辑器新增「五档盘口」「财务数据」数据集 tab: 五档预填
  symbol/asks/bids; 财务预填骨架三列, 下拉可选五表全部 canonical
  列(带中文标注); 两者均为逐批代码参数快照型, 隐藏起止时间参数
- test_dataset 财务分支注入 table=metrics, 与生产同口径
- validate: depth5/financial 豁免起止时间重名检查; 新增映射目标
  重复拦截(运行时 rename DuplicateError 会整表拉空)
- mapper.map_rows 改全量推断: 默认 100 行采样把稀疏财务列推断成
  Null 列, 第 101 行数值 append ComputeError → 整表拉空(batch=100
  必现, 与 fuyao 插件同因)
- 契约文档: 五档/financial 字段契约与五表 canonical 列清单,
  映射一对一限制、缺失列跨批合并补 null 的降级语义
- 回归 6 条: table/latest 注入、validate 豁免与拦截、metrics
  探测、稀疏列推断修复

验证: 后端全量 2798 passed + 前端 155 passed + tsc 通过;
mock 接口 E2E 真连 + 浏览器 UI 用户旅程实测。
2026-10-17 22:05:56 +08:00

132 lines
4.9 KiB
Python

"""自定义源 financial 数据集回归。
一个 financial 配置覆盖全部五表(metrics/income/balance_sheet/cash_flow/
shares): 请求时注入 table 参数由上游分发; shares 表额外注入 latest。
字段只要求映射出 symbol, 指标列按 canonical 口径宽表透传, 无
period_end 的帧由同步层合并链路自然过滤。
"""
from __future__ import annotations
import polars as pl
from app.data_providers.custom.config import config_from_dict
from app.data_providers.custom.provider import GenericHTTPProvider
def _provider(**overrides) -> GenericHTTPProvider:
cfg = {
"url": "http://example.test/financial",
"response_path": "data",
"field_map": {
"symbol": "symbol", "period_end": "period_end",
"announce_date": "announce_date",
"revenue": "revenue", "net_income": "net_income",
},
**overrides,
}
return GenericHTTPProvider(config_from_dict({
"name": "eltdx", "datasets": {"financial": cfg},
}))
def test_financial_request_injects_table_parameter(monkeypatch):
provider = _provider()
captured: dict = {}
def fake_request(cfg, **kwargs):
captured.update(kwargs)
return [{"symbol": "600000.SH", "period_end": "2026-06-30",
"announce_date": "2026-08-30",
"revenue": 1.0, "net_income": 0.1}]
monkeypatch.setattr(provider, "_request_rows", fake_request)
df = provider.get_financials("income", ["600000.SH"])
assert captured["symbols"] == ["600000.SH"]
assert captured["override_params"]["table"] == "income"
assert captured["override_body"]["table"] == "income"
assert df.columns == ["symbol", "period_end", "announce_date", "revenue", "net_income"]
assert df["symbol"][0] == "600000.SH"
def test_financial_shares_request_injects_latest_flag(monkeypatch):
provider = _provider()
captured: dict = {}
monkeypatch.setattr(
provider, "_request_rows",
lambda cfg, **kwargs: (captured.update(kwargs), [])[1],
)
provider.get_financials("shares", ["600000.SH"], latest_only=True)
assert captured["override_params"]["table"] == "shares"
assert captured["override_params"]["latest"] is True
provider.get_financials("shares", ["600000.SH"], latest_only=False)
assert captured["override_params"]["latest"] is False
provider.get_financials("metrics", ["600000.SH"])
# 非 shares 表不注入 latest
assert "latest" not in captured["override_params"]
def test_financial_validate_skips_date_parameter_conflicts():
# financial 逐批只注入代码参数: 起止时间不参与请求,
# 其与 symbols_param 重名不应被判 duplicate
provider = _provider(symbols_param="range", start_param="range")
assert provider.validate() == []
assert _provider().validate() == []
def test_financial_test_dataset_probes_metrics_table(monkeypatch):
provider = _provider()
captured: dict = {}
monkeypatch.setattr(
provider, "_request_rows",
lambda cfg, **kwargs: (captured.update(kwargs), [
{"symbol": "600000.SH", "period_end": "2026-06-30"},
])[1],
)
out = provider.test_dataset("financial", ["600000.SH"])
assert out["dataset"] == "financial"
assert out["rows"] == 1
# 测试与生产同口径: 注入 table=metrics
assert captured["override_params"]["table"] == "metrics"
def test_financial_sparse_column_survives_schema_inference(monkeypatch):
"""稀疏列回归: batch=100 时前 100 行全 null 的列不得被采样推断成 Null 列。
旧行为: pl.DataFrame(rows) 默认 100 行采样 → Null dtype → 第 101 行数值
append ComputeError → 整表拉空 (fuyao 插件同因踩坑)。
"""
provider = _provider()
rows = [
{"symbol": f"00000{i}.SZ", "period_end": "2026-06-30",
"announce_date": "2026-08-30", "revenue": None}
for i in range(100)
]
rows.append({"symbol": "600519.SH", "period_end": "2026-06-30",
"announce_date": "2026-08-30", "revenue": 5.0e10})
monkeypatch.setattr(provider, "_request_rows", lambda cfg, **kwargs: rows)
df = provider.get_financials("metrics", ["600519.SH"])
assert df["revenue"].dtype == pl.Float64
assert df["revenue"].drop_nulls().to_list() == [5.0e10]
def test_duplicate_mapped_targets_are_rejected():
"""两个外部字段映射同一内部列: rename 阶段 DuplicateError → 整表拉空,
validate 必须在保存时拦截 (前端映射行允许选重)。"""
provider = GenericHTTPProvider(config_from_dict({
"name": "eltdx",
"datasets": {"financial": {
"url": "http://example.test/financial",
"field_map": {"symbol": "symbol", "eps": "eps_basic",
"eps_dil": "eps_basic"}, # eps_dil 与 eps 同目标
}},
}))
errors = provider.validate()
assert "financial: duplicate mapped target fields: eps_basic" in errors