본문으로 이동
엔지니어링 가이드

예측 시장 데이터 정규화: Polymarket & Kalshi

결과, 가격 또는 해결 규칙을 혼합하지 않고 Polymarket 및 Kalshi 데이터를 정규화합니다. 실행 가능한 예제와 검증 체크리스트가 포함된 Node.js 튜토리얼입니다.

Gizmolab 팀 · 게시됨 · 16 분 읽기

1. 가격을 비교하기 전에 의미를 표준화하세요.

예측 시장 데이터 표준화는 장소별 레코드를 일반 모델로 변환하면서 그들을 해석하는 데 필요한 정보를 유지합니다. 어려운 부분은 필드를 재명하는 것이 아닙니다. 두 개의 레코드가 동일한 상태, 결과 방향, 관찰 시간 및 해상도 조건을 언급하기 전에 대시보드가 가격을 옆에 두는 것을 보장합니다.It is ensuring that two records refer to the same condition, outcome direction, observation time and resolution terms before a dashboard puts their prices side by side.

이 튜토리얼에서는 수집 및 검증을 위한 오프라인 Node.js 예제를 빌드합니다. 실시간 Polymarket 또는 Kalshi 응답이 아닌 가상의 시장과 명시적으로 정의된 어댑터 기록을 사용합니다. API 키, 네트워크 요청, 주문 또는 지갑 작업이 없습니다. 목표는 실제 소스를 연결하기 전에 테스트할 수 있는 작고 검사 가능한 경계입니다.

원본 소스 페이로드 또는 승인된 보관 참조를 정규화된 기록과 함께 보관하세요. 유용한 파이프라인에는 원시 장소 응답, 버전이 지정된 장소 어댑터, 검증된 공통 모델의 세 가지 레이어가 있습니다. 공급자가 필드나 규칙을 변경하면 어떤 어댑터가 레코드를 생성했는지 식별하고 이전 값의 의미를 추측하지 않고도 다시 처리할 수 있습니다.

2. 장소 식별자와 인용 의미를 명시적으로 매핑합니다.

Polymarket은 이벤트, 해결 가능한 시장 및 결과 토큰 ID를 구별합니다. 이벤트 제목을 글로벌 시장 키로 줄이는 대신 이러한 정체성을 보존하십시오. 그 가격 문서도 주문 책에서 사용할 수있는 제안 및 요청에서 표시 된 가격을 구별합니다. 마지막 거래, 중간 및 실행 가능한 인용은 모델에서 다른 라벨을 유지해야합니다.

Kalshi의 현재 명령서 문서에서는 yes_dollars 및 no_dollars를 포함한 orderbook_fp의 고정 포인트 문자열 쌍을 설명합니다. 그것의 고정 포인트 참조는 가격 및 양 필드에 의해 사용되는 단위를 정의합니다. 파싱을 통해 소스 문자열을 유지합니다.Keep the source string through parsing. 현재 달러 문자열 필드의 어댑터에 오래된 정수 센트 가정을 복사하지 마십시오.Do not copy an older integer-cents assumption into an adapter for a current dollar-string field.

Kalshi은 어떻게 보완적인 입찰 측면이 바이너리 주문서에 대한 요청을 의미할 수 있는지를 문서화합니다. 어댑터가 그 방법으로 가격을 추출하는 경우, 추출 및 소스 측면을 기록하십시오. 그 변환을 임의의 다중 결과 시장에 적용하거나 파생된 요청을 직접 공급 된 인용으로 혼란스럽게하지 마십시오. 아래 예제는 이미 식별된 인용 유형을 수락하고 주문서본을 재구성하지 않습니다.The example below accepts a already identified quote type and does not reconstruct an order book.

