omniverse1 commited on
Commit
f6259db
·
verified ·
1 Parent(s): 88b0bb9

Update utils.py

Browse files
Files changed (1) hide show
  1. utils.py +356 -153
utils.py CHANGED
@@ -6,34 +6,351 @@ from datetime import datetime, timedelta
6
  import plotly.graph_objects as go
7
  from plotly.subplots import make_subplots
8
  import spaces
9
- from chronos import Chronos2Pipeline
 
 
 
 
 
10
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
  def get_indonesian_stocks():
 
12
  return {
13
- "BBCA.JK": "Bank Central Asia",
14
- "BBRI.JK": "Bank BRI",
15
- "BBNI.JK": "Bank BNI",
16
- "BMRI.JK": "Bank Mandiri",
17
- "TLKM.JK": "Telkom Indonesia",
18
- "UNVR.JK": "Unilever Indonesia",
19
- "ASII.JK": "Astra International",
20
- "INDF.JK": "Indofood Sukses Makmur",
21
- "KLBF.JK": "Kalbe Farma",
22
- "HMSP.JK": "HM Sampoerna",
23
- "GGRM.JK": "Gudang Garam",
24
- "ADRO.JK": "Adaro Energy",
25
- "PGAS.JK": "Perusahaan Gas Negara",
26
- "JSMR.JK": "Jasa Marga",
27
- "WIKA.JK": "Wijaya Karya",
28
- "PTBA.JK": "Tambang Batubara Bukit Asam",
29
- "ANTM.JK": "Aneka Tambang",
30
- "SMGR.JK": "Semen Indonesia",
31
- "INTP.JK": "Indocement Tunggal Prakasa",
32
- "ITMG.JK": "Indo Tambangraya Megah"
33
  }
34
 
35
  def calculate_technical_indicators(data):
 
36
  indicators = {}
 
37
  def calculate_rsi(prices, period=14):
38
  delta = prices.diff()
39
  gain = (delta.where(delta > 0, 0)).rolling(window=period).mean()
@@ -41,7 +358,9 @@ def calculate_technical_indicators(data):
41
  rs = gain / loss
42
  rsi = 100 - (100 / (1 + rs))
43
  return rsi
44
- indicators['rsi'] = {'current': calculate_rsi(data['Close']).iloc[-1], 'values': calculate_rsi(data['Close'])}
 
 
45
  def calculate_macd(prices, fast=12, slow=26, signal=9):
46
  exp1 = prices.ewm(span=fast).mean()
47
  exp2 = prices.ewm(span=slow).mean()
@@ -51,6 +370,7 @@ def calculate_technical_indicators(data):
51
  return macd, signal_line, histogram
52
  macd, signal_line, histogram = calculate_macd(data['Close'])
53
  indicators['macd'] = {'macd': macd.iloc[-1], 'signal': signal_line.iloc[-1], 'histogram': histogram.iloc[-1], 'signal_text': 'BUY' if histogram.iloc[-1] > 0 else 'SELL', 'macd_values': macd, 'signal_values': signal_line}
 
54
  def calculate_bollinger_bands(prices, period=20, std_dev=2):
55
  sma = prices.rolling(window=period).mean()
56
  std = prices.rolling(window=period).std()
@@ -61,21 +381,25 @@ def calculate_technical_indicators(data):
61
  current_price = data['Close'].iloc[-1]
62
  bb_position = (current_price - lower.iloc[-1]) / (upper.iloc[-1] - lower.iloc[-1])
63
  indicators['bollinger'] = {
64
- 'upper': upper.iloc[-1],
65
- 'middle': middle.iloc[-1],
66
- 'lower': lower.iloc[-1],
67
- 'upper_values': upper,
68
- 'middle_values': middle,
69
- 'lower_values': lower,
70
  'position': 'UPPER' if bb_position > 0.8 else 'LOWER' if bb_position < 0.2 else 'MIDDLE'
71
  }
72
  sma_20_series = data['Close'].rolling(20).mean()
73
  sma_50_series = data['Close'].rolling(50).mean()
74
  indicators['moving_averages'] = {'sma_20': sma_20_series.iloc[-1], 'sma_50': sma_50_series.iloc[-1], 'sma_200': data['Close'].rolling(200).mean().iloc[-1], 'ema_12': data['Close'].ewm(span=12).mean().iloc[-1], 'ema_26': data['Close'].ewm(span=26).mean().iloc[-1], 'sma_20_values': sma_20_series, 'sma_50_values': sma_50_series}
75
  indicators['volume'] = {'current': data['Volume'].iloc[-1], 'avg_20': data['Volume'].rolling(20).mean().iloc[-1], 'ratio': data['Volume'].iloc[-1] / data['Volume'].rolling(20).mean().iloc[-1]}
 
 
 
 
 
 
 
76
  return indicators
