KarlQuant commited on
Commit
8a3f292
Β·
verified Β·
1 Parent(s): 2e08586

Upload 2 files

Browse files
Files changed (2) hide show
  1. hub_dashboard.html +174 -0
  2. hub_dashboard_service.py +35 -16
hub_dashboard.html CHANGED
@@ -2739,6 +2739,8 @@ function switchTab(tab) {
2739
  renderTradingTab();
2740
  } else if (tab === 'assets') {
2741
  renderAssetsTab();
 
 
2742
  }
2743
  }
2744
 
@@ -2962,6 +2964,178 @@ async function refresh(){
2962
  refresh(); setInterval(refresh,2000);
2963
  setInterval(()=>{ document.getElementById('kpi-upd').textContent=_health.last_update_ago!=null?_health.last_update_ago+'s ago':'β€”'; },1000);
2964
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2965
  /* ════════════════════════════════════════════════════════════════════════════
2966
  TRADING PANEL LOGIC
2967
  ════════════════════════════════════════════════════════════════════════════ */
 
2739
  renderTradingTab();
2740
  } else if (tab === 'assets') {
2741
  renderAssetsTab();
2742
+ } else if (tab === 'terminal') {
2743
+ updateTerminal();
2744
  }
2745
  }
2746
 
 
2964
  refresh(); setInterval(refresh,2000);
2965
  setInterval(()=>{ document.getElementById('kpi-upd').textContent=_health.last_update_ago!=null?_health.last_update_ago+'s ago':'β€”'; },1000);
2966
 
