File size: 18,162 Bytes
634118a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
import pandas as pd
import numpy as np
import yfinance as yf
import os
try:
    import talib as ta
except ImportError:
    ta = None
from datetime import datetime, timedelta
from newsapi import NewsApiClient
from vaderSentiment.vaderSentiment import SentimentIntensityAnalyzer
from sklearn.preprocessing import MinMaxScaler
from alpha_vantage.timeseries import TimeSeries
import time
import logging

# Configure logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')

def print_log(message, level='INFO'):
    if level == 'INFO':
        logging.info(message)
    elif level == 'WARNING':
        logging.warning(message)
    elif level == 'ERROR':
        logging.error(message)
    else:
        logging.debug(message)

analyzer = SentimentIntensityAnalyzer()

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):
    try:
        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'}")

        start_date = pd.to_datetime(start)
        end_date = pd.to_datetime(end)
        if start_date >= end_date:
            raise ValueError(f"Start date {start} must be before end date {end}")
        if end_date > datetime.now():
            print_log(f"End date {end} is in the future. Using current date as end date.", 'WARNING')
            end_date = datetime.now()

        df = pd.DataFrame()

        if data_src == 'csv' and file_upload:
            try:
                file_path = getattr(file_upload, 'name', file_upload)
                print_log(f"Loading CSV from {file_path}")
                df = pd.read_csv(file_path)
                if 'Date' not in df.columns:
                    raise ValueError("CSV must contain a 'Date' column")
                df['Date'] = pd.to_datetime(df['Date']).dt.tz_localize(None)
                df = df.set_index('Date')
                if 'Close' not in df.columns and 'value' not in df.columns:
                    raise ValueError("CSV must contain 'Close' or 'value' column")
                if 'Close' in df.columns:
                    df = df.rename(columns={'Close': 'value'})
                if df.empty:
                    raise ValueError(f"CSV data is empty for {ticker}")
                if df['value'].isna().all():
                    raise ValueError(f"CSV 'value' column contains only NaNs for {ticker}")
            except Exception as e:
                print_log(f"Failed to load CSV {file_path}: {str(e)}", 'ERROR')
                raise ValueError(f"Failed to load CSV: {str(e)}")
        else:
            print_log(f"Fetching data for {ticker} from Yahoo Finance")
            try:
                df = yf.download(ticker, start=start_date, end=end_date, interval=interval, progress=False, auto_adjust=False)
                if isinstance(df.columns, pd.MultiIndex):
                    df.columns = df.columns.droplevel(1)
                if df.empty:
                    raise ValueError(f"No data returned from Yahoo Finance for {ticker}")
                if 'Close' not in df.columns:
                    raise ValueError(f"Yahoo Finance data missing 'Close' column for {ticker}")
                df = df.rename(columns={'Close': 'value'})
                # The index is already datetime, no need to create a 'Date' column and then reset
                if df['value'].isna().all():
                    raise ValueError(f"Yahoo Finance 'value' column contains only NaNs for {ticker}")
                if df['value'].empty:
                    raise ValueError(f"Yahoo Finance 'value' column is empty for {ticker}")
            except Exception as e:
                print_log(f"Yahoo Finance failed for {ticker}: {str(e)}", 'ERROR')
                raise ValueError(f"Yahoo Finance failed for {ticker}: {str(e)}")

        # Alpha Vantage data loading (if applicable)
        # Note: Alpha Vantage data loading logic is commented out for now to simplify debugging
        # if interval in ['1m', '5m', '15m', '30m', '60m'] and alpha_api_key:
        #     print_log(f"Attempting Alpha Vantage for {ticker}, interval {interval}")
        #     try:
        #         ts = TimeSeries(key=alpha_api_key, output_format='pandas')
        #         df_av, _ = ts.get_intraday(symbol=ticker, interval=interval, outputsize='full')
        #         if df_av.empty:
        #             raise ValueError(f"No data returned from Alpha Vantage for {ticker}")
        #         if '4. close' not in df_av.columns:
        #             raise ValueError(f"Alpha Vantage data missing '4. close' column for {ticker}")
        #         df_av = df_av.rename(columns={'4. close': 'value', '1. open': 'Open', '2. high': 'High', '3. low': 'Low', '5. volume': 'Volume'}) # Standardize column names
        #         df_av['Date'] = pd.to_datetime(df_av.index)
        #         df_av = df_av.reset_index(drop=True)
        #         if df_av['value'].isna().all():
        #             raise ValueError(f"Alpha Vantage 'value' column contains only NaNs for {ticker}")
        #         if df_av['value'].empty:
        #             raise ValueError(f"Alpha Vantage 'value' column is empty for {ticker}")
        #         df = df_av # Use Alpha Vantage data if successful
        #     except Exception as e:
        #         print_log(f"Alpha Vantage failed: {str(e)}, using Yahoo Finance data", 'WARNING')

        if df.empty:
            raise ValueError(f"No data loaded for {ticker} from {data_src}")

        # Ensure index is DatetimeIndex and sorted
        if not isinstance(df.index, pd.DatetimeIndex):
            df.index = pd.to_datetime(df.index)
        df = df.sort_index()

        required_cols = ['Open', 'High', 'Low', 'value', 'Volume']
        for col in required_cols:
            if col not in df.columns:
                df[col] = np.nan # Add missing columns with NaNs

        if 'value' not in df.columns:
            raise ValueError(f"Target column 'value' is missing for {ticker}")
        if df['value'].isna().all():
            raise ValueError(f"Target column 'value' contains only NaNs for {ticker}")
        if df['value'].empty:
            raise ValueError(f"Target column 'value' is empty for {ticker}")

        print_log(f"Data loaded for {ticker} with date range: {df.index.min()} to {df.index.max()}, shape: {df.shape}")
        return df
    except Exception as e:
        print_log(f"Error in load_data for {ticker}: {str(e)}", 'ERROR')
        raise ValueError(f"Failed to load data for {ticker}: {str(e)}")