걱정지역별 출처공통 모델 규칙
아이덴티티Polymarket 이벤트, 조건 및 결과 토큰 식별자Namespace market 및 outcome IDs by venue; preserve source references에 대한 이름 공간 시장 및 결과 IDs by location; preserve source references
아이덴티티Kalshi 이벤트 및 시장 타이커이벤트 그룹화를 거래 가능한 시장에서 분리하십시오.Keep event grouping separate from the tradable market
가격Polymarket order-book bid/ask or display-related price 주문서 / 요청 또는 디스플레이 관련 가격상점 인용 유형 및 결과 방향 명시적으로
가격Kalshi 고정 포인트 달러 문자열Decimal strings using declared units; not infer cents (선언된 단위를 사용하여 십자형 문자열을 파스; 센트를 추론하지 마십시오)
생명주기각 장소의 상태 및 타이밍 필드검토된 매핑을 사용하여 원시 상태와 알려지지 않은 값을 유지합니다.Use a reviewed mapping; retain the raw status and unknown values

3. 좁은 표준 스키마 정의

다운로드 가능한 모듈은 문서화된 어댑터 계약을 기대합니다. ID 필드는 큰 숫자 모양의 ID를 포함한 비공개 문자열입니다.Identity fields are non-empty strings, including large numerical-looking IDs. 열쇠는 장소, 시장 및 결과의 세리화된 튜플이며, 피할 수 없는 식별자를 점수로 연결함으로써 발생하는 충돌을 피합니다.The key is a serialized tuple of venue, market and outcome, which avoids collisions caused by joining unescaped identifiers with punctuation. 단순히 숫자를 포함하기 때문에 토큰 ID를 JavaScript 번호로 변환하지 마십시오.

명시적 규칙을 통해 각 소스 결과를 Condition_true 또는 Condition_false에 매핑합니다. 배열 위치 0은 예의 일반적인 동의어가 아닙니다. 입찰, 요청, 마지막 및 중간 지점을 별도의 견적 종류로 유지합니다. 이 예는 명시된 $1 액면가를 사용하여 바이너리 계약을 처리합니다. 바구니, 가변 지급 또는 다중 결과 변환을 지원하지 않습니다.

별도의 소스At, receivedAt, closeAt, terms’ observationAt 및 resolvedAt. 소스 시간은 인용을 설명합니다; 수신 시간은 섭취를 설명합니다; 닫힌 시간은 거래 경계를 설명합니다; 관찰 시간은 측정되는 이벤트를 설명합니다. 시장은 그 결과가 알려지기 전에 거래를 중단 할 수 있습니다. 제품이 필요할 경우 예상 해상도 시간은 실제 해상도 시간을 대체하는 대신 다른 복구 필드에 속합니다.The expected resolution time, if your product needs it, belongs in another resellable field rather than replacing actual resolution time.

  • 인식되지 않은 장소 수명주기 값을 명시적으로 알려지지 않은 상태로 매핑하고 검토를 위해 경고합니다.Map unrecognized place lifecycle values to an explicit unknown state and alert for review.
  • 이 경계에서 캐논 UTC ISO 타임스탬프를 필요로 합니다; 현장 어댑터에서 소스 형식과 시간대를 변환합니다.Requires canonical UTC ISO timestamps at this limit; convert source formats and time zones in the venue adapter.
  • 인용 또는 소스 타임스탬프가 없을 때 null을 유지합니다.Preserve null when a quote or source timestamp is absent. 잃어버린 데이터를 제로 가격으로 변환하지 마십시오.
  • 실제 어댑터를 구축할 때 컴팩트 샘플 모델 밖에서 원형 장소 식별자를 저장하고 규칙 참조를 저장합니다.Store raw place identifiers and rule references outside the compact example model when building the real adapter.

4. 자동 반올림 없이 소수점 가격을 구문 분석합니다.

이 예에서는 BigInt 산술을 사용하여 달러 가격을 정수 백만분의 1로 확장한 다음 JSON-안전한 문자열을 반환합니다. 문자열 "0.615"와 "0.6150"은 모두 "615000"이 됩니다. 별도로 선언된 레거시 또는 합성 센트 입력 "61.5"는 동일한 결과를 생성합니다. 해당 센트 옵션은 명시적인 단위를 보여줍니다. Kalshi의 현재 응답 형식으로 표시되지 않습니다.

