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
8 changes: 8 additions & 0 deletions .github/workflows/ubuntu-smoke.yml
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,14 @@ jobs:
./build-linux/market_data_router_test --gtest_brief=1
./build-linux/market_data_router_example

- name: Build market_data_continuity_test and example
run: cmake --build build-linux --target market_data_continuity_test market_data_continuity_example -j

- name: Run market_data_continuity_test and example
run: |
./build-linux/market_data_continuity_test --gtest_brief=1
./build-linux/market_data_continuity_example

- 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 @@ -93,6 +93,11 @@ jobs:
- name: Build Telegram live bridge smoke example
run: cmake --build build-windows --config Debug --target telegram_live_bridge_smoke

- name: Build market data continuity test and example
run: >
cmake --build build-windows --config Debug --target
market_data_continuity_test market_data_continuity_example

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

Expand All @@ -113,6 +118,8 @@ jobs:
.\build-windows\Debug\telegram_signal_bridge_test.exe --gtest_brief=1
.\build-windows\Debug\telegram_worker_source_test.exe --gtest_brief=1
.\build-windows\Debug\trading_view_bridge_test.exe --gtest_brief=1
.\build-windows\Debug\market_data_continuity_test.exe --gtest_brief=1
.\build-windows\Debug\market_data_continuity_example.exe
.\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
35 changes: 35 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -455,6 +455,41 @@ if(OPTIONX_BUILD_EXAMPLES)
)
endif()

add_executable(market_data_continuity_example examples/market_data_continuity_example.cpp)

target_include_directories(market_data_continuity_example PRIVATE
${EXAMPLE_INCLUDE_DIRS}
${EXAMPLE_DEPS_INCLUDE_DIRS}
)

target_link_directories(market_data_continuity_example PRIVATE ${EXAMPLE_LIBRARY_DIRS})
target_compile_definitions(
market_data_continuity_example PRIVATE
${EXAMPLE_DEFINES}
LOGIT_BASE_PATH="${LOGIT_BASE_PATH_FWD}"
)
target_link_libraries(market_data_continuity_example PRIVATE ${EXAMPLE_LIBS} optionx_cpp)

if(OPTIONX_BUILD_DEPS)
add_dependencies(market_data_continuity_example mdbx-static AES)
endif()

foreach(dll ${EXAMPLE_DLL_FILES})
add_custom_command(TARGET market_data_continuity_example POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different
"${dll}" "$<TARGET_FILE_DIR:market_data_continuity_example>"
)
endforeach()

if(WIN32)
add_custom_command(TARGET market_data_continuity_example POST_BUILD
COMMAND ${CMAKE_COMMAND}
-DOPTIONX_RUNTIME_DLL_DIR="${EXAMPLE_BUILD_LIBS_DIR}/bin"
-DOPTIONX_RUNTIME_TARGET_DIR="$<TARGET_FILE_DIR:market_data_continuity_example>"
-P "${CMAKE_CURRENT_SOURCE_DIR}/cmake/copy_runtime_dlls.cmake"
)
endif()

add_executable(lifecycle_stack_example examples/lifecycle_stack_example.cpp)

