NSruman commited on
Commit
6590c42
·
verified ·
1 Parent(s): d2145b9

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +67 -104
app.py CHANGED
@@ -1,12 +1,11 @@
1
- # app.py — DataSynthis_ML_JobTask (ARIMA + LSTM, minimal UI)
2
 
3
  import os, math, json, pickle, traceback
4
- from typing import Tuple, Optional
5
-
6
  import numpy as np
7
  import pandas as pd
8
  import yfinance as yf
9
- from datetime import datetime, timedelta
10
  from pandas.tseries.offsets import BDay
11
 
12
  import gradio as gr
@@ -15,13 +14,21 @@ import matplotlib.pyplot as plt
15
  from statsmodels.tsa.arima.model import ARIMA
16
  from sklearn.metrics import mean_squared_error, mean_absolute_error, mean_absolute_percentage_error
17
 
 
 
 
 
 
 
 
 
18
  # -------------------------
19
  # Required artifact paths
20
  # -------------------------
21
- ARIMA_PKL = "arima_model.pkl" # used only to read (p,d,q) if possible; we don't unpickle the whole result
22
- LSTM_KERAS = "lstm_model.keras" # Keras 3 native format (no .h5)
23
  LSTM_INFO_1 = "lstm_info.json" # metadata (lookback, train_min, train_max, feature_range)
24
- LSTM_INFO_2 = "lstm_model/info.json" # alternate path if you saved inside a folder
25
 
26
  REQUIRE_LSTM = True # make LSTM mandatory
27
 
@@ -32,11 +39,14 @@ _LSTM_META = None
32
  # =========================
33
  # Data utilities
34
  # =========================
35
- def fetch_data(ticker: str, start: str, end: str) -> pd.DataFrame:
36
- """Download daily data, normalize to business days, forward-fill, return Close series."""
37
- df = yf.download(ticker, start=start, end=end, interval="1d", auto_adjust=True, progress=False)
 
 
 
38
  if df is None or df.empty:
39
- raise ValueError(f"No data returned for '{ticker}'. Check ticker or dates.")
40
  df = df[['Close']].copy()
41
  if df.index.tz is not None:
42
  df.index = df.index.tz_convert(None)
@@ -45,52 +55,35 @@ def fetch_data(ticker: str, start: str, end: str) -> pd.DataFrame:
45
  return df
46
 
47
  def extract_order_from_pickle(default_order=(2,1,2)) -> Tuple[int, int, int]:
48
- """
49
- Try to read (p,d,q) from a previously saved ARIMA pickle without importing
50
- version-specific internals. If it fails, just return the default.
51
- """
52
  try:
53
  if os.path.exists(ARIMA_PKL):
54
  with open(ARIMA_PKL, "rb") as f:
55
  obj = pickle.load(f)
56
- # statsmodels ResultWrapper typically exposes .model.order
57
  if hasattr(obj, "model") and hasattr(obj.model, "order"):
58
  return tuple(int(x) for x in obj.model.order)
59
- # some custom dict-like saves
60
  mo = getattr(obj, "model_orders", None)
61
  if isinstance(mo, dict):
62
  return (int(mo.get("ar", 0)), int(mo.get("diff", 0)), int(mo.get("ma", 0)))
63
  except Exception as e:
64
- print(f"[WARN] Could not read order from pickle, using default {default_order}: {e}")
65
  return default_order
66
 
67
  # =========================
68
  # ARIMA helpers
69
  # =========================
70
- def arima_roll_single_fit(train_s: pd.Series, test_s: pd.Series, order=(2,1,2)) -> Tuple[np.ndarray, np.ndarray]:
71
- """
72
- Fair eval: fit once on TRAIN, then predict 1-step ahead across TEST.
73
- Update model state each step (res.append), fallback to refit if needed.
74
- """
75
  res = ARIMA(train_s, order=order,
76
  enforce_stationarity=False, enforce_invertibility=False).fit()
77
  preds, trues = [], []
78
  for i in range(len(test_s)):
79
  fcast = res.forecast(1)
80
- # robust scalar extraction (works whether Series or ndarray)
81
- try:
82
- fval = float(np.asarray(fcast).reshape(-1)[0])
83
- except Exception:
84
- # as a backup, try pandas iloc if available
85
- fval = float(fcast.iloc[0]) if hasattr(fcast, "iloc") else float(fcast)
86
  preds.append(fval)
87
-
88
- y_true = float(test_s.iloc[i])
89
- trues.append(y_true)
90
-
91
- # update internal state or refit on expanded history
92
  try:
93
- res = res.append(test_s.iloc[i:i+1])
94
  except Exception:
95
  res = ARIMA(pd.concat([train_s, test_s.iloc[:i+1]]), order=order,
96
  enforce_stationarity=False, enforce_invertibility=False).fit()
