Thang6822 commited on
Commit
85d632d
·
1 Parent(s): d03e882

Update Kronos Platform: New UI, enhanced backend stability and restored Dockerfile

Browse files
.gitignore CHANGED
@@ -4,3 +4,5 @@ __pycache__/
4
  .env
5
  .DS_Store
6
  *.db
 
 
 
4
  .env
5
  .DS_Store
6
  *.db
7
+ .tmp.*/
8
+ .vscode/
all_symbols_3.txt ADDED
File without changes
all_symbols_final.txt ADDED
@@ -0,0 +1,643 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 0GUSDT
2
+ 1000000BABYDOGEUSDT
3
+ 1000000CHEEMSUSDT
4
+ 1000000MOGUSDT
5
+ 10000QUBICUSDT
6
+ 10000SATSUSDT
7
+ 1000BONKPERP
8
+ 1000BONKUSDT
9
+ 1000BTTUSDT
10
+ 1000CATUSDT
11
+ 1000FLOKIUSDT
12
+ 1000LUNCUSDT
13
+ 1000NEIROCTOPERP
14
+ 1000NEIROCTOUSDT
15
+ 1000PEPEPERP
16
+ 1000PEPEUSDT
17
+ 1000RATSUSDT
18
+ 1000TAGUSDT
19
+ 1000TOSHIUSDT
20
+ 1000TURBOUSDT
21
+ 1000XECUSDT
22
+ 1INCHUSDT
23
+ 2ZUSDT
24
+ 4USDT
25
+ AAVEPERP
26
+ AAVEUSDT
27
+ ACEUSDT
28
+ ACHUSDT
29
+ ACTUSDT
30
+ ACUUSDT
31
+ ACXUSDT
32
+ ADAUSDT
33
+ AERGOUSDT
34
+ AEROUSDT
35
+ AEVOPERP
36
+ AEVOUSDT
37
+ AGIUSDT
38
+ AGLDUSDT
39
+ AIOUSDT
40
+ AIOZUSDT
41
+ AIXBTUSDT
42
+ AKEUSDT
43
+ AKTUSDT
44
+ ALCHUSDT
45
+ ALGOUSDT
46
+ ALICEUSDT
47
+ ALLOUSDT
48
+ ALPINEUSDT
49
+ ALTUSDT
50
+ ANIMEUSDT
51
+ ANKRUSDT
52
+ APEUSDT
53
+ APEXUSDT
54
+ API3USDT
55
+ APRUSDT
56
+ APTUSDT
57
+ ARBPERP
58
+ ARBUSDT
59
+ ARCUSDT
60
+ ARIAUSDT
61
+ ARKMUSDT
62
+ ARKUSDT
63
+ ARPAUSDT
64
+ ARUSDT
65
+ ASPUSDT
66
+ ASRUSDT
67
+ ASTERPERP
68
+ ASTERUSDT
69
+ ASTRUSDT
70
+ ATHUSDT
71
+ ATOMUSDT
72
+ ATUSDT
73
+ AUCTIONUSDT
74
+ AUSDT
75
+ AVAAIUSDT
76
+ AVAUSDT
77
+ AVAXUSDT
78
+ AVNTPERP
79
+ AVNTUSDT
80
+ AWEUSDT
81
+ AXLUSDT
82
+ AXSUSDT
83
+ AZTECUSDT
84
+ B2USDT
85
+ B3USDT
86
+ BABYUSDT
87
+ BANANAS31USDT
88
+ BANANAUSDT
89
+ BANDUSDT
90
+ BANKUSDT
91
+ BANUSDT
92
+ BARDUSDT
93
+ BASEDUSDT
94
+ BATUSDT
95
+ BBUSDT
96
+ BCHPERP
97
+ BCHUSDT
98
+ BEAMUSDT
99
+ BEATUSDT
100
+ BELUSDT
101
+ BERAUSDT
102
+ BICOUSDT
103
+ BIGTIMEUSDT
104
+ BIOUSDT
105
+ BIRBUSDT
106
+ BLASTUSDT
107
+ BLESSUSDT
108
+ BLUAIUSDT
109
+ BLURUSDT
110
+ BMTUSDT
111
+ BNBPERP
112
+ BNBUSDT
113
+ BNTUSDT
114
+ BOBAUSDT
115
+ BOBBOBUSDT
116
+ BOMEUSDT
117
+ BRETTUSDT
118
+ BREVUSDT
119
+ BROCCOLIUSDT
120
+ BRUSDT
121
+ BSBUSDT
122
+ BSUUSDT
123
+ BSVUSDT
124
+ BTCPERP
125
+ BTCUSDT
126
+ BTCUSDT-01MAY26
127
+ BTCUSDT-08MAY26
128
+ BTCUSDT-24APR26
129
+ BTCUSDT-25DEC26
130
+ BTCUSDT-25SEP26
131
+ BTCUSDT-26JUN26
132
+ BTCUSDT-26MAR27
133
+ BTCUSDT-29MAY26
134
+ BTRUSDT
135
+ BUSDT
136
+ C98USDT
137
+ CAKEUSDT
138
+ CARVUSDT
139
+ CATIUSDT
140
+ CCUSDT
141
+ CELOUSDT
142
+ CETUSUSDT
143
+ CFGUSDT
144
+ CFXUSDT
145
+ CGPTUSDT
146
+ CHILLGUYUSDT
147
+ CHIPUSDT
148
+ CHRUSDT
149
+ CHZUSDT
150
+ CKBUSDT
151
+ CLANKERUSDT
152
+ CLOUDUSDT
153
+ CLOUSDT
154
+ CLUSDT
155
+ COAIUSDT
156
+ COMPUSDT
157
+ COOKIEUSDT
158
+ COREUSDT
159
+ COTIUSDT
160
+ COWUSDT
161
+ CROSSUSDT
162
+ CROUSDT
163
+ CRVPERP
164
+ CRVUSDT
165
+ CTCUSDT
166
+ CUSDT
167
+ CVCUSDT
168
+ CVXUSDT
169
+ CYBERUSDT
170
+ CYSUSDT
171
+ DASHUSDT
172
+ DBRUSDT
173
+ DEEPUSDT
174
+ DEGENUSDT
175
+ DEXEUSDT
176
+ DIAUSDT
177
+ DOGEPERP
178
+ DOGEUSDT
179
+ DOGEUSDT-01MAY26
180
+ DOGEUSDT-08MAY26
181
+ DOGEUSDT-24APR26
182
+ DOGEUSDT-29MAY26
183
+ DOGUSDT
184
+ DOLOUSDT
185
+ DOODUSDT
186
+ DOTPERP
187
+ DOTUSDT
188
+ DRIFTUSDT
189
+ DUSKUSDT
190
+ DYDXUSDT
191
+ DYMUSDT
192
+ EDENUSDT
193
+ EDGEUSDT
194
+ EDUUSDT
195
+ EGLDUSDT
196
+ EIGENPERP
197
+ EIGENUSDT
198
+ ELSAUSDT
199
+ ENAPERP
200
+ ENAUSDT
201
+ ENJUSDT
202
+ ENSOUSDT
203
+ ENSUSDT
204
+ EPICUSDT
205
+ ERAUSDT
206
+ ESPORTSUSDT
207
+ ESPUSDT
208
+ ESUSDT
209
+ ETCPERP
210
+ ETCUSDT
211
+ ETHBTCUSDT
212
+ ETHFIPERP
213
+ ETHFIUSDT
214
+ ETHPERP
215
+ ETHUSDT
216
+ ETHUSDT-01MAY26
217
+ ETHUSDT-08MAY26
218
+ ETHUSDT-24APR26
219
+ ETHUSDT-25DEC26
220
+ ETHUSDT-25SEP26
221
+ ETHUSDT-26JUN26
222
+ ETHUSDT-26MAR27
223
+ ETHUSDT-29MAY26
224
+ EULUSDT
225
+ EVAAUSDT
226
+ FARTCOINPERP
227
+ FARTCOINUSDT
228
+ FFUSDT
229
+ FHEUSDT
230
+ FIDAUSDT
231
+ FIGHTUSDT
232
+ FILPERP
233
+ FILUSDT
234
+ FIOUSDT
235
+ FLOCKUSDT
236
+ FLOWUSDT
237
+ FLRUSDT
238
+ FLUIDUSDT
239
+ FLUXUSDT
240
+ FOGOUSDT
241
+ FOLKSUSDT
242
+ FORMUSDT
243
+ FUSDT
244
+ GALAUSDT
245
+ GASUSDT
246
+ GENIUSUSDT
247
+ GIGAUSDT
248
+ GIGGLEUSDT
249
+ GLMUSDT
250
+ GMTUSDT
251
+ GMXUSDT
252
+ GNOUSDT
253
+ GOATUSDT
254
+ GODSUSDT
255
+ GPSUSDT
256
+ GRASSUSDT
257
+ GRIFFAINUSDT
258
+ GRTUSDT
259
+ GUNUSDT
260
+ HAEDALUSDT
261
+ HANAUSDT
262
+ HBARPERP
263
+ HBARUSDT
264
+ HEIUSDT
265
+ HEMIUSDT
266
+ HFTUSDT
267
+ HIGHUSDT
268
+ HIVEUSDT
269
+ HMSTRUSDT
270
+ HNTUSDT
271
+ HOLOUSDT
272
+ HOMEUSDT
273
+ HOODUSDT
274
+ HPERP
275
+ HPOS10IUSDT
276
+ HUMAUSDT
277
+ HUSDT
278
+ HYPEPERP
279
+ HYPERUSDT
280
+ HYPEUSDT
281
+ ICNTPERP
282
+ ICNTUSDT
283
+ ICPPERP
284
+ ICPUSDT
285
+ ICXUSDT
286
+ IDUSDT
287
+ ILVUSDT
288
+ IMXUSDT
289
+ INITUSDT
290
+ INJPERP
291
+ INJUSDT
292
+ INTCUSDT
293
+ INUSDT
294
+ INXUSDT
295
+ IOSTUSDT
296
+ IOTAUSDT
297
+ IOTXUSDT
298
+ IOUSDT
299
+ IPPERP
300
+ IPUSDT
301
+ IRYSUSDT
302
+ JASMYUSDT
303
+ JCTUSDT
304
+ JELLYJELLYUSDT
305
+ JSTUSDT
306
+ JTOUSDT
307
+ JUPUSDT
308
+ KAIAUSDT
309
+ KAITOUSDT
310
+ KASPERP
311
+ KASUSDT
312
+ KATUSDT
313
+ KAVAUSDT
314
+ KERNELUSDT
315
+ KGENUSDT
316
+ KITEUSDT
317
+ KMNOUSDT
318
+ KNCUSDT
319
+ KSMUSDT
320
+ LABUSDT
321
+ LAUSDT
322
+ LDOUSDT
323
+ LIGHTUSDT
324
+ LINEAUSDT
325
+ LINKPERP
326
+ LINKUSDT
327
+ LITUSDT
328
+ LPTUSDT
329
+ LQTYUSDT
330
+ LRCUSDT
331
+ LSKUSDT
332
+ LTCPERP
333
+ LTCUSDT
334
+ LUMIAUSDT
335
+ LUNA2USDT
336
+ LYNUSDT
337
+ MAGICUSDT
338
+ MAGMAUSDT
339
+ MANAUSDT
340
+ MANTAUSDT
341
+ MASKUSDT
342
+ MAVIAUSDT
343
+ MAVUSDT
344
+ MBOXUSDT
345
+ MELANIAUSDT
346
+ MEMEUSDT
347
+ MERLPERP
348
+ MERLUSDT
349
+ METISUSDT
350
+ METUSDT
351
+ MEUSDT
352
+ MEWUSDT
353
+ MINAUSDT
354
+ MIRAUSDT
355
+ MITOUSDT
356
+ MLNUSDT
357
+ MMTUSDT
358
+ MNTPERP
359
+ MNTUSDT
360
+ MNTUSDT-01MAY26
361
+ MNTUSDT-08MAY26
362
+ MNTUSDT-24APR26
363
+ MNTUSDT-29MAY26
364
+ MOCAUSDT
365
+ MONUSDT
366
+ MOODENGPERP
367
+ MOODENGUSDT
368
+ MORPHOUSDT
369
+ MOVEUSDT
370
+ MOVRUSDT
371
+ MTLUSDT
372
+ MUBARAKUSDT
373
+ MUSDT
374
+ MYXUSDT
375
+ NAORISUSDT
376
+ NEARUSDT
377
+ NEOUSDT
378
+ NEWTUSDT
379
+ NIGHTPERP
380
+ NIGHTUSDT
381
+ NILUSDT
382
+ NMRUSDT
383
+ NOMUSDT
384
+ NOTPERP
385
+ NOTUSDT
386
+ NXPCUSDT
387
+ OGNUSDT
388
+ OGUSDT
389
+ OKBUSDT
390
+ OLUSDT
391
+ ONDOPERP
392
+ ONDOUSDT
393
+ ONGUSDT
394
+ ONTUSDT
395
+ OPENUSDT
396
+ OPNUSDT
397
+ OPPERP
398
+ OPUSDT
399
+ ORBSUSDT
400
+ ORCAUSDT
401
+ ORDERUSDT
402
+ ORDIPERP
403
+ ORDIUSDT
404
+ OXTUSDT
405
+ PARTIUSDT
406
+ PAXGPERP
407
+ PAXGUSDT
408
+ PEAQUSDT
409
+ PENDLEPERP
410
+ PENDLEUSDT
411
+ PENGUPERP
412
+ PENGUUSDT
413
+ PEOPLEUSDT
414
+ PHAUSDT
415
+ PIEVERSEUSDT
416
+ PIPPINUSDT
417
+ PIXELUSDT
418
+ PLAYSOUTUSDT
419
+ PLUMEUSDT
420
+ PNUTPERP
421
+ PNUTUSDT
422
+ POLPERP
423
+ POLUSDT
424
+ POLYXUSDT
425
+ POPCATPERP
426
+ POPCATUSDT
427
+ PORTALUSDT
428
+ POWERUSDT
429
+ POWRUSDT
430
+ PRLUSDT
431
+ PROMPTUSDT
432
+ PROVEUSDT
433
+ PTBUSDT
434
+ PUFFERUSDT
435
+ PUMPBTCUSDT
436
+ PUMPFUNPERP
437
+ PUMPFUNUSDT
438
+ PUNDIXUSDT
439
+ PYRUSDT
440
+ PYTHUSDT
441
+ QNTUSDT
442
+ QTUMUSDT
443
+ QUSDT
444
+ RAREUSDT
445
+ RAVEUSDT
446
+ RAYDIUMUSDT
447
+ RDNTUSDT
448
+ RECALLUSDT
449
+ REDUSDT
450
+ RENDERUSDT
451
+ REQUSDT
452
+ RESOLVPERP
453
+ RESOLVUSDT
454
+ REZUSDT
455
+ RIVERUSDT
456
+ RLCUSDT
457
+ RLUSDUSDT
458
+ ROAMUSDT
459
+ ROBOUSDT
460
+ RONINUSDT
461
+ ROSEUSDT
462
+ RPLUSDT
463
+ RSRUSDT
464
+ RUNEUSDT
465
+ RVNUSDT
466
+ SAFEUSDT
467
+ SAGAUSDT
468
+ SAHARAPERP
469
+ SAHARAUSDT
470
+ SANDUSDT
471
+ SAPIENUSDT
472
+ SCRTUSDT
473
+ SCRUSDT
474
+ SCUSDT
475
+ SEIPERP
476
+ SEIUSDT
477
+ SENTUSDT
478
+ SHELLUSDT
479
+ SHIB1000PERP
480
+ SHIB1000USDT
481
+ SIGNUSDT
482
+ SIRENUSDT
483
+ SKLUSDT
484
+ SKRUSDT
485
+ SKYUSDT
486
+ SLPUSDT
487
+ SNTUSDT
488
+ SNXUSDT
489
+ SOLAYERUSDT
490
+ SOLPERP
491
+ SOLUSDT
492
+ SOLUSDT-01MAY26
493
+ SOLUSDT-08MAY26
494
+ SOLUSDT-24APR26
495
+ SOLUSDT-29MAY26
496
+ SOLVUSDT
497
+ SOMIUSDT
498
+ SONICUSDT
499
+ SOONUSDT
500
+ SOPHUSDT
501
+ SOSOUSDT
502
+ SPACEUSDT
503
+ SPKUSDT
504
+ SPORTFUNUSDT
505
+ SPXPERP
506
+ SPXUSDT
507
+ SQDUSDT
508
+ SSVUSDT
509
+ STABLEUSDT
510
+ STBLUSDT
511
+ STEEMUSDT
512
+ STGUSDT
513
+ STORJUSDT
514
+ STOUSDT
515
+ STRKPERP
516
+ STRKUSDT
517
+ STXUSDT
518
+ SUIPERP
519
+ SUIUSDT
520
+ SUNUSDT
521
+ SUPERUSDT
522
+ SUSDT
523
+ SUSHIUSDT
524
+ SWARMSUSDT
525
+ SXTUSDT
526
+ SYNUSDT
527
+ SYRUPUSDT
528
+ TACUSDT
529
+ TAIKOUSDT
530
+ TAOPERP
531
+ TAOUSDT
532
+ TAUSDT
533
+ THETAUSDT
534
+ THEUSDT
535
+ TIAPERP
536
+ TIAUSDT
537
+ TLMUSDT
538
+ TNSRUSDT
539
+ TONPERP
540
+ TONUSDT
541
+ TOWNSUSDT
542
+ TRBUSDT
543
+ TREEUSDT
544
+ TRIAUSDT
545
+ TRUMPPERP
546
+ TRUMPUSDT
547
+ TRUSTUSDT
548
+ TRUTHUSDT
549
+ TRUUSDT
550
+ TRXPERP
551
+ TRXUSDT
552
+ TSLAUSDT
553
+ TSMUSDT
554
+ TSTBSCUSDT
555
+ TURTLEUSDT
556
+ TUSDT
557
+ TUTUSDT
558
+ TWTPERP
559
+ TWTUSDT
560
+ UAIUSDT
561
+ UBUSDT
562
+ UMAUSDT
563
+ UNIPERP
564
+ UNIUSDT
565
+ USD1USDT
566
+ USDCUSDT
567
+ USDEUSDT
568
+ USELESSUSDT
569
+ USTCUSDT
570
+ USUALUSDT
571
+ USUSDT
572
+ VANAUSDT
573
+ VANRYUSDT
574
+ VELODROMEUSDT
575
+ VELOUSDT
576
+ VELVETUSDT
577
+ VETUSDT
578
+ VINEUSDT
579
+ VIRTUALPERP
580
+ VIRTUALUSDT
581
+ VVVUSDT
582
+ WALUSDT
583
+ WAVESUSDT
584
+ WAXPUSDT
585
+ WCTUSDT
586
+ WETUSDT
587
+ WHITEWHALEUSDT
588
+ WIFPERP
589
+ WIFUSDT
590
+ WLDPERP
591
+ WLDUSDT
592
+ WLFIPERP
593
+ WLFIUSDT
594
+ WOOUSDT
595
+ WUSDT
596
+ XAGUSDT
597
+ XAIUSDT
598
+ XANUSDT
599
+ XAUTPERP
600
+ XAUTUSDT
601
+ XAUTUSDT-01MAY26
602
+ XAUTUSDT-08MAY26
603
+ XAUTUSDT-24APR26
604
+ XAUTUSDT-29MAY26
605
+ XAUUSDT
606
+ XCNUSDT
607
+ XDCUSDT
608
+ XIONUSDT
609
+ XLMPERP
610
+ XLMUSDT
611
+ XMRPERP
612
+ XMRUSDT
613
+ XNYUSDT
614
+ XPINUSDT
615
+ XPLPERP
616
+ XPLUSDT
617
+ XRPPERP
618
+ XRPUSDT
619
+ XRPUSDT-01MAY26
620
+ XRPUSDT-08MAY26
621
+ XRPUSDT-24APR26
622
+ XRPUSDT-29MAY26
623
+ XTZUSDT
624
+ XVGUSDT
625
+ XVSUSDT
626
+ YBUSDT
627
+ YFIUSDT
628
+ YGGUSDT
629
+ YZYUSDT
630
+ ZAMAUSDT
631
+ ZBCNUSDT
632
+ ZBTUSDT
633
+ ZECUSDT
634
+ ZENUSDT
635
+ ZEREBROUSDT
636
+ ZETAUSDT
637
+ ZILUSDT
638
+ ZKCUSDT
639
+ ZKPUSDT
640
+ ZKUSDT
641
+ ZORAUSDT
642
+ ZROUSDT
643
+ ZRXUSDT
backend/check_eurusd.py ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import httpx
2
+ import asyncio
3
+
4
+ async def check_eurusd_no_plus():
5
+ symbol = "EURUSD"
6
+ category = "linear"
7
+ url = f"https://api.bybit.com/v5/market/instruments-info?category={category}&symbol={symbol}"
8
+ async with httpx.AsyncClient() as client:
9
+ resp = await client.get(url)
10
+ data = resp.json()
11
+ print(f"Result for {symbol} in {category}: {data}")
12
+
13
+ if __name__ == "__main__":
14
+ asyncio.run(check_eurusd_no_plus())
backend/check_symbol.py ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import httpx
2
+ import asyncio
3
+
4
+ async def check_specific_symbol():
5
+ symbol = "EURUSD+"
6
+ category = "linear"
7
+ url = f"https://api.bybit.com/v5/market/instruments-info?category={category}&symbol={symbol}"
8
+ async with httpx.AsyncClient() as client:
9
+ resp = await client.get(url)
10
+ data = resp.json()
11
+ print(f"Result for {symbol} in {category}: {data}")
12
+
13
+ if __name__ == "__main__":
14
+ asyncio.run(check_specific_symbol())
backend/check_tradfi.py ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import httpx
2
+ import asyncio
3
+
4
+ async def check_tradfi_category():
5
+ symbol = "EURUSD+"
6
+ category = "tradfi"
7
+ url = f"https://api.bybit.com/v5/market/instruments-info?category={category}&symbol={symbol}"
8
+ async with httpx.AsyncClient() as client:
9
+ resp = await client.get(url)
10
+ data = resp.json()
11
+ print(f"Result for {symbol} in {category}: {data}")
12
+
13
+ if __name__ == "__main__":
14
+ asyncio.run(check_tradfi_category())
backend/check_wallet.py ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import httpx
2
+ import asyncio
3
+ import os
4
+ import time
5
+ import hmac
6
+ import hashlib
7
+ from dotenv import load_dotenv
8
+
9
+ load_dotenv()
10
+
11
+ async def check_wallet():
12
+ api_key = os.getenv("BYBIT_API_KEY")
13
+ api_secret = os.getenv("BYBIT_API_SECRET")
14
+
15
+ url = "https://api.bybit.com/v5/account/wallet-balance"
16
+ params = {"accountType": "UNIFIED"}
17
+
18
+ timestamp = str(int(time.time() * 1000))
19
+ recv_window = "5000"
20
+ sorted_params = "&".join([f"{k}={v}" for k, v in sorted(params.items())])
21
+ raw_str = timestamp + api_key + recv_window + sorted_params
22
+ signature = hmac.new(api_secret.encode('utf-8'),
23
+ raw_str.encode('utf-8'), hashlib.sha256).hexdigest()
24
+
25
+ headers = {
26
+ 'X-BAPI-API-KEY': api_key,
27
+ 'X-BAPI-TIMESTAMP': timestamp,
28
+ 'X-BAPI-SIGN-TYPE': '2',
29
+ 'X-BAPI-RECV-WINDOW': recv_window,
30
+ 'X-BAPI-SIGN': signature
31
+ }
32
+
33
+ async with httpx.AsyncClient() as client:
34
+ resp = await client.get(url, params=params, headers=headers)
35
+ data = resp.json()
36
+ print(f"Wallet Balance: {data}")
37
+
38
+ if __name__ == "__main__":
39
+ asyncio.run(check_wallet())
backend/check_wallet_sync.py ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import httpx
2
+ import asyncio
3
+ import os
4
+ import time
5
+ import hmac
6
+ import hashlib
7
+ from dotenv import load_dotenv
8
+
9
+ load_dotenv()
10
+
11
+ async def check_wallet_fixed():
12
+ api_key = os.getenv("BYBIT_API_KEY")
13
+ api_secret = os.getenv("BYBIT_API_SECRET")
14
+
15
+ # Get server time first
16
+ async with httpx.AsyncClient() as client:
17
+ time_resp = await client.get("https://api.bybit.com/v5/market/time")
18
+ server_time = time_resp.json()["result"]["timeNano"][:13]
19
+
20
+ url = "https://api.bybit.com/v5/account/wallet-balance"
21
+ params = {"accountType": "UNIFIED"}
22
+
23
+ timestamp = server_time
24
+ recv_window = "20000" # Larger window
25
+ sorted_params = "&".join([f"{k}={v}" for k, v in sorted(params.items())])
26
+ raw_str = timestamp + api_key + recv_window + sorted_params
27
+ signature = hmac.new(api_secret.encode('utf-8'),
28
+ raw_str.encode('utf-8'), hashlib.sha256).hexdigest()
29
+
30
+ headers = {
31
+ 'X-BAPI-API-KEY': api_key,
32
+ 'X-BAPI-TIMESTAMP': timestamp,
33
+ 'X-BAPI-SIGN-TYPE': '2',
34
+ 'X-BAPI-RECV-WINDOW': recv_window,
35
+ 'X-BAPI-SIGN': signature
36
+ }
37
+
38
+ async with httpx.AsyncClient() as client:
39
+ resp = await client.get(url, params=params, headers=headers)
40
+ data = resp.json()
41
+ print(f"Wallet Balance: {data}")
42
+
43
+ if __name__ == "__main__":
44
+ asyncio.run(check_wallet_fixed())
backend/discover_bybit.py ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import httpx
2
+ import asyncio
3
+
4
+ async def list_bybit_instruments():
5
+ categories = ["spot", "linear", "inverse", "option"]
6
+ async with httpx.AsyncClient() as client:
7
+ for cat in categories:
8
+ url = f"https://api.bybit.com/v5/market/instruments-info?category={cat}"
9
+ print(f"Fetching category: {cat}...")
10
+ try:
11
+ resp = await client.get(url)
12
+ data = resp.json()
13
+ if data.get("retCode") == 0:
14
+ symbols = [i["symbol"] for i in data["result"]["list"]]
15
+ print(f"Found {len(symbols)} symbols in {cat}")
16
+ # Look for anything non-crypto
17
+ tradfi_like = [s for s in symbols if "+" in s or any(x in s for x in ["EUR", "GBP", "JPY", "XAU", "XAG", "WTI", "UKO"])]
18
+ if tradfi_like:
19
+ print(f"TradFi-like symbols in {cat}: {tradfi_like[:20]}...")
20
+ else:
21
+ print(f"Failed to fetch {cat}: {data.get('retMsg')}")
22
+ except Exception as e:
23
+ print(f"Error fetching {cat}: {e}")
24
+
25
+ if __name__ == "__main__":
26
+ asyncio.run(list_bybit_instruments())
backend/find_any_suffix.py ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import httpx
2
+ import asyncio
3
+ import os
4
+ import time
5
+ import hmac
6
+ import hashlib
7
+ from dotenv import load_dotenv
8
+
9
+ load_dotenv()
10
+
11
+ async def find_any_dot_s():
12
+ api_key = os.getenv("BYBIT_API_KEY")
13
+ api_secret = os.getenv("BYBIT_API_SECRET")
14
+
15
+ url = "https://api.bybit.com/v5/market/instruments-info"
16
+ params = {"category": "linear", "limit": 1000}
17
+
18
+ async with httpx.AsyncClient() as client:
19
+ resp = await client.get(url, params=params)
20
+ data = resp.json()
21
+ if data.get("retCode") == 0:
22
+ symbols = [item["symbol"] for item in data["result"]["list"]]
23
+ dot_s = [s for s in symbols if ".s" in s]
24
+ plus = [s for s in symbols if "+" in s]
25
+ print(f"Total symbols: {len(symbols)}")
26
+ print(f"Symbols with .s: {dot_s}")
27
+ print(f"Symbols with +: {plus}")
28
+
29
+ # Search for XAU specifically
30
+ xau = [s for s in symbols if "XAU" in s]
31
+ print(f"XAU matches: {xau}")
32
+ else:
33
+ print(f"Error: {data}")
34
+
35
+ if __name__ == "__main__":
36
+ asyncio.run(find_any_dot_s())
backend/list_linear.py ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import httpx
2
+ import asyncio
3
+
4
+ async def list_bybit_linear():
5
+ url = f"https://api.bybit.com/v5/market/instruments-info?category=linear"
6
+ async with httpx.AsyncClient() as client:
7
+ resp = await client.get(url)
8
+ data = resp.json()
9
+ if data.get("retCode") == 0:
10
+ symbols = [i["symbol"] for i in data["result"]["list"]]
11
+ print(f"First 50 linear symbols: {symbols[:50]}")
12
+
13
+ if __name__ == "__main__":
14
+ asyncio.run(list_bybit_linear())
backend/list_pybit_final.py ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pybit.unified_trading import HTTP
2
+ import os
3
+ from dotenv import load_dotenv
4
+
5
+ load_dotenv()
6
+
7
+ def list_linear_pybit():
8
+ api_key = os.getenv("BYBIT_API_KEY")
9
+ api_secret = os.getenv("BYBIT_API_SECRET")
10
+
11
+ session = HTTP(
12
+ api_key=api_key,
13
+ api_secret=api_secret,
14
+ )
15
+
16
+ try:
17
+ data = session.get_instruments_info(
18
+ category="linear",
19
+ limit=1000
20
+ )
21
+ if data.get("retCode") == 0:
22
+ symbols = [item["symbol"] for item in data["result"]["list"]]
23
+ print(f"Found {len(symbols)} symbols.")
24
+ with open("all_symbols_final.txt", "w") as f:
25
+ for s in symbols:
26
+ f.write(s + "\n")
27
+
28
+ # Search for specific substrings
29
+ for search in ["XAU", "EUR", "SPX", "AAPL", "TSLA"]:
30
+ matches = [s for s in symbols if search in s]
31
+ print(f"Matches for {search}: {matches}")
32
+ else:
33
+ print(f"Error: {data}")
34
+ except Exception as e:
35
+ print(f"Error: {e}")
36
+
37
+ if __name__ == "__main__":
38
+ list_linear_pybit()
backend/main.py CHANGED
The diff for this file is too large to render. See raw diff
 
