RayMelius Claude Sonnet 4.6 commited on
Commit
fb7c4e4
Β·
1 Parent(s): eb6b6ff

Add Start/End Day toggle, Manual/Automatic mode with schedule-based auto-trading

Browse files

- Merge Start of Day / End of Day into single toggle button (green→Start, orange→End)
- Add Manual/Automatic toggle button; Automatic mode disables manual day control
- Add shared_data/market_schedule.txt (Start 10:00, End 17:00)
- Backend: schedule_runner thread auto-starts at 10:00, auto-ends at 17:00
- Backend: POST /session/mode endpoint; broadcasts mode via SSE
- Fix scheduler logic: end check takes priority, nothing fires before 10:00

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

dashboard/dashboard.py CHANGED
@@ -2,7 +2,7 @@ import sys
2
  sys.path.insert(0, "/app")
3
 
4
  from flask import Flask, render_template, jsonify, Response, request
5
- import threading, json, os, time, requests, sqlite3
6
  from queue import Queue, Empty
7
 
8
  from shared.config import Config
@@ -20,7 +20,9 @@ sse_clients = []
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")
@@ -287,27 +289,94 @@ def amend_order():
287
  return jsonify({"status": "error", "error": str(e)}), 500
288
 
289
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
290
  # ── Session endpoints ──────────────────────────────────────────────────────────
291
 
292
  @app.route("/session/start", methods=["POST"])
293
  def session_start():
294
  try:
295
- # Reset current prices to start prices
296
- securities = load_securities_file()
297
- if securities:
298
- for sym in securities:
299
- securities[sym]["current"] = securities[sym]["start"]
300
- save_securities_file(securities)
301
-
302
- # Signal md_feeder to start
303
- p = get_producer()
304
- p.send(Config.CONTROL_TOPIC, {"action": "start"})
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"})
312
  except Exception as e:
313
  return jsonify({"status": "error", "error": str(e)}), 500
@@ -316,31 +385,7 @@ def session_start():
316
  @app.route("/session/end", methods=["POST"])
317
  def session_end():
318
  try:
319
- securities = load_securities_file()
320
- # Update current prices from BBO mid where available
321
- for sym in list(securities.keys()):
322
- try:
323
- r = requests.get(f"{Config.MATCHER_URL}/orderbook/{sym}", timeout=2)
324
- book = r.json()
325
- bids = book.get("bids", [])
326
- asks = book.get("asks", [])
327
- if bids and asks:
328
- best_bid = max(b["price"] for b in bids)
329
- best_ask = min(a["price"] for a in asks)
330
- securities[sym]["current"] = round((best_bid + best_ask) / 2, 2)
331
- except Exception:
332
- pass
333
- if securities:
334
- save_securities_file(securities)
335
-
336
- # Signal md_feeder to stop
337
- p = get_producer()
338
- p.send(Config.CONTROL_TOPIC, {"action": "stop"})
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
@@ -376,6 +421,18 @@ def session_resume():
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)
@@ -443,6 +500,7 @@ def stream():
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)
@@ -465,6 +523,9 @@ def stream():
465
  )
466
 
467
 
 
 
 
468
  if __name__ == "__main__":
469
  port = int(os.getenv("PORT", "5000"))
470
  app.run(host="0.0.0.0", port=port, debug=True, use_reloader=False)
 
2
  sys.path.insert(0, "/app")
3
 
4
  from flask import Flask, render_template, jsonify, Response, request
5
+ import threading, json, os, time, requests, sqlite3, datetime
6
  from queue import Queue, Empty
7
 
8
  from shared.config import Config
 
20
  sse_clients_lock = threading.Lock()
21
 
22
  # Session state
23
+ session_state = {"active": False, "start_time": None, "suspended": False, "mode": "manual"}
24
+
25
+ SCHEDULE_FILE = os.getenv("SCHEDULE_FILE", "/app/shared_data/market_schedule.txt")
26
 
27
  # ── OHLCV History ──────────────────────────────────────────────────────────────
28
  HISTORY_DB = os.getenv("HISTORY_DB", "/app/data/dashboard_history.db")
 
289
  return jsonify({"status": "error", "error": str(e)}), 500
290
 
291
 
