Spaces:
Sleeping
Sleeping
Delete BUG_REPORT.md
Browse files- BUG_REPORT.md +0 -511
BUG_REPORT.md
DELETED
|
@@ -1,511 +0,0 @@
|
|
| 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 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|