Thang6822 commited on
Commit
6ed48fa
·
1 Parent(s): dbfd1de

Deploy v6.0: Dashboard Scoring Fix, UI Refinement, and Auto-Scaling

Browse files
.gitignore CHANGED
@@ -10,3 +10,4 @@ build/
10
  dist/
11
  scratch/
12
  *.spec
 
 
10
  dist/
11
  scratch/
12
  *.spec
13
+ data/
SOURCES_AND_SYMBOLS.md DELETED
@@ -1,242 +0,0 @@
1
- # NGUỒN DỮ LIỆU MIỄN PHÍ — XÁC NHẬN & PHÂN LOẠI MÃ GIAO DỊCH
2
- # Free Data Sources — Confirmed & Symbol Classification v4.0
3
-
4
- ═══════════════════════════════════════════════════════════════════════
5
- ## A. BẢNG NGUỒN DỮ LIỆU MIỄN PHÍ (Confirmed Free Sources)
6
- ═══════════════════════════════════════════════════════════════════════
7
-
8
- | Nguồn | URL cơ sở | API Key? | Giới hạn miễn phí | Tốt nhất cho |
9
- |--------------|----------------------------------------|----------|--------------------------|---------------------------|
10
- | Binance | api.binance.com/api/v3 | Không | 1200 req/min | Crypto OHLCV (tốt nhất) |
11
- | Bybit V5 | api.bybit.com/v5/market | Không | 120 req/min | Crypto backup |
12
- | CoinGecko | api.coingecko.com/api/v3 | Không | 30 req/min | Crypto OHLCV + market cap |
13
- | yfinance | (scrapes finance.yahoo.com) | Không | Không giới hạn chính thức| Stocks, ETF, Forex, Chỉ số|
14
- | Finnhub | finnhub.io/api/v1 | Có (free)| 60 req/min | Forex (OANDA), Stock, Crypto|
15
- | Twelvedata | api.twelvedata.com | Có (free)| 800 credits/ngày, 8/phút | Forex, Stock, Crypto |
16
- | Alpha Vantage| alphavantage.co/query | Có (free)| 25 req/ngày | Forex fallback (hạn chế) |
17
- | FRED | fred.stlouisfed.org/graph/fredgraph | Không | Không giới hạn | Macro (lãi suất, DXY...) |
18
-
19
- ### Ưu tiên nguồn theo loại tài sản:
20
- - **Crypto**: Binance → Bybit → CoinGecko → yfinance → Finnhub
21
- - **Forex**: Twelvedata → Finnhub (OANDA) → yfinance
22
- - **Kim loại**: yfinance (futures =F) → Twelvedata → Finnhub
23
- - **Năng lượng**: yfinance (futures =F) → Twelvedata → Finnhub
24
- - **Nông sản**: yfinance (futures =F) → Twelvedata
25
- - **Chỉ số**: yfinance (^SYMBOL) → Twelvedata → Finnhub
26
- - **Cổ phiếu Mỹ**: yfinance → Finnhub → Twelvedata
27
- - **Cổ phiếu VN**: yfinance (.VN suffix)
28
- - **Trái phiếu**: yfinance (^TNX, ^TYX...)
29
-
30
- ═══════════════════════════════════════════════════════════════════════
31
- ## B. PHÂN LOẠI MÃ GIAO DỊCH CHUẨN
32
- ═══════════════════════════════════════════════════════════════════════
33
-
34
- ### 1. KIM LOẠI (Metals) — 13 mã
35
- | Mã | Tên VN | Ticker yfinance | Nguồn tốt nhất |
36
- |--------------|---------------------|-----------------|---------------------|
37
- | XAUUSD | Vàng | GC=F | Twelvedata, Finnhub |
38
- | XAGUSD | Bạc | SI=F | Twelvedata, Finnhub |
39
- | XPTUSD | Bạch kim | PL=F | Twelvedata, yfinance |
40
- | XPDUSD | Palladium | PA=F | Twelvedata, yfinance |
41
- | COPPER | Đồng COMEX | HG=F | yfinance, Twelvedata |
42
- | COPPER_LME | Đồng LME | HG=F | Twelvedata |
43
- | ALUMINUM | Nhôm COMEX | ALI=F | yfinance |
44
- | ALUMINUM_LME | Nhôm LME | LMAHDS03 | Twelvedata |
45
- | NICKEL_LME | Niken LME | NI1 | Twelvedata |
46
- | ZINC_LME | Kẽm LME | ZN1 | Twelvedata |
47
- | IRON_ORE | Quặng sắt | TIO=F | yfinance |
48
- | STEEL_HRC | Thép cuộn cán nóng | HRC=F | yfinance |
49
- | LITHIUM | Lithium (proxy ALB) | ALB | yfinance |
50
-
51
- ### 2. NĂNG LƯỢNG (Energy) — 8 mã
52
- | Mã | Tên VN | Ticker yfinance | Ghi chú |
53
- |--------------|---------------------|-----------------|----------------------|
54
- | WTI | Dầu thô WTI | CL=F | Chuẩn Mỹ |
55
- | BRENT | Dầu Brent | BZ=F | Chuẩn quốc tế |
56
- | NATURAL_GAS | Khí tự nhiên | NG=F | |
57
- | GASOLINE_RBOB| Xăng RBOB | RB=F | |
58
- | HEATING_OIL | Dầu sưởi | HO=F | |
59
- | LNG | Khí hoá lỏng proxy | LNG | Cổ phiếu Cheniere |
60
- | COAL | Than Newcastle | MTF=F | |
61
- | URANIUM | Uranium | UX=F | |
62
-
63
- ### 3. NÔNG SẢN (Agricultural) — 7 mã
64
- | Mã | Tên VN | Ticker yfinance |
65
- |--------------|---------------------|-----------------|
66
- | CORN | Ngô | ZC=F |
67
- | WHEAT | Lúa mì | ZW=F |
68
- | SOYBEAN | Đậu tương | ZS=F |
69
- | SOYBEAN_OIL | Dầu đậu tương | ZL=F |
70
- | SOYBEAN_MEAL | Khô đậu tương | ZM=F |
71
- | RICE | Gạo thô | ZR=F |
72
- | OATS | Yến mạch | ZO=F |
73
-
74
- ### 4. NGUYÊN LIỆU CÔNG NGHIỆP (Soft Commodities) — 10 mã
75
- | Mã | Tên VN | Ticker yfinance |
76
- |----------------|---------------------|-----------------|
77
- | COFFEE_ARABICA | Cà phê Arabica | KC=F |
78
- | COFFEE_ROBUSTA | Cà phê Robusta | RC=F |
79
- | COCOA | Ca cao | CC=F |
80
- | SUGAR_11 | Đường 11 | SB=F |
81
- | WHITE_SUGAR | Đường trắng | LSW=F |
82
- | COTTON | Bông | CT=F |
83
- | ORANGE_JUICE | Nước cam | OJ=F |
84
- | LUMBER | Gỗ xẻ | LBS=F |
85
- | RUBBER_RSS3 | Cao su RSS3 | JRU=F |
86
- | PALM_OIL | Dầu cọ thô | FCPO=F |
87
-
88
- ### 5. CRYPTO — 25 mã (Top by market cap)
89
- | Mã | Tên | Binance | CoinGecko ID |
90
- |----------|----------------|--------------|---------------------|
91
- | BTCUSD | Bitcoin | BTCUSDT | bitcoin |
92
- | ETHUSD | Ethereum | ETHUSDT | ethereum |
93
- | BNBUSD | BNB | BNBUSDT | binancecoin |
94
- | SOLUSD | Solana | SOLUSDT | solana |
95
- | XRPUSD | Ripple | XRPUSDT | ripple |
96
- | ADAUSD | Cardano | ADAUSDT | cardano |
97
- | AVAXUSD | Avalanche | AVAXUSDT | avalanche-2 |
98
- | DOTUSD | Polkadot | DOTUSDT | polkadot |
99
- | MATICUSD | Polygon | MATICUSDT | matic-network |
100
- | LINKUSD | Chainlink | LINKUSDT | chainlink |
101
- | LTCUSD | Litecoin | LTCUSDT | litecoin |
102
- | UNIUSD | Uniswap | UNIUSDT | uniswap |
103
- | ATOMUSD | Cosmos | ATOMUSDT | cosmos |
104
- | NEARUSD | NEAR Protocol | NEARUSDT | near |
105
- | APTUSD | Aptos | APTUSDT | aptos |
106
- | SUIUSD | Sui | SUIUSDT | sui |
107
- | ARBUSD | Arbitrum | ARBUSDT | arbitrum |
108
- | OPUSD | Optimism | OPUSDT | optimism |
109
- | INJUSD | Injective | INJUSDT | injective-protocol |
110
- | TONUSD | Toncoin | TONUSDT | the-open-network |
111
- | TRXUSD | Tron | TRXUSDT | tron |
112
- | XMRUSD | Monero | (Bybit only) | monero |
113
- | DOGEUSD | Dogecoin | DOGEUSDT | dogecoin |
114
- | SHIBAUSD | Shiba Inu | SHIBUSDT | shiba-inu |
115
- | PEPE | PEPE Coin | PEPEUSDT | pepe |
116
-
117
- ### 6. CẶP TIỀN (Forex) — 19 mã
118
- | Mã | Tên | Twelvedata | Finnhub | yfinance |
119
- |----------|---------------|--------------|------------------|--------------|
120
- | DXY | Chỉ số USD | — | — | DX-Y.NYB |
121
- | EURUSD | EUR/USD | EUR/USD | OANDA:EUR_USD | EURUSD=X |
122
- | GBPUSD | GBP/USD | GBP/USD | OANDA:GBP_USD | GBPUSD=X |
123
- | USDJPY | USD/JPY | USD/JPY | OANDA:USD_JPY | JPY=X |
124
- | USDCHF | USD/CHF | USD/CHF | OANDA:USD_CHF | CHF=X |
125
- | AUDUSD | AUD/USD | AUD/USD | OANDA:AUD_USD | AUDUSD=X |
126
- | USDCAD | USD/CAD | USD/CAD | OANDA:USD_CAD | CAD=X |
127
- | NZDUSD | NZD/USD | NZD/USD | OANDA:NZD_USD | NZDUSD=X |
128
- | GBPJPY | GBP/JPY | GBP/JPY | OANDA:GBP_JPY | GBPJPY=X |
129
- | EURJPY | EUR/JPY | EUR/JPY | OANDA:EUR_JPY | EURJPY=X |
130
- | EURGBP | EUR/GBP | EUR/GBP | OANDA:EUR_GBP | EURGBP=X |
131
- | CADCHF | CAD/CHF | CAD/CHF | OANDA:CAD_CHF | CADCHF=X |
132
- | AUDNZD | AUD/NZD | AUD/NZD | OANDA:AUD_NZD | AUDNZD=X |
133
- | USDVND | USD/VND | — | — | VND=X |
134
- | USDHKD | USD/HKD | USD/HKD | — | HKD=X |
135
- | USDSGD | USD/SGD | USD/SGD | — | SGD=X |
136
- | USDCNY | USD/CNY | USD/CNH | — | CNY=X |
137
- | USDINR | USD/INR | — | — | INR=X |
138
- | USDBRL | USD/BRL | — | — | BRL=X |
139
-
140
- ### 7. CHỈ SỐ (Stock Indices) — 20 mã
141
- | Mã | Tên | yfinance | Ghi chú |
142
- |------------|----------------------|------------|-----------------|
143
- | SP500 | S&P 500 | ^GSPC | Chuẩn Mỹ |
144
- | NASDAQ | Nasdaq Composite | ^IXIC | |
145
- | NASDAQ100 | Nasdaq 100 | ^NDX | |
146
- | DOW30 | Dow Jones 30 | ^DJI | |
147
- | RUSSELL2000| Russell 2000 | ^RUT | Small-cap Mỹ |
148
- | VIX | VIX Fear Index | ^VIX | |
149
- | VNINDEX | VN-Index (HOSE) | ^VNINDEX | Việt Nam |
150
- | HNX30 | HNX 30 | ^HNX30 | Việt Nam |
151
- | UK100 | FTSE 100 | ^FTSE | Anh |
152
- | DAX40 | DAX 40 | ^GDAXI | Đức |
153
- | EU50 | Euro Stoxx 50 | ^STOXX50E | Châu Âu |
154
- | CAC40 | CAC 40 | ^FCHI | Pháp |
155
- | NIKKEI225 | Nikkei 225 | ^N225 | Nhật Bản |
156
- | HSI | Hang Seng | ^HSI | Hong Kong |
157
- | SSE50 | SSE 50 | 000016.SS | Trung Quốc |
158
- | CSI300 | CSI 300 | 000300.SS | Trung Quốc |
159
- | ASX200 | ASX 200 | ^AXJO | Úc |
160
- | SENSEX | BSE Sensex | ^BSESN | Ấn Độ |
161
- | KOSPI | KOSPI | ^KS11 | Hàn Quốc |
162
- | SGX | Straits Times | ^STI | Singapore |
163
-
164
- ### 8. CỔ PHIẾU MỸ (US Stocks) — 21 mã
165
- AAPL, MSFT, NVDA, GOOGL, AMZN, META, TSLA, AVGO, JPM, V, MA,
166
- XOM, WMT, BAC, GS, AMD, INTC, NFLX, DIS, COIN, MSTR
167
- → Tất cả dùng yfinance + Finnhub + Twelvedata (ticker trực tiếp)
168
-
169
- ### 9. CỔ PHIẾU VIỆT NAM (VN Stocks) — 25 mã (yfinance .VN suffix)
170
- Ngân hàng: VCB, BID, CTG, TCB, MBB, VPB, ACB, HDB, SHB, STB
171
- Công nghệ: FPT
172
- Bất động sản: VIC, VHM
173
- FMCG: VNM, MSN, SAB
174
- Năng lượng/CN: GAS, PLX, POW, HPG, DGC
175
- Tiêu dùng: MWG
176
- Chứng khoán: SSI, VND, HCM
177
-
178
- ### 10. TRÁI PHIẾU & LÃI SUẤT (Bonds & Rates) — 5 mã
179
- | Mã | Tên | yfinance | Ghi chú |
180
- |--------|----------------------|--------------|------------------------|
181
- | US10Y | Trái phiếu Mỹ 10 năm | ^TNX | Chuẩn lãi suất toàn cầu|
182
- | US02Y | Trái phiếu Mỹ 2 năm | ^IRX | |
183
- | US30Y | Trái phiếu Mỹ 30 năm | ^TYX | |
184
- | DE10Y | Bund Đức 10 năm | ^DE10YT=RR | |
185
- | JP10Y | JGB Nhật 10 năm | ^JP10YT=RR | |
186
-
187
- ═══════════════════════════════════════════════════════════════════════
188
- ## C. API ENDPOINTS v4.0
189
- ═══════════════════════════════════════════════════════════════════════
190
-
191
- ### Dữ liệu lịch sử
192
- GET /api/symbols → Danh sách tất cả mã (có thể lọc theo category)
193
- GET /api/search?q=gold → Tìm kiếm mờ (fuzzy search)
194
- GET /api/historical/{symbol} → Nến OHLCV (interval, limit)
195
- GET /api/indicators/{symbol} → Chỉ báo kỹ thuật (RSI, MACD, BB, EMA, ATR)
196
-
197
- ### Giá thực tế
198
- GET /api/ticker/{symbol} → Giá cuối + 24h stats
199
- POST /api/watchlist/tickers → Batch ticker cho danh mục
200
- GET /api/crypto/market → Top N crypto theo vốn hoá (CoinGecko)
201
- WS /ws/price/{symbol} → WebSocket stream giá thực tế (5s)
202
-
203
- ### AI Forecast
204
- GET /api/forecast/{symbol} → Dự báo Kronos (horizon, quantiles 10/50/90)
205
-
206
- ### Thị trường
207
- GET /api/market-status → Trạng thái mở/đóng cửa các sàn thế giới
208
-
209
- ### Cache & Admin
210
- POST /api/switch → Xoá cache khi đổi symbol/interval
211
- DELETE /api/cache/{sym}/{iv} → Xoá cache cụ thể
212
- DELETE /api/cache → Xoá tất cả cache
213
- GET /api/cache/stats → Thống kê cache
214
- GET /api/health → Trạng thái server
215
-
216
- ═══════════════════════════════════════════════════════════════════════
217
- ## D. CÀI ĐẶT & CHẠY
218
- ═══════════════════════════════════════════════════════════════════════
219
-
220
- # 1. Cài đặt dependencies
221
- pip install -r requirements.txt
222
-
223
- # 2. Cài pytz (cần cho market-status)
224
- pip install pytz
225
-
226
- # 3. Chạy server
227
- uvicorn main:app --host 0.0.0.0 --port 8000 --reload
228
-
229
- # 4. Xem API docs
230
- http://localhost:8000/docs
231
-
232
- # 5. Kiểm tra health
233
- curl http://localhost:8000/api/health
234
-
235
- # 6. Test lấy giá vàng
236
- curl "http://localhost:8000/api/historical/XAUUSD?interval=1h&limit=100"
237
-
238
- # 7. Test chỉ báo kỹ thuật BTC
239
- curl "http://localhost:8000/api/indicators/BTCUSD?interval=4h"
240
-
241
- # 8. Test thị trường đang mở
242
- curl http://localhost:8000/api/market-status
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
all_symbols_final.txt DELETED
@@ -1,643 +0,0 @@
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
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
all_symbols_3.txt → backend/__init__.py RENAMED
File without changes
backend/check_eurusd.py DELETED
@@ -1,14 +0,0 @@
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_wallet.py DELETED
@@ -1,39 +0,0 @@
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 DELETED
@@ -1,44 +0,0 @@
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 DELETED
@@ -1,26 +0,0 @@
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 DELETED
@@ -1,36 +0,0 @@
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())
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
{Kronos-master → backend/kronos_core}/.gitignore RENAMED
File without changes
{Kronos-master → backend/kronos_core}/LICENSE RENAMED
File without changes
{Kronos-master → backend/kronos_core}/README.md RENAMED
File without changes
{Kronos-master → backend/kronos_core}/model/__init__.py RENAMED
File without changes
{Kronos-master → backend/kronos_core}/model/kronos.py RENAMED
File without changes
{Kronos-master → backend/kronos_core}/model/module.py RENAMED
File without changes
{Kronos-master → backend/kronos_core}/requirements.txt RENAMED
File without changes
backend/list_linear.py DELETED
@@ -1,14 +0,0 @@
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 DELETED
@@ -1,38 +0,0 @@
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
@@ -64,6 +64,8 @@ from dotenv import load_dotenv
64
 
