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