Saltar al contenido
guías de ingeniería

Normalización de datos del mercado de predicción: Polymarket y Kalshi

Normalice los datos de Polymarket y Kalshi sin mezclar resultados, precios o reglas de resolución. Un tutorial de Node.js con un ejemplo ejecutable y una lista de verificación de validación.

Gizmolab Equipo · Publicado · 16 min de lectura

1. Normalizar el significado antes de comparar precios

La normalización de los datos del mercado de predicción convierte los registros específicos del lugar en un modelo común y al mismo tiempo conserva la información necesaria para interpretarlos. La parte difícil no es cambiar el nombre de los campos. Se trata de garantizar que dos registros hagan referencia a la misma condición, dirección de resultado, tiempo de observación y términos de resolución antes de que un panel coloque sus precios uno al lado del otro.

Este tutorial crea un ejemplo de Node.js sin conexión para la ingesta y validación. Utiliza mercados ficticios y registros de adaptadores definidos explícitamente, no respuestas Polymarket o Kalshi en vivo. No hay claves API, solicitudes de red, pedidos ni operaciones de billetera. El objetivo es un límite pequeño e inspeccionable que puedas probar antes de conectar una fuente real.

Mantenga la carga útil de la fuente original o una referencia de archivo autorizada junto con el registro normalizado. Un canal útil tiene tres capas: la respuesta del lugar sin procesar, un adaptador de lugar versionado y un modelo común validado. Cuando un proveedor cambia un campo o regla, puede identificar qué adaptador produjo un registro y reprocesarlo sin adivinar qué significaba el valor anterior.

2. Asigne identificadores de lugares y cite la semántica explícitamente

Polymarket distingue eventos, mercados resolubles e ID de tokens de resultados. Preservar esas identidades en lugar de reducir el título de un evento a una clave de mercado global. Su documentación de precios también distingue los precios mostrados de la oferta y la demanda disponibles en el libro de pedidos. Una última cotización comercial, punto medio y ejecutable deben conservar etiquetas diferentes en su modelo.

La documentación actual del libro de pedidos de Kalshi describe pares de cadenas de punto fijo en orderbook_fp, incluidos yes_dollars y no_dollars. Su referencia de punto fijo define las unidades utilizadas por los campos de precio y cantidad. Mantenga las cadenas de origen mediante el análisis. No copie una suposición anterior de centavos enteros en un adaptador para un campo de cadena de dólares actual.

Kalshi documenta cómo las ofertas complementarias pueden implicar solicitudes para su cartera de órdenes binarias. Si su adaptador obtiene un precio de esa manera, registre la derivación y la fuente. No aplique esa transformación a un mercado arbitrario de múltiples resultados ni confunda una demanda derivada con una cotización proporcionada directamente. El siguiente ejemplo acepta un tipo de cotización ya identificado y no reconstruye un libro de pedidos.

PreocupaciónFuente específica del lugarRegla del modelo común
IdentidadPolymarket identificadores de token de evento, condición y resultadoMercado de espacios de nombres e ID de resultados por lugar; preservar las referencias fuente
IdentidadKalshi tickers de eventos y mercadosMantenga la agrupación de eventos separada del mercado negociable
PrecioPolymarket oferta/demanda del libro de pedidos o precio relacionado con la visualizaciónAlmacene el tipo de cotización y la dirección del resultado explícitamente
PrecioKalshi cadenas de dólares de punto fijoAnalizar cadenas decimales utilizando unidades declaradas; no inferir centavos
Ciclo de vidaLos campos de estado y tiempo de cada lugarUtilice un mapeo revisado; conservar el estado sin procesar y los valores desconocidos

3. Definir un esquema canónico limitado

El módulo descargable espera un contrato de adaptador documentado. Los campos de identidad son cadenas no vacías, incluidas identificaciones de aspecto numérico grandes. La clave es una tupla serializada de lugar, mercado y resultado, que evita colisiones causadas por la unión de identificadores sin escape con puntuación. Nunca convierta un ID de token en un número de JavaScript simplemente porque contiene dígitos.

Asigne cada resultado de origen a condition_true o condition_false mediante una regla explícita. La posición cero de la matriz no es un sinónimo universal de Sí. Mantenga la oferta, la demanda, el último y el punto medio como tipos de cotización separados. Este ejemplo maneja contratos binarios con un valor nominal $1 establecido; no admite canastas, pagos variables o transformaciones de múltiples resultados.

