RayMelius Claude Opus 4.6 commited on
Commit
a3b10ab
Β·
1 Parent(s): d8389a8

v0.4.0: C++ FIX Gateway, Clearing House & AI Trader actors

Browse files

Implement FIX gateway, Clearing House, and AI traders as proper C++
actors matching the Optiq architecture, replacing the Python-only
approach from v0.3.0.

New actors:
- FIXGatewayActor: C++ TCP FIX 4.4 acceptor (Logon, NewOrder, Cancel, Amend)
- ClearingHouseActor: Trade clearing, member positions, leaderboard
- AITraderActor: 10 members (MBR01-10) with momentum/mean_revert/random strategies

Changes:
- OrderBook: Trade now carries buySessionId/sellSessionId
- OrderBookActor: Optional clearing house pipe for trade forwarding
- OEGatewayActor: NewOrderEvent handler + exec report subscriber forwarding
- main.cpp: Full 4-core topology (OEG+FIX, Books, MarketData, CH+AI)
- SocketCompat.hpp: Cross-platform socket abstraction (winsock2/POSIX)
- CMakeLists.txt: New sources, ws2_32 on Windows, 3 new test targets
- 6 test suites (44 tests total), all passing

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

CMakeLists.txt CHANGED
@@ -1,5 +1,5 @@
1
  cmake_minimum_required(VERSION 3.16)
2
- project(EuNEx VERSION 0.2.0 LANGUAGES CXX)
3
 
4
  set(CMAKE_CXX_STANDARD 20)
5
  set(CMAKE_CXX_STANDARD_REQUIRED ON)
@@ -54,6 +54,9 @@ set(EUNEX_CORE_SOURCES
54
  src/actors/OrderBookActor.cpp
55
  src/actors/OEGatewayActor.cpp
56
  src/actors/MarketDataActor.cpp
 
 
 
57
  src/recovery/RecoveryProxy.cpp
58
  src/iaca/IacaAggregator.cpp
59
  )
@@ -69,6 +72,11 @@ endif()
69
  find_package(Threads REQUIRED)
70
  target_link_libraries(eunex_core PUBLIC Threads::Threads)
71
 
 
 
 
 
 
72
  # ── Main executable ─────────────────────────────────────────────────
73
  add_executable(eunex_me src/main.cpp)
74
  target_link_libraries(eunex_me PRIVATE eunex_core)
@@ -97,4 +105,16 @@ if(EUNEX_BUILD_TESTS)
97
  add_executable(test_threaded_engine tests/test_threaded_engine.cpp)
98
  target_link_libraries(test_threaded_engine PRIVATE eunex_core)
99
  add_test(NAME ThreadedEngineTest COMMAND test_threaded_engine)
 
 
 
 
 
 
 
 
 
 
 
 
100
  endif()
 
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)
 
54
  src/actors/OrderBookActor.cpp
55
  src/actors/OEGatewayActor.cpp
56
  src/actors/MarketDataActor.cpp
57
+ src/actors/ClearingHouseActor.cpp
58
+ src/actors/FIXGatewayActor.cpp
59
+ src/actors/AITraderActor.cpp
60
  src/recovery/RecoveryProxy.cpp
61
  src/iaca/IacaAggregator.cpp
62
  )
 
72
  find_package(Threads REQUIRED)
73
  target_link_libraries(eunex_core PUBLIC Threads::Threads)
74
 
75
+ # Windows socket library
76
+ if(WIN32)
77
+ target_link_libraries(eunex_core PUBLIC ws2_32)
78
+ endif()
79
+
80
  # ── Main executable ─────────────────────────────────────────────────
81
  add_executable(eunex_me src/main.cpp)
82
  target_link_libraries(eunex_me PRIVATE eunex_core)
 
105
  add_executable(test_threaded_engine tests/test_threaded_engine.cpp)
106
  target_link_libraries(test_threaded_engine PRIVATE eunex_core)
107
  add_test(NAME ThreadedEngineTest COMMAND test_threaded_engine)
108
+
109
+ add_executable(test_clearing_house tests/test_clearing_house.cpp)
110
+ target_link_libraries(test_clearing_house PRIVATE eunex_core)
111
+ add_test(NAME ClearingHouseTest COMMAND test_clearing_house)
112
+
113
+ add_executable(test_fix_gateway tests/test_fix_gateway.cpp)
114
+ target_link_libraries(test_fix_gateway PRIVATE eunex_core)
115
+ add_test(NAME FIXGatewayTest COMMAND test_fix_gateway)
116
+
117
+ add_executable(test_ai_trader tests/test_ai_trader.cpp)
118
+ target_link_libraries(test_ai_trader PRIVATE eunex_core)
119
+ add_test(NAME AITraderTest COMMAND test_ai_trader)
120
  endif()
