NSruman commited on
Commit
116b2cb
Β·
verified Β·
1 Parent(s): 7f8413b

Upload 6 files

Browse files
Files changed (7) hide show
  1. .gitattributes +1 -0
  2. app.py +393 -0
  3. arima_model.pkl +3 -0
  4. arima_order.json +1 -0
  5. lstm_info.json +1 -0
  6. lstm_model.keras +3 -0
  7. requirements.txt +7 -0
.gitattributes CHANGED
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ lstm_model.keras filter=lfs diff=lfs merge=lfs -text
app.py ADDED
@@ -0,0 +1,393 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # app.py β€” DataSynthis_ML_JobTask (ARIMA + optional LSTM, simple UI)
2
+
3
+ import os, json, math, pickle, traceback
4
+ from datetime import datetime, timedelta
5
+
6
+ import numpy as np
7
+ import pandas as pd
8
+ import yfinance as yf
9
+ from pandas.tseries.offsets import BDay
10
+ import matplotlib.pyplot as plt
11
+ import gradio as gr
12
+
13
+ from statsmodels.tsa.arima.model import ARIMA
14
+ from sklearn.metrics import mean_squared_error, mean_absolute_error, mean_absolute_percentage_error
15
+
16
+ # -------------------------
17
+ # Artifact Paths
18
+ # -------------------------
19
+ ARIMA_ORDER_JSON = "arima_order.json" # preferred way to get (p,d,q)
20
+ ARIMA_PICKLE = "arima_model.pkl" # optional local archive (not used for inference)
21
+ LSTM_KERAS = "lstm_model.keras" # optional: pre-trained LSTM (Keras 3)
22
+ LSTM_INFO = "lstm_info.json" # optional: {"lookback": int, "train_min": float, "train_max": float}
23
+
24
+ # -------------------------
25
+ # Globals (lazy-load LSTM)
26
+ # -------------------------
27
+ _LSTM_MODEL = None
28
+ _LSTM_META = None
29
+
30
+ # -------------------------
31
+ # Data utilities
32
+ # -------------------------
33
+ def fetch_data(ticker: str, start: str, end: str) -> pd.DataFrame:
34
+ """
35
+ Download daily data, convert to business-day frequency, forward-fill close.
36
+ """
37
+ df = yf.download(ticker, start=start, end=end, interval="1d",
38
+ auto_adjust=True, progress=False)
39
+ if df is None or df.empty:
40
+ raise ValueError(f"No data returned for '{ticker}'. Check ticker or dates.")
41
+ df = df[['Close']].copy()
42
+ if df.index.tz is not None:
43
+ df.index = df.index.tz_convert(None)
44
+ df = df.asfreq('B')
45
+ df['Close'] = df['Close'].ffill()
46
+ return df
47
+
48
+ def load_arima_order(default=(2, 1, 2)):
49
+ """
50
+ Prefer reading (p,d,q) from arima_order.json; fall back to pickle; else default.
51
+ """
52
+ # JSON first (version-proof)
53
+ if os.path.isfile(ARIMA_ORDER_JSON):
54
+ try:
55
+ with open(ARIMA_ORDER_JSON, "r") as f:
56
+ obj = json.load(f)
57
+ order = tuple(int(x) for x in obj.get("order", list(default)))
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:
66
+ res = pickle.load(f)
67
+ if hasattr(res, "model") and hasattr(res.model, "order"):
68
+ return res.model.order
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
+ # -------------------------
76
+ # ARIMA evaluation / forecast
77
+ # -------------------------
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 not available.
82
+ """
83
+ res = ARIMA(train_s, order=order,
84
+ enforce_stationarity=False, enforce_invertibility=False).fit()
85
+ preds, trues = [], []
86
+ for i in range(len(test_s)):
87
+ preds.append(res.forecast(1)[0])
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)
98
+
99
+ def arima_future_forecast(all_close: pd.Series, order, steps: int) -> np.ndarray:
100
+ """
101
+ Fit ARIMA on all data and forecast next 'steps' business days.
102
+ """
103
+ fitted = ARIMA(all_close, order=order,
104
+ enforce_stationarity=False, enforce_invertibility=False).fit()
105
+ fc = fitted.get_forecast(steps=steps)
106
+ return np.asarray(fc.predicted_mean).reshape(-1,)
107
+
108
+ # -------------------------
109
+ # LSTM helpers (Keras .keras + simple min-max scaling)
110
+ # -------------------------
111
+ def _maybe_load_lstm() -> bool:
112
+ """
113
+ Load LSTM and metadata once, if present. Returns True if available.
114
+ """
115
+ global _LSTM_MODEL, _LSTM_META
116
+ if _LSTM_MODEL is not None and _LSTM_META is not None:
117
+ return True
118
+ if not (os.path.isfile(LSTM_KERAS) and os.path.isfile(LSTM_INFO)):
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(LSTM_INFO, "r") as f:
124
+ _LSTM_META = json.load(f)
125
+ assert all(k in _LSTM_META for k in ("lookback", "train_min", "train_max"))
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
131
+
132
+ def _scale(x, xmin, xmax):
133
+ denom = (xmax - xmin) if (xmax - xmin) != 0 else 1.0
134
+ return (x - xmin) / denom
135
+
136
+ def _unscale(x, xmin, xmax):
137
+ return x * (xmax - xmin) + xmin
138
+
139
+ def _seq(data_1d: np.ndarray, L: int):
140
+ X, y = [], []
141
+ for i in range(L, len(data_1d)):
142
+ X.append(data_1d[i-L:i])
143
+ y.append(data_1d[i])
144
+ X = np.asarray(X)[..., None] # (N, L, 1)
145
+ y = np.asarray(y)
146
+ return X, y
147
+
148
+ def lstm_roll_eval_and_forecast(close_train: pd.Series, close_test: pd.Series, horizon: int):
149
+ """
150
+ Use the fixed pre-trained LSTM:
151
+ (1) fair 1-step-ahead evaluation across TEST
152
+ (2) recursive forecast for 'horizon' steps
153
+ """
154
+ if not _maybe_load_lstm():
155
+ return None, None, None
156
+
157
+ import tensorflow as tf # assured by requirements
158
+ model = _LSTM_MODEL
159
+ L = int(_LSTM_META["lookback"])
160
+ xmin = float(_LSTM_META["train_min"])
161
+ xmax = float(_LSTM_META["train_max"])
162
+
163
+ # Scale with training min/max
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
+
170
+ # Build windows over train+test to start test windows with exactly L prior points
171
+ all_scaled = np.concatenate([train_s, test_s], axis=0)
172
+ X_all, y_all = _seq(all_scaled, L)
173
+
174
+ test_start_idx = len(train_s) - L
175
+ if test_start_idx < 0:
176
+ return None, None, None # not enough history for the lookback
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)]
180
+
181
+ # Predict test
182
+ y_pred_scaled = model.predict(X_test, verbose=0).reshape(-1)
183
+ y_true = _unscale(y_test, xmin, xmax)
184
+ y_pred = _unscale(y_pred_scaled, xmin, xmax)
185
+
186
+ # Recursive future forecast using last L points from all history
187
+ hist_vals = np.concatenate([train_vals, test_vals], axis=0)
188
+ hist_scaled = _scale(hist_vals, xmin, xmax)
189
+ window = hist_scaled[-L:].copy()
190
+
191
+ fut = []
192
+ for _ in range(horizon):
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
+ # roll the window (note: keep in scaled space)
197
+ window = np.concatenate([window[1:], [yhat_s]], axis=0)
198
+
199
+ return y_true, y_pred, np.asarray(fut)
200
+
201
+ # -------------------------
202
+ # Metrics / formatting / plotting
203
+ # -------------------------
204
+ def metrics_df(y_true, y_pred) -> pd.DataFrame:
205
+ rmse = math.sqrt(mean_squared_error(y_true, y_pred))
206
+ mae = mean_absolute_error(y_true, y_pred)
207
+ mape = mean_absolute_percentage_error(y_true, y_pred) * 100
208
+ return pd.DataFrame({"RMSE": [rmse], "MAE": [mae], "MAPE (%)": [mape]})
209
+
210
+ def format_prices_df(df: pd.DataFrame, date_col: str, value_col: str, value_label: str):
211
+ dates_str = pd.Index(df[date_col]).astype(str)
212
+ vals = np.asarray(df[value_col]).reshape(-1,)
213
+ return pd.DataFrame({"Date": dates_str, value_label: np.round(vals, 2)})
214
+
215
+ def make_chart(hist_df: pd.DataFrame,
216
+ arima_fut: pd.DataFrame | None,
217
+ lstm_fut: pd.DataFrame | None):
218
+ try:
219
+ h = hist_df.copy()
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:
227
+ f = arima_fut.copy()
228
+ f['Date'] = pd.to_datetime(f['Date'])
229
+ ax.plot(f['Date'], f['Forecast ($)'], label="ARIMA Forecast", linewidth=2)
230
+
231
+ if lstm_fut is not None and not lstm_fut.empty:
232
+ lf = lstm_fut.copy()
233
+ lf['Date'] = pd.to_datetime(lf['Date'])
234
+ ax.plot(lf['Date'], lf['Forecast ($)'], label="LSTM Forecast", linewidth=2, linestyle="--")
235
+
236
+ ax.set_title("Price (last ~1 year) + next forecasts")
237
+ ax.set_xlabel("Date"); ax.set_ylabel("USD ($)")
238
+ ax.grid(True, alpha=0.3); ax.legend()
239
+ fig.tight_layout()
240
+ return fig
241
+ except Exception as e:
242
+ print(f"[WARN] chart error: {e}")
243
+ return None
244
+
245
+ # -------------------------
246
+ # Gradio callback
247
+ # -------------------------
248
+ def do_forecast(ticker_choice: str, years_back: int, horizon_days: int):
249
+ """
250
+ Returns:
251
+ - Historical table
252
+ - Future ARIMA forecast table
253
+ - KPI markdown (ARIMA + LSTM if available)
254
+ - Chart (ARIMA + optional LSTM)
255
+ - Comparison table (ARIMA vs LSTM) if LSTM present
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
+ msg = "Not enough data to evaluate. Increase 'How much history?'."
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)
279
+ hist = pd.DataFrame({"date": pd.Index(df.index).astype(str),
280
+ "close": np.asarray(df['Close']).reshape(-1,)})
281
+ future_dates = pd.bdate_range(df.index[-1] + BDay(1), periods=horizon_days)
282
+ fut_ar_df = pd.DataFrame({"date": pd.Index(future_dates).astype(str),
283
+ "forecast": fut_ar})
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 (optional) ----------
288
+ lstm_available = _maybe_load_lstm()
289
+ fut_lstm_out = None
290
+ comp_tbl = None
291
+ kpi_extra = ""
292
+
293
+ if lstm_available:
294
+ lstm_true, lstm_pred, fut_lstm = lstm_roll_eval_and_forecast(train_s, test_s, horizon_days)
295
+ if lstm_true is not None:
296
+ lstm_tbl = metrics_df(lstm_true, lstm_pred)
297
+ fut_lstm_df = pd.DataFrame({"date": pd.Index(future_dates).astype(str),
298
+ "forecast": fut_lstm.reshape(-1,)})
299
+ fut_lstm_out = format_prices_df(fut_lstm_df, "date", "forecast", "Forecast ($)")
300
+
301
+ comp_tbl = pd.DataFrame({
302
+ "Model": ["ARIMA", "LSTM"],
303
+ "RMSE": [ar_tbl["RMSE"][0], lstm_tbl["RMSE"][0]],
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
+ ) + kpi_extra
321
+
322
+ # Chart overlay
323
+ fig = make_chart(hist_out, fut_ar_out, fut_lstm_out)
324
+
325
+ return hist_out, fut_ar_out, kpi, fig, comp_tbl
326
+
327
+ except Exception as e:
328
+ traceback.print_exc()
329
+ err = f"❌ {str(e)}"
330
+ empty = pd.DataFrame()
331
+ return empty, empty, err, None, pd.DataFrame()
332
+
333
+ # -------------------------
334
+ # UI (simple & clear)
335
+ # -------------------------
336
+ POPULAR = ["AAPL", "MSFT", "GOOGL", "AMZN", "TSLA", "NVDA", "META"]
337
+
338
+ with gr.Blocks(title="DataSynthis_ML_JobTask", theme=gr.themes.Soft()) as demo:
339
+ gr.Markdown(
340
+ """
341
+ # DataSynthis_ML_JobTask β€” Stock Forecasts (Simple & Clear)
342
+ **What it does:** Downloads recent daily prices, evaluates an ARIMA model fairly,
343
+ predicts the next trading days, and (if provided) compares with a pre-trained LSTM.
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
+ )
347
+
348
+ with gr.Tab("Forecast"):
349
+ with gr.Row():
350
+ ticker_choice = gr.Dropdown(
351
+ choices=POPULAR, value="AAPL",
352
+ label="Choose a popular stock",
353
+ info="Pick one from the list."
354
+ )
355
+
356
+ with gr.Row():
357
+ years = gr.Slider(
358
+ 1, 15, value=5, step=1, label="How much history?",
359
+ info="We pull this many *years* of past daily prices."
360
+ )
361
+ horizon = gr.Slider(
362
+ 1, 30, value=5, step=1, label="How many days to predict?",
363
+ info="Business days only (markets closed on weekends/holidays)."
364
+ )
365
+
366
+ run_btn = gr.Button("Run Forecast", variant="primary")
367
+
368
+ gr.Markdown("### Results")
369
+ kpi = gr.Markdown(label="Model summary (lower is better)", value="")
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)
373
+ fut_df = gr.Dataframe(label="Future Forecasts (ARIMA)", wrap=True, interactive=False)
374
+ comp_df = gr.Dataframe(label="Score Comparison (ARIMA vs LSTM)", wrap=True, interactive=False)
375
+
376
+ run_btn.click(
377
+ do_forecast,
378
+ inputs=[ticker_choice, years, horizon],
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()
arima_model.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:a1c2a5cba0ba9ead4441778d5f14a9877cb982d9decc16b0aeddcffe458cf350
3
+ size 6492730
arima_order.json ADDED
@@ -0,0 +1 @@
 
 
1
+ {"order": [0, 1, 3]}
lstm_info.json ADDED
@@ -0,0 +1 @@
 
 
1
+ {"lookback": 40, "train_min": 105.80250549316406, "train_max": 233.4619903564453, "feature_range": [0, 1]}
lstm_model.keras ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:ea052f11008a93ab9dcd5dccb3b5552efd503d4bd0c5057176042ccbda04f461
3
+ size 1446141
requirements.txt ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ gradio>=4.0.0
2
+ yfinance>=0.2.36
3
+ pandas>=2.0.0
4
+ numpy>=1.24.0
5
+ statsmodels>=0.13.5
6
+ scikit-learn>=1.2.0
7
+ tensorflow-cpu==2.12.0