Overall Statistics
Total Trades
493
Average Win
3.07%
Average Loss
-1.37%
Compounding Annual Return
50.429%
Drawdown
23.600%
Expectancy
1.487
Net Profit
8033.822%
Sharpe Ratio
2.205
Probabilistic Sharpe Ratio
99.864%
Loss Rate
23%
Win Rate
77%
Profit-Loss Ratio
2.23
Alpha
0.384
Beta
0.341
Annual Standard Deviation
0.193
Annual Variance
0.037
Information Ratio
1.436
Tracking Error
0.212
Treynor Ratio
1.249
Total Fees
$20657.10
from clr import AddReference
AddReference("System")
AddReference("QuantConnect.Common")
AddReference("QuantConnect.Algorithm")
AddReference("QuantConnect.Algorithm.Framework")

from System import *
from QuantConnect import *
from QuantConnect.Algorithm import *
from QuantConnect.Algorithm.Framework import *
from QuantConnect.Algorithm.Framework.Alphas import AlphaModel, Insight, InsightType, InsightDirection

class LongOnlyConstantAlphaCreationModel(AlphaModel):

    def __init__(self, portfolioParametersDict):
        
        self.recalculateAtLaunch = portfolioParametersDict['recalculateAtLaunch']
        self.tradingHour = portfolioParametersDict['tradingHour']
        
        self.insightDirection = InsightDirection.Up # insight direction
        self.securities = [] # list to store securities to consider
        
        self.portfolioValueHigh = 0 # initialize portfolioValueHigh for drawdown calculation
        self.portfolioValueHighInitialized = False # initialize portfolioValueHighInitialized for drawdown calculation
        self.initBenchmarkPrice = 0
        
    def Update(self, algorithm, data):
        
        insights = [] # list to store the new insights to be created
        
        # wait until 9:00 to avoid when alpha runs before universe
        if algorithm.Time < datetime(algorithm.Time.year, algorithm.Time.month, algorithm.Time.day, 9, 00, 00, tzinfo = algorithm.Time.tzinfo):
            return insights
        
        # make sure we only send insights once a day at a specific time
        if algorithm.Time.hour == self.tradingHour or self.recalculateAtLaunch:
            self.recalculateAtLaunch = False
            
            ### plotting ----------------------------------------------------------------------------------------
            
            # simulate buy and hold the benchmark and plot its daily value
            self.UpdateBenchmarkValue(algorithm)
            algorithm.Plot('Strategy Equity', 'SPY', self.benchmarkValue)
        
            currentTotalPortfolioValue = algorithm.Portfolio.TotalPortfolioValue # get current portfolio value
            
            # plot the daily total portfolio exposure %
            totalPortfolioExposure = (algorithm.Portfolio.TotalHoldingsValue / currentTotalPortfolioValue) * 100
            algorithm.Plot('Chart Total Portfolio Exposure %', 'Daily Portfolio Exposure %', totalPortfolioExposure)
            
            # plot the drawdown % from the most recent high
            if not self.portfolioValueHighInitialized:
                self.portfolioHigh = currentTotalPortfolioValue # set initial portfolio value
                self.portfolioValueHighInitialized = True
                
            # update trailing high value of the portfolio
            if self.portfolioValueHigh < currentTotalPortfolioValue:
                self.portfolioValueHigh = currentTotalPortfolioValue
    
            currentDrawdownPercent = ((float(currentTotalPortfolioValue) / float(self.portfolioValueHigh)) - 1.0) * 100
            algorithm.Plot('Chart Drawdown %', 'Drawdown %', currentDrawdownPercent)
        
            ### generate insights ------------------------------------------------------------------------------
            
            # calculate insight expiry time
            insightExpiry = Expiry.EndOfDay(algorithm.Time)
            
            # loop through securities and generate insights
            for security in self.securities:
                # append the insights list with the prediction for each symbol
                insights.append(Insight.Price(security.Symbol, insightExpiry, self.insightDirection))
            
        return insights
        
    def UpdateBenchmarkValue(self, algorithm):
            
        ''' Simulate buy and hold the Benchmark '''
        
        if self.initBenchmarkPrice == 0:
            self.initBenchmarkCash = algorithm.Portfolio.Cash
            self.initBenchmarkPrice = algorithm.Benchmark.Evaluate(algorithm.Time)
            self.benchmarkValue = self.initBenchmarkCash
        else:
            currentBenchmarkPrice = algorithm.Benchmark.Evaluate(algorithm.Time)
            self.benchmarkValue = (currentBenchmarkPrice / self.initBenchmarkPrice) * self.initBenchmarkCash
    
    def OnSecuritiesChanged(self, algorithm, changes):
        
        '''
        Description:
            Event fired each time the we add/remove securities from the data feed
        Args:
            algorithm: The algorithm instance that experienced the change in securities
            changes: The security additions and removals from the algorithm
        '''
        
        # add new securities
        for added in changes.AddedSecurities:
            algorithm.Log('adding symbol: ' + str(added.Symbol))
            self.securities.append(added)

        # remove securities
        for removed in changes.RemovedSecurities:
            if removed in self.securities:
                self.securities.remove(removed)
from clr import AddReference
AddReference("QuantConnect.Common")
AddReference("QuantConnect.Algorithm.Framework")

from QuantConnect import Resolution, Extensions
from QuantConnect.Algorithm.Framework.Alphas import *
from QuantConnect.Algorithm.Framework.Portfolio import *
from itertools import groupby
from datetime import datetime, timedelta

import numpy as np
import pandas as pd
from optimizer import CustomPortfolioOptimizer

