RayMelius Claude Opus 4.6 commited on
Commit
ddafe7a
Β·
1 Parent(s): 9781afc

v0.8.0: AI Market Analyst and Developer Message Flow Visualizer

Browse files

F5: AI Analyst with three-provider fallback (Ollama/Groq/HuggingFace).
Builds market context prompt from live trades and order book, generates
3-4 sentence analysis. Provider switchable from UI. Insights broadcast
via SSE and cached in memory.

F6: Message Flow Visualizer traces the full order lifecycle through
OEG -> Book -> Match -> Trade -> DB -> CH pipeline. Uses function
patching to intercept engine methods. Shows per-stage counters and
live log with monospace formatting.

Dashboard now has 3 tabs: Trading, AI Analyst, Message Flow.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

CMakeLists.txt CHANGED
@@ -1,5 +1,5 @@
1
  cmake_minimum_required(VERSION 3.16)
2
- project(EuNEx VERSION 0.7.0 LANGUAGES CXX)
3
 
4
  set(CMAKE_CXX_STANDARD 20)
5
  set(CMAKE_CXX_STANDARD_REQUIRED ON)
 
1
  cmake_minimum_required(VERSION 3.16)
2
+ project(EuNEx VERSION 0.8.0 LANGUAGES CXX)
3
 
4
  set(CMAKE_CXX_STANDARD 20)
5
  set(CMAKE_CXX_STANDARD_REQUIRED ON)
