RayMelius Claude Opus 4.6 commited on
Commit
78d880e
Β·
1 Parent(s): f178600

v0.6.1: Market simulation with realistic prices

Browse files

C++ AITraderActor now uses per-symbol reference prices matching real
market levels instead of hardcoded 100-200 range. Dashboard adds
MarketSimulator thread that auto-generates orders and crossing trades
every 30s when session is active. Config updated with all 7 symbols.

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.6.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.6.1 LANGUAGES CXX)
3
 
4
  set(CMAKE_CXX_STANDARD 20)
5
  set(CMAKE_CXX_STANDARD_REQUIRED ON)
README.md CHANGED
@@ -219,7 +219,8 @@ EuNEx/
219
  2. ~~Kafka persistence~~ βœ“ KafkaStore + Docker Compose (KRaft mode)
220
  3. ~~FIX gateway~~ βœ“ C++ FIXAcceptorActor + Python fallback
221
  4. ~~Clearing House~~ βœ“ ClearingHouseActor + AITraderActor
222
- 5. **SBE encoding** β€” replace event structs with SBE-encoded messages
223
- 6. **Master/Mirror failover** β€” implement full Recovery replay on Mirror node
224
- 7. **Trading phases** β€” pre-open, uncrossing, continuous, close, TAL
225
- 8. **Additional order types** β€” Stop, Pegged, Mid-Point, Iceberg
 
 
219
  2. ~~Kafka persistence~~ βœ“ KafkaStore + Docker Compose (KRaft mode)
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
dashboard/app.py CHANGED
@@ -18,6 +18,7 @@ import json
18
  import time
19
  import queue
20
  import threading
 
21
  import sys
22
  import os
23
  import urllib.request
@@ -30,7 +31,7 @@ from dashboard.database import (
30
  init_db, save_order, save_trade, record_ohlcv,
31
  get_ohlcv, get_recent_orders, get_recent_trades, get_active_orders,
32
  )
33
- from shared.config import SYMBOLS, DASHBOARD_PORT, DASHBOARD_DB, CH_URL
34
 
35
  app = Flask(__name__)
36
 
@@ -397,6 +398,7 @@ def session_start():
397
  session_status = "active"
398
  broadcast_event("session", {"status": session_status})
399
  _notify_ch("start")
 
400
  return jsonify({"status": session_status})
401
 
402
  @app.route("/session/suspend", methods=["POST"])
@@ -428,6 +430,18 @@ def session_status_route():
428
  return jsonify({"status": session_status})
429
 
430
 
 
 
 
 
 
 
 
 
 
 
 
 
431
  def _notify_ch(action):
432
  try:
433
  data = json.dumps({"action": action}).encode()
@@ -442,10 +456,99 @@ def _notify_ch(action):
442
  pass
443
 
444
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
445
  if __name__ == "__main__":
446
  os.makedirs(os.path.dirname(db_path), exist_ok=True)
447
  init_db(db_path)
 
448
  port = DASHBOARD_PORT
449
  print(f"EuNEx Dashboard starting on http://localhost:{port}")
450
  print(f" Database: {db_path}")
 
451
  app.run(host="0.0.0.0", port=port, debug=True, threaded=True)
 
18
  import time
19
  import queue
20
  import threading
21
+ import random
22
  import sys
23
  import os
24
  import urllib.request
 
31
  init_db, save_order, save_trade, record_ohlcv,
32
  get_ohlcv, get_recent_orders, get_recent_trades, get_active_orders,
33
  )
34
+ from shared.config import SYMBOLS, DASHBOARD_PORT, DASHBOARD_DB, CH_URL, SIM_INTERVAL, SIM_ORDERS_PER_ROUND
35
 
36
  app = Flask(__name__)
37
 
 
398
  session_status = "active"
399
  broadcast_event("session", {"status": session_status})
400
  _notify_ch("start")
401
+ _seed_initial_orders()
402
  return jsonify({"status": session_status})
403
 
404
  @app.route("/session/suspend", methods=["POST"])
 
430
  return jsonify({"status": session_status})
431
 
432
 
433
+ def _seed_initial_orders():
434
+ for sym_id, info in symbols.items():
435
+ ref = info["startPrice"]
436
+ for offset, qty in [(-0.50, 150), (-1.00, 100), (0.50, 200), (1.00, 100)]:
437
+ side = "Buy" if offset < 0 else "Sell"
438
+ engine.submit_order(
439
+ symbol_id=sym_id, side=side, order_type="Limit",
440
+ price=round(ref + offset, 2), quantity=qty,
441
+ tif="Day", source="seed",
442
+ )
443
+
444
+
445
  def _notify_ch(action):