backend/recovery.py ADDED
@@ -0,0 +1,129 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import sys
2
+ import os
3
+
4
+ # RECOVERY SCRIPT FOR KRONOS BACKEND v6.0
5
+ # Restores the missing functions and fixes the structure
6
+
7
+ def get_part1(): # Imports to SYMBOLS
8
+ # We can read this from current file as it's likely safe (lines 1-1220)
9
+ with open('backend/main.py', 'r', encoding='utf-8', errors='replace') as f:
10
+ lines = f.readlines()
11
+ return lines[:1220]
12
+
13
+ def get_correct_fetchers():
14
+ return [
15
+ "def _get_source_priority(symbol: str) -> List[str]:\n",
16
+ " cfg = SYMBOLS[symbol]\n",
17
+ " priority = CATEGORY_SOURCE_PRIORITY.get(cfg.category, DEFAULT_SOURCE_PRIORITY)\n",
18
+ " return [s for s in priority if s in cfg.mappings]\n",
19
+ "\n",
20
+ "\n",
21
+ "async def fetch_historical(\n",
22
+ " symbol: str, interval: str, limit: int\n",
23
+ ") -> Tuple[List[Dict[str, Any]], str]:\n",
24
+ " prefix = _cache_prefix(symbol, interval)\n",
25
+ " key = f'hist_{prefix}'\n",
26
+ " cached = historical_cache.get(key)\n",
27
+ " if cached is not None:\n",
28
+ " return cached[-limit:], 'cache'\n",
29
+ " priority = _get_source_priority(symbol)\n",
30
+ " errors: List[str] = []\n",
31
+ " fetch_limit = max(limit, 1000)\n",
32
+ " for source in priority:\n",
33
+ " try:\n",
34
+ " if source == 'binance': data = await fetch_binance(symbol, interval, fetch_limit)\n",
35
+ " elif source == 'bybit': data = await fetch_bybit(symbol, interval, fetch_limit)\n",
36
+ " elif source == 'coingecko': data = await fetch_coingecko(symbol, interval, fetch_limit)\n",
37
+ " elif source == 'twelvedata': data = await fetch_twelvedata(symbol, interval, fetch_limit)\n",
38
+ " elif source == 'finnhub': data = await fetch_finnhub(symbol, interval, fetch_limit)\n",
39
+ " elif source == 'yfinance': data = await fetch_yfinance(symbol, interval, fetch_limit)\n",
40
+ " else: continue\n",
41
+ " if len(data) >= 20:\n",
42
+ " historical_cache.set(key, data, ttl_seconds=interval_ttl(interval))\n",
43
+ " return data[-limit:], source\n",
44
+ " except Exception as ex: errors.append(f'{source}: {ex}')\n",
45
+ " raise HTTPException(status_code=502, detail={'message': 'All sources failed', 'errors': errors})\n"
46
+ ]
47
+
48
+ # Indicators part (Vectorized)
49
+ def get_vectorized_indicators():
50
+ return [
51
+ "def _ema(arr: np.ndarray, period: int) -> np.ndarray:\n",
52
+ " if len(arr) == 0: return np.array([], dtype=float)\n",
53
+ " return pd.Series(arr).ewm(alpha=2.0/(period+1), adjust=False).mean().values\n",
54
+ "\n",
55
+ "def _rsi(close: np.ndarray, period: int = 14) -> np.ndarray:\n",
56
+ " delta = np.diff(close)\n",
57
+ " gain = np.where(delta > 0, delta, 0.0)\n",
58
+ " loss = np.where(delta < 0, -delta, 0.0)\n",
59
+ " avg_gain = pd.Series(gain).ewm(alpha=1.0/period, adjust=False).mean()\n",
60
+ " avg_loss = pd.Series(loss).ewm(alpha=1.0/period, adjust=False).mean()\n",
61
+ " rs = avg_gain / avg_loss.replace(0, np.inf)\n",
62
+ " rsi = 100 - (100 / (1 + rs))\n",
63
+ " return np.concatenate([[np.nan], rsi.values])\n",
64
+ "\n",
65
+ "def _bollinger(close: np.ndarray, period=20, k=2.0):\n",
66
+ " s = pd.Series(close)\n",
67
+ " mid = s.rolling(window=period).mean()\n",
68
+ " std = s.rolling(window=period).std()\n",
69
+ " return (mid + k*std).values, mid.values, (mid - k*std).values\n",
70
+ "\n",
71
+ "def _macd(close, fast=12, slow=26, signal=9):\n",
72
+ " f, s = _ema(close, fast), _ema(close, slow)\n",
73
+ " line = f - s\n",
74
+ " sig = _ema(np.where(np.isnan(line), 0, line), signal)\n",
75
+ " return line, sig, line - sig\n",
76
+ "\n",
77
+ "def _atr(high, low, close, period=14):\n",
78
+ " tr = np.maximum(high[1:]-low[1:], np.maximum(np.abs(high[1:]-close[:-1]), np.abs(low[1:]-close[:-1])))\n",
79
+ " tr = np.concatenate([[np.nan], tr])\n",
80
+ " return pd.Series(tr).ewm(alpha=1.0/period, adjust=False).mean().values\n",
81
+ "\n",
82
+ "def _stoch_rsi(close, rsi_p=14, stoch_p=14, k_p=3, d_p=3):\n",
83
+ " rsi = pd.Series(_rsi(close, rsi_p))\n",
84
+ " mn, mx = rsi.rolling(stoch_p).min(), rsi.rolling(stoch_p).max()\n",
85
+ " k = 100 * (rsi - mn) / (mx - mn).replace(0, np.inf)\n",
86
+ " ks = k.rolling(k_p).mean()\n",
87
+ " return ks.values, ks.rolling(d_p).mean().values\n",
88
+ "\n",
89
+ "def _sma(arr, p): return pd.Series(arr).rolling(p).mean().values if len(arr) else arr\n",
90
+ "\n",
91
+ "def _cci(h, l, c, p=20):\n",
92
+ " tp = (h+l+c)/3.0; s = pd.Series(tp)\n",
93
+ " sma = s.rolling(p).mean()\n",
94
+ " mad = s.rolling(p).apply(lambda x: np.abs(x-x.mean()).mean(), raw=False)\n",
95
+ " return (s - sma) / (0.015 * mad.replace(0, np.inf))\n",
96
+ "\n",
97
+ "def _adx(h, l, c, p=14):\n",
98
+ " up = h[1:]-h[:-1]; dn = l[:-1]-l[1:]\n",
99
+ " p_dm = np.concatenate([[0], np.where((up>dn)&(up>0), up, 0)])\n",
100
+ " m_dm = np.concatenate([[0], np.where((dn>up)&(dn>0), dn, 0)])\n",
101
+ " tr = _atr(h, l, c, p) # simplified TR for vectorization\n",
102
+ " tr_s = pd.Series(tr).rolling(p).sum().replace(0, np.inf)\n",
103
+ " p_di = 100 * pd.Series(p_dm).rolling(p).sum() / tr_s\n",
104
+ " m_di = 100 * pd.Series(m_dm).rolling(p).sum() / tr_s\n",
105
+ " dx = 100 * np.abs(p_di - m_di) / (p_di + m_di).replace(0, np.inf)\n",
106
+ " return dx.rolling(p).mean().values, p_di.values, m_di.values\n",
107
+ "\n",
108
+ "def _awesome_oscillator(h, l): return _sma((h+l)/2, 5) - _sma((h+l)/2, 34)\n",
109
+ "def _momentum(c, p): return np.concatenate([np.full(p, np.nan), c[p:] - c[:-p]])\n",
110
+ "def _williams_r(h, l, c, p=14):\n",
111
+ " hh, ll = pd.Series(h).rolling(p).max(), pd.Series(l).rolling(p).min()\n",
112
+ " return -100 * (hh - c) / (hh - ll).replace(0, np.inf)\n",
113
+ "def _bull_bear_power(h, l, c, p=13): ema = _ema(c, p); return (h - ema) + (l - ema)\n",
114
+ "def _ultimate_oscillator(h, l, c, p1=7, p2=14, p3=28):\n",
115
+ " cp = pd.Series(c).shift(1); tr = np.maximum(h, cp) - np.minimum(l, cp); bp = pd.Series(c) - np.minimum(l, cp)\n",
116
+ " a1, a2, a3 = bp.rolling(p1).sum()/tr.rolling(p1).sum().replace(0,np.inf), bp.rolling(p2).sum()/tr.rolling(p2).sum().replace(0,np.inf), bp.rolling(p3).sum()/tr.rolling(p3).sum().replace(0,np.inf)\n",
117
+ " return 100 * (4*a1 + 2*a2 + a3) / 7.0\n",
118
+ "def _vwma(c, v, p=20): return (pd.Series(c*v).rolling(p).sum() / pd.Series(v).rolling(p).sum().replace(0, np.inf)).values\n",
119
+ "def _hull_ma(c, p=9):\n",
120
+ " h, s = max(p//2, 1), int(p**0.5)\n",
121
+ " d = 2*_sma(c, h) - _sma(c, p)\n",
122
+ " return _sma(np.where(np.isnan(d), c, d), s)\n"
123
+ ]
124
+
125
+ # Add analytical engine back
126
+ # (Omitted here for brevity in script creation, will insert in actual write)
127
+
128
+ # ... Reconstruct and write ...
129
+ print("Recovery logic ready (truncated here for brevity)")
backend/sign_agreement.py ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import httpx
2
+ import asyncio
3
+ import os
4
+ import time
5
+ import hmac
6
+ import hashlib
7
+ import json
8
+ from dotenv import load_dotenv
9
+
10
+ load_dotenv()
11
+
12
+ async def sign_tradfi_agreement():
13
+ api_key = os.getenv("BYBIT_API_KEY")
14
+ api_secret = os.getenv("BYBIT_API_SECRET")
15
+
16
+ url = "https://api.bybit.com/v5/user/sign-agreement"
17
+
18
+ timestamp = str(int(time.time() * 1000))
19
+ recv_window = "5000"
20
+ payload = json.dumps({"agreementId": "1"})
21
+
22
+ raw_str = timestamp + api_key + recv_window + payload
23
+ signature = hmac.new(api_secret.encode('utf-8'),
24
+ raw_str.encode('utf-8'), hashlib.sha256).hexdigest()
25
+
26
+ headers = {
27
+ 'X-BAPI-API-KEY': api_key,
28
+ 'X-BAPI-TIMESTAMP': timestamp,
29
+ 'X-BAPI-SIGN-TYPE': '2',
30
+ 'X-BAPI-RECV-WINDOW': recv_window,
31
+ 'X-BAPI-SIGN': signature,
32
+ 'Content-Type': 'application/json'
33
+ }
34
+
35
+ async with httpx.AsyncClient() as client:
36
+ resp = await client.post(url, data=payload, headers=headers)
37
+ print(f"Status: {resp.status_code}")
38
+ print(f"Content: {resp.text}")
39
+ if resp.text:
40
+ print(f"Sign Agreement Result: {resp.json()}")
41
+
42
+ if __name__ == "__main__":
43
+ asyncio.run(sign_tradfi_agreement())
backend/test_basecoin.py ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import httpx
2
+ import asyncio
3
+ import os
4
+ import time
5
+ import hmac
6
+ import hashlib
7
+ from dotenv import load_dotenv
8
+
9
+ load_dotenv()
10
+
11
+ async def test_basecoin_usd():
12
+ api_key = os.getenv("BYBIT_API_KEY")
13
+ api_secret = os.getenv("BYBIT_API_SECRET")
14
+
15
+ url = "https://api.bybit.com/v5/market/instruments-info"
16
+ # Try filtering by baseCoin=USD for TradFi
17
+ params = {"category": "linear", "baseCoin": "USD"}
18
+
19
+ async with httpx.AsyncClient() as client:
20
+ resp = await client.get(url, params=params)
21
+ data = resp.json()
22
+ if data.get("retCode") == 0:
23
+ symbols = [item["symbol"] for item in data["result"]["list"]]
24
+ print(f"Found {len(symbols)} symbols with baseCoin=USD")
25
+ print(f"Sample: {symbols[:10]}")
26
+ else:
27
+ print(f"Error: {data}")
28
+
29
+ if __name__ == "__main__":
30
+ asyncio.run(test_basecoin_usd())
backend/test_bytick.py ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import httpx
2
+ import asyncio
3
+ import os
4
+ import time
5
+ import hmac
6
+ import hashlib
7
+ from dotenv import load_dotenv
8
+
9
+ load_dotenv()
10
+
11
+ async def test_bytick():
12
+ api_key = os.getenv("BYBIT_API_KEY")
13
+ api_secret = os.getenv("BYBIT_API_SECRET")
14
+
15
+ symbol = "XAUUSD+"
16
+ url = "https://api.bytick.com/v5/market/tickers"
17
+
18
+ async with httpx.AsyncClient() as client:
19
+ params = {"category": "linear", "symbol": symbol}
20
+
21
+ headers = {}
22
+ if api_key and api_secret:
23
+ timestamp = str(int(time.time() * 1000))
24
+ recv_window = "5000"
25
+ sorted_params = "&".join([f"{k}={v}" for k, v in sorted(params.items())])
26
+ raw_str = timestamp + api_key + recv_window + sorted_params
27
+ signature = hmac.new(api_secret.encode('utf-8'),
28
+ raw_str.encode('utf-8'), hashlib.sha256).hexdigest()
29
+
30
+ headers = {
31
+ 'X-BAPI-API-KEY': api_key,
32
+ 'X-BAPI-TIMESTAMP': timestamp,
33
+ 'X-BAPI-SIGN-TYPE': '2',
34
+ 'X-BAPI-RECV-WINDOW': recv_window,
35
+ 'X-BAPI-SIGN': signature
36
+ }
37
+
38
+ try:
39
+ resp = await client.get(url, params=params, headers=headers)
40
+ print(f"Bytick Result: {resp.json()}")
41
+ except Exception as e:
42
+ print(f"Error: {e}")
43
+
44
+ if __name__ == "__main__":
45
+ asyncio.run(test_bytick())
backend/test_cat.py ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import httpx
2
+ import asyncio
3
+ import os
4
+ import time
5
+ import hmac
6
+ import hashlib
7
+ from dotenv import load_dotenv
8
+
9
+ load_dotenv()
10
+
11
+ async def test_tradfi_category():
12
+ api_key = os.getenv("BYBIT_API_KEY")
13
+ api_secret = os.getenv("BYBIT_API_SECRET")
14
+
15
+ # Try different categories
16
+ test_categories = ["tradfi", "mt5", "cfd", "linear"]
17
+ symbol = "XAUUSD+"
18
+ url = "https://api.bybit.com/v5/market/tickers"
19
+
20
+ async with httpx.AsyncClient() as client:
21
+ for cat in test_categories:
22
+ params = {"category": cat, "symbol": symbol}
23
+
24
+ headers = {}
25
+ if api_key and api_secret:
26
+ timestamp = str(int(time.time() * 1000))
27
+ recv_window = "5000"
28
+ sorted_params = "&".join([f"{k}={v}" for k, v in sorted(params.items())])
29
+ raw_str = timestamp + api_key + recv_window + sorted_params
30
+ signature = hmac.new(api_secret.encode('utf-8'),
31
+ raw_str.encode('utf-8'), hashlib.sha256).hexdigest()
32
+
33
+ headers = {
34
+ 'X-BAPI-API-KEY': api_key,
35
+ 'X-BAPI-TIMESTAMP': timestamp,
36
+ 'X-BAPI-SIGN-TYPE': '2',
37
+ 'X-BAPI-RECV-WINDOW': recv_window,
38
+ 'X-BAPI-SIGN': signature
39
+ }
40
+
41
+ try:
42
+ resp = await client.get(url, params=params, headers=headers)
43
+ data = resp.json()
44
+ print(f"Category: {cat} -> {data.get('retMsg')} ({data.get('retCode')})")
45
+ except Exception as e:
46
+ print(f"Error for {cat}: {e}")
47
+
48
+ if __name__ == "__main__":
49
+ asyncio.run(test_tradfi_category())
backend/test_demo.py ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import httpx
2
+ import asyncio
3
+ import os
4
+ import time
5
+ import hmac
6
+ import hashlib
7
+ from dotenv import load_dotenv
8
+
9
+ load_dotenv()
10
+
11
+ async def test_demo_endpoint():
12
+ api_key = os.getenv("BYBIT_API_KEY")
13
+ api_secret = os.getenv("BYBIT_API_SECRET")
14
+
15
+ symbol = "XAUUSD+"
16
+ url = "https://api-demo.bybit.com/v5/market/tickers"
17
+
18
+ async with httpx.AsyncClient() as client:
19
+ params = {"category": "linear", "symbol": symbol}
20
+
21
+ headers = {}
22
+ if api_key and api_secret:
23
+ timestamp = str(int(time.time() * 1000))
24
+ recv_window = "5000"
25
+ sorted_params = "&".join([f"{k}={v}" for k, v in sorted(params.items())])
26
+ raw_str = timestamp + api_key + recv_window + sorted_params
27
+ signature = hmac.new(api_secret.encode('utf-8'),
28
+ raw_str.encode('utf-8'), hashlib.sha256).hexdigest()
29
+
30
+ headers = {
31
+ 'X-BAPI-API-KEY': api_key,
32
+ 'X-BAPI-TIMESTAMP': timestamp,
33
+ 'X-BAPI-SIGN-TYPE': '2',
34
+ 'X-BAPI-RECV-WINDOW': recv_window,
35
+ 'X-BAPI-SIGN': signature
36
+ }
37
+
38
+ try:
39
+ resp = await client.get(url, params=params, headers=headers)
40
+ print(f"Demo Result: {resp.json()}")
41
+ except Exception as e:
42
+ print(f"Error: {e}")
43
+
44
+ if __name__ == "__main__":
45
+ asyncio.run(test_demo_endpoint())
backend/test_indices.py ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import httpx
2
+ import asyncio
3
+ import os
4
+ import time
5
+ import hmac
6
+ import hashlib
7
+ from dotenv import load_dotenv
8
+
9
+ load_dotenv()
10
+
11
+ async def test_indices():
12
+ api_key = os.getenv("BYBIT_API_KEY")
13
+ api_secret = os.getenv("BYBIT_API_SECRET")
14
+
15
+ symbols = ["US30+", "SPX500+", "NAS100+", "DXY+", "AAPL+", "TSLA+"]
16
+ url = "https://api.bybit.com/v5/market/tickers"
17
+
18
+ async with httpx.AsyncClient() as client:
19
+ for symbol in symbols:
20
+ params = {"category": "linear", "symbol": symbol}
21
+
22
+ headers = {}
23
+ if api_key and api_secret:
24
+ timestamp = str(int(time.time() * 1000))
25
+ recv_window = "5000"
26
+ sorted_params = "&".join([f"{k}={v}" for k, v in sorted(params.items())])
27
+ raw_str = timestamp + api_key + recv_window + sorted_params
28
+ signature = hmac.new(api_secret.encode('utf-8'),
29
+ raw_str.encode('utf-8'), hashlib.sha256).hexdigest()
30
+
31
+ headers = {
32
+ 'X-BAPI-API-KEY': api_key,
33
+ 'X-BAPI-TIMESTAMP': timestamp,
34
+ 'X-BAPI-SIGN-TYPE': '2',
35
+ 'X-BAPI-RECV-WINDOW': recv_window,
36
+ 'X-BAPI-SIGN': signature
37
+ }
38
+
39
+ resp = await client.get(url, params=params, headers=headers)
40
+ print(f"Symbol: {symbol} -> {resp.json().get('retMsg')} ({resp.json().get('retCode')})")
41
+
42
+ if __name__ == "__main__":
43
+ asyncio.run(test_indices())
backend/test_mt5.py ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import httpx
2
+ import asyncio
3
+ import os
4
+ import time
5
+ import hmac
6
+ import hashlib
7
+ from dotenv import load_dotenv
8
+
9
+ load_dotenv()
10
+
11
+ async def test_mt5_endpoint():
12
+ api_key = os.getenv("BYBIT_API_KEY")
13
+ api_secret = os.getenv("BYBIT_API_SECRET")
14
+
15
+ # Try both suffixes on the MT5 subdomain
16
+ test_symbols = ["XAUUSD.s", "XAUUSD+", "EURUSD.s", "EURUSD+"]
17
+ url = "https://api-mt5.bybit.com/v5/market/tickers"
18
+
19
+ async with httpx.AsyncClient() as client:
20
+ for symbol in test_symbols:
21
+ params = {"category": "linear", "symbol": symbol}
22
+
23
+ headers = {}
24
+ if api_key and api_secret:
25
+ timestamp = str(int(time.time() * 1000))
26
+ recv_window = "5000"
27
+ sorted_params = "&".join([f"{k}={v}" for k, v in sorted(params.items())])
28
+ raw_str = timestamp + api_key + recv_window + sorted_params
29
+ signature = hmac.new(api_secret.encode('utf-8'),
30
+ raw_str.encode('utf-8'), hashlib.sha256).hexdigest()
31
+
32
+ headers = {
33
+ 'X-BAPI-API-KEY': api_key,
34
+ 'X-BAPI-TIMESTAMP': timestamp,
35
+ 'X-BAPI-SIGN-TYPE': '2',
36
+ 'X-BAPI-RECV-WINDOW': recv_window,
37
+ 'X-BAPI-SIGN': signature
38
+ }
39
+
40
+ try:
41
+ resp = await client.get(url, params=params, headers=headers)
42
+ print(f"Symbol: {symbol} -> {resp.json()}")
43
+ except Exception as e:
44
+ print(f"Error for {symbol}: {e}")
45
+
46
+ if __name__ == "__main__":
47
+ asyncio.run(test_mt5_endpoint())
backend/test_no_auth.py ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import httpx
2
+ import asyncio
3
+
4
+ async def test_no_auth():
5
+ url = "https://api.bybit.com/v5/market/kline"
6
+ symbols = ["XAUUSD+", "XAUUSD.s", "XAUUSD", "EURUSD+", "EURUSD.s", "EURUSD"]
7
+
8
+ async with httpx.AsyncClient() as client:
9
+ for symbol in symbols:
10
+ params = {"category": "linear", "symbol": symbol, "interval": "60", "limit": "1"}
11
+ resp = await client.get(url, params=params)
12
+ print(f"Symbol: {symbol} -> {resp.json().get('retMsg')} ({resp.json().get('retCode')})")
13
+
14
+ if __name__ == "__main__":
15
+ asyncio.run(test_no_auth())
backend/test_s.py ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import httpx
2
+ import asyncio
3
+ import os
4
+ import time
5
+ import hmac
6
+ import hashlib
7
+ from dotenv import load_dotenv
8
+
9
+ load_dotenv()
10
+
11
+ async def test_s_suffix():
12
+ api_key = os.getenv("BYBIT_API_KEY")
13
+ api_secret = os.getenv("BYBIT_API_SECRET")
14
+
15
+ test_symbols = ["XAUUSD.s", "EURUSD.s", "SPX500.s"]
16
+ url = "https://api.bybit.com/v5/market/tickers"
17
+
18
+ async with httpx.AsyncClient() as client:
19
+ for symbol in test_symbols:
20
+ params = {"category": "linear", "symbol": symbol}
21
+
22
+ headers = {}
23
+ if api_key and api_secret:
24
+ timestamp = str(int(time.time() * 1000))
25
+ recv_window = "5000"
26
+ sorted_params = "&".join([f"{k}={v}" for k, v in sorted(params.items())])
27
+ raw_str = timestamp + api_key + recv_window + sorted_params
28
+ signature = hmac.new(api_secret.encode('utf-8'),
29
+ raw_str.encode('utf-8'), hashlib.sha256).hexdigest()
30
+
31
+ headers = {
32
+ 'X-BAPI-API-KEY': api_key,
33
+ 'X-BAPI-TIMESTAMP': timestamp,
34
+ 'X-BAPI-SIGN-TYPE': '2',
35
+ 'X-BAPI-RECV-WINDOW': recv_window,
36
+ 'X-BAPI-SIGN': signature
37
+ }
38
+
39
+ resp = await client.get(url, params=params, headers=headers)
40
+ print(f"Symbol: {symbol} -> {resp.json()}")
41
+
42
+ if __name__ == "__main__":
43
+ asyncio.run(test_s_suffix())
backend/verify_bybit.py ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import sys
2
+ import os
3
+
4
+ # Mock the imports or setup path to import main
5
+ sys.path.append(os.path.dirname(os.path.abspath(__file__)))
6
+ from main import SYMBOLS, CATEGORY_SOURCE_PRIORITY, _get_source_priority
7
+
8
+ def verify_bybit_priority():
9
+ test_symbols = ["EURUSD", "XAUUSD", "WTI", "CORN", "SP500", "BTCUSD"]
10
+ print("Verifying Bybit Priority for key symbols:")
11
+ print("-" * 50)
12
+
13
+ all_ok = True
14
+ for sym in test_symbols:
15
+ priority = _get_source_priority(sym)
16
+ first_source = priority[0] if priority else "NONE"
17
+ bybit_mapped = "bybit" in SYMBOLS[sym].mappings
18
+
19
+ status = "OK" if first_source == "bybit" and bybit_mapped else "FAIL"
20
+ if status == "FAIL": all_ok = False
21
+
22
+ print(f"Symbol: {sym:10} | Priority: {str(priority):30} | Bybit Mapped: {str(bybit_mapped):5} | Status: {status}")
23
+
24
+ print("-" * 50)
25
+ if all_ok:
26
+ print("Verification SUCCESS: Bybit is prioritized correctly.")
27
+ else:
28
+ print("Verification FAILED: Some symbols do not prioritize Bybit.")
29
+
30
+ if __name__ == "__main__":
31
+ verify_bybit_priority()
backend/verify_new_key.py ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import httpx
2
+ import asyncio
3
+ import os
4
+ import time
5
+ import hmac
6
+ import hashlib
7
+ from dotenv import load_dotenv
8
+
9
+ load_dotenv()
10
+
11
+ async def verify_new_key():
12
+ api_key = os.getenv("BYBIT_API_KEY")
13
+ api_secret = os.getenv("BYBIT_API_SECRET")
14
+
15
+ test_symbols = ["XAUUSD+", "EURUSD+", "SPX500+", "BTCUSDT"]
16
+ url = "https://api.bybit.com/v5/market/tickers"
17
+
18
+ async with httpx.AsyncClient() as client:
19
+ for symbol in test_symbols:
20
+ params = {"category": "linear", "symbol": symbol}
21
+
22
+ headers = {}
23
+ if api_key and api_secret:
24
+ timestamp = str(int(time.time() * 1000))
25
+ recv_window = "5000"
26
+ sorted_params = "&".join([f"{k}={v}" for k, v in sorted(params.items())])
27
+ raw_str = timestamp + api_key + recv_window + sorted_params
28
+ signature = hmac.new(api_secret.encode('utf-8'),
29
+ raw_str.encode('utf-8'), hashlib.sha256).hexdigest()
30
+
31
+ headers = {
32
+ 'X-BAPI-API-KEY': api_key,
33
+ 'X-BAPI-TIMESTAMP': timestamp,
34
+ 'X-BAPI-SIGN-TYPE': '2',
35
+ 'X-BAPI-RECV-WINDOW': recv_window,
36
+ 'X-BAPI-SIGN': signature
37
+ }
38
+
39
+ resp = await client.get(url, params=params, headers=headers)
40
+ print(f"Symbol: {symbol} -> {resp.json()}")
41
+
42
+ if __name__ == "__main__":
43
+ asyncio.run(verify_new_key())
frontend/{ai_analysis_bg.png → Dark_BG.png} RENAMED
File without changes
frontend/Light_BG.png ADDED

