Overall Statistics
Total Trades
15173
Average Win
0.03%
Average Loss
-0.07%
Compounding Annual Return
597.289%
Drawdown
9.200%
Expectancy
0.473
Net Profit
1415.939%
Sharpe Ratio
4.659
Probabilistic Sharpe Ratio
99.706%
Loss Rate
2%
Win Rate
98%
Profit-Loss Ratio
0.51
Alpha
4.087
Beta
-0.059
Annual Standard Deviation
0.877
Annual Variance
0.769
Information Ratio
4.547
Tracking Error
0.889
Treynor Ratio
-68.834
Total Fees
$0.00
Estimated Strategy Capacity
$440000.00
Lowest Capacity Asset
LTCUSD E3
# GridBot V2-1
# Discussed here
# https://www.quantconnect.com/forum/discussion/13549/sharing-forex-grid-trading-strategy
# and here
# https://medium.com/@mikelhsia/looking-for-a-no-loss-trading-strategy-heres-the-strategy-that-you-should-look-at-bc15e516100f
# and
# https://quantpedia.com/whats-the-relation-between-grid-trading-and-delta-hedging/

import pandas as pdf
from datetime import datetime
import re
from enum import Enum

class ReferenceType(Enum):
    AVERAGE = 1
    PREVIOUS_CLOSE = 2

class VolatilityType(Enum):
    STANDARD_DEVIATION = 1
    HIGH_LOW = 2    # This trigger very little orders, ineffective.
    ATR = 3

class PositionAmountType(Enum):
    AVERAGE = 1
    INCREMENTAL = 2
    DOUBLEUP = 3


