Spaces:
Running
Running
Update utils.py
Browse files
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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
def get_indonesian_stocks():
|
|
|
|
| 12 |
return {
|
| 13 |
-
"BBCA.JK": "Bank Central Asia",
|
| 14 |
-
"
|
| 15 |
-
"
|
| 16 |
-
"
|
| 17 |
-
"
|
| 18 |
-
"
|
| 19 |
-
"
|
| 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 |
-
|
|
|
|
|
|
|
| 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 |
-
'
|
| 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)
|