2967
+ /* ════════════════════════════════════════════════════════════════════════════
2968
+ TERMINAL PANEL LOGIC β€” polls /api/trades every 2s
2969
+ ════════════════════════════════════════════════════════════════════════════ */
2970
+ let _trmEquityChart = null;
2971
+ let _trmEquityCurve = []; // running cumulative PnL
2972
+
2973
+ async function updateTerminal() {
2974
+ let data;
2975
+ try {
2976
+ const res = await fetch('/api/trades');
2977
+ if (!res.ok) throw new Error('trades api error');
2978
+ data = await res.json();
2979
+ } catch(e) {
2980
+ console.error('[Terminal] fetch error:', e);
2981
+ return;
2982
+ }
2983
+
2984
+ const open = data.open || [];
2985
+ const closed = data.closed || [];
2986
+ const stats = data.stats || {};
2987
+
2988
+ // ── KPI strip ──────────────────────────────────────────────────────────────
2989
+ const totalPnl = stats.total_pnl || 0;
2990
+ const totalClosed = stats.total_closed || 0;
2991
+ const winCount = stats.win_count || 0;
2992
+ const lossCount = stats.loss_count || 0;
2993
+ const winRate = stats.win_rate || 0;
2994
+ const avgPnl = totalClosed > 0 ? (totalPnl / totalClosed) : 0;
2995
+
2996
+ // Equity = sum of all closed PnL (running balance proxy)
2997
+ const pnlClr = v => v > 0 ? 'var(--green)' : v < 0 ? 'var(--red)' : 'var(--t1)';
2998
+ const fPnl = v => (v >= 0 ? '+' : '') + v.toFixed(2);
2999
+
3000
+ const eqEl = document.getElementById('trm-equity');
3001
+ if (eqEl) { eqEl.textContent = fPnl(totalPnl); eqEl.style.color = pnlClr(totalPnl); }
3002
+
3003
+ const opEl = document.getElementById('trm-open-pnl');
3004
+ if (opEl) { opEl.textContent = open.length ? open.length + ' open' : 'β€”'; }
3005
+
3006
+ const ocEl = document.getElementById('trm-open-count');
3007
+ if (ocEl) ocEl.textContent = open.length + ' position' + (open.length !== 1 ? 's' : '');
3008
+
3009
+ const rpEl = document.getElementById('trm-realized-pnl');
3010
+ if (rpEl) { rpEl.textContent = fPnl(totalPnl); rpEl.style.color = pnlClr(totalPnl); }
3011
+
3012
+ const ccEl = document.getElementById('trm-closed-count');
3013
+ if (ccEl) ccEl.textContent = totalClosed + ' closed trades';
3014
+
3015
+ const wrEl = document.getElementById('trm-winrate');
3016
+ if (wrEl) wrEl.textContent = winRate + '%';
3017
+
3018
+ const wlEl = document.getElementById('trm-wl');
3019
+ if (wlEl) wlEl.textContent = 'W:' + winCount + ' / L:' + lossCount;
3020
+
3021
+ const apEl = document.getElementById('trm-avg-pnl');
3022
+ if (apEl) { apEl.textContent = fPnl(avgPnl); apEl.style.color = pnlClr(avgPnl); }
3023
+
3024
+ // Update sidebar Logs counter with trade count
3025
+ const scLogsEl = document.getElementById('sc-logs');
3026
+ if (scLogsEl && !isNaN(totalClosed)) {
3027
+ // don't overwrite log count β€” leave it
3028
+ }
3029
+
3030
+ // ── Equity curve chart ─────────────────────────────────────────────────────
3031
+ // Rebuild cumulative curve from closed trades (newest-first β†’ reverse for chrono)
3032
+ const chronoClosed = [...closed].reverse();
3033
+ let cum = 0;
3034
+ const curveData = chronoClosed.map(t => { cum += (t.pnl || 0); return +cum.toFixed(4); });
3035
+ const curveLabels = chronoClosed.map(t => t.closed_at ? t.closed_at.slice(11,19) : '');
3036
+
3037
+ const chartCanvas = document.getElementById('trm-equity-chart');
3038
+ if (chartCanvas) {
3039
+ if (!_trmEquityChart) {
3040
+ _trmEquityChart = new Chart(chartCanvas, {
3041
+ type: 'line',
3042
+ data: {
3043
+ labels: curveLabels,
3044
+ datasets: [{
3045
+ data: curveData,
3046
+ borderColor: '#E8720A',
3047
+ backgroundColor: 'rgba(232,114,10,0.08)',
3048
+ borderWidth: 2,
3049
+ fill: true,
3050
+ pointRadius: 0,
3051
+ tension: 0.4,
3052
+ }]
3053
+ },
3054
+ options: {
3055
+ responsive: true, maintainAspectRatio: false,
3056
+ plugins: { legend: { display: false } },
3057
+ scales: {
3058
+ x: { display: false },
3059
+ y: { grid: { color: 'rgba(255,255,255,0.04)' },
3060
+ ticks: { color: 'rgba(255,255,255,0.3)', font: { size: 9 }, maxTicksLimit: 5 } }
3061
+ }
3062
+ }
3063
+ });
3064
+ } else {
3065
+ _trmEquityChart.data.labels = curveLabels;
3066
+ _trmEquityChart.data.datasets[0].data = curveData;
3067
+ _trmEquityChart.update('none');
3068
+ }
3069
+ }
3070
+ const cmEl = document.getElementById('trm-curve-meta');
3071
+ if (cmEl) cmEl.textContent = totalClosed + ' closed trades Β· ' + new Date().toLocaleTimeString('en-GB',{hour12:false});
3072
+
3073
+ // ── Open positions table ────────────────────────────────────────────────────
3074
+ const ob = document.getElementById('trm-open-body');
3075
+ if (ob) {
3076
+ if (!open.length) {
3077
+ ob.innerHTML = '<tr><td colspan="8" style="padding:32px;text-align:center;color:var(--t4);font-size:0.75rem">No open positions</td></tr>';
3078
+ } else {
3079
+ const now = new Date();
3080
+ ob.innerHTML = open.map(t => {
3081
+ const dirCls = t.direction === 'LONG' || t.direction === 'BUY' ? 'BUY' : 'SELL';
3082
+ // Duration from opened_at
3083
+ let dur = 'β€”';
3084
+ if (t.opened_at) {
3085
+ const diffMs = now - new Date(t.opened_at);
3086
+ const diffS = Math.floor(diffMs / 1000);
3087
+ dur = diffS < 60 ? diffS + 's' : diffS < 3600 ? Math.floor(diffS/60) + 'm' : Math.floor(diffS/3600) + 'h ' + Math.floor((diffS%3600)/60) + 'm';
3088
+ }
3089
+ return `<tr>
3090
+ <td style="padding:12px 16px;font-size:0.72rem;color:var(--t3);font-family:monospace">${t.trade_id}</td>
3091
+ <td style="padding:12px 16px;font-weight:800;color:var(--t0)">${t.asset || 'β€”'}</td>
3092
+ <td style="padding:12px 16px"><span class="sig ${dirCls}" style="font-size:0.65rem">${t.direction || 'β€”'}</span></td>
3093
+ <td style="padding:12px 16px;text-align:right;font-variant-numeric:tabular-nums;color:var(--cyan)">${t.entry != null ? t.entry.toFixed(4) : 'β€”'}</td>
3094
+ <td style="padding:12px 16px;text-align:right;font-variant-numeric:tabular-nums">${t.qty != null ? t.qty.toFixed(6) : 'β€”'}</td>
3095
+ <td style="padding:12px 16px;text-align:right;color:var(--t3)">β€”</td>
3096
+ <td style="padding:12px 16px;text-align:right;color:var(--t2)">${dur}</td>
3097
+ <td style="padding:12px 16px;text-align:right;font-size:0.7rem;color:var(--t3)">${t.opened_at ? t.opened_at.replace('T',' ') : 'β€”'}</td>
3098
+ </tr>`;
3099
+ }).join('');
3100
+ }
3101
+ }
3102
+ const omEl = document.getElementById('trm-open-meta');
3103
+ if (omEl) omEl.textContent = open.length + ' position' + (open.length!==1?'s':'') + ' Β· live Β· updates every 2s';
3104
+
3105
+ // ── Trade history table ─────────────────────────────────────────────────────
3106
+ const hb = document.getElementById('trm-hist-body');
3107
+ if (hb) {
3108
+ if (!closed.length) {
3109
+ hb.innerHTML = '<tr><td colspan="9" style="padding:32px;text-align:center;color:var(--t4);font-size:0.75rem">No trade history</td></tr>';
3110
+ } else {
3111
+ hb.innerHTML = closed.map(t => {
3112
+ const pnl = t.pnl || 0;
3113
+ const pnlCls = pnl >= 0 ? 'color:var(--green)' : 'color:var(--red)';
3114
+ const dirCls = t.direction === 'LONG' || t.direction === 'BUY' ? 'BUY' : 'SELL';
3115
+ return `<tr>
3116
+ <td style="padding:10px 16px;font-size:0.68rem;color:var(--t3);font-family:monospace">${t.trade_id}</td>
3117
+ <td style="padding:10px 16px;font-weight:800;color:var(--t0)">${t.asset || 'β€”'}</td>
3118
+ <td style="padding:10px 16px"><span class="sig ${dirCls}" style="font-size:0.6rem">${t.direction || 'β€”'}</span></td>
3119
+ <td style="padding:10px 16px;text-align:right;font-variant-numeric:tabular-nums">${t.entry != null ? t.entry.toFixed(4) : 'β€”'}</td>
3120
+ <td style="padding:10px 16px;text-align:right;font-variant-numeric:tabular-nums;color:var(--t2)">β€”</td>
3121
+ <td style="padding:10px 16px;text-align:right;font-variant-numeric:tabular-nums">${t.qty != null ? t.qty.toFixed(6) : 'β€”'}</td>
3122
+ <td style="padding:10px 16px;text-align:right;font-weight:800;${pnlCls}">${pnl >= 0 ? '+' : ''}${pnl.toFixed(4)}</td>
3123
+ <td style="padding:10px 16px;text-align:right;color:var(--t3)">β€”</td>
3124
+ <td style="padding:10px 16px;text-align:right;font-size:0.7rem;color:var(--t3)">${t.closed_at ? t.closed_at.replace('T',' ') : 'β€”'}</td>
3125
+ </tr>`;
3126
+ }).join('');
3127
+ }
3128
+ }
3129
+ const hmEl = document.getElementById('trm-hist-meta');
3130
+ if (hmEl) hmEl.textContent = 'last ' + closed.length + ' closed trades';
3131
+ }
3132
+
3133
+ // Auto-refresh terminal tab every 2s (also on tab switch)
3134
+ setInterval(function(){
3135
+ const tEl = document.getElementById('terminal-tab');
3136
+ if (tEl && tEl.classList.contains('active')) updateTerminal();
3137
+ }, 2000);
3138
+
3139
  /* ════════════════════════════════════════════════════════════════════════════
3140
  TRADING PANEL LOGIC
3141
  ════════════════════════════════════════════════════════════════════════════ */
