TypeScript 실패 후 Rust로 만든 Polymarket 거래 봇

발행: (2026년 6월 18일 PM 03:34 GMT+9)
10 분 소요
원문: Dev.to

출처: Dev.to

요즘 제가 이야기한 트레이더가 한 말이 인상 깊게 남았습니다:

계좌를 단순히 느린 체결이나 주문 취소 실패만으로 탕진했어요.

그는 CEX perpetuals에 대해 얘기했지만, Polymarket의 CLOB에서도 동일한 문제점이 존재합니다. 단위는 밀리초가 아니라 초 단위로 측정됩니다.

제 TypeScript 봇은 Polymarket Central Limit Order Book(CLB)에서 신호 감지 → 주문 배치까지 평균 340ms를 차지했습니다. 5분짜리 시장에서 약 2.7초의 가격 오차 창이 존재한다면, 이는 전체 기회 창의 **12%**가 되며, 첫 번째 바이트가 Polymarket 서버에 도달하기 전부터 시간이 소모됩니다.

시그널을 감지할 때 **70¢**였지만 실제 체결 시점에서는 **74¢**가 되었습니다. 시장이 이미 저에게 불리하게 재평가되었습니다.

그래서 Rust로 다시 썼습니다. 이 문서는 제가 발견한 점, 변화된 점, 그리고 그urally 변하지 않은 점을 정확히 기록합니다.


배경: 제 봇이 했던 일

여러분이 이전 글(아키텍처, 켈리 기준 사이징, 마지막 60초 캡처)를 읽어보셨다면 contexte는 익숙할 것입니다. 간단히 말하면:

  • 제 봇은 Polymarket의 5분 및 15분짜리 암호화폐 업/다운 이진 시장을 타깃으로 합니다(BTC, ETH, XRP, SOL, DOGE, BNB).
  • 전략은 간단합니다: 실제 스폿 모멘텀에 비해 brevemente mispriced(오류가 있는) 시장을 찾아, 공정 가치보다 할인된 가격에 진입하고, 해결될 때까지 보유합니다.

5분짜리 “XRP Up” 시장이 70¢에 거래되지만, 실시간 스폿 모멘텀은 82% 확률을 의미하므로 **+12¢**의 엣지가 달러당 생깁니다. 하루에 50회 disciplined sizing(규칙적인 포지션 사이징)을 한다면, 수학적으로는 수익이 나지만 실제로 그 가격에 체결될 수 있어야 합니다.

문제: TypeScript 봇은 신호 감지 → 주문 전송까지 340ms가 걸렸습니다.

  • 신호 감지(가격 폴 → 임계값 체크) → 평균 18ms, 전체의 5.3%
  • 시장 데이터 JSON 파싱 → 12ms, 3.5%
  • 주문 객체 생성 → 3ms, 0.9%
  • DNS 조회 → 47ms, 13.8%
  • TLS 핸드쉐이크 → 89ms, 26.2%
  • HTTP 요청 직렬화 → 8ms, 2.4%
  • 네트워크 전송 (Polygon RPC + CLOB) → 94ms, 27.6%
  • CLOB 응답 파싱 → 11ms, 3.2%
  • 주문 확인 로깅 → 58ms, 17.1%

총합: 340ms, 100%

주요 관찰

  • **DNS 조회 (47ms)**는 거의 전적으로 제거 가능했습니다. Node.js는 기본적으로 clob.polymarket.com을 매 요청마다 재조회하고, 적절한 영구 연결과 풀링을 사용하면 거의 사라집니다.
  • **TLS 핸드쉐이크 (89ms)**도 동일 문제입니다. 새 HTTPS 연결을 매 주문에 열면서 keep‑alive를 활용하지 않았습니다. 기본 HTTP/1.1 규칙을 간과한 것이죠.
  • **주문 확인 로깅 (58ms)**은 fs.writeFileSync로 JSON 파일을 기록하는_sync 호출이었습니다. 디버깅용으로 추가했지만, 이 blocking I/O가Critical Path에 존재한다는 점은 실망스럽습니다.
  • **네트워크 전송 (94ms)**은 제가 Europe에 위치해 Polymarket 인프라가 주로 US‑east에 위치해 있어, 클로저한 인프라를 배치하지 않는 한 제거할 수 없습니다. 이는 **불변의 바닥(floor)**입니다.

따라서 47 + 89 + 58 = 194ms의 피할 수 있는 지연이 존재했으며, 이를 통해 실제 네트워크 경로와 동일한 상황에서 ~146ms 정도의 이론적 최하한(latency floor)으로 내려갈 수 있었습니다.

더 깊은 문제: Node.js의 단일 스레드 이벤트 루프가 Head‑of‑Line 차단 생성

