Skip to content

Обнаружение событий и рынков ​

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

Официальный внешний контракт: Polymarket — List events (keyset pagination) и List markets (keyset pagination).

Назначение и граница ​

discovery — единственная подсистема, которая обнаруживает события и рынки, поддерживает их фактическое состояние и передаёт одни и те же данные нескольким движкам стратегий. Она не принимает торговых решений и не применяет фильтры допуска конкретной стратегии к каноническим данным.

EventRevision является неизменяемым корнем сквозного EventContext. Discovery публикует контекст стадии Discovered; последующие модули обогащают его новыми типизированными результатами, не копируя и не изменяя фактическую ревизию события.

Единица обнаружения, хранения, публикации и будущего шардирования — событие Gamma с ключом event_id. event_slug является отображаемым атрибутом и не может использоваться как ключ шардирования. condition_id используется как индекс рынка внутри события, но не как запасной ключ события: неизвестная принадлежность рынку к событию означает незавершённое обнаружение, а не право поместить рынок в отдельный шард.

text
Gamma /events/keyset ─┐
Gamma /markets/keyset ├──▶ discovery ──▶ EventRevisionStore
Market WS lifecycle ─┘                      │
                                            ├──▶ StrategyUniverse A
                                            ├──▶ StrategyUniverse B
                                            ├──▶ shard(event_id)
                                            └──▶ shared publisher

Границы модулей ​

Клиенты pm-sdk ​

  • GammaClient — единственный HTTP-клиент Gamma. Он владеет базовым URL, путями endpoint'ов, тайм-аутом, общим ограничителем частоты всех Gamma-запросов, отправкой, чтением тела, десериализацией и типизированными ошибками.
  • Все вызовы /events/keyset и /markets/keyset используют один и тот же Arc<GammaClient> и общий бюджет запросов. Создание отдельного HTTP-клиента или отдельного ограничителя внутри discovery запрещено.
  • MarketWsPool остаётся единым WS-клиентом. Один раз разобранные кадры разделяются по типизированным темам через Arc, как определено в event-context.md. Модуль discovery владеет одной lifecycle-подпиской; композиционный корень не создаёт отдельные подписки для листинга и закрытия.
  • Клиенты отражают внешний протокол и не содержат конфигурацию, фильтры допуска или решения конкретной стратегии.

Модуль pm-engine::adapters::market_data::discovery ​

text
discovery/
├── mod.rs          — публичная граница адаптера
├── runtime.rs      — таймеры, одна WS-подписка, остановка задач
├── coverage.rs     — профили покрытия и общий план запросов
├── events.rs       — полные проходы /events/keyset
├── markets.rs      — /markets/keyset по известным condition_ids
├── lifecycle.rs    — маршрутизация NewMarket и MarketResolved
├── listing.rs      — пакетная гидратация новых рынков
├── closure.rs      — немедленное закрытие и REST-сверка
├── model.rs        — EventRevision и MarketFact
├── store.rs        — атомарные ревизии и индексы
└── projection.rs   — формирование представления для потребителя

Разбиение на файлы может меняться без изменения архитектуры. Нормативны ответственности и направления зависимостей: транспорт находится в pm-sdk, фактическая модель и координация источников — в discovery, торговая политика — в движке стратегии.

Каноническая фактическая модель ​

rust
struct EventRevision {
    event_id: EventId,
    event_slug: String,
    revision: u64,
    observed_at: UnixMillis,
    markets: Arc<[MarketFact]>,
}

EventRevision содержит полный упорядоченный массив Event.markets[] до локальных фильтров рынка, окна входа и политики стратегии. Ревизия публикуется атомарно: потребитель видит либо предыдущую целую ревизию, либо следующую целую ревизию. Частично разобранное событие не становится текущим.

MarketFact содержит как минимум condition_id, токены, признаки жизненного цикла, названия, market slug, даты, ликвидность, объём и поля, необходимые действующим потребителям. MarketMetadata — производное представление для движка, а не исходная модель discovery.

Хранилище поддерживает индексы:

  • event_id → Arc<EventRevision>;
  • condition_id → event_id;
  • token_id → (event_id, condition_id);
  • coverage_id → множество event_id.

Контракт GET /events/keyset ​

Транспорт и пагинация ​

Параметры транспорта не принадлежат стратегии:

Параметр GammaТипТребование системы
limitinteger1..=500; значение выбирает клиент или планировщик, без скрытого значения вне контракта
orderstringсписок имён JSON-полей; одно значение сохраняется на всём проходе
ascendingbooleanдействует только вместе с order; отдельное значение без order отвергается как неработающая настройка
after_cursorstringнепрозрачен; только сборщик страниц подставляет курсор предыдущего ответа
offsetintegerзапрещён и не представлен в типе запроса; сервер документированно отвечает 422

Все поля фильтра, порядок и направление сортировки остаются неизменными на протяжении одного прохода. Между страницами меняется только after_cursor. Конец определяется только отсутствием next_cursor. Размер страницы не является признаком конца. Пустой или повторный курсор означает отсутствие прогресса и отменяет весь проход. Произвольного потолка страниц нет.

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