target_include_directories(lifecycle_stack_example PRIVATE
Expand Down
3 changes: 3 additions & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ Currently maintained examples:
- `market_data_subscriber_base_example.cpp` demonstrates a bot posting subscribe
and unsubscribe commands from its own thread by stable provider ID and alias,
while provider calls, delivery, and handle cleanup stay in one owner loop.
- `market_data_continuity_example.cpp` demonstrates bar history prefill,
history-before-live ordering, timestamp-gap recovery, and route-scoped
continuity status events with a deterministic local provider.
- `trading_condition_hub_example.cpp` demonstrates routing payout, session and
expiration-limit changes through `TradingConditionHub`, plus querying the
merged current condition snapshot for a concrete symbol.
Expand Down
152 changes: 152 additions & 0 deletions examples/market_data_continuity_example.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
#include <cstddef>
#include <cstdint>
#include <initializer_list>
#include <iostream>
#include <memory>
#include <string>
#include <utility>
#include <vector>

#include <optionx_cpp/market_data.hpp>

namespace md = optionx::market_data;

namespace {

class DemoBarProvider final : public md::BaseMarketDataProvider {
public:
bool subscribe_bars(
md::BarSubscriptionRequest request,
subscription_callback_t callback) override {
m_subscription = md::MarketDataSubscriptionHandle::from_bar_request(
provider_id(),
m_next_subscription_id++,
request);
if (callback) {
callback(md::MarketDataSubscriptionResult::subscribed(m_subscription));
}
return true;
}

bool unsubscribe(
md::MarketDataSubscriptionHandle subscription,
subscription_callback_t callback) override {
if (callback) {
callback(md::MarketDataSubscriptionResult::unsubscribed(
std::move(subscription)));
}
m_subscription = {};
return true;
}

bool fetch_bar_history(
const optionx::BarHistoryRequest& request,
bar_history_callback_t callback) override {
std::cout << "history request: " << request.symbol
<< " [" << request.from_ts << ", " << request.to_ts << "]\n";
m_history_callbacks.push_back(std::move(callback));
return true;
}

void emit_live_bar(std::uint64_t time_ms, double close) {
auto batch = std::make_unique<md::BarDataBatch>();
batch->subscription = m_subscription;
batch->type = md::MarketDataType::BARS;
batch->symbol = m_subscription.symbol;
batch->timeframe = m_subscription.timeframe;
batch->items.emplace_back(close - 0.1, close + 0.2, close - 0.3, close, 1.0, time_ms);
batch->items.back().set_flag(optionx::MarketDataFlags::REALTIME);
if (on_bar_data()) on_bar_data()(std::move(batch));
}

void complete_history(std::vector<optionx::Bar> bars) {
if (m_history_callbacks.empty()) return;

auto callback = std::move(m_history_callbacks.front());
m_history_callbacks.erase(m_history_callbacks.begin());

optionx::BarSequence sequence;
sequence.symbol = "EURUSD";
sequence.provider = "demo-provider";
sequence.timeframe = 60;
sequence.price_digits = 5;
sequence.volume_digits = 0;
sequence.price_source = optionx::BarPriceSource::MID;
sequence.bars = std::move(bars);
callback(optionx::BarHistoryResult::ok(std::move(sequence)));
}

private:
md::SubscriptionId m_next_subscription_id = 1;
md::MarketDataSubscriptionHandle m_subscription;
std::vector<bar_history_callback_t> m_history_callbacks;
};

class Chart final : public md::IMarketDataSubscriber {
public:
void on_bar_data(const md::BarDataBatch& batch) override {
for (const auto& bar : batch.items) {
const char* source = "live";
if (bar.has_flag(optionx::MarketDataFlags::HISTORICAL)) {
source = bar.has_flag(optionx::MarketDataFlags::BACKFILL)
? "backfill"
: "prefill";
}
std::cout << "chart bar: route provider subscription #"
<< batch.subscription.id
<< ", t=" << bar.time_ms
<< ", source=" << source << '\n';
}
}

void on_market_data_continuity(
const md::MarketDataContinuityUpdate& update) override {
std::cout << "continuity: subscription #" << update.subscription.id
<< ", status=" << md::to_str(update.status)
<< ", history items=" << update.delivered_items << '\n';
}
};

std::vector<optionx::Bar> make_bars(
std::initializer_list<std::uint64_t> timestamps) {
std::vector<optionx::Bar> bars;
for (const auto timestamp : timestamps) {
bars.emplace_back(100.0, 101.0, 99.0, 100.5, 1.0, timestamp);
}
return bars;
}

} // namespace

int main() {
DemoBarProvider provider;
md::MarketDataRouter router;
auto chart = std::make_shared<Chart>();

md::BarSubscriptionRequest request(
"EURUSD",
60,
optionx::BarPriceSource::MID,
md::MarketDataTransport::WEBSOCKET);
request.continuity.mode = md::MarketDataContinuityMode::PREFILL_AND_RECOVER;
request.continuity.prefill_bars = 2;
request.continuity.max_backfill_bars = 10;

auto route = router.subscribe_bars(provider, chart, request);
if (!route.active()) {
std::cerr << "could not create a bar route\n";
return 1;
}

// This live bar is buffered until the initial historical range is delivered.
provider.emit_live_bar(220000, 100.5);
provider.complete_history(make_bars({100000, 160000}));

// 280000 is missing, so the Router requests it before releasing 340000.
provider.emit_live_bar(340000, 101.5);
provider.complete_history(make_bars({280000}));

route.reset();
router.shutdown();
return 0;
}
16 changes: 15 additions & 1 deletion guides/api-and-header-contracts.md
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,7 @@ Market-data APIs are split into DTO/data types and a provider role:
`MarketDataSubscriptionHandle`, `MarketDataSubscriptionResult`,
`MarketDataBatch<T>`, `MarketDataHub`, `MarketDataRouter`,
`MarketDataSubscriberBase`, `IMarketDataSubscriber`, and
`MarketDataContinuityOptions`, `MarketDataContinuityUpdate`, and
`MarketDataContinuityService`.

Contract rules:
Expand Down Expand Up @@ -252,6 +253,17 @@ Contract rules:
- `MarketDataContinuityService` is the thin helper for routing recovered history
into the same bar batch pipeline. It marks payload bars as
`HISTORICAL` and, for gap recovery, `BACKFILL`.
- `BarSubscriptionRequest::continuity` enables Router-owned bar prefill and
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()`.
- 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.

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

Expand Down Expand Up @@ -359,7 +371,9 @@ Intrade Bar publishes condition snapshots for every supported symbol/option-type
scope after the account context becomes known. `TradingConditionManager` also
re-evaluates the time-dependent session, amount, open-trade and sprint-duration
limits from the same `AccountInfoData` model used to validate trade requests.
Only scopes whose values changed are emitted.
Only scopes whose values changed are emitted. During platform shutdown it emits
one final `tradable=false` patch for each cached scope before clearing the
manager, which prevents long-lived hubs from retaining stale availability.

Intrade Bar intentionally leaves `TradingConditionUpdate::payout` empty. Its
payout model depends on the concrete trade amount and duration, but those values
Expand Down
2 changes: 2 additions & 0 deletions guides/implementation-notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,8 @@ The manager emits only changed scopes. A scope contains the platform, account
type, currency, option type and normalized symbol. When account identity changes,
the previous scopes receive a final `tradable=false` patch before the new scopes
are published.
The same final unavailable patch is emitted for every cached scope during
platform shutdown before the manager clears its cache.

Do not fill `payout` from a made-up reference amount or duration. Current Intrade
payout rules are trade-parameter dependent, while `TradingConditionUpdate` does
Expand Down
Loading
Loading