77
 
78
  def generate_trading_signals(data, indicators):
 
79
  signals = {}
80
  current_price = data['Close'].iloc[-1]
81
  buy_signals = 0
@@ -134,6 +458,7 @@ def generate_trading_signals(data, indicators):
134
  return signals
135
 
136
  def get_fundamental_data(stock):
 
137
  try:
138
  info = stock.info
139
  history = stock.history(period="1d")
@@ -143,6 +468,7 @@ def get_fundamental_data(stock):
143
  return {'name': 'N/A', 'current_price': 0, 'market_cap': 0, 'pe_ratio': 0, 'dividend_yield': 0, 'volume': 0, 'info': 'Unable to fetch fundamental data'}
144
 
145
  def format_large_number(num):
 
146
  if num >= 1e12:
147
  return f"{num/1e12:.2f}T"
148
  elif num >= 1e9:
@@ -154,132 +480,8 @@ def format_large_number(num):
154
  else:
155
  return f"{num:.2f}"
156
 
157
- @spaces.GPU(duration=120)
158
- def predict_prices(data, model=None, tokenizer=None, prediction_days=30):
159
- try:
160
- # Panggil pipeline di sini untuk memastikan instance baru tiap run (mencegah error memori/state)
161
- pipeline = Chronos2Pipeline.from_pretrained("amazon/chronos-2", device_map="auto")
162
-
163
- # Chronos-2 with Covariate: Menggunakan Close (target) dan Volume (covariate)
164
- context_df = data[['Close', 'Volume']].reset_index()
165
- context_df.columns = ['timestamp', 'target', 'volume']
166
- context_df['id'] = 'stock_price'
167
-
168
- # Fix Error: Could not infer frequency & FIX VOLUME COVARIATE IMPUTATION
169
- context_df['timestamp'] = pd.to_datetime(context_df['timestamp'])
170
- context_df = context_df.set_index('timestamp').asfreq('D')
171
-
172
- # IMPUTATION FIX: Target ffill, Covariate (Volume) fillna(0)
173
- context_df['target'] = context_df['target'].fillna(method='ffill')
174
- context_df['volume'] = context_df['volume'].fillna(0)
175
-
176
- context_df = context_df.reset_index()
177
-
178
- # Pastikan kolom sesuai urutan Chronos-2: timestamp, target, covariate(s), id
179
- context_df['id'] = 'stock_price'
180
- context_df = context_df[['timestamp', 'target', 'volume', 'id']]
181
-
182
- with torch.no_grad():
183
- pred_df = pipeline.predict_df(
184
- context_df,
185
- prediction_length=prediction_days,
186
- id_column="id",
187
- timestamp_column="timestamp",
188
- target="target",
189
- quantile_levels=[0.1, 0.5, 0.9]
190
- )
191
-
192
- # --- FIX UTAMA: Pengecekan kolom hasil prediksi yang lebih ketat ---
193
- required_cols = ['target_0.1', 'target_0.5', 'target_0.9']
194
- if pred_df.empty or not all(col in pred_df.columns for col in required_cols):
195
- # Jika gagal, pastikan kita tahu errornya dan melempar Runtime yang akan ditangkap di luar
196
- missing = [col for col in required_cols if col not in pred_df.columns]
197
- raise RuntimeError(f"Prediction failed. Result DataFrame is empty or incomplete. Missing: {missing}")
198
- # ------------------------------------------------------------------
199
-
200
- # Ekstraksi hasil prediksi kuantil
201
- q05_forecast = pred_df['target_0.5'].values.astype(np.float32)
202
- q09_forecast = pred_df['target_0.9'].values.astype(np.float32)
203
- q01_forecast = pred_df['target_0.1'].values.astype(np.float32)
204
- predicted_dates = pred_df['timestamp']
205
-
206
- last_price = data['Close'].iloc[-1]
207
-
208
- predicted_high = float(np.max(q05_forecast))
209
- predicted_low = float(np.min(q05_forecast))
210
- predicted_mean = float(np.mean(q05_forecast))
211
- change_pct = ((predicted_mean - last_price) / last_price) * 100 if last_price != 0 else 0
212
-
213
- return {
214
- 'values': q05_forecast,
215
- 'dates': predicted_dates,
216
- 'high_30d': predicted_high,
217
- 'low_30d': predicted_low,
218
- 'mean_30d': predicted_mean,
219
- 'change_pct': change_pct,
220
- 'q01': q01_forecast,
221
- 'q09': q09_forecast,
222
- 'summary': f"AI Model: Amazon Chronos-2 (Volume Covariate)\nPredicted High: {predicted_high:.2f}\nPredicted Low: {predicted_low:.2f}\nExpected Change: {change_pct:.2f}%"
223
- }
224
-
225
- except Exception as e:
226
- error_message = f'Model prediction failed: {e}'
227
- print(f"Error in prediction: {e}")
228
- # Mengembalikan objek error yang valid
229
- return {'values': [], 'dates': [], 'high_30d': 0, 'low_30d': 0, 'mean_30d': 0, 'change_pct': 0, 'summary': error_message, 'q01': [], 'q09': []}
230
-
231
- def create_prediction_chart(data, predictions):
232
- if not len(predictions['values']) or not len(predictions['q01']):
233
- return go.Figure().update_layout(title="Prediction Failed: No Data Available")
234
-
235
- fig = go.Figure()
236
-
237
- # Historical Price: Menggunakan seluruh data historis
238
- fig.add_trace(go.Scatter(x=data.index, y=data['Close'].values, name='Historical Price', line=dict(color='blue', width=2)))
239
-
240
- # Prediction Interval (Band): Menggunakan Q0.1 dan Q0.9
241
- fig.add_trace(go.Scatter(
242
- x=predictions['dates'],
243
- y=predictions['q09'],
244
- name='90% Upper Bound',
245
- line=dict(color='lightcoral', width=0)
246
- ))
247
-
248
- fig.add_trace(go.Scatter(
249
- x=predictions['dates'],
250
- y=predictions['q01'],
251
- name='90% Confidence Band',
252
- line=dict(color='lightcoral', width=0),
253
- fill='tonexty',
254
- fillcolor='rgba(255,182,193,0.3)'
255
- ))
256
-
257
- # Median Forecast (Q0.5) - Garis Utama Prediksi
258
- fig.add_trace(go.Scatter(x=predictions['dates'], y=predictions['values'], name='Median Forecast (Q0.5)', line=dict(color='red', width=3, dash='solid')))
259
-
260
- fig.update_layout(
261
- title=f'Probabilistic Price Forecast - Next {len(predictions["dates"])} Days (Chronos-2)',
262
- xaxis_title='Date',
263
- yaxis_title='Price (IDR)',
264
- hovermode='x unified',
265
- height=600,
266
- legend=dict(yanchor="top", y=0.99, xanchor="left", x=0.01)
267
- )
268
-
269
- # Menandai titik harga terakhir
270
- last_hist_date = data.index[-1]
271
- last_hist_price = data['Close'].iloc[-1]
272
- fig.add_trace(go.Scatter(
273
- x=[last_hist_date],
274
- y=[last_hist_price],
275
- mode='markers',
276
- marker=dict(size=10, color='blue', symbol='circle'),
277
- name='Last Known Price'
278
- ))
279
-
280
- return fig
281
-
282
  def create_price_chart(data, indicators):
 
