📚 토스 자동매매 개발기 시리즈 (Phase 7/전체 21편)
- Phase 0: 자동매매 봇 기초 설계
- Phase 1: 설정 분리 및 재시도 로직 통합
- Phase 2: 전략 클래스 설계
- Phase 3: 데이터 영속성 설계
- Phase 4: 백테스트 엔진 구축
- Phase 5: 읽기 전용 대시보드 & 텔레그램 제어
- Phase 6: 스케줄러 워치독
- Phase 7-1: 주문 한도 가드
- Phase 7-2: 손실 한도 자동 일시정지
- Phase 7-3 (현재 글): 알림 이중화 + 리스크 관리 연동
← 이전 글: 손실 한도 초과 시 자동 일시정지 구현하기
💡 이번 Phase 7-3 요약
- 목적: 텔레그램 단일 알림 채널의 장애 리스크를 없애기 위해 SMTP 이메일 보조 채널을 붙여서 이중 알림 파이프라인을 완성했다.
- 핵심 포인트: 알림 등급(INFO/WARNING/ERROR/CRITICAL)별로 발송 채널을 자동 분기하고, 모든 발송 실패를 Best-Effort로 흡수해서 감시 루프가 절대 멈추지 않게 했다.
- 검증: pytest 134개 전체 통과 (신규 test_notifier.py 3개 포함)
텔레그램 하나만 믿고 있었다
지난 Phase 7-2에서 손실 한도 초과 시 자동 일시정지 로직을 붙이면서 이런 생각이 들었다. “그래서 일시정지가 됐는데, 그걸 나한테 알려주는 채널이 하나뿐이면?” 텔레그램 API가 타임아웃 나거나 네트워크가 잠깐 끊기는 순간, 정작 가장 중요한 순간의 알림이 그냥 증발해버리는 구조였다.
실제로 로컬 테스트 중에 텔레그램 서버 쪽 지연으로 요청이 5초 넘게 걸리는 걸 몇 번 봤다. 그게 실서비스에서 -10% 손실 한도 초과 알림이랑 겹치면, 알림이 안 갔는지도 모르고 지나갈 수 있다는 얘기다. 그래서 이번 Phase 7-3에서는 이메일(SMTP) 보조 채널을 만들어서 알림을 이중화했다.
notifier.py로 알림 로직 전담 분리
기존에는 알림 발송 코드가 auto_trader.py 안에 흩어져 있었다. 텔레그램 발송하는 부분, 콘솔에 출력하는 부분, DB에 기록하는 부분이 각자 따로 있어서 로직을 바꿀 때마다 여기저기 손봐야 했다. 이번에 notifier.py라는 모듈로 완전히 분리했다.
class Notifier:
def send_notification(self, message: str, level: str = "INFO"):
# 1단계: 콘솔 로깅 (항상 실행)
self._log_console(message, level)
# 2단계: SQLite alerts 테이블 저장 (항상 실행)
self._save_to_db(message, level)
# 3단계: 텔레그램 발송 (항상 시도)
self._send_telegram(message, level)
# 4단계: 이메일 발송 (WARNING 이상만)
if level in ("WARNING", "ERROR", "CRITICAL"):
self._send_email(message, level)
콘솔 로깅 → DB 저장 → 텔레그램 발송 → (조건부) 이메일 발송, 이렇게 4단계 파이프라인으로 정리하니까 알림 관련 로직을 고칠 일이 생겨도 이 파일 하나만 보면 된다.
Best-Effort 원칙: 알림 때문에 매매 루프가 죽으면 안 된다
여기서 제일 신경 쓴 부분이 이거다. 알림 발송이 실패한다고 감시 루프나 백그라운드 스케줄러가 멈추면 그게 더 큰 사고다. 그래서 텔레그램/SMTP 호출은 전부 개별적으로 try-except로 감쌌다.
def _send_email(self, message: str, level: str):
try:
with smtplib.SMTP_SSL("smtp.example.com", 465, timeout=5) as server:
server.login(EMAIL_ACCOUNT, EMAIL_APP_PASSWORD)
server.sendmail(EMAIL_ACCOUNT, ALERT_RECEIVER, self._build_mime(message, level))
except (smtplib.SMTPException, socket.timeout) as e:
# 이메일 발송 실패해도 여기서 끝. 상위로 예외 전파 안 함.
self._log_console(f"[EMAIL FAIL] {e}", "WARNING")
텔레그램 쪽도 동일한 패턴이다. 요청이 타임아웃 나든, SMTP 서버 연결이 거부되든, 예외는 이 함수 안에서 끝나고 콘솔에만 기록된다. 매매 감시 루프는 알림 채널 상태와 완전히 독립적으로 계속 돌아간다.
알림 등급별 자동 분기
모든 알림을 이메일까지 보내면 스팸이 된다. 그래서 등급을 나눴다.
- INFO: 일반 시세 감시, 시뮬레이션 체결 로그 → 콘솔 + DB + 텔레그램만
- WARNING / ERROR / CRITICAL: 손실 한도 초과, 주문 한도 초과, 연속 API 오류 → 텔레그램 + 이메일 동시 발송
리스크 관리 모듈(risk_control.py)과 연동
Phase 7-2에서 만든 손실 한도 로직에 이번 알림 파이프라인을 연결했다. 보유 종목(가상 종목 삼성전자 005930 기준) 전체 누적 평가손익률이 설정 한도(기본 -10%)를 넘으면 이렇게 동작한다.
def check_loss_limit(self, current_pnl_rate: float):
if current_pnl_rate <= self.LOSS_LIMIT_THRESHOLD: # 기본 -10%
engine_control.pause()
notifier.send_notification(
f"누적 손실률 {current_pnl_rate}% 감지, 매매 엔진 일시정지",
level="ERROR"
)
이 한 줄 level="ERROR" 덕분에 손실 한도 초과 상황에서는 텔레그램뿐 아니라 이메일로도 동시에 알림이 나간다. 가상 계좌 기준으로 테스트해봤을 때(가상 평가금액 1,000,000원 → 900,000원 하락 시나리오), 두 채널 모두 정상적으로 알림이 도착하는 걸 확인했다.
3중 안전장치는 그대로 유지했다: DRY_RUN = True, 목표 매수가는 1원으로 고정, 실제 주문 API 호출 부분은 물리적으로 주석 처리된 상태다.
pytest 134개 검증
신규로 추가한 test_notifier.py에서는 3가지를 확인했다.
- INFO 레벨일 때 이메일 함수가 호출되지 않는지
- ERROR 레벨일 때 텔레그램과 이메일 함수가 둘 다 호출되는지
- 이메일 발송 중 예외가 발생해도
send_notification자체는 예외 없이 정상 반환되는지 (Best-Effort 검증)
기존 131개 테스트에 이 3개가 추가돼서 전체 134개, 100% 통과로 마무리했다.
텔레그램 단일 알림 채널의 장애 리스크를 없애려면 SMTP 이메일 보조 채널을 붙이고, 알림 등급별로 발송 채널을 자동 분기하면 된다. 모든 발송 로직은 Best-Effort로 예외를 흡수해야 매매 감시 루프가 알림 장애와 무관하게 계속 돌아간다.
FAQ
Q1. 텔레그램만 써도 충분하지 않나요?
평상시엔 충분하다. 문제는 정말 중요한 순간(손실 한도 초과 같은)에 텔레그램 API 자체가 지연되거나 실패할 가능성이 낮게나마 존재한다는 점이다. 비상 알림만큼은 이중화해두는 게 안전하다.
Q2. 이메일 발송이 매매 루프를 느리게 하지 않나요?
SMTP 타임아웃을 5초로 짧게 잡아뒀고, 실패 시 즉시 예외를 흡수하도록 만들어서 최악의 경우에도 몇 초 지연 이상으로 루프에 영향을 주지 않는다.
Q3. INFO 레벨까지 이메일로 다 받으면 안 되나요?
기술적으로는 가능하지만 스팸이 돼서 오히려 중요한 알림을 놓치게 된다. 등급 분기를 둔 이유가 이거다.
Q4. 다음 Phase에서는 뭘 다루나요?
알림/리스크 관리까지 갖췄으니 다음은 실전 운영 전 최종 체크리스트와 모니터링 대시보드 고도화를 다룰 계획이다.
“SMTP 이메일 이중화로 텔레그램 알림 장애 막기 | 토스 자동매매 개발기 #21(Phase 7-3)”에 대한 3개의 생각