Alvin3y1 commited on
Commit
5a89d96
·
verified ·
1 Parent(s): c955262

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +141 -97
app.py CHANGED
@@ -8,7 +8,8 @@ import websockets
8
  # --- Configuration ---
9
  SYMBOL_KRAKEN = "BTC/USD"
10
  PORT = 7860
11
- HISTORY_LENGTH = 300 # Number of data points to keep for the price graph
 
12
 
13
  # --- Logging ---
14
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(message)s')
@@ -17,41 +18,46 @@ logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(message)s')
17
  market_state = {
18
  "bids": {},
19
  "asks": {},
20
- "history": [], # List of {'t': timestamp, 'p': price}
21
  "current_mid": 0.0,
22
  "ready": False
23
  }
24
 
25
- # --- HTML Frontend (Plotly.js) ---
26
  HTML_PAGE = f"""
27
  <!DOCTYPE html>
28
  <html>
29
  <head>
30
- <title>BTC-USD Advanced View</title>
31
  <script src="https://cdn.plot.ly/plotly-2.24.1.min.js"></script>
32
  <style>
33
  body {{ margin: 0; padding: 0; background-color: #0e0e0e; color: #ccc; font-family: sans-serif; overflow: hidden; }}
34
 
35
- /* Layout: Depth Chart on top, Price Chart on bottom */
36
- #depth-chart {{ width: 100vw; height: 65vh; }}
37
- #price-chart {{ width: 100vw; height: 35vh; border-top: 1px solid #333; }}
 
38
 
39
- #status {{ position: absolute; top: 10px; left: 60px; z-index: 10; font-size: 14px; background: rgba(0,0,0,0.6); padding: 5px 10px; border-radius: 4px; pointer-events: none;}}
40
  .green {{ color: #00e676; }}
41
  .red {{ color: #ff1744; }}
 
42
  </style>
43
  </head>
44
  <body>
45
  <div id="status">Connecting...</div>
46
  <div id="depth-chart"></div>
 
47
  <div id="price-chart"></div>
48
 
49
  <script>
50
  const depthDiv = document.getElementById('depth-chart');
 
51
  const priceDiv = document.getElementById('price-chart');
52
  const statusDiv = document.getElementById('status');
53
 
54
  let initDepth = false;
 
55
  let initPrice = false;
56
 
57
  async function updateCharts() {{
@@ -64,8 +70,17 @@ HTML_PAGE = f"""
64
  return;
65
  }}
66
 
67
- // --- Update Status Header ---
68
- statusDiv.innerHTML = `Midprice: <span class="${{data.mid >= data.prev_mid ? 'green' : 'red'}}">$${{data.mid.toLocaleString(undefined, {{minimumFractionDigits: 2}})}}</span> | Bids: ${{data.bids_count}} | Asks: ${{data.asks_count}}`;
 
 
 
 
 
 
 
 
 
69
 
70
  // --- 1. DEPTH CHART ---
71
  const traceBids = {{
@@ -89,19 +104,16 @@ HTML_PAGE = f"""
89
  }};
90
 
91
  const layoutDepth = {{
92
- title: '',
93
  autosize: true,
94
  paper_bgcolor: '#0e0e0e',
95
  plot_bgcolor: '#0e0e0e',
96
  font: {{ color: '#aaa' }},
97
  showlegend: false,
98
  margin: {{ t: 30, b: 30, l: 50, r: 20 }},
99
- xaxis: {{ title: 'Price Level', gridcolor: '#222' }},
100
- yaxis: {{ title: 'Volume', gridcolor: '#222' }}
101
  }};
102
 
103
- const config = {{ responsive: true, displayModeBar: false }};
104
-
105
  if (!initDepth) {{
106
  Plotly.newPlot(depthDiv, [traceBids, traceAsks], layoutDepth, config);
107
  initDepth = true;
@@ -109,8 +121,53 @@ HTML_PAGE = f"""
109
  Plotly.react(depthDiv, [traceBids, traceAsks], layoutDepth, config);
110
  }}
