feat(selection): 集成 B1 FastDTW 图形评分
This commit is contained in:
@@ -11,6 +11,12 @@ from datetime import date
|
||||
from typing import Literal, Protocol, cast
|
||||
|
||||
from ..domain.models import SelectionEvaluation, StockHistory
|
||||
from ..domain.pattern_scoring import (
|
||||
PatternCase,
|
||||
PatternCaseLibraryLoader,
|
||||
PatternScore,
|
||||
PatternScorer,
|
||||
)
|
||||
from ..domain.runs import (
|
||||
BatchSelectionRunStore,
|
||||
BatchSelectionUniverseReader,
|
||||
@@ -54,7 +60,10 @@ class RunZhixingB1:
|
||||
reader: SelectionUniverseReader,
|
||||
store: SelectionRunStore,
|
||||
evaluator: SelectionEvaluator | None = None,
|
||||
pattern_case_loader: PatternCaseLibraryLoader | None = None,
|
||||
pattern_scorer: PatternScorer | None = None,
|
||||
*,
|
||||
pattern_scoring_enabled: bool = False,
|
||||
max_workers: int = 4,
|
||||
batch_size: int = 200,
|
||||
) -> None:
|
||||
@@ -67,6 +76,9 @@ class RunZhixingB1:
|
||||
self.reader = reader
|
||||
self.store = store
|
||||
self.evaluator = evaluator or EvaluateZhixingB1(reader)
|
||||
self.pattern_case_loader = pattern_case_loader
|
||||
self.pattern_scorer = pattern_scorer
|
||||
self.pattern_scoring_enabled = pattern_scoring_enabled
|
||||
self.max_workers = max_workers
|
||||
self.batch_size = batch_size
|
||||
|
||||
@@ -106,7 +118,9 @@ class RunZhixingB1:
|
||||
read_seconds = 0.0
|
||||
evaluate_seconds = 0.0
|
||||
persist_seconds = 0.0
|
||||
scoring_seconds = 0.0
|
||||
try:
|
||||
pattern_cases, pattern_library_error = self._prepare_pattern_cases(prepared.run.id)
|
||||
with ThreadPoolExecutor(max_workers=self.max_workers) as executor:
|
||||
for batch_stocks in _chunks(stocks, self.batch_size):
|
||||
read_started = time.perf_counter()
|
||||
@@ -120,24 +134,39 @@ class RunZhixingB1:
|
||||
)
|
||||
|
||||
evaluate_started = time.perf_counter()
|
||||
evaluations = tuple(
|
||||
executor.map(
|
||||
self._evaluate_stock,
|
||||
batch_stocks,
|
||||
histories,
|
||||
[prepared.source.target_trade_date] * len(batch_stocks),
|
||||
)
|
||||
)
|
||||
evaluate_seconds += time.perf_counter() - evaluate_started
|
||||
|
||||
scoring_started = time.perf_counter()
|
||||
items = tuple(
|
||||
_to_item(
|
||||
stock.ts_code,
|
||||
stock.name,
|
||||
evaluation,
|
||||
)
|
||||
for stock, evaluation in zip(
|
||||
batch_stocks,
|
||||
executor.map(
|
||||
self._evaluate_stock,
|
||||
batch_stocks,
|
||||
histories,
|
||||
[prepared.source.target_trade_date] * len(batch_stocks),
|
||||
pattern_score=self._score_stock(
|
||||
prepared.run.id,
|
||||
stock,
|
||||
history,
|
||||
evaluation,
|
||||
pattern_cases,
|
||||
pattern_library_error,
|
||||
),
|
||||
)
|
||||
for stock, history, evaluation in zip(
|
||||
batch_stocks,
|
||||
histories,
|
||||
evaluations,
|
||||
strict=True,
|
||||
)
|
||||
)
|
||||
evaluate_seconds += time.perf_counter() - evaluate_started
|
||||
scoring_seconds += time.perf_counter() - scoring_started
|
||||
|
||||
evaluated_count += len(items)
|
||||
selected_stock_count += sum(item.status == "selected" for item in items)
|
||||
@@ -184,7 +213,7 @@ class RunZhixingB1:
|
||||
logger.info(
|
||||
"selection_run_summary run_id=%s stock_count=%d history_rows=%d "
|
||||
"batch_count=%d worker_count=%d read_seconds=%.3f "
|
||||
"evaluate_seconds=%.3f persist_seconds=%.3f",
|
||||
"evaluate_seconds=%.3f scoring_seconds=%.3f persist_seconds=%.3f",
|
||||
prepared.run.id,
|
||||
len(stocks),
|
||||
history_rows,
|
||||
@@ -192,9 +221,64 @@ class RunZhixingB1:
|
||||
self.max_workers,
|
||||
read_seconds,
|
||||
evaluate_seconds,
|
||||
scoring_seconds,
|
||||
persist_seconds,
|
||||
)
|
||||
|
||||
def _prepare_pattern_cases(
|
||||
self,
|
||||
run_id: str,
|
||||
) -> tuple[tuple[PatternCase, ...] | None, str | None]:
|
||||
"""Load the complete case library once without failing selection."""
|
||||
|
||||
if not self.pattern_scoring_enabled:
|
||||
return None, None
|
||||
if self.pattern_case_loader is None or self.pattern_scorer is None:
|
||||
reason = "pattern scoring is enabled but not configured"
|
||||
logger.error("selection_pattern_library_failed run_id=%s reason=%s", run_id, reason)
|
||||
return None, reason
|
||||
try:
|
||||
return self.pattern_case_loader.load(), None
|
||||
except Exception as exc: # noqa: BLE001 - scoring enrichment must not fail selection
|
||||
reason = _safe_item_error(exc)
|
||||
logger.warning(
|
||||
"selection_pattern_library_failed run_id=%s error_type=%s reason=%s",
|
||||
run_id,
|
||||
exc.__class__.__name__,
|
||||
reason,
|
||||
)
|
||||
return None, reason
|
||||
|
||||
def _score_stock(
|
||||
self,
|
||||
run_id: str,
|
||||
stock: SelectionStock,
|
||||
history: StockHistory | None,
|
||||
evaluation: SelectionEvaluation,
|
||||
cases: tuple[PatternCase, ...] | None,
|
||||
library_error: str | None,
|
||||
) -> PatternScore:
|
||||
"""Score one selected stock once and isolate enrichment failures."""
|
||||
|
||||
if not self.pattern_scoring_enabled or evaluation.status != "selected":
|
||||
return PatternScore()
|
||||
if library_error is not None:
|
||||
return PatternScore.failed(library_error)
|
||||
if history is None or cases is None or self.pattern_scorer is None:
|
||||
return PatternScore.failed("pattern scoring history or case library is unavailable")
|
||||
try:
|
||||
return self.pattern_scorer.score(history, cases)
|
||||
except Exception as exc: # noqa: BLE001 - one score must not fail the selection run
|
||||
reason = _safe_item_error(exc)
|
||||
logger.warning(
|
||||
"selection_pattern_score_failed run_id=%s ts_code=%s error_type=%s reason=%s",
|
||||
run_id,
|
||||
stock.ts_code,
|
||||
exc.__class__.__name__,
|
||||
reason,
|
||||
)
|
||||
return PatternScore.failed(reason)
|
||||
|
||||
def _load_histories(
|
||||
self,
|
||||
stocks: Sequence[SelectionStock],
|
||||
@@ -287,7 +371,13 @@ class RunZhixingB1:
|
||||
return self.store.get_latest_run(strategy, target_trade_date, query=query)
|
||||
|
||||
|
||||
def _to_item(ts_code: str, name: str, evaluation: SelectionEvaluation) -> SelectionRunItem:
|
||||
def _to_item(
|
||||
ts_code: str,
|
||||
name: str,
|
||||
evaluation: SelectionEvaluation,
|
||||
*,
|
||||
pattern_score: PatternScore | None = None,
|
||||
) -> SelectionRunItem:
|
||||
"""Translate a single-stock domain result into a stored item."""
|
||||
|
||||
return SelectionRunItem(
|
||||
@@ -296,6 +386,7 @@ def _to_item(ts_code: str, name: str, evaluation: SelectionEvaluation) -> Select
|
||||
status=evaluation.status,
|
||||
signal_count=len(evaluation.signals),
|
||||
reason=evaluation.reason,
|
||||
pattern_score=pattern_score or PatternScore(),
|
||||
signals=evaluation.signals,
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user