Your Trading Bot's Biggest Bottleneck Is the API Layer
By @CoinMarketMan - 14-Jul-2026
당신의 트레이딩 봇에서 가장 큰 병목은 API 레이어입니다
시그널 엔진을 다듬는 데 몇 주를 씁니다. Cohort 다이버전스, 청산 리스크 스코어링, 펀딩 레이트 극단값. 백테스트 결과는 날카롭습니다. 그런데 봇을 배포하고 48시간이 지나자 세 번의 거래 기회를 놓칩니다. 데이터 폴링 루프가 레이트 리밋에 부딪혀 20분 동안 조용히 오래된 데이터를 반환했기 때문입니다. 시그널은 괜찮았습니다. 그 주변의 아키텍처가 문제였습니다.
이것이 Hyperliquid 트레이딩 봇에서 가장 흔한 실패 패턴이며, 알파 생성과는 거의 무관합니다. 데이터 수집 레이어, 즉 API 호출을 통해 봇과 시장을 연결하는 부분에서 대부분의 봇이 무너집니다. 중복 폴링, 비효율적인 엔드포인트 선택, 캐싱 없음, 백오프 없음, 데이터가 실제로 언제 갱신되는지에 대한 인식 없음. 빌더들은 시그널을 최적화하면서 API 레이어를 배관처럼 취급합니다. 그러나 새는 배관은 설계도가 아무리 훌륭해도 집을 망칩니다.
이 가이드는 처음부터 그 레이어를 올바르게 설계하는 방법에 관한 것입니다. 구체적으로는 Hyperliquid에서 HyperTracker의 분석 API를 소비하는 봇을 위한 것입니다.
시그널보다 데이터 레이어가 더 중요한 이유
시그널 엔진은 순수 함수입니다. 같은 입력, 같은 출력, 매번. 깨끗한 데이터를 안정적인 주기로 공급하면 깨끗한 결정을 만들어냅니다. 오래된 데이터, 중복된 데이터, 또는 엔드포인트가 에러를 냈는데 코드가 그 예외를 삼켜버려 예상과 다르게 형태가 바뀐 데이터를 공급하면, 시그널 엔진은 회복할 수 없습니다. 더 이상 시장 상태를 반영하지 않는 정보를 바탕으로 확신에 찬 것처럼 보이는 결정을 만들어냅니다.
데이터 레이어는 봇과 봇이 행동의 근거로 삼는 모든 시장 정보 사이에 위치합니다. Hyperliquid에서 우리 데이터는 롤링 간격으로 갱신됩니다. 상태 스냅샷은 15분에서 20분마다, 주문 흐름 스냅샷은 5분 윈도우로. 즉, 폴링 주기, 캐싱 로직, 에러 처리가 그 특정한 갱신 리듬에 맞게 설계되어야 합니다. 데이터가 갱신되는 것보다 빠르게 폴링하면 요청 쿼터를 낭비합니다. 느리게 폴링하면 시그널 윈도우를 놓칩니다.
폴링 주기를 데이터 갱신 주기에 맞추기
봇이 할 수 있는 가장 낭비적인 일은 기반 데이터가 변하는 것보다 빠르게 엔드포인트를 폴링하는 것입니다. Cohort 메트릭이 15분에서 20분마다 갱신되는데 봇이 60초마다 폴링한다면, 열여섯 번의 호출 중 열다섯 번은 동일한 데이터를 반환합니다. 쿼터를 낭비하고, 이벤트 루프에 지연을 추가하며, 얻는 것은 없습니다.
설계의 기준이 될 갱신 일정
HyperTracker의 API는 엔드포인트 카테고리에 따라 갱신 주기가 다릅니다:
| 엔드포인트 카테고리 | 갱신 주기 | 권장 폴링 간격 |
| --- | --- | --- |
| Cohort 메트릭 (/cohort-metrics) | ~15-20분 | 5분마다 (갱신을 신속하게 포착) |
| 주문 흐름 스냅샷 (/orders) | 롤링 5분 윈도우 | 5분마다 (윈도우에 정렬) |
| 청산 리스크 (/liq-risk) | ~15-20분 | 5분마다 |
| 포지션 메트릭 (/position-metrics) | ~15-20분 | 5분마다 |
| 리더보드 (/leaderboard) | ~15-20분 | 15-30분마다 (시간 민감도 낮음) |
대부분의 엔드포인트에 대해 5분 폴링 간격을 권장하는 것은 균형의 문제입니다. 갱신된 데이터가 나오면 신속하게 포착하고 싶지만, 15분에서 20분 윈도우 안에서 정확히 언제 갱신될지는 알 수 없습니다. 5분마다 폴링하면 새 스냅샷이 나온 후 5분 이내에 포착할 수 있으며, 이는 1시간 이상의 타임프레임으로 운영하는 전략에 충분히 빠릅니다.
요청 예산 설계
모든 API 티어에는 월별 요청 한도와 분당 레이트 리밋이 있습니다. 봇의 아키텍처는 이 두 가지를 모두 준수해야 하며, 어떤 티어를 사용하느냐에 따라 제약 조건이 달라집니다.
| 티어 | 월별 요청 수 | 레이트 리밋 | 일별 유효 예산 | | --- | --- | --- | --- | | Pulse ($179/mo) | 50,000 | 60/분 | ~1,667 요청 | | Surge ($399/mo) | 150,000 | 100/분 | ~5,000 요청 | | Flow ($799/mo) | 400,000 | 200/분 | ~13,333 요청 | | Stream ($1,999/mo) | 2,000,000 | 500/분 | ~66,667 요청 |
Pulse 티어의 봇이 4개의 엔드포인트를 5분마다 폴링하면 하루에 4 x 288 = 1,152 요청을 사용합니다. 임시 쿼리, 백필, 또는 추가 자산을 위한 두 번째 폴링 루프에 515 요청이 남습니다. 여유는 있지만, 계획 없이 엔드포인트나 자산을 추가하면 빠듯해집니다.
Surge 티어에서는 동일한 4개 엔드포인트, 5분 루프가 같은 1,152 요청을 사용하지만 일별 예산이 ~5,000이므로, 여러 자산을 모니터링하거나 주요 자산의 주문 흐름을 위한 두 번째 빠른 루프를 추가할 수 있습니다. 핵심은 이것입니다. 폴링 코드를 작성하기 전에 예산을 모델링하세요. 레이트 리밋 인식 없이 구축한 봇에 나중에 끼워넣는 것은 고통스럽습니다.
요청 예산 스프레드시트 패턴
폴링 코드를 작성하기 전에 다음과 같은 표를 스케치해보세요:
엔드포인트 | 자산 수 | 간격 | 일별 호출 수
-------------------+--------+-------+----------
/cohort-metrics | 3 | 5분 | 864
/orders | 1 | 5분 | 288
/liq-risk | 3 | 5분 | 864
/position-metrics | 1 | 15분 | 96
-------------------+--------+-------+----------
합계 | | | 2,112
예산 (Pulse) | | | 1,667 <- 초과
수정: /liq-risk를 15분 간격으로 변경:
/liq-risk | 3 | 15분 | 288
새 합계 | | | 1,536 <- OK
단순한 산술 연습이지만, 대부분의 빌더는 이를 건너뛰고 봇이 프로덕션에서 429 에러를 반환할 때서야 예산 초과를 발견합니다. 먼저 모델링하고, 그 다음에 구축하세요.
캐싱: 대부분의 봇이 건너뛰는 레이어
봇은 로컬 데이터 저장소(메모리 내의 단순한 딕셔너리라도)를 유지해야 하며, 이는 폴링하는 모든 엔드포인트의 가장 최근 응답을 보관합니다. 이것은 세 가지 목적을 수행합니다.
첫 번째, 중복 제거. /cohort-metrics를 5분마다 폴링하지만 데이터가 15분에서 20분마다만 갱신된다면, 네 번의 폴링 중 세 번은 동일한 데이터를 반환합니다. 캐시는 수신된 응답을 저장된 버전과 비교하고, 실제로 변경된 경우에만 시그널 엔진으로 전파해야 합니다. 이렇게 하면 봇이 오래된 데이터에 대해 시그널을 재평가하고 불필요한 로그 노이즈를 생성하는 것을 방지합니다.
두 번째, 오래됨 감지. 모든 캐시 항목에 타임스탬프를 붙이세요. 특정 엔드포인트의 캐시가 예상 갱신 간격의 두 배보다 오래 업데이트되지 않았다면(예: 20분 갱신의 경우 40분), 무언가 잘못된 것입니다. API가 다운되었거나, 네트워크가 불안정하거나, 봇의 이벤트 루프가 막혔을 수 있습니다. 오래됨 경고는 봇이 한 시간 된 데이터로 거래하기 전에 조사를 촉발합니다.
세 번째, 우아한 저하. 폴링이 실패하면(네트워크 에러, 500, 레이트 리밋), 캐시는 여전히 이전의 좋은 응답을 보관하고 있습니다. 봇은 오래된 정보로 운영되고 있다는 명시적인 인식을 가지고 계속 운영할 수 있습니다. 그 인식이 중요합니다. 의식적으로 오래된 데이터로 거래하는 봇은 포지션 크기를 줄이거나 손절 폭을 넓힐 수 있습니다. 데이터가 오래되었다는 것을 모르는 봇은 적응할 수 없습니다.
class DataCache:
def __init__(self, stale_threshold_seconds=2400):
self.store = {}
self.stale_threshold = stale_threshold_seconds
def update(self, endpoint, data):
prev = self.store.get(endpoint)
changed = prev is None or prev["data"] != data
self.store[endpoint] = {
"data": data,
"fetched_at": time.time(),
"changed": changed,
}
return changed
def get(self, endpoint):
entry = self.store.get(endpoint)
if entry is None:
return None, True # no data, considered stale
age = time.time() - entry["fetched_at"]
return entry["data"], age > self.stale_threshold
def is_stale(self, endpoint):
_, stale = self.get(endpoint)
return stale
에러 처리와 백오프
API 호출은 실패합니다. 네트워크가 끊깁니다. 레이트 리밋에 부딪힙니다. 문제는 봇이 이 실패들을 우아하게 처리하느냐, 아니면 충돌하거나, 공격적으로 재시도하거나(쿼터를 더 소진), 최악의 경우 데이터 없이 조용히 진행하느냐입니다.
세 가지 실패 모드
레이트 리밋 (HTTP 429). API가 속도를 늦추라고 말하는 것입니다. 올바른 대응은 지수 백오프입니다. 1초 기다리고, 2초, 4초, 8초, 최대 60초로 제한. 즉시 재시도하지 마세요. 고정된 1초 간격으로 재시도하지 마세요. 이미 레이트 리밋에 걸린 상태에서 즉시 재시도할 때마다 구멍이 더 깊어집니다.
서버 에러 (HTTP 5xx). API 측에서 무언가 잘못되었습니다. 백오프로 재시도하되, 최대 3번까지. 3번 모두 실패하면, 에러를 로그하고, 캐시 항목을 잠재적으로 오래된 것으로 표시하고, 계속 진행하세요. 복구를 기다리며 전체 이벤트 루프를 차단하지 마세요.
클라이언트 에러 (HTTP 4xx, 429 제외). 요청이 잘못 형성되었습니다. 재시도해도 도움이 되지 않습니다. 디버깅을 위해 전체 요청 페이로드와 함께 에러를 로그하고, 이 폴링 주기를 건너뛰세요.
async def poll_with_backoff(endpoint, params, max_retries=3):
for attempt in range(max_retries):
try:
response = await fetch(endpoint, params)
if response.status == 200:
return response.json()
elif response.status == 429:
wait = min(2 ** attempt, 60)
logger.warning(f"Rate limited on {endpoint}, waiting {wait}s")
await asyncio.sleep(wait)
elif response.status >= 500:
wait = min(2 ** attempt, 30)
logger.warning(f"Server error {response.status} on {endpoint}")
await asyncio.sleep(wait)
else:
logger.error(f"Client error {response.status} on {endpoint}")
return None
except NetworkError as e:
logger.error(f"Network error on {endpoint}: {e}")
await asyncio.sleep(min(2 ** attempt, 30))
logger.error(f"All retries exhausted for {endpoint}")
return None
엔드포인트 선택: 필요한 것만 가져오기
HyperTracker는 21개의 엔드포인트를 제공합니다. 봇은 아마 그 중 3개에서 5개가 필요할 것입니다. 예산 낭비의 가장 큰 원인 중 하나는 시그널 엔진이 실제로 그 데이터를 소비하지 않는데도 "혹시 몰라서" 엔드포인트를 폴링하는 것입니다.
API가 아닌 시그널에서 시작하세요. 시그널 함수가 입력으로 어떤 데이터를 필요로 하는지 물어보세요. Cohort 포지셔닝을 사용한다면 /cohort-metrics를 폴링하세요. 청산 리스크를 사용한다면 /liq-risk를 폴링하세요. 리더보드 데이터를 사용하지 않는다면 /leaderboard를 폴링하지 마세요. 폴링 루프에 추가하는 모든 엔드포인트는 일별 요청 예산을 소모하고 잠재적인 실패 지점을 추가합니다.
일반적인 시그널-엔드포인트 매핑
| 시그널 유형 | 필요한 엔드포인트 | 선택적 보강 |
| --- | --- | --- |
| Cohort 다이버전스 | /cohort-metrics | /position-metrics (OI 맥락) |
| 청산 리스크 페이딩 | /liq-risk | /cohort-metrics (방향 확인) |
| 주문 흐름 불균형 | /orders | /cohort-metrics (흐름을 주도하는 주체) |
| 펀딩 레이트 극단값 | Hyperliquid 네이티브 API | /cohort-metrics (Smart Money 포지션) |
| 카피 트레이딩 / 지갑 추적 | /positions | /leaderboard (대상 지갑 선택) |
/cohort-metrics가 거의 모든 시그널 유형에서 기본 또는 보강 소스로 등장하는 것을 주목하세요. Cohort 인텔리전스를 중심으로 구축한다면, 이것이 핵심 엔드포인트입니다. 우리 데이터는 Hyperliquid의 모든 지갑을 16개의 행동 Cohort(계좌 크기별 8개, 누적 PnL별 8개)로 분류하며, Cohort 수준의 집계에 시그널 밀도가 집중되어 있습니다.
폴링 루프 구조화
폴링 루프는 봇의 심장박동입니다. 올바르게 구현하면 시스템이 몇 주 동안 원활하게 작동합니다. 잘못 구현하면 새벽 3시에 레이스 컨디션과 놓친 폴링을 디버깅하고 있을 것입니다.
엇갈린 간격 패턴
모든 엔드포인트 폴링을 같은 순간에 실행하지 마세요. 4개의 엔드포인트를 5분 주기로 폴링한다면, 간격 전체에 걸쳐 엇갈리게 배치하세요. t+0에서 /cohort-metrics, t+75s에서 /orders, t+150s에서 /liq-risk, t+225s에서 /position-metrics를 폴링하세요. 이렇게 하면 요청 속도가 고르게 되고, 분당 레이트 리밋을 유발할 수 있는 급증을 방지합니다.
POLL_SCHEDULE = [
{"endpoint": "/cohort-metrics", "offset_sec": 0, "interval_sec": 300},
{"endpoint": "/orders", "offset_sec": 75, "interval_sec": 300},
{"endpoint": "/liq-risk", "offset_sec": 150, "interval_sec": 300},
{"endpoint": "/position-metrics","offset_sec": 225, "interval_sec": 900},
]
async def run_polling_loop(cache, signal_engine):
while True:
now = time.time()
for task in POLL_SCHEDULE:
elapsed = (now - start_time) % task["interval_sec"]
if abs(elapsed - task["offset_sec"]) < 2:
data = await poll_with_backoff(task["endpoint"], params)
if data and cache.update(task["endpoint"], data):
signal_engine.evaluate(cache)
await asyncio.sleep(1)
핵심 세부사항: 시그널 엔진은 캐시가 실제 데이터 변경을 감지했을 때만 호출됩니다. 불필요한 평가 없음, 낭비된 연산 없음, 봇이 내리는 모든 결정은 최신 정보에 기반합니다.
Webhook 및 WebSocket 대안
Flow ($799/mo)와 Stream ($1,999/mo) 티어에서 HyperTracker는 webhook과 WebSocket 연결을 지원합니다. 이는 폴링 모델을 완전히 뒤집습니다. 봇이 타이머에 따라 데이터를 요청하는 대신, API가 데이터가 변경될 때 봇에 데이터를 푸시합니다.
Webhook은 적용되는 엔드포인트에 대한 전체 폴링 루프를 제거합니다. 콜백 URL을 등록하면, Cohort 메트릭이 업데이트될 때 봇이 새 데이터와 함께 POST를 받습니다. 폴링 없음, 낭비된 요청 없음, 오래됨 윈도우 없음.
WebSocket 연결(Stream 티어)은 지속적인 업데이트를 위한 영구 채널을 제공합니다. 데이터 갱신 후 몇 초 내에 반응해야 하는 고빈도 시스템을 구축하고 있다면, WebSocket 경로는 주기적 폴링의 지연을 완전히 제거합니다.
그러나 처음 시작하는 대부분의 빌더에게는 위에서 설명한 폴링 패턴이 올바른 토대입니다. Webhook과 WebSocket은 봇의 전략이 검증되고 더 낮은 지연 또는 더 높은 처리량이 필요할 때 성장하면서 도입하는 최적화입니다.
종합: 참조 아키텍처
Hyperliquid 트레이딩 봇의 잘 설계된 전체 스택입니다. API에서 실행까지:
+----------------------------------------------------------+
| 리스크 관리 |
| (시그널을 거부권으로 차단, 포지션 한도 적용, 중단 가능) |
+----------------------------------------------------------+
| ^
v |
+------------------+ +------------------+ |
| 시그널 엔진 |-->| 실행 |--+
| (순수 함수) | | (Hyperliquid |
| | | Python SDK) |
+------------------+ +------------------+
^
| (캐시 변경 시에만)
+----------------------------------------------------------+
| 데이터 캐시 |
| - 중복 제거 (변경되지 않은 응답 건너뜀) |
| - 오래됨 감지 (데이터 > 갱신 주기 2배면 경고) |
| - 우아한 저하 (에러 시 마지막으로 알려진 좋은 데이터 제공) |
+----------------------------------------------------------+
^
| (백오프가 있는 엇갈린 폴링)
+----------------------------------------------------------+
| API 폴링 레이어 |
| - 엔드포인트별 엇갈린 간격 |
| - 429/5xx에 대한 지수 백오프 |
| - 요청 예산 추적 (일별 + 분당) |
| - 시그널 요구사항 기반 엔드포인트 선택 |
+----------------------------------------------------------+
|
v
HyperTracker API
(REST / Webhooks / WebSocket)
각 레이어는 단일 책임을 가집니다. 폴링 레이어는 API 통신과 에러 복구를 처리합니다. 캐시 레이어는 데이터 신선도와 중복 제거를 처리합니다. 시그널 엔진은 의사결정을 처리합니다. 실행 레이어는 주문 관리를 처리합니다. 그리고 리스크 관리는 모든 것 위에 위치하며, 어떤 레이어의 어떤 행동도 차단할 권한을 가집니다.
데이터는 스택을 거슬러 올라가며, 각 레이어는 특정한 보장을 추가합니다. 시그널이 실행 레이어에 도달할 때쯤이면, 데이터가 최신이고, 시그널이 변경된(반복되지 않은) 입력에 대해 평가되었으며, 리스크 레이어가 거래를 승인했다는 것을 알 수 있습니다. 밤새 돌려놓을 수 있는 봇입니다.
사전 계산된 인텔리전스로 봇 구축하기
HyperTracker의 API는 Hyperliquid 전체에 걸쳐 Cohort 분석, 청산 리스크 스코어링, 주문 흐름 스냅샷, 포지션 메트릭을 제공합니다. 16개의 행동 Cohort, 분류된 모든 지갑, REST, webhook, 또는 WebSocket을 통해 제공됩니다. 무료 티어로 시작하고 봇이 성숙해짐에 따라 스케일업하세요.
흔한 실수들(그리고 피하는 방법)
모든 엔드포인트를 같은 빈도로 폴링하기. 리더보드 데이터는 주문 흐름 스냅샷만큼 빠르게 변하지 않습니다. 각 엔드포인트의 폴링 간격을 갱신 주기와 그 데이터에 대한 시그널의 민감도에 맞추세요. 모든 엔드포인트에 5분 처리가 필요한 것은 아닙니다.
너무 늦게까지 월별 한도를 무시하기. 3주 동안 잘 작동하다가 4주차에 월별 쿼터를 다 소진하는 봇은 약간 느리지만 한 달 내내 작동하는 봇보다 더 나쁩니다. 매일 누적 요청을 추적하고, 임계값에서 알림을 받으세요(예: 해당 날짜 기준으로 월별 예산의 비례 몫을 초과해서 사용했을 때).
에러를 조용히 삼키기. 가장 최악의 경우: 모든 예외를 잡아 None을 반환하는 try/except 블록이 시그널 엔진에 "데이터 없음"으로 전달되는 것입니다. 시그널 엔진은 "데이터 없음"을 "데이터가 흥미롭지 않다"와 다르게 해석합니다. "API가 빈 결과를 반환했다"(유효)와 "API 호출이 실패했다"(에러)를 구분하세요. 이는 다른 상태입니다.
핫 루프에서 과거 데이터 가져오기. HyperTracker는 포지션과 체결에 대해 몇 달에 걸친 과거 데이터를 가지고 있습니다. 그 데이터는 백테스팅과 연구를 위한 것이며, 한 번 가져와서 로컬에 저장하고, 라이브 폴링 루프에서는 절대 다시 가져오지 않아야 합니다. 과거 엔드포인트는 실시간 엔드포인트와 동일한 레이트 리밋을 가지므로, 반복적으로 가져오면 아무 이유 없이 예산을 소모합니다.
시그널 먼저 구축하고 데이터 레이어를 나중에 구축하기. 시그널이 재미있는 부분입니다. Cohort 다이버전스 스코어링, 복합 시그널, 임계값 튜닝. 그러나 얼마나 많은 API 호출을 감당할 수 있는지, 데이터가 얼마나 자주 갱신되는지, 에러가 어떻게 전파되는지를 모르고 시그널을 구축하면, 처음부터 설계했어야 할 제약 조건을 중심으로 데이터 레이어를 나중에 끼워 맞추게 됩니다. 배관을 먼저 구축하세요. 며칠 동안 로깅만 하는(거래 없이) 상태로 테스트하세요. 그 다음 시그널을 연결하세요.
핵심 요약
시그널이 알파입니다. API 레이어는 그 알파를 요청 예산을 태우거나 조용히 저하되지 않으면서 날마다 안정적으로 제공하는 인프라입니다. 이것을 올바르게 구현하는 빌더들은 몇 달씩 작동하는 봇을 만듭니다. 데이터 레이어를 나중에 생각하는 사람들은 엣지를 개선하기보다 인프라 디버깅에 더 많은 시간을 씁니다.
코드를 작성하기 전에 요청 예산을 모델링하세요. 적극적으로 캐싱하세요. 에러를 명시적으로 처리하세요. 데이터가 실제로 갱신되는 주기에 맞게 폴링하고, 요청을 엇갈리게 배치해서 절대 급증하지 않도록 하세요. 이 다섯 가지를 실행하면 API 레이어는 보이지 않게 됩니다. 그것이 바로 훌륭한 인프라가 되어야 할 모습입니다.