Overall Statistics
Total Trades
11628
Average Win
0.04%
Average Loss
-0.03%
Compounding Annual Return
21.995%
Drawdown
6.400%
Expectancy
0.108
Net Profit
22.261%
Sharpe Ratio
1.575
Probabilistic Sharpe Ratio
72.245%
Loss Rate
48%
Win Rate
52%
Profit-Loss Ratio
1.12
Alpha
0.149
Beta
0.086
Annual Standard Deviation
0.097
Annual Variance
0.009
Information Ratio
0.635
Tracking Error
0.161
Treynor Ratio
1.77
Total Fees
$29853.86
# QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
# Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from clr import AddReference
AddReference("QuantConnect.Algorithm")
AddReference("QuantConnect.Algorithm.Framework")
AddReference("QuantConnect.Indicators")
AddReference("QuantConnect.Logging")
AddReference("QuantConnect.Common")

from QuantConnect import *
from QuantConnect.Indicators import *
from QuantConnect.Logging import Log
from QuantConnect.Algorithm import *
from QuantConnect.Algorithm.Framework import *
from QuantConnect.Algorithm.Framework.Alphas import *
from datetime import timedelta
from enum import Enum

class RsiAlphaModel(AlphaModel):
    '''Uses Wilder's RSI to create insights.
    Using default settings, a cross over below 30 or above 70 will trigger a new insight.'''

    def __init__(self,
                 period = 14,
                 resolution = Resolution.Daily):
        '''Initializes a new instance of the RsiAlphaModel class
        Args:
            period: The RSI indicator period'''
        self.period = period
        self.resolution = resolution
        self.insightPeriod = Time.Multiply(Extensions.ToTimeSpan(resolution), period)
        self.symbolDataBySymbol ={}
        self.lowcutoff = 30.0
        self.highcutoff = 70.0
        self.buffer = 5.0

        resolutionString = Extensions.GetEnumString(resolution, Resolution)
        self.Name = '{}({},{})'.format(self.__class__.__name__, period, resolutionString)

    def Update(self, algorithm, data):
        '''Updates this alpha model with the latest data from the algorithm.
        This is called each time the algorithm receives data for subscribed securities
        Args:
            algorithm: The algorithm instance
            data: The new data available
        Returns:
            The new insights generated'''
        insights = []
        for symbol, symbolData in self.symbolDataBySymbol.items():
            rsi = symbolData.RSI
            previous_state = symbolData.State
            state = self.GetState(rsi, previous_state)

            if state != previous_state and rsi.IsReady:
                if state == State.TrippedLow:
                    insights.append(Insight.Price(symbol, self.insightPeriod, InsightDirection.Up, 0.01*(self.lowcutoff-rsi.Current.Value)/self.lowcutoff))
                if state == State.TrippedHigh:
                    insights.append(Insight.Price(symbol, self.insightPeriod, InsightDirection.Down, 0.01*(rsi.Current.Value-self.highcutoff)/(100-self.highcutoff)))

            symbolData.State = state

        return insights


    def OnSecuritiesChanged(self, algorithm, changes):
        '''Cleans out old security data and initializes the RSI for any newly added securities.
        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'''

        # clean up data for removed securities
        symbols = [ x.Symbol for x in changes.RemovedSecurities ]
        if len(symbols) > 0:
            for subscription in algorithm.SubscriptionManager.Subscriptions:
                if subscription.Symbol in symbols:
                    self.symbolDataBySymbol.pop(subscription.Symbol, None)
                    subscription.Consolidators.Clear()

        # initialize data for added securities

        addedSymbols = [ x.Symbol for x in changes.AddedSecurities if x.Symbol not in self.symbolDataBySymbol]
        if len(addedSymbols) == 0: return

        history = algorithm.History(addedSymbols, self.period, self.resolution)

        for symbol in addedSymbols:
            rsi = algorithm.RSI(symbol, self.period, MovingAverageType.Wilders, self.resolution)

            if not history.empty:
                ticker = SymbolCache.GetTicker(symbol)

                if ticker not in history.index.levels[0]:
                    Log.Trace(f'RsiAlphaModel.OnSecuritiesChanged: {ticker} not found in history data frame.')
                    continue

                for tuple in history.loc[ticker].itertuples():
                    rsi.Update(tuple.Index, tuple.close)

            self.symbolDataBySymbol[symbol] = SymbolData(symbol, rsi)


    def GetState(self, rsi, previous):
        ''' Determines the new state. This is basically cross-over detection logic that
        includes considerations for bouncing using the configured bounce tolerance.'''
        if rsi.Current.Value > self.highcutoff :
            return State.TrippedHigh
        if rsi.Current.Value < self.lowcutoff :
            return State.TrippedLow
        if previous == State.TrippedLow:
            if rsi.Current.Value > (self.lowcutoff+self.buffer):
                return State.Middle
        if previous == State.TrippedHigh:
            if rsi.Current.Value < (self.highcutoff-self.buffer):
                return State.Middle

        return previous


