RayMelius Claude Opus 4.6 commited on
Commit
b32ac07
·
1 Parent(s): 1a2d125

Add runtime NN model switching: LLM, NN1 (Adilbai), NN2 (RayMelius)

Browse files

Support dual NN model slots in ch_rl_trader with lazy loading per slot.
Six strategies: hybrid (USR01-04 LLM, USR05-07 NN1, USR08-10 NN2),
hybrid-nn1, hybrid-nn2, nn1, nn2, llm. Right-click context menu and
badges updated for NN1/NN2 distinction.

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

clearing_house/app.py CHANGED
@@ -111,17 +111,7 @@ def _build_leaderboard(bbos: dict) -> list[dict]:
111
  row["total_value"] = round(row["capital"] + holdings_value, 2)
112
  row["pnl"] = round(row["total_value"] - db.CH_STARTING_CAPITAL, 2)
113
  row["is_human"] = ai_trader.is_human_active(row["member_id"])
114
- # Determine AI type for this member
115
- strategy = ai_trader.get_strategy()
116
- if row["is_human"]:
117
- row["ai_type"] = "Human"
118
- elif strategy == "rl":
119
- row["ai_type"] = "NN"
120
- elif strategy == "llm":
121
- row["ai_type"] = "LLM"
122
- else: # hybrid
123
- member_num = int(row["member_id"][-2:])
124
- row["ai_type"] = "NN" if member_num <= 5 else "LLM"
125
  # Sort by total_value descending
126
  rows.sort(key=lambda r: r["total_value"], reverse=True)
127
  for i, row in enumerate(rows):
@@ -412,12 +402,24 @@ def _member_ai_type(member_id: str) -> str:
412
  if ai_trader.is_human_active(member_id):
413
  return "Human"
414
  strategy = ai_trader.get_strategy()
415
- if strategy == "rl":
416
- return "NN"
417
  if strategy == "llm":
418
  return "LLM"
419
  member_num = int(member_id[-2:])
420
- return "NN" if member_num <= 5 else "LLM"
 
 
 
 
 
 
 
 
 
 
 
 
421
 
422
 
423
  @app.route("/ch/api/market")
@@ -427,11 +429,17 @@ def api_market():
427
 
428
  @app.route("/ch/api/config")
429
  def api_config():
430
- return jsonify({
431
  "strategy": ai_trader.get_strategy(),
432
  "obligation": db.CH_DAILY_OBLIGATION,
433
  "ai_interval": int(os.getenv("CH_AI_INTERVAL", "45")),
434
- })
 
 
 
 
 
 
435
 
436
 
437
  @app.route("/ch/api/strategy", methods=["POST"])
 
111
  row["total_value"] = round(row["capital"] + holdings_value, 2)
112
  row["pnl"] = round(row["total_value"] - db.CH_STARTING_CAPITAL, 2)
113
  row["is_human"] = ai_trader.is_human_active(row["member_id"])
114
+ row["ai_type"] = _member_ai_type(row["member_id"])
 
 
 
 
 
 
 
 
 
 
115
  # Sort by total_value descending
116
  rows.sort(key=lambda r: r["total_value"], reverse=True)
117
  for i, row in enumerate(rows):
 
402
  if ai_trader.is_human_active(member_id):
403
  return "Human"
404
  strategy = ai_trader.get_strategy()
405
+ if strategy in ("nn1", "nn2"):
406
+ return strategy.upper()
407
  if strategy == "llm":
408
  return "LLM"
409
  member_num = int(member_id[-2:])
410
+ if strategy == "hybrid":
411
+ # USR01-04 LLM, USR05-07 NN1, USR08-10 NN2
412
+ if member_num <= 4:
413
+ return "LLM"
414
+ elif member_num <= 7:
415
+ return "NN1"
416
+ else:
417
+ return "NN2"
418
+ # hybrid-nn1 or hybrid-nn2
419
+ if strategy.startswith("hybrid-"):
420
+ nn_slot = strategy.split("-", 1)[1].upper()
421
+ return nn_slot if member_num <= 5 else "LLM"
422
+ return "LLM"
423
 
424
 
425
  @app.route("/ch/api/market")
 
429
 
430
  @app.route("/ch/api/config")
431
  def api_config():
432
+ result = {
433
  "strategy": ai_trader.get_strategy(),
434
  "obligation": db.CH_DAILY_OBLIGATION,
435
  "ai_interval": int(os.getenv("CH_AI_INTERVAL", "45")),
436
+ }
437
+ try:
438
+ from ch_rl_trader import get_model_info
439
+ result["nn_models"] = get_model_info()
440
+ except Exception:
441
+ pass
442
+ return jsonify(result)
443
 
444
 
445
  @app.route("/ch/api/strategy", methods=["POST"])
