From ea34b9998838a983b6032c11677c84382b79c15a Mon Sep 17 00:00:00 2001 From: GonzaloHD Date: Wed, 15 Jul 2026 15:51:58 +0100 Subject: [PATCH] Holding days --- src/lstm_alpha_strategy/data/alpaca_client.py | 31 +++--- .../features/technical_indicators.py | 104 ++++++++++++------ 2 files changed, 88 insertions(+), 47 deletions(-) diff --git a/src/lstm_alpha_strategy/data/alpaca_client.py b/src/lstm_alpha_strategy/data/alpaca_client.py index cceba36..d1d24e4 100644 --- a/src/lstm_alpha_strategy/data/alpaca_client.py +++ b/src/lstm_alpha_strategy/data/alpaca_client.py @@ -2,7 +2,11 @@ import os from alpaca.data.historical import StockHistoricalDataClient from alpaca.data.requests import StockBarsRequest from alpaca.data.timeframe import TimeFrame -from datetime import datetime, timedelta, UTC # , timezone (to use instead as timezone.utc in older python) +from datetime import ( + datetime, + timedelta, + UTC, +) # , timezone (to use instead as timezone.utc in older python) import pandas as pd from dotenv import load_dotenv @@ -19,37 +23,38 @@ data_client = StockHistoricalDataClient(API_KEY, SECRET_KEY) # 2. Define your small, liquid asset universe ASSET_UNIVERSE = ["AAPL", "MSFT", "NVDA", "AMD", "META", "AMZN", "GOOGL", "TSLA"] + def fetch_historical_data(symbols: list, years_back: int = 5) -> pd.DataFrame: """Fetches historical daily bars from Alpaca and returns a clean multi-index DataFrame.""" - end_date = datetime.now(UTC) - timedelta(minutes = 15) + end_date = datetime.now(UTC) - timedelta(minutes=15) start_date = end_date - timedelta(days=years_back * 365) - + request_params = StockBarsRequest( symbol_or_symbols=symbols, timeframe=TimeFrame.Day, start=start_date, - end=end_date + end=end_date, ) - + print(f"Fetching {years_back} years of data for {len(symbols)} stocks...") bars = data_client.get_stock_bars(request_params) - + # Convert directly to a pandas DataFrame df = bars.df - + # Alpaca returns a multi-index: (symbol, timestamp) # Reset index to make date-manipulation easier later df = df.reset_index() - df['timestamp'] = pd.to_datetime(df['timestamp']).dt.date + df["timestamp"] = pd.to_datetime(df["timestamp"]).dt.date - return df -if __name__ == '__main__': - df = fetch_historical_data(symbols=ASSET_UNIVERSE) - print(df.head()) - print(df.tail()) +if __name__ == "__main__": + df = fetch_historical_data(symbols=ASSET_UNIVERSE) + print(df.head(15)) + + print(df.tail(15)) print(df.describe()) diff --git a/src/lstm_alpha_strategy/features/technical_indicators.py b/src/lstm_alpha_strategy/features/technical_indicators.py index 922d3f7..490ca07 100644 --- a/src/lstm_alpha_strategy/features/technical_indicators.py +++ b/src/lstm_alpha_strategy/features/technical_indicators.py @@ -1,37 +1,49 @@ +import os import numpy as np import pandas as pd +from dotenv import load_dotenv + +load_dotenv() + +MIN_RETURN = float(os.getenv("MIN_RETURN", 0.010)) +HOLDING_DAYS = int(os.getenv("HOLDING_DAYS", 5)) + def engineer_features(df: pd.DataFrame) -> pd.DataFrame: """Calculates stationary features and binary targets per stock.""" processed_stocks = [] - - # Group by symbol so indicators don't bleed across different stocks - for symbol, group in df.groupby('symbol'): - group = group.sort_values('timestamp').copy() - - # 1. Calculate Stationary Log Returns (Instead of absolute price) - group['log_return'] = np.log(group['close'] / group['close'].shift(1)) - - # 2. Calculate Rolling Volatility (Standard deviation of returns) - group['volatility_5d'] = group['log_return'].rolling(window=5).std() - - # 3. Simple RSI implementation (Normalized between 0 and 1) - delta = group['close'].diff() - gain = (delta.where(delta > 0, 0)).rolling(window=14).mean() + + for symbol, group in df.groupby("symbol"): + + group = group.sort_values("timestamp").copy() + + # Feature 1: today's log return + group["log_return"] = np.log(group["close"] / group["close"].shift(1)) + + # Feature 2: recent volatility from past returns + group["volatility_5d"] = group["log_return"].rolling(window=5).std() + + # Feature 3: RSI + delta = group["close"].diff() + gain = delta.where(delta > 0, 0).rolling(window=14).mean() loss = (-delta.where(delta < 0, 0)).rolling(window=14).mean() - rs = gain / (loss + 1e-9) # Avoid division by zero + + rs = gain / (loss + 1e-9) rsi = 100 - (100 / (1 + rs)) - group['rsi_scaled'] = rsi / 100.0 # Scale to 0-1 range for Neural Network - - # 4. Create Binary Target: Will the stock go UP tomorrow? - # Shift tomorrow's return backward to align with today's features - group['tomorrow_return'] = group['log_return'].shift(-1) - group['target'] = (group['tomorrow_return'] > 0).astype(int) - - # Drop rows with NaN values resulting from rolling windows and shifts + group["rsi_scaled"] = rsi / 100.0 + + # Target: buy tomorrow open, sell after HOLDING_DAYS at close + group["future_trade_return"] = np.log( + group["close"].shift(-HOLDING_DAYS) / group["open"].shift(-1) + ) + + log_min_return = np.log(MIN_RETURN + 1) + + group["target"] = (group["future_trade_return"] > log_min_return).astype(int) + group = group.dropna() processed_stocks.append(group) - + return pd.concat(processed_stocks, ignore_index=True) @@ -39,27 +51,51 @@ def create_lstm_sequences(df: pd.DataFrame, lookback: int = 30): """Reshapes the DataFrame into 3D Numpy Arrays for TensorFlow.""" X_list = [] y_list = [] - + # Feature columns to feed into the model - feature_cols = ['log_return', 'volatility_5d', 'rsi_scaled'] - - for symbol, group in df.groupby('symbol'): - group = group.sort_values('timestamp') + feature_cols = ["log_return", "volatility_5d", "rsi_scaled"] + + for symbol, group in df.groupby("symbol"): + group = group.sort_values("timestamp") feature_data = group[feature_cols].values - target_data = group['target'].values - + target_data = group["target"].values + # Create rolling window sequences for i in range(len(group) - lookback): # Extract sequence from i to i + lookback X_seq = feature_data[i : i + lookback] - # Target is the outcome immediately following the lookback window + # Target for the day immediately after the lookback window. + # The target indicates whether buying on that day and + # holding for HOLDING_DAYS would achieve MIN_RETURN. y_label = target_data[i + lookback] - + X_list.append(X_seq) y_list.append(y_label) - + return np.array(X_list), np.array(y_list) + +if __name__ == "__main__": + from lstm_alpha_strategy.data.alpaca_client import fetch_historical_data + + ASSET_UNIVERSE = ["AAPL", "MSFT", "NVDA", "AMD", "META", "AMZN", "GOOGL", "TSLA"] + + raw_df = fetch_historical_data(ASSET_UNIVERSE) + + feature_df = engineer_features(raw_df) + + print(feature_df.head(60)) + + row = feature_df.iloc[0] + + buy_open = feature_df.iloc[1]["open"] + sell_close = feature_df.iloc[5]["close"] + + manual = np.log(sell_close / buy_open) + + print(manual) + print(row["future_trade_return"]) + # --- EXECUTION PIPELINE --- # 1. Pull raw_df # feature_df = engineer_features(raw_df)