class SymbolData:
    '''Contains data specific to a symbol required by this model'''
    def __init__(self, symbol, rsi):
        self.Symbol = symbol
        self.RSI = rsi
        self.State = State.Middle


class State(Enum):
    '''Defines the state. This is used to prevent signal spamming and aid in bounce detection.'''
    TrippedLow = 0
    Middle = 1
    TrippedHigh = 2
class PanicToExcuberanceAlphaModel(AlphaModel):
    
    def __init__(self, algorithm,
                 fastPeriod = 5,
                 slowPeriod = 252,
                 resolution = Resolution.Daily):
    
        # Set resolution for EMA Indicator
        self.resolution = resolution
        
        self.algorithm = algorithm
        self.slow = fastPeriod
        self.fast = slowPeriod
        
        # Add custom data from Quandl
        self.yearlyHighs = algorithm.AddData(QuandlData, 'URC/NASDAQ_52W_HI',self.resolution).Symbol
        self.yearlyLows = algorithm.AddData(QuandlData, 'URC/NASDAQ_52W_LOW', self.resolution).Symbol
        
        # Add indicators using custom data
        self.highema = algorithm.EMA(self.yearlyHighs, self.fast, self.resolution)
        self.highmax = algorithm.MAX(self.yearlyHighs, self.slow, self.resolution)
        self.highmin = algorithm.MIN(self.yearlyHighs, self.slow, self.resolution)
        self.lowema = algorithm.EMA(self.yearlyLows, self.fast, self.resolution)
        self.lowmax = algorithm.MAX(self.yearlyLows, self.slow, self.resolution)
        self.lowmin = algorithm.MIN(self.yearlyLows, self.slow, self.resolution)
        
        
        resolutionString = Extensions.GetEnumString(resolution, Resolution)
        self.Name = '{}({},{},{})'.format(self.__class__.__name__, fastPeriod, slowPeriod, resolutionString)
        # Set the prediction period
        self.period = timedelta(days=30)
            
    def Update(self, algorithm, data):
        insights = []
        
        # Return if no data
        if not (data.ContainsKey(self.yearlyHighs) or data.ContainsKey(self.yearlyLows)): return insights
        
        # Return if indicators are not ready
        if not (self.highmin.IsReady or self.highmax.IsReady): return insights
        if not (self.lowmin.IsReady or self.lowmax.IsReady): return insights
        
        # Calculate the numerator of the custom indicator
        highdelta = self.highmax.Current.Value - self.highmin.Current.Value
        lowdelta = self.lowmax.Current.Value - self.lowmin.Current.Value
        
        # Calculate the denominator of the custom indicator
        highscaled = self.highema.Current.Value - self.highmin.Current.Value
        lowscaled = self.lowema.Current.Value - self.lowmin.Current.Value
        
        # Calculate the value of the custom indicator
        highrank = highscaled / highdelta
        lowrank = lowscaled / lowdelta
        
        # Plot the custom indicator
        
        # Emit insight on SPY if the indicator value exceeds 0.5
        if highrank > 0.50:
            insights.append(Insight.Price("SPY", self.period, InsightDirection.Up, 0.01))
        
        # Emit insight on SPY if the indicator value exceeds 0.5
        if lowrank < 0.50:
            insights.append(Insight.Price("SPY", self.period, InsightDirection.Down, 0.01))
        
        return insights

        
class QuandlData(PythonQuandl):
# Custom data Quandl link: https://www.quandl.com/data/URC/NASDAQ_52W_HI-NASDAQ-Number-of-Stocks-Making-52-Week-Highs
    def __init__(self):
    ## Retrieve the data from the the Quandl object, specifying the data field used on Quandl
        self.ValueColumnName = "NUMBERS OF STOCKS"
    
    # More URC market data can be found at Quandl
    # https://www.quandl.com/data/URC-Unicorn-Research-Corporation
