Epoch 1/10
Traceback (most recent call last):
  File "/home/elliot/Downloads/quant work/transformers/script6.py", line 852, in <module>
    main()
  File "/home/elliot/Downloads/quant work/transformers/script6.py", line 832, in main
    history = transformer_model.fit(
              ^^^^^^^^^^^^^^^^^^^^^^
  File "/home/elliot/Downloads/quant work/transformers/trans/lib/python3.11/site-packages/keras/src/utils/traceback_utils.py", line 122, in error_handler
    raise e.with_traceback(filtered_tb) from None
  File "/home/elliot/Downloads/quant work/transformers/trans/lib/python3.11/site-packages/keras/src/backend/tensorflow/nn.py", line 652, in sparse_categorical_crossentropy
    raise ValueError(
ValueError: Argument `output` must have rank (ndim) `target.ndim - 1`. Received: target.shape=(None, 3), output.shape=(None, 2)

from tensorflow.keras.layers import Input, LayerNormalization, MultiHeadAttention, Dropout, Dense, GlobalAveragePooling1D
from tensorflow.keras.models import Model
import tensorflow as tf

# Transformer Block
def transformer_encoder(inputs, head_size, num_heads, ff_dim, dropout=0.1):
    """
    Transformer Encoder Block
    """
    # Layer Normalization 1
    x = LayerNormalization(epsilon=1e-6)(inputs)
    
    # Multi-Head Attention
    x = MultiHeadAttention(key_dim=head_size, num_heads=num_heads, dropout=dropout)(x, x)
    
    # Dropout
    x = Dropout(dropout)(x)
    
    # Residual Connection
    res = x + inputs
    
    # Layer Normalization 2
    x = LayerNormalization(epsilon=1e-6)(res)
    
    # Feed-Forward Network
    x = Dense(ff_dim, activation="relu")(x)
    x = Dropout(dropout)(x)
    x = Dense(inputs.shape[-1])(x)
    
    # Second Residual Connection
    return x + res

# Updated Transformer Model Definition
def build_transformer_model(input_shape, target_steps, num_classes, head_size=256, num_heads=4, ff_dim=128, dropout=0.1):
    """
    Build a Transformer-based model for multi-class classification.
    
    Parameters:
    - input_shape: Tuple, shape of the input data (sequence_length, num_features)
    - target_steps: Int, number of future steps to predict (currently not used in this model)
    - num_classes: Int, number of classification categories
    - head_size: Int, dimensionality of each attention head
    - num_heads: Int, number of attention heads
    - ff_dim: Int, dimensionality of the feed-forward network
    - dropout: Float, dropout rate
    """
    inputs = Input(shape=input_shape)  # e.g., (201, 16)
    
    # Positional Encoding (Learnable)
    position_indices = tf.range(start=0, limit=input_shape[0], delta=1)
    positional_embedding = Dense(input_shape[-1])(tf.expand_dims(position_indices, axis=1))
    x = inputs + positional_embedding  # Broadcast and add positional information

    # Transformer Encoder Blocks
    for _ in range(2):  # Number of Transformer blocks
        x = transformer_encoder(x, head_size=head_size, num_heads=num_heads, ff_dim=ff_dim, dropout=dropout)
    
    # Global Average Pooling
    x = GlobalAveragePooling1D()(x)  # Shape: (batch_size, features)
    
    # Dropout
    x = Dropout(dropout)(x)
    
    # Fully Connected Layers
    x = Dense(128, activation="relu")(x)
    x = Dropout(dropout)(x)
    
    # Output Layer
    outputs = Dense(num_classes, activation="softmax")(x)
    
    # Define the Model
    model = Model(inputs=inputs, outputs=outputs)
    
    # Compile the Model
    model.compile(optimizer="adam",
                  loss="sparse_categorical_crossentropy",
                  metrics=["accuracy"])
    
    return model
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from ta.volatility import AverageTrueRange, BollingerBands
from ta.momentum import RSIIndicator
from ta.trend import MACD, ADXIndicator
from binance.client import Client
import websocket
import json
import time
import datetime
from sklearn.preprocessing import LabelEncoder, StandardScaler
from tensorflow.keras import layers, models
import tensorflow as tf
from collections import deque

# ----------------------------
# Configuration and Constants
# ----------------------------


# Initialize Binance Client
client = Client(BINANCE_API_KEY, BINANCE_API_SECRET)

# Parameters
SYMBOLS = ['AAVEUSDT']  # List of symbols to monitor
SEQ_LENGTH = 201         # Input sequence length for the model
TARGET_STEPS = 3        # Number of future steps to predict
FEATURES = [
    'RSI', 'MACD', 'MACD_signal', 'ATR', 'BB_width', 'ADX', 
    'previous_leg_change', 'previous_leg_length',
    'current_leg_change', 'current_leg_length',
    'Volatility', 'Market_Environment'
]
TARGETS = ['percent_change_classification']

# Data Storage
data_frames = {symbol: pd.DataFrame() for symbol in SYMBOLS}

# Queues to store real-time data
data_queues = {symbol: deque(maxlen=1000) for symbol in SYMBOLS}  # Adjust maxlen as needed

# Label Encoders
label_encoders = {symbol: LabelEncoder() for symbol in SYMBOLS}

# Scalers
scalers = {symbol: StandardScaler() for symbol in SYMBOLS}

# ----------------------------
# Utility Function Definitions
# ----------------------------

def calculate_technical_indicators(df):
    """
    Calculate technical indicators and add them to the DataFrame.
    """
    if len(df) < 14:
        # Not enough data to calculate indicators
        return df

    # Calculate RSI
    rsi = RSIIndicator(close=df['close'], window=14)
    df['RSI'] = rsi.rsi()

    # Calculate MACD
    macd = MACD(close=df['close'], window_slow=26, window_fast=12, window_sign=9)
    df['MACD'] = macd.macd()
    df['MACD_signal'] = macd.macd_signal()
    df['MACD_diff'] = macd.macd_diff()

    # Calculate ATR
    atr = AverageTrueRange(high=df['high'], low=df['low'], close=df['close'], window=14)
    df['ATR'] = atr.average_true_range()

    # Calculate Bollinger Bands Width
    bollinger = BollingerBands(close=df['close'], window=20, window_dev=2)
    df['BB_upper'] = bollinger.bollinger_hband()
    df['BB_lower'] = bollinger.bollinger_lband()
    df['BB_width'] = (df['BB_upper'] - df['BB_lower']) / df['close']

    # Calculate ADX
    adx = ADXIndicator(high=df['high'], low=df['low'], close=df['close'], window=14)
    df['ADX'] = adx.adx()

    # Calculate Realized Volatility (Rolling 20-window standard deviation of returns)
    df['daily_return'] = df['close'].pct_change()
    df['RV'] = df['daily_return'].rolling(window=20).std() * np.sqrt(252)  # Annualized

    return df

def classify_trend(df):
    """
    Classify market trend as Bullish, Neutral, or Bearish based on moving averages, RSI, MACD, and ADX.
    """
    # Calculate Moving Averages
    df['MA_short'] = df['close'].rolling(window=50).mean()
    df['MA_long'] = df['close'].rolling(window=200).mean()

    # Trend based on Moving Averages
    def ma_trend(row):
        if pd.isna(row['MA_short']) or pd.isna(row['MA_long']):
            return 'Neutral'
        if row['MA_short'] > row['MA_long']:
            return 'Bullish'
        elif row['MA_short'] < row['MA_long']:
            return 'Bearish'
        else:
            return 'Neutral'

    df['MA_Trend'] = df.apply(ma_trend, axis=1)

    # Trend based on RSI
    def rsi_trend(row):
        if pd.isna(row['RSI']):
            return 'Neutral'
        if row['RSI'] > 60:
            return 'Bullish'
        elif row['RSI'] < 40:
            return 'Bearish'
        else:
            return 'Neutral'

    df['RSI_Trend'] = df.apply(rsi_trend, axis=1)

    # Trend based on MACD
    def macd_trend(row):
        if pd.isna(row['MACD']) or pd.isna(row['MACD_signal']):
            return 'Neutral'
        if row['MACD'] > row['MACD_signal'] and row['MACD'] > 0:
            return 'Bullish'
        elif row['MACD'] < row['MACD_signal'] and row['MACD'] < 0:
            return 'Bearish'
        else:
            return 'Neutral'

    df['MACD_Trend'] = df.apply(macd_trend, axis=1)

    # Trend Strength based on ADX
    def adx_strength(row):
        if pd.isna(row['ADX']):
            return 'Weak'
        if row['ADX'] > 25:
            return 'Strong'
        else:
            return 'Weak'

    df['ADX_Strength'] = df.apply(adx_strength, axis=1)

    # Combine Trends
    def combined_trend(row):
        trends = [row['MA_Trend'], row['RSI_Trend'], row['MACD_Trend']]
        bullish = trends.count('Bullish')
        bearish = trends.count('Bearish')
        neutral = trends.count('Neutral')

        if bullish > bearish and bullish > neutral:
            base_trend = 'Bullish'
        elif bearish > bullish and bearish > neutral:
            base_trend = 'Bearish'
        else:
            base_trend = 'Neutral'

        if row['ADX_Strength'] == 'Strong':
            return base_trend
        else:
            return 'Neutral'  # Treat weak trends as neutral

    df['Market_Trend'] = df.apply(combined_trend, axis=1)

    return df

def classify_volatility(df):
    """
    Classify volatility as High, Medium, or Low based on ATR, Bollinger Bands Width, and Realized Volatility.
    """
    if len(df) < 20:
        df['Volatility'] = 'Medium'
        return df

    # ATR Volatility Classification
    atr_mavg = df['ATR'].rolling(window=14).mean().shift(1)  # Shift to avoid look-ahead bias
    df['ATR_Vol'] = df.apply(
        lambda row: 'High' if row['ATR'] > 1.2 * atr_mavg.get(row.name, np.nan)
                    else ('Medium' if 0.9 * atr_mavg.get(row.name, np.nan) <= row['ATR'] <= 1.1 * atr_mavg.get(row.name, np.nan)
                          else 'Low'),
        axis=1
    )

    # Bollinger Bands Width Classification
    bb_mavg = df['BB_width'].rolling(window=20).mean().shift(1)
    df['BB_Vol'] = df.apply(
        lambda row: 'High' if row['BB_width'] > 1.2 * bb_mavg.get(row.name, np.nan)
                    else ('Medium' if 0.9 * bb_mavg.get(row.name, np.nan) <= row['BB_width'] <= 1.1 * bb_mavg.get(row.name, np.nan)
                          else 'Low'),
        axis=1
    )

    # Realized Volatility Classification
    rv_80 = df['RV'].quantile(0.8)
    rv_20 = df['RV'].quantile(0.2)
    df['RV_Vol'] = df['RV'].apply(
        lambda x: 'High' if x > rv_80
                  else ('Medium' if rv_20 <= x <= rv_80
                        else 'Low')
    )

    # Voting Mechanism for Volatility
    def volatility_vote(row):
        votes = [row['ATR_Vol'], row['BB_Vol'], row['RV_Vol']]
        high = votes.count('High')
        medium = votes.count('Medium')
        low = votes.count('Low')
        if high > medium and high > low:
            return 'High'
        elif medium > high and medium > low:
            return 'Medium'
        else:
            return 'Low'

    df['Volatility'] = df.apply(volatility_vote, axis=1)

    return df

def define_market_environment(df):
    """
    Combine Volatility and Market Trend to define Market Environment.
    """
    df['Market_Environment'] = df.apply(lambda row: f"{row['Volatility']} Vol/{row['Market_Trend']}", axis=1)
    return df

def calculate_leg_data(df):
    """
    Track the magnitude and duration of each leg (price movement direction).
    """
    previous_leg_change = 0
    previous_leg_length = 0
    current_leg_change = 0
    current_leg_length = 0

    legs = []

    for index, row in df.iterrows():
        if index == 0:
            # Initialize
            legs.append({
                'previous_leg_change': previous_leg_change,
                'previous_leg_length': previous_leg_length,
                'current_leg_change': current_leg_change,
                'current_leg_length': current_leg_length
            })
            continue

        percent_delta = (row['close'] - df.at[index-1, 'close']) / df.at[index-1, 'close']

        if current_leg_length == 0:
            current_leg_change = percent_delta
            current_leg_length = 1
        else:
            if (current_leg_change > 0 and percent_delta > 0) or (current_leg_change < 0 and percent_delta < 0):
                # Continue current leg
                current_leg_change += percent_delta
                current_leg_length += 1
            else:
                # Direction changed
                previous_leg_change = current_leg_change
                previous_leg_length = current_leg_length
                current_leg_change = percent_delta
                current_leg_length = 1

        legs.append({
            'previous_leg_change': previous_leg_change,
            'previous_leg_length': previous_leg_length,
            'current_leg_change': current_leg_change,
            'current_leg_length': current_leg_length
        })

    legs_df = pd.DataFrame(legs)
    df = pd.concat([df, legs_df], axis=1)
    return df

def classify_percent_change(df):
    """
    Discretize continuous percent changes into 7 classification buckets.
    """
    # Calculate percent changes
    df['percent_change'] = df['close'].pct_change()

    # Remove NaNs
    df = df.dropna(subset=['percent_change']).reset_index(drop=True)

    # Determine percentiles
    percentiles = df['percent_change'].quantile([0.05, 0.20, 0.40, 0.60, 0.80, 0.95]).to_dict()

    # Define classification function
    def classify(x, p):
        if x < p[0.05]:
            return 'Down a Lot'
        elif x < p[0.20]:
            return 'Down Moderate'
        elif x < p[0.40]:
            return 'Down a Little'
        elif x < p[0.60]:
            return 'No Change'
        elif x < p[0.80]:
            return 'Up a Little'
        elif x < p[0.95]:
            return 'Up Moderate'
        else:
            return 'Up a Lot'

    # Apply classification
    df['percent_change_classification'] = df['percent_change'].apply(lambda x: classify(x, percentiles))

    return df

def prepare_sequences(df, seq_length=SEQ_LENGTH, target_steps=TARGET_STEPS):
    """
    Prepare sequences of data for the Transformer model.
    """
    sequences = []
    targets = []
    for i in range(len(df) - seq_length - target_steps):
        seq = df.iloc[i:i+seq_length][FEATURES].values
        target = df.iloc[i+seq_length:i+seq_length+target_steps][TARGETS].values.flatten()
        sequences.append(seq)
        targets.append(target)
    sequences = np.array(sequences)
    targets = np.array(targets)
    return sequences, targets

# ----------------------------
# Data Loading Function
# ----------------------------

def load_data(symbols, interval='1s', sequence_length=SEQ_LENGTH):
    """
    Connect to Binance's Combined WebSocket Stream and collect live kline data.
    """
    # Create the combined stream name
    streams = '/'.join([f"{symbol.lower()}@kline_{interval}" for symbol in symbols])
    ws_url = f"wss://stream.binance.com:9443/stream?streams={streams}"
    ws = websocket.create_connection(ws_url)
    print(f"Subscribed to streams: {streams}")

    try:
        while True:
            message = ws.recv()
            msg = json.loads(message)
            
            # Debug: Print incoming message (optional)
            # print(json.dumps(msg, indent=2))
            
            # Check if the message contains 'data' and it is a kline event
            if 'data' in msg and msg['data']['e'] == 'kline':
                symbol = msg['data']['s']
                k = msg['data']['k']
                is_final = k['x']
                open_time = datetime.datetime.fromtimestamp(k['t'] / 1000)
                o = float(k['o'])
                h = float(k['h'])
                l = float(k['l'])
                c = float(k['c'])
                v = float(k['v'])

                new_row = {
                    'open_time': open_time,
                    'open': o,
                    'high': h,
                    'low': l,
                    'close': c,
                    'volume': v
                }
                
                # Replace append with pd.concat
                new_df = pd.DataFrame([new_row])
                data_frames[symbol] = pd.concat([data_frames[symbol], new_df], ignore_index=True)
                
                print(f"Received data for {symbol}: {new_row}")
                print(f"Data length for {symbol}: {len(data_frames[symbol])}")

                # Check if enough data is collected for all symbols
                if all(len(df) >= sequence_length + TARGET_STEPS for df in data_frames.values()):
                    print("Collected sufficient data for all symbols.")
                    break

    except KeyboardInterrupt:
        print("Data collection stopped by user.")
    except Exception as e:
        print(f"An error occurred: {e}")
    finally:
        ws.close()

    return data_frames

# ----------------------------
# Pre-Loading Historical Data Function
# ----------------------------

def pre_load_data(symbols, interval, limit):
    """
    Fetches historical kline data for the given symbols and populates data_frames.

    :param symbols: List of symbols to fetch data for.
    :param interval: Kline interval (e.g., '1s', '5m').
    :param limit: Number of historical data points to fetch.
    :return: None
    """
    for symbol in symbols:
        try:
            historicals = client.get_klines(symbol=symbol, interval=interval, limit=limit)
            for kl in historicals:
                open_time = datetime.datetime.fromtimestamp(kl[0] / 1000)
                o = float(kl[1])
                h = float(kl[2])
                l = float(kl[3])
                c = float(kl[4])
                v = float(kl[5])

                new_row = {
                    'open_time': open_time,
                    'open': o,
                    'high': h,
                    'low': l,
                    'close': c,
                    'volume': v
                }
                
                # Replace append with pd.concat
                new_df = pd.DataFrame([new_row])
                data_frames[symbol] = pd.concat([data_frames[symbol], new_df], ignore_index=True)
            print(f"Pre-loaded {len(historicals)} data points for {symbol}.")
        except Exception as e:
            print(f"Error fetching historical data for {symbol}: {e}")

# ----------------------------
# Transformer Model Definition
# ----------------------------
def transformer_encoder(inputs, head_size, num_heads, ff_dim, dropout=0.1):
    """
    Transformer Encoder Block
    """
    # Layer Normalization 1
    x = LayerNormalization(epsilon=1e-6)(inputs)
    
    # Multi-Head Attention
    x = MultiHeadAttention(key_dim=head_size, num_heads=num_heads, dropout=dropout)(x, x)
    
    # Dropout
    x = Dropout(dropout)(x)
    
    # Residual Connection
    res = x + inputs
    
    # Layer Normalization 2
    x = LayerNormalization(epsilon=1e-6)(res)
    
    # Feed-Forward Network
    x = Dense(ff_dim, activation="relu")(x)
    x = Dropout(dropout)(x)
    x = Dense(inputs.shape[-1])(x)
    
    # Second Residual Connection
    return x + res

# Updated Transformer Model Definition
def build_transformer_model(input_shape, target_steps, num_classes, head_size=256, num_heads=4, ff_dim=128, dropout=0.1):
    """
    Build a Transformer-based model for multi-class classification.
    
    Parameters:
    - input_shape: Tuple, shape of the input data (sequence_length, num_features)
    - target_steps: Int, number of future steps to predict (currently not used in this model)
    - num_classes: Int, number of classification categories
    - head_size: Int, dimensionality of each attention head
    - num_heads: Int, number of attention heads
    - ff_dim: Int, dimensionality of the feed-forward network
    - dropout: Float, dropout rate
    """
    inputs = Input(shape=input_shape)  # e.g., (201, 16)
    
    # Positional Encoding (Learnable)
    position_indices = tf.range(start=0, limit=input_shape[0], delta=1)
    positional_embedding = Dense(input_shape[-1])(tf.expand_dims(position_indices, axis=1))
    x = inputs + positional_embedding  # Broadcast and add positional information

    # Transformer Encoder Blocks
    for _ in range(2):  # Number of Transformer blocks
        x = transformer_encoder(x, head_size=head_size, num_heads=num_heads, ff_dim=ff_dim, dropout=dropout)
    
    # Global Average Pooling
    x = GlobalAveragePooling1D()(x)  # Shape: (batch_size, features)
    
    # Dropout
    x = Dropout(dropout)(x)
    
    # Fully Connected Layers
    x = Dense(128, activation="relu")(x)
    x = Dropout(dropout)(x)
    
    # Output Layer
    outputs = Dense(num_classes, activation="softmax")(x)
    
    # Define the Model
    model = Model(inputs=inputs, outputs=outputs)
    
    # Compile the Model
    model.compile(optimizer="adam",
                  loss="sparse_categorical_crossentropy",
                  metrics=["accuracy"])
    
    return model
# ----------------------------
# Data Processing and Feature Engineering
# ----------------------------

def update_features(symbol):
    """
    Update technical indicators and classifications for a given symbol.
    """
    df = data_frames[symbol]
    if len(df) < 200:
        # Not enough data to calculate indicators and classify
        return

    # Calculate technical indicators
    df = calculate_technical_indicators(df)

    # Classify trend
    df = classify_trend(df)

    # Classify volatility
    df = classify_volatility(df)

    # Define market environment
    df = define_market_environment(df)

    # Calculate leg data
    df = calculate_leg_data(df)

    # Classify percent change
    df = classify_percent_change(df)

    # Update the data_frames
    data_frames[symbol] = df



# ----------------------------
# Visualization
# ----------------------------

def visualize_indicators(symbol):
    """
    Plot technical indicators and market environment classifications for a given symbol.
    """
    df = data_frames[symbol]
    if len(df) < 200:
        print(f"Not enough data to visualize for {symbol}")
        return

    # Select the last 1000 data points for visualization
    plot_df = df.tail(1000)

    plt.figure(figsize=(14, 10))

    # Price and Moving Averages
    plt.subplot(4,1,1)
    plt.plot(plot_df['open_time'], plot_df['close'], label='Close Price')
    if 'MA_short' in plot_df.columns and 'MA_long' in plot_df.columns:
        plt.plot(plot_df['open_time'], plot_df['MA_short'], label='50 MA')
        plt.plot(plot_df['open_time'], plot_df['MA_long'], label='200 MA')
    plt.title(f'{symbol} Price and Moving Averages')
    plt.legend()

    # RSI
    plt.subplot(4,1,2)
    plt.plot(plot_df['open_time'], plot_df['RSI'], label='RSI')
    plt.axhline(60, color='green', linestyle='--')
    plt.axhline(40, color='red', linestyle='--')
    plt.title('Relative Strength Index (RSI)')
    plt.legend()

    # MACD
    plt.subplot(4,1,3)
    plt.plot(plot_df['open_time'], plot_df['MACD'], label='MACD')
    plt.plot(plot_df['open_time'], plot_df['MACD_signal'], label='Signal Line')
    plt.bar(plot_df['open_time'], plot_df['MACD_diff'], label='MACD Diff', alpha=0.3)
    plt.title('MACD')
    plt.legend()

    # ATR and Bollinger Bands Width
    plt.subplot(4,1,4)
    plt.plot(plot_df['open_time'], plot_df['ATR'], label='ATR')
    plt.plot(plot_df['open_time'], plot_df['BB_width'], label='Bollinger Bands Width')
    plt.title('ATR and Bollinger Bands Width')
    plt.legend()

    plt.tight_layout()
    plt.savefig('plot3.png', dpi=900, bbox_inches='tight') 

def visualize_market_environment(symbol):
    """
    Plot market environment classifications for a given symbol.
    """
    df = data_frames[symbol]
    if len(df) < 200:
        print(f"Not enough data to visualize for {symbol}")
        return

    # Select the last 1000 data points for visualization
    plot_df = df.tail(1000)

    plt.figure(figsize=(14, 6))

    # Market Environment
    plt.subplot(2,1,1)
    plt.plot(plot_df['open_time'], plot_df['Market_Environment'], label='Market Environment')
    plt.title('Market Environment')
    plt.legend()

    # Volatility
    plt.subplot(2,1,2)
    plt.plot(plot_df['open_time'], plot_df['Volatility'], label='Volatility')
    plt.title('Volatility')
    plt.legend()

    plt.tight_layout()
    plt.savefig('plot4.png', dpi=900, bbox_inches='tight') 

# ----------------------------
# Data Processing and Feature Engineering
# ----------------------------

def update_features(symbol):
    """
    Update technical indicators and classifications for a given symbol.
    """
    df = data_frames[symbol]
    if len(df) < 200:
        # Not enough data to calculate indicators and classify
        return

    # Calculate technical indicators
    df = calculate_technical_indicators(df)

    # Classify trend
    df = classify_trend(df)

    # Classify volatility
    df = classify_volatility(df)

    # Define market environment
    df = define_market_environment(df)

    # Calculate leg data
    df = calculate_leg_data(df)

    # Classify percent change
    df = classify_percent_change(df)

    # Update the data_frames
    data_frames[symbol] = df

# ----------------------------
# Main Execution
# ----------------------------

def main():
    # Step 1: Pre-fetch Historical Data
    print("Pre-loading historical data...")
    pre_load_data(symbols=SYMBOLS, interval='1s', limit=200+10)  # Adjust limit as needed
    print("Historical data pre-loading complete.")

    # Step 2: Update Features for Each Symbol
    print("Updating features for each symbol...")
    for symbol in SYMBOLS:
        update_features(symbol)

    # Step 3: Load Live Data from WebSockets
    print("Starting WebSocket connection and collecting live data...")
    load_data(symbols=SYMBOLS, interval='1s', sequence_length=SEQ_LENGTH)

    # Step 4: Update Features After Live Data Collection
    print("Updating features after live data collection...")
    for symbol in SYMBOLS:
        update_features(symbol)

    # Step 5: Prepare Data for the Transformer Model
    print("Preparing data for the Transformer model...")
    model_input = {}
    model_target = {}
    for symbol in SYMBOLS:
        df = data_frames[symbol]
        if len(df) < SEQ_LENGTH + TARGET_STEPS:
            print(f"Not enough data for {symbol}, skipping.")
            continue

        # Select necessary features
        feature_df = df[FEATURES].copy()
        # Handle missing values if any
        feature_df = feature_df.fillna(method='ffill').fillna(method='bfill')

        # Encode categorical features
        for feature in ['Volatility', 'Market_Environment']:
            if feature in feature_df.columns:
                feature_df[feature] = label_encoders[symbol].fit_transform(feature_df[feature])

        # Encode target
        target = df['percent_change_classification'].values
        target = label_encoders[symbol].fit_transform(target)

        # Scaling numerical features
        numerical_features = [
            'RSI', 'MACD', 'MACD_signal', 'ATR', 'BB_width', 'ADX', 
            'previous_leg_change', 'previous_leg_length',
            'current_leg_change', 'current_leg_length'
        ]
        feature_df[numerical_features] = scalers[symbol].fit_transform(feature_df[numerical_features])

        # Prepare sequences and targets
        combined_df = pd.concat(
            [feature_df, pd.DataFrame({'percent_change_classification': target})], axis=1
        )
        sequences, targets = prepare_sequences(combined_df)

        model_input[symbol] = sequences
        model_target[symbol] = targets

    # Step 6: Build and Train the Transformer Model for Each Symbol
    for symbol in SYMBOLS:
        if symbol not in model_input:
            continue

        print(f"Training Transformer model for {symbol}...")

        X = model_input[symbol]
        y = model_target[symbol]

        num_classes = len(np.unique(y))
        input_shape = X.shape[1:]  # (sequence_length, num_features)

        transformer_model = build_transformer_model(input_shape, TARGET_STEPS, num_classes)

        # Split into training and validation sets
        split_idx = int(0.8 * len(X))
        X_train, X_val = X[:split_idx], X[split_idx:]
        y_train, y_val = y[:split_idx], y[split_idx:]

        # Train the model
        history = transformer_model.fit(
            X_train, y_train,
            validation_data=(X_val, y_val),
            epochs=10,
            batch_size=32
        )

        # Save the model
        transformer_model.save(f"{symbol}_transformer_model.h5")
        print(f"Model for {symbol} saved.")

    # Step 7: Visualization (Optional)
    print("Visualizing indicators...")
    for symbol in SYMBOLS:
        print(f"Visualizing data for {symbol}...")
        visualize_indicators(symbol)

    print("Script execution complete.")

if __name__ == "__main__":
    main()