#05 텔레그램을 운영 콘솔로 바꾼 트레이딩 봇 리팩터링 기록

텔레그램을 운영 콘솔로 바꾼 트레이딩 봇 리팩터링 기록

일은 늘 예상보다 작은 문장에서 시작된다. 이번에도 그랬다. 처음 들여다본 건 “라이브 텔레그램 메시지 연동 부분에서 수정할 사항이 많아 보인다”는 한마디였다. 얼핏 들으면 문구 몇 줄, 버튼 몇 개, 알림 카드 조금 손보는 정도로 끝날 일처럼 보인다. 하지만 코드를 펼쳐놓고 천천히 따라가 보니, 문제는 메시지에만 머물러 있지 않았다.

텔레그램은 이미 이 봇의 운영 표면이었다. 상태를 보고, 급할 때 청산하고, 이상하면 동기화하고, 하루 손익을 확인하는 창구가 모두 그 안에 모여 있었다. 그런데 정작 내부 구조는 그 기대만큼 단단하지 않았다. 어떤 명령은 바뀐 것처럼 보였지만 실제 엔진 전체에는 일관되게 닿지 않았고, 어떤 명령은 상태를 조회하는 척하다가 다시 계산으로 이어질 여지를 품고 있었다. 예전 기능의 그림자도 남아 있어서, 더 이상 쓰지 않는 정보가 화면에 계속 떠오르곤 했다.

그래서 이번 작업은 텔레그램 메시지를 예쁘게 다듬는 정도로 끝날 수 없었다. 끝내고 보니, 이건 라이브 봇의 운영 콘솔을 다시 설계하는 일에 가까웠다.

작은 수정으로 시작했는데

라이브 봇에서 텔레그램은 바깥쪽 껍질처럼 보이지만, 실제로는 가장 깊은 곳까지 연결돼 있다. 포지션 상태, 리스크 매니저, 거래소 주문, 부분청산, 보호주문 재배치, 운영 중 확인 UX가 모두 그 끝에 매달려 있다.

그래서 /close 하나를 고치려 해도 질문이 연달아 따라왔다. 이 명령은 어느 락 안에서 실행되는가. 청산이 끝난 뒤 보호주문은 어떻게 정리되는가. 청산 직후 /sync를 누르면 상태만 복구되는가, 아니면 시그널 평가까지 다시 흘러가는가. 메시지 한 줄처럼 보였던 것이 사실은 상태 머신 전체의 문법과 맞닿아 있었던 셈이다.

결국 이번 작업의 기준은 자연스럽게 정해졌다. 보기 좋은 텔레그램이 아니라, 급한 순간에도 믿고 누를 수 있는 텔레그램. 그쪽으로 방향을 잡고 나니 무엇을 남기고 무엇을 덜어낼지도 조금씩 선명해졌다.

조용히 숨어 있던 문제들

처음 정리한 문제 목록은 생각보다 날카로웠다. 평소에는 잘 드러나지 않지만, 실제 포지션이 열려 있고 몇 초의 판단이 중요해지는 순간에야 비로소 얼굴을 내미는 종류의 문제들이었다.

  1. /config는 “변경됨”이라고 답하지만, 실제 엔진 전체가 같은 값을 보지 않을 수 있었다.
    주문 레이어와 리스크 레이어가 서로 다른 값을 들고 있으면, 운영자는 바뀌었다고 믿는데 봇은 다른 세계에 남아 있는 셈이다.
  2. /close는 수동 청산이면서도 라이브 포지션 루프와 충돌할 여지가 있었다.
    봉 처리, 1분 트레일링, 청산 후 후처리가 서로 엇갈리면 상태 불일치가 생길 수 있었다.
  3. /sync는 상태 복구 명령이라기보다 “즉시 다시 계산”에 가까웠다.
    그 구조에서는 수동 청산 직후 재진입 같은 불편한 장면이 충분히 나올 수 있었다.
  4. 메시지에는 이미 지나간 전략의 흔적이 남아 있었다.
    use_tp1=false인데도 TP1이 보이거나, 지금의 운영 흐름과 맞지 않는 설명이 그대로 남아 있었다.
  5. HTML 파싱 문제는 장애 순간일수록 알림을 더 취약하게 만들었다.
    에러 문자열 안에 <, >가 섞이는 것만으로도 중요한 메시지가 아예 실패할 수 있었다.

