Skip to content
Merged
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: 10 additions & 0 deletions .github/workflows/ubuntu-smoke.yml
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,16 @@ jobs:
- name: Run market_data_tick_history_contract_test
run: ./build-linux/market_data_tick_history_contract_test --gtest_brief=1

- name: Build market data tick continuity and observed history tests
run: >
cmake --build build-linux --target
market_data_tick_continuity_test intrade_observed_tick_history_test -j

- name: Run market data tick continuity and observed history tests
run: |
./build-linux/market_data_tick_continuity_test --gtest_brief=1
./build-linux/intrade_observed_tick_history_test --gtest_brief=1

- name: Build market_data_subscriber_base_test and example
run: cmake --build build-linux --target market_data_subscriber_base_test market_data_subscriber_base_example -j

Expand Down
7 changes: 7 additions & 0 deletions .github/workflows/windows-smoke.yml
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,11 @@ jobs:
cmake --build build-windows --config Debug --target
market_data_tick_history_contract_test

- name: Build market data tick continuity and observed history tests
run: >
cmake --build build-windows --config Debug --target
market_data_tick_continuity_test intrade_observed_tick_history_test

- name: Build TradingView extension bridge smoke example
run: cmake --build build-windows --config Debug --target tradingview_extension_bridge_smoke

Expand All @@ -126,6 +131,8 @@ jobs:
.\build-windows\Debug\market_data_continuity_test.exe --gtest_brief=1
.\build-windows\Debug\market_data_continuity_example.exe
.\build-windows\Debug\market_data_tick_history_contract_test.exe --gtest_brief=1
.\build-windows\Debug\market_data_tick_continuity_test.exe --gtest_brief=1
.\build-windows\Debug\intrade_observed_tick_history_test.exe --gtest_brief=1
.\build-windows\Debug\metatrader_file_bridge_smoke.exe --self-test
.\build-windows\Debug\metatrader_file_command_writer_smoke.exe --self-test
.\build-windows\Debug\metatrader_file_end_to_end_smoke.exe --self-test
Expand Down
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1110,6 +1110,7 @@ if(OPTIONX_BUILD_TESTS)
telegram_worker_source_test
trading_view_bridge_test
market_data_tick_history_contract_test
market_data_tick_continuity_test
intrade_observed_tick_history_test
)
if(OPTIONX_LIGHTWEIGHT_BRIDGE_SMOKE_TESTS)
Expand Down
27 changes: 20 additions & 7 deletions guides/api-and-header-contracts.md
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,11 @@ Contract rules:
assertion; successful observations with `false` must not be used as proof of
continuity. Equal timestamps are valid, and exact duplicate removal remains
provider/consumer policy.
- `BaseMarketDataProvider::provider_time_ms()` and
`BaseMarketDataProvider::tick_history_interval_ms()` are optional metadata
hooks for providers whose history backend has its own clock or sampling grid.
A zero value means that Router should use its local clock or event-oriented
range semantics.

### Intrade observed tick history

Expand Down Expand Up @@ -376,13 +381,21 @@ broker:
plain `PREFILL` remains startup-only. A cached invalidating status applies the
same transition before a newly accepted route may start prefill. Completed
`PREFILL` routes do not acquire reconnect or timestamp-gap recovery implicitly.
- Bar continuity is currently implemented by Router. The provider API also
defines a separate `fetch_tick_history()` contract with inclusive
millisecond ranges, explicit `range_complete`, and non-decreasing timestamp
order validated by the adapter. A non-empty result symbol must match the
request. No
current provider implements authoritative tick history yet; Router does not
apply tick continuity or a universal timestamp deduplication policy.
- Router continuity supports both bar and tick routes. The provider API defines
a separate `fetch_tick_history()` contract with inclusive millisecond ranges,
explicit `range_complete`, and non-decreasing timestamp order validated by the
adapter. A non-empty result symbol must match the request. Tick routes opt in
through `TickSubscriptionRequest::continuity`: `PREFILL` holds live ticks
until the requested lookback completes, while `PREFILL_AND_RECOVER` also
detects suspicious timestamp gaps and repairs them in bounded ranges.
- Tick `expected_interval_ms` is only a gap-detection hint because tick streams
are event-oriented and may contain sub-second or equal-timestamp events.
`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.

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

Expand Down
74 changes: 68 additions & 6 deletions guides/market-data-router.md
Original file line number Diff line number Diff line change
Expand Up @@ -634,10 +634,11 @@ service.request_tick_history_batch(
true); // require_complete_range
```

`MarketDataRouter` still has its mature continuity state machine on bars. The
Intrade Bar provider now also implements `fetch_tick_history()` as a bounded,
`MarketDataRouter` also applies the history contract to tick routes. The
Intrade Bar provider implements `fetch_tick_history()` as a bounded,
session-scoped archive of observed `/price_now` snapshots. This is useful for
short reconnect windows, but it is not an authoritative broker tick archive:
short prefill and reconnect windows, but it is not an authoritative broker tick
archive:

