fix: greedy模式从所有信号中顺延,而非仅holdings列表

问题:select_num=1时,holdings只有1个元素,ETF池不足时仓位闲置
- 有色金属(1ETF): 仓位25%,75%闲置
- 德国DAX(2ETF): 仓位50%,50%闲置
- 原油(3ETF): 仓位75%,25%闲置

修复:从所有信号中按动量排序顺延
- 有色金属(25%) → 顺延到第2名信号(如原油75%) → 100%满配

回测对比 (select_num=1):
- equal: 1053% 累计收益, 1.34 夏普
- greedy(修复前): 730% 累计收益, 1.31 夏普
- greedy(修复后): 1355% 累计收益, 1.69 夏普
This commit is contained in:
2026-06-21 13:59:01 +08:00
parent ac022020c7
commit 9d79ef9ca0

View File

@@ -531,9 +531,10 @@ class SimpleRotationStrategy:
"""Compute greedy weights for signal-level holdings. """Compute greedy weights for signal-level holdings.
Greedy Algorithm (only for select_num=1): Greedy Algorithm (only for select_num=1):
1. The top-1 index has absorption capacity = min(n_etfs, ceil(1/max_weight)) × max_weight 1. Get ALL signals sorted by momentum (not just holdings)
2. If capacity < 100%, remaining weight flows to next index by momentum 2. The top signal has absorption capacity = min(n_etfs, ceil(1/max_weight)) × max_weight
3. Continue until 100% allocated 3. If capacity < 100%, remaining weight flows to next signal by momentum
4. Continue until 100% allocated
Example (select_num=1, max_weight=0.25): Example (select_num=1, max_weight=0.25):
- 有色金属(1 ETF): capacity=25%, absorbs 25%, remaining=75% - 有色金属(1 ETF): capacity=25%, absorbs 25%, remaining=75%
@@ -558,10 +559,17 @@ class SimpleRotationStrategy:
w = 1.0 / len(holdings) w = 1.0 / len(holdings)
return {code: w for code in holdings} return {code: w for code in holdings}
# Get ALL signals sorted by momentum (for spill-over)
all_signals_sorted = sorted(
self.signal_codes,
key=lambda c: factors.get(c, 0),
reverse=True
)
signal_weights = {} signal_weights = {}
remaining_weight = 1.0 remaining_weight = 1.0
for signal_code in holdings: for signal_code in all_signals_sorted:
if remaining_weight <= 0: if remaining_weight <= 0:
break break
@@ -579,7 +587,8 @@ class SimpleRotationStrategy:
remaining_weight -= absorb remaining_weight -= absorb
# Assign weight to this signal # Assign weight to this signal
signal_weights[signal_code] = absorb if absorb > 0:
signal_weights[signal_code] = absorb
return signal_weights return signal_weights