v0.4.0: C++ FIX Gateway, Clearing House & AI Trader actors
Browse filesImplement 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 +21 -1
- src/actors/AITraderActor.cpp +183 -0
- src/actors/AITraderActor.hpp +84 -0
- src/actors/ClearingHouseActor.cpp +120 -0
- src/actors/ClearingHouseActor.hpp +68 -0
- src/actors/Events.hpp +5 -0
- src/actors/FIXGatewayActor.cpp +407 -0
- src/actors/FIXGatewayActor.hpp +91 -0
- src/actors/OEGatewayActor.cpp +26 -0
- src/actors/OEGatewayActor.hpp +7 -1
- src/actors/OrderBookActor.cpp +6 -2
- src/actors/OrderBookActor.hpp +4 -1
- src/common/OrderBook.cpp +4 -0
- src/common/Types.hpp +10 -0
- src/main.cpp +168 -89
- src/net/SocketCompat.hpp +61 -0
- tests/test_ai_trader.cpp +157 -0
- tests/test_clearing_house.cpp +184 -0
- tests/test_fix_gateway.cpp +102 -0
|
@@ -1,5 +1,5 @@
|
|
| 1 |
cmake_minimum_required(VERSION 3.16)
|
| 2 |
-
project(EuNEx VERSION 0.
|
| 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()
|
|
@@ -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
|
|
@@ -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
|
|
@@ -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
|
|
@@ -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
|
|
@@ -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) βββββββββββββββ
|
|
@@ -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
|
|
@@ -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
|
|
@@ -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
|
|
@@ -41,15 +41,21 @@ public:
|
|
| 41 |
SymbolIndex_t symbolIdx, Price_t newPrice,
|
| 42 |
Quantity_t newQty, SessionId_t session);
|
| 43 |
|
| 44 |
-
|
|
|
|
|
|
|
| 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 |
};
|
|
@@ -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) {
|
|
@@ -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 |
};
|
|
@@ -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 |
|
|
@@ -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;
|
|
@@ -3,135 +3,214 @@
|
|
| 3 |
//
|
| 4 |
// Multi-threaded actor topology (mirrors Optiq architecture):
|
| 5 |
//
|
| 6 |
-
// Core 0: OEGatewayActor
|
| 7 |
// Core 1: OrderBookActor per symbol (matching engine)
|
| 8 |
-
// Core 2: MarketDataActor
|
|
|
|
| 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.
|
| 27 |
-
std::cout << "
|
| 28 |
std::cout << "βββββββββββββββββββββββββββββββββββββββββββ\n\n";
|
| 29 |
|
| 30 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
auto oeGateway = std::make_unique<OEGatewayActor>();
|
| 32 |
-
auto mdActor = std::make_unique<MarketDataActor>();
|
| 33 |
|
| 34 |
-
|
| 35 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
oeGateway->
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
std::cout << "
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
std::cout << "
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
std::cout << "
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 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 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 106 |
}
|
| 107 |
-
|
|
|
|
| 108 |
}
|
| 109 |
|
| 110 |
-
// ββ
|
| 111 |
-
std::cout << "\
|
|
|
|
| 112 |
|
| 113 |
-
|
|
|
|
| 114 |
auto* snap = mdActor->getSnapshot(sym);
|
| 115 |
if (snap) {
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
<< "
|
| 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
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
std::cout << "
|
| 131 |
-
<< "
|
|
|
|
|
|
|
|
|
|
| 132 |
}
|
| 133 |
|
| 134 |
-
std::cout << "\
|
|
|
|
| 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 |
}
|
|
@@ -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 |
+
}
|
|
@@ -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 |
+
}
|
|
@@ -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 |
+
}
|
|
@@ -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 |
+
}
|