src/actors/AITraderActor.cpp ADDED
@@ -0,0 +1,183 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #include "actors/AITraderActor.hpp"
2
+ #include <algorithm>
3
+ #include <numeric>
4
+ #include <cstring>
5
+ #include <iostream>
6
+
7
+ namespace eunex {
8
+
9
+ AITraderActor::AITraderActor(const tredzone::ActorId& oeGatewayId,
10
+ const std::vector<SymbolIndex_t>& symbols)
11
+ : oePipe_(*this, oeGatewayId)
12
+ , symbols_(symbols)
13
+ , rng_(std::random_device{}())
14
+ {
15
+ registerEventHandler<BookUpdateEvent>(*this);
16
+ registerEventHandler<TradeEvent>(*this);
17
+ registerEventHandler<ExecReportEvent>(*this);
18
+ initMembers();
19
+ registerCallback(*this);
20
+ }
21
+
22
+ void AITraderActor::initMembers() {
23
+ Strategy strategies[] = {Strategy::Momentum, Strategy::MeanReversion, Strategy::Random};
24
+
25
+ for (int i = 0; i < NUM_MEMBERS; ++i) {
26
+ AITraderMember m{};
27
+ m.memberId = static_cast<MemberId_t>(i + 1);
28
+ m.sessionId = static_cast<SessionId_t>(200 + i);
29
+ std::snprintf(m.name, sizeof(m.name), "MBR%02d", i + 1);
30
+ m.strategy = strategies[i % 3];
31
+ m.orderCount = 0;
32
+ members_.push_back(m);
33
+ }
34
+ }
35
+
36
+ void AITraderActor::onEvent(const BookUpdateEvent& event) {
37
+ BBO bbo{};
38
+ if (event.bidDepth > 0) bbo.bestBid = event.bids[0].price;
39
+ if (event.askDepth > 0) bbo.bestAsk = event.asks[0].price;
40
+ bbos_[event.symbolIdx] = bbo;
41
+ }
42
+
43
+ void AITraderActor::onEvent(const TradeEvent& event) {
44
+ auto& history = priceHistory_[event.trade.symbolIdx];
45
+ history.push_back(event.trade.price);
46
+ if (history.size() > MAX_PRICE_HISTORY) {
47
+ history.erase(history.begin());
48
+ }
49
+ }
50
+
51
+ void AITraderActor::onEvent(const ExecReportEvent&) {
52
+ // Could track fills per member; not needed for basic trading
53
+ }
54
+
55
+ void AITraderActor::onCallback() {
56
+ tradeRound();
57
+ registerCallback(*this);
58
+ }
59
+
60
+ void AITraderActor::tradeRound() {
61
+ if (symbols_.empty()) return;
62
+
63
+ for (auto& member : members_) {
64
+ std::uniform_int_distribution<size_t> symDist(0, symbols_.size() - 1);
65
+ SymbolIndex_t symIdx = symbols_[symDist(rng_)];
66
+
67
+ auto bboIt = bbos_.find(symIdx);
68
+ if (bboIt == bbos_.end() || (bboIt->second.bestBid == 0 && bboIt->second.bestAsk == 0)) {
69
+ submitOrder(member, symIdx);
70
+ continue;
71
+ }
72
+
73
+ const BBO& bbo = bboIt->second;
74
+ auto histIt = priceHistory_.find(symIdx);
75
+ std::vector<Price_t> emptyHist;
76
+ const auto& history = (histIt != priceHistory_.end()) ? histIt->second : emptyHist;
77
+
78
+ switch (member.strategy) {
79
+ case Strategy::Momentum:
80
+ strategyMomentum(member, symIdx, bbo, history);
81
+ break;
82
+ case Strategy::MeanReversion:
83
+ strategyMeanReversion(member, symIdx, bbo, history);
84
+ break;
85
+ case Strategy::Random:
86
+ strategyRandom(member, symIdx, bbo);
87
+ break;
88
+ }
89
+ }
90
+ }
91
+
92
+ void AITraderActor::submitOrder(const AITraderMember& member, SymbolIndex_t symIdx) {
93
+ std::uniform_int_distribution<int> sideDist(0, 1);
94
+ std::uniform_int_distribution<int> priceDist(100, 200);
95
+ std::uniform_int_distribution<int> qtyDist(10, 100);
96
+
97
+ Side side = sideDist(rng_) ? Side::Buy : Side::Sell;
98
+ Price_t price = toFixedPrice(priceDist(rng_) * 1.0);
99
+ Quantity_t qty = qtyDist(rng_);
100
+ ClOrdId_t clOrdId = nextClOrdId_++;
101
+
102
+ oePipe_.push<NewOrderEvent>(clOrdId, symIdx, side, OrderType::Limit,
103
+ TimeInForce::Day, price, qty, member.sessionId);
104
+ }
105
+
106
+ void AITraderActor::strategyMomentum(const AITraderMember& member, SymbolIndex_t symIdx,
107
+ const BBO& bbo, const std::vector<Price_t>& history) {
108
+ if (history.size() < 3) {
109
+ submitOrder(member, symIdx);
110
+ return;
111
+ }
112
+
113
+ Price_t recent = history.back();
114
+ Price_t older = history[history.size() - 3];
115
+
116
+ Side side = (recent > older) ? Side::Buy : Side::Sell;
117
+ Price_t midPrice = (bbo.bestBid + bbo.bestAsk) / 2;
118
+ if (midPrice == 0) midPrice = recent;
119
+
120
+ std::uniform_int_distribution<int> spreadDist(-2, 2);
121
+ Price_t tickOffset = spreadDist(rng_) * (PRICE_SCALE / 100);
122
+ Price_t price = midPrice + tickOffset;
123
+ if (price <= 0) price = PRICE_SCALE;
124
+
125
+ std::uniform_int_distribution<int> qtyDist(10, 50);
126
+ Quantity_t qty = qtyDist(rng_);
127
+ ClOrdId_t clOrdId = nextClOrdId_++;
128
+
129
+ oePipe_.push<NewOrderEvent>(clOrdId, symIdx, side, OrderType::Limit,
130
+ TimeInForce::Day, price, qty, member.sessionId);
131
+ }
132
+
133
+ void AITraderActor::strategyMeanReversion(const AITraderMember& member, SymbolIndex_t symIdx,
134
+ const BBO& bbo, const std::vector<Price_t>& history) {
135
+ if (history.size() < 5) {
136
+ submitOrder(member, symIdx);
137
+ return;
138
+ }
139
+
140
+ int64_t sum = 0;
141
+ for (auto p : history) sum += p;
142
+ Price_t mean = static_cast<Price_t>(sum / static_cast<int64_t>(history.size()));
143
+ Price_t current = history.back();
144
+
145
+ Side side = (current > mean) ? Side::Sell : Side::Buy;
146
+
147
+ Price_t price = mean;
148
+ std::uniform_int_distribution<int> qtyDist(10, 50);
149
+ Quantity_t qty = qtyDist(rng_);
150
+ ClOrdId_t clOrdId = nextClOrdId_++;
151
+
152
+ oePipe_.push<NewOrderEvent>(clOrdId, symIdx, side, OrderType::Limit,
153
+ TimeInForce::Day, price, qty, member.sessionId);
154
+ }
155
+
156
+ void AITraderActor::strategyRandom(const AITraderMember& member, SymbolIndex_t symIdx,
157
+ const BBO& bbo) {
158
+ std::uniform_int_distribution<int> sideDist(0, 1);
159
+ Side side = sideDist(rng_) ? Side::Buy : Side::Sell;
160
+
161
+ Price_t midPrice = (bbo.bestBid + bbo.bestAsk) / 2;
162
+ if (midPrice == 0) midPrice = toFixedPrice(150.0);
163
+
164
+ std::uniform_int_distribution<int> spreadDist(-5, 5);
165
+ Price_t tickOffset = spreadDist(rng_) * (PRICE_SCALE / 100);
166
+ Price_t price = midPrice + tickOffset;
167
+ if (price <= 0) price = PRICE_SCALE;
168
+
169
+ std::uniform_int_distribution<int> qtyDist(5, 100);
170
+ Quantity_t qty = qtyDist(rng_);
171
+ ClOrdId_t clOrdId = nextClOrdId_++;
172
+
173
+ oePipe_.push<NewOrderEvent>(clOrdId, symIdx, side, OrderType::Limit,
174
+ TimeInForce::Day, price, qty, member.sessionId);
175
+ }
176
+
177
+ int AITraderActor::totalOrderCount() const {
178
+ int total = 0;
179
+ for (auto& m : members_) total += m.orderCount;
180
+ return total;
181
+ }
182
+
183
+ } // namespace eunex
src/actors/AITraderActor.hpp ADDED
@@ -0,0 +1,84 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #pragma once
2
+ // ════════════════════════════════════════════════════════════════════
3
+ // AITraderActor β€” Automated trading members
4
+ //
5
+ // Optiq equivalent: Clearing House member trading obligations
6
+ //
7
+ // 10 AI members (MBR01-MBR10) with 3 strategies:
8
+ // momentum, mean_reversion, random
9
+ //
10
+ // Uses Actor::Callback for periodic trading decisions (~30s intervals).
11
+ // Receives BookUpdateEvent for BBO data and TradeEvent for price history.
12
+ // Pushes NewOrderEvent to OEGateway.
13
+ // ════════════════════════════════════════════════════════════════════
14
+
15
+ #include "engine/SimplxShim.hpp"
16
+ #include "actors/Events.hpp"
17
+ #include <vector>
18
+ #include <random>
19
+ #include <string>
20
+ #include <unordered_map>
21
+
22
+ namespace eunex {
23
+
24
+ enum class Strategy : uint8_t {
25
+ Momentum = 0,
26
+ MeanReversion = 1,
27
+ Random = 2
28
+ };
29
+
30
+ struct AITraderMember {
31
+ MemberId_t memberId;
32
+ SessionId_t sessionId;
33
+ char name[8];
34
+ Strategy strategy;
35
+ int orderCount;
36
+ };
37
+
38
+ class AITraderActor : public tredzone::Actor, public tredzone::Actor::Callback {
39
+ public:
40
+ struct Service : tredzone::AsyncService {};
41
+
42
+ AITraderActor(const tredzone::ActorId& oeGatewayId,
43
+ const std::vector<SymbolIndex_t>& symbols);
44
+
45
+ void onEvent(const BookUpdateEvent& event);
46
+ void onEvent(const TradeEvent& event);
47
+ void onEvent(const ExecReportEvent& event);
48
+
49
+ void onCallback() override;
50
+
51
+ int totalOrderCount() const;
52
+
53
+ private:
54
+ static constexpr int NUM_MEMBERS = 10;
55
+ static constexpr int TRADE_INTERVAL_MS = 30000;
56
+ static constexpr int MAX_PRICE_HISTORY = 50;
57
+
58
+ tredzone::Actor::Event::Pipe oePipe_;
59
+ std::vector<SymbolIndex_t> symbols_;
60
+ std::vector<AITraderMember> members_;
61
+ ClOrdId_t nextClOrdId_ = 50000;
62
+
63
+ struct BBO {
64
+ Price_t bestBid = 0;
65
+ Price_t bestAsk = 0;
66
+ };
67
+ std::unordered_map<SymbolIndex_t, BBO> bbos_;
68
+ std::unordered_map<SymbolIndex_t, std::vector<Price_t>> priceHistory_;
69
+
70
+ std::mt19937 rng_;
71
+
72
+ void initMembers();
73
+ void tradeRound();
74
+ void submitOrder(const AITraderMember& member, SymbolIndex_t symIdx);
75
+
76
+ void strategyMomentum(const AITraderMember& member, SymbolIndex_t symIdx,
77
+ const BBO& bbo, const std::vector<Price_t>& history);
78
+ void strategyMeanReversion(const AITraderMember& member, SymbolIndex_t symIdx,
79
+ const BBO& bbo, const std::vector<Price_t>& history);
80
+ void strategyRandom(const AITraderMember& member, SymbolIndex_t symIdx,
81
+ const BBO& bbo);
82
+ };
83
+
84
+ } // namespace eunex
src/actors/ClearingHouseActor.cpp ADDED
@@ -0,0 +1,120 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #include "actors/ClearingHouseActor.hpp"
2
+ #include <cstring>
3
+ #include <algorithm>
4
+
5
+ namespace eunex {
6
+
7
+ ClearingHouseActor::ClearingHouseActor() {
8
+ registerEventHandler<TradeEvent>(*this);
9
+ initMembers();
10
+ }
11
+
12
+ void ClearingHouseActor::initMembers() {
13
+ for (int i = 0; i < NUM_MEMBERS; ++i) {
14
+ MemberId_t id = static_cast<MemberId_t>(i + 1);
15
+ MemberState ms{};
16
+ ms.memberId = id;
17
+ std::snprintf(ms.name, sizeof(ms.name), "MBR%02d", i + 1);
18
+ ms.capital = INITIAL_CAPITAL;
19
+ ms.initialCapital = INITIAL_CAPITAL;
20
+ ms.tradeCount = 0;
21
+ ms.distinctSymbols = 0;
22
+ members_[id] = std::move(ms);
23
+ }
24
+ }
25
+
26
+ void ClearingHouseActor::mapSession(SessionId_t sessionId, MemberId_t memberId) {
27
+ std::lock_guard<std::mutex> lock(mutex_);
28
+ sessionToMember_[sessionId] = memberId;
29
+ }
30
+
31
+ void ClearingHouseActor::onEvent(const TradeEvent& event) {
32
+ std::lock_guard<std::mutex> lock(mutex_);
33
+
34
+ const Trade& t = event.trade;
35
+
36
+ auto buyIt = sessionToMember_.find(t.buySessionId);
37
+ if (buyIt != sessionToMember_.end()) {
38
+ processTradeSide(buyIt->second, t.symbolIdx, t.price, t.quantity, true);
39
+ }
40
+
41
+ auto sellIt = sessionToMember_.find(t.sellSessionId);
42
+ if (sellIt != sessionToMember_.end()) {
43
+ processTradeSide(sellIt->second, t.symbolIdx, t.price, t.quantity, false);
44
+ }
45
+ }
46
+
47
+ void ClearingHouseActor::processTradeSide(MemberId_t memberId, SymbolIndex_t symbolIdx,
48
+ Price_t price, Quantity_t qty, bool isBuy) {
49
+ auto it = members_.find(memberId);
50
+ if (it == members_.end()) return;
51
+
52
+ MemberState& m = it->second;
53
+ m.tradeCount++;
54
+
55
+ double cost = toDouble(price) * static_cast<double>(qty);
56
+
57
+ if (isBuy) {
58
+ m.capital -= cost;
59
+ } else {
60
+ m.capital += cost;
61
+ }
62
+
63
+ auto hIt = m.holdings.find(symbolIdx);
64
+ if (hIt == m.holdings.end()) {
65
+ MemberHolding h{};
66
+ h.symbolIdx = symbolIdx;
67
+ h.quantity = isBuy ? static_cast<int64_t>(qty) : -static_cast<int64_t>(qty);
68
+ h.avgCost = price;
69
+ m.holdings[symbolIdx] = h;
70
+ m.distinctSymbols = static_cast<int>(m.holdings.size());
71
+ } else {
72
+ MemberHolding& h = hIt->second;
73
+ if (isBuy) {
74
+ double prevCost = toDouble(h.avgCost) * static_cast<double>(std::abs(h.quantity));
75
+ h.quantity += static_cast<int64_t>(qty);
76
+ if (h.quantity != 0) {
77
+ h.avgCost = toFixedPrice((prevCost + cost) / static_cast<double>(std::abs(h.quantity)));
78
+ }
79
+ } else {
80
+ h.quantity -= static_cast<int64_t>(qty);
81
+ if (h.quantity == 0) {
82
+ m.holdings.erase(hIt);
83
+ m.distinctSymbols = static_cast<int>(m.holdings.size());
84
+ }
85
+ }
86
+ }
87
+ }
88
+
89
+ std::vector<LeaderboardEntry> ClearingHouseActor::getLeaderboard() const {
90
+ std::lock_guard<std::mutex> lock(mutex_);
91
+
92
+ std::vector<LeaderboardEntry> entries;
93
+ entries.reserve(members_.size());
94
+
95
+ for (auto& [id, ms] : members_) {
96
+ LeaderboardEntry e{};
97
+ e.memberId = ms.memberId;
98
+ std::memcpy(e.name, ms.name, sizeof(e.name));
99
+ e.capital = ms.capital;
100
+ e.pnl = ms.capital - ms.initialCapital;
101
+ e.tradeCount = ms.tradeCount;
102
+ e.holdingCount = static_cast<int>(ms.holdings.size());
103
+ entries.push_back(e);
104
+ }
105
+
106
+ std::sort(entries.begin(), entries.end(),
107
+ [](const LeaderboardEntry& a, const LeaderboardEntry& b) {
108
+ return a.capital > b.capital;
109
+ });
110
+
111
+ return entries;
112
+ }
113
+
114
+ const MemberState* ClearingHouseActor::getMember(MemberId_t id) const {
115
+ std::lock_guard<std::mutex> lock(mutex_);
116
+ auto it = members_.find(id);
117
+ return (it != members_.end()) ? &it->second : nullptr;
118
+ }
119
+
120
+ } // namespace eunex
src/actors/ClearingHouseActor.hpp ADDED
@@ -0,0 +1,68 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #pragma once
2
+ // ════════════════════════════════════════════════════════════════════
3
+ // ClearingHouseActor β€” Trade clearing and member position management
4
+ //
5
+ // Optiq equivalent: Clearing House downstream from Kafka Bus / PTB
6
+ //
7
+ // Receives TradeEvent from OrderBookActor(s), attributes trades to
8
+ // members via SessionId→MemberId mapping, maintains capital and
9
+ // holdings per member. Exposes thread-safe getLeaderboard() for
10
+ // the Python bridge to read.
11
+ // ════════════════════════════════════════════════════════════════════
12
+
13
+ #include "engine/SimplxShim.hpp"
14
+ #include "actors/Events.hpp"
15
+ #include <unordered_map>
16
+ #include <vector>
17
+ #include <mutex>
18
+ #include <string>
19
+
20
+ namespace eunex {
21
+
22
+ struct MemberState {
23
+ MemberId_t memberId;
24
+ char name[8];
25
+ double capital;
26
+ double initialCapital;
27
+ int tradeCount;
28
+ int distinctSymbols;
29
+ std::unordered_map<SymbolIndex_t, MemberHolding> holdings;
30
+ };
31
+
32
+ struct LeaderboardEntry {
33
+ MemberId_t memberId;
34
+ char name[8];
35
+ double capital;
36
+ double pnl;
37
+ int tradeCount;
38
+ int holdingCount;
39
+ };
40
+
41
+ class ClearingHouseActor : public tredzone::Actor {
42
+ public:
43
+ struct Service : tredzone::AsyncService {};
44
+
45
+ ClearingHouseActor();
46
+
47
+ void onEvent(const TradeEvent& event);
48
+
49
+ void mapSession(SessionId_t sessionId, MemberId_t memberId);
50
+
51
+ std::vector<LeaderboardEntry> getLeaderboard() const;
52
+
53
+ const MemberState* getMember(MemberId_t id) const;
54
+
55
+ private:
56
+ static constexpr int NUM_MEMBERS = 10;
57
+ static constexpr double INITIAL_CAPITAL = 100000.0;
58
+
59
+ std::unordered_map<SessionId_t, MemberId_t> sessionToMember_;
60
+ std::unordered_map<MemberId_t, MemberState> members_;
61
+ mutable std::mutex mutex_;
62
+
63
+ void initMembers();
64
+ void processTradeSide(MemberId_t memberId, SymbolIndex_t symbolIdx,
65
+ Price_t price, Quantity_t qty, bool isBuy);
66
+ };
67
+
68
+ } // namespace eunex
src/actors/Events.hpp CHANGED
@@ -86,6 +86,11 @@ struct ExecReportEvent : tredzone::Actor::Event {
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) ───────────────
 
