Overall Statistics
Total Trades
1862
Average Win
0.16%
Average Loss
-0.11%
Compounding Annual Return
-2.446%
Drawdown
22.400%
Expectancy
-0.113
Net Profit
-11.664%
Sharpe Ratio
-0.145
Probabilistic Sharpe Ratio
0.097%
Loss Rate
63%
Win Rate
37%
Profit-Loss Ratio
1.41
Alpha
-0.006
Beta
-0.063
Annual Standard Deviation
0.09
Annual Variance
0.008
Information Ratio
-0.625
Tracking Error
0.189
Treynor Ratio
0.208
Total Fees
$1913.26
Estimated Strategy Capacity
$41000000.00
Lowest Capacity Asset
MELI TV0AIPE7984L
from collections import deque
from AlgorithmImports import *
from datetime import timedelta, time
import pandas as pd
import numpy as np


class BollBands(QCAlgorithm):
    #initialize method
    def Initialize(self):
        self.SetStartDate(2015, 10, 7)
        self.SetEndDate(2020, 10, 7)# Set Start Date
        self.SetCash(100000)  # Set Strategy Cash
        self.SetBenchmark('SPY')
        self.UniverseSettings.Resolution = Resolution.Daily
        self.SetWarmUp(timedelta(days=7))

        self.SetExecution(ImmediateExecutionModel())
        self.AddUniverse(self.CoarseUniverse, self.FineUniverse)
        self.SetPortfolioConstruction(EqualWeightingPortfolioConstructionModel())
        self.AddAlpha(AlphaBollingerBands())

        #used for rebalancing, and to select how many stocks goes to the coarse and fine.
        self.lastMonth = -1
        self.coarse_filter = 100
        self.fine_filter = 10

        self.vol_history = 120

    #Plotting standard variables
    def OnEndOfDay(self):
        self.Plot("Positions", "Num", len([x.Symbol for x in self.Portfolio.Values if self.Portfolio[x.Symbol].Invested]))
        self.Plot(f"Margin", "Used", self.Portfolio.TotalMarginUsed)
        self.Plot(f"Margin", "Remaining", self.Portfolio.MarginRemaining)
        self.Plot(f"Cash", "Remaining", self.Portfolio.Cash)
        self.Plot(f"Symbols", "Symbols", len(symbols_in_universe))

    def CoarseUniverse(self, coarse):
        #Rebalance function, once a month
        if self.Time.month == self.lastMonth:
            return Universe.Unchanged
        self.lastMonth = self.Time.month

        #sorting by stocks over 10 bucks, and that has fundamental data
        selected = sorted([x for x in coarse if x.Price > 10 and x.HasFundamentalData], key = lambda x: x.DollarVolume, reverse=True)

        return [x.Symbol for x in selected[:self.coarse_filter]]

    def FineUniverse(self, fine):

        #make a list that only contains the symbols
        filtered_fine = [x.Symbol for x in fine]

        #implement the methods that is described 
        stocks_by_vol = self.SortVolatility(filtered_fine, 360, Resolution.Daily)

        stocks_by_vol_keys = self.get_keys(stocks_by_vol)

        return [x for x in filtered_fine if str(x) in stocks_by_vol_keys[:self.fine_filter]]


    def SortVolatility(self, filtered_fine, lenght, resolution):
        #method that calculates the volatility with standard deviation, and returns it as a dict 
        history = self.History(filtered_fine, lenght, resolution)
        prices = history.drop_duplicates().close.unstack(level =0)
        vol = np.std(prices)
        vol_to_dict = vol.to_dict()
        rangeret = sorted(vol_to_dict, key = vol_to_dict.get, reverse = True)
        return {symbol: rank for rank, symbol in enumerate(rangeret, 1)}

    def get_keys(self, dic):
        #Method to get the 
        historie = {key:dic.get(key, 0) for key in set(dic)}
        historie = sorted(historie.items(), key = lambda x: x[1], reverse = False)
    
        listoflist = []

        for tuple1 in historie:
            list1 = list(tuple1)
            i = list1.pop(0)
            listoflist.append(i)
        
        return listoflist