README.md CHANGED
@@ -27,7 +27,7 @@ ch_ai_trader.py β†’ ClearingHouseActor β†’ Clearing House (P
27
  AI strategies β†’ AITraderActor β†’ Trading obligations
28
  ```
29
 
30
- ## Actor Topology (v0.7)
31
 
32
  ```
33
  Core 0: OEGActor + FIXAcceptorActor ← Order entry & FIX protocol
@@ -220,7 +220,9 @@ EuNEx/
220
  3. ~~FIX gateway~~ βœ“ C++ FIXAcceptorActor + Python fallback
221
  4. ~~Clearing House~~ βœ“ ClearingHouseActor + AITraderActor
222
  5. ~~Market simulation~~ βœ“ Realistic AI trading + Dashboard auto-simulation
223
- 6. **SBE encoding** β€” replace event structs with SBE-encoded messages
224
- 7. **Master/Mirror failover** β€” implement full Recovery replay on Mirror node
225
- 8. **Trading phases** β€” pre-open, uncrossing, continuous, close, TAL
226
- 9. **Additional order types** β€” Stop, Pegged, Mid-Point, Iceberg
 
 
 
27
  AI strategies β†’ AITraderActor β†’ Trading obligations
28
  ```
29
 
30
+ ## Actor Topology (v0.8)
31
 
32
  ```
33
  Core 0: OEGActor + FIXAcceptorActor ← Order entry & FIX protocol
 
220
  3. ~~FIX gateway~~ βœ“ C++ FIXAcceptorActor + Python fallback
221
  4. ~~Clearing House~~ βœ“ ClearingHouseActor + AITraderActor
222
  5. ~~Market simulation~~ βœ“ Realistic AI trading + Dashboard auto-simulation
223
+ 6. ~~AI Analyst~~ βœ“ Ollama/Groq/HuggingFace market commentary
224
+ 7. ~~Message Flow Visualizer~~ βœ“ Developer pipeline tracing tool
225
+ 8. **SBE encoding** β€” replace event structs with SBE-encoded messages
226
+ 9. **Master/Mirror failover** β€” implement full Recovery replay on Mirror node
227
+ 10. **Trading phases** β€” pre-open, uncrossing, continuous, close, TAL
228
+ 11. **Additional order types** β€” Stop, Pegged, Mid-Point, Iceberg
dashboard/app.py CHANGED
@@ -10,6 +10,8 @@ Provides:
10
  - Session controls (start/suspend/resume)
11
  - SSE streaming for live updates
12
  - Clearing House integration
 
 
13
 
14
  Run: python dashboard/app.py
15
  """
@@ -23,6 +25,7 @@ import sys
23
  import os
24
  import urllib.request
25
  import urllib.error
 
26
 
27
  sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
28
 
@@ -582,6 +585,274 @@ class MarketSimulator:
582
  simulator = MarketSimulator(engine)
583
 
584
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
585
  if __name__ == "__main__":
586
  os.makedirs(os.path.dirname(db_path), exist_ok=True)
587
  init_db(db_path)
@@ -590,4 +861,5 @@ if __name__ == "__main__":
590
  print(f"EuNEx Dashboard starting on http://localhost:{port}")
591
  print(f" Database: {db_path}")
592
  print(f" Simulation: every {SIM_INTERVAL}s, {SIM_ORDERS_PER_ROUND} orders/symbol/round")
 
593
  app.run(host="0.0.0.0", port=port, debug=True, threaded=True)
 
10
  - Session controls (start/suspend/resume)
11
  - SSE streaming for live updates
12
  - Clearing House integration
13
+ - AI Analyst (Ollama/Groq/HuggingFace)
14
+ - Developer Message Flow Visualizer
15
 
16
  Run: python dashboard/app.py
17
  """
 
25
  import os
26
  import urllib.request
27
  import urllib.error
28
+ from collections import deque
29
 
30
  sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
31
 
 
585
  simulator = MarketSimulator(engine)
586
 
587
 
588
+ # ── AI Analyst ─────────────────────────────────────────────────────
589
+
590
+ OLLAMA_HOST = os.environ.get("OLLAMA_HOST", "http://localhost:11434")
591
+ OLLAMA_MODEL = os.environ.get("OLLAMA_MODEL", "llama3.2:3b")
592
+ GROQ_API_KEY = os.environ.get("GROQ_API_KEY", "")
593
+ GROQ_MODEL = os.environ.get("GROQ_MODEL", "llama-3.1-8b-instant")
594
+ HF_TOKEN = os.environ.get("HF_TOKEN", "")
595
+ HF_MODEL = os.environ.get("HF_MODEL", "Qwen/Qwen2.5-7B-Instruct")
596
+
597
+ ai_insights = deque(maxlen=20)
598
+ ai_provider = "auto"
599
+ ai_model_override = None
600
+ ai_generating = False
601
+
602
+
603
+ def _build_market_prompt():
604
+ now_str = time.strftime("%H:%M:%S")
605
+ session = session_status.upper()
606
+
607
+ trade_lines = []
608
+ with state_lock:
609
+ by_sym = {}
610
+ for t in trades[-200:]:
611
+ sym = t.get("symbol", "?")
612
+ by_sym.setdefault(sym, []).append(t)
613
+ for sym, ts in sorted(by_sym.items()):
614
+ prices = [t["price"] for t in ts if t.get("price")]
615
+ vol = sum(t.get("quantity", 0) for t in ts)
616
+ if prices:
617
+ trade_lines.append(
618
+ f" {sym}: {len(ts)} trade(s), range {min(prices):.2f}-{max(prices):.2f}, "
619
+ f"vol {vol}, last {prices[-1]:.2f}"
620
+ )
621
+
622
+ book_lines = []
623
+ for sid, snap in sorted(snapshots.items()):
624
+ bid = snap.get("bestBid", 0)
625
+ ask = snap.get("bestAsk", 0)
626
+ spread = ask - bid if bid > 0 and ask > 0 else 0
627
+ book_lines.append(
628
+ f" {snap.get('symbol','?')}: Bid {bid:.2f} / Ask {ask:.2f} (spread {spread:.2f})"
629
+ )
630
+
631
+ trades_text = "\n".join(trade_lines) if trade_lines else " No recent trades"
632
+ book_text = "\n".join(book_lines) if book_lines else " No order book data"
633
+
634
+ return (
635
+ "You are a concise financial market analyst for the EuNEx simulated exchange "
636
+ "(Euronext Optiq architecture). "
637
+ f"Time: {now_str} | Session: {session}\n\n"
638
+ f"Recent trades:\n{trades_text}\n\n"
639
+ f"Order book:\n{book_text}\n\n"
640
+ "In 3-4 sentences: activity level, notable price moves, market sentiment. "
641
+ "Plain prose, no headers, no bullet points."
642
+ )
643
+
644
+
645
+ def _try_ollama(prompt, model=None):
646
+ model = model or OLLAMA_MODEL
647
+ try:
648
+ data = json.dumps({
649
+ "model": model,
650
+ "messages": [{"role": "user", "content": prompt}],
651
+ "stream": False,
652
+ }).encode()
653
+ req = urllib.request.Request(
654
+ f"{OLLAMA_HOST}/api/chat",
655
+ data=data,
656
+ headers={"Content-Type": "application/json"},
657
+ )
658
+ with urllib.request.urlopen(req, timeout=90) as resp:
659
+ result = json.loads(resp.read())
660
+ return result.get("message", {}).get("content", "")
661
+ except Exception:
662
+ return None
663
+
664
+
665
+ def _try_groq(prompt, model=None):
666
+ if not GROQ_API_KEY:
667
+ return None
668
+ model = model or GROQ_MODEL
669
+ try:
670
+ data = json.dumps({
671
+ "model": model,
672
+ "messages": [{"role": "user", "content": prompt}],
673
+ "max_tokens": 300,
674
+ "temperature": 0.7,
675
+ }).encode()
676
+ req = urllib.request.Request(
677
+ "https://api.groq.com/openai/v1/chat/completions",
678
+ data=data,
679
+ headers={
680
+ "Content-Type": "application/json",
681
+ "Authorization": f"Bearer {GROQ_API_KEY}",
682
+ },
683
+ )
684
+ with urllib.request.urlopen(req, timeout=30) as resp:
685
+ result = json.loads(resp.read())
686
+ return result["choices"][0]["message"]["content"]
687
+ except Exception:
688
+ return None
689
+
690
+
691
+ def _try_hf(prompt, model=None):
692
+ if not HF_TOKEN:
693
+ return None
694
+ model = model or HF_MODEL
695
+ try:
696
+ data = json.dumps({
697
+ "model": model,
698
+ "messages": [{"role": "user", "content": prompt}],
699
+ "max_tokens": 300,
700
+ "temperature": 0.7,
701
+ }).encode()
702
+ req = urllib.request.Request(
703
+ "https://router.huggingface.co/v1/chat/completions",
704
+ data=data,
705
+ headers={
706
+ "Content-Type": "application/json",
707
+ "Authorization": f"Bearer {HF_TOKEN}",
708
+ },
709
+ )
710
+ with urllib.request.urlopen(req, timeout=90) as resp:
711
+ result = json.loads(resp.read())
712
+ return result["choices"][0]["message"]["content"]
713
+ except Exception:
714
+ return None
715
+
716
+
717
+ def _call_llm(prompt):
718
+ provider = ai_provider
719
+ model = ai_model_override
720
+
721
+ if provider == "ollama":
722
+ return _try_ollama(prompt, model), "ollama"
723
+ elif provider == "groq":
724
+ return _try_groq(prompt, model), "groq"
725
+ elif provider == "hf":
726
+ return _try_hf(prompt, model), "hf"
727
+
728
+ for name, func in [("ollama", _try_ollama), ("groq", _try_groq), ("hf", _try_hf)]:
729
+ text = func(prompt, model)
730
+ if text:
731
+ return text, name
732
+ return None, None
733
+
734
+
735
+ def _generate_insight():
736
+ global ai_generating
737
+ if ai_generating:
738
+ return
739
+ ai_generating = True
740
+ try:
741
+ prompt = _build_market_prompt()
742
+ text, source = _call_llm(prompt)
743
+ if text:
744
+ insight = {
745
+ "text": text.strip(),
746
+ "timestamp": time.time(),
747
+ "source": source or "unknown",
748
+ }
749
+ ai_insights.append(insight)
750
+ broadcast_event("ai_insight", insight)
751
+ finally:
752
+ ai_generating = False
753
+
754
+
755
+ @app.route("/ai/generate", methods=["POST"])
756
+ def ai_generate():
757
+ threading.Thread(target=_generate_insight, daemon=True).start()
758
+ return jsonify({"status": "generating"})
759
+
760
+
761
+ @app.route("/ai/insights")
762
+ def ai_insights_list():
763
+ return jsonify(list(ai_insights))
764
+
765
+
766
+ @app.route("/ai/config")
767
+ def ai_config():
768
+ return jsonify({
769
+ "provider": ai_provider,
770
+ "model": ai_model_override,
771
+ "providers": {
772
+ "auto": {"label": "Auto (fallback)", "available": True},
773
+ "ollama": {"label": f"Ollama ({OLLAMA_MODEL})", "available": bool(OLLAMA_HOST)},
774
+ "groq": {"label": f"Groq ({GROQ_MODEL})", "available": bool(GROQ_API_KEY)},
775
+ "hf": {"label": f"HuggingFace ({HF_MODEL})", "available": bool(HF_TOKEN)},
776
+ },
777
+ })
778
+
779
+
780
+ @app.route("/ai/select", methods=["POST"])
781
+ def ai_select():
782
+ global ai_provider, ai_model_override
783
+ d = request.json
784
+ ai_provider = d.get("provider", "auto")
785
+ ai_model_override = d.get("model") or None
786
+ return jsonify({"provider": ai_provider, "model": ai_model_override})
787
+
788
+
789
+ # ── Developer Message Flow Log ─────────────────────────────────────
790
+
791
+ message_log = deque(maxlen=500)
792
+
793
+
794
+ def log_message(stage, detail):
795
+ entry = {
796
+ "timestamp": time.time(),
797
+ "stage": stage,
798
+ "detail": detail,
799
+ }
800
+ message_log.append(entry)
801
+ broadcast_event("msgflow", entry)
802
+
803
+
804
+ @app.route("/dev/messages")
805
+ def dev_messages():
806
+ limit = int(request.args.get("limit", 100))
807
+ items = list(message_log)[-limit:]
808
+ return jsonify(items)
809
+
810
+
811
+ # Patch engine to log message flow
812
+ _orig_submit = engine.submit_order
813
+
814
+ def _traced_submit(symbol_id, side, order_type, price, quantity,
815
+ tif="Day", source="dashboard", cl_ord_id=""):
816
+ sym_name = symbols.get(symbol_id, {}).get("name", "?")
817
+ log_message("OEG", f"NewOrder {sym_name} {side} {quantity}@{price:.2f} [{source}]")
818
+ result = _orig_submit(symbol_id, side, order_type, price, quantity, tif, source, cl_ord_id)
819
+ oid = result.get("orderId", "?")
820
+ status = result.get("status", "?")
821
+ log_message("Book", f"Order#{oid} β†’ {status}")
822
+ if status == "Filled":
823
+ log_message("Match", f"Order#{oid} fully filled")
824
+ elif status == "PartiallyFilled":
825
+ log_message("Match", f"Order#{oid} partial fill, rem={result.get('remainingQty', '?')}")
826
+ return result
827
+
828
+ engine.submit_order = _traced_submit
829
+
830
+ _orig_cancel = engine.cancel_order
831
+
832
+ def _traced_cancel(order_id):
833
+ log_message("OEG", f"CancelOrder #{order_id}")
834
+ result = _orig_cancel(order_id)
835
+ if result:
836
+ log_message("Book", f"Order#{order_id} cancelled")
837
+ return result
838
+
839
+ engine.cancel_order = _traced_cancel
840
+
841
+ # Patch trade saving to log trade and clearing steps
842
+ _orig_broadcast = broadcast_event
843
+
844
+ def _traced_broadcast(event_type, data):
845
+ if event_type == "trade":
846
+ tid = data.get("tradeId", "?")
847
+ sym = data.get("symbol", "?")
848
+ log_message("Trade", f"Trade#{tid} {sym} {data.get('quantity',0)}@{data.get('price',0):.2f}")
849
+ log_message("DB", f"Trade#{tid} persisted to SQLite")
850
+ log_message("CH", f"Trade#{tid} β†’ clearing (buy#{data.get('buyOrderId','?')}, sell#{data.get('sellOrderId','?')})")
851
+ _orig_broadcast(event_type, data)
852
+
853
+ broadcast_event = _traced_broadcast
854
+
855
+
856
  if __name__ == "__main__":
857
  os.makedirs(os.path.dirname(db_path), exist_ok=True)
858
  init_db(db_path)
 
861
  print(f"EuNEx Dashboard starting on http://localhost:{port}")
862
  print(f" Database: {db_path}")
863
  print(f" Simulation: every {SIM_INTERVAL}s, {SIM_ORDERS_PER_ROUND} orders/symbol/round")
864
+ print(f" AI Analyst: provider={ai_provider} (Ollama={OLLAMA_HOST})")
865
  app.run(host="0.0.0.0", port=port, debug=True, threaded=True)
dashboard/templates/index.html CHANGED
@@ -26,8 +26,18 @@ button:hover{opacity:.85}
26
  .btn-yellow{background:var(--yellow);color:var(--bg)}
27
  .btn-red{background:var(--red);color:#fff}
28
  .btn-blue{background:var(--blue);color:#fff}
 
29
  .btn-small{padding:3px 8px;font-size:11px}
30
 
 
 
 
 
 
 
 
 
 
31
  /* ── Ticker tape ─────────────────────────────────────── */
32
  .ticker-wrap{background:#0d1130;border-bottom:1px solid var(--border);overflow:hidden;height:32px;position:relative}
33
  .ticker-track{display:flex;align-items:center;height:100%;white-space:nowrap;animation:tickerScroll 30s linear infinite}
@@ -89,6 +99,43 @@ border-radius:6px;color:var(--text);font-size:12px}
89
  .lb-rank{color:var(--yellow);font-weight:700;width:20px}
90
  .lb-name{flex:1;margin-left:6px}
91
  .lb-val{font-weight:600}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
92
  </style>
93
  </head>
94
  <body>
@@ -109,6 +156,16 @@ border-radius:6px;color:var(--text);font-size:12px}
109
  <div class="ticker-track" id="tickerTrack"></div>
110
  </div>
111
 
 
 
 
 
 
 
 
 
 
 
112
  <!-- Row 1: Order Entry | Order Book | Market Snapshot -->
113
  <div class="grid">
114
  <div class="panel">
@@ -201,6 +258,68 @@ border-radius:6px;color:var(--text);font-size:12px}
201
  </div>
202
  </div>
203
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
204
  <!-- Amend Modal -->
205
  <div class="modal-overlay" id="amendModal">
206
  <div class="modal">
@@ -218,6 +337,19 @@ const state = {orders:[], trades:[], snapshots:{}, symbols:{}, session:'idle'};
218
  let chartPeriod = '1h';
219
  let priceChart = null;
220
  const prevPrices = {};
 
 
 
 
 
 
 
 
 
 
 
 
 
221
 
222
  async function init() {
223
  const resp = await fetch('/data');
@@ -278,6 +410,17 @@ function connectSSE() {
278
  state.session = s.status;
279
  renderSession();
280
  });
 
 
 
 
 
 
 
 
 
 
 
281
  }
