Aliazimi00 commited on
Commit
634118a
·
verified ·
1 Parent(s): 85f44ed

Upload 6 files

Browse files
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)}