Skip to content

Commit

Permalink
financial signals support for banks and insurances
Browse files Browse the repository at this point in the history
  • Loading branch information
thf24 committed Sep 13, 2021
1 parent c68f013 commit 3b0e5be
Show file tree
Hide file tree
Showing 5 changed files with 113 additions and 30 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# Changes to the SimFin Python package

## Version 0.8.2 (2021-09-13)

Added financial signals support for banks and insurance datasets.


## Version 0.8.0 (2020-07-08)

Public beta version.
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

# This is also defined in simfin/__init__.py and must be
# updated in both places.
MY_VERSION = '0.8.1'
MY_VERSION = '0.8.2'

setup(
name='simfin',
Expand Down
2 changes: 1 addition & 1 deletion simfin/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# This is also defined in setup.py and must be updated in both places.
__version__ = "0.8.1"
__version__ = "0.8.2"

# Expose the following as top-level imports.

Expand Down
18 changes: 18 additions & 0 deletions simfin/hubs.py
Original file line number Diff line number Diff line change
Expand Up @@ -649,11 +649,20 @@ def fin_signals(self, variant='daily', func=None):
cache_args = self._cache_args(datasets=datasets,
cache_ids=cache_ids)

# Check whether special datasets for banks and insurances should be used.
banks = False
insurance = False
if self._dataset_extension == "-banks":
banks = True
elif self._dataset_extension == "insurance":
insurance = True

# Calculate the signals, or load the DataFrame from the disk-cache.
df_result = fin_signals(df_income_ttm=df_income_ttm,
df_balance_ttm=df_balance_ttm,
df_cashflow_ttm=df_cashflow_ttm,
df_prices=df_prices, func=func,
banks=banks, insurance=insurance,
**self._signal_args, **cache_args)

return df_result
Expand Down Expand Up @@ -709,12 +718,21 @@ def val_signals(self, variant='daily', func=None,
cache_args = self._cache_args(datasets=datasets,
cache_ids=cache_ids)

# Check whether special datasets for banks and insurances should be used.
banks = False
insurance = False
if self._dataset_extension == "-banks":
banks = True
elif self._dataset_extension == "insurance":
insurance = True

# Calculate the signals, or load the DataFrame from the disk-cache.
df_result = val_signals(df_prices=df_prices,
df_income_ttm=df_income_ttm,
df_balance_ttm=df_balance_ttm,
df_cashflow_ttm=df_cashflow_ttm,
shares_index=shares_index, func=func,
banks=banks, insurance=insurance,
**self._signal_args, **cache_args)

return df_result
Expand Down
116 changes: 88 additions & 28 deletions simfin/signals.py
Original file line number Diff line number Diff line change
Expand Up @@ -280,7 +280,7 @@ def _signals(df):
@cache
def fin_signals(df_income_ttm, df_balance_ttm, df_cashflow_ttm, df_prices=None,
offset=None, func=None, fill_method='ffill',
date_index=REPORT_DATE, group_index=TICKER):
date_index=REPORT_DATE, group_index=TICKER, banks=False, insurance=False):
"""
Calculate financial signals such as Net Profit Margin, Debt Ratio, ROA,
etc. for all stocks in the given DataFrames.
Expand Down Expand Up @@ -339,6 +339,13 @@ def fin_signals(df_income_ttm, df_balance_ttm, df_cashflow_ttm, df_prices=None,
index-column. By default this is TICKER but it could also be e.g.
SIMFIN_ID if you are using that as an index in your DataFrame.
:param banks:
Boolean whether to use the special datasets for banks.
:param insurance:
Boolean whether to use the special datasets for insurance
companies.
:return:
Pandas DataFrame with financial signals.
"""
Expand All @@ -353,31 +360,45 @@ def _signals(df):
df_signals[NET_PROFIT_MARGIN] = df[NET_INCOME] / df[REVENUE]

# Gross Profit Margin.
df_signals[GROSS_PROFIT_MARGIN] = df[GROSS_PROFIT] / df[REVENUE]
# Note: Not available for banks or insurances.
if not banks and not insurance:
df_signals[GROSS_PROFIT_MARGIN] = df[GROSS_PROFIT] / df[REVENUE]

# R&D / Revenue.
# Note: RESEARCH_DEV must be negated.
df_signals[RD_REVENUE] = -df[RESEARCH_DEV] / df[REVENUE]
# Note: Not available for banks or insurances.
if not banks and not insurance:
df_signals[RD_REVENUE] = -df[RESEARCH_DEV] / df[REVENUE]

# R&D / Gross Profit.
# Note: RESEARCH_DEV must be negated.
df_signals[RD_GROSS_PROFIT] = -df[RESEARCH_DEV] / df[GROSS_PROFIT]
# Note: Not available for banks or insurances.
if not banks and not insurance:
df_signals[RD_GROSS_PROFIT] = -df[RESEARCH_DEV] / df[GROSS_PROFIT]

# Return on Research Capital (RORC).
# Note: RESEARCH_DEV must be negated.
df_signals[RORC] = df[GROSS_PROFIT] / -df[RESEARCH_DEV]
# Note: Not available for banks or insurances.
if not banks and not insurance:
df_signals[RORC] = df[GROSS_PROFIT] / -df[RESEARCH_DEV]

# Interest Coverage.
# Note: INTEREST_EXP_NET must be negated.
df_signals[INTEREST_COV] = df[OPERATING_INCOME] / -df[INTEREST_EXP_NET]
# Note: Not available for banks or insurances.
if not banks and not insurance:
df_signals[INTEREST_COV] = df[OPERATING_INCOME] / -df[INTEREST_EXP_NET]

# Current Ratio = Current Assets / Current Liabilities.
df_signals[CURRENT_RATIO] = df[TOTAL_CUR_ASSETS] / df[TOTAL_CUR_LIAB]
# Note: Not available for banks or insurances.
if not banks and not insurance:
df_signals[CURRENT_RATIO] = df[TOTAL_CUR_ASSETS] / df[TOTAL_CUR_LIAB]

#: Quick Ratio = (Cash + Equiv. + ST Inv. + Recv.) / Current Liab.
df_signals[QUICK_RATIO] = \
(df[CASH_EQUIV_ST_INVEST] + df[ACC_NOTES_RECV].fillna(0.0)) \
/ df[TOTAL_CUR_LIAB]
# Note: Not available for banks or insurances.
if not banks and not insurance:
df_signals[QUICK_RATIO] = \
(df[CASH_EQUIV_ST_INVEST] + df[ACC_NOTES_RECV].fillna(0.0)) \
/ df[TOTAL_CUR_LIAB]

# Debt Ratio = (Short-term Debt + Long-term Debt) / Total Assets.
df_signals[DEBT_RATIO] = (df[ST_DEBT] + df[LT_DEBT]) / df[TOTAL_ASSETS]
Expand All @@ -399,7 +420,9 @@ def _signals(df):
df_signals[ASSET_TURNOVER] = df[REVENUE] / df[TOTAL_ASSETS]

# Inventory Turnover = Revenue / Inventory. See note above.
df_signals[INVENTORY_TURNOVER] = df[REVENUE] / df[INVENTORIES]
# Note: Not available for banks or insurances.
if not banks and not insurance:
df_signals[INVENTORY_TURNOVER] = df[REVENUE] / df[INVENTORIES]

# Payout Ratio = Dividends / Free Cash Flow
# Note the negation because DIVIDENDS_PAID is negative.
Expand All @@ -418,8 +441,10 @@ def _signals(df):

# Net Acquisitions & Divestitures / Total Assets.
# Note the negation because NET_CASH_ACQ_DIVEST is usually negative.
df_signals[ACQ_ASSETS_RATIO] = \
-df[NET_CASH_ACQ_DIVEST] / df[TOTAL_ASSETS]
# Note: Not available for insurances.
if not insurance:
df_signals[ACQ_ASSETS_RATIO] = \
-df[NET_CASH_ACQ_DIVEST] / df[TOTAL_ASSETS]

# Capital Expenditures / (Depreciation + Amortization).
# Note the negation because CAPEX is negative.
Expand All @@ -431,19 +456,34 @@ def _signals(df):
return df_signals

# Get relevant data from Income Statements.
columns = [REVENUE, GROSS_PROFIT, OPERATING_INCOME, INTEREST_EXP_NET,
NET_INCOME, RESEARCH_DEV]
if banks or insurance:
columns = [REVENUE, OPERATING_INCOME,
NET_INCOME]
else:
columns = [REVENUE, GROSS_PROFIT, OPERATING_INCOME, INTEREST_EXP_NET,
NET_INCOME, RESEARCH_DEV]
df1 = df_income_ttm[columns]

# Get relevant data from Balance Sheets.
columns = [TOTAL_ASSETS, TOTAL_CUR_ASSETS, TOTAL_CUR_LIAB, TOTAL_EQUITY,
ST_DEBT, LT_DEBT, INVENTORIES, CASH_EQUIV_ST_INVEST,
ACC_NOTES_RECV]
if banks or insurance:
columns = [TOTAL_ASSETS, TOTAL_EQUITY,
ST_DEBT, LT_DEBT]
else:
columns = [TOTAL_ASSETS, TOTAL_CUR_ASSETS, TOTAL_CUR_LIAB, TOTAL_EQUITY,
ST_DEBT, LT_DEBT, INVENTORIES, CASH_EQUIV_ST_INVEST,
ACC_NOTES_RECV]
df2 = df_balance_ttm[columns]

# Get relevant data from Cash-Flow Statements.
columns = [DIVIDENDS_PAID, CASH_REPURCHASE_EQUITY, NET_CASH_ACQ_DIVEST,
CAPEX, DEPR_AMOR]
if banks:
columns = [DIVIDENDS_PAID, CASH_REPURCHASE_EQUITY, NET_CASH_ACQ_DIVEST,
CAPEX, DEPR_AMOR]
elif insurance:
columns = [DIVIDENDS_PAID, CASH_REPURCHASE_EQUITY,
CAPEX, DEPR_AMOR]
else:
columns = [DIVIDENDS_PAID, CASH_REPURCHASE_EQUITY, NET_CASH_ACQ_DIVEST,
CAPEX, DEPR_AMOR]
df3 = df_cashflow_ttm[columns]

# Calculate Free Cash Flow.
Expand Down Expand Up @@ -659,7 +699,7 @@ def growth_signals(df_income_ttm, df_income_qrt,
def val_signals(df_prices, df_income_ttm, df_balance_ttm, df_cashflow_ttm,
fill_method='ffill', offset=None, func=None,
date_index=REPORT_DATE, shares_index=SHARES_DILUTED,
group_index=TICKER):
group_index=TICKER, banks=False, insurance=False):
"""
Calculate valuation signals such as P/E and P/Sales ratios for all stocks
in the given DataFrames.
Expand Down Expand Up @@ -722,6 +762,13 @@ def val_signals(df_prices, df_income_ttm, df_balance_ttm, df_cashflow_ttm,
index-column. By default this is TICKER but it could also be e.g.
SIMFIN_ID if you are using that as an index in your DataFrame.
:param banks:
Boolean whether to use the special datasets for banks.
:param insurance:
Boolean whether to use the special datasets for insurance
companies.
:return:
Pandas DataFrame with valuation signals.
"""
Expand All @@ -731,8 +778,11 @@ def val_signals(df_prices, df_income_ttm, df_balance_ttm, df_cashflow_ttm,
df_inc = df_income_ttm[columns]

# Get the required data from the Balance Sheets.
columns = [TOTAL_CUR_ASSETS, CASH_EQUIV_ST_INVEST, ACC_NOTES_RECV,
INVENTORIES, TOTAL_LIABILITIES, TOTAL_EQUITY]
if banks or insurance:
columns = [TOTAL_ASSETS, TOTAL_LIABILITIES, TOTAL_EQUITY]
else:
columns = [TOTAL_CUR_ASSETS, CASH_EQUIV_ST_INVEST, ACC_NOTES_RECV,
INVENTORIES, TOTAL_LIABILITIES, TOTAL_EQUITY]
df_bal = df_balance_ttm[columns]

# Get the required data from the Cash-Flow Statements.
Expand All @@ -747,8 +797,12 @@ def val_signals(df_prices, df_income_ttm, df_balance_ttm, df_cashflow_ttm,
# This is only TTM data with 4 data-points per year, so it is
# faster than calculating it for the daily data-points below.
df[FCF] = free_cash_flow(df_cashflow_ttm)
df[NCAV] = ncav(df_balance_ttm)
df[NETNET] = netnet(df_balance_ttm)
# Note: Not for banks and insurances.
if not banks and not insurance:
df[NCAV] = ncav(df_balance_ttm)
# Note: Not for banks and insurances.
if not banks and not insurance:
df[NETNET] = netnet(df_balance_ttm)

# Add offset / lag to the index-dates of the financial data.
if offset is not None:
Expand Down Expand Up @@ -792,17 +846,23 @@ def val_signals(df_prices, df_income_ttm, df_balance_ttm, df_cashflow_ttm,

# Calculate Price / Net Current Asset Value (NCAV).
# This measures the share-price relative to estimated liquidation value.
df_signals[P_NCAV] = df_price / df_daily[NCAV]
# Note: Not for banks and insurances.
if not banks and not insurance:
df_signals[P_NCAV] = df_price / df_daily[NCAV]

# Calculate Price / Net-Net Working Capital (NNWC aka. NetNet).
# This measures the share-price relative to a more conservative estimate
# of liquidation value, which values the Receivables and Inventories at
# a discount to their book-value.
df_signals[P_NETNET] = df_price / df_daily[NETNET]
# Note: Not for banks and insurances.
if not banks and not insurance:
df_signals[P_NETNET] = df_price / df_daily[NETNET]

# Calculate Price / (Cash + Equivalents + Short-Term Investments)
# This can be used to screen for companies that might be takeover targets.
df_signals[P_CASH] = df_price / df_daily[CASH_EQUIV_ST_INVEST]
# Note: Not for banks and insurances.
if not banks and not insurance:
df_signals[P_CASH] = df_price / df_daily[CASH_EQUIV_ST_INVEST]

# Calculate Earnings Yield (inverse of the P/E ratio).
df_signals[EARNINGS_YIELD] = df_daily[NET_INCOME_COMMON] / df_price
Expand Down

0 comments on commit 3b0e5be

Please sign in to comment.