283
  fig = make_subplots(rows=3, cols=1, shared_xaxes=True, vertical_spacing=0.05)
284
  fig.add_trace(go.Candlestick(x=data.index, open=data['Open'], high=data['High'], low=data['Low'], close=data['Close'], name='Price'), row=1, col=1)
285
  fig.add_trace(go.Scatter(x=data.index, y=indicators['moving_averages']['sma_20_values'], name='SMA 20', line=dict(color='orange')), row=1, col=1)
@@ -291,6 +493,7 @@ def create_price_chart(data, indicators):
291
  return fig
292
 
293
  def create_technical_chart(data, indicators):
 
294
  fig = make_subplots(rows=2, cols=2, subplot_titles=('Bollinger Bands', 'Volume', 'Price vs MA', 'RSI Analysis'))
295
  fig.add_trace(go.Scatter(x=data.index, y=data['Close'], name='Price', line=dict(color='black')), row=1, col=1)
296
  fig.add_trace(go.Scatter(x=data.index, y=indicators['bollinger']['upper_values'], name='Upper Band', line=dict(color='red')), row=1, col=1)
 
6
  import plotly.graph_objects as go
7
  from plotly.subplots import make_subplots
8
  import spaces
9
+ import gc
10
+ import time
11
+ import random
12
+ from chronos import ChronosPipeline # Menggunakan ChronosPipeline untuk Chronos-2
13
+ from scipy.stats import skew, kurtosis
14
+ from typing import Dict, Union, List
15
 
