Back to Community
liquidity factor?

Here's an attempt at a liquidity alpha factor (via combination several liquidity alpha factors into one).

Clone Algorithm
25
Loading...
Backtest from to with initial capital
Total Returns
--
Alpha
--
Beta
--
Sharpe
--
Sortino
--
Max Drawdown
--
Benchmark Returns
--
Volatility
--
Returns 1 Month 3 Month 6 Month 12 Month
Alpha 1 Month 3 Month 6 Month 12 Month
Beta 1 Month 3 Month 6 Month 12 Month
Sharpe 1 Month 3 Month 6 Month 12 Month
Sortino 1 Month 3 Month 6 Month 12 Month
Volatility 1 Month 3 Month 6 Month 12 Month
Max Drawdown 1 Month 3 Month 6 Month 12 Month
# https://arxiv.org/pdf/1412.5072.pdf

from quantopian.algorithm import attach_pipeline, pipeline_output
from quantopian.pipeline import Pipeline
from quantopian.pipeline.factors import CustomFactor, Returns, SimpleMovingAverage
from quantopian.pipeline.data.builtin import USEquityPricing
from quantopian.pipeline.data import Fundamentals
import quantopian.optimize as opt
from sklearn import preprocessing
from quantopian.pipeline.filters import QTradableStocksUS
from scipy.stats.mstats import winsorize
import numpy as np
import pandas as pd

WIN_LIMIT = 0.05

def preprocess(a):
    
    a = a.astype(np.float64)
    a[np.isinf(a)] = np.nan
    not_nan_ind = np.argwhere(~np.isnan(a))
    if not_nan_ind.size > 0:
        a_win = winsorize(a[not_nan_ind], limits=[WIN_LIMIT,WIN_LIMIT])
        a[not_nan_ind] = a_win
    else:
        a = winsorize(a, limits=[WIN_LIMIT,WIN_LIMIT])
        
    a = np.nan_to_num(a - np.nanmean(a))
    
    return preprocessing.scale(a)

def normalize(x):
    
    r = x - x.mean()
    
    return r/r.abs().sum()

def make_factors():
    
    class LIX(CustomFactor):
        inputs = [USEquityPricing.high, USEquityPricing.low, USEquityPricing.close, USEquityPricing.volume]
        window_length = 21
        window_safe = True
        def compute(self, today, assets, out, high, low, close, volume):
            dv = close[-1,:]*np.sum(volume, axis=0)
            r = np.amax(high,axis=0) - np.amin(low,axis=0)
            out[:] = preprocess(-np.log10(dv/r))
            
    class ILLIQ(CustomFactor):
        inputs = [USEquityPricing.close, USEquityPricing.volume, Returns(window_length=21)]
        window_length = 21
        window_safe = True
        def compute(self, today, assets, out, close, volume, ret):
            ilq = np.absolute(ret)/(close*volume)
            out[:] = preprocess(-np.sum(ilq,axis=0))
            
    class HH(CustomFactor):
        inputs = [USEquityPricing.high, USEquityPricing.low, USEquityPricing.volume, Fundamentals.shares_outstanding]
        window_length = 21
        window_safe = True
        def compute(self, today, assets, out, high, low, volume, n_shares):
            dv = np.amin(low,axis=0)*np.sum(volume, axis=0)
            r = np.amax(high,axis=0) - np.amin(low,axis=0)
            out[:] = preprocess(-n_shares[-1]*r/dv)
            
    class share_turnover(CustomFactor):
        inputs = [USEquityPricing.volume, Fundamentals.shares_outstanding]
        window_length = 21
        window_safe = True
        def compute(self, today, assets, out, volume, n_shares):
            v = np.sum(volume, axis=0)
            out[:] = preprocess(-v/n_shares[-1])
            
    class dollar_turnover(CustomFactor):
        inputs = [USEquityPricing.close, USEquityPricing.volume, Fundamentals.shares_outstanding]
        window_length = 21
        window_safe = True
        def compute(self, today, assets, out, close, volume, n_shares):
            dv = np.sum(close*volume, axis=0)/close[-1,:]
            out[:] = preprocess(-dv/n_shares[-1])
            
    factors = [
            LIX,
            ILLIQ,
            HH,
            share_turnover,
            dollar_turnover,
        ]
    
    return factors

def factor_pipeline():
    
    universe = QTradableStocksUS()
    
    factors = make_factors()
  
    pipeline_columns = {}
    for k,f in enumerate(factors):
        pipeline_columns['alpha_'+str(k)] = SimpleMovingAverage(inputs=[f(mask=universe)], window_length=5)
 
    pipe = Pipeline(columns = pipeline_columns,
    screen = universe)
    
    return pipe

