fix: validate Shenwan constituent coverage
This commit is contained in:
@@ -8,7 +8,11 @@ from unittest.mock import MagicMock, patch
|
||||
from heaven_engine import _market_line_scores, build_manual_market_hexagram
|
||||
from realtime_aggregator import WebRealtimeAggregator
|
||||
from server import DashboardService
|
||||
from tushare_client import TushareClient
|
||||
from tushare_client import (
|
||||
TushareClient,
|
||||
_filter_members_by_listing,
|
||||
_sector_coverage_issue,
|
||||
)
|
||||
|
||||
|
||||
class HeavenMarketLineTests(unittest.TestCase):
|
||||
@@ -196,6 +200,75 @@ class HeavenMarketLineTests(unittest.TestCase):
|
||||
|
||||
|
||||
class ShenwanMembershipTests(unittest.TestCase):
|
||||
def test_confirmed_delisted_members_are_removed_for_the_target_date(self):
|
||||
members = [
|
||||
{"ts_code": "601318.SH", "name": "中国平安"},
|
||||
{"ts_code": "601319.SH", "name": "中国人保"},
|
||||
{"ts_code": "000627.SZ", "name": "退市成员"},
|
||||
{"ts_code": "999999.SZ", "name": "状态未知成员"},
|
||||
]
|
||||
reference = {
|
||||
"601318.SH": {"list_date": "20070228", "delist_date": ""},
|
||||
"601319.SH": {"list_date": "20181030", "delist_date": ""},
|
||||
"000627.SZ": {"list_date": "19961112", "delist_date": "20250930"},
|
||||
}
|
||||
|
||||
eligible, excluded = _filter_members_by_listing(
|
||||
members, reference, "20260723"
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
[item["ts_code"] for item in eligible],
|
||||
["601318.SH", "601319.SH", "999999.SZ"],
|
||||
)
|
||||
self.assertEqual(excluded[0]["ts_code"], "000627.SZ")
|
||||
self.assertEqual(excluded[0]["reason"], "目标日期前已退市")
|
||||
|
||||
def test_member_is_kept_for_dates_before_its_delisting(self):
|
||||
members = [{"ts_code": "000627.SZ", "name": "历史有效成员"}]
|
||||
reference = {
|
||||
"000627.SZ": {"list_date": "19961112", "delist_date": "20250930"}
|
||||
}
|
||||
|
||||
eligible, excluded = _filter_members_by_listing(
|
||||
members, reference, "20250929"
|
||||
)
|
||||
|
||||
self.assertEqual(eligible, members)
|
||||
self.assertEqual(excluded, [])
|
||||
|
||||
def test_sector_coverage_gate_adapts_to_member_count(self):
|
||||
self.assertEqual(_sector_coverage_issue(5, 5, 100), "")
|
||||
self.assertIn("全部可解释", _sector_coverage_issue(5, 4, 80))
|
||||
self.assertEqual(_sector_coverage_issue(5, 4, 100, 5), "")
|
||||
self.assertEqual(_sector_coverage_issue(10, 9, 90), "")
|
||||
self.assertIn("至少90%", _sector_coverage_issue(9, 8, 88.9))
|
||||
self.assertIn("最多缺1只", _sector_coverage_issue(20, 18, 90))
|
||||
self.assertEqual(_sector_coverage_issue(50, 45, 90), "")
|
||||
self.assertIn("低于90%", _sector_coverage_issue(50, 44, 88))
|
||||
|
||||
@patch.object(TushareClient, "query")
|
||||
def test_confirmed_suspension_explains_a_missing_quote(self, query: MagicMock):
|
||||
TushareClient._suspension_cache.clear()
|
||||
query.return_value = [{
|
||||
"ts_code": "601319.SH",
|
||||
"suspend_date": "20260720",
|
||||
"resume_date": "20260725",
|
||||
"suspend_reason": "重大事项",
|
||||
}]
|
||||
members = [
|
||||
{"ts_code": "601318.SH", "name": "中国平安"},
|
||||
{"ts_code": "601319.SH", "name": "中国人保"},
|
||||
]
|
||||
|
||||
suspended = TushareClient("token")._confirmed_suspended_members(
|
||||
members, {"601318.SH"}, "20260723"
|
||||
)
|
||||
|
||||
self.assertEqual(len(suspended), 1)
|
||||
self.assertEqual(suspended[0]["ts_code"], "601319.SH")
|
||||
self.assertEqual(suspended[0]["reason"], "重大事项")
|
||||
|
||||
@patch.object(TushareClient, "query")
|
||||
def test_latest_effective_membership_wins_over_stale_is_new_row(self, query: MagicMock):
|
||||
stale_y = {
|
||||
|
||||
@@ -5,6 +5,8 @@ import unittest
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from pathlib import Path
|
||||
|
||||
from tushare_client import _sector_coverage_issue
|
||||
|
||||
|
||||
def load_method(name: str):
|
||||
source = Path("server.py").read_text(encoding="utf-8")
|
||||
@@ -19,7 +21,11 @@ def load_method(name: str):
|
||||
and node.name == name
|
||||
)
|
||||
module = ast.Module(body=[method], type_ignores=[])
|
||||
namespace = {"datetime": datetime, "Any": object}
|
||||
namespace = {
|
||||
"datetime": datetime,
|
||||
"Any": object,
|
||||
"_sector_coverage_issue": _sector_coverage_issue,
|
||||
}
|
||||
exec(compile(ast.fix_missing_locations(module), "server.py", "exec"), namespace)
|
||||
return namespace[name]
|
||||
|
||||
|
||||
Reference in New Issue
Block a user