2026-08-09 09:34:46 +08:00
|
|
|
"""Domain contracts for persisted historical selection runs."""
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
2026-08-12 09:45:16 +08:00
|
|
|
from collections.abc import Sequence
|
2026-08-09 09:34:46 +08:00
|
|
|
from dataclasses import dataclass, field
|
|
|
|
|
from datetime import date, datetime
|
|
|
|
|
from decimal import Decimal
|
|
|
|
|
from typing import Literal, Protocol
|
|
|
|
|
|
2026-09-05 10:22:08 +08:00
|
|
|
from .models import (
|
|
|
|
|
SelectionEvaluationStatus,
|
|
|
|
|
SelectionSignal,
|
|
|
|
|
SelectionStrategyName,
|
|
|
|
|
StockHistory,
|
|
|
|
|
)
|
2026-08-31 16:14:16 +08:00
|
|
|
from .pattern_scoring import PatternScore
|
2026-08-09 09:34:46 +08:00
|
|
|
|
|
|
|
|
SelectionRunStatus = Literal["running", "success", "partial_success", "failed"]
|
|
|
|
|
SelectionRunItemStatus = SelectionEvaluationStatus
|
2026-09-05 10:22:08 +08:00
|
|
|
SelectionSignalCategoryFilter = Literal["pullback", "oversold", "original", "resonance"]
|
2026-08-31 16:14:16 +08:00
|
|
|
SelectionResultSort = Literal["code", "score_desc", "score_asc"]
|
2026-08-10 11:09:23 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@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
|
2026-08-31 16:14:16 +08:00
|
|
|
sort: SelectionResultSort = "code"
|
2026-08-09 09:34:46 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@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
|
2026-08-31 16:14:16 +08:00
|
|
|
pattern_score: PatternScore = field(default_factory=PatternScore)
|
2026-08-09 09:34:46 +08:00
|
|
|
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
|
2026-09-05 10:22:08 +08:00
|
|
|
strategy: SelectionStrategyName
|
2026-08-09 09:34:46 +08:00
|
|
|
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)
|
2026-08-28 11:39:05 +08:00
|
|
|
stocks_total: int | None = None
|
2026-08-09 09:34:46 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
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,
|
2026-09-05 10:22:08 +08:00
|
|
|
strategy: SelectionStrategyName,
|
2026-08-09 09:34:46 +08:00
|
|
|
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: ...
|
|
|
|
|
|
2026-08-10 11:09:23 +08:00
|
|
|
def get_run(
|
|
|
|
|
self,
|
|
|
|
|
run_id: str,
|
|
|
|
|
*,
|
|
|
|
|
query: SelectionResultQuery | None = None,
|
|
|
|
|
) -> SelectionRun | None: ...
|
2026-08-09 09:34:46 +08:00
|
|
|
|
|
|
|
|
def get_latest_run(
|
|
|
|
|
self,
|
2026-09-05 10:22:08 +08:00
|
|
|
strategy: SelectionStrategyName,
|
2026-08-09 09:34:46 +08:00
|
|
|
target_trade_date: date | None = None,
|
2026-08-10 11:09:23 +08:00
|
|
|
*,
|
|
|
|
|
query: SelectionResultQuery | None = None,
|
2026-08-09 09:34:46 +08:00
|
|
|
) -> SelectionRun | 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, ...]: ...
|