Overall Statistics
Total Orders
10237
Average Win
0.02%
Average Loss
-0.02%
Compounding Annual Return
34.970%
Drawdown
18.400%
Expectancy
0.237
Start Equity
1000000
End Equity
1323313.34
Net Profit
32.331%
Sharpe Ratio
1.454
Sortino Ratio
1.594
Probabilistic Sharpe Ratio
65.071%
Loss Rate
40%
Win Rate
60%
Profit-Loss Ratio
1.06
Alpha
0.164
Beta
0.483
Annual Standard Deviation
0.166
Annual Variance
0.028
Information Ratio
0.459
Tracking Error
0.175
Treynor Ratio
0.501
Total Fees
$10715.90
Estimated Strategy Capacity
$3600000.00
Lowest Capacity Asset
MYOK W546JDIWY3XH
Portfolio Turnover
3.72%
# 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 AlgorithmImports import *
import pandas as pd
import functools
import collections
import operator


class EqualWeightingPortfolioConstructionModel(PortfolioConstructionModel):
    '''Provides an implementation of IPortfolioConstructionModel that gives equal weighting to all securities.
    The target percent holdings of each security is 1/N where N is the number of securities.
    For insights of direction InsightDirection.UP, long targets are returned and
    for insights of direction InsightDirection.DOWN, short targets are returned.'''

    def __init__(self, rebalance = Resolution.DAILY, portfolio_bias = PortfolioBias.LONG_SHORT):
        '''Initialize a new instance of EqualWeightingPortfolioConstructionModel
        Args:
            rebalance: Rebalancing parameter. If it is a timedelta, date rules or Resolution, it will be converted into a function.
                              If None will be ignored.
                              The function returns the next expected rebalance time for a given algorithm UTC DateTime.
                              The function returns null if unknown, in which case the function will be called again in the
                              next loop. Returning current time will trigger rebalance.
            portfolio_bias: Specifies the bias of the portfolio (Short, Long/Short, Long)'''
        super().__init__()
        self.portfolio_bias = portfolio_bias

        # If the argument is an instance of Resolution or Timedelta
        # Redefine rebalancing_func
        rebalancing_func = rebalance
        if isinstance(rebalance, int):
            rebalance = Extensions.to_time_span(rebalance)
        if isinstance(rebalance, timedelta):
            rebalancing_func = lambda dt: dt + rebalance
        if rebalancing_func:
            self.set_rebalancing_func(rebalancing_func)

    def determine_target_percent(self, active_insights):
        '''Will determine the target percent for each insight
        Args:
            active_insights: The active insights to generate a target for'''
        result = {}

        # give equal weighting to each security
        count = sum(x.direction != InsightDirection.FLAT and self.respect_portfolio_bias(x) for x in active_insights)
        percent = 0 if count == 0 else 1.0 / count
        for insight in active_insights:
            result[insight] = (insight.direction if self.respect_portfolio_bias(insight) else InsightDirection.FLAT) * percent
        return result

    def respect_portfolio_bias(self, insight):
        '''Method that will determine if a given insight respects the portfolio bias
        Args:
            insight: The insight to create a target for
        '''
        return self.portfolio_bias == PortfolioBias.LONG_SHORT or insight.direction == self.portfolio_bias


