Commit Β·
8f4d2d0
0
Parent(s):
Initial commit: C++ actor-based matching engine (Optiq architecture)
Browse filesPort 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 +27 -0
- CMakeLists.txt +75 -0
- README.md +118 -0
- docs/process-diagram.md +631 -0
- examples/ping_pong.cpp +85 -0
- examples/simple_match.cpp +195 -0
- src/actors/Events.hpp +133 -0
- src/actors/MarketDataActor.cpp +50 -0
- src/actors/MarketDataActor.hpp +51 -0
- src/actors/OEGatewayActor.cpp +50 -0
- src/actors/OEGatewayActor.hpp +57 -0
- src/actors/OrderBookActor.cpp +106 -0
- src/actors/OrderBookActor.hpp +45 -0
- src/common/OrderBook.cpp +325 -0
- src/common/OrderBook.hpp +79 -0
- src/common/Types.hpp +131 -0
- src/engine/SimplxShim.hpp +345 -0
- src/iaca/Fragment.hpp +72 -0
- src/iaca/IacaAggregator.cpp +60 -0
- src/iaca/IacaAggregator.hpp +66 -0
- src/main.cpp +156 -0
- src/recovery/RecoveryProxy.cpp +3 -0
- src/recovery/RecoveryProxy.hpp +106 -0
- tests/test_matching_engine.cpp +142 -0
- tests/test_orderbook.cpp +301 -0
.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 |
+
}
|