111
 
112
- // --- 2. PRICE HISTORY CHART ---
113
- // Extract timestamps and prices
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
114
  const timeData = data.history.map(d => new Date(d.t * 1000));
115
  const priceData = data.history.map(d => d.p);
116
 
@@ -124,15 +181,13 @@ HTML_PAGE = f"""
124
  }};
125
 
126
  const layoutPrice = {{
127
- title: '',
128
  autosize: true,
129
  paper_bgcolor: '#0e0e0e',
130
  plot_bgcolor: '#0e0e0e',
131
- font: {{ color: '#aaa' }},
132
- showlegend: false,
133
  margin: {{ t: 10, b: 30, l: 50, r: 20 }},
134
  xaxis: {{ gridcolor: '#222', type: 'date' }},
135
- yaxis: {{ gridcolor: '#222', tickformat: '.1f', title: 'Mid Price' }}
136
  }};
137
 
138
  if (!initPrice) {{
@@ -143,19 +198,17 @@ HTML_PAGE = f"""
143
  }}
144
 
145
  }} catch (e) {{
146
- console.error("Fetch error:", e);
147
  }}
148
  }}
149
 
150
- // Poll every 250ms for smoother animation
151
- setInterval(updateCharts, 250);
152
  </script>
153
  </body>
154
  </html>
