| Overall Statistics |
|
Total Trades 66 Average Win 74.14% Average Loss -4.63% Compounding Annual Return 147.782% Drawdown 53.900% Expectancy 3.126 Net Profit 906.383% Sharpe Ratio 2.094 Probabilistic Sharpe Ratio 76.003% Loss Rate 76% Win Rate 24% Profit-Loss Ratio 16.02 Alpha 0.778 Beta 0.425 Annual Standard Deviation 0.581 Annual Variance 0.338 Information Ratio 0.278 Tracking Error 0.655 Treynor Ratio 2.867 Total Fees $40963.06 Estimated Strategy Capacity $6600000.00 Lowest Capacity Asset ETHUSD 2MN |
#region imports
from AlgorithmImports import *
#endregion
################################################################################
# KalmanFilterIndicator
#
# Core logic from @vladimir's KalmanFilter implementation:
# https://www.quantconnect.com/forum/discussion/12741/kalman-filter-for-bitcoin/p1
#
################################################################################
from pykalman import KalmanFilter
class KalmanFilterIndicator(PythonIndicator):
def __init__(self,name, period, selector=Field.Close,
transition_matrices = [1], observation_matrices = [1],
initial_state_mean = 0, initial_state_covariance = 1,
observation_covariance=1, transition_covariance=.01):
self.Name = name
self.period = period
self.Value = 0
self.barCalc = selector
self.transition_matrices = transition_matrices
self.observation_matrices = observation_matrices
self.initial_state_mean = initial_state_mean
self.initial_state_covariance = initial_state_covariance
self.observation_covariance = observation_covariance
self.transition_covariance = transition_covariance
self.rollingWindow = RollingWindow[float](self.period)
# ---------------------------------
def Update(self, inputBar):
effectiveBarValue = self.barCalc(inputBar)
self.rollingWindow.Add(effectiveBarValue)
if(not self.rollingWindow.IsReady):
return False
else:
basisValue = np.flipud(np.array([self.rollingWindow[i] for i in range(self.period)]))
self.kf = KalmanFilter( transition_matrices = self.transition_matrices,
observation_matrices = self.observation_matrices,
initial_state_mean = self.initial_state_mean,
initial_state_covariance = self.initial_state_covariance,
observation_covariance = self.observation_covariance,
transition_covariance = self.transition_covariance)
kf,_ = self.kf.filter(basisValue)
currKalman = kf[-1]
self.Value = float(currKalman)
return True
################################################################################
#
# LaguerreFilterIndicator
# ==============================
# Laguerre Filter as defined by John F. Ehlers in `Cybernetic Analysis for
# Stock and Futures`, 2004, published by Wiley. `ISBN: 978-0-471-46307-8
# https://www.mt5users.com/wp-content/uploads/2020/01/timewarp.pdf
#
# Copied from @vladimir's implementation
# https://www.quantconnect.com/forum/discussion/11788/another-digital-filter-laguerre-filter/p1/comment-34897
#
################################################################################
class LaguerreFilterIndicator(PythonIndicator):
def __init__(self, name, gamma ):
self.Name = name
self.gamma = gamma
self.prices = np.array([])
self.Value = 0
self.L0 = 0.0; self.L1 = 0.0; self.L2 = 0.0; self.L3 = 0.0
def Update(self, input):
mp = (input.High + input.Low)/2
self.prices = np.append(self.prices, mp)[-4:]
if len(self.prices) <= 1:
self.L0 = mp; self.L1 = mp; self.L2 = mp; self.L3 = mp;
if len(self.prices) != 4 : return
L01 = self.L0; L11 = self.L1; L21 = self.L2; L31 = self.L3;
g = self.gamma
self.L0 = (1 - g)*mp + g*L01
self.L1 = L01 - g*self.L0 + g*L11
self.L2 = L11 - g*self.L1 + g*L21
self.L3 = L21 - g*self.L2 + g*L31
if len(self.prices) != 4 :
self.Value = mp
return False
self.Value = (self.L0 + (2*self.L1) + 2*(self.L2) + self.L3) / 6
return True
#region imports
from AlgorithmImports import *
#endregion
###################################################
#
# Smart Rolling window
# ========================
# Convenience object to build on RollingWindow functionality
#
# Methods:
# -------------------------
# mySmartWindow.IsRising()
# mySmartWindow.IsFalling()
# mySmartWindow.crossedAboveValue(value)
# mySmartWindow.crossedBelowValue(value)
# mySmartWindow.crossedAbove(otherWindow)
# mySmartWindow.crossedBelow(otherWindow)
# mySmartWindow.IsFlat(decimalPrecision)
# mySmartWindow.hasAtLeastThisMany(value)
#
#
# Author:ekz
###################################################
class SmartRollingWindow():
def __init__(self, windowType, windowLength):
self.window = None
self.winLength = windowLength
if (windowType is "int"):self.window = RollingWindow[int](windowLength)
elif (windowType is "bool"):self.window = RollingWindow[bool](windowLength)
elif (windowType is "float"):self.window = RollingWindow[float](windowLength)
elif (windowType is "TradeBar"):self.window = RollingWindow[TradeBar](windowLength)
def crossedAboveValue(self, value):return (self.window[1] <= value < self.window[0])
def crossedBelowValue(self, value): return (self.window[1] >= value > self.window[0])
def crossedAbove(self, series): return (self.window[1] <= series[1] and self.window[0] > series[0])
def crossedBelow(self, series): return (self.window[1] >= series[1] and self.window[0] < series[0])
def isFlat(self): return (self.window[1] == self.window[0])
def isFalling(self): return (self.window[1] > self.window[0])
def isRising(self): return (self.window[1] < self.window[0])
def Add(self,value):
self.window.Add(value)
def IsReady(self):
return (self.window is not None) and \
(self.window.Count >= self.winLength) ## TODO: just use rw.IsReady?
def __getitem__(self, index):
return self.window[index]#region imports
from AlgorithmImports import *
#endregion
##########################################################################################
## Kalman Crossovers
## ----------------------------------------
## An exploration of Kalman Filter for trend entry signals, in combination with EMAs and
## Laguerre filters, taking positions when crossovers occur.
##
## Inspired by @vladimir's KalmanFilter and Laguerre implementations:
## https://www.quantconnect.com/forum/discussion/12741/kalman-filter-for-bitcoin/p1
## https://www.quantconnect.com/forum/discussion/11788/another-digital-filter-laguerre-filter/p1/comment-34897
##
## Author:ekz
##########################################################################################
from FilterIndicators import *
from SmartRollingWindow import *
from pykalman import KalmanFilter
import numpy as np
class KalmanCrossovers(QCAlgorithm):
# Initialize params, assets, indicators
# ------------------------------------=
def Initialize(self):
self.InitAlgoParams()
self.InitBacktestParams()
self.InitAssets()
self.InitIndicators()
# Set key system parameters. Called from Initialize().
# -----------------------------------------------------
def InitAlgoParams(self):
self.ticker = "ETHUSD"
self.lgrGamma = float(self.GetParameter("lgrGamma"))
self.emaPeriod = int(self.GetParameter("emaPeriod"))
self.kalPeriod = int(self.GetParameter("kalPeriod"))
self.entryMethod = int(self.GetParameter("entryMethod"))
self.exitMethod = int(self.GetParameter("exitMethod"))
self.entrySignalMessage = ""
self.exitSignalMessage = ""
# Set backtest params: dates, cash, etc. Called from Initialize().
# ------------------------------------------------------------------
def InitBacktestParams(self):
self.initCash = 100000 # todo: use this to track buy+hold
self.SetCash(self.initCash)
self.SetStartDate(2020, 1, 1)
#self.SetEndDate(2021, 11, 1)
# Initialize assets: Symbol, broker, ticker, etc. Called from Initialize().
# ------------------------------------------------------------------------------=
def InitAssets(self):
self.SetBrokerageModel(BrokerageName.FTX, AccountType.Margin)
self.crypto = self.AddCrypto(self.ticker, Resolution.Daily).Symbol
self.sma = self.SMA(self.crypto, 20, Resolution.Daily)
self.SetBenchmark(self.ticker)
self.AddRiskManagement(MaximumDrawdownPercentPerSecurity(0.025))
# Initialize indicators. Called from Initialize().
# ------------------------------------------------------
def InitIndicators(self):
## Inquire as to why such a long warmup...
self.SetWarmUp(5*self.kalPeriod, Resolution.Daily)
self.ema = self.EMA(self.crypto, self.emaPeriod, Resolution.Daily)
self.kalFilter = KalmanFilterIndicator(name='Kalman',period=self.kalPeriod, selector=Field.Low)
self.lgrFilter = LaguerreFilterIndicator('Laguerre', self.lgrGamma)
self.RegisterIndicator(self.crypto, self.kalFilter, Resolution.Daily)
self.RegisterIndicator(self.crypto, self.lgrFilter, Resolution.Daily)
## Using a consolidator to schedule entries/exists, in case
## we want to use a custom time frame (otherwise we'd use onData)
self.Consolidate(self.crypto, Resolution.Daily, self.OnConsolidatedBarClose)
## Initialize rolling windows. We'll use these to track filter values
self.kalWindow = SmartRollingWindow("float", 2)
self.emaWindow = SmartRollingWindow("float", 2)
self.lgrWindow = SmartRollingWindow("float", 2)
self.priceWindow = SmartRollingWindow("float", 2)
# Called after every bar close on our primary time frame (eg 30M, 1H, 4H, 1D).
# ----------------------------------------------------------------------------
def OnConsolidatedBarClose(self, bar):
## Update rolling windows, we'lll use these to check for crossovers
self.UpdateRollingWindows()
## If we're done warming up and indicators are ready,
## check for entry/exit signals and act accordingly
if (not self.IsWarmingUp and self.IndicatorsAreReady()):
if(self.EntrySignalFired()):
self.SetHoldings(self.crypto, 1, tag=self.entrySignalMessage)
self.entrySignalMessage = ""
elif(self.ExitSignalFired()):
self.Liquidate(tag=self.exitSignalMessage)
self.exitSignalMessage = ""
self.PlotCharts()
# If exit criteria is met, then set self.entrySignalMessage and return True
# -------------------------------------------------------------------------
def EntrySignalFired(self):
if( not self.Portfolio.Invested and self.Securities[self.crypto].Close <= self.sma.Current.Value):
## Entry 1: EMA crossed above Kalman
if( self.entryMethod == 1) and ( self.emaWindow.crossedAbove(self.kalWindow) ):
self.entrySignalMessage = "ENTRY: EMA x-above Kalman"
return True
## Entry 2: Laguerre crossed above Kalman
elif( self.entryMethod == 2) and ( self.lgrWindow.crossedAbove(self.kalWindow) ):
self.entrySignalMessage = "ENTRY: Laguerre x-above Kalman"
return True
## Entry 3: Price crossed above Kalman
elif( self.entryMethod == 3) and ( self.priceWindow.crossedAbove(self.kalWindow) ):
self.entrySignalMessage = "ENTRY: Price x-above Kalman"
return True
## Entry 4: Price is above Kalman
elif( self.entryMethod == 4) and ( self.priceWindow[0] > self.kalWindow[0]):
self.entrySignalMessage = "ENTRY: Price is above Kalman"
return True
return False
# If exit criteria is met, then set self.exitSignalMessage and return True
# ------------------------------------------------------------------------
def ExitSignalFired(self):
if( self.Portfolio.Invested):
profitpct = round(self.Securities[self.crypto].Holdings.UnrealizedProfitPercent,4)
winlossStr = 'WIN' if (profitpct > 0) else 'LOSS'
winlossStr = str(profitpct) +"% " + winlossStr
## Exit 1: EMA crossed under Kalman
if( self.exitMethod == 1 ) and ( self.emaWindow.crossedBelow(self.kalWindow) ):
self.exitSignalMessage = f"EXIT: Ema x-under Kalman @ {winlossStr}"
return True
## Exit 2: Laguerre crossed under Kalman
elif( self.exitMethod == 2 ) and ( self.lgrWindow.crossedBelow(self.kalWindow) ):
self.exitSignalMessage = f"EXIT: Laguerre x-under Kal @ {winlossStr}"
return True
## Exit 3: Price crossed under Kalman
elif( self.exitMethod == 3 ) and ( self.priceWindow.crossedBelow(self.kalWindow) ):
self.exitSignalMessage = f"EXIT: Price x-under Kalman @ {winlossStr}"
return True
## Exit 4: Price is below Kalman
elif( self.exitMethod == 4 ) and ( self.priceWindow[0] < self.kalWindow[0] ):
self.exitSignalMessage = f"EXIT: Price is under Kalman @ {winlossStr}"
return True
# Update rolling windows (willl need to check for crossovers etc)
# ---------------------------------------------------------------
def UpdateRollingWindows(self):
self.kalWindow.Add(self.kalFilter.Value)
self.emaWindow.Add(self.ema.Current.Value)
self.lgrWindow.Add(self.lgrFilter.Value)
self.priceWindow.Add(self.Securities[self.crypto].Price)
# Check if indicators are ready
# ----------------------------------------
def IndicatorsAreReady(self):
return self.kalFilter.IsReady and self.ema.IsReady and \
self.lgrFilter.IsReady
# Plot Charts
# ----------------------------------------
def PlotCharts(self):
self.Plot("charts", "Price", self.Securities[self.crypto].Price)
self.Plot("charts", "Kalman", self.kalFilter.Value)
self.Plot("charts", "EMA", self.ema.Current.Value)
self.Plot("charts", "Laguerre", float(self.lgrFilter.Value))