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

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

다이어그램 원문 (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
다이어그램 원문 (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 서버에서의 재측정과 장기 무중단 관측이 남아 있습니다.
Screens
Screens
