feat(selection): expose run sector aggregates and sector filter on results API
- sector_radar: add batch sector-count aggregation and sector member lookup
over the strict last-good membership snapshot (postgres + in-memory fakes)
- selection: add SelectionSectorReader port, list_sector_counts use case,
and sector_stock_codes filtering via run identity resolution; queries stay
inside the selection context per ADR 0001
- http: add GET /api/v1/selection/sectors and forward sector param on
/results and /runs/{run_id}
- fix stale positional args in pattern-scoring run tests; cover new behavior
with read-service, application, and HTTP contract tests
This commit is contained in:
@@ -27,6 +27,7 @@ from zhixing_server.modules.sector_radar.domain.models import (
|
||||
from zhixing_server.modules.sector_radar.domain.persistence import (
|
||||
MembershipRecord,
|
||||
RankingRecord,
|
||||
SectorCountEntry,
|
||||
)
|
||||
from zhixing_server.modules.sector_radar.domain.ranking import rank_metric_observations
|
||||
from zhixing_server.modules.sector_radar.infrastructure.memory import (
|
||||
@@ -315,3 +316,89 @@ def test_stock_membership_query_rejects_invalid_values() -> None:
|
||||
StockSectorQuery(trade_date=TARGET_DATE, ts_code="x" * 13)
|
||||
with pytest.raises(ValueError):
|
||||
StockSectorQuery(trade_date=TARGET_DATE, ts_code="000001.SZ", concept_limit=0)
|
||||
|
||||
|
||||
def test_sector_counts_aggregates_concepts_and_orders_by_count_then_code() -> None:
|
||||
repository = _membership_repository()
|
||||
repository.save_memberships(
|
||||
(
|
||||
_membership_record("000002.SZ", SectorType.CONCEPT, "BK0001.DC", "机器人"),
|
||||
_membership_record("000003.SZ", SectorType.CONCEPT, "BK0001.DC", "机器人"),
|
||||
_membership_record("000002.SZ", SectorType.CONCEPT, "BK0003.DC", "数字经济"),
|
||||
)
|
||||
)
|
||||
reader = ReadSectorRadar(repository)
|
||||
|
||||
snapshot = reader.sector_counts(["000001.SZ", "000002.SZ", "000003.SZ"], TARGET_DATE)
|
||||
|
||||
assert snapshot.status == "success"
|
||||
assert snapshot.trade_date == TARGET_DATE
|
||||
assert snapshot.sector_type is SectorType.CONCEPT
|
||||
assert snapshot.counts == (
|
||||
SectorCountEntry(sector_code="BK0001.DC", sector_name="机器人", stock_count=3),
|
||||
SectorCountEntry(sector_code="BK0003.DC", sector_name="数字经济", stock_count=2),
|
||||
SectorCountEntry(sector_code="BK0002.DC", sector_name="人工智能", stock_count=1),
|
||||
)
|
||||
|
||||
|
||||
def test_sector_counts_supports_industry_type() -> None:
|
||||
reader = ReadSectorRadar(_membership_repository())
|
||||
|
||||
snapshot = reader.sector_counts(
|
||||
["000001.SZ", "000002.SZ"],
|
||||
TARGET_DATE,
|
||||
sector_type=SectorType.INDUSTRY,
|
||||
)
|
||||
|
||||
assert snapshot.status == "success"
|
||||
assert snapshot.sector_type is SectorType.INDUSTRY
|
||||
assert snapshot.counts == (
|
||||
SectorCountEntry(sector_code="BK0901.DC", sector_name="银行", stock_count=1),
|
||||
SectorCountEntry(sector_code="BK0902.DC", sector_name="房地产", stock_count=1),
|
||||
)
|
||||
|
||||
|
||||
def test_sector_counts_without_publication_is_no_data() -> None:
|
||||
reader = ReadSectorRadar(InMemorySectorRadarRepository())
|
||||
|
||||
snapshot = reader.sector_counts(["000001.SZ"], TARGET_DATE)
|
||||
|
||||
assert snapshot.status == "no_data"
|
||||
assert snapshot.trade_date is None
|
||||
assert snapshot.counts == ()
|
||||
|
||||
|
||||
def test_sector_counts_with_empty_stock_set_skips_publication_lookup() -> None:
|
||||
reader = ReadSectorRadar(_membership_repository())
|
||||
|
||||
snapshot = reader.sector_counts([], TARGET_DATE)
|
||||
|
||||
assert snapshot.status == "no_data"
|
||||
assert snapshot.counts == ()
|
||||
|
||||
|
||||
def test_sector_member_codes_returns_available_members() -> None:
|
||||
reader = ReadSectorRadar(_membership_repository())
|
||||
|
||||
snapshot = reader.sector_member_codes(TARGET_DATE, "BK0001.DC")
|
||||
|
||||
assert snapshot.status == "success"
|
||||
assert snapshot.trade_date == TARGET_DATE
|
||||
assert snapshot.stock_codes == ("000001.SZ",)
|
||||
|
||||
|
||||
def test_sector_member_codes_without_publication_is_no_data() -> None:
|
||||
reader = ReadSectorRadar(InMemorySectorRadarRepository())
|
||||
|
||||
snapshot = reader.sector_member_codes(TARGET_DATE, "BK0001.DC")
|
||||
|
||||
assert snapshot.status == "no_data"
|
||||
assert snapshot.trade_date is None
|
||||
assert snapshot.stock_codes == ()
|
||||
|
||||
|
||||
def test_sector_member_codes_rejects_blank_sector_code() -> None:
|
||||
reader = ReadSectorRadar(_membership_repository())
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
reader.sector_member_codes(TARGET_DATE, " ")
|
||||
|
||||
Reference in New Issue
Block a user