feat: add Telegram notifications and configurable stop loss
- notify.py: buy/sell/error alerts via upbit_trading_jm_bot - STOP_LOSS_PCT: trailing stop configurable via .env (default -5%) - notify_buy/notify_sell called on every trade execution Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,9 @@
|
|||||||
ACCESS_KEY=
|
ACCESS_KEY=
|
||||||
SECRET_KEY=
|
SECRET_KEY=
|
||||||
|
|
||||||
|
# 트레일링 스탑: 최고가 대비 -N% 도달 시 청산
|
||||||
|
STOP_LOSS_PCT=5
|
||||||
|
|
||||||
# 타임 스탑: N시간 경과 후 수익률 M% 미만이면 청산
|
# 타임 스탑: N시간 경과 후 수익률 M% 미만이면 청산
|
||||||
TIME_STOP_HOURS=24
|
TIME_STOP_HOURS=24
|
||||||
TIME_STOP_MIN_GAIN_PCT=3
|
TIME_STOP_MIN_GAIN_PCT=3
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ from . import trader
|
|||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
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초마다 체크
|
CHECK_INTERVAL = 10 # 10초마다 체크
|
||||||
|
|
||||||
# 타임 스탑: N시간 경과 후 수익률이 M% 미만이면 청산
|
# 타임 스탑: N시간 경과 후 수익률이 M% 미만이면 청산
|
||||||
|
|||||||
51
core/notify.py
Normal file
51
core/notify.py
Normal file
@@ -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"📈 <b>[매수]</b> {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} <b>[매도]</b> {ticker}\n"
|
||||||
|
f"가격: {price:,.0f}원\n"
|
||||||
|
f"수익률: {pnl_pct:+.1f}%\n"
|
||||||
|
f"사유: {reason}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def notify_error(message: str) -> None:
|
||||||
|
_send(f"⚠️ <b>[오류]</b>\n{message}")
|
||||||
@@ -11,6 +11,7 @@ from typing import Optional
|
|||||||
|
|
||||||
import pyupbit
|
import pyupbit
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
|
from .notify import notify_buy, notify_sell, notify_error
|
||||||
|
|
||||||
load_dotenv()
|
load_dotenv()
|
||||||
|
|
||||||
@@ -80,9 +81,11 @@ def buy(ticker: str) -> bool:
|
|||||||
f"[매수] {ticker} @ {current:,.0f}원 | "
|
f"[매수] {ticker} @ {current:,.0f}원 | "
|
||||||
f"수량={amount} | 투자금={order_krw:,}원"
|
f"수량={amount} | 투자금={order_krw:,}원"
|
||||||
)
|
)
|
||||||
|
notify_buy(ticker, current, amount, order_krw)
|
||||||
return True
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"매수 예외 {ticker}: {e}")
|
logger.error(f"매수 예외 {ticker}: {e}")
|
||||||
|
notify_error(f"매수 실패 {ticker}: {e}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
@@ -106,10 +109,12 @@ def sell(ticker: str, reason: str = "") -> bool:
|
|||||||
f"[매도] {ticker} @ {current:,.0f}원 | "
|
f"[매도] {ticker} @ {current:,.0f}원 | "
|
||||||
f"수익률={pnl:+.1f}% | 사유={reason}"
|
f"수익률={pnl:+.1f}% | 사유={reason}"
|
||||||
)
|
)
|
||||||
|
notify_sell(ticker, current, pnl, reason)
|
||||||
del _positions[ticker]
|
del _positions[ticker]
|
||||||
return True
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"매도 예외 {ticker}: {e}")
|
logger.error(f"매도 예외 {ticker}: {e}")
|
||||||
|
notify_error(f"매도 실패 {ticker}: {e}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user