RayMelius Claude Opus 4.6 commited on
Commit
8f4d2d0
Β·
0 Parent(s):

Initial commit: C++ actor-based matching engine (Optiq architecture)

Browse files

Port of StockEx Python prototype to C++ with Simplx-compatible actor framework.
Includes price-time priority OrderBook, OEGateway/OrderBook/MarketData actors,
Recovery Cause/Effect (Master/Mirror HA), IACA fragment chain aggregation,
and 20 passing tests.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

.gitignore ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Build
2
+ build/
3
+ cmake-build-*/
4
+ out/
5
+ *.obj
6
+ *.lib
7
+ *.exe
8
+ *.pdb
9
+ *.ilk
10
+ *.exp
11
+
12
+ # IDE
13
+ .vs/
14
+ .vscode/
15
+ *.suo
16
+ *.user
17
+ *.sln
18
+ *.vcxproj*
19
+ CMakeSettings.json
20
+
21
+ # OS
22
+ Thumbs.db
23
+ Desktop.ini
24
+ .DS_Store
25
+
26
+ # Claude
27
+ .claude/
CMakeLists.txt ADDED
@@ -0,0 +1,75 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ cmake_minimum_required(VERSION 3.16)
2
+ project(EuNEx VERSION 0.1.0 LANGUAGES CXX)
3
+
4
+ set(CMAKE_CXX_STANDARD 20)
5
+ set(CMAKE_CXX_STANDARD_REQUIRED ON)
6
+ set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
7
+
8
+ # ── Options ──────────────────────────────────────────────────────────
9
+ option(EUNEX_USE_SIMPLX "Build with real Simplx framework (OFF = embedded shim)" OFF)
10
+ option(EUNEX_BUILD_TESTS "Build test binaries" ON)
11
+ option(EUNEX_BUILD_EXAMPLES "Build example binaries" ON)
12
+
13
+ # ── Simplx dependency ───────────────────────────────────────────────
14
+ if(EUNEX_USE_SIMPLX)
15
+ # When you have the real Simplx checked out:
16
+ # cmake -DEUNEX_USE_SIMPLX=ON -DSIMPLX_ROOT=/path/to/simplx ..
17
+ find_path(SIMPLX_INCLUDE_DIR simplx.h
18
+ HINTS ${SIMPLX_ROOT}/include /usr/local/include)
19
+ if(NOT SIMPLX_INCLUDE_DIR)
20
+ message(FATAL_ERROR "Simplx headers not found. Set SIMPLX_ROOT.")
21
+ endif()
22
+ include_directories(${SIMPLX_INCLUDE_DIR})
23
+ add_compile_definitions(EUNEX_REAL_SIMPLX=1)
24
+ else()
25
+ # Use our lightweight Simplx-compatible shim for development
26
+ include_directories(${CMAKE_SOURCE_DIR}/src/engine)
27
+ endif()
28
+
29
+ include_directories(${CMAKE_SOURCE_DIR}/src)
30
+
31
+ # ── Core library ────────────────────────────────────────────────────
32
+ add_library(eunex_core STATIC
33
+ src/common/Types.hpp
34
+ src/common/OrderBook.hpp
35
+ src/common/OrderBook.cpp
36
+ src/engine/SimplxShim.hpp
37
+ src/actors/OrderBookActor.hpp
38
+ src/actors/OrderBookActor.cpp
39
+ src/actors/OEGatewayActor.hpp
40
+ src/actors/OEGatewayActor.cpp
41
+ src/actors/MarketDataActor.hpp
42
+ src/actors/MarketDataActor.cpp
43
+ src/recovery/RecoveryProxy.hpp
44
+ src/recovery/RecoveryProxy.cpp
45
+ src/iaca/Fragment.hpp
46
+ src/iaca/IacaAggregator.hpp
47
+ src/iaca/IacaAggregator.cpp
48
+ )
49
+
50
+ target_include_directories(eunex_core PUBLIC ${CMAKE_SOURCE_DIR}/src)
51
+
52
+ # ── Main executable ─────────────────────────────────────────────────
53
+ add_executable(eunex_me src/main.cpp)
54
+ target_link_libraries(eunex_me PRIVATE eunex_core)
55
+
56
+ # ── Examples ────────────────────────────────────────────────────────
57
+ if(EUNEX_BUILD_EXAMPLES)
58
+ add_executable(ping_pong examples/ping_pong.cpp)
59
+ target_link_libraries(ping_pong PRIVATE eunex_core)
60
+
61
+ add_executable(simple_match examples/simple_match.cpp)
62
+ target_link_libraries(simple_match PRIVATE eunex_core)
63
+ endif()
64
+
65
+ # ── Tests ───────────────────────────────────────────────────────────
66
+ if(EUNEX_BUILD_TESTS)
67
+ enable_testing()
68
+ add_executable(test_orderbook tests/test_orderbook.cpp)
69
+ target_link_libraries(test_orderbook PRIVATE eunex_core)
70
+ add_test(NAME OrderBookTest COMMAND test_orderbook)
71
+
72
+ add_executable(test_matching_engine tests/test_matching_engine.cpp)
73
+ target_link_libraries(test_matching_engine PRIVATE eunex_core)
74
+ add_test(NAME MatchingEngineTest COMMAND test_matching_engine)
75
+ endif()
README.md ADDED
@@ -0,0 +1,118 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # EuNEx β€” Euronext Optiq Architecture Learning Project
2
+
3
+ C++ actor-based matching engine that mirrors the Euronext Optiq architecture,
4
+ ported from the [StockEx](https://github.com/Bonum/StockEx) Python prototype.
5
+
6
+ ## Architecture Mapping
7
+
8
+ ```
9
+ StockEx (Python/Kafka) EuNEx (C++/Simplx) Optiq (Production)
10
+ ───────────────────── ────────────────── ──────────────────
11
+ fix_oeg_server.py β†’ OEGatewayActor β†’ OEActor
12
+ Kafka 'orders' topic β†’ Event::Pipe β†’ Simplx Event::Pipe
13
+ matcher.py β†’ OrderBookActor β†’ LogicalCoreActor + Book
14
+ match_order() β†’ OrderBook::newOrder() β†’ RecoveryCause β†’ IACA Cause β†’ forwardToBook
15
+ handle_cancel() β†’ OrderBook::cancelOrder()β†’ CancelOrderData
16
+ handle_amend() β†’ OrderBook::modifyOrder()β†’ ModifyOrderData
17
+ Kafka 'trades' topic β†’ TradeEvent via Pipe β†’ IACA fragment chain
18
+ dashboard.py (SSE) β†’ MarketDataActor β†’ MDLimitLogicalCoreHandler
19
+ /orderbook/<sym> β†’ BookUpdateEvent β†’ PublishLimitUpdateRequest
20
+ /trades β†’ TradeEvent β†’ IACA β†’ IA SBE message
21
+ database.py (SQLite) β†’ RecoveryProxy (memory) β†’ RecoveryProxy β†’ Kafka
22
+ save_trade() β†’ FragmentStore::append() β†’ PersistenceAgent β†’ Kafka produce
23
+ β†’ IATCRecovery SBE envelope
24
+ (none) β†’ IacaAggregator β†’ IacaAggregatorActor
25
+ Fragment chains β†’ CoherentFragmentChain
26
+ FragmentHandler β†’ FastNewOrderHandler etc.
27
+ ```
28
+
29
+ ## Actor Topology
30
+
31
+ ```
32
+ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
33
+ β”‚ OEGatewayActor β”‚ ← External orders in
34
+ β”‚ (Core 0) β”‚ ← Execution reports out
35
+ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
36
+ β”‚ NewOrderEvent / CancelOrderEvent
37
+ β–Ό
38
+ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
39
+ β”‚ OrderBookActor(s) β”‚ ← One per symbol
40
+ β”‚ (Core 1) β”‚ ← Price-time matching
41
+ β””β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
42
+ β”‚ β”‚
43
+ ExecReport β”‚ β”‚ TradeEvent + BookUpdateEvent
44
+ β–Ό β–Ό
45
+ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
46
+ β”‚ OEGatewayActor β”‚ β”‚ MarketDataActor β”‚
47
+ β”‚ (ack back) β”‚ β”‚ (Core 2) β”‚
48
+ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
49
+ ```
50
+
51
+ ## Build & Run
52
+
53
+ ```bash
54
+ # Build (shim mode β€” no Simplx dependency)
55
+ mkdir build && cd build
56
+ cmake ..
57
+ cmake --build .
58
+
59
+ # Run matching engine demo
60
+ ./eunex_me
61
+
62
+ # Run tests
63
+ ctest
64
+ # or individually:
65
+ ./test_orderbook
66
+ ./test_matching_engine
67
+
68
+ # Run examples
69
+ ./ping_pong
70
+ ./simple_match
71
+ ```
72
+
73
+ ## With Real Simplx
74
+
75
+ ```bash
76
+ git clone https://github.com/Tredzone/simplx.git /path/to/simplx
77
+ cmake -DEUNEX_USE_SIMPLX=ON -DSIMPLX_ROOT=/path/to/simplx ..
78
+ ```
79
+
80
+ ## Project Structure
81
+
82
+ ```
83
+ EuNEx/
84
+ β”œβ”€β”€ src/
85
+ β”‚ β”œβ”€β”€ main.cpp # Entry point β€” wires actors together
86
+ β”‚ β”œβ”€β”€ engine/
87
+ β”‚ β”‚ └── SimplxShim.hpp # Simplx-compatible API shim
88
+ β”‚ β”œβ”€β”€ common/
89
+ β”‚ β”‚ β”œβ”€β”€ Types.hpp # Price, Order, Trade, enums
90
+ β”‚ β”‚ β”œβ”€β”€ OrderBook.hpp # Price-time priority order book
91
+ β”‚ β”‚ └── OrderBook.cpp
92
+ β”‚ β”œβ”€β”€ actors/
93
+ β”‚ β”‚ β”œβ”€β”€ Events.hpp # All inter-actor event types
94
+ β”‚ β”‚ β”œβ”€β”€ OrderBookActor.hpp/cpp # Matching engine actor
95
+ β”‚ β”‚ β”œβ”€β”€ OEGatewayActor.hpp/cpp # Order entry gateway actor
96
+ β”‚ β”‚ └── MarketDataActor.hpp/cpp # Market data publisher actor
97
+ β”‚ β”œβ”€β”€ recovery/
98
+ β”‚ β”‚ └── RecoveryProxy.hpp/cpp # Recovery Cause/Effect (simplified)
99
+ β”‚ └── iaca/
100
+ β”‚ β”œβ”€β”€ Fragment.hpp # IACA fragment definitions
101
+ β”‚ └── IacaAggregator.hpp/cpp # Fragment chain aggregation
102
+ β”œβ”€β”€ examples/
103
+ β”‚ β”œβ”€β”€ ping_pong.cpp # Actor basics tutorial
104
+ β”‚ └── simple_match.cpp # Matching with Recovery + IACA
105
+ β”œβ”€β”€ tests/
106
+ β”‚ β”œβ”€β”€ test_orderbook.cpp # OrderBook unit tests
107
+ β”‚ └── test_matching_engine.cpp # Actor integration tests
108
+ └── CMakeLists.txt
109
+ ```
110
+
111
+ ## Next Steps
112
+
113
+ 1. **Add real Simplx integration** β€” replace shim with real multi-threaded actors
114
+ 2. **Kafka persistence** β€” replace FragmentStore with actual Kafka produce/consume
115
+ 3. **SBE encoding** β€” replace event structs with SBE-encoded messages
116
+ 4. **FIX gateway** β€” add FIX 4.4 acceptor (replaces fix_oeg_server.py)
117
+ 5. **Master/Mirror failover** β€” implement full Recovery replay on Mirror node
118
+ 6. **Clearing House actors** β€” port AI trading members as actors
docs/process-diagram.md ADDED
@@ -0,0 +1,631 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # EuNEx Process Diagram
2
+
3
+ ## End-to-End Order Flow (Optiq-style)
4
+
5
+ ```
6
+ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
7
+ β”‚ EXTERNAL CLIENTS β”‚
8
+ β”‚ FIX 5.0 SP2 / SBE Binary Protocol β”‚
9
+ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
10
+ β”‚
11
+ β–Ό
12
+ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
13
+ β”‚ OEGatewayActor (Core 0) ← Optiq: OEG/OEActorβ”‚
14
+ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
15
+ β”‚ β”‚ β€’ Accepts NewOrder / Cancel / Modify requests β”‚ β”‚
16
+ β”‚ β”‚ β€’ Validates session, routes by SymbolIndex β†’ correct OrderBookActor β”‚ β”‚
17
+ β”‚ β”‚ β€’ Sends ExecutionReports back to client (ack, fill, reject) β”‚ β”‚
18
+ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
19
+ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
20
+ β”‚ NewOrderEvent / CancelOrderEvent / ModifyOrderEvent
21
+ β”‚ (Simplx Event::Pipe β€” lock-free cross-core delivery)
22
+ β–Ό
23
+ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
24
+ β”‚ OrderBookActor (Core 1) β€” one per symbol ← Optiq: LogicalCoreActor + Bookβ”‚
25
+ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
26
+ β”‚ β”‚ RECOVERY CAUSE β”‚ β”‚
27
+ β”‚ β”‚ RecoveryProxy.cause(persistenceId, order, callback) β”‚ β”‚
28
+ β”‚ β”‚ β€’ Persists incoming event to FragmentStore (β†’ Kafka in production) β”‚ β”‚
29
+ β”‚ β”‚ β€’ Assigns chainId + sequence for IACA tracking β”‚ β”‚
30
+ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
31
+ β”‚ β–Ό β”‚
32
+ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
33
+ β”‚ β”‚ MATCHING ENGINE β”‚ β”‚
34
+ β”‚ β”‚ OrderBook.newOrder() / cancelOrder() / modifyOrder() β”‚ β”‚
35
+ β”‚ β”‚ β€’ Price-time priority matching (std::map<Price, std::deque<Order>>) β”‚ β”‚
36
+ β”‚ β”‚ β€’ FOK: reject if insufficient liquidity β”‚ β”‚
37
+ β”‚ β”‚ β€’ IOC: fill what's available, cancel remainder β”‚ β”‚
38
+ β”‚ β”‚ β€’ Market: match at any price, never rests on book β”‚ β”‚
39
+ β”‚ β”‚ β€’ Multi-level sweep across price levels β”‚ β”‚
40
+ β”‚ β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
41
+ β”‚ β”‚ β”‚ β”‚
42
+ β”‚ β–Ό β–Ό β”‚
43
+ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
44
+ β”‚ β”‚ IACA CAUSE β”‚ β”‚ RECOVERY EFFECT (Master only) β”‚ β”‚
45
+ β”‚ β”‚ IacaFragment: β”‚ β”‚ RecoveryProxy.effect(callback) β”‚ β”‚
46
+ β”‚ β”‚ β€’ BOOK root frag β”‚ β”‚ β€’ Sends ExecReport β†’ OEGateway β”‚ β”‚
47
+ β”‚ β”‚ β€’ ACK child frag β”‚ β”‚ β€’ Sends TradeEvent β†’ MarketData β”‚ β”‚
48
+ β”‚ β”‚ β€’ TRADE child fragβ”‚ β”‚ β€’ Sends BookUpdate β†’ MarketData β”‚ β”‚
49
+ β”‚ β”‚ (nextCount check) β”‚ β”‚ β€’ Mirror skips all effects β”‚ β”‚
50
+ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
51
+ β”‚ β”‚ β”‚ β”‚ β”‚
52
+ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
53
+ β”‚ β”‚ β”‚
54
+ β–Ό β”‚ β”‚
55
+ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚
56
+ β”‚ IacaAggregator β”‚ β”‚ β”‚
57
+ β”‚ ← Optiq: IacaActor β”‚ β”‚ β”‚
58
+ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”‚
59
+ β”‚ β”‚ Collects fragments β”‚ β”‚ β”‚ β”‚
60
+ β”‚ β”‚ per chainId β”‚ β”‚ β”‚ β”‚
61
+ β”‚ β”‚ Detects completion β”‚ β”‚ β”‚ β”‚
62
+ β”‚ β”‚ (sum nextCount == β”‚ β”‚ β”‚ β”‚
63
+ β”‚ β”‚ total fragments -1) β”‚ β”‚ β”‚ β”‚
64
+ β”‚ β”‚ Fires handler β†’ β”‚ β”‚ β”‚ β”‚
65
+ β”‚ β”‚ IA SBE message β”‚ β”‚ β”‚ β”‚
66
+ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β”‚
67
+ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚
68
+ β”‚ IA message β”‚ β”‚
69
+ β–Ό β”‚ β”‚
70
+ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
71
+ β”‚ Future: IDS / PTB / β”‚ β”‚ MarketDataActor (Core 2) β”‚
72
+ β”‚ SATURN / Clearing β”‚ β”‚ ← Optiq: MDLimitLogicalCoreHandler β”‚
73
+ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
74
+ β”‚ β”‚ IDS: Intraday Data β”‚ β”‚ β”‚ β”‚ β€’ Maintains BBO snapshot per sym β”‚ β”‚
75
+ β”‚ β”‚ PTB: Post-Trade Bus β”‚ β”‚ β”‚ β”‚ β€’ Stores recent trades β”‚ β”‚
76
+ β”‚ β”‚ SATURN: ARM reportingβ”‚ β”‚ β”‚ β”‚ β€’ Publishes BookUpdateEvent β”‚ β”‚
77
+ β”‚ β”‚ Clearing: LCH/CC&G β”‚ β”‚ β”‚ β”‚ (bestBid, bestAsk, depths) β”‚ β”‚
78
+ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β”‚ β†’ Future: multicast SBE feed β”‚ β”‚
79
+ β”‚ (not yet implemented) β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
80
+ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
81
+ ```
82
+
83
+ ## Master / Mirror High Availability
84
+
85
+ ```
86
+ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
87
+ β”‚ MASTER NODE β”‚ β”‚ MIRROR NODE β”‚
88
+ β”‚ β”‚ β”‚ β”‚
89
+ β”‚ RecoveryProxy(isMaster=true) β”‚ β”‚ RecoveryProxy(isMaster=false) β”‚
90
+ β”‚ β”‚ β”‚ β”‚
91
+ β”‚ cause() β†’ persist + run callback βœ“ β”‚ β”‚ cause() β†’ persist + callback βœ“β”‚
92
+ β”‚ effect() β†’ execute side-effects βœ“ │────▢│ effect() β†’ SKIP (no-op) βœ— β”‚
93
+ β”‚ recoveryEffect() β†’ SKIP βœ— β”‚ β”‚ recoveryEffect() β†’ execute βœ“ β”‚
94
+ β”‚ β”‚ β”‚ β”‚
95
+ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
96
+ β”‚ β”‚ FragmentStore (shared memory) │────┼─────┼─▢│ Same FragmentStore β”‚ β”‚
97
+ β”‚ β”‚ β†’ Kafka topic in production β”‚ β”‚ β”‚ β”‚ (Kafka consumer replay) β”‚ β”‚
98
+ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
99
+ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
100
+ On failover: Mirror becomes Master,
101
+ replays FragmentStore, resumes effects
102
+ ```
103
+
104
+ ## IACA Fragment Chain Example (Buy order that matches)
105
+
106
+ ```
107
+ Chain #42: BUY 60 @ 50.00 (matches resting SELL 100 @ 50.00)
108
+
109
+ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
110
+ β”‚ Fragment 1 (ROOT) β”‚
111
+ β”‚ origin: BOOK:1:seq β”‚
112
+ β”‚ previousOrigin: null β”‚
113
+ β”‚ causeId: NEW_ORDER_BUY β”‚
114
+ β”‚ nextCount: 2 │──────┐
115
+ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
116
+ β”‚ β”‚
117
+ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ 2 children expected
118
+ β–Ό β–Ό β”‚ 2 children found β†’ COMPLETE
119
+ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
120
+ β”‚ Fragment 2 β”‚ β”‚ Fragment 3 β”‚ β”‚
121
+ β”‚ ACK_DATA β”‚ β”‚ TRADE_DATA β”‚ β”‚
122
+ β”‚ prev: ROOT β”‚ β”‚ prev: ROOT β”‚ β”‚
123
+ β”‚ nextCount: 0β”‚ β”‚ nextCount: 0β”‚ β”‚
124
+ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
125
+ β”‚
126
+ sum(nextCount) = 2 + 0 + 0 = 2 β”€β”€β”€β”€β”€β”˜
127
+ total fragments = 3
128
+ complete when: sum == total - 1 βœ“ (2 == 3-1)
129
+ β–Ό
130
+ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
131
+ β”‚ Handler fires: β”‚
132
+ β”‚ NewOrderHandler β†’ β”‚
133
+ β”‚ Generate IA SBE message β”‚
134
+ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
135
+ ```
136
+
137
+ ## Component Mapping: StockEx β†’ EuNEx β†’ Optiq Production
138
+
139
+ ```
140
+ StockEx (Python/Kafka) EuNEx (C++/Simplx shim) Optiq (Production)
141
+ ═══════════════════════ ═══════════════════════ ══════════════════
142
+
143
+ fix_oeg_server.py ───▢ OEGatewayActor ───▢ OEActor (FIX/SBE)
144
+ Kafka 'orders' topic ───▢ Event::Pipe ───▢ Simplx cross-core
145
+ session mgmt ───▢ submitNewOrder() ───▢ FIX 5.0 SP2 acceptor
146
+
147
+ matcher.py ───▢ OrderBookActor ───▢ LogicalCoreActor
148
+ match_order() ───▢ OrderBook::newOrder() ───▢ RecoveryCauseβ†’IACAβ†’Book
149
+ handle_cancel() ───▢ OrderBook::cancelOrder() ───▢ CancelOrderData
150
+ handle_amend() ───▢ OrderBook::modifyOrder() ───▢ ModifyOrderData
151
+ Kafka 'trades' topic ───▢ TradeEvent via Pipe ───▢ IACA fragment chain
152
+
153
+ dashboard.py (SSE) ───▢ MarketDataActor ───▢ MDLimitLogicalCoreHandler
154
+ /orderbook/<sym> ───▢ BookUpdateEvent ───▢ PublishLimitUpdateRequest
155
+ /trades ───▢ TradeEvent list ───▢ IAβ†’SBE multicast
156
+
157
+ database.py (SQLite) ───▢ RecoveryProxy (memory) ───▢ RecoveryProxy β†’ Kafka
158
+ save_trade() ───▢ FragmentStore::append() ───▢ PersistenceAgent produce
159
+
160
+ (none) ───▢ IacaAggregator ───▢ IacaAggregatorActor
161
+ ───▢ FragmentChain ───▢ CoherentFragmentChain
162
+ ───▢ FragmentHandler ───▢ FastNewOrderHandler
163
+
164
+ (none) ───▢ (future) ───▢ SATURN/ARM
165
+ (none) ───▢ (future) ───▢ IDS (Intraday Data)
166
+ (none) ───▢ (future) ───▢ PTB (Post-Trade Bus)
167
+ (none) ───▢ (future) ───▢ Clearing (LCH/CC&G)
168
+ ```
169
+
170
+ ## Data Flow Summary
171
+
172
+ ```
173
+ 1. Client ──FIX/SBE──▢ OEGatewayActor ──Event::Pipe──▢ OrderBookActor
174
+ β–² β”‚
175
+ β”‚ β”œβ”€ RecoveryProxy.cause() β†’ FragmentStore
176
+ β”‚ β”œβ”€ OrderBook.match()
177
+ β”‚ β”œβ”€ IACA fragments β†’ IacaAggregator
178
+ β”‚ β”‚
179
+ ExecReport β—€β”€β”˜ β”œβ”€ effect() [Master only]:
180
+ β”‚ β”œβ”€ ExecReport β†’ OEGateway β†’ Client
181
+ β”‚ β”œβ”€ TradeEvent β†’ MarketDataActor
182
+ β”‚ └─ BookUpdate β†’ MarketDataActor
183
+ β”‚
184
+ └─ IacaAggregator
185
+ └─ complete chain β†’ IA message
186
+ └─ (future) IDS/PTB/SATURN
187
+ ```
188
+
189
+ ---
190
+
191
+ ## Future Expansion: Full Optiq Production Topology
192
+
193
+ The diagram below shows the complete Optiq production trade path (from `Trades paths.drawio`
194
+ and the Optiq Overview/SATURN presentations) with EuNEx components mapped where they exist
195
+ and `[FUTURE]` markers where implementation is needed.
196
+
197
+ ```
198
+ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
199
+ β”‚ TRADING MEMBERS (Clients) β”‚
200
+ β”‚ FIX 5.0 SP2 / SBE / TCS OTC / SATURN Web / EMS / DAS β”‚
201
+ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
202
+ β”‚ β”‚ β”‚ β”‚
203
+ β–Ό β–Ό β–Ό β–Ό
204
+ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
205
+ β”‚ OEG.n.BOTH β”‚ β”‚ TCS.IN/OUT β”‚ β”‚ SATURN Web/API β”‚ β”‚ EMS / DAS β”‚
206
+ β”‚ (OEGateway β”‚ β”‚ [FUTURE] β”‚ β”‚ [FUTURE] β”‚ β”‚ [FUTURE] β”‚
207
+ β”‚ Actor) βœ“ β”‚ β”‚ Trade β”‚ β”‚ External trade β”‚ β”‚ Execution β”‚
208
+ β”‚ β”‚ β”‚ Capture β”‚ β”‚ declarations β”‚ β”‚ Mgmt System β”‚
209
+ β””β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ Service β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
210
+ β”‚ β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
211
+ β”‚ ord β”‚ Trade Decl β”‚
212
+ β–Ό β–Ό β”‚
213
+ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
214
+ β”‚ ME / Trading Chain β”‚
215
+ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
216
+ β”‚ β”‚ Book / LogCore#n (OrderBookActor) βœ“ β”‚ β”‚
217
+ β”‚ β”‚ β”‚ β”‚
218
+ β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚
219
+ β”‚ β”‚ β”‚ Recovery Cause β”‚ β”‚ Matching Engine β”‚ β”‚ IACA Inside β”‚ β”‚ β”‚
220
+ β”‚ β”‚ β”‚ (RecoveryProxy β”‚ β”‚ (OrderBook) βœ“ β”‚ β”‚ (IacaAggregator β”‚ β”‚ β”‚
221
+ β”‚ β”‚ β”‚ β†’ Kafka) βœ“ β”‚ β”‚ β”‚ β”‚ partial) βœ“ β”‚ β”‚ β”‚
222
+ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚
223
+ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚
224
+ β”‚ β”‚ β–Ό β–Ό β–Ό β”‚ β”‚
225
+ β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚
226
+ β”‚ β”‚ β”‚ Kafka Bus (KFK) [FUTURE β€” currently in-memory] β”‚ β”‚ β”‚
227
+ β”‚ β”‚ β””β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚
228
+ β”‚ β””β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
229
+ β””β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
230
+ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚
231
+ β–Ό β–Ό β–Ό β–Ό β–Ό β–Ό β–Ό β–Ό
232
+ ```
233
+
234
+ ### Downstream Consumers from Kafka Bus
235
+
236
+ ```
237
+ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ Kafka Topics ──────────────────────────────────────────────────────┐
238
+ β”‚ recovery+audit trail β”‚ MktUpdt,FullTradeInfo β”‚ Trade Fill β”‚ IACA fragments β”‚
239
+ β””β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
240
+ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚
241
+ β–Ό β–Ό β”‚ β”‚ β”‚ β”‚ β–Ό
242
+ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
243
+ β”‚ PE DB β”‚ β”‚ MDG β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ IACA FINISH β”‚
244
+ β”‚ [FUTURE] β”‚ β”‚(MarketData β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ [FUTURE] β”‚
245
+ β”‚ Persistence β”‚ β”‚ Actor) βœ“ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ Completes chains β”‚
246
+ β”‚ Engine DB β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β†’ IA SBE messagesβ”‚
247
+ β”‚ (order state β”‚ β”‚ Multicast β”‚ β”‚ β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
248
+ β”‚ snapshots) β”‚ β”‚ SBE feed β”‚ β”‚ β”‚ β”‚ β”‚ β”‚
249
+ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ [FUTURE] β”‚ β”‚ β”‚ β”‚ β”‚ β–Ό
250
+ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
251
+ β–Ό β”‚ β”‚ β”‚ β”‚ IACA COPY β”‚
252
+ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”‚ β”‚ [FUTURE] β”‚
253
+ β”‚ IDS β”‚ β”‚ β”‚ β”‚ β”‚ Copy to members β”‚
254
+ β”‚ [FUTURE] β”‚ β”‚ β”‚ β”‚ β”‚ (DropCopy/DCG) β”‚
255
+ β”‚ Intraday Data β”‚ β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
256
+ β”‚ Service β”‚ β”‚ β”‚ β”‚
257
+ β”‚ (files to β”‚ β”‚ β”‚ β”‚
258
+ β”‚ regulators) β”‚ β”‚ β”‚ β”‚
259
+ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β”‚
260
+ β–Ό β”‚ β”‚
261
+ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚
262
+ β”‚ PTB β”‚ β”‚ β”‚
263
+ β”‚ [FUTURE] β”‚ β”‚ β”‚
264
+ β”‚ Post-Trade Box β”‚ β”‚ β”‚
265
+ β”‚ (admin, β”‚ β”‚ β”‚
266
+ β”‚ resilience) β”‚ β”‚ β”‚
267
+ β””β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚
268
+ β”‚ β”‚ β”‚
269
+ β–Ό β–Ό β–Ό
270
+ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
271
+ β”‚ CLEARING [FUTURE] β”‚
272
+ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
273
+ β”‚ β”‚ EuroCCP β”‚ β”‚ LCH Ltd β”‚ β”‚ SIX Xclear β”‚ β”‚
274
+ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
275
+ β”‚ Enxt Clearing ← PTB2EC β”‚
276
+ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
277
+ ```
278
+
279
+ ---
280
+
281
+ ## Future Expansion: Trading Phases & Order Types
282
+
283
+ Based on the Euronext Trading Manual (Notice 4-01, Dec 2025).
284
+
285
+ ### Trading Day Phases (not yet implemented in EuNEx)
286
+
287
+ ```
288
+ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
289
+ β”‚ CONTINUOUS TRADING DAY LIFECYCLE β”‚
290
+ β”‚ β”‚
291
+ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
292
+ β”‚ β”‚ Pre-opening β”‚ β”‚ Opening β”‚ β”‚ Main β”‚ β”‚Pre-close β”‚ β”‚Closing β”‚ β”‚
293
+ β”‚ β”‚ Call Phase │──▢│Uncrossing│──▢│ Trading │──▢│Call Phase│──▢│Uncross β”‚ β”‚
294
+ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ Session β”‚ β”‚ β”‚ β”‚ β”‚ β”‚
295
+ β”‚ β”‚ Orders β”‚ β”‚ Book β”‚ β”‚ Continuous β”‚ β”‚ Orders β”‚ β”‚ Max β”‚ β”‚
296
+ β”‚ β”‚ accumulate, β”‚ β”‚ frozen, β”‚ β”‚ matching, β”‚ β”‚ accum., β”‚ β”‚ exec. β”‚ β”‚
297
+ β”‚ β”‚ IOP calc'd β”‚ β”‚ IOP β†’ β”‚ β”‚ price-time β”‚ β”‚ no β”‚ β”‚ price β”‚ β”‚
298
+ β”‚ β”‚ continuouslyβ”‚ β”‚ opening β”‚ β”‚ priority β”‚ β”‚ trades β”‚ β”‚ algo β”‚ β”‚
299
+ β”‚ β”‚ β”‚ β”‚ price β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚
300
+ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”¬β”€β”€β”€β”€β”˜ β”‚
301
+ β”‚ β”‚ β”‚
302
+ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚
303
+ β”‚ β”‚ AVD Orders β”‚ β”‚ Trading-at-Last β”‚β—€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
304
+ β”‚ β”‚ [FUTURE] β”‚ β”‚ [FUTURE] β”‚ β”‚
305
+ β”‚ β”‚ Auction Volume β”‚ β”‚ Trade only at TAL β”‚ β”‚
306
+ β”‚ β”‚ Discovery β€” β”‚ β”‚ price (= last trade β”‚ β”‚
307
+ β”‚ β”‚ after each β”‚ β”‚ or closing price) β”‚ β”‚
308
+ β”‚ β”‚ uncrossing β”‚ β”‚ β”‚ β”‚
309
+ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
310
+ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
311
+ ```
312
+
313
+ ### Order Types Roadmap
314
+
315
+ ```
316
+ Currently Implemented (EuNEx) To Be Added
317
+ ═══════���═══════════════════ ═════════════════════════════════════════
318
+
319
+ βœ“ Limit orders β–‘ Market-to-Limit orders
320
+ βœ“ Market orders (IOC) (converted to limit at best opposite)
321
+ βœ“ IOC (Immediate or Cancel) β–‘ Stop-Market orders
322
+ βœ“ FOK (Fill or Kill) (triggered when price crosses stop)
323
+ βœ“ Day validity β–‘ Stop-Limit orders
324
+ βœ“ Cancel order (stop trigger + limit price)
325
+ βœ“ Modify order β–‘ Primary Pegged orders
326
+ (pegged to BBO, auto-reprice)
327
+ β–‘ Mid-Point orders
328
+ (pegged to mid of BBO spread)
329
+ β–‘ Iceberg orders
330
+ (displayed qty + hidden reserve)
331
+ β–‘ AVD orders (Auction Volume Discovery)
332
+ (fill imbalance after uncrossing)
333
+ β–‘ GTD / GTC validity
334
+ (Good Till Date / Good Till Cancel)
335
+ β–‘ VFU / VFCU validity
336
+ (Valid For Uncrossing / Closing Uncr.)
337
+ ```
338
+
339
+ ### Uncrossing Algorithm (Opening / Closing)
340
+
341
+ ```
342
+ [FUTURE] Uncrossing price determination algorithm:
343
+
344
+ Step 1: Maximum Execution Principle
345
+ Find price P that maximizes executable volume
346
+
347
+ Step 2: Minimum Surplus
348
+ If tie β†’ pick P with smallest order imbalance
349
+
350
+ Step 3: Reference Price
351
+ If still tie β†’ pick P closest to previous reference price
352
+
353
+ Step 4: Market Pressure
354
+ If still tie β†’ pick higher P if buy surplus, lower if sell surplus
355
+
356
+ Currently EuNEx uses continuous matching only.
357
+ Uncrossing requires:
358
+ β€’ TradingPhaseManager actor
359
+ β€’ Theoretical Opening Price (IOP) calculation
360
+ β€’ Book freeze during uncrossing
361
+ β€’ AVD order processing after uncrossing
362
+ ```
363
+
364
+ ### Price Collars & Reservations
365
+
366
+ ```
367
+ [FUTURE] Price protection mechanisms:
368
+
369
+ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
370
+ β”‚ DYNAMIC COLLARS β”‚
371
+ β”‚ Based on last traded price Β± threshold % β”‚
372
+ β”‚ Breach β†’ order rejected or book reserved β”‚
373
+ β”‚ β”‚
374
+ β”‚ STATIC COLLARS β”‚
375
+ β”‚ Based on reference price Β± wider threshold % β”‚
376
+ β”‚ Breach during continuous β†’ reservation + uncrossing β”‚
377
+ β”‚ β”‚
378
+ β”‚ RESERVATION β”‚
379
+ β”‚ Book frozen, enters call phase β”‚
380
+ β”‚ Duration: configurable per trading group β”‚
381
+ β”‚ After reservation β†’ uncrossing to resume trading β”‚
382
+ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
383
+ ```
384
+
385
+ ---
386
+
387
+ ## Future Expansion: Segments & Partitions
388
+
389
+ From the Optiq Architecture presentation.
390
+
391
+ ```
392
+ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
393
+ β”‚ OPTIQ SEGMENTS β”‚
394
+ β”‚ β”‚
395
+ β”‚ Cash Derivatives β”‚
396
+ β”‚ β”Œβ”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β” β”‚
397
+ β”‚ β”‚ EQU β”‚ β”‚ ETF β”‚ β”‚ FXI β”‚ β”‚ WAR β”‚ β”‚ EQD β”‚ β”‚ IDD β”‚ β”‚ CMO β”‚ β”‚
398
+ β”‚ β”‚Equitβ”‚ β”‚Trackβ”‚ β”‚Bondsβ”‚ β”‚Warr β”‚ β”‚Eq. β”‚ β”‚Indexβ”‚ β”‚Commoβ”‚ β”‚
399
+ β”‚ β”‚ies β”‚ β”‚ers β”‚ β”‚Debt β”‚ β”‚ants β”‚ β”‚Derivβ”‚ β”‚Derivβ”‚ β”‚dity β”‚ β”‚
400
+ β”‚ β””β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”˜ β”‚
401
+ β”‚ β”‚
402
+ β”‚ β”Œβ”€β”€β”€β”€β”€β” β”‚
403
+ β”‚ β”‚ BLK β”‚ Block β€” large trades off main book β”‚
404
+ β”‚ β””β”€β”€β”€β”€β”€β”˜ β”‚
405
+ β”‚ β”‚
406
+ β”‚ Each segment has: β”‚
407
+ β”‚ β€’ Own OEG instances (OEG.n.BOTH) β”‚
408
+ β”‚ β€’ Own ME partitions β”‚
409
+ β”‚ β€’ Own MDG multicast channels β”‚
410
+ β”‚ β”‚
411
+ β”‚ EuNEx currently: single segment, single partition β”‚
412
+ β”‚ [FUTURE] Multi-segment support via SegmentManager actor β”‚
413
+ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
414
+
415
+ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
416
+ β”‚ PARTITIONS (Horizontal Scalability) β”‚
417
+ β”‚ β”‚
418
+ β”‚ 1 Partition = 1 Machine = 1 TRNODE β”‚
419
+ β”‚ β”‚
420
+ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
421
+ β”‚ β”‚ Partition 1β”‚ β”‚ Partition 2β”‚ β”‚ Partition 3β”‚ β”‚ Partition 4β”‚ β”‚
422
+ β”‚ β”‚ Group 11 β”‚ β”‚ Group 16 β”‚ β”‚ Group 35 β”‚ β”‚ Group 37 β”‚ β”‚
423
+ β”‚ β”‚ Group 12 β”‚ β”‚ Group 17 β”‚ β”‚ Group 44 β”‚ β”‚ ... β”‚ β”‚
424
+ β”‚ β”‚ Group 13 β”‚ β”‚ ... β”‚ β”‚ ... β”‚ β”‚ β”‚ β”‚
425
+ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
426
+ β”‚ β”‚
427
+ β”‚ Cross-partition orders β†’ routed via OEG to correct partition β”‚
428
+ β”‚ β”‚
429
+ β”‚ Trading Group = set of instruments with shared: β”‚
430
+ β”‚ β€’ Trading schedule (phases, times) β”‚
431
+ β”‚ β€’ Matching rules β”‚
432
+ β”‚ β€’ Collar parameters β”‚
433
+ β”‚ β€’ Market segment or liquidity level β”‚
434
+ β”‚ β”‚
435
+ β”‚ Each Instrument: β”‚
436
+ β”‚ β€’ Identified by SymbolIndex β”‚
437
+ β”‚ β€’ May have multiple order books β”‚
438
+ β”‚ β€’ Inherits trading group attributes β”‚
439
+ β”‚ β€’ Can override: APF (Authorized Price Fluctuation) β”‚
440
+ β”‚ β”‚
441
+ β”‚ EuNEx currently: OrderBookActor per symbol, single "partition" β”‚
442
+ β”‚ [FUTURE] PartitionManager distributes symbols across nodes β”‚
443
+ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
444
+ ```
445
+
446
+ ---
447
+
448
+ ## Future Expansion: SATURN (Regulatory Reporting)
449
+
450
+ From the SATURN Overview v1.0.4 presentation.
451
+
452
+ ```
453
+ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
454
+ β”‚ SATURN SYSTEM [FUTURE] β”‚
455
+ β”‚ β”‚
456
+ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
457
+ β”‚ β”‚ ARM β€” Approved Reporting Mechanism (MiFID II RTS 22) β”‚ β”‚
458
+ β”‚ β”‚ β”‚ β”‚
459
+ β”‚ β”‚ Trading Engine ──order msg──▢ Check Library ──▢ Reporting Tool ──▢ NCAs β”‚ β”‚
460
+ β”‚ β”‚ (ME) (incl. (MiFID II (RTS 22 (AMF, β”‚ β”‚
461
+ β”‚ β”‚ MiFID II controls, formatting) FSMA, β”‚ β”‚
462
+ β”‚ β”‚ data) acceptance/ CBI, β”‚ β”‚
463
+ β”‚ β”‚ rejection) AFM, β”‚ β”‚
464
+ β”‚ β”‚ Consob) β”‚ β”‚
465
+ β”‚ β”‚ External Trades ──▢ Saturn GUI / API / File Upload ──▢ same pipeline β”‚ β”‚
466
+ β”‚ β”‚ β”‚ β”‚
467
+ β”‚ β”‚ MiFID Firms: report to regulator of member's country β”‚ β”‚
468
+ β”‚ β”‚ Non-MiFID Firms: report to regulator of instrument's MIC β”‚ β”‚
469
+ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
470
+ β”‚ β”‚
471
+ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
472
+ β”‚ β”‚ OBOE β€” Off-Book On-Exchange (Dublin + Oslo) β”‚ β”‚
473
+ β”‚ β”‚ β”‚ β”‚
474
+ β”‚ β”‚ Trade Declaration ──▢ Check Module ──▢ MDG (publication) ──▢ Members β”‚ β”‚
475
+ β”‚ β”‚ (via Saturn Web / REST API) (deferred publication rules) β”‚ β”‚
476
+ β”‚ β”‚ β”‚ β”‚
477
+ β”‚ β”‚ Scope: instruments listed on Euronext Dublin & Oslo, β”‚ β”‚
478
+ β”‚ β”‚ tradable on Euronext Central Order Book β”‚ β”‚
479
+ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
480
+ β”‚ β”‚
481
+ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
482
+ β”‚ β”‚ SLC Manager β€” Short Long Code (MiFID II anonymization) β”‚ β”‚
483
+ β”‚ β”‚ β”‚ β”‚
484
+ β”‚ β”‚ Before: Order has Member LEI β†’ everyone sees originator β”‚ β”‚
485
+ β”‚ β”‚ After: Order has Short Code 123 β†’ only SATURN maps to LEI β”‚ β”‚
486
+ β”‚ β”‚ β”‚ β”‚
487
+ β”‚ β”‚ Entities hidden: Client ID (LEI), human client (National ID), β”‚ β”‚
488
+ β”‚ β”‚ AGGR/PNAL flags, Execution Decision maker, β”‚ β”‚
489
+ β”‚ β”‚ Investment Decision maker β”‚ β”‚
490
+ β”‚ β”‚ β”‚ β”‚
491
+ β”‚ β”‚ Transmitted to NCAs via RTS 22/24 regulatory reports β”‚ β”‚
492
+ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
493
+ β”‚ β”‚
494
+ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
495
+ β”‚ β”‚ Commodities Reporting (MiFID II RTS 21) β”‚ β”‚
496
+ β”‚ β”‚ β”‚ β”‚
497
+ β”‚ β”‚ Daily: positions per client β†’ ESMA/NCAs β”‚ β”‚
498
+ β”‚ β”‚ Weekly: Commitment of Traders (CoT) β†’ Euronext website β”‚ β”‚
499
+ β”‚ β”‚ β”‚ β”‚
500
+ β”‚ β”‚ Sources: Participants & EMS β†’ Saturn β†’ Check Library β”‚ β”‚
501
+ β”‚ β”‚ Reference: Matrix (IDS) + European Instruments Referential β”‚ β”‚
502
+ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
503
+ β”‚ β”‚
504
+ β”‚ Architecture: Kafka Engine + GUI + API + DB β”‚
505
+ β”‚ Communicates with: ESMA, NCAs (AMF, CBI, FCA, AFM, Consob, CNMV, CSSF, FSMA, β”‚
506
+ β”‚ CMVM), Euronext Clearing, Borsa Italiana β”‚
507
+ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
508
+ ```
509
+
510
+ ---
511
+
512
+ ## Future Expansion: Post-Trade Path
513
+
514
+ From `Trades paths.drawio` β€” complete downstream flow after trade execution.
515
+
516
+ ```
517
+ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
518
+ β”‚ COMPLETE POST-TRADE PATH [FUTURE] β”‚
519
+ β”‚ β”‚
520
+ β”‚ Book/LogCore (trade generated) β”‚
521
+ β”‚ β”‚ β”‚
522
+ β”‚ β”œβ”€β”€β–Ά Kafka (recovery + audit trail) ──▢ PE DB (Persistence Engine DB) β”‚
523
+ β”‚ β”‚ β”‚
524
+ β”‚ β”œβ”€β”€β–Ά IACA Inside (fragment emission) β”‚
525
+ β”‚ β”‚ β”‚ β”‚
526
+ β”‚ β”‚ β”œβ”€β”€β–Ά IACA FINISH (chain completion β†’ IA SBE message) β”‚
527
+ β”‚ β”‚ β”‚ β”‚ β”‚
528
+ β”‚ β”‚ β”‚ β”œβ”€β”€β–Ά IACA COPY (members) ──▢ Members (DropCopy) β”‚
529
+ β”‚ β”‚ β”‚ β”œβ”€β”€β–Ά IACA COPY (3rd parties) ──▢ DCG 3P's β”‚
530
+ β”‚ β”‚ β”‚ β”œβ”€β”€β–Ά IAkafka2DB.TRD ──▢ PE DB (trade persistence) β”‚
531
+ β”‚ β”‚ β”‚ └──▢ PackFULL / PackTOL ──▢ MDG Emitter ──▢ Members β”‚
532
+ β”‚ β”‚ β”‚ β”‚
533
+ β”‚ β”‚ └──▢ IDS (files to IDS) ──▢ Regulators (RTS nn) β”‚
534
+ β”‚ β”‚ β”‚
535
+ β”‚ β”œβ”€β”€β–Ά MktUpdt, FullTradeInfo ──▢ MDG ──▢ Members (market data multicast) β”‚
536
+ β”‚ β”‚ β”‚
537
+ β”‚ β”œβ”€β”€β–Ά Trade Fill ──▢ PTB (Post-Trade Box) β”‚
538
+ β”‚ β”‚ β”‚ β”‚
539
+ β”‚ β”‚ β”œβ”€β”€β–Ά PTB admin (resilience) β”‚
540
+ β”‚ β”‚ β”œβ”€β”€β–Ά SATURN/OBOE (regulatory reporting) β”‚
541
+ β”‚ β”‚ β”œβ”€β”€β–Ά PTB2EC ──▢ Enxt Clearing β”‚
542
+ β”‚ β”‚ β”œβ”€β”€β–Ά EC.POSTTRAD ──▢ Enxt Clearing β”‚
543
+ β”‚ β”‚ └──▢ Clearing houses: β”‚
544
+ β”‚ β”‚ EuroCCP, LCH Ltd, SIX Xclear β”‚
545
+ β”‚ β”‚ β”‚
546
+ β”‚ └──▢ ACK/NAK ──▢ OEG ──▢ Members (execution reports) β”‚
547
+ β”‚ β”‚
548
+ β”‚ External entities: DSS, BITA instr., Enxt Legacy instr., SMARTS β”‚
549
+ β”‚ Risk/Audit: Regulators, Risk/Audit systems β”‚
550
+ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
551
+ ```
552
+
553
+ ---
554
+
555
+ ## Market Data Dissemination
556
+
557
+ From Trading Manual Section 5.
558
+
559
+ ```
560
+ [FUTURE] Market data types to implement:
561
+
562
+ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
563
+ β”‚ 1. Market by Orders (MBO) β”‚
564
+ β”‚ Every individual order visible in the book β”‚
565
+ β”‚ (currently: not disseminated by EuNEx) β”‚
566
+ β”‚ β”‚
567
+ β”‚ 2. Market by Limits (MBL) ← EuNEx has this (BookUpdateEvent) β”‚
568
+ β”‚ Aggregated qty per price level (BBO + depth) β”‚
569
+ β”‚ getAsks(n) / getBids(n) already implemented β”‚
570
+ β”‚ β”‚
571
+ β”‚ 3. Trades ← EuNEx has this (TradeEvent) β”‚
572
+ β”‚ Each trade with price, qty, timestamp β”‚
573
+ β”‚ β”‚
574
+ β”‚ 4. Trading Day Price Summary β”‚
575
+ β”‚ Open, High, Low, Close, Volume, VWAP β”‚
576
+ β”‚ (currently: not tracked by EuNEx) β”‚
577
+ β”‚ β”‚
578
+ β”‚ Format: SBE multicast (production) vs in-memory events (EuNEx) β”‚
579
+ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
580
+ ```
581
+
582
+ ---
583
+
584
+ ## Implementation Priority Roadmap
585
+
586
+ ```
587
+ Phase 1 (Current) βœ“ DONE
588
+ β”œβ”€β”€ OrderBook with price-time priority matching
589
+ β”œβ”€β”€ OEGatewayActor β†’ OrderBookActor β†’ MarketDataActor pipeline
590
+ β”œβ”€β”€ Recovery Cause/Effect with Master/Mirror gating
591
+ β”œβ”€β”€ IACA fragment chains with completion detection
592
+ └── Simplx shim for single-threaded testing
593
+
594
+ Phase 2 β€” Core Trading Features
595
+ β”œβ”€β”€ Trading phases (pre-open, uncrossing, continuous, close, TAL)
596
+ β”œβ”€β”€ Uncrossing algorithm (max execution, min surplus, ref price)
597
+ β”œβ”€β”€ Price collars (dynamic + static) and reservations
598
+ β”œβ”€β”€ Additional order types (Stop, Pegged, Mid-Point, Iceberg, M2L)
599
+ β”œβ”€β”€ Additional validities (GTD, GTC, VFU, VFCU)
600
+ └── Trading groups and instrument configuration
601
+
602
+ Phase 3 β€” Infrastructure
603
+ β”œβ”€β”€ Real Simplx integration (multi-threaded, multi-core actors)
604
+ β”œβ”€β”€ Kafka persistence (replace FragmentStore with real Kafka)
605
+ β”œβ”€β”€ SBE encoding (replace event structs with SBE messages)
606
+ β”œβ”€β”€ PE DB (Persistence Engine database for order state)
607
+ └── FIX 5.0 SP2 gateway (replace shim OEG with FIX acceptor)
608
+
609
+ Phase 4 β€” Post-Trade & Distribution
610
+ β”œβ”€β”€ IACA FINISH + IACA COPY (DropCopy to members / 3rd parties)
611
+ β”œβ”€β”€ MDG multicast SBE feed
612
+ β”œβ”€β”€ IDS (Intraday Data Service β†’ files to regulators)
613
+ β”œβ”€β”€ PTB (Post-Trade Box) with trade routing
614
+ └── DCG (DropCopy Gateway for 3rd parties)
615
+
616
+ Phase 5 β€” Regulatory & Clearing
617
+ β”œβ”€β”€ SATURN ARM (MiFID II RTS 22 trade reporting to NCAs)
618
+ β”œβ”€β”€ SATURN OBOE (Off-Book On-Exchange for Dublin/Oslo)
619
+ β”œβ”€β”€ SLC Manager (Short Long Code anonymization)
620
+ β”œβ”€β”€ Commodities Reporting (RTS 21 position reporting)
621
+ β”œβ”€β”€ Clearing integration (EuroCCP, LCH Ltd, SIX Xclear)
622
+ └── Enxt Clearing (PTB2EC, EC.POSTTRAD)
623
+
624
+ Phase 6 β€” Scalability & Operations
625
+ β”œβ”€β”€ Multi-segment support (EQU, ETF, FXI, EQD, IDD, CMO, etc.)
626
+ β”œβ”€β”€ Partitioning (1 partition = 1 machine, cross-partition routing)
627
+ β”œβ”€β”€ Full Master/Mirror failover with Kafka replay
628
+ β”œβ”€β”€ TCS OTC (Trade Capture Service for OTC trades)
629
+ β”œβ”€β”€ SMARTS market surveillance integration
630
+ └── GUI resilience and operational tooling
631
+ ```
examples/ping_pong.cpp ADDED
@@ -0,0 +1,85 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // ════════════════════════════════════════════════════════════════════
2
+ // Ping-Pong Example β€” Learning the Simplx Actor Model
3
+ //
4
+ // Two actors exchange events back and forth, demonstrating:
5
+ // - Actor creation
6
+ // - Event definition and registration
7
+ // - Event::Pipe for sending events
8
+ // - getSourceActorId() for replies
9
+ //
10
+ // This is the first exercise for understanding the actor framework.
11
+ // ════════════════════════════════════════════════════════════════════
12
+
13
+ #include "engine/SimplxShim.hpp"
14
+ #include <iostream>
15
+ #include <string>
16
+
17
+ using namespace tredzone;
18
+
19
+ // ── Events ─────────────────────────────────────────────────────────
20
+ struct PingEvent : Actor::Event {
21
+ int counter;
22
+ explicit PingEvent(int c) : counter(c) {}
23
+ };
24
+
25
+ struct PongEvent : Actor::Event {
26
+ int counter;
27
+ explicit PongEvent(int c) : counter(c) {}
28
+ };
29
+
30
+ // ── PongActor: receives Ping, sends Pong back ─────────────────────
31
+ class PongActor : public Actor {
32
+ public:
33
+ PongActor() {
34
+ registerEventHandler<PingEvent>(*this);
35
+ std::cout << "[Pong] Created on core " << (int)getCore() << "\n";
36
+ }
37
+
38
+ void onEvent(const PingEvent& event) {
39
+ std::cout << "[Pong] Received ping #" << event.counter << "\n";
40
+ Event::Pipe pipe(*this, event.getSourceActorId());
41
+ pipe.push<PongEvent>(event.counter);
42
+ }
43
+ };
44
+
45
+ // ── PingActor: sends Ping, receives Pong ───────────────────────────
46
+ class PingActor : public Actor {
47
+ public:
48
+ PingActor(const ActorId& pongId, int maxRounds)
49
+ : pongPipe_(*this, pongId), maxRounds_(maxRounds)
50
+ {
51
+ registerEventHandler<PongEvent>(*this);
52
+ std::cout << "[Ping] Created on core " << (int)getCore() << "\n";
53
+
54
+ // Send first ping
55
+ std::cout << "[Ping] Sending ping #1\n";
56
+ pongPipe_.push<PingEvent>(1);
57
+ }
58
+
59
+ void onEvent(const PongEvent& event) {
60
+ std::cout << "[Ping] Received pong #" << event.counter << "\n";
61
+ if (event.counter < maxRounds_) {
62
+ int next = event.counter + 1;
63
+ std::cout << "[Ping] Sending ping #" << next << "\n";
64
+ pongPipe_.push<PingEvent>(next);
65
+ } else {
66
+ std::cout << "[Ping] Done after " << maxRounds_ << " rounds.\n";
67
+ }
68
+ }
69
+
70
+ private:
71
+ Event::Pipe pongPipe_;
72
+ int maxRounds_;
73
+ };
74
+
75
+ // ── Main ───────────────────────────────────────────────────────────
76
+ int main() {
77
+ std::cout << "=== Ping-Pong Actor Example ===\n\n";
78
+
79
+ // Create actors (in shim mode, single-threaded)
80
+ auto pong = std::make_unique<PongActor>();
81
+ auto ping = std::make_unique<PingActor>(pong->getActorId(), 5);
82
+
83
+ std::cout << "\n=== Complete ===\n";
84
+ return 0;
85
+ }
examples/simple_match.cpp ADDED
@@ -0,0 +1,195 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // ════════════════════════════════════════════════════════════════════
2
+ // Simple Match Example β€” End-to-end order matching with recovery
3
+ //
4
+ // Demonstrates the full Optiq-style flow:
5
+ // 1. Recovery Cause persists the incoming order
6
+ // 2. Business logic matches the order
7
+ // 3. Effect sends ack back to OE (Master only)
8
+ // 4. Effect publishes market data update
9
+ // 5. IACA fragments form a complete chain
10
+ //
11
+ // This bridges StockEx's match_order() with Optiq's Cause-Effect model.
12
+ // ════════════════════════════════════════════════════════════════════
13
+
14
+ #include "engine/SimplxShim.hpp"
15
+ #include "common/OrderBook.hpp"
16
+ #include "recovery/RecoveryProxy.hpp"
17
+ #include "iaca/Fragment.hpp"
18
+ #include "iaca/IacaAggregator.hpp"
19
+ #include <iostream>
20
+
21
+ using namespace eunex;
22
+ using namespace eunex::recovery;
23
+ using namespace eunex::iaca;
24
+
25
+ int main() {
26
+ std::cout << "=== Simple Match with Recovery + IACA ===\n\n";
27
+
28
+ // ── Setup infrastructure ───────────────────────────────────────
29
+ FragmentStore store;
30
+ RecoveryProxy recoveryProxy(ORIGIN_BOOK, /*key=*/1, store, /*isMaster=*/true);
31
+ IacaAggregator aggregator;
32
+
33
+ int iaMessagesGenerated = 0;
34
+ aggregator.registerHandler(std::make_shared<NewOrderHandler>(
35
+ [&](const FragmentChain& chain) {
36
+ ++iaMessagesGenerated;
37
+ std::cout << " [IACA] IA message generated from chain "
38
+ << chain.chainId << " (" << chain.fragments.size()
39
+ << " fragments)\n";
40
+ }
41
+ ));
42
+
43
+ OrderBook book(1);
44
+
45
+ // ── Process a sell order (Optiq-style) ─────────────────────────
46
+ std::cout << "── Processing SELL 100 @ 50.00 ──\n";
47
+ {
48
+ Order sellOrder{};
49
+ sellOrder.clOrdId = 1001;
50
+ sellOrder.symbolIdx = 1;
51
+ sellOrder.side = Side::Sell;
52
+ sellOrder.ordType = OrderType::Limit;
53
+ sellOrder.tif = TimeInForce::Day;
54
+ sellOrder.price = toFixedPrice(50.00);
55
+ sellOrder.quantity = 100;
56
+
57
+ // Step 1: Recovery Cause β€” persist the event
58
+ recoveryProxy.cause(/*persistenceId=*/1, sellOrder,
59
+ [&](uint64_t chainId, uint64_t seq) -> int {
60
+
61
+ // Step 2: IACA Cause β€” emit root fragment (BOOK)
62
+ IacaFragment bookFrag{};
63
+ bookFrag.chainId = chainId;
64
+ bookFrag.origin = {ORIGIN_BOOK, 1, seq};
65
+ bookFrag.previousOrigin = Origin::null();
66
+ bookFrag.causeId = CAUSE_NEW_ORDER_SELL;
67
+ bookFrag.nextCount = 1; // one child: the ack
68
+
69
+ // Step 3: Business logic β€” match the order
70
+ book.newOrder(sellOrder,
71
+ [](const Trade&) {},
72
+ [](const ExecutionReport& rpt) {
73
+ std::cout << " [ExecRpt] Status="
74
+ << (int)rpt.status << " Remaining="
75
+ << rpt.remainingQty << "\n";
76
+ }
77
+ );
78
+
79
+ // Step 4: Effect β€” send ack (Master only)
80
+ recoveryProxy.effect([&]() {
81
+ IacaFragment ackFrag{};
82
+ ackFrag.chainId = chainId;
83
+ ackFrag.origin = {ORIGIN_LOGICAL_CORE, 1, seq};
84
+ ackFrag.previousOrigin = bookFrag.origin;
85
+ ackFrag.causeId = CAUSE_ACK_DATA;
86
+ ackFrag.nextCount = 0;
87
+ aggregator.addFragment(ackFrag);
88
+ std::cout << " [Effect] Ack sent (Master)\n";
89
+ });
90
+
91
+ aggregator.addFragment(bookFrag);
92
+ return 0; // recovery nextCount always 0 for ME events
93
+ }
94
+ );
95
+ }
96
+
97
+ // ── Process a buy that matches ─────────────────────────────────
98
+ std::cout << "\n── Processing BUY 60 @ 50.00 ──\n";
99
+ {
100
+ Order buyOrder{};
101
+ buyOrder.clOrdId = 1002;
102
+ buyOrder.symbolIdx = 1;
103
+ buyOrder.side = Side::Buy;
104
+ buyOrder.ordType = OrderType::Limit;
105
+ buyOrder.tif = TimeInForce::Day;
106
+ buyOrder.price = toFixedPrice(50.00);
107
+ buyOrder.quantity = 60;
108
+
109
+ recoveryProxy.cause(2, buyOrder,
110
+ [&](uint64_t chainId, uint64_t seq) -> int {
111
+
112
+ IacaFragment bookFrag{};
113
+ bookFrag.chainId = chainId;
114
+ bookFrag.origin = {ORIGIN_BOOK, 1, seq};
115
+ bookFrag.previousOrigin = Origin::null();
116
+ bookFrag.causeId = CAUSE_NEW_ORDER_BUY;
117
+ bookFrag.nextCount = 2; // ack + trade
118
+
119
+ book.newOrder(buyOrder,
120
+ [&](const Trade& trade) {
121
+ std::cout << " [Trade] " << trade.quantity
122
+ << " @ " << toDouble(trade.price) << "\n";
123
+
124
+ // IACA fragment for trade
125
+ recoveryProxy.effect([&]() {
126
+ IacaFragment tradeFrag{};
127
+ tradeFrag.chainId = chainId;
128
+ tradeFrag.origin = {ORIGIN_LOGICAL_CORE, 1, seq + 100};
129
+ tradeFrag.previousOrigin = bookFrag.origin;
130
+ tradeFrag.causeId = CAUSE_TRADE_DATA;
131
+ tradeFrag.nextCount = 0;
132
+ aggregator.addFragment(tradeFrag);
133
+ });
134
+ },
135
+ [](const ExecutionReport& rpt) {
136
+ std::cout << " [ExecRpt] Status="
137
+ << (int)rpt.status << " Filled="
138
+ << rpt.filledQty << "\n";
139
+ }
140
+ );
141
+
142
+ recoveryProxy.effect([&]() {
143
+ IacaFragment ackFrag{};
144
+ ackFrag.chainId = chainId;
145
+ ackFrag.origin = {ORIGIN_LOGICAL_CORE, 1, seq + 200};
146
+ ackFrag.previousOrigin = bookFrag.origin;
147
+ ackFrag.causeId = CAUSE_ACK_DATA;
148
+ ackFrag.nextCount = 0;
149
+ aggregator.addFragment(ackFrag);
150
+ });
151
+
152
+ aggregator.addFragment(bookFrag);
153
+ return 0;
154
+ }
155
+ );
156
+ }
157
+
158
+ // ── Summary ────────────────────────────────────────────────────
159
+ std::cout << "\n── Summary ───────────────────────────────\n";
160
+ std::cout << " Recovery fragments persisted: " << store.size() << "\n";
161
+ std::cout << " IACA chains completed: " << aggregator.completedChainCount() << "\n";
162
+ std::cout << " IA messages generated: " << iaMessagesGenerated << "\n";
163
+ std::cout << " Book state: " << book.bidCount() << " bids, "
164
+ << book.askCount() << " asks\n";
165
+
166
+ auto bids = book.getBids(5);
167
+ auto asks = book.getAsks(5);
168
+ if (!asks.empty()) {
169
+ std::cout << " Best ask: " << toDouble(asks[0].price)
170
+ << " x " << asks[0].totalQty << "\n";
171
+ }
172
+ if (!bids.empty()) {
173
+ std::cout << " Best bid: " << toDouble(bids[0].price)
174
+ << " x " << bids[0].totalQty << "\n";
175
+ }
176
+
177
+ // ── Simulate Mirror replay ─────────────────────────────────────
178
+ std::cout << "\n── Mirror Replay Simulation ──────────────\n";
179
+ RecoveryProxy mirrorProxy(ORIGIN_BOOK, 1, store, /*isMaster=*/false);
180
+ OrderBook mirrorBook(1);
181
+
182
+ // Effects should NOT fire on mirror
183
+ mirrorProxy.effect([]() {
184
+ std::cout << " [BUG] This should not print on Mirror!\n";
185
+ });
186
+ std::cout << " Mirror effect correctly skipped.\n";
187
+
188
+ // RecoveryEffect SHOULD fire on mirror
189
+ mirrorProxy.recoveryEffect([]() {
190
+ std::cout << " [RecoveryEffect] Mirror-only logic executed.\n";
191
+ });
192
+
193
+ std::cout << "\n=== Complete ===\n";
194
+ return 0;
195
+ }
src/actors/Events.hpp ADDED
@@ -0,0 +1,133 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #pragma once
2
+ // ════════════════════════════════════════════════════════════════════
3
+ // Event definitions for inter-actor communication
4
+ //
5
+ // Maps to StockEx Kafka messages but as typed C++ structs flowing
6
+ // through Simplx Event::Pipe instead of JSON over Kafka.
7
+ //
8
+ // Optiq equivalent: these correspond to the internal events between
9
+ // OEActor, LogicalCoreActor, BookActor, and MD publishers.
10
+ // ════════════════════════════════════════════════════════════════════
11
+
12
+ #include "engine/SimplxShim.hpp"
13
+ #include "common/Types.hpp"
14
+ #include <cstring>
15
+
16
+ namespace eunex {
17
+
18
+ // ── NewOrder event (OE Gateway β†’ OrderBook Actor) ──────────────────
19
+ // Equivalent to StockEx: Kafka 'orders' topic message with type=new
20
+ // Optiq equivalent: FastNewLimitEvent / FastNewMarketEvent
21
+ struct NewOrderEvent : tredzone::Actor::Event {
22
+ ClOrdId_t clOrdId;
23
+ SymbolIndex_t symbolIdx;
24
+ Side side;
25
+ OrderType ordType;
26
+ TimeInForce tif;
27
+ Price_t price;
28
+ Quantity_t quantity;
29
+ SessionId_t sessionId;
30
+
31
+ NewOrderEvent() = default;
32
+ NewOrderEvent(ClOrdId_t cl, SymbolIndex_t sym, Side s, OrderType ot,
33
+ TimeInForce t, Price_t px, Quantity_t qty, SessionId_t sess)
34
+ : clOrdId(cl), symbolIdx(sym), side(s), ordType(ot),
35
+ tif(t), price(px), quantity(qty), sessionId(sess) {}
36
+ };
37
+
38
+ // ── CancelOrder event ──────────────────────────────────────────────
39
+ // Equivalent to StockEx: Kafka message with type=cancel
40
+ // Optiq equivalent: CancelOrderData
41
+ struct CancelOrderEvent : tredzone::Actor::Event {
42
+ OrderId_t orderId;
43
+ ClOrdId_t origClOrdId;
44
+ SymbolIndex_t symbolIdx;
45
+ SessionId_t sessionId;
46
+
47
+ CancelOrderEvent() = default;
48
+ CancelOrderEvent(OrderId_t oid, ClOrdId_t cl, SymbolIndex_t sym, SessionId_t sess)
49
+ : orderId(oid), origClOrdId(cl), symbolIdx(sym), sessionId(sess) {}
50
+ };
51
+
52
+ // ── ModifyOrder event ──────────────────────────────────────────────
53
+ // Equivalent to StockEx: Kafka message with type=amend
54
+ // Optiq equivalent: ModifyOrderData
55
+ struct ModifyOrderEvent : tredzone::Actor::Event {
56
+ OrderId_t orderId;
57
+ ClOrdId_t origClOrdId;
58
+ SymbolIndex_t symbolIdx;
59
+ Price_t newPrice;
60
+ Quantity_t newQuantity;
61
+ SessionId_t sessionId;
62
+
63
+ ModifyOrderEvent() = default;
64
+ ModifyOrderEvent(OrderId_t oid, ClOrdId_t cl, SymbolIndex_t sym,
65
+ Price_t px, Quantity_t qty, SessionId_t sess)
66
+ : orderId(oid), origClOrdId(cl), symbolIdx(sym),
67
+ newPrice(px), newQuantity(qty), sessionId(sess) {}
68
+ };
69
+
70
+ // ── ExecutionReport event (OrderBook Actor β†’ OE Gateway) ───────────
71
+ // Optiq equivalent: Ack sent back to OE frontal via sendToOEEffect
72
+ struct ExecReportEvent : tredzone::Actor::Event {
73
+ OrderId_t orderId;
74
+ ClOrdId_t clOrdId;
75
+ OrderStatus status;
76
+ Quantity_t filledQty;
77
+ Quantity_t remainingQty;
78
+ Price_t lastPrice;
79
+ Quantity_t lastQty;
80
+ TradeId_t tradeId;
81
+ SessionId_t sessionId;
82
+
83
+ ExecReportEvent() = default;
84
+ ExecReportEvent(const ExecutionReport& rpt, SessionId_t sess)
85
+ : orderId(rpt.orderId), clOrdId(rpt.clOrdId), status(rpt.status),
86
+ filledQty(rpt.filledQty), remainingQty(rpt.remainingQty),
87
+ lastPrice(rpt.lastPrice), lastQty(rpt.lastQty),
88
+ tradeId(rpt.tradeId), sessionId(sess) {}
89
+ };
90
+
91
+ // ── Trade event (OrderBook Actor β†’ MarketData Actor) ───────────────
92
+ // Optiq equivalent: trade fragment sent to MDLimit/MDIMP via IACA chain
93
+ struct TradeEvent : tredzone::Actor::Event {
94
+ Trade trade;
95
+
96
+ TradeEvent() = default;
97
+ explicit TradeEvent(const Trade& t) : trade(t) {}
98
+ };
99
+
100
+ // ── Market Data snapshot event (OrderBook β†’ MarketData Actor) ──────
101
+ // Optiq equivalent: PublishLimitUpdateRequest to MDLimitLogicalCoreHandler
102
+ struct BookUpdateEvent : tredzone::Actor::Event {
103
+ SymbolIndex_t symbolIdx;
104
+ struct Level {
105
+ Price_t price;
106
+ Quantity_t qty;
107
+ };
108
+ Level bids[10];
109
+ Level asks[10];
110
+ int bidDepth;
111
+ int askDepth;
112
+
113
+ BookUpdateEvent() : symbolIdx(0), bidDepth(0), askDepth(0) {
114
+ std::memset(bids, 0, sizeof(bids));
115
+ std::memset(asks, 0, sizeof(asks));
116
+ }
117
+ };
118
+
119
+ // ── Recovery fragment event (any actor β†’ PersistenceAgent) ─────────
120
+ // Optiq equivalent: WriteRecoveryFragmentEvent to CoreAgentActor
121
+ struct RecoveryFragmentEvent : tredzone::Actor::Event {
122
+ uint8_t persistenceId;
123
+ uint16_t originId;
124
+ uint32_t originKey;
125
+ uint64_t sequenceNumber;
126
+ uint8_t payload[4096];
127
+ size_t payloadSize;
128
+
129
+ RecoveryFragmentEvent() : persistenceId(0), originId(0), originKey(0),
130
+ sequenceNumber(0), payloadSize(0) {}
131
+ };
132
+
133
+ } // namespace eunex
src/actors/MarketDataActor.cpp ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #include "actors/MarketDataActor.hpp"
2
+
3
+ namespace eunex {
4
+
5
+ MarketDataActor::MarketDataActor() {
6
+ registerEventHandler<TradeEvent>(*this);
7
+ registerEventHandler<BookUpdateEvent>(*this);
8
+ }
9
+
10
+ void MarketDataActor::onEvent(const TradeEvent& event) {
11
+ const auto& t = event.trade;
12
+ auto& snap = snapshots_[t.symbolIdx];
13
+ snap.symbolIdx = t.symbolIdx;
14
+ snap.lastTradePrice = t.price;
15
+ snap.lastTradeQty = t.quantity;
16
+ snap.tradeCount++;
17
+ snap.updateTime = nowNs();
18
+
19
+ recentTrades_.push_back(t);
20
+ if (recentTrades_.size() > MAX_RECENT_TRADES) {
21
+ recentTrades_.erase(recentTrades_.begin());
22
+ }
23
+ }
24
+
25
+ void MarketDataActor::onEvent(const BookUpdateEvent& event) {
26
+ auto& snap = snapshots_[event.symbolIdx];
27
+ snap.symbolIdx = event.symbolIdx;
28
+ snap.updateTime = nowNs();
29
+
30
+ if (event.bidDepth > 0) {
31
+ snap.bestBid = event.bids[0].price;
32
+ snap.totalBidQty = 0;
33
+ for (int i = 0; i < event.bidDepth; ++i)
34
+ snap.totalBidQty += event.bids[i].qty;
35
+ }
36
+
37
+ if (event.askDepth > 0) {
38
+ snap.bestAsk = event.asks[0].price;
39
+ snap.totalAskQty = 0;
40
+ for (int i = 0; i < event.askDepth; ++i)
41
+ snap.totalAskQty += event.asks[i].qty;
42
+ }
43
+ }
44
+
45
+ const MarketDataSnapshot* MarketDataActor::getSnapshot(SymbolIndex_t sym) const {
46
+ auto it = snapshots_.find(sym);
47
+ return it != snapshots_.end() ? &it->second : nullptr;
48
+ }
49
+
50
+ } // namespace eunex
src/actors/MarketDataActor.hpp ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #pragma once
2
+ // ════════════════════════════════════════════════════════════════════
3
+ // MarketDataActor β€” Market Data Publisher
4
+ //
5
+ // StockEx equivalent: dashboard.py SSE streaming + mdf_simulator.py
6
+ // - Receives trades/snapshots, streams to UI
7
+ //
8
+ // Optiq equivalent: MDLimitLogicalCoreHandler + MDIMPLogicalCoreHandler
9
+ // - Receives PublishLimitUpdateRequest from Book
10
+ // - Computes limit updates, IMP values
11
+ // - Emits IACA fragments β†’ IA messages for downstream
12
+ // ════════════════════════════════════════════════════════════════════
13
+
14
+ #include "engine/SimplxShim.hpp"
15
+ #include "actors/Events.hpp"
16
+ #include <vector>
17
+ #include <unordered_map>
18
+
19
+ namespace eunex {
20
+
21
+ struct MarketDataSnapshot {
22
+ SymbolIndex_t symbolIdx;
23
+ Price_t lastTradePrice;
24
+ Quantity_t lastTradeQty;
25
+ Price_t bestBid;
26
+ Price_t bestAsk;
27
+ Quantity_t totalBidQty;
28
+ Quantity_t totalAskQty;
29
+ uint64_t tradeCount;
30
+ Timestamp_ns updateTime;
31
+ };
32
+
33
+ class MarketDataActor : public tredzone::Actor {
34
+ public:
35
+ struct Service : tredzone::AsyncService {};
36
+
37
+ MarketDataActor();
38
+
39
+ void onEvent(const TradeEvent& event);
40
+ void onEvent(const BookUpdateEvent& event);
41
+
42
+ const MarketDataSnapshot* getSnapshot(SymbolIndex_t sym) const;
43
+ const std::vector<Trade>& getRecentTrades() const { return recentTrades_; }
44
+
45
+ private:
46
+ std::unordered_map<SymbolIndex_t, MarketDataSnapshot> snapshots_;
47
+ std::vector<Trade> recentTrades_;
48
+ static constexpr size_t MAX_RECENT_TRADES = 200;
49
+ };
50
+
51
+ } // namespace eunex
src/actors/OEGatewayActor.cpp ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #include "actors/OEGatewayActor.hpp"
2
+
3
+ namespace eunex {
4
+
5
+ OEGatewayActor::OEGatewayActor() {
6
+ registerEventHandler<ExecReportEvent>(*this);
7
+ }
8
+
9
+ void OEGatewayActor::mapSymbol(SymbolIndex_t symbolIdx,
10
+ const tredzone::ActorId& bookActorId) {
11
+ symbolMap_[symbolIdx] = bookActorId;
12
+ }
13
+
14
+ void OEGatewayActor::submitNewOrder(ClOrdId_t clOrdId, SymbolIndex_t symbolIdx,
15
+ Side side, OrderType ordType, TimeInForce tif,
16
+ Price_t price, Quantity_t qty, SessionId_t session) {
17
+ auto it = symbolMap_.find(symbolIdx);
18
+ if (it == symbolMap_.end()) {
19
+ std::cerr << "OEGateway: unknown symbol " << symbolIdx << "\n";
20
+ return;
21
+ }
22
+
23
+ Event::Pipe pipe(*this, it->second);
24
+ pipe.push<NewOrderEvent>(clOrdId, symbolIdx, side, ordType, tif, price, qty, session);
25
+ }
26
+
27
+ void OEGatewayActor::submitCancel(OrderId_t orderId, ClOrdId_t origClOrdId,
28
+ SymbolIndex_t symbolIdx, SessionId_t session) {
29
+ auto it = symbolMap_.find(symbolIdx);
30
+ if (it == symbolMap_.end()) return;
31
+
32
+ Event::Pipe pipe(*this, it->second);
33
+ pipe.push<CancelOrderEvent>(orderId, origClOrdId, symbolIdx, session);
34
+ }
35
+
36
+ void OEGatewayActor::submitModify(OrderId_t orderId, ClOrdId_t origClOrdId,
37
+ SymbolIndex_t symbolIdx, Price_t newPrice,
38
+ Quantity_t newQty, SessionId_t session) {
39
+ auto it = symbolMap_.find(symbolIdx);
40
+ if (it == symbolMap_.end()) return;
41
+
42
+ Event::Pipe pipe(*this, it->second);
43
+ pipe.push<ModifyOrderEvent>(orderId, origClOrdId, symbolIdx, newPrice, newQty, session);
44
+ }
45
+
46
+ void OEGatewayActor::onEvent(const ExecReportEvent& event) {
47
+ reports_.push_back(event);
48
+ }
49
+
50
+ } // namespace eunex
src/actors/OEGatewayActor.hpp ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #pragma once
2
+ // ════════════════════════════════════════════════════════════════════
3
+ // OEGatewayActor β€” Order Entry Gateway
4
+ //
5
+ // StockEx equivalent: fix_oeg_server.py + consumer.py
6
+ // - Receives FIX messages, normalizes them, pushes to Kafka
7
+ // - Here: receives external input and routes to OrderBookActor
8
+ //
9
+ // Optiq equivalent: OEActor
10
+ // - Receives messages from OE frontal (SBE over TCP)
11
+ // - Validates session, routes to LogicalCoreActor
12
+ // - Sends acks/rejects back to OE frontal
13
+ // ════════════════════════════════════════════════════════════════════
14
+
15
+ #include "engine/SimplxShim.hpp"
16
+ #include "actors/Events.hpp"
17
+ #include <unordered_map>
18
+ #include <vector>
19
+ #include <iostream>
20
+
21
+ namespace eunex {
22
+
23
+ class OEGatewayActor : public tredzone::Actor {
24
+ public:
25
+ struct Service : tredzone::AsyncService {};
26
+
27
+ OEGatewayActor();
28
+
29
+ // Register an OrderBook actor for a symbol
30
+ void mapSymbol(SymbolIndex_t symbolIdx, const tredzone::ActorId& bookActorId);
31
+
32
+ // Submit order (called from external interface or test harness)
33
+ void submitNewOrder(ClOrdId_t clOrdId, SymbolIndex_t symbolIdx,
34
+ Side side, OrderType ordType, TimeInForce tif,
35
+ Price_t price, Quantity_t qty, SessionId_t session);
36
+
37
+ void submitCancel(OrderId_t orderId, ClOrdId_t origClOrdId,
38
+ SymbolIndex_t symbolIdx, SessionId_t session);
39
+
40
+ void submitModify(OrderId_t orderId, ClOrdId_t origClOrdId,
41
+ SymbolIndex_t symbolIdx, Price_t newPrice,
42
+ Quantity_t newQty, SessionId_t session);
43
+
44
+ // Handle execution reports coming back from OrderBookActor
45
+ void onEvent(const ExecReportEvent& event);
46
+
47
+ // Access received reports (for testing / downstream forwarding)
48
+ const std::vector<ExecReportEvent>& getReports() const { return reports_; }
49
+ void clearReports() { reports_.clear(); }
50
+
51
+ private:
52
+ std::unordered_map<SymbolIndex_t, tredzone::ActorId> symbolMap_;
53
+ std::vector<ExecReportEvent> reports_;
54
+ ClOrdId_t nextClOrdId_ = 1000;
55
+ };
56
+
57
+ } // namespace eunex
src/actors/OrderBookActor.cpp ADDED
@@ -0,0 +1,106 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #include "actors/OrderBookActor.hpp"
2
+ #include <iostream>
3
+
4
+ namespace eunex {
5
+
6
+ OrderBookActor::OrderBookActor(SymbolIndex_t symbolIdx,
7
+ const tredzone::ActorId& oeGatewayId,
8
+ const tredzone::ActorId& marketDataId)
9
+ : book_(symbolIdx)
10
+ , oePipe_(*this, oeGatewayId)
11
+ , mdPipe_(*this, marketDataId)
12
+ {
13
+ registerEventHandler<NewOrderEvent>(*this);
14
+ registerEventHandler<CancelOrderEvent>(*this);
15
+ registerEventHandler<ModifyOrderEvent>(*this);
16
+ }
17
+
18
+ // ── NewOrder ───────────────────────────────────────────────────────
19
+ // StockEx: match_order(order, producer)
20
+ // Optiq: RecoveryCause.onInput β†’ CauseOperator β†’ forwardToBook()
21
+
22
+ void OrderBookActor::onEvent(const NewOrderEvent& event) {
23
+ Order order{};
24
+ order.clOrdId = event.clOrdId;
25
+ order.symbolIdx = event.symbolIdx;
26
+ order.side = event.side;
27
+ order.ordType = event.ordType;
28
+ order.tif = event.tif;
29
+ order.price = event.price;
30
+ order.quantity = event.quantity;
31
+ order.sessionId = event.sessionId;
32
+
33
+ // In Optiq this would be wrapped in:
34
+ // RecoveryProxy::Cause β†’ persist to Kafka
35
+ // IACA Cause β†’ emit BOOK fragment to IacaAggregator
36
+ // Effect β†’ send ack to OE
37
+ // Effect β†’ publish limit update to MDLimit
38
+
39
+ book_.newOrder(order,
40
+ // Trade callback β€” emit trade to MarketData actor
41
+ [this](const Trade& trade) {
42
+ mdPipe_.push<TradeEvent>(trade);
43
+ },
44
+ // Execution report β€” send back to OE Gateway
45
+ [this, &event](const ExecutionReport& rpt) {
46
+ oePipe_.push<ExecReportEvent>(rpt, event.sessionId);
47
+ }
48
+ );
49
+
50
+ publishBookUpdate();
51
+ }
52
+
53
+ // ── Cancel ─────────────────────────────────────────────────────────
54
+ // StockEx: handle_cancel(msg, producer)
55
+
56
+ void OrderBookActor::onEvent(const CancelOrderEvent& event) {
57
+ ExecutionReport rpt{};
58
+ if (book_.cancelOrder(event.orderId, rpt)) {
59
+ oePipe_.push<ExecReportEvent>(rpt, event.sessionId);
60
+ publishBookUpdate();
61
+ } else {
62
+ ExecutionReport reject{event.orderId, event.origClOrdId,
63
+ OrderStatus::Rejected, 0, 0, 0, 0, 0};
64
+ oePipe_.push<ExecReportEvent>(reject, event.sessionId);
65
+ }
66
+ }
67
+
68
+ // ── Modify (Cancel-Replace) ────────────────────────────────────────
69
+ // StockEx: handle_amend(msg, producer)
70
+
71
+ void OrderBookActor::onEvent(const ModifyOrderEvent& event) {
72
+ ExecutionReport rpt{};
73
+ if (book_.modifyOrder(event.orderId, event.newPrice, event.newQuantity, rpt)) {
74
+ oePipe_.push<ExecReportEvent>(rpt, event.sessionId);
75
+ publishBookUpdate();
76
+ } else {
77
+ ExecutionReport reject{event.orderId, event.origClOrdId,
78
+ OrderStatus::Rejected, 0, 0, 0, 0, 0};
79
+ oePipe_.push<ExecReportEvent>(reject, event.sessionId);
80
+ }
81
+ }
82
+
83
+ // ── Publish book snapshot to MarketData actor ──────────────────────
84
+ // StockEx: the Dashboard reads orderbook via REST GET /orderbook/<symbol>
85
+ // Optiq: publishLimitEffect β†’ push to MDLimitLogicalCoreHandler
86
+
87
+ void OrderBookActor::publishBookUpdate() {
88
+ BookUpdateEvent update;
89
+ update.symbolIdx = book_.symbolIndex();
90
+
91
+ auto bidLevels = book_.getBids(10);
92
+ update.bidDepth = static_cast<int>(bidLevels.size());
93
+ for (int i = 0; i < update.bidDepth; ++i) {
94
+ update.bids[i] = {bidLevels[i].price, bidLevels[i].totalQty};
95
+ }
96
+
97
+ auto askLevels = book_.getAsks(10);
98
+ update.askDepth = static_cast<int>(askLevels.size());
99
+ for (int i = 0; i < update.askDepth; ++i) {
100
+ update.asks[i] = {askLevels[i].price, askLevels[i].totalQty};
101
+ }
102
+
103
+ mdPipe_.push<BookUpdateEvent>(update);
104
+ }
105
+
106
+ } // namespace eunex
src/actors/OrderBookActor.hpp ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #pragma once
2
+ // ════════════════════════════════════════════════════════════════════
3
+ // OrderBookActor β€” The matching engine core
4
+ //
5
+ // StockEx equivalent: matcher.py (match_order, handle_cancel, handle_amend)
6
+ // Optiq equivalent: LogicalCoreActor + RecoveryHelperCore + Book
7
+ //
8
+ // One actor per symbol (or group of symbols). Owns the OrderBook,
9
+ // processes incoming orders, and emits trades + execution reports.
10
+ //
11
+ // In Optiq, this would be a LogicalCoreActor with:
12
+ // - RecoveryProxy::Cause for persisting each incoming event
13
+ // - IACA Cause/Effect chain for producing IA messages
14
+ // - Effects gated by Master/Mirror role
15
+ //
16
+ // Here we implement the core matching with simplified recovery hooks.
17
+ // ════════════════════════════════════════════════════════════════════
18
+
19
+ #include "engine/SimplxShim.hpp"
20
+ #include "common/OrderBook.hpp"
21
+ #include "actors/Events.hpp"
22
+
23
+ namespace eunex {
24
+
25
+ class OrderBookActor : public tredzone::Actor {
26
+ public:
27
+ struct Service : tredzone::AsyncService {};
28
+
29
+ OrderBookActor(SymbolIndex_t symbolIdx,
30
+ const tredzone::ActorId& oeGatewayId,
31
+ const tredzone::ActorId& marketDataId);
32
+
33
+ void onEvent(const NewOrderEvent& event);
34
+ void onEvent(const CancelOrderEvent& event);
35
+ void onEvent(const ModifyOrderEvent& event);
36
+
37
+ private:
38
+ OrderBook book_;
39
+ tredzone::Actor::Event::Pipe oePipe_;
40
+ tredzone::Actor::Event::Pipe mdPipe_;
41
+
42
+ void publishBookUpdate();
43
+ };
44
+
45
+ } // namespace eunex
src/common/OrderBook.cpp ADDED
@@ -0,0 +1,325 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #include "common/OrderBook.hpp"
2
+ #include <algorithm>
3
+
4
+ namespace eunex {
5
+
6
+ OrderBook::OrderBook(SymbolIndex_t symbolIdx)
7
+ : symbolIdx_(symbolIdx) {}
8
+
9
+ // ── New Order Entry ────────────────────────────────────────────────
10
+
11
+ void OrderBook::newOrder(Order& order, const TradeCallback& onTrade,
12
+ const ExecCallback& onExec) {
13
+ order.orderId = allocateOrderId();
14
+ order.remainingQty = order.quantity;
15
+ order.status = OrderStatus::New;
16
+ order.entryTime = nowNs();
17
+
18
+ // FOK: check if full fill is possible before matching
19
+ if (order.tif == TimeInForce::FOK) {
20
+ Quantity_t available = 0;
21
+ if (order.side == Side::Buy) {
22
+ for (auto& [price, orders] : asks_) {
23
+ if (order.ordType != OrderType::Market && price > order.price)
24
+ break;
25
+ for (auto& resting : orders)
26
+ available += resting.remainingQty;
27
+ if (available >= order.quantity) break;
28
+ }
29
+ } else {
30
+ for (auto& [price, orders] : bids_) {
31
+ if (order.ordType != OrderType::Market && price < order.price)
32
+ break;
33
+ for (auto& resting : orders)
34
+ available += resting.remainingQty;
35
+ if (available >= order.quantity) break;
36
+ }
37
+ }
38
+ if (available < order.quantity) {
39
+ order.status = OrderStatus::Rejected;
40
+ ExecutionReport rpt{order.orderId, order.clOrdId, OrderStatus::Rejected,
41
+ 0, order.quantity, 0, 0, 0};
42
+ onExec(rpt);
43
+ return;
44
+ }
45
+ }
46
+
47
+ if (order.side == Side::Buy) {
48
+ matchBuy(order, onTrade, onExec);
49
+ } else {
50
+ matchSell(order, onTrade, onExec);
51
+ }
52
+
53
+ // Resting logic: Market and IOC never rest on book
54
+ if (order.remainingQty > 0) {
55
+ if (order.ordType == OrderType::Market || order.tif == TimeInForce::IOC) {
56
+ order.status = OrderStatus::Cancelled;
57
+ ExecutionReport rpt{order.orderId, order.clOrdId, OrderStatus::Cancelled,
58
+ order.quantity - order.remainingQty, 0, 0, 0, 0};
59
+ onExec(rpt);
60
+ } else {
61
+ order.status = (order.remainingQty < order.quantity)
62
+ ? OrderStatus::PartiallyFilled : OrderStatus::New;
63
+ insertResting(order);
64
+ ExecutionReport rpt{order.orderId, order.clOrdId, order.status,
65
+ order.quantity - order.remainingQty,
66
+ order.remainingQty, 0, 0, 0};
67
+ onExec(rpt);
68
+ }
69
+ } else {
70
+ order.status = OrderStatus::Filled;
71
+ ExecutionReport rpt{order.orderId, order.clOrdId, OrderStatus::Filled,
72
+ order.quantity, 0, 0, 0, 0};
73
+ onExec(rpt);
74
+ }
75
+ }
76
+
77
+ // ── Matching: Buy against Asks ─────────────────────────────────────
78
+
79
+ void OrderBook::matchBuy(Order& incoming, const TradeCallback& onTrade,
80
+ const ExecCallback& onExec) {
81
+ auto it = asks_.begin();
82
+ while (incoming.remainingQty > 0 && it != asks_.end()) {
83
+ Price_t askPrice = it->first;
84
+
85
+ if (incoming.ordType != OrderType::Market && askPrice > incoming.price)
86
+ break;
87
+
88
+ auto& level = it->second;
89
+ auto orderIt = level.begin();
90
+ while (incoming.remainingQty > 0 && orderIt != level.end()) {
91
+ Quantity_t traded = std::min(incoming.remainingQty, orderIt->remainingQty);
92
+ Price_t tradePrice = askPrice;
93
+
94
+ Trade trade{};
95
+ trade.tradeId = allocateTradeId();
96
+ trade.symbolIdx = symbolIdx_;
97
+ trade.price = tradePrice;
98
+ trade.quantity = traded;
99
+ trade.buyOrderId = incoming.orderId;
100
+ trade.sellOrderId = orderIt->orderId;
101
+ trade.buyClOrdId = incoming.clOrdId;
102
+ trade.sellClOrdId = orderIt->clOrdId;
103
+ trade.matchTime = nowNs();
104
+ onTrade(trade);
105
+
106
+ incoming.remainingQty -= traded;
107
+ orderIt->remainingQty -= traded;
108
+
109
+ // Execution report for the resting sell
110
+ ExecutionReport restingRpt{orderIt->orderId, orderIt->clOrdId,
111
+ orderIt->remainingQty == 0 ? OrderStatus::Filled : OrderStatus::PartiallyFilled,
112
+ orderIt->quantity - orderIt->remainingQty,
113
+ orderIt->remainingQty, tradePrice, traded, trade.tradeId};
114
+ onExec(restingRpt);
115
+
116
+ if (orderIt->remainingQty == 0) {
117
+ orderIndex_.erase(orderIt->orderId);
118
+ orderIt = level.erase(orderIt);
119
+ } else {
120
+ ++orderIt;
121
+ }
122
+ }
123
+
124
+ if (level.empty()) {
125
+ it = asks_.erase(it);
126
+ } else {
127
+ ++it;
128
+ }
129
+ }
130
+ }
131
+
132
+ // ── Matching: Sell against Bids ────────────────────────────────────
133
+
134
+ void OrderBook::matchSell(Order& incoming, const TradeCallback& onTrade,
135
+ const ExecCallback& onExec) {
136
+ auto it = bids_.begin();
137
+ while (incoming.remainingQty > 0 && it != bids_.end()) {
138
+ Price_t bidPrice = it->first;
139
+
140
+ if (incoming.ordType != OrderType::Market && bidPrice < incoming.price)
141
+ break;
142
+
143
+ auto& level = it->second;
144
+ auto orderIt = level.begin();
145
+ while (incoming.remainingQty > 0 && orderIt != level.end()) {
146
+ Quantity_t traded = std::min(incoming.remainingQty, orderIt->remainingQty);
147
+ Price_t tradePrice = bidPrice;
148
+
149
+ Trade trade{};
150
+ trade.tradeId = allocateTradeId();
151
+ trade.symbolIdx = symbolIdx_;
152
+ trade.price = tradePrice;
153
+ trade.quantity = traded;
154
+ trade.buyOrderId = orderIt->orderId;
155
+ trade.sellOrderId = incoming.orderId;
156
+ trade.buyClOrdId = orderIt->clOrdId;
157
+ trade.sellClOrdId = incoming.clOrdId;
158
+ trade.matchTime = nowNs();
159
+ onTrade(trade);
160
+
161
+ incoming.remainingQty -= traded;
162
+ orderIt->remainingQty -= traded;
163
+
164
+ ExecutionReport restingRpt{orderIt->orderId, orderIt->clOrdId,
165
+ orderIt->remainingQty == 0 ? OrderStatus::Filled : OrderStatus::PartiallyFilled,
166
+ orderIt->quantity - orderIt->remainingQty,
167
+ orderIt->remainingQty, tradePrice, traded, trade.tradeId};
168
+ onExec(restingRpt);
169
+
170
+ if (orderIt->remainingQty == 0) {
171
+ orderIndex_.erase(orderIt->orderId);
172
+ orderIt = level.erase(orderIt);
173
+ } else {
174
+ ++orderIt;
175
+ }
176
+ }
177
+
178
+ if (level.empty()) {
179
+ it = bids_.erase(it);
180
+ } else {
181
+ ++it;
182
+ }
183
+ }
184
+ }
185
+
186
+ // ── Cancel ─────────────────────────────────────────────────────────
187
+
188
+ bool OrderBook::cancelOrder(OrderId_t orderId, ExecutionReport& report) {
189
+ auto it = orderIndex_.find(orderId);
190
+ if (it == orderIndex_.end())
191
+ return false;
192
+
193
+ auto [side, price] = it->second;
194
+
195
+ if (side == Side::Buy) {
196
+ auto levelIt = bids_.find(price);
197
+ if (levelIt != bids_.end()) {
198
+ auto& vec = levelIt->second;
199
+ for (auto oit = vec.begin(); oit != vec.end(); ++oit) {
200
+ if (oit->orderId == orderId) {
201
+ report = {orderId, oit->clOrdId, OrderStatus::Cancelled,
202
+ oit->quantity - oit->remainingQty, 0, 0, 0, 0};
203
+ vec.erase(oit);
204
+ if (vec.empty()) bids_.erase(levelIt);
205
+ orderIndex_.erase(it);
206
+ return true;
207
+ }
208
+ }
209
+ }
210
+ } else {
211
+ auto levelIt = asks_.find(price);
212
+ if (levelIt != asks_.end()) {
213
+ auto& vec = levelIt->second;
214
+ for (auto oit = vec.begin(); oit != vec.end(); ++oit) {
215
+ if (oit->orderId == orderId) {
216
+ report = {orderId, oit->clOrdId, OrderStatus::Cancelled,
217
+ oit->quantity - oit->remainingQty, 0, 0, 0, 0};
218
+ vec.erase(oit);
219
+ if (vec.empty()) asks_.erase(levelIt);
220
+ orderIndex_.erase(it);
221
+ return true;
222
+ }
223
+ }
224
+ }
225
+ }
226
+ return false;
227
+ }
228
+
229
+ // ── Modify (Cancel-Replace) ────────────────────────────────────────
230
+
231
+ bool OrderBook::modifyOrder(OrderId_t orderId, Price_t newPrice, Quantity_t newQty,
232
+ ExecutionReport& report) {
233
+ ExecutionReport cancelRpt{};
234
+ if (!cancelOrder(orderId, cancelRpt))
235
+ return false;
236
+
237
+ Order replacement{};
238
+ replacement.clOrdId = cancelRpt.clOrdId;
239
+ replacement.symbolIdx = symbolIdx_;
240
+ replacement.side = (bids_.count(cancelRpt.lastPrice)) ? Side::Buy : Side::Sell;
241
+ replacement.ordType = OrderType::Limit;
242
+ replacement.tif = TimeInForce::Day;
243
+ replacement.price = newPrice;
244
+ replacement.quantity = newQty;
245
+ replacement.remainingQty = newQty;
246
+ replacement.orderId = allocateOrderId();
247
+ replacement.entryTime = nowNs();
248
+ replacement.status = OrderStatus::New;
249
+
250
+ insertResting(replacement);
251
+ report = {replacement.orderId, replacement.clOrdId, OrderStatus::New,
252
+ 0, newQty, 0, 0, 0};
253
+ return true;
254
+ }
255
+
256
+ // ── Insert resting order ───────────────────────────────────���───────
257
+
258
+ void OrderBook::insertResting(Order& order) {
259
+ orderIndex_[order.orderId] = {order.side, order.price};
260
+ if (order.side == Side::Buy) {
261
+ bids_[order.price].push_back(order);
262
+ } else {
263
+ asks_[order.price].push_back(order);
264
+ }
265
+ }
266
+
267
+ void OrderBook::removeOrder(OrderId_t orderId, Side side, Price_t price) {
268
+ orderIndex_.erase(orderId);
269
+ if (side == Side::Buy) {
270
+ auto it = bids_.find(price);
271
+ if (it != bids_.end()) {
272
+ auto& vec = it->second;
273
+ vec.erase(std::remove_if(vec.begin(), vec.end(),
274
+ [orderId](const Order& o) { return o.orderId == orderId; }), vec.end());
275
+ if (vec.empty()) bids_.erase(it);
276
+ }
277
+ } else {
278
+ auto it = asks_.find(price);
279
+ if (it != asks_.end()) {
280
+ auto& vec = it->second;
281
+ vec.erase(std::remove_if(vec.begin(), vec.end(),
282
+ [orderId](const Order& o) { return o.orderId == orderId; }), vec.end());
283
+ if (vec.empty()) asks_.erase(it);
284
+ }
285
+ }
286
+ }
287
+
288
+ // ── Query helpers ──────────────────────────────────────────────────
289
+
290
+ template<typename MapT>
291
+ std::vector<OrderBook::Level> OrderBook::getLevels(const MapT& map, int depth) const {
292
+ std::vector<Level> result;
293
+ result.reserve(depth);
294
+ int count = 0;
295
+ for (auto& [price, orders] : map) {
296
+ if (count >= depth) break;
297
+ Quantity_t totalQty = 0;
298
+ for (auto& o : orders) totalQty += o.remainingQty;
299
+ result.push_back({price, totalQty, static_cast<int>(orders.size())});
300
+ ++count;
301
+ }
302
+ return result;
303
+ }
304
+
305
+ std::vector<OrderBook::Level> OrderBook::getBids(int depth) const {
306
+ return getLevels(bids_, depth);
307
+ }
308
+
309
+ std::vector<OrderBook::Level> OrderBook::getAsks(int depth) const {
310
+ return getLevels(asks_, depth);
311
+ }
312
+
313
+ size_t OrderBook::bidCount() const {
314
+ size_t count = 0;
315
+ for (auto& [_, orders] : bids_) count += orders.size();
316
+ return count;
317
+ }
318
+
319
+ size_t OrderBook::askCount() const {
320
+ size_t count = 0;
321
+ for (auto& [_, orders] : asks_) count += orders.size();
322
+ return count;
323
+ }
324
+
325
+ } // namespace eunex
src/common/OrderBook.hpp ADDED
@@ -0,0 +1,79 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #pragma once
2
+ #include "common/Types.hpp"
3
+ #include <vector>
4
+ #include <map>
5
+ #include <unordered_map>
6
+ #include <functional>
7
+
8
+ namespace eunex {
9
+
10
+ // ── Trade callback type ────────────────────────────────────────────
11
+ using TradeCallback = std::function<void(const Trade&)>;
12
+ using ExecCallback = std::function<void(const ExecutionReport&)>;
13
+
14
+ // ── Price-Time Priority Order Book ─────────────────────────────────
15
+ //
16
+ // Mirrors StockEx matcher.py logic but uses sorted std::map for O(log N)
17
+ // insert/match instead of Python list.sort() on every match.
18
+ //
19
+ // In Optiq, each OrderBookActor owns exactly one OrderBook instance.
20
+ // The actor guarantees single-threaded access β€” no locks needed.
21
+ //
22
+ class OrderBook {
23
+ public:
24
+ explicit OrderBook(SymbolIndex_t symbolIdx);
25
+
26
+ // Insert a new order and attempt matching. Returns executions via callbacks.
27
+ void newOrder(Order& order, const TradeCallback& onTrade, const ExecCallback& onExec);
28
+
29
+ // Cancel resting order by orderId.
30
+ bool cancelOrder(OrderId_t orderId, ExecutionReport& report);
31
+
32
+ // Modify resting order (cancel-replace). Price change loses priority.
33
+ bool modifyOrder(OrderId_t orderId, Price_t newPrice, Quantity_t newQty,
34
+ ExecutionReport& report);
35
+
36
+ // ── Queries ────────────────────────────────────────────────────
37
+ struct Level {
38
+ Price_t price;
39
+ Quantity_t totalQty;
40
+ int orderCount;
41
+ };
42
+
43
+ std::vector<Level> getBids(int depth = 10) const;
44
+ std::vector<Level> getAsks(int depth = 10) const;
45
+ size_t bidCount() const;
46
+ size_t askCount() const;
47
+
48
+ SymbolIndex_t symbolIndex() const { return symbolIdx_; }
49
+
50
+ private:
51
+ SymbolIndex_t symbolIdx_;
52
+ TradeId_t nextTradeId_ = 1;
53
+ OrderId_t nextOrderId_ = 1;
54
+
55
+ // Price levels: map<price, vector<Order*>>
56
+ // Bids: descending price (std::greater), then FIFO within level
57
+ // Asks: ascending price (std::less), then FIFO within level
58
+ using BidMap = std::map<Price_t, std::vector<Order>, std::greater<Price_t>>;
59
+ using AskMap = std::map<Price_t, std::vector<Order>, std::less<Price_t>>;
60
+
61
+ BidMap bids_;
62
+ AskMap asks_;
63
+
64
+ // orderId β†’ pointer to resting order for O(1) cancel/modify lookup
65
+ std::unordered_map<OrderId_t, std::pair<Side, Price_t>> orderIndex_;
66
+
67
+ OrderId_t allocateOrderId() { return nextOrderId_++; }
68
+ TradeId_t allocateTradeId() { return nextTradeId_++; }
69
+
70
+ void matchBuy(Order& incoming, const TradeCallback& onTrade, const ExecCallback& onExec);
71
+ void matchSell(Order& incoming, const TradeCallback& onTrade, const ExecCallback& onExec);
72
+ void insertResting(Order& order);
73
+ void removeOrder(OrderId_t orderId, Side side, Price_t price);
74
+
75
+ template<typename MapT>
76
+ std::vector<Level> getLevels(const MapT& map, int depth) const;
77
+ };
78
+
79
+ } // namespace eunex
src/common/Types.hpp ADDED
@@ -0,0 +1,131 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #pragma once
2
+ #include <cstdint>
3
+ #include <string>
4
+ #include <chrono>
5
+ #include <limits>
6
+
7
+ namespace eunex {
8
+
9
+ // ── Price representation (fixed-point, 8 decimal places) ───────────
10
+ // Optiq uses integer-scaled prices to avoid floating-point in the hot path.
11
+ // 1 price unit = 10^-8. E.g., price 12.50 EUR = 1'250'000'000.
12
+ using Price_t = int64_t;
13
+ constexpr int64_t PRICE_SCALE = 100'000'000LL;
14
+ constexpr Price_t NULL_PRICE = std::numeric_limits<int64_t>::min();
15
+
16
+ inline Price_t toFixedPrice(double p) {
17
+ return static_cast<Price_t>(p * PRICE_SCALE);
18
+ }
19
+ inline double toDouble(Price_t p) {
20
+ return static_cast<double>(p) / PRICE_SCALE;
21
+ }
22
+
23
+ // ── Quantity ────────────────────────────────────────────────────────
24
+ using Quantity_t = uint64_t;
25
+
26
+ // ── Identifiers ────────────────────────────────────────────────────
27
+ using OrderId_t = uint64_t;
28
+ using ClOrdId_t = uint64_t;
29
+ using SymbolIndex_t = uint32_t;
30
+ using TradeId_t = uint64_t;
31
+ using SessionId_t = uint16_t;
32
+
33
+ // ── Enumerations matching Optiq/SBE definitions ────────────────────
34
+ enum class Side : uint8_t {
35
+ Buy = 1,
36
+ Sell = 2
37
+ };
38
+
39
+ enum class OrderType : uint8_t {
40
+ Market = 1,
41
+ Limit = 2
42
+ };
43
+
44
+ enum class TimeInForce : uint8_t {
45
+ Day = 0,
46
+ GTC = 1,
47
+ IOC = 3,
48
+ FOK = 4
49
+ };
50
+
51
+ enum class OrderStatus : uint8_t {
52
+ New = 0,
53
+ PartiallyFilled = 1,
54
+ Filled = 2,
55
+ Cancelled = 4,
56
+ Rejected = 8
57
+ };
58
+
59
+ enum class MessageType : uint8_t {
60
+ NewOrder = 1,
61
+ ModifyOrder = 2,
62
+ CancelOrder = 3,
63
+ MassCancel = 4
64
+ };
65
+
66
+ // ── Timestamps ─────────────────────────────────────────────────────
67
+ using Timestamp_ns = uint64_t;
68
+
69
+ inline Timestamp_ns nowNs() {
70
+ auto now = std::chrono::steady_clock::now();
71
+ return std::chrono::duration_cast<std::chrono::nanoseconds>(
72
+ now.time_since_epoch()).count();
73
+ }
74
+
75
+ // ── Order structure (compact, cache-friendly) ──────────────────────
76
+ // This is the resting order on the book. Incoming order events use
77
+ // the Event structs defined in actor headers.
78
+ #pragma pack(push, 1)
79
+ struct Order {
80
+ OrderId_t orderId;
81
+ ClOrdId_t clOrdId;
82
+ SymbolIndex_t symbolIdx;
83
+ Side side;
84
+ OrderType ordType;
85
+ TimeInForce tif;
86
+ Price_t price;
87
+ Quantity_t quantity;
88
+ Quantity_t remainingQty;
89
+ Timestamp_ns entryTime;
90
+ SessionId_t sessionId;
91
+ OrderStatus status;
92
+ };
93
+ #pragma pack(pop)
94
+
95
+ // ── Trade structure ────────────────────────────────────────────────
96
+ #pragma pack(push, 1)
97
+ struct Trade {
98
+ TradeId_t tradeId;
99
+ SymbolIndex_t symbolIdx;
100
+ Price_t price;
101
+ Quantity_t quantity;
102
+ OrderId_t buyOrderId;
103
+ OrderId_t sellOrderId;
104
+ ClOrdId_t buyClOrdId;
105
+ ClOrdId_t sellClOrdId;
106
+ Timestamp_ns matchTime;
107
+ };
108
+ #pragma pack(pop)
109
+
110
+ // ── Execution report ───────────────────────────────────────────
111
+ struct ExecutionReport {
112
+ OrderId_t orderId;
113
+ ClOrdId_t clOrdId;
114
+ OrderStatus status;
115
+ Quantity_t filledQty;
116
+ Quantity_t remainingQty;
117
+ Price_t lastPrice;
118
+ Quantity_t lastQty;
119
+ TradeId_t tradeId;
120
+ };
121
+
122
+ // ── Symbol definition ──────────────────────────────────────────────
123
+ struct SymbolDef {
124
+ SymbolIndex_t index;
125
+ char isin[12];
126
+ char mnemonic[8];
127
+ Price_t tickSize;
128
+ Quantity_t lotSize;
129
+ };
130
+
131
+ } // namespace eunex
src/engine/SimplxShim.hpp ADDED
@@ -0,0 +1,345 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #pragma once
2
+ // ════════════════════════════════════════════════════════════════════
3
+ // Simplx-Compatible Shim
4
+ //
5
+ // A lightweight, single-threaded emulation of the Simplx actor
6
+ // framework API. Allows development and testing without the real
7
+ // Simplx dependency. When EUNEX_REAL_SIMPLX is defined, this file
8
+ // is not used β€” #include "simplx.h" directly.
9
+ //
10
+ // Emulates: Actor, Event, Event::Pipe, Service, Callback, ActorId,
11
+ // Engine, StartSequence, CoreSet.
12
+ //
13
+ // Limitations vs real Simplx:
14
+ // - Single-threaded: all actors run on one "core"
15
+ // - No cross-core shared memory, no cache-line optimizations
16
+ // - No real CPU pinning or red/blue zone scheduling
17
+ // - Events delivered synchronously on push (not batched)
18
+ // ════════════════════════════════════════════════════════════════════
19
+
20
+ #ifdef EUNEX_REAL_SIMPLX
21
+ #include "simplx.h"
22
+ #else
23
+
24
+ #include <cstdint>
25
+ #include <functional>
26
+ #include <memory>
27
+ #include <vector>
28
+ #include <unordered_map>
29
+ #include <typeindex>
30
+ #include <typeinfo>
31
+ #include <cassert>
32
+ #include <iostream>
33
+ #include <thread>
34
+ #include <chrono>
35
+
36
+ namespace tredzone {
37
+
38
+ // ── Forward declarations ───────────────────────────────────────────
39
+ class Actor;
40
+ class Engine;
41
+
42
+ // ── ActorId ────────────────────────────────────────────────────────
43
+ struct ActorId {
44
+ uint64_t id = 0;
45
+ uint8_t coreId = 0;
46
+ bool operator==(const ActorId& o) const { return id == o.id; }
47
+ bool operator!=(const ActorId& o) const { return id != o.id; }
48
+ bool isNull() const { return id == 0; }
49
+ };
50
+
51
+ } // namespace tredzone
52
+
53
+ namespace std {
54
+ template<> struct hash<tredzone::ActorId> {
55
+ size_t operator()(const tredzone::ActorId& a) const { return hash<uint64_t>()(a.id); }
56
+ };
57
+ }
58
+
59
+ namespace tredzone {
60
+
61
+ // ── Global actor registry (shim-only) ──────────────────────────────
62
+ class ActorRegistry {
63
+ public:
64
+ static ActorRegistry& instance() {
65
+ static ActorRegistry reg;
66
+ return reg;
67
+ }
68
+
69
+ uint64_t nextId() { return ++nextId_; }
70
+
71
+ void registerActor(const ActorId& id, Actor* actor) {
72
+ actors_[id] = actor;
73
+ }
74
+
75
+ void unregisterActor(const ActorId& id) {
76
+ actors_.erase(id);
77
+ }
78
+
79
+ Actor* findActor(const ActorId& id) {
80
+ auto it = actors_.find(id);
81
+ return it != actors_.end() ? it->second : nullptr;
82
+ }
83
+
84
+ // Service registry
85
+ void registerService(std::type_index tag, const ActorId& id) {
86
+ services_[tag] = id;
87
+ }
88
+
89
+ ActorId getService(std::type_index tag) const {
90
+ auto it = services_.find(tag);
91
+ return it != services_.end() ? it->second : ActorId{};
92
+ }
93
+
94
+ private:
95
+ uint64_t nextId_ = 0;
96
+ std::unordered_map<ActorId, Actor*> actors_;
97
+ std::unordered_map<std::type_index, ActorId> services_;
98
+ };
99
+
100
+ // ── Service tag base ───────────────────────────────────────────────
101
+ struct AsyncService {};
102
+
103
+ // ── Actor base class ───────────────────────────────────────────────
104
+ class Actor {
105
+ public:
106
+ // ── Event base ─────────────────────────────────────────────────
107
+ struct Event {
108
+ ActorId sourceActorId_;
109
+
110
+ const ActorId& getSourceActorId() const { return sourceActorId_; }
111
+
112
+ // ── Pipe ───────────────────────────────────────────────────
113
+ class Pipe {
114
+ public:
115
+ Pipe(Actor& source, const ActorId& dest)
116
+ : source_(source), destId_(dest) {}
117
+
118
+ template<typename EventT, typename... Args>
119
+ EventT& push(Args&&... args) {
120
+ auto evt = std::make_unique<EventT>(std::forward<Args>(args)...);
121
+ evt->sourceActorId_ = source_.getActorId();
122
+ EventT& ref = *evt;
123
+
124
+ Actor* dest = ActorRegistry::instance().findActor(destId_);
125
+ if (dest) {
126
+ dest->deliverEvent(std::type_index(typeid(EventT)),
127
+ static_cast<Event*>(evt.release()));
128
+ } else {
129
+ source_.deliverUndelivered(std::type_index(typeid(EventT)),
130
+ static_cast<Event*>(evt.release()));
131
+ }
132
+ return ref;
133
+ }
134
+
135
+ void setDestinationActorId(const ActorId& id) { destId_ = id; }
136
+ const ActorId& getDestinationActorId() const { return destId_; }
137
+
138
+ private:
139
+ Actor& source_;
140
+ ActorId destId_;
141
+ };
142
+
143
+ // ── Batch (stub) ──────────────────────────────────────────
144
+ class Batch {
145
+ public:
146
+ Batch(Pipe&) {}
147
+ bool isPushCommitted(Pipe&) { return true; }
148
+ };
149
+ };
150
+
151
+ // ── Callback ───────────────────────────────────────────────────
152
+ class Callback {
153
+ public:
154
+ virtual ~Callback() = default;
155
+ virtual void onCallback() = 0;
156
+ };
157
+
158
+ Actor() {
159
+ actorId_.id = ActorRegistry::instance().nextId();
160
+ actorId_.coreId = 0;
161
+ ActorRegistry::instance().registerActor(actorId_, this);
162
+ }
163
+
164
+ virtual ~Actor() {
165
+ ActorRegistry::instance().unregisterActor(actorId_);
166
+ }
167
+
168
+ const ActorId& getActorId() const { return actorId_; }
169
+ uint8_t getCore() const { return actorId_.coreId; }
170
+
171
+ // Event handler registration
172
+ template<typename EventT, typename HandlerT>
173
+ void registerEventHandler(HandlerT& handler) {
174
+ eventHandlers_[std::type_index(typeid(EventT))] =
175
+ [&handler](Event* rawEvt) {
176
+ handler.onEvent(static_cast<const EventT&>(*rawEvt));
177
+ delete rawEvt;
178
+ };
179
+ }
180
+
181
+ template<typename EventT, typename HandlerT>
182
+ void registerUndeliveredEventHandler(HandlerT& handler) {
183
+ undeliveredHandlers_[std::type_index(typeid(EventT))] =
184
+ [&handler](Event* rawEvt) {
185
+ handler.onUndeliveredEvent(static_cast<const EventT&>(*rawEvt));
186
+ delete rawEvt;
187
+ };
188
+ }
189
+
190
+ void registerCallback(Callback& cb) {
191
+ pendingCallbacks_.push_back(&cb);
192
+ }
193
+
194
+ // Create child actors (same core in shim)
195
+ template<typename ActorT, typename... Args>
196
+ ActorId newUnreferencedActor(Args&&... args) {
197
+ auto actor = std::make_unique<ActorT>(std::forward<Args>(args)...);
198
+ ActorId id = actor->getActorId();
199
+ ownedActors_.push_back(std::move(actor));
200
+ return id;
201
+ }
202
+
203
+ template<typename ActorT, typename... Args>
204
+ std::shared_ptr<ActorT> newReferencedActor(Args&&... args) {
205
+ auto actor = std::make_shared<ActorT>(std::forward<Args>(args)...);
206
+ return actor;
207
+ }
208
+
209
+ void requestDestroy() { destroyRequested_ = true; }
210
+
211
+ // ── ServiceIndex (accessed via getEngine()) ────────────────────
212
+ class ServiceIndex {
213
+ public:
214
+ template<typename ServiceTag>
215
+ const ActorId& getServiceActorId() const {
216
+ static ActorId id = ActorRegistry::instance()
217
+ .getService(std::type_index(typeid(ServiceTag)));
218
+ return id;
219
+ }
220
+ };
221
+
222
+ // Shim: engine reference not needed, service index is global
223
+ struct EngineProxy {
224
+ ServiceIndex serviceIndex;
225
+ ServiceIndex& getServiceIndex() { return serviceIndex; }
226
+ };
227
+
228
+ EngineProxy& getEngine() {
229
+ static EngineProxy proxy;
230
+ return proxy;
231
+ }
232
+
233
+ void processPendingCallbacks() {
234
+ auto cbs = std::move(pendingCallbacks_);
235
+ pendingCallbacks_.clear();
236
+ for (auto* cb : cbs) {
237
+ cb->onCallback();
238
+ }
239
+ }
240
+
241
+ private:
242
+ friend class Engine;
243
+
244
+ ActorId actorId_;
245
+ bool destroyRequested_ = false;
246
+
247
+ using EventHandler = std::function<void(Event*)>;
248
+ std::unordered_map<std::type_index, EventHandler> eventHandlers_;
249
+ std::unordered_map<std::type_index, EventHandler> undeliveredHandlers_;
250
+ std::vector<Callback*> pendingCallbacks_;
251
+ std::vector<std::unique_ptr<Actor>> ownedActors_;
252
+
253
+ void deliverEvent(std::type_index type, Event* evt) {
254
+ auto it = eventHandlers_.find(type);
255
+ if (it != eventHandlers_.end()) {
256
+ it->second(evt);
257
+ } else {
258
+ delete evt;
259
+ }
260
+ }
261
+
262
+ void deliverUndelivered(std::type_index type, Event* evt) {
263
+ auto it = undeliveredHandlers_.find(type);
264
+ if (it != undeliveredHandlers_.end()) {
265
+ it->second(evt);
266
+ } else {
267
+ delete evt;
268
+ }
269
+ }
270
+ };
271
+
272
+ // ── Engine ─────────────────────────────────────────────────────────
273
+
274
+ class Engine {
275
+ public:
276
+ struct CoreSet {
277
+ void set(int) {}
278
+ };
279
+
280
+ static CoreSet FullCoreSet() { return CoreSet{}; }
281
+
282
+ struct StartSequence {
283
+ StartSequence() = default;
284
+ StartSequence(const CoreSet&) {}
285
+
286
+ template<typename ActorT, typename... Args>
287
+ void addActor(int coreId, Args&&... args) {
288
+ factories_.push_back([coreId, ... a = std::forward<Args>(args)]() mutable {
289
+ auto actor = std::make_unique<ActorT>(std::move(a)...);
290
+ actor->actorId_.coreId = static_cast<uint8_t>(coreId);
291
+ return actor;
292
+ });
293
+ }
294
+
295
+ template<typename ServiceTag, typename ActorT, typename... Args>
296
+ void addServiceActor(int coreId, Args&&... args) {
297
+ factories_.push_back([coreId, ... a = std::forward<Args>(args)]() mutable {
298
+ auto actor = std::make_unique<ActorT>(std::move(a)...);
299
+ actor->actorId_.coreId = static_cast<uint8_t>(coreId);
300
+ ActorRegistry::instance().registerService(
301
+ std::type_index(typeid(ServiceTag)), actor->getActorId());
302
+ return actor;
303
+ });
304
+ }
305
+
306
+ void setRedZoneCore(int) {}
307
+ void setBlueZoneCore(int) {}
308
+
309
+ std::vector<std::function<std::unique_ptr<Actor>()>> factories_;
310
+ };
311
+
312
+ explicit Engine(StartSequence& seq) {
313
+ for (auto& factory : seq.factories_) {
314
+ actors_.push_back(factory());
315
+ }
316
+ running_ = true;
317
+ }
318
+
319
+ ~Engine() {
320
+ running_ = false;
321
+ // Destroy in reverse order (services last)
322
+ while (!actors_.empty()) actors_.pop_back();
323
+ }
324
+
325
+ void runFor(std::chrono::milliseconds duration) {
326
+ auto end = std::chrono::steady_clock::now() + duration;
327
+ while (running_ && std::chrono::steady_clock::now() < end) {
328
+ for (auto& actor : actors_) {
329
+ actor->processPendingCallbacks();
330
+ }
331
+ std::this_thread::sleep_for(std::chrono::milliseconds(1));
332
+ }
333
+ }
334
+
335
+ void stop() { running_ = false; }
336
+ bool isRunning() const { return running_; }
337
+
338
+ private:
339
+ std::vector<std::unique_ptr<Actor>> actors_;
340
+ bool running_ = false;
341
+ };
342
+
343
+ } // namespace tredzone
344
+
345
+ #endif // EUNEX_REAL_SIMPLX
src/iaca/Fragment.hpp ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #pragma once
2
+ // ════════════════════════════════════════════════════════════════════
3
+ // IACA Fragment definitions
4
+ //
5
+ // Optiq equivalent: Fragment structure from IacaDirectory.hpp
6
+ // - Each processing step emits a fragment
7
+ // - Fragments form a tree via previousOrigin / nextCount
8
+ // - Complete trees are assembled into IA SBE messages
9
+ // ════════════════════════════════════════════════════════════════════
10
+
11
+ #include "common/Types.hpp"
12
+ #include <cstdint>
13
+
14
+ namespace eunex::iaca {
15
+
16
+ struct Origin {
17
+ uint16_t originId;
18
+ uint32_t key;
19
+ uint64_t sequenceNumber;
20
+
21
+ bool isNull() const { return originId == 0xFFFF && key == 0 && sequenceNumber == 0; }
22
+
23
+ static Origin null() { return {0xFFFF, 0, 0}; }
24
+
25
+ bool operator==(const Origin& o) const {
26
+ return originId == o.originId && key == o.key && sequenceNumber == o.sequenceNumber;
27
+ }
28
+ };
29
+
30
+ using ChainId_t = uint64_t;
31
+
32
+ // Origin IDs matching Optiq component names
33
+ enum OriginComponent : uint16_t {
34
+ ORIGIN_BOOK = 1,
35
+ ORIGIN_LOGICAL_CORE = 2,
36
+ ORIGIN_MD_LIMIT = 3,
37
+ ORIGIN_MD_IMP = 4,
38
+ ORIGIN_OE_ACTOR = 5
39
+ };
40
+
41
+ // Cause IDs identifying the payload type
42
+ enum CauseId : int16_t {
43
+ CAUSE_NEW_ORDER_BUY = 1,
44
+ CAUSE_NEW_ORDER_SELL = 2,
45
+ CAUSE_CANCEL_ORDER = 3,
46
+ CAUSE_MODIFY_ORDER = 4,
47
+ CAUSE_ACK_DATA = 10,
48
+ CAUSE_TRADE_DATA = 11,
49
+ CAUSE_LIMIT_UPDATE = 20,
50
+ CAUSE_IMP_RESULT = 21,
51
+ CAUSE_MARKET_STATUS = 30
52
+ };
53
+
54
+ struct IacaFragment {
55
+ ChainId_t chainId;
56
+ Origin origin;
57
+ Origin previousOrigin;
58
+ int nextCount;
59
+ CauseId causeId;
60
+ uint8_t payload[2048];
61
+ size_t payloadSize;
62
+ };
63
+
64
+ // Identity data embedded in cross-actor events (Optiq: FragmentIdentityData)
65
+ struct FragmentIdentityData {
66
+ ChainId_t iacaChainId;
67
+ Origin iacaOrigin;
68
+ ChainId_t recoveryChainId;
69
+ Origin recoveryOrigin;
70
+ };
71
+
72
+ } // namespace eunex::iaca
src/iaca/IacaAggregator.cpp ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #include "iaca/IacaAggregator.hpp"
2
+ #include <algorithm>
3
+
4
+ namespace eunex::iaca {
5
+
6
+ bool FragmentChain::checkComplete() const {
7
+ for (auto& frag : fragments) {
8
+ int childCount = 0;
9
+ for (auto& other : fragments) {
10
+ if (other.previousOrigin == frag.origin)
11
+ ++childCount;
12
+ }
13
+ if (childCount != frag.nextCount)
14
+ return false;
15
+ }
16
+ return !fragments.empty();
17
+ }
18
+
19
+ void IacaAggregator::addFragment(const IacaFragment& fragment) {
20
+ auto& chain = chains_[fragment.chainId];
21
+ chain.chainId = fragment.chainId;
22
+ chain.fragments.push_back(fragment);
23
+ tryComplete(fragment.chainId);
24
+ }
25
+
26
+ void IacaAggregator::registerHandler(std::shared_ptr<FragmentHandler> handler) {
27
+ handlers_.push_back(std::move(handler));
28
+ }
29
+
30
+ void IacaAggregator::tryComplete(ChainId_t chainId) {
31
+ auto it = chains_.find(chainId);
32
+ if (it == chains_.end()) return;
33
+
34
+ auto& chain = it->second;
35
+ if (!chain.checkComplete()) return;
36
+
37
+ chain.complete = true;
38
+ ++completedCount_;
39
+
40
+ for (auto& handler : handlers_) {
41
+ if (handler->canProcess(chain)) {
42
+ handler->process(chain);
43
+ }
44
+ }
45
+
46
+ chains_.erase(it);
47
+ }
48
+
49
+ bool NewOrderHandler::canProcess(const FragmentChain& chain) const {
50
+ return std::any_of(chain.fragments.begin(), chain.fragments.end(),
51
+ [](const IacaFragment& f) {
52
+ return f.causeId == CAUSE_NEW_ORDER_BUY || f.causeId == CAUSE_NEW_ORDER_SELL;
53
+ });
54
+ }
55
+
56
+ void NewOrderHandler::process(const FragmentChain& chain) {
57
+ if (callback_) callback_(chain);
58
+ }
59
+
60
+ } // namespace eunex::iaca
src/iaca/IacaAggregator.hpp ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #pragma once
2
+ // ════════════════════════════════════════════════════════════════════
3
+ // IacaAggregator β€” Fragment chain collector and IA message generator
4
+ //
5
+ // Optiq equivalent: IacaAggregatorActor.hpp
6
+ // - Receives fragments from all component proxies
7
+ // - Builds CoherentFragmentChain per chainId
8
+ // - Detects chain completion (all nextCount satisfied)
9
+ // - Invokes FragmentHandlers to generate IA SBE messages
10
+ // ════════════════════════════════════════════════════════════════════
11
+
12
+ #include "iaca/Fragment.hpp"
13
+ #include <vector>
14
+ #include <unordered_map>
15
+ #include <functional>
16
+
17
+ namespace eunex::iaca {
18
+
19
+ // ── A chain being assembled ────────────────────────────────────────
20
+ struct FragmentChain {
21
+ ChainId_t chainId;
22
+ std::vector<IacaFragment> fragments;
23
+ bool complete = false;
24
+
25
+ bool checkComplete() const;
26
+ };
27
+
28
+ // ── Handler interface (like Optiq FragmentHandler) ──────────────────
29
+ class FragmentHandler {
30
+ public:
31
+ virtual ~FragmentHandler() = default;
32
+ virtual bool canProcess(const FragmentChain& chain) const = 0;
33
+ virtual void process(const FragmentChain& chain) = 0;
34
+ };
35
+
36
+ // ── Aggregator ─────────────────────────────────────────────────────
37
+ class IacaAggregator {
38
+ public:
39
+ void addFragment(const IacaFragment& fragment);
40
+ void registerHandler(std::shared_ptr<FragmentHandler> handler);
41
+
42
+ size_t pendingChainCount() const { return chains_.size(); }
43
+ size_t completedChainCount() const { return completedCount_; }
44
+
45
+ private:
46
+ std::unordered_map<ChainId_t, FragmentChain> chains_;
47
+ std::vector<std::shared_ptr<FragmentHandler>> handlers_;
48
+ size_t completedCount_ = 0;
49
+
50
+ void tryComplete(ChainId_t chainId);
51
+ };
52
+
53
+ // ── Example handler: NewOrder IA message generator ─────────────────
54
+ class NewOrderHandler : public FragmentHandler {
55
+ public:
56
+ using Callback = std::function<void(const FragmentChain&)>;
57
+ explicit NewOrderHandler(Callback cb) : callback_(std::move(cb)) {}
58
+
59
+ bool canProcess(const FragmentChain& chain) const override;
60
+ void process(const FragmentChain& chain) override;
61
+
62
+ private:
63
+ Callback callback_;
64
+ };
65
+
66
+ } // namespace eunex::iaca
src/main.cpp ADDED
@@ -0,0 +1,156 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // ════════════════════════════════════════════════════════════════════
2
+ // EuNEx Matching Engine β€” Main Entry Point
3
+ //
4
+ // Sets up the actor topology and runs the matching engine.
5
+ //
6
+ // Actor topology (mirrors Optiq architecture):
7
+ //
8
+ // Core 0: OEGatewayActor (Order Entry β€” receives external orders)
9
+ // Core 1: OrderBookActor per symbol (matching engine)
10
+ // Core 2: MarketDataActor (publishes book updates, trades)
11
+ //
12
+ // StockEx equivalent topology:
13
+ // fix_oeg β†’ Kafka 'orders' β†’ matcher.py β†’ Kafka 'trades' β†’ dashboard.py
14
+ //
15
+ // Optiq equivalent topology:
16
+ // OEActor β†’ LogicalCoreActor (Book) β†’ MDLimit β†’ MDIMP
17
+ // β†’ OE Ack (back to OEActor)
18
+ // ════════════════════════════════════════════════════════════════════
19
+
20
+ #include "engine/SimplxShim.hpp"
21
+ #include "actors/OrderBookActor.hpp"
22
+ #include "actors/OEGatewayActor.hpp"
23
+ #include "actors/MarketDataActor.hpp"
24
+ #include <iostream>
25
+
26
+ using namespace tredzone;
27
+ using namespace eunex;
28
+
29
+ int main() {
30
+ std::cout << "═══════════════════════════════════════════\n";
31
+ std::cout << " EuNEx Matching Engine v0.1\n";
32
+ std::cout << " Actor-based (Simplx-compatible)\n";
33
+ std::cout << "═══════════════════════════════════════════\n\n";
34
+
35
+ // ── Build actor topology ───────────────────────────────────────
36
+ // In production Optiq, this is done via Engine::StartSequence
37
+ // with actors pinned to specific CPU cores.
38
+
39
+ // Create actors manually (shim mode β€” single thread)
40
+ auto oeGateway = std::make_unique<OEGatewayActor>();
41
+ auto mdActor = std::make_unique<MarketDataActor>();
42
+
43
+ // Create order books for two symbols
44
+ constexpr SymbolIndex_t SYM_AAPL = 1;
45
+ constexpr SymbolIndex_t SYM_MSFT = 2;
46
+
47
+ auto bookAAPL = std::make_unique<OrderBookActor>(
48
+ SYM_AAPL, oeGateway->getActorId(), mdActor->getActorId());
49
+ auto bookMSFT = std::make_unique<OrderBookActor>(
50
+ SYM_MSFT, oeGateway->getActorId(), mdActor->getActorId());
51
+
52
+ oeGateway->mapSymbol(SYM_AAPL, bookAAPL->getActorId());
53
+ oeGateway->mapSymbol(SYM_MSFT, bookMSFT->getActorId());
54
+
55
+ std::cout << "Actors created:\n";
56
+ std::cout << " OEGateway (id=" << oeGateway->getActorId().id << ")\n";
57
+ std::cout << " MarketData (id=" << mdActor->getActorId().id << ")\n";
58
+ std::cout << " Book AAPL (id=" << bookAAPL->getActorId().id << ")\n";
59
+ std::cout << " Book MSFT (id=" << bookMSFT->getActorId().id << ")\n\n";
60
+
61
+ // ── Submit some orders ─────────────────────────────────────────
62
+ // Equivalent to StockEx clearing house members sending orders
63
+
64
+ SessionId_t session = 1;
65
+
66
+ std::cout << "── Submitting orders ──────────────────────\n\n";
67
+
68
+ // Sell order: AAPL @ 150.00, qty 100
69
+ oeGateway->submitNewOrder(1001, SYM_AAPL, Side::Sell, OrderType::Limit,
70
+ TimeInForce::Day, toFixedPrice(150.00), 100, session);
71
+ std::cout << "SELL AAPL 100 @ 150.00\n";
72
+
73
+ // Sell order: AAPL @ 151.00, qty 50
74
+ oeGateway->submitNewOrder(1002, SYM_AAPL, Side::Sell, OrderType::Limit,
75
+ TimeInForce::Day, toFixedPrice(151.00), 50, session);
76
+ std::cout << "SELL AAPL 50 @ 151.00\n";
77
+
78
+ // Buy order: AAPL @ 150.00, qty 75 β€” should match 75 of the first sell
79
+ oeGateway->submitNewOrder(1003, SYM_AAPL, Side::Buy, OrderType::Limit,
80
+ TimeInForce::Day, toFixedPrice(150.00), 75, session);
81
+ std::cout << "BUY AAPL 75 @ 150.00 (should match 75 of sell@150)\n";
82
+
83
+ // Market buy: AAPL, qty 30 β€” should match remaining 25@150 + 5@151
84
+ oeGateway->submitNewOrder(1004, SYM_AAPL, Side::Buy, OrderType::Market,
85
+ TimeInForce::IOC, NULL_PRICE, 30, session);
86
+ std::cout << "BUY AAPL 30 MARKET IOC (should match 25@150 + 5@151)\n";
87
+
88
+ // FOK buy that should fail: AAPL 100@151 (only 45 available)
89
+ oeGateway->submitNewOrder(1005, SYM_AAPL, Side::Buy, OrderType::Limit,
90
+ TimeInForce::FOK, toFixedPrice(151.00), 100, session);
91
+ std::cout << "BUY AAPL 100 @ 151.00 FOK (should be rejected)\n";
92
+
93
+ // MSFT orders
94
+ oeGateway->submitNewOrder(2001, SYM_MSFT, Side::Buy, OrderType::Limit,
95
+ TimeInForce::Day, toFixedPrice(320.50), 200, session);
96
+ std::cout << "BUY MSFT 200 @ 320.50\n";
97
+
98
+ oeGateway->submitNewOrder(2002, SYM_MSFT, Side::Sell, OrderType::Limit,
99
+ TimeInForce::Day, toFixedPrice(320.50), 150, session);
100
+ std::cout << "SELL MSFT 150 @ 320.50 (should match 150)\n";
101
+
102
+ // ── Print results ──────────────────────────────────────────────
103
+ std::cout << "\n── Execution Reports ─────────────────────\n\n";
104
+
105
+ auto statusStr = [](OrderStatus s) -> const char* {
106
+ switch (s) {
107
+ case OrderStatus::New: return "NEW";
108
+ case OrderStatus::PartiallyFilled: return "PARTIAL";
109
+ case OrderStatus::Filled: return "FILLED";
110
+ case OrderStatus::Cancelled: return "CANCELLED";
111
+ case OrderStatus::Rejected: return "REJECTED";
112
+ default: return "UNKNOWN";
113
+ }
114
+ };
115
+
116
+ for (auto& rpt : oeGateway->getReports()) {
117
+ std::cout << " ClOrdId=" << rpt.clOrdId
118
+ << " OrderId=" << rpt.orderId
119
+ << " Status=" << statusStr(rpt.status)
120
+ << " Filled=" << rpt.filledQty
121
+ << " Remaining=" << rpt.remainingQty;
122
+ if (rpt.lastQty > 0) {
123
+ std::cout << " LastPx=" << toDouble(rpt.lastPrice)
124
+ << " LastQty=" << rpt.lastQty;
125
+ }
126
+ std::cout << "\n";
127
+ }
128
+
129
+ // ── Print market data ──────────────────────────────────────────
130
+ std::cout << "\n── Market Data ───────────────────────────\n\n";
131
+
132
+ auto printSnapshot = [&](SymbolIndex_t sym, const char* name) {
133
+ auto* snap = mdActor->getSnapshot(sym);
134
+ if (snap) {
135
+ std::cout << " " << name << ":"
136
+ << " LastPx=" << toDouble(snap->lastTradePrice)
137
+ << " BestBid=" << toDouble(snap->bestBid)
138
+ << " BestAsk=" << toDouble(snap->bestAsk)
139
+ << " Trades=" << snap->tradeCount << "\n";
140
+ }
141
+ };
142
+
143
+ printSnapshot(SYM_AAPL, "AAPL");
144
+ printSnapshot(SYM_MSFT, "MSFT");
145
+
146
+ std::cout << "\n Recent trades: " << mdActor->getRecentTrades().size() << "\n";
147
+ for (auto& t : mdActor->getRecentTrades()) {
148
+ const char* sym = (t.symbolIdx == SYM_AAPL) ? "AAPL" : "MSFT";
149
+ std::cout << " " << sym << " " << t.quantity << " @ " << toDouble(t.price)
150
+ << " (buy=" << t.buyOrderId << " sell=" << t.sellOrderId << ")\n";
151
+ }
152
+
153
+ std::cout << "\n═══════════════════════════════════════════\n";
154
+ std::cout << " Engine stopped.\n";
155
+ return 0;
156
+ }
src/recovery/RecoveryProxy.cpp ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ // RecoveryProxy is header-only for now (template-heavy).
2
+ // This file exists for the CMake target.
3
+ #include "recovery/RecoveryProxy.hpp"
src/recovery/RecoveryProxy.hpp ADDED
@@ -0,0 +1,106 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #pragma once
2
+ // ════════════════════════════════════════════════════════════════════
3
+ // RecoveryProxy β€” Simplified recovery layer
4
+ //
5
+ // Optiq equivalent: RecoveryProxy.hpp
6
+ // - Wraps every incoming event in a Recovery Cause
7
+ // - Persists fragment to Kafka (here: to in-memory log)
8
+ // - Gates Effects based on Master/Mirror role
9
+ //
10
+ // This is a teaching implementation that preserves the Optiq
11
+ // Cause-Effect API shape while storing fragments in memory.
12
+ // ════════════════════════════════════════════════════════════════════
13
+
14
+ #include "common/Types.hpp"
15
+ #include <vector>
16
+ #include <functional>
17
+ #include <cstring>
18
+ #include <iostream>
19
+
20
+ namespace eunex::recovery {
21
+
22
+ // ── Recovery Fragment (matches Optiq fragment structure) ────────────
23
+ struct Fragment {
24
+ uint64_t sequenceNumber;
25
+ uint16_t originId;
26
+ uint32_t originKey;
27
+ uint64_t chainId;
28
+ uint8_t persistenceId;
29
+ int nextCount;
30
+ uint8_t payload[4096];
31
+ size_t payloadSize;
32
+ };
33
+
34
+ // ── Fragment Store (replaces Kafka in this prototype) ───────────────
35
+ class FragmentStore {
36
+ public:
37
+ void append(const Fragment& frag) {
38
+ fragments_.push_back(frag);
39
+ }
40
+
41
+ const std::vector<Fragment>& fragments() const { return fragments_; }
42
+ size_t size() const { return fragments_.size(); }
43
+ void clear() { fragments_.clear(); }
44
+
45
+ private:
46
+ std::vector<Fragment> fragments_;
47
+ };
48
+
49
+ // ── RecoveryProxy ──────────────────────────────────────────────────
50
+ class RecoveryProxy {
51
+ public:
52
+ RecoveryProxy(uint16_t originId, uint32_t originKey, FragmentStore& store, bool isMaster)
53
+ : originId_(originId), originKey_(originKey), store_(store), isMaster_(isMaster) {}
54
+
55
+ // ── Cause: entry point for every state-changing event ──────────
56
+ // Returns the fragment sequence number for chaining.
57
+ template<typename Payload, typename CauseOp>
58
+ uint64_t cause(uint8_t persistenceId, const Payload& payload, CauseOp&& op) {
59
+ Fragment frag{};
60
+ frag.sequenceNumber = ++sequenceNumber_;
61
+ frag.originId = originId_;
62
+ frag.originKey = originKey_;
63
+ frag.chainId = frag.sequenceNumber;
64
+ frag.persistenceId = persistenceId;
65
+ frag.payloadSize = sizeof(Payload);
66
+ std::memcpy(frag.payload, &payload, sizeof(Payload));
67
+
68
+ // Execute business logic β€” returns nextCount
69
+ int nextCount = op(frag.chainId, frag.sequenceNumber);
70
+ frag.nextCount = nextCount;
71
+
72
+ // Persist (on both Master and Mirror during replay)
73
+ store_.append(frag);
74
+
75
+ return frag.sequenceNumber;
76
+ }
77
+
78
+ // ── Effect: executes only on Master ────────────────────────────
79
+ template<typename Fn, typename... Args>
80
+ void effect(Fn&& fn, Args&&... args) {
81
+ if (isMaster_) {
82
+ fn(std::forward<Args>(args)...);
83
+ }
84
+ }
85
+
86
+ // ── RecoveryEffect: executes only on Mirror ────────────────────
87
+ template<typename Fn, typename... Args>
88
+ void recoveryEffect(Fn&& fn, Args&&... args) {
89
+ if (!isMaster_) {
90
+ fn(std::forward<Args>(args)...);
91
+ }
92
+ }
93
+
94
+ bool isMaster() const { return isMaster_; }
95
+ void setMaster(bool v) { isMaster_ = v; }
96
+ uint64_t lastSequence() const { return sequenceNumber_; }
97
+
98
+ private:
99
+ uint16_t originId_;
100
+ uint32_t originKey_;
101
+ FragmentStore& store_;
102
+ bool isMaster_;
103
+ uint64_t sequenceNumber_ = 0;
104
+ };
105
+
106
+ } // namespace eunex::recovery
tests/test_matching_engine.cpp ADDED
@@ -0,0 +1,142 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // ════════════════════════════════════════════════════════════════════
2
+ // Integration test: full actor-based matching engine
3
+ //
4
+ // Tests the OEGateway β†’ OrderBook β†’ MarketData actor pipeline.
5
+ // Verifies that the actor topology produces correct trades,
6
+ // execution reports, and market data snapshots.
7
+ // ════════════════════════════════════════════════════════════════════
8
+
9
+ #include "actors/OrderBookActor.hpp"
10
+ #include "actors/OEGatewayActor.hpp"
11
+ #include "actors/MarketDataActor.hpp"
12
+ #include <iostream>
13
+ #include <cassert>
14
+
15
+ using namespace eunex;
16
+
17
+ static int testsPassed = 0;
18
+ static int testsFailed = 0;
19
+
20
+ #define TEST(name) \
21
+ std::cout << " " << #name << "... "; \
22
+ try { test_##name(); std::cout << "PASS\n"; ++testsPassed; } \
23
+ catch (const std::exception& e) { std::cout << "FAIL: " << e.what() << "\n"; ++testsFailed; }
24
+
25
+ #define ASSERT_EQ(a, b) \
26
+ if ((a) != (b)) throw std::runtime_error( \
27
+ std::string("Expected ") + std::to_string(static_cast<long long>(b)) + " got " + std::to_string(static_cast<long long>(a)))
28
+
29
+ #define ASSERT_TRUE(x) \
30
+ if (!(x)) throw std::runtime_error("Assertion failed: " #x)
31
+
32
+ // ── Test fixtures ──────────────────────────────────────────────────
33
+
34
+ struct Fixture {
35
+ std::unique_ptr<OEGatewayActor> oeGateway;
36
+ std::unique_ptr<MarketDataActor> mdActor;
37
+ std::unique_ptr<OrderBookActor> book;
38
+ static constexpr SymbolIndex_t SYM = 1;
39
+ static constexpr SessionId_t SESS = 1;
40
+
41
+ Fixture() {
42
+ oeGateway = std::make_unique<OEGatewayActor>();
43
+ mdActor = std::make_unique<MarketDataActor>();
44
+ book = std::make_unique<OrderBookActor>(
45
+ SYM, oeGateway->getActorId(), mdActor->getActorId());
46
+ oeGateway->mapSymbol(SYM, book->getActorId());
47
+ }
48
+ };
49
+
50
+ // ── Tests ──────────────────────────────────────────────────────────
51
+
52
+ void test_order_routed_to_book() {
53
+ Fixture f;
54
+ f.oeGateway->submitNewOrder(1, f.SYM, Side::Buy, OrderType::Limit,
55
+ TimeInForce::Day, toFixedPrice(100.0), 50, f.SESS);
56
+
57
+ ASSERT_TRUE(f.oeGateway->getReports().size() > 0);
58
+ ASSERT_EQ(f.oeGateway->getReports().back().status, OrderStatus::New);
59
+ }
60
+
61
+ void test_trade_reaches_market_data() {
62
+ Fixture f;
63
+
64
+ f.oeGateway->submitNewOrder(1, f.SYM, Side::Sell, OrderType::Limit,
65
+ TimeInForce::Day, toFixedPrice(50.0), 100, f.SESS);
66
+ f.oeGateway->submitNewOrder(2, f.SYM, Side::Buy, OrderType::Limit,
67
+ TimeInForce::Day, toFixedPrice(50.0), 60, f.SESS);
68
+
69
+ ASSERT_EQ(f.mdActor->getRecentTrades().size(), 1UL);
70
+ ASSERT_EQ(f.mdActor->getRecentTrades()[0].quantity, 60UL);
71
+ }
72
+
73
+ void test_market_data_snapshot_updated() {
74
+ Fixture f;
75
+
76
+ f.oeGateway->submitNewOrder(1, f.SYM, Side::Sell, OrderType::Limit,
77
+ TimeInForce::Day, toFixedPrice(50.0), 100, f.SESS);
78
+ f.oeGateway->submitNewOrder(2, f.SYM, Side::Buy, OrderType::Limit,
79
+ TimeInForce::Day, toFixedPrice(49.0), 100, f.SESS);
80
+
81
+ auto* snap = f.mdActor->getSnapshot(f.SYM);
82
+ ASSERT_TRUE(snap != nullptr);
83
+ ASSERT_EQ(snap->bestBid, toFixedPrice(49.0));
84
+ ASSERT_EQ(snap->bestAsk, toFixedPrice(50.0));
85
+ }
86
+
87
+ void test_cancel_via_gateway() {
88
+ Fixture f;
89
+
90
+ f.oeGateway->submitNewOrder(1, f.SYM, Side::Sell, OrderType::Limit,
91
+ TimeInForce::Day, toFixedPrice(50.0), 100, f.SESS);
92
+
93
+ auto orderId = f.oeGateway->getReports().back().orderId;
94
+ f.oeGateway->clearReports();
95
+
96
+ f.oeGateway->submitCancel(orderId, 1, f.SYM, f.SESS);
97
+
98
+ ASSERT_TRUE(f.oeGateway->getReports().size() > 0);
99
+ ASSERT_EQ(f.oeGateway->getReports().back().status, OrderStatus::Cancelled);
100
+ }
101
+
102
+ void test_multiple_fills_generate_reports() {
103
+ Fixture f;
104
+
105
+ f.oeGateway->submitNewOrder(1, f.SYM, Side::Sell, OrderType::Limit,
106
+ TimeInForce::Day, toFixedPrice(50.0), 30, f.SESS);
107
+ f.oeGateway->submitNewOrder(2, f.SYM, Side::Sell, OrderType::Limit,
108
+ TimeInForce::Day, toFixedPrice(51.0), 30, f.SESS);
109
+ f.oeGateway->clearReports();
110
+
111
+ f.oeGateway->submitNewOrder(3, f.SYM, Side::Buy, OrderType::Limit,
112
+ TimeInForce::Day, toFixedPrice(51.0), 50, f.SESS);
113
+
114
+ // Should have reports for: resting sell fill(s) + incoming buy
115
+ ASSERT_TRUE(f.oeGateway->getReports().size() >= 2);
116
+ ASSERT_EQ(f.mdActor->getRecentTrades().size(), 2UL);
117
+ }
118
+
119
+ void test_unknown_symbol_ignored() {
120
+ Fixture f;
121
+ f.oeGateway->submitNewOrder(1, 999, Side::Buy, OrderType::Limit,
122
+ TimeInForce::Day, toFixedPrice(50.0), 50, f.SESS);
123
+ ASSERT_EQ(f.oeGateway->getReports().size(), 0UL);
124
+ }
125
+
126
+ // ── Main ───────────────────────────────────────────────────────────
127
+
128
+ int main() {
129
+ std::cout << "Matching Engine Integration Tests\n";
130
+ std::cout << "───────────────────────────────────────────\n";
131
+
132
+ TEST(order_routed_to_book);
133
+ TEST(trade_reaches_market_data);
134
+ TEST(market_data_snapshot_updated);
135
+ TEST(cancel_via_gateway);
136
+ TEST(multiple_fills_generate_reports);
137
+ TEST(unknown_symbol_ignored);
138
+
139
+ std::cout << "───────────────────────────────────────────\n";
140
+ std::cout << testsPassed << " passed, " << testsFailed << " failed\n";
141
+ return testsFailed > 0 ? 1 : 0;
142
+ }
tests/test_orderbook.cpp ADDED
@@ -0,0 +1,301 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // ════════════════════════════════════════════════════════════════════
2
+ // OrderBook unit tests
3
+ //
4
+ // Ported from StockEx matcher/test_matcher.py, adapted for C++
5
+ // price-time priority matching with fixed-point prices.
6
+ // ════════════════════════════════════════════════════════════════════
7
+
8
+ #include "common/OrderBook.hpp"
9
+ #include <iostream>
10
+ #include <cassert>
11
+ #include <vector>
12
+ #include <cmath>
13
+
14
+ using namespace eunex;
15
+
16
+ static int testsPassed = 0;
17
+ static int testsFailed = 0;
18
+
19
+ #define TEST(name) \
20
+ std::cout << " " << #name << "... "; \
21
+ try { test_##name(); std::cout << "PASS\n"; ++testsPassed; } \
22
+ catch (const std::exception& e) { std::cout << "FAIL: " << e.what() << "\n"; ++testsFailed; }
23
+
24
+ #define ASSERT_EQ(a, b) \
25
+ if ((a) != (b)) throw std::runtime_error( \
26
+ std::string("Expected ") + std::to_string(static_cast<long long>(b)) + " got " + std::to_string(static_cast<long long>(a)))
27
+
28
+ #define ASSERT_TRUE(x) \
29
+ if (!(x)) throw std::runtime_error("Assertion failed: " #x)
30
+
31
+ // ── Helpers ────────────────────────────────────────────────────────
32
+
33
+ static Order makeOrder(Side side, OrderType ot, TimeInForce tif,
34
+ double price, uint64_t qty, uint64_t clOrdId = 0) {
35
+ Order o{};
36
+ o.clOrdId = clOrdId;
37
+ o.symbolIdx = 1;
38
+ o.side = side;
39
+ o.ordType = ot;
40
+ o.tif = tif;
41
+ o.price = toFixedPrice(price);
42
+ o.quantity = qty;
43
+ return o;
44
+ }
45
+
46
+ static std::vector<Trade> trades;
47
+ static std::vector<ExecutionReport> reports;
48
+
49
+ static void resetCallbacks() {
50
+ trades.clear();
51
+ reports.clear();
52
+ }
53
+
54
+ static auto onTrade = [](const Trade& t) { trades.push_back(t); };
55
+ static auto onExec = [](const ExecutionReport& r) { reports.push_back(r); };
56
+
57
+ // ── Tests ──────────────────────────────────────────────────────────
58
+
59
+ void test_limit_buy_rests_on_empty_book() {
60
+ OrderBook book(1);
61
+ resetCallbacks();
62
+
63
+ auto order = makeOrder(Side::Buy, OrderType::Limit, TimeInForce::Day, 100.0, 50);
64
+ book.newOrder(order, onTrade, onExec);
65
+
66
+ ASSERT_EQ(trades.size(), 0UL);
67
+ ASSERT_EQ(book.bidCount(), 1UL);
68
+ ASSERT_EQ(book.askCount(), 0UL);
69
+ ASSERT_EQ(reports.back().status, OrderStatus::New);
70
+ }
71
+
72
+ void test_limit_sell_rests_on_empty_book() {
73
+ OrderBook book(1);
74
+ resetCallbacks();
75
+
76
+ auto order = makeOrder(Side::Sell, OrderType::Limit, TimeInForce::Day, 100.0, 50);
77
+ book.newOrder(order, onTrade, onExec);
78
+
79
+ ASSERT_EQ(trades.size(), 0UL);
80
+ ASSERT_EQ(book.bidCount(), 0UL);
81
+ ASSERT_EQ(book.askCount(), 1UL);
82
+ }
83
+
84
+ void test_exact_match() {
85
+ OrderBook book(1);
86
+ resetCallbacks();
87
+
88
+ auto sell = makeOrder(Side::Sell, OrderType::Limit, TimeInForce::Day, 50.0, 100);
89
+ book.newOrder(sell, onTrade, onExec);
90
+
91
+ resetCallbacks();
92
+ auto buy = makeOrder(Side::Buy, OrderType::Limit, TimeInForce::Day, 50.0, 100);
93
+ book.newOrder(buy, onTrade, onExec);
94
+
95
+ ASSERT_EQ(trades.size(), 1UL);
96
+ ASSERT_EQ(trades[0].quantity, 100UL);
97
+ ASSERT_EQ(book.bidCount(), 0UL);
98
+ ASSERT_EQ(book.askCount(), 0UL);
99
+ }
100
+
101
+ void test_partial_fill() {
102
+ OrderBook book(1);
103
+ resetCallbacks();
104
+
105
+ auto sell = makeOrder(Side::Sell, OrderType::Limit, TimeInForce::Day, 50.0, 100);
106
+ book.newOrder(sell, onTrade, onExec);
107
+
108
+ resetCallbacks();
109
+ auto buy = makeOrder(Side::Buy, OrderType::Limit, TimeInForce::Day, 50.0, 60);
110
+ book.newOrder(buy, onTrade, onExec);
111
+
112
+ ASSERT_EQ(trades.size(), 1UL);
113
+ ASSERT_EQ(trades[0].quantity, 60UL);
114
+ ASSERT_EQ(book.askCount(), 1UL);
115
+
116
+ auto asks = book.getAsks(1);
117
+ ASSERT_EQ(asks[0].totalQty, 40UL);
118
+ }
119
+
120
+ void test_price_priority() {
121
+ OrderBook book(1);
122
+ resetCallbacks();
123
+
124
+ // Two sells at different prices
125
+ auto sell1 = makeOrder(Side::Sell, OrderType::Limit, TimeInForce::Day, 51.0, 50);
126
+ auto sell2 = makeOrder(Side::Sell, OrderType::Limit, TimeInForce::Day, 50.0, 50);
127
+ book.newOrder(sell1, onTrade, onExec);
128
+ book.newOrder(sell2, onTrade, onExec);
129
+
130
+ resetCallbacks();
131
+ // Buy should match cheaper sell first
132
+ auto buy = makeOrder(Side::Buy, OrderType::Limit, TimeInForce::Day, 52.0, 30);
133
+ book.newOrder(buy, onTrade, onExec);
134
+
135
+ ASSERT_EQ(trades.size(), 1UL);
136
+ ASSERT_EQ(toDouble(trades[0].price), 50.0);
137
+ ASSERT_EQ(trades[0].quantity, 30UL);
138
+ }
139
+
140
+ void test_time_priority() {
141
+ OrderBook book(1);
142
+ resetCallbacks();
143
+
144
+ // Two sells at same price β€” first should match first (FIFO)
145
+ auto sell1 = makeOrder(Side::Sell, OrderType::Limit, TimeInForce::Day, 50.0, 50, 101);
146
+ auto sell2 = makeOrder(Side::Sell, OrderType::Limit, TimeInForce::Day, 50.0, 50, 102);
147
+ book.newOrder(sell1, onTrade, onExec);
148
+ book.newOrder(sell2, onTrade, onExec);
149
+
150
+ resetCallbacks();
151
+ auto buy = makeOrder(Side::Buy, OrderType::Limit, TimeInForce::Day, 50.0, 30);
152
+ book.newOrder(buy, onTrade, onExec);
153
+
154
+ ASSERT_EQ(trades.size(), 1UL);
155
+ ASSERT_EQ(trades[0].sellClOrdId, 101UL);
156
+ }
157
+
158
+ void test_market_order_matches_any_price() {
159
+ OrderBook book(1);
160
+ resetCallbacks();
161
+
162
+ auto sell = makeOrder(Side::Sell, OrderType::Limit, TimeInForce::Day, 999.99, 50);
163
+ book.newOrder(sell, onTrade, onExec);
164
+
165
+ resetCallbacks();
166
+ auto buy = makeOrder(Side::Buy, OrderType::Market, TimeInForce::IOC, 0, 30);
167
+ book.newOrder(buy, onTrade, onExec);
168
+
169
+ ASSERT_EQ(trades.size(), 1UL);
170
+ ASSERT_EQ(trades[0].quantity, 30UL);
171
+ }
172
+
173
+ void test_market_order_does_not_rest() {
174
+ OrderBook book(1);
175
+ resetCallbacks();
176
+
177
+ auto buy = makeOrder(Side::Buy, OrderType::Market, TimeInForce::IOC, 0, 50);
178
+ book.newOrder(buy, onTrade, onExec);
179
+
180
+ ASSERT_EQ(trades.size(), 0UL);
181
+ ASSERT_EQ(book.bidCount(), 0UL);
182
+ ASSERT_TRUE(reports.back().status == OrderStatus::Cancelled);
183
+ }
184
+
185
+ void test_ioc_partial_fill_cancel_rest() {
186
+ OrderBook book(1);
187
+ resetCallbacks();
188
+
189
+ auto sell = makeOrder(Side::Sell, OrderType::Limit, TimeInForce::Day, 50.0, 30);
190
+ book.newOrder(sell, onTrade, onExec);
191
+
192
+ resetCallbacks();
193
+ auto buy = makeOrder(Side::Buy, OrderType::Limit, TimeInForce::IOC, 50.0, 100);
194
+ book.newOrder(buy, onTrade, onExec);
195
+
196
+ ASSERT_EQ(trades.size(), 1UL);
197
+ ASSERT_EQ(trades[0].quantity, 30UL);
198
+ ASSERT_EQ(book.bidCount(), 0UL); // unfilled portion cancelled
199
+ }
200
+
201
+ void test_fok_rejected_when_insufficient() {
202
+ OrderBook book(1);
203
+ resetCallbacks();
204
+
205
+ auto sell = makeOrder(Side::Sell, OrderType::Limit, TimeInForce::Day, 50.0, 30);
206
+ book.newOrder(sell, onTrade, onExec);
207
+
208
+ resetCallbacks();
209
+ auto buy = makeOrder(Side::Buy, OrderType::Limit, TimeInForce::FOK, 50.0, 100);
210
+ book.newOrder(buy, onTrade, onExec);
211
+
212
+ ASSERT_EQ(trades.size(), 0UL);
213
+ ASSERT_TRUE(reports.back().status == OrderStatus::Rejected);
214
+ ASSERT_EQ(book.askCount(), 1UL); // sell still on book
215
+ }
216
+
217
+ void test_fok_fills_when_sufficient() {
218
+ OrderBook book(1);
219
+ resetCallbacks();
220
+
221
+ auto sell = makeOrder(Side::Sell, OrderType::Limit, TimeInForce::Day, 50.0, 100);
222
+ book.newOrder(sell, onTrade, onExec);
223
+
224
+ resetCallbacks();
225
+ auto buy = makeOrder(Side::Buy, OrderType::Limit, TimeInForce::FOK, 50.0, 100);
226
+ book.newOrder(buy, onTrade, onExec);
227
+
228
+ ASSERT_EQ(trades.size(), 1UL);
229
+ ASSERT_EQ(trades[0].quantity, 100UL);
230
+ ASSERT_EQ(book.askCount(), 0UL);
231
+ }
232
+
233
+ void test_cancel_order() {
234
+ OrderBook book(1);
235
+ resetCallbacks();
236
+
237
+ auto sell = makeOrder(Side::Sell, OrderType::Limit, TimeInForce::Day, 50.0, 100);
238
+ book.newOrder(sell, onTrade, onExec);
239
+
240
+ ExecutionReport rpt{};
241
+ bool ok = book.cancelOrder(sell.orderId, rpt);
242
+ ASSERT_TRUE(ok);
243
+ ASSERT_EQ(rpt.status, OrderStatus::Cancelled);
244
+ ASSERT_EQ(book.askCount(), 0UL);
245
+ }
246
+
247
+ void test_cancel_nonexistent() {
248
+ OrderBook book(1);
249
+ ExecutionReport rpt{};
250
+ bool ok = book.cancelOrder(9999, rpt);
251
+ ASSERT_TRUE(!ok);
252
+ }
253
+
254
+ void test_multi_level_sweep() {
255
+ OrderBook book(1);
256
+ resetCallbacks();
257
+
258
+ // Three sell levels
259
+ auto s1 = makeOrder(Side::Sell, OrderType::Limit, TimeInForce::Day, 50.0, 20);
260
+ auto s2 = makeOrder(Side::Sell, OrderType::Limit, TimeInForce::Day, 51.0, 20);
261
+ auto s3 = makeOrder(Side::Sell, OrderType::Limit, TimeInForce::Day, 52.0, 20);
262
+ book.newOrder(s1, onTrade, onExec);
263
+ book.newOrder(s2, onTrade, onExec);
264
+ book.newOrder(s3, onTrade, onExec);
265
+
266
+ resetCallbacks();
267
+ // Buy sweeps all three levels
268
+ auto buy = makeOrder(Side::Buy, OrderType::Limit, TimeInForce::Day, 52.0, 50);
269
+ book.newOrder(buy, onTrade, onExec);
270
+
271
+ ASSERT_EQ(trades.size(), 3UL);
272
+ ASSERT_EQ(trades[0].quantity, 20UL);
273
+ ASSERT_EQ(trades[1].quantity, 20UL);
274
+ ASSERT_EQ(trades[2].quantity, 10UL);
275
+ ASSERT_EQ(book.askCount(), 1UL); // 10 remaining at 52.0
276
+ }
277
+
278
+ // ── Main ───────────────────────────────────────────────────────────
279
+ int main() {
280
+ std::cout << "OrderBook Tests\n";
281
+ std::cout << "───────────────────────────────────────────\n";
282
+
283
+ TEST(limit_buy_rests_on_empty_book);
284
+ TEST(limit_sell_rests_on_empty_book);
285
+ TEST(exact_match);
286
+ TEST(partial_fill);
287
+ TEST(price_priority);
288
+ TEST(time_priority);
289
+ TEST(market_order_matches_any_price);
290
+ TEST(market_order_does_not_rest);
291
+ TEST(ioc_partial_fill_cancel_rest);
292
+ TEST(fok_rejected_when_insufficient);
293
+ TEST(fok_fills_when_sufficient);
294
+ TEST(cancel_order);
295
+ TEST(cancel_nonexistent);
296
+ TEST(multi_level_sweep);
297
+
298
+ std::cout << "───────────────────────────────────────────\n";
299
+ std::cout << testsPassed << " passed, " << testsFailed << " failed\n";
300
+ return testsFailed > 0 ? 1 : 0;
301
+ }