Фильтры покрытия ​

Контракт запроса обязан поддерживать все документированные фильтры endpoint'а, а не только tag_id:

ГруппаПараметр GammaТип wireЛокальный тип и правило
Идентичностьidinteger[]Vec<GammaEventId>
Идентичностьslugstring[]Vec<EventSlug>; это фильтр события, не ключ шардирования
СостояниеclosedbooleanOption<bool>
СостояниеlivebooleanOption<bool>; не подменяет отсутствующий в keyset параметр active
СостояниеfeaturedbooleanOption<bool>
СостояниеcyombooleanOption<bool>
Текстtitle_searchstringсерверный поиск события по вхождению подстроки; строка передаётся без локального изменения регистра или разделителей
Ликвидностьliquidity_minnumberOption<Decimal>
Ликвидностьliquidity_maxnumberOption<Decimal>
Объёмvolume_minnumberOption<Decimal>
Объёмvolume_maxnumberOption<Decimal>
Датыstart_date_mindate-timeOption<UtcDateTime>
Датыstart_date_maxdate-timeOption<UtcDateTime>
Датыend_date_mindate-timeOption<UtcDateTime>
Датыend_date_maxdate-timeOption<UtcDateTime>
Датыstart_time_mindate-timeOption<UtcDateTime>
Датыstart_time_maxdate-timeOption<UtcDateTime>
Тегиtag_idinteger[]Vec<TagId>
Тегиtag_slugstringOption<TagSlug>
Тегиexclude_tag_idinteger[]Vec<TagId>; пересечение с tag_id запрещено внешним контрактом и отвергается до запроса
Тегиrelated_tagsbooleanOption<bool>
Тегиtag_matchstringнепрозрачное значение Gamma; система передаёт его дословно и не придумывает локальный enum без подтверждённого списка значений
Серияseries_idinteger[]Vec<SeriesId>
Играgame_idinteger[]Vec<GameId>
Календарьevent_datedate-timeOption<UtcDateTime>
Календарьevent_weekintegerOption<u32>
Выдачаfeatured_orderbooleanOption<bool>
Повторениеrecurrencestringнепрозрачная строка Gamma, передаваемая дословно
Авторcreated_bystring[]Vec<CreatorId>
Иерархияparent_event_idintegerOption<GammaEventId>
Иерархияinclude_childrenbooleanOption<bool>

active и archived отсутствуют в официальном контракте /events/keyset и не могут отправляться как скрытые параметры. Если потребителю нужны эти признаки, они проверяются по полям полной фактической ревизии после ответа. live не считается синонимом active.

Пустые и целиком пробельные строки для строковых фильтров отвергаются. Числовые границы ликвидности и объёма обязаны быть конечными и неотрицательными; для пары min/max выполняется min <= max. Временные значения сериализуются как RFC 3339 UTC; для каждой пары границ выполняется min <= max. Относительные выражения вроде «через сутки» не являются частью wire-контракта: до запроса они обязаны быть разрешены через управляемый ClockPort в конкретный UtcDateTime.

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

Параметры формы ответа ​

Следующие параметры не определяют торговую пригодность события. Они управляют дополнительными отношениями или локализацией ответа и поэтому отделены от EventDiscoveryFilter:

Параметр GammaТипНазначение по официальному контракту
partner_slugstringдобавляет external_partners для совпавших событий
include_chatbooleanдобавляет Chats и Series.Chats
include_templatebooleanдобавляет Templates
include_best_linesbooleanдобавляет BestLines
localestringвыбирает локаль ответа

Обычный discovery не запрашивает необязательные отношения, пока каноническая модель не имеет их фактического потребителя. Это предотвращает мёртвые поля и лишний объём ответа. Сам тип клиента обязан уметь выразить параметры, чтобы их будущий потребитель не создавал второй HTTP-путь.

Gamma всегда включает в успешный ответ Series, Tags, Markets и EventCreators. Вложенные рынки содержат clob_rewards и fee_schedule; компонент нормализации выбирает из ответа только поля канонической модели, но не может выбрасывать рынки события до проверки целостности ревизии.

Профили покрытия нескольких стратегий ​

Каждая стратегия объявляет один или несколько StrategyDiscoveryProfile. Профиль состоит из:

  1. списка точных EventDiscoveryFilter, определяющих область запросов Gamma;
  2. локальной StrategyUniversePolicy, определяющей, какие рынки из полной ревизии стратегия допускает к своей обработке.

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

rust
struct StrategyUniverseRevision {
    event: Arc<EventRevision>,
    admitted_markets: Vec<ConditionId>,
}

Стратегия получает ссылку на общую ревизию и собственный список допущенных рынков. Изменение конфигурации стратегии повторно строит её представление из текущего хранилища и не требует повторного HTTP-запроса, если профиль покрытия источника не изменился.