class FederalInterestRateAlphaModel(AlphaModel):
    
    def __init__(self, algorithm, insightDuration = 30):
        ## Add Quandl data for the Federal Interest Rate
        self.fedRate = algorithm.AddData(QuandlFedRateColumns, 'FRED/FEDFUNDS', Resolution.Daily).Symbol
        self.insightDuration = TimeSpan.FromDays(insightDuration)
        
    def Update(self, algorithm, data):
        insights = []

        ## Check for all Quandl Symbols in current data Slice
        if not data.ContainsKey(self.fedRate):
            return []
    
        ## The Federal Interest Rate (Federal Funds Rate) is one of the most important elements
        ## of the US economy and any change has profound effects across all markets, but especially
        ## in debt-based securities, currencies, consumer spending, and a negative effect on equities 
        ## whose companies primarily do business in international markets. The data is monthly, so
        ## Insight generation will not occur with a high frequency.
    
        ## Additional Federal Reserve data can be found at Quandl
        ## https://www.quandl.com/data/FRED-Federal-Reserve-Economic-Data
    
        ## Generate Insights here!

        return insights
    
class QuandlFedRateColumns(PythonQuandl):
    
    def __init__(self):
        ## Rename the Quandl object column to the data we want, which is the 'Value' column
        ## of the CSV that our API call returns
        self.ValueColumnName = "Value"
from datetime import timedelta
from QuantConnect.Data.UniverseSelection import * 
from Selection.FundamentalUniverseSelectionModel import FundamentalUniverseSelectionModel
from MaximumDrawdownPercentPerSecurity import MaximumDrawdownPercentPerSecurity
from PanicToExcuberanceAlphaModel import PanicToExcuberanceAlphaModel
from MacdAlphaModel import MacdAlphaModel
from RsiAlphaModel import RsiAlphaModel
from TingoAlphaModel import TingoAlphaModel

class LiquidValueStocks(QCAlgorithm):

    def Initialize(self):
        self.SetStartDate(2015, 8, 17)
        self.SetEndDate(2016, 8, 17)

        # No need to set End Date as the final submission will be tested
        # up until the review date

        # Set $1m Strategy Cash to trade significant AUM
        self.SetCash(1000000)

        # Add a relevant benchmark, with the default being SPY
        self.AddEquity('SPY')
        self.SetBenchmark('SPY')
        
        # On the Average Cross Chart we want 2 series, slow MA and fast MA
        # avgCross = Chart("Rank")
        # avgCross.AddSeries(Series("High Rank", SeriesType.Line, 0))
        # self.AddChart(avgCross)

        # Use the Alpha Streams Brokerage Model, developed in conjunction with
        # funds to model their actual fees, costs, etc.
        # Please do not add any additional reality modelling, such as Slippage, Fees, Buying Power, etc.
        self.SetBrokerageModel(AlphaStreamsBrokerageModel())
        self.UniverseSettings.Resolution = Resolution.Hour
        self.SetSecurityInitializer(lambda x: x.SetMarketPrice(self.GetLastKnownPrice(x)))
        self.AddUniverseSelection(LiquidValueUniverseSelectionModel())
        
        #1. Create and instance of the LongShortEYAlphaModel
        # self.AddAlpha(EmaCrossAlphaModel(resolution=Resolution.Hour))
        self.AddAlpha(PanicToExcuberanceAlphaModel(self, resolution = Resolution.Daily))
        # self.AddAlpha(FederalInterestRateAlphaModel(self))
        self.AddAlpha(MacdAlphaModel(12, 26, 9, MovingAverageType.Simple, Resolution.Daily))
        self.AddAlpha(RsiAlphaModel(10, Resolution.Daily))
        self.AddAlpha(TingoAlphaModel())
        
        self.SetPortfolioConstruction(EqualWeightingPortfolioConstructionModel())
        self.SetRiskManagement(MaximumDrawdownPercentPerSecurity(0.01))
        self.SetExecution(ImmediateExecutionModel())
        self.SetWarmUp(300, Resolution.Hour)
        



