Overall Statistics
Total Trades
1
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
$39.83
from QuantConnect.Indicators import *

class CryptoArb(QCAlgorithm):

    # ==================================================================================
    # Main entry point for the algo     
    # ==================================================================================
    def Initialize(self):
        self.InitAlgoParams()
        #self.Schedule.On(self.DateRules.EveryDay(),
        #         self.TimeRules.Every(timedelta(minutes=1)),
        #         self.Arbitrage)

    # ==================================================================================
    # Set algo params: Symbol, broker model, ticker, etc. Called from Initialize(). 
    # ==================================================================================
    def InitAlgoParams(self):
        
        # Set params for data and brokerage
        # -----------------------------------
        self.SetBrokerageModel(BrokerageName.Bitfinex, AccountType.Margin)
        self.initCash = 20000           # todo: use this to track buy+hold     
        self.SetStartDate(2018, 9, 10)  # Set Start Date 
        self.SetEndDate (2018, 9, 11) # Set End Date
        self.SetCash(self.initCash)     # Set Strategy Cash
        self.ETHUSD = self.AddCrypto("ETHUSD", Resolution.Tick).Symbol # ETH in USD
        self.BTCUSD = self.AddCrypto("BTCUSD", Resolution.Tick).Symbol # BTC in USD
        self.ETHBTC = self.AddCrypto("ETHBTC", Resolution.Tick).Symbol # ETH in BTC
        self.triangle = (
            self.ETHUSD,
            self.BTCUSD,
            self.ETHBTC
        )
        self.SetWarmup(10)
        # Tracking variables
        self.imbalance = None
        self.next_index = None
        self.buy_or_sell = 0 # none = 0, buy = 1, sell = 2
        self.trade_lock = False
        # Limit trades
        self.max_iterations = 20
        self.iteration = 0
        
    def find_imbalance(self, triangle):
        """Returns the index of the crypto pair which is out of balance.
        if the first crypto pair is cheaper than its synthetic, we want to arbitrage it
        if the first crypto pair is more expensive than its synthetic, we want to arbitrage the second pair
        """
        sym_1 = triangle[0] # BTCUSD
        sym_1_price = self.Securities[sym_1].Price
        sym_2 = triangle[1] # ETHUSD
        sym_2_price = self.Securities[sym_2].Price
        sym_3 = triangle[2] # BTCETH
        sym_3_price = self.Securities[sym_3].Price
        # Check imbalance (as percentage of asset price)
        imbalance = (sym_1_price - (sym_2_price*sym_3_price)) / sym_1_price # E.g. USD/ETH - USD/BTC*BTC/ETH
        return imbalance
    
    def reset_variables(self):
        self.imbalance = None
        self.next_index = None
        self.buy_or_sell = 0
        self.trade_lock = False
        self.iteration = self.iteration + 1
    
    def order_symbol_allocation(self, symbol, allocation):
        quantity = self.CalculateOrderQuantity(symbol, allocation)
        return self.MarketOrder(symbol, quantity)

    # ========================================================================
    # Plot Charts. Called whenever we want to plot current values.
    # ========================================================================
    def PlotCharts(self):
        self.Plot('Prices', 'ETHUSD', self.Securities[self.ETHUSD].Price)
        self.Plot('Prices', 'BTCUSD', self.Securities[self.BTCUSD].Price)
        self.Plot('Prices', 'ETHBTC', self.Securities[self.ETHBTC].Price)
    

    # ========================================================================
    # State change logic for handling order flow
    # ========================================================================
    def OnOrderEvent(self, orderEvent):
        order = self.Transactions.GetOrderById(orderEvent.OrderId)
        # We only want to execute when an order has been filled
        if order.Status != OrderStatus.Filled: 
            return
        # Second stage of the arbitrage
        ## Handle the intermediate step (exchanging the two cryptos for each other)
        if self.next_index == 2:
            # Intermediate sell
            if self.buy_or_sell == 2:
                self.Debug("Step 2: Selling {}".format(self.triangle[self.next_index]))
                self.next_index = 1 # Finally sell the alternate crypto for USD
                self.buy_or_sell = 2 # Need to sell back to USD
                self.SetHoldings(self.triangle[self.next_index], 0.0) # Sell the primary crypto for the alternate crypto
                return
            # Intermediate buy
            elif self.buy_or_sell == 1:
                self.Debug("Step 2: Buying {}".format(self.triangle[self.next_index]))
                self.next_index = 0 # Finally sell the primary crypto for USD
                self.buy_or_sell = 2 # Need to sell back to USD
                self.SetHoldings(self.triangle[self.next_index], 1.0) # Buy the primary crypto with the alternate crypto
                return
        # Final stage of the arbitrage
        ## Handle the final step (exchanging the crypto back to USD)
        elif self.next_index < 2 and self.next_index >= 0:
            # Final sell
            self.Debug("Step 3: Selling {}".format(self.triangle[self.next_index]))
            self.next_index = -1 # Last trade
            self.SetHoldings(self.triangle[self.next_index], 0.0) # Sell the primary or alternate crypto for USD
            return
        # Exited
        elif self.next_index == -1:
            self.reset_variables()
                    
    # ============================================================================
    # OnData handler. Triggered by data event (i.e. every time there is new data).
    # ============================================================================ 
    # todo: track 'close' on multiple resolutions (daily bars and hourly bars)
    def OnData(self, data):
        if self.IsWarmingUp:
            return
    
        # Exit on max iterations
        if self.iteration > self.max_iterations:
            self.Liquidate()
            self.Quit()
        
        # Don't initiate if we are in the middle of an arbitrage trade
        if self.trade_lock:
            return
    
        # We are not currently in the middle of an arbitrage trade
        if not self.imbalance and self.next_index is None:# or abs(self.imbalance) < 1:
            self.imbalance = self.find_imbalance(self.triangle)
    
        # TODO: Calculate if trade will be worth it after transaction fees
        if abs(self.imbalance) < .01:
            self.imbalance = None
            return
            
        # First stage of the arbitrage
        ## Initiates the order cascade
        if self.next_index is None and not self.trade_lock:
            self.Debug("imbalance: {}, next_index: {}, buy_or_sell: {}".format(self.imbalance, self.next_index, self.buy_or_sell))
            # We are going to start an arbitrage trade, lock trading
            self.trade_lock = True
            ## If the imbalance is negative, we buy the primary crypto
            if self.imbalance < 0:
                self.Debug("Step 1: Buying {}".format(self.triangle[0]))
                self.next_index = 2 # Need to sell to get the intermediate crypto next
                self.buy_or_sell = 2 # Need to sell the intermediate next
                self.SetHoldings(self.triangle[0], 1.0) # Buy the primary crypto
                return
            elif self.imbalance > 0:
                self.Debug("Step 1: Buying {}".format(self.triangle[1]))
                self.next_index = 2 # Need to buy the primary crypto with the alternate
                self.buy_or_sell = 1 # Need to buy the intermediate next
                self.SetHoldings(self.triangle[1], 1.0) # Buy the alternate crypto
                return