def initialize(context):    
    
    attach_pipeline(factor_pipeline(), 'factor_pipeline')
    
    # Schedule my rebalance function
    schedule_function(func=rebalance,
                      date_rule=date_rules.every_day(),
                      time_rule=time_rules.market_close(hours=1),
                      half_days=True)
    # record my portfolio variables at the end of day
    schedule_function(func=recording_statements,
                      date_rule=date_rules.every_day(),
                      time_rule=time_rules.market_close(),
                      half_days=True)
    
    set_commission(commission.PerShare(cost=0, min_trade_cost=0))
    set_slippage(slippage.FixedSlippage(spread=0))

def recording_statements(context, data):
 
    record(num_positions=len(context.portfolio.positions))
    record(leverage=context.account.leverage)
    
def rebalance(context, data):
    
    alpha = pipeline_output('factor_pipeline').sum(axis=1)
    order_optimal_portfolio(opt.TargetWeights(normalize(alpha)), constraints=[])
There was a runtime error.
23 responses

Interesting one, thanks @Grant! Think you might have found a reason for why Q may not want to license individual (weak) factors, but when combined, the factor of related factors becomes more robust and possibly more interesting?

Wouldn't shs_float_cf be a better liquidity gauge than shares_outstanding ?

@Grant,
a very interesting LIX indicator.
Thanks for sharing.
I created the LIX indicator to see how we can use it to determine market regime.

Loading notebook preview...
Notebook previews are currently unavailable.

And here's the alpha decay and risk exposure analysis just on two factors LIX and ILLIQ.

Loading notebook preview...
Notebook previews are currently unavailable.

@ Viridian Hawk -

Wouldn't shs_float_cf be a better liquidity gauge than shares_outstanding ?

Not sure. Could you explain your reasoning?

@ Vladimir -

I just spent a little time Googling factor etfs/smart beta/alpha factors, etc. and came across the idea of liquidity. For example:

https://investor.vanguard.com/etf/profile/VFLQ

There, it says:

Fund invests in stocks with relatively lower measures of trading liquidity.
The Liquidity factor is measured by percentage turnover, dollar turnover, and Amihud illiquidity.

It is worth noting that on Q, one could use minute bar data to construct liquidity (illiquidity) factors. There may be liquidity information not revealed in the daily bars.

“Shares outstanding” includes shares "closely held" by insiders and shares otherwise restricted from being traded. "Float" refers to the shares that can actually be traded freely on the public markets, and as such is probably a better fit conceptually for gauging liquidity.

Here's a first-cut at a liquidity factor. Good? Bad? And why?

Loading notebook preview...
Notebook previews are currently unavailable.

In my opinion...

The Good:

-Consistent specific IR (not too sporadic drops, or negative specific IR)
-Very little Style tilts
-Scoring most stocks in the QTU using TargetWeights
-Low turnover
-Might have value for certain Sectors?

The Bad:

-Relatively low Returns, both specific and common (specific is what matters, and it looks a bit better than common/total)
-Some ‘extreme’ Sector exposures.
-Seems fairly volatile

The Ugly:

-Sector tilts.

Grant ,

Recently, I spent some time working on the illiquidity ratio (ILLIQ), and found that it behaves very much like various volatility factors.

There were a lot of nans so I changed to

out[:] = np.nansum(ilq)  

I used DailyReturns instead of Returns and multiplied the value by 1000 to see the numbers.

Amihud (2002) illiquidity ratio, ILLIQ, is one of the most widely used in the industry and is
the daily ratio of absolute stock return to its dollar volume averaged over some period.

Here it is for QTradableStocksUS.

Loading notebook preview...
Notebook previews are currently unavailable.

Than I decided to test ILLIQ as volatility switch on Yulia Malitskaya conventional momentum winners (W_10).
I even left her magic threshold 0.27.
The results are not the best but very similar to backtests with other volatility factors.

Loading notebook preview...
Notebook previews are currently unavailable.

Thanks Vladimir -

I see this as a potential test case to see if Q is really interested in funding a lot more itsty-bitsy alphas under the new signal combination paradigm, as they've said they would. It seems that they should publish a list of 50-100 of such little projects, versus trying to find the goose that laid the golden egg. I'd be glad to get in on the action at some low, but consistent monthly payout, versus shooting for the $50M grand prize.