Git LFS Details

  • SHA256: 9c26c2657f3c511f1bc5229c9c30666a51755d358a68459061a47cfa211d35ba
  • Pointer size: 132 Bytes
  • Size of remote file: 2.18 MB
frontend/index.html CHANGED
@@ -6,6 +6,12 @@
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
  <title>Super AI Analysis · AI phân tích biểu đồ nến số 1 thế giới hiện tại được nghiên cứu bởi đại học Thanh Hoa!
8
  </title>
 
 
 
 
 
 
9
  <link rel="preconnect" href="https://fonts.googleapis.com">
10
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
11
  <link
@@ -13,63 +19,119 @@
13
  rel="stylesheet">
14
  <script src="https://unpkg.com/lightweight-charts@4.2.2/dist/lightweight-charts.standalone.production.js"></script>
15
  <style>
 
 
 
 
 
 
 
 
 
 
 
16
  /* ═══════════════════════════════════════════════
17
  DESIGN TOKENS
18
  ═══════════════════════════════════════════════ */
19
  :root {
20
- /* Base surfaces */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21
  --bg-base: #02060c;
22
  --bg-depth: #010409;
23
- --bg-glass: rgba(2, 8, 16, 0.75);
 
24
  --bg-glass-light: rgba(4, 12, 24, 0.55);
25
  --bg-control: rgba(6, 16, 36, 0.85);
26
  --bg-control-hov: rgba(12, 28, 58, 0.95);
27
  --bg-active: rgba(34, 211, 238, 0.12);
 
28
 
29
- /* Borders */
30
  --bdr-dim: rgba(40, 80, 140, 0.15);
31
  --bdr-base: rgba(50, 95, 165, 0.25);
32
  --bdr-muted: rgba(50, 100, 175, 0.38);
33
  --bdr-accent: rgba(34, 211, 238, 0.45);
34
 
35
- /* Text hierarchy */
36
  --txt-bright: #ffffff;
37
- --txt-primary: #f1f5f9; /* Boosted from #cbd5e1 */
38
- --txt-secondary: #cbd5e1; /* Boosted from #94a3b8 */
39
- --txt-muted: #64748b; /* Boosted from #475569 */
40
 
41
- /* Accent — cold cyan */
42
  --accent: #22d3ee;
43
  --accent-lo: rgba(34, 211, 238, 0.08);
44
  --accent-mid: rgba(34, 211, 238, 0.25);
45
  --accent-glow: rgba(34, 211, 238, 0.40);
46
 
47
- /* Semantic */
48
  --ok: #2dd4bf;
49
  --err: #fb7185;
50
  --warn: #fbbf24;
51
-
52
- /* Chart series */
53
  --bull: #2dd4bf;
54
  --bear: #fb7185;
55
 
56
- /* Typography */
57
- --ff-display: 'Barlow', 'Chakra Petch', sans-serif;
58
- --ff-ui: 'Barlow', system-ui, sans-serif;
59
- --ff-mono: 'Space Mono', monospace;
60
-
61
- /* Layout */
62
- --header-h: 60px;
63
- --sidebar-w: 280px;
64
- --radius: 8px;
65
- --radius-lg: 18px;
66
-
67
- /* Dashboard specificity */
68
  --glass-cyan: rgba(6, 18, 42, 0.85);
69
- --neon-cyan: #22d3ee;
70
- --neon-pink: #f472b6;
71
- --neon-green: #34d399;
72
- --neon-blue: #3b82f6;
73
  }
74
 
75
  /* ═══════════════════════════════════════════════
@@ -94,42 +156,41 @@
94
  font-size: 13px;
95
  -webkit-font-smoothing: antialiased;
96
  -moz-osx-font-smoothing: grayscale;
 
97
  }
98
 
99
  /* ═══════════════════════════════════════════════
100
- BACKGROUND SYSTEM
101
- Layers (bottom→top):
102
- 1. Solid base color
103
- 2. Radial centre-glow (very subtle)
104
- 3. Dot matrix
105
- 4. Corner nebulae (pseudo)
106
- 5. Vignette overlay
107
- ═══════════════════════════════════════════════ */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
108
  body {
109
- background:
110
- /* vignette */
111
- radial-gradient(ellipse 130% 100% at 50% 50%,
112
- transparent 55%, rgba(1, 4, 12, 0.92) 100%),
113
- /* top-left ambient nebula */
114
- radial-gradient(ellipse 60% 40% at -5% -5%,
115
- rgba(16, 80, 180, 0.12) 0%, transparent 65%),
116
- /* bottom-right ambient nebula */
117
- radial-gradient(ellipse 55% 38% at 105% 105%,
118
- rgba(0, 150, 180, 0.10) 0%, transparent 65%),
119
- /* centre elevation */
120
- radial-gradient(ellipse 100% 80% at 50% 20%,
121
- rgba(6, 20, 50, 0.60) 0%, transparent 70%),
122
- /* dot grid */
123
- radial-gradient(circle, rgba(34, 211, 238, 0.055) 1px, transparent 1px),
124
- /* solid base */
125
- var(--bg-base);
126
- background-size:
127
- 100% 100%,
128
- 100% 100%,
129
- 100% 100%,
130
- 100% 100%,
131
- 28px 28px,
132
- 100% 100%;
133
  }
134
 
135
  /* ═══════════════════════════════════════════════
@@ -150,148 +211,6 @@
150
  position: relative;
151
  }
152
 
153
- /* ── SIDEBAR ────────────────────────────────── */
154
- .sidebar {
155
- width: var(--sidebar-w);
156
- background: var(--bg-depth);
157
- border-right: 1px solid var(--bdr-dim);
158
- display: flex;
159
- flex-direction: column;
160
- flex-shrink: 0;
161
- z-index: 15;
162
- transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
163
- }
164
-
165
- .sidebar.collapsed {
166
- transform: translateX(-100%);
167
- margin-right: calc(var(--sidebar-w) * -1);
168
- }
169
-
170
- /* Floating Tab Trigger */
171
- .sidebar-trigger {
172
- position: absolute;
173
- left: 0;
174
- top: 50%;
175
- transform: translateY(-50%);
176
- width: 28px;
177
- height: 80px;
178
- background: var(--bg-depth);
179
- border: 1px solid var(--bdr-accent);
180
- border-left: none;
181
- border-radius: 0 8px 8px 0;
182
- display: none;
183
- align-items: center;
184
- justify-content: center;
185
- cursor: pointer;
186
- z-index: 20;
187
- transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
188
- box-shadow: 4px 0 15px rgba(0, 0, 0, 0.5);
189
- }
190
-
191
- .sidebar-trigger:hover {
192
- background: var(--bg-active);
193
- width: 32px;
194
- box-shadow: 0 0 20px var(--accent-glow);
195
- }
196
-
197
- .sidebar-trigger span {
198
- writing-mode: vertical-lr;
199
- transform: rotate(180deg);
200
- font-size: 0.7rem;
201
- font-weight: 700;
202
- letter-spacing: 0.15em;
203
- text-transform: uppercase;
204
- color: var(--accent);
205
- white-space: nowrap;
206
- }
207
-
208
- .sidebar-head {
209
- height: 48px;
210
- display: flex;
211
- align-items: center;
212
- justify-content: space-between;
213
- padding: 0 16px;
214
- border-bottom: 1px solid var(--bdr-dim);
215
- background: rgba(255, 255, 255, 0.02);
216
- }
217
-
218
- .sidebar-title {
219
- font-family: var(--ff-display);
220
- font-size: 0.75rem;
221
- font-weight: 700;
222
- letter-spacing: 0.15em;
223
- text-transform: uppercase;
224
- color: var(--txt-primary);
225
- }
226
-
227
- .sidebar-content {
228
- flex: 1;
229
- overflow-y: auto;
230
- padding: 12px;
231
- }
232
-
233
- /* Market Overview List */
234
- .market-list {
235
- display: flex;
236
- flex-direction: column;
237
- gap: 1px;
238
- }
239
-
240
- .market-item {
241
- display: flex;
242
- align-items: center;
243
- gap: 12px;
244
- padding: 10px 12px;
245
- border-radius: var(--radius);
246
- cursor: pointer;
247
- transition: background 0.2s;
248
- }
249
-
250
- .market-item:hover {
251
- background: rgba(255, 255, 255, 0.05);
252
- }
253
-
254
- .market-item.active {
255
- background: var(--bg-active);
256
- border: 1px solid var(--bdr-accent);
257
- }
258
-
259
- .m-sym {
260
- flex: 1;
261
- display: flex;
262
- flex-direction: column;
263
- }
264
-
265
- .m-ticker {
266
- font-family: var(--ff-mono);
267
- font-size: 0.85rem;
268
- font-weight: 700;
269
- color: var(--txt-bright);
270
- }
271
-
272
- .m-name {
273
- font-size: 0.72rem;
274
- color: var(--txt-secondary);
275
- }
276
-
277
- .m-price-box {
278
- text-align: right;
279
- }
280
-
281
- .m-price {
282
- font-family: var(--ff-mono);
283
- font-size: 0.85rem;
284
- font-weight: 600;
285
- color: var(--txt-bright);
286
- }
287
-
288
- .m-change {
289
- font-size: 0.72rem;
290
- font-weight: 700;
291
- }
292
-
293
- .m-change.up { color: var(--ok); }
294
- .m-change.down { color: var(--err); }
295
 
296
 
297
  /* ═══════════════════════════════════════════════
@@ -407,9 +326,11 @@
407
  .ctrls {
408
  display: flex;
409
  align-items: center;
410
- gap: 12px;
411
  flex: 1;
412
  justify-content: flex-end;
 
 
413
  }
414
 
415
  /* Search Omnibox */
@@ -516,10 +437,22 @@
516
  border-radius: 50%;
517
  }
518
 
519
- .m-stat.open { color: var(--ok); }
520
- .m-stat.open .pulse { background: var(--ok); box-shadow: 0 0 8px var(--ok); }
521
- .m-stat.closed { color: var(--txt-muted); }
522
- .m-stat.closed .pulse { background: var(--txt-muted); }
 
 
 
 
 
 
 
 
 
 
 
 
523
 
524
 
525
  .ctrl-unit {
@@ -569,13 +502,13 @@
569
  }
570
 
571
  .k-select optgroup {
572
- background: #060d20;
573
  color: var(--txt-secondary);
574
  font-size: 0.75rem;
575
  }
576
 
577
  .k-select option {
578
- background: #060d20;
579
  color: var(--txt-bright);
580
  }
581
 
@@ -607,7 +540,7 @@
607
  .btn-primary {
608
  height: var(--ctrl-h);
609
  padding: 0 18px;
610
- background: linear-gradient(135deg, rgba(34, 211, 238, 0.18) 0%, rgba(34, 211, 238, 0.10) 100%);
611
  border: 1px solid var(--bdr-accent);
612
  color: var(--accent);
613
  border-radius: var(--radius);
@@ -617,11 +550,7 @@
617
  letter-spacing: 0.12em;
618
  text-transform: uppercase;
619
  cursor: pointer;
620
- transition:
621
- background 0.22s ease,
622
- box-shadow 0.22s ease,
623
- transform 0.14s ease,
624
- border-color 0.22s ease;
625
  white-space: nowrap;
626
  }
627
 
@@ -696,229 +625,493 @@
696
  width: 100%;
697
  height: 100%;
698
  padding: 0;
699
- background: rgba(2, 6, 14, 0.98);
700
- backdrop-filter: blur(50px) saturate(220%);
701
- -webkit-backdrop-filter: blur(50px) saturate(220%);
702
- display: none; /* FULL HIDE */
703
  flex-direction: column;
704
  overflow: hidden;
705
  animation: dash-in 0.5s cubic-bezier(0.16, 1, 0.3, 1) both;
706
  }
 
707
  .analysis-panel.active {
708
  display: flex;
709
  }
 
710
  @keyframes dash-in {
711
- from { opacity: 0; transform: scale(0.98); }
712
- to { opacity: 1; transform: scale(1); }
 
 
 
 
 
 
 
713
  }
714
 
715
  /* ── Dashboard Header ── */
716
  .dash-header {
717
- display: flex; align-items: center; justify-content: space-between;
718
- padding: 20px 40px; flex-shrink: 0;
719
- border-bottom: 1px solid rgba(255,255,255,0.08);
720
- background: linear-gradient(180deg, rgba(255,255,255,0.03) 0%, rgba(255,255,255,0.01) 100%);
 
 
 
 
 
 
 
 
 
721
  }
