Overall Statistics
Total Orders
76
Average Win
1.68%
Average Loss
-1.10%
Compounding Annual Return
4.836%
Drawdown
13.200%
Expectancy
0.380
Start Equity
1000000
End Equity
1125103.02
Net Profit
12.510%
Sharpe Ratio
0.249
Sortino Ratio
0.314
Probabilistic Sharpe Ratio
12.842%
Loss Rate
45%
Win Rate
55%
Profit-Loss Ratio
1.53
Alpha
0.034
Beta
-0.132
Annual Standard Deviation
0.092
Annual Variance
0.008
Information Ratio
-0.451
Tracking Error
0.144
Treynor Ratio
-0.172
Total Fees
$1237.37
Estimated Strategy Capacity
$8900000.00
Lowest Capacity Asset
HAE R735QTJ8XC9X
Portfolio Turnover
0.65%
#region imports
from AlgorithmImports import *
#endregion
# https://quantpedia.com/Screener/Details/18


class LiquidityEffectAlgorithm(QCAlgorithm):

    def initialize(self):
        self.set_start_date(2016, 1, 1)   
        self.set_end_date(2018, 7, 1)         
        self.set_cash(1000000)

        self.universe_settings.resolution = Resolution.DAILY
        self.add_universe(self._coarse_selection_function, self._fine_selection_function)
        self.add_equity("SPY", Resolution.DAILY)
        
        self.schedule.on(self.date_rules.month_start("SPY"), self.time_rules.after_market_open("SPY"), self._rebalance)
        
        # Count the number of months that have passed since the algorithm starts
        self._months = -1
        self._yearly_rebalance = True
        self._num_coarse = 500
        self._long = None
        self._short = None
        
    def _coarse_selection_function(self, coarse):
        if self._yearly_rebalance:
            # drop stocks which have no fundamental data or have low price
            selected = [x for x in coarse if (x.has_fundamental_data) and (float(x.adjusted_price) > 5)]
            # rank the stocks by dollar volume 
            filtered = sorted(selected, key=lambda x: x.dollar_volume, reverse=True) 
            self.filtered_coarse = [ x.symbol for x in filtered[:self._num_coarse]]
            return self.filtered_coarse
        else: 
            return Universe.UNCHANGED

    def _fine_selection_function(self, fine):
        if self._yearly_rebalance:
            # The market capitalization must be no less than $10 million
            top_market_cap = list(filter(lambda x: x.market_cap > 10000000, fine))
            # Save all market cap values
            market_caps = [i.market_cap for i in top_market_cap]
            # Calculate the lowest market-cap quartile
            lowest_quartile = np.percentile(market_caps, 25)
            # Filter stocks in the lowest market-cap quartile
            lowest_market_cap = list(filter(lambda x: x.market_cap <= lowest_quartile, top_market_cap))
            turnovers = []
            # Divide into quartiles based on their turnover (the number of shares traded divided by the stock’s outstanding shares) in the last 12 months
            for i in lowest_market_cap[:]:
                hist = self.history([i.symbol], 21*12, Resolution.DAILY)
                if not hist.empty:    
                    mean_volume = np.mean(hist.loc[str(i.symbol)]['volume'])
                    i.turnover =  mean_volume / float(i.company_profile.shares_outstanding)
                    turnovers.append(i.turnover)
                else:
                    lowest_market_cap.remove(i)
            bottom_turnover = np.percentile(turnovers, 5) 
            top_turnover = np.percentile(turnovers, 95) 
            self._long = [x.symbol for x in lowest_market_cap if x.turnover < bottom_turnover]
            self._short = [x.symbol for x in lowest_market_cap if x.turnover > top_turnover]
                
            return self._long + self._short
        else:
            return Universe.UNCHANGED

    def _rebalance(self):
        # yearly rebalance
        self._months += 1
        if self._months % 12 == 0:
            self._yearly_rebalance = True

    def on_data(self, data):
        if not self._yearly_rebalance: 
            return 
        self._yearly_rebalance = False
            
        if self._long and self._short:
            stocks_invested = [symbol for symbol, security_holding in self.portfolio.items() if security_holding.invested]
            # liquidate stocks not in the trading list 
            for i in stocks_invested:
                if i not in self._long + self._short:
                    self.liquidate(i)   
            # goes long on stocks with the lowest turnover    
            for short_stock in self._short:
                self.set_holdings(short_stock, -0.5/len(self._short))            
            # short on stocks with the highest turnover        
            for long_stock in self._long:
                self.set_holdings(long_stock, 0.5/len(self._long))