@@ -100,10 +93,7 @@ def arima_roll_single_fit(train_s: pd.Series, test_s: pd.Series, order=(2,1,2))
100
  # LSTM helpers
101
  # =========================
102
  def _maybe_load_lstm(required: bool = False) -> bool:
103
- """
104
- Load LSTM model + metadata (lookback, min/max, feature_range).
105
- Uses a tolerant loader for Keras 3 + legacy initializers.
106
- """
107
  global _LSTM_MODEL, _LSTM_META
108
  if _LSTM_MODEL is not None and _LSTM_META is not None:
109
  return True
@@ -124,14 +114,12 @@ def _maybe_load_lstm(required: bool = False) -> bool:
124
 
125
  try:
126
  import tensorflow as tf
127
- # load with custom Orthogonal mapping; safe_mode=False for broader compatibility
128
  try:
129
  _LSTM_MODEL = tf.keras.models.load_model(
130
  LSTM_KERAS, compile=False, safe_mode=False,
131
  custom_objects={"Orthogonal": tf.keras.initializers.Orthogonal},
132
  )
133
  except TypeError:
134
- # backoff for older TF wheels
135
  _LSTM_MODEL = tf.keras.models.load_model(
136
  LSTM_KERAS, compile=False,
137
  custom_objects={"Orthogonal": tf.keras.initializers.Orthogonal},
@@ -145,8 +133,8 @@ def _maybe_load_lstm(required: bool = False) -> bool:
145
  raise RuntimeError(f"Missing key '{k}' in {info_path}")
146
  if "feature_range" not in _LSTM_META:
147
  _LSTM_META["feature_range"] = [0.0, 1.0]
148
-
149
  return True
 
150
  except Exception as e:
151
  if required:
152
  raise RuntimeError(f"LSTM load failed: {e}")
@@ -154,19 +142,16 @@ def _maybe_load_lstm(required: bool = False) -> bool:
154
  _LSTM_MODEL, _LSTM_META = None, None
155
  return False
156
 
157
- def _minmax_scale_1d(x: np.ndarray, vmin: float, vmax: float, fr_low: float, fr_high: float) -> np.ndarray:
158
  x = np.asarray(x, dtype="float32").reshape(-1,)
159
  denom = (vmax - vmin) if vmax != vmin else 1.0
160
  return (x - vmin) / denom * (fr_high - fr_low) + fr_low
161
 
162
- def _minmax_unscale_1d(z: np.ndarray, vmin: float, vmax: float, fr_low: float, fr_high: float) -> np.ndarray:
163
  z = np.asarray(z, dtype="float32").reshape(-1,)
164
  return ((z - fr_low) / (fr_high - fr_low)) * (vmax - vmin) + vmin
165
 
166
  def _make_lstm_input(last_close_series: pd.Series, lookback: int, meta: dict) -> np.ndarray:
167
- """
168
- Create a strict (1, lookback, 1) float32 input from the most recent closes.
169
- """
170
  closes = np.asarray(last_close_series.values, dtype="float32").reshape(-1,)
171
  if len(closes) < lookback:
172
  raise ValueError(f"Not enough history for LSTM: need {lookback}, have {len(closes)}.")
@@ -174,24 +159,17 @@ def _make_lstm_input(last_close_series: pd.Series, lookback: int, meta: dict) ->
174
  fr_low, fr_high = map(float, meta.get("feature_range", [0.0, 1.0]))
175
  vmin, vmax = float(meta["train_min"]), float(meta["train_max"])
176
  win_scaled = _minmax_scale_1d(window, vmin, vmax, fr_low, fr_high).reshape(lookback, 1)
177
- x = np.asarray(win_scaled, dtype="float32").reshape(1, lookback, 1)
178
- return x
179
 
180
- def lstm_roll_eval_and_forecast(train_s: pd.Series, test_s: pd.Series, horizon_days: int) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
181
- """
182
- Rolling 1-step evaluation on test; then multi-step forecast from all history.
183
- Returns (y_true, y_pred, future_forecast).
184
- """
185
  global _LSTM_MODEL, _LSTM_META
186
  model, meta = _LSTM_MODEL, _LSTM_META
187
  lookback = int(meta["lookback"])
188
  fr_low, fr_high = map(float, meta.get("feature_range", [0.0, 1.0]))
189
  vmin, vmax = float(meta["train_min"]), float(meta["train_max"])
190
 
191
- if len(train_s) < lookback:
192
- raise ValueError(f"LSTM lookback={lookback} exceeds training length={len(train_s)}. Increase 'Years'.")
193
-
194
- # ---- Rolling eval
195
  full_hist = train_s.copy()
196
  preds, trues = [], []
197
  for i in range(len(test_s)):
@@ -202,7 +180,7 @@ def lstm_roll_eval_and_forecast(train_s: pd.Series, test_s: pd.Series, horizon_d
202
  trues.append(float(test_s.iloc[i]))
203
  full_hist = pd.concat([full_hist, test_s.iloc[i:i+1]])
204
 
205
- # ---- Multi-step future
206
  hist_for_fc = pd.concat([train_s, test_s])
207
  future_preds = []
208
  hist_values = hist_for_fc.copy()
@@ -244,7 +222,7 @@ def make_chart(hist_df: pd.DataFrame, fut_df: pd.DataFrame):
244
  ax.plot(f['Date'], f['Forecast ($) (ARIMA)'], label="ARIMA Forecast", linewidth=2)
245
  if "Forecast ($) (LSTM)" in f.columns:
246
  ax.plot(f['Date'], f['Forecast ($) (LSTM)'], label="LSTM Forecast", linewidth=2)
247
- ax.set_title("Price (last ~1 year) + next forecasts")
248
  ax.set_xlabel("Date"); ax.set_ylabel("USD ($)")
249
  ax.grid(True, alpha=0.3); ax.legend()
250
  fig.tight_layout()
@@ -258,24 +236,23 @@ def make_chart(hist_df: pd.DataFrame, fut_df: pd.DataFrame):
258
  # =========================
259
  POPULAR = ["AAPL","MSFT","GOOGL","AMZN","TSLA","NVDA","META"]
260
 
261
- def do_forecast(ticker_choice: str, years_back: int, horizon_days: int):
262
  """
263
- Returns: historical table, future table, KPI markdown, chart
 
264
  """
265
  try:
266
  ticker = ticker_choice.strip().upper()
267
- end = datetime.utcnow().date()
268
- start = end - timedelta(days=int(years_back * 365.25))
269
 
270
- df = fetch_data(ticker, start.isoformat(), end.isoformat())
 
271
  split_idx = int(len(df) * 0.8)
272
 
273
  # ----------
274
- # ARIMA
275
  # ----------
276
  order = extract_order_from_pickle(default_order=(2,1,2))
277
  train_s, test_s = df['Close'][:split_idx], df['Close'][split_idx:]
278
-
279
  ar_true, ar_pred = arima_roll_single_fit(train_s, test_s, order=order)
280
  ar_tbl = metrics_df(ar_true, ar_pred)
281
 
@@ -285,7 +262,7 @@ def do_forecast(ticker_choice: str, years_back: int, horizon_days: int):
285
  arima_future = np.asarray(fc.predicted_mean).reshape(-1,)
286
 
287
  # ----------
288
- # LSTM (required)
289
  # ----------
290
  _maybe_load_lstm(required=REQUIRE_LSTM)
291
  lstm_true, lstm_pred, fut_lstm = lstm_roll_eval_and_forecast(train_s, test_s, horizon_days)
@@ -313,16 +290,13 @@ def do_forecast(ticker_choice: str, years_back: int, horizon_days: int):
313
  # KPI summary
314
  # ----------
315
  kpi = (
316
- f"**Ticker:** {ticker} | **Horizon:** {horizon_days} business days \n"
 
317
  f"**ARIMA {order}** — RMSE: {ar_tbl['RMSE'][0]:.2f} | MAE: {ar_tbl['MAE'][0]:.2f} | MAPE: {ar_tbl['MAPE (%)'][0]:.2f}% \n"
318
  f"**LSTM (lookback={_LSTM_META['lookback']})** — RMSE: {lstm_tbl['RMSE'][0]:.2f} | MAE: {lstm_tbl['MAE'][0]:.2f} | MAPE: {lstm_tbl['MAPE (%)'][0]:.2f}%"
319
  )
320
 
321
- # ----------
322
- # Chart
323
- # ----------
324
  chart = make_chart(hist_out, fut_out)
325
-
326
  return hist_out, fut_out, kpi, chart
327
 
328
  except Exception as e:
@@ -335,61 +309,50 @@ def do_forecast(ticker_choice: str, years_back: int, horizon_days: int):
335
  # =========================
336
  with gr.Blocks(title="DataSynthis_ML_JobTask", theme=gr.themes.Soft()) as demo:
337
  gr.Markdown(
338
- """
339
- # DataSynthis_ML_JobTask — Stock Forecasts (Simple & Clear)
340
- **What it does:** Downloads recent daily prices, evaluates two models fairly (ARIMA & LSTM),
341
- and predicts the next trading days.
342
-
343
- **How to use:**
344
- 1) Pick a stock.
345
- 2) Choose how many years of history to learn from.
346
- 3) Choose how many **trading days** to forecast.
347
- 4) Press **Run Forecast**.
348
  """
349
  )
350
 
351
  with gr.Row():
352
  ticker_choice = gr.Dropdown(
353
  choices=POPULAR, value="AAPL", label="Choose a stock",
354
- info="Use the dropdown to pick a symbol."
355
  )
356
-
357
- with gr.Row():
358
- years = gr.Slider(1, 15, value=5, step=1, label="How much history (years)?",
359
- info="We pull this many years of past daily prices.")
360
- horizon = gr.Slider(1, 30, value=5, step=1, label="How many days to predict?",
361
- info="Business days only (markets closed on weekends/holidays).")
362
 
363
  run_btn = gr.Button("Run Forecast", variant="primary")
364
 
365
  gr.Markdown("### Results")
366
  kpi = gr.Markdown(label="Model summary (lower = better)", value="")
367
- chart = gr.Plot(label="Chart (last year + forecasts)")
368
  with gr.Accordion("Tables (click to expand)", open=False):
369
  hist_df = gr.Dataframe(label="Historical Prices", wrap=True, interactive=False)
370
- fut_df = gr.Dataframe(label="Future Forecasts", wrap=True, interactive=False)
371
 
372
  run_btn.click(
373
  do_forecast,
374
- inputs=[ticker_choice, years, horizon],
375
  outputs=[hist_df, fut_df, kpi, chart]
376
  )
377
 
378
  with gr.Tab("Help & Glossary"):
379
  gr.Markdown(
380
  """
 
 
 
 
 
381
  ### Terms
382
- - **Ticker**: Stock symbol, e.g., `AAPL` for Apple.
383
- - **Years**: How far back we look to learn from history.
384
- - **Forecast horizon**: How many **trading days** we predict.
385
- - **ARIMA (p, d, q)**: Model settings that control how it uses past levels and past changes.
386
- - **LSTM lookback**: How many past days the neural net reads each time it predicts.
387
- - **RMSE / MAE**: Average error sizes in dollars (smaller is better).
388
- - **MAPE**: Average percentage error (smaller is better).
389
-
390
- ### Fair evaluation
391
- We split the data into **train** (older) and **test** (recent).
392
- Each model predicts **one day ahead** across the test period, always using only the information available up to that day.
393
  """
394
  )
395
 
 
1
+ # app.py — DataSynthis_ML_JobTask (ARIMA + LSTM, fixed date window)
2
 
3
  import os, math, json, pickle, traceback
4
+ from typing import Tuple
 
5
  import numpy as np
6
  import pandas as pd
7
  import yfinance as yf
8
+ from datetime import datetime, date
9
  from pandas.tseries.offsets import BDay
10
 
11
  import gradio as gr
 
14
  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
 
 
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)
 
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:
62
  obj = pickle.load(f)
 
63
  if hasattr(obj, "model") and hasattr(obj.model, "order"):
64
  return tuple(int(x) for x in obj.model.order)
 
65
  mo = getattr(obj, "model_orders", None)
66
  if isinstance(mo, dict):
67
  return (int(mo.get("ar", 0)), int(mo.get("diff", 0)), int(mo.get("ma", 0)))
68
  except Exception as e:
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
  # 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
 
114
 
115
  try:
116
  import tensorflow as tf
 
117
  try:
118
  _LSTM_MODEL = tf.keras.models.load_model(
119
  LSTM_KERAS, compile=False, safe_mode=False,
120
  custom_objects={"Orthogonal": tf.keras.initializers.Orthogonal},
121
  )
122
  except TypeError:
 
123
  _LSTM_MODEL = tf.keras.models.load_model(
124
  LSTM_KERAS, compile=False,
125
  custom_objects={"Orthogonal": tf.keras.initializers.Orthogonal},
 
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}")
 
142
  _LSTM_MODEL, _LSTM_META = None, None
143
  return False
144
 
145
+ def _minmax_scale_1d(x, vmin, vmax, fr_low, fr_high):
146
  x = np.asarray(x, dtype="float32").reshape(-1,)
147
  denom = (vmax - vmin) if vmax != vmin else 1.0
148
  return (x - vmin) / denom * (fr_high - fr_low) + fr_low
149
 
150
+ def _minmax_unscale_1d(z, vmin, vmax, fr_low, fr_high):
151
  z = np.asarray(z, dtype="float32").reshape(-1,)
152
  return ((z - fr_low) / (fr_high - fr_low)) * (vmax - vmin) + vmin
153
 
154
  def _make_lstm_input(last_close_series: pd.Series, lookback: int, meta: dict) -> np.ndarray:
 
 
 
155
  closes = np.asarray(last_close_series.values, dtype="float32").reshape(-1,)
156
  if len(closes) < lookback:
157
  raise ValueError(f"Not enough history for LSTM: need {lookback}, have {len(closes)}.")
 
159
  fr_low, fr_high = map(float, meta.get("feature_range", [0.0, 1.0]))
160
  vmin, vmax = float(meta["train_min"]), float(meta["train_max"])
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)):
 
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()
 
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()
 
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
 
 
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)
 
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:
 
309
  # =========================
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