Skip to main content

universal portfolio backtester

Project description

portfolio_backtester

  • portfolio_backtester is a Python library for backtesting built-in or user-defined portfolio construction strategies.

Given a portfolio construction strategy (a function that takes in stock-related data and returns portfolio weights), be

it pre-built-in or user-defined, and the data that the user wish the strategy to be tested on, the library can calculate several evaluation metrics of the portfolio, including

net returns, sharpe ratio, certainty equivalent returns, turnover, etc.

  • portfolio_backtester is fast and efficient, compatible with most dataframe and numpy objects and functions

The library is built on numpy and pandas, and mostly utilizes matrix operations to handle heavy calculations

to make the library light, fast and efficient.

  • portfolio_backtester is flexible and versatile

Various inputs can be modified to suit the needs of different strategies and backtesting scenarios,

such as change of frequency to enable testing on different timescales, price-impact models and

transaction costs to gauge the impact of trading activities on the strategy,

ability to take in extra data and trace back prior portfolios

to meet the need of portfolio construction strategy, etc.

  • portfolio_backtester is setting a universal standard for portfolio performance evaluation

Since the library is flexible enough to be compatible with most portfolio construction strategies, it aims

to provide benchmark performance evaluation for all portfolio construction

strategies, so that users can consistently compare the performance of different strategies

under varoius scenarios, without worrying about the possibility of cheating,

e.g. using future informationto build portfolio in the current period. By using one universal

standard, the library enforces a fair game for different strategies by competing on a same level

of battleground.

Requirements

The following libraries are required for the use of portfolio_backtester

Installation

Use the package manager pip to install portfolio_backtester.

pip install portfolio_backtester

Get started

1. Preparation

Before calling the library, user should decide whether he/she wants to use built-in portfolio construction strategies, or

defines one himself/herself. If it is the latter, it needs to be realised in Python function with signature

def strategy_name(list_df, extra_data, historical_portfolio): 

    pass

    return w

where list_df is the list of dataframes that the strategy works with in each period, the sequence of which

should corresponds to involved_data_type in the model initialization step later in this section.

extra_data and historical_portfolio are optional arguments if the strategy needs extra data

or need to refer to past portfolios to construct a new portfolio for the current period. The function should

return a portfolio weight allocation, denoted by w here, that sums up to 1, in the form of np.ndarray or

pd.Series or pd.DataFrame.

Note 1: The function must only return ONE weight allocation for each period.

Note 2: The sequence of assets in the return of the function should be consistent with the sequence of assets

in the data on which the strategy is to be tested throughout the whole process.

Other than the strategy function, the user also needs to prepare the data for the strategy to

be tested on, as well as the extra data for specific strategies to function, if applicable.

Note 1: The library provides several built-in datasets for users' reference. Please use fetch_data

function to gain access to these data

Note 2: If the strategy requires extra data to function, make sure that the index of extra data matches that

of the data for the strategy to be tested on.

2. Initialization

After preparation step, the user must first build the model using backtest_model from the library. It should

follow the form of:

from portfolio_backtester import backtest_model



strategy_model=backtest_model(strategy_name, involved_data_type, 

                              need_extra_data=False, trace_back=False, name='Unnamed')

where strategy_name and involved_data_type are necessary arguments, need_extra_data and trace_back

and name are optional arguments. strategy_name is the portfolio construction function defined in step 1.

involved_data_type is a list of str variables that indicate the types of dataframes that the

strategy function uses, the str variables are chosen from {'price','return','ex_return'}.

As mentioned above, the sequence in involved_data_type should match that of the list_df in the portfolio construction function

strategy_name. For example, if list_df=[price_data, return_data, ex_return_data] in strategy_name from

preparation, then involved_data_type=['price','return','ex_return'] in backtest_model from initialization.

Note: The library has some built-in ready-to-be-tested models for users' reference.

Now it is all set up! The model object is ready to be tested on selected data.

3. Test

To test the model on selected data, simply call the function backtest on the model object

strategy_model.backtest(data, freq_data, 

                        volume=pd.DataFrame(), data_type='price', rfr=pd.Series(dtype='float'),

                        interval=1, window=60,

                        freq_strategy='D',

                        price_impact=False, tc_a=0, tc_b=0, tc_f=0, c=1, initial_wealth=1E6,

                        extra_data=pd.DataFrame(), price_impact_model='default')

although only data and freq_data are necessary arguments, the user needs to make sure

