v0.6.1: Market simulation with realistic prices
Browse filesC++ 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 +1 -1
- README.md +5 -4
- dashboard/app.py +104 -1
- docs/developers-guide.md +67 -10
- shared/config.py +11 -4
- src/actors/AITraderActor.cpp +20 -3
- src/actors/AITraderActor.hpp +2 -0
CMakeLists.txt
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
cmake_minimum_required(VERSION 3.16)
|
| 2 |
-
project(EuNEx VERSION 0.6.
|
| 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.
|
| 223 |
-
6. **
|
| 224 |
-
7. **
|
| 225 |
-
8. **
|
|
|
|
|
|
| 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.
|
| 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. [
|
| 23 |
-
15. [
|
| 24 |
-
16. [
|
| 25 |
-
17. [
|
|
|
|
| 26 |
|
| 27 |
---
|
| 28 |
|
|
@@ -856,7 +857,63 @@ docker compose up --build
|
|
| 856 |
|
| 857 |
---
|
| 858 |
|
| 859 |
-
## 14.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 860 |
|
| 861 |
```
|
| 862 |
EuNEx/
|
|
@@ -918,7 +975,7 @@ docker compose up --build
|
|
| 918 |
|
| 919 |
---
|
| 920 |
|
| 921 |
-
##
|
| 922 |
|
| 923 |
### Prerequisites
|
| 924 |
|
|
@@ -977,7 +1034,7 @@ cd build && ctest -C Release --output-on-failure
|
|
| 977 |
|
| 978 |
---
|
| 979 |
|
| 980 |
-
##
|
| 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 |
-
##
|
| 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 |
-
|
| 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",
|
| 31 |
-
2: {"name": "MSFT",
|
| 32 |
-
3: {"name": "GOOGL", "segment": "EQU", "startPrice":
|
| 33 |
-
4: {"name": "
|
|
|
|
|
|
|
|
|
|
| 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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 =
|
| 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_;
|