| |
| |
| |
|
|
| #include "common/Book.hpp" |
| #include <iostream> |
| #include <cassert> |
| #include <vector> |
|
|
| using namespace eunex; |
|
|
| static int testsPassed = 0; |
| static int testsFailed = 0; |
|
|
| #define TEST(name) \ |
| std::cout << " " << #name << "... "; \ |
| try { test_##name(); std::cout << "PASS\n"; ++testsPassed; } \ |
| catch (const std::exception& e) { std::cout << "FAIL: " << e.what() << "\n"; ++testsFailed; } |
|
|
| #define ASSERT_EQ(a, b) \ |
| if ((a) != (b)) throw std::runtime_error( \ |
| std::string("Expected ") + std::to_string(static_cast<long long>(b)) + " got " + std::to_string(static_cast<long long>(a))) |
|
|
| #define ASSERT_TRUE(x) \ |
| if (!(x)) throw std::runtime_error("Assertion failed: " #x) |
|
|
| |
|
|
| struct TestCtx { |
| Book book; |
| std::vector<Trade> trades; |
| std::vector<ExecutionReport> reports; |
|
|
| TestCtx(SymbolIndex_t sym = 1) : book(sym) {} |
|
|
| void submit(Side side, OrderType ot, Price_t px, Quantity_t qty, |
| Price_t stop = 0, SessionId_t sess = 1) { |
| Order o{}; |
| o.symbolIdx = 1; |
| o.side = side; |
| o.ordType = ot; |
| o.tif = TimeInForce::Day; |
| o.price = px; |
| o.quantity = qty; |
| o.sessionId = sess; |
| o.stopPrice = stop; |
| book.newOrder(o, |
| [this](const Trade& t) { trades.push_back(t); }, |
| [this](const ExecutionReport& r) { reports.push_back(r); }); |
| } |
|
|
| void triggerStops(Price_t tradePrice) { |
| book.triggerStopOrders(tradePrice, |
| [this](const Trade& t) { trades.push_back(t); }, |
| [this](const ExecutionReport& r) { reports.push_back(r); }); |
| } |
|
|
| void uncross() { |
| book.uncross( |
| [this](const Trade& t) { trades.push_back(t); }, |
| [this](const ExecutionReport& r) { reports.push_back(r); }); |
| } |
| }; |
|
|
| |
|
|
| void test_stop_buy_parks_without_matching() { |
| TestCtx ctx; |
| |
| ctx.submit(Side::Sell, OrderType::Limit, toFixedPrice(100.0), 50); |
| |
| ctx.submit(Side::Buy, OrderType::StopMarket, 0, 50, toFixedPrice(105.0)); |
|
|
| ASSERT_EQ(ctx.book.stopOrderCount(), static_cast<size_t>(1)); |
| ASSERT_EQ(ctx.trades.size(), static_cast<size_t>(0)); |
| } |
|
|
| void test_stop_buy_triggers_on_price_rise() { |
| TestCtx ctx; |
| |
| ctx.submit(Side::Sell, OrderType::Limit, toFixedPrice(100.0), 50); |
| ctx.submit(Side::Sell, OrderType::Limit, toFixedPrice(110.0), 50); |
|
|
| |
| ctx.submit(Side::Buy, OrderType::StopMarket, 0, 30, toFixedPrice(105.0)); |
| ASSERT_EQ(ctx.book.stopOrderCount(), static_cast<size_t>(1)); |
|
|
| |
| ctx.triggerStops(toFixedPrice(100.0)); |
| ASSERT_EQ(ctx.book.stopOrderCount(), static_cast<size_t>(1)); |
|
|
| |
| ctx.triggerStops(toFixedPrice(106.0)); |
| ASSERT_EQ(ctx.book.stopOrderCount(), static_cast<size_t>(0)); |
| ASSERT_TRUE(ctx.trades.size() > 0); |
| } |
|
|
| void test_stop_sell_triggers_on_price_drop() { |
| TestCtx ctx; |
| |
| ctx.submit(Side::Buy, OrderType::Limit, toFixedPrice(100.0), 50); |
| ctx.submit(Side::Buy, OrderType::Limit, toFixedPrice(90.0), 50); |
|
|
| |
| ctx.submit(Side::Sell, OrderType::StopMarket, 0, 30, toFixedPrice(95.0)); |
| ASSERT_EQ(ctx.book.stopOrderCount(), static_cast<size_t>(1)); |
|
|
| |
| ctx.triggerStops(toFixedPrice(96.0)); |
| ASSERT_EQ(ctx.book.stopOrderCount(), static_cast<size_t>(1)); |
|
|
| |
| ctx.triggerStops(toFixedPrice(94.0)); |
| ASSERT_EQ(ctx.book.stopOrderCount(), static_cast<size_t>(0)); |
| ASSERT_TRUE(ctx.trades.size() > 0); |
| ASSERT_EQ(ctx.trades.back().price, toFixedPrice(100.0)); |
| } |
|
|
| void test_stop_limit_uses_limit_price() { |
| TestCtx ctx; |
| |
| ctx.submit(Side::Sell, OrderType::Limit, toFixedPrice(100.0), 50); |
| ctx.submit(Side::Sell, OrderType::Limit, toFixedPrice(110.0), 50); |
|
|
| |
| |
| ctx.submit(Side::Buy, OrderType::StopLimit, toFixedPrice(108.0), 80, toFixedPrice(105.0)); |
|
|
| ctx.triggerStops(toFixedPrice(106.0)); |
| ASSERT_EQ(ctx.book.stopOrderCount(), static_cast<size_t>(0)); |
| |
| ASSERT_TRUE(ctx.trades.size() > 0); |
| ASSERT_EQ(ctx.trades.back().price, toFixedPrice(100.0)); |
| ASSERT_EQ(ctx.trades.back().quantity, static_cast<Quantity_t>(50)); |
| |
| ASSERT_EQ(ctx.book.bidCount(), static_cast<size_t>(1)); |
| } |
|
|
| void test_cancel_stop_order() { |
| TestCtx ctx; |
| ctx.submit(Side::Buy, OrderType::StopMarket, 0, 50, toFixedPrice(105.0)); |
| ASSERT_EQ(ctx.book.stopOrderCount(), static_cast<size_t>(1)); |
|
|
| |
| OrderId_t stopId = ctx.reports.back().orderId; |
|
|
| ExecutionReport rpt{}; |
| bool ok = ctx.book.cancelOrder(stopId, rpt); |
| ASSERT_TRUE(ok); |
| ASSERT_EQ(rpt.status, OrderStatus::Cancelled); |
| ASSERT_EQ(ctx.book.stopOrderCount(), static_cast<size_t>(0)); |
| } |
|
|
| void test_multiple_stops_trigger_simultaneously() { |
| TestCtx ctx; |
| |
| ctx.submit(Side::Sell, OrderType::Limit, toFixedPrice(100.0), 100); |
|
|
| |
| ctx.submit(Side::Buy, OrderType::StopMarket, 0, 30, toFixedPrice(105.0)); |
| ctx.submit(Side::Buy, OrderType::StopMarket, 0, 40, toFixedPrice(103.0)); |
| ASSERT_EQ(ctx.book.stopOrderCount(), static_cast<size_t>(2)); |
|
|
| |
| ctx.triggerStops(toFixedPrice(106.0)); |
| ASSERT_EQ(ctx.book.stopOrderCount(), static_cast<size_t>(0)); |
| |
| Quantity_t totalTraded = 0; |
| for (auto& t : ctx.trades) totalTraded += t.quantity; |
| ASSERT_EQ(totalTraded, static_cast<Quantity_t>(70)); |
| } |
|
|
| |
|
|
| void test_preopen_accumulates_no_matching() { |
| TestCtx ctx; |
| ctx.book.setPhase(TradingPhase::PreOpen); |
|
|
| ctx.submit(Side::Sell, OrderType::Limit, toFixedPrice(100.0), 50); |
| ctx.submit(Side::Buy, OrderType::Limit, toFixedPrice(100.0), 50); |
|
|
| |
| ASSERT_EQ(ctx.trades.size(), static_cast<size_t>(0)); |
| ASSERT_EQ(ctx.book.bidCount(), static_cast<size_t>(1)); |
| ASSERT_EQ(ctx.book.askCount(), static_cast<size_t>(1)); |
| } |
|
|
| void test_iop_calculation() { |
| TestCtx ctx; |
| ctx.book.setPhase(TradingPhase::PreOpen); |
|
|
| |
| |
| |
| |
| |
| ctx.submit(Side::Buy, OrderType::Limit, toFixedPrice(105.0), 200); |
| ctx.submit(Side::Buy, OrderType::Limit, toFixedPrice(100.0), 100); |
| ctx.submit(Side::Sell, OrderType::Limit, toFixedPrice(95.0), 150); |
| ctx.submit(Side::Sell, OrderType::Limit, toFixedPrice(100.0), 100); |
|
|
| Price_t iop = ctx.book.getIOP(); |
| |
| |
| |
| |
| |
| ASSERT_EQ(iop, toFixedPrice(100.0)); |
| } |
|
|
| void test_uncrossing_generates_trades() { |
| TestCtx ctx; |
| ctx.book.setPhase(TradingPhase::PreOpen); |
|
|
| ctx.submit(Side::Buy, OrderType::Limit, toFixedPrice(105.0), 100); |
| ctx.submit(Side::Sell, OrderType::Limit, toFixedPrice(95.0), 80); |
|
|
| ASSERT_EQ(ctx.trades.size(), static_cast<size_t>(0)); |
|
|
| |
| ctx.uncross(); |
| ASSERT_TRUE(ctx.trades.size() > 0); |
| |
| Quantity_t totalTraded = 0; |
| for (auto& t : ctx.trades) totalTraded += t.quantity; |
| ASSERT_EQ(totalTraded, static_cast<Quantity_t>(80)); |
| |
| ASSERT_EQ(ctx.book.bidCount(), static_cast<size_t>(1)); |
| ASSERT_EQ(ctx.book.askCount(), static_cast<size_t>(0)); |
| } |
|
|
| void test_cts_matches_normally() { |
| TestCtx ctx; |
| ctx.book.setPhase(TradingPhase::CTS); |
|
|
| ctx.submit(Side::Sell, OrderType::Limit, toFixedPrice(100.0), 50); |
| ctx.submit(Side::Buy, OrderType::Limit, toFixedPrice(100.0), 50); |
|
|
| |
| ASSERT_TRUE(ctx.trades.size() > 0); |
| ASSERT_EQ(ctx.trades.back().quantity, static_cast<Quantity_t>(50)); |
| } |
|
|
| void test_closed_rejects_orders() { |
| TestCtx ctx; |
| ctx.book.setPhase(TradingPhase::Closed); |
|
|
| ctx.submit(Side::Buy, OrderType::Limit, toFixedPrice(100.0), 50); |
|
|
| ASSERT_EQ(ctx.trades.size(), static_cast<size_t>(0)); |
| ASSERT_TRUE(ctx.reports.back().status == OrderStatus::Rejected); |
| } |
|
|
| void test_preopen_to_cts_workflow() { |
| TestCtx ctx; |
| |
| ctx.book.setPhase(TradingPhase::PreOpen); |
| ctx.submit(Side::Buy, OrderType::Limit, toFixedPrice(102.0), 100); |
| ctx.submit(Side::Buy, OrderType::Limit, toFixedPrice(101.0), 50); |
| ctx.submit(Side::Sell, OrderType::Limit, toFixedPrice(99.0), 80); |
| ctx.submit(Side::Sell, OrderType::Limit, toFixedPrice(100.0), 60); |
| ASSERT_EQ(ctx.trades.size(), static_cast<size_t>(0)); |
|
|
| |
| ctx.book.setPhase(TradingPhase::Opening); |
| ctx.uncross(); |
| ASSERT_TRUE(ctx.trades.size() > 0); |
| |
| Price_t iopPrice = ctx.trades[0].price; |
| for (auto& t : ctx.trades) { |
| ASSERT_EQ(t.price, iopPrice); |
| } |
|
|
| |
| ctx.book.setPhase(TradingPhase::CTS); |
| size_t tradesBefore = ctx.trades.size(); |
| ctx.submit(Side::Sell, OrderType::Limit, toFixedPrice(100.0), 10); |
| |
| |
| ASSERT_TRUE(true); |
| } |
|
|
| |
|
|
| int main() { |
| std::cout << "Stop Orders & Trading Phase Tests\n"; |
| std::cout << "βββββββββββββββββββββββββββββββββββββββββββ\n"; |
|
|
| |
| TEST(stop_buy_parks_without_matching); |
| TEST(stop_buy_triggers_on_price_rise); |
| TEST(stop_sell_triggers_on_price_drop); |
| TEST(stop_limit_uses_limit_price); |
| TEST(cancel_stop_order); |
| TEST(multiple_stops_trigger_simultaneously); |
|
|
| |
| TEST(preopen_accumulates_no_matching); |
| TEST(iop_calculation); |
| TEST(uncrossing_generates_trades); |
| TEST(cts_matches_normally); |
| TEST(closed_rejects_orders); |
| TEST(preopen_to_cts_workflow); |
|
|
| std::cout << "βββββββββββββββββββββββββββββββββββββββββββ\n"; |
| std::cout << testsPassed << " passed, " << testsFailed << " failed\n"; |
| return testsFailed > 0 ? 1 : 0; |
| } |
|
|