Alvin3y1 commited on
Commit
b788fc7
·
verified ·
1 Parent(s): deb82b2

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +157 -304
app.py CHANGED
@@ -3,13 +3,14 @@ import json
3
  import logging
4
  import time
5
  import bisect
 
6
  from aiohttp import web
7
  import websockets
8
 
9
  # --- Configuration ---
10
  SYMBOL_KRAKEN = "BTC/USD"
11
  PORT = 7860
12
- HISTORY_LENGTH = 1000 # Increased history for better charts
13
 
14
  # --- Logging ---
15
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(message)s')
@@ -18,7 +19,7 @@ logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(message)s')
18
  market_state = {
19
  "bids": {},
20
  "asks": {},
21
- "history": [], # Stores { time: int, value: float }
22
  "current_mid": 0.0,
23
  "prev_mid": 0.0,
24
  "ready": False
@@ -26,15 +27,11 @@ market_state = {
26
 
27
  # --- AI Logic Helper ---
28
  def analyze_structure(diff_x, diff_y, current_mid):
29
- """
30
- Analyzes the Net Liquidity Curve to find Support, Resistance, and Projected Trend.
31
- """
32
  if not diff_y or len(diff_y) < 5:
33
  return None
34
 
35
  # 1. Momentum Projection
36
  net_total = diff_y[-1]
37
- # Damping factor
38
  momentum_shift = net_total * 0.2
39
  projected_price = current_mid + momentum_shift
40
 
@@ -49,11 +46,9 @@ def analyze_structure(diff_x, diff_y, current_mid):
49
  curr_val = diff_y[i]
50
  dist = diff_x[i]
51
 
52
- # Resistance: Buyers (Pos) -> Sellers (Neg)
53
  if prev_val > 0 and curr_val < 0 and resistance_level is None:
54
  resistance_level = current_mid + dist
55
 
56
- # Support: Sellers (Neg) -> Buyers (Pos)
57
  if prev_val < 0 and curr_val > 0 and support_level is None:
58
  support_level = current_mid - dist
59
 
@@ -71,259 +66,105 @@ HTML_PAGE = f"""
71
  <head>
72
  <meta charset="UTF-8">
73
  <title>AI Liquidity Dashboard | {SYMBOL_KRAKEN}</title>
74
-
75
- <!-- TradingView Lightweight Charts -->
76
  <script src="https://unpkg.com/lightweight-charts/dist/lightweight-charts.standalone.production.js"></script>
77
- <!-- Plotly for Depth Chart -->
78
  <script src="https://cdn.plot.ly/plotly-2.24.1.min.js"></script>
79
-
80
  <style>
81
  :root {{
82
  --bg-color: #0b0c10;
83
  --panel-bg: #1f2833;
84
  --text-main: #c5c6c7;
85
  --accent-green: #66fcf1;
86
- --accent-green-dim: #45a29e;
87
  --accent-red: #ff3b3b;
88
- --accent-red-dim: #a82828;
89
  --border: #2d3842;
90
  }}
91
-
92
- body {{
93
- margin: 0; padding: 0;
94
- background-color: var(--bg-color);
95
- color: var(--text-main);
96
- font-family: 'JetBrains Mono', 'Courier New', monospace;
97
- overflow: hidden;
98
- height: 100vh;
99
- width: 100vw;
100
- }}
101
-
102
- /* --- GRID LAYOUT --- */
103
- .grid-container {{
104
- display: grid;
105
- grid-template-columns: 3fr 1fr; /* Main chart 75%, Side panel 25% */
106
- grid-template-rows: 2fr 1fr; /* Top 66%, Bottom 33% */
107
- gap: 4px;
108
- height: 100vh;
109
- padding: 4px;
110
- box-sizing: border-box;
111
- }}
112
-
113
- .panel {{
114
- background: #12141a;
115
- border: 1px solid var(--border);
116
- border-radius: 4px;
117
- position: relative;
118
- display: flex;
119
- flex-direction: column;
120
- overflow: hidden;
121
- }}
122
-
123
- /* Grid Areas */
124
  #p-price {{ grid-column: 1 / 2; grid-row: 1 / 2; }}
125
  #p-depth {{ grid-column: 1 / 2; grid-row: 2 / 3; }}
126
- #p-stats {{ grid-column: 2 / 3; grid-row: 1 / 3; border-left: 2px solid var(--accent-green-dim); }}
127
-
128
- /* Headers */
129
- .panel-header {{
130
- padding: 8px 12px;
131
- background: #0f1116;
132
- border-bottom: 1px solid var(--border);
133
- font-size: 12px;
134
- text-transform: uppercase;
135
- font-weight: bold;
136
- display: flex;
137
- justify-content: space-between;
138
- align-items: center;
139
- color: var(--accent-green);
140
- }}
141
-
142
- /* --- CHART CONTAINERS --- */
143
- #tv-chart {{ flex: 1; width: 100%; }}
144
- #depth-chart {{ flex: 1; width: 100%; }}
145
-
146
- /* --- STATS PANEL --- */
147
- .stats-content {{
148
- padding: 15px;
149
- overflow-y: auto;
150
- flex: 1;
151
- }}
152
-
153
- .stat-box {{
154
- margin-bottom: 20px;
155
- padding: 10px;
156
- background: rgba(255,255,255,0.02);
157
- border-radius: 4px;
158
- }}
159
 
 
 
 
 
 
160
  .stat-label {{ font-size: 11px; color: #666; display: block; margin-bottom: 4px; }}
161
  .stat-value {{ font-size: 24px; font-weight: bold; }}
162
- .stat-sub {{ font-size: 12px; margin-left: 5px; }}
163
-
164
  .green {{ color: var(--accent-green); }}
165
- .red {{ color: var(--accent-red); }}
166
-
167
- /* --- TERMINAL --- */
168
- .terminal-box {{
169
- margin-top: auto;
170
- font-size: 11px;
171
- height: 300px;
172
- overflow-y: hidden;
173
- display: flex;
174
- flex-direction: column;
175
- }}
176
- .term-header {{ border-bottom: 1px dashed #444; margin-bottom: 5px; padding-bottom: 5px; opacity: 0.7; }}
177
  #term-logs {{ flex: 1; overflow-y: hidden; display: flex; flex-direction: column-reverse; }}
178
  .log-line {{ margin-top: 4px; padding-left: 8px; border-left: 2px solid #333; }}
179
- .log-bull {{ border-left-color: var(--accent-green); color: #e0f2f1; }}
180
- .log-bear {{ border-left-color: var(--accent-red); color: #ffebee; }}
181
-
182
- /* --- METER --- */
183
- .meter-container {{ width: 100%; height: 6px; background: #333; border-radius: 3px; margin-top: 10px; position: relative; overflow: hidden; }}
184
- .meter-bar {{ height: 100%; width: 50%; background: #555; transition: all 0.5s ease; position: absolute; left: 0; }}
185
  .mid-mark {{ position: absolute; left: 50%; height: 100%; width: 2px; background: #fff; z-index: 10; }}
186
 
187
- /* Load Overlay */
188
- #loader {{
189
- position: absolute; top:0; left:0; width:100%; height:100%;
190
- background: rgba(0,0,0,0.9); z-index: 999;
191
- display: flex; justify-content: center; align-items: center;
192
- color: var(--accent-green); font-size: 20px;
193
- }}
194
  </style>
195
  </head>
196
  <body>
197
 
198
- <div id="loader">INITIALIZING AI MODELS...</div>
 
 
 
 
199
 
200
  <div class="grid-container">
201
-
202
- <!-- PRICE CHART PANEL -->
203
  <div id="p-price" class="panel">
204
- <div class="panel-header">
205
- <span>BTC/USD Price Action</span>
206
- <span id="live-price" style="color:white;">---</span>
207
- </div>
208
  <div id="tv-chart"></div>
209
  </div>
210
-
211
- <!-- DEPTH / LIQUIDITY PANEL -->
212
  <div id="p-depth" class="panel">
213
- <div class="panel-header">
214
- <span>Net Liquidity Structure (Bid - Ask)</span>
215
- <span style="font-size:10px; color:#666;">DEPTH 300</span>
216
- </div>
217
  <div id="depth-chart"></div>
218
  </div>
219
-
220
- <!-- SIDEBAR / AI STATS -->
221
  <div id="p-stats" class="panel">
222
- <div class="panel-header">AI ANALYTICS ENGINE</div>
223
  <div class="stats-content">
224
-
225
- <!-- CURRENT SCORE -->
226
  <div class="stat-box">
227
  <span class="stat-label">NET LIQUIDITY SCORE</span>
228
- <div style="display:flex; align-items:baseline;">
229
- <span id="score-val" class="stat-value">0</span>
230
- </div>
231
- <div class="meter-container">
232
- <div class="mid-mark"></div>
233
- <div id="score-bar" class="meter-bar" style="width: 0%; left: 50%;"></div>
234
- </div>
235
  </div>
236
-
237
- <!-- KEY LEVELS -->
238
  <div class="stat-box">
239
- <span class="stat-label">DETECTED STRUCTURE</span>
240
- <div style="margin-top:8px;">
241
- <div style="display:flex; justify-content:space-between;">
242
- <span style="color:#aaa;">RESIST:</span>
243
- <span id="res-val" class="red">---</span>
244
- </div>
245
- <div style="display:flex; justify-content:space-between; margin-top:4px;">
246
- <span style="color:#aaa;">SUPPORT:</span>
247
- <span id="sup-val" class="green">---</span>
248
- </div>
249
- </div>
250
  </div>
251
-
252
- <!-- PREDICTED TARGET -->
253
  <div class="stat-box" style="border: 1px solid #444;">
254
- <span class="stat-label" style="color:var(--accent-green);">AI PRICE PROJECTION</span>
255
- <span id="proj-val" class="stat-value" style="font-size:20px;">---</span>
256
  </div>
257
-
258
- <!-- TERMINAL -->
259
  <div class="terminal-box">
260
  <div class="term-header">> SYSTEM LOGS</div>
261
  <div id="term-logs"></div>
262
  </div>
263
-
264
  </div>
265
  </div>
266
  </div>
267
 
268
  <script>
269
- // --- 1. SETUP TRADINGVIEW LIGHTWEIGHT CHART ---
270
- const chartContainer = document.getElementById('tv-chart');
271
- const chart = LightweightCharts.createChart(chartContainer, {{
272
- layout: {{ background: {{ type: 'solid', color: '#12141a' }}, textColor: '#888' }},
273
- grid: {{ vertLines: {{ color: '#1f2833' }}, horzLines: {{ color: '#1f2833' }} }},
274
- crosshair: {{ mode: LightweightCharts.CrosshairMode.Normal }},
275
- rightPriceScale: {{ borderColor: '#2d3842' }},
276
- timeScale: {{ borderColor: '#2d3842', timeVisible: true, secondsVisible: true }},
277
- }});
278
-
279
- const lineSeries = chart.addLineSeries({{
280
- color: '#2962FF',
281
- lineWidth: 2,
282
- crosshairMarkerVisible: true,
283
- lastValueVisible: false,
284
- priceLineVisible: false,
285
- }});
286
-
287
- // Forecast Line (Dashed)
288
- const predSeries = chart.addLineSeries({{
289
- color: '#ff9800',
290
- lineWidth: 2,
291
- lineStyle: LightweightCharts.LineStyle.Dotted,
292
- title: 'Forecast'
293
- }});
294
-
295
- // S/R Lines (Using PriceLines)
296
- let supportLine = null;
297
- let resistanceLine = null;
298
-
299
- // Auto Resize
300
- new ResizeObserver(entries => {{
301
- if (entries.length === 0 || entries[0].target !== chartContainer) {{ return; }}
302
- const newRect = entries[0].contentRect;
303
- chart.applyOptions({{ width: newRect.width, height: newRect.height }});
304
- }}).observe(chartContainer);
305
-
306
-
307
- // --- 2. SETUP PLOTLY (DEPTH) ---
308
- // We use Plotly here because LightweightCharts is bad at non-time-series X/Y plots
309
- const depthDiv = document.getElementById('depth-chart');
310
- const plotlyLayout = {{
311
- paper_bgcolor: '#12141a',
312
- plot_bgcolor: '#12141a',
313
- font: {{ color: '#888', family: 'JetBrains Mono' }},
314
- margin: {{ t: 10, b: 30, l: 40, r: 20 }},
315
- showlegend: false,
316
- xaxis: {{ gridcolor: '#1f2833', title: 'Distance ($)' }},
317
- yaxis: {{ gridcolor: '#1f2833' }}
318
  }};
319
- const plotlyConfig = {{ responsive: true, displayModeBar: false }};
320
- Plotly.newPlot(depthDiv, [], plotlyLayout, plotlyConfig);
321
-
322
 
323
- // --- 3. LOGIC & UPDATES ---
324
  const dom = {{
325
- price: document.getElementById('live-price'),
326
  loader: document.getElementById('loader'),
 
 
327
  scoreVal: document.getElementById('score-val'),
328
  scoreBar: document.getElementById('score-bar'),
329
  resVal: document.getElementById('res-val'),
@@ -332,110 +173,129 @@ HTML_PAGE = f"""
332
  logs: document.getElementById('term-logs')
333
  }};
