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()