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:
yuxuanhui
2026-09-05 19:48:30 +08:00
parent 41a9b4eb9a
commit 7e0f13d678
13 changed files with 1135 additions and 36 deletions
@@ -31,6 +31,52 @@ class SelectionResultQuery:
search: str | None = None
category: SelectionSignalCategoryFilter | None = None
sort: SelectionResultSort = "code"
sector: str | None = None
@dataclass(frozen=True, slots=True)
class SelectionSectorCount:
"""One sector and the number of this run's selected stocks it contains."""
sector_code: str
sector_name: str
stock_count: int
@dataclass(frozen=True, slots=True)
class SelectionSectorMembership:
"""Point-in-time sector membership aggregates for a set of stock codes."""
snapshot_trade_date: date | None
sector_counts: tuple[SelectionSectorCount, ...] = ()
@dataclass(frozen=True, slots=True)
class SelectionRunIdentity:
"""Minimal run locator used to resolve date-dependent filters."""
run_id: str
target_trade_date: date
class SelectionSectorReader(Protocol):
"""Port to the sector-radar context's point-in-time membership reads."""
def sector_counts(
self,
stock_codes: Sequence[str],
target_trade_date: date,
*,
sector_type: str = "concept",
) -> SelectionSectorMembership: ...
def sector_member_codes(
self,
target_trade_date: date,
sector_code: str,
*,
sector_type: str = "concept",
) -> tuple[str, ...]: ...
@dataclass(frozen=True, slots=True)
@@ -139,6 +185,7 @@ class SelectionRunStore(Protocol):
run_id: str,
*,
query: SelectionResultQuery | None = None,
sector_stock_codes: Sequence[str] | None = None,
) -> SelectionRun | None: ...
def get_latest_run(
@@ -147,8 +194,17 @@ class SelectionRunStore(Protocol):
target_trade_date: date | None = None,
*,
query: SelectionResultQuery | None = None,
sector_stock_codes: Sequence[str] | None = None,
) -> SelectionRun | None: ...
def get_run_identity(self, run_id: str) -> SelectionRunIdentity | None: ...
def get_latest_run_identity(
self,
strategy: SelectionStrategyName,
target_trade_date: date | None = None,
) -> SelectionRunIdentity | None: ...
class SelectionUniverseReader(Protocol):
"""Read a qualified market-data source snapshot for one strategy run."""