clearing_house/ch_ai_trader.py CHANGED
@@ -7,7 +7,14 @@ Three background threads:
7
  for each unoccupied member using the configured strategy.
8
  3. _control_listener_thread – listens for session start/stop/suspend/resume.
9
 
10
- Strategy is selected via CH_AI_STRATEGY env var: "llm", "rl", or "hybrid".
 
 
 
 
 
 
 
11
 
12
  Call start() once from app.py after init_db().
13
  Call set_human_active(member_id) / set_human_inactive(member_id) on login/logout.
@@ -41,7 +48,11 @@ except ImportError:
41
 
42
  # ── Config ─────────────────────────────────────────────────────────────────────
43
  CH_AI_INTERVAL = int(os.getenv("CH_AI_INTERVAL", "45")) # seconds between AI cycles
44
- CH_AI_STRATEGY = os.getenv("CH_AI_STRATEGY", "hybrid") # "llm", "rl", or "hybrid"
 
 
 
 
45
  CH_SOURCE = "CLRH"
46
 
47
  OLLAMA_HOST = os.getenv("OLLAMA_HOST", "")
@@ -91,12 +102,21 @@ def set_strategy(strategy: str) -> str:
91
  """Dynamically switch AI strategy. Returns the active strategy."""
92
  global CH_AI_STRATEGY
93
  strategy = strategy.lower().strip()
94
- if strategy not in ("llm", "rl", "hybrid"):
 
 
95
  return CH_AI_STRATEGY
96
- if strategy in ("rl", "hybrid") and not _rl_available:
97
  print(f"[CH-AI] Cannot switch to {strategy}: RL deps not installed")
98
  return CH_AI_STRATEGY
99
  CH_AI_STRATEGY = strategy
 
 
 
 
 
 
 
100
  print(f"[CH-AI] Strategy switched to: {strategy}")
101
  return CH_AI_STRATEGY
102
 
@@ -109,9 +129,9 @@ def start() -> None:
109
  threading.Thread(target=_simulation_thread, daemon=True, name="ch-ai-sim").start()
110
  threading.Thread(target=_control_listener_thread, daemon=True, name="ch-control").start()
111
  strategy = CH_AI_STRATEGY
112
- if strategy in ("rl", "hybrid") and not _rl_available:
113
  strategy = "llm"
114
- print("[CH-AI] WARNING: RL requested but deps missing, falling back to LLM")
115
  print(f"[CH-AI] Background threads started (strategy={strategy})")
116
 
117
 