334
 
335
- let lastTime = 0;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
336
 
337
  function log(msg, type='neutral') {{
338
  const div = document.createElement('div');
339
- div.className = `log-line ${{type === 'bull' ? 'log-bull' : type === 'bear' ? 'log-bear' : ''}}`;
340
- const timeStr = new Date().toLocaleTimeString('en-US', {{hour12:false}});
341
- div.innerHTML = `<span style="opacity:0.5; font-size:10px;">${{timeStr}}</span> ${{msg}}`;
342
  dom.logs.prepend(div);
343
  if (dom.logs.children.length > 20) dom.logs.removeChild(dom.logs.lastChild);
344
  }}
345
 
346
  async function fetchData() {{
347
  try {{
348
- const res = await fetch('/data');
 
349
  const data = await res.json();
350
-
351
- if (data.error) return; // Still initializing
 
 
 
 
 
352
  dom.loader.style.display = 'none';
353
 
354
- // -- A. UPDATE PRICE CHART --
355
- const history = data.history.map(d => ({{ time: Math.floor(d.t), value: d.p }}));
356
- // De-duplicate times for Lightweight Charts
357
  const uniqueHistory = [];
358
- const seenTimes = new Set();
359
- history.forEach(h => {{
360
- if(!seenTimes.has(h.time)) {{ seenTimes.add(h.time); uniqueHistory.push(h); }}
 
361
  }});
362
 
363
- lineSeries.setData(uniqueHistory);
364
- dom.price.innerHTML = `$${{data.mid.toLocaleString(undefined, {{minimumFractionDigits: 2}})}}`;
365
-
366
- // -- B. UPDATE AI ANALYSIS --
367
- if (data.analysis) {{
368
- const {{ projected, support, resistance, net_score }} = data.analysis;
369
-
370
- // 1. Prediction Line (Current Time -> Future)
371
- const lastT = uniqueHistory[uniqueHistory.length-1].time;
372
- predSeries.setData([
373
- {{ time: lastT, value: data.mid }},
374
- {{ time: lastT + 60, value: projected }} // 1 min projection
375
- ]);
376
- dom.projVal.innerHTML = `$${{projected.toLocaleString(undefined, {{maximumFractionDigits:0}})}}`;
377
- dom.projVal.style.color = projected > data.mid ? '#66fcf1' : '#ff3b3b';
378
-
379
- // 2. S/R Lines
380
- if (support) {{
381
- dom.supVal.innerText = `$${{support.toFixed(0)}}`;
382
- if (!supportLine) {{
383
- supportLine = lineSeries.createPriceLine({{
384
- price: support, color: '#00e676', lineWidth: 1, lineStyle: LightweightCharts.LineStyle.Solid, axisLabelVisible: true, title: 'SUP'
385
- }});
386
- }} else supportLine.applyOptions({{ price: support }});
387
- }} else {{
388
- dom.supVal.innerText = '---';
389
- if(supportLine) {{ lineSeries.removePriceLine(supportLine); supportLine=null; }}
390
- }}
391
-
392
- if (resistance) {{
393
- dom.resVal.innerText = `$${{resistance.toFixed(0)}}`;
394
- if (!resistanceLine) {{
395
- resistanceLine = lineSeries.createPriceLine({{
396
- price: resistance, color: '#ff1744', lineWidth: 1, lineStyle: LightweightCharts.LineStyle.Solid, axisLabelVisible: true, title: 'RES'
397
- }});
398
- }} else resistanceLine.applyOptions({{ price: resistance }});
399
- }} else {{
400
- dom.resVal.innerText = '---';
401
- if(resistanceLine) {{ lineSeries.removePriceLine(resistanceLine); resistanceLine=null; }}
402
- }}
403
-
404
- // 3. Score Meter
405
- dom.scoreVal.innerText = net_score.toFixed(1);
406
- dom.scoreVal.className = net_score > 0 ? "stat-value green" : "stat-value red";
407
 
408
- // Meter logic: Range approx -50 to 50
409
- let barWidth = Math.min(Math.abs(net_score)*2, 50);
410
- dom.scoreBar.style.width = `${{barWidth}}%`;
411
- dom.scoreBar.style.left = net_score > 0 ? "50%" : `${{50 - barWidth}}%`;
412
- dom.scoreBar.style.backgroundColor = net_score > 0 ? "var(--accent-green)" : "var(--accent-red)";
413
-
414
- // 4. Logs
415
- if (net_score > 25 && Math.random() > 0.9) log(`High Buying Pressure detected`, 'bull');
416
- if (net_score < -25 && Math.random() > 0.9) log(`High Selling Pressure detected`, 'bear');
417
- if (support && Math.random() > 0.95) log(`Price approaching support floor`, 'bull');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
418
  }}
419
 
420
- // -- C. UPDATE DEPTH CHART --
421
- const traceDiff = {{
422
- x: data.diff.x,
423
- y: data.diff.y,
424
- type: 'scatter',
425
- mode: 'lines',
426
- fill: 'tozeroy',
427
- line: {{color: '#e040fb', width: 2}},
428
- fillcolor: 'rgba(224, 64, 251, 0.1)'
429
- }};
430
-
431
- // Add a zero line
432
- const zeroLine = {{ type: 'line', x0: 0, x1: 1, xref: 'paper', y0: 0, y1: 0, line: {{color: '#444', width: 1}} }};
433
-
434
- const layoutUpdate = {{ ...plotlyLayout, shapes: [zeroLine] }};
435
- Plotly.react(depthDiv, [traceDiff], layoutUpdate, plotlyConfig);
436
 
437
  }} catch (e) {{
438
  console.error(e);
 
 
439
  }}
440
  }}
