RayMelius Claude Sonnet 4.6 commited on
Commit
375c179
Β·
1 Parent(s): 61af8e9

Add Suspend/Resume trading session button

Browse files

- md_feeder: add _suspended flag; handle suspend/resume control messages
to pause/unpause order generation without ending the session
- dashboard: add suspended field to session_state; add POST /session/suspend
and POST /session/resume endpoints that publish to control topic
- dashboard template: Suspend button (blue-grey) between Start and End of Day;
toggles to Resume (green) when paused; session badge shows PAUSED (amber)
with pulsing dot when suspended; button disabled when session is idle

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

dashboard/dashboard.py CHANGED
@@ -20,7 +20,7 @@ sse_clients = []
20
  sse_clients_lock = threading.Lock()
21
 
22
  # Session state
23
- session_state = {"active": False, "start_time": None}
24
 
25
  # ── OHLCV History ──────────────────────────────────────────────────────────────
26
  HISTORY_DB = os.getenv("HISTORY_DB", "/app/data/dashboard_history.db")
@@ -305,6 +305,7 @@ def session_start():
305
  p.flush()
306
 
307
  session_state["active"] = True
 
308
  session_state["start_time"] = time.time()
309
  broadcast_event("session", {"status": "started", "time": session_state["start_time"]})
310
  return jsonify({"status": "ok", "message": "Day started"})
@@ -338,12 +339,43 @@ def session_end():
338
  p.flush()
339
 
340
  session_state["active"] = False
 
341
  broadcast_event("session", {"status": "ended", "time": time.time()})
342
  return jsonify({"status": "ok", "message": "Day ended, closing prices saved"})
343
  except Exception as e:
344
  return jsonify({"status": "error", "error": str(e)}), 500
345
 
346
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
347
  @app.route("/session/status")
348
  def session_status():
349
  return jsonify(session_state)
@@ -404,7 +436,13 @@ def stream():
404
  f"{json.dumps({'orders': list(orders), 'bbos': dict(bbos), 'trades': list(trades_cache)})}\n\n"
405
  )
406
  # Also send current session state
407
- yield f"event: session\ndata: {json.dumps({'status': 'started' if session_state['active'] else 'ended'})}\n\n"
 
 
 
 
 
 
408
  while True:
409
  try:
410
  message = q.get(timeout=30)
 
20
  sse_clients_lock = threading.Lock()
21
 
22
  # Session state
23
+ session_state = {"active": False, "start_time": None, "suspended": False}
24
 
25
  # ── OHLCV History ──────────────────────────────────────────────────────────────
26
  HISTORY_DB = os.getenv("HISTORY_DB", "/app/data/dashboard_history.db")
 
305
  p.flush()
306
 
307
  session_state["active"] = True
308
+ session_state["suspended"] = False
309
  session_state["start_time"] = time.time()
310
  broadcast_event("session", {"status": "started", "time": session_state["start_time"]})
311
  return jsonify({"status": "ok", "message": "Day started"})
 
339
  p.flush()
340
 
341
  session_state["active"] = False
342
+ session_state["suspended"] = False
343
  broadcast_event("session", {"status": "ended", "time": time.time()})
344
  return jsonify({"status": "ok", "message": "Day ended, closing prices saved"})
345
  except Exception as e:
346
  return jsonify({"status": "error", "error": str(e)}), 500
347
 
348
 