class MLP_PortfolioConstructionModel(EqualWeightingPortfolioConstructionModel):
    '''Provides an implementation of IPortfolioConstructionModel that generates percent targets based on the
    Insight.WEIGHT. The target percent holdings of each Symbol is given by the Insight.WEIGHT from the last
    active Insight for that symbol.
    For insights of direction InsightDirection.UP, long targets are returned and for insights of direction
    InsightDirection.DOWN, short targets are returned.
    If the sum of all the last active Insight per symbol is bigger than 1, it will factor down each target
    percent holdings proportionally so the sum is 1.
    It will ignore Insight that have no Insight.WEIGHT value.'''

    def __init__(self, algorithm, model = None, rebalance = Resolution.DAILY, portfolio_bias = PortfolioBias.LONG_SHORT):
        '''Initialize a new instance of InsightWeightingPortfolioConstructionModel
        Args:
            rebalance: Rebalancing parameter. If it is a timedelta, date rules or Resolution, it will be converted into a function.
                              If None will be ignored.
                              The function returns the next expected rebalance time for a given algorithm UTC DateTime.
                              The function returns null if unknown, in which case the function will be called again in the
                              next loop. Returning current time will trigger rebalance.
            portfolio_bias: Specifies the bias of the portfolio (Short, Long/Short, Long)'''
        super().__init__(rebalance, portfolio_bias)
        self.algorithm = algorithm

    def should_create_target_for_insight(self, insight):
        '''Method that will determine if the portfolio construction model should create a
        target for this insight
        Args:
            insight: The insight to create a target for'''
        # Ignore insights that don't have Weight value
        return insight.weight is not None

    def determine_target_percent(self, activeInsights: List[Insight])-> Dict[Insight, float]:
        '''Will determine the target percent for each insight
        Args:
            activeInsights: The active insights to generate a target for'''
        # 1. Temp solution: Sum up weights from the mulitple insights
        Features = {}
        for insight in activeInsights:
            if insight.symbol not in Features.keys():
                Features[insight.symbol] = insight.weight
            else:
                Features[insight.symbol] = Features[insight.symbol] + insight.weight
        
        # 2. Compute long/ short weight sum to adjust long short ratio
        p_sum = 0
        n_sum = 0
        for symbol, weight in Features.items():
            if weight > 0:
                p_sum += weight
            elif weight < 0:
                n_sum += np.abs(weight)

        # 3. return results
        result = {}
        emitted_symbol = []
        weight_sums = sum([np.abs(weight) for weight in Features.values()])
        weight_factor = 1.0
        if weight_sums > 1:
            weight_factor = 1 / weight_sums

        for insight in activeInsights:
            if insight.weight * Features[insight.symbol] > 0:
                if insight.symbol not in emitted_symbol:
                    emitted_symbol.append(insight.symbol)
                    result[insight] = Features[insight.symbol] * weight_factor
                    
        return result

    def get_value(self, insight):
        '''Method that will determine which member will be used to compute the weights and gets its value
        Args:
            insight: The insight to create a target for
        Returns:
            The value of the selected insight member'''
        return abs(insight.weight)


    # Multi-Alpha: 
    def get_target_insights(self) -> List[Insight]:
        return list(self.algorithm.insights.get_active_insights(self.algorithm.utc_time))

#region imports
from AlgorithmImports import *
from indicators import *
from collections import deque
import numpy as np
import scipy as sp
#endregion

class TSZscore_VwapReversion(AlphaModel):

    def __init__(self):
        self.period = 20
        self.securities_list = []
        self.day = -1
        self.historical_VwapReversion_by_symbol = {}


    def on_securities_changed(self, algorithm: QCAlgorithm, changes: SecurityChanges) -> None:
        # Register each security in the universe
        for security in changes.added_securities:
            if security not in self.securities_list:
                self.historical_VwapReversion_by_symbol[security.symbol] = deque(maxlen=self.period)
                self.securities_list.append(security)

        for security in changes.removed_securities:
            if security in self.securities_list:
                self.securities_list.remove(security)


    def update(self, algorithm: QCAlgorithm, data: Slice) -> List[Insight]: 
        if data.quote_bars.count == 0:   # Only emit insights when there is quote data, not when a corporate action occurs (at midnight)
            return []
        if self.day == algorithm.time.day:  # Only emit insights once per day
            return []
        self.day = algorithm.time.day

        # Neutralize Vwap/Close of securities so it's mean 0, then append them to the list 
        temp_list = {}
        for security in self.securities_list:
            if security.Close != 0:
                temp_list[security.symbol] = algorithm.vwap(security.symbol).Current.Value/security.Close
            else: 
                temp_list[security.symbol] = 1
        temp_mean = sum(temp_list.values())/len(temp_list.values())

        for security in self.securities_list:
            self.historical_VwapReversion_by_symbol[security.symbol].appendleft(temp_list[security.symbol]-temp_mean)
        
        # Compute ts_zscore of current Vwap/Close
        zscore_by_symbol = {}
        for security in self.securities_list:
            zscore_by_symbol[security.symbol] = sp.stats.zscore(self.historical_VwapReversion_by_symbol[security.symbol])[0]
        
        # create insights to long / short the asset
        insights = []
        weights = {}
        for symbol, zscore in zscore_by_symbol.items():
            if not np.isnan(zscore):
                weight = zscore
            else:
                weight = 0
            weights[symbol] = weight


        # Make scale similar across alphas
        abs_weight = {key: abs(val) for key, val in weights.items()}
        weights_sum = sum(abs_weight.values())
        if weights_sum != 0:
            for symbol, weight in weights.items():
                weights[symbol] = weight/ weights_sum


        for symbol, weight in weights.items():
            if weight > 0:
                insights.append(Insight.price(symbol, Expiry.END_OF_DAY, InsightDirection.UP, weight=weight))
            elif weight < 0:
                insights.append(Insight.price(symbol, Expiry.END_OF_DAY, InsightDirection.DOWN, weight=weight))

        return insights