86
  filledQty(rpt.filledQty), remainingQty(rpt.remainingQty),
87
  lastPrice(rpt.lastPrice), lastQty(rpt.lastQty),
88
  tradeId(rpt.tradeId), sessionId(sess) {}
89
+ ExecReportEvent(const ExecReportEvent& other)
90
+ : orderId(other.orderId), clOrdId(other.clOrdId), status(other.status),
91
+ filledQty(other.filledQty), remainingQty(other.remainingQty),
92
+ lastPrice(other.lastPrice), lastQty(other.lastQty),
93
+ tradeId(other.tradeId), sessionId(other.sessionId) {}
94
  };
95
 
96
  // ── Trade event (OrderBook Actor β†’ MarketData Actor) ───────────────
src/actors/FIXGatewayActor.cpp ADDED
@@ -0,0 +1,407 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #include "actors/FIXGatewayActor.hpp"
2
+ #include <iostream>
3
+ #include <sstream>
4
+ #include <cstring>
5
+ #include <chrono>
6
+ #include <iomanip>
7
+ #include <ctime>
8
+ #include <cstdlib>
9
+
10
+ namespace eunex {
11
+
12
+ FIXGatewayActor::FIXGatewayActor(const tredzone::ActorId& oeGatewayId, uint16_t port)
13
+ : oePipe_(*this, oeGatewayId)
14
+ , port_(port)
15
+ {
16
+ registerEventHandler<ExecReportEvent>(*this);
17
+
18
+ symbolNames_[1] = "AAPL";
19
+ symbolNames_[2] = "MSFT";
20
+ symbolNames_[3] = "GOOGL";
21
+ symbolNames_[4] = "EURO50";
22
+
23
+ listenSock_ = socket(AF_INET, SOCK_STREAM, 0);
24
+ if (listenSock_ == INVALID_SOCK) {
25
+ std::cerr << "FIXGateway: socket() failed\n";
26
+ return;
27
+ }
28
+
29
+ setSocketReuseAddr(listenSock_);
30
+
31
+ sockaddr_in addr{};
32
+ addr.sin_family = AF_INET;
33
+ addr.sin_addr.s_addr = INADDR_ANY;
34
+ addr.sin_port = htons(port_);
35
+
36
+ if (bind(listenSock_, reinterpret_cast<sockaddr*>(&addr), sizeof(addr)) != 0) {
37
+ std::cerr << "FIXGateway: bind() failed on port " << port_ << "\n";
38
+ closeSocket(listenSock_);
39
+ listenSock_ = INVALID_SOCK;
40
+ return;
41
+ }
42
+
43
+ if (listen(listenSock_, 5) != 0) {
44
+ std::cerr << "FIXGateway: listen() failed\n";
45
+ closeSocket(listenSock_);
46
+ listenSock_ = INVALID_SOCK;
47
+ return;
48
+ }
49
+
50
+ running_ = true;
51
+ acceptThread_ = std::thread(&FIXGatewayActor::acceptLoop, this);
52
+
53
+ std::cout << "FIXGateway: listening on port " << port_ << "\n";
54
+ }
55
+
56
+ FIXGatewayActor::~FIXGatewayActor() {
57
+ stop();
58
+ }
59
+
60
+ void FIXGatewayActor::stop() {
61
+ if (!running_.exchange(false)) return;
62
+
63
+ if (listenSock_ != INVALID_SOCK) {
64
+ closeSocket(listenSock_);
65
+ listenSock_ = INVALID_SOCK;
66
+ }
67
+
68
+ if (acceptThread_.joinable()) acceptThread_.join();
69
+
70
+ std::lock_guard<std::mutex> lock(sessionMutex_);
71
+ for (auto& [id, sess] : sessions_) {
72
+ if (sess->sock != INVALID_SOCK) {
73
+ #ifdef _WIN32
74
+ shutdown(sess->sock, SD_BOTH);
75
+ #else
76
+ shutdown(sess->sock, SHUT_RDWR);
77
+ #endif
78
+ closeSocket(sess->sock);
79
+ sess->sock = INVALID_SOCK;
80
+ }
81
+ if (sess->recvThread.joinable()) sess->recvThread.join();
82
+ }
83
+ sessions_.clear();
84
+ }
85
+
86
+ int FIXGatewayActor::clientCount() const {
87
+ std::lock_guard<std::mutex> lock(sessionMutex_);
88
+ return static_cast<int>(sessions_.size());
89
+ }
90
+
91
+ // ── Accept loop (background thread) ──────────────────────────────
92
+
93
+ void FIXGatewayActor::acceptLoop() {
94
+ while (running_) {
95
+ sockaddr_in clientAddr{};
96
+ socklen_t len = sizeof(clientAddr);
97
+ socket_t clientSock = accept(listenSock_,
98
+ reinterpret_cast<sockaddr*>(&clientAddr), &len);
99
+
100
+ if (clientSock == INVALID_SOCK) {
101
+ if (running_) std::cerr << "FIXGateway: accept() failed\n";
102
+ continue;
103
+ }
104
+
105
+ std::lock_guard<std::mutex> lock(sessionMutex_);
106
+ SessionId_t sessId = nextSessionId_++;
107
+
108
+ auto sess = std::make_unique<FIXSession>();
109
+ sess->sock = clientSock;
110
+ sess->sessionId = sessId;
111
+ sess->msgSeqNum = 1;
112
+ sess->loggedOn = false;
113
+
114
+ FIXSession* raw = sess.get();
115
+ sess->recvThread = std::thread(&FIXGatewayActor::clientRecvLoop, this, sessId);
116
+
117
+ sessions_[sessId] = std::move(sess);
118
+ std::cout << "FIXGateway: client connected (session " << sessId << ")\n";
119
+ }
120
+ }
121
+
122
+ // ── Client recv loop (per-client thread) ─────────────────────────
123
+
124
+ void FIXGatewayActor::clientRecvLoop(SessionId_t sessionId) {
125
+ socket_t sock;
126
+ {
127
+ std::lock_guard<std::mutex> lock(sessionMutex_);
128
+ auto it = sessions_.find(sessionId);
129
+ if (it == sessions_.end()) return;
130
+ sock = it->second->sock;
131
+ }
132
+
133
+ char buf[4096];
134
+ std::string buffer;
135
+
136
+ while (running_) {
137
+ int n = recv(sock, buf, sizeof(buf) - 1, 0);
138
+ if (n <= 0) break;
139
+
140
+ buf[n] = '\0';
141
+ buffer.append(buf, n);
142
+
143
+ auto messages = parseFIXMessages(buffer);
144
+
145
+ for (auto& msg : messages) {
146
+ std::string msgType = getTag(msg, 35);
147
+
148
+ std::lock_guard<std::mutex> lock(sessionMutex_);
149
+ auto it = sessions_.find(sessionId);
150
+ if (it == sessions_.end()) return;
151
+ FIXSession& sess = *it->second;
152
+
153
+ if (msgType == "A") {
154
+ handleLogon(sess, msg);
155
+ } else if (msgType == "5") {
156
+ sendFIX(sess, "5", {});
157
+ return;
158
+ } else if (msgType == "0") {
159
+ sendFIX(sess, "0", {});
160
+ } else if (msgType == "D") {
161
+ handleNewOrderSingle(sess, msg);
162
+ } else if (msgType == "F") {
163
+ handleCancelRequest(sess, msg);
164
+ } else if (msgType == "G") {
165
+ handleCancelReplaceRequest(sess, msg);
166
+ }
167
+ }
168
+ }
169
+
170
+ std::lock_guard<std::mutex> lock(sessionMutex_);
171
+ auto it = sessions_.find(sessionId);
172
+ if (it != sessions_.end()) {
173
+ closeSocket(it->second->sock);
174
+ it->second->sock = INVALID_SOCK;
175
+ }
176
+ }
177
+
178
+ // ── FIX message parsing ──────────────────────────────────────────
179
+
180
+ std::vector<FIXGatewayActor::TagMap> FIXGatewayActor::parseFIXMessages(const std::string& data) {
181
+ std::vector<TagMap> result;
182
+ std::string remaining = data;
183
+
184
+ while (true) {
185
+ auto checkPos = remaining.find("10=");
186
+ if (checkPos == std::string::npos) break;
187
+
188
+ auto sohAfterCheck = remaining.find('\x01', checkPos);
189
+ if (sohAfterCheck == std::string::npos) break;
190
+
191
+ std::string rawMsg = remaining.substr(0, sohAfterCheck + 1);
192
+ remaining = remaining.substr(sohAfterCheck + 1);
193
+
194
+ TagMap tags;
195
+ size_t pos = 0;
196
+ while (pos < rawMsg.size()) {
197
+ auto eqPos = rawMsg.find('=', pos);
198
+ if (eqPos == std::string::npos) break;
199
+ auto sohPos = rawMsg.find('\x01', eqPos);
200
+ if (sohPos == std::string::npos) sohPos = rawMsg.size();
201
+
202
+ int tag = std::atoi(rawMsg.substr(pos, eqPos - pos).c_str());
203
+ std::string val = rawMsg.substr(eqPos + 1, sohPos - eqPos - 1);
204
+ tags[tag] = val;
205
+ pos = sohPos + 1;
206
+ }
207
+
208
+ if (!tags.empty()) result.push_back(std::move(tags));
209
+ }
210
+
211
+ // Put unparsed data back (caller should update buffer)
212
+ const_cast<std::string&>(data) = remaining;
213
+ return result;
214
+ }
215
+
216
+ std::string FIXGatewayActor::getTag(const TagMap& msg, int tag, const std::string& def) {
217
+ auto it = msg.find(tag);
218
+ return (it != msg.end()) ? it->second : def;
219
+ }
220
+
221
+ // ── FIX message handlers ─────────────────────────────────────────
222
+
223
+ void FIXGatewayActor::handleLogon(FIXSession& sess, const TagMap& msg) {
224
+ sess.senderCompId = getTag(msg, 49, "UNKNOWN");
225
+ sess.loggedOn = true;
226
+ std::cout << "FIXGateway: Logon from " << sess.senderCompId
227
+ << " (session " << sess.sessionId << ")\n";
228
+
229
+ sendFIX(sess, "A", {
230
+ {98, "0"},
231
+ {108, "30"}
232
+ });
233
+ }
234
+
235
+ void FIXGatewayActor::handleNewOrderSingle(FIXSession& sess, const TagMap& msg) {
236
+ std::string clOrdIdStr = getTag(msg, 11);
237
+ std::string symbol = getTag(msg, 55);
238
+ std::string sideStr = getTag(msg, 54);
239
+ std::string ordTypeStr = getTag(msg, 40);
240
+ std::string priceStr = getTag(msg, 44, "0");
241
+ std::string qtyStr = getTag(msg, 38);
242
+ std::string tifStr = getTag(msg, 59, "0");
243
+
244
+ ClOrdId_t clOrdId = std::strtoull(clOrdIdStr.c_str(), nullptr, 10);
245
+ SymbolIndex_t symIdx = symbolFromString(symbol);
246
+ Side side = (sideStr == "1") ? Side::Buy : Side::Sell;
247
+ OrderType ordType = (ordTypeStr == "1") ? OrderType::Market : OrderType::Limit;
248
+ Price_t price = toFixedPrice(std::strtod(priceStr.c_str(), nullptr));
249
+ Quantity_t qty = std::strtoull(qtyStr.c_str(), nullptr, 10);
250
+
251
+ TimeInForce tif = TimeInForce::Day;
252
+ if (tifStr == "1") tif = TimeInForce::GTC;
253
+ else if (tifStr == "3") tif = TimeInForce::IOC;
254
+ else if (tifStr == "4") tif = TimeInForce::FOK;
255
+
256
+ oePipe_.push<NewOrderEvent>(clOrdId, symIdx, side, ordType, tif, price, qty, sess.sessionId);
257
+ }
258
+
259
+ void FIXGatewayActor::handleCancelRequest(FIXSession& sess, const TagMap& msg) {
260
+ std::string origClOrdIdStr = getTag(msg, 41);
261
+ std::string orderIdStr = getTag(msg, 37, "0");
262
+ std::string symbol = getTag(msg, 55);
263
+
264
+ ClOrdId_t origClOrdId = std::strtoull(origClOrdIdStr.c_str(), nullptr, 10);
265
+ OrderId_t orderId = std::strtoull(orderIdStr.c_str(), nullptr, 10);
266
+ SymbolIndex_t symIdx = symbolFromString(symbol);
267
+
268
+ oePipe_.push<CancelOrderEvent>(orderId, origClOrdId, symIdx, sess.sessionId);
269
+ }
270
+
271
+ void FIXGatewayActor::handleCancelReplaceRequest(FIXSession& sess, const TagMap& msg) {
272
+ std::string orderIdStr = getTag(msg, 37, "0");
273
+ std::string origClOrdIdStr = getTag(msg, 41);
274
+ std::string symbol = getTag(msg, 55);
275
+ std::string priceStr = getTag(msg, 44, "0");
276
+ std::string qtyStr = getTag(msg, 38);
277
+
278
+ OrderId_t orderId = std::strtoull(orderIdStr.c_str(), nullptr, 10);
279
+ ClOrdId_t origClOrdId = std::strtoull(origClOrdIdStr.c_str(), nullptr, 10);
280
+ SymbolIndex_t symIdx = symbolFromString(symbol);
281
+ Price_t newPrice = toFixedPrice(std::strtod(priceStr.c_str(), nullptr));
282
+ Quantity_t newQty = std::strtoull(qtyStr.c_str(), nullptr, 10);
283
+
284
+ oePipe_.push<ModifyOrderEvent>(orderId, origClOrdId, symIdx, newPrice, newQty, sess.sessionId);
285
+ }
286
+
287
+ // ── Send FIX message ─────────────────────────────────────────────
288
+
289
+ void FIXGatewayActor::sendFIX(FIXSession& sess, const std::string& msgType,
290
+ const std::vector<std::pair<int, std::string>>& fields) {
291
+ std::ostringstream body;
292
+ body << "35=" << msgType << '\x01';
293
+ body << "49=EUNEX" << '\x01';
294
+ body << "56=" << sess.senderCompId << '\x01';
295
+ body << "34=" << sess.msgSeqNum++ << '\x01';
296
+
297
+ auto now = std::chrono::system_clock::now();
298
+ auto t = std::chrono::system_clock::to_time_t(now);
299
+ struct tm tmBuf;
300
+ #ifdef _WIN32
301
+ gmtime_s(&tmBuf, &t);
302
+ #else
303
+ gmtime_r(&t, &tmBuf);
304
+ #endif
305
+ char timeBuf[32];
306
+ std::strftime(timeBuf, sizeof(timeBuf), "%Y%m%d-%H:%M:%S", &tmBuf);
307
+ body << "52=" << timeBuf << '\x01';
308
+
309
+ for (auto& [tag, val] : fields) {
310
+ body << tag << "=" << val << '\x01';
311
+ }
312
+
313
+ std::string bodyStr = body.str();
314
+
315
+ std::ostringstream msg;
316
+ msg << "8=FIX.4.4" << '\x01';
317
+ msg << "9=" << bodyStr.size() << '\x01';
318
+ msg << bodyStr;
319
+
320
+ std::string raw = msg.str();
321
+ int checksum = 0;
322
+ for (char c : raw) checksum += static_cast<unsigned char>(c);
323
+ checksum %= 256;
324
+
325
+ char csStr[8];
326
+ std::snprintf(csStr, sizeof(csStr), "%03d", checksum);
327
+ raw += "10=";
328
+ raw += csStr;
329
+ raw += '\x01';
330
+
331
+ send(sess.sock, raw.c_str(), static_cast<int>(raw.size()), 0);
332
+ }
333
+
334
+ // ── Exec report handling ─────────────────────────────────────────
335
+
336
+ void FIXGatewayActor::onEvent(const ExecReportEvent& event) {
337
+ sendExecReport(event.sessionId, event);
338
+ }
339
+
340
+ void FIXGatewayActor::sendExecReport(SessionId_t sessionId, const ExecReportEvent& rpt) {
341
+ std::lock_guard<std::mutex> lock(sessionMutex_);
342
+ auto it = sessions_.find(sessionId);
343
+ if (it == sessions_.end()) return;
344
+ FIXSession& sess = *it->second;
345
+ if (!sess.loggedOn) return;
346
+
347
+ auto statusToOrdStatus = [](OrderStatus s) -> std::string {
348
+ switch (s) {
349
+ case OrderStatus::New: return "0";
350
+ case OrderStatus::PartiallyFilled: return "1";
351
+ case OrderStatus::Filled: return "2";
352
+ case OrderStatus::Cancelled: return "4";
353
+ case OrderStatus::Rejected: return "8";
354
+ default: return "0";
355
+ }
356
+ };
357
+
358
+ auto statusToExecType = [](OrderStatus s) -> std::string {
359
+ switch (s) {
360
+ case OrderStatus::New: return "0";
361
+ case OrderStatus::PartiallyFilled: return "F";
362
+ case OrderStatus::Filled: return "F";
363
+ case OrderStatus::Cancelled: return "4";
364
+ case OrderStatus::Rejected: return "8";
365
+ default: return "0";
366
+ }
367
+ };
368
+
369
+ std::vector<std::pair<int, std::string>> fields = {
370
+ {37, std::to_string(rpt.orderId)},
371
+ {11, std::to_string(rpt.clOrdId)},
372
+ {17, std::to_string(rpt.tradeId)},
373
+ {150, statusToExecType(rpt.status)},
374
+ {39, statusToOrdStatus(rpt.status)},
375
+ {14, std::to_string(rpt.filledQty)},
376
+ {151, std::to_string(rpt.remainingQty)},
377
+ };
378
+
379
+ if (rpt.lastQty > 0) {
380
+ fields.push_back({31, std::to_string(toDouble(rpt.lastPrice))});
381
+ fields.push_back({32, std::to_string(rpt.lastQty)});
382
+ }
383
+
384
+ sendFIX(sess, "8", fields);
385
+ }
386
+
387
+ // ── Symbol mapping ───────────────────────────────────────────────
388
+
389
+ SymbolIndex_t FIXGatewayActor::symbolFromString(const std::string& sym) {
390
+ if (sym == "AAPL") return 1;
391
+ if (sym == "MSFT") return 2;
392
+ if (sym == "GOOGL") return 3;
393
+ if (sym == "EURO50") return 4;
394
+ return static_cast<SymbolIndex_t>(std::strtoul(sym.c_str(), nullptr, 10));
395
+ }
396
+
397
+ std::string FIXGatewayActor::symbolToString(SymbolIndex_t idx) {
398
+ switch (idx) {
399
+ case 1: return "AAPL";
400
+ case 2: return "MSFT";
401
+ case 3: return "GOOGL";
402
+ case 4: return "EURO50";
403
+ default: return std::to_string(idx);
404
+ }
405
+ }
406
+
407
+ } // namespace eunex
src/actors/FIXGatewayActor.hpp ADDED
@@ -0,0 +1,91 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #pragma once
2
+ // ════════════════════════════════════════════════════════════════════
3
+ // FIXGatewayActor β€” C++ FIX 4.4 TCP acceptor
4
+ //
5
+ // Optiq equivalent: OEG FIX Gateway (FIX 4.4 acceptor for OE frontal)
6
+ //
7
+ // Runs a TCP accept loop on a background thread. Each connected client
8
+ // gets a recv thread that parses FIX messages and enqueues them to the
9
+ // actor's mailbox. The actor thread pushes NewOrderEvent / CancelEvent
10
+ // to OEGateway via Event::Pipe.
11
+ //
12
+ // Supported messages:
13
+ // Logon (35=A), Logout (35=5), Heartbeat (35=0),
14
+ // NewOrderSingle (35=D), OrderCancelRequest (35=F),
15
+ // OrderCancelReplaceRequest (35=G)
16
+ //
17
+ // Outbound:
18
+ // ExecutionReport (35=8) via onEvent(ExecReportEvent)
19
+ // ════════════════════════════════════════════════════════════════════
20
+
21
+ #include "engine/SimplxShim.hpp"
22
+ #include "actors/Events.hpp"
23
+ #include "net/SocketCompat.hpp"
24
+ #include <thread>
25
+ #include <atomic>
26
+ #include <unordered_map>
27
+ #include <mutex>
28
+ #include <string>
29
+ #include <vector>
30
+ #include <functional>
31
+
32
+ namespace eunex {
33
+
34
+ struct FIXSession {
35
+ socket_t sock;
36
+ SessionId_t sessionId;
37
+ std::string senderCompId;
38
+ int msgSeqNum;
39
+ bool loggedOn;
40
+ std::thread recvThread;
41
+ };
42
+
43
+ class FIXGatewayActor : public tredzone::Actor {
44
+ public:
45
+ struct Service : tredzone::AsyncService {};
46
+
47
+ FIXGatewayActor(const tredzone::ActorId& oeGatewayId, uint16_t port = 9001);
48
+ ~FIXGatewayActor();
49
+
50
+ void onEvent(const ExecReportEvent& event);
51
+
52
+ void stop();
53
+ bool isRunning() const { return running_.load(); }
54
+ int clientCount() const;
55
+
56
+ private:
57
+ tredzone::Actor::Event::Pipe oePipe_;
58
+ uint16_t port_;
59
+ std::atomic<bool> running_{false};
60
+ socket_t listenSock_ = INVALID_SOCK;
61
+ std::thread acceptThread_;
62
+ SocketInit sockInit_;
63
+
64
+ mutable std::mutex sessionMutex_;
65
+ std::unordered_map<SessionId_t, std::unique_ptr<FIXSession>> sessions_;
66
+ SessionId_t nextSessionId_ = 100;
67
+
68
+ std::unordered_map<SymbolIndex_t, std::string> symbolNames_;
69
+
70
+ void acceptLoop();
71
+ void clientRecvLoop(SessionId_t sessionId);
72
+
73
+ using TagMap = std::unordered_map<int, std::string>;
74
+ static std::vector<TagMap> parseFIXMessages(const std::string& data);
75
+ static std::string getTag(const TagMap& msg, int tag, const std::string& def = "");
76
+
77
+ void handleLogon(FIXSession& sess, const TagMap& msg);
78
+ void handleNewOrderSingle(FIXSession& sess, const TagMap& msg);
79
+ void handleCancelRequest(FIXSession& sess, const TagMap& msg);
80
+ void handleCancelReplaceRequest(FIXSession& sess, const TagMap& msg);
81
+
82
+ void sendFIX(FIXSession& sess, const std::string& msgType,
83
+ const std::vector<std::pair<int, std::string>>& fields);
84
+ void sendExecReport(SessionId_t sessionId, const ExecReportEvent& rpt);
85
+
86
+ public:
87
+ static SymbolIndex_t symbolFromString(const std::string& sym);
88
+ static std::string symbolToString(SymbolIndex_t idx);
89
+ };
90
+
91
+ } // namespace eunex
src/actors/OEGatewayActor.cpp CHANGED
@@ -3,6 +3,9 @@
3
  namespace eunex {
4
 
5
  OEGatewayActor::OEGatewayActor() {
 
 
 
6
  registerEventHandler<ExecReportEvent>(*this);
7
  }
8
 
@@ -43,8 +46,31 @@ void OEGatewayActor::submitModify(OrderId_t orderId, ClOrdId_t origClOrdId,
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
 
3
  namespace eunex {
4
 
5
  OEGatewayActor::OEGatewayActor() {
6
+ registerEventHandler<NewOrderEvent>(*this);
7
+ registerEventHandler<CancelOrderEvent>(*this);
8
+ registerEventHandler<ModifyOrderEvent>(*this);
9
  registerEventHandler<ExecReportEvent>(*this);
10
  }
11
 
 
46
  pipe.push<ModifyOrderEvent>(orderId, origClOrdId, symbolIdx, newPrice, newQty, session);
47
  }
48
 
49
+ void OEGatewayActor::onEvent(const NewOrderEvent& event) {
50
+ submitNewOrder(event.clOrdId, event.symbolIdx, event.side, event.ordType,
51
+ event.tif, event.price, event.quantity, event.sessionId);
52
+ }
53
+
54
+ void OEGatewayActor::onEvent(const CancelOrderEvent& event) {
55
+ submitCancel(event.orderId, event.origClOrdId, event.symbolIdx, event.sessionId);
56
+ }
57
+
58
+ void OEGatewayActor::onEvent(const ModifyOrderEvent& event) {
59
+ submitModify(event.orderId, event.origClOrdId, event.symbolIdx,
60
+ event.newPrice, event.newQuantity, event.sessionId);
61
+ }
62
+
63
+ void OEGatewayActor::addExecReportSubscriber(const tredzone::ActorId& subscriberId) {
64
+ execReportSubscribers_.push_back(subscriberId);
65
+ }
66
+
67
  void OEGatewayActor::onEvent(const ExecReportEvent& event) {
68
  reports_.push_back(event);
69
+
70
+ for (auto& subId : execReportSubscribers_) {
71
+ Event::Pipe pipe(*this, subId);
72
+ pipe.push<ExecReportEvent>(event);
73
+ }
74
  }
75
 
76
  } // namespace eunex
src/actors/OEGatewayActor.hpp CHANGED
@@ -41,15 +41,21 @@ public:
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
  };
 
