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 通过
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
"""选股 API(M6.3):提交/查询选股,结果落库可复现。
|
||||
|
||||
POST /api/selections 同步执行一次选股并落库 → {selection_id, result}
|
||||
GET /api/selections/{id} 读回某次选股完整结果
|
||||
GET /api/selections 历史选股元数据(可过滤 as_of/method)
|
||||
|
||||
同步执行:单日全市场因子评分/条件计算量轻(秒级);未来若超时再迁 Job。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import date
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Query
|
||||
from pydantic import BaseModel
|
||||
|
||||
from app.api.deps import (
|
||||
DbSession,
|
||||
SelectionRepoDep,
|
||||
SelectionServiceDep,
|
||||
)
|
||||
from app.application.services.job_executor import new_id
|
||||
from app.domain.entities.selection import SelectionMeta, SelectionQuery, SelectionResult
|
||||
|
||||
router = APIRouter(prefix="/selections", tags=["selections"])
|
||||
|
||||
|
||||
class SelectionRun(BaseModel):
|
||||
selection_id: str
|
||||
result: SelectionResult
|
||||
|
||||
|
||||
@router.post("", response_model=SelectionRun, summary="执行一次选股(同步)并落库")
|
||||
def run_selection(
|
||||
query: SelectionQuery,
|
||||
service: SelectionServiceDep,
|
||||
selection_repo: SelectionRepoDep,
|
||||
session: DbSession,
|
||||
) -> SelectionRun:
|
||||
result = service.select(query)
|
||||
selection_id = new_id("SEL")
|
||||
selection_repo.save(selection_id, result)
|
||||
session.commit()
|
||||
return SelectionRun(selection_id=selection_id, result=result)
|
||||
|
||||
|
||||
@router.get("/{selection_id}", response_model=SelectionResult, summary="读回一次选股结果")
|
||||
def get_selection(
|
||||
selection_id: str,
|
||||
selection_repo: SelectionRepoDep,
|
||||
) -> SelectionResult:
|
||||
result = selection_repo.get(selection_id)
|
||||
if result is None:
|
||||
raise HTTPException(status_code=404, detail=f"选股记录 {selection_id} 不存在")
|
||||
return result
|
||||
|
||||
|
||||
_AsOfQuery = Annotated[date | None, Query(description="按选股时点过滤")]
|
||||
_MethodQuery = Annotated[str | None, Query(pattern="^(score|condition)$")]
|
||||
_LimitQuery = Annotated[int, Query(ge=1, le=200)]
|
||||
|
||||
|
||||
@router.get("", response_model=list[SelectionMeta], summary="历史选股元数据列表")
|
||||
def list_selections(
|
||||
selection_repo: SelectionRepoDep,
|
||||
as_of: _AsOfQuery = None,
|
||||
method: _MethodQuery = None,
|
||||
limit: _LimitQuery = 20,
|
||||
) -> list[SelectionMeta]:
|
||||
return selection_repo.list_recent(as_of=as_of, method=method, limit=limit)
|
||||
Reference in New Issue
Block a user