refactor(selection): 统一信号与股票处理逻辑,重构相关数据结构与接口,更新前端展示以支持股票信息

This commit is contained in:
yuxuanhui
2026-08-28 11:39:05 +08:00
parent 3da992700b
commit ad9545ef55
12 changed files with 408 additions and 189 deletions
+31 -11
View File
@@ -91,7 +91,7 @@ class FakeSelectionService:
def _run(run_id: str, status: str) -> SelectionRun:
signal = SelectionSignal(
original_signal = SelectionSignal(
ts_code="000001.SZ",
name="平安银行",
target_trade_date=TARGET,
@@ -100,6 +100,15 @@ def _run(run_id: str, status: str) -> SelectionRun:
close=10.5,
details={"j": 12.0},
)
pullback_signal = SelectionSignal(
ts_code="000001.SZ",
name="平安银行",
target_trade_date=TARGET,
strategy="zhixing_b1",
category=ZhixingB1Category.PULLBACK_WHITE,
close=10.5,
details={"j": 13.0, "rsi": 20.0},
)
from zhixing_server.modules.selection.domain.runs import SelectionRunItem
return SelectionRun(
@@ -112,7 +121,7 @@ def _run(run_id: str, status: str) -> SelectionRun:
eligible_count=1,
evaluated_count=1,
selected_stock_count=1,
signal_count=1,
signal_count=2,
failed_count=0,
coverage=Decimal("1"),
items=(
@@ -120,11 +129,12 @@ def _run(run_id: str, status: str) -> SelectionRun:
ts_code="000001.SZ",
name="平安银行",
status="selected",
signal_count=1,
signals=(signal,),
signal_count=2,
signals=(original_signal, pullback_signal),
),
),
signals=(signal,),
signals=(original_signal, pullback_signal),
stocks_total=1,
)
@@ -209,7 +219,7 @@ def test_query_returns_no_data_without_fabricating_a_result() -> None:
assert response.status_code == 200
assert response.json()["status"] == "no_data"
assert response.json()["signals"] == []
assert response.json()["stocks"] == []
def test_query_returns_persisted_signal_details() -> None:
@@ -220,12 +230,22 @@ def test_query_returns_persisted_signal_details() -> None:
assert response.status_code == 200
body = response.json()
assert body["run_id"] == "run-http"
assert body["signal_count"] == 1
assert body["signal_count"] == 2
assert body["page"] == 1
assert body["page_size"] == 10
assert body["signals_total"] == 1
assert body["signals"][0]["category"] == "zhixing_b1_original_b1"
assert body["signals"][0]["details"] == {"j": 12.0}
assert body["stocks_total"] == 1
assert "signals_total" not in body
assert "signals" not in body
assert len(body["stocks"]) == 1
assert body["stocks"][0]["ts_code"] == "000001.SZ"
assert [signal["category"] for signal in body["stocks"][0]["signals"]] == [
"zhixing_b1_original_b1",
"zhixing_b1_pullback_white",
]
assert [signal["details"] for signal in body["stocks"][0]["signals"]] == [
{"j": 12.0},
{"j": 13.0, "rsi": 20.0},
]
def test_query_forwards_pagination_and_filters() -> None:
@@ -269,4 +289,4 @@ def test_run_polling_returns_the_persisted_terminal_result() -> None:
assert response.status_code == 200
assert response.json()["status"] == "success"
assert response.json()["signals"][0]["category"] == "zhixing_b1_original_b1"
assert response.json()["stocks"][0]["signals"][0]["category"] == ("zhixing_b1_original_b1")
@@ -283,8 +283,10 @@ class LoadConnection:
)
if "FROM selection_run_item" in query:
return LoadResult(rows=[("000001.SZ", "平安银行", "selected", 2, None)])
if "COUNT(*) FROM selection_signal" in query:
if "COUNT(DISTINCT ts_code) FROM selection_signal" in query:
return LoadResult(row=(2,))
if "SELECT DISTINCT ts_code" in query:
return LoadResult(rows=[("000001.SZ",)])
return LoadResult(
rows=[
(
@@ -301,7 +303,7 @@ class LoadConnection:
"平安银行",
TARGET,
"zhixing_b1",
ZHIXING_B1_SIGNAL_ORDER[0].value,
ZhixingB1Category.ORIGINAL_B1.value,
Decimal("10.5"),
{},
),
@@ -309,6 +311,16 @@ class LoadConnection:
)
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:
self.statements.append((query, parameters))
return LoadResult(rows=[])
return super().execute(query, parameters)
class LoadResult:
def __init__(
self,
@@ -326,7 +338,9 @@ class LoadResult:
return self.rows
def test_get_run_orders_signals_by_formula_priority(monkeypatch: pytest.MonkeyPatch) -> None:
def test_get_run_pages_stocks_and_loads_all_signals_for_category_matches(
monkeypatch: pytest.MonkeyPatch,
) -> None:
connection = LoadConnection()
def connect(database_url: str) -> LoadConnection:
@@ -337,7 +351,7 @@ def test_get_run_orders_signals_by_formula_priority(monkeypatch: pytest.MonkeyPa
run = PostgresSelectionRunRepository("postgresql://test").get_run(
"run-1",
query=SelectionResultQuery(
page=2,
page=1,
page_size=1,
search="100%",
category="pullback",
@@ -346,20 +360,57 @@ def test_get_run_orders_signals_by_formula_priority(monkeypatch: pytest.MonkeyPa
assert run is not None
assert [signal.category for signal in run.signals] == [
ZHIXING_B1_SIGNAL_ORDER[0],
ZhixingB1Category.ORIGINAL_B1,
ZHIXING_B1_SIGNAL_ORDER[-1],
]
assert run.stocks_total == 2
count_query, count_parameters = next(
(query, parameters)
for query, parameters in connection.statements
if "COUNT(*) FROM selection_signal" in query
if "COUNT(DISTINCT ts_code) FROM selection_signal" in query
)
assert "name ILIKE %s ESCAPE" in count_query
assert count_parameters == ("run-1", "%100\\%%", "%100\\%%", "zhixing_b1_pullback_%")
page_query, page_parameters = next(
stock_page_query, page_parameters = next(
(query, parameters)
for query, parameters in connection.statements
if "LIMIT %s OFFSET %s" in query
if "SELECT DISTINCT ts_code" in query
)
assert "ORDER BY ts_code, CASE category" in page_query
assert page_parameters[-2:] == (1, 1)
assert "ORDER BY ts_code" in stock_page_query
assert page_parameters[-2:] == (1, 0)
signal_query, signal_parameters = next(
(query, parameters)
for query, parameters in connection.statements
if "ts_code = ANY(%s)" in query and "SELECT\n" in query
)
assert "ORDER BY ts_code, CASE category" in signal_query
assert "category LIKE" not in signal_query
assert signal_parameters == ("run-1", ["000001.SZ"])
def test_get_run_does_not_load_signals_for_an_empty_stock_page(
monkeypatch: pytest.MonkeyPatch,
) -> None:
connection = EmptyStockPageConnection()
def connect(database_url: str) -> EmptyStockPageConnection:
assert database_url == "postgresql://test"
return connection
monkeypatch.setattr(psycopg, "connect", connect)
run = PostgresSelectionRunRepository("postgresql://test").get_run(
"run-1",
query=SelectionResultQuery(page=3, page_size=1),
)
assert run is not None
assert run.stocks_total == 2
assert run.signals == ()
stock_page_query, stock_page_parameters = next(
(query, parameters)
for query, parameters in connection.statements
if "SELECT DISTINCT ts_code" in query
)
assert "ORDER BY ts_code" 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)