Files
qlib/backend/app/domain/entities/research.py
T
Simon 692bdb3be5 feat(portfolio): M8.2 Portfolio Engine 模块化(等权收敛 + 约束显式标注)
- research.PortfolioSpec(weighting=equal;max_position_pct/max_industry_weight_pct 预留)
  + ResearchSpec.portfolio;config_snapshot 自动记录组合配置
- quant/portfolio.py:equal_weight_budget(与既有等权回测语义一致,行为收敛到本模块)+
  unimplemented_notes(设置约束即在结果中显式标注未建模,禁止假装支持)
- TopKBacktestRunner 预算与 unimplemented 改用 portfolio 模块;默认配置数值不变
  (一致性/quant 引擎回归通过);tests 补约束标注与 config_snapshot;全量 pytest 通过
2026-09-09 00:36:38 +08:00

240 lines
7.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""研究领域对象: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 PortfolioSpec(BaseModel):
"""组合构建(v2 §16)。MVP:等权;单股/行业上限等约束字段预留,
未建模约束在回测结果 unimplemented 中如实标注(禁止假装支持)。
"""
weighting: str = Field(default="equal", pattern="^(equal)$")
max_position_pct: float | None = Field(
default=None, gt=0, le=1, description="单股最大权重(预留,未建模)"
)
max_industry_weight_pct: float | None = Field(
default=None, gt=0, le=1, description="行业最大权重(预留,未建模)"
)
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()
price_adjustment: str = Field(
default="none", pattern="^(none|qfq)$",
description="研究行情口径:none 不复权(默认)/ qfq 前复权(result 与 config_snapshot 中显式)",
)
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()
portfolio: PortfolioSpec = PortfolioSpec()
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