refactor(selection): replace concept sector filter with sub-industry

- pin the selection sector vocabulary to industry: the /sectors endpoint
  no longer accepts sector_type, and the port, adapter, and use case all
  resolve counts and member codes with sector_type="industry"
- relabel the results filter to 细分行业 and drop concept-type plumbing
  from the frontend API, query keys, and types
- show only 细分行业 in the signal detail panel; remove the concept-board
  chips and keep the snapshot-date tooltip on the industry line
- update backend and frontend tests to the industry vocabulary and record
  the revised scope in the task PRD
This commit is contained in:
yuxuanhui
2026-09-05 21:04:00 +08:00
parent ade1713e86
commit 68282f5d46
14 changed files with 82 additions and 179 deletions
@@ -642,20 +642,22 @@ class RunZhixingB1:
target_trade_date: date,
sector_code: str,
) -> tuple[str, ...] | None:
"""Resolve one sector's members, or None when the port is absent."""
"""Resolve one sub-industry's members, or None when the port is absent."""
if self.sector_reader is None:
return None
return self.sector_reader.sector_member_codes(target_trade_date, sector_code)
return self.sector_reader.sector_member_codes(
target_trade_date,
sector_code,
sector_type="industry",
)
def list_sector_counts(
self,
strategy: StrategyName,
target_trade_date: date | None = None,
*,
sector_type: str = "concept",
) -> SelectionSectorAggregates | None:
"""Aggregate the current run's selected stocks by point-in-time sector."""
"""Aggregate the current run's selected stocks by sub-industry."""
run = self.store.get_latest_run(strategy, target_trade_date)
if run is None:
@@ -665,11 +667,11 @@ class RunZhixingB1:
for item in run.items
if item.status == "selected" and item.signal_count > 0
]
membership = self._sector_membership(selected_codes, run.target_trade_date, sector_type)
membership = self._sector_membership(selected_codes, run.target_trade_date)
return SelectionSectorAggregates(
run=run,
snapshot_trade_date=membership.snapshot_trade_date,
sector_type=sector_type,
sector_type="industry",
sectors=membership.sector_counts,
)
@@ -677,16 +679,15 @@ class RunZhixingB1:
self,
stock_codes: Sequence[str],
target_trade_date: date,
sector_type: str,
) -> SelectionSectorMembership:
"""Read sector counts for a stock set, tolerating a missing port."""
"""Read sub-industry counts for a stock set, tolerating a missing port."""
if self.sector_reader is None or not stock_codes:
return SelectionSectorMembership(snapshot_trade_date=None, sector_counts=())
return self.sector_reader.sector_counts(
stock_codes,
target_trade_date,
sector_type=sector_type,
sector_type="industry",
)
@@ -67,7 +67,7 @@ class SelectionSectorReader(Protocol):
stock_codes: Sequence[str],
target_trade_date: date,
*,
sector_type: str = "concept",
sector_type: str = "industry",
) -> SelectionSectorMembership: ...
def sector_member_codes(
@@ -75,7 +75,7 @@ class SelectionSectorReader(Protocol):
target_trade_date: date,
sector_code: str,
*,
sector_type: str = "concept",
sector_type: str = "industry",
) -> tuple[str, ...]: ...
@@ -43,7 +43,7 @@ class SectorRadarSelectionReader:
stock_codes: Sequence[str],
target_trade_date: date,
*,
sector_type: str = "concept",
sector_type: str = "industry",
) -> SelectionSectorMembership:
"""Return per-sector stock counts for one run's selected stocks."""
@@ -69,7 +69,7 @@ class SectorRadarSelectionReader:
target_trade_date: date,
sector_code: str,
*,
sector_type: str = "concept",
sector_type: str = "industry",
) -> tuple[str, ...]:
"""Return one sector's member stock codes on the aligned snapshot."""
@@ -231,14 +231,14 @@ def _empty_sectors() -> list[SelectionSectorCountResponse]:
class SelectionSectorsResponse(BaseModel):
"""The current run's selected stocks aggregated by point-in-time sector."""
"""The current run's selected stocks aggregated by point-in-time sub-industry."""
strategy: StrategyValue
target_trade_date: date | None
run_id: str | None
status: SelectionStatusValue
snapshot_trade_date: date | None
sector_type: Literal["concept", "industry"]
sector_type: Literal["industry"]
sectors: list[SelectionSectorCountResponse] = Field(default_factory=_empty_sectors)
@@ -391,16 +391,11 @@ def get_selection_sectors(
service: Annotated[RunZhixingB1, Depends(get_selection_service)],
strategy: StrategyValue = "zhixing_b1",
target_trade_date: date | None = None,
sector_type: Literal["concept", "industry"] = "concept",
) -> SelectionSectorsResponse:
"""Aggregate the current run's selected stocks by point-in-time sector."""
"""Aggregate the current run's selected stocks by point-in-time sub-industry."""
try:
aggregates = service.list_sector_counts(
strategy,
target_trade_date,
sector_type=sector_type,
)
aggregates = service.list_sector_counts(strategy, target_trade_date)
except SelectionRunStoreError as exc:
raise _http_error(503, "selection_storage_unavailable", str(exc)) from exc
if aggregates is None or aggregates.run is None:
@@ -410,7 +405,7 @@ def get_selection_sectors(
run_id=None,
status="no_data",
snapshot_trade_date=None,
sector_type=sector_type,
sector_type="industry",
)
run = aggregates.run
return SelectionSectorsResponse(
@@ -419,7 +414,7 @@ def get_selection_sectors(
run_id=run.id,
status=run.status,
snapshot_trade_date=aggregates.snapshot_trade_date,
sector_type=sector_type,
sector_type="industry",
sectors=[
SelectionSectorCountResponse(
sector_code=count.sector_code,