alphax/app/integrations/feishu_push.py
2026-05-13 22:49:47 +08:00

403 lines
16 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.

"""
山寨币监控飞书卡片推送模块
通过飞书机器人 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}")