722
- .dash-header-left { display: flex; align-items: center; gap: 14px; }
723
  .dash-logo-dot {
724
- width: 16px; height: 16px; border-radius: 50%;
 
 
725
  background: linear-gradient(135deg, #22d3ee, #3b82f6);
726
- box-shadow: 0 0 14px rgba(34,211,238,0.6);
727
  animation: dot-breathe 2.5s ease-in-out infinite;
728
  }
 
729
  @keyframes dot-breathe {
730
- 0%,100% { box-shadow: 0 0 8px rgba(34,211,238,0.4); }
731
- 50% { box-shadow: 0 0 22px rgba(34,211,238,0.8); }
 
 
 
 
 
 
 
732
  }
 
733
  .dash-title {
734
- font-family: var(--ff-display); font-size: 1.6rem; font-weight: 800;
735
- color: #ffffff; letter-spacing: 0.12em; text-transform: uppercase;
 
 
 
 
736
  }
 
737
  .dash-subtitle {
738
- font-size: 1rem; color: rgba(255,255,255,0.5); margin-left: 15px;
 
 
739
  letter-spacing: 0.08em;
740
  }
 
741
  .dash-close {
742
- width: 40px; height: 40px; display: flex; align-items: center; justify-content: center;
743
- cursor: pointer; color: rgba(255,255,255,0.4); border-radius: 10px;
744
- border: 1px solid rgba(255,255,255,0.08); background: rgba(255,255,255,0.03);
 
 
 
 
 
 
 
745
  transition: all 0.2s;
746
  }
747
- .dash-close:hover { background: rgba(255,255,255,0.1); color: #fff; border-color: rgba(255,255,255,0.2); }
 
 
 
 
 
748
 
749
  /* ── Dashboard Body — top gauges + bottom tables ── */
750
  .dash-body {
751
- flex: 1; display: flex; flex-direction: column; overflow: hidden;
 
 
 
752
  }
753
 
754
  /* ── TOP: 3 Big Gauges Row ── */
755
  .dash-gauges-hero {
756
- display: flex; justify-content: center; align-items: stretch;
757
- gap: 0; flex-shrink: 0;
758
- border-bottom: 1px solid rgba(255,255,255,0.06);
 
 
 
759
  }
 
760
  .gauge-hero-card {
761
- flex: 1; min-width: 0;
762
- display: flex; flex-direction: column; align-items: center;
763
- justify-content: center; gap: 15px;
 
 
 
 
764
  padding: 40px 30px;
765
- border-right: 1px solid rgba(255,255,255,0.08);
766
  transition: all 0.4s cubic-bezier(0.16, 1, 0.3, 1);
767
  position: relative;
768
  }
769
- .gauge-hero-card:last-child { border-right: none; }
770
- .gauge-hero-card:hover { background: rgba(255,255,255,0.03); transform: translateY(-4px); }
 
 
 
 
 
 
 
 
771
  .gauge-hero-card.hero-total {
772
- background: linear-gradient(180deg, rgba(34, 211, 238, 0.08) 0%, rgba(2,6,14,0) 100%);
773
  }
 
774
  .gauge-hero-card.hero-total::before {
775
- content: ''; position: absolute; top: 0; left: 10%; right: 10%; height: 4px;
 
 
 
 
 
776
  background: linear-gradient(90deg, transparent, var(--accent), transparent);
777
  border-radius: 4px;
778
  box-shadow: 0 0 20px var(--accent-glow);
779
  }
780
 
781
  .gauge-hero-title {
782
- font-family: var(--ff-display); font-size: 1.3rem; font-weight: 800;
783
- color: rgba(255,255,255,0.9); text-transform: uppercase;
784
- letter-spacing: 0.22em; text-align: center;
 
 
 
 
785
  }
 
786
  .gauge-hero-title.title-total {
787
- font-size: 1.55rem; color: #ffffff;
788
- text-shadow: 0 0 35px rgba(34,211,238,0.5);
 
789
  }
 
790
  .gauge-hero-svg-wrap {
791
  position: relative;
792
- width: 320px; height: 220px;
793
- display: flex; align-items: center; justify-content: center;
 
 
 
794
  }
 
795
  .gauge-hero-value-box {
796
  position: absolute;
797
- top: 62%; left: 50%;
 
 
798
  transform: translate(-50%, -50%);
799
- display: flex; flex-direction: column; align-items: center;
 
 
 
800
  }
 
801
  .gauge-hero-value-num {
802
- font-family: var(--ff-display); font-size: 2.2rem; font-weight: 900;
803
- color: #fff; line-height: 1;
804
- text-shadow: 0 0 20px rgba(255,255,255,0.3);
 
 
 
805
  }
 
806
  .gauge-hero-value-label {
807
- font-size: 0.7rem; font-weight: 700; color: rgba(255,255,255,0.4);
808
- text-transform: uppercase; letter-spacing: 0.1em; margin-top: 4px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
809
  }
810
 
811
  .gauge-hero-signal {
812
- font-family: var(--ff-display); font-size: 1.8rem; font-weight: 900;
813
- text-transform: uppercase; letter-spacing: 0.15em;
814
- text-shadow: 0 0 20px rgba(0,0,0,0.5);
 
 
 
815
  margin-top: 5px;
816
  }
817
- .gauge-hero-signal.strong-buy { color: #22c55e; text-shadow: 0 0 15px rgba(34, 197, 94, 0.4); }
818
- .gauge-hero-signal.buy { color: #86efac; }
819
- .gauge-hero-signal.strong-sell { color: #ef4444; text-shadow: 0 0 15px rgba(239, 68, 68, 0.4); }
820
- .gauge-hero-signal.sell { color: #fca5a5; }
821
- .gauge-hero-signal.neutral { color: #cbd5e1; }
822
- .gauge-hero-signal.signal-total { font-size: 2.2rem; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
823
  .gauge-hero-counts {
824
- display: flex; gap: 20px; font-size: 0.8rem; color: rgba(255,255,255,0.4);
 
 
 
825
  letter-spacing: 0.02em;
826
  }
827
- .gauge-hero-counts .ghc-label { text-transform: uppercase; font-weight: 600; }
 
 
 
 
 
828
  .gauge-hero-counts .ghc-value {
829
- font-family: var(--ff-mono); font-weight: 900;
830
- font-size: 1.1rem; color: #ffffff; display: block;
 
 
 
831
  margin-top: 2px;
832
  }
833
 
834
  /* ── BOTTOM: Data Tables Grid ── */
835
  .dash-tables-row {
836
- flex: 1; display: grid; overflow: hidden;
 
 
837
  grid-template-columns: 1fr 1fr 1fr;
838
  gap: 0;
839
  }
 
840
  .dash-col {
841
- display: flex; flex-direction: column;
842
- border-right: 1px solid rgba(255,255,255,0.04);
 
843
  overflow: hidden;
844
  }
845
- .dash-col:last-child { border-right: none; }
 
 
 
846
 
847
  .dc-header {
848
- padding: 14px 20px; flex-shrink: 0;
849
- border-bottom: 1px solid rgba(255,255,255,0.06);
850
- background: rgba(255,255,255,0.018);
 
851
  font-family: var(--ff-display);
852
- font-size: 0.85rem; font-weight: 800; color: #ffffff;
853
- text-transform: uppercase; letter-spacing: 0.12em;
 
 
 
854
  }
855
 
856
  .dash-table-wrap {
857
- flex: 1; overflow-y: auto; overflow-x: hidden;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
858
  }
859
- .dash-table-wrap::-webkit-scrollbar { width: 3px; }
860
- .dash-table-wrap::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.08); border-radius: 10px; }
861
 
862
- .dt { width: 100%; border-collapse: collapse; }
863
  .dt td {
864
- padding: 9px 18px; font-size: 0.88rem;
865
- border-bottom: 1px solid rgba(255,255,255,0.03);
 
866
  }
867
- .dt td:first-child { color: rgba(255,255,255,0.8); font-weight: 500; }
 
 
 
 
 
868
  .dt .dt-val {
869
- font-family: var(--ff-mono); font-weight: 700; color: #ffffff; text-align: right;
 
 
 
870
  }
 
871
  .dt .dt-act {
872
- font-weight: 800; text-align: right; font-size: 0.85rem; letter-spacing: 0.03em;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
873
  }
874
- .dt-act-buy { color: #60a5fa !important; }
875
- .dt-act-sell { color: #fb7185 !important; }
876
- .dt-act-neut { color: rgba(255,255,255,0.3) !important; }
877
 
878
- .pivot-table { width: 100%; border-collapse: collapse; }
879
  .pivot-table th {
880
- padding: 8px 10px; font-size: 0.7rem; font-weight: 800;
881
- color: rgba(255,255,255,0.4); text-transform: uppercase;
882
- letter-spacing: 0.08em; text-align: center;
883
- border-bottom: 1px solid rgba(255,255,255,0.06);
 
 
 
 
884
  }
 
885
  .pivot-table td {
886
- padding: 7px 10px; font-family: var(--ff-mono); font-size: 0.85rem;
887
- font-weight: 600; color: #ffffff; text-align: center;
888
- border-bottom: 1px solid rgba(255,255,255,0.025);
 
 
 
 
889
  }
 
890
  .pivot-table td:first-child {
891
- font-weight: 800; color: rgba(255,255,255,0.55); font-family: var(--ff-ui);
892
- text-align: left; padding-left: 18px;
 
 
 
893
  }
894
 
895
  .summary-disclaimer {
896
  padding: 10px 20px;
897
- font-size: 0.72rem; color: rgba(255,255,255,0.25); line-height: 1.5;
898
- border-top: 1px solid rgba(255,255,255,0.04);
899
- text-align: center; flex-shrink: 0;
 
 
 
 
 
 
 
 
900
  }
901
- .summary-disclaimer strong { color: rgba(255,255,255,0.4); }
902
 
903
  /* ── Loading state ── */
904
  .dash-loading {
905
- display: flex; flex-direction: column; align-items: center; justify-content: center;
906
- height: 100%; gap: 16px; color: rgba(255,255,255,0.4);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
907
  }
908
- .dash-loading .loader-ring { border-top-color: var(--accent); }
909
- .dash-loading p { font-size: 0.9rem; letter-spacing: 0.05em; animation: loader-pulse 1.4s ease-in-out infinite; }
910
 
911
  /* Dashboard responsive */
912
  @media (max-width: 1200px) {
913
- .dash-tables-row { grid-template-columns: 1fr 1fr; }
914
- .dash-col.col-pivots { display: none; }
915
- .gauge-hero-card { padding: 20px 14px 18px; }
916
- .gauge-hero-svg-wrap { width: 180px; height: 108px; }
 
 
 
 
 
 
 
 
 
 
 
 
917
  }
 
918
  @media (max-width: 800px) {
919
- .dash-gauges-hero { flex-direction: column; }
920
- .gauge-hero-card { border-right: none; border-bottom: 1px solid rgba(255,255,255,0.05); }
921
- .dash-tables-row { grid-template-columns: 1fr; }
 
 
 
 
 
 
 
 
 
922
  }
923
 
924
  #chart {
@@ -929,19 +1122,25 @@
929
  z-index: 5;
930
  }
931
 
932
- /* Futuristic AI Overlay */
933
  .chart-bg-overlay {
934
  position: absolute;
935
  inset: 0;
936
  pointer-events: none;
937
  z-index: 1;
938
- opacity: 0.17;
939
- background-image: url('ai_analysis_bg.png');
940
  background-size: contain;
941
  background-repeat: no-repeat;
942
  background-position: left bottom;
943
- mask-image: linear-gradient(to right, black 20%, transparent 80%);
944
- -webkit-mask-image: linear-gradient(to right, black 20%, transparent 80%);
 
 
 
 
 
 
945
  }
946
 
947
  .chart-logo-overlay {
@@ -955,14 +1154,19 @@
955
  font-weight: 700;
956
  letter-spacing: 0.25em;
957
  line-height: 1;
958
- background: linear-gradient(180deg, #fff 0%, var(--accent) 50%, #0a4d5c 100%);
959
  -webkit-background-clip: text;
960
  -webkit-text-fill-color: transparent;
961
  background-clip: text;
962
- filter: drop-shadow(0 0 15px rgba(34, 211, 238, 0.45));
963
- opacity: 0.55;
964
  text-transform: uppercase;
965
  white-space: nowrap;
 
 
 
 
 
966
  }
967
 
968
  /* ── Terminal Loader Overlay (Strict Isolation) ── */
@@ -971,7 +1175,8 @@
971
  inset: 0 !important;
972
  width: 100% !important;
973
  height: 100% !important;
974
- display: none; /* FULL HIDE */
 
975
  flex-direction: column;
976
  align-items: center;
977
  justify-content: center;
@@ -1145,63 +1350,411 @@
1145
  border-radius: 2px;
1146
  }
1147
 
1148
- ::-webkit-scrollbar-thumb:hover {
1149
- background: var(--txt-muted);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1150
  }
1151
 
1152
- /* ═══════════════════════════════════════════════
1153
- RESPONSIVE
1154
- ═══════════════════════════════════════════════ */
1155
- @media (max-width: 960px) {
1156
- .ctrl-label {
1157
- display: none;
1158
- }
1159
-
1160
- .ctrl-unit {
1161
- flex-direction: row;
1162
- align-items: center;
1163
- }
1164
-
1165
- #symbolSelect {
1166
- width: 150px;
1167
- }
1168
 
1169
- .logo-sub {
1170
- display: none;
1171
- }
 
 
 
 
 
 
 
1172
  }
1173
 
1174
- @media (max-width: 680px) {
1175
- .hdr {
1176
- padding: 0 12px;
1177
- gap: 8px;
1178
- }
1179
 
1180
- .logo-sub {
1181
- display: none;
1182
- }
 
 
 
 
 
1183
 
1184
- .hdr-divider {
1185
- display: none;
1186
- }
 
 
 
 
 
 
 
 
1187
 
1188
- #symbolSelect {
1189
- width: 130px;
1190
- }
 
 
 
1191
 
1192
- #timeframeSelect {
1193
- width: 60px;
1194
- }
 
 
 
1195
 
1196
- #horizonInput {
1197
- width: 56px;
1198
- }
 
 
 
 
1199
 
1200
- .btn-primary {
1201
- padding: 0 12px;
1202
- font-size: 0.68rem;
1203
- letter-spacing: 0.06em;
1204
- }
 
1205
  }
1206
  </style>
1207
  </head>
@@ -1240,8 +1793,9 @@
1240
  <!-- Controls -->
1241
  <div class="ctrls">
1242
 
1243
- <button class="btn-icon" id="toggleMarketBtn" title="Ẩn/Hiện Thị trường">
1244
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
 
1245
  <line x1="3" y1="12" x2="21" y2="12"></line>
1246
  <line x1="3" y1="6" x2="21" y2="6"></line>
1247
  <line x1="3" y1="18" x2="21" y2="18"></line>
@@ -1250,7 +1804,11 @@
1250
 
1251
  <!-- Omnibox Search -->
1252
  <div class="omnibox">
1253
- <i><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg></i>
 
 
 
 
1254
  <input type="text" id="symbolSearch" placeholder="Tìm kiếm mã (VD: Gold, BTC, AAPL...)" autocomplete="off">
1255
  <div class="search-results" id="searchResults"></div>
1256
  </div>
@@ -1283,6 +1841,21 @@
1283
  </select>
1284
  </div>
1285
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1286
  <button class="btn-primary" id="refreshBtn">Phân tích</button>
1287
 
1288
 
@@ -1299,26 +1872,6 @@
1299
 
1300
  <!-- ── BODY ──────────────────────────────── -->
1301
  <div class="app-body">
1302
- <div class="sidebar-trigger" id="reopenSidebarBtn" title="Mở danh sách thị trường">
1303
- <span>Thị trường</span>
1304
- </div>
1305
-
1306
- <!-- Side Sidebar (Market Overview) -->
1307
- <aside class="sidebar" id="sidebar">
1308
- <div class="sidebar-head">
1309
- <span class="sidebar-title">Thị trường</span>
1310
- <button class="btn-icon" id="toggleSidebarBtn" style="width: 28px; height: 28px; border: none; background: transparent;">
1311
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
1312
- <polyline points="15 18 9 12 15 6"></polyline>
1313
- </svg>
1314
- </button>
1315
- </div>
1316
- <div class="sidebar-content">
1317
- <div class="market-list" id="marketOverview">
1318
- <!-- Market items will be injected here -->
1319
- </div>
1320
- </div>
1321
- </aside>
1322
 
1323
  <!-- Main Area -->
1324
  <main class="main">
@@ -1346,10 +1899,44 @@
1346
  <!-- Chart canvas -->
1347
  <div id="chart"></div>
1348
 
1349
- <aside class="analysis-panel hidden" id="analysisPanel"></aside>
 
1350
 
 
1351
  </main>
1352
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1353
  </div>
1354
 
1355
  <!-- ══════════════════════════════════════════════
@@ -1374,9 +1961,6 @@
1374
  const terminalLoader = document.getElementById('terminalLoader');
1375
  const loaderText = document.getElementById('loaderText');
1376
  const analysisPanel = document.getElementById('analysisPanel');
1377
- const sidebar = document.getElementById('sidebar');
1378
- const toggleSidebarBtn = document.getElementById('toggleSidebarBtn');
1379
- const marketOverview = document.getElementById('marketOverview');
1380
  const marketStatusBar = document.getElementById('marketStatusBar');
1381
  const indicatorSelect = document.getElementById('indicatorSelect');
1382
 
@@ -1401,7 +1985,7 @@
1401
 
1402
  const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
1403
  const wsUrl = `${API_BASE.replace(/^https?:\/\//, wsProtocol)}/ws/price/${symbol}`;
1404
-
1405
  console.log(`[WS] Connecting to ${wsUrl}`);
1406
  ws = new WebSocket(wsUrl);
1407
 
@@ -1409,6 +1993,12 @@
1409
  const data = JSON.parse(event.data);
1410
  if (data.error) return;
1411
 
 
 
 
 
 
 
1412
  // Update current candle if price changed
1413
  const lastCandle = lastCandleData;
1414
  if (lastCandle && data.price) {
@@ -1423,8 +2013,6 @@
1423
  lastCandleData = update;
1424
  }
1425
 
1426
- // Update overview items if they exist
1427
- updateOverviewItem(data);
1428
  };
1429
 
1430
  ws.onclose = () => {
@@ -1453,43 +2041,6 @@
1453
  }
1454
  }
1455
 
1456
- async function refreshOverview() {
1457
- try {
1458
- const symbol = currentSymbol || 'BTCUSD';
1459
- const data = await apiRequest(`/api/market-peers?symbol=${symbol}`);
1460
- renderOverview(`${data.category.toUpperCase()} LIÊN QUAN`, data.peers);
1461
- } catch (e) {
1462
- console.warn('Overview fetch failed', e);
1463
- }
1464
- }
1465
-
1466
- function renderOverview(categoryLabel, peers) {
1467
- marketOverview.innerHTML = `
1468
- <div style="font-size:0.65rem; color:var(--txt-muted); margin-bottom:8px; padding-left:12px;">${categoryLabel}</div>
1469
- ${peers.map(p => `
1470
- <div class="market-item ${p.symbol === currentSymbol ? 'active' : ''}" onclick="switchSymbol('${p.symbol}')" id="overview-${p.symbol}">
1471
- <div class="m-sym">
1472
- <span class="m-ticker">${p.symbol}</span>
1473
- <span class="m-name">${p.name}</span>
1474
- </div>
1475
- <div class="m-price-box">
1476
- <div class="m-price">${formatPrice(p.price)}</div>
1477
- <div class="m-change ${p.change_24h >= 0 ? 'up' : 'down'}">${formatPct(p.change_24h)}</div>
1478
- </div>
1479
- </div>
1480
- `).join('')}
1481
- `;
1482
- }
1483
-
1484
- function updateOverviewItem(data) {
1485
- const itemPrice = document.querySelector(`#overview-${data.symbol} .m-price`);
1486
- const itemChange = document.querySelector(`#overview-${data.symbol} .m-change`);
1487
- if (itemPrice) itemPrice.textContent = formatPrice(data.price);
1488
- if (itemChange) {
1489
- itemChange.textContent = formatPct(data.change_pct);
1490
- itemChange.className = `m-change ${data.change_pct >= 0 ? 'up' : 'down'}`;
1491
- }
1492
- }
1493
 
1494
  /* ── Search Logic ───────────────────────────── */
1495
  let searchTimeout = null;
@@ -1517,8 +2068,13 @@
1517
  }
1518
  searchResults.innerHTML = results.map(r => `
1519
  <div class="search-item" onclick="switchSymbol('${r.symbol}')">
1520
- <span class="sym">${r.symbol}</span>
1521
- <span class="name">${r.label}</span>
 
 
 
 
 
1522
  </div>
1523
  `).join('');
1524
  searchResults.classList.add('visible');
@@ -1529,18 +2085,16 @@
1529
  currentSymbol = symbol;
1530
  symbolSearch.value = symbol;
1531
  searchResults.classList.remove('visible');
1532
-
1533
  // Update labels immediately for better UX responsiveness
1534
  const sLabel = symbolMap.get(symbol) || symbol;
1535
  updateStatus(`${sLabel} · ${currentInterval} — Đang nạp...`, 'loading');
1536
 
1537
- // Update active state in sidebar
1538
- document.querySelectorAll('.market-item').forEach(el => el.classList.remove('active'));
1539
- const activeItem = document.getElementById(`overview-${symbol}`);
1540
- if (activeItem) activeItem.classList.add('active');
1541
 
 
1542
  await refreshChart();
1543
- refreshOverview();
1544
  connectWS(symbol);
1545
  }
1546
 
@@ -1650,16 +2204,34 @@
1650
  function showLoader(msg = 'Đang tải dữ liệu') {
1651
  loaderText.textContent = msg;
1652
  terminalLoader.classList.add('active');
 
 
1653
  }
1654
 
1655
  function hideLoader() {
1656
  terminalLoader.classList.remove('active');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1657
  }
1658
 
1659
- function updateStatus(msg, type = '') {
1660
- statusText.textContent = msg;
1661
- statusDot.className = 'dot ' + type;
1662
- statusPill.className = 'status-pill ' + type;
 
 
1663
  }
1664
 
1665
  function formatPrice(value) {
@@ -1679,6 +2251,113 @@
1679
  return `${Number(value).toFixed(2)}R`;
1680
  }
1681
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1682
  /* ── State for cached analysis data ──────────── */
1683
  let lastAnalysisPayload = null;
1684
  let lastAnalysisSymbol = null;
@@ -1709,9 +2388,9 @@
1709
  return;
1710
  }
1711
 
1712
- const osc = a.oscillators || { sell:0, neutral:0, buy:0, signal:'--', data:[] };
1713
- const ma = a.moving_averages || { sell:0, neutral:0, buy:0, signal:'--', data:[] };
1714
- const summary = a.summary || { sell:0, neutral:0, buy:0, signal:'--' };
1715
  const pivots = (a.pivot_points || {}).data || [];
1716
 
1717
  // ── AI Forecast calculation ──
@@ -1748,69 +2427,23 @@
1748
  // ── TỔNG KẾT = (Phân tích kỹ thuật + Dự báo AI) / 2 ──
1749
  const techTotal = (summary.buy + summary.sell + summary.neutral) || 1;
1750
  const techScore = (summary.buy - summary.sell) / techTotal; // -1 to +1
1751
- const aiScore = forecastPctChange > 0.5 ? Math.min(forecastPctChange / 5, 1) :
1752
- forecastPctChange < -0.5 ? Math.max(forecastPctChange / 5, -1) : 0;
1753
- const combinedScore = (techScore + aiScore) / 2; // -1 to +1
1754
 
1755
- // Map combined score to buy/sell/neutral counts for gauge
1756
- let totalBuy = 0, totalSell = 0, totalNeutral = 0;
1757
- if (combinedScore > 0.15) { totalBuy = Math.round(Math.abs(combinedScore) * 10); totalNeutral = 10 - totalBuy; }
1758
- else if (combinedScore < -0.15) { totalSell = Math.round(Math.abs(combinedScore) * 10); totalNeutral = 10 - totalSell; }
1759
- else { totalNeutral = 10; }
1760
 
1761
  let totalSignal = 'Trung lập';
1762
  if (combinedScore > 0.4) totalSignal = 'Mua mạnh';
1763
- else if (combinedScore > 0.15) totalSignal = 'Mua';
1764
  else if (combinedScore < -0.4) totalSignal = 'Bán mạnh';
1765
- else if (combinedScore < -0.15) totalSignal = 'Bán';
1766
-
1767
- // ── Big SVG Gauge builder (Fix Clipping + Add Value) ──
1768
- function buildBigGauge(sell, neutral, buy, signal, w=320, h=220) {
1769
- const total = sell + neutral + buy || 1;
1770
- const score = (buy - sell) / total;
1771
- const displayValue = Math.round((score + 1) * 50);
1772
- const angle = score * 135;
1773
- const cx = w/2, cy = h * 0.52, r = (w/2) * 0.72; // Adjusted r and cy to prevent clipping
1774
- const strokeW = 18;
1775
- function arc(s, e, col) {
1776
- const sa = (s-90)*Math.PI/180, ea = (e-90)*Math.PI/180;
1777
- const x1=cx+r*Math.cos(sa), y1=cy+r*Math.sin(sa), x2=cx+r*Math.cos(ea), y2=cy+r*Math.sin(ea);
1778
- return `<path d="M${x1},${y1} A${r},${r} 0 ${(e-s)>180?1:0} 1 ${x2},${y2}" fill="none" stroke="${col}" stroke-width="${strokeW}" stroke-linecap="round" opacity="0.8"/>`;
1779
- }
1780
- // Needle
1781
- const na = (angle-90)*Math.PI/180, nl = r + 5;
1782
- const nx = cx+nl*Math.cos(na), ny = cy+nl*Math.sin(na);
1783
- // Needle dynamic color
1784
- const needleColor = score > 0.3 ? '#4ade80' : score > 0.1 ? '#bbf7d0' : score < -0.3 ? '#f87171' : score < -0.1 ? '#fecaca' : '#f1f5f9';
1785
-
1786
- return `
1787
- <div class="gauge-hero-value-box">
1788
- <div class="gauge-hero-value-num">${displayValue}</div>
1789
- <div class="gauge-hero-value-label">Chỉ số</div>
1790
- </div>
1791
- <svg width="${w}" height="${h}" viewBox="0 0 ${w} ${h}">
1792
- <defs>
1793
- <filter id="needle-glow"><feGaussianBlur stdDeviation="6" result="blur"/><feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge></filter>
1794
- </defs>
1795
- <!-- Arc segments -->
1796
- ${arc(-135,-81,'#ef4444')}${arc(-81,-27,'#fca5a5')}${arc(-27,27,'#f1f5f9')}${arc(27,81,'#bbf7d0')}${arc(81,135,'#22c55e')}
1797
- <!-- Labels -->
1798
- <text x="${cx + r*Math.cos((-135-90)*Math.PI/180)*1.35}" y="${cy + r*Math.sin((-135-90)*Math.PI/180)*1.35}" fill="rgba(255,255,255,0.3)" font-size="10" text-anchor="middle" font-family="var(--ff-display)" font-weight="900">BÁN</text>
1799
- <text x="${cx + r*Math.cos((135-90)*Math.PI/180)*1.35}" y="${cy + r*Math.sin((135-90)*Math.PI/180)*1.35}" fill="rgba(255,255,255,0.3)" font-size="10" text-anchor="middle" font-family="var(--ff-display)" font-weight="900">MUA</text>
1800
- <!-- Needle -->
1801
- <line x1="${cx}" y1="${cy}" x2="${nx}" y2="${ny}" stroke="${needleColor}" stroke-width="7" stroke-linecap="round" filter="url(#needle-glow)"/>
1802
- <circle cx="${cx}" cy="${cy}" r="14" fill="${needleColor}" />
1803
- <circle cx="${cx}" cy="${cy}" r="7" fill="#fff" />
1804
- </svg>
1805
- `;
1806
- }
1807
 
1808
  function signalClass(signal) {
1809
- if (signal.includes('Mua mạnh')) return 'strong-buy';
1810
- if (signal.includes('Mua')) return 'buy';
1811
- if (signal.includes('Bán mạnh')) return 'strong-sell';
1812
- if (signal.includes('Bán')) return 'sell';
1813
- return 'neutral';
1814
  }
1815
 
