feat(selection): 支持选股结果服务端分页

This commit is contained in:
yuxuanhui
2026-08-10 11:09:23 +08:00
parent ed7bdda515
commit 3af97bf65d
16 changed files with 431 additions and 63 deletions
+49 -1
View File
@@ -11,6 +11,7 @@ from zhixing_server.modules.selection.domain.models import SelectionSignal, Zhix
from zhixing_server.modules.selection.domain.runs import (
SelectionExecutionSource,
SelectionRerunRequired,
SelectionResultQuery,
SelectionRun,
SelectionRunInProgress,
SelectionStock,
@@ -28,6 +29,7 @@ class FakeSelectionService:
self.run = run
self.executed = False
self.mode = "ok"
self.last_query: SelectionResultQuery | None = None
def prepare(
self,
@@ -58,14 +60,23 @@ class FakeSelectionService:
def execute(self, prepared: PreparedSelectionRun) -> None:
self.executed = True
def get_run(self, run_id: str) -> SelectionRun | None:
def get_run(
self,
run_id: str,
*,
query: SelectionResultQuery | None = None,
) -> SelectionRun | None:
self.last_query = query
return self.run if self.run and self.run.id == run_id else None
def get_latest(
self,
strategy: str,
target_trade_date: date | None = None,
*,
query: SelectionResultQuery | None = None,
) -> SelectionRun | None:
self.last_query = query
if self.run is None:
return None
if target_trade_date is not None and self.run.target_trade_date != target_trade_date:
@@ -193,10 +204,47 @@ def test_query_returns_persisted_signal_details() -> None:
body = response.json()
assert body["run_id"] == "run-http"
assert body["signal_count"] == 1
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}
def test_query_forwards_pagination_and_filters() -> None:
service = FakeSelectionService(_run("run-http", "success"))
response = _client(service).get(
"/api/v1/selection/results",
params={
"strategy": "zhixing_b1",
"page": 2,
"page_size": 5,
"search": " 平安银行 ",
"category": "original",
},
)
assert response.status_code == 200
assert service.last_query == SelectionResultQuery(
page=2,
page_size=5,
search="平安银行",
category="original",
)
assert response.json()["page"] == 2
assert response.json()["page_size"] == 5
def test_query_rejects_invalid_page_size() -> None:
response = _client(FakeSelectionService()).get(
"/api/v1/selection/results",
params={"strategy": "zhixing_b1", "page_size": 101},
)
assert response.status_code == 422
def test_run_polling_returns_the_persisted_terminal_result() -> None:
response = _client(FakeSelectionService(_run("run-http", "success"))).get(
"/api/v1/selection/runs/run-http"
@@ -11,6 +11,7 @@ from zhixing_server.modules.selection.domain.models import SelectionSignal, Zhix
from zhixing_server.modules.selection.domain.runs import (
SelectionExecutionSource,
SelectionRerunRequired,
SelectionResultQuery,
SelectionRunInProgress,
SelectionRunItem,
SelectionStock,
@@ -163,6 +164,9 @@ def test_record_item_persists_independent_signals_as_jsonb(monkeypatch: pytest.M
class LoadConnection:
def __init__(self) -> None:
self.statements: list[tuple[str, tuple[object, ...]]] = []
def __enter__(self) -> "LoadConnection":
return self
@@ -170,6 +174,7 @@ class LoadConnection:
return None
def execute(self, query: str, parameters: tuple[object, ...]) -> "LoadResult":
self.statements.append((query, parameters))
if "FROM selection_run\n" in query:
return LoadResult(
row=(
@@ -193,6 +198,8 @@ class LoadConnection:
)
if "FROM selection_run_item" in query:
return LoadResult(rows=[("000001.SZ", "平安银行", "selected", 2, None)])
if "COUNT(*) FROM selection_signal" in query:
return LoadResult(row=(2,))
return LoadResult(
rows=[
(
@@ -242,10 +249,32 @@ def test_get_run_orders_signals_by_formula_priority(monkeypatch: pytest.MonkeyPa
return connection
monkeypatch.setattr(psycopg, "connect", connect)
run = PostgresSelectionRunRepository("postgresql://test").get_run("run-1")
run = PostgresSelectionRunRepository("postgresql://test").get_run(
"run-1",
query=SelectionResultQuery(
page=2,
page_size=1,
search="100%",
category="pullback",
),
)
assert run is not None
assert [signal.category for signal in run.signals] == [
ZHIXING_B1_SIGNAL_ORDER[0],
ZHIXING_B1_SIGNAL_ORDER[-1],
]
count_query, count_parameters = next(
(query, parameters)
for query, parameters in connection.statements
if "COUNT(*) 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(
(query, parameters)
for query, parameters in connection.statements
if "LIMIT %s OFFSET %s" in query
)
assert "ORDER BY ts_code, CASE category" in page_query
assert page_parameters[-2:] == (1, 1)
@@ -15,6 +15,7 @@ from zhixing_server.modules.selection.domain.models import (
)
from zhixing_server.modules.selection.domain.runs import (
SelectionExecutionSource,
SelectionResultQuery,
SelectionRun,
SelectionRunItem,
SelectionRunStatus,
@@ -100,10 +101,16 @@ class FakeStore:
kwargs["error_message"] = error_message
self.finished = (run_id, status, kwargs)
def get_run(self, run_id: str):
def get_run(self, run_id: str, *, query: SelectionResultQuery | None = None):
return None
def get_latest_run(self, strategy: str, target_trade_date: date | None = None):
def get_latest_run(
self,
strategy: str,
target_trade_date: date | None = None,
*,
query: SelectionResultQuery | None = None,
):
return None