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