feat(composite): M7.2b 因子组合落库 + /api/composites CRUD
- factor_composite 表(migration c3e9a0d1f4b5,MySQL 已应用;name 唯一) - CompositeDefinition/Component 实体 + CompositeRepository Protocol + SQLAlchemy 实现 - /api/composites:POST(注册表自动填充组件 direction;未注册因子 400)、GET 列表/详情、DELETE - tests/test_composites_api.py(repo CRUD/同名拒绝/删除、API 方向填充/404/400);全量 pytest 通过
This commit is contained in:
@@ -0,0 +1,69 @@
|
||||
"""因子组合 API(M7.2b):/api/composites CRUD。
|
||||
|
||||
组合 = 可复用因子权重集;计算时方向取因子注册表,落库冗余快照。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from fastapi import APIRouter, HTTPException
|
||||
|
||||
from app.api.deps import CompositeRepoDep, DbSession
|
||||
from app.application.services.job_executor import new_id
|
||||
from app.domain.entities.composite import CompositeComponent, CompositeDefinition
|
||||
from app.quant.factors import FactorError, get_factor
|
||||
|
||||
router = APIRouter(prefix="/composites", tags=["composites"])
|
||||
|
||||
|
||||
def _fill_direction(definition: CompositeDefinition) -> CompositeDefinition:
|
||||
"""以因子注册表元数据补齐/校正组件 direction(登记但不可计算的因子报错)。"""
|
||||
out = []
|
||||
for c in definition.components:
|
||||
try:
|
||||
defn, _fn = get_factor(c.name)
|
||||
except FactorError as exc:
|
||||
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
||||
out.append(CompositeComponent(name=c.name, weight=c.weight, direction=defn.direction))
|
||||
return definition.model_copy(update={"components": out})
|
||||
|
||||
|
||||
@router.post("", summary="保存因子组合", response_model=CompositeDefinition)
|
||||
def create_composite(
|
||||
definition: CompositeDefinition,
|
||||
composite_repo: CompositeRepoDep,
|
||||
session: DbSession,
|
||||
) -> CompositeDefinition:
|
||||
prepared = _fill_direction(definition.model_copy(update={"id": ""}))
|
||||
try:
|
||||
saved = composite_repo.save(
|
||||
prepared.model_copy(update={"id": new_id("CF")})
|
||||
)
|
||||
session.commit()
|
||||
return saved
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
||||
|
||||
|
||||
@router.get("", summary="因子组合列表", response_model=list[CompositeDefinition])
|
||||
def list_composites(composite_repo: CompositeRepoDep) -> list[CompositeDefinition]:
|
||||
return composite_repo.list()
|
||||
|
||||
|
||||
@router.get("/{composite_id}", summary="读取因子组合", response_model=CompositeDefinition)
|
||||
def get_composite(composite_id: str, composite_repo: CompositeRepoDep) -> CompositeDefinition:
|
||||
row = composite_repo.get(composite_id)
|
||||
if row is None:
|
||||
raise HTTPException(status_code=404, detail=f"组合 {composite_id} 不存在")
|
||||
return row
|
||||
|
||||
|
||||
@router.delete("/{composite_id}", summary="删除因子组合")
|
||||
def delete_composite(
|
||||
composite_id: str,
|
||||
composite_repo: CompositeRepoDep,
|
||||
session: DbSession,
|
||||
) -> dict:
|
||||
if not composite_repo.delete(composite_id):
|
||||
raise HTTPException(status_code=404, detail=f"组合 {composite_id} 不存在")
|
||||
session.commit()
|
||||
return {"deleted": composite_id}
|
||||
@@ -11,6 +11,7 @@ from fastapi import Depends
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.application.services.selection_service import SelectionService
|
||||
from app.domain.repositories.composite import CompositeRepository
|
||||
from app.domain.repositories.factor import FactorRepository
|
||||
from app.domain.repositories.jobs import ExperimentRepository, JobRepository
|
||||
from app.domain.repositories.market import (
|
||||
@@ -19,6 +20,9 @@ from app.domain.repositories.market import (
|
||||
StockRepository,
|
||||
)
|
||||
from app.domain.repositories.selection import SelectionRepository
|
||||
from app.infrastructure.persistence.sqlalchemy.repositories.composite_impl import (
|
||||
SqlAlchemyCompositeRepository,
|
||||
)
|
||||
from app.infrastructure.persistence.sqlalchemy.repositories.factor_impl import (
|
||||
SqlAlchemyFactorRepository,
|
||||
)
|
||||
@@ -78,6 +82,10 @@ def _factor_repo_factory(session: DbSession) -> FactorRepository:
|
||||
return SqlAlchemyFactorRepository(session)
|
||||
|
||||
|
||||
def _composite_repo_factory(session: DbSession) -> CompositeRepository:
|
||||
return SqlAlchemyCompositeRepository(session)
|
||||
|
||||
|
||||
StockRepoDep = Annotated[StockRepository, Depends(_stock_repo_factory)]
|
||||
DailyRepoDep = Annotated[DailyBarRepository, Depends(_daily_repo_factory)]
|
||||
EngineDep = Annotated[QuantEngine, Depends(_engine_factory)]
|
||||
@@ -85,6 +93,7 @@ ResearchServiceDep = Annotated[ResearchService, Depends(_service_factory)]
|
||||
SelectionServiceDep = Annotated[SelectionService, Depends(_selection_service_factory)]
|
||||
SelectionRepoDep = Annotated[SelectionRepository, Depends(_selection_repo_factory)]
|
||||
FactorRepoDep = Annotated[FactorRepository, Depends(_factor_repo_factory)]
|
||||
CompositeRepoDep = Annotated[CompositeRepository, Depends(_composite_repo_factory)]
|
||||
|
||||
|
||||
def _job_repo_factory(session: DbSession):
|
||||
|
||||
@@ -8,12 +8,23 @@ from __future__ import annotations
|
||||
|
||||
from fastapi import APIRouter
|
||||
|
||||
from app.api import agent, experiments, factors, health, jobs, research, selections, stocks
|
||||
from app.api import (
|
||||
agent,
|
||||
composites,
|
||||
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(composites.router)
|
||||
api_router.include_router(research.router)
|
||||
api_router.include_router(selections.router)
|
||||
api_router.include_router(jobs.router)
|
||||
|
||||
Reference in New Issue
Block a user