Files
AlphaFlow-MT5-ML-DL-Trading…/notebooks/strategies/ml/backtesting/pairs_trading_clustering.ipynb
T

536 KiB

Select all symbols

Step 1: Load Data for All Available Symbols

In [1]:
import MetaTrader5 as mt5
import pandas as pd
import numpy as np
import ta
from sklearn.decomposition import PCA
from sklearn.cluster import KMeans
from scipy.spatial.distance import euclidean
from statsmodels.tsa.stattools import coint

# ✅ Initialize MT5 Connection
if not mt5.initialize():
    print("MT5 Initialization Failed")
    mt5.shutdown()
    quit()

# ✅ Retrieve All Symbols in MT5
symbols = [s.name for s in mt5.symbols_get()]
print(f"✅ Found {len(symbols)} symbols in MT5")

# ✅ Limit to Top 50 Symbols for Performance (adjust as needed)
symbols = symbols[:50]

# ✅ Function to Fetch Price Data from MT5
def get_mt5_data(symbol, n_bars=1000, timeframe=mt5.TIMEFRAME_D1):
    """Fetches historical data from MT5"""
    rates = mt5.copy_rates_from_pos(symbol, timeframe, 0, n_bars)
    if rates is None:
        print(f"⚠️ Could not retrieve data for {symbol}")
        return None
    df = pd.DataFrame(rates)
    df["time"] = pd.to_datetime(df["time"], unit="s")
    df.set_index("time", inplace=True)
    return df[["close"]]

# ✅ Load Data for All Symbols
data = {symbol: get_mt5_data(symbol) for symbol in symbols if get_mt5_data(symbol) is not None}

# ✅ Merge Data into One DataFrame
df = pd.concat(data.values(), axis=1, keys=data.keys()).dropna()

print(f"📊 Data Loaded for {len(df.columns)//2} Symbols")
✅ Found 2061 symbols in MT5
📊 Data Loaded for 25 Symbols

Step 2: Add Technical Features

In [2]:
# ✅ Add Features
def add_features(df):
    for sym in symbols:
        df[(sym, "volatility")] = ta.volatility.bollinger_hband(df[(sym, "close")], window=20) - \
                                  ta.volatility.bollinger_lband(df[(sym, "close")], window=20)
        df[(sym, "momentum")] = ta.momentum.ROCIndicator(df[(sym, "close")], window=10).roc()
        df[(sym, "mean_reversion")] = df[(sym, "close")].rolling(50).mean() - df[(sym, "close")]

    return df.dropna()

df = add_features(df)
print(f"✅ Feature Engineering Complete. Shape: {df.shape}")
✅ Feature Engineering Complete. Shape: (947, 200)
C:\Users\moham\AppData\Local\Temp\ipykernel_11396\3836770481.py:7: PerformanceWarning: DataFrame is highly fragmented.  This is usually the result of calling `frame.insert` many times, which has poor performance.  Consider joining all columns at once using pd.concat(axis=1) instead. To get a de-fragmented frame, use `newframe = frame.copy()`
  df[(sym, "mean_reversion")] = df[(sym, "close")].rolling(50).mean() - df[(sym, "close")]
C:\Users\moham\AppData\Local\Temp\ipykernel_11396\3836770481.py:4: PerformanceWarning: DataFrame is highly fragmented.  This is usually the result of calling `frame.insert` many times, which has poor performance.  Consider joining all columns at once using pd.concat(axis=1) instead. To get a de-fragmented frame, use `newframe = frame.copy()`
  df[(sym, "volatility")] = ta.volatility.bollinger_hband(df[(sym, "close")], window=20) - \
C:\Users\moham\AppData\Local\Temp\ipykernel_11396\3836770481.py:6: PerformanceWarning: DataFrame is highly fragmented.  This is usually the result of calling `frame.insert` many times, which has poor performance.  Consider joining all columns at once using pd.concat(axis=1) instead. To get a de-fragmented frame, use `newframe = frame.copy()`
  df[(sym, "momentum")] = ta.momentum.ROCIndicator(df[(sym, "close")], window=10).roc()
C:\Users\moham\AppData\Local\Temp\ipykernel_11396\3836770481.py:7: PerformanceWarning: DataFrame is highly fragmented.  This is usually the result of calling `frame.insert` many times, which has poor performance.  Consider joining all columns at once using pd.concat(axis=1) instead. To get a de-fragmented frame, use `newframe = frame.copy()`
  df[(sym, "mean_reversion")] = df[(sym, "close")].rolling(50).mean() - df[(sym, "close")]
C:\Users\moham\AppData\Local\Temp\ipykernel_11396\3836770481.py:4: PerformanceWarning: DataFrame is highly fragmented.  This is usually the result of calling `frame.insert` many times, which has poor performance.  Consider joining all columns at once using pd.concat(axis=1) instead. To get a de-fragmented frame, use `newframe = frame.copy()`
  df[(sym, "volatility")] = ta.volatility.bollinger_hband(df[(sym, "close")], window=20) - \
C:\Users\moham\AppData\Local\Temp\ipykernel_11396\3836770481.py:6: PerformanceWarning: DataFrame is highly fragmented.  This is usually the result of calling `frame.insert` many times, which has poor performance.  Consider joining all columns at once using pd.concat(axis=1) instead. To get a de-fragmented frame, use `newframe = frame.copy()`
  df[(sym, "momentum")] = ta.momentum.ROCIndicator(df[(sym, "close")], window=10).roc()
C:\Users\moham\AppData\Local\Temp\ipykernel_11396\3836770481.py:7: PerformanceWarning: DataFrame is highly fragmented.  This is usually the result of calling `frame.insert` many times, which has poor performance.  Consider joining all columns at once using pd.concat(axis=1) instead. To get a de-fragmented frame, use `newframe = frame.copy()`
  df[(sym, "mean_reversion")] = df[(sym, "close")].rolling(50).mean() - df[(sym, "close")]
C:\Users\moham\AppData\Local\Temp\ipykernel_11396\3836770481.py:4: PerformanceWarning: DataFrame is highly fragmented.  This is usually the result of calling `frame.insert` many times, which has poor performance.  Consider joining all columns at once using pd.concat(axis=1) instead. To get a de-fragmented frame, use `newframe = frame.copy()`
  df[(sym, "volatility")] = ta.volatility.bollinger_hband(df[(sym, "close")], window=20) - \
C:\Users\moham\AppData\Local\Temp\ipykernel_11396\3836770481.py:6: PerformanceWarning: DataFrame is highly fragmented.  This is usually the result of calling `frame.insert` many times, which has poor performance.  Consider joining all columns at once using pd.concat(axis=1) instead. To get a de-fragmented frame, use `newframe = frame.copy()`
  df[(sym, "momentum")] = ta.momentum.ROCIndicator(df[(sym, "close")], window=10).roc()
C:\Users\moham\AppData\Local\Temp\ipykernel_11396\3836770481.py:7: PerformanceWarning: DataFrame is highly fragmented.  This is usually the result of calling `frame.insert` many times, which has poor performance.  Consider joining all columns at once using pd.concat(axis=1) instead. To get a de-fragmented frame, use `newframe = frame.copy()`
  df[(sym, "mean_reversion")] = df[(sym, "close")].rolling(50).mean() - df[(sym, "close")]
C:\Users\moham\AppData\Local\Temp\ipykernel_11396\3836770481.py:4: PerformanceWarning: DataFrame is highly fragmented.  This is usually the result of calling `frame.insert` many times, which has poor performance.  Consider joining all columns at once using pd.concat(axis=1) instead. To get a de-fragmented frame, use `newframe = frame.copy()`
  df[(sym, "volatility")] = ta.volatility.bollinger_hband(df[(sym, "close")], window=20) - \
C:\Users\moham\AppData\Local\Temp\ipykernel_11396\3836770481.py:6: PerformanceWarning: DataFrame is highly fragmented.  This is usually the result of calling `frame.insert` many times, which has poor performance.  Consider joining all columns at once using pd.concat(axis=1) instead. To get a de-fragmented frame, use `newframe = frame.copy()`
  df[(sym, "momentum")] = ta.momentum.ROCIndicator(df[(sym, "close")], window=10).roc()
C:\Users\moham\AppData\Local\Temp\ipykernel_11396\3836770481.py:7: PerformanceWarning: DataFrame is highly fragmented.  This is usually the result of calling `frame.insert` many times, which has poor performance.  Consider joining all columns at once using pd.concat(axis=1) instead. To get a de-fragmented frame, use `newframe = frame.copy()`
  df[(sym, "mean_reversion")] = df[(sym, "close")].rolling(50).mean() - df[(sym, "close")]
C:\Users\moham\AppData\Local\Temp\ipykernel_11396\3836770481.py:4: PerformanceWarning: DataFrame is highly fragmented.  This is usually the result of calling `frame.insert` many times, which has poor performance.  Consider joining all columns at once using pd.concat(axis=1) instead. To get a de-fragmented frame, use `newframe = frame.copy()`
  df[(sym, "volatility")] = ta.volatility.bollinger_hband(df[(sym, "close")], window=20) - \
C:\Users\moham\AppData\Local\Temp\ipykernel_11396\3836770481.py:6: PerformanceWarning: DataFrame is highly fragmented.  This is usually the result of calling `frame.insert` many times, which has poor performance.  Consider joining all columns at once using pd.concat(axis=1) instead. To get a de-fragmented frame, use `newframe = frame.copy()`
  df[(sym, "momentum")] = ta.momentum.ROCIndicator(df[(sym, "close")], window=10).roc()
C:\Users\moham\AppData\Local\Temp\ipykernel_11396\3836770481.py:7: PerformanceWarning: DataFrame is highly fragmented.  This is usually the result of calling `frame.insert` many times, which has poor performance.  Consider joining all columns at once using pd.concat(axis=1) instead. To get a de-fragmented frame, use `newframe = frame.copy()`
  df[(sym, "mean_reversion")] = df[(sym, "close")].rolling(50).mean() - df[(sym, "close")]
C:\Users\moham\AppData\Local\Temp\ipykernel_11396\3836770481.py:4: PerformanceWarning: DataFrame is highly fragmented.  This is usually the result of calling `frame.insert` many times, which has poor performance.  Consider joining all columns at once using pd.concat(axis=1) instead. To get a de-fragmented frame, use `newframe = frame.copy()`
  df[(sym, "volatility")] = ta.volatility.bollinger_hband(df[(sym, "close")], window=20) - \
C:\Users\moham\AppData\Local\Temp\ipykernel_11396\3836770481.py:6: PerformanceWarning: DataFrame is highly fragmented.  This is usually the result of calling `frame.insert` many times, which has poor performance.  Consider joining all columns at once using pd.concat(axis=1) instead. To get a de-fragmented frame, use `newframe = frame.copy()`
  df[(sym, "momentum")] = ta.momentum.ROCIndicator(df[(sym, "close")], window=10).roc()
C:\Users\moham\AppData\Local\Temp\ipykernel_11396\3836770481.py:7: PerformanceWarning: DataFrame is highly fragmented.  This is usually the result of calling `frame.insert` many times, which has poor performance.  Consider joining all columns at once using pd.concat(axis=1) instead. To get a de-fragmented frame, use `newframe = frame.copy()`
  df[(sym, "mean_reversion")] = df[(sym, "close")].rolling(50).mean() - df[(sym, "close")]
