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:
@@ -10,15 +10,22 @@ from typing import Annotated
|
||||
from fastapi import Depends
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.application.services.selection_service import SelectionService
|
||||
from app.domain.repositories.jobs import ExperimentRepository, JobRepository
|
||||
from app.domain.repositories.market import (
|
||||
DailyBarRepository,
|
||||
FinancialRepository,
|
||||
StockRepository,
|
||||
)
|
||||
from app.domain.repositories.selection import SelectionRepository
|
||||
from app.infrastructure.persistence.sqlalchemy.repositories.market_impl import (
|
||||
SqlAlchemyDailyBarRepository,
|
||||
SqlAlchemyFinancialRepository,
|
||||
SqlAlchemyStockRepository,
|
||||
)
|
||||
from app.infrastructure.persistence.sqlalchemy.repositories.selection_impl import (
|
||||
SqlAlchemySelectionRepository,
|
||||
)
|
||||
from app.infrastructure.persistence.sqlalchemy.session import get_session
|
||||
from app.quant.engine import LocalEngine, QuantEngine
|
||||
from app.quant.service import ResearchService
|
||||
@@ -34,6 +41,10 @@ def _daily_repo_factory(session: DbSession) -> DailyBarRepository:
|
||||
return SqlAlchemyDailyBarRepository(session)
|
||||
|
||||
|
||||
def _financial_repo_factory(session: DbSession) -> FinancialRepository:
|
||||
return SqlAlchemyFinancialRepository(session)
|
||||
|
||||
|
||||
def _engine_factory() -> QuantEngine:
|
||||
return LocalEngine()
|
||||
|
||||
@@ -46,10 +57,25 @@ def _service_factory(
|
||||
return ResearchService(stock_repo, daily_repo, engine)
|
||||
|
||||
|
||||
def _selection_service_factory(
|
||||
stock_repo: Annotated[StockRepository, Depends(_stock_repo_factory)],
|
||||
daily_repo: Annotated[DailyBarRepository, Depends(_daily_repo_factory)],
|
||||
financial_repo: Annotated[FinancialRepository, Depends(_financial_repo_factory)],
|
||||
) -> SelectionService:
|
||||
|
||||
return SelectionService(stock_repo, daily_repo, financial_repo)
|
||||
|
||||
|
||||
def _selection_repo_factory(session: DbSession) -> SelectionRepository:
|
||||
return SqlAlchemySelectionRepository(session)
|
||||
|
||||
|
||||
StockRepoDep = Annotated[StockRepository, Depends(_stock_repo_factory)]
|
||||
DailyRepoDep = Annotated[DailyBarRepository, Depends(_daily_repo_factory)]
|
||||
EngineDep = Annotated[QuantEngine, Depends(_engine_factory)]
|
||||
ResearchServiceDep = Annotated[ResearchService, Depends(_service_factory)]
|
||||
SelectionServiceDep = Annotated[SelectionService, Depends(_selection_service_factory)]
|
||||
SelectionRepoDep = Annotated[SelectionRepository, Depends(_selection_repo_factory)]
|
||||
|
||||
|
||||
def _job_repo_factory(session: DbSession):
|
||||
|
||||
@@ -8,13 +8,14 @@ from __future__ import annotations
|
||||
|
||||
from fastapi import APIRouter
|
||||
|
||||
from app.api import agent, experiments, factors, health, jobs, research, stocks
|
||||
from app.api import agent, experiments, factors, health, jobs, research, selections, stocks
|
||||
|
||||
api_router = APIRouter()
|
||||
api_router.include_router(health.router)
|
||||
api_router.include_router(stocks.router)
|
||||
api_router.include_router(factors.router)
|
||||
api_router.include_router(research.router)
|
||||
api_router.include_router(selections.router)
|
||||
api_router.include_router(jobs.router)
|
||||
api_router.include_router(experiments.router)
|
||||
api_router.include_router(agent.router)
|
||||
|
||||
@@ -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