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 AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,16 @@ supported public entry point or an internal leaf:
manager или provider.
- В async/HTTP/WebSocket коде сохраняй lifecycle: `run()` -> periodic
`process()` -> `shutdown()`/drain/cancel.
- Platform-owned HTTP requests and `RequestManager` state are owner-loop
confined: create/configure requests from the platform `TaskManager` loop.
External threads must use `BaseTradingPlatform::post_task()` or an
equivalent enqueue API; do not add direct cross-thread `RequestManager` or
`HttpClient` calls without an explicit synchronization and lifecycle contract.
- CPU-heavy work such as site-login proof-of-work must have both a bounded
attempt budget and a `std::chrono::steady_clock` deadline. Admit at most the
explicitly supported number of concurrent operations, reject excess work
before queueing, and release admission state on success, failure, cancel,
timeout, scheduler rejection, and shutdown.
- В recursive async/HTTP callbacks проверяй ownership graph. `lambda` или
`std::function` не должна через `std::shared_ptr` владеть объектом, который
содержит эту же callback-функцию: это создаёт постоянный reference cycle.
Expand Down
35 changes: 35 additions & 0 deletions guides/implementation-notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,41 @@ overloads. `run()` запускает worker thread, `process()` выполня
- Rate limit ids хранятся в `m_rate_limits`; используй enum class
`RateLimitType` внутри конкретной платформы.

## Platform Owner-Loop HTTP Contract

Platform-owned HTTP state is confined to the platform owner loop.

- `BaseTradingPlatform::run()` schedules the periodic owner-loop task.
- The owner loop processes the `EventBus`, HTTP futures, and manager callbacks
in one serialized `TaskManager` execution context.
- `RequestManager` and `HttpClient` configuration, request creation, and
follow-up requests must run on that owner loop.
- Concrete platform facade methods that initiate broker HTTP work must enqueue
before delegating to a manager. IntradeBar trade-result, trade-history, and
bar-history facade methods follow this rule; local read-only snapshots may
remain synchronous when their storage is independently synchronized.
- A `true` result from a facade enqueue means that the owner-loop task was
accepted, not that the broker request has completed. Once accepted, a typed
operation callback must complete exactly once on success, manager rejection,
validation failure, or pre-execution platform-shutdown cancellation. Use the
cancellation-aware `BaseTradingPlatform::post_task()` overload for this
contract; do not silently drop a callback when a queued task is drained.
- External threads must enqueue work with `BaseTradingPlatform::post_task()` or
use a facade API that performs the enqueue. They must not call
`RequestManager` or mutate `HttpClient` configuration directly.
- HTTP callbacks may create follow-up requests because they are delivered by
the owner loop. CPU-heavy helper work must use a dedicated bounded worker and
return its result through `post_task()`. Such work needs both a bounded
attempt count and a monotonic deadline; admission must be explicitly bounded
(currently one site-login PoW CPU calculation), with the slot released when
the calculation ends and on every
terminal path including cancellation, timeout, scheduler rejection, and
shutdown. A queued owner-loop continuation does not hold the CPU admission
slot.
- Shutdown stops the platform loop before component destruction. Dedicated
workers must be cancelable and joined, and their completion callbacks must
check shutdown and operation-generation state before touching platform data.

## Broker HTTP Adapter Pattern