C:\Users\moham\AppData\Local\Temp\ipykernel_11396\3836770481.py:4: PerformanceWarning: DataFrame is highly fragmented.  This is usually the result of calling `frame.insert` many times, which has poor performance.  Consider joining all columns at once using pd.concat(axis=1) instead. To get a de-fragmented frame, use `newframe = frame.copy()`
  df[(sym, "volatility")] = ta.volatility.bollinger_hband(df[(sym, "close")], window=20) - \
C:\Users\moham\AppData\Local\Temp\ipykernel_11396\3836770481.py:6: PerformanceWarning: DataFrame is highly fragmented.  This is usually the result of calling `frame.insert` many times, which has poor performance.  Consider joining all columns at once using pd.concat(axis=1) instead. To get a de-fragmented frame, use `newframe = frame.copy()`
  df[(sym, "momentum")] = ta.momentum.ROCIndicator(df[(sym, "close")], window=10).roc()
C:\Users\moham\AppData\Local\Temp\ipykernel_11396\3836770481.py:7: PerformanceWarning: DataFrame is highly fragmented.  This is usually the result of calling `frame.insert` many times, which has poor performance.  Consider joining all columns at once using pd.concat(axis=1) instead. To get a de-fragmented frame, use `newframe = frame.copy()`
  df[(sym, "mean_reversion")] = df[(sym, "close")].rolling(50).mean() - df[(sym, "close")]
C:\Users\moham\AppData\Local\Temp\ipykernel_11396\3836770481.py:4: PerformanceWarning: DataFrame is highly fragmented.  This is usually the result of calling `frame.insert` many times, which has poor performance.  Consider joining all columns at once using pd.concat(axis=1) instead. To get a de-fragmented frame, use `newframe = frame.copy()`
  df[(sym, "volatility")] = ta.volatility.bollinger_hband(df[(sym, "close")], window=20) - \
C:\Users\moham\AppData\Local\Temp\ipykernel_11396\3836770481.py:6: PerformanceWarning: DataFrame is highly fragmented.  This is usually the result of calling `frame.insert` many times, which has poor performance.  Consider joining all columns at once using pd.concat(axis=1) instead. To get a de-fragmented frame, use `newframe = frame.copy()`
  df[(sym, "momentum")] = ta.momentum.ROCIndicator(df[(sym, "close")], window=10).roc()
C:\Users\moham\AppData\Local\Temp\ipykernel_11396\3836770481.py:7: PerformanceWarning: DataFrame is highly fragmented.  This is usually the result of calling `frame.insert` many times, which has poor performance.  Consider joining all columns at once using pd.concat(axis=1) instead. To get a de-fragmented frame, use `newframe = frame.copy()`
  df[(sym, "mean_reversion")] = df[(sym, "close")].rolling(50).mean() - df[(sym, "close")]
C:\Users\moham\AppData\Local\Temp\ipykernel_11396\3836770481.py:4: PerformanceWarning: DataFrame is highly fragmented.  This is usually the result of calling `frame.insert` many times, which has poor performance.  Consider joining all columns at once using pd.concat(axis=1) instead. To get a de-fragmented frame, use `newframe = frame.copy()`
  df[(sym, "volatility")] = ta.volatility.bollinger_hband(df[(sym, "close")], window=20) - \
C:\Users\moham\AppData\Local\Temp\ipykernel_11396\3836770481.py:6: PerformanceWarning: DataFrame is highly fragmented.  This is usually the result of calling `frame.insert` many times, which has poor performance.  Consider joining all columns at once using pd.concat(axis=1) instead. To get a de-fragmented frame, use `newframe = frame.copy()`
  df[(sym, "momentum")] = ta.momentum.ROCIndicator(df[(sym, "close")], window=10).roc()
C:\Users\moham\AppData\Local\Temp\ipykernel_11396\3836770481.py:7: PerformanceWarning: DataFrame is highly fragmented.  This is usually the result of calling `frame.insert` many times, which has poor performance.  Consider joining all columns at once using pd.concat(axis=1) instead. To get a de-fragmented frame, use `newframe = frame.copy()`
  df[(sym, "mean_reversion")] = df[(sym, "close")].rolling(50).mean() - df[(sym, "close")]
C:\Users\moham\AppData\Local\Temp\ipykernel_11396\3836770481.py:4: PerformanceWarning: DataFrame is highly fragmented.  This is usually the result of calling `frame.insert` many times, which has poor performance.  Consider joining all columns at once using pd.concat(axis=1) instead. To get a de-fragmented frame, use `newframe = frame.copy()`
  df[(sym, "volatility")] = ta.volatility.bollinger_hband(df[(sym, "close")], window=20) - \
C:\Users\moham\AppData\Local\Temp\ipykernel_11396\3836770481.py:6: PerformanceWarning: DataFrame is highly fragmented.  This is usually the result of calling `frame.insert` many times, which has poor performance.  Consider joining all columns at once using pd.concat(axis=1) instead. To get a de-fragmented frame, use `newframe = frame.copy()`
  df[(sym, "momentum")] = ta.momentum.ROCIndicator(df[(sym, "close")], window=10).roc()
C:\Users\moham\AppData\Local\Temp\ipykernel_11396\3836770481.py:7: PerformanceWarning: DataFrame is highly fragmented.  This is usually the result of calling `frame.insert` many times, which has poor performance.  Consider joining all columns at once using pd.concat(axis=1) instead. To get a de-fragmented frame, use `newframe = frame.copy()`
  df[(sym, "mean_reversion")] = df[(sym, "close")].rolling(50).mean() - df[(sym, "close")]
C:\Users\moham\AppData\Local\Temp\ipykernel_11396\3836770481.py:4: PerformanceWarning: DataFrame is highly fragmented.  This is usually the result of calling `frame.insert` many times, which has poor performance.  Consider joining all columns at once using pd.concat(axis=1) instead. To get a de-fragmented frame, use `newframe = frame.copy()`
  df[(sym, "volatility")] = ta.volatility.bollinger_hband(df[(sym, "close")], window=20) - \
C:\Users\moham\AppData\Local\Temp\ipykernel_11396\3836770481.py:6: PerformanceWarning: DataFrame is highly fragmented.  This is usually the result of calling `frame.insert` many times, which has poor performance.  Consider joining all columns at once using pd.concat(axis=1) instead. To get a de-fragmented frame, use `newframe = frame.copy()`
  df[(sym, "momentum")] = ta.momentum.ROCIndicator(df[(sym, "close")], window=10).roc()
C:\Users\moham\AppData\Local\Temp\ipykernel_11396\3836770481.py:7: PerformanceWarning: DataFrame is highly fragmented.  This is usually the result of calling `frame.insert` many times, which has poor performance.  Consider joining all columns at once using pd.concat(axis=1) instead. To get a de-fragmented frame, use `newframe = frame.copy()`
  df[(sym, "mean_reversion")] = df[(sym, "close")].rolling(50).mean() - df[(sym, "close")]
C:\Users\moham\AppData\Local\Temp\ipykernel_11396\3836770481.py:4: PerformanceWarning: DataFrame is highly fragmented.  This is usually the result of calling `frame.insert` many times, which has poor performance.  Consider joining all columns at once using pd.concat(axis=1) instead. To get a de-fragmented frame, use `newframe = frame.copy()`
  df[(sym, "volatility")] = ta.volatility.bollinger_hband(df[(sym, "close")], window=20) - \
C:\Users\moham\AppData\Local\Temp\ipykernel_11396\3836770481.py:6: PerformanceWarning: DataFrame is highly fragmented.  This is usually the result of calling `frame.insert` many times, which has poor performance.  Consider joining all columns at once using pd.concat(axis=1) instead. To get a de-fragmented frame, use `newframe = frame.copy()`
  df[(sym, "momentum")] = ta.momentum.ROCIndicator(df[(sym, "close")], window=10).roc()
C:\Users\moham\AppData\Local\Temp\ipykernel_11396\3836770481.py:7: PerformanceWarning: DataFrame is highly fragmented.  This is usually the result of calling `frame.insert` many times, which has poor performance.  Consider joining all columns at once using pd.concat(axis=1) instead. To get a de-fragmented frame, use `newframe = frame.copy()`
  df[(sym, "mean_reversion")] = df[(sym, "close")].rolling(50).mean() - df[(sym, "close")]
C:\Users\moham\AppData\Local\Temp\ipykernel_11396\3836770481.py:4: PerformanceWarning: DataFrame is highly fragmented.  This is usually the result of calling `frame.insert` many times, which has poor performance.  Consider joining all columns at once using pd.concat(axis=1) instead. To get a de-fragmented frame, use `newframe = frame.copy()`
  df[(sym, "volatility")] = ta.volatility.bollinger_hband(df[(sym, "close")], window=20) - \
C:\Users\moham\AppData\Local\Temp\ipykernel_11396\3836770481.py:6: PerformanceWarning: DataFrame is highly fragmented.  This is usually the result of calling `frame.insert` many times, which has poor performance.  Consider joining all columns at once using pd.concat(axis=1) instead. To get a de-fragmented frame, use `newframe = frame.copy()`
  df[(sym, "momentum")] = ta.momentum.ROCIndicator(df[(sym, "close")], window=10).roc()
C:\Users\moham\AppData\Local\Temp\ipykernel_11396\3836770481.py:7: PerformanceWarning: DataFrame is highly fragmented.  This is usually the result of calling `frame.insert` many times, which has poor performance.  Consider joining all columns at once using pd.concat(axis=1) instead. To get a de-fragmented frame, use `newframe = frame.copy()`
  df[(sym, "mean_reversion")] = df[(sym, "close")].rolling(50).mean() - df[(sym, "close")]
C:\Users\moham\AppData\Local\Temp\ipykernel_11396\3836770481.py:4: PerformanceWarning: DataFrame is highly fragmented.  This is usually the result of calling `frame.insert` many times, which has poor performance.  Consider joining all columns at once using pd.concat(axis=1) instead. To get a de-fragmented frame, use `newframe = frame.copy()`
  df[(sym, "volatility")] = ta.volatility.bollinger_hband(df[(sym, "close")], window=20) - \
C:\Users\moham\AppData\Local\Temp\ipykernel_11396\3836770481.py:6: PerformanceWarning: DataFrame is highly fragmented.  This is usually the result of calling `frame.insert` many times, which has poor performance.  Consider joining all columns at once using pd.concat(axis=1) instead. To get a de-fragmented frame, use `newframe = frame.copy()`
  df[(sym, "momentum")] = ta.momentum.ROCIndicator(df[(sym, "close")], window=10).roc()
C:\Users\moham\AppData\Local\Temp\ipykernel_11396\3836770481.py:7: PerformanceWarning: DataFrame is highly fragmented.  This is usually the result of calling `frame.insert` many times, which has poor performance.  Consider joining all columns at once using pd.concat(axis=1) instead. To get a de-fragmented frame, use `newframe = frame.copy()`
  df[(sym, "mean_reversion")] = df[(sym, "close")].rolling(50).mean() - df[(sym, "close")]