class AlphaBollingerBands(AlphaModel):
    def __init__(self, 
                period = 10, 
                deviation = 2, 
                movingAverageType = MovingAverageType.DoubleExponential, 
                resolution = Resolution.Daily):
                    
        self.period = period
        self.deviation = deviation
        self.movingAverageType = movingAverageType
        self.resolution = resolution
        self.insightPeriode = Time.Multiply(Extensions.ToTimeSpan(resolution), period)
        self.symbolDataBySymbol = {}
        global symbols_in_universe
        symbols_in_universe = self.symbolDataBySymbol
        
        self.days = 10
    
    def Update(self, algorithm, data):
        
        if not self.days == 10:
            self.days += 1
            return []
        else:
            self.days = 0
            
        insights = []

        for symbol, symbolDataBySymbol in self.symbolDataBySymbol.items():

            direction = InsightDirection.Flat

            price = symbolDataBySymbol.Security.Price
            lower = symbolDataBySymbol.Bollinger.LowerBand.Current.Value
            upper = symbolDataBySymbol.Bollinger.UpperBand.Current.Value
            middle = symbolDataBySymbol.Bollinger.MiddleBand.Current.Value

            #implemeter "previous state" for at tjekke hvilken tilstand at den sidst var i
            if symbolDataBySymbol.Security.Invested:
                if algorithm.Portfolio[symbol.Symbol].IsLong:
                    if price >= middle:
                        direction = InsightDirection.Flat
                    else:
                        direction = InsightDirection.Up

                elif algorithm.Portfolio[symbol.Symbol].IsShort:
                    if price <= middle:
                        direction = InsightDirection.Flat
                    else:
                        direction = InsightDirection.Down

                    

            elif not symbolDataBySymbol.Security.Invested:
                if price <= lower:
                    direction = InsightDirection.Up
                elif price >= upper:
                    direction = InsightDirection.Down
                else:
                    direction = InsightDirection.Flat

            if direction == symbolDataBySymbol.PreviousDirection:
                continue

            insight = Insight.Price(symbolDataBySymbol.Security.Symbol, self.insightPeriode, direction)
            symbolDataBySymbol.PreviousDirections = insight.Direction
            insights.append(insight)
        
        return insights

    def OnSecuritiesChanged(self, algorithm, changes):

        for symbol in changes.AddedSecurities:
            if symbol not in self.symbolDataBySymbol:
                symbol_data = SymbolData(symbol)
                symbol_data.RegisterIndicatorBollinger(algorithm, self.period, self.deviation, self.movingAverageType, self.resolution)
                
                history = algorithm.History([symbol.Symbol], self.period, self.resolution)

                if not symbol_data.Bollinger.IsReady:
                    symbol_data.WarmUpIndicators(history, symbol)
                
                self.symbolDataBySymbol[symbol] = symbol_data

        
        for removed in changes.RemovedSecurities:
            data = self.symbolDataBySymbol.pop(removed.Symbol, None)
            if data is not None:
                SymbolData.RemoveConsolidators(algorithm)

        

class SymbolData:


    def __init__(self, symbol):
        self.Security = symbol

    def RegisterIndicatorBollinger(self, algorithm, period, deviation, movingAverageType, resolution):
        self.period = period
        self.deviation = deviation
        self.movingAverageType = movingAverageType
        self.Consolidator = None
        self.Bollinger = BollingerBands(self.period, deviation, movingAverageType)
        self.Consolidator = algorithm.ResolveConsolidator(self.Security.Symbol, resolution)
        algorithm.RegisterIndicator(self.Security.Symbol, self.Bollinger, self.Consolidator)
        self.PreviousDirection = None

    def RemoveConsolidators(self, algorithm):
        if self.Consolidator is not None:
            algorithm.SubscriptionManager.RemoveConsolidator(self.Security, self.Consolidator)

    def WarmUpIndicators(self, history, symbol):
        for time, row in history.loc[symbol.Symbol].iterrows():
            self.Bollinger.Update(time, row['close'])