fix(report): 修复报告生成中盈亏显示缺失的多个bug

- 修复 market_opened 检测中 df 变量名冲突导致 KeyError: holdings
- 维持仓位盈亏使用最近可用收盘价计算(不再依赖市场是否开盘)
- 调出标的盈亏:市场已开盘用当天开盘价,未开盘用前日收盘价
- 新调入标的在市场未开盘时正确显示待开盘状态(进场日期/盈亏为空)
This commit is contained in:
2026-06-08 08:35:31 +08:00
parent 4736b64eca
commit c32ce72579

View File

@@ -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