class TSZscore_DividendGrowth(AlphaModel):

    def __init__(self):
        self.period = 252
        self.day = -1
        self.securities_list = []
        self.dps = {}
        

    def on_securities_changed(self, algorithm: QCAlgorithm, changes: SecurityChanges) -> None:
        # Register each security in the universe
        for security in changes.added_securities:
            if security not in self.securities_list:
                self.dps[security.symbol] = deque(maxlen=self.period)
                self.securities_list.append(security)

        for security in changes.removed_securities:
            if security in self.securities_list:
                self.securities_list.remove(security)

    def update(self, algorithm: QCAlgorithm, data: Slice) -> List[Insight]:
        if data.quote_bars.count == 0:   # Only emit insights when there is quote data, not when a corporate action occurs (at midnight)
            return []
        if self.day == algorithm.time.day:  # Only emit insights once per day
            return []
        self.day = algorithm.time.day
        
        # Append dividend to the list, compute ts_zscore of current dividend
        zscore_by_symbol = {}
        for security in self.securities_list:
            if not np.isnan(security.fundamentals.earning_reports.dividend_per_share.Value):
                self.dps[security.symbol].appendleft(security.fundamentals.earning_reports.dividend_per_share.Value)
                zscore_by_symbol[security.symbol] = sp.stats.zscore(self.dps[security.symbol])[0]

        
        # create insights to long / short the asset
        insights = []
        weights = {}
        for symbol, zscore in zscore_by_symbol.items():
            if not np.isnan(zscore):
                weight = zscore
            else:
                weight = 0
            weights[symbol] = weight
            
        # Make scale similar across alphas
        abs_weight = {key: abs(val) for key, val in weights.items()}
        weights_sum = sum(abs_weight.values())
        if weights_sum != 0:
            for symbol, weight in weights.items():
                weights[symbol] = weight/ weights_sum

        for symbol, weight in weights.items():
            if weight >= 0:
                insights.append(Insight.price(symbol, Expiry.END_OF_DAY, InsightDirection.UP, weight=weight))
            else:
                insights.append(Insight.price(symbol, Expiry.END_OF_DAY, InsightDirection.DOWN, weight=weight))

        
        return insights


class Conditional_Reversion(AlphaModel):

    def __init__(self):
        self.condition_period = 5
        self.period = 3
        self.securities_list = []
        self.day = -1
        self.historical_volume_by_symbol = {}
        self.historical_close_by_symbol = {}
        

    def on_securities_changed(self, algorithm: QCAlgorithm, changes: SecurityChanges) -> None:
        # Register each security in the universe
        for security in changes.added_securities:
            if security not in self.securities_list:
                self.historical_volume_by_symbol[security.symbol] = deque(maxlen=self.condition_period)
                self.historical_close_by_symbol[security.symbol] = deque(maxlen=self.period)
                self.securities_list.append(security)

        for security in changes.removed_securities:
            if security in self.securities_list:
                self.securities_list.remove(security)


    def update(self, algorithm: QCAlgorithm, data: Slice) -> List[Insight]: 
        if data.quote_bars.count == 0:   # Only emit insights when there is quote data, not when a corporate action occurs (at midnight)
            return []
        if self.day == algorithm.time.day:  # Only emit insights once per month
            return []
        self.day = algorithm.time.day
        

        # Append volume and close to the list
        zscore_by_symbol = {}
        return_by_symbol = {}
        for security in self.securities_list:
            if (security.Close != 0 and security.Volume != 0):
                self.historical_close_by_symbol[security.symbol].appendleft(security.Close)
                self.historical_volume_by_symbol[security.symbol].appendleft(security.Volume)
                return_by_symbol[security.symbol] = (self.historical_close_by_symbol[security.symbol][0] - self.historical_close_by_symbol[security.symbol][-1])

        if return_by_symbol == {}:  # Don't emit insight if there's no valid data
            return []

        # Rank the 3 days return among securities to return value from 0 to 1
        sorted_return_by_symbol = sorted(return_by_symbol.items(), key=lambda x: x[1])
        return_rank_by_symbol = {}
        for item in sorted_return_by_symbol:   # item is a key-value pair. [0] is the security symbol and [1] is the return
            return_rank_by_symbol[item[0]] = (sorted_return_by_symbol.index(item))/ len(sorted_return_by_symbol)
        
        # Calculating the final weight
        weights = {}
        
        for security in self.securities_list:
            # If condition is met, assign weight
            if len(self.historical_volume_by_symbol[security.symbol]) != 0 and max(self.historical_volume_by_symbol[security.symbol]) == security.Volume:
                weight = -return_rank_by_symbol[security.symbol] # Change this sign and complete different behaviour if purely long. Investigate
            else:
                weight = 0 
            weights[security.symbol] = weight

        weights_mean = sum(weights.values())/len(weights.values())
        for symbol, weight in weights.items():
            weights[symbol] = weight - weights_mean

        # Make scale similar across alphas
        abs_weight = {key: abs(val) for key, val in weights.items()}
        weights_sum = sum(abs_weight.values())
        if weights_sum != 0:
            for symbol, weight in weights.items():
                weights[symbol] = weight/ weights_sum


        # Create insights to long / short the asset
        insights = []
        for symbol, weight in weights.items():
            if weight > 0:
                insights.append(Insight.price(symbol, Expiry.END_OF_DAY, InsightDirection.UP, weight=weight))
            elif weight < 0:
                insights.append(Insight.price(symbol, Expiry.END_OF_DAY, InsightDirection.DOWN, weight=weight))
        #Expiry.END_OF_DAY
        return insights


