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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +357 -193
app.py CHANGED
@@ -3,14 +3,13 @@ import json
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,13 +18,13 @@ 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
26
  }
27
 
28
- # --- AI Logic Helper (Prediction & S/R) ---
29
  def analyze_structure(diff_x, diff_y, current_mid):
30
  """
31
  Analyzes the Net Liquidity Curve to find Support, Resistance, and Projected Trend.
@@ -33,18 +32,16 @@ def analyze_structure(diff_x, diff_y, current_mid):
33
  if not diff_y or len(diff_y) < 5:
34
  return None
35
 
36
- # 1. Momentum Projection (Weighted average of the whole book)
37
- # If the book is net positive, price wants to go up.
38
- net_total = diff_y[-1] # Cumulative sum at max depth
39
- # Scaling factor: heavily damped to prevent unrealistic predictions
40
- momentum_shift = net_total * 0.5
41
  projected_price = current_mid + momentum_shift
42
 
43
- # 2. Find Structural Reversals (Zero Crossings)
44
  support_level = None
45
  resistance_level = None
46
 
47
- # We look at the 'Near' book (first 50% of depth) for immediate reversal levels
48
  scan_limit = len(diff_y) // 2
49
 
50
  for i in range(1, scan_limit):
@@ -52,15 +49,11 @@ def analyze_structure(diff_x, diff_y, current_mid):
52
  curr_val = diff_y[i]
53
  dist = diff_x[i]
54
 
55
- # Case A: Positive -> Negative (The "Ceiling")
56
- # Buyers were in control, but suddenly Sellers take over at this distance.
57
- # This implies a Resistance Wall at (Mid + Distance).
58
  if prev_val > 0 and curr_val < 0 and resistance_level is None:
59
  resistance_level = current_mid + dist
60
 
61
- # Case B: Negative -> Positive (The "Floor")
62
- # Sellers were in control, but suddenly Buyers take over at this distance.
63
- # This implies a Support Wall at (Mid - Distance).
64
  if prev_val < 0 and curr_val > 0 and support_level is None:
65
  support_level = current_mid - dist
66
 
@@ -71,214 +64,382 @@ def analyze_structure(diff_x, diff_y, current_mid):
71
  "net_score": net_total
72
  }
73
 
74
- # --- HTML Frontend ---
75
  HTML_PAGE = f"""
76
  <!DOCTYPE html>
77
- <html>
78
  <head>
79
- <title>BTC-USD Predictive Dashboard</title>
 
 
 
 
 
80
  <script src="https://cdn.plot.ly/plotly-2.24.1.min.js"></script>
 
81
  <style>
