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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +211 -175
app.py CHANGED
@@ -1,243 +1,279 @@
1
  import asyncio
2
  import json
3
- from aiohttp import web
4
- import websockets
5
  import logging
6
  import time
 
 
7
 
8
- # Configure logging
9
- logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
10
-
11
- # In-memory storage
12
- symbol_data = {}
13
-
14
- # HTML Template with Plotly.js for rendering the Depth Chart
15
- HTML_TEMPLATE = """
 
 
 
 
 
 
 
 
 
 
 
 
16
  <!DOCTYPE html>
17
  <html>
18
  <head>
19
- <title>Orderbook Depth Chart - {symbol}</title>
20
  <script src="https://cdn.plot.ly/plotly-2.24.1.min.js"></script>
21
  <style>
22
- body { margin: 0; padding: 0; background-color: #111; color: #eee; font-family: sans-serif; }
23
- #chart { width: 100%; height: 100vh; }
24
- .loading { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); }
 
 
25
  </style>
26
  </head>
27
  <body>
28
- <div id="loading" class="loading">Loading Orderbook Data for {symbol}...</div>
29
  <div id="chart"></div>
30
 
31
  <script>
32
- const symbol = "{symbol_url}";
33
- let firstRun = true;
 
34
 
35
- async function fetchData() {
36
- try {
37
- const response = await fetch(`/${symbol}/json`);
38
- const data = await response.json();
39
 
40
- if (data.error || data.bids.x.length === 0) return;
 
 
 
41
 
42
- document.getElementById('loading').style.display = 'none';
43
 
44
- const traceBids = {
45
  x: data.bids.x,
46
  y: data.bids.y,
47
  fill: 'tozeroy',
48
  type: 'scatter',
49
  mode: 'lines',
50
- name: 'Bids (Buy)',
51
- line: {color: '#00ff00', width: 2}
52
- };
53
 
54
- const traceAsks = {
55
  x: data.asks.x,
56
  y: data.asks.y,
57
  fill: 'tozeroy',
58
  type: 'scatter',
59
  mode: 'lines',
60
- name: 'Asks (Sell)',
61
- line: {color: '#ff0000', width: 2}
62
- };
63
-
64
- const layout = {
65
- title: `Orderbook Depth: ${symbol.toUpperCase()} | Price: $${data.price}`,
66
- paper_bgcolor: '#111',
67
- plot_bgcolor: '#111',
68
- font: { color: '#eee' },
69
- xaxis: { title: 'Price', gridcolor: '#333' },
70
- yaxis: { title: 'Cumulative Volume', gridcolor: '#333' },
71
- showlegend: true,
72
- margin: { t: 40, b: 40, l: 60, r: 20 }
73
- };
74
-
75
- const config = { responsive: true };
76
-
77
- if (firstRun) {
78
- Plotly.newPlot('chart', [traceBids, traceAsks], layout, config);
79
- firstRun = false;
80
- } else {
81
- // Use react for efficient updates
82
- Plotly.react('chart', [traceBids, traceAsks], layout, config);
83
- }
84
-
85
- } catch (e) {
86
- console.error("Error fetching data", e);
87
- }
88
- }
89
-
90
- // Poll every 1 second
91
- setInterval(fetchData, 1000);
92
- fetchData();
 
 
 
 
 
 
 
 
93
  </script>
94
  </body>
95
  </html>
96
  """
97
 
98
- async def kraken_client(symbol):
99
- """Background task to fetch data from Kraken and update memory."""
100
- data = symbol_data[symbol]
101
- B, A = data["B"], data["A"]
102
-
103
  while True:
104
  try:
105
- logging.info(f"Connecting to Kraken WebSocket for {symbol}...")
106
  async with websockets.connect("wss://ws.kraken.com/v2") as ws:
107
- logging.info(f"Connected to Kraken for {symbol}!")
108
-
109
- # Subscribe to book and ticker
110
- params_book = {"channel": "book", "symbol": [symbol], "depth": 500}
111
- params_ticker = {"channel": "ticker", "symbol": [symbol]}
112
 
113
- await ws.send(json.dumps({"method": "subscribe", "params": params_book}))
114
- await ws.send(json.dumps({"method": "subscribe", "params": params_ticker}))
115
-
116
- async for msg in ws:
117
- d = json.loads(msg)
118
- ch = d.get("channel")
119
-
120
- if d.get('event') == 'heartbeat':
121
- continue
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
122
 
123
- # Update Last Price
124
- if ch == "ticker":
125
- dat = d.get("data", [{}])[0]
126
- if 'last' in dat:
127
- data["lp"] = float(dat['last'])
128
  continue
129
 
130
- # Update Orderbook
131
- if ch == "book":
132
- dat = d.get("data", [])
133
- for x in dat:
134
- # Process Bids
135
- for i in x.get('bids', []):
136
- p, q = float(i['price']), float(i['qty'])
137
- if q == 0: B.pop(p, None)
138
- else: B[p] = q
139
- # Process Asks
140
- for i in x.get('asks', []):
141
- p, q = float(i['price']), float(i['qty'])
142
- if q == 0: A.pop(p, None)
143
- else: A[p] = q
 
 
 
 
 
 
 
 
 
 
 
 
144
 
145
- data['last_update'] = time.time()
146
 
147
  except Exception as e:
148
- logging.error(f"Kraken error for {symbol}: {e}. Reconnecting...")
149
- await asyncio.sleep(5)
150
 
151
- async def page_handler(request):
152
  """Serve the HTML page."""