hub_dashboard_service.py CHANGED
@@ -59,15 +59,22 @@ class TradeLogParser:
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)
@@ -109,7 +116,10 @@ class TradeLogParser:
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
 
@@ -122,12 +132,14 @@ class TradeLogParser:
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
@@ -153,25 +165,32 @@ class TradeLogParser:
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 ─────────────────────────────────────────────────────────
 
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 from ranker_logging.py:
63
+ # [2026-03-30 17:14:35] | INFO | TRADE | V100_1s | TRADE OPENED | ID=... | Dir=long | Entry=0.0000 | Qty=10.0
64
+ # [2026-03-30 17:14:35] | INFO | TRADE | CRASH500 | TRADE CLOSED | ID=... | pnl=-3.52 | return=+0.01%
65
+ TRADE_OPEN_RE = re.compile(
66
+ r'TRADE OPENED \| ID=(\S+) \| Dir=(\w+) \| Entry=([\d.]+) \| Qty=([\d.]+)'
67
+ )
68
+ TRADE_OPEN_RE_NOQTY = re.compile(
69
+ r'TRADE OPENED \| ID=(\S+) \| Dir=(\w+) \| Entry=([\d.]+)'
70
+ )
71
+
72
  # Pattern for CLOSE: ... | TRADE CLOSED | ID=xxx | pnl=xxx | return=xxx