class CustomPortfolioOptimizationConstructionModel(PortfolioConstructionModel):

    def __init__(self, emails, emailSubject, riskyAssetsParametersDict, portfolioParametersDict, activateWeightFiltering,
                lookbackNegativeYield, startCrisisYieldValue, cashDict, cashAssetsParametersDict, reallocationParametersDict):
        
        self.emails = emails
        self.emailSubject = emailSubject
        self.riskyAssetsParametersDict = riskyAssetsParametersDict
        self.recalculateAtLaunch = portfolioParametersDict['recalculateAtLaunch']
        self.maxTotalAllocationPercent = portfolioParametersDict['maxTotalAllocationPercent']
        self.recalculatingPeriod = portfolioParametersDict['recalculatingPeriod']
        self.defaultObjectiveFunction = portfolioParametersDict['objectiveFunction']
        self.defaultLookbackOptimization = portfolioParametersDict['lookbackOptimization']
        self.rebalancingPeriod = portfolioParametersDict['rebalancingPeriod']

        self.reallocationParametersDict = reallocationParametersDict

        self.activateWeightFiltering = activateWeightFiltering
        self.lookbackNegativeYield = lookbackNegativeYield
        self.startCrisisYieldValue = startCrisisYieldValue
        
        #self.cashTickers = cashAssetsParametersDict['cashTickers']
        self.cashTickers = cashDict.keys()
        self.cashDict = cashDict
        self.cashAssetsRecalculatingPeriod = cashAssetsParametersDict['recalculatingPeriod']
        self.cashAssetsObjectiveFunction = cashAssetsParametersDict['objectiveFunction']
        self.cashAssetsLookbackOptimization = cashAssetsParametersDict['lookbackOptimization']

        self.yieldSignalCrisis = False
        
        # initialize the optimizer
        self.optimizer = CustomPortfolioOptimizer(minWeight = 0, maxWeight = 1)
            
        # save the lookback periods for all the indicators for history calls
        self.indicatorsLookbackValues = []
        for ticker in riskyAssetsParametersDict.keys():
            if riskyAssetsParametersDict[ticker]['addTicker'][0]:
                self.indicatorsLookbackValues.append( riskyAssetsParametersDict[ticker]['sma'][0] )
                self.indicatorsLookbackValues.extend( riskyAssetsParametersDict[ticker]['macd'][0] )
        
        self.finalAllocationDict = {}
        self.initHoldingsValueDict = {}
        self.insightCollection = InsightCollection()
        
        self.recalculatingTime = None
        self.cashAssetsRecalculatingTime = None
        self.rebalancingTime = None
            
    def CreateTargets(self, algorithm, insights):

        '''
        Description:
            Create portfolio targets from the specified insights
        Args:
            algorithm: The algorithm instance
            insights: The insights to create portfolio targets from
        Returns:
            An enumerable of portfolio targets to be sent to the execution model
        '''
        
        # initialize the first recalculating time
        if self.recalculateAtLaunch:
            # allow to recalculate immediately at launch
            self.recalculatingTime = algorithm.Time
            self.cashAssetsRecalculatingTime = algorithm.Time
            self.rebalancingTime = algorithm.Time
            self.recalculateAtLaunch = False
        elif self.recalculatingTime is None:
            # get next recalculating and rebalancing times
            self.recalculatingTime = self.recalculatingPeriod(algorithm.Time)
            self.cashAssetsRecalculatingTime = algorithm.Time
            self.rebalancingTime = self.recalculatingPeriod(algorithm.Time)
        
        # empty list to store portfolio targets
        targets = []
        
        # check if there is new insights coming
        if len(insights) == 0:
            return targets
            
        # here we get the new insights and add them to our insight collection
        for insight in insights:
            self.insightCollection.Add(insight)
            
        # get insight that haven't expired of each symbol that is still in the universe
        activeInsights = self.insightCollection.GetActiveInsights(algorithm.UtcTime)
        
        # get the last generated active insight for each symbol
        lastActiveInsights = []
        for symbol, g in groupby(activeInsights, lambda x: x.Symbol):
            lastActiveInsights.append(sorted(g, key = lambda x: x.GeneratedTimeUtc)[-1])
        
        # calculate optimal weights for cash assets
        if algorithm.Time >= self.cashAssetsRecalculatingTime and len(self.cashTickers) > 0:
            self.cashAssetsDict = self.DetermineTargetPercent(algorithm, lastActiveInsights, 'cash', self.cashAssetsObjectiveFunction,
                                                                self.cashAssetsLookbackOptimization, self.cashAssetsLookbackOptimization)
            
            # update recalculating time for cashAssetsDict optimization
            self.cashAssetsRecalculatingTime = self.cashAssetsRecalculatingPeriod(algorithm.Time)
    
        # calculate the trigger reallocation: daily exposure deviation from initial weights above the triggerPercent
        if self.reallocationParametersDict['type'] is not None:
            triggerReallocation = self.CalculateTriggerReallocation(algorithm, self.reallocationParametersDict['triggerPercent'],
                                                                                self.reallocationParametersDict['direction'])
        
        # determine target percent for the given insights (check function DetermineTargetPercent for details)
        if (algorithm.Time >= self.recalculatingTime or
        (self.reallocationParametersDict['type'] == 'recalculating' and triggerReallocation)):
            
            if self.reallocationParametersDict['type'] is not None and triggerReallocation:
                objectiveFunction = self.reallocationParametersDict['objectiveFunction']
                lookbackOptimization = self.reallocationParametersDict['lookbackOptimization']
                lookbackHistory = max(lookbackOptimization, max(self.indicatorsLookbackValues))
            else:
                objectiveFunction = self.defaultObjectiveFunction
                lookbackOptimization = self.defaultLookbackOptimization
                lookbackHistory = max(lookbackOptimization, max(self.indicatorsLookbackValues))
                
            self.finalAllocationDict = self.DetermineTargetPercent(algorithm, lastActiveInsights, 'risky',
                                                                    objectiveFunction, lookbackOptimization, lookbackHistory)
            
            # update recalculating time
            self.recalculatingTime = self.recalculatingPeriod(algorithm.Time)
        
        elif ((self.rebalancingPeriod is not None and algorithm.Time >= self.rebalancingTime)
        or (self.reallocationParametersDict['type'] == 'rebalancing' and triggerReallocation)):
            algorithm.Log('rebalancing portfolio back to optimal weights for the period')
            for insight, weight in self.finalAllocationDict.items():
                if not algorithm.Portfolio[insight.Symbol].Invested:
                    self.finalAllocationDict[insight] = 0
                    
        else:
            return targets

        if not self.finalAllocationDict:
            return targets

        # refactor weights to make sure we only use maxTotalAllocationPercent
        for insight, weight in self.finalAllocationDict.items():
            self.finalAllocationDict[insight] = self.finalAllocationDict[insight] * self.maxTotalAllocationPercent
        
        # send email notification with final weights for the period
        infoLog = {insight.Symbol.Value: weight for insight, weight in self.finalAllocationDict.items()}
        algorithm.Log('refactored optimal weights for the period: ' + str(infoLog))
        for email in self.emails:
            algorithm.Notify.Email(email, self.emailSubject + ' Portfolio Optimization Strategy - Portfolio Weights Next Period', str(infoLog))
        
        # loop through insights and send portfolio targets
        for insight in self.finalAllocationDict:
            target = PortfolioTarget.Percent(algorithm, insight.Symbol, self.finalAllocationDict[insight])
            targets.append(target)
            
            algorithm.Plot('Chart Optimal Weights %', insight.Symbol.Value, float(self.finalAllocationDict[insight]))
        
        if self.rebalancingPeriod is not None:
            # update rebalancing time
            self.rebalancingTime = algorithm.Time + self.rebalancingPeriod
        
        return targets
        
    def CalculateTriggerReallocation(self, algorithm, triggerPercent, direction):
            
        ''' Calculate the daily exposure deviation for each holding '''
        
        triggerReallocation = False
        
        for insight, weight in self.finalAllocationDict.items():
            if weight == 0:
                continue
            
            # calculate daily deviation between current exposure and initial optimal weight for each holding
            portfolioValue = algorithm.Portfolio.TotalPortfolioValue
            currentAllocation = algorithm.Portfolio[insight.Symbol].HoldingsValue / portfolioValue
            exposureDeviation = currentAllocation - weight
            
            # check if the current deviation is above/below/both our bands
            if direction == 'above':
                triggerCheck = exposureDeviation > triggerPercent
            elif direction == 'below':
                triggerCheck = exposureDeviation < triggerPercent
            else:
                triggerCheck = abs(exposureDeviation) > triggerPercent
            
            # trigger reallocation when needed
            if triggerCheck:
                algorithm.Log('triggering reallocation: ' + insight.Symbol.Value + '; exposure deviation: ' + str(exposureDeviation))
                triggerReallocation = True
                break
                
        return triggerReallocation

    def DetermineTargetPercent(self, algorithm, activeInsights, targetAssets, objectiveFunction, lookbackOptimization, lookbackHistory):
        
        '''
        Description:
            Determine the target percent for each insight
        Args:
            algorithm: The algorithm instance
            activeInsights: The active insights to generate a target for
            targetAssets: risky/cash
            objectiveFunction: The objective function for optimization
            lookbackOptimization: The lookback period for the optimization
            lookbackHistory: Number of days for historical data
        '''
        
        # empty dictionary to store portfolio targets by symbol
        result = {}
        
        # create a mapping dictionary with {calculation insight: tradable insight}
        mapInsightsDict = {}
        if targetAssets == 'risky':
            for insight in activeInsights:
                if insight.Symbol.Value in self.riskyAssetsParametersDict.keys():
                    tradableTicker = self.riskyAssetsParametersDict[insight.Symbol.Value]['addTicker'][1]
                    tradableInsight = [insight for insight in activeInsights if insight.Symbol.Value == tradableTicker][0]
                    mapInsightsDict[insight] = tradableInsight
        else:
            for insight in activeInsights:
                if insight.Symbol.Value in self.cashDict.keys():
                    tradableTicker = self.cashDict[insight.Symbol.Value]['addTicker'][1]
                    tradableInsight = [insight for insight in activeInsights if insight.Symbol.Value == tradableTicker][0]
                    mapInsightsDict[insight] = tradableInsight
       
        # calculation symbols with active insights
        if targetAssets == 'risky':
            targetTickers = self.riskyAssetsParametersDict.keys()
        else:
            targetTickers = self.cashDict.keys()
        calculationSymbols = [x.Symbol for x in activeInsights if x.Symbol.Value in targetTickers]
        
        # get historical data for calculationSymbols for the last n trading days
        history = algorithm.History(calculationSymbols, lookbackHistory, Resolution.Daily)
        
        # empty dictionary for calculations
        calculations = {}
        
        # iterate over all symbols and perform calculations
        for symbol in calculationSymbols:
            # check if we have enough historical data, otherwise just skip this security
            if not self.CheckData(algorithm, symbol, history, lookbackHistory):
                algorithm.Log(str(symbol.Value) + ': skipping security due to no/not enough historical data')
                continue
            else:
                # add symbol to calculations
                if targetAssets == 'risky':
                    calculations[symbol] = SymbolData(symbol, self.riskyAssetsParametersDict)
                else:
                    calculations[symbol] = SymbolData(symbol)
                    #calculations[symbol] = SymbolData(symbol, self.cashDict)
                try:
                    # get series of log-returns
                    calculations[symbol].CalculateLogReturnSeries(history, lookbackOptimization)
                    # update technical indicators
                    calculations[symbol].UpdateIndicators(history)
                except:
                    algorithm.Log('returning empty weights for now due to calculations failing for: '
                                    + str(symbol.Value) + '; we will try again at the next iteration')
                    return result
        
        # calculate optimal weights
        optWeights = self.CalculateOptimalWeights(algorithm, calculations, objectiveFunction)
            
        algorithm.Log(targetAssets + ' assets; optimal weights for the period: ' + str(optWeights))
        
        if optWeights is None:
            algorithm.Log('returning empty weights due to optWeights being None; we will try again at the next iteration')
            return result
            
        # Adjust weights for leverage
        interimWeights = self.AdjustWeightsForLeverage(algorithm, calculations, optWeights, targetAssets)
        algorithm.Log('Interim weights for the period adjusted for leverage: ' + str(interimWeights))
        
       
        # if activateWeightFiltering is True, modify optimal weights using specific criteria
        if self.activateWeightFiltering and targetAssets == 'risky':
            finalWeights = self.FilterOptimalWeights(algorithm, calculations, interimWeights)
            algorithm.Log('Final filtered optimal weights for the period: ' + str(finalWeights))
        else:
            finalWeights = interimWeights
        
        #if targetAssets == 'cash':
        #    return finalWeights
        
        # loop through active securities and generate portfolio weights
        for insight in activeInsights:
            if insight.Symbol.Value in finalWeights.keys():
                if insight in mapInsightsDict.keys():
                    # get the tradableInsight
                    tradableInsight = mapInsightsDict[insight]
                    # check if the price of tradableInsight is zero
                    if algorithm.ActiveSecurities[tradableInsight.Symbol].Price == 0:
                        #we trade the original ticker
                        tradableInsight = insight
                    else:
                        # make sure we close positions in original tickers
                        result[insight] = 0
                        # create the portfolio target for the tradable security
                        result[tradableInsight] = insight.Direction * finalWeights[insight.Symbol.Value]
                
                # these are the cash tickers
                else:
                    # check if the price is zero
                    if algorithm.ActiveSecurities[insight.Symbol].Price > 0:
                        # create the portfolio target for the cash security
                        result[insight] = insight.Direction * finalWeights[insight.Symbol.Value]
            
        if targetAssets == 'cash':
            return result
        

        # check how much we have allocated so far to risky assets (and their cash tickers)
        totalAllocation = sum(result.values())
        if totalAllocation >= 1:
            totalAllocation = 1
        algorithm.Log('total allocation after weight filtering: ' + str(totalAllocation))
        
        # allocate remaining cash to cashAssetsDict
        cashAllocation = 1 - totalAllocation
        
        #for insight in activeInsights:
         #   if insight.Symbol.Value in self.cashAssetsDict.keys() and algorithm.ActiveSecurities[insight.Symbol].Price > 0:
          #      finalCashAllocation = self.cashAssetsDict[insight.Symbol.Value] * cashAllocation
           #     if insight in result.keys():
            #        result[insight] = result[insight] + finalCashAllocation
             #   else:
              #      result[insight] = finalCashAllocation
               #     algorithm.Log(str(insight.Symbol.Value) + '; adding remaining cash allocation: ' + str(result[insight]))
        
        for insight in activeInsights:
            for cashinsight in self.cashAssetsDict.keys():
                if cashinsight.Symbol == insight.Symbol and algorithm.ActiveSecurities[insight.Symbol].Price > 0:
                    finalCashAllocation = self.cashAssetsDict[cashinsight] * cashAllocation
                    if insight in result.keys():
                        result[insight] = result[insight] + finalCashAllocation
                    else:
                        result[insight] = finalCashAllocation
                        algorithm.Log(str(insight.Symbol.Value) + '; adding remaining cash allocation: ' + str(result[insight]))
                    
        # avoid very small numbers and make them 0
        for insight, weight in result.items():
            if weight <= 1e-10:
                result[insight] = 0
            
        return result
        
    def CalculateOptimalWeights(self, algorithm, calculations, objectiveFunction):
        
        '''
        Description:
            Calculate the individual weights for each symbol that optimize some given objective function
        Args:
            algorithm: The algorithm instance
            calculations: Dictionary containing calculations for each symbol
        '''
                
        # create a dictionary keyed by the symbols in calculations with a pandas.Series as value to create a dataframe of log-returns
        logReturnsDict = { symbol.Value: symbolData.logReturnSeries for symbol, symbolData in calculations.items() }
        logReturnsDf = pd.DataFrame(logReturnsDict)
        listTickers = list(logReturnsDf.columns)
        
        try:
            # portfolio optimizer finds the optimal weights for the given data
            listOptWeights = self.optimizer.Optimize(objectiveFunction, logReturnsDf)
        except:
            algorithm.Log('optimization failed')
            return None
        
        # create dictionary with the optimal weights by symbol
        weights = {listTickers[i]: listOptWeights[i] for i in range(len(listTickers))}
        
        # avoid very small numbers and make them 0
        for ticker, weight in weights.items():
            if weight <= 1e-10:
                weights[ticker] = 0
            
        return weights
    
    def FilterOptimalWeights(self, algorithm, calculations, optWeights):
        
        '''
        Description:
            Filter and modify the optimal weights using a combination of technical indicators
        Args:
            algorithm: The algorithm instance
            calculations: Dictionary containing calculations for each symbol
            optWeights: Dictionary with the optimal weights by symbol
        '''
        
        # check the yield condition -----------------------------------------------------------------
        # get the last six months of historical USTREASURY/YIELD values
        histYield = algorithm.History(['USTREASURY/YIELD'], self.lookbackNegativeYield + 1, Resolution.Daily).loc['USTREASURY/YIELD']
        histYield = histYield.rename(columns = {col: col.replace(' ', '') for col in histYield.columns})
        tenYr = histYield['10yr'] # get the 10-year yield
        threeMo = histYield['3mo'] # get the 3-month yield
        tenYrMinusThreeMo = tenYr - threeMo # calculate the difference between the two
        #algorithm.Plot('Chart Yield %', 'Yield %', float(tenYrMinusThreeMo[-1]))
        #algorithm.Plot('Chart Yield %', 'Zero Line', float(0))
        
        # get the first date when the yield turned negative
        indexNegative = tenYrMinusThreeMo[tenYrMinusThreeMo < 0].head(1).index
        # check if there was actually some negative yield values
        if len(indexNegative) > 0:
            cutOff = indexNegative[0]
            # filter the series for days after that day with negative value
            afterNegative = tenYrMinusThreeMo[tenYrMinusThreeMo.index > cutOff]
            # check if at some point it reached our startCrisisYieldValue
            if len(afterNegative) > 0 and max(afterNegative) > self.startCrisisYieldValue:
                self.yieldSignalCrisis = True
            else:
                self.yieldSignalCrisis = False
        else:
            self.yieldSignalCrisis = False
        # -------------------------------------------------------------------------------------------
        
        # empty dicitonary to store weights
        weights = {}
        
        # loop through calculations and check conditions for weight filtering ------------------------
        for symbol, symbolData in calculations.items():
            if symbolData.SMA.IsReady and symbolData.MACD.IsReady:
                currentPrice = algorithm.ActiveSecurities[symbol].Price

                # check if sma condition is met and act accordingly ----------------------------------
                smaLowerBoundCondition = self.riskyAssetsParametersDict[symbol.Value]['sma'][1][0]
                smaUpperBoundCondition = self.riskyAssetsParametersDict[symbol.Value]['sma'][1][1]
                smaConditionWeight = self.riskyAssetsParametersDict[symbol.Value]['sma'][2]
                
                algorithm.Log(str(symbol.Value)
                + '; current price: ' + str(round(currentPrice, 2))
                + '; SMA: ' + str(round(symbolData.SMA.Current.Value, 2))
                + '; Price vs SMA: ' + str(round((currentPrice / symbolData.SMA.Current.Value) - 1, 2)))
                
                #algorithm.Plot('Chart SMA', symbol.Value + ' - sma', symbolData.SMA.Current.Value)
                #algorithm.Plot('Chart SMA', symbol.Value + ' - price', currentPrice)
                
                if (currentPrice <= symbolData.SMA.Current.Value * (1 + smaLowerBoundCondition)
                or currentPrice >= symbolData.SMA.Current.Value * (1 + smaUpperBoundCondition)):
                    weights[symbol.Value] = min(optWeights[symbol.Value], smaConditionWeight)
                    algorithm.Log(str(symbol.Value)
                    + '; modifying weight due to sma filtering from '
                    + str(optWeights[symbol.Value]) + ' to ' + str(weights[symbol.Value]))
                    
                else:
                    weights[symbol.Value] = optWeights[symbol.Value]
                    
                smaModifiedWeight = weights[symbol.Value]
                
                # check if macd condition is met and act accordingly ----------------------------------
                macdCondition = self.riskyAssetsParametersDict[symbol.Value]['macd'][1]
                macdConditionWeight = self.riskyAssetsParametersDict[symbol.Value]['macd'][2]
                # calculate our macd vs signal score between -1 and 1
                macdMinusSignal = symbolData.MACD.Current.Value - symbolData.MACD.Signal.Current.Value
                macdVsSignalScore = macdMinusSignal / (1 + abs(macdMinusSignal))
                
                algorithm.Log(str(symbol.Value)
                + '; MACD: ' + str(round(symbolData.MACD.Current.Value, 2))
                + '; MACD Signal: ' + str(round(symbolData.MACD.Signal.Current.Value, 2))
                + '; MACD vs Signal Score: ' + str(round(macdVsSignalScore, 2)))
                
                #algorithm.Plot('Chart MACD', symbol.Value + ' - macd', symbolData.MACD.Current.Value)
                #algorithm.Plot('Chart MACD', symbol.Value + ' - signal', symbolData.MACD.Signal.Current.Value)
                
                if macdVsSignalScore <= macdCondition:
                    weights[symbol.Value] = min(smaModifiedWeight, macdConditionWeight)
                    algorithm.Log(str(symbol.Value)
                    + '; modifying weight due to macd filtering from '
                    + str(smaModifiedWeight) + ' to ' + str(weights[symbol.Value]))
                else:
                    weights[symbol.Value] = smaModifiedWeight
                    
                macdModifiedWeight = weights[symbol.Value]
                
                # check if yield condition is met and act accordingly ----------------------------------
                activateYield = self.riskyAssetsParametersDict[symbol.Value]['yield'][0]
                yieldConditionWeight = self.riskyAssetsParametersDict[symbol.Value]['yield'][1]
                
                if self.yieldSignalCrisis and activateYield:
                    weights[symbol.Value] = min(macdModifiedWeight, yieldConditionWeight)
                    algorithm.Log(str(symbol.Value)
                    + '; modifying weight due to yield curve filtering from '
                    + str(macdModifiedWeight) + ' to ' + str(weights[symbol.Value]))
                else:
                    weights[symbol.Value] = macdModifiedWeight
                    
                # allocate to cashTicker the difference between optimal weight and final weight --------
                cashAllocation = optWeights[symbol.Value] - weights[symbol.Value]
                cashTicker = self.riskyAssetsParametersDict[symbol.Value]['cashTicker'][0]
                multiple = self.riskyAssetsParametersDict[symbol.Value]['cashTicker'][1]
                finalCashAllocation = cashAllocation * multiple
                if cashTicker in weights:
                    weights[cashTicker] = weights[cashTicker] + finalCashAllocation
                else:
                    weights[cashTicker] = finalCashAllocation
                
                algorithm.Log(str(symbol.Value) + '; adding remaining cash allocation to '
                + str(cashTicker) + ': ' + str(weights[cashTicker]))
                    
            else:
                weights[symbol.Value] = 0
                cashTicker = self.riskyAssetsParametersDict[symbol.Value]['cashTicker'][0]
                weights[cashTicker] = 0
                algorithm.Log(str(symbol.Value) + '; indicators are not ready so assign zero weight')

        return weights
        
    
    def AdjustWeightsForLeverage(self, algorithm, calculations, optWeights, targetAssets):
        
        '''
        Description:
            Modify the filtered weights for leverage
        Args:
            algorithm: The algorithm instance
            calculations: Dictionary containing calculations for each symbol
            optWeights: Dictionary with the optimal weights by symbol
        '''
        
       
        # empty dictionary to store weights
        weights = {}
        totalweight = 0
        
        # loop through calculations and calculate total leverage weight --------------
        if targetAssets == "risky":
            for symbol, symbolData in calculations.items():
                    totalweight = optWeights[symbol.Value]*self.riskyAssetsParametersDict[symbol.Value]['LeverageFactor'][0] + totalweight
        else:
            for symbol, symbolData in calculations.items():
                totalweight = optWeights[symbol.Value]*self.cashDict[symbol.Value]['LeverageFactor'][0] + totalweight
    
                
        # loop through calculations and check conditions for weight filtering ------------------------
        if targetAssets == "risky":
            for symbol, symbolData in calculations.items():
                if totalweight > 0:
                    weights[symbol.Value] = optWeights[symbol.Value]*self.riskyAssetsParametersDict[symbol.Value]['LeverageFactor'][0]/totalweight
                    algorithm.Log(str(symbol.Value)
                    + '; modifying risky weights due to Leverage from '
                    + str(optWeights[symbol.Value]) + ' to ' + str(weights[symbol.Value]))
                    
                else:
                    weights[symbol.Value] = 0
                    cashTicker = self.riskyAssetsParametersDict[symbol.Value]['cashTicker'][0]
                    weights[cashTicker] = 0
                    algorithm.Log(str(symbol.Value) + '; indicators are not ready so assign zero weight')
        else:
            for symbol, symbolData in calculations.items():
                if totalweight > 0:
                    weights[symbol.Value] = optWeights[symbol.Value]*self.cashDict[symbol.Value]['LeverageFactor'][0]/totalweight
                    algorithm.Log(str(symbol.Value)
                    + '; modifying cash weights due to Leverage from '
                    + str(optWeights[symbol.Value]) + ' to ' + str(weights[symbol.Value]))
                    
                else:
                    weights[symbol.Value] = 0
                    algorithm.Log(str(symbol.Value) + '; indicators are not ready so assign zero weight')
        
        return weights
        
    def CheckData(self, algorithm, symbol, history, lookbackHistory):
        
        ''' Check if the history dataframe is valid '''
        
        if (str(symbol) not in history.index
        or history.loc[str(symbol)].get('close') is None
        or history.loc[str(symbol)].get('close').isna().any()
        or symbol not in algorithm.ActiveSecurities.Keys
        or len(history.loc[str(symbol)]) < lookbackHistory):
            return False
        else:
            return True
        