class LiquidValueUniverseSelectionModel(FundamentalUniverseSelectionModel):
    
    def __init__(self):
        super().__init__(True, None, None)

    def SelectCoarse(self, algorithm, coarse):
        sortedByDollarVolume = sorted([x for x in coarse if x.HasFundamentalData and x.Volume > 0 and x.Price > 10],
                                     key = lambda x: x.DollarVolume, reverse=True)[:300]

        # If no security has met the QC500 criteria, the universe is unchanged.
        # A new selection will be attempted on the next trading day as self.lastMonth is not updated
        if len(sortedByDollarVolume) == 0:
            return Universe.Unchanged

        # return the symbol objects our sorted collection
        return [f.Symbol for f in sortedByDollarVolume]
        
    def SelectFine(self, algorithm, fine):

                                        
        filtered_fine = [x for x in fine if x.OperationRatios.ROE.ThreeMonths
                                        and x.OperationRatios.ROIC.ThreeMonths
                                        and x.OperationRatios.OperationRevenueGrowth3MonthAvg.ThreeMonths
                                        and x.ValuationRatios.EarningYield
                                        and x.EarningReports.TotalDividendPerShare.ThreeMonths
                                        and x.ValuationRatios.PriceChange1M 
                                        and x.ValuationRatios.BookValuePerShare
                                        and x.ValuationRatios.FCFYield
                                        and x.ValuationRatios.PERatio
                                        and x.CompanyReference.CountryId == "USA"
                                        and x.CompanyReference.PrimaryExchangeID in ["NYS","NAS"]
                                        and (algorithm.Time - x.SecurityReference.IPODate).days > 180
                                        and x.MarketCap > 5e8]
                                        
        sortedByfactor1 = sorted(filtered_fine, key=lambda x: x.OperationRatios.ROIC.OneYear, reverse=True)
        sortedByfactor2 = sorted(filtered_fine, key=lambda x: x.OperationRatios.ROE.OneYear, reverse=True)
        sortedByfactor3 = sorted(filtered_fine, key=lambda x: x.OperationRatios.OperationRevenueGrowth3MonthAvg.OneYear, reverse=True)
        sortedByfactor4 = sorted(filtered_fine, key=lambda x: x.ValuationRatios.EarningYield, reverse=True)

        sortedByfactor5 = sorted(filtered_fine, key=lambda x: x.EarningReports.TotalDividendPerShare.Value, reverse=True)
        sortedByfactor6 = sorted(filtered_fine, key=lambda x: x.ValuationRatios.PriceChange1M, reverse=False)
        sortedByfactor7 = sorted(filtered_fine, key=lambda x: x.ValuationRatios.BookValuePerShare, reverse=True)
        sortedByfactor8 = sorted(filtered_fine, key=lambda x: x.ValuationRatios.FCFYield, reverse=True)
        sortedByfactor9 = sorted(filtered_fine, key=lambda x: x.ValuationRatios.PERatio, reverse=False)
                
        num_stocks = len(filtered_fine)
    
        stock_dict = {}
        
        for i,ele in enumerate(sortedByfactor1):
            rank1 = i
            rank2 = sortedByfactor2.index(ele)
            rank3 = sortedByfactor3.index(ele)
            rank4 = sortedByfactor4.index(ele)
            rank5 = sortedByfactor5.index(ele)
            rank6 = sortedByfactor6.index(ele)
            rank7 = sortedByfactor7.index(ele)
            rank8 = sortedByfactor8.index(ele)
            rank9 = sortedByfactor9.index(ele)
            score = [rank1/num_stocks,
                     rank2/num_stocks,
                     rank9/num_stocks]
            score = sum(score)
            stock_dict[ele] = score
        self.sorted_stock = sorted(stock_dict.items(), key=lambda d:d[1],reverse=False)
        sorted_symbol = [self.sorted_stock[i][0] for i in range(len(self.sorted_stock))]
        
        universe = sorted_symbol[:50] 
        # algorithm.Log(f"Fine update")
        
        # for ff in universe:
            # algorithm.Log(f"{ff.Symbol} {stock_dict[ff]} roe {ff.OperationRatios.ROE.OneYear} roic {ff.OperationRatios.ROIC.OneYear} PErat {ff.ValuationRatios.PERatio}")
        
        if len(universe) == 0:
            return Universe.Unchanged
            
        return [f.Symbol for f in universe]
# QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
# Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

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 MaximumDrawdownPercentPerSecurity(RiskManagementModel):
    '''Provides an implementation of IRiskManagementModel that limits the drawdown per holding to the specified percentage'''

    def __init__(self, maximumDrawdownPercent = 0.05):
        '''Initializes a new instance of the MaximumDrawdownPercentPerSecurity class
        Args:
            maximumDrawdownPercent: The maximum percentage drawdown allowed for any single security holding'''
        self.maximumDrawdownPercent = -abs(maximumDrawdownPercent)

    def ManageRisk(self, algorithm, targets):
        '''Manages the algorithm's risk at each time step
        Args:
            algorithm: The algorithm instance
            targets: The current portfolio targets to be assessed for risk'''
        targets = []
        for kvp in algorithm.Securities:
            security = kvp.Value

            if not security.Invested:
                continue
            vol = security.Volume
            pnl = security.Holdings.UnrealizedProfitPercent
            if pnl < self.maximumDrawdownPercent:
                # liquidate
                targets.append(PortfolioTarget(security.Symbol, 0))

        return targets
# QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
# Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from clr import AddReference
AddReference("QuantConnect.Common")
AddReference("QuantConnect.Algorithm")
AddReference("QuantConnect.Algorithm.Framework")
AddReference("QuantConnect.Indicators")

from QuantConnect import *
from QuantConnect.Indicators import *
from QuantConnect.Algorithm import *
from QuantConnect.Algorithm.Framework import *
from QuantConnect.Algorithm.Framework.Alphas import *
import numpy as np


class EmaCrossAlphaModel(AlphaModel):
    '''Alpha model that uses an EMA cross to create insights'''

    def __init__(self,
                 fastPeriod = 12,
                 slowPeriod = 26,
                 resolution = Resolution.Daily):
        '''Initializes a new instance of the EmaCrossAlphaModel class
        Args:
            fastPeriod: The fast EMA period
            slowPeriod: The slow EMA period'''
        self.fastPeriod = fastPeriod
        self.slowPeriod = slowPeriod
        self.resolution = resolution
        self.predictionInterval = Time.Multiply(Extensions.ToTimeSpan(resolution), fastPeriod)
        self.symbolDataBySymbol = {}

        resolutionString = Extensions.GetEnumString(resolution, Resolution)
        self.Name = '{}({},{},{})'.format(self.__class__.__name__, fastPeriod, slowPeriod, resolutionString)


    def Update(self, algorithm, data):
        '''Updates this alpha model with the latest data from the algorithm.
        This is called each time the algorithm receives data for subscribed securities
        Args:
            algorithm: The algorithm instance
            data: The new data available
        Returns:
            The new insights generated'''
        insights = []
        pos = 0
        neg = 0.0
        for symbol, symbolData in self.symbolDataBySymbol.items():
            if symbolData.Fast.IsReady and symbolData.Slow.IsReady:
                mag = (symbolData.Fast.Current.Value-symbolData.Slow.Current.Value )/\
                np.mean([symbolData.Fast.Current.Value,symbolData.Slow.Current.Value])

                if symbolData.FastIsOverSlow:
                    if symbolData.Slow > symbolData.Fast:
                        neg += -mag

                elif symbolData.SlowIsOverFast:
                    if symbolData.Fast > symbolData.Slow:
                        pos += mag
        tot = pos + neg                
        for symbol, symbolData in self.symbolDataBySymbol.items():
            if symbolData.Fast.IsReady and symbolData.Slow.IsReady:
                mag = (symbolData.Fast.Current.Value-symbolData.Slow.Current.Value )/\
                np.mean([symbolData.Fast.Current.Value,symbolData.Slow.Current.Value])

                if symbolData.FastIsOverSlow:
                    if symbolData.Slow > symbolData.Fast:
                        insights.append(Insight.Price(symbolData.Symbol, 
                        self.predictionInterval, 
                        InsightDirection.Down, 
                        mag,-mag,
                        "MyAlphaModel",-mag/tot))

                elif symbolData.SlowIsOverFast:
                    if symbolData.Fast > symbolData.Slow:
                        insights.append(Insight.Price(symbolData.Symbol, 
                        self.predictionInterval, 
                        InsightDirection.Up,
                        mag,
                        mag,
                        "MyAlphaModel",mag/tot))
                        
            symbolData.FastIsOverSlow = symbolData.Fast > symbolData.Slow

        return insights

    def OnSecuritiesChanged(self, algorithm, changes):
        '''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'''
        for added in changes.AddedSecurities:
            symbolData = self.symbolDataBySymbol.get(added.Symbol)
            if symbolData is None:
                # create fast/slow EMAs
                symbolData = SymbolData(added)
                symbolData.Fast = algorithm.EMA(added.Symbol, self.fastPeriod, self.resolution)
                symbolData.Slow = algorithm.EMA(added.Symbol, self.slowPeriod, self.resolution)
                self.symbolDataBySymbol[added.Symbol] = symbolData
            else:
                # a security that was already initialized was re-added, reset the indicators
                symbolData.Fast.Reset()
                symbolData.Slow.Reset()


