Перейти к содержимому
Инженерные руководства

Нормализация рыночных данных прогнозирования: Polymarket и Kalshi

Нормализуйте данные Polymarket и Kalshi без смешивания результатов, цен или правил разрешения. Учебное пособие по Node.js с работоспособным примером и контрольным списком проверки.

Gizmolab Команда · Опубликовано · 16 мин чтения

1. Нормализуйте значение, прежде чем сравнивать цены

Нормализация рыночных данных прогнозов преобразует записи, относящиеся к конкретному месту проведения, в общую модель, сохраняя при этом информацию, необходимую для их интерпретации. Самое сложное — не переименовывать поля. Это гарантирует, что две записи относятся к одному и тому же состоянию, направлению результата, времени наблюдения и условиям разрешения, прежде чем информационная панель сопоставит их цены рядом.

В этом руководстве создается автономный пример Node.js для приема и проверки. Он использует вымышленные рынки и явно определенные записи адаптеров, а не живые ответы Polymarket или Kalshi. Здесь нет ключей API, сетевых запросов, ордеров или операций с кошельком. Целью является небольшая, проверяемая граница, которую можно проверить перед подключением реального источника.

Сохраняйте исходную исходную информацию или авторизованную архивную ссылку вместе с нормализованной записью. Полезный конвейер имеет три уровня: необработанный ответ места проведения, адаптер места проведения с поддержкой версий и проверенную общую модель. Когда поставщик меняет поле или правило, вы можете определить, какой адаптер создал запись, и повторно обработать ее, не догадываясь, что означало старое значение.

2. Явно сопоставьте идентификаторы площадок и семантику цитат.

Polymarket различает события, разрешимые рынки и идентификаторы токенов результата. Сохраняйте эту идентичность, а не сводите название мероприятия к ключу глобального рынка. Его ценовая документация также отличает отображаемые цены от цен спроса и предложения, доступных в книге заказов. Последняя сделка, средняя точка и исполняемая котировка должны иметь разные метки в вашей модели.

Текущая документация книги заказов Kalshi описывает пары строк с фиксированной точкой в orderbook_fp, включая yes_dollars и no_dollars. Его ссылка с фиксированной точкой определяет единицы измерения, используемые в полях цены и количества. Сохраняйте исходные строки во время анализа. Не копируйте старое предположение о целых центах в адаптер для текущего поля долларовой строки.

Kalshi документирует, как взаимодополняющие стороны заявок могут подразумевать запросы на бинарную книгу заказов. Если ваш адаптер рассчитывает цену таким образом, запишите деривацию и источник. Не применяйте это преобразование к произвольному рынку с множеством исходов и не путайте производный запрос с напрямую предоставленной котировкой. В приведенном ниже примере принимается уже определенный тип котировки и не восстанавливается книга заказов.

БеспокойствоИсточник для конкретного места проведенияПравило общей модели
идентичностьPolymarket идентификаторы событий, условий и токенов результатаРынок пространств имен и идентификаторы результатов по местам проведения; сохранять ссылки на источники
идентичностьKalshi событие и тикеры рынкаДержите группировку событий отдельно от торгуемого рынка.
ЦенаPolymarket цена покупки/продажи в книге заказов или цена, связанная с отображениемЯвно сохраняйте тип цитаты и направление результата.
ЦенаKalshi долларовые строки с фиксированной точкойРазбирать десятичные строки, используя объявленные единицы измерения; не делайте выводов о центах
Жизненный циклПоля статуса и времени каждого объектаИспользуйте проверенное сопоставление; сохранить необработанный статус и неизвестные значения

3. Определить узкую каноническую схему

Загружаемый модуль ожидает документированного контракта адаптера. Поля идентификаторов представляют собой непустые строки, включая большие числовые идентификаторы. Ключ представляет собой сериализованный кортеж места проведения, рынка и результата, который позволяет избежать коллизий, вызванных объединением неэкранированных идентификаторов со знаками препинания. Никогда не преобразуйте идентификатор токена в номер JavaScript только потому, что он содержит цифры.

Сопоставьте каждый исходный результат с условием_истина или условие_ложь с помощью явного правила. Нулевая позиция массива не является универсальным синонимом «Да». Сохраняйте бид, аск, последнюю и среднюю точку как отдельные виды котировок. В этом примере обрабатываются бинарные контракты с указанной номинальной стоимостью $1; он не поддерживает корзины, переменные выплаты или преобразования с несколькими исходами.