16
+ # Global variable for model pipeline
17
+ pipeline = None
18
+
19
+ # --- ADVANCED UTILITIES & CONFIG ---
20
+
21
+ # Sumber data Covariate eksternal
22
+ COVARIATE_SOURCES = {
23
+ 'market_indices': ['^GSPC', '^DJI', '^IXIC', '^VIX'],
24
+ 'commodities': ['GC=F', 'CL=F'],
25
+ }
26
+
27
+ def clear_gpu_memory():
28
+ """Membersihkan cache memori GPU"""
29
+ if torch.cuda.is_available():
30
+ torch.cuda.empty_cache()
31
+ gc.collect()
32
+
33
+ @spaces.GPU()
34
+ def load_pipeline():
35
+ """
36
+ Memuat model Chronos-2 dengan konfigurasi GPU canggih.
37
+ Menggunakan device_map="cuda" dan torch_dtype=torch.float16.
38
+ """
39
+ global pipeline
40
+ try:
41
+ model_name = "amazon/chronos-2"
42
+
43
+ if pipeline is None:
44
+ clear_gpu_memory()
45
+ print(f"Loading Chronos model: {model_name}...")
46
+
47
+ # PENTING: Optimasi untuk Chronos-2
48
+ pipeline = ChronosPipeline.from_pretrained(
49
+ model_name,
50
+ device_map="cuda",
51
+ torch_dtype=torch.float16,
52
+ low_cpu_mem_usage=True,
53
+ trust_remote_code=True,
54
+ use_safetensors=True
55
+ )
56
+ pipeline.model = pipeline.model.eval()
57
+ for param in pipeline.model.parameters():
58
+ param.requires_grad = False
59
+ print(f"Chronos model {model_name} loaded successfully on CUDA")
60
+
61
+ return pipeline
62
+
63
+ except Exception as e:
64
+ print(f"Error loading pipeline on CUDA, trying CPU: {str(e)}")
65
+ try:
66
+ # Fallback ke CPU
67
+ pipeline = ChronosPipeline.from_pretrained(model_name, device_map="cpu")
68
+ pipeline.model = pipeline.model.eval()
69
+ print(f"Chronos model {model_name} loaded successfully on CPU (performance degraded)")
70
+ return pipeline
71
+ except Exception as cpu_e:
72
+ raise RuntimeError(f"Failed to load model {model_name} on both CUDA and CPU: {str(cpu_e)}")
73
+
74
+ def retry_yfinance_request(func, max_retries=3, initial_delay=1):
75
+ """Mekanisme retry untuk permintaan yfinance dengan backoff eksponensial."""
76
+ for attempt in range(max_retries):
77
+ try:
78
+ result = func()
79
+ if result is not None and not result.empty:
80
+ return result
81
+ if attempt == max_retries - 1:
82
+ return None
83
+
84
+ delay = min(8.0, initial_delay * (2 ** attempt) + random.uniform(0, 1))
85
+ time.sleep(delay)
86
+ except Exception:
87
+ if attempt == max_retries - 1:
88
+ return None
89
+ delay = min(8.0, initial_delay * (2 ** attempt) + random.uniform(0, 1))
90
+ time.sleep(delay)
91
+
92
+ def fetch_enhanced_covariates(data: pd.DataFrame) -> pd.DataFrame:
93
+ """Mengambil data covariate (Indeks Pasar) dan menggabungkannya."""
94
+ start_date = data.index.min().strftime('%Y-%m-%d')
95
+ end_date = data.index.max().strftime('%Y-%m-%d')
96
+ date_range = pd.date_range(start=start_date, end=end_date, freq='D')
97
+
98
+ # 1. Reindex data asli ke range hari yang kontinu
99
+ data_full = data.reindex(date_range)
100
+ data_full['Close'] = data_full['Close'].fillna(method='ffill')
101
+ data_full['Volume'] = data_full['Volume'].fillna(0)
102
+
103
+ covariate_df = pd.DataFrame(index=date_range)
104
+
105
+ # 2. Ambil data dari semua sumber covariate eksternal
106
+ for source_key, symbols in COVARIATE_SOURCES.items():
107
+ for symbol in symbols:
108
+ def fetch_covariate():
109
+ return yf.download(symbol, start=start_date, end=end_date, interval="1d", progress=False)
110
+
111
+ cov_data = retry_yfinance_request(fetch_covariate)
112
+
113
+ if cov_data is not None and not cov_data.empty:
114
+ cov_data = cov_data['Close'].rename(f'cov_{symbol.replace("^", "_").replace("=", "_")}')
115
+ cov_data = cov_data.reindex(date_range)
116
+ covariate_df = covariate_df.merge(cov_data, left_index=True, right_index=True, how='left')
117
+
118
+ # 3. Gabungkan dan imputasi
119
+ final_df = data_full.merge(covariate_df, left_index=True, right_index=True, how='left')
120
+ cov_cols = [col for col in final_df.columns if col.startswith('cov_') or col == 'Volume']
121
+
122
+ # Imputasi Covariates: Forward fill untuk harga/indeks, 0 untuk Volume
123
+ final_df['Volume'] = final_df['Volume'].fillna(0)
124
+ final_df[[col for col in cov_cols if col != 'Volume']] = final_df[[col for col in cov_cols if col != 'Volume']].fillna(method='ffill')
125
+
126
+ final_df = final_df.dropna(subset=['Close'], how='all')
127
+
128
+ # Ganti nama kolom sesuai format Chronos
129
+ return final_df.rename(columns={'Close': 'target', 'Volume': 'cov_volume'})
130
+
131
+ def calculate_advanced_risk_metrics(df: pd.DataFrame, risk_free_rate: float = 0.05) -> Dict[str, Union[float, str]]:
132
+ """Menghitung metrik risiko dan performa lanjutan (Sharpe Ratio, VaR, CVaR, Max Drawdown)."""
133
+ if df.empty or 'Close' not in df.columns:
134
+ return {"error": "Data historis tidak valid untuk perhitungan risiko."}
135
+
136
+ try:
137
+ df['Returns'] = df['Close'].pct_change()
138
+ returns = df['Returns'].dropna()
139
+
140
+ if returns.empty:
141
+ return {"error": "Return historis tidak tersedia."}
142
+
143
+ days_per_year = 252
144
+
145
+ annual_return = returns.mean() * days_per_year
146
+ annual_vol = returns.std() * np.sqrt(days_per_year)
147
+
148
+ sharpe_ratio = (annual_return - risk_free_rate) / annual_vol if annual_vol != 0 else 0
149
+
150
+ var_95 = np.percentile(returns, 5) * -1
151
+ cvar_95 = returns[returns < -var_95].mean() * -1
152
+
153
+ cumulative_returns = (1 + returns).cumprod()
154
+ peak = cumulative_returns.expanding(min_periods=1).max()
155
+ drawdown = (cumulative_returns / peak) - 1
156
+ max_drawdown = drawdown.min()
157
+
158
+ skewness = skew(returns)
159
+ kurtosis_val = kurtosis(returns)
160
+
161
+ return {
162
+ "Annual_Return": f"{annual_return*100:.2f}%",
163
+ "Annual_Volatility": f"{annual_vol*100:.2f}%",
164
+ "Sharpe_Ratio": f"{sharpe_ratio:.2f}",
165
+ "Max_Drawdown": f"{max_drawdown*100:.2f}%",
166
+ "VaR_95_Daily_Loss": f"{var_95*100:.2f}%",
167
+ "CVaR_95_Avg_Loss": f"{cvar_95*100:.2f}%",
168
+ "Skewness": f"{skewness:.2f}",
169
+ "Kurtosis": f"{kurtosis_val:.2f}",
170
+ }
171
+
172
+ except Exception as e:
173
+ return {"error": f"Risk calculation failed: {str(e)}"}
174
+
175
+ def predict_technical_indicators_future(data: pd.DataFrame, price_prediction: np.ndarray) -> Dict[str, np.ndarray]:
176
+ """Memprediksi MACD dan Bollinger Bands di masa depan berdasarkan prediksi harga."""
177
+ predictions = {}
178
+
179
+ full_price_series = np.concatenate([data['Close'].values, price_prediction])
180
+ full_price_series = pd.Series(full_price_series)
181
+
182
+ # MACD dan Signal Line Future
183
+ def calculate_ema(prices, span):
184
+ return prices.ewm(span=span, adjust=False).mean()
185
+
186
+ ema_12_full = calculate_ema(full_price_series, 12)
187
+ ema_26_full = calculate_ema(full_price_series, 26)
188
+ macd_full = ema_12_full - ema_26_full
189
+ macd_signal_full = calculate_ema(macd_full, 9)
190
+
191
+ predictions['MACD_Future'] = macd_full.iloc[-len(price_prediction):].values
192
+ predictions['MACD_Signal_Future'] = macd_signal_full.iloc[-len(price_prediction):].values
193
+
194
+ # Bollinger Bands Future
195
+ period = 20
196
+ std_dev = 2
197
+ middle_band_full = full_price_series.rolling(window=period).mean()
198
+ std_full = full_price_series.rolling(window=period).std()
199
+ upper_band_full = middle_band_full + (std_full * std_dev)
200
+ lower_band_full = middle_band_full - (std_full * std_dev)
201
+
202
+ predictions['BB_Upper_Future'] = upper_band_full.iloc[-len(price_prediction):].values
203
+ predictions['BB_Lower_Future'] = lower_band_full.iloc[-len(price_prediction):].values
204
+
205
+ return predictions
206
+
207
+
208
+ @spaces.GPU(duration=120)
209
+ def predict_prices(data, prediction_days=30):
210
+ """Fungsi prediksi utama menggunakan Chronos-2 dengan enhanced covariates."""
211
+ try:
212
+ # 1. Load Model
213
+ pipeline = load_pipeline()
214
+
215
+ data_original = data.copy()
216
+
217
+ # 2. Enhanced Data Preprocessing & Covariate
218
+ data_enhanced = fetch_enhanced_covariates(data_original)
219
+
220
+ context_df = data_enhanced.reset_index()
221
+ context_df.columns = ['timestamp'] + [col for col in context_df.columns[1:]]
222
+ context_df['id'] = 'stock_price'
223
+
224
+ all_covariates = [col for col in context_df.columns if col not in ['timestamp', 'id', 'target']]
225
+
226
+ # 3. Model Prediction
227
+ with torch.no_grad():
228
+ pred_df = pipeline.predict_df(
229
+ context_df,
230
+ prediction_length=prediction_days,
231
+ id_column="id",
232
+ timestamp_column="timestamp",
233
+ target="target",
234
+ covariates=all_covariates,
235
+ quantile_levels=[0.1, 0.5, 0.9]
236
+ )
237
+
238
+ required_cols = ['target_0.1', 'target_0.5', 'target_0.9']
239
+ if pred_df.empty or not all(col in pred_df.columns for col in required_cols):
240
+ missing = [col for col in required_cols if col not in pred_df.columns]
241
+ raise RuntimeError(f"Prediction output incomplete. Missing: {missing}")
242
+
243
+ q05_forecast = pred_df['target_0.5'].values.astype(np.float32)
244
+ q09_forecast = pred_df['target_0.9'].values.astype(np.float32)
245
+ q01_forecast = pred_df['target_0.1'].values.astype(np.float32)
246
+ predicted_dates = pred_df['timestamp']
247
+
248
+ last_price = data_original['Close'].iloc[-1]
249
+
250
+ # Proyeksi Indikator Teknikal Masa Depan
251
+ future_indicators = predict_technical_indicators_future(data_original, q05_forecast)
252
+
253
+ predicted_high = float(np.max(q05_forecast))
254
+ predicted_low = float(np.min(q05_forecast))
255
+ predicted_mean = float(np.mean(q05_forecast))
256
+ change_pct = ((predicted_mean - last_price) / last_price) * 100 if last_price != 0 else 0
257
+
258
+ # Menambahkan data teknikal prediksi ke hasil
259
+ return {
260
+ 'values': q05_forecast,
261
+ 'dates': predicted_dates,
262
+ 'high_30d': predicted_high,
263
+ 'low_30d': predicted_low,
264
+ 'mean_30d': predicted_mean,
265
+ 'change_pct': change_pct,
266
+ 'q01': q01_forecast,
267
+ 'q09': q09_forecast,
268
+ 'future_macd': future_indicators.get('MACD_Future', []),
269
+ 'future_macd_signal': future_indicators.get('MACD_Signal_Future', []),
270
+ 'future_bb_upper': future_indicators.get('BB_Upper_Future', []),
271
+ 'future_bb_lower': future_indicators.get('BB_Lower_Future', []),
272
+ 'summary': f"AI Model: Amazon Chronos-2 (Enhanced Covariates: {len(all_covariates)} features)\nExpected High: {predicted_high:.2f}\nExpected Low: {predicted_low:.2f}\nExpected Change: {change_pct:.2f}%"
273
+ }
274
+
275
+ except Exception as e:
276
+ error_message = f'Model prediction failed: {e}'
277
+ print(f"Error in prediction: {e}")
278
+ return {'values': [], 'dates': [], 'high_30d': 0, 'low_30d': 0, 'mean_30d': 0, 'change_pct': 0, 'summary': error_message, 'q01': [], 'q09': [], 'future_macd': [], 'future_macd_signal': [], 'future_bb_upper': [], 'future_bb_lower': []}
279
+
280
+ # Memperbarui fungsi create_prediction_chart untuk menampilkan Quantile Bands (q01, q09) dan Future BB
281
+ def create_prediction_chart(data, predictions):
282
+ if not len(predictions['values']) or not len(predictions['q01']):
283
+ return go.Figure().update_layout(title="Prediction Failed: No Data Available")
284
+
285
+ fig = make_subplots(rows=2, cols=1, shared_xaxes=True, vertical_spacing=0.05,
286
+ row_heights=[0.7, 0.3], subplot_titles=('Price Forecast & Confidence Band', 'MACD Forecast'))
287
+
288
+ # 1. Price Forecast (Row 1)
289
+ fig.add_trace(go.Scatter(x=data.index, y=data['Close'].values, name='Historical Price', line=dict(color='blue', width=2)), row=1, col=1)
290
+
291
+ # Upper/Lower Quantile Band (Confidence)
292
+ fig.add_trace(go.Scatter(x=predictions['dates'], y=predictions['q09'], name='90% Upper Bound (Q0.9)', line=dict(color='lightcoral', width=0)), row=1, col=1)
293
+
294
+ fig.add_trace(go.Scatter(
295
+ x=predictions['dates'], y=predictions['q01'], name='90% Confidence Band',
296
+ line=dict(color='lightcoral', width=0), fill='tonexty', fillcolor='rgba(255,182,193,0.3)'
297
+ ), row=1, col=1)
298
+
299
+ fig.add_trace(go.Scatter(x=predictions['dates'], y=predictions['values'], name='Median Forecast (Q0.5)', line=dict(color='red', width=3, dash='solid')), row=1, col=1)
300
+
301
+ # Future Bollinger Bands
302
+ if len(predictions['future_bb_upper']) == len(predictions['dates']):
303
+ fig.add_trace(go.Scatter(x=predictions['dates'], y=predictions['future_bb_upper'], name='BB Upper (Future)', line=dict(color='green', width=1, dash='dot')), row=1, col=1)
304
+ fig.add_trace(go.Scatter(x=predictions['dates'], y=predictions['future_bb_lower'], name='BB Lower (Future)', line=dict(color='green', width=1, dash='dot')), row=1, col=1)
305
+
306
+ last_hist_date = data.index[-1]
307
+ last_hist_price = data['Close'].iloc[-1]
308
+ fig.add_trace(go.Scatter(x=[last_hist_date], y=[last_hist_price], mode='markers', marker=dict(size=10, color='blue', symbol='circle'), name='Last Known Price'), row=1, col=1)
309
+
310
+ # 2. MACD Forecast (Row 2)
311
+ if len(predictions['future_macd']) == len(predictions['dates']):
312
+
313
+ macd_hist = data['Close'].ewm(span=12).mean() - data['Close'].ewm(span=26).mean()
314
+ macd_signal_hist = macd_hist.ewm(span=9).mean()
315
+
316
+ macd_full = np.concatenate([macd_hist.iloc[-60:].values, predictions['future_macd']])
317
+ macd_signal_full = np.concatenate([macd_signal_hist.iloc[-60:].values, predictions['future_macd_signal']])
318
+ macd_dates_full = pd.to_datetime(np.concatenate([data.index[-60:].values, predictions['dates']]))
319
+
320
+ fig.add_trace(go.Scatter(x=macd_dates_full, y=macd_full, name='MACD Line', line=dict(color='blue', width=2)), row=2, col=1)
321
+ fig.add_trace(go.Scatter(x=macd_dates_full, y=macd_signal_full, name='Signal Line', line=dict(color='red', width=1)), row=2, col=1)
322
+
323
+ fig.add_vline(x=data.index[-1], line_width=1, line_dash="dash", line_color="gray", row=2, col=1)
324
+ fig.add_vline(x=data.index[-1], line_width=1, line_dash="dash", line_color="gray", row=1, col=1)
325
+
326
+ fig.update_layout(
327
+ title=f'Advanced Price & Technical Forecast - Next {len(predictions["dates"])} Days (Chronos-2)',
328
+ xaxis_title='Date', yaxis_title='Price (IDR)', hovermode='x unified', height=900,
329
+ legend=dict(yanchor="top", y=0.99, xanchor="left", x=0.01)
330
+ )
331
+
332
+ fig.update_yaxes(title_text="Price (IDR)", row=1, col=1)
333
+ fig.update_yaxes(title_text="MACD Value", row=2, col=1)
334
+
335
+ return fig
336
+
337
+ # --- Fungsi lama yang harus tetap ada ---
338
  def get_indonesian_stocks():
