BTC 5분봉 가격 예측시장 자동 배팅 엔진 — Spring Boot 기반 실시간 퀀트 트레이딩 봇
Chainlink 가격 피드와 Polymarket CLOB을 연결한 실시간 latency-arbitrage 시스템
poly-sniper는 Polymarket의 BTC 5분봉 Up/Down 예측시장 자동 배팅 봇입니다.
Polymarket의 btc-updown-5m-* 마켓은 5분마다 "BTC 가격이 오를까, 내릴까"를 예측하는 이진 예측시장입니다. 실시간 가격은 Chainlink에서 약 1초 내외로 갱신되지만 시장의 오즈는 거래자들의 주문에 의해 수 초~수십 초 뒤에 반영됩니다. 이 비효율 구간을 100ms 주기로 탐지해 +EV 구간에만 선택적으로 진입합니다.
- 100ms 주기 실시간 스캔 루프
- Polymarket RTDS WebSocket으로 Chainlink BTC/USD 가격 수신 (좀비 연결 자동 재연결)
- EIP-712 주문 서명 Web3j 직접 구현 (SDK 미사용)
- OkHttp HTTP/2 멀티플렉싱 + 커넥션 프리워밍
- Thymeleaf 실시간 대시보드 (500ms 폴링)
- DRY-RUN / LIVE 이중 모드
| 기법 | 출처 | 용도 |
|---|---|---|
| CUSUM 필터 | Lopez de Prado, Advances in Financial ML (2018) | 횡보장 방향성 필터 |
| ATR(14) 동적 임계값 | Wilder (1978) | 변동성 적응형 진입 기준 |
| Kelly Criterion | Kelly (1956), Thorp (1969) | 엣지 비례 배팅 사이징 |
| 변동성 레짐 스위칭 | Hamilton (1989) 기반 단순화 | 4단계 레짐별 파라미터 자동 조정 |
| 서킷브레이커 | 일반 리스크 관리 | 3연패 시 5분 강제 중단 |
┌─────────────────────────────────────────────────────────────────────┐
│ poly-sniper (Spring Boot) │
│ │
│ ┌───────────────────────┐ ┌──────────────────────────┐ │
│ │ ChainlinkPriceService │ │ OddsService │ │
│ │ ─────────────────────│ │ ────────────────────────│ │
│ │ • RTDS WebSocket │ │ • Gamma API (이벤트) │ │
│ │ • BTC/USD 실시간 │ │ • CLOB API (오즈) │ │
│ │ • ATR(14) 계산 │ │ • 100ms 프리페치 │ │
│ │ • 변동성 레짐 분류 │ │ • slug 변경 감지 │ │
│ └──────────┬───────────┘ └────────────┬─────────────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ SniperScanner (100ms scan loop) │ │
│ │ ─────────────────────────────────────────────── │ │
│ │ ① 연결 확인 → ② 캔들 포지션 → ③ 변동폭 필터 │ │
│ │ ④ 횡보 감지 → ⑤ CUSUM 필터 → ⑥ 스프레드 체크 │ │
│ │ ⑦ 모멘텀 일관성 → ⑧ 오즈 상한 → ⑨ 티어드 조기진입│ │
│ │ ⑩ EvCalculator → (EV > 임계값) → executeTrade │ │
│ └────────────┬─────────────────────────────────────┘ │
│ │ │
│ ┌─────────┼──────────┐ │
│ ▼ ▼ ▼ │
│ ┌───────────┐ ┌─────────┐ ┌────────────────┐ │
│ │EvCalculator│ │BalanceSvc│ │ OrderService │ │
│ │Kelly+EV │ │USDC 동기화│ │ EIP-712 서명 │ │
│ └───────────┘ └─────────┘ │ FOK 주문 │ │
│ │ HMAC-SHA256 │ │
│ └───────┬────────┘ │
│ ▼ │
│ ┌──────────────────────┐ │
│ │ Polymarket CLOB │ │
│ └──────────┬───────────┘ │
│ │ │
│ ┌───────────────────────────┴────────────────────┐ │
│ ▼ ▼ │
│ ┌──────────────┐ ┌──────────────────┐ │
│ │ResultChecker │ (체결/결과 확인, PENDING→WIN/LOSE)│ RedeemService ││
│ │ │ │ (Python 스크립트) │ │
│ └──────────────┘ └──────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ DashboardController (Thymeleaf + REST API) │ │
│ │ GET /api/scan (500ms) | /api/stats | POST /api/toggle │ │
│ └─────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
com.sniper.btc
├── BtcSniperApplication.java # @SpringBootApplication + @EnableScheduling
│
├── controller/
│ └── DashboardController.java # 실시간 대시보드 + REST API (282 LoC)
│
├── entity/
│ └── Trade.java # JPA 엔티티 — 거래 이력
│
├── repository/
│ └── TradeRepository.java # Spring Data JPA
│
└── service/
├── ChainlinkPriceService.java # WebSocket 가격 수신 + ATR (374 LoC)
├── OddsService.java # Polymarket 오즈 캐시 + 프리페치 (278 LoC)
├── EvCalculator.java # 확률 추정 + Kelly 배팅 사이징 (275 LoC)
├── SniperScanner.java # ★ 메인 스캔 엔진 (1,043 LoC)
├── OrderService.java # EIP-712 서명 + FOK 주문 (441 LoC)
├── BalanceService.java # USDC 잔액 동기화 (299 LoC)
├── ResultChecker.java # 체결 확인 + 결과 판정 (498 LoC)
└── RedeemService.java # CTF 토큰 상환 (252 LoC)
총 3,800+ LoC (Java) + Python 스크립트 (CTF redeem)
스캐너는 매 스캔마다 10개의 직렬 필터를 통과시켜 거짓 신호를 걸러냅니다. 한 필터라도 실패하면 즉시 리턴(early-exit)하여 평균 스캔 시간을 최소화합니다.
[0] 마스터 스위치 OFF → 대시보드 토글
[1] Chainlink 연결 / 워밍업 → 첫 5M 경계 전 배팅 금지
[2] 캔들 쿨다운 → 같은 5분봉에서 1건만
[3] 서킷브레이커 (3연패 → 5분) → 손실 연쇄 차단
[4] ATR 기반 동적 최소 변동폭 → 노이즈 필터
[5] 횡보 감지 (시초가 5회+ 교차) → 방향성 없는 캔들 제외
[6] CUSUM 필터 → 누적 이탈 임계값 미만이면 스킵
[7] 캔들 포지션 (0~4초/285초+) → 시초가 동기화 대기 + 마감 차단
[8] 스프레드 검증 (upOdds+downOdds ≤ 1.05)
[9] 모멘텀 일관성 (10틱, 레짐별 동적 임계값)
+ 모멘텀 방향 ≠ 가격 방향이면 스킵 (mean-reversion 함정 방지)
[10] 오즈 상한 (> 60¢이면 손익비 불리 → 스킵)
횡보장에서 가짜 방향성에 속지 않기 위해 누적합(CUSUM) 필터를 구현했습니다.
// SniperScanner.java — trackCusum()
double ret = ((currentPrice - lastCusumPrice) / lastCusumPrice) * 100;
cusumPos = Math.max(0, cusumPos + (ret - CUSUM_EXPECTED_RETURN)); // 상승 누적
cusumNeg = Math.min(0, cusumNeg + (ret + CUSUM_EXPECTED_RETURN)); // 하락 누적
double h = chainlink.getATRPct() * params[3]; // 임계값 = ATR × 레짐별 배율
if (cusumPos > h || Math.abs(cusumNeg) > h) {
cusumTriggered = true; // 유의미한 방향성 감지
}왜 이게 중요한가? — 단순 변동폭 필터는 "오르락내리락 ±0.1%" 같은 횡보장에서도 트리거됩니다. CUSUM은 한 방향으로 누적 이탈해야만 발동하므로, 진짜 추세가 생길 때까지 진입을 보류합니다. 각 캔들 시작 시 cusumPos, cusumNeg를 0으로 리셋하여 캔들별로 독립 판정합니다.
ATR(14) 퍼센트 값에 따라 시장을 4단계 레짐으로 분류하고, 각 레짐마다 5개 파라미터를 동적으로 조정합니다.
| 레짐 | ATR% 범위 | entryMult | rangeMult | momentumMin | cusumMult | gapAdj |
|---|---|---|---|---|---|---|
| LOW 🟢 | < 0.04% | 0.40 | 0.25 | 0.35 | 0.35 | −0.01 |
| NORMAL 🔵 | 0.04–0.10% | 0.50 | 0.30 | 0.40 | 0.40 | 0.00 |
| HIGH 🟡 | 0.10–0.18% | 0.60 | 0.35 | 0.50 | 0.50 | +0.01 |
| EXTREME 🔴 | ≥ 0.18% | 0.70 | 0.40 | 0.60 | 0.60 | +0.02 |
조용한 시장에선 공격적으로, 격동기엔 방어적으로 — Sharpe Ratio 최적화를 염두에 둔 파라미터 적응입니다.
엣지가 클 때만 크게 배팅하는 Kelly 공식을 오즈 스케일링으로 보정했습니다.
// EvCalculator.java — calcBetSize()
double kellyFraction = ev / (payout - 1.0); // 이론 Kelly
double kellyMultiplier = ev >= 1.0 ? 0.35 :
ev >= 0.5 ? 0.30 :
ev >= 0.3 ? 0.25 : 0.20; // Fractional Kelly
double safeFraction = kellyFraction * kellyMultiplier;
// 잔액/시드 비율에 따른 티어 상한
double ratio = balance / initialBalance;
double maxFraction = ratio < 1.0 ? 0.03 : // 시드 이하: 안정
ratio < 2.0 ? 0.04 : // ~2배: 성장
ratio < 5.0 ? 0.05 : 0.06; // 5배+: 풀 공격
// 오즈 스케일링 — 싼 오즈(30¢)일수록 엣지가 크므로 더 과감하게
double oddsScale = clamp(0.50 / marketOdds, 0.80, 2.0);
maxFraction = Math.min(maxFraction * oddsScale, 0.12); // 최대 12% 하드캡캔들 시작 후 15~40초 구간은 특별한 구간입니다. 가격은 이미 움직였지만 오즈는 아직 반영되지 않은 비효율의 창입니다.
// 15초~: Tier 1 — 큰 움직임(≥0.10%) + 매우 싼 오즈(≤45¢) → 즉시 진입
// 30초~: Tier 2 — 중간 움직임(≥0.08%) + 적당 오즈(≤50¢) → 진입
// 40초~: 기본 진입 허용
// 285초~: 마감 15초 차단 (마켓 정산/교체 구간)Polymarket CLOB은 FOK(Fill-Or-Kill) 주문을 지원합니다. 체결 실패 시 scan() 재진입 없이 executeTrade() 내부에서 직접 재시도하여 오즈 상한 필터의 재평가를 회피합니다.
1차 주문 실패 → +2틱 올려서 재시도 (50ms 대기)
2차 주문 실패 → +2틱 추가 (누적 +4틱)
3차 주문 실패 → 이 캔들 포기
60¢ 초과 시 → 손익비 불리, 즉시 포기
Polymarket CLOB은 EIP-712 구조화 서명을 요구합니다. SDK 없이 Web3j만으로 처음부터 구현했습니다.
// OrderService.java — 핵심 발췌
private static final String ORDER_TYPE_STRING =
"Order(uint256 salt,address maker,address signer,address taker," +
"uint256 tokenId,uint256 makerAmount,uint256 takerAmount," +
"uint256 expiration,uint256 nonce,uint256 feeRateBps," +
"uint8 side,uint8 signatureType)";
private static final byte[] ORDER_TYPE_HASH_BYTES =
Hash.sha3(ORDER_TYPE_STRING.getBytes(StandardCharsets.UTF_8));| 최적화 | 효과 |
|---|---|
| Credentials 사전 캐싱 | 서명마다 Credentials.create() 호출 생략 |
| DomainSeparator 1회 계산 | EIP-712 도메인 해시 프리빌드 |
| HMAC 키 SecretKeySpec 캐시 | 인증 서명마다 키 초기화 생략 |
| padded address 바이트 프리빌드 | maker/signer/taker 32바이트 패딩 재사용 |
| tokenId BigInteger 프리파싱 | 오즈 변경 시에만 재계산 |
| HTTP/2 멀티플렉싱 + ConnectionPool | TCP/TLS 핸드셰이크 제거 |
| 커넥션 프리워밍 | 애플리케이션 시작 시 워밍업 요청 |
결과: 스캔 → 서명 → 주문 전송까지 평균 수 ms 이내.
| 계층 | 기술 |
|---|---|
| 런타임 | Java 17 (toolchain) |
| 프레임워크 | Spring Boot 3.4.2, Spring Web, Spring Data JPA, Thymeleaf |
| 블록체인 | Web3j 4.10.3 (EIP-712 서명) |
| HTTP | OkHttp 4.12.0 (HTTP/2, 커넥션 풀) |
| 직렬화 | Jackson Databind |
| DB | H2 (파일 기반, AUTO_SERVER 모드) |
| 빌드 | Gradle 8.x (Spring Dependency Management) |
| 유틸 | Lombok (불변 객체, 로깅) |
| 정산 | Python 3 (py_clob_client, Builder SDK) |
- JDK 17+
- Python 3.9+ (CTF redeem 스크립트용)
- Polymarket 계정 + CLOB API 크레덴셜
- (선택) 프록시 지갑 funder 주소
export POLY_PRIVATE_KEY=0x... # EOA 프라이빗 키
export POLY_API_KEY=... # CLOB API 키
export POLY_API_SECRET=... # CLOB 시크릿
export POLY_PASSPHRASE=... # CLOB 패스프레이즈
export BUILDER_API_KEY=... # Builder API (선택)
export BUILDER_SECRET=...
export BUILDER_PASSPHRASE=...# 1. 저장소 클론
git clone https://github.com/grbuguj/poly-sniper.git
cd poly-sniper
# 2. DRY-RUN 모드 확인 (application.yml)
# sniper.dry-run: true ← 실거래 전 반드시 확인
# 3. 빌드 + 실행
./gradlew bootRun
# 4. 대시보드 접속
open http://localhost:8090
# H2 콘솔: http://localhost:8090/h2sniper:
dry-run: true # DRY-RUN / LIVE 스위칭
initial-balance: 80.0 # 초기 시드 ($)
scan-interval-ms: 100 # 스캔 주기
min-bet: 1.0 # 최소 배팅
max-bet: 50.0 # 최대 배팅 (유동성 한계)
odds-cache-ttl-ms: 500 # 오즈 캐시 TTL
http-timeout-ms: 2000실시간 Thymeleaf 대시보드는 2가지 폴링 주기로 서버 부하를 분산합니다.
GET /api/scan(500ms 폴링, DB 조회 없음)- 스캔 속도 (scans/sec), 마지막 필터 히트, ATR%, CUSUM S±, 변동성 레짐
GET /api/stats(5s 폴링, DB 집계)- 뱅크롤, ROI, 승률, 전체 거래 이력, 이퀄리티 커브
| Method | Path | 설명 |
|---|---|---|
GET |
/ |
Thymeleaf 대시보드 |
GET |
/api/scan |
경량 스캔 메트릭 (500ms 폴링용) |
GET |
/api/stats |
전체 통계 + 거래 이력 |
POST |
/api/toggle |
스나이퍼 ON/OFF |
POST |
/api/reset |
통계 초기화 |
GET |
/api/test/balance |
Polymarket 실잔액 조회 |
POST |
/api/test/order |
수동 주문 (디버깅용) |
GET |
/api/test/redeem |
수동 CTF 상환 |
왜 Spring Boot? — 단일 인스턴스 + 단순 스케줄링이면 충분한 규모. JPA, Thymeleaf, Actuator 같은 배터리 포함 생태계가 빠른 개발에 유리. 스캔 루프는 Spring 관리 밖에서 ScheduledExecutorService로 돌려 오버헤드 제거.
왜 H2 파일 DB? — 거래 이력 몇천 건 수준이라 외부 DB는 오버엔지니어링. 재시작 시 이력 보존 필요해서 인메모리 불가. AUTO_SERVER=TRUE로 외부 H2 Console 접근 가능해서 디버깅 편함.
왜 Web3j로 EIP-712 직접? — Polymarket 공식 SDK가 Python/TypeScript만 제공. Java에서 바이트 단위로 직접 짜서 서명 경로 전체를 통제하고 지연을 최소화.
왜 FOK? — 5분봉 마켓은 오즈가 급변해서 미체결 대기가 손해. FOK는 슬리피지 노출 제거 + 부분 체결 복잡도 제거. 실패 시 즉시 다음 기회로.
- 예측시장/파생상품 거래는 원금 손실 가능성이 있습니다
- 일부 국가(미국 등)에서는 Polymarket 접근이 제한됩니다
- 프라이빗 키 관리에 주의하세요 (
.env를 커밋하지 마세요) - 이 소프트웨어의 사용으로 인한 금전적 손실에 대해 저자는 책임지지 않습니다
- Marcos López de Prado. Advances in Financial Machine Learning (Wiley, 2018) — CUSUM filter
- J. L. Kelly Jr. A New Interpretation of Information Rate (Bell System Technical Journal, 1956)
- Edward O. Thorp. Optimal Gambling Systems for Favorable Games (1969) — Fractional Kelly
- J. Welles Wilder. New Concepts in Technical Trading Systems (1978) — ATR
- EIP-712: Typed structured data hashing and signing
- Polymarket CLOB Documentation
Claude와 페어 프로그래밍으로 만들었습니다. 전략 설계(CUSUM 필터 도입, 변동성 레짐 스위칭, Kelly 기반 사이징 등)와 파라미터 튜닝은 실거래 관찰하면서 직접 결정했고, 구현은 AI와 주고받으며 진행했습니다.
☕ Java 17 · 🍃 Spring Boot 3.4.2 · 🤖 Vibe-coded with Claude