65
  # D-1: Load environment variables from .env file (v6.0)
66
  load_dotenv()
 
 
67
  import re
68
  import sys
69
  import time
@@ -201,7 +203,7 @@ else:
201
  PROJECT_ROOT = os.path.dirname(CURRENT_DIR)
202
  logger.info("Running in DEV mode. PROJECT_ROOT: %s", PROJECT_ROOT)
203
 
204
- KRONOS_PATH = os.path.join(PROJECT_ROOT, "Kronos-master")
205
 
206
  if KRONOS_PATH not in sys.path:
207
  sys.path.append(KRONOS_PATH)
@@ -314,7 +316,7 @@ class PersistentCache:
314
  except Exception as ex:
315
  logger.error("[Persistence] Eviction error: %s", ex)
316
 
317
- persistent_cache = PersistentCache(os.path.join(PROJECT_ROOT, "kronos_v5.db"))
318
 
319
 
320
  # B-12: Global Configuration Instances
@@ -1409,6 +1411,26 @@ def _stoch_rsi(close: np.ndarray, rsi_period=14, stoch_period=14,
1409
  return k_smooth.values, d_smooth.values
1410
 
1411
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1412
  def _sma(arr: np.ndarray, period: int) -> np.ndarray:
1413
  """Vectorized Simple Moving Average."""
1414
  if len(arr) == 0: return np.array([], dtype=float)
@@ -1562,6 +1584,22 @@ def _ichimoku_base(high: np.ndarray, low: np.ndarray, period: int = 26) -> np.nd
1562
  return ((hh + ll) / 2.0).values
1563
 
1564
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1565
  def _vwma(close: np.ndarray, volume: np.ndarray, period: int = 20) -> np.ndarray:
1566
  """Vectorized Volume Weighted Moving Average."""
1567
  cv = pd.Series(close * volume)
@@ -1680,6 +1718,11 @@ def _osc_action(name: str, value: float, **kw) -> str:
1680
  """Classify oscillator value as 'Mua' / 'Bán' / 'Trung lập'."""
1681
  if value is None or math.isnan(value):
1682
  return "Trung lập"
 
 
 
 
 
1683
  if name == "roc":
1684
  return "Mua" if value > 1.0 else "Bán" if value < -1.0 else "Trung lập"
1685
  if name == "trix":
@@ -1689,13 +1732,19 @@ def _osc_action(name: str, value: float, **kw) -> str:
1689
  if name == "cmo":
1690
  return "Mua" if value > 20 else "Bán" if value < -20 else "Trung lập"
1691
  if name == "dpo":
1692
- return "Mua" if value > 0 else "Bán" if value < 0 else "Trung lập"
1693
  if name == "aroon":
1694
  return "Mua" if value > 25 else "Bán" if value < -25 else "Trung lập"
1695
  if name == "tsi":
1696
  return "Mua" if value > 5 else "Bán" if value < -5 else "Trung lập"
1697
  if name == "rsi":
1698
- return "Bán" if value > 70 else "Mua" if value < 30 else "Trung lập"
 
 
 
 
 
 
1699
  if name == "stoch":
1700
  return "Bán" if value > 80 else "Mua" if value < 20 else "Trung lập"
1701
  if name == "cci":
@@ -1707,7 +1756,12 @@ def _osc_action(name: str, value: float, **kw) -> str:
1707
  return "Trung lập"
1708
  return "Mua" if plus_di > minus_di else "Bán"
1709
  if name == "ao":
1710
- return "Mua" if value > 0 else "Bán" if value < 0 else "Trung lập"
 
 
 
 
 
1711
  if name == "momentum":
1712
  return "Mua" if value > 0 else "Bán" if value < 0 else "Trung lập"
1713
  if name == "macd":
@@ -1718,17 +1772,52 @@ def _osc_action(name: str, value: float, **kw) -> str:
1718
  if name == "williams":
1719
  return "Mua" if value < -80 else "Bán" if value > -20 else "Trung lập"
1720
  if name == "bbp":
1721
- return "Mua" if value > 0 else "Bán" if value < 0 else "Trung lập"
 
1722
  if name == "ultimate":
1723
  return "Bán" if value > 70 else "Mua" if value < 30 else "Trung lập"
1724
  return "Trung lập"
1725
 
1726
 
1727
- def _ma_action(price: float, ma_val: float) -> str:
1728
- """Classify MA as 'Mua' (price > MA) or 'Bán' (price < MA)."""
1729
  if ma_val is None or math.isnan(ma_val):
1730
  return "Trung lập"
1731
- return "Mua" if price > ma_val else "Bán"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1732
 
1733
 
1734
  def _osc_signal_score(name: str, value: float, **kw) -> float:
@@ -1739,7 +1828,12 @@ def _osc_signal_score(name: str, value: float, **kw) -> float:
1739
  scale = max(float(kw.get("scale", 1.0) or 1.0), 1e-8)
1740
 
1741
  if name == "rsi":
1742
- return _clamp((50.0 - value) / 25.0, -1.0, 1.0)
 
 
 
 
 
1743
  if name in {"stoch", "stoch_rsi"}:
1744
  return _clamp((50.0 - value) / 30.0, -1.0, 1.0)
1745
  if name == "cci":
@@ -1750,13 +1844,18 @@ def _osc_signal_score(name: str, value: float, **kw) -> float:
1750
  strength = _clamp((value - 18.0) / 22.0, 0.0, 1.0)
1751
  direction = math.tanh((plus_di - minus_di) / max(plus_di + minus_di, 10.0) * 3.0)
1752
  return _clamp(direction * strength, -1.0, 1.0)
1753
- if name in {"ao", "momentum", "bbp", "dpo"}:
1754
  return _clamp(math.tanh(value / scale), -1.0, 1.0)
 
 
 
 
 
1755
  if name == "macd":
1756
  signal = float(kw.get("signal", 0.0) or 0.0)
1757
  return _clamp(math.tanh((value - signal) / scale), -1.0, 1.0)
1758
  if name == "williams":
1759
- return _clamp(((-50.0) - value) / 30.0, -1.0, 1.0)
1760
  if name == "ultimate":
1761
  return _clamp((50.0 - value) / 25.0, -1.0, 1.0)
1762
  if name == "roc":
@@ -2542,43 +2641,43 @@ def _build_reasoning(
2542
 
2543
  # ── Technical Analysis Weights & Constants (Dashboard Rework v6.1) ────────────
2544
  OSC_WEIGHTS = {
2545
- "rsi": 2.5, # Leading indicator, battle-tested
2546
- "macd": 2.2, # Trend + momentum hybrid
2547
- "stoch_rsi": 1.8, # High sensitivity
2548
  "stoch": 1.3, # Classic momentum
2549
- "cci": 1.5, # Good for extreme detection
2550
- "adx": 1.5, # Trend strength (direction via DI)
2551
- "williams": 1.3, # Complement to RSI
2552
  "ultimate": 1.2, # Multi-period, less noise
2553
  "bbp": 1.0, # Trend-following
2554
  "ao": 0.9, # Noisy, short-term only
2555
  "momentum": 0.8, # Lagging, lowest weight
2556
- "roc": 1.4, # Clean rate-of-change confirmation
2557
- "trix": 1.2, # Smoothed trend momentum
2558
- "ppo": 1.6, # Percentage trend acceleration
2559
  "cmo": 1.3, # Momentum regime strength
2560
- "dpo": 1.0, # Mean-reversion / cycle context
2561
- "aroon": 1.5, # Trend freshness / breakout context
2562
- "tsi": 1.4, # Smoothed momentum quality
2563
  }
2564
 
2565
  MA_WEIGHT_MAP = {
2566
- "ema_200": 3.0, "sma_200": 2.8,
2567
  "ema_100": 2.3, "sma_100": 2.1,
2568
- "ema_50": 1.8, "sma_50": 1.6,
2569
  "ema_30": 1.3, "sma_30": 1.2,
2570
- "ema_20": 1.1, "sma_20": 1.0,
2571
  "ema_10": 0.8, "sma_10": 0.7,
2572
- "vwma_20": 1.5, "ichimoku": 1.4, "hull_9": 1.3
2573
  }
2574
 
2575
  def _extract_osc_key(label: str) -> str:
2576
  l = label.lower()
2577
- if "rsi" in l and "nhanh" not in l: return "rsi"
2578
  if "macd" in l: return "macd"
2579
  if "stochastic %k" in l: return "stoch"
2580
  if "nhanh" in l or "stoch_rsi" in l: return "stoch_rsi"
2581
- if "cci" in l: return "cci"
2582
  if "định hướng" in l or "adx" in l: return "adx"
2583
  if "williams" in l: return "williams"
2584
  if "ultimate" in l: return "ultimate"
@@ -2596,6 +2695,8 @@ def _extract_osc_key(label: str) -> str:
2596
 
2597
  def _get_ma_weight(label: str) -> float:
2598
  l = label.lower()
 
 
2599
  if "hàm mũ" in l:
2600
  p = re.findall(r"\d+", l)
2601
  if p: return MA_WEIGHT_MAP.get(f"ema_{p[0]}", 1.0)
@@ -2603,17 +2704,26 @@ def _get_ma_weight(label: str) -> float:
2603
  p = re.findall(r"\d+", l)
2604
  if p: return MA_WEIGHT_MAP.get(f"sma_{p[0]}", 1.0)
2605
  if "ichimoku" in l: return MA_WEIGHT_MAP["ichimoku"]
2606
- if "vwma" in l or "tỷ trọng tuyến tính" in l: return MA_WEIGHT_MAP["vwma_20"]
2607
  if "hull" in l: return MA_WEIGHT_MAP["hull_9"]
2608
  return 1.0
2609
 
2610
- def _gauge_to_signal(gauge: float) -> str:
2611
- """Unified 5-level signal converter."""
2612
- if gauge >= 75: return "Mua mạnh"
2613
- elif gauge >= 58: return "Mua"
2614
- elif gauge >= 42: return "Trung lập"
2615
- elif gauge >= 25: return "Bán"
2616
- else: return "Bán mạnh"
 
 
 
 
 
 
 
 
 
 
2617
 
2618
 
2619
  def _gauge_to_normalized_score(gauge: float) -> float:
@@ -2671,24 +2781,28 @@ def _forecast_path_metrics(p50_path: np.ndarray, last_close: float) -> Dict[str,
2671
  "mean_step_return_pct": round(mean_step_return_pct, 3),
2672
  }
2673
 
2674
- def _calc_osc_score(osc_data: list) -> dict:
2675
  """MODULE 1: Weighted scoring for oscillators."""
2676
  total_weight = 0.0
2677
  weighted_score = 0.0
2678
  buy = sell = neutral = 0
 
2679
 
2680
  for item in osc_data:
2681
- key = _extract_osc_key(item["name"])
2682
  w = OSC_WEIGHTS.get(key, 1.0)
2683
  v = float(item.get("score", 0.0) or 0.0)
2684
  # Robust case-insensitive check
2685
  act = str(item.get("action", "")).strip().lower()
2686
  if act == "mua":
2687
  buy += 1
 
2688
  elif act == "bán":
2689
  sell += 1
 
2690
  else:
2691
  neutral += 1
 
2692
 
2693
  weighted_score += w * v
2694
  total_weight += w
@@ -2698,28 +2812,33 @@ def _calc_osc_score(osc_data: list) -> dict:
2698
  return {
2699
  "gauge": round(gauge, 1),
2700
  "normalized_score": round(normalized, 4),
2701
- "signal": _gauge_to_signal(gauge),
2702
- "buy": buy, "sell": sell, "neutral": neutral
 
 
 
2703
  }
2704
 
2705
- def _calc_ma_score(ma_data: list, closes: np.ndarray) -> dict:
2706
  """MODULE 2: Period-weighted scoring for MAs + Cross Bonus."""
2707
  total_weight = 0.0
2708
  weighted_score = 0.0
2709
  buy = sell = neutral = 0
 
2710
 
2711
  for item in ma_data:
2712
- w = _get_ma_weight(item["name"])
2713
- v = 0.0
2714
  act = str(item.get("action", "")).strip().lower()
2715
  if act == "mua":
2716
- v = 1.0
2717
  buy += 1
 
2718
  elif act == "bán":
2719
- v = -1.0
2720
  sell += 1
 
2721
  else:
2722
  neutral += 1
 
2723
 
2724
  weighted_score += w * v
2725
  total_weight += w
@@ -2740,8 +2859,11 @@ def _calc_ma_score(ma_data: list, closes: np.ndarray) -> dict:
2740
  return {
2741
  "gauge": round(gauge, 1),
2742
  "normalized_score": round(normalized, 4),
2743
- "signal": _gauge_to_signal(gauge),
2744
  "buy": buy, "sell": sell, "neutral": neutral,
 
 
 
2745
  "golden_cross": cross_bonus > 0,
2746
  "death_cross": cross_bonus < 0
2747
  }
@@ -2844,7 +2966,7 @@ def _calc_ai_forecast_score(
2844
  "monotonicity": round(monotonicity * 100.0, 1),
2845
  "max_adverse_excursion_pct": round(adverse_excursion_pct, 2),
2846
  "path_metrics": path_metrics,
2847
- "signal": _gauge_to_signal(gauge)
2848
  }
2849
 
2850
  def _calc_summary_score(osc_score: dict, ma_score: dict, ai_score: dict) -> dict:
@@ -2885,9 +3007,18 @@ def _calc_summary_score(osc_score: dict, ma_score: dict, ai_score: dict) -> dict
2885
  }
2886
  }