class SymbolData:
    '''Contains data specific to a symbol required by this model'''
    def __init__(self, security):
        self.Security = security
        self.Symbol = security.Symbol
        self.Fast = None
        self.Slow = None

        # True if the fast is above the slow, otherwise false.
        # This is used to prevent emitting the same signal repeatedly
        self.FastIsOverSlow = False

    @property
    def SlowIsOverFast(self):
        return not self.FastIsOverSlow
# QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
# Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from clr import AddReference
AddReference("QuantConnect.Algorithm")
AddReference("QuantConnect.Algorithm.Framework")
AddReference("QuantConnect.Indicators")
AddReference("QuantConnect.Logging")
AddReference("QuantConnect.Common")

from QuantConnect.Data.Custom.Tiingo import *
from datetime import datetime, timedelta
import numpy as np


class TingoAlphaModel:
    
    def __init__(self):
        
        # the sample pool of word sentiments
        self.wordSentiment = {
            "bad": -0.5, "good": 0.5, "negative": -0.5, 
            "great": 0.5, "growth": 0.5, "fail": -0.5, 
            "failed": -0.5, "success": 0.5, "nailed": 0.5,
            "beat": 0.5, "missed": -0.5, "profitable": 0.5,
            "beneficial": 0.5, "right": 0.5, "positive": 0.5, 
            "large":0.5, "attractive": 0.5, "sound": 0.5, 
            "excellent": 0.5, "wrong": -0.5, "unproductive": -0.5, 
            "lose": -0.5, "missing": -0.5, "mishandled": -0.5, 
            "un_lucrative": -0.5, "up": 0.5, "down": -0.5,
            "unproductive": -0.5, "poor": -0.5, "wrong": -0.5,
            "worthwhile": 0.5, "lucrative": 0.5, "solid": 0.5
        }
        self.day = -1
        self.custom = {}
        
    
    def Update(self, algorithm, data):
        insights = []
        
        # Run the model daily
        if algorithm.Time.day == self.day:
            return insights
            
        self.day = algorithm.Time.day
        
        
        weights = {}
        
        # Fetch the wordSentiment data for the active securities and trade on any
        for key, security in self.custom.items():
            
            if not data.ContainsKey(security):
                continue
                
            news = data[security]
            if news is None:
                continue
            descriptionWords = news.Description.lower().split(" ")
            # Get the intersection words between sentiment sample pool and news description
            intersection = set(self.wordSentiment.keys()).intersection(descriptionWords)
            # Calculate the score sum of word sentiment
            sentimentSum = sum([self.wordSentiment[i] for i in intersection])

            if sentimentSum > 0:
                weights[security.Underlying] = sentimentSum
        
        # Sort securities by sentiment ranking, 
        count = min(10, len(weights)) 
        if count == 0:
            return insights
            
        # Order the sentiment by value and select the top 10
        sortedbyValue = sorted(weights.items(), key = lambda x:x[1], reverse=True)
        selected = {kv[0]:kv[1] for kv in sortedbyValue[:count]}
        
        # Populate the list of insights with the selected data where the sentiment sign is the direction and its value is the weight
        closeTimeLocal = Expiry.EndOfDay(algorithm.Time)
        for symbol, weight in selected.items():
            insights.append(Insight.Price(symbol, closeTimeLocal, InsightDirection.Up, weight))
            
        return insights
        
        
    def OnSecuritiesChanged(self, algorithm, changes):
        for security in changes.AddedSecurities:
            # Tiingo's News is for US Equities
            if security.Type == SecurityType.Equity:
                self.custom[security.Symbol] = (algorithm.AddData(TiingoNews, security.Symbol).Symbol)
        for security in changes.RemovedSecurities:
            # Tiingo's News is for US Equities
            if security.Type == SecurityType.Equity and security.Symbol in self.custom:
                data = self.custom.pop(security.Symbol,None)
# QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
# Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from clr import AddReference
AddReference("QuantConnect.Common")
AddReference("QuantConnect.Algorithm")
AddReference("QuantConnect.Algorithm.Framework")
AddReference("QuantConnect.Indicators")

from QuantConnect import *
from QuantConnect.Indicators import *
from QuantConnect.Algorithm import *
from QuantConnect.Algorithm.Framework import *
from QuantConnect.Algorithm.Framework.Alphas import *


class MacdAlphaModel(AlphaModel):
    '''Defines a custom alpha model that uses MACD crossovers. The MACD signal line
    is used to generate up/down insights if it's stronger than the bounce threshold.
    If the MACD signal is within the bounce threshold then a flat price insight is returned.'''

    def __init__(self,
                 fastPeriod = 12,
                 slowPeriod = 26,
                 signalPeriod = 9,
                 movingAverageType = MovingAverageType.Exponential,
                 resolution = Resolution.Daily):
        ''' Initializes a new instance of the MacdAlphaModel class
        Args:
            fastPeriod: The MACD fast period
            slowPeriod: The MACD slow period</param>
            signalPeriod: The smoothing period for the MACD signal
            movingAverageType: The type of moving average to use in the MACD'''
        self.fastPeriod = fastPeriod
        self.slowPeriod = slowPeriod
        self.signalPeriod = signalPeriod
        self.movingAverageType = movingAverageType
        self.resolution = resolution
        self.insightPeriod = Time.Multiply(Extensions.ToTimeSpan(resolution), fastPeriod)
        self.bounceThresholdPercent = 0.01
        self.symbolData = {}

        resolutionString = Extensions.GetEnumString(resolution, Resolution)
        movingAverageTypeString = Extensions.GetEnumString(movingAverageType, MovingAverageType)
        self.Name = '{}({},{},{},{},{})'.format(self.__class__.__name__, fastPeriod, slowPeriod, signalPeriod, movingAverageTypeString, resolutionString)


    def Update(self, algorithm, data):
        ''' Determines an insight for each security based on it's current MACD signal
        Args:
            algorithm: The algorithm instance
            data: The new data available
        Returns:
            The new insights generated'''
        insights = []

        for key, sd in self.symbolData.items():
            if sd.Security.Price == 0:
                continue

            direction = InsightDirection.Flat
            normalized_signal = sd.MACD.Signal.Current.Value / sd.Security.Price

            if normalized_signal > self.bounceThresholdPercent:
                direction = InsightDirection.Up
            elif normalized_signal < -self.bounceThresholdPercent:
                direction = InsightDirection.Down

            # ignore signal for same direction as previous signal
            if direction == sd.PreviousDirection:
                continue

            insight = Insight.Price(sd.Security.Symbol, self.insightPeriod, direction,normalized_signal)
            sd.PreviousDirection = insight.Direction
            insights.append(insight)

        return insights


    def OnSecuritiesChanged(self, algorithm, changes):
        '''Event fired each time the we add/remove securities from the data feed.
        This initializes the MACD for each added security and cleans up the indicator for each removed security.
        Args:
            algorithm: The algorithm instance that experienced the change in securities
            changes: The security additions and removals from the algorithm'''
        for added in changes.AddedSecurities:
            self.symbolData[added.Symbol] = SymbolData(algorithm, added, self.fastPeriod, self.slowPeriod, self.signalPeriod, self.movingAverageType, self.resolution)

        for removed in changes.RemovedSecurities:
            data = self.symbolData.pop(removed.Symbol, None)
            if data is not None:
                # clean up our consolidator
                algorithm.SubscriptionManager.RemoveConsolidator(removed.Symbol, data.Consolidator)

class SymbolData:
    def __init__(self, algorithm, security, fastPeriod, slowPeriod, signalPeriod, movingAverageType, resolution):
        self.Security = security
        self.MACD = MovingAverageConvergenceDivergence(fastPeriod, slowPeriod, signalPeriod, movingAverageType)

        self.Consolidator = algorithm.ResolveConsolidator(security.Symbol, resolution)
        algorithm.RegisterIndicator(security.Symbol, self.MACD, self.Consolidator)

        self.PreviousDirection = None