@@ -258,25 +278,38 @@ def _decide_order(member_id, capital, holdings, daily_trades, bbos, obligation_r
258
  """Dispatch to the configured strategy."""
259
  strategy = CH_AI_STRATEGY
260
 
261
- # Hybrid: split members between RL and LLM
 
262
  if strategy == "hybrid" and _rl_available:
263
- member_num = int(member_id[-2:])
264
- strategy = "rl" if member_num <= 5 else "llm"
265
-
266
- if strategy == "rl" and _rl_available:
 
 
 
 
 
 
 
 
 
 
 
267
  try:
268
  order = rl_trader.decide_order_rl(
269
  member_id, capital, holdings, bbos, obligation_remaining,
 
270
  )
271
  if order and _validate_order(order, capital, holdings, bbos):
272
  try:
273
- db.record_ai_decision(member_id, f"RL: {order}", order, source="rl")
274
  except Exception:
275
  pass
276
  return order
277
  except Exception as e:
278
- print(f"[CH-AI] RL strategy error for {member_id}: {e}")
279
- # Fall through to LLM on RL failure
280
  return _decide_order_llm(
281
  member_id, capital, holdings, daily_trades, bbos, obligation_remaining,
282
  )
 
7
  for each unoccupied member using the configured strategy.
8
  3. _control_listener_thread – listens for session start/stop/suspend/resume.
9
 
10
+ Strategy is selected via CH_AI_STRATEGY env var:
11
+ "hybrid" – USR01-04 LLM, USR05-07 NN1, USR08-10 NN2 (default)
12
+ "hybrid-nn1" – USR01-05 NN1, USR06-10 LLM
13
+ "hybrid-nn2" – USR01-05 NN2, USR06-10 LLM
14
+ "llm" – all members use LLM
15
+ "nn1" – all members use NN1 (Adilbai/stock-trading-rl-agent)
16
+ "nn2" – all members use NN2 (RayMelius/stockex-nn-agent)
17
+ Legacy alias: "rl" → "nn1"
18
 
19
  Call start() once from app.py after init_db().
20
  Call set_human_active(member_id) / set_human_inactive(member_id) on login/logout.
 
48
 
49
  # ── Config ─────────────────────────────────────────────────────────────────────
50
  CH_AI_INTERVAL = int(os.getenv("CH_AI_INTERVAL", "45")) # seconds between AI cycles
51
+ # Normalize legacy strategy names on startup
52
+ _raw_strategy = os.getenv("CH_AI_STRATEGY", "hybrid")
53
+ _STRATEGY_ALIASES = {"rl": "nn1", "hybrid-nn1": "hybrid-nn1", "hybrid-nn2": "hybrid-nn2"}
54
+ CH_AI_STRATEGY = _STRATEGY_ALIASES.get(_raw_strategy, _raw_strategy)
55
+ VALID_STRATEGIES = {"llm", "nn1", "nn2", "hybrid", "hybrid-nn1", "hybrid-nn2"}
56
  CH_SOURCE = "CLRH"
57
 
58
  OLLAMA_HOST = os.getenv("OLLAMA_HOST", "")
 
102
  """Dynamically switch AI strategy. Returns the active strategy."""
103
  global CH_AI_STRATEGY
104
  strategy = strategy.lower().strip()
105
+ # Support legacy aliases
106
+ strategy = _STRATEGY_ALIASES.get(strategy, strategy)
107
+ if strategy not in VALID_STRATEGIES:
108
  return CH_AI_STRATEGY
109
+ if strategy != "llm" and not _rl_available:
110
  print(f"[CH-AI] Cannot switch to {strategy}: RL deps not installed")
111
  return CH_AI_STRATEGY
112
  CH_AI_STRATEGY = strategy
113
+ # Tell RL trader which model slot to use
114
+ if _rl_available and strategy in ("nn1", "nn2"):
115
+ rl_trader.set_active_model(strategy)
116
+ elif _rl_available and strategy.startswith("hybrid-"):
117
+ nn_slot = strategy.split("-", 1)[1]
118
+ rl_trader.set_active_model(nn_slot)
119
+ # "hybrid" uses both nn1 and nn2, no single active model to set
120
  print(f"[CH-AI] Strategy switched to: {strategy}")
121
  return CH_AI_STRATEGY
122
 
 
129
  threading.Thread(target=_simulation_thread, daemon=True, name="ch-ai-sim").start()
130
  threading.Thread(target=_control_listener_thread, daemon=True, name="ch-control").start()
131
  strategy = CH_AI_STRATEGY
132
+ if strategy != "llm" and not _rl_available:
133
  strategy = "llm"
134
+ print("[CH-AI] WARNING: NN requested but deps missing, falling back to LLM")
135
  print(f"[CH-AI] Background threads started (strategy={strategy})")
136
 
137
 
 
278
  """Dispatch to the configured strategy."""
279
  strategy = CH_AI_STRATEGY
280
 
281
+ # Hybrid modes: split members between strategies
282
+ member_num = int(member_id[-2:])
283
  if strategy == "hybrid" and _rl_available:
284
+ # Default hybrid: USR01-04 LLM, USR05-07 NN1, USR08-10 NN2
285
+ if member_num <= 4:
286
+ strategy = "llm"
287
+ elif member_num <= 7:
288
+ strategy = "nn1"
289
+ else:
290
+ strategy = "nn2"
291
+ elif strategy.startswith("hybrid-") and _rl_available:
292
+ nn_slot = strategy.split("-", 1)[1] # "nn1" or "nn2"
293
+ if member_num <= 5:
294
+ strategy = nn_slot
295
+ else:
296
+ strategy = "llm"
297
+
298
+ if strategy in ("nn1", "nn2") and _rl_available:
299
  try:
300
  order = rl_trader.decide_order_rl(
301
  member_id, capital, holdings, bbos, obligation_remaining,
302
+ model_slot=strategy,
303
  )
304
  if order and _validate_order(order, capital, holdings, bbos):
305
  try:
306
+ db.record_ai_decision(member_id, f"RL({strategy}): {order}", order, source=strategy)
307
  except Exception:
308
  pass
309
  return order
310
  except Exception as e:
311
+ print(f"[CH-AI] {strategy} strategy error for {member_id}: {e}")
312
+ # Fall through to LLM on NN failure
313
  return _decide_order_llm(
314
  member_id, capital, holdings, daily_trades, bbos, obligation_remaining,
315
  )
clearing_house/ch_rl_trader.py CHANGED
@@ -1,4 +1,8 @@
1
- """RL-based trading strategy using Adilbai/stock-trading-rl-agent (PPO).
 
 
 
 
2
 
3
  Provides decide_order_rl() with the same return type as _decide_order_llm()
4
  so it can be used as a drop-in alternative in ch_ai_trader.py.
@@ -15,17 +19,21 @@ from typing import Optional
15
  import numpy as np
16
 
17
  # ── Config ────────────────────────────────────────────────────────────────────
18
- RL_MODEL_REPO = os.getenv("CH_RL_MODEL_REPO", "Adilbai/stock-trading-rl-agent")
 
 
 
19
  RL_MODEL_CACHE = os.getenv("CH_RL_MODEL_CACHE", "/app/data/rl_model")
20
  RL_BAR_INTERVAL = int(os.getenv("CH_RL_BAR_INTERVAL", "60")) # seconds per bar
21
  RL_MIN_BARS = int(os.getenv("CH_RL_MIN_BARS", "30")) # min bars before RL kicks in
22
  RL_LOOKBACK = 60
23
 
24
  # ── Shared state ──────────────────────────────────────────────────────────────
25
- _model = None
26
- _scaler = None
 
27
  _model_lock = threading.Lock()
28
- _load_attempted = False
29
 
30
  # Per-symbol rolling price bars: {symbol: deque of {open, high, low, close, volume}}
31
  _price_bars: dict[str, deque] = {}
@@ -36,45 +44,81 @@ _current_bar: dict[str, dict] = {}
36
 
37
  # ── Model loading ─────────────────────────────────────────────────────────────
38
 
39
- def _load_model():
40
- """Download and load the PPO model + scaler from HuggingFace Hub."""
41
- global _model, _scaler, _load_attempted
42
  with _model_lock:
43
- if _load_attempted:
44
- return _model is not None
45
- _load_attempted = True
 
 
 
 
 
46
 
47
  try:
48
  from huggingface_hub import hf_hub_download
49
  from stable_baselines3 import PPO
50
 
51
- os.makedirs(RL_MODEL_CACHE, exist_ok=True)
52
- print(f"[CH-RL] Downloading model from {RL_MODEL_REPO}...")
 
53
 
54
  model_path = hf_hub_download(
55
- repo_id=RL_MODEL_REPO, filename="final_model.zip",
56
- cache_dir=RL_MODEL_CACHE,
57
  )
58
  scaler_path = hf_hub_download(
59
- repo_id=RL_MODEL_REPO, filename="scaler.pkl",
60
- cache_dir=RL_MODEL_CACHE,
61
  )
62
 
 
 
 
 
63
  with _model_lock:
64
- _model = PPO.load(model_path)
65
- with open(scaler_path, "rb") as f:
66
- _scaler = pickle.load(f)
67
 
68
- print("[CH-RL] Model loaded successfully")
69
  return True
70
  except Exception as e:
71
- print(f"[CH-RL] Failed to load model: {e}")
72
  return False
73
 
74
 
75
- def is_available() -> bool:
76
- """Check if RL model is loaded and ready."""
77
- return _model is not None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
78
 
79
 
80
  # ── Price history ─────────────────────────────────────────────────────────────
@@ -283,6 +327,7 @@ def _build_observation(
283
  capital: float,
284
  holdings: list,
285
  bbos: dict,
 
286
  ) -> Optional[np.ndarray]:
287
  """Build the 3008-dim observation vector for one symbol."""
288
  with _bars_lock:
@@ -299,10 +344,15 @@ def _build_observation(
299
 
300
  indicators = _compute_indicators(bars) # (60, 50)
301
 
302
- # Scale using the loaded scaler
303
- if _scaler is not None:
 
 
 
 
 
304
  try:
305
- indicators = _scaler.transform(indicators)
306
  except Exception:
307
  # Shape mismatch — normalize manually
308
  mean = indicators.mean(axis=0)
@@ -348,11 +398,19 @@ def decide_order_rl(
348
  holdings: list,
349
  bbos: dict,
350
  obligation_remaining: int,
 
351
  ) -> Optional[dict]:
352
  """Use the RL model to decide a trade. Returns order dict or None."""
353
- if not _model:
354
- if not _load_model():
 
 
 
 
 
 
355
  return None
 
356
 
357
  # Seed price history from BBOs for symbols we haven't seen
358
  for sym, bbo in bbos.items():
@@ -370,12 +428,12 @@ def decide_order_rl(
370
  candidates = []
371
 
372
  for sym, bbo in bbos.items():
373
- obs = _build_observation(sym, capital, holdings, bbos)
374
  if obs is None:
375
  continue
376
 
377
  try:
378
- action, _ = _model.predict(obs, deterministic=False)
379
  action_type = int(round(float(action[0])))
380
  position_size = float(np.clip(action[1], 0.05, 0.5))
381
 
 
1
+ """RL-based trading strategy using PPO models from HuggingFace Hub.
2
+
3
+ Supports two model slots:
4
+ - nn1: Adilbai/stock-trading-rl-agent
5
+ - nn2: RayMelius/stockex-nn-agent
6
 
7
  Provides decide_order_rl() with the same return type as _decide_order_llm()
8
  so it can be used as a drop-in alternative in ch_ai_trader.py.
 
19
  import numpy as np
20
 
21
  # ── Config ────────────────────────────────────────────────────────────────────
22
+ RL_MODEL_REPOS = {
23
+ "nn1": os.getenv("CH_RL_MODEL_REPO_NN1", os.getenv("CH_RL_MODEL_REPO", "Adilbai/stock-trading-rl-agent")),
24
+ "nn2": os.getenv("CH_RL_MODEL_REPO_NN2", "RayMelius/stockex-nn-agent"),
25
+ }
26
  RL_MODEL_CACHE = os.getenv("CH_RL_MODEL_CACHE", "/app/data/rl_model")
27
  RL_BAR_INTERVAL = int(os.getenv("CH_RL_BAR_INTERVAL", "60")) # seconds per bar
28
  RL_MIN_BARS = int(os.getenv("CH_RL_MIN_BARS", "30")) # min bars before RL kicks in
29
  RL_LOOKBACK = 60
30
 
31
  # ── Shared state ──────────────────────────────────────────────────────────────
32
+ # Per-model-slot: {"nn1": (model, scaler), "nn2": (model, scaler)}
33
+ _models: dict[str, tuple] = {}
34
+ _load_attempted: dict[str, bool] = {}
35
  _model_lock = threading.Lock()
36
+ _active_model: str = "nn1" # which model slot to use by default
37
 
38
  # Per-symbol rolling price bars: {symbol: deque of {open, high, low, close, volume}}
39
  _price_bars: dict[str, deque] = {}
 
44
 
45
  # ── Model loading ─────────────────────────────────────────────────────────────
46
 
47
+ def _load_model(slot: str = "nn1") -> bool:
48
+ """Download and load a PPO model + scaler from HuggingFace Hub."""
 
49
  with _model_lock:
50
+ if _load_attempted.get(slot):
51
+ return slot in _models
52
+ _load_attempted[slot] = True
53
+
54
+ repo = RL_MODEL_REPOS.get(slot)
55
+ if not repo:
56
+ print(f"[CH-RL] No repo configured for slot '{slot}'")
57
+ return False
58
 
59
  try:
60
  from huggingface_hub import hf_hub_download
61
  from stable_baselines3 import PPO
62
 
63
+ cache_dir = os.path.join(RL_MODEL_CACHE, slot)
64
+ os.makedirs(cache_dir, exist_ok=True)
65
+ print(f"[CH-RL] Downloading {slot} model from {repo}...")
66
 
67
  model_path = hf_hub_download(
68
+ repo_id=repo, filename="final_model.zip",
69
+ cache_dir=cache_dir,
70
  )
71
  scaler_path = hf_hub_download(
72
+ repo_id=repo, filename="scaler.pkl",
73
+ cache_dir=cache_dir,
74
  )
75
 
76
+ model = PPO.load(model_path)
77
+ with open(scaler_path, "rb") as f:
78
+ scaler = pickle.load(f)
79
+
80
  with _model_lock:
81
+ _models[slot] = (model, scaler)
 
 
82
 
83
+ print(f"[CH-RL] Model '{slot}' loaded successfully from {repo}")
84
  return True
85
  except Exception as e:
86
+ print(f"[CH-RL] Failed to load model '{slot}' from {repo}: {e}")
87
  return False
88
 
89
 
90
+ def is_available(slot: str | None = None) -> bool:
91
+ """Check if an RL model is loaded and ready."""
92
+ if slot:
93
+ return slot in _models or not _load_attempted.get(slot, False)
94
+ # At least one model available or not yet attempted
95
+ return bool(_models) or not all(_load_attempted.get(s, False) for s in RL_MODEL_REPOS)
96
+
97
+
98
+ def get_active_model() -> str:
99
+ """Return the currently active model slot name."""
100
+ return _active_model
101
+
102
+
103
+ def set_active_model(slot: str) -> str:
104
+ """Switch the active NN model. Returns the active slot name."""
105
+ global _active_model
106
+ if slot in RL_MODEL_REPOS:
107
+ _active_model = slot
108
+ print(f"[CH-RL] Active model switched to: {slot} ({RL_MODEL_REPOS[slot]})")
109
+ return _active_model
110
+
111
+
112
+ def get_model_info() -> dict:
113
+ """Return info about available model slots."""
114
+ return {
115
+ slot: {
116
+ "repo": repo,
117
+ "loaded": slot in _models,
118
+ "active": slot == _active_model,
119
+ }
120
+ for slot, repo in RL_MODEL_REPOS.items()
121
+ }
122
 
123
 
124
  # ── Price history ─────────────────────────────────────────────────────────────
 
327
  capital: float,
328
  holdings: list,
329
  bbos: dict,
330
+ slot: str | None = None,
331
  ) -> Optional[np.ndarray]:
332
  """Build the 3008-dim observation vector for one symbol."""
333
  with _bars_lock:
 
344
 
345
  indicators = _compute_indicators(bars) # (60, 50)
346
 
347
+ # Scale using the scaler for the requested model slot
348
+ scaler = None
349
+ use_slot = slot or _active_model
350
+ with _model_lock:
351
+ if use_slot in _models:
352
+ scaler = _models[use_slot][1]
353
+ if scaler is not None:
354
  try:
355
+ indicators = scaler.transform(indicators)
356
  except Exception:
357
  # Shape mismatch — normalize manually
358
  mean = indicators.mean(axis=0)
 
398
  holdings: list,
399
  bbos: dict,
400
  obligation_remaining: int,
401
+ model_slot: str | None = None,
402
  ) -> Optional[dict]:
403
  """Use the RL model to decide a trade. Returns order dict or None."""
404
+ slot = model_slot or _active_model
405
+ with _model_lock:
406
+ model_loaded = slot in _models
407
+ if not model_loaded:
408
+ if not _load_model(slot):
409
+ return None
410
+ with _model_lock:
411
+ if slot not in _models:
412
  return None
413
+ model, _slot_scaler = _models[slot]
414
 
415
  # Seed price history from BBOs for symbols we haven't seen
416
  for sym, bbo in bbos.items():
 
