feat(selection): 支持选股结果服务端分页
This commit is contained in:
+68
-9
@@ -18,6 +18,7 @@ from ..domain.models import SelectionSignal, ZhixingB1Category
|
||||
from ..domain.runs import (
|
||||
SelectionExecutionSource,
|
||||
SelectionRerunRequired,
|
||||
SelectionResultQuery,
|
||||
SelectionRun,
|
||||
SelectionRunError,
|
||||
SelectionRunInProgress,
|
||||
@@ -29,6 +30,19 @@ from ..domain.runs import (
|
||||
from ..domain.zhixing_b1 import ZHIXING_B1_SIGNAL_ORDER
|
||||
|
||||
_SIGNAL_PRIORITY = {category: index for index, category in enumerate(ZHIXING_B1_SIGNAL_ORDER)}
|
||||
_CATEGORY_PREFIXES = {
|
||||
"pullback": "zhixing_b1_pullback_",
|
||||
"oversold": "zhixing_b1_oversold_",
|
||||
"original": "zhixing_b1_original_b1",
|
||||
}
|
||||
_SIGNAL_ORDER_SQL = (
|
||||
"CASE category "
|
||||
+ " ".join(
|
||||
f"WHEN '{category.value}' THEN {index}"
|
||||
for index, category in enumerate(ZHIXING_B1_SIGNAL_ORDER)
|
||||
)
|
||||
+ f" ELSE {len(ZHIXING_B1_SIGNAL_ORDER)} END"
|
||||
)
|
||||
|
||||
|
||||
class PostgresSelectionRunRepository(SelectionRunStore):
|
||||
@@ -224,12 +238,17 @@ class PostgresSelectionRunRepository(SelectionRunStore):
|
||||
except psycopg.Error as exc:
|
||||
raise SelectionRunStoreError(f"failed to finish selection run {run_id}") from exc
|
||||
|
||||
def get_run(self, run_id: str) -> SelectionRun | None:
|
||||
"""Read one run with its item failures and signal details."""
|
||||
def get_run(
|
||||
self,
|
||||
run_id: str,
|
||||
*,
|
||||
query: SelectionResultQuery | None = None,
|
||||
) -> SelectionRun | None:
|
||||
"""Read one run with filtered, paged signals and item failures."""
|
||||
|
||||
try:
|
||||
with self._connection() as connection:
|
||||
return self._load_run(connection, run_id)
|
||||
return self._load_run(connection, run_id, query or SelectionResultQuery())
|
||||
except psycopg.Error as exc:
|
||||
raise SelectionRunStoreError(f"failed to load selection run {run_id}") from exc
|
||||
|
||||
@@ -237,6 +256,8 @@ class PostgresSelectionRunRepository(SelectionRunStore):
|
||||
self,
|
||||
strategy: Literal["zhixing_b1"],
|
||||
target_trade_date: date | None = None,
|
||||
*,
|
||||
query: SelectionResultQuery | None = None,
|
||||
) -> SelectionRun | None:
|
||||
"""Read the current run for a date or the latest date for a strategy."""
|
||||
|
||||
@@ -263,12 +284,20 @@ class PostgresSelectionRunRepository(SelectionRunStore):
|
||||
""",
|
||||
(strategy, target_trade_date),
|
||||
).fetchone()
|
||||
return self._load_run(connection, str(row[0])) if row else None
|
||||
return (
|
||||
self._load_run(connection, str(row[0]), query or SelectionResultQuery())
|
||||
if row
|
||||
else None
|
||||
)
|
||||
except psycopg.Error as exc:
|
||||
raise SelectionRunStoreError("failed to load latest selection run") from exc
|
||||
|
||||
@staticmethod
|
||||
def _load_run(connection: Any, run_id: str) -> SelectionRun | None:
|
||||
def _load_run(
|
||||
connection: Any,
|
||||
run_id: str,
|
||||
query: SelectionResultQuery,
|
||||
) -> SelectionRun | None:
|
||||
row = connection.execute(
|
||||
"""
|
||||
SELECT
|
||||
@@ -292,15 +321,23 @@ class PostgresSelectionRunRepository(SelectionRunStore):
|
||||
""",
|
||||
(run_id,),
|
||||
).fetchall()
|
||||
signal_filter, signal_parameters = _signal_filter(query, run_id)
|
||||
signal_total_row = connection.execute(
|
||||
f"SELECT COUNT(*) FROM selection_signal WHERE {signal_filter}",
|
||||
tuple(signal_parameters),
|
||||
).fetchone()
|
||||
signal_total = int(signal_total_row[0] or 0) if signal_total_row else 0
|
||||
offset = (query.page - 1) * query.page_size
|
||||
signal_rows = connection.execute(
|
||||
"""
|
||||
f"""
|
||||
SELECT
|
||||
ts_code, name, target_trade_date, strategy, category, close, details
|
||||
FROM selection_signal
|
||||
WHERE run_id = %s
|
||||
ORDER BY ts_code, category
|
||||
WHERE {signal_filter}
|
||||
ORDER BY ts_code, {_SIGNAL_ORDER_SQL}
|
||||
LIMIT %s OFFSET %s
|
||||
""",
|
||||
(run_id,),
|
||||
tuple((*signal_parameters, query.page_size, offset)),
|
||||
).fetchall()
|
||||
signals = tuple(
|
||||
sorted(
|
||||
@@ -353,6 +390,7 @@ class PostgresSelectionRunRepository(SelectionRunStore):
|
||||
finished_at=cast(datetime | None, row[15]),
|
||||
items=items,
|
||||
signals=signals,
|
||||
signals_total=signal_total,
|
||||
)
|
||||
|
||||
@contextmanager
|
||||
@@ -380,6 +418,27 @@ def _signal_from_row(row: tuple[object, ...]) -> SelectionSignal:
|
||||
)
|
||||
|
||||
|
||||
def _signal_filter(query: SelectionResultQuery, run_id: str) -> tuple[str, list[object]]:
|
||||
"""Build the parameterized WHERE clause shared by count and page reads."""
|
||||
|
||||
clauses = ["run_id = %s"]
|
||||
parameters: list[object] = [run_id]
|
||||
if query.search:
|
||||
pattern = f"%{_escape_like(query.search)}%"
|
||||
clauses.append("(name ILIKE %s ESCAPE '\\' OR ts_code ILIKE %s ESCAPE '\\')")
|
||||
parameters.extend((pattern, pattern))
|
||||
if query.category:
|
||||
clauses.append("category LIKE %s")
|
||||
parameters.append(f"{_CATEGORY_PREFIXES[query.category]}%")
|
||||
return " AND ".join(clauses), parameters
|
||||
|
||||
|
||||
def _escape_like(value: str) -> str:
|
||||
"""Escape user wildcards before placing text inside a SQL LIKE pattern."""
|
||||
|
||||
return value.replace("\\", "\\\\").replace("%", "\\%").replace("_", "\\_")
|
||||
|
||||
|
||||
def _details(value: object) -> dict[str, float | str | None]:
|
||||
"""Normalize JSONB details into the domain's scalar-only mapping."""
|
||||
|
||||
|
||||
Reference in New Issue
Block a user