292
+ # ── Market schedule ─────────────────────────────────────────────────────────────
293
+
294
+ def load_market_schedule():
295
+ schedule = {}
296
+ try:
297
+ with open(SCHEDULE_FILE) as f:
298
+ for line in f:
299
+ line = line.strip()
300
+ if not line or line.startswith("#"):
301
+ continue
302
+ parts = line.split()
303
+ if len(parts) == 2:
304
+ schedule[parts[0].lower()] = parts[1]
305
+ except Exception:
306
+ pass
307
+ return schedule
308
+
309
+
310
+ def _do_session_start():
311
+ securities = load_securities_file()
312
+ if securities:
313
+ for sym in securities:
314
+ securities[sym]["current"] = securities[sym]["start"]
315
+ save_securities_file(securities)
316
+ p = get_producer()
317
+ p.send(Config.CONTROL_TOPIC, {"action": "start"})
318
+ p.flush()
319
+ session_state["active"] = True
320
+ session_state["suspended"] = False
321
+ session_state["start_time"] = time.time()
322
+ broadcast_event("session", {"status": "started", "time": session_state["start_time"]})
323
+
324
+
325
+ def _do_session_end():
326
+ securities = load_securities_file()
327
+ for sym in list(securities.keys()):
328
+ try:
329
+ r = requests.get(f"{Config.MATCHER_URL}/orderbook/{sym}", timeout=2)
330
+ book = r.json()
331
+ bids = book.get("bids", [])
332
+ asks = book.get("asks", [])
333
+ if bids and asks:
334
+ best_bid = max(b["price"] for b in bids)
335
+ best_ask = min(a["price"] for a in asks)
336
+ securities[sym]["current"] = round((best_bid + best_ask) / 2, 2)
337
+ except Exception:
338
+ pass
339
+ if securities:
340
+ save_securities_file(securities)
341
+ p = get_producer()
342
+ p.send(Config.CONTROL_TOPIC, {"action": "stop"})
343
+ p.flush()
344
+ session_state["active"] = False
345
+ session_state["suspended"] = False
346
+ broadcast_event("session", {"status": "ended", "time": time.time()})
347
+
348
+
349
+ def schedule_runner():
350
+ """Background thread: auto start/end session based on market_schedule.txt."""
351
+ while True:
352
+ try:
353
+ if session_state.get("mode") == "automatic":
354
+ sched = load_market_schedule()
355
+ start_str = sched.get("start")
356
+ end_str = sched.get("end")
357
+ if start_str and end_str:
358
+ now = datetime.datetime.now()
359
+ sh, sm = int(start_str.split(":")[0]), int(start_str.split(":")[1])
360
+ eh, em = int(end_str.split(":")[0]), int(end_str.split(":")[1])
361
+ start_t = now.replace(hour=sh, minute=sm, second=0, microsecond=0)
362
+ end_t = now.replace(hour=eh, minute=em, second=0, microsecond=0)
363
+ if now >= end_t and session_state["active"]:
364
+ print("[Scheduler] Auto end of day")
365
+ _do_session_end()
366
+ elif now >= start_t and not session_state["active"]:
367
+ print("[Scheduler] Auto start of day")
368
+ _do_session_start()
369
+ except Exception as e:
370
+ print(f"[Scheduler] Error: {e}")
371
+ time.sleep(30)
372
+
373
+
374
  # ── Session endpoints ──────────────────────────────────────────────────────────
375
 
376
  @app.route("/session/start", methods=["POST"])
377
  def session_start():
378
  try:
379
+ _do_session_start()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
380
  return jsonify({"status": "ok", "message": "Day started"})
381
  except Exception as e:
382
  return jsonify({"status": "error", "error": str(e)}), 500
 
385
  @app.route("/session/end", methods=["POST"])
386
  def session_end():
387
  try:
388
+ _do_session_end()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
389
  return jsonify({"status": "ok", "message": "Day ended, closing prices saved"})
390
  except Exception as e:
391
  return jsonify({"status": "error", "error": str(e)}), 500
 
421
  return jsonify({"status": "error", "error": str(e)}), 500
422
 
423
 
424
+ @app.route("/session/mode", methods=["POST"])
425
+ def session_mode():
426
+ try:
427
+ current = session_state.get("mode", "manual")
428
+ new_mode = "automatic" if current == "manual" else "manual"
429
+ session_state["mode"] = new_mode
430
+ broadcast_event("mode", {"mode": new_mode})
431
+ return jsonify({"status": "ok", "mode": new_mode})
432
+ except Exception as e:
433
+ return jsonify({"status": "error", "error": str(e)}), 500
434
+
435
+
436
  @app.route("/session/status")
437
  def session_status():
438
  return jsonify(session_state)
 
500
  else:
501
  _sess_status = "started"
502
  yield f"event: session\ndata: {json.dumps({'status': _sess_status})}\n\n"
503
+ yield f"event: mode\ndata: {json.dumps({'mode': session_state.get('mode', 'manual')})}\n\n"
504
  while True:
505
  try:
506
  message = q.get(timeout=30)
 
523
  )
524
 
525
 
526
+ _scheduler = threading.Thread(target=schedule_runner, daemon=True)
527
+ _scheduler.start()
528
+
529
  if __name__ == "__main__":
530
  port = int(os.getenv("PORT", "5000"))
531
  app.run(host="0.0.0.0", port=port, debug=True, use_reloader=False)