339
+ # ... (kode yang sama)
340
  return {
341
+ "BBCA.JK": "Bank Central Asia", "BBRI.JK": "Bank BRI", "BBNI.JK": "Bank BNI",
342
+ "BMRI.JK": "Bank Mandiri", "TLKM.JK": "Telkom Indonesia", "UNVR.JK": "Unilever Indonesia",
343
+ "ASII.JK": "Astra International", "INDF.JK": "Indofood Sukses Makmur", "KLBF.JK": "Kalbe Farma",
344
+ "HMSP.JK": "HM Sampoerna", "GGRM.JK": "Gudang Garam", "ADRO.JK": "Adaro Energy",
345
+ "PGAS.JK": "Perusahaan Gas Negara", "JSMR.JK": "Jasa Marga", "WIKA.JK": "Wijaya Karya",
346
+ "PTBA.JK": "Tambang Batubara Bukit Asam", "ANTM.JK": "Aneka Tambang", "SMGR.JK": "Semen Indonesia",
347
+ "INTP.JK": "Indocement Tunggal Prakasa", "ITMG.JK": "Indo Tambangraya Megah"
 
 
 
 
 
 
 
 
 
 
 
 
 
348
  }
349
 
350
  def calculate_technical_indicators(data):
351
+ # Disesuaikan agar dapat menambahkan RSI, MACD, Signal ke DataFrame
352
  indicators = {}
