- 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 通过
70 lines
2.6 KiB
Python
70 lines
2.6 KiB
Python
"""因子组合 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}
|