Holding days

This commit is contained in:
GonzaloHD 2026-07-15 15:51:58 +01:00
parent a18da5cb31
commit ea34b99988
2 changed files with 88 additions and 47 deletions

View file

@ -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,6 +23,7 @@ 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)
@ -28,7 +33,7 @@ def fetch_historical_data(symbols: list, years_back: int = 5) -> pd.DataFrame:
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...")
@ -40,16 +45,16 @@ def fetch_historical_data(symbols: list, years_back: int = 5) -> pd.DataFrame:
# 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())

View file

@ -1,34 +1,46 @@
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()
for symbol, group in df.groupby("symbol"):
# 1. Calculate Stationary Log Returns (Instead of absolute price)
group['log_return'] = np.log(group['close'] / group['close'].shift(1))
group = group.sort_values("timestamp").copy()
# 2. Calculate Rolling Volatility (Standard deviation of returns)
group['volatility_5d'] = group['log_return'].rolling(window=5).std()
# Feature 1: today's log return
group["log_return"] = np.log(group["close"] / group["close"].shift(1))
# 3. Simple RSI implementation (Normalized between 0 and 1)
delta = group['close'].diff()
gain = (delta.where(delta > 0, 0)).rolling(window=14).mean()
# 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
group["rsi_scaled"] = rsi / 100.0
# 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)
# 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)
# Drop rows with NaN values resulting from rolling windows and shifts
group = group.dropna()
processed_stocks.append(group)
@ -41,18 +53,20 @@ def create_lstm_sequences(df: pd.DataFrame, lookback: int = 30):
y_list = []
# Feature columns to feed into the model
feature_cols = ['log_return', 'volatility_5d', 'rsi_scaled']
feature_cols = ["log_return", "volatility_5d", "rsi_scaled"]
for symbol, group in df.groupby('symbol'):
group = group.sort_values('timestamp')
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)
@ -60,6 +74,28 @@ def create_lstm_sequences(df: pd.DataFrame, lookback: int = 30):
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)