441
 
@@ -463,12 +323,10 @@ async def kraken_worker():
463
 
464
  if channel == "book":
465
  for item in data_entries:
466
- # Process Bids
467
  for bid in item.get('bids', []):
468
  q, p = float(bid['qty']), float(bid['price'])
469
  if q == 0: market_state['bids'].pop(p, None)
470
  else: market_state['bids'][p] = q
471
- # Process Asks
472
  for ask in item.get('asks', []):
473
  q, p = float(ask['qty']), float(ask['price'])
474
  if q == 0: market_state['asks'].pop(p, None)
@@ -478,14 +336,13 @@ async def kraken_worker():
478
  best_bid = max(market_state['bids'].keys())
479
  best_ask = min(market_state['asks'].keys())
480
  mid = (best_bid + best_ask) / 2
481
-
482
  market_state['prev_mid'] = market_state['current_mid']
483
  market_state['current_mid'] = mid
484
  market_state['ready'] = True
485
-
486
  now = time.time()
487
- # Throttle history updates slightly to prevent spamming duplicates
488
- if not market_state['history'] or (now - market_state['history'][-1]['t'] > 0.5):
489
  market_state['history'].append({'t': now, 'p': mid})
490
  if len(market_state['history']) > HISTORY_LENGTH:
491
  market_state['history'].pop(0)