여러 시장을 동시에 스캔할 때, 한 시장의 느린 응답이 다른 시장의 주문 배치에 지연을 줍니다. 이벤트 루프는 latency를 없애지 않고 ** interleaving**합니다.


왜 Rust인가?

Rust가 제공하는 것과 그렇지 않은 점을 명확히 하고자 합니다.

Rust가 주는 것

  • tokio의 async 런타임과 멀티스레드 executor 덕분에 진정한 병렬성. 시장 스캔과 주문 배치가 별도 스레드에서 실행되어 느린 스캔이 뜨거운 주문 경로를 차단하지 않음.
  • Zero‑cost abstractions: 컴파일러가 JavaScript 런타임이 감당할 수 없는 오버헤드를 없앱. serde로 JSON 디시리얼라이징은 복잡한 중첩 객체에서 JSON.parse보다 3~5배 빠름.
  • 명시적 연결 풀 관리: reqwest와 keep‑alive 옵션을 사용해 연결 재사용을 직접 제어.
  • GC 없는 실행: V8 GC가 예측 불가능한 latency 스파이크를 5~50ms까지 유발할 수 있으며, 평균 340ms에서는 노이즈에 불과하지만 평균 50ms이면 치명적입니다.
  • 컴파일 타임 정합성: CLOB API 응답 형태를 compile‑time에 타입으로 정의하므로, 잘못된 응답은 런타임 크래시가 아니라 컴파일 오류 또는 Result 처리 로직이 됩니다.

Rust가 주지 않는 것

  • 네트워크 물리 법칙. 빛의 속도가 빠르다고 해서 Rust가 더 빨라지는 것은 아닙니다.
  • Alpha 없음. 신호 로직이 잘못되어 있다면 Rust는 더 빠르게 실행할 뿐, 정확도는 변하지 않음.
  • 개발 루프 간소화. Rust 첫 버전은 TypeScript 대비 3배 더 오래 걸렸습니다.

Rust가 적합한 이유는 CPU와 I/O 관리 문제(연결 풀, 이벤트 루프 경합)였기 때문입니다. 네트워크 자체 latency가 bottleneck이라면 Rust의 이점은 미미할 것입니다. 하지만 연결 재사용과 이벤트 루프 경쟁을 해결함으로써 실질적인 성능 향상을 얻었습니다.


Rust 아키텍처

adelan-bot-rs/
├── Cargo.toml
├── src/
│   ├── main.rs              ← tokio runtime entrypoint
│   ├── scanner.rs           ← market scanning (parallel tasks)
│   ├── strategy/
│   │   ├── mod.rs
│   │   ├── signal.rs        ← 신호 감지 로직
│   │   └── sizing.rs        ← Kelly / fixed sizing
│   ├── market/
│   │   ├── polymarket.rs    ← CLOB + Gamma API 클라이언트
│   │   └── clob.rs          ← 주문 배치, 연결 풀
│   ├── trading/
│   │   ├── paper.rs         ← paper trading engine
│   │   └── portfolio.rs     ← 포지션 + P&L 추적
│   └── types.rs             ← 공유 타입

의존성 (Cargo.toml)

[dependencies]
tokio = { version = "1", features = ["full"] }
reqwest = { version = "0.12", features = ["json", "rustls-tls"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
chrono = { version = "0.4", features = ["serde"] }
uuid = { version = "1", features = ["v4"] }
tracing = "0.1"
tracing-subscriber = "0.3"
anyhow = "1"
tokio-tungstenite = "0.24"    # WebSocket for real‑time price feed
rust_decimal = "1"            # precise decimal arithmetic for prices

연결 풀: 가장 중요한 변경점

단일 변경 – 영구 HTTP 연결을 사용한 reqwest::Client 공유 –가 전체 latency 개선의 **약 60%**를 차지했습니다.

// polymarket.rs
use reqwest::Client;
use std::time::Duration;

pub struct PolymarketClient {
    client: Client,
    clob_base: String,
    gamma_base: String,
}

impl PolymarketClient {
    pub fn new() -> anyhow::Result<Self> {
        let client = Client::builder()
            // 연결 유지하여 요청당 TLS 핸드쉐이크를 없앱
            .tcp_keepalive(Duration::from_secs(60))
            .build()?;
        Ok(Self {
            client,
            clob_base: "https://clob.polymarket.com".into(),
            gamma_base: "https://gamma.polymarket.com".into(),
        })
    }
}

이렇게Persistent connections를 사용함으로써 TLS 핸드쉐이크와 DNS 조회 비용을 크게 줄일 수 있었습니다.

0 조회
Back to Blog

관련 글

더 보기 »

코드 리뷰가 잘못됐다

!Cover image for Code Review Gone Wronghttps://media2.dev.to/dynamic/image/width=1000,height=420,fit=cover,gravity=auto,format=auto/https%3A%2F%2Flavkesh.com%2F...