Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
| 1 |
-
# app.py β DataSynthis_ML_JobTask
|
|
|
|
| 2 |
|
| 3 |
import os, math, json, pickle, traceback
|
| 4 |
from typing import Tuple
|
|
@@ -15,38 +16,42 @@ from statsmodels.tsa.arima.model import ARIMA
|
|
| 15 |
from sklearn.metrics import mean_squared_error, mean_absolute_error, mean_absolute_percentage_error
|
| 16 |
|
| 17 |
# -------------------------
|
| 18 |
-
#
|
| 19 |
# -------------------------
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
START_DATE = datetime.strptime(START_DATE_STR, "%Y-%m-%d").date()
|
| 23 |
-
END_DATE = datetime.strptime(END_DATE_STR, "%Y-%m-%d").date()
|
| 24 |
|
| 25 |
# -------------------------
|
| 26 |
-
#
|
| 27 |
# -------------------------
|
| 28 |
-
ARIMA_PKL = "arima_model.pkl"
|
| 29 |
-
LSTM_KERAS = "lstm_model.keras"
|
| 30 |
-
LSTM_INFO_1 = "lstm_info.json"
|
| 31 |
LSTM_INFO_2 = "lstm_model/info.json"
|
| 32 |
|
| 33 |
-
REQUIRE_LSTM = True #
|
| 34 |
|
| 35 |
-
#
|
| 36 |
_LSTM_MODEL = None
|
| 37 |
_LSTM_META = None
|
| 38 |
|
|
|
|
|
|
|
| 39 |
# =========================
|
| 40 |
# Data utilities
|
| 41 |
# =========================
|
| 42 |
-
def
|
| 43 |
-
"""Download daily
|
| 44 |
df = yf.download(
|
| 45 |
-
ticker,
|
| 46 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
)
|
| 48 |
if df is None or df.empty:
|
| 49 |
-
raise ValueError(f"No data returned for '{ticker}'
|
| 50 |
df = df[['Close']].copy()
|
| 51 |
if df.index.tz is not None:
|
| 52 |
df.index = df.index.tz_convert(None)
|
|
@@ -54,8 +59,41 @@ def fetch_data(ticker: str, start: date, end: date) -> pd.DataFrame:
|
|
| 54 |
df['Close'] = df['Close'].ffill()
|
| 55 |
return df
|
| 56 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 57 |
def extract_order_from_pickle(default_order=(2,1,2)) -> Tuple[int, int, int]:
|
| 58 |
-
"""
|
| 59 |
try:
|
| 60 |
if os.path.exists(ARIMA_PKL):
|
| 61 |
with open(ARIMA_PKL, "rb") as f:
|
|
@@ -69,21 +107,18 @@ def extract_order_from_pickle(default_order=(2,1,2)) -> Tuple[int, int, int]:
|
|
| 69 |
print(f"[WARN] Could not read order from pickle: {e}")
|
| 70 |
return default_order
|
| 71 |
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
# =========================
|
| 75 |
-
def arima_roll_single_fit(train_s: pd.Series, test_s: pd.Series, order=(2,1,2)):
|
| 76 |
-
"""Fit once on TRAIN, roll 1-step across TEST, updating state each step."""
|
| 77 |
res = ARIMA(train_s, order=order,
|
| 78 |
enforce_stationarity=False, enforce_invertibility=False).fit()
|
| 79 |
preds, trues = [], []
|
| 80 |
for i in range(len(test_s)):
|
| 81 |
fcast = res.forecast(1)
|
| 82 |
-
fval = float(np.asarray(fcast).reshape(-1)[0]) # robust scalar
|
| 83 |
preds.append(fval)
|
| 84 |
trues.append(float(test_s.iloc[i]))
|
| 85 |
try:
|
| 86 |
-
res = res.append(test_s.iloc[i:i+1]) # update
|
| 87 |
except Exception:
|
| 88 |
res = ARIMA(pd.concat([train_s, test_s.iloc[:i+1]]), order=order,
|
| 89 |
enforce_stationarity=False, enforce_invertibility=False).fit()
|
|
@@ -93,7 +128,7 @@ def arima_roll_single_fit(train_s: pd.Series, test_s: pd.Series, order=(2,1,2)):
|
|
| 93 |
# LSTM helpers
|
| 94 |
# =========================
|
| 95 |
def _maybe_load_lstm(required: bool = False) -> bool:
|
| 96 |
-
"""Load LSTM model + metadata with robust compatibility."""
|
| 97 |
global _LSTM_MODEL, _LSTM_META
|
| 98 |
if _LSTM_MODEL is not None and _LSTM_META is not None:
|
| 99 |
return True
|
|
@@ -104,11 +139,9 @@ def _maybe_load_lstm(required: bool = False) -> bool:
|
|
| 104 |
missing.append(LSTM_KERAS)
|
| 105 |
if info_path is None:
|
| 106 |
missing.append("lstm_info.json")
|
| 107 |
-
|
| 108 |
if missing:
|
| 109 |
msg = "LSTM artifacts missing: " + ", ".join(missing)
|
| 110 |
-
if required:
|
| 111 |
-
raise RuntimeError(msg)
|
| 112 |
print("[WARN]", msg)
|
| 113 |
return False
|
| 114 |
|
|
@@ -124,20 +157,16 @@ def _maybe_load_lstm(required: bool = False) -> bool:
|
|
| 124 |
LSTM_KERAS, compile=False,
|
| 125 |
custom_objects={"Orthogonal": tf.keras.initializers.Orthogonal},
|
| 126 |
)
|
| 127 |
-
|
| 128 |
with open(info_path, "r") as f:
|
| 129 |
_LSTM_META = json.load(f)
|
| 130 |
-
|
| 131 |
for k in ("lookback", "train_min", "train_max"):
|
| 132 |
if k not in _LSTM_META:
|
| 133 |
raise RuntimeError(f"Missing key '{k}' in {info_path}")
|
| 134 |
if "feature_range" not in _LSTM_META:
|
| 135 |
_LSTM_META["feature_range"] = [0.0, 1.0]
|
| 136 |
return True
|
| 137 |
-
|
| 138 |
except Exception as e:
|
| 139 |
-
if required:
|
| 140 |
-
raise RuntimeError(f"LSTM load failed: {e}")
|
| 141 |
print(f"[WARN] LSTM load failed: {e}")
|
| 142 |
_LSTM_MODEL, _LSTM_META = None, None
|
| 143 |
return False
|
|
@@ -161,15 +190,14 @@ def _make_lstm_input(last_close_series: pd.Series, lookback: int, meta: dict) ->
|
|
| 161 |
win_scaled = _minmax_scale_1d(window, vmin, vmax, fr_low, fr_high).reshape(lookback, 1)
|
| 162 |
return np.asarray(win_scaled, dtype="float32").reshape(1, lookback, 1)
|
| 163 |
|
| 164 |
-
def
|
| 165 |
-
"""Rolling 1-step evaluation
|
| 166 |
global _LSTM_MODEL, _LSTM_META
|
| 167 |
model, meta = _LSTM_MODEL, _LSTM_META
|
| 168 |
lookback = int(meta["lookback"])
|
| 169 |
fr_low, fr_high = map(float, meta.get("feature_range", [0.0, 1.0]))
|
| 170 |
vmin, vmax = float(meta["train_min"]), float(meta["train_max"])
|
| 171 |
|
| 172 |
-
# rolling eval
|
| 173 |
full_hist = train_s.copy()
|
| 174 |
preds, trues = [], []
|
| 175 |
for i in range(len(test_s)):
|
|
@@ -178,21 +206,10 @@ def lstm_roll_eval_and_forecast(train_s: pd.Series, test_s: pd.Series, horizon_d
|
|
| 178 |
yhat = _minmax_unscale_1d([yhat_scaled], vmin, vmax, fr_low, fr_high)[0]
|
| 179 |
preds.append(float(yhat))
|
| 180 |
trues.append(float(test_s.iloc[i]))
|
|
|
|
| 181 |
full_hist = pd.concat([full_hist, test_s.iloc[i:i+1]])
|
| 182 |
|
| 183 |
-
|
| 184 |
-
hist_for_fc = pd.concat([train_s, test_s])
|
| 185 |
-
future_preds = []
|
| 186 |
-
hist_values = hist_for_fc.copy()
|
| 187 |
-
for _ in range(int(horizon_days)):
|
| 188 |
-
x = _make_lstm_input(hist_values, lookback, meta)
|
| 189 |
-
yhat_scaled = model.predict(x, verbose=0).reshape(-1)[0]
|
| 190 |
-
yhat = _minmax_unscale_1d([yhat_scaled], vmin, vmax, fr_low, fr_high)[0]
|
| 191 |
-
future_preds.append(float(yhat))
|
| 192 |
-
next_idx = hist_values.index[-1] + BDay(1)
|
| 193 |
-
hist_values.loc[next_idx] = yhat
|
| 194 |
-
|
| 195 |
-
return np.array(trues), np.array(preds), np.array(future_preds)
|
| 196 |
|
| 197 |
# =========================
|
| 198 |
# Metrics & formatting
|
|
@@ -203,106 +220,85 @@ def metrics_df(y_true, y_pred) -> pd.DataFrame:
|
|
| 203 |
mape = mean_absolute_percentage_error(y_true, y_pred) * 100
|
| 204 |
return pd.DataFrame({"RMSE":[rmse], "MAE":[mae], "MAPE (%)":[mape]})
|
| 205 |
|
| 206 |
-
def
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
h['Date'] = pd.to_datetime(h['Date'])
|
| 216 |
-
f['Date'] = pd.to_datetime(f['Date'])
|
| 217 |
-
h_last = h.iloc[-252:] if len(h) > 252 else h
|
| 218 |
-
|
| 219 |
-
fig, ax = plt.subplots(figsize=(9, 4))
|
| 220 |
-
ax.plot(h_last['Date'], h_last['Close ($)'], label="Historical Close", linewidth=2)
|
| 221 |
-
if "Forecast ($) (ARIMA)" in f.columns:
|
| 222 |
-
ax.plot(f['Date'], f['Forecast ($) (ARIMA)'], label="ARIMA Forecast", linewidth=2)
|
| 223 |
-
if "Forecast ($) (LSTM)" in f.columns:
|
| 224 |
-
ax.plot(f['Date'], f['Forecast ($) (LSTM)'], label="LSTM Forecast", linewidth=2)
|
| 225 |
-
ax.set_title("Price (fixed history) + next forecasts (not evaluated)")
|
| 226 |
-
ax.set_xlabel("Date"); ax.set_ylabel("USD ($)")
|
| 227 |
-
ax.grid(True, alpha=0.3); ax.legend()
|
| 228 |
-
fig.tight_layout()
|
| 229 |
-
return fig
|
| 230 |
-
except Exception as e:
|
| 231 |
-
print(f"[WARN] chart error: {e}")
|
| 232 |
-
return None
|
| 233 |
|
| 234 |
# =========================
|
| 235 |
-
# Main
|
| 236 |
# =========================
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
def do_forecast(ticker_choice: str, horizon_days: int):
|
| 240 |
"""
|
| 241 |
-
|
| 242 |
-
|
| 243 |
"""
|
| 244 |
try:
|
| 245 |
ticker = ticker_choice.strip().upper()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 246 |
|
| 247 |
-
#
|
| 248 |
-
df = fetch_data(ticker, START_DATE, END_DATE)
|
| 249 |
-
split_idx = int(len(df) * 0.8)
|
| 250 |
-
|
| 251 |
-
# ----------
|
| 252 |
-
# ARIMA (eval inside fixed window)
|
| 253 |
-
# ----------
|
| 254 |
order = extract_order_from_pickle(default_order=(2,1,2))
|
| 255 |
-
|
| 256 |
-
ar_true, ar_pred = arima_roll_single_fit(train_s, test_s, order=order)
|
| 257 |
ar_tbl = metrics_df(ar_true, ar_pred)
|
| 258 |
|
| 259 |
-
|
| 260 |
-
|
| 261 |
-
fc = fitted.get_forecast(steps=horizon_days)
|
| 262 |
-
arima_future = np.asarray(fc.predicted_mean).reshape(-1,)
|
| 263 |
-
|
| 264 |
-
# ----------
|
| 265 |
-
# LSTM (required; eval inside fixed window)
|
| 266 |
-
# ----------
|
| 267 |
-
_maybe_load_lstm(required=REQUIRE_LSTM)
|
| 268 |
-
lstm_true, lstm_pred, fut_lstm = lstm_roll_eval_and_forecast(train_s, test_s, horizon_days)
|
| 269 |
lstm_tbl = metrics_df(lstm_true, lstm_pred)
|
| 270 |
|
| 271 |
-
#
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
|
| 278 |
-
|
| 279 |
-
|
| 280 |
-
"
|
| 281 |
-
"
|
| 282 |
-
"lstm": np.asarray(fut_lstm).reshape(-1,)
|
| 283 |
})
|
| 284 |
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
|
|
|
|
|
|
|
| 288 |
|
| 289 |
-
#
|
| 290 |
-
# KPI summary
|
| 291 |
-
# ----------
|
| 292 |
kpi = (
|
| 293 |
-
f"**
|
| 294 |
-
f"**Ticker:** {ticker} \n"
|
| 295 |
f"**ARIMA {order}** β RMSE: {ar_tbl['RMSE'][0]:.2f} | MAE: {ar_tbl['MAE'][0]:.2f} | MAPE: {ar_tbl['MAPE (%)'][0]:.2f}% \n"
|
| 296 |
-
f"**LSTM (lookback={
|
| 297 |
)
|
| 298 |
|
| 299 |
-
|
| 300 |
-
return hist_out, fut_out, kpi, chart
|
| 301 |
|
| 302 |
except Exception as e:
|
| 303 |
traceback.print_exc()
|
| 304 |
-
|
| 305 |
-
return pd.DataFrame(), pd.DataFrame(), err, None
|
| 306 |
|
| 307 |
# =========================
|
| 308 |
# UI
|
|
@@ -310,49 +306,49 @@ def do_forecast(ticker_choice: str, horizon_days: int):
|
|
| 310 |
with gr.Blocks(title="DataSynthis_ML_JobTask", theme=gr.themes.Soft()) as demo:
|
| 311 |
gr.Markdown(
|
| 312 |
f"""
|
| 313 |
-
# DataSynthis_ML_JobTask β
|
| 314 |
-
**
|
| 315 |
-
|
| 316 |
-
|
| 317 |
"""
|
| 318 |
)
|
| 319 |
|
| 320 |
with gr.Row():
|
| 321 |
-
ticker_choice = gr.Dropdown(
|
| 322 |
-
|
| 323 |
-
|
| 324 |
-
)
|
| 325 |
-
|
| 326 |
-
info="Business days only; not used for evaluation.")
|
| 327 |
|
| 328 |
-
run_btn = gr.Button("Run
|
| 329 |
|
| 330 |
gr.Markdown("### Results")
|
| 331 |
-
kpi = gr.Markdown(label="Model
|
| 332 |
-
|
| 333 |
-
|
| 334 |
-
|
| 335 |
-
|
| 336 |
|
| 337 |
run_btn.click(
|
| 338 |
-
|
| 339 |
-
inputs=[ticker_choice,
|
| 340 |
-
outputs=[
|
| 341 |
)
|
| 342 |
|
| 343 |
-
with gr.Tab("Help
|
| 344 |
gr.Markdown(
|
| 345 |
"""
|
| 346 |
-
###
|
| 347 |
-
|
| 348 |
-
|
| 349 |
-
|
| 350 |
-
|
| 351 |
-
|
| 352 |
-
|
| 353 |
-
-
|
| 354 |
-
|
| 355 |
-
-
|
|
|
|
| 356 |
"""
|
| 357 |
)
|
| 358 |
|
|
|
|
| 1 |
+
# app.py β DataSynthis_ML_JobTask
|
| 2 |
+
# Evaluate pretrained ARIMA + LSTM on a user-picked historical window inside 2020-10-03..2025-10-03
|
| 3 |
|
| 4 |
import os, math, json, pickle, traceback
|
| 5 |
from typing import Tuple
|
|
|
|
| 16 |
from sklearn.metrics import mean_squared_error, mean_absolute_error, mean_absolute_percentage_error
|
| 17 |
|
| 18 |
# -------------------------
|
| 19 |
+
# Historical universe bounds (DO NOT change)
|
| 20 |
# -------------------------
|
| 21 |
+
UNIVERSE_START = date(2020, 10, 3)
|
| 22 |
+
UNIVERSE_END = date(2025, 10, 3)
|
|
|
|
|
|
|
| 23 |
|
| 24 |
# -------------------------
|
| 25 |
+
# Artifact paths (must exist)
|
| 26 |
# -------------------------
|
| 27 |
+
ARIMA_PKL = "arima_model.pkl" # only to read (p,d,q) back
|
| 28 |
+
LSTM_KERAS = "lstm_model.keras" # Keras 3 native format
|
| 29 |
+
LSTM_INFO_1 = "lstm_info.json" # scaler/meta
|
| 30 |
LSTM_INFO_2 = "lstm_model/info.json"
|
| 31 |
|
| 32 |
+
REQUIRE_LSTM = True # enforce LSTM presence
|
| 33 |
|
| 34 |
+
# lazy globals
|
| 35 |
_LSTM_MODEL = None
|
| 36 |
_LSTM_META = None
|
| 37 |
|
| 38 |
+
POPULAR = ["AAPL","MSFT","GOOGL","AMZN","TSLA","NVDA","META","NFLX","IBM","ORCL"]
|
| 39 |
+
|
| 40 |
# =========================
|
| 41 |
# Data utilities
|
| 42 |
# =========================
|
| 43 |
+
def fetch_universe_data(ticker: str) -> pd.DataFrame:
|
| 44 |
+
"""Download daily close for the fixed universe, B-day freq, forward-fill."""
|
| 45 |
df = yf.download(
|
| 46 |
+
ticker,
|
| 47 |
+
start=UNIVERSE_START.isoformat(),
|
| 48 |
+
end=UNIVERSE_END.isoformat(),
|
| 49 |
+
interval="1d",
|
| 50 |
+
auto_adjust=True,
|
| 51 |
+
progress=False,
|
| 52 |
)
|
| 53 |
if df is None or df.empty:
|
| 54 |
+
raise ValueError(f"No data returned for '{ticker}' within the universe window.")
|
| 55 |
df = df[['Close']].copy()
|
| 56 |
if df.index.tz is not None:
|
| 57 |
df.index = df.index.tz_convert(None)
|
|
|
|
| 59 |
df['Close'] = df['Close'].ffill()
|
| 60 |
return df
|
| 61 |
|
| 62 |
+
def clip_to_universe(d: date) -> date:
|
| 63 |
+
return min(max(d, UNIVERSE_START), UNIVERSE_END)
|
| 64 |
+
|
| 65 |
+
def build_train_test_for_window(df: pd.DataFrame, start_sel: date, end_sel: date):
|
| 66 |
+
"""
|
| 67 |
+
Train = all history strictly before start_sel (business days).
|
| 68 |
+
Test = start_sel..end_sel inclusive (business days).
|
| 69 |
+
"""
|
| 70 |
+
# ensure B-day indexes exist
|
| 71 |
+
idx = df.index
|
| 72 |
+
# align selection to nearest business days inside universe
|
| 73 |
+
start_sel = clip_to_universe(start_sel)
|
| 74 |
+
end_sel = clip_to_universe(end_sel)
|
| 75 |
+
if start_sel > end_sel:
|
| 76 |
+
raise ValueError("Selected start date must be <= end date.")
|
| 77 |
+
|
| 78 |
+
# Slice by label (dates) safely
|
| 79 |
+
train_mask = idx < pd.Timestamp(start_sel)
|
| 80 |
+
test_mask = (idx >= pd.Timestamp(start_sel)) & (idx <= pd.Timestamp(end_sel))
|
| 81 |
+
|
| 82 |
+
train_s = df.loc[train_mask, 'Close']
|
| 83 |
+
test_s = df.loc[test_mask, 'Close']
|
| 84 |
+
|
| 85 |
+
if len(test_s) < 5:
|
| 86 |
+
raise ValueError("Selected test window is too short. Pick a wider evaluation window.")
|
| 87 |
+
if len(train_s) < 40:
|
| 88 |
+
raise ValueError("Not enough training history before the selected start date.")
|
| 89 |
+
|
| 90 |
+
return train_s, test_s
|
| 91 |
+
|
| 92 |
+
# =========================
|
| 93 |
+
# ARIMA helpers
|
| 94 |
+
# =========================
|
| 95 |
def extract_order_from_pickle(default_order=(2,1,2)) -> Tuple[int, int, int]:
|
| 96 |
+
"""Read (p,d,q) from your saved ARIMA pickle if possible; fallback to default."""
|
| 97 |
try:
|
| 98 |
if os.path.exists(ARIMA_PKL):
|
| 99 |
with open(ARIMA_PKL, "rb") as f:
|
|
|
|
| 107 |
print(f"[WARN] Could not read order from pickle: {e}")
|
| 108 |
return default_order
|
| 109 |
|
| 110 |
+
def arima_roll_eval(train_s: pd.Series, test_s: pd.Series, order=(2,1,2)):
|
| 111 |
+
"""Fit once on TRAIN, then roll 1-step across TEST, updating state each step."""
|
|
|
|
|
|
|
|
|
|
| 112 |
res = ARIMA(train_s, order=order,
|
| 113 |
enforce_stationarity=False, enforce_invertibility=False).fit()
|
| 114 |
preds, trues = [], []
|
| 115 |
for i in range(len(test_s)):
|
| 116 |
fcast = res.forecast(1)
|
| 117 |
+
fval = float(np.asarray(fcast).reshape(-1)[0]) # robust scalar, avoids FutureWarning
|
| 118 |
preds.append(fval)
|
| 119 |
trues.append(float(test_s.iloc[i]))
|
| 120 |
try:
|
| 121 |
+
res = res.append(test_s.iloc[i:i+1]) # update state with the realized value
|
| 122 |
except Exception:
|
| 123 |
res = ARIMA(pd.concat([train_s, test_s.iloc[:i+1]]), order=order,
|
| 124 |
enforce_stationarity=False, enforce_invertibility=False).fit()
|
|
|
|
| 128 |
# LSTM helpers
|
| 129 |
# =========================
|
| 130 |
def _maybe_load_lstm(required: bool = False) -> bool:
|
| 131 |
+
"""Load LSTM model + metadata with robust compatibility across TF/Keras builds."""
|
| 132 |
global _LSTM_MODEL, _LSTM_META
|
| 133 |
if _LSTM_MODEL is not None and _LSTM_META is not None:
|
| 134 |
return True
|
|
|
|
| 139 |
missing.append(LSTM_KERAS)
|
| 140 |
if info_path is None:
|
| 141 |
missing.append("lstm_info.json")
|
|
|
|
| 142 |
if missing:
|
| 143 |
msg = "LSTM artifacts missing: " + ", ".join(missing)
|
| 144 |
+
if required: raise RuntimeError(msg)
|
|
|
|
| 145 |
print("[WARN]", msg)
|
| 146 |
return False
|
| 147 |
|
|
|
|
| 157 |
LSTM_KERAS, compile=False,
|
| 158 |
custom_objects={"Orthogonal": tf.keras.initializers.Orthogonal},
|
| 159 |
)
|
|
|
|
| 160 |
with open(info_path, "r") as f:
|
| 161 |
_LSTM_META = json.load(f)
|
|
|
|
| 162 |
for k in ("lookback", "train_min", "train_max"):
|
| 163 |
if k not in _LSTM_META:
|
| 164 |
raise RuntimeError(f"Missing key '{k}' in {info_path}")
|
| 165 |
if "feature_range" not in _LSTM_META:
|
| 166 |
_LSTM_META["feature_range"] = [0.0, 1.0]
|
| 167 |
return True
|
|
|
|
| 168 |
except Exception as e:
|
| 169 |
+
if required: raise RuntimeError(f"LSTM load failed: {e}")
|
|
|
|
| 170 |
print(f"[WARN] LSTM load failed: {e}")
|
| 171 |
_LSTM_MODEL, _LSTM_META = None, None
|
| 172 |
return False
|
|
|
|
| 190 |
win_scaled = _minmax_scale_1d(window, vmin, vmax, fr_low, fr_high).reshape(lookback, 1)
|
| 191 |
return np.asarray(win_scaled, dtype="float32").reshape(1, lookback, 1)
|
| 192 |
|
| 193 |
+
def lstm_roll_eval(train_s: pd.Series, test_s: pd.Series):
|
| 194 |
+
"""Rolling 1-step evaluation across the TEST window."""
|
| 195 |
global _LSTM_MODEL, _LSTM_META
|
| 196 |
model, meta = _LSTM_MODEL, _LSTM_META
|
| 197 |
lookback = int(meta["lookback"])
|
| 198 |
fr_low, fr_high = map(float, meta.get("feature_range", [0.0, 1.0]))
|
| 199 |
vmin, vmax = float(meta["train_min"]), float(meta["train_max"])
|
| 200 |
|
|
|
|
| 201 |
full_hist = train_s.copy()
|
| 202 |
preds, trues = [], []
|
| 203 |
for i in range(len(test_s)):
|
|
|
|
| 206 |
yhat = _minmax_unscale_1d([yhat_scaled], vmin, vmax, fr_low, fr_high)[0]
|
| 207 |
preds.append(float(yhat))
|
| 208 |
trues.append(float(test_s.iloc[i]))
|
| 209 |
+
# update history with the REAL observed value (fair evaluation)
|
| 210 |
full_hist = pd.concat([full_hist, test_s.iloc[i:i+1]])
|
| 211 |
|
| 212 |
+
return np.array(trues), np.array(preds)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 213 |
|
| 214 |
# =========================
|
| 215 |
# Metrics & formatting
|
|
|
|
| 220 |
mape = mean_absolute_percentage_error(y_true, y_pred) * 100
|
| 221 |
return pd.DataFrame({"RMSE":[rmse], "MAE":[mae], "MAPE (%)":[mape]})
|
| 222 |
|
| 223 |
+
def plot_series(dates, actual, pred, title):
|
| 224 |
+
fig, ax = plt.subplots(figsize=(9, 4))
|
| 225 |
+
ax.plot(dates, actual, label="Actual", linewidth=2)
|
| 226 |
+
ax.plot(dates, pred, label="Prediction", linewidth=2)
|
| 227 |
+
ax.set_title(title)
|
| 228 |
+
ax.set_xlabel("Date"); ax.set_ylabel("USD ($)")
|
| 229 |
+
ax.grid(True, alpha=0.3); ax.legend()
|
| 230 |
+
fig.tight_layout()
|
| 231 |
+
return fig
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 232 |
|
| 233 |
# =========================
|
| 234 |
+
# Main callback
|
| 235 |
# =========================
|
| 236 |
+
def run_evaluation(ticker_choice: str, start_sel: str, end_sel: str):
|
|
|
|
|
|
|
| 237 |
"""
|
| 238 |
+
Evaluate pretrained ARIMA + LSTM on [start_sel, end_sel] inside the fixed universe.
|
| 239 |
+
Show metrics, two plots, and table (Date, Actual, ARIMA, LSTM).
|
| 240 |
"""
|
| 241 |
try:
|
| 242 |
ticker = ticker_choice.strip().upper()
|
| 243 |
+
if not ticker:
|
| 244 |
+
raise ValueError("Please choose a stock.")
|
| 245 |
+
|
| 246 |
+
# Parse user-selected dates and clip to universe
|
| 247 |
+
s_sel = clip_to_universe(datetime.strptime(start_sel, "%Y-%m-%d").date())
|
| 248 |
+
e_sel = clip_to_universe(datetime.strptime(end_sel, "%Y-%m-%d").date())
|
| 249 |
+
if s_sel > e_sel:
|
| 250 |
+
raise ValueError("Start date must be before or equal to end date.")
|
| 251 |
+
# Fetch universe data and build window train/test
|
| 252 |
+
df = fetch_universe_data(ticker)
|
| 253 |
+
train_s, test_s = build_train_test_for_window(df, s_sel, e_sel)
|
| 254 |
+
|
| 255 |
+
# Ensure LSTM can run (needs lookback history)
|
| 256 |
+
_maybe_load_lstm(required=REQUIRE_LSTM)
|
| 257 |
+
lookback = int(_LSTM_META["lookback"])
|
| 258 |
+
if len(train_s) < lookback:
|
| 259 |
+
raise ValueError(f"Not enough training history for LSTM lookback={lookback}. "
|
| 260 |
+
f"Pick an earlier start date.")
|
| 261 |
|
| 262 |
+
# ARIMA
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 263 |
order = extract_order_from_pickle(default_order=(2,1,2))
|
| 264 |
+
ar_true, ar_pred = arima_roll_eval(train_s, test_s, order=order)
|
|
|
|
| 265 |
ar_tbl = metrics_df(ar_true, ar_pred)
|
| 266 |
|
| 267 |
+
# LSTM
|
| 268 |
+
lstm_true, lstm_pred = lstm_roll_eval(train_s, test_s)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 269 |
lstm_tbl = metrics_df(lstm_true, lstm_pred)
|
| 270 |
|
| 271 |
+
# Build aligned outputs
|
| 272 |
+
dates = pd.Index(test_s.index).astype(str).tolist()
|
| 273 |
+
actual = np.asarray(test_s.values).reshape(-1,)
|
| 274 |
+
arima_p = np.asarray(ar_pred).reshape(-1,)
|
| 275 |
+
lstm_p = np.asarray(lstm_pred).reshape(-1,)
|
| 276 |
+
|
| 277 |
+
table = pd.DataFrame({
|
| 278 |
+
"Date": dates,
|
| 279 |
+
"Actual ($)": np.round(actual, 2),
|
| 280 |
+
"ARIMA ($)": np.round(arima_p, 2),
|
| 281 |
+
"LSTM ($)": np.round(lstm_p, 2),
|
|
|
|
| 282 |
})
|
| 283 |
|
| 284 |
+
# Plots
|
| 285 |
+
fig_arima = plot_series(pd.to_datetime(dates), actual, arima_p,
|
| 286 |
+
f"ARIMA {order} β Actual vs Prediction")
|
| 287 |
+
fig_lstm = plot_series(pd.to_datetime(dates), actual, lstm_p,
|
| 288 |
+
f"LSTM (lookback={lookback}) β Actual vs Prediction")
|
| 289 |
|
| 290 |
+
# KPI Markdown
|
|
|
|
|
|
|
| 291 |
kpi = (
|
| 292 |
+
f"**Ticker:** {ticker} | **Window:** {s_sel} β {e_sel} \n"
|
|
|
|
| 293 |
f"**ARIMA {order}** β RMSE: {ar_tbl['RMSE'][0]:.2f} | MAE: {ar_tbl['MAE'][0]:.2f} | MAPE: {ar_tbl['MAPE (%)'][0]:.2f}% \n"
|
| 294 |
+
f"**LSTM (lookback={lookback})** β RMSE: {lstm_tbl['RMSE'][0]:.2f} | MAE: {lstm_tbl['MAE'][0]:.2f} | MAPE: {lstm_tbl['MAPE (%)'][0]:.2f}%"
|
| 295 |
)
|
| 296 |
|
| 297 |
+
return kpi, fig_arima, fig_lstm, table
|
|
|
|
| 298 |
|
| 299 |
except Exception as e:
|
| 300 |
traceback.print_exc()
|
| 301 |
+
return f"β {str(e)}", None, None, pd.DataFrame()
|
|
|
|
| 302 |
|
| 303 |
# =========================
|
| 304 |
# UI
|
|
|
|
| 306 |
with gr.Blocks(title="DataSynthis_ML_JobTask", theme=gr.themes.Soft()) as demo:
|
| 307 |
gr.Markdown(
|
| 308 |
f"""
|
| 309 |
+
# DataSynthis_ML_JobTask β ARIMA & LSTM (Historical Window Evaluation)
|
| 310 |
+
**Universe:** {UNIVERSE_START} β {UNIVERSE_END}
|
| 311 |
+
Pick a stock and a date range **inside the universe**. Weβll evaluate both pretrained models using
|
| 312 |
+
**rolling 1-day-ahead** predictions across that range and show metrics, plots, and a comparison table.
|
| 313 |
"""
|
| 314 |
)
|
| 315 |
|
| 316 |
with gr.Row():
|
| 317 |
+
ticker_choice = gr.Dropdown(choices=POPULAR, value="AAPL",
|
| 318 |
+
label="Choose a stock")
|
| 319 |
+
with gr.Row():
|
| 320 |
+
start_sel = gr.Date(label="Start date (inside universe)", value=str(UNIVERSE_START))
|
| 321 |
+
end_sel = gr.Date(label="End date (inside universe)", value=str(UNIVERSE_END))
|
|
|
|
| 322 |
|
| 323 |
+
run_btn = gr.Button("Run Evaluation", variant="primary")
|
| 324 |
|
| 325 |
gr.Markdown("### Results")
|
| 326 |
+
kpi = gr.Markdown(label="Model scores (lower = better)", value="")
|
| 327 |
+
with gr.Row():
|
| 328 |
+
chart_arima = gr.Plot(label="ARIMA β Actual vs Predicted")
|
| 329 |
+
chart_lstm = gr.Plot(label="LSTM β Actual vs Predicted")
|
| 330 |
+
table = gr.Dataframe(label="Details", interactive=False)
|
| 331 |
|
| 332 |
run_btn.click(
|
| 333 |
+
run_evaluation,
|
| 334 |
+
inputs=[ticker_choice, start_sel, end_sel],
|
| 335 |
+
outputs=[kpi, chart_arima, chart_lstm, table]
|
| 336 |
)
|
| 337 |
|
| 338 |
+
with gr.Tab("Help"):
|
| 339 |
gr.Markdown(
|
| 340 |
"""
|
| 341 |
+
### What happens here?
|
| 342 |
+
- We fetch daily closing prices for your stock within the **fixed universe** (2020-10-03 β 2025-10-03).
|
| 343 |
+
- For your selected range, we train on all history **before** the start date, then predict **one day ahead** repeatedly
|
| 344 |
+
across the selected range (fair rolling evaluation).
|
| 345 |
+
- We do this for **both** models (ARIMA and LSTM) and show the metrics, plots, and a detailed table.
|
| 346 |
+
|
| 347 |
+
### Notes
|
| 348 |
+
- If the start date is too close to the universe start, LSTM may not have enough history for its lookback.
|
| 349 |
+
Pick an earlier start or reduce lookback when you train your LSTM.
|
| 350 |
+
- ARIMA order is read from your saved `arima_model.pkl`.
|
| 351 |
+
- LSTM scaling/metadata is read from `lstm_info.json` (or `lstm_model/info.json`).
|
| 352 |
"""
|
| 353 |
)
|
| 354 |
|