Spaces:
Sleeping
Sleeping
| from typing import List | |
| import numpy as np | |
| from fastapi import HTTPException | |
| from tensorflow.keras.models import load_model | |
| from datetime import datetime, timedelta | |
| from app.config import get_settings | |
| from app.indicators import classify_signal, classify_volatility | |
| from app.services.market_data import get_enriched_history, last_n_candles | |
| settings = get_settings() | |
| _model = None | |
| def get_model(): | |
| """ | |
| Lazy-load model to avoid HF Space startup crashes | |
| """ | |
| global _model | |
| if _model is None: | |
| try: | |
| _model = load_model(settings.model_path) | |
| except Exception as e: | |
| print(f"[Prediction] Failed to load model: {e}") | |
| raise HTTPException( | |
| status_code=500, | |
| detail="Prediction model not available", | |
| ) | |
| return _model | |
| def is_model_available() -> bool: | |
| """ | |
| Used by health endpoint. | |
| Does NOT force model loading failure to crash the app. | |
| """ | |
| try: | |
| get_model() | |
| return True | |
| except Exception: | |
| return False | |
| # Normalization | |
| def _normalize_window(window: np.ndarray): | |
| """ | |
| window shape: (history_window, 5) | |
| [Close, RSI, MACD, Log_Volume, MA50] | |
| """ | |
| start_p = window[0, 0] | |
| if start_p <= 0: | |
| raise HTTPException( | |
| status_code=500, | |
| detail="Invalid price normalization window", | |
| ) | |
| vol_mean = np.mean(window[:, 3]) | |
| if vol_mean <= 0: | |
| vol_mean = 1.0 # safety guard | |
| norm = np.zeros_like(window) | |
| norm[:, 0] = (window[:, 0] / start_p) - 1 | |
| norm[:, 1] = window[:, 1] / 100.0 | |
| norm[:, 2] = window[:, 2] / start_p | |
| norm[:, 3] = (window[:, 3] / vol_mean) - 1 | |
| norm[:, 4] = (window[:, 4] / start_p) - 1 | |
| return norm, start_p | |
| # Prediction | |
| def predict_path(ticker: str, days: int): | |
| model = get_model() | |
| if days > settings.max_forecast_days: | |
| days = settings.max_forecast_days | |
| df_raw, df_tech = get_enriched_history(ticker) | |
| if len(df_tech) < settings.history_window: | |
| raise HTTPException( | |
| status_code=400, | |
| detail=( | |
| f"Not enough clean data after indicators " | |
| f"(required={settings.history_window}, got={len(df_tech)})" | |
| ), | |
| ) | |
| features = df_tech[ | |
| ["Close", "RSI", "MACD", "Log_Volume", "MA50"] | |
| ].values | |
| last_window = features[-settings.history_window:] | |
| current_price = float(last_window[-1, 0]) | |
| temp_window = last_window.copy() | |
| forecast_points: List[dict] = [] | |
| for i in range(days): | |
| norm, start_p = _normalize_window(temp_window) | |
| pred_pct = model.predict( | |
| norm.reshape(1, settings.history_window, 5), | |
| verbose=0, | |
| )[0][0] | |
| # 🔒 Safety clamp | |
| pred_pct = float(np.clip(pred_pct, -0.2, 0.2)) | |
| next_price = float(start_p * (1 + pred_pct)) | |
| volatility = 0.015 * (i + 1) | |
| lower_bound = float(next_price * (1 - volatility)) | |
| upper_bound = float(next_price * (1 + volatility)) | |
| forecast_points.append( | |
| { | |
| "day": i + 1, | |
| "price": next_price, | |
| "lower": lower_bound, | |
| "upper": upper_bound, | |
| } | |
| ) | |
| new_row = temp_window[-1].copy() | |
| new_row[0] = next_price | |
| temp_window = np.vstack([temp_window[1:], new_row]) | |
| # Summary | |
| target_price = forecast_points[-1]["price"] | |
| predicted_change = ((target_price - current_price) / current_price) * 100 | |
| vol_label = classify_volatility(df_raw) | |
| signal = classify_signal(predicted_change) | |
| rsi_latest = float(df_tech["RSI"].iloc[-1]) | |
| macd_latest = float(df_tech["MACD"].iloc[-1]) | |
| macd_label = "Bullish" if macd_latest >= 0 else "Bearish" | |
| historical = last_n_candles(df_raw, settings.history_window) | |
| prediction_payload = { | |
| "ticker": ticker.upper(), | |
| "currentPrice": current_price, | |
| "targetPrice": target_price, | |
| "predictedChange": predicted_change, | |
| "signal": signal, | |
| "rsi": rsi_latest, | |
| "macd": macd_label, | |
| "volatility": vol_label, | |
| "historicalPrices": historical, | |
| "predictedPrice": target_price, | |
| "forecastData": [ | |
| { | |
| "day": p["day"], | |
| "price": p["price"], | |
| "changePct": ((p["price"] - current_price) / current_price) * 100, | |
| } | |
| for p in forecast_points | |
| ], | |
| } | |
| # Chart payload | |
| last_date = datetime.strptime(historical[-1]["date"], "%Y-%m-%d") | |
| chart_forecast_payload = { | |
| "ticker": ticker.upper(), | |
| "points": [ | |
| { | |
| "date": historical[-1]["date"], | |
| "price": current_price, | |
| "lower": current_price, | |
| "upper": current_price, | |
| } | |
| ], | |
| } | |
| for i, p in enumerate(forecast_points, start=1): | |
| d = last_date + timedelta(days=i) | |
| chart_forecast_payload["points"].append( | |
| { | |
| "date": d.strftime("%Y-%m-%d"), | |
| "price": p["price"], | |
| "lower": p["lower"], | |
| "upper": p["upper"], | |
| } | |
| ) | |
| return prediction_payload, chart_forecast_payload | |