Akshit Chaturvedi commited on
Commit
aa53e8c
ยท
1 Parent(s): 45ac940

migrate to yfin

Browse files
Files changed (2) hide show
  1. app.py +67 -107
  2. requirements.txt +1 -1
app.py CHANGED
@@ -5,24 +5,17 @@ import numpy as np
5
  import os
6
  import json
7
  from datetime import datetime, timedelta
8
- from alpha_vantage.timeseries import TimeSeries # Alpha Vantage library
9
  import traceback # For detailed error tracebacks
10
 
11
  # --- Configuration ---
12
  # Directory where your .json model files are (for hyperparameters)
13
  MODEL_PARAMS_DIR = "./trained_models" # Assuming this is at the root of your Space
14
  MODEL_PARAMS_PREFIX = "prophet_model_"
15
- DATA_CACHE_FILE = "data_cache.json" # File to cache Alpha Vantage data
16
-
17
- # Fetch Alpha Vantage API Key from Hugging Face Space Secrets
18
- ALPHAVANTAGE_API_KEY = os.environ.get("ALPHAVANTAGE_API_KEY")
19
-
20
- # This will be printed to the Space's container logs when the app starts
21
- if not ALPHAVANTAGE_API_KEY:
22
- print("CRITICAL STARTUP WARNING: ALPHAVANTAGE_API_KEY secret not found in Space settings!")
23
- else:
24
- print("STARTUP INFO: ALPHAVANTAGE_API_KEY found.")
25
 
 
 
26
 
27
  # Default Prophet parameters (can be overridden by those in JSON files)
