alphax/feishu_review_push.py
2026-05-13 22:32:50 +08:00

298 lines
11 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
飞书复盘报告推送模块
推送三类卡片:
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}")