class SymbolData:
    
    ''' Contain data specific to a symbol required by this model '''
    
    def __init__(self, symbol, riskyAssetsParametersDict = None):
        
        self.Symbol = symbol
        self.logReturnSeries = None
        self.riskyAssetsParametersDict = riskyAssetsParametersDict
        
        if self.riskyAssetsParametersDict is not None:
            smaPeriod = riskyAssetsParametersDict[symbol.Value]['sma'][0]
            self.SMA = SimpleMovingAverage(smaPeriod)
            
            macdFastPeriod = riskyAssetsParametersDict[self.Symbol.Value]['macd'][0][0]
            macdSlowPeriod = riskyAssetsParametersDict[self.Symbol.Value]['macd'][0][1]
            macdSignalPeriod = riskyAssetsParametersDict[self.Symbol.Value]['macd'][0][2]
            self.MACD = MovingAverageConvergenceDivergence(macdFastPeriod, macdSlowPeriod, macdSignalPeriod, MovingAverageType.Exponential)
            self.LeverageFactor = riskyAssetsParametersDict[self.Symbol.Value]['LeverageFactor'][0]
    
    def CalculateLogReturnSeries(self, history, lookbackOptimization):
        
        ''' Calculate the log-returns series for each security '''
        
        tempLogReturnSeries = np.log(1 + history.loc[str(self.Symbol)]['close'].pct_change(periods = 2).dropna()) # 1-day log-returns
        self.logReturnSeries = tempLogReturnSeries[-lookbackOptimization:]
        
    def UpdateIndicators(self, history):
        
        ''' Update the indicators with historical data '''
        
        if self.riskyAssetsParametersDict is not None:
            for index, row in history.loc[str(self.Symbol)].iterrows():
                self.SMA.Update(index, row['close'])
                self.MACD.Update(index, row['close'])
