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
12 changes: 12 additions & 0 deletions guides/api-and-header-contracts.md
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,18 @@ Rules:
status events. Market-data subscriptions report prices; condition subscribers
report whether and how a trade can currently be opened.

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.

Intrade Bar intentionally leaves `TradingConditionUpdate::payout` empty. Its
payout model depends on the concrete trade amount and duration, but those values
are not part of the current condition scope. Publishing one payout per symbol
would therefore be ambiguous. Use `AccountInfoRequest` for the exact prospective
trade until the condition API gains an amount/duration-aware scope.

## Typed Broker Result Pattern

Broker HTTP adapters используют typed result wrappers, чтобы не смешивать
Expand Down
17 changes: 17 additions & 0 deletions guides/implementation-notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,23 @@ facade lifecycle или остаться probe/internal component.
Не меняй account info напрямую из application code. Для user-facing чтения
используй `BaseTradingPlatform::get_info<T>()`.

### Intrade Bar Trading Conditions

`platforms/IntradeBarPlatform/TradingConditionManager.hpp` converts the current
Intrade account condition model into `TradingConditionUpdateEvent` snapshots.
It reacts to account lifecycle/context/open-trade updates and re-evaluates
time-dependent values from `BaseComponent::process()` once per Unix second.

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.

Do not fill `payout` from a made-up reference amount or duration. Current Intrade
payout rules are trade-parameter dependent, while `TradingConditionUpdate` does
not identify those parameters. Exact pre-trade payout checks remain queries to
`AccountInfoData` through `AccountInfoRequest`.

## Session Storage

Опорный файл: `storages/ServiceSessionDB.hpp`.
Expand Down
7 changes: 7 additions & 0 deletions guides/platform-api-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,13 @@
Market-data callbacks (`on_tick_data`, `on_bar_data`, `on_market_data_status`)
живут на `market_data::BaseMarketDataProvider`.

### Intrade Bar Condition Updates

`IntradeBarPlatform::on_trading_condition()` emits current supported-symbol
snapshots after account context is known and whenever time-dependent limits
change. Intrade payout remains absent because it depends on a concrete amount
and duration; query that exact trade through `AccountInfoRequest`.

## `market_data::BaseMarketDataProvider`