155
  """
156
 
157
  async def kraken_worker():
158
- """Connects to Kraken WS and maintains orderbook + price history."""
159
  global market_state
160
 
161
  while True:
@@ -163,42 +216,30 @@ async def kraken_worker():
163
  async with websockets.connect("wss://ws.kraken.com/v2") as ws:
164
  logging.info(f"🔌 Connected to Kraken ({SYMBOL_KRAKEN})")
165
 
166
- # Subscribe to book only (we can derive price from book)
167
- msg = {
168
  "method": "subscribe",
169
- "params": {
170
- "channel": "book",
171
- "symbol": [SYMBOL_KRAKEN],
172
- "depth": 500
173
- }
174
- }
175
- await ws.send(json.dumps(msg))
176
 
177
  async for message in ws:
178
  payload = json.loads(message)
179
  channel = payload.get("channel")
180
  data_entries = payload.get("data", [])
181
 
182
- if payload.get("type") == "error":
183
- logging.error(f"Kraken Error: {payload}")
184
- continue
185
 
186
  if channel == "book":
187
- # Update Orderbook
188
  for item in data_entries:
189
  for bid in item.get('bids', []):
190
- qty = float(bid['qty'])
191
- price = float(bid['price'])
192
- if qty == 0: market_state['bids'].pop(price, None)
193
- else: market_state['bids'][price] = qty
194
 
195
  for ask in item.get('asks', []):
196
- qty = float(ask['qty'])
197
- price = float(ask['price'])
198
- if qty == 0: market_state['asks'].pop(price, None)
199
- else: market_state['asks'][price] = qty
200
 
201
- # Calculate Midprice
202
  if market_state['bids'] and market_state['asks']:
203
  best_bid = max(market_state['bids'].keys())
204
  best_ask = min(market_state['asks'].keys())
@@ -206,17 +247,14 @@ async def kraken_worker():
206
  market_state['current_mid'] = mid
207
  market_state['ready'] = True
208
 
209
- # Update History (Throttled slightly to avoid duplicate timestamps)
210
  now = time.time()
211
- # Only add if history is empty or last point was > 0.1s ago
212
- if not market_state['history'] or (now - market_state['history'][-1]['t'] > 0.1):
213
  market_state['history'].append({'t': now, 'p': mid})
214
- # Trim history
215
  if len(market_state['history']) > HISTORY_LENGTH:
216
  market_state['history'].pop(0)
217
 
218
  except Exception as e:
219
- logging.warning(f"⚠️ Kraken Connection lost: {e}. Reconnecting in 3s...")
220
  await asyncio.sleep(3)
221
 
222
  async def handle_index(request):
@@ -226,75 +264,81 @@ async def handle_data(request):
226
  if not market_state['ready']:
227
  return web.json_response({"error": "Initializing..."})
228
 
229
- # Prepare Depth Data
230
- # 1. Sort
231
- sorted_bids = sorted(market_state['bids'].items(), key=lambda x: -x[0])
232
- sorted_asks = sorted(market_state['asks'].items(), key=lambda x: x[0])
233
 
234
- # 2. Slice (Optimization)
235
- DEPTH_limit = 300
236
- sorted_bids = sorted_bids[:DEPTH_limit]
237
- sorted_asks = sorted_asks[:DEPTH_limit]
238
 
239
- # 3. Cumulative Sums
240
- b_x, b_y = [], []
 
 
 
 
241
  cum = 0
242
- for p, q in sorted_bids:
243
  cum += q
244
- b_x.append(p)
245
- b_y.append(cum)
246
- # Reverse bids for chart visualization (center outwards)
247
- b_x.reverse()
248
- b_y.reverse()
249
-
250
- a_x, a_y = [], []
251
  cum = 0
252
- for p, q in sorted_asks:
253
  cum += q
254
- a_x.append(p)
255
- a_y.append(cum)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
256
 
257
  return web.json_response({
258
  "mid": market_state['current_mid'],
259
  "prev_mid": market_state['history'][-2]['p'] if len(market_state['history']) > 1 else market_state['current_mid'],
260
- "bids_count": len(market_state['bids']),
261
- "asks_count": len(market_state['asks']),
262
  "depth": {
263
- "bids_x": b_x, "bids_y": b_y,
264
- "asks_x": a_x, "asks_y": a_y
265
  },
 
266
  "history": market_state['history']
267
  })
268
 
269
- async def start_background_tasks(app):
270
- app['kraken_task'] = asyncio.create_task(kraken_worker())
271
-
272
- async def cleanup_background_tasks(app):
273
- app['kraken_task'].cancel()
274
- await app['kraken_task']
275
-
276
  async def main():
277
  app = web.Application()
278
  app.router.add_get('/', handle_index)
279
  app.router.add_get('/data', handle_data)
 
280
 
281
- app.on_startup.append(start_background_tasks)
282
- app.on_cleanup.append(cleanup_background_tasks)
283
-
284
  runner = web.AppRunner(app)
285
  await runner.setup()
286
  site = web.TCPSite(runner, '0.0.0.0', PORT)
287
  await site.start()
288
-
289
- print("="*50)
290
- print(f"🚀 BTC-USD Dashboard Running")
291
- print(f"👉 Open: http://localhost:{PORT}")
292
- print("="*50)
293
-
294
  await asyncio.Event().wait()
295
 
296
  if __name__ == "__main__":
297
- try:
298
- asyncio.run(main())
299
- except KeyboardInterrupt:
300
- pass
 
8
  # --- Configuration ---
9
  SYMBOL_KRAKEN = "BTC/USD"
10
  PORT = 7860
11
+ HISTORY_LENGTH = 300
12
+ DEPTH_LIMIT = 400 # How many order book levels to process
13
 
14
  # --- Logging ---
15
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(message)s')
 
18
  market_state = {
19
  "bids": {},
20
  "asks": {},
21
+ "history": [],
22
  "current_mid": 0.0,
23
  "ready": False
24
  }
25
 
26
+ # --- HTML Frontend ---
27
  HTML_PAGE = f"""
28
  <!DOCTYPE html>
29
  <html>
30
  <head>
31
+ <title>BTC-USD Tri-View</title>
32
  <script src="https://cdn.plot.ly/plotly-2.24.1.min.js"></script>
33
  <style>