28
  DEFAULT_PROPHET_PARAMS = {
@@ -34,8 +27,6 @@ DEFAULT_PROPHET_PARAMS = {
34
  'growth': 'linear'
35
  }
36
 
37
- # In app.py, near the top with other configurations
38
-
39
  TICKER_TO_FULL_NAME = {
40
  "DBA": "Invesco DB Agriculture Fund (DBA)",
41
  "FCX": "Freeport-McMoRan Inc. (FCX)",
@@ -49,7 +40,7 @@ TICKER_TO_FULL_NAME = {
49
 
50
  # --- Load Model Hyperparameters ---
51
  model_hyperparams_catalogue = {}
52
- # This will now store (display_name, ticker_symbol) for the dropdown
53
  dropdown_choices = []
54
 
55
  print("STARTUP INFO: Loading model hyperparameter configurations...")
@@ -59,7 +50,7 @@ if os.path.exists(MODEL_PARAMS_DIR):
59
  ticker_symbol = filename.replace(MODEL_PARAMS_PREFIX, "").replace(".json", "")
60
 
61
  # Store the actual hyperparameters with the ticker_symbol as the key
62
- model_hyperparams_catalogue[ticker_symbol] = DEFAULT_PROPHET_PARAMS.copy() # Or load from JSON if needed
63
 
64
  # Get the full name for display, default to ticker if not found
65
  display_name = TICKER_TO_FULL_NAME.get(ticker_symbol, ticker_symbol)
@@ -98,70 +89,51 @@ def save_data_cache(cache):
98
  except Exception as e:
99
  print(f"CACHE ERROR: Error saving cache file {DATA_CACHE_FILE}: {e}")
100
 
101
- def get_timeseries_data_from_alphavantage(ticker_symbol):
102
- if not ALPHAVANTAGE_API_KEY:
103
- print("AV_FETCH ERROR: Alpha Vantage API key is not configured (checked within function).")
104
- raise ValueError("Alpha Vantage API key is not configured.")
105
-
106
- print(f"AV_FETCH INFO: Attempting to fetch UNADJUSTED daily data for {ticker_symbol} from Alpha Vantage using TIME_SERIES_DAILY...")
107
- ts = TimeSeries(key=ALPHAVANTAGE_API_KEY, output_format='pandas')
108
  try:
109
- # Use get_daily() for the TIME_SERIES_DAILY (unadjusted) endpoint
110
- data_av, meta_data = ts.get_daily(symbol=ticker_symbol, outputsize='full')
 
111
 
112
- # --- Process Alpha Vantage DataFrame (for TIME_SERIES_DAILY) ---
113
- # 1. Sort by date (Alpha Vantage usually returns newest first for this endpoint too)
114
- data_av = data_av.sort_index(ascending=True)
 
 
 
 
 
 
115
 
116
- # 2. Rename date index to 'ds' and the chosen price column to 'y'
117
- # For TIME_SERIES_DAILY, the close column is typically '4. close'.
118
- close_column_name = '4. close'
119
- if close_column_name not in data_av.columns:
120
- print(f"AV_FETCH WARNING: Column '{close_column_name}' not found in TIME_SERIES_DAILY data for {ticker_symbol}. Available columns: {data_av.columns.tolist()}")
121
- # You might want to try '5. adjusted close' if that's what your key somehow provides, or other fallbacks.
122
- # For now, let's assume this is critical.
123
- raise KeyError(f"Expected column '{close_column_name}' not found in TIME_SERIES_DAILY response for {ticker_symbol}.")
124
-
125
- df_prophet = data_av[[close_column_name]].reset_index()
126
- df_prophet.rename(columns={'date': 'ds', close_column_name: 'y'}, inplace=True)
127
 
128
- # 3. Ensure 'ds' is datetime
129
- df_prophet['ds'] = pd.to_datetime(df_prophet['ds'])
130
 
131
- # 4. Ensure 'y' is numeric
132
  df_prophet['y'] = pd.to_numeric(df_prophet['y'], errors='coerce')
133
- df_prophet.dropna(subset=['y'], inplace=True) # Remove rows where y could not be coerced
134
 
135
  if df_prophet.empty:
136
- print(f"AV_FETCH WARNING: No valid data returned from Alpha Vantage for {ticker_symbol} after processing TIME_SERIES_DAILY.")
137
- raise ValueError(f"Processed Alpha Vantage TIME_SERIES_DAILY data for {ticker_symbol} is empty.")
138
 
139
- print(f"AV_FETCH INFO: Successfully fetched and processed {len(df_prophet)} unadjusted data points for {ticker_symbol}.")
140
  return df_prophet[['ds', 'y']]
141
 
142
- except KeyError as ke:
143
- error_detail = f"KeyError accessing TIME_SERIES_DAILY Alpha Vantage data for {ticker_symbol}: {str(ke)}. Check column names."
144
- print(f"AV_FETCH ERROR: {error_detail}")
145
- raise Exception(error_detail) # Re-raise with more context
146
- except ValueError as ve: # Catch specific errors like empty processed data
147
- error_detail = f"ValueError processing TIME_SERIES_DAILY Alpha Vantage data for {ticker_symbol}: {str(ve)}."
148
- print(f"AV_FETCH ERROR: {error_detail}")
149
- raise Exception(error_detail) # Re-raise with more context
150
- except Exception as e: # General catch-all for other AV errors
151
- error_detail = f"Alpha Vantage API Error (TIME_SERIES_DAILY) for {ticker_symbol}: {type(e).__name__} - {str(e)}."
152
- print(f"AV_FETCH ERROR: {error_detail}")
153
- if "invalid api call" in str(e).lower() or "does not exist" in str(e).lower():
154
- print(f"AV_FETCH DETAIL: Ticker {ticker_symbol} might not be valid on Alpha Vantage or API call syntax issue.")
155
- elif "premium membership" in str(e).lower() or "rate limit" in str(e).lower():
156
- # This condition should be less likely now with TIME_SERIES_DAILY, but keep for safety
157
- print("AV_FETCH DETAIL: Alpha Vantage rate limit likely exceeded OR an unexpected premium issue with TIME_SERIES_DAILY.")
158
- raise Exception(error_detail) # Re-raise with more context
159
 
160
 
161
  def get_and_cache_data(ticker_symbol, min_history_days=730):
162
  cache = load_data_cache()
163
  today_str = datetime.now().strftime("%Y-%m-%d")
164
- status_updates = [] # To collect messages for UI
165
 
166
  if ticker_symbol in cache and cache[ticker_symbol].get("date_fetched") == today_str:
167
  status_updates.append(f"Using cached data for {ticker_symbol} from {today_str}.")
@@ -174,17 +146,23 @@ def get_and_cache_data(ticker_symbol, min_history_days=730):
174
  status_updates.append(f"Error loading data from cache for {ticker_symbol}: {e}. Will try fetching.")
175
  print(f"CACHE ERROR: Error loading data from cache for {ticker_symbol}: {e}. Will try fetching.")
176
 
177
- status_updates.append(f"No fresh cache for {ticker_symbol}. Attempting to fetch from Alpha Vantage...")
178
  try:
179
- df_new_data = get_timeseries_data_from_alphavantage(ticker_symbol)
180
- except ValueError as ve: # Catch missing API key error specifically
181
- status_updates.append(f"Data Fetch ERROR: {str(ve)}")
 
 
 
 
 
 
 
182
  return None, "\n".join(status_updates)
183
 
184
-
185
  if df_new_data is not None and not df_new_data.empty:
186
  status_updates.append(f"Successfully fetched {len(df_new_data)} new data points for {ticker_symbol}.")
187
- if len(df_new_data) < min_history_days / 4: # Stricter check for very short history
188
  warning_msg = f"WARNING: Fetched data for {ticker_symbol} is very short ({len(df_new_data)} days). Forecast quality may be poor."
189
  status_updates.append(warning_msg)
190
  print(f"DATA_QUALITY WARNING: {warning_msg}")
@@ -198,7 +176,7 @@ def get_and_cache_data(ticker_symbol, min_history_days=730):
198
  save_data_cache(cache)
199
  return df_new_data, "\n".join(status_updates)
200
  else:
201
- status_updates.append(f"Failed to fetch new data for {ticker_symbol} from Alpha Vantage.")
202
  if ticker_symbol in cache and "data" in cache[ticker_symbol]:
203
  status_updates.append(f"Using older cached data for {ticker_symbol} as a fallback.")
204
  print(f"CACHE INFO: Using older cached data for {ticker_symbol} as fallback.")
@@ -214,10 +192,6 @@ def predict_dynamic_forecast(ticker_selection, forecast_periods_str):
214
  status_message = ""
215
  empty_forecast_df = pd.DataFrame(columns=['Date (ds)', 'Predicted Price (yhat)', 'Lower Bound', 'Upper Bound'])
216
 
217
- if not ALPHAVANTAGE_API_KEY: # Check at the very beginning of the request
218
- print("PREDICT_ERROR: Alpha Vantage API Key not configured.")
219
- return "ERROR: Alpha Vantage API Key not configured in Space Secrets. Please check Space settings.", empty_forecast_df
220
-
221
  if not ticker_selection:
222
  return "Please select a ticker.", empty_forecast_df
223
 
@@ -230,12 +204,12 @@ def predict_dynamic_forecast(ticker_selection, forecast_periods_str):
230
 
231
  hyperparams = model_hyperparams_catalogue.get(ticker_selection)
232
  if not hyperparams:
233
- return f"Internal Error: Configuration for '{ticker_selection}' not found (though it should be in dropdown).", empty_forecast_df
234
 
235
  try:
236
  status_message += f"Initiating forecast for {ticker_selection} for {forecast_periods} days...\n"
237
 
238
- historical_df, data_fetch_status = get_and_cache_data(ticker_selection, min_history_days=365 * 1) # Min 1 year
239
  status_message += data_fetch_status + "\n"
240
 
241
  if historical_df is None or historical_df.empty:
@@ -246,31 +220,24 @@ def predict_dynamic_forecast(ticker_selection, forecast_periods_str):
246
  status_message += f"Data loaded ({len(historical_df)} rows). Preprocessing for Prophet (log transform 'y')...\n"
247
  print(f"PREDICT_INFO: Data loaded for {ticker_selection}, rows: {len(historical_df)}")
248
 
249
- # Prophet needs at least 2 data points, practically more.
250
  if len(historical_df) < 10:
251
- status_message += f"Historical data for {ticker_selection} is too short ({len(historical_df)} points) to make a reliable forecast."
252
- print(f"PREDICT_ERROR: Data too short for {ticker_selection} ({len(historical_df)} points)")
253
  return status_message, empty_forecast_df
254
 
255
  fit_df = historical_df.copy()
256
- # Ensure 'y' is positive before log transform
257
  if (fit_df['y'] <= 0).any():
258
- status_message += "WARNING: Historical data contains zero or negative prices. These will be removed before log transformation. This might affect data quantity.\n"
259
- print(f"PREDICT_WARNING: Zero/negative prices found for {ticker_selection}. Filtering them out.")
260
  fit_df = fit_df[fit_df['y'] > 0]
261
  if len(fit_df) < 10:
262
- status_message += f"After removing non-positive prices, data for {ticker_selection} is too short ({len(fit_df)} points)."
263
- print(f"PREDICT_ERROR: Data too short after filtering non-positive prices for {ticker_selection}")
264
  return status_message, empty_forecast_df
265
 
266
  fit_df['y'] = np.log(fit_df['y'])
267
- # No need to replace inf/nan if we filter y > 0 before log.
268
- # fit_df.replace([np.inf, -np.inf], np.nan, inplace=True) # Should not be needed if y > 0
269
- # fit_df['y'] = fit_df['y'].ffill().bfill() # Should not be needed if y > 0 and no other NaNs
270
 
271
- if fit_df['y'].isnull().any(): # Check if any NaNs created for other reasons
272
- status_message += f"NaNs present in log-transformed 'y' for {ticker_selection}. Cannot fit model."
273
- print(f"PREDICT_ERROR: NaNs in log-transformed y for {ticker_selection}")
274
  return status_message, empty_forecast_df
275
 
276
  status_message += f"Fitting Prophet model for {ticker_selection}...\n"
@@ -289,10 +256,9 @@ def predict_dynamic_forecast(ticker_selection, forecast_periods_str):
289
  output_df['Upper Bound (yhat_upper)'] = np.exp(forecast_log_scale['yhat_upper'])
290
 
291
  final_forecast_df = output_df.tail(forecast_periods).reset_index(drop=True)
292
- final_forecast_df['Date (ds)'] = final_forecast_df['ds'].dt.strftime('%Y-%m-%d') # Format date string
293
  final_forecast_df = final_forecast_df[['Date (ds)', 'Predicted Price (yhat)', 'Lower Bound (yhat_lower)', 'Upper Bound (yhat_upper)']]
294
 
295
-
296
  status_message += "Forecast generated successfully."
297
  print(f"PREDICT_INFO: Forecast successful for {ticker_selection}")
298
  return status_message, final_forecast_df
@@ -307,9 +273,8 @@ def predict_dynamic_forecast(ticker_selection, forecast_periods_str):
307
  f"Message: {str(e)}\n\n"
308
  f"--- Traceback (last few lines) ---\n"
309
  )
310
- # Add last few lines of traceback, ensuring not to overflow status box too much
311
  traceback_lines = tb_str.strip().splitlines()
312
- for line in traceback_lines[-7:]: # Show last 7 lines
313
  error_ui_message += line + "\n"
314
 
315
  status_message += f"\n{error_ui_message}"
@@ -319,23 +284,19 @@ def predict_dynamic_forecast(ticker_selection, forecast_periods_str):
319
  with gr.Blocks(css="footer {visibility: hidden}", title="Stock/Commodity Forecaster") as iface:
320
  gr.Markdown("# Stock & Commodity Price Forecaster")
321
  gr.Markdown(
322
- "This tool fetches the latest market data using the Alpha Vantage API, "
323
  "re-fits a Prophet time series model on-the-fly using pre-defined hyperparameters, "
324
  "and generates a future price forecast. [More details in the readme](https://github.com/akshit0201/Prophet-Commodity-Stock-analysis/blob/main/README.md)"
325
  "\n\n**Note:** Forecasts are for informational purposes only and not financial advice. "
326
- "Data fetching may be slow on the first request for a ticker each day. "
327
- "Alpha Vantage free tier has API call limits (5 calls/min, 100 calls/day)."
328
  )
329
- if not ALPHAVANTAGE_API_KEY: # Display warning in UI if key is missing at startup
330
- gr.Markdown("<h3 style='color:red;'>WARNING: Alpha Vantage API Key is not configured in Space Secrets. Data fetching will fail.</h3>")
331
  if not dropdown_choices:
332
  gr.Markdown("<h3 style='color:red;'>WARNING: No model configurations loaded. Ticker selection will be empty. Check 'trained_models' folder and filenames.</h3>")
333
 
334
-
335
  with gr.Row():
336
  with gr.Column(scale=1):
337
  ticker_dropdown = gr.Dropdown(
338
- choices=dropdown_choices, # Use the list of (label, value) tuples
339
  label="Select Ticker Symbol",
340
  info="Choose the stock/commodity to forecast."
341
  )
@@ -343,7 +304,7 @@ with gr.Blocks(css="footer {visibility: hidden}", title="Stock/Commodity Forecas
343
  value=30,
344
  label="Forecast Periods (Days)",
345
  minimum=1,
346
- maximum=365 * 2, # Max 2 years forecast
347
  step=1,
348
  info="Number of future days to predict."
349
  )
@@ -352,7 +313,7 @@ with gr.Blocks(css="footer {visibility: hidden}", title="Stock/Commodity Forecas
352
  with gr.Column(scale=3):
353
  status_textbox = gr.Textbox(
354
  label="Process Status & Logs",
355
- lines=15, # Increased lines for more detailed logs/errors
356
  interactive=False,
357
  placeholder="Status messages will appear here..."
358
  )
@@ -360,7 +321,6 @@ with gr.Blocks(css="footer {visibility: hidden}", title="Stock/Commodity Forecas
360
  gr.Markdown("## Forecast Results")
361
  forecast_output_table = gr.DataFrame(
362
  label="Price Forecast Data"
363
- # Headers will be inferred from the DataFrame column names now
364
  )
365
 
366
  predict_button.click(
@@ -372,12 +332,12 @@ with gr.Blocks(css="footer {visibility: hidden}", title="Stock/Commodity Forecas
372
  gr.Markdown("---")
373
  gr.Markdown(
374
  "**How it works:** Models are based on Facebook's Prophet. Hyperparameters are pre-set. "
375
- "Historical data for the selected ticker is fetched from Alpha Vantage, log-transformed, and used to fit the model. "
376
  "Predictions are then exponentiated back to the original price scale. "
377
- "Fetched data is cached daily in the Space's temporary storage to minimize Alpha Vantage API calls and speed up subsequent requests for the same ticker on the same day."
378
  )
379
 
380
  # --- Launch the Gradio App ---
381
  if __name__ == "__main__":
382
  print("STARTUP INFO: Launching Gradio interface...")
383
- iface.launch() # In Hugging Face Spaces, this is all you need.
 
5
  import os
6
  import json
7
  from datetime import datetime, timedelta
8
+ import yfinance as yf # Changed from alpha_vantage to yfinance
9
  import traceback # For detailed error tracebacks
10
 
11
  # --- Configuration ---
12
  # Directory where your .json model files are (for hyperparameters)
13
  MODEL_PARAMS_DIR = "./trained_models" # Assuming this is at the root of your Space
14
  MODEL_PARAMS_PREFIX = "prophet_model_"
15
+ DATA_CACHE_FILE = "data_cache.json" # File to cache Yahoo Finance data
 
 
 
 
 
 
 
 
 
16
 
17
+ # Startup log
18
+ print("STARTUP INFO: Running with yfinance integration. No API key needed.")
19
 
20
  # Default Prophet parameters (can be overridden by those in JSON files)
21
  DEFAULT_PROPHET_PARAMS = {
 
27
  'growth': 'linear'
28
  }
29
 
 
 
30
  TICKER_TO_FULL_NAME = {
31
  "DBA": "Invesco DB Agriculture Fund (DBA)",
32
  "FCX": "Freeport-McMoRan Inc. (FCX)",
 
40
 
41
  # --- Load Model Hyperparameters ---
42
  model_hyperparams_catalogue = {}
43
+ # This stores (display_name, ticker_symbol) for the dropdown
44
  dropdown_choices = []
45
 
46
  print("STARTUP INFO: Loading model hyperparameter configurations...")
 
50
  ticker_symbol = filename.replace(MODEL_PARAMS_PREFIX, "").replace(".json", "")
51
 
52
  # Store the actual hyperparameters with the ticker_symbol as the key
53
+ model_hyperparams_catalogue[ticker_symbol] = DEFAULT_PROPHET_PARAMS.copy()
54
 
55
  # Get the full name for display, default to ticker if not found
56
  display_name = TICKER_TO_FULL_NAME.get(ticker_symbol, ticker_symbol)
 
89
  except Exception as e:
90
  print(f"CACHE ERROR: Error saving cache file {DATA_CACHE_FILE}: {e}")
91
 
92
+ def get_timeseries_data_from_yfinance(ticker_symbol):
93
+ print(f"YF_FETCH INFO: Fetching daily history for {ticker_symbol} from Yahoo Finance...")
 
 
 
 
 
94
  try:
95
+ ticker = yf.Ticker(ticker_symbol)
96
+ # Fetch complete history
97
+ data_yf = ticker.history(period="max")
98
 
99
+ if data_yf.empty:
100
+ print(f"YF_FETCH WARNING: No data returned from yfinance for {ticker_symbol}.")
101
+ raise ValueError(f"No data found on Yahoo Finance for ticker {ticker_symbol}.")
102
+
103
+ # Ensure dates are chronological
104
+ data_yf = data_yf.sort_index(ascending=True)
105
+
106
+ # Reset index to convert 'Date' or 'Datetime' from index to a normal column
107
+ df_prophet = data_yf[['Close']].reset_index()
108
 
109
+ # Rename date column to 'ds' and 'Close' to 'y'
110
+ # Utilizing .columns[0] guarantees correctness even if the index is named 'Date' or 'Datetime'
111
+ df_prophet.rename(columns={df_prophet.columns[0]: 'ds', 'Close': 'y'}, inplace=True)
 
 
 
 
 
 
 
 
112
 
113
+ # Ensure 'ds' is datetime and strip timezone info to prevent Prophet formatting errors
114
+ df_prophet['ds'] = pd.to_datetime(df_prophet['ds']).dt.tz_localize(None)
115
 
116
+ # Convert prices to numeric and remove null rows
117
  df_prophet['y'] = pd.to_numeric(df_prophet['y'], errors='coerce')
118
+ df_prophet.dropna(subset=['y'], inplace=True)
119
 
120
  if df_prophet.empty:
121
+ print(f"YF_FETCH WARNING: No valid data after processing {ticker_symbol}.")
122
+ raise ValueError(f"Processed Yahoo Finance data for {ticker_symbol} is empty.")
123
 
124
+ print(f"YF_FETCH INFO: Successfully fetched {len(df_prophet)} data points for {ticker_symbol}.")
125
  return df_prophet[['ds', 'y']]
126
 
127
+ except Exception as e:
128
+ error_detail = f"Yahoo Finance API Error for {ticker_symbol}: {type(e).__name__} - {str(e)}."
129
+ print(f"YF_FETCH ERROR: {error_detail}")
130
+ raise Exception(error_detail)
 
 
 
 
 
 
 
 
 
 
 
 
 
131
 
132
 
133
  def get_and_cache_data(ticker_symbol, min_history_days=730):
134
  cache = load_data_cache()
135
  today_str = datetime.now().strftime("%Y-%m-%d")
136
+ status_updates = []
137
 
138
  if ticker_symbol in cache and cache[ticker_symbol].get("date_fetched") == today_str:
139
  status_updates.append(f"Using cached data for {ticker_symbol} from {today_str}.")
 
146
  status_updates.append(f"Error loading data from cache for {ticker_symbol}: {e}. Will try fetching.")
147
  print(f"CACHE ERROR: Error loading data from cache for {ticker_symbol}: {e}. Will try fetching.")
148
 
149
+ status_updates.append(f"No fresh cache for {ticker_symbol}. Attempting to fetch from Yahoo Finance...")
150
  try:
151
+ df_new_data = get_timeseries_data_from_yfinance(ticker_symbol)
152
+ except Exception as e:
153
+ status_updates.append(f"Data Fetch ERROR: {str(e)}")
154
+ # Try falling back to older cache if fetching fails
155
+ if ticker_symbol in cache and "data" in cache[ticker_symbol]:
156
+ status_updates.append(f"Using older cached data for {ticker_symbol} as a fallback.")
157
+ print(f"CACHE INFO: Using older cached data for {ticker_symbol} as fallback.")
158
+ df_data = pd.DataFrame(cache[ticker_symbol]["data"])
159
+ df_data['ds'] = pd.to_datetime(df_data['ds'])
160
+ return df_data, "\n".join(status_updates)
161
  return None, "\n".join(status_updates)
162
 
 
163
  if df_new_data is not None and not df_new_data.empty:
164
  status_updates.append(f"Successfully fetched {len(df_new_data)} new data points for {ticker_symbol}.")
165
+ if len(df_new_data) < min_history_days / 4:
166
  warning_msg = f"WARNING: Fetched data for {ticker_symbol} is very short ({len(df_new_data)} days). Forecast quality may be poor."
167
  status_updates.append(warning_msg)
168
  print(f"DATA_QUALITY WARNING: {warning_msg}")
 
176
  save_data_cache(cache)
177
  return df_new_data, "\n".join(status_updates)
178
  else:
179
+ status_updates.append(f"Failed to fetch new data for {ticker_symbol} from Yahoo Finance.")
180
  if ticker_symbol in cache and "data" in cache[ticker_symbol]:
181
  status_updates.append(f"Using older cached data for {ticker_symbol} as a fallback.")
182
  print(f"CACHE INFO: Using older cached data for {ticker_symbol} as fallback.")
 
192
  status_message = ""
193
  empty_forecast_df = pd.DataFrame(columns=['Date (ds)', 'Predicted Price (yhat)', 'Lower Bound', 'Upper Bound'])
194
 
 
 
 
 
195
  if not ticker_selection:
196
  return "Please select a ticker.", empty_forecast_df
197
 
 
204
 
205
  hyperparams = model_hyperparams_catalogue.get(ticker_selection)
206
  if not hyperparams:
207
+ return f"Internal Error: Configuration for '{ticker_selection}' not found in configuration list.", empty_forecast_df
208
 
209
  try:
210
  status_message += f"Initiating forecast for {ticker_selection} for {forecast_periods} days...\n"
211
 
212
+ historical_df, data_fetch_status = get_and_cache_data(ticker_selection, min_history_days=365 * 1)
213
  status_message += data_fetch_status + "\n"
214
 
215
  if historical_df is None or historical_df.empty:
 
220
  status_message += f"Data loaded ({len(historical_df)} rows). Preprocessing for Prophet (log transform 'y')...\n"
221
  print(f"PREDICT_INFO: Data loaded for {ticker_selection}, rows: {len(historical_df)}")
222
 
 
223
  if len(historical_df) < 10:
224
+ status_message += f"Historical data for {ticker_selection} is too short ({len(historical_df)} points) to fit a model."
225
+ print(f"PREDICT_ERROR: Data too short for {ticker_selection}")
226
  return status_message, empty_forecast_df
227
 
228
  fit_df = historical_df.copy()
229
+ # Filter out zero or negative prices before the log transform
230
  if (fit_df['y'] <= 0).any():
231
+ status_message += "WARNING: Historical data contains zero or negative prices. These will be removed before log transformation.\n"
 
232
  fit_df = fit_df[fit_df['y'] > 0]
233
  if len(fit_df) < 10:
234
+ status_message += f"After filtering, data for {ticker_selection} is too short ({len(fit_df)} points)."
 
235
  return status_message, empty_forecast_df
236
 
237
  fit_df['y'] = np.log(fit_df['y'])
 
 
 
238
 
239
+ if fit_df['y'].isnull().any():
240
+ status_message += f"NaNs present in log-transformed 'y' for {ticker_selection}. Aborting model fit."
 
241
  return status_message, empty_forecast_df
242
 
243
  status_message += f"Fitting Prophet model for {ticker_selection}...\n"
 
256
  output_df['Upper Bound (yhat_upper)'] = np.exp(forecast_log_scale['yhat_upper'])
257
 
258
  final_forecast_df = output_df.tail(forecast_periods).reset_index(drop=True)
259
+ final_forecast_df['Date (ds)'] = final_forecast_df['ds'].dt.strftime('%Y-%m-%d')
260
  final_forecast_df = final_forecast_df[['Date (ds)', 'Predicted Price (yhat)', 'Lower Bound (yhat_lower)', 'Upper Bound (yhat_upper)']]
261
 
 
262
  status_message += "Forecast generated successfully."
263
  print(f"PREDICT_INFO: Forecast successful for {ticker_selection}")
264
  return status_message, final_forecast_df
 
273
  f"Message: {str(e)}\n\n"
274
  f"--- Traceback (last few lines) ---\n"
275
  )
 
276
  traceback_lines = tb_str.strip().splitlines()
277
+ for line in traceback_lines[-7:]:
278
  error_ui_message += line + "\n"
279
 
280
  status_message += f"\n{error_ui_message}"
 
284
  with gr.Blocks(css="footer {visibility: hidden}", title="Stock/Commodity Forecaster") as iface:
285
  gr.Markdown("# Stock & Commodity Price Forecaster")
286
  gr.Markdown(
287
+ "This tool fetches the latest market data using Yahoo Finance via yfinance, "
288
  "re-fits a Prophet time series model on-the-fly using pre-defined hyperparameters, "
289
  "and generates a future price forecast. [More details in the readme](https://github.com/akshit0201/Prophet-Commodity-Stock-analysis/blob/main/README.md)"
290
  "\n\n**Note:** Forecasts are for informational purposes only and not financial advice. "
291
+ "Data fetching may take a moment on the first request for a ticker each day."
 
292
  )
 
 
293
  if not dropdown_choices:
294
  gr.Markdown("<h3 style='color:red;'>WARNING: No model configurations loaded. Ticker selection will be empty. Check 'trained_models' folder and filenames.</h3>")
295
 
 
296
  with gr.Row():
297
  with gr.Column(scale=1):
298
  ticker_dropdown = gr.Dropdown(
299
+ choices=dropdown_choices,
300
  label="Select Ticker Symbol",
301
  info="Choose the stock/commodity to forecast."
302
  )
 
304
  value=30,
305
  label="Forecast Periods (Days)",
306
  minimum=1,
307
+ maximum=365 * 2,
308
  step=1,
309
  info="Number of future days to predict."
310
  )
 
313
  with gr.Column(scale=3):
314
  status_textbox = gr.Textbox(
315
  label="Process Status & Logs",
316
+ lines=15,
317
  interactive=False,
318
  placeholder="Status messages will appear here..."
319
  )
 
321
  gr.Markdown("## Forecast Results")
322
  forecast_output_table = gr.DataFrame(
323
  label="Price Forecast Data"
 
324
  )
325
 
326
  predict_button.click(
 
332
  gr.Markdown("---")
333
  gr.Markdown(
334
  "**How it works:** Models are based on Facebook's Prophet. Hyperparameters are pre-set. "
335
+ "Historical data for the selected ticker is fetched from Yahoo Finance, log-transformed, and used to fit the model. "
336
  "Predictions are then exponentiated back to the original price scale. "
337
+ "Fetched data is cached daily in the Space's temporary storage to minimize network requests and speed up subsequent requests for the same ticker on the same day."
338
  )
339
 
340
  # --- Launch the Gradio App ---
341
  if __name__ == "__main__":
342
  print("STARTUP INFO: Launching Gradio interface...")
343
+ iface.launch()
requirements.txt CHANGED
@@ -2,4 +2,4 @@ gradio
2
  prophet
3
  pandas
4
  numpy
5
- alpha-vantage
 
2
  prophet
3
  pandas
4
  numpy
5
+ yfinance