Планирование общих запросов ​

  • Побайтово одинаковые после типизированной сериализации фильтры разных стратегий выполняются один раз за такт и разделяют результат.
  • Различающиеся фильтры выполняются как отдельные keyset-проходы. Нельзя объединять их конкатенацией tag_id, выбором более широкого диапазона или переносом части условий на клиент: документация не задаёт общую AND/OR- семантику сочетаний, а такая оптимизация может изменить множество событий.
  • Один профиль может содержать несколько запросов. Их результаты образуют объединение по event_id только для этого профиля.
  • Общий такт заранее присваивает каждому уникальному запросу стабильный coverage_id из полного типизированного запроса. Завершение HTTP в другом порядке не меняет результат: конфликтующие наблюдения одного event_id разрешаются по стабильному порядку coverage_id, и более поздняя в этом порядке целая строка заменяет предыдущую целиком.
  • Частичного слияния полей двух строк одного события нет. Производственного счётчика или отдельной бизнес-сущности «дубликат» нет; повтор строки проверяет только корректность пагинации и детерминированность замещения.
  • Состав coverage_id → event_id публикуется только после полного успешного прохода. Ошибка не превращает неизвестность в пустое множество и не удаляет события из представления стратегии.

WS lifecycle внутри discovery ​

DiscoveryRuntime сам получает одну lifecycle-подписку от общего WS data plane. Она содержит только два события:

rust
enum LifecycleEvent {
    NewMarket { condition_id: ConditionId },
    MarketResolved {
        condition_id: ConditionId,
        winning_token_id: Option<TokenId>,
        ts_ms: u64,
    },
}

Остальные события Market WS принадлежат market_data; они не проходят через внутреннюю шину discovery и не заставляют discovery повторно разбирать тот же кадр.

  • NewMarket гидратируется пакетным /markets/keyset по condition_ids. Официальный ответ markets keyset включает связь Events; discovery обязан получить из неё event_id и полную принадлежность рынка. Если связь отсутствует, неоднозначна или ещё не опубликована Gamma, рынок остаётся в PendingEventAssignment до полного /events/keyset. Он не публикуется и не шардируется по condition_id.
  • После разрешения event_id новое наблюдение обновляет целую ревизию события, а затем все заинтересованные стратегии повторно строят свои представления.
  • MarketResolved для известного condition_id немедленно обновляет состояние рынка и выпускает ClosureEvent, не ожидая HTTP. Закрытие не ставится в одну очередь с медленной гидратацией листинга.
  • Периодический /markets/keyset?condition_ids=...&closed=true остаётся авторитетной сверкой отслеживаемых рынков после пропуска WS, отставания потребителя или переподключения.
  • Очередь гидратации новых рынков ограничена. Её переполнение может задержать обнаружение до следующего полного /events/keyset, но не может блокировать чтение WS или потерять закрытие известного рынка.

Шардирование и переиспользование ​

Полный контракт владения контекстом, ветвления нескольких стратегий и zero-copy передачи между стадиями определён в event-context.md. Этот раздел определяет только discovery-часть: выбор владельца по event_id и публикацию канонической ревизии до разветвления стратегий.

event_id разрешается до допуска рынка и является неизменным ключом назначения на всё время жизни события в обработке. Все рынки и токены одного события попадают в один шард. Изменение event_slug не меняет назначение.

Общая ревизия содержит только факты источника. Каждый движок стратегии владеет своей политикой, позициями, заявками и решениями. В одном процессе он получает Arc<EventRevision> из общего хранилища; в разных процессах shared-market-data-publisher переносит ту же целую ревизию с event_id, порядковым номером, эпохой, временем источника и контролем целостности. Издатель не применяет фильтры стратегии и не создаёт вторую модель события.

Медленный потребитель не создаёт противодавление источнику: он переходит в явное состояние разрыва и восстанавливается полным снимком. Потерянная или частичная ревизия не становится текущей и не выдаётся стратегии за свежие данные.

Проверяемые требования ​

До признания реализации соответствующей обязательны как минимум:

  • контрактный тест сериализации каждого параметра /events/keyset с точным wire-именем и типом;
  • отрицательные тесты offset, пересечения tag_id/exclude_tag_id, пустых строк, неверных диапазонов, ascending без order и подмены active/archived;
  • многостраничные тесты неизменности фильтра, передачи курсора, полной последней страницы без курсора, повторного курсора и ошибки промежуточной страницы;
  • тест атомарности EventRevision и запрета публикации частичного события;
  • тест одинаковых профилей двух стратегий с одним HTTP-проходом и разными локальными представлениями;
  • тест разных профилей без опасного объединения их фильтров;
  • тест event_id-шардирования всех рынков события и запрета fallback на event_slug или condition_id;
  • тест NewMarket без событийной связи: состояние pending, без допуска и без назначения шарда;
  • тест, что медленная гидратация листинга не задерживает MarketResolved;
  • тест, что WS-потеря восстанавливается полными events/markets keyset-проходами;
  • тест, что изменение фильтра одной стратегии не меняет вселенную другой.