82
- body {{ margin: 0; padding: 0; background-color: #0e0e0e; color: #ccc; font-family: 'Courier New', monospace; overflow: hidden; }}
83
-
84
- #container {{ display: flex; flex-direction: column; height: 100vh; width: 100vw; }}
85
-
86
- #row-top {{ flex: 1; display: flex; width: 100%; border-bottom: 1px solid #333; }}
87
- #row-bot {{ flex: 1; display: flex; width: 100%; }}
88
-
89
- .col {{ width: 50%; height: 100%; border-right: 1px solid #333; position: relative; }}
90
- .col-ai {{ width: 50%; height: 100%; background-color: #050505; display: flex; flex-direction: column; padding: 10px; overflow-y: auto; }}
 
91
 
92
- .terminal-header {{ color: #00bcd4; font-weight: bold; border-bottom: 1px dashed #333; padding-bottom: 5px; margin-bottom: 10px; }}
93
- .log-entry {{ margin-bottom: 8px; font-size: 13px; border-left: 3px solid transparent; padding-left: 8px; }}
94
- .log-time {{ color: #555; font-size: 11px; margin-right: 8px; }}
95
-
96
- .bullish {{ border-left-color: #00e676; color: #e8f5e9; }}
97
- .bearish {{ border-left-color: #ff1744; color: #ffebee; }}
98
- .neutral {{ border-left-color: #999; color: #ccc; }}
 
 
99
 
100
- .chart {{ width: 100%; height: 100%; }}
101
-
102
- #status {{ position: absolute; top: 10px; left: 60px; z-index: 100; font-size: 14px; background: rgba(0,0,0,0.8); padding: 5px 10px; border-radius: 4px; border: 1px solid #333; }}
103
- .green {{ color: #00e676; }}
104
- .red {{ color: #ff1744; }}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
105
  </style>
106
  </head>
107
  <body>
108
- <div id="status">Initializing AI Model...</div>
109
-
110
- <div id="container">
111
- <!-- ROW 1 -->
112
- <div id="row-top">
113
- <div class="col">
114
- <!-- PRICE + PREDICTION -->
115
- <div id="price-chart" class="chart"></div>
116
- </div>
117
- <div class="col">
118
- <div id="vol-chart" class="chart"></div>
119
  </div>
 
120
  </div>
121
 
122
- <!-- ROW 2 -->
123
- <div id="row-bot">
124
- <div class="col">
125
- <div id="diff-chart" class="chart"></div>
 
126
  </div>
127
- <div class="col-ai">
128
- <div class="terminal-header">> PREDICTIVE ANALYTICS ENGINE</div>
129
- <div id="terminal-logs"></div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
130
  </div>
131
  </div>
132
  </div>
133
 
134
  <script>
135
- const priceDiv = document.getElementById('price-chart');
136
- const volDiv = document.getElementById('vol-chart');
137
- const diffDiv = document.getElementById('diff-chart');
138
- const termLogs = document.getElementById('terminal-logs');
139
- const statusDiv = document.getElementById('status');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
140
 
141
- let initPrice = false, initVol = false, initDiff = false;
142
- let lastLogText = "";
143
-
144
- const commonConfig = {{ responsive: true, displayModeBar: false }};
145
- const commonLayout = {{
146
- paper_bgcolor: '#0e0e0e',
147
- plot_bgcolor: '#0e0e0e',
148
- font: {{ color: '#aaa', family: 'Courier New' }},
149
- margin: {{ t: 30, b: 25, l: 45, r: 20 }},
150
  showlegend: false,
151
- xaxis: {{ gridcolor: '#222' }},
152
- yaxis: {{ gridcolor: '#222' }}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
153
  }};
154
 
155
- function addLog(text, type) {{
156
- if (text === lastLogText) return;
157
- lastLogText = text;
158
 
 
159
  const div = document.createElement('div');
160
- div.className = `log-entry ${{type}}`;
161
- div.innerHTML = `<span class="log-time">${{new Date().toLocaleTimeString()}}</span> ${{text}}`;
162
- termLogs.prepend(div);
163
- if (termLogs.children.length > 15) termLogs.removeChild(termLogs.lastChild);
 
164
  }}
165
 
166
- async function updateCharts() {{
167
  try {{
168
  const res = await fetch('/data');
169
  const data = await res.json();
170
-
171
- if (data.error) {{
172
- statusDiv.innerHTML = "Waiting for data...";
173
- return;
174
- }}
175
-
176
- statusDiv.innerHTML = `Mid: <span class="${{data.mid >= data.prev_mid ? 'green' : 'red'}}">$${{data.mid.toLocaleString(undefined, {{minimumFractionDigits: 2}})}}</span>`;
177
-
178
- // --- 1. PRICE CHART WITH PREDICTION & S/R LINES ---
179
 
180
- // Historical Price Trace
181
- const historyX = data.history.map(d => new Date(d.t*1000));
182
- const historyY = data.history.map(d => d.p);
183
- const tracePrice = {{ x: historyX, y: historyY, type: 'scatter', mode:'lines', line: {{color: '#29b6f6', width: 2}}, name: 'Price' }};
184
-
185
- // Future Projection Trace
186
- let traceProj = {{}};
187
- const lastTime = historyX[historyX.length-1];
188
- if (data.analysis && lastTime) {{
189
- const futureTime = new Date(lastTime.getTime() + 60000); // +1 min
190
- traceProj = {{
191
- x: [lastTime, futureTime],
192
- y: [historyY[historyY.length-1], data.analysis.projected],
193
- type: 'scatter', mode: 'lines',
194
- line: {{color: '#ff9800', width: 2, dash: 'dot'}},
195
- name: 'Forecast'
196
- }};
197
- }}
198
-
199
- // Support & Resistance Shapes
200
- const shapes = [];
201
- let logMsg = "";
202
- let logType = "neutral";
203
 
 
204
  if (data.analysis) {{
205
- // Support Line (Green)
206
- if (data.analysis.support) {{
207
- shapes.push({{
208
- type: 'line', x0: historyX[0], x1: lastTime,
209
- y0: data.analysis.support, y1: data.analysis.support,
210
- line: {{color: '#00e676', width: 1, dash: 'dash'}}
211
- }});
212
- // Add Annotation via Layout? keeping it simple with shapes for now
213
- }}
214
-
215
- // Resistance Line (Red)
216
- if (data.analysis.resistance) {{
217
- shapes.push({{
218
- type: 'line', x0: historyX[0], x1: lastTime,
219
- y0: data.analysis.resistance, y1: data.analysis.resistance,
220
- line: {{color: '#ff1744', width: 1, dash: 'dash'}}
221
- }});
 
 
 
 
 
222
  }}
223
 
224
- // AI Commentary Logic based on Prediction
225
- const net = data.analysis.net_score;
226
- if (data.analysis.support && !data.analysis.resistance) {{
227
- logMsg = `🧱 <b>FLOOR DETECTED:</b> Strong Support at $${{data.analysis.support.toFixed(0)}}. Price expected to bounce.`;
228
- logType = "bullish";
229
- }} else if (!data.analysis.support && data.analysis.resistance) {{
230
- logMsg = `🧱 <b>CEILING DETECTED:</b> Resistance at $${{data.analysis.resistance.toFixed(0)}}. Upside capped.`;
231
- logType = "bearish";
232
- }} else if (net > 30) {{
233
- logMsg = `🚀 <b>MOMENTUM:</b> Liquidity is purely Bullish. Target: $${{data.analysis.projected.toFixed(0)}}`;
234
- logType = "bullish";
235
- }} else if (net < -30) {{
236
- logMsg = `📉 <b>MOMENTUM:</b> Liquidity is purely Bearish. Target: $${{data.analysis.projected.toFixed(0)}}`;
237
- logType = "bearish";
238
  }} else {{
239
- logMsg = `⚖️ <b>RANGE BOUND:</b> Price stuck between flows.`;
 
240
  }}
241
- }}
242
-
243
- if (logMsg) addLog(logMsg, logType);
244
-
245
- const priceLayout = {{
246
- ...commonLayout,
247
- title: '<b>Price Analysis & Forecast</b>',
248
- xaxis: {{type:'date', gridcolor:'#222'}},
249
- shapes: shapes
250
- }};
251
-
252
- if (!initPrice) {{ Plotly.newPlot(priceDiv, [tracePrice, traceProj], priceLayout, commonConfig); initPrice = true; }}
253
- else {{ Plotly.react(priceDiv, [tracePrice, traceProj], priceLayout, commonConfig); }}
254
-
255
-
256
- // --- 2. VOLUME CHART ---
257
- const tracesVol = [
258
- {{ x: data.vol.dist_bids, y: data.vol.vol_bids, type: 'scatter', name: 'Bids', line: {{color: '#00e676'}} }},
259
- {{ x: data.vol.dist_asks, y: data.vol.vol_asks, type: 'scatter', name: 'Ask', line: {{color: '#ff1744'}} }}
260
- ];
261
- if (!initVol) {{ Plotly.newPlot(volDiv, tracesVol, {{ ...commonLayout, title: '<b>Cumulative Volume</b>', xaxis: {{title:'Distance ($)'}} }}, commonConfig); initVol = true; }}
262
- else {{ Plotly.react(volDiv, tracesVol, {{ ...commonLayout, title: '<b>Cumulative Volume</b>', xaxis: {{title:'Distance ($)'}} }}, commonConfig); }}
263
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
264
 
265
- // --- 3. NET DIFFERENCE ---
266
- const traceDiff = {{ x: data.diff.x, y: data.diff.y, type: 'scatter', mode: 'lines', fill: 'tozeroy', line: {{color: '#e040fb', width: 2}} }};
267
- // Add Zero Line
268
- const diffLayout = {{
269
- ...commonLayout,
270
- title: '<b>Net Liquidity (Bids - Asks)</b>',
271
- xaxis: {{title:'Distance ($)'}},
272
- shapes: [{{ type: 'line', x0: 0, x1: 1, xref: 'paper', y0: 0, y1: 0, line: {{color: '#666', width: 1}} }}]
 
273
  }};
274
 
275
- if (!initDiff) {{ Plotly.newPlot(diffDiv, [traceDiff], diffLayout, commonConfig); initDiff = true; }}
276
- else {{ Plotly.react(diffDiv, [traceDiff], diffLayout, commonConfig); }}
 
 
 
277
 
278
- }} catch (e) {{ console.error("Fetch error:", e); }}
 
 
279
  }}
280
 
281
- setInterval(updateCharts, 1000);
282
  </script>
283
  </body>
284
  </html>
@@ -302,10 +463,12 @@ async def kraken_worker():
302
 
303
  if channel == "book":
304
  for item in data_entries:
 
305
  for bid in item.get('bids', []):
306
  q, p = float(bid['qty']), float(bid['price'])
307
  if q == 0: market_state['bids'].pop(p, None)
308
  else: market_state['bids'][p] = q
 
309
  for ask in item.get('asks', []):
310
  q, p = float(ask['qty']), float(ask['price'])
311
  if q == 0: market_state['asks'].pop(p, None)
@@ -314,12 +477,14 @@ async def kraken_worker():
314
  if market_state['bids'] and market_state['asks']:
315
  best_bid = max(market_state['bids'].keys())
316
  best_ask = min(market_state['asks'].keys())
317
- market_state['prev_mid'] = market_state['current_mid']
318
  mid = (best_bid + best_ask) / 2
 
 
319
  market_state['current_mid'] = mid
320
  market_state['ready'] = True
321
 
322
  now = time.time()
 
323
  if not market_state['history'] or (now - market_state['history'][-1]['t'] > 0.5):
324
  market_state['history'].append({'t': now, 'p': mid})
325
  if len(market_state['history']) > HISTORY_LENGTH:
@@ -338,11 +503,11 @@ async def handle_data(request):
338
 
339
  mid = market_state['current_mid']
340
 
341
- # --- Prepare Data ---
342
  raw_bids = sorted(market_state['bids'].items(), key=lambda x: -x[0])[:300]
343
  raw_asks = sorted(market_state['asks'].items(), key=lambda x: x[0])[:300]
344
 
345
- # Calculate Distances
346
  d_b_x, d_b_y, cum = [], [], 0
347
  for p, q in raw_bids:
348
  d = mid - p
@@ -357,15 +522,16 @@ async def handle_data(request):
357
  cum += q
358
  d_a_x.append(d); d_a_y.append(cum)
359
 
360
- # --- Calculate Net Liquidity Array (Diff) ---
361
  diff_x, diff_y = [], []
362
-
363
  if d_b_x and d_a_x:
364
  max_dist = min(d_b_x[-1], d_a_x[-1])
 
365
  step_size = max_dist / 100
366
  steps = [i * step_size for i in range(1, 101)]
367
 
368
  for s in steps:
 
369
  idx_b = bisect.bisect_right(d_b_x, s)
370
  vol_b = d_b_y[idx_b-1] if idx_b > 0 else 0
371
 
@@ -373,18 +539,16 @@ async def handle_data(request):
373
  vol_a = d_a_y[idx_a-1] if idx_a > 0 else 0
374
 
375
  diff_x.append(s)
376
- diff_y.append(vol_b - vol_a)
377
 
378
- # --- Run AI Analysis ---
379
  analysis = analyze_structure(diff_x, diff_y, mid)
380
 
381
  return web.json_response({
382
  "mid": mid,
383
- "prev_mid": market_state['prev_mid'],
384
- "vol": { "dist_bids": d_b_x, "vol_bids": d_b_y, "dist_asks": d_a_x, "vol_asks": d_a_y },
385
  "diff": { "x": diff_x, "y": diff_y },
386
- "analysis": analysis,
387
- "history": market_state['history']
388
  })
389
 
390
  async def start_background(app):
@@ -407,7 +571,7 @@ async def main():
407
  site = web.TCPSite(runner, '0.0.0.0', PORT)
408
  await site.start()
409
 
410
- print(f"🚀 BTC-USD Predictive Dashboard: http://localhost:{PORT}")
411
  await asyncio.Event().wait()
412
 
413
  if __name__ == "__main__":
 
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
  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
25
  }
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.
 
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
 
41
+ # 2. Find Structural Reversals
42
  support_level = None
43
  resistance_level = None
44
 
 
45
  scan_limit = len(diff_y) // 2
46
 
47
  for i in range(1, scan_limit):
 
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
 
 
64
  "net_score": net_total
65
  }
66
 
67
+ # --- Improved HTML Frontend ---
68
  HTML_PAGE = f"""
69
  <!DOCTYPE html>
70
+ <html lang="en">
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'),
330
+ supVal: document.getElementById('sup-val'),
331
+ projVal: document.getElementById('proj-val'),
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
 
442
+ setInterval(fetchData, 1000);
443
  </script>
444
  </body>
445
  </html>
 
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)
 
477
  if market_state['bids'] and market_state['asks']:
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:
 
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
  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
 
 
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
  })
553
 
554
  async def start_background(app):
 
571
  site = web.TCPSite(runner, '0.0.0.0', PORT)
572
  await site.start()
573
 
574
+ print(f"🚀 AI Dashboard: http://localhost:{PORT}")
575
  await asyncio.Event().wait()
576
 
577
  if __name__ == "__main__":