"""策略 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)