- StrategyDefinition:universe/factors/selection/rebalance/costs/portfolio +
price_adjustment(除 period 外完整策略定义);to_research_spec(period) 展开为标准 Spec
- strategy 表(migration e1f2a3b4c5d6,MySQL 已应用;name 唯一)+ StrategyRepository
- /api/strategies:POST/GET/DELETE + POST /{id}/expand(period+initial_capital → ResearchSpec)
- tests/test_strategies.py(repo CRUD/同名/expand、API CRUD/400/404);全量 pytest 通过
81 lines
2.8 KiB
Python
81 lines
2.8 KiB
Python
"""策略 API(M8.3):/api/strategies CRUD + 展开为 ResearchSpec。
|
||
|
||
POST /api/strategies 保存策略(name 唯一)
|
||
GET /api/strategies 列表
|
||
GET /api/strategies/{id}
|
||
DELETE /api/strategies/{id}
|
||
POST /api/strategies/{id}/expand body: {period:[start,end], initial_capital?} → ResearchSpec
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
from datetime import date
|
||
|
||
from fastapi import APIRouter, HTTPException
|
||
from pydantic import BaseModel, Field
|
||
|
||
from app.api.deps import DbSession, StrategyRepoDep
|
||
from app.application.services.job_executor import new_id
|
||
from app.domain.entities.research import ResearchSpec
|
||
from app.domain.entities.strategy import StrategyDefinition
|
||
|
||
router = APIRouter(prefix="/strategies", tags=["strategies"])
|
||
|
||
|
||
class ExpandRequest(BaseModel):
|
||
period: tuple[date, date]
|
||
initial_capital: float = Field(default=1_000_000.0, gt=0)
|
||
|
||
|
||
@router.post("", response_model=StrategyDefinition, summary="保存策略")
|
||
def create_strategy(
|
||
definition: StrategyDefinition,
|
||
strategy_repo: StrategyRepoDep,
|
||
session: DbSession,
|
||
) -> StrategyDefinition:
|
||
try:
|
||
saved = strategy_repo.save(definition.model_copy(update={"id": new_id("STG")}))
|
||
session.commit()
|
||
return saved
|
||
except ValueError as exc:
|
||
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
||
|
||
|
||
@router.get("", response_model=list[StrategyDefinition], summary="策略列表")
|
||
def list_strategies(strategy_repo: StrategyRepoDep) -> list[StrategyDefinition]:
|
||
return strategy_repo.list()
|
||
|
||
|
||
@router.get("/{strategy_id}", response_model=StrategyDefinition, summary="读取策略")
|
||
def get_strategy(strategy_id: str, strategy_repo: StrategyRepoDep) -> StrategyDefinition:
|
||
row = strategy_repo.get(strategy_id)
|
||
if row is None:
|
||
raise HTTPException(status_code=404, detail=f"策略 {strategy_id} 不存在")
|
||
return row
|
||
|
||
|
||
@router.delete("/{strategy_id}", summary="删除策略")
|
||
def delete_strategy(
|
||
strategy_id: str,
|
||
strategy_repo: StrategyRepoDep,
|
||
session: DbSession,
|
||
) -> dict:
|
||
if not strategy_repo.delete(strategy_id):
|
||
raise HTTPException(status_code=404, detail=f"策略 {strategy_id} 不存在")
|
||
session.commit()
|
||
return {"deleted": strategy_id}
|
||
|
||
|
||
@router.post("/{strategy_id}/expand", response_model=ResearchSpec, summary="展开为研究 Spec")
|
||
def expand_strategy(
|
||
strategy_id: str,
|
||
req: ExpandRequest,
|
||
strategy_repo: StrategyRepoDep,
|
||
) -> ResearchSpec:
|
||
row = strategy_repo.get(strategy_id)
|
||
if row is None:
|
||
raise HTTPException(status_code=404, detail=f"策略 {strategy_id} 不存在")
|
||
if req.period[0] >= req.period[1]:
|
||
raise HTTPException(status_code=400, detail="period 必须满足 start < end")
|
||
return row.to_research_spec(period=req.period, initial_capital=req.initial_capital)
|