Alvin3y1 commited on
Commit
0dee7c2
·
verified ·
1 Parent(s): 8b57377

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +270 -620
app.py CHANGED
@@ -11,7 +11,7 @@ from sklearn.ensemble import RandomForestRegressor
11
  SYMBOL_KRAKEN = "BTC/USD"
12
  PORT = 7860
13
  BROADCAST_RATE = 1.0
14
- PREDICTION_HORIZON = 100
15
 
16
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(message)s')
17
 
@@ -36,15 +36,11 @@ def calculate_indicators(candles):
36
  df[c] = df[c].astype(float)
37
 
38
  df['ema'] = df['close'].ewm(span=20, adjust=False).mean()
39
- df['ema_fast'] = df['close'].ewm(span=9, adjust=False).mean()
40
- df['ema_slow'] = df['close'].ewm(span=50, adjust=False).mean()
41
-
42
  df['sma20'] = df['close'].rolling(window=20).mean()
43
  df['std'] = df['close'].rolling(window=20).std()
44
  df['bb_upper'] = df['sma20'] + (df['std'] * 2)
45
  df['bb_lower'] = df['sma20'] - (df['std'] * 2)
46
- df['bb_mid'] = df['sma20']
47
-
48
  delta = df['close'].diff()
49
  gain = (delta.where(delta > 0, 0)).rolling(window=14).mean()
50
  loss = (-delta.where(delta < 0, 0)).rolling(window=14).mean()
@@ -60,26 +56,20 @@ def calculate_indicators(candles):
60
  low_min = df['low'].rolling(window=14).min()
61
  high_max = df['high'].rolling(window=14).max()
62
  df['stoch_k'] = 100 * ((df['close'] - low_min) / (high_max - low_min))
63
- df['stoch_d'] = df['stoch_k'].rolling(window=3).mean()
64
-
65
  df['tr0'] = abs(df['high'] - df['low'])
66
  df['tr1'] = abs(df['high'] - df['close'].shift())
67
  df['tr2'] = abs(df['low'] - df['close'].shift())
68
  df['tr'] = df[['tr0', 'tr1', 'tr2']].max(axis=1)
69
  df['atr'] = df['tr'].rolling(window=14).mean()
70
 
71
- df['obv'] = (np.sign(df['close'].diff()) * df['volume']).fillna(0).cumsum()
72
-
73
  df['tp'] = (df['high'] + df['low'] + df['close']) / 3
74
  df['vwap'] = (df['tp'] * df['volume']).cumsum() / df['volume'].cumsum()
75
 
76
  return df
77
 
78
  def train_model(df):
79
- logging.info("Training ML Model...")
80
-
81
- feature_cols = ['close', 'ema', 'bb_upper', 'bb_lower', 'rsi', 'macd', 'stoch_k', 'atr', 'obv', 'vwap']
82
-
83
  data = df.dropna().copy()
84
 
85
  future_shifts = {}
@@ -95,23 +85,20 @@ def train_model(df):
95
  data = data.dropna()
96
 
97
  if len(data) < 100:
98
- logging.warning("Not enough data to train model yet.")
99
  return None
100
 
101
  X = data[feature_cols].values
102
  y = data[targets].values
103
 
104
- model = RandomForestRegressor(n_estimators=50, max_depth=10, n_jobs=-1, random_state=42)
105
  model.fit(X, y)
106
-
107
- logging.info(f"Model Trained on {len(X)} samples.")
108
  return model
109
 
110
  def get_prediction(df, model):
111
  if model is None:
112
  return []
113
 
114
- feature_cols = ['close', 'ema', 'bb_upper', 'bb_lower', 'rsi', 'macd', 'stoch_k', 'atr', 'obv', 'vwap']
115
  last_row = df.iloc[[-1]][feature_cols]
116
 
117
  if last_row.isnull().values.any():
@@ -147,21 +134,21 @@ def process_market_data():
147
  predictions = []
148
  try:
149
  predictions = get_prediction(df, market_state['model'])
150
- except Exception as e:
151
- logging.error(f"Prediction failed: {e}")
152
 
153
  df_clean = df.replace([np.inf, -np.inf], np.nan)
 
154
 
155
- last_close = float(df_clean['close'].iloc[-1]) if len(df_clean) > 0 else 0
156
- first_close = float(df_clean['close'].iloc[0]) if len(df_clean) > 0 else 0
157
  price_change = ((last_close - first_close) / first_close * 100) if first_close > 0 else 0
158
 
159
  market_state['last_price'] = last_close
160
  market_state['price_change'] = price_change
161
 
162
- full_data = df_clean.where(pd.notnull(df_clean), None).to_dict('records')
163
-
164
- last_row = df_clean.iloc[-1] if len(df_clean) > 0 else {}
165
 
166
  return {
167
  "data": full_data,
@@ -169,10 +156,10 @@ def process_market_data():
169
  "stats": {
170
  "price": last_close,
171
  "change": round(price_change, 2),
172
- "rsi": round(float(last_row.get('rsi', 0)), 1) if pd.notna(last_row.get('rsi')) else 0,
173
- "macd": round(float(last_row.get('macd', 0)), 2) if pd.notna(last_row.get('macd')) else 0,
174
- "atr": round(float(last_row.get('atr', 0)), 2) if pd.notna(last_row.get('atr')) else 0,
175
- "volume": round(float(last_row.get('volume', 0)), 2) if pd.notna(last_row.get('volume')) else 0
176
  }
177
  }
178
 
