298 lines
11 KiB
Python
298 lines
11 KiB
Python
"""
|
||
飞书复盘报告推送模块
|
||
|
||
推送三类卡片:
|
||
1. push_review_report — 策略复盘报告(蓝色主题)
|
||
2. push_reverse_analysis_report — 逆向分析报告(紫色主题)
|
||
3. push_rule_update_notification — 新规律通知(绿色主题)
|
||
|
||
复用 feishu_push.py 的认证模式(load_feishu_creds → get_token → push_card)
|
||
"""
|
||
|
||
import os
|
||
import sys
|
||
import json
|
||
|
||
sys.path.insert(0, os.path.dirname(__file__))
|
||
from feishu_push import push_card
|
||
|
||
CHAT_ID = "oc_2c597ad94167102922de142928e2917a"
|
||
|
||
|
||
# ==================== 1. 策略复盘报告 ====================
|
||
|
||
def push_review_report(review_results):
|
||
"""
|
||
推送策略复盘报告卡片 — 📊 蓝色主题
|
||
|
||
Section 1: 推荐命中统计 (hit/fail/flat counts, hit rate %)
|
||
Section 2: 信号绩效TOP5 (best performing signals)
|
||
Section 3: 遗漏爆炸 (missed coins, why, what features)
|
||
Section 4: 权重调整 (weight changes)
|
||
"""
|
||
reviews = review_results.get("review_details", [])
|
||
weight_adj = review_results.get("weight_adjustments", [])
|
||
missed = review_results.get("missed_explosions", [])
|
||
|
||
# Section 1: 命中统计
|
||
hit_count = sum(1 for r in reviews if r.get("outcome") == "爆发")
|
||
fail_count = sum(1 for r in reviews if r.get("outcome") == "失败")
|
||
flat_count = sum(1 for r in reviews if r.get("outcome") == "横盘")
|
||
total = len(reviews)
|
||
hit_rate_pct = round(hit_count / total * 100, 1) if total > 0 else 0
|
||
|
||
# 命中统计文案
|
||
hit_emoji = "🔥" if hit_rate_pct >= 50 else "⚠️" if hit_rate_pct >= 30 else "❌"
|
||
stats_line = (
|
||
f"本次复盘 **{total}** 条推荐:\n"
|
||
f" • 爆发(命中): **{hit_count}** ({hit_emoji})\n"
|
||
f" • 横盘: **{flat_count}**\n"
|
||
f" • 失败: **{fail_count}**\n"
|
||
f" • 命中率: **{hit_rate_pct}%**"
|
||
)
|
||
|
||
# Section 2: 信号绩效TOP5
|
||
# 从review_results中提取信号绩效信息
|
||
from altcoin_db import get_signal_weights
|
||
weights = get_signal_weights()
|
||
sig_perf_list = sorted(
|
||
[(sig, data) for sig, data in weights.items() if data.get("total_count", 0) >= 3],
|
||
key=lambda x: x[1].get("hit_rate", 0),
|
||
reverse=True,
|
||
)[:5]
|
||
|
||
sig_lines = ""
|
||
if sig_perf_list:
|
||
for sig, data in sig_perf_list:
|
||
hr = data.get("hit_rate", 0)
|
||
w = data.get("weight", 0)
|
||
total_n = data.get("total_count", 0)
|
||
cat = data.get("category", "")
|
||
emoji = "✅" if hr >= 50 else "⚠️" if hr >= 30 else "❌"
|
||
sig_lines += f"\n • {emoji} **{sig}**({cat}): 命中率{hr}% | 权重{w} | 样本{total_n}"
|
||
else:
|
||
sig_lines = "\n • 样本不足,暂无绩效数据"
|
||
|
||
# Section 3: 遗漏爆炸
|
||
missed_lines = ""
|
||
if missed:
|
||
for m in missed[:5]: # 最多展示5只
|
||
symbol = m.get("symbol", "")
|
||
gain = m.get("gain_pct", 0)
|
||
reason = m.get("reason_missed", m.get("reason", ""))
|
||
features = m.get("features_detected", [])
|
||
if isinstance(features, str):
|
||
try:
|
||
features = json.loads(features)
|
||
except:
|
||
features = [features]
|
||
feat_str = ", ".join(str(f) for f in features[:3]) if features else "无"
|
||
missed_lines += f"\n • 💥 **{symbol}** 涨{gain}% | 原因: {reason} | 特征: {feat_str}"
|
||
else:
|
||
missed_lines = "\n • ✅ 无遗漏爆炸"
|
||
|
||
# Section 4: 权重调整
|
||
adj_lines = ""
|
||
if weight_adj:
|
||
for adj in weight_adj:
|
||
adj_lines += f"\n • {adj}"
|
||
else:
|
||
adj_lines = "\n • 无权重调整"
|
||
|
||
# 构建卡片
|
||
card = {
|
||
"config": {"wide_screen_mode": True},
|
||
"header": {
|
||
"template": "blue",
|
||
"title": {"tag": "plain_text", "content": "📊 山寨币策略复盘报告"},
|
||
},
|
||
"elements": [
|
||
{
|
||
"tag": "div",
|
||
"text": {
|
||
"tag": "lark_md",
|
||
"content": (
|
||
f"**=== 推荐命中统计 ===**\n{stats_line}\n\n"
|
||
f"**=== 信号绩效TOP5 ===**\n{sig_lines}\n\n"
|
||
f"**=== 遗漏爆炸 ===**\n{missed_lines}\n\n"
|
||
f"**=== 权重调整 ===**\n{adj_lines}"
|
||
),
|
||
},
|
||
},
|
||
{
|
||
"tag": "action",
|
||
"actions": [
|
||
{
|
||
"tag": "button",
|
||
"text": {"tag": "plain_text", "content": f"📊 命中率{hit_rate_pct}%"},
|
||
"type": "primary" if hit_rate_pct >= 50 else "warning" if hit_rate_pct >= 30 else "danger",
|
||
}
|
||
],
|
||
},
|
||
],
|
||
}
|
||
return push_card(card)
|
||
|
||
|
||
# ==================== 2. 逆向分析报告 ====================
|
||
|
||
def push_reverse_analysis_report(reverse_results):
|
||
"""
|
||
推送逆向分析报告卡片 — 🔍 紫色主题
|
||
|
||
Section 1: 今日涨幅榜TOP10 (symbol, gain%, sector)
|
||
Section 2: 起爆前共性特征 (pattern summary with percentages)
|
||
Section 3: 新发现规律 (any new rules)
|
||
"""
|
||
top_gainers = reverse_results.get("top_gainers", [])
|
||
pattern_summary = reverse_results.get("pattern_summary", [])
|
||
new_rules = reverse_results.get("new_rules", [])
|
||
total_unrecommended = reverse_results.get("total_unrecommended", 0)
|
||
total_analyzed = reverse_results.get("total_analyzed", 0)
|
||
|
||
# Section 1: 涨幅榜TOP10
|
||
gainer_lines = ""
|
||
for i, g in enumerate(top_gainers[:10], 1):
|
||
symbol = g.get("symbol", "").replace("/USDT", "")
|
||
gain = g.get("gain_pct", 0)
|
||
sector = g.get("sector", [])
|
||
sector_str = sector[0] if isinstance(sector, list) and sector else (sector if sector else "未知")
|
||
volume = g.get("volume_24h", 0)
|
||
vol_str = f"${volume / 1e6:.1f}M" if volume > 0 else ""
|
||
gainer_lines += f"\n {i}. **{symbol}** +{gain}% | {sector_str} | {vol_str}"
|
||
|
||
if not gainer_lines:
|
||
gainer_lines = "\n • 今日无明显涨幅"
|
||
|
||
# Section 2: 起爆前共性特征
|
||
pattern_lines = ""
|
||
for p in pattern_summary[:8]: # 最多展示8个特征
|
||
label = p.get("label", p.get("feature", ""))
|
||
pct = p.get("percentage", 0)
|
||
count = p.get("count", 0)
|
||
total = p.get("total", 0)
|
||
bar = "█" * int(pct / 10) + "░" * (10 - int(pct / 10))
|
||
emoji = "🔥" if pct >= 60 else "✅" if pct >= 40 else "⚠️" if pct >= 20 else "❌"
|
||
pattern_lines += f"\n • {emoji} **{label}**: {pct}%({count}/{total}) {bar}"
|
||
|
||
if not pattern_lines:
|
||
pattern_lines = "\n • 分析样本不足"
|
||
|
||
# Section 3: 新发现规律
|
||
rule_lines = ""
|
||
if new_rules:
|
||
for r in new_rules:
|
||
rule_id = r.get("rule_id", "")
|
||
desc = r.get("description", "")
|
||
score_adj = r.get("score_adjust", 0)
|
||
rule_type = r.get("type", "")
|
||
rule_lines += f"\n • 🧠 **{rule_id}**: {desc} → 评分{score_adj}({rule_type})"
|
||
else:
|
||
rule_lines = "\n • 暂无新规律达到显著性阈值"
|
||
|
||
# 分析概况
|
||
overview = (
|
||
f"涨幅榜共{len(top_gainers)}只 ≥10%\n"
|
||
f"未被推荐: {total_unrecommended}只\n"
|
||
f"已做PA分析: {total_analyzed}只"
|
||
)
|
||
|
||
# 构建卡片(紫色主题用 "violet" — 飞书卡片没有purple,用indigo近似)
|
||
card = {
|
||
"config": {"wide_screen_mode": True},
|
||
"header": {
|
||
"template": "indigo",
|
||
"title": {"tag": "plain_text", "content": "🔍 逆向分析报告 — 涨幅榜复盘"},
|
||
},
|
||
"elements": [
|
||
{
|
||
"tag": "div",
|
||
"text": {
|
||
"tag": "lark_md",
|
||
"content": (
|
||
f"**{overview}**\n\n"
|
||
f"**=== 今日涨幅榜TOP10 ===**\n{gainer_lines}\n\n"
|
||
f"**=== 起爆前共性特征 ===**\n{pattern_lines}\n\n"
|
||
f"**=== 新发现规律 ===**\n{rule_lines}"
|
||
),
|
||
},
|
||
},
|
||
{
|
||
"tag": "action",
|
||
"actions": [
|
||
{
|
||
"tag": "button",
|
||
"text": {"tag": "plain_text", "content": f"🔍 分析{total_analyzed}只暴涨币"},
|
||
"type": "primary",
|
||
}
|
||
],
|
||
},
|
||
],
|
||
}
|
||
return push_card(card)
|
||
|
||
|
||
# ==================== 3. 新规律通知 ====================
|
||
|
||
def push_rule_update_notification(rule_id, description, status="候选规则,未生效"):
|
||
"""
|
||
推送新规律学习通知 — 🧠 绿色主题
|
||
简洁卡片,告知策略自动迭代
|
||
"""
|
||
card = {
|
||
"config": {"wide_screen_mode": True},
|
||
"header": {
|
||
"template": "green",
|
||
"title": {"tag": "plain_text", "content": "🧠 策略自学习 — 候选规则发现"},
|
||
},
|
||
"elements": [
|
||
{
|
||
"tag": "div",
|
||
"text": {
|
||
"tag": "lark_md",
|
||
"content": (
|
||
f"规则ID: **{rule_id}**\n\n"
|
||
f"状态: **{status}**\n\n"
|
||
f"描述: {description}\n\n"
|
||
f"说明: 该规则仅进入候选池/灰度评估,未通过发布闸门前不会写入正式规则库,也不会影响下次选币。"
|
||
),
|
||
},
|
||
},
|
||
{
|
||
"tag": "action",
|
||
"actions": [
|
||
{
|
||
"tag": "button",
|
||
"text": {"tag": "plain_text", "content": f"🧠 {rule_id}"},
|
||
"type": "primary",
|
||
}
|
||
],
|
||
},
|
||
],
|
||
}
|
||
return push_card(card)
|
||
|
||
|
||
# ==================== 测试 ====================
|
||
|
||
if __name__ == "__main__":
|
||
print("测试复盘报告推送...")
|
||
|
||
# 测试review report
|
||
test_review = {
|
||
"review_details": [
|
||
{"symbol": "FET/USDT", "outcome": "爆发", "pnl_48h": 12.5},
|
||
{"symbol": "ARB/USDT", "outcome": "横盘", "pnl_48h": 1.2},
|
||
{"symbol": "PEPE/USDT", "outcome": "失败", "pnl_48h": -4.5},
|
||
],
|
||
"weight_adjustments": ["量价齐飞: 3→4.0 (命中率67%)"],
|
||
"missed_explosions": [
|
||
{"symbol": "INJ/USDT", "gain_pct": 25, "reason_missed": "细筛淘汰(score=4)", "features_detected": ["ignition_point", "Q7_zone"]},
|
||
],
|
||
}
|
||
ok1, r1 = push_review_report(test_review)
|
||
print(f"复盘报告: ok={ok1}")
|
||
|
||
# 测试rule notification
|
||
ok2, r2 = push_rule_update_notification("rule_20260429_001", "涨幅榜60%有起爆点 → 起爆点是爆发前必现信号")
|
||
print(f"规律通知: ok={ok2}") |