- quant/universe.py:filter_stocks 从 quant/service 迁出并集中(ST/上市天数/退市过滤), as_of 当前/历史日语义由 delist/list_date 保证;exclude_suspended 依赖停牌表未建模, 由上层显式标注(选股结果 unimplemented) - research.UniverseSpec 增加 symbols 白名单(非空时仅白名单内参与,再叠加其余过滤) - quant/service re-export filter_stocks(外部引用不变);SelectionService 已共用 - tests/test_universe.py:6 例覆盖当前/历史日、ST、上市天数、退市、白名单;全量 pytest 通过
221 lines
6.5 KiB
Python
221 lines
6.5 KiB
Python
"""研究领域对象:Research Specification、标准化研究结果。
|
||
|
||
原则(AGENT.md §16/§21/§24、ARCHITECTURE §14):
|
||
- 前端 / Agent / 后端统一经 Research Specification 描述任务,禁止直接拼引擎配置
|
||
- 回测结果一律标准化为 BacktestResult;未建模的成本/市场约束显式列在
|
||
unimplemented,禁止默认「无成本 / 永远可成交」假设
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
from datetime import date, datetime
|
||
|
||
from pydantic import BaseModel, Field, field_validator, model_validator
|
||
|
||
# ---------- Research Specification ----------
|
||
|
||
|
||
class UniverseSpec(BaseModel):
|
||
"""股票池口径。MVP:市场 + 过滤条件;指数成分等 Phase 3 扩展。
|
||
|
||
symbols 白名单:非空时仅这些股票参与(再叠加其余过滤);供自选池/测试使用。
|
||
market 目前为预留字段(stock.market 存储主板/创业板/科创板等中文枚举,过滤未启用)。
|
||
"""
|
||
|
||
market: str = Field(default="CN_A", description="CN_A / CN_B / ...(预留)")
|
||
exclude_st: bool = True
|
||
exclude_suspended: bool = True
|
||
min_listing_days: int = Field(default=250, ge=0, description="上市至少 N 个自然日")
|
||
symbols: list[str] = Field(
|
||
default_factory=list,
|
||
description="白名单(可选):非空时仅这些 symbol 参与选股/回测",
|
||
)
|
||
|
||
|
||
class FactorSpec(BaseModel):
|
||
"""引用一个已注册因子并给定权重。"""
|
||
|
||
name: str
|
||
weight: float = Field(default=1.0, gt=0)
|
||
|
||
|
||
class SelectionSpec(BaseModel):
|
||
"""选股方式。MVP:按加权因子得分取 Top N 等权。"""
|
||
|
||
top_n: int = Field(default=30, ge=1, le=1000)
|
||
|
||
|
||
class CostSpec(BaseModel):
|
||
"""交易成本模型(单边比例)。
|
||
|
||
buy = commission + slippage;sell = commission + stamp_tax + slippage。
|
||
"""
|
||
|
||
commission_rate: float = Field(default=0.0003, ge=0, le=0.01)
|
||
stamp_tax_rate: float = Field(default=0.0005, ge=0, le=0.01)
|
||
slippage_rate: float = Field(default=0.001, ge=0, le=0.05)
|
||
benchmark: str = Field(default="000300.SH", description="对照基准指数代码")
|
||
|
||
|
||
class ResearchSpec(BaseModel):
|
||
"""一次研究的完整描述。type 决定执行路径。"""
|
||
|
||
type: str = Field(default="backtest", pattern="^(factor_test|backtest)$")
|
||
universe: UniverseSpec = UniverseSpec()
|
||
factors: list[FactorSpec] = Field(min_length=1)
|
||
selection: SelectionSpec = SelectionSpec()
|
||
rebalance: str = Field(default="monthly", pattern="^(weekly|monthly)$")
|
||
period: tuple[date, date]
|
||
costs: CostSpec = CostSpec()
|
||
initial_capital: float = Field(default=1_000_000.0, gt=0)
|
||
|
||
@field_validator("period")
|
||
@classmethod
|
||
def _period_ordered(cls, period: tuple[date, date]) -> tuple[date, date]:
|
||
if period[0] >= period[1]:
|
||
raise ValueError("period 必须满足 start < end")
|
||
return period
|
||
|
||
@model_validator(mode="after")
|
||
def _no_duplicate_factors(self) -> ResearchSpec:
|
||
names = [f.name for f in self.factors]
|
||
if len(set(names)) != len(names):
|
||
raise ValueError("factors 存在重复因子名")
|
||
return self
|
||
|
||
|
||
# ---------- 回测结果 ----------
|
||
|
||
|
||
class CurvePoint(BaseModel):
|
||
date: date
|
||
value: float
|
||
|
||
|
||
class MonthlyReturn(BaseModel):
|
||
year: int
|
||
month: int
|
||
return_pct: float # 百分数,如 3.2 表示 +3.2%
|
||
|
||
|
||
class YearlyReturn(BaseModel):
|
||
year: int
|
||
return_pct: float
|
||
|
||
|
||
class BacktestSummary(BaseModel):
|
||
start: date
|
||
end: date
|
||
initial_capital: float
|
||
final_equity: float
|
||
total_return_pct: float
|
||
annual_return_pct: float
|
||
sharpe: float
|
||
max_drawdown_pct: float
|
||
volatility_pct: float
|
||
win_rate_pct: float
|
||
total_trades: int
|
||
avg_turnover_pct: float
|
||
benchmark_return_pct: float | None = None
|
||
|
||
|
||
class Trade(BaseModel):
|
||
entry_date: date
|
||
exit_date: date
|
||
symbol: str
|
||
entry_price: float
|
||
exit_price: float
|
||
return_pct: float
|
||
|
||
|
||
class Position(BaseModel):
|
||
date: date
|
||
symbol: str
|
||
weight: float
|
||
|
||
|
||
class BacktestResult(BaseModel):
|
||
"""标准化回测结果(ARCHITECTURE §14)。前端只依赖该结构。"""
|
||
|
||
summary: BacktestSummary
|
||
equity_curve: list[CurvePoint]
|
||
drawdown: list[CurvePoint]
|
||
monthly_returns: list[MonthlyReturn]
|
||
yearly_returns: list[YearlyReturn]
|
||
positions: list[Position]
|
||
trades: list[Trade]
|
||
turnover_pct: float
|
||
unimplemented: list[str] = Field(
|
||
default_factory=list,
|
||
description="本结果中未建模的约束(AGENT §24:必须显式标注,禁止假装支持)",
|
||
)
|
||
config_snapshot: dict = Field(default_factory=dict, description="复现用完整配置快照")
|
||
|
||
|
||
# ---------- 因子测试结果 ----------
|
||
|
||
|
||
class QuantileReturn(BaseModel):
|
||
"""分层收益:按因子值升序分 N 层后各层等权组合的区间收益。"""
|
||
|
||
quantile: int
|
||
return_pct: float
|
||
|
||
|
||
class FactorTestReport(BaseModel):
|
||
factor_name: str
|
||
ic_mean: float
|
||
icir: float
|
||
rank_ic_mean: float
|
||
positive_ratio_pct: float
|
||
quantile_returns: list[QuantileReturn]
|
||
spread_quantile: int | None = Field(
|
||
default=None, description="分层价差 = 最高层收益 - 最低层收益(若多头/空头语义适用)"
|
||
)
|
||
sample_days: int
|
||
unimplemented: list[str] = Field(default_factory=list)
|
||
config_snapshot: dict = Field(default_factory=dict)
|
||
|
||
|
||
# ---------- 异步 Job 与 Experiment(Phase 4) ----------
|
||
|
||
|
||
class JobStatus(str):
|
||
"""统一状态机(AGENT.md §20):queued→running→(success|failed|cancelled)。"""
|
||
|
||
QUEUED = "queued"
|
||
RUNNING = "running"
|
||
SUCCESS = "success"
|
||
FAILED = "failed"
|
||
CANCELLED = "cancelled"
|
||
|
||
|
||
class JobRecord(BaseModel):
|
||
"""一次异步研究任务。spec/result 以 JSON 文本存储(保持 Schema 演进自由)。"""
|
||
|
||
id: str
|
||
kind: str # factor_test | backtest
|
||
status: str = JobStatus.QUEUED
|
||
stage: str | None = None
|
||
spec_json: str
|
||
error: str | None = None
|
||
result_json: str | None = None
|
||
experiment_id: str | None = None
|
||
created_at: datetime | None = None
|
||
started_at: datetime | None = None
|
||
finished_at: datetime | None = None
|
||
|
||
|
||
class ExperimentRecord(BaseModel):
|
||
"""一次研究的可复现存档(AGENT.md §21)。"""
|
||
|
||
id: str
|
||
kind: str # factor_test | backtest
|
||
spec_json: str
|
||
result_json: str
|
||
summary_text: str | None = None # 便于列表展示的摘要(如 total_return_pct)
|
||
code_version: str | None = None # git commit / 代码指纹
|
||
data_version: str | None = None
|
||
job_id: str | None = None
|
||
created_at: datetime | None = None
|