Spaces:
Sleeping
Sleeping
Upload 6 files
Browse files- core/data (1).py +345 -0
- core/model_runner (2).py +144 -0
- core/models (1).py +219 -0
- core/plot (9).py +490 -0
- core/signals (1).py +142 -0
- core/train_eval (3).py +324 -0
core/data (1).py
ADDED
|
@@ -0,0 +1,345 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import pandas as pd
|
| 2 |
+
import numpy as np
|
| 3 |
+
import yfinance as yf
|
| 4 |
+
import os
|
| 5 |
+
try:
|
| 6 |
+
import talib as ta
|
| 7 |
+
except ImportError:
|
| 8 |
+
ta = None
|
| 9 |
+
from datetime import datetime, timedelta
|
| 10 |
+
from newsapi import NewsApiClient
|
| 11 |
+
from vaderSentiment.vaderSentiment import SentimentIntensityAnalyzer
|
| 12 |
+
from sklearn.preprocessing import MinMaxScaler
|
| 13 |
+
from alpha_vantage.timeseries import TimeSeries
|
| 14 |
+
import time
|
| 15 |
+
import logging
|
| 16 |
+
|
| 17 |
+
# Configure logging
|
| 18 |
+
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
| 19 |
+
|
| 20 |
+
def print_log(message, level='INFO'):
|
| 21 |
+
if level == 'INFO':
|
| 22 |
+
logging.info(message)
|
| 23 |
+
elif level == 'WARNING':
|
| 24 |
+
logging.warning(message)
|
| 25 |
+
elif level == 'ERROR':
|
| 26 |
+
logging.error(message)
|
| 27 |
+
else:
|
| 28 |
+
logging.debug(message)
|
| 29 |
+
|
| 30 |
+
analyzer = SentimentIntensityAnalyzer()
|
| 31 |
+
|
| 32 |
+
def load_data(data_src='yahoo', ticker='AAPL', start='2020-01-01', end='2023-01-01', interval='1d', file_upload=None, alpha_api_key=None):
|
| 33 |
+
try:
|
| 34 |
+
print_log(f"Loading data: source={data_src}, ticker={ticker}, start={start}, end={end}, interval={interval}, file_upload={'set' if file_upload else 'unset'}, alpha_api_key={'set' if alpha_api_key else 'unset'}")
|
| 35 |
+
|
| 36 |
+
start_date = pd.to_datetime(start)
|
| 37 |
+
end_date = pd.to_datetime(end)
|
| 38 |
+
if start_date >= end_date:
|
| 39 |
+
raise ValueError(f"Start date {start} must be before end date {end}")
|
| 40 |
+
if end_date > datetime.now():
|
| 41 |
+
print_log(f"End date {end} is in the future. Using current date as end date.", 'WARNING')
|
| 42 |
+
end_date = datetime.now()
|
| 43 |
+
|
| 44 |
+
df = pd.DataFrame()
|
| 45 |
+
|
| 46 |
+
if data_src == 'csv' and file_upload:
|
| 47 |
+
try:
|
| 48 |
+
file_path = getattr(file_upload, 'name', file_upload)
|
| 49 |
+
print_log(f"Loading CSV from {file_path}")
|
| 50 |
+
df = pd.read_csv(file_path)
|
| 51 |
+
if 'Date' not in df.columns:
|
| 52 |
+
raise ValueError("CSV must contain a 'Date' column")
|
| 53 |
+
df['Date'] = pd.to_datetime(df['Date']).dt.tz_localize(None)
|
| 54 |
+
df = df.set_index('Date')
|
| 55 |
+
if 'Close' not in df.columns and 'value' not in df.columns:
|
| 56 |
+
raise ValueError("CSV must contain 'Close' or 'value' column")
|
| 57 |
+
if 'Close' in df.columns:
|
| 58 |
+
df = df.rename(columns={'Close': 'value'})
|
| 59 |
+
if df.empty:
|
| 60 |
+
raise ValueError(f"CSV data is empty for {ticker}")
|
| 61 |
+
if df['value'].isna().all():
|
| 62 |
+
raise ValueError(f"CSV 'value' column contains only NaNs for {ticker}")
|
| 63 |
+
except Exception as e:
|
| 64 |
+
print_log(f"Failed to load CSV {file_path}: {str(e)}", 'ERROR')
|
| 65 |
+
raise ValueError(f"Failed to load CSV: {str(e)}")
|
| 66 |
+
else:
|
| 67 |
+
print_log(f"Fetching data for {ticker} from Yahoo Finance")
|
| 68 |
+
try:
|
| 69 |
+
df = yf.download(ticker, start=start_date, end=end_date, interval=interval, progress=False, auto_adjust=False)
|
| 70 |
+
if isinstance(df.columns, pd.MultiIndex):
|
| 71 |
+
df.columns = df.columns.droplevel(1)
|
| 72 |
+
if df.empty:
|
| 73 |
+
raise ValueError(f"No data returned from Yahoo Finance for {ticker}")
|
| 74 |
+
if 'Close' not in df.columns:
|
| 75 |
+
raise ValueError(f"Yahoo Finance data missing 'Close' column for {ticker}")
|
| 76 |
+
df = df.rename(columns={'Close': 'value'})
|
| 77 |
+
# The index is already datetime, no need to create a 'Date' column and then reset
|
| 78 |
+
if df['value'].isna().all():
|
| 79 |
+
raise ValueError(f"Yahoo Finance 'value' column contains only NaNs for {ticker}")
|
| 80 |
+
if df['value'].empty:
|
| 81 |
+
raise ValueError(f"Yahoo Finance 'value' column is empty for {ticker}")
|
| 82 |
+
except Exception as e:
|
| 83 |
+
print_log(f"Yahoo Finance failed for {ticker}: {str(e)}", 'ERROR')
|
| 84 |
+
raise ValueError(f"Yahoo Finance failed for {ticker}: {str(e)}")
|
| 85 |
+
|
| 86 |
+
# Alpha Vantage data loading (if applicable)
|
| 87 |
+
# Note: Alpha Vantage data loading logic is commented out for now to simplify debugging
|
| 88 |
+
# if interval in ['1m', '5m', '15m', '30m', '60m'] and alpha_api_key:
|
| 89 |
+
# print_log(f"Attempting Alpha Vantage for {ticker}, interval {interval}")
|
| 90 |
+
# try:
|
| 91 |
+
# ts = TimeSeries(key=alpha_api_key, output_format='pandas')
|
| 92 |
+
# df_av, _ = ts.get_intraday(symbol=ticker, interval=interval, outputsize='full')
|
| 93 |
+
# if df_av.empty:
|
| 94 |
+
# raise ValueError(f"No data returned from Alpha Vantage for {ticker}")
|
| 95 |
+
# if '4. close' not in df_av.columns:
|
| 96 |
+
# raise ValueError(f"Alpha Vantage data missing '4. close' column for {ticker}")
|
| 97 |
+
# df_av = df_av.rename(columns={'4. close': 'value', '1. open': 'Open', '2. high': 'High', '3. low': 'Low', '5. volume': 'Volume'}) # Standardize column names
|
| 98 |
+
# df_av['Date'] = pd.to_datetime(df_av.index)
|
| 99 |
+
# df_av = df_av.reset_index(drop=True)
|
| 100 |
+
# if df_av['value'].isna().all():
|
| 101 |
+
# raise ValueError(f"Alpha Vantage 'value' column contains only NaNs for {ticker}")
|
| 102 |
+
# if df_av['value'].empty:
|
| 103 |
+
# raise ValueError(f"Alpha Vantage 'value' column is empty for {ticker}")
|
| 104 |
+
# df = df_av # Use Alpha Vantage data if successful
|
| 105 |
+
# except Exception as e:
|
| 106 |
+
# print_log(f"Alpha Vantage failed: {str(e)}, using Yahoo Finance data", 'WARNING')
|
| 107 |
+
|
| 108 |
+
if df.empty:
|
| 109 |
+
raise ValueError(f"No data loaded for {ticker} from {data_src}")
|
| 110 |
+
|
| 111 |
+
# Ensure index is DatetimeIndex and sorted
|
| 112 |
+
if not isinstance(df.index, pd.DatetimeIndex):
|
| 113 |
+
df.index = pd.to_datetime(df.index)
|
| 114 |
+
df = df.sort_index()
|
| 115 |
+
|
| 116 |
+
required_cols = ['Open', 'High', 'Low', 'value', 'Volume']
|
| 117 |
+
for col in required_cols:
|
| 118 |
+
if col not in df.columns:
|
| 119 |
+
df[col] = np.nan # Add missing columns with NaNs
|
| 120 |
+
|
| 121 |
+
if 'value' not in df.columns:
|
| 122 |
+
raise ValueError(f"Target column 'value' is missing for {ticker}")
|
| 123 |
+
if df['value'].isna().all():
|
| 124 |
+
raise ValueError(f"Target column 'value' contains only NaNs for {ticker}")
|
| 125 |
+
if df['value'].empty:
|
| 126 |
+
raise ValueError(f"Target column 'value' is empty for {ticker}")
|
| 127 |
+
|
| 128 |
+
print_log(f"Data loaded for {ticker} with date range: {df.index.min()} to {df.index.max()}, shape: {df.shape}")
|
| 129 |
+
return df
|
| 130 |
+
except Exception as e:
|
| 131 |
+
print_log(f"Error in load_data for {ticker}: {str(e)}", 'ERROR')
|
| 132 |
+
raise ValueError(f"Failed to load data for {ticker}: {str(e)}")
|
| 133 |
+
|
| 134 |
+
def add_technical_indicators(df, selected_indicators):
|
| 135 |
+
try:
|
| 136 |
+
print_log(f"Starting add_technical_indicators with indicators: {selected_indicators}")
|
| 137 |
+
if df.empty:
|
| 138 |
+
print_log("DataFrame is empty, skipping technical indicator calculation.", "WARNING")
|
| 139 |
+
return df, []
|
| 140 |
+
|
| 141 |
+
# Ensure columns are numeric and handle missing ones
|
| 142 |
+
for col in ['Open', 'High', 'Low', 'value', 'Volume']:
|
| 143 |
+
if col not in df.columns:
|
| 144 |
+
df[col] = np.nan
|
| 145 |
+
df[col] = pd.to_numeric(df[col], errors='coerce')
|
| 146 |
+
|
| 147 |
+
# Drop rows with NaN in core columns after indicator calculation
|
| 148 |
+
df.dropna(subset=['Open', 'High', 'Low', 'value', 'Volume'], inplace=True)
|
| 149 |
+
if df.empty:
|
| 150 |
+
print_log("DataFrame is empty after dropping NaNs for technical indicators.", "WARNING")
|
| 151 |
+
return df, []
|
| 152 |
+
|
| 153 |
+
if ta is None:
|
| 154 |
+
print_log("TA-Lib not available. Cannot compute indicators. Falling back to 'value'.", 'ERROR')
|
| 155 |
+
return df, []
|
| 156 |
+
|
| 157 |
+
close = df['value'].values
|
| 158 |
+
high = df['High'].values
|
| 159 |
+
low = df['Low'].values
|
| 160 |
+
volume = df['Volume'].values
|
| 161 |
+
open_ = df['Open'].values
|
| 162 |
+
|
| 163 |
+
indicator_map = {
|
| 164 |
+
'rsi': {'func': ta.RSI, 'inputs': ['close'], 'params': {'timeperiod': 14}, 'output': ['rsi_14']},
|
| 165 |
+
'macd': {'func': ta.MACD, 'inputs': ['close'], 'params': {'fastperiod': 12, 'slowperiod': 26, 'signalperiod': 9}, 'output': ['macd_12_26_9', 'macds_12_26_9', 'macdh_12_26_9']},
|
| 166 |
+
'bbands': {'func': ta.BBANDS, 'inputs': ['close'], 'params': {'timeperiod': 20, 'nbdevup': 2, 'nbdevdn': 2}, 'output': ['bbu_20_2.0', 'bbm_20_2.0', 'bbl_20_2.0']},
|
| 167 |
+
'sma': {'func': ta.SMA, 'inputs': ['close'], 'params': {'timeperiod': 20}, 'output': ['sma_20']},
|
| 168 |
+
'ema': {'func': ta.EMA, 'inputs': ['close'], 'params': {'timeperiod': 20}, 'output': ['ema_20']},
|
| 169 |
+
'atr': {'func': ta.ATR, 'inputs': ['high', 'low', 'close'], 'params': {'timeperiod': 14}, 'output': ['atr_14']},
|
| 170 |
+
'stoch': {'func': ta.STOCH, 'inputs': ['high', 'low', 'close'], 'params': {'fastk_period': 14, 'slowk_period': 3, 'slowd_period': 3}, 'output': ['stochk_14_3_3', 'stochd_14_3_3']},
|
| 171 |
+
'adx': {'func': ta.ADX, 'inputs': ['high', 'low', 'close'], 'params': {'timeperiod': 14}, 'output': ['adx_14']},
|
| 172 |
+
'willr': {'func': ta.WILLR, 'inputs': ['high', 'low', 'close'], 'params': {'timeperiod': 14}, 'output': ['willr_14']},
|
| 173 |
+
'cci': {'func': ta.CCI, 'inputs': ['high', 'low', 'close'], 'params': {'timeperiod': 20}, 'output': ['cci_20']},
|
| 174 |
+
'pdi': {'func': ta.PLUS_DI, 'inputs': ['high', 'low', 'close'], 'params': {'timeperiod': 14}, 'output': ['pdi_14']},
|
| 175 |
+
'mdi': {'func': ta.MINUS_DI, 'inputs': ['high', 'low', 'close'], 'params': {'timeperiod': 14}, 'output': ['mdi_14']}
|
| 176 |
+
}
|
| 177 |
+
|
| 178 |
+
input_dict = {'close': close, 'high': high, 'low': low, 'open': open_, 'volume': volume}
|
| 179 |
+
|
| 180 |
+
valid_indicators = []
|
| 181 |
+
for ind in selected_indicators:
|
| 182 |
+
if ind in indicator_map:
|
| 183 |
+
print_log(f"Computing indicator: {ind}")
|
| 184 |
+
config = indicator_map[ind]
|
| 185 |
+
func = config['func']
|
| 186 |
+
inputs = config['inputs']
|
| 187 |
+
params = config['params']
|
| 188 |
+
try:
|
| 189 |
+
input_arrays = [input_dict[inp] for inp in inputs]
|
| 190 |
+
result = func(*input_arrays, **params)
|
| 191 |
+
if isinstance(result, tuple):
|
| 192 |
+
for j, (res, out_col) in enumerate(zip(result, config['output'])):
|
| 193 |
+
if isinstance(res, np.ndarray) and len(res) == len(df):
|
| 194 |
+
df[out_col] = res
|
| 195 |
+
nan_count = np.isnan(res).sum()
|
| 196 |
+
if nan_count < len(res) * 0.5:
|
| 197 |
+
valid_indicators.append(out_col)
|
| 198 |
+
else:
|
| 199 |
+
print_log(f"{out_col} has excessive NaNs: {nan_count}/{len(res)}. Excluding from valid indicators.", 'WARNING')
|
| 200 |
+
else:
|
| 201 |
+
print_log(f"Invalid output for {out_col}: {type(res)}, length: {len(res) if hasattr(res, '__len__') else 'N/A'}", 'WARNING')
|
| 202 |
+
else:
|
| 203 |
+
if isinstance(result, np.ndarray) and len(result) == len(df):
|
| 204 |
+
df[config['output'][0]] = result
|
| 205 |
+
nan_count = np.isnan(result).sum()
|
| 206 |
+
if nan_count < len(result) * 0.5:
|
| 207 |
+
valid_indicators.append(config['output'][0])
|
| 208 |
+
else:
|
| 209 |
+
print_log(f"{config['output'][0]} has excessive NaNs: {nan_count}/{len(result)}. Excluding from valid indicators.", 'WARNING')
|
| 210 |
+
else:
|
| 211 |
+
print_log(f"Invalid output for {ind}: {type(result)}, length: {len(result) if hasattr(result, '__len__') else 'N/A'}", 'WARNING')
|
| 212 |
+
except Exception as e:
|
| 213 |
+
print_log(f"Error computing {ind}: {str(e)}", 'ERROR')
|
| 214 |
+
else:
|
| 215 |
+
print_log(f"Indicator {ind} not supported by TA-Lib", 'WARNING')
|
| 216 |
+
|
| 217 |
+
# Drop rows with NaN in 'value', preserve valid indicators
|
| 218 |
+
initial_rows = len(df)
|
| 219 |
+
df = df.dropna(subset=['value']).reset_index(drop=False) # Keep index as a column for now
|
| 220 |
+
print_log(f"Dropped {initial_rows - len(df)} rows with NaN in 'value'")
|
| 221 |
+
|
| 222 |
+
# Drop columns with excessive NaNs, but protect 'value'
|
| 223 |
+
for col in df.columns:
|
| 224 |
+
if col not in ['Date', 'Open', 'High', 'Low', 'value', 'Volume']:
|
| 225 |
+
nan_ratio = df[col].isna().mean()
|
| 226 |
+
if nan_ratio > 0.5:
|
| 227 |
+
print_log(f"Dropping {col} due to excessive NaNs: {nan_ratio:.2%}", 'WARNING')
|
| 228 |
+
df = df.drop(columns=[col])
|
| 229 |
+
if col in valid_indicators:
|
| 230 |
+
valid_indicators.remove(col)
|
| 231 |
+
|
| 232 |
+
if not valid_indicators:
|
| 233 |
+
print_log("No valid indicators computed. Falling back to 'value'.", "WARNING")
|
| 234 |
+
valid_indicators.append('value')
|
| 235 |
+
|
| 236 |
+
print_log(f"Valid indicators: {valid_indicators}")
|
| 237 |
+
print_log(f"Technical indicators added successfully, shape: {df.shape}")
|
| 238 |
+
df.set_index('Date', inplace=True) # Set index back to Date after all processing
|
| 239 |
+
return df, valid_indicators
|
| 240 |
+
except Exception as e:
|
| 241 |
+
print_log(f"Error in add_technical_indicators: {str(e)}", 'ERROR')
|
| 242 |
+
# If an error occurs, return the original DataFrame to prevent further errors
|
| 243 |
+
return df, []
|
| 244 |
+
|
| 245 |
+
def add_sentiment(df, ticker, news_api_key, start_date, end_date):
|
| 246 |
+
try:
|
| 247 |
+
print_log(f"Starting add_sentiment for {ticker} from {start_date} to {end_date}")
|
| 248 |
+
if not news_api_key:
|
| 249 |
+
print_log("News API key not provided. Skipping sentiment analysis.", "WARNING")
|
| 250 |
+
df['sentiment_score'] = 0.0
|
| 251 |
+
return df
|
| 252 |
+
|
| 253 |
+
newsapi = NewsApiClient(api_key=news_api_key)
|
| 254 |
+
all_articles = []
|
| 255 |
+
current_date = pd.to_datetime(start_date)
|
| 256 |
+
end_date = pd.to_datetime(end_date)
|
| 257 |
+
|
| 258 |
+
while current_date <= end_date:
|
| 259 |
+
from_param = current_date.strftime("%Y-%m-%d")
|
| 260 |
+
to_param = (current_date + timedelta(days=1)).strftime("%Y-%m-%d")
|
| 261 |
+
print_log(f"Fetching news for {ticker} from {from_param} to {to_param}")
|
| 262 |
+
try:
|
| 263 |
+
articles = newsapi.get_everything(q=ticker, language='en', sort_by='relevancy', from_param=from_param, to=to_param)
|
| 264 |
+
all_articles.extend(articles["articles"])
|
| 265 |
+
except Exception as e:
|
| 266 |
+
print_log(f"Error fetching news for {ticker} on {from_param}: {str(e)}", 'ERROR')
|
| 267 |
+
current_date += timedelta(days=1)
|
| 268 |
+
time.sleep(0.1)
|
| 269 |
+
|
| 270 |
+
if not all_articles:
|
| 271 |
+
print_log(f"No articles found for {ticker}. Setting sentiment to 0.", 'WARNING')
|
| 272 |
+
df['sentiment_score'] = 0.0
|
| 273 |
+
return df
|
| 274 |
+
|
| 275 |
+
sentiment_data = []
|
| 276 |
+
for article in all_articles:
|
| 277 |
+
if article["publishedAt"] and article["description"]:
|
| 278 |
+
date = pd.to_datetime(article["publishedAt"]).tz_localize(None).date()
|
| 279 |
+
text = article["description"]
|
| 280 |
+
vs = analyzer.polarity_scores(text)
|
| 281 |
+
sentiment_data.append({"Date": date, "sentiment_score": vs["compound"]})
|
| 282 |
+
|
| 283 |
+
sentiment_df = pd.DataFrame(sentiment_data)
|
| 284 |
+
sentiment_df["Date"] = pd.to_datetime(sentiment_df["Date"])
|
| 285 |
+
sentiment_df = sentiment_df.groupby("Date")["sentiment_score"].mean().reset_index()
|
| 286 |
+
|
| 287 |
+
df.reset_index(inplace=True)
|
| 288 |
+
df['Date'] = pd.to_datetime(df['Date'])
|
| 289 |
+
df = pd.merge(df, sentiment_df, on="Date", how="left")
|
| 290 |
+
df['sentiment_score'] = df['sentiment_score'].fillna(0.0)
|
| 291 |
+
df.set_index('Date', inplace=True)
|
| 292 |
+
|
| 293 |
+
print_log(f"Sentiment analysis completed for {ticker}. Added sentiment_score column.")
|
| 294 |
+
return df
|
| 295 |
+
except Exception as e:
|
| 296 |
+
print_log(f"Error in add_sentiment for {ticker}: {str(e)}", 'ERROR')
|
| 297 |
+
df['sentiment_score'] = 0.0
|
| 298 |
+
return df
|
| 299 |
+
|
| 300 |
+
def preprocess_data(df, features, target, window_size, horizon):
|
| 301 |
+
try:
|
| 302 |
+
print_log(f"Starting preprocessing: features={features}, target={target}, window={window_size}, horizon={horizon}")
|
| 303 |
+
|
| 304 |
+
# Ensure the DataFrame index is a DatetimeIndex
|
| 305 |
+
if not isinstance(df.index, pd.DatetimeIndex):
|
| 306 |
+
raise ValueError("DataFrame index must be a DatetimeIndex for preprocessing.")
|
| 307 |
+
|
| 308 |
+
# Filter features to only include those present in the DataFrame columns
|
| 309 |
+
updated_feature_cols = [f for f in features if f in df.columns]
|
| 310 |
+
if not updated_feature_cols:
|
| 311 |
+
raise ValueError("No valid features found in DataFrame after indicator calculation.")
|
| 312 |
+
|
| 313 |
+
full_features = updated_feature_cols + [target]
|
| 314 |
+
data = df[full_features].copy()
|
| 315 |
+
data.dropna(inplace=True)
|
| 316 |
+
|
| 317 |
+
if data.empty:
|
| 318 |
+
raise ValueError("DataFrame is empty after dropping NaNs. Cannot proceed with scaling.")
|
| 319 |
+
|
| 320 |
+
feature_scaler = MinMaxScaler()
|
| 321 |
+
target_scaler = MinMaxScaler()
|
| 322 |
+
|
| 323 |
+
data_features_scaled = feature_scaler.fit_transform(data[updated_feature_cols])
|
| 324 |
+
data_target_scaled = target_scaler.fit_transform(data[[target]])
|
| 325 |
+
|
| 326 |
+
full_scaled = np.hstack((data_features_scaled, data_target_scaled))
|
| 327 |
+
target_idx = len(updated_feature_cols)
|
| 328 |
+
|
| 329 |
+
X, y = [], []
|
| 330 |
+
for i in range(len(full_scaled) - window_size - horizon + 1):
|
| 331 |
+
X.append(full_scaled[i:i + window_size])
|
| 332 |
+
y.append(full_scaled[i + window_size:i + window_size + horizon, target_idx])
|
| 333 |
+
|
| 334 |
+
X = np.array(X)
|
| 335 |
+
y = np.array(y)
|
| 336 |
+
|
| 337 |
+
if X.shape[0] == 0 or y.shape[0] == 0:
|
| 338 |
+
raise ValueError(f"Insufficient data after preprocessing. Data length: {len(full_scaled)}, window_size: {window_size}, horizon: {horizon}")
|
| 339 |
+
|
| 340 |
+
print_log(f"Preprocessed data: X.shape={X.shape}, y.shape={y.shape}, Final features: {full_features}, Target idx: {target_idx}")
|
| 341 |
+
return X, y, feature_scaler, target_scaler, full_features, target_idx, None, updated_feature_cols
|
| 342 |
+
except Exception as e:
|
| 343 |
+
print_log(f"Preprocessing error: {str(e)}", 'ERROR')
|
| 344 |
+
raise ValueError(f"Preprocessing failed: {str(e)}")
|
| 345 |
+
|
core/model_runner (2).py
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# core/model_runner.py
|
| 2 |
+
import torch
|
| 3 |
+
import logging
|
| 4 |
+
from core.train_eval import train_and_evaluate
|
| 5 |
+
from core.models import (
|
| 6 |
+
LSTMModel,
|
| 7 |
+
GRUModel,
|
| 8 |
+
CNNModel,
|
| 9 |
+
MLPModel,
|
| 10 |
+
HybridCNNGRUModel,
|
| 11 |
+
TransformerModel,
|
| 12 |
+
BiLSTMModel,
|
| 13 |
+
)
|
| 14 |
+
|
| 15 |
+
logging.basicConfig(
|
| 16 |
+
level=logging.INFO,
|
| 17 |
+
filename="/tmp/app_log.txt",
|
| 18 |
+
filemode="a",
|
| 19 |
+
format="%(asctime)s - %(levelname)s - %(message)s",
|
| 20 |
+
)
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
def get_model(
|
| 24 |
+
df,
|
| 25 |
+
features,
|
| 26 |
+
target,
|
| 27 |
+
model_name="LSTM",
|
| 28 |
+
horizon=1,
|
| 29 |
+
# Hidden size aliases
|
| 30 |
+
hidden=None,
|
| 31 |
+
hidden_units=None,
|
| 32 |
+
# Layers aliases
|
| 33 |
+
layers=None,
|
| 34 |
+
n_layers=None,
|
| 35 |
+
# Learning rate aliases
|
| 36 |
+
lr=None,
|
| 37 |
+
learning_rate=None,
|
| 38 |
+
# Betas for optimizer
|
| 39 |
+
beta1=0.9,
|
| 40 |
+
beta2=0.999,
|
| 41 |
+
# Other hyperparams
|
| 42 |
+
epochs=50,
|
| 43 |
+
weight_decay=0.01,
|
| 44 |
+
dropout=0.2,
|
| 45 |
+
# Window aliases
|
| 46 |
+
window=None,
|
| 47 |
+
window_size=None,
|
| 48 |
+
test_split=0.2,
|
| 49 |
+
selector_method="RandomForest",
|
| 50 |
+
importance_threshold=0.0,
|
| 51 |
+
scheduler_type="None",
|
| 52 |
+
device=None,
|
| 53 |
+
verbose=True,
|
| 54 |
+
):
|
| 55 |
+
"""
|
| 56 |
+
Wrapper that accepts many common argument names used by the UI/calls,
|
| 57 |
+
normalizes them, and calls train_and_evaluate(...) with the canonical names.
|
| 58 |
+
"""
|
| 59 |
+
try:
|
| 60 |
+
# --- Normalize aliases & defaults ---
|
| 61 |
+
# hidden size: prefer explicit hidden_units, then hidden, else default 64
|
| 62 |
+
if hidden_units is not None:
|
| 63 |
+
hidden = hidden_units
|
| 64 |
+
if hidden is None:
|
| 65 |
+
hidden = 64
|
| 66 |
+
|
| 67 |
+
# layers: prefer explicit n_layers, then layers, else default 1
|
| 68 |
+
if n_layers is not None:
|
| 69 |
+
layers = n_layers
|
| 70 |
+
if layers is None:
|
| 71 |
+
layers = 1
|
| 72 |
+
|
| 73 |
+
# learning rate: prefer learning_rate then lr, else default 0.001
|
| 74 |
+
if learning_rate is not None:
|
| 75 |
+
lr = learning_rate
|
| 76 |
+
if lr is None:
|
| 77 |
+
lr = 0.001
|
| 78 |
+
|
| 79 |
+
# window size: prefer window_size then window, else default 30
|
| 80 |
+
if window_size is not None:
|
| 81 |
+
window = window_size
|
| 82 |
+
if window is None:
|
| 83 |
+
window = 30
|
| 84 |
+
|
| 85 |
+
# device: caller may pass it; otherwise detect automatically
|
| 86 |
+
if device is None:
|
| 87 |
+
device = "cuda" if torch.cuda.is_available() else "cpu"
|
| 88 |
+
|
| 89 |
+
logging.info(
|
| 90 |
+
f"get_model called: model={model_name}, device={device}, hidden={hidden}, layers={layers}, lr={lr}, window={window}, epochs={epochs}"
|
| 91 |
+
)
|
| 92 |
+
|
| 93 |
+
# --- Select model class mapping (keys as used in UI) ---
|
| 94 |
+
model_classes = {
|
| 95 |
+
"LSTM": LSTMModel,
|
| 96 |
+
"GRU": GRUModel,
|
| 97 |
+
"CNN": CNNModel,
|
| 98 |
+
"MLP": MLPModel,
|
| 99 |
+
"Hybrid": HybridCNNGRUModel,
|
| 100 |
+
"HybridCNNGRU": HybridCNNGRUModel,
|
| 101 |
+
"Transformer": TransformerModel,
|
| 102 |
+
"BiLSTM": BiLSTMModel,
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
model_cls = model_classes.get(model_name, LSTMModel)
|
| 106 |
+
|
| 107 |
+
# --- Call the core training function with canonical param names ---
|
| 108 |
+
result = train_and_evaluate(
|
| 109 |
+
df=df,
|
| 110 |
+
features=features,
|
| 111 |
+
target=target,
|
| 112 |
+
model_cls=model_cls,
|
| 113 |
+
horizon=horizon,
|
| 114 |
+
hidden=hidden,
|
| 115 |
+
layers=layers,
|
| 116 |
+
epochs=epochs,
|
| 117 |
+
lr=lr,
|
| 118 |
+
beta1=beta1,
|
| 119 |
+
beta2=beta2,
|
| 120 |
+
weight_decay=weight_decay,
|
| 121 |
+
dropout=dropout,
|
| 122 |
+
window=window,
|
| 123 |
+
test_split=test_split,
|
| 124 |
+
selector_method=selector_method,
|
| 125 |
+
importance_threshold=importance_threshold,
|
| 126 |
+
scheduler_type=scheduler_type,
|
| 127 |
+
device=device,
|
| 128 |
+
verbose=verbose,
|
| 129 |
+
)
|
| 130 |
+
|
| 131 |
+
# --- Normalize return ---
|
| 132 |
+
if not result:
|
| 133 |
+
logging.error(f"{model_name} returned empty result.")
|
| 134 |
+
return {"error": "Empty result from training"}
|
| 135 |
+
if isinstance(result, dict) and result.get("error"):
|
| 136 |
+
logging.error(f"{model_name} training error: {result['error']}")
|
| 137 |
+
return {"error": result["error"]}
|
| 138 |
+
|
| 139 |
+
logging.info(f"{model_name} training completed successfully")
|
| 140 |
+
return result
|
| 141 |
+
|
| 142 |
+
except Exception as e:
|
| 143 |
+
logging.error(f"Model runner error for {model_name}: {str(e)}", exc_info=True)
|
| 144 |
+
return {"error": str(e)}
|
core/models (1).py
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# core/models.py
|
| 2 |
+
import torch
|
| 3 |
+
import logging
|
| 4 |
+
import torch.nn as nn
|
| 5 |
+
import math
|
| 6 |
+
|
| 7 |
+
# ---------------- Base ----------------
|
| 8 |
+
class BaseTimeSeriesModel(nn.Module):
|
| 9 |
+
def __init__(self):
|
| 10 |
+
super(BaseTimeSeriesModel, self).__init__()
|
| 11 |
+
|
| 12 |
+
def reset_weights(self):
|
| 13 |
+
for layer in self.children():
|
| 14 |
+
if hasattr(layer, "reset_parameters"):
|
| 15 |
+
layer.reset_parameters()
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
# ---------------- LSTM ----------------
|
| 19 |
+
class LSTMModel(nn.Module):
|
| 20 |
+
def __init__(self, input_size, hidden_size, num_layers, output_size, dropout=0.2):
|
| 21 |
+
super(LSTMModel, self).__init__()
|
| 22 |
+
self.hidden_size = hidden_size
|
| 23 |
+
self.num_layers = num_layers
|
| 24 |
+
self.lstm = nn.LSTM(
|
| 25 |
+
input_size=input_size,
|
| 26 |
+
hidden_size=hidden_size,
|
| 27 |
+
num_layers=num_layers,
|
| 28 |
+
batch_first=True,
|
| 29 |
+
dropout=dropout if num_layers > 1 else 0.0,
|
| 30 |
+
)
|
| 31 |
+
self.fc = nn.Linear(hidden_size, output_size)
|
| 32 |
+
self.dropout = nn.Dropout(dropout)
|
| 33 |
+
|
| 34 |
+
def forward(self, x):
|
| 35 |
+
logging.debug(f"Inside forward: initial x type={type(x)}, x={x}")
|
| 36 |
+
if isinstance(x, (tuple, list)):
|
| 37 |
+
logging.debug(f"Model forward received tuple/list: type={type(x)}, length={len(x)}")
|
| 38 |
+
x = x[0]
|
| 39 |
+
if not isinstance(x, torch.Tensor):
|
| 40 |
+
x = torch.tensor(
|
| 41 |
+
x, dtype=torch.float32, device=next(self.parameters()).device
|
| 42 |
+
)
|
| 43 |
+
batch_size = x.size(0)
|
| 44 |
+
h0 = torch.zeros(self.num_layers, batch_size, self.hidden_size).to(x.device)
|
| 45 |
+
c0 = torch.zeros(self.num_layers, batch_size, self.hidden_size).to(x.device)
|
| 46 |
+
out, _ = self.lstm(x, (h0, c0))
|
| 47 |
+
out = self.dropout(out[:, -1, :])
|
| 48 |
+
return self.fc(out)
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
# ---------------- GRU ----------------
|
| 52 |
+
class GRUModel(nn.Module):
|
| 53 |
+
def __init__(self, input_size, hidden_size, num_layers, output_size, dropout=0.2):
|
| 54 |
+
super(GRUModel, self).__init__()
|
| 55 |
+
self.hidden_size = hidden_size
|
| 56 |
+
self.num_layers = num_layers
|
| 57 |
+
self.gru = nn.GRU(
|
| 58 |
+
input_size=input_size,
|
| 59 |
+
hidden_size=hidden_size,
|
| 60 |
+
num_layers=num_layers,
|
| 61 |
+
batch_first=True,
|
| 62 |
+
dropout=dropout if num_layers > 1 else 0.0,
|
| 63 |
+
)
|
| 64 |
+
self.fc = nn.Linear(hidden_size, output_size)
|
| 65 |
+
self.dropout = nn.Dropout(dropout)
|
| 66 |
+
|
| 67 |
+
def forward(self, x):
|
| 68 |
+
logging.debug(f"Inside forward: initial x type={type(x)}, x={x}")
|
| 69 |
+
if isinstance(x, (tuple, list)):
|
| 70 |
+
logging.debug(f"Model forward received tuple/list: type={type(x)}, length={len(x)}")
|
| 71 |
+
x = x[0]
|
| 72 |
+
if not isinstance(x, torch.Tensor):
|
| 73 |
+
x = torch.tensor(
|
| 74 |
+
x, dtype=torch.float32, device=next(self.parameters()).device
|
| 75 |
+
)
|
| 76 |
+
batch_size = x.size(0)
|
| 77 |
+
h0 = torch.zeros(self.num_layers, batch_size, self.hidden_size).to(x.device)
|
| 78 |
+
out, _ = self.gru(x, h0)
|
| 79 |
+
out = self.dropout(out[:, -1, :])
|
| 80 |
+
return self.fc(out)
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
# ---------------- CNN ----------------
|
| 84 |
+
class CNNModel(nn.Module):
|
| 85 |
+
def __init__(self, input_size, hidden_size, num_layers, output_size, dropout=0.2):
|
| 86 |
+
super(CNNModel, self).__init__()
|
| 87 |
+
self.conv1 = nn.Conv1d(input_size, hidden_size, kernel_size=3, padding=1)
|
| 88 |
+
self.relu = nn.ReLU()
|
| 89 |
+
self.dropout = nn.Dropout(dropout)
|
| 90 |
+
self.fc = nn.Linear(hidden_size, output_size)
|
| 91 |
+
|
| 92 |
+
def forward(self, x):
|
| 93 |
+
logging.debug(f"Inside forward: initial x type={type(x)}, x={x}")
|
| 94 |
+
if isinstance(x, (tuple, list)):
|
| 95 |
+
logging.debug(f"Model forward received tuple/list: type={type(x)}, length={len(x)}")
|
| 96 |
+
x = x[0]
|
| 97 |
+
if not isinstance(x, torch.Tensor):
|
| 98 |
+
x = torch.tensor(
|
| 99 |
+
x, dtype=torch.float32, device=next(self.parameters()).device
|
| 100 |
+
)
|
| 101 |
+
x = x.transpose(1, 2) # [batch, features, seq_len]
|
| 102 |
+
out = self.conv1(x)
|
| 103 |
+
out = self.relu(out)
|
| 104 |
+
out = out.mean(dim=2) # global avg pooling
|
| 105 |
+
out = self.dropout(out)
|
| 106 |
+
return self.fc(out)
|
| 107 |
+
|
| 108 |
+
|
| 109 |
+
# ---------------- MLP ----------------
|
| 110 |
+
class MLPModel(nn.Module):
|
| 111 |
+
def __init__(self, input_size, hidden_size, num_layers, output_size, dropout=0.2):
|
| 112 |
+
super(MLPModel, self).__init__()
|
| 113 |
+
layers = []
|
| 114 |
+
in_features = input_size
|
| 115 |
+
for _ in range(num_layers):
|
| 116 |
+
layers.append(nn.Linear(in_features, hidden_size))
|
| 117 |
+
layers.append(nn.ReLU())
|
| 118 |
+
layers.append(nn.Dropout(dropout))
|
| 119 |
+
in_features = hidden_size
|
| 120 |
+
layers.append(nn.Linear(hidden_size, output_size))
|
| 121 |
+
self.mlp = nn.Sequential(*layers)
|
| 122 |
+
|
| 123 |
+
def forward(self, x):
|
| 124 |
+
logging.debug(f"Inside forward: initial x type={type(x)}, x={x}")
|
| 125 |
+
if isinstance(x, (tuple, list)):
|
| 126 |
+
logging.debug(f"Model forward received tuple/list: type={type(x)}, length={len(x)}")
|
| 127 |
+
x = x[0]
|
| 128 |
+
if not isinstance(x, torch.Tensor):
|
| 129 |
+
x = torch.tensor(
|
| 130 |
+
x, dtype=torch.float32, device=next(self.parameters()).device
|
| 131 |
+
)
|
| 132 |
+
return self.mlp(x[:, -1, :]) # flatten last timestep
|
| 133 |
+
|
| 134 |
+
|
| 135 |
+
# ---------------- Hybrid CNN-GRU ----------------
|
| 136 |
+
class HybridCNNGRUModel(nn.Module):
|
| 137 |
+
def __init__(self, input_size, hidden_size, num_layers, output_size, dropout=0.2):
|
| 138 |
+
super(HybridCNNGRUModel, self).__init__()
|
| 139 |
+
self.conv1 = nn.Conv1d(input_size, hidden_size, kernel_size=3, padding=1)
|
| 140 |
+
self.gru = nn.GRU(hidden_size, hidden_size, num_layers, batch_first=True)
|
| 141 |
+
self.fc = nn.Linear(hidden_size, output_size)
|
| 142 |
+
self.dropout = nn.Dropout(dropout)
|
| 143 |
+
|
| 144 |
+
def forward(self, x):
|
| 145 |
+
logging.debug(f"Inside forward: initial x type={type(x)}, x={x}")
|
| 146 |
+
if isinstance(x, (tuple, list)):
|
| 147 |
+
logging.debug(f"Model forward received tuple/list: type={type(x)}, length={len(x)}")
|
| 148 |
+
x = x[0]
|
| 149 |
+
if not isinstance(x, torch.Tensor):
|
| 150 |
+
x = torch.tensor(
|
| 151 |
+
x, dtype=torch.float32, device=next(self.parameters()).device
|
| 152 |
+
)
|
| 153 |
+
x = x.transpose(1, 2)
|
| 154 |
+
out = self.conv1(x).transpose(1, 2)
|
| 155 |
+
out, _ = self.gru(out)
|
| 156 |
+
out = self.dropout(out[:, -1, :])
|
| 157 |
+
return self.fc(out)
|
| 158 |
+
|
| 159 |
+
|
| 160 |
+
# ---------------- Transformer ----------------
|
| 161 |
+
class TransformerModel(nn.Module):
|
| 162 |
+
def __init__(
|
| 163 |
+
self, input_size, hidden_size, num_layers, output_size, dropout=0.2, nhead=4
|
| 164 |
+
):
|
| 165 |
+
super(TransformerModel, self).__init__()
|
| 166 |
+
self.embedding = nn.Linear(input_size, hidden_size)
|
| 167 |
+
encoder_layer = nn.TransformerEncoderLayer(
|
| 168 |
+
d_model=hidden_size, nhead=nhead, dropout=dropout
|
| 169 |
+
)
|
| 170 |
+
self.transformer = nn.TransformerEncoder(encoder_layer, num_layers=num_layers)
|
| 171 |
+
self.fc = nn.Linear(hidden_size, output_size)
|
| 172 |
+
|
| 173 |
+
def forward(self, x):
|
| 174 |
+
logging.debug(f"Inside forward: initial x type={type(x)}, x={x}")
|
| 175 |
+
if isinstance(x, (tuple, list)):
|
| 176 |
+
logging.debug(f"Model forward received tuple/list: type={type(x)}, length={len(x)}")
|
| 177 |
+
x = x[0]
|
| 178 |
+
if not isinstance(x, torch.Tensor):
|
| 179 |
+
x = torch.tensor(
|
| 180 |
+
x, dtype=torch.float32, device=next(self.parameters()).device
|
| 181 |
+
)
|
| 182 |
+
x = self.embedding(x)
|
| 183 |
+
out = self.transformer(x.transpose(0, 1)) # seq_first
|
| 184 |
+
out = out[-1, :, :]
|
| 185 |
+
return self.fc(out)
|
| 186 |
+
|
| 187 |
+
|
| 188 |
+
# ---------------- BiLSTM ----------------
|
| 189 |
+
class BiLSTMModel(nn.Module):
|
| 190 |
+
def __init__(self, input_size, hidden_size, num_layers, output_size, dropout=0.2):
|
| 191 |
+
super(BiLSTMModel, self).__init__()
|
| 192 |
+
self.hidden_size = hidden_size
|
| 193 |
+
self.num_layers = num_layers
|
| 194 |
+
self.lstm = nn.LSTM(
|
| 195 |
+
input_size=input_size,
|
| 196 |
+
hidden_size=hidden_size,
|
| 197 |
+
num_layers=num_layers,
|
| 198 |
+
batch_first=True,
|
| 199 |
+
dropout=dropout if num_layers > 1 else 0.0,
|
| 200 |
+
bidirectional=True,
|
| 201 |
+
)
|
| 202 |
+
self.fc = nn.Linear(hidden_size * 2, output_size)
|
| 203 |
+
self.dropout = nn.Dropout(dropout)
|
| 204 |
+
|
| 205 |
+
def forward(self, x):
|
| 206 |
+
logging.debug(f"Inside forward: initial x type={type(x)}, x={x}")
|
| 207 |
+
if isinstance(x, (tuple, list)):
|
| 208 |
+
logging.debug(f"Model forward received tuple/list: type={type(x)}, length={len(x)}")
|
| 209 |
+
x = x[0]
|
| 210 |
+
if not isinstance(x, torch.Tensor):
|
| 211 |
+
x = torch.tensor(
|
| 212 |
+
x, dtype=torch.float32, device=next(self.parameters()).device
|
| 213 |
+
)
|
| 214 |
+
batch_size = x.size(0)
|
| 215 |
+
h0 = torch.zeros(self.num_layers * 2, batch_size, self.hidden_size).to(x.device)
|
| 216 |
+
c0 = torch.zeros(self.num_layers * 2, batch_size, self.hidden_size).to(x.device)
|
| 217 |
+
out, _ = self.lstm(x, (h0, c0))
|
| 218 |
+
out = self.dropout(out[:, -1, :])
|
| 219 |
+
return self.fc(out)
|
core/plot (9).py
ADDED
|
@@ -0,0 +1,490 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import plotly.graph_objects as go
|
| 2 |
+
import plotly.express as px
|
| 3 |
+
import pandas as pd
|
| 4 |
+
import logging
|
| 5 |
+
import matplotlib.pyplot as plt
|
| 6 |
+
from plotly.subplots import make_subplots
|
| 7 |
+
import numpy as np
|
| 8 |
+
|
| 9 |
+
logging.basicConfig(level=logging.INFO)
|
| 10 |
+
|
| 11 |
+
def plot_indicators(df, ticker):
|
| 12 |
+
try:
|
| 13 |
+
fig = make_subplots(
|
| 14 |
+
rows=7, cols=1, shared_xaxes=True, vertical_spacing=0.03,
|
| 15 |
+
subplot_titles=(
|
| 16 |
+
'Price & Moving Averages', 'Volume', 'MACD & RSI',
|
| 17 |
+
'Stochastic & Williams %R', 'ADX & DI', 'ATR & CCI', 'Signal Strength'
|
| 18 |
+
),
|
| 19 |
+
row_heights=[0.4, 0.1, 0.15, 0.15, 0.15, 0.15, 0.15]
|
| 20 |
+
)
|
| 21 |
+
|
| 22 |
+
# Price and Moving Averages
|
| 23 |
+
fig.add_trace(
|
| 24 |
+
go.Candlestick(
|
| 25 |
+
x=df.index, open=df['Open'], high=df['High'], low=df['Low'], close=df['value'],
|
| 26 |
+
name='Price', increasing_line_color='#00CC96', decreasing_line_color='#EF553B'
|
| 27 |
+
), row=1, col=1
|
| 28 |
+
)
|
| 29 |
+
for ma in ['sma_10', 'sma_20', 'sma_50', 'ema_12', 'ema_26', 'ema_50']:
|
| 30 |
+
if ma in df:
|
| 31 |
+
fig.add_trace(
|
| 32 |
+
go.Scatter(x=df.index, y=df[ma], name=ma.upper(), line=dict(width=1.5)),
|
| 33 |
+
row=1, col=1
|
| 34 |
+
)
|
| 35 |
+
if 'bbu_20_2' in df:
|
| 36 |
+
fig.add_trace(
|
| 37 |
+
go.Scatter(x=df.index, y=df['bbu_20_2'], name='BB Upper', line=dict(color='gray', dash='dot')),
|
| 38 |
+
row=1, col=1
|
| 39 |
+
)
|
| 40 |
+
fig.add_trace(
|
| 41 |
+
go.Scatter(x=df.index, y=df['bbm_20_2'], name='BB Middle', line=dict(color='gray')),
|
| 42 |
+
row=1, col=1
|
| 43 |
+
)
|
| 44 |
+
fig.add_trace(
|
| 45 |
+
go.Scatter(x=df.index, y=df["bbl_20_2"], name="BB Lower", line=dict(color="gray", dash="dot")),
|
| 46 |
+
row=1, col=1
|
| 47 |
+
)
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
# Position Size and Risk Annotation
|
| 52 |
+
if 'atr_14' in df:
|
| 53 |
+
atr = df['atr_14'].iloc[-1]
|
| 54 |
+
stop_distance = atr * 2
|
| 55 |
+
position_size = (10000 * 0.01) / stop_distance
|
| 56 |
+
fig.add_annotation(
|
| 57 |
+
text=f"Position Size: {position_size:.0f} shares (1% risk, ATR {atr:.2f})",
|
| 58 |
+
xref="paper", yref="paper", x=0.05, y=0.95, showarrow=False,
|
| 59 |
+
font=dict(color="black", size=12)
|
| 60 |
+
)
|
| 61 |
+
|
| 62 |
+
# Volume
|
| 63 |
+
fig.add_trace(
|
| 64 |
+
go.Bar(x=df.index, y=df["Volume"], name="Volume", marker_color="blue", opacity=0.5),
|
| 65 |
+
row=2, col=1
|
| 66 |
+
)
|
| 67 |
+
|
| 68 |
+
# MACD & RSI
|
| 69 |
+
if 'macd_12_26_9' in df:
|
| 70 |
+
fig.add_trace(
|
| 71 |
+
go.Scatter(x=df.index, y=df['macd_12_26_9'], name='MACD', line=dict(color='blue')),
|
| 72 |
+
row=3, col=1
|
| 73 |
+
)
|
| 74 |
+
fig.add_trace(
|
| 75 |
+
go.Scatter(x=df.index, y=df["macds_12_26_9"], name="MACD Signal", line=dict(color="orange")),
|
| 76 |
+
row=3, col=1
|
| 77 |
+
)
|
| 78 |
+
fig.add_trace(
|
| 79 |
+
go.Bar(x=df.index, y=df["macdh_12_26_9"], name="MACD Hist", marker_color="gray"),
|
| 80 |
+
row=3, col=1
|
| 81 |
+
)
|
| 82 |
+
if 'rsi_14' in df:
|
| 83 |
+
fig.add_trace(
|
| 84 |
+
go.Scatter(x=df.index, y=df["rsi_14"], name="RSI 14", line=dict(color="purple")),
|
| 85 |
+
row=3, col=1
|
| 86 |
+
)
|
| 87 |
+
fig.add_hline(y=70, line_dash="dash", line_color="red", row=3, col=1)
|
| 88 |
+
fig.add_hline(y=30, line_dash="dash", line_color="green", row=3, col=1)
|
| 89 |
+
if 'rsi_21' in df:
|
| 90 |
+
fig.add_trace(
|
| 91 |
+
go.Scatter(x=df.index, y=df["rsi_21"], name="RSI 21", line=dict(color="magenta", dash="dash")),
|
| 92 |
+
row=3, col=1
|
| 93 |
+
)
|
| 94 |
+
if 'rsi_50' in df:
|
| 95 |
+
fig.add_trace(
|
| 96 |
+
go.Scatter(x=df.index, y=df["rsi_50"], name="RSI 50", line=dict(color="cyan", dash="dot")),
|
| 97 |
+
row=3, col=1
|
| 98 |
+
)
|
| 99 |
+
|
| 100 |
+
# Stochastic & Williams %R
|
| 101 |
+
if 'stochk_14_3_3' in df:
|
| 102 |
+
fig.add_trace(
|
| 103 |
+
go.Scatter(x=df.index, y=df["stochk_14_3_3"], name="Stoch %K", line=dict(color="blue")),
|
| 104 |
+
row=4, col=1
|
| 105 |
+
)
|
| 106 |
+
fig.add_trace(
|
| 107 |
+
go.Scatter(x=df.index, y=df["stochd_14_3_3"], name="Stoch %D", line=dict(color="orange")),
|
| 108 |
+
row=4, col=1
|
| 109 |
+
)
|
| 110 |
+
fig.add_hline(y=80, line_dash="dash", line_color="red", row=4, col=1)
|
| 111 |
+
fig.add_hline(y=20, line_dash="dash", line_color="green", row=4, col=1)
|
| 112 |
+
if 'willr_14' in df:
|
| 113 |
+
fig.add_trace(
|
| 114 |
+
go.Scatter(x=df.index, y=df["willr_14"], name="Williams %R", line=dict(color="green")),
|
| 115 |
+
row=4, col=1
|
| 116 |
+
)
|
| 117 |
+
fig.add_hline(y=-20, line_dash="dash", line_color="red", row=4, col=1)
|
| 118 |
+
fig.add_hline(y=-80, line_dash="dash", line_color="green", row=4, col=1)
|
| 119 |
+
|
| 120 |
+
# ADX & DI
|
| 121 |
+
if 'adx_14' in df:
|
| 122 |
+
fig.add_trace(
|
| 123 |
+
go.Scatter(x=df.index, y=df['adx_14'], name='ADX', line=dict(color='blue')),
|
| 124 |
+
row=5, col=1
|
| 125 |
+
)
|
| 126 |
+
fig.add_trace(
|
| 127 |
+
go.Scatter(x=df.index, y=df.get('pdi_14'), name='+DI', line=dict(color='green')),
|
| 128 |
+
row=5, col=1
|
| 129 |
+
)
|
| 130 |
+
fig.add_trace(
|
| 131 |
+
go.Scatter(x=df.index, y=df.get('mdi_14'), name='-DI', line=dict(color='red')),
|
| 132 |
+
row=5, col=1
|
| 133 |
+
)
|
| 134 |
+
fig.add_hline(y=25, line_dash="dash", line_color="black", row=5, col=1)
|
| 135 |
+
|
| 136 |
+
# ATR & CCI
|
| 137 |
+
if 'atr_14' in df:
|
| 138 |
+
fig.add_trace(
|
| 139 |
+
go.Scatter(x=df.index, y=df["atr_14"], name="ATR", line=dict(color="orange")),
|
| 140 |
+
row=6, col=1
|
| 141 |
+
)
|
| 142 |
+
if 'cci_20' in df:
|
| 143 |
+
fig.add_trace(
|
| 144 |
+
go.Scatter(x=df.index, y=df["cci_20"], name="CCI", line=dict(color="purple")),
|
| 145 |
+
row=6, col=1
|
| 146 |
+
)
|
| 147 |
+
fig.add_hline(y=100, line_dash="dash", line_color="red", row=6, col=1)
|
| 148 |
+
fig.add_hline(y=-100, line_dash="dash", line_color="green", row=6, col=1)
|
| 149 |
+
|
| 150 |
+
# Signal Strength Plot
|
| 151 |
+
if all(col in df for col in ['RSI_Signal', 'MACD_Signal', 'ADX_Signal', 'Sentiment_Signal', 'Model_Signal']):
|
| 152 |
+
signal_strength = (
|
| 153 |
+
df['RSI_Signal'].abs() +
|
| 154 |
+
df['MACD_Signal'].abs() +
|
| 155 |
+
df['ADX_Signal'].abs() +
|
| 156 |
+
df['Sentiment_Signal'].abs() +
|
| 157 |
+
df['Model_Signal'].abs()
|
| 158 |
+
)
|
| 159 |
+
fig.add_trace(
|
| 160 |
+
go.Scatter(
|
| 161 |
+
x=df.index, y=signal_strength, name='Signal Strength',
|
| 162 |
+
line=dict(color='teal'), fill='tozeroy'
|
| 163 |
+
), row=7, col=1
|
| 164 |
+
)
|
| 165 |
+
fig.add_hline(y=3, line_dash="dash", line_color="orange", row=7, col=1, annotation_text="Strong Signal Threshold")
|
| 166 |
+
|
| 167 |
+
fig.update_layout(
|
| 168 |
+
title=f"{ticker} Price and Technical Indicators",
|
| 169 |
+
template="plotly_white",
|
| 170 |
+
height=2400,
|
| 171 |
+
width=1400,
|
| 172 |
+
showlegend=True,
|
| 173 |
+
xaxis_rangeslider_visible=False,
|
| 174 |
+
margin=dict(l=50, r=50, t=100, b=50),
|
| 175 |
+
xaxis=dict(tickformat="%Y-%m-%d", minor=dict(ticks="inside", showgrid=True), gridcolor="lightgrey"),
|
| 176 |
+
plot_bgcolor="white",
|
| 177 |
+
paper_bgcolor="white",
|
| 178 |
+
hovermode="x unified"
|
| 179 |
+
)
|
| 180 |
+
return fig
|
| 181 |
+
except Exception as e:
|
| 182 |
+
logging.error(f"Plot indicators error: {e}")
|
| 183 |
+
return None
|
| 184 |
+
def plot_future_forecast(df, result, indicators):
|
| 185 |
+
fig, ax = plt.subplots(figsize=(10, 6))
|
| 186 |
+
ax.plot(df.index, df["value"], label="Historical Value", color="blue", linewidth=2)
|
| 187 |
+
for ind in indicators:
|
| 188 |
+
if ind in df.columns:
|
| 189 |
+
ax.plot(df.index, df[ind], label=ind, linestyle='--')
|
| 190 |
+
if "latest_prediction" in result:
|
| 191 |
+
last_date = df.index[-1]
|
| 192 |
+
horizon = len(result["latest_prediction"])
|
| 193 |
+
future_dates = pd.date_range(start=last_date + pd.Timedelta(days=1), periods=horizon, freq='B')
|
| 194 |
+
ax.plot(future_dates, result["latest_prediction"], label="Forecast Value", color="orange", linestyle="--", linewidth=2)
|
| 195 |
+
for i, val in enumerate(result["latest_prediction"]):
|
| 196 |
+
ax.text(future_dates[i], val, f"{val:.2f}", color="orange")
|
| 197 |
+
ax.legend()
|
| 198 |
+
ax.set_title("Historical Data, Indicators, and Future Forecast")
|
| 199 |
+
ax.set_xlabel("Date")
|
| 200 |
+
ax.set_ylabel("Value")
|
| 201 |
+
ax.grid(True)
|
| 202 |
+
plt.tight_layout()
|
| 203 |
+
return fig
|
| 204 |
+
|
| 205 |
+
# Other plotting functions remain unchanged
|
| 206 |
+
def plot_forecast(result, df):
|
| 207 |
+
try:
|
| 208 |
+
actual = result.get("actual", [])
|
| 209 |
+
forecast = result.get("forecast", [])
|
| 210 |
+
if not actual or not forecast:
|
| 211 |
+
return None
|
| 212 |
+
dates = df.index[-len(actual):]
|
| 213 |
+
fig = go.Figure()
|
| 214 |
+
fig.add_trace(go.Scatter(x=dates, y=actual, name='Actual', line=dict(color='blue')))
|
| 215 |
+
fig.add_trace(go.Scatter(x=dates, y=forecast, name='Forecast', line=dict(color='orange')))
|
| 216 |
+
fig.update_layout(
|
| 217 |
+
title="Backtest: Actual vs Forecast",
|
| 218 |
+
template="plotly_white",
|
| 219 |
+
height=600,
|
| 220 |
+
xaxis_title="Date",
|
| 221 |
+
yaxis_title="Price",
|
| 222 |
+
xaxis=dict(tickformat="%Y-%m-%d", minor=dict(ticks="inside", showgrid=True), gridcolor="lightgrey"),
|
| 223 |
+
yaxis=dict(gridcolor="lightgrey"),
|
| 224 |
+
plot_bgcolor="white",
|
| 225 |
+
paper_bgcolor="white"
|
| 226 |
+
)
|
| 227 |
+
return fig
|
| 228 |
+
except Exception as e:
|
| 229 |
+
logging.error(f"Plot forecast error: {e}")
|
| 230 |
+
return None
|
| 231 |
+
|
| 232 |
+
# ... (other plotting functions like plot_future_forecast, plot_metrics_r2, etc., remain as provided)
|
| 233 |
+
|
| 234 |
+
def plot_metrics_r2(result):
|
| 235 |
+
try:
|
| 236 |
+
metrics = result.get("metrics", {})
|
| 237 |
+
if not metrics:
|
| 238 |
+
return None
|
| 239 |
+
fig = go.Figure()
|
| 240 |
+
fig.add_trace(go.Bar(
|
| 241 |
+
x=['R²', 'MAPE'],
|
| 242 |
+
y=[metrics.get('R2', 0), metrics.get('MAPE', 0)],
|
| 243 |
+
marker_color=['#1E90FF', '#FF6347']
|
| 244 |
+
))
|
| 245 |
+
fig.update_layout(
|
| 246 |
+
title="R² and MAPE Metrics",
|
| 247 |
+
template="plotly_white",
|
| 248 |
+
height=600,
|
| 249 |
+
xaxis_title="Metric",
|
| 250 |
+
yaxis_title="Value",
|
| 251 |
+
xaxis=dict(minor=dict(ticks="inside", showgrid=True), gridcolor="lightgrey"),
|
| 252 |
+
yaxis=dict(gridcolor="lightgrey"),
|
| 253 |
+
plot_bgcolor="white",
|
| 254 |
+
paper_bgcolor="white"
|
| 255 |
+
)
|
| 256 |
+
return fig
|
| 257 |
+
except Exception as e:
|
| 258 |
+
logging.error(f"Plot R2 error: {e}")
|
| 259 |
+
return None
|
| 260 |
+
|
| 261 |
+
def plot_metrics_errors(result):
|
| 262 |
+
try:
|
| 263 |
+
metrics = result.get("metrics", {})
|
| 264 |
+
if not metrics:
|
| 265 |
+
return None
|
| 266 |
+
fig = go.Figure()
|
| 267 |
+
fig.add_trace(go.Bar(
|
| 268 |
+
x=['RMSE', 'MAE'],
|
| 269 |
+
y=[metrics.get('RMSE', 0), metrics.get('MAE', 0)],
|
| 270 |
+
marker_color=['#32CD32', '#9370DB']
|
| 271 |
+
))
|
| 272 |
+
fig.update_layout(
|
| 273 |
+
title="Error Metrics",
|
| 274 |
+
template="plotly_white",
|
| 275 |
+
height=600,
|
| 276 |
+
xaxis_title="Metric",
|
| 277 |
+
yaxis_title="Value",
|
| 278 |
+
xaxis=dict(minor=dict(ticks="inside", showgrid=True), gridcolor="lightgrey"),
|
| 279 |
+
yaxis=dict(gridcolor="lightgrey"),
|
| 280 |
+
plot_bgcolor="white",
|
| 281 |
+
paper_bgcolor="white"
|
| 282 |
+
)
|
| 283 |
+
return fig
|
| 284 |
+
except Exception as e:
|
| 285 |
+
logging.error(f"Plot metrics errors: {e}")
|
| 286 |
+
return None
|
| 287 |
+
|
| 288 |
+
def plot_metrics_precision_recall(result):
|
| 289 |
+
try:
|
| 290 |
+
metrics = result.get("metrics", {})
|
| 291 |
+
if not metrics:
|
| 292 |
+
return None
|
| 293 |
+
fig = go.Figure()
|
| 294 |
+
fig.add_trace(go.Bar(
|
| 295 |
+
x=['Precision', 'Recall'],
|
| 296 |
+
y=[metrics.get('Precision', 0), metrics.get('Recall', 0)],
|
| 297 |
+
marker_color=['#1E90FF', '#FF6347']
|
| 298 |
+
))
|
| 299 |
+
fig.update_layout(
|
| 300 |
+
title="Precision and Recall Metrics",
|
| 301 |
+
template="plotly_white",
|
| 302 |
+
height=600,
|
| 303 |
+
xaxis_title="Metric",
|
| 304 |
+
yaxis_title="Value",
|
| 305 |
+
xaxis=dict(minor=dict(ticks="inside", showgrid=True), gridcolor="lightgrey"),
|
| 306 |
+
yaxis=dict(gridcolor="lightgrey"),
|
| 307 |
+
plot_bgcolor="white",
|
| 308 |
+
paper_bgcolor="white"
|
| 309 |
+
)
|
| 310 |
+
return fig
|
| 311 |
+
except Exception as e:
|
| 312 |
+
logging.error(f"Plot precision recall error: {e}")
|
| 313 |
+
return None
|
| 314 |
+
|
| 315 |
+
def plot_metrics_risk(result):
|
| 316 |
+
try:
|
| 317 |
+
metrics = result.get("metrics", {})
|
| 318 |
+
if not metrics:
|
| 319 |
+
return None
|
| 320 |
+
fig = go.Figure()
|
| 321 |
+
fig.add_trace(go.Bar(
|
| 322 |
+
x=['MASE', 'Sharpe', 'Volatility'],
|
| 323 |
+
y=[metrics.get('MASE', 0), metrics.get('Sharpe', 0), metrics.get('Volatility', 0)],
|
| 324 |
+
marker_color=['#32CD32', '#9370DB', '#FFD700']
|
| 325 |
+
))
|
| 326 |
+
fig.update_layout(
|
| 327 |
+
title="Risk Metrics",
|
| 328 |
+
template="plotly_white",
|
| 329 |
+
height=600,
|
| 330 |
+
xaxis_title="Metric",
|
| 331 |
+
yaxis_title="Value",
|
| 332 |
+
xaxis=dict(minor=dict(ticks="inside", showgrid=True), gridcolor="lightgrey"),
|
| 333 |
+
yaxis=dict(gridcolor="lightgrey"),
|
| 334 |
+
plot_bgcolor="white",
|
| 335 |
+
paper_bgcolor="white"
|
| 336 |
+
)
|
| 337 |
+
return fig
|
| 338 |
+
except Exception as e:
|
| 339 |
+
logging.error(f"Plot risk metrics error: {e}")
|
| 340 |
+
return None
|
| 341 |
+
|
| 342 |
+
def plot_loss_curve(result):
|
| 343 |
+
try:
|
| 344 |
+
train_loss = result.get("train_loss", [])
|
| 345 |
+
val_loss = result.get("val_loss", [])
|
| 346 |
+
if not train_loss:
|
| 347 |
+
return None
|
| 348 |
+
epochs = list(range(1, len(train_loss) + 1))
|
| 349 |
+
fig = go.Figure()
|
| 350 |
+
fig.add_trace(go.Scatter(x=epochs, y=train_loss, mode='lines', name='Train Loss', line=dict(color='#00CC96')))
|
| 351 |
+
fig.add_trace(go.Scatter(x=epochs, y=val_loss, mode='lines', name='Validation Loss', line=dict(color='#EF553B')))
|
| 352 |
+
fig.update_layout(
|
| 353 |
+
title="Training and Validation Loss",
|
| 354 |
+
template="plotly_white",
|
| 355 |
+
height=600,
|
| 356 |
+
xaxis_title="Epoch",
|
| 357 |
+
yaxis_title="Loss",
|
| 358 |
+
xaxis=dict(minor=dict(ticks="inside", showgrid=True), gridcolor="lightgrey"),
|
| 359 |
+
yaxis=dict(gridcolor="lightgrey"),
|
| 360 |
+
plot_bgcolor="white",
|
| 361 |
+
paper_bgcolor="white"
|
| 362 |
+
)
|
| 363 |
+
return fig
|
| 364 |
+
except Exception as e:
|
| 365 |
+
logging.error(f"Plot loss curve error: {e}")
|
| 366 |
+
return None
|
| 367 |
+
|
| 368 |
+
def plot_model_architecture(result):
|
| 369 |
+
try:
|
| 370 |
+
summary_text = result.get("model_summary", "No model summary available.")
|
| 371 |
+
if not summary_text or summary_text == "No model summary available.":
|
| 372 |
+
summary_text = "Model architecture summary could not be generated."
|
| 373 |
+
fig = go.Figure()
|
| 374 |
+
fig.add_annotation(
|
| 375 |
+
text=summary_text.replace('\n', '<br>'),
|
| 376 |
+
xref="paper",
|
| 377 |
+
yref="paper",
|
| 378 |
+
x=0,
|
| 379 |
+
y=1,
|
| 380 |
+
showarrow=False,
|
| 381 |
+
font=dict(size=14, family="Courier New", color="#1E90FF"),
|
| 382 |
+
align="left",
|
| 383 |
+
bgcolor="white",
|
| 384 |
+
bordercolor="black",
|
| 385 |
+
borderwidth=1,
|
| 386 |
+
width=600,
|
| 387 |
+
height=400
|
| 388 |
+
)
|
| 389 |
+
fig.update_layout(
|
| 390 |
+
title="Model Architecture",
|
| 391 |
+
template="plotly_white",
|
| 392 |
+
height=600,
|
| 393 |
+
showlegend=False,
|
| 394 |
+
xaxis=dict(visible=False),
|
| 395 |
+
yaxis=dict(visible=False),
|
| 396 |
+
plot_bgcolor="white",
|
| 397 |
+
paper_bgcolor="white"
|
| 398 |
+
)
|
| 399 |
+
return fig
|
| 400 |
+
except Exception as e:
|
| 401 |
+
logging.error(f"Plot model architecture error: {e}")
|
| 402 |
+
return None
|
| 403 |
+
|
| 404 |
+
def plot_signals(signals_df, ticker):
|
| 405 |
+
try:
|
| 406 |
+
fig = go.Figure()
|
| 407 |
+
fig.add_trace(go.Scatter(
|
| 408 |
+
x=signals_df.index,
|
| 409 |
+
y=signals_df['Price'],
|
| 410 |
+
mode='lines',
|
| 411 |
+
name='Price'
|
| 412 |
+
))
|
| 413 |
+
buy_signals = signals_df[signals_df['Signal'] == 'Buy']
|
| 414 |
+
sell_signals = signals_df[signals_df['Signal'] == 'Sell']
|
| 415 |
+
|
| 416 |
+
fig.add_trace(go.Scatter(
|
| 417 |
+
x=buy_signals.index,
|
| 418 |
+
y=buy_signals['Price'],
|
| 419 |
+
mode='markers',
|
| 420 |
+
marker=dict(symbol='triangle-up', size=10, color='green'),
|
| 421 |
+
name='Buy Signal'
|
| 422 |
+
))
|
| 423 |
+
fig.add_trace(go.Scatter(
|
| 424 |
+
x=sell_signals.index,
|
| 425 |
+
y=sell_signals['Price'],
|
| 426 |
+
mode='markers',
|
| 427 |
+
marker=dict(symbol='triangle-down', size=10, color='red'),
|
| 428 |
+
name='Sell Signal'
|
| 429 |
+
))
|
| 430 |
+
|
| 431 |
+
fig.update_layout(
|
| 432 |
+
title=f'{ticker} Trading Signals',
|
| 433 |
+
xaxis_title='Date',
|
| 434 |
+
yaxis_title='Price',
|
| 435 |
+
template="plotly_white",
|
| 436 |
+
xaxis_rangeslider_visible=False
|
| 437 |
+
)
|
| 438 |
+
return fig
|
| 439 |
+
except Exception as e:
|
| 440 |
+
logging.error(f"Plot signals error: {e}")
|
| 441 |
+
return None
|
| 442 |
+
|
| 443 |
+
def plot_backtest(equity_df, trades_df, ticker):
|
| 444 |
+
try:
|
| 445 |
+
fig = go.Figure()
|
| 446 |
+
fig.add_trace(go.Scatter(
|
| 447 |
+
x=equity_df.index,
|
| 448 |
+
y=equity_df['Equity'],
|
| 449 |
+
mode='lines',
|
| 450 |
+
name='Equity Curve'
|
| 451 |
+
))
|
| 452 |
+
|
| 453 |
+
buy_trades = trades_df[trades_df['Type'] == 'Buy']
|
| 454 |
+
sell_trades = trades_df[trades_df['Type'] == 'Sell']
|
| 455 |
+
exit_trades = trades_df[trades_df['Type'] == 'Exit']
|
| 456 |
+
|
| 457 |
+
fig.add_trace(go.Scatter(
|
| 458 |
+
x=buy_trades['Date'],
|
| 459 |
+
y=buy_trades['Price'],
|
| 460 |
+
mode='markers',
|
| 461 |
+
marker=dict(symbol='triangle-up', size=10, color='green'),
|
| 462 |
+
name='Buy Trades'
|
| 463 |
+
))
|
| 464 |
+
fig.add_trace(go.Scatter(
|
| 465 |
+
x=sell_trades['Date'],
|
| 466 |
+
y=sell_trades['Price'],
|
| 467 |
+
mode='markers',
|
| 468 |
+
marker=dict(symbol='triangle-down', size=10, color='red'),
|
| 469 |
+
name='Sell Trades'
|
| 470 |
+
))
|
| 471 |
+
fig.add_trace(go.Scatter(
|
| 472 |
+
x=exit_trades['Date'],
|
| 473 |
+
y=exit_trades['Price'],
|
| 474 |
+
mode='markers',
|
| 475 |
+
marker=dict(symbol='circle', size=8, color='blue'),
|
| 476 |
+
name='Exit Trades'
|
| 477 |
+
))
|
| 478 |
+
|
| 479 |
+
fig.update_layout(
|
| 480 |
+
title=f'{ticker} Backtest Equity Curve and Trades',
|
| 481 |
+
xaxis_title='Date',
|
| 482 |
+
yaxis_title='Equity / Price',
|
| 483 |
+
template="plotly_white",
|
| 484 |
+
xaxis_rangeslider_visible=False
|
| 485 |
+
)
|
| 486 |
+
return fig
|
| 487 |
+
except Exception as e:
|
| 488 |
+
logging.error(f"Plot backtest error: {e}")
|
| 489 |
+
return None
|
| 490 |
+
|
core/signals (1).py
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import pandas as pd
|
| 2 |
+
import numpy as np
|
| 3 |
+
import logging
|
| 4 |
+
|
| 5 |
+
logging.basicConfig(level=logging.DEBUG, filename="debug.log", filemode="a")
|
| 6 |
+
|
| 7 |
+
def generate_signals(df, result, volatility_window=14):
|
| 8 |
+
try:
|
| 9 |
+
signals_df = pd.DataFrame(index=df.index)
|
| 10 |
+
signals_df["Price"] = df["value"]
|
| 11 |
+
signals_df["Signal"] = "Hold"
|
| 12 |
+
signals_df["Position_Size"] = 0.0
|
| 13 |
+
signals_df["Stop_Loss"] = np.nan
|
| 14 |
+
signals_df["Take_Profit"] = np.nan
|
| 15 |
+
|
| 16 |
+
rsi_key = "rsi_14"
|
| 17 |
+
macd_key = "macdh_12_26_9"
|
| 18 |
+
adx_key = "adx_14"
|
| 19 |
+
pdi_key = "pdi_14"
|
| 20 |
+
mdi_key = "mdi_14"
|
| 21 |
+
atr_key = "atr_14"
|
| 22 |
+
sentiment_key = "sentiment"
|
| 23 |
+
|
| 24 |
+
for i in range(1, len(df)):
|
| 25 |
+
vote = 0
|
| 26 |
+
rsi_signal = macd_signal = adx_signal = sentiment_signal = model_signal = 0
|
| 27 |
+
|
| 28 |
+
if rsi_key in df.columns and not pd.isna(df[rsi_key].iloc[i]):
|
| 29 |
+
rsi = df[rsi_key].iloc[i]
|
| 30 |
+
rsi_signal = 1 if rsi < 50 else -1 if rsi > 50 else 0
|
| 31 |
+
vote += rsi_signal
|
| 32 |
+
logging.debug(f"RSI at {df.index[i]}: value={rsi:.2f}, signal={rsi_signal}")
|
| 33 |
+
|
| 34 |
+
if macd_key in df.columns and not pd.isna(df[macd_key].iloc[i]):
|
| 35 |
+
macd = df[macd_key].iloc[i]
|
| 36 |
+
macd_prev = df[macd_key].iloc[i-1] if i > 0 else 0
|
| 37 |
+
macd_signal = 1 if macd > 0 and macd_prev <= 0 else -1 if macd < 0 and macd_prev >= 0 else 0
|
| 38 |
+
vote += macd_signal
|
| 39 |
+
logging.debug(f"MACD at {df.index[i]}: value={macd:.2f}, prev={macd_prev:.2f}, signal={macd_signal}")
|
| 40 |
+
|
| 41 |
+
if adx_key in df.columns and pdi_key in df.columns and mdi_key in df.columns:
|
| 42 |
+
adx = df[adx_key].iloc[i]
|
| 43 |
+
pdi = df[pdi_key].iloc[i]
|
| 44 |
+
mdi = df[mdi_key].iloc[i]
|
| 45 |
+
if not pd.isna(adx) and adx > 20:
|
| 46 |
+
adx_signal = 1 if pdi > mdi else -1 if mdi > pdi else 0
|
| 47 |
+
vote += adx_signal
|
| 48 |
+
logging.debug(f"ADX at {df.index[i]}: adx={adx:.2f}, pdi={pdi:.2f}, mdi={mdi:.2f}, signal={adx_signal}")
|
| 49 |
+
|
| 50 |
+
if sentiment_key in df.columns and not pd.isna(df[sentiment_key].iloc[i]):
|
| 51 |
+
sentiment = df[sentiment_key].iloc[i]
|
| 52 |
+
sentiment_signal = 1 if sentiment > 0.1 else -1 if sentiment < -0.1 else 0
|
| 53 |
+
vote += sentiment_signal
|
| 54 |
+
logging.debug(f"Sentiment at {df.index[i]}: value={sentiment:.2f}, signal={sentiment_signal}")
|
| 55 |
+
|
| 56 |
+
if "forecast" in result and len(result["forecast"]) > i:
|
| 57 |
+
forecast = result["forecast"][i]
|
| 58 |
+
actual = df["value"].iloc[i]
|
| 59 |
+
model_signal = 1 if forecast > actual * 1.01 else -1 if forecast < actual * 0.99 else 0
|
| 60 |
+
vote += model_signal
|
| 61 |
+
logging.debug(f"Model at {df.index[i]}: forecast={forecast:.2f}, actual={actual:.2f}, signal={model_signal}")
|
| 62 |
+
|
| 63 |
+
signals_df.loc[df.index[i], "Signal"] = "Buy" if vote >= 2 else "Sell" if vote <= -2 else "Hold"
|
| 64 |
+
signals_df.loc[df.index[i], "Position_Size"] = min(0.1 * abs(vote), 1.0)
|
| 65 |
+
|
| 66 |
+
if atr_key in df.columns and not pd.isna(df[atr_key].iloc[i]):
|
| 67 |
+
atr = df[atr_key].iloc[i]
|
| 68 |
+
signals_df.loc[df.index[i], "Stop_Loss"] = df["value"].iloc[i] - 2 * atr if vote >= 2 else df["value"].iloc[i] + 2 * atr if vote <= -2 else np.nan
|
| 69 |
+
signals_df.loc[df.index[i], "Take_Profit"] = df["value"].iloc[i] + 3 * atr if vote >= 2 else df["value"].iloc[i] - 3 * atr if vote <= -2 else np.nan
|
| 70 |
+
|
| 71 |
+
current_signal = signals_df.iloc[i]["Signal"]
|
| 72 |
+
logging.debug(f"Signal at {df.index[i]}: RSI={rsi_signal}, MACD={macd_signal}, ADX={adx_signal}, Sentiment={sentiment_signal}, Model={model_signal}, Vote={vote}, Signal={current_signal}")
|
| 73 |
+
|
| 74 |
+
trades_df, equity_df = backtest_signals(signals_df, df)
|
| 75 |
+
signals_df["Equity"] = equity_df["Equity"]
|
| 76 |
+
|
| 77 |
+
signal_counts = signals_df["Signal"].value_counts().to_dict()
|
| 78 |
+
total = sum(signal_counts.values())
|
| 79 |
+
signal_dist = {k: f"{v} ({v/total*100:.2f}%)" for k, v in signal_counts.items()}
|
| 80 |
+
signal_dist_str = ", ".join([f'{k}={v}' for k, v in signal_dist.items()])
|
| 81 |
+
logging.info(f"Signal distribution: {signal_dist_str}")
|
| 82 |
+
logging.info(f"Signals generated: {signal_counts}")
|
| 83 |
+
return signals_df, trades_df, equity_df
|
| 84 |
+
except Exception as e:
|
| 85 |
+
logging.error(f"Error in generate_signals: {e}")
|
| 86 |
+
return pd.DataFrame(), pd.DataFrame(), pd.DataFrame()
|
| 87 |
+
|
| 88 |
+
def backtest_signals(signals_df, df, initial_balance=10000):
|
| 89 |
+
try:
|
| 90 |
+
balance = initial_balance
|
| 91 |
+
position = 0
|
| 92 |
+
trades = []
|
| 93 |
+
equity_curve = [balance]
|
| 94 |
+
entry_price = 0
|
| 95 |
+
|
| 96 |
+
# Iterate through the original DataFrame's index to ensure equity_curve aligns
|
| 97 |
+
for idx, row in df.iterrows():
|
| 98 |
+
# Find the corresponding signal for this date
|
| 99 |
+
signal_row = signals_df.loc[idx] if idx in signals_df.index else None
|
| 100 |
+
|
| 101 |
+
if signal_row is not None:
|
| 102 |
+
price = signal_row["Price"]
|
| 103 |
+
signal = signal_row["Signal"]
|
| 104 |
+
position_size = signal_row["Position_Size"]
|
| 105 |
+
stop_loss = signal_row["Stop_Loss"]
|
| 106 |
+
take_profit = signal_row["Take_Profit"]
|
| 107 |
+
|
| 108 |
+
if signal == "Buy" and position == 0:
|
| 109 |
+
shares = position_size * balance / price
|
| 110 |
+
position = shares
|
| 111 |
+
entry_price = price
|
| 112 |
+
trades.append({"Date": str(idx.date()), "Type": "Buy", "Price": price, "Shares": shares})
|
| 113 |
+
logging.debug(f"Buy at {price:.2f}, Shares: {shares:.2f}")
|
| 114 |
+
|
| 115 |
+
elif signal == "Sell" and position > 0:
|
| 116 |
+
balance += position * (price - entry_price)
|
| 117 |
+
trades.append({"Date": str(idx.date()), "Type": "Sell", "Price": price, "Shares": position, "Profit": position * (price - entry_price)})
|
| 118 |
+
position = 0
|
| 119 |
+
profit_val = trades[-1]["Profit"]
|
| 120 |
+
logging.debug(f"Sell at {price:.2f}, Profit: {profit_val:.2f}")
|
| 121 |
+
|
| 122 |
+
if position > 0 and not pd.isna(stop_loss) and not pd.isna(take_profit):
|
| 123 |
+
if price <= stop_loss or price >= take_profit:
|
| 124 |
+
balance += position * (price - entry_price)
|
| 125 |
+
trades.append({"Date": str(idx.date()), "Type": "Exit", "Price": price, "Shares": position, "Profit": position * (price - entry_price)})
|
| 126 |
+
position = 0
|
| 127 |
+
profit_val = trades[-1]["Profit"]
|
| 128 |
+
logging.debug(f"Exit at {price:.2f}, Profit: {profit_val:.2f}")
|
| 129 |
+
|
| 130 |
+
current_equity = balance + position * (row["value"] - entry_price) if position > 0 else balance
|
| 131 |
+
equity_curve.append(current_equity)
|
| 132 |
+
|
| 133 |
+
# The first element of equity_curve is the initial balance, remove it to align with df.index
|
| 134 |
+
equity_curve = equity_curve[1:]
|
| 135 |
+
|
| 136 |
+
trades_df = pd.DataFrame(trades)
|
| 137 |
+
equity_df = pd.DataFrame({"Equity": equity_curve}, index=df.index)
|
| 138 |
+
logging.info(f"Backtest completed: {len(trades)} trades, Final Balance: {balance:.2f}")
|
| 139 |
+
return trades_df, equity_df
|
| 140 |
+
except Exception as e:
|
| 141 |
+
logging.error(f"Backtest error: {e}")
|
| 142 |
+
return pd.DataFrame(), pd.DataFrame(), pd.DataFrame(), pd.DataFrame()
|
core/train_eval (3).py
ADDED
|
@@ -0,0 +1,324 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import numpy as np
|
| 2 |
+
import pandas as pd
|
| 3 |
+
import torch
|
| 4 |
+
from torch import nn, optim
|
| 5 |
+
from torch.utils.data import DataLoader, TensorDataset
|
| 6 |
+
from sklearn.preprocessing import MinMaxScaler
|
| 7 |
+
from sklearn.metrics import (
|
| 8 |
+
mean_squared_error,
|
| 9 |
+
mean_absolute_error,
|
| 10 |
+
r2_score,
|
| 11 |
+
precision_score,
|
| 12 |
+
recall_score,
|
| 13 |
+
)
|
| 14 |
+
from sklearn.decomposition import PCA
|
| 15 |
+
from sklearn.ensemble import RandomForestRegressor
|
| 16 |
+
import logging
|
| 17 |
+
import torch.optim.lr_scheduler as lr_scheduler
|
| 18 |
+
from io import StringIO
|
| 19 |
+
import sys
|
| 20 |
+
|
| 21 |
+
try:
|
| 22 |
+
from torchsummary import summary
|
| 23 |
+
except Exception:
|
| 24 |
+
summary = None
|
| 25 |
+
|
| 26 |
+
logging.basicConfig(
|
| 27 |
+
level=logging.DEBUG,
|
| 28 |
+
filename="/tmp/app_log.txt",
|
| 29 |
+
filemode="a",
|
| 30 |
+
format="%(asctime)s - %(levelname)s - %(message)s",
|
| 31 |
+
)
|
| 32 |
+
|
| 33 |
+
# ---------------- Utility metrics ----------------
|
| 34 |
+
def mean_absolute_percentage_error(y_true, y_pred):
|
| 35 |
+
y_true, y_pred = np.array(y_true), np.array(y_pred)
|
| 36 |
+
non_zero = np.abs(y_true) > 0
|
| 37 |
+
if np.sum(non_zero) == 0:
|
| 38 |
+
logging.warning("All true values are zero in MAPE calculation")
|
| 39 |
+
return np.nan
|
| 40 |
+
return np.mean(np.abs((y_true[non_zero] - y_pred[non_zero]) / y_true[non_zero])) * 100
|
| 41 |
+
|
| 42 |
+
def directional_accuracy(y_true, y_pred):
|
| 43 |
+
true_diff = np.diff(y_true)
|
| 44 |
+
pred_diff = np.diff(y_pred)
|
| 45 |
+
if len(true_diff) == 0:
|
| 46 |
+
logging.warning("Insufficient data for directional accuracy")
|
| 47 |
+
return np.nan
|
| 48 |
+
return np.mean(np.sign(true_diff) == np.sign(pred_diff))
|
| 49 |
+
|
| 50 |
+
def mase(y_true, y_pred, y_train):
|
| 51 |
+
mae_val = mean_absolute_error(y_true, y_pred)
|
| 52 |
+
naive_mae = mean_absolute_error(y_train[1:], y_train[:-1]) if len(y_train) > 1 else np.nan
|
| 53 |
+
if naive_mae == 0:
|
| 54 |
+
logging.warning("Naive MAE is zero in MASE calculation")
|
| 55 |
+
return np.nan
|
| 56 |
+
return mae_val / naive_mae
|
| 57 |
+
|
| 58 |
+
def compute_volatility(y_pred):
|
| 59 |
+
returns = np.diff(y_pred) / y_pred[:-1]
|
| 60 |
+
if len(returns) == 0:
|
| 61 |
+
logging.warning("Insufficient data for volatility calculation")
|
| 62 |
+
return np.nan
|
| 63 |
+
return np.std(returns) * np.sqrt(252)
|
| 64 |
+
|
| 65 |
+
def compute_sharpe_ratio(y_pred, risk_free_rate=0.01):
|
| 66 |
+
returns = np.diff(y_pred) / y_pred[:-1]
|
| 67 |
+
if len(returns) == 0:
|
| 68 |
+
logging.warning("Insufficient data for Sharpe ratio calculation")
|
| 69 |
+
return np.nan
|
| 70 |
+
mean_return = np.mean(returns)
|
| 71 |
+
std_return = np.std(returns)
|
| 72 |
+
if std_return == 0:
|
| 73 |
+
logging.warning("Standard deviation of returns is zero in Sharpe ratio")
|
| 74 |
+
return np.nan
|
| 75 |
+
return (mean_return - risk_free_rate) / std_return
|
| 76 |
+
|
| 77 |
+
def compute_precision_recall(y_true, y_pred):
|
| 78 |
+
true_diff = np.sign(np.diff(y_true))
|
| 79 |
+
pred_diff = np.sign(np.diff(y_pred))
|
| 80 |
+
if len(true_diff) == 0:
|
| 81 |
+
logging.warning("Insufficient data for precision/recall calculation")
|
| 82 |
+
return np.nan, np.nan
|
| 83 |
+
precision = precision_score(true_diff > 0, pred_diff > 0, zero_division=0)
|
| 84 |
+
recall = recall_score(true_diff > 0, pred_diff > 0, zero_division=0)
|
| 85 |
+
return precision, recall
|
| 86 |
+
|
| 87 |
+
# ---------------- Feature selection ----------------
|
| 88 |
+
def select_features(df, features, target, selector_method, importance_threshold):
|
| 89 |
+
logging.info(
|
| 90 |
+
f"Selecting features with method: {selector_method}, threshold: {importance_threshold}"
|
| 91 |
+
)
|
| 92 |
+
if selector_method == "RandomForest":
|
| 93 |
+
try:
|
| 94 |
+
X = df[features].dropna()
|
| 95 |
+
y = df[target].loc[X.index]
|
| 96 |
+
rf = RandomForestRegressor(n_estimators=100, random_state=42)
|
| 97 |
+
rf.fit(X, y)
|
| 98 |
+
importances = pd.Series(rf.feature_importances_, index=features)
|
| 99 |
+
selected_features = importances[importances >= importance_threshold].index.tolist()
|
| 100 |
+
logging.debug(f"RandomForest selected features: {selected_features}, importances: {importances.to_dict()}")
|
| 101 |
+
return selected_features if selected_features else features
|
| 102 |
+
except Exception as e:
|
| 103 |
+
logging.error(f"RandomForest feature selection failed: {str(e)}")
|
| 104 |
+
return features
|
| 105 |
+
elif selector_method == "PCA":
|
| 106 |
+
try:
|
| 107 |
+
X = df[features].dropna()
|
| 108 |
+
scaler = MinMaxScaler()
|
| 109 |
+
X_scaled = scaler.fit_transform(X)
|
| 110 |
+
n_components = min(len(features), X_scaled.shape[0], 10)
|
| 111 |
+
pca = PCA(n_components=n_components)
|
| 112 |
+
pca.fit(X_scaled)
|
| 113 |
+
explained_variance_ratio = pca.explained_variance_ratio_.cumsum()
|
| 114 |
+
n_selected = sum(explained_variance_ratio < 0.95) + 1 if any(explained_variance_ratio < 0.95) else n_components
|
| 115 |
+
selected_features = features[:n_selected]
|
| 116 |
+
logging.debug(f"PCA selected features: {selected_features}, explained variance: {explained_variance_ratio.tolist()}")
|
| 117 |
+
return selected_features if selected_features else features
|
| 118 |
+
except Exception as e:
|
| 119 |
+
logging.error(f"PCA feature selection failed: {str(e)}")
|
| 120 |
+
return features
|
| 121 |
+
else:
|
| 122 |
+
logging.warning(f"Unsupported selector_method: {selector_method}, using all features")
|
| 123 |
+
return features
|
| 124 |
+
|
| 125 |
+
def train_and_evaluate(
|
| 126 |
+
df,
|
| 127 |
+
features,
|
| 128 |
+
target,
|
| 129 |
+
model_cls,
|
| 130 |
+
horizon=1,
|
| 131 |
+
hidden=64,
|
| 132 |
+
layers=1,
|
| 133 |
+
epochs=50,
|
| 134 |
+
lr=0.001,
|
| 135 |
+
beta1=0.9,
|
| 136 |
+
beta2=0.999,
|
| 137 |
+
weight_decay=0.01,
|
| 138 |
+
dropout=0.2,
|
| 139 |
+
window=30,
|
| 140 |
+
test_split=0.2,
|
| 141 |
+
selector_method="RandomForest",
|
| 142 |
+
importance_threshold=0.0,
|
| 143 |
+
scheduler_type="None",
|
| 144 |
+
device='cpu',
|
| 145 |
+
verbose=True
|
| 146 |
+
):
|
| 147 |
+
try:
|
| 148 |
+
logging.info(f"Starting train_and_evaluate: model={model_cls.__name__}, features={len(features)}, window={window}, horizon={horizon}, scheduler={scheduler_type}, selector_method={selector_method}")
|
| 149 |
+
from .data import preprocess_data
|
| 150 |
+
|
| 151 |
+
selected_features = select_features(df, features, target, selector_method, importance_threshold)
|
| 152 |
+
logging.info(f"Selected features: {selected_features}")
|
| 153 |
+
|
| 154 |
+
X, y, feature_scaler, target_scaler, full_features, target_idx, pca, updated_feature_cols = preprocess_data(df, selected_features, target, window, horizon)
|
| 155 |
+
logging.debug(f"Preprocess: type(X)={type(X)}, example={X if isinstance(X, tuple) else X.shape}, type(y)={type(y)}, example={y if isinstance(y, tuple) else y.shape}")
|
| 156 |
+
|
| 157 |
+
if X.shape[0] < 10:
|
| 158 |
+
logging.error(f"Insufficient data samples: {X.shape[0]}")
|
| 159 |
+
return {"error": f"Insufficient data samples: {X.shape[0]}"}
|
| 160 |
+
|
| 161 |
+
train_size = int((1 - test_split) * len(X))
|
| 162 |
+
X_train, X_test = X[:train_size], X[train_size:]
|
| 163 |
+
y_train, y_test = y[:train_size], y[train_size:]
|
| 164 |
+
logging.debug(f"Train size: {len(X_train)}, Test size: {len(X_test)}")
|
| 165 |
+
logging.debug(f"X_train type: {type(X_train)}, shape: {X_train.shape if isinstance(X_train, np.ndarray) else 'not ndarray'}")
|
| 166 |
+
logging.debug(f"X_test type: {type(X_test)}, shape: {X_test.shape if isinstance(X_test, np.ndarray) else 'not ndarray'}")
|
| 167 |
+
|
| 168 |
+
train_dataset = TensorDataset(torch.tensor(X_train, dtype=torch.float32).to(device),
|
| 169 |
+
torch.tensor(y_train, dtype=torch.float32).to(device))
|
| 170 |
+
test_dataset = TensorDataset(torch.tensor(X_test, dtype=torch.float32).to(device),
|
| 171 |
+
torch.tensor(y_test, dtype=torch.float32).to(device))
|
| 172 |
+
train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True)
|
| 173 |
+
test_loader = DataLoader(test_dataset, batch_size=32, shuffle=False)
|
| 174 |
+
|
| 175 |
+
# Debug DataLoader output
|
| 176 |
+
for batch_X, batch_y in train_loader:
|
| 177 |
+
logging.debug(f"DataLoader train batch: X_type={type(batch_X)}, X_shape={batch_X.shape}, y_type={type(batch_y)}, y_shape={batch_y.shape}")
|
| 178 |
+
break
|
| 179 |
+
for batch_X, batch_y in test_loader:
|
| 180 |
+
logging.debug(f"DataLoader test batch: X_type={type(batch_X)}, X_shape={batch_X.shape}, y_type={type(batch_y)}, y_shape={batch_y.shape}")
|
| 181 |
+
break
|
| 182 |
+
|
| 183 |
+
input_size = X.shape[2]
|
| 184 |
+
model = model_cls(input_size=input_size, hidden_size=hidden, num_layers=layers, output_size=horizon, dropout=dropout).to(device)
|
| 185 |
+
logging.debug(f"Model initialized: {model_cls.__name__}, input_size={input_size}, hidden={hidden}, layers={layers}")
|
| 186 |
+
|
| 187 |
+
# if verbose and summary:
|
| 188 |
+
# try:
|
| 189 |
+
# output = StringIO()
|
| 190 |
+
# sys.stdout = output
|
| 191 |
+
# summary(model, input_size=(window, input_size))
|
| 192 |
+
# sys.stdout = sys.__stdout__
|
| 193 |
+
# logging.debug(f"Model summary:\n{output.getvalue()}")
|
| 194 |
+
# except Exception as e:
|
| 195 |
+
# logging.warning(f"Failed to generate model summary: {str(e)}")
|
| 196 |
+
|
| 197 |
+
optimizer = optim.Adam(model.parameters(), lr=lr, betas=(beta1, beta2), weight_decay=weight_decay)
|
| 198 |
+
criterion = nn.MSELoss()
|
| 199 |
+
scheduler = None
|
| 200 |
+
if scheduler_type == "ReduceLROnPlateau":
|
| 201 |
+
scheduler = lr_scheduler.ReduceLROnPlateau(optimizer, mode='min', factor=0.5, patience=10, verbose=verbose)
|
| 202 |
+
logging.debug("Initialized ReduceLROnPlateau scheduler")
|
| 203 |
+
elif scheduler_type != "None":
|
| 204 |
+
logging.warning(f"Unsupported scheduler type: {scheduler_type}, using None")
|
| 205 |
+
|
| 206 |
+
train_losses = []
|
| 207 |
+
val_losses = []
|
| 208 |
+
|
| 209 |
+
for epoch in range(epochs):
|
| 210 |
+
model.train()
|
| 211 |
+
train_loss = 0.0
|
| 212 |
+
for batch_X, batch_y in train_loader:
|
| 213 |
+
logging.debug(f"Training Batch_X type: {type(batch_X)}, shape: {batch_X.shape}")
|
| 214 |
+
logging.debug(f"Training Batch_Y type: {type(batch_y)}, shape: {batch_y.shape}")
|
| 215 |
+
optimizer.zero_grad()
|
| 216 |
+
logging.debug(f"Training input to model: type={type(batch_X)}, example={batch_X if isinstance(batch_X, tuple) else batch_X.shape}")
|
| 217 |
+
try:
|
| 218 |
+
outputs = model(batch_X)
|
| 219 |
+
logging.debug(f"Training model output shape: {outputs.shape}")
|
| 220 |
+
except Exception as e:
|
| 221 |
+
logging.error(f"Training model forward error: {str(e)}, batch_X_type={type(batch_X)}, batch_X_shape={batch_X.shape}")
|
| 222 |
+
raise
|
| 223 |
+
loss = criterion(outputs, batch_y)
|
| 224 |
+
loss.backward()
|
| 225 |
+
optimizer.step()
|
| 226 |
+
train_loss += loss.item() * batch_X.size(0)
|
| 227 |
+
train_loss /= len(train_loader.dataset)
|
| 228 |
+
train_losses.append(train_loss)
|
| 229 |
+
|
| 230 |
+
model.eval()
|
| 231 |
+
val_loss = 0.0
|
| 232 |
+
with torch.no_grad():
|
| 233 |
+
for batch_X, batch_y in test_loader:
|
| 234 |
+
logging.debug(f"Validation Batch_X type: {type(batch_X)}, shape: {batch_X.shape}")
|
| 235 |
+
logging.debug(f"Validation Batch_Y type: {type(batch_y)}, shape: {batch_y.shape}")
|
| 236 |
+
logging.debug(f"Validation input to model: type={type(batch_X)}, example={batch_X if isinstance(batch_X, tuple) else batch_X.shape}")
|
| 237 |
+
try:
|
| 238 |
+
outputs = model(batch_X)
|
| 239 |
+
logging.debug(f"Validation model output shape: {outputs.shape}")
|
| 240 |
+
except Exception as e:
|
| 241 |
+
logging.error(f"Validation model forward error: {str(e)}, batch_X_type={type(batch_X)}, batch_X_shape={batch_X.shape}")
|
| 242 |
+
raise
|
| 243 |
+
loss = criterion(outputs, batch_y)
|
| 244 |
+
val_loss += loss.item() * batch_X.size(0)
|
| 245 |
+
val_loss /= len(test_loader.dataset)
|
| 246 |
+
val_losses.append(val_loss)
|
| 247 |
+
|
| 248 |
+
if scheduler:
|
| 249 |
+
scheduler.step(val_loss)
|
| 250 |
+
current_lr = optimizer.param_groups[0]['lr']
|
| 251 |
+
logging.debug(f"Epoch {epoch+1}/{epochs}, Train Loss: {train_loss:.6f}, Val Loss: {val_loss:.6f}, LR: {current_lr}")
|
| 252 |
+
else:
|
| 253 |
+
logging.debug(f"Epoch {epoch+1}/{epochs}, Train Loss: {train_loss:.6f}, Val Loss: {val_loss:.6f}")
|
| 254 |
+
|
| 255 |
+
# ---------------- Evaluation ----------------
|
| 256 |
+
model.eval()
|
| 257 |
+
with torch.no_grad():
|
| 258 |
+
X_test_tensor = torch.tensor(X_test, dtype=torch.float32).to(device)
|
| 259 |
+
logging.debug(f"Eval model call: type={type(X_test_tensor)}, example={X_test_tensor if isinstance(X_test_tensor, tuple) else X_test_tensor.shape}")
|
| 260 |
+
try:
|
| 261 |
+
y_pred_scaled = model(X_test_tensor).cpu().numpy()
|
| 262 |
+
logging.debug(f"Eval model output shape: {y_pred_scaled.shape}")
|
| 263 |
+
except Exception as e:
|
| 264 |
+
logging.error(f"Eval model forward error: {str(e)}, X_test_type={type(X_test_tensor)}, X_test_shape={X_test_tensor.shape}")
|
| 265 |
+
raise
|
| 266 |
+
|
| 267 |
+
y_test_unscaled = target_scaler.inverse_transform(y_test.reshape(-1, horizon)).flatten()
|
| 268 |
+
y_pred_unscaled = target_scaler.inverse_transform(y_pred_scaled.reshape(-1, horizon)).flatten()
|
| 269 |
+
|
| 270 |
+
precision, recall = compute_precision_recall(y_test_unscaled, y_pred_unscaled)
|
| 271 |
+
|
| 272 |
+
metrics = {
|
| 273 |
+
"R2": float(r2_score(y_test_unscaled, y_pred_unscaled)),
|
| 274 |
+
"MAPE": float(mean_absolute_percentage_error(y_test_unscaled, y_pred_unscaled)),
|
| 275 |
+
"RMSE": float(np.sqrt(mean_squared_error(y_test_unscaled, y_pred_unscaled))),
|
| 276 |
+
"MAE": float(mean_absolute_error(y_test_unscaled, y_pred_unscaled)),
|
| 277 |
+
"DirAcc": float(directional_accuracy(y_test_unscaled, y_pred_unscaled)),
|
| 278 |
+
"MASE": float(
|
| 279 |
+
mase(
|
| 280 |
+
y_test_unscaled,
|
| 281 |
+
y_pred_unscaled,
|
| 282 |
+
target_scaler.inverse_transform(y_train.reshape(-1, horizon)).flatten(),
|
| 283 |
+
)
|
| 284 |
+
),
|
| 285 |
+
"Volatility": float(compute_volatility(y_pred_unscaled)),
|
| 286 |
+
"Sharpe": float(compute_sharpe_ratio(y_pred_unscaled)),
|
| 287 |
+
"Precision": float(np.nan if np.isnan(precision) else precision),
|
| 288 |
+
"Recall": float(np.nan if np.isnan(recall) else recall),
|
| 289 |
+
}
|
| 290 |
+
|
| 291 |
+
# Latest prediction (use last window from original X)
|
| 292 |
+
latest_data = torch.tensor(X[-1:], dtype=torch.float32).to(device)
|
| 293 |
+
with torch.no_grad():
|
| 294 |
+
logging.debug(f"Latest prediction input: type={type(latest_data)}, shape={latest_data.shape}")
|
| 295 |
+
latest_prediction_scaled = model(latest_data).cpu().numpy()
|
| 296 |
+
latest_prediction = target_scaler.inverse_transform(
|
| 297 |
+
latest_prediction_scaled.reshape(-1, horizon)
|
| 298 |
+
).flatten()
|
| 299 |
+
|
| 300 |
+
result = {
|
| 301 |
+
"model": model,
|
| 302 |
+
"train_loss": train_losses,
|
| 303 |
+
"val_loss": val_losses,
|
| 304 |
+
"metrics": metrics,
|
| 305 |
+
"actual": y_test_unscaled,
|
| 306 |
+
"forecast": y_pred_unscaled,
|
| 307 |
+
"latest_prediction": latest_prediction,
|
| 308 |
+
"arch": {
|
| 309 |
+
"input_size": input_size,
|
| 310 |
+
"hidden": hidden,
|
| 311 |
+
"layers": layers,
|
| 312 |
+
"dropout": dropout,
|
| 313 |
+
"window": window,
|
| 314 |
+
},
|
| 315 |
+
"scalers": {"feature_scaler": feature_scaler, "target_scaler": target_scaler},
|
| 316 |
+
"features": updated_feature_cols,
|
| 317 |
+
}
|
| 318 |
+
|
| 319 |
+
logging.info("Training and evaluation completed successfully")
|
| 320 |
+
return result
|
| 321 |
+
|
| 322 |
+
except Exception as e:
|
| 323 |
+
logging.error(f"Error in train_and_evaluate: {str(e)}")
|
| 324 |
+
return {"error": str(e)}
|