| 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}")