@@ -498,16 +355,16 @@ async def handle_index(request):
498
  return web.Response(text=HTML_PAGE, content_type='text/html')
499
 
500
  async def handle_data(request):
 
501
  if not market_state['ready']:
502
  return web.json_response({"error": "Initializing..."})
503
 
504
  mid = market_state['current_mid']
505
 
506
- # Snapshot Bids/Asks
507
  raw_bids = sorted(market_state['bids'].items(), key=lambda x: -x[0])[:300]
508
  raw_asks = sorted(market_state['asks'].items(), key=lambda x: x[0])[:300]
509
 
510
- # Calculate Volume at Distance (Cumulative)
511
  d_b_x, d_b_y, cum = [], [], 0
512
  for p, q in raw_bids:
513
  d = mid - p
@@ -522,31 +379,27 @@ async def handle_data(request):
522
  cum += q
523
  d_a_x.append(d); d_a_y.append(cum)
524
 
525
- # Calculate Net Liquidity Curve (The "Diff" Chart)
526
  diff_x, diff_y = [], []
527
  if d_b_x and d_a_x:
528
  max_dist = min(d_b_x[-1], d_a_x[-1])
529
- # Create interpolated steps
530
  step_size = max_dist / 100
531
  steps = [i * step_size for i in range(1, 101)]