34
  body {{ margin: 0; padding: 0; background-color: #0e0e0e; color: #ccc; font-family: sans-serif; overflow: hidden; }}
35
 
36
+ /* Layout: 50% Depth, 25% Ratio, 25% Price */
37
+ #depth-chart {{ width: 100vw; height: 50vh; }}
38
+ #ratio-chart {{ width: 100vw; height: 25vh; border-top: 1px solid #333; }}
39
+ #price-chart {{ width: 100vw; height: 25vh; border-top: 1px solid #333; }}
40
 
41
+ #status {{ position: absolute; top: 10px; left: 60px; z-index: 10; font-size: 14px; background: rgba(0,0,0,0.7); padding: 5px 10px; border-radius: 4px; pointer-events: none; border: 1px solid #333; }}
42
  .green {{ color: #00e676; }}
43
  .red {{ color: #ff1744; }}
44
+ .blue {{ color: #29b6f6; }}
45
  </style>
46
  </head>
47
  <body>
48
  <div id="status">Connecting...</div>
49
  <div id="depth-chart"></div>
50
+ <div id="ratio-chart"></div>
51
  <div id="price-chart"></div>
52
 
53
  <script>
54
  const depthDiv = document.getElementById('depth-chart');
55
+ const ratioDiv = document.getElementById('ratio-chart');
56
  const priceDiv = document.getElementById('price-chart');
57
  const statusDiv = document.getElementById('status');
58
 
59
  let initDepth = false;
60
+ let initRatio = false;
61
  let initPrice = false;
62
 
63
  async function updateCharts() {{
 
70
  return;
71
  }}
72
 
73
+ // --- Header ---
74
+ // Calculate current imbalance ratio (end of the curve)
75
+ const currentRatio = data.ratios[data.ratios.length -1] || 1;
76
+ const ratioColor = currentRatio > 1 ? 'red' : 'green';
77
+
78
+ statusDiv.innerHTML = `
79
+ Mid: <span class="${{data.mid >= data.prev_mid ? 'green' : 'red'}}">$${{data.mid.toLocaleString(undefined, {{minimumFractionDigits: 0}})}}</span> |
80
+ Imbalance: <span class="${{ratioColor}}">${{currentRatio.toFixed(2)}}x</span>
81
+ `;
82
+
83
+ const config = {{ responsive: true, displayModeBar: false }};
84
 
85
  // --- 1. DEPTH CHART ---
86
  const traceBids = {{
 
104
  }};
105
 
106
  const layoutDepth = {{
 
107
  autosize: true,
108
  paper_bgcolor: '#0e0e0e',
109
  plot_bgcolor: '#0e0e0e',
110
  font: {{ color: '#aaa' }},
111
  showlegend: false,
112
  margin: {{ t: 30, b: 30, l: 50, r: 20 }},
113
+ xaxis: {{ title: '', gridcolor: '#222' }},
114
+ yaxis: {{ title: 'Cum Vol', gridcolor: '#222' }}
115
  }};
116
 
 
 
117
  if (!initDepth) {{
118
  Plotly.newPlot(depthDiv, [traceBids, traceAsks], layoutDepth, config);
119
  initDepth = true;
 
121
  Plotly.react(depthDiv, [traceBids, traceAsks], layoutDepth, config);
122
  }}
123
 
124
+ // --- 2. RATIO CHART (Imbalance) ---
125
+ // X axis is simply the "Depth Step" (1 to 300)
126
+ const steps = Array.from({{length: data.ratios.length}}, (_, i) => i + 1);
127
+
128
+ const traceRatio = {{
129
+ x: steps,
130
+ y: data.ratios,
131
+ type: 'scatter',
132
+ mode: 'lines',
133
+ name: 'Ask/Bid Ratio',
134
+ line: {{ color: '#fb8c00', width: 2 }},
135
+ fill: 'tozeroy'
136
+ }};
137
+
138
+ const layoutRatio = {{
139
+ title: '',
140
+ autosize: true,
141
+ paper_bgcolor: '#0e0e0e',
142
+ plot_bgcolor: '#0e0e0e',
143
+ font: {{ color: '#aaa', size: 10 }},
144
+ margin: {{ t: 10, b: 20, l: 50, r: 20 }},
145
+ xaxis: {{ title: 'Order Depth (Steps away from price)', gridcolor: '#222' }},
146
+ yaxis: {{
147
+ title: 'Ask/Bid Ratio',
148
+ gridcolor: '#222',
149
+ zeroline: true,
150
+ zerolinecolor: '#fff',
151
+ zerolinewidth: 1
152
+ }},
153
+ # Draw a line at 1.0
154
+ shapes: [{{
155
+ type: 'line',
156
+ x0: 0, x1: data.ratios.length,
157
+ y0: 1, y1: 1,
158
+ line: {{ color: 'white', width: 1, dash: 'dot' }}
159
+ }}]
160
+ }};
161
+
162
+ if (!initRatio) {{
163
+ Plotly.newPlot(ratioDiv, [traceRatio], layoutRatio, config);
164
+ initRatio = true;
165
+ }} else {{
166
+ layoutRatio.shapes[0].x1 = data.ratios.length; // Update line length
167
+ Plotly.react(ratioDiv, [traceRatio], layoutRatio, config);
168
+ }}
169
+
170
+ // --- 3. PRICE CHART ---
171
  const timeData = data.history.map(d => new Date(d.t * 1000));
172
  const priceData = data.history.map(d => d.p);
173
 
 
181
  }};
182
 
183
  const layoutPrice = {{
 
184
  autosize: true,
185
  paper_bgcolor: '#0e0e0e',
186
  plot_bgcolor: '#0e0e0e',
187
+ font: {{ color: '#aaa', size: 10 }},
 
188
  margin: {{ t: 10, b: 30, l: 50, r: 20 }},
189
  xaxis: {{ gridcolor: '#222', type: 'date' }},
190
+ yaxis: {{ gridcolor: '#222', tickformat: '.0f', title: 'Price' }}
191
  }};
192
 
193
  if (!initPrice) {{
 
198
  }}
199
 
200
  }} catch (e) {{
201
+ console.error(e);
202
  }}
203
  }}
204
 
205
+ setInterval(updateCharts, 500);
 
206
  </script>
207
  </body>
208
  </html>
209
  """
210
 
211
  async def kraken_worker():
 
212
  global market_state
213
 
214
  while True:
 
216
  async with websockets.connect("wss://ws.kraken.com/v2") as ws:
217
  logging.info(f"🔌 Connected to Kraken ({SYMBOL_KRAKEN})")
218
 
219
+ await ws.send(json.dumps({
 
220
  "method": "subscribe",
221
+ "params": {"channel": "book", "symbol": [SYMBOL_KRAKEN], "depth": 500}
222
+ }))
 
 
 
 
 
223
 
224
  async for message in ws:
225
  payload = json.loads(message)
226
  channel = payload.get("channel")
227
  data_entries = payload.get("data", [])
228
 
229
+ if payload.get("type") == "error": continue
 
 
230
 
231
  if channel == "book":
 
232
  for item in data_entries:
233
  for bid in item.get('bids', []):
234
+ q, p = float(bid['qty']), float(bid['price'])
235
+ if q == 0: market_state['bids'].pop(p, None)
236
+ else: market_state['bids'][p] = q
 
237
 
238
  for ask in item.get('asks', []):
239
+ q, p = float(ask['qty']), float(ask['price'])
240
+ if q == 0: market_state['asks'].pop(p, None)
241
+ else: market_state['asks'][p] = q
 
242
 
 
243
  if market_state['bids'] and market_state['asks']:
244
  best_bid = max(market_state['bids'].keys())
245
  best_ask = min(market_state['asks'].keys())
 
247
  market_state['current_mid'] = mid
248
  market_state['ready'] = True
249
 
 
250
  now = time.time()
251
+ if not market_state['history'] or (now - market_state['history'][-1]['t'] > 0.5):
 
252
  market_state['history'].append({'t': now, 'p': mid})
 
253
  if len(market_state['history']) > HISTORY_LENGTH:
254
  market_state['history'].pop(0)
255
 
256
  except Exception as e:
257
+ logging.warning(f"⚠️ Reconnecting... {e}")
258
  await asyncio.sleep(3)
259
 
260
  async def handle_index(request):
 
264
  if not market_state['ready']:
265
  return web.json_response({"error": "Initializing..."})
266
 
267
+ # 1. Sort Data
268
+ sorted_bids = sorted(market_state['bids'].items(), key=lambda x: -x[0]) # Descending (Best Bid first)
269
+ sorted_asks = sorted(market_state['asks'].items(), key=lambda x: x[0]) # Ascending (Best Ask first)
 
270
 
271
+ # 2. Slice to limit
272
+ sorted_bids = sorted_bids[:DEPTH_LIMIT]
273
+ sorted_asks = sorted_asks[:DEPTH_LIMIT]
 
274
 
275
+ # 3. Calculate Cumulative Volumes & Ratios
276
+ # We must calculate ratios BEFORE reversing the bids for the visual chart
277
+
278
+ b_y_raw = [] # Cumulative volume for bids (Best -> Worst)
279
+ a_y_raw = [] # Cumulative volume for asks (Best -> Worst)
280
+
281
  cum = 0
282
+ for _, q in sorted_bids:
283
  cum += q
284
+ b_y_raw.append(cum)
285
+
 
 
 
 
 
286
  cum = 0
287
+ for _, q in sorted_asks:
288
  cum += q
289
+ a_y_raw.append(cum)
290
+
291
+ # Calculate Ratio: Cumulative Ask Vol / Cumulative Bid Vol
292
+ # Logic: Index 0 is the "spread", Index 100 is "deep in the book"
293
+ ratios = []
294
+ calc_len = min(len(b_y_raw), len(a_y_raw))
295
+
296
+ for i in range(calc_len):
297
+ bid_vol = b_y_raw[i]
298
+ ask_vol = a_y_raw[i]
299
+
300
+ if bid_vol == 0: ratio = 1 # Prevent Div/0
301
+ else: ratio = ask_vol / bid_vol
302
+
303
+ ratios.append(ratio)
304
+
305
+ # 4. Prepare Visual Arrays for Depth Chart
306
+ # For the depth chart, Bids need to be reversed (Low -> High Price) so they connect to Asks in the middle
307
+ b_x_visual = [p for p, q in sorted_bids]
308
+ b_y_visual = b_y_raw[:] # Copy
309
+
310
+ # Reverse for Plotly "Mountain" effect
311
+ b_x_visual.reverse()
312
+ b_y_visual.reverse()
313
+
314
+ a_x_visual = [p for p, q in sorted_asks]
315
+ a_y_visual = a_y_raw # Asks are already Best -> Worst (Low -> High Price), which is correct for Right side of chart
316
 
317
  return web.json_response({
318
  "mid": market_state['current_mid'],
319
  "prev_mid": market_state['history'][-2]['p'] if len(market_state['history']) > 1 else market_state['current_mid'],
 
 
320
  "depth": {
321
+ "bids_x": b_x_visual, "bids_y": b_y_visual,
322
+ "asks_x": a_x_visual, "asks_y": a_y_visual
323
  },
324
+ "ratios": ratios,
325
  "history": market_state['history']
326
  })
327
 
 
 
 
 
 
 
 
328
  async def main():
329
  app = web.Application()
330
  app.router.add_get('/', handle_index)
331
  app.router.add_get('/data', handle_data)
332
+ app.on_startup.append(lambda a: a.setdefault('task', asyncio.create_task(kraken_worker())))
333
 
 
 
 
334
  runner = web.AppRunner(app)
335
  await runner.setup()
336
  site = web.TCPSite(runner, '0.0.0.0', PORT)
337
  await site.start()
338
+
339
+ print(f"🚀 BTC-USD Tri-View Running at http://localhost:{PORT}")
 
 
 
 
340
  await asyncio.Event().wait()
341
 
342
  if __name__ == "__main__":
343
+ try: asyncio.run(main())
344
+ except KeyboardInterrupt: pass