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