@ Vladimir -

I used DailyReturns

Thanks for the tip! I think I had the Amihud (2002) illiquidity ratio incorrect.

Now I have:

    class ILLIQ(CustomFactor):  
        inputs = [USEquityPricing.close, USEquityPricing.volume, DailyReturns(window_length=2)]  
        window_length = 253  
        window_safe = True  
        def compute(self, today, assets, out, close, volume, ret):  
            ilq = np.absolute(ret)/(close*volume)  
            out[:] = preprocess(np.nansum(ilq,axis=0))  

with


from sklearn import preprocessing  
def preprocess(a):  
    a = a.astype(np.float64)  
    a[np.isinf(a)] = np.nan  
    not_nan_ind = np.argwhere(~np.isnan(a))  
    if not_nan_ind.size > 0:  
        a_win = winsorize(a[not_nan_ind], limits=[WIN_LIMIT,WIN_LIMIT])  
        a[not_nan_ind] = a_win  
    else:  
        a = winsorize(a, limits=[WIN_LIMIT,WIN_LIMIT])  
    a = np.nan_to_num(a - np.nanmean(a))  
    return preprocessing.scale(a)  

Grant ,

It is not necessary to specify a window length for DailyReturns

DailyReturns() is the same as  DailyReturns(window_length=2)  

I did not use preprocessing just to see how it looks like originally.

Thanks Vladimir -

One extension here would be to use the Q minute bar data. I have to think there's more than just thin air in liquidity, if Vanguard (and perhaps others) went to the trouble of launching an ETF. There's probably a bunch of academic and industry research on the topic, or they'd be sticking their necks out pretty far.

Grant -- according to Quantopian's TOS as soon as you've posted an algorithm to the forum they are free to use it license-free -- you basically give up any authorship rights to that code. So I hope for your sake you're keeping the really juicy stuff secret.

@ Viridian Hawk -

It's a hobby so there's really nothing to lose.

Anyway, if you have anything technical to add to this thread, on the topic of liquidity, please contribute.

Glancing at https://investor.vanguard.com/etf/profile/portfolio/vflq suggests that this "factor" ETF may have significant exposure to some "common" factors (e.g. size and value). I'm guessing that even though it is called a "factor" ETF, it is more like a "smart beta" ETF in that it has a constraint to attempt to kinda-sorta track the Russell 3000 benchmark versus actually isolating liquidity while controlling for other common factors.

May be of interest:

https://papers.ssrn.com/sol3/papers.cfm?abstract_id=2291942

We find that the pricing of the Amihud measure is not attributable to
the construction of the return-to-volume ratio that is intended to
capture price impact, but driven by the trading volume component

@ Antony -

Thanks. I gather that you are an academic finance type. Is there any kind of consensus on liquidity as a factor in academic circles? I imagine that at some point, there was a sort of consensus on the original Fama and French factors being "real" (of course, academics make a living out of arguing over minutiae, so "consensus" is probably too strong of a word); perhaps there is similar acknowledgement that liquidity is not just vaporware.

Not an overly academic thing, @Grant.

If you buy an asset knowing that it's going to be difficult to unwind when you most need to, you'll probably want to pay less for it. And that's where the risk premium is earned.

@ Antony -

O.K., and I guess it works the other way too? Stocks that are easy to trade are a bit over-priced due to the fact that folks are willing to pay extra for the assurance that they can cash out whenever they want.

If you buy an asset knowing that it's going to be difficult to unwind when you most need to, you'll probably want to pay less for it. And that's where the risk premium is earned.

Would it be reasonable then to assume then that heavily shorted stocks would have an offsetting effect, since you'll probably want to short the stock at a higher basis for the same reasons and collect risk premium on the short side?

@ Viridian Hawk -

I think you are suggesting that liquidity may be different from a long versus a short perspective, since there may be a difference in closing out the position. In effect, there could be "long liquidity" and "short liquidity".