Отдельные sourceAt, gotAt, closeAt, термины ObserveAt и ResolveAt. Время источника описывает цитату; время получения описывает прием; время закрытия описывает торговую границу; время наблюдения описывает измеряемое событие. Рынок может прекратить торговлю до того, как станет известен ее результат. Ожидаемое время разрешения, если оно необходимо вашему продукту, находится в другом поле, допускающем значение NULL, а не заменяет фактическое время разрешения.

  • Сопоставьте нераспознанные значения жизненного цикла объекта с явным неизвестным состоянием и уведомите о необходимости проверки.
  • Требовать канонические временные метки UTC ISO на этой границе; конвертировать исходные форматы и часовые пояса в адаптере объекта.
  • Сохраняйте значение null, если цитата или временная метка источника отсутствуют. Не превращайте недостающие данные в нулевую цену.
  • При создании реального адаптера храните необработанные идентификаторы площадок и ссылки на правила за пределами компактного примера модели.

4. Анализ десятичных цен без автоматического округления

В примере цена в долларах масштабируется до целых миллионных долей с использованием арифметики BigInt, а затем возвращается строка, безопасная для JSON. Строки «0,615» и «0,6150» становятся «615000». Отдельно объявленный ввод устаревших или синтетических центов «61,5» дает тот же результат. Этот вариант центов демонстрирует явные единицы измерения; он не представлен как текущий формат ответа Kalshi.

Принимаются только десятичные строки без знака или ноль. Экспоненциальная запись, числа с плавающей запятой, цены, выходящие за пределы диапазона, и точность, которую невозможно представить точно, отклоняются. Дополнительные конечные нули принимаются, если значение остается точным. Эта политика вынуждает владельца адаптера принимать обдуманное и точное решение вместо того, чтобы незаметно менять цену.

Шкала — это выбор преподавателя, а не утверждение, что в каждом месте или будущей схеме используются шесть десятичных знаков. Отдельно проверьте допустимые интервалы цен текущего поставщика, точность количества и семантику полей. Цена, количество, объем, условный и открытый интерес — это разные меры; не запускайте их все через одну и ту же функцию и не называйте торговую цену независимо калиброванной вероятностью.

5. Отклоняйте ложные совпадения в правилах разрешения.

Подобные титулы не доказывают эквивалентность контрактов. В документации по разрешению Polymarket объясняется, что правила разрешения определяют источник и обработку крайних случаев. Сохраните ссылку на эти правила и просмотрите поправки. Рынок, измеряющий официальное опубликованное наблюдение, может существенно отличаться от рынка, измеряющего предварительные показания или другой источник, даже если в обоих заголовках упоминается одно и то же событие.

Наш синтетический реестр сравнивает рассматриваемый объект, предикат, время наблюдения, источник решения, правило недействительности и условия выплат, а также каноническое направление результата. Предикат должен включать свой оператор: «больше 100» отличается от «минимум 100». Различия создают причины отклонения, а не нечеткую оценку достоверности, которую нижестоящий код может принять за одобрение.

Проверенный флаг является лишь обучающим заполнителем. В рабочей среде принимайте проверенные сопоставления из проверенного реестра с поддержкой версий с идентификатором проверяющего, ссылками на исходные правила и историей изменений. Равенство текста не является семантическим доказательством: компаратор проверяет структурированные значения, которые кто-то уже проверил. Злонамеренному или ошибочному адаптеру нельзя позволять объявлять произвольные рыночные эквиваленты.

В этом примере также отклоняются отличающиеся или отсутствующие времена закрытия в качестве консервативного критерия проверки. Два контракта могут измерять одно и то же событие, закрываясь в разное время, поэтому это намеренно создает ложноотрицательные результаты. Сохраняйте семантическую эквивалентность и сопоставимые торговые окна как отдельные производственные решения. Даже утвержденное совпадение ничего не говорит о доступности исполнения, комиссиях, залоге, ликвидности или прибыльной сделке.

6. Запустите пример автономной нормализации.

Загрузите Predict-market-normalization.mjs по ссылке ниже, сохраните его локально и запустите с поддерживаемой в настоящее время версией Node.js. Он не имеет зависимостей от пакетов. Полный исходный код, напечатанный на этой странице, представляет собой тот же файл, который содержится в ссылке для скачивания, поэтому объяснение и работоспособный артефакт остаются согласованными.