@@ -182,585 +169,296 @@ HTML_PAGE = """
182
  <head>
183
  <meta charset="UTF-8">
184
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
185
- <title>BTC/USD AI Predictor</title>
186
  <script src="https://unpkg.com/lightweight-charts@4.1.1/dist/lightweight-charts.standalone.production.js"></script>
187
- <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
188
  <style>
 
 
 
 
 
 
 
 
 
 
 
189
  * { margin: 0; padding: 0; box-sizing: border-box; }
190
-
191
  body {
192
- font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
193
- background: linear-gradient(135deg, #0a0a0f 0%, #1a1a2e 100%);
194
- color: #ffffff;
195
  height: 100vh;
196
- display: flex;
197
- flex-direction: column;
198
  overflow: hidden;
 
 
199
  }
200
-
201
  .header {
202
- background: rgba(15, 15, 25, 0.95);
203
- backdrop-filter: blur(20px);
204
- border-bottom: 1px solid rgba(255, 255, 255, 0.05);
205
- padding: 12px 24px;
206
  display: flex;
207
  align-items: center;
208
  justify-content: space-between;
209
- z-index: 100;
 
210
  }
211
-
212
- .logo-section {
213
  display: flex;
214
  align-items: center;
215
- gap: 16px;
216
- }
217
-
218
- .logo {
219
- font-size: 24px;
220
- font-weight: 700;
221
- background: linear-gradient(135deg, #00ff88 0%, #00d4ff 100%);
222
- -webkit-background-clip: text;
223
- -webkit-text-fill-color: transparent;
224
- letter-spacing: -0.5px;
225
- }
226
-
227
- .symbol-badge {
228
- background: rgba(0, 255, 136, 0.1);
229
- border: 1px solid rgba(0, 255, 136, 0.3);
230
- padding: 6px 14px;
231
- border-radius: 20px;
232
- font-size: 13px;
233
- font-weight: 600;
234
- color: #00ff88;
235
- }
236
-
237
- .stats-row {
238
- display: flex;
239
- gap: 24px;
240
- align-items: center;
241
- }
242
-
243
- .stat-item {
244
- display: flex;
245
- flex-direction: column;
246
- align-items: flex-end;
247
- }
248
-
249
- .stat-label {
250
- font-size: 10px;
251
- color: #666;
252
- text-transform: uppercase;
253
- letter-spacing: 0.5px;
254
  }
255
-
256
- .stat-value {
257
- font-size: 15px;
 
 
 
 
258
  font-weight: 600;
259
- font-variant-numeric: tabular-nums;
260
- }
261
-
262
- .stat-value.positive { color: #00ff88; }
263
- .stat-value.negative { color: #ff4757; }
264
- .stat-value.neutral { color: #ffd700; }
265
-
266
- .status-indicator {
267
- display: flex;
268
- align-items: center;
269
- gap: 8px;
270
- font-size: 12px;
271
- color: #888;
272
- }
273
-
274
- .status-dot {
275
- width: 8px;
276
- height: 8px;
277
- border-radius: 50%;
278
- background: #00ff88;
279
- animation: pulse 2s infinite;
280
- }
281
-
282
- .status-dot.disconnected {
283
- background: #ff4757;
284
- animation: none;
285
- }
286
-
287
- @keyframes pulse {
288
- 0%, 100% { opacity: 1; box-shadow: 0 0 0 0 rgba(0, 255, 136, 0.4); }
289
- 50% { opacity: 0.8; box-shadow: 0 0 0 8px rgba(0, 255, 136, 0); }
290
- }
291
-
292
- .indicator-panel {
293
- background: rgba(15, 15, 25, 0.8);
294
- border-bottom: 1px solid rgba(255, 255, 255, 0.05);
295
- padding: 10px 24px;
296
- display: flex;
297
- gap: 32px;
298
- overflow-x: auto;
299
- }
300
-
301
- .indicator-group {
302
  display: flex;
303
  align-items: center;
304
- gap: 12px;
305
- }
306
-
307
- .indicator-label {
308
- font-size: 11px;
309
- color: #666;
310
- text-transform: uppercase;
311
  }
 
 
312
 
313
- .indicator-value {
314
- font-size: 13px;
315
- font-weight: 500;
316
- font-variant-numeric: tabular-nums;
 
317
  }
318
-
319
- .charts-container {
320
- flex: 1;
 
321
  display: flex;
322
  flex-direction: column;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
323
  position: relative;
324
  }
325
-
326
- .chart-wrapper {
327
- position: relative;
328
- border-bottom: 1px solid rgba(255, 255, 255, 0.05);
329
- }
330
-
331
- #main-chart { flex: 5; }
332
- #volume-chart { flex: 1; min-height: 60px; }
333
- #osc-chart { flex: 1.5; min-height: 80px; }
334
-
335
- .chart-label {
336
  position: absolute;
337
- top: 12px;
338
- left: 16px;
339
- z-index: 10;
340
  display: flex;
341
- gap: 16px;
342
- font-size: 11px;
 
343
  pointer-events: none;
344
  }
 
 
345
 
346
- .chart-label span {
347
- display: flex;
348
- align-items: center;
349
- gap: 6px;
350
- }
351
-
352
- .chart-label .dot {
353
- width: 8px;
354
- height: 8px;
355
- border-radius: 50%;
356
- }
357
-
358
- .loading-overlay {
359
  position: absolute;
360
- top: 0;
361
- left: 0;
362
- right: 0;
363
- bottom: 0;
364
- background: rgba(10, 10, 15, 0.95);
365
  display: flex;
366
  flex-direction: column;
367
  align-items: center;
368
  justify-content: center;
369
- z-index: 1000;
370
- transition: opacity 0.5s ease;
371
- }
372
-
373
- .loading-overlay.hidden {
374
- opacity: 0;
375
- pointer-events: none;
376
- }
377
-
378
- .loader {
379
- width: 50px;
380
- height: 50px;
381
- border: 3px solid rgba(0, 255, 136, 0.1);
382
- border-top-color: #00ff88;
383
  border-radius: 50%;
384
  animation: spin 1s linear infinite;
385
  }
 
 
386
 
387
- @keyframes spin {
388
- to { transform: rotate(360deg); }
389
- }
390
-
391
- .loading-text {
392
- margin-top: 20px;
393
- font-size: 14px;
394
- color: #666;
395
- }
396
-
397
- .prediction-badge {
398
- position: absolute;
399
- top: 12px;
400
- right: 16px;
401
- background: rgba(191, 90, 242, 0.15);
402
- border: 1px solid rgba(191, 90, 242, 0.3);
403
  padding: 4px 10px;
404
  border-radius: 12px;
405
- font-size: 10px;
406
- color: #bf5af2;
407
- z-index: 10;
408
  }
409
  </style>
410
  </head>
411
  <body>
412
  <div class="header">
413
- <div class="logo-section">
414
- <div class="logo">QuantAI</div>
415
- <div class="symbol-badge">BTC/USD</div>
416
- </div>
417
-
418
- <div class="stats-row">
419
- <div class="stat-item">
420
- <span class="stat-label">Price</span>
421
- <span id="price" class="stat-value">$--</span>
422
- </div>
423
- <div class="stat-item">
424
- <span class="stat-label">Change</span>
425
- <span id="change" class="stat-value neutral">--%</span>
426
- </div>
427
- <div class="stat-item">
428
- <span class="stat-label">RSI</span>
429
- <span id="rsi" class="stat-value">--</span>
430
- </div>
431
- <div class="stat-item">
432
- <span class="stat-label">ATR</span>
433
- <span id="atr" class="stat-value">--</span>
434
- </div>
435
  </div>
436
-
437
- <div class="status-indicator">
438
- <div id="status-dot" class="status-dot"></div>
439
- <span id="status-text">Connecting...</span>
440
  </div>
441
  </div>
442
 
443
- <div class="indicator-panel">
444
- <div class="indicator-group">
445
- <span class="indicator-label">EMA 20</span>
446
- <span id="ema-val" class="indicator-value" style="color: #2962FF">--</span>
447
- </div>
448
- <div class="indicator-group">
449
- <span class="indicator-label">BB Upper</span>
450
- <span id="bb-upper" class="indicator-value" style="color: #26a69a">--</span>
451
- </div>
452
- <div class="indicator-group">
453
- <span class="indicator-label">BB Lower</span>
454
- <span id="bb-lower" class="indicator-value" style="color: #ef5350">--</span>
455
- </div>
456
- <div class="indicator-group">
457
- <span class="indicator-label">MACD</span>
458
- <span id="macd-val" class="indicator-value">--</span>
459
- </div>
460
- <div class="indicator-group">
461
- <span class="indicator-label">Stoch K</span>
462
- <span id="stoch-val" class="indicator-value" style="color: #ff9800">--</span>
463
- </div>
464
- <div class="indicator-group">
465
- <span class="indicator-label">Volume</span>
466
- <span id="vol-val" class="indicator-value" style="color: #888">--</span>
467
- </div>
468
- </div>
469
-
470
- <div class="charts-container">
471
- <div class="loading-overlay" id="loading">
472
- <div class="loader"></div>
473
- <div class="loading-text">Loading market data...</div>
474
- </div>
475
-
476
- <div id="main-chart" class="chart-wrapper">
477
- <div class="chart-label">
478
- <span><div class="dot" style="background: #00ff88"></div>Price</span>
479
- <span><div class="dot" style="background: #2962FF"></div>EMA 20</span>
480
- <span><div class="dot" style="background: #26a69a; opacity: 0.5"></div>Bollinger</span>
481
  </div>
482
- <div class="prediction-badge">AI Forecast: 100 candles</div>
483
- </div>
484
-
485
- <div id="volume-chart" class="chart-wrapper">
486
- <div class="chart-label">
487
- <span><div class="dot" style="background: #5c6bc0"></div>Volume</span>
 
 
 
 
 
 
 
 
 
488
  </div>
489
  </div>
490
 
491
- <div id="osc-chart" class="chart-wrapper">
492
- <div class="chart-label">
493
- <span><div class="dot" style="background: #9C27B0"></div>RSI</span>
494
- <span><div class="dot" style="background: #26a69a"></div>MACD Hist</span>
 
 
 
 
 
 
 
 
495
  </div>
 
496
  </div>
497
  </div>
498
 
499
  <script>
500
- document.addEventListener('DOMContentLoaded', () => {
501
- const mainEl = document.getElementById('main-chart');
502
- const volEl = document.getElementById('volume-chart');
503
- const oscEl = document.getElementById('osc-chart');
504
- const loading = document.getElementById('loading');
505
-
506
- const chartOptions = {
507
- layout: {
508
- background: { type: 'solid', color: 'transparent' },
509
- textColor: '#666'
510
- },
511
- grid: {
512
- vertLines: { color: 'rgba(255,255,255,0.03)' },
513
- horzLines: { color: 'rgba(255,255,255,0.03)' }
514
- },
515
- timeScale: {
516
- timeVisible: true,
517
- secondsVisible: false,
518
- borderColor: 'rgba(255,255,255,0.1)'
519
- },
520
- rightPriceScale: {
521
- borderColor: 'rgba(255,255,255,0.1)'
522
- },
523
- crosshair: {
524
- mode: LightweightCharts.CrosshairMode.Normal,
525
- vertLine: {
526
- color: 'rgba(255,255,255,0.2)',
527
- labelBackgroundColor: '#1a1a2e'
528
- },
529
- horzLine: {
530
- color: 'rgba(255,255,255,0.2)',
531
- labelBackgroundColor: '#1a1a2e'
532
- }
533
- }
534
  };
535
-
536
- const mainChart = LightweightCharts.createChart(mainEl, chartOptions);
537
- const volChart = LightweightCharts.createChart(volEl, chartOptions);
538
- const oscChart = LightweightCharts.createChart(oscEl, chartOptions);
539
-
540
- const candles = mainChart.addCandlestickSeries({
541
- upColor: '#00ff88',
542
- downColor: '#ff4757',
543
- borderUpColor: '#00ff88',
544
- borderDownColor: '#ff4757',
545
- wickUpColor: '#00ff88',
546
- wickDownColor: '#ff4757'
547
- });
548
-
549
- const ema = mainChart.addLineSeries({
550
- color: '#2962FF',
551
- lineWidth: 2,
552
- crosshairMarkerVisible: false
553
- });
554
-
555
- const bbUpper = mainChart.addLineSeries({
556
- color: 'rgba(38, 166, 154, 0.4)',
557
- lineWidth: 1,
558
- crosshairMarkerVisible: false
559
- });
560
-
561
- const bbLower = mainChart.addLineSeries({
562
- color: 'rgba(239, 83, 80, 0.4)',
563
- lineWidth: 1,
564
- crosshairMarkerVisible: false
565
- });
566
-
567
- const predLine = mainChart.addLineSeries({
568
- color: '#bf5af2',
569
- lineWidth: 2,
570
- lineStyle: LightweightCharts.LineStyle.Dashed,
571
- crosshairMarkerVisible: false
572
- });
573
-
574
- const volumeSeries = volChart.addHistogramSeries({
575
- priceFormat: { type: 'volume' },
576
- priceScaleId: ''
577
- });
578
- volChart.priceScale('').applyOptions({
579
- scaleMargins: { top: 0.1, bottom: 0 }
580
- });
581
-
582
- const rsi = oscChart.addLineSeries({
583
- color: '#9C27B0',
584
- lineWidth: 2,
585
- priceScaleId: 'rsi'
586
- });
587
- oscChart.priceScale('rsi').applyOptions({
588
- scaleMargins: { top: 0.1, bottom: 0.1 }
589
- });
590
-
591
- const macdHist = oscChart.addHistogramSeries({
592
- priceScaleId: 'macd'
593
- });
594
- oscChart.priceScale('macd').applyOptions({
595
- scaleMargins: { top: 0.6, bottom: 0 }
596
  });
597
-
598
- function resizeCharts() {
599
- const mainH = mainEl.clientHeight;
600
- const volH = volEl.clientHeight;
601
- const oscH = oscEl.clientHeight;
602
- const w = mainEl.clientWidth;
603
-
604
- mainChart.applyOptions({ width: w, height: mainH });
605
- volChart.applyOptions({ width: w, height: volH });
606
- oscChart.applyOptions({ width: w, height: oscH });
607
- }
608
-
609
- new ResizeObserver(resizeCharts).observe(document.body);
610
- setTimeout(resizeCharts, 100);
611
-
612
- function syncTimeScales(charts) {
613
- charts.forEach((chart, i) => {
614
- chart.timeScale().subscribeVisibleLogicalRangeChange(range => {
615
- if (range) {
616
- charts.forEach((c, j) => {
617
- if (i !== j) c.timeScale().setVisibleLogicalRange(range);
618
- });
619
- }
620
- });
621
- });
622
- }
623
- syncTimeScales([mainChart, volChart, oscChart]);
624
-
625
- function updateStats(stats, lastData) {
626
- if (stats) {
627
- document.getElementById('price').textContent = '$' + stats.price.toLocaleString('en-US', {minimumFractionDigits: 2, maximumFractionDigits: 2});
628
-
629
- const changeEl = document.getElementById('change');
630
- changeEl.textContent = (stats.change >= 0 ? '+' : '') + stats.change + '%';
631
- changeEl.className = 'stat-value ' + (stats.change > 0 ? 'positive' : stats.change < 0 ? 'negative' : 'neutral');
632
-
633
- const rsiVal = stats.rsi;
634
- const rsiEl = document.getElementById('rsi');
635
- rsiEl.textContent = rsiVal;
636
- rsiEl.className = 'stat-value ' + (rsiVal > 70 ? 'negative' : rsiVal < 30 ? 'positive' : 'neutral');
637
-
638
- document.getElementById('atr').textContent = stats.atr;
639
- }
640
-
641
- if (lastData) {
642
- document.getElementById('ema-val').textContent = lastData.ema ? lastData.ema.toFixed(2) : '--';
643
- document.getElementById('bb-upper').textContent = lastData.bb_upper ? lastData.bb_upper.toFixed(2) : '--';
644
- document.getElementById('bb-lower').textContent = lastData.bb_lower ? lastData.bb_lower.toFixed(2) : '--';
645
-
646
- const macdVal = lastData.macd;
647
- const macdEl = document.getElementById('macd-val');
648
- if (macdVal !== null && macdVal !== undefined) {
649
- macdEl.textContent = macdVal.toFixed(2);
650
- macdEl.style.color = macdVal >= 0 ? '#26a69a' : '#ef5350';
651
- }
652
-
653
- document.getElementById('stoch-val').textContent = lastData.stoch_k ? lastData.stoch_k.toFixed(1) : '--';
654
- document.getElementById('vol-val').textContent = lastData.volume ? lastData.volume.toFixed(2) : '--';
655
- }
656
- }
657
-
658
- function setStatus(connected) {
659
- const dot = document.getElementById('status-dot');
660
- const text = document.getElementById('status-text');
661
- if (connected) {
662
- dot.className = 'status-dot';
663
- text.textContent = 'Live';
664
- } else {
665
- dot.className = 'status-dot disconnected';
666
- text.textContent = 'Reconnecting...';
667
- }
668
  }
669
-
670
- let hasData = false;
671
-
 
 
 
 
672
  function connect() {
673
- const protocol = location.protocol === 'https:' ? 'wss' : 'ws';
674
- const ws = new WebSocket(protocol + '://' + location.host + '/ws');
675
 
676
- ws.onopen = () => setStatus(true);
 
 
 
 
 
 
 
 
 
677
 
678
- ws.onmessage = (e) => {
679
  try {
680
- const payload = JSON.parse(e.data);
681
- if (!payload.data || payload.data.length === 0) return;
682
 
683
- const d = payload.data;
 
684
 
685
- const safeMap = (arr, key) => arr
686
- .filter(x => x && x.time && x[key] !== null && x[key] !== undefined && !isNaN(x[key]))
687
- .map(x => ({ time: x.time, value: x[key] }));
688
-
689
- const candleData = d
690
- .filter(x => x && x.time && x.open && x.high && x.low && x.close)
691
- .map(x => ({
692
- time: x.time,
693
- open: x.open,
694
- high: x.high,
695
- low: x.low,
696
- close: x.close
697
- }));
698
-
699
- if (candleData.length > 0) {
700
- candles.setData(candleData);
701
-
702
- const emaData = safeMap(d, 'ema');
703
- if (emaData.length > 0) ema.setData(emaData);
704
-
705
- const bbUpperData = safeMap(d, 'bb_upper');
706
- if (bbUpperData.length > 0) bbUpper.setData(bbUpperData);
707
-
708
- const bbLowerData = safeMap(d, 'bb_lower');
709
- if (bbLowerData.length > 0) bbLower.setData(bbLowerData);
710
-
711
- const volData = d
712
- .filter(x => x && x.time && x.volume !== null && x.volume !== undefined)
713
- .map(x => ({
714
- time: x.time,
715
- value: x.volume,
716
- color: x.close >= x.open ? 'rgba(0, 255, 136, 0.5)' : 'rgba(255, 71, 87, 0.5)'
717
- }));
718
- if (volData.length > 0) volumeSeries.setData(volData);
719
-
720
- const rsiData = safeMap(d, 'rsi');
721
- if (rsiData.length > 0) rsi.setData(rsiData);
722
-
723
- const macdData = d
724
- .filter(x => x && x.time && x.macd_hist !== null && x.macd_hist !== undefined && !isNaN(x.macd_hist))
725
- .map(x => ({
726
- time: x.time,
727
- value: x.macd_hist,
728
- color: x.macd_hist >= 0 ? '#26a69a' : '#ef5350'
729
- }));
730
- if (macdData.length > 0) macdHist.setData(macdData);
731
-
732
- if (payload.prediction && payload.prediction.length > 0) {
733
- const lastCandle = candleData[candleData.length - 1];
734
- const predData = [
735
- { time: lastCandle.time, value: lastCandle.close },
736
- ...payload.prediction.filter(p => p && p.time && p.value !== null && !isNaN(p.value))
737
- ];
738
- predLine.setData(predData);
739
- }
740
-
741
- updateStats(payload.stats, d[d.length - 1]);
742
-
743
- if (!hasData) {
744
- hasData = true;
745
- loading.classList.add('hidden');
746
- mainChart.timeScale().fitContent();
747
- }
748
  }
749
- } catch (err) {
750
- console.error("Chart error:", err);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
751
  }
752
  };
753
-
754
- ws.onclose = () => {
755
- setStatus(false);
756
- setTimeout(connect, 2000);
757
- };
758
-
759
- ws.onerror = () => ws.close();
760
  }
761
 
762
  connect();
763
- });
764
  </script>
765
  </body>
766
  </html>
@@ -770,7 +468,7 @@ async def fetch_initial_data():
770
  try:
771
  async with aiohttp.ClientSession() as session:
772
  url = "https://api.kraken.com/0/public/OHLC?pair=XBTUSD&interval=1"
773
- async with session.get(url, timeout=aiohttp.ClientTimeout(total=30)) as response:
774
  if response.status == 200:
775
  data = await response.json()
776
  if 'result' in data:
@@ -778,131 +476,83 @@ async def fetch_initial_data():
778
  if key != 'last':
779
  raw = data['result'][key]
780
  market_state['ohlc_history'] = [
781
- {
782
- 'time': int(c[0]),
783
- 'open': float(c[1]),
784
- 'high': float(c[2]),
785
- 'low': float(c[3]),
786
- 'close': float(c[4]),
787
- 'volume': float(c[6])
788
- }
789
  for c in raw[-720:]
790
  ]
791
  market_state['ready'] = True
792
- logging.info(f"Loaded {len(market_state['ohlc_history'])} initial candles")
793
- return True
794
- except Exception as e:
795
- logging.error(f"Initial data fetch error: {e}")
796
- return False
797
 
798
- async def kraken_rest_worker():
799
  await fetch_initial_data()
800
-
801
  while True:
802
  try:
803
  async with aiohttp.ClientSession() as session:
804
  url = "https://api.kraken.com/0/public/OHLC?pair=XBTUSD&interval=1"
805
- async with session.get(url, timeout=aiohttp.ClientTimeout(total=30)) as response:
806
  if response.status == 200:
807
  data = await response.json()
808
- if 'result' in data:
809
- for key in data['result']:
810
- if key != 'last':
811
- raw = data['result'][key]
812
- new_candles = [
813
- {
814
- 'time': int(c[0]),
815
- 'open': float(c[1]),
816
- 'high': float(c[2]),
817
- 'low': float(c[3]),
818
- 'close': float(c[4]),
819
- 'volume': float(c[6])
820
- }
821
- for c in raw[-10:]
822
- ]
823
-
824
- if market_state['ohlc_history']:
825
- existing_times = {c['time'] for c in market_state['ohlc_history']}
826
- for nc in new_candles:
827
- if nc['time'] in existing_times:
828
- for i, ec in enumerate(market_state['ohlc_history']):
829
- if ec['time'] == nc['time']:
830
- market_state['ohlc_history'][i] = nc
831
- break
832
- else:
833
- market_state['ohlc_history'].append(nc)
834
-
835
- market_state['ohlc_history'].sort(key=lambda x: x['time'])
836
-
837
- if len(market_state['ohlc_history']) > 800:
838
- market_state['ohlc_history'] = market_state['ohlc_history'][-800:]
839
-
840
- market_state['ready'] = True
841
- break
842
- except Exception as e:
843
- logging.warning(f"REST update error: {e}")
844
-
845
- await asyncio.sleep(5)
846
 
847
- async def broadcast_worker():
848
  while True:
849
  if connected_clients and market_state['ready']:
850
  payload = process_market_data()
851
  if payload and "data" in payload:
852
  msg = json.dumps(payload)
853
- disconnected = set()
854
- for ws in connected_clients:
855
  try:
856
  await ws.send_str(msg)
857
  except Exception:
858
- disconnected.add(ws)
859
- connected_clients.difference_update(disconnected)
860
  await asyncio.sleep(BROADCAST_RATE)
861
 
862
- async def websocket_handler(request):
863
  ws = web.WebSocketResponse()
864
  await ws.prepare(request)
865
  connected_clients.add(ws)
866
- logging.info(f"Client connected. Total: {len(connected_clients)}")
867
  try:
868
- async for msg in ws:
869
- pass
870
  finally:
871
  connected_clients.discard(ws)
872
- logging.info(f"Client disconnected. Total: {len(connected_clients)}")
873
  return ws
874
 
875
- async def handle_index(request):
876
  return web.Response(text=HTML_PAGE, content_type='text/html')
877
 
878
- async def handle_health(request):
879
- return web.json_response({
880
- "status": "ok",
881
- "ready": market_state['ready'],
882
- "candles": len(market_state['ohlc_history']),
883
- "clients": len(connected_clients)
884
- })
885
-
886
  async def main():
887
  app = web.Application()
888
- app.router.add_get('/', handle_index)
889
- app.router.add_get('/ws', websocket_handler)
890
- app.router.add_get('/health', handle_health)
891
-
892
- asyncio.create_task(kraken_rest_worker())
893
- asyncio.create_task(broadcast_worker())
894
-
895
  runner = web.AppRunner(app)
896
  await runner.setup()
897
  site = web.TCPSite(runner, '0.0.0.0', PORT)
898
  await site.start()
899
-
900
- logging.info(f"Server running at http://localhost:{PORT}")
901
-
902
  await asyncio.Event().wait()
903
 
904
  if __name__ == "__main__":
905
  try:
906
  asyncio.run(main())
907
  except KeyboardInterrupt:
908
- logging.info("Shutting down...")
 
11
  SYMBOL_KRAKEN = "BTC/USD"
12
  PORT = 7860
13
  BROADCAST_RATE = 1.0
14
+ PREDICTION_HORIZON = 60
15
 
16
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(message)s')
17
 
 
36
  df[c] = df[c].astype(float)
37
 
38
  df['ema'] = df['close'].ewm(span=20, adjust=False).mean()
 
 
 
39
  df['sma20'] = df['close'].rolling(window=20).mean()
40
  df['std'] = df['close'].rolling(window=20).std()
41
  df['bb_upper'] = df['sma20'] + (df['std'] * 2)
42
  df['bb_lower'] = df['sma20'] - (df['std'] * 2)
43
+
 
44
  delta = df['close'].diff()
45
  gain = (delta.where(delta > 0, 0)).rolling(window=14).mean()
46
  loss = (-delta.where(delta < 0, 0)).rolling(window=14).mean()
 
56
  low_min = df['low'].rolling(window=14).min()
57
  high_max = df['high'].rolling(window=14).max()
58
  df['stoch_k'] = 100 * ((df['close'] - low_min) / (high_max - low_min))
59
+
 
60
  df['tr0'] = abs(df['high'] - df['low'])
61
  df['tr1'] = abs(df['high'] - df['close'].shift())
62
  df['tr2'] = abs(df['low'] - df['close'].shift())
63
  df['tr'] = df[['tr0', 'tr1', 'tr2']].max(axis=1)
64
  df['atr'] = df['tr'].rolling(window=14).mean()
65
 
 
 
66
  df['tp'] = (df['high'] + df['low'] + df['close']) / 3
67
  df['vwap'] = (df['tp'] * df['volume']).cumsum() / df['volume'].cumsum()
68
 
69
  return df
70
 
71
  def train_model(df):
72
+ feature_cols = ['close', 'ema', 'bb_upper', 'bb_lower', 'rsi', 'macd', 'stoch_k', 'atr', 'vwap']
 
 
 
73
  data = df.dropna().copy()
74
 
75
  future_shifts = {}
 
85
  data = data.dropna()
86
 
87
  if len(data) < 100:
 
88
  return None
89
 
90
  X = data[feature_cols].values
91
  y = data[targets].values
92
 
93
+ model = RandomForestRegressor(n_estimators=30, max_depth=8, n_jobs=-1, random_state=42)
94
  model.fit(X, y)
 
 
95
  return model
96
 
97
  def get_prediction(df, model):
98
  if model is None:
99
  return []
100
 
101
+ feature_cols = ['close', 'ema', 'bb_upper', 'bb_lower', 'rsi', 'macd', 'stoch_k', 'atr', 'vwap']
102
  last_row = df.iloc[[-1]][feature_cols]
103
 
104
  if last_row.isnull().values.any():
 
134
  predictions = []
135
  try:
136
  predictions = get_prediction(df, market_state['model'])
137
+ except Exception:
138
+ pass
139
 
140
  df_clean = df.replace([np.inf, -np.inf], np.nan)
141
+ df_clean = df_clean.astype(object).where(pd.notnull(df_clean), None)
142
 
143
+ last_close = float(df['close'].iloc[-1]) if len(df) > 0 else 0
144
+ first_close = float(df['close'].iloc[0]) if len(df) > 0 else 0
145
  price_change = ((last_close - first_close) / first_close * 100) if first_close > 0 else 0
146
 
147
  market_state['last_price'] = last_close
148
  market_state['price_change'] = price_change
149
 
150
+ full_data = df_clean.to_dict('records')
151
+ last_row = df.iloc[-1] if len(df) > 0 else {}
 
152
 
153
  return {
154
  "data": full_data,
 
156
  "stats": {
157
  "price": last_close,
158
  "change": round(price_change, 2),
159
+ "rsi": round(float(last_row.get('rsi')), 1) if pd.notna(last_row.get('rsi')) else 0,
160
+ "macd": round(float(last_row.get('macd')), 2) if pd.notna(last_row.get('macd')) else 0,
161
+ "atr": round(float(last_row.get('atr')), 2) if pd.notna(last_row.get('atr')) else 0,
162
+ "volume": round(float(last_row.get('volume')), 2) if pd.notna(last_row.get('volume')) else 0
163
  }
164
  }
165
 
 
169
  <head>
170
  <meta charset="UTF-8">
171
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
172
+ <title>QuantAI Terminal | BTC/USD</title>
173
  <script src="https://unpkg.com/lightweight-charts@4.1.1/dist/lightweight-charts.standalone.production.js"></script>
174
+ <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;700&family=Inter:wght@400;500;600&display=swap" rel="stylesheet">
175
  <style>
176
+ :root {
177
+ --bg-dark: #0b0e11;
178
+ --bg-panel: #151a21;
179
+ --accent: #2962ff;
180
+ --accent-glow: rgba(41, 98, 255, 0.3);
181
+ --up: #00e396;
182
+ --down: #ff004c;
183
+ --text-main: #eceff1;
184
+ --text-dim: #94a3b8;
185
+ --border: rgba(255, 255, 255, 0.08);
186
+ }
187
  * { margin: 0; padding: 0; box-sizing: border-box; }
 
188
  body {
189
+ background-color: var(--bg-dark);
190
+ color: var(--text-main);
191
+ font-family: 'Inter', sans-serif;
192
  height: 100vh;
 
 
193
  overflow: hidden;
194
+ display: grid;
195
+ grid-template-rows: 60px 1fr;
196
  }
 
197
  .header {
198
+ background: var(--bg-panel);
199
+ border-bottom: 1px solid var(--border);
 
 
200
  display: flex;
201
  align-items: center;
202
  justify-content: space-between;
203
+ padding: 0 24px;
204
+ z-index: 10;
205
  }
206
+ .brand {
 
207
  display: flex;
208
  align-items: center;
209
+ gap: 12px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
210
  }
211
+ .brand-text { font-family: 'JetBrains Mono', monospace; font-weight: 700; font-size: 18px; color: #fff; }
212
+ .live-badge {
213
+ background: rgba(0, 227, 150, 0.15);
214
+ color: var(--up);
215
+ padding: 4px 8px;
216
+ border-radius: 4px;
217
+ font-size: 11px;
218
  font-weight: 600;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
219
  display: flex;
220
  align-items: center;
221
+ gap: 6px;
 
 
 
 
 
 
222
  }
223
+ .live-dot { width: 6px; height: 6px; background: var(--up); border-radius: 50%; animation: pulse 1.5s infinite; }
224
+ .live-dot.offline { background: var(--down); animation: none; }
225
 
226
+ .dashboard-grid {
227
+ display: grid;
228
+ grid-template-columns: 280px 1fr;
229
+ height: 100%;
230
+ overflow: hidden;
231
  }
232
+ .sidebar {
233
+ background: var(--bg-panel);
234
+ border-right: 1px solid var(--border);
235
+ padding: 20px;
236
  display: flex;
237
  flex-direction: column;
238
+ gap: 20px;
239
+ }
240
+ .stat-card {
241
+ background: rgba(255,255,255,0.02);
242
+ border: 1px solid var(--border);
243
+ border-radius: 8px;
244
+ padding: 16px;
245
+ }
246
+ .stat-label { font-size: 11px; color: var(--text-dim); text-transform: uppercase; margin-bottom: 6px; letter-spacing: 0.5px; }
247
+ .stat-value { font-family: 'JetBrains Mono', monospace; font-size: 20px; font-weight: 500; }
248
+ .stat-value.up { color: var(--up); }
249
+ .stat-value.down { color: var(--down); }
250
+
251
+ .main-view {
252
+ display: grid;
253
+ grid-template-rows: 3fr 1fr;
254
  position: relative;
255
  }
256
+ .chart-container { position: relative; width: 100%; height: 100%; }
257
+ .chart-overlay {
 
 
 
 
 
 
 
 
 
258
  position: absolute;
259
+ top: 20px;
260
+ left: 20px;
261
+ z-index: 5;
262
  display: flex;
263
+ gap: 15px;
264
+ font-size: 12px;
265
+ font-family: 'JetBrains Mono', monospace;
266
  pointer-events: none;
267
  }
268
+ .legend-item { display: flex; align-items: center; gap: 6px; color: var(--text-dim); }
269
+ .legend-dot { width: 8px; height: 8px; border-radius: 2px; }
270
 
271
+ .loader-screen {
 
 
 
 
 
 
 
 
 
 
 
 
272
  position: absolute;
273
+ inset: 0;
274
+ background: var(--bg-dark);
 
 
 
275
  display: flex;
276
  flex-direction: column;
277
  align-items: center;
278
  justify-content: center;
279
+ z-index: 50;
280
+ transition: opacity 0.3s;
281
+ }
282
+ .spinner {
283
+ width: 40px;
284
+ height: 40px;
285
+ border: 3px solid rgba(41, 98, 255, 0.2);
286
+ border-top-color: var(--accent);
 
 
 
 
 
 
287
  border-radius: 50%;
288
  animation: spin 1s linear infinite;
289
  }
290
+ @keyframes spin { to { transform: rotate(360deg); } }
291
+ @keyframes pulse { 0% { opacity: 1; } 50% { opacity: 0.4; } 100% { opacity: 1; } }
292
 
293
+ .pred-pill {
294
+ background: linear-gradient(135deg, rgba(140, 0, 255, 0.2), rgba(41, 98, 255, 0.2));
295
+ border: 1px solid rgba(140, 0, 255, 0.4);
296
+ color: #d09bff;
 
 
 
 
 
 
 
 
 
 
 
 
297
  padding: 4px 10px;
298
  border-radius: 12px;
299
+ font-size: 11px;
300
+ font-weight: 600;
 
301
  }
302
  </style>
303
  </head>
304
  <body>
305
  <div class="header">
306
+ <div class="brand">
307
+ <div style="width: 24px; height: 24px; background: var(--accent); border-radius: 6px;"></div>
308
+ <span class="brand-text">QuantAI Terminal</span>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
309
  </div>
310
+ <div class="live-badge">
311
+ <div id="status-dot" class="live-dot"></div>
312
+ <span id="status-text">CONNECTING</span>
 
313
  </div>
314
  </div>
315
 
316
+ <div class="dashboard-grid">
317
+ <div class="sidebar">
318
+ <div class="stat-card">
319
+ <div class="stat-label">Market Price</div>
320
+ <div id="price" class="stat-value">--</div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
321
  </div>
322
+ <div class="stat-card">
323
+ <div class="stat-label">24h Change</div>
324
+ <div id="change" class="stat-value">--</div>
325
+ </div>
326
+ <div class="stat-card">
327
+ <div class="stat-label">RSI (14)</div>
328
+ <div id="rsi" class="stat-value">--</div>
329
+ </div>
330
+ <div class="stat-card">
331
+ <div class="stat-label">ATR Volatility</div>
332
+ <div id="atr" class="stat-value">--</div>
333
+ </div>
334
+ <div class="stat-card">
335
+ <div class="stat-label">AI Model Status</div>
336
+ <div id="model-status" style="font-size: 13px; color: var(--accent);">Active</div>
337
  </div>
338
  </div>
339
 
340
+ <div class="main-view">
341
+ <div id="main-chart" class="chart-container">
342
+ <div class="loader-screen" id="loader">
343
+ <div class="spinner"></div>
344
+ <div style="margin-top:15px; font-size:12px; color: #666;">PROCESSING MARKET DATA</div>
345
+ </div>
346
+ <div class="chart-overlay">
347
+ <div class="legend-item"><div class="legend-dot" style="background: #00e396"></div>Price</div>
348
+ <div class="legend-item"><div class="legend-dot" style="background: #2962ff"></div>EMA 20</div>
349
+ <div class="legend-item"><div class="legend-dot" style="background: #d09bff"></div>AI Forecast</div>
350
+ <div class="pred-pill">RF MODEL V2</div>
351
+ </div>
352
  </div>
353
+ <div id="sub-chart" class="chart-container" style="border-top: 1px solid var(--border);"></div>
354
  </div>
355
  </div>
356
 
357
  <script>
358
+ const upColor = '#00e396';
359
+ const downColor = '#ff004c';
360
+ const bgDark = '#0b0e11';
361
+
362
+ const mainChartEl = document.getElementById('main-chart');
363
+ const subChartEl = document.getElementById('sub-chart');
364
+
365
+ const chartOpts = {
366
+ layout: { background: { color: bgDark }, textColor: '#94a3b8' },
367
+ grid: { vertLines: { color: 'rgba(255,255,255,0.04)' }, horzLines: { color: 'rgba(255,255,255,0.04)' } },
368
+ timeScale: { borderColor: 'rgba(255,255,255,0.1)', timeVisible: true },
369
+ rightPriceScale: { borderColor: 'rgba(255,255,255,0.1)' },
370
+ crosshair: { mode: LightweightCharts.CrosshairMode.Normal }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
371
  };
372
+
373
+ const chart = LightweightCharts.createChart(mainChartEl, chartOpts);
374
+ const subChart = LightweightCharts.createChart(subChartEl, chartOpts);
375
+
376
+ const candles = chart.addCandlestickSeries({
377
+ upColor: bgDark, downColor: downColor, borderUpColor: upColor, borderDownColor: downColor,
378
+ wickUpColor: upColor, wickDownColor: downColor
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
379
  });
380
+
381
+ const emaSeries = chart.addLineSeries({ color: '#2962ff', lineWidth: 2, crosshairMarkerVisible: false });
382
+ const predSeries = chart.addLineSeries({ color: '#d09bff', lineWidth: 2, lineStyle: 2, crosshairMarkerVisible: false });
383
+
384
+ const rsiSeries = subChart.addLineSeries({ color: '#f9a825', lineWidth: 2, priceScaleId: 'right' });
385
+ const rsiLine70 = subChart.createPriceLine({ price: 70, color: 'rgba(255,255,255,0.3)', lineWidth: 1, lineStyle: 2, axisLabelVisible: false });
386
+ const rsiLine30 = subChart.createPriceLine({ price: 30, color: 'rgba(255,255,255,0.3)', lineWidth: 1, lineStyle: 2, axisLabelVisible: false });
387
+
388
+ function resize() {
389
+ chart.applyOptions({ width: mainChartEl.clientWidth, height: mainChartEl.clientHeight });
390
+ subChart.applyOptions({ width: subChartEl.clientWidth, height: subChartEl.clientHeight });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
391
  }
392
+ window.onresize = resize;
393
+
394
+ chart.timeScale().subscribeVisibleLogicalRangeChange(range => subChart.timeScale().setVisibleLogicalRange(range));
395
+ subChart.timeScale().subscribeVisibleLogicalRangeChange(range => chart.timeScale().setVisibleLogicalRange(range));
396
+
397
+ let isLoaded = false;
398
+
399
  function connect() {
400
+ const ws = new WebSocket((location.protocol === 'https:' ? 'wss://' : 'ws://') + location.host + '/ws');
 
401
 
402
+ ws.onopen = () => {
403
+ document.getElementById('status-dot').className = 'live-dot';
404
+ document.getElementById('status-text').innerText = 'LIVE FEED';
405
+ };
406
+
407
+ ws.onclose = () => {
408
+ document.getElementById('status-dot').className = 'live-dot offline';
409
+ document.getElementById('status-text').innerText = 'RECONNECTING';
410
+ setTimeout(connect, 2000);
411
+ };
412
 
413
+ ws.onmessage = (event) => {
414
  try {
415
+ const msg = JSON.parse(event.data);
416
+ if(msg.error) return;
417
 
418
+ const data = msg.data;
419
+ const last = data[data.length - 1];
420
 
421
+ const candleData = data.map(d => ({
422
+ time: d.time, open: d.open, high: d.high, low: d.low, close: d.close
423
+ }));
424
+
425
+ candles.setData(candleData);
426
+
427
+ const emaData = data.filter(d => d.ema).map(d => ({ time: d.time, value: d.ema }));
428
+ emaSeries.setData(emaData);
429
+
430
+ const rsiData = data.filter(d => d.rsi).map(d => ({ time: d.time, value: d.rsi }));
431
+ rsiSeries.setData(rsiData);
432
+
433
+ if (msg.prediction && msg.prediction.length) {
434
+ const predPoints = [{ time: last.time, value: last.close }, ...msg.prediction];
435
+ predSeries.setData(predPoints);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
436
  }
437
+
438
+ document.getElementById('price').innerText = '$' + msg.stats.price.toLocaleString(undefined, {minimumFractionDigits: 2});
439
+
440
+ const chg = msg.stats.change;
441
+ const chgEl = document.getElementById('change');
442
+ chgEl.innerText = (chg > 0 ? '+' : '') + chg + '%';
443
+ chgEl.className = 'stat-value ' + (chg >= 0 ? 'up' : 'down');
444
+
445
+ document.getElementById('rsi').innerText = msg.stats.rsi;
446
+ document.getElementById('atr').innerText = msg.stats.atr;
447
+
448
+ if (!isLoaded) {
449
+ isLoaded = true;
450
+ document.getElementById('loader').style.opacity = '0';
451
+ setTimeout(() => document.getElementById('loader').remove(), 300);
452
+ chart.timeScale().fitContent();
453
+ }
454
+ } catch (e) {
455
+ console.error(e);
456
  }
457
  };
 
 
 
 
 
 
 
458
  }
459
 
460
  connect();
461
+ setTimeout(resize, 100);
462
  </script>
463
  </body>
464
  </html>
 
468
  try:
469
  async with aiohttp.ClientSession() as session:
470
  url = "https://api.kraken.com/0/public/OHLC?pair=XBTUSD&interval=1"
471
+ async with session.get(url, timeout=30) as response:
472
  if response.status == 200:
473
  data = await response.json()
474
  if 'result' in data:
 
476
  if key != 'last':
477
  raw = data['result'][key]
478
  market_state['ohlc_history'] = [
479
+ {'time': int(c[0]), 'open': float(c[1]), 'high': float(c[2]), 'low': float(c[3]), 'close': float(c[4]), 'volume': float(c[6])}
 
 
 
 
 
 
 
480
  for c in raw[-720:]
481
  ]
482
  market_state['ready'] = True
483
+ except Exception:
484
+ pass
 
 
 
485
 
486
+ async def worker_kraken():
487
  await fetch_initial_data()
 
488
  while True:
489
  try:
490
  async with aiohttp.ClientSession() as session:
491
  url = "https://api.kraken.com/0/public/OHLC?pair=XBTUSD&interval=1"
492
+ async with session.get(url, timeout=10) as response:
493
  if response.status == 200:
494
  data = await response.json()
495
+ for key in data['result']:
496
+ if key != 'last':
497
+ raw = data['result'][key]
498
+ new_candles = [
499
+ {'time': int(c[0]), 'open': float(c[1]), 'high': float(c[2]), 'low': float(c[3]), 'close': float(c[4]), 'volume': float(c[6])}
500
+ for c in raw[-5:]
501
+ ]
502
+ current_times = {c['time'] for c in market_state['ohlc_history']}
503
+ for c in new_candles:
504
+ if c['time'] not in current_times:
505
+ market_state['ohlc_history'].append(c)
506
+ else:
507
+ for i, exist in enumerate(market_state['ohlc_history']):
508
+ if exist['time'] == c['time']:
509
+ market_state['ohlc_history'][i] = c
510
+ market_state['ohlc_history'] = market_state['ohlc_history'][-800:]
511
+ market_state['ready'] = True
512
+ except Exception:
513
+ pass
514
+ await asyncio.sleep(10)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
515
 
516
+ async def worker_broadcast():
517
  while True:
518
  if connected_clients and market_state['ready']:
519
  payload = process_market_data()
520
  if payload and "data" in payload:
521
  msg = json.dumps(payload)
522
+ for ws in list(connected_clients):
 
523
  try:
524
  await ws.send_str(msg)
525
  except Exception:
526
+ connected_clients.discard(ws)
 
527
  await asyncio.sleep(BROADCAST_RATE)
528
 
529
+ async def ws_handler(request):
530
  ws = web.WebSocketResponse()
531
  await ws.prepare(request)
532
  connected_clients.add(ws)
 
533
  try:
534
+ async for msg in ws: pass
 
535
  finally:
536
  connected_clients.discard(ws)
 
537
  return ws
538
 
539
+ async def idx_handler(request):
540
  return web.Response(text=HTML_PAGE, content_type='text/html')
541
 
 
 
 
 
 
 
 
 
542
  async def main():
543
  app = web.Application()
544
+ app.add_routes([web.get('/', idx_handler), web.get('/ws', ws_handler)])
545
+ asyncio.create_task(worker_kraken())
546
+ asyncio.create_task(worker_broadcast())
 
 
 
 
547
  runner = web.AppRunner(app)
548
  await runner.setup()
549
  site = web.TCPSite(runner, '0.0.0.0', PORT)
550
  await site.start()
551
+ logging.info(f"http://localhost:{PORT}")
 
 
552
  await asyncio.Event().wait()
553
 
554
  if __name__ == "__main__":
555
  try:
556
  asyncio.run(main())
557
  except KeyboardInterrupt:
558
+ pass