feat(selection): 集成 B1 FastDTW 图形评分
This commit is contained in:
@@ -8,6 +8,12 @@ import pytest
|
||||
from psycopg.types.json import Jsonb
|
||||
|
||||
from zhixing_server.modules.selection.domain.models import SelectionSignal, ZhixingB1Category
|
||||
from zhixing_server.modules.selection.domain.pattern_scoring import (
|
||||
PATTERN_SCORING_VERSION,
|
||||
ZHIXING_B1_PATTERN_CASES,
|
||||
PatternScore,
|
||||
PatternScoreBreakdown,
|
||||
)
|
||||
from zhixing_server.modules.selection.domain.runs import (
|
||||
SelectionExecutionSource,
|
||||
SelectionRerunRequired,
|
||||
@@ -222,6 +228,14 @@ def test_record_items_uses_one_delete_and_two_batch_upserts(
|
||||
name="平安银行",
|
||||
status="selected",
|
||||
signal_count=2,
|
||||
pattern_score=PatternScore(
|
||||
status="matched",
|
||||
value=86.4,
|
||||
threshold=60.0,
|
||||
version=PATTERN_SCORING_VERSION,
|
||||
case=ZHIXING_B1_PATTERN_CASES[0],
|
||||
breakdown=PatternScoreBreakdown(71.2, 83.0, 88.0, 90.1),
|
||||
),
|
||||
signals=(first, second),
|
||||
),
|
||||
SelectionRunItem(
|
||||
@@ -238,6 +252,28 @@ def test_record_items_uses_one_delete_and_two_batch_upserts(
|
||||
assert delete_parameters == ("run-1", ["000001.SZ", "600000.SH"])
|
||||
assert len(connection.executemany_calls) == 2
|
||||
assert "INSERT INTO selection_run_item" in connection.executemany_calls[0][0]
|
||||
item_parameters = connection.executemany_calls[0][1]
|
||||
assert item_parameters[0][6:13] == (
|
||||
"matched",
|
||||
86.4,
|
||||
60.0,
|
||||
PATTERN_SCORING_VERSION,
|
||||
"case_001",
|
||||
"华纳药厂",
|
||||
date(2025, 5, 12),
|
||||
)
|
||||
assert isinstance(item_parameters[0][13], Jsonb)
|
||||
assert item_parameters[1][6:] == (
|
||||
"not_executed",
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
assert "INSERT INTO selection_signal" in connection.executemany_calls[1][0]
|
||||
signal_parameters = connection.executemany_calls[1][1]
|
||||
assert len(signal_parameters) == 2
|
||||
@@ -281,11 +317,35 @@ class LoadConnection:
|
||||
None,
|
||||
)
|
||||
)
|
||||
if "FROM selection_run_item" in query:
|
||||
return LoadResult(rows=[("000001.SZ", "平安银行", "selected", 2, None)])
|
||||
if "COUNT(DISTINCT ts_code) FROM selection_signal" in query:
|
||||
if "FROM selection_run_item\n" in query:
|
||||
return LoadResult(
|
||||
rows=[
|
||||
(
|
||||
"000001.SZ",
|
||||
"平安银行",
|
||||
"selected",
|
||||
2,
|
||||
None,
|
||||
"matched",
|
||||
Decimal("86.40"),
|
||||
Decimal("60.00"),
|
||||
PATTERN_SCORING_VERSION,
|
||||
"case_001",
|
||||
"华纳药厂",
|
||||
date(2025, 5, 12),
|
||||
{
|
||||
"trend_structure": 71.2,
|
||||
"kdj_state": 83.0,
|
||||
"volume_pattern": 88.0,
|
||||
"price_shape": 90.1,
|
||||
},
|
||||
None,
|
||||
)
|
||||
]
|
||||
)
|
||||
if "SELECT COUNT(*) FROM selection_run_item AS item" in query:
|
||||
return LoadResult(row=(2,))
|
||||
if "SELECT DISTINCT ts_code" in query:
|
||||
if "SELECT item.ts_code" in query:
|
||||
return LoadResult(rows=[("000001.SZ",)])
|
||||
return LoadResult(
|
||||
rows=[
|
||||
@@ -315,7 +375,7 @@ class EmptyStockPageConnection(LoadConnection):
|
||||
"""Return a non-zero filtered total with no stocks on the requested page."""
|
||||
|
||||
def execute(self, query: str, parameters: tuple[object, ...]) -> "LoadResult":
|
||||
if "SELECT DISTINCT ts_code" in query:
|
||||
if "SELECT item.ts_code" in query:
|
||||
self.statements.append((query, parameters))
|
||||
return LoadResult(rows=[])
|
||||
return super().execute(query, parameters)
|
||||
@@ -355,6 +415,7 @@ def test_get_run_pages_stocks_and_loads_all_signals_for_category_matches(
|
||||
page_size=1,
|
||||
search="100%",
|
||||
category="pullback",
|
||||
sort="score_desc",
|
||||
),
|
||||
)
|
||||
|
||||
@@ -364,19 +425,21 @@ def test_get_run_pages_stocks_and_loads_all_signals_for_category_matches(
|
||||
ZHIXING_B1_SIGNAL_ORDER[-1],
|
||||
]
|
||||
assert run.stocks_total == 2
|
||||
assert run.items[0].pattern_score.status == "matched"
|
||||
assert run.items[0].pattern_score.value == 86.4
|
||||
count_query, count_parameters = next(
|
||||
(query, parameters)
|
||||
for query, parameters in connection.statements
|
||||
if "COUNT(DISTINCT ts_code) FROM selection_signal" in query
|
||||
if "SELECT COUNT(*) FROM selection_run_item AS item" in query
|
||||
)
|
||||
assert "name ILIKE %s ESCAPE" in count_query
|
||||
assert count_parameters == ("run-1", "%100\\%%", "%100\\%%", "zhixing_b1_pullback_%")
|
||||
stock_page_query, page_parameters = next(
|
||||
(query, parameters)
|
||||
for query, parameters in connection.statements
|
||||
if "SELECT DISTINCT ts_code" in query
|
||||
if "SELECT item.ts_code" in query
|
||||
)
|
||||
assert "ORDER BY ts_code" in stock_page_query
|
||||
assert "ORDER BY item.score_value DESC NULLS LAST, item.ts_code ASC" in stock_page_query
|
||||
assert page_parameters[-2:] == (1, 0)
|
||||
signal_query, signal_parameters = next(
|
||||
(query, parameters)
|
||||
@@ -409,8 +472,30 @@ def test_get_run_does_not_load_signals_for_an_empty_stock_page(
|
||||
stock_page_query, stock_page_parameters = next(
|
||||
(query, parameters)
|
||||
for query, parameters in connection.statements
|
||||
if "SELECT DISTINCT ts_code" in query
|
||||
if "SELECT item.ts_code" in query
|
||||
)
|
||||
assert "ORDER BY ts_code" in stock_page_query
|
||||
assert "ORDER BY item.ts_code ASC" in stock_page_query
|
||||
assert stock_page_parameters[-2:] == (1, 2)
|
||||
assert not any("ts_code = ANY(%s)" in query for query, _ in connection.statements)
|
||||
|
||||
|
||||
def test_get_run_sorts_scores_ascending_with_nulls_last_and_code_tiebreak(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
connection = LoadConnection()
|
||||
|
||||
def connect(database_url: str) -> LoadConnection:
|
||||
assert database_url == "postgresql://test"
|
||||
return connection
|
||||
|
||||
monkeypatch.setattr(psycopg, "connect", connect)
|
||||
run = PostgresSelectionRunRepository("postgresql://test").get_run(
|
||||
"run-1",
|
||||
query=SelectionResultQuery(sort="score_asc"),
|
||||
)
|
||||
|
||||
assert run is not None
|
||||
stock_page_query = next(
|
||||
query for query, _ in connection.statements if "SELECT item.ts_code" in query
|
||||
)
|
||||
assert "ORDER BY item.score_value ASC NULLS LAST, item.ts_code ASC" in stock_page_query
|
||||
|
||||
Reference in New Issue
Block a user