本文へスキップ
エンジニアガイド

予測市場データの正規化: Polymarket & Kalshi

Polymarket および Kalshi データを標準化し、結果、価格、または解像度のルールを混同することなく。 実行可能な例と検証チェックリストを含むNode.jsチュートリアル。

Gizmolab Team · 掲載 · 16 分で読めます

1. 価格を比較する前に意味を正規化する

予測市場データの正規化は、会場固有の記録を解釈するために必要な情報を保持しながら、共通モデルに変換します。難しいのはフィールドの名前を変更することではありません。ダッシュボードで価格を並べる前に、2 つのレコードが同じ条件、結果の方向、観察時間、解決条件を参照していることを確認します。

このチュートリアルは、摂取と検証のためのオフライン Node.js 例を構築します。 フィクションの市場と明確に定義されたアダプターレコードを使用し、PolymarketまたはKalshiのリアルな応答は使用しません。 API キー、ネットワーク リクエスト、オーダー、または ウォレット 操作はありません。 目標は、実際のソースを接続する前にテストできる小さな、検証可能な境界です。

元のソース ペイロードまたは承認されたアーカイブ参照を、正規化されたレコードと一緒に保管してください。有用なパイプラインには、生の会場応答、バージョン管理された会場アダプター、検証済みの共通モデルの 3 つの層があります。プロバイダーがフィールドまたはルールを変更すると、古い値の意味を推測することなく、どのアダプターがレコードを生成したかを特定し、それを再処理できます。

2. 会場識別子をマッピングし、セマンティクスを明示的に引用する

Polymarketは、イベント、解決可能な市場、結果トークンIDを区別します。 イベントタイトルをグローバル市場キーに縮小するのではなく、これらのアイデンティティを保存してください。 価格ドキュメントはまた、注文書に記載されているオファーとリクエストから表示された価格を区別します。 最後の取引、中間点、および実行可能な引用は、あなたのモデルで異なるラベルを保持しなければなりません。

Kalshi の現在のオーダーブック文書は、yes_dollars および no_dollars を含む orderbook_fp で固定点列のカップルを説明します。 固定点参照は、価格と量のフィールドによって使用される単位を定義します。 パルシングを通じてソースの文字列を保持します。 古い整数セントの仮定を現在のドル文字列フィールドのアダプターにコピーしないでください。

Kalshi は、補完的な入札側が、そのバイナリオーダーブックの要求を意味する方法を文書化します。 アダプタがこの方法で価格を引き出した場合は、引き出しとソース側を記録します。 その変換を任意の複数の結果市場に適用しないでください、または直接供給されたオファーと由来のリクエストを混同しないでください。 下記の例では、既に識別された引用型を受け入れ、注文書を再構築しません。

関心事地元特有の情報源コモンモデルルール
アイデンティティPolymarket イベント、条件および結果トークン識別子会場別の名前空間市場と結果 ID。ソース参照を保持する
アイデンティティKalshi イベント&マーケットティッカーイベントグループを取引可能な市場から別々に保つ
価格Polymarket order-book bid/ask or display-related price オーダー・ブック・オファー/askまたはディスプレイ関連価格Store quote kind and outcome direction explicitly. 明確な引用と結果の方向
価格Kalshi fixed-point dollar strings ドルシート宣言された単位を使用して 10 進文字列を解析します。セントを推測しないでください
ライフサイクル各会場のステータスとタイミングフィールドレビューされたマッピングを使用する; retain the raw status and unknown values

3. 狭い正規スキーマを定義する

ダウンロード可能なモジュールは、文書化されたアダプター契約を期待します。 アイデンティティフィールドは、大きな数値外観のIDを含む非空の文字列です。 キーは、場所、市場、結果の連続化されたトップルであり、逃れられない識別子を点数で接続することによって引き起こされる衝突を回避します。 トークンIDをJavaScript番号に変換しないでください、単に数字が含まれているためです。

明示的なルールで各ソース結果を condition_true または condition_false にマッピングします。 Array position zero is not a universal synonym for Yes. Array position zero is not a universal synonym for Yes. Bid, ask, last and midpoint を別々の引用タイプとして保持します。 この例では、$1の表面値を指定したバイナリー契約を扱います; それは、バスケット、変数支払い、または複数の結果変換をサポートしません。