that

  1. data_type actually matches the type of data that the model is tested on;

  2. freq_strategy matches the frequency at which the user wants the strategy to rebalance the portfolio.

For available choices and specific requirements of each argument, please refer to the full manual for detailed explaination

and description.

4. Results & Analysis

There are several outputs and metrics available. The user can simply call these functions using the model object. A few

examples are shown below:

strategy_model.general_performance()

strategy_model.get_net_returns()

strategy_model.get_ceq(x=1) # x is the risk aversion factor

Examples

  1. Naive 1/N strategy

A simple example with naive 1/N portfolio construction strategy, no extra data needed, do not require

trace back of historical portfolios:

# 1. Preparation

# prepare the data

import numpy as np

import pandas as pd

from portfolio_backtester import fetch_data

data = fetch_data('SPSectors.csv')              # We are using built-in datasets in the library



# prepare the portfolio construction function

def __naive_alloc(list_df):

    df = list_df[0]

    n = df.shape[1]

    res = np.ones(n) / n

    return res



# 2. Initialization

from portfolio_backtester import backtest_model



naive_alloc = backtest_model(__naive_alloc, ['ex_return'], name='naive allocation portfolio')



# Note: `naive_alloc` is actually one of the built-in portfolio construction models in the library

# so the user can skip step 1 & 2 by calling in the model from the library directly and go straight to step 3

# The user still needs to prepare the data!

from portfolio_backtester import naive_alloc



# 3. Test

# Most basic version of testing, no change of frequency, price impact not included, no transaction cost, etc.

naive_alloc.backtest(data.iloc[:,1:],'M',window=120,rfr=data.iloc[:,0],

                     data_type='ex_return',freq_strategy='M')

Here are a few showcases of results the user can get:

strategy name                              naive allocation portfolio

Price impact                                                      OFF

Start date of portfolio                           1991-02-28 00:00:00

End date of portfolio                             2002-12-31 00:00:00

Frequency of rebalance                                        1 Month

Duration                                                  143 periods

Final Portfolio Return (%)                                  422.5701%

Peak Portfolio Return (%)                                   524.5902%

Bottom Portfolio Return (%)                                 107.3055%

Historical Volatiltiy (%)                                     4.1633%

Sharpe Ratio                                                   0.1799

Sortino Ratio                                                  0.2701

Calmar Ratio                                                   0.0094

Max. Drawdown (%)                                            79.5449%

Max. Drawdown Duration                             3745 days 00:00:00

% of positive-net-excess-return periods                      62.9371%

% of positive-net-return periods                             64.3357%

Average turnover (%)                                          3.0853%

Total turnover (%)                                          444.2821%

95% VaR on net-excess returns                                -5.5679%

95% VaR on net returns                                       -5.1337%

dtype: object



>>> naive_alloc.get_net_returns()

Date

1991-02-28    0.073055

1991-03-28    0.030464

1991-04-30   -0.001455

1991-05-31    0.041645

1991-06-28   -0.044282

                ...   

2002-08-30    0.023282

2002-09-30   -0.112382

2002-10-31    0.080945

2002-11-29    0.080691

2002-12-31   -0.031318

Length: 143, dtype: float64



>>> naive_alloc.get_ceq(1)

0.006603752164323863



>>> naive_alloc.get_ceq(np.array([1,2,3]))

array([0.00660375, 0.00574349, 0.00488322])
  1. Fama-French 3-factor strategy

This is a more advanced and sophisticated portfolio construction strategy. It requires factor data as *extra

data*, but does not need to trace back historical portfolios.

# 1. Preperation

# prepare the data

import numpy as np

import pandas as pd

from portfolio_backtester import fetch_data

data=fetch_data('SPSectors.csv')



extra_data=fetch_data('FF3_monthly_192607-202106.csv')     # extra factor data that is also included in the library

start = '1981-01'

end = '2002-12'

extra_data = extra_data.loc[start:end]

extra_data.index = data.index                      # need to make sure that the index of the two dataframes match



# prepare the portfolio construction function

from sklearn.linear_model import LinearRegression

def __FF3(list_df, extra_data):

    df = list_df[0]



    X = extra_data

    y = df

    reg = LinearRegression(fit_intercept=True).fit(X, y)

    beta = reg.coef_

    var_epi = (y - reg.predict(X)).var(axis=0)

    cov = np.dot(np.dot(beta, X.cov()), beta.T) + np.diag(var_epi)



    in_cov = np.linalg.inv(cov)

    n = df.shape[1]

    w = np.dot(in_cov, np.ones(n))

    w /= w.sum()

    return w