Separe fuente en, recibida en, cierre en, los términos observación en y resolución en. La hora fuente describe la cita; el tiempo de recepción describe la ingestión; el tiempo de cierre describe la frontera comercial; El tiempo de observación describe el evento que se está midiendo. Un mercado puede dejar de operar antes de que se conozca su resultado. El tiempo de resolución esperado, si su producto lo necesita, pertenece a otro campo que acepta valores NULL en lugar de reemplazar el tiempo de resolución real.

  • Asigne valores del ciclo de vida del lugar no reconocidos a un estado desconocido explícito y alerte para su revisión.
  • Requerir marcas de tiempo canónicas UTC ISO en este límite; convierta formatos de origen y zonas horarias en el adaptador de lugar.
  • Conservar nulo cuando falta una cita o una marca de tiempo de origen. No convierta los datos faltantes en un precio cero.
  • Almacene identificadores de lugar sin procesar y referencias de reglas fuera del modelo de ejemplo compacto al crear el adaptador real.

4. Analizar precios decimales sin redondeo silencioso

El ejemplo escala un precio en dólares a millonésimas enteras utilizando la aritmética BigInt y luego devuelve una cadena segura JSON. Las cadenas "0.615" y "0.6150" se convierten en "615000". Una entrada de centavos heredada o sintética declarada por separado de “61,5” produce el mismo resultado. Esa opción de centavos demuestra unidades explícitas; no se presenta como el formato de respuesta actual de Kalshi.

Sólo se aceptan cadenas decimales sin signo o nulas. Se rechazan la notación de exponentes, los números de punto flotante, los precios fuera de rango y la precisión que no se puede representar exactamente. Se aceptan ceros finales adicionales cuando el valor sigue siendo exacto. Esta política obliga al propietario del adaptador a tomar una decisión deliberada y precisa en lugar de cambiar silenciosamente una cotización.

La escala es una elección de enseñanza, no una afirmación de que cada lugar o esquema futuro utilice seis decimales. Valide los intervalos de precios permitidos, la precisión cuantitativa y la semántica de campos del proveedor actual por separado. El precio, la cantidad, el volumen, el interés nocional y abierto son medidas diferentes; no los ejecute todos a través de la misma función ni etiquete un precio negociado como una probabilidad calibrada de forma independiente.

5. Rechazar coincidencias falsas en las reglas de resolución

Títulos similares no prueban contratos equivalentes. La documentación de resolución de Polymarket explica que las reglas de resolución definen el origen y el manejo de los casos extremos. Preservar una referencia a esas reglas y revisar las enmiendas. Un mercado que mide una observación oficial publicada puede diferir materialmente de uno que mide una lectura preliminar u otra fuente, incluso si ambos títulos mencionan el mismo evento.

Nuestro registro sintético compara un tema revisado, un predicado, un tiempo de observación, una fuente de resolución, una regla de nulidad y términos de pago, además de la dirección del resultado canónico. El predicado debe incluir su operador: “mayor que 100” difiere de “al menos 100”. Las diferencias producen motivos de rechazo en lugar de una puntuación de confianza confusa que el código posterior podría confundir con aprobación.

La bandera revisada es sólo un marcador de posición de enseñanza. En producción, acepte asignaciones revisadas de un registro versionado y autenticado con identidad del revisor, referencias de reglas de origen e historial de cambios. La igualdad del texto no es una prueba semántica: el comparador verifica valores estructurados que alguien ya ha revisado. No se debe permitir que un adaptador malicioso o equivocado declare mercados arbitrarios equivalentes.

El ejemplo también rechaza tiempos de cierre diferentes o faltantes como puerta de revisión conservadora. Dos contratos pueden medir el mismo evento y cerrarse en momentos diferentes, por lo que esto crea intencionalmente algunos falsos negativos. Mantenga la equivalencia semántica y las ventanas comerciales comparables como decisiones de producción separadas. Incluso una coincidencia aprobada no dice nada sobre la disponibilidad de ejecución, tarifas, garantías, liquidez o una operación rentable.

6. Ejecute el ejemplo de normalización sin conexión.

Descargue predict-market-normalization.mjs usando el enlace a continuación, guárdelo localmente y ejecútelo con una versión de Node.js actualmente compatible. No tiene dependencias de paquetes. La fuente completa impresa en esta página es el mismo archivo proporcionado por el enlace de descarga, por lo que la explicación y el artefacto ejecutable permanecen alineados.

Los dos lugares ficticios informan precios de oferta iguales pero especifican diferentes fuentes de resolución. La normalización tiene éxito porque cada registro es estructuralmente válido. Luego, la comparación rechaza el par porque no cumple con los criterios de coincidencia revisados. Esta separación es útil: un registro válido aún puede no ser adecuado para una comparación entre lugares.

