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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +165 -158
app.py CHANGED
@@ -6,11 +6,9 @@ from aiohttp import web
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')
@@ -24,38 +22,51 @@ market_state = {
24
  "ready": False
25
  }
26
 
27
- # --- HTML Frontend ---
28
  HTML_PAGE = f"""
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>
51
 
52
  <script>
53
  const depthDiv = document.getElementById('depth-chart');
54
- const ratioDiv = document.getElementById('ratio-chart');
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,24 +74,16 @@ HTML_PAGE = f"""
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',
@@ -91,114 +94,98 @@ HTML_PAGE = f"""
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,21 +194,14 @@ async def kraken_worker():
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,53 +210,80 @@ async def handle_data(request):
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']
281
  })
282
 
@@ -284,14 +291,14 @@ async def main():
284
  app = web.Application()
285
  app.router.add_get('/', handle_index)
286
  app.router.add_get('/data', handle_data)
287
- app.on_startup.append(lambda a: a.setdefault('task', asyncio.create_task(kraken_worker())))
288
 
289
  runner = web.AppRunner(app)
290
  await runner.setup()
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__":
 
6
  import websockets
7
 
8
  # --- Configuration ---
9
+ SYMBOL_KRAKEN = "BTC/USD"
 
10
  PORT = 7860
11
+ HISTORY_LENGTH = 300
 
12
 
13
  # --- Logging ---
14
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(message)s')
 
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 3-Way Analysis</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: 50% Depth, 25% Distance, 25% History */
36
  #depth-chart {{ width: 100vw; height: 50vh; }}
37
+ #imbalance-chart {{ width: 100vw; height: 25vh; border-top: 1px solid #333; }}
38
  #price-chart {{ width: 100vw; height: 25vh; border-top: 1px solid #333; }}
39
 
40
+ #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; }}
41
  .green {{ color: #00e676; }}
42
  .red {{ color: #ff1744; }}
43
  </style>
44
  </head>
45
  <body>
46
+ <div id="status">Connecting...</div>
47
+
48
  <div id="depth-chart"></div>
49
+ <div id="imbalance-chart"></div>
50
  <div id="price-chart"></div>
51
 
52
  <script>
53
  const depthDiv = document.getElementById('depth-chart');
54
+ const imbDiv = document.getElementById('imbalance-chart');
55
  const priceDiv = document.getElementById('price-chart');
56
  const statusDiv = document.getElementById('status');
57
 
58
+ let initDepth = false;
59
+ let initImb = false;
60
+ let initPrice = false;
61
+
62
+ const commonConfig = {{ responsive: true, displayModeBar: false }};
63
+ const commonLayout = {{
64
+ paper_bgcolor: '#0e0e0e',
65
+ plot_bgcolor: '#0e0e0e',
66
+ font: {{ color: '#aaa' }},
67
+ margin: {{ t: 25, b: 25, l: 50, r: 20 }},
68
+ showlegend: false
69
+ }};
70
 
71
  async function updateCharts() {{
72
  try {{
 
74
  const data = await res.json();
75
 
76
  if (data.error) {{
77
+ statusDiv.innerHTML = "Waiting for data...";
78
  return;
79
  }}
80
 
81
+ // --- Update Status Header ---
82
+ statusDiv.innerHTML = `Mid: <span class="${{data.mid >= data.prev_mid ? 'green' : 'red'}}">$${{data.mid.toLocaleString(undefined, {{minimumFractionDigits: 2}})}}</span> | Bids: ${{data.bids_count}} | Asks: ${{data.asks_count}}`;
 
 
 
 
 
 
 
 
 
 
83
 
84
+ // ============================================================
85
+ // 1. DEPTH CHART (Standard Price Levels)
86
+ // ============================================================
87
  const traceBids = {{
88
  x: data.depth.bids_x, y: data.depth.bids_y,
89
  fill: 'tozeroy', type: 'scatter', mode: 'lines',
 
94
  fill: 'tozeroy', type: 'scatter', mode: 'lines',
95
  name: 'Asks', line: {{color: '#ff1744', width: 2}}
96
  }};
97
+ const layoutDepth = {{ ...commonLayout,
98
+ title: 'Orderbook Depth (Price Levels)',
99
+ xaxis: {{ gridcolor: '#222' }},
100
+ yaxis: {{ title: 'Volume', gridcolor: '#222' }}
 
 
101
  }};
102
+
103
+ if (!initDepth) {{ Plotly.newPlot(depthDiv, [traceBids, traceAsks], layoutDepth, commonConfig); initDepth = true; }}
104
+ else {{ Plotly.react(depthDiv, [traceBids, traceAsks], layoutDepth, commonConfig); }}
105
+
106
 
107
+ // ============================================================
108
+ // 2. IMBALANCE CHART (Cumulative Volume by Distance)
109
+ // ============================================================
110
+ const traceImbBids = {{
111
+ x: data.imbalance.dist_bids, y: data.imbalance.vol_bids,
112
+ type: 'scatter', mode: 'lines', name: 'Bid Strength',
113
+ line: {{color: '#00e676', width: 2, dash: 'solid'}}
 
 
 
 
114
  }};
115
+ const traceImbAsks = {{
116
+ x: data.imbalance.dist_asks, y: data.imbalance.vol_asks,
117
+ type: 'scatter', mode: 'lines', name: 'Ask Strength',
118
+ line: {{color: '#ff1744', width: 2, dash: 'solid'}}
 
 
 
119
  }};
120
+
121
+ const layoutImb = {{ ...commonLayout,
122
+ title: 'Liquidity Comparison (Distance from Mid)',
123
+ xaxis: {{ title: 'Distance ($)', gridcolor: '#222' }},
124
+ yaxis: {{ title: 'Cum Volume', gridcolor: '#222' }},
125
+ hovermode: 'x unified'
126
+ }};
127
+
128
+ if (!initImb) {{ Plotly.newPlot(imbDiv, [traceImbBids, traceImbAsks], layoutImb, commonConfig); initImb = true; }}
129
+ else {{ Plotly.react(imbDiv, [traceImbBids, traceImbAsks], layoutImb, commonConfig); }}
130
 
 
 
131
 
132
+ // ============================================================
133
+ // 3. PRICE HISTORY CHART
134
+ // ============================================================
135
+ const timeData = data.history.map(d => new Date(d.t * 1000));
136
+ const priceData = data.history.map(d => d.p);
137
  const traceHistory = {{
138
+ x: timeData, y: priceData,
139
+ type: 'scatter', mode: 'lines', name: 'Midprice',
140
+ line: {{ color: '#29b6f6', width: 2 }}
 
141
  }};
142
+ const layoutPrice = {{ ...commonLayout,
143
+ title: 'Midprice History',
 
 
144
  xaxis: {{ gridcolor: '#222', type: 'date' }},
145
+ yaxis: {{ gridcolor: '#222', tickformat: '.1f' }}
146
  }};
147
 
148
+ if (!initPrice) {{ Plotly.newPlot(priceDiv, [traceHistory], layoutPrice, commonConfig); initPrice = true; }}
149
+ else {{ Plotly.react(priceDiv, [traceHistory], layoutPrice, commonConfig); }}
150
 
151
+ }} catch (e) {{ console.error("Fetch error:", e); }}
152
  }}
153
+
154
+ setInterval(updateCharts, 500);
155
  </script>
156
  </body>
157
  </html>
158
  """
