Skip to main content

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

Project description

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.hdf.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)

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

portsort-0.2.5.tar.gz (32.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

portsort-0.2.5-py3-none-any.whl (29.9 kB view details)

Uploaded Python 3

File details

Details for the file portsort-0.2.5.tar.gz.

File metadata

  • Download URL: portsort-0.2.5.tar.gz
  • Upload date:
  • Size: 32.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/4.0.1 CPython/3.10.4

File hashes

Hashes for portsort-0.2.5.tar.gz
Algorithm Hash digest
SHA256 393e9848445d8fe0189d841c0b685da956a720bb188d62a110f2ee34bfec2129
MD5 15771dfa0fd8094fda344880b6c04a44
BLAKE2b-256 0b861c5693322c383fb6ab3234b459e83b94369a7ccbc295c69c121d3197bd29

See more details on using hashes here.

File details

Details for the file portsort-0.2.5-py3-none-any.whl.

File metadata

  • Download URL: portsort-0.2.5-py3-none-any.whl
  • Upload date:
  • Size: 29.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/4.0.1 CPython/3.10.4

File hashes

Hashes for portsort-0.2.5-py3-none-any.whl
Algorithm Hash digest
SHA256 e7ff964f528e47bb50bcd0cebd88b9cac1bc9bb9ca3f7ee01227e6b5ca439cd1
MD5 1845b9ba6121f92894de6aef8e93ceb2
BLAKE2b-256 6a6e00aedd975f49980b5c05b8e8c63c0f3041b38d7f2104d4621bdb511e09b0

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page