2887
 
2888
- def _calc_technical_score_v2(osc_score: dict, ma_score: dict) -> dict:
2889
- """Blend oscillators and moving averages into a single technical gauge."""
2890
- base = (osc_score["gauge"] * 0.42) + (ma_score["gauge"] * 0.58)
 
 
 
 
 
 
 
 
 
2891
  osc_delta = osc_score["gauge"] - 50.0
2892
  ma_delta = ma_score["gauge"] - 50.0
2893
  osc_dir = math.copysign(1.0, osc_delta) if abs(osc_delta) >= 1.0 else 0.0
@@ -2906,10 +3037,13 @@ def _calc_technical_score_v2(osc_score: dict, ma_score: dict) -> dict:
2906
  return {
2907
  "gauge": round(final_gauge, 1),
2908
  "normalized_score": round(_gauge_to_normalized_score(final_gauge), 4),
2909
- "signal": _gauge_to_signal(final_gauge),
2910
  "buy": osc_score["buy"] + ma_score["buy"],
2911
  "sell": osc_score["sell"] + ma_score["sell"],
2912
  "neutral": osc_score["neutral"] + ma_score["neutral"],
 
 
 
2913
  "alignment": osc_dir == ma_dir and osc_dir != 0.0,
2914
  "components": {
2915
  "oscillators": round(osc_score["gauge"], 1),
@@ -2917,10 +3051,12 @@ def _calc_technical_score_v2(osc_score: dict, ma_score: dict) -> dict:
2917
  }
2918
  }