- broker timestamps have one-second granularity;
- the archive starts empty for a new authenticated session and evicts old data;
Expand All @@ -648,9 +649,70 @@ short reconnect windows, but it is not an authoritative broker tick archive:
- `trade_check2.php` remains a settlement/trade-result API and is not used for
range history.

Router tick continuity is the next layer. Until that integration is enabled,
callers can use the provider operation directly and must treat
`range_complete=false` as an observation result rather than continuity proof.
Providers may expose optional history-clock metadata through
`BaseMarketDataProvider::provider_time_ms()` and
`BaseMarketDataProvider::tick_history_interval_ms()`. The first is a provider
time estimate in milliseconds; the second describes the history backend's
sampling grid and is not the live gap-detection threshold. Intrade estimates
its provider time from recent `(Tick::time_ms - Tick::received_ms)` samples,
uses their median to reject polling jitter, and rounds the estimate down to
the one-second history grid. With no estimate, Router falls back to the local
clock. Providers without a discrete history grid leave both optional hooks at
their defaults.

### Tick continuity in Router

Set `TickSubscriptionRequest::continuity` to enable history-first delivery for
one tick route:

```cpp
md::TickSubscriptionRequest request("EURUSD");
request.continuity.mode = md::MarketDataContinuityMode::PREFILL_AND_RECOVER;
request.continuity.prefill_lookback_ms = 60'000;
request.continuity.expected_interval_ms = 1'000;
request.continuity.max_backfill_ms = 60'000;

auto route = router.subscribe_ticks(provider, bot, request);
```

`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
gap-detection hint, not a claim that every tick must arrive on a fixed grid;
equal timestamps and sub-second live ticks are valid. The provider's
`range_complete` remains the authority for whether a requested history range
proves continuity.

Recovery requests use inclusive timestamp ranges. For an event-oriented
provider, the suspicious interval is requested without inventing missing tick
slots, and the live tick that triggered recovery remains buffered. When a
provider declares a history grid, Router aligns the start down and the history
end down to the last completed provider boundary. An off-grid live observation
is not requested as a future history sample: it stays in the continuity buffer
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.

The Router sends historical ticks first, marks them `HISTORICAL`, and then
replays held live ticks as `LIVE_SOURCE | CATCHUP`. A complete result is required
before the route can report `LIVE`. An incomplete result may still be delivered
as observations, but it reports `FAILED` followed by sticky `DEGRADED`; a later
unrelated successful range cannot hide the earlier unresolved watermark.
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
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
restarts from the original lookback start after `READY` and extends the request
through the current time, so the interrupted interval is not silently skipped.

## Owner Loop And Bot Threads

Expand Down
76 changes: 68 additions & 8 deletions guides/market-data-router.ru.md
Original file line number Diff line number Diff line change
Expand Up @@ -791,11 +791,11 @@ service.request_tick_history_batch(
true); // require_complete_range
```

`MarketDataRouter` уже имеет полноценную state machine continuity для bars.
Кроме того, Intrade Bar теперь реализует `fetch_tick_history(...)` через
ограниченный архив наблюдаемых snapshots из `/price_now`. Это полезно для
короткого восстановления после reconnect, но не является authoritative
broker tick archive:
`MarketDataRouter` теперь также применяет history contract к tick routes.
Intrade Bar реализует `fetch_tick_history(...)` через ограниченный
session-scoped архив наблюдаемых snapshots из `/price_now`. Это полезно для
короткого prefill и reconnect recovery, но не является authoritative broker
tick archive:

- broker timestamps имеют гранулярность в одну секунду;
- архив пуст для новой authenticated session и вытесняет старые данные;
Expand All @@ -806,6 +806,66 @@ broker tick archive:
- `trade_check2.php` остаётся settlement/trade-result API и не используется
для range history.

Интеграция tick continuity в Router выполняется следующим слоем. До неё
вызывающий код может использовать provider operation напрямую и обязан
считать `range_complete=false` observations, а не доказательством continuity.
Провайдер может дополнительно реализовать метаданные времени через
`BaseMarketDataProvider::provider_time_ms()` и
`BaseMarketDataProvider::tick_history_interval_ms()`. Первый метод возвращает
оценку времени провайдера в миллисекундах, второй описывает сетку backend
истории и не является порогом live gap detection. Intrade получает эту оценку
по недавним samples `(Tick::time_ms - Tick::received_ms)`, берёт их медиану,
чтобы отфильтровать polling jitter, и округляет результат вниз до секундной
сетки истории. Если оценка недоступна, Router использует локальные часы.
Провайдеры без дискретной сетки оставляют оба optional hook со значениями по
умолчанию.

### Tick continuity в Router

Чтобы включить history-first delivery для одного tick route, настройте
`TickSubscriptionRequest::continuity`:

