RayMelius Claude Opus 4.6 commited on
Commit
06cfa59
·
1 Parent(s): ec32206

v0.5.0: Kafka Bus integration (Optiq KFK model)

Browse files

Add KafkaBus multi-topic publisher mirroring Optiq KFK between ME and
downstream consumers. MECoreActor publishes orders, trades, and BBO
snapshots to Kafka when EUNEX_KAFKA_BROKERS is set. No-op stub compiles
when EUNEX_USE_KAFKA is OFF. Switch Docker from bitnami/kafka to
apache/kafka:3.9.0 (KRaft mode). Add .dockerignore, update docs.

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

.dockerignore ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ build/
2
+ .git/
3
+ *.exe
4
+ *.obj
5
+ *.pdb
6
+ __pycache__/
7
+ *.pyc
8
+ .vs/
9
+ .vscode/
CMakeLists.txt CHANGED
@@ -1,5 +1,5 @@
1
  cmake_minimum_required(VERSION 3.16)
2
- project(EuNEx VERSION 0.4.0 LANGUAGES CXX)
3
 
4
  set(CMAKE_CXX_STANDARD 20)
5
  set(CMAKE_CXX_STANDARD_REQUIRED ON)
 
1
  cmake_minimum_required(VERSION 3.16)
2
+ project(EuNEx VERSION 0.5.0 LANGUAGES CXX)
3
 
4
  set(CMAKE_CXX_STANDARD 20)
5
  set(CMAKE_CXX_STANDARD_REQUIRED ON)
