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 .github/workflows/ubuntu-smoke.yml
Original file line number Diff line number Diff line change
Expand Up @@ -217,11 +217,21 @@ jobs:
cmake --build build-linux --target
market_data_tick_continuity_test intrade_observed_tick_history_test -j

- name: Build trading system batch tests
run: >
cmake --build build-linux --target
trading_system_batch1_test trading_system_batch2_test -j

- name: Run market data tick continuity and observed history tests
run: |
./build-linux/market_data_tick_continuity_test --gtest_brief=1
./build-linux/intrade_observed_tick_history_test --gtest_brief=1

- name: Run trading system batch tests
run: |
./build-linux/trading_system_batch1_test --gtest_brief=1
./build-linux/trading_system_batch2_test --gtest_brief=1

- name: Build market_data_subscriber_base_test and example
run: cmake --build build-linux --target market_data_subscriber_base_test market_data_subscriber_base_example -j

Expand Down
7 changes: 7 additions & 0 deletions .github/workflows/windows-smoke.yml
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,11 @@ jobs:
cmake --build build-windows --config Debug --target
market_data_tick_continuity_test intrade_observed_tick_history_test

- name: Build trading system batch tests
run: >
cmake --build build-windows --config Debug --target
trading_system_batch1_test trading_system_batch2_test

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

