2026-08-09 09:34:46 +08:00
|
|
|
"""Application tests for persisted whole-universe selection runs."""
|
|
|
|
|
|
2026-08-12 09:45:16 +08:00
|
|
|
import threading
|
|
|
|
|
import time
|
2026-08-31 16:14:16 +08:00
|
|
|
from collections.abc import Sequence
|
2026-08-09 09:34:46 +08:00
|
|
|
from datetime import date
|
|
|
|
|
from decimal import Decimal
|
|
|
|
|
from typing import Literal
|
|
|
|
|
|
|
|
|
|
from zhixing_server.modules.selection.application.run import (
|
|
|
|
|
PreparedSelectionRun,
|
|
|
|
|
RunZhixingB1,
|
|
|
|
|
)
|
|
|
|
|
from zhixing_server.modules.selection.domain.models import (
|
|
|
|
|
SelectionEvaluation,
|
|
|
|
|
SelectionSignal,
|
|
|
|
|
StockHistory,
|
|
|
|
|
)
|
2026-08-31 16:14:16 +08:00
|
|
|
from zhixing_server.modules.selection.domain.pattern_scoring import (
|
|
|
|
|
PATTERN_SCORE_THRESHOLD,
|
|
|
|
|
PATTERN_SCORING_VERSION,
|
|
|
|
|
ZHIXING_B1_PATTERN_CASES,
|
|
|
|
|
PatternCase,
|
|
|
|
|
PatternScore,
|
|
|
|
|
PatternScoreBreakdown,
|
|
|
|
|
)
|
2026-08-09 09:34:46 +08:00
|
|
|
from zhixing_server.modules.selection.domain.runs import (
|
|
|
|
|
SelectionExecutionSource,
|
2026-08-10 11:09:23 +08:00
|
|
|
SelectionResultQuery,
|
2026-08-09 09:34:46 +08:00
|
|
|
SelectionRun,
|
|
|
|
|
SelectionRunItem,
|
|
|
|
|
SelectionRunStatus,
|
|
|
|
|
SelectionStock,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
TARGET = date(2026, 8, 8)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class FakeReader:
|
|
|
|
|
def __init__(self, source: SelectionExecutionSource) -> None:
|
|
|
|
|
self.source = source
|
|
|
|
|
|
|
|
|
|
def load_execution_source(
|
|
|
|
|
self,
|
|
|
|
|
strategy: str,
|
|
|
|
|
target_trade_date: date,
|
|
|
|
|
) -> SelectionExecutionSource:
|
|
|
|
|
assert strategy == "zhixing_b1"
|
|
|
|
|
assert target_trade_date == TARGET
|
|
|
|
|
return self.source
|
|
|
|
|
|
|
|
|
|
def load_history(self, ts_code: str, target_trade_date: date) -> StockHistory:
|
|
|
|
|
raise AssertionError("the fake evaluator should be used")
|
|
|
|
|
|
|
|
|
|
|
2026-08-12 09:45:16 +08:00
|
|
|
class BatchReader(FakeReader):
|
|
|
|
|
def __init__(self, source: SelectionExecutionSource) -> None:
|
|
|
|
|
super().__init__(source)
|
|
|
|
|
self.batch_calls: list[tuple[str, ...]] = []
|
|
|
|
|
|
|
|
|
|
def load_histories(
|
|
|
|
|
self,
|
|
|
|
|
stocks: tuple[SelectionStock, ...],
|
|
|
|
|
target_trade_date: date,
|
|
|
|
|
) -> tuple[StockHistory, ...]:
|
|
|
|
|
self.batch_calls.append(tuple(stock.ts_code for stock in stocks))
|
|
|
|
|
return tuple(StockHistory(ts_code=stock.ts_code, name=stock.name) for stock in stocks)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class PartialBatchReader(BatchReader):
|
|
|
|
|
def load_histories(
|
|
|
|
|
self,
|
|
|
|
|
stocks: tuple[SelectionStock, ...],
|
|
|
|
|
target_trade_date: date,
|
|
|
|
|
) -> tuple[StockHistory, ...]:
|
|
|
|
|
self.batch_calls.append(tuple(stock.ts_code for stock in stocks))
|
|
|
|
|
return ()
|
|
|
|
|
|
|
|
|
|
|
2026-08-09 09:34:46 +08:00
|
|
|
class FakeStore:
|
|
|
|
|
def __init__(self) -> None:
|
|
|
|
|
self.items: list[SelectionRunItem] = []
|
|
|
|
|
self.finished: tuple[str, SelectionRunStatus, dict[str, object]] | None = None
|
|
|
|
|
|
|
|
|
|
def prepare_run(
|
|
|
|
|
self,
|
|
|
|
|
strategy: Literal["zhixing_b1"],
|
|
|
|
|
target_trade_date: date,
|
|
|
|
|
source: SelectionExecutionSource,
|
|
|
|
|
*,
|
|
|
|
|
rerun: bool,
|
|
|
|
|
) -> SelectionRun:
|
|
|
|
|
assert strategy == "zhixing_b1"
|
|
|
|
|
assert target_trade_date == TARGET
|
|
|
|
|
assert rerun is False
|
|
|
|
|
return SelectionRun(
|
|
|
|
|
id="run-1",
|
|
|
|
|
strategy="zhixing_b1",
|
|
|
|
|
target_trade_date=TARGET,
|
|
|
|
|
market_sync_batch_id=source.market_sync_batch_id,
|
|
|
|
|
status="running",
|
|
|
|
|
target_count=source.target_count,
|
|
|
|
|
eligible_count=len(source.stocks),
|
|
|
|
|
evaluated_count=0,
|
|
|
|
|
selected_stock_count=0,
|
|
|
|
|
signal_count=0,
|
|
|
|
|
failed_count=0,
|
|
|
|
|
coverage=source.coverage,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def record_item(self, run_id: str, item: SelectionRunItem) -> None:
|
|
|
|
|
assert run_id == "run-1"
|
|
|
|
|
self.items.append(item)
|
|
|
|
|
|
|
|
|
|
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:
|
|
|
|
|
kwargs: dict[str, object] = {
|
|
|
|
|
"evaluated_count": evaluated_count,
|
|
|
|
|
"selected_stock_count": selected_stock_count,
|
|
|
|
|
"signal_count": signal_count,
|
|
|
|
|
"failed_count": failed_count,
|
|
|
|
|
}
|
|
|
|
|
if error_type is not None:
|
|
|
|
|
kwargs["error_type"] = error_type
|
|
|
|
|
if error_message is not None:
|
|
|
|
|
kwargs["error_message"] = error_message
|
|
|
|
|
self.finished = (run_id, status, kwargs)
|
|
|
|
|
|
2026-09-05 19:48:30 +08:00
|
|
|
def get_run(
|
|
|
|
|
self,
|
|
|
|
|
run_id: str,
|
|
|
|
|
*,
|
|
|
|
|
query: SelectionResultQuery | None = None,
|
|
|
|
|
sector_stock_codes: Sequence[str] | None = None,
|
|
|
|
|
):
|
2026-08-09 09:34:46 +08:00
|
|
|
return None
|
|
|
|
|
|
2026-08-10 11:09:23 +08:00
|
|
|
def get_latest_run(
|
|
|
|
|
self,
|
|
|
|
|
strategy: str,
|
|
|
|
|
target_trade_date: date | None = None,
|
|
|
|
|
*,
|
|
|
|
|
query: SelectionResultQuery | None = None,
|
2026-09-05 19:48:30 +08:00
|
|
|
sector_stock_codes: Sequence[str] | None = None,
|
2026-08-10 11:09:23 +08:00
|
|
|
):
|
2026-08-09 09:34:46 +08:00
|
|
|
return None
|
|
|
|
|
|
2026-09-05 19:48:30 +08:00
|
|
|
def get_run_identity(self, run_id: str):
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
def get_latest_run_identity(self, strategy: str, target_trade_date: date | None = None):
|
|
|
|
|
return None
|
|
|
|
|
|
2026-08-09 09:34:46 +08:00
|
|
|
|
2026-08-12 09:45:16 +08:00
|
|
|
class BatchStore(FakeStore):
|
|
|
|
|
def __init__(self) -> None:
|
|
|
|
|
super().__init__()
|
|
|
|
|
self.batches: list[tuple[SelectionRunItem, ...]] = []
|
|
|
|
|
|
|
|
|
|
def record_item(self, run_id: str, item: SelectionRunItem) -> None:
|
|
|
|
|
raise AssertionError("the batch path should use record_items")
|
|
|
|
|
|
|
|
|
|
def record_items(self, run_id: str, items: tuple[SelectionRunItem, ...]) -> None:
|
|
|
|
|
assert run_id == "run-1"
|
|
|
|
|
batch = tuple(items)
|
|
|
|
|
self.batches.append(batch)
|
|
|
|
|
self.items.extend(batch)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class FailingBatchStore(BatchStore):
|
|
|
|
|
def record_items(self, run_id: str, items: tuple[SelectionRunItem, ...]) -> None:
|
|
|
|
|
raise RuntimeError("batch write unavailable")
|
|
|
|
|
|
|
|
|
|
|
2026-08-09 09:34:46 +08:00
|
|
|
class FakeEvaluator:
|
|
|
|
|
def __init__(self, results: dict[str, SelectionEvaluation]) -> None:
|
|
|
|
|
self.results = results
|
|
|
|
|
|
|
|
|
|
def execute(self, ts_code: str, target_trade_date: date) -> SelectionEvaluation:
|
|
|
|
|
return self.results[ts_code]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class RaisingEvaluator:
|
|
|
|
|
def execute(self, ts_code: str, target_trade_date: date) -> SelectionEvaluation:
|
|
|
|
|
if ts_code == "600000.SH":
|
|
|
|
|
raise RuntimeError("temporary evaluator failure")
|
|
|
|
|
return SelectionEvaluation(ts_code, target_trade_date, "no_signal")
|
|
|
|
|
|
|
|
|
|
|
2026-08-12 09:45:16 +08:00
|
|
|
class ConcurrentHistoryEvaluator:
|
|
|
|
|
def __init__(self) -> None:
|
|
|
|
|
self.active = 0
|
|
|
|
|
self.peak = 0
|
|
|
|
|
self._lock = threading.Lock()
|
|
|
|
|
|
|
|
|
|
def execute(self, ts_code: str, target_trade_date: date) -> SelectionEvaluation:
|
|
|
|
|
raise AssertionError("the batch evaluator path should be used")
|
|
|
|
|
|
|
|
|
|
def execute_history(
|
|
|
|
|
self,
|
|
|
|
|
history: StockHistory,
|
|
|
|
|
target_trade_date: date,
|
|
|
|
|
) -> SelectionEvaluation:
|
|
|
|
|
with self._lock:
|
|
|
|
|
self.active += 1
|
|
|
|
|
self.peak = max(self.peak, self.active)
|
|
|
|
|
time.sleep(0.02)
|
|
|
|
|
with self._lock:
|
|
|
|
|
self.active -= 1
|
|
|
|
|
return SelectionEvaluation(history.ts_code, target_trade_date, "no_signal")
|
|
|
|
|
|
|
|
|
|
|
2026-08-31 16:14:16 +08:00
|
|
|
class FakePatternCaseLoader:
|
|
|
|
|
def __init__(self, *, error: Exception | None = None) -> None:
|
|
|
|
|
self.calls = 0
|
|
|
|
|
self.error = error
|
|
|
|
|
|
|
|
|
|
def load(self) -> tuple[PatternCase, ...]:
|
|
|
|
|
self.calls += 1
|
|
|
|
|
if self.error is not None:
|
|
|
|
|
raise self.error
|
|
|
|
|
return ()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class FakePatternScorer:
|
|
|
|
|
def __init__(self, *, error: Exception | None = None) -> None:
|
|
|
|
|
self.calls: list[str] = []
|
|
|
|
|
self.error = error
|
|
|
|
|
|
|
|
|
|
def score(self, history: StockHistory, cases: Sequence[PatternCase]) -> PatternScore:
|
|
|
|
|
self.calls.append(history.ts_code)
|
|
|
|
|
if self.error is not None:
|
|
|
|
|
raise self.error
|
|
|
|
|
return PatternScore(
|
|
|
|
|
status="matched",
|
|
|
|
|
value=88.0,
|
|
|
|
|
threshold=PATTERN_SCORE_THRESHOLD,
|
|
|
|
|
version=PATTERN_SCORING_VERSION,
|
|
|
|
|
case=ZHIXING_B1_PATTERN_CASES[0],
|
|
|
|
|
breakdown=PatternScoreBreakdown(80.0, 85.0, 90.0, 88.0),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
2026-08-09 09:34:46 +08:00
|
|
|
def _source() -> SelectionExecutionSource:
|
|
|
|
|
return SelectionExecutionSource(
|
|
|
|
|
market_sync_batch_id="market-run-1",
|
|
|
|
|
target_trade_date=TARGET,
|
|
|
|
|
target_count=2,
|
|
|
|
|
valid_count=2,
|
|
|
|
|
coverage=Decimal("1"),
|
|
|
|
|
stocks=(
|
|
|
|
|
SelectionStock("000001.SZ", "平安银行"),
|
|
|
|
|
SelectionStock("600000.SH", "浦发银行"),
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _signal(ts_code: str, category: str) -> SelectionSignal:
|
|
|
|
|
from zhixing_server.modules.selection.domain.models import ZhixingB1Category
|
|
|
|
|
|
|
|
|
|
return SelectionSignal(
|
|
|
|
|
ts_code=ts_code,
|
|
|
|
|
name="平安银行",
|
|
|
|
|
target_trade_date=TARGET,
|
|
|
|
|
strategy="zhixing_b1",
|
|
|
|
|
category=ZhixingB1Category(category),
|
|
|
|
|
close=10.5,
|
|
|
|
|
details={"j": 12.0},
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_prepare_captures_market_source_and_execute_persists_all_categories() -> None:
|
|
|
|
|
source = _source()
|
|
|
|
|
store = FakeStore()
|
|
|
|
|
evaluator = FakeEvaluator(
|
|
|
|
|
{
|
|
|
|
|
"000001.SZ": SelectionEvaluation(
|
|
|
|
|
"000001.SZ",
|
|
|
|
|
TARGET,
|
|
|
|
|
"selected",
|
|
|
|
|
signals=(
|
|
|
|
|
_signal("000001.SZ", "zhixing_b1_original_b1"),
|
|
|
|
|
_signal("000001.SZ", "zhixing_b1_pullback_white"),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
"600000.SH": SelectionEvaluation(
|
|
|
|
|
"600000.SH",
|
|
|
|
|
TARGET,
|
|
|
|
|
"no_signal",
|
|
|
|
|
reason="no category matched",
|
|
|
|
|
),
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
service = RunZhixingB1(FakeReader(source), store, evaluator)
|
|
|
|
|
|
|
|
|
|
prepared = service.prepare("zhixing_b1", TARGET, rerun=False)
|
|
|
|
|
assert isinstance(prepared, PreparedSelectionRun)
|
|
|
|
|
service.execute(prepared)
|
|
|
|
|
|
|
|
|
|
assert [item.status for item in store.items] == ["selected", "no_signal"]
|
|
|
|
|
assert store.items[0].signal_count == 2
|
|
|
|
|
assert store.finished is not None
|
|
|
|
|
assert store.finished[0:2] == ("run-1", "success")
|
|
|
|
|
assert store.finished[2] == {
|
|
|
|
|
"evaluated_count": 2,
|
|
|
|
|
"selected_stock_count": 1,
|
|
|
|
|
"signal_count": 2,
|
|
|
|
|
"failed_count": 0,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_execute_marks_partial_success_when_one_stock_lacks_history() -> None:
|
|
|
|
|
source = _source()
|
|
|
|
|
store = FakeStore()
|
|
|
|
|
evaluator = FakeEvaluator(
|
|
|
|
|
{
|
|
|
|
|
"000001.SZ": SelectionEvaluation("000001.SZ", TARGET, "no_signal"),
|
|
|
|
|
"600000.SH": SelectionEvaluation(
|
|
|
|
|
"600000.SH",
|
|
|
|
|
TARGET,
|
|
|
|
|
"insufficient_history",
|
|
|
|
|
reason="warm-up data is incomplete",
|
|
|
|
|
),
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
service = RunZhixingB1(FakeReader(source), store, evaluator)
|
|
|
|
|
|
|
|
|
|
service.execute(service.prepare("zhixing_b1", TARGET, rerun=False))
|
|
|
|
|
|
|
|
|
|
assert store.finished is not None
|
|
|
|
|
assert store.finished[0:2] == ("run-1", "partial_success")
|
|
|
|
|
assert store.finished[2]["failed_count"] == 1
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_execute_isolates_unexpected_single_stock_failure() -> None:
|
|
|
|
|
source = _source()
|
|
|
|
|
store = FakeStore()
|
|
|
|
|
service = RunZhixingB1(FakeReader(source), store, RaisingEvaluator())
|
|
|
|
|
|
|
|
|
|
service.execute(service.prepare("zhixing_b1", TARGET, rerun=False))
|
|
|
|
|
|
|
|
|
|
assert [item.status for item in store.items] == ["no_signal", "data_error"]
|
|
|
|
|
assert store.items[1].reason == "temporary evaluator failure"
|
|
|
|
|
assert store.finished is not None
|
|
|
|
|
assert store.finished[0:2] == ("run-1", "partial_success")
|
|
|
|
|
assert store.finished[2]["evaluated_count"] == 2
|
|
|
|
|
assert store.finished[2]["failed_count"] == 1
|
2026-08-12 09:45:16 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_execute_batches_history_reads_writes_and_limits_evaluation_workers() -> None:
|
|
|
|
|
source = SelectionExecutionSource(
|
|
|
|
|
market_sync_batch_id="market-run-1",
|
|
|
|
|
target_trade_date=TARGET,
|
|
|
|
|
target_count=8,
|
|
|
|
|
valid_count=8,
|
|
|
|
|
coverage=Decimal("1"),
|
|
|
|
|
stocks=tuple(SelectionStock(f"{index:06d}.SZ", f"stock-{index}") for index in range(8)),
|
|
|
|
|
)
|
|
|
|
|
reader = BatchReader(source)
|
|
|
|
|
store = BatchStore()
|
|
|
|
|
evaluator = ConcurrentHistoryEvaluator()
|
|
|
|
|
|
|
|
|
|
service = RunZhixingB1(
|
|
|
|
|
reader,
|
|
|
|
|
store,
|
|
|
|
|
evaluator,
|
|
|
|
|
max_workers=4,
|
|
|
|
|
batch_size=4,
|
|
|
|
|
)
|
|
|
|
|
service.execute(service.prepare("zhixing_b1", TARGET, rerun=False))
|
|
|
|
|
|
|
|
|
|
assert reader.batch_calls == [
|
|
|
|
|
("000000.SZ", "000001.SZ", "000002.SZ", "000003.SZ"),
|
|
|
|
|
("000004.SZ", "000005.SZ", "000006.SZ", "000007.SZ"),
|
|
|
|
|
]
|
|
|
|
|
assert [len(batch) for batch in store.batches] == [4, 4]
|
|
|
|
|
assert evaluator.peak <= 4
|
|
|
|
|
assert evaluator.peak >= 2
|
|
|
|
|
assert store.finished is not None
|
|
|
|
|
assert store.finished[0:2] == ("run-1", "success")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_execute_maps_missing_batch_history_without_a_single_stock_read() -> None:
|
|
|
|
|
source = _source()
|
|
|
|
|
reader = PartialBatchReader(source)
|
|
|
|
|
store = FakeStore()
|
|
|
|
|
|
|
|
|
|
service = RunZhixingB1(reader, store)
|
|
|
|
|
service.execute(service.prepare("zhixing_b1", TARGET, rerun=False))
|
|
|
|
|
|
|
|
|
|
assert reader.batch_calls == [("000001.SZ", "600000.SH")]
|
|
|
|
|
assert [item.status for item in store.items] == ["missing_target_bar", "missing_target_bar"]
|
|
|
|
|
assert store.finished is not None
|
|
|
|
|
assert store.finished[0:2] == ("run-1", "failed")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_execute_marks_batch_write_failure_as_failed() -> None:
|
|
|
|
|
source = _source()
|
|
|
|
|
reader = BatchReader(source)
|
|
|
|
|
store = FailingBatchStore()
|
|
|
|
|
evaluator = ConcurrentHistoryEvaluator()
|
|
|
|
|
|
|
|
|
|
service = RunZhixingB1(reader, store, evaluator)
|
|
|
|
|
service.execute(service.prepare("zhixing_b1", TARGET, rerun=False))
|
|
|
|
|
|
|
|
|
|
assert store.finished is not None
|
|
|
|
|
assert store.finished[0:2] == ("run-1", "failed")
|
|
|
|
|
assert store.finished[2]["error_type"] == "batch_error"
|
|
|
|
|
assert store.finished[2]["failed_count"] == 1
|
2026-08-31 16:14:16 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_execute_loads_cases_once_and_scores_only_selected_stocks() -> None:
|
|
|
|
|
source = _source()
|
|
|
|
|
reader = BatchReader(source)
|
|
|
|
|
store = FakeStore()
|
|
|
|
|
loader = FakePatternCaseLoader()
|
|
|
|
|
scorer = FakePatternScorer()
|
|
|
|
|
evaluator = FakeEvaluator(
|
|
|
|
|
{
|
|
|
|
|
"000001.SZ": SelectionEvaluation(
|
|
|
|
|
"000001.SZ",
|
|
|
|
|
TARGET,
|
|
|
|
|
"selected",
|
|
|
|
|
signals=(_signal("000001.SZ", "zhixing_b1_original_b1"),),
|
|
|
|
|
),
|
|
|
|
|
"600000.SH": SelectionEvaluation("600000.SH", TARGET, "no_signal"),
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
service = RunZhixingB1(
|
|
|
|
|
reader,
|
|
|
|
|
store,
|
|
|
|
|
evaluator,
|
2026-09-05 19:48:30 +08:00
|
|
|
pattern_case_loader=loader,
|
|
|
|
|
pattern_scorer=scorer,
|
2026-08-31 16:14:16 +08:00
|
|
|
pattern_scoring_enabled=True,
|
|
|
|
|
batch_size=1,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
service.execute(service.prepare("zhixing_b1", TARGET, rerun=False))
|
|
|
|
|
|
|
|
|
|
assert loader.calls == 1
|
|
|
|
|
assert scorer.calls == ["000001.SZ"]
|
|
|
|
|
assert [item.pattern_score.status for item in store.items] == ["matched", "not_executed"]
|
|
|
|
|
assert store.finished is not None
|
|
|
|
|
assert store.finished[0:2] == ("run-1", "success")
|
|
|
|
|
assert store.finished[2]["failed_count"] == 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_execute_isolates_pattern_scoring_failure_from_selection_status() -> None:
|
|
|
|
|
source = _source()
|
|
|
|
|
store = FakeStore()
|
|
|
|
|
loader = FakePatternCaseLoader()
|
|
|
|
|
scorer = FakePatternScorer(error=RuntimeError("FastDTW unavailable"))
|
|
|
|
|
evaluator = FakeEvaluator(
|
|
|
|
|
{
|
|
|
|
|
"000001.SZ": SelectionEvaluation(
|
|
|
|
|
"000001.SZ",
|
|
|
|
|
TARGET,
|
|
|
|
|
"selected",
|
|
|
|
|
signals=(_signal("000001.SZ", "zhixing_b1_original_b1"),),
|
|
|
|
|
),
|
|
|
|
|
"600000.SH": SelectionEvaluation("600000.SH", TARGET, "no_signal"),
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
service = RunZhixingB1(
|
|
|
|
|
BatchReader(source),
|
|
|
|
|
store,
|
|
|
|
|
evaluator,
|
2026-09-05 19:48:30 +08:00
|
|
|
pattern_case_loader=loader,
|
|
|
|
|
pattern_scorer=scorer,
|
2026-08-31 16:14:16 +08:00
|
|
|
pattern_scoring_enabled=True,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
service.execute(service.prepare("zhixing_b1", TARGET, rerun=False))
|
|
|
|
|
|
|
|
|
|
assert store.items[0].status == "selected"
|
|
|
|
|
assert store.items[0].pattern_score == PatternScore.failed("FastDTW unavailable")
|
|
|
|
|
assert store.finished is not None
|
|
|
|
|
assert store.finished[0:2] == ("run-1", "success")
|
|
|
|
|
assert store.finished[2]["failed_count"] == 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_execute_skips_pattern_dependencies_when_feature_flag_is_disabled() -> None:
|
|
|
|
|
source = _source()
|
|
|
|
|
store = FakeStore()
|
|
|
|
|
loader = FakePatternCaseLoader(error=AssertionError("loader must not run"))
|
|
|
|
|
scorer = FakePatternScorer(error=AssertionError("scorer must not run"))
|
|
|
|
|
evaluator = FakeEvaluator(
|
|
|
|
|
{
|
|
|
|
|
"000001.SZ": SelectionEvaluation(
|
|
|
|
|
"000001.SZ",
|
|
|
|
|
TARGET,
|
|
|
|
|
"selected",
|
|
|
|
|
signals=(_signal("000001.SZ", "zhixing_b1_original_b1"),),
|
|
|
|
|
),
|
|
|
|
|
"600000.SH": SelectionEvaluation("600000.SH", TARGET, "no_signal"),
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
service = RunZhixingB1(
|
|
|
|
|
BatchReader(source),
|
|
|
|
|
store,
|
|
|
|
|
evaluator,
|
2026-09-05 19:48:30 +08:00
|
|
|
pattern_case_loader=loader,
|
|
|
|
|
pattern_scorer=scorer,
|
2026-08-31 16:14:16 +08:00
|
|
|
pattern_scoring_enabled=False,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
service.execute(service.prepare("zhixing_b1", TARGET, rerun=False))
|
|
|
|
|
|
|
|
|
|
assert loader.calls == 0
|
|
|
|
|
assert scorer.calls == []
|
|
|
|
|
assert [item.pattern_score.status for item in store.items] == [
|
|
|
|
|
"not_executed",
|
|
|
|
|
"not_executed",
|
|
|
|
|
]
|
|
|
|
|
assert store.items[0].signal_count == 1
|
|
|
|
|
assert store.finished is not None
|
|
|
|
|
assert store.finished[2]["failed_count"] == 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_execute_marks_scores_failed_when_case_library_is_unavailable() -> None:
|
|
|
|
|
source = _source()
|
|
|
|
|
store = FakeStore()
|
|
|
|
|
loader = FakePatternCaseLoader(error=RuntimeError("case_011 requires 25 qfq rows"))
|
|
|
|
|
scorer = FakePatternScorer()
|
|
|
|
|
evaluator = FakeEvaluator(
|
|
|
|
|
{
|
|
|
|
|
"000001.SZ": SelectionEvaluation(
|
|
|
|
|
"000001.SZ",
|
|
|
|
|
TARGET,
|
|
|
|
|
"selected",
|
|
|
|
|
signals=(_signal("000001.SZ", "zhixing_b1_original_b1"),),
|
|
|
|
|
),
|
|
|
|
|
"600000.SH": SelectionEvaluation("600000.SH", TARGET, "no_signal"),
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
service = RunZhixingB1(
|
|
|
|
|
BatchReader(source),
|
|
|
|
|
store,
|
|
|
|
|
evaluator,
|
2026-09-05 19:48:30 +08:00
|
|
|
pattern_case_loader=loader,
|
|
|
|
|
pattern_scorer=scorer,
|
2026-08-31 16:14:16 +08:00
|
|
|
pattern_scoring_enabled=True,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
service.execute(service.prepare("zhixing_b1", TARGET, rerun=False))
|
|
|
|
|
|
|
|
|
|
assert loader.calls == 1
|
|
|
|
|
assert scorer.calls == []
|
|
|
|
|
assert store.items[0].status == "selected"
|
|
|
|
|
assert store.items[0].pattern_score.status == "failed"
|
|
|
|
|
assert store.items[0].signals[0].category.value == "zhixing_b1_original_b1"
|
|
|
|
|
assert store.finished is not None
|
|
|
|
|
assert store.finished[0:2] == ("run-1", "success")
|
|
|
|
|
assert store.finished[2]["failed_count"] == 0
|