v0.8.0: AI Market Analyst and Developer Message Flow Visualizer
Browse filesF5: 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 +1 -1
- README.md +7 -5
- dashboard/app.py +272 -0
- dashboard/templates/index.html +241 -22
- docs/developers-guide.md +110 -12
CMakeLists.txt
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
cmake_minimum_required(VERSION 3.16)
|
| 2 |
-
project(EuNEx VERSION 0.
|
| 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.
|
| 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.
|
| 224 |
-
7.
|
| 225 |
-
8. **
|
| 226 |
-
9. **
|
|
|
|
|
|
|
|
|
| 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.
|
| 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 |
-
|
| 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 |
-
|
| 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(
|
| 470 |
-
const i =
|
| 471 |
-
if (
|
| 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">▶</div>
|
| 303 |
+
<div class="flow-stage stage-book"><span class="count" id="flowCntBook">0</span>Book</div>
|
| 304 |
+
<div class="flow-arrow">▶</div>
|
| 305 |
+
<div class="flow-stage stage-match"><span class="count" id="flowCntMatch">0</span>Match</div>
|
| 306 |
+
<div class="flow-arrow">▶</div>
|
| 307 |
+
<div class="flow-stage stage-trade"><span class="count" id="flowCntTrade">0</span>Trade</div>
|
| 308 |
+
<div class="flow-arrow">▶</div>
|
| 309 |
+
<div class="flow-stage stage-db"><span class="count" id="flowCntDB">0</span>DB</div>
|
| 310 |
+
<div class="flow-arrow">▶</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.
|
| 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. [
|
| 24 |
-
16. [
|
| 25 |
-
17. [
|
| 26 |
-
18. [
|
|
|
|
|
|
|
| 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.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
##
|
| 1015 |
|
| 1016 |
### Prerequisites
|
| 1017 |
|
|
@@ -1070,7 +1166,7 @@ cd build && ctest -C Release --output-on-failure
|
|
| 1070 |
|
| 1071 |
---
|
| 1072 |
|
| 1073 |
-
##
|
| 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 |
-
##
|
| 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) β‘
|
| 1152 |
-
β Ticker tape + OHLCV charts β‘
|
| 1153 |
-
β Daily close persistence β‘
|
|
|
|
|
|
|
| 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)
|