Нормализация рыночных данных прогнозирования: Polymarket и Kalshi
Нормализуйте данные Polymarket и Kalshi без смешивания результатов, цен или правил разрешения. Учебное пособие по Node.js с работоспособным примером и контрольным списком проверки.
Gizmolab Команда · Опубликовано · 16 мин чтения
В этом уроке
- 1. Нормализуйте значение, прежде чем сравнивать цены
- 2. Явно сопоставьте идентификаторы площадок и семантику цитат.
- 3. Определить узкую каноническую схему
- 4. Анализ десятичных цен без автоматического округления
- 5. Отклоняйте ложные совпадения в правилах разрешения.
- 6. Запустите пример автономной нормализации.
- 7. Рассматривайте свежесть и восстановление транспорта как отдельные проверки.
- 8. Проверка крайних случаев перед подключением информационной панели
- 9. Удлините адаптер, не теряя происхождения.
- Полный работоспособный исходный код
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 для конкретной площадки и небольшой набор рынков. Напишите документ сопоставления, который идентифицирует источник каждого канонического поля, его единицы измерения, нулевое поведение и правила проверки. Сохраняйте количество источников и глубину книги заказов в их собственных точных десятичных структурах, а не добавляйте непроверенные числа в запись цен.
Затем добавьте тесты жизненного цикла, разрешенные исторические настройки и обработку изменений правил. Решите, как проверенное сопоставление будет отменено при изменении исходных правил и как потребители узнают, что их предыдущее сравнение больше не одобрено. Рыночный каталог, поток цен и реестр правил имеют разные потребности в обновлении; сделайте эти обязанности видимыми в дизайне.
Для информационной панели или продукта с оповещениями рядом с ценой укажите место проведения, тип предложения, время источника, актуальность и состояние проверки правил. Если интерфейс стирает эти различия, правильный бэкэнд все равно может привести к вводящему в заблуждение сравнению. Создайте формат отображения и экспорта на основе одной и той же проверенной модели.
Полный работоспособный исходный код
// 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 →