From 80682d30107181ba4753904072db5d04489ad3ae Mon Sep 17 00:00:00 2001 From: Aster Seker Date: Thu, 27 Aug 2026 13:57:43 +0300 Subject: [PATCH] feat(intrade): publish trading condition snapshots --- guides/api-and-header-contracts.md | 12 + guides/implementation-notes.md | 17 ++ guides/platform-api-guide.md | 7 + .../platforms/IntradeBarPlatform.hpp | 3 + .../IntradeBarPlatform/AccountInfoData.hpp | 15 +- .../TradingConditionManager.hpp | 284 ++++++++++++++++++ .../IntradeBarPlatform/symbol_utils.hpp | 40 ++- .../intrade_bar_api_response_test.cpp | 112 +++++++ 8 files changed, 464 insertions(+), 26 deletions(-) create mode 100644 include/optionx_cpp/platforms/IntradeBarPlatform/TradingConditionManager.hpp diff --git a/guides/api-and-header-contracts.md b/guides/api-and-header-contracts.md index 553c045c..bace461c 100644 --- a/guides/api-and-header-contracts.md +++ b/guides/api-and-header-contracts.md @@ -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, чтобы не смешивать diff --git a/guides/implementation-notes.md b/guides/implementation-notes.md index 3fa89e5d..d045eec5 100644 --- a/guides/implementation-notes.md +++ b/guides/implementation-notes.md @@ -251,6 +251,23 @@ facade lifecycle или остаться probe/internal component. Не меняй account info напрямую из application code. Для user-facing чтения используй `BaseTradingPlatform::get_info()`. +### 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`. diff --git a/guides/platform-api-guide.md b/guides/platform-api-guide.md index deba70fc..ad8b7572 100644 --- a/guides/platform-api-guide.md +++ b/guides/platform-api-guide.md @@ -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`. diff --git a/include/optionx_cpp/platforms/IntradeBarPlatform.hpp b/include/optionx_cpp/platforms/IntradeBarPlatform.hpp index 7af24cf4..a0922846 100644 --- a/include/optionx_cpp/platforms/IntradeBarPlatform.hpp +++ b/include/optionx_cpp/platforms/IntradeBarPlatform.hpp @@ -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" @@ -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), @@ -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. diff --git a/include/optionx_cpp/platforms/IntradeBarPlatform/AccountInfoData.hpp b/include/optionx_cpp/platforms/IntradeBarPlatform/AccountInfoData.hpp index 05421881..65e94c40 100644 --- a/include/optionx_cpp/platforms/IntradeBarPlatform/AccountInfoData.hpp +++ b/include/optionx_cpp/platforms/IntradeBarPlatform/AccountInfoData.hpp @@ -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 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; diff --git a/include/optionx_cpp/platforms/IntradeBarPlatform/TradingConditionManager.hpp b/include/optionx_cpp/platforms/IntradeBarPlatform/TradingConditionManager.hpp new file mode 100644 index 00000000..05da6c9a --- /dev/null +++ b/include/optionx_cpp/platforms/IntradeBarPlatform/TradingConditionManager.hpp @@ -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 +#include +#include +#include +#include +#include +#include + +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 account_info) + : BaseComponent(platform.event_bus()), + m_account_info(std::dynamic_pointer_cast( + std::move(account_info))) { + subscribe(); + 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(event); + if (!update) return; + + if (auto account_info = + std::dynamic_pointer_cast(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 m_account_info; ///< Current Intrade account data. + std::vector 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::numeric_limits::max()); + return static_cast(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(request); + request.type = AccountInfoType::MAX_AMOUNT; + snapshot.max_amount = account.get_info(request); + request.type = AccountInfoType::MAX_TRADES; + snapshot.max_open_trades = clamp_u32(account.get_info(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(request); + request.type = AccountInfoType::MAX_DURATION; + const auto max_duration = account.get_info(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(*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 make_snapshots( + std::int64_t timestamp) const { + std::vector 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& 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(), + [¤t](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 diff --git a/include/optionx_cpp/platforms/IntradeBarPlatform/symbol_utils.hpp b/include/optionx_cpp/platforms/IntradeBarPlatform/symbol_utils.hpp index e5bf1ec1..1c427015 100644 --- a/include/optionx_cpp/platforms/IntradeBarPlatform/symbol_utils.hpp +++ b/include/optionx_cpp/platforms/IntradeBarPlatform/symbol_utils.hpp @@ -12,6 +12,21 @@ namespace optionx::platforms::intrade_bar { + /// \brief Returns the symbols supported by the Intrade Bar trading model. + /// \return Stable list of normalized broker symbol names. + inline const std::array& supported_symbols() noexcept { + static constexpr std::array 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; + } + /// \brief Converts public symbol aliases to broker-side Intrade Bar names. /// \param symbol Public or broker symbol name. /// \return Normalized broker symbol name. @@ -36,26 +51,25 @@ namespace optionx::platforms::intrade_bar { return normalize_symbol_name(symbol) == "BTCUSDT"; } - /// \brief Checks whether `/fxconnect` is expected to support this FX symbol. + /// \brief Checks whether a symbol is supported by the Intrade Bar trading model. /// \param symbol Public or broker symbol name. - /// \return True for known Intrade Bar FX websocket symbols. - inline bool is_fxconnect_supported_symbol(const std::string& symbol) { + /// \return True for a known FX or BTC symbol. + inline bool is_supported_symbol(const std::string& symbol) { const auto normalized = normalize_symbol_name(symbol); - static constexpr std::array symbols = {{ - "AUDCAD", "AUDCHF", "AUDJPY", "AUDNZD", "AUDUSD", - "CADJPY", - "EURAUD", "EURCAD", "EURCHF", "EURGBP", "EURJPY", "EURUSD", - "GBPAUD", "GBPCHF", "GBPJPY", "GBPNZD", - "NZDJPY", "NZDUSD", - "USDCAD", "USDCHF", "USDJPY" - }}; - - for (const auto* item : symbols) { + for (const auto* item : supported_symbols()) { if (normalized == item) return true; } return false; } + /// \brief Checks whether `/fxconnect` is expected to support this FX symbol. + /// \param symbol Public or broker symbol name. + /// \return True for known Intrade Bar FX websocket symbols. + inline bool is_fxconnect_supported_symbol(const std::string& symbol) { + const auto normalized = normalize_symbol_name(symbol); + return normalized != "BTCUSDT" && is_supported_symbol(normalized); + } + /// \brief Converts a normalized FX symbol to the `/fxconnect` stream format. /// \param symbol Public or broker symbol name. /// \return Slash-separated stream symbol, such as `EUR/USD`; empty for non-FX symbols. diff --git a/tests/intrade_bar_api/intrade_bar_api_response_test.cpp b/tests/intrade_bar_api/intrade_bar_api_response_test.cpp index 8dea6a2f..6741bfde 100644 --- a/tests/intrade_bar_api/intrade_bar_api_response_test.cpp +++ b/tests/intrade_bar_api/intrade_bar_api_response_test.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -17,6 +18,7 @@ #include #include +#include #include using namespace optionx; @@ -2011,6 +2013,116 @@ TEST(IntradeBarApiResponses, FxWebSocketSubscriptionSurvivesAccountDisconnect) { platform.shutdown(); } +TEST(IntradeBarTradingConditions, ConnectedAccountPublishesSupportedScopes) { + IntradeBarPlatform platform; + std::vector updates; + platform.on_trading_condition() = + [&updates](const TradingConditionUpdate& update) { + updates.push_back(update); + }; + + auto account = std::make_shared(); + account->connect = true; + account->account_type = AccountType::DEMO; + account->currency = CurrencyType::USD; + platform.event_bus().notify_async( + std::make_unique( + account, + AccountUpdateStatus::CONNECTED)); + platform.event_bus().drain(); + + EXPECT_EQ(updates.size(), (supported_symbols().size() * 2) - 1); + const auto find_update = [&updates]( + const std::string& symbol, + OptionType option_type) { + return std::find_if( + updates.begin(), + updates.end(), + [&symbol, option_type](const TradingConditionUpdate& update) { + return update.symbol == symbol && + update.option_type == option_type; + }); + }; + + const auto eur = find_update("EURUSD", OptionType::SPRINT); + ASSERT_NE(eur, updates.end()); + EXPECT_EQ(eur->platform_type, PlatformType::INTRADE_BAR); + EXPECT_EQ(eur->account_type, AccountType::DEMO); + EXPECT_EQ(eur->currency, CurrencyType::USD); + ASSERT_TRUE(eur->min_amount); + EXPECT_DOUBLE_EQ(*eur->min_amount, account->min_usd_amount); + ASSERT_TRUE(eur->max_amount); + ASSERT_TRUE(eur->max_open_trades); + EXPECT_GT(*eur->max_open_trades, 0u); + EXPECT_EQ(eur->min_duration, std::optional(60u)); + EXPECT_EQ( + eur->session_start, + std::optional(account->start_time)); + EXPECT_EQ( + eur->session_end, + std::optional(account->end_time)); + EXPECT_FALSE(eur->payout); + + const auto btc = find_update("BTCUSDT", OptionType::SPRINT); + ASSERT_NE(btc, updates.end()); + EXPECT_EQ(btc->market_open, std::optional(true)); + EXPECT_EQ(btc->tradable, std::optional(true)); + EXPECT_EQ( + btc->min_duration, + std::optional( + static_cast(account->min_btc_duration))); + EXPECT_EQ( + btc->max_duration, + std::optional( + static_cast(account->max_duration))); + EXPECT_EQ(find_update("BTCUSDT", OptionType::CLASSIC), updates.end()); + + platform.shutdown(); +} + +TEST(IntradeBarTradingConditions, OpenTradeLimitUpdatesPlatformBoundHub) { + IntradeBarPlatform platform; + components::TradingConditionHub hub; + hub.bind_to(platform.on_trading_condition()); + + auto account = std::make_shared(); + account->connect = true; + account->account_type = AccountType::REAL; + account->currency = CurrencyType::RUB; + platform.event_bus().notify_async( + std::make_unique( + account, + AccountUpdateStatus::CONNECTED)); + platform.event_bus().drain(); + + TradingConditionUpdate scope; + scope.symbol = "BTCUSDT"; + scope.platform_type = PlatformType::INTRADE_BAR; + scope.account_type = AccountType::REAL; + scope.currency = CurrencyType::RUB; + scope.option_type = OptionType::SPRINT; + + auto current = hub.current_condition(scope); + ASSERT_TRUE(current); + EXPECT_EQ(current->tradable, std::optional(true)); + EXPECT_EQ( + current->min_amount, + std::optional(account->min_rub_amount)); + + account->open_trades = account->max_trades; + platform.event_bus().notify_async( + std::make_unique( + account, + AccountUpdateStatus::OPEN_TRADES_CHANGED)); + platform.event_bus().drain(); + + current = hub.current_condition(scope); + ASSERT_TRUE(current); + EXPECT_EQ(current->tradable, std::optional(false)); + + platform.shutdown(); +} + TEST(IntradeBarAccountInfo, AcceptsBtcAliasAndUsesBtcDurationRules) { AccountInfoData account; const int64_t day_timestamp = 1712345600;