dashboard/templates/index.html CHANGED
@@ -140,12 +140,16 @@
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; }
@@ -197,9 +201,9 @@
197
  Trading Dashboard
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>
205
  <div class="container">
@@ -1012,6 +1016,11 @@
1012
  updateSessionBadge(active, suspended);
1013
  });
1014
 
 
 
 
 
 
1015
  eventSource.onerror = () => {
1016
  setStatus("disconnected", "Disconnected");
1017
  state.connected = false;
@@ -1041,31 +1050,80 @@
1041
 
1042
  // ── Session control ────────────────────────────────────────────────────────
1043
 
 
1044
  let _sessionSuspended = false;
 
1045
 
1046
  function updateSessionBadge(active, suspended) {
1047
- const badge = document.getElementById("session-badge");
1048
- const text = document.getElementById("session-text");
1049
- const btn = document.getElementById("suspend-btn");
 
 
1050
  _sessionSuspended = !!suspended;
 
1051
  if (!active) {
1052
  badge.className = "status idle";
1053
  text.textContent = "IDLE";
1054
  btn.disabled = true;
1055
  btn.textContent = "Suspend";
1056
  btn.className = "btn-day btn-suspend";
 
 
 
1057
  } else if (suspended) {
1058
  badge.className = "status suspended";
1059
  text.textContent = "PAUSED";
1060
  btn.disabled = false;
1061
  btn.textContent = "Resume";
1062
  btn.className = "btn-day btn-resume";
 
 
 
1063
  } else {
1064
  badge.className = "status active";
1065
  text.textContent = "ACTIVE";
1066
  btn.disabled = false;
1067
  btn.textContent = "Suspend";
1068
  btn.className = "btn-day btn-suspend";
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1069
  }
1070
  }
1071
 
@@ -1080,32 +1138,11 @@
1080
  }
1081
  }
1082
 
