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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +116 -161
app.py CHANGED
@@ -6,10 +6,11 @@ from aiohttp import web
6
  import websockets
7
 
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')
@@ -28,24 +29,22 @@ 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>
@@ -56,9 +55,7 @@ HTML_PAGE = f"""
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() {{
64
  try {{
@@ -66,180 +63,142 @@ HTML_PAGE = f"""
66
  const data = await res.json();
67
 
68
  if (data.error) {{
69
- statusDiv.innerHTML = "Waiting for data...";
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 = {{
87
- x: data.depth.bids_x,
88
- y: data.depth.bids_y,
89
- fill: 'tozeroy',
90
- type: 'scatter',
91
- mode: 'lines',
92
- name: 'Bids',
93
- line: {{color: '#00e676', width: 2}}
94
  }};
95
-
96
  const traceAsks = {{
97
- x: data.depth.asks_x,
98
- y: data.depth.asks_y,
99
- fill: 'tozeroy',
100
- type: 'scatter',
101
- mode: 'lines',
102
- name: 'Asks',
103
- line: {{color: '#ff1744', width: 2}}
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;
120
- }} else {{
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
-
174
  const traceHistory = {{
175
- x: timeData,
176
- y: priceData,
177
- type: 'scatter',
178
- mode: 'lines',
179
- name: 'Midprice',
180
- line: {{ color: '#29b6f6', width: 2 }}
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) {{
194
- Plotly.newPlot(priceDiv, [traceHistory], layoutPrice, config);
195
- initPrice = true;
196
- }} else {{
197
- Plotly.react(priceDiv, [traceHistory], layoutPrice, config);
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:
215
  try:
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())
@@ -248,14 +207,21 @@ async def kraken_worker():
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):
261
  return web.Response(text=HTML_PAGE, content_type='text/html')
@@ -264,62 +230,51 @@ async def handle_data(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']
@@ -336,7 +291,7 @@ async def main():
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__":
 
6
  import websockets
7
 
8
  # --- Configuration ---
9
+ # Kraken V1 requires 'XBT/USD', not 'BTC/USD'
10
+ SYMBOL_KRAKEN = "XBT/USD"
11
  PORT = 7860
12
  HISTORY_LENGTH = 300
13
+ DEPTH_LIMIT = 500
14
 
15
  # --- Logging ---
16
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(message)s')
 
29
  <!DOCTYPE html>
30
  <html>
31
  <head>
32
+ <title>BTC-USD Live Analytics</title>
33
  <script src="https://cdn.plot.ly/plotly-2.24.1.min.js"></script>
34
  <style>
35
  body {{ margin: 0; padding: 0; background-color: #0e0e0e; color: #ccc; font-family: sans-serif; overflow: hidden; }}
36
 
 
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.8); padding: 5px 10px; border-radius: 4px; pointer-events: none; border: 1px solid #444; }}
42
  .green {{ color: #00e676; }}
43
  .red {{ color: #ff1744; }}
 
44
  </style>
45
  </head>
46
  <body>
47
+ <div id="status">Waiting for stream...</div>
48
  <div id="depth-chart"></div>
49
  <div id="ratio-chart"></div>
50
  <div id="price-chart"></div>
 
55
  const priceDiv = document.getElementById('price-chart');
56
  const statusDiv = document.getElementById('status');
57
 
58
+ let initDepth = false, initRatio = false, initPrice = false;
 
 
59
 
60
  async function updateCharts() {{
61
  try {{
 
63
  const data = await res.json();
64
 
65
  if (data.error) {{
66
+ statusDiv.innerHTML = "Initializing Kraken Stream...";
67
  return;
68
  }}
69
 
70
+ // --- Header Stats ---
 
71
  const currentRatio = data.ratios[data.ratios.length -1] || 1;
72
  const ratioColor = currentRatio > 1 ? 'red' : 'green';
73
+ const ratioText = currentRatio > 1 ? 'Bearish (Sell Wall)' : 'Bullish (Buy Support)';
74
 
75
  statusDiv.innerHTML = `
76
+ <b>BTC/USD</b> |
77
+ Price: <span class="${{data.mid >= data.prev_mid ? 'green' : 'red'}}">$${{data.mid.toLocaleString(undefined, {{minimumFractionDigits: 1}})}}</span> |
78
+ Imbalance: <span class="${{ratioColor}}">${{currentRatio.toFixed(2)}}x (${{ratioText}})</span>
79
  `;
80
 
81
  const config = {{ responsive: true, displayModeBar: false }};
82
 
83
  // --- 1. DEPTH CHART ---
84
  const traceBids = {{
85
+ x: data.depth.bids_x, y: data.depth.bids_y,
86
+ fill: 'tozeroy', type: 'scatter', mode: 'lines',
87
+ name: 'Bids', line: {{color: '#00e676', width: 2}}
 
 
 
 
88
  }};
 
89
  const traceAsks = {{
90
+ x: data.depth.asks_x, y: data.depth.asks_y,
91
+ fill: 'tozeroy', type: 'scatter', mode: 'lines',
92
+ name: 'Asks', line: {{color: '#ff1744', width: 2}}
 
 
 
 
93
  }};
 
94
  const layoutDepth = {{
95
+ autosize: true, paper_bgcolor: '#0e0e0e', plot_bgcolor: '#0e0e0e',
96
+ font: {{ color: '#aaa' }}, showlegend: false,
97
+ margin: {{ t: 30, b: 20, l: 50, r: 20 }},
98
+ xaxis: {{ showgrid: true, gridcolor: '#222' }},
99
+ yaxis: {{ title: 'Cumulative Vol', gridcolor: '#222' }}
 
 
 
100
  }};
101
 
102
+ if (!initDepth) {{ Plotly.newPlot(depthDiv, [traceBids, traceAsks], layoutDepth, config); initDepth = true; }}
103
+ else {{ Plotly.react(depthDiv, [traceBids, traceAsks], layoutDepth, config); }}
 
 
 
 
104
 
105
+ // --- 2. RATIO CHART ---
 
106
  const steps = Array.from({{length: data.ratios.length}}, (_, i) => i + 1);
 
107
  const traceRatio = {{
108
+ x: steps, y: data.ratios,
109
+ type: 'scatter', mode: 'lines',
 
 
110
  name: 'Ask/Bid Ratio',
111
  line: {{ color: '#fb8c00', width: 2 }},
112
+ fill: 'tozeroy'
113
  }};
 
114
  const layoutRatio = {{
115
+ autosize: true, paper_bgcolor: '#0e0e0e', plot_bgcolor: '#0e0e0e',
 
 
 
116
  font: {{ color: '#aaa', size: 10 }},
117
  margin: {{ t: 10, b: 20, l: 50, r: 20 }},
118
+ xaxis: {{ title: 'Depth Steps', gridcolor: '#222' }},
119
+ yaxis: {{ title: 'Imbalance Ratio', gridcolor: '#222' }},
120
+ shapes: [{{ type: 'line', x0: 0, x1: data.ratios.length, y0: 1, y1: 1, line: {{ color: 'white', width: 1, dash: 'dot' }} }}]
 
 
 
 
 
 
 
 
 
 
 
 
121
  }};
122
 
123
+ if (!initRatio) {{ Plotly.newPlot(ratioDiv, [traceRatio], layoutRatio, config); initRatio = true; }}
124
+ else {{ layoutRatio.shapes[0].x1 = data.ratios.length; Plotly.react(ratioDiv, [traceRatio], layoutRatio, config); }}
 
 
 
 
 
125
 
126
  // --- 3. PRICE CHART ---
 
 
 
127
  const traceHistory = {{
128
+ x: data.history.map(d => new Date(d.t * 1000)),
129
+ y: data.history.map(d => d.p),
130
+ type: 'scatter', mode: 'lines',
131
+ name: 'Price', line: {{ color: '#29b6f6', width: 2 }}
 
 
132
  }};
 
133
  const layoutPrice = {{
134
+ autosize: true, paper_bgcolor: '#0e0e0e', plot_bgcolor: '#0e0e0e',
 
 
135
  font: {{ color: '#aaa', size: 10 }},
136
  margin: {{ t: 10, b: 30, l: 50, r: 20 }},
137
  xaxis: {{ gridcolor: '#222', type: 'date' }},
138
+ yaxis: {{ gridcolor: '#222', tickformat: '.1f', title: 'Mid Price' }}
139
  }};
140
 
141
+ if (!initPrice) {{ Plotly.newPlot(priceDiv, [traceHistory], layoutPrice, config); initPrice = true; }}
142
+ else {{ Plotly.react(priceDiv, [traceHistory], layoutPrice, config); }}
 
 
 
 
143
 
144
+ }} catch (e) {{ console.error(e); }}
 
 
145
  }}
146
+ setInterval(updateCharts, 1000);
 
147
  </script>
148
  </body>
149
  </html>
150
  """
151
 
152
  async def kraken_worker():
153
+ """Connects to Standard Kraken V1 API (More Reliable)"""
154
  global market_state
155
 
156
  while True:
157
  try:
158
+ # Note: Using V1 Endpoint
159
+ async with websockets.connect("wss://ws.kraken.com") as ws:
160
+ logging.info(f"🔌 Connected to Kraken V1 ({SYMBOL_KRAKEN})")
161
 
162
+ # V1 Subscribe Message
163
+ sub_msg = {
164
+ "event": "subscribe",
165
+ "pair": [SYMBOL_KRAKEN],
166
+ "subscription": {"name": "book", "depth": 1000}
167
+ }
168
+ await ws.send(json.dumps(sub_msg))
169
 
170
  async for message in ws:
171
+ data = json.loads(message)
172
+
173
+ # V1 sends lists for data, dicts for events.
174
+ # [CHANNEL_ID, DATA_DICT, CHANNEL_NAME, PAIR]
175
+ if isinstance(data, list):
176
+ payload = data[1]
 
 
 
 
 
 
 
 
 
 
 
177
 
178
+ # Snapshot or Update?
179
+ # Snapshot has keys "bs" (bids snapshot) and "as" (asks snapshot)
180
+ if "bs" in payload or "as" in payload:
181
+ # logging.info("Received Snapshot")
182
+ for bid in payload.get("bs", []):
183
+ price, qty = float(bid[0]), float(bid[1])
184
+ market_state['bids'][price] = qty
185
+ for ask in payload.get("as", []):
186
+ price, qty = float(ask[0]), float(ask[1])
187
+ market_state['asks'][price] = qty
188
+
189
+ # Updates have keys "b" (bids) or "a" (asks)
190
+ else:
191
+ for bid in payload.get("b", []):
192
+ price, qty = float(bid[0]), float(bid[1])
193
+ if qty == 0: market_state['bids'].pop(price, None)
194
+ else: market_state['bids'][price] = qty
195
+
196
+ for ask in payload.get("a", []):
197
+ price, qty = float(ask[0]), float(ask[1])
198
+ if qty == 0: market_state['asks'].pop(price, None)
199
+ else: market_state['asks'][price] = qty
200
+
201
+ # --- Calculation Trigger ---
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())
 
207
  market_state['ready'] = True
208
 
209
  now = time.time()
210
+ # Throttle history update to ~200ms
211
+ if not market_state['history'] or (now - market_state['history'][-1]['t'] > 0.2):
212
  market_state['history'].append({'t': now, 'p': mid})
213
  if len(market_state['history']) > HISTORY_LENGTH:
214
  market_state['history'].pop(0)
215
+
216
+ elif isinstance(data, dict):
217
+ if data.get("event") == "heartbeat":
218
+ continue
219
+ if data.get("event") == "systemStatus":
220
+ logging.info(f"System Status: {data.get('status')}")
221
 
222
  except Exception as e:
223
+ logging.warning(f"⚠️ Connection error: {e}. Retrying in 5s...")
224
+ await asyncio.sleep(5)
225
 
226
  async def handle_index(request):
227
  return web.Response(text=HTML_PAGE, content_type='text/html')
 
230
  if not market_state['ready']:
231
  return web.json_response({"error": "Initializing..."})
232
 
233
+ # Sort
234
+ sorted_bids = sorted(market_state['bids'].items(), key=lambda x: -x[0])
235
+ sorted_asks = sorted(market_state['asks'].items(), key=lambda x: x[0])
236
 
237
+ # Slice
238
  sorted_bids = sorted_bids[:DEPTH_LIMIT]
239
  sorted_asks = sorted_asks[:DEPTH_LIMIT]
240
 
241
+ # Cumulative Calc
242
+ b_y_raw = []
 
 
 
 
243
  cum = 0
244
  for _, q in sorted_bids:
245
  cum += q
246
  b_y_raw.append(cum)
247
+
248
+ a_y_raw = []
249
  cum = 0
250
  for _, q in sorted_asks:
251
  cum += q
252
  a_y_raw.append(cum)
253
 
254
+ # Ratio Calc
 
255
  ratios = []
256
  calc_len = min(len(b_y_raw), len(a_y_raw))
 
257
  for i in range(calc_len):
258
  bid_vol = b_y_raw[i]
259
  ask_vol = a_y_raw[i]
260
+ ratio = ask_vol / bid_vol if bid_vol > 0 else 1
 
 
 
261
  ratios.append(ratio)
262
 
263
+ # Visual Prep
264
+ b_x = [p for p, q in sorted_bids]
265
+ # Reverse bids for Plotly
266
+ b_x.reverse()
267
+ b_y_visual = b_y_raw[:]
 
 
268
  b_y_visual.reverse()
269
 
270
+ a_x = [p for p, q in sorted_asks]
 
271
 
272
  return web.json_response({
273
  "mid": market_state['current_mid'],
274
  "prev_mid": market_state['history'][-2]['p'] if len(market_state['history']) > 1 else market_state['current_mid'],
275
  "depth": {
276
+ "bids_x": b_x, "bids_y": b_y_visual,
277
+ "asks_x": a_x, "asks_y": a_y_raw
278
  },
279
  "ratios": ratios,
280
  "history": market_state['history']
 
291
  site = web.TCPSite(runner, '0.0.0.0', PORT)
292
  await site.start()
293
 
294
+ print(f"🚀 Live at http://localhost:{PORT}")
295
  await asyncio.Event().wait()
296
 
297
  if __name__ == "__main__":