import numpy as np
from scipy.optimize import minimize

class CustomPortfolioOptimizer:
    
    '''
    Description:
        Implementation of a custom optimizer that calculates the weights for each asset to optimize a given objective function
    Details:
        Optimization can be:
            - Maximize Portfolio Sharpe Ratio
            - Maximize Portfolio Sortino Ratio
            - Maximize Portfolio Return
            - Minimize Portfolio Standard Deviation
            - Risk Parity Portfolio
        Constraints:
            - Weights must be between some given boundaries
            - Weights must sum to 1
    '''
    
    def __init__(self, 
                 minWeight = -1,
                 maxWeight = 1):
                     
        '''
        Description:
            Initialize the CustomPortfolioOptimizer
        Args:
            minWeight(float): The lower bound on portfolio weights
            maxWeight(float): The upper bound on portfolio weights
        '''
        
        self.minWeight = minWeight
        self.maxWeight = maxWeight

    def Optimize(self, objFunction, historicalLogReturns):
        
        '''
        Description:
            Perform portfolio optimization using a provided matrix of historical returns and covariance (optional)
        Args:
            objFunction: The objective function to optimize (sharpe, sortino, return, std, riskParity)
            historicalLogReturns: Matrix of historical log-returns where each column represents a security and each row log-returns for the given date/time (size: K x N)
        Returns:
            Array of double with the portfolio weights (size: K x 1)
        '''
        
        # get sample covariance matrix
        covariance = historicalLogReturns.cov()
            
        # get the sample covariance matrix of only negative returns for sortino ratio
        historicalNegativeLogReturns = historicalLogReturns[historicalLogReturns < 0]
        covarianceNegativeReturns = historicalNegativeLogReturns.cov()

        size = historicalLogReturns.columns.size # K x 1
        x0 = np.array(size * [1. / size])
        
        # apply equality constraints
        constraints = ({'type': 'eq', 'fun': lambda weights: self.GetBudgetConstraint(weights)})

        opt = minimize(lambda weights: self.ObjectiveFunction(objFunction, weights, historicalLogReturns,
                                                                covariance, covarianceNegativeReturns),     # Objective function
                        x0,                                                                                 # Initial guess
                        bounds = self.GetBoundaryConditions(size),                                          # Bounds for variables
                        constraints = constraints,                                                          # Constraints definition
                        method = 'SLSQP')                                                                   # Optimization method: Sequential Least Squares Programming
                        
        return opt['x']

    def ObjectiveFunction(self, objFunction, weights, historicalLogReturns, covariance, covarianceNegativeReturns):
        
        '''
        Description:
            Compute the objective function
        Args:
            weights: Portfolio weights
            historicalLogReturns: Matrix of historical log-returns
            covariance: Covariance matrix of historical log-returns
        '''
        
        # calculate the annual return of portfolio
        annualizedPortfolioReturns = np.sum(historicalLogReturns.mean() * 252 * weights)

        # calculate the annual standard deviation of portfolio
        annualizedPortfolioStd = np.sqrt( np.dot(weights.T, np.dot(covariance * 252, weights)) )
        
        annualizedPortfolioNegativeStd = np.sqrt( np.dot(weights.T, np.dot(covarianceNegativeReturns * 252, weights)) )
        
        if annualizedPortfolioStd == 0 or annualizedPortfolioNegativeStd == 0:
            raise ValueError(f'CustomPortfolioOptimizer.ObjectiveFunction: annualizedPortfolioStd/annualizedPortfolioNegativeStd cannot be zero. Weights: {weights}')
        
        # calculate annual sharpe ratio of portfolio
        annualizedPortfolioSharpeRatio = (annualizedPortfolioReturns / annualizedPortfolioStd)
        
        # calculate annual sortino ratio of portfolio
        annualizedPortfolioSortinoRatio = (annualizedPortfolioReturns / annualizedPortfolioNegativeStd)
        
        # Spuni's formulation for risk parity portfolio
        size = historicalLogReturns.columns.size
        assetsRiskBudget = np.array(size * [1. / size])
        portfolioVolatility = np.sqrt( np.dot(weights.T, np.dot(covariance, weights)) )
        x = weights / portfolioVolatility
        riskParity = (np.dot(x.T, np.dot(covariance, x)) / 2) - np.dot(assetsRiskBudget.T, np.log(x))
            
        if objFunction == 'sharpe':
            return -annualizedPortfolioSharpeRatio # convert to negative to be minimized
        elif objFunction == 'sortino':
            return -annualizedPortfolioSortinoRatio # convert to negative to be minimized
        elif objFunction == 'return':
            return -annualizedPortfolioReturns # convert to negative to be minimized
        elif objFunction == 'std':
            return annualizedPortfolioStd
        elif objFunction == 'riskParity':
            return riskParity
        else:
            raise ValueError(f'CustomPortfolioOptimizer.ObjectiveFunction: objFunction input has to be one of sharpe, sortino, return, std or riskParity')

    def GetBoundaryConditions(self, size):
        
        ''' Create the boundary condition for the portfolio weights '''
        
        return tuple((self.minWeight, self.maxWeight) for x in range(size))

    def GetBudgetConstraint(self, weights):
        
        ''' Define a budget constraint: the sum of the weights equal to 1 '''
        
        return np.sum(weights) - 1
