NOT-OMEGA commited on
Commit
ba6114e
·
verified ·
1 Parent(s): defba9f

Upload 22 files

Browse files
.github/workflows/ci.yml ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Build & Test
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+
9
+ jobs:
10
+ build:
11
+ runs-on: ubuntu-latest
12
+
13
+ steps:
14
+ - uses: actions/checkout@v4
15
+
16
+ - name: Install dependencies
17
+ run: sudo apt-get update && sudo apt-get install -y cmake g++
18
+
19
+ - name: Configure CMake
20
+ run: cmake -B build -DCMAKE_BUILD_TYPE=Release
21
+
22
+ - name: Build all targets
23
+ run: cmake --build build -j$(nproc)
24
+
25
+ - name: Run unit tests
26
+ run: |
27
+ cd build
28
+ ctest --output-on-failure
29
+
30
+ - name: Run benchmark
31
+ run: ./build/hft_bench
BUG_REPORT.md ADDED
@@ -0,0 +1,511 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Complete Bug Report — HFT Exchange Simulator
2
+
3
+ **Total Bugs Found: 17**
4
+ **Files Modified: 8**
5
+ **Severity: 4 Critical, 9 Medium, 4 Low**
6
+
7
+ ---
8
+
9
+ ## BUG #1 — Windows Pipe Crash (CRITICAL)
10
+
11
+ **File:** `app.py` (line ~72)
12
+ **What was the bug:**
13
+ The old code used `select.select()` to read the C++ engine's stdout with a timeout. On Linux, `select.select()` works with pipe file descriptors. But on Windows, `select.select()` ONLY works with sockets — NOT with pipes. When the Python server tried to read the engine response, Windows threw an `OSError` and the entire server crashed immediately on every single API call.
14
+
15
+ **What the old code looked like:**
16
+ ```python
17
+ ready, _, _ = select.select([self.proc.stdout], [], [], ENGINE_TIMEOUT)
18
+ if ready:
19
+ response = self.proc.stdout.readline().strip()
20
+ ```
21
+
22
+ **How I fixed it:**
23
+ Replaced `select.select()` with a cross-platform thread-based approach. I spawn a short-lived daemon thread that calls `readline()`. The main thread waits for that thread with a timeout using `thread.join(timeout=ENGINE_TIMEOUT)`. If the thread is still alive after the timeout, we kill the engine and restart it.
24
+
25
+ **What the fixed code looks like:**
26
+ ```python
27
+ response_container = []
28
+ def _read():
29
+ resp = self.proc.stdout.readline()
30
+ response_container.append(resp)
31
+
32
+ reader = threading.Thread(target=_read, daemon=True)
33
+ reader.start()
34
+ reader.join(timeout=ENGINE_TIMEOUT)
35
+
36
+ if reader.is_alive():
37
+ self.proc.kill()
38
+ self.proc = None
39
+ self._start()
40
+ raise HTTPException(504, "Engine timeout — restarted")
41
+ ```
42
+
43
+ **Why this matters:** Without this fix, the entire application was 100% non-functional on Windows. Every API request would crash.
44
+
45
+ ---
46
+
47
+ ## BUG #2 — Server Crashes If Binary Is Missing (CRITICAL)
48
+
49
+ **File:** `app.py` (line ~34)
50
+ **What was the bug:**
51
+ The `EngineProcess` was created at module-level (`engine = EngineProcess()`). Inside `__init__`, it called `_start()` which tried to open the C++ binary via `subprocess.Popen()`. If the binary file didn't exist (because you haven't built the C++ code yet), `Popen` would throw an exception and the entire FastAPI server would fail to start — you couldn't even see an error page.
52
+
53
+ **How I fixed it:**
54
+ 1. In `_start()`, added a check: if the binary file doesn't exist, set `self.proc = None` and return gracefully instead of crashing.
55
+ 2. Changed the module-level `engine = EngineProcess()` to `engine = None` (lazy initialization). The engine is only created when the first API request comes in via `_get_engine()`.
56
+
57
+ **What the fixed code looks like:**
58
+ ```python
59
+ engine: Optional[EngineProcess] = None
60
+
61
+ def _get_engine() -> EngineProcess:
62
+ global engine
63
+ if engine is None:
64
+ engine = EngineProcess()
65
+ return engine
66
+ ```
67
+
68
+ **Why this matters:** Developers can now start the Python server, see the API docs, and get a clear "binary not found" error message instead of an opaque crash.
69
+
70
+ ---
71
+
72
+ ## BUG #3 — Missing .exe Suffix on Windows (CRITICAL)
73
+
74
+ **File:** `app.py` (line ~21)
75
+ **What was the bug:**
76
+ The binary path was hardcoded as `"hft_engine"`. On Windows, compiled C++ executables have a `.exe` extension. So the server was looking for a file called `hft_engine` which doesn't exist on Windows — it should be looking for `hft_engine.exe`.
77
+
78
+ **What the old code looked like:**
79
+ ```python
80
+ ENGINE_PATH = Path(__file__).parent / "build" / "hft_engine"
81
+ ```
82
+
83
+ **How I fixed it:**
84
+ ```python
85
+ _engine_name = "hft_engine.exe" if sys.platform == "win32" else "hft_engine"
86
+ ENGINE_PATH = Path(__file__).parent / "build" / _engine_name
87
+ ```
88
+
89
+ **Why this matters:** Without this, even if the binary was built successfully on Windows, the server would never find it.
90
+
91
+ ---
92
+
93
+ ## BUG #4 — Sell-Side Iterator Invalidation (CRITICAL)
94
+
95
+ **File:** `src/core/order_book.cpp` (line ~113)
96
+ **What was the bug:**
97
+ This is the most dangerous C++ bug in the entire project. When a SELL order comes in, the matching engine needs to match it against the best BID (highest price). The old code used a `for` loop iterating backwards through `bids_` (a `std::map`):
98
+
99
+ ```cpp
100
+ for (auto it = bids_.end(); it != bids_.begin() && incoming.remaining() > 0; ) {
101
+ --it;
102
+ // ... matching logic ...
103
+ if (resting->is_done()) {
104
+ level.orders.pop_front();
105
+ orders_.erase(resting->id);
106
+ if (level.empty()) {
107
+ it = bids_.erase(it); // ← erases the current element
108
+ break; // ← breaks inner while loop
109
+ }
110
+ }
111
+ }
112
+ // Back to outer for loop → does --it on the ERASED iterator → UNDEFINED BEHAVIOR
113
+ ```
114
+
115
+ The problem: After `bids_.erase(it)`, the iterator `it` is set to the next element. Then `break` exits the inner `while` loop. Then the outer `for` loop does `--it` on this already-advanced iterator. But worse — if we erased the first element, `--it` could go before `begin()`, which is undefined behavior. This could cause random crashes, corrupted order books, or silent wrong fills under heavy load.
116
+
117
+ **How I fixed it:**
118
+ Rewrote the entire sell-side loop to use a `while` loop that re-fetches `std::prev(bids_.end())` on each iteration. This way, after erasing a level, we simply go back to the top of the `while` loop and grab the new best bid. No iterator can be invalidated.
119
+
120
+ ```cpp
121
+ while (!bids_.empty() && incoming.remaining() > 0) {
122
+ auto it = std::prev(bids_.end()); // always get fresh best bid
123
+ auto& [px, level] = *it;
124
+
125
+ if (incoming.type == OrderType::LIMIT || incoming.type == OrderType::IOC) {
126
+ if (px < incoming.price) break;
127
+ }
128
+
129
+ while (!level.orders.empty() && incoming.remaining() > 0) {
130
+ // ... matching logic (unchanged) ...
131
+ }
132
+
133
+ if (level.empty()) {
134
+ bids_.erase(it); // safe — we won't use 'it' again
135
+ } else {
136
+ break;
137
+ }
138
+ }
139
+ ```
140
+
141
+ **Why this matters:** This is the kind of bug that works fine in testing with small order books but causes random crashes in production under heavy load. A quant firm would lose real money.
142
+
143
+ ---
144
+
145
+ ## BUG #5 — to_price() Rounds Wrong for Negative Numbers
146
+
147
+ **File:** `src/core/types.hpp` (line ~17)
148
+ **What was the bug:**
149
+ The `to_price()` function converts a `double` (like 185.5050) to an `int64_t` (like 1855050) by multiplying by 10000 and adding 0.5:
150
+
151
+ ```cpp
152
+ inline Price to_price(double d) { return static_cast<Price>(d * PRICE_SCALE + 0.5); }
153
+ ```
154
+
155
+ This `+0.5` trick works for positive numbers (it rounds 1.6 → 2, 1.4 → 1). But for negative numbers, it's WRONG:
156
+ - `-1.5 * 10000 + 0.5 = -14999.5` → truncated to `-14999`
157
+ - Correct answer should be `-15000`
158
+
159
+ This matters because PnL calculations can produce negative intermediate values.
160
+
161
+ **How I fixed it:**
162
+ ```cpp
163
+ inline Price to_price(double d) { return static_cast<Price>(std::llround(d * PRICE_SCALE)); }
164
+ ```
165
+
166
+ `std::llround()` correctly rounds both positive and negative numbers (rounds away from zero).
167
+
168
+ **Why this matters:** Wrong rounding = wrong prices = wrong PnL = wrong risk calculations. Off-by-one tick errors compound over thousands of trades.
169
+
170
+ ---
171
+
172
+ ## BUG #6 — spread() Returns Garbage for Empty or Crossed Books
173
+
174
+ **File:** `src/core/types.hpp` (line ~86)
175
+ **What was the bug:**
176
+ The `spread()` function calculated spread without checking if bids/asks existed:
177
+
178
+ ```cpp
179
+ double spread() const {
180
+ return from_price(best_ask() - best_bid());
181
+ }
182
+ ```
183
+
184
+ If `bids` is empty, `best_bid()` returns 0. If `asks` is empty, `best_ask()` returns 0. So `spread()` would return `from_price(0 - 0) = 0` which is accidentally correct for empty books, BUT if you have a crossed book (best_bid > best_ask, which can happen during fast markets), you'd get a negative spread which makes no sense.
185
+
186
+ **How I fixed it:**
187
+ ```cpp
188
+ double spread() const {
189
+ if (bids.empty() || asks.empty()) return 0.0;
190
+ Price s = best_ask() - best_bid();
191
+ return s > 0 ? from_price(s) : 0.0;
192
+ }
193
+ ```
194
+
195
+ **Why this matters:** Strategies use spread to decide whether to quote. A negative spread could cause strategies to place inverted orders.
196
+
197
+ ---
198
+
199
+ ## BUG #7 — on_tick() Lock Placed After the Real Work
200
+
201
+ **File:** `src/core/simulator.cpp` (line ~78)
202
+ **What was the bug:**
203
+ The `on_tick()` function was supposed to update market prices and record the latency. But the lock was placed AFTER the `update_market_price()` call:
204
+
205
+ ```cpp
206
+ void ExchangeSimulator::on_tick(const MarketDataTick& tick) {
207
+ int64_t t0 = now_ns();
208
+ risk_.update_market_price(tick.symbol, from_price(tick.last_price)); // ← NO LOCK
209
+ std::lock_guard<std::mutex> lock(mtx_); // ← lock acquired AFTER the work
210
+ tick_lat_.record(now_ns() - t0); // ← latency includes lock wait time
211
+ }
212
+ ```
213
+
214
+ Two problems:
215
+ 1. `update_market_price()` runs without any protection from the simulator's mutex, so if another thread calls `submit_order()` at the same time, you have a data race on the position's unrealized PnL.
216
+ 2. The latency recorded includes the time spent waiting for the lock, which makes the metric useless — it measures contention, not processing speed.
217
+
218
+ **How I fixed it:**
219
+ ```cpp
220
+ void ExchangeSimulator::on_tick(const MarketDataTick& tick) {
221
+ int64_t t0 = now_ns();
222
+ std::lock_guard<std::mutex> lock(mtx_); // ← lock FIRST
223
+ risk_.update_market_price(tick.symbol, from_price(tick.last_price));
224
+ tick_lat_.record(now_ns() - t0);
225
+ }
226
+ ```
227
+
228
+ **Why this matters:** Data races cause undefined behavior — corrupted PnL numbers, positions that don't add up, and intermittent crashes that are nearly impossible to reproduce.
229
+
230
+ ---
231
+
232
+ ## BUG #8 — symbols() Missing Lock (Data Race)
233
+
234
+ **File:** `src/core/simulator.cpp` (line ~92)
235
+ **What was the bug:**
236
+ The `symbols()` function iterates over the `books_` map to return a list of symbol names. But it didn't acquire the mutex:
237
+
238
+ ```cpp
239
+ std::vector<std::string> ExchangeSimulator::symbols() const {
240
+ std::vector<std::string> s;
241
+ for (auto& [k, _] : books_) s.push_back(k); // ← iterating without lock
242
+ return s;
243
+ }
244
+ ```
245
+
246
+ If another thread calls `add_symbol()` at the same time (which DOES hold the lock and modifies `books_`), you get a data race. On `std::unordered_map`, concurrent read + write is undefined behavior — it can crash, return garbage, or corrupt the map's internal hash table.
247
+
248
+ **How I fixed it:**
249
+ ```cpp
250
+ std::vector<std::string> ExchangeSimulator::symbols() const {
251
+ std::lock_guard<std::mutex> lock(mtx_); // ← added lock
252
+ std::vector<std::string> s;
253
+ for (auto& [k, _] : books_) s.push_back(k);
254
+ return s;
255
+ }
256
+ ```
257
+
258
+ **Why this matters:** Same as above — data races are undefined behavior in C++.
259
+
260
+ ---
261
+
262
+ ## BUG #9 — Rate Limiter Counts Rejected Orders (DoS Vulnerability)
263
+
264
+ **File:** `src/risk/risk_manager.cpp` (line ~22)
265
+ **What was the bug:**
266
+ The old code recorded the order timestamp at the TOP of `check_order()`, BEFORE any validation:
267
+
268
+ ```cpp
269
+ RiskResult RiskManager::check_order(const Order& order, double current_price) {
270
+ std::lock_guard<std::mutex> lock(mtx_);
271
+ ++stats_checked_;
272
+ order_timestamps_ns_.push_back(now_ns()); // ← counted even if rejected
273
+ // ... then check qty, price, position, etc.
274
+ }
275
+ ```
276
+
277
+ This means: if someone sends 2000 orders with `qty = -1` (which gets rejected immediately), all 2000 are counted toward the rate limit. Now the rate limit is exhausted, and VALID orders from a legitimate strategy also get rejected for the next second.
278
+
279
+ This is a denial-of-service attack vector — a rogue client can disable all trading by spamming invalid orders.
280
+
281
+ **How I fixed it:**
282
+ Moved the timestamp recording to AFTER all validation checks pass:
283
+
284
+ ```cpp
285
+ RiskResult RiskManager::check_order(const Order& order, double current_price) {
286
+ std::lock_guard<std::mutex> lock(mtx_);
287
+ ++stats_checked_;
288
+
289
+ // All validation checks first...
290
+ if (order.qty <= 0) return REJECT;
291
+ if (order.price <= 0) return REJECT;
292
+ // ... position, notional, rate checks ...
293
+
294
+ // Only count approved orders toward rate limit
295
+ order_timestamps_ns_.push_back(now_ns());
296
+ return {true, "OK"};
297
+ }
298
+ ```
299
+
300
+ Also changed the rate check from `>` to `>=` (off-by-one: the old code allowed `max + 1` orders per second).
301
+
302
+ **Why this matters:** In a real exchange, a rate limiter that counts rejected orders is a security vulnerability. Any client could freeze trading for everyone.
303
+
304
+ ---
305
+
306
+ ## BUG #10 — JSON Injection in Error Response (Command Name)
307
+
308
+ **File:** `src/engine_cli.cpp` (line ~285)
309
+ **What was the bug:**
310
+ When an unknown command was received, the error message included the raw command name without JSON escaping:
311
+
312
+ ```cpp
313
+ return "{\"error\":\"unknown command: " + cmd + "\"}";
314
+ ```
315
+
316
+ If someone sends a command like `test"injected":"value`, the output becomes:
317
+ ```json
318
+ {"error":"unknown command: test"injected":"value"}
319
+ ```
320
+
321
+ This is malformed JSON. The Python side does `json.loads()` on this response, which would throw an exception, causing a 500 error instead of a clean error message.
322
+
323
+ **How I fixed it:**
324
+ ```cpp
325
+ return "{\"error\":" + J::str("unknown command: " + cmd) + "}";
326
+ ```
327
+
328
+ `J::str()` properly escapes quotes and backslashes inside the string.
329
+
330
+ **Why this matters:** Malformed JSON breaks the entire Python-C++ communication protocol. Any unusual input could crash the API.
331
+
332
+ ---
333
+
334
+ ## BUG #11 — JSON Injection in Exception Message
335
+
336
+ **File:** `src/engine_cli.cpp` (line ~307)
337
+ **What was the bug:**
338
+ Same problem as Bug #10, but in the exception handler:
339
+
340
+ ```cpp
341
+ catch (const std::exception& e) {
342
+ std::cout << "{\"error\":\"" << e.what() << "\"}\n";
343
+ }
344
+ ```
345
+
346
+ If `e.what()` contains a quote character (which many standard library exceptions do), the JSON output is malformed.
347
+
348
+ **How I fixed it:**
349
+ ```cpp
350
+ catch (const std::exception& e) {
351
+ std::cout << "{\"error\":" << J::str(e.what()) << "}\n";
352
+ }
353
+ ```
354
+
355
+ **Why this matters:** Same as Bug #10 — any exception with a quote in the message would break JSON parsing.
356
+
357
+ ---
358
+
359
+ ## BUG #12 — Out-of-Bounds Read in jget() (engine_cli.cpp)
360
+
361
+ **File:** `src/engine_cli.cpp` (line ~22-23)
362
+ **What was the bug:**
363
+ The `jget()` JSON parser skips whitespace after the colon:
364
+
365
+ ```cpp
366
+ while (pos < body.size() && (body[pos] == ' ' || body[pos] == '\t')) ++pos;
367
+ if (body[pos] == '"') { // ← if pos == body.size(), this is OUT OF BOUNDS
368
+ ```
369
+
370
+ After the `while` loop, `pos` could be equal to `body.size()`. Accessing `body[pos]` when `pos == body.size()` reads past the end of the string. In C++, this is undefined behavior — it could read garbage memory, crash, or appear to work.
371
+
372
+ **How I fixed it:**
373
+ ```cpp
374
+ while (pos < body.size() && (body[pos] == ' ' || body[pos] == '\t')) ++pos;
375
+ if (pos >= body.size()) return ""; // ← added bounds check
376
+ if (body[pos] == '"') {
377
+ ```
378
+
379
+ **Why this matters:** Out-of-bounds reads are undefined behavior. They can be exploited for information disclosure or cause crashes.
380
+
381
+ ---
382
+
383
+ ## BUG #13 — Out-of-Bounds Read in json_get() (main.cpp)
384
+
385
+ **File:** `src/main.cpp` (line ~56-57)
386
+ **What was the bug:**
387
+ Exact same bug as Bug #12, but in the `json_get()` function in `main.cpp` (the HTTP server version). It's a copy-pasted function with the same flaw.
388
+
389
+ **How I fixed it:**
390
+ Same fix — added `if (pos >= body.size()) return "";` after the whitespace skip loop.
391
+
392
+ **Why this matters:** Same as Bug #12.
393
+
394
+ ---
395
+
396
+ ## BUG #14 — Feed Index Race Condition
397
+
398
+ **File:** `src/main.cpp` (line ~240-241)
399
+ **What was the bug:**
400
+ The feed stepping code used an atomic counter but had a race in the reset logic:
401
+
402
+ ```cpp
403
+ size_t idx = g_feed_idx.fetch_add(1);
404
+ if (idx >= g_tick_feed.size()) { g_feed_idx = 0; idx = 0; }
405
+ ```
406
+
407
+ The problem: Two threads can both see `idx >= size` at the same time. Both reset `g_feed_idx = 0`. Both use `idx = 0`. Now two requests replay the same tick. Also, `g_feed_idx = 0` is a non-atomic store on an atomic variable — technically a data race.
408
+
409
+ **How I fixed it:**
410
+ ```cpp
411
+ size_t idx = g_feed_idx.fetch_add(1) % g_tick_feed.size();
412
+ ```
413
+
414
+ Using modulo (`%`) on the atomically-fetched value gives correct wraparound without any reset. Lock-free, race-free, one line.
415
+
416
+ **Why this matters:** In an HFT context, replaying the same tick twice or skipping ticks means your backtest results are wrong.
417
+
418
+ ---
419
+
420
+ ## BUG #15 — Dead Code / No-Op Expression
421
+
422
+ **File:** `app.py`
423
+ **What was the bug:**
424
+ There was a line that looked like it was trying to do something but was actually a no-op expression that Python evaluated and threw away:
425
+
426
+ ```python
427
+ self.proc.stdout._sock if hasattr(self.proc.stdout, '_sock') else None
428
+ ```
429
+
430
+ This evaluates to either a socket object or `None`, but the result is never assigned to anything or used. It does nothing.
431
+
432
+ **How I fixed it:** Removed the dead code entirely.
433
+
434
+ **Why this matters:** Dead code confuses future developers and suggests the original author didn't understand what they were writing.
435
+
436
+ ---
437
+
438
+ ## BUG #16 — depth Parameter Not Forwarded
439
+
440
+ **File:** `app.py` (line ~191)
441
+ **What was the bug:**
442
+ The `/api/book/{symbol}` endpoint accepted a `depth` query parameter, but never actually sent it to the C++ engine:
443
+
444
+ ```python
445
+ @app.get("/api/book/{symbol}")
446
+ def book(symbol: str, depth: int = 10):
447
+ return _get_engine().send("book", {"symbol": symbol}) # ← depth is ignored!
448
+ ```
449
+
450
+ So no matter what depth the user requested, they always got the default 10 levels.
451
+
452
+ **How I fixed it:**
453
+ ```python
454
+ return _get_engine().send("book", {"symbol": symbol, "depth": depth})
455
+ ```
456
+
457
+ **Why this matters:** Silent parameter ignoring is a bug that erodes API trust. Users think they're controlling the depth but they're not.
458
+
459
+ ---
460
+
461
+ ## BUG #17 — Dangling Declaration (Linker Bomb)
462
+
463
+ **File:** `src/core/simulator.hpp` (line ~104)
464
+ **What was the bug:**
465
+ The `Backtester` class declared a method `replay_journal(const std::string& path)` in the header, but the implementation (`.cpp` file) never defined it:
466
+
467
+ ```cpp
468
+ class Backtester {
469
+ public:
470
+ // ...
471
+ BacktestResult replay_journal(const std::string& path); // ← declared but never defined
472
+ };
473
+ ```
474
+
475
+ Right now, nothing calls this function, so there's no linker error. But if anyone ever tries to call `bt.replay_journal("file.journal")`, they'll get a cryptic linker error: `undefined reference to Backtester::replay_journal`. This is a maintenance trap.
476
+
477
+ **How I fixed it:** Removed the declaration entirely.
478
+
479
+ **Why this matters:** Dangling declarations are tech debt that confuses future developers and causes linker errors when someone tries to use the function.
480
+
481
+ ---
482
+
483
+ ## BONUS FIXES (Cleanup)
484
+
485
+ - **Removed unused imports** in `app.py`: `asyncio`, `StaticFiles`, `JSONResponse`, `time` — none were used anywhere in the file.
486
+ - **Added `self.proc = None`** before restart in error paths — ensures the old dead process handle doesn't leak.
487
+
488
+ ---
489
+
490
+ ## Summary by File
491
+
492
+ | File | Bugs Fixed | Bug Numbers |
493
+ |------|-----------|-------------|
494
+ | `app.py` | 7 | #1, #2, #3, #15, #16, +cleanup |
495
+ | `src/core/types.hpp` | 2 | #5, #6 |
496
+ | `src/core/order_book.cpp` | 1 | #4 |
497
+ | `src/core/simulator.cpp` | 2 | #7, #8 |
498
+ | `src/core/simulator.hpp` | 1 | #17 |
499
+ | `src/risk/risk_manager.cpp` | 1 | #9 |
500
+ | `src/engine_cli.cpp` | 3 | #10, #11, #12 |
501
+ | `src/main.cpp` | 2 | #13, #14 |
502
+
503
+ ---
504
+
505
+ ## Summary by Severity
506
+
507
+ | Severity | Count | Bug Numbers |
508
+ |----------|-------|-------------|
509
+ | 🔴 CRITICAL (crash/UB) | 4 | #1, #2, #3, #4 |
510
+ | 🟡 MEDIUM (wrong behavior) | 9 | #5, #6, #7, #8, #9, #10, #11, #12, #13, #14 |
511
+ | 🟢 LOW (cleanup/maintenance) | 4 | #15, #16, #17, +imports |
CMakeLists.txt ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ cmake_minimum_required(VERSION 3.16)
2
+ project(hft_simulator CXX)
3
+
4
+ set(CMAKE_CXX_STANDARD 17)
5
+ set(CMAKE_CXX_STANDARD_REQUIRED ON)
6
+
7
+ set(CMAKE_CXX_FLAGS_RELEASE "-O3 -march=native -DNDEBUG")
8
+ if(NOT CMAKE_BUILD_TYPE)
9
+ set(CMAKE_BUILD_TYPE Release)
10
+ endif()
11
+
12
+ include_directories(
13
+ ${CMAKE_SOURCE_DIR}/include
14
+ ${CMAKE_SOURCE_DIR}/src
15
+ )
16
+
17
+ set(CORE_SOURCES
18
+ src/core/order_book.cpp
19
+ src/core/simulator.cpp
20
+ src/risk/risk_manager.cpp
21
+ src/journal/journal.cpp
22
+ )
23
+
24
+ # ── Production targets ────────────────────────────────────────────────────────
25
+
26
+ add_executable(hft_server ${CORE_SOURCES} src/main.cpp)
27
+ target_compile_options(hft_server PRIVATE -Wall -Wextra -pthread)
28
+ target_link_libraries(hft_server PRIVATE pthread)
29
+
30
+ add_executable(hft_engine ${CORE_SOURCES} src/engine_cli.cpp)
31
+ target_compile_options(hft_engine PRIVATE -Wall -Wextra -pthread)
32
+ target_link_libraries(hft_engine PRIVATE pthread)
33
+
34
+ install(TARGETS hft_engine DESTINATION bin)
35
+
36
+ # ── Test targets ──────────────────────────────────────────────────────────────
37
+
38
+ add_executable(hft_test_orderbook
39
+ ${CORE_SOURCES}
40
+ tests/test_order_book.cpp
41
+ )
42
+ target_compile_options(hft_test_orderbook PRIVATE -Wall -Wextra -pthread)
43
+ target_link_libraries(hft_test_orderbook PRIVATE pthread)
44
+
45
+ add_executable(hft_test_risk
46
+ ${CORE_SOURCES}
47
+ tests/test_risk_manager.cpp
48
+ )
49
+ target_compile_options(hft_test_risk PRIVATE -Wall -Wextra -pthread)
50
+ target_link_libraries(hft_test_risk PRIVATE pthread)
51
+
52
+ # ── Benchmark target ──────────────────────────────────────────────────────────
53
+
54
+ add_executable(hft_bench
55
+ ${CORE_SOURCES}
56
+ benchmark/bench_matching_engine.cpp
57
+ )
58
+ target_compile_options(hft_bench PRIVATE -Wall -Wextra -pthread -O3)
59
+ target_link_libraries(hft_bench PRIVATE pthread)
60
+
61
+ # ── CTest integration ────────────────────────────────────────────────────────
62
+
63
+ enable_testing()
64
+ add_test(NAME OrderBookTests COMMAND hft_test_orderbook)
65
+ add_test(NAME RiskManagerTests COMMAND hft_test_risk)
Dockerfile ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM ubuntu:22.04
2
+
3
+ ENV DEBIAN_FRONTEND=noninteractive
4
+
5
+ # Install dependencies
6
+ RUN apt-get update && apt-get install -y \
7
+ build-essential \
8
+ cmake \
9
+ g++ \
10
+ python3 \
11
+ python3-pip \
12
+ wget \
13
+ curl \
14
+ && rm -rf /var/lib/apt/lists/*
15
+
16
+ # Install Python packages
17
+ RUN pip3 install fastapi uvicorn
18
+
19
+ WORKDIR /app
20
+
21
+ # Copy all files
22
+ COPY . .
23
+
24
+ # Download httplib.h
25
+ RUN wget -q https://raw.githubusercontent.com/yhirose/cpp-httplib/master/httplib.h \
26
+ -O src/httplib.h
27
+
28
+ # Build C++ engine
29
+ RUN mkdir -p build && cd build && \
30
+ cmake .. -DCMAKE_BUILD_TYPE=Release && \
31
+ make -j$(nproc) hft_engine
32
+
33
+ # Expose port
34
+ EXPOSE 7860
35
+
36
+ # Run FastAPI server
37
+ CMD ["python3", "app.py"]
app.py ADDED
@@ -0,0 +1,256 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ HFT Exchange Simulator — FastAPI server
3
+ Wraps the C++ hft_engine binary via persistent subprocess.
4
+ """
5
+ import json
6
+ import os
7
+ import subprocess
8
+ import sys
9
+ import threading
10
+ from pathlib import Path
11
+ from typing import Optional
12
+
13
+ from fastapi import FastAPI, HTTPException
14
+ from fastapi.middleware.cors import CORSMiddleware
15
+ from fastapi.responses import FileResponse
16
+ from pydantic import BaseModel
17
+
18
+ # BUG-FIX #1: Detect platform and pick the correct binary name.
19
+ # On Windows the compiled binary has a .exe extension.
20
+ _engine_name = "hft_engine.exe" if sys.platform == "win32" else "hft_engine"
21
+ ENGINE_PATH = Path(__file__).parent / "build" / _engine_name
22
+ ENGINE_TIMEOUT = 10.0
23
+
24
+
25
+ class EngineProcess:
26
+ """Manages a persistent child process that speaks line-delimited JSON."""
27
+
28
+ def __init__(self):
29
+ self.proc: Optional[subprocess.Popen] = None
30
+ self.lock = threading.Lock()
31
+ self._start()
32
+
33
+ # ------------------------------------------------------------------
34
+ def _start(self):
35
+ if not ENGINE_PATH.exists():
36
+ # BUG-FIX #2: Do not crash the whole server if the binary is
37
+ # missing — let individual requests report the problem instead.
38
+ self.proc = None
39
+ return
40
+ self.proc = subprocess.Popen(
41
+ [str(ENGINE_PATH)],
42
+ stdin=subprocess.PIPE,
43
+ stdout=subprocess.PIPE,
44
+ stderr=subprocess.DEVNULL,
45
+ text=True,
46
+ bufsize=1,
47
+ )
48
+
49
+ # ------------------------------------------------------------------
50
+ def send(self, cmd: str, payload: dict = None) -> dict:
51
+ if payload is None:
52
+ payload = {}
53
+ line = f"{cmd} {json.dumps(payload)}\n"
54
+
55
+ with self.lock:
56
+ # BUG-FIX #3: If the engine binary was never found, report it
57
+ # clearly instead of an opaque NoneType crash.
58
+ if self.proc is None or self.proc.poll() is not None:
59
+ self._start()
60
+ if self.proc is None:
61
+ raise HTTPException(
62
+ 503,
63
+ f"Engine binary not found at {ENGINE_PATH}. "
64
+ "Build the C++ engine first.",
65
+ )
66
+
67
+ try:
68
+ self.proc.stdin.write(line)
69
+ self.proc.stdin.flush()
70
+
71
+ # BUG-FIX #4 (THE CRASH BUG):
72
+ # The old code used `select.select()` which ONLY works with
73
+ # sockets on Windows — it raises OSError on pipe file
74
+ # descriptors. Replaced with a cross-platform thread-based
75
+ # readline with timeout.
76
+ response_container: list = []
77
+ error_container: list = []
78
+
79
+ def _read():
80
+ try:
81
+ resp = self.proc.stdout.readline()
82
+ response_container.append(resp)
83
+ except Exception as exc:
84
+ error_container.append(exc)
85
+
86
+ reader = threading.Thread(target=_read, daemon=True)
87
+ reader.start()
88
+ reader.join(timeout=ENGINE_TIMEOUT)
89
+
90
+ if reader.is_alive():
91
+ # Timed out — kill and restart the engine
92
+ self.proc.kill()
93
+ self.proc = None
94
+ self._start()
95
+ raise HTTPException(504, "Engine timeout — restarted")
96
+
97
+ if error_container:
98
+ raise error_container[0]
99
+
100
+ response = (
101
+ response_container[0].strip() if response_container else ""
102
+ )
103
+ if not response:
104
+ raise HTTPException(500, "Engine returned empty response")
105
+
106
+ return json.loads(response)
107
+
108
+ except HTTPException:
109
+ raise
110
+ except Exception as e:
111
+ try:
112
+ self.proc.kill()
113
+ except Exception:
114
+ pass
115
+ self.proc = None
116
+ self._start()
117
+ raise HTTPException(500, f"Engine error: {e}")
118
+
119
+
120
+ # BUG-FIX #5: Lazy-initialise so the import doesn't blow up when running
121
+ # under test-harnesses or when the binary hasn't been built yet.
122
+ engine: Optional[EngineProcess] = None
123
+
124
+
125
+ def _get_engine() -> EngineProcess:
126
+ global engine
127
+ if engine is None:
128
+ engine = EngineProcess()
129
+ return engine
130
+
131
+
132
+ app = FastAPI(title="HFT Exchange Simulator", version="1.0.0")
133
+
134
+ app.add_middleware(
135
+ CORSMiddleware,
136
+ allow_origins=["*"],
137
+ allow_methods=["*"],
138
+ allow_headers=["*"],
139
+ )
140
+
141
+
142
+ # ── Pydantic models ──────────────────────────────────────────────────────────
143
+
144
+ class OrderRequest(BaseModel):
145
+ symbol: str = "AAPL"
146
+ side: str = "BUY"
147
+ type: str = "LIMIT"
148
+ price: float = 0.0
149
+ qty: int = 100
150
+ client_id: str = ""
151
+ market_price: float = 0.0
152
+
153
+
154
+ class CancelRequest(BaseModel):
155
+ symbol: str
156
+ id: int
157
+
158
+
159
+ class FeedStepRequest(BaseModel):
160
+ n: int = 10
161
+
162
+
163
+ class BacktestRequest(BaseModel):
164
+ strategy: str = "MarketMaking"
165
+ symbol: str = "AAPL"
166
+ ticks: int = 5000
167
+ start_price: float = 185.0
168
+
169
+
170
+ class RiskLimitsRequest(BaseModel):
171
+ max_order_qty: int = 10000
172
+ max_position: int = 500000
173
+ max_notional_usd: float = 50_000_000.0
174
+ max_orders_per_sec: int = 2000
175
+
176
+
177
+ # ── API routes ────────────────────────────────────────────────────────────────
178
+
179
+ @app.get("/api/status")
180
+ def status():
181
+ return _get_engine().send("status")
182
+
183
+
184
+ @app.get("/api/symbols")
185
+ def symbols():
186
+ return _get_engine().send("symbols")
187
+
188
+
189
+ @app.get("/api/book/{symbol}")
190
+ def book(symbol: str, depth: int = 10):
191
+ # BUG-FIX #6: The `depth` query-param was accepted but never forwarded
192
+ # to the engine. Now included in the payload.
193
+ return _get_engine().send("book", {"symbol": symbol, "depth": depth})
194
+
195
+
196
+ @app.get("/api/positions")
197
+ def positions():
198
+ return _get_engine().send("positions")
199
+
200
+
201
+ @app.get("/api/trades")
202
+ def trades():
203
+ return _get_engine().send("trades")
204
+
205
+
206
+ @app.get("/api/orders")
207
+ def orders():
208
+ return _get_engine().send("orders")
209
+
210
+
211
+ @app.post("/api/order")
212
+ def submit_order(req: OrderRequest):
213
+ return _get_engine().send("order", req.model_dump())
214
+
215
+
216
+ @app.delete("/api/order")
217
+ def cancel_order(req: CancelRequest):
218
+ return _get_engine().send("cancel", req.model_dump())
219
+
220
+
221
+ @app.post("/api/feed/step")
222
+ def feed_step(req: FeedStepRequest):
223
+ return _get_engine().send("feed_step", {"n": req.n})
224
+
225
+
226
+ @app.post("/api/backtest")
227
+ def backtest(req: BacktestRequest):
228
+ return _get_engine().send("backtest", req.model_dump())
229
+
230
+
231
+ @app.get("/api/risk/limits")
232
+ def get_limits():
233
+ return _get_engine().send("get_limits")
234
+
235
+
236
+ @app.post("/api/risk/limits")
237
+ def set_limits(req: RiskLimitsRequest):
238
+ return _get_engine().send("risk_limits", req.model_dump())
239
+
240
+
241
+ @app.post("/api/reset")
242
+ def reset():
243
+ return _get_engine().send("reset")
244
+
245
+
246
+ @app.get("/")
247
+ def serve_index():
248
+ return FileResponse(str(Path(__file__).parent / "index.html"))
249
+
250
+
251
+ # ── Entry point ───────────────────────────────────────────────────────────────
252
+
253
+ if __name__ == "__main__":
254
+ import uvicorn
255
+ port = int(os.environ.get("PORT", 7860))
256
+ uvicorn.run(app, host="0.0.0.0", port=port, log_level="info")
benchmark/bench_matching_engine.cpp ADDED
@@ -0,0 +1,303 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // benchmark/bench_matching_engine.cpp
2
+ // Performance benchmark — measures throughput and latency under load
3
+ // Build: cmake --build build --target hft_bench && ./build/hft_bench
4
+
5
+ #include "core/order_book.hpp"
6
+ #include "core/simulator.hpp"
7
+ #include "core/types.hpp"
8
+ #include "journal/journal.hpp"
9
+ #include <iostream>
10
+ #include <vector>
11
+ #include <chrono>
12
+ #include <random>
13
+ #include <algorithm>
14
+ #include <iomanip>
15
+ #include <cmath>
16
+ #include <numeric>
17
+
18
+ using namespace hft;
19
+ using Clock = std::chrono::high_resolution_clock;
20
+
21
+ struct BenchResult {
22
+ std::string name;
23
+ int64_t ops;
24
+ double total_us;
25
+ double ops_per_sec;
26
+ double p50_ns;
27
+ double p99_ns;
28
+ double p999_ns;
29
+ double avg_ns;
30
+ };
31
+
32
+ static BenchResult run_bench(const std::string& name, int64_t ops,
33
+ std::vector<int64_t>& latencies) {
34
+ std::sort(latencies.begin(), latencies.end());
35
+ double total_ns = 0;
36
+ for (auto l : latencies) total_ns += l;
37
+
38
+ BenchResult r;
39
+ r.name = name;
40
+ r.ops = ops;
41
+ r.total_us = total_ns / 1000.0;
42
+ r.ops_per_sec = ops / (total_ns / 1e9);
43
+ r.avg_ns = total_ns / ops;
44
+ r.p50_ns = latencies[latencies.size() * 50 / 100];
45
+ r.p99_ns = latencies[latencies.size() * 99 / 100];
46
+ r.p999_ns = latencies[latencies.size() * 999 / 1000];
47
+ return r;
48
+ }
49
+
50
+ static void print_result(const BenchResult& r) {
51
+ std::cout << std::left << std::setw(35) << r.name
52
+ << std::right
53
+ << " ops: " << std::setw(8) << r.ops
54
+ << " throughput: " << std::setw(10) << std::fixed << std::setprecision(0) << r.ops_per_sec << " ops/s"
55
+ << " avg: " << std::setw(8) << std::setprecision(1) << r.avg_ns << " ns"
56
+ << " p50: " << std::setw(8) << r.p50_ns << " ns"
57
+ << " p99: " << std::setw(8) << r.p99_ns << " ns"
58
+ << " p999: " << std::setw(8) << r.p999_ns << " ns"
59
+ << "\n";
60
+ }
61
+
62
+ // ─── Benchmark 1: Order Insert (no match) ───────────────────────────────────
63
+
64
+ static BenchResult bench_insert_no_match(int n) {
65
+ OrderBook book("BENCH");
66
+ std::mt19937 rng(42);
67
+ std::uniform_int_distribution<int> price_dist(1000, 2000); // spread out
68
+ std::uniform_int_distribution<int> qty_dist(100, 1000);
69
+ std::vector<int64_t> latencies;
70
+ latencies.reserve(n);
71
+
72
+ for (int i = 0; i < n; ++i) {
73
+ Order o;
74
+ o.id = i + 1;
75
+ o.symbol = "BENCH";
76
+ o.side = (i % 2 == 0) ? Side::BUY : Side::SELL;
77
+ o.type = OrderType::LIMIT;
78
+ // Spread prices so no matching occurs (buys low, sells high)
79
+ o.price = (o.side == Side::BUY) ? to_price(100.0 + price_dist(rng) * 0.01)
80
+ : to_price(200.0 + price_dist(rng) * 0.01);
81
+ o.qty = qty_dist(rng);
82
+
83
+ auto t0 = Clock::now();
84
+ book.add_order(o);
85
+ auto t1 = Clock::now();
86
+ latencies.push_back(std::chrono::duration_cast<std::chrono::nanoseconds>(t1 - t0).count());
87
+ }
88
+
89
+ return run_bench("Insert (no match)", n, latencies);
90
+ }
91
+
92
+ // ─── Benchmark 2: Order Match (aggressive orders) ───────────────────────────
93
+
94
+ static BenchResult bench_match(int n) {
95
+ OrderBook book("BENCH");
96
+ std::mt19937 rng(123);
97
+ std::uniform_int_distribution<int> qty_dist(50, 200);
98
+ std::vector<int64_t> latencies;
99
+ latencies.reserve(n);
100
+
101
+ // Pre-populate the book with resting orders
102
+ for (int i = 0; i < n; ++i) {
103
+ Order o;
104
+ o.id = i + 1;
105
+ o.symbol = "BENCH";
106
+ o.side = Side::SELL;
107
+ o.type = OrderType::LIMIT;
108
+ o.price = to_price(100.0 + (i % 50) * 0.01); // 50 price levels
109
+ o.qty = qty_dist(rng);
110
+ book.add_order(o);
111
+ }
112
+
113
+ // Now send aggressive buy orders that match
114
+ for (int i = 0; i < n; ++i) {
115
+ Order o;
116
+ o.id = n + i + 1;
117
+ o.symbol = "BENCH";
118
+ o.side = Side::BUY;
119
+ o.type = OrderType::MARKET;
120
+ o.price = 0;
121
+ o.qty = 1; // small qty to match single order partially
122
+
123
+ auto t0 = Clock::now();
124
+ book.add_order(o);
125
+ auto t1 = Clock::now();
126
+ latencies.push_back(std::chrono::duration_cast<std::chrono::nanoseconds>(t1 - t0).count());
127
+ }
128
+
129
+ return run_bench("Match (market order)", n, latencies);
130
+ }
131
+
132
+ // ─── Benchmark 3: Cancel Order ──────────────────────────────────────────────
133
+
134
+ static BenchResult bench_cancel(int n) {
135
+ OrderBook book("BENCH");
136
+ std::mt19937 rng(456);
137
+ std::uniform_int_distribution<int> price_dist(1, 1000);
138
+ std::vector<int64_t> latencies;
139
+ latencies.reserve(n);
140
+
141
+ // Insert orders
142
+ for (int i = 0; i < n; ++i) {
143
+ Order o;
144
+ o.id = i + 1;
145
+ o.symbol = "BENCH";
146
+ o.side = Side::BUY;
147
+ o.type = OrderType::LIMIT;
148
+ o.price = to_price(100.0 + price_dist(rng) * 0.01);
149
+ o.qty = 100;
150
+ book.add_order(o);
151
+ }
152
+
153
+ // Cancel in random order
154
+ std::vector<int> ids(n);
155
+ std::iota(ids.begin(), ids.end(), 1);
156
+ std::shuffle(ids.begin(), ids.end(), rng);
157
+
158
+ for (int id : ids) {
159
+ auto t0 = Clock::now();
160
+ book.cancel_order(id);
161
+ auto t1 = Clock::now();
162
+ latencies.push_back(std::chrono::duration_cast<std::chrono::nanoseconds>(t1 - t0).count());
163
+ }
164
+
165
+ return run_bench("Cancel (random)", n, latencies);
166
+ }
167
+
168
+ // ─── Benchmark 4: Snapshot ──────────────────────────────────────────────────
169
+
170
+ static BenchResult bench_snapshot(int n) {
171
+ OrderBook book("BENCH");
172
+
173
+ // Build a book with many levels
174
+ for (int i = 0; i < 500; ++i) {
175
+ Order buy, sell;
176
+ buy.id = i * 2 + 1; buy.symbol = "BENCH"; buy.side = Side::BUY;
177
+ buy.type = OrderType::LIMIT; buy.price = to_price(100.0 - i * 0.01); buy.qty = 100;
178
+
179
+ sell.id = i * 2 + 2; sell.symbol = "BENCH"; sell.side = Side::SELL;
180
+ sell.type = OrderType::LIMIT; sell.price = to_price(101.0 + i * 0.01); sell.qty = 100;
181
+
182
+ book.add_order(buy);
183
+ book.add_order(sell);
184
+ }
185
+
186
+ std::vector<int64_t> latencies;
187
+ latencies.reserve(n);
188
+
189
+ for (int i = 0; i < n; ++i) {
190
+ auto t0 = Clock::now();
191
+ auto snap = book.snapshot(10);
192
+ auto t1 = Clock::now();
193
+ latencies.push_back(std::chrono::duration_cast<std::chrono::nanoseconds>(t1 - t0).count());
194
+ (void)snap;
195
+ }
196
+
197
+ return run_bench("Snapshot (10 levels)", n, latencies);
198
+ }
199
+
200
+ // ─── Benchmark 5: Full Pipeline (submit_order via ExchangeSimulator) ────────
201
+
202
+ static BenchResult bench_full_pipeline(int n) {
203
+ RiskLimits limits;
204
+ limits.max_order_qty = 100000;
205
+ limits.max_position = 10000000;
206
+ limits.max_notional_usd = 1e12;
207
+ limits.max_orders_per_sec = 1000000;
208
+
209
+ ExchangeSimulator sim(limits);
210
+ sim.add_symbol("BENCH");
211
+
212
+ std::mt19937 rng(789);
213
+ std::uniform_int_distribution<int> price_dist(9900, 10100);
214
+ std::uniform_int_distribution<int> qty_dist(1, 10);
215
+ std::vector<int64_t> latencies;
216
+ latencies.reserve(n);
217
+
218
+ for (int i = 0; i < n; ++i) {
219
+ Order o;
220
+ o.id = sim.next_order_id();
221
+ o.symbol = "BENCH";
222
+ o.side = (i % 2 == 0) ? Side::BUY : Side::SELL;
223
+ o.type = OrderType::LIMIT;
224
+ o.price = to_price(price_dist(rng) * 0.01); // ~99.00 - 101.00
225
+ o.qty = qty_dist(rng);
226
+
227
+ auto t0 = Clock::now();
228
+ sim.submit_order(o);
229
+ auto t1 = Clock::now();
230
+ latencies.push_back(std::chrono::duration_cast<std::chrono::nanoseconds>(t1 - t0).count());
231
+ }
232
+
233
+ return run_bench("Full pipeline (risk+match)", n, latencies);
234
+ }
235
+
236
+ // ─── Benchmark 6: Backtest Throughput ────────────────────────────────────────
237
+
238
+ static void bench_backtest_throughput() {
239
+ std::cout << "\n--- Backtest Throughput ---\n";
240
+
241
+ int ticks = 50000;
242
+ MarketDataParser::Config cfg;
243
+ cfg.symbol = "BENCH"; cfg.start_price = 100.0;
244
+ cfg.ticks = ticks; cfg.volatility = 0.001; cfg.seed = 42;
245
+ auto tick_data = MarketDataParser::generate_synthetic(cfg);
246
+
247
+ RiskLimits rl;
248
+ rl.max_order_qty = 10000;
249
+ rl.max_position = 1000000;
250
+ rl.max_notional_usd = 1e12;
251
+ rl.max_orders_per_sec = 100000;
252
+
253
+ auto t0 = Clock::now();
254
+
255
+ Backtester bt(rl);
256
+ bt.add_symbol("BENCH");
257
+ bt.set_strategy(std::make_unique<MarketMakingStrategy>(5.0, 100));
258
+ auto result = bt.run(tick_data, false);
259
+
260
+ auto t1 = Clock::now();
261
+ double elapsed_s = std::chrono::duration<double>(t1 - t0).count();
262
+ double throughput = ticks / elapsed_s;
263
+
264
+ std::cout << " Ticks: " << ticks << "\n";
265
+ std::cout << " Elapsed: " << std::fixed << std::setprecision(3) << elapsed_s << " s\n";
266
+ std::cout << " Throughput: " << std::setprecision(0) << throughput << " ticks/s\n";
267
+ std::cout << " Orders: " << result.orders_submitted << "\n";
268
+ std::cout << " Fills: " << result.orders_filled << "\n";
269
+ std::cout << " Trades: " << result.trades_count << "\n";
270
+ std::cout << " PnL: " << std::setprecision(2) << result.total_pnl << "\n";
271
+ }
272
+
273
+ // ─── Main ────────────────────────────────────────────────────────────────────
274
+
275
+ int main() {
276
+ std::cout << "\n";
277
+ std::cout << "╔════════════════════════════════════════════════════���══════════════════════════════════════════════════════════════╗\n";
278
+ std::cout << "║ Orderflow Replay Engine — Performance Benchmark ║\n";
279
+ std::cout << "╚═══════════════════════════════════════════════════════════════════════════════════════════════════════════════════╝\n\n";
280
+
281
+ const int N = 100000;
282
+
283
+ std::vector<BenchResult> results;
284
+ results.push_back(bench_insert_no_match(N));
285
+ results.push_back(bench_match(N));
286
+ results.push_back(bench_cancel(N));
287
+ results.push_back(bench_snapshot(N));
288
+ results.push_back(bench_full_pipeline(N));
289
+
290
+ std::cout << "--- Latency Results (N = " << N << " ops each) ---\n\n";
291
+ for (auto& r : results) print_result(r);
292
+
293
+ bench_backtest_throughput();
294
+
295
+ std::cout << "\n--- Memory ---\n";
296
+ std::cout << " sizeof(Order): " << sizeof(Order) << " bytes\n";
297
+ std::cout << " sizeof(Trade): " << sizeof(Trade) << " bytes\n";
298
+ std::cout << " sizeof(PriceLevel): " << sizeof(PriceLevel) << " bytes\n";
299
+ std::cout << " sizeof(MarketDataTick): " << sizeof(MarketDataTick) << " bytes\n";
300
+
301
+ std::cout << "\nDone.\n\n";
302
+ return 0;
303
+ }
index.html ADDED
@@ -0,0 +1,1278 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>HFT Exchange Simulator</title>
7
+ <style>
8
+ @import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;500;600;700&family=Space+Mono:wght@400;700&display=swap');
9
+
10
+ :root {
11
+ --bg: #080c10;
12
+ --bg1: #0d1219;
13
+ --bg2: #121820;
14
+ --bg3: #1a2230;
15
+ --border: #1e2d3d;
16
+ --border2: #253545;
17
+ --text: #c8d8e8;
18
+ --dim: #5a7a90;
19
+ --green: #00e676;
20
+ --green2: #00c853;
21
+ --red: #ff1744;
22
+ --red2: #d50000;
23
+ --yellow: #ffea00;
24
+ --blue: #40c4ff;
25
+ --purple: #ea80fc;
26
+ --orange: #ff9100;
27
+ --cyan: #18ffff;
28
+ --accent: #00e5ff;
29
+ --glow: 0 0 12px rgba(0,229,255,0.25);
30
+ --glow-g: 0 0 12px rgba(0,230,118,0.3);
31
+ --glow-r: 0 0 12px rgba(255,23,68,0.3);
32
+ --mono: 'JetBrains Mono', 'Space Mono', monospace;
33
+ }
34
+
35
+ * { box-sizing: border-box; margin: 0; padding: 0; }
36
+
37
+ body {
38
+ font-family: var(--mono);
39
+ background: var(--bg);
40
+ color: var(--text);
41
+ min-height: 100vh;
42
+ overflow-x: hidden;
43
+ font-size: 12px;
44
+ }
45
+
46
+ /* Scanline overlay */
47
+ body::before {
48
+ content: '';
49
+ position: fixed; inset: 0;
50
+ background: repeating-linear-gradient(0deg,
51
+ transparent, transparent 2px,
52
+ rgba(0,0,0,0.03) 2px, rgba(0,0,0,0.03) 4px);
53
+ pointer-events: none; z-index: 9999;
54
+ }
55
+
56
+ /* Header */
57
+ .header {
58
+ display: flex; align-items: center; justify-content: space-between;
59
+ padding: 10px 20px;
60
+ background: linear-gradient(90deg, #050a10 0%, #0d1825 50%, #050a10 100%);
61
+ border-bottom: 1px solid var(--accent);
62
+ box-shadow: 0 0 30px rgba(0,229,255,0.1);
63
+ position: sticky; top: 0; z-index: 100;
64
+ }
65
+
66
+ .logo {
67
+ font-size: 15px; font-weight: 700; letter-spacing: 3px;
68
+ color: var(--accent);
69
+ text-shadow: 0 0 20px rgba(0,229,255,0.6);
70
+ }
71
+ .logo span { color: var(--green); }
72
+
73
+ .header-stats {
74
+ display: flex; gap: 24px;
75
+ }
76
+ .hstat { text-align: center; }
77
+ .hstat-label { font-size: 9px; color: var(--dim); text-transform: uppercase; letter-spacing: 1px; }
78
+ .hstat-val { font-size: 13px; font-weight: 600; }
79
+
80
+ .conn-dot {
81
+ width: 8px; height: 8px; border-radius: 50%;
82
+ background: var(--green);
83
+ box-shadow: 0 0 8px var(--green);
84
+ animation: pulse 2s ease-in-out infinite;
85
+ }
86
+ @keyframes pulse {
87
+ 0%,100% { opacity: 1; } 50% { opacity: 0.4; }
88
+ }
89
+
90
+ /* Main grid */
91
+ .grid {
92
+ display: grid;
93
+ grid-template-columns: 320px 1fr 300px;
94
+ grid-template-rows: auto auto auto;
95
+ gap: 1px;
96
+ background: var(--border);
97
+ height: calc(100vh - 48px);
98
+ }
99
+
100
+ .panel {
101
+ background: var(--bg1);
102
+ overflow: hidden;
103
+ display: flex; flex-direction: column;
104
+ }
105
+
106
+ .panel-title {
107
+ font-size: 9px; font-weight: 700;
108
+ text-transform: uppercase; letter-spacing: 2px;
109
+ color: var(--dim);
110
+ padding: 8px 14px;
111
+ border-bottom: 1px solid var(--border);
112
+ display: flex; align-items: center; justify-content: space-between;
113
+ background: var(--bg2);
114
+ flex-shrink: 0;
115
+ }
116
+ .panel-title .tag {
117
+ font-size: 8px; padding: 2px 6px;
118
+ background: var(--bg3); border-radius: 2px;
119
+ color: var(--accent); letter-spacing: 1px;
120
+ }
121
+
122
+ .panel-body { flex: 1; overflow-y: auto; padding: 12px; }
123
+ .panel-body::-webkit-scrollbar { width: 3px; }
124
+ .panel-body::-webkit-scrollbar-track { background: var(--bg1); }
125
+ .panel-body::-webkit-scrollbar-thumb { background: var(--border2); border-radius: 2px; }
126
+
127
+ /* ORDER BOOK */
128
+ .book-header {
129
+ display: grid; grid-template-columns: 1fr 80px 80px;
130
+ font-size: 9px; color: var(--dim); letter-spacing: 1px;
131
+ padding: 4px 12px; border-bottom: 1px solid var(--border);
132
+ text-transform: uppercase;
133
+ }
134
+
135
+ .book-row {
136
+ display: grid; grid-template-columns: 1fr 80px 80px;
137
+ padding: 3px 12px;
138
+ position: relative; cursor: default;
139
+ transition: background 0.1s;
140
+ }
141
+ .book-row:hover { background: rgba(255,255,255,0.03); }
142
+
143
+ .book-bar {
144
+ position: absolute; top: 0; bottom: 0;
145
+ opacity: 0.12; pointer-events: none;
146
+ transition: width 0.3s ease;
147
+ }
148
+
149
+ .ask-row .book-bar { background: var(--red); right: 0; }
150
+ .bid-row .book-bar { background: var(--green); left: 0; }
151
+
152
+ .ask-row .price { color: var(--red); font-weight: 600; }
153
+ .bid-row .price { color: var(--green); font-weight: 600; }
154
+ .qty-val { text-align: right; color: var(--text); }
155
+ .orders-val { text-align: right; color: var(--dim); }
156
+
157
+ .spread-row {
158
+ display: flex; align-items: center; justify-content: center;
159
+ padding: 4px; font-size: 9px; color: var(--yellow);
160
+ border-top: 1px solid var(--border); border-bottom: 1px solid var(--border);
161
+ background: var(--bg2);
162
+ letter-spacing: 2px;
163
+ }
164
+
165
+ /* ORDER ENTRY */
166
+ .tabs {
167
+ display: flex; border-bottom: 1px solid var(--border);
168
+ flex-shrink: 0;
169
+ }
170
+ .tab {
171
+ flex: 1; padding: 8px; font-family: var(--mono); font-size: 9px;
172
+ font-weight: 700; letter-spacing: 1px; text-transform: uppercase;
173
+ border: none; cursor: pointer;
174
+ transition: all 0.2s;
175
+ background: var(--bg2); color: var(--dim);
176
+ }
177
+ .tab.active-buy { background: rgba(0,230,118,0.1); color: var(--green); border-bottom: 2px solid var(--green); }
178
+ .tab.active-sell { background: rgba(255,23,68,0.1); color: var(--red); border-bottom: 2px solid var(--red); }
179
+ .tab:hover { background: var(--bg3); }
180
+
181
+ .form-group { margin-bottom: 10px; }
182
+ label { font-size: 9px; color: var(--dim); text-transform: uppercase; letter-spacing: 1px; display: block; margin-bottom: 4px; }
183
+
184
+ input, select {
185
+ width: 100%; padding: 8px 10px;
186
+ background: var(--bg3); border: 1px solid var(--border2);
187
+ color: var(--text); font-family: var(--mono); font-size: 12px;
188
+ border-radius: 2px; outline: none;
189
+ transition: border-color 0.2s, box-shadow 0.2s;
190
+ }
191
+ input:focus, select:focus {
192
+ border-color: var(--accent);
193
+ box-shadow: var(--glow);
194
+ }
195
+ select option { background: var(--bg2); }
196
+
197
+ .btn {
198
+ width: 100%; padding: 10px;
199
+ font-family: var(--mono); font-size: 11px; font-weight: 700;
200
+ letter-spacing: 2px; text-transform: uppercase;
201
+ border: none; border-radius: 2px; cursor: pointer;
202
+ transition: all 0.2s; margin-top: 4px;
203
+ }
204
+ .btn-buy {
205
+ background: linear-gradient(135deg, #00c853, #00e676);
206
+ color: #001a0a;
207
+ box-shadow: 0 0 15px rgba(0,230,118,0.3);
208
+ }
209
+ .btn-buy:hover { box-shadow: 0 0 25px rgba(0,230,118,0.5); transform: translateY(-1px); }
210
+ .btn-sell {
211
+ background: linear-gradient(135deg, #d50000, #ff1744);
212
+ color: #1a0000;
213
+ box-shadow: 0 0 15px rgba(255,23,68,0.3);
214
+ }
215
+ .btn-sell:hover { box-shadow: 0 0 25px rgba(255,23,68,0.5); transform: translateY(-1px); }
216
+ .btn-neutral {
217
+ background: var(--bg3); color: var(--text);
218
+ border: 1px solid var(--border2);
219
+ }
220
+ .btn-neutral:hover { border-color: var(--accent); color: var(--accent); }
221
+ .btn-accent {
222
+ background: linear-gradient(135deg, #006080, #00acc1);
223
+ color: #001a20;
224
+ box-shadow: 0 0 15px rgba(0,229,255,0.2);
225
+ }
226
+ .btn-accent:hover { box-shadow: 0 0 25px rgba(0,229,255,0.4); transform: translateY(-1px); }
227
+
228
+ /* PRICE CHART */
229
+ canvas { display: block; }
230
+ #price-canvas { width: 100%; }
231
+
232
+ /* LATENCY / STATS */
233
+ .stat-row {
234
+ display: flex; justify-content: space-between;
235
+ padding: 5px 0; border-bottom: 1px solid rgba(30,45,61,0.5);
236
+ }
237
+ .stat-key { color: var(--dim); font-size: 10px; }
238
+ .stat-val { font-weight: 600; font-size: 11px; }
239
+ .green { color: var(--green); }
240
+ .red { color: var(--red); }
241
+ .yellow { color: var(--yellow); }
242
+ .blue { color: var(--blue); }
243
+ .cyan { color: var(--cyan); }
244
+
245
+ /* TRADE LOG */
246
+ .trade-entry {
247
+ display: flex; gap: 8px; padding: 5px 0;
248
+ border-bottom: 1px solid rgba(30,45,61,0.4);
249
+ font-size: 10px; align-items: center;
250
+ animation: fadeIn 0.3s ease;
251
+ }
252
+ @keyframes fadeIn { from { opacity: 0; transform: translateX(-5px); } to { opacity: 1; transform: translateX(0); } }
253
+
254
+ .trade-side {
255
+ padding: 2px 5px; font-size: 8px; font-weight: 700;
256
+ letter-spacing: 1px; border-radius: 2px;
257
+ min-width: 30px; text-align: center;
258
+ }
259
+ .buy-badge { background: rgba(0,230,118,0.15); color: var(--green); }
260
+ .sell-badge { background: rgba(255,23,68,0.15); color: var(--red); }
261
+
262
+ /* POSITION TABLE */
263
+ .pos-table { width: 100%; border-collapse: collapse; font-size: 10px; }
264
+ .pos-table th {
265
+ text-align: left; padding: 4px 6px;
266
+ color: var(--dim); font-size: 8px; text-transform: uppercase; letter-spacing: 1px;
267
+ border-bottom: 1px solid var(--border);
268
+ }
269
+ .pos-table td { padding: 5px 6px; border-bottom: 1px solid rgba(30,45,61,0.3); }
270
+
271
+ /* BACKTEST PANEL */
272
+ .bt-result {
273
+ background: var(--bg2); border: 1px solid var(--border2);
274
+ border-radius: 3px; padding: 10px; margin-top: 10px;
275
+ }
276
+ .bt-metric {
277
+ display: flex; justify-content: space-between;
278
+ padding: 3px 0; font-size: 10px;
279
+ }
280
+
281
+ /* EQUITY CURVE */
282
+ #equity-canvas { width: 100%; border-top: 1px solid var(--border); }
283
+
284
+ /* LATENCY BARS */
285
+ .lat-bar-wrap { margin: 4px 0; }
286
+ .lat-bar-label { display: flex; justify-content: space-between; font-size: 9px; margin-bottom: 2px; }
287
+ .lat-bar { height: 6px; background: var(--bg3); border-radius: 3px; overflow: hidden; }
288
+ .lat-bar-fill { height: 100%; border-radius: 3px; transition: width 0.5s ease; }
289
+
290
+ /* RISK PANEL */
291
+ .risk-meter {
292
+ height: 8px; background: var(--bg3); border-radius: 4px; overflow: hidden;
293
+ margin: 4px 0 10px;
294
+ }
295
+ .risk-fill {
296
+ height: 100%; border-radius: 4px; transition: width 0.5s ease;
297
+ }
298
+
299
+ /* FEED CONTROL */
300
+ .feed-control {
301
+ display: flex; gap: 6px; padding: 8px 12px;
302
+ border-top: 1px solid var(--border);
303
+ flex-shrink: 0;
304
+ }
305
+ .feed-control .btn { margin: 0; flex: 1; padding: 7px; font-size: 9px; }
306
+
307
+ /* Symbol selector strip */
308
+ .sym-strip {
309
+ display: flex; gap: 4px; padding: 6px 12px;
310
+ border-bottom: 1px solid var(--border);
311
+ overflow-x: auto; flex-shrink: 0;
312
+ }
313
+ .sym-btn {
314
+ padding: 3px 10px; font-family: var(--mono); font-size: 9px;
315
+ font-weight: 700; letter-spacing: 1px; text-transform: uppercase;
316
+ border: 1px solid var(--border2); border-radius: 2px;
317
+ cursor: pointer; background: var(--bg3); color: var(--dim);
318
+ transition: all 0.15s; white-space: nowrap;
319
+ }
320
+ .sym-btn.active, .sym-btn:hover {
321
+ border-color: var(--accent); color: var(--accent);
322
+ background: rgba(0,229,255,0.08);
323
+ }
324
+
325
+ /* Notification toast */
326
+ .toast {
327
+ position: fixed; top: 60px; right: 20px;
328
+ background: var(--bg2); border: 1px solid var(--accent);
329
+ padding: 10px 16px; border-radius: 3px;
330
+ font-size: 11px; z-index: 1000;
331
+ animation: toastIn 0.3s ease, toastOut 0.3s ease 2.7s forwards;
332
+ box-shadow: var(--glow);
333
+ }
334
+ @keyframes toastIn { from { opacity: 0; transform: translateX(20px); } to { opacity: 1; transform: translateX(0); } }
335
+ @keyframes toastOut { to { opacity: 0; transform: translateX(20px); } }
336
+
337
+ /* Scrollable sections */
338
+ .scroll-area { flex: 1; overflow-y: auto; }
339
+ .scroll-area::-webkit-scrollbar { width: 3px; }
340
+ .scroll-area::-webkit-scrollbar-thumb { background: var(--border2); }
341
+
342
+ /* Responsive col spans */
343
+ .span2 { grid-column: span 2; }
344
+ .col-left { grid-column: 1; grid-row: 1 / span 3; }
345
+ .col-mid { grid-column: 2; }
346
+ .col-right { grid-column: 3; grid-row: 1 / span 3; }
347
+
348
+ .grid-inner {
349
+ display: grid;
350
+ grid-template-columns: 320px 1fr 300px;
351
+ grid-template-rows: 360px 260px 1fr;
352
+ gap: 1px;
353
+ background: var(--border);
354
+ height: calc(100vh - 48px);
355
+ }
356
+
357
+ /* Alert for rejected orders */
358
+ .rejected { border-left: 3px solid var(--red) !important; animation: shake 0.3s; }
359
+ @keyframes shake {
360
+ 0%,100% { transform: translateX(0); }
361
+ 25% { transform: translateX(-4px); }
362
+ 75% { transform: translateX(4px); }
363
+ }
364
+
365
+ /* Blinking cursor */
366
+ .cursor::after { content: '█'; animation: blink 1s step-end infinite; color: var(--accent); }
367
+ @keyframes blink { 50% { opacity: 0; } }
368
+
369
+ /* Number animations */
370
+ .num-up { animation: numUp 0.4s ease; }
371
+ .num-down { animation: numDown 0.4s ease; }
372
+ @keyframes numUp { 0% { color: var(--green); } 100% { color: inherit; } }
373
+ @keyframes numDown { 0% { color: var(--red); } 100% { color: inherit; } }
374
+
375
+ /* Progress ring for latency */
376
+ .ring-wrap { position: relative; display: inline-flex; align-items: center; justify-content: center; }
377
+
378
+ /* Hover row highlight for order book */
379
+ .book-asks .book-row:hover .price { text-shadow: 0 0 8px var(--red); }
380
+ .book-bids .book-row:hover .price { text-shadow: 0 0 8px var(--green); }
381
+
382
+ /* Section divider */
383
+ .divider { height: 1px; background: var(--border); margin: 10px 0; }
384
+
385
+ /* Blinking red for reject */
386
+ .flash-red { animation: flashRed 0.5s; }
387
+ @keyframes flashRed { 0%,100% { background: var(--bg1); } 50% { background: rgba(255,23,68,0.15); } }
388
+
389
+ /* Toggle switch */
390
+ .toggle { position: relative; display: inline-flex; align-items: center; gap: 8px; cursor: pointer; }
391
+ .toggle input { display: none; }
392
+ .toggle-track {
393
+ width: 32px; height: 16px; background: var(--bg3);
394
+ border: 1px solid var(--border2); border-radius: 8px;
395
+ transition: background 0.2s;
396
+ position: relative;
397
+ }
398
+ .toggle input:checked + .toggle-track { background: var(--green2); border-color: var(--green); }
399
+ .toggle-thumb {
400
+ position: absolute; top: 1px; left: 1px;
401
+ width: 12px; height: 12px; border-radius: 6px;
402
+ background: var(--dim); transition: transform 0.2s, background 0.2s;
403
+ }
404
+ .toggle input:checked ~ .toggle-thumb {
405
+ transform: translateX(16px); background: white;
406
+ }
407
+
408
+ .mode-tabs { display: flex; gap: 1px; border-bottom: 1px solid var(--border); flex-shrink: 0; }
409
+ .mode-tab {
410
+ flex: 1; padding: 6px; font-family: var(--mono); font-size: 9px; font-weight: 700;
411
+ letter-spacing: 1px; text-transform: uppercase; border: none; cursor: pointer;
412
+ background: var(--bg2); color: var(--dim); transition: all 0.15s;
413
+ }
414
+ .mode-tab.active { background: var(--bg3); color: var(--accent); border-bottom: 1px solid var(--accent); }
415
+ </style>
416
+ </head>
417
+ <body>
418
+
419
+ <!-- HEADER -->
420
+ <div class="header">
421
+ <div style="display:flex;align-items:center;gap:16px">
422
+ <div class="conn-dot" id="conn-dot"></div>
423
+ <div class="logo">HFT<span>·</span>EXCHANGE<span>·</span>SIM</div>
424
+ <div style="font-size:9px;color:var(--dim);letter-spacing:1px">C++ MATCHING ENGINE</div>
425
+ </div>
426
+ <div class="header-stats">
427
+ <div class="hstat"><div class="hstat-label">Orders</div><div class="hstat-val cyan" id="h-orders">0</div></div>
428
+ <div class="hstat"><div class="hstat-label">Trades</div><div class="hstat-val green" id="h-trades">0</div></div>
429
+ <div class="hstat"><div class="hstat-label">Rejects</div><div class="hstat-val red" id="h-rejects">0</div></div>
430
+ <div class="hstat"><div class="hstat-label">P&L</div><div class="hstat-val" id="h-pnl">$0.00</div></div>
431
+ <div class="hstat"><div class="hstat-label">p99 Latency</div><div class="hstat-val yellow" id="h-p99">—</div></div>
432
+ <div class="hstat"><div class="hstat-label">Throughput</div><div class="hstat-val blue" id="h-thru">—</div></div>
433
+ </div>
434
+ </div>
435
+
436
+ <!-- MAIN GRID -->
437
+ <div class="grid-inner">
438
+
439
+ <!-- COL 1: Order Book + Order Entry -->
440
+ <div class="panel" style="grid-column:1;grid-row:1/span 3;display:flex;flex-direction:column">
441
+
442
+ <!-- Symbol strip -->
443
+ <div class="sym-strip" id="sym-strip"></div>
444
+
445
+ <!-- Order Book -->
446
+ <div class="panel-title">
447
+ Order Book — L2 <span id="book-symbol-tag" class="tag">AAPL</span>
448
+ </div>
449
+ <div class="book-header">
450
+ <span>PRICE</span><span style="text-align:right">QTY</span><span style="text-align:right">ODS</span>
451
+ </div>
452
+
453
+ <div style="flex:1;overflow-y:auto;display:flex;flex-direction:column">
454
+ <div class="book-asks scroll-area" id="book-asks" style="flex:1;display:flex;flex-direction:column;justify-content:flex-end"></div>
455
+ <div class="spread-row" id="spread-row">SPREAD: —</div>
456
+ <div class="book-bids scroll-area" id="book-bids" style="flex:1"></div>
457
+ </div>
458
+
459
+ <!-- Order Entry -->
460
+ <div class="divider" style="margin:0"></div>
461
+ <div class="panel-title">Order Entry</div>
462
+
463
+ <div class="tabs">
464
+ <button class="tab active-buy" id="tab-buy" onclick="setOrderSide('BUY')">▲ BUY</button>
465
+ <button class="tab" id="tab-sell" onclick="setOrderSide('SELL')">▼ SELL</button>
466
+ </div>
467
+
468
+ <div style="padding:12px;flex-shrink:0">
469
+ <div class="form-group">
470
+ <label>Type</label>
471
+ <select id="oe-type" onchange="updateOrderForm()">
472
+ <option value="LIMIT">LIMIT</option>
473
+ <option value="MARKET">MARKET</option>
474
+ <option value="IOC">IOC (Immediate-Or-Cancel)</option>
475
+ <option value="FOK">FOK (Fill-Or-Kill)</option>
476
+ </select>
477
+ </div>
478
+ <div class="form-group">
479
+ <label>Symbol</label>
480
+ <select id="oe-symbol">
481
+ <option>AAPL</option><option>GOOG</option><option>MSFT</option>
482
+ <option>TSLA</option><option>NVDA</option>
483
+ </select>
484
+ </div>
485
+ <div class="form-group" id="oe-price-group">
486
+ <label>Price</label>
487
+ <input type="number" id="oe-price" step="0.01" placeholder="0.00">
488
+ </div>
489
+ <div class="form-group">
490
+ <label>Quantity</label>
491
+ <input type="number" id="oe-qty" value="100" min="1">
492
+ </div>
493
+ <div class="form-group" style="display:none">
494
+ <label>Client ID (optional)</label>
495
+ <input type="text" id="oe-cid" placeholder="STRAT_01">
496
+ </div>
497
+ <button class="btn btn-buy" id="submit-btn" onclick="submitOrder()">▲ PLACE BUY ORDER</button>
498
+ </div>
499
+
500
+ <div class="feed-control">
501
+ <button class="btn btn-neutral" onclick="feedStep(10)">▶ +10 TICKS</button>
502
+ <button class="btn btn-neutral" onclick="feedStep(100)">▶▶ +100</button>
503
+ <button class="btn btn-accent" onclick="autoFeed()" id="auto-btn">AUTO</button>
504
+ </div>
505
+ </div>
506
+
507
+ <!-- COL 2, ROW 1: Price Chart -->
508
+ <div class="panel" style="grid-column:2;grid-row:1">
509
+ <div class="panel-title">
510
+ Price Feed — Real-time Tick
511
+ <div style="display:flex;gap:10px;align-items:center">
512
+ <span id="chart-symbol" class="tag">AAPL</span>
513
+ <span id="chart-price" style="font-size:13px;font-weight:700;color:var(--accent)">—</span>
514
+ </div>
515
+ </div>
516
+ <canvas id="price-canvas" height="290"></canvas>
517
+ </div>
518
+
519
+ <!-- COL 2, ROW 2: Latency / Throughput -->
520
+ <div class="panel" style="grid-column:2;grid-row:2">
521
+ <div class="panel-title">Latency Profiler <span class="tag">C++ ENGINE</span></div>
522
+ <div class="panel-body" style="display:grid;grid-template-columns:1fr 1fr;gap:16px">
523
+ <div>
524
+ <div style="font-size:9px;color:var(--dim);text-transform:uppercase;letter-spacing:1px;margin-bottom:8px">Order Latency</div>
525
+ <div class="lat-bar-wrap">
526
+ <div class="lat-bar-label"><span style="color:var(--dim)">p50</span><span id="lat-o-p50" style="color:var(--green)">—</span></div>
527
+ <div class="lat-bar"><div class="lat-bar-fill" id="lat-o-p50-bar" style="background:var(--green);width:0%"></div></div>
528
+ </div>
529
+ <div class="lat-bar-wrap">
530
+ <div class="lat-bar-label"><span style="color:var(--dim)">p99</span><span id="lat-o-p99" style="color:var(--yellow)">—</span></div>
531
+ <div class="lat-bar"><div class="lat-bar-fill" id="lat-o-p99-bar" style="background:var(--yellow);width:0%"></div></div>
532
+ </div>
533
+ <div class="lat-bar-wrap">
534
+ <div class="lat-bar-label"><span style="color:var(--dim)">avg</span><span id="lat-o-avg" style="color:var(--blue)">—</span></div>
535
+ <div class="lat-bar"><div class="lat-bar-fill" id="lat-o-avg-bar" style="background:var(--blue);width:0%"></div></div>
536
+ </div>
537
+ <div class="divider"></div>
538
+ <div class="stat-row"><span class="stat-key">Checked</span><span id="risk-checked" class="stat-val cyan">0</span></div>
539
+ <div class="stat-row"><span class="stat-key">Rejected</span><span id="risk-rejected" class="stat-val red">0</span></div>
540
+ <div class="stat-row"><span class="stat-key">Fill Rate</span><span id="fill-rate" class="stat-val green">0%</span></div>
541
+ </div>
542
+ <div>
543
+ <div style="font-size:9px;color:var(--dim);text-transform:uppercase;letter-spacing:1px;margin-bottom:8px">Tick Latency</div>
544
+ <div class="lat-bar-wrap">
545
+ <div class="lat-bar-label"><span style="color:var(--dim)">p50</span><span id="lat-t-p50" style="color:var(--green)">—</span></div>
546
+ <div class="lat-bar"><div class="lat-bar-fill" id="lat-t-p50-bar" style="background:var(--green);width:0%"></div></div>
547
+ </div>
548
+ <div class="lat-bar-wrap">
549
+ <div class="lat-bar-label"><span style="color:var(--dim)">p99</span><span id="lat-t-p99" style="color:var(--yellow)">—</span></div>
550
+ <div class="lat-bar"><div class="lat-bar-fill" id="lat-t-p99-bar" style="background:var(--yellow);width:0%"></div></div>
551
+ </div>
552
+ <div class="divider"></div>
553
+ <div class="stat-row"><span class="stat-key">Feed Size</span><span id="feed-size" class="stat-val">—</span></div>
554
+ <div class="stat-row"><span class="stat-key">Feed Index</span><span id="feed-idx" class="stat-val">—</span></div>
555
+ <div class="stat-row"><span class="stat-key">Total P&L</span><span id="stat-pnl" class="stat-val">—</span></div>
556
+ </div>
557
+ </div>
558
+ </div>
559
+
560
+ <!-- COL 2, ROW 3: Trade Log -->
561
+ <div class="panel" style="grid-column:2;grid-row:3">
562
+ <div class="panel-title">
563
+ Trade Log
564
+ <div style="display:flex;gap:8px">
565
+ <span id="trade-count" class="tag">0 trades</span>
566
+ <button class="btn btn-neutral" style="padding:2px 8px;font-size:8px;margin:0;width:auto" onclick="resetSim()">RESET</button>
567
+ </div>
568
+ </div>
569
+ <div class="scroll-area" style="flex:1" id="trade-log"></div>
570
+ </div>
571
+
572
+ <!-- COL 3: Positions + Backtest + Risk -->
573
+ <div class="panel" style="grid-column:3;grid-row:1/span 3;display:flex;flex-direction:column">
574
+
575
+ <div class="mode-tabs">
576
+ <button class="mode-tab active" onclick="setRightMode('positions')">POSITIONS</button>
577
+ <button class="mode-tab" onclick="setRightMode('backtest')">BACKTEST</button>
578
+ <button class="mode-tab" onclick="setRightMode('risk')">RISK</button>
579
+ </div>
580
+
581
+ <!-- POSITIONS VIEW -->
582
+ <div id="view-positions" style="flex:1;overflow:hidden;display:flex;flex-direction:column">
583
+ <div class="panel-title">Live Positions</div>
584
+ <div class="panel-body" style="overflow:auto">
585
+ <table class="pos-table" id="pos-table">
586
+ <thead><tr>
587
+ <th>SYM</th><th>POS</th><th>AVG PX</th><th>RPNL</th><th>UPNL</th>
588
+ </tr></thead>
589
+ <tbody id="pos-body"></tbody>
590
+ </table>
591
+ <div class="divider"></div>
592
+ <div style="font-size:9px;color:var(--dim);text-transform:uppercase;letter-spacing:1px;margin-bottom:6px">Recent Orders</div>
593
+ <div id="order-log" style="max-height:200px;overflow-y:auto"></div>
594
+ </div>
595
+ </div>
596
+
597
+ <!-- BACKTEST VIEW -->
598
+ <div id="view-backtest" style="flex:1;overflow:hidden;display:flex;flex-direction:column;display:none">
599
+ <div class="panel-title">Backtest Engine <span class="tag">DETERMINISTIC</span></div>
600
+ <div class="panel-body" style="overflow:auto">
601
+ <div class="form-group">
602
+ <label>Strategy</label>
603
+ <select id="bt-strategy">
604
+ <option value="MarketMaking">Market Making (MM)</option>
605
+ <option value="Momentum">Momentum (EMA Cross)</option>
606
+ <option value="MeanReversion">Mean Reversion (Z-Score)</option>
607
+ </select>
608
+ </div>
609
+ <div class="form-group">
610
+ <label>Symbol</label>
611
+ <select id="bt-symbol">
612
+ <option>AAPL</option><option>GOOG</option><option>MSFT</option>
613
+ <option>TSLA</option><option>NVDA</option>
614
+ </select>
615
+ </div>
616
+ <div class="form-group">
617
+ <label>Ticks to Replay</label>
618
+ <input type="number" id="bt-ticks" value="5000" min="200" max="50000">
619
+ </div>
620
+ <div class="form-group">
621
+ <label>Start Price</label>
622
+ <input type="number" id="bt-price" value="185.00" step="0.01">
623
+ </div>
624
+ <button class="btn btn-accent" onclick="runBacktest()" id="bt-btn">▶ RUN BACKTEST</button>
625
+
626
+ <div id="bt-result" style="display:none" class="bt-result">
627
+ <div style="font-size:9px;color:var(--dim);letter-spacing:1px;margin-bottom:8px">BACKTEST RESULTS</div>
628
+ <div class="bt-metric"><span class="stat-key">Strategy</span><span id="bt-r-strategy" class="stat-val cyan"></span></div>
629
+ <div class="bt-metric"><span class="stat-key">Ticks</span><span id="bt-r-ticks" class="stat-val"></span></div>
630
+ <div class="bt-metric"><span class="stat-key">Orders</span><span id="bt-r-orders" class="stat-val"></span></div>
631
+ <div class="bt-metric"><span class="stat-key">Fills</span><span id="bt-r-fills" class="stat-val green"></span></div>
632
+ <div class="bt-metric"><span class="stat-key">Rejected</span><span id="bt-r-rej" class="stat-val red"></span></div>
633
+ <div class="bt-metric"><span class="stat-key">Trades</span><span id="bt-r-trades" class="stat-val"></span></div>
634
+ <div class="bt-metric"><span class="stat-key">Total P&L</span><span id="bt-r-pnl" class="stat-val"></span></div>
635
+ <div class="bt-metric"><span class="stat-key">Max Drawdown</span><span id="bt-r-dd" class="stat-val red"></span></div>
636
+ <div class="bt-metric"><span class="stat-key">Sharpe</span><span id="bt-r-sharpe" class="stat-val yellow"></span></div>
637
+ <div class="bt-metric"><span class="stat-key">Order p50</span><span id="bt-r-p50" class="stat-val green"></span></div>
638
+ <div class="bt-metric"><span class="stat-key">Order p99</span><span id="bt-r-p99" class="stat-val yellow"></span></div>
639
+ <div class="bt-metric"><span class="stat-key">Throughput</span><span id="bt-r-thru" class="stat-val blue"></span></div>
640
+ <canvas id="equity-canvas" height="100" style="margin-top:10px;width:100%;border:1px solid var(--border)"></canvas>
641
+ </div>
642
+ </div>
643
+ </div>
644
+
645
+ <!-- RISK VIEW -->
646
+ <div id="view-risk" style="flex:1;overflow:hidden;display:flex;flex-direction:column;display:none">
647
+ <div class="panel-title">Pre-Trade Risk <span class="tag">LIVE</span></div>
648
+ <div class="panel-body" style="overflow:auto">
649
+ <div class="form-group">
650
+ <label>Max Order Qty</label>
651
+ <input type="number" id="r-max-qty" value="10000">
652
+ </div>
653
+ <div class="form-group">
654
+ <label>Max Position</label>
655
+ <input type="number" id="r-max-pos" value="500000">
656
+ </div>
657
+ <div class="form-group">
658
+ <label>Max Notional ($)</label>
659
+ <input type="number" id="r-max-notional" value="50000000">
660
+ </div>
661
+ <div class="form-group">
662
+ <label>Max Orders/sec</label>
663
+ <input type="number" id="r-max-rate" value="2000">
664
+ </div>
665
+ <button class="btn btn-accent" onclick="setRiskLimits()">UPDATE LIMITS</button>
666
+
667
+ <div class="divider"></div>
668
+ <div style="font-size:9px;color:var(--dim);text-transform:uppercase;letter-spacing:1px;margin-bottom:8px">Risk Utilization</div>
669
+ <div class="lat-bar-wrap">
670
+ <div class="lat-bar-label"><span style="color:var(--dim)">Order Rate</span><span id="r-rate-pct" style="color:var(--yellow)">0%</span></div>
671
+ <div class="lat-bar"><div class="lat-bar-fill" id="r-rate-bar" style="width:0%;background:var(--yellow)"></div></div>
672
+ </div>
673
+ <div class="lat-bar-wrap">
674
+ <div class="lat-bar-label"><span style="color:var(--dim)">Reject Rate</span><span id="r-rej-pct" style="color:var(--red)">0%</span></div>
675
+ <div class="lat-bar"><div class="lat-bar-fill" id="r-rej-bar" style="width:0%;background:var(--red)"></div></div>
676
+ </div>
677
+ <div class="divider"></div>
678
+ <div class="stat-row"><span class="stat-key">Checked</span><span id="r-checked" class="stat-val">0</span></div>
679
+ <div class="stat-row"><span class="stat-key">Approved</span><span id="r-approved" class="stat-val green">0</span></div>
680
+ <div class="stat-row"><span class="stat-key">Rejected</span><span id="r-rejected2" class="stat-val red">0</span></div>
681
+
682
+ <div class="divider"></div>
683
+ <div style="font-size:9px;color:var(--dim);text-transform:uppercase;letter-spacing:1px;margin-bottom:8px">Risk Check Types</div>
684
+ <div style="font-size:10px;color:var(--text);line-height:1.8">
685
+ ✓ Max Order Size<br>
686
+ ✓ Max Position Limit<br>
687
+ ✓ Max Notional Exposure<br>
688
+ ✓ Max Order Rate (1s window)<br>
689
+ ✓ Invalid Price/Qty<br>
690
+ ✓ Symbol Validation
691
+ </div>
692
+ </div>
693
+ </div>
694
+
695
+ </div>
696
+ </div>
697
+
698
+ <script>
699
+ // ── State ─────────────────────────────────────────────────────────────────────
700
+ const API = '';
701
+ let selectedSymbol = 'AAPL';
702
+ let orderSide = 'BUY';
703
+ let autoFeedInterval = null;
704
+ let priceHistory = {}; // symbol -> [{ts, price}]
705
+ let rightMode = 'positions';
706
+
707
+ // Price chart
708
+ const priceCtx = document.getElementById('price-canvas');
709
+ let priceChart = null;
710
+
711
+ // ── Init ──────────────────────────────────────────────────────────────────────
712
+ window.addEventListener('DOMContentLoaded', () => {
713
+ buildSymbolStrip();
714
+ selectSymbol('AAPL');
715
+ startPolling();
716
+ feedStep(50); // warm up
717
+ });
718
+
719
+ function buildSymbolStrip() {
720
+ const syms = ['AAPL','GOOG','MSFT','TSLA','NVDA'];
721
+ const strip = document.getElementById('sym-strip');
722
+ strip.innerHTML = syms.map(s =>
723
+ `<button class="sym-btn ${s===selectedSymbol?'active':''}" onclick="selectSymbol('${s}')">${s}</button>`
724
+ ).join('');
725
+ }
726
+
727
+ function selectSymbol(sym) {
728
+ selectedSymbol = sym;
729
+ document.getElementById('oe-symbol').value = sym;
730
+ document.getElementById('book-symbol-tag').textContent = sym;
731
+ document.getElementById('chart-symbol').textContent = sym;
732
+ buildSymbolStrip();
733
+ refreshBook();
734
+ refreshChart();
735
+ }
736
+
737
+ // ── API calls ─────────────────────────────────────────────────────────────────
738
+ async function api(method, path, body=null) {
739
+ const opts = { method, headers: {'Content-Type':'application/json'} };
740
+ if (body) opts.body = JSON.stringify(body);
741
+ const r = await fetch(API + path, opts);
742
+ return r.json();
743
+ }
744
+
745
+ // ── Polling ───────────────────────────────────────────────────────────────────
746
+ function startPolling() {
747
+ refreshStatus();
748
+ refreshBook();
749
+ refreshPositions();
750
+ refreshTrades();
751
+ setInterval(refreshStatus, 1500);
752
+ setInterval(refreshBook, 500);
753
+ setInterval(refreshPositions, 2000);
754
+ setInterval(refreshTrades, 1000);
755
+ }
756
+
757
+ async function refreshStatus() {
758
+ const d = await api('GET', '/api/status');
759
+ if (d.error) return;
760
+
761
+ setVal('h-orders', d.total_orders);
762
+ setVal('h-trades', d.total_trades);
763
+ setVal('h-rejects', d.total_rejects);
764
+
765
+ const pnl = d.total_pnl || 0;
766
+ const pnlEl = document.getElementById('h-pnl');
767
+ pnlEl.textContent = '$' + pnl.toFixed(2);
768
+ pnlEl.className = 'hstat-val ' + (pnl >= 0 ? 'green' : 'red');
769
+
770
+ const p99 = d.order_p99_us || 0;
771
+ document.getElementById('h-p99').textContent = p99 > 0 ? p99.toFixed(1)+'µs' : '—';
772
+
773
+ // Latency bars (relative to 1000µs max)
774
+ setLatBar('lat-o-p50', 'lat-o-p50-bar', d.order_p50_us, 500);
775
+ setLatBar('lat-o-p99', 'lat-o-p99-bar', d.order_p99_us, 500);
776
+ setLatBar('lat-t-p50', 'lat-t-p50-bar', d.tick_p50_us, 100);
777
+ setLatBar('lat-t-p99', 'lat-t-p99-bar', d.tick_p99_us, 100);
778
+
779
+ setVal('risk-checked', d.risk_checked);
780
+ setVal('risk-rejected', d.risk_rejected);
781
+ setVal('r-checked', d.risk_checked);
782
+ setVal('r-rejected2', d.risk_rejected);
783
+ setVal('r-approved', Math.max(0, d.risk_checked - d.risk_rejected));
784
+
785
+ const fillRate = d.total_orders > 0
786
+ ? ((d.total_trades / d.total_orders) * 100).toFixed(1)+'%'
787
+ : '0%';
788
+ document.getElementById('fill-rate').textContent = fillRate;
789
+
790
+ setVal('feed-size', d.feed_size);
791
+ setVal('feed-idx', d.feed_idx);
792
+
793
+ const pnlEl2 = document.getElementById('stat-pnl');
794
+ pnlEl2.textContent = '$' + pnl.toFixed(2);
795
+ pnlEl2.className = 'stat-val ' + (pnl >= 0 ? 'green' : 'red');
796
+
797
+ // Reject rate bar
798
+ if (d.risk_checked > 0) {
799
+ const rejPct = (d.risk_rejected / d.risk_checked * 100);
800
+ setBarWidth('r-rej-bar', rejPct);
801
+ document.getElementById('r-rej-pct').textContent = rejPct.toFixed(1)+'%';
802
+ }
803
+
804
+ // Connection indicator
805
+ document.getElementById('conn-dot').style.background = 'var(--green)';
806
+ }
807
+
808
+ function setLatBar(valId, barId, val, maxVal) {
809
+ const el = document.getElementById(valId);
810
+ const bar = document.getElementById(barId);
811
+ if (!el || !bar) return;
812
+ if (val === undefined || val === null) return;
813
+ el.textContent = val > 0 ? val.toFixed(2)+'µs' : '< 1µs';
814
+ const pct = Math.min(100, (val / maxVal) * 100);
815
+ bar.style.width = pct + '%';
816
+ }
817
+
818
+ function setBarWidth(id, pct) {
819
+ const el = document.getElementById(id);
820
+ if (el) el.style.width = Math.min(100, pct) + '%';
821
+ }
822
+
823
+ function setVal(id, val) {
824
+ const el = document.getElementById(id);
825
+ if (el) el.textContent = val;
826
+ }
827
+
828
+ // ── Order Book ────────────────────────────────────────────────────────────────
829
+ async function refreshBook() {
830
+ const d = await api('GET', `/api/book/${selectedSymbol}`);
831
+ if (d.error || !d.asks) return;
832
+ renderBook(d);
833
+ }
834
+
835
+ function renderBook(d) {
836
+ const maxQty = Math.max(
837
+ ...(d.bids||[]).map(b=>b.qty),
838
+ ...(d.asks||[]).map(a=>a.qty), 1
839
+ );
840
+
841
+ // Asks: show in ascending order but display reversed (best ask at bottom)
842
+ const asks = (d.asks||[]).slice(0,10);
843
+ const asksEl = document.getElementById('book-asks');
844
+ asksEl.innerHTML = [...asks].reverse().map(a => {
845
+ const pct = (a.qty / maxQty * 100).toFixed(1);
846
+ return `<div class="book-row ask-row">
847
+ <div class="book-bar" style="width:${pct}%"></div>
848
+ <span class="price">${a.price.toFixed(4)}</span>
849
+ <span class="qty-val">${fmtNum(a.qty)}</span>
850
+ <span class="orders-val">${a.orders}</span>
851
+ </div>`;
852
+ }).join('');
853
+
854
+ const spreadEl = document.getElementById('spread-row');
855
+ if (d.spread > 0) {
856
+ spreadEl.textContent = `BID ${d.best_bid.toFixed(4)} ─── SPREAD ${d.spread.toFixed(4)} ─── ASK ${d.best_ask.toFixed(4)}`;
857
+ }
858
+
859
+ // Update chart price
860
+ const mid = d.mid || d.best_bid;
861
+ if (mid > 0) {
862
+ document.getElementById('chart-price').textContent = '$' + mid.toFixed(2);
863
+ }
864
+
865
+ const bids = (d.bids||[]).slice(0,10);
866
+ const bidsEl = document.getElementById('book-bids');
867
+ bidsEl.innerHTML = bids.map(b => {
868
+ const pct = (b.qty / maxQty * 100).toFixed(1);
869
+ return `<div class="book-row bid-row">
870
+ <div class="book-bar" style="width:${pct}%"></div>
871
+ <span class="price">${b.price.toFixed(4)}</span>
872
+ <span class="qty-val">${fmtNum(b.qty)}</span>
873
+ <span class="orders-val">${b.orders}</span>
874
+ </div>`;
875
+ }).join('');
876
+ }
877
+
878
+ // ── Price Chart ───────────────────────────────────────────────────────────────
879
+ const MAX_HISTORY = 300;
880
+
881
+ async function refreshChart() {
882
+ // Just use feed data from status — we'll seed from order book mid
883
+ }
884
+
885
+ function addPricePoint(sym, price, ts) {
886
+ if (!priceHistory[sym]) priceHistory[sym] = [];
887
+ priceHistory[sym].push({price, ts});
888
+ if (priceHistory[sym].length > MAX_HISTORY)
889
+ priceHistory[sym].shift();
890
+ if (sym === selectedSymbol) drawPriceChart();
891
+ }
892
+
893
+ function drawPriceChart() {
894
+ const canvas = document.getElementById('price-canvas');
895
+ const ctx = canvas.getContext('2d');
896
+ const W = canvas.parentElement.offsetWidth;
897
+ const H = 290;
898
+ canvas.width = W; canvas.height = H;
899
+
900
+ ctx.fillStyle = '#0d1219';
901
+ ctx.fillRect(0, 0, W, H);
902
+
903
+ const data = priceHistory[selectedSymbol] || [];
904
+ if (data.length < 2) {
905
+ ctx.fillStyle = '#1e2d3d';
906
+ ctx.font = '11px JetBrains Mono';
907
+ ctx.textAlign = 'center';
908
+ ctx.fillText('Feed ticks to populate chart', W/2, H/2);
909
+ return;
910
+ }
911
+
912
+ const prices = data.map(d => d.price);
913
+ const minP = Math.min(...prices);
914
+ const maxP = Math.max(...prices);
915
+ const range = maxP - minP || 1;
916
+ const pad = { t:20, r:60, b:30, l:10 };
917
+ const cW = W - pad.l - pad.r;
918
+ const cH = H - pad.t - pad.b;
919
+
920
+ const xOf = i => pad.l + (i / (data.length-1)) * cW;
921
+ const yOf = p => pad.t + (1 - (p - minP) / range) * cH;
922
+
923
+ // Grid lines
924
+ ctx.strokeStyle = '#1a2230';
925
+ ctx.lineWidth = 1;
926
+ for (let i = 0; i <= 4; i++) {
927
+ const y = pad.t + (i / 4) * cH;
928
+ ctx.beginPath(); ctx.moveTo(pad.l, y); ctx.lineTo(W - pad.r, y); ctx.stroke();
929
+ const price = maxP - (i / 4) * range;
930
+ ctx.fillStyle = '#5a7a90'; ctx.font = '9px JetBrains Mono';
931
+ ctx.textAlign = 'right';
932
+ ctx.fillText('$' + price.toFixed(2), W - 5, y + 3);
933
+ }
934
+
935
+ // Gradient fill
936
+ const grad = ctx.createLinearGradient(0, pad.t, 0, H - pad.b);
937
+ const last = data[data.length - 1];
938
+ const first = data[0];
939
+ const isUp = last.price >= first.price;
940
+ if (isUp) {
941
+ grad.addColorStop(0, 'rgba(0,230,118,0.2)');
942
+ grad.addColorStop(1, 'rgba(0,230,118,0.01)');
943
+ } else {
944
+ grad.addColorStop(0, 'rgba(255,23,68,0.2)');
945
+ grad.addColorStop(1, 'rgba(255,23,68,0.01)');
946
+ }
947
+
948
+ ctx.beginPath();
949
+ ctx.moveTo(xOf(0), yOf(data[0].price));
950
+ for (let i = 1; i < data.length; i++) {
951
+ const x1 = xOf(i-1), x2 = xOf(i);
952
+ const y1 = yOf(data[i-1].price), y2 = yOf(data[i].price);
953
+ const cpX = (x1 + x2) / 2;
954
+ ctx.bezierCurveTo(cpX, y1, cpX, y2, x2, y2);
955
+ }
956
+ ctx.lineTo(xOf(data.length-1), H - pad.b);
957
+ ctx.lineTo(xOf(0), H - pad.b);
958
+ ctx.closePath();
959
+ ctx.fillStyle = grad;
960
+ ctx.fill();
961
+
962
+ // Line
963
+ ctx.beginPath();
964
+ ctx.moveTo(xOf(0), yOf(data[0].price));
965
+ for (let i = 1; i < data.length; i++) {
966
+ const x1 = xOf(i-1), x2 = xOf(i);
967
+ const y1 = yOf(data[i-1].price), y2 = yOf(data[i].price);
968
+ const cpX = (x1 + x2) / 2;
969
+ ctx.bezierCurveTo(cpX, y1, cpX, y2, x2, y2);
970
+ }
971
+ ctx.strokeStyle = isUp ? '#00e676' : '#ff1744';
972
+ ctx.lineWidth = 2;
973
+ ctx.shadowColor = isUp ? '#00e676' : '#ff1744';
974
+ ctx.shadowBlur = 6;
975
+ ctx.stroke();
976
+ ctx.shadowBlur = 0;
977
+
978
+ // Last price dot
979
+ const lx = xOf(data.length - 1);
980
+ const ly = yOf(last.price);
981
+ ctx.beginPath(); ctx.arc(lx, ly, 4, 0, Math.PI*2);
982
+ ctx.fillStyle = isUp ? '#00e676' : '#ff1744';
983
+ ctx.fill();
984
+
985
+ // OHLC mini labels
986
+ ctx.fillStyle = '#5a7a90'; ctx.font = '9px JetBrains Mono';
987
+ ctx.textAlign = 'left';
988
+ ctx.fillText(`↑ ${maxP.toFixed(2)}`, pad.l + 5, pad.t + 12);
989
+ ctx.fillText(`↓ ${minP.toFixed(2)}`, pad.l + 5, H - pad.b - 5);
990
+ }
991
+
992
+ // ── Feed ──────────────────────────────────────────────────────────────────────
993
+ async function feedStep(n) {
994
+ const ticks = await api('POST', '/api/feed/step', {n});
995
+ if (!Array.isArray(ticks)) return;
996
+ ticks.forEach(t => {
997
+ addPricePoint(t.symbol, t.last, t.ts);
998
+ if (t.symbol === selectedSymbol) {
999
+ document.getElementById('chart-price').textContent = '$' + t.last.toFixed(2);
1000
+ // Pre-fill order price
1001
+ const priceInput = document.getElementById('oe-price');
1002
+ if (!priceInput.value || priceInput.value == '0') {
1003
+ priceInput.value = t.bid.toFixed(2);
1004
+ }
1005
+ }
1006
+ });
1007
+ refreshBook();
1008
+ }
1009
+
1010
+ let autoRunning = false;
1011
+ function autoFeed() {
1012
+ const btn = document.getElementById('auto-btn');
1013
+ if (autoRunning) {
1014
+ clearInterval(autoFeedInterval);
1015
+ autoRunning = false;
1016
+ btn.textContent = 'AUTO';
1017
+ btn.classList.remove('btn-sell');
1018
+ btn.classList.add('btn-accent');
1019
+ } else {
1020
+ autoRunning = true;
1021
+ btn.textContent = '■ STOP';
1022
+ btn.classList.remove('btn-accent');
1023
+ btn.classList.add('btn-sell');
1024
+ autoFeedInterval = setInterval(() => feedStep(5), 200);
1025
+ }
1026
+ }
1027
+
1028
+ // ── Order Entry ───────────────────────────────────────────────────────────────
1029
+ function setOrderSide(side) {
1030
+ orderSide = side;
1031
+ const buyTab = document.getElementById('tab-buy');
1032
+ const sellTab = document.getElementById('tab-sell');
1033
+ const btn = document.getElementById('submit-btn');
1034
+ if (side === 'BUY') {
1035
+ buyTab.className = 'tab active-buy';
1036
+ sellTab.className = 'tab';
1037
+ btn.className = 'btn btn-buy';
1038
+ btn.textContent = '▲ PLACE BUY ORDER';
1039
+ } else {
1040
+ buyTab.className = 'tab';
1041
+ sellTab.className = 'tab active-sell';
1042
+ btn.className = 'btn btn-sell';
1043
+ btn.textContent = '▼ PLACE SELL ORDER';
1044
+ }
1045
+ }
1046
+
1047
+ function updateOrderForm() {
1048
+ const type = document.getElementById('oe-type').value;
1049
+ const priceGroup = document.getElementById('oe-price-group');
1050
+ priceGroup.style.display = (type === 'MARKET') ? 'none' : 'block';
1051
+ }
1052
+
1053
+ async function submitOrder() {
1054
+ const sym = document.getElementById('oe-symbol').value;
1055
+ const type = document.getElementById('oe-type').value;
1056
+ const price = parseFloat(document.getElementById('oe-price').value) || 0;
1057
+ const qty = parseInt(document.getElementById('oe-qty').value) || 100;
1058
+ const cid = document.getElementById('oe-cid').value;
1059
+
1060
+ const body = {
1061
+ symbol: sym, side: orderSide, type,
1062
+ price, qty, client_id: cid,
1063
+ market_price: price
1064
+ };
1065
+
1066
+ const r = await api('POST', '/api/order', body);
1067
+ if (!r) return;
1068
+
1069
+ if (r.approved) {
1070
+ showToast(`✓ ${orderSide} ${qty} ${sym} @ ${type === 'MARKET' ? 'MKT' : '$'+price} — ${r.order.status}`, 'green');
1071
+ if (r.trades && r.trades.length > 0) {
1072
+ showToast(`⚡ ${r.trades.length} trade(s) executed!`, 'green');
1073
+ }
1074
+ } else {
1075
+ showToast(`✗ REJECTED: ${r.reject_reason}`, 'red');
1076
+ document.body.classList.add('flash-red');
1077
+ setTimeout(() => document.body.classList.remove('flash-red'), 500);
1078
+ }
1079
+
1080
+ refreshBook();
1081
+ refreshPositions();
1082
+ refreshTrades();
1083
+ }
1084
+
1085
+ // ── Positions ─────────────────────────────────────────────────────────────────
1086
+ async function refreshPositions() {
1087
+ const pos = await api('GET', '/api/positions');
1088
+ if (!Array.isArray(pos)) return;
1089
+
1090
+ const tbody = document.getElementById('pos-body');
1091
+ tbody.innerHTML = pos.filter(p => p.net_qty !== 0).map(p => {
1092
+ const rPnl = p.realized_pnl || 0;
1093
+ const uPnl = p.unrealized_pnl || 0;
1094
+ const posDir = p.net_qty > 0 ? '+' : '';
1095
+ return `<tr>
1096
+ <td style="color:var(--cyan);font-weight:700">${p.symbol}</td>
1097
+ <td style="color:${p.net_qty>0?'var(--green)':'var(--red)'}">${posDir}${fmtNum(p.net_qty)}</td>
1098
+ <td>$${p.avg_price.toFixed(2)}</td>
1099
+ <td style="color:${rPnl>=0?'var(--green)':'var(--red)'}">${rPnl>=0?'+':''}$${rPnl.toFixed(2)}</td>
1100
+ <td style="color:${uPnl>=0?'var(--green)':'var(--red)'}">${uPnl>=0?'+':''}$${uPnl.toFixed(2)}</td>
1101
+ </tr>`;
1102
+ }).join('') || '<tr><td colspan="5" style="text-align:center;color:var(--dim);padding:10px">No open positions</td></tr>';
1103
+
1104
+ // Order log
1105
+ const orders = await api('GET', '/api/orders');
1106
+ if (!Array.isArray(orders)) return;
1107
+ const logEl = document.getElementById('order-log');
1108
+ logEl.innerHTML = orders.slice(-20).reverse().map(o => {
1109
+ const isRej = o.status === 'REJECTED';
1110
+ return `<div class="trade-entry" style="${isRej?'border-left:2px solid var(--red)':''}">
1111
+ <span class="trade-side ${o.side==='BUY'?'buy-badge':'sell-badge'}">${o.side}</span>
1112
+ <span style="color:var(--cyan)">${o.symbol}</span>
1113
+ <span style="color:var(--dim)">${o.type}</span>
1114
+ <span>$${(o.price||0).toFixed(2)}</span>
1115
+ <span style="color:var(--dim)">×${fmtNum(o.qty)}</span>
1116
+ <span style="color:${isRej?'var(--red)':o.status==='FILLED'?'var(--green)':'var(--dim)'}">
1117
+ ${o.status}
1118
+ </span>
1119
+ </div>`;
1120
+ }).join('');
1121
+ }
1122
+
1123
+ // ── Trades ────────────────────────────────────────────────────────────────────
1124
+ async function refreshTrades() {
1125
+ const trades = await api('GET', '/api/trades');
1126
+ if (!Array.isArray(trades)) return;
1127
+
1128
+ document.getElementById('trade-count').textContent = `${trades.length} trades`;
1129
+ document.getElementById('h-trades').textContent = trades.length;
1130
+
1131
+ const logEl = document.getElementById('trade-log');
1132
+ logEl.innerHTML = trades.slice().reverse().slice(0, 100).map(t => {
1133
+ const time = new Date(t.timestamp / 1e6).toISOString().substr(11,12);
1134
+ return `<div class="trade-entry" style="padding:5px 12px">
1135
+ <span style="color:var(--dim);font-size:9px">${time}</span>
1136
+ <span style="color:var(--cyan);font-weight:700">${t.symbol}</span>
1137
+ <span style="color:var(--text)">$${t.price.toFixed(4)}</span>
1138
+ <span style="color:var(--dim)">×</span>
1139
+ <span style="font-weight:600">${fmtNum(t.qty)}</span>
1140
+ <span style="color:var(--dim);font-size:9px">#${t.trade_id}</span>
1141
+ </div>`;
1142
+ }).join('');
1143
+ }
1144
+
1145
+ // ── Backtest ──────────────────────────────────────────────────────────────────
1146
+ async function runBacktest() {
1147
+ const btn = document.getElementById('bt-btn');
1148
+ btn.textContent = '⟳ RUNNING...'; btn.disabled = true;
1149
+
1150
+ try {
1151
+ const r = await api('POST', '/api/backtest', {
1152
+ strategy: document.getElementById('bt-strategy').value,
1153
+ symbol: document.getElementById('bt-symbol').value,
1154
+ ticks: parseInt(document.getElementById('bt-ticks').value),
1155
+ start_price: parseFloat(document.getElementById('bt-price').value)
1156
+ });
1157
+
1158
+ document.getElementById('bt-result').style.display = 'block';
1159
+ setVal('bt-r-strategy', r.strategy);
1160
+ setVal('bt-r-ticks', fmtNum(r.ticks_processed));
1161
+ setVal('bt-r-orders', fmtNum(r.orders_submitted));
1162
+ setVal('bt-r-fills', fmtNum(r.orders_filled));
1163
+ setVal('bt-r-rej', fmtNum(r.orders_rejected));
1164
+ setVal('bt-r-trades', fmtNum(r.trades_count));
1165
+
1166
+ const pnlEl = document.getElementById('bt-r-pnl');
1167
+ pnlEl.textContent = (r.total_pnl >= 0 ? '+' : '') + '$' + r.total_pnl.toFixed(2);
1168
+ pnlEl.className = 'stat-val ' + (r.total_pnl >= 0 ? 'green' : 'red');
1169
+
1170
+ document.getElementById('bt-r-dd').textContent = '-$' + r.max_drawdown.toFixed(2);
1171
+ document.getElementById('bt-r-sharpe').textContent = r.sharpe.toFixed(3);
1172
+ document.getElementById('bt-r-p50').textContent = r.order_p50_us.toFixed(2) + 'µs';
1173
+ document.getElementById('bt-r-p99').textContent = r.order_p99_us.toFixed(2) + 'µs';
1174
+ document.getElementById('bt-r-thru').textContent = fmtNum(Math.round(r.throughput_eps)) + ' ev/s';
1175
+
1176
+ drawEquityCurve(r.equity_curve || []);
1177
+ } finally {
1178
+ btn.textContent = '▶ RUN BACKTEST'; btn.disabled = false;
1179
+ }
1180
+ }
1181
+
1182
+ function drawEquityCurve(curve) {
1183
+ const canvas = document.getElementById('equity-canvas');
1184
+ const ctx = canvas.getContext('2d');
1185
+ const W = canvas.parentElement.offsetWidth - 2;
1186
+ const H = 100;
1187
+ canvas.width = W; canvas.height = H;
1188
+
1189
+ ctx.fillStyle = '#0d1219';
1190
+ ctx.fillRect(0,0,W,H);
1191
+
1192
+ if (curve.length < 2) { return; }
1193
+
1194
+ const minV = Math.min(...curve);
1195
+ const maxV = Math.max(...curve);
1196
+ const range = maxV - minV || 1;
1197
+ const pad = 15;
1198
+
1199
+ const xOf = i => pad + (i / (curve.length-1)) * (W - 2*pad);
1200
+ const yOf = v => pad + (1 - (v - minV)/range) * (H - 2*pad);
1201
+
1202
+ // Zero line
1203
+ const zy = yOf(0);
1204
+ if (zy > pad && zy < H-pad) {
1205
+ ctx.strokeStyle = '#1e2d3d';
1206
+ ctx.setLineDash([3,3]);
1207
+ ctx.beginPath(); ctx.moveTo(pad, zy); ctx.lineTo(W-pad, zy); ctx.stroke();
1208
+ ctx.setLineDash([]);
1209
+ }
1210
+
1211
+ ctx.beginPath();
1212
+ ctx.moveTo(xOf(0), yOf(curve[0]));
1213
+ for (let i = 1; i < curve.length; i++) ctx.lineTo(xOf(i), yOf(curve[i]));
1214
+ ctx.strokeStyle = curve[curve.length-1] >= 0 ? '#00e676' : '#ff1744';
1215
+ ctx.lineWidth = 1.5;
1216
+ ctx.stroke();
1217
+
1218
+ ctx.fillStyle = '#5a7a90'; ctx.font = '8px JetBrains Mono';
1219
+ ctx.textAlign = 'right';
1220
+ ctx.fillText('$'+maxV.toFixed(1), W-2, pad+8);
1221
+ ctx.fillText('$'+minV.toFixed(1), W-2, H-pad);
1222
+ }
1223
+
1224
+ // ── Risk Limits ───────────────────────────────────────────────────────────────
1225
+ async function setRiskLimits() {
1226
+ const r = await api('POST', '/api/risk/limits', {
1227
+ max_order_qty: parseInt(document.getElementById('r-max-qty').value),
1228
+ max_position: parseInt(document.getElementById('r-max-pos').value),
1229
+ max_notional_usd: parseFloat(document.getElementById('r-max-notional').value),
1230
+ max_orders_per_sec: parseInt(document.getElementById('r-max-rate').value),
1231
+ });
1232
+ showToast(r.ok ? '✓ Risk limits updated' : '✗ Failed', r.ok ? 'green' : 'red');
1233
+ }
1234
+
1235
+ // ── Reset ─────────────────────────────────────────────────────────────────────
1236
+ async function resetSim() {
1237
+ await api('POST', '/api/reset');
1238
+ priceHistory = {};
1239
+ document.getElementById('trade-log').innerHTML = '';
1240
+ showToast('↺ Simulator reset', 'cyan');
1241
+ }
1242
+
1243
+ // ── Right mode ────────────────────────────────────────────────────────────────
1244
+ function setRightMode(mode) {
1245
+ rightMode = mode;
1246
+ ['positions','backtest','risk'].forEach(m => {
1247
+ document.getElementById('view-'+m).style.display = (m === mode) ? 'flex' : 'none';
1248
+ });
1249
+ document.querySelectorAll('.mode-tab').forEach((t,i) => {
1250
+ const modes = ['positions','backtest','risk'];
1251
+ t.className = 'mode-tab' + (modes[i]===mode ? ' active' : '');
1252
+ });
1253
+ }
1254
+
1255
+ // ── Helpers ───────────────────────────────────────────────────────────────────
1256
+ function fmtNum(n) {
1257
+ if (n >= 1e6) return (n/1e6).toFixed(1)+'M';
1258
+ if (n >= 1e3) return (n/1e3).toFixed(1)+'K';
1259
+ return n.toString();
1260
+ }
1261
+
1262
+ function showToast(msg, color='cyan') {
1263
+ const t = document.createElement('div');
1264
+ t.className = 'toast';
1265
+ t.style.borderColor = `var(--${color})`;
1266
+ t.style.color = `var(--${color})`;
1267
+ t.textContent = msg;
1268
+ document.body.appendChild(t);
1269
+ setTimeout(() => t.remove(), 3000);
1270
+ }
1271
+
1272
+ // Resize handler for chart
1273
+ window.addEventListener('resize', () => {
1274
+ if (priceHistory[selectedSymbol]) drawPriceChart();
1275
+ });
1276
+ </script>
1277
+ </body>
1278
+ </html>
src/core/order_book.cpp ADDED
@@ -0,0 +1,240 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #include "core/order_book.hpp"
2
+ #include <stdexcept>
3
+ #include <cassert>
4
+
5
+ namespace hft {
6
+
7
+ OrderBook::OrderBook(std::string symbol) : symbol_(std::move(symbol)) {}
8
+
9
+ void OrderBook::notify_order(const Order& o) {
10
+ if (order_cb_) order_cb_(o);
11
+ }
12
+
13
+ Trade OrderBook::make_trade(Order& buy, Order& sell, Price px, Quantity qty) {
14
+ Trade t;
15
+ t.trade_id = next_trade_id_++;
16
+ t.buy_order_id = buy.id;
17
+ t.sell_order_id = sell.id;
18
+ t.symbol = symbol_;
19
+ t.price = px;
20
+ t.qty = qty;
21
+ t.timestamp = now_ns();
22
+
23
+ buy.filled_qty += qty;
24
+ sell.filled_qty += qty;
25
+ buy.status = (buy.remaining() == 0) ? OrderStatus::FILLED : OrderStatus::PARTIALLY_FILLED;
26
+ sell.status = (sell.remaining() == 0) ? OrderStatus::FILLED : OrderStatus::PARTIALLY_FILLED;
27
+ return t;
28
+ }
29
+
30
+ void OrderBook::add_to_book(Order& order) {
31
+ auto& book = (order.side == Side::BUY) ? bids_ : asks_;
32
+ auto& level = book[order.price];
33
+ level.price = order.price;
34
+ level.orders.push_back(&order);
35
+ level.total_qty += order.remaining();
36
+ orders_[order.id] = &order;
37
+ }
38
+
39
+ void OrderBook::remove_from_book(Order& order) {
40
+ auto& book = (order.side == Side::BUY) ? bids_ : asks_;
41
+ auto it = book.find(order.price);
42
+ if (it == book.end()) return;
43
+
44
+ auto& level = it->second;
45
+ Quantity removed = 0;
46
+ level.orders.remove_if([&](Order* o) {
47
+ if (o->id == order.id) {
48
+ removed = o->remaining();
49
+ return true;
50
+ }
51
+ return false;
52
+ });
53
+ level.total_qty -= removed;
54
+ if (level.empty()) book.erase(it);
55
+ orders_.erase(order.id);
56
+ }
57
+
58
+ std::vector<Trade> OrderBook::try_match(Order& incoming, bool fok_check) {
59
+ std::vector<Trade> trades;
60
+
61
+ if (fok_check) {
62
+ Quantity available = 0;
63
+ if (incoming.side == Side::BUY) {
64
+ for (auto& [px, lvl] : asks_) {
65
+ if (incoming.type != OrderType::MARKET && px > incoming.price) break;
66
+ available += lvl.total_qty;
67
+ if (available >= incoming.qty) break;
68
+ }
69
+ } else {
70
+ for (auto rit = bids_.rbegin(); rit != bids_.rend(); ++rit) {
71
+ if (incoming.type != OrderType::MARKET && rit->first < incoming.price) break;
72
+ available += rit->second.total_qty;
73
+ if (available >= incoming.qty) break;
74
+ }
75
+ }
76
+ if (available < incoming.qty) {
77
+ incoming.status = OrderStatus::EXPIRED;
78
+ notify_order(incoming);
79
+ return trades;
80
+ }
81
+ }
82
+
83
+ if (incoming.side == Side::BUY) {
84
+ for (auto it = asks_.begin(); it != asks_.end() && incoming.remaining() > 0; ) {
85
+ auto& [px, level] = *it;
86
+ if (incoming.type == OrderType::LIMIT || incoming.type == OrderType::IOC) {
87
+ if (px > incoming.price) break;
88
+ }
89
+ while (!level.orders.empty() && incoming.remaining() > 0) {
90
+ Order* resting = level.orders.front();
91
+ Quantity fill_qty = std::min(incoming.remaining(), resting->remaining());
92
+ Price fill_px = resting->price;
93
+
94
+ auto trade = make_trade(incoming, *resting, fill_px, fill_qty);
95
+ trades.push_back(trade);
96
+ if (trade_cb_) trade_cb_(trade);
97
+
98
+ level.total_qty -= fill_qty;
99
+ notify_order(*resting);
100
+ notify_order(incoming);
101
+
102
+ if (resting->is_done()) {
103
+ level.orders.pop_front();
104
+ orders_.erase(resting->id);
105
+ }
106
+ }
107
+ if (level.empty()) {
108
+ it = asks_.erase(it);
109
+ } else {
110
+ ++it;
111
+ }
112
+ }
113
+ } else {
114
+ // Sell-side: match against bids (highest first = rbegin)
115
+ // We iterate from the back using a reverse approach but erase safely.
116
+ while (!bids_.empty() && incoming.remaining() > 0) {
117
+ auto it = std::prev(bids_.end()); // best bid = last element
118
+ auto& [px, level] = *it;
119
+ if (incoming.type == OrderType::LIMIT || incoming.type == OrderType::IOC) {
120
+ if (px < incoming.price) break;
121
+ }
122
+ while (!level.orders.empty() && incoming.remaining() > 0) {
123
+ Order* resting = level.orders.front();
124
+ Quantity fill_qty = std::min(incoming.remaining(), resting->remaining());
125
+ Price fill_px = resting->price;
126
+
127
+ auto trade = make_trade(*resting, incoming, fill_px, fill_qty);
128
+ trades.push_back(trade);
129
+ if (trade_cb_) trade_cb_(trade);
130
+
131
+ level.total_qty -= fill_qty;
132
+ notify_order(*resting);
133
+ notify_order(incoming);
134
+
135
+ if (resting->is_done()) {
136
+ level.orders.pop_front();
137
+ orders_.erase(resting->id);
138
+ }
139
+ }
140
+ if (level.empty()) {
141
+ bids_.erase(it);
142
+ } else {
143
+ break; // Remaining qty at this level, stop matching
144
+ }
145
+ }
146
+ }
147
+
148
+ return trades;
149
+ }
150
+
151
+ std::vector<Trade> OrderBook::add_order(Order& order) {
152
+ std::vector<Trade> trades;
153
+ order.status = OrderStatus::NEW;
154
+ notify_order(order);
155
+
156
+ switch (order.type) {
157
+ case OrderType::LIMIT: {
158
+ trades = try_match(order);
159
+ if (!order.is_done()) add_to_book(order);
160
+ break;
161
+ }
162
+ case OrderType::MARKET: {
163
+ trades = try_match(order);
164
+ if (order.remaining() > 0) {
165
+ order.status = OrderStatus::CANCELLED;
166
+ notify_order(order);
167
+ }
168
+ break;
169
+ }
170
+ case OrderType::IOC: {
171
+ trades = try_match(order);
172
+ if (order.remaining() > 0 && !order.is_done()) {
173
+ order.status = OrderStatus::EXPIRED;
174
+ notify_order(order);
175
+ }
176
+ break;
177
+ }
178
+ case OrderType::FOK: {
179
+ trades = try_match(order, true);
180
+ break;
181
+ }
182
+ }
183
+ return trades;
184
+ }
185
+
186
+ bool OrderBook::cancel_order(OrderId id) {
187
+ auto it = orders_.find(id);
188
+ if (it == orders_.end()) return false;
189
+ Order* o = it->second;
190
+ remove_from_book(*o);
191
+ o->status = OrderStatus::CANCELLED;
192
+ notify_order(*o);
193
+ return true;
194
+ }
195
+
196
+ bool OrderBook::modify_order(OrderId id, Price new_price, Quantity new_qty) {
197
+ auto it = orders_.find(id);
198
+ if (it == orders_.end()) return false;
199
+ Order* o = it->second;
200
+ if (new_qty <= o->filled_qty) return false;
201
+ remove_from_book(*o);
202
+ o->price = new_price;
203
+ o->qty = new_qty;
204
+ add_to_book(*o);
205
+ return true;
206
+ }
207
+
208
+ std::optional<Order*> OrderBook::find_order(OrderId id) {
209
+ auto it = orders_.find(id);
210
+ if (it == orders_.end()) return std::nullopt;
211
+ return it->second;
212
+ }
213
+
214
+ OrderBookSnapshot OrderBook::snapshot(int depth) const {
215
+ OrderBookSnapshot snap;
216
+ snap.symbol = symbol_;
217
+ snap.timestamp = now_ns();
218
+
219
+ int cnt = 0;
220
+ for (auto it = bids_.rbegin(); it != bids_.rend() && cnt < depth; ++it, ++cnt) {
221
+ L2Level l;
222
+ l.price = it->first;
223
+ l.qty = it->second.total_qty;
224
+ l.order_count = (int)it->second.orders.size();
225
+ snap.bids.push_back(l);
226
+ }
227
+
228
+ cnt = 0;
229
+ for (auto it = asks_.begin(); it != asks_.end() && cnt < depth; ++it, ++cnt) {
230
+ L2Level l;
231
+ l.price = it->first;
232
+ l.qty = it->second.total_qty;
233
+ l.order_count = (int)it->second.orders.size();
234
+ snap.asks.push_back(l);
235
+ }
236
+
237
+ return snap;
238
+ }
239
+
240
+ } // namespace hft
src/core/order_book.hpp ADDED
@@ -0,0 +1,73 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #pragma once
2
+ #include "types.hpp"
3
+ #include <map>
4
+ #include <unordered_map>
5
+ #include <list>
6
+ #include <functional>
7
+ #include <vector>
8
+ #include <optional>
9
+ #include <algorithm>
10
+
11
+ namespace hft {
12
+
13
+ // Price level: all orders at same price, FIFO queue
14
+ struct PriceLevel {
15
+ Price price;
16
+ std::list<Order*> orders;
17
+ Quantity total_qty = 0;
18
+
19
+ void add(Order* o) {
20
+ orders.push_back(o);
21
+ total_qty += o->remaining();
22
+ }
23
+ bool empty() const { return orders.empty(); }
24
+ };
25
+
26
+ using TradeCallback = std::function<void(const Trade&)>;
27
+ using OrderCallback = std::function<void(const Order&)>;
28
+
29
+ class OrderBook {
30
+ public:
31
+ explicit OrderBook(std::string symbol);
32
+
33
+ // Returns trades generated
34
+ std::vector<Trade> add_order(Order& order);
35
+ bool cancel_order(OrderId id);
36
+ bool modify_order(OrderId id, Price new_price, Quantity new_qty);
37
+
38
+ OrderBookSnapshot snapshot(int depth = 10) const;
39
+ std::optional<Order*> find_order(OrderId id);
40
+
41
+ void set_trade_callback(TradeCallback cb) { trade_cb_ = std::move(cb); }
42
+ void set_order_callback(OrderCallback cb) { order_cb_ = std::move(cb); }
43
+
44
+ size_t order_count() const { return orders_.size(); }
45
+ size_t bid_levels() const { return bids_.size(); }
46
+ size_t ask_levels() const { return asks_.size(); }
47
+ Price best_bid() const { return bids_.empty() ? 0 : bids_.rbegin()->first; }
48
+ Price best_ask() const { return asks_.empty() ? 0 : asks_.begin()->first; }
49
+ const std::string& symbol() const { return symbol_; }
50
+
51
+ private:
52
+ std::string symbol_;
53
+ uint64_t next_trade_id_ = 1;
54
+
55
+ // bids: descending (highest first) → map with reverse
56
+ std::map<Price, PriceLevel> bids_; // sorted asc, iterate rbegin for best
57
+ std::map<Price, PriceLevel> asks_; // sorted asc, begin = best ask
58
+
59
+ std::unordered_map<OrderId, Order*> orders_;
60
+
61
+ TradeCallback trade_cb_;
62
+ OrderCallback order_cb_;
63
+
64
+ std::vector<Trade> match_limit(Order& incoming);
65
+ std::vector<Trade> match_market(Order& incoming);
66
+ std::vector<Trade> try_match(Order& incoming, bool fok_check = false);
67
+ Trade make_trade(Order& buy, Order& sell, Price px, Quantity qty);
68
+ void add_to_book(Order& order);
69
+ void remove_from_book(Order& order);
70
+ void notify_order(const Order& o);
71
+ };
72
+
73
+ } // namespace hft
src/core/simulator.cpp ADDED
@@ -0,0 +1,252 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #include "core/simulator.hpp"
2
+ #include <algorithm>
3
+ #include <numeric>
4
+ #include <cmath>
5
+ #include <unordered_set>
6
+
7
+ namespace hft {
8
+
9
+ ExchangeSimulator::ExchangeSimulator(RiskLimits limits)
10
+ : risk_(limits), journal_("session.journal") {}
11
+
12
+ void ExchangeSimulator::add_symbol(const std::string& symbol) {
13
+ std::lock_guard<std::mutex> lock(mtx_);
14
+ if (books_.count(symbol)) return;
15
+ auto book = std::make_unique<OrderBook>(symbol);
16
+ book->set_trade_callback([this](const Trade& t) {
17
+ total_trades_.fetch_add(1, std::memory_order_relaxed);
18
+ if (trade_cb_) trade_cb_(t);
19
+ });
20
+ book->set_order_callback([this](const Order& o) {
21
+ if (order_cb_) order_cb_(o);
22
+ });
23
+ books_[symbol] = std::move(book);
24
+ }
25
+
26
+ // Fix: Now takes const Order& and safely clones it to guarantee memory persistence
27
+ ExchangeSimulator::SubmitResult ExchangeSimulator::submit_order(const Order& order_in, double market_price) {
28
+ int64_t t0 = now_ns();
29
+ total_orders_.fetch_add(1, std::memory_order_relaxed);
30
+
31
+ auto risk_result = risk_.check_order(order_in, market_price);
32
+ if (!risk_result.approved) {
33
+ total_rejects_.fetch_add(1, std::memory_order_relaxed);
34
+ return {false, risk_result.message, {}};
35
+ }
36
+
37
+ std::lock_guard<std::mutex> lock(mtx_);
38
+ auto it = books_.find(order_in.symbol);
39
+ if (it == books_.end()) {
40
+ total_rejects_.fetch_add(1, std::memory_order_relaxed);
41
+ return {false, "Unknown symbol: " + order_in.symbol, {}};
42
+ }
43
+
44
+ // Fix: Allocate order uniquely so OrderBook holds a stable, valid pointer
45
+ auto order_ptr = std::make_unique<Order>(order_in);
46
+ order_ptr->timestamp = now_ns();
47
+ Order& order = *order_ptr;
48
+ order_store_[order.id] = std::move(order_ptr);
49
+
50
+ auto trades = it->second->add_order(order);
51
+
52
+ for (auto& t : trades) {
53
+ Side fill_side = (t.buy_order_id == order.id) ? Side::BUY : Side::SELL;
54
+ risk_.on_fill(t.symbol, fill_side, t.qty, from_price(t.price));
55
+ }
56
+
57
+ // Fix: Recorded inside the mutex scope to prevent vector corruption
58
+ order_lat_.record(now_ns() - t0);
59
+
60
+ return {true, "OK", trades};
61
+ }
62
+
63
+ bool ExchangeSimulator::cancel_order(const std::string& symbol, OrderId id) {
64
+ std::lock_guard<std::mutex> lock(mtx_);
65
+ auto it = books_.find(symbol);
66
+ if (it == books_.end()) return false;
67
+ return it->second->cancel_order(id);
68
+ }
69
+
70
+ bool ExchangeSimulator::modify_order(const std::string& symbol, OrderId id,
71
+ Price new_price, Quantity new_qty) {
72
+ std::lock_guard<std::mutex> lock(mtx_);
73
+ auto it = books_.find(symbol);
74
+ if (it == books_.end()) return false;
75
+ return it->second->modify_order(id, new_price, new_qty);
76
+ }
77
+
78
+ void ExchangeSimulator::on_tick(const MarketDataTick& tick) {
79
+ int64_t t0 = now_ns();
80
+ std::lock_guard<std::mutex> lock(mtx_);
81
+ risk_.update_market_price(tick.symbol, from_price(tick.last_price));
82
+ tick_lat_.record(now_ns() - t0);
83
+ }
84
+
85
+ OrderBookSnapshot ExchangeSimulator::get_snapshot(const std::string& symbol, int depth) const {
86
+ std::lock_guard<std::mutex> lock(mtx_);
87
+ auto it = books_.find(symbol);
88
+ if (it == books_.end()) return {};
89
+ return it->second->snapshot(depth);
90
+ }
91
+
92
+ std::vector<std::string> ExchangeSimulator::symbols() const {
93
+ std::lock_guard<std::mutex> lock(mtx_);
94
+ std::vector<std::string> s;
95
+ for (auto& [k, _] : books_) s.push_back(k);
96
+ return s;
97
+ }
98
+
99
+ Position ExchangeSimulator::get_position(const std::string& symbol) const {
100
+ return risk_.get_position(symbol);
101
+ }
102
+
103
+ std::unordered_map<std::string, Position> ExchangeSimulator::all_positions() const {
104
+ return risk_.all_positions();
105
+ }
106
+
107
+ double ExchangeSimulator::total_pnl() const {
108
+ return risk_.total_pnl();
109
+ }
110
+
111
+ Backtester::Backtester(RiskLimits limits) : limits_(limits) {}
112
+
113
+ void Backtester::set_strategy(std::unique_ptr<IStrategy> strategy) {
114
+ strategy_ = std::move(strategy);
115
+ }
116
+
117
+ void Backtester::add_symbol(const std::string& symbol) {
118
+ symbols_.push_back(symbol);
119
+ }
120
+
121
+ void Backtester::process_signals(const std::vector<StrategySignal>& sigs,
122
+ const MarketDataTick& tick,
123
+ BacktestResult& result,
124
+ std::unordered_map<OrderId, Order>& live_orders) {
125
+ for (auto& sig : sigs) {
126
+ if (sig.action == StrategySignal::Action::NONE) continue;
127
+
128
+ if (sig.action == StrategySignal::Action::CANCEL) {
129
+ sim_->cancel_order(tick.symbol, sig.cancel_id);
130
+ live_orders.erase(sig.cancel_id);
131
+ continue;
132
+ }
133
+ if (sig.action == StrategySignal::Action::CANCEL_ALL) {
134
+ for (auto& [id, ord] : live_orders)
135
+ sim_->cancel_order(ord.symbol, id);
136
+ live_orders.clear();
137
+ continue;
138
+ }
139
+
140
+ Order order;
141
+ order.id = sim_->next_order_id();
142
+ order.symbol = sig.symbol.empty() ? tick.symbol : sig.symbol;
143
+ order.side = (sig.action == StrategySignal::Action::BUY) ? Side::BUY : Side::SELL;
144
+ order.type = sig.type;
145
+ order.price = sig.price;
146
+ order.qty = sig.qty;
147
+ order.client_id = sig.reason;
148
+
149
+ ++result.orders_submitted;
150
+ auto res = sim_->submit_order(order, from_price(tick.last_price));
151
+
152
+ if (!res.approved) {
153
+ ++result.orders_rejected;
154
+ } else {
155
+ if (!order.is_done()) live_orders[order.id] = order;
156
+ for (auto& t : res.trades) {
157
+ ++result.orders_filled;
158
+ ++result.trades_count;
159
+ result.trades.push_back(t);
160
+ strategy_->on_trade(t);
161
+ }
162
+ strategy_->on_order_update(order);
163
+ }
164
+ }
165
+ }
166
+
167
+ BacktestResult Backtester::run(const std::vector<MarketDataTick>& ticks,
168
+ bool enable_journal,
169
+ const std::string& journal_path) {
170
+ BacktestResult result;
171
+ if (!strategy_ || ticks.empty()) return result;
172
+ result.strategy_name = strategy_->name();
173
+
174
+ sim_ = std::make_unique<ExchangeSimulator>(limits_);
175
+ for (auto& sym : symbols_) sim_->add_symbol(sym);
176
+ if (symbols_.empty() && !ticks.empty()) {
177
+ std::unordered_map<std::string, bool> seen;
178
+ for (auto& t : ticks) {
179
+ if (!seen[t.symbol]) {
180
+ sim_->add_symbol(t.symbol);
181
+ seen[t.symbol] = true;
182
+ }
183
+ }
184
+ }
185
+
186
+ std::unordered_set<std::string> inited;
187
+ for (auto& tick : ticks) {
188
+ if (!inited.count(tick.symbol)) {
189
+ strategy_->on_init(tick.symbol);
190
+ inited.insert(tick.symbol);
191
+ }
192
+ }
193
+
194
+ std::unordered_map<OrderId, Order> live_orders;
195
+ double peak_equity = std::numeric_limits<double>::lowest();
196
+ int64_t ts_start = now_ns();
197
+
198
+ for (auto& tick : ticks) {
199
+ int64_t t0 = now_ns();
200
+ sim_->on_tick(tick);
201
+
202
+ auto snap = sim_->get_snapshot(tick.symbol);
203
+ auto sigs = strategy_->on_tick(tick, snap);
204
+
205
+ process_signals(sigs, tick, result, live_orders);
206
+
207
+ if (result.ticks_processed % 100 == 0) {
208
+ double equity = sim_->total_pnl();
209
+ result.equity_curve.push_back(equity);
210
+ if (equity > peak_equity) peak_equity = equity;
211
+ if (peak_equity > std::numeric_limits<double>::lowest()) {
212
+ double drawdown = peak_equity - equity;
213
+ if (drawdown > result.max_drawdown) result.max_drawdown = drawdown;
214
+ }
215
+ }
216
+
217
+ result.tick_latency.record(now_ns() - t0);
218
+ ++result.ticks_processed;
219
+ }
220
+
221
+ int64_t elapsed_ns = now_ns() - ts_start;
222
+ result.total_pnl = sim_->total_pnl();
223
+
224
+ auto pos = sim_->all_positions();
225
+ for (auto& [sym, p] : pos) {
226
+ result.realized_pnl += p.realized_pnl;
227
+ result.unrealized_pnl += p.unrealized_pnl;
228
+ }
229
+
230
+ result.order_latency = sim_->order_latency();
231
+ result.order_latency.compute_percentiles();
232
+ result.tick_latency.compute_percentiles();
233
+
234
+ double elapsed_s = elapsed_ns / 1e9;
235
+ result.throughput_eps = elapsed_s > 0 ? result.ticks_processed / elapsed_s : 0;
236
+
237
+ if (result.equity_curve.size() > 2) {
238
+ std::vector<double> returns;
239
+ returns.reserve(result.equity_curve.size() - 1);
240
+ for (size_t i = 1; i < result.equity_curve.size(); ++i)
241
+ returns.push_back(result.equity_curve[i] - result.equity_curve[i - 1]);
242
+ double mean = std::accumulate(returns.begin(), returns.end(), 0.0) / returns.size();
243
+ double var = 0;
244
+ for (double r : returns) var += (r - mean) * (r - mean);
245
+ double std_dev = std::sqrt(var / returns.size());
246
+ result.sharpe = (std_dev > 1e-9) ? (mean / std_dev * std::sqrt(252.0)) : 0;
247
+ }
248
+
249
+ return result;
250
+ }
251
+
252
+ } // namespace hft
src/core/simulator.hpp ADDED
@@ -0,0 +1,118 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #pragma once
2
+ #include "core/types.hpp"
3
+ #include "core/order_book.hpp"
4
+ #include "risk/risk_manager.hpp"
5
+ #include "journal/journal.hpp"
6
+ #include "strategy/strategy.hpp"
7
+ #include <unordered_map>
8
+ #include <atomic>
9
+ #include <memory>
10
+ #include <mutex>
11
+ #include <deque>
12
+ #include <functional>
13
+
14
+ namespace hft {
15
+
16
+ struct BacktestResult {
17
+ std::string strategy_name;
18
+ int64_t ticks_processed = 0;
19
+ int64_t orders_submitted = 0;
20
+ int64_t orders_filled = 0;
21
+ int64_t orders_rejected = 0;
22
+ int64_t trades_count = 0;
23
+ double total_pnl = 0;
24
+ double realized_pnl = 0;
25
+ double unrealized_pnl = 0;
26
+ double max_drawdown = 0;
27
+ double sharpe = 0;
28
+ LatencyStats order_latency;
29
+ LatencyStats tick_latency;
30
+ double throughput_eps = 0;
31
+ std::vector<double> equity_curve;
32
+ std::vector<Trade> trades;
33
+ };
34
+
35
+ class ExchangeSimulator {
36
+ public:
37
+ explicit ExchangeSimulator(RiskLimits limits = {});
38
+
39
+ void add_symbol(const std::string& symbol);
40
+
41
+ struct SubmitResult {
42
+ bool approved;
43
+ std::string reject_reason;
44
+ std::vector<Trade> trades;
45
+ };
46
+
47
+ // Fix: Takes const reference to avoid scope expiration
48
+ SubmitResult submit_order(const Order& order, double market_price = 0.0);
49
+
50
+ bool cancel_order(const std::string& symbol, OrderId id);
51
+ bool modify_order(const std::string& symbol, OrderId id, Price new_price, Quantity new_qty);
52
+ void on_tick(const MarketDataTick& tick);
53
+
54
+ OrderBookSnapshot get_snapshot(const std::string& symbol, int depth = 10) const;
55
+ std::vector<std::string> symbols() const;
56
+
57
+ Position get_position(const std::string& symbol) const;
58
+ std::unordered_map<std::string, Position> all_positions() const;
59
+ double total_pnl() const;
60
+ RiskManager& risk() { return risk_; }
61
+
62
+ void set_trade_callback(TradeCallback cb) { trade_cb_ = std::move(cb); }
63
+ void set_order_callback(OrderCallback cb) { order_cb_ = std::move(cb); }
64
+
65
+ LatencyStats& order_latency() { return order_lat_; }
66
+ LatencyStats& tick_latency() { return tick_lat_; }
67
+ int64_t total_orders() const { return total_orders_.load(std::memory_order_relaxed); }
68
+ int64_t total_trades() const { return total_trades_.load(std::memory_order_relaxed); }
69
+ int64_t total_rejects() const { return total_rejects_.load(std::memory_order_relaxed); }
70
+
71
+ OrderId next_order_id() { return ++next_oid_; }
72
+
73
+ private:
74
+ std::unordered_map<std::string, std::unique_ptr<OrderBook>> books_;
75
+ // Fix: Correct key type and properly controls memory lifetime
76
+ std::unordered_map<OrderId, std::unique_ptr<Order>> order_store_;
77
+ RiskManager risk_;
78
+ BinaryJournal journal_;
79
+ mutable std::mutex mtx_;
80
+
81
+ TradeCallback trade_cb_;
82
+ OrderCallback order_cb_;
83
+
84
+ std::atomic<OrderId> next_oid_{1000};
85
+ LatencyStats order_lat_, tick_lat_;
86
+
87
+ // Fix: Atomic counters for thread-safe operations in main.cpp
88
+ std::atomic<int64_t> total_orders_{0};
89
+ std::atomic<int64_t> total_trades_{0};
90
+ std::atomic<int64_t> total_rejects_{0};
91
+ };
92
+
93
+ class Backtester {
94
+ public:
95
+ explicit Backtester(RiskLimits limits = {});
96
+
97
+ void set_strategy(std::unique_ptr<IStrategy> strategy);
98
+ void add_symbol(const std::string& symbol);
99
+
100
+ BacktestResult run(const std::vector<MarketDataTick>& ticks,
101
+ bool enable_journal = true,
102
+ const std::string& journal_path = "replay.journal");
103
+
104
+
105
+
106
+ private:
107
+ std::unique_ptr<IStrategy> strategy_;
108
+ std::unique_ptr<ExchangeSimulator> sim_;
109
+ RiskLimits limits_;
110
+ std::vector<std::string> symbols_;
111
+
112
+ void process_signals(const std::vector<StrategySignal>& sigs,
113
+ const MarketDataTick& tick,
114
+ BacktestResult& result,
115
+ std::unordered_map<OrderId, Order>& live_orders);
116
+ };
117
+
118
+ } // namespace hft
src/core/types.hpp ADDED
@@ -0,0 +1,141 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #pragma once
2
+ #include <cstdint>
3
+ #include <string>
4
+ #include <chrono>
5
+ #include <vector>
6
+ #include <algorithm>
7
+ #include <cmath>
8
+
9
+ namespace hft {
10
+
11
+ using Price = int64_t; // price in ticks (e.g. 1234567 = $123.4567)
12
+ using Quantity = int64_t;
13
+ using OrderId = uint64_t;
14
+ using Timestamp = int64_t; // nanoseconds since epoch
15
+
16
+ constexpr Price PRICE_SCALE = 10000; // 4 decimal places
17
+
18
+ inline Price to_price(double d) { return static_cast<Price>(std::llround(d * PRICE_SCALE)); }
19
+ inline double from_price(Price p) { return static_cast<double>(p) / PRICE_SCALE; }
20
+
21
+ enum class Side : uint8_t { BUY = 0, SELL = 1 };
22
+ enum class OrderType : uint8_t { LIMIT = 0, MARKET = 1, IOC = 2, FOK = 3 };
23
+ enum class OrderStatus : uint8_t {
24
+ PENDING, NEW, PARTIALLY_FILLED, FILLED, CANCELLED, REJECTED, EXPIRED
25
+ };
26
+ enum class TimeInForce : uint8_t { GTC = 0, IOC = 1, FOK = 2, DAY = 3 };
27
+
28
+ struct Order {
29
+ OrderId id = 0;
30
+ std::string symbol;
31
+ Side side = Side::BUY;
32
+ OrderType type = OrderType::LIMIT;
33
+ Price price = 0;
34
+ Quantity qty = 0;
35
+ Quantity filled_qty = 0;
36
+ OrderStatus status = OrderStatus::PENDING;
37
+ Timestamp timestamp = 0;
38
+ std::string client_id;
39
+
40
+ Quantity remaining() const { return qty - filled_qty; }
41
+ bool is_done() const {
42
+ return status == OrderStatus::FILLED ||
43
+ status == OrderStatus::CANCELLED ||
44
+ status == OrderStatus::REJECTED ||
45
+ status == OrderStatus::EXPIRED;
46
+ }
47
+ };
48
+
49
+ struct Trade {
50
+ uint64_t trade_id;
51
+ OrderId buy_order_id;
52
+ OrderId sell_order_id;
53
+ std::string symbol;
54
+ Price price;
55
+ Quantity qty;
56
+ Timestamp timestamp;
57
+ };
58
+
59
+ struct MarketDataTick {
60
+ Timestamp timestamp;
61
+ std::string symbol;
62
+ Price bid_price;
63
+ Quantity bid_qty;
64
+ Price ask_price;
65
+ Quantity ask_qty;
66
+ Price last_price;
67
+ Quantity last_qty;
68
+ };
69
+
70
+ struct L2Level {
71
+ Price price;
72
+ Quantity qty;
73
+ int order_count;
74
+ };
75
+
76
+ struct OrderBookSnapshot {
77
+ std::string symbol;
78
+ Timestamp timestamp;
79
+ std::vector<L2Level> bids;
80
+ std::vector<L2Level> asks;
81
+ Price best_bid() const { return bids.empty() ? 0 : bids[0].price; }
82
+ Price best_ask() const { return asks.empty() ? 0 : asks[0].price; }
83
+ Price mid() const {
84
+ if (bids.empty() || asks.empty()) return 0;
85
+ return (best_bid() + best_ask()) / 2;
86
+ }
87
+ double spread() const {
88
+ if (bids.empty() || asks.empty()) return 0.0;
89
+ Price s = best_ask() - best_bid();
90
+ return s > 0 ? from_price(s) : 0.0;
91
+ }
92
+ };
93
+
94
+ struct RiskLimits {
95
+ Quantity max_order_qty = 10000;
96
+ Quantity max_position = 100000;
97
+ double max_notional_usd = 1000000.0;
98
+ int max_orders_per_sec = 1000;
99
+ bool enabled = true;
100
+ };
101
+
102
+ struct Position {
103
+ std::string symbol;
104
+ Quantity net_qty = 0;
105
+ double avg_price = 0.0;
106
+ double realized_pnl = 0.0;
107
+ double unrealized_pnl = 0.0;
108
+ double notional() const { return std::abs(net_qty * avg_price); }
109
+ };
110
+
111
+ struct LatencyStats {
112
+ int64_t count = 0;
113
+ int64_t sum_ns = 0;
114
+ int64_t min_ns = INT64_MAX;
115
+ int64_t max_ns = 0;
116
+ int64_t p50_ns = 0;
117
+ int64_t p99_ns = 0;
118
+ std::vector<int64_t> samples;
119
+
120
+ void record(int64_t ns) {
121
+ ++count; sum_ns += ns;
122
+ if (ns < min_ns) min_ns = ns;
123
+ if (ns > max_ns) max_ns = ns;
124
+ samples.push_back(ns);
125
+ }
126
+ void compute_percentiles() {
127
+ if (samples.empty()) return;
128
+ std::vector<int64_t> sorted = samples; // Fix: Copy before sort
129
+ std::sort(sorted.begin(), sorted.end());
130
+ p50_ns = sorted[sorted.size() * 50 / 100];
131
+ p99_ns = sorted[sorted.size() * 99 / 100];
132
+ }
133
+ double avg_us() const { return count ? (double)sum_ns / count / 1000.0 : 0; }
134
+ };
135
+
136
+ inline Timestamp now_ns() {
137
+ return std::chrono::duration_cast<std::chrono::nanoseconds>(
138
+ std::chrono::high_resolution_clock::now().time_since_epoch()).count();
139
+ }
140
+
141
+ } // namespace hft
src/engine_cli.cpp ADDED
@@ -0,0 +1,313 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #include "core/simulator.hpp"
2
+ #include "core/types.hpp"
3
+ #include "strategy/strategy.hpp"
4
+ #include "network/serializer.hpp"
5
+
6
+ #include <iostream>
7
+ #include <string>
8
+ #include <sstream>
9
+ #include <unordered_map>
10
+ #include <memory>
11
+ #include <unordered_set>
12
+ #include <fstream>
13
+
14
+ using namespace hft;
15
+
16
+ static std::string jget(const std::string& body, const std::string& key) {
17
+ auto pos = body.find("\"" + key + "\"");
18
+ if (pos == std::string::npos) return "";
19
+ pos = body.find(":", pos);
20
+ if (pos == std::string::npos) return "";
21
+ ++pos;
22
+ while (pos < body.size() && (body[pos] == ' ' || body[pos] == '\t')) ++pos;
23
+ if (pos >= body.size()) return "";
24
+ if (body[pos] == '"') {
25
+ ++pos;
26
+ size_t end = body.find('"', pos);
27
+ return body.substr(pos, end - pos);
28
+ }
29
+ size_t end = body.find_first_of(",}\n", pos);
30
+ return body.substr(pos, end - pos);
31
+ }
32
+
33
+ static RiskLimits g_limits;
34
+ static std::unique_ptr<ExchangeSimulator> g_sim;
35
+ static std::vector<MarketDataTick> g_tick_feed;
36
+ static size_t g_feed_idx = 0;
37
+ static std::deque<std::string> g_trade_log;
38
+ static std::deque<std::string> g_order_log;
39
+
40
+ static void init_sim() {
41
+ g_sim = std::make_unique<ExchangeSimulator>(g_limits);
42
+ for (auto& sym : {"AAPL", "GOOG", "MSFT", "TSLA", "NVDA"})
43
+ g_sim->add_symbol(sym);
44
+ g_sim->set_trade_callback([](const Trade& t) {
45
+ g_trade_log.push_back(trade_to_json(t));
46
+ if ((int)g_trade_log.size() > 500) g_trade_log.pop_front();
47
+ });
48
+ g_sim->set_order_callback([](const Order& o) {
49
+ g_order_log.push_back(order_to_json(o));
50
+ if ((int)g_order_log.size() > 500) g_order_log.pop_front();
51
+ });
52
+ }
53
+
54
+ static void init_feed() {
55
+ const char* syms[] = {"AAPL", "GOOG", "MSFT", "TSLA", "NVDA"};
56
+ double prices[] = {185.0, 140.0, 375.0, 245.0, 485.0};
57
+ g_tick_feed.clear();
58
+ for (int i = 0; i < 5; ++i) {
59
+ MarketDataParser::Config cfg;
60
+ cfg.symbol = syms[i];
61
+ cfg.start_price = prices[i];
62
+ cfg.ticks = 3000;
63
+ cfg.volatility = 0.0008;
64
+ cfg.spread_bps = 3.0;
65
+ cfg.seed = 42 + i;
66
+ auto ticks = MarketDataParser::generate_synthetic(cfg);
67
+ for (auto& t : ticks) g_tick_feed.push_back(t);
68
+ }
69
+ std::sort(g_tick_feed.begin(), g_tick_feed.end(),
70
+ [](const MarketDataTick& a, const MarketDataTick& b) {
71
+ return a.timestamp < b.timestamp;
72
+ });
73
+ g_feed_idx = 0;
74
+ }
75
+
76
+ static std::string handle(const std::string& cmd, const std::string& payload) {
77
+
78
+ if (cmd == "status") {
79
+ g_sim->order_latency().compute_percentiles();
80
+ g_sim->tick_latency().compute_percentiles();
81
+ return "{\"total_orders\":" + J::num(g_sim->total_orders()) +
82
+ ",\"total_trades\":" + J::num(g_sim->total_trades()) +
83
+ ",\"total_rejects\":" + J::num(g_sim->total_rejects()) +
84
+ ",\"total_pnl\":" + J::num(g_sim->total_pnl()) +
85
+ ",\"order_p50_us\":" + J::num(g_sim->order_latency().p50_ns / 1000.0) +
86
+ ",\"order_p99_us\":" + J::num(g_sim->order_latency().p99_ns / 1000.0) +
87
+ ",\"tick_p50_us\":" + J::num(g_sim->tick_latency().p50_ns / 1000.0) +
88
+ ",\"tick_p99_us\":" + J::num(g_sim->tick_latency().p99_ns / 1000.0) +
89
+ ",\"risk_checked\":" + J::num(g_sim->risk().orders_checked()) +
90
+ ",\"risk_rejected\":" + J::num(g_sim->risk().orders_rejected()) +
91
+ ",\"feed_size\":" + J::num((int64_t)g_tick_feed.size()) +
92
+ ",\"feed_idx\":" + J::num((int64_t)g_feed_idx) + "}";
93
+ }
94
+
95
+ if (cmd == "reset") {
96
+ g_trade_log.clear();
97
+ g_order_log.clear();
98
+ init_sim();
99
+ init_feed();
100
+ return "{\"ok\":true}";
101
+ }
102
+
103
+ if (cmd == "book") {
104
+ std::string sym = jget(payload, "symbol");
105
+ if (sym.empty()) sym = "AAPL";
106
+ auto snap = g_sim->get_snapshot(sym);
107
+ return snapshot_to_json(snap);
108
+ }
109
+
110
+ if (cmd == "positions") {
111
+ auto pos = g_sim->all_positions();
112
+ std::string r = "[";
113
+ bool first = true;
114
+ for (auto& [sym, p] : pos) {
115
+ if (!first) r += ",";
116
+ r += position_to_json(p);
117
+ first = false;
118
+ }
119
+ return r + "]";
120
+ }
121
+
122
+ if (cmd == "trades") {
123
+ std::string r = "[";
124
+ bool first = true;
125
+ for (auto& t : g_trade_log) {
126
+ if (!first) r += ",";
127
+ r += t;
128
+ first = false;
129
+ }
130
+ return r + "]";
131
+ }
132
+
133
+ if (cmd == "orders") {
134
+ std::string r = "[";
135
+ bool first = true;
136
+ for (auto& o : g_order_log) {
137
+ if (!first) r += ",";
138
+ r += o;
139
+ first = false;
140
+ }
141
+ return r + "]";
142
+ }
143
+
144
+ if (cmd == "order") {
145
+ Order order;
146
+ order.id = g_sim->next_order_id();
147
+ order.symbol = jget(payload, "symbol");
148
+ if (order.symbol.empty()) order.symbol = "AAPL";
149
+ std::string side_s = jget(payload, "side");
150
+ order.side = (side_s == "SELL") ? Side::SELL : Side::BUY;
151
+ std::string type_s = jget(payload, "type");
152
+ if (type_s == "MARKET") order.type = OrderType::MARKET;
153
+ else if (type_s == "IOC") order.type = OrderType::IOC;
154
+ else if (type_s == "FOK") order.type = OrderType::FOK;
155
+ else order.type = OrderType::LIMIT;
156
+ try {
157
+ order.price = to_price(std::stod(jget(payload, "price")));
158
+ order.qty = std::stoll(jget(payload, "qty"));
159
+ } catch (...) {
160
+ return "{\"error\":\"invalid price or qty\"}";
161
+ }
162
+ order.client_id = jget(payload, "client_id");
163
+
164
+ double mkt_px = 0;
165
+ try { mkt_px = std::stod(jget(payload, "market_price")); } catch (...) {}
166
+
167
+ auto result = g_sim->submit_order(order, mkt_px);
168
+ std::string r = "{\"approved\":" + J::boolean(result.approved) +
169
+ ",\"order\":" + order_to_json(order) +
170
+ ",\"reject_reason\":" + J::str(result.reject_reason) +
171
+ ",\"trades\":[";
172
+ for (size_t i = 0; i < result.trades.size(); ++i) {
173
+ if (i) r += ",";
174
+ r += trade_to_json(result.trades[i]);
175
+ }
176
+ return r + "]}";
177
+ }
178
+
179
+ if (cmd == "cancel") {
180
+ OrderId id = 0;
181
+ std::string sym = jget(payload, "symbol");
182
+ try { id = std::stoull(jget(payload, "id")); } catch (...) {}
183
+ bool ok = g_sim->cancel_order(sym, id);
184
+ return "{\"cancelled\":" + J::boolean(ok) + "}";
185
+ }
186
+
187
+ if (cmd == "feed_step") {
188
+ int n = 10;
189
+ std::string n_str = jget(payload, "n");
190
+ if (!n_str.empty()) {
191
+ try { n = std::stoi(n_str); } catch (...) {}
192
+ }
193
+ n = std::max(1, std::min(n, 200));
194
+
195
+ std::string r = "[";
196
+ bool first = true;
197
+ for (int i = 0; i < n; ++i) {
198
+ if (g_feed_idx >= g_tick_feed.size()) g_feed_idx = 0;
199
+ auto& tick = g_tick_feed[g_feed_idx++];
200
+ g_sim->on_tick(tick);
201
+ if (!first) r += ",";
202
+ r += "{\"symbol\":" + J::str(tick.symbol) +
203
+ ",\"bid\":" + J::num(from_price(tick.bid_price)) +
204
+ ",\"ask\":" + J::num(from_price(tick.ask_price)) +
205
+ ",\"last\":" + J::num(from_price(tick.last_price)) +
206
+ ",\"bid_qty\":" + J::num(tick.bid_qty) +
207
+ ",\"ask_qty\":" + J::num(tick.ask_qty) +
208
+ ",\"ts\":" + J::num(tick.timestamp) + "}";
209
+ first = false;
210
+ }
211
+ return r + "]";
212
+ }
213
+
214
+ if (cmd == "backtest") {
215
+ std::string strategy_name = jget(payload, "strategy");
216
+ std::string symbol = jget(payload, "symbol");
217
+ if (symbol.empty()) symbol = "AAPL";
218
+ int ticks = 5000;
219
+ double start_price = 150.0;
220
+ try { ticks = std::stoi(jget(payload, "ticks")); } catch (...) {}
221
+ try { start_price = std::stod(jget(payload, "start_price")); } catch (...) {}
222
+ ticks = std::min(std::max(ticks, 200), 50000);
223
+
224
+ MarketDataParser::Config cfg;
225
+ cfg.symbol = symbol;
226
+ cfg.start_price = start_price;
227
+ cfg.ticks = ticks;
228
+ cfg.volatility = 0.001;
229
+ cfg.seed = 777;
230
+ auto bt_ticks = MarketDataParser::generate_synthetic(cfg);
231
+
232
+ RiskLimits rl;
233
+ rl.max_order_qty = 10000;
234
+ rl.max_position = 500000;
235
+ rl.max_notional_usd = 1e9;
236
+ rl.max_orders_per_sec = 10000;
237
+
238
+ Backtester bt(rl);
239
+ bt.add_symbol(symbol);
240
+
241
+ std::unique_ptr<IStrategy> strat;
242
+ if (strategy_name == "Momentum")
243
+ strat = std::make_unique<MomentumStrategy>();
244
+ else if (strategy_name == "MeanReversion")
245
+ strat = std::make_unique<MeanReversionStrategy>();
246
+ else
247
+ strat = std::make_unique<MarketMakingStrategy>();
248
+ bt.set_strategy(std::move(strat));
249
+
250
+ auto result = bt.run(bt_ticks, false);
251
+ return backtest_result_to_json(result);
252
+ }
253
+
254
+ if (cmd == "risk_limits") {
255
+ try {
256
+ g_limits.max_order_qty = std::stoll(jget(payload, "max_order_qty"));
257
+ g_limits.max_position = std::stoll(jget(payload, "max_position"));
258
+ g_limits.max_notional_usd = std::stod(jget(payload, "max_notional_usd"));
259
+ g_limits.max_orders_per_sec = std::stoi(jget(payload, "max_orders_per_sec"));
260
+ g_sim->risk().set_limits(g_limits);
261
+ return "{\"ok\":true}";
262
+ } catch (...) {
263
+ return "{\"error\":\"bad limits\"}";
264
+ }
265
+ }
266
+
267
+ if (cmd == "get_limits") {
268
+ auto l = g_sim->risk().get_limits();
269
+ return "{\"max_order_qty\":" + J::num(l.max_order_qty) +
270
+ ",\"max_position\":" + J::num(l.max_position) +
271
+ ",\"max_notional_usd\":" + J::num(l.max_notional_usd, 0) +
272
+ ",\"max_orders_per_sec\":" + J::num((int64_t)l.max_orders_per_sec) +
273
+ ",\"enabled\":" + J::boolean(l.enabled) + "}";
274
+ }
275
+
276
+ if (cmd == "symbols") {
277
+ auto syms = g_sim->symbols();
278
+ std::string r = "[";
279
+ for (size_t i = 0; i < syms.size(); ++i) {
280
+ if (i) r += ",";
281
+ r += J::str(syms[i]);
282
+ }
283
+ return r + "]";
284
+ }
285
+
286
+ return "{\"error\":" + J::str("unknown command: " + cmd) + "}";
287
+ }
288
+
289
+ int main() {
290
+ g_limits.max_order_qty = 10000;
291
+ g_limits.max_position = 500000;
292
+ g_limits.max_notional_usd = 50000000.0;
293
+ g_limits.max_orders_per_sec = 2000;
294
+
295
+ init_sim();
296
+ init_feed();
297
+
298
+ std::string line;
299
+ while (std::getline(std::cin, line)) {
300
+ if (line.empty()) continue;
301
+ size_t sp = line.find(' ');
302
+ std::string cmd = (sp == std::string::npos) ? line : line.substr(0, sp);
303
+ std::string payload = (sp == std::string::npos) ? "{}" : line.substr(sp + 1);
304
+ try {
305
+ std::cout << handle(cmd, payload) << "\n";
306
+ std::cout.flush();
307
+ } catch (const std::exception& e) {
308
+ std::cout << "{\"error\":" << J::str(e.what()) << "}\n";
309
+ std::cout.flush();
310
+ }
311
+ }
312
+ return 0;
313
+ }
src/journal/journal.cpp ADDED
@@ -0,0 +1,204 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #include "journal/journal.hpp"
2
+ #include <cmath>
3
+ #include <random>
4
+ #include <stdexcept>
5
+ #include <sstream>
6
+ #include <fstream>
7
+
8
+ namespace hft {
9
+
10
+ BinaryJournal::BinaryJournal(const std::string& path) : path_(path) {}
11
+
12
+ BinaryJournal::~BinaryJournal() { close(); }
13
+
14
+ bool BinaryJournal::open_write() {
15
+ wfile_.open(path_, std::ios::binary | std::ios::trunc);
16
+ if (!wfile_.good()) return false;
17
+ // Write magic header
18
+ const char magic[] = "HFTJ\x01\x00";
19
+ wfile_.write(magic, 6);
20
+ return true;
21
+ }
22
+
23
+ bool BinaryJournal::open_read() {
24
+ rfile_.open(path_, std::ios::binary);
25
+ if (!rfile_.good()) return false;
26
+ char magic[6];
27
+ rfile_.read(magic, 6);
28
+ return true;
29
+ }
30
+
31
+ void BinaryJournal::close() {
32
+ if (wfile_.is_open()) wfile_.close();
33
+ if (rfile_.is_open()) rfile_.close();
34
+ }
35
+
36
+ uint32_t BinaryJournal::checksum(const uint8_t* data, size_t len) {
37
+ uint32_t crc = 0xFFFFFFFF;
38
+ for (size_t i = 0; i < len; ++i) {
39
+ crc ^= data[i];
40
+ for (int j = 0; j < 8; ++j)
41
+ crc = (crc >> 1) ^ (0xEDB88320 & -(crc & 1));
42
+ }
43
+ return crc ^ 0xFFFFFFFF;
44
+ }
45
+
46
+ void BinaryJournal::write_record(JournalRecordType type, const void* data, size_t size) {
47
+ if (!wfile_.is_open()) return;
48
+ uint8_t rtype = (uint8_t)type;
49
+ uint32_t psz = (uint32_t)size;
50
+ uint32_t crc = checksum((const uint8_t*)data, size);
51
+
52
+ wfile_.write((char*)&rtype, 1);
53
+ wfile_.write((char*)&psz, 4);
54
+ wfile_.write((char*)data, size);
55
+ wfile_.write((char*)&crc, 4);
56
+ ++records_written_;
57
+ }
58
+
59
+ void BinaryJournal::write_order(JournalRecordType type, const Order& order) {
60
+ JournalOrderRecord rec{};
61
+ rec.id = order.id;
62
+ rec.price = order.price;
63
+ rec.qty = order.qty;
64
+ rec.timestamp = order.timestamp;
65
+ rec.side = (uint8_t)order.side;
66
+ rec.type = (uint8_t)order.type;
67
+ rec.status = (uint8_t)order.status;
68
+ strncpy(rec.symbol, order.symbol.c_str(), 15);
69
+ strncpy(rec.client_id, order.client_id.c_str(), 31);
70
+ write_record(type, &rec, sizeof(rec));
71
+ }
72
+
73
+ void BinaryJournal::write_trade(const Trade& trade) {
74
+ JournalTradeRecord rec{};
75
+ rec.trade_id = trade.trade_id;
76
+ rec.buy_id = trade.buy_order_id;
77
+ rec.sell_id = trade.sell_order_id;
78
+ rec.price = trade.price;
79
+ rec.qty = trade.qty;
80
+ rec.timestamp = trade.timestamp;
81
+ strncpy(rec.symbol, trade.symbol.c_str(), 15);
82
+ write_record(JournalRecordType::TRADE, &rec, sizeof(rec));
83
+ }
84
+
85
+ void BinaryJournal::write_tick(const MarketDataTick& tick) {
86
+ JournalTickRecord rec{};
87
+ rec.timestamp = tick.timestamp;
88
+ rec.bid_price = tick.bid_price;
89
+ rec.bid_qty = tick.bid_qty;
90
+ rec.ask_price = tick.ask_price;
91
+ rec.ask_qty = tick.ask_qty;
92
+ rec.last_price = tick.last_price;
93
+ rec.last_qty = tick.last_qty;
94
+ strncpy(rec.symbol, tick.symbol.c_str(), 15);
95
+ write_record(JournalRecordType::TICK, &rec, sizeof(rec));
96
+ }
97
+
98
+ void BinaryJournal::write_session_start() {
99
+ int64_t ts = now_ns();
100
+ write_record(JournalRecordType::SESSION_START, &ts, sizeof(ts));
101
+ }
102
+
103
+ void BinaryJournal::write_session_end() {
104
+ int64_t ts = now_ns();
105
+ write_record(JournalRecordType::SESSION_END, &ts, sizeof(ts));
106
+ }
107
+
108
+ std::vector<BinaryJournal::Record> BinaryJournal::read_all() {
109
+ std::vector<Record> records;
110
+ if (!rfile_.is_open()) return records;
111
+
112
+ while (rfile_.good() && !rfile_.eof()) {
113
+ uint8_t rtype;
114
+ rfile_.read((char*)&rtype, 1);
115
+ if (rfile_.eof()) break;
116
+
117
+ uint32_t psz;
118
+ rfile_.read((char*)&psz, 4);
119
+ if (rfile_.fail() || psz > 1<<20) break;
120
+
121
+ Record rec;
122
+ rec.type = (JournalRecordType)rtype;
123
+ rec.payload.resize(psz);
124
+ rfile_.read((char*)rec.payload.data(), psz);
125
+
126
+ uint32_t crc_stored;
127
+ rfile_.read((char*)&crc_stored, 4);
128
+
129
+ // Verify
130
+ uint32_t crc_calc = checksum(rec.payload.data(), psz);
131
+ if (crc_calc != crc_stored) break; // corruption
132
+
133
+ records.push_back(std::move(rec));
134
+ }
135
+ return records;
136
+ }
137
+
138
+ // ─── Market Data Generator ────────────────────────────────────────────────────
139
+
140
+ std::vector<MarketDataTick> MarketDataParser::generate_synthetic(const Config& cfg) {
141
+ std::mt19937_64 rng(cfg.seed);
142
+ std::normal_distribution<double> noise(0.0, cfg.volatility);
143
+ std::uniform_int_distribution<int> qty_dist(100, 5000);
144
+
145
+ std::vector<MarketDataTick> ticks;
146
+ ticks.reserve(cfg.ticks);
147
+
148
+ double price = cfg.start_price;
149
+ double spread = cfg.start_price * cfg.spread_bps / 10000.0;
150
+
151
+ for (int i = 0; i < cfg.ticks; ++i) {
152
+ // Geometric brownian motion tick
153
+ price *= (1.0 + noise(rng));
154
+ if (price <= 0) price = 0.01;
155
+
156
+ spread = price * cfg.spread_bps / 10000.0;
157
+
158
+ MarketDataTick tick{};
159
+ tick.timestamp = cfg.start_ts + (int64_t)i * cfg.tick_interval_ns;
160
+ tick.symbol = cfg.symbol;
161
+ tick.bid_price = to_price(price - spread / 2.0);
162
+ tick.ask_price = to_price(price + spread / 2.0);
163
+ tick.bid_qty = qty_dist(rng);
164
+ tick.ask_qty = qty_dist(rng);
165
+ tick.last_price = to_price(price);
166
+ tick.last_qty = qty_dist(rng) / 2;
167
+ ticks.push_back(tick);
168
+ }
169
+ return ticks;
170
+ }
171
+
172
+ std::vector<MarketDataTick> MarketDataParser::parse_csv(const std::string& path) {
173
+ std::vector<MarketDataTick> ticks;
174
+ std::ifstream f(path);
175
+ if (!f.good()) return ticks;
176
+
177
+ std::string line;
178
+ std::getline(f, line); // skip header
179
+
180
+ while (std::getline(f, line)) {
181
+ if (line.empty()) continue;
182
+ std::istringstream ss(line);
183
+ std::string tok;
184
+ std::vector<std::string> cols;
185
+ while (std::getline(ss, tok, ',')) cols.push_back(tok);
186
+ if (cols.size() < 7) continue;
187
+
188
+ MarketDataTick tick{};
189
+ try {
190
+ tick.timestamp = std::stoll(cols[0]);
191
+ tick.symbol = cols[1];
192
+ tick.bid_price = to_price(std::stod(cols[2]));
193
+ tick.bid_qty = std::stoll(cols[3]);
194
+ tick.ask_price = to_price(std::stod(cols[4]));
195
+ tick.ask_qty = std::stoll(cols[5]);
196
+ tick.last_price = to_price(std::stod(cols[6]));
197
+ tick.last_qty = cols.size() > 7 ? std::stoll(cols[7]) : 0;
198
+ ticks.push_back(tick);
199
+ } catch (...) { continue; }
200
+ }
201
+ return ticks;
202
+ }
203
+
204
+ } // namespace hft
src/journal/journal.hpp ADDED
@@ -0,0 +1,63 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #pragma once
2
+ #include "core/types.hpp"
3
+ #include <fstream>
4
+ #include <string>
5
+ #include <vector>
6
+ #include <cstring>
7
+
8
+ namespace hft {
9
+
10
+ enum class JournalRecordType : uint8_t {
11
+ ORDER_NEW=1, ORDER_CANCEL=2, ORDER_MODIFY=3, TRADE=4,
12
+ TICK=5, SNAPSHOT=6, SESSION_START=7, SESSION_END=8
13
+ };
14
+
15
+ #pragma pack(push, 1)
16
+ struct JournalOrderRecord {
17
+ OrderId id; int64_t price; int64_t qty; int64_t timestamp;
18
+ uint8_t side; uint8_t type; uint8_t status;
19
+ char symbol[16]; char client_id[32];
20
+ };
21
+ struct JournalTradeRecord {
22
+ uint64_t trade_id; OrderId buy_id; OrderId sell_id;
23
+ int64_t price; int64_t qty; int64_t timestamp; char symbol[16];
24
+ };
25
+ struct JournalTickRecord {
26
+ int64_t timestamp, bid_price, bid_qty, ask_price, ask_qty, last_price, last_qty;
27
+ char symbol[16];
28
+ };
29
+ #pragma pack(pop)
30
+
31
+ class BinaryJournal {
32
+ public:
33
+ explicit BinaryJournal(const std::string& path);
34
+ ~BinaryJournal();
35
+ bool open_write(); bool open_read(); void close();
36
+ void write_order(JournalRecordType type, const Order& order);
37
+ void write_trade(const Trade& trade);
38
+ void write_tick(const MarketDataTick& tick);
39
+ void write_session_start(); void write_session_end();
40
+ struct Record { JournalRecordType type; std::vector<uint8_t> payload; };
41
+ std::vector<Record> read_all();
42
+ size_t records_written() const { return records_written_; }
43
+ private:
44
+ std::string path_;
45
+ std::ofstream wfile_; std::ifstream rfile_;
46
+ size_t records_written_ = 0;
47
+ uint32_t checksum(const uint8_t* data, size_t len);
48
+ void write_record(JournalRecordType type, const void* data, size_t size);
49
+ };
50
+
51
+ class MarketDataParser {
52
+ public:
53
+ struct Config {
54
+ std::string symbol = "AAPL"; double start_price = 150.0;
55
+ double volatility = 0.001; double spread_bps = 5.0; int ticks = 10000;
56
+ int64_t start_ts = 1700000000000000000LL; int64_t tick_interval_ns = 100000;
57
+ uint64_t seed = 42;
58
+ };
59
+ static std::vector<MarketDataTick> generate_synthetic(const Config& cfg);
60
+ static std::vector<MarketDataTick> parse_csv(const std::string& path);
61
+ };
62
+
63
+ } // namespace hft
src/main.cpp ADDED
@@ -0,0 +1,374 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #include "core/simulator.hpp"
2
+ #include "core/types.hpp"
3
+ #include "strategy/strategy.hpp"
4
+ #include "network/serializer.hpp"
5
+
6
+ // httplib single-header
7
+ #define CPPHTTPLIB_OPENSSL_SUPPORT 0
8
+ #include "httplib.h"
9
+
10
+ #include <iostream>
11
+ #include <sstream>
12
+ #include <string>
13
+ #include <thread>
14
+ #include <atomic>
15
+ #include <vector>
16
+ #include <deque>
17
+ #include <mutex>
18
+ #include <unordered_map>
19
+ #include <memory>
20
+ #include <cstring>
21
+
22
+ using namespace hft;
23
+
24
+ // ── Global state ──────────────────────────────────────────────────────────────
25
+ static RiskLimits g_limits;
26
+ static std::unique_ptr<ExchangeSimulator> g_sim;
27
+ static std::vector<MarketDataTick> g_tick_feed;
28
+ static std::atomic<bool> g_feed_running{false};
29
+ static std::mutex g_feed_mtx;
30
+
31
+ static std::deque<std::string> g_recent_trades; // JSON strings, max 200
32
+ static std::deque<std::string> g_recent_orders;
33
+ static std::mutex g_log_mtx;
34
+ static const int MAX_LOG = 200;
35
+
36
+ static void log_trade(const Trade& t) {
37
+ std::lock_guard<std::mutex> lk(g_log_mtx);
38
+ g_recent_trades.push_back(trade_to_json(t));
39
+ if ((int)g_recent_trades.size() > MAX_LOG) g_recent_trades.pop_front();
40
+ }
41
+
42
+ static void log_order(const Order& o) {
43
+ std::lock_guard<std::mutex> lk(g_log_mtx);
44
+ g_recent_orders.push_back(order_to_json(o));
45
+ if ((int)g_recent_orders.size() > MAX_LOG) g_recent_orders.pop_front();
46
+ }
47
+
48
+ // ── Simple JSON param parser ──────────────────────────────────────────────────
49
+ static std::string json_get(const std::string& body, const std::string& key) {
50
+ // Extremely basic key extraction — good enough for our controlled API
51
+ auto pos = body.find("\"" + key + "\"");
52
+ if (pos == std::string::npos) return "";
53
+ pos = body.find(":", pos);
54
+ if (pos == std::string::npos) return "";
55
+ ++pos;
56
+ while (pos < body.size() && (body[pos] == ' ' || body[pos] == '\t')) ++pos;
57
+ if (pos >= body.size()) return "";
58
+ if (body[pos] == '"') {
59
+ ++pos;
60
+ size_t end = body.find('"', pos);
61
+ return body.substr(pos, end - pos);
62
+ }
63
+ size_t end = body.find_first_of(",}\n", pos);
64
+ return body.substr(pos, end - pos);
65
+ }
66
+
67
+ static void reset_sim() {
68
+ g_sim = std::make_unique<ExchangeSimulator>(g_limits);
69
+ // Default symbols
70
+ for (auto& sym : {"AAPL", "GOOG", "MSFT", "TSLA", "NVDA"})
71
+ g_sim->add_symbol(sym);
72
+ g_sim->set_trade_callback(log_trade);
73
+ g_sim->set_order_callback(log_order);
74
+ }
75
+
76
+ int main(int argc, char* argv[]) {
77
+ int port = 7860; // HuggingFace default
78
+ if (argc > 1) port = std::atoi(argv[1]);
79
+
80
+ g_limits.max_order_qty = 10000;
81
+ g_limits.max_position = 500000;
82
+ g_limits.max_notional_usd = 50000000.0;
83
+ g_limits.max_orders_per_sec = 2000;
84
+ reset_sim();
85
+
86
+ // Pre-generate tick feed
87
+ {
88
+ const char* syms[] = {"AAPL", "GOOG", "MSFT", "TSLA", "NVDA"};
89
+ double prices[] = {185.0, 140.0, 375.0, 245.0, 485.0};
90
+ for (int i = 0; i < 5; ++i) {
91
+ MarketDataParser::Config cfg;
92
+ cfg.symbol = syms[i]; cfg.start_price = prices[i];
93
+ cfg.ticks = 2000; cfg.volatility = 0.0008;
94
+ cfg.seed = 42 + i;
95
+ auto ticks = MarketDataParser::generate_synthetic(cfg);
96
+ for (auto& t : ticks) g_tick_feed.push_back(t);
97
+ }
98
+ // Sort by timestamp
99
+ std::sort(g_tick_feed.begin(), g_tick_feed.end(),
100
+ [](auto& a, auto& b){ return a.timestamp < b.timestamp; });
101
+ }
102
+
103
+ httplib::Server svr;
104
+
105
+ // CORS
106
+ svr.set_default_headers({
107
+ {"Access-Control-Allow-Origin", "*"},
108
+ {"Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS"},
109
+ {"Access-Control-Allow-Headers", "Content-Type"}
110
+ });
111
+ svr.Options(".*", [](auto&, auto& res){ res.status = 204; });
112
+
113
+ // ── GET /api/status ──────────────────────────────────────────────────────
114
+ svr.Get("/api/status", [](const httplib::Request&, httplib::Response& res) {
115
+ auto& s = *g_sim;
116
+ std::string body = "{\"status\":\"ok\""
117
+ ",\"total_orders\":" + J::num(s.total_orders()) +
118
+ ",\"total_trades\":" + J::num(s.total_trades()) +
119
+ ",\"total_rejects\":" + J::num(s.total_rejects()) +
120
+ ",\"total_pnl\":" + J::num(s.total_pnl()) +
121
+ ",\"order_p50_us\":" + J::num(s.order_latency().p50_ns / 1000.0) +
122
+ ",\"order_p99_us\":" + J::num(s.order_latency().p99_ns / 1000.0) +
123
+ ",\"tick_p50_us\":" + J::num(s.tick_latency().p50_ns / 1000.0) +
124
+ ",\"tick_p99_us\":" + J::num(s.tick_latency().p99_ns / 1000.0) +
125
+ ",\"risk_checked\":" + J::num(s.risk().orders_checked()) +
126
+ ",\"risk_rejected\":" + J::num(s.risk().orders_rejected()) +
127
+ ",\"feed_size\":" + J::num((int64_t)g_tick_feed.size()) +
128
+ "}";
129
+ res.set_content(body, "application/json");
130
+ });
131
+
132
+ // ── GET /api/book/:symbol ─────────────────────────────────────────────────
133
+ svr.Get(R"(/api/book/([A-Z]+))", [](const httplib::Request& req, httplib::Response& res) {
134
+ auto sym = req.matches[1].str();
135
+ auto snap = g_sim->get_snapshot(sym);
136
+ if (snap.symbol.empty()) {
137
+ res.status = 404;
138
+ res.set_content("{\"error\":\"symbol not found\"}", "application/json");
139
+ return;
140
+ }
141
+ res.set_content(snapshot_to_json(snap), "application/json");
142
+ });
143
+
144
+ // ── GET /api/positions ───────────────────────────────────────────────────
145
+ svr.Get("/api/positions", [](const httplib::Request&, httplib::Response& res) {
146
+ auto pos = g_sim->all_positions();
147
+ std::string body = "[";
148
+ bool first = true;
149
+ for (auto& [sym, p] : pos) {
150
+ if (!first) body += ",";
151
+ body += position_to_json(p);
152
+ first = false;
153
+ }
154
+ body += "]";
155
+ res.set_content(body, "application/json");
156
+ });
157
+
158
+ // ── GET /api/trades ───────────────────────────────────────────────────────
159
+ svr.Get("/api/trades", [](const httplib::Request&, httplib::Response& res) {
160
+ std::lock_guard<std::mutex> lk(g_log_mtx);
161
+ std::string body = "[";
162
+ bool first = true;
163
+ for (auto& t : g_recent_trades) {
164
+ if (!first) body += ",";
165
+ body += t; first = false;
166
+ }
167
+ body += "]";
168
+ res.set_content(body, "application/json");
169
+ });
170
+
171
+ // ── GET /api/orders ───────────────────────────────────────────────────────
172
+ svr.Get("/api/orders", [](const httplib::Request&, httplib::Response& res) {
173
+ std::lock_guard<std::mutex> lk(g_log_mtx);
174
+ std::string body = "[";
175
+ bool first = true;
176
+ for (auto& o : g_recent_orders) {
177
+ if (!first) body += ",";
178
+ body += o; first = false;
179
+ }
180
+ body += "]";
181
+ res.set_content(body, "application/json");
182
+ });
183
+
184
+ // ── POST /api/order ──────────────────────────────────────────────────────
185
+ svr.Post("/api/order", [](const httplib::Request& req, httplib::Response& res) {
186
+ auto& body = req.body;
187
+ Order order;
188
+ order.id = g_sim->next_order_id();
189
+ order.symbol = json_get(body, "symbol");
190
+ std::string side_s = json_get(body, "side");
191
+ order.side = (side_s == "SELL") ? Side::SELL : Side::BUY;
192
+ std::string type_s = json_get(body, "type");
193
+ if (type_s == "MARKET") order.type = OrderType::MARKET;
194
+ else if (type_s == "IOC") order.type = OrderType::IOC;
195
+ else if (type_s == "FOK") order.type = OrderType::FOK;
196
+ else order.type = OrderType::LIMIT;
197
+ try {
198
+ order.price = to_price(std::stod(json_get(body, "price")));
199
+ order.qty = std::stoll(json_get(body, "qty"));
200
+ } catch (...) {
201
+ res.status = 400;
202
+ res.set_content("{\"error\":\"invalid price or qty\"}", "application/json");
203
+ return;
204
+ }
205
+ order.client_id = json_get(body, "client_id");
206
+
207
+ auto result = g_sim->submit_order(order);
208
+ std::string resp = "{\"approved\":" + J::boolean(result.approved) +
209
+ ",\"order\":" + order_to_json(order) +
210
+ ",\"reject_reason\":" + J::str(result.reject_reason) +
211
+ ",\"trades\":[";
212
+ for (size_t i = 0; i < result.trades.size(); ++i) {
213
+ if (i) resp += ",";
214
+ resp += trade_to_json(result.trades[i]);
215
+ }
216
+ resp += "]}";
217
+ res.set_content(resp, "application/json");
218
+ });
219
+
220
+ // ── DELETE /api/order/:id/:symbol ────────────────────────────────────────
221
+ svr.Delete(R"(/api/order/(\d+)/([A-Z]+))", [](const httplib::Request& req, httplib::Response& res) {
222
+ OrderId id = std::stoull(req.matches[1].str());
223
+ std::string sym = req.matches[2].str();
224
+ bool ok = g_sim->cancel_order(sym, id);
225
+ res.set_content("{\"cancelled\":" + J::boolean(ok) + "}", "application/json");
226
+ });
227
+
228
+ // ── POST /api/feed/step?n=N ──────────────────────────────────────────────
229
+ // Advance feed by N ticks, return the ticks
230
+ static std::atomic<size_t> g_feed_idx{0};
231
+ svr.Post("/api/feed/step", [&g_feed_idx](const httplib::Request& req, httplib::Response& res) {
232
+ int n = 1;
233
+ if (req.has_param("n")) {
234
+ try { n = std::stoi(req.get_param_value("n")); } catch(...) {}
235
+ }
236
+ n = std::min(n, 100);
237
+
238
+ std::string body = "[";
239
+ bool first = true;
240
+ for (int i = 0; i < n; ++i) {
241
+ size_t idx = g_feed_idx.fetch_add(1) % g_tick_feed.size();
242
+ auto& tick = g_tick_feed[idx];
243
+ g_sim->on_tick(tick);
244
+ if (!first) body += ",";
245
+ body += "{\"symbol\":" + J::str(tick.symbol) +
246
+ ",\"bid\":" + J::num(from_price(tick.bid_price)) +
247
+ ",\"ask\":" + J::num(from_price(tick.ask_price)) +
248
+ ",\"last\":" + J::num(from_price(tick.last_price)) +
249
+ ",\"bid_qty\":" + J::num(tick.bid_qty) +
250
+ ",\"ask_qty\":" + J::num(tick.ask_qty) +
251
+ ",\"ts\":" + J::num(tick.timestamp) + "}";
252
+ first = false;
253
+ }
254
+ body += "]";
255
+ res.set_content(body, "application/json");
256
+ });
257
+
258
+ // ── POST /api/backtest ───────────────────────────────────────────────────
259
+ svr.Post("/api/backtest", [](const httplib::Request& req, httplib::Response& res) {
260
+ std::string strategy_name = json_get(req.body, "strategy");
261
+ std::string symbol = json_get(req.body, "symbol");
262
+ int ticks = 5000;
263
+ try { ticks = std::stoi(json_get(req.body, "ticks")); } catch(...) {}
264
+ ticks = std::min(std::max(ticks, 100), 50000);
265
+
266
+ double start_price = 150.0;
267
+ try { start_price = std::stod(json_get(req.body, "start_price")); } catch(...) {}
268
+
269
+ if (symbol.empty()) symbol = "AAPL";
270
+
271
+ // Generate fresh tick set
272
+ MarketDataParser::Config cfg;
273
+ cfg.symbol = symbol;
274
+ cfg.start_price = start_price;
275
+ cfg.ticks = ticks;
276
+ cfg.volatility = 0.001;
277
+ cfg.seed = 123;
278
+ auto bt_ticks = MarketDataParser::generate_synthetic(cfg);
279
+
280
+ RiskLimits rl;
281
+ rl.max_order_qty = 10000;
282
+ rl.max_position = 200000;
283
+ rl.max_notional_usd = 100000000.0;
284
+ rl.max_orders_per_sec = 5000;
285
+
286
+ Backtester bt(rl);
287
+ bt.add_symbol(symbol);
288
+
289
+ std::unique_ptr<IStrategy> strat;
290
+ if (strategy_name == "Momentum")
291
+ strat = std::make_unique<MomentumStrategy>();
292
+ else if (strategy_name == "MeanReversion")
293
+ strat = std::make_unique<MeanReversionStrategy>();
294
+ else
295
+ strat = std::make_unique<MarketMakingStrategy>();
296
+ bt.set_strategy(std::move(strat));
297
+
298
+ auto result = bt.run(bt_ticks, false);
299
+ res.set_content(backtest_result_to_json(result), "application/json");
300
+ });
301
+
302
+ // ── POST /api/risk/limits ────────────────────────────────────────────────
303
+ svr.Post("/api/risk/limits", [](const httplib::Request& req, httplib::Response& res) {
304
+ try {
305
+ auto& b = req.body;
306
+ g_limits.max_order_qty = std::stoll(json_get(b, "max_order_qty"));
307
+ g_limits.max_position = std::stoll(json_get(b, "max_position"));
308
+ g_limits.max_notional_usd = std::stod(json_get(b, "max_notional_usd"));
309
+ g_limits.max_orders_per_sec = std::stoi(json_get(b, "max_orders_per_sec"));
310
+ g_sim->risk().set_limits(g_limits);
311
+ res.set_content("{\"ok\":true}", "application/json");
312
+ } catch (...) {
313
+ res.status = 400;
314
+ res.set_content("{\"error\":\"bad limits\"}", "application/json");
315
+ }
316
+ });
317
+
318
+ // ── GET /api/risk/limits ──────────────────────────────────────────────────
319
+ svr.Get("/api/risk/limits", [](const httplib::Request&, httplib::Response& res) {
320
+ auto l = g_sim->risk().get_limits();
321
+ res.set_content(
322
+ "{\"max_order_qty\":" + J::num(l.max_order_qty) +
323
+ ",\"max_position\":" + J::num(l.max_position) +
324
+ ",\"max_notional_usd\":" + J::num(l.max_notional_usd, 0) +
325
+ ",\"max_orders_per_sec\":" + J::num((int64_t)l.max_orders_per_sec) +
326
+ ",\"enabled\":" + J::boolean(l.enabled) + "}",
327
+ "application/json");
328
+ });
329
+
330
+ // ── POST /api/reset ───────────────────────────────────────────────────────
331
+ svr.Post("/api/reset", [](const httplib::Request&, httplib::Response& res) {
332
+ {
333
+ std::lock_guard<std::mutex> lk(g_log_mtx);
334
+ g_recent_trades.clear();
335
+ g_recent_orders.clear();
336
+ }
337
+ reset_sim();
338
+ res.set_content("{\"reset\":true}", "application/json");
339
+ });
340
+
341
+ // ── GET /api/symbols ───────────────────────────────────────────────────────
342
+ svr.Get("/api/symbols", [](const httplib::Request&, httplib::Response& res) {
343
+ auto syms = g_sim->symbols();
344
+ std::string body = "[";
345
+ for (size_t i = 0; i < syms.size(); ++i) {
346
+ if (i) body += ",";
347
+ body += J::str(syms[i]);
348
+ }
349
+ body += "]";
350
+ res.set_content(body, "application/json");
351
+ });
352
+
353
+ // ── Serve frontend ────────────────────────────────────────────────────────
354
+ svr.set_mount_point("/", "./frontend/dist");
355
+ // Fallback for SPA
356
+ svr.Get("/.*", [](const httplib::Request&, httplib::Response& res) {
357
+ std::ifstream f("./frontend/dist/index.html");
358
+ if (f.good()) {
359
+ std::string content((std::istreambuf_iterator<char>(f)),
360
+ std::istreambuf_iterator<char>());
361
+ res.set_content(content, "text/html");
362
+ } else {
363
+ res.status = 404;
364
+ }
365
+ });
366
+
367
+ std::cout << "╔══════════════════════════════════════════╗\n";
368
+ std::cout << "║ HFT Exchange Simulator ║\n";
369
+ std::cout << "║ Listening on http://0.0.0.0:" << port << " ║\n";
370
+ std::cout << "╚══════════════════════════════════════════╝\n";
371
+
372
+ svr.listen("0.0.0.0", port);
373
+ return 0;
374
+ }
src/network/serializer.hpp ADDED
@@ -0,0 +1,133 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #pragma once
2
+ #include "core/simulator.hpp"
3
+ #include "core/types.hpp"
4
+ #include <string>
5
+ #include <sstream>
6
+ #include <iomanip>
7
+
8
+ namespace hft {
9
+
10
+ struct J {
11
+ static std::string str(const std::string& s) {
12
+ std::ostringstream o; o << "\"";
13
+ for (char c : s) {
14
+ if (c == '"') o << "\\\"";
15
+ else if (c == '\\') o << "\\\\";
16
+ else o << c;
17
+ }
18
+ o << "\""; return o.str();
19
+ }
20
+ static std::string num(double v, int prec = 4) {
21
+ std::ostringstream o; o << std::fixed << std::setprecision(prec) << v; return o.str();
22
+ }
23
+ static std::string num(int64_t v) { return std::to_string(v); }
24
+ static std::string num(uint64_t v) { return std::to_string(v); }
25
+ static std::string boolean(bool b) { return b ? "true" : "false"; }
26
+ };
27
+
28
+ inline std::string order_to_json(const Order& o) {
29
+ return "{\"id\":" + J::num(o.id) +
30
+ ",\"symbol\":" + J::str(o.symbol) +
31
+ ",\"side\":" + J::str(o.side == Side::BUY ? "BUY" : "SELL") +
32
+ ",\"type\":" + J::str([&]{ switch(o.type){
33
+ case OrderType::LIMIT: return "LIMIT";
34
+ case OrderType::MARKET: return "MARKET";
35
+ case OrderType::IOC: return "IOC";
36
+ case OrderType::FOK: return "FOK";
37
+ } return "LIMIT"; }()) +
38
+ ",\"price\":" + J::num(from_price(o.price)) +
39
+ ",\"qty\":" + J::num(o.qty) +
40
+ ",\"filled_qty\":" + J::num(o.filled_qty) +
41
+ ",\"status\":" + J::str([&]{ switch(o.status){
42
+ case OrderStatus::PENDING: return "PENDING";
43
+ case OrderStatus::NEW: return "NEW";
44
+ case OrderStatus::PARTIALLY_FILLED: return "PARTIALLY_FILLED";
45
+ case OrderStatus::FILLED: return "FILLED";
46
+ case OrderStatus::CANCELLED: return "CANCELLED";
47
+ case OrderStatus::REJECTED: return "REJECTED";
48
+ case OrderStatus::EXPIRED: return "EXPIRED";
49
+ } return "UNKNOWN"; }()) +
50
+ ",\"timestamp\":" + J::num(o.timestamp) + "}";
51
+ }
52
+
53
+ inline std::string trade_to_json(const Trade& t) {
54
+ return "{\"trade_id\":" + J::num(t.trade_id) +
55
+ ",\"buy_order_id\":" + J::num(t.buy_order_id) +
56
+ ",\"sell_order_id\":" + J::num(t.sell_order_id) +
57
+ ",\"symbol\":" + J::str(t.symbol) +
58
+ ",\"price\":" + J::num(from_price(t.price)) +
59
+ ",\"qty\":" + J::num(t.qty) +
60
+ ",\"timestamp\":" + J::num(t.timestamp) + "}";
61
+ }
62
+
63
+ inline std::string snapshot_to_json(const OrderBookSnapshot& s, int depth = 10) {
64
+ std::string bids = "[";
65
+ for (int i = 0; i < (int)s.bids.size() && i < depth; ++i) {
66
+ if (i) bids += ",";
67
+ bids += "{\"price\":" + J::num(from_price(s.bids[i].price)) +
68
+ ",\"qty\":" + J::num(s.bids[i].qty) +
69
+ ",\"orders\":" + J::num((int64_t)s.bids[i].order_count) + "}";
70
+ }
71
+ bids += "]";
72
+ std::string asks = "[";
73
+ for (int i = 0; i < (int)s.asks.size() && i < depth; ++i) {
74
+ if (i) asks += ",";
75
+ asks += "{\"price\":" + J::num(from_price(s.asks[i].price)) +
76
+ ",\"qty\":" + J::num(s.asks[i].qty) +
77
+ ",\"orders\":" + J::num((int64_t)s.asks[i].order_count) + "}";
78
+ }
79
+ asks += "]";
80
+ return "{\"symbol\":" + J::str(s.symbol) +
81
+ ",\"timestamp\":" + J::num(s.timestamp) +
82
+ ",\"best_bid\":" + J::num(from_price(s.best_bid())) +
83
+ ",\"best_ask\":" + J::num(from_price(s.best_ask())) +
84
+ ",\"mid\":" + J::num(from_price(s.mid())) +
85
+ ",\"spread\":" + J::num(s.spread(), 6) +
86
+ ",\"bids\":" + bids +
87
+ ",\"asks\":" + asks + "}";
88
+ }
89
+
90
+ inline std::string position_to_json(const Position& p) {
91
+ return "{\"symbol\":" + J::str(p.symbol) +
92
+ ",\"net_qty\":" + J::num(p.net_qty) +
93
+ ",\"avg_price\":" + J::num(p.avg_price) +
94
+ ",\"realized_pnl\":" + J::num(p.realized_pnl) +
95
+ ",\"unrealized_pnl\":" + J::num(p.unrealized_pnl) +
96
+ ",\"notional\":" + J::num(p.notional()) + "}";
97
+ }
98
+
99
+ inline std::string backtest_result_to_json(const BacktestResult& r) {
100
+ std::string equity = "[";
101
+ for (size_t i = 0; i < r.equity_curve.size(); ++i) {
102
+ if (i) equity += ",";
103
+ equity += J::num(r.equity_curve[i]);
104
+ }
105
+ equity += "]";
106
+ std::string trades_str = "[";
107
+ for (size_t i = 0; i < r.trades.size() && i < 500; ++i) {
108
+ if (i) trades_str += ",";
109
+ trades_str += trade_to_json(r.trades[i]);
110
+ }
111
+ trades_str += "]";
112
+ return "{\"strategy\":" + J::str(r.strategy_name) +
113
+ ",\"ticks_processed\":" + J::num(r.ticks_processed) +
114
+ ",\"orders_submitted\":" + J::num(r.orders_submitted) +
115
+ ",\"orders_filled\":" + J::num(r.orders_filled) +
116
+ ",\"orders_rejected\":" + J::num(r.orders_rejected) +
117
+ ",\"trades_count\":" + J::num(r.trades_count) +
118
+ ",\"total_pnl\":" + J::num(r.total_pnl) +
119
+ ",\"realized_pnl\":" + J::num(r.realized_pnl) +
120
+ ",\"unrealized_pnl\":" + J::num(r.unrealized_pnl) +
121
+ ",\"max_drawdown\":" + J::num(r.max_drawdown) +
122
+ ",\"sharpe\":" + J::num(r.sharpe) +
123
+ ",\"order_p50_us\":" + J::num(r.order_latency.p50_ns / 1000.0) +
124
+ ",\"order_p99_us\":" + J::num(r.order_latency.p99_ns / 1000.0) +
125
+ ",\"order_avg_us\":" + J::num(r.order_latency.avg_us()) +
126
+ ",\"tick_p50_us\":" + J::num(r.tick_latency.p50_ns / 1000.0) +
127
+ ",\"tick_p99_us\":" + J::num(r.tick_latency.p99_ns / 1000.0) +
128
+ ",\"throughput_eps\":" + J::num(r.throughput_eps) +
129
+ ",\"equity_curve\":" + equity +
130
+ ",\"trades\":" + trades_str + "}";
131
+ }
132
+
133
+ } // namespace hft
src/risk/risk_manager.cpp ADDED
@@ -0,0 +1,135 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #include "risk/risk_manager.hpp"
2
+ #include <cmath>
3
+ #include <algorithm>
4
+
5
+ namespace hft {
6
+
7
+ RiskManager::RiskManager(RiskLimits limits) : limits_(std::move(limits)) {}
8
+
9
+ int RiskManager::orders_in_last_second() {
10
+ int64_t now = now_ns();
11
+ int64_t cutoff = now - 1'000'000'000LL;
12
+ while (!order_timestamps_ns_.empty() && order_timestamps_ns_.front() < cutoff)
13
+ order_timestamps_ns_.pop_front();
14
+ return (int)order_timestamps_ns_.size();
15
+ }
16
+
17
+ RiskResult RiskManager::check_order(const Order& order, double current_price) {
18
+ std::lock_guard<std::mutex> lock(mtx_);
19
+ ++stats_checked_;
20
+
21
+ if (!limits_.enabled) {
22
+ order_timestamps_ns_.push_back(now_ns());
23
+ return {true, RiskRejectReason::NONE, "OK"};
24
+ }
25
+
26
+ if (order.qty <= 0)
27
+ return (++stats_rejected_, RiskResult{false, RiskRejectReason::INVALID_QTY, "Quantity must be positive"});
28
+
29
+ if (order.type == OrderType::LIMIT && order.price <= 0)
30
+ return (++stats_rejected_, RiskResult{false, RiskRejectReason::INVALID_PRICE, "Limit price must be positive"});
31
+
32
+ if (order.qty > limits_.max_order_qty)
33
+ return (++stats_rejected_, RiskResult{false, RiskRejectReason::MAX_ORDER_QTY,
34
+ "Order qty " + std::to_string(order.qty) + " > limit " + std::to_string(limits_.max_order_qty)});
35
+
36
+ double price_usd = (current_price > 0) ? current_price : from_price(order.price);
37
+ double notional = price_usd * order.qty;
38
+ if (notional > limits_.max_notional_usd)
39
+ return (++stats_rejected_, RiskResult{false, RiskRejectReason::MAX_NOTIONAL,
40
+ "Notional $" + std::to_string(notional) + " > limit $" + std::to_string(limits_.max_notional_usd)});
41
+
42
+ auto pit = positions_.find(order.symbol);
43
+ Quantity cur_pos = (pit != positions_.end()) ? pit->second.net_qty : 0;
44
+ Quantity projected = cur_pos + (order.side == Side::BUY ? order.qty : -order.qty);
45
+ if (std::abs(projected) > limits_.max_position)
46
+ return (++stats_rejected_, RiskResult{false, RiskRejectReason::MAX_POSITION,
47
+ "Projected position " + std::to_string(projected) + " > limit " + std::to_string(limits_.max_position)});
48
+
49
+ // Rate check — only count approved orders toward the rate limit
50
+ int rate = orders_in_last_second();
51
+ if (rate >= limits_.max_orders_per_sec)
52
+ return (++stats_rejected_, RiskResult{false, RiskRejectReason::MAX_ORDER_RATE,
53
+ "Order rate " + std::to_string(rate) + "/s >= limit " + std::to_string(limits_.max_orders_per_sec)});
54
+
55
+ // All checks passed — record timestamp for rate limiting
56
+ order_timestamps_ns_.push_back(now_ns());
57
+ return {true, RiskRejectReason::NONE, "OK"};
58
+ }
59
+
60
+ void RiskManager::on_fill(const std::string& symbol, Side side, Quantity qty, double fill_price) {
61
+ std::lock_guard<std::mutex> lock(mtx_);
62
+ auto& pos = positions_[symbol];
63
+ pos.symbol = symbol;
64
+
65
+ int64_t signed_qty = (side == Side::BUY) ? (int64_t)qty : -(int64_t)qty;
66
+ bool adding = (pos.net_qty == 0) ||
67
+ (side == Side::BUY && pos.net_qty > 0) ||
68
+ (side == Side::SELL && pos.net_qty < 0);
69
+
70
+ if (adding) {
71
+ double total_cost = pos.avg_price * std::abs((double)pos.net_qty) + fill_price * qty;
72
+ pos.net_qty += signed_qty;
73
+ pos.avg_price = (pos.net_qty != 0)
74
+ ? total_cost / std::abs((double)pos.net_qty)
75
+ : 0.0;
76
+ } else {
77
+ Quantity closing = std::min(qty, (Quantity)std::abs(pos.net_qty));
78
+ Quantity opening = qty - closing;
79
+
80
+ double realized = (fill_price - pos.avg_price) * closing *
81
+ (pos.net_qty > 0 ? 1.0 : -1.0);
82
+ pos.realized_pnl += realized;
83
+ pos.net_qty += signed_qty;
84
+
85
+ if (pos.net_qty == 0) {
86
+ pos.avg_price = 0.0;
87
+ } else if (opening > 0) {
88
+ pos.avg_price = fill_price;
89
+ }
90
+ }
91
+
92
+ if (pos.net_qty != 0) {
93
+ double mkt = market_prices_.count(symbol) ? market_prices_.at(symbol) : fill_price;
94
+ pos.unrealized_pnl = (mkt - pos.avg_price) * pos.net_qty;
95
+ } else {
96
+ pos.unrealized_pnl = 0.0;
97
+ }
98
+ }
99
+
100
+ void RiskManager::update_market_price(const std::string& sym, double px) {
101
+ std::lock_guard<std::mutex> lock(mtx_);
102
+ market_prices_[sym] = px;
103
+ if (positions_.count(sym)) {
104
+ auto& pos = positions_[sym];
105
+ pos.unrealized_pnl = (pos.net_qty != 0)
106
+ ? (px - pos.avg_price) * pos.net_qty
107
+ : 0.0;
108
+ }
109
+ }
110
+
111
+ void RiskManager::on_cancel(const std::string& symbol, const Order& order) {
112
+ (void)symbol; (void)order;
113
+ }
114
+
115
+ Position RiskManager::get_position(const std::string& symbol) const {
116
+ std::lock_guard<std::mutex> lock(mtx_);
117
+ auto it = positions_.find(symbol);
118
+ if (it == positions_.end()) return Position{symbol, 0, 0, 0, 0};
119
+ return it->second;
120
+ }
121
+
122
+ std::unordered_map<std::string, Position> RiskManager::all_positions() const {
123
+ std::lock_guard<std::mutex> lock(mtx_);
124
+ return positions_;
125
+ }
126
+
127
+ double RiskManager::total_pnl() const {
128
+ std::lock_guard<std::mutex> lock(mtx_);
129
+ double total = 0;
130
+ for (auto& [sym, pos] : positions_)
131
+ total += pos.realized_pnl + pos.unrealized_pnl;
132
+ return total;
133
+ }
134
+
135
+ } // namespace hft
src/risk/risk_manager.hpp ADDED
@@ -0,0 +1,63 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #pragma once
2
+ #include "core/types.hpp"
3
+ #include <unordered_map>
4
+ #include <string>
5
+ #include <deque>
6
+ #include <mutex>
7
+
8
+ namespace hft {
9
+
10
+ enum class RiskRejectReason {
11
+ NONE,
12
+ MAX_ORDER_QTY,
13
+ MAX_POSITION,
14
+ MAX_NOTIONAL,
15
+ MAX_ORDER_RATE,
16
+ INVALID_PRICE,
17
+ INVALID_QTY,
18
+ SYMBOL_NOT_ALLOWED
19
+ };
20
+
21
+ struct RiskResult {
22
+ bool approved = false;
23
+ RiskRejectReason reason = RiskRejectReason::NONE;
24
+ std::string message;
25
+ };
26
+
27
+ class RiskManager {
28
+ public:
29
+ explicit RiskManager(RiskLimits limits);
30
+
31
+ RiskResult check_order(const Order& order, double current_price = 0.0);
32
+ void on_fill(const std::string& symbol, Side side, Quantity qty, double fill_price);
33
+ void on_cancel(const std::string& symbol, const Order& order);
34
+
35
+ Position get_position(const std::string& symbol) const;
36
+ std::unordered_map<std::string, Position> all_positions() const;
37
+ double total_pnl() const;
38
+
39
+ void set_limits(RiskLimits l) { limits_ = l; }
40
+ RiskLimits get_limits() const { return limits_; }
41
+ void update_market_price(const std::string& sym, double px);
42
+
43
+ // Stats
44
+ int64_t orders_checked() const { return stats_checked_; }
45
+ int64_t orders_rejected() const { return stats_rejected_; }
46
+
47
+ private:
48
+ RiskLimits limits_;
49
+ mutable std::mutex mtx_;
50
+
51
+ std::unordered_map<std::string, Position> positions_;
52
+ std::unordered_map<std::string, double> market_prices_;
53
+
54
+ // Order rate tracking (sliding window 1 sec)
55
+ std::deque<int64_t> order_timestamps_ns_;
56
+
57
+ int64_t stats_checked_ = 0;
58
+ int64_t stats_rejected_ = 0;
59
+
60
+ int orders_in_last_second();
61
+ };
62
+
63
+ } // namespace hft
src/strategy/strategy.hpp ADDED
@@ -0,0 +1,239 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #pragma once
2
+ #include "core/types.hpp"
3
+ #include <vector>
4
+ #include <string>
5
+ #include <functional>
6
+ #include <memory>
7
+ #include <deque>
8
+ #include <cmath>
9
+
10
+ namespace hft {
11
+
12
+ struct StrategySignal {
13
+ enum class Action { NONE, BUY, SELL, CANCEL, CANCEL_ALL } action = Action::NONE;
14
+ std::string symbol;
15
+ OrderType type = OrderType::LIMIT;
16
+ Price price = 0;
17
+ Quantity qty = 0;
18
+ OrderId cancel_id = 0;
19
+ std::string reason;
20
+ };
21
+
22
+ class IStrategy {
23
+ public:
24
+ virtual ~IStrategy() = default;
25
+ virtual std::string name() const = 0;
26
+ virtual void on_init(const std::string& symbol) { (void)symbol; }
27
+ virtual std::vector<StrategySignal> on_tick(const MarketDataTick& tick,
28
+ const OrderBookSnapshot& book) = 0;
29
+ virtual void on_order_update(const Order& order) { (void)order; }
30
+ virtual void on_trade(const Trade& trade) { (void)trade; }
31
+ virtual std::string summary() const { return ""; }
32
+ };
33
+
34
+ class MarketMakingStrategy : public IStrategy {
35
+ public:
36
+ explicit MarketMakingStrategy(double spread_bps = 5.0, Quantity qty = 100)
37
+ : spread_bps_(spread_bps), qty_(qty) {}
38
+
39
+ std::string name() const override { return "MarketMaking"; }
40
+
41
+ std::vector<StrategySignal> on_tick(const MarketDataTick& tick,
42
+ const OrderBookSnapshot& book) override {
43
+ std::vector<StrategySignal> sigs;
44
+ if (book.bids.empty() || book.asks.empty()) return sigs;
45
+
46
+ double mid = from_price(book.mid());
47
+ double half = mid * spread_bps_ / 20000.0;
48
+
49
+ if (bid_id_) {
50
+ StrategySignal s;
51
+ s.action = StrategySignal::Action::CANCEL;
52
+ s.cancel_id = bid_id_;
53
+ sigs.push_back(s);
54
+ bid_id_ = 0;
55
+ }
56
+ if (ask_id_) {
57
+ StrategySignal s;
58
+ s.action = StrategySignal::Action::CANCEL;
59
+ s.cancel_id = ask_id_;
60
+ sigs.push_back(s);
61
+ ask_id_ = 0;
62
+ }
63
+
64
+ StrategySignal buy;
65
+ buy.action = StrategySignal::Action::BUY;
66
+ buy.symbol = tick.symbol;
67
+ buy.type = OrderType::LIMIT;
68
+ buy.price = to_price(mid - half);
69
+ buy.qty = qty_;
70
+ buy.reason = "MM_BID";
71
+ sigs.push_back(buy);
72
+
73
+ StrategySignal sell;
74
+ sell.action = StrategySignal::Action::SELL;
75
+ sell.symbol = tick.symbol;
76
+ sell.type = OrderType::LIMIT;
77
+ sell.price = to_price(mid + half);
78
+ sell.qty = qty_;
79
+ sell.reason = "MM_ASK";
80
+ sigs.push_back(sell);
81
+
82
+ return sigs;
83
+ }
84
+
85
+ void on_order_update(const Order& order) override {
86
+ if (order.status == OrderStatus::REJECTED ||
87
+ order.status == OrderStatus::CANCELLED ||
88
+ order.status == OrderStatus::EXPIRED) {
89
+ if (order.id == bid_id_) bid_id_ = 0;
90
+ if (order.id == ask_id_) ask_id_ = 0;
91
+ return;
92
+ }
93
+ if (order.status == OrderStatus::NEW) {
94
+ if (order.side == Side::BUY) bid_id_ = order.id;
95
+ if (order.side == Side::SELL) ask_id_ = order.id;
96
+ }
97
+ if (order.status == OrderStatus::FILLED) {
98
+ ++fills_;
99
+ if (order.id == bid_id_) bid_id_ = 0;
100
+ if (order.id == ask_id_) ask_id_ = 0;
101
+ }
102
+ }
103
+
104
+ std::string summary() const override {
105
+ return "MarketMaking fills=" + std::to_string(fills_);
106
+ }
107
+
108
+ private:
109
+ double spread_bps_;
110
+ Quantity qty_;
111
+ OrderId bid_id_ = 0;
112
+ OrderId ask_id_ = 0;
113
+ int fills_ = 0;
114
+ };
115
+
116
+ class MomentumStrategy : public IStrategy {
117
+ public:
118
+ explicit MomentumStrategy(int lookback = 20, Quantity qty = 200)
119
+ : lookback_(lookback), qty_(qty) {}
120
+
121
+ std::string name() const override { return "Momentum"; }
122
+
123
+ std::vector<StrategySignal> on_tick(const MarketDataTick& tick,
124
+ const OrderBookSnapshot& book) override {
125
+ std::vector<StrategySignal> sigs;
126
+ double mid = from_price(tick.last_price);
127
+ prices_.push_back(mid);
128
+ if ((int)prices_.size() > lookback_ * 2) prices_.pop_front();
129
+ if ((int)prices_.size() < lookback_) return sigs;
130
+
131
+ int n = (int)prices_.size();
132
+ double fast = 0, slow = 0;
133
+ int fast_n = lookback_ / 2;
134
+ for (int i = n - fast_n; i < n; ++i) fast += prices_[i];
135
+ fast /= fast_n;
136
+ for (int i = n - lookback_; i < n; ++i) slow += prices_[i];
137
+ slow /= lookback_;
138
+
139
+ if (fast > slow * 1.0002 && !long_) {
140
+ StrategySignal s;
141
+ s.action = StrategySignal::Action::BUY;
142
+ s.symbol = tick.symbol;
143
+ s.type = OrderType::MARKET;
144
+ s.qty = qty_;
145
+ s.reason = "MOMENTUM_LONG";
146
+ sigs.push_back(s);
147
+ long_ = true;
148
+ ++signals_;
149
+ } else if (fast < slow * 0.9998 && long_) {
150
+ StrategySignal s;
151
+ s.action = StrategySignal::Action::SELL;
152
+ s.symbol = tick.symbol;
153
+ s.type = OrderType::MARKET;
154
+ s.qty = qty_;
155
+ s.reason = "MOMENTUM_FLAT";
156
+ sigs.push_back(s);
157
+ long_ = false;
158
+ ++signals_;
159
+ }
160
+ return sigs;
161
+ }
162
+
163
+ std::string summary() const override {
164
+ return "Momentum signals=" + std::to_string(signals_);
165
+ }
166
+
167
+ private:
168
+ int lookback_;
169
+ Quantity qty_;
170
+ std::deque<double> prices_;
171
+ bool long_ = false;
172
+ int signals_ = 0;
173
+ };
174
+
175
+ class MeanReversionStrategy : public IStrategy {
176
+ public:
177
+ explicit MeanReversionStrategy(int window = 30, double z_entry = 2.0, Quantity qty = 100)
178
+ : window_(window), z_entry_(z_entry), qty_(qty) {}
179
+
180
+ std::string name() const override { return "MeanReversion"; }
181
+
182
+ std::vector<StrategySignal> on_tick(const MarketDataTick& tick,
183
+ const OrderBookSnapshot& book) override {
184
+ std::vector<StrategySignal> sigs;
185
+ double px = from_price(tick.last_price);
186
+ buf_.push_back(px);
187
+ if ((int)buf_.size() > window_) buf_.pop_front();
188
+ if ((int)buf_.size() < window_) return sigs;
189
+
190
+ double mean = 0;
191
+ for (double v : buf_) mean += v;
192
+ mean /= window_;
193
+
194
+ double var = 0;
195
+ for (double v : buf_) var += (v - mean) * (v - mean);
196
+ double stddev = std::sqrt(var / window_);
197
+ if (stddev < 1e-9) return sigs;
198
+
199
+ double z = (px - mean) / stddev;
200
+
201
+ if (z < -z_entry_ && !in_trade_) {
202
+ StrategySignal s;
203
+ s.action = StrategySignal::Action::BUY;
204
+ s.symbol = tick.symbol;
205
+ s.type = OrderType::LIMIT;
206
+ s.price = tick.ask_price;
207
+ s.qty = qty_;
208
+ s.reason = "MR_LONG z=" + std::to_string(z).substr(0, 6);
209
+ sigs.push_back(s);
210
+ in_trade_ = true;
211
+ } else if (z > z_entry_ && !in_trade_) {
212
+ StrategySignal s;
213
+ s.action = StrategySignal::Action::SELL;
214
+ s.symbol = tick.symbol;
215
+ s.type = OrderType::LIMIT;
216
+ s.price = tick.bid_price;
217
+ s.qty = qty_;
218
+ s.reason = "MR_SHORT z=" + std::to_string(z).substr(0, 6);
219
+ sigs.push_back(s);
220
+ in_trade_ = true;
221
+ } else if (std::abs(z) < 0.3 && in_trade_) {
222
+ StrategySignal s;
223
+ s.action = StrategySignal::Action::CANCEL_ALL;
224
+ s.reason = "MR_EXIT";
225
+ sigs.push_back(s);
226
+ in_trade_ = false;
227
+ }
228
+ return sigs;
229
+ }
230
+
231
+ private:
232
+ int window_;
233
+ double z_entry_;
234
+ Quantity qty_;
235
+ std::deque<double> buf_;
236
+ bool in_trade_ = false;
237
+ };
238
+
239
+ } // namespace hft
tests/test_order_book.cpp ADDED
@@ -0,0 +1,415 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // tests/test_order_book.cpp
2
+ // Unit tests for the matching engine core
3
+ // Build: cmake --build build --target hft_tests && ./build/hft_tests
4
+
5
+ #include "core/order_book.hpp"
6
+ #include "core/types.hpp"
7
+ #include <cassert>
8
+ #include <iostream>
9
+ #include <string>
10
+ #include <vector>
11
+ #include <cmath>
12
+
13
+ using namespace hft;
14
+
15
+ static int tests_passed = 0;
16
+ static int tests_failed = 0;
17
+
18
+ #define TEST(name) \
19
+ void name(); \
20
+ struct name##_register { name##_register() { std::cout << " " << #name << "... "; name(); } } name##_instance; \
21
+ void name()
22
+
23
+ #define ASSERT_EQ(a, b) do { \
24
+ if ((a) != (b)) { \
25
+ std::cerr << "FAIL: " << #a << " == " << (a) << ", expected " << (b) << " at line " << __LINE__ << "\n"; \
26
+ ++tests_failed; return; \
27
+ } \
28
+ } while(0)
29
+
30
+ #define ASSERT_TRUE(x) do { \
31
+ if (!(x)) { \
32
+ std::cerr << "FAIL: " << #x << " at line " << __LINE__ << "\n"; \
33
+ ++tests_failed; return; \
34
+ } \
35
+ } while(0)
36
+
37
+ #define PASS() do { std::cout << "PASS\n"; ++tests_passed; } while(0)
38
+
39
+ // ─── Price Conversion Tests ──────────────────────────────────────────────────
40
+
41
+ TEST(test_to_price_positive) {
42
+ ASSERT_EQ(to_price(185.50), 1855000);
43
+ ASSERT_EQ(to_price(0.0001), 1);
44
+ ASSERT_EQ(to_price(100.0), 1000000);
45
+ PASS();
46
+ }
47
+
48
+ TEST(test_to_price_negative) {
49
+ ASSERT_EQ(to_price(-1.5), -15000);
50
+ ASSERT_EQ(to_price(-0.0001), -1);
51
+ ASSERT_EQ(to_price(-100.0), -1000000);
52
+ PASS();
53
+ }
54
+
55
+ TEST(test_to_price_roundtrip) {
56
+ double original = 185.4567;
57
+ Price p = to_price(original);
58
+ double back = from_price(p);
59
+ ASSERT_TRUE(std::abs(back - original) < 0.00005);
60
+ PASS();
61
+ }
62
+
63
+ // ─── Order Book: Basic Insert ────────────────────────────────────────────────
64
+
65
+ TEST(test_add_limit_order_no_match) {
66
+ OrderBook book("AAPL");
67
+ Order buy;
68
+ buy.id = 1; buy.symbol = "AAPL"; buy.side = Side::BUY;
69
+ buy.type = OrderType::LIMIT; buy.price = to_price(185.0); buy.qty = 100;
70
+
71
+ auto trades = book.add_order(buy);
72
+ ASSERT_EQ(trades.size(), 0u);
73
+ ASSERT_EQ(book.order_count(), 1u);
74
+ ASSERT_EQ(book.bid_levels(), 1u);
75
+ ASSERT_EQ(book.ask_levels(), 0u);
76
+ ASSERT_EQ(book.best_bid(), to_price(185.0));
77
+ PASS();
78
+ }
79
+
80
+ TEST(test_multiple_levels) {
81
+ OrderBook book("AAPL");
82
+ Order o1, o2, o3;
83
+ o1.id = 1; o1.symbol = "AAPL"; o1.side = Side::BUY;
84
+ o1.type = OrderType::LIMIT; o1.price = to_price(185.0); o1.qty = 100;
85
+
86
+ o2.id = 2; o2.symbol = "AAPL"; o2.side = Side::BUY;
87
+ o2.type = OrderType::LIMIT; o2.price = to_price(184.0); o2.qty = 200;
88
+
89
+ o3.id = 3; o3.symbol = "AAPL"; o3.side = Side::SELL;
90
+ o3.type = OrderType::LIMIT; o3.price = to_price(186.0); o3.qty = 150;
91
+
92
+ book.add_order(o1);
93
+ book.add_order(o2);
94
+ book.add_order(o3);
95
+
96
+ ASSERT_EQ(book.bid_levels(), 2u);
97
+ ASSERT_EQ(book.ask_levels(), 1u);
98
+ ASSERT_EQ(book.best_bid(), to_price(185.0));
99
+ ASSERT_EQ(book.best_ask(), to_price(186.0));
100
+ PASS();
101
+ }
102
+
103
+ // ─── Order Book: Matching ────────────────────────────────────────────────────
104
+
105
+ TEST(test_limit_buy_matches_ask) {
106
+ OrderBook book("AAPL");
107
+
108
+ Order sell;
109
+ sell.id = 1; sell.symbol = "AAPL"; sell.side = Side::SELL;
110
+ sell.type = OrderType::LIMIT; sell.price = to_price(185.0); sell.qty = 100;
111
+ book.add_order(sell);
112
+
113
+ Order buy;
114
+ buy.id = 2; buy.symbol = "AAPL"; buy.side = Side::BUY;
115
+ buy.type = OrderType::LIMIT; buy.price = to_price(185.0); buy.qty = 100;
116
+ auto trades = book.add_order(buy);
117
+
118
+ ASSERT_EQ(trades.size(), 1u);
119
+ ASSERT_EQ(trades[0].qty, 100);
120
+ ASSERT_EQ(trades[0].price, to_price(185.0));
121
+ ASSERT_EQ(book.order_count(), 0u);
122
+ PASS();
123
+ }
124
+
125
+ TEST(test_limit_sell_matches_bid) {
126
+ OrderBook book("AAPL");
127
+
128
+ Order buy;
129
+ buy.id = 1; buy.symbol = "AAPL"; buy.side = Side::BUY;
130
+ buy.type = OrderType::LIMIT; buy.price = to_price(185.0); buy.qty = 100;
131
+ book.add_order(buy);
132
+
133
+ Order sell;
134
+ sell.id = 2; sell.symbol = "AAPL"; sell.side = Side::SELL;
135
+ sell.type = OrderType::LIMIT; sell.price = to_price(185.0); sell.qty = 100;
136
+ auto trades = book.add_order(sell);
137
+
138
+ ASSERT_EQ(trades.size(), 1u);
139
+ ASSERT_EQ(trades[0].qty, 100);
140
+ ASSERT_EQ(book.order_count(), 0u);
141
+ PASS();
142
+ }
143
+
144
+ TEST(test_partial_fill) {
145
+ OrderBook book("AAPL");
146
+
147
+ Order sell;
148
+ sell.id = 1; sell.symbol = "AAPL"; sell.side = Side::SELL;
149
+ sell.type = OrderType::LIMIT; sell.price = to_price(185.0); sell.qty = 200;
150
+ book.add_order(sell);
151
+
152
+ Order buy;
153
+ buy.id = 2; buy.symbol = "AAPL"; buy.side = Side::BUY;
154
+ buy.type = OrderType::LIMIT; buy.price = to_price(185.0); buy.qty = 50;
155
+ auto trades = book.add_order(buy);
156
+
157
+ ASSERT_EQ(trades.size(), 1u);
158
+ ASSERT_EQ(trades[0].qty, 50);
159
+ ASSERT_EQ(book.order_count(), 1u); // resting sell still has 150
160
+ ASSERT_EQ(book.ask_levels(), 1u);
161
+ PASS();
162
+ }
163
+
164
+ TEST(test_multi_level_sweep) {
165
+ OrderBook book("AAPL");
166
+
167
+ Order s1, s2;
168
+ s1.id = 1; s1.symbol = "AAPL"; s1.side = Side::SELL;
169
+ s1.type = OrderType::LIMIT; s1.price = to_price(185.0); s1.qty = 100;
170
+
171
+ s2.id = 2; s2.symbol = "AAPL"; s2.side = Side::SELL;
172
+ s2.type = OrderType::LIMIT; s2.price = to_price(186.0); s2.qty = 100;
173
+
174
+ book.add_order(s1);
175
+ book.add_order(s2);
176
+
177
+ // Aggressive buy sweeps both levels
178
+ Order buy;
179
+ buy.id = 3; buy.symbol = "AAPL"; buy.side = Side::BUY;
180
+ buy.type = OrderType::LIMIT; buy.price = to_price(187.0); buy.qty = 200;
181
+ auto trades = book.add_order(buy);
182
+
183
+ ASSERT_EQ(trades.size(), 2u);
184
+ ASSERT_EQ(trades[0].price, to_price(185.0)); // best ask first
185
+ ASSERT_EQ(trades[1].price, to_price(186.0));
186
+ ASSERT_EQ(book.order_count(), 0u);
187
+ ASSERT_EQ(book.ask_levels(), 0u);
188
+ PASS();
189
+ }
190
+
191
+ TEST(test_sell_side_multi_level_sweep) {
192
+ // This specifically tests the sell-side iterator fix (Bug #4)
193
+ OrderBook book("AAPL");
194
+
195
+ Order b1, b2, b3;
196
+ b1.id = 1; b1.symbol = "AAPL"; b1.side = Side::BUY;
197
+ b1.type = OrderType::LIMIT; b1.price = to_price(185.0); b1.qty = 100;
198
+
199
+ b2.id = 2; b2.symbol = "AAPL"; b2.side = Side::BUY;
200
+ b2.type = OrderType::LIMIT; b2.price = to_price(184.0); b2.qty = 100;
201
+
202
+ b3.id = 3; b3.symbol = "AAPL"; b3.side = Side::BUY;
203
+ b3.type = OrderType::LIMIT; b3.price = to_price(183.0); b3.qty = 100;
204
+
205
+ book.add_order(b1);
206
+ book.add_order(b2);
207
+ book.add_order(b3);
208
+
209
+ // Aggressive sell sweeps all 3 bid levels
210
+ Order sell;
211
+ sell.id = 4; sell.symbol = "AAPL"; sell.side = Side::SELL;
212
+ sell.type = OrderType::MARKET; sell.price = 0; sell.qty = 300;
213
+ auto trades = book.add_order(sell);
214
+
215
+ ASSERT_EQ(trades.size(), 3u);
216
+ ASSERT_EQ(trades[0].price, to_price(185.0)); // best bid first
217
+ ASSERT_EQ(trades[1].price, to_price(184.0));
218
+ ASSERT_EQ(trades[2].price, to_price(183.0));
219
+ ASSERT_EQ(book.bid_levels(), 0u);
220
+ ASSERT_EQ(book.order_count(), 0u);
221
+ PASS();
222
+ }
223
+
224
+ // ─── Order Types ─────────────────────────────────────────────────────────────
225
+
226
+ TEST(test_market_order_no_counterparty) {
227
+ OrderBook book("AAPL");
228
+
229
+ Order buy;
230
+ buy.id = 1; buy.symbol = "AAPL"; buy.side = Side::BUY;
231
+ buy.type = OrderType::MARKET; buy.price = 0; buy.qty = 100;
232
+ auto trades = book.add_order(buy);
233
+
234
+ ASSERT_EQ(trades.size(), 0u);
235
+ ASSERT_EQ(buy.status, OrderStatus::CANCELLED);
236
+ ASSERT_EQ(book.order_count(), 0u);
237
+ PASS();
238
+ }
239
+
240
+ TEST(test_ioc_partial_fill) {
241
+ OrderBook book("AAPL");
242
+
243
+ Order sell;
244
+ sell.id = 1; sell.symbol = "AAPL"; sell.side = Side::SELL;
245
+ sell.type = OrderType::LIMIT; sell.price = to_price(185.0); sell.qty = 50;
246
+ book.add_order(sell);
247
+
248
+ Order buy;
249
+ buy.id = 2; buy.symbol = "AAPL"; buy.side = Side::BUY;
250
+ buy.type = OrderType::IOC; buy.price = to_price(185.0); buy.qty = 100;
251
+ auto trades = book.add_order(buy);
252
+
253
+ ASSERT_EQ(trades.size(), 1u);
254
+ ASSERT_EQ(trades[0].qty, 50);
255
+ ASSERT_EQ(buy.status, OrderStatus::EXPIRED); // unfilled remainder expired
256
+ ASSERT_EQ(book.order_count(), 0u); // IOC not added to book
257
+ PASS();
258
+ }
259
+
260
+ TEST(test_fok_rejected_insufficient_liquidity) {
261
+ OrderBook book("AAPL");
262
+
263
+ Order sell;
264
+ sell.id = 1; sell.symbol = "AAPL"; sell.side = Side::SELL;
265
+ sell.type = OrderType::LIMIT; sell.price = to_price(185.0); sell.qty = 50;
266
+ book.add_order(sell);
267
+
268
+ Order buy;
269
+ buy.id = 2; buy.symbol = "AAPL"; buy.side = Side::BUY;
270
+ buy.type = OrderType::FOK; buy.price = to_price(185.0); buy.qty = 100;
271
+ auto trades = book.add_order(buy);
272
+
273
+ ASSERT_EQ(trades.size(), 0u);
274
+ ASSERT_EQ(buy.status, OrderStatus::EXPIRED);
275
+ ASSERT_EQ(book.order_count(), 1u); // resting sell untouched
276
+ PASS();
277
+ }
278
+
279
+ TEST(test_fok_filled_sufficient_liquidity) {
280
+ OrderBook book("AAPL");
281
+
282
+ Order sell;
283
+ sell.id = 1; sell.symbol = "AAPL"; sell.side = Side::SELL;
284
+ sell.type = OrderType::LIMIT; sell.price = to_price(185.0); sell.qty = 100;
285
+ book.add_order(sell);
286
+
287
+ Order buy;
288
+ buy.id = 2; buy.symbol = "AAPL"; buy.side = Side::BUY;
289
+ buy.type = OrderType::FOK; buy.price = to_price(185.0); buy.qty = 100;
290
+ auto trades = book.add_order(buy);
291
+
292
+ ASSERT_EQ(trades.size(), 1u);
293
+ ASSERT_EQ(trades[0].qty, 100);
294
+ ASSERT_EQ(book.order_count(), 0u);
295
+ PASS();
296
+ }
297
+
298
+ // ─── Cancel & Modify ─────────────────────────────────────────────────────────
299
+
300
+ TEST(test_cancel_order) {
301
+ OrderBook book("AAPL");
302
+
303
+ Order buy;
304
+ buy.id = 1; buy.symbol = "AAPL"; buy.side = Side::BUY;
305
+ buy.type = OrderType::LIMIT; buy.price = to_price(185.0); buy.qty = 100;
306
+ book.add_order(buy);
307
+
308
+ ASSERT_TRUE(book.cancel_order(1));
309
+ ASSERT_EQ(book.order_count(), 0u);
310
+ ASSERT_EQ(book.bid_levels(), 0u);
311
+ ASSERT_EQ(buy.status, OrderStatus::CANCELLED);
312
+ PASS();
313
+ }
314
+
315
+ TEST(test_cancel_nonexistent) {
316
+ OrderBook book("AAPL");
317
+ ASSERT_TRUE(!book.cancel_order(999));
318
+ PASS();
319
+ }
320
+
321
+ TEST(test_modify_order) {
322
+ OrderBook book("AAPL");
323
+
324
+ Order buy;
325
+ buy.id = 1; buy.symbol = "AAPL"; buy.side = Side::BUY;
326
+ buy.type = OrderType::LIMIT; buy.price = to_price(185.0); buy.qty = 100;
327
+ book.add_order(buy);
328
+
329
+ ASSERT_TRUE(book.modify_order(1, to_price(186.0), 200));
330
+ ASSERT_EQ(book.best_bid(), to_price(186.0));
331
+ PASS();
332
+ }
333
+
334
+ // ─── Snapshot ────────────────────────────────────────────────────────────────
335
+
336
+ TEST(test_snapshot_depth) {
337
+ OrderBook book("AAPL");
338
+ for (int i = 1; i <= 20; ++i) {
339
+ Order o;
340
+ o.id = i; o.symbol = "AAPL"; o.side = Side::BUY;
341
+ o.type = OrderType::LIMIT; o.price = to_price(185.0 - i * 0.01); o.qty = 100;
342
+ book.add_order(o);
343
+ }
344
+
345
+ auto snap = book.snapshot(5);
346
+ ASSERT_EQ((int)snap.bids.size(), 5); // only top 5
347
+ ASSERT_TRUE(snap.bids[0].price > snap.bids[1].price); // sorted descending
348
+ PASS();
349
+ }
350
+
351
+ TEST(test_snapshot_spread) {
352
+ OrderBook book("AAPL");
353
+ Order buy, sell;
354
+ buy.id = 1; buy.symbol = "AAPL"; buy.side = Side::BUY;
355
+ buy.type = OrderType::LIMIT; buy.price = to_price(185.0); buy.qty = 100;
356
+
357
+ sell.id = 2; sell.symbol = "AAPL"; sell.side = Side::SELL;
358
+ sell.type = OrderType::LIMIT; sell.price = to_price(185.10); sell.qty = 100;
359
+
360
+ book.add_order(buy);
361
+ book.add_order(sell);
362
+
363
+ auto snap = book.snapshot();
364
+ ASSERT_TRUE(snap.spread() > 0);
365
+ ASSERT_TRUE(std::abs(snap.spread() - 0.10) < 0.001);
366
+ PASS();
367
+ }
368
+
369
+ TEST(test_empty_book_spread) {
370
+ OrderBookSnapshot empty;
371
+ ASSERT_TRUE(empty.spread() == 0.0);
372
+ ASSERT_TRUE(empty.mid() == 0);
373
+ PASS();
374
+ }
375
+
376
+ // ─── FIFO Priority ──────────────────────────────────────────────────────────
377
+
378
+ TEST(test_fifo_priority) {
379
+ OrderBook book("AAPL");
380
+
381
+ // Two sells at same price — first should fill first
382
+ Order s1, s2;
383
+ s1.id = 1; s1.symbol = "AAPL"; s1.side = Side::SELL;
384
+ s1.type = OrderType::LIMIT; s1.price = to_price(185.0); s1.qty = 100;
385
+
386
+ s2.id = 2; s2.symbol = "AAPL"; s2.side = Side::SELL;
387
+ s2.type = OrderType::LIMIT; s2.price = to_price(185.0); s2.qty = 100;
388
+
389
+ book.add_order(s1);
390
+ book.add_order(s2);
391
+
392
+ Order buy;
393
+ buy.id = 3; buy.symbol = "AAPL"; buy.side = Side::BUY;
394
+ buy.type = OrderType::LIMIT; buy.price = to_price(185.0); buy.qty = 100;
395
+ auto trades = book.add_order(buy);
396
+
397
+ ASSERT_EQ(trades.size(), 1u);
398
+ ASSERT_EQ(trades[0].sell_order_id, 1u); // s1 filled first (FIFO)
399
+ ASSERT_EQ(book.order_count(), 1u); // s2 still resting
400
+ PASS();
401
+ }
402
+
403
+ // ─── Main ────────────────────────────────────────────────────────────────────
404
+
405
+ int main() {
406
+ std::cout << "\n=== Orderflow Replay Engine — Unit Tests ===\n\n";
407
+ // Tests run automatically via static initialization
408
+
409
+ std::cout << "\n─────────────────────────────────────────────\n";
410
+ std::cout << " Passed: " << tests_passed << "\n";
411
+ std::cout << " Failed: " << tests_failed << "\n";
412
+ std::cout << "─────────────────────────────────────────────\n\n";
413
+
414
+ return tests_failed > 0 ? 1 : 0;
415
+ }
tests/test_risk_manager.cpp ADDED
@@ -0,0 +1,241 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // tests/test_risk_manager.cpp
2
+ // Unit tests for the risk management gateway
3
+
4
+ #include "risk/risk_manager.hpp"
5
+ #include "core/types.hpp"
6
+ #include <cassert>
7
+ #include <iostream>
8
+ #include <string>
9
+ #include <thread>
10
+ #include <chrono>
11
+
12
+ using namespace hft;
13
+
14
+ static int tests_passed = 0;
15
+ static int tests_failed = 0;
16
+
17
+ #define TEST(name) \
18
+ void name(); \
19
+ struct name##_register { name##_register() { std::cout << " " << #name << "... "; name(); } } name##_instance; \
20
+ void name()
21
+
22
+ #define ASSERT_EQ(a, b) do { \
23
+ if ((a) != (b)) { \
24
+ std::cerr << "FAIL: " << #a << " == " << (a) << ", expected " << (b) << " at line " << __LINE__ << "\n"; \
25
+ ++tests_failed; return; \
26
+ } \
27
+ } while(0)
28
+
29
+ #define ASSERT_TRUE(x) do { \
30
+ if (!(x)) { \
31
+ std::cerr << "FAIL: " << #x << " at line " << __LINE__ << "\n"; \
32
+ ++tests_failed; return; \
33
+ } \
34
+ } while(0)
35
+
36
+ #define PASS() do { std::cout << "PASS\n"; ++tests_passed; } while(0)
37
+
38
+ static Order make_order(const std::string& sym, Side side, OrderType type,
39
+ double price, Quantity qty) {
40
+ Order o;
41
+ o.id = 1; o.symbol = sym; o.side = side; o.type = type;
42
+ o.price = to_price(price); o.qty = qty;
43
+ return o;
44
+ }
45
+
46
+ // ─── Basic Approval ──────────────────────────────────────────────────────────
47
+
48
+ TEST(test_valid_order_approved) {
49
+ RiskLimits limits;
50
+ limits.max_order_qty = 1000;
51
+ limits.max_position = 10000;
52
+ limits.max_notional_usd = 1000000;
53
+ limits.max_orders_per_sec = 100;
54
+ RiskManager rm(limits);
55
+
56
+ auto o = make_order("AAPL", Side::BUY, OrderType::LIMIT, 185.0, 100);
57
+ auto result = rm.check_order(o);
58
+
59
+ ASSERT_TRUE(result.approved);
60
+ ASSERT_EQ(rm.orders_checked(), 1);
61
+ ASSERT_EQ(rm.orders_rejected(), 0);
62
+ PASS();
63
+ }
64
+
65
+ // ─── Rejection Tests ─────────────────────────────────────────────────────────
66
+
67
+ TEST(test_reject_qty_too_large) {
68
+ RiskLimits limits;
69
+ limits.max_order_qty = 100;
70
+ RiskManager rm(limits);
71
+
72
+ auto o = make_order("AAPL", Side::BUY, OrderType::LIMIT, 185.0, 500);
73
+ auto result = rm.check_order(o);
74
+
75
+ ASSERT_TRUE(!result.approved);
76
+ ASSERT_EQ(result.reason, RiskRejectReason::MAX_ORDER_QTY);
77
+ PASS();
78
+ }
79
+
80
+ TEST(test_reject_zero_qty) {
81
+ RiskLimits limits;
82
+ RiskManager rm(limits);
83
+
84
+ auto o = make_order("AAPL", Side::BUY, OrderType::LIMIT, 185.0, 0);
85
+ auto result = rm.check_order(o);
86
+
87
+ ASSERT_TRUE(!result.approved);
88
+ ASSERT_EQ(result.reason, RiskRejectReason::INVALID_QTY);
89
+ PASS();
90
+ }
91
+
92
+ TEST(test_reject_negative_qty) {
93
+ RiskLimits limits;
94
+ RiskManager rm(limits);
95
+
96
+ auto o = make_order("AAPL", Side::BUY, OrderType::LIMIT, 185.0, -10);
97
+ auto result = rm.check_order(o);
98
+
99
+ ASSERT_TRUE(!result.approved);
100
+ ASSERT_EQ(result.reason, RiskRejectReason::INVALID_QTY);
101
+ PASS();
102
+ }
103
+
104
+ TEST(test_reject_negative_limit_price) {
105
+ RiskLimits limits;
106
+ RiskManager rm(limits);
107
+
108
+ auto o = make_order("AAPL", Side::BUY, OrderType::LIMIT, -5.0, 100);
109
+ auto result = rm.check_order(o);
110
+
111
+ ASSERT_TRUE(!result.approved);
112
+ ASSERT_EQ(result.reason, RiskRejectReason::INVALID_PRICE);
113
+ PASS();
114
+ }
115
+
116
+ TEST(test_reject_notional_too_large) {
117
+ RiskLimits limits;
118
+ limits.max_order_qty = 100000;
119
+ limits.max_notional_usd = 1000.0; // very low limit
120
+ RiskManager rm(limits);
121
+
122
+ auto o = make_order("AAPL", Side::BUY, OrderType::LIMIT, 185.0, 100);
123
+ // notional = 185 * 100 = 18500 > 1000
124
+ auto result = rm.check_order(o);
125
+
126
+ ASSERT_TRUE(!result.approved);
127
+ ASSERT_EQ(result.reason, RiskRejectReason::MAX_NOTIONAL);
128
+ PASS();
129
+ }
130
+
131
+ TEST(test_reject_position_limit) {
132
+ RiskLimits limits;
133
+ limits.max_order_qty = 10000;
134
+ limits.max_position = 100;
135
+ limits.max_notional_usd = 1e9;
136
+ limits.max_orders_per_sec = 10000;
137
+ RiskManager rm(limits);
138
+
139
+ // Fill a position of 90
140
+ rm.on_fill("AAPL", Side::BUY, 90, 185.0);
141
+
142
+ // Try to buy 20 more → projected = 110 > max 100
143
+ auto o = make_order("AAPL", Side::BUY, OrderType::LIMIT, 185.0, 20);
144
+ auto result = rm.check_order(o);
145
+
146
+ ASSERT_TRUE(!result.approved);
147
+ ASSERT_EQ(result.reason, RiskRejectReason::MAX_POSITION);
148
+ PASS();
149
+ }
150
+
151
+ // ─── Rate Limiter ────────────────────────────────────────────────────────────
152
+
153
+ TEST(test_rejected_orders_dont_count_toward_rate_limit) {
154
+ // This is the Bug #9 regression test
155
+ RiskLimits limits;
156
+ limits.max_order_qty = 100;
157
+ limits.max_orders_per_sec = 5;
158
+ limits.max_notional_usd = 1e9;
159
+ limits.max_position = 1000000;
160
+ RiskManager rm(limits);
161
+
162
+ // Send 10 INVALID orders (qty too large) ��� these should NOT count
163
+ for (int i = 0; i < 10; ++i) {
164
+ auto bad = make_order("AAPL", Side::BUY, OrderType::LIMIT, 185.0, 500);
165
+ rm.check_order(bad);
166
+ }
167
+
168
+ // Now send a valid order — it should still be approved
169
+ auto good = make_order("AAPL", Side::BUY, OrderType::LIMIT, 185.0, 50);
170
+ auto result = rm.check_order(good);
171
+
172
+ ASSERT_TRUE(result.approved); // rate limit not exhausted by rejects
173
+ PASS();
174
+ }
175
+
176
+ // ─── Position & PnL Tracking ─────────────────────────────────────────────────
177
+
178
+ TEST(test_position_after_fill) {
179
+ RiskLimits limits;
180
+ RiskManager rm(limits);
181
+
182
+ rm.on_fill("AAPL", Side::BUY, 100, 185.0);
183
+ auto pos = rm.get_position("AAPL");
184
+
185
+ ASSERT_EQ(pos.net_qty, 100);
186
+ ASSERT_TRUE(std::abs(pos.avg_price - 185.0) < 0.01);
187
+ PASS();
188
+ }
189
+
190
+ TEST(test_realized_pnl) {
191
+ RiskLimits limits;
192
+ RiskManager rm(limits);
193
+
194
+ // Buy 100 @ 185
195
+ rm.on_fill("AAPL", Side::BUY, 100, 185.0);
196
+ // Sell 100 @ 190 → realized = (190 - 185) * 100 = 500
197
+ rm.on_fill("AAPL", Side::SELL, 100, 190.0);
198
+
199
+ auto pos = rm.get_position("AAPL");
200
+ ASSERT_EQ(pos.net_qty, 0);
201
+ ASSERT_TRUE(std::abs(pos.realized_pnl - 500.0) < 0.01);
202
+ PASS();
203
+ }
204
+
205
+ TEST(test_unrealized_pnl) {
206
+ RiskLimits limits;
207
+ RiskManager rm(limits);
208
+
209
+ rm.on_fill("AAPL", Side::BUY, 100, 185.0);
210
+ rm.update_market_price("AAPL", 190.0);
211
+
212
+ auto pos = rm.get_position("AAPL");
213
+ // unrealized = (190 - 185) * 100 = 500
214
+ ASSERT_TRUE(std::abs(pos.unrealized_pnl - 500.0) < 0.01);
215
+ PASS();
216
+ }
217
+
218
+ TEST(test_disabled_risk_approves_all) {
219
+ RiskLimits limits;
220
+ limits.enabled = false;
221
+ RiskManager rm(limits);
222
+
223
+ auto o = make_order("AAPL", Side::BUY, OrderType::LIMIT, 185.0, 999999);
224
+ auto result = rm.check_order(o);
225
+
226
+ ASSERT_TRUE(result.approved);
227
+ PASS();
228
+ }
229
+
230
+ // ─── Main ────────────────────────────────────────────────────────────────────
231
+
232
+ int main() {
233
+ std::cout << "\n=== Risk Manager — Unit Tests ===\n\n";
234
+
235
+ std::cout << "\n─────────────────────────────────────────────\n";
236
+ std::cout << " Passed: " << tests_passed << "\n";
237
+ std::cout << " Failed: " << tests_failed << "\n";
238
+ std::cout << "─────────────────────────────────────────────\n\n";
239
+
240
+ return tests_failed > 0 ? 1 : 0;
241
+ }