For example, the QTradableStocksUS() uses the 200-day median daily dollar volume as a proxy for liquidity (see https://www.quantopian.com/help#quantopian_pipeline_experimental_QTradableStocksUS). However, maybe this tilts the QTU toward long liquidity? But really one needs to be able to go long or short with the same ease, I think, for the kind of fund Q is constructing. It also costs more to short, so there's a barrier to shorting relative to going long.

Does Quantopian have so-called short interest data? Along with relative borrowing rates... Seems like a pretty basic set of data...

I think I'll put this aside for awhile. By the way, I sent an e-mail to Q asking if they'd be interested in a liquidity factor--no response. I guess that means "no" but who knows.

Clone Algorithm
1
Loading...
Backtest from to with initial capital
Total Returns
--
Alpha
--
Beta
--
Sharpe
--
Sortino
--
Max Drawdown
--
Benchmark Returns
--
Volatility
--
Returns 1 Month 3 Month 6 Month 12 Month
Alpha 1 Month 3 Month 6 Month 12 Month
Beta 1 Month 3 Month 6 Month 12 Month
Sharpe 1 Month 3 Month 6 Month 12 Month
Sortino 1 Month 3 Month 6 Month 12 Month
Volatility 1 Month 3 Month 6 Month 12 Month
Max Drawdown 1 Month 3 Month 6 Month 12 Month
from quantopian.algorithm import attach_pipeline, pipeline_output
from quantopian.pipeline import Pipeline
from quantopian.pipeline.factors import CustomFactor, DailyReturns
from quantopian.pipeline.data.builtin import USEquityPricing
from quantopian.pipeline.data import Fundamentals, factset
import quantopian.optimize as opt
from quantopian.pipeline.filters import QTradableStocksUS
from scipy.stats.mstats import winsorize
from scipy.stats import rankdata
import numpy as np
import pandas as pd
from quantopian.pipeline.classifiers.morningstar import Sector

WIN_LIMIT = 0.0

def preprocess(a):
    
    a = a.astype(np.float64)
    a[np.isinf(a)] = np.nan
    not_nan_ind = np.argwhere(~np.isnan(a))
    if not_nan_ind.size > 0:
        a_win = winsorize(a[not_nan_ind], limits=[WIN_LIMIT,WIN_LIMIT])
        a[not_nan_ind] = a_win
    else:
        a = winsorize(a, limits=[WIN_LIMIT,WIN_LIMIT])
        
    a = np.nan_to_num(a - np.nanmean(a))
    
    a = rankdata(a)
    a = a/np.amax(a)
    a = a - 0.5
    
    return a/np.sum(np.absolute(a))

def normalize(x):
    
    r = x.rank()
    r = r/r.max()
    r = r - 0.5
    
    return r/r.abs().sum()

def make_factors():
            
    class ILLIQ(CustomFactor):
        inputs = [USEquityPricing.close, USEquityPricing.volume, DailyReturns(), Fundamentals.shares_outstanding]
        window_length = 253
        def compute(self, today, assets, out, close, volume, ret, n_shares):
            ilq = np.absolute(ret)/(close*volume)
            a = preprocess(np.nanmean(close,axis=0)*np.nanmean(ilq,axis=0))
            st = n_shares/volume
            b = preprocess(np.nanmean(st,axis=0))
            dt = n_shares/(close*volume)
            c = preprocess(np.nanmean(close,axis=0)*np.nanmean(dt,axis=0))
            out[:] = preprocess(a+b+c)
            
    factors = [
            ILLIQ,
            ]
    
    return factors

def factor_pipeline():
    
    factors = make_factors()
    
    sectors = [101,102,103,104,205,206,207,308,309,310,311]
    
    pipeline_columns = {}  
    for k,f in enumerate(factors):
        for s in sectors:
            universe = QTradableStocksUS() & Sector().eq(s)
            pipeline_columns['alpha_'+str(k)+'_'+str(s)] = f(mask=universe)

    pipe = Pipeline(columns = pipeline_columns,
    screen = QTradableStocksUS())
    
    return pipe

def initialize(context):    
    
    attach_pipeline(factor_pipeline(), 'factor_pipeline')
    
    # Schedule my rebalance function
    schedule_function(func=rebalance,
                      date_rule=date_rules.every_day(),
                      time_rule=time_rules.market_close(hours=1),
                      half_days=True)
    # record my portfolio variables at the end of day
    schedule_function(func=recording_statements,
                      date_rule=date_rules.every_day(),
                      time_rule=time_rules.market_close(),
                      half_days=True)
    
    set_commission(commission.PerShare(cost=0, min_trade_cost=0))
    set_slippage(slippage.FixedSlippage(spread=0))

def recording_statements(context, data):
 
    record(num_positions=len(context.portfolio.positions))
    record(leverage=context.account.leverage)
    
def rebalance(context, data):
    
    alpha = pipeline_output('factor_pipeline').sum(axis=1)
    
    objective = opt.TargetWeights(normalize(alpha))
    
    order_optimal_portfolio(objective=objective,
                            constraints=[]
                           )
There was a runtime error.