Overall Statistics
Total Trades
169
Average Win
0%
Average Loss
0%
Compounding Annual Return
0%
Drawdown
0%
Expectancy
0
Net Profit
0%
Sharpe Ratio
0
Probabilistic Sharpe Ratio
0%
Loss Rate
0%
Win Rate
0%
Profit-Loss Ratio
0
Alpha
0
Beta
0
Annual Standard Deviation
0
Annual Variance
0
Information Ratio
0
Tracking Error
0
Treynor Ratio
0
Total Fees
BUSD0.00
Estimated Strategy Capacity
BUSD13000.00
Lowest Capacity Asset
BAKEBUSD 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"] 
       
        # 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)

                    if self.bid_price == 0:
                        # in case the bid price cannot be retrieved.
                        return True
                    
                    portfolio_value = portfolio.TotalPortfolioValue
                    # In case quant connect cannot retrieve the price
                    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)

                    if self.ask_price == 0:
                        # in case the ask price cannot be retrieved.
                        return True
                    
                    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()