Skip to content
Draft
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
5 changes: 5 additions & 0 deletions examples/market_data_continuity_example.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,11 @@ int main() {
request.continuity.mode = md::MarketDataContinuityMode::PREFILL_AND_RECOVER;
request.continuity.prefill_bars = 2;
request.continuity.max_backfill_bars = 10;
request.continuity.max_buffered_batches = 32;
request.continuity.max_buffered_items = 256;
request.continuity.retry.max_attempts = 3;
request.continuity.retry.initial_backoff_ms = 100;
request.continuity.retry.max_backoff_ms = 1000;

auto route = router.subscribe_bars(provider, chart, request);
if (!route.active()) {
Expand Down
24 changes: 20 additions & 4 deletions guides/api-and-header-contracts.md
Original file line number Diff line number Diff line change
Expand Up @@ -257,13 +257,29 @@ Contract rules:
optional timestamp-gap recovery. Router buffers live batches until the
corresponding history operation completes and reports route-scoped progress
through `IMarketDataSubscriber::on_market_data_continuity()`.
- `prefill_bars` requests inclusive timeframe slots ending at the start of the
current timeframe bucket. `max_buffered_batches` and `max_buffered_items`
bound live batches held during history; zero disables each limit. On buffer
overflow Router reports continuity `FAILED`, releases the held live data,
disables continuity for that route, and resumes with `LIVE` delivery.
- `MarketDataContinuityOptions::retry` bounds failed history requests with a
capped exponential backoff. Retries are scheduled by the periodic Router
`process()` call, so no continuity timer thread is created. Buffered live
batches remain withheld while retrying.
- Successful empty history is terminal: an empty prefill proceeds to live
delivery, while an empty gap backfill reports failed recovery and releases
buffered live data without retrying the same range.
- Router preserves provider bar revisions and leaves timestamp upsert or
deduplication to the consumer. A chart or storage component should upsert by
stream and `time_ms` when it needs one current candle value.
- Continuity updates carry the concrete provider subscription handle and must
not be confused with stream-level `MarketDataStatusUpdate`. History failure
does not terminate the live route: Router reports `FAILED`, releases buffered
live batches, and returns to `LIVE`.
- Generic history continuity is currently defined for bars only. Tick history,
provider-aware retries, and a universal overlap-deduplication policy remain
separate contracts.
live batches, and returns to `LIVE` after the retry budget is exhausted.
- Generic history continuity is currently defined for bars only. Tick history
remains a separate provider contract. The strict bar policy is a route-local
timestamp filter, not a universal semantic deduplication rule for all
providers.

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

Expand Down
48 changes: 32 additions & 16 deletions guides/market-data-router.md
Original file line number Diff line number Diff line change
Expand Up @@ -319,13 +319,26 @@ The options have these meanings:
- `LIVE_ONLY` leaves live delivery unchanged. `PREFILL` requests
`prefill_bars` before the first live delivery. `PREFILL_AND_RECOVER` does
both and also repairs timestamp gaps.
- `prefill_bars` is the count-based initial history depth. Router builds an
inclusive timeframe range for that many slots; a provider may still return
fewer bars when part of the range has no data. A `PREFILL` request must
specify a positive count. `PREFILL_AND_RECOVER` may use zero when the
application wants recovery only.
- `prefill_bars` is the count-based initial history depth. Router aligns the
end of the inclusive range to the start of the current timeframe bucket and
requests `prefill_bars` slots, so the range is `boundary - (N - 1) *
timeframe` through `boundary`. A provider may still return fewer bars when
part of the range has no data. A `PREFILL` request must specify a positive
count. `PREFILL_AND_RECOVER` may use zero when the application wants
recovery only.
- `max_backfill_bars` bounds one gap request. Zero means that the provider
request is not count-bounded by Router.
- `max_buffered_batches` and `max_buffered_items` bound live data retained
while a history request is in flight. Zero disables the corresponding limit.
When either limit is exceeded, Router reports continuity `FAILED`, releases
the queued and current live batches, disables continuity for that route, and
reports `LIVE`. This is an explicit loss-of-recovery fallback: live data
continues, but the route no longer promises historical ordering.
- `retry.max_attempts` is the total number of attempts, including the initial
request. `retry.initial_backoff_ms` starts a capped exponential backoff, and
`retry.max_backoff_ms` limits it. The default is one attempt, preserving the
existing `FAILED -> LIVE` behavior. Retries are advanced by periodic
`MarketDataRouter::process()` calls on the owner loop.

For initial prefill, the delivery order is:

Expand Down Expand Up @@ -369,17 +382,20 @@ stream-level `MarketDataStatusUpdate` events such as `READY` or `DISCONNECTED`.
missing interval. When a provider returns a partial but non-empty backfill,
Router keeps draining the queued live batches and can issue another bounded
request for the next remaining gap. An empty successful response for a detected
gap is treated as a failed recovery, so Router releases the queued live data
once instead of retrying the same range forever.

If a history request fails or is rejected, Router emits `FAILED`, releases any
buffered live batches, and then emits `LIVE`. The route stays usable and live
delivery continues, but the missing historical range is not reconstructed.
Applications that require a complete time series should record the failure and
apply their own retry policy. A provider may return overlapping snapshots for
an in-progress bar; this first continuity layer does not impose a universal
payload deduplication policy, so consumers should correlate bars by stream and
`time_ms` according to their finalized/incomplete-bar policy.
gap is treated as a terminal failed recovery, so Router releases the queued live
data once instead of retrying the same range forever. A successful empty
prefill is terminal as well: it carries no historical batch and proceeds to
the buffered live stream without retry.

If a history request fails or is rejected and attempts remain, Router emits
`RETRYING`, keeps buffered live batches, and waits for a later owner-loop
`process()` call. After the retry budget is exhausted, Router emits `FAILED`,
releases any buffered live batches, and then emits `LIVE`. The route stays usable
and live delivery continues, but an unrecovered historical range is reported to
the consumer. Router preserves provider bar revisions and does not apply a
generic timestamp deduplication policy; consumers such as charts or storage
should upsert by stream and `time_ms` when they need one current value per
candle while still accepting later finalized revisions.

`MarketDataContinuityService` is the lower-level helper for applications that
want to request history directly. It converts a `BarHistoryResult` into a
Expand Down
51 changes: 34 additions & 17 deletions guides/market-data-router.ru.md
Original file line number Diff line number Diff line change
Expand Up @@ -319,13 +319,27 @@ auto route = router.subscribe_bars("intrade", chart, request);
- `LIVE_ONLY` оставляет live-доставку без изменений. `PREFILL` запрашивает
`prefill_bars` перед первой live-доставкой. `PREFILL_AND_RECOVER` делает оба
действия и дополнительно восстанавливает разрывы по timestamp.
- `prefill_bars` задаёт глубину начальной истории в барах. Router строит
inclusive-диапазон на это количество timeframe slots; провайдер всё равно
может вернуть меньше баров, если часть диапазона не содержит данных. Для
`PREFILL` требуется положительное значение. `PREFILL_AND_RECOVER` может
использовать ноль, если приложению нужно только восстановление разрывов.
- `prefill_bars` задаёт глубину начальной истории в барах. Router выравнивает
конец inclusive-диапазона к началу текущего timeframe bucket и запрашивает
`prefill_bars` slots: от `boundary - (N - 1) * timeframe` до `boundary`.
Провайдер всё равно может вернуть меньше баров, если часть диапазона не
содержит данных. Для `PREFILL` требуется положительное значение.
`PREFILL_AND_RECOVER` может использовать ноль, если приложению нужно только
восстановление разрывов.
- `max_backfill_bars` ограничивает один запрос для разрыва. Ноль означает,
что Router не ограничивает provider request количеством баров.
- `max_buffered_batches` и `max_buffered_items` ограничивают live data,
удерживаемые во время history request. Ноль отключает соответствующее
ограничение. При превышении любого лимита Router публикует continuity
`FAILED`, выпускает накопленные и текущий live batch, отключает continuity
для этого route и публикует `LIVE`. Это явный fallback с потерей обещания
historical ordering: live-доставка продолжается.
- `retry.max_attempts` задаёт общее количество попыток вместе с первой.
`retry.initial_backoff_ms` включает ограниченный exponential backoff, а
`retry.max_backoff_ms` задаёт его предел. По умолчанию выполняется одна
попытка, поэтому сохраняется прежняя политика `FAILED -> LIVE`. Повторы
запускаются периодическими вызовами `MarketDataRouter::process()` в owner
loop.

Для initial prefill порядок доставки такой:

Expand Down Expand Up @@ -370,18 +384,21 @@ stream-level `MarketDataStatusUpdate`, например `READY` или `DISCONNE
отсутствующий интервал. Если provider вернул неполный, но непустой backfill,
Router продолжит разбирать очередь live batches и может выполнить следующий
ограниченный запрос для оставшегося gap. Успешный пустой ответ для найденного
gap считается failed recovery: Router один раз выпускает накопленные live data и
не зацикливает запрос того же диапазона.

Если history request завершился ошибкой или provider его отклонил, Router
публикует `FAILED`, выпускает накопленные live batches, затем публикует `LIVE`.
Route остаётся пригодным для работы и live-доставка продолжается, но
отсутствующий исторический диапазон не восстанавливается. Приложение, которому
нужен полный временной ряд, должно записать ошибку и применить собственную
политику повторной попытки. Provider может возвращать пересекающиеся snapshots
для незавершённого бара; этот первый слой continuity не вводит универсальную
политику deduplication, поэтому consumer должен сам сопоставлять бары по
stream и `time_ms` с учётом политики `FINALIZED`/незавершённых баров.
gap считается terminal failed recovery: Router один раз выпускает накопленные
live data и не зацикливает запрос того же диапазона. Успешный пустой prefill
тоже является terminal result: исторический batch не доставляется, а Router
переходит к накопленному live-потоку без retry.

Если history request завершился ошибкой или provider его отклонил, но попытки
ещё остались, Router публикует `RETRYING`, сохраняет накопленные live batches и
ждёт следующего вызова `process()` в owner loop. После исчерпания retry budget
Router публикует `FAILED`, выпускает накопленные live batches, затем публикует
`LIVE`. Route остаётся пригодным для работы, live-доставка продолжается, а
невосстановленный исторический диапазон явно сообщается consumer. Router
сохраняет revisions баров от provider и не применяет универсальную
timestamp-дедупликацию. Если графику или storage нужно одно текущее значение
на candle, consumer должен делать upsert по stream и `time_ms`, сохраняя
возможность принять более позднюю finalized revision.

`MarketDataContinuityService` остаётся низкоуровневым helper для приложений,
которые хотят запрашивать историю напрямую. Он превращает `BarHistoryResult` в
Expand Down
21 changes: 8 additions & 13 deletions guides/refactor-backlog.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,24 +5,19 @@ series. Keep it short and remove items once they are handled.

## Next PR Candidates

- Extend market-data continuity beyond the first bar-only route implementation:
define provider support for tick history, retries, and a documented
history-to-live boundary for each provider.
- Add robust gap recovery policy with provider-aware retry/backoff, sequence or
timestamp validation, and an explicit deduplication policy for overlapping
historical, backfill, and live bar snapshots.
- Add route-scoped continuity metrics and failure visibility for applications
that need to prove that a chart or strategy has a complete time series.
- Add a generic tick-history provider contract and extend continuity from bars
to ticks where a provider can supply historical tick data.
- Add a fuller CMake package/export story for consumers that do not use the
project as a direct submodule. The current `optionx_cpp::optionx_cpp`
interface target covers build-tree/submodule consumption.

## Explicitly Deferred

- Add a generic tick-history provider contract. Current continuity support is
intentionally bar-first because providers expose bar history only.
- Continue generation-safe lifecycle hardening for legacy bridge transports
when their behavior is changed; do not mix that work into market-data API
PRs.
- `TradeUpPlatform` remains a partial implementation. Do not refactor it as
part of generic cleanup PRs unless the task is specifically about TradeUp.
- `TradeUpPlatform` sources remain historical examples. The broker is no
longer available, so do not add new production work or generic cleanup for
this platform.
- Legacy bridge lifecycle hardening is complete. Preserve the existing
generation, callback, and shutdown patterns when changing those bridges, but
do not track the already completed audit as active work here.
3 changes: 3 additions & 0 deletions include/optionx_cpp/market_data/MarketDataContinuity.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ namespace optionx::market_data {
PREFILLING, ///< Historical initialization is being requested.
GAP_DETECTED, ///< A timestamp gap was found in the live stream.
BACKFILLING, ///< Historical bars are being loaded for a gap.
RETRYING, ///< A failed history request will be attempted again.
LIVE, ///< Live delivery is current, with no pending history work.
FAILED ///< History work failed; live delivery continues without it.
};
Expand All @@ -31,6 +32,8 @@ namespace optionx::market_data {
return "GAP_DETECTED";
case MarketDataContinuityStatus::BACKFILLING:
return "BACKFILLING";
case MarketDataContinuityStatus::RETRYING:
return "RETRYING";
case MarketDataContinuityStatus::LIVE:
return "LIVE";
case MarketDataContinuityStatus::FAILED:
Expand Down
47 changes: 44 additions & 3 deletions include/optionx_cpp/market_data/MarketDataContinuityOptions.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
/// \brief Defines history prefill and gap-recovery options for bar routes.

#include <cstddef>
#include <cstdint>
#include <limits>

namespace optionx::market_data {

Expand All @@ -17,22 +19,61 @@ namespace optionx::market_data {
PREFILL_AND_RECOVER ///< Prefill and repair timestamp gaps in live bars.
};

/// \struct MarketDataContinuityRetryPolicy
/// \brief Configures bounded history retry attempts and exponential backoff.
struct MarketDataContinuityRetryPolicy {
std::size_t max_attempts = 1; ///< Total attempts, including the first request.
std::uint64_t initial_backoff_ms = 0; ///< Delay before the second attempt.
std::uint64_t max_backoff_ms = 30000; ///< Backoff cap; zero means no cap.

/// \brief Returns true when retry settings can be applied safely.
[[nodiscard]] bool valid() const noexcept {
return max_attempts > 0 &&
(max_backoff_ms == 0 || max_backoff_ms >= initial_backoff_ms);
}

/// \brief Calculates the delay after a failed one-based attempt.
[[nodiscard]] std::uint64_t delay_after_attempt(
std::size_t attempt) const noexcept {
if (attempt == 0 || initial_backoff_ms == 0) return 0;

auto delay = initial_backoff_ms;
for (std::size_t index = 1; index < attempt; ++index) {
if (delay > std::numeric_limits<std::uint64_t>::max() / 2U) {
delay = std::numeric_limits<std::uint64_t>::max();
break;
}
delay *= 2U;
if (max_backoff_ms > 0 && delay >= max_backoff_ms) {
delay = max_backoff_ms;
break;
}
}
return max_backoff_ms > 0
? (delay < max_backoff_ms ? delay : max_backoff_ms)
: delay;
}
};

/// \struct MarketDataContinuityOptions
/// \brief Configures history prefill and timestamp-gap recovery for a bar route.
/// \brief Configures history prefill, recovery, retries, and buffering.
struct MarketDataContinuityOptions {
MarketDataContinuityMode mode = MarketDataContinuityMode::LIVE_ONLY;
std::size_t prefill_bars = 0; ///< Number of historical bars requested before live delivery.
std::size_t max_backfill_bars = 1000; ///< Maximum bars per detected gap; zero is unbounded.
MarketDataContinuityRetryPolicy retry;
std::size_t max_buffered_batches = 1024; ///< Maximum live batches held during history; zero is unbounded.
std::size_t max_buffered_items = 100000; ///< Maximum live items held during history; zero is unbounded.

/// \brief Returns true when the option combination is usable.
[[nodiscard]] bool valid() const noexcept {
switch (mode) {
case MarketDataContinuityMode::LIVE_ONLY:
return prefill_bars == 0;
case MarketDataContinuityMode::PREFILL:
return prefill_bars > 0;
return prefill_bars > 0 && retry.valid();
case MarketDataContinuityMode::PREFILL_AND_RECOVER:
return true;
return retry.valid();
default:
return false;
}
Expand Down
Loading
Loading