Alvin3y1 commited on
Commit
c955262
Β·
verified Β·
1 Parent(s): a5396a6

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +117 -96
app.py CHANGED
@@ -6,20 +6,19 @@ from aiohttp import web
6
  import websockets
7
 
8
  # --- Configuration ---
9
- SYMBOL_DISPLAY = "BTC-USD"
10
  SYMBOL_KRAKEN = "BTC/USD"
11
  PORT = 7860
 
12
 
13
  # --- Logging ---
14
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(message)s')
15
 
16
  # --- In-Memory State ---
17
- # We store the orderbook in dictionaries: price -> quantity
18
- # This allows O(1) updates when Kraken sends changes.
19
  market_state = {
20
  "bids": {},
21
  "asks": {},
22
- "price": 0.0,
 
23
  "ready": False
24
  }
25
 
@@ -28,26 +27,34 @@ HTML_PAGE = f"""
28
  <!DOCTYPE html>
29
  <html>
30
  <head>
31
- <title>BTC-USD Depth Chart</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: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; }}
35
- #chart {{ width: 100vw; height: 100vh; }}
36
- #status {{ position: absolute; top: 10px; left: 60px; z-index: 10; font-size: 14px; background: rgba(0,0,0,0.5); padding: 5px; border-radius: 4px; pointer-events: none;}}
 
 
 
 
37
  .green {{ color: #00e676; }}
38
  .red {{ color: #ff1744; }}
39
  </style>
40
  </head>
41
  <body>
42
  <div id="status">Connecting...</div>
43
- <div id="chart"></div>
 
44
 
45
  <script>
46
- const chartDiv = document.getElementById('chart');
 
47
  const statusDiv = document.getElementById('status');
48
- let initialized = false;
 
 
49
 
50
- async function updateChart() {{
51
  try {{
52
  const res = await fetch('/data');
53
  const data = await res.json();
@@ -57,11 +64,13 @@ HTML_PAGE = f"""
57
  return;
58
  }}
59
 
60
- statusDiv.innerHTML = `Price: <span class="${{data.price >= data.prev_price ? 'green' : 'red'}}">$${{data.price.toLocaleString()}}</span> | Bids: ${{data.bids_count}} | Asks: ${{data.asks_count}}`;
 
61
 
 
62
  const traceBids = {{
63
- x: data.bids.x,
64
- y: data.bids.y,
65
  fill: 'tozeroy',
66
  type: 'scatter',
67
  mode: 'lines',
@@ -70,8 +79,8 @@ HTML_PAGE = f"""
70
  }};
71
 
72
  const traceAsks = {{
73
- x: data.asks.x,
74
- y: data.asks.y,
75
  fill: 'tozeroy',
76
  type: 'scatter',
77
  mode: 'lines',
@@ -79,50 +88,74 @@ HTML_PAGE = f"""
79
  line: {{color: '#ff1744', width: 2}}
80
  }};
81
 
82
- const layout = {{
83
- title: '<b>BTC/USD Depth</b>',
 
84
  paper_bgcolor: '#0e0e0e',
85
  plot_bgcolor: '#0e0e0e',
86
  font: {{ color: '#aaa' }},
87
  showlegend: false,
88
- xaxis: {{
89
- title: 'Price (USD)',
90
- gridcolor: '#333',
91
- tickformat: '.0f'
92
- }},
93
- yaxis: {{
94
- title: 'Volume (BTC)',
95
- gridcolor: '#333'
96
- }},
97
- margin: {{ t: 40, b: 40, l: 50, r: 20 }},
98
- hovermode: 'x unified'
99
  }};
100
 
101
  const config = {{ responsive: true, displayModeBar: false }};
102
 
103
- if (!initialized) {{
104
- Plotly.newPlot(chartDiv, [traceBids, traceAsks], layout, config);
105
- initialized = true;
106
  }} else {{
107
- Plotly.react(chartDiv, [traceBids, traceAsks], layout, config);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
108
  }}
109
 
110
  }} catch (e) {{
111
  console.error("Fetch error:", e);
112
- statusDiv.innerText = "Connection lost. Retrying...";
113
  }}
114
  }}
115
 
116
- // Poll every 500ms
117
- setInterval(updateChart, 500);
118
- updateChart();
119
  </script>
120
  </body>
121
  </html>