2919
 
2920
- def _calc_summary_score_v2(tech_score: dict, ai_score: dict) -> dict:
2921
- """Final decision score defined as the simple mean of technical and AI gauges."""
2922
- composite = (float(tech_score["gauge"]) + float(ai_score["gauge"])) / 2.0
2923
- final_gauge = max(5.0, min(95.0, composite))
 
 
2924
  dist = abs(final_gauge - 50.0)
2925
  conviction = "Rất mạnh" if dist >= 25 else "Mạnh" if dist >= 15 else "Trung bình" if dist >= 8 else "Yếu"
2926
 
@@ -2933,17 +3069,22 @@ def _calc_summary_score_v2(tech_score: dict, ai_score: dict) -> dict:
2933
  return {
2934
  "gauge": round(final_gauge, 1),
2935
  "normalized_score": round(_gauge_to_normalized_score(final_gauge), 4),
2936
- "signal": _gauge_to_signal(final_gauge),
2937
  "conviction": conviction,
2938
  "bias": bias,
2939
  "buy": tech_score["buy"],
2940
  "sell": tech_score["sell"],
2941
  "neutral": tech_score["neutral"],
 
 
 
2942
  "components": {
2943
  "technical": round(tech_score["gauge"], 1),
2944
  "oscillators": round(tech_score["components"]["oscillators"], 1),
2945
  "moving_averages": round(tech_score["components"]["moving_averages"], 1),
2946
- "ai_forecast": round(ai_score["gauge"], 1)
 
 
2947
  }
2948
  }
2949
 
@@ -2968,6 +3109,9 @@ def _build_dashboard_payload(
2968
  "buy": technical_score["buy"],
2969
  "sell": technical_score["sell"],
2970
  "neutral": technical_score["neutral"],
 
 
 
2971
  },
2972
  "ai": {
2973
  "gauge": ai_score["gauge"],
@@ -2989,6 +3133,9 @@ def _build_dashboard_payload(
2989
  "signal": summary["signal"],
2990
  "conviction": summary["conviction"],
2991
  "bias": summary["bias"],
 
 
 
2992
  },
2993
  }
2994
 
