Overall Statistics
Total Orders
1702
Average Win
0.34%
Average Loss
-0.29%
Compounding Annual Return
0.893%
Drawdown
8.300%
Expectancy
0.086
Start Equity
100000.00
End Equity
117301.41
Net Profit
17.301%
Sharpe Ratio
-0.505
Sortino Ratio
-0.583
Probabilistic Sharpe Ratio
0.006%
Loss Rate
50%
Win Rate
50%
Profit-Loss Ratio
1.16
Alpha
-0.012
Beta
-0.005
Annual Standard Deviation
0.025
Annual Variance
0.001
Information Ratio
-0.479
Tracking Error
0.165
Treynor Ratio
2.67
Total Fees
$0.00
Estimated Strategy Capacity
$740000.00
Lowest Capacity Asset
USDJPY 8G
Portfolio Turnover
13.98%
from AlgorithmImports import *
import numpy as np
import pandas as pd

class ForexStochasticBollinger(QCAlgorithm):
    def Initialize(self):
        self.SetStartDate(2007, 1, 1)  # Test period start
        self.SetEndDate(2024, 12, 31)  # Test period end
        self.SetCash(100000)
        
        # List of currency pairs
        self.pairs = ["EURUSD", "GBPUSD", "USDCAD", "USDCHF", "AUDUSD", "NZDUSD", "USDJPY", "USDSEK", "USDNOK"]
        self.symbols = [self.AddForex(pair, Resolution.Daily).Symbol for pair in self.pairs]

        # Log generated symbols for debugging
        self.Debug(f"Generated Symbols: {', '.join(str(symbol) for symbol in self.symbols)}")

        # Initialize indicators and data structures
        self.indicators = {symbol: (self.STO(symbol, 14, 1, 3), self.BB(symbol, 20, 2)) for symbol in self.symbols}
        self.daily_returns = {symbol: [] for symbol in self.symbols}
        self.current_positions = {}
        self.trades_today = 0

        # Initialize transaction log
        self.transaction_log = []

        # Set warm-up period for indicators
        self.SetWarmUp(30, Resolution.Daily)

        # Schedule trading logic to run at 0:00 AM daily
        self.Schedule.On(self.DateRules.EveryDay(),
                            self.TimeRules.At(0, 00),
                            self.ExecuteStrategy)

        # Schedule liquidation event at 23:59 every day
        self.Schedule.On(self.DateRules.EveryDay(),
                            self.TimeRules.At(23, 59),
                            self.LiquidatePositions)


    def LiquidatePositions(self):
        self.Debug("Liquidating all positions at 23:59.")
        self.liquidate()


    def ExecuteStrategy(self):
        if self.IsWarmingUp:
            return

        # Reset trades count at the start of the day
        self.trades_today = 0
        
        long_signals = []
        short_signals = []

        # Evaluate signals for each pair
        for symbol in self.symbols:
            stoch, bb = self.indicators[symbol]
            current_k = stoch.StochK.Current.Value
            current_d = stoch.StochD.Current.Value
            upper_band = bb.UpperBand.Current.Value
            lower_band = bb.LowerBand.Current.Value

            # Calculate ADX
            adx_value = self.CalculateADX(symbol)

            # Skip trade if ADX
            if adx_value < 10:
                self.Debug(f"Skipping trade for {symbol} due to low ADX {adx_value:.4f}.")
                continue

            # Check long entry condition
            if current_k < 30 and current_d < 30 and self.Securities[symbol].Price < lower_band:
                long_signals.append(symbol)

            # Check short entry condition
            elif current_k > 70 and current_d > 70 and self.Securities[symbol].Price > upper_band:
                short_signals.append(symbol)

        total_signals = len(long_signals) + len(short_signals)

        # Proceed only if there are more than 2 signals
        if total_signals > 0:
            self.Debug(f"Detected Long Signals: {', '.join(str(symbol.Value) for symbol in long_signals)}")
            self.Debug(f"Detected Short Signals: {', '.join(str(symbol.Value) for symbol in short_signals)}")

        if total_signals > 2:
            # Retrieve historical prices for detected signals
            history = self.History(long_signals + short_signals, 30, Resolution.Daily)

            if history.empty:
                self.Debug("No historical data returned.")
                return

            close_prices = history['close']
            price_data = close_prices.reset_index().pivot(index='time', columns='symbol', values='close')
            daily_returns = price_data.pct_change().dropna()
            correlation_matrix = daily_returns.corr()

            # Log the correlation matrix for debugging
            self.Debug(f"Correlation Matrix:\n{correlation_matrix}")
            
            # Find the pairs with the lowest absolute correlation
            lowest_abs_corr = float('inf')
            selected_symbols = None

            # Iterate through the correlation matrix
            for i in range(len(correlation_matrix.columns)):
                for j in range(i + 1, len(correlation_matrix.columns)):
                    current_corr = correlation_matrix.iloc[i, j]
                    abs_corr = abs(current_corr)
                    if abs_corr < lowest_abs_corr:
                        lowest_abs_corr = abs_corr
                        selected_symbols = (correlation_matrix.columns[i], correlation_matrix.columns[j])

            if selected_symbols is not None:
                self.Debug(f"Selected Symbols: {selected_symbols[0]} and {selected_symbols[1]} with lowest absolute correlation {lowest_abs_corr:.4f}")
                allocation = 0.5  # Equal weight for two trades

                # Execute trades based on selected symbols
                for symbol in selected_symbols:
                    if symbol in long_signals:
                        self.Trade(symbol, True, allocation)
                    elif symbol in short_signals:
                        self.Trade(symbol, False, allocation)

            else:
                self.Debug("No pairs found with sufficient correlation.")
                return  # If no pairs found, exit

        else:
            selected_symbols = long_signals + short_signals  # Use available signals
            allocation = 1.0 / total_signals if total_signals > 0 else 0  # 100% for one signal or equal for two

            # Execute trades based on available signals
            for symbol in selected_symbols:
                if symbol in long_signals:
                    self.Trade(symbol, True, allocation)
                elif symbol in short_signals:
                    self.Trade(symbol, False, allocation)


    def Trade(self, symbol, long_signal, allocation):
        if self.trades_today < 2:
            if long_signal:
                if symbol not in self.current_positions or self.current_positions[symbol] != 1:
                    self.Liquidate(symbol)
                    self.SetHoldings(symbol, allocation)  # Set holdings based on allocation
                    self.current_positions[symbol] = 1
                    self.Debug(f"Entered Long for {symbol} with allocation {allocation:.4f}")
                    self.LogTransaction(symbol, allocation)  # Log the transaction

            else:
                if symbol not in self.current_positions or self.current_positions[symbol] != -1:
                    self.Liquidate(symbol)
                    self.SetHoldings(symbol, -allocation)  # Set holdings based on allocation
                    self.current_positions[symbol] = -1
                    self.Debug(f"Entered Short for {symbol} with allocation {allocation:.4f}")
                    self.LogTransaction(symbol, -allocation)  # Log the transaction

            self.trades_today += 1


    def LogTransaction(self, symbol, amount):
        transaction_date = self.Time  # Get the current date and time
        self.transaction_log.append({
            "date": transaction_date,  # Store the full date and time
            "pair": symbol,
            "amount": amount
        })
        self.Debug(f"Logged Transaction: {transaction_date}, {symbol}, Amount: {amount:.4f}")


    def CalculateADX(self, symbol, period=14):
        """Calculate ADX for the given symbol."""
        # Fetch historical data for ADX calculation
        history = self.History(symbol, period + 1, Resolution.Daily)
        if history.empty:
            return 0
        
        high = history['high'].values
        low = history['low'].values
        close = history['close'].values

        # Calculate the directional movements
        up_move = np.maximum(high[1:] - high[:-1], 0)
        down_move = np.maximum(low[:-1] - low[1:], 0)

        # Calculate True Range
        tr1 = high[1:] - low[1:]
        tr2 = abs(high[1:] - close[:-1])
        tr3 = abs(low[1:] - close[:-1])
        true_range = np.maximum(np.maximum(tr1, tr2), tr3)

        # Calculate the smoothed averages
        atr = np.mean(true_range[-period:])  # Average True Range
        up_movement_avg = np.mean(up_move[-period:])  # Average Up Movement
        down_movement_avg = np.mean(down_move[-period:])  # Average Down Movement

        # Calculate the Directional Index
        if up_movement_avg + down_movement_avg == 0:
            return 0
        plus_di = 100 * (up_movement_avg / atr)
        minus_di = 100 * (down_movement_avg / atr)

        # Calculate ADX
        adx = 100 * np.mean(np.abs(plus_di - minus_di) / (plus_di + minus_di)) if (plus_di + minus_di) != 0 else 0

        return adx

# Entry point for the algorithm
model = ForexStochasticBollinger()