# 2. Initialization

from portfolio_backtester import backtest_model



FF_3_factor_model = backtest_model(__FF3, ['ex_return'], need_extra_data=True,

                                   name='Fama-French 3-factor model portfolio')



# Note: this model is also one of the built-in models in the library, user can call it directly

# Again, user still needs to prepare the data

from portfolio_backtester import FF_3_factor_model



# 3. Test

# Add transaction cost into the backtest

FF_3_factor_model.backtest(data.iloc[:, 1:], 'M', window=120, rfr=data.iloc[:, 0],

                           data_type='ex_return', freq_strategy='M',

                           price_impact=False, ptc_buy=0.01 / 200, ptc_sell=0.01 / 100, 

                           extra_data=extra_data.iloc[:, :-1])

And some results are shown below:

>>> FF_3_factor_model.general_performance()

strategy name                              Fama-French 3-factor model portfolio

Price impact                                                                OFF

Start date of portfolio                                     1991-02-28 00:00:00

End date of portfolio                                       2002-12-31 00:00:00

Frequency of rebalance                                                  1 Month

Duration                                                            143 periods

Final Portfolio Return (%)                                            259.1316%

Peak Portfolio Return (%)                                             354.4271%

Bottom Portfolio Return (%)                                           105.9731%

Historical Volatiltiy (%)                                               3.5768%

Sharpe Ratio                                                             0.1065

Sortino Ratio                                                            0.1608

Calmar Ratio                                                             0.0054

Max. Drawdown (%)                                                      70.1002%

Max. Drawdown Duration                                       3501 days 00:00:00

% of positive-net-excess-return periods                                58.0420%

% of positive-net-return periods                                       59.4406%

Average turnover (%)                                                   13.3530%

Total turnover (%)                                                   1922.8387%

95% VaR on net-excess returns                                          -5.7882%

95% VaR on net returns                                                 -5.3991%

dtype: object



>>> FF_3_factor_model.get_net_excess_returns()

Date

1991-02-28    0.054931

1991-03-28    0.018295

1991-04-30   -0.020621

1991-05-31    0.010298

1991-06-28   -0.022145

                ...   

2002-08-30    0.025228

2002-09-30   -0.092177

2002-10-31    0.032344

2002-11-29    0.025310

2002-12-31   -0.003465

Length: 143, dtype: float64

Roadmap

  • Add in documents for the description of each data file

In the future:

  • More performance metrics

  • More built-in portfolio construction models

  • Add in more price-impact model options

Contributing

Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change.

License

MIT

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

portfolio_backtester-0.1.0.tar.gz (45.5 MB view details)

Uploaded Source

Built Distribution

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

portfolio_backtester-0.1.0-py3-none-any.whl (45.8 MB view details)

Uploaded Python 3

File details

Details for the file portfolio_backtester-0.1.0.tar.gz.

File metadata

  • Download URL: portfolio_backtester-0.1.0.tar.gz
  • Upload date:
  • Size: 45.5 MB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/3.4.2 importlib_metadata/4.6.4 pkginfo/1.6.1 requests/2.24.0 requests-toolbelt/0.9.1 tqdm/4.50.2 CPython/3.8.5

File hashes

Hashes for portfolio_backtester-0.1.0.tar.gz
Algorithm Hash digest
SHA256 118e498e120dcbb39401b18f77d243553467d4503160761e7bc707a7f5ed0ddf
MD5 7af3d35646b7ae91af506ea04d30adab
BLAKE2b-256 225fdcc3df9943eed11286c73e5f0efb19facfff605acc4db8bcf42da54a69a5

See more details on using hashes here.

File details

Details for the file portfolio_backtester-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: portfolio_backtester-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 45.8 MB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/3.4.2 importlib_metadata/4.6.4 pkginfo/1.6.1 requests/2.24.0 requests-toolbelt/0.9.1 tqdm/4.50.2 CPython/3.8.5

File hashes

Hashes for portfolio_backtester-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 55eda7fcf6a94f84d348437f3a214ef8ecfe63b4681496675fe38c18020dca0b
MD5 e9fc42b230e52388f80adea621450533
BLAKE2b-256 1e3431edf5fae2e7a1ad1591144973dc17f0f9a0cc5c0b48207e930ff7aa7ac3

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