README.md CHANGED
@@ -27,7 +27,7 @@ ch_ai_trader.py → ClearingHouseActor → Clearing House (P
27
  AI strategies → AITraderActor → Trading obligations
28
  ```
29
 
30
- ## Actor Topology (v0.4)
31
 
32
  ```
33
  Core 0: OEGActor + FIXAcceptorActor ← Order entry & FIX protocol
@@ -109,17 +109,25 @@ cmake --build build --config Release
109
  # Run matching engine
110
  ./build/Release/eunex_me
111
 
112
- # Run all tests (6 suites)
113
  cd build && ctest -C Release
114
  ```
115
 
116
  ## With Kafka Persistence
117
 
118
  ```bash
 
119
  cmake -B build -DEUNEX_USE_KAFKA=ON
120
  cmake --build build --config Release
 
 
 
 
 
121
  ```
122
 
 
 
123
  ## FIX Gateway
124
 
125
  The C++ engine includes a built-in FIX 4.4 acceptor on TCP port 9001.
@@ -163,6 +171,7 @@ EuNEx/
163
  │ ├── net/SocketCompat.hpp # Cross-platform socket abstraction
164
  │ ├── persistence/
165
  │ │ ├── PersistenceStore.hpp # Abstract store + InMemoryStore
 
166
  │ │ └── KafkaStore.hpp # Kafka persistence (optional)
167
  │ ├── recovery/RecoveryProxy.hpp/cpp # Recovery Cause/Effect
168
  │ └── iaca/
 
27
  AI strategies → AITraderActor → Trading obligations
28
  ```
29
 
30
+ ## Actor Topology (v0.5)
31
 
32
  ```
33
  Core 0: OEGActor + FIXAcceptorActor ← Order entry & FIX protocol
 
109
  # Run matching engine
110
  ./build/Release/eunex_me
111
 
112
+ # Run all tests (7 suites)
113
  cd build && ctest -C Release
114
  ```
115
 
116
  ## With Kafka Persistence
117
 
118
  ```bash
119
+ # Compile with Kafka support (requires librdkafka-dev)
120
  cmake -B build -DEUNEX_USE_KAFKA=ON
121
  cmake --build build --config Release
122
+
123
+ # Run with Kafka (set broker address)
124
+ EUNEX_KAFKA_BROKERS=localhost:9092 ./build/Release/eunex_me
125
+
126
+ # Topics: eunex.orders, eunex.trades, eunex.market-data, eunex.recovery.fragments
127
  ```
128
 
129
+ Without `EUNEX_USE_KAFKA`, the engine compiles with a no-op stub and runs standalone.
130
+
131
  ## FIX Gateway
132
 
133
  The C++ engine includes a built-in FIX 4.4 acceptor on TCP port 9001.
 
171
  │ ├── net/SocketCompat.hpp # Cross-platform socket abstraction
172
  │ ├── persistence/
173
  │ │ ├── PersistenceStore.hpp # Abstract store + InMemoryStore
174
+ │ │ ├── KafkaBus.hpp # Multi-topic Kafka publisher (Optiq KFK)
175
  │ │ └── KafkaStore.hpp # Kafka persistence (optional)
176
  │ ├── recovery/RecoveryProxy.hpp/cpp # Recovery Cause/Effect
177
  │ └── iaca/
docker/Dockerfile CHANGED
@@ -32,6 +32,10 @@ COPY --from=builder /app/build/eunex_me /app/
32
  COPY --from=builder /app/build/test_orderbook /app/
33
  COPY --from=builder /app/build/test_matching_engine /app/
34
  COPY --from=builder /app/build/test_threaded_engine /app/
 
 
 
 
35
  COPY --from=builder /app/build/simple_match /app/
36
  COPY --from=builder /app/dashboard/ /app/dashboard/
37
  COPY --from=builder /app/shared/ /app/shared/
 
32
  COPY --from=builder /app/build/test_orderbook /app/
33
  COPY --from=builder /app/build/test_matching_engine /app/
34
  COPY --from=builder /app/build/test_threaded_engine /app/
35
+ COPY --from=builder /app/build/test_clearing_house /app/
36
+ COPY --from=builder /app/build/test_fix_gateway /app/
37
+ COPY --from=builder /app/build/test_ai_trader /app/
38
+ COPY --from=builder /app/build/test_stop_orders /app/
39
  COPY --from=builder /app/build/simple_match /app/
40
  COPY --from=builder /app/dashboard/ /app/dashboard/
41
  COPY --from=builder /app/shared/ /app/shared/
docker/docker-compose.yml CHANGED
@@ -1,44 +1,45 @@
1
  services:
2
  kafka:
3
- image: bitnami/kafka:3.9
4
  ports:
5
  - "9092:9092"
6
  environment:
7
- KAFKA_CFG_NODE_ID: 1
8
- KAFKA_CFG_PROCESS_ROLES: controller,broker
9
- KAFKA_CFG_CONTROLLER_QUORUM_VOTERS: 1@kafka:9093
10
- KAFKA_CFG_LISTENERS: PLAINTEXT://:9092,CONTROLLER://:9093
11
- KAFKA_CFG_ADVERTISED_LISTENERS: PLAINTEXT://kafka:9092
12
- KAFKA_CFG_LISTENER_SECURITY_PROTOCOL_MAP: CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT
13
- KAFKA_CFG_CONTROLLER_LISTENER_NAMES: CONTROLLER
14
- KAFKA_CFG_AUTO_CREATE_TOPICS_ENABLE: "true"
15
- KAFKA_CFG_NUM_PARTITIONS: 4
16
- KAFKA_CFG_LOG_RETENTION_HOURS: 24
 
17
  volumes:
18
- - kafka_data:/bitnami/kafka
19
  healthcheck:
20
- test: ["CMD-SHELL", "kafka-topics.sh --bootstrap-server localhost:9092 --list || exit 1"]
21
  interval: 10s
22
  timeout: 5s
23
  retries: 5
24
 
25
  kafka-init:
26
- image: bitnami/kafka:3.9
27
  depends_on:
28
  kafka:
29
  condition: service_healthy
30
  entrypoint: ["/bin/bash", "-c"]
31
  command:
32
  - |
33
- kafka-topics.sh --bootstrap-server kafka:9092 --create --if-not-exists \
34
  --topic eunex.recovery.fragments --partitions 4 --replication-factor 1
35
- kafka-topics.sh --bootstrap-server kafka:9092 --create --if-not-exists \
36
  --topic eunex.trades --partitions 4 --replication-factor 1
37
- kafka-topics.sh --bootstrap-server kafka:9092 --create --if-not-exists \
38
  --topic eunex.market-data --partitions 4 --replication-factor 1
39
- kafka-topics.sh --bootstrap-server kafka:9092 --create --if-not-exists \
40
  --topic eunex.orders --partitions 4 --replication-factor 1
41
- kafka-topics.sh --bootstrap-server kafka:9092 --create --if-not-exists \
42
  --topic eunex.control --partitions 1 --replication-factor 1
43
  echo "Topics created."
44
 
 
1
  services:
2
  kafka:
3
+ image: apache/kafka:3.9.0
4
  ports:
5
  - "9092:9092"
6
  environment:
7
+ KAFKA_NODE_ID: 1
8
+ KAFKA_PROCESS_ROLES: broker,controller
9
+ KAFKA_CONTROLLER_QUORUM_VOTERS: 1@kafka:9093
10
+ KAFKA_LISTENERS: PLAINTEXT://:9092,CONTROLLER://:9093
11
+ KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:9092
12
+ KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT
13
+ KAFKA_CONTROLLER_LISTENER_NAMES: CONTROLLER
14
+ KAFKA_AUTO_CREATE_TOPICS_ENABLE: "true"
15
+ KAFKA_NUM_PARTITIONS: 4
16
+ KAFKA_LOG_RETENTION_HOURS: 24
17
+ CLUSTER_ID: "EuNEx-kafka-cluster-001"
18
  volumes:
19
+ - kafka_data:/tmp/kraft-combined-logs
20
  healthcheck:
21
+ test: ["CMD-SHELL", "/opt/kafka/bin/kafka-topics.sh --bootstrap-server localhost:9092 --list || exit 1"]
22
  interval: 10s
23
  timeout: 5s
24
  retries: 5
25
 
26
  kafka-init:
27
+ image: apache/kafka:3.9.0
28
  depends_on:
29
  kafka:
30
  condition: service_healthy
31
  entrypoint: ["/bin/bash", "-c"]
32
  command:
33
  - |
34
+ /opt/kafka/bin/kafka-topics.sh --bootstrap-server kafka:9092 --create --if-not-exists \
35
  --topic eunex.recovery.fragments --partitions 4 --replication-factor 1
36
+ /opt/kafka/bin/kafka-topics.sh --bootstrap-server kafka:9092 --create --if-not-exists \
37
  --topic eunex.trades --partitions 4 --replication-factor 1
38
+ /opt/kafka/bin/kafka-topics.sh --bootstrap-server kafka:9092 --create --if-not-exists \
39
  --topic eunex.market-data --partitions 4 --replication-factor 1
40
+ /opt/kafka/bin/kafka-topics.sh --bootstrap-server kafka:9092 --create --if-not-exists \
41
  --topic eunex.orders --partitions 4 --replication-factor 1
42
+ /opt/kafka/bin/kafka-topics.sh --bootstrap-server kafka:9092 --create --if-not-exists \
43
  --topic eunex.control --partitions 1 --replication-factor 1
44
  echo "Topics created."
45
 
docs/developers-guide.md CHANGED
@@ -1,6 +1,6 @@
1
  # EuNEx Developers Guide
2
 
3
- **Version 0.4.0** | Euronext Optiq-Modeled Exchange Simulator
4
 
5
  ---
6
 
@@ -15,13 +15,14 @@
15
  7. [Event System](#7-event-system)
16
  8. [Order Book & Matching](#8-order-book--matching)
17
  9. [Recovery & IACA](#9-recovery--iaca)
18
- 10. [FIX Protocol Gateway](#10-fix-protocol-gateway)
19
- 11. [Clearing House](#11-clearing-house)
20
- 12. [AI Trading Members](#12-ai-trading-members)
21
- 13. [Project Structure](#13-project-structure)
22
- 14. [Build & Test](#14-build--test)
23
- 15. [Configuration](#15-configuration)
24
- 16. [Extending EuNEx](#16-extending-eunex)
 
25
 
26
  ---
27
 
@@ -523,7 +524,72 @@ The `Book` class implements price-time priority matching:
523
 
524
  ---
525
 
526
- ## 10. FIX Protocol Gateway
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
527
 
528
  ### FIXAcceptorActor (`src/actors/FIXAcceptorActor.hpp`)
529
 
@@ -583,7 +649,7 @@ The `Book` class implements price-time priority matching:
583
 
584
  ---
585
 
586
- ## 11. Clearing House
587
 
588
  ### ClearingHouseActor (`src/actors/ClearingHouseActor.hpp`)
589
 
@@ -630,7 +696,7 @@ The `Book` class implements price-time priority matching:
630
 
631
  ---
632
 
633
- ## 12. AI Trading Members
634
 
635
  ### AITraderActor (`src/actors/AITraderActor.hpp`)
636
 
@@ -682,7 +748,7 @@ The `Book` class implements price-time priority matching:
682
 
683
  ---
684
 
685
- ## 13. Project Structure
686
 
687
  ```
688
  EuNEx/
@@ -744,7 +810,7 @@ The `Book` class implements price-time priority matching:
744
 
745
  ---
746
 
747
- ## 14. Build & Test
748
 
749
  ### Prerequisites
750
 
@@ -803,7 +869,7 @@ cd build && ctest -C Release --output-on-failure
803
 
804
  ---
805
 
806
- ## 15. Configuration
807
 
808
  ### Runtime Configuration (main.cpp)
809
 
@@ -829,7 +895,7 @@ The engine pre-populates order books with spread-defining orders:
829
 
830
  ---
831
 
832
- ## 16. Extending EuNEx
833
 
834
  ### Adding a New Symbol
835
 
 
1
  # EuNEx Developers Guide
2
 
3
+ **Version 0.5.0** | Euronext Optiq-Modeled Exchange Simulator
4
 
5
  ---
6
 
 
15
  7. [Event System](#7-event-system)
16
  8. [Order Book & Matching](#8-order-book--matching)
17
  9. [Recovery & IACA](#9-recovery--iaca)
18
+ 10. [Kafka Bus](#10-kafka-bus)
19
+ 11. [FIX Protocol Gateway](#11-fix-protocol-gateway)
20
+ 12. [Clearing House](#12-clearing-house)
21
+ 13. [AI Trading Members](#13-ai-trading-members)
22
+ 14. [Project Structure](#14-project-structure)
23
+ 15. [Build & Test](#15-build--test)
24
+ 16. [Configuration](#16-configuration)
25
+ 17. [Extending EuNEx](#17-extending-eunex)
26
 
27
  ---
28
 
 
524
 
525
  ---
526
 
527
+ ## 10. Kafka Bus
528
+
529
+ ### Overview
530
+
531
+ The Kafka Bus (`src/persistence/KafkaBus.hpp`) mirrors the Optiq KFK (Kafka Bus) that connects the Matching Engine to downstream consumers: MDG, PTB, Clearing, IDS, and SATURN.
532
+
533
+ When `EUNEX_USE_KAFKA` is enabled at compile time and `EUNEX_KAFKA_BROKERS` is set at runtime, the bus publishes events to Kafka topics in real time. When disabled, a no-op stub compiles in its place so the engine runs standalone.
534
+
535
+ ### Topics
536
+
537
+ ```
538
+ Topic Content Key
539
+ ─────────────────────────────────────────────────────────────
540
+ eunex.orders Raw Order structs symbolIdx
541
+ eunex.trades Trade structs symbolIdx
542
+ eunex.market-data BBO snapshots symbolIdx
543
+ eunex.recovery.fragments Recovery fragments originId:originKey
544
+ eunex.control Control messages (reserved)
545
+ ```
546
+
547
+ ### Architecture
548
+
549
+ ```
550
+ MECoreActor (per symbol)
551
+
552
+ ├─ onEvent(NewOrderEvent)
553
+ │ ├─ kafka_->publishOrder(order) → eunex.orders
554
+ │ ├─ onTrade callback:
555
+ │ │ ├─ kafka_->publishTrade(trade) → eunex.trades
556
+ │ │ └─ mdPipe_ / chPipe_ (actors)
557
+ │ └─ publishBookUpdate()
558
+ │ └─ kafka_->publishMarketData(...) → eunex.market-data
559
+
560
+ └─ KafkaBus* kafka_ (nullptr when disabled)
561
+ ```
562
+
563
+ ### Compile-Time Toggle
564
+
565
+ ```cmake
566
+ cmake .. -DEUNEX_USE_KAFKA=ON # requires librdkafka-dev
567
+ cmake .. -DEUNEX_USE_KAFKA=OFF # no-op stub (default)
568
+ ```
569
+
570
+ ### Runtime Configuration
571
+
572
+ Set `EUNEX_KAFKA_BROKERS` environment variable:
573
+
574
+ ```bash
575
+ export EUNEX_KAFKA_BROKERS=kafka:9092
576
+ ./eunex_me
577
+ ```
578
+
579
+ The engine prints Kafka connection status at startup and publishes cumulative stats (orders, trades, market-data messages) every 10 rounds.
580
+
581
+ ### Docker Deployment
582
+
583
+ `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 with `EUNEX_KAFKA_BROKERS=kafka:9092`.
584
+
585
+ ```bash
586
+ cd docker
587
+ docker compose up --build
588
+ ```
589
+
590
+ ---
591
+
592
+ ## 11. FIX Protocol Gateway
593
 
594
  ### FIXAcceptorActor (`src/actors/FIXAcceptorActor.hpp`)
595
 
 
649
 
650
  ---
651
 
652
+ ## 12. Clearing House
653
 
654
  ### ClearingHouseActor (`src/actors/ClearingHouseActor.hpp`)
655
 
 
696
 
697
  ---
698
 
699
+ ## 13. AI Trading Members
700
 
701
  ### AITraderActor (`src/actors/AITraderActor.hpp`)
702
 
 
748
 
749
  ---
750
 
751
+ ## 14. Project Structure
752
 
753
  ```
754
  EuNEx/
 
810
 
811
  ---
812
 
813
+ ## 15. Build & Test
814
 
815
  ### Prerequisites
816
 
 
869
 
870
  ---
871
 
872
+ ## 16. Configuration
873
 
874
  ### Runtime Configuration (main.cpp)
875
 
 
895
 
896
  ---
897
 
898
+ ## 17. Extending EuNEx
899
 
900
  ### Adding a New Symbol
901
 
src/actors/MECoreActor.cpp CHANGED
@@ -6,10 +6,12 @@ namespace eunex {
6
  MECoreActor::MECoreActor(SymbolIndex_t symbolIdx,
7
  const tredzone::ActorId& oeGatewayId,
8
  const tredzone::ActorId& marketDataId,
9
- const tredzone::ActorId& clearingHouseId)
 
10
  : book_(symbolIdx)
11
  , oePipe_(*this, oeGatewayId)
12
  , mdPipe_(*this, marketDataId)
 
13
  {
14
  if (clearingHouseId.id != 0) {
15
  chPipe_.emplace(*this, clearingHouseId);
@@ -19,10 +21,6 @@ MECoreActor::MECoreActor(SymbolIndex_t symbolIdx,
19
  registerEventHandler<ModifyOrderEvent>(*this);
20
  }
21
 
22
- // ── NewOrder ───────────────────────────────────────────────────────
23
- // StockEx: match_order(order, producer)
24
- // Optiq: RecoveryCause.onInput → CauseOperator → forwardToBook()
25
-
26
  void MECoreActor::onEvent(const NewOrderEvent& event) {
27
  Order order{};
28
  order.clOrdId = event.clOrdId;
@@ -35,9 +33,12 @@ void MECoreActor::onEvent(const NewOrderEvent& event) {
35
  order.sessionId = event.sessionId;
36
  order.stopPrice = event.stopPrice;
37
 
 
 
38
  auto onTrade = [this](const Trade& trade) {
39
  mdPipe_.push<TradeEvent>(trade);
40
  if (chPipe_) chPipe_->push<TradeEvent>(trade);
 
41
  };
42
  auto onExec = [this, &event](const ExecutionReport& rpt) {
43
  oePipe_.push<ExecReportEvent>(rpt, event.sessionId);
@@ -45,7 +46,6 @@ void MECoreActor::onEvent(const NewOrderEvent& event) {
45
 
46
  book_.newOrder(order, onTrade, onExec);
47
 
48
- // Trigger any stop orders whose stopPrice has been crossed
49
  if (book_.stopOrderCount() > 0 && book_.lastTradePrice() != 0) {
50
  book_.triggerStopOrders(book_.lastTradePrice(), onTrade, onExec);
51
  }
@@ -53,9 +53,6 @@ void MECoreActor::onEvent(const NewOrderEvent& event) {
53
  publishBookUpdate();
54
  }
55
 
56
- // ── Cancel ─────────────────────────────────────────────────────────
57
- // StockEx: handle_cancel(msg, producer)
58
-
59
  void MECoreActor::onEvent(const CancelOrderEvent& event) {
60
  ExecutionReport rpt{};
61
  if (book_.cancelOrder(event.orderId, rpt)) {
@@ -68,9 +65,6 @@ void MECoreActor::onEvent(const CancelOrderEvent& event) {
68
  }
69
  }
70
 
71
- // ── Modify (Cancel-Replace) ────────────────────────────────────────
72
- // StockEx: handle_amend(msg, producer)
73
-
74
  void MECoreActor::onEvent(const ModifyOrderEvent& event) {
75
  ExecutionReport rpt{};
76
  if (book_.modifyOrder(event.orderId, event.newPrice, event.newQuantity, rpt)) {
@@ -83,10 +77,6 @@ void MECoreActor::onEvent(const ModifyOrderEvent& event) {
83
  }
84
  }
85
 
86
- // ── Publish book snapshot to MarketData actor ──────────────────────
87
- // StockEx: the Dashboard reads orderbook via REST GET /orderbook/<symbol>
88
- // Optiq: publishLimitEffect → push to MDLimitLogicalCoreHandler
89
-
90
  void MECoreActor::publishBookUpdate() {
91
  BookUpdateEvent update;
92
  update.symbolIdx = book_.symbolIndex();
@@ -104,6 +94,14 @@ void MECoreActor::publishBookUpdate() {
104
  }
105
 
106
  mdPipe_.push<BookUpdateEvent>(update);
 
 
 
 
 
 
 
 
107
  }
108
 
109
  } // namespace eunex
 
6
  MECoreActor::MECoreActor(SymbolIndex_t symbolIdx,
7
  const tredzone::ActorId& oeGatewayId,
8
  const tredzone::ActorId& marketDataId,
9
+ const tredzone::ActorId& clearingHouseId,
10
+ KafkaBus* kafkaBus)
11
  : book_(symbolIdx)
12
  , oePipe_(*this, oeGatewayId)
13
  , mdPipe_(*this, marketDataId)
14
+ , kafka_(kafkaBus)
15
  {
16
  if (clearingHouseId.id != 0) {
17
  chPipe_.emplace(*this, clearingHouseId);
 
21
  registerEventHandler<ModifyOrderEvent>(*this);
22
  }
23
 
 
 
 
 
24
  void MECoreActor::onEvent(const NewOrderEvent& event) {
25
  Order order{};
26
  order.clOrdId = event.clOrdId;
 
33
  order.sessionId = event.sessionId;
34
  order.stopPrice = event.stopPrice;
35
 
36
+ if (kafka_) kafka_->publishOrder(order);
37
+
38
  auto onTrade = [this](const Trade& trade) {
39
  mdPipe_.push<TradeEvent>(trade);
40
  if (chPipe_) chPipe_->push<TradeEvent>(trade);
41
+ if (kafka_) kafka_->publishTrade(trade);
42
  };
43
  auto onExec = [this, &event](const ExecutionReport& rpt) {
44
  oePipe_.push<ExecReportEvent>(rpt, event.sessionId);
 
46
 
47
  book_.newOrder(order, onTrade, onExec);
48
 
 
49
  if (book_.stopOrderCount() > 0 && book_.lastTradePrice() != 0) {
50
  book_.triggerStopOrders(book_.lastTradePrice(), onTrade, onExec);
51
  }
 
53
  publishBookUpdate();
54
  }
55
 
 
 
 
56
  void MECoreActor::onEvent(const CancelOrderEvent& event) {
57
  ExecutionReport rpt{};
58
  if (book_.cancelOrder(event.orderId, rpt)) {
 
65
  }
66
  }
67
 
 
 
 
68
  void MECoreActor::onEvent(const ModifyOrderEvent& event) {
69
  ExecutionReport rpt{};
70
  if (book_.modifyOrder(event.orderId, event.newPrice, event.newQuantity, rpt)) {
 
77
  }
78
  }
79
 
 
 
 
 
80
  void MECoreActor::publishBookUpdate() {
81
  BookUpdateEvent update;
82
  update.symbolIdx = book_.symbolIndex();
 
94
  }
95
 
96
  mdPipe_.push<BookUpdateEvent>(update);
97
+
98
+ if (kafka_) {
99
+ Price_t bb = bidLevels.empty() ? 0 : bidLevels[0].price;
100
+ Price_t ba = askLevels.empty() ? 0 : askLevels[0].price;
101
+ Quantity_t bq = bidLevels.empty() ? 0 : bidLevels[0].totalQty;
102
+ Quantity_t aq = askLevels.empty() ? 0 : askLevels[0].totalQty;
103
+ kafka_->publishMarketData(book_.symbolIndex(), bb, ba, bq, aq);
104
+ }
105
  }
106
 
107
  } // namespace eunex
src/actors/MECoreActor.hpp CHANGED
@@ -1,24 +1,8 @@
1
  #pragma once
2
- // ════════════════════════════════════════════════════════════════════
3
- // MECoreActor — 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/Book.hpp"
21
  #include "actors/Events.hpp"
 
22
  #include <optional>
23
 
24
  namespace eunex {
@@ -30,7 +14,8 @@ public:
30
  MECoreActor(SymbolIndex_t symbolIdx,
31
  const tredzone::ActorId& oeGatewayId,
32
  const tredzone::ActorId& marketDataId,
33
- const tredzone::ActorId& clearingHouseId = tredzone::ActorId{});
 
34
 
35
  void onEvent(const NewOrderEvent& event);
36
  void onEvent(const CancelOrderEvent& event);
@@ -41,6 +26,7 @@ private:
41
  tredzone::Actor::Event::Pipe oePipe_;
42
  tredzone::Actor::Event::Pipe mdPipe_;
43
  std::optional<tredzone::Actor::Event::Pipe> chPipe_;
 
44
 
45
  void publishBookUpdate();
46
  };
 
1
  #pragma once
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
  #include "engine/SimplxShim.hpp"
3
  #include "common/Book.hpp"
4
  #include "actors/Events.hpp"
5
+ #include "persistence/KafkaBus.hpp"
6
  #include <optional>
7
 
8
  namespace eunex {
 
14
  MECoreActor(SymbolIndex_t symbolIdx,
15
  const tredzone::ActorId& oeGatewayId,
16
  const tredzone::ActorId& marketDataId,
17
+ const tredzone::ActorId& clearingHouseId = tredzone::ActorId{},
18
+ KafkaBus* kafkaBus = nullptr);
19
 
20
  void onEvent(const NewOrderEvent& event);
21
  void onEvent(const CancelOrderEvent& event);
 
26
  tredzone::Actor::Event::Pipe oePipe_;
27
  tredzone::Actor::Event::Pipe mdPipe_;
28
  std::optional<tredzone::Actor::Event::Pipe> chPipe_;
29
+ KafkaBus* kafka_ = nullptr;
30
 
31
  void publishBookUpdate();
32
  };
src/main.cpp CHANGED
@@ -21,11 +21,13 @@
21
  #include "actors/ClearingHouseActor.hpp"
22
  #include "actors/FIXAcceptorActor.hpp"
23
  #include "actors/AITraderActor.hpp"
 
24
  #include <iostream>
25
  #include <thread>
26
  #include <chrono>
27
  #include <csignal>
28
  #include <atomic>
 
29
 
30
  using namespace tredzone;
31
  using namespace eunex;
@@ -43,6 +45,15 @@ int main() {
43
  std::signal(SIGINT, signalHandler);
44
  std::signal(SIGTERM, signalHandler);
45
 
 
 
 
 
 
 
 
 
 
46
  // --Symbol definitions ----------------------------------------
47
  constexpr SymbolIndex_t SYM_AAPL = 1;
48
  constexpr SymbolIndex_t SYM_MSFT = 2;
@@ -72,14 +83,15 @@ int main() {
72
  }
73
 
74
  // --Core 1: Order Books (per symbol) --------------------------
 
75
  auto bookAAPL = std::make_unique<MECoreActor>(
76
- SYM_AAPL, oeGateway->getActorId(), mdActor->getActorId(), chActor->getActorId());
77
  auto bookMSFT = std::make_unique<MECoreActor>(
78
- SYM_MSFT, oeGateway->getActorId(), mdActor->getActorId(), chActor->getActorId());
79
  auto bookGOOGL = std::make_unique<MECoreActor>(
80
- SYM_GOOGL, oeGateway->getActorId(), mdActor->getActorId(), chActor->getActorId());
81
  auto bookEURO50 = std::make_unique<MECoreActor>(
82
- SYM_EURO50, oeGateway->getActorId(), mdActor->getActorId(), chActor->getActorId());
83
 
84
  oeGateway->mapSymbol(SYM_AAPL, bookAAPL->getActorId());
85
  oeGateway->mapSymbol(SYM_MSFT, bookMSFT->getActorId());
@@ -111,7 +123,13 @@ int main() {
111
  std::cout << "Services:\n";
112
  std::cout << " FIX Gateway: TCP port 9001\n";
113
  std::cout << " AI Traders: 10 members (MBR01-MBR10)\n";
114
- std::cout << " Symbols: AAPL, MSFT, GOOGL, EURO50\n\n";
 
 
 
 
 
 
115
 
116
  // --Seed initial orders for AI to have market data ------------
117
  std::cout << "Seeding initial order book...\n";
@@ -179,6 +197,11 @@ int main() {
179
  if (fixGateway->isRunning()) {
180
  std::cout << " FIX clients: " << fixGateway->clientCount() << "\n";
181
  }
 
 
 
 
 
182
  std::cout << "\n";
183
  }
184
 
@@ -210,6 +233,14 @@ int main() {
210
  }
211
 
212
  std::cout << "\nTrades processed: " << mdActor->getRecentTrades().size() << "\n";
 
 
 
 
 
 
 
 
213
  std::cout << "===========================================\n";
214
  std::cout << " Engine stopped.\n";
215
  return 0;
 
21
  #include "actors/ClearingHouseActor.hpp"
22
  #include "actors/FIXAcceptorActor.hpp"
23
  #include "actors/AITraderActor.hpp"
24
+ #include "persistence/KafkaBus.hpp"
25
  #include <iostream>
26
  #include <thread>
27
  #include <chrono>
28
  #include <csignal>
29
  #include <atomic>
30
+ #include <cstdlib>
31
 
32
  using namespace tredzone;
33
  using namespace eunex;
 
45
  std::signal(SIGINT, signalHandler);
46
  std::signal(SIGTERM, signalHandler);
47
 
48
+ // --Kafka Bus (optional) --------------------------------------
49
+ std::unique_ptr<KafkaBus> kafkaBus;
50
+ const char* kafkaBrokers = std::getenv("EUNEX_KAFKA_BROKERS");
51
+ if (kafkaBrokers && kafkaBrokers[0] != '\0') {
52
+ KafkaBusConfig cfg;
53
+ cfg.brokers = kafkaBrokers;
54
+ kafkaBus = std::make_unique<KafkaBus>(cfg);
55
+ }
56
+
57
  // --Symbol definitions ----------------------------------------
58
  constexpr SymbolIndex_t SYM_AAPL = 1;
59
  constexpr SymbolIndex_t SYM_MSFT = 2;
 
83
  }
84
 
85
  // --Core 1: Order Books (per symbol) --------------------------
86
+ KafkaBus* kb = kafkaBus.get();
87
  auto bookAAPL = std::make_unique<MECoreActor>(
88
+ SYM_AAPL, oeGateway->getActorId(), mdActor->getActorId(), chActor->getActorId(), kb);
89
  auto bookMSFT = std::make_unique<MECoreActor>(
90
+ SYM_MSFT, oeGateway->getActorId(), mdActor->getActorId(), chActor->getActorId(), kb);
91
  auto bookGOOGL = std::make_unique<MECoreActor>(
92
+ SYM_GOOGL, oeGateway->getActorId(), mdActor->getActorId(), chActor->getActorId(), kb);
93
  auto bookEURO50 = std::make_unique<MECoreActor>(
94
+ SYM_EURO50, oeGateway->getActorId(), mdActor->getActorId(), chActor->getActorId(), kb);
95
 
96
  oeGateway->mapSymbol(SYM_AAPL, bookAAPL->getActorId());
97
  oeGateway->mapSymbol(SYM_MSFT, bookMSFT->getActorId());
 
123
  std::cout << "Services:\n";
124
  std::cout << " FIX Gateway: TCP port 9001\n";
125
  std::cout << " AI Traders: 10 members (MBR01-MBR10)\n";
126
+ std::cout << " Symbols: AAPL, MSFT, GOOGL, EURO50\n";
127
+ if (kafkaBus && kafkaBus->isConnected()) {
128
+ std::cout << " Kafka Bus: " << kafkaBrokers << " (connected)\n";
129
+ } else {
130
+ std::cout << " Kafka Bus: disabled (set EUNEX_KAFKA_BROKERS to enable)\n";
131
+ }
132
+ std::cout << "\n";
133
 
134
  // --Seed initial orders for AI to have market data ------------
135
  std::cout << "Seeding initial order book...\n";
 
197
  if (fixGateway->isRunning()) {
198
  std::cout << " FIX clients: " << fixGateway->clientCount() << "\n";
199
  }
200
+ if (kafkaBus && kafkaBus->isConnected()) {
201
+ std::cout << " Kafka: orders=" << kafkaBus->orderCount()
202
+ << " trades=" << kafkaBus->tradeCount()
203
+ << " md=" << kafkaBus->mdCount() << "\n";
204
+ }
205
  std::cout << "\n";
206
  }
207
 
 
233
  }
234
 
235
  std::cout << "\nTrades processed: " << mdActor->getRecentTrades().size() << "\n";
236
+
237
+ if (kafkaBus) {
238
+ kafkaBus->flush();
239
+ std::cout << "Kafka totals: orders=" << kafkaBus->orderCount()
240
+ << " trades=" << kafkaBus->tradeCount()
241
+ << " md=" << kafkaBus->mdCount() << "\n";
242
+ }
243
+
244
  std::cout << "===========================================\n";
245
  std::cout << " Engine stopped.\n";
246
  return 0;
src/persistence/KafkaBus.hpp ADDED
@@ -0,0 +1,156 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #pragma once
2
+ // ====================================================================
3
+ // KafkaBus -- Multi-topic Kafka publisher for the EuNEx event bus
4
+ //
5
+ // Optiq equivalent: Kafka Bus (KFK) between ME and downstream
6
+ // consumers (MDG, PTB, Clearing, IDS, SATURN).
7
+ //
8
+ // Topics:
9
+ // eunex.orders -- incoming orders (NewOrder, Cancel, Modify)
10
+ // eunex.trades -- executed trades
11
+ // eunex.market-data -- BBO / depth snapshots
12
+ // eunex.recovery.fragments -- recovery fragments (Master/Mirror)
13
+ //
14
+ // When EUNEX_USE_KAFKA is OFF, KafkaBus is replaced by a no-op stub
15
+ // so actors compile and run without librdkafka.
16
+ // ====================================================================
17
+
18
+ #include "common/Types.hpp"
19
+ #include <string>
20
+ #include <cstring>
21
+ #include <vector>
22
+ #include <iostream>
23
+ #include <atomic>
24
+
25
+ #ifdef EUNEX_USE_KAFKA
26
+ #include <librdkafka/rdkafkacpp.h>
27
+ #endif
28
+
29
+ namespace eunex {
30
+
31
+ struct KafkaBusConfig {
32
+ std::string brokers = "localhost:9092";
33
+ std::string ordersTopic = "eunex.orders";
34
+ std::string tradesTopic = "eunex.trades";
35
+ std::string marketDataTopic = "eunex.market-data";
36
+ std::string recoveryTopic = "eunex.recovery.fragments";
37
+ int flushTimeoutMs = 5000;
38
+ };
39
+
40
+ #ifdef EUNEX_USE_KAFKA
41
+
42
+ class KafkaBus {
43
+ public:
44
+ explicit KafkaBus(const KafkaBusConfig& cfg) : config_(cfg) {
45
+ std::string errstr;
46
+ auto conf = std::unique_ptr<RdKafka::Conf>(
47
+ RdKafka::Conf::create(RdKafka::Conf::CONF_GLOBAL));
48
+ conf->set("bootstrap.servers", config_.brokers, errstr);
49
+ conf->set("queue.buffering.max.messages", "100000", errstr);
50
+ conf->set("linger.ms", "5", errstr);
51
+
52
+ producer_.reset(RdKafka::Producer::create(conf.get(), errstr));
53
+ if (!producer_) {
54
+ std::cerr << "[KafkaBus] producer creation failed: " << errstr << "\n";
55
+ return;
56
+ }
57
+ connected_ = true;
58
+ std::cout << " Kafka Bus: " << config_.brokers << "\n";
59
+ }
60
+
61
+ ~KafkaBus() {
62
+ if (producer_) producer_->flush(config_.flushTimeoutMs);
63
+ }
64
+
65
+ void publishTrade(const Trade& trade) {
66
+ std::string key = std::to_string(trade.symbolIdx);
67
+ publish(config_.tradesTopic, key,
68
+ reinterpret_cast<const char*>(&trade), sizeof(Trade));
69
+ tradeCount_.fetch_add(1);
70
+ }
71
+
72
+ void publishOrder(const Order& order) {
73
+ std::string key = std::to_string(order.symbolIdx);
74
+ publish(config_.ordersTopic, key,
75
+ reinterpret_cast<const char*>(&order), sizeof(Order));
76
+ orderCount_.fetch_add(1);
77
+ }
78
+
79
+ void publishMarketData(SymbolIndex_t sym,
80
+ Price_t bestBid, Price_t bestAsk,
81
+ Quantity_t bidQty, Quantity_t askQty) {
82
+ struct MDSnapshot {
83
+ SymbolIndex_t sym;
84
+ Price_t bestBid;
85
+ Price_t bestAsk;
86
+ Quantity_t bidQty;
87
+ Quantity_t askQty;
88
+ Timestamp_ns ts;
89
+ };
90
+ MDSnapshot snap{sym, bestBid, bestAsk, bidQty, askQty, nowNs()};
91
+ std::string key = std::to_string(sym);
92
+ publish(config_.marketDataTopic, key,
93
+ reinterpret_cast<const char*>(&snap), sizeof(snap));
94
+ mdCount_.fetch_add(1);
95
+ }
96
+
97
+ void publishRecoveryFragment(const void* data, size_t len,
98
+ uint16_t originId, uint32_t originKey) {
99
+ std::string key = std::to_string(originId) + ":" + std::to_string(originKey);
100
+ publish(config_.recoveryTopic, key,
101
+ reinterpret_cast<const char*>(data), len);
102
+ }
103
+
104
+ bool isConnected() const { return connected_; }
105
+ size_t tradeCount() const { return tradeCount_.load(); }
106
+ size_t orderCount() const { return orderCount_.load(); }
107
+ size_t mdCount() const { return mdCount_.load(); }
108
+
109
+ void flush() {
110
+ if (producer_) producer_->flush(config_.flushTimeoutMs);
111
+ }
112
+
113
+ private:
114
+ KafkaBusConfig config_;
115
+ std::unique_ptr<RdKafka::Producer> producer_;
116
+ bool connected_ = false;
117
+ std::atomic<size_t> tradeCount_{0};
118
+ std::atomic<size_t> orderCount_{0};
119
+ std::atomic<size_t> mdCount_{0};
120
+
121
+ void publish(const std::string& topic, const std::string& key,
122
+ const char* data, size_t len) {
123
+ if (!producer_) return;
124
+ RdKafka::ErrorCode err = producer_->produce(
125
+ topic, RdKafka::Topic::PARTITION_UA,
126
+ RdKafka::Producer::RK_MSG_COPY,
127
+ const_cast<char*>(data), len,
128
+ key.data(), key.size(),
129
+ 0, nullptr);
130
+ if (err != RdKafka::ERR_NO_ERROR) {
131
+ std::cerr << "[KafkaBus] produce to " << topic
132
+ << " failed: " << RdKafka::err2str(err) << "\n";
133
+ }
134
+ producer_->poll(0);
135
+ }
136
+ };
137
+
138
+ #else // No Kafka -- stub
139
+
140
+ class KafkaBus {
141
+ public:
142
+ explicit KafkaBus(const KafkaBusConfig&) {}
143
+ void publishTrade(const Trade&) {}
144
+ void publishOrder(const Order&) {}
145
+ void publishMarketData(SymbolIndex_t, Price_t, Price_t, Quantity_t, Quantity_t) {}
146
+ void publishRecoveryFragment(const void*, size_t, uint16_t, uint32_t) {}
147
+ bool isConnected() const { return false; }
148
+ size_t tradeCount() const { return 0; }
149
+ size_t orderCount() const { return 0; }
150
+ size_t mdCount() const { return 0; }
151
+ void flush() {}
152
+ };
153
+
154
+ #endif // EUNEX_USE_KAFKA
155
+
156
+ } // namespace eunex