From 0da03068949a4f6c184f1604ad03e56d0df295c7 Mon Sep 17 00:00:00 2001 From: aszerW Date: Sun, 21 Jun 2026 13:13:17 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20greedy=E6=A8=A1=E5=BC=8F=E4=BB=85?= =?UTF-8?q?=E5=9C=A8select=5Fnum=3D1=E6=97=B6=E7=94=9F=E6=95=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 问题:greedy在select_num>1时仓位分配取决于ETF池大小,而非动量强度 - 场景1: 有色金属(1ETF)>原油(3ETF)>黄金(4ETF) → 有色25%,原油75%,黄金0% - 场景2: 黄金(4ETF)>创业板(4ETF)>纳指(4ETF) → 黄金100%,其他0% 修复:select_num>1时greedy退化为equal权重 回测对比: - select_num=3, rank: 326.60% - select_num=1, greedy: 730.61% (集中度更高,收益更好) --- rotation/simple_rotation.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/rotation/simple_rotation.py b/rotation/simple_rotation.py index 08666e2..8262abc 100644 --- a/rotation/simple_rotation.py +++ b/rotation/simple_rotation.py @@ -530,17 +530,18 @@ class SimpleRotationStrategy: def _compute_greedy_weights(self, holdings: List[str], factors: Dict[str, float]) -> Dict[str, float]: """Compute greedy weights for signal-level holdings. - Greedy Algorithm: - 1. Each index has absorption capacity = min(n_etfs, ceil(1/max_weight)) × max_weight - 2. Iterate through holdings in order (sorted by momentum) - 3. Each index absorbs up to its capacity - 4. Remaining weight flows to next index + Greedy Algorithm (only for select_num=1): + 1. The top-1 index has absorption capacity = min(n_etfs, ceil(1/max_weight)) × max_weight + 2. If capacity < 100%, remaining weight flows to next index by momentum + 3. Continue until 100% allocated Example (select_num=1, max_weight=0.25): - 有色金属(1 ETF): capacity=25%, absorbs 25%, remaining=75% - 原油(3 ETFs): capacity=75%, absorbs 75%, remaining=0% - Total: 100% + For select_num>1, falls back to equal weight (greedy not applicable). + Args: holdings: List of signal codes (sorted by momentum desc) factors: Dict of signal_code -> momentum score @@ -551,6 +552,12 @@ class SimpleRotationStrategy: if not holdings: return {} + # Greedy only for select_num=1 + if self.select_num > 1: + # Fall back to equal weight + w = 1.0 / len(holdings) + return {code: w for code in holdings} + signal_weights = {} remaining_weight = 1.0