portsort

A package to sort stocks into portfolios and calculate weighted-average returns.


Keywords
python, finance, asset-pricing, portfolio, sort, stocks, returns, portfolios, sorting
License
GPL-3.0
Install
pip install portsort==0.2.5

Documentation

PortSort

The PortSort class enables single, double or triple sorting of entities in portfolios. Construction of portfolios based on firm characteristics is possible with the ff_portfolios() method. Sorts can be conditional or uncondtional or a mix of both in triple sorting.

How to install

pip install portsort

How to use

The PortSort_Example.py file is used to demonstrate the functionality of the PortSort class and its methods.

A panel dataset of the characteristics of 800 random US domiciled and traded securities for 2018-2020 and a dataset of monthly returns are used for the example.

The 'FirmCharacteristics2018.csv' dataset has 8 columns:

  1. 'year' : Calendar year
  2. 'notPERMNO' : Randomized security identifier based on the true CRSP PERMNO
  3. 'CAP' : Market Capitalization of the last business day of the current year
  4. 'CAP_W' : Market Capitalization of the last business day of the previous year
  5. 'RET_total' : Total return (including dividends) of a stock in a year
  6. 'SPREAD_PC_median' : Daily median of the ratio of the Bid-Ask spread over the closing price of the stock in a year
  7. 'FF30' : Fama-French 30 industry classification of a security based on its SIC.
  8. 'EXCHCD' : Market exchange in which the security is traded. 1 is NYSE, 2 is AMEX and 3 is NASDAQ.

The 'STOCKmonthlydata2019.csv' dataset has 4 columns:

  1. 'date_m': Date in YYYYmm format.
  2. 'year' : Calendar year
  3. 'RET' : Returns of securities in monthly frequency (not in percentage).
  4. 'notPERMNO' : Randomized security identifier based on the true CRSP PERMNO
"""
Examples of the PortSort class and its methods.
"""

import os
import pandas as pd
import numpy as np



# Main directory
wdir = r'C:\Users\ropot\OneDrive\Desktop\PortSort-main'
os.chdir(wdir)


# Import the PortSort class. For more details: 
# https://github.com/ioannisrpt/portsort.git 
# pip install portsort 
from portsort import portsort as ps


# Import FirmCharacteristics table (annual frequency)
ftotype32 = {'year' : np.int32, 
             'CAP' : np.float32,
             'CAP_W' : np.float32,
             'RET_total' : np.float32, 
             'SPREAD_PC_median' : np.float32, 
             'EXCHCD' : np.int32,
             'notPERMNO' : np.int32}
firmchars = pd.read_csv(os.path.join(wdir, 'FirmCharacteristics2018.csv')).astype(ftotype32)

# Import return data (monthly frequency)
ctotype32 = {'RET' : np.float32,
             'date_m' : np.int32, 
             'year' : np.int32, 
             'notPERMNO' : np.int32}
crspm = pd.read_csv(os.path.join(wdir, 'STOCKmonthlydata2019.csv')).astype(ctotype32)


# Define the PortSort class
portchar = ps.PortSort(df = firmchars, 
                       entity_id = 'notPERMNO', 
                       time_id = 'year', 
                       save_dir = wdir)


# -------------
# single_sort()
# -------------

# Single sort stocks into quintile portfolios based on  the market 
# capitalization of the last business day of the previous year ('CAP')
portchar.single_sort(firm_characteristic = 'CAP', 
                    lagged_periods = 1, 
                    n_portfolios = 5)
print(portchar.single_sorted.head(20))


# Single sort stocks into 3 portfolios (30%, 40% 30%) based on the market 
# capitalization of the last business day of the previous year ('CAP').
# NYSE breakpoints for size are used.
portchar.single_sort(firm_characteristic = 'CAP', 
                    lagged_periods = 1, 
                    n_portfolios = np.array([0, 0.3, 0.7]), 
                    quantile_filter = ['EXCHCD', 1])
print(portchar.single_sorted.head(20))


# -------------
# double_sort()
# -------------

# Double sort stocks unconditionally into 5x2 portfolios based on the market
# capitalization of the last business day of the previous year ('CAP') and the 
# total annual return of the past year ('RET_total').
portchar.double_sort(firm_characteristics = ['CAP', 'RET_total'], 
                    lagged_periods = [1,1], 
                    n_portfolios = [5,2])