@@ -3032,58 +3179,89 @@ def _build_trade_analysis(
3032
  last_close = closes[-1]
3033
 
3034
  def _lv(arr):
 
 
3035
  if isinstance(arr, pd.Series): arr = arr.values
3036
- v = arr[-1] if len(arr) else float('nan')
3037
- return None if (v is None or (isinstance(v, float) and math.isnan(v))) else round(float(v), 2)
 
 
 
3038
 
3039
  # ── 1. Oscillators ──
3040
  osc_data = []
3041
  atr_scale = max(float(indicators.get("atr", {}).get("value") or last_close * 0.01), max(last_close * 0.0015, 1e-6))
3042
- def _add_osc(label, val, action_name, **kw):
3043
  if isinstance(val, (np.ndarray, pd.Series, list)): v = _lv(val)
3044
  else: v = round(float(val), 2) if val is not None else None
3045
- act = _osc_action(action_name, v if v is not None else 0, **kw)
3046
- score = _osc_signal_score(action_name, v if v is not None else 0.0, **kw)
3047
- osc_data.append({"name": label, "value": v, "action": act, "score": round(score, 4)})
3048
-
3049
- _add_osc("Chỉ số Sức mạnh tương đối (14)", _rsi(closes, 14), "rsi")
3050
- _add_osc("Stochastic %K (14, 3, 3)", _stoch_rsi(closes, 14, 14, 3, 3)[0], "stoch")
3051
- _add_osc("Chỉ số Kênh hàng hóa (20)", _cci(highs, lows, closes, 20), "cci")
 
 
 
 
 
 
 
 
 
 
 
3052
  adx_vals = _adx(highs, lows, closes, 14)
3053
- _add_osc("Chỉ số Định hướng Trung bình (14)", adx_vals[0], "adx", plus_di=_lv(adx_vals[1]) or 0, minus_di=_lv(adx_vals[2]) or 0)
3054
- _add_osc("Chỉ số Dao động AO", _awesome_oscillator(highs, lows), "ao", scale=atr_scale * 0.8)
3055
- _add_osc("Xung lượng (10)", _momentum(closes, 10), "momentum", scale=atr_scale * 1.15)
 
 
3056
  macd_vals = _macd(closes, 12, 26, 9)
3057
- _add_osc("Cấp độ MACD (12, 26)", macd_vals[0], "macd", signal=_lv(macd_vals[1]) or 0, scale=atr_scale * 0.18)
3058
- _add_osc("Đường RSI Nhanh (3, 3, 14, 14)", _stoch_rsi(closes, 14, 14, 3, 3)[0], "stoch_rsi")
3059
- _add_osc("Vùng Phần trăm Williams (14)", _williams_r(highs, lows, closes, 14), "williams")
3060
- _add_osc("Sức Mạnh Giá Lên và Giá Xuống", _bull_bear_power(highs, lows, closes, 13), "bbp", scale=atr_scale * 0.9)
3061
- _add_osc("Dao động Ultimate (7, 14, 28)", _ultimate_oscillator(highs, lows, closes, 7, 14, 28), "ultimate")
3062
- _add_osc("Tốc độ biến động ROC (12)", _roc(closes, 12), "roc")
3063
- _add_osc("TRIX (18)", _trix(closes, 18), "trix")
3064
- _add_osc("PPO (12, 26)", _ppo(closes, 12, 26), "ppo")
3065
- _add_osc("CMO (14)", _cmo(closes, 14), "cmo")
3066
- _add_osc("DPO (20)", _dpo(closes, 20), "dpo", scale=atr_scale * 0.75)
3067
- _add_osc("Aroon Oscillator (25)", _aroon_oscillator(highs, lows, 25), "aroon")
3068
- _add_osc("TSI (25, 13)", _tsi(closes, 25, 13), "tsi")
3069
-
3070
- osc_score = _calc_osc_score(osc_data)
 
3071
 
3072
  # ── 2. Moving Averages ──
3073
  ma_data = []
3074
- def _add_ma(label, val_arr):
3075
  v = _lv(val_arr)
3076
- act = _ma_action(last_close, v if v is not None else last_close)
3077
- ma_data.append({"name": label, "value": v, "action": act})
 
 
 
 
 
 
 
3078
 
3079
  for p in [10, 20, 30, 50, 100, 200]:
3080
- _add_ma(f"Trung bình Trượt Hàm mũ ({p})", _ema(closes, p))
3081
- _add_ma(f"Đường Trung bình trượt Đơn giản ({p})", _sma(closes, p))
3082
- _add_ma("Đường sở Ichimoku (9, 26, 52, 26)", _ichimoku_base(highs, lows, 26))
3083
- _add_ma("Đường Trung bình di động Tỷ trọng tuyến tính (20)", _vwma(closes, vols, 20))
3084
- _add_ma("Đường trung bình trượt Hull (9)", _hull_ma(closes, 9))
3085
-
3086
- ma_score = _calc_ma_score(ma_data, closes)
 
 
 
 
 
3087
 
3088
  # ── 3. AI Forecast Gauge ──
3089
  if not blended:
@@ -3104,8 +3282,8 @@ def _build_trade_analysis(
3104
  )
3105
 
3106
  # ── 4. Summary ──
3107
- technical_score = _calc_technical_score_v2(osc_score, ma_score)
3108
- summary = _calc_summary_score_v2(technical_score, ai_score)
3109
 
3110
  # Pivot Points
3111
  last_h = float(highs[-2]) if len(highs) > 1 else float(highs[-1])
@@ -3116,6 +3294,7 @@ def _build_trade_analysis(
3116
 
3117
  return {
3118
  "style": "tradingview",
 
3119
  "dashboard": dashboard,
3120
  "summary": summary,
3121
  "technicals": technical_score,
@@ -3125,6 +3304,9 @@ def _build_trade_analysis(
3125
  "buy": osc_score["buy"],
3126
  "sell": osc_score["sell"],
3127
  "neutral": osc_score["neutral"],
 
 
 
3128
  "data": osc_data
3129
  },
3130
  "moving_averages": {
@@ -3133,6 +3315,9 @@ def _build_trade_analysis(
3133
  "buy": ma_score["buy"],
3134
  "sell": ma_score["sell"],
3135
  "neutral": ma_score["neutral"],
 
 
 
3136
  "golden_cross": ma_score["golden_cross"],
3137
  "death_cross": ma_score["death_cross"],
3138
  "data": ma_data
@@ -3581,7 +3766,7 @@ async def _source_selftest():
3581
  ("bybit", "https://api.bybit.com/v5/market/time"),
3582
  ("coingecko", "https://api.coingecko.com/api/v3/ping"),
3583
  ("twelvedata", f"https://api.twelvedata.com/api_usage?apikey={TWELVEDATA_API_KEY}"),
3584
- ("finnhub", f"https://finnhub.io/api/v1/status?token={FINNHUB_API_KEY}"),
3585
  ]
3586
  async with httpx.AsyncClient(timeout=8) as c:
3587
  for name, url in tests:
 
64
 
65
  # D-1: Load environment variables from .env file (v6.0)
66
  load_dotenv()
67
+ os.environ["HF_HUB_DISABLE_SYMLINKS_WARNING"] = "1"
68
+ os.environ["HF_HUB_DISABLE_TELEMETRY"] = "1"
69
  import re
70
  import sys
71
  import time
 
203
  PROJECT_ROOT = os.path.dirname(CURRENT_DIR)
204
  logger.info("Running in DEV mode. PROJECT_ROOT: %s", PROJECT_ROOT)
205
 
206
+ KRONOS_PATH = os.path.join(CURRENT_DIR, "kronos_core")
207
 
208
  if KRONOS_PATH not in sys.path:
209
  sys.path.append(KRONOS_PATH)
 
316
  except Exception as ex:
317
  logger.error("[Persistence] Eviction error: %s", ex)
318
 
319
+ persistent_cache = PersistentCache(os.path.join(PROJECT_ROOT, "data", "kronos_v5.db"))
320
 
321
 
322
  # B-12: Global Configuration Instances
 
1411
  return k_smooth.values, d_smooth.values
1412
 
1413
 
1414
+ def _stoch_kd(
1415
+ high: np.ndarray,
1416
+ low: np.ndarray,
1417
+ close: np.ndarray,
1418
+ k_period: int = 14,
1419
+ smooth_k: int = 3,
1420
+ d_period: int = 3,
1421
+ ) -> Tuple[np.ndarray, np.ndarray]:
1422
+ """Standard stochastic oscillator computed from price high/low/close."""
1423
+ high_s = pd.Series(high)
1424
+ low_s = pd.Series(low)
1425
+ close_s = pd.Series(close)
1426
+ hh = high_s.rolling(window=k_period).max()
1427
+ ll = low_s.rolling(window=k_period).min()
1428
+ raw_k = 100.0 * (close_s - ll) / (hh - ll).replace(0, np.nan)
1429
+ k = raw_k.rolling(window=smooth_k).mean()
1430
+ d = k.rolling(window=d_period).mean()
1431
+ return k.values, d.values
1432
+
1433
+
1434
  def _sma(arr: np.ndarray, period: int) -> np.ndarray:
1435
  """Vectorized Simple Moving Average."""
1436
  if len(arr) == 0: return np.array([], dtype=float)
 
1584
  return ((hh + ll) / 2.0).values
1585
 
1586
 
1587
+ def _ichimoku_cloud(high: np.ndarray, low: np.ndarray) -> Dict[str, np.ndarray]:
1588
+ """Basic Ichimoku lines for cloud-position scoring."""
1589
+ hs = pd.Series(high)
1590
+ ls = pd.Series(low)
1591
+ tenkan = ((hs.rolling(9).max() + ls.rolling(9).min()) / 2.0).values
1592
+ kijun = ((hs.rolling(26).max() + ls.rolling(26).min()) / 2.0).values
1593
+ span_a = ((pd.Series(tenkan) + pd.Series(kijun)) / 2.0).values
1594
+ span_b = ((hs.rolling(52).max() + ls.rolling(52).min()) / 2.0).values
1595
+ return {
1596
+ "tenkan": tenkan,
1597
+ "kijun": kijun,
1598
+ "span_a": span_a,
1599
+ "span_b": span_b,
1600
+ }
1601
+
1602
+
1603
  def _vwma(close: np.ndarray, volume: np.ndarray, period: int = 20) -> np.ndarray:
1604
  """Vectorized Volume Weighted Moving Average."""
1605
  cv = pd.Series(close * volume)
 
1718
  """Classify oscillator value as 'Mua' / 'Bán' / 'Trung lập'."""
1719
  if value is None or math.isnan(value):
1720
  return "Trung lập"
1721
+ last_close = float(kw.get("last_close", 0.0) or 0.0)
1722
+ ema50 = float(kw.get("ema50", last_close) or last_close)
1723
+ ema200 = float(kw.get("ema200", last_close) or last_close)
1724
+ ema50_prev = float(kw.get("ema50_prev", ema50) or ema50)
1725
+ atr = max(float(kw.get("atr", 0.0) or 0.0), 1e-8)
1726
  if name == "roc":
1727
  return "Mua" if value > 1.0 else "Bán" if value < -1.0 else "Trung lập"
1728
  if name == "trix":
 
1732
  if name == "cmo":
1733
  return "Mua" if value > 20 else "Bán" if value < -20 else "Trung lập"
1734
  if name == "dpo":
1735
+ return "Trung lập"
1736
  if name == "aroon":
1737
  return "Mua" if value > 25 else "Bán" if value < -25 else "Trung lập"
1738
  if name == "tsi":
1739
  return "Mua" if value > 5 else "Bán" if value < -5 else "Trung lập"
1740
  if name == "rsi":
1741
+ ema50_rising = ema50 >= ema50_prev
1742
+ above_ema200 = last_close >= ema200
1743
+ if value > 70:
1744
+ return "Trung lập" if above_ema200 and ema50_rising else "Bán"
1745
+ if value < 30:
1746
+ return "Mua" if above_ema200 else "Trung lập"
1747
+ return "Mua" if value >= 50 else "Bán"
1748
  if name == "stoch":
1749
  return "Bán" if value > 80 else "Mua" if value < 20 else "Trung lập"
1750
  if name == "cci":
 
1756
  return "Trung lập"
1757
  return "Mua" if plus_di > minus_di else "Bán"
1758
  if name == "ao":
1759
+ prev = float(kw.get("prev", value) or value)
1760
+ if value > 0 and value > prev:
1761
+ return "Mua"
1762
+ if value < 0 and value < prev:
1763
+ return "Bán"
1764
+ return "Trung lập"
1765
  if name == "momentum":
1766
  return "Mua" if value > 0 else "Bán" if value < 0 else "Trung lập"
1767
  if name == "macd":
 
1772
  if name == "williams":
1773
  return "Mua" if value < -80 else "Bán" if value > -20 else "Trung lập"
1774
  if name == "bbp":
1775
+ bbp_norm = float(kw.get("bbp_norm", value / atr))
1776
+ return "Mua" if bbp_norm > 0.5 else "Bán" if bbp_norm < -0.5 else "Trung lập"
1777
  if name == "ultimate":
1778
  return "Bán" if value > 70 else "Mua" if value < 30 else "Trung lập"
1779
  return "Trung lập"
1780
 
1781
 
1782
+ def _ma_action(price: float, ma_val: float, threshold_pct: float = 0.3) -> str:
1783
+ """Classify MA with a neutral zone when price is too close to the average."""
1784
  if ma_val is None or math.isnan(ma_val):
1785
  return "Trung lập"
1786
+ dist_pct = _pct(price, ma_val)
1787
+ if abs(dist_pct) < threshold_pct:
1788
+ return "Trung lập"
1789
+ return "Mua" if dist_pct > 0 else "Bán"
1790
+
1791
+
1792
+ def _ma_threshold_pct(ma_key: str) -> float:
1793
+ thresholds = {
1794
+ "ema_200": 3.0, "sma_200": 3.0,
1795
+ "ema_100": 2.2, "sma_100": 2.2,
1796
+ "ema_50": 1.5, "sma_50": 1.5,
1797
+ "ema_30": 1.2, "sma_30": 1.2,
1798
+ "ema_20": 1.0, "sma_20": 1.0,
1799
+ "ema_10": 0.8, "sma_10": 0.8,
1800
+ "ichimoku": 1.5, "hull_9": 1.0,
1801
+ }
1802
+ return thresholds.get(ma_key, 1.2)
1803
+
1804
+
1805
+ def _ma_signal_score(price: float, ma_val: float, ma_prev: Optional[float], threshold_pct: float) -> float:
1806
+ if ma_val is None or math.isnan(ma_val):
1807
+ return 0.0
1808
+ dist_pct = _pct(price, ma_val)
1809
+ score = math.tanh(dist_pct / max(threshold_pct, 0.1))
1810
+ if ma_prev is not None and not math.isnan(ma_prev):
1811
+ slope_pct = _pct(ma_val, ma_prev)
1812
+ if score > 0 and slope_pct > 0:
1813
+ score *= 1.2
1814
+ elif score > 0 and slope_pct < 0:
1815
+ score *= 0.8
1816
+ elif score < 0 and slope_pct < 0:
1817
+ score *= 1.2
1818
+ elif score < 0 and slope_pct > 0:
1819
+ score *= 0.8
1820
+ return _clamp(score, -1.0, 1.0)
1821
 
1822
 
1823
  def _osc_signal_score(name: str, value: float, **kw) -> float:
 
1828
  scale = max(float(kw.get("scale", 1.0) or 1.0), 1e-8)
1829
 
1830
  if name == "rsi":
1831
+ trend_bias = 0.0
1832
+ if kw.get("last_close", 0.0) >= kw.get("ema200", kw.get("last_close", 0.0)):
1833
+ trend_bias = 0.15 if value >= 50 else -0.05
1834
+ else:
1835
+ trend_bias = -0.15 if value <= 50 else 0.05
1836
+ return _clamp((value - 50.0) / 25.0 + trend_bias, -1.0, 1.0)
1837
  if name in {"stoch", "stoch_rsi"}:
1838
  return _clamp((50.0 - value) / 30.0, -1.0, 1.0)
1839
  if name == "cci":
 
1844
  strength = _clamp((value - 18.0) / 22.0, 0.0, 1.0)
1845
  direction = math.tanh((plus_di - minus_di) / max(plus_di + minus_di, 10.0) * 3.0)
1846
  return _clamp(direction * strength, -1.0, 1.0)
1847
+ if name in {"ao", "momentum"}:
1848
  return _clamp(math.tanh(value / scale), -1.0, 1.0)
1849
+ if name == "bbp":
1850
+ bbp_norm = float(kw.get("bbp_norm", value / max(float(kw.get("atr", scale) or scale), 1e-8)))
1851
+ return _clamp(math.tanh(bbp_norm / 1.0), -1.0, 1.0)
1852
+ if name == "dpo":
1853
+ return 0.0
1854
  if name == "macd":
1855
  signal = float(kw.get("signal", 0.0) or 0.0)
1856
  return _clamp(math.tanh((value - signal) / scale), -1.0, 1.0)
1857
  if name == "williams":
1858
+ return _clamp(((-value) - 50.0) / 30.0, -1.0, 1.0)
1859
  if name == "ultimate":
1860
  return _clamp((50.0 - value) / 25.0, -1.0, 1.0)
1861
  if name == "roc":
 
2641
 
2642
  # ── Technical Analysis Weights & Constants (Dashboard Rework v6.1) ────────────
2643
  OSC_WEIGHTS = {
2644
+ "rsi": 2.0,
2645
+ "macd": 2.5,
2646
+ "stoch_rsi": 1.2,
2647
  "stoch": 1.3, # Classic momentum
2648
+ "cci": 1.6,
2649
+ "adx": 1.8,
2650
+ "williams": 1.4,
2651
  "ultimate": 1.2, # Multi-period, less noise
2652
  "bbp": 1.0, # Trend-following
2653
  "ao": 0.9, # Noisy, short-term only
2654
  "momentum": 0.8, # Lagging, lowest weight
2655
+ "roc": 1.3,
2656
+ "trix": 1.6,
2657
+ "ppo": 1.5,
2658
  "cmo": 1.3, # Momentum regime strength
2659
+ "dpo": 0.3,
2660
+ "aroon": 1.6,
2661
+ "tsi": 1.8,
2662
  }
2663
 
2664
  MA_WEIGHT_MAP = {
2665
+ "ema_200": 2.5, "sma_200": 2.3,
2666
  "ema_100": 2.3, "sma_100": 2.1,
2667
+ "ema_50": 2.0, "sma_50": 1.8,
2668
  "ema_30": 1.3, "sma_30": 1.2,
2669
+ "ema_20": 1.4, "sma_20": 1.2,
2670
  "ema_10": 0.8, "sma_10": 0.7,
2671
+ "ichimoku": 1.6, "hull_9": 1.3
2672
  }
2673
 
2674
  def _extract_osc_key(label: str) -> str:
2675
  l = label.lower()
2676
+ if "sức mạnh tương đối" in l or ("rsi" in l and "nhanh" not in l): return "rsi"
2677
  if "macd" in l: return "macd"
2678
  if "stochastic %k" in l: return "stoch"
2679
  if "nhanh" in l or "stoch_rsi" in l: return "stoch_rsi"
2680
+ if "kênh hàng hóa" in l or "cci" in l: return "cci"
2681
  if "định hướng" in l or "adx" in l: return "adx"
2682
  if "williams" in l: return "williams"
2683
  if "ultimate" in l: return "ultimate"
 
2695
 
2696
  def _get_ma_weight(label: str) -> float:
2697
  l = label.lower()
2698
+ if l in MA_WEIGHT_MAP:
2699
+ return MA_WEIGHT_MAP[l]
2700
  if "hàm mũ" in l:
2701
  p = re.findall(r"\d+", l)
2702
  if p: return MA_WEIGHT_MAP.get(f"ema_{p[0]}", 1.0)
 
2704
  p = re.findall(r"\d+", l)
2705
  if p: return MA_WEIGHT_MAP.get(f"sma_{p[0]}", 1.0)
2706
  if "ichimoku" in l: return MA_WEIGHT_MAP["ichimoku"]
 
2707
  if "hull" in l: return MA_WEIGHT_MAP["hull_9"]
2708
  return 1.0
2709
 
2710
+ def _gauge_to_signal(gauge: float, interval: str = "1h") -> str:
2711
+ """Interval-aware 5-level signal converter."""
2712
+ thresholds = {
2713
+ "1m": (80, 62, 38, 20),
2714
+ "5m": (80, 62, 38, 20),
2715
+ "15m": (77, 60, 40, 23),
2716
+ "1h": (77, 60, 40, 23),
2717
+ "4h": (75, 58, 42, 25),
2718
+ "1d": (75, 58, 42, 25),
2719
+ "1w": (72, 56, 44, 28),
2720
+ }
2721
+ strong_buy, buy, sell, strong_sell = thresholds.get(interval, thresholds["1h"])
2722
+ if gauge >= strong_buy: return "Mua mạnh"
2723
+ if gauge >= buy: return "Mua"
2724
+ if gauge > sell: return "Trung lập"
2725
+ if gauge > strong_sell: return "Bán"
2726
+ return "Bán mạnh"
2727
 
2728
 
2729
  def _gauge_to_normalized_score(gauge: float) -> float:
 
2781
  "mean_step_return_pct": round(mean_step_return_pct, 3),
2782
  }
2783
 
2784
+ def _calc_osc_score(osc_data: list, interval: str = "1h") -> dict:
2785
  """MODULE 1: Weighted scoring for oscillators."""
2786
  total_weight = 0.0
2787
  weighted_score = 0.0
2788
  buy = sell = neutral = 0
2789
+ buy_weight = sell_weight = neutral_weight = 0.0
2790
 
2791
  for item in osc_data:
2792
+ key = item.get("key") or _extract_osc_key(item["name"])
2793
  w = OSC_WEIGHTS.get(key, 1.0)
2794
  v = float(item.get("score", 0.0) or 0.0)
2795
  # Robust case-insensitive check
2796
  act = str(item.get("action", "")).strip().lower()
2797
  if act == "mua":
2798
  buy += 1
2799
+ buy_weight += w
2800
  elif act == "bán":
2801
  sell += 1
2802
+ sell_weight += w
2803
  else:
2804
  neutral += 1
2805
+ neutral_weight += w
2806
 
2807
  weighted_score += w * v
2808
  total_weight += w
 
2812
  return {
2813
  "gauge": round(gauge, 1),
2814
  "normalized_score": round(normalized, 4),
2815
+ "signal": _gauge_to_signal(gauge, interval),
2816
+ "buy": buy, "sell": sell, "neutral": neutral,
2817
+ "buy_weight": round(buy_weight, 2),
2818
+ "sell_weight": round(sell_weight, 2),
2819
+ "neutral_weight": round(neutral_weight, 2),
2820
  }
2821
 
2822
+ def _calc_ma_score(ma_data: list, closes: np.ndarray, interval: str = "1h") -> dict:
2823
  """MODULE 2: Period-weighted scoring for MAs + Cross Bonus."""
2824
  total_weight = 0.0
2825
  weighted_score = 0.0
2826
  buy = sell = neutral = 0
2827
+ buy_weight = sell_weight = neutral_weight = 0.0
2828
 
2829
  for item in ma_data:
2830
+ w = _get_ma_weight(item.get("key") or item["name"])
2831
+ v = float(item.get("score", 0.0) or 0.0)
2832
  act = str(item.get("action", "")).strip().lower()
2833
  if act == "mua":
 
2834
  buy += 1
2835
+ buy_weight += w
2836
  elif act == "bán":
 
2837
  sell += 1
2838
+ sell_weight += w
2839
  else:
2840
  neutral += 1
2841
+ neutral_weight += w
2842
 
2843
  weighted_score += w * v
2844
  total_weight += w
 
2859
  return {
2860
  "gauge": round(gauge, 1),
2861
  "normalized_score": round(normalized, 4),
2862
+ "signal": _gauge_to_signal(gauge, interval),
2863
  "buy": buy, "sell": sell, "neutral": neutral,
2864
+ "buy_weight": round(buy_weight, 2),
2865
+ "sell_weight": round(sell_weight, 2),
2866
+ "neutral_weight": round(neutral_weight, 2),
2867
  "golden_cross": cross_bonus > 0,
2868
  "death_cross": cross_bonus < 0
2869
  }
 
2966
  "monotonicity": round(monotonicity * 100.0, 1),
2967
  "max_adverse_excursion_pct": round(adverse_excursion_pct, 2),
2968
  "path_metrics": path_metrics,
2969
+ "signal": _gauge_to_signal(gauge, interval)
2970
  }
2971
 
2972
  def _calc_summary_score(osc_score: dict, ma_score: dict, ai_score: dict) -> dict:
 
3007
  }
3008
  }
