EuNEx (Euronext Exchange Simulator) is a C++20 actor-based matching engine that mirrors the Euronext Optiq production architecture. It implements the full order lifecycle: entry, validation, matching, market data dissemination, trade clearing, and FIX protocol connectivity.
EuNEx components mirror Euronext Optiq production terminology:
| Optiq Production | EuNEx Class | File |
|---|---|---|
| OEActor (OE Gateway) | OEGActor | src/actors/OEGActor.hpp |
| LogicalCoreActor + Book | MECoreActor | src/actors/MECoreActor.hpp |
| Book (order book engine) | Book | src/common/Book.hpp |
| MDLimitLogicalCoreHandler | MDGActor | src/actors/MDGActor.hpp |
| OEG FIX Gateway | FIXAcceptorActor | src/actors/FIXAcceptorActor.hpp |
| Clearing House (PTB path) | ClearingHouseActor | src/actors/ClearingHouseActor.hpp |
| Member Trading Bots | AITraderActor | src/actors/AITraderActor.hpp |
| Kafka Bus (KFK) | KafkaBus | src/persistence/KafkaBus.hpp |
| RecoveryProxy | RecoveryProxy | src/recovery/RecoveryProxy.hpp |
| IacaAggregatorActor | IacaAggregator | src/iaca/IacaAggregator.hpp |
OEG Order Entry Gateway — ME Matching Engine — MDG Market Data Gateway — KFK Kafka Bus
The actor engine emulates the Tredzone Simplx framework API:
| Simplx Concept | EuNEx Implementation |
|---|---|
Actor | Base class with event handlers, mailbox |
Event::Pipe | Lock-free cross-actor channel |
ActorId | {id: uint64, coreId: uint8} |
Engine | Multi-threaded scheduler with core affinity |
Callback | Timer-like periodic invocation |
AsyncService | Service locator pattern |
Price_t = int64_t // Fixed-point, PRICE_SCALE = 100'000'000
Quantity_t = uint64_t
OrderId_t = uint64_t // Exchange-assigned order ID
ClOrdId_t = uint64_t // Client order ID
SymbolIndex_t = uint32_t // Instrument identifier
SessionId_t = uint16_t // Client session identifier
MemberId_t = uint16_t // Clearing member identifier
Price_t px = toFixedPrice(150.25); // → 15'025'000'000
double d = toDouble(px); // → 150.25
Events flow between actors via Event::Pipe. Each event struct inherits tredzone::Actor::Event:
| Event | Direction | Purpose |
|---|---|---|
NewOrderEvent | OEG → ME | New order submission |
CancelOrderEvent | OEG → ME | Cancel resting order |
ModifyOrderEvent | OEG → ME | Modify price/qty (cancel-replace) |
ExecReportEvent | ME → OEG | Ack, fill, partial, reject |
TradeEvent | ME → MDG/CH | Trade execution |
BookUpdateEvent | ME → MDG/AI | BBO + depth snapshot |
RecoveryFragmentEvent | ME → Persist | Recovery persistence |
// In MECoreActor constructor:
oePipe_ = Event::Pipe(*this, oeGatewayId);
mdPipe_ = Event::Pipe(*this, marketDataId);
// Pushing an event (lock-free):
oePipe_.push<ExecReportEvent>(report, sessionId);
mdPipe_.push<TradeEvent>(trade);
| Order Type | Behavior |
|---|---|
| Limit | Rests on book if no match; price-time priority |
| Market | Sweeps all levels; never rests (IOC behavior) |
| StopMarket | Parks until trigger price hit, converts to Market |
| StopLimit | Parks until trigger price hit, converts to Limit |
| Time-in-Force | Behavior |
|---|---|
Day | Rests until end of session |
IOC | Fill what's available, cancel remainder |
FOK | Fill all or reject entirely |
Master/Mirror gating for high availability. cause() always executes (both sides). effect() only on Master. recoveryEffect() only on Mirror during failover replay.
Fragments form a tree. Completion check: sum(nextCount) == total_fragments - 1. On completion, the handler fires and generates IA SBE messages.
The Kafka Bus (src/persistence/KafkaBus.hpp) mirrors the Optiq KFK that connects ME to downstream consumers: MDG, PTB, Clearing, IDS, SATURN.
| Topic | Content | Key |
|---|---|---|
eunex.orders | Raw Order structs | symbolIdx |
eunex.trades | Trade structs | symbolIdx |
eunex.market-data | BBO snapshots | symbolIdx |
eunex.recovery.fragments | Recovery fragments | originId:originKey |
eunex.control | Control messages | (reserved) |
cmake .. -DEUNEX_USE_KAFKA=ON # requires librdkafka-dev
cmake .. -DEUNEX_USE_KAFKA=OFF # no-op stub (default)
export EUNEX_KAFKA_BROKERS=kafka:9092
./eunex_me
docker/docker-compose.yml runs Kafka in KRaft mode (no ZooKeeper) using apache/kafka:3.9.0, creates all topics via kafka-init, then starts the engine.
cd docker
docker compose up --build
TCP server on port 9001 with per-client receive threads. Parses FIX 4.4 messages and routes to the actor engine via Event::Pipe.
| Tag 35 | Message | Direction |
|---|---|---|
A | Logon | Inbound |
5 | Logout | Inbound |
0 | Heartbeat | Inbound |
D | NewOrderSingle | Inbound |
F | OrderCancelRequest | Inbound |
G | OrderCancelReplaceRequest | Inbound |
8 | ExecutionReport | Outbound |
| Symbol String | SymbolIndex_t |
|---|---|
| AAPL | 1 |
| MSFT | 2 |
| GOOGL | 3 |
| EURO50 | 4 |
Receives TradeEvent from MECoreActor. Maps sessions to 10 clearing members. Tracks capital, holdings per symbol, P&L, and trade counts.
| Session Range | Members | Source |
|---|---|---|
| 100-109 | MBR01-MBR10 | FIX gateway clients |
| 200-209 | MBR01-MBR10 | AI traders |
On each trade: Buy side reduces capital, updates weighted average cost. Sell side adds proceeds, realizes P&L. getLeaderboard() returns members sorted by capital (thread-safe, mutex-protected).
10 automated trading members with 3 rotating strategies:
| Strategy | Logic |
|---|---|
| Momentum | If last N prices trending up → BUY. Down → SELL. Follow the trend. |
| Mean Reversion | If price > moving average → SELL. Below → BUY. Fade the move. |
| Random | Random side (50/50), random qty (10-100), price around midpoint. |
Data sources: BookUpdateEvent (BBO), TradeEvent (price history), ExecReportEvent (fill confirmations).
Output: NewOrderEvent → OEGActor via Event::Pipe, ~3s intervals via Actor::Callback.
EuNEx/
├── CMakeLists.txt Build configuration
├── README.md Project overview
├── src/
│ ├── main.cpp Entry point, actor wiring
│ ├── common/
│ │ ├── Types.hpp Core types (Price_t, Order, Trade)
│ │ ├── Book.hpp / Book.cpp Price-time priority matching
│ ├── engine/
│ │ └── SimplxShim.hpp Actor framework (Simplx API)
│ ├── actors/
│ │ ├── Events.hpp Inter-actor event definitions
│ │ ├── OEGActor.hpp/cpp Order Entry Gateway
│ │ ├── MECoreActor.hpp/cpp Matching Engine core (per symbol)
│ │ ├── MDGActor.hpp/cpp Market Data Gateway
│ │ ├── FIXAcceptorActor.hpp/cpp FIX 4.4 TCP protocol gateway
│ │ ├── ClearingHouseActor.hpp/cpp Trade clearing & positions
│ │ └── AITraderActor.hpp/cpp Automated trading members
│ ├── net/SocketCompat.hpp Cross-platform sockets
│ ├── persistence/
│ │ ├── KafkaBus.hpp Multi-topic Kafka publisher
│ │ ├── PersistenceStore.hpp Store interface
│ │ └── KafkaStore.hpp Kafka adapter (optional)
│ ├── recovery/RecoveryProxy.hpp Master/Mirror recovery
│ └── iaca/
│ ├── Fragment.hpp IACA fragment definitions
│ └── IacaAggregator.hpp/cpp Fragment chain assembly
├── tests/ 7 test suites
├── examples/ ping_pong, simple_match
├── dashboard/ Flask web UI (:8090)
├── clearing_house/ Flask clearing UI (:8091)
├── fix_gateway/ Python FIX gateway (alt)
└── docker/
├── Dockerfile Multi-stage build
├── docker-compose.yml Kafka + engine + nginx
└── nginx.conf Reverse proxy config
# Configure
cmake -B build -DEUNEX_BUILD_TESTS=ON
# Build
cmake --build build --config Release
# Run tests (7 suites)
cd build && ctest -C Release
# Run engine
./build/Release/eunex_me
| Option | Default | Description |
|---|---|---|
EUNEX_USE_SIMPLX | OFF | Use real Simplx framework |
EUNEX_USE_KAFKA | OFF | Enable Kafka persistence |
EUNEX_BUILD_TESTS | ON | Build test binaries |
EUNEX_BUILD_EXAMPLES | ON | Build example binaries |
| Suite | Cases | Verifies |
|---|---|---|
| OrderBookTest | 26 | Book matching, all TIFs, multi-level sweeps, cancels |
| MatchingEngineTest | — | MECoreActor event handling |
| ThreadedEngineTest | — | Multi-core Engine, mailbox |
| ClearingHouseTest | 7 | Capital, holdings, P&L, session mapping |
| FIXGatewayTest | 5 | Symbol mapping, actor lifecycle, OEG routing |
| AITraderTest | 6 | Strategy execution, multi-symbol, clearing |
| StopOrdersTest | 12 | Stop triggers, phases, IOP, uncrossing |
| Parameter | Value | Description |
|---|---|---|
| FIX port | 9001 | FIXAcceptorActor TCP listen port |
| Symbols | 4 | AAPL(1), MSFT(2), GOOGL(3), EURO50(4) |
| AI members | 10 | MBR01–MBR10 |
| Initial capital | 100,000.0 | Per clearing member |
| AI trade interval | ~3s | Per round in main loop |
| Price scale | 108 | Fixed-point decimal places |
| Kafka brokers | EUNEX_KAFKA_BROKERS | Env var, e.g. kafka:9092 |
AAPL: Sell 155.00/100, 154.00/200 | Buy 153.00/150, 152.00/100
MSFT: Sell 325.00/100, 324.00/150 | Buy 323.00/200, 322.00/100
GOOGL: Sell 142.00/100, 141.00/200 | Buy 140.00/150, 139.00/100
EURO50: Sell 5050.00/50, 5040.00/80 | Buy 5030.00/60, 5020.00/40
SymbolIndex_t constant in main.cppMECoreActor passing OEG, MDG, CH actor IDsoeGateway->mapSymbol(newSym, bookActor->getActorId())AITraderActor's symbol listsrc/actors/MyActor.hpp inheriting tredzone::ActorregisterEventHandler<EventT>(*this)Event::Pipe members for destination actors.cpp to EUNEX_CORE_SOURCES in CMakeLists.txtmain.cppEvents.hpp inheriting tredzone::Actor::EventonEvent(const MyEvent&) to receiving actorsregisterEventHandler<MyEvent>(*this)pipe.push<MyEvent>(args...)Strategy in AITraderActor.hppstrategyMyStrategy() methodsubmitOrder() switchinitMembers()