"""选股 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.chart import SelectionHit 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_by_symbol(self, symbol: str, limit: int = 50) -> list[SelectionHit]: rows = self._session.execute( select(SelectionResultModel, SelectionSnapshotModel.as_of, SelectionSnapshotModel.method) .join(SelectionSnapshotModel, SelectionSnapshotModel.id == SelectionResultModel.selection_id) .where(SelectionResultModel.symbol == symbol) .order_by(SelectionSnapshotModel.as_of.desc(), SelectionResultModel.rank) .limit(limit) ).all() return [ SelectionHit( selection_id=r[0].selection_id, as_of=r[1], method=r[2], symbol=r[0].symbol, rank=r[0].rank, score=float(r[0].score), selection_reason=json.loads(r[0].reason_json or "[]"), ) for r in rows ] 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