데이터 · 방법론

마지막 업데이트: 2026-04-10

요약

데이터 소스

Binance Futures REST (무료, 키 불필요)

Binance 공개 엔드포인트는 IP당 2,400 weight/분의 요청 한도를 가지며, 현재 사용량은 한계의 약 5–8%입니다.

Binance Spot REST (무료, 키 불필요)

Binance WebSocket (무료)

CoinMarketCap Pro (유료 키 필요)

CoinMarketCap Basic(무료) 플랜의 크레딧은 월 10,000개입니다. 현재 15m 주기로 한 번에 한 배치(최대 99개 심볼)를 호출하므로, 월 사용량은 약 2,880건(하루 약 96건)으로 무료 한도 내입니다.

갱신 주기와 캐시

  1. 15m마다 크론 실행 — Cloudflare Workers의 cron trigger(*/15 * * * *)가 scheduled 핸들러를 호출합니다.
  2. 한 배치 처리 — 400여 개 심볼을 99개씩 쪼갠 배치 중 하나를 처리합니다. 현재 커서가 D1의 kv 테이블에 저장되어 있습니다.
  3. Binance + CMC 호출 — 해당 배치의 심볼들에 대해 klines, open interest, CMC quotes를 병렬로 가져옵니다.
  4. 지표 계산 — RSI-10, EMA-21/50 계산. 점수 공식 적용.
  5. D1 upsertmarket_data 테이블에 쓰기. 다음 배치 커서 저장.
  6. 요청 처리 — 방문자가 랭킹 페이지를 열면 D1에서 읽어 정렬만 수행합니다. Binance·CMC는 재호출하지 않습니다.

전체 심볼 한 바퀴 갱신이 끝나면 커서가 0으로 리셋되고 다음 주기에 첫 배치부터 다시 시작합니다. 따라서 모든 심볼이 최대 약 75분의 지연을 가질 수 있습니다.

"예정 주기"와 "마지막 업데이트"는 같은 게 아닙니다

홈 페이지 상단에 두 개의 숫자 카드가 있습니다. 언뜻 비슷해 보이지만 의미는 다릅니다.

두 값이 어긋나면(예: "15m 예정"인데 "마지막 업데이트 2시간 전") 크론이 실제로 동작하지 않고 있다는 뜻입니다. 이 경우 카드에 자동으로 호박색 경고가 표시됩니다.

개발 모드에서는 크론이 실행되지 않습니다

astro dev(로컬 개발 서버)는 Cloudflare의 cron trigger를 흉내내지 않습니다. miniflare가 fetch 핸들러는 실행하지만 scheduled 핸들러는 실행하지 않기 때문입니다. 그래서 로컬에서는 /api/refresh를 수동으로 호출해야만 데이터가 갱신됩니다.

프로덕션(wrangler deploy 이후)에서는 Cloudflare가 실제로 cron을 15m마다 실행하므로 "마지막 업데이트" 값은 자동으로 15m 이내에 머뭅니다. 호박색 경고가 떠 있다면 CMC 장애, Binance IP 제한, 코드 에러 등 실제 운영 장애를 의심해야 합니다.

스코어링 공식

숏 후보 점수는 다음과 같이 계산됩니다(src/lib/scoring.ts):

score = RSI_10         × 0.40
      + max(7d %,  0)  × 0.15
      + max(30d %, 0)  × 0.30
      + ema_score                  (EMA 21 > EMA 50 일 때만 양수)
      + oi_score                   (OI z-score × 0.2)

ema_score = ((EMA_21 - EMA_50) / EMA_50) × 0.25  if gap > 0 else 0

각 항목의 의미

가중치에 대한 솔직한 한 마디

현재 가중치는 경험적으로 선택한 값이며 백테스트로 검증된 최적치가 아닙니다. 원본 Python 코드(api/short_coins.py)의 주석에도 "RSI is defo important, idk if more important but for sure. EMA score idk honestly, so is oi score idk honestly"라고 적혀 있습니다. 향후 과거 데이터로 IR(Information Ratio)을 측정해 최적 가중치를 찾을 계획입니다.

지표 계산 세부사항

RSI-10 (Wilder's smoothing)

gain_i, loss_i = max(close_i - close_{i-1}, 0), max(close_{i-1} - close_i, 0)
avgGain_0 = SMA(gains[0..N])
avgLoss_0 = SMA(losses[0..N])
avgGain_i = (avgGain_{i-1} × (N-1) + gain_i) / N   (N = 10)
RSI = 100 - 100 / (1 + avgGain/avgLoss)

EMA-21 / EMA-50 (SMA-seeded)

EMA_0 = SMA(prices[0..period])
k     = 2 / (period + 1)
EMA_i = price_i × k + EMA_{i-1} × (1 - k)

두 함수 모두 TA-Lib의 기본 구현을 매칭하도록 TypeScript로 포팅되어 있습니다. 소수점 오차 수준에서 Python 원본(api/short_coins.py)과 동일한 값을 출력합니다.

한계 · 경고

인프라