서명되지 않은 십자열 또는 null 만 허용됩니다.Only unsigned decimal strings or null are accepted. 정확하게 표현할 수 없는 상징적 지표, 부동점 숫자, 범위 밖의 가격 및 정확성은 거부됩니다.Exponent notation, floating-point numbers, out-of-range prices and precision that cannot be exactly represented are rejected. 값이 정확한 상태로 남아있을 때 추가 추적 제로가 수락됩니다.Additional trailing zeros are accepted when the value remains correct. 이 정책은 어댑터 소유자가 조언을 조용히 변경하는 대신 의도적 인 정확성 결정을 내리도록 강요합니다.

규모는 교육 선택이지 모든 장소나 향후 스키마가 소수점 6자리를 사용한다는 주장이 아닙니다. 현재 공급자가 허용하는 가격 간격, 수량 정밀도 및 필드 의미를 별도로 검증합니다. 가격, 수량, 거래량, 명목 및 미결제약정은 서로 다른 측정값입니다. 동일한 기능을 통해 모든 항목을 실행하거나 거래 가격을 독립적으로 보정된 확률로 표시하지 마세요.

5. 해결 규칙 전반에 걸쳐 잘못된 일치를 거부합니다.

비슷한 타이틀은 동등한 계약을 증명하지 않습니다. Polymarket의 해결 문서에서는 해결 규칙이 가장자리 사례의 출처와 취급을 정의한다고 설명합니다. 그 규칙에 대한 참조를 유지하고 변경 사항을 검토하십시오. 공식 발표 된 관찰을 측정하는 시장은 예비 읽기 또는 다른 출처를 측정하는 것과 상당히 다를 수 있습니다.A market measuring an official published observation can differ materially from one measuring a preliminary reading or another source, even if both titles mention the same event.

우리의 합성 레지스트리는 검토 된 주제, 프레디카이트, 관찰 시간, 해상도 원본, 비드 규칙 및 지불 조건을 비교하고, 카논 결과 방향을 더합니다. 프레디카트에는 연산자가 포함되어야 한다: "100보다 크다"는 "최소 100"과 다릅니다. 차이점은 아래 코드가 승인을 위해 잘못 될 수 있다는 흐릿한 신뢰 점수를 얻는 것보다 거부 이유를 생성합니다.Differences produce rejection reasons rather than a fuzzy confidence score that downstream code might mistake for approval.

검토된 깃발은 단지 가르침 장소 보유자입니다. 생산에서, 리뷰자 정체성, 소스 규칙 참조 및 변경 기록을 가진 인증 된 버전 레지스트리에서 검토된 매핑을 수락합니다.In production, accept reviewed mappings from an authenticated, versioned registry with reviewer identity, source rule references and change history. 텍스트 평등은 정의적 증거가 아닙니다: 비교기는 누군가가 이미 검토한 구조화된 값을 확인합니다. 악의적이거나 잘못된 어댑터는 임의의 시장을 동등하게 선언할 수 없습니다.

이 예제는 또한 보수적 인 검토 게이트로서 차이가 있거나 실종되는 가까운 시간을 거부합니다.The example also rejects differing or missing close times as a conservative review gate. 두 개의 계약은 서로 다른 시간에 종료하는 동안 동일한 이벤트를 측정할 수 있으므로 일부 가짜 부정자를 의도적으로 생성합니다. 분리된 생산 결정으로서 시멘틱 동등성과 비교 가능한 거래 창을 유지하십시오. 승인 된 일치조차도 실행 가용성, 수수료, 보증금, 유동성 또는 수익성있는 거래에 대해 아무것도 말하지 않습니다.

6. 오프라인 정규화 예제 실행

아래 링크를 사용하여 prediction-market-normalization.mjs를 다운로드하고 로컬로 저장하고 현재 지원되는 Node.js 릴리스로 실행하십시오. 그것은 패키지 의존성을 가지고 있지 않습니다. 이 페이지에 인쇄된 전체 소스는 다운로드 링크에 의해 제공되는 동일한 파일이므로 설명과 실행 가능한 유물이 일치합니다.

