| 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)