353
+
354
  def calculate_rsi(prices, period=14):
355
  delta = prices.diff()
356
  gain = (delta.where(delta > 0, 0)).rolling(window=period).mean()
 
358
  rs = gain / loss
359
  rsi = 100 - (100 / (1 + rs))
360
  return rsi
361
+ rsi_series = calculate_rsi(data['Close'])
362
+ indicators['rsi'] = {'current': rsi_series.iloc[-1], 'values': rsi_series}
363
+
364
  def calculate_macd(prices, fast=12, slow=26, signal=9):
365
  exp1 = prices.ewm(span=fast).mean()
366
  exp2 = prices.ewm(span=slow).mean()
 
370
  return macd, signal_line, histogram
371
  macd, signal_line, histogram = calculate_macd(data['Close'])
372
  indicators['macd'] = {'macd': macd.iloc[-1], 'signal': signal_line.iloc[-1], 'histogram': histogram.iloc[-1], 'signal_text': 'BUY' if histogram.iloc[-1] > 0 else 'SELL', 'macd_values': macd, 'signal_values': signal_line}
373
+
374
  def calculate_bollinger_bands(prices, period=20, std_dev=2):
375
  sma = prices.rolling(window=period).mean()
376
  std = prices.rolling(window=period).std()
 
381
  current_price = data['Close'].iloc[-1]