Platform `RequestManager` classes should keep broker-specific HTTP and parser
Expand Down
167 changes: 144 additions & 23 deletions include/optionx_cpp/platforms/IntradeBarPlatform.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -102,63 +102,184 @@ namespace optionx::platforms {
/// \param query Broker-side trade identity and retry settings.
/// \param result Partially filled result object to update with broker data.
/// \param callback Callback receiving the updated result.
/// \details The request is queued onto the platform owner loop before
/// the trade manager touches account or HTTP state.
/// \return True if the check was accepted for processing; false otherwise.
bool fetch_trade_result(
TradeResultQuery query,
std::unique_ptr<TradeResult> result,
trade_result_check_callback_t callback) override {
return m_trade_manager.fetch_trade_result(
std::move(query),
std::move(result),
std::move(callback));
if (!result || !callback) return false;

auto result_holder =
std::make_shared<std::unique_ptr<TradeResult>>(std::move(result));
auto fallback_result = result_holder->get()->clone_shared();
auto completed = std::make_shared<std::atomic<bool>>(false);
auto callback_holder =
std::make_shared<trade_result_check_callback_t>(std::move(callback));
auto complete = std::make_shared<
std::function<void(std::unique_ptr<TradeResult>)>>();
*complete = [completed, callback_holder, fallback_result](
std::unique_ptr<TradeResult> completed_result) mutable {
if (completed->exchange(true, std::memory_order_acq_rel)) return;
if (!completed_result) {
completed_result = fallback_result->clone_unique();
completed_result->trade_state = completed_result->live_state = TradeState::CHECK_ERROR;
completed_result->error_code = TradeErrorCode::PARSING_ERROR;
completed_result->error_desc =
"Intrade Bar trade result callback returned no result.";
}
auto terminal_callback = std::move(*callback_holder);
if (terminal_callback) {
terminal_callback(std::move(completed_result));
}
};
auto fail = [complete, fallback_result](
TradeErrorCode error_code,
std::string reason) mutable {
auto failed_result = fallback_result->clone_unique();
failed_result->trade_state = failed_result->live_state = TradeState::CHECK_ERROR;
failed_result->error_code = error_code;
failed_result->error_desc = std::move(reason);
(*complete)(std::move(failed_result));
};

return post_task(
[this,
query = std::move(query),
result_holder,
complete,
fail]() mutable {
auto owned_result = std::move(*result_holder);
if (!owned_result) {
fail(
TradeErrorCode::INVALID_REQUEST,
"Intrade Bar trade result request lost its result object.");
return;
}
const bool accepted = m_trade_manager.fetch_trade_result(
std::move(query),
std::move(owned_result),
[complete](std::unique_ptr<TradeResult> fetched_result) mutable {
(*complete)(std::move(fetched_result));
});
if (!accepted) {
fail(
TradeErrorCode::INVALID_REQUEST,
"Intrade Bar trade result request was rejected by TradeManager.");
}
},
[fail]() mutable {
fail(
TradeErrorCode::CLIENT_FORCED_CLOSE,
"Intrade Bar trade result request was cancelled during platform shutdown.");
});
}

/// \brief Requests closed Intrade Bar trade history.
/// \param request Trade history range and timestamp field.
/// \param callback Callback receiving closed trade history or an error.
/// \details The request is queued onto the platform owner loop.
/// \return True if the history request was accepted for processing; false otherwise.
bool fetch_trade_history(
const TradeHistoryRequest& request,
trade_history_callback_t callback) override {
return m_trade_manager.fetch_trade_history(request, std::move(callback));
if (!callback || !request.has_valid_range()) return false;

auto completed = std::make_shared<std::atomic<bool>>(false);
auto callback_holder =
std::make_shared<trade_history_callback_t>(std::move(callback));
auto complete = std::make_shared<
std::function<void(TradeHistoryResult)>>();
*complete = [completed, callback_holder](
TradeHistoryResult result) mutable {
if (completed->exchange(true, std::memory_order_acq_rel)) return;
auto terminal_callback = std::move(*callback_holder);
if (terminal_callback) terminal_callback(std::move(result));
};
auto fail = [complete](std::string reason) mutable {
(*complete)(TradeHistoryResult::fail(std::move(reason)));
};

return post_task(
[this, request, complete, fail]() mutable {
const bool accepted = m_trade_manager.fetch_trade_history(
request,
[complete](TradeHistoryResult result) mutable {
(*complete)(std::move(result));
});
if (!accepted) {
fail("Intrade Bar trade history request was rejected by TradeManager.");
}
},
[fail]() mutable {
fail("Intrade Bar trade history request was cancelled during platform shutdown.");
});
}

/// \brief Requests all available closed Intrade Bar trade history.
/// \param callback Callback receiving closed trade history or an error.
/// \return True if the history request was accepted for processing; false otherwise.
bool fetch_trade_history(trade_history_callback_t callback) override {
return m_trade_manager.fetch_trade_history(
return fetch_trade_history(
TradeHistoryRequest::all(),
std::move(callback));
}

/// \brief Requests historical Intrade Bar candle data.
/// \param request Symbol, timeframe, range, and preferred price source.
/// \param callback Callback receiving parsed bars or a failure reason.
/// \details The request is queued onto the platform owner loop before
/// the broker HTTP client is configured or invoked.
/// \return True if the history request was accepted for processing; false otherwise.
bool fetch_bar_history(
const BarHistoryRequest& request,
market_data::BaseMarketDataProvider::bar_history_callback_t callback) override {
if (!callback) return false;
m_request_manager.request_bar_history_result(
request,
[callback = std::move(callback), request](intrade_bar::BarHistoryApiResult result) {
if (result) {
const auto range_complete =
intrade_bar::bar_sequence_covers_dense_range(
result.value,
request);
callback(BarHistoryResult::ok(
std::move(result.value),
result.status_code,
range_complete));
return;

auto completed = std::make_shared<std::atomic<bool>>(false);
auto callback_holder =
std::make_shared<market_data::BaseMarketDataProvider::bar_history_callback_t>(
std::move(callback));
auto complete = std::make_shared<
std::function<void(BarHistoryResult)>>();
*complete = [completed, callback_holder](
BarHistoryResult result) mutable {
if (completed->exchange(true, std::memory_order_acq_rel)) return;
auto terminal_callback = std::move(*callback_holder);
if (terminal_callback) terminal_callback(std::move(result));
};
auto fail = [complete](std::string reason) mutable {
(*complete)(BarHistoryResult::fail(std::move(reason)));
};

return post_task(
[this, request, complete, fail]() mutable {
const bool accepted = m_request_manager.request_bar_history_result(
request,
[complete, request](intrade_bar::BarHistoryApiResult result) mutable {
if (result) {
const auto range_complete =
intrade_bar::bar_sequence_covers_dense_range(
result.value,
request);
(*complete)(BarHistoryResult::ok(
std::move(result.value),
result.status_code,
range_complete));
return;
}
(*complete)(BarHistoryResult::fail(
std::move(result.error_message),
result.status_code));
});
if (!accepted) {
fail("Intrade Bar bar-history request was rejected by RequestManager.");
}
callback(BarHistoryResult::fail(
std::move(result.error_message),
result.status_code));
},
[fail]() mutable {
fail("Intrade Bar bar-history request was cancelled during platform shutdown.");
});
return true;
}

/// \brief Returns observed Intrade polling ticks from the in-memory archive.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,40 @@ namespace optionx::platforms::intrade_bar {
bool available = false;
};

/// \brief Hidden login challenge and cookies parsed from the main page.
/// \brief Login protocol detected from the broker page contents.
enum class LoginPageFlow {
LEGACY_FORM, ///< Retired hidden-input login form.
MODERN_NUXT, ///< Nuxt landing page with the current SPA login API.
SITE_FORM ///< Server-rendered site form with challenge proof-of-work.
};

/// \brief Challenge and cookies prepared for the detected login protocol.
struct MainPageChallenge {
LoginPageFlow flow = LoginPageFlow::LEGACY_FORM;
std::string origin;
std::string req_id;
std::string req_value;
std::string cookies;
std::string site_api_path;
std::string site_csrf;
std::string site_pow;
std::string site_nonce;
std::uint32_t site_pow_bits = 0;
};

/// \brief Converts a login protocol to a stable diagnostic string.
inline const char* login_page_flow_to_string(LoginPageFlow flow) noexcept {
switch (flow) {
case LoginPageFlow::LEGACY_FORM:
return "legacy_form";
case LoginPageFlow::MODERN_NUXT:
return "modern_nuxt";
case LoginPageFlow::SITE_FORM:
return "site_form";
}
return "unknown";
}

/// \brief User credentials returned by the login page flow.
struct LoginCredentials {
std::string user_id;
Expand Down
17 changes: 11 additions & 6 deletions include/optionx_cpp/platforms/IntradeBarPlatform/AuthManager.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,7 @@ namespace optionx::platforms::intrade_bar {
connection_callback_t callback) {
cancel_pending_connect_callback(reason);
const std::uint64_t generation = ++m_auth_generation;
m_request_manager.invalidate_auth_operations();
if (callback) {
m_pending_connect_generation = generation;
m_pending_connect_callback = std::move(callback);
Expand All @@ -344,6 +345,7 @@ namespace optionx::platforms::intrade_bar {

inline void AuthManager::invalidate_auth_generation(const char* reason) {
const std::uint64_t generation = ++m_auth_generation;
m_request_manager.invalidate_auth_operations();
cancel_pending_connect_callback(reason);
LOGIT_DEBUG(
"Intrade Bar auth: generation invalidated. reason=",
Expand Down Expand Up @@ -914,7 +916,9 @@ namespace optionx::platforms::intrade_bar {
return;
}
if (!success) {
LOGIT_WARN("Intrade Bar auth: auth endpoint rejected fresh credentials.");
LOGIT_WARN(
"Intrade Bar auth: authentication flow failed. reason=",
reason);
result_callback(false, reason);
return;
}
Expand Down Expand Up @@ -965,9 +969,7 @@ namespace optionx::platforms::intrade_bar {
// Step 1: Handle main page response
auto handle_main_page_response = [this, handle_login_response, generation](
bool success,
const std::string& req_id,
const std::string& req_value,
const std::string& cookies,
const MainPageChallenge& challenge,
const std::string& reason) {
if (!is_auth_generation_current(generation, "main-page-challenge")) {
return;
Expand All @@ -979,8 +981,11 @@ namespace optionx::platforms::intrade_bar {
}

// Proceed to login
LOGIT_INFO("Intrade Bar auth: main page challenge succeeded, sending login.");
m_request_manager.request_login(req_id, req_value, cookies, m_auth_data,
LOGIT_INFO(
"Intrade Bar auth: login page recognized as ",
login_page_flow_to_string(challenge.flow),
", sending login.");
m_request_manager.request_login(challenge, m_auth_data,
[handle_login_response](
bool success,
const std::string& user_id,
Expand Down
Loading
Loading