import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.preprocessing import MinMaxScaler
import tensorflow as tf
from tensorflow.keras.models import Model
from tensorflow.keras.layers import Input, Conv1D, LSTM, Dense, Flatten, concatenate, BatchNormalization
from tensorflow.keras.optimizers import Adam
import yfinance as yf

# 1. Load the SPY ETF data
spy = yf.download('TQQQ', start='2012-01-01', end='2024-09-01')

# 2. Data Preprocessing
spy['Returns'] = spy['Adj Close'].pct_change().fillna(0)

# Select target and features
X = spy[['Open', 'High', 'Low', 'Close', 'Volume']].values
y = spy['Adj Close'].shift(-1).fillna(method='ffill').values

# Scale the data
scaler_X = MinMaxScaler()
X_scaled = scaler_X.fit_transform(X)

scaler_y = MinMaxScaler()
y_scaled = scaler_y.fit_transform(y.reshape(-1, 1))

# Prepare the input for sequence-based models (3D input for CNN, LSTM, TCN)
sequence_length = 45  # Using a window of 30 days for predictions

def create_sequences(data, target, sequence_length):
    sequences = []
    labels = []
    for i in range(len(data) - sequence_length):
        sequences.append(data[i:i + sequence_length])
        labels.append(target[i + sequence_length])
    return np.array(sequences), np.array(labels)

X_seq, y_seq = create_sequences(X_scaled, y_scaled, sequence_length)

# 3. CNN Model Definition
def create_cnn_model(input_shape):
    input_layer = Input(shape=input_shape)
    x = Conv1D(filters=64, kernel_size=3, padding="same", activation='relu')(input_layer)
    x = BatchNormalization()(x)
    x = Flatten()(x)
    output = Dense(64, activation='relu')(x)
    return Model(inputs=input_layer, outputs=output)

# 4. LSTM Model with Attention Definition
class Attention(tf.keras.layers.Layer):
    def __init__(self):
        super(Attention, self).__init__()

    def build(self, input_shape):
        self.W = self.add_weight(shape=(input_shape[-1], input_shape[-1]),
                                 initializer="glorot_uniform",
                                 trainable=True)
        self.b = self.add_weight(shape=(input_shape[-1],),
                                 initializer="zeros",
                                 trainable=True)
        self.u = self.add_weight(shape=(input_shape[-1],),
                                 initializer="glorot_uniform",
                                 trainable=True)
        super(Attention, self).build(input_shape)

    def call(self, inputs):
        score = tf.nn.tanh(tf.tensordot(inputs, self.W, axes=1) + self.b)
        score = tf.tensordot(score, self.u, axes=1)
        attention_weights = tf.nn.softmax(score, axis=1)
        context_vector = attention_weights[:, :, tf.newaxis] * inputs
        context_vector = tf.reduce_sum(context_vector, axis=1)
        return context_vector

def create_lstm_model_with_attention(input_shape):
    input_layer = Input(shape=input_shape)
    lstm_output = LSTM(64, return_sequences=True)(input_layer)
    attention_output = Attention()(lstm_output)
    output = Dense(64, activation='relu')(attention_output)
    return Model(inputs=input_layer, outputs=output)

# 5. TCN Model Definition
def create_tcn_model(input_shape):
    input_layer = Input(shape=input_shape)
    x = Conv1D(filters=64, kernel_size=3, padding="causal", activation='relu', dilation_rate=2)(input_layer)
    x = Conv1D(filters=64, kernel_size=3, padding="causal", activation='relu', dilation_rate=4)(x)
    x = Flatten()(x)
    output = Dense(64, activation='relu')(x)
    return Model(inputs=input_layer, outputs=output)

# Input shape for sequence models
input_shape = (sequence_length, X_scaled.shape[1])

# Create the individual models
cnn_model = create_cnn_model(input_shape)
lstm_model_with_attention = create_lstm_model_with_attention(input_shape)
tcn_model = create_tcn_model(input_shape)

# 6. Combine the models into an ensemble
combined_input = concatenate([cnn_model.output, lstm_model_with_attention.output, tcn_model.output])
x = Dense(64, activation="relu")(combined_input)
x = Dense(32, activation="relu")(x)
output = Dense(1)(x)

# Create final ensemble model
ensemble_model = Model(inputs=[cnn_model.input, lstm_model_with_attention.input, tcn_model.input], outputs=output)

# Compile the model
ensemble_model.compile(optimizer=Adam(learning_rate=0.001), loss='mean_squared_error')

# 7. Train the Model
ensemble_model.fit([X_seq, X_seq, X_seq], y_seq, epochs=15, batch_size=32, validation_split=0.3)

# 8. Make Predictions
predictions = ensemble_model.predict([X_seq, X_seq, X_seq])

# Inverse transform predictions to get actual values
predicted_prices = scaler_y.inverse_transform(predictions)

# 9. Backtesting
spy['Predicted'] = np.nan
spy['Predicted'].iloc[sequence_length:] = predicted_prices[:, 0]

# Create shifted predictions for comparison
spy['Predicted_shifted'] = spy['Predicted'].shift(1)

# Calculate actual returns
spy['Actual_Return'] = spy['Returns'].shift(-1)  # Shifted for alignment with predicted values

# Calculate the median of actual returns
median_return = spy['Actual_Return'].median()

# Calculate predicted returns
spy['Predicted_Return'] = (spy['Predicted'] - spy['Predicted_shifted']) / spy['Predicted_shifted']

# Buy signal: today's predicted return > median actual return
spy['Signal'] = np.where(
    (spy['Predicted_Return'] > median_return), 
    1, 
    0
)

# Calculate returns based on strategy
spy['Strategy_Returns'] = spy['Returns'] * spy['Signal'].shift(1).fillna(0)

# 10. Calculate Cumulative Returns
spy['Cumulative_Strategy_Returns'] = (1 + spy['Strategy_Returns']).cumprod() - 1
spy['Cumulative_Market_Returns'] = (1 + spy['Returns']).cumprod() - 1

# 11. Calculate Performance Metrics
def calculate_sharpe_ratio(returns, risk_free_rate=0.0, periods_per_year=252):
    excess_returns = returns - risk_free_rate
    annualized_return = np.mean(excess_returns) * periods_per_year
    annualized_volatility = np.std(excess_returns) * np.sqrt(periods_per_year)
    sharpe_ratio = annualized_return / annualized_volatility
    return sharpe_ratio

sharpe_ratio = calculate_sharpe_ratio(spy['Strategy_Returns'].dropna())

def calculate_max_drawdown(returns):
    cumulative = (1 + returns).cumprod()
    peak = cumulative.cummax()
    drawdown = (cumulative - peak) / peak
    max_drawdown = drawdown.min()
    return max_drawdown

max_drawdown = calculate_max_drawdown(spy['Strategy_Returns'].dropna())

# 12. Plot the Results
plt.figure(figsize=(14, 7))
plt.plot(spy['Cumulative_Strategy_Returns'], label='Strategy Cumulative Returns')
plt.plot(spy['Cumulative_Market_Returns'], label='Market Cumulative Returns (Buy and Hold)')
plt.legend()
plt.title(f"Backtest of SPY Ensemble Model with CNN, LSTM+Attention, and TCN\nSharpe Ratio: {sharpe_ratio:.2f} | Max Drawdown: {max_drawdown:.2%}")
plt.show()