3009
 
3010
+ def _calc_technical_score_v2(osc_score: dict, ma_score: dict, regime: str = "sideways", interval: str = "1h") -> dict:
3011
+ """Blend oscillators and moving averages using regime-aware weighting."""
3012
+ if regime in {"stable_bull", "stable_bear"}:
3013
+ osc_weight, ma_weight = 0.45, 0.55
3014
+ elif regime in {"tight_range", "sideways"}:
3015
+ osc_weight, ma_weight = 0.60, 0.40
3016
+ elif regime in {"overextended_bull", "capitulation"}:
3017
+ osc_weight, ma_weight = 0.55, 0.45
3018
+ else:
3019
+ osc_weight, ma_weight = 0.50, 0.50
3020
+
3021
+ base = (osc_score["gauge"] * osc_weight) + (ma_score["gauge"] * ma_weight)
3022
  osc_delta = osc_score["gauge"] - 50.0
3023
  ma_delta = ma_score["gauge"] - 50.0
3024
  osc_dir = math.copysign(1.0, osc_delta) if abs(osc_delta) >= 1.0 else 0.0
 
3037
  return {
3038
  "gauge": round(final_gauge, 1),
3039
  "normalized_score": round(_gauge_to_normalized_score(final_gauge), 4),
3040
+ "signal": _gauge_to_signal(final_gauge, interval),
3041
  "buy": osc_score["buy"] + ma_score["buy"],
3042
  "sell": osc_score["sell"] + ma_score["sell"],
3043
  "neutral": osc_score["neutral"] + ma_score["neutral"],
3044
+ "buy_weight": round(osc_score.get("buy_weight", 0.0) + ma_score.get("buy_weight", 0.0), 2),
3045
+ "sell_weight": round(osc_score.get("sell_weight", 0.0) + ma_score.get("sell_weight", 0.0), 2),
3046
+ "neutral_weight": round(osc_score.get("neutral_weight", 0.0) + ma_score.get("neutral_weight", 0.0), 2),
3047
  "alignment": osc_dir == ma_dir and osc_dir != 0.0,
3048
  "components": {
3049
  "oscillators": round(osc_score["gauge"], 1),
 
3051
  }
3052
  }
3053
 
3054
+ def _calc_summary_score_v2(tech_score: dict, ai_score: dict, interval: str = "1h") -> dict:
3055
+ """Final decision score: simple average of Technical and AI gauges."""
3056
+ # TỔNG KẾT = (PHÂN TÍCH KỸ THUẬT + DỰ BÁO AI)/2
3057
+ ai_weight, tech_weight = 0.5, 0.5
3058
+ final_gauge = (float(tech_score["gauge"]) + float(ai_score["gauge"])) / 2.0
3059
+ final_gauge = max(5.0, min(95.0, final_gauge))
3060
  dist = abs(final_gauge - 50.0)
3061
  conviction = "Rất mạnh" if dist >= 25 else "Mạnh" if dist >= 15 else "Trung bình" if dist >= 8 else "Yếu"
3062
 
 
3069
  return {
3070
  "gauge": round(final_gauge, 1),
3071
  "normalized_score": round(_gauge_to_normalized_score(final_gauge), 4),
3072
+ "signal": _gauge_to_signal(final_gauge, interval),
3073
  "conviction": conviction,
3074
  "bias": bias,
3075
  "buy": tech_score["buy"],
3076
  "sell": tech_score["sell"],
3077
  "neutral": tech_score["neutral"],
3078
+ "buy_weight": tech_score.get("buy_weight", 0.0),
3079
+ "sell_weight": tech_score.get("sell_weight", 0.0),
3080
+ "neutral_weight": tech_score.get("neutral_weight", 0.0),
3081
  "components": {
3082
  "technical": round(tech_score["gauge"], 1),
3083
  "oscillators": round(tech_score["components"]["oscillators"], 1),
3084
  "moving_averages": round(tech_score["components"]["moving_averages"], 1),
3085
+ "ai_forecast": round(ai_score["gauge"], 1),
3086
+ "ai_weight": round(ai_weight, 2),
3087
+ "technical_weight": round(tech_weight, 2),
3088
  }
3089
  }
3090
 
 
3109
  "buy": technical_score["buy"],
3110
  "sell": technical_score["sell"],
3111
  "neutral": technical_score["neutral"],
3112
+ "buy_weight": technical_score.get("buy_weight", 0.0),
3113
+ "sell_weight": technical_score.get("sell_weight", 0.0),
3114
+ "neutral_weight": technical_score.get("neutral_weight", 0.0),
3115
  },
3116
  "ai": {
3117
  "gauge": ai_score["gauge"],
 
3133
  "signal": summary["signal"],
3134
  "conviction": summary["conviction"],
3135
  "bias": summary["bias"],
3136
+ "buy_weight": summary.get("buy_weight", 0.0),
3137
+ "sell_weight": summary.get("sell_weight", 0.0),
3138
+ "neutral_weight": summary.get("neutral_weight", 0.0),
3139
  },
3140
  }
3141
 
 
3179
  last_close = closes[-1]
3180
 
3181
  def _lv(arr):
3182
+ if arr is None: return None
3183
+ if isinstance(arr, (float, int)): return round(float(arr), 2)
3184
  if isinstance(arr, pd.Series): arr = arr.values
3185
+ v = arr[-1] if (hasattr(arr, '__len__') and len(arr) > 0) else float('nan')
3186
+ if v is None or (isinstance(v, float) and math.isnan(v)): return None
3187
+ return round(float(v), 2)
3188
+
3189
+ regime, regime_label = _classify_regime(indicators, last_close, _safe(indicators.get("atr", {}).get("pct"), 1.0))
3190
 
3191
  # ── 1. Oscillators ──
