feat(selection): 支持选股结果服务端分页
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user