382
  bb_position = (current_price - lower.iloc[-1]) / (upper.iloc[-1] - lower.iloc[-1])
383
  indicators['bollinger'] = {
384
+ 'upper': upper.iloc[-1], 'middle': middle.iloc[-1], 'lower': lower.iloc[-1],
385
+ 'upper_values': upper, 'middle_values': middle, 'lower_values': lower,
 
 
 
 
386
  'position': 'UPPER' if bb_position > 0.8 else 'LOWER' if bb_position < 0.2 else 'MIDDLE'
387
  }
388
  sma_20_series = data['Close'].rolling(20).mean()
389
  sma_50_series = data['Close'].rolling(50).mean()
390
  indicators['moving_averages'] = {'sma_20': sma_20_series.iloc[-1], 'sma_50': sma_50_series.iloc[-1], 'sma_200': data['Close'].rolling(200).mean().iloc[-1], 'ema_12': data['Close'].ewm(span=12).mean().iloc[-1], 'ema_26': data['Close'].ewm(span=26).mean().iloc[-1], 'sma_20_values': sma_20_series, 'sma_50_values': sma_50_series}
391
  indicators['volume'] = {'current': data['Volume'].iloc[-1], 'avg_20': data['Volume'].rolling(20).mean().iloc[-1], 'ratio': data['Volume'].iloc[-1] / data['Volume'].rolling(20).mean().iloc[-1]}
