? Работа с сырыми спортивными коэффициентами — это как пытаться собрать модель корабля из разбросанных деталей конструктора. Без инструкции. И с половиной лишних запчастей.
Контекст
Я аналитик в финтех-компании, и одна из наших задач — автоматический анализ спортивных линий. Цель — находить сигналы для прогнозов, искать переоценённые исходы, аномалии, иногда даже арбитражные окна.
Идея простая: подключить внешний источник с коэффициентами, привести данные в порядок и прогонять через нашу модель.
На этапе тестов выбор пал на 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 — это куча неструктурированного шума. Но оказалось, что если приложить немного усилий (и много нормализации), можно превратить этот поток в мощный источник для спортивной аналитики.
Главное — не бояться грязных данных. Они — как руда. Пока не переработаешь — золота не будет.