349
+ @app.route("/session/suspend", methods=["POST"])
350
+ def session_suspend():
351
+ try:
352
+ if not session_state["active"]:
353
+ return jsonify({"status": "error", "error": "No active session"}), 400
354
+ p = get_producer()
355
+ p.send(Config.CONTROL_TOPIC, {"action": "suspend"})
356
+ p.flush()
357
+ session_state["suspended"] = True
358
+ broadcast_event("session", {"status": "suspended"})
359
+ return jsonify({"status": "ok", "message": "Session suspended"})
360
+ except Exception as e:
361
+ return jsonify({"status": "error", "error": str(e)}), 500
362
+
363
+
364
+ @app.route("/session/resume", methods=["POST"])
365
+ def session_resume():
366
+ try:
367
+ if not session_state["active"]:
368
+ return jsonify({"status": "error", "error": "No active session"}), 400
369
+ p = get_producer()
370
+ p.send(Config.CONTROL_TOPIC, {"action": "resume"})
371
+ p.flush()
372
+ session_state["suspended"] = False
373
+ broadcast_event("session", {"status": "active"})
374
+ return jsonify({"status": "ok", "message": "Session resumed"})
375
+ except Exception as e:
376
+ return jsonify({"status": "error", "error": str(e)}), 500
377
+
378
+
379
  @app.route("/session/status")
380
  def session_status():
381
  return jsonify(session_state)
 
436
  f"{json.dumps({'orders': list(orders), 'bbos': dict(bbos), 'trades': list(trades_cache)})}\n\n"
437
  )
438
  # Also send current session state
439
+ if not session_state["active"]:
440
+ _sess_status = "ended"
441
+ elif session_state["suspended"]:
442
+ _sess_status = "suspended"
443
+ else:
444
+ _sess_status = "started"
445
+ yield f"event: session\ndata: {json.dumps({'status': _sess_status})}\n\n"
446
  while True:
447
  try:
448
  message = q.get(timeout=30)
dashboard/templates/index.html CHANGED
@@ -140,10 +140,17 @@
140
  }