1816
  function actCls(act) {
@@ -1830,7 +2463,7 @@
1830
  <div class="dash-header-left">
1831
  <div class="dash-logo-dot"></div>
1832
  <span class="dash-title">${sLabel}</span>
1833
- <span class="dash-subtitle">· ${tLabel} · Bảng Phân tích Tổng hợp</span>
1834
  </div>
1835
  <div class="dash-close" id="dashCloseBtn" title="Đóng">
1836
  <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
@@ -1846,11 +2479,11 @@
1846
  <!-- TOP ROW: 3 Big Gauges -->
1847
  <div class="dash-gauges-hero">
1848
 
1849
- <!-- Gauge 1: Phân tích kỹ thuật (was TỔNG KẾT) -->
1850
  <div class="gauge-hero-card">
1851
  <div class="gauge-hero-title">PHÂN TÍCH KỸ THUẬT</div>
1852
  <div class="gauge-hero-svg-wrap">
1853
- ${buildBigGauge(summary.sell, summary.neutral, summary.buy, summary.signal)}
1854
  </div>
1855
  <div class="gauge-hero-signal ${signalClass(summary.signal)}">${summary.signal}</div>
1856
  <div class="gauge-hero-counts">
@@ -1860,20 +2493,34 @@
1860
  </div>
1861
  </div>
1862
 
1863
- <!-- Gauge 2: Dự báo AI (Buy/Sell gauge) -->
1864
  <div class="gauge-hero-card">
1865
  <div class="gauge-hero-title">DỰ BÁO AI</div>
1866
  <div class="gauge-hero-svg-wrap">
1867
- ${buildBigGauge(aiSell, aiNeutral, aiBuy, aiSignal)}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1868
  </div>
1869
  <div class="gauge-hero-signal ${signalClass(aiSignal)}">${aiSignal}</div>
1870
  </div>
1871
 
1872
- <!-- Gauge 3: TỔNG KẾT = (Phân tích KT + Dự báo AI) / 2 -->
1873
  <div class="gauge-hero-card hero-total">
1874
  <div class="gauge-hero-title title-total">⚡ TỔNG KẾT</div>
1875
  <div class="gauge-hero-svg-wrap">
1876
- ${buildBigGauge(totalSell, totalNeutral, totalBuy, totalSignal)}
1877
  </div>
1878
  <div class="gauge-hero-signal signal-total ${signalClass(totalSignal)}">${totalSignal}</div>
1879
  </div>
@@ -1936,7 +2583,7 @@
1936
  p50Series.applyOptions({ visible: false });
1937
  p10Series.applyOptions({ visible: false });
1938
  p90Series.applyOptions({ visible: false });
1939
-
1940
  // Clear indicators
1941
  bbMiddleSeries.setData([]);
1942
  bbUpperSeries.setData([]);
@@ -1945,6 +2592,9 @@
1945
 
1946
  // Hide dashboard and clear cached data on symbol switch
1947
  analysisPanel.classList.remove('active');
 
 
 
1948
  lastAnalysisPayload = null;
1949
  lastAnalysisSymbol = null;
1950
  chart.priceScale('right').applyOptions({ autoScale: true });
@@ -1964,7 +2614,7 @@
1964
  async function apiRequest(path) {
1965
  const sep = path.includes('?') ? '&' : '?';
1966
  const url = `${API_BASE}${path}${sep}_t=${Date.now()}`;
1967
-
1968
  const options = { method: 'GET' };
1969
  if (fetchController) options.signal = fetchController.signal;
1970
 
@@ -1982,9 +2632,8 @@
1982
  async function loadSymbols() {
1983
  try {
1984
  const data = await apiRequest('/api/symbols');
 
1985
  data.symbols.forEach(s => symbolMap.set(s.symbol, s.label));
1986
- // Default overview refresh
1987
- refreshOverview();
1988
  } catch (e) {
1989
  updateStatus('Lỗi nạp metadata: ' + e.message, 'error');
1990
  }
@@ -1994,6 +2643,7 @@
1994
  async function refreshChart() {
1995
  const symbol = currentSymbol;
1996
  const interval = timeframeSelect.value;
 
1997
  const horizon = Math.max(5, Math.min(300, parseInt(horizonInput.value) || 10));
1998
  if (!symbol) return;
1999
 
@@ -2010,10 +2660,11 @@
2010
  /* Update chart watermark */
2011
  const sLabel = symbolMap.get(symbol) || symbol;
2012
  const tLabel = timeframeMap[interval] || interval;
 
2013
  chart.applyOptions({
2014
  watermark: {
2015
  text: `${sLabel} · ${tLabel}`,
2016
- color: 'rgba(34, 211, 238, 0.27)',
2017
  fontSize: 72,
2018
  },
2019
  });
@@ -2036,7 +2687,7 @@
2036
  const type = indicatorSelect.value;
2037
  const indicators = indData.indicators || {};
2038
  const series = indicators.series || {};
2039
-
2040
  if (type === 'bb' || type === 'both') {
2041
  if (series.bb_upper) bbUpperSeries.setData(series.bb_upper);
2042
  if (series.bb_mid) bbMiddleSeries.setData(series.bb_mid);
@@ -2044,7 +2695,7 @@
2044
  }
2045
 
2046
  // Add RSI update if needed
2047
-
2048
  chart.timeScale().fitContent();
2049
  hideLoader();
2050
  updateStatus(`${symbol} · ${interval} | Đang nạp AI...`, 'loading');
@@ -2065,7 +2716,7 @@
2065
  /* ── Stage 2: Background AI Forecast ───────────────── */
2066
  async function fetchAIAnalysis(symbol, interval) {
2067
  if (currentSymbol !== symbol) return; // Prevent race conditions
2068
-
2069
  const panel = document.getElementById('analysisPanel');
2070
  panel.innerHTML = `
2071
  <div class="dash-loading">
@@ -2073,14 +2724,15 @@
2073
  <p>AI đang tính toán...</p>
2074
  </div>
2075
  `;
2076
-
2077
  try {
2078
  const horizon = horizonInput.value || 24;
2079
  const fData = await apiRequest(`/api/forecast/${encodeURIComponent(symbol)}?interval=${interval}&horizon=${horizon}`);
2080
-
2081
  if (currentSymbol !== symbol) return; // Discard if user switched
2082
-
2083
  renderAnalysisPanel(symbol, interval, fData);
 
2084
 
2085
  let isBull = false;
2086
  let lastForecastVal = 0;
@@ -2091,19 +2743,23 @@
2091
  // Safeguard: Ensure we have candle data before aligning
2092
  if (!lastCandleData || !lastCandleData.time) {
2093
  updateStatus(`${symbol} · ${interval} — AI Sẵn sàng`, 'ok');
2094
- return;
2095
  }
2096
 
2097
- // Alignment Fix: Prepend the current price candle to the forecast data
2098
- const currentPoint = { time: lastCandleData.time, value: lastCandleData.close };
2099
-
2100
- const p50 = [currentPoint, ...fData.forecast.map(d => ({ time: d.time, value: d.p50 }))];
2101
- const p10 = [currentPoint, ...fData.forecast.map(d => ({ time: d.time, value: d.p10 }))];
2102
- const p90 = [currentPoint, ...fData.forecast.map(d => ({ time: d.time, value: d.p90 }))];
 
 
 
 
2103
 
2104
- const first = p50[0]?.value ?? 0;
2105
- lastForecastVal = p50[p50.length - 1]?.value ?? 0;
2106
- isBull = lastForecastVal >= first;
2107
 
2108
  const aiMain = isBull ? '#1dba8a' : '#e05560';
2109
  const aiBand = isBull ? 'rgba(29,186,138,0.1)' : 'rgba(224,85,96,0.1)';
@@ -2111,18 +2767,19 @@
2111
  p50Series.applyOptions({ color: aiMain, visible: true });
2112
  p10Series.applyOptions({ color: aiBand, visible: true });
2113
  p90Series.applyOptions({ color: aiBand, visible: true });
2114
-
2115
- p50Series.setData(p50);
2116
- p10Series.setData(p10);
2117
- p90Series.setData(p90);
2118
  }
2119
 
 
 
 
2120
  const trend = isBull ? '↑ TĂNG' : '↓ GIẢM';
2121
  const confidence = fData.analysis?.confidence ?? fData.ensemble?.confidence ?? 0;
 
 
2122
  if (currentSymbol !== symbol || timeframeSelect.value !== interval) return;
2123
-
2124
  updateStatus(
2125
- `${symbol} · ${interval} | AI: ${trend} (${Math.round(confidence)}%) | Giá: ${formatPrice(lastCandleData?.close)}`,
2126
  'ok'
2127
  );
2128
 
@@ -2135,23 +2792,99 @@
2135
  }
2136
 
2137
  /* ── Event listeners ───────────────────────── */
2138
- const reopenSidebarBtn = document.getElementById('reopenSidebarBtn');
2139
- const toggleMarketBtn = document.getElementById('toggleMarketBtn');
2140
-
2141
- function toggleSidebar(forceOpen = null) {
2142
- const isCollapsed = sidebar.classList.contains('collapsed');
2143
- const targetState = (forceOpen !== null) ? !forceOpen : !isCollapsed;
2144
-
2145
- sidebar.classList.toggle('collapsed', targetState);
2146
- reopenSidebarBtn.style.display = targetState ? 'flex' : 'none';
2147
-
2148
- setTimeout(() => chart.applyOptions({ width: chartEl.clientWidth }), 300);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2149
  }
2150
 
2151
- // Unified toggle handlers
2152
- toggleSidebarBtn.onclick = () => toggleSidebar(false); // Close
2153
- reopenSidebarBtn.onclick = () => toggleSidebar(true); // Open
2154
- toggleMarketBtn.onclick = () => toggleSidebar(); // Toggle
 
 
 
2155
 
2156
  timeframeSelect.onchange = refreshChart;
2157
  indicatorSelect.onchange = refreshChart;
@@ -2183,11 +2916,22 @@
2183
 
2184
  fitBtn.onclick = () => chart.timeScale().fitContent();
2185
 
2186
- /* ── Click out to close search ─────────────── */
 
 
 
 
 
 
 
 
2187
  document.addEventListener('click', (e) => {
2188
  if (!symbolSearch.contains(e.target) && !searchResults.contains(e.target)) {
2189
  searchResults.classList.remove('visible');
2190
  }
 
 
 
2191
  });
2192
 
2193
  /* ── Resize handler ────────────────────────── */
@@ -2199,19 +2943,69 @@
2199
  });
2200
  ro.observe(chartEl);
2201
 
2202
- /* ── Bootstrap ─────────────────────────────── */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2203
  (async () => {
 
 
 
 
2204
  await loadSymbols();
2205
  await refreshMarketStatus();
2206
-
2207
  // Start polling
2208
  setInterval(refreshMarketStatus, 60000); // 1m
2209
- setInterval(refreshOverview, 15000); // 15s
2210
 
2211
  // Default start
2212
  await switchSymbol('XAUUSD');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2213
  })();
2214
  </script>
2215
  </body>
2216
 
2217
- </html>
 
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
  <title>Super AI Analysis · AI phân tích biểu đồ nến số 1 thế giới hiện tại được nghiên cứu bởi đại học Thanh Hoa!
8
  </title>
9
+ <meta name="description"
10
+ content="Kronos AI - Nền tảng phân tích kỹ thuật và dự báo thị trường tài chính bằng AI hàng đầu thế giới. Hỗ trợ Crypto, Forex, Chứng khoán và Kim loại." />
11
+ <meta property="og:title" content="Kronos AI Trading Terminal" />
12
+ <meta property="og:description"
13
+ content="Hệ thống phân tích biểu đồ nến và dự báo xu hướng sử dụng trí tuệ nhân tạo." />
14
+ <meta property="og:image" content="/ai_analysis_bg.png" />
15
  <link rel="preconnect" href="https://fonts.googleapis.com">
16
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
17
  <link
 
19
  rel="stylesheet">
20
  <script src="https://unpkg.com/lightweight-charts@4.2.2/dist/lightweight-charts.standalone.production.js"></script>
21
  <style>
22
+ /* ── Chart Transition (P2) ── */
23
+ #chart {
24
+ transition: opacity 0.4s cubic-bezier(0.4, 0, 0.2, 1), filter 0.4s ease;
25
+ }
26
+
27
+ #chart.loading {
28
+ opacity: 0.35;
29
+ filter: blur(4px);
30
+ pointer-events: none;
31
+ }
32
+
33
  /* ═══════════════════════════════════════════════
34
  DESIGN TOKENS
35
  ═══════════════════════════════════════════════ */
36
  :root {
37
+ /* --- LIGHT MODE (DEFAULT - PREMIUM) --- */
38
+ --bg-base: #f8fafc;
39
+ --bg-depth: #ffffff;
40
+ --bg-panel: rgba(255, 255, 255, 0.9);
41
+ --bg-glass: rgba(255, 255, 255, 0.85);
42
+ --bg-glass-light: rgba(248, 250, 252, 0.75);
43
+ --bg-control: #ffffff;
44
+ --bg-control-hov: #f1f5f9;
45
+ --bg-active: rgba(14, 165, 233, 0.08);
46
+ --bg-sidebar: #f1f5f9;
47
+
48
+ /* Borders */
49
+ --bdr-dim: rgba(0, 0, 0, 0.04);
50
+ --bdr-base: rgba(0, 0, 0, 0.07);
51
+ --bdr-muted: rgba(0, 0, 0, 0.12);
52
+ --bdr-accent: rgba(14, 165, 233, 0.4);
53
+
54
+ /* Text hierarchy */
55
+ --txt-bright: #0f172a;
56
+ --txt-primary: #1e293b;
57
+ --txt-secondary: #475569;
58
+ --txt-muted: #94a3b8;
59
+
60
+ /* Accent — modern blue */
61
+ --accent: #0ea5e9;
62
+ --accent-lo: rgba(14, 165, 233, 0.06);
63
+ --accent-mid: rgba(14, 165, 233, 0.18);
64
+ --accent-glow: rgba(14, 165, 233, 0.25);
65
+
66
+ /* Semantic */
67
+ --ok: #10b981;
68
+ --err: #f43f5e;
69
+ --warn: #f59e0b;
70
+
71
+ /* Chart series */
72
+ --bull: #10b981;
73
+ --bear: #f43f5e;
74
+
75
+ /* Typography */
76
+ --ff-display: 'Barlow', 'Chakra Petch', sans-serif;
77
+ --ff-ui: 'Barlow', system-ui, sans-serif;
78
+ --ff-mono: 'Space Mono', monospace;
79
+
80
+ /* Layout */
81
+ --header-h: 60px;
82
+ --sidebar-w: 280px;
83
+ --radius: 8px;
84
+ --radius-lg: 18px;
85
+ --ctrl-h: 38px;
86
+ --ok-glow: rgba(16, 185, 129, 0.3);
87
+ --err-glow: rgba(244, 63, 94, 0.3);
88
+ --glow-accent: 0 0 15px rgba(14, 165, 233, 0.25);
89
+ --shadow-lg: 0 20px 50px rgba(0, 0, 0, 0.08), 0 0 0 1px rgba(0, 0, 0, 0.02);
90
+
91
+ /* Dashboard specificity */
92
+ --glass-cyan: rgba(255, 255, 255, 0.9);
93
+ --neon-cyan: #0ea5e9;
94
+ --neon-pink: #ec4899;
95
+ --neon-green: #10b981;
96
+ --neon-blue: #3b82f6;
97
+ }
98
+
99
+ body.dark-theme {
100
+ /* --- DARK MODE (PRESERVED) --- */
101
  --bg-base: #02060c;
102
  --bg-depth: #010409;
103
+ --bg-panel: rgba(2, 6, 14, 0.9);
104
+ --bg-glass: rgba(2, 8, 16, 0.8);
105
  --bg-glass-light: rgba(4, 12, 24, 0.55);
106
  --bg-control: rgba(6, 16, 36, 0.85);
107
  --bg-control-hov: rgba(12, 28, 58, 0.95);
108
  --bg-active: rgba(34, 211, 238, 0.12);
109
+ --bg-sidebar: rgba(1, 4, 12, 0.65);
110
 
 
111
  --bdr-dim: rgba(40, 80, 140, 0.15);
112
  --bdr-base: rgba(50, 95, 165, 0.25);
113
  --bdr-muted: rgba(50, 100, 175, 0.38);
114
  --bdr-accent: rgba(34, 211, 238, 0.45);
115
 
 
116
  --txt-bright: #ffffff;
117
+ --txt-primary: #f1f5f9;
118
+ --txt-secondary: #cbd5e1;
119
+ --txt-muted: #64748b;
120
 
 
121
  --accent: #22d3ee;
122
  --accent-lo: rgba(34, 211, 238, 0.08);
123
  --accent-mid: rgba(34, 211, 238, 0.25);
124
  --accent-glow: rgba(34, 211, 238, 0.40);
125
 
 
126
  --ok: #2dd4bf;
127
  --err: #fb7185;
128
  --warn: #fbbf24;
 
 
129
  --bull: #2dd4bf;
130
  --bear: #fb7185;
131
 
132
+ --glow-accent: 0 0 20px rgba(34, 211, 238, 0.4);
 
 
 
 
 
 
 
 
 
 
 
133
  --glass-cyan: rgba(6, 18, 42, 0.85);
134
+ --shadow-lg: 0 30px 60px rgba(0, 0, 0, 0.8), 0 0 0 1px var(--bdr-accent);
 
 
 
135
  }
136
 
137
  /* ═══════════════════════════════════════════════
 
156
  font-size: 13px;
157
  -webkit-font-smoothing: antialiased;
158
  -moz-osx-font-smoothing: grayscale;
159
+ transition: background 0.3s ease, color 0.3s ease;
160
  }
161
 
162
  /* ═══════════════════════════════════════════════
163
+ BACKGROUND SYSTEM (Adaptive)
164
+ ═══════════════════════════════════════════════ */
165
+ body::before {
166
+ content: "";
167
+ position: fixed;
168
+ inset: 0;
169
+ z-index: -1;
170
+ background: url("Light_BG.png");
171
+ background-size: cover;
172
+ background-position: center;
173
+ background-repeat: no-repeat;
174
+ opacity: 0.01; /* 1% opacity for the image as requested */
175
+ transition: background 0.3s ease, opacity 0.3s ease;
176
+ }
177
+
178
+ body.dark-theme::before {
179
+ background: url("Dark_BG.png");
180
+ background-size: cover;
181
+ background-position: center;
182
+ background-repeat: no-repeat;
183
+ opacity: 0.01;
184
+ }
185
+
186
  body {
187
+ background: var(--bg-base);
188
+ color: var(--txt-primary);
189
+ font-family: var(--ff-ui);
190
+ font-size: 13px;
191
+ -webkit-font-smoothing: antialiased;
192
+ -moz-osx-font-smoothing: grayscale;
193
+ transition: background 0.3s ease, color 0.3s ease;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
194
  }
195
 
196
  /* ═══════════════════════════════════════════════
 
211
  position: relative;
212
  }
213
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
214
 
215
 
216
  /* ═══════════════════════════════════════════════
 
326
  .ctrls {
327
  display: flex;
328
  align-items: center;
329
+ gap: 10px;
330
  flex: 1;
331
  justify-content: flex-end;
332
+ flex-wrap: nowrap;
333
+ /* Keep tight, but handle overflow via media queries */
334
  }
335
 
336
  /* Search Omnibox */
 
437
  border-radius: 50%;
438
  }
439
 
440
+ .m-stat.open {
441
+ color: var(--ok);
442
+ }
443
+
444
+ .m-stat.open .pulse {
445
+ background: var(--ok);
446
+ box-shadow: 0 0 8px var(--ok);
447
+ }
448
+
449
+ .m-stat.closed {
450
+ color: var(--txt-muted);
451
+ }
452
+
453
+ .m-stat.closed .pulse {
454
+ background: var(--txt-muted);
455
+ }
456
 
457
 
458
  .ctrl-unit {
 
502
  }
503
 
504
  .k-select optgroup {
505
+ background: var(--bg-depth);
506
  color: var(--txt-secondary);
507
  font-size: 0.75rem;
508
  }
509
 
510
  .k-select option {
511
+ background: var(--bg-depth);
512
  color: var(--txt-bright);
513
  }
514
 
 
540
  .btn-primary {
541
  height: var(--ctrl-h);
542
  padding: 0 18px;
543
+ background: var(--accent-lo);
544
  border: 1px solid var(--bdr-accent);
545
  color: var(--accent);
546
  border-radius: var(--radius);
 
550
  letter-spacing: 0.12em;
551
  text-transform: uppercase;
552
  cursor: pointer;
553
+ transition: all 0.22s ease;
 
 
 
 
554
  white-space: nowrap;
555
  }
556
 
 
625
  width: 100%;
626
  height: 100%;
627
  padding: 0;
628
+ background: var(--bg-panel);
629
+ backdrop-filter: blur(15px) saturate(220%);
630
+ -webkit-backdrop-filter: blur(15px) saturate(220%);
631
+ display: none;
632
  flex-direction: column;
633
  overflow: hidden;
634
  animation: dash-in 0.5s cubic-bezier(0.16, 1, 0.3, 1) both;
635
  }
636
+
637
  .analysis-panel.active {
638
  display: flex;
639
  }
640
+
641
  @keyframes dash-in {
642
+ from {
643
+ opacity: 0;
644
+ transform: scale(0.98);
645
+ }
646
+
647
+ to {
648
+ opacity: 1;
649
+ transform: scale(1);
650
+ }
651
  }
652
 
653
  /* ── Dashboard Header ── */
654
  .dash-header {
655
+ display: flex;
656
+ align-items: center;
657
+ justify-content: space-between;
658
+ padding: 20px 40px;
659
+ flex-shrink: 0;
660
+ border-bottom: 1px solid rgba(255, 255, 255, 0.08);
661
+ background: linear-gradient(180deg, rgba(255, 255, 255, 0.03) 0%, rgba(255, 255, 255, 0.01) 100%);
662
+ }
663
+
664
+ .dash-header-left {
665
+ display: flex;
666
+ align-items: center;
667
+ gap: 14px;
668
  }
