Overall Statistics |
Total Trades 1564 Average Win 0.01% Average Loss -0.01% Compounding Annual Return 832.245% Drawdown 0.600% Expectancy 0.482 Net Profit 2.477% Sharpe Ratio 50.636 Probabilistic Sharpe Ratio 0% Loss Rate 29% Win Rate 71% Profit-Loss Ratio 1.10 Alpha 8.988 Beta -0.406 Annual Standard Deviation 0.176 Annual Variance 0.031 Information Ratio 20.676 Tracking Error 0.418 Treynor Ratio -21.868 Total Fees $0.00 Estimated Strategy Capacity $61000.00 Lowest Capacity Asset XMRUSD E3 |
#region imports from AlgorithmImports import * #endregion # Starting cash for backtests and base currency CURRENCY = "USD" CASH = 10000 # position size in base currency (keep in mind number of coins) POSITION_SIZE = 100 # number of seconds between each quote adjustment QUOTE_ADJUSTMENT_INTERVAL = 10 # number of seconds the algo holds onto a losing position under sma LOSING_POSITION_HOLD_TIME = 300 # SMA parameter SMA = 90 # Resolution of bars for SMA. (e.g. Resolution.Second, Resolution.Minute, etc.) SMA_RESOLUTION = Resolution.Minute COINS = [ "BTCUSD", "ETHUSD", "ADAUSD", "SOLUSD", "LTCUSD", "XRPUSD", "AVAXUSD", "DOTUSD", "EOSUSD", "LINKUSD", "MATICUSD", "XMRUSD", "DOGEUSD", "TRXUSD", "SHIBUSD", "XLMUSD", "OMGUSD", "SUSHIUSD", "IOTAUSD", "UNIUSD", "XTZUSD", "ZECUSD", "ALGOUSD", "JASMYUSD", #24 ] ### ================================ PRICING PARAMS ================================ # True if using Pips to set pricing, False if using proportions PIP_BASED_PRICING = False # Distance as a percentage of the position from the best bid/ask (e.g. 0.1 = 0.1%) if not using # Pip Based Pricing OPEN_POSITION_SPREAD_PROP = 0.2 CLOSE_POSITION_SPREAD_PROP = 0.4 # Number of pips from best bid/ask if using Pip based Pricing OPEN_POSITION_SPREAD_PIP_DEFAULT = 0.01 CLOSE_POSITION_SPREAD_PIP_DEFAULT = 0.03 OPEN_POSITION_SPREAD_PIP = { "BTCUSD": 1, "LTCUSD": 1, "ETHUSD": 1, "SOLUSD": 1, "XRPUSD": 1 } CLOSE_POSITION_SPREAD_PIP = { "BTCUSD": 4, "LTCUSD": 4, "ETHUSD": 4, "SOLUSD": 4, "XRPUSD": 4 } # Price Rounding Precision for Each Coin PRICE_ROUNDING_PRECISION = { "BTCUSD": 0, "LTCUSD": 3, "ETHUSD": 1, "SOLUSD": 3, "XRPUSD": 5, "ADAUSD": 5, "AVAXUSD": 3, "DOTUSD": 4, "EOSUSD": 5, "LINKUSD": 4, "MATICUSD": 5, "XMRUSD": 2 } # number of decimals to which price is rounded (e.g. 5 = 0.0000X) if coin not listed in PRICE_ROUNDING_PRECISION PRICE_ROUNDING_PRECISION_DEFAULT = 10 ### ================================ VOLUME PARAMS ================================ # Volume Rounding Precisions for Each Coin VOLUME_ROUNDING_PRECISION = { "BTCUSD": 4, "LTCUSD": 1, "ETHUSD": 2, "SOLUSD": 1, "XRPUSD": 1, "ADAUSD": 1, "AVAXUSD": 2, "DOTUSD": 1, "EOSUSD": 1, "LINKUSD": 1, "MATICUSD": 1, "XMRUSD": 2 } # Number of Decimals to which volume is rounded if coin not listed in VOLUME_ROUNDING_PRECISION VOLUME_ROUNDING_PRECISION_DEFAULT = 3
# region imports from AlgorithmImports import * import pytz import config from param_helper import get_mapped_value # endregion class CyptoMarketMakerV2_2(QCAlgorithm): def Initialize(self): # 1/5/22 - 1/6/22 for benchmark testing self.SetStartDate(2022, 6, 1) # Set Start Date self.SetEndDate(2022, 6, 4) # Set end Date # Set Bitfinex Default Order Properties self.DefaultOrderProperties = BitfinexOrderProperties() self.DefaultOrderProperties.TimeInForce = TimeInForce.GoodTilCanceled self.DefaultOrderProperties.Hidden = False self.DefaultOrderProperties.PostOnly = True # bar resolution self.Resolution = Resolution.Second self.SetAccountCurrency(config.CURRENCY) self.SetCash(config.CASH) # self.SetTimeZone("GMT") # self.position_size = config.POSITION_SIZE # self.quote_adjustment_interval = config.QUOTE_ADJUSTMENT_INTERVAL # self.spread = config.POSITION_SPREAD self.EnableAutomaticIndicatorWarmUp = True # adds coins from config file self.crypto_assets = {} self.smas = {} # self.order_tickets = {} # self.history for ticker in config.COINS: self.crypto_assets[ticker] = self.AddCrypto(ticker, Resolution.Second, Market.Bitfinex) self.crypto_assets[ticker].SetFeeModel(ConstantFeeModel(0)) # sets feemodels to 0 for backtesting # self.smas[ticker] = self.SMA(ticker, config.SMA*60, Resolution.Second) # creates SMAs self.smas[ticker] = self.SMA(self.crypto_assets[ticker].Symbol, config.SMA, config.SMA_RESOLUTION) # creates SMAs # self.crypto_assets[coin] = self.crypto_assets[coin].Symbol # creates symbol objects def OnData(self, data: Slice): # for symbol, quote_bar in data.QuoteBars.items(): # self.Debug(symbol, quote_bar) for ticker, coin in self.crypto_assets.items(): symbol = coin.Symbol # skips any coins where the SMA is not ready if not self.smas[ticker].IsReady: self.Debug(f"SMA for {ticker} Not Ready ({self.smas[ticker]})") continue #Guard to skip coins not yet in data if symbol not in data.QuoteBars: self.Debug(f"Symbol for {ticker} Not Found ({coin}). Skipped Coin.") continue # read in quote bars QuoteBar = data.QuoteBars[symbol] PriceClose = QuoteBar.Close # BidClose = QuoteBar.Bid.Close # AskClose = QuoteBar.Ask.Close BidClose = coin.BidPrice AskClose = coin.AskPrice self.current_time = pytz.timezone("GMT").localize(QuoteBar.EndTime) # get custom or default rounding precisions for coins volume_rounding_precision = get_mapped_value(ticker, config.VOLUME_ROUNDING_PRECISION, config.VOLUME_ROUNDING_PRECISION_DEFAULT) price_rounding_precision = get_mapped_value(ticker, config.PRICE_ROUNDING_PRECISION, config.PRICE_ROUNDING_PRECISION_DEFAULT) # set and round bid and ask prices for open and closing trades if config.PIP_BASED_PRICING: open_pip_spread = get_mapped_value(ticker, config.OPEN_POSITION_SPREAD_PIP, config.OPEN_POSITION_SPREAD_PIP_DEFAULT) close_pip_spread = get_mapped_value(ticker, config.CLOSE_POSITION_SPREAD_PIP, config.CLOSE_POSITION_SPREAD_PIP_DEFAULT) open_bid_price = round(BidClose - (coin.SymbolProperties.MinimumPriceVariation * open_pip_spread),price_rounding_precision) open_ask_price = round(AskClose + (coin.SymbolProperties.MinimumPriceVariation * open_pip_spread), price_rounding_precision) close_bid_price = round(BidClose - (coin.SymbolProperties.MinimumPriceVariation * close_pip_spread), price_rounding_precision) close_ask_price = round(AskClose + (coin.SymbolProperties.MinimumPriceVariation * close_pip_spread), price_rounding_precision) else: open_bid_factor = 1-(config.OPEN_POSITION_SPREAD_PROP/100) open_ask_factor = 1+(config.OPEN_POSITION_SPREAD_PROP/100) close_bid_factor = 1-(config.CLOSE_POSITION_SPREAD_PROP/100) close_ask_factor = 1+(config.CLOSE_POSITION_SPREAD_PROP/100) open_bid_price = round(BidClose*open_bid_factor, price_rounding_precision) open_ask_price = round(AskClose*open_ask_factor, price_rounding_precision) close_bid_price = round(BidClose*close_bid_factor, price_rounding_precision) close_ask_price = round(AskClose*close_ask_factor, price_rounding_precision) # set and round volume for trades bars close bid_volume = round(config.POSITION_SIZE/open_bid_price, volume_rounding_precision) ask_volume = round(-config.POSITION_SIZE/open_ask_price, volume_rounding_precision) # gets all the open orders for the current coin (returns empty list if no open orders) # open_orders = self.Transactions.GetOpenOrders(symbol) # open_tickets = list(self.Transactions.GetOpenOrderTickets(symbol)) open_tickets = list(self.Transactions.GetOpenOrderTickets(lambda x : (x.Symbol.Value == symbol.Value) and (x.Status not in [6]))) # Checks if no open position or existing order # NOTE: WHAT ABOUT PARTIALLY FILLED? # if self.Portfolio[ticker].Quantity == 0: # if ticker not in self.Portfolio: # if abs(self.Portfolio[ticker].Quantity) < abs(bid_volume) or abs(self.Portfolio[ticker].Quantity) < abs(ask_volume): # if no open position if abs(self.Portfolio[ticker].Quantity) < abs(coin.SymbolProperties.MinimumOrderSize): # Checks if any open orders, if not places an order if len(open_tickets) == 0: # checks SMA against close of previous bar (second) goes long if close over and short if close under if PriceClose >= self.smas[ticker].Current.Value: self.LimitOrder(symbol, bid_volume, open_bid_price) else: self.LimitOrder(symbol, ask_volume, open_ask_price) # Checks if open order is still above/below SMA if len(open_tickets) == 1: # checks SMA against close of previous bar (second) position_size = self.Portfolio[ticker].Quantity if (PriceClose < self.smas[ticker].Current.Value and position_size > 0): self.MarketOrder(symbol, -position_size) elif (PriceClose > self.smas[ticker].Current.Value and position_size < 0): self.MarketOrder(symbol, abs(position_size)) # if open orders then adjusts price if time period is a multiple of x seconds else: for ticket in open_tickets: self.cancel_or_adjust_ticket(coin, ticket, open_bid_price, open_ask_price) # debug message incase more than 1 open order, will need to build a handle later if len(open_tickets) > 1: self.Debug(f"A Larger than expected number of orders exists for {ticker}, expected 1 got {len(open_tickets)} : {open_tickets}") # if open position already exists then checks if closing order exists otherwise creates one # if a closing order does exist it may need to be moved or cancelled else: position_size = self.Portfolio[ticker].Quantity position_price = self.Portfolio[ticker].AveragePrice # checks if there is also an open ticket if len(open_tickets) == 0: # if position is short then close with long if position_size < 0: self.LimitOrder(symbol, -position_size, close_bid_price) # if position is long then close with an ask else: self.LimitOrder(symbol, -position_size, close_ask_price) else: # checks if winning long or short if (position_price < PriceClose and position_size > 0) or (position_price > PriceClose and position_size < 0): force_update = True else: force_update = False for ticket in open_tickets: self.cancel_or_adjust_ticket(coin, ticket, close_bid_price, close_ask_price, force_update) # debug message incase more than 1 open order, will need to build a handle later if len(open_tickets) > 1: self.Debug(f"A Larger than expected number of orders exists for {ticker}, expected 1 got {len(open_tickets)} : {open_tickets}") def cancel_or_adjust_ticket(self, coin, ticket, bid_price, ask_price, force_update=False): last_updated = ticket.Time # on restart ticket times will be timezone unaware if last_updated.tzinfo is None: last_updated = pytz.timezone("GMT").localize(last_updated) # checks if time period correct for adjusting price if round((last_updated - self.current_time).total_seconds() % config.QUOTE_ADJUSTMENT_INTERVAL) == 0: # self.Debug(f"cancel_or_adjust_ticket passed quote adjustment {coin} {ticket} Bid: {bid_price}, Ask {ask_price}") # checks if under cancel threshold based on currency if abs(ticket.Quantity) - abs(ticket.QuantityFilled) < coin.SymbolProperties.MinimumOrderSize: response = ticket.Cancel() if response.IsSuccess: self.Debug(f"TICKET for {ticket.Symbol} CANCELLED: {ticket.QuantityFilled} of {ticket.Quantity} Filled \ (Threshold: {coin.SymbolProperties.MinimumOrderSize})") # adjusts price depending on if bid or ask (this is on the open side), also has check if within range else: # get current Ticket Price current_price = ticket.Get(OrderField.LimitPrice) # if sell side (adjust ask) if ticket.Quantity < 0 and (ask_price < current_price or force_update): updateSettings = UpdateOrderFields() updateSettings.LimitPrice = ask_price response = ticket.Update(updateSettings) if not response.IsSuccess: self.Debug(f"Limit Order (ASK) could not be updated. Code:{response.ErrorCode}, Error: {response.ErrorMessage}") # if buy side (adjust bid) elif ticket.Quantity > 0 and (bid_price > current_price or force_update): updateSettings = UpdateOrderFields() updateSettings.LimitPrice = bid_price response = ticket.Update(updateSettings) if not response.IsSuccess: self.Debug(f"Limit Order (BID) could not be updated. Code:{response.ErrorCode}, Error: {response.ErrorMessage}") # if neither then skips
#region imports from AlgorithmImports import * #endregion def get_mapped_value(key, dictionary, default): """ checks for a key in dictionary and returns value otherwise returns default value """ if key in dictionary: value = dictionary[key] else: value = default return value