3192
  osc_data = []
3193
  atr_scale = max(float(indicators.get("atr", {}).get("value") or last_close * 0.01), max(last_close * 0.0015, 1e-6))
3194
+ def _add_osc(label, val, action_name, key=None, **kw):
3195
  if isinstance(val, (np.ndarray, pd.Series, list)): v = _lv(val)
3196
  else: v = round(float(val), 2) if val is not None else None
3197
+ osc_key = key or action_name
3198
+ osc_kwargs = {
3199
+ "last_close": last_close,
3200
+ "ema50": _lv(indicators["ema"].get("ema50")) or last_close,
3201
+ "ema200": _lv(indicators["ema"].get("ema200")) or last_close,
3202
+ "ema50_prev": float(_ema(closes, 50)[-6]) if len(closes) >= 6 and not math.isnan(_ema(closes, 50)[-6]) else (_lv(indicators["ema"].get("ema50")) or last_close),
3203
+ "atr": atr_scale,
3204
+ **kw,
3205
+ }
3206
+ act = _osc_action(action_name, v if v is not None else 0, **osc_kwargs)
3207
+ score = _osc_signal_score(action_name, v if v is not None else 0.0, **osc_kwargs)
3208
+ osc_data.append({"key": osc_key, "name": label, "value": v, "action": act, "score": round(score, 4)})
3209
+
3210
+ stoch_price_k, stoch_price_d = _stoch_kd(highs, lows, closes, 14, 3, 3)
3211
+ stoch_rsi_k, stoch_rsi_d = _stoch_rsi(closes, 14, 14, 3, 3)
3212
+ _add_osc("Chỉ số Sức mạnh tương đối (14)", _rsi(closes, 14), "rsi", key="rsi")
3213
+ _add_osc("Stochastic %K (14, 3, 3)", stoch_price_k, "stoch", key="stoch")
3214
+ _add_osc("Chỉ số Kênh hàng hóa (20)", _cci(highs, lows, closes, 20), "cci", key="cci")
3215
  adx_vals = _adx(highs, lows, closes, 14)
3216
+ _add_osc("Chỉ số Định hướng Trung bình (14)", adx_vals[0], "adx", key="adx", plus_di=_lv(adx_vals[1]) or 0, minus_di=_lv(adx_vals[2]) or 0)
3217
+ ao_vals = _awesome_oscillator(highs, lows)
3218
+ ao_prev = _lv(ao_vals[:-1]) if len(ao_vals) > 1 else _lv(ao_vals)
3219
+ _add_osc("Chỉ số Dao động AO", ao_vals, "ao", key="ao", scale=atr_scale * 0.8, prev=ao_prev or 0.0)
3220
+ _add_osc("Xung lượng (10)", _momentum(closes, 10), "momentum", key="momentum", scale=atr_scale * 1.15)
3221
  macd_vals = _macd(closes, 12, 26, 9)
3222
+ _add_osc("Cấp độ MACD (12, 26)", macd_vals[0], "macd", key="macd", signal=_lv(macd_vals[1]) or 0, scale=atr_scale * 0.18)
3223
+ _add_osc("Đường RSI Nhanh (3, 3, 14, 14)", stoch_rsi_k, "stoch_rsi", key="stoch_rsi")
3224
+ _add_osc("Vùng Phần trăm Williams (14)", _williams_r(highs, lows, closes, 14), "williams", key="williams")
3225
+ bbp_vals = _bull_bear_power(highs, lows, closes, 13)
3226
+ _add_osc("Sức Mạnh Giá Lên Giá Xuống", bbp_vals, "bbp", key="bbp", scale=atr_scale * 0.9, bbp_norm=(_lv(bbp_vals) or 0.0) / atr_scale)
3227
+ _add_osc("Dao động Ultimate (7, 14, 28)", _ultimate_oscillator(highs, lows, closes, 7, 14, 28), "ultimate", key="ultimate")
3228
+ _add_osc("Tốc độ biến động ROC (12)", _roc(closes, 12), "roc", key="roc")
3229
+ _add_osc("TRIX (18)", _trix(closes, 18), "trix", key="trix")
3230
+ _add_osc("PPO (12, 26)", _ppo(closes, 12, 26), "ppo", key="ppo")
3231
+ _add_osc("CMO (14)", _cmo(closes, 14), "cmo", key="cmo")
3232
+ _add_osc("DPO (20)", _dpo(closes, 20), "dpo", key="dpo", scale=atr_scale * 0.75)
3233
+ _add_osc("Aroon Oscillator (25)", _aroon_oscillator(highs, lows, 25), "aroon", key="aroon")
3234
+ _add_osc("TSI (25, 13)", _tsi(closes, 25, 13), "tsi", key="tsi")
3235
+
3236
+ osc_score = _calc_osc_score(osc_data, interval)
3237
 
3238
  # ── 2. Moving Averages ──
3239
  ma_data = []
3240
+ def _add_ma(label, val_arr, key=None, threshold_pct=None):
3241
  v = _lv(val_arr)
3242
+ ma_key = key or label.lower()
3243
+ threshold = threshold_pct if threshold_pct is not None else _ma_threshold_pct(ma_key)
3244
+ prev_val = None
3245
+ if isinstance(val_arr, (np.ndarray, list, pd.Series)) and len(val_arr) >= 6:
3246
+ prev_raw = val_arr[-6]
3247
+ prev_val = None if prev_raw is None or (isinstance(prev_raw, float) and math.isnan(prev_raw)) else float(prev_raw)
3248
+ act = _ma_action(last_close, v if v is not None else last_close, min(0.5, threshold * 0.25))
3249
+ score = _ma_signal_score(last_close, v if v is not None else last_close, prev_val, threshold)
3250
+ ma_data.append({"key": ma_key, "name": label, "value": v, "action": act, "score": round(score, 4)})
3251
 
3252
  for p in [10, 20, 30, 50, 100, 200]:
3253
+ _add_ma(f"Trung bình Trượt Hàm mũ ({p})", _ema(closes, p), key=f"ema_{p}")
3254
+ _add_ma(f"Đường Trung bình trượt Đơn giản ({p})", _sma(closes, p), key=f"sma_{p}")
3255
+ cloud = _ichimoku_cloud(highs, lows)
3256
+ span_a = _lv(cloud["span_a"])
3257
+ span_b = _lv(cloud["span_b"])
3258
+ cloud_top = max(span_a or last_close, span_b or last_close)
3259
+ cloud_bot = min(span_a or last_close, span_b or last_close)
3260
+ cloud_proxy = cloud_top if last_close >= cloud_top else cloud_bot if last_close <= cloud_bot else last_close
3261
+ _add_ma("Đám mây Ichimoku (9, 26, 52, 26)", np.array([cloud_proxy]), key="ichimoku", threshold_pct=1.5)
3262
+ _add_ma("Đường trung bình trượt Hull (9)", _hull_ma(closes, 9), key="hull_9")
3263
+
3264
+ ma_score = _calc_ma_score(ma_data, closes, interval)
3265
 
3266
  # ── 3. AI Forecast Gauge ──
3267
  if not blended:
 
3282
  )
3283
 
3284
  # ── 4. Summary ──
3285
+ technical_score = _calc_technical_score_v2(osc_score, ma_score, regime, interval)
3286
+ summary = _calc_summary_score_v2(technical_score, ai_score, interval)
3287
 
3288
  # Pivot Points
3289
  last_h = float(highs[-2]) if len(highs) > 1 else float(highs[-1])
 
3294
 
3295
  return {
3296
  "style": "tradingview",
3297
+ "regime": {"key": regime, "label": regime_label},
3298
  "dashboard": dashboard,
3299
  "summary": summary,
3300
  "technicals": technical_score,
 
3304
  "buy": osc_score["buy"],
3305
  "sell": osc_score["sell"],
3306
  "neutral": osc_score["neutral"],
3307
+ "buy_weight": osc_score.get("buy_weight", 0.0),
3308
+ "sell_weight": osc_score.get("sell_weight", 0.0),
3309
+ "neutral_weight": osc_score.get("neutral_weight", 0.0),
3310
  "data": osc_data
3311
  },
3312
  "moving_averages": {
 
3315
  "buy": ma_score["buy"],
3316
  "sell": ma_score["sell"],
3317
  "neutral": ma_score["neutral"],
3318
+ "buy_weight": ma_score.get("buy_weight", 0.0),
3319
+ "sell_weight": ma_score.get("sell_weight", 0.0),
3320
+ "neutral_weight": ma_score.get("neutral_weight", 0.0),
3321
  "golden_cross": ma_score["golden_cross"],
3322
  "death_cross": ma_score["death_cross"],
3323
  "data": ma_data
 
3766
  ("bybit", "https://api.bybit.com/v5/market/time"),
3767
  ("coingecko", "https://api.coingecko.com/api/v3/ping"),
3768
  ("twelvedata", f"https://api.twelvedata.com/api_usage?apikey={TWELVEDATA_API_KEY}"),
3769
+ ("finnhub", f"https://finnhub.io/api/v1/quote?symbol=AAPL&token={FINNHUB_API_KEY}"),
3770
  ]
3771
  async with httpx.AsyncClient(timeout=8) as c:
3772
  for name, url in tests:
backend/recovery.py DELETED
@@ -1,129 +0,0 @@
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 DELETED
@@ -1,43 +0,0 @@
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 DELETED
@@ -1,30 +0,0 @@
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 DELETED
@@ -1,45 +0,0 @@
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 DELETED
@@ -1,49 +0,0 @@
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_indices.py DELETED
@@ -1,43 +0,0 @@
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_no_auth.py DELETED
@@ -1,15 +0,0 @@
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 DELETED
@@ -1,43 +0,0 @@
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 DELETED
@@ -1,31 +0,0 @@
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 DELETED
@@ -1,43 +0,0 @@
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/Dark_BG.png CHANGED

Git LFS Details

  • SHA256: 4fef831149733daf4c259d703f115767fb8a05282db731d31e6966b58c17ccf9
  • Pointer size: 131 Bytes
  • Size of remote file: 296 kB

Git LFS Details

  • SHA256: 7d75d3e828c1033f73d3b6ec2ce08d0b5eb5de0401aca37d3088e47180ec4727
  • Pointer size: 131 Bytes
  • Size of remote file: 214 kB
frontend/Light_BG.png CHANGED

Git LFS Details

  • SHA256: 14f84503397b2e2bc4637eb819728f71d5c90d97df71036d8f89f9265f9ed332
  • Pointer size: 131 Bytes
  • Size of remote file: 308 kB

Git LFS Details

  • SHA256: 852b8051417c216d180e18052e05716b9d9b4cce7f27105f151b7fda88371fc1
  • Pointer size: 131 Bytes
  • Size of remote file: 189 kB
frontend/index.html CHANGED
@@ -1,4 +1,4 @@
1
- <!doctype html>
2
  <html lang="vi">
3
 
4
  <head>
@@ -100,8 +100,8 @@
100
  --logo-primary: #0ea5e9;
101
  --logo-secondary: #8b5cf6;
102
  --logo-glow: rgba(14, 165, 233, 0.3);