from clr import AddReference
AddReference("System")
AddReference("QuantConnect.Common")
AddReference("QuantConnect.Algorithm")
AddReference("QuantConnect.Algorithm.Framework")

from QuantConnect import *
from QuantConnect.Algorithm import *
from QuantConnect.Algorithm.Framework import *
from QuantConnect.Algorithm.Framework.Portfolio import PortfolioTarget
from QuantConnect.Algorithm.Framework.Risk import RiskManagementModel

class ATRTrailingStopRiskManagementModel(RiskManagementModel):

    def __init__(self, emails, emailSubject, riskyAssetsParametersDict = None, recalculatingPeriod = Expiry.EndOfMonth):
        
        self.emails = emails
        self.emailSubject = emailSubject
        self.riskyAssetsParametersDict = riskyAssetsParametersDict
        self.recalculatingPeriod = recalculatingPeriod
        
        # add the relevant keys to the dictionaries for atrPeriod and atrMultiple
        self.recentAtrPeriodByTicker = {}
        self.pastAtrPeriodByTicker = {}
        self.percentRecentAbovePastAtrByTicker = {}
        self.atrMultipleByTicker = {}
        self.emergencyAtrMultipleByTicker = {}
        for ticker in riskyAssetsParametersDict.keys():
            if riskyAssetsParametersDict[ticker]['addTicker'][0] and riskyAssetsParametersDict[ticker]['atrTrailStop'][0]:
                self.recentAtrPeriodByTicker[ticker] = riskyAssetsParametersDict[ticker]['atrTrailStop'][1][0]
                self.pastAtrPeriodByTicker[ticker] = riskyAssetsParametersDict[ticker]['atrTrailStop'][1][1]
                self.percentRecentAbovePastAtrByTicker[ticker] = riskyAssetsParametersDict[ticker]['atrTrailStop'][1][2]
                self.atrMultipleByTicker[ticker] = riskyAssetsParametersDict[ticker]['atrTrailStop'][2]
                self.emergencyAtrMultipleByTicker[ticker] = riskyAssetsParametersDict[ticker]['atrTrailStop'][3]
                
                tradableTicker = riskyAssetsParametersDict[ticker]['addTicker'][1]
                self.recentAtrPeriodByTicker[tradableTicker] = riskyAssetsParametersDict[ticker]['atrTrailStop'][1][0]
                self.pastAtrPeriodByTicker[tradableTicker] = riskyAssetsParametersDict[ticker]['atrTrailStop'][1][1]
                self.percentRecentAbovePastAtrByTicker[tradableTicker] = riskyAssetsParametersDict[ticker]['atrTrailStop'][1][2]
                self.atrMultipleByTicker[tradableTicker] = riskyAssetsParametersDict[ticker]['atrTrailStop'][2]
                self.emergencyAtrMultipleByTicker[tradableTicker] = riskyAssetsParametersDict[ticker]['atrTrailStop'][3]

        self.recalculatingTime = None
        
    def ManageRisk(self, algorithm, targets):
        
        '''
        Description:
            Manages the algorithm's risk at each time step
        Args:
            algorithm: The algorithm instance
            targets: The current portfolio targets to be assessed for risk
        '''
        
        # initialize the first recalculating time
        if self.recalculatingTime is None or algorithm.Time >= self.recalculatingTime:
            self.calculations = {} # dictionary to store calculations for each security
            self.trailingStopTargetBySymbol = {} # dictionary to store stop-loss target by symbol
            
            # get next recalculating time
            self.recalculatingTime = self.recalculatingPeriod(algorithm.Time)
         
        # empty list to store portfolio targets for liquidation
        riskAdjustedTargets = list()
        
        # make sure we only run risk management once a day at a specific time
        if algorithm.Time.hour == 15:
            for security in algorithm.ActiveSecurities.Values:
                # if not invested in the security or trailing stop not activated, make sure the dictionaries are empty
                if (not security.Invested or security.Symbol.Value not in self.recentAtrPeriodByTicker.keys()
                or security.Symbol.Value not in self.pastAtrPeriodByTicker.keys()
                or security.Symbol.Value not in self.percentRecentAbovePastAtrByTicker.keys()
                or security.Symbol.Value not in self.atrMultipleByTicker.keys()
                or security.Symbol.Value not in self.emergencyAtrMultipleByTicker.keys()):
                    self.calculations.pop(security.Symbol, None)
                    self.trailingStopTargetBySymbol.pop(security.Symbol, None)
                    continue
                
                if (security.Symbol in self.calculations
                and self.calculations[security.Symbol].entryPrice != algorithm.ActiveSecurities[security.Symbol].Holdings.AveragePrice):
                    self.calculations.pop(security.Symbol, None)
                    self.trailingStopTargetBySymbol.pop(security.Symbol, None)
                
                # if the security is not already part of calculations we add it to calculations
                if security.Symbol not in self.calculations:
                    history = algorithm.History(security.Symbol, self.pastAtrPeriodByTicker[security.Symbol.Value], Resolution.Daily)
                
                    if not self.CheckHistory(security, history):
                        algorithm.Log('no history found for: ' + str(security.Symbol.Value))
                        continue
                    else:
                        try:
                            # add symbol to calculations
                            # we need to capture the entry price for the stopPercent calculation
                            self.calculations[security.Symbol] = SymbolData(security.Symbol, algorithm.ActiveSecurities[security.Symbol].Holdings.AveragePrice,
                                                                            self.recentAtrPeriodByTicker[security.Symbol.Value],
                                                                            self.pastAtrPeriodByTicker[security.Symbol.Value],
                                                                            self.percentRecentAbovePastAtrByTicker[security.Symbol.Value],
                                                                            self.atrMultipleByTicker[security.Symbol.Value],
                                                                            self.emergencyAtrMultipleByTicker[security.Symbol.Value])
                                                                            
                            self.calculations[security.Symbol].WarmUpIndicators(history)
                        except Exception as e:
                            algorithm.Log('removing from calculations due to exception: ' + str(e))
                            self.calculations.pop(security.Symbol, None)
                            self.trailingStopTargetBySymbol.pop(security.Symbol, None)
                            continue
                        
                if self.calculations[security.Symbol].stopPercent is None:
                    self.calculations.pop(security.Symbol, None)
                    self.trailingStopTargetBySymbol.pop(security.Symbol, None)
                    continue
            
                # check if there is already a trailing stop level or need to create one
                if security.Symbol in self.trailingStopTargetBySymbol:
                    # if current price is already below the trailing stop level, liquidate long position
                    if security.Price < self.trailingStopTargetBySymbol[security.Symbol]:
                        # add portfolio target of zero for the security
                        riskAdjustedTargets.append(PortfolioTarget(security.Symbol, 0))
                
                        # remove security from dictionaries as the position is closed
                        self.calculations.pop(security.Symbol, None)
                        self.trailingStopTargetBySymbol.pop(security.Symbol, None)

                        infoLog = str(security.Symbol.Value) + '; liquidate position due to trailing stop triggering'
                        algorithm.Log(infoLog)
                        algorithm.Plot('Chart Optimal Weights %', security.Symbol.Value, float(0))
                        
                        # send email notification alerting that trailing stop triggered
                        for email in self.emails:
                            algorithm.Notify.Email(email, self.emailSubject + ' Portfolio Optimization Strategy - Trailing Stop Triggered', str(infoLog))
                    
                    else:
                        # update trailing stop level as max value between current level and (current price * (1 - stopPercent))
                        self.trailingStopTargetBySymbol[security.Symbol] = max(self.trailingStopTargetBySymbol[security.Symbol],
                                                                                security.Price * (1 - self.calculations[security.Symbol].stopPercent))
                else:
                    # get the initial stop level
                    initialStop = self.calculations[security.Symbol].entryPrice * (1 - self.calculations[security.Symbol].stopPercent)
                    self.trailingStopTargetBySymbol[security.Symbol] = initialStop
                    
                    infoLog = (str(security.Symbol.Value)
                    + '; activate trailing stop'
                    + '; recentAverageTrueRange: ' + str(round(self.calculations[security.Symbol].recentAverageTrueRange.Current.Value, 2))
                    + '; pastAverageTrueRange: ' + str(round(self.calculations[security.Symbol].pastAverageTrueRange.Current.Value, 2))
                    + '; entry average holding price: ' + str(round(self.calculations[security.Symbol].entryPrice, 2))
                    + '; initial stop-loss level: ' + str(round(self.trailingStopTargetBySymbol[security.Symbol], 2))
                    + '; current market price: ' + str(round(security.Price, 2)))
                    
                    algorithm.Log(infoLog)
                    
                    # send email notification with information about the initial trailing stop for the period
                    for email in self.emails:
                        algorithm.Notify.Email(email, self.emailSubject + ' Portfolio Optimization Strategy - Activate Trailing Stop', str(infoLog))
                
        return riskAdjustedTargets
      
    def CheckHistory(self, security, history):
        
        ''' Check if the history dataframe is valid '''
        
        if (str(security.Symbol) not in history.index
        or history.loc[str(security.Symbol)].get('open') is None
        or history.loc[str(security.Symbol)].get('open').isna().any()
        or history.loc[str(security.Symbol)].get('high') is None
        or history.loc[str(security.Symbol)].get('high').isna().any()
        or history.loc[str(security.Symbol)].get('low') is None
        or history.loc[str(security.Symbol)].get('low').isna().any()
        or history.loc[str(security.Symbol)].get('close') is None
        or history.loc[str(security.Symbol)].get('close').isna().any()):
            return False
        else:
            return True
        
