Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions guides/api-and-header-contracts.md
Original file line number Diff line number Diff line change
Expand Up @@ -393,9 +393,13 @@ broker:
`range_complete` remains the provider's continuity authority. Router reports
incomplete history as operation-level `FAILED` and sticky `DEGRADED`, while
still allowing returned observations to be delivered. Reconnect recovery waits
for `READY`; exact overlap identity is `(time_ms, ask, bid, last, volume)`, so
`received_ms`/flags do not distinguish duplicates and different same-second
observations remain distinct.
for `READY`; history overlap identity is selected by
`TickSubscriptionRequest::continuity.deduplication_mode`. `TIMESTAMP` uses
only `time_ms`, `TIME_AND_PRICES` adds quote/trade prices, and
`EXACT_OBSERVATION` also adds volume. `received_ms`/flags never distinguish
duplicates. `PROVIDER_DEFAULT` uses the provider hook, which is timestamp
based for Intrade; choose `EXACT_OBSERVATION` when same-second observations
must remain distinct.

`MarketDataRouter` is the subscription-scoped alternative to `MarketDataHub`:

Expand Down
29 changes: 24 additions & 5 deletions guides/market-data-router.md
Original file line number Diff line number Diff line change
Expand Up @@ -675,6 +675,24 @@ request.continuity.max_backfill_ms = 60'000;
auto route = router.subscribe_ticks(provider, bot, request);
```

History-overlap identity is configurable per route through
`TickSubscriptionRequest::continuity.deduplication_mode`. The default
`PROVIDER_DEFAULT` uses `BaseMarketDataProvider::tick_deduplication_mode()`;
Intrade uses `TIMESTAMP` because its broker observations are one-second
snapshots. A route can override that choice when it needs richer identity:

```cpp
request.continuity.deduplication_mode =
md::MarketDataTickDeduplicationMode::TIME_AND_PRICES;
```

`TIMESTAMP` matches only `time_ms`, `TIME_AND_PRICES` matches `time_ms` plus
`ask`, `bid`, and `last`, and `EXACT_OBSERVATION` also matches `volume`.
`received_ms` and flags never identify an observation. The policy is applied
when resolving history overlap; it does not collapse distinct live events
before they are buffered or delivered. Use `EXACT_OBSERVATION` when distinct
same-timestamp snapshots must remain separate.

`PREFILL` requests the configured lookback before releasing live ticks.
`PREFILL_AND_RECOVER` also holds the live tail when the difference between
successive observed timestamps exceeds `expected_interval_ms`. That value is a
Expand All @@ -693,7 +711,7 @@ and is released after the completed range is verified. Bounded chunks keep
their size limit and overlap at the previous end point whenever that overlap
can advance the range; if the limit is smaller than a provider grid step,
Router advances to the next provider boundary instead of repeating the same
request. The overlap is removed only by exact observation identity.
request. History overlap is resolved with the selected tick identity policy.

The Router sends historical ticks first, marks them `HISTORICAL`, and then
replays held live ticks as `LIVE_SOURCE | CATCHUP`. A complete result is required
Expand All @@ -704,10 +722,11 @@ History requests are bounded by `max_backfill_ms` and are scheduled by
`process()`, so tick continuity does not create a timer thread.

On reconnect, tick continuity reports `STALE`, waits for `READY`, and requests
the unresolved range through the latest observed time. Exact overlap is removed
by `(time_ms, ask, bid, last, volume)` identity. `received_ms` and flags do not
make an otherwise identical observation distinct, while different observations
with the same second remain separate events. If the continuity buffer exceeds
the unresolved range through the latest observed time. History overlap is
removed according to the selected tick identity policy. The default provider
policy for Intrade is timestamp-based; use `EXACT_OBSERVATION` to retain
different observations with the same second. `received_ms` and flags do not
make an otherwise identical observation distinct. If the continuity buffer exceeds
its batch or item limit, Router releases the held live data, reports
`FAILED`/`DEGRADED`, disables continuity for that route, and resumes ordinary
live delivery. If transport is interrupted during the initial prefill, Router
Expand Down
31 changes: 25 additions & 6 deletions guides/market-data-router.ru.md
Original file line number Diff line number Diff line change
Expand Up @@ -832,6 +832,24 @@ request.continuity.max_backfill_ms = 60'000;
auto route = router.subscribe_ticks(provider, bot, request);
```

Identity history-overlap настраивается для каждого route через
`TickSubscriptionRequest::continuity.deduplication_mode`. Значение по умолчанию
`PROVIDER_DEFAULT` использует `BaseMarketDataProvider::tick_deduplication_mode()`;
для Intrade это `TIMESTAMP`, потому что broker observations имеют секундную
метку. При необходимости route может выбрать более подробную identity:

```cpp
request.continuity.deduplication_mode =
md::MarketDataTickDeduplicationMode::TIME_AND_PRICES;
```