C:\Users\moham\AppData\Local\Temp\ipykernel_11396\3836770481.py:4: PerformanceWarning: DataFrame is highly fragmented.  This is usually the result of calling `frame.insert` many times, which has poor performance.  Consider joining all columns at once using pd.concat(axis=1) instead. To get a de-fragmented frame, use `newframe = frame.copy()`
  df[(sym, "volatility")] = ta.volatility.bollinger_hband(df[(sym, "close")], window=20) - \
C:\Users\moham\AppData\Local\Temp\ipykernel_11396\3836770481.py:6: PerformanceWarning: DataFrame is highly fragmented.  This is usually the result of calling `frame.insert` many times, which has poor performance.  Consider joining all columns at once using pd.concat(axis=1) instead. To get a de-fragmented frame, use `newframe = frame.copy()`
  df[(sym, "momentum")] = ta.momentum.ROCIndicator(df[(sym, "close")], window=10).roc()
C:\Users\moham\AppData\Local\Temp\ipykernel_11396\3836770481.py:7: PerformanceWarning: DataFrame is highly fragmented.  This is usually the result of calling `frame.insert` many times, which has poor performance.  Consider joining all columns at once using pd.concat(axis=1) instead. To get a de-fragmented frame, use `newframe = frame.copy()`
  df[(sym, "mean_reversion")] = df[(sym, "close")].rolling(50).mean() - df[(sym, "close")]
C:\Users\moham\AppData\Local\Temp\ipykernel_11396\3836770481.py:4: PerformanceWarning: DataFrame is highly fragmented.  This is usually the result of calling `frame.insert` many times, which has poor performance.  Consider joining all columns at once using pd.concat(axis=1) instead. To get a de-fragmented frame, use `newframe = frame.copy()`
  df[(sym, "volatility")] = ta.volatility.bollinger_hband(df[(sym, "close")], window=20) - \
C:\Users\moham\AppData\Local\Temp\ipykernel_11396\3836770481.py:6: PerformanceWarning: DataFrame is highly fragmented.  This is usually the result of calling `frame.insert` many times, which has poor performance.  Consider joining all columns at once using pd.concat(axis=1) instead. To get a de-fragmented frame, use `newframe = frame.copy()`
  df[(sym, "momentum")] = ta.momentum.ROCIndicator(df[(sym, "close")], window=10).roc()
C:\Users\moham\AppData\Local\Temp\ipykernel_11396\3836770481.py:7: PerformanceWarning: DataFrame is highly fragmented.  This is usually the result of calling `frame.insert` many times, which has poor performance.  Consider joining all columns at once using pd.concat(axis=1) instead. To get a de-fragmented frame, use `newframe = frame.copy()`
  df[(sym, "mean_reversion")] = df[(sym, "close")].rolling(50).mean() - df[(sym, "close")]
C:\Users\moham\AppData\Local\Temp\ipykernel_11396\3836770481.py:4: PerformanceWarning: DataFrame is highly fragmented.  This is usually the result of calling `frame.insert` many times, which has poor performance.  Consider joining all columns at once using pd.concat(axis=1) instead. To get a de-fragmented frame, use `newframe = frame.copy()`
  df[(sym, "volatility")] = ta.volatility.bollinger_hband(df[(sym, "close")], window=20) - \
C:\Users\moham\AppData\Local\Temp\ipykernel_11396\3836770481.py:6: PerformanceWarning: DataFrame is highly fragmented.  This is usually the result of calling `frame.insert` many times, which has poor performance.  Consider joining all columns at once using pd.concat(axis=1) instead. To get a de-fragmented frame, use `newframe = frame.copy()`
  df[(sym, "momentum")] = ta.momentum.ROCIndicator(df[(sym, "close")], window=10).roc()
C:\Users\moham\AppData\Local\Temp\ipykernel_11396\3836770481.py:7: PerformanceWarning: DataFrame is highly fragmented.  This is usually the result of calling `frame.insert` many times, which has poor performance.  Consider joining all columns at once using pd.concat(axis=1) instead. To get a de-fragmented frame, use `newframe = frame.copy()`
  df[(sym, "mean_reversion")] = df[(sym, "close")].rolling(50).mean() - df[(sym, "close")]
C:\Users\moham\AppData\Local\Temp\ipykernel_11396\3836770481.py:4: PerformanceWarning: DataFrame is highly fragmented.  This is usually the result of calling `frame.insert` many times, which has poor performance.  Consider joining all columns at once using pd.concat(axis=1) instead. To get a de-fragmented frame, use `newframe = frame.copy()`
  df[(sym, "volatility")] = ta.volatility.bollinger_hband(df[(sym, "close")], window=20) - \
C:\Users\moham\AppData\Local\Temp\ipykernel_11396\3836770481.py:6: PerformanceWarning: DataFrame is highly fragmented.  This is usually the result of calling `frame.insert` many times, which has poor performance.  Consider joining all columns at once using pd.concat(axis=1) instead. To get a de-fragmented frame, use `newframe = frame.copy()`
  df[(sym, "momentum")] = ta.momentum.ROCIndicator(df[(sym, "close")], window=10).roc()
C:\Users\moham\AppData\Local\Temp\ipykernel_11396\3836770481.py:7: PerformanceWarning: DataFrame is highly fragmented.  This is usually the result of calling `frame.insert` many times, which has poor performance.  Consider joining all columns at once using pd.concat(axis=1) instead. To get a de-fragmented frame, use `newframe = frame.copy()`
  df[(sym, "mean_reversion")] = df[(sym, "close")].rolling(50).mean() - df[(sym, "close")]
C:\Users\moham\AppData\Local\Temp\ipykernel_11396\3836770481.py:4: PerformanceWarning: DataFrame is highly fragmented.  This is usually the result of calling `frame.insert` many times, which has poor performance.  Consider joining all columns at once using pd.concat(axis=1) instead. To get a de-fragmented frame, use `newframe = frame.copy()`
  df[(sym, "volatility")] = ta.volatility.bollinger_hband(df[(sym, "close")], window=20) - \
C:\Users\moham\AppData\Local\Temp\ipykernel_11396\3836770481.py:6: PerformanceWarning: DataFrame is highly fragmented.  This is usually the result of calling `frame.insert` many times, which has poor performance.  Consider joining all columns at once using pd.concat(axis=1) instead. To get a de-fragmented frame, use `newframe = frame.copy()`
  df[(sym, "momentum")] = ta.momentum.ROCIndicator(df[(sym, "close")], window=10).roc()
C:\Users\moham\AppData\Local\Temp\ipykernel_11396\3836770481.py:7: PerformanceWarning: DataFrame is highly fragmented.  This is usually the result of calling `frame.insert` many times, which has poor performance.  Consider joining all columns at once using pd.concat(axis=1) instead. To get a de-fragmented frame, use `newframe = frame.copy()`
  df[(sym, "mean_reversion")] = df[(sym, "close")].rolling(50).mean() - df[(sym, "close")]
C:\Users\moham\AppData\Local\Temp\ipykernel_11396\3836770481.py:4: PerformanceWarning: DataFrame is highly fragmented.  This is usually the result of calling `frame.insert` many times, which has poor performance.  Consider joining all columns at once using pd.concat(axis=1) instead. To get a de-fragmented frame, use `newframe = frame.copy()`
  df[(sym, "volatility")] = ta.volatility.bollinger_hband(df[(sym, "close")], window=20) - \
C:\Users\moham\AppData\Local\Temp\ipykernel_11396\3836770481.py:6: PerformanceWarning: DataFrame is highly fragmented.  This is usually the result of calling `frame.insert` many times, which has poor performance.  Consider joining all columns at once using pd.concat(axis=1) instead. To get a de-fragmented frame, use `newframe = frame.copy()`
  df[(sym, "momentum")] = ta.momentum.ROCIndicator(df[(sym, "close")], window=10).roc()
C:\Users\moham\AppData\Local\Temp\ipykernel_11396\3836770481.py:7: PerformanceWarning: DataFrame is highly fragmented.  This is usually the result of calling `frame.insert` many times, which has poor performance.  Consider joining all columns at once using pd.concat(axis=1) instead. To get a de-fragmented frame, use `newframe = frame.copy()`
  df[(sym, "mean_reversion")] = df[(sym, "close")].rolling(50).mean() - df[(sym, "close")]
C:\Users\moham\AppData\Local\Temp\ipykernel_11396\3836770481.py:4: PerformanceWarning: DataFrame is highly fragmented.  This is usually the result of calling `frame.insert` many times, which has poor performance.  Consider joining all columns at once using pd.concat(axis=1) instead. To get a de-fragmented frame, use `newframe = frame.copy()`
  df[(sym, "volatility")] = ta.volatility.bollinger_hband(df[(sym, "close")], window=20) - \
C:\Users\moham\AppData\Local\Temp\ipykernel_11396\3836770481.py:6: PerformanceWarning: DataFrame is highly fragmented.  This is usually the result of calling `frame.insert` many times, which has poor performance.  Consider joining all columns at once using pd.concat(axis=1) instead. To get a de-fragmented frame, use `newframe = frame.copy()`
  df[(sym, "momentum")] = ta.momentum.ROCIndicator(df[(sym, "close")], window=10).roc()
C:\Users\moham\AppData\Local\Temp\ipykernel_11396\3836770481.py:7: PerformanceWarning: DataFrame is highly fragmented.  This is usually the result of calling `frame.insert` many times, which has poor performance.  Consider joining all columns at once using pd.concat(axis=1) instead. To get a de-fragmented frame, use `newframe = frame.copy()`
  df[(sym, "mean_reversion")] = df[(sym, "close")].rolling(50).mean() - df[(sym, "close")]
C:\Users\moham\AppData\Local\Temp\ipykernel_11396\3836770481.py:4: PerformanceWarning: DataFrame is highly fragmented.  This is usually the result of calling `frame.insert` many times, which has poor performance.  Consider joining all columns at once using pd.concat(axis=1) instead. To get a de-fragmented frame, use `newframe = frame.copy()`
  df[(sym, "volatility")] = ta.volatility.bollinger_hband(df[(sym, "close")], window=20) - \
C:\Users\moham\AppData\Local\Temp\ipykernel_11396\3836770481.py:6: PerformanceWarning: DataFrame is highly fragmented.  This is usually the result of calling `frame.insert` many times, which has poor performance.  Consider joining all columns at once using pd.concat(axis=1) instead. To get a de-fragmented frame, use `newframe = frame.copy()`
  df[(sym, "momentum")] = ta.momentum.ROCIndicator(df[(sym, "close")], window=10).roc()
C:\Users\moham\AppData\Local\Temp\ipykernel_11396\3836770481.py:7: PerformanceWarning: DataFrame is highly fragmented.  This is usually the result of calling `frame.insert` many times, which has poor performance.  Consider joining all columns at once using pd.concat(axis=1) instead. To get a de-fragmented frame, use `newframe = frame.copy()`
  df[(sym, "mean_reversion")] = df[(sym, "close")].rolling(50).mean() - df[(sym, "close")]