41
  SymbolIndex_t symbolIdx, Price_t newPrice,
42
  Quantity_t newQty, SessionId_t session);
43
 
44
+ void onEvent(const NewOrderEvent& event);
45
+ void onEvent(const CancelOrderEvent& event);
46
+ void onEvent(const ModifyOrderEvent& event);
47
  void onEvent(const ExecReportEvent& event);
48
 
49
+ // Subscribe to execution reports (FIXGateway, AITrader, etc.)
50
+ void addExecReportSubscriber(const tredzone::ActorId& subscriberId);
51
+
52
  // Access received reports (for testing / downstream forwarding)
53
  const std::vector<ExecReportEvent>& getReports() const { return reports_; }
54
  void clearReports() { reports_.clear(); }
55
 
56
  private:
57
  std::unordered_map<SymbolIndex_t, tredzone::ActorId> symbolMap_;
58
+ std::vector<tredzone::ActorId> execReportSubscribers_;
59
  std::vector<ExecReportEvent> reports_;
60
  ClOrdId_t nextClOrdId_ = 1000;
61
  };
src/actors/OrderBookActor.cpp CHANGED
@@ -5,11 +5,15 @@ 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);
@@ -37,9 +41,9 @@ void OrderBookActor::onEvent(const NewOrderEvent& event) {
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) {
 
5
 
6
  OrderBookActor::OrderBookActor(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);
16
+ }
17
  registerEventHandler<NewOrderEvent>(*this);