class CryptoMomentumAlgorithmV21(QCAlgorithm):
    def Initialize(self):
        self.SetStartDate(2020, 12, 28)
        # self.SetStartDate(2017, 12, 28)
        self.SetEndDate(datetime.now())
        self.SetCash(1000000)
        #self.SetCash(100000)
        self.resolution = Resolution.Minute
        self._grid_number = 10
        self._leverage_ratio = 2
        #self._leverage_ratio = 50
        self._reference_price = None
        self._std = None
        self._stop_loss = None
        self._cash_preserve_ratio = 0.10
        # self._cash_preserve_ratio = 0.99
        self._cash_per_position = None

        self.IS_READY = False
        self.REFERENCE_PRICE_METHOD = ReferenceType.PREVIOUS_CLOSE
        self.POSITION_AMOUNT_METHOD = PositionAmountType.AVERAGE
        self.VOLATILITY_METHOD = VolatilityType.STANDARD_DEVIATION
        self.VOLATILITY_MULTIPLIER = 2
        self.STOP_LOSS_MODE = False
        self.STOP_LOSS_RESCALE = False
        self.STOP_LOSS_MULTIPLIER = 1.2 # 10% more on upper side and 10% less on lower side
        '''
       
        '''
        # self.pair = 'GBPCHF'
        # self.pair = 'AUDCAD'
        # self.pair = 'GBPCAD'
        # self.pair = 'USDJPY'
        # self.pair = 'AUDJPY'
       
        self.pair = 'LTCUSD'
        # self.pair = 'BTCUSD'

        self.crypto = self.AddCrypto(
            self.pair,
            self.resolution,
            Market.Bitfinex,
            True,
            self._leverage_ratio
        )
        # self.Debug(f'Our Leverage: {self.crypto.MarginModel.GetLeverage(self.crypto)}')
       
        ################################################
        # Adding Schedule event
        self.Schedule.On(
            self.DateRules.MonthStart(self.pair),
            self.TimeRules.AfterMarketOpen(self.pair, 0),
            self.openMarket
        )
        self.Schedule.On(
            self.DateRules.MonthEnd(self.pair),
            self.TimeRules.BeforeMarketClose(self.pair, 5),
            self.closeMarket
        )

    def OnData(self, data):
        if not self.IS_READY or self.pair not in data:
            return

        if not self.STOP_LOSS_MODE:
            return

        # Stop loss
        if data[self.pair].Close < (self._reference_price - self._stop_loss) or data[self.pair].Close > (self._reference_price + self._stop_loss):
            # self.Debug(f'{self.Time}: Break out and clean the open orders')
            # Liquidate all positions and close all open orders
            self.Liquidate(self.pair)
            if self.STOP_LOSS_RESCALE:
                self.openMarket()

    def setParameters(self):
        history = self.History([self.pair], 93, Resolution.Daily)
        history = history.droplevel(0)

        # Set up reference price
        if self.REFERENCE_PRICE_METHOD == ReferenceType.PREVIOUS_CLOSE:
            # Get close of previous day
            self._reference_price = self.History(
                [self.pair],
                2880,
                Resolution.Minute
            ).droplevel(0).close[-1]
        elif self.REFERENCE_PRICE_METHOD == ReferenceType.AVERAGE:
            # Daily moving average
            self.Debug('Average mean')
            self._reference_price = history.close[-20:].mean()

        # Cash amount per position
        if self.POSITION_AMOUNT_METHOD == PositionAmountType.AVERAGE:
            # Leveraged Equal weighted
            self._cash_per_position = (self.Portfolio.MarginRemaining * self._leverage_ratio * self._cash_preserve_ratio) / (self._grid_number * 2)
        elif self.POSITION_AMOUNT_METHOD == PositionAmountType.INCREMENTAL:
            # Leveraged incremental weighted
            self._cash_per_position = (self.Portfolio.MarginRemaining * self._leverage_ratio * self._cash_preserve_ratio) / (sum(range(1, self._grid_number+1)) * 2)
        elif self.POSITION_AMOUNT_METHOD == PositionAmountType.DOUBLEUP:
            # Leveraged double up weighted
            self._cash_per_position = (self.Portfolio.MarginRemaining * self._leverage_ratio * self._cash_preserve_ratio) / ((2**self._grid_number - 1) * 2)

        # Get volatility of previous day
        if self.VOLATILITY_METHOD == VolatilityType.STANDARD_DEVIATION:
            # The 2 * Standardization = 95%
            self._std = history.close.std() * self.VOLATILITY_MULTIPLIER
        elif self.VOLATILITY_METHOD == VolatilityType.HIGH_LOW:
            # The high and low of the previous market open day
            self._std = abs(history.high.max() - history.low.min())
        elif self.VOLATILITY_METHOD == VolatilityType.ATR:
            # ATR
            self._std = ta.ATR(history.high, history.low, history.close, timeperiod=60)[-1]

        self._stop_loss = self._std * self.STOP_LOSS_MULTIPLIER


    def openMarket(self):
        # self.Debug(f'{self.Time} Market open. Order number ({len(self.Transactions.GetOpenOrders(self.pair))})')
        self.setParameters()

        # Cancel all open orders before creating new orders
        self.Liquidate(self.pair)

        # Set up the grid limit order

        for i in range(1, self._grid_number + 1):
            qty = int(self._cash_per_position * self.get_qty_multiplier(i) / (self._reference_price - self._std / self._grid_number * i))
            order = self.LimitOrder(
                self.pair,
                qty,
                round(self._reference_price - self._std / self._grid_number * i, 5),
                f'Long{i}'
            )
            qty = int(self._cash_per_position * self.get_qty_multiplier(i) / (self._reference_price + self._std / self._grid_number * i))
            order = self.LimitOrder(
                self.pair,
                -qty,
                round(self._reference_price + self._std / self._grid_number * i, 5),
                f'Short{i}'
            )
        self.IS_READY = True

    def closeMarket(self):
        # self.Debug(f'{self.Time} Market close')

        # Liquidate all positions by the end of the day
        self.Liquidate(self.pair)

        # self.Debug(f'{self.Time} Market close. Order number ({len(self.Transactions.GetOpenOrders(self.pair))})')

    def OnOrderEvent(self, orderEvent):
        order = self.Transactions.GetOrderById(orderEvent.OrderId)
        if orderEvent.Status == OrderStatus.Filled:
            # self.Debug(
            #     "{0}: {1} ({2})".format(
            #         self.Time,
            #         orderEvent,
            #         order.Tag
            #     )
            # )

            match = re.match(r'(.*)(\d+)(.*)$', order.Tag)

            if not match:
                return

            if match.group(1) == 'Long':
                i = int(match.group(2))
                if match.group(3) == '-Liquidate':
                    qty = int(self._cash_per_position * self.get_qty_multiplier(i) / (self._reference_price - self._std / self._grid_number * i))
                    self.LimitOrder(
                        self.pair,
                        qty,
                        round(self._reference_price - self._std / self._grid_number * i, 5),
                        f'Long{match.group(2)}'
                    )
                else:
                    if (i - 1) == 0:
                        self.LimitOrder(
                            self.pair,
                            -abs(order.Quantity),
                            round(self._reference_price, 5),
                            f'Long{match.group(2)}-Liquidate'
                        )
                    elif i > 1:
                        self.LimitOrder(
                            self.pair,
                            -abs(order.Quantity),
                            round(self._reference_price - self._std / self._grid_number * (i - 1), 5),
                            f'Long{match.group(2)}-Liquidate'
                        )
            elif match.group(1) == 'Short':
                i = int(match.group(2))
                if match.group(3) == '-Liquidate':
                    qty = int(self._cash_per_position * self.get_qty_multiplier(i) / (self._reference_price + self._std / self._grid_number * i))
                    self.LimitOrder(
                        self.pair,
                        -qty,
                        round(self._reference_price + self._std / self._grid_number * i, 5),
                        f'Short{match.group(2)}'
                    )
                    openOrders = self.Transactions.GetOpenOrders(self.pair)
                    for order in openOrders:
                        if order.Tag in [f'Short{match.group(2)}-Liquidate', f'Short{match.group(2)}-Stoploss']:
                            self.Transactions.CancelOrder(order.Id)
                else:
                    if (i - 1) == 0:
                        self.LimitOrder(
                            self.pair,
                            abs(order.Quantity),
                            round(self._reference_price, 5),
                            f'Short{match.group(2)}-Liquidate'
                        )
                    else:
                        self.LimitOrder(
                            self.pair,
                            abs(order.Quantity),
                            round(self._reference_price + self._std / self._grid_number * (i - 1), 5),
                            f'Short{match.group(2)}-Liquidate'
                        )
        self.IS_READY = False

    def get_qty_multiplier(self, grid_number):
        if self.POSITION_AMOUNT_METHOD == PositionAmountType.AVERAGE:
            # Even weighted
            return 1
        elif self.POSITION_AMOUNT_METHOD == PositionAmountType.INCREMENTAL:
            # Incremental
            return grid_number
        elif self.POSITION_AMOUNT_METHOD == PositionAmountType.DOUBLEUP:
            # Double up weighted
            if not isinstance(grid_number, (int)):
                grid_number = int(grid_number)
            return 2**(grid_number - 1)