C:\Users\moham\AppData\Local\Temp\ipykernel_11396\3836770481.py:4: PerformanceWarning: DataFrame is highly fragmented.  This is usually the result of calling `frame.insert` many times, which has poor performance.  Consider joining all columns at once using pd.concat(axis=1) instead. To get a de-fragmented frame, use `newframe = frame.copy()`
  df[(sym, "volatility")] = ta.volatility.bollinger_hband(df[(sym, "close")], window=20) - \
C:\Users\moham\AppData\Local\Temp\ipykernel_11396\3836770481.py:6: PerformanceWarning: DataFrame is highly fragmented.  This is usually the result of calling `frame.insert` many times, which has poor performance.  Consider joining all columns at once using pd.concat(axis=1) instead. To get a de-fragmented frame, use `newframe = frame.copy()`
  df[(sym, "momentum")] = ta.momentum.ROCIndicator(df[(sym, "close")], window=10).roc()
C:\Users\moham\AppData\Local\Temp\ipykernel_11396\3836770481.py:7: PerformanceWarning: DataFrame is highly fragmented.  This is usually the result of calling `frame.insert` many times, which has poor performance.  Consider joining all columns at once using pd.concat(axis=1) instead. To get a de-fragmented frame, use `newframe = frame.copy()`
  df[(sym, "mean_reversion")] = df[(sym, "close")].rolling(50).mean() - df[(sym, "close")]
C:\Users\moham\AppData\Local\Temp\ipykernel_11396\3836770481.py:4: PerformanceWarning: DataFrame is highly fragmented.  This is usually the result of calling `frame.insert` many times, which has poor performance.  Consider joining all columns at once using pd.concat(axis=1) instead. To get a de-fragmented frame, use `newframe = frame.copy()`
  df[(sym, "volatility")] = ta.volatility.bollinger_hband(df[(sym, "close")], window=20) - \
C:\Users\moham\AppData\Local\Temp\ipykernel_11396\3836770481.py:6: PerformanceWarning: DataFrame is highly fragmented.  This is usually the result of calling `frame.insert` many times, which has poor performance.  Consider joining all columns at once using pd.concat(axis=1) instead. To get a de-fragmented frame, use `newframe = frame.copy()`
  df[(sym, "momentum")] = ta.momentum.ROCIndicator(df[(sym, "close")], window=10).roc()
C:\Users\moham\AppData\Local\Temp\ipykernel_11396\3836770481.py:7: PerformanceWarning: DataFrame is highly fragmented.  This is usually the result of calling `frame.insert` many times, which has poor performance.  Consider joining all columns at once using pd.concat(axis=1) instead. To get a de-fragmented frame, use `newframe = frame.copy()`
  df[(sym, "mean_reversion")] = df[(sym, "close")].rolling(50).mean() - df[(sym, "close")]
C:\Users\moham\AppData\Local\Temp\ipykernel_11396\3836770481.py:4: PerformanceWarning: DataFrame is highly fragmented.  This is usually the result of calling `frame.insert` many times, which has poor performance.  Consider joining all columns at once using pd.concat(axis=1) instead. To get a de-fragmented frame, use `newframe = frame.copy()`
  df[(sym, "volatility")] = ta.volatility.bollinger_hband(df[(sym, "close")], window=20) - \
C:\Users\moham\AppData\Local\Temp\ipykernel_11396\3836770481.py:6: PerformanceWarning: DataFrame is highly fragmented.  This is usually the result of calling `frame.insert` many times, which has poor performance.  Consider joining all columns at once using pd.concat(axis=1) instead. To get a de-fragmented frame, use `newframe = frame.copy()`
  df[(sym, "momentum")] = ta.momentum.ROCIndicator(df[(sym, "close")], window=10).roc()
C:\Users\moham\AppData\Local\Temp\ipykernel_11396\3836770481.py:7: PerformanceWarning: DataFrame is highly fragmented.  This is usually the result of calling `frame.insert` many times, which has poor performance.  Consider joining all columns at once using pd.concat(axis=1) instead. To get a de-fragmented frame, use `newframe = frame.copy()`
  df[(sym, "mean_reversion")] = df[(sym, "close")].rolling(50).mean() - df[(sym, "close")]
C:\Users\moham\AppData\Local\Temp\ipykernel_11396\3836770481.py:4: PerformanceWarning: DataFrame is highly fragmented.  This is usually the result of calling `frame.insert` many times, which has poor performance.  Consider joining all columns at once using pd.concat(axis=1) instead. To get a de-fragmented frame, use `newframe = frame.copy()`
  df[(sym, "volatility")] = ta.volatility.bollinger_hband(df[(sym, "close")], window=20) - \
C:\Users\moham\AppData\Local\Temp\ipykernel_11396\3836770481.py:6: PerformanceWarning: DataFrame is highly fragmented.  This is usually the result of calling `frame.insert` many times, which has poor performance.  Consider joining all columns at once using pd.concat(axis=1) instead. To get a de-fragmented frame, use `newframe = frame.copy()`
  df[(sym, "momentum")] = ta.momentum.ROCIndicator(df[(sym, "close")], window=10).roc()
C:\Users\moham\AppData\Local\Temp\ipykernel_11396\3836770481.py:7: PerformanceWarning: DataFrame is highly fragmented.  This is usually the result of calling `frame.insert` many times, which has poor performance.  Consider joining all columns at once using pd.concat(axis=1) instead. To get a de-fragmented frame, use `newframe = frame.copy()`
  df[(sym, "mean_reversion")] = df[(sym, "close")].rolling(50).mean() - df[(sym, "close")]
C:\Users\moham\AppData\Local\Temp\ipykernel_11396\3836770481.py:4: PerformanceWarning: DataFrame is highly fragmented.  This is usually the result of calling `frame.insert` many times, which has poor performance.  Consider joining all columns at once using pd.concat(axis=1) instead. To get a de-fragmented frame, use `newframe = frame.copy()`
  df[(sym, "volatility")] = ta.volatility.bollinger_hband(df[(sym, "close")], window=20) - \
C:\Users\moham\AppData\Local\Temp\ipykernel_11396\3836770481.py:6: PerformanceWarning: DataFrame is highly fragmented.  This is usually the result of calling `frame.insert` many times, which has poor performance.  Consider joining all columns at once using pd.concat(axis=1) instead. To get a de-fragmented frame, use `newframe = frame.copy()`
  df[(sym, "momentum")] = ta.momentum.ROCIndicator(df[(sym, "close")], window=10).roc()
C:\Users\moham\AppData\Local\Temp\ipykernel_11396\3836770481.py:7: PerformanceWarning: DataFrame is highly fragmented.  This is usually the result of calling `frame.insert` many times, which has poor performance.  Consider joining all columns at once using pd.concat(axis=1) instead. To get a de-fragmented frame, use `newframe = frame.copy()`
  df[(sym, "mean_reversion")] = df[(sym, "close")].rolling(50).mean() - df[(sym, "close")]
C:\Users\moham\AppData\Local\Temp\ipykernel_11396\3836770481.py:4: PerformanceWarning: DataFrame is highly fragmented.  This is usually the result of calling `frame.insert` many times, which has poor performance.  Consider joining all columns at once using pd.concat(axis=1) instead. To get a de-fragmented frame, use `newframe = frame.copy()`
  df[(sym, "volatility")] = ta.volatility.bollinger_hband(df[(sym, "close")], window=20) - \
C:\Users\moham\AppData\Local\Temp\ipykernel_11396\3836770481.py:6: PerformanceWarning: DataFrame is highly fragmented.  This is usually the result of calling `frame.insert` many times, which has poor performance.  Consider joining all columns at once using pd.concat(axis=1) instead. To get a de-fragmented frame, use `newframe = frame.copy()`
  df[(sym, "momentum")] = ta.momentum.ROCIndicator(df[(sym, "close")], window=10).roc()
C:\Users\moham\AppData\Local\Temp\ipykernel_11396\3836770481.py:7: PerformanceWarning: DataFrame is highly fragmented.  This is usually the result of calling `frame.insert` many times, which has poor performance.  Consider joining all columns at once using pd.concat(axis=1) instead. To get a de-fragmented frame, use `newframe = frame.copy()`
  df[(sym, "mean_reversion")] = df[(sym, "close")].rolling(50).mean() - df[(sym, "close")]
C:\Users\moham\AppData\Local\Temp\ipykernel_11396\3836770481.py:4: PerformanceWarning: DataFrame is highly fragmented.  This is usually the result of calling `frame.insert` many times, which has poor performance.  Consider joining all columns at once using pd.concat(axis=1) instead. To get a de-fragmented frame, use `newframe = frame.copy()`
  df[(sym, "volatility")] = ta.volatility.bollinger_hband(df[(sym, "close")], window=20) - \
C:\Users\moham\AppData\Local\Temp\ipykernel_11396\3836770481.py:6: PerformanceWarning: DataFrame is highly fragmented.  This is usually the result of calling `frame.insert` many times, which has poor performance.  Consider joining all columns at once using pd.concat(axis=1) instead. To get a de-fragmented frame, use `newframe = frame.copy()`
  df[(sym, "momentum")] = ta.momentum.ROCIndicator(df[(sym, "close")], window=10).roc()
C:\Users\moham\AppData\Local\Temp\ipykernel_11396\3836770481.py:7: PerformanceWarning: DataFrame is highly fragmented.  This is usually the result of calling `frame.insert` many times, which has poor performance.  Consider joining all columns at once using pd.concat(axis=1) instead. To get a de-fragmented frame, use `newframe = frame.copy()`
  df[(sym, "mean_reversion")] = df[(sym, "close")].rolling(50).mean() - df[(sym, "close")]
C:\Users\moham\AppData\Local\Temp\ipykernel_11396\3836770481.py:4: PerformanceWarning: DataFrame is highly fragmented.  This is usually the result of calling `frame.insert` many times, which has poor performance.  Consider joining all columns at once using pd.concat(axis=1) instead. To get a de-fragmented frame, use `newframe = frame.copy()`
  df[(sym, "volatility")] = ta.volatility.bollinger_hband(df[(sym, "close")], window=20) - \
C:\Users\moham\AppData\Local\Temp\ipykernel_11396\3836770481.py:6: PerformanceWarning: DataFrame is highly fragmented.  This is usually the result of calling `frame.insert` many times, which has poor performance.  Consider joining all columns at once using pd.concat(axis=1) instead. To get a de-fragmented frame, use `newframe = frame.copy()`
  df[(sym, "momentum")] = ta.momentum.ROCIndicator(df[(sym, "close")], window=10).roc()
C:\Users\moham\AppData\Local\Temp\ipykernel_11396\3836770481.py:7: PerformanceWarning: DataFrame is highly fragmented.  This is usually the result of calling `frame.insert` many times, which has poor performance.  Consider joining all columns at once using pd.concat(axis=1) instead. To get a de-fragmented frame, use `newframe = frame.copy()`
  df[(sym, "mean_reversion")] = df[(sym, "close")].rolling(50).mean() - df[(sym, "close")]
