From c32ce725791904d5baf842b758aab2457356a11a Mon Sep 17 00:00:00 2001 From: aszerW Date: Mon, 8 Jun 2026 08:35:31 +0800 Subject: [PATCH] =?UTF-8?q?fix(report):=20=E4=BF=AE=E5=A4=8D=E6=8A=A5?= =?UTF-8?q?=E5=91=8A=E7=94=9F=E6=88=90=E4=B8=AD=E7=9B=88=E4=BA=8F=E6=98=BE?= =?UTF-8?q?=E7=A4=BA=E7=BC=BA=E5=A4=B1=E7=9A=84=E5=A4=9A=E4=B8=AAbug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 修复 market_opened 检测中 df 变量名冲突导致 KeyError: holdings - 维持仓位盈亏使用最近可用收盘价计算(不再依赖市场是否开盘) - 调出标的盈亏:市场已开盘用当天开盘价,未开盘用前日收盘价 - 新调入标的在市场未开盘时正确显示待开盘状态(进场日期/盈亏为空) --- rotation/simple_rotation.py | 75 ++++++++++++++++++++++++++++++------- 1 file changed, 61 insertions(+), 14 deletions(-) diff --git a/rotation/simple_rotation.py b/rotation/simple_rotation.py index 94157be..6a21feb 100644 --- a/rotation/simple_rotation.py +++ b/rotation/simple_rotation.py @@ -1192,6 +1192,16 @@ class SimpleRotationStrategy: holdings = last_rec['holdings'] factors = last_rec.get('factors', {}) + # Check if market has opened for last_date (ETF data available for exact date) + market_opened = False + for code in holdings: + trade_code = self.signal_to_trade.get(code, code) + if trade_code in self.etf_data: + etf_df = self.etf_data[trade_code] + if len(etf_df) > 0 and etf_df.index[-1] >= last_date: + market_opened = True + break + # Build code config dict for display code_config = {} for name, asset in self.config.asset_pools.assets.items(): @@ -1235,17 +1245,28 @@ class SimpleRotationStrategy: entry_price = None holding_days = 0 pnl = None - for rec in reversed(self.daily_records): - if code in rec['holdings']: - entry_date = pd.Timestamp(rec['date']) - p = self._get_etf_prices(trade_code, entry_date) - entry_price = p['open'] if p else None - else: - break - if entry_date is not None: - holding_days = (last_date - entry_date).days - if entry_price and entry_price > 0 and etf_close and etf_close > 0: - pnl = etf_close / entry_price - 1 + is_new_entry = (is_rebalance_day and code not in prev_holdings) + # New entries on a day when market hasn't opened yet: no execution data + if is_new_entry and not market_opened: + entry_date = None + entry_price = None + holding_days = 0 + pnl = None + else: + for rec in reversed(self.daily_records): + if code in rec['holdings']: + rec_date = pd.Timestamp(rec['date']) + p = self._get_etf_prices(trade_code, rec_date) + if p is not None: + entry_date = rec_date + entry_price = p['open'] + else: + break + if entry_date is not None: + holding_days = (last_date - entry_date).days + # For maintained positions, use latest available close for pnl + if entry_price and entry_price > 0 and etf_close and etf_close > 0: + pnl = etf_close / entry_price - 1 positions_info.append({ 'name': name, 'code': code, 'etf': etf_code, @@ -1264,6 +1285,8 @@ class SimpleRotationStrategy: old_holdings = set(self.daily_records[last_idx - 1]['holdings']) new_holdings = set(holdings) removed = old_holdings - new_holdings + prev_rec = self.daily_records[last_idx - 1] + prev_weights = prev_rec.get('position_weights', {}) for code in sorted(removed): cfg = code_config.get(code, {}) name = cfg.get('name', code) @@ -1273,13 +1296,37 @@ class SimpleRotationStrategy: etf_close = self._get_etf_close(trade_code, last_date) premium = self._get_latest_premium(trade_code, last_date) score = factors.get(code) + # Recover holding info from historical records + exit_entry_date = None + exit_entry_price = None + exit_holding_days = 0 + exit_pnl = None + for rec in reversed(self.daily_records[:last_idx]): + if code in rec['holdings']: + exit_entry_date = pd.Timestamp(rec['date']) + p = self._get_etf_prices(trade_code, exit_entry_date) + exit_entry_price = p['open'] if p else None + else: + break + if exit_entry_date is not None: + exit_holding_days = (last_date - exit_entry_date).days + # Exit price: today's open if market opened, else prev_close + exit_prices = self._get_etf_prices(trade_code, last_date) + if market_opened and exit_prices: + sell_price = exit_prices['open'] + elif exit_prices: + sell_price = exit_prices['prev_close'] + else: + sell_price = None + if exit_entry_price and exit_entry_price > 0 and sell_price and sell_price > 0: + exit_pnl = sell_price / exit_entry_price - 1 exit_positions.append({ 'name': name, 'code': code, 'etf': etf_code, - 'weight': 0, 'score': score, + 'weight': prev_weights.get(code, 0.0), 'score': score, 'idx_close': idx_close, 'etf_close': etf_close, 'premium': premium, 'action': '调出', - 'entry_date': None, 'entry_price': None, - 'holding_days': 0, 'pnl': None, + 'entry_date': exit_entry_date, 'entry_price': exit_entry_price, + 'holding_days': exit_holding_days, 'pnl': exit_pnl, }) # Build unselected (未入选) positions: all signal codes not held and not exited