이런 종류의 문제는 평온한 시간에는 거의 소리가 없다. 그래서 더 위험하다. 아무 일도 없을 때는 기능처럼 보이지만, 일이 생기면 모호함이 바로 리스크가 된다.

그래서 무엇을 바꿨나

1. 먼저 개발 기준부터 고정했다

의외로 가장 먼저 손본 건 텔레그램 코드가 아니었다. 리팩터링이 길어질수록 필요한 건 “이제부터 무엇을 기준으로 맞출 것인가”에 대한 공통 바닥이기 때문이다. 그래서 레포 입구와 검증 루프를 먼저 정리했다.

  • README.md를 만들어 실행과 검증 진입점을 정리했다.
  • pyproject.toml, pytest.ini, requirements-dev.txt를 추가해 lint/test 기준을 고정했다.
  • 기본 검증 루프를 ruff check .pytest -q로 통일했다.

이 작업은 겉으로는 소박하지만, 이후 모든 수정의 톤을 바꿨다. 더 이상 “대충 고쳤으니 될 것 같다”가 아니라, 고치고 나서 같은 기준으로 확인하는 흐름이 생겼기 때문이다.

2. 명령의 의미를 다시 나눴다

운영 도구에서 가장 위험한 것은 기능 부족보다 의미의 혼선이다. 그래서 이번에는 명령 각각이 정확히 무엇을 하는지 다시 나눴다.

  • /status는 상태 조회 전용으로 정리했다.
  • /config는 쓰기 기능을 사실상 제거하고 읽기 전용 스냅샷으로 축소했다.
  • /sync는 신규 진입 평가를 하지 않고, 상태 정합성만 맞추도록 바꿨다.
  • /close는 포지션 락 안에서 실행되도록 옮겼다.

특히 /sync를 “전략 실행”에서 떼어내고 “상태 복구”로 되돌린 것이 중요했다. 운영자가 /sync를 눌렀을 때 기대하는 건 보통 새로운 판단이 아니라, 지금 이 순간의 진실이 무엇인지 정리해달라는 요청에 가깝다. 그 기대와 실제 동작이 같아지는 것이 먼저였다.

3. 부분청산을 비공식 루트가 아니라 정식 기능으로 넣었다

이전까지 부분청산은 어딘가 늘 반쯤 바깥에 놓여 있었다. 바이낸스 앱에서 손으로 줄일 수는 있지만, 그러면 봇 내부 상태를 어떻게 다시 맞출지 늘 뒤에 따라붙었다. 운영자의 손과 봇의 상태 머신이 따로 노는 구조였다.

그래서 이번엔 부분청산을 텔레그램 안으로 정식으로 데려왔다.

  • /close는 전체 청산
  • /close 25, /close 50%는 부분청산
  • 부분청산 후에는 남은 수량 기준으로 SL 보호주문을 다시 배치
  • 외부 앱에서 직접 부분청산했을 때도 /sync로 복구 가능

이 변화의 핵심은 편의성보다 일관성이다. 텔레그램에서 하든 앱에서 하든, 결국 하나의 상태 모델로 다시 모일 수 있어야 운영이 덜 불안해진다.

4. 채팅창을 상태판으로 바꿨다

이번 작업에서 가장 눈에 띄는 변화는 텔레그램 UX였다. 예전처럼 상태 메시지가 새로 쌓이는 방식 대신, 하나의 상태판을 계속 수정하는 Pinned HUD 방식으로 바꿨다.

  • 공유 HUD 메시지 1개를 만들고 가능하면 pin
  • /status와 자동 상태 갱신은 같은 메시지를 edit
  • 진입, 청산, 에러는 별도 이벤트 카드로 유지
  • /balance, /pnl은 HUD 하단 패널 on/off

이 구조로 바꾸고 나니 채팅창의 성격이 완전히 달라졌다. 더 이상 알림이 뒤엉켜 흘러가는 방이 아니라, 최신 상태가 한 장의 화면에 머무는 콘솔처럼 보이기 시작했다. 실전에서 이런 차이는 생각보다 크다. 바쁠 때는 스크롤보다 한 장의 상태판이 훨씬 믿을 만하다.

5. “왜”를 보여주는 명령을 넣었다

운영할 때 가장 자주 떠오르는 질문은 늘 비슷하다. “왜 이번 봉에서 안 들어갔지?” “왜 막혔지?” “왜 그냥 기다리고 있지?” 그래서 이번에는 결과만 보여주는 대신, 판단 근거를 보여주는 /why, /signal 명령을 추가했다.