C:\Users\moham\AppData\Local\Temp\ipykernel_11396\3836770481.py:4: PerformanceWarning: DataFrame is highly fragmented.  This is usually the result of calling `frame.insert` many times, which has poor performance.  Consider joining all columns at once using pd.concat(axis=1) instead. To get a de-fragmented frame, use `newframe = frame.copy()`
  df[(sym, "volatility")] = ta.volatility.bollinger_hband(df[(sym, "close")], window=20) - \
C:\Users\moham\AppData\Local\Temp\ipykernel_11396\3836770481.py:6: PerformanceWarning: DataFrame is highly fragmented.  This is usually the result of calling `frame.insert` many times, which has poor performance.  Consider joining all columns at once using pd.concat(axis=1) instead. To get a de-fragmented frame, use `newframe = frame.copy()`
  df[(sym, "momentum")] = ta.momentum.ROCIndicator(df[(sym, "close")], window=10).roc()
C:\Users\moham\AppData\Local\Temp\ipykernel_11396\3836770481.py:7: PerformanceWarning: DataFrame is highly fragmented.  This is usually the result of calling `frame.insert` many times, which has poor performance.  Consider joining all columns at once using pd.concat(axis=1) instead. To get a de-fragmented frame, use `newframe = frame.copy()`
  df[(sym, "mean_reversion")] = df[(sym, "close")].rolling(50).mean() - df[(sym, "close")]
C:\Users\moham\AppData\Local\Temp\ipykernel_11396\3836770481.py:4: PerformanceWarning: DataFrame is highly fragmented.  This is usually the result of calling `frame.insert` many times, which has poor performance.  Consider joining all columns at once using pd.concat(axis=1) instead. To get a de-fragmented frame, use `newframe = frame.copy()`
  df[(sym, "volatility")] = ta.volatility.bollinger_hband(df[(sym, "close")], window=20) - \
C:\Users\moham\AppData\Local\Temp\ipykernel_11396\3836770481.py:6: PerformanceWarning: DataFrame is highly fragmented.  This is usually the result of calling `frame.insert` many times, which has poor performance.  Consider joining all columns at once using pd.concat(axis=1) instead. To get a de-fragmented frame, use `newframe = frame.copy()`
  df[(sym, "momentum")] = ta.momentum.ROCIndicator(df[(sym, "close")], window=10).roc()
C:\Users\moham\AppData\Local\Temp\ipykernel_11396\3836770481.py:7: PerformanceWarning: DataFrame is highly fragmented.  This is usually the result of calling `frame.insert` many times, which has poor performance.  Consider joining all columns at once using pd.concat(axis=1) instead. To get a de-fragmented frame, use `newframe = frame.copy()`
  df[(sym, "mean_reversion")] = df[(sym, "close")].rolling(50).mean() - df[(sym, "close")]
C:\Users\moham\AppData\Local\Temp\ipykernel_11396\3836770481.py:4: PerformanceWarning: DataFrame is highly fragmented.  This is usually the result of calling `frame.insert` many times, which has poor performance.  Consider joining all columns at once using pd.concat(axis=1) instead. To get a de-fragmented frame, use `newframe = frame.copy()`
  df[(sym, "volatility")] = ta.volatility.bollinger_hband(df[(sym, "close")], window=20) - \
C:\Users\moham\AppData\Local\Temp\ipykernel_11396\3836770481.py:6: PerformanceWarning: DataFrame is highly fragmented.  This is usually the result of calling `frame.insert` many times, which has poor performance.  Consider joining all columns at once using pd.concat(axis=1) instead. To get a de-fragmented frame, use `newframe = frame.copy()`
  df[(sym, "momentum")] = ta.momentum.ROCIndicator(df[(sym, "close")], window=10).roc()
C:\Users\moham\AppData\Local\Temp\ipykernel_11396\3836770481.py:7: PerformanceWarning: DataFrame is highly fragmented.  This is usually the result of calling `frame.insert` many times, which has poor performance.  Consider joining all columns at once using pd.concat(axis=1) instead. To get a de-fragmented frame, use `newframe = frame.copy()`
  df[(sym, "mean_reversion")] = df[(sym, "close")].rolling(50).mean() - df[(sym, "close")]

Step 3: PCA for Pair Selection

In [3]:
from sklearn.preprocessing import StandardScaler

# ✅ Standardize the Data
scaler = StandardScaler()
scaled_data = scaler.fit_transform(df.xs("close", axis=1, level=1))

# ✅ PCA: Find Principal Components
pca = PCA(n_components=3)  # Keep 3 Principal Components
pca_components = pca.fit_transform(scaled_data)

# ✅ Assign PCA Factor Weights to Each Symbol
factor_df = pd.DataFrame(pca.components_.T, index=symbols, columns=[f"PC{i+1}" for i in range(pca.n_components_)])
print(factor_df)

# ✅ Find Closest Pairs Based on PCA Factor Similarity
def find_pca_pairs(factor_df):
    pairs = []
    for i, sym1 in enumerate(factor_df.index):
        for j, sym2 in enumerate(factor_df.index):
            if i < j:
                distance = euclidean(factor_df.loc[sym1], factor_df.loc[sym2])
                pairs.append((sym1, sym2, distance))
    
    # ✅ Select Best Pairs with Smallest Distance
    sorted_pairs = sorted(pairs, key=lambda x: x[2])
    return sorted_pairs[:10]  # Return Top 10 Closest Pairs

selected_pairs = find_pca_pairs(factor_df)
print("🎯 Best PCA-Based Pairs:", selected_pairs)
             PC1       PC2       PC3
EURUSD -0.074120  0.270696  0.039489
GBPUSD -0.061762  0.274868 -0.131038
USDCHF -0.108607 -0.215683 -0.051013
USDJPY  0.173301 -0.089506 -0.055852
USDCAD  0.164355 -0.045407  0.066992
AUDUSD -0.162769  0.089750 -0.103257
AUDNZD  0.085779 -0.196785 -0.144085
AUDCAD -0.086638  0.126954 -0.108870
AUDCHF -0.174758 -0.053159 -0.103954
AUDJPY  0.155454 -0.083599 -0.149799
CHFJPY  0.182018 -0.008838 -0.035827
EURGBP -0.009282 -0.105880  0.444556
EURAUD  0.128375  0.172335  0.163352
EURJPY  0.178240  0.011701 -0.047627
EURCHF -0.166736  0.074977 -0.007875
EURNZD  0.165860  0.075182  0.087998
EURCAD  0.087572  0.246827  0.111828
GBPCHF -0.151606  0.108690 -0.173693
GBPJPY  0.175006  0.031103 -0.130606
CADCHF -0.166122 -0.101996 -0.081290
CADJPY  0.159921 -0.106765 -0.103625
GBPAUD  0.122411  0.200551 -0.034171
GBPCAD  0.081074  0.256699 -0.086531
GBPNZD  0.154990  0.109517 -0.096284
NZDCAD -0.109840  0.200551  0.019302
NZDCHF -0.180453  0.004945 -0.058713
NZDJPY  0.155090 -0.037133 -0.126900
NZDUSD -0.158342  0.134926 -0.038115
USDSGD -0.057412 -0.223057 -0.079595
AUDSGD -0.174453  0.007578 -0.127653
CHFSGD  0.125553  0.162212  0.016395
EURDKK  0.168947  0.074703 -0.044841
EURHKD -0.077918  0.264445  0.052825
EURNOK  0.168174  0.075413  0.125895
EURPLN -0.138813 -0.154186  0.145267
EURSEK  0.166724 -0.012255  0.137284
EURSGD -0.116597  0.205586  0.004615
EURTRY  0.177953  0.036838 -0.081277
EURZAR  0.151932  0.071716  0.207157
GBPDKK  0.022237  0.108872 -0.439560
GBPNOK  0.168420  0.100331  0.009281
GBPSEK  0.170438  0.026468 -0.026887
GBPSGD -0.095840  0.220007 -0.184375
GBPTRY  0.176567  0.040673 -0.100254
NOKJPY  0.100376 -0.101244 -0.281504
NOKSEK -0.102030 -0.175865 -0.071520
SEKJPY  0.149099  0.026119 -0.195165
SGDJPY  0.179101 -0.048630 -0.042813
USDCNH  0.172838 -0.067409  0.055439
USDCZK  0.066981 -0.211104 -0.209816
🎯 Best PCA-Based Pairs: [('EURUSD', 'EURHKD', 0.015209774849310981), ('NZDCAD', 'EURSGD', 0.016933687040313963), ('EURTRY', 'GBPTRY', 0.01940957888058514), ('CHFJPY', 'EURJPY', 0.02398707933964542), ('USDCAD', 'USDCNH', 0.026259049063238678), ('EURJPY', 'GBPSEK', 0.026627907561686315), ('GBPJPY', 'GBPTRY', 0.03186350419122791), ('EURNZD', 'EURNOK', 0.037968480416527), ('CHFJPY', 'GBPSEK', 0.03821655894445786), ('CHFJPY', 'SGDJPY', 0.04050607133426434)]

Step 4: K-Means Clustering for Pair Selection

In [6]:
from sklearn.cluster import KMeans

# Use factor_df from PCA as input for clustering
kmeans = KMeans(n_clusters=3, random_state=42)
factor_df['cluster'] = kmeans.fit_predict(factor_df)

print("📌 Symbol Clusters:")
print(factor_df[['cluster']])

# Group symbols by cluster and form pairs within each cluster
def find_cluster_pairs_from_factor_df(factor_df):
    pairs = []
    for cluster in factor_df['cluster'].unique():
        symbols_in_cluster = factor_df[factor_df['cluster'] == cluster].index.tolist()
        for i, sym1 in enumerate(symbols_in_cluster):
            for j, sym2 in enumerate(symbols_in_cluster):
                if i < j:
                    pairs.append((sym1, sym2))
    return pairs

cluster_pairs = find_cluster_pairs_from_factor_df(factor_df)
print("🎯 Cluster-Based Pairs:", cluster_pairs)
📌 Symbol Clusters:
        cluster
