KarlQuant commited on
Commit
1d61f10
·
verified ·
1 Parent(s): fd32a74

Delete hub_dashboard_service.py.py

Browse files
Files changed (1) hide show
  1. hub_dashboard_service.py.py +0 -449
hub_dashboard_service.py.py DELETED
@@ -1,449 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- ╔══════════════════════════════════════════════════════════════════════════════════════╗
4
- ║ K1RL QUASAR — HUB DASHBOARD SERVICE (with Trade Log Parser) ║
5
- ║ ────────────────────────────────────────────────────────────────────────────────── ║
6
- ║ Architecture role: READ-ONLY subscriber → serves dashboard UI ║
7
- ║ VERSION: v2.0 (FIXED) | 2026-03-30 ║
8
- ╚══════════════════════════════════════════════════════════════════════════════════════╝
9
- """
10
-
11
- import json
12
- import logging
13
- import os
14
- import sys
15
- import re
16
- import glob
17
- import threading
18
- import time
19
- from collections import deque, defaultdict
20
- from datetime import datetime
21
- from pathlib import Path
22
- from typing import Dict, List, Optional
23
-
24
- import websocket
25
- from flask import Flask, jsonify, request, send_from_directory
26
- from flask_cors import CORS
27
-
28
- # ── Logging ───────────────────────────────────────────────────────────────────────────
29
- logging.basicConfig(
30
- level=logging.INFO,
31
- format="%(asctime)s | %(levelname)s | %(name)s | %(message)s",
32
- datefmt="%Y-%m-%d %H:%M:%S",
33
- stream=sys.stdout,
34
- )
35
- logger = logging.getLogger("HubDashboardService")
36
-
37
- # ── Config ────────────────────────────────────────────────────────────────────────────
38
- _HUB_HOST = os.environ.get("QUASAR_HUB_HOST", "karlquant-quasar-executo.hf.space")
39
- _DASHBOARD_PORT = int(os.environ.get("DASHBOARD_PORT", "8051"))
40
- _HTML_PATH = os.environ.get(
41
- "DASHBOARD_HTML",
42
- str(Path(__file__).parent / "hub_dashboard.html"),
43
- )
44
- _LOG_DIR = os.environ.get("RANKER_LOG_DIR", "/app/ranker_logs")
45
- _METRIC_HISTORY_LEN = int(os.environ.get("QUASAR_METRIC_HISTORY", "200"))
46
-
47
-
48
- # ══════════════════════════════════════════════════════════════════════════════════════
49
- # SECTION 1 — TRADE LOG PARSER (FIXED for actual log format)
50
- # ══════════════════════════════════════════════════════════════════════════════════════
51
-
52
- class TradeLogParser:
53
- """
54
- Tails ranker log files and maintains open/closed trade state.
55
- Runs in a background thread, refreshing every 2 seconds.
56
-
57
- Expected log format from ranker_logging.py:
58
- [2026-03-30 16:20:40] | INFO | TRADE | CRASH500 | TRADE OPENED | ID=CRASH500_123 | Dir=long | Entry=3524.6485 | Qty=0.000284
59
- [2026-03-30 16:20:39] | INFO | TRADE | CRASH500 | TRADE CLOSED | ID=CRASH500_456 | pnl=-3.5246 | return=+0.01%
60
- """
61
-
62
- # Regex patterns matching the actual log format
63
- # Pattern for OPEN: ... | TRADE OPENED | ID=xxx | Dir=xxx | Entry=xxx
64
- TRADE_OPEN_RE = re.compile(r'TRADE OPENED \| ID=(\S+) \| Dir=(\w+) \| Entry=([\d.]+)')
65
-
66
- # Pattern for CLOSE: ... | TRADE CLOSED | ID=xxx | pnl=xxx | return=xxx
67
- TRADE_CLOSE_RE = re.compile(r'TRADE CLOSED \| ID=(\S+) \| pnl=([+-]?[\d.]+)')
68
-
69
- # Pattern to extract asset from the log line (the part after TRADE |)
70
- TRADE_ASSET_RE = re.compile(r'TRADE \| (\w+) \|')
71
-
72
- def __init__(self, log_dir: str = _LOG_DIR):
73
- self.log_dir = Path(log_dir)
74
- self._open: Dict[str, dict] = {}
75
- self._closed: List[dict] = []
76
- self._last_pos: Dict[str, int] = {}
77
- self._lock = threading.RLock()
78
- self._stats = {
79
- "total_opened": 0,
80
- "total_closed": 0,
81
- "total_pnl": 0.0,
82
- "win_count": 0,
83
- "loss_count": 0,
84
- }
85
- self._running = False
86
- self._thread: Optional[threading.Thread] = None
87
-
88
- # Create log directory if it doesn't exist
89
- self.log_dir.mkdir(parents=True, exist_ok=True)
90
- logger.info(f"[TradeLogParser] Initialized | log_dir={self.log_dir}")
91
-
92
- def start_background(self, interval: float = 2.0) -> None:
93
- """Launch a daemon thread that calls refresh() every `interval` seconds."""
94
- if self._running:
95
- return
96
-
97
- self._running = True
98
- self._thread = threading.Thread(target=self._loop, daemon=True, name="TradeLogParser")
99
- self._thread.start()
100
- logger.info(f"[TradeLogParser] Started — watching {self.log_dir} (interval={interval}s)")
101
-
102
- def _loop(self) -> None:
103
- """Background loop."""
104
- while self._running:
105
- try:
106
- self.refresh()
107
- except Exception as e:
108
- logger.error(f"[TradeLogParser] refresh error: {e}")
109
- time.sleep(2.0)
110
-
111
- def refresh(self) -> None:
112
- """Find all log files, read new lines since last position."""
113
- pattern = str(self.log_dir / "*.log")
114
- files = sorted(glob.glob(pattern))
115
-
116
- if not files:
117
- # Also check for .txt files
118
- pattern = str(self.log_dir / "*.txt")
119
- files = sorted(glob.glob(pattern))
120
-
121
- for fpath in files:
122
- self._tail_file(fpath)
123
-
124
- def _tail_file(self, fpath: str) -> None:
125
- """Read only new bytes from fpath since last call."""
126
- try:
127
- size = os.path.getsize(fpath)
128
- except OSError:
129
- return
130
-
131
- last = self._last_pos.get(fpath, 0)
132
- if size <= last:
133
- return
134
-
135
- try:
136
- with open(fpath, "r", encoding="utf-8", errors="replace") as f:
137
- f.seek(last)
138
- new_lines = f.readlines()
139
- self._last_pos[fpath] = f.tell()
140
- except OSError:
141
- return
142
-
143
- for line in new_lines:
144
- self._parse_line(line)
145
-
146
- def _parse_line(self, line: str) -> None:
147
- """Extract trade events from a single log line."""
148
-
149
- # Extract asset from the line (if present)
150
- asset_match = self.TRADE_ASSET_RE.search(line)
151
- asset = asset_match.group(1) if asset_match else None
152
-
153
- # ── TRADE OPENED ─────────────────────────────────────────────────────────
154
- m = self.TRADE_OPEN_RE.search(line)
155
- if m:
156
- trade_id, direction, entry = m.group(1), m.group(2), float(m.group(3))
157
- # Normalize direction: long -> LONG, short -> SHORT
158
- direction = direction.upper()
159
-
160
- # Parse timestamp from log line
161
- ts = self._parse_timestamp(line)
162
-
163
- with self._lock:
164
- self._open[trade_id] = {
165
- "trade_id": trade_id,
166
- "asset": asset or trade_id.split('_')[0],
167
- "direction": direction,
168
- "entry": entry,
169
- "opened_at": ts,
170
- "status": "OPEN",
171
- }
172
- self._stats["total_opened"] += 1
173
-
174
- logger.debug(f"[TradeLogParser] OPEN: {trade_id} | {direction} @ {entry}")
175
- return
176
-
177
- # ── TRADE CLOSED ─────────────────────────────────────────────────────────
178
- m = self.TRADE_CLOSE_RE.search(line)
179
- if m:
180
- trade_id, pnl = m.group(1), float(m.group(2))
181
- ts = self._parse_timestamp(line)
182
-
183
- with self._lock:
184
- # Find the matching open trade
185
- trade = self._open.pop(trade_id, None)
186
-
187
- closed = {
188
- "trade_id": trade_id,
189
- "asset": asset or (trade.get("asset") if trade else trade_id.split('_')[0]),
190
- "pnl": pnl,
191
- "closed_at": ts,
192
- "status": "CLOSED",
193
- }
194
-
195
- if trade:
196
- closed["direction"] = trade.get("direction", "?")
197
- closed["entry"] = trade.get("entry", 0.0)
198
-
199
- self._closed.append(closed)
200
- self._stats["total_closed"] += 1
201
- self._stats["total_pnl"] += pnl
202
-
203
- if pnl >= 0:
204
- self._stats["win_count"] += 1
205
- else:
206
- self._stats["loss_count"] += 1
207
-
208
- logger.debug(f"[TradeLogParser] CLOSE: {trade_id} | pnl={pnl:+.2f}")
209
- return
210
-
211
- @staticmethod
212
- def _parse_timestamp(line: str) -> str:
213
- """Extract ISO timestamp from log line prefix."""
214
- # Format: [2026-03-30 16:20:40] | ...
215
- match = re.search(r'\[(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\]', line)
216
- if match:
217
- return match.group(1).replace(" ", "T")
218
- return datetime.utcnow().isoformat()[:19]
219
-
220
- def get_state(self) -> dict:
221
- """Return current trade state."""
222
- with self._lock:
223
- open_trades = list(self._open.values())
224
- closed_trades = list(reversed(self._closed[-100:])) # newest first
225
-
226
- stats = dict(self._stats)
227
- stats["win_rate"] = (
228
- round(stats["win_count"] / stats["total_closed"] * 100, 1)
229
- if stats["total_closed"] > 0 else 0.0
230
- )
231
-
232
- return {
233
- "open": open_trades,
234
- "closed": closed_trades,
235
- "stats": stats,
236
- "timestamp": datetime.utcnow().isoformat() + "Z",
237
- }
238
-
239
- def stop(self) -> None:
240
- """Stop the background thread."""
241
- self._running = False
242
- if self._thread:
243
- self._thread.join(timeout=5)
244
-
245
-
246
- # ══════════════════════════════════════════════════════════════════════════════════════
247
- # SECTION 2 — IN-MEMORY STATE STORE (minimal for dashboard)
248
- # ══════════════════════════════════════════════════════════════════════════════════════
249
-
250
- class DashboardState:
251
- """Thread-safe cache of everything the dashboard needs."""
252
-
253
- def __init__(self, history_len: int = _METRIC_HISTORY_LEN):
254
- self._lock = threading.RLock()
255
- self._history_len = history_len
256
-
257
- self.hub_connected = False
258
- self.start_time = time.time()
259
- self.messages_rx = 0
260
- self.last_update_ts = 0.0
261
-
262
- self._rankings: List[dict] = []
263
- self._snapshots: Dict[str, dict] = {}
264
-
265
- def get_state(self) -> dict:
266
- with self._lock:
267
- return {
268
- "rankings": list(self._rankings),
269
- "metric_history": {},
270
- "health": self._health_snapshot(),
271
- "timestamp": datetime.utcnow().isoformat() + "Z",
272
- }
273
-
274
- def _health_snapshot(self) -> dict:
275
- return {
276
- "hub_connected": self.hub_connected,
277
- "spaces_connected": len(self._snapshots),
278
- "messages_rx": self.messages_rx,
279
- "last_update_ts": self.last_update_ts,
280
- "last_update_ago": round(time.time() - self.last_update_ts, 1) if self.last_update_ts else None,
281
- "uptime_seconds": round(time.time() - self.start_time, 0),
282
- "reconnect_count": 0,
283
- }
284
-
285
-
286
- # ══════════════════════════════════════════════════════════════════════════════════════
287
- # SECTION 3 — FLASK APP
288
- # ══════════════════════════════════════════════════════════════════════════════════════
289
-
290
- _state = DashboardState()
291
- _trade_parser = TradeLogParser(log_dir=_LOG_DIR)
292
- _trade_parser.start_background()
293
-
294
- app = Flask(__name__)
295
- CORS(app)
296
-
297
-
298
- @app.route("/")
299
- def index():
300
- """Serve the dashboard HTML."""
301
- html_path = Path(_HTML_PATH)
302
- if html_path.exists():
303
- return send_from_directory(str(html_path.parent), html_path.name)
304
- return (
305
- "<h1>hub_dashboard.html not found</h1>"
306
- f"<p>Expected: <code>{_HTML_PATH}</code></p>",
307
- 404,
308
- )
309
-
310
-
311
- @app.route("/api/state")
312
- def api_state():
313
- """Full dashboard state — polled by hub_dashboard.html every 2 s."""
314
- return jsonify(_state.get_state())
315
-
316
-
317
- @app.route("/api/rankings")
318
- def api_rankings():
319
- """Get current rankings."""
320
- return jsonify({"rankings": _state.get_state()["rankings"]})
321
-
322
-
323
- @app.route("/api/trades")
324
- def api_trades():
325
- """Returns open trades, recent closed trades, and summary stats."""
326
- return jsonify(_trade_parser.get_state())
327
-
328
-
329
- @app.route("/api/trades/open")
330
- def api_trades_open():
331
- """Get only open trades."""
332
- state = _trade_parser.get_state()
333
- return jsonify({"open": state["open"]})
334
-
335
-
336
- @app.route("/api/trades/closed")
337
- def api_trades_closed():
338
- """Get closed trades and stats."""
339
- limit = int(request.args.get("limit", 50))
340
- state = _trade_parser.get_state()
341
- return jsonify({
342
- "closed": state["closed"][:limit],
343
- "stats": state["stats"]
344
- })
345
-
346
-
347
- @app.route("/api/ranker/logs/recent")
348
- def api_logs_recent():
349
- """Get recent ranker logs (minimal implementation)."""
350
- limit = int(request.args.get("limit", 50))
351
-
352
- # Read directly from log file
353
- logs = []
354
- pattern = str(Path(_LOG_DIR) / "*.log")
355
- files = sorted(glob.glob(pattern))
356
-
357
- for fpath in files[-3:]: # Last 3 log files
358
- try:
359
- with open(fpath, "r") as f:
360
- lines = f.readlines()[-limit:]
361
- for line in lines:
362
- # Parse log line into structured format
363
- match = re.match(r'\[([^\]]+)\] \| (\w+) \| (\w+) \| (.*)', line.strip())
364
- if match:
365
- logs.append({
366
- "ts": match.group(1),
367
- "level": match.group(2),
368
- "category": match.group(3),
369
- "message": match.group(4),
370
- })
371
- except Exception:
372
- pass
373
-
374
- return jsonify({"logs": logs[-limit:], "count": len(logs)})
375
-
376
-
377
- @app.route("/api/ranker/logs/stats")
378
- def api_logs_stats():
379
- """Get log statistics."""
380
- pattern = str(Path(_LOG_DIR) / "*.log")
381
- files = glob.glob(pattern)
382
-
383
- total_lines = 0
384
- for fpath in files:
385
- try:
386
- with open(fpath, "r") as f:
387
- total_lines += sum(1 for _ in f)
388
- except Exception:
389
- pass
390
-
391
- return jsonify({
392
- "total_events": total_lines,
393
- "by_level": {"INFO": total_lines},
394
- "by_category": {},
395
- "by_asset": {},
396
- "errors": {}
397
- })
398
-
399
-
400
- @app.route("/api/health")
401
- def api_health():
402
- """Service health check."""
403
- return jsonify({
404
- "service": "hub_dashboard_service",
405
- "version": "v2.0",
406
- "status": "running",
407
- "log_dir": str(_LOG_DIR),
408
- "log_files": len(glob.glob(str(Path(_LOG_DIR) / "*.log"))),
409
- "timestamp": datetime.utcnow().isoformat() + "Z",
410
- **_state.get_state()["health"],
411
- })
412
-
413
-
414
- @app.errorhandler(404)
415
- def not_found(error):
416
- return jsonify({"error": "Endpoint not found"}), 404
417
-
418
-
419
- @app.errorhandler(500)
420
- def internal_error(error):
421
- return jsonify({"error": "Internal server error"}), 500
422
-
423
-
424
- # ══════════════════════════════════════════════════════════════════════════════════════
425
- # SECTION 4 — MAIN
426
- # ══════════════════════════════════════════════════════════════════════════════════════
427
-
428
- def main():
429
- """Start the dashboard service."""
430
- logger.info("=" * 70)
431
- logger.info(f"K1RL QUASAR — Hub Dashboard Service (v2.0 with Trade Log Parser)")
432
- logger.info(f"Dashboard HTML : {_HTML_PATH}")
433
- logger.info(f"Log directory : {_LOG_DIR}")
434
- logger.info(f"Service port : {_DASHBOARD_PORT}")
435
- logger.info("=" * 70)
436
-
437
- # Ensure log directory exists
438
- Path(_LOG_DIR).mkdir(parents=True, exist_ok=True)
439
-
440
- app.run(
441
- host="0.0.0.0",
442
- port=_DASHBOARD_PORT,
443
- debug=False,
444
- threaded=True,
445
- )
446
-
447
-
448
- if __name__ == "__main__":
449
- main()