From a0990c5fd581d84b5d586ca2cad90a36cd53a96f Mon Sep 17 00:00:00 2001 From: 21in7 Date: Mon, 30 Mar 2026 16:31:12 +0900 Subject: [PATCH] feat: add Discord webhook notifications to MTF dry-run bot Sends alerts for: bot start, virtual entry (LONG/SHORT with SL/TP), and SL/TP exits with PnL. Uses existing DiscordNotifier via DISCORD_WEBHOOK_URL from .env. Also added env_file to mtf-bot docker-compose service. Co-Authored-By: Claude Opus 4.6 (1M context) --- docker-compose.yml | 2 ++ main_mtf.py | 3 +++ src/mtf_bot.py | 37 ++++++++++++++++++++++++++++++++++++- 3 files changed, 41 insertions(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index 05ae194..532ae87 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -56,6 +56,8 @@ services: image: git.gihyeon.com/gihyeon/cointrader:latest container_name: mtf-bot restart: unless-stopped + env_file: + - .env environment: - TZ=Asia/Seoul - PYTHONUNBUFFERED=1 diff --git a/main_mtf.py b/main_mtf.py index 809c32e..6e2050b 100644 --- a/main_mtf.py +++ b/main_mtf.py @@ -2,10 +2,13 @@ import asyncio import signal as sig +from dotenv import load_dotenv from loguru import logger from src.mtf_bot import MTFPullbackBot from src.logger_setup import setup_logger +load_dotenv() + async def main(): setup_logger(log_level="INFO") diff --git a/src/mtf_bot.py b/src/mtf_bot.py index fd5f072..d6f2f78 100644 --- a/src/mtf_bot.py +++ b/src/mtf_bot.py @@ -15,6 +15,7 @@ Module 4: ExecutionManager (Dry-run 가상 주문 + SL/TP 관리) """ import asyncio +import os from datetime import datetime, timezone from collections import deque from typing import Optional, Dict, List @@ -23,6 +24,7 @@ import pandas as pd import pandas_ta as ta import ccxt.async_support as ccxt from loguru import logger +from src.notifier import DiscordNotifier # ═══════════════════════════════════════════════════════════════════ @@ -540,6 +542,9 @@ class MTFPullbackBot: self.meta = MetaFilter(self.fetcher) self.trigger = TriggerStrategy() self.executor = ExecutionManager() + self.notifier = DiscordNotifier( + webhook_url=os.getenv("DISCORD_WEBHOOK_URL", ""), + ) self._last_15m_check_ts: int = 0 # 중복 체크 방지 async def run(self): @@ -552,6 +557,11 @@ class MTFPullbackBot: meta_state = self.meta.get_market_state() atr = self.meta.get_current_atr() logger.info(f"[MTFBot] 초기 상태: Meta={meta_state}, ATR={atr}") + self.notifier.notify_info( + f"**[MTF Dry-run] 봇 시작**\n" + f"심볼: `{self.symbol}` | Meta: `{meta_state}` | ATR: `{atr:.6f}`" if atr else + f"**[MTF Dry-run] 봇 시작**\n심볼: `{self.symbol}` | Meta: `{meta_state}` | ATR: N/A" + ) try: while True: @@ -615,6 +625,17 @@ class MTFPullbackBot: result = self.executor.execute(signal, current_price, atr) if result: logger.info(f"[MTFBot] 거래 기록: {result}") + side = result["action"] + sl_dist = abs(result["entry_price"] - result["sl_price"]) + tp_dist = abs(result["tp_price"] - result["entry_price"]) + self.notifier._send( + f"📌 **[MTF Dry-run] 가상 {side} 진입**\n" + f"진입가: `{result['entry_price']:.4f}` | ATR: `{result['atr']:.6f}`\n" + f"SL: `{result['sl_price']:.4f}` ({sl_dist:.4f}) | " + f"TP: `{result['tp_price']:.4f}` ({tp_dist:.4f})\n" + f"R:R = `1:{result['risk_reward']}` | Meta: `{meta_state}`\n" + f"사유: {info.get('reason', '')}" + ) else: logger.debug(f"[MTFBot] HOLD | {info.get('reason', '')}") @@ -642,21 +663,35 @@ class MTFPullbackBot: hit_tp = last["low"] <= tp if hit_sl and hit_tp: - # 보수적: SL 우선 exit_price = sl pnl = (exit_price - entry) / entry if pos == "LONG" else (entry - exit_price) / entry logger.info(f"[MTFBot] SL+TP 동시 히트 → SL 우선 청산 | PnL: {pnl*10000:+.1f}bps") self.executor.close_position(f"SL 히트 ({exit_price:.4f})") + self.notifier._send( + f"❌ **[MTF Dry-run] {pos} SL 청산**\n" + f"진입: `{entry:.4f}` → 청산: `{exit_price:.4f}`\n" + f"PnL: `{pnl*10000:+.1f}bps`" + ) elif hit_sl: exit_price = sl pnl = (exit_price - entry) / entry if pos == "LONG" else (entry - exit_price) / entry logger.info(f"[MTFBot] SL 히트 | 청산가: {exit_price:.4f} | PnL: {pnl*10000:+.1f}bps") self.executor.close_position(f"SL 히트 ({exit_price:.4f})") + self.notifier._send( + f"❌ **[MTF Dry-run] {pos} SL 청산**\n" + f"진입: `{entry:.4f}` → 청산: `{exit_price:.4f}`\n" + f"PnL: `{pnl*10000:+.1f}bps`" + ) elif hit_tp: exit_price = tp pnl = (exit_price - entry) / entry if pos == "LONG" else (entry - exit_price) / entry logger.info(f"[MTFBot] TP 히트 | 청산가: {exit_price:.4f} | PnL: {pnl*10000:+.1f}bps") self.executor.close_position(f"TP 히트 ({exit_price:.4f})") + self.notifier._send( + f"✅ **[MTF Dry-run] {pos} TP 청산**\n" + f"진입: `{entry:.4f}` → 청산: `{exit_price:.4f}`\n" + f"PnL: `{pnl*10000:+.1f}bps`" + ) # ═══════════════════════════════════════════════════════════════════