Calculating returns with short positions (backtest)

1.7k views Asked by At

My goal is to write a function that returns a vector of portfolio returns for each period (i.e. day) from a pandas dataframe of security prices. For simplicity, let's assume that the initial weights are equally split between securities A and B. Prices are given by the following dataframe:

import pandas as pd
import numpy as np
dates = pd.date_range('20130101', periods=20)
prices = pd.DataFrame({'A': np.linspace(20, 50, num=20),
                       'B': np.linspace(100, 200, num=20)},
                      index=dates)

Further, we assume that asset A is the asset where we initiate a short position and we go long asset B.

Calculating discrete returns from a "zero-investment position" like a short position (i.e. in asset A) in a first step and overall portfolio returns from the weighted returns of single assets that constitute the portfolio in a second step is not trivial, and before I put my so far attempt, which is not working correctly (key problem being the loss from the short position in asset A exceeding -100% on 2013-01-14), I am greatful for any kind of help - may it be theoretical or code.

2

There are 2 answers

0
Stripedbass On

You are forgetting asset “C”, as in collateral. No matter how generous your broker might be (not!), most exchanges and national regulatory organizations would require collateral. you might read about some wealthy hedge fund guy doing these trades with just long and short positions, but when it goes south you also read about the HF guy losing his art collection— which was the collateral.

Equity margin requirements in the USA would require 50% collateral to start, and at least 25% maintenance margin while the short trades were open. This is enforced by exchanges and regulatory authorities. Treasury bonds might have more favorable requirements, but even then the margin (collateral) is not zero.

Since a long/short doubles your risk (what happens if the long position goes down, and short position goes up?), your broker likely would require more margin than the minimum.

Add asset “C”, collateral, to your calculations and the portfolio returns become straight forward

2
Pat On

Thank you for your answer @Stripedbass. Based on your comment, can the return process of a portfolio consisting of the two stocks be described by the following equations?:

enter image description here

The terms with + and - are the market values of the long and short position respectively such that the difference of them represents the net value of the two positions. If we assume that we want to be "market neutral" at the beginning of the trade t=0, the net value is zero.

For t > 0 these net positions represent the unrealised gains or losses of the long and short position that were opened and have not yet been closed. The term C denotes the money that we actually hold. It consists of the initial collateral and the cumulative gains and losses from the stock positions.

The overall return per period from trading the two securtities is then calculated as the simple return of the account V.

Based on this, you could define the following function and for short posititions choose option type='shares':

def weighted_return(type, df, weights):
    
    capital = 100
    #given the input dataframe contains return series
    if type == "returns":
        # create price indices
        df.fillna(0, inplace=True)
        df_price_index = pd.DataFrame(index=df.index, columns=df.columns)
        df_price_index.iloc[0] = 100 + df.iloc[0]
        for i in np.arange(1, len(df_price_index)):
            for col in df_price_index.columns:
                df_price_index[col].iloc[i] = df_price_index[col].iloc[i - 1] * (1 + df[col].iloc[i])
        n = 0
        ind_acc = []
        for stock in df.columns:
            ind_capital = capital * weights[n]
            moves = (df_price_index[stock].diff()) * ind_capital / df_price_index[stock][0]
            ind_acc.append(moves)
            n += 1

        pair_ind_accounts = pd.concat(ind_acc, axis=1)
        portfolio_acc = pair_ind_accounts.sum(1).cumsum() + capital
        df_temp_returns_combined = portfolio_acc.pct_change()

        df_temp_returns_combined[0] = np.sum(weights * df.iloc[0].values)

        df_temp_returns_combined = pd.DataFrame(df_temp_returns_combined)
        df_temp_returns_combined.columns = ["combinedReturns"]

    #given the input dataframe contains price series
    if type == "prices":
        n = 0
        ind_acc = []
        for stock in df.columns:
            ind_capital = capital * weights[n]
            moves = (df[stock].diff()) * ind_capital / df[stock][0]
            ind_acc.append(moves)
            n += 1

        pair_ind_accounts = pd.concat(ind_acc, axis=1)
        portfolio_acc = pair_ind_accounts.sum(1).cumsum() + capital
        df_temp_returns_combined = portfolio_acc.pct_change()

        df_temp_returns_combined[0] = np.NaN

        df_temp_returns_combined = pd.DataFrame(df_temp_returns_combined)
        df_temp_returns_combined.columns = ["combinedReturns"]
    
    #given the input dataframe contains return series and the strategy is long/short
    if type == "shares":
        exposures = []
        for stock in df.columns:
            shares = 1/df[stock][0]
            exposure = df[stock] * shares
            exposures.append(exposure)

        df_temp = pd.concat(exposures, axis=1)

        index_long = np.where(np.array(weights) == 1)
        index_short = np.where(np.array(weights) == -1)

        df_temp_account = pd.DataFrame(df_temp.iloc[:,index_long[0]].values - df_temp.iloc[:,index_short[0]].values) + 1
        df_temp_returns_combined = df_temp_account.pct_change()
        df_temp_returns_combined.columns = ["combinedReturns"]

        df_temp_returns_combined.index = df.index

    return pd.DataFrame(df_temp_returns_combined)