class SymbolData:
    
    ''' Make all the calculations needed for each symbol '''
    
    def __init__(self, symbol, entryPrice, recentAtrPeriod, pastAtrPeriod, percentRecentAbovePastAtr,
                atrMultiple, emergencyAtrMultiple):
                    
        self.symbol = symbol
        self.entryPrice = entryPrice
        self.recentAverageTrueRange = AverageTrueRange(recentAtrPeriod, MovingAverageType.Exponential)
        self.pastAverageTrueRange = AverageTrueRange(pastAtrPeriod, MovingAverageType.Exponential)
        self.percentRecentAbovePastAtr = percentRecentAbovePastAtr
        self.atrMultiple = atrMultiple
        self.emergencyAtrMultiple = emergencyAtrMultiple
        
    def WarmUpIndicators(self, history):
        
        # get the single index dataframe for the symbol
        symbolHistory = history.loc[str(self.symbol)]
        
        for index, row in symbolHistory.iterrows():
            if 'open' in row and 'high' in row and 'low' in row and 'close' in row:
                bar = TradeBar(index, self.symbol, row['open'], row['high'], row['low'], row['close'], 0)
                self.recentAverageTrueRange.Update(bar)
                self.pastAverageTrueRange.Update(bar)
            else:
                raise Exception('missing some OHLC data for: ' + str(self.symbol.Value))
                
    @property        
    def multipleTrailingStop(self):
        if self.recentAverageTrueRange.IsReady and self.pastAverageTrueRange.IsReady:
            recentAtr = self.recentAverageTrueRange.Current.Value
            pastAtr = self.pastAverageTrueRange.Current.Value
            if recentAtr >= pastAtr * (1 + self.percentRecentAbovePastAtr):
                return self.emergencyAtrMultiple
            else:
                return self.atrMultiple
        else:
            return None
        
    @property        
    def stopPercent(self):
        if self.entryPrice > 0 and self.recentAverageTrueRange.IsReady and self.multipleTrailingStop is not None:
            recentAtr = self.recentAverageTrueRange.Current.Value
            stop = recentAtr * self.multipleTrailingStop # stop value (dollar value)
            return stop / self.entryPrice # stop %
        else:
            return None