141
  .btn-start { background: #28a745; color: #fff; }
142
  .btn-start:hover { background: #218838; }
 
 
 
 
143
  .btn-end { background: #ff9800; color: #fff; }
144
  .btn-end:hover { background: #e68900; }
 
145
  .status.active { background: #d4edda; color: #155724; }
146
  .status.active .dot { background: #28a745; }
 
 
147
  .status.idle { background: #e8e8e8; color: #666; }
148
  .status.idle .dot { background: #9e9e9e; }
149
 
@@ -191,6 +198,7 @@
191
  <span id="status" class="status connecting"><span class="dot"></span><span id="status-text">Connecting...</span></span>
192
  <span id="session-badge" class="status idle"><span class="dot"></span><span id="session-text">IDLE</span></span>
193
  <button onclick="startDay()" class="btn-day btn-start">Start of Day</button>
 
194
  <button onclick="endDay()" class="btn-day btn-end">End of Day</button>
195
  <a href="/fix/" style="margin-left:auto; padding:4px 14px; background:#6c757d; color:#fff; border-radius:20px; font-size:12px; font-weight:bold; text-decoration:none;">FIX UI</a>
196
  </h1>
@@ -1018,7 +1026,9 @@
1018
 
1019
  eventSource.addEventListener("session", (e) => {
1020
  const data = JSON.parse(e.data);
1021
- updateSessionBadge(data.status === "started");
 
 
1022
  });
1023
 
1024
  eventSource.onerror = () => {
@@ -1050,11 +1060,43 @@
1050
 
1051
  // ── Session control ────────────────────────────────────────────────────────
1052
 
1053
- function updateSessionBadge(active) {
 
 
1054
  const badge = document.getElementById("session-badge");
1055
  const text = document.getElementById("session-text");
1056
- badge.className = "status " + (active ? "active" : "idle");
1057
- text.textContent = active ? "ACTIVE" : "IDLE";
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1058
  }
1059
 
1060
  async function startDay() {
@@ -1063,7 +1105,7 @@
1063
  const r = await fetch("/session/start", { method: "POST" });
1064
  const result = await r.json();
1065
  if (result.status === "ok") {
1066
- updateSessionBadge(true);
1067
  } else {
1068
  alert("Error: " + (result.error || "Unknown error"));
1069
  }
@@ -1078,7 +1120,7 @@
1078
  const r = await fetch("/session/end", { method: "POST" });
1079
  const result = await r.json();
1080
  if (result.status === "ok") {
1081
- updateSessionBadge(false);
1082
  alert("Day ended. Closing prices saved.");
1083
  } else {
1084
  alert("Error: " + (result.error || "Unknown error"));
 
140
  }
141
  .btn-start { background: #28a745; color: #fff; }
142
  .btn-start:hover { background: #218838; }
143
+ .btn-suspend { background: #607d8b; color: #fff; }
144
+ .btn-suspend:hover { background: #455a64; }
145
+ .btn-resume { background: #4caf50; color: #fff; }
146
+ .btn-resume:hover { background: #388e3c; }
147
  .btn-end { background: #ff9800; color: #fff; }
148
  .btn-end:hover { background: #e68900; }
149
+ .btn-day:disabled { background: #ccc !important; color: #999; cursor: not-allowed; }
150
  .status.active { background: #d4edda; color: #155724; }
151
  .status.active .dot { background: #28a745; }
152
+ .status.suspended { background: #fff3cd; color: #856404; }
153
+ .status.suspended .dot { background: #ffc107; animation: pulse 1s infinite; }
154
  .status.idle { background: #e8e8e8; color: #666; }
155
  .status.idle .dot { background: #9e9e9e; }
156
 
 
198
  <span id="status" class="status connecting"><span class="dot"></span><span id="status-text">Connecting...</span></span>
199
  <span id="session-badge" class="status idle"><span class="dot"></span><span id="session-text">IDLE</span></span>
200
  <button onclick="startDay()" class="btn-day btn-start">Start of Day</button>
201
+ <button id="suspend-btn" onclick="toggleSuspend()" class="btn-day btn-suspend" disabled>Suspend</button>
202
  <button onclick="endDay()" class="btn-day btn-end">End of Day</button>
203
  <a href="/fix/" style="margin-left:auto; padding:4px 14px; background:#6c757d; color:#fff; border-radius:20px; font-size:12px; font-weight:bold; text-decoration:none;">FIX UI</a>
204
  </h1>
 
1026
 
1027
  eventSource.addEventListener("session", (e) => {
1028
  const data = JSON.parse(e.data);
1029
+ const active = data.status === "started" || data.status === "active" || data.status === "suspended";
1030
+ const suspended = data.status === "suspended";
1031
+ updateSessionBadge(active, suspended);
1032
  });
1033
 
1034
  eventSource.onerror = () => {
 
1060
 
1061
  // ── Session control ────────────────────────────────────────────────────────
1062
 
1063
+ let _sessionSuspended = false;
1064
+
1065
+ function updateSessionBadge(active, suspended) {
1066
  const badge = document.getElementById("session-badge");
1067
  const text = document.getElementById("session-text");
1068
+ const btn = document.getElementById("suspend-btn");
1069
+ _sessionSuspended = !!suspended;
1070
+ if (!active) {
1071
+ badge.className = "status idle";
1072
+ text.textContent = "IDLE";
1073
+ btn.disabled = true;
1074
+ btn.textContent = "Suspend";
1075
+ btn.className = "btn-day btn-suspend";
1076
+ } else if (suspended) {
1077
+ badge.className = "status suspended";
1078
+ text.textContent = "PAUSED";
1079
+ btn.disabled = false;
1080
+ btn.textContent = "Resume";
1081
+ btn.className = "btn-day btn-resume";
1082
+ } else {
1083
+ badge.className = "status active";
1084
+ text.textContent = "ACTIVE";
1085
+ btn.disabled = false;
1086
+ btn.textContent = "Suspend";
1087
+ btn.className = "btn-day btn-suspend";
1088
+ }
1089
+ }
1090
+
1091
+ async function toggleSuspend() {
1092
+ try {
1093
+ const url = _sessionSuspended ? "/session/resume" : "/session/suspend";
1094
+ const r = await fetch(url, { method: "POST" });
1095
+ const result = await r.json();
1096
+ if (result.status !== "ok") alert("Error: " + (result.error || "Unknown error"));
1097
+ } catch (e) {
1098
+ alert("Error: " + e.message);
1099
+ }
1100
  }
1101
 
1102
  async function startDay() {
 
1105
  const r = await fetch("/session/start", { method: "POST" });
1106
  const result = await r.json();
1107
  if (result.status === "ok") {
1108
+ updateSessionBadge(true, false);
1109
  } else {
1110
  alert("Error: " + (result.error || "Unknown error"));
1111
  }
 
1120
  const r = await fetch("/session/end", { method: "POST" });
1121
  const result = await r.json();
1122
  if (result.status === "ok") {
1123
+ updateSessionBadge(false, false);
1124
  alert("Day ended. Closing prices saved.");
1125
  } else {
1126
  alert("Error: " + (result.error || "Unknown error"));
md_feeder/mdf_simulator.py CHANGED
@@ -12,6 +12,7 @@ ORDER_INTERVAL = 60.0 / Config.ORDERS_PER_MIN
12
  # Module-level state (shared with control listener thread)
13
  _securities = {}
14
  _running = True
 
15
 
16
 
17
  def load_securities():
@@ -68,14 +69,15 @@ def make_snapshot(symbol, best_bid, best_ask, bid_size, ask_size):
68
 
69
 
70
  def listen_control(ctrl_consumer):
71
- """Background thread: listen for start/stop control messages."""
72
- global _running, _securities
73
  print("[MDF] Control listener started")
74
  for msg in ctrl_consumer:
75
  action = (msg.value or {}).get("action")
76
  if action == "stop":
77
  _running = False
78
- print("[MDF] STOP signal received – pausing simulation")
 
79
  elif action == "start":
80
  try:
81
  new_secs = load_securities()
@@ -84,8 +86,15 @@ def listen_control(ctrl_consumer):
84
  print(f"[MDF] START signal – reloaded securities: {list(_securities.keys())}")
85
  except Exception as e:
86
  print(f"[MDF] Error reloading securities on start: {e}")
 
87
  _running = True
88
- print("[MDF] Simulation resumed")
 
 
 
 
 
 
89
 
90
 
91
  if __name__ == "__main__":
@@ -112,12 +121,12 @@ if __name__ == "__main__":
112
 
113
  try:
114
  while True:
115
- if not _running:
116
  time.sleep(0.5)
117
  continue
118
 
119
  for sym, vals in list(_securities.items()):
120
- if not _running:
121
  break
122
 
123
  mid = vals["current"]
 
12
  # Module-level state (shared with control listener thread)
13
  _securities = {}
14
  _running = True
15
+ _suspended = False
16
 
17
 
18
  def load_securities():
 
69
 
70
 
71
  def listen_control(ctrl_consumer):
72
+ """Background thread: listen for start/stop/suspend/resume control messages."""
73
+ global _running, _suspended, _securities
74
  print("[MDF] Control listener started")
75
  for msg in ctrl_consumer:
76
  action = (msg.value or {}).get("action")
77
  if action == "stop":
78
  _running = False
79
+ _suspended = False
80
+ print("[MDF] STOP signal received – simulation stopped")
81
  elif action == "start":
82
  try:
83
  new_secs = load_securities()
 
86
  print(f"[MDF] START signal – reloaded securities: {list(_securities.keys())}")
87
  except Exception as e:
88
  print(f"[MDF] Error reloading securities on start: {e}")
89
+ _suspended = False
90
  _running = True
91
+ print("[MDF] Simulation started")
92
+ elif action == "suspend":
93
+ _suspended = True
94
+ print("[MDF] SUSPEND signal received – order generation paused")
95
+ elif action == "resume":
96
+ _suspended = False
97
+ print("[MDF] RESUME signal received – order generation resumed")
98
 
99
 
100
  if __name__ == "__main__":
 
121
 
122
  try:
123
  while True:
124
+ if not _running or _suspended:
125
  time.sleep(0.5)
126
  continue
127
 
128
  for sym, vals in list(_securities.items()):
129
+ if not _running or _suspended:
130
  break
131
 
132
  mid = vals["current"]