두 가상의 장소는 동일한 입찰 가격을보고하지만 다른 해상도 소스를 지정합니다. 각 레코드가 구조적으로 유효하기 때문에 표준화가 성공합니다. 비교는 검토 된 일치 기준을 충족하지 않기 때문에 쌍을 거부합니다.Comparison then rejects the pair because they do not meet the reviewed matching criteria. 이 분리는 유용합니다: 유효한 레코드가 여전히 크로스 수익 비교에 적합하지 않을 수 있습니다.

다운로드한 모듈을 실행하세요.
node prediction-market-normalization.mjs
합성 설비의 예상 출력
{
  "prices": ["615000", "615000"],
  "comparison": {
    "equivalent": false,
    "reasons": ["Different resolutionSource"]
  }
}

7. 신선도와 운송 회복을 별도의 점검으로 처리하십시오.

받은 메시지는 반드시 최근의 인용이 아닙니다. 예제의 신선함 함수는 명시적인 평가 시간과 최대 연령을 사용하고, 실종된 소스 시간을 표시하며, 타임스탬프가 미래에 있거나 불일치할 때 시계 검토를 요구합니다.The freshness function uses an explicit evaluation time and maximum age, flags missing source time, and requires clock review when timestamps are in the future or inconsistent. 그것은 신선한 마지막 거래가 실행 가능한 요청이라고 추론하지 않습니다. 견적 유형, 시장 상태 및 규칙 검토 게이트를 별도로 적용합니다.Apply quote-type, market status and rule-review gates separately.

생산 스트림은 또한 다시 연결하는 행동을 필요로합니다 : 마지막으로 허용된 시퀀스 또는 소스 체크 포인트를 사용할 수 있는 경우 유지하고, 틈새를 감지하고, 신선한 스냅샷을 얻고, 장소의 주문 계약에 따라 업데이트를 적용합니다. 문서화된 소스 정체성에 의한 디다플리케이션; 타임스탬프가 고유하다고 가정하지 마십시오.Deduplicate by a documented source identity; do not assume timestamps are unique. 회복이 불완전한 동안 먹이 스테일을 표시합니다.Mark the feed stale while recovery is incomplete.

공급자 권한 및 보존 요구 사항에 따라 디버깅을 위해 제한적인 원시 기록을 보관하세요. 어댑터 버전, 수신 시간, 유효성 검사 실패 및 규칙 검토 버전을 유지합니다. 스키마 변경으로 인해 필수 필드가 삭제되면 해당 필드를 격리하고 작동 신호를 발생시킵니다. 누락된 필드를 0으로 채우면 그럴듯해 보이는 차트 뒤에 끊어진 파이프라인이 숨겨집니다.

8. 대시보드를 연결하기 전에 극단적인 사례를 검증하세요

다운로드 가능한 코드는 웹사이트의 자동 테스트에 의해 실행됩니다. 이 케이스는 또한 자신의 어댑터에 대한 유용한 수락 검사입니다. 라이브 APIs를 연결할 때 실제, 허용된 공급자 장비 및 운송 테스트를 추가하십시오; 합성 모듈 혼자서 소스 가용성, 엔드포인트 액세스 또는 시장 규칙 해석을 확인할 수 없습니다.

입력 또는 변경예상되는 행동문제는 방지하는
Null 인용 및 실수 0두 개의 서로 다른 값을 유지합니다.가격으로 표시된 데이터가 없음
Decimal strings in declared units - 선언된 단위들정확한 정상화된 integer string센트/달러 및 플로잉 포인트 오류
과도한 정확성 또는 알려지지 않은 단위라운드 또는 추측 대신 거부조용한 인용문명 잔소리
큰 시장 또는 토큰 IDString Identity 보존숫자 정확성 손실
반대의 결과 방향경기를 거부하는예 / 아니오 반전
Source, Predicate, Time 또는 Void Rule 변경Reject pending reviewed mapping에 대한 리뷰 보기가짜 십자형 수입 동등성
닫혀 있지만 해결되지 않은null에서 해결됨 유지조기 정산 주장
지속, 미래 또는 부재 소스 시간Flag stale, clock-review or missing - 시계 검토 또는 실종가짜 신선함

9. 출처를 잃지 않고 어댑터 확장

