NSruman commited on
Commit
5ed2a0f
Β·
verified Β·
1 Parent(s): 6590c42

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +153 -157
app.py CHANGED
@@ -1,4 +1,5 @@
1
- # app.py β€” DataSynthis_ML_JobTask (ARIMA + LSTM, fixed date window)
 
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
- # Fixed historical window (for fair evaluation)
19
  # -------------------------
20
- START_DATE_STR = "2020-10-03"
21
- END_DATE_STR = "2025-10-03"
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
- # Required artifact paths
27
  # -------------------------
28
- ARIMA_PKL = "arima_model.pkl" # used only to read (p,d,q) if possible
29
- LSTM_KERAS = "lstm_model.keras" # Keras 3 native format
30
- LSTM_INFO_1 = "lstm_info.json" # metadata (lookback, train_min, train_max, feature_range)
31
  LSTM_INFO_2 = "lstm_model/info.json"
32
 
33
- REQUIRE_LSTM = True # make LSTM mandatory
34
 
35
- # globals (lazy-loaded)
36
  _LSTM_MODEL = None
37
  _LSTM_META = None
38
 
 
 
39
  # =========================
40
  # Data utilities
41
  # =========================
42
- def fetch_data(ticker: str, start: date, end: date) -> pd.DataFrame:
43
- """Download daily data in the fixed window, business-day freq, forward-fill."""
44
  df = yf.download(
45
- ticker, start=start.isoformat(), end=end.isoformat(),
46
- interval="1d", auto_adjust=True, progress=False
 
 
 
 
47
  )
48
  if df is None or df.empty:
49
- raise ValueError(f"No data returned for '{ticker}' in {start}..{end}.")
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
- """Try to read (p,d,q) from your saved ARIMA pickle; fallback to default."""
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
- # ARIMA helpers
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 internal state
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 lstm_roll_eval_and_forecast(train_s: pd.Series, test_s: pd.Series, horizon_days: int):
165
- """Rolling 1-step evaluation on TEST; then multi-step forecast beyond END_DATE."""
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
- # multi-step future starting after END_DATE (not evaluated)
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 format_prices_df(df: pd.DataFrame, date_col: str, value_col: str, label: str) -> pd.DataFrame:
207
- dates_str = pd.Index(df[date_col]).astype(str)
208
- vals = np.asarray(df[value_col]).reshape(-1,)
209
- return pd.DataFrame({"Date": dates_str, label: np.round(vals, 2)})
210
-
211
- def make_chart(hist_df: pd.DataFrame, fut_df: pd.DataFrame):
212
- try:
213
- h = hist_df.copy()
214
- f = fut_df.copy()
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 forecast callback
236
  # =========================
237
- POPULAR = ["AAPL","MSFT","GOOGL","AMZN","TSLA","NVDA","META"]
238
-
239
- def do_forecast(ticker_choice: str, horizon_days: int):
240
  """
241
- Returns: historical table (fixed window), future table (beyond END_DATE),
242
- KPI markdown (evaluation only inside fixed window), chart.
243
  """
244
  try:
245
  ticker = ticker_choice.strip().upper()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
246
 
247
- # Fixed window
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
- train_s, test_s = df['Close'][:split_idx], df['Close'][split_idx:]
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
- fitted = ARIMA(df['Close'], order=order,
260
- enforce_stationarity=False, enforce_invertibility=False).fit()
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
- # Tables (safe types)
273
- # ----------
274
- hist = pd.DataFrame({
275
- "date": pd.Index(df.index).astype(str),
276
- "close": np.asarray(df['Close']).reshape(-1,)
277
- })
278
- future_dates = pd.bdate_range(df.index[-1] + BDay(1), periods=horizon_days)
279
- fut = pd.DataFrame({
280
- "date": pd.Index(future_dates).astype(str),
281
- "arima": np.asarray(arima_future).reshape(-1,),
282
- "lstm": np.asarray(fut_lstm).reshape(-1,)
283
  })
284
 
285
- hist_out = format_prices_df(hist, "date", "close", "Close ($)")
286
- fut_out = format_prices_df(fut.rename(columns={"arima":"forecast"}), "date", "forecast", "Forecast ($) (ARIMA)")
287
- fut_out["Forecast ($) (LSTM)"] = np.round(fut["lstm"].values, 2)
 
 
288
 
289
- # ----------
290
- # KPI summary
291
- # ----------
292
  kpi = (
293
- f"**Window:** {START_DATE_STR} β†’ {END_DATE_STR} | **Horizon (not evaluated):** {horizon_days} business days \n"
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={_LSTM_META['lookback']})** β€” RMSE: {lstm_tbl['RMSE'][0]:.2f} | MAE: {lstm_tbl['MAE'][0]:.2f} | MAPE: {lstm_tbl['MAPE (%)'][0]:.2f}%"
297
  )
298
 
299
- chart = make_chart(hist_out, fut_out)
300
- return hist_out, fut_out, kpi, chart
301
 
302
  except Exception as e:
303
  traceback.print_exc()
304
- err = f"❌ {str(e)}"
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 β€” Fixed-Window Evaluation (ARIMA & LSTM)
314
- **Evaluation window:** **{START_DATE_STR} β†’ {END_DATE_STR}** (historical prices only)
315
- We fairly evaluate both models **inside this fixed history** using rolling 1-day-ahead predictions.
316
- Forecasts **beyond {END_DATE_STR}** are shown for reference but **not scored** (future is unknown in this setup).
317
  """
318
  )
319
 
320
  with gr.Row():
321
- ticker_choice = gr.Dropdown(
322
- choices=POPULAR, value="AAPL", label="Choose a stock",
323
- info="Pick a symbol. Data is limited to the fixed evaluation window above."
324
- )
325
- horizon = gr.Slider(1, 30, value=5, step=1, label="Forecast days (beyond fixed window)",
326
- info="Business days only; not used for evaluation.")
327
 
328
- run_btn = gr.Button("Run Forecast", variant="primary")
329
 
330
  gr.Markdown("### Results")
331
- kpi = gr.Markdown(label="Model summary (lower = better)", value="")
332
- chart = gr.Plot(label="Chart (fixed window + non-evaluated forecasts)")
333
- with gr.Accordion("Tables (click to expand)", open=False):
334
- hist_df = gr.Dataframe(label="Historical Prices", wrap=True, interactive=False)
335
- fut_df = gr.Dataframe(label="Future Forecasts (not evaluated)", wrap=True, interactive=False)
336
 
337
  run_btn.click(
338
- do_forecast,
339
- inputs=[ticker_choice, horizon],
340
- outputs=[hist_df, fut_df, kpi, chart]
341
  )
342
 
343
- with gr.Tab("Help & Glossary"):
344
  gr.Markdown(
345
  """
346
- ### Why fix the dates?
347
- To evaluate fairly, we must only use **known history**. We therefore pin the window to fixed dates so
348
- every β€œtest day” has a real, known closing price. Any forecast **after** the window is shown for context
349
- but **not scored**.
350
-
351
- ### Terms
352
- - **ARIMA (p,d,q)**: settings for the statistical model.
353
- - **LSTM lookback**: how many past days the neural net reads each time it predicts.
354
- - **RMSE / MAE / MAPE**: error metrics (smaller is better).
355
- - **Rolling 1-day ahead**: predict tomorrow from information available today, walk forward one day at a time.
 
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