`TIMESTAMP` сравнивает только `time_ms`, `TIME_AND_PRICES` добавляет `ask`,
`bid` и `last`, а `EXACT_OBSERVATION` также сравнивает `volume`.
`received_ms` и flags не являются частью identity. Политика используется при
разрешении history overlap и не схлопывает разные live events до их
buffering или delivery. Если разные snapshots одной секунды должны сохраниться,
выберите `EXACT_OBSERVATION`.

`PREFILL` запрашивает заданный lookback до освобождения live ticks.
`PREFILL_AND_RECOVER` дополнительно удерживает live tail, когда дельта между
последовательными timestamps больше `expected_interval_ms`. Это только
Expand All @@ -848,8 +866,8 @@ history sample: она остаётся в continuity buffer и выпускае
завершённого диапазона. Bounded chunks сохраняют лимит размера и перекрываются
в предыдущей конечной точке, когда такой overlap позволяет продвинуть диапазон.
Если лимит меньше шага provider grid, Router переходит к следующей provider
boundary, а не повторяет тот же запрос. Overlap удаляется только по exact
observation identity.
boundary, а не повторяет тот же запрос. History overlap разрешается по
выбранной tick identity policy.

Router сначала отправляет historical ticks с флагом `HISTORICAL`, затем
воспроизводит удержанные live ticks с флагами `LIVE_SOURCE | CATCHUP`. До
Expand All @@ -860,10 +878,11 @@ Router сначала отправляет historical ticks с флагом `HIS
создания отдельного timer thread.

После reconnect tick continuity публикует `STALE`, ждёт `READY` и запрашивает
unresolved range до последнего observed time. Exact overlap удаляется по
identity `(time_ms, ask, bid, last, volume)`; `received_ms` и flags не делают
полностью одинаковый snapshot новым, но разные observations той же секунды
сохраняются. При переполнении buffer Router освобождает live data, публикует
unresolved range до последнего observed time. History overlap удаляется по
выбранной tick identity policy. Provider default для Intrade использует только
timestamp; чтобы сохранить разные observations той же секунды, выберите
`EXACT_OBSERVATION`. `received_ms` и flags не делают полностью одинаковый
snapshot новым. При переполнении buffer Router освобождает live data, публикует
`FAILED`/`DEGRADED`, отключает continuity для этого route и возобновляет
обычную live delivery.
Если transport прервался во время initial prefill, после `READY` Router
Expand Down
11 changes: 11 additions & 0 deletions include/optionx_cpp/market_data/BaseMarketDataProvider.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,17 @@ namespace optionx::market_data {
return 0;
}

/// \brief Returns the provider's default tick identity policy.
/// \details Router uses this policy when a tick request leaves
/// `deduplication_mode` at PROVIDER_DEFAULT. The policy is
/// used only to remove observations repeated by inclusive
/// history overlap; it never collapses distinct live events
/// before they are buffered or delivered.
/// \return Provider-specific tick identity policy.
virtual MarketDataTickDeduplicationMode tick_deduplication_mode() const noexcept {
return MarketDataTickDeduplicationMode::EXACT_OBSERVATION;
}

/// \brief Requests a live tick stream subscription.
/// \param request Tick subscription parameters.
/// \param callback Callback receiving desired-subscription acceptance or failure.
Expand Down
22 changes: 22 additions & 0 deletions include/optionx_cpp/market_data/MarketDataContinuityOptions.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,15 @@ namespace optionx::market_data {
PREFILL_AND_RECOVER ///< Prefill and repair live/reconnect timestamp gaps.
};

/// \enum MarketDataTickDeduplicationMode
/// \brief Selects which tick fields identify an already delivered observation.
enum class MarketDataTickDeduplicationMode {
PROVIDER_DEFAULT = 0, ///< Use the provider's identity contract.
TIMESTAMP, ///< Treat one timestamp as one observation.
TIME_AND_PRICES, ///< Match timestamp, ask, bid, and last price.
EXACT_OBSERVATION ///< Match timestamp, prices, and volume.
};

/// \struct MarketDataContinuityRetryPolicy
/// \brief Configures bounded history retry attempts and exponential backoff.
struct MarketDataContinuityRetryPolicy {
Expand Down Expand Up @@ -82,10 +91,23 @@ namespace optionx::market_data {
MarketDataContinuityRetryPolicy retry;
std::size_t max_buffered_batches = 1024;
std::size_t max_buffered_items = 100000;
/// Identity policy used to remove inclusive history overlap.
/// Provider default keeps the policy provider-specific.
MarketDataTickDeduplicationMode deduplication_mode =
MarketDataTickDeduplicationMode::PROVIDER_DEFAULT;

/// \brief Returns true when the option combination is usable.
[[nodiscard]] bool valid() const noexcept {
if (!retry.valid()) return false;
switch (deduplication_mode) {
case MarketDataTickDeduplicationMode::PROVIDER_DEFAULT:
case MarketDataTickDeduplicationMode::TIMESTAMP:
case MarketDataTickDeduplicationMode::TIME_AND_PRICES:
case MarketDataTickDeduplicationMode::EXACT_OBSERVATION:
break;
default:
return false;
}
if (mode == MarketDataContinuityMode::LIVE_ONLY) {
return prefill_lookback_ms == 0;
}
Expand Down
Loading
Loading