From ec5ba165effa4c3f9a080478e315de43c044e9b0 Mon Sep 17 00:00:00 2001 From: Aster Seker Date: Wed, 2 Sep 2026 19:25:17 +0300 Subject: [PATCH 1/4] feat(lifecycle): add staged module supervisor Introduce the optional ILifecycleModule contract and a non-owning LifecycleStack that processes dependencies forward and shuts them down one stage at a time in reverse order. Integrate MarketDataRouter and BaseTradingPlatform, add a routed-cleanup example and focused tests, and document the contract in synchronized English and Russian guides. --- AGENTS.md | 8 + CMakeLists.txt | 36 ++++ README.md | 2 + examples/lifecycle_stack_example.cpp | 135 +++++++++++++ guides/api-and-header-contracts.md | 22 +++ guides/build-and-test.md | 1 + guides/codebase-orientation.md | 4 +- guides/implementation-notes.md | 13 ++ guides/lifecycle-stack.md | 178 ++++++++++++++++++ guides/lifecycle-stack.ru.md | 177 +++++++++++++++++ guides/market-data-router.md | 4 + guides/market-data-router.ru.md | 4 + guides/platform-api-guide.md | 13 ++ guides/project-overview.md | 6 + include/optionx_cpp/lifecycle.hpp | 17 ++ .../lifecycle/ILifecycleModule.hpp | 30 +++ .../optionx_cpp/lifecycle/LifecycleStack.hpp | 114 +++++++++++ include/optionx_cpp/market_data.hpp | 1 + .../market_data/MarketDataRouter.hpp | 16 +- include/optionx_cpp/optionx.hpp | 1 + include/optionx_cpp/platforms.hpp | 1 + .../platforms/common/BaseTradingPlatform.hpp | 10 +- tests/lifecycle_event_safety_test.cpp | 10 + tests/lifecycle_stack_test.cpp | 132 +++++++++++++ tests/market_data_router_test.cpp | 10 + 25 files changed, 937 insertions(+), 8 deletions(-) create mode 100644 examples/lifecycle_stack_example.cpp create mode 100644 guides/lifecycle-stack.md create mode 100644 guides/lifecycle-stack.ru.md create mode 100644 include/optionx_cpp/lifecycle.hpp create mode 100644 include/optionx_cpp/lifecycle/ILifecycleModule.hpp create mode 100644 include/optionx_cpp/lifecycle/LifecycleStack.hpp create mode 100644 tests/lifecycle_stack_test.cpp diff --git a/AGENTS.md b/AGENTS.md index d3af5b70..23aa8339 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -18,6 +18,10 @@ контракт Router, provider registry, replay, ownership и bot-thread dispatch. - [Market data router guide RU](guides/market-data-router.ru.md) - русский перевод, обновляемый вместе с каноническим руководством. +- [Lifecycle stack guide](guides/lifecycle-stack.md) - канонический контракт + общего process/shutdown API и staged reverse-order shutdown. +- [Lifecycle stack guide RU](guides/lifecycle-stack.ru.md) - русский перевод, + обновляемый вместе с каноническим руководством. - [Codebase orientation](guides/codebase-orientation.md) - карта проекта, DDD-слои, зависимости, расширение и безопасные точки входа. - [Build and test](guides/build-and-test.md) - CMake options, зависимости, @@ -91,3 +95,7 @@ `guides/market-data-router.md`. Любое смысловое изменение синхронизируй с `guides/market-data-router.ru.md` в том же PR; русский перевод не является источником обратных изменений английского контракта. +- Для общего lifecycle stack каноническая версия - английская + `guides/lifecycle-stack.md`. Любое смысловое изменение синхронизируй с + `guides/lifecycle-stack.ru.md` в том же PR; русский перевод не является + источником обратных изменений английского контракта. diff --git a/CMakeLists.txt b/CMakeLists.txt index c0339143..019ceb2f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -455,6 +455,41 @@ if(OPTIONX_BUILD_EXAMPLES) ) endif() + add_executable(lifecycle_stack_example examples/lifecycle_stack_example.cpp) + + target_include_directories(lifecycle_stack_example PRIVATE + ${EXAMPLE_INCLUDE_DIRS} + ${EXAMPLE_DEPS_INCLUDE_DIRS} + ) + + target_link_directories(lifecycle_stack_example PRIVATE ${EXAMPLE_LIBRARY_DIRS}) + target_compile_definitions( + lifecycle_stack_example PRIVATE + ${EXAMPLE_DEFINES} + LOGIT_BASE_PATH="${LOGIT_BASE_PATH_FWD}" + ) + target_link_libraries(lifecycle_stack_example PRIVATE ${EXAMPLE_LIBS} optionx_cpp) + + if(OPTIONX_BUILD_DEPS) + add_dependencies(lifecycle_stack_example mdbx-static AES) + endif() + + foreach(dll ${EXAMPLE_DLL_FILES}) + add_custom_command(TARGET lifecycle_stack_example POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different + "${dll}" "$" + ) + endforeach() + + if(WIN32) + add_custom_command(TARGET lifecycle_stack_example POST_BUILD + COMMAND ${CMAKE_COMMAND} + -DOPTIONX_RUNTIME_DLL_DIR="${EXAMPLE_BUILD_LIBS_DIR}/bin" + -DOPTIONX_RUNTIME_TARGET_DIR="$" + -P "${CMAKE_CURRENT_SOURCE_DIR}/cmake/copy_runtime_dlls.cmake" + ) + endif() + add_executable(intrade_market_data_example examples/intrade_market_data_example.cpp) target_include_directories(intrade_market_data_example PRIVATE @@ -1032,6 +1067,7 @@ if(OPTIONX_BUILD_TESTS) set(OPTIONX_LIGHTWEIGHT_TESTS bridge_host_test + lifecycle_stack_test metatrader_paths_test telegram_dto_test telegram_signal_parser_test diff --git a/README.md b/README.md index 19747915..5de13390 100644 --- a/README.md +++ b/README.md @@ -5,3 +5,5 @@ OptionX is a C++ library for working with APIs of various brokers and trading pl - Market-data routing, provider selection, replay, and bot threads: [English](guides/market-data-router.md) | [Русский](guides/market-data-router.ru.md) +- Optional common module processing and staged shutdown: + [English](guides/lifecycle-stack.md) | [Русский](guides/lifecycle-stack.ru.md) diff --git a/examples/lifecycle_stack_example.cpp b/examples/lifecycle_stack_example.cpp new file mode 100644 index 00000000..cec1552c --- /dev/null +++ b/examples/lifecycle_stack_example.cpp @@ -0,0 +1,135 @@ +#include +#include + +#include +#include +#include +#include +#include +#include + +namespace { + +namespace lifecycle = optionx::lifecycle; +namespace market_data = optionx::market_data; + +class OwnerLoop final : public lifecycle::ILifecycleModule { +public: + bool post(std::function task) { + if (!task) return false; + std::lock_guard lock(m_mutex); + if (m_shutdown_requested) return false; + m_tasks.push_back(std::move(task)); + return true; + } + + void process() override { + std::vector> tasks; + { + std::lock_guard lock(m_mutex); + tasks.swap(m_tasks); + } + for (auto& task : tasks) task(); + + std::lock_guard lock(m_mutex); + if (m_shutdown_requested && m_tasks.empty()) m_stopped = true; + } + + void shutdown() noexcept override { + std::lock_guard lock(m_mutex); + m_shutdown_requested = true; + if (m_tasks.empty()) m_stopped = true; + } + + [[nodiscard]] bool is_stopped() const noexcept override { + std::lock_guard lock(m_mutex); + return m_stopped; + } + +private: + mutable std::mutex m_mutex; + std::vector> m_tasks; + bool m_shutdown_requested = false; + bool m_stopped = false; +}; + +class DeferredProvider final : public market_data::BaseMarketDataProvider { +public: + explicit DeferredProvider(OwnerLoop& owner_loop) + : m_owner_loop(owner_loop) {} + + bool subscribe_ticks( + market_data::TickSubscriptionRequest request, + subscription_callback_t callback) override { + auto subscription = market_data::MarketDataSubscriptionHandle::from_tick_request( + provider_id(), + m_next_subscription_id++, + request); + if (callback) { + callback(market_data::MarketDataSubscriptionResult::subscribed( + std::move(subscription))); + } + return true; + } + + bool unsubscribe( + market_data::MarketDataSubscriptionHandle subscription, + subscription_callback_t callback) override { + return m_owner_loop.post( + [subscription = std::move(subscription), + callback = std::move(callback)]() mutable { + if (callback) { + callback(market_data::MarketDataSubscriptionResult::unsubscribed( + std::move(subscription))); + } + }); + } + +private: + OwnerLoop& m_owner_loop; + market_data::SubscriptionId m_next_subscription_id = 1; +}; + +class QuoteSink final : public market_data::IMarketDataSubscriber {}; + +} // namespace + +int main() { + OwnerLoop owner_loop; + DeferredProvider provider(owner_loop); + market_data::MarketDataRouter router( + [&owner_loop](market_data::MarketDataRouter::owner_task_t task) { + return owner_loop.post(std::move(task)); + }); + auto subscriber = std::make_shared(); + + lifecycle::LifecycleStack application; + // Registration order declares dependencies: executor first, Router next. + if (!application.add_module(owner_loop) || + !application.add_module(router)) { + std::cerr << "Lifecycle registration failed\n"; + return 1; + } + + auto route = router.subscribe_ticks( + provider, + subscriber, + market_data::TickSubscriptionRequest("EURUSD")); + application.process(); + if (!route.active()) { + std::cerr << "Subscription did not become active\n"; + return 1; + } + + application.shutdown(); + while (!application.is_stopped()) { + application.process(); + } + if (route.valid()) { + std::cerr << "Routed subscription cleanup did not finish\n"; + return 1; + } + + std::cout << "Lifecycle stopped after routed subscription cleanup\n"; + return 0; +} diff --git a/guides/api-and-header-contracts.md b/guides/api-and-header-contracts.md index dd4ceae0..553c045c 100644 --- a/guides/api-and-header-contracts.md +++ b/guides/api-and-header-contracts.md @@ -108,6 +108,28 @@ etc.) являются implementation detail конкретной платфор метод сначала должен появиться на facade/base contract, а затем делегироваться в manager. +## Common Lifecycle Contract + +`optionx_cpp/lifecycle.hpp` exposes the optional +`lifecycle::ILifecycleModule` and `lifecycle::LifecycleStack` API. + +- A module implements `process()`, idempotent `shutdown() noexcept`, and the + terminal predicate `is_stopped()`. +- The stack stores non-owning references. Registered modules must outlive the + stack and its complete shutdown. +- Registration order is dependency order. Processing runs forward; shutdown is + staged in reverse, one module at a time. +- Dependencies remain processable until the current dependent module stops. +- Stack calls are owner-loop confined. The stack does not create threads. +- Initialization and `run()` remain explicit because existing module startup + contracts differ. +- `MarketDataRouter` and `BaseTradingPlatform` implement the common interface. + Direct lifecycle calls remain supported. + +See [lifecycle-stack.md](lifecycle-stack.md) for the complete contract and +[lifecycle-stack.ru.md](lifecycle-stack.ru.md) for the synchronized Russian +version. + ## Account Info Subscriber Contract `components::AccountInfoHub` is an optional fan-out adapter for the single diff --git a/guides/build-and-test.md b/guides/build-and-test.md index cf89559b..3943972c 100644 --- a/guides/build-and-test.md +++ b/guides/build-and-test.md @@ -207,6 +207,7 @@ targets. - `examples/event_mediator_test.cpp` - `examples/intrade_bar_api_example.cpp` +- `examples/lifecycle_stack_example.cpp` - `examples/market_data_router_example.cpp` - `examples/market_data_subscriber_base_example.cpp` - `examples/task_manager_example.cpp` diff --git a/guides/codebase-orientation.md b/guides/codebase-orientation.md index 5a173804..869bce38 100644 --- a/guides/codebase-orientation.md +++ b/guides/codebase-orientation.md @@ -24,8 +24,8 @@ ## Public Include Points Главная include-точка - `include/optionx_cpp/optionx.hpp`. Она включает -`utils.hpp`, `data.hpp`, `storages.hpp`, `components.hpp`, `platforms.hpp`, -`bridges.hpp`. +`utils.hpp`, `lifecycle.hpp`, `data.hpp`, `storages.hpp`, `components.hpp`, +`platforms.hpp`, `bridges.hpp`. Aggregate headers в корне `include/optionx_cpp` - часть публичной поверхности. Если добавляешь новый публичный DTO/component/platform, проверь соответствующий diff --git a/guides/implementation-notes.md b/guides/implementation-notes.md index e719c80f..3fa89e5d 100644 --- a/guides/implementation-notes.md +++ b/guides/implementation-notes.md @@ -50,6 +50,19 @@ Lifecycle: - Registered components хранятся как raw pointers; concrete platform должна владеть ими как fields и гарантировать lifetime. +### Optional Application Lifecycle Stack + +`lifecycle::LifecycleStack` composes top-level modules without replacing their +direct APIs. Register dependencies first. Normal processing runs forward; +shutdown is staged in reverse and does not stop a lower-level executor until +the current dependent module reports `is_stopped()`. + +The stack is non-owning and owner-loop confined. It intentionally does not call +`initialize()` or `run()` because existing platform, component, Router, and bot +startup contracts are not equivalent. Keep startup explicit and use the stack +only when common processing and shutdown are useful. The canonical contract is +in [lifecycle-stack.md](lifecycle-stack.md). + ### IntradeBar Delayed Retry Lifecycle Note Do not report a use-after-free risk for the current IntradeBar settings-switch diff --git a/guides/lifecycle-stack.md b/guides/lifecycle-stack.md new file mode 100644 index 00000000..8505102e --- /dev/null +++ b/guides/lifecycle-stack.md @@ -0,0 +1,178 @@ +# Lifecycle Stack Guide + +This is the canonical guide for the optional common lifecycle API. Keep +[`lifecycle-stack.ru.md`](lifecycle-stack.ru.md) synchronized when this contract +changes. + +## Purpose + +`LifecycleStack` lets an application drive several modules through one +`process()` call and one staged `shutdown()` request. It is useful when a +platform owner loop, `MarketDataRouter`, bots, node systems, and similar modules +start and stop together. + +The stack is optional. Every module keeps its direct lifecycle API and may be +managed without `LifecycleStack`. + +Public include: + +```cpp +#include +``` + +## Module Contract + +Modules implement `optionx::lifecycle::ILifecycleModule`: + +```cpp +class ILifecycleModule { +public: + virtual void process() = 0; + virtual void shutdown() noexcept = 0; + virtual bool is_stopped() const noexcept = 0; +}; +``` + +The contract is deliberately small: + +- `process()` advances normal work or an in-progress graceful shutdown; +- `shutdown()` is an idempotent request to stop accepting new work; +- `is_stopped()` becomes true only after module-owned work and cleanup finish. + +`shutdown()` does not have to complete asynchronous cleanup before returning. +The owner loop continues calling `process()` until the terminal state is +reached. + +`MarketDataRouter` implements this interface. Its common `is_stopped()` state is +the same as `is_shutdown_complete()`. `BaseTradingPlatform` also implements the +interface and reports its existing terminal lifecycle state. + +## Registration And Ownership + +Register dependencies first and dependents last: + +```text +platform / owner executor + -> provider-facing Router + -> bots or node systems +``` + +```cpp +lifecycle::LifecycleStack application; +application.add_module(platform); +application.add_module(router); +application.add_module(bot); +``` + +The stack stores non-owning pointers. Every registered module must outlive the +stack and its complete shutdown. Duplicate registration, self-registration, and +registration after shutdown starts are rejected. + +Registration order is a dependency declaration, not merely presentation order: + +- normal `process()` runs forward; +- `shutdown()` runs in reverse; +- only one dependent shutdown stage is active at a time; +- lower-level modules keep processing until the current dependent reports + `is_stopped()`. + +Synchronous stages can collapse into one `shutdown()` call. An asynchronous +stage pauses the reverse walk until later `process()` calls complete it. + +## Startup + +`LifecycleStack` does not call `initialize()` or `run()`. Existing objects have +different startup contracts: a platform has `run(bool)`, a component has +`initialize()`, and a bot may require application-specific configuration. Keep +that work explicit: + +```cpp +platform.configure_auth(...); +platform.run(false); +bot.start(); + +lifecycle::LifecycleStack application; +application.add_module(platform); +application.add_module(router); +application.add_module(bot); +``` + +This avoids inventing a lowest-common-denominator startup API. A separate +initialization capability can be added later if multiple real modules share the +same semantics. + +## Owner Loop + +Call `LifecycleStack::process()` and `shutdown()` from the same owner loop. The +stack itself does not create a thread and does not add synchronization around +module methods. + +For a manually driven platform, the host loop becomes: + +```cpp +while (running) { + application.process(); +} + +application.shutdown(); +while (!application.is_stopped()) { + application.process(); +} +``` + +With registration order `platform -> router -> bot`, each tick first lets the +platform execute queued provider callbacks, then lets Router consume retained +lifecycle completions, then processes the bot while it is still active. + +Do not also drive a platform manually when it already owns a worker thread. +For that mode, schedule the common supervisor on the actual owner loop or keep +using the modules' direct lifecycle APIs. + +## Staged Shutdown + +Given this registration order: + +```text +platform -> router -> bot +``` + +the shutdown sequence is: + +```text +bot.shutdown() +while bot is not stopped: + platform.process() + router.process() + bot.process() + +router.shutdown() +while router is not stopped: + platform.process() + router.process() + +platform.shutdown() +``` + +Application code sees only `application.shutdown()` and +`application.process()`. The stack keeps the platform/executor alive while +Router waits for late provider completions and physical unsubscribe results. + +`LifecycleStack` is also an `ILifecycleModule`, so stacks may be nested when a +larger application has independently composed subsystems. + +## Failures And Limits + +The stack does not invent retry, timeout, or abandon policies. For example, a +failed Router unsubscribe keeps Router and therefore the whole stack in a +non-terminal state. The application may inspect +`failed_unsubscribe_count()` and call `retry_failed_unsubscribes()` with its own +backoff while the provider remains alive. + +`process()` exceptions propagate to the caller. Module shutdown is `noexcept` +by contract. The stack does not own modules, destroy them, or call shutdown from +its destructor. + +The runnable integration example is +[`examples/lifecycle_stack_example.cpp`](../examples/lifecycle_stack_example.cpp). +It combines an owner-loop executor, a deferred market-data provider, and +`MarketDataRouter`, then drains them through the common stack. diff --git a/guides/lifecycle-stack.ru.md b/guides/lifecycle-stack.ru.md new file mode 100644 index 00000000..b176f77c --- /dev/null +++ b/guides/lifecycle-stack.ru.md @@ -0,0 +1,177 @@ +# Руководство По Lifecycle Stack + +Это русский перевод канонического руководства по необязательному общему +lifecycle API. При изменении контракта синхронизируй его с +[`lifecycle-stack.md`](lifecycle-stack.md). Русская версия не является +источником обратных смысловых правок английского документа. + +## Назначение + +`LifecycleStack` позволяет приложению управлять несколькими модулями через один +вызов `process()` и один staged-запрос `shutdown()`. Это полезно, когда platform +owner loop, `MarketDataRouter`, боты, системы нод и похожие модули запускаются и +останавливаются вместе. + +Stack необязателен. Каждый модуль сохраняет прямой lifecycle API и может +управляться без `LifecycleStack`. + +Публичный include: + +```cpp +#include +``` + +## Контракт Модуля + +Модули реализуют `optionx::lifecycle::ILifecycleModule`: + +```cpp +class ILifecycleModule { +public: + virtual void process() = 0; + virtual void shutdown() noexcept = 0; + virtual bool is_stopped() const noexcept = 0; +}; +``` + +Контракт намеренно мал: + +- `process()` продвигает обычную работу или уже начатый graceful shutdown; +- `shutdown()` является идемпотентным запросом прекратить приём новой работы; +- `is_stopped()` становится true только после завершения принадлежащей модулю + работы и cleanup. + +`shutdown()` не обязан завершать асинхронный cleanup до возврата. Owner loop +продолжает вызывать `process()` до достижения terminal state. + +`MarketDataRouter` реализует этот интерфейс. Его общий `is_stopped()` совпадает +с `is_shutdown_complete()`. `BaseTradingPlatform` также реализует интерфейс и +возвращает своё существующее terminal lifecycle state. + +## Регистрация И Владение + +Сначала регистрируй зависимости, затем зависящие от них модули: + +```text +platform / owner executor + -> provider-facing Router + -> bots или node systems +``` + +```cpp +lifecycle::LifecycleStack application; +application.add_module(platform); +application.add_module(router); +application.add_module(bot); +``` + +Stack хранит non-owning указатели. Каждый зарегистрированный модуль должен жить +дольше stack и полного завершения его shutdown. Повторная регистрация, +self-registration и регистрация после начала shutdown отклоняются. + +Порядок регистрации описывает зависимости, а не только порядок отображения: + +- обычный `process()` идёт вперёд; +- `shutdown()` идёт в обратном порядке; +- одновременно активна только одна стадия shutdown зависимого модуля; +- нижележащие модули продолжают обрабатываться, пока текущий зависимый модуль не + вернёт true из `is_stopped()`. + +Синхронные стадии могут завершиться за один вызов `shutdown()`. Асинхронная +стадия приостанавливает обратный проход до следующих вызовов `process()`. + +## Запуск + +`LifecycleStack` не вызывает `initialize()` или `run()`. У существующих объектов +разные контракты запуска: у платформы есть `run(bool)`, у component есть +`initialize()`, а боту может требоваться прикладная конфигурация. Оставляй эти +действия явными: + +```cpp +platform.configure_auth(...); +platform.run(false); +bot.start(); + +lifecycle::LifecycleStack application; +application.add_module(platform); +application.add_module(router); +application.add_module(bot); +``` + +Так не появляется искусственный startup API по наименьшему общему знаменателю. +Отдельную initialization capability можно добавить позже, если несколько +реальных модулей получат одинаковую семантику. + +## Owner Loop + +Вызывай `LifecycleStack::process()` и `shutdown()` из одного owner loop. Stack не +создаёт поток и не добавляет синхронизацию вокруг методов модулей. + +Для платформы в ручном режиме host loop выглядит так: + +```cpp +while (running) { + application.process(); +} + +application.shutdown(); +while (!application.is_stopped()) { + application.process(); +} +``` + +При порядке регистрации `platform -> router -> bot` каждый tick сначала +позволяет платформе выполнить queued provider callbacks, затем Router забирает +сохранённые lifecycle completions, после чего обрабатывается ещё активный бот. + +Не управляй платформой вручную, если она уже использует собственный worker +thread. В таком режиме запускай общий supervisor в настоящем owner loop или +продолжай использовать прямые lifecycle API модулей. + +## Staged Shutdown + +Для такого порядка регистрации: + +```text +platform -> router -> bot +``` + +остановка выполняется так: + +```text +bot.shutdown() +пока bot не stopped: + platform.process() + router.process() + bot.process() + +router.shutdown() +пока router не stopped: + platform.process() + router.process() + +platform.shutdown() +``` + +Прикладной код видит только `application.shutdown()` и +`application.process()`. Stack сохраняет platform/executor живым, пока Router +ждёт поздние provider completions и результаты физического unsubscribe. + +`LifecycleStack` сам является `ILifecycleModule`, поэтому stacks можно вкладывать, +если большое приложение состоит из независимо собранных подсистем. + +## Ошибки И Ограничения + +Stack не придумывает retry, timeout или abandon policy. Например, ошибка Router +unsubscribe оставляет Router и поэтому весь stack в non-terminal состоянии. +Приложение может проверить `failed_unsubscribe_count()` и вызвать +`retry_failed_unsubscribes()` со своим backoff, пока provider ещё жив. + +Исключения из `process()` передаются caller. Shutdown модуля имеет контракт +`noexcept`. Stack не владеет модулями, не уничтожает их и не вызывает shutdown +из своего destructor. + +Рабочий интеграционный пример находится в +[`examples/lifecycle_stack_example.cpp`](../examples/lifecycle_stack_example.cpp). +Он объединяет owner-loop executor, deferred market-data provider и +`MarketDataRouter`, а затем дренирует их через общий stack. diff --git a/guides/market-data-router.md b/guides/market-data-router.md index 834fa96a..596d7873 100644 --- a/guides/market-data-router.md +++ b/guides/market-data-router.md @@ -397,6 +397,10 @@ Applications that compose several process/shutdown modules should put this drain loop in their lifecycle supervisor rather than special-case Router in business code. +The optional [`LifecycleStack`](lifecycle-stack.md) provides that supervisor. +Register the platform/executor before Router; it processes them forward and +keeps the executor alive while stopping Router first in reverse order. + Do not defer subscriber destruction until after the dispatcher is closed. `MarketDataSubscriberBase` normally posts remaining handles as one cleanup task; if posting is no longer possible, its destructor falls back to synchronous diff --git a/guides/market-data-router.ru.md b/guides/market-data-router.ru.md index 22666993..73e1ef5c 100644 --- a/guides/market-data-router.ru.md +++ b/guides/market-data-router.ru.md @@ -396,6 +396,10 @@ provider и owner dispatcher Если приложение объединяет несколько process/shutdown modules, этот drain loop должен находиться в lifecycle supervisor, а не в Router-specific business code. +Необязательный [`LifecycleStack`](lifecycle-stack.ru.md) предоставляет такой +supervisor. Зарегистрируй platform/executor перед Router: process пойдёт вперёд, +а при обратном shutdown executor останется жив до полной остановки Router. + Не откладывай уничтожение subscriber до момента, когда dispatcher уже закрыт. Обычно `MarketDataSubscriberBase` отправляет оставшиеся handles одной cleanup задачей; если posting уже невозможен, destructor переходит к синхронному diff --git a/guides/platform-api-guide.md b/guides/platform-api-guide.md index 7c56cba9..deba70fc 100644 --- a/guides/platform-api-guide.md +++ b/guides/platform-api-guide.md @@ -119,6 +119,19 @@ Subscription rules: websocket feeds may still run for trade lifecycle needs even when there are no public subscribers. +## `lifecycle::LifecycleStack` + +Файлы: `include/optionx_cpp/lifecycle/ILifecycleModule.hpp` и +`include/optionx_cpp/lifecycle/LifecycleStack.hpp`. + +Полное руководство: [English](lifecycle-stack.md) | +[Русский](lifecycle-stack.ru.md). + +Это необязательный application-level supervisor для модулей с `process()`, +`shutdown()` и terminal predicate `is_stopped()`. Зависимости регистрируются +первыми, обычный process идёт вперёд, а shutdown выполняется по одной стадии в +обратном порядке. Startup остаётся явным. + ## `market_data::MarketDataRouter` Файл: `include/optionx_cpp/market_data/MarketDataRouter.hpp`. diff --git a/guides/project-overview.md b/guides/project-overview.md index 965b5a3b..aaa66da7 100644 --- a/guides/project-overview.md +++ b/guides/project-overview.md @@ -24,10 +24,16 @@ broker API и bridges. Основной сценарий: пользовател ## Public Include Surface +`optionx_cpp/lifecycle.hpp` exposes the optional `ILifecycleModule` and +`LifecycleStack` API for applications that compose platform, Router, bot, and +node lifecycles. See [English](lifecycle-stack.md) or +[Русский](lifecycle-stack.ru.md). + | Include | Что открывает | Когда использовать | |---|---|---| | `optionx_cpp/optionx.hpp` | Все основные subsystems | В приложениях и examples, когда нужен полный API | | `optionx_cpp/utils.hpp` | Pub-sub, tasks, crypto, strings, time, ids | Для инфраструктурного кода и новых components | +| `optionx_cpp/lifecycle.hpp` | `ILifecycleModule`, `LifecycleStack` | Для необязательной общей обработки и staged shutdown модулей | | `optionx_cpp/data.hpp` | DTO, events, enums, account/symbol/tick/bar/trading data | Для API boundary и сообщений | | `optionx_cpp/components.hpp` | `BaseComponent`, HTTP и trade execution base classes | Для нового manager/component | | `optionx_cpp/market_data.hpp` | Market-data provider role, subscription DTOs and statuses | Для live tick/bar subscriptions и history API contracts | diff --git a/include/optionx_cpp/lifecycle.hpp b/include/optionx_cpp/lifecycle.hpp new file mode 100644 index 00000000..3c1b6c05 --- /dev/null +++ b/include/optionx_cpp/lifecycle.hpp @@ -0,0 +1,17 @@ +#pragma once +#ifndef OPTIONX_HEADER_LIFECYCLE_HPP_INCLUDED +#define OPTIONX_HEADER_LIFECYCLE_HPP_INCLUDED + +/// \file lifecycle.hpp +/// \brief Includes the optional common lifecycle module API. +/// \note Headers under the lifecycle directory are intended to be included +/// through this aggregate header. + +#include +#include +#include + +#include "lifecycle/ILifecycleModule.hpp" +#include "lifecycle/LifecycleStack.hpp" + +#endif // OPTIONX_HEADER_LIFECYCLE_HPP_INCLUDED diff --git a/include/optionx_cpp/lifecycle/ILifecycleModule.hpp b/include/optionx_cpp/lifecycle/ILifecycleModule.hpp new file mode 100644 index 00000000..ad9cbd49 --- /dev/null +++ b/include/optionx_cpp/lifecycle/ILifecycleModule.hpp @@ -0,0 +1,30 @@ +#pragma once +#ifndef OPTIONX_HEADER_LIFECYCLE_ILIFECYCLE_MODULE_HPP_INCLUDED +#define OPTIONX_HEADER_LIFECYCLE_ILIFECYCLE_MODULE_HPP_INCLUDED + +/// \file ILifecycleModule.hpp +/// \brief Declares the common process and graceful-shutdown module contract. + +namespace optionx::lifecycle { + + /// \class ILifecycleModule + /// \brief Common interface for modules driven by an application owner loop. + /// \details shutdown() requests an idempotent stop. A module may need later + /// process() calls before is_stopped() becomes true. + class ILifecycleModule { + public: + virtual ~ILifecycleModule() noexcept = default; + + /// \brief Advances normal work or an in-progress graceful shutdown. + virtual void process() = 0; + + /// \brief Requests an idempotent graceful shutdown. + virtual void shutdown() noexcept = 0; + + /// \brief Returns true after all module-owned work and cleanup finished. + [[nodiscard]] virtual bool is_stopped() const noexcept = 0; + }; + +} // namespace optionx::lifecycle + +#endif // OPTIONX_HEADER_LIFECYCLE_ILIFECYCLE_MODULE_HPP_INCLUDED diff --git a/include/optionx_cpp/lifecycle/LifecycleStack.hpp b/include/optionx_cpp/lifecycle/LifecycleStack.hpp new file mode 100644 index 00000000..873093f5 --- /dev/null +++ b/include/optionx_cpp/lifecycle/LifecycleStack.hpp @@ -0,0 +1,114 @@ +#pragma once +#ifndef OPTIONX_HEADER_LIFECYCLE_LIFECYCLE_STACK_HPP_INCLUDED +#define OPTIONX_HEADER_LIFECYCLE_LIFECYCLE_STACK_HPP_INCLUDED + +/// \file LifecycleStack.hpp +/// \brief Defines optional ordered processing and staged shutdown for modules. + +namespace optionx::lifecycle { + + /// \class LifecycleStack + /// \brief Drives non-owning lifecycle modules in dependency order. + /// \details Register lower-level dependencies first and dependents last. + /// process() runs forward. shutdown() stops one module at a time in + /// reverse order, while lower-level dependencies keep processing. + /// All methods must be called from the same owner loop. + class LifecycleStack final : public ILifecycleModule { + public: + LifecycleStack() = default; + + LifecycleStack(const LifecycleStack&) = delete; + LifecycleStack& operator=(const LifecycleStack&) = delete; + LifecycleStack(LifecycleStack&&) = delete; + LifecycleStack& operator=(LifecycleStack&&) = delete; + + /// \brief Registers a non-owning module reference. + /// \param module Module that must outlive this stack and its shutdown. + /// \return False for duplicate/self registration or after shutdown starts. + bool add_module(ILifecycleModule& module) { + if (m_shutdown_requested || &module == this) return false; + if (std::find(m_modules.begin(), m_modules.end(), &module) != + m_modules.end()) { + return false; + } + m_modules.push_back(&module); + return true; + } + + /// \brief Returns the number of registered modules. + [[nodiscard]] std::size_t size() const noexcept { + return m_modules.size(); + } + + /// \brief Returns true when no modules are registered. + [[nodiscard]] bool empty() const noexcept { + return m_modules.empty(); + } + + /// \brief Returns true after staged shutdown has been requested. + [[nodiscard]] bool is_shutdown_requested() const noexcept { + return m_shutdown_requested; + } + + /// \brief Processes modules forward and advances staged shutdown. + void process() override { + if (m_stopped) return; + + const auto process_count = m_shutdown_requested + ? m_shutdown_cursor + : m_modules.size(); + for (std::size_t index = 0; index < process_count; ++index) { + auto* module = m_modules[index]; + if (module && !module->is_stopped()) { + module->process(); + } + } + + if (m_shutdown_requested) advance_shutdown(); + } + + /// \brief Starts staged reverse-order shutdown. + void shutdown() noexcept override { + if (m_stopped || m_shutdown_requested) return; + m_shutdown_requested = true; + m_shutdown_cursor = m_modules.size(); + advance_shutdown(); + } + + /// \brief Returns true after every registered module stopped. + [[nodiscard]] bool is_stopped() const noexcept override { + return m_stopped; + } + + private: + void advance_shutdown() noexcept { + while (m_shutdown_cursor != 0) { + auto* module = m_modules[m_shutdown_cursor - 1]; + if (!module || module->is_stopped()) { + --m_shutdown_cursor; + m_current_shutdown_requested = false; + continue; + } + + if (!m_current_shutdown_requested) { + m_current_shutdown_requested = true; + module->shutdown(); + } + if (!module->is_stopped()) return; + + --m_shutdown_cursor; + m_current_shutdown_requested = false; + } + m_stopped = true; + } + + std::vector m_modules; + std::size_t m_shutdown_cursor = 0; + bool m_shutdown_requested = false; + bool m_current_shutdown_requested = false; + bool m_stopped = false; + }; + +} // namespace optionx::lifecycle + +#endif // OPTIONX_HEADER_LIFECYCLE_LIFECYCLE_STACK_HPP_INCLUDED diff --git a/include/optionx_cpp/market_data.hpp b/include/optionx_cpp/market_data.hpp index 90ab96fc..60d907e2 100644 --- a/include/optionx_cpp/market_data.hpp +++ b/include/optionx_cpp/market_data.hpp @@ -20,6 +20,7 @@ #include #include +#include "lifecycle.hpp" #include "data.hpp" #include "market_data/enums.hpp" #include "market_data/MarketDataSubscription.hpp" diff --git a/include/optionx_cpp/market_data/MarketDataRouter.hpp b/include/optionx_cpp/market_data/MarketDataRouter.hpp index 85bac1d8..ee61ca33 100644 --- a/include/optionx_cpp/market_data/MarketDataRouter.hpp +++ b/include/optionx_cpp/market_data/MarketDataRouter.hpp @@ -227,7 +227,7 @@ namespace optionx::market_data { /// or fails physical unsubscription, the router retains that provider /// handle, keeps the callback binding, and rejects new routes through the /// affected provider until retry_failed_unsubscribes() succeeds. - class MarketDataRouter { + class MarketDataRouter : public lifecycle::ILifecycleModule { public: using SubscriptionHandle = MarketDataRouterSubscription; ///< Move-only route owner. using subscription_callback_t = BaseMarketDataProvider::subscription_callback_t; ///< Operation callback. @@ -254,8 +254,9 @@ namespace optionx::market_data { /// \brief Move assignment is disabled because handles refer to one router state. MarketDataRouter& operator=(MarketDataRouter&&) = delete; - /// \brief Stops routed subscriptions and releases provider callbacks. - ~MarketDataRouter(); + /// \brief Requests shutdown for any routes still owned by this instance. + /// \details Drain asynchronous provider operations before destruction. + ~MarketDataRouter() override; /// \brief Adds a non-owning provider registration with stable aliases. /// \details Registration does not bind provider callbacks. Aliases are @@ -407,16 +408,21 @@ namespace optionx::market_data { /// \details Processes provider completions retained during shutdown and /// starts or completes physical subscription cleanup. This method /// does not poll providers or transport data. - void process(); + void process() override; /// \brief Returns true after shutdown drained every provider operation. [[nodiscard]] bool is_shutdown_complete() const noexcept; + /// \brief Returns the common lifecycle terminal state. + [[nodiscard]] bool is_stopped() const noexcept override { + return is_shutdown_complete(); + } + /// \brief Starts an idempotent graceful shutdown. /// \details New routes and user delivery stop immediately. Pending provider /// operations remain owned until later process() calls finish their /// physical cleanup on the owner loop. - void shutdown() noexcept; + void shutdown() noexcept override; private: std::shared_ptr m_state; diff --git a/include/optionx_cpp/optionx.hpp b/include/optionx_cpp/optionx.hpp index d6db941e..6eef746a 100644 --- a/include/optionx_cpp/optionx.hpp +++ b/include/optionx_cpp/optionx.hpp @@ -6,6 +6,7 @@ /// \brief Includes core headers for the OptionX library. #include "utils.hpp" +#include "lifecycle.hpp" #include "data.hpp" #include "market_data.hpp" #include "storages.hpp" diff --git a/include/optionx_cpp/platforms.hpp b/include/optionx_cpp/platforms.hpp index acad1574..64c98b7a 100644 --- a/include/optionx_cpp/platforms.hpp +++ b/include/optionx_cpp/platforms.hpp @@ -18,6 +18,7 @@ #include "config.hpp" #include "utils.hpp" +#include "lifecycle.hpp" #include "data.hpp" #include "market_data.hpp" #include "storages.hpp" diff --git a/include/optionx_cpp/platforms/common/BaseTradingPlatform.hpp b/include/optionx_cpp/platforms/common/BaseTradingPlatform.hpp index a6833ab7..98b6b944 100644 --- a/include/optionx_cpp/platforms/common/BaseTradingPlatform.hpp +++ b/include/optionx_cpp/platforms/common/BaseTradingPlatform.hpp @@ -9,7 +9,10 @@ namespace optionx::platforms { /// \class BaseTradingPlatform /// \brief Base endpoint facade for trading platforms, account data, lifecycle, and connection state. - class BaseTradingPlatform : public BaseEndpoint, public BaseTradingApi { + class BaseTradingPlatform + : public BaseEndpoint, + public BaseTradingApi, + public lifecycle::ILifecycleModule { public: BaseTradingPlatform(std::shared_ptr account_info) : m_account_info(std::move(account_info)), @@ -241,6 +244,11 @@ namespace optionx::platforms { m_stopped.store(true, std::memory_order_release); }; + /// \brief Returns true after the platform lifecycle has stopped. + [[nodiscard]] bool is_stopped() const noexcept override { + return m_stopped.load(std::memory_order_acquire); + } + /// \brief Returns a reference to the event bus. utils::EventBus& event_bus() { return m_event_bus; } diff --git a/tests/lifecycle_event_safety_test.cpp b/tests/lifecycle_event_safety_test.cpp index d0943eb6..472e49f5 100644 --- a/tests/lifecycle_event_safety_test.cpp +++ b/tests/lifecycle_event_safety_test.cpp @@ -62,6 +62,16 @@ class TestPlatform final : public optionx::platforms::BaseTradingPlatform { } // namespace +TEST(BaseTradingPlatformLifecycle, ImplementsCommonLifecycleModule) { + TestPlatform platform; + optionx::lifecycle::ILifecycleModule& module = platform; + + EXPECT_FALSE(module.is_stopped()); + module.process(); + module.shutdown(); + EXPECT_TRUE(module.is_stopped()); +} + TEST(BaseTradingPlatformLifecycle, RepeatedRunDoesNotDuplicateLifecycleTasks) { TestPlatform platform; diff --git a/tests/lifecycle_stack_test.cpp b/tests/lifecycle_stack_test.cpp new file mode 100644 index 00000000..a424f201 --- /dev/null +++ b/tests/lifecycle_stack_test.cpp @@ -0,0 +1,132 @@ +#include + +#include + +#include +#include +#include + +namespace { + +using optionx::lifecycle::ILifecycleModule; +using optionx::lifecycle::LifecycleStack; + +class RecordingModule final : public ILifecycleModule { +public: + RecordingModule( + std::string name, + std::size_t shutdown_process_count, + std::vector& events) + : m_name(std::move(name)), + m_shutdown_process_count(shutdown_process_count), + m_events(events) {} + + void process() override { + m_events.push_back("process:" + m_name); + if (!m_shutdown_requested || m_shutdown_process_count == 0) return; + --m_shutdown_process_count; + if (m_shutdown_process_count == 0) m_stopped = true; + } + + void shutdown() noexcept override { + m_events.push_back("shutdown:" + m_name); + m_shutdown_requested = true; + if (m_shutdown_process_count == 0) m_stopped = true; + } + + [[nodiscard]] bool is_stopped() const noexcept override { + return m_stopped; + } + +private: + std::string m_name; + std::size_t m_shutdown_process_count = 0; + std::vector& m_events; + bool m_shutdown_requested = false; + bool m_stopped = false; +}; + +TEST(LifecycleStack, ProcessesForwardAndShutsDownDependentsOneAtATime) { + std::vector events; + RecordingModule platform("platform", 0, events); + RecordingModule router("router", 1, events); + RecordingModule bot("bot", 1, events); + LifecycleStack stack; + + ASSERT_TRUE(stack.add_module(platform)); + ASSERT_TRUE(stack.add_module(router)); + ASSERT_TRUE(stack.add_module(bot)); + EXPECT_FALSE(stack.add_module(router)); + EXPECT_FALSE(stack.add_module(stack)); + + stack.process(); + EXPECT_EQ(events, (std::vector{ + "process:platform", + "process:router", + "process:bot"})); + events.clear(); + + stack.shutdown(); + EXPECT_TRUE(stack.is_shutdown_requested()); + EXPECT_FALSE(stack.is_stopped()); + EXPECT_EQ(events, (std::vector{"shutdown:bot"})); + events.clear(); + + stack.process(); + EXPECT_FALSE(stack.is_stopped()); + EXPECT_EQ(events, (std::vector{ + "process:platform", + "process:router", + "process:bot", + "shutdown:router"})); + events.clear(); + + stack.process(); + EXPECT_TRUE(stack.is_stopped()); + EXPECT_EQ(events, (std::vector{ + "process:platform", + "process:router", + "shutdown:platform"})); + + events.clear(); + stack.process(); + stack.shutdown(); + EXPECT_TRUE(events.empty()); + EXPECT_FALSE(stack.add_module(bot)); +} + +TEST(LifecycleStack, StopsSynchronousModulesInReverseOrder) { + std::vector events; + RecordingModule first("first", 0, events); + RecordingModule second("second", 0, events); + LifecycleStack stack; + + ASSERT_TRUE(stack.add_module(first)); + ASSERT_TRUE(stack.add_module(second)); + stack.shutdown(); + + EXPECT_TRUE(stack.is_stopped()); + EXPECT_EQ(events, (std::vector{ + "shutdown:second", + "shutdown:first"})); +} + +TEST(LifecycleStack, EmptyStackStopsOnRequest) { + LifecycleStack stack; + + EXPECT_TRUE(stack.empty()); + EXPECT_EQ(stack.size(), 0u); + EXPECT_FALSE(stack.is_stopped()); + + stack.shutdown(); + + EXPECT_TRUE(stack.is_shutdown_requested()); + EXPECT_TRUE(stack.is_stopped()); +} + +} // namespace + +int main(int argc, char** argv) { + testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/tests/market_data_router_test.cpp b/tests/market_data_router_test.cpp index 36498c60..86bab1a9 100644 --- a/tests/market_data_router_test.cpp +++ b/tests/market_data_router_test.cpp @@ -274,6 +274,16 @@ MarketDataStatusUpdate ready_status( } // namespace +TEST(MarketDataRouter, ImplementsCommonLifecycleModule) { + MarketDataRouter router; + optionx::lifecycle::ILifecycleModule& module = router; + + EXPECT_FALSE(module.is_stopped()); + module.process(); + module.shutdown(); + EXPECT_TRUE(module.is_stopped()); +} + TEST(MarketDataRouter, UsesStrongRoutedSubscriptionIds) { const RoutedSubscriptionId empty; EXPECT_FALSE(empty.valid()); From a171d41d42358b450c25d2eeb36afa4fdd0dcc72 Mon Sep 17 00:00:00 2001 From: Aster Seker Date: Wed, 2 Sep 2026 22:29:35 +0300 Subject: [PATCH 2/4] ci(lifecycle): cover lifecycle stack smoke targets Run the lifecycle stack test and example on Ubuntu and Windows smoke jobs. Document that nested lifecycle stacks must remain acyclic while keeping cycle detection outside the optional stack implementation. --- .github/workflows/ubuntu-smoke.yml | 8 ++++++++ .github/workflows/windows-smoke.yml | 12 ++++++++++++ guides/lifecycle-stack.md | 5 +++++ guides/lifecycle-stack.ru.md | 5 +++++ 4 files changed, 30 insertions(+) diff --git a/.github/workflows/ubuntu-smoke.yml b/.github/workflows/ubuntu-smoke.yml index 2636ed71..da0c279e 100644 --- a/.github/workflows/ubuntu-smoke.yml +++ b/.github/workflows/ubuntu-smoke.yml @@ -32,6 +32,14 @@ jobs: - name: Configure run: cmake -S . -B build-linux -DOPTIONX_BUILD_DEPS=ON -DOPTIONX_BUILD_TESTS=ON -DOPTIONX_BUILD_EXAMPLES=ON + - name: Build lifecycle stack test and example + run: cmake --build build-linux --target lifecycle_stack_test lifecycle_stack_example -j + + - name: Run lifecycle stack test and example + run: | + ./build-linux/lifecycle_stack_test --gtest_brief=1 + ./build-linux/lifecycle_stack_example + - name: Build trade_record_db_test run: cmake --build build-linux --target trade_record_db_test -j diff --git a/.github/workflows/windows-smoke.yml b/.github/workflows/windows-smoke.yml index 12134a56..850ddba5 100644 --- a/.github/workflows/windows-smoke.yml +++ b/.github/workflows/windows-smoke.yml @@ -32,6 +32,18 @@ jobs: -DOPTIONX_BUILD_EXAMPLES=ON -DOPTIONX_LIGHTWEIGHT_BRIDGE_SMOKE_TESTS=ON + - name: Build lifecycle stack test and example + run: > + cmake --build build-windows --config Debug --target + lifecycle_stack_test lifecycle_stack_example + + - name: Run lifecycle stack test and example + shell: pwsh + run: | + $env:PATH = "$PWD\build-windows\bin;$PWD\build-windows\Debug;$env:PATH" + .\build-windows\Debug\lifecycle_stack_test.exe --gtest_brief=1 + .\build-windows\Debug\lifecycle_stack_example.exe + - name: Build MetaTrader path discovery test run: cmake --build build-windows --config Debug --target metatrader_paths_test diff --git a/guides/lifecycle-stack.md b/guides/lifecycle-stack.md index 8505102e..5868c873 100644 --- a/guides/lifecycle-stack.md +++ b/guides/lifecycle-stack.md @@ -160,6 +160,11 @@ Router waits for late provider completions and physical unsubscribe results. `LifecycleStack` is also an `ILifecycleModule`, so stacks may be nested when a larger application has independently composed subsystems. +Nested lifecycle stacks must form an acyclic dependency graph. `add_module()` +rejects self-registration and duplicate references, but it does not detect an +indirect cycle such as `stack_a -> stack_b -> stack_a`; the application must +avoid such registrations. + ## Failures And Limits The stack does not invent retry, timeout, or abandon policies. For example, a diff --git a/guides/lifecycle-stack.ru.md b/guides/lifecycle-stack.ru.md index b176f77c..675968d9 100644 --- a/guides/lifecycle-stack.ru.md +++ b/guides/lifecycle-stack.ru.md @@ -160,6 +160,11 @@ platform.shutdown() `LifecycleStack` сам является `ILifecycleModule`, поэтому stacks можно вкладывать, если большое приложение состоит из независимо собранных подсистем. +Вложенные lifecycle stacks должны образовывать ацикличный граф зависимостей. +`add_module()` отклоняет саморегистрацию и повторную регистрацию одной ссылки, +но не обнаруживает косвенный цикл вроде `stack_a -> stack_b -> stack_a`; +приложение должно не допускать такие регистрации. + ## Ошибки И Ограничения Stack не придумывает retry, timeout или abandon policy. Например, ошибка Router From 413f704c65ed79315396f376a3ac81677858d41b Mon Sep 17 00:00:00 2001 From: LimiNode Date: Wed, 2 Sep 2026 23:42:02 +0300 Subject: [PATCH 3/4] fix(lifecycle): narrow market-data aggregate dependencies --- include/optionx_cpp/market_data.hpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/include/optionx_cpp/market_data.hpp b/include/optionx_cpp/market_data.hpp index 60d907e2..2b5a3652 100644 --- a/include/optionx_cpp/market_data.hpp +++ b/include/optionx_cpp/market_data.hpp @@ -21,7 +21,9 @@ #include #include "lifecycle.hpp" -#include "data.hpp" +#include "data/market.hpp" +#include "data/bars.hpp" +#include "data/ticks.hpp" #include "market_data/enums.hpp" #include "market_data/MarketDataSubscription.hpp" #include "market_data/MarketDataBatch.hpp" From ed224ecf54bc1b523d2d61264ac3ccae2b473971 Mon Sep 17 00:00:00 2001 From: LimiNode Date: Wed, 2 Sep 2026 23:48:43 +0300 Subject: [PATCH 4/4] fix(lifecycle): include market-data numeric utility --- include/optionx_cpp/market_data.hpp | 1 + 1 file changed, 1 insertion(+) diff --git a/include/optionx_cpp/market_data.hpp b/include/optionx_cpp/market_data.hpp index 2b5a3652..8c935bf5 100644 --- a/include/optionx_cpp/market_data.hpp +++ b/include/optionx_cpp/market_data.hpp @@ -21,6 +21,7 @@ #include #include "lifecycle.hpp" +#include "utils/fixed_point.hpp" #include "data/market.hpp" #include "data/bars.hpp" #include "data/ticks.hpp"