File size: 12,047 Bytes
5759f0b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
// ════════════════════════════════════════════════════════════════════
// Stop order and trading phase tests
// ════════════════════════════════════════════════════════════════════

#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)

// ── Helpers ──────────────────────────────────────────────────────

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); });
    }
};

// ── Stop Order Tests ─────────────────────────────────────────────

void test_stop_buy_parks_without_matching() {
    TestCtx ctx;
    // Resting sell at 100
    ctx.submit(Side::Sell, OrderType::Limit, toFixedPrice(100.0), 50);
    // Stop buy at stop=105 β€” should NOT match, just park
    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;
    // Resting sells
    ctx.submit(Side::Sell, OrderType::Limit, toFixedPrice(100.0), 50);
    ctx.submit(Side::Sell, OrderType::Limit, toFixedPrice(110.0), 50);

    // Stop buy: triggers when price >= 105
    ctx.submit(Side::Buy, OrderType::StopMarket, 0, 30, toFixedPrice(105.0));
    ASSERT_EQ(ctx.book.stopOrderCount(), static_cast<size_t>(1));

    // Trade at 100 β€” stop NOT triggered (100 < 105)
    ctx.triggerStops(toFixedPrice(100.0));
    ASSERT_EQ(ctx.book.stopOrderCount(), static_cast<size_t>(1));

    // Trade at 106 β€” stop IS triggered, becomes market buy, fills at best ask
    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;
    // Resting buys
    ctx.submit(Side::Buy, OrderType::Limit, toFixedPrice(100.0), 50);
    ctx.submit(Side::Buy, OrderType::Limit, toFixedPrice(90.0), 50);

    // Stop sell: triggers when price <= 95
    ctx.submit(Side::Sell, OrderType::StopMarket, 0, 30, toFixedPrice(95.0));
    ASSERT_EQ(ctx.book.stopOrderCount(), static_cast<size_t>(1));

    // Trade at 96 β€” not triggered
    ctx.triggerStops(toFixedPrice(96.0));
    ASSERT_EQ(ctx.book.stopOrderCount(), static_cast<size_t>(1));

    // Trade at 94 β€” triggered, becomes market sell, fills at best bid
    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;
    // Resting sells at various prices
    ctx.submit(Side::Sell, OrderType::Limit, toFixedPrice(100.0), 50);
    ctx.submit(Side::Sell, OrderType::Limit, toFixedPrice(110.0), 50);

    // StopLimit buy: stop=105, limit price=108
    // When triggered, becomes Limit buy at 108 β€” should match sell@100 but NOT sell@110
    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));
    // Should have filled 50 at 100.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));
    // Remaining 30 should rest at limit price 108
    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));

    // The stop order's orderId is in the last report
    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;
    // Resting sells
    ctx.submit(Side::Sell, OrderType::Limit, toFixedPrice(100.0), 100);

    // Two stop buys
    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));

    // Both trigger at 106
    ctx.triggerStops(toFixedPrice(106.0));
    ASSERT_EQ(ctx.book.stopOrderCount(), static_cast<size_t>(0));
    // Both should have traded (30 + 40 = 70, resting has 100)
    Quantity_t totalTraded = 0;
    for (auto& t : ctx.trades) totalTraded += t.quantity;
    ASSERT_EQ(totalTraded, static_cast<Quantity_t>(70));
}

// ── Pre-Open / Opening / CTS Phase Tests ─────────────────────────

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);

    // Orders cross at 100 but should NOT match during PreOpen
    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);

    // Build crossing book:
    // Buy  200 @ 105
    // Buy  100 @ 100
    // Sell 150 @ 95
    // Sell 100 @ 100
    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();
    // At 100: cumBuy = 300 (200@105 + 100@100), cumSell = 250 (150@95 + 100@100)
    // executable = min(300, 250) = 250
    // At 105: cumBuy = 200, cumSell = 250 β†’ exec = 200
    // At 95: cumBuy = 300, cumSell = 150 β†’ exec = 150
    // Best is 100 with volume 250
    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));

    // Uncross at IOP
    ctx.uncross();
    ASSERT_TRUE(ctx.trades.size() > 0);
    // Should trade 80 (smaller side) at IOP
    Quantity_t totalTraded = 0;
    for (auto& t : ctx.trades) totalTraded += t.quantity;
    ASSERT_EQ(totalTraded, static_cast<Quantity_t>(80));
    // Remaining 20 buy should still be on book
    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);

    // Should match immediately in CTS
    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;
    // Phase 1: PreOpen β€” accumulate crossing orders
    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));

    // Phase 2: Opening uncross
    ctx.book.setPhase(TradingPhase::Opening);
    ctx.uncross();
    ASSERT_TRUE(ctx.trades.size() > 0);
    // All trades should be at IOP
    Price_t iopPrice = ctx.trades[0].price;
    for (auto& t : ctx.trades) {
        ASSERT_EQ(t.price, iopPrice);
    }

    // Phase 3: CTS β€” normal matching
    ctx.book.setPhase(TradingPhase::CTS);
    size_t tradesBefore = ctx.trades.size();
    ctx.submit(Side::Sell, OrderType::Limit, toFixedPrice(100.0), 10);
    // If there are resting buys above 100, this should match
    // Otherwise it rests
    ASSERT_TRUE(true);
}

// ── Main ──────────────────────────────────────────────────────────

int main() {
    std::cout << "Stop Orders & Trading Phase Tests\n";
    std::cout << "═══════════════════════════════════════════\n";

    // Stop orders
    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);

    // Trading phases
    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;
}