From 07c5b25043da7f8ad00b85dd07ba13b63db8ef24 Mon Sep 17 00:00:00 2001 From: yuxuanhui Date: Sun, 6 Sep 2026 16:15:08 +0800 Subject: [PATCH] refactor(selection): drop residual concept-sector surface from sub-industry path - remove the sector_type parameter from the selection port, adapter, and use case; the adapter now passes SectorType.INDUSTRY directly, deleting the dead concept vocabulary mapping - stop parsing concepts/concept_total/concept_limit from the sector-radar membership response in the web API layer and narrow the response type to industries only - update tests and the task PRD accordingly; backend pyright errors drop from the 16 baseline to 14 --- .../prd.md | 3 ++- .../modules/selection/application/run.py | 12 ++------- .../modules/selection/domain/runs.py | 4 --- .../infrastructure/sector_membership.py | 27 ++++--------------- .../unit/selection/test_sector_filter.py | 18 +++++-------- .../sector-radar/api/sector-radar.api.test.ts | 8 ------ .../sector-radar/api/sector-radar.api.ts | 15 +---------- .../sector-radar/api/sector-radar.query.ts | 2 +- .../sector-radar/api/sector-radar.types.ts | 3 --- .../components/signal-detail-panel.test.tsx | 9 ------- 10 files changed, 18 insertions(+), 83 deletions(-) diff --git a/.trellis/tasks/09-05-selection-layout-sector-filter/prd.md b/.trellis/tasks/09-05-selection-layout-sector-filter/prd.md index 8d5001f..8511037 100644 --- a/.trellis/tasks/09-05-selection-layout-sector-filter/prd.md +++ b/.trellis/tasks/09-05-selection-layout-sector-filter/prd.md @@ -8,7 +8,8 @@ ## 关键决策 -- 板块口径:细分行业(sector_type=industry,即东财行业快照),selection 上下文固定为 industry——HTTP `/sectors` 不再接受 sector_type 参数,端口/适配器默认值均为 industry;「细分行业」仍是 sector 词汇的一种,内部参数名保持 `sector` 不变。 +- 板块口径:细分行业(sector_type=industry,即东财行业快照),selection 上下文固定为 industry——HTTP `/sectors` 不再接受 sector_type 参数,端口/适配器方法签名不再携带 sector_type,适配器固定传 `SectorType.INDUSTRY`;「细分行业」仍是 sector 词汇的一种,内部参数名保持 `sector` 不变。 +- 详情面板 membership 消费面收窄到 industries:前端 `sector-radar.api.ts` / 类型不再解析 concepts 字段(后端 `/sector-radar/stocks/{ts_code}/membership` 契约仍返回 concepts,供板块雷达等其余消费方使用)。 - 单选下拉;排序按 stock_count 倒序(后端保证),名称升序 tie-break。 - 行业数据不在 selection 表中,按 ADR 0001 通过端口委托 sector_radar 读服务(不跨上下文 join SQL)。 diff --git a/zhixing-server/src/zhixing_server/modules/selection/application/run.py b/zhixing-server/src/zhixing_server/modules/selection/application/run.py index 457c521..057dd32 100644 --- a/zhixing-server/src/zhixing_server/modules/selection/application/run.py +++ b/zhixing-server/src/zhixing_server/modules/selection/application/run.py @@ -646,11 +646,7 @@ class RunZhixingB1: if self.sector_reader is None: return None - return self.sector_reader.sector_member_codes( - target_trade_date, - sector_code, - sector_type="industry", - ) + return self.sector_reader.sector_member_codes(target_trade_date, sector_code) def list_sector_counts( self, @@ -684,11 +680,7 @@ class RunZhixingB1: 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="industry", - ) + return self.sector_reader.sector_counts(stock_codes, target_trade_date) def _to_item( diff --git a/zhixing-server/src/zhixing_server/modules/selection/domain/runs.py b/zhixing-server/src/zhixing_server/modules/selection/domain/runs.py index 2219a0d..7ffaca1 100644 --- a/zhixing-server/src/zhixing_server/modules/selection/domain/runs.py +++ b/zhixing-server/src/zhixing_server/modules/selection/domain/runs.py @@ -66,16 +66,12 @@ class SelectionSectorReader(Protocol): self, stock_codes: Sequence[str], target_trade_date: date, - *, - sector_type: str = "industry", ) -> SelectionSectorMembership: ... def sector_member_codes( self, target_trade_date: date, sector_code: str, - *, - sector_type: str = "industry", ) -> tuple[str, ...]: ... diff --git a/zhixing-server/src/zhixing_server/modules/selection/infrastructure/sector_membership.py b/zhixing-server/src/zhixing_server/modules/selection/infrastructure/sector_membership.py index 83772d0..b33b769 100644 --- a/zhixing-server/src/zhixing_server/modules/selection/infrastructure/sector_membership.py +++ b/zhixing-server/src/zhixing_server/modules/selection/infrastructure/sector_membership.py @@ -2,7 +2,8 @@ The selection context owns no sector-membership storage. This adapter keeps the port contract local to selection while delegating point-in-time reads to -the sector-radar application service in the composition root. +the sector-radar application service in the composition root. Selection only +speaks the sub-industry vocabulary, so the radar sector type is fixed here. """ from __future__ import annotations @@ -17,20 +18,6 @@ from zhixing_server.modules.selection.domain.runs import ( SelectionSectorMembership, ) -SELECTION_SECTOR_TYPES: dict[str, SectorType] = { - "concept": SectorType.CONCEPT, - "industry": SectorType.INDUSTRY, -} - - -def _sector_type(value: str) -> SectorType: - """Map the public sector-type vocabulary onto the radar domain enum.""" - - try: - return SELECTION_SECTOR_TYPES[value] - except KeyError: - raise ValueError(f"unsupported sector type: {value}") from None - class SectorRadarSelectionReader: """Resolve selection sector aggregates through the sector-radar context.""" @@ -42,15 +29,13 @@ class SectorRadarSelectionReader: self, stock_codes: Sequence[str], target_trade_date: date, - *, - sector_type: str = "industry", ) -> SelectionSectorMembership: """Return per-sector stock counts for one run's selected stocks.""" snapshot = self._reader.sector_counts( stock_codes, target_trade_date, - sector_type=_sector_type(sector_type), + sector_type=SectorType.INDUSTRY, ) return SelectionSectorMembership( snapshot_trade_date=snapshot.trade_date, @@ -68,17 +53,15 @@ class SectorRadarSelectionReader: self, target_trade_date: date, sector_code: str, - *, - sector_type: str = "industry", ) -> tuple[str, ...]: """Return one sector's member stock codes on the aligned snapshot.""" snapshot = self._reader.sector_member_codes( target_trade_date, sector_code, - sector_type=_sector_type(sector_type), + sector_type=SectorType.INDUSTRY, ) return snapshot.stock_codes -__all__ = ["SELECTION_SECTOR_TYPES", "SectorRadarSelectionReader"] +__all__ = ["SectorRadarSelectionReader"] diff --git a/zhixing-server/tests/unit/selection/test_sector_filter.py b/zhixing-server/tests/unit/selection/test_sector_filter.py index 3a8fd03..f0c8b83 100644 --- a/zhixing-server/tests/unit/selection/test_sector_filter.py +++ b/zhixing-server/tests/unit/selection/test_sector_filter.py @@ -41,17 +41,15 @@ class FakeSectorReader: ) -> None: self.counts = counts self.member_codes = member_codes - self.count_calls: list[tuple[tuple[str, ...], date, str]] = [] - self.member_calls: list[tuple[date, str, str]] = [] + self.count_calls: list[tuple[tuple[str, ...], date]] = [] + self.member_calls: list[tuple[date, str]] = [] def sector_counts( self, stock_codes: Sequence[str], target_trade_date: date, - *, - sector_type: str = "industry", ) -> SelectionSectorMembership: - self.count_calls.append((tuple(stock_codes), target_trade_date, sector_type)) + self.count_calls.append((tuple(stock_codes), target_trade_date)) return SelectionSectorMembership( snapshot_trade_date=target_trade_date, sector_counts=self.counts, @@ -61,10 +59,8 @@ class FakeSectorReader: self, target_trade_date: date, sector_code: str, - *, - sector_type: str = "industry", ) -> tuple[str, ...]: - self.member_calls.append((target_trade_date, sector_code, sector_type)) + self.member_calls.append((target_trade_date, sector_code)) return self.member_codes @@ -161,7 +157,7 @@ def test_list_sector_counts_aggregates_only_selected_stocks() -> None: aggregates = service.list_sector_counts("zhixing_b1") assert isinstance(aggregates, SelectionSectorAggregates) - assert sector_reader.count_calls == [(("000001.SZ", "000002.SZ"), TARGET, "industry")] + assert sector_reader.count_calls == [(("000001.SZ", "000002.SZ"), TARGET)] assert aggregates.snapshot_trade_date == TARGET assert aggregates.sector_type == "industry" assert aggregates.sectors == sector_reader.counts @@ -198,7 +194,7 @@ def test_get_latest_resolves_sector_filter_against_run_snapshot() -> None: ) assert run is store.run - assert sector_reader.member_calls == [(TARGET, "BK0001.DC", "industry")] + assert sector_reader.member_calls == [(TARGET, "BK0001.DC")] assert store.sector_codes_seen == ("000001.SZ", "000002.SZ") assert store.queries[-1].sector is None assert store.queries[-1].page == 2 @@ -239,7 +235,7 @@ def test_get_run_resolves_sector_filter_against_run_snapshot() -> None: run = service.get_run("run-1", query=SelectionResultQuery(sector="BK0001.DC")) assert run is store.run - assert sector_reader.member_calls == [(TARGET, "BK0001.DC", "industry")] + assert sector_reader.member_calls == [(TARGET, "BK0001.DC")] assert store.sector_codes_seen == ("000001.SZ",) diff --git a/zhixing-web/src/features/sector-radar/api/sector-radar.api.test.ts b/zhixing-web/src/features/sector-radar/api/sector-radar.api.test.ts index 3f817eb..b451c9f 100644 --- a/zhixing-web/src/features/sector-radar/api/sector-radar.api.test.ts +++ b/zhixing-web/src/features/sector-radar/api/sector-radar.api.test.ts @@ -290,12 +290,6 @@ describe("stock sector membership adapter", () => { requested_trade_date: "2026-08-28", trade_date: "2026-08-28", industries: [{ code: "BK0901.DC", name: "银行" }], - concepts: [ - { code: "BK0001.DC", name: "机器人" }, - { code: "BK0002.DC", name: "人工智能" }, - ], - concept_total: 2, - concept_limit: 20, } it("requests the snake-case endpoint and validates the payload", async () => { @@ -316,8 +310,6 @@ describe("stock sector membership adapter", () => { status: "no_data", trade_date: null, industries: [], - concepts: [], - concept_total: 0, }) const result = await getStockSectorMembership("000001.SZ", "2026-08-28") diff --git a/zhixing-web/src/features/sector-radar/api/sector-radar.api.ts b/zhixing-web/src/features/sector-radar/api/sector-radar.api.ts index cde85a8..269ff41 100644 --- a/zhixing-web/src/features/sector-radar/api/sector-radar.api.ts +++ b/zhixing-web/src/features/sector-radar/api/sector-radar.api.ts @@ -153,7 +153,7 @@ export function parseRadarRankingsResponse( } /** - * Fetch one stock's industries and concepts from the last-good build snapshot. + * Fetch one stock's sub-industries from the last-good build snapshot. * * @param tsCode - Stock code such as `000001.SZ`. * @param tradeDate - Requested trade date; the backend answers with the @@ -193,19 +193,6 @@ export function parseStockSectorMembershipResponse( ), trade_date: readNullableDate(record.trade_date, "membership.trade_date"), industries: readSectorRefs(record.industries, "membership.industries"), - concepts: readSectorRefs(record.concepts, "membership.concepts"), - concept_total: readIntegerInRange( - record.concept_total, - 0, - Number.MAX_SAFE_INTEGER, - "membership.concept_total", - ), - concept_limit: readIntegerInRange( - record.concept_limit, - 1, - 100, - "membership.concept_limit", - ), } } diff --git a/zhixing-web/src/features/sector-radar/api/sector-radar.query.ts b/zhixing-web/src/features/sector-radar/api/sector-radar.query.ts index 149abce..5af3644 100644 --- a/zhixing-web/src/features/sector-radar/api/sector-radar.query.ts +++ b/zhixing-web/src/features/sector-radar/api/sector-radar.query.ts @@ -31,7 +31,7 @@ export const stockSectorMembershipQueryKey = ( ] as const /** - * Read one stock's point-in-time industries and concepts for a detail view. + * Read one stock's point-in-time sub-industries for a detail view. * * Disabled until both the stock code and target trade date are known; the * query stays cached per code-date pair so switching selections refetches diff --git a/zhixing-web/src/features/sector-radar/api/sector-radar.types.ts b/zhixing-web/src/features/sector-radar/api/sector-radar.types.ts index 7fb05cc..e78d5bc 100644 --- a/zhixing-web/src/features/sector-radar/api/sector-radar.types.ts +++ b/zhixing-web/src/features/sector-radar/api/sector-radar.types.ts @@ -128,9 +128,6 @@ export interface StockSectorMembershipResponse { requested_trade_date: string trade_date: string | null industries: StockSectorRef[] - concepts: StockSectorRef[] - concept_total: number - concept_limit: number } export interface RadarSectorQuery { diff --git a/zhixing-web/src/features/selection/components/signal-detail-panel.test.tsx b/zhixing-web/src/features/selection/components/signal-detail-panel.test.tsx index 74dbf25..57945e5 100644 --- a/zhixing-web/src/features/selection/components/signal-detail-panel.test.tsx +++ b/zhixing-web/src/features/selection/components/signal-detail-panel.test.tsx @@ -56,12 +56,6 @@ describe("SignalDetailPanel", () => { requested_trade_date: "2026-08-28", trade_date: "2026-08-28", industries: [{ code: "BK0901.DC", name: "银行" }], - concepts: [ - { code: "BK0001.DC", name: "机器人" }, - { code: "BK0002.DC", name: "人工智能" }, - ], - concept_total: 2, - concept_limit: 20, }, isError: false, isPending: false, @@ -82,9 +76,6 @@ describe("SignalDetailPanel", () => { requested_trade_date: "2026-08-28", trade_date: null, industries: [], - concepts: [], - concept_total: 0, - concept_limit: 20, }, isError: false, isPending: false,