EURUSD        1
GBPUSD        1
USDCHF        2
USDJPY        0
USDCAD        0
AUDUSD        1
AUDNZD        2
AUDCAD        1
AUDCHF        2
AUDJPY        0
CHFJPY        0
EURGBP        2
EURAUD        0
EURJPY        0
EURCHF        1
EURNZD        0
EURCAD        0
GBPCHF        1
GBPJPY        0
CADCHF        2
CADJPY        0
GBPAUD        0
GBPCAD        1
GBPNZD        0
NZDCAD        1
NZDCHF        1
NZDJPY        0
NZDUSD        1
USDSGD        2
AUDSGD        1
CHFSGD        0
EURDKK        0
EURHKD        1
EURNOK        0
EURPLN        2
EURSEK        0
EURSGD        1
EURTRY        0
EURZAR        0
GBPDKK        1
GBPNOK        0
GBPSEK        0
GBPSGD        1
GBPTRY        0
NOKJPY        0
NOKSEK        2
SEKJPY        0
SGDJPY        0
USDCNH        0
USDCZK        2
🎯 Cluster-Based Pairs: [('EURUSD', 'GBPUSD'), ('EURUSD', 'AUDUSD'), ('EURUSD', 'AUDCAD'), ('EURUSD', 'EURCHF'), ('EURUSD', 'GBPCHF'), ('EURUSD', 'GBPCAD'), ('EURUSD', 'NZDCAD'), ('EURUSD', 'NZDCHF'), ('EURUSD', 'NZDUSD'), ('EURUSD', 'AUDSGD'), ('EURUSD', 'EURHKD'), ('EURUSD', 'EURSGD'), ('EURUSD', 'GBPDKK'), ('EURUSD', 'GBPSGD'), ('GBPUSD', 'AUDUSD'), ('GBPUSD', 'AUDCAD'), ('GBPUSD', 'EURCHF'), ('GBPUSD', 'GBPCHF'), ('GBPUSD', 'GBPCAD'), ('GBPUSD', 'NZDCAD'), ('GBPUSD', 'NZDCHF'), ('GBPUSD', 'NZDUSD'), ('GBPUSD', 'AUDSGD'), ('GBPUSD', 'EURHKD'), ('GBPUSD', 'EURSGD'), ('GBPUSD', 'GBPDKK'), ('GBPUSD', 'GBPSGD'), ('AUDUSD', 'AUDCAD'), ('AUDUSD', 'EURCHF'), ('AUDUSD', 'GBPCHF'), ('AUDUSD', 'GBPCAD'), ('AUDUSD', 'NZDCAD'), ('AUDUSD', 'NZDCHF'), ('AUDUSD', 'NZDUSD'), ('AUDUSD', 'AUDSGD'), ('AUDUSD', 'EURHKD'), ('AUDUSD', 'EURSGD'), ('AUDUSD', 'GBPDKK'), ('AUDUSD', 'GBPSGD'), ('AUDCAD', 'EURCHF'), ('AUDCAD', 'GBPCHF'), ('AUDCAD', 'GBPCAD'), ('AUDCAD', 'NZDCAD'), ('AUDCAD', 'NZDCHF'), ('AUDCAD', 'NZDUSD'), ('AUDCAD', 'AUDSGD'), ('AUDCAD', 'EURHKD'), ('AUDCAD', 'EURSGD'), ('AUDCAD', 'GBPDKK'), ('AUDCAD', 'GBPSGD'), ('EURCHF', 'GBPCHF'), ('EURCHF', 'GBPCAD'), ('EURCHF', 'NZDCAD'), ('EURCHF', 'NZDCHF'), ('EURCHF', 'NZDUSD'), ('EURCHF', 'AUDSGD'), ('EURCHF', 'EURHKD'), ('EURCHF', 'EURSGD'), ('EURCHF', 'GBPDKK'), ('EURCHF', 'GBPSGD'), ('GBPCHF', 'GBPCAD'), ('GBPCHF', 'NZDCAD'), ('GBPCHF', 'NZDCHF'), ('GBPCHF', 'NZDUSD'), ('GBPCHF', 'AUDSGD'), ('GBPCHF', 'EURHKD'), ('GBPCHF', 'EURSGD'), ('GBPCHF', 'GBPDKK'), ('GBPCHF', 'GBPSGD'), ('GBPCAD', 'NZDCAD'), ('GBPCAD', 'NZDCHF'), ('GBPCAD', 'NZDUSD'), ('GBPCAD', 'AUDSGD'), ('GBPCAD', 'EURHKD'), ('GBPCAD', 'EURSGD'), ('GBPCAD', 'GBPDKK'), ('GBPCAD', 'GBPSGD'), ('NZDCAD', 'NZDCHF'), ('NZDCAD', 'NZDUSD'), ('NZDCAD', 'AUDSGD'), ('NZDCAD', 'EURHKD'), ('NZDCAD', 'EURSGD'), ('NZDCAD', 'GBPDKK'), ('NZDCAD', 'GBPSGD'), ('NZDCHF', 'NZDUSD'), ('NZDCHF', 'AUDSGD'), ('NZDCHF', 'EURHKD'), ('NZDCHF', 'EURSGD'), ('NZDCHF', 'GBPDKK'), ('NZDCHF', 'GBPSGD'), ('NZDUSD', 'AUDSGD'), ('NZDUSD', 'EURHKD'), ('NZDUSD', 'EURSGD'), ('NZDUSD', 'GBPDKK'), ('NZDUSD', 'GBPSGD'), ('AUDSGD', 'EURHKD'), ('AUDSGD', 'EURSGD'), ('AUDSGD', 'GBPDKK'), ('AUDSGD', 'GBPSGD'), ('EURHKD', 'EURSGD'), ('EURHKD', 'GBPDKK'), ('EURHKD', 'GBPSGD'), ('EURSGD', 'GBPDKK'), ('EURSGD', 'GBPSGD'), ('GBPDKK', 'GBPSGD'), ('USDCHF', 'AUDNZD'), ('USDCHF', 'AUDCHF'), ('USDCHF', 'EURGBP'), ('USDCHF', 'CADCHF'), ('USDCHF', 'USDSGD'), ('USDCHF', 'EURPLN'), ('USDCHF', 'NOKSEK'), ('USDCHF', 'USDCZK'), ('AUDNZD', 'AUDCHF'), ('AUDNZD', 'EURGBP'), ('AUDNZD', 'CADCHF'), ('AUDNZD', 'USDSGD'), ('AUDNZD', 'EURPLN'), ('AUDNZD', 'NOKSEK'), ('AUDNZD', 'USDCZK'), ('AUDCHF', 'EURGBP'), ('AUDCHF', 'CADCHF'), ('AUDCHF', 'USDSGD'), ('AUDCHF', 'EURPLN'), ('AUDCHF', 'NOKSEK'), ('AUDCHF', 'USDCZK'), ('EURGBP', 'CADCHF'), ('EURGBP', 'USDSGD'), ('EURGBP', 'EURPLN'), ('EURGBP', 'NOKSEK'), ('EURGBP', 'USDCZK'), ('CADCHF', 'USDSGD'), ('CADCHF', 'EURPLN'), ('CADCHF', 'NOKSEK'), ('CADCHF', 'USDCZK'), ('USDSGD', 'EURPLN'), ('USDSGD', 'NOKSEK'), ('USDSGD', 'USDCZK'), ('EURPLN', 'NOKSEK'), ('EURPLN', 'USDCZK'), ('NOKSEK', 'USDCZK'), ('USDJPY', 'USDCAD'), ('USDJPY', 'AUDJPY'), ('USDJPY', 'CHFJPY'), ('USDJPY', 'EURAUD'), ('USDJPY', 'EURJPY'), ('USDJPY', 'EURNZD'), ('USDJPY', 'EURCAD'), ('USDJPY', 'GBPJPY'), ('USDJPY', 'CADJPY'), ('USDJPY', 'GBPAUD'), ('USDJPY', 'GBPNZD'), ('USDJPY', 'NZDJPY'), ('USDJPY', 'CHFSGD'), ('USDJPY', 'EURDKK'), ('USDJPY', 'EURNOK'), ('USDJPY', 'EURSEK'), ('USDJPY', 'EURTRY'), ('USDJPY', 'EURZAR'), ('USDJPY', 'GBPNOK'), ('USDJPY', 'GBPSEK'), ('USDJPY', 'GBPTRY'), ('USDJPY', 'NOKJPY'), ('USDJPY', 'SEKJPY'), ('USDJPY', 'SGDJPY'), ('USDJPY', 'USDCNH'), ('USDCAD', 'AUDJPY'), ('USDCAD', 'CHFJPY'), ('USDCAD', 'EURAUD'), ('USDCAD', 'EURJPY'), ('USDCAD', 'EURNZD'), ('USDCAD', 'EURCAD'), ('USDCAD', 'GBPJPY'), ('USDCAD', 'CADJPY'), ('USDCAD', 'GBPAUD'), ('USDCAD', 'GBPNZD'), ('USDCAD', 'NZDJPY'), ('USDCAD', 'CHFSGD'), ('USDCAD', 'EURDKK'), ('USDCAD', 'EURNOK'), ('USDCAD', 'EURSEK'), ('USDCAD', 'EURTRY'), ('USDCAD', 'EURZAR'), ('USDCAD', 'GBPNOK'), ('USDCAD', 'GBPSEK'), ('USDCAD', 'GBPTRY'), ('USDCAD', 'NOKJPY'), ('USDCAD', 'SEKJPY'), ('USDCAD', 'SGDJPY'), ('USDCAD', 'USDCNH'), ('AUDJPY', 'CHFJPY'), ('AUDJPY', 'EURAUD'), ('AUDJPY', 'EURJPY'), ('AUDJPY', 'EURNZD'), ('AUDJPY', 'EURCAD'), ('AUDJPY', 'GBPJPY'), ('AUDJPY', 'CADJPY'), ('AUDJPY', 'GBPAUD'), ('AUDJPY', 'GBPNZD'), ('AUDJPY', 'NZDJPY'), ('AUDJPY', 'CHFSGD'), ('AUDJPY', 'EURDKK'), ('AUDJPY', 'EURNOK'), ('AUDJPY', 'EURSEK'), ('AUDJPY', 'EURTRY'), ('AUDJPY', 'EURZAR'), ('AUDJPY', 'GBPNOK'), ('AUDJPY', 'GBPSEK'), ('AUDJPY', 'GBPTRY'), ('AUDJPY', 'NOKJPY'), ('AUDJPY', 'SEKJPY'), ('AUDJPY', 'SGDJPY'), ('AUDJPY', 'USDCNH'), ('CHFJPY', 'EURAUD'), ('CHFJPY', 'EURJPY'), ('CHFJPY', 'EURNZD'), ('CHFJPY', 'EURCAD'), ('CHFJPY', 'GBPJPY'), ('CHFJPY', 'CADJPY'), ('CHFJPY', 'GBPAUD'), ('CHFJPY', 'GBPNZD'), ('CHFJPY', 'NZDJPY'), ('CHFJPY', 'CHFSGD'), ('CHFJPY', 'EURDKK'), ('CHFJPY', 'EURNOK'), ('CHFJPY', 'EURSEK'), ('CHFJPY', 'EURTRY'), ('CHFJPY', 'EURZAR'), ('CHFJPY', 'GBPNOK'), ('CHFJPY', 'GBPSEK'), ('CHFJPY', 'GBPTRY'), ('CHFJPY', 'NOKJPY'), ('CHFJPY', 'SEKJPY'), ('CHFJPY', 'SGDJPY'), ('CHFJPY', 'USDCNH'), ('EURAUD', 'EURJPY'), ('EURAUD', 'EURNZD'), ('EURAUD', 'EURCAD'), ('EURAUD', 'GBPJPY'), ('EURAUD', 'CADJPY'), ('EURAUD', 'GBPAUD'), ('EURAUD', 'GBPNZD'), ('EURAUD', 'NZDJPY'), ('EURAUD', 'CHFSGD'), ('EURAUD', 'EURDKK'), ('EURAUD', 'EURNOK'), ('EURAUD', 'EURSEK'), ('EURAUD', 'EURTRY'), ('EURAUD', 'EURZAR'), ('EURAUD', 'GBPNOK'), ('EURAUD', 'GBPSEK'), ('EURAUD', 'GBPTRY'), ('EURAUD', 'NOKJPY'), ('EURAUD', 'SEKJPY'), ('EURAUD', 'SGDJPY'), ('EURAUD', 'USDCNH'), ('EURJPY', 'EURNZD'), ('EURJPY', 'EURCAD'), ('EURJPY', 'GBPJPY'), ('EURJPY', 'CADJPY'), ('EURJPY', 'GBPAUD'), ('EURJPY', 'GBPNZD'), ('EURJPY', 'NZDJPY'), ('EURJPY', 'CHFSGD'), ('EURJPY', 'EURDKK'), ('EURJPY', 'EURNOK'), ('EURJPY', 'EURSEK'), ('EURJPY', 'EURTRY'), ('EURJPY', 'EURZAR'), ('EURJPY', 'GBPNOK'), ('EURJPY', 'GBPSEK'), ('EURJPY', 'GBPTRY'), ('EURJPY', 'NOKJPY'), ('EURJPY', 'SEKJPY'), ('EURJPY', 'SGDJPY'), ('EURJPY', 'USDCNH'), ('EURNZD', 'EURCAD'), ('EURNZD', 'GBPJPY'), ('EURNZD', 'CADJPY'), ('EURNZD', 'GBPAUD'), ('EURNZD', 'GBPNZD'), ('EURNZD', 'NZDJPY'), ('EURNZD', 'CHFSGD'), ('EURNZD', 'EURDKK'), ('EURNZD', 'EURNOK'), ('EURNZD', 'EURSEK'), ('EURNZD', 'EURTRY'), ('EURNZD', 'EURZAR'), ('EURNZD', 'GBPNOK'), ('EURNZD', 'GBPSEK'), ('EURNZD', 'GBPTRY'), ('EURNZD', 'NOKJPY'), ('EURNZD', 'SEKJPY'), ('EURNZD', 'SGDJPY'), ('EURNZD', 'USDCNH'), ('EURCAD', 'GBPJPY'), ('EURCAD', 'CADJPY'), ('EURCAD', 'GBPAUD'), ('EURCAD', 'GBPNZD'), ('EURCAD', 'NZDJPY'), ('EURCAD', 'CHFSGD'), ('EURCAD', 'EURDKK'), ('EURCAD', 'EURNOK'), ('EURCAD', 'EURSEK'), ('EURCAD', 'EURTRY'), ('EURCAD', 'EURZAR'), ('EURCAD', 'GBPNOK'), ('EURCAD', 'GBPSEK'), ('EURCAD', 'GBPTRY'), ('EURCAD', 'NOKJPY'), ('EURCAD', 'SEKJPY'), ('EURCAD', 'SGDJPY'), ('EURCAD', 'USDCNH'), ('GBPJPY', 'CADJPY'), ('GBPJPY', 'GBPAUD'), ('GBPJPY', 'GBPNZD'), ('GBPJPY', 'NZDJPY'), ('GBPJPY', 'CHFSGD'), ('GBPJPY', 'EURDKK'), ('GBPJPY', 'EURNOK'), ('GBPJPY', 'EURSEK'), ('GBPJPY', 'EURTRY'), ('GBPJPY', 'EURZAR'), ('GBPJPY', 'GBPNOK'), ('GBPJPY', 'GBPSEK'), ('GBPJPY', 'GBPTRY'), ('GBPJPY', 'NOKJPY'), ('GBPJPY', 'SEKJPY'), ('GBPJPY', 'SGDJPY'), ('GBPJPY', 'USDCNH'), ('CADJPY', 'GBPAUD'), ('CADJPY', 'GBPNZD'), ('CADJPY', 'NZDJPY'), ('CADJPY', 'CHFSGD'), ('CADJPY', 'EURDKK'), ('CADJPY', 'EURNOK'), ('CADJPY', 'EURSEK'), ('CADJPY', 'EURTRY'), ('CADJPY', 'EURZAR'), ('CADJPY', 'GBPNOK'), ('CADJPY', 'GBPSEK'), ('CADJPY', 'GBPTRY'), ('CADJPY', 'NOKJPY'), ('CADJPY', 'SEKJPY'), ('CADJPY', 'SGDJPY'), ('CADJPY', 'USDCNH'), ('GBPAUD', 'GBPNZD'), ('GBPAUD', 'NZDJPY'), ('GBPAUD', 'CHFSGD'), ('GBPAUD', 'EURDKK'), ('GBPAUD', 'EURNOK'), ('GBPAUD', 'EURSEK'), ('GBPAUD', 'EURTRY'), ('GBPAUD', 'EURZAR'), ('GBPAUD', 'GBPNOK'), ('GBPAUD', 'GBPSEK'), ('GBPAUD', 'GBPTRY'), ('GBPAUD', 'NOKJPY'), ('GBPAUD', 'SEKJPY'), ('GBPAUD', 'SGDJPY'), ('GBPAUD', 'USDCNH'), ('GBPNZD', 'NZDJPY'), ('GBPNZD', 'CHFSGD'), ('GBPNZD', 'EURDKK'), ('GBPNZD', 'EURNOK'), ('GBPNZD', 'EURSEK'), ('GBPNZD', 'EURTRY'), ('GBPNZD', 'EURZAR'), ('GBPNZD', 'GBPNOK'), ('GBPNZD', 'GBPSEK'), ('GBPNZD', 'GBPTRY'), ('GBPNZD', 'NOKJPY'), ('GBPNZD', 'SEKJPY'), ('GBPNZD', 'SGDJPY'), ('GBPNZD', 'USDCNH'), ('NZDJPY', 'CHFSGD'), ('NZDJPY', 'EURDKK'), ('NZDJPY', 'EURNOK'), ('NZDJPY', 'EURSEK'), ('NZDJPY', 'EURTRY'), ('NZDJPY', 'EURZAR'), ('NZDJPY', 'GBPNOK'), ('NZDJPY', 'GBPSEK'), ('NZDJPY', 'GBPTRY'), ('NZDJPY', 'NOKJPY'), ('NZDJPY', 'SEKJPY'), ('NZDJPY', 'SGDJPY'), ('NZDJPY', 'USDCNH'), ('CHFSGD', 'EURDKK'), ('CHFSGD', 'EURNOK'), ('CHFSGD', 'EURSEK'), ('CHFSGD', 'EURTRY'), ('CHFSGD', 'EURZAR'), ('CHFSGD', 'GBPNOK'), ('CHFSGD', 'GBPSEK'), ('CHFSGD', 'GBPTRY'), ('CHFSGD', 'NOKJPY'), ('CHFSGD', 'SEKJPY'), ('CHFSGD', 'SGDJPY'), ('CHFSGD', 'USDCNH'), ('EURDKK', 'EURNOK'), ('EURDKK', 'EURSEK'), ('EURDKK', 'EURTRY'), ('EURDKK', 'EURZAR'), ('EURDKK', 'GBPNOK'), ('EURDKK', 'GBPSEK'), ('EURDKK', 'GBPTRY'), ('EURDKK', 'NOKJPY'), ('EURDKK', 'SEKJPY'), ('EURDKK', 'SGDJPY'), ('EURDKK', 'USDCNH'), ('EURNOK', 'EURSEK'), ('EURNOK', 'EURTRY'), ('EURNOK', 'EURZAR'), ('EURNOK', 'GBPNOK'), ('EURNOK', 'GBPSEK'), ('EURNOK', 'GBPTRY'), ('EURNOK', 'NOKJPY'), ('EURNOK', 'SEKJPY'), ('EURNOK', 'SGDJPY'), ('EURNOK', 'USDCNH'), ('EURSEK', 'EURTRY'), ('EURSEK', 'EURZAR'), ('EURSEK', 'GBPNOK'), ('EURSEK', 'GBPSEK'), ('EURSEK', 'GBPTRY'), ('EURSEK', 'NOKJPY'), ('EURSEK', 'SEKJPY'), ('EURSEK', 'SGDJPY'), ('EURSEK', 'USDCNH'), ('EURTRY', 'EURZAR'), ('EURTRY', 'GBPNOK'), ('EURTRY', 'GBPSEK'), ('EURTRY', 'GBPTRY'), ('EURTRY', 'NOKJPY'), ('EURTRY', 'SEKJPY'), ('EURTRY', 'SGDJPY'), ('EURTRY', 'USDCNH'), ('EURZAR', 'GBPNOK'), ('EURZAR', 'GBPSEK'), ('EURZAR', 'GBPTRY'), ('EURZAR', 'NOKJPY'), ('EURZAR', 'SEKJPY'), ('EURZAR', 'SGDJPY'), ('EURZAR', 'USDCNH'), ('GBPNOK', 'GBPSEK'), ('GBPNOK', 'GBPTRY'), ('GBPNOK', 'NOKJPY'), ('GBPNOK', 'SEKJPY'), ('GBPNOK', 'SGDJPY'), ('GBPNOK', 'USDCNH'), ('GBPSEK', 'GBPTRY'), ('GBPSEK', 'NOKJPY'), ('GBPSEK', 'SEKJPY'), ('GBPSEK', 'SGDJPY'), ('GBPSEK', 'USDCNH'), ('GBPTRY', 'NOKJPY'), ('GBPTRY', 'SEKJPY'), ('GBPTRY', 'SGDJPY'), ('GBPTRY', 'USDCNH'), ('NOKJPY', 'SEKJPY'), ('NOKJPY', 'SGDJPY'), ('NOKJPY', 'USDCNH'), ('SEKJPY', 'SGDJPY'), ('SEKJPY', 'USDCNH'), ('SGDJPY', 'USDCNH')]

