403 lines
16 KiB
Python
403 lines
16 KiB
Python
"""
|
||
山寨币监控飞书卡片推送模块
|
||
|
||
通过飞书机器人 Webhook 直发,不经过 Hermes Agent 的飞书通道。
|
||
Webhook 支持 v2 interactive cards。
|
||
"""
|
||
|
||
import os
|
||
import json
|
||
import requests
|
||
|
||
# === 飞书 Webhook URL(用户指定的山寨币专用 webhook)===
|
||
FEISHU_WEBHOOK_URL = os.getenv("ALTCOIN_FEISHU_WEBHOOK", "").strip()
|
||
|
||
|
||
def push_card(card_content):
|
||
"""通过 webhook 推送飞书交互式卡片"""
|
||
payload = {
|
||
"msg_type": "interactive",
|
||
"card": card_content,
|
||
}
|
||
try:
|
||
if not FEISHU_WEBHOOK_URL:
|
||
return False, "ALTCOIN_FEISHU_WEBHOOK not configured"
|
||
r = requests.post(FEISHU_WEBHOOK_URL, json=payload, timeout=10)
|
||
result = r.json()
|
||
ok = (r.status_code == 200 and result.get("StatusCode") == 0)
|
||
return ok, result
|
||
except Exception as e:
|
||
return False, str(e)
|
||
|
||
|
||
def push_altcoin_burst_alert(symbol, price, signals, entry_plan, sector="", leader_status="", direction="多头启动"):
|
||
"""
|
||
推荐确认爆发推送 — 只做做多,方向永远多头🟢
|
||
"""
|
||
dir_emoji = "🟢"
|
||
dir_color = "green"
|
||
coin_name = symbol.replace('/USDT', '')
|
||
|
||
entry_lines = ""
|
||
if entry_plan:
|
||
rr_ok = "✅" if entry_plan.get("risk_reward_ok") else "❌"
|
||
entry_lines = f"""---
|
||
**入场方案**:
|
||
• 入场价: ${entry_plan['entry_price']}
|
||
• 入场方式: {entry_plan['entry_method']}
|
||
• 止损价: ${entry_plan['stop_loss']} ({entry_plan['stop_pct']}%)
|
||
• 止盈1: ${entry_plan['tp1']} (RR={entry_plan['rr1']} {rr_ok})
|
||
• 止盈2: ${entry_plan['tp2']} (RR={entry_plan['rr2']})
|
||
• 当前价: ${entry_plan['current_price']}"""
|
||
|
||
sector_line = f"\n**板块**: {sector}" if sector else ""
|
||
leader_line = f"\n**龙头状态**: {leader_status}" if leader_status else ""
|
||
signal_lines = "\n".join([f" • {s}" for s in signals])
|
||
|
||
card = {
|
||
"config": {"wide_screen_mode": True},
|
||
"header": {
|
||
"template": dir_color,
|
||
"title": {"tag": "plain_text", "content": f"{dir_emoji} {direction}确认 — {coin_name}"},
|
||
},
|
||
"elements": [
|
||
{
|
||
"tag": "div",
|
||
"text": {
|
||
"tag": "lark_md",
|
||
"content": f"**方向**: {dir_emoji} {direction}\n**价格**: ${price}{sector_line}{leader_line}\n\n**确认信号**:\n{signal_lines}{entry_lines}",
|
||
},
|
||
},
|
||
{
|
||
"tag": "action",
|
||
"actions": [
|
||
{
|
||
"tag": "button",
|
||
"text": {"tag": "plain_text", "content": f"🔥 {symbol.replace('/USDT','')} 爆发确认"},
|
||
"type": "danger",
|
||
}
|
||
],
|
||
},
|
||
],
|
||
}
|
||
return push_card(card)
|
||
|
||
|
||
def push_recommendation_state_alert(item, title_prefix=None):
|
||
"""主链路推荐状态推送:只渲染 DB/API 已派生好的状态,不做推荐判断。"""
|
||
if not item:
|
||
return True, {"skipped": True, "reason": "empty_mainline_item"}
|
||
symbol = item.get("symbol", "")
|
||
coin = symbol.replace("/USDT", "")
|
||
execution_status = item.get("execution_status", "")
|
||
action_status = item.get("action_status", "")
|
||
execution_label = item.get("execution_label", "") or action_status or execution_status
|
||
if execution_status == "buy_now":
|
||
color, title = "blue", title_prefix or "入场窗口"
|
||
elif execution_status == "wait_pullback":
|
||
color, title = "yellow", title_prefix or "观察池:等回踩"
|
||
elif execution_status == "observe":
|
||
color, title = "blue", title_prefix or "观察池更新"
|
||
else:
|
||
color, title = "grey", title_prefix or "状态更新"
|
||
|
||
entry_plan = item.get("entry_plan") or {}
|
||
price = item.get("current_price") or item.get("entry_price") or 0
|
||
entry_ref = entry_plan.get("entry_price") if execution_status == "wait_pullback" else item.get("entry_price")
|
||
if not entry_ref:
|
||
entry_ref = item.get("entry_price") or entry_plan.get("entry_price") or 0
|
||
risk_line = entry_plan.get("stop_loss") or item.get("stop_loss") or 0
|
||
space_ref = entry_plan.get("tp1") or item.get("tp1") or 0
|
||
signals = item.get("signals") or []
|
||
if isinstance(signals, str):
|
||
try:
|
||
signals = json.loads(signals)
|
||
except Exception:
|
||
signals = [signals]
|
||
signal_lines = "\n".join([f" • {x}" for x in signals[:5]]) or " • 主链路状态更新"
|
||
rec_id = item.get("id", "")
|
||
reason = item.get("execution_reason", "")
|
||
ver = item.get("strategy_version", "")
|
||
card = {
|
||
"config": {"wide_screen_mode": True},
|
||
"header": {
|
||
"template": color,
|
||
"title": {"tag": "plain_text", "content": f"{title} — {coin}"},
|
||
},
|
||
"elements": [
|
||
{
|
||
"tag": "div",
|
||
"text": {
|
||
"tag": "lark_md",
|
||
"content": (
|
||
f"**币种**: {symbol}\n"
|
||
f"**主链路状态**: {execution_label}\n"
|
||
f"**当前价**: ${price}\n"
|
||
f"**参考价**: ${entry_ref} | **风险边界**: ${risk_line} | **上方空间参考**: ${space_ref}\n"
|
||
f"**推荐ID**: #{rec_id} | **版本**: {ver}\n"
|
||
f"**说明**: {reason}\n\n"
|
||
f"**信号摘要**:\n{signal_lines}"
|
||
),
|
||
},
|
||
}
|
||
],
|
||
}
|
||
return push_card(card)
|
||
|
||
|
||
def push_altcoin_accelerating_alert(symbol, price, signals, score, sector="", leader_status="", direction="多头启动"):
|
||
"""
|
||
加速信号推送 — 只做做多🟢
|
||
⚠️ 用户要求:不再推送到飞书,此函数保留但只写日志
|
||
"""
|
||
coin_name = symbol.replace('/USDT', '')
|
||
print(f"[飞书跳过] 🟠 加速信号 — {coin_name} @ ${price} 评分{score}/20 (用户要求不推送)")
|
||
return True, {"skipped": True, "reason": "用户要求不推送加速信号"}
|
||
|
||
|
||
def push_altcoin_sector_alert(hot_sectors, leaders_info):
|
||
"""
|
||
推送板块联动告警
|
||
⚠️ 用户要求:不再推送到飞书,此函数保留但只写日志
|
||
"""
|
||
print(f"[飞书跳过] 🔵 板块联动信号 — {len(hot_sectors)}个板块 (用户要求不推送)")
|
||
return True, {"skipped": True, "reason": "用户要求不推送板块联动"}
|
||
|
||
|
||
def push_altcoin_tp_sl_alert(symbol, current_price, entry_price, pnl_pct, action_status, signals, stop_loss=0, tp1=0, tp2=0):
|
||
"""推送交易执行告警 — 可即刻买入 + 🆕v1.7.8 跟踪止盈触发。止盈/止损/衰减只落库展示,不发飞书。"""
|
||
if action_status not in ("可即刻买入", "跟踪止盈"):
|
||
print(f"[飞书跳过] {symbol} {action_status} — 用户要求止盈/止损/衰减不推送,只在网站展示")
|
||
return True, {"skipped": True, "reason": "only_buy_now_and_trailing_stop_push_enabled"}
|
||
|
||
# v1.7.8: 跟踪止盈用独立的醒目卡片
|
||
if action_status == "跟踪止盈":
|
||
coin = symbol.replace("/USDT", "")
|
||
signal_lines = "\n".join([f" • {s}" for s in signals])
|
||
trail_info = f"入场${entry_price:.4f} → 当前${current_price:.4f}"
|
||
if pnl_pct > 0:
|
||
trail_info += f"\n**累计盈利: +{pnl_pct:.2f}%** 📈"
|
||
elif pnl_pct < 0:
|
||
trail_info += f"\n**保本出场: {pnl_pct:.2f}%**"
|
||
|
||
card = {
|
||
"config": {"wide_screen_mode": True},
|
||
"header": {
|
||
"template": "red",
|
||
"title": {"tag": "plain_text", "content": f"🎯 跟踪止盈触发 — {coin}"},
|
||
},
|
||
"elements": [
|
||
{
|
||
"tag": "div",
|
||
"text": {
|
||
"tag": "lark_md",
|
||
"content": f"{trail_info}\n\n**信号详情**:\n{signal_lines}\n\n💡 跟踪止盈触发,建议立即平仓锁定利润!",
|
||
},
|
||
},
|
||
{
|
||
"tag": "action",
|
||
"actions": [
|
||
{
|
||
"tag": "button",
|
||
"text": {"tag": "plain_text", "content": f"🎯 {coin} 跟踪止盈"},
|
||
"type": "danger",
|
||
}
|
||
],
|
||
},
|
||
],
|
||
}
|
||
return push_card(card)
|
||
|
||
# 当前只保留入场时机到位推送
|
||
event_config = {
|
||
"可即刻买入": ("blue", "🟢", "入场时机到位"),
|
||
}
|
||
cfg = event_config.get(action_status, ("blue", "⚠️", action_status))
|
||
color, emoji, title_prefix = cfg
|
||
|
||
coin = symbol.replace("/USDT", "")
|
||
signal_lines = "\n".join([f" • {s}" for s in signals])
|
||
|
||
pnl_emoji = "📈" if pnl_pct > 0 else "📉" if pnl_pct < 0 else "➡️"
|
||
price_lines = f"**入场价**: ${entry_price} → **当前价**: ${current_price} → **盈亏**: {pnl_emoji} {pnl_pct}%"
|
||
if stop_loss > 0:
|
||
price_lines += f"\n**止损**: ${stop_loss}"
|
||
if tp1 > 0:
|
||
price_lines += f"\n**止盈1**: ${tp1}"
|
||
|
||
card = {
|
||
"config": {"wide_screen_mode": True},
|
||
"header": {
|
||
"template": color,
|
||
"title": {"tag": "plain_text", "content": f"{emoji} {title_prefix} — {coin}"},
|
||
},
|
||
"elements": [
|
||
{
|
||
"tag": "div",
|
||
"text": {
|
||
"tag": "lark_md",
|
||
"content": f"{price_lines}\n\n**操作建议**: {action_status}\n\n**信号详情**:\n{signal_lines}",
|
||
},
|
||
},
|
||
],
|
||
}
|
||
return push_card(card)
|
||
|
||
|
||
def push_altcoin_exhaustion_alert(symbol, current_price, pnl_pct, exhaustion):
|
||
"""
|
||
推送趋势衰减告警 — ⚠️ 橙色卡片
|
||
"""
|
||
coin = symbol.replace("/USDT", "")
|
||
severity = exhaustion.get("severity", "low")
|
||
sev_emoji = "⚠️" if severity == "medium" else "🔴"
|
||
ex_signals = exhaustion.get("signals", [])
|
||
signal_lines = "\n".join([f" • {s}" for s in ex_signals])
|
||
|
||
card = {
|
||
"config": {"wide_screen_mode": True},
|
||
"header": {
|
||
"template": "orange",
|
||
"title": {"tag": "plain_text", "content": f"{sev_emoji} 趋势衰减 — {coin}"},
|
||
},
|
||
"elements": [
|
||
{
|
||
"tag": "div",
|
||
"text": {
|
||
"tag": "lark_md",
|
||
"content": f"**当前价**: ${current_price} | **盈亏**: {pnl_pct}%\n\n**衰减信号**:\n{signal_lines}\n\n💡 建议:关注止盈机会,趋势可能即将反转",
|
||
},
|
||
},
|
||
],
|
||
}
|
||
return push_card(card)
|
||
|
||
|
||
def push_sentiment_alert(alert):
|
||
"""
|
||
推送舆情异动卡片 — 📢 蓝色信息卡
|
||
alert: {"type": "holding_trending"|"new_trending", "symbol", "name", "trend_rank", "alert"}
|
||
"""
|
||
coin = alert["symbol"].replace("/USDT", "")
|
||
alert_type = alert["type"]
|
||
emoji = "🔔" if alert_type == "holding_trending" else "🆕"
|
||
color = "red" if alert_type == "holding_trending" else "blue"
|
||
|
||
extra = ""
|
||
if alert_type == "holding_trending":
|
||
extra = f"\n⚠️ 持仓币进入热搜,关注价格异动"
|
||
|
||
card = {
|
||
"config": {"wide_screen_mode": True},
|
||
"header": {
|
||
"template": color,
|
||
"title": {"tag": "plain_text", "content": f"{emoji} 舆情异动 — {coin}"},
|
||
},
|
||
"elements": [
|
||
{
|
||
"tag": "div",
|
||
"text": {
|
||
"tag": "lark_md",
|
||
"content": (
|
||
f"**{alert['name']}** 进入 CoinGecko Trending #{alert['trend_rank']}\n"
|
||
f"{alert['alert']}{extra}\n\n"
|
||
f"💡 消息面热度上升,建议结合技术面判断入场时机"
|
||
),
|
||
},
|
||
},
|
||
],
|
||
}
|
||
return push_card(card)
|
||
|
||
|
||
def push_event_driven_alert(event, result, rec_id=0):
|
||
"""事件驱动舆情触发选币推送。重大消息触发后,根据技术检查结果分为推荐/观察/风险。"""
|
||
symbol = event.get("symbol", "")
|
||
coin = symbol.replace("/USDT", "")
|
||
decision = result.get("decision", "observe")
|
||
importance = event.get("importance", "")
|
||
title = event.get("title", "")
|
||
source = event.get("source", "")
|
||
url = event.get("url", "")
|
||
published_at = event.get("published_at", "")
|
||
price = result.get("price", 0)
|
||
score = result.get("score", 0)
|
||
reason = result.get("reason", "")
|
||
signals = result.get("signals", [])
|
||
entry_plan = result.get("entry_plan", {}) or {}
|
||
|
||
if decision == "recommend":
|
||
color, emoji, headline = "red", "🚨", "重大舆情触发:可交易机会"
|
||
elif decision == "risk":
|
||
color, emoji, headline = "orange", "⚠️", "重大舆情风险:不建议追"
|
||
else:
|
||
color, emoji, headline = "blue", "👀", "重大舆情观察:等待技术确认"
|
||
|
||
signal_lines = "\n".join([f" • {s}" for s in signals[:8]])
|
||
link_line = f"\n**来源链接**: [查看原文]({url})" if url else ""
|
||
entry_lines = ""
|
||
if entry_plan:
|
||
entry_lines = (
|
||
f"\n---\n**交易计划**:\n"
|
||
f"• 动作: {entry_plan.get('entry_action', '')}\n"
|
||
f"• 入场: ${entry_plan.get('entry_price', '')}\n"
|
||
f"• 止损: ${entry_plan.get('stop_loss', '')} ({entry_plan.get('stop_pct', '')}%)\n"
|
||
f"• TP1/TP2: ${entry_plan.get('tp1', '')} / ${entry_plan.get('tp2', '')}"
|
||
)
|
||
rec_line = f"\n**推荐ID**: #{rec_id}" if rec_id else ""
|
||
|
||
card = {
|
||
"config": {"wide_screen_mode": True},
|
||
"header": {
|
||
"template": color,
|
||
"title": {"tag": "plain_text", "content": f"{emoji} {headline} — {coin}"},
|
||
},
|
||
"elements": [
|
||
{
|
||
"tag": "div",
|
||
"text": {
|
||
"tag": "lark_md",
|
||
"content": (
|
||
f"**币种**: {symbol}\n"
|
||
f"**重要性**: {importance}级 | **来源**: {source}\n"
|
||
f"**发布时间**: {published_at}\n"
|
||
f"**消息**: {title}{link_line}\n\n"
|
||
f"**技术决策**: {reason}\n"
|
||
f"**当前价**: ${price} | **技术分**: {score}\n"
|
||
f"{rec_line}\n\n"
|
||
f"**触发信号**:\n{signal_lines}"
|
||
f"{entry_lines}"
|
||
),
|
||
},
|
||
},
|
||
],
|
||
}
|
||
return push_card(card)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
# 测试推送
|
||
print(f"Webhook URL: {FEISHU_WEBHOOK_URL[:50]}...")
|
||
print("\n测试爆发卡片推送...")
|
||
ok, result = push_altcoin_burst_alert(
|
||
"FET/USDT", 2.15,
|
||
["1H放量突破阻力(2.3倍)", "1H MACD金叉", "1H 均线多头初成", "15min 5阳线+量递增"],
|
||
{"entry_price": 2.10, "entry_method": "回踩确认", "stop_loss": 1.95,
|
||
"stop_pct": 3.0, "tp1": 2.55, "tp2": 2.80, "rr1": 3.0, "rr2": 5.0,
|
||
"risk_reward_ok": True, "current_price": 2.15, "atr_1h": 0.10},
|
||
sector="AI_DePIN", leader_status="板块龙头(AI_DePIN)",
|
||
)
|
||
print(f"爆发卡片: ok={ok}, result={result}")
|
||
|
||
print("\n测试加速卡片推送(应被跳过)...")
|
||
ok2, result2 = push_altcoin_accelerating_alert(
|
||
"ARB/USDT", 0.125,
|
||
["4H MACD金叉", "4H RSI拐点(35→52)", "板块联动: Layer2龙头启动"],
|
||
score=10, sector="Layer2",
|
||
)
|
||
print(f"加速卡片: ok={ok2}, result={result2}")
|
||
|
||
print("\n测试板块联动推送(应被跳过)...")
|
||
ok4, result4 = push_altcoin_sector_alert(["AI"], {"AI": {"leader": "FET/USDT", "leader_pct": 12.5, "is_leader_hot": True}})
|
||
print(f"板块联动: ok={ok4}, result={result4}")
|