diff --git a/.env.example b/.env.example index 789730b..894f8de 100644 --- a/.env.example +++ b/.env.example @@ -1,6 +1,9 @@ ACCESS_KEY= SECRET_KEY= +# 트레일링 스탑: 최고가 대비 -N% 도달 시 청산 +STOP_LOSS_PCT=5 + # 타임 스탑: N시간 경과 후 수익률 M% 미만이면 청산 TIME_STOP_HOURS=24 TIME_STOP_MIN_GAIN_PCT=3 diff --git a/core/monitor.py b/core/monitor.py index d2dbd59..a4538eb 100644 --- a/core/monitor.py +++ b/core/monitor.py @@ -10,7 +10,7 @@ from . import trader logger = logging.getLogger(__name__) -STOP_LOSS_PCT = 0.10 # 최고가 대비 -10% → 트레일링 스탑 +STOP_LOSS_PCT = float(os.getenv("STOP_LOSS_PCT", "5")) / 100 # 최고가 대비 -N% → 트레일링 스탑 CHECK_INTERVAL = 10 # 10초마다 체크 # 타임 스탑: N시간 경과 후 수익률이 M% 미만이면 청산 diff --git a/core/notify.py b/core/notify.py new file mode 100644 index 0000000..4526382 --- /dev/null +++ b/core/notify.py @@ -0,0 +1,51 @@ +"""Telegram 매매 알림.""" + +from __future__ import annotations + +import logging +import os + +import requests + +logger = logging.getLogger(__name__) + +_TOKEN = os.getenv("TELEGRAM_TRADE_TOKEN", "") +_CHAT_ID = os.getenv("TELEGRAM_CHAT_ID", "") +_API = "https://api.telegram.org/bot{token}/sendMessage" + + +def _send(text: str) -> None: + 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) -> None: + _send( + f"📈 [매수] {ticker}\n" + f"가격: {price:,.0f}원\n" + f"수량: {amount}\n" + f"투자금: {invested_krw:,}원" + ) + + +def notify_sell(ticker: str, price: float, pnl_pct: float, reason: str) -> None: + emoji = "✅" if pnl_pct >= 0 else "🔴" + _send( + f"{emoji} [매도] {ticker}\n" + f"가격: {price:,.0f}원\n" + f"수익률: {pnl_pct:+.1f}%\n" + f"사유: {reason}" + ) + + +def notify_error(message: str) -> None: + _send(f"⚠️ [오류]\n{message}") diff --git a/core/trader.py b/core/trader.py index b50b3ff..22786ec 100644 --- a/core/trader.py +++ b/core/trader.py @@ -11,6 +11,7 @@ from typing import Optional import pyupbit from dotenv import load_dotenv +from .notify import notify_buy, notify_sell, notify_error load_dotenv() @@ -80,9 +81,11 @@ def buy(ticker: str) -> bool: f"[매수] {ticker} @ {current:,.0f}원 | " f"수량={amount} | 투자금={order_krw:,}원" ) + notify_buy(ticker, current, amount, order_krw) return True except Exception as e: logger.error(f"매수 예외 {ticker}: {e}") + notify_error(f"매수 실패 {ticker}: {e}") return False @@ -106,10 +109,12 @@ def sell(ticker: str, reason: str = "") -> bool: f"[매도] {ticker} @ {current:,.0f}원 | " f"수익률={pnl:+.1f}% | 사유={reason}" ) + notify_sell(ticker, current, pnl, reason) del _positions[ticker] return True except Exception as e: logger.error(f"매도 예외 {ticker}: {e}") + notify_error(f"매도 실패 {ticker}: {e}") return False