| Overall Statistics |
|
Total Orders 264 Average Win 8.32% Average Loss -2.49% Compounding Annual Return 15.194% Drawdown 44.500% Expectancy 0.398 Start Equity 100000 End Equity 211804.78 Net Profit 111.805% Sharpe Ratio 0.393 Sortino Ratio 0.399 Probabilistic Sharpe Ratio 7.173% Loss Rate 68% Win Rate 32% Profit-Loss Ratio 3.34 Alpha 0.131 Beta -0.053 Annual Standard Deviation 0.323 Annual Variance 0.104 Information Ratio 0.154 Tracking Error 0.355 Treynor Ratio -2.388 Total Fees $540.40 Estimated Strategy Capacity $1200000000.00 Lowest Capacity Asset TSLA UNU3P8Y3WFAD Portfolio Turnover 7.99% Drawdown Recovery 506 |
#region imports
from AlgorithmImports import *
#endregion
import numpy as np
"""
DYNAMIC BREAKOUT STRATEGY WITH FLEXIBLE ENTRY AND LOOP CONTROL
This strategy trades TSLA using a dynamic breakout model. The lookback window
adjusts with recent volatility. When volatility rises, the lookback can expand.
When volatility falls, the lookback can contract. The lookback is kept between
a minimum floor and a maximum ceiling.
The model calculates breakout levels using previous completed daily bars only.
This is important. If the current day's high is included in the breakout level,
the current price will almost never be above the highest high, so the strategy may
not trade.
The strategy uses three signals:
1. Dynamic breakout:
Price breaks above the prior lookback high or below the prior lookback low.
2. Dynamic band:
Price moves above the upper dynamic band or below the lower dynamic band.
3. Trend confirmation:
The short moving average must be above the long moving average for long trades,
and below the long moving average for short trades.
Trading logic:
- Go long when price shows upside breakout or upper-band strength, with positive trend.
- Go short when price shows downside breakout or lower-band weakness, with negative trend.
- Stay in cash when signals are mixed.
Risk management:
- Exit a long position if price falls below the long liquidation level.
- Exit a short position if price rises above the short liquidation level.
- After a risk exit, wait a few days before re-entering.
The model avoids repeated orders by trading only when the target weight changes
meaningfully and by allowing only one trading decision per day.
"""
class DynamicBreakoutAlgorithm(QCAlgorithm):
def Initialize(self):
# ------------------------------------------------------------
# 1. BACKTEST SETTINGS
# ------------------------------------------------------------
self.SetStartDate(2021, 1, 15)
self.SetEndDate(2026, 5, 5)
self.initial_cash = 100000
self.SetCash(self.initial_cash)
# ------------------------------------------------------------
# 2. SECURITIES
# ------------------------------------------------------------
self.symbol = self.AddEquity("TSLA", Resolution.Daily, Market.USA).Symbol
self.spy = self.AddEquity("SPY", Resolution.Daily, Market.USA).Symbol
self.SetBenchmark(self.spy)
# ------------------------------------------------------------
# 3. PARAMETERS
# ------------------------------------------------------------
self.numdays = 20
self.ceiling = 60
self.floor = 10
# Wider band = fewer trades. Smaller band = more trades.
self.band_width = 0.40
self.long_weight = 1.00
self.short_weight = -0.50
self.cash_weight = 0.00
# Trend confirmation.
self.fast_ma_period = 10
self.slow_ma_period = 30
# Loop-control parameters.
self.rebalance_threshold = 0.05
self.cooldown_days = 3
self.cooldown_until = datetime.min.date()
self.last_decision_date = None
# ------------------------------------------------------------
# 4. INDICATORS
# ------------------------------------------------------------
self.fast_ma = self.SMA(self.symbol, self.fast_ma_period, Resolution.Daily)
self.slow_ma = self.SMA(self.symbol, self.slow_ma_period, Resolution.Daily)
self.SetWarmUp(max(self.ceiling, self.slow_ma_period) + 5, Resolution.Daily)
# ------------------------------------------------------------
# 5. LEVELS AND STATE
# ------------------------------------------------------------
self.buy_point = None
self.sell_point = None
self.long_liq_point = None
self.short_liq_point = None
self.upper_band = None
self.lower_band = None
self.middle_band = None
self.current_target_weight = 0.0
self.initial_tsla_price = None
self.initial_spy_price = None
# ------------------------------------------------------------
# 6. SCHEDULED SIGNAL
# ------------------------------------------------------------
self.Schedule.On(
self.DateRules.EveryDay(self.symbol),
self.TimeRules.BeforeMarketClose(self.symbol, 5),
self.SetSignal
)
def SetSignal(self):
# ------------------------------------------------------------
# 1. LOOP CONTROL
# ------------------------------------------------------------
if self.IsWarmingUp:
return
if self.Time.date() <= self.cooldown_until:
return
if self.last_decision_date == self.Time.date():
return
if not self.fast_ma.IsReady or not self.slow_ma.IsReady:
return
# ------------------------------------------------------------
# 2. HISTORY
# ------------------------------------------------------------
history_length = max(self.ceiling + 5, 70)
history = self.History(
self.symbol,
history_length,
Resolution.Daily
)
if history.empty:
return
closes = history["close"]
highs = history["high"]
lows = history["low"]
opens = history["open"]
if len(closes) < 35:
return
# Use previous completed bars only.
# This avoids comparing current price to today's own high/low.
closes_prev = closes.iloc[:-1]
highs_prev = highs.iloc[:-1]
lows_prev = lows.iloc[:-1]
opens_prev = opens.iloc[:-1]
if len(closes_prev) < 31:
return
# ------------------------------------------------------------
# 3. DYNAMIC LOOKBACK
# ------------------------------------------------------------
today_vol = np.std(closes_prev.iloc[-30:])
yesterday_vol = np.std(closes_prev.iloc[-31:-1])
if today_vol <= 0:
return
delta_vol = (today_vol - yesterday_vol) / today_vol
new_lookback = int(round(self.numdays * (1 + delta_vol)))
self.numdays = max(self.floor, min(self.ceiling, new_lookback))
if len(closes_prev) < self.numdays:
return
lookback_highs = highs_prev.iloc[-self.numdays:]
lookback_lows = lows_prev.iloc[-self.numdays:]
lookback_closes = closes_prev.iloc[-self.numdays:]
lookback_opens = opens_prev.iloc[-self.numdays:]
self.buy_point = max(lookback_highs)
self.sell_point = min(lookback_lows)
self.middle_band = np.mean(lookback_closes)
band_std = np.std(lookback_closes)
self.upper_band = self.middle_band + self.band_width * band_std
self.lower_band = self.middle_band - self.band_width * band_std
self.long_liq_point = np.mean(lookback_closes)
self.short_liq_point = np.mean(lookback_opens)
price = self.Securities[self.symbol].Price
if price <= 0:
return
# ------------------------------------------------------------
# 4. SIGNAL LOGIC
# ------------------------------------------------------------
current_weight = self.GetCurrentWeight(self.symbol)
target_weight = self.cash_weight
uptrend = self.fast_ma.Current.Value > self.slow_ma.Current.Value
downtrend = self.fast_ma.Current.Value < self.slow_ma.Current.Value
upside_strength = (
price > self.upper_band
or price > self.buy_point
)
downside_weakness = (
price < self.lower_band
or price < self.sell_point
)
# Risk exits first.
if current_weight > 0 and price <= self.long_liq_point:
target_weight = 0.0
self.cooldown_until = self.Time.date() + timedelta(days=self.cooldown_days)
elif current_weight < 0 and price >= self.short_liq_point:
target_weight = 0.0
self.cooldown_until = self.Time.date() + timedelta(days=self.cooldown_days)
else:
if upside_strength and uptrend:
target_weight = self.long_weight
elif downside_weakness and downtrend:
target_weight = self.short_weight
else:
target_weight = self.cash_weight
# ------------------------------------------------------------
# 5. EXECUTION
# ------------------------------------------------------------
if abs(target_weight - current_weight) < self.rebalance_threshold:
self.last_decision_date = self.Time.date()
return
self.SetHoldings(self.symbol, target_weight)
self.current_target_weight = target_weight
self.last_decision_date = self.Time.date()
self.Debug(
"Trade "
+ str(self.Time.date())
+ " price="
+ str(round(price, 2))
+ " lookback="
+ str(self.numdays)
+ " target="
+ str(round(target_weight, 2))
+ " uptrend="
+ str(uptrend)
+ " downtrend="
+ str(downtrend)
)
def OnData(self, data):
if self.symbol not in data or data[self.symbol] is None:
return
if self.spy not in data or data[self.spy] is None:
return
tsla_price = self.Securities[self.symbol].Price
spy_price = self.Securities[self.spy].Price
if tsla_price <= 0 or spy_price <= 0:
return
if self.initial_tsla_price is None:
self.initial_tsla_price = tsla_price
if self.initial_spy_price is None:
self.initial_spy_price = spy_price
# ------------------------------------------------------------
# 1. EQUITY CURVE
# ------------------------------------------------------------
self.Plot(
"Strategy Equity",
"Portfolio Value",
self.Portfolio.TotalPortfolioValue
)
self.Plot(
"Strategy Equity",
"Buy Hold TSLA",
self.initial_cash * tsla_price / self.initial_tsla_price
)
self.Plot(
"Strategy Equity",
"Buy Hold SPY",
self.initial_cash * spy_price / self.initial_spy_price
)
# ------------------------------------------------------------
# 2. LEVELS
# ------------------------------------------------------------
self.Plot(
"Breakout Levels",
"TSLA Price",
tsla_price
)
if self.buy_point is not None:
self.Plot("Breakout Levels", "Buy Point", self.buy_point)
if self.sell_point is not None:
self.Plot("Breakout Levels", "Sell Point", self.sell_point)
if self.upper_band is not None:
self.Plot("Dynamic Bands", "Upper Band", self.upper_band)
if self.middle_band is not None:
self.Plot("Dynamic Bands", "Middle Band", self.middle_band)
if self.lower_band is not None:
self.Plot("Dynamic Bands", "Lower Band", self.lower_band)
if self.long_liq_point is not None:
self.Plot("Risk Levels", "Long Liquidation", self.long_liq_point)
if self.short_liq_point is not None:
self.Plot("Risk Levels", "Short Liquidation", self.short_liq_point)
# ------------------------------------------------------------
# 3. DIAGNOSTICS
# ------------------------------------------------------------
self.Plot("Diagnostics", "Dynamic Lookback", self.numdays)
self.Plot("Diagnostics", "Target Weight", self.current_target_weight)
if self.fast_ma.IsReady:
self.Plot("Trend", "Fast MA", self.fast_ma.Current.Value)
if self.slow_ma.IsReady:
self.Plot("Trend", "Slow MA", self.slow_ma.Current.Value)
def GetCurrentWeight(self, symbol):
if self.Portfolio.TotalPortfolioValue <= 0:
return 0.0
return (
self.Portfolio[symbol].HoldingsValue
/ self.Portfolio.TotalPortfolioValue
)