fix(report): 修复报告生成中盈亏显示缺失的多个bug
- 修复 market_opened 检测中 df 变量名冲突导致 KeyError: holdings - 维持仓位盈亏使用最近可用收盘价计算(不再依赖市场是否开盘) - 调出标的盈亏:市场已开盘用当天开盘价,未开盘用前日收盘价 - 新调入标的在市场未开盘时正确显示待开盘状态(进场日期/盈亏为空)
This commit is contained in:
@@ -1192,6 +1192,16 @@ class SimpleRotationStrategy:
|
|||||||
holdings = last_rec['holdings']
|
holdings = last_rec['holdings']
|
||||||
factors = last_rec.get('factors', {})
|
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
|
# Build code config dict for display
|
||||||
code_config = {}
|
code_config = {}
|
||||||
for name, asset in self.config.asset_pools.assets.items():
|
for name, asset in self.config.asset_pools.assets.items():
|
||||||
@@ -1235,15 +1245,26 @@ class SimpleRotationStrategy:
|
|||||||
entry_price = None
|
entry_price = None
|
||||||
holding_days = 0
|
holding_days = 0
|
||||||
pnl = None
|
pnl = None
|
||||||
|
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):
|
for rec in reversed(self.daily_records):
|
||||||
if code in rec['holdings']:
|
if code in rec['holdings']:
|
||||||
entry_date = pd.Timestamp(rec['date'])
|
rec_date = pd.Timestamp(rec['date'])
|
||||||
p = self._get_etf_prices(trade_code, entry_date)
|
p = self._get_etf_prices(trade_code, rec_date)
|
||||||
entry_price = p['open'] if p else None
|
if p is not None:
|
||||||
|
entry_date = rec_date
|
||||||
|
entry_price = p['open']
|
||||||
else:
|
else:
|
||||||
break
|
break
|
||||||
if entry_date is not None:
|
if entry_date is not None:
|
||||||
holding_days = (last_date - entry_date).days
|
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:
|
if entry_price and entry_price > 0 and etf_close and etf_close > 0:
|
||||||
pnl = etf_close / entry_price - 1
|
pnl = etf_close / entry_price - 1
|
||||||
|
|
||||||
@@ -1264,6 +1285,8 @@ class SimpleRotationStrategy:
|
|||||||
old_holdings = set(self.daily_records[last_idx - 1]['holdings'])
|
old_holdings = set(self.daily_records[last_idx - 1]['holdings'])
|
||||||
new_holdings = set(holdings)
|
new_holdings = set(holdings)
|
||||||
removed = old_holdings - new_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):
|
for code in sorted(removed):
|
||||||
cfg = code_config.get(code, {})
|
cfg = code_config.get(code, {})
|
||||||
name = cfg.get('name', code)
|
name = cfg.get('name', code)
|
||||||
@@ -1273,13 +1296,37 @@ class SimpleRotationStrategy:
|
|||||||
etf_close = self._get_etf_close(trade_code, last_date)
|
etf_close = self._get_etf_close(trade_code, last_date)
|
||||||
premium = self._get_latest_premium(trade_code, last_date)
|
premium = self._get_latest_premium(trade_code, last_date)
|
||||||
score = factors.get(code)
|
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({
|
exit_positions.append({
|
||||||
'name': name, 'code': code, 'etf': etf_code,
|
'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,
|
'idx_close': idx_close, 'etf_close': etf_close,
|
||||||
'premium': premium, 'action': '调出',
|
'premium': premium, 'action': '调出',
|
||||||
'entry_date': None, 'entry_price': None,
|
'entry_date': exit_entry_date, 'entry_price': exit_entry_price,
|
||||||
'holding_days': 0, 'pnl': None,
|
'holding_days': exit_holding_days, 'pnl': exit_pnl,
|
||||||
})
|
})
|
||||||
|
|
||||||
# Build unselected (未入选) positions: all signal codes not held and not exited
|
# Build unselected (未入选) positions: all signal codes not held and not exited
|
||||||
|
|||||||
Reference in New Issue
Block a user