669
+
670
  .dash-logo-dot {
671
+ width: 16px;
672
+ height: 16px;
673
+ border-radius: 50%;
674
  background: linear-gradient(135deg, #22d3ee, #3b82f6);
675
+ box-shadow: 0 0 14px rgba(34, 211, 238, 0.6);
676
  animation: dot-breathe 2.5s ease-in-out infinite;
677
  }
678
+
679
  @keyframes dot-breathe {
680
+
681
+ 0%,
682
+ 100% {
683
+ box-shadow: 0 0 8px rgba(34, 211, 238, 0.4);
684
+ }
685
+
686
+ 50% {
687
+ box-shadow: 0 0 22px rgba(34, 211, 238, 0.8);
688
+ }
689
  }
690
+
691
  .dash-title {
692
+ font-family: var(--ff-display);
693
+ font-size: 1.6rem;
694
+ font-weight: 800;
695
+ color: var(--txt-bright);
696
+ letter-spacing: 0.12em;
697
+ text-transform: uppercase;
698
  }
699
+
700
  .dash-subtitle {
701
+ font-size: 1rem;
702
+ color: var(--txt-muted);
703
+ margin-left: 15px;
704
  letter-spacing: 0.08em;
705
  }
706
+
707
  .dash-close {
708
+ width: 40px;
709
+ height: 40px;
710
+ display: flex;
711
+ align-items: center;
712
+ justify-content: center;
713
+ cursor: pointer;
714
+ color: var(--txt-secondary);
715
+ border-radius: 10px;
716
+ border: 1px solid var(--bdr-base);
717
+ background: var(--bg-control);
718
  transition: all 0.2s;
719
  }
720
+
721
+ .dash-close:hover {
722
+ background: var(--bg-control-hov);
723
+ color: var(--txt-bright);
724
+ border-color: var(--bdr-muted);
725
+ }
726
 
727
  /* ── Dashboard Body — top gauges + bottom tables ── */
728
  .dash-body {
729
+ flex: 1;
730
+ display: flex;
731
+ flex-direction: column;
732
+ overflow: hidden;
733
  }
734
 
735
  /* ── TOP: 3 Big Gauges Row ── */
736
  .dash-gauges-hero {
737
+ display: flex;
738
+ justify-content: center;
739
+ align-items: stretch;
740
+ gap: 0;
741
+ flex-shrink: 0;
742
+ border-bottom: 1px solid rgba(255, 255, 255, 0.06);
743
  }
744
+
745
  .gauge-hero-card {
746
+ flex: 1;
747
+ min-width: 0;
748
+ display: flex;
749
+ flex-direction: column;
750
+ align-items: center;
751
+ justify-content: center;
752
+ gap: 15px;
753
  padding: 40px 30px;
754
+ border-right: 1px solid var(--bdr-dim);
755
  transition: all 0.4s cubic-bezier(0.16, 1, 0.3, 1);
756
  position: relative;
757
  }
758
+
759
+ .gauge-hero-card:last-child {
760
+ border-right: none;
761
+ }
762
+
763
+ .gauge-hero-card:hover {
764
+ background: var(--bg-active);
765
+ transform: translateY(-4px);
766
+ }
767
+
768
  .gauge-hero-card.hero-total {
769
+ background: linear-gradient(180deg, var(--accent-lo) 0%, transparent 100%);
770
  }
771
+
772
  .gauge-hero-card.hero-total::before {
773
+ content: '';
774
+ position: absolute;
775
+ top: 0;
776
+ left: 10%;
777
+ right: 10%;
778
+ height: 4px;
779
  background: linear-gradient(90deg, transparent, var(--accent), transparent);
780
  border-radius: 4px;
781
  box-shadow: 0 0 20px var(--accent-glow);
782
  }
783
 
784
  .gauge-hero-title {
785
+ font-family: var(--ff-display);
786
+ font-size: 1.3rem;
787
+ font-weight: 900;
788
+ color: var(--txt-bright) !important;
789
+ text-transform: uppercase;
790
+ letter-spacing: 0.22em;
791
+ text-align: center;
792
  }
793
+
794
  .gauge-hero-title.title-total {
795
+ font-size: 1.55rem;
796
+ color: var(--accent);
797
+ text-shadow: 0 0 35px var(--accent-glow);
798
  }
799
+
800
  .gauge-hero-svg-wrap {
801
  position: relative;
802
+ width: 320px;
803
+ height: 220px;
804
+ display: flex;
805
+ align-items: center;
806
+ justify-content: center;
807
  }
808
+
809
  .gauge-hero-value-box {
810
  position: absolute;
811
+ top: 52%;
812
+ left: 50%;
813
+ /* Shifted higher (BUG FIX) */
814
  transform: translate(-50%, -50%);
815
+ display: flex;
816
+ flex-direction: column;
817
+ align-items: center;
818
+ pointer-events: none;
819
  }
820
+
821
  .gauge-hero-value-num {
822
+ font-family: var(--ff-display);
823
+ font-size: 2.2rem;
824
+ font-weight: 900;
825
+ color: var(--txt-bright) !important;
826
+ line-height: 1;
827
+ text-shadow: 0 0 20px rgba(255, 255, 255, 0.3);
828
  }
829
+
830
  .gauge-hero-value-label {
831
+ font-size: 0.7rem;
832
+ font-weight: 700;
833
+ color: var(--txt-muted);
834
+ text-transform: uppercase;
835
+ letter-spacing: 0.1em;
836
+ margin-top: 4px;
837
+ }
838
+
839
+ /* Theme Toggle Button */
840
+ .btn-theme {
841
+ height: var(--ctrl-h);
842
+ padding: 0 12px;
843
+ background: var(--bg-control);
844
+ border: 1px solid var(--bdr-base);
845
+ color: var(--txt-primary);
846
+ border-radius: var(--radius);
847
+ display: flex;
848
+ align-items: center;
849
+ gap: 8px;
850
+ cursor: pointer;
851
+ transition: all 0.2s;
852
+ }
853
+
854
+ .btn-theme:hover {
855
+ background: var(--bg-control-hov);
856
+ border-color: var(--bdr-muted);
857
+ }
858
+
859
+ .btn-theme svg {
860
+ width: 16px;
861
+ height: 16px;
862
  }
863
 
864
  .gauge-hero-signal {
865
+ font-family: var(--ff-display);
866
+ font-size: 1.8rem;
867
+ font-weight: 900;
868
+ text-transform: uppercase;
869
+ letter-spacing: 0.15em;
870
+ text-shadow: 0 0 20px rgba(0, 0, 0, 0.5);
871
  margin-top: 5px;
872
  }
873
+
874
+ .gauge-hero-signal.strong-buy {
875
+ color: #22c55e;
876
+ text-shadow: 0 0 15px rgba(34, 197, 94, 0.4);
877
+ }
878
+
879
+ .gauge-hero-signal.buy {
880
+ color: #86efac;
881
+ }
882
+
883
+ .gauge-hero-signal.strong-sell {
884
+ color: #ef4444;
885
+ text-shadow: 0 0 15px rgba(239, 68, 68, 0.4);
886
+ }
887
+
888
+ .gauge-hero-signal.sell {
889
+ color: #fca5a5;
890
+ }
891
+
892
+ .gauge-hero-signal.neutral {
893
+ color: #cbd5e1;
894
+ }
895
+
896
+ .gauge-hero-signal.signal-total {
897
+ font-size: 2.2rem;
898
+ }
899
+
900
  .gauge-hero-counts {
901
+ display: flex;
902
+ gap: 20px;
903
+ font-size: 0.8rem;
904
+ color: rgba(255, 255, 255, 0.4);
905
  letter-spacing: 0.02em;
906
  }
907
+
908
+ .gauge-hero-counts .ghc-label {
909
+ text-transform: uppercase;
910
+ font-weight: 600;
911
+ }
912
+
913
  .gauge-hero-counts .ghc-value {
914
+ font-family: var(--ff-mono);
915
+ font-weight: 900;
916
+ font-size: 1.1rem;
917
+ color: #ffffff;
918
+ display: block;
919
  margin-top: 2px;
920
  }
921
 
922
  /* ── BOTTOM: Data Tables Grid ── */
923
  .dash-tables-row {
924
+ flex: 1;
925
+ display: grid;
926
+ overflow: hidden;
927
  grid-template-columns: 1fr 1fr 1fr;
928
  gap: 0;
929
  }
930
+
931
  .dash-col {
932
+ display: flex;
933
+ flex-direction: column;
934
+ border-right: 1px solid var(--bdr-dim);
935
  overflow: hidden;
936
  }
937
+
938
+ .dash-col:last-child {
939
+ border-right: none;
940
+ }
941
 
942
  .dc-header {
943
+ padding: 14px 20px;
944
+ flex-shrink: 0;
945
+ border-bottom: 1px solid var(--bdr-dim);
946
+ background: var(--bg-glass-light);
947
  font-family: var(--ff-display);
948
+ font-size: 0.85rem;
949
+ font-weight: 800;
950
+ color: var(--txt-bright);
951
+ text-transform: uppercase;
952
+ letter-spacing: 0.12em;
953
  }
954
 
955
  .dash-table-wrap {
956
+ flex: 1;
957
+ overflow-y: auto;
958
+ overflow-x: hidden;
959
+ }
960
+
961
+ .dash-table-wrap::-webkit-scrollbar {
962
+ width: 3px;
963
+ }
964
+
965
+ .dash-table-wrap::-webkit-scrollbar-thumb {
966
+ background: rgba(255, 255, 255, 0.08);
967
+ border-radius: 10px;
968
+ }
969
+
970
+ .dt {
971
+ width: 100%;
972
+ border-collapse: collapse;
973
  }
 
 
974
 
 
975
  .dt td {
976
+ padding: 9px 18px;
977
+ font-size: 0.88rem;
978
+ border-bottom: 1px solid var(--bdr-dim);
979
  }
980
+
981
+ .dt td:first-child {
982
+ color: var(--txt-secondary);
983
+ font-weight: 500;
984
+ }
985
+
986
  .dt .dt-val {
987
+ font-family: var(--ff-mono);
988
+ font-weight: 700;
989
+ color: var(--txt-bright);
990
+ text-align: right;
991
  }
992
+
993
  .dt .dt-act {
994
+ font-weight: 800;
995
+ text-align: right;
996
+ font-size: 0.85rem;
997
+ letter-spacing: 0.03em;
998
+ }
999
+
1000
+ .dt-act-buy {
1001
+ color: #60a5fa !important;
1002
+ }
1003
+
1004
+ .dt-act-sell {
1005
+ color: #fb7185 !important;
1006
+ }
1007
+
1008
+ .dt-act-neut {
1009
+ color: var(--txt-muted) !important;
1010
+ }
1011
+
1012
+ .pivot-table {
1013
+ width: 100%;
1014
+ border-collapse: collapse;
1015
  }
 
 
 
1016
 
 
1017
  .pivot-table th {
1018
+ padding: 8px 10px;
1019
+ font-size: 0.7rem;
1020
+ font-weight: 800;
1021
+ color: var(--txt-muted);
1022
+ text-transform: uppercase;
1023
+ letter-spacing: 0.08em;
1024
+ text-align: center;
1025
+ border-bottom: 1px solid var(--bdr-dim);
1026
  }
1027
+
1028
  .pivot-table td {
1029
+ padding: 7px 10px;
1030
+ font-family: var(--ff-mono);
1031
+ font-size: 0.85rem;
1032
+ font-weight: 600;
1033
+ color: var(--txt-bright);
1034
+ text-align: center;
1035
+ border-bottom: 1px solid var(--bdr-dim);
1036
  }
1037
+
1038
  .pivot-table td:first-child {
1039
+ font-weight: 800;
1040
+ color: var(--txt-muted);
1041
+ font-family: var(--ff-ui);
1042
+ text-align: left;
1043
+ padding-left: 18px;
1044
  }
1045
 
1046
  .summary-disclaimer {
1047
  padding: 10px 20px;
1048
+ font-size: 0.72rem;
1049
+ color: var(--txt-muted);
1050
+ opacity: 0.7;
1051
+ line-height: 1.5;
1052
+ border-top: 1px solid var(--bdr-dim);
1053
+ text-align: center;
1054
+ flex-shrink: 0;
1055
+ }
1056
+
1057
+ .summary-disclaimer strong {
1058
+ color: rgba(255, 255, 255, 0.4);
1059
  }
 
1060
 
1061
  /* ── Loading state ── */
1062
  .dash-loading {
1063
+ display: flex;
1064
+ flex-direction: column;
1065
+ align-items: center;
1066
+ justify-content: center;
1067
+ height: 100%;
1068
+ gap: 16px;
1069
+ color: rgba(255, 255, 255, 0.4);
1070
+ }
1071
+
1072
+ .dash-loading .loader-ring {
1073
+ border-top-color: var(--accent);
1074
+ }
1075
+
1076
+ .dash-loading p {
1077
+ font-size: 0.9rem;
1078
+ letter-spacing: 0.05em;
1079
+ animation: loader-pulse 1.4s ease-in-out infinite;
1080
  }
 
 
1081
 
1082
  /* Dashboard responsive */
1083
  @media (max-width: 1200px) {
1084
+ .dash-tables-row {
1085
+ grid-template-columns: 1fr 1fr;
1086
+ }
1087
+
1088
+ .dash-col.col-pivots {
1089
+ display: none;
1090
+ }
1091
+
1092
+ .gauge-hero-card {
1093
+ padding: 20px 14px 18px;
1094
+ }
1095
+
1096
+ .gauge-hero-svg-wrap {
1097
+ width: 180px;
1098
+ height: 108px;
1099
+ }
1100
  }
1101
+
1102
  @media (max-width: 800px) {
1103
+ .dash-gauges-hero {
1104
+ flex-direction: column;
1105
+ }
1106
+
1107
+ .gauge-hero-card {
1108
+ border-right: none;
1109
+ border-bottom: 1px solid rgba(255, 255, 255, 0.05);
1110
+ }
1111
+
1112
+ .dash-tables-row {
1113
+ grid-template-columns: 1fr;
1114
+ }
1115
  }
1116
 
1117
  #chart {
 
1122
  z-index: 5;
1123
  }
1124
 
1125
+ /* Futuristic AI Overlay (Adaptive Backgrounds) */
1126
  .chart-bg-overlay {
1127
  position: absolute;
1128
  inset: 0;
1129
  pointer-events: none;
1130
  z-index: 1;
1131
+ opacity: 0.12;
1132
+ background-image: url('Light_BG.png');
1133
  background-size: contain;
1134
  background-repeat: no-repeat;
1135
  background-position: left bottom;
1136
+ mask-image: linear-gradient(to right, black 30%, transparent 85%);
1137
+ -webkit-mask-image: linear-gradient(to right, black 30%, transparent 85%);
1138
+ transition: background-image 0.4s ease, opacity 0.4s ease;
1139
+ }
1140
+
1141
+ body.dark-theme .chart-bg-overlay {
1142
+ background-image: url('Dark_BG.png');
1143
+ opacity: 0.18;
1144
  }
1145
 
1146
  .chart-logo-overlay {
 
1154
  font-weight: 700;
1155
  letter-spacing: 0.25em;
1156
  line-height: 1;
1157
+ background: linear-gradient(180deg, var(--txt-bright) 0%, var(--accent) 50%, var(--bg-base) 100%);
1158
  -webkit-background-clip: text;
1159
  -webkit-text-fill-color: transparent;
1160
  background-clip: text;
1161
+ filter: drop-shadow(0 0 15px var(--accent-glow));
1162
+ opacity: 0.35;
1163
  text-transform: uppercase;
1164
  white-space: nowrap;
1165
+ transition: opacity 0.3s ease;
1166
+ }
1167
+
1168
+ body.dark-theme .chart-logo-overlay {
1169
+ opacity: 0.55;
1170
  }
1171
 
1172
  /* ── Terminal Loader Overlay (Strict Isolation) ── */
 
1175
  inset: 0 !important;
1176
  width: 100% !important;
1177
  height: 100% !important;
1178
+ display: none;
1179
+ /* FULL HIDE */
1180
  flex-direction: column;
1181
  align-items: center;
1182
  justify-content: center;
 
1350
  border-radius: 2px;
1351
  }
1352
 
1353
+ .sidebar-nav-quick {
1354
+ display: flex;
1355
+ gap: 4px;
1356
+ padding: 8px 12px;
1357
+ overflow-x: auto;
1358
+ border-bottom: 1px solid var(--bdr-dim);
1359
+ background: var(--bg-glass-light);
1360
+ }
1361
+
1362
+ .sidebar-nav-quick::-webkit-scrollbar {
1363
+ display: none;
1364
+ }
1365
+
1366
+ .nav-q-btn {
1367
+ padding: 4px 10px;
1368
+ font-size: 0.65rem;
1369
+ background: var(--bg-control);
1370
+ border: 1px solid var(--bdr-dim);
1371
+ border-radius: 12px;
1372
+ color: var(--txt-muted);
1373
+ cursor: pointer;
1374
+ white-space: nowrap;
1375
+ transition: all 0.2s;
1376
+ }
1377
+
1378
+ .nav-q-btn:hover {
1379
+ background: rgba(255, 255, 255, 0.08);
1380
+ color: var(--txt-secondary);
1381
+ }
1382
+
1383
+ .nav-q-btn.active {
1384
+ background: var(--bg-active);
1385
+ border-color: var(--accent);
1386
+ color: var(--accent);
1387
+ }
1388
+
1389
+ ::-webkit-scrollbar-thumb:hover {
1390
+ background: var(--txt-muted);
1391
+ }
1392
+
1393
+ /* ═══════════════════════════════════════════════
1394
+ RESPONSIVE
1395
+ ═══════════════════════════════════════════════ */
1396
+ @media (max-width: 960px) {
1397
+ .ctrl-label {
1398
+ display: none;
1399
+ }
1400
+
1401
+ .ctrl-unit {
1402
+ flex-direction: row;
1403
+ align-items: center;
1404
+ }
1405
+
1406
+ #symbolSelect {
1407
+ width: 150px;
1408
+ }
1409
+
1410
+ .logo-sub {
1411
+ display: none;
1412
+ }
1413
+ }
1414
+
1415
+ @media (max-width: 1024px) {
1416
+ .omnibox {
1417
+ width: 200px;
1418
+ }
1419
+
1420
+ .ctrl-label {
1421
+ display: none;
1422
+ }
1423
+
1424
+ /* Hide labels to save space */
1425
+ }
1426
+
1427
+ @media (max-width: 680px) {
1428
+ .hdr {
1429
+ padding: 0 10px;
1430
+ gap: 6px;
1431
+ }
1432
+
1433
+ .omnibox {
1434
+ width: 140px;
1435
+ }
1436
+
1437
+ .logo-name {
1438
+ font-size: 1.1rem;
1439
+ }
1440
+
1441
+ #symbolSelect {
1442
+ width: 120px;
1443
+ }
1444
+
1445
+ #timeframeSelect {
1446
+ width: 55px;
1447
+ }
1448
+
1449
+ #horizonInput {
1450
+ width: 50px;
1451
+ }
1452
+
1453
+ .btn-primary {
1454
+ padding: 0 10px;
1455
+ font-size: 0.7rem;
1456
+ }
1457
+ }
1458
+
1459
+ .gauge-hero-ai-details {
1460
+ margin-top: 15px;
1461
+ display: flex;
1462
+ flex-direction: column;
1463
+ gap: 6px;
1464
+ width: 100%;
1465
+ padding: 10px 15px;
1466
+ background: var(--bg-control-hov);
1467
+ border-radius: 8px;
1468
+ border: 1px solid var(--bdr-muted);
1469
+ }
1470
+
1471
+ .gh-ai-row {
1472
+ display: flex;
1473
+ justify-content: space-between;
1474
+ align-items: center;
1475
+ font-size: 0.8rem;
1476
+ }
1477
+
1478
+ .gh-ai-label {
1479
+ color: var(--txt-muted) !important;
1480
+ font-weight: 700;
1481
+ font-size: 0.72rem;
1482
+ text-transform: uppercase;
1483
+ }
1484
+
1485
+ .gh-ai-val {
1486
+ font-family: var(--ff-mono);
1487
+ font-weight: 800;
1488
+ color: var(--txt-bright) !important;
1489
+ }
1490
+
1491
+ .gh-ai-val.up {
1492
+ color: var(--bull);
1493
+ }
1494
+
1495
+ .gh-ai-val.down {
1496
+ color: var(--bear);
1497
+ }
1498
+
1499
+ /* ── Compact Gauges Overlay (v6.0) ── */
1500
+ .chart-gauges-container {
1501
+ position: absolute;
1502
+ top: 65px;
1503
+ left: 15px;
1504
+ z-index: 100;
1505
+ display: flex;
1506
+ gap: 8px;
1507
+ pointer-events: none;
1508
+ animation: gauges-fade-in 0.8s ease-out both;
1509
+ }
1510
+
1511
+ @keyframes gauges-fade-in {
1512
+ from { opacity: 0; transform: translateX(-10px); }
1513
+ to { opacity: 1; transform: translateX(0); }
1514
+ }
1515
+
1516
+ .compact-gauge-card {
1517
+ background: var(--bg-glass-light);
1518
+ backdrop-filter: blur(12px) saturate(180%);
1519
+ -webkit-backdrop-filter: blur(12px) saturate(180%);
1520
+ border: 1px solid var(--bdr-dim);
1521
+ border-radius: 12px;
1522
+ padding: 10px 14px;
1523
+ display: flex;
1524
+ flex-direction: column;
1525
+ align-items: center;
1526
+ gap: 6px;
1527
+ min-width: 105px;
1528
+ box-shadow: 0 4px 15px rgba(0, 0, 0, 0.12), 0 0 0 1px rgba(255, 255, 255, 0.05);
1529
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
1530
+ pointer-events: auto;
1531
+ cursor: default;
1532
+ }
1533
+
1534
+ .compact-gauge-card:hover {
1535
+ transform: translateY(-2px);
1536
+ background: var(--bg-glass);
1537
+ border-color: var(--bdr-accent);
1538
+ box-shadow: 0 8px 25px rgba(0, 0, 0, 0.18), 0 0 10px var(--accent-lo);
1539
+ }
1540
+
1541
+ .compact-gauge-title {
1542
+ font-family: var(--ff-display);
1543
+ font-size: 0.62rem;
1544
+ font-weight: 800;
1545
+ color: var(--txt-muted);
1546
+ text-transform: uppercase;
1547
+ letter-spacing: 0.08em;
1548
+ line-height: 1;
1549
+ }
1550
+
1551
+ .compact-gauge-svg-wrap {
1552
+ width: 70px;
1553
+ height: 45px;
1554
+ position: relative;
1555
+ }
1556
+
1557
+ .compact-gauge-signal {
1558
+ font-family: var(--ff-ui);
1559
+ font-size: 0.8rem;
1560
+ font-weight: 800;
1561
+ text-transform: uppercase;
1562
+ letter-spacing: 0.02em;
1563
+ }
1564
+
1565
+ .compact-gauge-card.hero {
1566
+ background: linear-gradient(135deg, var(--bg-glass-light) 0%, var(--accent-lo) 100%);
1567
+ border-color: var(--accent-mid);
1568
+ }
1569
+
1570
+ .compact-gauge-card.hero .compact-gauge-title {
1571
+ color: var(--accent);
1572
+ }
1573
+
1574
+ /* ═══════════════════════════════════════════════
1575
+ PRODUCT EXPLORER (MODAL)
1576
+ ═══════════════════════════════════════════════ */
1577
+ .explorer-overlay {
1578
+ position: fixed;
1579
+ top: 0;
1580
+ left: 0;
1581
+ right: 0;
1582
+ bottom: 0;
1583
+ background: rgba(1, 4, 12, 0.92);
1584
+ backdrop-filter: blur(12px);
1585
+ z-index: 2000;
1586
+ display: none;
1587
+ align-items: center;
1588
+ justify-content: center;
1589
+ padding: 40px;
1590
+ opacity: 0;
1591
+ transition: opacity 0.3s ease;
1592
+ }
1593
+
1594
+ .explorer-overlay.active {
1595
+ display: flex;
1596
+ opacity: 1;
1597
+ }
1598
+
1599
+ .explorer-window {
1600
+ width: 100%;
1601
+ max-width: 1100px;
1602
+ height: 100%;
1603
+ max-height: 720px;
1604
+ background: var(--bg-depth);
1605
+ border: 1px solid var(--bdr-muted);
1606
+ border-radius: var(--radius-lg);
1607
+ display: flex;
1608
+ flex-direction: column;
1609
+ overflow: hidden;
1610
+ box-shadow: var(--shadow-lg);
1611
+ transform: scale(0.95);
1612
+ transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
1613
+ }
1614
+
1615
+ .explorer-overlay.active .explorer-window {
1616
+ transform: scale(1);
1617
+ }
1618
+
1619
+ .explorer-head {
1620
+ height: 64px;
1621
+ padding: 0 24px;
1622
+ display: flex;
1623
+ align-items: center;
1624
+ justify-content: space-between;
1625
+ border-bottom: 1px solid var(--bdr-dim);
1626
+ background: var(--bg-depth);
1627
+ }
1628
+
1629
+ .explorer-title {
1630
+ font-family: var(--ff-display);
1631
+ font-size: 1.1rem;
1632
+ font-weight: 700;
1633
+ letter-spacing: 0.15em;
1634
+ color: var(--accent);
1635
+ }
1636
+
1637
+ .explorer-body {
1638
+ flex: 1;
1639
+ display: flex;
1640
+ overflow: hidden;
1641
+ }
1642
+
1643
+ .explorer-sidebar {
1644
+ width: 240px;
1645
+ border-right: 1px solid var(--bdr-dim);
1646
+ overflow-y: auto;
1647
+ padding: 16px 0;
1648
+ background: var(--bg-sidebar);
1649
+ }
1650
+
1651
+ .explorer-cat-item {
1652
+ padding: 12px 24px;
1653
+ font-weight: 600;
1654
+ color: var(--txt-secondary);
1655
+ cursor: pointer;
1656
+ transition: all 0.2s;
1657
+ border-left: 3px solid transparent;
1658
+ font-size: 0.9rem;
1659
+ }
1660
+
1661
+ .explorer-cat-item:hover {
1662
+ background: var(--bg-control-hov);
1663
+ color: var(--txt-bright);
1664
+ }
1665
+
1666
+ .explorer-cat-item.active {
1667
+ background: var(--bg-active);
1668
+ color: var(--accent);
1669
+ border-left-color: var(--accent);
1670
+ }
1671
+
1672
+ .explorer-main {
1673
+ flex: 1;
1674
+ display: flex;
1675
+ flex-direction: column;
1676
+ padding: 24px;
1677
+ background: var(--bg-depth);
1678
+ }
1679
+
1680
+ .explorer-search {
1681
+ position: relative;
1682
+ margin-bottom: 20px;
1683
  }
1684
 
1685
+ .explorer-search i {
1686
+ position: absolute;
1687
+ left: 12px;
1688
+ top: 50%;
1689
+ transform: translateY(-50%);
1690
+ color: var(--txt-muted);
1691
+ }
 
 
 
 
 
 
 
 
 
1692
 
1693
+ .explorer-search input {
1694
+ width: 100%;
1695
+ height: 42px;
1696
+ background: rgba(255, 255, 255, 0.03);
1697
+ border: 1px solid var(--bdr-dim);
1698
+ border-radius: var(--radius);
1699
+ padding: 0 40px;
1700
+ color: #fff;
1701
+ outline: none;
1702
+ transition: border-color 0.2s;
1703
  }
1704
 
1705
+ .explorer-search input:focus {
1706
+ border-color: var(--accent);
1707
+ }
 
 
1708
 