Separate sourceAt, receivedAt, closeAt, the terms’ observationAt and resolvedAt. 別々のソースAt、 receivedAt、 closeAt、 term’s observationAt、 and resolvedAt。 ソースタイムは引用を記述し、受信時間は摂取を記述し、閉鎖時間は取引の限界を記述し、観察時間は測定されるイベントを記述する。 市場は、その結果が知られる前に取引を止めることができます。 予想された解像度時間は、製品がそれを必要とする場合、実際の解像度時間を置き換えるのではなく、別の取り消し可能なフィールドに属します。

  • 認識されていない場所のライフサイクル値を明示的な未知の状態にマップし、レビューのための警告を表示します。
  • この境界でカノニカル UTC ISO タイムスタンプを必要とします; 場所アダプターでソース形式とタイムゾーンを変換します。
  • 引用またはソースタイムスタンプが欠けているときに null を保持します。 欠けているデータをゼロ価格に変えるな。
  • 実際のアダプタを構築する際に、コンパクトサンプルモデル以外の原始場所識別子と規則参照を格納します。

4. サイレント四捨五入を行わずに小数点価格を解析する

この例では、BigInt 演算を使用してドル価格を 100 万分の 1 の整数にスケールし、JSON セーフな文字列を返します。文字列「0.615」と「0.6150」は両方とも「615000」になります。個別に宣言されたレガシーまたは合成セント入力「61.5」は、同じ結果を生成します。このセント オプションは明示的な単位を示します。 Kalshi の現在の応答形式としては表示されません。

署名されていない decimal 文字列または null のみが承認されます。 表記、浮動点数、範囲外価格、正確に表すことができない精度は拒否されます。 値が正確である場合に追加の追跡ゼロが受け入れられます。 このポリシーは、アダプターの所有者に、暗黙に引数を変更する代わりに、意図的な正確な決定を下すことを強制します。

このスケールは教育上の選択であり、すべての会場や将来のスキーマが小数点以下 6 桁を使用するという主張ではありません。現在のプロバイダーで許可されている価格間隔、数量の精度、およびフィールドのセマンティクスを個別に検証します。価格、数量、出来高、想定元本および建玉は異なる尺度です。すべてを同じ関数で実行したり、取引価格を独立して調整された確率としてラベル付けしたりしないでください。

5. 解決ルール全体で誤った一致を拒否する

同様のタイトルは、同等の契約を証明しない。 Polymarket の解決文書は、解決規則がエッジケースの起源と処理を定義することを説明しています。 これらのルールへの参照を保存し、修正を検討する。 公表された公式観察を測定する市場は、同じイベントを両方のタイトルで言及しているとしても、初期読書または別の情報源を測定する市場と本質的に異なる場合があります。

当社の合成レジストリは、レビューされたトピック、プレディカート、観察時間、解析源、無効規則、支払い条件、およびカノニカルな結果の方向を比較します。 プレディカットには、そのオペレーターを含む必要があります: 「100 より大きい」は、「少なくとも 100」とは異なります。 差異は、下流のコードが承認のために間違っている可能性があるという曖昧な信頼のスコアではなく、拒否の理由を生成します。

見直しの旗は、教える場所の保有者にすぎない。 生産では、レビュー者アイデンティティ、ソースルール参照、変更履歴を持つ認証されたバージョンレジストリからレビューされたマッピングを受け入れます。 テキストの平等は、セマンティックな証拠ではありません: 比較器は、すでに誰かがレビューした構造化された値をチェックします。 悪意あるいは誤ったアダプタは、任意の市場を同等のものと宣言することは許されない。

この例では、保守的なレビュー ゲートとして、異なる終了時間や終了時間の欠落も拒否します。 2 つの契約は、異なる時間に終了しながら同じイベントを測定できるため、意図的に偽陰性が作成されます。セマンティックの同等性と同等の取引ウィンドウを別個の運用上の決定として維持します。承認されたマッチであっても、約定の可能性、手数料、担保、流動性、または収益性の高い取引については何も語られていません。

6. オフライン正規化サンプルを実行します。

以下のリンクを使用して prediction-market-normalization.mjs をダウンロードし、ローカルに保存し、現在サポートされている Node.js リリースで実行します。 パッケージ依存性がない。 このページに印刷された完全なソースは、ダウンロードリンクによって提供される同じファイルなので、説明と実行可能なアーティファクトは並行します。

二つのフィクションの場所は、平等なオファー価格を報告しますが、異なる解析源を指定します。 標準化は、各レコードが構造的に有効であるため、成功する。 Comparison は、それらがレビューされた合致基準を満たしていないため、このカップルを拒否します。 この分離は役に立つ:有効な記録は、クロスインベント比較にはまだ適していません。

