import logging import time import requests import config logger = logging.getLogger(__name__) def _send(payload: dict): """Send a message to Slack via webhook.""" url = config.SLACK_WEBHOOK_URL if not url: logger.debug("Slack webhook not configured, skipping notification") return try: resp = requests.post(url, json=payload, timeout=10) if resp.status_code != 200: logger.warning("Slack webhook returned %d: %s", resp.status_code, resp.text) except Exception as e: logger.error("Failed to send Slack notification: %s", e) def send_trade_alert(trade: dict): """Send a trade execution notification.""" sym = trade.get("symbol", "?") name = config.SYMBOL_NAMES.get(sym, sym) action = trade.get("action", "?") emoji = "🟢 BUY" if action == "BUY" else "🔴 SELL" mode = trade.get("mode", "paper").upper() text = ( f"{emoji} *{name}* [{mode}]\n" f"Amount: {trade.get('amount', 0):.6f} (~{trade.get('amount_usdt', 0):.2f} USDT)\n" f"Price: {trade.get('price', 0):.6g}\n" f"Confidence: {trade.get('confidence', 0):.0%}\n" f"Reason: {trade.get('reason', 'N/A')}\n" f"Status: {trade.get('status', 'unknown')}" ) _send({"text": text}) def send_status_update(summary: str): """Send periodic portfolio status update.""" _send({"text": summary}) def send_error_alert(error_msg: str): """Send an error alert.""" _send({"text": f"⚠️ *Trading Bot Error*\n```{error_msg}```"}) def send_cycle_report( cycle_number: int, signals: list[dict], executed: list[dict], rejected: list[dict], tp_closed: list[dict], portfolio_summary: str, llm_ok: bool = True, ): """Send a unified cycle report combining analysis, execution, and portfolio status.""" lines = [f"📊 *Trading Cycle Report #{cycle_number}*\n"] # --- Take-profit / auto actions --- if tp_closed: lines.append("⚡ *自動操作:*") for tp in tp_closed: name = config.SYMBOL_NAMES.get(tp["symbol"], tp["symbol"]) pnl = tp.get("reason", "") amount_usdt = tp.get("amount_usdt", 0) pnl_str = "" if tp.get("realized_pnl") is not None: pnl_val = tp["realized_pnl"] pnl_pct_val = tp["realized_pnl_pct"] sign = "+" if pnl_pct_val > 0 else "" pnl_str = f",收益 {sign}{pnl_val:.2f} USDT ({sign}{pnl_pct_val:.2f}%)" lines.append(f" 🔴 SELL {name} — {pnl},平倉 {amount_usdt:.1f} USDT{pnl_str}") lines.append("") # --- LLM signals --- buys = [s for s in signals if s.get("action") == "BUY"] sells = [s for s in signals if s.get("action") == "SELL"] holds = [s for s in signals if s.get("action") == "HOLD"] lines.append("🤖 *LLM 分析結果:*") if buys: for s in buys: name = config.SYMBOL_NAMES.get(s["symbol"], s["symbol"]) lines.append( f" 🟢 BUY {name} — conf {s.get('confidence', 0):.0%}, {s.get('reason', '')}" ) if sells: for s in sells: name = config.SYMBOL_NAMES.get(s["symbol"], s["symbol"]) lines.append( f" 🔴 SELL {name} — conf {s.get('confidence', 0):.0%}, {s.get('reason', '')}" ) if holds: lines.append(f" ⏸️ 其餘 {len(holds)} 幣種 HOLD") if not buys and not sells: if not llm_ok: lines.append(" ⚠️ LLM 分析失敗,本次跳過。") else: lines.append(" All symbols → HOLD, no action this cycle.") lines.append("") # --- Executed trades --- has_trades = bool(executed) if executed: lines.append("✅ *實際執行:*") for t in executed: name = config.SYMBOL_NAMES.get(t["symbol"], t["symbol"]) action = t.get("action", "?") emoji = "🟢 BUY" if action == "BUY" else "🔴 SELL" amount = t.get("amount", 0) price = t.get("price", 0) amount_usdt = t.get("amount_usdt", 0) detail = f"{amount:.4f} @ {price:.6g} ({amount_usdt:.1f} USDT)" extra = "" if action == "BUY" and t.get("stop_price"): extra = f" — 止損掛 {t['stop_price']:.6g}" elif action == "SELL" and t.get("realized_pnl") is not None: pnl = t["realized_pnl"] pnl_pct = t["realized_pnl_pct"] sign = "+" if pnl_pct > 0 else "" extra = f" — 收益 {sign}{pnl:.2f} USDT ({sign}{pnl_pct:.2f}%)" tag = " [止損觸發]" if t.get("is_stop_loss") else "" lines.append(f" {emoji} {name} — {detail}{extra}{tag}") lines.append("") # --- Rejected signals --- if rejected: lines.append("❌ *未執行:*") for r in rejected: name = config.SYMBOL_NAMES.get(r["symbol"], r["symbol"]) action = r.get("action", "?") emoji = "🟢 BUY" if action == "BUY" else "🔴 SELL" lines.append(f" {emoji} {name} — {r.get('reject_reason', '未知原因')}") lines.append("") # --- Portfolio summary --- lines.append(f"💰 {portfolio_summary}") text = "\n".join(lines) # @channel when actual trades were executed if has_trades: text = f"\n{text}" _send({"text": text}) def send_startup_message(): """Notify that the bot has started.""" mode = "PAPER" if config.PAPER_TRADING else "LIVE" _send({ "text": ( f"🚀 *Trading Bot Started* [{mode} mode]\n" f"Monitoring {len(config.TOP_15_SYMBOLS)} symbols\n" f"Interval: {config.RUN_INTERVAL_MINUTES}min" ) })