1709
+ .explorer-grid {
1710
+ flex: 1;
1711
+ display: grid;
1712
+ grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
1713
+ gap: 12px;
1714
+ overflow-y: auto;
1715
+ align-content: start;
1716
+ }
1717
 
1718
+ .explorer-symbol-card {
1719
+ background: var(--bg-control);
1720
+ border: 1px solid var(--bdr-dim);
1721
+ border-radius: var(--radius);
1722
+ padding: 14px;
1723
+ cursor: pointer;
1724
+ transition: all 0.2s;
1725
+ display: flex;
1726
+ flex-direction: column;
1727
+ gap: 4px;
1728
+ }
1729
 
1730
+ .explorer-symbol-card:hover {
1731
+ background: var(--bg-active);
1732
+ border-color: var(--accent);
1733
+ transform: translateY(-2px);
1734
+ box-shadow: var(--shadow-lg);
1735
+ }
1736
 
1737
+ .esc-ticker {
1738
+ font-family: var(--ff-mono);
1739
+ font-weight: 700;
1740
+ font-size: 1rem;
1741
+ color: var(--accent);
1742
+ }
1743
 
1744
+ .esc-label {
1745
+ font-size: 0.75rem;
1746
+ color: var(--txt-secondary);
1747
+ white-space: nowrap;
1748
+ overflow: hidden;
1749
+ text-overflow: ellipsis;
1750
+ }
1751
 
1752
+ .esc-source {
1753
+ font-size: 0.65rem;
1754
+ color: var(--txt-muted);
1755
+ margin-top: 4px;
1756
+ text-transform: uppercase;
1757
+ letter-spacing: 0.05em;
1758
  }
1759
  </style>
1760
  </head>
 
1793
  <!-- Controls -->
1794
  <div class="ctrls">
1795
 
1796
+ <button class="btn-icon" id="toggleMarketBtn" title="Khám phá Thị trường">
1797
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
1798
+ stroke-linecap="round" stroke-linejoin="round">
1799
  <line x1="3" y1="12" x2="21" y2="12"></line>
1800
  <line x1="3" y1="6" x2="21" y2="6"></line>
1801
  <line x1="3" y1="18" x2="21" y2="18"></line>
 
1804
 
1805
  <!-- Omnibox Search -->
1806
  <div class="omnibox">
1807
+ <i><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
1808
+ stroke-linecap="round" stroke-linejoin="round">
1809
+ <circle cx="11" cy="11" r="8"></circle>
1810
+ <line x1="21" y1="21" x2="16.65" y2="16.65"></line>
1811
+ </svg></i>
1812
  <input type="text" id="symbolSearch" placeholder="Tìm kiếm mã (VD: Gold, BTC, AAPL...)" autocomplete="off">
1813
  <div class="search-results" id="searchResults"></div>
1814
  </div>
 
1841
  </select>
1842
  </div>
1843
 
1844
+ <button class="btn-theme" id="themeToggleBtn" title="Chuyển đổi giao diện">
1845
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
1846
+ stroke-linejoin="round" class="sun-icon">
1847
+ <circle cx="12" cy="12" r="5"></circle>
1848
+ <line x1="12" y1="1" x2="12" y2="3"></line>
1849
+ <line x1="12" y1="21" x2="12" y2="23"></line>
1850
+ <line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line>
1851
+ <line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line>
1852
+ <line x1="1" y1="12" x2="3" y2="12"></line>
1853
+ <line x1="21" y1="12" x2="23" y2="12"></line>
1854
+ <line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line>
1855
+ <line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line>
1856
+ </svg>
1857
+ </button>
1858
+
1859
  <button class="btn-primary" id="refreshBtn">Phân tích</button>
1860
 
1861
 
 
1872
 
1873
  <!-- ── BODY ──────────────────────────────── -->
1874
  <div class="app-body">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1875
 
1876
  <!-- Main Area -->
1877
  <main class="main">
 
1899
  <!-- Chart canvas -->
1900
  <div id="chart"></div>
1901
 
1902
+ <!-- Compact Gauges Overlay (v6.0) -->
1903
+ <div class="chart-gauges-container" id="chartGauges"></div>
1904
 
1905
+ <aside class="analysis-panel hidden" id="analysisPanel"></aside>
1906
  </main>
1907
  </div>
1908
+
1909
+ <!-- ── PRODUCT EXPLORER OVERLAY ────────── -->
1910
+ <div class="explorer-overlay" id="explorerOverlay">
1911
+ <div class="explorer-window">
1912
+ <div class="explorer-head">
1913
+ <div class="explorer-title">KHÁM PHÁ THỊ TRƯỜNG</div>
1914
+ <button class="btn-icon" id="closeExplorerBtn">
1915
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
1916
+ <line x1="18" y1="6" x2="6" y2="18"></line>
1917
+ <line x1="6" y1="6" x2="18" y2="18"></line>
1918
+ </svg>
1919
+ </button>
1920
+ </div>
1921
+ <div class="explorer-body">
1922
+ <div class="explorer-sidebar" id="explorerCats">
1923
+ <!-- Categories injected here -->
1924
+ </div>
1925
+ <div class="explorer-main">
1926
+ <div class="explorer-search">
1927
+ <i><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
1928
+ <circle cx="11" cy="11" r="8"></circle>
1929
+ <line x1="21" y1="21" x2="16.65" y2="16.65"></line>
1930
+ </svg></i>
1931
+ <input type="text" id="explorerSearchInput" placeholder="Tìm kiếm nhanh trong danh mục...">
1932
+ </div>
1933
+ <div class="explorer-grid" id="explorerGrid">
1934
+ <!-- Symbols injected here -->
1935
+ </div>
1936
+ </div>
1937
+ </div>
1938
+ </div>
1939
+ </div>
1940
  </div>
1941
 
1942
  <!-- ══════════════════════════════════════════════
 
1961
  const terminalLoader = document.getElementById('terminalLoader');
1962
  const loaderText = document.getElementById('loaderText');
1963
  const analysisPanel = document.getElementById('analysisPanel');
 
 
 
1964
  const marketStatusBar = document.getElementById('marketStatusBar');
1965
  const indicatorSelect = document.getElementById('indicatorSelect');
1966
 
 
1985
 
1986
  const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
1987
  const wsUrl = `${API_BASE.replace(/^https?:\/\//, wsProtocol)}/ws/price/${symbol}`;
1988
+
1989
  console.log(`[WS] Connecting to ${wsUrl}`);
1990
  ws = new WebSocket(wsUrl);
1991
 
 
1993
  const data = JSON.parse(event.data);
1994
  if (data.error) return;
1995
 
1996
+ // Handle ping from server (P2)
1997
+ if (data.type === 'ping') {
1998
+ ws.send(JSON.stringify({ type: 'pong', ts: Date.now() }));
1999
+ return;
2000
+ }
2001
+
2002
  // Update current candle if price changed
2003
  const lastCandle = lastCandleData;
2004
  if (lastCandle && data.price) {
 
2013
  lastCandleData = update;
2014
  }
2015
 
 
 
2016
  };
2017
 
2018
  ws.onclose = () => {
 
2041
  }
2042
  }
2043
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2044
 
2045
  /* ── Search Logic ───────────────────────────── */
2046
  let searchTimeout = null;
 
2068
  }
2069
  searchResults.innerHTML = results.map(r => `
2070
  <div class="search-item" onclick="switchSymbol('${r.symbol}')">
2071
+ <div style="display:flex; flex-direction:column;">
2072
+ <span style="font-weight:600; color:var(--txt-primary);">${r.symbol}</span>
2073
+ <span style="font-size:0.7rem; color:var(--txt-muted);">${r.label}</span>
2074
+ </div>
2075
+ <div style="font-size:0.6rem; padding:2px 6px; background:rgba(255,255,255,0.05); border-radius:4px; color:var(--accent);">
2076
+ ${r.category}
2077
+ </div>
2078
  </div>
2079
  `).join('');
2080
  searchResults.classList.add('visible');
 
2085
  currentSymbol = symbol;
2086
  symbolSearch.value = symbol;
2087
  searchResults.classList.remove('visible');
2088
+
2089
  // Update labels immediately for better UX responsiveness
2090
  const sLabel = symbolMap.get(symbol) || symbol;
2091
  updateStatus(`${sLabel} · ${currentInterval} — Đang nạp...`, 'loading');
2092
 
2093
+ // 2. Clear chart and indicators to avoid showing old data
2094
+ clearChartImmediate();
 
 
2095
 
2096
+ // 3. Load new data
2097
  await refreshChart();
 
2098
  connectWS(symbol);
2099
  }
2100
 
 
2204
  function showLoader(msg = 'Đang tải dữ liệu') {
2205
  loaderText.textContent = msg;
2206
  terminalLoader.classList.add('active');
2207
+ const cEl = document.getElementById('chart');
2208
+ if (cEl) cEl.classList.add('loading');
2209
  }
2210
 
2211
  function hideLoader() {
2212
  terminalLoader.classList.remove('active');
2213
+ const cEl = document.getElementById('chart');
2214
+ if (cEl) cEl.classList.remove('loading');
2215
+ }
2216
+
2217
+ function updateStatus(text, mode = 'normal') {
2218
+ const el = document.getElementById('marketStatus');
2219
+ if (el) {
2220
+ el.textContent = text;
2221
+ el.className = `market-status ${mode}`;
2222
+ } else {
2223
+ statusText.textContent = text;
2224
+ statusDot.className = 'dot ' + mode;
2225
+ statusPill.className = 'status-pill ' + mode;
2226
+ }
2227
  }
2228
 
2229
+ function clearChartImmediate() {
2230
+ // Show loading placeholder on chart
2231
+ const chartContainer = document.getElementById('chart-container');
2232
+ if (chartContainer) {
2233
+ chartContainer.style.opacity = '0.5';
2234
+ }
2235
  }
2236
 
2237
  function formatPrice(value) {
 
2251
  return `${Number(value).toFixed(2)}R`;
2252
  }
2253
 
2254
+ /* ── Shared Gauge Logic (v6.0) ── */
2255
+ function buildGaugeSvg(rawScore, w = 320, h = 220, showValue = true) {
2256
+ const score = Math.max(-1, Math.min(1, rawScore));
2257
+ const displayValue = Math.round((score + 1) * 50);
2258
+ const angle = score * 135;
2259
+ const cx = w / 2, cy = h * 0.65, r = (w / 2) * 0.62;
2260
+ const strokeW = w > 150 ? 16 : 8;
2261
+
2262
+ function arc(s, e, col) {
2263
+ const sa = (s - 90) * Math.PI / 180, ea = (e - 90) * Math.PI / 180;
2264
+ const x1 = cx + r * Math.cos(sa), y1 = cy + r * Math.sin(sa), x2 = cx + r * Math.cos(ea), y2 = cy + r * Math.sin(ea);
2265
+ return `<path d="M${x1},${y1} A${r},${r} 0 ${(e - s) > 180 ? 1 : 0} 1 ${x2},${y2}" fill="none" stroke="${col}" stroke-width="${strokeW}" stroke-linecap="round" opacity="0.8"/>`;
2266
+ }
2267
+
2268
+ const na = (angle - 90) * Math.PI / 180, nl = r + 2;
2269
+ const nx = cx + nl * Math.cos(na), ny = cy + nl * Math.sin(na);
2270
+ const needleColor = score > 0.3 ? '#4ade80' : score > 0.1 ? '#bbf7d0' : score < -0.3 ? '#f87171' : score < -0.1 ? '#fecaca' : '#f1f5f9';
2271
+
2272
+ let valueHtml = '';
2273
+ if (showValue) {
2274
+ valueHtml = `
2275
+ <div class="${w > 150 ? 'gauge-hero-value-box' : 'compact-gauge-value-box'}">
2276
+ <div class="${w > 150 ? 'gauge-hero-value-num' : 'compact-gauge-value-num'}">${displayValue}</div>
2277
+ ${w > 150 ? '<div class="gauge-hero-value-label">Chỉ số</div>' : ''}
2278
+ </div>
2279
+ `;
2280
+ }
2281
+
2282
+ return `
2283
+ ${valueHtml}
2284
+ <svg width="100%" height="100%" viewBox="0 0 ${w} ${h}" preserveAspectRatio="xMidYMid meet">
2285
+ <defs>
2286
+ <filter id="needle-glow-${w}"><feGaussianBlur stdDeviation="${w > 150 ? 5 : 2}" result="blur"/><feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge></filter>
2287
+ </defs>
2288
+ ${arc(-135, -81, '#ef4444')}${arc(-81, -27, '#fca5a5')}${arc(-27, 27, 'var(--bdr-muted)')}${arc(27, 81, '#bbf7d0')}${arc(81, 135, '#22c55e')}
2289
+ ${w > 150 ? `
2290
+ <text x="${cx + r * Math.cos((-135 - 90) * Math.PI / 180) * 1.25}" y="${cy + r * Math.sin((-135 - 90) * Math.PI / 180) * 1.25}" fill="var(--txt-muted)" font-size="9" text-anchor="middle" font-weight="900">BÁN</text>
2291
+ <text x="${cx + r * Math.cos((135 - 90) * Math.PI / 180) * 1.25}" y="${cy + r * Math.sin((135 - 90) * Math.PI / 180) * 1.25}" fill="var(--txt-muted)" font-size="9" text-anchor="middle" font-weight="900">MUA</text>
2292
+ ` : ''}
2293
+ <line x1="${cx}" y1="${cy}" x2="${nx}" y2="${ny}" stroke="${needleColor}" stroke-width="${w > 150 ? 6 : 3}" stroke-linecap="round" filter="url(#needle-glow-${w})"/>
2294
+ <circle cx="${cx}" cy="${cy}" r="${w > 150 ? 12 : 5}" fill="${needleColor}" />
2295
+ <circle cx="${cx}" cy="${cy}" r="${w > 150 ? 6 : 2}" fill="#02060c" />
2296
+ </svg>
2297
+ `;
2298
+ }
2299
+
2300
+ function getSignalClass(signal) {
2301
+ if (signal.includes('Mua mạnh')) return 'strong-buy';
2302
+ if (signal.includes('Mua')) return 'buy';
2303
+ if (signal.includes('Bán mạnh')) return 'strong-sell';
2304
+ if (signal.includes('Bán')) return 'sell';
2305
+ return 'neutral';
2306
+ }
2307
+
2308
+ function renderCompactGauges(symbol, interval, payload) {
2309
+ const container = document.getElementById('chartGauges');
2310
+ if (!container || !payload?.analysis) {
2311
+ if (container) container.innerHTML = '';
2312
+ return;
2313
+ }
2314
+
2315
+ const a = payload.analysis;
2316
+ const summary = a.summary || { sell: 0, neutral: 0, buy: 0, signal: '--' };
2317
+
2318
+ // Re-calculate scores (logic mirrored from renderAnalysisPanel)
2319
+ const lastClose = payload.last_close || 0;
2320
+ const forecastRows = payload.forecast || [];
2321
+ let forecastEnd = lastClose;
2322
+ if (forecastRows.length > 1) forecastEnd = forecastRows[forecastRows.length - 1]?.p50 ?? lastClose;
2323
+ const forecastPctChange = lastClose > 0 ? ((forecastEnd - lastClose) / lastClose) * 100 : 0;
2324
+
2325
+ const techTotal = (summary.buy + summary.sell + summary.neutral) || 1;
2326
+ const techScore = (summary.buy - summary.sell) / techTotal;
2327
+ const aiScore = Math.max(-1, Math.min(1, forecastPctChange / 2));
2328
+ const combinedScore = (techScore + aiScore) / 2;
2329
+
2330
+ let aiSignal = 'Trung lập';
2331
+ if (forecastPctChange > 2) aiSignal = 'Mua mạnh';
2332
+ else if (forecastPctChange > 0.5) aiSignal = 'Mua';
2333
+ else if (forecastPctChange < -2) aiSignal = 'Bán mạnh';
2334
+ else if (forecastPctChange < -0.5) aiSignal = 'Bán';
2335
+
2336
+ let totalSignal = 'Trung lập';
2337
+ if (combinedScore > 0.4) totalSignal = 'Mua mạnh';
2338
+ else if (combinedScore > 0.1) totalSignal = 'Mua';
2339
+ else if (combinedScore < -0.4) totalSignal = 'Bán mạnh';
2340
+ else if (combinedScore < -0.1) totalSignal = 'Bán';
2341
+
2342
+ container.innerHTML = `
2343
+ <div class="compact-gauge-card">
2344
+ <div class="compact-gauge-title">Kỹ thuật</div>
2345
+ <div class="compact-gauge-svg-wrap">${buildGaugeSvg(techScore, 80, 50, false)}</div>
2346
+ <div class="compact-gauge-signal ${getSignalClass(summary.signal)}">${summary.signal}</div>
2347
+ </div>
2348
+ <div class="compact-gauge-card">
2349
+ <div class="compact-gauge-title">Dự báo AI</div>
2350
+ <div class="compact-gauge-svg-wrap">${buildGaugeSvg(aiScore, 80, 50, false)}</div>
2351
+ <div class="compact-gauge-signal ${getSignalClass(aiSignal)}">${aiSignal}</div>
2352
+ </div>
2353
+ <div class="compact-gauge-card hero">
2354
+ <div class="compact-gauge-title">Tổng kết</div>
2355
+ <div class="compact-gauge-svg-wrap">${buildGaugeSvg(combinedScore, 80, 50, false)}</div>
2356
+ <div class="compact-gauge-signal ${getSignalClass(totalSignal)}">${totalSignal}</div>
2357
+ </div>
2358
+ `;
2359
+ }
2360
+
2361
  /* ── State for cached analysis data ──────────── */
2362
  let lastAnalysisPayload = null;
2363
  let lastAnalysisSymbol = null;
 
2388
  return;
2389
  }
2390
 
2391
+ const osc = a.oscillators || { sell: 0, neutral: 0, buy: 0, signal: '--', data: [] };
2392
+ const ma = a.moving_averages || { sell: 0, neutral: 0, buy: 0, signal: '--', data: [] };
2393
+ const summary = a.summary || { sell: 0, neutral: 0, buy: 0, signal: '--' };
2394
  const pivots = (a.pivot_points || {}).data || [];
2395
 
2396
  // ── AI Forecast calculation ──
 
2427
  // ── TỔNG KẾT = (Phân tích kỹ thuật + Dự báo AI) / 2 ──
2428
  const techTotal = (summary.buy + summary.sell + summary.neutral) || 1;
2429
  const techScore = (summary.buy - summary.sell) / techTotal; // -1 to +1
 
 
 
2430
 
2431
+ // Normalized AI Score: Map forecast change to -1 to +1 range
2432
+ // We'll consider 2% change as the "strong" threshold
2433
+ const aiScore = Math.max(-1, Math.min(1, forecastPctChange / 2));
2434
+
2435
+ const combinedScore = (techScore + aiScore) / 2; // -1 to +1
2436
 
2437
  let totalSignal = 'Trung lập';
2438
  if (combinedScore > 0.4) totalSignal = 'Mua mạnh';
2439
+ else if (combinedScore > 0.1) totalSignal = 'Mua';
2440
  else if (combinedScore < -0.4) totalSignal = 'Bán mạnh';
2441
+ else if (combinedScore < -0.1) totalSignal = 'Bán';
2442
+
2443
+ // ── Big SVG Gauge builder (Refactored to buildGaugeSvg) ──
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2444
 
2445
  function signalClass(signal) {
2446
+ return getSignalClass(signal);
 
 
 
 
2447
  }
2448
 
