158 lines
5.8 KiB
Python
158 lines
5.8 KiB
Python
from __future__ import annotations
|
|
|
|
import unittest
|
|
from datetime import datetime, timedelta
|
|
|
|
from backend.data import (
|
|
DataPolicyError,
|
|
DataQualityError,
|
|
DataSourcePolicy,
|
|
QualityEvidence,
|
|
build_data_gateway,
|
|
)
|
|
from backend.data.quality import market_timezone
|
|
|
|
|
|
class DataGatewayTests(unittest.TestCase):
|
|
def test_policy_allows_registered_calculation_source(self) -> None:
|
|
policy = DataSourcePolicy.load()
|
|
contract = policy.assert_allowed(
|
|
"market.stock_daily", "tushare", "calculation"
|
|
)
|
|
self.assertEqual(contract.primary, "tushare")
|
|
|
|
def test_policy_rejects_public_web_source_for_calculation(self) -> None:
|
|
policy = DataSourcePolicy.load()
|
|
with self.assertRaises(DataPolicyError):
|
|
policy.assert_allowed(
|
|
"observation.realtime_indices", "eastmoney", "calculation"
|
|
)
|
|
|
|
def test_policy_rejects_blocked_dataset(self) -> None:
|
|
policy = DataSourcePolicy.load()
|
|
with self.assertRaises(DataPolicyError):
|
|
policy.assert_allowed("market.level2", "unresolved", "display")
|
|
|
|
def test_gateway_uses_live_token_supplier_and_shared_ifind(self) -> None:
|
|
token = {"value": "first"}
|
|
gateway = build_data_gateway(
|
|
{"ifind_refresh_token": "refresh", "ifind_access_token": "access"},
|
|
lambda: token["value"],
|
|
)
|
|
self.assertEqual(gateway.tushare().token, "first")
|
|
token["value"] = "second"
|
|
self.assertEqual(gateway.tushare().token, "second")
|
|
self.assertIs(gateway.chart_data.ifind, gateway.ifind)
|
|
|
|
def test_server_has_no_direct_runtime_tushare_construction(self) -> None:
|
|
from pathlib import Path
|
|
|
|
source = (
|
|
Path(__file__).resolve().parents[1]
|
|
/ "backend"
|
|
/ "features"
|
|
/ "market"
|
|
/ "service.py"
|
|
).read_text(encoding="utf-8")
|
|
self.assertEqual(source.count("TushareClient(self.token)"), 1)
|
|
self.assertIn("return gateway.tushare()", source)
|
|
|
|
def test_quality_gate_accepts_matching_daily_evidence(self) -> None:
|
|
timezone = market_timezone()
|
|
now = datetime(2026, 7, 29, 16, 0, tzinfo=timezone)
|
|
gateway = build_data_gateway({})
|
|
report = gateway.require_quality(
|
|
QualityEvidence(
|
|
dataset_id="market.stock_daily",
|
|
provider_id="tushare",
|
|
data_time="2026-07-29",
|
|
observed_at=now,
|
|
actual_count=5000,
|
|
expected_count=5000,
|
|
adjustment="current-unadjusted",
|
|
units={
|
|
"open": "CNY/share", "high": "CNY/share", "low": "CNY/share",
|
|
"close": "CNY/share", "pct_chg": "percent",
|
|
"volume_shares": "share", "amount_yuan": "CNY",
|
|
},
|
|
),
|
|
"calculation",
|
|
now,
|
|
)
|
|
self.assertTrue(report.accepted)
|
|
self.assertEqual(report.coverage_ratio, 1.0)
|
|
|
|
def test_quality_gate_rejects_stale_dynamic_auction(self) -> None:
|
|
timezone = market_timezone()
|
|
now = datetime(2026, 7, 29, 9, 24, tzinfo=timezone)
|
|
gateway = build_data_gateway({})
|
|
with self.assertRaises(DataQualityError):
|
|
gateway.require_quality(
|
|
QualityEvidence(
|
|
dataset_id="market.auction_dynamic",
|
|
provider_id="ifind",
|
|
data_time=now - timedelta(seconds=30),
|
|
observed_at=now - timedelta(seconds=29),
|
|
units={
|
|
"price": "CNY/share", "volume_shares": "share",
|
|
"amount_yuan": "CNY", "pre_close": "CNY/share",
|
|
"turnover_rate_pct": "percent", "volume_ratio": "ratio",
|
|
"float_share": "share",
|
|
},
|
|
),
|
|
"calculation",
|
|
now,
|
|
)
|
|
|
|
def test_quality_gate_rejects_low_coverage_and_wrong_adjustment(self) -> None:
|
|
timezone = market_timezone()
|
|
now = datetime(2026, 7, 29, 16, 0, tzinfo=timezone)
|
|
gateway = build_data_gateway({})
|
|
report = gateway.quality.evaluate(
|
|
QualityEvidence(
|
|
dataset_id="market.stock_daily",
|
|
provider_id="tushare",
|
|
data_time="2026-07-29",
|
|
observed_at=now,
|
|
actual_count=4000,
|
|
expected_count=5000,
|
|
adjustment="forward1",
|
|
),
|
|
"calculation",
|
|
now,
|
|
)
|
|
self.assertFalse(report.accepted)
|
|
self.assertTrue(any("Coverage" in issue for issue in report.issues))
|
|
self.assertTrue(any("Adjustment" in issue for issue in report.issues))
|
|
|
|
def test_quality_gate_enforces_financial_point_in_time(self) -> None:
|
|
timezone = market_timezone()
|
|
now = datetime(2026, 7, 29, 16, 0, tzinfo=timezone)
|
|
gateway = build_data_gateway({})
|
|
report = gateway.quality.evaluate(
|
|
QualityEvidence(
|
|
dataset_id="market.fundamentals",
|
|
provider_id="tushare",
|
|
data_time="2026-06-30",
|
|
observed_at=now,
|
|
available_at="2026-08-15",
|
|
),
|
|
"calculation",
|
|
now,
|
|
)
|
|
self.assertFalse(report.accepted)
|
|
self.assertTrue(any("not available" in issue for issue in report.issues))
|
|
|
|
def test_provider_chain_never_silently_promotes_display_fallback(self) -> None:
|
|
gateway = build_data_gateway({})
|
|
self.assertEqual(
|
|
gateway.provider_chain("chart.intraday", "display"),
|
|
("ifind", "eastmoney"),
|
|
)
|
|
with self.assertRaises(RuntimeError):
|
|
gateway.provider_chain("chart.intraday", "calculation")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|