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
This commit is contained in:
@@ -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)。
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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, ...]: ...
|
||||
|
||||
|
||||
|
||||
+5
-22
@@ -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"]
|
||||
|
||||
@@ -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",)
|
||||
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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",
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user