122
  """
123
 
124
  async def kraken_worker():
125
- """Connects to Kraken WS and maintains the orderbook in memory."""
126
  global market_state
127
 
128
  while True:
@@ -130,7 +163,7 @@ async def kraken_worker():
130
  async with websockets.connect("wss://ws.kraken.com/v2") as ws:
131
  logging.info(f"πŸ”Œ Connected to Kraken ({SYMBOL_KRAKEN})")
132
 
133
- # Subscribe
134
  msg = {
135
  "method": "subscribe",
136
  "params": {
@@ -140,16 +173,6 @@ async def kraken_worker():
140
  }
141
  }
142
  await ws.send(json.dumps(msg))
143
-
144
- # Also subscribe to ticker for the last traded price
145
- msg_ticker = {
146
- "method": "subscribe",
147
- "params": {
148
- "channel": "ticker",
149
- "symbol": [SYMBOL_KRAKEN]
150
- }
151
- }
152
- await ws.send(json.dumps(msg_ticker))
153
 
154
  async for message in ws:
155
  payload = json.loads(message)
@@ -160,75 +183,70 @@ async def kraken_worker():
160
  logging.error(f"Kraken Error: {payload}")
161
  continue
162
 
163
- if channel == "ticker":
164
- # Update Last Price
165
- for item in data_entries:
166
- if 'last' in item:
167
- market_state['price'] = float(item['last'])
168
-
169
- elif channel == "book":
170
  # Update Orderbook
171
  for item in data_entries:
172
- # Update Bids
173
  for bid in item.get('bids', []):
174
- price = float(bid['price'])
175
  qty = float(bid['qty'])
176
- if qty == 0:
177
- market_state['bids'].pop(price, None)
178
- else:
179
- market_state['bids'][price] = qty
180
 
181
- # Update Asks
182
  for ask in item.get('asks', []):
183
- price = float(ask['price'])
184
  qty = float(ask['qty'])
185
- if qty == 0:
186
- market_state['asks'].pop(price, None)
187
- else:
188
- market_state['asks'][price] = qty
189
 
190
- market_state['ready'] = True
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
191
 
192
  except Exception as e:
193
  logging.warning(f"⚠️ Kraken Connection lost: {e}. Reconnecting in 3s...")
194
  await asyncio.sleep(3)
195
 
196
  async def handle_index(request):
197
- """Serve the HTML page."""
198
  return web.Response(text=HTML_PAGE, content_type='text/html')
199
 
200
  async def handle_data(request):
201
- """Return the calculated cumulative volume arrays for Plotly."""
202
  if not market_state['ready']:
203
  return web.json_response({"error": "Initializing..."})
204
 
205
- # Prepare data snapshots (Thread-safe enough for this use case due to GIL/asyncio)
206
- bids = market_state['bids'].copy()
207
- asks = market_state['asks'].copy()
208
- current_price = market_state['price']
209
-
210
- # Sort Bids: High -> Low
211
- sorted_bids = sorted(bids.items(), key=lambda x: -x[0])
212
- # Sort Asks: Low -> High
213
- sorted_asks = sorted(asks.items(), key=lambda x: x[0])
214
 
215
- # Slice to keep chart performant (e.g., closest 300 orders)
216
- DEPTH = 400
217
- sorted_bids = sorted_bids[:DEPTH]
218
- sorted_asks = sorted_asks[:DEPTH]
219
 
220
- # Calculate Cumulative Volume for Bids
221
  b_x, b_y = [], []
222
  cum = 0
223
  for p, q in sorted_bids:
224
  cum += q
225
  b_x.append(p)
226
  b_y.append(cum)
227
- # Reverse bids for the chart line to draw outwards from center
228
  b_x.reverse()
229
  b_y.reverse()
230
 
231
- # Calculate Cumulative Volume for Asks
232
  a_x, a_y = [], []
233
  cum = 0
234
  for p, q in sorted_asks:
@@ -237,11 +255,15 @@ async def handle_data(request):
237
  a_y.append(cum)
238
 
239
  return web.json_response({
240
- "price": current_price,
241
- "bids": {"x": b_x, "y": b_y},
242
- "asks": {"x": a_x, "y": a_y},
243
- "bids_count": len(bids),
244
- "asks_count": len(asks)
 
 
 
 
245
  })
246
 
247
  async def start_background_tasks(app):
@@ -265,15 +287,14 @@ async def main():
265
  await site.start()
266
 
267
  print("="*50)
268
- print(f"πŸš€ BTC-USD Orderbook Chart Running")
269
  print(f"πŸ‘‰ Open: http://localhost:{PORT}")
270
  print("="*50)
271
 
272
- # Keep alive
273
  await asyncio.Event().wait()
274
 
275
  if __name__ == "__main__":
276
  try:
277
  asyncio.run(main())
278
  except KeyboardInterrupt:
279
- print("\nStopping server...")
 
6
  import websockets
7
 
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')
15
 
16
  # --- In-Memory State ---
 
 
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
 
 
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() {{
58
  try {{
59
  const res = await fetch('/data');
60
  const data = await res.json();
 
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 = {{
72
+ x: data.depth.bids_x,
73
+ y: data.depth.bids_y,
74
  fill: 'tozeroy',
75
  type: 'scatter',
76
  mode: 'lines',
 
79
  }};
80
 
81
  const traceAsks = {{
82
+ x: data.depth.asks_x,
83
+ y: data.depth.asks_y,
84
  fill: 'tozeroy',
85
  type: 'scatter',
86
  mode: 'lines',
 
88
  line: {{color: '#ff1744', width: 2}}
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;
108
  }} else {{
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
+
117
+ const traceHistory = {{
118
+ x: timeData,
119
+ y: priceData,
120
+ type: 'scatter',
121
+ mode: 'lines',
122
+ name: 'Midprice',
123
+ line: {{ color: '#29b6f6', width: 2 }}
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) {{
139
+ Plotly.newPlot(priceDiv, [traceHistory], layoutPrice, config);
140
+ initPrice = true;
141
+ }} else {{
142
+ Plotly.react(priceDiv, [traceHistory], layoutPrice, config);
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
  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": {
 
173
  }
174
  }
175
  await ws.send(json.dumps(msg))
 
 
 
 
 
 
 
 
 
 
176
 
177
  async for message in ws:
178
  payload = json.loads(message)
 
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())
205
+ mid = (best_bid + best_ask) / 2
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):
 
223
  return web.Response(text=HTML_PAGE, content_type='text/html')
224
 
225
  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:
 
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):
 
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