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:
Simon
2026-09-09 00:38:09 +08:00
parent 692bdb3be5
commit 9d25d466e5
10 changed files with 436 additions and 0 deletions
+53
View File
@@ -0,0 +1,53 @@
"""策略领域实体(M8.3,v2 §17/§5.3)。
Strategy = 完整策略定义(universe + factors + selection + rebalance + costs +
portfolio,除回测区间 period 外),保存为命名资产;回测时补 period 展开为
ResearchSpec(v2 §18 Research Specification 为统一契约,策略是其持久化形态)。
"""
from __future__ import annotations
from datetime import date, datetime
from pydantic import BaseModel, Field
from app.domain.entities.research import (
CostSpec,
FactorSpec,
PortfolioSpec,
SelectionSpec,
UniverseSpec,
)
class StrategyDefinition(BaseModel):
id: str = ""
name: str = Field(min_length=1, max_length=64)
description: str = ""
spec_type: str = Field(default="backtest", pattern="^(backtest|factor_test)$")
universe: UniverseSpec = UniverseSpec()
price_adjustment: str = Field(default="none", pattern="^(none|qfq)$")
factors: list[FactorSpec] = Field(min_length=1)
selection: SelectionSpec = SelectionSpec()
rebalance: str = Field(default="monthly", pattern="^(weekly|monthly)$")
costs: CostSpec = CostSpec()
portfolio: PortfolioSpec = PortfolioSpec()
version: str = "1"
created_at: datetime | None = None
def to_research_spec(self, period: tuple[date, date], initial_capital: float | None = None):
"""补全回测区间/资金后展开为标准 ResearchSpec(可在 Job/回测执行)。"""
from app.domain.entities.research import ResearchSpec
return ResearchSpec(
type=self.spec_type,
universe=self.universe,
price_adjustment=self.price_adjustment,
factors=self.factors,
selection=self.selection,
rebalance=self.rebalance,
period=period,
costs=self.costs,
portfolio=self.portfolio,
initial_capital=initial_capital if initial_capital else 1_000_000.0,
)