feat(selection): show stock industry and concept boards in detail panel
Add point-in-time stock membership lookup to the sector radar module
(GET /sector-radar/stocks/{ts_code}/membership) reading the existing
dc_index/dc_member snapshots, and surface industries plus concept chips
in the selection signal detail panel with graceful no-data hiding.
This commit is contained in:
@@ -2,10 +2,14 @@ from dataclasses import replace
|
||||
from datetime import UTC, date, datetime, timedelta
|
||||
from decimal import Decimal
|
||||
|
||||
import pytest
|
||||
|
||||
from zhixing_server.modules.sector_radar.application.read import (
|
||||
RadarQuery,
|
||||
RadarView,
|
||||
ReadSectorRadar,
|
||||
SectorRef,
|
||||
StockSectorQuery,
|
||||
)
|
||||
from zhixing_server.modules.sector_radar.domain.metrics import AmountNetStrategy
|
||||
from zhixing_server.modules.sector_radar.domain.models import (
|
||||
@@ -20,7 +24,10 @@ from zhixing_server.modules.sector_radar.domain.models import (
|
||||
RankSide,
|
||||
SectorType,
|
||||
)
|
||||
from zhixing_server.modules.sector_radar.domain.persistence import RankingRecord
|
||||
from zhixing_server.modules.sector_radar.domain.persistence import (
|
||||
MembershipRecord,
|
||||
RankingRecord,
|
||||
)
|
||||
from zhixing_server.modules.sector_radar.domain.ranking import rank_metric_observations
|
||||
from zhixing_server.modules.sector_radar.infrastructure.memory import (
|
||||
InMemorySectorRadarRepository,
|
||||
@@ -187,3 +194,124 @@ def test_latest_partial_attempt_is_visible_but_does_not_replace_last_good() -> N
|
||||
assert index.last_good is not None
|
||||
assert index.last_good.publication_id == "publication-success"
|
||||
assert index.available_dates == (TARGET_DATE,)
|
||||
|
||||
|
||||
def _membership_record(
|
||||
stock_code: str,
|
||||
sector_type: SectorType,
|
||||
sector_code: str,
|
||||
sector_name: str,
|
||||
*,
|
||||
trade_date: date = TARGET_DATE,
|
||||
) -> MembershipRecord:
|
||||
return MembershipRecord(
|
||||
source_snapshot_id="b" * 64,
|
||||
trade_date=trade_date,
|
||||
sector_type=sector_type,
|
||||
sector_code=sector_code,
|
||||
sector_name=sector_name,
|
||||
stock_code=stock_code,
|
||||
stock_name="测试股票",
|
||||
)
|
||||
|
||||
|
||||
def _membership_repository() -> InMemorySectorRadarRepository:
|
||||
repository = _published_repository()
|
||||
repository.save_memberships(
|
||||
(
|
||||
_membership_record("000001.SZ", SectorType.INDUSTRY, "BK0902.DC", "房地产"),
|
||||
_membership_record("000001.SZ", SectorType.CONCEPT, "BK0003.DC", "数字经济"),
|
||||
_membership_record("000001.SZ", SectorType.CONCEPT, "BK0001.DC", "机器人"),
|
||||
_membership_record("000001.SZ", SectorType.CONCEPT, "BK0002.DC", "人工智能"),
|
||||
_membership_record("000002.SZ", SectorType.INDUSTRY, "BK0901.DC", "银行"),
|
||||
)
|
||||
)
|
||||
return repository
|
||||
|
||||
|
||||
def test_stock_membership_groups_types_and_orders_by_sector_code() -> None:
|
||||
reader = ReadSectorRadar(_membership_repository())
|
||||
|
||||
membership = reader.stock_membership(
|
||||
StockSectorQuery(trade_date=TARGET_DATE, ts_code="000001.SZ")
|
||||
)
|
||||
|
||||
assert membership.status == "success"
|
||||
assert membership.trade_date == TARGET_DATE
|
||||
assert membership.industries == (SectorRef(code="BK0902.DC", name="房地产"),)
|
||||
assert [ref.code for ref in membership.concepts] == [
|
||||
"BK0001.DC",
|
||||
"BK0002.DC",
|
||||
"BK0003.DC",
|
||||
]
|
||||
assert membership.concept_total == 3
|
||||
other = reader.stock_membership(StockSectorQuery(trade_date=TARGET_DATE, ts_code="000002.SZ"))
|
||||
assert other.industries == (SectorRef(code="BK0901.DC", name="银行"),)
|
||||
assert other.concepts == ()
|
||||
assert other.concept_total == 0
|
||||
|
||||
|
||||
def test_stock_membership_truncates_concepts_with_total_preserved() -> None:
|
||||
reader = ReadSectorRadar(_membership_repository())
|
||||
|
||||
membership = reader.stock_membership(
|
||||
StockSectorQuery(trade_date=TARGET_DATE, ts_code="000001.SZ", concept_limit=2)
|
||||
)
|
||||
|
||||
assert [ref.code for ref in membership.concepts] == ["BK0001.DC", "BK0002.DC"]
|
||||
assert membership.concept_total == 3
|
||||
|
||||
|
||||
def test_stock_membership_uses_newest_build_at_or_before_requested_date() -> None:
|
||||
repository = _membership_repository()
|
||||
earlier = _running("publication-earlier", TARGET_DATE - timedelta(days=1))
|
||||
repository.create_publication(earlier)
|
||||
repository.finish_publication(_finish(earlier, PublicationStatus.SUCCESS))
|
||||
repository.save_memberships(
|
||||
(
|
||||
_membership_record(
|
||||
"000001.SZ",
|
||||
SectorType.INDUSTRY,
|
||||
"BK0899.DC",
|
||||
"旧行业",
|
||||
trade_date=TARGET_DATE - timedelta(days=1),
|
||||
),
|
||||
)
|
||||
)
|
||||
reader = ReadSectorRadar(repository)
|
||||
|
||||
membership = reader.stock_membership(
|
||||
StockSectorQuery(trade_date=TARGET_DATE, ts_code="000001.SZ")
|
||||
)
|
||||
|
||||
assert membership.trade_date == TARGET_DATE
|
||||
assert membership.industries == (SectorRef(code="BK0902.DC", name="房地产"),)
|
||||
behind = reader.stock_membership(
|
||||
StockSectorQuery(trade_date=TARGET_DATE - timedelta(days=1), ts_code="000001.SZ")
|
||||
)
|
||||
assert behind.trade_date == TARGET_DATE - timedelta(days=1)
|
||||
assert behind.industries == (SectorRef(code="BK0899.DC", name="旧行业"),)
|
||||
assert behind.concepts == ()
|
||||
|
||||
|
||||
def test_stock_membership_without_any_publication_is_no_data() -> None:
|
||||
reader = ReadSectorRadar(InMemorySectorRadarRepository())
|
||||
|
||||
membership = reader.stock_membership(
|
||||
StockSectorQuery(trade_date=TARGET_DATE, ts_code="000001.SZ")
|
||||
)
|
||||
|
||||
assert membership.status == "no_data"
|
||||
assert membership.trade_date is None
|
||||
assert membership.industries == ()
|
||||
assert membership.concepts == ()
|
||||
assert membership.concept_total == 0
|
||||
|
||||
|
||||
def test_stock_membership_query_rejects_invalid_values() -> None:
|
||||
with pytest.raises(ValueError):
|
||||
StockSectorQuery(trade_date=TARGET_DATE, ts_code=" ")
|
||||
with pytest.raises(ValueError):
|
||||
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)
|
||||
|
||||
Reference in New Issue
Block a user