392
+
393
+ # Tambahkan kolom indikator ke DataFrame input untuk digunakan nanti (di predict_technical_indicators_future)
394
+ # Catatan: Perubahan ini memodifikasi 'data' in-place.
395
+ data['RSI'] = rsi_series
396
+ data['MACD'] = macd
397
+ data['MACD_Signal'] = signal_line
398
+
399
  return indicators
400
 
401
  def generate_trading_signals(data, indicators):
402
+ # ... (kode yang sama)
403
  signals = {}
404
  current_price = data['Close'].iloc[-1]
405
  buy_signals = 0
 
458
  return signals
459
 
460
  def get_fundamental_data(stock):
461
+ # ... (kode yang sama)
462
  try:
463
  info = stock.info
464
  history = stock.history(period="1d")
 
468
  return {'name': 'N/A', 'current_price': 0, 'market_cap': 0, 'pe_ratio': 0, 'dividend_yield': 0, 'volume': 0, 'info': 'Unable to fetch fundamental data'}
469
 
470
  def format_large_number(num):
471
+ # ... (kode yang sama)
472
  if num >= 1e12:
473
  return f"{num/1e12:.2f}T"
474
  elif num >= 1e9:
 
480
  else:
481
  return f"{num:.2f}"
482
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
483
  def create_price_chart(data, indicators):
484
+ # ... (kode yang sama)
485
  fig = make_subplots(rows=3, cols=1, shared_xaxes=True, vertical_spacing=0.05)
486
  fig.add_trace(go.Candlestick(x=data.index, open=data['Open'], high=data['High'], low=data['Low'], close=data['Close'], name='Price'), row=1, col=1)
487
  fig.add_trace(go.Scatter(x=data.index, y=indicators['moving_averages']['sma_20_values'], name='SMA 20', line=dict(color='orange')), row=1, col=1)
 
493
  return fig
494
 
495
  def create_technical_chart(data, indicators):
496
+ # ... (kode yang sama)
497
  fig = make_subplots(rows=2, cols=2, subplot_titles=('Bollinger Bands', 'Volume', 'Price vs MA', 'RSI Analysis'))
498
  fig.add_trace(go.Scatter(x=data.index, y=data['Close'], name='Price', line=dict(color='black')), row=1, col=1)
499
  fig.add_trace(go.Scatter(x=data.index, y=indicators['bollinger']['upper_values'], name='Upper Band', line=dict(color='red')), row=1, col=1)