Python을 사용하여 Polymarket 트레이딩 봇에서 중복 주문 방지하기
요약
본 튜토리얼은 Polymarket 트레이딩 봇에서 발생할 수 있는 중복 주문 문제를 해결하는 방법을 다룹니다. 단순히 지연 시간을 추가하는 대신, 멱등성 이벤트 처리, 영속적 상태 관리, 그리고 주문 조정 같은 안정적인 설계 원칙을 적용해야 합니다.
핵심 포인트
- 중복 주문은 반복되는 이벤트, 재시작, 네트워크 시간 초과 등 다양한 실패 모드에서 발생합니다.
- 단순한 지연 시간 추가로는 문제를 해결할 수 없으며, 멱등성 처리가 필수적입니다.
- 이벤트의 고유성을 보장하기 위해 트랜잭션 해시와 로그 인덱스 같은 안정적인 식별자를 사용해야 합니다.
- 처리된 이벤트는 메모리 대신 SQLite와 같은 영구 데이터베이스에 저장하여 상태를 유지해야 합니다.
Polymarket 트레이딩 봇은 동일한 이벤트를 여러 번 수신하거나, 충돌 후 재시작되거나, 주문 제출 중에 연결이 끊어질 수 있습니다. 애플리케이션이 반복되는 모든 신호를 새로운 지침으로 처리한다면, 중복 주문을 할 수 있고 의도했던 것보다 더 많은 자본을 노출할 수 있습니다.
해결책은 단순히 거래 사이에 지연 시간을 추가하는 것이 아닙니다. 안정적인 봇은멱등성 이벤트 처리(idempotent event processing), 영속적 상태(persistent state), 그리고 주문 조정(order reconciliation)이 필요합니다.
본 튜토리얼에서는 카피 트레이딩 봇을 예시로 사용하여 Python에서 이러한 안전장치를 설계하는 방법을 설명합니다. 동일한 원칙은 다른 자동화된 거래 시스템에도 적용됩니다.
중복 주문이 발생하는 이유
대상 지갑의 거래를 모니터링하고 이를 복사하는 봇을 가정해 봅시다.
봇이 매수 이벤트를 감지하고 이에 상응하는 주문을 제출합니다. 여러 실패 시나리오가 의도하지 않은 두 번째 주문을 생성할 수 있습니다.
- 반복되는 이벤트: 재연결 또는 폴링 주기 때문에 동일한 출처 활동이 다시 처리됩니다.
- 애플리케이션 재시작: 프로세스가 거래를 감지했지만, 해당 이벤트가 처리되었음을 기록하기 전에 충돌합니다.
- 네트워크 시간 초과: 주문 요청이 트레이딩 API에 도달하지만 응답이 봇에게 도착하지 않습니다. 애플리케이션은 주문이 수락되었는지 알 수 없습니다.
- 동시 워커(Concurrent workers): 두 개의 워커가 각각 완료로 기록하기 전에 동일한 이벤트를 처리합니다.
- 부분 실패: 봇이 주문을 제출하기 전에 거래를 처리된 것으로 저장했다가 충돌합니다. 재시작 시, 실제로 제출되지 않은 거래는 건너뛸 수 있습니다.
이들은 서로 다른 실패 모드입니다. 단일 인메모리(in-memory) 이벤트 집합으로는 이 모든 것을 해결할 수 없습니다.
- 모든 출처 이벤트에 안정적인 식별자 부여
지정학적 위험(geopolitical risk)만으로는 중복 주문을 방지할 수 없습니다. 트레이더는 동일한 결과에 대해 합법적으로 여러 번 구매할 수 있기 때문입니다.
다음은 간단한 Python 예시입니다:
def event_key(event: dict) -> str:
tx_hash = event.get("transaction_hash")
log_index = event.get("log_index")
```python
if tx_hash is None or log_index is None:
raise ValueError("Missing stable event identity")
...
이 예시는 소스(source)가 두 필드 모두를 노출하고, 이 조합이 관련 이벤트를 고유하게 식별한다고 가정합니다. 사용하기 전에 실제 스키마를 확인하십시오.
데이터 제공업체(data provider)가 다른 안정적인 식별자를 노출하는 경우, 그것을 사용하십시오. 신뢰할 수 있는 식별자가 없는 경우, 복합 지문(composite fingerprint)이 필요할 수 있지만, 이는 소스의 의미론적 구조를 기반으로 설계되어야 하며 합법적인 거래를 실수로 병합할 수 있습니다.
원칙은 간단합니다: 유사성(similarity)이 아닌 **식별자(identity)**를 기준으로 이벤트를 중복 제거하는 것입니다.
1. 처리된 이벤트 영구 저장하기
메모리 내 세트(in-memory set)는 프로세스가 재시작되면 사라집니다. 영구 데이터베이스(persistent database)는 봇이 이미 처리한 소스 이벤트를 기억할 수 있게 합니다.
SQLite는 단일 프로세스 Python 봇을 위한 실용적인 시작점입니다.
```python
import sqlite3
db = sqlite3.connect("bot_state.db")
db.execute("""
CREATE TABLE IF NOT EXISTS source_events (
event_key TEXT PRIMARY KEY,
status TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
)
"""
db.commit()
기본 키(primary key)는 데이터베이스가 동일한 이벤트 식별자를 두 번 저장하는 것을 방지합니다.
다음과 같이 기존 키를 무시하고 삽입하여 이벤트를 등록하려고 시도할 수 있습니다:
def register_event(event_id: str) -> bool:
cursor = db.execute(
"""
INSERT OR IGNORE INTO source_events
(event_key, status)
VALUES (?, ?)
"""
,
(event_id, "received"),
)
```python
db.commit()
return cursor.rowcount == 1
만약 함수가
이는 set을 확인한 후 나중에 삽입하는 것보다 더 신뢰할 수 있는데, 데이터베이스가 고유성을 강제하기 때문입니다. 다중 워커 시스템에서는 클레임(claim) 작업과 관련된 상태 전환에도 적절한 트랜잭션 및 동시성 처리가 여전히 필요합니다.
무엇보다 중요한 것은, 이벤트를 등록하는 것이 그 주문이 실행되었다는 것을 의미하지 않는다는 것입니다.
이러한 구분이 다음 안전장치로 이어집니다.
1. 주문 상태를 명시적으로 추적하기
견고한 봇은 이벤트와 관련된 주문의 생명주기를 표현해야 합니다.
간소화된 상태 모델에는 다음이 포함될 수 있습니다:
- "received": 소스 이벤트가 등록되었습니다.
- "validated": 이벤트가 마켓, 가격, 잔액 및 위험 검사를 통과했습니다.
- "submitting": 봇이 주문을 전송하려고 시도하고 있습니다.
- "submitted": API가 주문을 승인했습니다.
- "filled": 주문이 확정적으로 체결되었습니다.
- "rejected": 주문이 거부되었습니다.
- "unknown": 요청 결과가 아직 확립될 수 없습니다.
이러한 상태들은 상호 교환 가능한 것으로 취급되어서는 안 됩니다.
예를 들어, 제출된(submitted) 주문은 열려 있거나, 부분적으로 체결되거나, 취소될 수 있습니다. 거부된(rejected) 주문은 응답 시간이 초과된(timed out) 주문과는 다릅니다.
유용한 데이터베이스 설계는 소스 이벤트와 주문 시도를 분리합니다. 하나의 소스 이벤트가 검증에 실패하여 어떤 주문도 생성하지 않을 수 있는 반면, 유효한 이벤트는 신중하게 정의된 복구 정책 하에서 하나 이상의 주문 시도를 필요로 할 수 있습니다.
시스템은 소스 이벤트의 식별자, 의도된 마켓 및 결과, 요청 크기, 사용 가능한 경우 주문 식별자, 현재 상태, 타임스탬프 및 모든 오류 세부 정보를 기록해야 합니다.
이러한 기록은 원래의 작업을 맹목적으로 반복하지 않고도 복구를 가능하게 합니다.
1. 시간 초과를 알 수 없는 결과로 처리하기
이는 자동화된 실행에서 가장 중요한 규칙 중 하나입니다.
봇이 주문 요청을 전송했다고 가정해 봅시다. 서버가 이를 수락했지만, 응답이 도착하기 전에 네트워크 연결이 끊어집니다.
봇은 시간 초과를 감지합니다. 하지만 그 주문이 승인되었는지 여부를 알지 못합니다.
만약 같은 주문을 즉시 다시 제출한다면, 계정에 두 개의 주문이 생길 수 있습니다.
더 안전한 복구 프로세스는 다음과 같습니다:
1. 시도를 "알 수 없음(unknown)"으로 표시합니다.
2. 맹목적으로 재제출하지 않습니다.
3. 사용 가능한 식별자를 사용하여 트레이딩 API에서 주문 또는 관련 활동을 조회합니다.
4. 응답을 로컬에 저장된 상태와 조정(reconcile)합니다.
5. 원래 요청이 주문을 생성하지 않았음이 알려졌고 재시도가 유효한 경우에만 재시도합니다.
정확한 조정 절차는 현재 API와 그것이 노출하는 정보에 따라 달라집니다. 구현하기 전에 최신 "Polymarket 개발자 문서(developer documentation)"([https://docs.polymarket.com/](https://docs.polymarket.com/))를 읽어보세요.
여기에는 중요한 제한 사항이 있습니다: 로컬 데이터베이스 트랜잭션으로는 데이터베이스 업데이트와 원격 트레이딩 주문을 동시에 원자적으로(atomically) 커밋할 수 없습니다. 서버 지원의 Idempotency 메커니즘 없이는, 애플리케이션은 모든 네트워크 오류에 걸쳐 정확히 한 번 실행(exactly-once execution)을 보장할 수 없습니다.
실질적인 목표는 중복 실행 가능성을 낮추고, 모호한 결과를 감지하며, 보수적으로 복구하는 것입니다.
1. 이벤트 감지와 주문 실행 분리
전체 트레이딩 워크플로우를 WebSocket 메시지 핸들러 안에 넣지 마세요.
만약 이벤트 감지, 데이터베이스 쓰기, 위험 검사(risk checks), 네트워크 요청, 그리고 주문 제출이 모두 하나의 콜백에서 발생한다면, 느린 API 응답이 들어오는 이벤트를 처리하는 것을 방해할 수 있습니다.
더 깔끔한 아키텍처는 책임들을 분리합니다:
시장 또는 지갑 활동 $\downarrow$
이벤트 정규화(Event normalizer) $\downarrow$
영구 이벤트 레지스트리(Persistent event registry) $\downarrow$
검증 및 위험 관리(Validation and risk) $\downarrow$
주문 제출(Order submission) $\downarrow$
주문 조정(Order reconciliation) $\downarrow$
영구 상태 업데이트(Persistent state update)
큐(queue)는 컴포넌트들을 연결할 수 있지만, 큐 자체만으로는 중복 제거 메커니즘이 아닙니다. 이벤트가 한 번 이상 전달될 수도 있고, 워커(worker)가 충돌하거나 메시지가 재시도될 수도 있습니다.
지속적인 이벤트 식별자(persistent event identity)와 상태 기계(state machine)는 여전히 필수적입니다.
1. 성공적인 거래뿐만 아니라 실패 사례를 테스트하세요
정상적인 테스트 실행 중에는 작동하는 봇이라도 요청이 시간 초과되거나 프로세스가 재시작될 때 실패할 수 있습니다.
최소한 다음 시나리오들을 테스트해야 합니다:
* **중복 이벤트(Duplicate event):** 동일한 소스 이벤트를 두 번 전달합니다. 활성 처리 기록이 하나 이상 생성되지 않음을 확인하세요.
* **제출 전 충돌(Crash before submission):** 이벤트를 등록했지만 주문을 보내기 전에 애플리케이션을 재시작합니다. 복구가 안전하게 재개됨을 확인하세요.
* **제출 후 시간 초과(Timeout after submission):** 결과가 알려지지 않은 요청을 시뮬레이션합니다. 봇이 다른 제출을 고려하기 전에 조정(reconcile)하는지 확인하세요.
* **동시 처리(Concurrent processing):** 두 개의 워커가 동일한 이벤트를 동시에 수신하도록 합니다. 데이터베이스의 고유성 및 트랜잭션 처리가 중복 처리를 방지하는지 확인하세요.
* **부분 체결(Partial fill):** 봇이 모든 체결을 완전히 완료된 주문으로 취급하기보다, 체결되고 남은 수량을 추적하는지 확인하세요.
* **오래된 상태(Stale state):** 기존의 미체결 주문과 포지션이 있는 상태에서 봇을 재시작합니다. 새로운 실행을 재개하기 전에 외부 상태를 조정하는지 확인하세요.
이러한 테스트들은 목업 API 응답(mocked API responses)이나 드라이-런 환경(dry-run environment)을 사용해야 합니다. 라이브 자금을 위험에 빠뜨릴 필요는 없습니다.
1. 실패 사례를 진단할 수 있도록 관측 가능성(observability)을 추가하세요
주문이 거부되거나 이벤트가 건너뛰어질 경우, 로그에는 그 이유가 설명되어야 합니다.
소스 이벤트 식별자(source event identifier), 상태 전이(state transition), 검증 결과(validation outcome), 주문 식별자(order identifier), 요청된 수량(requested size), 응답 상태(response status), 그리고 조정 결과(reconciliation result)를 기록하세요. 절대로 개인 키(private keys), API 비밀(API secrets) 또는 기타 자격 증명은 로깅하지 마세요.
유용한 운영 메트릭에는 다음이 포함됩니다:
- 중복 소스 이벤트 감지됨.
- 주문 제출 및 거부됨.
- 결과를 알 수 없는 요청들.
- 조정(reconciliation)을 기다리는 주문들.
- 부분 체결 및 미해결된 주문 상태.
- 이벤트 감지부터 주문 확인까지 소요된 시간.
이러한 측정값들은 데이터 피드 문제인지, 실행 문제인지, 아니면 상태 관리 버그인지를 구별하는 데 도움이 됩니다.
**결론**
Polymarket 트레이딩 봇에서 중복 주문을 방지하려면 단순히 거래가 이전에 감지되었는지 여부를 확인하는 것 이상이 필요합니다.
안정적인 이벤트 식별자(stable event identities), 영속적인 고유성 제약 조건(persistent uniqueness constraints), 명시적인 주문 상태(explicit order states), 보수적인 타임아웃 복구(conservative timeout recovery), 그리고 외부 거래 시스템과의 조정(reconciliation)을 사용해야 합니다. 그런 다음 중복 전송, 재시작, 동시성(concurrency), 모호한 네트워크 결과 조건에서 시스템을 테스트하십시오.
더 진보된 자동화를 구축하는 개발자들에게 이러한 안전장치들은 처음부터 트레이딩 아키텍처의 일부가 되어야 합니다.
Dexoryn Labs는 개발자들이 기존 구현을 평가할 때 검토할 수 있는 공개
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기