153
- symbol_url = request.match_info['symbol'].lower()
154
-
155
- if '-' not in symbol_url:
156
- return web.Response(text="Invalid symbol. Use format: btc-usd", status=400)
157
-
158
- # Convert url format (btc-usd) to Kraken format (BTC/USD)
159
- parts = symbol_url.split('-')
160
- symbol_kraken = f"{parts[0].upper()}/{parts[1].upper()}"
161
-
162
- # Ensure background task is running
163
- if symbol_kraken not in symbol_data:
164
- symbol_data[symbol_kraken] = {
165
- "B": {}, "A": {}, "lp": 0.0, "last_update": 0,
166
- "task": asyncio.create_task(kraken_client(symbol_kraken))
167
- }
168
-
169
- html = HTML_TEMPLATE.format(symbol=symbol_kraken, symbol_url=symbol_url)
170
- return web.Response(text=html, content_type='text/html')
171
-
172
- async def data_handler(request):
173
- """API Endpoint: Returns processed JSON data for the chart."""
174
- symbol_url = request.match_info['symbol'].lower()
175
- parts = symbol_url.split('-')
176
- symbol_kraken = f"{parts[0].upper()}/{parts[1].upper()}"
177
-
178
- if symbol_kraken not in symbol_data:
179
- return web.json_response({"error": "Data not initialized"}, status=404)
180
-
181
- data = symbol_data[symbol_kraken]
182
- B, A = data["B"], data["A"]
183
-
184
- # Sort Bids (Descending) and Asks (Ascending)
185
- sorted_bids = sorted(B.items(), key=lambda x: -x[0])
186
- sorted_asks = sorted(A.items(), key=lambda x: x[0])
187
-
188
- # Limit depth for the chart to keep it responsive (e.g., closest 200 points)
189
- depth_limit = 300
190
- sorted_bids = sorted_bids[:depth_limit]
191
- sorted_asks = sorted_asks[:depth_limit]
192
-
193
- # Calculate Cumulative Volume
194
- bids_x, bids_y = [], []
195
- cum_vol = 0
196
  for p, q in sorted_bids:
197
- cum_vol += q
198
- bids_x.append(p)
199
- bids_y.append(cum_vol)
200
-
201
- # Reverse bids so the line draws from left to right correctly in Plotly
202
- bids_x.reverse()
203
- bids_y.reverse()
204
-
205
- asks_x, asks_y = [], []
206
- cum_vol = 0
207
  for p, q in sorted_asks:
208
- cum_vol += q
209
- asks_x.append(p)
210
- asks_y.append(cum_vol)
211
 
212
  return web.json_response({
213
- "price": data["lp"],
214
- "bids": {"x": bids_x, "y": bids_y},
215
- "asks": {"x": asks_x, "y": asks_y}
 
 
216
  })
217
 
 
 
 
 
 
 
 
218
  async def main():
219
  app = web.Application()
 
 
220
 
221
- # Route for the HTML page
222
- app.router.add_get('/{symbol}', page_handler)
223
- # Route for the JSON data (polled by the HTML page)
224
- app.router.add_get('/{symbol}/json', data_handler)
225
 
226
  runner = web.AppRunner(app)
227
  await runner.setup()
228
- site = web.TCPSite(runner, '0.0.0.0', 7860)
229
  await site.start()
230
 
231
- print("=" * 60)
232
- print("πŸ“ˆ Orderbook Depth Chart Server Running")
233
- print("πŸ‘‰ Open in Browser: http://localhost:7860/btc-usd")
234
- print("πŸ‘‰ Or: http://localhost:7860/sol-usd")
235
- print("=" * 60)
236
 
 
237
  await asyncio.Event().wait()
238
 
239
- if __name__ == '__main__':
240
  try:
241
  asyncio.run(main())
242
  except KeyboardInterrupt:
243
- pass
 
1
  import asyncio
2
  import json
 
 
3
  import logging
4
  import time
5
+ 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
+
26
+ # --- HTML Frontend (Plotly.js) ---
27
+ 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();
54
 
55
+ if (data.error) {{
56
+ statusDiv.innerHTML = "Waiting for data...";
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',
68
+ name: 'Bids',
69
+ line: {{color: '#00e676', width: 2}}
70
+ }};
71
 
72
+ const traceAsks = {{
73
  x: data.asks.x,
74
  y: data.asks.y,
75
  fill: 'tozeroy',
76
  type: 'scatter',
77
  mode: 'lines',
78
+ name: 'Asks',
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:
129
  try:
 
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": {
137
+ "channel": "book",
138
+ "symbol": [SYMBOL_KRAKEN],
139
+ "depth": 500
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)
156
+ channel = payload.get("channel")
157
+ data_entries = payload.get("data", [])
158
 
159
+ if payload.get("type") == "error":
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:
235
+ cum += q
236
+ a_x.append(p)
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):
248
+ app['kraken_task'] = asyncio.create_task(kraken_worker())
249
+
250
+ async def cleanup_background_tasks(app):
251
+ app['kraken_task'].cancel()
252
+ await app['kraken_task']
253
+
254
  async def main():
255
  app = web.Application()
256
+ app.router.add_get('/', handle_index)
257
+ app.router.add_get('/data', handle_data)
258
 
259
+ app.on_startup.append(start_background_tasks)
260
+ app.on_cleanup.append(cleanup_background_tasks)
 
 
261
 
262
  runner = web.AppRunner(app)
263
  await runner.setup()
264
+ site = web.TCPSite(runner, '0.0.0.0', PORT)
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...")