Skip to content
Snippets Groups Projects
Commit fd5d062b authored by li67@drexel.edu's avatar li67@drexel.edu
Browse files

update everything

parent bdb4ba29
Branches
No related tags found
No related merge requests found
# app.py # app.py
# --- Imports ---
import pandas as pd import pandas as pd
import traceback import traceback
from flask import Flask, render_template, url_for from flask import Flask, render_template, request
from prophet import Prophet from prophet import Prophet
from prophet.diagnostics import cross_validation, performance_metrics from prophet.diagnostics import cross_validation, performance_metrics
from datetime import datetime from datetime import datetime
import math import json
import os # For environment variables (recommended for API keys)
# --- Import data fetching functions --- # --- Import data fetching functions ---
try: try:
from data import (fetch_crypto_price, from data import (fetch_crypto_price, fetch_gemini_historical_data,
fetch_gemini_historical_data, fetch_current_fear_greed, fetch_current_fear_greed, fetch_historical_fear_greed,
fetch_historical_fear_greed, FNG_LIB_AVAILABLE) # Import flag too fetch_reddit_sentiment)
print("--- Successfully imported functions from data.py ---") print("--- Successfully imported functions from data.py ---")
except ImportError as e: except ImportError as e:
print(f"!!! ERROR importing from data.py: {e} !!!") print(f"!!! ERROR importing from data.py: {e} !!!")
# Define dummy functions if import fails
def fetch_crypto_price(*args, **kwargs): return None def fetch_crypto_price(*args, **kwargs): return None
def fetch_gemini_historical_data(*args, **kwargs): return None def fetch_gemini_historical_data(*args, **kwargs): return None
def fetch_current_fear_greed(*args, **kwargs): return None def fetch_current_fear_greed(*args, **kwargs): return None
def fetch_historical_fear_greed(*args, **kwargs): return None def fetch_historical_fear_greed(*args, **kwargs): return None
FNG_LIB_AVAILABLE = False def fetch_reddit_sentiment(*args, **kwargs): return None
# --- Flask App Initialization --- # --- Flask App Initialization ---
app = Flask(__name__) # Looks for templates/ and static/ folders app = Flask(__name__)
# --- Main Route --- # --- Main Route ---
@app.route('/') @app.route('/')
...@@ -34,8 +33,8 @@ def index(): ...@@ -34,8 +33,8 @@ def index():
print(f"\n--- Request received at {start_time.strftime('%Y-%m-%d %H:%M:%S')} ---") print(f"\n--- Request received at {start_time.strftime('%Y-%m-%d %H:%M:%S')} ---")
# --- Configuration --- # --- Configuration ---
symbol = 'BTCUSD' symbol = request.args.get('symbol', 'BTCUSD') # Support dropdown selection
coin_gecko_id = 'bitcoin' coin_gecko_id = {'BTCUSD': 'bitcoin', 'ETHUSD': 'ethereum', 'SOLUSD': 'solana'}.get(symbol, 'bitcoin')
timeframe = '1hr' timeframe = '1hr'
min_data_points = 25 min_data_points = 25
...@@ -48,7 +47,7 @@ def index(): ...@@ -48,7 +47,7 @@ def index():
df_merged = None df_merged = None
df_fng = None df_fng = None
fng_available = False fng_available = False
used_current_fng_for_future = False reddit_sentiment = None
model = None model = None
forecast = None forecast = None
future = None future = None
...@@ -62,39 +61,53 @@ def index(): ...@@ -62,39 +61,53 @@ def index():
historical_accuracy_percent = None historical_accuracy_percent = None
data_confidence_score = 0 data_confidence_score = 0
prediction_interval_penalty = 0 prediction_interval_penalty = 0
used_current_fng_for_future = False
plot_data = None plot_data = None
error_message = None error_message = None
sources = [] # List to store sources for frontend
# --- 1. FETCH DATA --- # --- 1. FETCH DATA ---
try: try:
print("--- Fetching Data ---") print("--- Fetching Data ---")
# Fetch in parallel might be faster but adds complexity, sequential for now
historical_data = fetch_gemini_historical_data(symbol, timeframe=timeframe) historical_data = fetch_gemini_historical_data(symbol, timeframe=timeframe)
if historical_data:
sources.append({"name": "Gemini API", "type": "Price Data", "reliability": "High"})
current_price = fetch_crypto_price(coin_id=coin_gecko_id) current_price = fetch_crypto_price(coin_id=coin_gecko_id)
if FNG_LIB_AVAILABLE:
current_fng_data = fetch_current_fear_greed() current_fng_data = fetch_current_fear_greed()
current_fng_value = current_fng_data.get('value') if isinstance(current_fng_data, dict) else None current_fng_value = current_fng_data.get('value') if current_fng_data else None
print(f"Fetched: Current Price={current_price}, Current F&G={current_fng_value}") if current_fng_value:
sources.append({"name": "Fear & Greed Index", "type": "Market Sentiment", "reliability": "Medium"})
reddit_sentiment = fetch_reddit_sentiment(subreddit="CryptoCurrency", query=coin_gecko_id, timeframe_hours=24)
if reddit_sentiment:
sources.append({"name": "Reddit (r/CryptoCurrency)", "type": "Social Media", "reliability": "Medium"})
print(f"Fetched: Current Price={current_price}, Current F&G={current_fng_value}, Reddit Sentiment={reddit_sentiment}")
except Exception as fetch_e: except Exception as fetch_e:
error_message = "Error during initial data fetching." error_message = "Error during initial data fetching."
print(f"!!! {error_message}: {fetch_e} !!!") print(f"!!! {error_message}: {fetch_e} !!!")
traceback.print_exc()
# --- 2. PROCESS PRICE/VOLUME DATA --- # --- 2. PROCESS PRICE/VOLUME DATA ---
if historical_data and isinstance(historical_data, list) and len(historical_data) >= min_data_points: if historical_data and len(historical_data) >= min_data_points:
try: try:
print("--- Processing Price/Volume data ---") print("--- Processing Price/Volume data ---")
df = pd.DataFrame(historical_data, columns=['timestamp_ms', 'y', 'volume']) df = pd.DataFrame(historical_data, columns=['timestamp_ms', 'y', 'volume'])
df['ds'] = pd.to_datetime(df['timestamp_ms'], unit='ms') df['ds'] = pd.to_datetime(df['timestamp_ms'], unit='ms')
df.dropna(subset=['ds', 'y'], inplace=True) # Essential columns must be valid df.dropna(subset=['ds', 'y'], inplace=True)
if df.empty: if df.empty:
raise ValueError("No valid rows remaining after dropping NaNs in 'ds' or 'y'.") raise ValueError("No valid rows remaining after dropping NaNs in 'ds' or 'y'.")
df['volume'] = pd.to_numeric(df['volume'], errors='coerce').fillna(0) # Clean volume early
df_prophet = df[['ds', 'y', 'volume']].copy() df_prophet = df[['ds', 'y', 'volume']].copy()
data_confidence_score = 50 # Calculate base confidence score
if len(df_prophet) > 100: data_confidence_score += 10 data_confidence_score = 50 # Base for having minimum data
print(f"Data Confidence: Base score {data_confidence_score} after price/vol check.") data_confidence_score += 5 * len(sources) # +5 per source
if len(df_prophet) > 100:
data_confidence_score += 10
# Add reliability bonus
for source in sources:
if source['reliability'] == "High":
data_confidence_score += 10
elif source['reliability'] == "Medium":
data_confidence_score += 5
print(f"Data Confidence: Base score {data_confidence_score} after price/vol and sources check.")
except Exception as e: except Exception as e:
error_message = "Error processing historical price/volume data." error_message = "Error processing historical price/volume data."
print(f"!!! {error_message}: {e} !!!") print(f"!!! {error_message}: {e} !!!")
...@@ -102,21 +115,25 @@ def index(): ...@@ -102,21 +115,25 @@ def index():
df_prophet = None df_prophet = None
data_confidence_score = 0 data_confidence_score = 0
else: else:
# Handle insufficient or failed price data fetch if historical_data is None:
if historical_data is None: error_message = f"Failed to fetch historical data from API for {symbol}/{timeframe}." error_message = f"Failed to fetch historical data from API for {symbol}/{timeframe}."
elif not isinstance(historical_data, list): error_message = f"Historical data received in unexpected format: {type(historical_data)}" else:
else: error_message = f"Insufficient historical data ({len(historical_data)} points) for prediction." error_message = f"Insufficient historical data ({len(historical_data)} points) for prediction."
print(f"!!! {error_message} Confidence Score: {data_confidence_score} !!!") print(f"!!! {error_message} Confidence Score: {data_confidence_score} !!!")
df_prophet = None df_prophet = None
# Render early if core data is missing/insufficient
processing_time = (datetime.now() - start_time).total_seconds() processing_time = (datetime.now() - start_time).total_seconds()
print(f"--- Request processing finished early in {processing_time:.2f} seconds due to data issue. Rendering template... ---") print(f"--- Request processing finished early in {processing_time:.2f} seconds due to data issue. Rendering template... ---")
return render_template('dashboard.html', symbol=symbol, current_price=current_price, current_fng_value=current_fng_value, return render_template('dashboard.html',
error_message=error_message, data_confidence_score=data_confidence_score, plot_data=None, symbol=symbol,
processing_time=f"{processing_time:.2f} sec", last_updated=datetime.now().strftime("%Y-%m-%d %H:%M:%S %Z")) error_message=error_message,
data_confidence_score=data_confidence_score,
plot_data=None,
sources=sources,
processing_time=f"{processing_time:.2f} sec",
last_updated=datetime.now().strftime("%Y-%m-%d %H:%M:%S %Z"))
# --- 2b. FETCH & ATTEMPT MERGE FEAR/GREED DATA --- # --- 2b. FETCH & ATTEMPT MERGE FEAR/GREED DATA ---
if df_prophet is not None and FNG_LIB_AVAILABLE: if df_prophet is not None:
try: try:
start_date = df_prophet['ds'].min() start_date = df_prophet['ds'].min()
end_date = df_prophet['ds'].max() end_date = df_prophet['ds'].max()
...@@ -128,134 +145,116 @@ def index(): ...@@ -128,134 +145,116 @@ def index():
df_prophet_with_date['ds_date'] = df_prophet_with_date['ds'].dt.normalize() df_prophet_with_date['ds_date'] = df_prophet_with_date['ds'].dt.normalize()
temp_merged = pd.merge(df_prophet_with_date, df_fng, on='ds_date', how='left') temp_merged = pd.merge(df_prophet_with_date, df_fng, on='ds_date', how='left')
temp_merged['fear_greed'] = temp_merged['fear_greed'].ffill().bfill() temp_merged['fear_greed'] = temp_merged['fear_greed'].ffill().bfill()
# FIX for Pandas FutureWarning: Use direct assignment instead of inplace=True on potentially chained access if temp_merged['fear_greed'].isnull().any():
temp_merged['fear_greed'] = temp_merged['fear_greed'].fillna(50) # Final fallback print("!!! Warning: Could not fill all missing Fear/Greed values. Filling remaining with 50. !!!")
temp_merged['fear_greed'].fillna(50, inplace=True)
df_merged = temp_merged[['ds', 'y', 'volume', 'fear_greed']].copy() df_merged = temp_merged[['ds', 'y', 'volume', 'fear_greed']].copy()
fng_available = True fng_available = True
data_confidence_score += 25 data_confidence_score += 15
print(f"Data Confidence: Added F&G bonus. Score = {data_confidence_score}") print(f"Data Confidence: Added F&G bonus. Score = {data_confidence_score}")
print(f"Successfully merged F&G data. Fitting DataFrame has columns: {list(df_merged.columns)}")
else: else:
print("--- Proceeding without Fear/Greed data (fetch failed or no data). ---") print("--- Proceeding without Fear/Greed data (fetch failed or no data). ---")
df_merged = df_prophet.copy() df_merged = df_prophet.copy()
fng_available = False fng_available = False
print(f"Data Confidence: No F&G bonus applied. Score = {data_confidence_score}")
except Exception as e: except Exception as e:
if not error_message: error_message = "Error merging Fear/Greed data." if not error_message:
error_message = "Error merging Fear/Greed data."
print(f"!!! {error_message}: {e} !!!") print(f"!!! {error_message}: {e} !!!")
traceback.print_exc() traceback.print_exc()
df_merged = df_prophet.copy() df_merged = df_prophet.copy()
fng_available = False fng_available = False
print(f"Data Confidence: F&G merge failed. Score = {data_confidence_score}")
elif df_prophet is not None: # FNG lib not available # --- 2c. MERGE REDDIT SENTIMENT ---
print("--- Skipping F&G merge: Library not available ---") if df_prophet is not None and reddit_sentiment and 'average_score' in reddit_sentiment:
try:
df_merged['reddit_sentiment'] = reddit_sentiment['average_score']
# Add precision bonus based on sentiment variance
if 'variance' in reddit_sentiment:
variance = reddit_sentiment['variance']
if variance < 0.1: # Low variance = precise
data_confidence_score += 10
elif variance < 0.3:
data_confidence_score += 5
print(f"Data Confidence: Added Reddit sentiment precision bonus (variance={variance}). Score = {data_confidence_score}")
except Exception as e:
print(f"!!! Error merging Reddit sentiment: {e} !!!")
if df_merged is None:
df_merged = df_prophet.copy() df_merged = df_prophet.copy()
fng_available = False
# --- 3. MODEL TRAINING & PREDICTION --- # --- 3. MODEL TRAINING & PREDICTION ---
if df_merged is not None: if df_merged is not None:
try: try:
print("--- Instantiating Prophet model ---") print("--- Instantiating Prophet model ---")
model = Prophet(interval_width=0.95) # 95% prediction interval model = Prophet(interval_width=0.95)
volume_regressor_added = False # <<< FIX: Initialize flag if 'volume' in df_merged.columns and not df_merged['volume'].isnull().all():
fng_regressor_added = False # <<< FIX: Initialize flag df_merged['volume'] = pd.to_numeric(df_merged['volume'], errors='coerce').fillna(0)
print("--- Adding Regressors ---")
# Add 'volume' regressor if valid
if 'volume' in df_merged.columns and df_merged['volume'].notna().any():
model.add_regressor('volume') model.add_regressor('volume')
volume_regressor_added = True # <<< FIX: Set flag if fng_available and 'fear_greed' in df_merged.columns and not df_merged['fear_greed'].isnull().all():
print("Added 'volume' regressor.") df_merged['fear_greed'] = pd.to_numeric(df_merged['fear_greed'], errors='coerce').fillna(50)
else: print("--- Skipping 'volume' regressor (column missing or all NaN) ---")
# Add 'fear_greed' regressor if valid and available
if fng_available and 'fear_greed' in df_merged.columns and df_merged['fear_greed'].notna().any():
model.add_regressor('fear_greed') model.add_regressor('fear_greed')
fng_regressor_added = True # <<< FIX: Set flag if 'reddit_sentiment' in df_merged.columns and not df_merged['reddit_sentiment'].isnull().all():
print("Added 'fear_greed' regressor.") df_merged['reddit_sentiment'] = pd.to_numeric(df_merged['reddit_sentiment'], errors='coerce').fillna(0)
else: model.add_regressor('reddit_sentiment')
fng_available = False # Ensure state consistency
print("Fitting model WITHOUT 'fear_greed' regressor (not available or all NaN).") initial_rows = len(df_merged)
df_merged.dropna(subset=['y'], inplace=True)
# Final check on data points before fitting if len(df_merged) < initial_rows:
df_merged.dropna(subset=['y'], inplace=True) # Drop rows if 'y' became NaN print(f"--- Warning: Dropped {initial_rows - len(df_merged)} rows due to NaN in 'y' before fitting. ---")
if len(df_merged) < min_data_points: if len(df_merged) < min_data_points:
raise ValueError(f"Insufficient data ({len(df_merged)} rows) remaining for model fitting after processing.") raise ValueError(f"Insufficient data ({len(df_merged)} rows) remaining for model fitting after processing regressors.")
print(f"--- Fitting model on DataFrame with columns: {list(df_merged.columns)} ({len(df_merged)} rows) ---") print(f"Fitting model on DataFrame with columns: {list(df_merged.columns)} ({len(df_merged)} rows)")
model.fit(df_merged) model.fit(df_merged)
print("--- Making future dataframe... ---") print("Making future dataframe...")
future = model.make_future_dataframe(periods=24, freq='h') future = model.make_future_dataframe(periods=24, freq='h')
print("--- Populating future regressor values... ---") if 'volume' in model.regressors:
# <<< FIX: Use flags instead of checking model.regressors >>> future['volume'] = df_merged['volume'].iloc[-1] if not df_merged['volume'].empty else 0
if volume_regressor_added: if 'fear_greed' in model.regressors:
last_volume = df_merged['volume'].iloc[-1] if not df_merged['volume'].empty else 0 if current_fng_value is not None:
future['volume'] = last_volume
print(f" - Set future 'volume' to: {last_volume}")
# <<< FIX: Use flags instead of checking model.regressors >>>
if fng_regressor_added:
if current_fng_value is not None and pd.notna(current_fng_value):
future['fear_greed'] = float(current_fng_value) future['fear_greed'] = float(current_fng_value)
used_current_fng_for_future = True used_current_fng_for_future = True
print(f" - Set future 'fear_greed' to current value: {current_fng_value}.")
elif 'fear_greed' in df_merged.columns and not df_merged['fear_greed'].empty:
last_hist_fng = df_merged['fear_greed'].iloc[-1]
future['fear_greed'] = last_hist_fng
print(f" - Warning: Using last historical value ({last_hist_fng}) for future 'fear_greed'.")
else:
future['fear_greed'] = 50
print(" - Warning: Using 50 for future 'fear_greed'.")
if used_current_fng_for_future:
data_confidence_score += 5 data_confidence_score += 5
print(f"Data Confidence: Added Current F&G bonus. Score = {data_confidence_score}") else:
else: print("Data Confidence: No Current F&G bonus (used fallback).") future['fear_greed'] = df_merged['fear_greed'].iloc[-1] if not df_merged['fear_greed'].empty else 50
if 'reddit_sentiment' in model.regressors:
future['reddit_sentiment'] = reddit_sentiment['average_score'] if reddit_sentiment and 'average_score' in reddit_sentiment else 0
print("--- Predicting future prices... ---") print("Predicting...")
forecast = model.predict(future) forecast = model.predict(future)
print("--- Prediction complete. ---")
# --- Calculate Prediction Interval Penalty --- # Prediction Interval Penalty
if forecast is not None and len(forecast) >= len(df_merged) + 24: # Check full forecast length if forecast is not None and len(forecast) >= 24:
pred_24h_row = forecast.iloc[-1] pred_24h_row = forecast.iloc[-1]
yhat = pred_24h_row.get('yhat') yhat = pred_24h_row.get('yhat')
yhat_lower = pred_24h_row.get('yhat_lower') yhat_lower = pred_24h_row.get('yhat_lower')
yhat_upper = pred_24h_row.get('yhat_upper') yhat_upper = pred_24h_row.get('yhat_upper')
if pd.notna(yhat) and yhat != 0 and pd.notna(yhat_lower) and pd.notna(yhat_upper): if pd.notna(yhat) and yhat != 0 and pd.notna(yhat_lower) and pd.notna(yhat_upper):
interval_width = yhat_upper - yhat_lower interval_width = yhat_upper - yhat_lower
relative_width = abs(interval_width / yhat) relative_width = abs(interval_width / yhat)
print(f"Prediction Interval (24h): Width={interval_width:.2f}, Relative Width={relative_width:.3f}") if relative_width > 0.20:
if relative_width > 0.20: prediction_interval_penalty = 20 prediction_interval_penalty = 20
elif relative_width > 0.10: prediction_interval_penalty = 10 elif relative_width > 0.10:
elif relative_width > 0.05: prediction_interval_penalty = 5 prediction_interval_penalty = 10
else: prediction_interval_penalty = 0 elif relative_width > 0.05:
prediction_interval_penalty = 5
data_confidence_score -= prediction_interval_penalty data_confidence_score -= prediction_interval_penalty
print(f"Data Confidence: Applied Interval Penalty: -{prediction_interval_penalty}. Score = {data_confidence_score}") print(f"Data Confidence: Applied Interval Penalty: -{prediction_interval_penalty}. Score = {data_confidence_score}")
else: print("Data Confidence: Could not calculate interval penalty (missing forecast/interval values).")
# --- Extract specific point predictions --- # Extract predictions
if forecast is not None: if forecast is not None:
hist_len = len(df_merged) predicted_price_1h = forecast.iloc[-24].get('yhat') if len(forecast) >= 24 else None
# Check length before accessing iloc predicted_price_12h = forecast.iloc[-13].get('yhat') if len(forecast) >= 13 else None
if len(forecast) > hist_len: predicted_price_1h = forecast.iloc[hist_len].get('yhat') predicted_price_24h = forecast.iloc[-1].get('yhat') if len(forecast) >= 1 else None
if len(forecast) > hist_len + 11: predicted_price_12h = forecast.iloc[hist_len + 11].get('yhat')
if len(forecast) > hist_len: predicted_price_24h = forecast.iloc[-1].get('yhat') # Last row is 24h forecast
if predicted_price_1h is None or predicted_price_12h is None or predicted_price_24h is None:
if not error_message: error_message = "Prediction generated incomplete forecast."
print(f"!!! {error_message} Forecast length: {len(forecast)} !!!")
else: predicted_price_1h = predicted_price_12h = predicted_price_24h = None
except Exception as e: except Exception as e:
if not error_message: error_message = "Prediction model fitting or forecasting failed." if not error_message:
error_message = "Prediction model fitting or forecasting failed."
print(f"!!! {error_message}: {e} !!!") print(f"!!! {error_message}: {e} !!!")
traceback.print_exc() traceback.print_exc()
predicted_price_1h = predicted_price_12h = predicted_price_24h = None predicted_price_1h = predicted_price_12h = predicted_price_24h = None
forecast = None; model = None forecast = None
model = None
data_confidence_score = max(0, data_confidence_score - 30) data_confidence_score = max(0, data_confidence_score - 30)
# --- Clamp final confidence score --- # --- Clamp final confidence score ---
...@@ -271,45 +270,36 @@ def index(): ...@@ -271,45 +270,36 @@ def index():
print(f"\n--- Running Prophet cross-validation (initial='{initial_train_period}', period='{period_between_forecasts}', horizon='{forecast_horizon}') ---") print(f"\n--- Running Prophet cross-validation (initial='{initial_train_period}', period='{period_between_forecasts}', horizon='{forecast_horizon}') ---")
df_cv = cross_validation(model, initial=initial_train_period, period=period_between_forecasts, horizon=forecast_horizon, parallel="processes", disable_tqdm=True) df_cv = cross_validation(model, initial=initial_train_period, period=period_between_forecasts, horizon=forecast_horizon, parallel="processes", disable_tqdm=True)
df_p = performance_metrics(df_cv) df_p = performance_metrics(df_cv)
print("\nCross-Validation Performance Metrics (Sample):")
print(df_p[['horizon', 'mape', 'mae', 'rmse']].head())
avg_mape = df_p['mape'].mean() avg_mape = df_p['mape'].mean()
avg_mae = df_p['mae'].mean() avg_mae = df_p['mae'].mean()
if pd.notna(avg_mape) and pd.notna(avg_mae): accuracy_metrics = {'mape': avg_mape * 100, 'mae': avg_mae} if pd.notna(avg_mape) and pd.notna(avg_mae):
else: accuracy_metrics = None accuracy_metrics = {'mape': avg_mape * 100, 'mae': avg_mae}
if pd.notna(avg_mape): if pd.notna(avg_mape):
clamped_mape = max(0, min(avg_mape, 1.0)) clamped_mape = max(0, min(avg_mape, 1.0))
historical_accuracy_percent = (1 - clamped_mape) * 100 historical_accuracy_percent = (1 - clamped_mape) * 100
print(f"Calculated Historical Accuracy: {historical_accuracy_percent:.2f}% (based on Avg MAPE: {avg_mape:.4f})")
else:
print("--- Could not calculate historical accuracy (MAPE unavailable) ---")
historical_accuracy_percent = None
except Exception as cv_e: except Exception as cv_e:
print(f"!!! Cross-validation failed: {cv_e} !!!") print(f"!!! Cross-validation failed: {cv_e} !!!")
traceback.print_exc() traceback.print_exc()
if not error_message: error_message = "Accuracy cross-validation failed." if not error_message:
accuracy_metrics = None; historical_accuracy_percent = None error_message = "Accuracy cross-validation failed."
elif model is not None: accuracy_metrics = None
print(f"--- Skipping cross-validation due to insufficient data ({len(df_merged)} rows) ---") historical_accuracy_percent = None
if not error_message: error_message = "Not enough historical data for accuracy test."
accuracy_metrics = None; historical_accuracy_percent = None
# --- 4. CALCULATE PERCENTAGE CHANGES --- # --- 4. CALCULATE PERCENTAGE CHANGES ---
print("--- Calculating percentage changes ---") print("--- Calculating percentage changes ---")
if current_price is not None and current_price != 0: if current_price is not None and current_price != 0:
try: try:
if pd.notna(predicted_price_1h): percentage_change_1h = ((predicted_price_1h - current_price) / current_price) * 100 if predicted_price_1h is not None and pd.notna(predicted_price_1h):
if pd.notna(predicted_price_12h): percentage_change_12h = ((predicted_price_12h - current_price) / current_price) * 100 percentage_change_1h = ((predicted_price_1h - current_price) / current_price) * 100
if pd.notna(predicted_price_24h): percentage_change_24h = ((predicted_price_24h - current_price) / current_price) * 100 if predicted_price_12h is not None and pd.notna(predicted_price_12h):
percentage_change_12h = ((predicted_price_12h - current_price) / current_price) * 100
if predicted_price_24h is not None and pd.notna(predicted_price_24h):
percentage_change_24h = ((predicted_price_24h - current_price) / current_price) * 100
except Exception as e: except Exception as e:
print(f"!!! Error calculating percentage changes: {e} !!!") print(f"!!! Error calculating percentage changes: {e} !!!")
if not error_message: error_message = "Error calculating prediction changes."
percentage_change_1h = percentage_change_12h = percentage_change_24h = None percentage_change_1h = percentage_change_12h = percentage_change_24h = None
elif current_price == 0: print("Warning: Current price is 0, cannot calculate percentage change.")
else: print("Warning: Current price not available, cannot calculate percentage change.")
# --- 5. PREPARE DATA FOR PLOTTING --- # --- 5. PREPARE DATA FOR PLOTTING ---
plot_data = None
if df_merged is not None and forecast is not None: if df_merged is not None and forecast is not None:
try: try:
print("--- Preparing data for plotting ---") print("--- Preparing data for plotting ---")
...@@ -318,12 +308,14 @@ def index(): ...@@ -318,12 +308,14 @@ def index():
if len(forecast) > len(df_merged): if len(forecast) > len(df_merged):
future_plot_points = forecast.iloc[len(df_merged):][['ds', 'yhat', 'yhat_lower', 'yhat_upper']].copy() future_plot_points = forecast.iloc[len(df_merged):][['ds', 'yhat', 'yhat_lower', 'yhat_upper']].copy()
else: else:
print("--- Warning: Forecast length not greater than merged data length. No future points to plot. ---")
future_plot_points = pd.DataFrame(columns=['ds', 'yhat', 'yhat_lower', 'yhat_upper']) future_plot_points = pd.DataFrame(columns=['ds', 'yhat', 'yhat_lower', 'yhat_upper'])
hist_dates = history_to_plot['ds'] hist_dates = history_to_plot['ds']
future_dates = future_plot_points['ds'] future_dates = future_plot_points['ds']
all_dates_dt = pd.concat([hist_dates, future_dates], ignore_index=True).sort_values() if not future_dates.empty else hist_dates.sort_values() if not future_dates.empty:
all_dates_dt = pd.concat([pd.Series(hist_dates), pd.Series(future_dates)], ignore_index=True).sort_values()
else:
all_dates_dt = hist_dates.sort_values()
all_dates_str = all_dates_dt.dt.strftime('%Y-%m-%dT%H:%M:%S').unique().tolist() all_dates_str = all_dates_dt.dt.strftime('%Y-%m-%dT%H:%M:%S').unique().tolist()
hist_values_dict = history_to_plot.set_index(history_to_plot['ds'].dt.strftime('%Y-%m-%dT%H:%M:%S'))['y'] hist_values_dict = history_to_plot.set_index(history_to_plot['ds'].dt.strftime('%Y-%m-%dT%H:%M:%S'))['y']
...@@ -335,9 +327,7 @@ def index(): ...@@ -335,9 +327,7 @@ def index():
pred_upper = [pred_values_dict.get(dt, {}).get('yhat_upper') for dt in all_dates_str] pred_upper = [pred_values_dict.get(dt, {}).get('yhat_upper') for dt in all_dates_str]
def round_if_not_none(val, digits=2): def round_if_not_none(val, digits=2):
if val is None or (isinstance(val, (float, int)) and math.isnan(float(val))): return None return round(val, digits) if pd.notna(val) else None
try: return round(float(val), digits)
except (ValueError, TypeError): return None
plot_data = { plot_data = {
"labels": all_dates_str, "labels": all_dates_str,
...@@ -346,40 +336,39 @@ def index(): ...@@ -346,40 +336,39 @@ def index():
"pred_lower": [round_if_not_none(v) for v in pred_lower], "pred_lower": [round_if_not_none(v) for v in pred_lower],
"pred_upper": [round_if_not_none(v) for v in pred_upper] "pred_upper": [round_if_not_none(v) for v in pred_upper]
} }
print(f"--- Prepared plot_data with {len(all_dates_str)} labels. ---")
except Exception as plot_e: except Exception as plot_e:
print(f"!!! Error preparing plot data: {plot_e} !!!") print(f"!!! Error preparing plot data: {plot_e} !!!")
traceback.print_exc() traceback.print_exc()
plot_data = None plot_data = None
else:
print("--- Skipping plot data preparation (missing model data or forecast) ---")
plot_data = None
# --- 6. RENDER TEMPLATE --- # --- 6. FORMAT VARIABLES FOR DISPLAY ---
current_price_display = "${:.2f}".format(current_price) if current_price is not None else "N/A"
predicted_price_1h_display = "${:.2f}".format(predicted_price_1h) if predicted_price_1h is not None else "N/A"
predicted_price_12h_display = "${:.2f}".format(predicted_price_12h) if predicted_price_12h is not None else "N/A"
predicted_price_24h_display = "${:.2f}".format(predicted_price_24h) if predicted_price_24h is not None else "N/A"
percentage_change_1h_display = "+{:.2f}%".format(percentage_change_1h) if percentage_change_1h is not None and percentage_change_1h > 0 else ("{:.2f}%".format(percentage_change_1h) if percentage_change_1h is not None else "N/A")
percentage_change_12h_display = "+{:.2f}%".format(percentage_change_12h) if percentage_change_12h is not None and percentage_change_12h > 0 else ("{:.2f}%".format(percentage_change_12h) if percentage_change_12h is not None else "N/A")
percentage_change_24h_display = "+{:.2f}%".format(percentage_change_24h) if percentage_change_24h is not None and percentage_change_24h > 0 else ("{:.2f}%".format(percentage_change_24h) if percentage_change_24h is not None else "N/A")
# --- 7. RENDER TEMPLATE ---
processing_time = (datetime.now() - start_time).total_seconds() processing_time = (datetime.now() - start_time).total_seconds()
print(f"--- Request processing finished in {processing_time:.2f} seconds. Rendering template... ---") print(f"--- Request processing finished in {processing_time:.2f} seconds. Rendering template... ---")
return render_template('dashboard.html', return render_template('dashboard.html',
symbol=symbol, symbol=symbol,
current_price=current_price, current_price_display=current_price_display,
current_fng_value=current_fng_value, current_fng_value=current_fng_value,
predicted_price_1h=predicted_price_1h, predicted_price_1h_display=predicted_price_1h_display,
predicted_price_12h=predicted_price_12h, predicted_price_12h_display=predicted_price_12h_display,
predicted_price_24h=predicted_price_24h, predicted_price_24h_display=predicted_price_24h_display,
percentage_change_1h=percentage_change_1h, percentage_change_1h_display=percentage_change_1h_display,
percentage_change_12h=percentage_change_12h, percentage_change_12h_display=percentage_change_12h_display,
percentage_change_24h=percentage_change_24h, percentage_change_24h_display=percentage_change_24h_display,
accuracy_metrics=accuracy_metrics,
historical_accuracy_percent=historical_accuracy_percent,
data_confidence_score=data_confidence_score, data_confidence_score=data_confidence_score,
plot_data=plot_data, plot_data=plot_data,
sources=sources,
error_message=error_message, error_message=error_message,
processing_time=f"{processing_time:.2f} sec", processing_time=f"{processing_time:.2f} sec",
last_updated=datetime.now().strftime("%Y-%m-%d %H:%M:%S %Z") last_updated=datetime.now().strftime("%Y-%m-%d %H:%M:%S %Z"))
)
# --- Main execution block ---
if __name__ == '__main__': if __name__ == '__main__':
# Use host='0.0.0.0' to make accessible on local network
# Port 5000 is default, change if needed
# Debug=True provides more error detail and auto-reloads on code changes
app.run(debug=True, host='0.0.0.0', port=5000) app.run(debug=True, host='0.0.0.0', port=5000)
\ No newline at end of file
This diff is collapsed.
static/images/Reallogo.png

28.3 KiB

/* static/styles.css */
/* --- Global Styles & Fonts --- */
@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700&display=swap');
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body { body {
font-family: 'Poppins', sans-serif; margin: 0;
background-color: #f4f7f6; /* Light grey background */ font-family: 'Roboto', Arial, sans-serif;
color: #333; background-color: #f5f7fa;
line-height: 1.6; color: #0b527a;
}
.container {
max-width: 1300px;
margin: 0 auto;
padding: 20px;
}
h1, h2, h3, h4, h5, h6 {
color: #2c3e50; /* Dark blue-grey */
margin-bottom: 0.75em;
margin-top: 1.25em;
font-weight: 600;
} }
h1 { font-size: 2.2rem; text-align: center; margin-top: 0.5em;} /* Splash Screen */
h2 { font-size: 1.6rem; border-bottom: 2px solid #e0e0e0; padding-bottom: 0.4em; margin-top: 2em;}
h3 { font-size: 1.3rem; font-weight: 500; color: #34495e; }
p { margin-bottom: 1em; color: #555; }
a { color: #3498db; text-decoration: none; transition: color 0.3s ease; }
a:hover { color: #2980b9; }
hr { border: 0; border-top: 1px solid #eee; margin: 2em 0; }
small { font-size: 0.85em; color: #777; }
/* --- Splash Screen --- */
.splash-screen { .splash-screen {
display: flex;
align-items: center;
justify-content: center;
position: fixed; position: fixed;
top: 0; top: 0;
left: 0; left: 0;
width: 100%; width: 100%;
height: 100%; height: 100%;
/* Gradient background or solid color */ background-color: #0b527a;
background: linear-gradient(135deg, #ffffff 0%, #e8f0f5 100%); z-index: 1000;
/* background-color: #fff; */ transition: opacity 1.5s ease, visibility 1.5s ease;
display: flex;
justify-content: center;
align-items: center;
z-index: 10000; /* Ensure it's on top */
opacity: 1;
visibility: visible;
transition: opacity 0.3s ease-out, visibility 0s linear 0.3s; /* Match JS timing */
} }
.splash-logo { .splash-logo {
width: 250px; /* Adjust size as needed */ width: 300px;
max-width: 70%; animation: fadeIn 1.5s ease;
height: auto;
opacity: 0;
transform: scale(0.8);
animation: splashFadeIn 1s ease-out 0.3s forwards, splashPulse 1.5s infinite alternate 1.3s;
}
@keyframes splashFadeIn {
to {
opacity: 1;
transform: scale(1);
}
} }
@keyframes splashPulse { @keyframes fadeIn {
from { transform: scale(1); } 0% { opacity: 0; }
to { transform: scale(1.03); } 100% { opacity: 1; }
} }
/* Header */
/* --- Header --- */
.main-header { .main-header {
background: #ffffff; /* White header */
padding: 15px 30px;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.08); background-color: #0b527a;
position: sticky; padding: 1rem 2rem;
top: 0; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
z-index: 1000; /* Below splash, above content */
border-bottom: 1px solid #eee;
} }
.logo-container { .logo img {
display: flex; height: 50px;
align-items: center;
} }
.header-logo { .dropdown {
height: 45px; /* Adjust as needed */ position: relative;
width: auto;
display: block; /* Prevents extra space below image */
} }
.header-title { .dropdown-btn {
font-size: 1.5rem; background: #fff;
font-weight: 600; border: none;
color: #34495e; /* Darker blue-grey */ padding: 0.5rem 1rem;
margin-left: 15px; font-size: 1rem;
cursor: pointer;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
transition: background 0.3s ease;
} }
.header-info span { .dropdown-btn:hover {
font-weight: 500; background: #f5f7fa;
color: #555;
} }
/* --- Main Content Sections --- */ .dropdown-content {
section { display: none;
background-color: #ffffff; position: absolute;
padding: 25px; right: 0;
background-color: #fff;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
border-radius: 8px; border-radius: 8px;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.06); overflow: hidden;
margin-bottom: 30px;
}
/* --- Status Section --- */
.status-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
margin-top: 15px;
} }
.status-grid > div { .dropdown:hover .dropdown-content {
background-color: #f8f9fa; display: block;
padding: 15px;
border-radius: 6px;
border: 1px solid #e9ecef;
} }
.status-grid .label { .dropdown-content a {
display: block; display: block;
font-size: 0.9rem; padding: 0.5rem 1rem;
color: #6c757d; color: #0b527a;
margin-bottom: 5px; text-decoration: none;
font-weight: 500; transition: background 0.3s ease;
} }
.status-grid .value { .dropdown-content a:hover {
font-size: 1.2rem; background-color: #6ba2c2;
font-weight: 600; color: #fff;
color: #343a40;
} }
.status-grid .date-value {
font-size: 1rem;
font-weight: 500;
}
/* --- Chart Section --- */ /* Main Content */
.chart-container { .current-status, .predictions, .data-sources {
position: relative; padding: 1rem 2rem;
height: 450px; /* Adjust height as needed */
width: 100%;
margin-top: 15px;
} }
.chart-error {
text-align: center; .current-status h2, .predictions h3, .data-sources h3 {
color: #dc3545; color: #0b527a;
font-weight: 500; border-bottom: 2px solid #6ba2c2;
padding: 20px; padding-bottom: 0.5rem;
} }
/* --- Scores Section --- */ .graphs-section {
.scores-grid { display: flex;
display: grid; justify-content: space-around;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); padding: 2rem;
gap: 25px; gap: 1rem;
margin-top: 15px;
} }
.score-card { .graph {
background-color: #f8f9fa; width: 45%;
padding: 20px; padding: 1rem;
border-radius: 6px; border: 1px solid #6ba2c2;
text-align: center; border-radius: 8px;
border: 1px solid #e9ecef; background-color: #fff;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
transition: transform 0.3s ease;
} }
.score-card h3 { .graph:hover {
margin-top: 0; transform: scale(1.02);
margin-bottom: 10px;
font-size: 1.1rem;
color: #495057;
} }
.score-value { .graph h3 {
font-size: 2rem; text-align: center;
font-weight: 600; margin-bottom: 1rem;
color: #2c3e50; color: #0b527a;
margin-bottom: 5px;
} }
.score-desc { .graph canvas {
font-size: 0.85rem; height: 300px !important;
color: #6c757d;
margin-bottom: 0;
} }
/* --- Predictions Section --- */ .predictions .prediction-item {
.predictions-grid { margin: 0.5rem 0;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
margin-top: 15px;
} }
.prediction-card { .prediction-score {
background-color: #f8f9fa;
padding: 15px;
border-radius: 6px;
text-align: center; text-align: center;
border: 1px solid #e9ecef; font-size: 1.2rem;
margin: 1rem 0;
color: #6ba2c2;
} }
.prediction-card h3 { .prediction-score small {
margin-top: 0; color: #0b527a;
margin-bottom: 10px;
font-size: 1rem;
font-weight: 500;
} }
.prediction-price { /* Data Sources */
font-size: 1.4rem; .data-sources ul {
font-weight: 600; list-style: none;
margin-bottom: 5px; padding: 0;
color: #343a40; max-height: 200px;
overflow-y: auto;
border: 1px solid #6ba2c2;
border-radius: 8px;
background-color: #fff;
} }
.prediction-change { .data-sources li {
font-size: 1.1rem; padding: 0.5rem 1rem;
font-weight: 500; border-bottom: 1px solid #f5f7fa;
margin-bottom: 0; transition: background 0.3s ease;
} }
.prediction-change.positive { color: #28a745; } .data-sources li:last-child {
.prediction-change.negative { color: #dc3545; } border-bottom: none;
/* --- Sources Section --- */
.sources-section ul {
list-style-type: none;
padding-left: 0;
} }
.sources-section li { .data-sources li:hover {
background-color: #f8f9fa; background-color: #6ba2c2;
margin-bottom: 8px; color: #fff;
padding: 10px 15px;
border-radius: 4px;
border: 1px solid #e9ecef;
font-weight: 500;
} }
/* --- Banners & Disclaimers --- */ /* Error Message */
.error-banner { .error-message {
background-color: #f8d7da; background-color: #ffe6e6;
color: #721c24; color: #d8000c;
border: 1px solid #f5c6cb; padding: 1rem;
padding: 15px; margin: 1rem 2rem;
border-radius: 8px; border-radius: 8px;
margin-bottom: 25px; text-align: center;
} }
.error-banner p { margin-bottom: 0; }
.disclaimer { /* Footer */
.footer {
text-align: center; text-align: center;
margin-top: 30px; padding: 1rem;
font-style: italic; margin-top: 2rem;
color: #6c757d; border-top: 1px solid #6ba2c2;
color: #0b527a;
font-size: 0.9rem; font-size: 0.9rem;
} }
\ No newline at end of file
/* --- Responsive Adjustments --- */
@media (max-width: 768px) {
h1 { font-size: 1.8rem; }
h2 { font-size: 1.4rem; }
.main-header { padding: 10px 15px; flex-direction: column; gap: 10px;}
.header-title { font-size: 1.3rem; }
.header-logo { height: 40px; }
.container { padding: 15px; }
section { padding: 20px; }
.chart-container { height: 350px; }
.status-grid, .scores-grid, .predictions-grid { grid-template-columns: 1fr; } /* Stack on smaller screens */
}
This diff is collapsed.
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please to comment