2449
  function actCls(act) {
 
2463
  <div class="dash-header-left">
2464
  <div class="dash-logo-dot"></div>
2465
  <span class="dash-title">${sLabel}</span>
2466
+ <span class="dash-subtitle">· ${payload.source || 'N/A'} · ${tLabel} · Bảng Phân tích Tổng hợp</span>
2467
  </div>
2468
  <div class="dash-close" id="dashCloseBtn" title="Đóng">
2469
  <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
 
2479
  <!-- TOP ROW: 3 Big Gauges -->
2480
  <div class="dash-gauges-hero">
2481
 
2482
+ <!-- Gauge 1: Phân tích kỹ thuật -->
2483
  <div class="gauge-hero-card">
2484
  <div class="gauge-hero-title">PHÂN TÍCH KỸ THUẬT</div>
2485
  <div class="gauge-hero-svg-wrap">
2486
+ ${buildGaugeSvg(techScore)}
2487
  </div>
2488
  <div class="gauge-hero-signal ${signalClass(summary.signal)}">${summary.signal}</div>
2489
  <div class="gauge-hero-counts">
 
2493
  </div>
2494
  </div>
2495
 
2496
+ <!-- Gauge 2: Dự báo AI -->
2497
  <div class="gauge-hero-card">
2498
  <div class="gauge-hero-title">DỰ BÁO AI</div>
2499
  <div class="gauge-hero-svg-wrap">
2500
+ ${buildGaugeSvg(aiScore)}
2501
+ </div>
2502
+ <div class="gauge-hero-ai-details">
2503
+ <div class="gh-ai-row">
2504
+ <span class="gh-ai-label">Hiện tại:</span>
2505
+ <span class="gh-ai-val">${formatPrice(lastClose)}</span>
2506
+ </div>
2507
+ <div class="gh-ai-row">
2508
+ <span class="gh-ai-label">Dự kiến:</span>
2509
+ <span class="gh-ai-val">${formatPrice(forecastEnd)}</span>
2510
+ </div>
2511
+ <div class="gh-ai-row">
2512
+ <span class="gh-ai-label">Biến động:</span>
2513
+ <span class="gh-ai-val ${forecastPctChange >= 0 ? 'up' : 'down'}">${forecastPctChange >= 0 ? '↑' : '↓'} ${Math.abs(forecastPctChange).toFixed(2)}%</span>
2514
+ </div>
2515
  </div>
2516
  <div class="gauge-hero-signal ${signalClass(aiSignal)}">${aiSignal}</div>
2517
  </div>
2518
 
2519
+ <!-- Gauge 3: TỔNG KẾT -->
2520
  <div class="gauge-hero-card hero-total">
2521
  <div class="gauge-hero-title title-total">⚡ TỔNG KẾT</div>
2522
  <div class="gauge-hero-svg-wrap">
2523
+ ${buildGaugeSvg(combinedScore)}
2524
  </div>
2525
  <div class="gauge-hero-signal signal-total ${signalClass(totalSignal)}">${totalSignal}</div>
2526
  </div>
 
2583
  p50Series.applyOptions({ visible: false });
2584
  p10Series.applyOptions({ visible: false });
2585
  p90Series.applyOptions({ visible: false });
2586
+
2587
  // Clear indicators
2588
  bbMiddleSeries.setData([]);
2589
  bbUpperSeries.setData([]);
 
2592
 
2593
  // Hide dashboard and clear cached data on symbol switch
2594
  analysisPanel.classList.remove('active');
2595
+ const gContainer = document.getElementById('chartGauges');
2596
+ if (gContainer) gContainer.innerHTML = '';
2597
+
2598
  lastAnalysisPayload = null;
2599
  lastAnalysisSymbol = null;
2600
  chart.priceScale('right').applyOptions({ autoScale: true });
 
2614
  async function apiRequest(path) {
2615
  const sep = path.includes('?') ? '&' : '?';
2616
  const url = `${API_BASE}${path}${sep}_t=${Date.now()}`;
2617
+
2618
  const options = { method: 'GET' };
2619
  if (fetchController) options.signal = fetchController.signal;
2620
 
 
2632
  async function loadSymbols() {
2633
  try {
2634
  const data = await apiRequest('/api/symbols');
2635
+ allSymbolsData = data; // Cache globally (BUG FIX)
2636
  data.symbols.forEach(s => symbolMap.set(s.symbol, s.label));
 
 
2637
  } catch (e) {
2638
  updateStatus('Lỗi nạp metadata: ' + e.message, 'error');
2639
  }
 
2643
  async function refreshChart() {
2644
  const symbol = currentSymbol;
2645
  const interval = timeframeSelect.value;
2646
+ currentInterval = interval;
2647
  const horizon = Math.max(5, Math.min(300, parseInt(horizonInput.value) || 10));
2648
  if (!symbol) return;
2649
 
 
2660
  /* Update chart watermark */
2661
  const sLabel = symbolMap.get(symbol) || symbol;
2662
  const tLabel = timeframeMap[interval] || interval;
2663
+ const isDark = document.body.classList.contains('dark-theme');
2664
  chart.applyOptions({
2665
  watermark: {
2666
  text: `${sLabel} · ${tLabel}`,
2667
+ color: isDark ? 'rgba(34, 211, 238, 0.27)' : 'rgba(15, 23, 42, 0.12)',
2668
  fontSize: 72,
2669
  },
2670
  });
 
2687
  const type = indicatorSelect.value;
2688
  const indicators = indData.indicators || {};
2689
  const series = indicators.series || {};
2690
+
2691
  if (type === 'bb' || type === 'both') {
2692
  if (series.bb_upper) bbUpperSeries.setData(series.bb_upper);
2693
  if (series.bb_mid) bbMiddleSeries.setData(series.bb_mid);
 
2695
  }
2696
 
2697
  // Add RSI update if needed
2698
+
2699
  chart.timeScale().fitContent();
2700
  hideLoader();
2701
  updateStatus(`${symbol} · ${interval} | Đang nạp AI...`, 'loading');
 
2716
  /* ── Stage 2: Background AI Forecast ───────────────── */
2717
  async function fetchAIAnalysis(symbol, interval) {
2718
  if (currentSymbol !== symbol) return; // Prevent race conditions
2719
+
2720
  const panel = document.getElementById('analysisPanel');
2721
  panel.innerHTML = `
2722
  <div class="dash-loading">
 
2724
  <p>AI đang tính toán...</p>
2725
  </div>
2726
  `;
2727
+
2728
  try {
2729
  const horizon = horizonInput.value || 24;
2730
  const fData = await apiRequest(`/api/forecast/${encodeURIComponent(symbol)}?interval=${interval}&horizon=${horizon}`);
2731
+
2732
  if (currentSymbol !== symbol) return; // Discard if user switched
2733
+
2734
  renderAnalysisPanel(symbol, interval, fData);
2735
+ renderCompactGauges(symbol, interval, fData);
2736
 
2737
  let isBull = false;
2738
  let lastForecastVal = 0;
 
2743
  // Safeguard: Ensure we have candle data before aligning
2744
  if (!lastCandleData || !lastCandleData.time) {
2745
  updateStatus(`${symbol} · ${interval} — AI Sẵn sàng`, 'ok');
2746
+ return;
2747
  }
2748
 
2749
+ const forecastPoints = fData.forecast; // Backend already has anchor point is_actual
2750
+
2751
+ // Filter correctly: use is_actual as connection point
2752
+ const p50 = forecastPoints.map(d => ({ time: d.time, value: d.p50 }));
2753
+ const p10 = forecastPoints.map(d => ({ time: d.time, value: d.p10 }));
2754
+ const p90 = forecastPoints.map(d => ({ time: d.time, value: d.p90 }));
2755
+
2756
+ p50Series.setData(p50);
2757
+ p10Series.setData(p10);
2758
+ p90Series.setData(p90);
2759
 
2760
+ const anchorVal = forecastPoints[0]?.p50 ?? lastCandleData.close;
2761
+ lastForecastVal = forecastPoints[forecastPoints.length - 1]?.p50 ?? anchorVal;
2762
+ isBull = lastForecastVal >= anchorVal;
2763
 
2764
  const aiMain = isBull ? '#1dba8a' : '#e05560';
2765
  const aiBand = isBull ? 'rgba(29,186,138,0.1)' : 'rgba(224,85,96,0.1)';
 
2767
  p50Series.applyOptions({ color: aiMain, visible: true });
2768
  p10Series.applyOptions({ color: aiBand, visible: true });
2769
  p90Series.applyOptions({ color: aiBand, visible: true });
 
 
 
 
2770
  }
2771
 
2772
+ const currentPrice = lastCandleData?.close || 0;
2773
+ const pctChange = currentPrice > 0 ? ((lastForecastVal - currentPrice) / currentPrice) * 100 : 0;
2774
+ const pctLabel = (pctChange >= 0 ? '+' : '') + pctChange.toFixed(2) + '%';
2775
  const trend = isBull ? '↑ TĂNG' : '↓ GIẢM';
2776
  const confidence = fData.analysis?.confidence ?? fData.ensemble?.confidence ?? 0;
2777
+ const source = fData.source || 'N/A';
2778
+
2779
  if (currentSymbol !== symbol || timeframeSelect.value !== interval) return;
2780
+
2781
  updateStatus(
2782
+ `${symbol} - ${source} - ${interval} - ${formatPrice(currentPrice)} => ${formatPrice(lastForecastVal)} (${trend} ${pctLabel})`,
2783
  'ok'
2784
  );
2785
 
 
2792
  }
2793
 
2794
  /* ── Event listeners ───────────────────────── */
2795
+
2796
+
2797
+ /* ── Product Explorer Logic ────────────────── */
2798
+ const explorerOverlay = document.getElementById('explorerOverlay');
2799
+ const closeExplorerBtn = document.getElementById('closeExplorerBtn');
2800
+ const explorerCats = document.getElementById('explorerCats');
2801
+ const explorerGrid = document.getElementById('explorerGrid');
2802
+ const explorerSearchInput = document.getElementById('explorerSearchInput');
2803
+ let allSymbolsData = null;
2804
+ let currentExplorerCat = null;
2805
+
2806
+ let explorerHtmlCache = {}; // Category -> HTML string
2807
+
2808
+ async function openExplorer() {
2809
+ explorerOverlay.classList.add('active');
2810
+
2811
+ if (!allSymbolsData) {
2812
+ try {
2813
+ allSymbolsData = await apiRequest('/api/symbols');
2814
+ renderExplorerCats();
2815
+ if (allSymbolsData.categories.length > 0) {
2816
+ selectExplorerCat(allSymbolsData.categories[0]);
2817
+ }
2818
+ } catch (e) {
2819
+ console.error('Failed to load explorer symbols', e);
2820
+ }
2821
+ } else {
2822
+ // Ensure UI is initialized if loadSymbols was already called
2823
+ if (explorerCats.children.length === 0) {
2824
+ renderExplorerCats();
2825
+ if (allSymbolsData.categories.length > 0) {
2826
+ selectExplorerCat(allSymbolsData.categories[0]);
2827
+ }
2828
+ }
2829
+ }
2830
+ explorerSearchInput.focus();
2831
+ }
2832
+
2833
+ function closeExplorer() {
2834
+ explorerOverlay.classList.remove('active');
2835
+ }
2836
+
2837
+ function renderExplorerCats() {
2838
+ explorerCats.innerHTML = allSymbolsData.categories.map(cat => `
2839
+ <div class="explorer-cat-item ${cat === currentExplorerCat ? 'active' : ''}"
2840
+ onclick="selectExplorerCat('${cat}')">
2841
+ ${cat}
2842
+ </div>
2843
+ `).join('');
2844
+ }
2845
+
2846
+ function selectExplorerCat(cat) {
2847
+ currentExplorerCat = cat;
2848
+ renderExplorerCats();
2849
+ renderExplorerGrid();
2850
+ }
2851
+
2852
+ function renderExplorerGrid() {
2853
+ const search = explorerSearchInput.value.toLowerCase();
2854
+ if (!search && explorerHtmlCache[currentExplorerCat]) {
2855
+ explorerGrid.innerHTML = explorerHtmlCache[currentExplorerCat];
2856
+ return;
2857
+ }
2858
+ const symbols = allSymbolsData.symbols_by_category[currentExplorerCat] || [];
2859
+ const filtered = search
2860
+ ? symbols.filter(s => s.symbol.toLowerCase().includes(search) || s.label.toLowerCase().includes(search))
2861
+ : symbols;
2862
+ let html = '';
2863
+ for (let s of filtered) {
2864
+ html += `
2865
+ <div class="explorer-symbol-card" onclick="explorerSelectSymbol('${s.symbol}')">
2866
+ <div class="esc-ticker">${s.symbol}</div>
2867
+ <div class="esc-label">${s.label}</div>
2868
+ <div class="esc-source">${s.sources.join(' · ')}</div>
2869
+ </div>
2870
+ `;
2871
+ }
2872
+ explorerGrid.innerHTML = html;
2873
+ if (!search) explorerHtmlCache[currentExplorerCat] = html;
2874
+ }
2875
+
2876
+ function explorerSelectSymbol(sym) {
2877
+ switchSymbol(sym);
2878
+ closeExplorer();
2879
  }
2880
 
2881
+ let explorerSearchTimeout = null;
2882
+ explorerSearchInput.oninput = () => {
2883
+ clearTimeout(explorerSearchTimeout);
2884
+ explorerSearchTimeout = setTimeout(renderExplorerGrid, 150);
2885
+ };
2886
+ closeExplorerBtn.onclick = closeExplorer;
2887
+ toggleMarketBtn.onclick = openExplorer;
2888
 
2889
  timeframeSelect.onchange = refreshChart;
2890
  indicatorSelect.onchange = refreshChart;
 
2916
 
2917
  fitBtn.onclick = () => chart.timeScale().fitContent();
2918
 
2919
+ document.addEventListener('keydown', (e) => {
2920
+ if (e.key === 'Escape') {
2921
+ analysisPanel.classList.remove('active');
2922
+ searchResults.classList.remove('visible');
2923
+ closeExplorer();
2924
+ }
2925
+ });
2926
+
2927
+ /* ── Click out to close search & explorer ──── */
2928
  document.addEventListener('click', (e) => {
2929
  if (!symbolSearch.contains(e.target) && !searchResults.contains(e.target)) {
2930
  searchResults.classList.remove('visible');
2931
  }
2932
+ if (e.target === explorerOverlay) {
2933
+ closeExplorer();
2934
+ }
2935
  });
2936
 
2937
  /* ── Resize handler ────────────────────────── */
 
2943
  });
2944
  ro.observe(chartEl);
2945
 
2946
+ /* ── Theme Management ── */
2947
+ function applyTheme(theme) {
2948
+ const isDark = theme === 'dark';
2949
+ document.body.classList.toggle('dark-theme', isDark);
2950
+
2951
+ if (chart) {
2952
+ chart.applyOptions({
2953
+ layout: {
2954
+ textColor: isDark ? 'rgba(100, 150, 200, 0.85)' : '#475569',
2955
+ },
2956
+ watermark: {
2957
+ color: isDark ? 'rgba(34, 211, 238, 0.27)' : 'rgba(15, 23, 42, 0.12)',
2958
+ }
2959
+ });
2960
+ // No full refreshChart() here to keep it smooth (BUG-FIX)
2961
+ }
2962
+ }
2963
+
2964
+ document.getElementById('themeToggleBtn').onclick = () => {
2965
+ const newTheme = document.body.classList.contains('dark-theme') ? 'light' : 'dark';
2966
+ localStorage.setItem('kronos_theme', newTheme);
2967
+ applyTheme(newTheme);
2968
+ };
2969
+
2970
+ /* ── Bootstrap ── */
2971
  (async () => {
2972
+ // Restore theme preference (Default: light)
2973
+ const savedTheme = localStorage.getItem('kronos_theme') || 'light';
2974
+ applyTheme(savedTheme);
2975
+
2976
  await loadSymbols();
2977
  await refreshMarketStatus();
2978
+
2979
  // Start polling
2980
  setInterval(refreshMarketStatus, 60000); // 1m
2981
+
2982
 
2983
  // Default start
2984
  await switchSymbol('XAUUSD');
2985
+
2986
+ // Setup auto-refresh (P1-10)
2987
+ let autoRefreshTimer = null;
2988
+ function scheduleAutoRefresh() {
2989
+ if (autoRefreshTimer) clearTimeout(autoRefreshTimer);
2990
+
2991
+ // Interval logic: 1m for 1m/5m, 5m for 15m/1h, etc.
2992
+ const intv = timeframeSelect.value;
2993
+ let delay = 300000; // 5m default
2994
+ if (intv === '1m' || intv === '5m') delay = 60000;
2995
+ else if (intv === '15m' || intv === '30m') delay = 180000;
2996
+ else if (intv === '1h' || intv === '4h') delay = 600000;
2997
+
2998
+ autoRefreshTimer = setTimeout(async () => {
2999
+ if (!document.hidden) {
3000
+ console.log('[AutoRefresh] Triggering...');
3001
+ await refreshChart();
3002
+ }
3003
+ scheduleAutoRefresh();
3004
+ }, delay);
3005
+ }
3006
+ scheduleAutoRefresh();
3007
  })();
3008
  </script>
3009
  </body>
3010
 
3011
+ </html>
requirements.txt CHANGED
@@ -13,3 +13,4 @@ huggingface_hub==0.33.1
13
  matplotlib==3.9.3
14
  tqdm==4.67.1
15
  safetensors==0.6.2
 
 
13
  matplotlib==3.9.3
14
  tqdm==4.67.1
15
  safetensors==0.6.2
16
+ python-dotenv==1.1.0
run.bat CHANGED
@@ -1,73 +1,47 @@
1
  @echo off
2
- setlocal EnableExtensions EnableDelayedExpansion
3
- cd /d "%~dp0"
4
-
5
- title Kronos AI Analysis
6
-
7
- set "PYTHONUTF8=1"
8
- set "PYTHONIOENCODING=utf-8"
9
- set "KRONOS_PRELOAD=1"
10
- set "VENV_DIR=%CD%\venv"
11
- set "VENV_PY=%VENV_DIR%\Scripts\python.exe"
12
-
13
- echo ==========================================
14
- echo KRONOS AI ANALYSIS - STARTUP
15
- echo ==========================================
16
- echo.
17
-
18
- echo [1/5] Checking Python 3.11...
19
- py -3.11 -V >nul 2>&1
20
- if errorlevel 1 (
21
- echo [ERROR] Python 3.11 is required but was not found.
22
- echo [HINT] Install Python 3.11 and run this script again.
23
- goto :end
24
- )
25
-
26
- echo [2/5] Checking virtual environment...
27
- if exist "%VENV_PY%" (
28
- "%VENV_PY%" -V >nul 2>&1
29
- if errorlevel 1 (
30
- echo [WARN] Existing venv is invalid. Rebuilding a clean one...
31
- if exist "%CD%\venv_legacy_broken" rmdir /s /q "%CD%\venv_legacy_broken"
32
- ren "%VENV_DIR%" "venv_legacy_broken"
33
  )
34
  )
35
 
36
- if not exist "%VENV_PY%" (
37
- echo [INFO] Creating fresh Python 3.11 virtual environment...
38
- py -3.11 -m venv "%VENV_DIR%"
39
- if errorlevel 1 (
40
- echo [ERROR] Failed to create virtual environment.
41
- goto :end
42
- )
43
  )
44
 
45
- set "REQUESTS_CA_BUNDLE=%VENV_DIR%\Lib\site-packages\pip\_vendor\certifi\cacert.pem"
 
46
 
47
- echo [3/5] Installing or verifying dependencies...
48
- "%VENV_PY%" -m pip install -r requirements.txt
49
- if errorlevel 1 (
50
- echo [ERROR] Dependency installation failed.
51
- goto :end
52
- )
53
 
54
- echo [4/5] Running Kronos runtime self-check...
55
- "%VENV_PY%" -c "import backend.main as m; assert m.KRONOS_AVAILABLE, 'Kronos import failed'; print('KRONOS_PATH=', m.KRONOS_PATH); print('SYMBOLS=', len(m.SYMBOLS))"
56
- if errorlevel 1 (
57
- echo [ERROR] Backend self-check failed.
58
- goto :end
59
- )
60
 
61
- echo [5/5] Starting backend...
62
- echo [INFO] Opening http://127.0.0.1:8000
63
- start "" http://127.0.0.1:8000
64
- echo.
65
- echo ------------------------------------------
66
- echo KEEP THIS WINDOW OPEN WHILE USING THE APP
67
- echo TO STOP: PRESS CTRL+C
68
- echo ------------------------------------------
69
- echo.
70
- "%VENV_PY%" -m uvicorn backend.main:app --host 127.0.0.1 --port 8000 --reload
71
 
72
- :end
73
- pause
 
 
 
 
1
  @echo off
2
+ TITLE Kronos AI Trading Terminal v6.0
3
+ COLOR 0B
4
+
5
+ echo ====================================================
6
+ echo KRONOS AI TRADING TERMINAL - STARTUP
7
+ echo ====================================================
8
+
9
+ :: Port to check
10
+ set PORT=7860
11
+
12
+ :: Check if port is already in use and try to free it
13
+ echo [1/3] Checking port %PORT%...
14
+ for /f "tokens=5" %%a in ('netstat -aon ^| findstr :%PORT% ^| findstr LISTENING') do (
15
+ if not "%%a"=="" (
16
+ echo [INFO] Port %PORT% is in use by PID %%a. Freeing port...
17
+ taskkill /F /PID %%a >nul 2>&1
18
+ timeout /t 2 >nul
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  )
20
  )
21
 
22
+ :: Check if virtual environment exists
23
+ if not exist "venv\" (
24
+ echo [ERROR] Virtual environment 'venv' not found.
25
+ echo Please create it using: python -m venv venv
26
+ pause
27
+ exit /b
 
28
  )
29
 
30
+ echo [2/3] Activating Virtual Environment...
31
+ call venv\Scripts\activate
32
 
33
+ echo [3/3] Launching AI Trading Terminal...
34
+ echo [INFO] Application will open in your browser automatically.
35
+ echo [INFO] Press CTRL+C in this window to stop the server.
 
 
 
36
 
37
+ :: Open browser in background
38
+ start http://localhost:%PORT%
 
 
 
 
39
 
40
+ :: Run the app
41
+ python app.py
 
 
 
 
 
 
 
 
42
 
43
+ if %ERRORLEVEL% neq 0 (
44
+ echo.
45
+ echo [ERROR] Backend failed to start.
46
+ pause
47
+ )
update_symbols.py ADDED
@@ -0,0 +1,84 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import re
2
+ import sys
3
+
4
+ def main():
5
+ with open('backend/main.py', 'r', encoding='utf-8') as f:
6
+ content = f.read()
7
+
8
+ # Update priorities
9
+ content = content.replace(
10
+ '"Cổ phiếu Mỹ": ["yfinance", "finnhub", "twelvedata"],',
11
+ '"Cổ phiếu Mỹ": ["bybit", "yfinance", "finnhub", "twelvedata"],'
12
+ )
13
+ content = content.replace(
14
+ '"Trái phiếu": ["yfinance"],',
15
+ '"Trái phiếu": ["bybit", "yfinance"],'
16
+ )
17
+ content = content.replace(
18
+ '"ETF": ["yfinance", "finnhub"],',
19
+ '"ETF": ["bybit", "yfinance", "finnhub"],'
20
+ )
21
+ content = content.replace(
22
+ 'DEFAULT_SOURCE_PRIORITY: List[str] = ["yfinance", "twelvedata", "finnhub", "binance"]',
23
+ 'DEFAULT_SOURCE_PRIORITY: List[str] = ["bybit", "yfinance", "twelvedata", "finnhub", "binance"]'
24
+ )
25
+
26
+ # We will do regex replacements for specific symbols
27
+ replacements = {
28
+ r'"DXY":\s*_s\("DXY",[^\{]+\{([^}]+)\}': lambda m: m.group(0).replace('"bybit":"DXY"', '"bybit":"DXY+"'),
29
+ r'"VIX":\s*_s\("VIX",[^\{]+\{([^}]+)\}': lambda m: m.group(0).replace(m.group(1), '"bybit":"VIX+", ' + m.group(1)) if '"bybit"' not in m.group(1) else m.group(0),
30
+ r'"US10Y":\s*_s\("US10Y",[^\{]+\{([^}]+)\}': lambda m: m.group(0).replace(m.group(1), '"bybit":"US10Y+", ' + m.group(1)) if '"bybit"' not in m.group(1) else m.group(0),
31
+
32
+ # Indices
33
+ r'"DOW30":\s*_s\("DOW30",[^\{]+\{([^}]+)\}': lambda m: m.group(0).replace('"bybit":"DJ30"', '"bybit":"US30"'),
34
+ r'"NIKKEI225":\s*_s\("NIKKEI225",[^\{]+\{([^}]+)\}': lambda m: m.group(0).replace('"bybit":"Nikkei225"', '"bybit":"JPN225"'),
35
+ r'"EU50":\s*_s\("EU50",[^\{]+\{([^}]+)\}': lambda m: m.group(0).replace(m.group(1), '"bybit":"EU50", ' + m.group(1)) if '"bybit"' not in m.group(1) else m.group(0),
36
+ r'"ASX200":\s*_s\("ASX200",[^\{]+\{([^}]+)\}': lambda m: m.group(0).replace(m.group(1), '"bybit":"AUS200", ' + m.group(1)) if '"bybit"' not in m.group(1) else m.group(0),
37
+
38
+ # Energy & Metals & Agri
39
+ r'"NATURAL_GAS":\s*_s\("NATURAL_GAS",[^\{]+\{([^}]+)\}': lambda m: m.group(0).replace('"bybit":"NG"', '"bybit":"NGAS"'),
40
+ r'"COPPER":\s*_s\("COPPER",[^\{]+\{([^}]+)\}': lambda m: m.group(0).replace('"bybit":"Copper"', '"bybit":"COPPER"'),
41
+ r'"WHEAT":\s*_s\("WHEAT",[^\{]+\{([^}]+)\}': lambda m: m.group(0).replace('"bybit":"Wheat"', '"bybit":"WHEAT"'),
42
+ r'"CORN":\s*_s\("CORN",[^\{]+\{([^}]+)\}': lambda m: m.group(0).replace('"bybit":"Corn"', '"bybit":"CORN"'),
43
+ r'"SOYBEAN":\s*_s\("SOYBEAN",[^\{]+\{([^}]+)\}': lambda m: m.group(0).replace('"bybit":"Soybean"', '"bybit":"SOYBEAN"'),
44
+ r'"SUGAR_11":\s*_s\("SUGAR_11",[^\{]+\{([^}]+)\}': lambda m: m.group(0).replace('"bybit":"Sugar"', '"bybit":"SUGAR"'),
45
+ r'"COFFEE_ARABICA":\s*_s\("COFFEE_ARABICA",[^\{]+\{([^}]+)\}': lambda m: m.group(0).replace('"bybit":"Coffee"', '"bybit":"COFFEE"'),
46
+ r'"COCOA":\s*_s\("COCOA",[^\{]+\{([^}]+)\}': lambda m: m.group(0).replace('"bybit":"Cocoa"', '"bybit":"COCOA"'),
47
+ r'"COTTON":\s*_s\("COTTON",[^\{]+\{([^}]+)\}': lambda m: m.group(0).replace('"bybit":"Cotton"', '"bybit":"COTTON"'),
48
+ }
49
+
50
+ for pat, rep in replacements.items():
51
+ content = re.sub(pat, rep, content, count=1)
52
+
53
+ # Stocks & ETF (add bybit to existing ones)
54
+ stocks_to_update = ["AAPL", "MSFT", "NVDA", "GOOGL", "AMZN", "META", "TSLA", "JPM", "V", "MA", "WMT", "AMD", "INTC", "NFLX", "DIS"]
55
+ for stock in stocks_to_update:
56
+ pat = r'"XYZ":\s*_s\("XYZ",[^{]+\{([^}]+)\}'.replace('XYZ', stock)
57
+ def repl(m, s=stock):
58
+ if '"bybit"' not in m.group(1):
59
+ return m.group(0).replace(m.group(1), f'"bybit":"{s}", ' + m.group(1))
60
+ return m.group(0)
61
+ content = re.sub(pat, repl, content, count=1)
62
+
63
+ # Insert missing stocks and QQQ ETF
64
+ missing_symbols = """
65
+ "JNJ": _s("JNJ","Johnson & Johnson","JNJ","Cổ phiếu Mỹ",{"bybit":"JNJ","yfinance":"JNJ"}),
66
+ "PG": _s("PG","Procter & Gamble","P&G","Cổ phiếu Mỹ",{"bybit":"PG","yfinance":"PG"}),
67
+ "KO": _s("KO","Coca-Cola","Coca-Cola","Cổ phiếu Mỹ",{"bybit":"KO","yfinance":"KO"}),
68
+ "MCD": _s("MCD","McDonald's","McDonald's","Cổ phiếu Mỹ",{"bybit":"MCD","yfinance":"MCD"}),
69
+ "BA": _s("BA","Boeing","Boeing","Cổ phiếu Mỹ",{"bybit":"BA","yfinance":"BA"}),
70
+ "BABA": _s("BABA","Alibaba","Alibaba","Cổ phiếu Mỹ",{"bybit":"BABA","yfinance":"BABA"}),
71
+ "QQQ": _s("QQQ","Invesco QQQ Trust","QQQ","ETF",{"bybit":"QQQ+","yfinance":"QQQ"}),
72
+ """
73
+ # Insert right before Cổ phiếu Việt Nam
74
+ if '"JNJ"' not in content:
75
+ content = content.replace(
76
+ ' # ══════════════════════════════════════════════════════════════════════\n # 9. CỔ PHIẾU VIỆT NAM',
77
+ missing_symbols + '\n # ══════════════════════════════════════════════════════════════════════\n # 9. CỔ PHIẾU VIỆT NAM'
78
+ )
79
+
80
+ with open('backend/main.py', 'w', encoding='utf-8') as f:
81
+ f.write(content)
82
+
83
+ if __name__ == '__main__':
84
+ main()