/~learn/s/1bb6c1ef
> For Claude: Use Skill(name="subagent-driven-development") to implement this plan task-by-task.
Goal: 重新设计截面选基策略引擎的推送消息 UI,解决手机端 Markdown 渲染格式异常、行长过长、仓位分级歧义、信息冗余等问题。
架构: 单文件修改 engine.py 的 generate_push_message() 方法(约120行),调整消息格式、修复Markdown兼容性、精简内容、增加调仓提醒。
Tech Stack: Python 3.12, Server酱 Markdown desp
---
generate_push_message() 消息格式文件:
adata/aohan/quant/selection/engine.py:L198-318变更总览(当前格式 → 新格式):
| 区块 | 旧版 | 新版 | |
| ------ | ------ | ------ | |
| 标题 | 📊 截面选基策略引擎 \ | 日期 | 同上 + ETF总数 |
| 分隔线 | = x 72 | = x 48(更紧凑) | |
| 市场状态 | 占3行+空行 | 压缩到2行,调仓日嵌入 | |
| 下次调仓 | 无 | ⏰ 下次调仓: MM-DD 周X 前置醒目 | |
| TOP 10 序号 | #1 → Markdown标题 | ① 纯文本序号 | |
| TOP 10 每行 | 单行长行(60-80字) | 拆成两行紧凑版 | |
| TOP 10 仓位 | ○ 中仓 文字分级 | 配置 11.5% 百分比 | |
| BOTTOM 10 | #36 格式 | ① 格式,一行紧凑 | |
| 配置方案 | 有仓位文字分级 | 纯百分比,加合计行 | |
| 风格信号 | 5-8行 ✅❌ | 移除 | |
| 操作建议 | 多行长行 | 精简一行一个 | |
| 调仓提醒底部 | 无 | ▶ 下次调仓: MM-DD 周X |
Step 1: 添加 circled number 工具函数
在 engine.py 顶部附近添加:
def _to_circled(n: int) -> str:
circled = ['①', '②', '③', '④', '⑤', '⑥', '⑦', '⑧', '⑨', '⑩',
'⑪', '⑫', '⑬', '⑭', '⑮', '⑯', '⑰', '⑱', '⑲', '⑳']
if 1 <= n <= len(circled):
return circled[n - 1]
return str(n)
Step 2: 计算下次调仓日
添加辅助函数:
def _next_rebalance_date(from_date: str = None) -> str:
"""计算下次调仓日(本周五)"""
if from_date is None:
from_date = datetime.now().strftime('%Y-%m-%d')
dt = datetime.strptime(from_date, '%Y-%m-%d')
days_ahead = 4 - dt.weekday() # Friday=4
if days_ahead <= 0:
days_ahead += 7
next_friday = dt + timedelta(days=days_ahead)
weekday_names = ['周一', '周二', '周三', '周四', '周五', '周六', '周日']
return f"{next_friday.strftime('%m-%d')} {weekday_names[next_friday.weekday()]}"
Step 3: 重写 generate_push_message()
完整替换 L198-318。关键变更点:
(a) 标题区 — 加 ETF 总数
lines.append(f"📊 截面选基策略引擎 | {date}")
lines.append(f" 共扫描 {len(result_df)} 只ETF")
lines.append("=" * 48)
lines.append("")
(b) 市场状态 + 下次调仓 — 压缩到两行
lines.append(f"📈 {regime_label} | 仓位上限 {exposure_cap:.0%}")
lines.append(f" 价格vs年线:{price_vs_ma:+.1%} 波动率比:{vol_ratio:.2f}x")
lines.append(f"⏰ 下次调仓: {_next_rebalance_date(date)}")
lines.append("")
(c) TOP 10 — 两行紧凑 + ① 序号 + 百分比仓位
lines.append("🔥 TOP 10 最强品种")
lines.append("-" * 48)
for i, (_, row) in enumerate(result_df.head(10).iterrows()):
rank = int(row['排名'])
score = row['综合评分']
weight = portfolio.loc[portfolio['代码'] == row['代码'], '建议权重'].values
adj_weight = weight[0] * exposure_cap if len(weight) > 0 else 0
lines.append(f"{_to_circled(i+1)} {row['代码']:>10s} {row['名称']:<12s} [{score:.3f}]")
lines.append(f" {row['类别']} · 配置 {adj_weight:>5.1%}")
lines.append("")
(d) BOTTOM 10 — 一行紧凑 + ① 序号
lines.append("🧊 BOTTOM 10 最弱品种")
lines.append("-" * 48)
for row in result_df.tail(10).iterrows():
r = row[1] # 兼容iterrows返回的(index, Series)
break
# 实际用:
for i, (_, row) in enumerate(result_df.tail(10).iterrows()):
lines.append(f"{_to_circled(i+1)} {row['代码']:>10s} {row['名称']:<12s} {row['综合评分']:.3f} {row['类别']}")
lines.append("")
(e) 配置方案 — 纯百分比 + 合计行
lines.append(f"📋 Top 10 配置方案 (总仓位上限 {exposure_cap:.0%})")
lines.append("-" * 48)
total_weight = 0
for _, row in portfolio.head(10).iterrows():
adj_weight = row['建议权重'] * exposure_cap
total_weight += adj_weight
lines.append(f" {row['代码']:>10s} {row['名称']:<12s} {adj_weight:>5.1%}")
lines.append(f" {'合计':>10s} {'':<12s} {total_weight:>5.1%}")
lines.append("")
(f) 移除风格信号块 — 直接删除 L284-292
(g) 操作建议 — 精简 + 调仓提醒
lines.append("💡 操作建议")
lines.append("-" * 48)
# 市场环境建议
if regime in ('BEAR', 'SEVERE_BEAR'):
lines.append(f"▶ 当前熊市,仓位控制在 {exposure_cap:.0%} 以内,防御为主")
elif regime == 'HIGH_VOL':
lines.append(f"▶ 高波动环境,缩舱至 {exposure_cap:.0%},优先低波品种")
elif regime == 'BULL':
lines.append(f"▶ 牛市环境,仓位上限 {exposure_cap:.0%},积极参与强势板块")
else:
lines.append(f"▶ 震荡市,仓位上限 {exposure_cap:.0%},均衡配置")
# 关注品种(取排名前3,精简格式)
top3 = result_df.head(3)
codes_str = " ".join(f"{r['名称']}({r['代码']})" for _, r in top3.iterrows())
lines.append(f"▶ 关注: {codes_str}")
# 回避品种(取BOTTOM 10中评分最低的3只)
bot3 = result_df.tail(3)
codes_str = " ".join(f"{r['名称']}({r['代码']})" for _, r in bot3.iterrows())
lines.append(f"▶ 回避: {codes_str}")
# 调仓提醒
lines.append(f"▶ 下次调仓: {_next_rebalance_date(date)}")
lines.append("")
lines.append("=" * 48)
Step 4: 验证输出格式
运行:
./run_push.sh -c 399102.SZ
检查 /Users/aohan/PycharmProjects/adata/adata/aohan/quant/backtest/strategy/custom_signal/消息推送.txt 的格式。
预期输出示例:
📊 截面选基策略引擎 | 2026-05-12
共扫描 45 只ETF
════════════════════════════════════════
📈 🟢 强势牛市 | 仓位上限 97%
价格vs年线:+7.4% 波动率比:1.10x
⏰ 下次调仓: 05-16 周五
🔥 TOP 10 最强品种
────────────────────────────────
① 159949.SZ 创业板50ETF [0.833]
宽基指数 · 配置 11.5%
② 159937.SZ 黄金ETF [0.829]
商品 · 配置 11.5%
③ 515030.SH 新能源车ETF [0.729]
行业主题 · 配置 10.1%
④ 159915.SZ 创业板ETF [0.720]
宽基指数 · 配置 9.9%
⑤ 159790.SZ 碳中和ETF [0.671]
行业主题 · 配置 9.3%
⑥ 515050.SH 5GETF [0.660]
行业主题 · 配置 9.1%
⑦ 159870.SZ 化工ETF [0.660]
行业主题 · 配置 9.1%
⑧ 159985.SZ 豆粕ETF [0.658]
商品 · 配置 9.1%
⑨ 159399.SZ 现金流ETF [0.634]
策略风格 · 配置 8.8%
⑩ 512480.SH 半导体ETF [0.628]
行业主题 · 配置 8.7%
🧊 BOTTOM 10 最弱品种
────────────────────────────────
① 512010.SH 医药ETF 0.383 行业主题
② 159920.SZ 恒生ETF 0.377 跨境
③ 159865.SZ 养殖ETF 0.363 行业主题
④ 159928.SZ 消费ETF 0.334 行业主题
⑤ 512880.SH 证券ETF 0.329 行业主题
⑥ 513180.SH 恒生科技ETF 0.315 跨境
⑦ 513500.SH 标普500ETF 0.285 跨境
⑧ 510050.SH 上证50ETF 0.272 宽基指数
⑨ 515060.SH 房地产ETF 0.246 行业主题
⑩ 516160.SH 新能源ETF 0.183 行业主题
📋 Top 10 配置方案 (总仓位上限 97%)
────────────────────────────────
159949.SZ 创业板50ETF 11.5%
159937.SZ 黄金ETF 11.5%
515030.SH 新能源车ETF 10.1%
159915.SZ 创业板ETF 9.9%
159790.SZ 碳中和ETF 9.3%
515050.SH 5GETF 9.1%
159870.SZ 化工ETF 9.1%
159985.SZ 豆粕ETF 9.1%
159399.SZ 现金流ETF 8.8%
512480.SH 半导体ETF 8.7%
合计: 97.0%
💡 操作建议
────────────────────────────────
▶ 牛市环境,仓位上限 97%,积极参与强势板块
▶ 关注: 创业板50ETF(159949) 新能源车ETF(515030) 碳中和ETF(159790)
▶ 回避: 证券ETF(512880) 房地产ETF(515060) 新能源ETF(516160)
▶ 下次调仓: 05-16 周五
════════════════════════════════════════
Step 5: Server酱端线测试(仅限本地验证)
① 字符和 Markdown 格式= 和 - 不干扰 Markdown 渲染# 开头验证清单:
# 开头的行列(已全部替换为 ① 等)═ 和 ─ 代替 = 和 -?(待定,取决于Server酱渲染)