feat(report): 全标的动量排名表替代原调仓信号表
Panel 0 从仅展示持仓调仓扩展为全标的排名表: - 新增未入选标的行,按动量降序展示 - 新增排名、市场两列 - 表格按调入→维持→调出→未入选顺序排列 - 调出标的也展示真实得分(便于分析调出原因) - 标题显示当前动态阈值 - 未入选标的浅灰背景区分
This commit is contained in:
@@ -1256,11 +1256,9 @@ class SimpleRotationStrategy:
|
|||||||
'holding_days': holding_days, 'pnl': pnl,
|
'holding_days': holding_days, 'pnl': pnl,
|
||||||
})
|
})
|
||||||
|
|
||||||
# Find exit positions: ONLY show when the last day is a rebalance day
|
# Build exit positions: ONLY when the last day is a rebalance day
|
||||||
# If last day is NOT rebalance, don't show any "调出" info (already past)
|
|
||||||
exit_positions = []
|
exit_positions = []
|
||||||
if last_rec.get('is_rebalance', False):
|
if last_rec.get('is_rebalance', False):
|
||||||
# Find the previous day's holdings to compare
|
|
||||||
last_idx = len(self.daily_records) - 1
|
last_idx = len(self.daily_records) - 1
|
||||||
if last_idx > 0:
|
if last_idx > 0:
|
||||||
old_holdings = set(self.daily_records[last_idx - 1]['holdings'])
|
old_holdings = set(self.daily_records[last_idx - 1]['holdings'])
|
||||||
@@ -1274,71 +1272,100 @@ class SimpleRotationStrategy:
|
|||||||
trade_code = self.signal_to_trade.get(code, code)
|
trade_code = self.signal_to_trade.get(code, code)
|
||||||
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)
|
||||||
exit_positions.append({
|
exit_positions.append({
|
||||||
'name': name, 'code': code, 'etf': etf_code,
|
'name': name, 'code': code, 'etf': etf_code,
|
||||||
'weight': 0, 'score': None,
|
'weight': 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': None, 'entry_price': None,
|
||||||
'holding_days': 0, 'pnl': None,
|
'holding_days': 0, 'pnl': None,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
# Build unselected (未入选) positions: all signal codes not held and not exited
|
||||||
|
held_or_exited = set(holdings) | {p['code'] for p in exit_positions}
|
||||||
|
unselected_positions = []
|
||||||
|
# Sort unselected by momentum descending
|
||||||
|
unselected_codes = [c for c in self.signal_codes if c not in held_or_exited]
|
||||||
|
unselected_codes.sort(key=lambda c: factors.get(c, -999), reverse=True)
|
||||||
|
for code in unselected_codes:
|
||||||
|
cfg = code_config.get(code, {})
|
||||||
|
name = cfg.get('name', code)
|
||||||
|
etf_code = cfg.get('etf', '—')
|
||||||
|
score = factors.get(code)
|
||||||
|
idx_close = self._get_index_close(code, last_date)
|
||||||
|
trade_code = self.signal_to_trade.get(code, code)
|
||||||
|
etf_close = self._get_etf_close(trade_code, last_date)
|
||||||
|
premium = self._get_latest_premium(trade_code, last_date)
|
||||||
|
unselected_positions.append({
|
||||||
|
'name': name, 'code': code, 'etf': etf_code,
|
||||||
|
'weight': 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,
|
||||||
|
})
|
||||||
|
|
||||||
# ==================== Plot ====================
|
# ==================== Plot ====================
|
||||||
plt.rcParams["font.sans-serif"] = ["Arial Unicode MS", "WenQuanYi Zen Hei", "DejaVu Sans"]
|
plt.rcParams["font.sans-serif"] = ["Arial Unicode MS", "WenQuanYi Zen Hei", "DejaVu Sans"]
|
||||||
plt.rcParams["axes.unicode_minus"] = False
|
plt.rcParams["axes.unicode_minus"] = False
|
||||||
|
|
||||||
n_rows = len(positions_info) + len(exit_positions)
|
n_rows = len(positions_info) + len(exit_positions) + len(unselected_positions)
|
||||||
signal_h = max(1.5, 0.5 + n_rows * 0.35)
|
signal_h = max(1.5, 0.5 + n_rows * 0.35)
|
||||||
fig = plt.figure(figsize=(16, 10 + signal_h + 1.2 + 8))
|
fig = plt.figure(figsize=(16, 10 + signal_h + 1.2 + 8))
|
||||||
gs = fig.add_gridspec(5, 1, height_ratios=[signal_h, 1.2, 3, 1, 1.2], hspace=0.35)
|
gs = fig.add_gridspec(5, 1, height_ratios=[signal_h, 1.2, 3, 1, 1.2], hspace=0.35)
|
||||||
|
|
||||||
# Panel 0: Signal table
|
# Panel 0: Full asset ranking table
|
||||||
ax0 = fig.add_subplot(gs[0])
|
ax0 = fig.add_subplot(gs[0])
|
||||||
ax0.axis("off")
|
ax0.axis("off")
|
||||||
ax0.set_title(f"最新调仓信号 (信号日期: {last_date.strftime('%Y-%m-%d')},下一交易日执行)",
|
threshold_val = last_rec.get('threshold', 0.0)
|
||||||
|
ax0.set_title(f"全标的动量排名 (信号日期: {last_date.strftime('%Y-%m-%d')},阈值: {threshold_val:.4f})",
|
||||||
fontsize=14, fontweight="bold", loc="left", pad=15)
|
fontsize=14, fontweight="bold", loc="left", pad=15)
|
||||||
|
|
||||||
col_labels = ["标的名称", "指数代码", "ETF代码", "仓位", "得分", "指数最新价",
|
# Build rank map for all codes by momentum
|
||||||
"ETF收盘价", "溢价率", "操作", "持有天数", "盈亏"]
|
ranked_all = sorted(factors.keys(), key=lambda c: factors[c], reverse=True)
|
||||||
|
rank_map = {c: i + 1 for i, c in enumerate(ranked_all)}
|
||||||
|
|
||||||
|
col_labels = ["排名", "标的名称", "市场", "指数代码", "ETF代码", "仓位", "得分",
|
||||||
|
"指数最新价", "ETF收盘价", "溢价率", "状态", "持有天数", "盈亏"]
|
||||||
table_data = []
|
table_data = []
|
||||||
for p in positions_info:
|
row_actions = [] # track action for coloring
|
||||||
score_s = f"{p['score']:.2f}" if p['score'] is not None else "—"
|
|
||||||
|
# Ordered: 调入 -> 维持 -> 调出 -> 未入选
|
||||||
|
all_rows = positions_info + exit_positions + unselected_positions
|
||||||
|
for p in all_rows:
|
||||||
|
rank = rank_map.get(p['code'], '—')
|
||||||
|
score_s = f"{p['score']:.4f}" if p['score'] is not None else "—"
|
||||||
idx_s = f"{p['idx_close']:.2f}" if p['idx_close'] is not None else "—"
|
idx_s = f"{p['idx_close']:.2f}" if p['idx_close'] is not None else "—"
|
||||||
etf_s = f"{p['etf_close']:.3f}" if p['etf_close'] is not None else "—"
|
etf_s = f"{p['etf_close']:.3f}" if p['etf_close'] is not None else "—"
|
||||||
prem_s = f"{p['premium']:+.2%}" if p['premium'] is not None else "—"
|
prem_s = f"{p['premium']:+.2%}" if p['premium'] is not None else "—"
|
||||||
days_s = str(p['holding_days']) if p['holding_days'] > 0 else "—"
|
days_s = str(p['holding_days']) if p['holding_days'] > 0 else "—"
|
||||||
pnl_s = f"{p['pnl']:+.2%}" if p['pnl'] is not None else "—"
|
pnl_s = f"{p['pnl']:+.2%}" if p['pnl'] is not None else "—"
|
||||||
|
weight_s = f"{p['weight']:.0%}" if p['weight'] > 0 else "—"
|
||||||
|
market = code_config.get(p['code'], {}).get('market', '—')
|
||||||
table_data.append([
|
table_data.append([
|
||||||
p['name'], p['code'], p['etf'], f"{p['weight']:.0%}",
|
rank, p['name'], market, p['code'], p['etf'], weight_s,
|
||||||
score_s, idx_s, etf_s, prem_s, p['action'], days_s, pnl_s
|
score_s, idx_s, etf_s, prem_s, p['action'], days_s, pnl_s
|
||||||
])
|
])
|
||||||
for p in exit_positions:
|
row_actions.append(p['action'])
|
||||||
idx_s = f"{p['idx_close']:.2f}" if p['idx_close'] is not None else "—"
|
|
||||||
etf_s = f"{p['etf_close']:.3f}" if p['etf_close'] is not None else "—"
|
|
||||||
prem_s = f"{p['premium']:+.2%}" if p['premium'] is not None else "—"
|
|
||||||
table_data.append([
|
|
||||||
p['name'], p['code'], p['etf'], f"{p['weight']:.0%}",
|
|
||||||
"—", idx_s, etf_s, prem_s, "调出", "—", "—"
|
|
||||||
])
|
|
||||||
|
|
||||||
if table_data:
|
if table_data:
|
||||||
tbl = ax0.table(cellText=table_data, colLabels=col_labels, loc="center",
|
tbl = ax0.table(cellText=table_data, colLabels=col_labels, loc="center",
|
||||||
cellLoc="center", bbox=[0, 0, 1, 1])
|
cellLoc="center", bbox=[0, 0, 1, 1])
|
||||||
tbl.auto_set_font_size(False)
|
tbl.auto_set_font_size(False)
|
||||||
tbl.set_fontsize(9)
|
tbl.set_fontsize(8)
|
||||||
tbl.scale(1, 1.8)
|
tbl.scale(1, 1.6)
|
||||||
for j in range(len(col_labels)):
|
for j in range(len(col_labels)):
|
||||||
tbl[0, j].set_facecolor("#2C3E50")
|
tbl[0, j].set_facecolor("#2C3E50")
|
||||||
tbl[0, j].set_text_props(color="white", fontweight="bold")
|
tbl[0, j].set_text_props(color="white", fontweight="bold")
|
||||||
|
action_colors = {
|
||||||
|
"调入": "#d4edda", # 浅绿色
|
||||||
|
"调出": "#f8d7da", # 浅红色
|
||||||
|
"维持": "#fff3cd", # 浅黄色
|
||||||
|
"未入选": "#f0f0f0", # 浅灰色
|
||||||
|
}
|
||||||
for i in range(len(table_data)):
|
for i in range(len(table_data)):
|
||||||
action = table_data[i][8]
|
color = action_colors.get(row_actions[i], "#ffffff")
|
||||||
# Legacy color scheme: 调入=green, 维持=yellow, 调出=red
|
|
||||||
if action == "调入":
|
|
||||||
color = "#d4edda" # 浅绿色
|
|
||||||
elif action == "调出":
|
|
||||||
color = "#f8d7da" # 浅红色
|
|
||||||
else:
|
|
||||||
color = "#fff3cd" # 浅黄色(维持)
|
|
||||||
for j in range(len(col_labels)):
|
for j in range(len(col_labels)):
|
||||||
tbl[i + 1, j].set_facecolor(color)
|
tbl[i + 1, j].set_facecolor(color)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user