Fix C++ engine buy/sell and add Optiq actor stages to Message Flow
Browse files- ClOrdID: use numeric IDs (C++ strtoull parsed 'D-42' as 0)
- ExecType: handle FIX standard '150=F' for trades (was checking '1'/'2')
- Use order data from _pending instead of missing exec report tags
- Add MECore stage to pipeline visualizer (OEG > Book > MECore > Match)
- Log full C++ actor path: OEG > Book > MECore > Match > Trade > DB > CH
- Auto-restart FIX gateway on engine crash
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- dashboard/app.py +36 -28
- dashboard/templates/index.html +9 -5
dashboard/app.py
CHANGED
|
@@ -339,7 +339,7 @@ class CppEngineBridge:
|
|
| 339 |
global next_order_id
|
| 340 |
oid = next_order_id
|
| 341 |
next_order_id += 1
|
| 342 |
-
cl_ord_id = cl_ord_id or
|
| 343 |
|
| 344 |
sym_name = symbols.get(symbol_id, {}).get("name", "???")
|
| 345 |
fix_side = "1" if side == "Buy" else "2"
|
|
@@ -377,10 +377,13 @@ class CppEngineBridge:
|
|
| 377 |
|
| 378 |
if self._connected:
|
| 379 |
self._pending[cl_ord_id] = order
|
|
|
|
|
|
|
| 380 |
self._send_fix(fields)
|
| 381 |
else:
|
| 382 |
order["status"] = "Rejected"
|
| 383 |
order["remainingQty"] = quantity
|
|
|
|
| 384 |
|
| 385 |
with state_lock:
|
| 386 |
orders.append(order)
|
|
@@ -515,40 +518,46 @@ class CppEngineBridge:
|
|
| 515 |
def _handle_exec_report(self, fields):
|
| 516 |
cl_ord_id = fields.get(11, "")
|
| 517 |
exec_type = fields.get(150, "")
|
| 518 |
-
|
| 519 |
-
side = "Buy" if fields.get(54) == "1" else "Sell"
|
| 520 |
last_px = float(fields.get(31, 0))
|
| 521 |
last_qty = int(fields.get(32, 0))
|
| 522 |
leaves_qty = int(fields.get(151, 0))
|
| 523 |
|
| 524 |
order = self._pending.get(cl_ord_id)
|
| 525 |
-
if order:
|
| 526 |
-
|
| 527 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 528 |
order["status"] = "Filled"
|
| 529 |
-
|
| 530 |
order["status"] = "PartiallyFilled"
|
| 531 |
-
|
| 532 |
-
|
| 533 |
-
|
| 534 |
-
|
| 535 |
-
|
| 536 |
-
|
| 537 |
-
|
| 538 |
-
|
| 539 |
-
|
| 540 |
-
|
| 541 |
-
|
| 542 |
-
|
| 543 |
-
|
| 544 |
trade = {
|
| 545 |
"tradeId": len(trades) + 1,
|
| 546 |
-
"symbolIdx": sym_id
|
| 547 |
"symbol": symbol,
|
| 548 |
"price": last_px,
|
| 549 |
"quantity": last_qty,
|
| 550 |
-
"buyOrderId":
|
| 551 |
-
"sellOrderId":
|
| 552 |
"timestamp": time.time(),
|
| 553 |
"source": "cpp-engine",
|
| 554 |
}
|
|
@@ -556,6 +565,10 @@ class CppEngineBridge:
|
|
| 556 |
trades.append(trade)
|
| 557 |
save_trade(db_path, trade)
|
| 558 |
record_ohlcv(db_path, symbol, last_px, last_qty)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 559 |
broadcast_event("trade", trade)
|
| 560 |
if sym_id:
|
| 561 |
engine._update_snapshot(sym_id)
|
|
@@ -802,11 +815,6 @@ def _active_engine():
|
|
| 802 |
def new_order():
|
| 803 |
d = request.json
|
| 804 |
active = _active_engine()
|
| 805 |
-
if engine_mode == "cpp":
|
| 806 |
-
sym_name = symbols.get(int(d["symbolIdx"]), {}).get("name", "?")
|
| 807 |
-
log_message("OEG", f"[C++] NewOrder {sym_name} {d['side']} {d['quantity']}@{float(d.get('price',0)):.2f}",
|
| 808 |
-
order_id=0)
|
| 809 |
-
log_message("FIX", f"[C++] 35=D β eunex_me:{cpp_bridge.port}", order_id=0)
|
| 810 |
order = active.submit_order(
|
| 811 |
symbol_id=int(d["symbolIdx"]),
|
| 812 |
side=d["side"],
|
|
|
|
| 339 |
global next_order_id
|
| 340 |
oid = next_order_id
|
| 341 |
next_order_id += 1
|
| 342 |
+
cl_ord_id = cl_ord_id or str(oid)
|
| 343 |
|
| 344 |
sym_name = symbols.get(symbol_id, {}).get("name", "???")
|
| 345 |
fix_side = "1" if side == "Buy" else "2"
|
|
|
|
| 377 |
|
| 378 |
if self._connected:
|
| 379 |
self._pending[cl_ord_id] = order
|
| 380 |
+
log_message("OEG", f"[C++] NewOrder {sym_name} {side} {quantity}@{price:.2f} β FIXAcceptor", order_id=oid)
|
| 381 |
+
log_message("Book", f"[C++] 35=D clOrdId={cl_ord_id} β OEGActor β MECoreActor", order_id=oid)
|
| 382 |
self._send_fix(fields)
|
| 383 |
else:
|
| 384 |
order["status"] = "Rejected"
|
| 385 |
order["remainingQty"] = quantity
|
| 386 |
+
log_message("OEG", f"[C++] NewOrder REJECTED β not connected", order_id=oid)
|
| 387 |
|
| 388 |
with state_lock:
|
| 389 |
orders.append(order)
|
|
|
|
| 518 |
def _handle_exec_report(self, fields):
|
| 519 |
cl_ord_id = fields.get(11, "")
|
| 520 |
exec_type = fields.get(150, "")
|
| 521 |
+
ord_status = fields.get(39, "")
|
|
|
|
| 522 |
last_px = float(fields.get(31, 0))
|
| 523 |
last_qty = int(fields.get(32, 0))
|
| 524 |
leaves_qty = int(fields.get(151, 0))
|
| 525 |
|
| 526 |
order = self._pending.get(cl_ord_id)
|
| 527 |
+
if not order:
|
| 528 |
+
return
|
| 529 |
+
|
| 530 |
+
symbol = order.get("symbol", "?")
|
| 531 |
+
side = order.get("side", "?")
|
| 532 |
+
oid = order.get("orderId", "?")
|
| 533 |
+
|
| 534 |
+
order["remainingQty"] = leaves_qty
|
| 535 |
+
if exec_type == "F" or ord_status == "2":
|
| 536 |
+
if leaves_qty == 0:
|
| 537 |
order["status"] = "Filled"
|
| 538 |
+
else:
|
| 539 |
order["status"] = "PartiallyFilled"
|
| 540 |
+
elif exec_type == "0" or ord_status == "0":
|
| 541 |
+
order["status"] = "New"
|
| 542 |
+
elif exec_type == "4" or ord_status == "4":
|
| 543 |
+
order["status"] = "Cancelled"
|
| 544 |
+
elif exec_type == "8" or ord_status == "8":
|
| 545 |
+
order["status"] = "Rejected"
|
| 546 |
+
save_order(db_path, order)
|
| 547 |
+
broadcast_event("order", order)
|
| 548 |
+
|
| 549 |
+
log_message("MECore", f"[C++] ExecReport Order#{oid} β {order['status']}", order_id=oid)
|
| 550 |
+
|
| 551 |
+
if exec_type == "F" and last_qty > 0:
|
| 552 |
+
sym_id = order.get("symbolIdx", 0)
|
| 553 |
trade = {
|
| 554 |
"tradeId": len(trades) + 1,
|
| 555 |
+
"symbolIdx": sym_id,
|
| 556 |
"symbol": symbol,
|
| 557 |
"price": last_px,
|
| 558 |
"quantity": last_qty,
|
| 559 |
+
"buyOrderId": oid if side == "Buy" else 0,
|
| 560 |
+
"sellOrderId": oid if side == "Sell" else 0,
|
| 561 |
"timestamp": time.time(),
|
| 562 |
"source": "cpp-engine",
|
| 563 |
}
|
|
|
|
| 565 |
trades.append(trade)
|
| 566 |
save_trade(db_path, trade)
|
| 567 |
record_ohlcv(db_path, symbol, last_px, last_qty)
|
| 568 |
+
log_message("Match", f"[C++] Trade#{trade['tradeId']} {symbol} {last_qty}@{last_px:.2f}", order_id=oid)
|
| 569 |
+
log_message("Trade", f"[C++] Trade#{trade['tradeId']} persisted", order_id=oid, trade_id=trade["tradeId"])
|
| 570 |
+
log_message("DB", f"[C++] Trade#{trade['tradeId']} β SQLite", trade_id=trade["tradeId"])
|
| 571 |
+
log_message("CH", f"[C++] Trade#{trade['tradeId']} β clearing", order_id=oid, trade_id=trade["tradeId"])
|
| 572 |
broadcast_event("trade", trade)
|
| 573 |
if sym_id:
|
| 574 |
engine._update_snapshot(sym_id)
|
|
|
|
| 815 |
def new_order():
|
| 816 |
d = request.json
|
| 817 |
active = _active_engine()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 818 |
order = active.submit_order(
|
| 819 |
symbol_id=int(d["symbolIdx"]),
|
| 820 |
side=d["side"],
|
dashboard/templates/index.html
CHANGED
|
@@ -159,9 +159,11 @@ border-radius:6px;color:var(--text);font-size:12px}
|
|
| 159 |
.stage-match{background:#f0c04022;color:var(--yellow);border:1px solid #f0c04044}
|
| 160 |
.stage-trade{background:#7c5cfc22;color:var(--purple);border:1px solid #7c5cfc44}
|
| 161 |
.stage-db{background:#ff6b6b22;color:var(--red);border:1px solid #ff6b6b44}
|
|
|
|
| 162 |
.stage-ch{background:#00d4aa22;color:var(--accent);border:1px solid #00d4aa44}
|
| 163 |
.stage-oeg.active{background:#4a90d944;box-shadow:0 0 12px #4a90d944}
|
| 164 |
.stage-book.active{background:#00d4aa44;box-shadow:0 0 12px #00d4aa44}
|
|
|
|
| 165 |
.stage-match.active{background:#f0c04044;box-shadow:0 0 12px #f0c04044}
|
| 166 |
.stage-trade.active{background:#7c5cfc44;box-shadow:0 0 12px #7c5cfc44}
|
| 167 |
.stage-db.active{background:#ff6b6b44;box-shadow:0 0 12px #ff6b6b44}
|
|
@@ -172,7 +174,7 @@ border-radius:6px;color:var(--text);font-size:12px}
|
|
| 172 |
@keyframes fadeFlow{from{opacity:0;transform:translateX(-8px)}to{opacity:1;transform:translateX(0)}}
|
| 173 |
.flow-ts{color:var(--muted);min-width:75px;font-size:10px}
|
| 174 |
.flow-tag{min-width:50px;font-weight:600;font-size:10px;text-transform:uppercase}
|
| 175 |
-
.flow-tag-oeg{color:var(--blue)}.flow-tag-book{color:var(--accent)}.flow-tag-match{color:var(--yellow)}
|
| 176 |
.flow-tag-trade{color:var(--purple)}.flow-tag-db{color:var(--red)}.flow-tag-ch{color:var(--accent)}
|
| 177 |
.flow-detail{color:var(--text)}
|
| 178 |
.flow-empty{padding:30px;text-align:center;color:var(--muted)}
|
|
@@ -363,12 +365,14 @@ border-radius:6px;color:var(--text);font-size:12px}
|
|
| 363 |
<div class="flow-arrow" id="fa1">▶</div>
|
| 364 |
<div class="flow-stage stage-book" id="fpBook"><span class="count">-</span>Book</div>
|
| 365 |
<div class="flow-arrow" id="fa2">▶</div>
|
| 366 |
-
<div class="flow-stage stage-
|
| 367 |
<div class="flow-arrow" id="fa3">▶</div>
|
| 368 |
-
<div class="flow-stage stage-
|
| 369 |
<div class="flow-arrow" id="fa4">▶</div>
|
| 370 |
-
<div class="flow-stage stage-
|
| 371 |
<div class="flow-arrow" id="fa5">▶</div>
|
|
|
|
|
|
|
| 372 |
<div class="flow-stage stage-ch" id="fpCH"><span class="count">-</span>CH</div>
|
| 373 |
</div>
|
| 374 |
<div class="flow-msgs" id="flowMsgs">
|
|
@@ -876,7 +880,7 @@ function renderOrderDetail(oid) {
|
|
| 876 |
const o = flowOrderMap[oid];
|
| 877 |
if (!o) return;
|
| 878 |
const stages = new Set(o.entries.map(e => e.stage));
|
| 879 |
-
const stageOrder = ['OEG','Book','Match','Trade','DB','CH'];
|
| 880 |
const reachedIdx = Math.max(...stageOrder.map((s,i) => stages.has(s) ? i : -1));
|
| 881 |
|
| 882 |
stageOrder.forEach((s, i) => {
|
|
|
|
| 159 |
.stage-match{background:#f0c04022;color:var(--yellow);border:1px solid #f0c04044}
|
| 160 |
.stage-trade{background:#7c5cfc22;color:var(--purple);border:1px solid #7c5cfc44}
|
| 161 |
.stage-db{background:#ff6b6b22;color:var(--red);border:1px solid #ff6b6b44}
|
| 162 |
+
.stage-mecore{background:#ff8c0022;color:#ff8c00;border:1px solid #ff8c0044}
|
| 163 |
.stage-ch{background:#00d4aa22;color:var(--accent);border:1px solid #00d4aa44}
|
| 164 |
.stage-oeg.active{background:#4a90d944;box-shadow:0 0 12px #4a90d944}
|
| 165 |
.stage-book.active{background:#00d4aa44;box-shadow:0 0 12px #00d4aa44}
|
| 166 |
+
.stage-mecore.active{background:#ff8c0044;box-shadow:0 0 12px #ff8c0044}
|
| 167 |
.stage-match.active{background:#f0c04044;box-shadow:0 0 12px #f0c04044}
|
| 168 |
.stage-trade.active{background:#7c5cfc44;box-shadow:0 0 12px #7c5cfc44}
|
| 169 |
.stage-db.active{background:#ff6b6b44;box-shadow:0 0 12px #ff6b6b44}
|
|
|
|
| 174 |
@keyframes fadeFlow{from{opacity:0;transform:translateX(-8px)}to{opacity:1;transform:translateX(0)}}
|
| 175 |
.flow-ts{color:var(--muted);min-width:75px;font-size:10px}
|
| 176 |
.flow-tag{min-width:50px;font-weight:600;font-size:10px;text-transform:uppercase}
|
| 177 |
+
.flow-tag-oeg{color:var(--blue)}.flow-tag-book{color:var(--accent)}.flow-tag-mecore{color:#ff8c00}.flow-tag-match{color:var(--yellow)}
|
| 178 |
.flow-tag-trade{color:var(--purple)}.flow-tag-db{color:var(--red)}.flow-tag-ch{color:var(--accent)}
|
| 179 |
.flow-detail{color:var(--text)}
|
| 180 |
.flow-empty{padding:30px;text-align:center;color:var(--muted)}
|
|
|
|
| 365 |
<div class="flow-arrow" id="fa1">▶</div>
|
| 366 |
<div class="flow-stage stage-book" id="fpBook"><span class="count">-</span>Book</div>
|
| 367 |
<div class="flow-arrow" id="fa2">▶</div>
|
| 368 |
+
<div class="flow-stage stage-mecore" id="fpMECore"><span class="count">-</span>MECore</div>
|
| 369 |
<div class="flow-arrow" id="fa3">▶</div>
|
| 370 |
+
<div class="flow-stage stage-match" id="fpMatch"><span class="count">-</span>Match</div>
|
| 371 |
<div class="flow-arrow" id="fa4">▶</div>
|
| 372 |
+
<div class="flow-stage stage-trade" id="fpTrade"><span class="count">-</span>Trade</div>
|
| 373 |
<div class="flow-arrow" id="fa5">▶</div>
|
| 374 |
+
<div class="flow-stage stage-db" id="fpDB"><span class="count">-</span>DB</div>
|
| 375 |
+
<div class="flow-arrow" id="fa6">▶</div>
|
| 376 |
<div class="flow-stage stage-ch" id="fpCH"><span class="count">-</span>CH</div>
|
| 377 |
</div>
|
| 378 |
<div class="flow-msgs" id="flowMsgs">
|
|
|
|
| 880 |
const o = flowOrderMap[oid];
|
| 881 |
if (!o) return;
|
| 882 |
const stages = new Set(o.entries.map(e => e.stage));
|
| 883 |
+
const stageOrder = ['OEG','Book','MECore','Match','Trade','DB','CH'];
|
| 884 |
const reachedIdx = Math.max(...stageOrder.map((s,i) => stages.has(s) ? i : -1));
|
| 885 |
|
| 886 |
stageOrder.forEach((s, i) => {
|