Notebook

CHS Model (RISK OF FINANCIAL DISTRESS)

In a 2008 paper called "In Search of Distress Risk", John Campbell, Jens Hilscher, and Jan Szilagyi comprehensively explore the determinants of corporate failure.

  • NIMTA = weighted average (quarter's net income / MTA)
  • MTA = market value of total assets = book value of liabilities + market cap
  • TLMTA = total liabilities / MTA
  • CASHMTA = cash and equivalents/ / MTA
  • EXRET = weighted average (log(1 + stock's return) − log(1 + S&P 500 return)
  • SIGMA = annualized stock's standard deviation over the previous 3 months
  • RSIZE = log(stock market cap / S&P 500 total market value)
  • MB = MTA / adjusted book value, where adjusted book value = book value + .1× (market cap-book value)
  • PRICE = log(recent stock price), capped at 15, so a stock with a stock price of 20, would be given a value of log(15) instead of log(20)
  • XAVG =.5333 × t +.2666 × t −1+.1333 × t −2 +.0666 × t −3

The logit model generates a binary dependent variable or logit value, “logit probability of financial distress” or LPFD, calculated as follows:

LPFD = −20.26 × NIMTAAVG +1.42 × TLMTA −7.13 × EXRETAVG +1.41 × SIGMA −0.045 × RSIZE −2.13 × CASHMTA +0.075 × MB −0.058 × PRICE −9.16

The paper was updated on Januany, 2010 with new weights.

Link to the Paper: http://scholar.harvard.edu/campbell/publications/search-distress-risk

OLD: https://www.quantopian.com/posts/campbell-hilscher-szilagyi-chs-model-probability-of-corporate-failure NEW: https://www.quantopian.com/posts/campbell-hilscher-szilagyi-chs-model-probability-of-corporate-failure-update-version¶ Share

In [257]:
# Initialisation

import pandas as pd
import numpy as np
import datetime
from collections import OrderedDict

fundamentals = init_fundamentals()
In [258]:
from zipline.utils.tradingcalendar import get_trading_days
from datetime import date, datetime, timedelta

def three_month_ago(t):
    m = t.month - 3
    y = t.year
    if m < 1:
        m += 12
        y -= 1

    # return the first day of the month that's a trading day
    s = datetime(y,m,1)
    e = s + timedelta(days=7)
    return date(get_trading_days(s, e)[0].year, 
                         get_trading_days(s, e)[0].month, 
                         get_trading_days(s, e)[0].day)
#lag = 0
#today = datetime.now()
#t0 = date(today.year - lag, today.month, 1)
t0 = date(2015, 9, 1) # this is the ending date
t1 = three_month_ago(t0)
t2 = three_month_ago(t1)
t3 = three_month_ago(t2)
t4 = three_month_ago(t3)
print t0, t1, t2, t3, t4
2015-09-01 2015-06-01 2015-03-02 2014-12-01 2014-09-02
In [265]:
def fund_df(t):
    return get_fundamentals(
        query(
            fundamentals.valuation.market_cap,
            fundamentals.valuation.shares_outstanding,
            fundamentals.balance_sheet.cash_and_cash_equivalents,
            fundamentals.balance_sheet.stockholders_equity,
            fundamentals.balance_sheet.total_assets,
            fundamentals.balance_sheet.total_debt,
            fundamentals.income_statement.net_income
        )
        
         # No Financials (103), Real Estate (104), Utilities (207) and ADR
        .filter(fundamentals.company_reference.industry_template_code != 'B')
        .filter(fundamentals.company_reference.industry_template_code != 'I')
        .filter(fundamentals.company_reference.industry_template_code != 'F')
        .filter(fundamentals.asset_classification.morningstar_sector_code != 103)
        .filter(fundamentals.asset_classification.morningstar_sector_code != 104)
        .filter(fundamentals.asset_classification.morningstar_sector_code != 207)
        .filter(fundamentals.share_class_reference.is_depositary_receipt == False)
        .filter(fundamentals.share_class_reference.is_primary_share == True)
        
        # Only pick active common stocks
        .filter(fundamentals.share_class_reference.share_class_status == "A")
        .filter(fundamentals.share_class_reference.security_type == "ST00000001")
        # Exclude When Distributed(WD), When Issued(WI) and VJ - usuallly companies in bankruptcy
        .filter(~fundamentals.share_class_reference.symbol.like('%\_WI'))
        .filter(~fundamentals.share_class_reference.symbol.like('%\_WD'))
        .filter(~fundamentals.share_class_reference.symbol.like('%\_VJ'))
        # Exclude Halted stocks
        .filter(~fundamentals.share_class_reference.symbol.like('%\_V'))
        .filter(~fundamentals.share_class_reference.symbol.like('%\_H'))
        
        
        # Only NYSE, AMEX and Nasdaq
        .filter(fundamentals.company_reference.primary_exchange_id.in_(['NYSE', 'NAS', 'AMEX']))
               
        # Sanity check 
        # TODO better None or > 0 ?
        .filter(fundamentals.valuation.market_cap > 0)
        .filter(fundamentals.valuation.shares_outstanding > 0)
        .filter(fundamentals.balance_sheet.invested_capital > 0)
        .filter(fundamentals.balance_sheet.cash_and_cash_equivalents > 0)
        .filter(fundamentals.balance_sheet.current_assets > 0)
        #.filter(fundamentals.balance_sheet.current_assets is not None)
        .filter(fundamentals.balance_sheet.total_assets > 0)
        #.filter(fundamentals.balance_sheet.total_assets is not None)
        .filter(fundamentals.cash_flow_statement.free_cash_flow is not None)
        .filter(fundamentals.valuation.enterprise_value > 0),
        
        t)

fund_df0 = fund_df(t0)
fund_df1 = fund_df(t1)
fund_df2 = fund_df(t2)
fund_df3 = fund_df(t3)
fund_df4 = fund_df(t4)
In [267]:
qtr0 = "%d-%02d-%02d" % (t0.year, t0.month, t0.day)
qtr1 = "%d-%02d-%02d" % (t1.year, t1.month, t1.day)
qtr2 = "%d-%02d-%02d" % (t2.year, t2.month, t2.day)
qtr3 = "%d-%02d-%02d" % (t3.year, t3.month, t3.day)
qtr4 = "%d-%02d-%02d" % (t4.year, t4.month, t4.day)

fundamental_dict = OrderedDict()
fundamental_dict[qtr4] = fund_df4
fundamental_dict[qtr3] = fund_df3
fundamental_dict[qtr2] = fund_df2
fundamental_dict[qtr1] = fund_df1
fundamental_dict[qtr0] = fund_df0

fundamental_data = pd.Panel(fundamental_dict)
items = fundamental_data.items
fundamental_data.minor_axis
Out[267]:
Index([      Equity(2 [AA]),    Equity(24 [AAPL]),    Equity(31 [ABAX]),
          Equity(41 [ARCB]),     Equity(52 [ABM]),    Equity(53 [ABMD]),
           Equity(62 [ABT]),    Equity(67 [ADSK]),    Equity(69 [ACAT]),
           Equity(76 [TAP]), 
       ...
       Equity(49096 [PTXP]), Equity(49117 [IVTY]), Equity(49126 [WING]),
       Equity(49131 [OESX]), Equity(49155 [FOGO]), Equity(49179 [CETX]),
       Equity(49200 [YECO]),  Equity(49229 [KHC]), Equity(49259 [ITUS]),
       Equity(49330 [SITO])],
      dtype='object', length=3382)
In [268]:
symbols = fundamental_data.minor_axis
price_history = get_pricing(symbols, fields='close_price', start_date=t1, end_date=t0)
price_history.head()
Out[268]:
Equity(2 [AA]) Equity(24 [AAPL]) Equity(31 [ABAX]) Equity(41 [ARCB]) Equity(52 [ABM]) Equity(53 [ABMD]) Equity(62 [ABT]) Equity(67 [ADSK]) Equity(69 [ACAT]) Equity(76 [TAP]) ... Equity(49096 [PTXP]) Equity(49117 [IVTY]) Equity(49126 [WING]) Equity(49131 [OESX]) Equity(49155 [FOGO]) Equity(49179 [CETX]) Equity(49200 [YECO]) Equity(49229 [KHC]) Equity(49259 [ITUS]) Equity(49330 [SITO])
2015-06-01 00:00:00+00:00 12.4100 130.53 53.0100 34.94 32.48 60.30 48.94 54.40 32.73 73.07 ... NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
2015-06-02 00:00:00+00:00 12.6299 129.96 52.9600 34.78 32.37 59.70 48.94 54.35 32.60 74.03 ... NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
2015-06-03 00:00:00+00:00 12.6100 130.16 53.0803 34.80 33.16 62.05 48.87 54.91 32.86 74.79 ... NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
2015-06-04 00:00:00+00:00 12.5100 129.47 52.8000 34.71 32.70 63.31 48.70 53.71 32.66 74.90 ... NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
2015-06-05 00:00:00+00:00 12.4100 128.65 53.0000 35.48 32.70 63.84 48.46 54.61 33.41 72.64 ... 19.5 NaN NaN NaN NaN NaN NaN NaN NaN NaN

5 rows × 3382 columns

In [269]:
prices_sp_all = get_pricing('SPY', fields='close_price', start_date=t4, end_date=t0)
prices_sp_index = pd.date_range(prices_sp_all.index[0], prices_sp_all.index[-1])
prices_sp_all = prices_sp_all.reindex(prices_sp_index, method='ffill')
dates = []
#items = fundamental_data.items
#for date_str in items[-5:]:
#    date_str = date_str + "-01"
#    dates.append(pd.Timestamp(date_str))
dates.append(pd.Timestamp(t4))
dates.append(pd.Timestamp(t3))
dates.append(pd.Timestamp(t2))
dates.append(pd.Timestamp(t1))
dates.append(pd.Timestamp(t0))
prices_sp = 10.0*prices_sp_all.loc[dates]
print prices_sp

returns_sp = ((prices_sp - prices_sp.shift(1)) / prices_sp.shift(1))[1:]
returns_sp
2014-09-02    2005.9
2014-12-01    2058.0
2015-03-02    2120.0
2015-06-01    2115.7
2015-09-01    1916.1
Name: Equity(8554 [SPY]), dtype: float64
Out[269]:
2014-12-01    0.025973
2015-03-02    0.030126
2015-06-01   -0.002028
2015-09-01   -0.094342
Name: Equity(8554 [SPY]), dtype: float64
In [270]:
mta = fundamental_data[-4:].loc[:,'total_debt'] + fundamental_data[-4:].loc[:,'market_cap']
nimta = fundamental_data[-4:].loc[:,'net_income'] / mta
tlmta = fundamental_data[-4:].loc[:,'total_debt'] / mta
cashmta = fundamental_data[-4:].loc[:,'cash_and_cash_equivalents'] / mta

print nimta.head()
print tlmta.head()
print cashmta.head()
                   2014-12-01  2015-03-02  2015-06-01  2015-09-01
Equity(2 [AA])       0.005088         NaN         NaN         NaN
Equity(24 [AAPL])    0.011583    0.022642    0.016865    0.015288
Equity(31 [ABAX])    0.004329    0.004247    0.009407    0.006526
Equity(41 [ARCB])    0.015355    0.011796    0.000670    0.021871
Equity(52 [ABM])     0.010545         NaN         NaN         NaN
                   2014-12-01  2015-03-02  2015-06-01  2015-09-01
Equity(2 [AA])       0.306891         NaN         NaN         NaN
Equity(24 [AAPL])    0.048282    0.045730    0.054526    0.077922
Equity(31 [ABAX])    0.000426    0.000364    0.000399    0.000425
Equity(41 [ARCB])    0.110939    0.116662    0.152603    0.176182
Equity(52 [ABM])     0.169426         NaN         NaN         NaN
                   2014-12-01  2015-03-02  2015-06-01  2015-09-01
Equity(2 [AA])       0.111721         NaN         NaN         NaN
Equity(24 [AAPL])    0.018938    0.024469    0.018008    0.021935
Equity(31 [ABAX])    0.067813    0.064781    0.088845    0.072662
Equity(41 [ARCB])    0.124750    0.127383    0.149230    0.210325
Equity(52 [ABM])     0.013371         NaN         NaN         NaN
In [271]:
prices = fundamental_data[-5:].loc[:,'market_cap'] / fundamental_data[-5:].loc[:,'shares_outstanding']
prices.head()
Out[271]:
2014-09-02 2014-12-01 2015-03-02 2015-06-01 2015-09-01
Equity(2 [AA]) 16.709994 17.220017 NaN NaN NaN
Equity(24 [AAPL]) 102.129973 118.625061 130.415039 132.044964 111.777061
Equity(31 [ABAX]) 47.159792 55.330138 61.460136 53.419850 47.253440
Equity(41 [ARCB]) 36.460051 43.629918 41.829977 36.250034 28.919441
Equity(52 [ABM]) 26.390025 27.410068 NaN NaN NaN
In [272]:
#Equity(24 [AAPL]) 	643.86 	100.750000 	112.5200 	126.37 	126.750000
# (100.750000 - 643.86) / 643.86
returns = ((prices - prices.shift(1, axis=1)) / prices.shift(1, axis=1)).iloc[:,1:]
returns.head()
Out[272]:
2014-12-01 2015-03-02 2015-06-01 2015-09-01
Equity(2 [AA]) 0.030522 NaN NaN NaN
Equity(24 [AAPL]) 0.161511 0.099389 0.012498 -0.153492
Equity(31 [ABAX]) 0.173248 0.110789 -0.130821 -0.115433
Equity(41 [ARCB]) 0.196650 -0.041255 -0.133396 -0.202223
Equity(52 [ABM]) 0.038653 NaN NaN NaN
In [273]:
exret = (np.log(returns.add(1))).sub(np.log(returns_sp.add(1)))
exret.head()
Out[273]:
2014-12-01 2015-03-02 2015-06-01 2015-09-01
Equity(2 [AA]) 0.004424 NaN NaN NaN
Equity(24 [AAPL]) 0.124080 0.065073 0.014451 -0.067542
Equity(31 [ABAX]) 0.134134 0.075390 -0.138176 -0.023563
Equity(41 [ARCB]) 0.153884 -0.071811 -0.141143 -0.126832
Equity(52 [ABM]) 0.012282 NaN NaN NaN
In [274]:
returns_daily = ((price_history - price_history.shift(1, axis=0)) / price_history.shift(1, axis=0)).iloc[1:]
n = len(returns_daily)
sigma = returns_daily.sub(returns_daily.mean()).pow(2).sum().multiply(252.0/(n-1.0)).pow(0.5)
sigma.describe()
Out[274]:
count    3382.000000
mean        0.473942
std         0.406541
min         0.000000
25%         0.268317
50%         0.387369
75%         0.577580
max        10.412462
dtype: float64
In [275]:
sp500_divisor = 9350070273.0
sp500_market_value = sp500_divisor * prices_sp[-1] * 10.0
rsize = np.log(fund_df0.loc['market_cap'] / sp500_market_value)
rsize.head()
Out[275]:
Equity(24 [AAPL])    -5.628393
Equity(31 [ABAX])   -12.027115
Equity(41 [ARCB])   -12.380895
Equity(53 [ABMD])   -10.677809
Equity(67 [ADSK])    -9.663930
Name: market_cap, dtype: float64
In [276]:
adjusted_book_value= fund_df0.loc['total_assets'] + 0.1*(fund_df0.loc['market_cap'] - fund_df0.loc['total_assets'])
#adjusted_book_value
mb = mta.div(adjusted_book_value, axis=0)
mb.head()
Out[276]:
2014-12-01 2015-03-02 2015-06-01 2015-09-01
Equity(2 [AA]) NaN NaN NaN NaN
Equity(24 [AAPL]) 2.356347 2.565952 2.593506 2.251126
Equity(31 [ABAX]) 3.649855 4.054512 3.524216 3.135882
Equity(41 [ARCB]) 1.112846 1.073848 0.969054 0.795215
Equity(52 [ABM]) NaN NaN NaN NaN
In [277]:
capped_prices = prices.iloc[:,-1]
capped_prices[capped_prices > 15] = 15
log_prices = np.log(capped_prices)
log_prices.head()
Out[277]:
Equity(2 [AA])           NaN
Equity(24 [AAPL])    2.70805
Equity(31 [ABAX])    2.70805
Equity(41 [ARCB])    2.70805
Equity(52 [ABM])         NaN
Name: 2015-09-01, dtype: float64
In [278]:
nimtaavg = 0.5333*nimta.iloc[:,-1] + 0.2666*nimta.iloc[:,-2] + 0.1333*nimta.iloc[:,-3] + 0.0666*nimta.iloc[:,-4]
nimtaavg.head()
Out[278]:
Equity(2 [AA])            NaN
Equity(24 [AAPL])    0.016439
Equity(31 [ABAX])    0.006843
Equity(41 [ARCB])    0.014437
Equity(52 [ABM])          NaN
dtype: float64
In [279]:
exretavg = 0.5333*exret.iloc[:,-1] + 0.2666*exret.iloc[:,-2] + 0.1333*exret.iloc[:,-3] + 0.0666*exret.iloc[:,-4]
exretavg.head()
Out[279]:
Equity(2 [AA])            NaN
Equity(24 [AAPL])   -0.015230
Equity(31 [ABAX])   -0.030421
Equity(41 [ARCB])   -0.104592
Equity(52 [ABM])          NaN
dtype: float64
In [280]:
# Descriptive Statistics

nimta_stats = nimta.iloc[:,-1].describe()
nimta_stats.name = 'NIMTA'

tlmta_stats = tlmta.iloc[:,-1].describe()
tlmta_stats.name = 'TLMTA'

exret_stats = exret.iloc[:,-1].describe()
exret_stats.name = 'EXRET'

rsize_stats = rsize.describe()
rsize_stats.name = 'RSIZE'

sigma_stats = sigma.describe()
sigma_stats.name = 'SIGMA'

cashmta_stats = cashmta.iloc[:,-1].describe()
cashmta_stats.name = 'CASHMTA'

mb_stats = mb.iloc[:,-1].describe()
mb_stats.name = 'MB'

log_prices_stats = log_prices.describe()
log_prices_stats.name = 'PRICE'

pd.concat([nimta_stats, tlmta_stats, exret_stats, rsize_stats, sigma_stats, cashmta_stats, mb_stats, log_prices_stats], axis=1)

# Summary Statistics as reported in the Paper (Table II)
#        NIMTA   TLMTA    EXRET     RSIZE   SIGMA  CASHMTA     MB   PRICE
# Mean   0.000   0.445   -0.011   -10.456   0.562    0.084  2.041   2.019
# Median 0.006   0.427   -0.009   -10.570   0.471    0.045  1.557   2.474
Out[280]:
NIMTA TLMTA EXRET RSIZE SIGMA CASHMTA MB PRICE
count 1676.000000 1677.000000 1712.000000 1757.000000 3382.000000 1677.000000 1677.000000 1757.000000
mean -0.020178 0.177275 -0.029887 -13.100094 0.473942 0.132321 1.782424 1.956159
std 0.105720 0.222488 0.307096 1.996604 0.406541 0.147111 1.168670 1.032227
min -2.670819 0.000000 -1.896079 -20.068121 0.000000 0.000014 0.088748 -6.004885
25% -0.023189 0.005372 -0.171968 -14.506248 0.268317 0.031468 0.959132 1.444560
50% 0.001905 0.090838 -0.008978 -13.168160 0.387369 0.086265 1.453468 2.509932
75% 0.010307 0.263114 0.124292 -11.790754 0.577580 0.181938 2.288715 2.708050
max 0.471589 0.998121 2.045751 -5.628393 10.412462 1.157933 8.016513 2.708050
In [281]:
lpfd = -20.12*nimtaavg +1.60*tlmta.iloc[:,-1] -7.88*exretavg +1.55*sigma -0.005*rsize -2.27*cashmta.iloc[:,-1] + 0.07*mb.iloc[:,-1] - 0.09*log_prices - 8.87
nimtaavg
lpfd.head()
Out[281]:
Equity(2 [AA])            NaN
Equity(24 [AAPL])   -8.597363
Equity(31 [ABAX])   -8.373510
Equity(41 [ARCB])   -8.141505
Equity(52 [ABM])          NaN
dtype: float64
In [283]:
# PFD (Probability of Financial Distress). The probability of financial distress ranges between zero and 100 percent.
# Zero implies no probability of financial distress in the next 12 months, while 100 percent suggests certain financial distress.
pfd = 1.0 / (1.0 + np.exp(-lpfd))
distressed_companies = pfd[pfd > 0.50].order(ascending=False)
for equity in distressed_companies.index:
    work = "%-5s %-40s %.2f%%" % (equity.symbol, equity.asset_name, 100.0*distressed_companies[equity])
    print(work)
    
SPEX  SPHERIX INC                              100.00%
PRSN  PERSEON CORP                             100.00%
XGTI  XG TECHNOLOGY INC                        100.00%
ASTI  ASCENT SOLAR TECHNOLOGIES INC            100.00%
DRYS  DRYSHIPS INC                             99.98%
ESCR  ESCALERA RESOURCES CO                    99.88%
VPCO  VAPOR CORP                               99.75%
INPH  INTERPHASE CORP                          99.70%
UNXL  UNI-PIXEL INC                            99.60%
DXM   DEX MEDIA INC                            99.33%
USEG  U S ENERGY CORP                          99.02%
AQXP  AQUINOX PHARMACEUTICALS INC              98.83%
CNIT  CHINA INFORMATION TECHNOLOGY I           98.70%
FNCX  FUNCTION(X) INC                          98.11%
LINC  LINCOLN EDUCATIONAL SERVICES CORP        93.73%
WRES  WARREN RESOURCES INC                     91.21%
SWSH  SWISHER HYGIENE INC                      90.38%
LNCO  LINN CO LLC                              89.25%
XOMA  XOMA CORP                                88.76%
SFXE  SFX ENTERTAINMENT INC                    86.57%
NETE  NET ELEMENT INC                          83.78%
ZAZA  ZAZA ENERGY CORP                         83.24%
VTL   VITAL THERAPIES INC                      81.90%
ADAT  AUTHENTIDATE HOLDING CORP                79.91%
RJET  REPUBLIC AIRWAYS HOLDINGS INC            77.62%
AMCF  ANDATEE CHINA MARINE FUEL SERVICES CORP  76.29%
LINE  LINN ENERGY LLC                          75.20%
INTX  INTERSECTIONS INC                        74.69%
ETRM  ENTEROMEDICS INC                         72.46%
PRKR  PARKERVISION INC                         62.73%
TRCH  TORCHLIGHT ENERGY RESOURCES INC          61.84%
MPET  MAGELLAN PETROLEUM CORP                  55.14%
ESSX  ESSEX RENTAL CORP                        53.95%
In [ ]:
 
In [ ]: