feat(strategy): M8.3 策略模型 + /api/strategies(命名配置资产,可展开为 ResearchSpec)
- 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 通过
This commit is contained in:
@@ -22,6 +22,7 @@ from app.domain.repositories.market import (
|
||||
)
|
||||
from app.domain.repositories.selection import SelectionRepository
|
||||
from app.domain.repositories.signal import SignalRepository
|
||||
from app.domain.repositories.strategy import StrategyRepository
|
||||
from app.infrastructure.persistence.sqlalchemy.repositories.composite_impl import (
|
||||
SqlAlchemyCompositeRepository,
|
||||
)
|
||||
@@ -39,6 +40,9 @@ from app.infrastructure.persistence.sqlalchemy.repositories.selection_impl impor
|
||||
from app.infrastructure.persistence.sqlalchemy.repositories.signal_impl import (
|
||||
SqlAlchemySignalRepository,
|
||||
)
|
||||
from app.infrastructure.persistence.sqlalchemy.repositories.strategy_impl import (
|
||||
SqlAlchemyStrategyRepository,
|
||||
)
|
||||
from app.infrastructure.persistence.sqlalchemy.session import get_session
|
||||
from app.quant.engine import LocalEngine, QuantEngine
|
||||
from app.quant.service import ResearchService
|
||||
@@ -102,6 +106,10 @@ def _signal_repo_factory(session: DbSession) -> SignalRepository:
|
||||
return SqlAlchemySignalRepository(session)
|
||||
|
||||
|
||||
def _strategy_repo_factory(session: DbSession) -> StrategyRepository:
|
||||
return SqlAlchemyStrategyRepository(session)
|
||||
|
||||
|
||||
StockRepoDep = Annotated[StockRepository, Depends(_stock_repo_factory)]
|
||||
DailyRepoDep = Annotated[DailyBarRepository, Depends(_daily_repo_factory)]
|
||||
EngineDep = Annotated[QuantEngine, Depends(_engine_factory)]
|
||||
@@ -112,6 +120,7 @@ FactorRepoDep = Annotated[FactorRepository, Depends(_factor_repo_factory)]
|
||||
CompositeRepoDep = Annotated[CompositeRepository, Depends(_composite_repo_factory)]
|
||||
SignalRepoDep = Annotated[SignalRepository, Depends(_signal_repo_factory)]
|
||||
SignalServiceDep = Annotated[SignalService, Depends(_signal_service_factory)]
|
||||
StrategyRepoDep = Annotated[StrategyRepository, Depends(_strategy_repo_factory)]
|
||||
|
||||
|
||||
def _job_repo_factory(session: DbSession):
|
||||
|
||||
@@ -19,6 +19,7 @@ from app.api import (
|
||||
selections,
|
||||
signals,
|
||||
stocks,
|
||||
strategies,
|
||||
)
|
||||
|
||||
api_router = APIRouter()
|
||||
@@ -29,6 +30,7 @@ api_router.include_router(composites.router)
|
||||
api_router.include_router(research.router)
|
||||
api_router.include_router(selections.router)
|
||||
api_router.include_router(signals.router)
|
||||
api_router.include_router(strategies.router)
|
||||
api_router.include_router(jobs.router)
|
||||
api_router.include_router(experiments.router)
|
||||
api_router.include_router(agent.router)
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
"""策略 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)
|
||||
Reference in New Issue
Block a user