All private work

Private work 02 / 05

sellbot

입금 감지 즉시 매도 봇

Status
개발 중
Stack
Python · asyncio · httpx · websockets · PyJWT · aiosqlite · pydantic
Access
코드 비공개 · 요청 시 열람 가능열람 요청은 아래 연락처로
OBSERVE 모드 패널: 매도 방식 선택, 거래소·티커 arm, 대기 중인 arm과 최근 job (가상 티커)

Overview

DEX에서 산 코인을 국내 거래소로 보내면 입금이 반영될 때까지 앱을 붙잡고 기다려야 하고, 반영 시점은 예측할 수 없습니다. sellbot은 사용자가 보내기 전에 거래소와 티커를 지정(arm)해 두면 반영 순간에 매도합니다. 목표는 반영부터 매도까지의 시간을 줄이는 것이고, 팔면 안 되는 것을 파는 일이 없도록 막는 설계에 가장 많은 공을 들였습니다.

Features

  • 입금 감지를 여러 신호로 동시에: Private WebSocket, 잔고 백업 폴링, 입금내역 폴링을 하나의 이벤트 버스로 모으고 먼저 조건을 만족한 신호에서 발사
  • IDLE/HOT 모드: 진행 중인 입금이 보이는 코인만 HOT으로 올려 호가 구독과 고빈도 잔고 폴링을 켜서 rate limit 예산을 아낌
  • 세 가지 매도 방식: 시장가 전량, 최우선 매수호가 IOC, 최우선 매도호가 지정가 추격
  • OBSERVE/LIVE 이중 모드: OBSERVE는 발사 판정까지 똑같이 돌고 "이 시각에 발사했을 것"만 기록, 신호별 수신 시각을 episode 단위로 남김
  • 로컬 패널·텔레그램 알림·재시작 복구: 진행 중이던 주문을 클라이언트 주문 ID로 조회해 결과를 확정한 뒤 이어서 처리

Architecture

아키텍처: 신호 소스 → 이벤트 버스 → 트리거
Private WS, 잔고 폴링, 입금내역 감시가 이벤트 버스로 모이고, 트리거가 OBSERVE면 기록기로, LIVE면 매도 실행기로 보내는 구조도
다이어그램 원문 (Mermaid)
flowchart TB
  subgraph EX["거래소 (업비트 · 빗썸)"]
    PWS["Private WS<br/>myAsset · myOrder"]
    REST["REST API<br/>잔고 · 입금내역 · 호가 · 주문"]
    OBWS["Public WS<br/>orderbook"]
  end
  subgraph SRC["신호 소스"]
    AWS["asset_ws"]
    BP["balance_poll<br/>(HOT 전용)"]
    DW["deposit_watch"]
    OB["orderbook_ws<br/>최우선 호가 캐시"]
  end
  PWS --> AWS
  REST --> BP
  REST --> DW
  OBWS --> OB
  AWS --> BUS
  BP --> BUS
  DW --> BUS
  BUS["이벤트 버스<br/>monotonic 수신 시각 기록"]
  BUS --> TRG["트리거<br/>발사 조건 · 코인별 락<br/>DB UNIQUE 중복 방지"]
  BUS --> REC["지연 기록기"]
  DW -. pending 입금 .-> MODE["모드 컨트롤러<br/>IDLE / HOT"]
  MODE -. on/off .-> BP
  MODE -. 구독 .-> OB
  TRG --> SW{"OBSERVE / LIVE"}
  SW -- OBSERVE --> REC
  SW -- LIVE --> EXE["매도 실행기<br/>market · chase · maker"]
  OB --> EXE
  EXE -- "매도 주문만" --> ADP["거래소 어댑터<br/>JWT · rate limit"]
  ADP --> REST
  REC --> DB[("SQLite<br/>arms · sell_jobs · orders<br/>signals · episodes")]
  EXE --> DB
  EXE --> TG["텔레그램 알림"]
  PANEL["로컬 패널 / CLI<br/>127.0.0.1 + Bearer 토큰"] -- arm · disarm · style --> DB
  DB -- 폴링 --> MODE
원본 크기로 보기
SellJob 상태
SellJob 상태 전이: ARMED, HOT, CREDITED, SELLING을 거쳐 DONE, FAILED, NEEDS_MANUAL
다이어그램 원문 (Mermaid)
stateDiagram-v2
  direction LR
  [*] --> ARMED: arm
  ARMED --> HOT: pending 입금 감지 / 수동 HOT
  HOT --> CREDITED: 매도 가능 잔고 증가
  CREDITED --> SELLING: 발사
  SELLING --> DONE: 전량 체결 / 잔량이 최소 주문 금액 미만
  SELLING --> FAILED: 재시도 불가 주문 오류
  SELLING --> NEEDS_MANUAL: 중단 · 조회 실패 · rate limit
  HOT --> NEEDS_MANUAL: 입금 반환·보류 계열 상태
원본 크기로 보기

Key decisions

실측 경로와 실전 경로를 같은 코드로

WS와 폴링 중 무엇이 잔고 반영을 먼저 알려 주는지는 거래소 문서에 없어서, 별도 측정 스크립트 대신 봇 본체를 OBSERVE로 돌려 쟀습니다. 실측 결과 WS와 폴링은 번갈아 먼저 도착해 둘 다 유지했고, 입금내역의 진행 중 상태가 잔고보다 먼저 보여 HOT 진입의 기본 경로로 삼았습니다. 트리거는 입금내역이 아니라 잔고 신호로만 발사합니다.

중복 발사를 세 겹으로 막았다

같은 입금에 WS 이벤트와 폴링 결과가 거의 동시에 들어오므로 (거래소, 코인)별 락, SQLite UNIQUE 제약, 클라이언트 주문 ID 조회를 겹쳐 둡니다. 주문 POST가 타임아웃되면 재전송하지 않고 ID로 접수 여부를 확정합니다.

거래소 차이는 어댑터 안에 가뒀다

JWT 서명, 주문 파라미터, IOC 부분 체결 표현, rate limit 구조가 거래소마다 다르지만 core는 거래소 이름으로 분기하지 않습니다. 어댑터가 공통 모델로 바꿔 넘기고, API 필드명은 공식 문서로 확인한 결과만 씁니다.

Security

  • 출금·자산 이전 코드가 없음: 어댑터에 해당 엔드포인트 자체를 만들지 않고, 출금 권한 없는 키와 IP 허용 목록을 사용
  • 매수 경로가 없음: 주문 메서드는 매도 전용이고 side 파라미터를 받지 않아 타입 수준에서 매수 불가
  • 기본값은 "아무것도 팔지 않음": arm되지 않은 코인은 주문하지 않고, 기본 범위는 arm 이후 늘어난 수량만
  • LIVE 전환은 설정 플래그와 CLI 확인 문구의 이중 확인, 재시작 시 플래그가 없으면 OBSERVE로 강등
  • 패널은 루프백 + Bearer 토큰(상수 시간 비교), 모드 전환·임의 주문 엔드포인트 없음
  • 금액은 Decimal로 파싱하고 DB에 TEXT로 저장, 로그 마스킹 필터로 키·JWT 제거

Status

개발 중

설계한 마일스톤 코드를 모두 구현했고, 두 거래소에서 입금 감지 실측(OBSERVE)과 소액 실매도 검증(LIVE)을 마쳤습니다. 고정 IP 서버에서의 재측정과 장기 무중단 관측이 남아 있습니다.

07 — Contact

Let's
talk.

Email

이메일 주소는 스팸 수집을 막기 위해 JavaScript로 표시합니다. GitHub로도 연락할 수 있습니다.

GitHub

github.com/znan2