Overall Statistics
Total Orders
861
Average Win
5.85%
Average Loss
-2.65%
Compounding Annual Return
80.495%
Drawdown
47.400%
Expectancy
0.312
Start Equity
100000
End Equity
1918772.30
Net Profit
1818.772%
Sharpe Ratio
1.5
Sortino Ratio
2.035
Probabilistic Sharpe Ratio
76.665%
Loss Rate
59%
Win Rate
41%
Profit-Loss Ratio
2.21
Alpha
0.571
Beta
0.032
Annual Standard Deviation
0.382
Annual Variance
0.146
Information Ratio
1.273
Tracking Error
0.406
Treynor Ratio
18.139
Total Fees
$0.00
Estimated Strategy Capacity
$39000.00
Lowest Capacity Asset
XAUUSD 8I
Portfolio Turnover
280.29%
Drawdown Recovery
270
# region imports
from AlgorithmImports import *
# endregion


class LongShortXAUUSDVVolatilityBK(QCAlgorithm):

    def initialize(self):
        self.set_start_date(self.end_date - timedelta(5 * 365))
        self.set_cash(100000)
        # Trade gold against the US dollar on hourly OANDA CFD data, capped at the original 10x leverage.
        self._security = self.add_cfd("XAUUSD", Resolution.HOUR, Market.OANDA)
        self._symbol = self._security.symbol
        self._max_leverage = 10.0
        self._security.set_leverage(self._max_leverage)
        self._risk_per_trade = 0.05
        self._atr_multiplier = float(self.get_parameter("atr_multiplier", 3.5))
        self._take_profit_multiplier = float(self.get_parameter("take_profit_multiplier", 8.0))
        self._bollinger_multiplier = float(self.get_parameter("bollinger_multiplier", 2.0))
        self._keltner_length = 20
        self._keltner_multiplier = float(self.get_parameter("keltner_multiplier", 1.5))
        self._trend_length = int(self.get_parameter("trend_length", 200))
        # Build the Bollinger-inside-Keltner squeeze detector plus the trend filter and sizing ATR.
        self._bollinger = self.bb(self._security, 20, self._bollinger_multiplier, MovingAverageType.SIMPLE)
        self._keltner = self.kch(self._security, self._keltner_length, self._keltner_multiplier, MovingAverageType.SIMPLE)
        self._trend_sma = self.sma(self._security, self._trend_length)
        self._atr = self.atr(self._security, 14, MovingAverageType.WILDERS)
        self._highest = self.max(self._security, self._keltner_length, Resolution.HOUR, Field.HIGH)
        self._lowest = self.min(self._security, self._keltner_length, Resolution.HOUR, Field.LOW)
        self._average = self.sma(self._security, self._keltner_length)
        self._momentum_source_window = RollingWindow[float](self._keltner_length)
        self._regression_value_window = RollingWindow[float](2)
        self._bands_inside_channels_window = RollingWindow[bool](2)
        self._trailing_stop = 0.0
        self._take_profit = 0.0
        self.set_warm_up(self._trend_length + self._keltner_length)

    def on_data(self, data: Slice):
        if self._symbol not in data or data[self._symbol] is None:
            return
        if self.is_warming_up or not self._bollinger.is_ready or not self._atr.is_ready:
            return
        # Flag the pre-breakout state where the Bollinger Bands sit inside the Keltner Channels.
        self._bands_inside_channels_window.add(
            (self._bollinger.lower_band.current.value > self._keltner.lower_band.current.value and 
            self._bollinger.upper_band.current.value < self._keltner.upper_band.current.value)
        )
        self._momentum_source_window.add(
            (self._security.price - ((self._highest.current.value + self._lowest.current.value) / 2 + self._average.current.value) / 2)
        )
        if not self._momentum_source_window.is_ready or not self._bands_inside_channels_window.is_ready:
            return
        # Fit a linear regression to the momentum source and project its latest value.
        y_values = np.array(list(self._momentum_source_window))[::-1]
        slope, intercept = np.polyfit(np.arange(len(y_values)), y_values, 1)
        self._regression_value_window.add(slope * (self._keltner_length - 1) + intercept)
        if not self._regression_value_window.is_ready:
            return
        price = self._security.price
        if not self._security.holdings.invested:
            # Enter on a fresh breakout, sizing the position so the ATR stop risks a fixed fraction of equity.
            if self._bands_inside_channels_window[1] and not self._bands_inside_channels_window[0]:
                current_momentum = self._regression_value_window[0]
                previous_momentum = self._regression_value_window[1]
                trend_value = self._trend_sma.current.value
                atr_value = self._atr.current.value
                go_long = current_momentum > 0 and price > trend_value and current_momentum > previous_momentum
                if go_long or (current_momentum < 0 and price < trend_value and current_momentum < previous_momentum):
                    stop_distance = atr_value * self._atr_multiplier
                    if stop_distance <= 0:
                        return
                    final_quantity = min(
                        (self.portfolio.total_portfolio_value * self._risk_per_trade) / stop_distance, 
                        (self.portfolio.total_portfolio_value * (self._max_leverage - 0.2)) / price
                    )
                    lot_size = self._security.symbol_properties.lot_size
                    final_quantity = int(final_quantity / lot_size) * lot_size
                    if final_quantity <= 0:
                        return
                    if go_long:
                        self.market_order(self._security, final_quantity)
                        self._trailing_stop = price - stop_distance
                        self._take_profit = price + (atr_value * self._take_profit_multiplier)
                    else:
                        self.market_order(self._security, -final_quantity)
                        self._trailing_stop = price + stop_distance
                        self._take_profit = price - (atr_value * self._take_profit_multiplier)
        else:
            current_value = self._regression_value_window[0]
            previous_value = self._regression_value_window[1]
            current_atr_buffer = self._atr.current.value * self._atr_multiplier
            if self._security.holdings.is_long:
                if price >= self._take_profit:
                    self.liquidate(self._security, "Take Profit Hit")
                    return
                self._trailing_stop = max(self._trailing_stop, price - current_atr_buffer)
                if price < self._trailing_stop:
                    self.liquidate(self._security, "Trailing Stop Hit")
                    return
                if current_value <= 0 or (current_value < previous_value and current_value < (0.5 * previous_value)):
                    self.liquidate(self._security, "Momentum Fade")
            else:
                if price <= self._take_profit:
                    self.liquidate(self._security, "Take Profit Hit")
                    return
                self._trailing_stop = min(self._trailing_stop, price + current_atr_buffer)
                if price > self._trailing_stop:
                    self.liquidate(self._security, "Trailing Stop Hit")
                    return
                if current_value >= 0 or (current_value > previous_value and current_value > (0.5 * previous_value)):
                    self.liquidate(self._security, "Momentum Fade")

    def on_warmup_finished(self):
        self.log(f"Strategy initialized. Risk per trade: {self._risk_per_trade}")