446
  try:
447
  data = json.dumps({"action": action}).encode()
 
456
  pass
457
 
458
 
459
+ # ── Market Simulation ──────────────────────────────────────────────
460
+
461
+ class MarketSimulator:
462
+ def __init__(self, engine_ref, interval=SIM_INTERVAL, orders_per_round=SIM_ORDERS_PER_ROUND):
463
+ self.engine = engine_ref
464
+ self.interval = interval
465
+ self.orders_per_round = orders_per_round
466
+ self._thread = None
467
+ self._running = False
468
+ self._ref_prices = {sid: info["startPrice"] for sid, info in symbols.items()}
469
+
470
+ def start(self):
471
+ if self._running:
472
+ return
473
+ self._running = True
474
+ self._thread = threading.Thread(target=self._run, daemon=True)
475
+ self._thread.start()
476
+
477
+ def stop(self):
478
+ self._running = False
479
+
480
+ def _run(self):
481
+ while self._running:
482
+ if session_status == "active":
483
+ self._round()
484
+ time.sleep(self.interval)
485
+
486
+ def _round(self):
487
+ sym_ids = list(symbols.keys())
488
+ random.shuffle(sym_ids)
489
+
490
+ for sym_id in sym_ids:
491
+ ref = self._current_ref(sym_id)
492
+ for _ in range(self.orders_per_round):
493
+ side = random.choice(["Buy", "Sell"])
494
+ spread_pct = random.uniform(-0.005, 0.005)
495
+ price = round(ref * (1 + spread_pct), 2)
496
+ if price <= 0:
497
+ price = 0.01
498
+ qty = random.randint(10, 200)
499
+
500
+ self.engine.submit_order(
501
+ symbol_id=sym_id,
502
+ side=side,
503
+ order_type="Limit",
504
+ price=price,
505
+ quantity=qty,
506
+ tif="Day",
507
+ source="sim",
508
+ )
509
+
510
+ cross_side = random.choice(["Buy", "Sell"])
511
+ snap = snapshots.get(sym_id, {})
512
+ if cross_side == "Buy" and snap.get("bestAsk", 0) > 0:
513
+ cross_price = snap["bestAsk"] + 0.01
514
+ elif cross_side == "Sell" and snap.get("bestBid", 0) > 0:
515
+ cross_price = snap["bestBid"] - 0.01
516
+ else:
517
+ cross_price = ref
518
+ if cross_price <= 0:
519
+ cross_price = 0.01
520
+ cross_qty = random.randint(10, 50)
521
+
522
+ self.engine.submit_order(
523
+ symbol_id=sym_id,
524
+ side=cross_side,
525
+ order_type="Limit",
526
+ price=round(cross_price, 2),
527
+ quantity=cross_qty,
528
+ tif="Day",
529
+ source="sim",
530
+ )
531
+
532
+ def _current_ref(self, sym_id):
533
+ snap = snapshots.get(sym_id, {})
534
+ bid = snap.get("bestBid", 0)
535
+ ask = snap.get("bestAsk", 0)
536
+ if bid > 0 and ask > 0:
537
+ return (bid + ask) / 2
538
+ if snap.get("lastPrice", 0) > 0:
539
+ return snap["lastPrice"]
540
+ return self._ref_prices.get(sym_id, 100.0)
541
+
542
+
543
+ simulator = MarketSimulator(engine)
544
+
545
+
546
  if __name__ == "__main__":
547
  os.makedirs(os.path.dirname(db_path), exist_ok=True)
548
  init_db(db_path)
549
+ simulator.start()
550
  port = DASHBOARD_PORT
551
  print(f"EuNEx Dashboard starting on http://localhost:{port}")
552
  print(f" Database: {db_path}")
553
+ print(f" Simulation: every {SIM_INTERVAL}s, {SIM_ORDERS_PER_ROUND} orders/symbol/round")
554
  app.run(host="0.0.0.0", port=port, debug=True, threaded=True)
docs/developers-guide.md CHANGED
@@ -1,6 +1,6 @@
1
  # EuNEx Developers Guide
2
 
3
- **Version 0.6.0** | Euronext Optiq-Modeled Exchange Simulator
4
 
