Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
# app.py β DataSynthis_ML_JobTask (ARIMA +
|
| 2 |
|
| 3 |
import os, json, math, pickle, traceback
|
| 4 |
from datetime import datetime, timedelta
|
|
@@ -16,10 +16,15 @@ from sklearn.metrics import mean_squared_error, mean_absolute_error, mean_absolu
|
|
| 16 |
# -------------------------
|
| 17 |
# Artifact Paths
|
| 18 |
# -------------------------
|
| 19 |
-
ARIMA_ORDER_JSON = "arima_order.json"
|
| 20 |
-
ARIMA_PICKLE = "arima_model.pkl"
|
| 21 |
-
|
| 22 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
|
| 24 |
# -------------------------
|
| 25 |
# Globals (lazy-load LSTM)
|
|
@@ -47,19 +52,14 @@ def fetch_data(ticker: str, start: str, end: str) -> pd.DataFrame:
|
|
| 47 |
|
| 48 |
def load_arima_order(default=(2, 1, 2)):
|
| 49 |
"""
|
| 50 |
-
|
| 51 |
"""
|
| 52 |
-
# JSON first (version-proof)
|
| 53 |
if os.path.isfile(ARIMA_ORDER_JSON):
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
return order
|
| 59 |
-
except Exception as e:
|
| 60 |
-
print(f"[WARN] Failed to read {ARIMA_ORDER_JSON}: {e}")
|
| 61 |
|
| 62 |
-
# Fallback: try to extract order from pickle (not used for inference otherwise)
|
| 63 |
if os.path.isfile(ARIMA_PICKLE):
|
| 64 |
try:
|
| 65 |
with open(ARIMA_PICKLE, "rb") as f:
|
|
@@ -69,7 +69,6 @@ def load_arima_order(default=(2, 1, 2)):
|
|
| 69 |
except Exception as e:
|
| 70 |
print(f"[WARN] Failed to read order from {ARIMA_PICKLE}: {e}")
|
| 71 |
|
| 72 |
-
# Final fallback
|
| 73 |
return default
|
| 74 |
|
| 75 |
# -------------------------
|
|
@@ -78,7 +77,7 @@ def load_arima_order(default=(2, 1, 2)):
|
|
| 78 |
def arima_roll_single_fit(train_s: pd.Series, test_s: pd.Series, order=(2, 1, 2)):
|
| 79 |
"""
|
| 80 |
Fair evaluation: fit once on TRAIN, then roll 1-step ahead through TEST.
|
| 81 |
-
Uses ARIMAResults.append() to update state; refits if append
|
| 82 |
"""
|
| 83 |
res = ARIMA(train_s, order=order,
|
| 84 |
enforce_stationarity=False, enforce_invertibility=False).fit()
|
|
@@ -88,10 +87,8 @@ def arima_roll_single_fit(train_s: pd.Series, test_s: pd.Series, order=(2, 1, 2)
|
|
| 88 |
y_true = test_s.iloc[i]
|
| 89 |
trues.append(y_true)
|
| 90 |
try:
|
| 91 |
-
# Fast state update
|
| 92 |
res = res.append(test_s.iloc[i:i+1])
|
| 93 |
except Exception:
|
| 94 |
-
# Rare fallback: refit to train + observed test slice
|
| 95 |
res = ARIMA(pd.concat([train_s, test_s.iloc[:i+1]]), order=order,
|
| 96 |
enforce_stationarity=False, enforce_invertibility=False).fit()
|
| 97 |
return np.array(trues), np.array(preds)
|
|
@@ -108,23 +105,57 @@ def arima_future_forecast(all_close: pd.Series, order, steps: int) -> np.ndarray
|
|
| 108 |
# -------------------------
|
| 109 |
# LSTM helpers (Keras .keras + simple min-max scaling)
|
| 110 |
# -------------------------
|
| 111 |
-
def
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 112 |
"""
|
| 113 |
-
Load LSTM and metadata once
|
| 114 |
"""
|
| 115 |
global _LSTM_MODEL, _LSTM_META
|
|
|
|
| 116 |
if _LSTM_MODEL is not None and _LSTM_META is not None:
|
| 117 |
return True
|
| 118 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 119 |
return False
|
|
|
|
| 120 |
try:
|
| 121 |
import tensorflow as tf # import lazily
|
| 122 |
_LSTM_MODEL = tf.keras.models.load_model(LSTM_KERAS, compile=False)
|
| 123 |
-
with open(
|
| 124 |
_LSTM_META = json.load(f)
|
| 125 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 126 |
return True
|
|
|
|
| 127 |
except Exception as e:
|
|
|
|
|
|
|
| 128 |
print(f"[WARN] LSTM load failed: {e}")
|
| 129 |
_LSTM_MODEL, _LSTM_META = None, None
|
| 130 |
return False
|
|
@@ -151,8 +182,8 @@ def lstm_roll_eval_and_forecast(close_train: pd.Series, close_test: pd.Series, h
|
|
| 151 |
(1) fair 1-step-ahead evaluation across TEST
|
| 152 |
(2) recursive forecast for 'horizon' steps
|
| 153 |
"""
|
| 154 |
-
if not
|
| 155 |
-
|
| 156 |
|
| 157 |
import tensorflow as tf # assured by requirements
|
| 158 |
model = _LSTM_MODEL
|
|
@@ -164,6 +195,12 @@ def lstm_roll_eval_and_forecast(close_train: pd.Series, close_test: pd.Series, h
|
|
| 164 |
train_vals = close_train.values.astype("float32")
|
| 165 |
test_vals = close_test.values.astype("float32")
|
| 166 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 167 |
train_s = _scale(train_vals, xmin, xmax)
|
| 168 |
test_s = _scale(test_vals, xmin, xmax)
|
| 169 |
|
|
@@ -173,7 +210,10 @@ def lstm_roll_eval_and_forecast(close_train: pd.Series, close_test: pd.Series, h
|
|
| 173 |
|
| 174 |
test_start_idx = len(train_s) - L
|
| 175 |
if test_start_idx < 0:
|
| 176 |
-
|
|
|
|
|
|
|
|
|
|
| 177 |
|
| 178 |
X_test = X_all[test_start_idx : test_start_idx + len(test_s)]
|
| 179 |
y_test = y_all[test_start_idx : test_start_idx + len(test_s)]
|
|
@@ -193,8 +233,7 @@ def lstm_roll_eval_and_forecast(close_train: pd.Series, close_test: pd.Series, h
|
|
| 193 |
x = window.reshape(1, L, 1)
|
| 194 |
yhat_s = model.predict(x, verbose=0).reshape(-1)[0]
|
| 195 |
fut.append(_unscale(yhat_s, xmin, xmax))
|
| 196 |
-
|
| 197 |
-
window = np.concatenate([window[1:], [yhat_s]], axis=0)
|
| 198 |
|
| 199 |
return y_true, y_pred, np.asarray(fut)
|
| 200 |
|
|
@@ -220,7 +259,7 @@ def make_chart(hist_df: pd.DataFrame,
|
|
| 220 |
h['Date'] = pd.to_datetime(h['Date'])
|
| 221 |
h_last = h.iloc[-252:] if len(h) > 252 else h
|
| 222 |
|
| 223 |
-
fig, ax = plt.subplots(figsize=(9, 4))
|
| 224 |
ax.plot(h_last['Date'], h_last['Close ($)'], label="Historical Close", linewidth=2)
|
| 225 |
|
| 226 |
if arima_fut is not None and not arima_fut.empty:
|
|
@@ -250,29 +289,29 @@ def do_forecast(ticker_choice: str, years_back: int, horizon_days: int):
|
|
| 250 |
Returns:
|
| 251 |
- Historical table
|
| 252 |
- Future ARIMA forecast table
|
| 253 |
-
- KPI markdown (ARIMA + LSTM
|
| 254 |
-
- Chart (ARIMA +
|
| 255 |
-
- Comparison table (ARIMA vs LSTM)
|
| 256 |
"""
|
| 257 |
try:
|
|
|
|
|
|
|
|
|
|
| 258 |
ticker = (ticker_choice or "").strip().upper()
|
| 259 |
end = datetime.utcnow().date()
|
| 260 |
start = end - timedelta(days=int(years_back * 365.25))
|
| 261 |
|
| 262 |
df = fetch_data(ticker, start.isoformat(), end.isoformat())
|
| 263 |
if len(df) < 80:
|
| 264 |
-
|
| 265 |
-
empty = pd.DataFrame()
|
| 266 |
-
return empty, empty, msg, None, pd.DataFrame()
|
| 267 |
|
| 268 |
-
split_idx = int(len(df) * 0.8)
|
| 269 |
order = load_arima_order(default=(2, 1, 2))
|
|
|
|
| 270 |
|
| 271 |
# ---------- ARIMA ----------
|
| 272 |
train_s, test_s = df['Close'][:split_idx], df['Close'][split_idx:]
|
| 273 |
y_true_a, y_pred_a = arima_roll_single_fit(train_s, test_s, order=order)
|
| 274 |
ar_tbl = metrics_df(y_true_a, y_pred_a)
|
| 275 |
-
|
| 276 |
fut_ar = arima_future_forecast(df['Close'], order, horizon_days)
|
| 277 |
|
| 278 |
# Tables for UI (ARIMA)
|
|
@@ -284,40 +323,30 @@ def do_forecast(ticker_choice: str, years_back: int, horizon_days: int):
|
|
| 284 |
hist_out = format_prices_df(hist, "date", "close", "Close ($)")
|
| 285 |
fut_ar_out = format_prices_df(fut_ar_df, "date", "forecast", "Forecast ($)")
|
| 286 |
|
| 287 |
-
# ---------- LSTM (
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
|
| 291 |
-
|
| 292 |
-
|
| 293 |
-
|
| 294 |
-
|
| 295 |
-
|
| 296 |
-
|
| 297 |
-
|
| 298 |
-
|
| 299 |
-
|
| 300 |
-
|
| 301 |
-
|
| 302 |
-
|
| 303 |
-
|
| 304 |
-
"MAE": [ar_tbl["MAE"][0], lstm_tbl["MAE"][0]],
|
| 305 |
-
"MAPE (%)": [ar_tbl["MAPE (%)"][0], lstm_tbl["MAPE (%)"][0]],
|
| 306 |
-
}).round(3)
|
| 307 |
-
|
| 308 |
-
kpi_extra = (f"\n**LSTM** | Test RMSE β {lstm_tbl['RMSE'][0]:.2f}, "
|
| 309 |
-
f"MAE β {lstm_tbl['MAE'][0]:.2f}, "
|
| 310 |
-
f"MAPE β {lstm_tbl['MAPE (%)'][0]:.2f}%")
|
| 311 |
-
|
| 312 |
-
if comp_tbl is None:
|
| 313 |
-
comp_tbl = pd.DataFrame(columns=["Model", "RMSE", "MAE", "MAPE (%)"])
|
| 314 |
-
|
| 315 |
-
# KPI text (always show ARIMA; add LSTM if available)
|
| 316 |
kpi = (
|
| 317 |
f"**ARIMA {order}** | Test RMSE β {ar_tbl['RMSE'][0]:.2f}, "
|
| 318 |
f"MAE β {ar_tbl['MAE'][0]:.2f}, MAPE β {ar_tbl['MAPE (%)'][0]:.2f}% "
|
| 319 |
-
f"| **Ticker:** {ticker} | **Forecast days:** {horizon_days}"
|
| 320 |
-
|
|
|
|
|
|
|
| 321 |
|
| 322 |
# Chart overlay
|
| 323 |
fig = make_chart(hist_out, fut_ar_out, fut_lstm_out)
|
|
@@ -328,6 +357,7 @@ def do_forecast(ticker_choice: str, years_back: int, horizon_days: int):
|
|
| 328 |
traceback.print_exc()
|
| 329 |
err = f"β {str(e)}"
|
| 330 |
empty = pd.DataFrame()
|
|
|
|
| 331 |
return empty, empty, err, None, pd.DataFrame()
|
| 332 |
|
| 333 |
# -------------------------
|
|
@@ -338,9 +368,9 @@ POPULAR = ["AAPL", "MSFT", "GOOGL", "AMZN", "TSLA", "NVDA", "META"]
|
|
| 338 |
with gr.Blocks(title="DataSynthis_ML_JobTask", theme=gr.themes.Soft()) as demo:
|
| 339 |
gr.Markdown(
|
| 340 |
"""
|
| 341 |
-
# DataSynthis_ML_JobTask β Stock Forecasts (
|
| 342 |
-
**What it does:** Downloads recent daily prices, evaluates
|
| 343 |
-
predicts the next trading days
|
| 344 |
**How to use:** Pick a stock, choose how many years of history and how far ahead to forecast, then press **Run Forecast**.
|
| 345 |
"""
|
| 346 |
)
|
|
@@ -366,7 +396,7 @@ with gr.Blocks(title="DataSynthis_ML_JobTask", theme=gr.themes.Soft()) as demo:
|
|
| 366 |
run_btn = gr.Button("Run Forecast", variant="primary")
|
| 367 |
|
| 368 |
gr.Markdown("### Results")
|
| 369 |
-
kpi = gr.Markdown(label="Model summary
|
| 370 |
chart = gr.Plot(label="Chart (last year + forecast)")
|
| 371 |
with gr.Accordion("Tables (click to expand)", open=False):
|
| 372 |
hist_df = gr.Dataframe(label="Historical Prices", wrap=True, interactive=False)
|
|
@@ -379,15 +409,4 @@ with gr.Blocks(title="DataSynthis_ML_JobTask", theme=gr.themes.Soft()) as demo:
|
|
| 379 |
outputs=[hist_df, fut_df, kpi, chart, comp_df]
|
| 380 |
)
|
| 381 |
|
| 382 |
-
with gr.Tab("Help & Glossary"):
|
| 383 |
-
gr.Markdown(
|
| 384 |
-
"""
|
| 385 |
-
### Notes
|
| 386 |
-
- **ARIMA (p,d,q)** is read from `arima_order.json` and fit on your *selected history* for future forecasts.
|
| 387 |
-
- **Fair evaluation:** For both ARIMA and (if present) LSTM, we train on the first 80% of your selected history,
|
| 388 |
-
then predict **one day ahead** across the remaining 20% (no peeking), and report RMSE/MAE/MAPE.
|
| 389 |
-
- **LSTM (optional):** If `lstm_model.keras` and `lstm_info.json` are present, we score and overlay its forecast.
|
| 390 |
-
"""
|
| 391 |
-
)
|
| 392 |
-
|
| 393 |
demo.launch()
|
|
|
|
| 1 |
+
# app.py β DataSynthis_ML_JobTask (ARIMA + LSTM REQUIRED, simple UI)
|
| 2 |
|
| 3 |
import os, json, math, pickle, traceback
|
| 4 |
from datetime import datetime, timedelta
|
|
|
|
| 16 |
# -------------------------
|
| 17 |
# Artifact Paths
|
| 18 |
# -------------------------
|
| 19 |
+
ARIMA_ORDER_JSON = "arima_order.json" # required for ARIMA order (p,d,q)
|
| 20 |
+
ARIMA_PICKLE = "arima_model.pkl" # optional (not used for inference)
|
| 21 |
+
|
| 22 |
+
# LSTM artifacts (LSTM is REQUIRED in this build)
|
| 23 |
+
LSTM_KERAS = "lstm_model.keras" # required
|
| 24 |
+
LSTM_INFO_PRIMARY = "lstm_info.json" # primary metadata location
|
| 25 |
+
LSTM_INFO_ALT = "lstm_model/info.json" # alternate metadata location
|
| 26 |
+
|
| 27 |
+
REQUIRE_LSTM = True # <- make LSTM mandatory
|
| 28 |
|
| 29 |
# -------------------------
|
| 30 |
# Globals (lazy-load LSTM)
|
|
|
|
| 52 |
|
| 53 |
def load_arima_order(default=(2, 1, 2)):
|
| 54 |
"""
|
| 55 |
+
Read (p,d,q) from arima_order.json; fallback to pickle; else default.
|
| 56 |
"""
|
|
|
|
| 57 |
if os.path.isfile(ARIMA_ORDER_JSON):
|
| 58 |
+
with open(ARIMA_ORDER_JSON, "r") as f:
|
| 59 |
+
obj = json.load(f)
|
| 60 |
+
order = tuple(int(x) for x in obj.get("order", list(default)))
|
| 61 |
+
return order
|
|
|
|
|
|
|
|
|
|
| 62 |
|
|
|
|
| 63 |
if os.path.isfile(ARIMA_PICKLE):
|
| 64 |
try:
|
| 65 |
with open(ARIMA_PICKLE, "rb") as f:
|
|
|
|
| 69 |
except Exception as e:
|
| 70 |
print(f"[WARN] Failed to read order from {ARIMA_PICKLE}: {e}")
|
| 71 |
|
|
|
|
| 72 |
return default
|
| 73 |
|
| 74 |
# -------------------------
|
|
|
|
| 77 |
def arima_roll_single_fit(train_s: pd.Series, test_s: pd.Series, order=(2, 1, 2)):
|
| 78 |
"""
|
| 79 |
Fair evaluation: fit once on TRAIN, then roll 1-step ahead through TEST.
|
| 80 |
+
Uses ARIMAResults.append() to update state; refits if append is unavailable.
|
| 81 |
"""
|
| 82 |
res = ARIMA(train_s, order=order,
|
| 83 |
enforce_stationarity=False, enforce_invertibility=False).fit()
|
|
|
|
| 87 |
y_true = test_s.iloc[i]
|
| 88 |
trues.append(y_true)
|
| 89 |
try:
|
|
|
|
| 90 |
res = res.append(test_s.iloc[i:i+1])
|
| 91 |
except Exception:
|
|
|
|
| 92 |
res = ARIMA(pd.concat([train_s, test_s.iloc[:i+1]]), order=order,
|
| 93 |
enforce_stationarity=False, enforce_invertibility=False).fit()
|
| 94 |
return np.array(trues), np.array(preds)
|
|
|
|
| 105 |
# -------------------------
|
| 106 |
# LSTM helpers (Keras .keras + simple min-max scaling)
|
| 107 |
# -------------------------
|
| 108 |
+
def _find_lstm_info_path():
|
| 109 |
+
if os.path.isfile(LSTM_INFO_PRIMARY):
|
| 110 |
+
return LSTM_INFO_PRIMARY
|
| 111 |
+
if os.path.isfile(LSTM_INFO_ALT):
|
| 112 |
+
return LSTM_INFO_ALT
|
| 113 |
+
return None
|
| 114 |
+
|
| 115 |
+
def _maybe_load_lstm(required: bool = False) -> bool:
|
| 116 |
"""
|
| 117 |
+
Load LSTM and metadata once. If `required=True`, raise a RuntimeError when missing.
|
| 118 |
"""
|
| 119 |
global _LSTM_MODEL, _LSTM_META
|
| 120 |
+
|
| 121 |
if _LSTM_MODEL is not None and _LSTM_META is not None:
|
| 122 |
return True
|
| 123 |
+
|
| 124 |
+
info_path = _find_lstm_info_path()
|
| 125 |
+
missing_parts = []
|
| 126 |
+
if not os.path.isfile(LSTM_KERAS):
|
| 127 |
+
missing_parts.append(LSTM_KERAS)
|
| 128 |
+
if info_path is None:
|
| 129 |
+
missing_parts.append("lstm_info.json (or lstm_model/info.json)")
|
| 130 |
+
|
| 131 |
+
if missing_parts:
|
| 132 |
+
msg = ("LSTM artifacts missing: " + ", ".join(missing_parts) +
|
| 133 |
+
". Please add them to the repo. (LSTM is required.)")
|
| 134 |
+
if required:
|
| 135 |
+
raise RuntimeError(msg)
|
| 136 |
+
print("[WARN]", msg)
|
| 137 |
return False
|
| 138 |
+
|
| 139 |
try:
|
| 140 |
import tensorflow as tf # import lazily
|
| 141 |
_LSTM_MODEL = tf.keras.models.load_model(LSTM_KERAS, compile=False)
|
| 142 |
+
with open(info_path, "r") as f:
|
| 143 |
_LSTM_META = json.load(f)
|
| 144 |
+
|
| 145 |
+
# Validate keys
|
| 146 |
+
for k in ("lookback", "train_min", "train_max"):
|
| 147 |
+
if k not in _LSTM_META:
|
| 148 |
+
raise RuntimeError(f"Missing key '{k}' in {info_path}")
|
| 149 |
+
|
| 150 |
+
# Optional: feature_range
|
| 151 |
+
if "feature_range" not in _LSTM_META:
|
| 152 |
+
_LSTM_META["feature_range"] = [0.0, 1.0]
|
| 153 |
+
|
| 154 |
return True
|
| 155 |
+
|
| 156 |
except Exception as e:
|
| 157 |
+
if required:
|
| 158 |
+
raise RuntimeError(f"LSTM load failed: {e}")
|
| 159 |
print(f"[WARN] LSTM load failed: {e}")
|
| 160 |
_LSTM_MODEL, _LSTM_META = None, None
|
| 161 |
return False
|
|
|
|
| 182 |
(1) fair 1-step-ahead evaluation across TEST
|
| 183 |
(2) recursive forecast for 'horizon' steps
|
| 184 |
"""
|
| 185 |
+
# MUST be loaded; raise if not
|
| 186 |
+
_maybe_load_lstm(required=True)
|
| 187 |
|
| 188 |
import tensorflow as tf # assured by requirements
|
| 189 |
model = _LSTM_MODEL
|
|
|
|
| 195 |
train_vals = close_train.values.astype("float32")
|
| 196 |
test_vals = close_test.values.astype("float32")
|
| 197 |
|
| 198 |
+
if len(train_vals) < L:
|
| 199 |
+
raise RuntimeError(
|
| 200 |
+
f"Not enough history for LSTM lookback L={L}. "
|
| 201 |
+
f"Increase 'How much history?' so train length β₯ {L}."
|
| 202 |
+
)
|
| 203 |
+
|
| 204 |
train_s = _scale(train_vals, xmin, xmax)
|
| 205 |
test_s = _scale(test_vals, xmin, xmax)
|
| 206 |
|
|
|
|
| 210 |
|
| 211 |
test_start_idx = len(train_s) - L
|
| 212 |
if test_start_idx < 0:
|
| 213 |
+
raise RuntimeError(
|
| 214 |
+
f"Not enough history for LSTM lookback L={L}. "
|
| 215 |
+
f"Increase 'How much history?' so train length β₯ {L}."
|
| 216 |
+
)
|
| 217 |
|
| 218 |
X_test = X_all[test_start_idx : test_start_idx + len(test_s)]
|
| 219 |
y_test = y_all[test_start_idx : test_start_idx + len(test_s)]
|
|
|
|
| 233 |
x = window.reshape(1, L, 1)
|
| 234 |
yhat_s = model.predict(x, verbose=0).reshape(-1)[0]
|
| 235 |
fut.append(_unscale(yhat_s, xmin, xmax))
|
| 236 |
+
window = np.concatenate([window[1:], [yhat_s]], axis=0) # roll in scaled space
|
|
|
|
| 237 |
|
| 238 |
return y_true, y_pred, np.asarray(fut)
|
| 239 |
|
|
|
|
| 259 |
h['Date'] = pd.to_datetime(h['Date'])
|
| 260 |
h_last = h.iloc[-252:] if len(h) > 252 else h
|
| 261 |
|
| 262 |
+
fig, ax = plt.subplots(figsize=( nine := 9, four := 4))
|
| 263 |
ax.plot(h_last['Date'], h_last['Close ($)'], label="Historical Close", linewidth=2)
|
| 264 |
|
| 265 |
if arima_fut is not None and not arima_fut.empty:
|
|
|
|
| 289 |
Returns:
|
| 290 |
- Historical table
|
| 291 |
- Future ARIMA forecast table
|
| 292 |
+
- KPI markdown (ARIMA + LSTM)
|
| 293 |
+
- Chart (ARIMA + LSTM)
|
| 294 |
+
- Comparison table (ARIMA vs LSTM)
|
| 295 |
"""
|
| 296 |
try:
|
| 297 |
+
# Require LSTM to be present & loadable (will raise otherwise)
|
| 298 |
+
_maybe_load_lstm(required=REQUIRE_LSTM)
|
| 299 |
+
|
| 300 |
ticker = (ticker_choice or "").strip().upper()
|
| 301 |
end = datetime.utcnow().date()
|
| 302 |
start = end - timedelta(days=int(years_back * 365.25))
|
| 303 |
|
| 304 |
df = fetch_data(ticker, start.isoformat(), end.isoformat())
|
| 305 |
if len(df) < 80:
|
| 306 |
+
raise RuntimeError("Not enough data to evaluate. Increase 'How much history?'.")
|
|
|
|
|
|
|
| 307 |
|
|
|
|
| 308 |
order = load_arima_order(default=(2, 1, 2))
|
| 309 |
+
split_idx = int(len(df) * 0.8)
|
| 310 |
|
| 311 |
# ---------- ARIMA ----------
|
| 312 |
train_s, test_s = df['Close'][:split_idx], df['Close'][split_idx:]
|
| 313 |
y_true_a, y_pred_a = arima_roll_single_fit(train_s, test_s, order=order)
|
| 314 |
ar_tbl = metrics_df(y_true_a, y_pred_a)
|
|
|
|
| 315 |
fut_ar = arima_future_forecast(df['Close'], order, horizon_days)
|
| 316 |
|
| 317 |
# Tables for UI (ARIMA)
|
|
|
|
| 323 |
hist_out = format_prices_df(hist, "date", "close", "Close ($)")
|
| 324 |
fut_ar_out = format_prices_df(fut_ar_df, "date", "forecast", "Forecast ($)")
|
| 325 |
|
| 326 |
+
# ---------- LSTM (REQUIRED) ----------
|
| 327 |
+
lstm_true, lstm_pred, fut_lstm = lstm_roll_eval_and_forecast(train_s, test_s, horizon_days)
|
| 328 |
+
lstm_tbl = metrics_df(lstm_true, lstm_pred)
|
| 329 |
+
|
| 330 |
+
fut_lstm_df = pd.DataFrame({"date": pd.Index(future_dates).astype(str),
|
| 331 |
+
"forecast": fut_lstm.reshape(-1,)})
|
| 332 |
+
fut_lstm_out = format_prices_df(fut_lstm_df, "date", "forecast", "Forecast ($)")
|
| 333 |
+
|
| 334 |
+
# Comparison table
|
| 335 |
+
comp_tbl = pd.DataFrame({
|
| 336 |
+
"Model": ["ARIMA", "LSTM"],
|
| 337 |
+
"RMSE": [ar_tbl["RMSE"][0], lstm_tbl["RMSE"][0]],
|
| 338 |
+
"MAE": [ar_tbl["MAE"][0], lstm_tbl["MAE"][0]],
|
| 339 |
+
"MAPE (%)": [ar_tbl["MAPE (%)"][0], lstm_tbl["MAPE (%)"][0]],
|
| 340 |
+
}).round(3)
|
| 341 |
+
|
| 342 |
+
# KPI text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 343 |
kpi = (
|
| 344 |
f"**ARIMA {order}** | Test RMSE β {ar_tbl['RMSE'][0]:.2f}, "
|
| 345 |
f"MAE β {ar_tbl['MAE'][0]:.2f}, MAPE β {ar_tbl['MAPE (%)'][0]:.2f}% "
|
| 346 |
+
f"| **Ticker:** {ticker} | **Forecast days:** {horizon_days}\n"
|
| 347 |
+
f"**LSTM** | Test RMSE β {lstm_tbl['RMSE'][0]:.2f}, "
|
| 348 |
+
f"MAE β {lstm_tbl['MAE'][0]:.2f}, MAPE β {lstm_tbl['MAPE (%)'][0]:.2f}%"
|
| 349 |
+
)
|
| 350 |
|
| 351 |
# Chart overlay
|
| 352 |
fig = make_chart(hist_out, fut_ar_out, fut_lstm_out)
|
|
|
|
| 357 |
traceback.print_exc()
|
| 358 |
err = f"β {str(e)}"
|
| 359 |
empty = pd.DataFrame()
|
| 360 |
+
# Return an error message in KPI area so users immediately see what's wrong
|
| 361 |
return empty, empty, err, None, pd.DataFrame()
|
| 362 |
|
| 363 |
# -------------------------
|
|
|
|
| 368 |
with gr.Blocks(title="DataSynthis_ML_JobTask", theme=gr.themes.Soft()) as demo:
|
| 369 |
gr.Markdown(
|
| 370 |
"""
|
| 371 |
+
# DataSynthis_ML_JobTask β Stock Forecasts (ARIMA + LSTM)
|
| 372 |
+
**What it does:** Downloads recent daily prices, evaluates ARIMA and LSTM fairly,
|
| 373 |
+
and predicts the next trading days.
|
| 374 |
**How to use:** Pick a stock, choose how many years of history and how far ahead to forecast, then press **Run Forecast**.
|
| 375 |
"""
|
| 376 |
)
|
|
|
|
| 396 |
run_btn = gr.Button("Run Forecast", variant="primary")
|
| 397 |
|
| 398 |
gr.Markdown("### Results")
|
| 399 |
+
kpi = gr.Markdown(label="Model summary", value="")
|
| 400 |
chart = gr.Plot(label="Chart (last year + forecast)")
|
| 401 |
with gr.Accordion("Tables (click to expand)", open=False):
|
| 402 |
hist_df = gr.Dataframe(label="Historical Prices", wrap=True, interactive=False)
|
|
|
|
| 409 |
outputs=[hist_df, fut_df, kpi, chart, comp_df]
|
| 410 |
)
|
| 411 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 412 |
demo.launch()
|