From b5c1c03ab91cd453e363bb214aeaff8e94c31894 Mon Sep 17 00:00:00 2001 From: LimiNode <20008174+LimiNode@users.noreply.github.com> Date: Sat, 31 Oct 2026 02:31:40 +0300 Subject: [PATCH] docs(trading): define trading system foundation --- AGENTS.md | 8 + guides/TRADING_SYSTEM_ARCHITECTURE.md | 142 ++++++++++++++ guides/TRADING_SYSTEM_CONTRACTS.md | 188 +++++++++++++++++++ guides/TRADING_SYSTEM_IMPLEMENTATION_PLAN.md | 128 +++++++++++++ guides/TRADING_SYSTEM_REUSE_AUDIT.md | 69 +++++++ 5 files changed, 535 insertions(+) create mode 100644 guides/TRADING_SYSTEM_ARCHITECTURE.md create mode 100644 guides/TRADING_SYSTEM_CONTRACTS.md create mode 100644 guides/TRADING_SYSTEM_IMPLEMENTATION_PLAN.md create mode 100644 guides/TRADING_SYSTEM_REUSE_AUDIT.md diff --git a/AGENTS.md b/AGENTS.md index 2dda306..6c64b02 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -10,6 +10,14 @@ задач с изменением файлов. - [Project overview](guides/project-overview.md) - назначение библиотеки, публичная поверхность, основные домены и поток выполнения. +- [Trading system architecture](guides/TRADING_SYSTEM_ARCHITECTURE.md) - + границы нового домена торговых систем, identity model и adapter boundary. +- [Trading system reuse audit](guides/TRADING_SYSTEM_REUSE_AUDIT.md) - + инвентаризация существующих DTO, платформ, Router и storage перед runtime. +- [Trading system contracts](guides/TRADING_SYSTEM_CONTRACTS.md) - предложенные + минимальные lifecycle, context, branching и risk contracts. +- [Trading system implementation plan](guides/TRADING_SYSTEM_IMPLEMENTATION_PLAN.md) - + последовательность небольших PR и первый synthetic vertical slice. - [Platform API guide](guides/platform-api-guide.md) - практический справочник по платформам, модулям, событиям, DTO, storage и bridge API. - [API and header contracts](guides/api-and-header-contracts.md) - публичный diff --git a/guides/TRADING_SYSTEM_ARCHITECTURE.md b/guides/TRADING_SYSTEM_ARCHITECTURE.md new file mode 100644 index 0000000..1affa63 --- /dev/null +++ b/guides/TRADING_SYSTEM_ARCHITECTURE.md @@ -0,0 +1,142 @@ +# OptionX Trading System Architecture + +## Status + +This is the foundation decision for a future `optionx::trading_system` domain. +It is intentionally independent of ADELIA, MegaConnector, GUI code and broker +implementations. This document defines boundaries and invariants; it does not +authorize a production runtime implementation by itself. + +## Responsibility Boundary + +OptionX has two related but separate responsibilities: + +| Area | Responsibility | +| --- | --- | +| Broker and data APIs | Authentication, account data, market data, trade requests, broker results and persistence primitives. | +| Trading system | Signals, decision context, filters, sizing, risk admission, intent lifecycle, series progression and recovery contracts. | + +MegaConnector composes these primitives into an application. ADELIA may expose +the same primitives as graph nodes and typed edges. Neither dependency is +visible from OptionX headers. A normal C++ bot must be able to use the trading +system without either project. + +The intended dependency direction is: + +```text +broker/data APIs -> trading-system contracts -> host composition + ^ + ADELIA/MegaConnector adapter +``` + +The arrow is a boundary, not a required call sequence. A signal can originate +from a bridge, a market-data event, a schedule, or an active module. + +## Core Identity Model + +The system keeps these identities distinct: + +| Identity | Meaning | Lifetime | +| --- | --- | --- | +| `signal_id` | Immutable source event and correlation identity. | Persistent source record. | +| `intent_id` | One candidate logical operation after policy decisions. | Persistent until terminal outcome. | +| `series_id` | Stateful progression such as martingale or anti-martingale. | Persistent across operations and restarts. | +| `execution_group_id` | One logical operation represented by one or more physical broker trades. | Persistent until all members settle or become unknown. | +| `trade_id` | OptionX persistent identity for one physical trade record. | Existing `TradeRecordDB` identity. | +| `broker_trade_id` | Broker-side numeric or string identity. | Available only after broker acceptance, or unresolved. | + +`TradeSignal` remains the source event. It must not be mutated to represent a +later branch. A `TradeIntent` is the mutable, policy-approved candidate. A +`TradeRequest` is the broker-facing physical request. A `TradeResult` describes +the observed execution/outcome, and a `TradeRecord` is its durable snapshot. + +## Lifecycle Invariants + +The proposed logical intent lifecycle is: + +```text +PROPOSED -> REJECTED + -> ADMITTED -> SUBMITTING -> EXECUTED -> SETTLED + \-> UNKNOWN + -> CANCELLED +``` + +`UNKNOWN` is not `CANCELLED`. It covers a request that may have reached the +broker but whose acceptance is not proven. It requires reconciliation before a +retry or release of reserved risk. Terminal delivery is idempotent by +`intent_id`, `execution_group_id`, `trade_id` and a settlement/event identity; +duplicate callbacks must not advance a series twice. + +The state source of truth is durable operation state. Event delivery is a +notification mechanism and cannot be required to reach every upstream module +for correctness. RAII manages subscriptions and runtime resources; it does +not replace persistence or recovery after a process crash. + +## Branching and Execution Cardinality + +Branching is allowed when the intent declares its cardinality: + +| Policy | Meaning | +| --- | --- | +| `ZERO_OR_ONE` | One physical execution or an explicit rejection. | +| `EXACTLY_ONE` | Exactly one eligible alternative must be selected. | +| `MULTIPLE` | Several independent physical executions are allowed. | +| `SPLIT_GROUP` | Several physical trades form one logical execution group. | + +Series progression consumes the aggregate logical outcome, not an arbitrary +branch callback. A split group remains non-terminal while any member is +unknown. For a settled group, aggregation is explicit, normally by total net +profit rather than by the count of wins. + +## Contexts + +Trading decisions read a single immutable decision snapshot composed of small +contexts: + +- signal context; +- account context, including freshness and availability; +- market context, including ticks/bars and their provenance; +- calendar/news context; +- portfolio context; +- execution context, including reservations and active intents. + +These contexts are not one universal datastore. Account values must distinguish +`SUPPORTED`, `UNAVAILABLE`, `STALE` and `UNKNOWN`; a zero default is not a safe +substitute. News is a calendar context, not a market tick. Historical/replay +consumers must not observe facts before their publication time. + +For market data, prefer an immutable provider-owned snapshot held through a +shared lifetime when asynchronous modules need to retain it. A borrowed view is +valid only while that snapshot is alive. Do not expose a raw `span` whose owner +can disappear after a callback. Existing `TickSequence` and routed batches +remain the source of truth until a dedicated snapshot contract is justified. + +## Module Model + +The first design uses small capability interfaces rather than one mandatory +`ITradingModule` with every possible method: + +- signal/event consumers; +- stateless filters; +- position sizing policies; +- series progression policies; +- risk admission; +- execution adapters; +- settlement/recovery participants. + +Modules normally run on the existing owner loop and task system. A separate +worker is reserved for genuinely long or latency-sensitive computation. The +final risk admission and reservation still happen through the common owner of +account/risk state. + +## ADELIA and MegaConnector Adapter Boundary + +The host may map a graph node to one or more small OptionX capabilities and map +typed graph edges to `TradeIntent` or lifecycle events. The host owns graph +topology, configuration, UI and node scheduling. OptionX owns trading-domain +contracts and broker adapters. No OptionX type should mention a graph node, +graph editor, ADELIA, ImNodeFlow or MegaConnector. + +The adapter must preserve identities, cardinality, cancellation semantics and +unknown execution state. It must not convert a rejected intent into a loss or +turn a missing broker response into a cancellation. diff --git a/guides/TRADING_SYSTEM_CONTRACTS.md b/guides/TRADING_SYSTEM_CONTRACTS.md new file mode 100644 index 0000000..7472c88 --- /dev/null +++ b/guides/TRADING_SYSTEM_CONTRACTS.md @@ -0,0 +1,188 @@ +# Trading System Contracts + +## Status + +These are proposed C++17 contracts for review. They are intentionally small +and header-only in spirit. Names and field types may change in the first +implementation PR; the invariants are the important part. + +## Stable IDs and States + +```cpp +using IntentId = std::uint64_t; +using SeriesId = std::uint64_t; +using ExecutionGroupId = std::uint64_t; +using SettlementId = std::uint64_t; + +enum class IntentStatus { + PROPOSED, + REJECTED, + ADMITTED, + SUBMITTING, + EXECUTION_UNKNOWN, + EXECUTED, + SETTLED, + CANCELLED +}; + +enum class ExecutionCardinality { + ZERO_OR_ONE, + EXACTLY_ONE, + MULTIPLE, + SPLIT_GROUP +}; +``` + +State transitions must be validated against the current revision and an +idempotency key. A repeated terminal event is a no-op. An out-of-date revision +cannot silently overwrite a newer series state. + +## Signal, Intent and Execution + +```cpp +struct TradeIntent { + IntentId intent_id = 0; + SignalId origin_signal_id = 0; + SeriesId series_id = 0; + std::int32_t series_step = -1; + ExecutionGroupId execution_group_id = 0; + ExecutionCardinality cardinality = ExecutionCardinality::ZERO_OR_ONE; + IntentStatus status = IntentStatus::PROPOSED; + TradeRequest request; + std::uint64_t state_revision = 0; +}; + +struct TradeOutcome { + IntentId intent_id = 0; + ExecutionGroupId execution_group_id = 0; + SettlementId settlement_id = 0; + TradeState state = TradeState::UNKNOWN; + double net_profit = 0.0; + bool final = false; +}; +``` + +This is a contract sketch, not a request to add these exact structs before the +first vertical slice is tested. `TradeSignal` remains immutable. A physical +`TradeRequest` still flows through the existing platform API and keeps the +existing persistent `trade_id`. + +## Decision Context + +```cpp +enum class DataAvailability { SUPPORTED, UNAVAILABLE, STALE, UNKNOWN }; + +struct AccountContext { + DataAvailability availability = DataAvailability::UNKNOWN; + std::uint64_t captured_at_ms = 0; + std::uint64_t revision = 0; + // Immutable account/balance/payout values. +}; + +struct TradingDecisionContext { + const TradeSignal* signal = nullptr; + const AccountContext* account = nullptr; + const MarketSnapshot* market = nullptr; + const CalendarSnapshot* calendar = nullptr; + const PortfolioSnapshot* portfolio = nullptr; + const ExecutionSnapshot* execution = nullptr; +}; +``` + +The final implementation may replace raw pointers with owned immutable +snapshot handles. A pointer in this sketch is never valid beyond the lifetime +of the context call. Asynchronous modules must retain a shared snapshot, not a +borrowed pointer or a `span` over mutable storage. + +## Small Module Interfaces + +The intended capabilities are separate: + +```cpp +struct IntentDecision { + bool accepted = false; + std::string reason; + std::optional intent; +}; + +class IIntentFilter { +public: + virtual ~IIntentFilter() = default; + virtual IntentDecision evaluate(const TradingDecisionContext&) = 0; +}; + +class IPositionSizer { +public: + virtual ~IPositionSizer() = default; + virtual std::optional size(const TradingDecisionContext&) = 0; +}; + +class ISeriesProgression { +public: + virtual ~ISeriesProgression() = default; + virtual void apply_outcome(const TradeOutcome&, std::uint64_t expected_revision) = 0; +}; +``` + +Active/event-driven modules may consume ticks, bars, calendar events or +settlement events and emit intents. They do not need a private thread by +default. The existing task manager and lifecycle stack remain the execution +mechanism. + +## Risk Admission + +Risk admission is the final gate immediately before submission: + +```cpp +struct RiskReservation { + std::uint64_t reservation_id = 0; + bool valid = false; +}; + +class IRiskAdmission { +public: + virtual ~IRiskAdmission() = default; + virtual std::optional reserve( + const TradeIntent&, const TradingDecisionContext&) = 0; + virtual bool commit(const RiskReservation&, const TradeRequest&) = 0; + virtual void release(const RiskReservation&) = 0; + virtual void mark_unknown(const RiskReservation&) = 0; +}; +``` + +Two strategies must not both reserve against the same stale balance snapshot. +`mark_unknown` retains the reservation until reconciliation; it is not an +alias for `release`. + +## Execution Groups + +For `SPLIT_GROUP`, all physical members share an `execution_group_id`. The +group is final only when every required member is settled or the group policy +explicitly records a terminal failure. Aggregate outcome is defined by policy, +normally total confirmed net P&L. One unknown member keeps the group pending. + +## Persistence and Recovery + +The persistence adapter must support: + +- create/update intent with monotonic revision; +- create/update series state; +- create execution group and physical trade links; +- append or idempotently apply settlement identity; +- find unresolved `SUBMITTING`/`EXECUTION_UNKNOWN` operations; +- reconcile broker identity before retry; +- restore state before accepting new series progression. + +`SignalRecordDB` and `TradeRecordDB` remain adapters and sources of existing +signal/trade facts. They should not be silently treated as a complete journal. +The first implementation can use a dedicated table or a carefully scoped +extension, but it must not hide logical intent state in free-form comments. + +## Cancellation Rules + +- Before submission: transition to `CANCELLED` or `REJECTED`, release the + reservation, and emit one terminal event. +- During submission: record the operation as `SUBMITTING`; cancellation cannot + claim that the broker did not receive it. +- After transport ambiguity: transition to `EXECUTION_UNKNOWN`, retain risk, + reconcile by broker identity/history, and only then settle or cancel. diff --git a/guides/TRADING_SYSTEM_IMPLEMENTATION_PLAN.md b/guides/TRADING_SYSTEM_IMPLEMENTATION_PLAN.md new file mode 100644 index 0000000..5b1ec62 --- /dev/null +++ b/guides/TRADING_SYSTEM_IMPLEMENTATION_PLAN.md @@ -0,0 +1,128 @@ +# Trading System Implementation Plan + +## Delivery Rule + +The new domain is delivered as small, testable batches. The current batch is +the architecture/reuse audit only. It adds no production trading runtime and +does not open live trades. Each later batch must preserve the existing broker, +Router, task and storage contracts. + +## Batch 0: Foundation Audit + +Deliverables: + +- architecture boundary and adapter rules; +- reuse matrix for existing DTOs, platform APIs, Router and storage; +- proposed lifecycle, context, branching and risk contracts; +- staged implementation plan. + +Acceptance: + +- no dependency on ADELIA or MegaConnector in OptionX; +- no second scheduler, event bus, graph runtime or storage engine; +- unresolved execution is explicitly different from cancellation; +- existing `trade_id` and broker identity paths are preserved. + +## Batch 1: Identity and Pure Domain DTOs + +Add the smallest tested representation for `TradeIntent`, `TradeSeries` and +execution-group identity. Keep `TradeSignal`, `TradeRequest`, `TradeResult` and +`TradeRecord` source compatibility where possible. + +Tests must cover cloning, serialization compatibility, invalid IDs, revision +checks and the four cardinality policies. No broker calls are involved. + +## Batch 2: Stateless Policies + +Implement pure filters and sizing policies first: + +- symbol/source filter; +- trading-window filter; +- fixed stake; +- percent-of-balance sizing with explicit unavailable/stale behavior. + +Every policy consumes a deterministic context and returns a typed reason on +rejection. Existing MM enums and parameter interfaces are reused; formula +implementations are not copied into broker managers. + +## Batch 3: Series and Idempotent Progression + +Implement a minimal `TradeSeries` state machine and progression policy. The +first scenario must prove: + +- a rejected intent does not count as a loss; +- a duplicate settlement does not advance twice; +- a revision conflict is rejected; +- a split group waits for every required member; +- an unknown member blocks progression. + +Martingale is a later policy test, not a reason to weaken the lifecycle. + +## Batch 4: Risk Admission + +Add a single owner for reservations and a fake/simulated account context. +Prove that two concurrent candidates cannot reserve the same available risk. +Prove that rejected-before-submit releases risk while unknown execution keeps +it reserved. Do not couple this batch to Intrade HTTP details. + +## Batch 5: Persistence and Recovery + +Extend existing storage through an explicit adapter or narrowly scoped tables. +The recovery test must simulate a crash at each boundary: + +1. before admission; +2. after reservation; +3. after request persistence and before broker submission; +4. after submission and before broker ID; +5. after broker ID and before result; +6. after result before settlement application. + +Restart must find unresolved operations, reconcile them, and apply a settlement +identity at most once. No automatic retry is permitted for unresolved broker +submission until reconciliation says it is safe. + +## Batch 6: Synthetic Vertical Slice + +The first end-to-end test is: + +```text +SyntheticSignal + -> SymbolFilter + -> TimeFilter + -> FixedStake + -> RiskAdmission + -> SimulatedExecution + -> TradeOutcome + -> PersistentState +``` + +The same slice then adds one martingale step, one filter rejection and a +restart/recovery cycle. It must run as ordinary C++ with no GUI, graph engine, +real account or broker network. + +## Batch 7: Host and Broker Adapters + +Only after the synthetic slice is stable: + +- map ADELIA/MegaConnector nodes to small OptionX capabilities; +- connect existing platform execution through the intent/group adapter; +- preserve owner-loop dispatch and staged shutdown; +- add broker-specific reconciliation paths where the broker provides enough + identity to make them safe. + +This batch must not move strategy formulas into `IntradeBarPlatform`. + +## Review Gates + +Every batch requires: + +- focused unit/regression tests; +- a compile consumer through supported aggregate headers; +- an explicit negative test for cancellation, unknown result and duplicate + delivery where relevant; +- no secret or live-account material in fixtures; +- a diff review showing that unrelated user changes were preserved. + +The first production PR after this audit should therefore be Batch 1 only. It +should not include Kelly, Labouchere, news providers, GUI integration or a +graph runtime. diff --git a/guides/TRADING_SYSTEM_REUSE_AUDIT.md b/guides/TRADING_SYSTEM_REUSE_AUDIT.md new file mode 100644 index 0000000..8fa74c5 --- /dev/null +++ b/guides/TRADING_SYSTEM_REUSE_AUDIT.md @@ -0,0 +1,69 @@ +# Trading System Reuse Audit + +## Scope + +This audit was performed before adding a new trading-system runtime. The goal +is to reuse the existing OptionX contracts and identify only the missing +pieces. The result is an implementation map, not a claim that the proposed +domain already exists. + +## Inventory + +| Existing component | Decision | Findings and boundary | +| --- | --- | --- | +| `data/trading/TradeSignal.hpp` | EXTEND | Already carries source identity, symbol, trade parameters, MM type/step/group and decision/MM parameter objects. Keep it immutable as the source event; do not overload it with branch state. | +| `data/trading/TradeRequest.hpp` | REUSE + EXTEND | Correct broker-facing DTO and callback snapshot. It already carries `trade_id`, `signal_id`, account and execution fields. Add intent/group correlation only when the contract is approved. | +| `data/trading/TradeResult.hpp` | REUSE + EXTEND | Contains broker identities, prices, P&L, balances, timestamps and `TradeState`. It does not by itself model an unresolved submission or idempotent settlement event. | +| `data/trading/TradeRecord.hpp` | REUSE | Durable trade snapshot, broker identity matching and conversion from signal/request/result already exist. It is not a journal of every intent transition. | +| `data/trading/SignalRecord.hpp` | REUSE + EXTEND | Durable signal-to-trade linkage exists and can support source recovery. It has no general series/intent journal or revision protocol. | +| `data/trading/enums.hpp` and MM parameter interfaces | REUSE | `MmSystemType` already includes fixed, percent, Kelly, martingale, anti-martingale, Labouchere and related modes. Do not create parallel money-management enums. Pure policy adapters can be added around these types. | +| `platforms/common/BaseTradingApi.hpp` | REUSE | Existing callbacks cover place, trade ID, result and history operations. It is the broker boundary, not the trading-system policy boundary. | +| `platforms/common/BaseTradingPlatform.hpp` | REUSE | Owns platform lifecycle, owner-loop dispatch and public callbacks. Trading-system modules should schedule through existing task/lifecycle contracts. | +| `components/.../AccountInfoProvider.hpp` | EXTEND | Provides account information by account, symbol, option and order. A decision layer still needs an immutable, freshness-aware account snapshot. | +| `market_data/` and Router | REUSE | Provider selection, subscription ownership, history continuity and source metadata already exist. The trading system should consume snapshots/batches and not reimplement routing. | +| `utils/tasks`, `utils/pubsub`, `lifecycle.hpp` | REUSE | TaskManager, EventBus/EventMediator and LifecycleStack already define scheduling and staged shutdown. No new scheduler or bus is needed. | +| `storages/SignalRecordDB.hpp` | EXTEND | Async/synchronous queueing, signal ID, trade ID and timestamp lookup are available. Add a dedicated operation/series persistence adapter rather than hiding new state in signal fields. | +| `storages/TradeRecordDB.hpp` | EXTEND | Persistent linear trade IDs, UID/broker identity indices, upsert and recovery queries exist. It can store physical trade snapshots but not all logical intent/group transitions. | +| MDBX dependency | REUSE | Existing storage is the persistence engine. A second storage engine would split recovery semantics. | +| ADELIA/MegaConnector concepts | NOT_NEEDED | They belong to the host/adapter layer and must not become OptionX dependencies. | + +## Gaps That Are Real + +1. A durable `intent_id`, `series_id`, `execution_group_id` and revision model is + missing from the broker-facing DTOs. +2. There is no common typed lifecycle for rejected, cancelled, admitted, + submitted, unknown, executed and settled logical intents. +3. There is no atomic risk admission/reservation owner shared by concurrent + strategies. +4. `TradeRecordDB` and `SignalRecordDB` do not constitute an append-only + lifecycle journal or an idempotent settlement ledger. +5. Account data is available through providers, but a decision snapshot needs + freshness/availability metadata and a coherent capture boundary. +6. There is no generic split-group aggregator or explicit execution-cardinality + contract. + +## Existing Recovery Strengths + +The current code already supports useful parts of recovery: + +- a persistent `trade_id` can be allocated before broker submission; +- `TradeRequest -> TradeResult -> TradeRecord` can preserve that identity; +- trade records can be found by persistent ID, user UID and broker identity; +- signal records can link a signal to produced trade IDs; +- queued storage work has explicit `process`, `flush` and `shutdown` behavior. + +The dangerous gap is the crash after submission and before broker ID/result. +The correct state is `UNKNOWN`, not a fresh retry. A future adapter must keep +that record visible until broker reconciliation proves the outcome. + +## Reuse Rules + +- Keep policy code independent of platform-specific HTTP and broker response + formats. +- Keep one owner for mutable series and reservation state. +- Use immutable snapshots for cross-module reads. +- Carry existing `trade_id` through execution; do not make `unique_id` a + replacement for the persistent database identity. +- Use existing lifecycle/task/pub-sub facilities. +- Add persistence only after the state machine and idempotency keys are + specified.