| Overall Statistics |
|
Total Orders 1647 Average Win 0.13% Average Loss -0.12% Compounding Annual Return 17.428% Drawdown 6.000% Expectancy 0.177 Start Equity 2400000 End Equity 2819105.80 Net Profit 17.463% Sharpe Ratio 0.782 Sortino Ratio 0.941 Probabilistic Sharpe Ratio 66.236% Loss Rate 45% Win Rate 55% Profit-Loss Ratio 1.15 Alpha -0.016 Beta 0.701 Annual Standard Deviation 0.085 Annual Variance 0.007 Information Ratio -0.946 Tracking Error 0.054 Treynor Ratio 0.095 Total Fees $0.00 Estimated Strategy Capacity $11000000.00 Lowest Capacity Asset CHTR UPXX4G43SIN9 Portfolio Turnover 18.71% |
# main.py
from AlgorithmImports import *
from datetime import timedelta, datetime
from time import sleep
from QuantConnect.Indicators import *
from symbols import SYMBOLS
class OptimizedBuySellStrategy(QCAlgorithm):
def initialize(self):
self.webhook_url = "https://wwww.mysite.com/webhook"
self.webhook_headers = {} # set to { 'Authorization': 'Basic MY_PASSWORD' } if you have basic auth in place
self.bar_counts = {}
# Get parameters with defaults
start_date_str = self.get_parameter("start_date") or "2024-01-01"
end_date_str = self.get_parameter("end_date") or "2024-12-31"
cash_str = self.get_parameter("cash") or 100_000 * len(SYMBOLS)
# Parse parameters
try:
start_date = datetime.strptime(start_date_str, "%Y-%m-%d")
end_date = datetime.strptime(end_date_str, "%Y-%m-%d")
cash = float(cash_str)
except ValueError as e:
raise ValueError(f"Invalid date or cash format: {e}")
# Set values
self.set_start_date(start_date.year, start_date.month, start_date.day)
self.set_end_date(end_date.year, end_date.month, end_date.day)
self.set_cash(cash)
self.timeframe_map = {
"15m": timedelta(minutes=15),
"30m": timedelta(minutes=30),
"45m": timedelta(minutes=45),
"1H": timedelta(hours=1),
"2H": timedelta(hours=2),
"3H": timedelta(hours=3),
"4H": timedelta(hours=4),
"1D": timedelta(days=1),
"1W": timedelta(weeks=1)
}
# set brokerage model for backtesting
self.set_brokerage_model(BrokerageName.ALPACA, AccountType.MARGIN) # ALPACA has zero fees
self.universe_settings.leverage = 2
self.symbol_data = {}
# [OUTDATED] Get single symbol_info from parameters
# symbol_info_str = self.get_parameter("symbol_info")
# if symbol_info_str:
# import json
# symbol_info = json.loads(symbol_info_str)
# else:
# raise ValueError("No symbol_info parameter provided")
for symbol_info in SYMBOLS:
# Process single symbol
symbol = self.add_equity(symbol_info["ticker"], Resolution.MINUTE).symbol
timeframe = symbol_info["timeframe"]
if timeframe not in self.timeframe_map:
raise ValueError(f"Invalid timeframe '{timeframe}' for {symbol_info['ticker']}")
self.symbol_data[symbol] = {
"ticker": symbol_info["ticker"],
"timeframe": self.timeframe_map[timeframe],
"risk_index": symbol_info.get("risk_index", 2),
"last_trade_time": None,
"entry_price": None,
"stop_loss": None,
"take_profit": None,
"is_liquidating": False,
"consolidator": self.consolidate(symbol, self.timeframe_map[timeframe], lambda bar: self.bar_handler(bar)),
"rsi": RelativeStrengthIndex(14),
"macd": MovingAverageConvergenceDivergence(12, 26, 9),
"atr": AverageTrueRange(14),
"support": Minimum(50),
"resistance": Maximum(50),
"history": RollingWindow[TradeBar](50),
"bullish_run_window": RollingWindow[TradeBar](4)
}
# Warm up indicators
self.set_warm_up(50)
def bar_handler(self, bar: TradeBar):
symbol = bar.symbol
data = self.symbol_data[symbol]
# Update indicators
data["rsi"].update(bar.end_time, bar.close)
data["macd"].update(bar.end_time, bar.close)
data["atr"].update(bar)
data["support"].update(bar.end_time, bar.low)
data["resistance"].update(bar.end_time, bar.high)
data["history"].add(bar)
data["bullish_run_window"].add(bar)
def on_data(self, data):
for symbol in self.symbol_data:
if not self.symbol_data[symbol]["rsi"].is_ready:
continue
data = self.symbol_data[symbol]
holding = self.portfolio[symbol]
# Configurable parameters
risk_index = data["risk_index"]
rsi_overbought = 70 if risk_index == 2 else 65 if risk_index == 1 else 75
rsi_oversold = 30 if risk_index == 2 else 35 if risk_index == 1 else 25
atr_multiplier = 2.0 if risk_index == 2 else 1.5 if risk_index == 1 else 2.5
cooldown_bars = 10
# Get current bar and previous bars
current_bar = data["history"][0]
prev_bar = data["history"][1] if data["history"].count > 1 else None
if prev_bar is None or data["bullish_run_window"].count < 4:
continue
# Cooldown management
cooldown_over = (data["last_trade_time"] is None or
(self.time - data["last_trade_time"]) > (data["timeframe"] * cooldown_bars))
# Bullish run detection
current_highs = [data["bullish_run_window"][i].high for i in range(3)]
prev_high = data["bullish_run_window"][3].high
current_lows = [data["bullish_run_window"][i].low for i in range(3)]
prev_low = data["bullish_run_window"][3].low
bullish_run = max(current_highs) > prev_high and min(current_lows) > prev_low
# Candle patterns
bullish_engulfing = (current_bar.close > prev_bar.open and
current_bar.close > current_bar.open)
hammer = (current_bar.close > current_bar.open and
(prev_bar.low - current_bar.low) > 2 * (current_bar.close - current_bar.open))
# Buy signal
buy_signal = cooldown_over and not holding.invested and not data["is_liquidating"] and (
(data["rsi"].current.value < rsi_oversold and
data["macd"].fast.current.value > data["macd"].signal.current.value) or
bullish_engulfing or
hammer or
bullish_run
)
# Sell signal
sell_signal = holding.invested and not data["is_liquidating"] and (
(data["rsi"].current.value > rsi_overbought and
data["macd"].fast.current.value < data["macd"].signal.current.value) or
current_bar.close >= data["resistance"].current.value
)
# Execute trades
if buy_signal:
weight = self.universe_settings.leverage / len(self.symbol_data) / 2
self.set_holdings(symbol, weight)
data["stop_loss"] = current_bar.low - (data["atr"].current.value * atr_multiplier)
data["take_profit"] = data["resistance"].current.value
data["last_trade_time"] = self.time
data["entry_price"] = current_bar.close
data["is_liquidating"] = False
self.log(f"Buy {symbol} at {current_bar.close}")
self.send_notification(self.buy_text(data))
if holding.invested and not data["is_liquidating"]:
if bullish_run and data["stop_loss"] is not None:
data["stop_loss"] = max(data["stop_loss"],
current_bar.close - (data["atr"].current.value * atr_multiplier))
stop_loss_triggered = data["stop_loss"] is not None and current_bar.close <= data["stop_loss"]
take_profit_triggered = data["take_profit"] is not None and current_bar.close >= data["take_profit"]
if stop_loss_triggered or take_profit_triggered or sell_signal:
self.liquidate(symbol)
data["is_liquidating"] = True
price = current_bar.close
profit = price - data["entry_price"]
profit_percent = (profit / data["entry_price"]) * 100
self.log(f"Sell {symbol} at {price}, Profit: {profit_percent:.2f}%")
if stop_loss_triggered:
reason = "stop_loss"
elif take_profit_triggered:
reason = "take_profit"
else:
reason = "sell_signal"
self.send_notification(self.sell_text(data, reason, price, profit_percent))
data["stop_loss"] = None
data["take_profit"] = None
data["entry_price"] = None
data["last_trade_time"] = self.time
if not holding.invested and data["is_liquidating"]:
data["is_liquidating"] = False
def buy_text(self, data, reason="buy_signal"):
# format: Smart BUY (ticker, buy_reason, purchase price, take profit price, stop loss price, risk_index, time)
return (f'Smart BUY ({data["ticker"]}, {reason}, {round(data["entry_price"], 2)}, {round(data["take_profit"], 2)}, '
f'{round(data["stop_loss"], 2)}, {data["risk_index"]}, {self.time})')
def sell_text(self, data, reason, price, profit_percent):
# format: Smart SELL (ticker, reason, sell price, profit %, risk_index, time)
return (f'Smart SELL ({data["ticker"]}, {reason}, {round(price, 2)}, {round(profit_percent, 2)}%, '
f'{data["risk_index"]}, {self.time})')
def send_notification(self, text):
if not self.live_mode:
self.log(f"Webhook message: {text}")
return
# try sending notification 3 times in case of exception
for _ in range(3):
try:
self.notify.web(address=self.webhook_url, data=text, headers=self.webhook_headers)
break
except Exception as e:
self.debug(f"Exception while trying to send web hook update: {type(e)} {e}")
sleep(1)
# region imports
from AlgorithmImports import *
# endregion
# This file defines the symbols and their respective candlestick intervals.
SYMBOLS = [
{"ticker": "AAPL", "timeframe": "3H"},
{"ticker": "MSFT", "timeframe": "1D"},
{"ticker": "TSLA", "timeframe": "1W"},
{"ticker": "F", "timeframe": "1D"},
{"ticker": "GE", "timeframe": "4H"},
{"ticker": "NVDA", "timeframe": "4H"},
{"ticker": "BAC", "timeframe": "1D"},
{"ticker": "C", "timeframe": "2H"},
{"ticker": "JPM", "timeframe": "1D"},
{"ticker": "AMZN", "timeframe": "1D"},
{"ticker": "GOOGL", "timeframe": "3H"},
{"ticker": "GOOG", "timeframe": "2H"},
{"ticker": "META", "timeframe": "2H"},
{"ticker": "NFLX", "timeframe": "4H"},
{"ticker": "INTC", "timeframe": "1D"},
{"ticker": "QCOM", "timeframe": "4H"},
{"ticker": "CSCO", "timeframe": "4H"},
{"ticker": "VZ", "timeframe": "2H"},
{"ticker": "T", "timeframe": "1D"},
{"ticker": "TMUS", "timeframe": "1D"},
{"ticker": "CMCSA", "timeframe": "1H"},
{"ticker": "CHTR", "timeframe": "3H"},
{"ticker": "MCD", "timeframe": "3H"},
{"ticker": "SBUX", "timeframe": "1D"}
# Add additional symbols as needed...
]