Overall Statistics
Total Trades
7870
Average Win
0.20%
Average Loss
-0.45%
Compounding Annual Return
10140.037%
Drawdown
42.600%
Expectancy
0.352
Net Profit
23826.111%
Sharpe Ratio
4.054
Loss Rate
6%
Win Rate
94%
Profit-Loss Ratio
0.44
Alpha
-0.004
Beta
259.69
Annual Standard Deviation
0.881
Annual Variance
0.776
Information Ratio
4.038
Tracking Error
0.881
Treynor Ratio
0.014
Total Fees
$0.00
import numpy as np
import decimal as d
import math
from datetime import timedelta

### <summary>
### Basic mean reversion of bitcoin, buy when above sma. Sell when below.
### </summary>
class BTCMeanReversion(QCAlgorithm):

    def Initialize(self):
        '''Initialise the data and resolution required, as well as the cash and start-end dates for the algorithm'''

        self.starting_capitol = 100000
        self.SetStartDate(2017, 1, 1)
        # self.SetEndDate(2018, , 15)    #Set End Date
        self.SetCash(self.starting_capitol)           #Set Strategy Cash

        self.SetBrokerageModel(BrokerageName.GDAX, AccountType.Cash)
        self.SetBenchmark("BTCUSD")
        self.AddCrypto("BTCUSD", Resolution.Minute)
        self.AddCrypto("ETHUSD", Resolution.Minute)

        # This is used to calculate the profit if we were to buy and hold
        self.starting_quantity = 0

        self.weights = {
            "ETHUSD" : 0,
            # "BTCUSD" : 0
        }
        
        self.wait_to_order = {
            "ETHUSD" : False,
            "BTCUSD" : False
        }

        self.short_sma = {
            "ETHUSD" : self.SMA("ETHUSD", 20, Resolution.Daily),
            "BTCUSD" : self.SMA("BTCUSD", 20, Resolution.Daily),
        }

        self.short_ema = {
            "ETHUSD" : self.EMA("ETHUSD", 7, Resolution.Daily),
            "BTCUSD" : self.EMA("BTCUSD", 7, Resolution.Daily),
        }

        # self.long_minute_ema = self.EMA("ETHUSD", 150, Resolution.Minute)
        # self.short_minute_ema = self.EMA("ETHUSD", 600, Resolution.Minute)

        stockPlot = Chart('Trade Plot')
        stockPlot.AddSeries(Series('Equity', SeriesType.Line, 0))
        stockPlot.AddSeries(Series('Benchmark', SeriesType.Line, 0))

        self.Schedule.On(self.DateRules.EveryDay(), self.TimeRules.Every(timedelta(minutes=15)), Action(self.Rebalance))
        self.Schedule.On(self.DateRules.EveryDay(), self.TimeRules.Every(timedelta(hours=12)), Action(self.PlotData))

    def RebalanceCoin(self, symbol):
        price = self.Securities[symbol].Price
        sma = self.short_sma[symbol].Current.Value
        ema = self.short_ema[symbol].Current.Value
        invested = self.Portfolio[symbol].Invested

        if price > sma and sma > 0:
            if price > ema:
                self.weights[symbol] = 1 #min(self.weights[symbol] + 0.2, 1)
            elif price < ema:
                self.weights[symbol] = 0 #max(self.weights[symbol] - 0.1, 0)
        elif invested:
            self.weights[symbol] = 0

    def Rebalance(self):
        # self.PrintUpdate()
        self.RebalancePortfolio()
        self.ReInvestPortfolio()

    def RebalancePortfolio(self):
        for symbol in self.weights:
            self.RebalanceCoin(symbol)

        amount = 1/len(self.weights)
        total = sum(self.weights.values())
        div = 1/total if total > 1 else 1
        if total > 1:
            for symbol, weight in self.weights.items():
                self.weights[symbol] = self.weights[symbol] * div
                # print('New Weight %s : %0.2f ' % (symbol, w))
                # invest the leftover money in others
                # for j in self.weights:
                #     if self.weights[j] > 0:
                #         self.weights[j] = self.weights[j] + amount


    def ReInvestPortfolio(self):
        # Always be fully invested
        for symbol, weight in self.weights.items():
            self.SetHoldings(symbol, min(weight, 1))

    def PlotData(self):
        price = self.Securities["BTCUSD"].Price
        if self.starting_quantity == 0 and price > 0:
           self.starting_quantity = self.starting_capitol / price

        self.Plot('Trade Plot', 'Equity', self.Portfolio.TotalPortfolioValue)
        self.Plot('Trade Plot', 'Benchmark', (price * self.starting_quantity))

    def PrintSignal(self, signal):
        self.Plot('MA Plot', signal, self.Securities["BTCUSD"].Price)
        print('| Signal %s | Porfolio $%0.2f |' % (signal, self.Portfolio.TotalPortfolioValue))

    def PrintUpdate(self):
        print('| %s Porfolio $%0.2f | Benchmark $%0.2f | %s weight %0.2f | %s weight %0.2f ' % (self.Transactions.UtcTime, self.Portfolio.TotalPortfolioValue, (self.Securities["BTCUSD"].Price * self.starting_quantity), "BTCUSD", self.Portfolio["BTCUSD"].Quantity, "ETHUSD", self.Portfolio["ETHUSD"].Quantity))

    def OnOrderEvent(self, newEvent):
        if newEvent.Status == OrderStatus.Canceled:
            pass
            # self.Rebalance()
        elif newEvent.Status == OrderStatus.Filled:
            s = 'BUY' if newEvent.Direction == OrderDirection.Buy else 'SELL'
            # self.PrintSignal(s)

    def roundDown(self, n):
        return math.floor(n*100)/100

    def SetHoldings(self, symbol, ratio):
        security = self.Securities[symbol]
        if not security.IsTradable:
            self.Debug("{} is not tradable.".format(symbol))
            return    # passive fail

        ratio = d.Decimal(ratio)
        price = security.Price

        if price == 0:
            self.Debug("{} price is 0".format(symbol))
            return
        quantity = security.Holdings.Quantity

        # Keep 1% Cash    (for the limit order, rounding errors, and safety)
        totalPortfolioValue = self.Portfolio.TotalPortfolioValue * d.Decimal(0.99)
        desiredQuantity = totalPortfolioValue * ratio / price
        orderQuantity = desiredQuantity - quantity
        if orderQuantity > 0.01 or orderQuantity < -0.01:
            # limit needs to be inverse when selling
            limitPrice = price - d.Decimal(0.01) if orderQuantity > 0 else price + d.Decimal(0.01)
            limitPrice = self.roundDown(limitPrice)
            open_orders = self.Transactions.GetOpenOrders(symbol)
            if len(open_orders) > 0: # need to fill an order before placing the next\
                # self.Log("Cancel Orders")
                if not self.wait_to_order[symbol]:
                    self.Transactions.CancelOpenOrders(symbol)
                    self.wait_to_order[symbol] = True
                else:
                    self.wait_to_order[symbol] = False
            elif len(open_orders) == 0:
                if orderQuantity > 0 and self.Portfolio.Cash > d.Decimal(limitPrice) * d.Decimal(orderQuantity):
                    # self.Log("Cash $%0.2f | Limit Order %s: %0.2f coins @ $%0.2f per coin | actual price $%0.2f" % (ratio, self.Portfolio.Cash, symbol, orderQuantity, limitPrice, price))
                    self.LimitOrder(symbol, orderQuantity, limitPrice)
                elif orderQuantity < 0:
                    # self.Log("Cash $%0.2f | Limit Order %s: %0.2f coins @ $%0.2f per coin | actual price $%0.2f" % (ratio, self.Portfolio.Cash, symbol, orderQuantity, limitPrice, price))
                    self.LimitOrder(symbol, orderQuantity, limitPrice)