Две вымышленные площадки сообщают о равных ценах заявок, но указывают разные источники разрешения. Нормализация успешна, поскольку каждая запись структурно допустима. Затем при сравнении пара отклоняется, поскольку они не соответствуют рассмотренным критериям соответствия. Такое разделение полезно: действительная запись может оказаться непригодной для сравнения между площадками.

Запускаем скачанный модуль
node prediction-market-normalization.mjs
Ожидаемый результат от синтетических светильников
{
  "prices": ["615000", "615000"],
  "comparison": {
    "equivalent": false,
    "reasons": ["Different resolutionSource"]
  }
}

7. Рассматривайте свежесть и восстановление транспорта как отдельные проверки.

Полученное сообщение не обязательно является недавней цитатой. Функция свежести в примере использует явное время оценки и максимальный возраст, помечает отсутствие исходного времени и требует проверки часов, когда временные метки находятся в будущем или несовместимы. Это не означает, что новая последняя сделка является исполняемым аском. Применяйте шлюзы типа котировки, статуса рынка и проверки правил отдельно.

Производственный поток также требует повторного подключения: сохраняйте последнюю принятую последовательность или контрольную точку источника, если она доступна, выявляйте пробелы, получайте свежий снимок и применяйте обновления в соответствии с договором заказа с местом проведения. Дедупликация с использованием документированной идентификации источника; не думайте, что временные метки уникальны. Отметьте канал как устаревший, пока восстановление не завершено.

Сохраняйте ограниченные необработанные записи для отладки в соответствии с разрешениями вашего провайдера и требованиями к хранению. Сохранение версии адаптера, времени получения, ошибок проверки и версии проверки правил. Если в результате изменения схемы удаляется обязательное поле, поместите его в карантин и подайте рабочий сигнал. Заполнение недостающего поля нулем скроет сломанный конвейер за правдоподобным графиком.

8. Проверка крайних случаев перед подключением информационной панели

Загружаемый код проверяется автоматическими тестами веб-сайта. Эти случаи также являются полезной приемочной проверкой вашего собственного адаптера. Добавляйте реальные, разрешенные устройства провайдера и транспортные тесты при подключении в реальном времени APIs; синтетический модуль сам по себе не может проверить доступность источника, доступ к конечной точке или интерпретацию рыночных правил.

Ввод или изменениеОжидаемое поведениеПроблема, которую он предотвращает
Нулевая цитата и реальный нольСохраните два разных значенияОтсутствующие данные показаны в виде цены.
Десятичные строки в объявленных единицах измеренияТочная нормализованная целочисленная строкаЦенты/доллары и ошибки с плавающей запятой
Чрезмерная точность или неизвестные единицы измерения.Отклонять, а не округлять или гадатьПовреждение тихой цитаты
Большой идентификатор рынка или токенаСохранять идентичность строкиПотеря точности числа
Противоположное направление результатаОтклонить совпадениеДа/Нет инверсия
Изменено правило источника, предиката, времени или недействительности.Отклонить ожидающее рассмотрения сопоставлениеЛожная эквивалентность между площадками
Закрыто, но не решеноОставить решеннымНольПретензии о досрочном урегулировании
Устаревшее, будущее или отсутствующее исходное времяПометка устарела, просмотр часов или отсутствуетОбманчивая свежесть

9. Удлините адаптер, не теряя происхождения.

Начните настоящую интеграцию, выбрав версию API для конкретной площадки и небольшой набор рынков. Напишите документ сопоставления, который идентифицирует источник каждого канонического поля, его единицы измерения, нулевое поведение и правила проверки. Сохраняйте количество источников и глубину книги заказов в их собственных точных десятичных структурах, а не добавляйте непроверенные числа в запись цен.

Затем добавьте тесты жизненного цикла, разрешенные исторические настройки и обработку изменений правил. Решите, как проверенное сопоставление будет отменено при изменении исходных правил и как потребители узнают, что их предыдущее сравнение больше не одобрено. Рыночный каталог, поток цен и реестр правил имеют разные потребности в обновлении; сделайте эти обязанности видимыми в дизайне.

Для информационной панели или продукта с оповещениями рядом с ценой укажите место проведения, тип предложения, время источника, актуальность и состояние проверки правил. Если интерфейс стирает эти различия, правильный бэкэнд все равно может привести к вводящему в заблуждение сравнению. Создайте формат отображения и экспорта на основе одной и той же проверенной модели.

Полный работоспособный исходный код

предсказание-рынок-нормализация.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 →