428
  candidates = []
429
 
430
  for sym, bbo in bbos.items():
431
+ obs = _build_observation(sym, capital, holdings, bbos, slot=slot)
432
  if obs is None:
433
  continue
434
 
435
  try:
436
+ action, _ = model.predict(obs, deterministic=False)
437
  action_type = int(round(float(action[0])))
438
  position_size = float(np.clip(action[1], 0.05, 0.5))
439
 
clearing_house/templates/dashboard.html CHANGED
@@ -51,8 +51,10 @@
51
  <td>
52
  {% if row.is_human %}
53
  <span class="badge-human">Human</span>
54
- {% elif row.ai_type == 'NN' %}
55
- <span class="badge-ai" style="background:#00695c;">NN</span>
 
 
56
  {% else %}
57
  <span class="badge-ai" style="background:#5c6bc0;">LLM</span>
58
  {% endif %}
@@ -82,7 +84,7 @@
82
  <p style="color:var(--muted); font-size:11px; margin-top:8px;">
83
  Daily obligation: each member must trade at least {{ obligation }} securities.
84
  Holdings value is calculated at current market mid-price.
85
- Click a row for full member detail. Right-click table to switch AI strategy. Refreshes every 10 seconds.
86
  </p>
87
 
88
  <!-- Member detail slide-out panel -->
