Overall Statistics |
Total Trades 1493 Average Win 0.54% Average Loss -0.45% Compounding Annual Return 5828090.120% Drawdown 13.400% Expectancy 0.071 Net Profit 23.422% Sharpe Ratio 52211.678 Probabilistic Sharpe Ratio 90.719% Loss Rate 51% Win Rate 49% Profit-Loss Ratio 1.20 Alpha 91583.223 Beta 2.212 Annual Standard Deviation 1.754 Annual Variance 3.077 Information Ratio 54676.957 Tracking Error 1.675 Treynor Ratio 41411.192 Total Fees BUSD0.00 Estimated Strategy Capacity BUSD31000.00 Lowest Capacity Asset MANABUSD 18N |
from AlgorithmImports import * import datetime import math from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor # GUIDE DOCS # Core Concepts # Portfolio object https://www.quantconnect.com/docs/v2/writing-algorithms/portfolio/key-concepts # Order algo https://github.com/QuantConnect/Lean/blob/master/Algorithm.Python/OrderTicketDemoAlgorithm.py # BInance Price Data https://www.quantconnect.com/datasets/binance-crypto-price-data # Quote bars https://www.quantconnect.com/docs/v2/writing-algorithms/securities/asset-classes?ref=v1#Handling-Data-QuoteBars # Indicators https://www.quantconnect.com/docs/v2/writing-algorithms/indicators/supported-indicators class ZEdge43(QCAlgorithm): def Initialize(self): self.SetStartDate(2022, 1, 1) self.SetEndDate(2022,1,7) self.SetAccountCurrency('BUSD') self.SetCash('BUSD', 10000) self.SetWarmup(datetime.timedelta(days=2)) self.UniverseSettings.Resolution = Resolution.Second self.SetTimeZone(TimeZones.Utc) self.thread_executor = ThreadPoolExecutor(max_workers=5) # Account types: https://www.quantconnect.com/docs/v2/cloud-platform/live-trading/brokerages/binance self.SetBrokerageModel(BrokerageName.Binance, AccountType.Margin) # Set default order properties self.DefaultOrderProperties = BinanceOrderProperties() self.DefaultOrderProperties.TimeInForce = TimeInForce.GoodTilCanceled self.DefaultOrderProperties.PostOnly = True self.min_invested = 3 #BUSD . If less than this considered not invested (needed for min order sizes/gas fees etc) # Variables self.SMA_period = 60 self.RSI_period = 2 self.LongSlopeThresh = float(self.GetParameter('LongSlopeThresh')) self.LongRSIThresh = float(self.GetParameter('LongRSIThresh')) self.LongSellWait_mins = float(self.GetParameter('LongSellWait_mins')) self.ShortSlopeThresh = float(self.GetParameter('ShortSlopeThresh')) self.ShortRSIThresh = float(self.GetParameter('ShortRSIThresh')) self.ShortCoverWait_mins = float(self.GetParameter('ShortCoverWait_mins')) # Indicator periods self.sma_numerator = 1*60 # mins self.sma_denominator = 6*60 # mins self.rsi_range = 2*60 # mins self.account_equity_multiplier = 3 self.account_buffer = 0.95 self.OrderUpdateWait_secs = 5 # how long before closing maker positions or updating closing positions #Coins # self.coins = ["ADABUSD","ALGOBUSD","ALICEBUSD","ANCBUSD","ANKRBUSD"]#,"APEBUSD","ATOMBUSD","AUCTIONBUSD","AUDIOBUSD","AVAXBUSD", \ # "AXSBUSD","BAKEBUSD","BELBUSD","BNBBUSD","BNXBUSD","BONDBUSD","BTCBUSD","BURGERBUSD","C98BUSD","CAKEBUSD","CELOBUSD","CHRBUSD", \ # "COMPBUSD","CRVBUSD","DARBUSD","DOGEBUSD","DOTBUSD","DYDXBUSD","EGLDBUSD","ENJBUSD","ENSBUSD","EOSBUSD","ETCBUSD","ETHBUSD", \ # "FILBUSD","FLOWBUSD","FLUXBUSD","FRONTBUSD","FTMBUSD","FTTBUSD","GALABUSD","GALBUSD","GMTBUSD","HBARBUSD","HIVEBUSD","HNTBUSD", \ # "HOTBUSD","ICPBUSD","IDEXBUSD","IMXBUSD","IOTXBUSD","JASMYBUSD","KAVABUSD","KDABUSD","KLAYBUSD","LDOBUSD","LEVERBUSD","LINKBUSD", \ # "LRCBUSD","LTCBUSD","MANABUSD","MATICBUSD","MINABUSD","NEARBUSD","NEXOBUSD","ONEBUSD","OPBUSD","PEOPLEBUSD","PONDBUSD","PYRBUSD", \ # "QNTBUSD","RAREBUSD","REEFBUSD","REIBUSD","RNDRBUSD","ROSEBUSD","RUNEBUSD","SANDBUSD","SFPBUSD","SHIBBUSD","SLPBUSD","SOLBUSD", \ # "SPELLBUSD","STGBUSD","TLMBUSD","TRBBUSD","TRIBEBUSD","TRXBUSD","UNIBUSD","VETBUSD","VOXELBUSD","WINBUSD","WINGBUSD","XLMBUSD", \ # "XRPBUSD","XTZBUSD","YGGBUSD","ZILBUSD"] self.coins = ['ETHBUSD','MANABUSD','FTMBUSD'] # Dictionaries for coins data self.securities = {} self.sma_windows = {} self.sma = {} self.rsi_windows = {} self.rsi = {} self.tickets_maker = {} self.tickets_closing = {} self.positions = {} self.tickets_maker_cancellation = {} self.order_qty = {} self.total_closed = {} self.qty_filled = {} self.qty_filled_closing = {} self.closing_qty = {} self.first_close = {} # Filling the dictionaries for coin in self.coins: self.securities[coin] = self.AddCrypto(coin, Resolution.Second) symbol = self.securities[coin].Symbol self.securities[coin].SetFeeModel(ConstantFeeModel(0)) self.sma_windows[coin] = RollingWindow[float](7*60+1) self.rsi_windows[coin] = RollingWindow[float](2*60+1) self.sma[coin] = self.SMA(symbol,self.SMA_period, resolution=Resolution.Minute) self.rsi[coin] = self.RSI(symbol, self.RSI_period, resolution=Resolution.Minute) self.tickets_maker[coin] = None self.tickets_closing[coin] = None self.positions[coin] = None self.tickets_maker_cancellation[coin] = None self.order_qty[coin] = 0.0 self.total_closed[coin] = 0.0 self.qty_filled[coin] = 0.0 self.qty_filled_closing[coin] = 0.0 self.closing_qty[coin] = 0.0 self.first_close[coin] = False self.Debug(f'Number of coins! {len(self.coins)}') def process_coin(self, coin: str, data: Slice) -> bool: portfolio = self.Portfolio security = self.securities[coin] symbol = security.Symbol symbol_value = symbol.Value # Add sma values to windows if self.sma[coin].IsReady: self.sma_windows[coin].Add(self.sma[coin].Current.Value) if self.rsi[coin].IsReady: self.rsi_windows[coin].Add(self.rsi[coin].Current.Value) # calculating invested amount quote_currency = security.QuoteCurrency.Symbol base_currency = symbol_value.replace(quote_currency,'') invested = portfolio[symbol].Invested amount_invested = portfolio.CashBook[base_currency].Amount amount_invested_accy = portfolio.CashBook[base_currency].ValueInAccountCurrency # Check if indicators are ready if not self.sma_windows[coin].IsReady or not self.rsi_windows[coin].IsReady: return True #STRATEGY if not invested or (amount_invested_accy <= self.min_invested and amount_invested_accy >= -self.min_invested): if self.tickets_maker[coin]: if self.UtcTime >= self.tickets_maker[coin].Time + datetime.timedelta(seconds=self.OrderUpdateWait_secs) \ and self.tickets_maker[coin].Status == OrderStatus.Submitted: # Check if submitted order exists, then cancel it self.tickets_maker[coin].Cancel('Closing maker order as it hasnt been filled') self.Log(f'{self.UtcTime} - {symbol} - Cancel - Cancelling maker order as {self.OrderUpdateWait_secs} seconds have passed.') self.positions[coin] = None if self.UtcTime.second == 0: # RUN EVERY MINUTE if (self.sma_windows[coin][self.sma_numerator]/self.sma_windows[coin][self.sma_denominator]) >= self.LongSlopeThresh \ and self.rsi_windows[coin][self.rsi_range] < self.LongRSIThresh \ and self.positions[coin] == None: # GO LONG! # QTY and Price self.bid_price = round(data.QuoteBars[symbol].Bid.High,2) portfolio_value = portfolio.TotalPortfolioValue self.order_qty[coin] = math.floor(((portfolio_value * self.account_equity_multiplier * self.account_buffer) / len(self.coins)) / self.bid_price) # Round down. # Limit order self.tickets_maker[coin] = self.LimitOrder(symbol, self.order_qty[coin], self.bid_price) self.Debug(f'{self.UtcTime} - {symbol} - LONG ! - Creating bid maker order {self.order_qty[coin]}@{self.bid_price} Current bid price: {data.QuoteBars[symbol].Bid}') # Reset closing order parameters self.total_closed[coin] = 0.0 self.qty_filled[coin] = 0.0 self.qty_filled_closing[coin] = 0.0 self.positions[coin] = 'long' self.first_close[coin] = False if (self.sma_windows[coin][self.sma_numerator]/self.sma_windows[coin][self.sma_denominator]) < self.ShortSlopeThresh \ and self.rsi_windows[coin][self.rsi_range] > self.ShortRSIThresh \ and self.positions[coin] == None: # GO SHORT! # QTY and Price self.ask_price = round(data.QuoteBars[symbol].Ask.Low,2) portfolio_value = portfolio.TotalPortfolioValue self.order_qty[coin] = -math.floor(((portfolio_value * self.account_equity_multiplier * self.account_buffer) / len(self.coins)) / self.ask_price)# Round down. # Limit order self.tickets_maker[coin] = self.LimitOrder(symbol, self.order_qty[coin], self.ask_price) self.Debug(f'{self.UtcTime} - {symbol} - SHORT! - Creating ask maker order {self.order_qty[coin]}@{self.ask_price} Current bid price: {data.QuoteBars[symbol].Ask}') # Reset closing order parameters self.total_closed[coin] = 0.0 self.qty_filled[coin] = 0.0 self.qty_filled_closing[coin] = 0.0 self.closing_qty[coin] = 0.0 self.positions[coin] = 'short' self.first_close[coin] = False if invested and (amount_invested_accy > self.min_invested or amount_invested_accy < -self.min_invested): # We have holdings. if (self.UtcTime >= self.tickets_maker[coin].Time + datetime.timedelta(seconds=self.OrderUpdateWait_secs)) and self.qty_filled[coin] == 0: #We have waited 5 seconds since making order. We cancel order incase partial filled. self.qty_filled[coin] = self.tickets_maker[coin].QuantityFilled self.tickets_maker_cancellation[coin] = self.tickets_maker[coin].Cancel(f'{self.OrderUpdateWait_secs} seconds passed. \ Cancelled order') self.Log(f'{self.UtcTime} - {symbol} - Cancelled - {self.OrderUpdateWait_secs} seconds passed. \ #Cancelled {self.positions[coin]} order. \ #Filled {self.qty_filled[coin]} / {self.order_qty[coin]}.') self.first_close[coin] = True if (self.UtcTime >= self.tickets_maker[coin].Time + datetime.timedelta(minutes=self.LongSellWait_mins) and self.positions[coin] == 'long')\ or (self.UtcTime >= self.tickets_maker[coin].Time + datetime.timedelta(minutes=self.ShortCoverWait_mins) and self.positions[coin] == 'short'): #We have waited 15 minutes since making order and its filled. if self.first_close[coin]: # >=15 mins after maker order and we have position. Place closing order self.closing_qty[coin] = -amount_invested if self.positions[coin] == 'long': self.price = data.QuoteBars[symbol].Ask.Low else: self.price = data.QuoteBars[symbol].Bid.High self.tickets_closing[coin] = self.LimitOrder(symbol, self.closing_qty[coin], self.price) self.Log(f'{self.UtcTime} - {symbol} - Order - closing {self.positions[coin]} maker order {self.closing_qty[coin]}@{self.price}.') self.first_close[coin] = False if not self.first_close[coin]: # >=15. mins after make order and closing ticket exist. Placing new closing order if self.UtcTime >= self.tickets_closing[coin].Time + datetime.timedelta(seconds=self.OrderUpdateWait_secs): # 5 seconds after closing order and we still have a position. # Close ticket self.qty_filled_closing[coin] = self.tickets_closing[coin].QuantityFilled self.tickets_closing[coin].Cancel('closing order') self.total_closed[coin] += self.qty_filled_closing[coin] self.Log(f'{self.UtcTime} - {symbol} - {self.OrderUpdateWait_secs} seconds passed closing order. \ # Filled {self.qty_filled_closing[coin]}. Total closed {self.total_closed[coin]}/{self.qty_filled[coin]}') # Create New Ticket self.closing_qty[coin] = -amount_invested if self.positions[coin] == 'long': self.price = data.QuoteBars[symbol].Ask.Low else: self.price = data.QuoteBars[symbol].Bid.High self.tickets_closing[coin] = self.LimitOrder(symbol, self.closing_qty[coin], self.price) self.Log(f'{self.UtcTime} - {symbol} - Creating new closing {self.positions[coin]} maker order {self.closing_qty[coin]}@{self.price}.') return True def OnData(self, data: Slice) -> None: if self.IsWarmingUp: return # load C# variables into Python for faster loading # REF: https://www.quantconnect.com/docs/v2/writing-algorithms/key-concepts/algorithm-engine for coin in self.coins: # self.thread_executor.submit(self.process_coin, coin, data) self.process_coin(coin, data) def OnOrderEvent(self, orderEvent: OrderEvent) -> None: order = self.Transactions.GetOrderById(orderEvent.OrderId) if orderEvent.Status == OrderStatus.Filled: symbol = orderEvent.Symbol symbol_value = symbol.Value self.Log(f"{self.UtcTime} - {symbol} - ORDER EVENT: {order.Type} : {orderEvent}") quote_currency = self.securities[symbol_value].QuoteCurrency.Symbol base_currency = symbol_value.replace(quote_currency,'') cb_amount = self.Portfolio.CashBook[base_currency].ValueInAccountCurrency if not self.Portfolio[symbol].Invested or (cb_amount < self.min_invested and cb_amount > - self.min_invested): # If symbol is now neutral, we reset position to None self.Log(f'resetting for {symbol_value}') self.positions[symbol_value] = None def OnEndOfAlgorithm(self) -> None: self.thread_executor.shutdown()