Thang6822 commited on
Commit
7c78a60
·
0 Parent(s):

Deploy to Hugging Face with LFS

Browse files
.gitattributes ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ *.png filter=lfs diff=lfs merge=lfs -text
2
+ *.jpg filter=lfs diff=lfs merge=lfs -text
3
+ *.db filter=lfs diff=lfs merge=lfs -text
.gitignore ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ venv/
2
+ __pycache__/
3
+ *.pyc
4
+ .env
5
+ .DS_Store
6
+ *.db
Kronos-master/.gitignore ADDED
@@ -0,0 +1,76 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ *.so
6
+ .Python
7
+ build/
8
+ develop-eggs/
9
+ dist/
10
+ downloads/
11
+ eggs/
12
+ .eggs/
13
+ lib/
14
+ lib64/
15
+ parts/
16
+ sdist/
17
+ var/
18
+ wheels/
19
+ *.egg-info/
20
+ .installed.cfg
21
+ *.egg
22
+ MANIFEST
23
+
24
+ # Jupyter Notebook
25
+ .ipynb_checkpoints
26
+
27
+ # PyCharm
28
+ .idea/
29
+
30
+ # VS Code
31
+ .vscode/
32
+
33
+ # macOS
34
+ .DS_Store
35
+ .AppleDouble
36
+ .LSOverride
37
+
38
+ # Windows
39
+ Thumbs.db
40
+ ehthumbs.db
41
+ Desktop.ini
42
+
43
+ # Linux
44
+ *~
45
+
46
+ # Data files (large files)
47
+ *.feather
48
+ *.parquet
49
+ *.h5
50
+ *.hdf5
51
+
52
+ # Model files (large files)
53
+ *.pth
54
+ *.pt
55
+ *.ckpt
56
+ *.bin
57
+
58
+ # Logs
59
+ *.log
60
+ logs/
61
+
62
+ # Environment
63
+ .env
64
+ .venv
65
+ env/
66
+ venv/
67
+ ENV/
68
+ env.bak/
69
+ venv.bak/
70
+
71
+ # Temporary files
72
+ *.tmp
73
+ *.temp
74
+ temp/
75
+ tmp/
76
+ .python-version
Kronos-master/LICENSE ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ MIT License
2
+
3
+ Copyright (c) 2025 ShiYu
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
Kronos-master/README.md ADDED
@@ -0,0 +1,337 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <div align="center">
2
+ <h2><b>Kronos: A Foundation Model for the Language of Financial Markets </b></h2>
3
+ </div>
4
+
5
+
6
+ <div align="center">
7
+
8
+ </a>
9
+ <a href="https://huggingface.co/NeoQuasar">
10
+ <img src="https://img.shields.io/badge/🤗-Hugging_Face-yellow" alt="Hugging Face">
11
+ </a>
12
+ <a href="https://shiyu-coder.github.io/Kronos-demo/"> <img src="https://img.shields.io/badge/🚀-Live_Demo-brightgreen" alt="Live Demo"> </a>
13
+ <a href="https://github.com/shiyu-coder/Kronos/graphs/commit-activity">
14
+ <img src="https://img.shields.io/github/last-commit/shiyu-coder/Kronos?color=blue" alt="Last Commit">
15
+ </a>
16
+ <a href="https://github.com/shiyu-coder/Kronos/stargazers">
17
+ <img src="https://img.shields.io/github/stars/shiyu-coder/Kronos?color=lightblue" alt="GitHub Stars">
18
+ </a>
19
+ <a href="https://github.com/shiyu-coder/Kronos/network/members">
20
+ <img src="https://img.shields.io/github/forks/shiyu-coder/Kronos?color=yellow" alt="GitHub Forks">
21
+ </a>
22
+ <a href="./LICENSE">
23
+ <img src="https://img.shields.io/github/license/shiyu-coder/Kronos?color=green" alt="License">
24
+ </a>
25
+
26
+ </div>
27
+
28
+ <div align="center">
29
+ <!-- Keep these links. Translations will automatically update with the README. -->
30
+ <a href="https://zdoc.app/de/shiyu-coder/Kronos">Deutsch</a> |
31
+ <a href="https://zdoc.app/es/shiyu-coder/Kronos">Español</a> |
32
+ <a href="https://zdoc.app/fr/shiyu-coder/Kronos">Français</a> |
33
+ <a href="https://zdoc.app/ja/shiyu-coder/Kronos">日本語</a> |
34
+ <a href="https://zdoc.app/ko/shiyu-coder/Kronos">한국어</a> |
35
+ <a href="https://zdoc.app/pt/shiyu-coder/Kronos">Português</a> |
36
+ <a href="https://zdoc.app/ru/shiyu-coder/Kronos">Русский</a> |
37
+ <a href="https://zdoc.app/zh/shiyu-coder/Kronos">中文</a>
38
+ </div>
39
+
40
+ <p align="center">
41
+
42
+ <img src="./figures/logo.png" width="100">
43
+
44
+ </p>
45
+
46
+ > Kronos is the **first open-source foundation model** for financial candlesticks (K-lines),
47
+ > trained on data from over **45 global exchanges**.
48
+
49
+
50
+ </div>
51
+
52
+ ## 📰 News
53
+ * 🚩 **[2025.11.10]** Kronos has been accpeted by AAAI 2026.
54
+ * 🚩 **[2025.08.17]** We have released the scripts for fine-tuning! Check them out to adapt Kronos to your own tasks.
55
+ * 🚩 **[2025.08.02]** Our paper is now available on [arXiv](https://arxiv.org/abs/2508.02739)!
56
+
57
+ <p align="center">
58
+
59
+ ## 📜 Introduction
60
+
61
+ **Kronos** is a family of decoder-only foundation models, pre-trained specifically for the "language" of financial markets—K-line sequences. Unlike general-purpose TSFMs, Kronos is designed to handle the unique, high-noise characteristics of financial data. It leverages a novel two-stage framework:
62
+ 1. A specialized tokenizer first quantizes continuous, multi-dimensional K-line data (OHLCV) into **hierarchical discrete tokens**.
63
+ 2. A large, autoregressive Transformer is then pre-trained on these tokens, enabling it to serve as a unified model for diverse quantitative tasks.
64
+
65
+ <p align="center">
66
+ <img src="figures/overview.png" alt="" align="center" width="700px" />
67
+ </p>
68
+
69
+ ## ✨ Live Demo
70
+ We have set up a live demo to visualize Kronos's forecasting results. The webpage showcases a forecast for the **BTC/USDT** trading pair over the next 24 hours.
71
+
72
+ **👉 [Access the Live Demo Here](https://shiyu-coder.github.io/Kronos-demo/)**
73
+
74
+ ## 📦 Model Zoo
75
+ We release a family of pre-trained models with varying capacities to suit different computational and application needs. All models are readily accessible from the Hugging Face Hub.
76
+
77
+ | Model | Tokenizer | Context length | Params | Open-source |
78
+ |--------------|---------------------------------------------------------------------------------| -------------- | ------ |---------------------------------------------------------------------------|
79
+ | Kronos-mini | [Kronos-Tokenizer-2k](https://huggingface.co/NeoQuasar/Kronos-Tokenizer-2k) | 2048 | 4.1M | ✅ [NeoQuasar/Kronos-mini](https://huggingface.co/NeoQuasar/Kronos-mini) |
80
+ | Kronos-small | [Kronos-Tokenizer-base](https://huggingface.co/NeoQuasar/Kronos-Tokenizer-base) | 512 | 24.7M | ✅ [NeoQuasar/Kronos-small](https://huggingface.co/NeoQuasar/Kronos-small) |
81
+ | Kronos-base | [Kronos-Tokenizer-base](https://huggingface.co/NeoQuasar/Kronos-Tokenizer-base) | 512 | 102.3M | ✅ [NeoQuasar/Kronos-base](https://huggingface.co/NeoQuasar/Kronos-base) |
82
+ | Kronos-large | [Kronos-Tokenizer-base](https://huggingface.co/NeoQuasar/Kronos-Tokenizer-base) | 512 | 499.2M | ❌ |
83
+
84
+
85
+ ## 🚀 Getting Started
86
+
87
+ ### Installation
88
+
89
+ 1. Install Python 3.10+, and then install the dependencies:
90
+
91
+ ```shell
92
+ pip install -r requirements.txt
93
+ ```
94
+
95
+ ### 📈 Making Forecasts
96
+
97
+ Forecasting with Kronos is straightforward using the `KronosPredictor` class. It handles data preprocessing, normalization, prediction, and inverse normalization, allowing you to get from raw data to forecasts in just a few lines of code.
98
+
99
+ **Important Note**: The `max_context` for `Kronos-small` and `Kronos-base` is **512**. This is the maximum sequence length the model can process. For optimal performance, it is recommended that your input data length (i.e., `lookback`) does not exceed this limit. The `KronosPredictor` will automatically handle truncation for longer contexts.
100
+
101
+ Here is a step-by-step guide to making your first forecast.
102
+
103
+ #### 1. Load the Tokenizer and Model
104
+
105
+ First, load a pre-trained Kronos model and its corresponding tokenizer from the Hugging Face Hub.
106
+
107
+ ```python
108
+ from model import Kronos, KronosTokenizer, KronosPredictor
109
+
110
+ # Load from Hugging Face Hub
111
+ tokenizer = KronosTokenizer.from_pretrained("NeoQuasar/Kronos-Tokenizer-base")
112
+ model = Kronos.from_pretrained("NeoQuasar/Kronos-small")
113
+ ```
114
+
115
+ #### 2. Instantiate the Predictor
116
+
117
+ Create an instance of `KronosPredictor`, passing the model, tokenizer, and desired device.
118
+
119
+ ```python
120
+ # Initialize the predictor
121
+ predictor = KronosPredictor(model, tokenizer, max_context=512)
122
+ ```
123
+
124
+ #### 3. Prepare Input Data
125
+
126
+ The `predict` method requires three main inputs:
127
+ - `df`: A pandas DataFrame containing the historical K-line data. It must include columns `['open', 'high', 'low', 'close']`. `volume` and `amount` are optional.
128
+ - `x_timestamp`: A pandas Series of timestamps corresponding to the historical data in `df`.
129
+ - `y_timestamp`: A pandas Series of timestamps for the future periods you want to predict.
130
+
131
+ ```python
132
+ import pandas as pd
133
+
134
+ # Load your data
135
+ df = pd.read_csv("./data/XSHG_5min_600977.csv")
136
+ df['timestamps'] = pd.to_datetime(df['timestamps'])
137
+
138
+ # Define context window and prediction length
139
+ lookback = 400
140
+ pred_len = 120
141
+
142
+ # Prepare inputs for the predictor
143
+ x_df = df.loc[:lookback-1, ['open', 'high', 'low', 'close', 'volume', 'amount']]
144
+ x_timestamp = df.loc[:lookback-1, 'timestamps']
145
+ y_timestamp = df.loc[lookback:lookback+pred_len-1, 'timestamps']
146
+ ```
147
+
148
+ #### 4. Generate Forecasts
149
+
150
+ Call the `predict` method to generate forecasts. You can control the sampling process with parameters like `T`, `top_p`, and `sample_count` for probabilistic forecasting.
151
+
152
+ ```python
153
+ # Generate predictions
154
+ pred_df = predictor.predict(
155
+ df=x_df,
156
+ x_timestamp=x_timestamp,
157
+ y_timestamp=y_timestamp,
158
+ pred_len=pred_len,
159
+ T=1.0, # Temperature for sampling
160
+ top_p=0.9, # Nucleus sampling probability
161
+ sample_count=1 # Number of forecast paths to generate and average
162
+ )
163
+
164
+ print("Forecasted Data Head:")
165
+ print(pred_df.head())
166
+ ```
167
+
168
+ The `predict` method returns a pandas DataFrame containing the forecasted values for `open`, `high`, `low`, `close`, `volume`, and `amount`, indexed by the `y_timestamp` you provided.
169
+
170
+ For efficient processing of multiple time series, Kronos provides a `predict_batch` method that enables parallel prediction on multiple datasets simultaneously. This is particularly useful when you need to forecast multiple assets or time periods at once.
171
+
172
+ ```python
173
+ # Prepare multiple datasets for batch prediction
174
+ df_list = [df1, df2, df3] # List of DataFrames
175
+ x_timestamp_list = [x_ts1, x_ts2, x_ts3] # List of historical timestamps
176
+ y_timestamp_list = [y_ts1, y_ts2, y_ts3] # List of future timestamps
177
+
178
+ # Generate batch predictions
179
+ pred_df_list = predictor.predict_batch(
180
+ df_list=df_list,
181
+ x_timestamp_list=x_timestamp_list,
182
+ y_timestamp_list=y_timestamp_list,
183
+ pred_len=pred_len,
184
+ T=1.0,
185
+ top_p=0.9,
186
+ sample_count=1,
187
+ verbose=True
188
+ )
189
+
190
+ # pred_df_list contains prediction results in the same order as input
191
+ for i, pred_df in enumerate(pred_df_list):
192
+ print(f"Predictions for series {i}:")
193
+ print(pred_df.head())
194
+ ```
195
+
196
+ **Important Requirements for Batch Prediction:**
197
+ - All series must have the same historical length (lookback window)
198
+ - All series must have the same prediction length (`pred_len`)
199
+ - Each DataFrame must contain the required columns: `['open', 'high', 'low', 'close']`
200
+ - `volume` and `amount` columns are optional and will be filled with zeros if missing
201
+
202
+ The `predict_batch` method leverages GPU parallelism for efficient processing and automatically handles normalization and denormalization for each series independently.
203
+
204
+ #### 5. Example and Visualization
205
+
206
+ For a complete, runnable script that includes data loading, prediction, and plotting, please see [`examples/prediction_example.py`](examples/prediction_example.py).
207
+
208
+ Running this script will generate a plot comparing the ground truth data against the model's forecast, similar to the one shown below:
209
+
210
+ <p align="center">
211
+ <img src="figures/prediction_example.png" alt="Forecast Example" align="center" width="600px" />
212
+ </p>
213
+
214
+ Additionally, we provide a script that makes predictions without Volume and Amount data, which can be found in [`examples/prediction_wo_vol_example.py`](examples/prediction_wo_vol_example.py).
215
+
216
+
217
+ ## 🔧 Finetuning on Your Own Data (A-Share Market Example)
218
+
219
+ We provide a complete pipeline for finetuning Kronos on your own datasets. As an example, we demonstrate how to use [Qlib](https://github.com/microsoft/qlib) to prepare data from the Chinese A-share market and conduct a simple backtest.
220
+
221
+ > **Disclaimer:** This pipeline is intended as a demonstration to illustrate the finetuning process. It is a simplified example and not a production-ready quantitative trading system. A robust quantitative strategy requires more sophisticated techniques, such as portfolio optimization and risk factor neutralization, to achieve stable alpha.
222
+
223
+ The finetuning process is divided into four main steps:
224
+
225
+ 1. **Configuration**: Set up paths and hyperparameters.
226
+ 2. **Data Preparation**: Process and split your data using Qlib.
227
+ 3. **Model Finetuning**: Finetune the Tokenizer and the Predictor models.
228
+ 4. **Backtesting**: Evaluate the finetuned model's performance.
229
+
230
+ ### Prerequisites
231
+
232
+ 1. First, ensure you have all dependencies from `requirements.txt` installed.
233
+ 2. This pipeline relies on `qlib`. Please install it:
234
+ ```shell
235
+ pip install pyqlib
236
+ ```
237
+ 3. You will need to prepare your Qlib data. Follow the [official Qlib guide](https://github.com/microsoft/qlib) to download and set up your data locally. The example scripts assume you are using daily frequency data.
238
+
239
+ ### Step 1: Configure Your Experiment
240
+
241
+ All settings for data, training, and model paths are centralized in `finetune/config.py`. Before running any scripts, please **modify the following paths** according to your environment:
242
+
243
+ * `qlib_data_path`: Path to your local Qlib data directory.
244
+ * `dataset_path`: Directory where the processed train/validation/test pickle files will be saved.
245
+ * `save_path`: Base directory for saving model checkpoints.
246
+ * `backtest_result_path`: Directory for saving backtesting results.
247
+ * `pretrained_tokenizer_path` and `pretrained_predictor_path`: Paths to the pre-trained models you want to start from (can be local paths or Hugging Face model names).
248
+
249
+ You can also adjust other parameters like `instrument`, `train_time_range`, `epochs`, and `batch_size` to fit your specific task. If you don't use [Comet.ml](https://www.comet.com/), set `use_comet = False`.
250
+
251
+ ### Step 2: Prepare the Dataset
252
+
253
+ Run the data preprocessing script. This script will load raw market data from your Qlib directory, process it, split it into training, validation, and test sets, and save them as pickle files.
254
+
255
+ ```shell
256
+ python finetune/qlib_data_preprocess.py
257
+ ```
258
+
259
+ After running, you will find `train_data.pkl`, `val_data.pkl`, and `test_data.pkl` in the directory specified by `dataset_path` in your config.
260
+
261
+ ### Step 3: Run the Finetuning
262
+
263
+ The finetuning process consists of two stages: finetuning the tokenizer and then the predictor. Both training scripts are designed for multi-GPU training using `torchrun`.
264
+
265
+ #### 3.1 Finetune the Tokenizer
266
+
267
+ This step adjusts the tokenizer to the data distribution of your specific domain.
268
+
269
+ ```shell
270
+ # Replace NUM_GPUS with the number of GPUs you want to use (e.g., 2)
271
+ torchrun --standalone --nproc_per_node=NUM_GPUS finetune/train_tokenizer.py
272
+ ```
273
+
274
+ The best tokenizer checkpoint will be saved to the path configured in `config.py` (derived from `save_path` and `tokenizer_save_folder_name`).
275
+
276
+ #### 3.2 Finetune the Predictor
277
+
278
+ This step finetunes the main Kronos model for the forecasting task.
279
+
280
+ ```shell
281
+ # Replace NUM_GPUS with the number of GPUs you want to use (e.g., 2)
282
+ torchrun --standalone --nproc_per_node=NUM_GPUS finetune/train_predictor.py
283
+ ```
284
+
285
+ The best predictor checkpoint will be saved to the path configured in `config.py`.
286
+
287
+ ### Step 4: Evaluate with Backtesting
288
+
289
+ Finally, run the backtesting script to evaluate your finetuned model. This script loads the models, performs inference on the test set, generates prediction signals (e.g., forecasted price change), and runs a simple top-K strategy backtest.
290
+
291
+ ```shell
292
+ # Specify the GPU for inference
293
+ python finetune/qlib_test.py --device cuda:0
294
+ ```
295
+
296
+ The script will output a detailed performance analysis in your console and generate a plot showing the cumulative return curves of your strategy against the benchmark, similar to the one below:
297
+
298
+ <p align="center">
299
+ <img src="figures/backtest_result_example.png" alt="Backtest Example" align="center" width="700px" />
300
+ </p>
301
+
302
+ ### 💡 From Demo to Production: Important Considerations
303
+
304
+ * **Raw Signals vs. Pure Alpha**: The signals generated by the model in this demo are raw predictions. In a real-world quantitative workflow, these signals would typically be fed into a portfolio optimization model. This model would apply constraints to neutralize exposure to common risk factors (e.g., market beta, style factors like size and value), thereby isolating the **"pure alpha"** and improving the strategy's robustness.
305
+ * **Data Handling**: The provided `QlibDataset` is an example. For different data sources or formats, you will need to adapt the data loading and preprocessing logic.
306
+ * **Strategy and Backtesting Complexity**: The simple top-K strategy used here is a basic starting point. Production-level strategies often incorporate more complex logic for portfolio construction, dynamic position sizing, and risk management (e.g., stop-loss/take-profit rules). Furthermore, a high-fidelity backtest should meticulously model transaction costs, slippage, and market impact to provide a more accurate estimate of real-world performance.
307
+
308
+ > **📝 AI-Generated Comments**: Please note that many of the code comments within the `finetune/` directory were generated by an AI assistant (Gemini 2.5 Pro) for explanatory purposes. While they aim to be helpful, they may contain inaccuracies. We recommend treating the code itself as the definitive source of logic.
309
+
310
+ ## 📖 Citation
311
+
312
+ If you use Kronos in your research, we would appreciate a citation to our [paper](https://arxiv.org/abs/2508.02739):
313
+
314
+ ```
315
+ @misc{shi2025kronos,
316
+ title={Kronos: A Foundation Model for the Language of Financial Markets},
317
+ author={Yu Shi and Zongliang Fu and Shuo Chen and Bohan Zhao and Wei Xu and Changshui Zhang and Jian Li},
318
+ year={2025},
319
+ eprint={2508.02739},
320
+ archivePrefix={arXiv},
321
+ primaryClass={q-fin.ST},
322
+ url={https://arxiv.org/abs/2508.02739},
323
+ }
324
+ ```
325
+
326
+ ## 📜 License
327
+ This project is licensed under the [MIT License](./LICENSE).
328
+
329
+
330
+
331
+
332
+
333
+
334
+
335
+
336
+
337
+
Kronos-master/model/__init__.py ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from .kronos import KronosTokenizer, Kronos, KronosPredictor
2
+
3
+ model_dict = {
4
+ 'kronos_tokenizer': KronosTokenizer,
5
+ 'kronos': Kronos,
6
+ 'kronos_predictor': KronosPredictor
7
+ }
8
+
9
+
10
+ def get_model_class(model_name):
11
+ if model_name in model_dict:
12
+ return model_dict[model_name]
13
+ else:
14
+ print(f"Model {model_name} not found in model_dict")
15
+ raise NotImplementedError
16
+
17
+
Kronos-master/model/kronos.py ADDED
@@ -0,0 +1,664 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import numpy as np
2
+ import pandas as pd
3
+ import torch
4
+ from huggingface_hub import PyTorchModelHubMixin
5
+ import sys
6
+
7
+ from tqdm import trange
8
+
9
+ sys.path.append("../")
10
+ from model.module import *
11
+
12
+
13
+ class KronosTokenizer(nn.Module, PyTorchModelHubMixin):
14
+ """
15
+ KronosTokenizer module for tokenizing input data using a hybrid quantization approach.
16
+
17
+ This tokenizer utilizes a combination of encoder and decoder Transformer blocks
18
+ along with the Binary Spherical Quantization (BSQuantizer) to compress and decompress input data.
19
+
20
+ Args:
21
+ d_in (int): Input dimension.
22
+ d_model (int): Model dimension.
23
+ n_heads (int): Number of attention heads.
24
+ ff_dim (int): Feed-forward dimension.
25
+ n_enc_layers (int): Number of encoder layers.
26
+ n_dec_layers (int): Number of decoder layers.
27
+ ffn_dropout_p (float): Dropout probability for feed-forward networks.
28
+ attn_dropout_p (float): Dropout probability for attention mechanisms.
29
+ resid_dropout_p (float): Dropout probability for residual connections.
30
+ s1_bits (int): Number of bits for the pre token in BSQuantizer.
31
+ s2_bits (int): Number of bits for the post token in BSQuantizer.
32
+ beta (float): Beta parameter for BSQuantizer.
33
+ gamma0 (float): Gamma0 parameter for BSQuantizer.
34
+ gamma (float): Gamma parameter for BSQuantizer.
35
+ zeta (float): Zeta parameter for BSQuantizer.
36
+ group_size (int): Group size parameter for BSQuantizer.
37
+
38
+ """
39
+
40
+ def __init__(self, d_in, d_model, n_heads, ff_dim, n_enc_layers, n_dec_layers, ffn_dropout_p, attn_dropout_p, resid_dropout_p, s1_bits, s2_bits, beta, gamma0, gamma, zeta, group_size):
41
+
42
+ super().__init__()
43
+ self.d_in = d_in
44
+ self.d_model = d_model
45
+ self.n_heads = n_heads
46
+ self.ff_dim = ff_dim
47
+ self.enc_layers = n_enc_layers
48
+ self.dec_layers = n_dec_layers
49
+ self.ffn_dropout_p = ffn_dropout_p
50
+ self.attn_dropout_p = attn_dropout_p
51
+ self.resid_dropout_p = resid_dropout_p
52
+
53
+ self.s1_bits = s1_bits
54
+ self.s2_bits = s2_bits
55
+ self.codebook_dim = s1_bits + s2_bits # Total dimension of the codebook after quantization
56
+ self.embed = nn.Linear(self.d_in, self.d_model)
57
+ self.head = nn.Linear(self.d_model, self.d_in)
58
+
59
+ # Encoder Transformer Blocks
60
+ self.encoder = nn.ModuleList([
61
+ TransformerBlock(self.d_model, self.n_heads, self.ff_dim, self.ffn_dropout_p, self.attn_dropout_p, self.resid_dropout_p)
62
+ for _ in range(self.enc_layers - 1)
63
+ ])
64
+ # Decoder Transformer Blocks
65
+ self.decoder = nn.ModuleList([
66
+ TransformerBlock(self.d_model, self.n_heads, self.ff_dim, self.ffn_dropout_p, self.attn_dropout_p, self.resid_dropout_p)
67
+ for _ in range(self.dec_layers - 1)
68
+ ])
69
+ self.quant_embed = nn.Linear(in_features=self.d_model, out_features=self.codebook_dim) # Linear layer before quantization
70
+ self.post_quant_embed_pre = nn.Linear(in_features=self.s1_bits, out_features=self.d_model) # Linear layer after quantization (pre part - s1 bits)
71
+ self.post_quant_embed = nn.Linear(in_features=self.codebook_dim, out_features=self.d_model) # Linear layer after quantization (full codebook)
72
+ self.tokenizer = BSQuantizer(self.s1_bits, self.s2_bits, beta, gamma0, gamma, zeta, group_size) # BSQuantizer module
73
+
74
+ def forward(self, x):
75
+ """
76
+ Forward pass of the KronosTokenizer.
77
+
78
+ Args:
79
+ x (torch.Tensor): Input tensor of shape (batch_size, seq_len, d_in).
80
+
81
+ Returns:
82
+ tuple: A tuple containing:
83
+ - tuple: (z_pre, z) - Reconstructed outputs from decoder with s1_bits and full codebook respectively,
84
+ both of shape (batch_size, seq_len, d_in).
85
+ - torch.Tensor: bsq_loss - Loss from the BSQuantizer.
86
+ - torch.Tensor: quantized - Quantized representation from BSQuantizer.
87
+ - torch.Tensor: z_indices - Indices from the BSQuantizer.
88
+ """
89
+ z = self.embed(x)
90
+
91
+ for layer in self.encoder:
92
+ z = layer(z)
93
+
94
+ z = self.quant_embed(z) # (B, T, codebook)
95
+
96
+ bsq_loss, quantized, z_indices = self.tokenizer(z)
97
+
98
+ quantized_pre = quantized[:, :, :self.s1_bits] # Extract the first part of quantized representation (s1_bits)
99
+ z_pre = self.post_quant_embed_pre(quantized_pre)
100
+
101
+ z = self.post_quant_embed(quantized)
102
+
103
+ # Decoder layers (for pre part - s1 bits)
104
+ for layer in self.decoder:
105
+ z_pre = layer(z_pre)
106
+ z_pre = self.head(z_pre)
107
+
108
+ # Decoder layers (for full codebook)
109
+ for layer in self.decoder:
110
+ z = layer(z)
111
+ z = self.head(z)
112
+
113
+ return (z_pre, z), bsq_loss, quantized, z_indices
114
+
115
+ def indices_to_bits(self, x, half=False):
116
+ """
117
+ Converts indices to bit representations and scales them.
118
+
119
+ Args:
120
+ x (torch.Tensor): Indices tensor.
121
+ half (bool, optional): Whether to process only half of the codebook dimension. Defaults to False.
122
+
123
+ Returns:
124
+ torch.Tensor: Bit representation tensor.
125
+ """
126
+ if half:
127
+ x1 = x[0] # Assuming x is a tuple of indices if half is True
128
+ x2 = x[1]
129
+ mask = 2 ** torch.arange(self.codebook_dim//2, device=x1.device, dtype=torch.long) # Create a mask for bit extraction
130
+ x1 = (x1.unsqueeze(-1) & mask) != 0 # Extract bits for the first half
131
+ x2 = (x2.unsqueeze(-1) & mask) != 0 # Extract bits for the second half
132
+ x = torch.cat([x1, x2], dim=-1) # Concatenate the bit representations
133
+ else:
134
+ mask = 2 ** torch.arange(self.codebook_dim, device=x.device, dtype=torch.long) # Create a mask for bit extraction
135
+ x = (x.unsqueeze(-1) & mask) != 0 # Extract bits
136
+
137
+ x = x.float() * 2 - 1 # Convert boolean to bipolar (-1, 1)
138
+ q_scale = 1. / (self.codebook_dim ** 0.5) # Scaling factor
139
+ x = x * q_scale
140
+ return x
141
+
142
+ def encode(self, x, half=False):
143
+ """
144
+ Encodes the input data into quantized indices.
145
+
146
+ Args:
147
+ x (torch.Tensor): Input tensor of shape (batch_size, seq_len, d_in).
148
+ half (bool, optional): Whether to use half quantization in BSQuantizer. Defaults to False.
149
+
150
+ Returns:
151
+ torch.Tensor: Quantized indices from BSQuantizer.
152
+ """
153
+ z = self.embed(x)
154
+ for layer in self.encoder:
155
+ z = layer(z)
156
+ z = self.quant_embed(z)
157
+
158
+ bsq_loss, quantized, z_indices = self.tokenizer(z, half=half, collect_metrics=False)
159
+ return z_indices
160
+
161
+ def decode(self, x, half=False):
162
+ """
163
+ Decodes quantized indices back to the input data space.
164
+
165
+ Args:
166
+ x (torch.Tensor): Quantized indices tensor.
167
+ half (bool, optional): Whether the indices were generated with half quantization. Defaults to False.
168
+
169
+ Returns:
170
+ torch.Tensor: Reconstructed output tensor of shape (batch_size, seq_len, d_in).
171
+ """
172
+ quantized = self.indices_to_bits(x, half)
173
+ z = self.post_quant_embed(quantized)
174
+ for layer in self.decoder:
175
+ z = layer(z)
176
+ z = self.head(z)
177
+ return z
178
+
179
+
180
+ class Kronos(nn.Module, PyTorchModelHubMixin):
181
+ """
182
+ Kronos Model.
183
+
184
+ Args:
185
+ s1_bits (int): Number of bits for pre tokens.
186
+ s2_bits (int): Number of bits for post tokens.
187
+ n_layers (int): Number of Transformer blocks.
188
+ d_model (int): Dimension of the model's embeddings and hidden states.
189
+ n_heads (int): Number of attention heads in the MultiheadAttention layers.
190
+ ff_dim (int): Dimension of the feedforward network in the Transformer blocks.
191
+ ffn_dropout_p (float): Dropout probability for the feedforward network.
192
+ attn_dropout_p (float): Dropout probability for the attention layers.
193
+ resid_dropout_p (float): Dropout probability for residual connections.
194
+ token_dropout_p (float): Dropout probability for token embeddings.
195
+ learn_te (bool): Whether to use learnable temporal embeddings.
196
+ """
197
+
198
+ def __init__(self, s1_bits, s2_bits, n_layers, d_model, n_heads, ff_dim, ffn_dropout_p, attn_dropout_p, resid_dropout_p, token_dropout_p, learn_te):
199
+ super().__init__()
200
+ self.s1_bits = s1_bits
201
+ self.s2_bits = s2_bits
202
+ self.n_layers = n_layers
203
+ self.d_model = d_model
204
+ self.n_heads = n_heads
205
+ self.learn_te = learn_te
206
+ self.ff_dim = ff_dim
207
+ self.ffn_dropout_p = ffn_dropout_p
208
+ self.attn_dropout_p = attn_dropout_p
209
+ self.resid_dropout_p = resid_dropout_p
210
+ self.token_dropout_p = token_dropout_p
211
+
212
+ self.s1_vocab_size = 2 ** self.s1_bits
213
+ self.token_drop = nn.Dropout(self.token_dropout_p)
214
+ self.embedding = HierarchicalEmbedding(self.s1_bits, self.s2_bits, self.d_model)
215
+ self.time_emb = TemporalEmbedding(self.d_model, self.learn_te)
216
+ self.transformer = nn.ModuleList([
217
+ TransformerBlock(self.d_model, self.n_heads, self.ff_dim, self.ffn_dropout_p, self.attn_dropout_p, self.resid_dropout_p)
218
+ for _ in range(self.n_layers)
219
+ ])
220
+ self.norm = RMSNorm(self.d_model)
221
+ self.dep_layer = DependencyAwareLayer(self.d_model)
222
+ self.head = DualHead(self.s1_bits, self.s2_bits, self.d_model)
223
+ self.apply(self._init_weights)
224
+
225
+ def _init_weights(self, module):
226
+
227
+ if isinstance(module, nn.Linear):
228
+ nn.init.xavier_normal_(module.weight)
229
+ if module.bias is not None:
230
+ nn.init.zeros_(module.bias)
231
+ elif isinstance(module, nn.Embedding):
232
+ nn.init.normal_(module.weight, mean=0, std=self.embedding.d_model ** -0.5)
233
+ elif isinstance(module, nn.LayerNorm):
234
+ nn.init.ones_(module.weight)
235
+ nn.init.zeros_(module.bias)
236
+ elif isinstance(module, RMSNorm):
237
+ nn.init.ones_(module.weight)
238
+
239
+ def forward(self, s1_ids, s2_ids, stamp=None, padding_mask=None, use_teacher_forcing=False, s1_targets=None):
240
+ """
241
+ Args:
242
+ s1_ids (torch.Tensor): Input tensor of s1 token IDs. Shape: [batch_size, seq_len]
243
+ s2_ids (torch.Tensor): Input tensor of s2 token IDs. Shape: [batch_size, seq_len]
244
+ stamp (torch.Tensor, optional): Temporal stamp tensor. Shape: [batch_size, seq_len]. Defaults to None.
245
+ padding_mask (torch.Tensor, optional): Mask for padding tokens. Shape: [batch_size, seq_len]. Defaults to None.
246
+ use_teacher_forcing (bool, optional): Whether to use teacher forcing for s1 decoding. Defaults to False.
247
+ s1_targets (torch.Tensor, optional): Target s1 token IDs for teacher forcing. Shape: [batch_size, seq_len]. Defaults to None.
248
+
249
+ Returns:
250
+ Tuple[torch.Tensor, torch.Tensor]:
251
+ - s1 logits: Logits for s1 token predictions. Shape: [batch_size, seq_len, s1_vocab_size]
252
+ - s2_logits: Logits for s2 token predictions, conditioned on s1. Shape: [batch_size, seq_len, s2_vocab_size]
253
+ """
254
+ x = self.embedding([s1_ids, s2_ids])
255
+ if stamp is not None:
256
+ time_embedding = self.time_emb(stamp)
257
+ x = x + time_embedding
258
+ x = self.token_drop(x)
259
+
260
+ for layer in self.transformer:
261
+ x = layer(x, key_padding_mask=padding_mask)
262
+
263
+ x = self.norm(x)
264
+
265
+ s1_logits = self.head(x)
266
+
267
+ if use_teacher_forcing:
268
+ sibling_embed = self.embedding.emb_s1(s1_targets)
269
+ else:
270
+ s1_probs = F.softmax(s1_logits.detach(), dim=-1)
271
+ sample_s1_ids = torch.multinomial(s1_probs.view(-1, self.s1_vocab_size), 1).view(s1_ids.shape)
272
+ sibling_embed = self.embedding.emb_s1(sample_s1_ids)
273
+
274
+ x2 = self.dep_layer(x, sibling_embed, key_padding_mask=padding_mask) # Dependency Aware Layer: Condition on s1 embeddings
275
+ s2_logits = self.head.cond_forward(x2)
276
+ return s1_logits, s2_logits
277
+
278
+ def decode_s1(self, s1_ids, s2_ids, stamp=None, padding_mask=None):
279
+ """
280
+ Decodes only the s1 tokens.
281
+
282
+ This method performs a forward pass to predict only s1 tokens. It returns the s1 logits
283
+ and the context representation from the Transformer, which can be used for subsequent s2 decoding.
284
+
285
+ Args:
286
+ s1_ids (torch.Tensor): Input tensor of s1 token IDs. Shape: [batch_size, seq_len]
287
+ s2_ids (torch.Tensor): Input tensor of s2 token IDs. Shape: [batch_size, seq_len]
288
+ stamp (torch.Tensor, optional): Temporal stamp tensor. Shape: [batch_size, seq_len]. Defaults to None.
289
+ padding_mask (torch.Tensor, optional): Mask for padding tokens. Shape: [batch_size, seq_len]. Defaults to None.
290
+
291
+ Returns:
292
+ Tuple[torch.Tensor, torch.Tensor]:
293
+ - s1 logits: Logits for s1 token predictions. Shape: [batch_size, seq_len, s1_vocab_size]
294
+ - context: Context representation from the Transformer. Shape: [batch_size, seq_len, d_model]
295
+ """
296
+ x = self.embedding([s1_ids, s2_ids])
297
+ if stamp is not None:
298
+ time_embedding = self.time_emb(stamp)
299
+ x = x + time_embedding
300
+ x = self.token_drop(x)
301
+
302
+ for layer in self.transformer:
303
+ x = layer(x, key_padding_mask=padding_mask)
304
+
305
+ x = self.norm(x)
306
+
307
+ s1_logits = self.head(x)
308
+ return s1_logits, x
309
+
310
+ def decode_s2(self, context, s1_ids, padding_mask=None):
311
+ """
312
+ Decodes the s2 tokens, conditioned on the context and s1 tokens.
313
+
314
+ This method decodes s2 tokens based on a pre-computed context representation (typically from `decode_s1`)
315
+ and the s1 token IDs. It uses the dependency-aware layer and the conditional s2 head to predict s2 tokens.
316
+
317
+ Args:
318
+ context (torch.Tensor): Context representation from the transformer (output of decode_s1).
319
+ Shape: [batch_size, seq_len, d_model]
320
+ s1_ids (torch.Tensor): Input tensor of s1 token IDs. Shape: [batch_size, seq_len]
321
+ padding_mask (torch.Tensor, optional): Mask for padding tokens. Shape: [batch_size, seq_len]. Defaults to None.
322
+
323
+ Returns:
324
+ torch.Tensor: s2 logits. Shape: [batch_size, seq_len, s2_vocab_size]
325
+ """
326
+ sibling_embed = self.embedding.emb_s1(s1_ids)
327
+ x2 = self.dep_layer(context, sibling_embed, key_padding_mask=padding_mask)
328
+ return self.head.cond_forward(x2)
329
+
330
+
331
+ def top_k_top_p_filtering(
332
+ logits,
333
+ top_k: int = 0,
334
+ top_p: float = 1.0,
335
+ filter_value: float = -float("Inf"),
336
+ min_tokens_to_keep: int = 1,
337
+ ):
338
+ """Filter a distribution of logits using top-k and/or nucleus (top-p) filtering
339
+ Args:
340
+ logits: logits distribution shape (batch size, vocabulary size)
341
+ if top_k > 0: keep only top k tokens with highest probability (top-k filtering).
342
+ if top_p < 1.0: keep the top tokens with cumulative probability >= top_p (nucleus filtering).
343
+ Nucleus filtering is described in Holtzman et al. (http://arxiv.org/abs/1904.09751)
344
+ Make sure we keep at least min_tokens_to_keep per batch example in the output
345
+ From: https://gist.github.com/thomwolf/1a5a29f6962089e871b94cbd09daf317
346
+ """
347
+ if top_k > 0:
348
+ top_k = min(max(top_k, min_tokens_to_keep), logits.size(-1)) # Safety check
349
+ # Remove all tokens with a probability less than the last token of the top-k
350
+ indices_to_remove = logits < torch.topk(logits, top_k)[0][..., -1, None]
351
+ logits[indices_to_remove] = filter_value
352
+ return logits
353
+
354
+ if top_p < 1.0:
355
+ sorted_logits, sorted_indices = torch.sort(logits, descending=True)
356
+ cumulative_probs = torch.cumsum(F.softmax(sorted_logits, dim=-1), dim=-1)
357
+
358
+ # Remove tokens with cumulative probability above the threshold (token with 0 are kept)
359
+ sorted_indices_to_remove = cumulative_probs > top_p
360
+ if min_tokens_to_keep > 1:
361
+ # Keep at least min_tokens_to_keep (set to min_tokens_to_keep-1 because we add the first one below)
362
+ sorted_indices_to_remove[..., :min_tokens_to_keep] = 0
363
+ # Shift the indices to the right to keep also the first token above the threshold
364
+ sorted_indices_to_remove[..., 1:] = sorted_indices_to_remove[..., :-1].clone()
365
+ sorted_indices_to_remove[..., 0] = 0
366
+
367
+ # scatter sorted tensors to original indexing
368
+ indices_to_remove = sorted_indices_to_remove.scatter(1, sorted_indices, sorted_indices_to_remove)
369
+ logits[indices_to_remove] = filter_value
370
+ return logits
371
+
372
+
373
+ def sample_from_logits(logits, temperature=1.0, top_k=None, top_p=None, sample_logits=True):
374
+ logits = logits / temperature
375
+ if top_k is not None or top_p is not None:
376
+ if top_k > 0 or top_p < 1.0:
377
+ logits = top_k_top_p_filtering(logits, top_k=top_k, top_p=top_p)
378
+
379
+ probs = F.softmax(logits, dim=-1)
380
+
381
+ if not sample_logits:
382
+ _, x = torch.topk(probs, k=1, dim=-1)
383
+ else:
384
+ x = torch.multinomial(probs, num_samples=1)
385
+
386
+ return x
387
+
388
+
389
+ def auto_regressive_inference(tokenizer, model, x, x_stamp, y_stamp, max_context, pred_len, clip=5, T=1.0, top_k=0, top_p=0.99, sample_count=5, verbose=False, return_samples=False):
390
+ with torch.no_grad():
391
+ x = torch.clip(x, -clip, clip)
392
+
393
+ device = x.device
394
+ x = x.unsqueeze(1).repeat(1, sample_count, 1, 1).reshape(-1, x.size(1), x.size(2)).to(device)
395
+ x_stamp = x_stamp.unsqueeze(1).repeat(1, sample_count, 1, 1).reshape(-1, x_stamp.size(1), x_stamp.size(2)).to(device)
396
+ y_stamp = y_stamp.unsqueeze(1).repeat(1, sample_count, 1, 1).reshape(-1, y_stamp.size(1), y_stamp.size(2)).to(device)
397
+
398
+ x_token = tokenizer.encode(x, half=True)
399
+
400
+ initial_seq_len = x.size(1)
401
+ batch_size = x_token[0].size(0)
402
+ total_seq_len = initial_seq_len + pred_len
403
+ full_stamp = torch.cat([x_stamp, y_stamp], dim=1)
404
+
405
+ generated_pre = x_token[0].new_empty(batch_size, pred_len)
406
+ generated_post = x_token[1].new_empty(batch_size, pred_len)
407
+
408
+ pre_buffer = x_token[0].new_zeros(batch_size, max_context)
409
+ post_buffer = x_token[1].new_zeros(batch_size, max_context)
410
+ buffer_len = min(initial_seq_len, max_context)
411
+ if buffer_len > 0:
412
+ start_idx = max(0, initial_seq_len - max_context)
413
+ pre_buffer[:, :buffer_len] = x_token[0][:, start_idx:start_idx + buffer_len]
414
+ post_buffer[:, :buffer_len] = x_token[1][:, start_idx:start_idx + buffer_len]
415
+
416
+ if verbose:
417
+ ran = trange
418
+ else:
419
+ ran = range
420
+ for i in ran(pred_len):
421
+ current_seq_len = initial_seq_len + i
422
+ window_len = min(current_seq_len, max_context)
423
+
424
+ if current_seq_len <= max_context:
425
+ input_tokens = [
426
+ pre_buffer[:, :window_len],
427
+ post_buffer[:, :window_len]
428
+ ]
429
+ else:
430
+ input_tokens = [pre_buffer, post_buffer]
431
+
432
+ context_end = current_seq_len
433
+ context_start = max(0, context_end - max_context)
434
+ current_stamp = full_stamp[:, context_start:context_end, :].contiguous()
435
+
436
+ s1_logits, context = model.decode_s1(input_tokens[0], input_tokens[1], current_stamp)
437
+ s1_logits = s1_logits[:, -1, :]
438
+ sample_pre = sample_from_logits(s1_logits, temperature=T, top_k=top_k, top_p=top_p, sample_logits=True)
439
+
440
+ s2_logits = model.decode_s2(context, sample_pre)
441
+ s2_logits = s2_logits[:, -1, :]
442
+ sample_post = sample_from_logits(s2_logits, temperature=T, top_k=top_k, top_p=top_p, sample_logits=True)
443
+
444
+ generated_pre[:, i] = sample_pre.squeeze(-1)
445
+ generated_post[:, i] = sample_post.squeeze(-1)
446
+
447
+ if current_seq_len < max_context:
448
+ pre_buffer[:, current_seq_len] = sample_pre.squeeze(-1)
449
+ post_buffer[:, current_seq_len] = sample_post.squeeze(-1)
450
+ else:
451
+ pre_buffer.copy_(torch.roll(pre_buffer, shifts=-1, dims=1))
452
+ post_buffer.copy_(torch.roll(post_buffer, shifts=-1, dims=1))
453
+ pre_buffer[:, -1] = sample_pre.squeeze(-1)
454
+ post_buffer[:, -1] = sample_post.squeeze(-1)
455
+
456
+ full_pre = torch.cat([x_token[0], generated_pre], dim=1)
457
+ full_post = torch.cat([x_token[1], generated_post], dim=1)
458
+
459
+ context_start = max(0, total_seq_len - max_context)
460
+ input_tokens = [
461
+ full_pre[:, context_start:total_seq_len].contiguous(),
462
+ full_post[:, context_start:total_seq_len].contiguous()
463
+ ]
464
+ z = tokenizer.decode(input_tokens, half=True)
465
+ z = z.reshape(-1, sample_count, z.size(1), z.size(2))
466
+ preds = z.cpu().numpy()
467
+
468
+ if not return_samples:
469
+ preds = np.mean(preds, axis=1)
470
+
471
+ return preds
472
+
473
+
474
+ def calc_time_stamps(x_timestamp):
475
+ time_df = pd.DataFrame()
476
+ time_df['minute'] = x_timestamp.dt.minute
477
+ time_df['hour'] = x_timestamp.dt.hour
478
+ time_df['weekday'] = x_timestamp.dt.weekday
479
+ time_df['day'] = x_timestamp.dt.day
480
+ time_df['month'] = x_timestamp.dt.month
481
+ return time_df
482
+
483
+
484
+ class KronosPredictor:
485
+
486
+ def __init__(self, model, tokenizer, device=None, max_context=512, clip=5):
487
+ self.tokenizer = tokenizer
488
+ self.model = model
489
+ self.max_context = max_context
490
+ self.clip = clip
491
+ self.price_cols = ['open', 'high', 'low', 'close']
492
+ self.vol_col = 'volume'
493
+ self.amt_vol = 'amount'
494
+ self.time_cols = ['minute', 'hour', 'weekday', 'day', 'month']
495
+
496
+ # Auto-detect device if not specified
497
+ if device is None:
498
+ if torch.cuda.is_available():
499
+ device = "cuda:0"
500
+ elif hasattr(torch.backends, 'mps') and torch.backends.mps.is_available():
501
+ device = "mps"
502
+ else:
503
+ device = "cpu"
504
+
505
+ self.device = device
506
+
507
+ self.tokenizer = self.tokenizer.to(self.device)
508
+ self.model = self.model.to(self.device)
509
+
510
+ def generate(self, x, x_stamp, y_stamp, pred_len, T, top_k, top_p, sample_count, verbose, return_samples=False):
511
+
512
+ x_tensor = torch.from_numpy(np.array(x).astype(np.float32)).to(self.device)
513
+ x_stamp_tensor = torch.from_numpy(np.array(x_stamp).astype(np.float32)).to(self.device)
514
+ y_stamp_tensor = torch.from_numpy(np.array(y_stamp).astype(np.float32)).to(self.device)
515
+
516
+ preds = auto_regressive_inference(self.tokenizer, self.model, x_tensor, x_stamp_tensor, y_stamp_tensor, self.max_context, pred_len,
517
+ self.clip, T, top_k, top_p, sample_count, verbose, return_samples=return_samples)
518
+ preds = preds[:, -pred_len:, :]
519
+ return preds
520
+
521
+ def predict(self, df, x_timestamp, y_timestamp, pred_len, T=1.0, top_k=0, top_p=0.9, sample_count=1, verbose=True):
522
+
523
+ if not isinstance(df, pd.DataFrame):
524
+ raise ValueError("Input must be a pandas DataFrame.")
525
+
526
+ if not all(col in df.columns for col in self.price_cols):
527
+ raise ValueError(f"Price columns {self.price_cols} not found in DataFrame.")
528
+
529
+ df = df.copy()
530
+ if self.vol_col not in df.columns:
531
+ df[self.vol_col] = 0.0 # Fill missing volume with zeros
532
+ df[self.amt_vol] = 0.0 # Fill missing amount with zeros
533
+ if self.amt_vol not in df.columns and self.vol_col in df.columns:
534
+ df[self.amt_vol] = df[self.vol_col] * df[self.price_cols].mean(axis=1)
535
+
536
+ if df[self.price_cols + [self.vol_col, self.amt_vol]].isnull().values.any():
537
+ raise ValueError("Input DataFrame contains NaN values in price or volume columns.")
538
+
539
+ x_time_df = calc_time_stamps(x_timestamp)
540
+ y_time_df = calc_time_stamps(y_timestamp)
541
+
542
+ x = df[self.price_cols + [self.vol_col, self.amt_vol]].values.astype(np.float32)
543
+ x_stamp = x_time_df.values.astype(np.float32)
544
+ y_stamp = y_time_df.values.astype(np.float32)
545
+
546
+ x_mean, x_std = np.mean(x, axis=0), np.std(x, axis=0)
547
+
548
+ x = (x - x_mean) / (x_std + 1e-5)
549
+ x = np.clip(x, -self.clip, self.clip)
550
+
551
+ x = x[np.newaxis, :]
552
+ x_stamp = x_stamp[np.newaxis, :]
553
+ y_stamp = y_stamp[np.newaxis, :]
554
+
555
+ preds = self.generate(x, x_stamp, y_stamp, pred_len, T, top_k, top_p, sample_count, verbose)
556
+
557
+ preds = preds.squeeze(0)
558
+ preds = preds * (x_std + 1e-5) + x_mean
559
+
560
+ pred_df = pd.DataFrame(preds, columns=self.price_cols + [self.vol_col, self.amt_vol], index=y_timestamp)
561
+ return pred_df
562
+
563
+
564
+ def predict_batch(self, df_list, x_timestamp_list, y_timestamp_list, pred_len, T=1.0, top_k=0, top_p=0.9, sample_count=1, verbose=True):
565
+ """
566
+ Perform parallel (batch) prediction on multiple time series. All series must have the same historical length and prediction length (pred_len).
567
+
568
+ Args:
569
+ df_list (List[pd.DataFrame]): List of input DataFrames, each containing price columns and optional volume/amount columns.
570
+ x_timestamp_list (List[pd.DatetimeIndex or Series]): List of timestamps corresponding to historical data, length should match the number of rows in each DataFrame.
571
+ y_timestamp_list (List[pd.DatetimeIndex or Series]): List of future prediction timestamps, length should equal pred_len.
572
+ pred_len (int): Number of prediction steps.
573
+ T (float): Sampling temperature.
574
+ top_k (int): Top-k filtering threshold.
575
+ top_p (float): Top-p (nucleus sampling) threshold.
576
+ sample_count (int): Number of parallel samples per series, automatically averaged internally.
577
+ verbose (bool): Whether to display autoregressive progress.
578
+
579
+ Returns:
580
+ List[pd.DataFrame]: List of prediction results in the same order as input, each DataFrame contains
581
+ `open, high, low, close, volume, amount` columns, indexed by corresponding `y_timestamp`.
582
+ """
583
+ # Basic validation
584
+ if not isinstance(df_list, (list, tuple)) or not isinstance(x_timestamp_list, (list, tuple)) or not isinstance(y_timestamp_list, (list, tuple)):
585
+ raise ValueError("df_list, x_timestamp_list, y_timestamp_list must be list or tuple types.")
586
+ if not (len(df_list) == len(x_timestamp_list) == len(y_timestamp_list)):
587
+ raise ValueError("df_list, x_timestamp_list, y_timestamp_list must have consistent lengths.")
588
+
589
+ num_series = len(df_list)
590
+
591
+ x_list = []
592
+ x_stamp_list = []
593
+ y_stamp_list = []
594
+ means = []
595
+ stds = []
596
+ seq_lens = []
597
+ y_lens = []
598
+
599
+ for i in range(num_series):
600
+ df = df_list[i]
601
+ if not isinstance(df, pd.DataFrame):
602
+ raise ValueError(f"Input at index {i} is not a pandas DataFrame.")
603
+ if not all(col in df.columns for col in self.price_cols):
604
+ raise ValueError(f"DataFrame at index {i} is missing price columns {self.price_cols}.")
605
+
606
+ df = df.copy()
607
+ if self.vol_col not in df.columns:
608
+ df[self.vol_col] = 0.0
609
+ df[self.amt_vol] = 0.0
610
+ if self.amt_vol not in df.columns and self.vol_col in df.columns:
611
+ df[self.amt_vol] = df[self.vol_col] * df[self.price_cols].mean(axis=1)
612
+
613
+ if df[self.price_cols + [self.vol_col, self.amt_vol]].isnull().values.any():
614
+ raise ValueError(f"DataFrame at index {i} contains NaN values in price or volume columns.")
615
+
616
+ x_timestamp = x_timestamp_list[i]
617
+ y_timestamp = y_timestamp_list[i]
618
+
619
+ x_time_df = calc_time_stamps(x_timestamp)
620
+ y_time_df = calc_time_stamps(y_timestamp)
621
+
622
+ x = df[self.price_cols + [self.vol_col, self.amt_vol]].values.astype(np.float32)
623
+ x_stamp = x_time_df.values.astype(np.float32)
624
+ y_stamp = y_time_df.values.astype(np.float32)
625
+
626
+ if x.shape[0] != x_stamp.shape[0]:
627
+ raise ValueError(f"Inconsistent lengths at index {i}: x has {x.shape[0]} vs x_stamp has {x_stamp.shape[0]}.")
628
+ if y_stamp.shape[0] != pred_len:
629
+ raise ValueError(f"y_timestamp length at index {i} should equal pred_len={pred_len}, got {y_stamp.shape[0]}.")
630
+
631
+ x_mean, x_std = np.mean(x, axis=0), np.std(x, axis=0)
632
+ x_norm = (x - x_mean) / (x_std + 1e-5)
633
+ x_norm = np.clip(x_norm, -self.clip, self.clip)
634
+
635
+ x_list.append(x_norm)
636
+ x_stamp_list.append(x_stamp)
637
+ y_stamp_list.append(y_stamp)
638
+ means.append(x_mean)
639
+ stds.append(x_std)
640
+
641
+ seq_lens.append(x_norm.shape[0])
642
+ y_lens.append(y_stamp.shape[0])
643
+
644
+ # Require all series to have consistent historical and prediction lengths for batch processing
645
+ if len(set(seq_lens)) != 1:
646
+ raise ValueError(f"Parallel prediction requires all series to have consistent historical lengths, got: {seq_lens}")
647
+ if len(set(y_lens)) != 1:
648
+ raise ValueError(f"Parallel prediction requires all series to have consistent prediction lengths, got: {y_lens}")
649
+
650
+ x_batch = np.stack(x_list, axis=0).astype(np.float32) # (B, seq_len, feat)
651
+ x_stamp_batch = np.stack(x_stamp_list, axis=0).astype(np.float32) # (B, seq_len, time_feat)
652
+ y_stamp_batch = np.stack(y_stamp_list, axis=0).astype(np.float32) # (B, pred_len, time_feat)
653
+
654
+ preds = self.generate(x_batch, x_stamp_batch, y_stamp_batch, pred_len, T, top_k, top_p, sample_count, verbose)
655
+ # preds: (B, pred_len, feat)
656
+
657
+ pred_dfs = []
658
+ for i in range(num_series):
659
+ preds_i = preds[i] * (stds[i] + 1e-5) + means[i]
660
+ pred_df = pd.DataFrame(preds_i, columns=self.price_cols + [self.vol_col, self.amt_vol], index=y_timestamp_list[i])
661
+ pred_dfs.append(pred_df)
662
+
663
+ return pred_dfs
664
+
Kronos-master/model/module.py ADDED
@@ -0,0 +1,570 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import math
2
+
3
+ from einops import rearrange, reduce
4
+ import torch
5
+ import torch.nn as nn
6
+ from torch.autograd import Function
7
+ import torch.nn.functional as F
8
+
9
+
10
+ class DifferentiableEntropyFunction(Function):
11
+ @staticmethod
12
+ def forward(ctx, zq, basis, K, eps):
13
+ zb = (zq + 1) / 2
14
+ zi = ((zb * basis).sum(-1)).to(torch.int64)
15
+ cnt = torch.scatter_reduce(torch.zeros(2 ** K, device=zq.device, dtype=zq.dtype),
16
+ 0,
17
+ zi.flatten(),
18
+ torch.ones_like(zi.flatten()).to(zq.dtype),
19
+ 'sum')
20
+ prob = (cnt + eps) / (cnt + eps).sum()
21
+ H = -(prob * torch.log(prob)).sum()
22
+ ctx.save_for_backward(zq, zi, prob)
23
+ ctx.K = K
24
+ return H
25
+
26
+ @staticmethod
27
+ def backward(ctx, grad_output):
28
+ zq, zi, prob = ctx.saved_tensors
29
+ grad_array = -grad_output * (torch.log(prob) + 1) / zi.numel() / ctx.K
30
+ reord_grad = grad_array[zi.flatten()].reshape(zi.shape)
31
+ grad_input = reord_grad.unsqueeze(-1) * zq
32
+ return grad_input, None, None, None, None
33
+
34
+
35
+ def codebook_entropy(zq, basis, K, eps=1e-4):
36
+ return DifferentiableEntropyFunction.apply(zq, basis, K, eps)
37
+
38
+
39
+ class BinarySphericalQuantizer(nn.Module):
40
+ def __init__(self, embed_dim, beta, gamma0, gamma, zeta,
41
+ input_format='bchw',
42
+ soft_entropy=True, group_size=9,
43
+ persample_entropy_compute='analytical',
44
+ cb_entropy_compute='group',
45
+ l2_norm=True,
46
+ inv_temperature=1):
47
+ """
48
+ Paper link: https://arxiv.org/pdf/2406.07548.pdf
49
+ Here we use the official implementation of the BinarySphericalQuantizer.
50
+ """
51
+ super().__init__()
52
+ self.embed_dim = embed_dim
53
+ self.beta = beta # loss weight for commit loss
54
+ self.gamma0 = gamma0 # loss weight for entropy penalty
55
+ self.gamma = gamma # loss weight for entropy penalty
56
+ self.zeta = zeta # loss weight for entire entropy penalty
57
+ self.input_format = input_format
58
+ assert self.embed_dim % group_size == 0, "embed_dim must be divisible by group_size"
59
+ self.num_groups = self.embed_dim // group_size
60
+ self.group_size = group_size
61
+ assert persample_entropy_compute in ['group', 'analytical'], "persample_entropy_compute must be either 'group' or 'analytical'"
62
+ assert cb_entropy_compute in ['group', 'nce'], "cb_entropy_compute must be either 'group' or 'nce'"
63
+ self.persample_entropy_compute = persample_entropy_compute
64
+ self.cb_entropy_compute = cb_entropy_compute
65
+ self.l2_norm = l2_norm
66
+ self.inv_temperature = inv_temperature
67
+
68
+ self.register_buffer('basis', 2 ** torch.arange(embed_dim - 1, -1, -1))
69
+ self.register_buffer('group_basis', 2 ** torch.arange(group_size - 1, -1, -1))
70
+
71
+ self.num_dimensions = 2 ** embed_dim
72
+ self.bits_per_index = embed_dim
73
+
74
+ # we only need to keep the codebook portion up to the group size
75
+ # because we approximate the H loss with this subcode
76
+ group_codes = torch.arange(2 ** self.group_size)
77
+ group_codebook = self.indexes_to_codes(group_codes).float()[:, -group_size:]
78
+ self.register_buffer('group_codebook', group_codebook, persistent=False)
79
+
80
+ self.soft_entropy = soft_entropy # soft_entropy: Sec 3.2 of https://arxiv.org/pdf/1911.05894.pdf
81
+
82
+ def quantize(self, z):
83
+ assert z.shape[-1] == self.embed_dim, f"Expected {self.embed_dim} dimensions, got {z.shape[-1]}"
84
+
85
+ zhat = torch.where(z > 0,
86
+ torch.tensor(1, dtype=z.dtype, device=z.device),
87
+ torch.tensor(-1, dtype=z.dtype, device=z.device))
88
+ return z + (zhat - z).detach()
89
+
90
+ def forward(self, z, collect_metrics=True):
91
+ # if self.input_format == 'bchw':
92
+ # z = rearrange(z, 'b c h w -> b h w c')
93
+ zq = self.quantize(z)
94
+
95
+ q_scale = 1. / (self.embed_dim ** 0.5) if self.l2_norm else 1.
96
+
97
+ zq = zq * q_scale
98
+
99
+ if not collect_metrics:
100
+ return zq, zq.new_zeros(()), {}
101
+
102
+ indices = self.codes_to_indexes(zq.detach())
103
+ group_indices = self.codes_to_group_indexes(zq.detach())
104
+ if not self.training:
105
+ used_codes = torch.unique(indices, return_counts=False)
106
+ else:
107
+ used_codes = None
108
+
109
+ if self.soft_entropy:
110
+ persample_entropy, cb_entropy, avg_prob = self.soft_entropy_loss(z)
111
+ entropy_penalty = self.gamma0 * persample_entropy - self.gamma * cb_entropy
112
+ else:
113
+ zb_by_sample = ((zq + 1) / 2).reshape(z.shape[0], -1, z.shape[-1]).to(torch.float32)
114
+ persample_entropy = self.get_hard_per_sample_entropy(zb_by_sample)
115
+ cb_entropy = codebook_entropy(zq, self.basis, self.embed_dim)
116
+ entropy_penalty = self.gamma0 * persample_entropy - self.gamma * cb_entropy
117
+
118
+ # commit loss
119
+ commit_loss = self.beta * torch.mean(((zq.detach() - z) ** 2).sum(dim=-1))
120
+
121
+ # if self.input_format == 'bchw':
122
+ # zq = rearrange(zq, 'b h w c -> b c h w')
123
+
124
+ return (
125
+ zq,
126
+ commit_loss + self.zeta * entropy_penalty / self.inv_temperature,
127
+ {"H": cb_entropy, "used_codes": used_codes, "indices": indices, "group_indices": group_indices,
128
+ "avg_prob": avg_prob}
129
+ )
130
+
131
+ def soft_entropy_loss(self, z):
132
+ # if we divide the code in subgroups of size group_size, the codebook will be of size 2 ** group_size
133
+ # the sub-code is the last group_size bits of the full code
134
+ group_code_book = self.group_codebook / (self.embed_dim ** 0.5 if self.l2_norm else 1)
135
+ divided_z = rearrange(z, '... (g c) -> ... g c', c=self.group_size)
136
+
137
+ # we calculate the distance between the divided_z and the codebook for each subgroup
138
+ distance = - 2 * torch.einsum('... g c, d c ->... g d', divided_z, group_code_book)
139
+ prob = (-distance * self.inv_temperature).softmax(dim=-1)
140
+ if self.persample_entropy_compute == 'analytical':
141
+ if self.l2_norm:
142
+ p = torch.sigmoid(-4 * z / (self.embed_dim ** 0.5) * self.inv_temperature)
143
+ else:
144
+ p = torch.sigmoid(-4 * z * self.inv_temperature)
145
+ prob = torch.stack([p, 1 - p], dim=-1)
146
+ per_sample_entropy = self.get_entropy(prob, dim=-1, normalize=False).sum(dim=-1).mean()
147
+ else:
148
+ per_sample_entropy = self.get_entropy(prob, dim=-1, normalize=False).sum(dim=-1).mean()
149
+
150
+ # macro average of the probability of each subgroup
151
+ avg_prob = reduce(prob, '... g d ->g d', 'mean')
152
+ codebook_entropy = self.get_entropy(avg_prob, dim=-1, normalize=False)
153
+
154
+ # the approximation of the entropy is the sum of the entropy of each subgroup
155
+ return per_sample_entropy, codebook_entropy.sum(), avg_prob
156
+
157
+ def get_hard_per_sample_entropy(self, zb_by_sample):
158
+ probs_per_dim = zb_by_sample.sum(1) / zb_by_sample.shape[1]
159
+ persample_entropy = - probs_per_dim * torch.log(probs_per_dim + 1e-8) - (1 - probs_per_dim) * torch.log(1 - probs_per_dim + 1e-8)
160
+ persample_entropy = persample_entropy.sum(-1)
161
+ return persample_entropy.mean()
162
+
163
+ def codes_to_indexes(self, zhat):
164
+ """Converts a `code` to an index in the codebook.
165
+ Args:
166
+ zhat: A tensor of shape (B, ..., C) containing the codes. must be in {-1, 1}
167
+ """
168
+ assert zhat.shape[-1] == self.embed_dim, f"Expected {self.embed_dim} dimensions, got {zhat.shape[-1]}"
169
+ return ((zhat + 1) / 2 * self.basis).sum(axis=-1).to(torch.int64)
170
+
171
+ def codes_to_group_indexes(self, zhat):
172
+ """Converts a `code` to a list of indexes (in groups) in the codebook.
173
+ Args:
174
+ zhat: A tensor of shape (B, ..., C) containing the codes. must be in {-1, 1}
175
+ """
176
+ zhat_in_group = rearrange(zhat, 'b ... (g c) -> b ... g c', c=self.group_size)
177
+ return ((zhat_in_group + 1) / 2 * self.group_basis).sum(axis=-1).to(torch.int64)
178
+
179
+ def indexes_to_codes(self, indices):
180
+ """Inverse of `indexes_to_codes`."""
181
+ indices = indices.unsqueeze(-1)
182
+ codes_non_centered = torch.remainder(
183
+ torch.floor_divide(indices, self.basis), 2
184
+ )
185
+ return codes_non_centered * 2 - 1
186
+
187
+ def group_indexes_to_codes(self, group_indices):
188
+ """Inverse of `group_indexes_to_codes`."""
189
+ group_indices = group_indices.unsqueeze(-1)
190
+ codes_non_centered = torch.remainder(
191
+ torch.floor_divide(group_indices, self.group_basis), 2
192
+ )
193
+ codes_non_centered = rearrange(codes_non_centered, 'b ... g c -> b ... (g c)')
194
+ return codes_non_centered * 2 - 1
195
+
196
+ def get_entropy(self, count, dim=-1, eps=1e-4, normalize=True):
197
+ if normalize:
198
+ probs = (count + eps) / (count + eps).sum(dim=dim, keepdim=True)
199
+ else:
200
+ probs = count
201
+ H = -(probs * torch.log(probs + 1e-8)).sum(dim=dim)
202
+ return H
203
+
204
+ def get_group_codebook_entry(self, group_indices):
205
+ z_q = self.group_indexes_to_codes(group_indices)
206
+ q_scale = 1. / (self.embed_dim ** 0.5) if self.l2_norm else 1.
207
+ z_q = z_q * q_scale
208
+ if self.input_format == 'bchw':
209
+ h, w = int(z_q.shape[1] ** 0.5)
210
+ assert h * w == z_q.shape[1], 'Invalid sequence length'
211
+ z_q = rearrange(z_q, 'b (h w) c -> b c h w', h=h)
212
+ return z_q
213
+
214
+ def get_codebook_entry(self, indices):
215
+ z_q = self.indexes_to_codes(indices)
216
+ q_scale = 1. / (self.embed_dim ** 0.5) if self.l2_norm else 1.
217
+ z_q = z_q * q_scale
218
+ if self.input_format == 'bchw':
219
+ h, w = int(z_q.shape[1] ** 0.5)
220
+ assert h * w == z_q.shape[1], 'Invalid sequence length'
221
+ z_q = rearrange(z_q, 'b (h w) c -> b c h w', h=h)
222
+ return z_q
223
+
224
+
225
+ class BSQuantizer(nn.Module):
226
+
227
+ def __init__(self, s1_bits, s2_bits, beta, gamma0, gamma, zeta, group_size):
228
+ super().__init__()
229
+ self.codebook_dim = s1_bits + s2_bits
230
+ self.s1_bits = s1_bits
231
+ self.s2_bits = s2_bits
232
+ self.bsq = BinarySphericalQuantizer(self.codebook_dim, beta, gamma0, gamma, zeta, group_size=group_size)
233
+
234
+ def bits_to_indices(self, bits):
235
+ bits = (bits >= 0).to(torch.long)
236
+ indices = 2 ** torch.arange(
237
+ 0,
238
+ bits.shape[-1],
239
+ 1,
240
+ dtype=torch.long,
241
+ device=bits.device,
242
+ )
243
+ return (bits * indices).sum(-1)
244
+
245
+ def forward(self, z, half=False, collect_metrics=True):
246
+ z = F.normalize(z, dim=-1)
247
+ quantized, bsq_loss, metrics = self.bsq(z, collect_metrics=collect_metrics)
248
+ if half:
249
+ q_pre = quantized[:, :, :self.s1_bits]
250
+ q_post = quantized[:, :, self.s1_bits:]
251
+ z_indices = [self.bits_to_indices(q_pre), self.bits_to_indices(q_post)]
252
+ else:
253
+ z_indices = self.bits_to_indices(quantized)
254
+ return bsq_loss, quantized, z_indices
255
+
256
+
257
+ class RMSNorm(torch.nn.Module):
258
+ def __init__(self, dim: int, eps: float = 1e-5):
259
+ super().__init__()
260
+ self.eps = eps
261
+ self.weight = nn.Parameter(torch.ones(dim))
262
+
263
+ def _norm(self, x):
264
+ return x * torch.rsqrt(torch.mean(x * x, dim=-1, keepdim=True) + self.eps)
265
+
266
+ def forward(self, x):
267
+ output = self._norm(x.float()).type_as(x)
268
+ return output * self.weight
269
+
270
+
271
+ class FeedForward(nn.Module):
272
+ def __init__(self, d_model, ff_dim, ffn_dropout_p=0.0):
273
+ super().__init__()
274
+
275
+ self.w1 = nn.Linear(d_model, ff_dim, bias=False)
276
+ self.w3 = nn.Linear(d_model, ff_dim, bias=False)
277
+ self.w2 = nn.Linear(ff_dim, d_model, bias=False)
278
+ self.ffn_dropout = nn.Dropout(ffn_dropout_p)
279
+
280
+ def forward(self, x):
281
+ return self.ffn_dropout(self.w2(F.silu(self.w1(x)) * self.w3(x)))
282
+
283
+
284
+ class RotaryPositionalEmbedding(nn.Module):
285
+ def __init__(self, dim):
286
+ super().__init__()
287
+ inv_freq = 1.0 / (10000 ** (torch.arange(0, dim, 2).float() / dim))
288
+ self.register_buffer("inv_freq", inv_freq)
289
+ self.seq_len_cached = None
290
+ self.cos_cached = None
291
+ self.sin_cached = None
292
+
293
+ def _update_cos_sin_cache(self, x, seq_len):
294
+ if seq_len != self.seq_len_cached:
295
+ self.seq_len_cached = seq_len
296
+ t = torch.arange(seq_len, device=x.device).type_as(self.inv_freq)
297
+ freqs = torch.einsum('i,j->ij', t, self.inv_freq)
298
+ emb = torch.cat((freqs, freqs), dim=-1).to(x.device)
299
+ self.cos_cached = emb.cos()[None, None, :, :]
300
+ self.sin_cached = emb.sin()[None, None, :, :]
301
+ return self.cos_cached, self.sin_cached
302
+
303
+ def forward(self, q, k):
304
+ cos, sin = self._update_cos_sin_cache(q, q.shape[-2])
305
+ return (
306
+ (q * cos) + (self._rotate_half(q) * sin),
307
+ (k * cos) + (self._rotate_half(k) * sin),
308
+ )
309
+
310
+ def _rotate_half(self, x):
311
+ x1, x2 = x.chunk(2, dim=-1)
312
+ return torch.cat((-x2, x1), dim=-1)
313
+
314
+
315
+ class MultiHeadAttentionWithRoPE(nn.Module):
316
+ def __init__(self, d_model, n_heads, attn_dropout_p=0.0, resid_dropout_p=0.0):
317
+ super().__init__()
318
+ self.d_model = d_model
319
+ self.n_heads = n_heads
320
+ self.head_dim = d_model // n_heads
321
+
322
+ self.q_proj = nn.Linear(d_model, d_model)
323
+ self.k_proj = nn.Linear(d_model, d_model)
324
+ self.v_proj = nn.Linear(d_model, d_model)
325
+ self.out_proj = nn.Linear(d_model, d_model)
326
+ self.rotary = RotaryPositionalEmbedding(self.head_dim)
327
+ self.attn_dropout_p = attn_dropout_p
328
+ self.resid_dropout = nn.Dropout(resid_dropout_p)
329
+
330
+ def forward(self, x, key_padding_mask=None):
331
+ batch_size, seq_len, _ = x.shape
332
+
333
+ q = self.q_proj(x).view(batch_size, seq_len, self.n_heads, self.head_dim).transpose(1, 2)
334
+ k = self.k_proj(x).view(batch_size, seq_len, self.n_heads, self.head_dim).transpose(1, 2)
335
+ v = self.v_proj(x).view(batch_size, seq_len, self.n_heads, self.head_dim).transpose(1, 2)
336
+
337
+ q, k = self.rotary(q, k)
338
+
339
+ if key_padding_mask is not None:
340
+ attn_mask = key_padding_mask.unsqueeze(1).unsqueeze(2) # [batch, 1, 1, seq_len]
341
+ attn_mask = attn_mask.expand(-1, self.n_heads, seq_len, -1) # [batch, n_heads, q_len, k_len]
342
+ else:
343
+ attn_mask = None
344
+
345
+ attn_output = F.scaled_dot_product_attention(
346
+ q, k, v,
347
+ attn_mask=attn_mask,
348
+ dropout_p=self.attn_dropout_p if self.training else 0.0,
349
+ is_causal=True
350
+ )
351
+
352
+ attn_output = attn_output.transpose(1, 2).contiguous().view(batch_size, seq_len, self.d_model)
353
+ return self.resid_dropout(self.out_proj(attn_output))
354
+
355
+
356
+ class MultiHeadCrossAttentionWithRoPE(nn.Module):
357
+ def __init__(self, d_model, n_heads, attn_dropout_p=0.0, resid_dropout=0.0):
358
+ super().__init__()
359
+ self.d_model = d_model
360
+ self.n_heads = n_heads
361
+ self.head_dim = d_model // n_heads
362
+
363
+ self.q_proj = nn.Linear(d_model, d_model)
364
+ self.k_proj = nn.Linear(d_model, d_model)
365
+ self.v_proj = nn.Linear(d_model, d_model)
366
+ self.out_proj = nn.Linear(d_model, d_model)
367
+ self.rotary = RotaryPositionalEmbedding(self.head_dim)
368
+ self.attn_dropout_p = attn_dropout_p
369
+ self.resid_dropout = nn.Dropout(resid_dropout)
370
+
371
+ def forward(self, query, key, value, key_padding_mask=None):
372
+ batch_size, q_len, _ = query.shape
373
+ _, seq_len, _ = key.shape
374
+
375
+ q = self.q_proj(query).view(batch_size, q_len, self.n_heads, self.head_dim).transpose(1, 2)
376
+ k = self.k_proj(key).view(batch_size, seq_len, self.n_heads, self.head_dim).transpose(1, 2)
377
+ v = self.v_proj(value).view(batch_size, seq_len, self.n_heads, self.head_dim).transpose(1, 2)
378
+
379
+ q, k = self.rotary(q, k)
380
+
381
+ if key_padding_mask is not None:
382
+ attn_mask = key_padding_mask.unsqueeze(1).unsqueeze(2)
383
+ attn_mask = attn_mask.expand(-1, self.n_heads, q_len, -1)
384
+ else:
385
+ attn_mask = None
386
+
387
+ is_causal_flag = self.training
388
+
389
+ attn_output = F.scaled_dot_product_attention(
390
+ q, k, v,
391
+ attn_mask=attn_mask,
392
+ dropout_p=self.attn_dropout_p if self.training else 0.0,
393
+ is_causal=is_causal_flag
394
+ )
395
+
396
+ attn_output = attn_output.transpose(1, 2).contiguous().view(batch_size, q_len, self.d_model)
397
+ return self.resid_dropout(self.out_proj(attn_output))
398
+
399
+
400
+ class HierarchicalEmbedding(nn.Module):
401
+ def __init__(self, s1_bits, s2_bits, d_model=256):
402
+ super().__init__()
403
+ self.s1_bits = s1_bits
404
+ self.s2_bits = s2_bits
405
+
406
+ vocab_s1 = 2 ** s1_bits
407
+ vocab_s2 = 2 ** s2_bits
408
+
409
+ self.emb_s1 = nn.Embedding(vocab_s1, d_model)
410
+ self.emb_s2 = nn.Embedding(vocab_s2, d_model)
411
+ self.d_model = d_model
412
+ self.fusion_proj = nn.Linear(d_model * 2, d_model)
413
+
414
+ nn.init.normal_(self.emb_s1.weight, mean=0, std=d_model ** -0.5)
415
+ nn.init.normal_(self.emb_s2.weight, mean=0, std=d_model ** -0.5)
416
+
417
+ def split_token(self, token_ids: torch.Tensor, s2_bits: int):
418
+ """Inputs:
419
+ token_ids (torch.Tensor): Composite token IDs of shape [batch_size, seq_len] or [N], each in range [0, 2^(s1_bits + s2_bits) - 1].
420
+ s2_bits (int): Number of low bits used for the fine token (s2).
421
+ """
422
+ assert isinstance(s2_bits, int) and s2_bits > 0, "s2_bits must be a positive integer"
423
+
424
+ t = token_ids.long()
425
+ mask = (1 << s2_bits) - 1
426
+ s2_ids = t & mask # extract low bits
427
+ s1_ids = t >> s2_bits # extract high bits
428
+ return s1_ids, s2_ids
429
+
430
+ def forward(self, token_ids):
431
+ """Inputs:
432
+ token_ids:
433
+ - tuple or list: (s1_ids, s2_ids), each of shape [batch_size, seq_len], or
434
+ - torch.Tensor: composite token IDs of shape [batch_size, seq_len], which will be split into (s1_ids, s2_ids) internally.
435
+ Output: [batch_size, seq_len, d_model]
436
+ """
437
+ if isinstance(token_ids, tuple) or isinstance(token_ids, list):
438
+ s1_ids, s2_ids = token_ids
439
+ else:
440
+ s1_ids, s2_ids = self.split_token(token_ids, self.s2_bits)
441
+ s1_emb = self.emb_s1(s1_ids) * math.sqrt(self.d_model)
442
+ s2_emb = self.emb_s2(s2_ids) * math.sqrt(self.d_model)
443
+ return self.fusion_proj(torch.cat([s1_emb, s2_emb], dim=-1))
444
+
445
+
446
+ class DependencyAwareLayer(nn.Module):
447
+ def __init__(self, d_model, n_heads=4, attn_dropout_p=0.0, resid_dropout=0.0):
448
+ super().__init__()
449
+ self.cross_attn = MultiHeadCrossAttentionWithRoPE(d_model, n_heads, attn_dropout_p, resid_dropout)
450
+ self.norm = RMSNorm(d_model)
451
+
452
+ def forward(self, hidden_states, sibling_embed, key_padding_mask=None):
453
+ """hidden_states: [batch, seq_len, d_model]
454
+ sibling_embed: Embedding from another subtoken
455
+ """
456
+ attn_out = self.cross_attn(
457
+ query=sibling_embed,
458
+ key=hidden_states,
459
+ value=hidden_states,
460
+ key_padding_mask=key_padding_mask
461
+ )
462
+ return self.norm(hidden_states + attn_out)
463
+
464
+
465
+ class TransformerBlock(nn.Module):
466
+ def __init__(self, d_model, n_heads, ff_dim=1024, ffn_dropout_p=0.0, attn_dropout_p=0.0, resid_dropout_p=0.0):
467
+ super().__init__()
468
+ self.norm1 = RMSNorm(d_model)
469
+ self.self_attn = MultiHeadAttentionWithRoPE(d_model, n_heads, attn_dropout_p, resid_dropout_p)
470
+ self.norm2 = RMSNorm(d_model)
471
+ self.ffn = FeedForward(d_model, ff_dim, ffn_dropout_p)
472
+
473
+ def forward(self, x, key_padding_mask=None):
474
+ residual = x
475
+ x = self.norm1(x)
476
+ attn_out = self.self_attn(x, key_padding_mask=key_padding_mask)
477
+ x = residual + attn_out
478
+
479
+ residual = x
480
+ x = self.norm2(x)
481
+ ffn_out = self.ffn(x)
482
+ x = residual + ffn_out
483
+ return x
484
+
485
+
486
+ class DualHead(nn.Module):
487
+ def __init__(self, s1_bits, s2_bits, d_model):
488
+ super().__init__()
489
+ self.vocab_s1 = 2 ** s1_bits
490
+ self.vocab_s2 = 2 ** s2_bits
491
+ self.proj_s1 = nn.Linear(d_model, self.vocab_s1)
492
+ self.proj_s2 = nn.Linear(d_model, self.vocab_s2)
493
+
494
+ def compute_loss(self, s1_logits, s2_logits, s1_targets, s2_targets, padding_mask=None):
495
+ if padding_mask is not None:
496
+ valid_mask = (padding_mask == 0)
497
+ s1_logits = s1_logits[valid_mask]
498
+ s2_logits = s2_logits[valid_mask]
499
+ s1_targets = s1_targets[valid_mask]
500
+ s2_targets = s2_targets[valid_mask]
501
+ ce_s1 = F.cross_entropy(s1_logits, s1_targets)
502
+ ce_s2 = F.cross_entropy(s2_logits, s2_targets)
503
+ else:
504
+ ce_s1 = F.cross_entropy(s1_logits.reshape(-1, self.vocab_s1), s1_targets.reshape(-1))
505
+ ce_s2 = F.cross_entropy(s2_logits.reshape(-1, self.vocab_s2), s2_targets.reshape(-1))
506
+ ce_loss = (ce_s1 + ce_s2) / 2
507
+ return ce_loss, ce_s1, ce_s2
508
+
509
+ def forward(self, x):
510
+ return self.proj_s1(x)
511
+
512
+ def cond_forward(self, x2):
513
+ return self.proj_s2(x2)
514
+
515
+
516
+ class FixedEmbedding(nn.Module):
517
+ def __init__(self, c_in, d_model):
518
+ super(FixedEmbedding, self).__init__()
519
+
520
+ w = torch.zeros(c_in, d_model).float()
521
+ w.require_grad = False
522
+
523
+ position = torch.arange(0, c_in).float().unsqueeze(1)
524
+ div_term = (torch.arange(0, d_model, 2).float() * -(math.log(10000.0) / d_model)).exp()
525
+
526
+ w[:, 0::2] = torch.sin(position * div_term)
527
+ w[:, 1::2] = torch.cos(position * div_term)
528
+
529
+ self.emb = nn.Embedding(c_in, d_model)
530
+ self.emb.weight = nn.Parameter(w, requires_grad=False)
531
+
532
+ def forward(self, x):
533
+ return self.emb(x).detach()
534
+
535
+
536
+ class TemporalEmbedding(nn.Module):
537
+ def __init__(self, d_model, learn_pe):
538
+ super(TemporalEmbedding, self).__init__()
539
+
540
+ minute_size = 60
541
+ hour_size = 24
542
+ weekday_size = 7
543
+ day_size = 32
544
+ month_size = 13
545
+
546
+ Embed = FixedEmbedding if not learn_pe else nn.Embedding
547
+ self.minute_embed = Embed(minute_size, d_model)
548
+ self.hour_embed = Embed(hour_size, d_model)
549
+ self.weekday_embed = Embed(weekday_size, d_model)
550
+ self.day_embed = Embed(day_size, d_model)
551
+ self.month_embed = Embed(month_size, d_model)
552
+
553
+ def forward(self, x):
554
+ x = x.long()
555
+
556
+ minute_x = self.minute_embed(x[:, :, 0])
557
+ hour_x = self.hour_embed(x[:, :, 1])
558
+ weekday_x = self.weekday_embed(x[:, :, 2])
559
+ day_x = self.day_embed(x[:, :, 3])
560
+ month_x = self.month_embed(x[:, :, 4])
561
+
562
+ return hour_x + weekday_x + day_x + month_x + minute_x
563
+
564
+
565
+
566
+
567
+
568
+
569
+
570
+
Kronos-master/requirements.txt ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ numpy
2
+ pandas
3
+ torch>=2.0.0
4
+
5
+ einops==0.8.1
6
+ huggingface_hub==0.33.1
7
+ matplotlib==3.9.3
8
+ pandas==2.2.2
9
+ tqdm==4.67.1
10
+ safetensors==0.6.2
README.md ADDED
@@ -0,0 +1,91 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Kronos AI Analysis
2
+
3
+ Kronos AI Analysis is a FastAPI + Lightweight Charts application for:
4
+
5
+ - loading market data from multiple providers with automatic fallback
6
+ - calculating technical indicators
7
+ - generating AI forecasts with Kronos
8
+ - serving the web UI directly from the backend
9
+
10
+ ## Runtime Requirements
11
+
12
+ - Windows with Python 3.11 installed
13
+ - Internet access for live market data
14
+ - Internet access on first Kronos model load if the model is not already cached locally
15
+
16
+ ## Project Layout
17
+
18
+ ```text
19
+ Kronos_AI_Analysis/
20
+ |-- backend/
21
+ | |-- main.py
22
+ | `-- launcher.py
23
+ |-- frontend/
24
+ | `-- index.html
25
+ |-- Kronos-master/
26
+ | `-- model/
27
+ |-- dist/
28
+ | `-- SuperAI_Analysis.exe
29
+ |-- requirements.txt
30
+ `-- run_server.bat
31
+ ```
32
+
33
+ ## Recommended Start
34
+
35
+ Run:
36
+
37
+ ```bat
38
+ run_server.bat
39
+ ```
40
+
41
+ The startup script now:
42
+
43
+ 1. checks Python 3.11
44
+ 2. repairs a broken virtual environment if needed
45
+ 3. installs the full runtime dependency set
46
+ 4. verifies Kronos can be imported
47
+ 5. starts the FastAPI server on `http://127.0.0.1:8000`
48
+
49
+ ## Manual Start
50
+
51
+ ```bat
52
+ py -3.11 -m venv venv
53
+ venv\Scripts\python.exe -m pip install -r requirements.txt
54
+ venv\Scripts\python.exe -m uvicorn backend.main:app --host 127.0.0.1 --port 8000 --reload
55
+ ```
56
+
57
+ ## Important Notes
58
+
59
+ - This project uses Kronos, not TimesFM.
60
+ - Kronos source code is loaded from `Kronos-master`.
61
+ - Kronos weights are loaded from Hugging Face via:
62
+ - `NeoQuasar/Kronos-base`
63
+ - `NeoQuasar/Kronos-Tokenizer-base`
64
+ - The first AI forecast can take longer because the model may warm up or download into cache.
65
+
66
+ ## Key Endpoints
67
+
68
+ - `GET /api/health`
69
+ - `GET /api/symbols`
70
+ - `GET /api/historical/{symbol}`
71
+ - `GET /api/indicators/{symbol}`
72
+ - `GET /api/forecast/{symbol}`
73
+ - `GET /api/market-status`
74
+ - `GET /api/crypto/market`
75
+ - `WS /ws/price/{symbol}`
76
+
77
+ ## Health Expectations
78
+
79
+ When the system is healthy:
80
+
81
+ - `/api/health` returns `status: online`
82
+ - `kronos.available` is `true`
83
+ - `kronos.loaded` becomes `true` after warmup or after the first forecast
84
+ - the frontend loads from the same backend origin
85
+
86
+ ## Troubleshooting
87
+
88
+ - If Python 3.11 is missing, install it first.
89
+ - If a previous `venv` was copied from another machine, `run_server.bat` rebuilds it.
90
+ - If forecasts fail, check `/api/health` and confirm Kronos reports as available and loaded.
91
+ - If market data fails, check network reachability to Binance, Bybit, CoinGecko, Twelve Data, Finnhub, and Yahoo Finance.
SOURCES_AND_SYMBOLS.md ADDED
@@ -0,0 +1,242 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
app.py ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ import os
2
+ import uvicorn
3
+ from backend.main import app
4
+
5
+ if __name__ == "__main__":
6
+ # Hugging Face Spaces yêu cầu ứng dụng phải chạy trên cổng 7860
7
+ uvicorn.run(app, host="0.0.0.0", port=7860)
backend/launcher.py ADDED
@@ -0,0 +1,71 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import sys
2
+ import os
3
+ import time
4
+ import webbrowser
5
+ import threading
6
+ import uvicorn
7
+ import logging
8
+
9
+ # Set up professional logging
10
+ logging.basicConfig(
11
+ level=logging.INFO,
12
+ format="%(asctime)s | %(levelname)s | %(message)s",
13
+ )
14
+ logger = logging.getLogger("super-ai-launcher")
15
+
16
+ # ASCII Art Header
17
+ BANNER = r"""
18
+ ____ _ _ ____ _____ ____ _ ___ _ _ _ _ _ __ ______ ___ ____
19
+ / ___|| | | | _ \| ____| _ \ / \ |_ _| / \ | \ | | / \ | | \ \ / / ___|_ _/ ___|
20
+ \___ \| | | | |_) | _| | |_) | / _ \ | | / _ \ | \| | / _ \ | | \ V /\___ \ | \___ \
21
+ ___) | |_| | __/| |___| _ < / ___ \ | | / ___ \| |\ |/ ___ \| |___| | ___) | | ___) |
22
+ |____/ \___/|_| |_____|_| \_\/_/ \_\___| /_/ \_\_| \_/_/ \_\_____|_| |____/___|____/
23
+
24
+ ══════════════════════════════════════════════════════════════════════════════════════════════
25
+ TRADING INTELLIGENT TERMINAL - AI PHÂN TÍCH BIỂU ĐỒ NẾN SỐ 1 THẾ GIỚI
26
+ ══════════════════════════════════════════════════════════════════════════════════════════════
27
+ """
28
+
29
+ def print_banner():
30
+ # Clear console for a clean startup
31
+ os.system('cls' if os.name == 'nt' else 'clear')
32
+ print("\033[96m" + BANNER + "\033[0m")
33
+ print(" [*] Đang khởi động hệ thống phân tích AI...")
34
+ print(" [*] Đang nạp cơ sở dữ liệu thị trường...")
35
+ print(" [*] Hệ thống sẽ tự động mở trình duyệt sau khi hoàn tất.")
36
+ print(" ══════════════════════════════════════════════════════════════════════════════════════════════\n")
37
+
38
+ # Import the FastAPI app
39
+ try:
40
+ from main import app
41
+ except ImportError:
42
+ # If running from within the backend folder
43
+ sys.path.append(os.path.dirname(__file__))
44
+ from main import app
45
+
46
+ def open_browser():
47
+ """Wait for the server to start and then open the browser."""
48
+ # Give the server a few seconds to initialize
49
+ time.sleep(2.5)
50
+ url = "http://127.0.0.1:8000"
51
+ logger.info(f"Kết nối thành công! Đang mở bảng điều khiển tại {url}...")
52
+ webbrowser.open(url)
53
+
54
+ if __name__ == "__main__":
55
+ print_banner()
56
+
57
+ # Start the browser opener in a separate thread
58
+ threading.Thread(target=open_browser, daemon=True).start()
59
+
60
+ # Run the uvicorn server
61
+ logger.info("Khởi động Backend Core...")
62
+ try:
63
+ # Hide the detailed uvicorn logs for a cleaner professional feel,
64
+ # or keep them for transparency. Let's use 'warning' level for uvicorn
65
+ # but keep our own logs at 'info'.
66
+ uvicorn.run(app, host="127.0.0.1", port=8000, log_level="warning")
67
+ except Exception as e:
68
+ logger.error(f"Lỗi khởi động hệ thống: {e}")
69
+ print("\n [!] Có lỗi xảy ra trong quá trình khởi động.")
70
+ print(" [!] Nhấn Enter để xem chi tiết lỗi và thoát...")
71
+ input()
backend/main.py ADDED
The diff for this file is too large to render. See raw diff
 
frontend/ai_analysis_bg.png ADDED

Git LFS Details

  • SHA256: 51d9d42e632ed1761ab07c13882a35d6a652a9d0afc6d356dee7894801723cc2
  • Pointer size: 131 Bytes
  • Size of remote file: 951 kB
frontend/index.html ADDED
@@ -0,0 +1,2217 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="vi">
3
+
4
+ <head>
5
+ <meta charset="UTF-8" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <title>Super AI Analysis · AI phân tích biểu đồ nến số 1 thế giới hiện tại được nghiên cứu bởi đại học Thanh Hoa!
8
+ </title>
9
+ <link rel="preconnect" href="https://fonts.googleapis.com">
10
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
11
+ <link
12
+ href="https://fonts.googleapis.com/css2?family=Chakra+Petch:ital,wght@0,400;0,500;0,600;0,700;1,400&family=Barlow:wght@300;400;500;600&family=Space+Mono:wght@400;700&display=swap"
13
+ rel="stylesheet">
14
+ <script src="https://unpkg.com/lightweight-charts@4.2.2/dist/lightweight-charts.standalone.production.js"></script>
15
+ <style>
16
+ /* ═══════════════════════════════════════════════
17
+ DESIGN TOKENS
18
+ ═══════════════════════════════════════════════ */
19
+ :root {
20
+ /* Base surfaces */
21
+ --bg-base: #02060c;
22
+ --bg-depth: #010409;
23
+ --bg-glass: rgba(2, 8, 16, 0.75);
24
+ --bg-glass-light: rgba(4, 12, 24, 0.55);
25
+ --bg-control: rgba(6, 16, 36, 0.85);
26
+ --bg-control-hov: rgba(12, 28, 58, 0.95);
27
+ --bg-active: rgba(34, 211, 238, 0.12);
28
+
29
+ /* Borders */
30
+ --bdr-dim: rgba(40, 80, 140, 0.15);
31
+ --bdr-base: rgba(50, 95, 165, 0.25);
32
+ --bdr-muted: rgba(50, 100, 175, 0.38);
33
+ --bdr-accent: rgba(34, 211, 238, 0.45);
34
+
35
+ /* Text hierarchy */
36
+ --txt-bright: #ffffff;
37
+ --txt-primary: #f1f5f9; /* Boosted from #cbd5e1 */
38
+ --txt-secondary: #cbd5e1; /* Boosted from #94a3b8 */
39
+ --txt-muted: #64748b; /* Boosted from #475569 */
40
+
41
+ /* Accent — cold cyan */
42
+ --accent: #22d3ee;
43
+ --accent-lo: rgba(34, 211, 238, 0.08);
44
+ --accent-mid: rgba(34, 211, 238, 0.25);
45
+ --accent-glow: rgba(34, 211, 238, 0.40);
46
+
47
+ /* Semantic */
48
+ --ok: #2dd4bf;
49
+ --err: #fb7185;
50
+ --warn: #fbbf24;
51
+
52
+ /* Chart series */
53
+ --bull: #2dd4bf;
54
+ --bear: #fb7185;
55
+
56
+ /* Typography */
57
+ --ff-display: 'Barlow', 'Chakra Petch', sans-serif;
58
+ --ff-ui: 'Barlow', system-ui, sans-serif;
59
+ --ff-mono: 'Space Mono', monospace;
60
+
61
+ /* Layout */
62
+ --header-h: 60px;
63
+ --sidebar-w: 280px;
64
+ --radius: 8px;
65
+ --radius-lg: 18px;
66
+
67
+ /* Dashboard specificity */
68
+ --glass-cyan: rgba(6, 18, 42, 0.85);
69
+ --neon-cyan: #22d3ee;
70
+ --neon-pink: #f472b6;
71
+ --neon-green: #34d399;
72
+ --neon-blue: #3b82f6;
73
+ }
74
+
75
+ /* ═══════════════════════════════════════════════
76
+ RESET
77
+ ═══════════════════════════════════════════════ */
78
+ *,
79
+ *::before,
80
+ *::after {
81
+ box-sizing: border-box;
82
+ margin: 0;
83
+ padding: 0;
84
+ }
85
+
86
+ html,
87
+ body {
88
+ width: 100%;
89
+ height: 100%;
90
+ overflow: hidden;
91
+ background: var(--bg-base);
92
+ color: var(--txt-primary);
93
+ font-family: var(--ff-ui);
94
+ font-size: 13px;
95
+ -webkit-font-smoothing: antialiased;
96
+ -moz-osx-font-smoothing: grayscale;
97
+ }
98
+
99
+ /* ═══════════════════════════════════════════════
100
+ BACKGROUND SYSTEM
101
+ Layers (bottom→top):
102
+ 1. Solid base color
103
+ 2. Radial centre-glow (very subtle)
104
+ 3. Dot matrix
105
+ 4. Corner nebulae (pseudo)
106
+ 5. Vignette overlay
107
+ ═══════════════════════════════════════════════ */
108
+ body {
109
+ background:
110
+ /* vignette */
111
+ radial-gradient(ellipse 130% 100% at 50% 50%,
112
+ transparent 55%, rgba(1, 4, 12, 0.92) 100%),
113
+ /* top-left ambient nebula */
114
+ radial-gradient(ellipse 60% 40% at -5% -5%,
115
+ rgba(16, 80, 180, 0.12) 0%, transparent 65%),
116
+ /* bottom-right ambient nebula */
117
+ radial-gradient(ellipse 55% 38% at 105% 105%,
118
+ rgba(0, 150, 180, 0.10) 0%, transparent 65%),
119
+ /* centre elevation */
120
+ radial-gradient(ellipse 100% 80% at 50% 20%,
121
+ rgba(6, 20, 50, 0.60) 0%, transparent 70%),
122
+ /* dot grid */
123
+ radial-gradient(circle, rgba(34, 211, 238, 0.055) 1px, transparent 1px),
124
+ /* solid base */
125
+ var(--bg-base);
126
+ background-size:
127
+ 100% 100%,
128
+ 100% 100%,
129
+ 100% 100%,
130
+ 100% 100%,
131
+ 28px 28px,
132
+ 100% 100%;
133
+ }
134
+
135
+ /* ═══════════════════════════════════════════════
136
+ APP SHELL
137
+ ═══════════════════════════════════════════════ */
138
+ #app {
139
+ display: flex;
140
+ flex-direction: column;
141
+ width: 100%;
142
+ height: 100%;
143
+ position: relative;
144
+ }
145
+
146
+ .app-body {
147
+ display: flex;
148
+ flex: 1;
149
+ overflow: hidden;
150
+ position: relative;
151
+ }
152
+
153
+ /* ── SIDEBAR ────────────────────────────────── */
154
+ .sidebar {
155
+ width: var(--sidebar-w);
156
+ background: var(--bg-depth);
157
+ border-right: 1px solid var(--bdr-dim);
158
+ display: flex;
159
+ flex-direction: column;
160
+ flex-shrink: 0;
161
+ z-index: 15;
162
+ transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
163
+ }
164
+
165
+ .sidebar.collapsed {
166
+ transform: translateX(-100%);
167
+ margin-right: calc(var(--sidebar-w) * -1);
168
+ }
169
+
170
+ /* Floating Tab Trigger */
171
+ .sidebar-trigger {
172
+ position: absolute;
173
+ left: 0;
174
+ top: 50%;
175
+ transform: translateY(-50%);
176
+ width: 28px;
177
+ height: 80px;
178
+ background: var(--bg-depth);
179
+ border: 1px solid var(--bdr-accent);
180
+ border-left: none;
181
+ border-radius: 0 8px 8px 0;
182
+ display: none;
183
+ align-items: center;
184
+ justify-content: center;
185
+ cursor: pointer;
186
+ z-index: 20;
187
+ transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
188
+ box-shadow: 4px 0 15px rgba(0, 0, 0, 0.5);
189
+ }
190
+
191
+ .sidebar-trigger:hover {
192
+ background: var(--bg-active);
193
+ width: 32px;
194
+ box-shadow: 0 0 20px var(--accent-glow);
195
+ }
196
+
197
+ .sidebar-trigger span {
198
+ writing-mode: vertical-lr;
199
+ transform: rotate(180deg);
200
+ font-size: 0.7rem;
201
+ font-weight: 700;
202
+ letter-spacing: 0.15em;
203
+ text-transform: uppercase;
204
+ color: var(--accent);
205
+ white-space: nowrap;
206
+ }
207
+
208
+ .sidebar-head {
209
+ height: 48px;
210
+ display: flex;
211
+ align-items: center;
212
+ justify-content: space-between;
213
+ padding: 0 16px;
214
+ border-bottom: 1px solid var(--bdr-dim);
215
+ background: rgba(255, 255, 255, 0.02);
216
+ }
217
+
218
+ .sidebar-title {
219
+ font-family: var(--ff-display);
220
+ font-size: 0.75rem;
221
+ font-weight: 700;
222
+ letter-spacing: 0.15em;
223
+ text-transform: uppercase;
224
+ color: var(--txt-primary);
225
+ }
226
+
227
+ .sidebar-content {
228
+ flex: 1;
229
+ overflow-y: auto;
230
+ padding: 12px;
231
+ }
232
+
233
+ /* Market Overview List */
234
+ .market-list {
235
+ display: flex;
236
+ flex-direction: column;
237
+ gap: 1px;
238
+ }
239
+
240
+ .market-item {
241
+ display: flex;
242
+ align-items: center;
243
+ gap: 12px;
244
+ padding: 10px 12px;
245
+ border-radius: var(--radius);
246
+ cursor: pointer;
247
+ transition: background 0.2s;
248
+ }
249
+
250
+ .market-item:hover {
251
+ background: rgba(255, 255, 255, 0.05);
252
+ }
253
+
254
+ .market-item.active {
255
+ background: var(--bg-active);
256
+ border: 1px solid var(--bdr-accent);
257
+ }
258
+
259
+ .m-sym {
260
+ flex: 1;
261
+ display: flex;
262
+ flex-direction: column;
263
+ }
264
+
265
+ .m-ticker {
266
+ font-family: var(--ff-mono);
267
+ font-size: 0.85rem;
268
+ font-weight: 700;
269
+ color: var(--txt-bright);
270
+ }
271
+
272
+ .m-name {
273
+ font-size: 0.72rem;
274
+ color: var(--txt-secondary);
275
+ }
276
+
277
+ .m-price-box {
278
+ text-align: right;
279
+ }
280
+
281
+ .m-price {
282
+ font-family: var(--ff-mono);
283
+ font-size: 0.85rem;
284
+ font-weight: 600;
285
+ color: var(--txt-bright);
286
+ }
287
+
288
+ .m-change {
289
+ font-size: 0.72rem;
290
+ font-weight: 700;
291
+ }
292
+
293
+ .m-change.up { color: var(--ok); }
294
+ .m-change.down { color: var(--err); }
295
+
296
+
297
+ /* ═══════════════════════════════════════════════
298
+ HEADER
299
+ ═══════════════════════════════════════════════ */
300
+ .hdr {
301
+ height: var(--header-h);
302
+ flex-shrink: 0;
303
+ display: flex;
304
+ align-items: center;
305
+ justify-content: space-between;
306
+ gap: 16px;
307
+ padding: 0 20px 0 18px;
308
+ background: var(--bg-glass);
309
+ backdrop-filter: blur(20px) saturate(160%);
310
+ -webkit-backdrop-filter: blur(20px) saturate(160%);
311
+ border-bottom: 1px solid var(--bdr-dim);
312
+ position: relative;
313
+ z-index: 20;
314
+ animation: hdr-slide-in 0.5s ease both;
315
+ }
316
+
317
+ /* thin accent line bottom */
318
+ .hdr::after {
319
+ content: '';
320
+ position: absolute;
321
+ bottom: -1px;
322
+ left: 0;
323
+ right: 0;
324
+ height: 1px;
325
+ background: linear-gradient(90deg,
326
+ transparent 0%, var(--accent-mid) 20%,
327
+ var(--accent-mid) 80%, transparent 100%);
328
+ pointer-events: none;
329
+ }
330
+
331
+ @keyframes hdr-slide-in {
332
+ from {
333
+ transform: translateY(-100%);
334
+ opacity: 0;
335
+ }
336
+
337
+ to {
338
+ transform: translateY(0);
339
+ opacity: 1;
340
+ }
341
+ }
342
+
343
+ /* ── Logo ─────────────────────────────────────── */
344
+ .logo {
345
+ display: flex;
346
+ align-items: center;
347
+ gap: 11px;
348
+ flex-shrink: 0;
349
+ }
350
+
351
+ .logo-mark {
352
+ position: relative;
353
+ width: 48px;
354
+ height: 48px;
355
+ flex-shrink: 0;
356
+ }
357
+
358
+ .logo-mark svg {
359
+ width: 100%;
360
+ height: 100%;
361
+ filter: drop-shadow(0 0 6px var(--accent-glow));
362
+ }
363
+
364
+ .logo-text {
365
+ display: flex;
366
+ flex-direction: column;
367
+ gap: 1px;
368
+ }
369
+
370
+ .logo-name {
371
+ font-family: var(--ff-display);
372
+ font-size: 1.45rem;
373
+ font-weight: 700;
374
+ letter-spacing: 0.12em;
375
+ color: #ffffff;
376
+ line-height: 1;
377
+ background: linear-gradient(90deg, #ffffff 0%, #a8f4ff 60%, #ffffff 100%);
378
+ -webkit-background-clip: text;
379
+ -webkit-text-fill-color: transparent;
380
+ background-clip: text;
381
+ }
382
+
383
+ .logo-name span {
384
+ font-weight: 700;
385
+ letter-spacing: 0.12em;
386
+ opacity: 1;
387
+ }
388
+
389
+ .logo-sub {
390
+ font-size: 0.78rem;
391
+ font-family: var(--ff-ui);
392
+ font-weight: 400;
393
+ letter-spacing: 0.05em;
394
+ color: #ffffff;
395
+ line-height: 1.2;
396
+ }
397
+
398
+ /* ── Divider ──────────────────────────────────── */
399
+ .hdr-divider {
400
+ width: 1px;
401
+ height: 28px;
402
+ background: var(--bdr-base);
403
+ flex-shrink: 0;
404
+ }
405
+
406
+ /* ── Controls bar ─────────────────────────────── */
407
+ .ctrls {
408
+ display: flex;
409
+ align-items: center;
410
+ gap: 12px;
411
+ flex: 1;
412
+ justify-content: flex-end;
413
+ }
414
+
415
+ /* Search Omnibox */
416
+ .omnibox {
417
+ position: relative;
418
+ width: 320px;
419
+ }
420
+
421
+ .omnibox input {
422
+ width: 100%;
423
+ height: 38px;
424
+ background: var(--bg-control);
425
+ border: 1px solid var(--bdr-base);
426
+ border-radius: var(--radius);
427
+ padding: 0 12px;
428
+ padding-left: 36px;
429
+ color: var(--txt-bright);
430
+ font-family: var(--ff-ui);
431
+ font-size: 0.9rem;
432
+ outline: none;
433
+ transition: all 0.22s;
434
+ }
435
+
436
+ .omnibox input:focus {
437
+ border-color: var(--accent);
438
+ box-shadow: 0 0 0 3px var(--accent-lo);
439
+ background: var(--bg-control-hov);
440
+ }
441
+
442
+ .omnibox i {
443
+ position: absolute;
444
+ left: 12px;
445
+ top: 50%;
446
+ transform: translateY(-50%);
447
+ color: var(--txt-secondary);
448
+ pointer-events: none;
449
+ }
450
+
451
+ .search-results {
452
+ position: absolute;
453
+ top: 100%;
454
+ left: 0;
455
+ right: 0;
456
+ margin-top: 8px;
457
+ background: var(--bg-glass);
458
+ backdrop-filter: blur(20px);
459
+ border: 1px solid var(--bdr-muted);
460
+ border-radius: var(--radius);
461
+ box-shadow: 0 10px 25px rgba(0, 0, 0, 0.5);
462
+ max-height: 400px;
463
+ overflow-y: auto;
464
+ z-index: 100;
465
+ display: none;
466
+ }
467
+
468
+ .search-results.visible {
469
+ display: block;
470
+ }
471
+
472
+ .search-item {
473
+ padding: 10px 14px;
474
+ display: flex;
475
+ justify-content: space-between;
476
+ align-items: center;
477
+ cursor: pointer;
478
+ transition: background 0.15s;
479
+ }
480
+
481
+ .search-item:hover {
482
+ background: rgba(255, 255, 255, 0.08);
483
+ }
484
+
485
+ .search-item .sym {
486
+ font-family: var(--ff-mono);
487
+ font-weight: 700;
488
+ color: var(--accent);
489
+ }
490
+
491
+ .search-item .name {
492
+ font-size: 0.8rem;
493
+ color: var(--txt-secondary);
494
+ }
495
+
496
+ .market-status-bar {
497
+ display: flex;
498
+ align-items: center;
499
+ gap: 16px;
500
+ margin-right: 20px;
501
+ }
502
+
503
+ .m-stat {
504
+ display: flex;
505
+ align-items: center;
506
+ gap: 6px;
507
+ font-size: 0.72rem;
508
+ font-weight: 600;
509
+ text-transform: uppercase;
510
+ letter-spacing: 0.05em;
511
+ }
512
+
513
+ .m-stat .pulse {
514
+ width: 6px;
515
+ height: 6px;
516
+ border-radius: 50%;
517
+ }
518
+
519
+ .m-stat.open { color: var(--ok); }
520
+ .m-stat.open .pulse { background: var(--ok); box-shadow: 0 0 8px var(--ok); }
521
+ .m-stat.closed { color: var(--txt-muted); }
522
+ .m-stat.closed .pulse { background: var(--txt-muted); }
523
+
524
+
525
+ .ctrl-unit {
526
+ display: flex;
527
+ flex-direction: column;
528
+ gap: 3px;
529
+ }
530
+
531
+ .ctrl-label {
532
+ font-family: var(--ff-display);
533
+ font-size: 0.78rem;
534
+ font-weight: 500;
535
+ letter-spacing: 0.18em;
536
+ color: #ffffff;
537
+ text-transform: uppercase;
538
+ padding-left: 2px;
539
+ line-height: 1;
540
+ }
541
+
542
+ /* unified form control look */
543
+ .k-select,
544
+ .k-input {
545
+ height: var(--ctrl-h);
546
+ background: var(--bg-control);
547
+ border: 1px solid var(--bdr-base);
548
+ color: var(--txt-bright);
549
+ padding: 0 10px;
550
+ border-radius: var(--radius);
551
+ font-family: var(--ff-ui);
552
+ font-size: 0.95rem;
553
+ font-weight: 500;
554
+ outline: none;
555
+ cursor: pointer;
556
+ transition:
557
+ border-color 0.22s ease,
558
+ background 0.22s ease,
559
+ box-shadow 0.22s ease;
560
+ appearance: none;
561
+ -webkit-appearance: none;
562
+ }
563
+
564
+ .k-select {
565
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6' fill='none'%3E%3Cpath d='M1 1l4 4 4-4' stroke='%235080a8' stroke-width='1.4' stroke-linecap='round'/%3E%3C/svg%3E");
566
+ background-repeat: no-repeat;
567
+ background-position: right 9px center;
568
+ padding-right: 28px;
569
+ }
570
+
571
+ .k-select optgroup {
572
+ background: #060d20;
573
+ color: var(--txt-secondary);
574
+ font-size: 0.75rem;
575
+ }
576
+
577
+ .k-select option {
578
+ background: #060d20;
579
+ color: var(--txt-bright);
580
+ }
581
+
582
+ .k-select:hover,
583
+ .k-input:hover,
584
+ .k-select:focus,
585
+ .k-input:focus {
586
+ border-color: var(--bdr-accent);
587
+ background: var(--bg-control-hov);
588
+ box-shadow: 0 0 0 3px var(--accent-lo), inset 0 0 0 1px var(--accent-lo);
589
+ }
590
+
591
+ #symbolSelect {
592
+ width: 280px;
593
+ }
594
+
595
+ #timeframeSelect {
596
+ width: 110px;
597
+ }
598
+
599
+ #horizonInput {
600
+ width: 90px;
601
+ text-align: center;
602
+ font-family: var(--ff-mono);
603
+ font-size: 0.95rem;
604
+ }
605
+
606
+ /* primary button */
607
+ .btn-primary {
608
+ height: var(--ctrl-h);
609
+ padding: 0 18px;
610
+ background: linear-gradient(135deg, rgba(34, 211, 238, 0.18) 0%, rgba(34, 211, 238, 0.10) 100%);
611
+ border: 1px solid var(--bdr-accent);
612
+ color: var(--accent);
613
+ border-radius: var(--radius);
614
+ font-family: var(--ff-display);
615
+ font-size: 0.95rem;
616
+ font-weight: 700;
617
+ letter-spacing: 0.12em;
618
+ text-transform: uppercase;
619
+ cursor: pointer;
620
+ transition:
621
+ background 0.22s ease,
622
+ box-shadow 0.22s ease,
623
+ transform 0.14s ease,
624
+ border-color 0.22s ease;
625
+ white-space: nowrap;
626
+ }
627
+
628
+ .btn-primary:hover {
629
+ background: linear-gradient(135deg, rgba(34, 211, 238, 0.30) 0%, rgba(34, 211, 238, 0.18) 100%);
630
+ box-shadow: var(--glow-accent);
631
+ border-color: var(--accent);
632
+ }
633
+
634
+ .btn-primary:active {
635
+ transform: scale(0.97);
636
+ }
637
+
638
+ .btn-primary:disabled {
639
+ opacity: 0.35;
640
+ cursor: not-allowed;
641
+ box-shadow: none;
642
+ transform: none;
643
+ border-color: var(--bdr-dim);
644
+ }
645
+
646
+ /* secondary icon button */
647
+ .btn-icon {
648
+ height: var(--ctrl-h);
649
+ width: var(--ctrl-h);
650
+ display: flex;
651
+ align-items: center;
652
+ justify-content: center;
653
+ background: var(--bg-control);
654
+ border: 1px solid var(--bdr-base);
655
+ color: var(--txt-secondary);
656
+ border-radius: var(--radius);
657
+ cursor: pointer;
658
+ font-size: 0.90rem;
659
+ line-height: 1;
660
+ transition:
661
+ border-color 0.22s,
662
+ background 0.22s,
663
+ color 0.22s,
664
+ box-shadow 0.22s,
665
+ transform 0.14s;
666
+ }
667
+
668
+ .btn-icon:hover {
669
+ border-color: var(--bdr-muted);
670
+ background: var(--bg-control-hov);
671
+ color: var(--txt-bright);
672
+ }
673
+
674
+ .btn-icon:active {
675
+ transform: scale(0.93);
676
+ }
677
+
678
+ /* ═══════════════════════════════════════════════
679
+ MAIN CHART AREA
680
+ ═══════════════════════════════════════════════ */
681
+ .main {
682
+ flex: 1;
683
+ position: relative;
684
+ overflow: hidden;
685
+ /* subtle inner top shadow */
686
+ box-shadow: inset 0 8px 24px rgba(0, 0, 0, 0.35);
687
+ }
688
+
689
+ /* ═══════════════════════════════════════════════
690
+ ANALYSIS DASHBOARD v6.0 — 3 GAUGE HERO
691
+ ═══════════════════════════════════════════════ */
692
+ .analysis-panel {
693
+ position: absolute;
694
+ inset: 0;
695
+ z-index: 100;
696
+ width: 100%;
697
+ height: 100%;
698
+ padding: 0;
699
+ background: rgba(2, 6, 14, 0.98);
700
+ backdrop-filter: blur(50px) saturate(220%);
701
+ -webkit-backdrop-filter: blur(50px) saturate(220%);
702
+ display: none; /* FULL HIDE */
703
+ flex-direction: column;
704
+ overflow: hidden;
705
+ animation: dash-in 0.5s cubic-bezier(0.16, 1, 0.3, 1) both;
706
+ }
707
+ .analysis-panel.active {
708
+ display: flex;
709
+ }
710
+ @keyframes dash-in {
711
+ from { opacity: 0; transform: scale(0.98); }
712
+ to { opacity: 1; transform: scale(1); }
713
+ }
714
+
715
+ /* ── Dashboard Header ── */
716
+ .dash-header {
717
+ display: flex; align-items: center; justify-content: space-between;
718
+ padding: 20px 40px; flex-shrink: 0;
719
+ border-bottom: 1px solid rgba(255,255,255,0.08);
720
+ background: linear-gradient(180deg, rgba(255,255,255,0.03) 0%, rgba(255,255,255,0.01) 100%);
721
+ }
722
+ .dash-header-left { display: flex; align-items: center; gap: 14px; }
723
+ .dash-logo-dot {
724
+ width: 16px; height: 16px; border-radius: 50%;
725
+ background: linear-gradient(135deg, #22d3ee, #3b82f6);
726
+ box-shadow: 0 0 14px rgba(34,211,238,0.6);
727
+ animation: dot-breathe 2.5s ease-in-out infinite;
728
+ }
729
+ @keyframes dot-breathe {
730
+ 0%,100% { box-shadow: 0 0 8px rgba(34,211,238,0.4); }
731
+ 50% { box-shadow: 0 0 22px rgba(34,211,238,0.8); }
732
+ }
733
+ .dash-title {
734
+ font-family: var(--ff-display); font-size: 1.6rem; font-weight: 800;
735
+ color: #ffffff; letter-spacing: 0.12em; text-transform: uppercase;
736
+ }
737
+ .dash-subtitle {
738
+ font-size: 1rem; color: rgba(255,255,255,0.5); margin-left: 15px;
739
+ letter-spacing: 0.08em;
740
+ }
741
+ .dash-close {
742
+ width: 40px; height: 40px; display: flex; align-items: center; justify-content: center;
743
+ cursor: pointer; color: rgba(255,255,255,0.4); border-radius: 10px;
744
+ border: 1px solid rgba(255,255,255,0.08); background: rgba(255,255,255,0.03);
745
+ transition: all 0.2s;
746
+ }
747
+ .dash-close:hover { background: rgba(255,255,255,0.1); color: #fff; border-color: rgba(255,255,255,0.2); }
748
+
749
+ /* ── Dashboard Body — top gauges + bottom tables ── */
750
+ .dash-body {
751
+ flex: 1; display: flex; flex-direction: column; overflow: hidden;
752
+ }
753
+
754
+ /* ── TOP: 3 Big Gauges Row ── */
755
+ .dash-gauges-hero {
756
+ display: flex; justify-content: center; align-items: stretch;
757
+ gap: 0; flex-shrink: 0;
758
+ border-bottom: 1px solid rgba(255,255,255,0.06);
759
+ }
760
+ .gauge-hero-card {
761
+ flex: 1; min-width: 0;
762
+ display: flex; flex-direction: column; align-items: center;
763
+ justify-content: center; gap: 15px;
764
+ padding: 40px 30px;
765
+ border-right: 1px solid rgba(255,255,255,0.08);
766
+ transition: all 0.4s cubic-bezier(0.16, 1, 0.3, 1);
767
+ position: relative;
768
+ }
769
+ .gauge-hero-card:last-child { border-right: none; }
770
+ .gauge-hero-card:hover { background: rgba(255,255,255,0.03); transform: translateY(-4px); }
771
+ .gauge-hero-card.hero-total {
772
+ background: linear-gradient(180deg, rgba(34, 211, 238, 0.08) 0%, rgba(2,6,14,0) 100%);
773
+ }
774
+ .gauge-hero-card.hero-total::before {
775
+ content: ''; position: absolute; top: 0; left: 10%; right: 10%; height: 4px;
776
+ background: linear-gradient(90deg, transparent, var(--accent), transparent);
777
+ border-radius: 4px;
778
+ box-shadow: 0 0 20px var(--accent-glow);
779
+ }
780
+
781
+ .gauge-hero-title {
782
+ font-family: var(--ff-display); font-size: 1.3rem; font-weight: 800;
783
+ color: rgba(255,255,255,0.9); text-transform: uppercase;
784
+ letter-spacing: 0.22em; text-align: center;
785
+ }
786
+ .gauge-hero-title.title-total {
787
+ font-size: 1.55rem; color: #ffffff;
788
+ text-shadow: 0 0 35px rgba(34,211,238,0.5);
789
+ }
790
+ .gauge-hero-svg-wrap {
791
+ position: relative;
792
+ width: 320px; height: 220px;
793
+ display: flex; align-items: center; justify-content: center;
794
+ }
795
+ .gauge-hero-value-box {
796
+ position: absolute;
797
+ top: 62%; left: 50%;
798
+ transform: translate(-50%, -50%);
799
+ display: flex; flex-direction: column; align-items: center;
800
+ }
801
+ .gauge-hero-value-num {
802
+ font-family: var(--ff-display); font-size: 2.2rem; font-weight: 900;
803
+ color: #fff; line-height: 1;
804
+ text-shadow: 0 0 20px rgba(255,255,255,0.3);
805
+ }
806
+ .gauge-hero-value-label {
807
+ font-size: 0.7rem; font-weight: 700; color: rgba(255,255,255,0.4);
808
+ text-transform: uppercase; letter-spacing: 0.1em; margin-top: 4px;
809
+ }
810
+
811
+ .gauge-hero-signal {
812
+ font-family: var(--ff-display); font-size: 1.8rem; font-weight: 900;
813
+ text-transform: uppercase; letter-spacing: 0.15em;
814
+ text-shadow: 0 0 20px rgba(0,0,0,0.5);
815
+ margin-top: 5px;
816
+ }
817
+ .gauge-hero-signal.strong-buy { color: #22c55e; text-shadow: 0 0 15px rgba(34, 197, 94, 0.4); }
818
+ .gauge-hero-signal.buy { color: #86efac; }
819
+ .gauge-hero-signal.strong-sell { color: #ef4444; text-shadow: 0 0 15px rgba(239, 68, 68, 0.4); }
820
+ .gauge-hero-signal.sell { color: #fca5a5; }
821
+ .gauge-hero-signal.neutral { color: #cbd5e1; }
822
+ .gauge-hero-signal.signal-total { font-size: 2.2rem; }
823
+ .gauge-hero-counts {
824
+ display: flex; gap: 20px; font-size: 0.8rem; color: rgba(255,255,255,0.4);
825
+ letter-spacing: 0.02em;
826
+ }
827
+ .gauge-hero-counts .ghc-label { text-transform: uppercase; font-weight: 600; }
828
+ .gauge-hero-counts .ghc-value {
829
+ font-family: var(--ff-mono); font-weight: 900;
830
+ font-size: 1.1rem; color: #ffffff; display: block;
831
+ margin-top: 2px;
832
+ }
833
+
834
+ /* ── BOTTOM: Data Tables Grid ── */
835
+ .dash-tables-row {
836
+ flex: 1; display: grid; overflow: hidden;
837
+ grid-template-columns: 1fr 1fr 1fr;
838
+ gap: 0;
839
+ }
840
+ .dash-col {
841
+ display: flex; flex-direction: column;
842
+ border-right: 1px solid rgba(255,255,255,0.04);
843
+ overflow: hidden;
844
+ }
845
+ .dash-col:last-child { border-right: none; }
846
+
847
+ .dc-header {
848
+ padding: 14px 20px; flex-shrink: 0;
849
+ border-bottom: 1px solid rgba(255,255,255,0.06);
850
+ background: rgba(255,255,255,0.018);
851
+ font-family: var(--ff-display);
852
+ font-size: 0.85rem; font-weight: 800; color: #ffffff;
853
+ text-transform: uppercase; letter-spacing: 0.12em;
854
+ }
855
+
856
+ .dash-table-wrap {
857
+ flex: 1; overflow-y: auto; overflow-x: hidden;
858
+ }
859
+ .dash-table-wrap::-webkit-scrollbar { width: 3px; }
860
+ .dash-table-wrap::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.08); border-radius: 10px; }
861
+
862
+ .dt { width: 100%; border-collapse: collapse; }
863
+ .dt td {
864
+ padding: 9px 18px; font-size: 0.88rem;
865
+ border-bottom: 1px solid rgba(255,255,255,0.03);
866
+ }
867
+ .dt td:first-child { color: rgba(255,255,255,0.8); font-weight: 500; }
868
+ .dt .dt-val {
869
+ font-family: var(--ff-mono); font-weight: 700; color: #ffffff; text-align: right;
870
+ }
871
+ .dt .dt-act {
872
+ font-weight: 800; text-align: right; font-size: 0.85rem; letter-spacing: 0.03em;
873
+ }
874
+ .dt-act-buy { color: #60a5fa !important; }
875
+ .dt-act-sell { color: #fb7185 !important; }
876
+ .dt-act-neut { color: rgba(255,255,255,0.3) !important; }
877
+
878
+ .pivot-table { width: 100%; border-collapse: collapse; }
879
+ .pivot-table th {
880
+ padding: 8px 10px; font-size: 0.7rem; font-weight: 800;
881
+ color: rgba(255,255,255,0.4); text-transform: uppercase;
882
+ letter-spacing: 0.08em; text-align: center;
883
+ border-bottom: 1px solid rgba(255,255,255,0.06);
884
+ }
885
+ .pivot-table td {
886
+ padding: 7px 10px; font-family: var(--ff-mono); font-size: 0.85rem;
887
+ font-weight: 600; color: #ffffff; text-align: center;
888
+ border-bottom: 1px solid rgba(255,255,255,0.025);
889
+ }
890
+ .pivot-table td:first-child {
891
+ font-weight: 800; color: rgba(255,255,255,0.55); font-family: var(--ff-ui);
892
+ text-align: left; padding-left: 18px;
893
+ }
894
+
895
+ .summary-disclaimer {
896
+ padding: 10px 20px;
897
+ font-size: 0.72rem; color: rgba(255,255,255,0.25); line-height: 1.5;
898
+ border-top: 1px solid rgba(255,255,255,0.04);
899
+ text-align: center; flex-shrink: 0;
900
+ }
901
+ .summary-disclaimer strong { color: rgba(255,255,255,0.4); }
902
+
903
+ /* ── Loading state ── */
904
+ .dash-loading {
905
+ display: flex; flex-direction: column; align-items: center; justify-content: center;
906
+ height: 100%; gap: 16px; color: rgba(255,255,255,0.4);
907
+ }
908
+ .dash-loading .loader-ring { border-top-color: var(--accent); }
909
+ .dash-loading p { font-size: 0.9rem; letter-spacing: 0.05em; animation: loader-pulse 1.4s ease-in-out infinite; }
910
+
911
+ /* Dashboard responsive */
912
+ @media (max-width: 1200px) {
913
+ .dash-tables-row { grid-template-columns: 1fr 1fr; }
914
+ .dash-col.col-pivots { display: none; }
915
+ .gauge-hero-card { padding: 20px 14px 18px; }
916
+ .gauge-hero-svg-wrap { width: 180px; height: 108px; }
917
+ }
918
+ @media (max-width: 800px) {
919
+ .dash-gauges-hero { flex-direction: column; }
920
+ .gauge-hero-card { border-right: none; border-bottom: 1px solid rgba(255,255,255,0.05); }
921
+ .dash-tables-row { grid-template-columns: 1fr; }
922
+ }
923
+
924
+ #chart {
925
+ position: absolute;
926
+ inset: 0;
927
+ width: 100%;
928
+ height: 100%;
929
+ z-index: 5;
930
+ }
931
+
932
+ /* Futuristic AI Overlay */
933
+ .chart-bg-overlay {
934
+ position: absolute;
935
+ inset: 0;
936
+ pointer-events: none;
937
+ z-index: 1;
938
+ opacity: 0.17;
939
+ background-image: url('ai_analysis_bg.png');
940
+ background-size: contain;
941
+ background-repeat: no-repeat;
942
+ background-position: left bottom;
943
+ mask-image: linear-gradient(to right, black 20%, transparent 80%);
944
+ -webkit-mask-image: linear-gradient(to right, black 20%, transparent 80%);
945
+ }
946
+
947
+ .chart-logo-overlay {
948
+ position: absolute;
949
+ bottom: 35px;
950
+ right: 85px;
951
+ pointer-events: none;
952
+ z-index: 2;
953
+ font-family: var(--ff-display);
954
+ font-size: 2.2rem;
955
+ font-weight: 700;
956
+ letter-spacing: 0.25em;
957
+ line-height: 1;
958
+ background: linear-gradient(180deg, #fff 0%, var(--accent) 50%, #0a4d5c 100%);
959
+ -webkit-background-clip: text;
960
+ -webkit-text-fill-color: transparent;
961
+ background-clip: text;
962
+ filter: drop-shadow(0 0 15px rgba(34, 211, 238, 0.45));
963
+ opacity: 0.55;
964
+ text-transform: uppercase;
965
+ white-space: nowrap;
966
+ }
967
+
968
+ /* ── Terminal Loader Overlay (Strict Isolation) ── */
969
+ .terminal-loader-overlay {
970
+ position: absolute;
971
+ inset: 0 !important;
972
+ width: 100% !important;
973
+ height: 100% !important;
974
+ display: none; /* FULL HIDE */
975
+ flex-direction: column;
976
+ align-items: center;
977
+ justify-content: center;
978
+ background: rgba(1, 4, 12, 0.98);
979
+ backdrop-filter: blur(50px) saturate(200%);
980
+ -webkit-backdrop-filter: blur(50px) saturate(200%);
981
+ z-index: 9999;
982
+ opacity: 0;
983
+ transition: opacity 0.5s ease;
984
+ }
985
+
986
+ .terminal-loader-overlay.active {
987
+ display: flex;
988
+ opacity: 1;
989
+ }
990
+
991
+ .loader-inner {
992
+ display: flex;
993
+ flex-direction: column;
994
+ align-items: center;
995
+ gap: 14px;
996
+ }
997
+
998
+ .loader-ring {
999
+ width: 42px;
1000
+ height: 42px;
1001
+ border-radius: 50%;
1002
+ border: 1.5px solid var(--bdr-dim);
1003
+ border-top-color: var(--accent);
1004
+ animation: spin 0.8s linear infinite;
1005
+ }
1006
+
1007
+ @keyframes spin {
1008
+ to {
1009
+ transform: rotate(360deg);
1010
+ }
1011
+ }
1012
+
1013
+ .loader-text {
1014
+ font-family: var(--ff-display);
1015
+ font-size: 0.68rem;
1016
+ letter-spacing: 0.22em;
1017
+ color: var(--txt-secondary);
1018
+ text-transform: uppercase;
1019
+ animation: loader-pulse 1.4s ease-in-out infinite;
1020
+ }
1021
+
1022
+ @keyframes loader-pulse {
1023
+
1024
+ 0%,
1025
+ 100% {
1026
+ opacity: 0.4;
1027
+ }
1028
+
1029
+ 50% {
1030
+ opacity: 1;
1031
+ }
1032
+ }
1033
+
1034
+ /* ═══════════════════════════════════════════════
1035
+ STATUS OVERLAY
1036
+ ═══════════════════════════════════════════════ */
1037
+ .status-wrap {
1038
+ position: absolute;
1039
+ top: 16px;
1040
+ left: 16px;
1041
+ z-index: 10;
1042
+ pointer-events: none;
1043
+ animation: fade-in 0.6s 0.3s ease both;
1044
+ }
1045
+
1046
+ @keyframes fade-in {
1047
+ from {
1048
+ opacity: 0;
1049
+ transform: translateY(-6px);
1050
+ }
1051
+
1052
+ to {
1053
+ opacity: 1;
1054
+ transform: translateY(0);
1055
+ }
1056
+ }
1057
+
1058
+ .status-pill {
1059
+ display: inline-flex;
1060
+ align-items: center;
1061
+ gap: 9px;
1062
+ padding: 7px 14px 7px 10px;
1063
+ background: var(--bg-glass);
1064
+ backdrop-filter: blur(14px) saturate(160%);
1065
+ -webkit-backdrop-filter: blur(14px) saturate(160%);
1066
+ border: 1px solid var(--bdr-dim);
1067
+ border-radius: 20px;
1068
+ transition: border-color 0.3s ease;
1069
+ }
1070
+
1071
+ .status-pill.ok {
1072
+ border-color: rgba(45, 212, 170, 0.30);
1073
+ }
1074
+
1075
+ .status-pill.error {
1076
+ border-color: rgba(240, 96, 112, 0.30);
1077
+ }
1078
+
1079
+ .status-pill.loading {
1080
+ border-color: rgba(34, 211, 238, 0.28);
1081
+ }
1082
+
1083
+ .dot {
1084
+ width: 7px;
1085
+ height: 7px;
1086
+ border-radius: 50%;
1087
+ background: var(--txt-muted);
1088
+ flex-shrink: 0;
1089
+ transition: background 0.3s, box-shadow 0.3s;
1090
+ }
1091
+
1092
+ .dot.ok {
1093
+ background: var(--ok);
1094
+ box-shadow: 0 0 8px var(--ok-glow);
1095
+ }
1096
+
1097
+ .dot.error {
1098
+ background: var(--err);
1099
+ box-shadow: 0 0 8px var(--err-glow);
1100
+ }
1101
+
1102
+ .dot.loading {
1103
+ background: var(--accent);
1104
+ box-shadow: 0 0 8px var(--accent-glow);
1105
+ animation: dot-pulse 1.2s ease-in-out infinite;
1106
+ }
1107
+
1108
+ @keyframes dot-pulse {
1109
+
1110
+ 0%,
1111
+ 100% {
1112
+ transform: scale(0.85);
1113
+ opacity: 0.5;
1114
+ }
1115
+
1116
+ 50% {
1117
+ transform: scale(1.20);
1118
+ opacity: 1.0;
1119
+ }
1120
+ }
1121
+
1122
+ .status-label {
1123
+ font-family: var(--ff-ui);
1124
+ font-size: 0.72rem;
1125
+ font-weight: 500;
1126
+ color: var(--txt-primary);
1127
+ letter-spacing: 0.02em;
1128
+ white-space: nowrap;
1129
+ }
1130
+
1131
+ /* ═══════════════════════════════════════════════
1132
+ SCROLLBAR
1133
+ ═══════════════════════════════════════════════ */
1134
+ ::-webkit-scrollbar {
1135
+ width: 4px;
1136
+ height: 4px;
1137
+ }
1138
+
1139
+ ::-webkit-scrollbar-track {
1140
+ background: transparent;
1141
+ }
1142
+
1143
+ ::-webkit-scrollbar-thumb {
1144
+ background: var(--bdr-base);
1145
+ border-radius: 2px;
1146
+ }
1147
+
1148
+ ::-webkit-scrollbar-thumb:hover {
1149
+ background: var(--txt-muted);
1150
+ }
1151
+
1152
+ /* ═══════════════════════════════════════════════
1153
+ RESPONSIVE
1154
+ ═══════════════════════════════════════════════ */
1155
+ @media (max-width: 960px) {
1156
+ .ctrl-label {
1157
+ display: none;
1158
+ }
1159
+
1160
+ .ctrl-unit {
1161
+ flex-direction: row;
1162
+ align-items: center;
1163
+ }
1164
+
1165
+ #symbolSelect {
1166
+ width: 150px;
1167
+ }
1168
+
1169
+ .logo-sub {
1170
+ display: none;
1171
+ }
1172
+ }
1173
+
1174
+ @media (max-width: 680px) {
1175
+ .hdr {
1176
+ padding: 0 12px;
1177
+ gap: 8px;
1178
+ }
1179
+
1180
+ .logo-sub {
1181
+ display: none;
1182
+ }
1183
+
1184
+ .hdr-divider {
1185
+ display: none;
1186
+ }
1187
+
1188
+ #symbolSelect {
1189
+ width: 130px;
1190
+ }
1191
+
1192
+ #timeframeSelect {
1193
+ width: 60px;
1194
+ }
1195
+
1196
+ #horizonInput {
1197
+ width: 56px;
1198
+ }
1199
+
1200
+ .btn-primary {
1201
+ padding: 0 12px;
1202
+ font-size: 0.68rem;
1203
+ letter-spacing: 0.06em;
1204
+ }
1205
+ }
1206
+ </style>
1207
+ </head>
1208
+
1209
+ <body>
1210
+ <div id="app">
1211
+
1212
+ <!-- ── HEADER ───────────────────────────────── -->
1213
+ <header class="hdr">
1214
+
1215
+ <!-- Logo -->
1216
+ <div class="logo">
1217
+ <div class="logo-mark">
1218
+ <!-- Stylised K hexagon mark -->
1219
+ <svg viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
1220
+ <polygon points="16,2 28,9 28,23 16,30 4,23 4,9" stroke="rgba(34,211,238,0.55)" stroke-width="1"
1221
+ fill="rgba(34,211,238,0.06)" />
1222
+ <polygon points="16,6 24,11 24,21 16,26 8,21 8,11" stroke="rgba(34,211,238,0.22)" stroke-width="0.5"
1223
+ fill="none" />
1224
+ <!-- AI shape -->
1225
+ <path d="M9,22 L13,10 L17,22 M10.5,18 L15.5,18" stroke="#22d3ee" stroke-width="1.8" stroke-linecap="round"
1226
+ stroke-linejoin="round" />
1227
+ <line x1="22" y1="10" x2="22" y2="22" stroke="#22d3ee" stroke-width="1.8" stroke-linecap="round" />
1228
+ <!-- corner accents -->
1229
+ <circle cx="16" cy="2" r="1" fill="rgba(34,211,238,0.5)" />
1230
+ <circle cx="28" cy="9" r="1" fill="rgba(34,211,238,0.3)" />
1231
+ <circle cx="4" cy="23" r="1" fill="rgba(34,211,238,0.3)" />
1232
+ </svg>
1233
+ </div>
1234
+ <!-- Market Status -->
1235
+ <div class="market-status-bar" id="marketStatusBar"></div>
1236
+ </div>
1237
+
1238
+ <div class="hdr-divider"></div>
1239
+
1240
+ <!-- Controls -->
1241
+ <div class="ctrls">
1242
+
1243
+ <button class="btn-icon" id="toggleMarketBtn" title="Ẩn/Hiện Thị trường">
1244
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
1245
+ <line x1="3" y1="12" x2="21" y2="12"></line>
1246
+ <line x1="3" y1="6" x2="21" y2="6"></line>
1247
+ <line x1="3" y1="18" x2="21" y2="18"></line>
1248
+ </svg>
1249
+ </button>
1250
+
1251
+ <!-- Omnibox Search -->
1252
+ <div class="omnibox">
1253
+ <i><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg></i>
1254
+ <input type="text" id="symbolSearch" placeholder="Tìm kiếm mã (VD: Gold, BTC, AAPL...)" autocomplete="off">
1255
+ <div class="search-results" id="searchResults"></div>
1256
+ </div>
1257
+
1258
+ <div class="ctrl-unit">
1259
+ <span class="ctrl-label">Khung thời gian</span>
1260
+ <select class="k-select" id="timeframeSelect">
1261
+ <option>1m</option>
1262
+ <option>5m</option>
1263
+ <option>15m</option>
1264
+ <option>1h</option>
1265
+ <option>4h</option>
1266
+ <option selected>1d</option>
1267
+ <option>1w</option>
1268
+ </select>
1269
+ </div>
1270
+
1271
+ <div class="ctrl-unit">
1272
+ <span class="ctrl-label">Dự báo (nến)</span>
1273
+ <input class="k-input" id="horizonInput" type="number" min="5" max="300" value="10" />
1274
+ </div>
1275
+
1276
+ <div class="ctrl-unit">
1277
+ <span class="ctrl-label">Chỉ báo</span>
1278
+ <select class="k-select" id="indicatorSelect" style="width: 140px;">
1279
+ <option value="none">Không có</option>
1280
+ <option value="bb">Bollinger Bands</option>
1281
+ <option value="rsi">RSI (14)</option>
1282
+ <option value="both">Cả hai</option>
1283
+ </select>
1284
+ </div>
1285
+
1286
+ <button class="btn-primary" id="refreshBtn">Phân tích</button>
1287
+
1288
+
1289
+ <button class="btn-icon" id="fitBtn" title="Khớp toàn bộ dữ liệu">
1290
+ <!-- expand icon -->
1291
+ <svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
1292
+ <path d="M1 5V1h4M9 1h4v4M13 9v4H9M5 13H1V9" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"
1293
+ stroke-linejoin="round" />
1294
+ </svg>
1295
+ </button>
1296
+
1297
+ </div>
1298
+ </header>
1299
+
1300
+ <!-- ── BODY ──────────────────────────────── -->
1301
+ <div class="app-body">
1302
+ <div class="sidebar-trigger" id="reopenSidebarBtn" title="Mở danh sách thị trường">
1303
+ <span>Thị trường</span>
1304
+ </div>
1305
+
1306
+ <!-- Side Sidebar (Market Overview) -->
1307
+ <aside class="sidebar" id="sidebar">
1308
+ <div class="sidebar-head">
1309
+ <span class="sidebar-title">Thị trường</span>
1310
+ <button class="btn-icon" id="toggleSidebarBtn" style="width: 28px; height: 28px; border: none; background: transparent;">
1311
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
1312
+ <polyline points="15 18 9 12 15 6"></polyline>
1313
+ </svg>
1314
+ </button>
1315
+ </div>
1316
+ <div class="sidebar-content">
1317
+ <div class="market-list" id="marketOverview">
1318
+ <!-- Market items will be injected here -->
1319
+ </div>
1320
+ </div>
1321
+ </aside>
1322
+
1323
+ <!-- Main Area -->
1324
+ <main class="main">
1325
+
1326
+ <!-- Status badge -->
1327
+ <div class="status-wrap">
1328
+ <div class="status-pill" id="statusPill">
1329
+ <div class="dot" id="statusDot"></div>
1330
+ <span class="status-label" id="statusText">Hệ thống sẵn sàng</span>
1331
+ </div>
1332
+ </div>
1333
+
1334
+ <!-- Terminal Loading overlay -->
1335
+ <div class="terminal-loader-overlay" id="terminalLoader">
1336
+ <div class="loader-inner">
1337
+ <div class="loader-ring"></div>
1338
+ <div class="loader-text" id="loaderText">Hệ thống đang nạp dữ liệu...</div>
1339
+ </div>
1340
+ </div>
1341
+
1342
+ <!-- Futuristic Overlays -->
1343
+ <div class="chart-bg-overlay"></div>
1344
+ <div class="chart-logo-overlay">KRONOS AI</div>
1345
+
1346
+ <!-- Chart canvas -->
1347
+ <div id="chart"></div>
1348
+
1349
+ <aside class="analysis-panel hidden" id="analysisPanel"></aside>
1350
+
1351
+ </main>
1352
+ </div>
1353
+ </div>
1354
+
1355
+ <!-- ══════════════════════════════════════════════
1356
+ JAVASCRIPT — all original logic preserved
1357
+ ══════════════════════════════════════════════ -->
1358
+ <script>
1359
+ const API_BASE = (window.location.origin && window.location.origin !== 'null')
1360
+ ? window.location.origin
1361
+ : 'http://127.0.0.1:8000';
1362
+
1363
+ /* ── DOM refs ──────────────────────────────── */
1364
+ /* ── DOM refs ──────────────────────────────── */
1365
+ const symbolSearch = document.getElementById('symbolSearch');
1366
+ const searchResults = document.getElementById('searchResults');
1367
+ const timeframeSelect = document.getElementById('timeframeSelect');
1368
+ const horizonInput = document.getElementById('horizonInput');
1369
+ const refreshBtn = document.getElementById('refreshBtn');
1370
+ const fitBtn = document.getElementById('fitBtn');
1371
+ const statusText = document.getElementById('statusText');
1372
+ const statusDot = document.getElementById('statusDot');
1373
+ const statusPill = document.getElementById('statusPill');
1374
+ const terminalLoader = document.getElementById('terminalLoader');
1375
+ const loaderText = document.getElementById('loaderText');
1376
+ const analysisPanel = document.getElementById('analysisPanel');
1377
+ const sidebar = document.getElementById('sidebar');
1378
+ const toggleSidebarBtn = document.getElementById('toggleSidebarBtn');
1379
+ const marketOverview = document.getElementById('marketOverview');
1380
+ const marketStatusBar = document.getElementById('marketStatusBar');
1381
+ const indicatorSelect = document.getElementById('indicatorSelect');
1382
+
1383
+ /* ── State ─────────────────────────────────── */
1384
+ let currentSymbol = 'XAUUSD';
1385
+ let currentInterval = '1d';
1386
+ let ws = null;
1387
+ let fetchController = null;
1388
+ const symbolMap = new Map();
1389
+ const timeframeMap = {
1390
+ '1m': '1 Phút', '5m': '5 Phút', '15m': '15 Phút',
1391
+ '1h': '1 Giờ', '4h': '4 Giờ', '1d': '1 Ngày', '1w': '1 Tuần'
1392
+ };
1393
+
1394
+ /* ── Indicator Calculation Helpers ────────── */
1395
+ /* ── WebSocket Management ────────────────────── */
1396
+ function connectWS(symbol) {
1397
+ if (ws) {
1398
+ ws.close();
1399
+ ws = null;
1400
+ }
1401
+
1402
+ const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
1403
+ const wsUrl = `${API_BASE.replace(/^https?:\/\//, wsProtocol)}/ws/price/${symbol}`;
1404
+
1405
+ console.log(`[WS] Connecting to ${wsUrl}`);
1406
+ ws = new WebSocket(wsUrl);
1407
+
1408
+ ws.onmessage = (event) => {
1409
+ const data = JSON.parse(event.data);
1410
+ if (data.error) return;
1411
+
1412
+ // Update current candle if price changed
1413
+ const lastCandle = lastCandleData;
1414
+ if (lastCandle && data.price) {
1415
+ const update = {
1416
+ time: lastCandle.time,
1417
+ open: lastCandle.open,
1418
+ high: Math.max(lastCandle.high, data.price),
1419
+ low: Math.min(lastCandle.low, data.price),
1420
+ close: data.price
1421
+ };
1422
+ candleSeries.update(update);
1423
+ lastCandleData = update;
1424
+ }
1425
+
1426
+ // Update overview items if they exist
1427
+ updateOverviewItem(data);
1428
+ };
1429
+
1430
+ ws.onclose = () => {
1431
+ console.log('[WS] Disconnected');
1432
+ // Reconnect after 5s if still active
1433
+ setTimeout(() => {
1434
+ if (currentSymbol === symbol) connectWS(symbol);
1435
+ }, 5000);
1436
+ };
1437
+ }
1438
+
1439
+ let lastCandleData = null;
1440
+
1441
+ /* ── Market Status & Overview ────────────────── */
1442
+ async function refreshMarketStatus() {
1443
+ try {
1444
+ const data = await apiRequest('/api/market-status');
1445
+ marketStatusBar.innerHTML = data.markets.map(m => `
1446
+ <div class="m-stat ${m.status}">
1447
+ <div class="pulse"></div>
1448
+ <span>${m.market}</span>
1449
+ </div>
1450
+ `).join('');
1451
+ } catch (e) {
1452
+ console.warn('Market status fetch failed', e);
1453
+ }
1454
+ }
1455
+
1456
+ async function refreshOverview() {
1457
+ try {
1458
+ const symbol = currentSymbol || 'BTCUSD';
1459
+ const data = await apiRequest(`/api/market-peers?symbol=${symbol}`);
1460
+ renderOverview(`${data.category.toUpperCase()} LIÊN QUAN`, data.peers);
1461
+ } catch (e) {
1462
+ console.warn('Overview fetch failed', e);
1463
+ }
1464
+ }
1465
+
1466
+ function renderOverview(categoryLabel, peers) {
1467
+ marketOverview.innerHTML = `
1468
+ <div style="font-size:0.65rem; color:var(--txt-muted); margin-bottom:8px; padding-left:12px;">${categoryLabel}</div>
1469
+ ${peers.map(p => `
1470
+ <div class="market-item ${p.symbol === currentSymbol ? 'active' : ''}" onclick="switchSymbol('${p.symbol}')" id="overview-${p.symbol}">
1471
+ <div class="m-sym">
1472
+ <span class="m-ticker">${p.symbol}</span>
1473
+ <span class="m-name">${p.name}</span>
1474
+ </div>
1475
+ <div class="m-price-box">
1476
+ <div class="m-price">${formatPrice(p.price)}</div>
1477
+ <div class="m-change ${p.change_24h >= 0 ? 'up' : 'down'}">${formatPct(p.change_24h)}</div>
1478
+ </div>
1479
+ </div>
1480
+ `).join('')}
1481
+ `;
1482
+ }
1483
+
1484
+ function updateOverviewItem(data) {
1485
+ const itemPrice = document.querySelector(`#overview-${data.symbol} .m-price`);
1486
+ const itemChange = document.querySelector(`#overview-${data.symbol} .m-change`);
1487
+ if (itemPrice) itemPrice.textContent = formatPrice(data.price);
1488
+ if (itemChange) {
1489
+ itemChange.textContent = formatPct(data.change_pct);
1490
+ itemChange.className = `m-change ${data.change_pct >= 0 ? 'up' : 'down'}`;
1491
+ }
1492
+ }
1493
+
1494
+ /* ── Search Logic ───────────────────────────── */
1495
+ let searchTimeout = null;
1496
+ symbolSearch.oninput = (e) => {
1497
+ clearTimeout(searchTimeout);
1498
+ const q = e.target.value.trim();
1499
+ if (!q) {
1500
+ searchResults.classList.remove('visible');
1501
+ return;
1502
+ }
1503
+ searchTimeout = setTimeout(async () => {
1504
+ try {
1505
+ const data = await apiRequest(`/api/search?q=${encodeURIComponent(q)}`);
1506
+ renderSearchResults(data.results);
1507
+ } catch (e) {
1508
+ console.error('Search failed', e);
1509
+ }
1510
+ }, 300);
1511
+ };
1512
+
1513
+ function renderSearchResults(results) {
1514
+ if (!results.length) {
1515
+ searchResults.classList.remove('visible');
1516
+ return;
1517
+ }
1518
+ searchResults.innerHTML = results.map(r => `
1519
+ <div class="search-item" onclick="switchSymbol('${r.symbol}')">
1520
+ <span class="sym">${r.symbol}</span>
1521
+ <span class="name">${r.label}</span>
1522
+ </div>
1523
+ `).join('');
1524
+ searchResults.classList.add('visible');
1525
+ }
1526
+
1527
+ /* ── Switch Logic ───────────────────────────── */
1528
+ async function switchSymbol(symbol) {
1529
+ currentSymbol = symbol;
1530
+ symbolSearch.value = symbol;
1531
+ searchResults.classList.remove('visible');
1532
+
1533
+ // Update labels immediately for better UX responsiveness
1534
+ const sLabel = symbolMap.get(symbol) || symbol;
1535
+ updateStatus(`${sLabel} · ${currentInterval} — Đang nạp...`, 'loading');
1536
+
1537
+ // Update active state in sidebar
1538
+ document.querySelectorAll('.market-item').forEach(el => el.classList.remove('active'));
1539
+ const activeItem = document.getElementById(`overview-${symbol}`);
1540
+ if (activeItem) activeItem.classList.add('active');
1541
+
1542
+ await refreshChart();
1543
+ refreshOverview();
1544
+ connectWS(symbol);
1545
+ }
1546
+
1547
+ /* ── Chart init ────────────────────────────── */
1548
+ const chartEl = document.getElementById('chart');
1549
+
1550
+ const chart = LightweightCharts.createChart(chartEl, {
1551
+ layout: {
1552
+ background: { type: 'solid', color: 'transparent' },
1553
+ textColor: 'rgba(100, 150, 200, 0.85)',
1554
+ fontSize: 11,
1555
+ fontFamily: "'Space Mono', 'Courier New', monospace",
1556
+ },
1557
+ grid: {
1558
+ vertLines: { visible: false },
1559
+ horzLines: { visible: false },
1560
+ },
1561
+ rightPriceScale: {
1562
+ borderColor: 'rgba(40, 80, 140, 0.25)',
1563
+ autoScale: true,
1564
+ scaleMargins: { top: 0.08, bottom: 0.08 },
1565
+ },
1566
+ timeScale: {
1567
+ borderColor: 'rgba(40, 80, 140, 0.25)',
1568
+ timeVisible: true,
1569
+ secondsVisible: false,
1570
+ fixLeftEdge: false,
1571
+ fixRightEdge: false,
1572
+ },
1573
+ crosshair: {
1574
+ mode: LightweightCharts.CrosshairMode.Normal,
1575
+ vertLine: {
1576
+ color: 'rgba(34, 211, 238, 0.35)',
1577
+ width: 1,
1578
+ labelBackgroundColor: '#040d1e',
1579
+ },
1580
+ horzLine: {
1581
+ color: 'rgba(34, 211, 238, 0.35)',
1582
+ width: 1,
1583
+ labelBackgroundColor: '#040d1e',
1584
+ },
1585
+ },
1586
+ watermark: {
1587
+ visible: true,
1588
+ fontSize: 64,
1589
+ horzAlign: 'center',
1590
+ vertAlign: 'center',
1591
+ color: 'rgba(34, 211, 238, 0.04)',
1592
+ text: 'Super AI Analysis',
1593
+ },
1594
+ handleScroll: true,
1595
+ handleScale: true,
1596
+ });
1597
+
1598
+ /* ── Series ────────────────────────────────── */
1599
+ const candleSeries = chart.addCandlestickSeries({
1600
+ upColor: '#1dba8a',
1601
+ downColor: '#e05560',
1602
+ borderVisible: false,
1603
+ wickUpColor: '#1dba8a',
1604
+ wickDownColor: '#e05560',
1605
+ });
1606
+
1607
+ const p50Series = chart.addLineSeries({
1608
+ color: '#22d3ee',
1609
+ lineWidth: 2,
1610
+ title: 'Dự báo AI',
1611
+ priceLineVisible: false,
1612
+ lastValueVisible: true,
1613
+ visible: false,
1614
+ });
1615
+
1616
+ const p10Series = chart.addLineSeries({
1617
+ color: 'rgba(34, 211, 238, 0.22)',
1618
+ lineWidth: 1,
1619
+ lineStyle: LightweightCharts.LineStyle.Dashed,
1620
+ priceLineVisible: false,
1621
+ lastValueVisible: false,
1622
+ visible: false,
1623
+ });
1624
+
1625
+ const p90Series = chart.addLineSeries({
1626
+ color: 'rgba(34, 211, 238, 0.22)',
1627
+ lineWidth: 1,
1628
+ lineStyle: LightweightCharts.LineStyle.Dashed,
1629
+ priceLineVisible: false,
1630
+ lastValueVisible: false,
1631
+ visible: false,
1632
+ });
1633
+
1634
+ /* ── Indicator Series ──────────────────────── */
1635
+ const bbMiddleSeries = chart.addLineSeries({ color: 'rgba(255, 255, 255, 0.2)', lineWidth: 1, priceLineVisible: false, lastValueVisible: false });
1636
+ const bbUpperSeries = chart.addLineSeries({ color: 'rgba(34, 211, 238, 0.3)', lineWidth: 1, priceLineVisible: false, lastValueVisible: false });
1637
+ const bbLowerSeries = chart.addLineSeries({ color: 'rgba(34, 211, 238, 0.3)', lineWidth: 1, priceLineVisible: false, lastValueVisible: false });
1638
+
1639
+ let rsiSeries = null;
1640
+
1641
+ /* ── Status helpers ────────────────────────── */
1642
+ /* ── UI Sync & Loader helpers ──────────────── */
1643
+ function clearAllOverlays() {
1644
+ // Unify state by hiding all primary overlays
1645
+ terminalLoader.classList.remove('active');
1646
+ analysisPanel.classList.remove('active');
1647
+ searchResults.classList.remove('visible');
1648
+ }
1649
+
1650
+ function showLoader(msg = 'Đang tải dữ liệu') {
1651
+ loaderText.textContent = msg;
1652
+ terminalLoader.classList.add('active');
1653
+ }
1654
+
1655
+ function hideLoader() {
1656
+ terminalLoader.classList.remove('active');
1657
+ }
1658
+
1659
+ function updateStatus(msg, type = '') {
1660
+ statusText.textContent = msg;
1661
+ statusDot.className = 'dot ' + type;
1662
+ statusPill.className = 'status-pill ' + type;
1663
+ }
1664
+
1665
+ function formatPrice(value) {
1666
+ if (value === null || value === undefined || Number.isNaN(Number(value))) return '--';
1667
+ return Number(value).toLocaleString('en-US', { maximumFractionDigits: 4 });
1668
+ }
1669
+
1670
+ function formatPct(value) {
1671
+ if (value === null || value === undefined || Number.isNaN(Number(value))) return '--';
1672
+ const num = Number(value);
1673
+ const sign = num > 0 ? '+' : '';
1674
+ return `${sign}${num.toFixed(2)}%`;
1675
+ }
1676
+
1677
+ function formatRR(value) {
1678
+ if (value === null || value === undefined || Number.isNaN(Number(value))) return '--';
1679
+ return `${Number(value).toFixed(2)}R`;
1680
+ }
1681
+
1682
+ /* ── State for cached analysis data ──────────── */
1683
+ let lastAnalysisPayload = null;
1684
+ let lastAnalysisSymbol = null;
1685
+ let lastAnalysisInterval = null;
1686
+
1687
+ /* ── Fullscreen Dashboard v6.0 — 3 Gauge Hero ──── */
1688
+ function renderAnalysisPanel(symbol, interval, payload) {
1689
+ // Cache the payload for toggle reuse
1690
+ if (payload?.analysis) {
1691
+ lastAnalysisPayload = payload;
1692
+ lastAnalysisSymbol = symbol;
1693
+ lastAnalysisInterval = interval;
1694
+ }
1695
+
1696
+ if (!payload?.analysis) {
1697
+ analysisPanel.innerHTML = `
1698
+ <div class="dash-loading">
1699
+ <div class="loader-ring" style="width:42px;height:42px;border-radius:50%;border:1.5px solid rgba(40,80,140,0.15);border-top-color:var(--accent);animation:spin 0.8s linear infinite;"></div>
1700
+ <p>Nhấn "Phân tích" để hiển thị bảng phân tích kỹ thuật</p>
1701
+ </div>
1702
+ `;
1703
+ return;
1704
+ }
1705
+
1706
+ const a = payload.analysis;
1707
+ if (!a.oscillators && !a.moving_averages) {
1708
+ analysisPanel.innerHTML = '<div class="dash-loading"><div class="loader-ring" style="width:42px;height:42px;border-radius:50%;border:1.5px solid rgba(40,80,140,0.15);border-top-color:var(--accent);animation:spin 0.8s linear infinite;"></div><p>Đang tính toán...</p></div>';
1709
+ return;
1710
+ }
1711
+
1712
+ const osc = a.oscillators || { sell:0, neutral:0, buy:0, signal:'--', data:[] };
1713
+ const ma = a.moving_averages || { sell:0, neutral:0, buy:0, signal:'--', data:[] };
1714
+ const summary = a.summary || { sell:0, neutral:0, buy:0, signal:'--' };
1715
+ const pivots = (a.pivot_points || {}).data || [];
1716
+
1717
+ // ── AI Forecast calculation ──
1718
+ const confidence = payload.ensemble?.confidence ?? 0;
1719
+ const forecastRows = payload.forecast || [];
1720
+ const lastClose = payload.last_close || 0;
1721
+ let forecastEnd = lastClose;
1722
+ if (forecastRows.length > 1) {
1723
+ forecastEnd = forecastRows[forecastRows.length - 1]?.p50 ?? lastClose;
1724
+ }
1725
+ const forecastPctChange = lastClose > 0 ? ((forecastEnd - lastClose) / lastClose) * 100 : 0;
1726
+
1727
+ // ── Compute AI Forecast as Buy/Sell gauge ──
1728
+ // Map forecast % change to a score: >0 = buy, <0 = sell
1729
+ // Strength determines strong/weak
1730
+ const absChange = Math.abs(forecastPctChange);
1731
+ let aiBuy = 0, aiSell = 0, aiNeutral = 0;
1732
+ if (forecastPctChange > 0.5) {
1733
+ aiBuy = absChange > 2 ? 2 : 1;
1734
+ aiNeutral = absChange > 2 ? 0 : 1;
1735
+ } else if (forecastPctChange < -0.5) {
1736
+ aiSell = absChange > 2 ? 2 : 1;
1737
+ aiNeutral = absChange > 2 ? 0 : 1;
1738
+ } else {
1739
+ aiNeutral = 1;
1740
+ }
1741
+ const aiTotal = aiBuy + aiSell + aiNeutral;
1742
+ let aiSignal = 'Trung lập';
1743
+ if (forecastPctChange > 2) aiSignal = 'Mua mạnh';
1744
+ else if (forecastPctChange > 0.5) aiSignal = 'Mua';
1745
+ else if (forecastPctChange < -2) aiSignal = 'Bán mạnh';
1746
+ else if (forecastPctChange < -0.5) aiSignal = 'Bán';
1747
+
1748
+ // ── TỔNG KẾT = (Phân tích kỹ thuật + Dự báo AI) / 2 ──
1749
+ const techTotal = (summary.buy + summary.sell + summary.neutral) || 1;
1750
+ const techScore = (summary.buy - summary.sell) / techTotal; // -1 to +1
1751
+ const aiScore = forecastPctChange > 0.5 ? Math.min(forecastPctChange / 5, 1) :
1752
+ forecastPctChange < -0.5 ? Math.max(forecastPctChange / 5, -1) : 0;
1753
+ const combinedScore = (techScore + aiScore) / 2; // -1 to +1
1754
+
1755
+ // Map combined score to buy/sell/neutral counts for gauge
1756
+ let totalBuy = 0, totalSell = 0, totalNeutral = 0;
1757
+ if (combinedScore > 0.15) { totalBuy = Math.round(Math.abs(combinedScore) * 10); totalNeutral = 10 - totalBuy; }
1758
+ else if (combinedScore < -0.15) { totalSell = Math.round(Math.abs(combinedScore) * 10); totalNeutral = 10 - totalSell; }
1759
+ else { totalNeutral = 10; }
1760
+
1761
+ let totalSignal = 'Trung lập';
1762
+ if (combinedScore > 0.4) totalSignal = 'Mua mạnh';
1763
+ else if (combinedScore > 0.15) totalSignal = 'Mua';
1764
+ else if (combinedScore < -0.4) totalSignal = 'Bán mạnh';
1765
+ else if (combinedScore < -0.15) totalSignal = 'Bán';
1766
+
1767
+ // ── Big SVG Gauge builder (Fix Clipping + Add Value) ──
1768
+ function buildBigGauge(sell, neutral, buy, signal, w=320, h=220) {
1769
+ const total = sell + neutral + buy || 1;
1770
+ const score = (buy - sell) / total;
1771
+ const displayValue = Math.round((score + 1) * 50);
1772
+ const angle = score * 135;
1773
+ const cx = w/2, cy = h * 0.52, r = (w/2) * 0.72; // Adjusted r and cy to prevent clipping
1774
+ const strokeW = 18;
1775
+ function arc(s, e, col) {
1776
+ const sa = (s-90)*Math.PI/180, ea = (e-90)*Math.PI/180;
1777
+ const x1=cx+r*Math.cos(sa), y1=cy+r*Math.sin(sa), x2=cx+r*Math.cos(ea), y2=cy+r*Math.sin(ea);
1778
+ return `<path d="M${x1},${y1} A${r},${r} 0 ${(e-s)>180?1:0} 1 ${x2},${y2}" fill="none" stroke="${col}" stroke-width="${strokeW}" stroke-linecap="round" opacity="0.8"/>`;
1779
+ }
1780
+ // Needle
1781
+ const na = (angle-90)*Math.PI/180, nl = r + 5;
1782
+ const nx = cx+nl*Math.cos(na), ny = cy+nl*Math.sin(na);
1783
+ // Needle dynamic color
1784
+ const needleColor = score > 0.3 ? '#4ade80' : score > 0.1 ? '#bbf7d0' : score < -0.3 ? '#f87171' : score < -0.1 ? '#fecaca' : '#f1f5f9';
1785
+
1786
+ return `
1787
+ <div class="gauge-hero-value-box">
1788
+ <div class="gauge-hero-value-num">${displayValue}</div>
1789
+ <div class="gauge-hero-value-label">Chỉ số</div>
1790
+ </div>
1791
+ <svg width="${w}" height="${h}" viewBox="0 0 ${w} ${h}">
1792
+ <defs>
1793
+ <filter id="needle-glow"><feGaussianBlur stdDeviation="6" result="blur"/><feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge></filter>
1794
+ </defs>
1795
+ <!-- Arc segments -->
1796
+ ${arc(-135,-81,'#ef4444')}${arc(-81,-27,'#fca5a5')}${arc(-27,27,'#f1f5f9')}${arc(27,81,'#bbf7d0')}${arc(81,135,'#22c55e')}
1797
+ <!-- Labels -->
1798
+ <text x="${cx + r*Math.cos((-135-90)*Math.PI/180)*1.35}" y="${cy + r*Math.sin((-135-90)*Math.PI/180)*1.35}" fill="rgba(255,255,255,0.3)" font-size="10" text-anchor="middle" font-family="var(--ff-display)" font-weight="900">BÁN</text>
1799
+ <text x="${cx + r*Math.cos((135-90)*Math.PI/180)*1.35}" y="${cy + r*Math.sin((135-90)*Math.PI/180)*1.35}" fill="rgba(255,255,255,0.3)" font-size="10" text-anchor="middle" font-family="var(--ff-display)" font-weight="900">MUA</text>
1800
+ <!-- Needle -->
1801
+ <line x1="${cx}" y1="${cy}" x2="${nx}" y2="${ny}" stroke="${needleColor}" stroke-width="7" stroke-linecap="round" filter="url(#needle-glow)"/>
1802
+ <circle cx="${cx}" cy="${cy}" r="14" fill="${needleColor}" />
1803
+ <circle cx="${cx}" cy="${cy}" r="7" fill="#fff" />
1804
+ </svg>
1805
+ `;
1806
+ }
1807
+
1808
+ function signalClass(signal) {
1809
+ if (signal.includes('Mua mạnh')) return 'strong-buy';
1810
+ if (signal.includes('Mua')) return 'buy';
1811
+ if (signal.includes('Bán mạnh')) return 'strong-sell';
1812
+ if (signal.includes('Bán')) return 'sell';
1813
+ return 'neutral';
1814
+ }
1815
+
1816
+ function actCls(act) {
1817
+ return act === 'Mua' ? 'dt-act-buy' : act === 'Bán' ? 'dt-act-sell' : 'dt-act-neut';
1818
+ }
1819
+
1820
+ const oscRows = osc.data.map(d => `<tr><td>${d.name}</td><td class="dt-val">${d.value !== null ? d.value : '—'}</td><td class="dt-act ${actCls(d.action)}">${d.action}</td></tr>`).join('');
1821
+ const maRows = ma.data.map(d => `<tr><td>${d.name}</td><td class="dt-val">${d.value !== null ? d.value : '—'}</td><td class="dt-act ${actCls(d.action)}">${d.action}</td></tr>`).join('');
1822
+ const pivotRows = pivots.map(p => `<tr><td>${p.level}</td><td>${p.classic ?? '—'}</td><td>${p.fibonacci ?? '—'}</td><td>${p.camarilla ?? '—'}</td><td>${p.woodie ?? '—'}</td><td>${p.dm ?? '—'}</td></tr>`).join('');
1823
+
1824
+ const sLabel = symbolMap.get(symbol) || symbol;
1825
+ const tLabel = timeframeMap[interval] || interval;
1826
+
1827
+ analysisPanel.innerHTML = `
1828
+ <!-- Dashboard Header -->
1829
+ <div class="dash-header">
1830
+ <div class="dash-header-left">
1831
+ <div class="dash-logo-dot"></div>
1832
+ <span class="dash-title">${sLabel}</span>
1833
+ <span class="dash-subtitle">· ${tLabel} · Bảng Phân tích Tổng hợp</span>
1834
+ </div>
1835
+ <div class="dash-close" id="dashCloseBtn" title="Đóng">
1836
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
1837
+ <line x1="18" y1="6" x2="6" y2="18"></line>
1838
+ <line x1="6" y1="6" x2="18" y2="18"></line>
1839
+ </svg>
1840
+ </div>
1841
+ </div>
1842
+
1843
+ <!-- Dashboard Body -->
1844
+ <div class="dash-body">
1845
+
1846
+ <!-- TOP ROW: 3 Big Gauges -->
1847
+ <div class="dash-gauges-hero">
1848
+
1849
+ <!-- Gauge 1: Phân tích kỹ thuật (was TỔNG KẾT) -->
1850
+ <div class="gauge-hero-card">
1851
+ <div class="gauge-hero-title">PHÂN TÍCH KỸ THUẬT</div>
1852
+ <div class="gauge-hero-svg-wrap">
1853
+ ${buildBigGauge(summary.sell, summary.neutral, summary.buy, summary.signal)}
1854
+ </div>
1855
+ <div class="gauge-hero-signal ${signalClass(summary.signal)}">${summary.signal}</div>
1856
+ <div class="gauge-hero-counts">
1857
+ <span><span class="ghc-label">Bán</span><span class="ghc-value">${summary.sell}</span></span>
1858
+ <span><span class="ghc-label">Trung lập</span><span class="ghc-value">${summary.neutral}</span></span>
1859
+ <span><span class="ghc-label">Mua</span><span class="ghc-value">${summary.buy}</span></span>
1860
+ </div>
1861
+ </div>
1862
+
1863
+ <!-- Gauge 2: Dự báo AI (Buy/Sell gauge) -->
1864
+ <div class="gauge-hero-card">
1865
+ <div class="gauge-hero-title">DỰ BÁO AI</div>
1866
+ <div class="gauge-hero-svg-wrap">
1867
+ ${buildBigGauge(aiSell, aiNeutral, aiBuy, aiSignal)}
1868
+ </div>
1869
+ <div class="gauge-hero-signal ${signalClass(aiSignal)}">${aiSignal}</div>
1870
+ </div>
1871
+
1872
+ <!-- Gauge 3: TỔNG KẾT = (Phân tích KT + Dự báo AI) / 2 -->
1873
+ <div class="gauge-hero-card hero-total">
1874
+ <div class="gauge-hero-title title-total">⚡ TỔNG KẾT</div>
1875
+ <div class="gauge-hero-svg-wrap">
1876
+ ${buildBigGauge(totalSell, totalNeutral, totalBuy, totalSignal)}
1877
+ </div>
1878
+ <div class="gauge-hero-signal signal-total ${signalClass(totalSignal)}">${totalSignal}</div>
1879
+ </div>
1880
+
1881
+ </div>
1882
+
1883
+ <!-- BOTTOM: Data Tables -->
1884
+ <div class="dash-tables-row">
1885
+
1886
+ <!-- Oscillators -->
1887
+ <div class="dash-col">
1888
+ <div class="dc-header">Chỉ số Dao động</div>
1889
+ <div class="dash-table-wrap">
1890
+ <table class="dt"><tbody>${oscRows}</tbody></table>
1891
+ </div>
1892
+ </div>
1893
+
1894
+ <!-- Moving Averages -->
1895
+ <div class="dash-col">
1896
+ <div class="dc-header">Trung bình trượt</div>
1897
+ <div class="dash-table-wrap">
1898
+ <table class="dt"><tbody>${maRows}</tbody></table>
1899
+ </div>
1900
+ </div>
1901
+
1902
+ <!-- Pivot Points -->
1903
+ <div class="dash-col col-pivots">
1904
+ <div class="dc-header">Điểm xoay</div>
1905
+ <div class="dash-table-wrap">
1906
+ <table class="pivot-table">
1907
+ <thead><tr><th>Mức</th><th>CL</th><th>FB</th><th>CM</th><th>WD</th><th>DM</th></tr></thead>
1908
+ <tbody>${pivotRows}</tbody>
1909
+ </table>
1910
+ </div>
1911
+ </div>
1912
+
1913
+ </div>
1914
+
1915
+ <div class="summary-disclaimer">
1916
+ <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.
1917
+ </div>
1918
+
1919
+ </div>
1920
+ `;
1921
+
1922
+ // Close logic
1923
+ const closeBtn = document.getElementById('dashCloseBtn');
1924
+ if (closeBtn) closeBtn.onclick = () => analysisPanel.classList.remove('active');
1925
+ }
1926
+
1927
+
1928
+
1929
+ /* ── Clear chart immediately on switch ─────── */
1930
+ function clearChartImmediate() {
1931
+ candleSeries.setData([]);
1932
+ p50Series.setData([]);
1933
+ p10Series.setData([]);
1934
+ p90Series.setData([]);
1935
+
1936
+ p50Series.applyOptions({ visible: false });
1937
+ p10Series.applyOptions({ visible: false });
1938
+ p90Series.applyOptions({ visible: false });
1939
+
1940
+ // Clear indicators
1941
+ bbMiddleSeries.setData([]);
1942
+ bbUpperSeries.setData([]);
1943
+ bbLowerSeries.setData([]);
1944
+ if (rsiSeries) rsiSeries.setData([]);
1945
+
1946
+ // Hide dashboard and clear cached data on symbol switch
1947
+ analysisPanel.classList.remove('active');
1948
+ lastAnalysisPayload = null;
1949
+ lastAnalysisSymbol = null;
1950
+ chart.priceScale('right').applyOptions({ autoScale: true });
1951
+
1952
+ const symbol = currentSymbol;
1953
+ const interval = timeframeSelect.value;
1954
+ if (symbol && interval) {
1955
+ fetch(`${API_BASE}/api/switch`, {
1956
+ method: 'POST',
1957
+ headers: { 'Content-Type': 'application/json' },
1958
+ body: JSON.stringify({ symbol, interval }),
1959
+ }).catch(e => console.warn('[switch] failed', e));
1960
+ }
1961
+ }
1962
+
1963
+ /* ── API helper ────────────────────────────── */
1964
+ async function apiRequest(path) {
1965
+ const sep = path.includes('?') ? '&' : '?';
1966
+ const url = `${API_BASE}${path}${sep}_t=${Date.now()}`;
1967
+
1968
+ const options = { method: 'GET' };
1969
+ if (fetchController) options.signal = fetchController.signal;
1970
+
1971
+ const resp = await fetch(url, options);
1972
+ if (!resp.ok) {
1973
+ const err = await resp.json().catch(() => ({ detail: resp.statusText }));
1974
+ throw new Error(typeof err.detail === 'object'
1975
+ ? err.detail.message || JSON.stringify(err.detail)
1976
+ : (err.detail || 'Lỗi kết nối'));
1977
+ }
1978
+ return resp.json();
1979
+ }
1980
+
1981
+ /* ── Load symbol list metadata ─────────────── */
1982
+ async function loadSymbols() {
1983
+ try {
1984
+ const data = await apiRequest('/api/symbols');
1985
+ data.symbols.forEach(s => symbolMap.set(s.symbol, s.label));
1986
+ // Default overview refresh
1987
+ refreshOverview();
1988
+ } catch (e) {
1989
+ updateStatus('Lỗi nạp metadata: ' + e.message, 'error');
1990
+ }
1991
+ }
1992
+
1993
+ /* ── Main refresh ──────────────────────────── */
1994
+ async function refreshChart() {
1995
+ const symbol = currentSymbol;
1996
+ const interval = timeframeSelect.value;
1997
+ const horizon = Math.max(5, Math.min(300, parseInt(horizonInput.value) || 10));
1998
+ if (!symbol) return;
1999
+
2000
+ // New: Abort previous requests
2001
+ if (fetchController) fetchController.abort();
2002
+ fetchController = new AbortController();
2003
+
2004
+ refreshBtn.disabled = true;
2005
+ // Don't auto-show dashboard panel on chart refresh
2006
+ updateStatus(`${symbol} · ${interval} — Đang nạp dữ liệu...`, 'loading');
2007
+
2008
+ showLoader('Đang nạp dữ liệu...');
2009
+
2010
+ /* Update chart watermark */
2011
+ const sLabel = symbolMap.get(symbol) || symbol;
2012
+ const tLabel = timeframeMap[interval] || interval;
2013
+ chart.applyOptions({
2014
+ watermark: {
2015
+ text: `${sLabel} · ${tLabel}`,
2016
+ color: 'rgba(34, 211, 238, 0.27)',
2017
+ fontSize: 72,
2018
+ },
2019
+ });
2020
+
2021
+ clearChartImmediate();
2022
+
2023
+ // Start fetching Stage 1 (Fast: OHLCV + Indicators)
2024
+ try {
2025
+ const [histData, indData] = await Promise.all([
2026
+ apiRequest(`/api/historical/${encodeURIComponent(symbol)}?interval=${interval}&limit=500`),
2027
+ apiRequest(`/api/indicators/${encodeURIComponent(symbol)}?interval=${interval}&limit=500`)
2028
+ ]);
2029
+
2030
+ if (histData.data.length > 0) {
2031
+ candleSeries.setData(histData.data);
2032
+ lastCandleData = histData.data[histData.data.length - 1];
2033
+ }
2034
+
2035
+ /* ── Indicators ── */
2036
+ const type = indicatorSelect.value;
2037
+ const indicators = indData.indicators || {};
2038
+ const series = indicators.series || {};
2039
+
2040
+ if (type === 'bb' || type === 'both') {
2041
+ if (series.bb_upper) bbUpperSeries.setData(series.bb_upper);
2042
+ if (series.bb_mid) bbMiddleSeries.setData(series.bb_mid);
2043
+ if (series.bb_lower) bbLowerSeries.setData(series.bb_lower);
2044
+ }
2045
+
2046
+ // Add RSI update if needed
2047
+
2048
+ chart.timeScale().fitContent();
2049
+ hideLoader();
2050
+ updateStatus(`${symbol} · ${interval} | Đang nạp AI...`, 'loading');
2051
+
2052
+ // Trigger Stage 2 in background (Slow: AI Forecast)
2053
+ fetchAIAnalysis(symbol, interval);
2054
+
2055
+ } catch (e) {
2056
+ if (e.name === 'AbortError') return; // Ignore canceled requests
2057
+ console.error('Stage 1 Fetch Error:', e);
2058
+ updateStatus('Lỗi nạp dữ liệu: ' + e.message, 'error');
2059
+ } finally {
2060
+ refreshBtn.disabled = false;
2061
+ hideLoader();
2062
+ }
2063
+ }
2064
+
2065
+ /* ── Stage 2: Background AI Forecast ───────────────── */
2066
+ async function fetchAIAnalysis(symbol, interval) {
2067
+ if (currentSymbol !== symbol) return; // Prevent race conditions
2068
+
2069
+ const panel = document.getElementById('analysisPanel');
2070
+ panel.innerHTML = `
2071
+ <div class="dash-loading">
2072
+ <div class="loader-ring" style="width:42px;height:42px;border-radius:50%;border:1.5px solid rgba(40,80,140,0.15);border-top-color:var(--accent);animation:spin 0.8s linear infinite;"></div>
2073
+ <p>AI đang tính toán...</p>
2074
+ </div>
2075
+ `;
2076
+
2077
+ try {
2078
+ const horizon = horizonInput.value || 24;
2079
+ const fData = await apiRequest(`/api/forecast/${encodeURIComponent(symbol)}?interval=${interval}&horizon=${horizon}`);
2080
+
2081
+ if (currentSymbol !== symbol) return; // Discard if user switched
2082
+
2083
+ renderAnalysisPanel(symbol, interval, fData);
2084
+
2085
+ let isBull = false;
2086
+ let lastForecastVal = 0;
2087
+
2088
+ if (fData.error || !fData.forecast || fData.forecast.length === 0) {
2089
+ updateStatus('AI: ' + (fData.error || 'Thiếu dữ liệu dự báo'), 'warning');
2090
+ } else {
2091
+ // Safeguard: Ensure we have candle data before aligning
2092
+ if (!lastCandleData || !lastCandleData.time) {
2093
+ updateStatus(`${symbol} · ${interval} — AI Sẵn sàng`, 'ok');
2094
+ return;
2095
+ }
2096
+
2097
+ // Alignment Fix: Prepend the current price candle to the forecast data
2098
+ const currentPoint = { time: lastCandleData.time, value: lastCandleData.close };
2099
+
2100
+ const p50 = [currentPoint, ...fData.forecast.map(d => ({ time: d.time, value: d.p50 }))];
2101
+ const p10 = [currentPoint, ...fData.forecast.map(d => ({ time: d.time, value: d.p10 }))];
2102
+ const p90 = [currentPoint, ...fData.forecast.map(d => ({ time: d.time, value: d.p90 }))];
2103
+
2104
+ const first = p50[0]?.value ?? 0;
2105
+ lastForecastVal = p50[p50.length - 1]?.value ?? 0;
2106
+ isBull = lastForecastVal >= first;
2107
+
2108
+ const aiMain = isBull ? '#1dba8a' : '#e05560';
2109
+ const aiBand = isBull ? 'rgba(29,186,138,0.1)' : 'rgba(224,85,96,0.1)';
2110
+
2111
+ p50Series.applyOptions({ color: aiMain, visible: true });
2112
+ p10Series.applyOptions({ color: aiBand, visible: true });
2113
+ p90Series.applyOptions({ color: aiBand, visible: true });
2114
+
2115
+ p50Series.setData(p50);
2116
+ p10Series.setData(p10);
2117
+ p90Series.setData(p90);
2118
+ }
2119
+
2120
+ const trend = isBull ? '↑ TĂNG' : '↓ GIẢM';
2121
+ const confidence = fData.analysis?.confidence ?? fData.ensemble?.confidence ?? 0;
2122
+ if (currentSymbol !== symbol || timeframeSelect.value !== interval) return;
2123
+
2124
+ updateStatus(
2125
+ `${symbol} · ${interval} | AI: ${trend} (${Math.round(confidence)}%) | Giá: ${formatPrice(lastCandleData?.close)}`,
2126
+ 'ok'
2127
+ );
2128
+
2129
+ } catch (e) {
2130
+ if (e.name === 'AbortError') return; // Silent abort
2131
+ console.error('Stage 2 Fetch Error:', e);
2132
+ panel.innerHTML = `<div class="dash-loading"><p style="color:#fb7185;">Không thể kết nối AI: ${e.message}</p></div>`;
2133
+ updateStatus('AI Offline', 'warning');
2134
+ }
2135
+ }
2136
+
2137
+ /* ── Event listeners ───────────────────────── */
2138
+ const reopenSidebarBtn = document.getElementById('reopenSidebarBtn');
2139
+ const toggleMarketBtn = document.getElementById('toggleMarketBtn');
2140
+
2141
+ function toggleSidebar(forceOpen = null) {
2142
+ const isCollapsed = sidebar.classList.contains('collapsed');
2143
+ const targetState = (forceOpen !== null) ? !forceOpen : !isCollapsed;
2144
+
2145
+ sidebar.classList.toggle('collapsed', targetState);
2146
+ reopenSidebarBtn.style.display = targetState ? 'flex' : 'none';
2147
+
2148
+ setTimeout(() => chart.applyOptions({ width: chartEl.clientWidth }), 300);
2149
+ }
2150
+
2151
+ // Unified toggle handlers
2152
+ toggleSidebarBtn.onclick = () => toggleSidebar(false); // Close
2153
+ reopenSidebarBtn.onclick = () => toggleSidebar(true); // Open
2154
+ toggleMarketBtn.onclick = () => toggleSidebar(); // Toggle
2155
+
2156
+ timeframeSelect.onchange = refreshChart;
2157
+ indicatorSelect.onchange = refreshChart;
2158
+
2159
+ // "Phân tích" button: toggle dashboard ON/OFF without reloading chart
2160
+ refreshBtn.onclick = () => {
2161
+ const isActive = analysisPanel.classList.contains('active');
2162
+ if (!isActive) {
2163
+ // Show dashboard — use cached data if available, or fetch fresh
2164
+ if (lastAnalysisPayload && lastAnalysisSymbol === currentSymbol) {
2165
+ renderAnalysisPanel(lastAnalysisSymbol, lastAnalysisInterval, lastAnalysisPayload);
2166
+ analysisPanel.classList.add('active');
2167
+ } else {
2168
+ // No cached data: fetch from API without reloading chart
2169
+ analysisPanel.classList.add('active');
2170
+ analysisPanel.innerHTML = `
2171
+ <div class="dash-loading">
2172
+ <div class="loader-ring" style="width:48px;height:48px;"></div>
2173
+ <p>Hệ thống AI đang khởi tạo dữ liệu...</p>
2174
+ </div>
2175
+ `;
2176
+ fetchAIAnalysis(currentSymbol, timeframeSelect.value);
2177
+ }
2178
+ } else {
2179
+ // Hide dashboard
2180
+ analysisPanel.classList.remove('active');
2181
+ }
2182
+ };
2183
+
2184
+ fitBtn.onclick = () => chart.timeScale().fitContent();
2185
+
2186
+ /* ── Click out to close search ─────────────── */
2187
+ document.addEventListener('click', (e) => {
2188
+ if (!symbolSearch.contains(e.target) && !searchResults.contains(e.target)) {
2189
+ searchResults.classList.remove('visible');
2190
+ }
2191
+ });
2192
+
2193
+ /* ── Resize handler ────────────────────────── */
2194
+ const ro = new ResizeObserver(() => {
2195
+ chart.applyOptions({
2196
+ width: chartEl.clientWidth,
2197
+ height: chartEl.clientHeight,
2198
+ });
2199
+ });
2200
+ ro.observe(chartEl);
2201
+
2202
+ /* ── Bootstrap ─────────────────────────────── */
2203
+ (async () => {
2204
+ await loadSymbols();
2205
+ await refreshMarketStatus();
2206
+
2207
+ // Start polling
2208
+ setInterval(refreshMarketStatus, 60000); // 1m
2209
+ setInterval(refreshOverview, 15000); // 15s
2210
+
2211
+ // Default start
2212
+ await switchSymbol('XAUUSD');
2213
+ })();
2214
+ </script>
2215
+ </body>
2216
+
2217
+ </html>
requirements.txt ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ fastapi==0.136.0
2
+ uvicorn[standard]==0.44.0
3
+ httpx==0.28.1
4
+ pandas==2.2.2
5
+ numpy==2.4.4
6
+ yfinance==1.3.0
7
+ ccxt==4.5.49
8
+ torch==2.11.0
9
+ aiofiles==25.1.0
10
+ pytz==2026.1.post1
11
+ einops==0.8.1
12
+ huggingface_hub==0.33.1
13
+ matplotlib==3.9.3
14
+ tqdm==4.67.1
15
+ safetensors==0.6.2
run.bat ADDED
@@ -0,0 +1,73 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @echo off
2
+ setlocal EnableExtensions EnableDelayedExpansion
3
+ cd /d "%~dp0"
4
+
5
+ title Kronos AI Analysis
6
+
7
+ set "PYTHONUTF8=1"
8
+ set "PYTHONIOENCODING=utf-8"
9
+ set "KRONOS_PRELOAD=1"
10
+ set "VENV_DIR=%CD%\venv"
11
+ set "VENV_PY=%VENV_DIR%\Scripts\python.exe"
12
+
13
+ echo ==========================================
14
+ echo KRONOS AI ANALYSIS - STARTUP
15
+ echo ==========================================
16
+ echo.
17
+
18
+ echo [1/5] Checking Python 3.11...
19
+ py -3.11 -V >nul 2>&1
20
+ if errorlevel 1 (
21
+ echo [ERROR] Python 3.11 is required but was not found.
22
+ echo [HINT] Install Python 3.11 and run this script again.
23
+ goto :end
24
+ )
25
+
26
+ echo [2/5] Checking virtual environment...
27
+ if exist "%VENV_PY%" (
28
+ "%VENV_PY%" -V >nul 2>&1
29
+ if errorlevel 1 (
30
+ echo [WARN] Existing venv is invalid. Rebuilding a clean one...
31
+ if exist "%CD%\venv_legacy_broken" rmdir /s /q "%CD%\venv_legacy_broken"
32
+ ren "%VENV_DIR%" "venv_legacy_broken"
33
+ )
34
+ )
35
+
36
+ if not exist "%VENV_PY%" (
37
+ echo [INFO] Creating fresh Python 3.11 virtual environment...
38
+ py -3.11 -m venv "%VENV_DIR%"
39
+ if errorlevel 1 (
40
+ echo [ERROR] Failed to create virtual environment.
41
+ goto :end
42
+ )
43
+ )
44
+
45
+ set "REQUESTS_CA_BUNDLE=%VENV_DIR%\Lib\site-packages\pip\_vendor\certifi\cacert.pem"
46
+
47
+ echo [3/5] Installing or verifying dependencies...
48
+ "%VENV_PY%" -m pip install -r requirements.txt
49
+ if errorlevel 1 (
50
+ echo [ERROR] Dependency installation failed.
51
+ goto :end
52
+ )
53
+
54
+ echo [4/5] Running Kronos runtime self-check...
55
+ "%VENV_PY%" -c "import backend.main as m; assert m.KRONOS_AVAILABLE, 'Kronos import failed'; print('KRONOS_PATH=', m.KRONOS_PATH); print('SYMBOLS=', len(m.SYMBOLS))"
56
+ if errorlevel 1 (
57
+ echo [ERROR] Backend self-check failed.
58
+ goto :end
59
+ )
60
+
61
+ echo [5/5] Starting backend...
62
+ echo [INFO] Opening http://127.0.0.1:8000
63
+ start "" http://127.0.0.1:8000
64
+ echo.
65
+ echo ------------------------------------------
66
+ echo KEEP THIS WINDOW OPEN WHILE USING THE APP
67
+ echo TO STOP: PRESS CTRL+C
68
+ echo ------------------------------------------
69
+ echo.
70
+ "%VENV_PY%" -m uvicorn backend.main:app --host 127.0.0.1 --port 8000 --reload
71
+
72
+ :end
73
+ pause