@@ -121,9 +123,11 @@ async function refreshLeaderboard() {
121
  : `<span class="badge-bad">${row.total_securities}/{{ obligation }}</span>`;
122
  const typeBadge = row.is_human
123
  ? `<span class="badge-human">Human</span>`
124
- : row.ai_type === 'NN'
125
- ? `<span class="badge-ai" style="background:#00695c;">NN</span>`
126
- : `<span class="badge-ai" style="background:#5c6bc0;">LLM</span>`;
 
 
127
  tbody.innerHTML += `
128
  <tr data-member="${row.member_id}" style="cursor:pointer">
129
  <td style="color:var(--muted)">${row.rank}</td>
@@ -183,9 +187,11 @@ async function openDetail(memberId) {
183
  const pnlSign = d.pnl >= 0 ? '+' : '';
184
  const typeBadge = d.is_human
185
  ? '<span class="badge-human">Human</span>'
186
- : d.ai_type === 'NN'
187
- ? '<span class="badge-ai" style="background:#00695c;">NN</span>'
188
- : '<span class="badge-ai" style="background:#5c6bc0;">LLM</span>';
 
 
189
  const oblStatus = d.daily.total_securities >= d.obligation
190
  ? `<span class="badge-ok">Met (${d.daily.total_securities}/${d.obligation})</span>`
191
  : `<span class="badge-bad">${d.daily.total_securities}/${d.obligation}</span>`;
@@ -273,13 +279,16 @@ async function openDetail(memberId) {
273
  html += `<h3 style="margin:20px 0 8px;">AI Reasoning (last ${d.ai_decisions.length})</h3>`;
274
  d.ai_decisions.forEach(dec => {
275
  const ts = new Date(dec.timestamp * 1000).toLocaleTimeString();
276
- const srcBadge = dec.source === 'llm'
277
- ? '<span style="background:#e8eaf6; color:#5c6bc0; padding:1px 6px; border-radius:8px; font-size:10px;">LLM</span>'
278
- : dec.source === 'rl'
279
- ? '<span style="background:#e0f2f1; color:#00695c; padding:1px 6px; border-radius:8px; font-size:10px;">NN</span>'
280
- : dec.source === 'fallback'
281
- ? '<span style="background:#fff3e0; color:#e65100; padding:1px 6px; border-radius:8px; font-size:10px;">Fallback</span>'
282
- : `<span style="background:#ffebee; color:#c62828; padding:1px 6px; border-radius:8px; font-size:10px;">${dec.source}</span>`;
 
 
 
283
  const order = dec.parsed_order;
284
  const orderLine = order
285
  ? `<span class="${order.side === 'BUY' ? 'positive' : 'negative'}" style="font-weight:bold;">${order.side}</span> ${order.quantity} <strong>${order.symbol}</strong> @ €${fmt(order.price)}`
@@ -315,8 +324,12 @@ ctxMenu.style.cssText = `
315
  `;
316
  ctxMenu.innerHTML = `
317
  <div style="padding:6px 14px; color:var(--muted); font-size:11px; font-weight:bold;">AI Strategy</div>
318
- <div class="ctx-item" data-strategy="hybrid" style="padding:8px 14px; cursor:pointer;">Hybrid (NN + LLM)</div>
319
- <div class="ctx-item" data-strategy="rl" style="padding:8px 14px; cursor:pointer;">All NN (Neural Network)</div>
 
 
 
 
320
  <div class="ctx-item" data-strategy="llm" style="padding:8px 14px; cursor:pointer;">All LLM</div>
321
  `;
322
  document.body.appendChild(ctxMenu);
@@ -341,6 +354,15 @@ document.getElementById('lb-table').addEventListener('contextmenu', (e) => {
341
  // Hide on click elsewhere
342
  document.addEventListener('click', () => ctxMenu.style.display = 'none');
343
 
 
 
 
 
 
 
 
 
 
344
  async function highlightCurrentStrategy() {
345
  try {
346
  const resp = await fetch('/ch/api/config');
@@ -349,9 +371,7 @@ async function highlightCurrentStrategy() {
349
  const isCurrent = item.dataset.strategy === cfg.strategy;
350
  item.style.fontWeight = isCurrent ? 'bold' : 'normal';
351
  item.style.color = isCurrent ? 'var(--accent, #42a5f5)' : 'var(--text, #ccc)';
352
- // Reset text to base, then append checkmark
353
- const base = item.dataset.strategy === 'hybrid' ? 'Hybrid (NN + LLM)'
354
- : item.dataset.strategy === 'rl' ? 'All NN (Neural Network)' : 'All LLM';
355
  item.textContent = isCurrent ? base + ' \u2713' : base;
356
  });
357
  } catch(e) {}
 
51
  <td>
52
  {% if row.is_human %}
53
  <span class="badge-human">Human</span>
54
+ {% elif row.ai_type == 'NN1' %}
55
+ <span class="badge-ai" style="background:#00695c;">NN1</span>
56
+ {% elif row.ai_type == 'NN2' %}
57
+ <span class="badge-ai" style="background:#00838f;">NN2</span>
58
  {% else %}
59
  <span class="badge-ai" style="background:#5c6bc0;">LLM</span>
60
  {% endif %}
 
84
  <p style="color:var(--muted); font-size:11px; margin-top:8px;">
85
  Daily obligation: each member must trade at least {{ obligation }} securities.
86
  Holdings value is calculated at current market mid-price.
87
+ Click a row for full member detail. Right-click table to switch AI model (LLM / NN1 / NN2). Refreshes every 10 seconds.
88
  </p>
89
 
90
  <!-- Member detail slide-out panel -->
 
123
  : `<span class="badge-bad">${row.total_securities}/{{ obligation }}</span>`;
124
  const typeBadge = row.is_human
125
  ? `<span class="badge-human">Human</span>`
126
+ : row.ai_type === 'NN1'
127
+ ? `<span class="badge-ai" style="background:#00695c;">NN1</span>`
128
+ : row.ai_type === 'NN2'
129
+ ? `<span class="badge-ai" style="background:#00838f;">NN2</span>`
130
+ : `<span class="badge-ai" style="background:#5c6bc0;">LLM</span>`;
131
  tbody.innerHTML += `
132
  <tr data-member="${row.member_id}" style="cursor:pointer">
133
  <td style="color:var(--muted)">${row.rank}</td>
 
187
  const pnlSign = d.pnl >= 0 ? '+' : '';
188
  const typeBadge = d.is_human
189
  ? '<span class="badge-human">Human</span>'
190
+ : d.ai_type === 'NN1'
191
+ ? '<span class="badge-ai" style="background:#00695c;">NN1</span>'
192
+ : d.ai_type === 'NN2'
193
+ ? '<span class="badge-ai" style="background:#00838f;">NN2</span>'
194
+ : '<span class="badge-ai" style="background:#5c6bc0;">LLM</span>';
195
  const oblStatus = d.daily.total_securities >= d.obligation
196
  ? `<span class="badge-ok">Met (${d.daily.total_securities}/${d.obligation})</span>`
197
  : `<span class="badge-bad">${d.daily.total_securities}/${d.obligation}</span>`;
 
279
  html += `<h3 style="margin:20px 0 8px;">AI Reasoning (last ${d.ai_decisions.length})</h3>`;
280
  d.ai_decisions.forEach(dec => {
281
  const ts = new Date(dec.timestamp * 1000).toLocaleTimeString();
282
+ const srcColors = {
283
+ 'llm': {bg:'#e8eaf6', fg:'#5c6bc0'},
284
+ 'nn1': {bg:'#e0f2f1', fg:'#00695c'},
285
+ 'nn2': {bg:'#e0f7fa', fg:'#00838f'},
286
+ 'rl': {bg:'#e0f2f1', fg:'#00695c'},
287
+ 'fallback': {bg:'#fff3e0', fg:'#e65100'},
288
+ };
289
+ const sc = srcColors[dec.source] || {bg:'#ffebee', fg:'#c62828'};
290
+ const srcLabel = dec.source === 'rl' ? 'NN1' : dec.source.toUpperCase();
291
+ const srcBadge = `<span style="background:${sc.bg}; color:${sc.fg}; padding:1px 6px; border-radius:8px; font-size:10px;">${srcLabel}</span>`;
292
  const order = dec.parsed_order;
293
  const orderLine = order
294
  ? `<span class="${order.side === 'BUY' ? 'positive' : 'negative'}" style="font-weight:bold;">${order.side}</span> ${order.quantity} <strong>${order.symbol}</strong> @ €${fmt(order.price)}`
 
324
  `;
325
  ctxMenu.innerHTML = `
326
  <div style="padding:6px 14px; color:var(--muted); font-size:11px; font-weight:bold;">AI Strategy</div>
327
+ <div class="ctx-item" data-strategy="hybrid" style="padding:8px 14px; cursor:pointer;">Hybrid (LLM + NN1 + NN2)</div>
328
+ <div class="ctx-item" data-strategy="hybrid-nn1" style="padding:8px 14px; cursor:pointer;">Hybrid (NN1 + LLM)</div>
329
+ <div class="ctx-item" data-strategy="hybrid-nn2" style="padding:8px 14px; cursor:pointer;">Hybrid (NN2 + LLM)</div>
330
+ <div style="border-top:1px solid var(--border, #333); margin:4px 0;"></div>
331
+ <div class="ctx-item" data-strategy="nn1" style="padding:8px 14px; cursor:pointer;">All NN1</div>
332
+ <div class="ctx-item" data-strategy="nn2" style="padding:8px 14px; cursor:pointer;">All NN2</div>
333
  <div class="ctx-item" data-strategy="llm" style="padding:8px 14px; cursor:pointer;">All LLM</div>
334
  `;
335
  document.body.appendChild(ctxMenu);
 
354
  // Hide on click elsewhere
355
  document.addEventListener('click', () => ctxMenu.style.display = 'none');
356
 
357
+ const STRATEGY_LABELS = {
358
+ 'hybrid': 'Hybrid (LLM + NN1 + NN2)',
359
+ 'hybrid-nn1': 'Hybrid (NN1 + LLM)',
360
+ 'hybrid-nn2': 'Hybrid (NN2 + LLM)',
361
+ 'nn1': 'All NN1',
362
+ 'nn2': 'All NN2',
363
+ 'llm': 'All LLM',
364
+ };
365
+
366
  async function highlightCurrentStrategy() {
367
  try {
368
  const resp = await fetch('/ch/api/config');
 
371
  const isCurrent = item.dataset.strategy === cfg.strategy;
372
  item.style.fontWeight = isCurrent ? 'bold' : 'normal';
373
  item.style.color = isCurrent ? 'var(--accent, #42a5f5)' : 'var(--text, #ccc)';
374
+ const base = STRATEGY_LABELS[item.dataset.strategy] || item.dataset.strategy;
 
 
375
  item.textContent = isCurrent ? base + ' \u2713' : base;
376
  });
377
  } catch(e) {}
docker-compose.yml CHANGED
@@ -204,6 +204,8 @@ services:
204
  - GROQ_MODEL=${GROQ_MODEL:-llama-3.1-8b-instant}
205
  - OLLAMA_HOST=${OLLAMA_HOST:-}
206
  - CH_AI_STRATEGY=${CH_AI_STRATEGY:-hybrid}
 
 
207
  extra_hosts:
208
  - "host.docker.internal:host-gateway"
209
 
 
204
  - GROQ_MODEL=${GROQ_MODEL:-llama-3.1-8b-instant}
205
  - OLLAMA_HOST=${OLLAMA_HOST:-}
206
  - CH_AI_STRATEGY=${CH_AI_STRATEGY:-hybrid}
207
+ - CH_RL_MODEL_REPO_NN1=${CH_RL_MODEL_REPO_NN1:-Adilbai/stock-trading-rl-agent}
208
+ - CH_RL_MODEL_REPO_NN2=${CH_RL_MODEL_REPO_NN2:-RayMelius/stockex-nn-agent}
209
  extra_hosts:
210
  - "host.docker.internal:host-gateway"
211