Akshit Chaturvedi commited on
Commit ยท
aa53e8c
1
Parent(s): 45ac940
migrate to yfin
Browse files- app.py +67 -107
- 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 |
-
|
| 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
|
| 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
|
| 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()
|
| 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
|
| 102 |
-
|
| 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 |
-
|
| 110 |
-
|
|
|
|
| 111 |
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 115 |
|
| 116 |
-
#
|
| 117 |
-
#
|
| 118 |
-
|
| 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 |
-
#
|
| 129 |
-
df_prophet['ds'] = pd.to_datetime(df_prophet['ds'])
|
| 130 |
|
| 131 |
-
#
|
| 132 |
df_prophet['y'] = pd.to_numeric(df_prophet['y'], errors='coerce')
|
| 133 |
-
df_prophet.dropna(subset=['y'], inplace=True)
|
| 134 |
|
| 135 |
if df_prophet.empty:
|
| 136 |
-
print(f"
|
| 137 |
-
raise ValueError(f"Processed
|
| 138 |
|
| 139 |
-
print(f"
|
| 140 |
return df_prophet[['ds', 'y']]
|
| 141 |
|
| 142 |
-
except
|
| 143 |
-
error_detail = f"
|
| 144 |
-
print(f"
|
| 145 |
-
raise Exception(error_detail)
|
| 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 = []
|
| 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
|
| 178 |
try:
|
| 179 |
-
df_new_data =
|
| 180 |
-
except
|
| 181 |
-
status_updates.append(f"Data Fetch ERROR: {str(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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:
|
| 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
|
| 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
|
| 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)
|
| 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
|
| 252 |
-
print(f"PREDICT_ERROR: Data too short for {ticker_selection}
|
| 253 |
return status_message, empty_forecast_df
|
| 254 |
|
| 255 |
fit_df = historical_df.copy()
|
| 256 |
-
#
|
| 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.
|
| 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
|
| 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():
|
| 272 |
-
status_message += f"NaNs present in log-transformed 'y' for {ticker_selection}.
|
| 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')
|
| 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:]:
|
| 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
|
| 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
|
| 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,
|
| 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,
|
| 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,
|
| 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
|
| 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
|
| 378 |
)
|
| 379 |
|
| 380 |
# --- Launch the Gradio App ---
|
| 381 |
if __name__ == "__main__":
|
| 382 |
print("STARTUP INFO: Launching Gradio interface...")
|
| 383 |
-
iface.launch()
|
|
|
|
| 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 |
-
|
|
|
|
| 2 |
prophet
|
| 3 |
pandas
|
| 4 |
numpy
|
| 5 |
+
yfinance
|