Ejecute el módulo descargado
node prediction-market-normalization.mjs
Rendimiento esperado de accesorios sintéticos
{
  "prices": ["615000", "615000"],
  "comparison": {
    "equivalent": false,
    "reasons": ["Different resolutionSource"]
  }
}

7. Trate la frescura y la recuperación del transporte como controles separados.

Un mensaje recibido no es necesariamente una cita reciente. La función de actualización del ejemplo utiliza un tiempo de evaluación explícito y una edad máxima, señala el tiempo de origen faltante y requiere una revisión del reloj cuando las marcas de tiempo son futuras o son inconsistentes. No se infiere que una última operación nueva sea una solicitud ejecutable. Aplique puertas de revisión de reglas, tipo de cotización y estado de mercado por separado.

Un flujo de producción también necesita un comportamiento de reconexión: mantener la última secuencia aceptada o punto de control de origen cuando esté disponible, detectar brechas, obtener una instantánea nueva y aplicar actualizaciones de acuerdo con el contrato de pedido del lugar. Deduplicar mediante una identidad de origen documentada; no asuma que las marcas de tiempo son únicas. Marque el feed como obsoleto mientras la recuperación esté incompleta.

Mantenga registros sin procesar delimitados para la depuración de acuerdo con los permisos de su proveedor y los requisitos de retención. Conservar la versión del adaptador, la hora de recepción, los errores de validación y la versión de revisión de reglas. Si un cambio de esquema elimina un campo obligatorio, póngalo en cuarentena y genere una señal operativa. Llenar el campo faltante con cero ocultaría una tubería rota detrás de un gráfico de apariencia plausible.

8. Valide los casos extremos antes de conectar un panel

El código descargable se ejerce mediante pruebas automatizadas del sitio web. Estos casos también son útiles controles de aceptación para su propio adaptador. Agregue dispositivos de proveedores reales y autorizados y pruebas de transporte al conectarse en vivo APIs; el módulo sintético por sí solo no puede verificar la disponibilidad de la fuente, el acceso a los puntos finales o la interpretación de las reglas del mercado.

Entrada o cambioComportamiento esperadoProblema que previene
Cotización nula y cero realConservar dos valores diferentesFaltan datos mostrados como precio
Cadenas decimales en unidades declaradasCadena entera normalizada exactaCentavos/dólares y errores de punto flotante
Exceso de precisión o unidades desconocidasRechazar en lugar de redondear o adivinarCorrupción de citas silenciosas
ID de token o mercado grandePreservar la identidad de la cadenaPérdida de precisión numérica
Dirección de resultado opuestaRechazar el partidoSí/No inversión
Se modificó la fuente, el predicado, el tiempo o la regla de nulidad.Rechazar mapeo pendiente de revisiónFalsa equivalencia entre sedes
Cerrado pero sin resolverMantener resuelto en nuloReclamaciones de acuerdos prematuros
Hora fuente obsoleta, futura o ausenteBandera obsoleta, revisión de reloj o faltanteFrescura engañosa

9. Extiende el adaptador sin perder procedencia

Inicie una integración real eligiendo una versión de lugar API específica y un conjunto de mercado pequeño. Escriba un documento de mapeo que identifique la fuente de cada campo canónico, sus unidades, comportamiento nulo y regla de validación. Mantenga las cantidades de origen y la profundidad del libro de pedidos en sus propias estructuras decimales exactas en lugar de agregar números no validados al registro de precios.

A continuación, agregue pruebas de ciclo de vida, elementos históricos autorizados y manejo de cambios de reglas. Decida cómo se revoca una asignación revisada cuando cambian las reglas de origen y cómo se enteran los consumidores de que su comparación anterior ya no está aprobada. Un directorio de mercado, un feed de precios y un registro de reglas tienen diferentes necesidades de actualización; hacer visibles esas responsabilidades en el diseño.

Para un panel o producto de alerta, muestre el lugar, el tipo de cotización, la hora de origen, la actualidad y el estado de revisión de las reglas junto con el precio. Si la interfaz colapsa esas distinciones, un backend correcto aún puede producir una comparación engañosa. Cree el formato de visualización y exportación a partir del mismo modelo validado.

Fuente ejecutable completa

predict-market-normalization.mjs — accesorios sintéticos, sin llamadas de red
// 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))
}

Cree una integración de datos de mercado de predicción

Gizmolab ayuda a los equipos a analizar los adaptadores de lugares, los esquemas canónicos, la recuperación de feeds y las interfaces de productos. Comience con los lugares requeridos, los derechos de datos y los criterios de aceptación.

Explorar la integración de Predict API →