? Работа с сырыми спортивными коэффициентами — это как пытаться собрать модель корабля из разбросанных деталей конструктора. Без инструкции. И с половиной лишних запчастей.


Контекст

Я аналитик в финтех-компании, и одна из наших задач — автоматический анализ спортивных линий. Цель — находить сигналы для прогнозов, искать переоценённые исходы, аномалии, иногда даже арбитражные окна.

Идея простая: подключить внешний источник с коэффициентами, привести данные в порядок и прогонять через нашу модель.

На этапе тестов выбор пал на raceodds.net — платформу с открытым API и охватом по сотням лиг, включая мелкие чемпионаты. И казалось бы — бери и используй. Но...


Проблемы на входе

Подключился к их API — и сразу несколько сюрпризов:

1. Шум и мусор в ответах

Каждый ответ — это массив из 50–70 полей, из которых реально нужно 6–8. Остальное: технические идентификаторы, флаги, устаревшие ключи, пустые поля.

2. Неоднородность форматов

  • Даты в разных форматах: 2025-07-21T19:30:00Z, 21/07/2025 19:30, unix timestamp

  • Названия команд отличались даже внутри одной лиги:

  • Manchester Utd, Man United, Manchester United FC

3. Дублирование событий

Одно и то же событие приходило под разными ID — от разных источников, но с разными наборами маркетов и коэффициентов. Часто — с разной актуальностью.

4. Отсутствие нормализации маркетов

Например, рынок "Обе забьют" представлен как:

BTTS, BothTeamsScore, BTS, goalgoal — всё это одно и то же, но в базе они шли как независимые записи.


Как я это решил: пайплайн

Примерно через неделю экспериментов и ругани с JSON-ответами API, решил собрать собственный ETL-процесс, который решил сразу несколько задач.

1. Сбор данных: fetcher.py

  • Параллельная выгрузка по лигам через aiohttp

  • Лимит на частоту запросов (иначе API режет)

  • Авто-ретрай при 5xx/timeout

python

async def fetch_league_data(session, league_id):
    async with session.get(f"{API_BASE}/odds?league={league_id}") as resp:
        return await resp.json(

2. Нормализация: normalizer.py

  • Все даты — в UTC ISO 8601

  • Команды — через словарь соответствий + fuzzy matching (fuzzywuzzy)

  • Названия маркетов — через enum-сопоставление

  • Поля удаляю жёстким whitelist-фильтром

python

NORMALIZED_FIELDS = ['event_id', 'league', 'teams', 'start_time', 'market', 'odds']

def clean_entry(entry):
    return {k: entry[k] for k in NORMALIZED_FIELDS if k in entry}

3. Объединение событий: deduplicator.py

  • События считаются одним, если:

  • Разница во времени < 10 минут

  • Названия команд совпадают на ≥ 85%

  • Лига та же

  • Для оценки схожести использую Levenshtein distance

python

def is_same_event(a, b):
    return (a['league'] == b['league'] and
            abs(a['start_time'] - b['start_time']) < 600 and
            fuzz.ratio(a['teams'][0], b['teams'][0]) > 85)

4. Хранилище: PostgreSQL

  • Данные пишутся в две таблицы:

  • events_normalized — уникальные события

  • odds_snapshots — история изменения коэффициентов по времени

Добавлен индекс по (event_id, market, timestamp) — чтобы быстро вытаскивать движение линии.


Результат: аналитика, а не каша

После нормализации и структурирования данных, стало возможным:

  • Анализировать движение линии по времени: видно, как изменяется коэффициент на победу команды X за 24 часа до матча

  • Выявлять «запаздывающих» букмекеров: кто дольше всех реагирует на изменение рынка

  • Выделять value-bets: если коэффициент у одной БК выбивается из общего тренда — сигнал


Визуализация

Собрал простой дашборд на Streamlit:

  • Поиск по лигам и датам

  • График движения коэффициентов по событиям

  • Таблица отклонений между букмекерами

  • Фильтр по аномалиям (например, скачки >10% за 1 час)

Вот пример графика по матчу «Лидс — Суонси»:

? Коэф. на победу Лидса:

→ вырос с 2.10 до 2.65 за 3 часа — новость о травме капитана


Что стало дальше

  • Добавил вторичный источник (Betdata.io) и теперь сравниваю скорость обновлений

  • Планирую натренировать LightGBM-модель на признаках движения линии, чтобы предсказывать вероятность value-бета

  • Подумываю об open-source версии нормализатора — таких кейсов ещё много


Вывод

На старте казалось, что raceodds.net — это куча неструктурированного шума. Но оказалось, что если приложить немного усилий (и много нормализации), можно превратить этот поток в мощный источник для спортивной аналитики.

Главное — не бояться грязных данных. Они — как руда. Пока не переработаешь — золота не будет.

Комментарии (0)