from LongOnlyConstantAlphaCreation import LongOnlyConstantAlphaCreationModel
from CustomPortfolioOptimizationConstruction import CustomPortfolioOptimizationConstructionModel
from ATRTrailingStopRiskManagement import ATRTrailingStopRiskManagementModel

from QuantConnect.Python import PythonQuandl

class PortfolioOptimizationFrameworkAlgorithm(QCAlgorithmFramework):
    
    '''
    Trading Logic:
    
    Modules:
        - Universe: Manual Universe of tickers
        - Alpha: Long-only insights are created on a daily basis
        - Portfolio: Optimal weights are calculated using Portfolio Sharpe Ratio, Sortino, Return, Variance or Risk Parity,
            and then modified using a combination of technical indicators and macroeconomic factors.
            Portfolio will be recalculated on selected date rule.https://www.quantconnect.com/terminal/#live-view-tab
        - Execution: Immediate Execution with Market Orders
        - Risk: ATR-based Trailing Stop with a tighter stop that activates when Recent ATR is greater than (Past ATR + buffer)
    '''

    def Initialize(self):
        
        ### USER-DEFINED INPUTS ---------------------------------------------------------------------------------------------------

        self.SetStartDate(2010, 1, 1)   # set start date
        #self.SetStartDate(2020, 9, 1)   # set start date
        #self.SetEndDate(2005, 1, 15)     # set end date
        self.SetCash(100000)           # set strategy cash
        
        # EMAIL NOTIFICATIONS --------------------------------------------------
        
        emails = ['sudhir.holla@gmail.com', 'efb@innoquantivity.com']
        emailSubject = 'koalla.tech'
        
        # UNIVERSE -------------------------------------------------------------
        
        # select tickers to create the Universe and add indicators and parameters for weight filtering
        riskyAssetsParametersDict = {
                                        'SPY':
                                                {'addTicker':
                                                    [True, 'TQQQ'], # [boolean to add/not add the ticker, ticker to actually trade]
                                                'cashTicker':
                                                    ['UVXY', 0.0], # [ticker to allocate remaining cash from this ticker, % (0 to 1) of that cash to use for the ticker]
                                                'sma':
                                                    [200, (-0.10, 0.10), 0], # [period, (lower % threshold, upper % threshold; price vs sma), weight if condition met]
                                                'macd':
                                                    [(231, 567, 168), 0, 0], # [(fast, slow, signal), score macd vs signal (-1 to 1), weight if condition met]
                                                'yield':
                                                    [True, 0], # [boolean to activate the yield curve filtering, weight if condition met]
                                                'atrTrailStop':
                                                    [True, (10, 63, 0.5), 6, 1],
                                                'LeverageFactor':
                                                    [1]}, # [activate, (recentAtrPeriod, pastAtrPeriod, % above), atrMultiple, emergencyAtrMultiple]
                                                   
                                        'TLT':
                                                {'addTicker':
                                                    [True, 'TMF'], # [boolean to add/not add the ticker, ticker to actually trade]
                                                'cashTicker':
                                                    ['GSY', 1], # [ticker to allocate remaining cash from this ticker, % (0 to 1) of that cash to use for the ticker]
                                                'sma':
                                                    [588, (-0.20, 0.20), 0], # [period, (lower % threshold, upper % threshold; price vs sma), weight if condition met]
                                                'macd':
                                                    [(63, 168, 42), 0, 0], # [(fast, slow, signal), score macd vs signal (-1 to 1), weight if condition met]
                                                'yield':
                                                    [False, 0], # [boolean to activate the yield curve filtering, weight if condition met]
                                                'atrTrailStop':
                                                    [True, (10, 63, 0.5), 6, 1],
                                                'LeverageFactor':
                                                    [1]}, # [activate, (recentAtrPeriod, pastAtrPeriod, % above), atrMultiple, emergencyAtrMultiple]
                    
                                        'GLD':
                                                {'addTicker':
                                                    [True, 'DGP'], # [boolean to add/not add the ticker, ticker to actually trade]
                                                'cashTicker':
                                                    ['GSY', 0], # [ticker to allocate remaining cash from this ticker, % (0 to 1) of that cash to use for the ticker]
                                                'sma':
                                                    [84, (-0.09, 0.09), 0], # [period, (lower % threshold, upper % threshold; price vs sma), weight if condition met]
                                                'macd':
                                                    [(63, 168, 42), 0, 0.2], # [(fast, slow, signal), score macd vs signal (-1 to 1), weight if condition met]
                                                'yield':
                                                    [False, 0], # [boolean to activate the yield curve filtering, weight if condition met]
                                                'atrTrailStop':
                                                    [True, (10, 63, 0.5), 6, 1],
                                                'LeverageFactor':
                                                    [1]}, # [activate, (recentAtrPeriod, pastAtrPeriod, % above), atrMultiple, emergencyAtrMultiple]
                                        
                                    }
        
        # PORTFOLIO ------------------------------------------------------------
        
        # parameters for portfolio weights calculations -----------
        #   - recalculateAtLaunch: variable to control if we want to recalculate at launch (True), or we want to wait until next recalculating date (False)
        #   - maxTotalAllocationPercent: select the percentage of the total cash to be invested (e.g. 3 for 300%, 2 for 200%, 1 for 100%, 0.5 for 50%)
        #   - recalculatingPeriod: select the logic for recalculating period (Expiry.EndOfDay, Expiry.EndOfWeek, Expiry.EndOfMonth, Expiry.EndOfQuarter, Expiry.EndOfYear)
        #   - objFunction: select the objective function to optimize the portfolio weights (sharpe, sortino, return, std, riskParity)
        #   - lookbackOptimization: select number of lookback days for optimization
        #   - rebalancingPeriod: select the number of trading days to rebalance back to optimal weights for the period (timedelta(number of trading days) or None to disable)
        #   - tradingHour: select the hour of the day to execute orders (integer between 10 and 16 (note 16 would be a MarketOnOpen order for next day))
        portfolioParametersDict = {'recalculateAtLaunch': False, 'maxTotalAllocationPercent': 1,
                                    'recalculatingPeriod': Expiry.EndOfMonth, 'objectiveFunction': 'std', 'lookbackOptimization': 63,
                                    'rebalancingPeriod': None, 'tradingHour': 11}
        
        # weights filtering ---------------------------------------
        
        activateWeightFiltering = True # activate/deactivate the weights filtering
        
        # yield curve crisis signal:
        # crisis signal will happen when yield curve turnes negative in the last lookbackNegativeYield days,
        # and then in the last lookbackPositiveYield days it receached startCrisisYieldValue
        lookbackNegativeYield = 147 # number of days to lookback for negative values
        startCrisisYieldValue = 0.25 # the yield value above which we apply the yield weight condition (e.g. 0.1 0.1% yield)
        
        # tickers to allocate remaining cash after filtering
        #   - cashTickers: list of tickers (enter [] to just stay in cash)
        #   - the rest of the parameters control the optimization to run on the cash assets tickers to calculate their weights
        cashDict = {
                    'SPY':    {'addTicker': [True, 'TQQQ'], # [boolean to add/not add the ticker, ticker to actually trade]
                               'LeverageFactor': [1]},
                    'TLT':    {'addTicker': [True, 'TMF'], # [boolean to add/not add the ticker, ticker to actually trade]
                               'LeverageFactor': [1]},
                    'GLD':    {'addTicker': [True, 'DGP'], # [boolean to add/not add the ticker, ticker to actually trade]
                               'LeverageFactor': [1]}
                    }           
        
        #cashAssetsParametersDict = {'cashTickers': ['TMF', 'TQQQ', 'DGP'],
        #                            'recalculatingPeriod': Expiry.EndOfWeek,
        #                            'objectiveFunction': 'riskParity', 'lookbackOptimization': 126}
        
        cashAssetsParametersDict = {
                                    'recalculatingPeriod': Expiry.EndOfWeek,
                                    'objectiveFunction': 'riskParity', 'lookbackOptimization': 126
                                    }
        
        # reallocation logic ---------------------------------------
        
        # add parameters for reallocation trigger (daily exposure deviation from initial weights above the triggerPercent)
        #reallocationParametersDict = '{'type': 'rebalancing'/'recalculating'/None, 'triggerPercent': 0.02,
        #                               'direction': 'above'/'below/'both',
        #                               'objectiveFunction': 'sharpe'/'sortino'/'return'/'std'/'riskParity', lookbackOptimization: integer}
        reallocationParametersDict = {'type': 'recalculating', 'triggerPercent': 0.09, 'direction': 'above',
                                        'objectiveFunction': 'std', 'lookbackOptimization': 63}
        
        ### -------------------------------------------------------------------------------------------------------------------------
        
        # add benchmark
        self.SetBenchmark('SPY')
        
        # let's plot the series of treasury yield
        #yieldPlot = Chart('Chart Yield %')
        #yieldPlot.AddSeries(Series('Yield %', SeriesType.Line, '%'))
        #yieldPlot.AddSeries(Series('Zero Line', SeriesType.Line, '%'))
        #self.AddChart(yieldPlot)
        
        # let's plot the series of daily total portfolio exposure %
        portfolioExposurePlot = Chart('Chart Total Portfolio Exposure %')
        portfolioExposurePlot.AddSeries(Series('Daily Portfolio Exposure %', SeriesType.Line, ''))
        self.AddChart(portfolioExposurePlot)
        
        # let's plot the series of drawdown % from the most recent high
        drawdownPlot = Chart('Chart Drawdown %')
        drawdownPlot.AddSeries(Series('Drawdown %', SeriesType.Line, '%'))
        self.AddChart(drawdownPlot)
        
        plottedTickers = []
        # let's plot the series of optimal weights
        optWeightsPlot = Chart('Chart Optimal Weights %')
        for ticker in riskyAssetsParametersDict.keys():
            if riskyAssetsParametersDict[ticker]['addTicker'][0]:
                optWeightsPlot.AddSeries(Series(ticker, SeriesType.Line, '%'))
                plottedTickers.append(ticker)
                
                tradableTicker = riskyAssetsParametersDict[ticker]['addTicker'][1]
                if tradableTicker not in plottedTickers:
                    optWeightsPlot.AddSeries(Series(tradableTicker, SeriesType.Line, '%'))
                    plottedTickers.append(tradableTicker)
                    
                cashTicker = riskyAssetsParametersDict[ticker]['cashTicker'][0]
                if cashTicker not in plottedTickers:
                    optWeightsPlot.AddSeries(Series(cashTicker, SeriesType.Line, '%'))
                    plottedTickers.append(cashTicker)
                    
        # add cashTickers as well
        #for ticker in cashAssetsParametersDict['cashTickers']:
        for ticker in cashDict.keys():
            if ticker not in plottedTickers:
                optWeightsPlot.AddSeries(Series(ticker, SeriesType.Line, '%'))
                
        self.AddChart(optWeightsPlot)
        
        # let's plot the series of sma
        #smaPlot = Chart('Chart SMA')
        #for ticker in riskyAssetsParametersDict.keys():
        #    if riskyAssetsParametersDict[ticker]['addTicker'][0]:
        #        smaPlot.AddSeries(Series(ticker + ' - price', SeriesType.Line, ''))
        #        smaPlot.AddSeries(Series(ticker + ' - sma', SeriesType.Line, ''))
        #        tradableTicker = riskyAssetsParametersDict[ticker]['addTicker'][1]
        #        if tradableTicker != ticker:
        #            smaPlot.AddSeries(Series(tradableTicker + ' - price', SeriesType.Line, ''))
        #            smaPlot.AddSeries(Series(tradableTicker + ' - sma', SeriesType.Line, ''))
        #self.AddChart(smaPlot)
        
        # let's plot the series of macd
        #macdPlot = Chart('Chart MACD')
        #for ticker in riskyAssetsParametersDict.keys():
        #    if riskyAssetsParametersDict[ticker]['addTicker'][0]:
        #        macdPlot.AddSeries(Series(ticker + ' - macd', SeriesType.Line, ''))
        #        macdPlot.AddSeries(Series(ticker + ' - signal', SeriesType.Line, ''))
        #        tradableTicker = riskyAssetsParametersDict[ticker]['addTicker'][1]
        #        if tradableTicker != ticker:
        #            macdPlot.AddSeries(Series(tradableTicker + ' - macd', SeriesType.Line, ''))
        #            macdPlot.AddSeries(Series(tradableTicker + ' - signal', SeriesType.Line, ''))
        #self.AddChart(macdPlot)

        if portfolioParametersDict['maxTotalAllocationPercent'] <= 1:
            # set the brokerage model for slippage and fees
            self.SetBrokerageModel(AlphaStreamsBrokerageModel())
        else:
            # add leverage
            self.UniverseSettings.Leverage = portfolioParametersDict['maxTotalAllocationPercent'] + 1
        
        # set requested data resolution and disable fill forward data
        self.UniverseSettings.Resolution = Resolution.Hour
        
        ### select modules --------------------------------------------------------------------------------------------------------
        
        # Universe Selection ---------------------------------------------------
        
        self.AddData(QuandlTreasuryRates, 'USTREASURY/YIELD', Resolution.Daily)
        
        addedTickers = []
        symbols = []
        # loop through the tickers and create symbols for the universe
        for ticker in riskyAssetsParametersDict.keys():
            if riskyAssetsParametersDict[ticker]['addTicker'][0]:
                symbols.append(Symbol.Create(ticker, SecurityType.Equity, Market.USA))
                addedTickers.append(ticker)
                
                tradableTicker = riskyAssetsParametersDict[ticker]['addTicker'][1]
                if tradableTicker not in addedTickers:
                    symbols.append(Symbol.Create(tradableTicker, SecurityType.Equity, Market.USA))
                    addedTickers.append(tradableTicker)
                    
                cashTicker = riskyAssetsParametersDict[ticker]['cashTicker'][0]
                if cashTicker not in addedTickers:
                    symbols.append(Symbol.Create(cashTicker, SecurityType.Equity, Market.USA))
                    addedTickers.append(cashTicker)
                    
        #for ticker in cashAssetsParametersDict['cashTickers']:
        for ticker in cashDict.keys():
            if cashDict[ticker]['addTicker'][0] :
                if ticker not in addedTickers:
                    symbols.append(Symbol.Create(ticker, SecurityType.Equity, Market.USA))
                    addedTickers.append(ticker)
                
                tradableTicker = cashDict[ticker]['addTicker'][1]
                if tradableTicker not in addedTickers:
                    symbols.append(Symbol.Create(tradableTicker, SecurityType.Equity, Market.USA))
                    addedTickers.append(tradableTicker)
        
        #for ticker in cashDict.keys():    
        #    if ticker not in addedTickers:
        #        symbols.append(Symbol.Create(ticker, SecurityType.Equity, Market.USA))
        
        self.SetUniverseSelection(ManualUniverseSelectionModel(symbols))
                                                                        
        # Alpha Creation -------------------------------------------------------
        self.SetAlpha(LongOnlyConstantAlphaCreationModel(portfolioParametersDict = portfolioParametersDict))
        
        # Portfolio Construction -----------------------------------------------
        self.SetPortfolioConstruction(CustomPortfolioOptimizationConstructionModel(emails = emails,
                                                                                emailSubject = emailSubject,
                                                                                riskyAssetsParametersDict = riskyAssetsParametersDict,
                                                                                portfolioParametersDict = portfolioParametersDict,
                                                                                activateWeightFiltering = activateWeightFiltering,
                                                                                lookbackNegativeYield = lookbackNegativeYield,
                                                                                startCrisisYieldValue = startCrisisYieldValue,
                                                                                cashDict = cashDict,
                                                                                cashAssetsParametersDict = cashAssetsParametersDict,
                                                                                reallocationParametersDict = reallocationParametersDict))
        
        # Execution ------------------------------------------------------------
        self.SetExecution(ImmediateExecutionModel())
        
        # Risk Management ------------------------------------------------------
        self.SetRiskManagement(ATRTrailingStopRiskManagementModel(emails = emails,
                                                                emailSubject = emailSubject,
                                                                riskyAssetsParametersDict = riskyAssetsParametersDict,
                                                                recalculatingPeriod = Expiry.EndOfWeek))
        
class QuandlTreasuryRates(PythonQuandl):
    def __init__(self):
        self.ValueColumnName = 'value'