Fundamental analysis

In [ ]:
# Import basic libraries for manipulating data.

# Please refer to xarray.pydata.org for xarray documentation.

# xarray works optimally with N-dimensional datasets in Python
# and is well suited for financial datasets with labels "time",
# "field" and "asset". xarray data structures can also be easily
# converted to pandas dataframes.

import xarray as xr

import numpy as np
import pandas as pd

# Import quantnet libraries.

import qnt.data as qndata          # data loading and manipulation
import qnt.stepper as qnstepper    # strategy definition
import qnt.stats as qnstats        # key statistics
import qnt.graph as qngraph        # graphical tools
import qnt.forward_looking as qnfl # forward looking checking
import qnt.ta as qnta              # TA functions

# display function for fancy displaying:
from IPython.display import display
import json
import time

import datetime as dt
In [ ]:
# Load all available asset names since given date.

assets = qndata.load_assets(tail=dt.timedelta(days=5*365))

assets_names = [i["id"] for i in assets]

# Load all available data since given date.

# It is possible to set a max_date in the call in order to
# develop the system on a limited in-sample period and later
# test the system on unseen data after max_date.

# A submission will be accepted only if no max_date is set,
# as submissions will be evaluated on live data on a daily basis.

data = qndata.load_data(tail=dt.timedelta(days=5*365),
                        dims=("time", "field", "asset"),
                        assets=assets_names,
                        forward_order=True)
In [ ]:
# A buy-and-hold strategy on liquid assets allocates
# constant fractions of capital to all liquid assets.
# Here xarray data structures are converted to pandas
# dataframes for simplicity in order to describe the
# development process.

# xarray.DataArray are converted to pandas dataframes:
is_liquid = data.loc[:,"is_liquid",:]

# set and normalize weights:
weights = is_liquid / is_liquid.sum('asset').fillna(0.0)

# display statistics for B'n'H strategy
    
stat = qnstats.calc_stat(data, weights, slippage_factor=0.05)

display(stat.to_pandas().tail())

Fundamental data

We will use some fundamental indicators to improve this strategy.

At first, let's retrieve the data from the fundamental database.

You can also discover available attributes here: http://xbrlview.fasb.org/yeti/resources/yeti-gwt/Yeti.jsp (us-gaap taxonomy)

In [ ]:
# we will use this function to load parsed XBRL fillings
help(qndata.load_secgov_forms)
In [ ]:
# build dict cik -> asset
ciks = [(a['cik'], a) for a in assets if a['cik'] is not None]
ciks = dict(ciks)

# define result array
fundamental = xr.concat(
    [data.sel(field='close')] * 4, 
    pd.Index(['assets', 'liabilities', 'shares', 'eps'], name='field')
)
fundamental[:] = np.nan


def extract_last_fact(facts, name):
    res = tuple(f for f in facts if f['name'] == name)
    if len(res) > 0:
        res = max(res, key = lambda f: f['period']['value'])
        res = res['value']
    else:    
        res = None 
    return res


print_first_record = True
progress = 0
st = time.time()

for form in qndata.load_secgov_forms(
    ciks=list(ciks.keys()), # load only liquid ciks
    types=['10-Q'], # only quarter reports 
    facts=[
        'us-gaap:EarningsPerShareDiluted',
        'us-gaap:Liabilities', 
        'us-gaap:Assets',
        'us-gaap:CommonStockSharesOutstanding'
    ],
    skip_segment = True,
    tail=dt.timedelta(days=5*365)
):
    facts = form['facts']
    
    # print first report to analyze the structure
    if len(facts) > 0 and print_first_record:
        print("The report example:")
        display(form)
        print_first_record = False
        
    # display progress
    progress += 1
    if progress % 500 == 0:
        print("Progress:", progress, form['date'], time.time() - st)
    
    # process report
    asset_id = ciks[form['cik']]['id']
    if asset_id not in data.asset:
        continue
    
    date = form['date']
    date = fundamental.time.loc[date:]
    if len(date) < 1:
        print("wrong date", form['date'])
        continue
    date = date[0].values
    
    fundamental.loc[{'asset':asset_id, 'time':date, 'field':'shares'}] \
            = extract_last_fact(facts, 'us-gaap:CommonStockSharesOutstanding')
    
    fundamental.loc[{'asset':asset_id, 'time':date, 'field':'eps'}] \
            = extract_last_fact(facts, 'us-gaap:EarningsPerShareDiluted')
    
    fundamental.loc[{'asset':asset_id, 'time':date, 'field':'liabilities'}] \
            = extract_last_fact(facts, 'us-gaap:Liabilities')
    
    fundamental.loc[{'asset':asset_id, 'time':date, 'field':'assets'}] \
            = extract_last_fact(facts, 'us-gaap:Assets')
    