5
  ---
6
 
@@ -19,10 +19,11 @@
19
  11. [FIX Protocol Gateway](#11-fix-protocol-gateway)
20
  12. [Clearing House](#12-clearing-house)
21
  13. [AI Trading Members](#13-ai-trading-members)
22
- 14. [Project Structure](#14-project-structure)
23
- 15. [Build & Test](#15-build--test)
24
- 16. [Configuration](#16-configuration)
25
- 17. [Extending EuNEx](#17-extending-eunex)
 
26
 
27
  ---
28
 
@@ -856,7 +857,63 @@ docker compose up --build
856
 
857
  ---
858
 
859
- ## 14. Project Structure
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
860
 
861
  ```
862
  EuNEx/
@@ -918,7 +975,7 @@ docker compose up --build
918
 
919
  ---
920
 
921
- ## 15. Build & Test
922
 
923
  ### Prerequisites
924
 
@@ -977,7 +1034,7 @@ cd build && ctest -C Release --output-on-failure
977
 
978
  ---
979
 
980
- ## 16. Configuration
981
 
982
  ### Runtime Configuration (main.cpp)
983
 
@@ -1006,7 +1063,7 @@ The engine pre-populates order books with spread-defining orders:
1006
 
1007
  ---
1008
 
1009
- ## 17. Extending EuNEx
1010
 
1011
  ### Adding a New Symbol
1012
 
@@ -1055,7 +1112,7 @@ The engine pre-populates order books with spread-defining orders:
1055
  βœ“ Clearing house + AI traders β–‘ EuroCCP/LCH integration
1056
  βœ“ IACA fragments β–‘ IACA FINISH + COPY + IDS
1057
  βœ“ Python bridge (JSON) β–‘ SBE multicast MDG
1058
- β–‘ Dashboard ticker + charts
1059
  β–‘ AI analyst (Ollama/Llama)
1060
  β–‘ SQLite trade persistence
1061
  β–‘ Developer message visualizer
 
1
  # EuNEx Developers Guide
2
 
3
+ **Version 0.6.1** | Euronext Optiq-Modeled Exchange Simulator
4
 
5
  ---
6
 
 
19
  11. [FIX Protocol Gateway](#11-fix-protocol-gateway)
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
 
 
857
 
858
  ---
859
 
860
+ ## 14. Market Simulation
861
+
862
+ Both the C++ engine and the Python dashboard include market simulation to continuously generate orders and trades.
863
+
864
+ ### C++ Engine (AITraderActor)
865
+
866
+ The `AITraderActor` generates orders every ~3 seconds per round. Each of the 10 AI members picks a random symbol and applies its strategy (Momentum, Mean Reversion, or Random). All strategies use per-symbol **reference prices** matching real market levels:
867
+
868
+ | Symbol | Reference Price |
869
+ |--------|----------------|
870
+ | AAPL | $154.00 |
871
+ | MSFT | $324.00 |
872
+ | GOOGL | $141.00 |
873
+ | TSLA | $375.00 |
874
+ | NVDA | $201.00 |
875
+ | AMD | $320.00 |
876
+ | ENX | €146.00 |
877
+
878
+ When no BBO data is available yet, the fallback `submitOrder()` generates prices within Β±3 ticks of the reference price instead of random values.
879
+
880
+ ### Python Dashboard (MarketSimulator)
881
+
882
+ The dashboard runs a `MarketSimulator` thread that generates orders when the session status is `"active"`:
883
+
884
+ ```
885
+ Configuration (shared/config.py):
886
+ SIM_INTERVAL = 30s (env: EUNEX_SIM_INTERVAL)
887
+ SIM_ORDERS_PER_ROUND = 4 (env: EUNEX_SIM_ORDERS)
888
+
889
+ Per round (every SIM_INTERVAL seconds):
890
+ For each of 7 symbols:
891
+ 1. Submit SIM_ORDERS_PER_ROUND limit orders near current mid price (Β±0.5%)
892
+ 2. Submit 1 crossing order that will match against the book
893
+ β†’ Generates resting orders + at least 1 trade per symbol per round
894
+ ```
895
+
896
+ **Order flow:**
897
+ - Session "Start Day" β†’ seeds initial orders (2 bid + 2 ask per symbol)
898
+ - Simulation thread wakes every 30s β†’ submits ~35 orders β†’ ~7 trades
899
+ - All trades are persisted to SQLite (trades table + OHLCV aggregation)
900
+ - Dashboard chart shows candlestick data from OHLCV buckets
901
+
902
+ ### Seed Orders (Session Start)
903
+
904
+ When the dashboard session starts, 4 orders are seeded per symbol to establish a market:
905
+
906
+ ```
907
+ For each symbol at startPrice:
908
+ Buy @ startPrice - 1.00, qty=100
909
+ Buy @ startPrice - 0.50, qty=150
910
+ Sell @ startPrice + 0.50, qty=200
911
+ Sell @ startPrice + 1.00, qty=100
912
+ ```
913
+
914
+ ---
915
+
916
+ ## 15. Project Structure
917
 
918
  ```
919
  EuNEx/
 
975
 
976
  ---
977
 
978
+ ## 16. Build & Test
979
 
980
  ### Prerequisites
981
 
 
1034
 
1035
  ---
1036
 
1037
+ ## 17. Configuration
1038
 
1039
  ### Runtime Configuration (main.cpp)
1040
 
 
1063
 
1064
  ---
1065
 
1066
+ ## 18. Extending EuNEx
1067
 
1068
  ### Adding a New Symbol
1069
 
 
1112
  βœ“ Clearing house + AI traders β–‘ EuroCCP/LCH integration
1113
  βœ“ IACA fragments β–‘ IACA FINISH + COPY + IDS
1114
  βœ“ Python bridge (JSON) β–‘ SBE multicast MDG
1115
+ βœ“ Market simulation (C++ + Py) β–‘ Dashboard ticker + charts
1116
  β–‘ AI analyst (Ollama/Llama)
1117
  β–‘ SQLite trade persistence
1118
  β–‘ Developer message visualizer
shared/config.py CHANGED
@@ -27,12 +27,19 @@ KAFKA_TOPIC_CONTROL = "eunex.control"
27
 
28
  # ── Symbols ────────────────────────────────────────────────────────
29
  SYMBOLS = {
30
- 1: {"name": "AAPL", "segment": "EQU", "startPrice": 185.0, "tickSize": 0.01, "lotSize": 1},
31
- 2: {"name": "MSFT", "segment": "EQU", "startPrice": 420.0, "tickSize": 0.01, "lotSize": 1},
32
- 3: {"name": "GOOGL", "segment": "EQU", "startPrice": 175.0, "tickSize": 0.01, "lotSize": 1},
33
- 4: {"name": "EURO50", "segment": "EQD", "startPrice": 5050.0, "tickSize": 0.5, "lotSize": 1},
 
 
 
34
  }
35
 
 
 
 
 
36
  # ── Clearing House ─────────────────────────────────────────────────
37
  CH_MEMBERS = {
38
  f"MBR{i:02d}": {"capital": 100_000.0} for i in range(1, 11)
 
27
 
28
  # ── Symbols ────────────────────────────────────────────────────────
29
  SYMBOLS = {
30
+ 1: {"name": "AAPL", "segment": "EQU", "startPrice": 154.0, "tickSize": 0.01, "lotSize": 1},
31
+ 2: {"name": "MSFT", "segment": "EQU", "startPrice": 324.0, "tickSize": 0.01, "lotSize": 1},
32
+ 3: {"name": "GOOGL", "segment": "EQU", "startPrice": 141.0, "tickSize": 0.01, "lotSize": 1},
33
+ 4: {"name": "TSLA", "segment": "EQU", "startPrice": 375.0, "tickSize": 0.01, "lotSize": 1},
34
+ 5: {"name": "NVDA", "segment": "EQU", "startPrice": 201.0, "tickSize": 0.01, "lotSize": 1},
35
+ 6: {"name": "AMD", "segment": "EQU", "startPrice": 320.0, "tickSize": 0.01, "lotSize": 1},
36
+ 7: {"name": "ENX", "segment": "EQU", "startPrice": 146.0, "tickSize": 0.01, "lotSize": 1},
37
  }
38
 
39
+ # ── Simulation ─────────────────────────────────────────────────────
40
+ SIM_INTERVAL = int(os.environ.get("EUNEX_SIM_INTERVAL", 30))
41
+ SIM_ORDERS_PER_ROUND = int(os.environ.get("EUNEX_SIM_ORDERS", 4))
42
+
43
  # ── Clearing House ─────────────────────────────────────────────────
44
  CH_MEMBERS = {
45
  f"MBR{i:02d}": {"capital": 100_000.0} for i in range(1, 11)
src/actors/AITraderActor.cpp CHANGED
@@ -89,13 +89,30 @@ void AITraderActor::tradeRound() {
89
  }
90
  }
91
 
 
 
 
 
 
 
 
 
 
 
 
 
 
92
  void AITraderActor::submitOrder(const AITraderMember& member, SymbolIndex_t symIdx) {
93
  std::uniform_int_distribution<int> sideDist(0, 1);
94
- std::uniform_int_distribution<int> priceDist(100, 200);
95
  std::uniform_int_distribution<int> qtyDist(10, 100);
96
 
97
  Side side = sideDist(rng_) ? Side::Buy : Side::Sell;
98
- Price_t price = toFixedPrice(priceDist(rng_) * 1.0);
 
 
 
 
 
99
  Quantity_t qty = qtyDist(rng_);
100
  ClOrdId_t clOrdId = nextClOrdId_++;
101
 
@@ -159,7 +176,7 @@ void AITraderActor::strategyRandom(const AITraderMember& member, SymbolIndex_t s
159
  Side side = sideDist(rng_) ? Side::Buy : Side::Sell;
160
 
161
  Price_t midPrice = (bbo.bestBid + bbo.bestAsk) / 2;
162
- if (midPrice == 0) midPrice = toFixedPrice(150.0);
163
 
164
  std::uniform_int_distribution<int> spreadDist(-5, 5);
165
  Price_t tickOffset = spreadDist(rng_) * (PRICE_SCALE / 100);
 
89
  }
90
  }
91
 
92
+ Price_t AITraderActor::referencePrice(SymbolIndex_t sym) {
93
+ switch (sym) {
94
+ case 1: return toFixedPrice(154.0); // AAPL
95
+ case 2: return toFixedPrice(324.0); // MSFT
96
+ case 3: return toFixedPrice(141.0); // GOOGL
97
+ case 4: return toFixedPrice(375.0); // TSLA
98
+ case 5: return toFixedPrice(201.0); // NVDA
99
+ case 6: return toFixedPrice(320.0); // AMD
100
+ case 7: return toFixedPrice(146.0); // ENX
101
+ default: return toFixedPrice(100.0);
102
+ }
103
+ }
104
+
105
  void AITraderActor::submitOrder(const AITraderMember& member, SymbolIndex_t symIdx) {
106
  std::uniform_int_distribution<int> sideDist(0, 1);
 
107
  std::uniform_int_distribution<int> qtyDist(10, 100);
108
 
109
  Side side = sideDist(rng_) ? Side::Buy : Side::Sell;
110
+ Price_t refPrice = referencePrice(symIdx);
111
+ std::uniform_int_distribution<int> spreadDist(-3, 3);
112
+ Price_t tickOffset = spreadDist(rng_) * (PRICE_SCALE / 100);
113
+ Price_t price = refPrice + tickOffset;
114
+ if (price <= 0) price = PRICE_SCALE;
115
+
116
  Quantity_t qty = qtyDist(rng_);
117
  ClOrdId_t clOrdId = nextClOrdId_++;
118
 
 
176
  Side side = sideDist(rng_) ? Side::Buy : Side::Sell;
177
 
178
  Price_t midPrice = (bbo.bestBid + bbo.bestAsk) / 2;
179
+ if (midPrice == 0) midPrice = referencePrice(symIdx);
180
 
181
  std::uniform_int_distribution<int> spreadDist(-5, 5);
182
  Price_t tickOffset = spreadDist(rng_) * (PRICE_SCALE / 100);
src/actors/AITraderActor.hpp CHANGED
@@ -55,6 +55,8 @@ private:
55
  static constexpr int TRADE_INTERVAL_MS = 30000;
56
  static constexpr int MAX_PRICE_HISTORY = 50;
57
 
 
 
58
  tredzone::Actor::Event::Pipe oePipe_;
59
  std::vector<SymbolIndex_t> symbols_;
60
  std::vector<AITraderMember> members_;
 
55
  static constexpr int TRADE_INTERVAL_MS = 30000;
56
  static constexpr int MAX_PRICE_HISTORY = 50;
57
 
58
+ static Price_t referencePrice(SymbolIndex_t sym);
59
+
60
  tredzone::Actor::Event::Pipe oePipe_;
61
  std::vector<SymbolIndex_t> symbols_;
62
  std::vector<AITraderMember> members_;