Expand All @@ -133,6 +138,8 @@ jobs:
.\build-windows\Debug\market_data_tick_history_contract_test.exe --gtest_brief=1
.\build-windows\Debug\market_data_tick_continuity_test.exe --gtest_brief=1
.\build-windows\Debug\intrade_observed_tick_history_test.exe --gtest_brief=1
.\build-windows\Debug\trading_system_batch1_test.exe --gtest_brief=1
.\build-windows\Debug\trading_system_batch2_test.exe --gtest_brief=1
.\build-windows\Debug\metatrader_file_bridge_smoke.exe --self-test
.\build-windows\Debug\metatrader_file_command_writer_smoke.exe --self-test
.\build-windows\Debug\metatrader_file_end_to_end_smoke.exe --self-test
Expand Down
2 changes: 2 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@
локальные проверки, примеры и generated output.
- [Implementation notes](guides/implementation-notes.md) - lifecycle, pub-sub,
task scheduling, HTTP/WebSocket, trade queue, session DB и ограничения.
- [Trading system Batch 2](guides/trading-system-batch2.md) - pure filters,
decision snapshots and fixed-point sizing policies.
- [Trade check API research](guides/trade-check2-research.md) - границы
доказанного контракта `trade_check2.php` и решение не использовать его как
authoritative tick-history API.
Expand Down
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1113,6 +1113,7 @@ if(OPTIONX_BUILD_TESTS)
market_data_tick_continuity_test
intrade_observed_tick_history_test
trading_system_batch1_test
trading_system_batch2_test
)
if(OPTIONX_LIGHTWEIGHT_BRIDGE_SMOKE_TESTS)
list(APPEND OPTIONX_LIGHTWEIGHT_TESTS
Expand Down
7 changes: 4 additions & 3 deletions guides/api-and-header-contracts.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
```cpp
#include <optionx_cpp/optionx.hpp>
#include <optionx_cpp/data.hpp>
#include <optionx_cpp/trading_system.hpp>
#include <optionx_cpp/market_data.hpp>
#include <optionx_cpp/platforms.hpp>
#include <optionx_cpp/platforms/IntradeBarPlatform.hpp>
Expand All @@ -41,9 +42,9 @@
angle brackets и установленный префикс `<optionx_cpp/...>`.
- Не полагайся на `include/optionx_cpp` как на дополнительный include-root:
consumer contract должен работать только с корнем `include`.
- Public aggregate headers (`optionx.hpp`, `data.hpp`, `market_data.hpp`,
`platforms.hpp`, `storages.hpp`, `components.hpp`, `utils.hpp`,
`bridges.hpp`) задают публичные точки подключения.
- Public aggregate headers (`optionx.hpp`, `data.hpp`, `trading_system.hpp`,
`market_data.hpp`, `platforms.hpp`, `storages.hpp`, `components.hpp`,
`utils.hpp`, `bridges.hpp`) задают публичные точки подключения.
- Bridge families are public only through `bridges.hpp` or the family umbrella
headers: `bridges/metatrader_file.hpp`, `bridges/legacy_trading.hpp`,
`bridges/named_pipe.hpp` and `bridges/trading_view.hpp`.
Expand Down
4 changes: 2 additions & 2 deletions guides/codebase-orientation.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@
## Public Include Points

Главная include-точка - `include/optionx_cpp/optionx.hpp`. Она включает
`utils.hpp`, `lifecycle.hpp`, `data.hpp`, `storages.hpp`, `components.hpp`,
`platforms.hpp`, `bridges.hpp`.
`utils.hpp`, `lifecycle.hpp`, `data.hpp`, `trading_system.hpp`, `storages.hpp`,
`components.hpp`, `platforms.hpp`, `bridges.hpp`.

Aggregate headers в корне `include/optionx_cpp` - часть публичной поверхности.
Если добавляешь новый публичный DTO/component/platform, проверь соответствующий
Expand Down
2 changes: 2 additions & 0 deletions guides/project-overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ node lifecycles. See [English](lifecycle-stack.md) or
| `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/trading_system.hpp` | Trading-system identity, decision context, filters and sizing policies | Для broker-independent admission decisions before execution |
| `optionx_cpp/components.hpp` | `BaseComponent`, HTTP и trade execution base classes | Для нового manager/component |
| `optionx_cpp/market_data.hpp` | Provider role, subscription DTOs, Router, subscriber base and continuity | Для live tick/bar subscriptions, history prefill and routed delivery |
| `optionx_cpp/platforms.hpp` | Base platform и Intrade Bar platform | Для клиентского кода платформ |
Expand All @@ -51,6 +52,7 @@ node lifecycles. See [English](lifecycle-stack.md) or
| Platform facade | `optionx::platforms`, `include/optionx_cpp/platforms` | Публичный API платформы: connect, auth, trades, account info |
| Components/managers | `optionx::components`, platform subnamespaces | Lifecycle-компоненты, которые получают events и выполняют работу |
| Trading data | `optionx`, `include/optionx_cpp/data/trading` | `TradeRequest`, `TradeResult`, enums, signals |
| Trading system | `optionx::trading_system`, `include/optionx_cpp/trading_system` | Intent/series identity, pure policy decisions and fixed-point sizing |
| Market data | `optionx::market_data`, `include/optionx_cpp/market_data`, `data/bars`, `data/ticks`, `data/symbol` | Provider subscriptions, routed delivery, history prefill/recovery, ticks, bars and symbols |
| Events | `optionx::events`, `include/optionx_cpp/data/events` | Pub-sub контракты между components |
| Infrastructure | `optionx::utils` | EventBus, tasks, crypto, ids, HTTP helpers |
Expand Down
101 changes: 101 additions & 0 deletions guides/trading-system-batch2.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
# Trading System Batch 2

Batch 2 adds deterministic, broker-independent policies to the
`optionx::trading_system` aggregate. It does not submit trades, read a live
account, persist state, or own a scheduler.

## Decision Context

`TradingDecisionContext` is a value snapshot. It copies the symbol and source
identity needed by filters, the explicit evaluation timestamp and
`TradeTimeZone`, and a `BalanceSnapshot` for sizing. Policy calls do not read
the system clock. Build the context once and pass it as a `const` value.

`TradeTimeZone` delegates named-zone and DST behavior to `time-shield`. Fixed
offset mode is available when the caller has an explicit broker offset. The
general `TradeTimeZone` conversion helpers retain their UTC/identity fallback
for compatibility. `TradingWindowFilter` uses the checked conversion API and
returns `UNAVAILABLE / INVALID_CONFIGURATION` when a requested named zone
cannot be resolved; it never evaluates a trading window against a silent UTC
fallback.

## Filter Decisions

`SymbolFilter`, `SourceFilter` and `TradingWindowFilter` return a
`PolicyDecision` with one of:

- `ALLOW` when the input matches;
- `REJECT` when a configured policy denies it;
- `UNAVAILABLE` when required context is missing or a symbol/time configuration
cannot be evaluated.

`SymbolFilter` uses generic separator/case normalization by default, so
`EUR/USD` and `eurusd` compare as `EURUSD`. A caller may inject an existing
broker normalizer, such as the Intrade Bar alias helper, and provide a known
symbol set. Unknown symbols in that explicit set are `UNAVAILABLE`, while an
explicitly disallowed known symbol is `REJECT`.

`TradingTimeWindow` uses half-open intervals `[start, end)`. An interval may
cross midnight, for example `{22 * 3600, 2 * 3600}`. Midnight is a valid end
point, so `{22 * 3600, 0}` means 22:00 through 00:00 while `{0, 0}` remains
invalid. Weekday restrictions identify the day on which an allowed window
starts: the after-midnight part of a Monday 22:00-02:00 window still belongs to
Monday. With no allowed windows, weekdays retain calendar-day semantics. The
filter evaluates local time after applying the timezone from the context, and
denied windows always take precedence over an allowed match.

## Fixed-Point Sizing

`MoneyAmount` stores integer minor units plus an explicit decimal precision.
`MoneyAmount::from_decimal()` rejects significant digits beyond the declared
precision instead of silently rounding them.

`FixedStake` returns its configured positive amount. `PercentOfBalance` accepts
a percentage in the range `(0, 100]`; the `double` constructor explicitly
converts it to basis points, while `from_basis_points()` is available when the
caller already has an exact value. The calculated amount uses integer
arithmetic. `MoneyRounding` explicitly selects `DOWN`, `HALF_UP`, or `UP` for
the percentage and output-precision boundaries.

The output precision may be equal to or finer than the balance precision. A
coarser output precision is rejected as `PRECISION_MISMATCH`; the policy does
not silently discard balance digits before calculating the percentage.

Stale, unavailable, unknown, or missing balances produce an `UNAVAILABLE`
sizing result. Zero and negative balances, invalid amounts, currency
mismatches, and zero calculated stakes are rejected. The policy only proposes
an amount; broker limits, payout, reservations, and final admission remain
later responsibilities.

## Sequential State Guards

`TradeIntent::advance_revision()` and `TradeSeries::advance_revision()` remain
local sequential guards for one state owner. They are not thread-safe CAS
operations. Their callers must serialize DTO access; persistent/runtime state
coordination belongs to a single owner or a storage-layer compare-and-swap.
Revision overflow is rejected rather than wrapped to zero.

## Example

```cpp
auto context = TradingDecisionContext::from_intent(
intent, evaluation_time_ms, optionx::TradeTimeZone::utc());
context.account.availability = DataAvailability::SUPPORTED;
context.account.currency = optionx::CurrencyType::USD;
context.account.balance = MoneyAmount::from_decimal(
"100.00", optionx::CurrencyType::USD, 2);

const SymbolFilter symbols({"EUR/USD"});
const SourceFilter source({7}, {}, {"alpha"});
const TradingWindowFilter window({{9 * 3600, 10 * 3600}});
const FixedStake stake(*MoneyAmount::from_decimal(
"5.00", optionx::CurrencyType::USD, 2));

const auto symbol_result = symbols.evaluate(context);
const auto source_result = source.evaluate(context);
const auto window_result = window.evaluate(context);
const auto size_result = stake.evaluate(context);
```

The example is intentionally a pure decision chain. It does not imply that a
successful policy result is permission to submit a broker request.
38 changes: 35 additions & 3 deletions include/optionx_cpp/data/trading/TradeTimeZone.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,13 @@ namespace optionx {
}

/// \brief Creates named-zone context.
/// \param value time-shield zone value. UNKNOWN falls back to UTC.
/// \param value time-shield zone value. UNKNOWN keeps UTC conversion
/// fallback but remains detectable as an unsupported configuration.
static TradeTimeZone named(time_shield::TimeZone value) noexcept {
if (value == time_shield::UNKNOWN) {
return utc();
auto result = utc();
result.m_named_zone_supported = false;
return result;
}

TradeTimeZone result;
Expand Down Expand Up @@ -103,9 +106,37 @@ namespace optionx {
: 0;
}

/// \brief Tries to convert UTC milliseconds to local civil milliseconds.
/// \param utc_ms UTC timestamp in milliseconds.
/// \param local_ms Receives the converted local timestamp on success.
/// \return False when a requested named zone cannot be resolved.
bool try_to_local_ms(
std::int64_t utc_ms,
std::int64_t& local_ms) const noexcept {
if (!m_named_zone_supported) {
return false;
}
if (!is_named_zone()) {
local_ms = utc_ms + m_offset_sec * time_shield::MS_PER_SEC;
return true;
}

time_shield::tz_t resolved = 0;
if (!time_shield::zone_offset_at_utc_ms(
static_cast<time_shield::ts_ms_t>(utc_ms),
m_zone,
resolved)) {
return false;
}
local_ms = utc_ms +
static_cast<std::int64_t>(resolved) * time_shield::MS_PER_SEC;
return true;
}

/// \brief Converts UTC milliseconds to this context's local civil milliseconds.
std::int64_t to_local_ms(std::int64_t utc_ms) const noexcept {
return utc_ms + offset_at_utc_ms(utc_ms) * time_shield::MS_PER_SEC;
std::int64_t local_ms = utc_ms;
return try_to_local_ms(utc_ms, local_ms) ? local_ms : utc_ms;
}

/// \brief Converts local civil milliseconds in this context back to UTC.
Expand Down Expand Up @@ -151,6 +182,7 @@ namespace optionx {
TradeTimeZoneMode m_mode = TradeTimeZoneMode::FIXED_OFFSET;
std::int64_t m_offset_sec = 0;
time_shield::TimeZone m_zone = time_shield::UTC;
bool m_named_zone_supported = true;
};

} // namespace optionx
Expand Down
4 changes: 4 additions & 0 deletions include/optionx_cpp/trading_system.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,9 @@
#include "trading_system/Identity.hpp"
#include "trading_system/TradeSeries.hpp"
#include "trading_system/TradeIntent.hpp"
#include "trading_system/PolicyTypes.hpp"
#include "trading_system/DecisionContext.hpp"
#include "trading_system/Filters.hpp"
#include "trading_system/Sizing.hpp"

#endif // OPTIONX_HEADER_TRADING_SYSTEM_HPP_INCLUDED
79 changes: 79 additions & 0 deletions include/optionx_cpp/trading_system/DecisionContext.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
#pragma once
#ifndef OPTIONX_HEADER_TRADING_SYSTEM_DECISION_CONTEXT_HPP_INCLUDED
#define OPTIONX_HEADER_TRADING_SYSTEM_DECISION_CONTEXT_HPP_INCLUDED

/// \file DecisionContext.hpp
/// \brief Defines the small value snapshot consumed by Batch 2 policies.

#include "Identity.hpp"
#include "PolicyTypes.hpp"
#include "TradeIntent.hpp"

#include <optional>
#include <string>
#include <utility>

namespace optionx::trading_system {

/// \brief Account balance snapshot supplied to pure sizing policies.
struct BalanceSnapshot {
DataAvailability availability = DataAvailability::UNKNOWN;
CurrencyType currency = CurrencyType::UNKNOWN;
std::optional<MoneyAmount> balance;
std::int64_t captured_at_ms = 0;
std::uint64_t revision = 0;
};

/// \brief Immutable-by-convention input snapshot for stateless policies.
///
/// The snapshot owns all strings and scalar values needed by Batch 2. It does
/// not retain broker/platform pointers, and policy evaluation never reads the
/// system clock or calls a live provider. Construct it once and pass it as a
/// const value to policy objects.
struct TradingDecisionContext {
IntentId intent_id = 0;
SignalId signal_id = 0;
std::string symbol;
BridgeId bridge_id = 0;
std::string signal_name;
std::optional<std::int64_t> evaluation_time_ms;
TradeTimeZone time_zone = TradeTimeZone::utc();
BalanceSnapshot account;

static TradingDecisionContext from_signal(
const TradeSignal& signal,
std::int64_t evaluation_time_ms,
TradeTimeZone time_zone,
BalanceSnapshot account = {}) {
TradingDecisionContext result;
result.signal_id = signal.signal_id;
result.symbol = signal.symbol;
result.bridge_id = signal.bridge_id;
result.signal_name = signal.signal_name;
result.evaluation_time_ms = evaluation_time_ms;
result.time_zone = time_zone;
result.account = std::move(account);
return result;
}

static TradingDecisionContext from_intent(
const TradeIntent& intent,
std::int64_t evaluation_time_ms,
TradeTimeZone time_zone,
BalanceSnapshot account = {}) {
TradingDecisionContext result;
result.intent_id = intent.intent_id;
result.signal_id = intent.origin_signal_id;
result.symbol = intent.request.symbol;
result.bridge_id = intent.request.bridge_id;
result.signal_name = intent.request.signal_name;
result.evaluation_time_ms = evaluation_time_ms;
result.time_zone = time_zone;
result.account = std::move(account);
return result;
}
};

} // namespace optionx::trading_system

#endif // OPTIONX_HEADER_TRADING_SYSTEM_DECISION_CONTEXT_HPP_INCLUDED
Loading
Loading