class MomentumQuantilesAlphaModel(AlphaModel):

    def __init__(self):
        self.quantiles = 10
        self.lookback_months = 6
        self.securities_list = []
        self.day = -1

    def on_securities_changed(self, algorithm: QCAlgorithm, changes: SecurityChanges) -> None:
        # Create and register indicator for each security in the universe
        security_by_symbol = {}
        for security in changes.added_securities:

            # Create an indicator
            security_by_symbol[security.symbol] = security
            #security.indicator = CustomMomentumPercent("custom", self.lookback_months, self.securities_list)
            security.indicator = MomentumPercent(self.lookback_months)
            self._register_indicator(algorithm, security)
            self.securities_list.append(security)
        
        # Warm up the indicators of newly-added stocks
        if security_by_symbol:
            history = algorithm.history[TradeBar](list(security_by_symbol.keys()), (self.lookback_months+1) * 30, Resolution.DAILY, data_normalization_mode=DataNormalizationMode.SCALED_RAW)
            for trade_bars in history:
                for bar in trade_bars.values():
                    security_by_symbol[bar.symbol].consolidator.update(bar)

        # Stop updating consolidator when the security is removed from the universe
        for security in changes.removed_securities:
            if security in self.securities_list:
                algorithm.subscription_manager.remove_consolidator(security.symbol, security.consolidator)
                self.securities_list.remove(security)

    def update(self, algorithm: QCAlgorithm, data: Slice) -> List[Insight]:
        # Reset indicators when corporate actions occur
        for symbol in set(data.splits.keys() + data.dividends.keys()):
            security = algorithm.securities[symbol]
            if security in self.securities_list:
                security.indicator.reset()
                algorithm.subscription_manager.remove_consolidator(security.symbol, security.consolidator)
                self._register_indicator(algorithm, security)

                history = algorithm.history[TradeBar](security.symbol, (security.indicator.warm_up_period+1) * 30, Resolution.DAILY, data_normalization_mode=DataNormalizationMode.SCALED_RAW)
                for bar in history:
                    security.consolidator.update(bar)
        
        # Only emit insights when there is quote data, not when a corporate action occurs (at midnight)
        if data.quote_bars.count == 0:
            return []
        
        # Only emit insights once per day
        if self.day == algorithm.time.day:
            return []
        self.day = algorithm.time.day



        # Get the momentum of each asset in the universe
        momentum_by_symbol = {security.symbol : security.indicator.current.value 
            for security in self.securities_list if security.symbol in data.quote_bars and security.indicator.is_ready}
                
        # Determine how many assets to hold in the portfolio
        quantile_size = int(len(momentum_by_symbol)/self.quantiles)
        if quantile_size == 0:
            return []

        # Create insights to long the assets in the universe with the greatest momentum
        weight = 1 / (quantile_size+1)

        insights = []
        for symbol, _ in sorted(momentum_by_symbol.items(), key=lambda x: x[1], reverse=True)[:quantile_size]:
            insights.append(Insight.price(symbol, Expiry.END_OF_DAY, InsightDirection.UP, weight=weight))

        return insights

    

    def _register_indicator(self, algorithm, security):
        # Update the indicator with monthly bars
        security.consolidator = TradeBarConsolidator(Calendar.MONTHLY)
        algorithm.subscription_manager.add_consolidator(security.symbol, security.consolidator)
        algorithm.register_indicator(security.symbol, security.indicator, security.consolidator)
