- domain/repositories/selection.py + SelectionMeta:选股 Repository Protocol
- models/selection.py + migration a6c91d4e7f20:selection_snapshot(查询/统计快照)+
selection_result(逐候选:rank/score/factor_values/filter_status/reason JSON)
- repositories/selection_impl.py:save/get/list_recent(读回重建 SelectionResult)
- api/selections.py:POST /api/selections(同步执行+落库,返回 selection_id+result)、
GET /{id}、GET 列表(as_of/method 过滤);deps 装配 SelectionService/Repo
- config._build_mysql_url:仅编码破坏 URL 结构的字符(修 Alembic configparser 遇 %21 崩)
- 迁移已在 MySQL 应用(alembic head a6c91d4e7f20);tests/test_selections_api.py 4 例
提交→读回一致/404/列表过滤/condition;全量 pytest 通过
113 lines
3.9 KiB
Python
113 lines
3.9 KiB
Python
"""选股 Repository 的 SQLAlchemy 实现(M6.3)。
|
||
|
||
save:snapshot + 候选逐行(同 session,由调用方 commit);
|
||
get:读回并重建 SelectionResult;list_recent:历史元数据。
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import json
|
||
from datetime import date, datetime
|
||
|
||
from sqlalchemy import select
|
||
from sqlalchemy.orm import Session
|
||
|
||
from app.domain.entities.selection import (
|
||
SelectionCandidate,
|
||
SelectionMeta,
|
||
SelectionResult,
|
||
SelectionStatistics,
|
||
)
|
||
from app.infrastructure.persistence.sqlalchemy.models.selection import (
|
||
SelectionResultModel,
|
||
SelectionSnapshotModel,
|
||
)
|
||
|
||
|
||
class SqlAlchemySelectionRepository:
|
||
def __init__(self, session: Session) -> None:
|
||
self._session = session
|
||
|
||
def save(self, selection_id: str, result: SelectionResult) -> None:
|
||
self._session.add(
|
||
SelectionSnapshotModel(
|
||
id=selection_id,
|
||
as_of=result.as_of_date,
|
||
method=result.method,
|
||
query_json=json.dumps(result.config_snapshot, ensure_ascii=False),
|
||
statistics_json=json.dumps(result.statistics.model_dump(mode="json")),
|
||
created_at=datetime.now(),
|
||
)
|
||
)
|
||
now = datetime.now()
|
||
for c in result.candidates:
|
||
self._session.add(
|
||
SelectionResultModel(
|
||
selection_id=selection_id,
|
||
symbol=c.symbol,
|
||
rank=c.rank,
|
||
score=c.score,
|
||
factor_values_json=json.dumps(c.factor_values, ensure_ascii=False),
|
||
filter_status_json=json.dumps(c.filter_status, ensure_ascii=False),
|
||
reason_json=json.dumps(c.selection_reason, ensure_ascii=False),
|
||
created_at=now,
|
||
)
|
||
)
|
||
self._session.flush()
|
||
|
||
def get(self, selection_id: str) -> SelectionResult | None:
|
||
snap = self._session.get(SelectionSnapshotModel, selection_id)
|
||
if snap is None:
|
||
return None
|
||
rows = self._session.scalars(
|
||
select(SelectionResultModel)
|
||
.where(SelectionResultModel.selection_id == selection_id)
|
||
.order_by(SelectionResultModel.rank)
|
||
).all()
|
||
stats = SelectionStatistics.model_validate_json(snap.statistics_json)
|
||
candidates = [
|
||
SelectionCandidate(
|
||
symbol=r.symbol,
|
||
rank=r.rank,
|
||
score=float(r.score),
|
||
factor_values=json.loads(r.factor_values_json or "{}"),
|
||
filter_status=json.loads(r.filter_status_json or "[]"),
|
||
selection_reason=json.loads(r.reason_json or "[]"),
|
||
)
|
||
for r in rows
|
||
]
|
||
return SelectionResult(
|
||
as_of_date=snap.as_of,
|
||
method=snap.method,
|
||
statistics=stats,
|
||
candidates=candidates,
|
||
config_snapshot=json.loads(snap.query_json),
|
||
)
|
||
|
||
def list_recent(
|
||
self,
|
||
as_of: date | None = None,
|
||
method: str | None = None,
|
||
limit: int = 20,
|
||
) -> list[SelectionMeta]:
|
||
stmt = select(SelectionSnapshotModel).order_by(SelectionSnapshotModel.created_at.desc())
|
||
if as_of is not None:
|
||
stmt = stmt.where(SelectionSnapshotModel.as_of == as_of)
|
||
if method is not None:
|
||
stmt = stmt.where(SelectionSnapshotModel.method == method)
|
||
stmt = stmt.limit(limit)
|
||
metas: list[SelectionMeta] = []
|
||
for snap in self._session.scalars(stmt).all():
|
||
stats = SelectionStatistics.model_validate_json(snap.statistics_json)
|
||
metas.append(
|
||
SelectionMeta(
|
||
id=snap.id,
|
||
as_of=snap.as_of,
|
||
method=snap.method,
|
||
universe_size=stats.universe_size,
|
||
selected=stats.selected,
|
||
created_at=snap.created_at,
|
||
)
|
||
)
|
||
return metas
|