Step 5: Backtest the Pair Trading Strategy

In [7]:
import vectorbt as vbt

# Option 1: Use the PCA-selected pair
pair1, pair2 = selected_pairs[0][:2]

# Option 2: Alternatively, use a cluster-based pair
# pair1, pair2 = cluster_pairs[0]

print(f"Testing pair: {pair1} vs {pair2}")

df1, df2 = data[pair1], data[pair2]

# Compute Spread & Z-Score
spread = df1["close"] - df2["close"]
z_score = (spread - spread.rolling(60).mean()) / spread.rolling(60).std()

# Define Entry & Exit Signals
entries = z_score < -1.5       # Buy pair1, Sell pair2
short_entries = z_score > 1.5    # Sell pair1, Buy pair2
exits = abs(z_score) < 0.5       # Exit when mean reversion occurs

# Backtest using vectorbt
portfolio = vbt.Portfolio.from_signals(
    close=df1["close"],
    entries=entries,
    exits=exits,
    short_entries=short_entries,
    short_exits=exits,
    size=1,  
    size_type="percent",
    init_cash=10000,
    fees=0.0002,
    freq="1H"
)

print(portfolio.stats())
portfolio.plot().show()
Testing pair: EURUSD vs EURHKD
Start                         2021-04-29 00:00:00
End                           2025-03-05 00:00:00
Period                           41 days 16:00:00
Start Value                               10000.0
End Value                             9726.830626
Total Return [%]                        -2.731694
Benchmark Return [%]                   -12.028977
Max Gross Exposure [%]                      100.0
Total Fees Paid                         95.663717
Max Drawdown [%]                        11.550894
Max Drawdown Duration            30 days 09:00:00
Total Trades                                   25
Total Closed Trades                            24
Total Open Trades                               1
Open Trade PnL                          31.724849
Win Rate [%]                                 37.5
Best Trade [%]                           4.555476
Worst Trade [%]                         -1.846803
Avg Winning Trade [%]                    1.590328
Avg Losing Trade [%]                    -1.137923
Avg Winning Trade Duration        1 days 16:13:20
Avg Losing Trade Duration         0 days 13:08:00
Profit Factor                            0.817878
Expectancy                             -12.703926
Sharpe Ratio                            -0.579445
Calmar Ratio                            -1.865092
Omega Ratio                              0.978605
Sortino Ratio                           -0.829624
Name: close, dtype: object
c:\Users\moham\miniconda3\envs\ml\Lib\site-packages\vectorbt\utils\datetime_.py:24: FutureWarning:

'H' is deprecated and will be removed in a future version. Please use 'h' instead of 'H'.

[Data output - unsupported data type map[string]interface {} for mime type application/vnd.plotly.v1+json]

Step 6: Live Trading with MT5

In [ ]:
def live_pair_trading(pair1, pair2):
    """Executes pair trading strategy on MT5."""
    df_signals = generate_pair_signals(pair1, pair2)

    latest = df_signals.iloc[-1]

    if latest["long_signal"]:
        place_order(pair1, LOT_SIZE, is_buy=True, magic=MAGIC_NUMBER)
        place_order(pair2, LOT_SIZE, is_buy=False, magic=MAGIC_NUMBER)
    elif latest["short_signal"]:
        place_order(pair1, LOT_SIZE, is_buy=False, magic=MAGIC_NUMBER)
        place_order(pair2, LOT_SIZE, is_buy=True, magic=MAGIC_NUMBER)
    elif latest["exit_signal"]:
        close_positions(pair1, MAGIC_NUMBER)
        close_positions(pair2, MAGIC_NUMBER)

# ✅ Run Live Trading
while True:
    live_pair_trading(pair1, pair2)
    time.sleep(3600)  # Run every hour

Select specific symbols

Step 1: Load & Feature Engineer Data

In [1]:
import MetaTrader5 as mt5
import pandas as pd
import numpy as np
import ta
from sklearn.decomposition import PCA
from sklearn.cluster import KMeans
from scipy.spatial.distance import euclidean
from statsmodels.tsa.stattools import coint

# ✅ Initialize MT5 Connection
if not mt5.initialize():
    print("MT5 Initialization Failed")
    mt5.shutdown()

# ✅ Fetch Data for Multiple Symbols
symbols = ["AUDUSD", "NZDUSD", "EURUSD", "GBPUSD", "USDJPY", "USDCAD"]  # Can be expanded
n_bars = 2000  # Historical lookback

def get_mt5_data(symbol, n_bars, timeframe=mt5.TIMEFRAME_H1):
    """Fetches historical data from MT5"""
    rates = mt5.copy_rates_from_pos(symbol, timeframe, 0, n_bars)
    if rates is None:
        raise ValueError(f"Could not retrieve data for {symbol}")
    df = pd.DataFrame(rates)
    df["time"] = pd.to_datetime(df["time"], unit="s")
    df.set_index("time", inplace=True)
    return df[["close"]]

# ✅ Load Data for All Symbols
data = {symbol: get_mt5_data(symbol, n_bars) for symbol in symbols}

# ✅ Merge Data into One DataFrame
df = pd.concat(data.values(), axis=1, keys=data.keys())

# ✅ Add Technical Features (Volatility, Momentum, etc.)
def add_features(df):
    for sym in symbols:
        df[(sym, "volatility")] = ta.volatility.bollinger_hband(df[(sym, "close")], window=20) - \
                                  ta.volatility.bollinger_lband(df[(sym, "close")], window=20)
        df[(sym, "momentum")] = ta.momentum.ROCIndicator(df[(sym, "close")], window=10).roc()
        df[(sym, "mean_reversion")] = df[(sym, "close")].rolling(50).mean() - df[(sym, "close")]

    return df.dropna()

df = add_features(df)

print(df.head())  # Verify Features
                      AUDUSD   NZDUSD   EURUSD   GBPUSD   USDJPY   USDCAD  \
                       close    close    close    close    close    close   
time                                                                        
2024-11-07 19:00:00  0.66772  0.60247  1.08048  1.29899  153.016  1.38504   
2024-11-07 20:00:00  0.66700  0.60199  1.07981  1.29726  152.963  1.38617   
2024-11-07 21:00:00  0.66675  0.60170  1.07806  1.29645  153.085  1.38616   
2024-11-07 22:00:00  0.66773  0.60238  1.08025  1.29827  152.853  1.38613   
2024-11-07 23:00:00  0.66802  0.60252  1.08031  1.29871  152.899  1.38594   

                        AUDUSD                              NZDUSD  ...  \
                    volatility  momentum mean_reversion volatility  ...   
time                                                                ...   
2024-11-07 19:00:00   0.014067  0.819883      -0.007538   0.011677  ...   
2024-11-07 20:00:00   0.013384  0.521446      -0.006740   0.011059  ...   
2024-11-07 21:00:00   0.012231  0.571679      -0.006423   0.010070  ...   
2024-11-07 22:00:00   0.010929  0.689125      -0.007317   0.008573  ...   
2024-11-07 23:00:00   0.009795  0.741970      -0.007517   0.007439  ...   

                            EURUSD     GBPUSD                           \
                    mean_reversion volatility  momentum mean_reversion   
time                                                                     
2024-11-07 19:00:00      -0.002424   0.015236  0.462487      -0.006039   
2024-11-07 20:00:00      -0.001998   0.014693  0.354303      -0.004392   
2024-11-07 21:00:00      -0.000547   0.014055  0.441604      -0.003720   
2024-11-07 22:00:00      -0.002993   0.013454  0.600533      -0.005625   
2024-11-07 23:00:00      -0.003309   0.012935  0.633848      -0.006133   

                        USDJPY                              USDCAD            \
                    volatility  momentum mean_reversion volatility  momentum   
time                                                                           
2024-11-07 19:00:00   1.974957 -0.640251        0.60886   0.010612 -0.267865   
2024-11-07 20:00:00   2.063996 -0.698524        0.68188   0.009988 -0.118891   
2024-11-07 21:00:00   2.118684 -0.561225        0.58790   0.008682 -0.215240   
2024-11-07 22:00:00   2.138153 -0.756405        0.84584   0.007324 -0.169250   
2024-11-07 23:00:00   2.109487 -0.759400        0.82878   0.005920 -0.241130   

                                    
                    mean_reversion  
time                                
2024-11-07 19:00:00       0.004597  
2024-11-07 20:00:00       0.003484  
2024-11-07 21:00:00       0.003525  
2024-11-07 22:00:00       0.003583  
2024-11-07 23:00:00       0.003809  

[5 rows x 24 columns]

Step 2: PCA for Pair Selection

In [ ]:
from sklearn.preprocessing import StandardScaler

# ✅ Standardize the Data
scaler = StandardScaler()
scaled_data = scaler.fit_transform(df.xs("close", axis=1, level=1))

# ✅ PCA: Find Principal Components
pca = PCA(n_components=3)  # Keep 3 Principal Components
pca_components = pca.fit_transform(scaled_data)

# ✅ Assign PCA Factor Weights to Each Symbol
factor_df = pd.DataFrame(pca.components_.T, index=symbols, columns=[f"PC{i+1}" for i in range(pca.n_components_)])
print(factor_df)

# ✅ Find Closest Pairs Based on PCA Factor Similarity
def find_pca_pairs(factor_df):
    pairs = []
    for i, sym1 in enumerate(factor_df.index):
        for j, sym2 in enumerate(factor_df.index):
            if i < j:
                distance = euclidean(factor_df.loc[sym1], factor_df.loc[sym2])
                pairs.append((sym1, sym2, distance))
    
    # ✅ Select Best Pairs with Smallest Distance
    sorted_pairs = sorted(pairs, key=lambda x: x[2])
    return sorted_pairs[:10]  # Return Top 10 Closest Pairs

selected_pairs = find_pca_pairs(factor_df)
print("🎯 Best PCA-Based Pairs:", selected_pairs)

Step 3: K-Means Clustering for Pair Selection

In [ ]:
from sklearn.cluster import KMeans

# ✅ K-Means Clustering
kmeans = KMeans(n_clusters=3, random_state=42)
df["cluster"] = kmeans.fit_predict(scaled_data)

# ✅ Group Stocks by Clusters
clusters = df.groupby("cluster").apply(lambda x: x.index.tolist())
print("📌 Identified Clusters:", clusters)

# ✅ Select Best Pairs within Clusters
def find_cluster_pairs(clusters):
    best_pairs = []
    for cluster in clusters:
        for i, sym1 in enumerate(cluster):
            for j, sym2 in enumerate(cluster):
                if i < j:
                    best_pairs.append((sym1, sym2))
    return best_pairs

cluster_pairs = find_cluster_pairs(clusters)
print("🎯 Cluster-Based Pairs:", cluster_pairs)

Step 4: Backtest the Pair Trading Strategy

In [ ]:
import vectorbt as vbt

# ✅ Select Pair for Backtest
pair1, pair2 = selected_pairs[0][:2]
df1, df2 = data[pair1], data[pair2]

# ✅ Compute Spread & Z-Score
spread = df1["close"] - df2["close"]
z_score = (spread - spread.rolling(60).mean()) / spread.rolling(60).std()

# ✅ Define Entry & Exit Signals
entries = z_score < -1.5  # Buy Pair1, Sell Pair2
exits = abs(z_score) < 0.5  # Exit when mean reversion happens
short_entries = z_score > 1.5  # Sell Pair1, Buy Pair2

# ✅ Backtest with vectorbt
portfolio = vbt.Portfolio.from_signals(
    close=df1["close"],
    entries=entries,
    exits=exits,
    short_entries=short_entries,
    short_exits=exits,
    size=1,  
    size_type="percent",
    init_cash=10000,
    fees=0.0002,
    freq="1H"
)

# ✅ Results
print(portfolio.stats())
portfolio.plot().show()

Step 5: Live Trading with MT5

In [ ]:
def live_pair_trading(pair1, pair2):
    """Executes pair trading strategy on MT5."""
    df_signals = generate_pair_signals(pair1, pair2)

    latest = df_signals.iloc[-1]

    if latest["long_signal"]:
        place_order(pair1, LOT_SIZE, is_buy=True, magic=MAGIC_NUMBER)
        place_order(pair2, LOT_SIZE, is_buy=False, magic=MAGIC_NUMBER)
    elif latest["short_signal"]:
        place_order(pair1, LOT_SIZE, is_buy=False, magic=MAGIC_NUMBER)
        place_order(pair2, LOT_SIZE, is_buy=True, magic=MAGIC_NUMBER)
    elif latest["exit_signal"]:
        close_positions(pair1, MAGIC_NUMBER)
        close_positions(pair2, MAGIC_NUMBER)

# ✅ Run Live Trading
while True:
    live_pair_trading(pair1, pair2)
    time.sleep(3600)  # Run every hour