ダウンロードしたモジュールを実行する
node prediction-market-normalization.mjs
合成フィクスチャからの期待される出力
{
  "prices": ["615000", "615000"],
  "comparison": {
    "equivalent": false,
    "reasons": ["Different resolutionSource"]
  }
}

7. 鮮度および輸送の回復を別個のチェックとして扱う

受信したメッセージは、必ずしも最近の引用ではありません。 この例の新鮮さ関数は、明示的な評価時間と最大年齢を使用し、欠けているソース時間を表示し、タイムスタンプが将来あるいは不一致である場合の時計のレビューを必要とする。 新しい最後の取引が実行可能なリクエストであることを推測するものではありません。 引用タイプ、市場状態、ルールレビューゲートを別々に適用します。

生産ストリームはまた、再接続行動を必要とします: 利用可能な場合に最後の受け入れられたセクションまたはソースチェックポイントを保持し、空白を検出し、新鮮なスナップショットを取得し、会場の注文契約に従ってアップデートを適用します。 文書化されたソースアイデンティティによるデダプライク; タイムスタンプがユニークであると仮定しないでください。 回復が不完全な間、フィードスタールをマークします。

あなたのプロバイダの許可および保存要件に従って、デバッグのために限られた原始レコードを保持します。 Adapter バージョンを維持し、タイム、検証エラー、ルールレビューバージョンを受け取ります。 スケジュールの変更が必要なフィールドを落とす場合、それを隔離し、操作信号を上げます。 欠けているフィールドをゼロで埋め込むと、信頼性の高いグラフの後ろに破損したパイプラインが隠れます。

8. ダッシュボードを接続する前にエッジケースを検証する

ダウンロード可能なコードは、ウェブサイトの自動テストによって実行されます。 これらのケースは、あなた自身のアダプターのための有用な受け入れチェックでもあります。 ライブ APIs を接続する際には、実際の、許可されたプロバイダーの接続と輸送テストを追加します; 合成モジュールだけでは、ソース可用性、エンドポイントアクセス、または市場ルールの解釈を検証できません。

入力または変更予想される行動予防する問題
ゼロとリアルゼロ二つの異なる価値観を守る欠けているデータは価格として表示されます。
Decimal strings in declared units 宣言された単位正確な正常化整数文字列セント/ドルと浮動ポイントエラー
過度の精度または未知のユニットラウンドや推測の代わりに拒否する腐敗の静かな言葉
大きな市場またはトークンIDString Identityの保存数値正確性損失
逆方向の結果試合を拒否Yes / No 逆転
変更されたソース、プレディカート、時間または無効規則Reject pending reviewed mapping(マッピングをレビューするまで拒否)偽の交差収入等価
閉ざされたが未解決ゼロで解決する決済 早期申告
永続的、未来的、または不在のソースタイムFlag stale, clock-review or missing (フラグスタイル、時計レビュー、または行方不明)偽りの新鮮さ

9. 起源を失わずにアダプターを拡張する

特定の場所の API バージョンと小さな市場セットを選択することで、実際の統合を開始します。 各カノニカルフィールドのソース、その単位、null behavior、および検証ルールを識別するマッピングドキュメントを書く。 ソースの量とオーダーブックの深さは、価格記録に未確認の数字を追加する代わりに、独自の正確な十数値構造に保管します。

次に、ライフサイクルテスト、許可された履歴設定、ルール変更処理を追加します。 ソースルールが変更された場合、レビューされたマッピングがどのように取り消され、消費者が以前の比較がもはや承認されていないことをどのように知るかを決定します。 市場ディレクトリ、価格フィード、ルールレジストリには、異なるリフレッシュニーズがあり、これらの責任をデザインで明らかにします。

ダッシュボードまたは警告製品の場合、価格とともに場所、引用タイプ、ソースタイム、新鮮さ、ルールレビューの状態を表示します。 インターフェイスがこれらの区別を崩壊させる場合、正しいバックエンドは依然として誤った比較を生成する可能性があります。 同じ検証されたモデルから表示およびエクスポート形式を作成します。

完全な実行可能なソース

precision-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 は、チームがサイトアダプター、カノニカル スケジュール、フィード リカバリ、製品 インターフェイスを範囲化するのに役立ちます。 必要な場所、データ権利、および受け入れ基準から始める。

Predict API インテグレーション