```cpp
md::TickSubscriptionRequest request("EURUSD");
request.continuity.mode = md::MarketDataContinuityMode::PREFILL_AND_RECOVER;
request.continuity.prefill_lookback_ms = 60'000;
request.continuity.expected_interval_ms = 1'000;
request.continuity.max_backfill_ms = 60'000;

auto route = router.subscribe_ticks(provider, bot, request);
```

`PREFILL` запрашивает заданный lookback до освобождения live ticks.
`PREFILL_AND_RECOVER` дополнительно удерживает live tail, когда дельта между
последовательными timestamps больше `expected_interval_ms`. Это только
gap-detection hint, а не требование плотной сетки: equal timestamps и live
ticks чаще одной секунды допустимы. Для доказательства continuity Router
доверяет только значению `range_complete` в provider result.

Recovery использует inclusive timestamp ranges. Для event-oriented provider
Router не синтезирует пропущенные tick slots, а сохраняет в buffer live tick,
который запустил recovery. Если provider объявляет history grid, Router
округляет начало вниз, а конец history - вниз до последней завершённой
provider boundary. Off-grid live observation не запрашивается как будущий
history sample: она остаётся в continuity buffer и выпускается после проверки
завершённого диапазона. Bounded chunks сохраняют лимит размера и перекрываются
в предыдущей конечной точке, когда такой overlap позволяет продвинуть диапазон.
Если лимит меньше шага provider grid, Router переходит к следующей provider
boundary, а не повторяет тот же запрос. Overlap удаляется только по exact
observation identity.

Router сначала отправляет historical ticks с флагом `HISTORICAL`, затем
воспроизводит удержанные live ticks с флагами `LIVE_SOURCE | CATCHUP`. До
`LIVE` нужен complete result. Неполный result можно доставить как observations,
но Router отправит `FAILED`, затем sticky `DEGRADED`; поздний независимый
успешный запрос не скроет раннюю unresolved boundary. `max_backfill_ms`
ограничивает каждый history request, а `process()` обслуживает retries без
создания отдельного 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, публикует
`FAILED`/`DEGRADED`, отключает continuity для этого route и возобновляет
обычную live delivery.
Если transport прервался во время initial prefill, после `READY` Router
начинает повторный запрос с исходного начала lookback и расширяет его до
текущего времени, поэтому прерванный интервал не пропускается молча.
14 changes: 9 additions & 5 deletions guides/platform-api-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,15 +148,19 @@ Subscription rules:
slots before emitting `LIVE`.
Cached invalidating status replay blocks the same work until a later live
`READY`, while a plain or completed `PREFILL` route does not gain outage
recovery. Tick routes do not have this guarantee because the provider contract
still lacks generic tick-history.
- Router continuity is currently bar-first. Intrade additionally exposes a
bounded, session-scoped observed-tick archive populated by `/price_now`.
recovery. Tick routes can use `TickSubscriptionRequest::continuity` with the
same history-first lifecycle when the provider implements
`fetch_tick_history()`.
- Router continuity supports bars and tick routes. Intrade additionally exposes
a bounded, session-scoped observed-tick archive populated by `/price_now`.
Its timestamps have one-second broker granularity, and `range_complete=true`
means every expected observed second is present in the retained archive, not
that every broker micro-event was captured. The archive is non-persistent and
starts empty for a new authenticated session. `trade_check2.php` remains a
settlement/trade-result endpoint and is not used for tick history.
settlement/trade-result endpoint and is not used for tick history. Router
tick continuity uses the archive's explicit `range_complete` assertion,
keeps incomplete ranges `DEGRADED`, and preserves distinct same-second
observations while removing only exact overlaps.
- `BaseMarketDataProvider` is non-copyable and non-movable so provider identity
cannot be duplicated after handles were issued.
- Public subscriptions describe consumer routing. Internal platform polling or
Expand Down
8 changes: 6 additions & 2 deletions guides/refactor-backlog.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,15 @@ series. Keep it short and remove items once they are handled.
`/price_now`. It preserves distinct same-second snapshots and reports
`range_complete` only for proven one-second coverage; it is not a persistent
or authoritative broker tick archive.
- Router tick continuity now uses that provider history contract for opt-in
`PREFILL` and `PREFILL_AND_RECOVER` routes. It buffers live ticks, treats
`range_complete` as the authority, retries transport failures through
`process()`, performs bounded recovery, preserves distinct same-second
events, removes only exact overlaps, and keeps incomplete history
`DEGRADED`.

## Next PR Candidates

- Integrate Intrade's observed-tick archive with Router continuity while keeping
incomplete ranges fail-closed and documenting the session/retention limit.
- Add an authoritative provider tick-history implementation only if a broker
later exposes one; do not treat `trade_check2.php` as a range-history API.
- Replace the dense-bar assumption with an explicit provider completeness
Expand Down
Loading
Loading