특정 장소 API 버전과 소규모 시장 세트를 선택하여 실제 통합을 시작합니다. 각 캐논 필드의 원본, 그 단위, null 행동 및 검증 규칙을 식별하는 매핑 문서를 작성합니다.Write a mapping document that identifies the source for each canonical field, its units, null behavior and validation rule. 인증되지 않은 숫자를 가격 기록에 추가하는 대신 원본 양과 주문서 깊이를 자신의 정확한 십자형 구조로 유지하십시오.Keep source quantities and order-book depth in their own exact-decimal structures rather than adding unvalidated numbers to the price record.

다음으로, 라이프 사이클 테스트, 허용된 역사적인 장치 및 규칙 변경 취급을 추가합니다. 소스 규칙이 변경되었을 때 검토된 매핑이 어떻게 취소되었는지 결정하고 소비자가 이전 비교가 더 이상 승인되지 않는다는 것을 어떻게 알 수 있는지 결정합니다. 시장 디렉토리, 가격 피드 및 규칙 레지스트리에는 다른 업그레이드 요구 사항이 있습니다; 이러한 책임을 디자인에서 눈에 띄게하십시오.

대시보드 또는 경고 제품의 경우 가격과 함께 장소, 인용 유형, 소스 시간, 신선함 및 규칙 리뷰 상태를 표시합니다.For a dashboard or alerting product, show the venue, quote type, source time, freshness and rule-review state alongside the price. 인터페이스가 이러한 차이를 무너뜨리면, 올바른 백엔드가 여전히 오해를 일으킬 수 있습니다.If the interface collapses those distinctions, a correct backend can still produce a misleading comparison. 동일한 검증된 모델에서 디스플레이 및 내보내기 형식을 구축합니다.Build the display and export format from the same validated model.

완전한 실행 가능한 소스

Prediction-market-normalization.mjs — 합성 설비, 네트워크 호출 없음
// Offline teaching example. These are synthetic adapter records, NOT venue API responses.
// Run with Node.js: node prediction-market-normalization.mjs
import { pathToFileURL } from 'node:url'

function nonempty(value, name) {
  if (typeof value !== 'string' || !value.trim()) throw new TypeError(`${name} must be a nonempty string`)
  return value
}

function timestamp(value, name, nullable = false) {
  if (nullable && value === null) return null
  nonempty(value, name)
  const parsed = new Date(value)
  if (!Number.isFinite(parsed.valueOf()) || parsed.toISOString() !== value) {
    throw new TypeError(`${name} must be canonical UTC ISO, including milliseconds`)
  }
  return value
}

// Price per $1-face-value contract, scaled to millionths; no binary float arithmetic.
// 'cents' is an explicit legacy/synthetic adapter unit, not an assumed live API format.
export function normalizePrice(value, unit) {
  if (!['dollars', 'cents'].includes(unit)) throw new TypeError('Unknown price unit')
  if (value === null) return null // Missing quote is different from a real zero price.
  if (typeof value !== 'string' || !/^\d+(\.\d+)?$/.test(value) || value.length > 80) {
    throw new TypeError('Price must be a bounded unsigned decimal string or null')
  }
  const [whole, fraction = ''] = value.split('.')
  const numerator = BigInt(whole + fraction) * 1_000_000n
  const denominator = 10n ** BigInt(fraction.length) * (unit === 'cents' ? 100n : 1n)
  if (numerator > denominator * 1_000_000n) throw new RangeError('Price exceeds $1 face value')
  if (numerator % denominator !== 0n) throw new RangeError('Price needs more than six decimal places')
  return (numerator / denominator).toString()
}