1083
- async function startDay() {
1084
- if (!confirm("Start of Day: reset all security prices to their opening values and begin simulation?")) return;
1085
  try {
1086
- const r = await fetch("/session/start", { method: "POST" });
1087
  const result = await r.json();
1088
- if (result.status === "ok") {
1089
- updateSessionBadge(true, false);
1090
- } else {
1091
- alert("Error: " + (result.error || "Unknown error"));
1092
- }
1093
- } catch (e) {
1094
- alert("Error: " + e.message);
1095
- }
1096
- }
1097
-
1098
- async function endDay() {
1099
- if (!confirm("End of Day: stop simulation and save closing prices?")) return;
1100
- try {
1101
- const r = await fetch("/session/end", { method: "POST" });
1102
- const result = await r.json();
1103
- if (result.status === "ok") {
1104
- updateSessionBadge(false, false);
1105
- alert("Day ended. Closing prices saved.");
1106
- } else {
1107
- alert("Error: " + (result.error || "Unknown error"));
1108
- }
1109
  } catch (e) {
1110
  alert("Error: " + e.message);
1111
  }
 
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
  .btn-suspend { background: #607d8b; color: #fff; }
146
  .btn-suspend:hover { background: #455a64; }
147
  .btn-resume { background: #4caf50; color: #fff; }
148
  .btn-resume:hover { background: #388e3c; }
149
+ .btn-manual { background: #607d8b; color: #fff; }
150
+ .btn-manual:hover { background: #455a64; }
151
+ .btn-automatic { background: #1565c0; color: #fff; }
152
+ .btn-automatic:hover { background: #0d47a1; }
153
  .btn-day:disabled { background: #ccc !important; color: #999; cursor: not-allowed; }
154
  .status.active { background: #d4edda; color: #155724; }
155
  .status.active .dot { background: #28a745; }
 
201
  Trading Dashboard
202
  <span id="status" class="status connecting"><span class="dot"></span><span id="status-text">Connecting...</span></span>
203
  <span id="session-badge" class="status idle"><span class="dot"></span><span id="session-text">IDLE</span></span>
204
+ <button id="day-btn" onclick="toggleDay()" class="btn-day btn-start">Start of Day</button>
205
  <button id="suspend-btn" onclick="toggleSuspend()" class="btn-day btn-suspend" disabled>Suspend</button>
206
+ <button id="mode-btn" onclick="toggleMode()" class="btn-day btn-manual">Manual</button>
207
  <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>
208
  </h1>
209
  <div class="container">
 
1016
  updateSessionBadge(active, suspended);
1017
  });
1018
 
1019
+ eventSource.addEventListener("mode", (e) => {
1020
+ const data = JSON.parse(e.data);
1021
+ updateModeBtn(data.mode);
1022
+ });
1023
+
1024
  eventSource.onerror = () => {
1025
  setStatus("disconnected", "Disconnected");
1026
  state.connected = false;
 
1050
 
1051
  // ── Session control ────────────────────────────────────────────────────────
1052
 
1053
+ let _sessionActive = false;
1054
  let _sessionSuspended = false;
1055
+ let _sessionMode = "manual";
1056
 
1057
  function updateSessionBadge(active, suspended) {
1058
+ const badge = document.getElementById("session-badge");
1059
+ const text = document.getElementById("session-text");
1060
+ const btn = document.getElementById("suspend-btn");
1061
+ const dayBtn = document.getElementById("day-btn");
1062
+ _sessionActive = !!active;
1063
  _sessionSuspended = !!suspended;
1064
+ const isAuto = _sessionMode === "automatic";
1065
  if (!active) {
1066
  badge.className = "status idle";
1067
  text.textContent = "IDLE";
1068
  btn.disabled = true;
1069
  btn.textContent = "Suspend";
1070
  btn.className = "btn-day btn-suspend";
1071
+ dayBtn.textContent = "Start of Day";
1072
+ dayBtn.className = "btn-day btn-start";
1073
+ dayBtn.disabled = isAuto;
1074
  } else if (suspended) {
1075
  badge.className = "status suspended";
1076
  text.textContent = "PAUSED";
1077
  btn.disabled = false;
1078
  btn.textContent = "Resume";
1079
  btn.className = "btn-day btn-resume";
1080
+ dayBtn.textContent = "End of Day";
1081
+ dayBtn.className = "btn-day btn-end";
1082
+ dayBtn.disabled = isAuto;
1083
  } else {
1084
  badge.className = "status active";
1085
  text.textContent = "ACTIVE";
1086
  btn.disabled = false;
1087
  btn.textContent = "Suspend";
1088
  btn.className = "btn-day btn-suspend";
1089
+ dayBtn.textContent = "End of Day";
1090
+ dayBtn.className = "btn-day btn-end";
1091
+ dayBtn.disabled = isAuto;
1092
+ }
1093
+ }
1094
+
1095
+ function updateModeBtn(mode) {
1096
+ const modeBtn = document.getElementById("mode-btn");
1097
+ _sessionMode = mode;
1098
+ if (mode === "automatic") {
1099
+ modeBtn.textContent = "Automatic";
1100
+ modeBtn.className = "btn-day btn-automatic";
1101
+ } else {
1102
+ modeBtn.textContent = "Manual";
1103
+ modeBtn.className = "btn-day btn-manual";
1104
+ }
1105
+ updateSessionBadge(_sessionActive, _sessionSuspended);
1106
+ }
1107
+
1108
+ async function toggleDay() {
1109
+ if (_sessionActive) {
1110
+ if (!confirm("End of Day: stop simulation and save closing prices?")) return;
1111
+ try {
1112
+ const r = await fetch("/session/end", { method: "POST" });
1113
+ const result = await r.json();
1114
+ if (result.status !== "ok") alert("Error: " + (result.error || "Unknown error"));
1115
+ } catch (e) {
1116
+ alert("Error: " + e.message);
1117
+ }
1118
+ } else {
1119
+ if (!confirm("Start of Day: reset all security prices to their opening values and begin simulation?")) return;
1120
+ try {
1121
+ const r = await fetch("/session/start", { method: "POST" });
1122
+ const result = await r.json();
1123
+ if (result.status !== "ok") alert("Error: " + (result.error || "Unknown error"));
1124
+ } catch (e) {
1125
+ alert("Error: " + e.message);
1126
+ }
1127
  }
1128
  }
1129
 
 
1138
  }
1139
  }
1140
 
1141
+ async function toggleMode() {
 
1142
  try {
1143
+ const r = await fetch("/session/mode", { method: "POST" });
1144
  const result = await r.json();
1145
+ if (result.status !== "ok") alert("Error: " + (result.error || "Unknown error"));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1146
  } catch (e) {
1147
  alert("Error: " + e.message);
1148
  }
shared_data/market_schedule.txt ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ Start 10:00
2
+ End 17:00
shared_data/securities.txt CHANGED
@@ -1,11 +1,6 @@
1
  #SYMBOL <start_price> <current_price>
2
- ALPHA 24.95 25.55
3
  PEIR 18.05 18.35
4
- EXAE 42.05 41.85
5
- QUEST 12.60 13.00
6
- NBG 18.05 18.10
7
- ATTIKA 3.95 3.95
8
- INTKA 3.95 3.95
9
- LAMDA 3.95 3.95
10
- AEG 3.95 3.95
11
- AAAK 3.95 3.95
 
1
  #SYMBOL <start_price> <current_price>
2
+ ALPHA 24.95 25.65
3
  PEIR 18.05 18.35
4
+ EXAE 42.05 41.90
5
+ QUEST 12.60 13.35
6
+ NBG 18.05 18.00