Files
zhixing-system/zhixing-server/src/zhixing_server/modules/selection/domain/runs.py
T

235 lines
6.5 KiB
Python
Raw Normal View History

"""Domain contracts for persisted historical selection runs."""
from __future__ import annotations
2026-08-12 09:45:16 +08:00
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: ...
2026-08-12 09:45:16 +08:00
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, ...]: ...