18
  registerEventHandler<CancelOrderEvent>(*this);
19
  registerEventHandler<ModifyOrderEvent>(*this);
 
41
  // Effect β†’ publish limit update to MDLimit
42
 
43
  book_.newOrder(order,
 
44
  [this](const Trade& trade) {
45
  mdPipe_.push<TradeEvent>(trade);
46
+ if (chPipe_) chPipe_->push<TradeEvent>(trade);
47
  },
48
  // Execution report β€” send back to OE Gateway
49
  [this, &event](const ExecutionReport& rpt) {
src/actors/OrderBookActor.hpp CHANGED
@@ -19,6 +19,7 @@
19
  #include "engine/SimplxShim.hpp"
20
  #include "common/OrderBook.hpp"
21
  #include "actors/Events.hpp"
 
22
 
23
  namespace eunex {
24
 
@@ -28,7 +29,8 @@ public:
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);
@@ -38,6 +40,7 @@ private:
38
  OrderBook book_;
39
  tredzone::Actor::Event::Pipe oePipe_;
40
  tredzone::Actor::Event::Pipe mdPipe_;
 
41
 
42
  void publishBookUpdate();
43
  };
 
19
  #include "engine/SimplxShim.hpp"
20
  #include "common/OrderBook.hpp"
21
  #include "actors/Events.hpp"
22
+ #include <optional>
23
 
24
  namespace eunex {
25
 
 
29
 
30
  OrderBookActor(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);
 
40
  OrderBook book_;
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
  };
src/common/OrderBook.cpp CHANGED
@@ -100,6 +100,8 @@ void OrderBook::matchBuy(Order& incoming, const TradeCallback& onTrade,
100
  trade.sellOrderId = orderIt->orderId;
101
  trade.buyClOrdId = incoming.clOrdId;
102
  trade.sellClOrdId = orderIt->clOrdId;
 
 
103
  trade.matchTime = nowNs();
104
  onTrade(trade);
105
 
@@ -155,6 +157,8 @@ void OrderBook::matchSell(Order& incoming, const TradeCallback& onTrade,
155
  trade.sellOrderId = incoming.orderId;
156
  trade.buyClOrdId = orderIt->clOrdId;
157
  trade.sellClOrdId = incoming.clOrdId;
 
 
158
  trade.matchTime = nowNs();
159
  onTrade(trade);
160
 
 
100
  trade.sellOrderId = orderIt->orderId;
101
  trade.buyClOrdId = incoming.clOrdId;
102
  trade.sellClOrdId = orderIt->clOrdId;
103
+ trade.buySessionId = incoming.sessionId;
104
+ trade.sellSessionId = orderIt->sessionId;
105
  trade.matchTime = nowNs();
106
  onTrade(trade);
107
 
 
157
  trade.sellOrderId = incoming.orderId;
158
  trade.buyClOrdId = orderIt->clOrdId;
159
  trade.sellClOrdId = incoming.clOrdId;
160
+ trade.buySessionId = orderIt->sessionId;
161
+ trade.sellSessionId = incoming.sessionId;
162
  trade.matchTime = nowNs();
163
  onTrade(trade);
164
 
src/common/Types.hpp CHANGED
@@ -29,6 +29,7 @@ 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 {
@@ -104,6 +105,8 @@ struct Trade {
104
  ClOrdId_t buyClOrdId;
105
  ClOrdId_t sellClOrdId;
106
  Timestamp_ns matchTime;
 
 
107
  };
108
  #pragma pack(pop)
109
 
@@ -119,6 +122,13 @@ struct ExecutionReport {
119
  TradeId_t tradeId;
120
  };
121
 
 
 
 
 
 
 
 
122
  // ── Symbol definition ──────────────────────────────────────────────
123
  struct SymbolDef {
124
  SymbolIndex_t index;
 
29
  using SymbolIndex_t = uint32_t;
30
  using TradeId_t = uint64_t;
31
  using SessionId_t = uint16_t;
32
+ using MemberId_t = uint16_t;
33
 
34
  // ── Enumerations matching Optiq/SBE definitions ────────────────────
35
  enum class Side : uint8_t {
 
105
  ClOrdId_t buyClOrdId;
106
  ClOrdId_t sellClOrdId;
107
  Timestamp_ns matchTime;
108
+ SessionId_t buySessionId;
109
+ SessionId_t sellSessionId;
110
  };
111
  #pragma pack(pop)
112
 
 
122
  TradeId_t tradeId;
123
  };
124
 
125
+ // ── Member holding (for Clearing House) ───────────────────────────
126
+ struct MemberHolding {
127
+ SymbolIndex_t symbolIdx;
128
+ int64_t quantity;
129
+ Price_t avgCost;
130
+ };
131
+
132
  // ── Symbol definition ──────────────────────────────────────────────
133
  struct SymbolDef {
134
  SymbolIndex_t index;
src/main.cpp CHANGED
@@ -3,135 +3,214 @@
3
  //
4
  // Multi-threaded actor topology (mirrors Optiq architecture):
5
  //
6
- // Core 0: OEGatewayActor (Order Entry β€” receives external orders)
7
  // Core 1: OrderBookActor per symbol (matching engine)
8
- // Core 2: MarketDataActor (publishes book updates, trades)
 
9
  //
10
  // Optiq equivalent topology:
11
  // OEActor β†’ LogicalCoreActor (Book) β†’ MDLimit β†’ MDIMP
12
  // β†’ OE Ack (back to OEActor)
 
13
  // ════════════════════════════════════════════════════════════════════
14
 
15
  #include "engine/SimplxShim.hpp"
16
  #include "actors/OrderBookActor.hpp"
17
  #include "actors/OEGatewayActor.hpp"
18
  #include "actors/MarketDataActor.hpp"
 
 
 
19
  #include <iostream>
 
 
 
 
20
 
21
  using namespace tredzone;
22
  using namespace eunex;
23
 
 
 
 
 
24
  int main() {
25
  std::cout << "═══════════════════════════════════════════\n";
26
- std::cout << " EuNEx Matching Engine v0.2\n";
27
- std::cout << " Multi-threaded actor engine\n";
28
  std::cout << "═══════════════════════════════════════════\n\n";
29
 
30
- // ── Build actor topology ───────────────────────────────────────
 
 
 
 
 
 
 
 
 
 
 
31
  auto oeGateway = std::make_unique<OEGatewayActor>();
32
- auto mdActor = std::make_unique<MarketDataActor>();
33
 
34
- constexpr SymbolIndex_t SYM_AAPL = 1;
35
- constexpr SymbolIndex_t SYM_MSFT = 2;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36
 
 
37
  auto bookAAPL = std::make_unique<OrderBookActor>(
38
- SYM_AAPL, oeGateway->getActorId(), mdActor->getActorId());
39
  auto bookMSFT = std::make_unique<OrderBookActor>(
40
- SYM_MSFT, oeGateway->getActorId(), mdActor->getActorId());
 
 
 
 
41
 
42
  oeGateway->mapSymbol(SYM_AAPL, bookAAPL->getActorId());
43
  oeGateway->mapSymbol(SYM_MSFT, bookMSFT->getActorId());
44
-
45
- std::cout << "Actors created:\n";
46
- std::cout << " OEGateway (id=" << oeGateway->getActorId().id << ")\n";
47
- std::cout << " MarketData (id=" << mdActor->getActorId().id << ")\n";
48
- std::cout << " Book AAPL (id=" << bookAAPL->getActorId().id << ")\n";
49
- std::cout << " Book MSFT (id=" << bookMSFT->getActorId().id << ")\n\n";
50
-
51
- // ── Submit orders ──────────────────────────────────────────────
52
- SessionId_t session = 1;
53
- std::cout << "── Submitting orders ──────────────────────\n\n";
54
-
55
- oeGateway->submitNewOrder(1001, SYM_AAPL, Side::Sell, OrderType::Limit,
56
- TimeInForce::Day, toFixedPrice(150.00), 100, session);
57
- std::cout << "SELL AAPL 100 @ 150.00\n";
58
-
59
- oeGateway->submitNewOrder(1002, SYM_AAPL, Side::Sell, OrderType::Limit,
60
- TimeInForce::Day, toFixedPrice(151.00), 50, session);
61
- std::cout << "SELL AAPL 50 @ 151.00\n";
62
-
63
- oeGateway->submitNewOrder(1003, SYM_AAPL, Side::Buy, OrderType::Limit,
64
- TimeInForce::Day, toFixedPrice(150.00), 75, session);
65
- std::cout << "BUY AAPL 75 @ 150.00 (should match 75 of sell@150)\n";
66
-
67
- oeGateway->submitNewOrder(1004, SYM_AAPL, Side::Buy, OrderType::Market,
68
- TimeInForce::IOC, NULL_PRICE, 30, session);
69
- std::cout << "BUY AAPL 30 MARKET IOC (should match 25@150 + 5@151)\n";
70
-
71
- oeGateway->submitNewOrder(1005, SYM_AAPL, Side::Buy, OrderType::Limit,
72
- TimeInForce::FOK, toFixedPrice(151.00), 100, session);
73
- std::cout << "BUY AAPL 100 @ 151.00 FOK (should be rejected)\n";
74
-
75
- oeGateway->submitNewOrder(2001, SYM_MSFT, Side::Buy, OrderType::Limit,
76
- TimeInForce::Day, toFixedPrice(320.50), 200, session);
77
- std::cout << "BUY MSFT 200 @ 320.50\n";
78
-
79
- oeGateway->submitNewOrder(2002, SYM_MSFT, Side::Sell, OrderType::Limit,
80
- TimeInForce::Day, toFixedPrice(320.50), 150, session);
81
- std::cout << "SELL MSFT 150 @ 320.50 (should match 150)\n";
82
-
83
- // ── Print results ──────────────────────────────────────────────
84
- std::cout << "\n── Execution Reports ─────────────────────\n\n";
85
-
86
- auto statusStr = [](OrderStatus s) -> const char* {
87
- switch (s) {
88
- case OrderStatus::New: return "NEW";
89
- case OrderStatus::PartiallyFilled: return "PARTIAL";
90
- case OrderStatus::Filled: return "FILLED";
91
- case OrderStatus::Cancelled: return "CANCELLED";
92
- case OrderStatus::Rejected: return "REJECTED";
93
- default: return "UNKNOWN";
94
- }
95
  };
96
 
97
- for (auto& rpt : oeGateway->getReports()) {
98
- std::cout << " ClOrdId=" << rpt.clOrdId
99
- << " OrderId=" << rpt.orderId
100
- << " Status=" << statusStr(rpt.status)
101
- << " Filled=" << rpt.filledQty
102
- << " Remaining=" << rpt.remainingQty;
103
- if (rpt.lastQty > 0) {
104
- std::cout << " LastPx=" << toDouble(rpt.lastPrice)
105
- << " LastQty=" << rpt.lastQty;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
106
  }
107
- std::cout << "\n";
 
108
  }
109
 
110
- // ── Print market data ──────────────────────────────────────────
111
- std::cout << "\n── Market Data ───────────────────────────\n\n";
 
112
 
113
- auto printSnapshot = [&](SymbolIndex_t sym, const char* name) {
 
114
  auto* snap = mdActor->getSnapshot(sym);
115
  if (snap) {
116
- std::cout << " " << name << ":"
117
- << " LastPx=" << toDouble(snap->lastTradePrice)
118
- << " BestBid=" << toDouble(snap->bestBid)
119
- << " BestAsk=" << toDouble(snap->bestAsk)
120
- << " Trades=" << snap->tradeCount << "\n";
121
  }
122
- };
123
-
124
- printSnapshot(SYM_AAPL, "AAPL");
125
- printSnapshot(SYM_MSFT, "MSFT");
126
 
127
- std::cout << "\n Recent trades: " << mdActor->getRecentTrades().size() << "\n";
128
- for (auto& t : mdActor->getRecentTrades()) {
129
- const char* sym = (t.symbolIdx == SYM_AAPL) ? "AAPL" : "MSFT";
130
- std::cout << " " << sym << " " << t.quantity << " @ " << toDouble(t.price)
131
- << " (buy=" << t.buyOrderId << " sell=" << t.sellOrderId << ")\n";
 
 
 
132
  }
133
 
134
- std::cout << "\n═══════════════════════════════════════════\n";
 
135
  std::cout << " Engine stopped.\n";
136
  return 0;
137
  }
 
3
  //
4
  // Multi-threaded actor topology (mirrors Optiq architecture):
5
  //
6
+ // Core 0: OEGatewayActor + FIXGatewayActor
7
  // Core 1: OrderBookActor per symbol (matching engine)
8
+ // Core 2: MarketDataActor
9
+ // Core 3: ClearingHouseActor + AITraderActor
10
  //
11
  // Optiq equivalent topology:
12
  // OEActor β†’ LogicalCoreActor (Book) β†’ MDLimit β†’ MDIMP
13
  // β†’ OE Ack (back to OEActor)
14
+ // β†’ ClearingHouse (via Kafka/PTB)
15
  // ════════════════════════════════════════════════════════════════════
16
 
17
  #include "engine/SimplxShim.hpp"
18
  #include "actors/OrderBookActor.hpp"
19
  #include "actors/OEGatewayActor.hpp"
20
  #include "actors/MarketDataActor.hpp"
21
+ #include "actors/ClearingHouseActor.hpp"
22
+ #include "actors/FIXGatewayActor.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;
32
 
33
+ static std::atomic<bool> g_running{true};
34
+
35
+ void signalHandler(int) { g_running = false; }
36
+
37
  int main() {
38
  std::cout << "═══════════════════════════════════════════\n";
39
+ std::cout << " EuNEx Matching Engine v0.4\n";
40
+ std::cout << " C++ Actor Architecture (Optiq model)\n";
41
  std::cout << "═══════════════════════════════════════════\n\n";
42
 
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;
49
+ constexpr SymbolIndex_t SYM_GOOGL = 3;
50
+ constexpr SymbolIndex_t SYM_EURO50 = 4;
51
+
52
+ std::vector<SymbolIndex_t> allSymbols = {SYM_AAPL, SYM_MSFT, SYM_GOOGL, SYM_EURO50};
53
+
54
+ // ── Core 0: OE Gateway ────────────────────────────────────────
55
  auto oeGateway = std::make_unique<OEGatewayActor>();
 
56
 
57
+ // ── Core 2: Market Data ───────────────────────────────────────
58
+ auto mdActor = std::make_unique<MarketDataActor>();
59
+
60
+ // ── Core 3: Clearing House ────────────────────────────────────
61
+ auto chActor = std::make_unique<ClearingHouseActor>();
62
+
63
+ // Map AI trader sessions to clearing house members
64
+ for (int i = 0; i < 10; ++i) {
65
+ chActor->mapSession(static_cast<SessionId_t>(200 + i),
66
+ static_cast<MemberId_t>(i + 1));
67
+ }
68
+ // Map FIX gateway sessions (100-109) to members too
69
+ for (int i = 0; i < 10; ++i) {
70
+ chActor->mapSession(static_cast<SessionId_t>(100 + i),
71
+ static_cast<MemberId_t>(i + 1));
72
+ }
73
 
74
+ // ── Core 1: Order Books (per symbol) ──────────────────────────
75
  auto bookAAPL = std::make_unique<OrderBookActor>(
76
+ SYM_AAPL, oeGateway->getActorId(), mdActor->getActorId(), chActor->getActorId());
77
  auto bookMSFT = std::make_unique<OrderBookActor>(
78
+ SYM_MSFT, oeGateway->getActorId(), mdActor->getActorId(), chActor->getActorId());
79
+ auto bookGOOGL = std::make_unique<OrderBookActor>(
80
+ SYM_GOOGL, oeGateway->getActorId(), mdActor->getActorId(), chActor->getActorId());
81
+ auto bookEURO50 = std::make_unique<OrderBookActor>(
82
+ SYM_EURO50, oeGateway->getActorId(), mdActor->getActorId(), chActor->getActorId());
83
 
84
  oeGateway->mapSymbol(SYM_AAPL, bookAAPL->getActorId());
85
  oeGateway->mapSymbol(SYM_MSFT, bookMSFT->getActorId());
86
+ oeGateway->mapSymbol(SYM_GOOGL, bookGOOGL->getActorId());
87
+ oeGateway->mapSymbol(SYM_EURO50, bookEURO50->getActorId());
88
+
89
+ // ── Core 0: FIX Gateway ──────────────────────────────────────
90
+ auto fixGateway = std::make_unique<FIXGatewayActor>(oeGateway->getActorId(), 9001);
91
+
92
+ // ── Core 3: AI Trader ─────────────────────────────────────────
93
+ auto aiTrader = std::make_unique<AITraderActor>(oeGateway->getActorId(), allSymbols);
94
+
95
+ // ── Wire exec report subscribers ──────────────────────────────
96
+ oeGateway->addExecReportSubscriber(fixGateway->getActorId());
97
+ oeGateway->addExecReportSubscriber(aiTrader->getActorId());
98
+
99
+ // ── Print topology ────────────────────────────────────────────
100
+ std::cout << "Actor topology:\n";
101
+ std::cout << " Core 0: OEGateway (id=" << oeGateway->getActorId().id
102
+ << "), FIXGateway (id=" << fixGateway->getActorId().id << ")\n";
103
+ std::cout << " Core 1: Book AAPL (id=" << bookAAPL->getActorId().id
104
+ << "), MSFT (id=" << bookMSFT->getActorId().id
105
+ << "), GOOGL (id=" << bookGOOGL->getActorId().id
106
+ << "), EURO50 (id=" << bookEURO50->getActorId().id << ")\n";
107
+ std::cout << " Core 2: MarketData (id=" << mdActor->getActorId().id << ")\n";
108
+ std::cout << " Core 3: ClearingHouse (id=" << chActor->getActorId().id
109
+ << "), AITrader (id=" << aiTrader->getActorId().id << ")\n\n";
110
+
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";
118
+ SessionId_t seedSession = 200;
119
+
120
+ struct SeedOrder { SymbolIndex_t sym; Side side; double price; Quantity_t qty; };
121
+ SeedOrder seeds[] = {
122
+ {SYM_AAPL, Side::Sell, 155.00, 100}, {SYM_AAPL, Side::Sell, 154.00, 200},
123
+ {SYM_AAPL, Side::Buy, 153.00, 150}, {SYM_AAPL, Side::Buy, 152.00, 100},
124
+ {SYM_MSFT, Side::Sell, 325.00, 100}, {SYM_MSFT, Side::Sell, 324.00, 150},
125
+ {SYM_MSFT, Side::Buy, 323.00, 200}, {SYM_MSFT, Side::Buy, 322.00, 100},
126
+ {SYM_GOOGL, Side::Sell, 142.00, 100}, {SYM_GOOGL, Side::Sell, 141.00, 200},
127
+ {SYM_GOOGL, Side::Buy, 140.00, 150}, {SYM_GOOGL, Side::Buy, 139.00, 100},
128
+ {SYM_EURO50, Side::Sell, 5050.00, 50}, {SYM_EURO50, Side::Sell, 5040.00, 80},
129
+ {SYM_EURO50, Side::Buy, 5030.00, 60}, {SYM_EURO50, Side::Buy, 5020.00, 40},
 
 
 
 
 
 
 
130
  };
131
 
132
+ ClOrdId_t seedClOrd = 1;
133
+ for (auto& s : seeds) {
134
+ oeGateway->submitNewOrder(seedClOrd++, s.sym, s.side, OrderType::Limit,
135
+ TimeInForce::Day, toFixedPrice(s.price), s.qty, seedSession);
136
+ }
137
+ oeGateway->clearReports();
138
+
139
+ std::cout << "Initial orders seeded.\n\n";
140
+
141
+ // ── Run AI trading rounds ─────────────────────────────────────
142
+ std::cout << "Running AI trading... (Ctrl+C to stop)\n";
143
+ std::cout << "FIX clients can connect to localhost:9001\n\n";
144
+
145
+ int round = 0;
146
+ while (g_running) {
147
+ aiTrader->onCallback();
148
+ round++;
149
+
150
+ if (round % 10 == 0) {
151
+ std::cout << "── Round " << round << " ──\n";
152
+
153
+ auto leaderboard = chActor->getLeaderboard();
154
+ std::cout << " Leaderboard:\n";
155
+ for (int i = 0; i < std::min(5, static_cast<int>(leaderboard.size())); ++i) {
156
+ auto& e = leaderboard[i];
157
+ std::cout << " " << e.name
158
+ << " Capital=" << static_cast<int>(e.capital)
159
+ << " P&L=" << static_cast<int>(e.pnl)
160
+ << " Trades=" << e.tradeCount << "\n";
161
+ }
162
+
163
+ auto printBBO = [&](SymbolIndex_t sym, const char* name) {
164
+ auto* snap = mdActor->getSnapshot(sym);
165
+ if (snap) {
166
+ std::cout << " " << name << ":"
167
+ << " Bid=" << toDouble(snap->bestBid)
168
+ << " Ask=" << toDouble(snap->bestAsk)
169
+ << " Last=" << toDouble(snap->lastTradePrice)
170
+ << " Trades=" << snap->tradeCount << "\n";
171
+ }
172
+ };
173
+
174
+ printBBO(SYM_AAPL, "AAPL");
175
+ printBBO(SYM_MSFT, "MSFT");
176
+ printBBO(SYM_GOOGL, "GOOGL");
177
+ printBBO(SYM_EURO50, "EURO50");
178
+
179
+ if (fixGateway->isRunning()) {
180
+ std::cout << " FIX clients: " << fixGateway->clientCount() << "\n";
181
+ }
182
+ std::cout << "\n";
183
  }
184
+
185
+ std::this_thread::sleep_for(std::chrono::seconds(3));
186
  }
187
 
188
+ // ── Shutdown ──────────────────────────────────────────────────
189
+ std::cout << "\nShutting down...\n";
190
+ fixGateway->stop();
191
 
192
+ std::cout << "\n── Final Market Data ─────────────────────\n";
193
+ for (auto sym : allSymbols) {
194
  auto* snap = mdActor->getSnapshot(sym);
195
  if (snap) {
196
+ const char* names[] = {"", "AAPL", "MSFT", "GOOGL", "EURO50"};
197
+ std::cout << " " << names[sym] << ": "
198
+ << snap->tradeCount << " trades, last=" << toDouble(snap->lastTradePrice) << "\n";
 
 
199
  }
200
+ }
 
 
 
201
 
202
+ std::cout << "\n── Final Leaderboard ─────────────────────\n";
203
+ auto lb = chActor->getLeaderboard();
204
+ for (auto& e : lb) {
205
+ std::cout << " " << e.name
206
+ << " Capital=" << static_cast<int>(e.capital)
207
+ << " P&L=" << static_cast<int>(e.pnl)
208
+ << " Trades=" << e.tradeCount
209
+ << " Holdings=" << e.holdingCount << "\n";
210
  }
211
 
212
+ std::cout << "\nTrades processed: " << mdActor->getRecentTrades().size() << "\n";
213
+ std::cout << "═══════════════════════════════════════════\n";
214
  std::cout << " Engine stopped.\n";
215
  return 0;
216
  }
src/net/SocketCompat.hpp ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #pragma once
2
+ // Cross-platform socket abstraction for FIXGatewayActor
3
+
4
+ #ifdef _WIN32
5
+ #ifndef WIN32_LEAN_AND_MEAN
6
+ #define WIN32_LEAN_AND_MEAN
7
+ #endif
8
+ #ifndef NOMINMAX
9
+ #define NOMINMAX
10
+ #endif
11
+ #include <winsock2.h>
12
+ #include <ws2tcpip.h>
13
+
14
+ using socket_t = SOCKET;
15
+ constexpr socket_t INVALID_SOCK = INVALID_SOCKET;
16
+
17
+ inline int closeSocket(socket_t s) { return closesocket(s); }
18
+ inline int socketError() { return WSAGetLastError(); }
19
+
20
+ struct SocketInit {
21
+ SocketInit() { WSADATA w; WSAStartup(MAKEWORD(2,2), &w); }
22
+ ~SocketInit() { WSACleanup(); }
23
+ };
24
+
25
+ #else
26
+ #include <sys/socket.h>
27
+ #include <netinet/in.h>
28
+ #include <netinet/tcp.h>
29
+ #include <arpa/inet.h>
30
+ #include <unistd.h>
31
+ #include <cerrno>
32
+ #include <fcntl.h>
33
+
34
+ using socket_t = int;
35
+ constexpr socket_t INVALID_SOCK = -1;
36
+
37
+ inline int closeSocket(socket_t s) { return close(s); }
38
+ inline int socketError() { return errno; }
39
+
40
+ struct SocketInit {
41
+ SocketInit() {}
42
+ ~SocketInit() {}
43
+ };
44
+
45
+ #endif
46
+
47
+ inline bool setSocketNonBlocking(socket_t s) {
48
+ #ifdef _WIN32
49
+ u_long mode = 1;
50
+ return ioctlsocket(s, FIONBIO, &mode) == 0;
51
+ #else
52
+ int flags = fcntl(s, F_GETFL, 0);
53
+ return fcntl(s, F_SETFL, flags | O_NONBLOCK) == 0;
54
+ #endif
55
+ }
56
+
57
+ inline bool setSocketReuseAddr(socket_t s) {
58
+ int opt = 1;
59
+ return setsockopt(s, SOL_SOCKET, SO_REUSEADDR,
60
+ reinterpret_cast<const char*>(&opt), sizeof(opt)) == 0;
61
+ }
tests/test_ai_trader.cpp ADDED
@@ -0,0 +1,157 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // ════════════════════════════════════════════════════════════════════
2
+ // AITraderActor tests β€” strategy execution and order submission
3
+ // ════════════════════════════════════════════════════════════════════
4
+
5
+ #include "actors/AITraderActor.hpp"
6
+ #include "actors/OEGatewayActor.hpp"
7
+ #include "actors/OrderBookActor.hpp"
8
+ #include "actors/MarketDataActor.hpp"
9
+ #include "actors/ClearingHouseActor.hpp"
10
+ #include <iostream>
11
+ #include <cassert>
12
+
13
+ using namespace eunex;
14
+
15
+ static int testsPassed = 0;
16
+ static int testsFailed = 0;
17
+
18
+ #define TEST(name) \
19
+ std::cout << " " << #name << "... "; \
20
+ try { test_##name(); std::cout << "PASS\n"; ++testsPassed; } \
21
+ catch (const std::exception& e) { std::cout << "FAIL: " << e.what() << "\n"; ++testsFailed; }
22
+
23
+ #define ASSERT_EQ(a, b) \
24
+ if ((a) != (b)) throw std::runtime_error( \
25
+ std::string("Expected ") + std::to_string(static_cast<long long>(b)) + " got " + std::to_string(static_cast<long long>(a)))
26
+
27
+ #define ASSERT_TRUE(x) \
28
+ if (!(x)) throw std::runtime_error("Assertion failed: " #x)
29
+
30
+ // ── Tests ─────────────────────────────────────────────────────────
31
+
32
+ void test_ai_trader_creates() {
33
+ auto oe = std::make_unique<OEGatewayActor>();
34
+ std::vector<SymbolIndex_t> syms = {1, 2};
35
+ auto ai = std::make_unique<AITraderActor>(oe->getActorId(), syms);
36
+ ASSERT_TRUE(ai != nullptr);
37
+ }
38
+
39
+ void test_ai_submits_orders() {
40
+ auto oe = std::make_unique<OEGatewayActor>();
41
+ auto md = std::make_unique<MarketDataActor>();
42
+
43
+ auto book1 = std::make_unique<OrderBookActor>(1, oe->getActorId(), md->getActorId());
44
+ auto book2 = std::make_unique<OrderBookActor>(2, oe->getActorId(), md->getActorId());
45
+ oe->mapSymbol(1, book1->getActorId());
46
+ oe->mapSymbol(2, book2->getActorId());
47
+
48
+ std::vector<SymbolIndex_t> syms = {1, 2};
49
+ auto ai = std::make_unique<AITraderActor>(oe->getActorId(), syms);
50
+
51
+ ai->onCallback();
52
+
53
+ ASSERT_TRUE(oe->getReports().size() > 0);
54
+ }
55
+
56
+ void test_ai_responds_to_book_update() {
57
+ auto oe = std::make_unique<OEGatewayActor>();
58
+ auto md = std::make_unique<MarketDataActor>();
59
+ auto book = std::make_unique<OrderBookActor>(1, oe->getActorId(), md->getActorId());
60
+ oe->mapSymbol(1, book->getActorId());
61
+
62
+ std::vector<SymbolIndex_t> syms = {1};
63
+ auto ai = std::make_unique<AITraderActor>(oe->getActorId(), syms);
64
+
65
+ BookUpdateEvent bue;
66
+ bue.symbolIdx = 1;
67
+ bue.bidDepth = 1;
68
+ bue.askDepth = 1;
69
+ bue.bids[0] = {toFixedPrice(150.0), 100};
70
+ bue.asks[0] = {toFixedPrice(151.0), 100};
71
+ ai->onEvent(bue);
72
+
73
+ ai->onCallback();
74
+
75
+ ASSERT_TRUE(oe->getReports().size() > 0);
76
+ }
77
+
78
+ void test_ai_responds_to_trade() {
79
+ auto oe = std::make_unique<OEGatewayActor>();
80
+ std::vector<SymbolIndex_t> syms = {1};
81
+ auto ai = std::make_unique<AITraderActor>(oe->getActorId(), syms);
82
+
83
+ Trade t{};
84
+ t.symbolIdx = 1;
85
+ t.price = toFixedPrice(150.0);
86
+ t.quantity = 10;
87
+ TradeEvent evt(t);
88
+
89
+ for (int i = 0; i < 10; ++i) {
90
+ t.price = toFixedPrice(150.0 + i * 0.5);
91
+ TradeEvent e(t);
92
+ ai->onEvent(e);
93
+ }
94
+
95
+ ASSERT_TRUE(true);
96
+ }
97
+
98
+ void test_ai_with_clearing_house() {
99
+ auto oe = std::make_unique<OEGatewayActor>();
100
+ auto md = std::make_unique<MarketDataActor>();
101
+ auto ch = std::make_unique<ClearingHouseActor>();
102
+
103
+ for (int i = 0; i < 10; ++i) {
104
+ ch->mapSession(static_cast<SessionId_t>(200 + i),
105
+ static_cast<MemberId_t>(i + 1));
106
+ }
107
+
108
+ auto book = std::make_unique<OrderBookActor>(
109
+ 1, oe->getActorId(), md->getActorId(), ch->getActorId());
110
+ oe->mapSymbol(1, book->getActorId());
111
+
112
+ std::vector<SymbolIndex_t> syms = {1};
113
+ auto ai = std::make_unique<AITraderActor>(oe->getActorId(), syms);
114
+
115
+ for (int i = 0; i < 5; ++i) {
116
+ ai->onCallback();
117
+ }
118
+
119
+ auto lb = ch->getLeaderboard();
120
+ ASSERT_EQ(static_cast<int>(lb.size()), 10);
121
+ }
122
+
123
+ void test_multiple_symbols() {
124
+ auto oe = std::make_unique<OEGatewayActor>();
125
+ auto md = std::make_unique<MarketDataActor>();
126
+
127
+ std::vector<SymbolIndex_t> syms = {1, 2, 3, 4};
128
+ std::vector<std::unique_ptr<OrderBookActor>> books;
129
+ for (auto s : syms) {
130
+ auto book = std::make_unique<OrderBookActor>(s, oe->getActorId(), md->getActorId());
131
+ oe->mapSymbol(s, book->getActorId());
132
+ books.push_back(std::move(book));
133
+ }
134
+
135
+ auto ai = std::make_unique<AITraderActor>(oe->getActorId(), syms);
136
+ ai->onCallback();
137
+
138
+ ASSERT_TRUE(oe->getReports().size() > 0);
139
+ }
140
+
141
+ // ── Main ────────────────────────────���─────────────────────────────
142
+
143
+ int main() {
144
+ std::cout << "AITrader Tests\n";
145
+ std::cout << "═══════════════════════════════════════════\n";
146
+
147
+ TEST(ai_trader_creates);
148
+ TEST(ai_submits_orders);
149
+ TEST(ai_responds_to_book_update);
150
+ TEST(ai_responds_to_trade);
151
+ TEST(ai_with_clearing_house);
152
+ TEST(multiple_symbols);
153
+
154
+ std::cout << "═══════════════════════════════════════════\n";
155
+ std::cout << testsPassed << " passed, " << testsFailed << " failed\n";
156
+ return testsFailed > 0 ? 1 : 0;
157
+ }
tests/test_clearing_house.cpp ADDED
@@ -0,0 +1,184 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // ════════════════════════════════════════════════════════════════════
2
+ // ClearingHouseActor tests
3
+ // ════════════════════════════════════════════════════════════════════
4
+
5
+ #include "actors/ClearingHouseActor.hpp"
6
+ #include "actors/OrderBookActor.hpp"
7
+ #include "actors/OEGatewayActor.hpp"
8
+ #include "actors/MarketDataActor.hpp"
9
+ #include <iostream>
10
+ #include <cassert>
11
+
12
+ using namespace eunex;
13
+
14
+ static int testsPassed = 0;
15
+ static int testsFailed = 0;
16
+
17
+ #define TEST(name) \
18
+ std::cout << " " << #name << "... "; \
19
+ try { test_##name(); std::cout << "PASS\n"; ++testsPassed; } \
20
+ catch (const std::exception& e) { std::cout << "FAIL: " << e.what() << "\n"; ++testsFailed; }
21
+
22
+ #define ASSERT_EQ(a, b) \
23
+ if ((a) != (b)) throw std::runtime_error( \
24
+ std::string("Expected ") + std::to_string(static_cast<long long>(b)) + " got " + std::to_string(static_cast<long long>(a)))
25
+
26
+ #define ASSERT_TRUE(x) \
27
+ if (!(x)) throw std::runtime_error("Assertion failed: " #x)
28
+
29
+ // ── Tests ─────────────────────────────────────────────────────────
30
+
31
+ void test_initial_members() {
32
+ ClearingHouseActor ch;
33
+ auto lb = ch.getLeaderboard();
34
+ ASSERT_EQ(static_cast<int>(lb.size()), 10);
35
+ for (auto& e : lb) {
36
+ ASSERT_EQ(static_cast<int>(e.capital), 100000);
37
+ ASSERT_EQ(static_cast<int>(e.pnl), 0);
38
+ ASSERT_EQ(e.tradeCount, 0);
39
+ }
40
+ }
41
+
42
+ void test_session_mapping() {
43
+ ClearingHouseActor ch;
44
+ ch.mapSession(100, 1);
45
+ ch.mapSession(101, 2);
46
+
47
+ Trade t{};
48
+ t.symbolIdx = 1;
49
+ t.price = toFixedPrice(150.0);
50
+ t.quantity = 10;
51
+ t.buySessionId = 100;
52
+ t.sellSessionId = 101;
53
+
54
+ TradeEvent evt(t);
55
+ ch.onEvent(evt);
56
+
57
+ auto lb = ch.getLeaderboard();
58
+ int totalTrades = 0;
59
+ for (auto& e : lb) totalTrades += e.tradeCount;
60
+ ASSERT_EQ(totalTrades, 2);
61
+ }
62
+
63
+ void test_buy_reduces_capital() {
64
+ ClearingHouseActor ch;
65
+ ch.mapSession(100, 1);
66
+
67
+ Trade t{};
68
+ t.symbolIdx = 1;
69
+ t.price = toFixedPrice(100.0);
70
+ t.quantity = 10;
71
+ t.buySessionId = 100;
72
+ t.sellSessionId = 999;
73
+
74
+ TradeEvent evt(t);
75
+ ch.onEvent(evt);
76
+
77
+ auto* m = ch.getMember(1);
78
+ ASSERT_TRUE(m != nullptr);
79
+ ASSERT_TRUE(m->capital < 100000.0);
80
+ ASSERT_EQ(static_cast<int>(m->capital), 99000);
81
+ }
82
+
83
+ void test_sell_increases_capital() {
84
+ ClearingHouseActor ch;
85
+ ch.mapSession(100, 1);
86
+
87
+ Trade t{};
88
+ t.symbolIdx = 1;
89
+ t.price = toFixedPrice(100.0);
90
+ t.quantity = 10;
91
+ t.buySessionId = 999;
92
+ t.sellSessionId = 100;
93
+
94
+ TradeEvent evt(t);
95
+ ch.onEvent(evt);
96
+
97
+ auto* m = ch.getMember(1);
98
+ ASSERT_TRUE(m != nullptr);
99
+ ASSERT_TRUE(m->capital > 100000.0);
100
+ ASSERT_EQ(static_cast<int>(m->capital), 101000);
101
+ }
102
+
103
+ void test_holdings_tracked() {
104
+ ClearingHouseActor ch;
105
+ ch.mapSession(100, 1);
106
+
107
+ Trade t{};
108
+ t.symbolIdx = 1;
109
+ t.price = toFixedPrice(150.0);
110
+ t.quantity = 10;
111
+ t.buySessionId = 100;
112
+ t.sellSessionId = 999;
113
+
114
+ TradeEvent evt(t);
115
+ ch.onEvent(evt);
116
+
117
+ auto* m = ch.getMember(1);
118
+ ASSERT_TRUE(m != nullptr);
119
+ ASSERT_EQ(static_cast<int>(m->holdings.size()), 1);
120
+ auto hIt = m->holdings.find(1);
121
+ ASSERT_TRUE(hIt != m->holdings.end());
122
+ ASSERT_EQ(hIt->second.quantity, 10);
123
+ }
124
+
125
+ void test_leaderboard_sorted() {
126
+ ClearingHouseActor ch;
127
+ ch.mapSession(100, 1);
128
+ ch.mapSession(101, 2);
129
+
130
+ Trade t1{};
131
+ t1.symbolIdx = 1;
132
+ t1.price = toFixedPrice(100.0);
133
+ t1.quantity = 50;
134
+ t1.buySessionId = 100;
135
+ t1.sellSessionId = 101;
136
+
137
+ TradeEvent evt1(t1);
138
+ ch.onEvent(evt1);
139
+
140
+ auto lb = ch.getLeaderboard();
141
+ ASSERT_TRUE(lb[0].capital >= lb[1].capital);
142
+ }
143
+
144
+ void test_trade_with_clearing_pipe() {
145
+ auto oe = std::make_unique<OEGatewayActor>();
146
+ auto md = std::make_unique<MarketDataActor>();
147
+ auto ch = std::make_unique<ClearingHouseActor>();
148
+
149
+ ch->mapSession(1, 1);
150
+ ch->mapSession(2, 2);
151
+
152
+ auto book = std::make_unique<OrderBookActor>(
153
+ 1, oe->getActorId(), md->getActorId(), ch->getActorId());
154
+ oe->mapSymbol(1, book->getActorId());
155
+
156
+ oe->submitNewOrder(1, 1, Side::Sell, OrderType::Limit,
157
+ TimeInForce::Day, toFixedPrice(100.0), 50, 1);
158
+ oe->submitNewOrder(2, 1, Side::Buy, OrderType::Limit,
159
+ TimeInForce::Day, toFixedPrice(100.0), 50, 2);
160
+
161
+ auto lb = ch->getLeaderboard();
162
+ int totalTrades = 0;
163
+ for (auto& e : lb) totalTrades += e.tradeCount;
164
+ ASSERT_TRUE(totalTrades >= 2);
165
+ }
166
+
167
+ // ── Main ───────────────────────────────────���──────────────────────
168
+
169
+ int main() {
170
+ std::cout << "ClearingHouse Tests\n";
171
+ std::cout << "═══════════════════════════════════════════\n";
172
+
173
+ TEST(initial_members);
174
+ TEST(session_mapping);
175
+ TEST(buy_reduces_capital);
176
+ TEST(sell_increases_capital);
177
+ TEST(holdings_tracked);
178
+ TEST(leaderboard_sorted);
179
+ TEST(trade_with_clearing_pipe);
180
+
181
+ std::cout << "═══════════════════════════════════════════\n";
182
+ std::cout << testsPassed << " passed, " << testsFailed << " failed\n";
183
+ return testsFailed > 0 ? 1 : 0;
184
+ }
tests/test_fix_gateway.cpp ADDED
@@ -0,0 +1,102 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // ════════════════════════════════════════════════════════════════════
2
+ // FIXGatewayActor tests β€” protocol parsing and symbol mapping
3
+ // ════════════════════════════════════════════════════════════════════
4
+
5
+ #include "actors/FIXGatewayActor.hpp"
6
+ #include "actors/OEGatewayActor.hpp"
7
+ #include "actors/OrderBookActor.hpp"
8
+ #include "actors/MarketDataActor.hpp"
9
+ #include <iostream>
10
+ #include <cassert>
11
+ #include <string>
12
+
13
+ using namespace eunex;
14
+
15
+ static int testsPassed = 0;
16
+ static int testsFailed = 0;
17
+
18
+ #define TEST(name) \
19
+ std::cout << " " << #name << "... "; \
20
+ try { test_##name(); std::cout << "PASS\n"; ++testsPassed; } \
21
+ catch (const std::exception& e) { std::cout << "FAIL: " << e.what() << "\n"; ++testsFailed; }
22
+
23
+ #define ASSERT_EQ(a, b) \
24
+ if ((a) != (b)) throw std::runtime_error( \
25
+ std::string("Expected ") + std::to_string(static_cast<long long>(b)) + " got " + std::to_string(static_cast<long long>(a)))
26
+
27
+ #define ASSERT_TRUE(x) \
28
+ if (!(x)) throw std::runtime_error("Assertion failed: " #x)
29
+
30
+ // ── Tests ─────────────────────────────────────────────────────────
31
+
32
+ void test_symbol_from_string() {
33
+ ASSERT_EQ(FIXGatewayActor::symbolFromString("AAPL"), 1u);
34
+ ASSERT_EQ(FIXGatewayActor::symbolFromString("MSFT"), 2u);
35
+ ASSERT_EQ(FIXGatewayActor::symbolFromString("GOOGL"), 3u);
36
+ ASSERT_EQ(FIXGatewayActor::symbolFromString("EURO50"), 4u);
37
+ }
38
+
39
+ void test_symbol_to_string() {
40
+ ASSERT_TRUE(FIXGatewayActor::symbolToString(1) == "AAPL");
41
+ ASSERT_TRUE(FIXGatewayActor::symbolToString(2) == "MSFT");
42
+ ASSERT_TRUE(FIXGatewayActor::symbolToString(3) == "GOOGL");
43
+ ASSERT_TRUE(FIXGatewayActor::symbolToString(4) == "EURO50");
44
+ ASSERT_TRUE(FIXGatewayActor::symbolToString(99) == "99");
45
+ }
46
+
47
+ void test_fix_gateway_creates() {
48
+ auto oe = std::make_unique<OEGatewayActor>();
49
+ auto fix = std::make_unique<FIXGatewayActor>(oe->getActorId(), 19010);
50
+ ASSERT_TRUE(fix->isRunning());
51
+ ASSERT_EQ(fix->clientCount(), 0);
52
+ fix->stop();
53
+ ASSERT_TRUE(!fix->isRunning());
54
+ }
55
+
56
+ void test_oe_gateway_routes_new_order_event() {
57
+ auto oe = std::make_unique<OEGatewayActor>();
58
+ auto md = std::make_unique<MarketDataActor>();
59
+ auto book = std::make_unique<OrderBookActor>(1, oe->getActorId(), md->getActorId());
60
+ oe->mapSymbol(1, book->getActorId());
61
+
62
+ NewOrderEvent evt(5001, 1, Side::Buy, OrderType::Limit, TimeInForce::Day,
63
+ toFixedPrice(150.0), 100, 1);
64
+ oe->onEvent(evt);
65
+
66
+ ASSERT_TRUE(oe->getReports().size() > 0);
67
+ ASSERT_EQ(oe->getReports().back().status, OrderStatus::New);
68
+ }
69
+
70
+ void test_exec_report_forwarding() {
71
+ auto oe = std::make_unique<OEGatewayActor>();
72
+ auto md = std::make_unique<MarketDataActor>();
73
+ auto book = std::make_unique<OrderBookActor>(1, oe->getActorId(), md->getActorId());
74
+ oe->mapSymbol(1, book->getActorId());
75
+
76
+ auto fix = std::make_unique<FIXGatewayActor>(oe->getActorId(), 19011);
77
+ oe->addExecReportSubscriber(fix->getActorId());
78
+
79
+ oe->submitNewOrder(1, 1, Side::Buy, OrderType::Limit,
80
+ TimeInForce::Day, toFixedPrice(100.0), 50, 1);
81
+
82
+ ASSERT_TRUE(oe->getReports().size() > 0);
83
+
84
+ fix->stop();
85
+ }
86
+
87
+ // ── Main ──────────────────────────────────────────────────────────
88
+
89
+ int main() {
90
+ std::cout << "FIXGateway Tests\n";
91
+ std::cout << "═══════════════════════════════════════════\n";
92
+
93
+ TEST(symbol_from_string);
94
+ TEST(symbol_to_string);
95
+ TEST(fix_gateway_creates);
96
+ TEST(oe_gateway_routes_new_order_event);
97
+ TEST(exec_report_forwarding);
98
+
99
+ std::cout << "═══════════════════════════════════════════\n";
100
+ std::cout << testsPassed << " passed, " << testsFailed << " failed\n";
101
+ return testsFailed > 0 ? 1 : 0;
102
+ }