532
 
533
  for s in steps:
534
- # Find volume at distance 's' for bid and ask
535
  idx_b = bisect.bisect_right(d_b_x, s)
536
  vol_b = d_b_y[idx_b-1] if idx_b > 0 else 0
537
-
538
  idx_a = bisect.bisect_right(d_a_x, s)
539
  vol_a = d_a_y[idx_a-1] if idx_a > 0 else 0
540
 
541
  diff_x.append(s)
542
- diff_y.append(vol_b - vol_a) # Positive = Bullish Wall, Negative = Bearish Wall
543
 
544
- # Run Analysis
545
  analysis = analyze_structure(diff_x, diff_y, mid)
546
 
547
  return web.json_response({
548
  "mid": mid,
549
- "history": market_state['history'], # List of {t, p}
550
  "diff": { "x": diff_x, "y": diff_y },
551
  "analysis": analysis
552
  })
 
3
  import logging
4
  import time
5
  import bisect
6
+ import random
7
  from aiohttp import web
8
  import websockets
9
 
10
  # --- Configuration ---
11
  SYMBOL_KRAKEN = "BTC/USD"
12
  PORT = 7860
13
+ HISTORY_LENGTH = 300
14
 
15
  # --- Logging ---
16
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(message)s')
 
19
  market_state = {
20
  "bids": {},
21
  "asks": {},
22
+ "history": [],
23
  "current_mid": 0.0,
24
  "prev_mid": 0.0,
25
  "ready": False
 
27
 
28
  # --- AI Logic Helper ---
29
  def analyze_structure(diff_x, diff_y, current_mid):
 
 
 
30
  if not diff_y or len(diff_y) < 5:
31
  return None
32
 
33
  # 1. Momentum Projection
34
  net_total = diff_y[-1]
 
35
  momentum_shift = net_total * 0.2
36
  projected_price = current_mid + momentum_shift
37
 
 
46
  curr_val = diff_y[i]
47
  dist = diff_x[i]
48
 
 
49
  if prev_val > 0 and curr_val < 0 and resistance_level is None:
50
  resistance_level = current_mid + dist
51
 
 
52
  if prev_val < 0 and curr_val > 0 and support_level is None:
53
  support_level = current_mid - dist
54
 
 
66
  <head>
67
  <meta charset="UTF-8">
68
  <title>AI Liquidity Dashboard | {SYMBOL_KRAKEN}</title>
 
 
69
  <script src="https://unpkg.com/lightweight-charts/dist/lightweight-charts.standalone.production.js"></script>
 
70
  <script src="https://cdn.plot.ly/plotly-2.24.1.min.js"></script>
 
71
  <style>
72
  :root {{
73
  --bg-color: #0b0c10;
74
  --panel-bg: #1f2833;
75
  --text-main: #c5c6c7;
76
  --accent-green: #66fcf1;
 
77
  --accent-red: #ff3b3b;
 
78
  --border: #2d3842;
79
  }}
80
+ body {{ margin: 0; padding: 0; background-color: var(--bg-color); color: var(--text-main); font-family: monospace; overflow: hidden; height: 100vh; width: 100vw; }}
81
+
82
+ .grid-container {{ display: grid; grid-template-columns: 3fr 1fr; grid-template-rows: 2fr 1fr; gap: 4px; height: 100vh; padding: 4px; box-sizing: border-box; }}
83
+ .panel {{ background: #12141a; border: 1px solid var(--border); border-radius: 4px; position: relative; display: flex; flex-direction: column; overflow: hidden; }}
84
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
85
  #p-price {{ grid-column: 1 / 2; grid-row: 1 / 2; }}
86
  #p-depth {{ grid-column: 1 / 2; grid-row: 2 / 3; }}
87
+ #p-stats {{ grid-column: 2 / 3; grid-row: 1 / 3; border-left: 2px solid #45a29e; }}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
88
 
89
+ .panel-header {{ padding: 8px 12px; background: #0f1116; border-bottom: 1px solid var(--border); font-size: 12px; font-weight: bold; display: flex; justify-content: space-between; color: var(--accent-green); }}
90
+
91
+ #tv-chart, #depth-chart {{ flex: 1; width: 100%; }}
92
+ .stats-content {{ padding: 15px; overflow-y: auto; flex: 1; }}
93
+ .stat-box {{ margin-bottom: 20px; padding: 10px; background: rgba(255,255,255,0.02); border-radius: 4px; }}
94
  .stat-label {{ font-size: 11px; color: #666; display: block; margin-bottom: 4px; }}
95
  .stat-value {{ font-size: 24px; font-weight: bold; }}
 
 
96
  .green {{ color: var(--accent-green); }}
97
+ .red {{ color: var(--accent-red); }}
98
+
99
+ .terminal-box {{ margin-top: auto; font-size: 11px; height: 300px; display: flex; flex-direction: column; }}
100
+ .term-header {{ border-bottom: 1px dashed #444; margin-bottom: 5px; opacity: 0.7; }}
 
 
 
 
 
 
 
 
101
  #term-logs {{ flex: 1; overflow-y: hidden; display: flex; flex-direction: column-reverse; }}
102
  .log-line {{ margin-top: 4px; padding-left: 8px; border-left: 2px solid #333; }}
103
+
104
+ .meter-container {{ width: 100%; height: 6px; background: #333; margin-top: 10px; position: relative; overflow: hidden; }}
105
+ .meter-bar {{ height: 100%; width: 50%; background: #555; position: absolute; left: 0; transition: all 0.5s; }}
 
 
 
106
  .mid-mark {{ position: absolute; left: 50%; height: 100%; width: 2px; background: #fff; z-index: 10; }}
107
 
108
+ #loader {{ position: absolute; top:0; left:0; width:100%; height:100%; background: rgba(0,0,0,0.95); z-index: 999; display: flex; flex-direction: column; justify-content: center; align-items: center; color: var(--accent-green); }}
109
+ #error-log {{ color: var(--accent-red); font-size: 12px; margin-top: 20px; max-width: 80%; text-align: center; }}
 
 
 
 
 
110
  </style>
111
  </head>
112
  <body>
113
 
114
+ <div id="loader">
115
+ <div style="font-size: 24px; margin-bottom: 10px;">INITIALIZING AI MODELS...</div>
116
+ <div id="loading-status" style="font-size: 14px; color: #888;">Connecting to WebSocket...</div>
117
+ <div id="error-log"></div>
118
+ </div>
119
 
120
  <div class="grid-container">
 
 
121
  <div id="p-price" class="panel">
122
+ <div class="panel-header"><span>BTC/USD Price Action</span><span id="live-price">---</span></div>
 
 
 
123
  <div id="tv-chart"></div>
124
  </div>
 
 
125
  <div id="p-depth" class="panel">
126
+ <div class="panel-header"><span>Liquidity Structure</span><span>DEPTH 300</span></div>
 
 
 
127
  <div id="depth-chart"></div>
128
  </div>
 
 
129
  <div id="p-stats" class="panel">
130
+ <div class="panel-header">ANALYTICS ENGINE</div>
131
  <div class="stats-content">
 
 
132
  <div class="stat-box">
133
  <span class="stat-label">NET LIQUIDITY SCORE</span>
134
+ <span id="score-val" class="stat-value">0</span>
135
+ <div class="meter-container"><div class="mid-mark"></div><div id="score-bar" class="meter-bar" style="left: 50%; width: 0%;"></div></div>
 
 
 
 
 
136
  </div>
 
 
137
  <div class="stat-box">
138
+ <span class="stat-label">STRUCTURE</span>
139
+ <div style="display:flex; justify-content:space-between;"><span>RES:</span><span id="res-val" class="red">---</span></div>
140
+ <div style="display:flex; justify-content:space-between;"><span>SUP:</span><span id="sup-val" class="green">---</span></div>
 
 
 
 
 
 
 
 
141
  </div>
 
 
142
  <div class="stat-box" style="border: 1px solid #444;">
143
+ <span class="stat-label" style="color:var(--accent-green);">AI PROJECTION</span>
144
+ <span id="proj-val" class="stat-value">---</span>
145
  </div>
 
 
146
  <div class="terminal-box">
147
  <div class="term-header">> SYSTEM LOGS</div>
148
  <div id="term-logs"></div>
149
  </div>
 
150
  </div>
151
  </div>
152
  </div>
153
 
154
  <script>
155
+ // Global Error Handler
156
+ window.onerror = function(msg, url, lineNo, columnNo, error) {{
157
+ const errDiv = document.getElementById('error-log');
158
+ errDiv.innerHTML += `ERROR: ${{msg}} (Line: ${{lineNo}})<br>`;
159
+ document.getElementById('loading-status').style.color = 'red';
160
+ document.getElementById('loading-status').innerText = "SYSTEM FAILURE - CHECK CONSOLE";
161
+ return false;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
162
  }};
 
 
 
163
 
 
164
  const dom = {{
 
165
  loader: document.getElementById('loader'),
166
+ status: document.getElementById('loading-status'),
167
+ price: document.getElementById('live-price'),
168
  scoreVal: document.getElementById('score-val'),
169
  scoreBar: document.getElementById('score-bar'),
170
  resVal: document.getElementById('res-val'),
 
173
  logs: document.getElementById('term-logs')
174
  }};
175
 
176
+ // Verify Libraries
177
+ if (typeof LightweightCharts === 'undefined') throw new Error("LightweightCharts CDN failed to load.");
178
+ if (typeof Plotly === 'undefined') throw new Error("Plotly CDN failed to load.");
179
+
180
+ // --- CHART SETUP ---
181
+ const chart = LightweightCharts.createChart(document.getElementById('tv-chart'), {{
182
+ layout: {{ background: {{ type: 'solid', color: '#12141a' }}, textColor: '#888' }},
183
+ grid: {{ vertLines: {{ color: '#1f2833' }}, horzLines: {{ color: '#1f2833' }} }},
184
+ timeScale: {{ timeVisible: true, secondsVisible: true }},
185
+ }});
186
+ const lineSeries = chart.addLineSeries({{ color: '#2962FF', lineWidth: 2 }});
187
+ const predSeries = chart.addLineSeries({{ color: '#ff9800', lineWidth: 2, lineStyle: 2 }}); // Dotted
188
+
189
+ let supportLine = null, resistanceLine = null;
190
+
191
+ // Auto Resize
192
+ new ResizeObserver(entries => {{
193
+ if (entries[0].target) {{
194
+ const r = entries[0].contentRect;
195
+ chart.applyOptions({{ width: r.width, height: r.height }});
196
+ }}
197
+ }}).observe(document.getElementById('tv-chart'));
198
+
199
+ // Plotly Setup
200
+ Plotly.newPlot('depth-chart', [], {{
201
+ paper_bgcolor: '#12141a', plot_bgcolor: '#12141a',
202
+ font: {{ color: '#888', family: 'monospace' }},
203
+ margin: {{ t: 10, b: 30, l: 40, r: 20 }},
204
+ xaxis: {{ gridcolor: '#1f2833' }}, yaxis: {{ gridcolor: '#1f2833' }},
205
+ showlegend: false
206
+ }}, {{ responsive: true, displayModeBar: false }});
207
 
208
  function log(msg, type='neutral') {{
209
  const div = document.createElement('div');
210
+ div.className = 'log-line';
211
+ div.style.borderLeftColor = type === 'bull' ? '#66fcf1' : type === 'bear' ? '#ff3b3b' : '#333';
212
+ div.innerHTML = `<span style="opacity:0.5">${{new Date().toLocaleTimeString()}}</span> ${{msg}}`;
213
  dom.logs.prepend(div);
214
  if (dom.logs.children.length > 20) dom.logs.removeChild(dom.logs.lastChild);
215
  }}
216
 
217
  async function fetchData() {{
218
  try {{
219
+ // Cache busting with timestamp to prevent stuck "Initializing" state
220
+ const res = await fetch('/data?t=' + Date.now());
221
  const data = await res.json();
222
+
223
+ if (data.error) {{
224
+ dom.status.innerText = "Waiting for market data...";
225
+ return;
226
+ }}
227
+
228
+ // Data received - Hide Loader
229
  dom.loader.style.display = 'none';
230
 
231
+ // 1. Update Price Chart
 
 
232
  const uniqueHistory = [];
233
+ const seen = new Set();
234
+ data.history.forEach(d => {{
235
+ const t = Math.floor(d.t);
236
+ if (!seen.has(t)) {{ seen.add(t); uniqueHistory.push({{ time: t, value: d.p }}); }}
237
  }});
238
 
239
+ if (uniqueHistory.length > 0) {{
240
+ lineSeries.setData(uniqueHistory);
241
+ const last = uniqueHistory[uniqueHistory.length - 1];
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
242
 
243
+ if (data.analysis) {{
244
+ const {{ projected, support, resistance, net_score }} = data.analysis;
245
+
246
+ // Prediction
247
+ predSeries.setData([last, {{ time: last.time + 60, value: projected }}]);
248
+ dom.projVal.innerText = projected.toFixed(2);
249
+ dom.price.innerText = data.mid.toFixed(2);
250
+
251
+ // S/R Lines
252
+ if (support) {{
253
+ dom.supVal.innerText = support.toFixed(0);
254
+ if (!supportLine) supportLine = lineSeries.createPriceLine({{ price: support, color: '#00e676', title: 'SUP' }});
255
+ else supportLine.applyOptions({{ price: support }});
256
+ }} else {{
257
+ dom.supVal.innerText = '---';
258
+ if (supportLine) {{ lineSeries.removePriceLine(supportLine); supportLine = null; }}
259
+ }}
260
+
261
+ if (resistance) {{
262
+ dom.resVal.innerText = resistance.toFixed(0);
263
+ if (!resistanceLine) resistanceLine = lineSeries.createPriceLine({{ price: resistance, color: '#ff1744', title: 'RES' }});
264
+ else resistanceLine.applyOptions({{ price: resistance }});
265
+ }} else {{
266
+ dom.resVal.innerText = '---';
267
+ if (resistanceLine) {{ lineSeries.removePriceLine(resistanceLine); resistanceLine = null; }}
268
+ }}
269
+
270
+ // Score Meter
271
+ dom.scoreVal.innerText = net_score.toFixed(1);
272
+ dom.scoreVal.className = net_score > 0 ? "stat-value green" : "stat-value red";
273
+ let barW = Math.min(Math.abs(net_score)*2, 50);
274
+ dom.scoreBar.style.width = barW + '%';
275
+ dom.scoreBar.style.left = net_score > 0 ? '50%' : (50 - barW) + '%';
276
+ dom.scoreBar.style.background = net_score > 0 ? '#66fcf1' : '#ff3b3b';
277
+
278
+ // Random Logs
279
+ if (Math.random() > 0.95) {{
280
+ if (net_score > 30) log("Significant Bullish flow", 'bull');
281
+ if (net_score < -30) log("Significant Bearish flow", 'bear');
282
+ }}
283
+ }}
284
  }}
285
 
286
+ // 2. Update Depth Chart
287
+ const trace = {{ x: data.diff.x, y: data.diff.y, type: 'scatter', fill: 'tozeroy', line: {{color: '#e040fb'}} }};
288
+ Plotly.react('depth-chart', [trace], {{
289
+ paper_bgcolor: '#12141a', plot_bgcolor: '#12141a',
290
+ font: {{ color: '#888' }}, margin: {{ t: 10, b: 20, l: 30, r: 10 }},
291
+ xaxis: {{ showgrid: false }}, yaxis: {{ showgrid: true, gridcolor: '#333' }},
292
+ shapes: [{{ type: 'line', x0: 0, x1: 1, xref: 'paper', y0: 0, y1: 0, line: {{color: '#555'}} }}]
293
+ }}, {{ displayModeBar: false }});
 
 
 
 
 
 
 
 
294
 
295
  }} catch (e) {{
296
  console.error(e);
297
+ // Do not show error on UI for transient fetch errors, just log to console
298
+ // unless it persists
299
  }}
300
  }}
301
 
 
323
 
324
  if channel == "book":
325
  for item in data_entries:
 
326
  for bid in item.get('bids', []):
327
  q, p = float(bid['qty']), float(bid['price'])
328
  if q == 0: market_state['bids'].pop(p, None)
329
  else: market_state['bids'][p] = q
 
330
  for ask in item.get('asks', []):
331
  q, p = float(ask['qty']), float(ask['price'])
332
  if q == 0: market_state['asks'].pop(p, None)
 
336
  best_bid = max(market_state['bids'].keys())
337
  best_ask = min(market_state['asks'].keys())
338
  mid = (best_bid + best_ask) / 2
 
339
  market_state['prev_mid'] = market_state['current_mid']
340
  market_state['current_mid'] = mid
341
  market_state['ready'] = True
342
+
343
  now = time.time()
344
+ # Throttle history recording (200ms)
345
+ if not market_state['history'] or (now - market_state['history'][-1]['t'] > 0.2):
346
  market_state['history'].append({'t': now, 'p': mid})
347
  if len(market_state['history']) > HISTORY_LENGTH:
348
  market_state['history'].pop(0)
 
355
  return web.Response(text=HTML_PAGE, content_type='text/html')
356
 
357
  async def handle_data(request):
358
+ # Returns JSON. If not ready, returns error field.
359
  if not market_state['ready']:
360
  return web.json_response({"error": "Initializing..."})
361
 
362
  mid = market_state['current_mid']
363
 
364
+ # Snapshot & process
365
  raw_bids = sorted(market_state['bids'].items(), key=lambda x: -x[0])[:300]
366
  raw_asks = sorted(market_state['asks'].items(), key=lambda x: x[0])[:300]
367
 
 
368
  d_b_x, d_b_y, cum = [], [], 0
369
  for p, q in raw_bids:
370
  d = mid - p
 
379
  cum += q
380
  d_a_x.append(d); d_a_y.append(cum)
381
 
382
+ # Net Liquidity Curve
383
  diff_x, diff_y = [], []
384
  if d_b_x and d_a_x:
385
  max_dist = min(d_b_x[-1], d_a_x[-1])
 
386
  step_size = max_dist / 100
387
  steps = [i * step_size for i in range(1, 101)]
388
 
389
  for s in steps:
 
390
  idx_b = bisect.bisect_right(d_b_x, s)
391
  vol_b = d_b_y[idx_b-1] if idx_b > 0 else 0
 
392
  idx_a = bisect.bisect_right(d_a_x, s)
393
  vol_a = d_a_y[idx_a-1] if idx_a > 0 else 0
394
 
395
  diff_x.append(s)
396
+ diff_y.append(vol_b - vol_a)
397
 
 
398
  analysis = analyze_structure(diff_x, diff_y, mid)
399
 
400
  return web.json_response({
401
  "mid": mid,
402
+ "history": market_state['history'],
403
  "diff": { "x": diff_x, "y": diff_y },
404
  "analysis": analysis
405
  })