이 명령은 최근 봉 기준으로 아래 상태를 한 번에 보여준다.

  • slope
  • accel
  • deadzone
  • guard
  • safe_mode
  • skip_next_entry

즉, 단순히 “롱 안 들어감”이 아니라, 왜 안 들어갔는지가 함께 남는다. 전략이 버그로 멈춘 건지, 의도한 가드가 작동한 건지, 운영자는 더 이상 감으로 짐작할 필요가 없다.

6. 버튼까지 붙여서 운영 리듬을 바꿨다

HUD가 생기고 나면, 다음은 자연스럽게 버튼이다. 지금은 상태판 아래에서 아래 액션을 바로 누를 수 있다.

  • Refresh
  • Why
  • Sync
  • Wallet
  • PnL
  • Skip
  • Close 25%
  • Close All
  • Stop

위험한 동작은 바로 실행되지 않고, 한 번 더 확인 단계를 거친다. 겉보기엔 작은 차이지만, 모바일에서 운영할 때는 이런 작은 완충 장치가 실수를 크게 줄여준다. 운영 리듬 자체가 더 차분해진다.

검증은 어떻게 가져갔나

라이브 운영 기능은 “잘 될 것 같다”는 감으로 넘기기 어렵다. 이번 작업에서는 새 기능을 붙일 때마다 테스트와 스모크 체크를 같이 끌고 갔다.

  • ruff check . 통과
  • pytest -q 통과
  • 최종 기준 자동화 테스트: 164 passed
  • --no-download 백테스트 스모크 경로 별도 검증

캐시 기반 백테스트 스모크 체크에서는 아래 결과도 다시 확인했다.

  • 2048 trades
  • +$8,383.93 PnL
  • -29.32% Max Drawdown

그리고 마지막에는 실제 서버 배포까지 마쳐서, 라이브 서비스가 다시 정상적으로 올라오는 것까지 확인했다. 이 순서가 중요했다. 기능이 추가됐다는 사실보다, 그 기능이 실전까지 도달했다는 사실이 더 중요했기 때문이다.

이번 작업이 남긴 것

이번에 가장 크게 배운 건, 운영 도구는 기능 수보다도 의미가 분명한가가 더 중요하다는 점이었다.

  1. 라이브 수동 제어는 많을수록 좋은 게 아니다.
    무엇을 하는지 모호한 명령은 결국 운영 리스크가 된다.
  2. 상태 조회와 상태 변경은 분리돼야 한다.
    /status, /sync, /close가 각자 명확한 역할을 가질 때 비로소 예측 가능성이 생긴다.
  3. 좋은 텔레그램 UX는 보기 좋은 카드가 아니라, 사고를 줄이는 인터페이스다.
    HUD, 설명형 상태판, 확인 버튼은 모두 같은 방향을 가리킨다.
  4. 결국 이런 리팩터링도 문서와 테스트가 받쳐줘야 한다.
    README, lint, pytest 같은 바닥이 없으면 기능은 늘어도 신뢰는 자라지 않는다.

마무리

처음에는 “텔레그램 메시지 쪽을 조금 정리하자”는, 말하자면 아주 가벼운 청소처럼 시작했다. 그런데 막상 끝나고 보니 방 구조 자체가 달라져 있었다. 이번 작업은 단순한 알림 UI 손질이 아니라, 라이브 트레이딩 봇의 운영 표면을 다시 그린 작업이었다.

이제 텔레그램은 단순히 메시지를 받아보는 곳이 아니다. 상태를 읽고, 이유를 확인하고, 필요할 때 조심스럽게 개입하는 장소가 됐다. 운영자는 조금 덜 불안해지고, 봇은 조금 더 예측 가능해졌다. 그 두 가지가 만나는 지점이, 이번 리팩터링이 남긴 가장 큰 변화였다.

다음 단계가 있다면 아마 여기서 조금 더 멀리 갈 것이다. watchdog 알림, /today 저널 요약, 청산 결과 카드 강화 같은 것들 말이다. 하지만 그 전에 먼저 확인하고 싶은 건 더 단순하다. 이번에 만든 이 콘솔이, 실제 운영의 밤에도 조용히 제 역할을 해내는지. 그걸 보는 일이 다음 장면이 될 것 같다.

댓글 남기기