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