print(portchar.double_sorted.head(20))


# Double sort stocks conditionally into 3x2 portfolios based on  the market 
# capitalization of the last business day of the previous year ('CAP') and the
# total annual return of the past year ('RET_total').
# NYSE breakpoints for size are used.
portchar.double_sort(firm_characteristics = ['CAP', 'RET_total'], 
                    lagged_periods = [1,1], 
                    n_portfolios = [np.array([0, 0.3, 0.7]), 2],
                    quantile_filters = [['EXCHCD', 1], None], 
                    conditional = True)
print(portchar.double_sorted.head(20))



# -------------
# triple_sort()
# -------------

# Triple Sort stocks unconditionally into 2x2x2 portfolios based on  the market
# capitalization of the last business day of the previous year ('CAP'), total 
# annual return ('RET_total') and daily median spread percentage 
# ('SPREAD_PC_median') of the past year.
# NYSE breakpoitns are used for size and spread percentage 
# but not for total return.
portchar.triple_sort(firm_characteristics=['CAP', 'RET_total', 'SPREAD_PC_median'], 
                    lagged_periods = [1,1,1], 
                    n_portfolios = [2,2,2], 
                    quantile_filters = [['EXCHCD', 1], None, ['EXCHCD', 1]])
print(portchar.triple_sorted.head(20))


# Triple Sort stocks into 2x2x2 portfolios based on  the market capitalization 
# of the last business day of the previous year ('CAP'), total annual return 
# ('RET_total') and daily median spread percentage ('SPREAD_PC_median') of the 
# past year.
# First stocks are uncondtionally sorted by size and total annual return and 
# then within these portfolios they are conditionally sorted by spread.
# If A, B, C are the characteristics in that order and '+', '|' correspond
# to intersection and conditionality of sets, then conditional = [False, True] 
# is equivalent to  C|(A+B).
# Type help(PortSort.triple_sort) for more details.
# NYSE breakpoitns are used for size and spread percentage 
# but not for total return.
portchar.triple_sort(firm_characteristics=['CAP', 'RET_total', 'SPREAD_PC_median'], 
                    lagged_periods = [1,1,1], 
                    n_portfolios = [2,2,2], 
                    quantile_filters = [['EXCHCD', 1], None, ['EXCHCD', 1]],
                    conditional = [False, True])
print(portchar.triple_sorted.head(20))


# Triple Sort stocks into 2x2x2 portfolios based on  the market capitalization 
# of the last business day of the previous year ('CAP'), total annual return 
# ('RET_total') and daily median spread percentage ('SPREAD_PC_median') of the
# past year. Entities conditional on size, are then sorted into 2x2 
# unconditional return and spread portfolios. 
# If A, B, C are the characteristics in that exact order and
# '+', '|' correspond to intersection and conditionality of sets, 
# then conditional = [True, False] is  equivalent to (B + C)| A. 
# Type help(PortSort.triple_sort) for more details.
# NYSE breakpoitns are used for size and spread percentage 
# but not for total return.
portchar.triple_sort(firm_characteristics=['CAP', 'RET_total', 'SPREAD_PC_median'], 
                    lagged_periods = [1,1,1], 
                    n_portfolios = [2,2,2], 
                    quantile_filters = [['EXCHCD', 1], None, ['EXCHCD', 1]],
                    conditional = [True, False])
print(portchar.triple_sorted.head(20))


# ---------------------
# augment_last_traded()
# ---------------------


# First we adjust for delisted returns during the calendar
# year. PortSort handles the firm characteristics and the
# return dataframe separately and only merge them together
# at the very end for the calculation of portfolio returns.
# As such, we need to augment the characteristics dataset
# with the data for stocks that are delisted but need to be
# included in the sorting procedure. augment_last_traded()
# method allows for that adjustment while it fills the extra
# rows with the weighting variable 'CAP_W' and the exchange
# market 'EXCHCD'. If we don't adjust for the delistings,
# our results will suffer from look-ahead bias.
portchar.augment_last_traded(ret_data = crspm,
                            ret_time_id = 'date_m',
                            col_w='CAP',
                            col_w_lagged_periods=1,
                            col_w_suffix = 'W',
                            fill_cols=['EXCHCD'])