def add_technical_indicators(df, selected_indicators):
    try:
        print_log(f"Starting add_technical_indicators with indicators: {selected_indicators}")
        if df.empty:
            print_log("DataFrame is empty, skipping technical indicator calculation.", "WARNING")
            return df, []

        # Ensure columns are numeric and handle missing ones
        for col in ['Open', 'High', 'Low', 'value', 'Volume']:
            if col not in df.columns:
                df[col] = np.nan
            df[col] = pd.to_numeric(df[col], errors='coerce')
        
        # Drop rows with NaN in core columns after indicator calculation
        df.dropna(subset=['Open', 'High', 'Low', 'value', 'Volume'], inplace=True)
        if df.empty:
            print_log("DataFrame is empty after dropping NaNs for technical indicators.", "WARNING")
            return df, []

        if ta is None:
            print_log("TA-Lib not available. Cannot compute indicators. Falling back to 'value'.", 'ERROR')
            return df, []

        close = df['value'].values
        high = df['High'].values
        low = df['Low'].values
        volume = df['Volume'].values
        open_ = df['Open'].values

        indicator_map = {
            'rsi': {'func': ta.RSI, 'inputs': ['close'], 'params': {'timeperiod': 14}, 'output': ['rsi_14']},
            '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']},
            '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']},
            'sma': {'func': ta.SMA, 'inputs': ['close'], 'params': {'timeperiod': 20}, 'output': ['sma_20']},
            'ema': {'func': ta.EMA, 'inputs': ['close'], 'params': {'timeperiod': 20}, 'output': ['ema_20']},
            'atr': {'func': ta.ATR, 'inputs': ['high', 'low', 'close'], 'params': {'timeperiod': 14}, 'output': ['atr_14']},
            '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']},
            'adx': {'func': ta.ADX, 'inputs': ['high', 'low', 'close'], 'params': {'timeperiod': 14}, 'output': ['adx_14']},
            'willr': {'func': ta.WILLR, 'inputs': ['high', 'low', 'close'], 'params': {'timeperiod': 14}, 'output': ['willr_14']},
            'cci': {'func': ta.CCI, 'inputs': ['high', 'low', 'close'], 'params': {'timeperiod': 20}, 'output': ['cci_20']},
            'pdi': {'func': ta.PLUS_DI, 'inputs': ['high', 'low', 'close'], 'params': {'timeperiod': 14}, 'output': ['pdi_14']},
            'mdi': {'func': ta.MINUS_DI, 'inputs': ['high', 'low', 'close'], 'params': {'timeperiod': 14}, 'output': ['mdi_14']}
        }

        input_dict = {'close': close, 'high': high, 'low': low, 'open': open_, 'volume': volume}

        valid_indicators = []
        for ind in selected_indicators:
            if ind in indicator_map:
                print_log(f"Computing indicator: {ind}")
                config = indicator_map[ind]
                func = config['func']
                inputs = config['inputs']
                params = config['params']
                try:
                    input_arrays = [input_dict[inp] for inp in inputs]
                    result = func(*input_arrays, **params)
                    if isinstance(result, tuple):
                        for j, (res, out_col) in enumerate(zip(result, config['output'])):
                            if isinstance(res, np.ndarray) and len(res) == len(df):
                                df[out_col] = res
                                nan_count = np.isnan(res).sum()
                                if nan_count < len(res) * 0.5:
                                    valid_indicators.append(out_col)
                                else:
                                    print_log(f"{out_col} has excessive NaNs: {nan_count}/{len(res)}. Excluding from valid indicators.", 'WARNING')
                            else:
                                print_log(f"Invalid output for {out_col}: {type(res)}, length: {len(res) if hasattr(res, '__len__') else 'N/A'}", 'WARNING')
                    else:
                        if isinstance(result, np.ndarray) and len(result) == len(df):
                            df[config['output'][0]] = result
                            nan_count = np.isnan(result).sum()
                            if nan_count < len(result) * 0.5:
                                valid_indicators.append(config['output'][0])
                            else:
                                print_log(f"{config['output'][0]} has excessive NaNs: {nan_count}/{len(result)}. Excluding from valid indicators.", 'WARNING')
                        else:
                            print_log(f"Invalid output for {ind}: {type(result)}, length: {len(result) if hasattr(result, '__len__') else 'N/A'}", 'WARNING')
                except Exception as e:
                    print_log(f"Error computing {ind}: {str(e)}", 'ERROR')
            else:
                print_log(f"Indicator {ind} not supported by TA-Lib", 'WARNING')

        # Drop rows with NaN in 'value', preserve valid indicators
        initial_rows = len(df)
        df = df.dropna(subset=['value']).reset_index(drop=False) # Keep index as a column for now
        print_log(f"Dropped {initial_rows - len(df)} rows with NaN in 'value'")

        # Drop columns with excessive NaNs, but protect 'value'
        for col in df.columns:
            if col not in ['Date', 'Open', 'High', 'Low', 'value', 'Volume']:
                nan_ratio = df[col].isna().mean()
                if nan_ratio > 0.5:
                    print_log(f"Dropping {col} due to excessive NaNs: {nan_ratio:.2%}", 'WARNING')
                    df = df.drop(columns=[col])
                    if col in valid_indicators:
                        valid_indicators.remove(col)

        if not valid_indicators:
            print_log("No valid indicators computed. Falling back to 'value'.", "WARNING")
            valid_indicators.append('value')

        print_log(f"Valid indicators: {valid_indicators}")
        print_log(f"Technical indicators added successfully, shape: {df.shape}")
        df.set_index('Date', inplace=True) # Set index back to Date after all processing
        return df, valid_indicators
    except Exception as e:
        print_log(f"Error in add_technical_indicators: {str(e)}", 'ERROR')
        # If an error occurs, return the original DataFrame to prevent further errors
        return df, []

