|
| 1 | +# -*- coding: utf-8 -*- |
| 2 | +"""Functions for testing exercises. |
| 3 | +
|
| 4 | +This module is designed to perform various manipulations and analyses on stock price data. |
| 5 | +The data is expected to be in a pandas DataFrame with the following columns: |
| 6 | +
|
| 7 | +- Date: The date of the stock data, in 'YYYY-MM-DD' format. |
| 8 | +- Open: The opening price of the stock for the day. |
| 9 | +- High: The highest price of the stock for the day. |
| 10 | +- Low: The lowest price of the stock for the day. |
| 11 | +- Close: The closing price of the stock for the day. |
| 12 | +- Adj Close: The adjusted closing price of the stock for the day (adjusted for splits and dividends). |
| 13 | +- Volume: The number of shares traded during the day. |
| 14 | +
|
| 15 | +DataFrame example: |
| 16 | +
|
| 17 | + Date Open High Low Close Adj Close Volume |
| 18 | + 2020-01-01 100 110 95 105 105 10000 |
| 19 | + 2020-01-02 105 115 100 110 110 15000 |
| 20 | + ... |
| 21 | +
|
| 22 | +These functions are designed to prepare stock price data for analysis and modeling. |
| 23 | +They enable users to clean and transform the data, extract meaningful features, and ensure the data is |
| 24 | +in a suitable format for various analytical tasks. |
| 25 | +""" |
| 26 | + |
| 27 | +from typing import Union |
| 28 | + |
| 29 | +import pandas as pd |
| 30 | + |
| 31 | +from ._utils import db_connector |
| 32 | + |
| 33 | + |
| 34 | +def preprocess( |
| 35 | + start_date: Union[str, pd.Timestamp], |
| 36 | + end_date: Union[str, pd.Timestamp], |
| 37 | + window: int = 20, |
| 38 | + lag_days: int = 5, |
| 39 | +): |
| 40 | + """Applies a series of preprocessing steps to stock price data. |
| 41 | +
|
| 42 | + This function integrates several preprocessing steps: initial data preparation and normalization, |
| 43 | + technical indicator enhancement, and the addition of lag features. It's designed to prepare stock |
| 44 | + price data for further analysis or modeling. |
| 45 | +
|
| 46 | + :param df: The input DataFrame containing stock price data. |
| 47 | + :param start_date: The start date for filtering the data, in 'YYYY-MM-DD' format. |
| 48 | + :param end_date: The end date for filtering the data, in 'YYYY-MM-DD' format. |
| 49 | + :param window: The window size for calculating moving averages and volatility. Defaults to 20. |
| 50 | + :param lag_days: The number of days to create lag features for. Defaults to 5. |
| 51 | +
|
| 52 | + :returns: The processed DataFrame, ready for analysis or modeling, with normalized dates, |
| 53 | + filtered by the specified date range, enhanced with technical indicators, and augmented with lag features. |
| 54 | + """ |
| 55 | + df = db_connector().read_from_db() |
| 56 | + |
| 57 | + if df is None: |
| 58 | + return None |
| 59 | + |
| 60 | + return ( |
| 61 | + df.pipe(_prepare_and_normalize_data, start_date=start_date, end_date=end_date) |
| 62 | + .pipe(_enhance_with_technical_indicators, window=window) |
| 63 | + .pipe(_add_lags, lag_days=lag_days) |
| 64 | + ) |
| 65 | + |
| 66 | + |
| 67 | +def _prepare_and_normalize_data( |
| 68 | + df: pd.DataFrame, start_date: Union[str, pd.Timestamp], end_date: Union[str, pd.Timestamp] |
| 69 | +): |
| 70 | + """Prepares and normalizes stock price data for analysis. |
| 71 | +
|
| 72 | + This function standardizes the date format, filters the dataset based on a specified date range, |
| 73 | + generates additional date-related features, calculates daily returns, and ensures that the dataset |
| 74 | + has no missing dates within the specified range by forward filling missing values. This comprehensive |
| 75 | + preprocessing step is crucial for subsequent financial analysis and modeling tasks. |
| 76 | +
|
| 77 | + :param df: DataFrame with stock price data, expecting columns 'Date' and 'Adj Close'. |
| 78 | + :param start_date: String representing the start date in 'YYYY-MM-DD' format. |
| 79 | + :param end_date: String representing the end date in 'YYYY-MM-DD' format. |
| 80 | +
|
| 81 | + :return: A DataFrame that has been processed to include only the specified date range, |
| 82 | + with added 'Month' and 'DayOfWeek' features, daily returns calculated, and missing dates |
| 83 | + forward-filled. The DataFrame is indexed by the 'Date' column. |
| 84 | +
|
| 85 | + :raises: ValueError: If the input DataFrame does not contain a 'Date' column or if the 'Date' column |
| 86 | + cannot be converted to datetime format. Also raised if the specified start_date or end_date |
| 87 | + are not in the correct format. |
| 88 | + """ |
| 89 | + if "Date" not in df.columns: |
| 90 | + raise ValueError("DataFrame must contain a 'Date' column.") |
| 91 | + try: |
| 92 | + df["Date"] = pd.to_datetime(df["Date"]) |
| 93 | + except ValueError as e: |
| 94 | + raise ValueError( |
| 95 | + "Error converting 'Date' column to datetime. Ensure the dates are in a correct format." |
| 96 | + ) from e |
| 97 | + |
| 98 | + # Filter Date Range |
| 99 | + start_date = pd.to_datetime(start_date) |
| 100 | + end_date = pd.to_datetime(end_date) |
| 101 | + df = df[(df["Date"] >= start_date) & (df["Date"] <= end_date)] |
| 102 | + |
| 103 | + # Generate Date Features |
| 104 | + df["Month"] = df["Date"].dt.month |
| 105 | + df["DayOfWeek"] = df["Date"].dt.dayofweek |
| 106 | + |
| 107 | + # Calculate Daily Returns |
| 108 | + df["Daily Returns"] = df["Adj Close"].pct_change() |
| 109 | + |
| 110 | + # Fill Missing Dates (forward fill for simplicity) |
| 111 | + df = df.set_index("Date").resample("D").ffill() |
| 112 | + |
| 113 | + return df |
| 114 | + |
| 115 | + |
| 116 | +def _enhance_with_technical_indicators(df: pd.DataFrame, window: int = 20): |
| 117 | + """Enriches a DataFrame with key technical indicators to aid in financial analysis. |
| 118 | +
|
| 119 | + This function calculates and appends several technical indicators to the input DataFrame, |
| 120 | + specifically a moving average and volatility measure. These indicators are widely used in |
| 121 | + stock market analysis to understand price trends and market volatility over a specified |
| 122 | + window of time. Additionally, the function ensures that the necessary prerequisites for |
| 123 | + calculation are met, such as the presence of required columns. |
| 124 | +
|
| 125 | + :param df: DataFrame after initial preprocessing, with 'Date' and 'Close' columns. |
| 126 | + :param window: Integer representing the window size for the moving average. |
| 127 | +
|
| 128 | + :return: The original DataFrame augmented with two new columns: |
| 129 | + - 'MA_{window}': The moving average of the 'Close' prices over the specified window. |
| 130 | + - 'Volatility': The rolling standard deviation of 'Daily Returns', representing price |
| 131 | + volatility over the same window. |
| 132 | +
|
| 133 | + :raises: |
| 134 | + - ValueError: If the input DataFrame lacks the required 'Close' or 'Daily Returns' columns, |
| 135 | + or if the 'window' parameter is not a positive integer. |
| 136 | + - RuntimeError: If an unexpected error occurs during the calculation of technical indicators. |
| 137 | + """ |
| 138 | + # Validate required columns |
| 139 | + required_columns = ["Close", "Daily Returns"] |
| 140 | + missing_columns = [col for col in required_columns if col not in df.columns] |
| 141 | + if missing_columns: |
| 142 | + raise ValueError( |
| 143 | + f"DataFrame is missing required columns for technical indicator calculations: {', '.join(missing_columns)}" |
| 144 | + ) |
| 145 | + |
| 146 | + # Validate window size |
| 147 | + if not isinstance(window, int) or window <= 0: |
| 148 | + raise ValueError("'window' must be a positive integer.") |
| 149 | + |
| 150 | + try: |
| 151 | + # Add Moving Average |
| 152 | + df[f"MA_{window}"] = df["Close"].rolling(window=window).mean() |
| 153 | + # Calculate Volatility (rolling standard deviation of daily returns) |
| 154 | + df["Volatility"] = df["Daily Returns"].rolling(window=window).std() |
| 155 | + except Exception as e: |
| 156 | + raise RuntimeError(f"An error occurred while calculating technical indicators: {e}") |
| 157 | + |
| 158 | + return df |
| 159 | + |
| 160 | + |
| 161 | +def _add_lags(df: pd.DataFrame, lag_days: int = 5): |
| 162 | + """Enhances a DataFrame by adding lagged features for the 'Close' price column. |
| 163 | +
|
| 164 | + This function creates new columns in the DataFrame, each representing the 'Close' price |
| 165 | + shifted by a number of days specified by the 'lag_days' parameter. These lagged features |
| 166 | + are useful for time series analysis and forecasting models, as they allow the model to |
| 167 | + consider historical price movements. |
| 168 | +
|
| 169 | + :param df: DataFrame with stock price data. |
| 170 | + :param lag_days: Number of days to lag features by. |
| 171 | +
|
| 172 | + :return: A DataFrame identical to the input but with additional columns for each |
| 173 | + lagged feature. The names of these new columns follow the pattern 'Close_lag_X', where |
| 174 | + X is the number of days the 'Close' price is lagged by. |
| 175 | +
|
| 176 | + :raises: ValueError: If the input DataFrame does not contain a 'Close' column or if the 'lag_days' |
| 177 | + parameter is not a positive integer. |
| 178 | + """ |
| 179 | + # Validate 'Close' column presence |
| 180 | + if "Close" not in df.columns: |
| 181 | + raise ValueError("DataFrame must contain a 'Close' column for lag feature calculations.") |
| 182 | + |
| 183 | + # Validate lag_days parameter |
| 184 | + if not isinstance(lag_days, int) or lag_days <= 0: |
| 185 | + raise ValueError("'lag_days' must be a positive integer.") |
| 186 | + |
| 187 | + try: |
| 188 | + # Create Lag Features for 'Close' Price |
| 189 | + for i in range(1, lag_days + 1): |
| 190 | + df[f"Close_lag_{i}"] = df["Close"].shift(i) |
| 191 | + except Exception as e: |
| 192 | + # Catching a broad exception to handle unexpected errors during lag feature creation |
| 193 | + raise RuntimeError(f"An error occurred while adding lag features: {e}") |
| 194 | + |
| 195 | + return df |
0 commit comments