# ---------------
# ff_portfolios()
# ---------------


# Monthly returns of 10 value-weighted portfolios on size ('CAP'). 
# NYSE breakpoints are used.
portchar.ff_portfolios(ret_data = crspm,
                      ret_time_id = 'date_m',
                      ff_characteristics = ['CAP'], 
                      ff_lagged_periods = [1], 
                      ff_n_portfolios = [10], 
                      ff_quantile_filters = [['EXCHCD',1]], 
                      weight_col = 'CAP_W', 
                      return_col = 'RET', 
                      ff_save = True)
print(portchar.portfolios.head(30))
print(portchar.num_stocks)


# Monthly returns of 3x2 value-weighted portfolios on size ('CAP') and 
# liquidity ('SPREAD_PC_median').
# The sort is unconditional and NYSE breakpoints are used for size. 
# By specifying the market_cap_cols, the portfolio turnover is also returned.
# market_cap_cols is a list =
# [capitalization of the stock at the end of the previous period, 
# capitalization of the stock at the ned of the current period]
portchar.ff_portfolios(ret_data = crspm, 
                      ret_time_id = 'date_m', 
                      ff_characteristics = ['CAP', 'SPREAD_PC_median'], 
                      ff_lagged_periods = [1, 1],
                      ff_n_portfolios = [np.array([0, 0.3, 0.7]),2], 
                      ff_quantile_filters = [['EXCHCD',1], None],
                      ff_conditional = [False], 
                      weight_col = 'CAP_W', 
                      return_col = 'RET', 
                      market_cap_cols = ['CAP_W', 'CAP'],
                      ff_save = True)
print(portchar.portfolios.head(30))
print(portchar.num_stocks)
print(portchar.turnover)
print('Acess the explicit portfolio weights of the stocks: \n')
print(portchar.turnover_raw.head(20))



    
# Monthly returns of 2x2x2 value-weighted portfolios on size ('CAP'), 
# liquidity ('SPREAD_PC_median') and annual returns ('RET_total') of the
# previous year. 
# The sorts are all conditional (the order matters).
# NYSE breakpoints are used only for size.
portchar.ff_portfolios(ret_data = crspm,
                      ret_time_id = 'date_m', 
                      ff_characteristics = ['CAP', 'SPREAD_PC_median', 'RET_total'], 
                      ff_lagged_periods = [1,1,1], 
                      ff_n_portfolios = [2,2,2], 
                      ff_quantile_filters = [['EXCHCD',1], None, None],
                      ff_conditional = [True, True], 
                      weight_col = 'CAP_W', 
                      return_col = 'RET', 
                      market_cap_cols = ['CAP_W', 'CAP'],
                      ff_save = True)
print(portchar.portfolios.head(30))
print(portchar.num_stocks)    
print(portchar.turnover)




# Monthly returns of 2x2x2 value-weighted portfolios on size ('CAP'),
# liquidity ('SPREAD_PC_median') and annual returns ('RET_total') of the 
# previous year.
# Entities conditional on size, are then sorted into 2x2 unconditional return 
# and spread portfolios. 
# If A, B, C are the characteristics in that exact order and
# '+', '|' correspond to intersection and conditionality of sets, 
# then conditional = [True, False] is  equivalent to (B + C)| A. 
# Type help(PortSort.ff_portfolios) for more details.
# NYSE breakpoints are used for size.
portchar.ff_portfolios(ret_data = crspm,
                      ret_time_id = 'date_m', 
                      ff_characteristics=['CAP', 'SPREAD_PC_median', 'RET_total'], 
                      ff_lagged_periods = [1,1,1], 
                      ff_n_portfolios = [2,2,2], 
                      ff_quantile_filters = [['EXCHCD',1], None, None],
                      ff_conditional = [True, False], 
                      weight_col = 'CAP_W', 
                      return_col = 'RET', 
                      market_cap_cols = ['CAP_W', 'CAP'],
                      ff_save = True)
print(portchar.portfolios.head(30))
print(portchar.num_stocks)    
print(portchar.turnover)