def add_sentiment(df, ticker, news_api_key, start_date, end_date):
    try:
        print_log(f"Starting add_sentiment for {ticker} from {start_date} to {end_date}")
        if not news_api_key:
            print_log("News API key not provided. Skipping sentiment analysis.", "WARNING")
            df['sentiment_score'] = 0.0
            return df

        newsapi = NewsApiClient(api_key=news_api_key)
        all_articles = []
        current_date = pd.to_datetime(start_date)
        end_date = pd.to_datetime(end_date)

        while current_date <= end_date:
            from_param = current_date.strftime("%Y-%m-%d")
            to_param = (current_date + timedelta(days=1)).strftime("%Y-%m-%d")
            print_log(f"Fetching news for {ticker} from {from_param} to {to_param}")
            try:
                articles = newsapi.get_everything(q=ticker, language='en', sort_by='relevancy', from_param=from_param, to=to_param)
                all_articles.extend(articles["articles"])
            except Exception as e:
                print_log(f"Error fetching news for {ticker} on {from_param}: {str(e)}", 'ERROR')
            current_date += timedelta(days=1)
            time.sleep(0.1)

        if not all_articles:
            print_log(f"No articles found for {ticker}. Setting sentiment to 0.", 'WARNING')
            df['sentiment_score'] = 0.0
            return df

        sentiment_data = []
        for article in all_articles:
            if article["publishedAt"] and article["description"]:
                date = pd.to_datetime(article["publishedAt"]).tz_localize(None).date()
                text = article["description"]
                vs = analyzer.polarity_scores(text)
                sentiment_data.append({"Date": date, "sentiment_score": vs["compound"]})

        sentiment_df = pd.DataFrame(sentiment_data)
        sentiment_df["Date"] = pd.to_datetime(sentiment_df["Date"])
        sentiment_df = sentiment_df.groupby("Date")["sentiment_score"].mean().reset_index()

        df.reset_index(inplace=True)
        df['Date'] = pd.to_datetime(df['Date'])
        df = pd.merge(df, sentiment_df, on="Date", how="left")
        df['sentiment_score'] = df['sentiment_score'].fillna(0.0)
        df.set_index('Date', inplace=True)

        print_log(f"Sentiment analysis completed for {ticker}. Added sentiment_score column.")
        return df
    except Exception as e:
        print_log(f"Error in add_sentiment for {ticker}: {str(e)}", 'ERROR')
        df['sentiment_score'] = 0.0
        return df

