Overall Statistics
Total Orders
1898
Average Win
0.62%
Average Loss
-0.63%
Compounding Annual Return
-2.163%
Drawdown
37.600%
Expectancy
-0.018
Start Equity
100000
End Equity
86580.12
Net Profit
-13.420%
Sharpe Ratio
-0.124
Sortino Ratio
-0.16
Probabilistic Sharpe Ratio
0.064%
Loss Rate
50%
Win Rate
50%
Profit-Loss Ratio
0.98
Alpha
-0.013
Beta
-0.042
Annual Standard Deviation
0.134
Annual Variance
0.018
Information Ratio
-0.67
Tracking Error
0.172
Treynor Ratio
0.396
Total Fees
$9213.19
Estimated Strategy Capacity
$1000.00
Lowest Capacity Asset
SIF R735QTJ8XC9X
Portfolio Turnover
4.05%
#region imports
from AlgorithmImports import *

from collections import deque
#endregion
# https://quantpedia.com/Screener/Details/66


class CombiningMomentumEffectWithVolume(QCAlgorithm):

    def initialize(self):
        self.set_start_date(2012, 1, 1)
        self.set_end_date(2018, 8, 1)
        self.set_cash(100_000)

        self.universe_settings.resolution = Resolution.DAILY
        self.add_universe(self._coarse_selection_function, self._fine_selection_function)
        self._data_dict = {}
        # 1/3 of the portfolio is rebalanced every month
        self._portfolios = deque(maxlen=3)
        self.schedule.on(
            self.date_rules.month_start(Symbol.create("SPY", SecurityType.EQUITY, Market.USA)),
            self.time_rules.at(0, 0), self._rebalance)
        # the lookback period for return calculation
        self._lookback = 252
        self._filtered_fine = None
        self._monthly_rebalance = False
        self.set_warmup(self._lookback + 1)

    def _coarse_selection_function(self, coarse):
        for i in coarse:
            if i.symbol not in self._data_dict:
                self._data_dict[i.symbol] = SymbolData(i.symbol, self._lookback)
            self._data_dict[i.symbol].roc.update(i.end_time, i.adjusted_price)
            self._data_dict[i.symbol].volume = i.volume
        
        if self._monthly_rebalance:
            # drop stocks which have no fundamental data 
            return [x.symbol for x in coarse if x.has_fundamental_data]
        return []

    def _fine_selection_function(self, fine):
        if self._monthly_rebalance:
            data_ready = {symbol: symbolData for (symbol, symbolData) in self._data_dict.items() if symbolData.roc.is_ready}
            if len(data_ready) < 100: 
                self._filtered_fine = []
            else:
                sorted_fine = [i for i in fine if i.earning_reports.basic_average_shares.three_months != 0 and i.symbol in data_ready]
                sorted_fine_symbols = [i.symbol for i in sorted_fine]
                filtered_data = {symbol: symbolData for (symbol, symbolData) in data_ready.items() if symbol in sorted_fine_symbols}
                for i in sorted_fine:
                    if i.symbol in filtered_data and filtered_data[i.symbol].volume != 0:
                        filtered_data[i.symbol].turnover = i.earning_reports.basic_average_shares.three_months / filtered_data[i.symbol].volume
                sorted_by_roc = sorted(filtered_data.values(), key = lambda x: x.roc.current.value, reverse = True)
                top_roc = sorted_by_roc[:int(len(sorted_by_roc)*0.2)]
                bottom_roc = sorted_by_roc[-int(len(sorted_by_roc)*0.2):]
                
                high_turnover_top_roc = sorted(top_roc, key = lambda x: x.turnover, reverse = True)
                high_turnover_bottom_roc = sorted(bottom_roc, key = lambda x: x.turnover, reverse = True)
                
                self._long =  [i.symbol for i in high_turnover_top_roc[:int(len(high_turnover_top_roc)*0.01)]]
                self._short = [i.symbol for i in high_turnover_bottom_roc[:int(len(high_turnover_bottom_roc)*0.01)]]
                self._filtered_fine = self._long + self._short
                self._portfolios.append(self._filtered_fine)
        else:
            self._filtered_fine = []
        
        if not self._filtered_fine:
            self._monthly_rebalance = False

        return self._filtered_fine
        
    def _rebalance(self):
        self._monthly_rebalance = True
           
    def on_data(self, data):
        if self._monthly_rebalance and self._filtered_fine:
            self._filtered_fine = None
            self._monthly_rebalance = False
            
            # 1/3 of the portfolio is rebalanced every month
            if len(self._portfolios) == self._portfolios.maxlen:
                for i in list(self._portfolios)[0]:
                    self.liquidate(i)
            
            # stocks are equally weighted and held for 3 months 
            short = [s for s in self._short if s in data.Bars]
            short_weight = 1/len(short)
            for i in short:
                self.set_holdings(i, -1/3*short_weight)
            
            long_ = [s for s in self._long if s in data.Bars]
            long_weight = 1/len(long_)
            for i in long_:
                self.set_holdings(i, 1/3*long_weight)            
 

class SymbolData:
    
    def __init__(self, symbol, lookback):
        self.symbol = symbol
        self.roc = RateOfChange(lookback)
        self.volume = None