103
- --light-bg-image: url("Light_BG.png?v=3");
104
- --dark-bg-image: url("Dark_BG.png?v=3");
105
  }
106
 
107
  body.dark-theme {
@@ -184,8 +184,8 @@
184
  background-size: cover;
185
  background-position: center;
186
  background-repeat: no-repeat;
187
- opacity: 0.01;
188
- /* 1% opacity for the image as requested */
189
  transition: background 0.3s ease, opacity 0.3s ease;
190
  }
191
 
@@ -194,7 +194,7 @@
194
  background-size: cover;
195
  background-position: center;
196
  background-repeat: no-repeat;
197
- opacity: 0.01;
198
  }
199
 
200
  body {
@@ -350,7 +350,7 @@
350
  /* Search Omnibox (Shrunk 50% horizontally from original 320px) */
351
  .omnibox {
352
  position: relative;
353
- width: 160px;
354
  }
355
 
356
  .omnibox input {
@@ -816,8 +816,9 @@
816
 
817
  .gauge-hero-svg-wrap {
818
  position: relative;
819
- width: 320px;
820
- height: 220px;
 
821
  display: flex;
822
  align-items: center;
823
  justify-content: center;
@@ -886,6 +887,8 @@
886
  letter-spacing: 0.15em;
887
  text-shadow: 0 0 20px rgba(0, 0, 0, 0.5);
888
  margin-top: 5px;
 
 
889
  }
890
 
891
  .gauge-hero-signal.strong-buy {
@@ -1588,6 +1591,8 @@
1588
  font-weight: 800;
1589
  text-transform: uppercase;
1590
  letter-spacing: 0.02em;
 
 
1591
  }
1592
 
1593
  .compact-gauge-card.hero {
@@ -2088,7 +2093,7 @@
2088
  }
2089
 
2090
  .omnibox {
2091
- width: 250px;
2092
  }
2093
 
2094
  .omnibox input,
@@ -3196,7 +3201,8 @@
3196
  const prev = points[i - 1];
3197
  const curr = points[i];
3198
  const diff = (curr?.value ?? 0) - (prev?.value ?? 0);
3199
- const color = diff > EPSILON ? '#45a9ff' : diff < -EPSILON ? '#ff6b7a' : '#f6c94a';
 
3200
  const segSeries = chart.addLineSeries({
3201
  color,
3202
  lineWidth: 2,
@@ -3458,8 +3464,9 @@
3458
  </div>
3459
  </div>
3460
 
3461
- <!-- Dashboard Body -->
3462
- <div class="dash-body">
 
3463
 
3464
  <!-- TOP ROW: 3 Big Gauges -->
3465
  <div class="dash-gauges-hero">
@@ -3554,9 +3561,10 @@
3554
 
3555
  <div class="summary-disclaimer">
3556
  <strong>⚠ Cảnh báo</strong> — Thông tin phân tích kỹ thuật này không phải lời khuyên đầu tư. Hãy luôn quản lý rủi ro.
3557
- </div>
3558
 
3559
- </div>
 
3560
  `;
3561
 
3562
  // Close logic
@@ -3742,6 +3750,7 @@
3742
  if (currentSymbol !== symbol) return; // Discard if user switched
3743
 
3744
  renderAnalysisPanel(symbol, interval, fData);
 
3745
  renderCompactGauges(symbol, interval, fData);
3746
 
3747
  let isBull = false;
@@ -3912,6 +3921,7 @@
3912
  if (lastAnalysisPayload && lastAnalysisSymbol === currentSymbol) {
3913
  renderAnalysisPanel(lastAnalysisSymbol, lastAnalysisInterval, lastAnalysisPayload);
3914
  analysisPanel.classList.add('active');
 
3915
  } else {
3916
  // No cached data: fetch from API without reloading chart
3917
  analysisPanel.classList.add('active');
@@ -3922,6 +3932,7 @@
3922
  </div>
3923
  `;
3924
  fetchAIAnalysis(currentSymbol, timeframeSelect.value);
 
3925
  }
3926
  } else {
3927
  // Hide dashboard
@@ -3949,14 +3960,44 @@
3949
  }
3950
  });
3951
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3952
  /* ── Resize handler ────────────────────────── */
3953
  const ro = new ResizeObserver(() => {
3954
  chart.applyOptions({
3955
  width: chartEl.clientWidth,
3956
  height: chartEl.clientHeight,
3957
  });
 
3958
  });
3959
  ro.observe(chartEl);
 
3960
 
3961
  /* ── Theme Management ── */
3962
  function applyTheme(theme) {
@@ -4023,4 +4064,4 @@
4023
  </script>
4024
  </body>
4025
 
4026
- </html>
 
1
+ <!doctype html>
2
  <html lang="vi">
3
 
4
  <head>
 
100
  --logo-primary: #0ea5e9;
101
  --logo-secondary: #8b5cf6;
102
  --logo-glow: rgba(14, 165, 233, 0.3);
103
+ --light-bg-image: url("Light_BG.png?v=1");
104
+ --dark-bg-image: url("Dark_BG.png?v=1");
105
  }
106
 
107
  body.dark-theme {
 
184
  background-size: cover;
185
  background-position: center;
186
  background-repeat: no-repeat;
187
+ opacity: 1;
188
+ /* Main background set to full visibility as requested */
189
  transition: background 0.3s ease, opacity 0.3s ease;
190
  }
191
 
 
194
  background-size: cover;
195
  background-position: center;
196
  background-repeat: no-repeat;
197
+ opacity: 1;
198
  }
199
 
200
  body {
 
350
  /* Search Omnibox (Shrunk 50% horizontally from original 320px) */
351
  .omnibox {
352
  position: relative;
353
+ width: 80px;
354
  }
355
 
356
  .omnibox input {
 
816
 
817
  .gauge-hero-svg-wrap {
818
  position: relative;
819
+ width: 100%;
820
+ max-width: 320px;
821
+ aspect-ratio: 320 / 220;
822
  display: flex;
823
  align-items: center;
824
  justify-content: center;
 
887
  letter-spacing: 0.15em;
888
  text-shadow: 0 0 20px rgba(0, 0, 0, 0.5);
889
  margin-top: 5px;
890
+ background: none !important;
891
+ padding: 0 !important;
892
  }
893
 
894
  .gauge-hero-signal.strong-buy {
 
1591
  font-weight: 800;
1592
  text-transform: uppercase;
1593
  letter-spacing: 0.02em;
1594
+ background: none !important;
1595
+ padding: 0 !important;
1596
  }
1597
 
1598
  .compact-gauge-card.hero {
 
2093
  }
2094
 
2095
  .omnibox {
2096
+ width: 125px;
2097
  }
2098
 
2099
  .omnibox input,
 
3201
  const prev = points[i - 1];
3202
  const curr = points[i];
3203
  const diff = (curr?.value ?? 0) - (prev?.value ?? 0);
3204
+ // Tăng: xanh lá, Giảm: đỏ, Đi ngang: vàng nhạt
3205
+ const color = diff > EPSILON ? '#10b981' : diff < -EPSILON ? '#f43f5e' : '#fef08a';
3206
  const segSeries = chart.addLineSeries({
3207
  color,
3208
  lineWidth: 2,
 
3464
  </div>
3465
  </div>
3466
 
3467
+ <!-- Dashboard Body Scaler -->
3468
+ <div class="dash-body-scaler" style="flex:1; overflow-y:auto; overflow-x:hidden; display:flex; flex-direction:column;">
3469
+ <div class="dash-body" style="transform-origin: top center; transition: transform 0.2s ease;">
3470
 
3471
  <!-- TOP ROW: 3 Big Gauges -->
3472
  <div class="dash-gauges-hero">
 
3561
 
3562
  <div class="summary-disclaimer">
3563
  <strong>⚠ Cảnh báo</strong> — Thông tin phân tích kỹ thuật này không phải lời khuyên đầu tư. Hãy luôn quản lý rủi ro.
3564
+ </div> <!-- End summary-disclaimer -->
3565
 
3566
+ </div> <!-- End dash-body -->
3567
+ </div> <!-- End dash-body-scaler -->
3568
  `;
3569
 
3570
  // Close logic
 
3750
  if (currentSymbol !== symbol) return; // Discard if user switched
3751
 
3752
  renderAnalysisPanel(symbol, interval, fData);
3753
+ setTimeout(updateDashboardScale, 10);
3754
  renderCompactGauges(symbol, interval, fData);
3755
 
3756
  let isBull = false;
 
3921
  if (lastAnalysisPayload && lastAnalysisSymbol === currentSymbol) {
3922
  renderAnalysisPanel(lastAnalysisSymbol, lastAnalysisInterval, lastAnalysisPayload);
3923
  analysisPanel.classList.add('active');
3924
+ setTimeout(updateDashboardScale, 10);
3925
  } else {
3926
  // No cached data: fetch from API without reloading chart
3927
  analysisPanel.classList.add('active');
 
3932
  </div>
3933
  `;
3934
  fetchAIAnalysis(currentSymbol, timeframeSelect.value);
3935
+ setTimeout(updateDashboardScale, 10);
3936
  }
3937
  } else {
3938
  // Hide dashboard
 
3960
  }
3961
  });
3962
 
3963
+ /* ── Dashboard Auto-Scale ────────────────── */
3964
+ function updateDashboardScale() {
3965
+ const panel = document.getElementById('analysisPanel');
3966
+ if (!panel || !panel.classList.contains('active')) return;
3967
+ const body = panel.querySelector('.dash-body');
3968
+ const scaler = panel.querySelector('.dash-body-scaler');
3969
+ if (!body || !scaler) return;
3970
+
3971
+ const windowWidth = window.innerWidth;
3972
+ const windowHeight = window.innerHeight;
3973
+
3974
+ // Target width for 1:1 scale (Full HD standard)
3975
+ const targetWidth = 1400;
3976
+ const targetHeight = 850;
3977
+
3978
+ let scaleW = windowWidth / targetWidth;
3979
+ let scaleH = (windowHeight - 80) / targetHeight; // 80px for header
3980
+ let scale = Math.min(scaleW, scaleH);
3981
+
3982
+ // Clamp scale
3983
+ if (scale > 1.1) scale = 1.1;
3984
+ if (scale < 0.45) scale = 0.45;
3985
+
3986
+ body.style.transform = `scale(${scale})`;
3987
+ body.style.width = `${100 / scale}%`;
3988
+ // body.style.height = `${100 / scale}%`;
3989
+ }
3990
+
3991
  /* ── Resize handler ────────────────────────── */
3992
  const ro = new ResizeObserver(() => {
3993
  chart.applyOptions({
3994
  width: chartEl.clientWidth,
3995
  height: chartEl.clientHeight,
3996
  });
3997
+ updateDashboardScale();
3998
  });
3999
  ro.observe(chartEl);
4000
+ window.addEventListener('resize', updateDashboardScale);
4001
 
4002
  /* ── Theme Management ── */
4003
  function applyTheme(theme) {
 
4064
  </script>
4065
  </body>
4066
 
4067
+ </html>
update_symbols.py DELETED
@@ -1,84 +0,0 @@
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()