159
 
160
  async def kraken_worker():
 
161
  global market_state
 
162
  while True:
163
  try:
164
+ async with websockets.connect("wss://ws.kraken.com/v2") as ws:
165
+ logging.info(f"🔌 Connected to Kraken ({SYMBOL_KRAKEN})")
 
166
 
167
+ await ws.send(json.dumps({
168
+ "method": "subscribe",
169
+ "params": {"channel": "book", "symbol": [SYMBOL_KRAKEN], "depth": 500}
170
+ }))
 
 
 
171
 
172
  async for message in ws:
173
+ payload = json.loads(message)
174
+ channel = payload.get("channel")
175
+ data_entries = payload.get("data", [])
176
+
177
+ if channel == "book":
178
+ for item in data_entries:
179
+ for bid in item.get('bids', []):
180
+ q, p = float(bid['qty']), float(bid['price'])
181
+ if q == 0: market_state['bids'].pop(p, None)
182
+ else: market_state['bids'][p] = q
 
 
 
 
 
 
 
 
 
 
 
 
 
 
183
 
184
+ for ask in item.get('asks', []):
185
+ q, p = float(ask['qty']), float(ask['price'])
186
+ if q == 0: market_state['asks'].pop(p, None)
187
+ else: market_state['asks'][p] = q
188
+
 
189
  if market_state['bids'] and market_state['asks']:
190
  best_bid = max(market_state['bids'].keys())
191
  best_ask = min(market_state['asks'].keys())
 
194
  market_state['ready'] = True
195
 
196
  now = time.time()