export function normalizeRecord(input) {
  const venue = nonempty(input.venue, 'venue')
  const marketId = nonempty(input.marketId, 'marketId')
  const outcomeId = nonempty(input.outcomeId, 'outcomeId')
  if (!['condition_true', 'condition_false'].includes(input.canonicalOutcome)) throw new TypeError('Explicit outcome mapping required')
  if (!['bid', 'ask', 'last', 'midpoint'].includes(input.quoteKind)) throw new TypeError('Explicit quote kind required')
  if (!['open', 'closed', 'resolved', 'unknown'].includes(input.status)) throw new TypeError('Explicit adapter status required')
  return {
    key: JSON.stringify([venue, marketId, outcomeId]), // No collisions from punctuation in IDs.
    venue, marketId, outcomeId,
    canonicalOutcome: input.canonicalOutcome,
    quoteKind: input.quoteKind,
    priceMicros: normalizePrice(input.price, input.priceUnit),
    sourceAt: timestamp(input.sourceAt, 'sourceAt', true),
    receivedAt: timestamp(input.receivedAt, 'receivedAt'),
    closeAt: timestamp(input.closeAt, 'closeAt', true),
    resolvedAt: timestamp(input.resolvedAt, 'resolvedAt', true),
    status: input.status,
    rawStatus: nonempty(input.rawStatus, 'rawStatus'),
    // A teaching flag cannot replace an authenticated, versioned rule-review registry.
    terms: input.terms ? { ...input.terms } : null,
  }
}

const TERM_FIELDS = ['subject', 'predicate', 'observationAt', 'resolutionSource', 'voidRule', 'payoutTerms']

export function compareContracts(left, right) {
  const reasons = []
  if (left.canonicalOutcome !== right.canonicalOutcome) reasons.push('Different outcome mapping')
  for (const [label, record] of [['left', left], ['right', right]]) {
    if (record.terms?.reviewed !== true) reasons.push(`${label}: terms have not been reviewed`)
    for (const field of TERM_FIELDS) {
      if (typeof record.terms?.[field] !== 'string' || !record.terms[field].trim()) reasons.push(`${label}: missing ${field}`)
    }
  }
  for (const field of TERM_FIELDS) {
    if (left.terms?.[field] !== right.terms?.[field]) reasons.push(`Different ${field}`)
  }
  // Conservative gate: differing close times require review even if the event is the same.
  if (left.closeAt === null || right.closeAt === null || left.closeAt !== right.closeAt) reasons.push('Close times require review')
  return { equivalent: reasons.length === 0, reasons }
}

export function quoteFreshness(record, now, maxAgeMs) {
  timestamp(now, 'now')
  if (!Number.isSafeInteger(maxAgeMs) || maxAgeMs < 0) throw new TypeError('Invalid maximum age')
  if (record.priceMicros === null || record.sourceAt === null) return 'missing'
  const received = Date.parse(record.receivedAt)
  const source = Date.parse(record.sourceAt)
  const current = Date.parse(now)
  if (source > current || received > current || source > received) return 'clock-review'
  return current - source > maxAgeMs ? 'stale' : 'fresh'
}

export function demoRecords() {
  const common = {
    marketId: 'demo-market-900719925474099312345', outcomeId: 'demo-yes',
    canonicalOutcome: 'condition_true', quoteKind: 'bid', priceUnit: 'dollars',
    sourceAt: '2026-10-01T12:00:00.000Z', receivedAt: '2026-10-01T12:00:01.000Z',
    closeAt: '2026-10-30T23:00:00.000Z', resolvedAt: null, status: 'open', rawStatus: 'adapter-demo-open',
    terms: { reviewed: true, subject: 'Fictional Example Index', predicate: 'index > 100',
      observationAt: '2026-10-31T00:00:00.000Z', resolutionSource: 'Example Authority A',
      voidRule: 'void if official observation is unavailable', payoutTerms: 'USD 1 per winning contract' },
  }
  return [
    normalizeRecord({ ...common, venue: 'demo-venue-a', price: '0.615' }),
    normalizeRecord({ ...common, venue: 'demo-venue-b', price: '0.6150',
      terms: { ...common.terms, resolutionSource: 'Example Authority B' } }),
  ]
}

if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
  const [left, right] = demoRecords()
  console.log(JSON.stringify({ prices: [left.priceMicros, right.priceMicros], comparison: compareContracts(left, right) }, null, 2))
}

예측 시장 데이터 통합 구축

Gizmolab 팀이 장소 어댑터, 표준 스키마, 피드 복구 및 제품 인터페이스를 파악하는 데 도움이 됩니다. 필요한 장소, 데이터 권리 및 승인 기준부터 시작하세요.

예측 API 통합 살펴보기 →