73
  TRADE_CLOSE_RE = re.compile(r'TRADE CLOSED \| ID=(\S+) \| pnl=([+-]?[\d.]+)')
74
+
75
+ # Asset sits between the 4th and 5th pipe-separated fields:
76
+ # "[ts] | LEVEL | TRADE | <ASSET> | ..."
77
+ TRADE_ASSET_RE = re.compile(r'\|\s*TRADE\s*\|\s*(\w+)\s*\|')
78
 
79
  def __init__(self, log_dir: str = _LOG_DIR):
80
  self.log_dir = Path(log_dir)
 
116
  time.sleep(2.0)
117
 
118
  def refresh(self) -> None:
119
+ """Find all log files, read new lines since last position.
120
+ On first call for each file, always scan from the beginning so trades
121
+ that were written before the service started are not missed.
122
+ """
123
  pattern = str(self.log_dir / "*.log")
124
  files = sorted(glob.glob(pattern))
125
 
 
132
  self._tail_file(fpath)
133
 
134
  def _tail_file(self, fpath: str) -> None:
135
+ """Read only new bytes from fpath since last call.
136
+ First encounter: start from byte 0 (full scan) so pre-existing trades are loaded."""
137
  try:
138
  size = os.path.getsize(fpath)
139
  except OSError:
140
  return
141
 
142
+ # Use 0 as default so a file seen for the first time is fully scanned
143
  last = self._last_pos.get(fpath, 0)
144
  if size <= last:
145
  return
 
165
  # ── TRADE OPENED ─────────────────────────────────────────────────────────
166
  m = self.TRADE_OPEN_RE.search(line)
167
  if m:
168
+ trade_id, direction, entry, qty = m.group(1), m.group(2), float(m.group(3)), float(m.group(4))
169
+ else:
170
+ m2 = self.TRADE_OPEN_RE_NOQTY.search(line)
171
+ if m2:
172
+ trade_id, direction, entry, qty = m2.group(1), m2.group(2), float(m2.group(3)), 0.0
173
+ else:
174
+ m2 = None
175
+ m = m2 # unify the branch below
176
+
177
+ if m:
178
  direction = direction.upper()
 
 
179
  ts = self._parse_timestamp(line)
180
+
181
  with self._lock:
182
  self._open[trade_id] = {
183
  "trade_id": trade_id,
184
  "asset": asset or trade_id.split('_')[0],
185
  "direction": direction,
186
  "entry": entry,
187
+ "qty": qty,
188
  "opened_at": ts,
189
  "status": "OPEN",
190
  }
191
  self._stats["total_opened"] += 1
192
+
193
+ logger.debug(f"[TradeLogParser] OPEN: {trade_id} | {direction} @ {entry} qty={qty}")
194
  return
195
 
196
  # ── TRADE CLOSED ─────────────────────────────────────────────────────────