Alvin3y1 commited on
Commit
c43af34
·
verified ·
1 Parent(s): 75f3b2f

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +136 -125
app.py CHANGED
@@ -2,6 +2,7 @@ import asyncio
2
  import json
3
  import logging
4
  import time
 
5
  from aiohttp import web
6
  import websockets
7
 
@@ -27,15 +28,27 @@ 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; }}
@@ -45,18 +58,29 @@ HTML_PAGE = f"""
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 }};
@@ -64,8 +88,10 @@ HTML_PAGE = f"""
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() {{
@@ -78,75 +104,72 @@ HTML_PAGE = f"""
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',
90
- name: 'Bids', line: {{color: '#00e676', width: 2}}
91
- }};
92
- const traceAsks = {{
93
- x: data.depth.asks_x, y: data.depth.asks_y,
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
  }}
@@ -163,7 +186,6 @@ async def kraken_worker():
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}
@@ -180,7 +202,6 @@ async def kraken_worker():
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)
@@ -213,26 +234,17 @@ async def handle_data(request):
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
@@ -241,70 +253,69 @@ async def handle_data(request):
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
 
290
  async def start_background(app):
291
- """Correct way to start background task in aiohttp"""
292
  app['kraken_task'] = asyncio.create_task(kraken_worker())
293
 
294
  async def cleanup_background(app):
295
- """Correct way to cleanup background task"""
296
  app['kraken_task'].cancel()
297
- try:
298
- await app['kraken_task']
299
- except asyncio.CancelledError:
300
- pass
301
 
302
  async def main():
303
  app = web.Application()
304
  app.router.add_get('/', handle_index)
305
  app.router.add_get('/data', handle_data)
306
-
307
- # Register startup and cleanup handlers safely
308
  app.on_startup.append(start_background)
309
  app.on_cleanup.append(cleanup_background)
310
 
@@ -313,7 +324,7 @@ async def main():
313
  site = web.TCPSite(runner, '0.0.0.0', PORT)
314
  await site.start()
315
 
316
- print(f"🚀 BTC-USD 3-Way Dashboard: http://localhost:{PORT}")
317
  await asyncio.Event().wait()
318
 
319
  if __name__ == "__main__":
 
2
  import json
3
  import logging
4
  import time
5
+ import bisect
6
  from aiohttp import web
7
  import websockets
8
 
 
28
  <!DOCTYPE html>
29
  <html>
30
  <head>
31
+ <title>BTC-USD 4-Way Analysis</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 Grid */
37
+ #container {{ display: flex; flex-direction: column; height: 100vh; width: 100vw; }}
38
+
39
+ /* Row 1: Depth (50%) */
40
+ #row-top {{ flex: 2; width: 100%; border-bottom: 1px solid #333; }}
41
+
42
+ /* Row 2: Split Middle (25%) */
43
+ #row-mid {{ flex: 1; display: flex; width: 100%; border-bottom: 1px solid #333; }}
44
+ #imbalance-vol {{ width: 50%; border-right: 1px solid #333; }}
45
+ #imbalance-ratio {{ width: 50%; }}
46
+
47
+ /* Row 3: History (25%) */
48
+ #row-bot {{ flex: 1; width: 100%; }}
49
+
50
+ /* Charts fill their containers */
51
+ .chart {{ width: 100%; height: 100%; }}
52
 
53
  #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; }}
54
  .green {{ color: #00e676; }}
 
58
  <body>
59
  <div id="status">Connecting...</div>
60
 
61
+ <div id="container">
62
+ <div id="row-top">
63
+ <div id="depth-chart" class="chart"></div>
64
+ </div>
65
+ <div id="row-mid">
66
+ <div id="imbalance-vol" class="chart"></div>
67
+ <div id="imbalance-ratio" class="chart"></div>
68
+ </div>
69
+ <div id="row-bot">
70
+ <div id="price-chart" class="chart"></div>
71
+ </div>
72
+ </div>
73
 
74
  <script>
75
  const depthDiv = document.getElementById('depth-chart');
76
+ const volDiv = document.getElementById('imbalance-vol');
77
+ const ratioDiv = document.getElementById('imbalance-ratio');
78
  const priceDiv = document.getElementById('price-chart');
79
  const statusDiv = document.getElementById('status');
80
 
81
  let initDepth = false;
82
+ let initVol = false;
83
+ let initRatio = false;
84
  let initPrice = false;
85
 
86
  const commonConfig = {{ responsive: true, displayModeBar: false }};
 
88
  paper_bgcolor: '#0e0e0e',
89
  plot_bgcolor: '#0e0e0e',
90
  font: {{ color: '#aaa' }},
91
+ margin: {{ t: 30, b: 30, l: 50, r: 20 }},
92
+ showlegend: false,
93
+ xaxis: {{ gridcolor: '#222' }},
94
+ yaxis: {{ gridcolor: '#222' }}
95
  }};
96
 
97
  async function updateCharts() {{
 
104
  return;
105
  }}
106
 
107
+ statusDiv.innerHTML = `Mid: <span class="${{data.mid >= data.prev_mid ? 'green' : 'red'}}">$${{data.mid.toLocaleString(undefined, {{minimumFractionDigits: 2}})}}</span> | Ratio: ${{data.ratio.last_val ? data.ratio.last_val.toFixed(2) : '-'}}`;
108
+
109
+ // 1. DEPTH CHART
110
+ if (!initDepth) {{
111
+ Plotly.newPlot(depthDiv, [
112
+ {{ x: data.depth.bids_x, y: data.depth.bids_y, fill: 'tozeroy', type: 'scatter', name: 'Bids', line: {{color: '#00e676'}} }},
113
+ {{ x: data.depth.asks_x, y: data.depth.asks_y, fill: 'tozeroy', type: 'scatter', name: 'Asks', line: {{color: '#ff1744'}} }}
114
+ ], {{ ...commonLayout, title: 'Orderbook Depth' }}, commonConfig);
115
+ initDepth = true;
116
+ }} else {{
117
+ Plotly.react(depthDiv, [
118
+ {{ x: data.depth.bids_x, y: data.depth.bids_y, fill: 'tozeroy', type: 'scatter', name: 'Bids', line: {{color: '#00e676'}} }},
119
+ {{ x: data.depth.asks_x, y: data.depth.asks_y, fill: 'tozeroy', type: 'scatter', name: 'Asks', line: {{color: '#ff1744'}} }}
120
+ ], {{ ...commonLayout, title: 'Orderbook Depth' }}, commonConfig);
121
+ }}
 
 
 
 
 
 
 
 
 
122
 
123
+ // 2. IMBALANCE VOL
124
+ const layoutVol = {{ ...commonLayout, title: 'Liquidity (Volume vs Distance)', xaxis: {{title: 'Distance ($)', gridcolor:'#222'}} }};
125
+ if (!initVol) {{
126
+ Plotly.newPlot(volDiv, [
127
+ {{ x: data.imbalance.dist_bids, y: data.imbalance.vol_bids, type: 'scatter', name: 'Bid Vol', line: {{color: '#00e676'}} }},
128
+ {{ x: data.imbalance.dist_asks, y: data.imbalance.vol_asks, type: 'scatter', name: 'Ask Vol', line: {{color: '#ff1744'}} }}
129
+ ], layoutVol, commonConfig);
130
+ initVol = true;
131
+ }} else {{
132
+ Plotly.react(volDiv, [
133
+ {{ x: data.imbalance.dist_bids, y: data.imbalance.vol_bids, type: 'scatter', name: 'Bid Vol', line: {{color: '#00e676'}} }},
134
+ {{ x: data.imbalance.dist_asks, y: data.imbalance.vol_asks, type: 'scatter', name: 'Ask Vol', line: {{color: '#ff1744'}} }}
135
+ ], layoutVol, commonConfig);
136
+ }}
137
 
138
+ // 3. IMBALANCE RATIO
139
+ const layoutRatio = {{ ...commonLayout,
140
+ title: 'Buy/Sell Ratio (Bid/Ask)',
141
+ xaxis: {{title: 'Distance ($)', gridcolor:'#222'}},
142
+ shapes: [{{ type: 'line', x0: 0, x1: 1, xref: 'paper', y0: 1, y1: 1, line: {{color: '#666', width: 1, dash:'dot'}} }}]
 
 
 
 
 
 
 
143
  }};
144
 
145
+ // Color the line based on if it's mostly bullish or bearish?
146
+ // We'll just use a neutral color or gradient. Let's use Cyan.
147
+ if (!initRatio) {{
148
+ Plotly.newPlot(ratioDiv, [
149
+ {{ x: data.ratio.x, y: data.ratio.y, type: 'scatter', name: 'Ratio', line: {{color: '#00bcd4', width:2}} }}
150
+ ], layoutRatio, commonConfig);
151
+ initRatio = true;
152
+ }} else {{
153
+ Plotly.react(ratioDiv, [
154
+ {{ x: data.ratio.x, y: data.ratio.y, type: 'scatter', name: 'Ratio', line: {{color: '#00bcd4', width:2}} }}
155
+ ], layoutRatio, commonConfig);
156
+ }}
157
 
158
+ // 4. HISTORY
 
 
159
  const timeData = data.history.map(d => new Date(d.t * 1000));
160
  const priceData = data.history.map(d => d.p);
161
+ const layoutHist = {{ ...commonLayout, title: 'Midprice History', xaxis: {{type:'date', gridcolor:'#222'}} }};
162
+
163
+ if (!initPrice) {{
164
+ Plotly.newPlot(priceDiv, [
165
+ {{ x: timeData, y: priceData, type: 'scatter', name: 'Mid', line: {{color: '#29b6f6'}} }}
166
+ ], layoutHist, commonConfig);
167
+ initPrice = true;
168
+ }} else {{
169
+ Plotly.react(priceDiv, [
170
+ {{ x: timeData, y: priceData, type: 'scatter', name: 'Mid', line: {{color: '#29b6f6'}} }}
171
+ ], layoutHist, commonConfig);
172
+ }}
 
173
 
174
  }} catch (e) {{ console.error("Fetch error:", e); }}
175
  }}
 
186
  try:
187
  async with websockets.connect("wss://ws.kraken.com/v2") as ws:
188
  logging.info(f"🔌 Connected to Kraken ({SYMBOL_KRAKEN})")
 
189
  await ws.send(json.dumps({
190
  "method": "subscribe",
191
  "params": {"channel": "book", "symbol": [SYMBOL_KRAKEN], "depth": 500}
 
202
  q, p = float(bid['qty']), float(bid['price'])
203
  if q == 0: market_state['bids'].pop(p, None)
204
  else: market_state['bids'][p] = q
 
205
  for ask in item.get('asks', []):
206
  q, p = float(ask['qty']), float(ask['price'])
207
  if q == 0: market_state['asks'].pop(p, None)
 
234
  mid = market_state['current_mid']
235
 
236
  # --- Prepare Raw Data ---
237
+ raw_bids = sorted(market_state['bids'].items(), key=lambda x: -x[0])[:300]
238
+ raw_asks = sorted(market_state['asks'].items(), key=lambda x: x[0])[:300]
 
 
239
 
240
+ # --- 1. Depth (Price) ---
 
 
 
 
 
241
  b_x, b_y = [], []
242
  cum = 0
243
  for p, q in raw_bids:
244
  cum += q
245
  b_x.append(p)
246
  b_y.append(cum)
247
+ b_x.reverse(); b_y.reverse()
 
 
248
 
249
  a_x, a_y = [], []
250
  cum = 0
 
253
  a_x.append(p)
254
  a_y.append(cum)
255
 
256
+ # --- 2. Distance Arrays ---
257
+ dist_bids = [(mid - p, q) for p, q in raw_bids if mid - p >= 0]
 
 
 
 
 
 
 
 
258
  d_b_x, d_b_y = [], []
259
  cum_d = 0
260
+ for d, q in dist_bids:
261
  cum_d += q
262
  d_b_x.append(d)
263
  d_b_y.append(cum_d)
264
 
265
+ dist_asks = [(p - mid, q) for p, q in raw_asks if p - mid >= 0]
 
 
 
 
 
266
  d_a_x, d_a_y = [], []
267
  cum_d = 0
268
+ for d, q in dist_asks:
269
  cum_d += q
270
  d_a_x.append(d)
271
  d_a_y.append(cum_d)
272
 
273
+ # --- 3. Ratio Calculation (Interp) ---
274
+ # We need a common distance grid to compute Ratio = B_vol / A_vol
275
+ r_x, r_y = [], []
276
+ if d_b_x and d_a_x:
277
+ max_dist = min(d_b_x[-1], d_a_x[-1])
278
+ # Generate 100 steps
279
+ step_size = max_dist / 100
280
+ steps = [i * step_size for i in range(1, 101)]
281
+
282
+ for s in steps:
283
+ # Find B vol at distance s
284
+ idx_b = bisect.bisect_right(d_b_x, s)
285
+ vol_b = d_b_y[idx_b-1] if idx_b > 0 else 0
286
+
287
+ # Find A vol at distance s
288
+ idx_a = bisect.bisect_right(d_a_x, s)
289
+ vol_a = d_a_y[idx_a-1] if idx_a > 0 else 0
290
+
291
+ if vol_a > 0:
292
+ ratio = vol_b / vol_a
293
+ # Cap extreme ratios for display sanity
294
+ if ratio > 5: ratio = 5
295
+ r_x.append(s)
296
+ r_y.append(ratio)
297
+
298
  return web.json_response({
299
  "mid": mid,
300
  "prev_mid": market_state['history'][-2]['p'] if len(market_state['history']) > 1 else mid,
 
 
 
301
  "depth": { "bids_x": b_x, "bids_y": b_y, "asks_x": a_x, "asks_y": a_y },
302
+ "imbalance": { "dist_bids": d_b_x, "vol_bids": d_b_y, "dist_asks": d_a_x, "vol_asks": d_a_y },
303
+ "ratio": { "x": r_x, "y": r_y, "last_val": r_y[-1] if r_y else 0 },
 
 
 
 
304
  "history": market_state['history']
305
  })
306
 
307
  async def start_background(app):
 
308
  app['kraken_task'] = asyncio.create_task(kraken_worker())
309
 
310
  async def cleanup_background(app):
 
311
  app['kraken_task'].cancel()
312
+ try: await app['kraken_task']
313
+ except asyncio.CancelledError: pass
 
 
314
 
315
  async def main():
316
  app = web.Application()
317
  app.router.add_get('/', handle_index)
318
  app.router.add_get('/data', handle_data)
 
 
319
  app.on_startup.append(start_background)
320
  app.on_cleanup.append(cleanup_background)
321
 
 
324
  site = web.TCPSite(runner, '0.0.0.0', PORT)
325
  await site.start()
326
 
327
+ print(f"🚀 BTC-USD 4-Way Dashboard: http://localhost:{PORT}")
328
  await asyncio.Event().wait()
329
 
330
  if __name__ == "__main__":