Overall Statistics
Total Trades
148
Average Win
17.81%
Average Loss
-1.83%
Compounding Annual Return
453.180%
Drawdown
33.800%
Expectancy
3.207
Net Profit
3082.024%
Sharpe Ratio
4.959
Probabilistic Sharpe Ratio
99.447%
Loss Rate
61%
Win Rate
39%
Profit-Loss Ratio
9.73
Alpha
2.301
Beta
0.407
Annual Standard Deviation
0.567
Annual Variance
0.321
Information Ratio
2.477
Tracking Error
0.629
Treynor Ratio
6.91
Total Fees
$45830.80
Estimated Strategy Capacity
$500000.00
Lowest Capacity Asset
BNBUSDT 18N
## https://www.quantconnect.com/forum/discussion/12768/share-kalman-filter-crossovers-for-crypto-and-smart-rollingwindows/p1/comment-38206

from datetime import timedelta
from AlgorithmImports import *
import math
# from Shared.IntradayMomentumIndex importIn
from SmartRollingWindow import *

def truncate(number, decimals=0):
    """
    Returns a value truncated to a specific number of decimal places.
    """
    if not isinstance(decimals, int):
        raise TypeError("decimal places must be an integer.")
    elif decimals < 0:
        raise ValueError("decimal places has to be 0 or more.")
    elif decimals == 0:
        return math.trunc(number)

    factor = 10.0 ** decimals
    return math.trunc(number * factor) / factor

class Asset():
    def __init__(self, algorithm, symbol):
        self.symbol = symbol
        self.algorithm = algorithm
    def InitIndicators(self, slowPeriod, fastPeriod, resolution):
        self.fast = self.algorithm.EMA(self.symbol, fastPeriod, Resolution.Daily)
        self.slow = self.algorithm.EMA(self.symbol, slowPeriod, Resolution.Daily)
        self.slowWindow = SmartRollingWindow('float', 2)
        self.fastWindow = SmartRollingWindow('float', 2)
        self.priceWindow = SmartRollingWindow('float', 2)
    def UpdateAssetWindows(self):
        self.slowWindow.Add(self.slow.Current.Value)
        self.fastWindow.Add(self.fast.Current.Value)
        self.priceWindow.Add(self.algorithm.Securities[self.symbol].Price)
    
    def PrefixWithSymbol(self, str):
        return "{}: {}".format(self.symbol.Value, str)
    def Plot(self, chartName):
        self.algorithm.Plot(chartName, self.PrefixWithSymbol("Price"), self.algorithm.Securities[self.symbol].Price)        
        self.algorithm.Plot(chartName, self.PrefixWithSymbol("Slow"), self.slow.Current.Value)
        self.algorithm.Plot(chartName, self.PrefixWithSymbol("Fast"), self.fast.Current.Value)

class FocusedYellowGreenJellyfish(QCAlgorithm):

    def Initialize(self):
        self.InitAlgoParams()
        self.InitBacktestParams()
        self.InitAssets()
        self.InitIndicators()

    def InitAlgoParams(self):
        self.benchmarkTicker = 'BTCUSDT';
        self.tickers = ["SOLUSDT", "ETHUSDT", "BNBUSDT"]
        self.assets = {}
        self.ticker       = "SOLUSDT"
        self.resolution = Resolution.Minute
        self.slowPeriod = int(self.GetParameter('slowPeriod'))
        self.fastPeriod = int(self.GetParameter('fastPeriod'))
        
    def InitBacktestParams(self):
        self.SetAccountCurrency("USDT")
        self.SetCash(100000)  
        self.SetStartDate(2020, 1, 1)
        
    def InitAssets(self):
        self.SetBrokerageModel(BrokerageName.Binance, AccountType.Cash)
        self.benchmarkSymbol = self.AddCrypto(self.benchmarkTicker, self.resolution).Symbol
        for ticker in self.tickers:
            symbol = self.AddCrypto(ticker, self.resolution).Symbol
            self.assets[ticker] = Asset(self, symbol)
        self.SetBenchmark(self.benchmarkTicker) 
        
    def InitIndicators(self):
        self.SetWarmUp(self.slowPeriod * 2, self.resolution)
        for asset in self.assets.values():
            asset.InitIndicators(self.slowPeriod, self.fastPeriod, self.resolution)        
        self.Consolidate(self.benchmarkSymbol, Resolution.Daily, self.OnConsolidatedBarClose)
        
    def UpdateRollingWindows(self):
        for asset in self.assets.values():
            asset.UpdateAssetWindows()
        
    def ShouldExit(self, asset):
        return asset.priceWindow.isBelow(asset.slowWindow) or asset.slowWindow.isAbove(asset.fastWindow)
    def ShouldEnter(self, asset):
        return asset.fastWindow.isAbove(asset.slowWindow) and asset.priceWindow.isAbove(asset.fastWindow)
    
    def OnEndOfDay(self):
        self.PlotCharts()
        
    def OnConsolidatedBarClose(self, bar):
        self.UpdateRollingWindows()
        for asset in self.assets.values():
            if not asset.slowWindow.IsReady():
                return
            if self.Portfolio[asset.symbol].Invested and self.ShouldExit(asset):
                self.Liquidate(asset.symbol)
            elif not self.Portfolio[asset.symbol].Invested and self.ShouldEnter(asset):
                percent = truncate(1 / len(self.assets.values()), 1)
                cost = self.Portfolio.TotalPortfolioValue * percent
                cash = self.Portfolio.TotalPortfolioValue - self.Portfolio.TotalHoldingsValue
                
                self.Log("Cost: {}, Cash: {}".format(cost, cash))
                if (cost > cash):
                    percent = truncate(cash / self.Portfolio.TotalPortfolioValue, 1)
                self.SetHoldings(asset.symbol, percent);          

    def PlotCharts(self):
        chartName = "Charts"
        # self.Plot('Benchmark', "Benchmark", self.Securities[self.benchmarkSymbol].Price)
        for asset in self.assets.values():
            asset.Plot(chartName)
        
        
###################################################
#
#  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 isAbove(self, series): return (self.window[0] > series[0])
    def isBelow(self, series): return (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]