"""Domain contracts for persisted historical selection runs.""" from __future__ import annotations from collections.abc import Sequence from dataclasses import dataclass, field from datetime import date, datetime from decimal import Decimal from typing import Literal, Protocol from .models import ( SelectionEvaluationStatus, SelectionSignal, SelectionStrategyName, StockHistory, ) from .pattern_scoring import PatternScore SelectionRunStatus = Literal["running", "success", "partial_success", "failed"] SelectionRunItemStatus = SelectionEvaluationStatus SelectionSignalCategoryFilter = Literal["pullback", "oversold", "original", "resonance"] SelectionResultSort = Literal["code", "score_desc", "score_asc"] @dataclass(frozen=True, slots=True) class SelectionResultQuery: """Validated query options for a paged selection-result read.""" page: int = 1 page_size: int = 10 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) class SelectionStock: """One eligible current stock that will be evaluated for a run.""" ts_code: str name: str @dataclass(frozen=True, slots=True) class SelectionExecutionSource: """Market-data batch and eligible stock snapshot used by one run.""" market_sync_batch_id: str target_trade_date: date target_count: int valid_count: int coverage: Decimal stocks: tuple[SelectionStock, ...] = field(default_factory=tuple) @dataclass(frozen=True, slots=True) class SelectionRunItem: """Persistable per-stock evaluation state and its independent signals.""" ts_code: str name: str status: SelectionRunItemStatus signal_count: int = 0 reason: str | None = None pattern_score: PatternScore = field(default_factory=PatternScore) signals: tuple[SelectionSignal, ...] = field(default_factory=tuple) @dataclass(frozen=True, slots=True) class SelectionRun: """A current execution attempt and its materialized result rows.""" id: str strategy: SelectionStrategyName target_trade_date: date market_sync_batch_id: str | None status: SelectionRunStatus target_count: int eligible_count: int evaluated_count: int selected_stock_count: int signal_count: int failed_count: int coverage: Decimal error_type: str | None = None error_message: str | None = None created_at: datetime | None = None finished_at: datetime | None = None items: tuple[SelectionRunItem, ...] = field(default_factory=tuple) signals: tuple[SelectionSignal, ...] = field(default_factory=tuple) stocks_total: int | None = None class SelectionRunError(RuntimeError): """Base class for expected selection-run persistence failures.""" class SelectionRunInProgress(SelectionRunError): """The requested strategy and date already have a running attempt.""" class SelectionRerunRequired(SelectionRunError): """A terminal result exists and an explicit rerun confirmation is missing.""" class SelectionRunStoreError(SelectionRunError): """The selection-run repository could not complete a database operation.""" class SelectionRunStore(Protocol): """Persistence port for current selection runs and their materialized rows.""" def prepare_run( self, strategy: SelectionStrategyName, target_trade_date: date, source: SelectionExecutionSource, *, rerun: bool, ) -> SelectionRun: ... def record_item(self, run_id: str, item: SelectionRunItem) -> None: ... def finish_run( self, run_id: str, status: SelectionRunStatus, *, evaluated_count: int, selected_stock_count: int, signal_count: int, failed_count: int, error_type: str | None = None, error_message: str | None = None, ) -> None: ... def get_run( self, run_id: str, *, query: SelectionResultQuery | None = None, sector_stock_codes: Sequence[str] | None = None, ) -> SelectionRun | None: ... def get_latest_run( self, strategy: SelectionStrategyName, 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.""" def load_execution_source( self, strategy: str, target_trade_date: date, ) -> SelectionExecutionSource: ... def load_history(self, ts_code: str, target_trade_date: date) -> StockHistory: ... class BatchSelectionRunStore(Protocol): """Optional batch-write extension for selection stores.""" def record_items(self, run_id: str, items: Sequence[SelectionRunItem]) -> None: ... class BatchSelectionUniverseReader(Protocol): """Optional bounded batch-history extension for selection readers.""" def load_histories( self, stocks: Sequence[SelectionStock], target_trade_date: date, ) -> tuple[StockHistory, ...]: ...