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 通过
This commit is contained in:
Simon
2026-09-09 00:36:38 +08:00
parent ba52edc2d6
commit 692bdb3be5
4 changed files with 72 additions and 2 deletions
+3 -2
View File
@@ -32,6 +32,7 @@ from app.quant.composite import ( # noqa: F401 —— re-export(模块化后
cross_sectional_zscore,
)
from app.quant.evaluation import run_factor_test
from app.quant.portfolio import equal_weight_budget, unimplemented_notes
TRADING_DAYS = 252
_DEFAULT_UNIMPLEMENTED = [
@@ -169,7 +170,7 @@ class TopKBacktestRunner:
targets.append(s)
if targets:
budget = cash / len(targets)
budget = equal_weight_budget(cash, len(targets))
for s in targets:
c = float(close_d[s])
price_in = c * (1 + self.costs.slippage_rate)
@@ -263,7 +264,7 @@ class TopKBacktestRunner:
positions=positions,
trades=trades,
turnover_pct=round(sum(notional) / max(init, 1) * 100, 2),
unimplemented=list(_DEFAULT_UNIMPLEMENTED),
unimplemented=list(_DEFAULT_UNIMPLEMENTED) + unimplemented_notes(self.spec.portfolio),
config_snapshot=self.spec.model_dump(mode="json"),
)
+31
View File
@@ -0,0 +1,31 @@
"""Portfolio Engine(v2 §16)—— 组合构建模块(M8.2)。
MVP:等权资金拆分(与既有 TopK 回测等权语义一致,行为收敛到本模块);
单股/行业上限等约束为预留字段,未建模时由回测器写入 unimplemented
(禁止假装支持,AGENT.md §24)。
"""
from __future__ import annotations
from app.domain.entities.research import PortfolioSpec
def equal_weight_budget(cash: float, target_count: int) -> float:
"""等权单标的预算:现金均分(target_count>0)。"""
if target_count <= 0:
return 0.0
return cash / target_count
def unimplemented_notes(portfolio: PortfolioSpec) -> list[str]:
"""组合层未建模项说明(默认空;设置约束即显式标注)。"""
notes: list[str] = []
if portfolio.max_position_pct is not None:
notes.append(
f"最大单股权重 {portfolio.max_position_pct:.0%} 约束未建模(Portfolio v1 仅等权)"
)
if portfolio.max_industry_weight_pct is not None:
notes.append(
f"最大行业权重 {portfolio.max_industry_weight_pct:.0%} 约束未建模(Portfolio v1 仅等权)"
)
return notes