Файл: `include/optionx_cpp/market_data/BaseMarketDataProvider.hpp`.
Expand Down
3 changes: 3 additions & 0 deletions include/optionx_cpp/platforms/IntradeBarPlatform.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
#include "IntradeBarPlatform/TradeExecutionComponent.hpp"
#include "IntradeBarPlatform/BalanceManager.hpp"
#include "IntradeBarPlatform/ActiveTradesSyncManager.hpp"
#include "IntradeBarPlatform/TradingConditionManager.hpp"
#include "IntradeBarPlatform/PriceManager.hpp"
#include "IntradeBarPlatform/BtcPriceManager.hpp"
#include "IntradeBarPlatform/FxPriceWebSocketManager.hpp"
Expand Down Expand Up @@ -53,6 +54,7 @@ namespace optionx::platforms {
m_auth_manager(*this, m_request_manager, m_account_info),
m_balance_manager(*this, m_request_manager, m_account_info),
m_active_trades_sync_manager(*this, m_request_manager, m_account_info),
m_trading_condition_manager(*this, m_account_info),
m_price_manager(*this, m_request_manager),
m_btc_price_manager(*this),
m_fx_price_websocket_manager(*this),
Expand Down Expand Up @@ -233,6 +235,7 @@ namespace optionx::platforms {
intrade_bar::AuthManager m_auth_manager; ///< Handles authentication processes.
intrade_bar::BalanceManager m_balance_manager; ///< Tracks account balance.
intrade_bar::ActiveTradesSyncManager m_active_trades_sync_manager; ///< Syncs broker active trade snapshots.
intrade_bar::TradingConditionManager m_trading_condition_manager; ///< Publishes current trading conditions.
intrade_bar::PriceManager m_price_manager; ///< Retrieves and updates price data.
intrade_bar::BtcPriceManager m_btc_price_manager;///< Retrieves BTC/USDT quotes from the websocket stream.
intrade_bar::FxPriceWebSocketManager m_fx_price_websocket_manager; ///< Retrieves FX quotes from websocket streams.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,19 +75,8 @@ namespace optionx::platforms::intrade_bar {
switch (request.type) {
case AccountInfoType::CONNECTION_STATUS:
return connect;
case AccountInfoType::SYMBOL_AVAILABILITY: {
static const std::set<std::string> symbols = {
"AUDCAD","AUDCHF","AUDJPY",
"AUDNZD","AUDUSD","CADJPY",
"EURAUD","EURCAD","EURCHF",
"EURGBP","EURJPY","EURUSD",
"GBPAUD","GBPCHF","GBPJPY",
"GBPNZD","NZDJPY","NZDUSD",
"USDCAD","USDCHF","USDJPY",
"BTCUSDT"
};
return (symbols.find(normalize_symbol_name(request.symbol)) != symbols.end());
}
case AccountInfoType::SYMBOL_AVAILABILITY:
return is_supported_symbol(request.symbol);
case AccountInfoType::OPTION_TYPE_AVAILABILITY:
if (request.option_type == OptionType::CLASSIC &&
is_btc_symbol(request.symbol)) return false;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,284 @@
#pragma once
#ifndef OPTIONX_HEADER_PLATFORMS_INTRADE_BAR_PLATFORM_TRADING_CONDITION_MANAGER_HPP_INCLUDED
#define OPTIONX_HEADER_PLATFORMS_INTRADE_BAR_PLATFORM_TRADING_CONDITION_MANAGER_HPP_INCLUDED

/// \file TradingConditionManager.hpp
/// \brief Publishes computed Intrade Bar trading-condition snapshots.

#include <algorithm>
#include <cstdint>
#include <limits>
#include <memory>
#include <string>
#include <utility>
#include <vector>

namespace optionx::platforms::intrade_bar {

/// \class TradingConditionManager
/// \brief Mirrors the Intrade Bar account condition model into public events.
class TradingConditionManager final : public components::BaseComponent {
public:
/// \brief Constructs and registers the condition manager.
/// \param platform Owning platform.
/// \param account_info Platform account snapshot.
explicit TradingConditionManager(
BaseTradingPlatform& platform,
std::shared_ptr<BaseAccountInfoData> account_info)
: BaseComponent(platform.event_bus()),
m_account_info(std::dynamic_pointer_cast<AccountInfoData>(
std::move(account_info))) {
subscribe<events::AccountInfoUpdateEvent>();
platform.register_component(this);
}

/// \brief Handles account lifecycle and state changes.
/// \param event Incoming event.
void on_event(const utils::Event* const event) override {
const auto* update =
dynamic_cast<const events::AccountInfoUpdateEvent*>(event);
if (!update) return;

if (auto account_info =
std::dynamic_pointer_cast<AccountInfoData>(update->account_info)) {
m_account_info = std::move(account_info);
}
if (!m_account_info) return;

switch (update->status) {
case AccountUpdateStatus::CONNECTING:
case AccountUpdateStatus::DISCONNECTED:
case AccountUpdateStatus::FAILED_TO_CONNECT:
m_connected = false;
break;
case AccountUpdateStatus::CONNECTED:
m_connected = true;
break;
case AccountUpdateStatus::BALANCE_UPDATED:
case AccountUpdateStatus::ACCOUNT_TYPE_CHANGED:
case AccountUpdateStatus::CURRENCY_CHANGED:
case AccountUpdateStatus::OPEN_TRADES_CHANGED:
m_connected = m_account_info->connect;
break;
case AccountUpdateStatus::UNKNOWN:
return;
}

refresh(current_timestamp_sec());
}

/// \brief Refreshes time-dependent conditions once per Unix second.
void process() override {
const auto timestamp = current_timestamp_sec();
if (timestamp == m_last_refresh_sec) return;
refresh(timestamp);
}

/// \brief Clears local condition state during platform shutdown.
void shutdown() override {
m_connected = false;
m_last_refresh_sec = 0;
m_last_snapshots.clear();
}

private:
std::shared_ptr<AccountInfoData> m_account_info; ///< Current Intrade account data.
std::vector<TradingConditionUpdate> m_last_snapshots; ///< Last published values by scope.
std::int64_t m_last_refresh_sec = 0; ///< Last evaluated Unix second.
bool m_connected = false; ///< Account lifecycle state from account events.

/// \brief Returns the current Unix timestamp in seconds.
static std::int64_t current_timestamp_sec() noexcept {
return time_shield::ms_to_sec(OPTIONX_TIMESTAMP_MS);
}

/// \brief Clamps a signed condition value to its public DTO width.
static std::uint32_t clamp_u32(std::int64_t value) noexcept {
if (value <= 0) return 0;
const auto maximum =
static_cast<std::int64_t>(std::numeric_limits<std::uint32_t>::max());
return static_cast<std::uint32_t>(std::min(value, maximum));
}

/// \brief Returns the earliest aligned classic expiry candidate.
static std::int64_t next_classic_expiry(std::int64_t timestamp) noexcept {
const auto future = timestamp + (8 * time_shield::SEC_PER_MIN);
return future - (future % time_shield::SEC_PER_5_MIN);
}

/// \brief Builds one current condition snapshot.
static TradingConditionUpdate make_snapshot(
const AccountInfoData& account,
const std::string& symbol,
OptionType option_type,
std::int64_t timestamp,
bool connected) {
TradingConditionUpdate snapshot;
snapshot.symbol = symbol;
snapshot.platform_type = PlatformType::INTRADE_BAR;
snapshot.account_type = account.account_type;
snapshot.currency = account.currency;
snapshot.option_type = option_type;
snapshot.timestamp = timestamp;

const bool btc = is_btc_symbol(symbol);
const auto second_of_day = time_shield::sec_of_day(timestamp);
const auto session_start = btc ? account.start_btc_time : account.start_time;
const auto session_end = btc ? account.end_btc_time : account.end_time;
const bool session_open =
second_of_day >= session_start && second_of_day < session_end;
const bool market_open = session_open &&
(btc || !time_shield::is_day_off(timestamp));

AccountInfoRequest request;
request.symbol = symbol;
request.account_type = account.account_type;
request.currency = account.currency;
request.option_type = option_type;
request.timestamp = timestamp;

request.type = AccountInfoType::MIN_AMOUNT;
snapshot.min_amount = account.get_info<double>(request);
request.type = AccountInfoType::MAX_AMOUNT;
snapshot.max_amount = account.get_info<double>(request);
request.type = AccountInfoType::MAX_TRADES;
snapshot.max_open_trades = clamp_u32(account.get_info<std::int64_t>(request));

snapshot.market_open = market_open;
snapshot.session_start = session_start;
snapshot.session_end = session_end;

bool expiration_available = true;
if (option_type == OptionType::SPRINT) {
request.type = AccountInfoType::MIN_DURATION;
const auto min_duration = account.get_info<std::int64_t>(request);
request.type = AccountInfoType::MAX_DURATION;
const auto max_duration = account.get_info<std::int64_t>(request);
snapshot.min_duration = clamp_u32(min_duration);
snapshot.max_duration = clamp_u32(max_duration);
expiration_available = max_duration >= min_duration;
} else {
const auto expiry_time = next_classic_expiry(timestamp);
const auto expiry_second_of_day =
time_shield::sec_of_day(expiry_time);
expiration_available =
expiry_second_of_day >= session_start &&
expiry_second_of_day <= session_end &&
(expiry_time % time_shield::SEC_PER_5_MIN) == 0 &&
(expiry_time - timestamp) >= time_shield::SEC_PER_3_MIN;
}

const bool trade_slot_available =
account.open_trades < static_cast<std::int64_t>(*snapshot.max_open_trades);
snapshot.tradable = connected && market_open &&
expiration_available && trade_slot_available;
return snapshot;
}

/// \brief Builds current snapshots for all supported symbol/option scopes.
std::vector<TradingConditionUpdate> make_snapshots(
std::int64_t timestamp) const {
std::vector<TradingConditionUpdate> snapshots;
snapshots.reserve((supported_symbols().size() * 2) - 1);
for (const auto* symbol : supported_symbols()) {
snapshots.push_back(make_snapshot(
*m_account_info,
symbol,
OptionType::SPRINT,
timestamp,
m_connected));
if (!is_btc_symbol(symbol)) {
snapshots.push_back(make_snapshot(
*m_account_info,
symbol,
OptionType::CLASSIC,
timestamp,
m_connected));
}
}
return snapshots;
}

/// \brief Compares generated condition values while ignoring timestamp.
static bool same_values(
const TradingConditionUpdate& left,
const TradingConditionUpdate& right) {
return left.same_scope(right) &&
left.market_open == right.market_open &&
left.tradable == right.tradable &&
left.payout == right.payout &&
left.min_amount == right.min_amount &&
left.max_amount == right.max_amount &&
left.min_refund == right.min_refund &&
left.max_refund == right.max_refund &&
left.min_duration == right.min_duration &&
left.max_duration == right.max_duration &&
left.max_open_trades == right.max_open_trades &&
left.session_start == right.session_start &&
left.session_end == right.session_end &&
left.message == right.message;
}

/// \brief Publishes one condition update synchronously on the platform bus.
void publish(const TradingConditionUpdate& update) const {
notify(events::TradingConditionUpdateEvent(update));
}

/// \brief Marks cached scopes unavailable before replacing their identity.
void retire_missing_scopes(
const std::vector<TradingConditionUpdate>& next,
std::int64_t timestamp) const {
for (const auto& previous : m_last_snapshots) {
const auto found = std::find_if(
next.begin(),
next.end(),
[&previous](const TradingConditionUpdate& candidate) {
return previous.same_scope(candidate);
});
if (found != next.end()) continue;

TradingConditionUpdate retired;
retired.symbol = previous.symbol;
retired.platform_type = previous.platform_type;
retired.account_type = previous.account_type;
retired.currency = previous.currency;
retired.option_type = previous.option_type;
retired.timestamp = timestamp;
retired.tradable = false;
retired.message = "Intrade Bar account condition scope changed.";
publish(retired);
}
}

/// \brief Recomputes snapshots and publishes only changed scopes.
void refresh(std::int64_t timestamp) {
m_last_refresh_sec = timestamp;
if (!m_account_info ||
m_account_info->account_type == AccountType::UNKNOWN ||
m_account_info->currency == CurrencyType::UNKNOWN) {
retire_missing_scopes({}, timestamp);
m_last_snapshots.clear();
return;
}

auto next = make_snapshots(timestamp);
retire_missing_scopes(next, timestamp);
for (const auto& current : next) {
const auto previous = std::find_if(
m_last_snapshots.begin(),
m_last_snapshots.end(),
[&current](const TradingConditionUpdate& candidate) {
return current.same_scope(candidate);
});
if (previous == m_last_snapshots.end() ||
!same_values(*previous, current)) {
publish(current);
}
}
m_last_snapshots = std::move(next);
}
};

} // namespace optionx::platforms::intrade_bar

#endif // OPTIONX_HEADER_PLATFORMS_INTRADE_BAR_PLATFORM_TRADING_CONDITION_MANAGER_HPP_INCLUDED
Loading
Loading