| Overall Statistics |
|
Total Trades 790 Average Win 0.00% Average Loss 0.00% Compounding Annual Return -32.005% Drawdown 0.400% Expectancy -0.628 Net Profit -0.422% Sharpe Ratio -10.382 Probabilistic Sharpe Ratio 0% Loss Rate 78% Win Rate 22% Profit-Loss Ratio 0.67 Alpha -0.05 Beta -0.015 Annual Standard Deviation 0.005 Annual Variance 0 Information Ratio -1.051 Tracking Error 0.286 Treynor Ratio 3.492 Total Fees $0.00 Estimated Strategy Capacity $15000.00 Lowest Capacity Asset DOTUSD 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 = 5
# number of seconds the algo holds onto a losing position under sma
LOSING_POSITION_HOLD_TIME = 300
# SMA parameter
SMA = 25
# 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.0
CLOSE_POSITION_SPREAD_PROP = 0.025
# Number of pips from best bid/ask if using Pip based Pricing
OPEN_POSITION_SPREAD_PIP_DEFAULT = 0.00
CLOSE_POSITION_SPREAD_PIP_DEFAULT = 0.025
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