print('Loaded')
In [ ]:
# now we will prepare some ratios
ratios = xr.concat(
    [data.sel(field='close')] * 4, 
    pd.Index(['price/earnings', 'price/equity', 'liabilites/equity', 'market_cap'], name='field')
)

eps_y = qnta.sma(fundamental.sel(field='eps'), 4) * 4
ratios.loc[{'field':'price/earnings'}] = data.sel(field='close') / eps_y#.ffill('time')

equity = fundamental.sel(field='assets') - fundamental.sel(field='liabilities')
equity_per_share = equity/fundamental.sel(field='shares')

ratios.loc[{'field':'price/equity'}] = data.sel(field='close') / equity_per_share#.ffill('time')
ratios.loc[{'field':'liabilites/equity'}] = fundamental.sel(field='liabilities') / equity#.ffill('time')
ratios.loc[{'field':'market_cap'}] = data.sel(field='close') * fundamental.sel(field='shares')#.ffill('time')

ratios = ratios.ffill('time')
In [ ]:
# we will use these ratios to build the output
output = data.sel(field="is_liquid")

output = output.where(ratios.sel(field='price/earnings') > 4)
output = output.where(ratios.sel(field='price/earnings') < 25)

output = output.where(ratios.sel(field='price/equity') > 0.7)
output = output.where(ratios.sel(field='price/equity') < 1.3)

output = output.where(ratios.sel(field='liabilites/equity') > 2)
output = output.where(ratios.sel(field='liabilites/equity') < 8)

output = output.where(ratios.sel(field='market_cap').rank('asset') > 50)

output = output.fillna(0)
output /= output.sum('asset')
output = xr.where(output > 0.1, 0.1, output)

# display stats
stat = qnstats.calc_stat(data, output, slippage_factor=0.05)

display(stat.to_pandas().tail())

# print(output.isel(time=-1).dropna('asset').asset.values)

Statistics

In [ ]:
def print_stat(stat):
    """Prints selected statistical key indicators:
       - the global Sharpe ratio of the strategy;
       - the global mean profit;
       - the global volatility;
       - the maximum drawdown.

       Note that Sharpe ratio, mean profit and volatility
       apply to  max simulation period, and not to the
       rolling basis of 3 years.
    """

    days = len(stat.coords["time"])
    
    returns = stat.loc[:, "relative_return"]

    equity = stat.loc[:, "equity"]
    
    sharpe_ratio = qnstats.calc_sharpe_ratio_annualized(
        returns,
        max_periods=days,
        min_periods=days).to_pandas().values[-1]

    profit = (qnstats.calc_mean_return_annualized(
        returns,
        max_periods=days,
        min_periods=days).to_pandas().values[-1])*100.0

    volatility = (qnstats.calc_volatility_annualized(
        returns,
        max_periods=days,
        min_periods=days).to_pandas().values[-1])*100.0

    max_ddown = (qnstats.calc_max_drawdown(
        qnstats.calc_underwater(equity)).to_pandas().values[-1])*100.0

    print("Sharpe Ratio         : ", "{0:.3f}".format(sharpe_ratio))
    print("Mean Return [%]      : ", "{0:.3f}".format(profit))
    print("Volatility [%]       : ", "{0:.3f}".format(volatility))
    print("Maximum Drawdown [%] : ", "{0:.3f}".format(-max_ddown))

print_stat(stat)
In [ ]:
# show plot with profit and losses:
performance = stat.to_pandas()["equity"].iloc[(252*3):]
qngraph.make_plot_filled(performance.index, performance, name="PnL (Equity)", type="log")
In [ ]:
# show underwater chart:
UWchart = stat.to_pandas()["underwater"].iloc[(252*3):]
qngraph.make_plot_filled(UWchart.index, UWchart, color="darkred", name="Underwater Chart", range_max=0)
In [ ]:
# show rolling Sharpe ratio on a 3-year basis:
SRchart = stat.to_pandas()["sharpe_ratio"].iloc[(252*3):]
qngraph.make_plot_filled(SRchart.index, SRchart, color="#F442C5", name="Rolling SR")
In [ ]:
# show bias chart:
biaschart = stat.to_pandas()["bias"].iloc[(252*3):]
qngraph.make_plot_filled(biaschart.index, biaschart, color="#5A6351", name="Bias Chart")

Write output

In [ ]:
# Finally, we write the last mandatory step for submission,
# namely writing output to file:

qndata.write_output(output)