#region imports
from AlgorithmImports import *
from collections import deque
import scipy as sp
import numpy as np
#endregion

def EWMA(value_history):
    output = value_history[0]
    for i in range(1, len(value_history)):
        output = 0.7 * value_history[i] + 0.3 * output
    return output
    
        
class CustomMomentumPercent(PythonIndicator):
    def __init__(self, name, period):
        self.name = name
        self.time = datetime.min
        self.value = 0
        self.momentum = MomentumPercent(period)

    def Update(self, input):
        self.momentum.update(IndicatorDataPoint(input.Symbol, input.EndTime, input.Close))
        self.time = input.EndTime
        self.value = self.momentum.Current.Value * input.Volume
        return self.momentum.IsReady


class Skewness(PythonIndicator):    # Doesn't work on 3th August 2020
    def __init__(self, name, period):
        self.name = name
        self.count = 0
        self.time = datetime.min
        self.value = 0
        self.queue = deque(maxlen=period)
        self.change_in_close = deque(maxlen=period)

    def Update(self, input):
        self.queue.appendleft(input.Close)
        if len(self.queue) > 1:
            self.change_in_close.appendleft(self.queue[0]/self.queue[1]-1)
        self.time = input.EndTime

        self.count = len(self.change_in_close)
        if self.count == self.queue.maxlen:
            self.value = sp.stats.skew(self.change_in_close, nan_policy="omit")
        
        return count == self.change_in_close.maxlen  


class VwapReversion(PythonIndicator):
    def __init__(self, name, symbol, algorithm):
        self.name = name
        self.time = datetime.min
        self.value = 0
        self.previous_value = 0
        self._vwap = algorithm.vwap(symbol)
        self.queue = deque(maxlen=30)
        

    def update(self, input):
        self._vwap.update(input)
        self.time = input.EndTime
        self.queue.appendleft(self._vwap.Current.Value / input.Close)

        count = len(self.queue)
        if count == self.queue.maxlen:
            z_array = sp.stats.zscore(self.queue)
            if np.isfinite(z_array[0]):
                self.previous_value = self.value
                self.value = 0.7 * z_array[0] + 0.3 * self.previous_value
                
        return count == self.queue.maxlen


    
# region imports
from AlgorithmImports import *
from alpha import *
from PortfolioConstructor import MLP_PortfolioConstructionModel
# endregion


class LiquidEquityAlgorithm(QCAlgorithm):

    def initialize(self):
        self.set_start_date(2020, 1, 1)
        self.set_end_date(2021, 1, 1) 
        self.set_cash(1_000_000)
        self.SetBenchmark(self.AddEquity("SPY").Symbol)
        self.set_brokerage_model(BrokerageName.INTERACTIVE_BROKERS_BROKERAGE, AccountType.MARGIN)
        self.settings.minimum_order_margin_portfolio_percentage = 0
        self.settings.rebalance_portfolio_on_security_changes = False
        self.settings.rebalance_portfolio_on_insight_changes = False
        self.day = -1
        self.set_warm_up(timedelta(1))

        self.universe_settings.asynchronous = True
        self.add_universe_selection(FundamentalUniverseSelectionModel(self.fundamental_filter_function))
        
        self.add_alpha(TSZscore_VwapReversion())
        self.add_alpha(TSZscore_DividendGrowth())
        self.add_alpha(Conditional_Reversion())
        self.add_alpha(MomentumQuantilesAlphaModel())

        self.set_portfolio_construction(MLP_PortfolioConstructionModel(algorithm=self, rebalance=Expiry.EndOfMonth))
        
        self.add_risk_management(NullRiskManagementModel())

        self.set_execution(ImmediateExecutionModel())
        
    def fundamental_filter_function(self, fundamental: List[Fundamental]):
        filtered = [f for f in fundamental if f.symbol.value != "AMC" and f.has_fundamental_data and not np.isnan(f.dollar_volume)]
        sorted_by_dollar_volume = sorted(filtered, key=lambda f: f.dollar_volume, reverse=True)
        return [f.symbol for f in sorted_by_dollar_volume[0:1000]]