"""Telegram 매매 알림.""" from __future__ import annotations import logging import os import requests logger = logging.getLogger(__name__) _API = "https://api.telegram.org/bot{token}/sendMessage" def _send(text: str) -> None: token = os.getenv("TELEGRAM_TRADE_TOKEN", "") chat_id = os.getenv("TELEGRAM_CHAT_ID", "") if not token or not chat_id: logger.warning("Telegram 설정 없음, 알림 스킵") return try: requests.post( _API.format(token=token), json={"chat_id": chat_id, "text": text, "parse_mode": "HTML"}, timeout=5, ) except Exception as e: logger.error(f"Telegram 알림 실패: {e}") def notify_buy( ticker: str, price: float, amount: float, invested_krw: int, max_budget: int = 0, per_position: int = 0, fng: int = 0, ) -> None: budget_line = ( f"운용예산: {max_budget:,}원 (포지션당 {per_position:,}원)\n" if max_budget else "" ) fng_label = ( "극탐욕" if fng >= 76 else "탐욕" if fng >= 56 else "중립" if fng >= 46 else "약공포" if fng >= 41 else "공포" if fng >= 26 else "극공포" ) if fng else "" fng_line = f"F&G: {fng} ({fng_label})\n" if fng else "" _send( f"📈 [매수] {ticker}\n" f"가격: {price:,.2f}원\n" f"수량: {amount:.8f}\n" f"투자금: {invested_krw:,.2f}원\n" f"{fng_line}" f"{budget_line}" ) def notify_sell( ticker: str, price: float, pnl_pct: float, reason: str, krw_profit: float = 0.0, fee_krw: float = 0.0, cum_profit: float = 0.0, ) -> None: trade_emoji = "✅" if pnl_pct >= 0 else "❌" cum_emoji = "💚" if cum_profit >= 0 else "🔴" _send( f"{trade_emoji} [매도] {ticker}\n" f"가격: {price:,.2f}원\n" f"수익률: {pnl_pct:+.2f}%\n" f"실손익: {krw_profit:+,.2f}원 (수수료 {fee_krw:,.2f}원)\n" f"{cum_emoji} 누적손익: {cum_profit:+,.2f}원\n" f"사유: {reason}" ) def notify_signal(ticker: str, signal_price: float, vol_mult: float, fng: int = 0) -> None: """거래량 축적 신호 감지 알림.""" from .fng import FNG_MIN_ENTRY fng_label = ( "극탐욕" if fng >= 76 else "탐욕" if fng >= 56 else "중립" if fng >= 46 else "약공포" if fng >= 41 else "공포" if fng >= 26 else "극공포" ) if fng else "" fng_line = f"F&G: {fng} ({fng_label})\n" if fng else "" warn_line = ( f"⚠️ F&G={fng} < {FNG_MIN_ENTRY} → 진입차단중\n" if fng and fng < FNG_MIN_ENTRY else "" ) _send( f"🔍 [축적감지] {ticker}\n" f"신호가: {signal_price:,.2f}원\n" f"거래량: {vol_mult:.1f}x 급증 + 2h 횡보\n" f"{fng_line}" f"{warn_line}" f"진입 목표: {signal_price * 1.048:,.2f}원 (+4.8%)" ) def notify_error(message: str) -> None: _send(f"⚠️ [오류]\n{message}") def notify_status( positions: dict, max_budget: int = 0, per_position: int = 0, cum_profit: float = 0.0, ) -> None: """정각마다 시장 레짐 + 1시간 이상 보유 포지션 현황 전송.""" from datetime import datetime import pyupbit from .market_regime import get_regime now = datetime.now().strftime("%H:%M") cum_sign = "+" if cum_profit >= 0 else "" # 시장 레짐 regime = get_regime() regime_line = ( f"{regime['emoji']} 시장: {regime['name'].upper()} " f"(score {regime['score']:+.2f}%) " f"| 조건 TREND≥{regime['trend_pct']}% / VOL≥{regime['vol_mult']}x\n" ) # F&G 지수 from .fng import get_fng, FNG_MIN_ENTRY fv = get_fng() fng_label = ( "극탐욕" if fv >= 76 else "탐욕" if fv >= 56 else "중립" if fv >= 46 else "약공포" if fv >= 41 else "공포" if fv >= 26 else "극공포" ) fng_status = "✅진입허용" if fv >= FNG_MIN_ENTRY else "🚫진입차단" fng_line = f"😨 F&G: {fv} ({fng_label}) {fng_status}\n" # 1시간 이상 보유 포지션만 필터 long_positions = { ticker: pos for ticker, pos in positions.items() if (datetime.now() - pos["entry_time"]).total_seconds() >= 3600 } cum_emoji = "💚" if cum_profit >= 0 else "🔴" budget_info = ( f"💰 운용예산: {max_budget:,}원 | 포지션당: {per_position:,}원\n" f"{cum_emoji} 누적손익: {cum_sign}{cum_profit:,.0f}원\n" if max_budget else "" ) # 포지션 없어도 레짐 정보는 전송 header = f"📊 [{now} 현황]\n{regime_line}{fng_line}{budget_info}" if not long_positions: _send(header + "1h+ 보유 포지션 없음") return lines = [header] for ticker, pos in long_positions.items(): current = pyupbit.get_current_price(ticker) if not current: continue pnl = (current - pos["buy_price"]) / pos["buy_price"] * 100 peak = pos["peak_price"] drop = (peak - current) / peak * 100 elapsed = (datetime.now() - pos["entry_time"]).total_seconds() / 3600 emoji = "📈" if pnl >= 0 else "📉" lines.append( f"{emoji} {ticker}\n" f" 현재가: {current:,.2f}원\n" f" 수익률: {pnl:+.2f}%\n" f" 최고가 대비: -{drop:.2f}%\n" f" 보유: {elapsed:.2f}h" ) _send("\n".join(lines))