282
 
283
  // ── Ticker ────────────────────────────────────────────
@@ -404,7 +547,6 @@ function updateChart(bars, symName) {
404
  const opens = bars.map(b => b.open);
405
  const closes = bars.map(b => b.close);
406
  const volumes = bars.map(b => b.volume);
407
- const colors = bars.map(b => b.close >= b.open ? '#00d4aa' : '#ff6b6b');
408
  const bgColors = bars.map(b => b.close >= b.open ? '#00d4aa44' : '#ff6b6b44');
409
 
410
  if (priceChart) priceChart.destroy();
@@ -417,7 +559,6 @@ function updateChart(bars, symName) {
417
  if (!meta || !meta.data.length) return;
418
  const yScale = chart.scales.y;
419
  const xScale = chart.scales.x;
420
-
421
  meta.data.forEach((point, i) => {
422
  if (i >= bars.length) return;
423
  const x = point.x;
@@ -427,20 +568,13 @@ function updateChart(bars, symName) {
427
  const close = yScale.getPixelForValue(closes[i]);
428
  const color = closes[i] >= opens[i] ? '#00d4aa' : '#ff6b6b';
429
  const barW = Math.max(4, (xScale.width / bars.length) * 0.6);
430
-
431
  ctx.save();
432
- ctx.strokeStyle = color;
433
- ctx.lineWidth = 1;
434
- ctx.beginPath();
435
- ctx.moveTo(x, high);
436
- ctx.lineTo(x, low);
437
- ctx.stroke();
438
-
439
  ctx.fillStyle = color;
440
  const top = Math.min(open, close);
441
  const bot = Math.max(open, close);
442
- const bodyH = Math.max(bot - top, 1);
443
- ctx.fillRect(x - barW/2, top, barW, bodyH);
444
  ctx.restore();
445
  });
446
  }
@@ -451,12 +585,10 @@ function updateChart(bars, symName) {
451
  data: {
452
  labels,
453
  datasets: [{
454
- label: symName + ' Close',
455
- data: closes, borderColor: 'transparent', backgroundColor: 'transparent',
456
- pointRadius: 0, borderWidth: 0, yAxisID: 'y',
457
  },{
458
- label: 'Volume', data: volumes, type: 'bar',
459
- backgroundColor: bgColors, yAxisID: 'y1',
460
  }]
461
  },
462
  options: {
@@ -466,11 +598,9 @@ function updateChart(bars, symName) {
466
  legend:{display:false},
467
  tooltip: {
468
  callbacks: {
469
- label: function(ctx) {
470
- const i = ctx.dataIndex;
471
- if (ctx.datasetIndex === 0 && i < bars.length) {
472
- return 'O:'+fmt(opens[i])+' H:'+fmt(highs[i])+' L:'+fmt(lows[i])+' C:'+fmt(closes[i]);
473
- }
474
  return 'Vol: ' + (volumes[i]||0);
475
  }
476
  }
@@ -510,6 +640,95 @@ async function loadLeaderboard() {
510
  }
511
  }
512
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
513
  async function submitOrder(side) {
514
  const data = {
515
  symbolIdx: parseInt(document.getElementById('orderSymbol').value),
 
26
  .btn-yellow{background:var(--yellow);color:var(--bg)}
27
  .btn-red{background:var(--red);color:#fff}
28
  .btn-blue{background:var(--blue);color:#fff}
29
+ .btn-purple{background:var(--purple);color:#fff}
30
  .btn-small{padding:3px 8px;font-size:11px}
31
 
32
+ /* ── Tabs ────────────────────────────────────────────── */
33
+ .tab-bar{display:flex;gap:2px;padding:0 20px;background:var(--card);border-bottom:1px solid var(--border)}
34
+ .tab-btn{padding:10px 20px;background:transparent;color:var(--muted);border:none;border-bottom:2px solid transparent;
35
+ cursor:pointer;font-size:13px;font-weight:600;transition:all .2s}
36
+ .tab-btn:hover{color:var(--text)}
37
+ .tab-btn.active{color:var(--accent);border-bottom-color:var(--accent)}
38
+ .tab-content{display:none}
39
+ .tab-content.active{display:block}
40
+
41
  /* ── Ticker tape ─────────────────────────────────────── */
42
  .ticker-wrap{background:#0d1130;border-bottom:1px solid var(--border);overflow:hidden;height:32px;position:relative}
43
  .ticker-track{display:flex;align-items:center;height:100%;white-space:nowrap;animation:tickerScroll 30s linear infinite}
 
99
  .lb-rank{color:var(--yellow);font-weight:700;width:20px}
100
  .lb-name{flex:1;margin-left:6px}
101
  .lb-val{font-weight:600}
102
+
103
+ /* ── AI Analyst panel ────────────────────────────────── */
104
+ .ai-panel{border-top:3px solid var(--purple);margin:12px 20px;background:var(--card);border-radius:8px;padding:14px;border:1px solid var(--border)}
105
+ .ai-panel h3{display:flex;align-items:center;gap:10px;flex-wrap:wrap}
106
+ .ai-controls{display:flex;gap:6px;align-items:center;margin-left:auto}
107
+ .ai-controls select{padding:4px 8px;background:#1a1f45;border:1px solid var(--border);border-radius:4px;color:var(--muted);font-size:11px}
108
+ .ai-badge{font-size:10px;padding:2px 8px;border-radius:8px;background:#7c5cfc22;color:var(--purple);font-weight:600}
109
+ .ai-status{font-size:11px;color:var(--muted)}
110
+ .insight-card{border-left:3px solid var(--purple);background:#1a1f4566;padding:10px 14px;margin-bottom:8px;border-radius:0 6px 6px 0;animation:fadeIn .4s ease}
111
+ .insight-time{font-size:10px;color:var(--muted);margin-bottom:4px;display:flex;gap:8px;align-items:center}
112
+ .insight-src{font-size:9px;padding:1px 6px;border-radius:4px;background:#7c5cfc22;color:var(--purple)}
113
+ .insight-text{line-height:1.5;font-size:12px}
114
+ @keyframes fadeIn{from{opacity:0;transform:translateY(-5px)}to{opacity:1;transform:translateY(0)}}
115
+
116
+ /* ── Message Flow Visualizer ─────────────────────────── */
117
+ .flow-panel{margin:12px 20px;background:var(--card);border:1px solid var(--border);border-radius:8px;padding:14px}
118
+ .flow-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:12px}
119
+ .flow-pipeline{display:flex;gap:2px;margin-bottom:16px;justify-content:center;flex-wrap:wrap}
120
+ .flow-stage{text-align:center;padding:8px 14px;border-radius:6px;font-size:11px;font-weight:600;min-width:90px;position:relative}
121
+ .flow-stage .count{font-size:16px;display:block;margin-bottom:2px}
122
+ .flow-arrow{color:var(--muted);font-size:18px;display:flex;align-items:center;padding:0 2px}
123
+ .stage-oeg{background:#4a90d922;color:var(--blue);border:1px solid #4a90d944}
124
+ .stage-book{background:#00d4aa22;color:var(--accent);border:1px solid #00d4aa44}
125
+ .stage-match{background:#f0c04022;color:var(--yellow);border:1px solid #f0c04044}
126
+ .stage-trade{background:#7c5cfc22;color:var(--purple);border:1px solid #7c5cfc44}
127
+ .stage-db{background:#ff6b6b22;color:var(--red);border:1px solid #ff6b6b44}
128
+ .stage-ch{background:#00d4aa22;color:var(--accent);border:1px solid #00d4aa44}
129
+ .flow-log{max-height:300px;overflow-y:auto;font-family:'Cascadia Code','Fira Code',monospace;font-size:11px}
130
+ .flow-entry{padding:3px 8px;border-bottom:1px solid #1a1f4522;display:flex;gap:10px}
131
+ .flow-entry:hover{background:#1a1f4544}
132
+ .flow-ts{color:var(--muted);min-width:75px;font-size:10px}
133
+ .flow-tag{min-width:50px;font-weight:600;font-size:10px;text-transform:uppercase}
134
+ .flow-tag-oeg{color:var(--blue)}.flow-tag-book{color:var(--accent)}.flow-tag-match{color:var(--yellow)}
135
+ .flow-tag-trade{color:var(--purple)}.flow-tag-db{color:var(--red)}.flow-tag-ch{color:var(--accent)}
136
+ .flow-detail{color:var(--text)}
137
+ .flow-active{animation:pulse .6s ease}
138
+ @keyframes pulse{0%{background:#7c5cfc22}100%{background:transparent}}
139
  </style>
140
  </head>
141
  <body>
 
156
  <div class="ticker-track" id="tickerTrack"></div>
157
  </div>
158
 
159
+ <!-- Tab bar -->
160
+ <div class="tab-bar">
161
+ <button class="tab-btn active" onclick="switchTab('trading')">Trading</button>
162
+ <button class="tab-btn" onclick="switchTab('analyst')">AI Analyst</button>
163
+ <button class="tab-btn" onclick="switchTab('devflow')">Message Flow</button>
164
+ </div>
165
+
166
+ <!-- ═══════════════ TAB: Trading ═══════════════ -->
167
+ <div id="tab-trading" class="tab-content active">
168
+
169
  <!-- Row 1: Order Entry | Order Book | Market Snapshot -->
170
  <div class="grid">
171
  <div class="panel">
 
258
  </div>
259
  </div>
260
 
261
+ </div><!-- /tab-trading -->
262
+
263
+ <!-- ═══════════════ TAB: AI Analyst ═══════════════ -->
264
+ <div id="tab-analyst" class="tab-content">
265
+ <div class="ai-panel">
266
+ <h3>AI Market Analyst
267
+ <div class="ai-controls">
268
+ <select id="aiProvider" onchange="onAIProviderChange()">
269
+ <option value="auto">Auto</option>
270
+ <option value="ollama">Ollama</option>
271
+ <option value="groq">Groq</option>
272
+ <option value="hf">HuggingFace</option>
273
+ </select>
274
+ <span class="ai-badge" id="aiBadge">auto</span>
275
+ <button class="btn-purple" id="aiGenerateBtn" onclick="triggerAI()">Generate Analysis</button>
276
+ <span class="ai-status" id="aiStatus">Ready</span>
277
+ </div>
278
+ </h3>
279
+ <div id="aiInsights" style="max-height:500px;overflow-y:auto">
280
+ <div class="muted" style="padding:20px;text-align:center">
281
+ Click "Generate Analysis" to get AI market commentary.<br>
282
+ <span style="font-size:11px">Requires Ollama running locally, or GROQ_API_KEY / HF_TOKEN set.</span>
283
+ </div>
284
+ </div>
285
+ </div>
286
+ </div>
287
+
288
+ <!-- ═══════════════ TAB: Message Flow ═══════════════ -->
289
+ <div id="tab-devflow" class="tab-content">
290
+ <div class="flow-panel">
291
+ <div class="flow-header">
292
+ <h3 style="color:var(--accent);margin:0">Message Flow Visualizer</h3>
293
+ <div style="display:flex;gap:8px;align-items:center">
294
+ <span class="muted" id="flowCount">0 messages</span>
295
+ <button class="btn-small btn-red" onclick="clearFlowLog()">Clear</button>
296
+ </div>
297
+ </div>
298
+
299
+ <!-- Pipeline diagram -->
300
+ <div class="flow-pipeline">
301
+ <div class="flow-stage stage-oeg"><span class="count" id="flowCntOEG">0</span>OEG</div>
302
+ <div class="flow-arrow">&#9654;</div>
303
+ <div class="flow-stage stage-book"><span class="count" id="flowCntBook">0</span>Book</div>
304
+ <div class="flow-arrow">&#9654;</div>
305
+ <div class="flow-stage stage-match"><span class="count" id="flowCntMatch">0</span>Match</div>
306
+ <div class="flow-arrow">&#9654;</div>
307
+ <div class="flow-stage stage-trade"><span class="count" id="flowCntTrade">0</span>Trade</div>
308
+ <div class="flow-arrow">&#9654;</div>
309
+ <div class="flow-stage stage-db"><span class="count" id="flowCntDB">0</span>DB</div>
310
+ <div class="flow-arrow">&#9654;</div>
311
+ <div class="flow-stage stage-ch"><span class="count" id="flowCntCH">0</span>CH</div>
312
+ </div>
313
+
314
+ <!-- Live message log -->
315
+ <div class="flow-log" id="flowLog">
316
+ <div class="muted" style="padding:20px;text-align:center">
317
+ Waiting for messages... Submit an order or start a session to see the flow.
318
+ </div>
319
+ </div>
320
+ </div>
321
+ </div>
322
+
323
  <!-- Amend Modal -->
324
  <div class="modal-overlay" id="amendModal">
325
  <div class="modal">
 
337
  let chartPeriod = '1h';
338
  let priceChart = null;
339
  const prevPrices = {};
340
+ const flowCounts = {OEG:0, Book:0, Match:0, Trade:0, DB:0, CH:0};
341
+ let flowEntries = [];
342
+ let flowStarted = false;
343
+
344
+ // ── Tab switching ─────────────────────────────────────
345
+ function switchTab(name) {
346
+ document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
347
+ document.querySelectorAll('.tab-content').forEach(t => t.classList.remove('active'));
348
+ document.getElementById('tab-'+name).classList.add('active');
349
+ document.querySelector('.tab-btn[onclick*="'+name+'"]').classList.add('active');
350
+ if (name === 'analyst') loadAIInsights();
351
+ if (name === 'devflow') loadFlowLog();
352
+ }
353
 
354
  async function init() {
355
  const resp = await fetch('/data');
 
410
  state.session = s.status;
411
  renderSession();
412
  });
413
+ es.addEventListener('ai_insight', e => {
414
+ const insight = JSON.parse(e.data);
415
+ addInsightCard(insight);
416
+ const btn = document.getElementById('aiGenerateBtn');
417
+ btn.disabled = false; btn.textContent = 'Generate Analysis';
418
+ document.getElementById('aiStatus').textContent = 'Last: ' + new Date().toLocaleTimeString();
419
+ });
420
+ es.addEventListener('msgflow', e => {
421
+ const entry = JSON.parse(e.data);
422
+ addFlowEntry(entry);
423
+ });
424
  }
425
 
426
  // ── Ticker ────────────────────────────────────────────
 
547
  const opens = bars.map(b => b.open);
548
  const closes = bars.map(b => b.close);
549
  const volumes = bars.map(b => b.volume);
 
550
  const bgColors = bars.map(b => b.close >= b.open ? '#00d4aa44' : '#ff6b6b44');
551
 
552
  if (priceChart) priceChart.destroy();
 
559
  if (!meta || !meta.data.length) return;
560
  const yScale = chart.scales.y;
561
  const xScale = chart.scales.x;
 
562
  meta.data.forEach((point, i) => {
563
  if (i >= bars.length) return;
564
  const x = point.x;
 
568
  const close = yScale.getPixelForValue(closes[i]);
569
  const color = closes[i] >= opens[i] ? '#00d4aa' : '#ff6b6b';
570
  const barW = Math.max(4, (xScale.width / bars.length) * 0.6);
 
571
  ctx.save();
572
+ ctx.strokeStyle = color; ctx.lineWidth = 1;
573
+ ctx.beginPath(); ctx.moveTo(x, high); ctx.lineTo(x, low); ctx.stroke();
 
 
 
 
 
574
  ctx.fillStyle = color;
575
  const top = Math.min(open, close);
576
  const bot = Math.max(open, close);
577
+ ctx.fillRect(x - barW/2, top, barW, Math.max(bot - top, 1));
 
578
  ctx.restore();
579
  });
580
  }
 
585
  data: {
586
  labels,
587
  datasets: [{
588
+ label: symName + ' Close', data: closes, borderColor: 'transparent',
589
+ backgroundColor: 'transparent', pointRadius: 0, borderWidth: 0, yAxisID: 'y',
 
590
  },{
591
+ label: 'Volume', data: volumes, type: 'bar', backgroundColor: bgColors, yAxisID: 'y1',
 
592
  }]
593
  },
594
  options: {
 
598
  legend:{display:false},
599
  tooltip: {
600
  callbacks: {
601
+ label: function(c) {
602
+ const i = c.dataIndex;
603
+ if (c.datasetIndex === 0 && i < bars.length) return 'O:'+fmt(opens[i])+' H:'+fmt(highs[i])+' L:'+fmt(lows[i])+' C:'+fmt(closes[i]);
 
 
604
  return 'Vol: ' + (volumes[i]||0);
605
  }
606
  }
 
640
  }
641
  }
642
 
643
+ // ── AI Analyst ────────────────────────────────────────
644
+ async function loadAIInsights() {
645
+ try {
646
+ const resp = await fetch('/ai/insights');
647
+ const insights = await resp.json();
648
+ const el = document.getElementById('aiInsights');
649
+ if (insights.length === 0) return;
650
+ el.innerHTML = '';
651
+ insights.forEach(addInsightCard);
652
+ } catch(e) {}
653
+ }
654
+
655
+ function addInsightCard(insight) {
656
+ const el = document.getElementById('aiInsights');
657
+ if (el.querySelector('.muted')) el.innerHTML = '';
658
+ const div = document.createElement('div');
659
+ div.className = 'insight-card';
660
+ const t = new Date(insight.timestamp * 1000).toLocaleTimeString();
661
+ div.innerHTML = '<div class="insight-time">' + t +
662
+ ' <span class="insight-src">' + (insight.source||'') + '</span></div>' +
663
+ '<div class="insight-text">' + insight.text + '</div>';
664
+ el.prepend(div);
665
+ while (el.children.length > 10) el.removeChild(el.lastChild);
666
+ }
667
+
668
+ async function triggerAI() {
669
+ const btn = document.getElementById('aiGenerateBtn');
670
+ btn.disabled = true; btn.textContent = 'Generating...';
671
+ document.getElementById('aiStatus').textContent = 'Calling LLM...';
672
+ await fetch('/ai/generate', {method:'POST'});
673
+ }
674
+
675
+ async function onAIProviderChange() {
676
+ const provider = document.getElementById('aiProvider').value;
677
+ document.getElementById('aiBadge').textContent = provider;
678
+ await fetch('/ai/select', {method:'POST', headers:{'Content-Type':'application/json'},
679
+ body: JSON.stringify({provider})});
680
+ }
681
+
682
+ // ── Message Flow Visualizer ───────────────────────────
683
+ async function loadFlowLog() {
684
+ if (flowStarted) return;
685
+ flowStarted = true;
686
+ try {
687
+ const resp = await fetch('/dev/messages?limit=100');
688
+ const msgs = await resp.json();
689
+ msgs.forEach(addFlowEntry);
690
+ } catch(e) {}
691
+ }
692
+
693
+ function addFlowEntry(entry) {
694
+ flowEntries.push(entry);
695
+ if (flowEntries.length > 500) flowEntries.shift();
696
+
697
+ const stage = entry.stage || '?';
698
+ if (flowCounts[stage] !== undefined) {
699
+ flowCounts[stage]++;
700
+ const el = document.getElementById('flowCnt' + stage);
701
+ if (el) el.textContent = flowCounts[stage];
702
+ }
703
+
704
+ const log = document.getElementById('flowLog');
705
+ if (log.querySelector('.muted')) log.innerHTML = '';
706
+
707
+ const div = document.createElement('div');
708
+ div.className = 'flow-entry flow-active';
709
+ const ts = new Date(entry.timestamp * 1000).toLocaleTimeString();
710
+ const tagCls = 'flow-tag-' + stage.toLowerCase();
711
+ div.innerHTML = '<span class="flow-ts">' + ts + '</span>' +
712
+ '<span class="flow-tag ' + tagCls + '">' + stage + '</span>' +
713
+ '<span class="flow-detail">' + (entry.detail||'') + '</span>';
714
+ log.prepend(div);
715
+
716
+ while (log.children.length > 200) log.removeChild(log.lastChild);
717
+ document.getElementById('flowCount').textContent = flowEntries.length + ' messages';
718
+ }
719
+
720
+ function clearFlowLog() {
721
+ flowEntries = [];
722
+ Object.keys(flowCounts).forEach(k => flowCounts[k] = 0);
723
+ document.getElementById('flowLog').innerHTML = '<div class="muted" style="padding:20px;text-align:center">Log cleared</div>';
724
+ ['OEG','Book','Match','Trade','DB','CH'].forEach(s => {
725
+ const el = document.getElementById('flowCnt'+s);
726
+ if (el) el.textContent = '0';
727
+ });
728
+ document.getElementById('flowCount').textContent = '0 messages';
729
+ }
730
+
731
+ // ── Order actions ─────────────────────────────────────
732
  async function submitOrder(side) {
733
  const data = {
734
  symbolIdx: parseInt(document.getElementById('orderSymbol').value),
docs/developers-guide.md CHANGED
@@ -1,6 +1,6 @@
1
  # EuNEx Developers Guide
2
 
3
- **Version 0.7.0** | Euronext Optiq-Modeled Exchange Simulator
4
 
5
  ---
6
 
@@ -20,10 +20,12 @@
20
  12. [Clearing House](#12-clearing-house)
21
  13. [AI Trading Members](#13-ai-trading-members)
22
  14. [Market Simulation](#14-market-simulation)
23
- 15. [Project Structure](#15-project-structure)
24
- 16. [Build & Test](#16-build--test)
25
- 17. [Configuration](#17-configuration)
26
- 18. [Extending EuNEx](#18-extending-eunex)
 
 
27
 
28
  ---
29
 
@@ -949,7 +951,101 @@ The price chart renders OHLCV data as candlestick bars using a custom Chart.js p
949
 
950
  ---
951
 
952
- ## 15. Project Structure
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
953
 
954
  ```
955
  EuNEx/
@@ -1011,7 +1107,7 @@ The price chart renders OHLCV data as candlestick bars using a custom Chart.js p
1011
 
1012
  ---
1013
 
1014
- ## 16. Build & Test
1015
 
1016
  ### Prerequisites
1017
 
@@ -1070,7 +1166,7 @@ cd build && ctest -C Release --output-on-failure
1070
 
1071
  ---
1072
 
1073
- ## 17. Configuration
1074
 
1075
  ### Runtime Configuration (main.cpp)
1076
 
@@ -1099,7 +1195,7 @@ The engine pre-populates order books with spread-defining orders:
1099
 
1100
  ---
1101
 
1102
- ## 18. Extending EuNEx
1103
 
1104
  ### Adding a New Symbol
1105
 
@@ -1148,9 +1244,11 @@ The engine pre-populates order books with spread-defining orders:
1148
  βœ“ Clearing house + AI traders β–‘ EuroCCP/LCH integration
1149
  βœ“ IACA fragments β–‘ IACA FINISH + COPY + IDS
1150
  βœ“ Python bridge (JSON) β–‘ SBE multicast MDG
1151
- βœ“ Market simulation (C++ + Py) β–‘ AI analyst (Ollama/Llama)
1152
- βœ“ Ticker tape + OHLCV charts β–‘ SBE multicast MDG
1153
- βœ“ Daily close persistence β–‘ Multi-day backtesting
 
 
1154
  β–‘ SQLite trade persistence
1155
  β–‘ Developer message visualizer
1156
  β–‘ SATURN ARM (MiFID II RTS 22)
 
1
  # EuNEx Developers Guide
2
 
3
+ **Version 0.8.0** | Euronext Optiq-Modeled Exchange Simulator
4
 
5
  ---
6
 
 
20
  12. [Clearing House](#12-clearing-house)
21
  13. [AI Trading Members](#13-ai-trading-members)
22
  14. [Market Simulation](#14-market-simulation)
23
+ 15. [AI Market Analyst](#15-ai-market-analyst)
24
+ 16. [Developer Message Flow Visualizer](#16-developer-message-flow-visualizer)
25
+ 17. [Project Structure](#17-project-structure)
26
+ 18. [Build & Test](#18-build--test)
27
+ 19. [Configuration](#19-configuration)
28
+ 20. [Extending EuNEx](#20-extending-eunex)
29
 
30
  ---
31
 
 
951
 
952
  ---
953
 
954
+ ## 15. AI Market Analyst
955
+
956
+ The dashboard includes an AI analyst powered by local or cloud LLM providers.
957
+
958
+ ### Provider Fallback Chain
959
+
960
+ ```
961
+ Auto mode tries in order:
962
+ 1. Ollama (local) β†’ POST {OLLAMA_HOST}/api/chat
963
+ 2. Groq (cloud) β†’ POST api.groq.com/openai/v1/chat/completions
964
+ 3. HuggingFace β†’ POST router.huggingface.co/v1/chat/completions
965
+ ```
966
+
967
+ ### Environment Variables
968
+
969
+ | Variable | Default | Description |
970
+ |---------------|-------------------------------|--------------------------|
971
+ | OLLAMA_HOST | http://localhost:11434 | Ollama API endpoint |
972
+ | OLLAMA_MODEL | llama3.2:3b | Default Ollama model |
973
+ | GROQ_API_KEY | (empty) | Groq API key |
974
+ | GROQ_MODEL | llama-3.1-8b-instant | Default Groq model |
975
+ | HF_TOKEN | (empty) | HuggingFace token |
976
+ | HF_MODEL | Qwen/Qwen2.5-7B-Instruct | Default HF model |
977
+
978
+ ### Prompt Template
979
+
980
+ The analyst builds a market context prompt from current trades and order book:
981
+
982
+ ```
983
+ You are a concise financial market analyst for the EuNEx simulated exchange.
984
+ Time: HH:MM:SS | Session: ACTIVE
985
+
986
+ Recent trades:
987
+ AAPL: 12 trade(s), range 153.50-154.50, vol 1200, last 154.10
988
+ MSFT: 8 trade(s), range 323.80-324.20, vol 800, last 324.00
989
+ ...
990
+
991
+ Order book:
992
+ AAPL: Bid 153.80 / Ask 154.20 (spread 0.40)
993
+ ...
994
+
995
+ In 3-4 sentences: activity level, notable moves, market sentiment.
996
+ ```
997
+
998
+ ### API Endpoints
999
+
1000
+ | Endpoint | Method | Description |
1001
+ |-------------------|--------|--------------------------------|
1002
+ | /ai/generate | POST | Trigger async LLM generation |
1003
+ | /ai/insights | GET | Get cached insight history |
1004
+ | /ai/config | GET | Provider availability/status |
1005
+ | /ai/select | POST | Switch provider/model |
1006
+
1007
+ Insights are broadcast via SSE `ai_insight` event and cached in memory (last 20).
1008
+
1009
+ ---
1010
+
1011
+ ## 16. Developer Message Flow Visualizer
1012
+
1013
+ The dashboard includes a "Message Flow" tab that traces the full order lifecycle through all system components. This is a developer tool for understanding and debugging the Optiq-modeled pipeline.
1014
+
1015
+ ### Pipeline Stages
1016
+
1017
+ ```
1018
+ β”Œβ”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”
1019
+ β”‚ OEG β”‚ ─► β”‚ Book β”‚ ─► β”‚ Match β”‚ ─► β”‚ Trade β”‚ ─► β”‚ DB β”‚ ─► β”‚ CH β”‚
1020
+ β””β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”˜
1021
+ Order Insert Fill Record SQLite Clear
1022
+ Entry /Status Partial Trade Persist House
1023
+ ```
1024
+
1025
+ Each stage logs a timestamped message with detail text. Messages flow via SSE `msgflow` events for real-time display.
1026
+
1027
+ ### Implementation
1028
+
1029
+ The visualizer uses Python function patching to intercept the matching engine methods:
1030
+ - `engine.submit_order()` β†’ logs OEG (entry) + Book (status) + Match (fill)
1031
+ - `engine.cancel_order()` β†’ logs OEG (cancel) + Book (cancelled)
1032
+ - `broadcast_event("trade", ...)` β†’ logs Trade + DB + CH steps
1033
+
1034
+ ### API
1035
+
1036
+ | Endpoint | Method | Description |
1037
+ |-------------------|--------|---------------------------------|
1038
+ | /dev/messages | GET | Get recent message log (500 max)|
1039
+
1040
+ ### UI Features
1041
+
1042
+ - **Pipeline counter**: shows cumulative message count per stage
1043
+ - **Live log**: new messages appear at top with highlight animation
1044
+ - **Clear**: resets all counters and log entries
1045
+
1046
+ ---
1047
+
1048
+ ## 17. Project Structure
1049
 
1050
  ```
1051
  EuNEx/
 
1107
 
1108
  ---
1109
 
1110
+ ## 18. Build & Test
1111
 
1112
  ### Prerequisites
1113
 
 
1166
 
1167
  ---
1168
 
1169
+ ## 19. Configuration
1170
 
1171
  ### Runtime Configuration (main.cpp)
1172
 
 
1195
 
1196
  ---
1197
 
1198
+ ## 20. Extending EuNEx
1199
 
1200
  ### Adding a New Symbol
1201
 
 
1244
  βœ“ Clearing house + AI traders β–‘ EuroCCP/LCH integration
1245
  βœ“ IACA fragments β–‘ IACA FINISH + COPY + IDS
1246
  βœ“ Python bridge (JSON) β–‘ SBE multicast MDG
1247
+ βœ“ Market simulation (C++ + Py) β–‘ SBE multicast MDG
1248
+ βœ“ Ticker tape + OHLCV charts β–‘ Multi-day backtesting
1249
+ βœ“ Daily close persistence β–‘ Real Simplx multi-core
1250
+ βœ“ AI Analyst (Ollama/Groq/HF) β–‘ EuroCCP/LCH integration
1251
+ βœ“ Message Flow Visualizer β–‘ FIX 5.0 SP2 + SBE binary
1252
  β–‘ SQLite trade persistence
1253
  β–‘ Developer message visualizer
1254
  β–‘ SATURN ARM (MiFID II RTS 22)