bifitnex-trading/slack_notifier.py
kroutony d261b36460 Add hourly Slack trend report, log all HOLD reasons, whale correlation analysis
- hourly_trend_report.py: standalone cron script (XX:00:30) sends 1h bullish/bearish status
- slack_notifier.py: add send_market_trend_report() — simple bullish/bearish only, no entry signals
- main.py: log all 15 HOLD reasons (not just first 3) for debugging all-HOLD cycles
- backtest/whale_correlation.py: blockchain.com on-chain correlation analysis (result: no signal)
- memory/: update project memory with architecture split, cron layout, feedback

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 13:45:52 +00:00

211 lines
7.1 KiB
Python

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"<!channel>\n{text}"
_send({"text": text})
def send_market_trend_report(htf_by_symbol: dict, current_prices: dict,
indicators_5m: dict | None = None):
"""Send hourly market trend summary to Slack."""
import pandas as pd
lines = ["📈 *每小時 1h 趨勢報告*\n"]
bullish = []
bearish = []
for sym in sorted(htf_by_symbol):
df = htf_by_symbol[sym]
if df.empty or len(df) < 2:
continue
last = df.iloc[-1]
name = config.SYMBOL_NAMES.get(sym, sym)
price = current_prices.get(sym, 0)
ema9 = last.get("ema9", 0)
ema21 = last.get("ema21", 0)
adx_val = last.get("adx", 0)
rsi_1h = last.get("rsi", 50)
is_bullish = ema9 > ema21 if pd.notna(ema9) and pd.notna(ema21) else False
adx_val = adx_val if pd.notna(adx_val) else 0
rsi_1h = rsi_1h if pd.notna(rsi_1h) else 50
price_str = f"{price:.6g}" if price > 0 else "N/A"
info = f"{name}: ADX={adx_val:.0f} | RSI={rsi_1h:.0f} | ${price_str}"
if is_bullish:
bullish.append(f"🟢 {info}")
else:
bearish.append(f"🔴 {info}")
if bullish:
lines.append(f"*多頭 ({len(bullish)}):*")
lines.extend(f" {s}" for s in bullish)
lines.append("")
if bearish:
lines.append(f"*空頭 ({len(bearish)}):*")
lines.extend(f" {s}" for s in bearish)
_send({"text": "\n".join(lines)})
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"
)
})