Files
qlib/backend/app/infrastructure/persistence/sqlalchemy/repositories/selection_impl.py
T
Simon c60dc78c88 feat(selection): M6.3 选股结果落库 + /api/selections(快照 + 逐候选行)
- 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 通过
2026-09-09 00:20:42 +08:00

113 lines
3.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""选股 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