fix(report): 修复报告生成中盈亏显示缺失的多个bug
- 修复 market_opened 检测中 df 变量名冲突导致 KeyError: holdings - 维持仓位盈亏使用最近可用收盘价计算(不再依赖市场是否开盘) - 调出标的盈亏:市场已开盘用当天开盘价,未开盘用前日收盘价 - 新调入标的在市场未开盘时正确显示待开盘状态(进场日期/盈亏为空)
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user