pushing this all to git before cleaing it. putting this thing on my fucking Dell Poweredge bc god knows im not waiting 2 hours to run another ML test on this 12 core machine
This commit is contained in:
@@ -0,0 +1,452 @@
|
||||
import os
|
||||
import sys
|
||||
import argparse # Added for argument parsing
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import matplotlib.pyplot as plt
|
||||
import seaborn as sns
|
||||
import logging
|
||||
from tabulate import tabulate
|
||||
|
||||
from sklearn.preprocessing import MinMaxScaler
|
||||
from sklearn.metrics import mean_squared_error, mean_absolute_error, r2_score
|
||||
from sklearn.model_selection import TimeSeriesSplit, GridSearchCV
|
||||
|
||||
import tensorflow as tf
|
||||
from tensorflow.keras.models import Sequential
|
||||
from tensorflow.keras.layers import LSTM, GRU, Dense, Dropout, Bidirectional
|
||||
from tensorflow.keras.optimizers import Adam, Nadam
|
||||
from tensorflow.keras.callbacks import EarlyStopping, ReduceLROnPlateau
|
||||
from tensorflow.keras.losses import Huber
|
||||
|
||||
import xgboost as xgb
|
||||
|
||||
import optuna
|
||||
from optuna.integration import KerasPruningCallback
|
||||
|
||||
# For Reinforcement Learning
|
||||
import gym
|
||||
from gym import spaces
|
||||
from stable_baselines3 import DQN
|
||||
from stable_baselines3.common.vec_env import DummyVecEnv
|
||||
|
||||
# To handle parallelization
|
||||
import multiprocessing
|
||||
|
||||
# Suppress TensorFlow warnings
|
||||
os.environ['TF_CPP_MIN_LOG_LEVEL'] = '2' # Suppress INFO and WARNING messages
|
||||
|
||||
# Configure logging
|
||||
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
||||
|
||||
# 1. Data Loading and Preprocessing
|
||||
def load_data(file_path):
|
||||
logging.info(f"Loading data from: {file_path}")
|
||||
try:
|
||||
# Parse 'time' column as dates
|
||||
data = pd.read_csv(file_path, parse_dates=['time'])
|
||||
except FileNotFoundError:
|
||||
logging.error(f"File not found: {file_path}")
|
||||
sys.exit(1)
|
||||
except pd.errors.ParserError as e:
|
||||
logging.error(f"Error parsing CSV file: {e}")
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
logging.error(f"Unexpected error: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
# Rename columns to match script expectations
|
||||
rename_mapping = {
|
||||
'time': 'Date',
|
||||
'open': 'Open',
|
||||
'high': 'High',
|
||||
'low': 'Low',
|
||||
'close': 'Close'
|
||||
}
|
||||
data.rename(columns=rename_mapping, inplace=True)
|
||||
|
||||
logging.info(f"Data columns after renaming: {data.columns.tolist()}")
|
||||
|
||||
# Sort and reset index
|
||||
data.sort_values('Date', inplace=True)
|
||||
data.reset_index(drop=True, inplace=True)
|
||||
logging.info("Data loaded and sorted successfully.")
|
||||
return data
|
||||
|
||||
def compute_rsi(series, window=14):
|
||||
delta = series.diff()
|
||||
gain = (delta.where(delta > 0, 0)).rolling(window=window).mean()
|
||||
loss = (-delta.where(delta < 0, 0)).rolling(window=window).mean()
|
||||
RS = gain / loss
|
||||
RSI = 100 - (100 / (1 + RS))
|
||||
return RSI
|
||||
|
||||
def compute_macd(series, span_short=12, span_long=26, span_signal=9):
|
||||
ema_short = series.ewm(span=span_short, adjust=False).mean()
|
||||
ema_long = series.ewm(span=span_long, adjust=False).mean()
|
||||
MACD = ema_short - ema_long
|
||||
signal = MACD.ewm(span=span_signal, adjust=False).mean()
|
||||
return MACD - signal
|
||||
|
||||
def compute_adx(df, window=14):
|
||||
# Placeholder for ADX calculation
|
||||
return df['Close'].rolling(window=window).std() # Simplistic placeholder
|
||||
|
||||
def compute_obv(df):
|
||||
# On-Balance Volume calculation
|
||||
OBV = (np.sign(df['Close'].diff()) * df['Volume']).fillna(0).cumsum()
|
||||
return OBV
|
||||
|
||||
def calculate_technical_indicators(df):
|
||||
logging.info("Calculating technical indicators...")
|
||||
df['SMA_5'] = df['Close'].rolling(window=5).mean()
|
||||
df['SMA_10'] = df['Close'].rolling(window=10).mean()
|
||||
df['EMA_5'] = df['Close'].ewm(span=5, adjust=False).mean()
|
||||
df['EMA_10'] = df['Close'].ewm(span=10, adjust=False).mean()
|
||||
df['STDDEV_5'] = df['Close'].rolling(window=5).std()
|
||||
df['RSI'] = compute_rsi(df['Close'], window=14)
|
||||
df['MACD'] = compute_macd(df['Close'])
|
||||
df['ADX'] = compute_adx(df)
|
||||
df['OBV'] = compute_obv(df)
|
||||
df.dropna(inplace=True) # Drop rows with NaN values after feature engineering
|
||||
logging.info("Technical indicators calculated successfully.")
|
||||
return df
|
||||
|
||||
# Argument Parsing
|
||||
def parse_arguments():
|
||||
parser = argparse.ArgumentParser(description='Train LSTM and DQN models for stock trading.')
|
||||
parser.add_argument('csv_path', type=str, help='Path to the CSV data file.')
|
||||
return parser.parse_args()
|
||||
|
||||
def main():
|
||||
# Parse command-line arguments
|
||||
args = parse_arguments()
|
||||
csv_path = args.csv_path
|
||||
|
||||
# Load and preprocess data
|
||||
data = load_data(csv_path)
|
||||
data = calculate_technical_indicators(data)
|
||||
|
||||
# Feature selection
|
||||
feature_columns = ['SMA_5', 'SMA_10', 'EMA_5', 'EMA_10', 'STDDEV_5', 'RSI', 'MACD', 'ADX', 'OBV', 'Volume', 'Open', 'High', 'Low']
|
||||
target_column = 'Close'
|
||||
data = data[['Date'] + feature_columns + [target_column]]
|
||||
data.dropna(inplace=True)
|
||||
|
||||
# Scaling
|
||||
scaler_features = MinMaxScaler()
|
||||
scaler_target = MinMaxScaler()
|
||||
|
||||
scaled_features = scaler_features.fit_transform(data[feature_columns])
|
||||
scaled_target = scaler_target.fit_transform(data[[target_column]]).flatten()
|
||||
|
||||
# Create sequences for LSTM
|
||||
def create_sequences(features, target, window_size=15):
|
||||
X, y = [], []
|
||||
for i in range(len(features) - window_size):
|
||||
X.append(features[i:i+window_size])
|
||||
y.append(target[i+window_size])
|
||||
return np.array(X), np.array(y)
|
||||
|
||||
window_size = 15
|
||||
X, y = create_sequences(scaled_features, scaled_target, window_size)
|
||||
|
||||
# Split data into training, validation, and testing sets
|
||||
train_size = int(len(X) * 0.7)
|
||||
val_size = int(len(X) * 0.15)
|
||||
test_size = len(X) - train_size - val_size
|
||||
|
||||
X_train, X_val, X_test = X[:train_size], X[train_size:train_size+val_size], X[train_size+val_size:]
|
||||
y_train, y_val, y_test = y[:train_size], y[train_size:train_size+val_size], y[train_size+val_size:]
|
||||
|
||||
logging.info(f"Scaled training features shape: {X_train.shape}")
|
||||
logging.info(f"Scaled validation features shape: {X_val.shape}")
|
||||
logging.info(f"Scaled testing features shape: {X_test.shape}")
|
||||
logging.info(f"Scaled training target shape: {y_train.shape}")
|
||||
logging.info(f"Scaled validation target shape: {y_val.shape}")
|
||||
logging.info(f"Scaled testing target shape: {y_test.shape}")
|
||||
|
||||
# 2. Device Configuration
|
||||
def configure_device():
|
||||
gpus = tf.config.list_physical_devices('GPU')
|
||||
if gpus:
|
||||
try:
|
||||
for gpu in gpus:
|
||||
tf.config.experimental.set_memory_growth(gpu, True)
|
||||
logging.info(f"{len(gpus)} GPU(s) detected and configured.")
|
||||
except RuntimeError as e:
|
||||
logging.error(e)
|
||||
else:
|
||||
logging.info("No GPU detected, using CPU.")
|
||||
|
||||
configure_device()
|
||||
|
||||
# 3. Model Building
|
||||
def build_advanced_lstm(input_shape, hyperparams):
|
||||
model = Sequential()
|
||||
for i in range(hyperparams['num_lstm_layers']):
|
||||
return_sequences = True if i < hyperparams['num_lstm_layers'] - 1 else False
|
||||
model.add(Bidirectional(LSTM(
|
||||
hyperparams['lstm_units'],
|
||||
return_sequences=return_sequences,
|
||||
kernel_regularizer=tf.keras.regularizers.l2(0.001)
|
||||
)))
|
||||
model.add(Dropout(hyperparams['dropout_rate']))
|
||||
model.add(Dense(1, activation='linear'))
|
||||
|
||||
if hyperparams['optimizer'] == 'Adam':
|
||||
optimizer = Adam(learning_rate=hyperparams['learning_rate'], decay=hyperparams['decay'])
|
||||
elif hyperparams['optimizer'] == 'Nadam':
|
||||
optimizer = Nadam(learning_rate=hyperparams['learning_rate'])
|
||||
else:
|
||||
optimizer = Adam(learning_rate=hyperparams['learning_rate'])
|
||||
|
||||
model.compile(optimizer=optimizer, loss=Huber(), metrics=['mae'])
|
||||
return model
|
||||
|
||||
def build_xgboost_model(X_train, y_train, hyperparams):
|
||||
model = xgb.XGBRegressor(
|
||||
objective='reg:squarederror',
|
||||
n_estimators=hyperparams['n_estimators'],
|
||||
max_depth=hyperparams['max_depth'],
|
||||
learning_rate=hyperparams['learning_rate'],
|
||||
subsample=hyperparams['subsample'],
|
||||
colsample_bytree=hyperparams['colsample_bytree'],
|
||||
random_state=42,
|
||||
n_jobs=-1
|
||||
)
|
||||
model.fit(X_train.reshape(X_train.shape[0], -1), y_train)
|
||||
return model
|
||||
|
||||
# 4. Hyperparameter Tuning with Optuna
|
||||
def objective(trial):
|
||||
# Hyperparameter suggestions
|
||||
num_lstm_layers = trial.suggest_int('num_lstm_layers', 1, 3)
|
||||
lstm_units = trial.suggest_categorical('lstm_units', [32, 64, 96, 128])
|
||||
dropout_rate = trial.suggest_float('dropout_rate', 0.1, 0.5)
|
||||
learning_rate = trial.suggest_loguniform('learning_rate', 1e-5, 1e-2)
|
||||
optimizer_name = trial.suggest_categorical('optimizer', ['Adam', 'Nadam'])
|
||||
decay = trial.suggest_float('decay', 0.0, 1e-4)
|
||||
|
||||
hyperparams = {
|
||||
'num_lstm_layers': num_lstm_layers,
|
||||
'lstm_units': lstm_units,
|
||||
'dropout_rate': dropout_rate,
|
||||
'learning_rate': learning_rate,
|
||||
'optimizer': optimizer_name,
|
||||
'decay': decay
|
||||
}
|
||||
|
||||
model = build_advanced_lstm((X_train.shape[1], X_train.shape[2]), hyperparams)
|
||||
|
||||
early_stop = EarlyStopping(monitor='val_loss', patience=10, restore_best_weights=True)
|
||||
lr_reduce = ReduceLROnPlateau(monitor='val_loss', factor=0.5, patience=5, min_lr=1e-6)
|
||||
|
||||
history = model.fit(
|
||||
X_train, y_train,
|
||||
epochs=100,
|
||||
batch_size=16,
|
||||
validation_data=(X_val, y_val),
|
||||
callbacks=[early_stop, lr_reduce, KerasPruningCallback(trial, 'val_loss')],
|
||||
verbose=0
|
||||
)
|
||||
|
||||
val_mae = min(history.history['val_mae'])
|
||||
return val_mae
|
||||
|
||||
# Optuna study
|
||||
logging.info("Starting hyperparameter optimization with Optuna...")
|
||||
study = optuna.create_study(direction='minimize')
|
||||
study.optimize(objective, n_trials=50)
|
||||
|
||||
best_params = study.best_params
|
||||
logging.info(f"Best Hyperparameters from Optuna: {best_params}")
|
||||
|
||||
# 5. Train the Best LSTM Model
|
||||
best_model = build_advanced_lstm((X_train.shape[1], X_train.shape[2]), best_params)
|
||||
|
||||
early_stop = EarlyStopping(monitor='val_loss', patience=20, restore_best_weights=True)
|
||||
lr_reduce = ReduceLROnPlateau(monitor='val_loss', factor=0.5, patience=5, min_lr=1e-6)
|
||||
|
||||
logging.info("Training the best LSTM model with optimized hyperparameters...")
|
||||
history = best_model.fit(
|
||||
X_train, y_train,
|
||||
epochs=300,
|
||||
batch_size=16,
|
||||
validation_data=(X_val, y_val),
|
||||
callbacks=[early_stop, lr_reduce],
|
||||
verbose=1
|
||||
)
|
||||
|
||||
# 6. Evaluate the Model
|
||||
def evaluate_model(model, X_test, y_test):
|
||||
logging.info("Evaluating model...")
|
||||
y_pred_scaled = model.predict(X_test).flatten()
|
||||
y_pred_scaled = np.clip(y_pred_scaled, 0, 1) # Ensure predictions are within [0,1]
|
||||
y_pred = scaler_target.inverse_transform(y_pred_scaled.reshape(-1, 1)).flatten()
|
||||
y_test_actual = scaler_target.inverse_transform(y_test.reshape(-1, 1)).flatten()
|
||||
|
||||
mse = mean_squared_error(y_test_actual, y_pred)
|
||||
rmse = np.sqrt(mse)
|
||||
mae = mean_absolute_error(y_test_actual, y_pred)
|
||||
r2 = r2_score(y_test_actual, y_pred)
|
||||
|
||||
# Directional Accuracy
|
||||
direction_actual = np.sign(np.diff(y_test_actual))
|
||||
direction_pred = np.sign(np.diff(y_pred))
|
||||
directional_accuracy = np.mean(direction_actual == direction_pred)
|
||||
|
||||
logging.info(f"Test MSE: {mse}")
|
||||
logging.info(f"Test RMSE: {rmse}")
|
||||
logging.info(f"Test MAE: {mae}")
|
||||
logging.info(f"Test R2 Score: {r2}")
|
||||
logging.info(f"Directional Accuracy: {directional_accuracy}")
|
||||
|
||||
# Plot Actual vs Predicted
|
||||
plt.figure(figsize=(14, 7))
|
||||
plt.plot(y_test_actual, label='Actual Price')
|
||||
plt.plot(y_pred, label='Predicted Price')
|
||||
plt.title('Actual vs Predicted Prices')
|
||||
plt.xlabel('Time Step')
|
||||
plt.ylabel('Price')
|
||||
plt.legend()
|
||||
plt.grid(True)
|
||||
plt.savefig('actual_vs_predicted.png') # Save the plot
|
||||
plt.close()
|
||||
logging.info("Actual vs Predicted plot saved as 'actual_vs_predicted.png'")
|
||||
|
||||
# Tabulate first 40 predictions
|
||||
table = [[i, round(actual, 2), round(pred, 2)] for i, (actual, pred) in enumerate(zip(y_test_actual[:40], y_pred[:40]))]
|
||||
headers = ["Index", "Actual Price", "Predicted Price"]
|
||||
print(tabulate(table, headers=headers, tablefmt="pretty"))
|
||||
|
||||
return mse, rmse, mae, r2, directional_accuracy
|
||||
|
||||
mse, rmse, mae, r2, directional_accuracy = evaluate_model(best_model, X_test, y_test)
|
||||
|
||||
# 7. Save the Model and Scalers
|
||||
best_model.save('optimized_lstm_model.h5')
|
||||
import joblib
|
||||
joblib.dump(scaler_features, 'scaler_features.save')
|
||||
joblib.dump(scaler_target, 'scaler_target.save')
|
||||
logging.info("Model and scalers saved as 'optimized_lstm_model.h5', 'scaler_features.save', and 'scaler_target.save'.")
|
||||
|
||||
# 8. Reinforcement Learning: Deep Q-Learning for Trading Actions
|
||||
class StockTradingEnv(gym.Env):
|
||||
"""
|
||||
A simple stock trading environment for OpenAI gym
|
||||
"""
|
||||
metadata = {'render.modes': ['human']}
|
||||
|
||||
def __init__(self, df, initial_balance=10000):
|
||||
super(StockTradingEnv, self).__init__()
|
||||
|
||||
self.df = df.reset_index()
|
||||
self.initial_balance = initial_balance
|
||||
self.balance = initial_balance
|
||||
self.net_worth = initial_balance
|
||||
self.max_steps = len(df)
|
||||
self.current_step = 0
|
||||
self.shares_held = 0
|
||||
self.cost_basis = 0
|
||||
|
||||
# Actions: 0 = Sell, 1 = Hold, 2 = Buy
|
||||
self.action_space = spaces.Discrete(3)
|
||||
|
||||
# Observations: [normalized features + balance + shares held + cost basis]
|
||||
self.observation_space = spaces.Box(low=0, high=1, shape=(len(feature_columns) + 3,), dtype=np.float32)
|
||||
|
||||
def reset(self):
|
||||
self.balance = self.initial_balance
|
||||
self.net_worth = self.initial_balance
|
||||
self.current_step = 0
|
||||
self.shares_held = 0
|
||||
self.cost_basis = 0
|
||||
return self._next_observation()
|
||||
|
||||
def _next_observation(self):
|
||||
obs = self.df.loc[self.current_step, feature_columns].values
|
||||
# Normalize features by their max to ensure [0,1] range
|
||||
obs = obs / np.max(obs)
|
||||
# Append balance, shares held, and cost basis
|
||||
additional = np.array([
|
||||
self.balance / self.initial_balance,
|
||||
self.shares_held / 100, # Assuming a maximum of 100 shares for normalization
|
||||
self.cost_basis / self.initial_balance
|
||||
])
|
||||
return np.concatenate([obs, additional])
|
||||
|
||||
def step(self, action):
|
||||
current_price = self.df.loc[self.current_step, 'Close']
|
||||
|
||||
if action == 2: # Buy
|
||||
total_possible = self.balance // current_price
|
||||
shares_bought = total_possible
|
||||
if shares_bought > 0:
|
||||
self.balance -= shares_bought * current_price
|
||||
self.shares_held += shares_bought
|
||||
self.cost_basis = (self.cost_basis * (self.shares_held - shares_bought) + shares_bought * current_price) / self.shares_held
|
||||
elif action == 0: # Sell
|
||||
if self.shares_held > 0:
|
||||
self.balance += self.shares_held * current_price
|
||||
self.shares_held = 0
|
||||
self.cost_basis = 0
|
||||
# Hold does nothing
|
||||
|
||||
self.net_worth = self.balance + self.shares_held * current_price
|
||||
self.current_step += 1
|
||||
|
||||
done = self.current_step >= self.max_steps - 1
|
||||
|
||||
# Reward: change in net worth
|
||||
reward = self.net_worth - self.initial_balance
|
||||
|
||||
obs = self._next_observation()
|
||||
|
||||
return obs, reward, done, {}
|
||||
|
||||
def render(self, mode='human', close=False):
|
||||
profit = self.net_worth - self.initial_balance
|
||||
print(f'Step: {self.current_step}')
|
||||
print(f'Balance: {self.balance}')
|
||||
print(f'Shares held: {self.shares_held} (Cost Basis: {self.cost_basis})')
|
||||
print(f'Net worth: {self.net_worth}')
|
||||
print(f'Profit: {profit}')
|
||||
|
||||
def train_dqn_agent(env):
|
||||
logging.info("Training DQN Agent...")
|
||||
try:
|
||||
model = DQN(
|
||||
'MlpPolicy',
|
||||
env,
|
||||
verbose=1,
|
||||
learning_rate=1e-3,
|
||||
buffer_size=10000,
|
||||
learning_starts=1000,
|
||||
batch_size=64,
|
||||
tau=1.0,
|
||||
gamma=0.99,
|
||||
train_freq=4,
|
||||
target_update_interval=1000,
|
||||
exploration_fraction=0.1,
|
||||
exploration_final_eps=0.02,
|
||||
tensorboard_log="./dqn_stock_tensorboard/"
|
||||
)
|
||||
model.learn(total_timesteps=100000)
|
||||
model.save("dqn_stock_trading")
|
||||
logging.info("DQN Agent trained and saved as 'dqn_stock_trading.zip'.")
|
||||
return model
|
||||
except Exception as e:
|
||||
logging.error(f"Error training DQN Agent: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
# Initialize trading environment
|
||||
trading_env = StockTradingEnv(data)
|
||||
trading_env = DummyVecEnv([lambda: trading_env])
|
||||
|
||||
# Train DQN agent
|
||||
dqn_model = train_dqn_agent(trading_env)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
Reference in New Issue
Block a user