def preprocess_data(df, features, target, window_size, horizon):
    try:
        print_log(f"Starting preprocessing: features={features}, target={target}, window={window_size}, horizon={horizon}")
        
        # Ensure the DataFrame index is a DatetimeIndex
        if not isinstance(df.index, pd.DatetimeIndex):
            raise ValueError("DataFrame index must be a DatetimeIndex for preprocessing.")

        # Filter features to only include those present in the DataFrame columns
        updated_feature_cols = [f for f in features if f in df.columns]
        if not updated_feature_cols:
            raise ValueError("No valid features found in DataFrame after indicator calculation.")

        full_features = updated_feature_cols + [target]
        data = df[full_features].copy()
        data.dropna(inplace=True)

        if data.empty:
            raise ValueError("DataFrame is empty after dropping NaNs. Cannot proceed with scaling.")

        feature_scaler = MinMaxScaler()
        target_scaler = MinMaxScaler()

        data_features_scaled = feature_scaler.fit_transform(data[updated_feature_cols])
        data_target_scaled = target_scaler.fit_transform(data[[target]])

        full_scaled = np.hstack((data_features_scaled, data_target_scaled))
        target_idx = len(updated_feature_cols)

        X, y = [], []
        for i in range(len(full_scaled) - window_size - horizon + 1):
            X.append(full_scaled[i:i + window_size])
            y.append(full_scaled[i + window_size:i + window_size + horizon, target_idx])

        X = np.array(X)
        y = np.array(y)

        if X.shape[0] == 0 or y.shape[0] == 0:
            raise ValueError(f"Insufficient data after preprocessing. Data length: {len(full_scaled)}, window_size: {window_size}, horizon: {horizon}")

        print_log(f"Preprocessed data: X.shape={X.shape}, y.shape={y.shape}, Final features: {full_features}, Target idx: {target_idx}")
        return X, y, feature_scaler, target_scaler, full_features, target_idx, None, updated_feature_cols
    except Exception as e:
        print_log(f"Preprocessing error: {str(e)}", 'ERROR')
        raise ValueError(f"Preprocessing failed: {str(e)}")