feat(portfolio): C2 单股上限约束真实执行(Portfolio v1.1)
- portfolio.allocate_with_max_position:无上限=等权(与原实现一致);有上限=迭代 把超过 cap×当日组合市值的标的封顶并把剩余现金在其余标的中再分配,超出留现金 - TopKBacktestRunner 买入按约束分流(默认等权路径位级不变,回归数值保持) - unimplemented 只保留行业上限(依赖行业元数据注入,如实标注) - tests/test_portfolio_constraints.py(分配数值/封顶留现金/回测持仓权重≤上限+容差、 config_snapshot)+ 旧断言更新(单股不再标注);全量 pytest 通过
This commit is contained in:
@@ -34,7 +34,11 @@ 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
|
||||
from app.quant.portfolio import (
|
||||
allocate_with_max_position,
|
||||
equal_weight_budget,
|
||||
unimplemented_notes,
|
||||
)
|
||||
|
||||
TRADING_DAYS = 252
|
||||
_DEFAULT_UNIMPLEMENTED = [
|
||||
@@ -204,8 +208,25 @@ class TopKBacktestRunner:
|
||||
|
||||
# BUY 信号/成交记录:意图入选(filled)或意图被拒(原因);替补成交同样如实记录
|
||||
if targets:
|
||||
budget = equal_weight_budget(cash, len(targets))
|
||||
cap = self.spec.portfolio.max_position_pct
|
||||
if cap is None:
|
||||
# 默认等权(与原实现位级一致,保持回归数值不变)
|
||||
budget = equal_weight_budget(cash, len(targets))
|
||||
spends = {s: budget for s in targets}
|
||||
total_spend = budget * len(targets)
|
||||
else:
|
||||
# Portfolio v1.1:按单股上限(相对当日组合市值)分配,超出部分留现金
|
||||
equity_now = cash + sum(
|
||||
float(self.close.at[d, s] * qty)
|
||||
for s, qty in shares.items()
|
||||
if qty > 0 and not _nan(self.close.at[d, s])
|
||||
)
|
||||
spends = allocate_with_max_position(cash, targets, equity_now, cap)
|
||||
total_spend = sum(spends.values())
|
||||
for s in targets:
|
||||
budget = spends[s]
|
||||
if budget <= 1e-9:
|
||||
continue
|
||||
c = float(close_d[s])
|
||||
price_in = c * (1 + self.costs.slippage_rate)
|
||||
invest = budget * (1 - self.costs.commission_rate)
|
||||
@@ -217,7 +238,7 @@ class TopKBacktestRunner:
|
||||
ActionRecord(date=day, symbol=s, signal="BUY", filled=True,
|
||||
price=round(price_in, 4))
|
||||
)
|
||||
cash -= budget * len(targets)
|
||||
cash -= total_spend
|
||||
for sym in picks:
|
||||
if sym in target_set:
|
||||
continue
|
||||
|
||||
Reference in New Issue
Block a user