197
+ if not market_state['history'] or (now - market_state['history'][-1]['t'] > 0.5):
 
198
  market_state['history'].append({'t': now, 'p': mid})
199
  if len(market_state['history']) > HISTORY_LENGTH:
200
  market_state['history'].pop(0)
 
 
 
 
 
 
201
 
202
  except Exception as e:
203
+ logging.warning(f"⚠️ Reconnecting: {e}")
204
+ await asyncio.sleep(3)
205
 
206
  async def handle_index(request):
207
  return web.Response(text=HTML_PAGE, content_type='text/html')
 
210
  if not market_state['ready']:
211
  return web.json_response({"error": "Initializing..."})
212
 
213
+ mid = market_state['current_mid']
214
+
215
+ # --- Prepare Raw Data ---
216
+ # Sort Bids: High to Low (Closest to mid first)
217
+ raw_bids = sorted(market_state['bids'].items(), key=lambda x: -x[0])
218
+ # Sort Asks: Low to High (Closest to mid first)
219
+ raw_asks = sorted(market_state['asks'].items(), key=lambda x: x[0])
220
+
221
+ # Limit depth for processing speed
222
+ LIMIT = 300
223
+ raw_bids = raw_bids[:LIMIT]
224
+ raw_asks = raw_asks[:LIMIT]
225
+
226
+ # --- 1. Standard Depth Data (Price on X) ---
227
+ b_x, b_y = [], []
228
  cum = 0
229
+ for p, q in raw_bids:
230
  cum += q
231
+ b_x.append(p)
232
+ b_y.append(cum)
233
+ # Reverse bids for visual chart (left to right)
234
+ b_x.reverse()
235
+ b_y.reverse()
236
 
237
+ a_x, a_y = [], []
238
  cum = 0
239
+ for p, q in raw_asks:
240
  cum += q
241
+ a_x.append(p)
242
+ a_y.append(cum)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
243
 
244
+ # --- 2. Imbalance Data (Distance on X) ---
245
+ # Logic: Calculate absolute distance from Mid, sort by distance, then cumulate.
246
+
247
+ # Process Bids by Distance
248
+ # (Distance, Quantity)
249
+ dist_bids_list = []
250
+ for p, q in raw_bids: # Already sorted closest to mid
251
+ d = mid - p
252
+ if d >= 0: dist_bids_list.append((d, q))
253
+
254
+ d_b_x, d_b_y = [], []
255
+ cum_d = 0
256
+ for d, q in dist_bids_list:
257
+ cum_d += q
258
+ d_b_x.append(d)
259
+ d_b_y.append(cum_d)
260
+
261
+ # Process Asks by Distance
262
+ dist_asks_list = []
263
+ for p, q in raw_asks: # Already sorted closest to mid
264
+ d = p - mid
265
+ if d >= 0: dist_asks_list.append((d, q))
266
+
267
+ d_a_x, d_a_y = [], []
268
+ cum_d = 0
269
+ for d, q in dist_asks_list:
270
+ cum_d += q
271
+ d_a_x.append(d)
272
+ d_a_y.append(cum_d)
273
 
274
  return web.json_response({
275
+ "mid": mid,
276
+ "prev_mid": market_state['history'][-2]['p'] if len(market_state['history']) > 1 else mid,
277
+ "bids_count": len(market_state['bids']),
278
+ "asks_count": len(market_state['asks']),
279
+ # Data for Top Chart
280
+ "depth": { "bids_x": b_x, "bids_y": b_y, "asks_x": a_x, "asks_y": a_y },
281
+ # Data for Middle Chart (Distance)
282
+ "imbalance": {
283
+ "dist_bids": d_b_x, "vol_bids": d_b_y,
284
+ "dist_asks": d_a_x, "vol_asks": d_a_y
285
  },
286
+ # Data for Bottom Chart
287
  "history": market_state['history']
288
  })
289
 
 
291
  app = web.Application()
292
  app.router.add_get('/', handle_index)
293
  app.router.add_get('/data', handle_data)
294
+ app.on_startup.append(lambda a: a.__setitem__('t', asyncio.create_task(kraken_worker())))
295
 
296
  runner = web.AppRunner(app)
297
  await runner.setup()
298
  site = web.TCPSite(runner, '0.0.0.0', PORT)
299
  await site.start()
300
 
301
+ print(f"🚀 BTC-USD 3-Way Dashboard: http://localhost:{PORT}")
302
  await asyncio.Event().wait()
303
 
304
  if __name__ == "__main__":