Momentum Strategy - An Introduction to Quantitative Trading

February 8, 2024
By
Daniel Kotas
Coding for Finance

This is a blog from students to students. We want to share information and give food for thought. Here you can find recommendation lists, interviews with alumni, professors and industry professionals, study and research materials, as well as some fun facts. Enjoy and do not hesitate to reach out in case you have a topic we need to write about.

In this post, we are going to look at a very well established strategy called "momentum", or colloquially, the strategy of "buying winners and selling losers". In fact, this approach has been established as a common risk factor in the Fama-French style by Jegadeesh and Titman (1993). Moreover, this post will serve as a simple benchmark to outline what constitutes a sensible trading strategy, what to look for and what to avoid. Let's get into it.

Intuition and setup

Every strategy needs to be intuitive in an economic context and the mechanism as to why the strategy should work has to be clear. For example,  many strategies are devised on some sort of co-movement or correlation of two or more variables. However, in the real world, there are many "random" correlations (remember, correlation ≠ causation), which are exemplified in the Spurious Correlations website. For instance,  just because there is a correlation between cheese consumption and people dying entangled in bed sheet, one would not take this as a signal to buy cheese producing stocks when a news report emerges of people dying in this bizarre way.

For momentum, the mechanism has several explanations well described in the paper, therefore we will not dwell too much on it. Put simply, we consider the definition that market participants transpose past returns of assets into the future. People follow the rationale of thinking that if a stock has been going up, there is probably a good reason for it and they will want to jump on that wave.

Construction

Now that we have an idea what momentum is and why it should work, we need to talk about how to trade it. There are many ways one can do that, but we will stay true to the paper and do it in a more precise, academic way.

We said momentum is buying winners and selling losers, which gives a good idea how we construct the portfolio. The pseudo-algorithm for construction is as follows:

  1. calculate performance (gross return) of our assets in the past XY periods (look-back period)
  2. rank these assets by the performance from highest to lowest
  3. select X number of  the highest ranked assets, i.e. "winners" and go long (buy them)
  4. select Y number of  the lowest ranked assets, i.e. "losers" and go short (sell them)
  5. hold these for a defined number of periods
  6. repeat every period, adding to portfolio

Setup

After understanding the intuition and technical construction, let's define the parameters to make the strategy more concrete, namely:

  • traded assets: we will use the SPDR® sector ETFs - think financials, utilities, consumer staples etc.
  • risk-free rate: trading US assets in the US Dollar, we need to use US risk-free rate, ideally consistent with frequency
  • frequency: to avoid trading costs, we will use monthly frequency
  • time-frame: for a proper backtest, we need to use at least 200 periods as a rule of thumb, thus, for monthly frequency, we need around 20 years
  • look-back period: initially, we will use 6 months
  • holding period: also 6 months
  • how many assets in the long/short leg: having 9 assets, we will trade 3 assets in each leg
  • how much of our own capital we are using: traditionally, we have two options, either 0 (pure long-short, zero-cost strategy) or 1, being invested 100% with our own capital. The proceeds of shorting will be used for additional buying, i.e. creating leverage
  • long and short leg caps: defining how much we can go long and short if shorting is allowed. Traditionally, short-cap is set to -1, thus setting long-cap to 2 if we are using 100% of our capital (long and short cap have to be equal to our own capital)
  • benchmark: it is not sufficient that a strategy makes money; it has to outperform the benchmark (either the market or equal-weights portfolio of selected assets). Thus, benchmark selection is crucial
  • trading costs: many people forget this crucial part. It is not difficult the create a highly profitable crazy-high-frequency strategy. However, once transaction costs are taken into account, these usually tend to lose money. In our case, we will use 20bps as one-way transaction cost, given we are trading very liquid, broad ETFs

Now we have everything important we need to get started, so let's put it into code! (If you want the whole code to just copy and run it, jump to the end or visit our GitHub and download the repository!)

# %% Imports
import pandas as pd
import numpy as np
import yfinance as yf
import matplotlib.pyplot as plt
import pickle
import seaborn as sns
import warnings

# define start date for Yahoo import
start_date = '2000-01-01'

# define a list of tickers we want to have prices for. Ticker has to adhere to Yahoo's convention (ticker in the bracket)
# https://finance.yahoo.com/quote/XLE?p=XLE&.tsrc=fin-srch . Tickers can be also loaded from an .xlsx
# NOTE: ^TNX is a 10-year treasury rate. We should be rather using a 1-month libor rate since we hold our investments monthly, 
# but that is unavailable on Yahoo Finance, thus we're using 10-year as a proxy.
rf_string = '^TNX'
tickers = ['XLE', 'XLF', 'XLU', 'XLI', 'XLK', 'XLV', 'XLY', 'XLP', 'XLB', rf_string]

# define lookback period, i.e. how much back in time do we look to determine which stocks performed the best and worst
lookback = 6
# define holding period, i.e. how much into the future do we think the returns will be persisent
holding_period = 6

# define number of stocks in the long and short leg
n_long = 3
n_short = 3

own_capital = 1 # how much of our own capital we are using in decimal (1 = 100%)

# define short_cap used for scaling final weights in the short leg of the strategy
short_cap = -1

# define long_cap for scaling final weights in the long leg. We want the legs to sum up to 1 (defined above), 
# and the short leg finances the rest of the long long leg. We manipulate equation " 1 = short_cap + long_cap "
long_cap = own_capital - short_cap

# we need to define onw-way transaction costs. This varies wildly from the asset, broker etc, 
# but a rule of thumb is to set somewhere from 5 to 20bps
tc = 0.002 #20bps

Data download

Getting the relevant data and wrangling it is probably one of the most challenging aspects of trading. We might even have great trading ideas which in hindsight work, but we just don't have the right data in a timely manner to test it. For this reason, some people prefer working only with price data, as these are usually easily accessible.

  Data is the new oil. - Clive Humby, British mathematician

For our purposes, we will utilise the yfinance Python package and download data from Yahoo Finance. Side note: if you visit the full code at the end or visit our GitHub, you will have the option to load prepared data from Excel or to "pickle" file just by changing parameters. For now, le'ts focus on Yahoo.

After having downloaded the data, we will need to process it; resample the daily data to monthly according to our setup, manipulate the risk-free rate and most importantly, calculate returns:

# %% Download and processing

df_prices = yf.download(tickers, start=start_date)['Adj Close']


## resample to monthly data
df_prices_reindex = df_prices.copy()
df_prices_reindex['Date'] = df_prices_reindex.index
df_prices_monthly = df_prices_reindex.loc[df_prices_reindex.groupby(pd.Grouper(key='Date', freq='1M')).Date.idxmax()].drop(['Date'],axis=1)

# calculate ordinary returns from prices as 1-period percentage change. As there is no period before the very first,
# this generates a NaN - we want to drop it to have a clean dataframe
df_returns_w_rf = df_prices_monthly.pct_change(1).dropna(axis=0)

# because we are using our own capital, we need to calculate SR while taking risk-free returns into account, i.e.
# taking into consideration what we would have earned if we invested into risk free asset and subtract it. 
# If we modify how much of our own capital we use, let's say 80%, then we also need to subtract only 80% of risk free return

# we need also divide the risk_free by 100 because to express as decimals and by 12 to get from p.a. to p.m., assuming monthly periodicity.
df_rf = pd.DataFrame(df_returns_w_rf[rf_string]) /  1200 
df_rf_scaled = df_rf * own_capital

# clean the returns dataframe
df_returns = df_returns_w_rf.drop([rf_string], axis = 1) 

Determining the weights

This is the most crucial and error-prone step of any backtest. In order to determine the returns of a back-tested strategy, we need two things: observed returns of asset in the past periods, and the weights we would have had in that period, according to our strategy. Since returns are fixed in time, we can only change the weights, which we are going to do now.

The biggest pitfall of this step, especially in parametric approaches, is the look-ahead bias, i.e., using information in some period which was not available at that time. In our case, we have to eliminate this, such that the weights which we determine have to be set into +1 period ahead.

Let us also revisit the pseudo-algorithm from above to get a better understanding of the code that follows:

  1. in each period i, starting with a delay of our look-back (6), calculate the gross return (cumulative product) of returns of our assets
  2. rank these assets from highest to lowest gross return
  3. determine the winners/losers
  4. in the i period, assign weights for the following i+1+holding periods according to our rules
  5. repeat for next period

This sounds like a job for a for-loop! Before we initiate the loop, let us set up auxiliary functions, namely for:

  • determining the losers/winners, using a sliced dataframe of returns in desired period
  • determining the index position of these asset in our dataframe
# %% Functions

def selector(returns, n_long, n_short):
    cumprod_for_sorting = np.prod(1 + returns) - 1
    winners = list(cumprod_for_sorting.nlargest(n_long).keys())
    losers = list(cumprod_for_sorting.nsmallest(n_short).keys())

    # determine weights by cross-sectional scaling
    loser_weights = cumprod_for_sorting[losers].div(cumprod_for_sorting[losers].sum(axis=0))
    winner_weights = cumprod_for_sorting[winners].div(cumprod_for_sorting[winners].sum(axis=0))
    return winners, losers, winner_weights, loser_weights

def column_index(df, query_cols):
    cols = df.columns.values
    sidx = np.argsort(cols)
    return sidx[np.searchsorted(cols,query_cols,sorter=sidx)]

Let us also briefly talk about the rules for setting up the weights; we will introduce three flavors of momentum:

  1. Pure time-series momentum. We determine the winners/losers and equal-weight them, meaning if we have 3 assets in long-leg, each will receive 0.33 and 3 in short-leg, each will receive -0.33. These will be then scaled by long/short caps
  2. Cumulative time-series momentum. We determine the weights in the same way as before, but we cumulate the weights. Imagine you allocate 0.33 to asset "ABC" in period one and then in the next period you assign 0.33 to the asset again, meaning you are increasing the allocation to this asset and cumulating the weight. Of course, this will be then scaled not to break budget-constraints. This way is the most "consistent" way to do it according to the paper
  3. Cumulative time-series and cross-sectional momentum. We again determine the winners and losers, but not only that - now we don't simply assign equal weights to the asset, but we scale them by cross-sectional performance. That means if we have three winners A,B and C with returns of 5%,10% and 5% respectively, we would determine the weight of asset i as weight_i=return_i / sum[returns]. In this case, the weights would be 0.25, 0.5 and 0.25 respectively. This is already built into our function.
# %% Strategy, portfolio weights, returns calculation

# set up a list of dataframes names for each type of momentum
l_weights_names = list(['df_weights_pureTS', 'df_weights_TS_cumulative', 'df_weights_scaling'])
# set up a list of names of strategies for more intuitive reading, namely for plotting and summary statistics
strategy_names = list(['Momentum Pure TS', 'Momentum Cumulative TS', 'Momentum TS & CS'])


# we need to set up an empty dictionary, where all dataframes will be stored. Option No.1: loop through the names of strategies 
# and write a new dataframe in each iteration of the loop
dict_weights = {}
for weights_name in l_weights_names:
    # we call the dictionary and subset it with the [] brackets to write a new object into it, in our case a dataframe of zeros, 
    # inheriting the index and columns names from our dataframe with title returns
    dict_weights[weights_name] = pd.DataFrame(data=0, index=df_returns.index, columns=df_returns.keys())

# Option No. 2: This line does exactly the same as the loop above, but is more neatly written ("pandas loop"). 
# It helps when our dataframes are large, but it this case, the efficiency doesn't play a big role.
# dict_weights = {weights_name: pd.DataFrame(data=0, index=df_returns.index, columns=df_returns.keys()) for weights_name in l_weights_names}


for i in range(lookback, len(df_returns)):

    # slicing our returns to desired lookback period, which we then pass to our selector function
    lookback_returns = df_returns.iloc[i - lookback:i, ]
    
    # using selector function to determine winners and losers in the past "lookback" months. 
    # Will be used for weights assingning by subsetting
    
    winners, losers, winner_weights, loser_weights = selector(lookback_returns, n_long, n_short)

    # we determine the column index of winners/losers to assign weights, because we are looping through integer index 
    # using iloc and assignign to a row-wise sliced df and subset by columns. Note it isn't possible to use 
    # df.iloc[i+1:i+1:holding_period][winners/loser], because this returns only a COPY of the desired subset and
    # a slice of the original df, but does not access it directly.
    
    # determine index of winners/loser with our function
    winners_index = column_index(df_returns, winners)
    loser_index = column_index(df_returns, losers)

    # Momentum variation No.1: Pure Momentum. Every period, we determine the winners/losers and assign weights for the 
    # following holding period to them based on desired number of titles in long and short leg. Note that this
    # variation makes most sense when holding_period = 1, otherwise we are just overwriting the weights
    dict_weights['df_weights_pureTS'].iloc[i+1:i+1 + holding_period, winners_index] = 1 / n_long
    dict_weights['df_weights_pureTS'].iloc[i+1:i+1 + holding_period, loser_index] = -1 / n_short

    # Momentum variation No.2: Cumulative. Every period, we determine the winners/loser and assign weights for the following holding period, but add them to the weights which we determined previously. 
    # We are cumulating the weights which were determined in previous periods
    dict_weights['df_weights_TS_cumulative'].iloc[i+1:i+1 + holding_period, winners_index] = dict_weights['df_weights_TS_cumulative'].iloc[i+1:i+1 + holding_period, winners_index] + 1 / n_long 
    dict_weights['df_weights_TS_cumulative'].iloc[i+1:i+1 + holding_period, loser_index] = dict_weights['df_weights_TS_cumulative'].iloc[i+1:i+1 + holding_period, loser_index] - 1 / n_short

    # Momentum variation No.3: Scaled cumulative. Until now, we have been assigning the weights in an equal weights fashion, i.e. if we had 3 "winners", each received a weights of 0.33. 
    # Now we want to scale the weights such that the winners which won the most will have the highest weight and vice versa. It means that not only the highest performing stocks will receive positive weights, 
    # but we also take magnitude of the performance into account.
   
    dict_weights['df_weights_scaling'].iloc[i+1:i+1 + holding_period, winners_index] = dict_weights['df_weights_scaling'].iloc[i+1:i+1 + holding_period, winners_index] + winner_weights.values
    dict_weights['df_weights_scaling'].iloc[i+1:i+1 + holding_period, loser_index] = dict_weights['df_weights_scaling'].iloc[i+1:i+1 + holding_period, loser_index]  - 1 * loser_weights.values

Scaling and returns calculation

We have figured out the weights and saved them into a dictionary holding all the dataframes. We are now almost ready to multiply the dataframes of weights with the returns to determine how the different strategies would have performed. Before we do that though, we need to scale the weights according to our caps and budget constraints.

The reason behind this is that in the cumulative strategies, the weight could be cumulating infinitely after we sum the newly determined weight in current period with the previous weights. Also, because we have positive and negative weights, we have to scale them separately to take into account the different caps.

This sounds again like a good job for a for-loop function; we will loop through the dictionary of weights, calling a different dataframe each time and doing the scaling. When we scale the weights, we will obtain our final weights. Therefore in the same step, we can complete 3 additional tasks:

  • calculate the returns of a portfolio as cross-sectional (i.e. "horizontal") sum of the returns dataframe
  • calculate the turnover, and from that the overall transaction costs every period to subtract from returns
  • calculate earning on the risk-free rate if we are using less than 100% of our capital or the costs of borrowing if we are using more than 100% of our capital

As a finishing touch, we create a dataframe for benchmark - in our case an equal-weights long-only portfolio (note that this is an EW not allowing for drift and also we neglect transaction costs of the benchmark).

The code for this, where the dictionary holding portfolio returns is of particular interest to us,  looks as follows:

# Weights determined - need to scale them and multiply with returns to get returns and then sum them up to get strategy returns
dict_scaled_weights = {}
dict_strategy_returns = {}
dict_portfolio_returns = {}
dict_avg_turnover = {}

## scaling: 
# We need to scale the weights such that the long leg and the short leg sum to 1 (we are using 100% of our capital). 
# Also, we take into account long and short caps, meaning we limit how much can we actually go short 
# (it would be possible to have long and short legs sum to 1, while the short leg would have a cumulative weight of say 100, 
# meaning we would have extreme, i.e. unrealistically high leverage)

for weights_name, strategy in zip(dict_weights, strategy_names):
    print(weights_name, strategy)
    temp_scaled = dict_weights[weights_name].copy()
    temp_not_scaled = dict_weights[weights_name].copy()

    temp_scaled[temp_not_scaled > 0] = temp_not_scaled.div(temp_not_scaled[temp_not_scaled > 0].sum(axis=1), axis=0) * long_cap
    temp_scaled[temp_not_scaled < 0] = (temp_not_scaled.div(temp_not_scaled[temp_not_scaled < 0].sum(axis=1), axis=0)) * short_cap

    turnover = temp_scaled.diff(1).abs().sum(axis=1)
    dict_avg_turnover[strategy] = turnover.mean()
    transaction_costs = turnover * tc


    dict_scaled_weights[weights_name + '_scaled'] = temp_scaled
    dict_strategy_returns[strategy] = temp_scaled * df_returns

    # we sum across assets to get portfolio returns, accounting for transactions costs calculate above and also risk-free earned
    dict_portfolio_returns[strategy] = (temp_scaled * df_returns).sum(axis=1) - transaction_costs + (df_rf[rf_string] * (1-own_capital)) 


# adding an equal weights benchmark (1 divided by number of assets available or simply an arithmetic mean of CS returns)
df_BM = pd.DataFrame(df_returns.mean(axis=1))
df_BM.columns = ['Benchmark']
df_all_strategies = pd.DataFrame(dict_portfolio_returns)

Summary statistics

Now comes the most exciting part of creating an investment strategy; seeing whether it works! We can judge that in many ways, but the key metrics to look at are:

  • geometric average over the whole period (annualized)
  • volatility of return (annualized)
  • Sharpe ratio
  • Information ratio
  • Skewness
  • Kurtosis
  • Alpha & Beta
  • NAVs plot

All metrics are on an absolute basis as well as relative to the benchmark (except for IR, alpha and beta of course). We built a simple class with two functions which takes 4 arguments:

  • dataframe of portfolio returns, where each column is a time-series of returns of each strategy
  • annualisation factor: if we have monthly data, it is equal to 12, daily 252, weekly 52
  • [optional]: dataframe of time-series of benchmark returns
  • [optional]: risk-free rate to calculate Sharpe ratio. If we have pure long-short strategy, we omit this.

You can either import this class together with the other functions, or treat it as a separate file and import it similarly to packages (this will be shown in the overall code). This approach is generally preferred for auxiliary functions, as you can keep a centralised repository of your functions which you can re-use and modify for future use.

import pandas as pd
import numpy as np
import statsmodels.api as sm

class port_an:



    def __init__(self, returns,ann_factor, benchmark=None, rf=None ):
        self.returns = returns
        self.ann_factor = ann_factor


        if rf is None:
            rf = pd.DataFrame(data = 0, index = returns.index, columns = ['rf'])

        else:
            rf.columns = ['rf']
        if benchmark is None:
            returns_all = returns

        else:
            self.benchmark = benchmark.sub(rf['rf'], axis=0)
            returns_all = pd.concat([returns, benchmark], axis=1)

        xsReturns = returns_all
        self.xsReturns = xsReturns


        nPeriods = len(returns_all)
        self.nPeriods = nPeriods

        NAVs = (1 + returns_all).cumprod()
        self.NAVs = NAVs

        gAvg = (1 + returns_all).prod() ** (ann_factor / nPeriods) - 1
        self.gAvg = gAvg
        gAvg_xs = (1 + xsReturns).prod() ** (ann_factor / nPeriods) - 1
        self.gAvg_xs = gAvg_xs

        vol = xsReturns.std() * np.sqrt(ann_factor)
        SR = gAvg_xs / vol

        self.SR = SR
        self.vol = vol
        self.skew_strat = returns_all.skew()
        self.kurtosis_strat = returns_all.kurtosis()
        self.alphas = list()
        self.betas = list()
        if benchmark is None:
            benchmark = 0
        else:
            for strat in xsReturns.columns:

                OLS_model = sm.OLS(xsReturns[strat], sm.add_constant(self.benchmark)).fit()
                self.alphas.append(OLS_model.params[0])
                self.betas.append(OLS_model.params[1])

    def an_w_BM(self):
        self.IR = (self.gAvg_xs.iloc[0:-1] - self.gAvg_xs.iloc[-1]) / self.vol
        df_summary = pd.DataFrame(index = self.xsReturns.keys(), columns =['Geometric Average - Excess (%)', 'Volatility Annual (%)', 'Sharpe Ratio', 'Information Ratio', 'Skewness', 'Excess Kurtosis', 'Beta', 'Alpha (%)'])
        df_summary.iloc[:, 0] = round(self.gAvg_xs * 100,2)
        df_summary.iloc[:, 1] = round(self.vol * 100, 2)
        df_summary.iloc[:, 2] = round(self.SR,2)
        df_summary.iloc[:, 3] = round(self.IR , 2)
        df_summary.iloc[:, 4] = round(self.skew_strat, 2)
        df_summary.iloc[:, 5] = round(self.kurtosis_strat, 2)
        df_summary.iloc[:, 6] = self.betas
        df_summary.iloc[:, 6] = round(df_summary.iloc[:, 6],2)
        df_summary.iloc[:, 7] = self.alphas
        df_summary.iloc[:, 7] = round(df_summary.iloc[:, 7]*100,2)
        return df_summary.fillna(0)

    def an_no_BM(self):
        df_summary = pd.DataFrame(index = self.xsReturns.keys(), columns =['Geometric Average - Excess (%)', 'Volatility Annual (%)', 'Sharpe Ratio', 'Skewness', 'Excess Kurtosis', 'Beta', 'Alpha (%)'])
        df_summary.iloc[:, 0] = round(self.gAvg_xs * 100,2)
        df_summary.iloc[:, 1] = round(self.vol * 100, 2)
        df_summary.iloc[:, 2] = round(self.SR,2)
        df_summary.iloc[:, 3] = round(self.skew_strat, 2)
        df_summary.iloc[:, 4] = round(self.kurtosis_strat, 2)

        return df_summary.fillna(0)

Now, having imported this class, let us use it on our dataframe of portfolio returns, export a dataframe with summary statistics, and print what the best strategy is.

# %% Summary statistics

# we defined a class with functions which calculate basic summary statistics. It is necessary to pass a labeled 
# dataframe with returns observed in the strategies and the annualization factor (if monthly data = 12) for calculating 
# the statistics. We have an optinonal argument "benchmark" for comparison of strategies to benchmark

summary = port_an(returns = df_all_strategies, ann_factor= 12,benchmark=df_BM ,rf = df_rf_scaled)

df_summary = summary.an_w_BM()
max_return = df_summary['Geometric Average - Excess (%)'].max()
max_return_name = df_summary['Geometric Average - Excess (%)'].idxmax()
print('Highest average return of', max_return ,'% was achieved by ', max_return_name,'strategy')

max_sharpe = df_summary['Sharpe Ratio'].max()
max_sharpe_name = df_summary['Sharpe Ratio'].idxmax()
print('Highest sharpe ratio of', max_sharpe ,'was achieved by',max_sharpe_name,'strategy')

Summary statistics

Visualisations

Even if not absolutely essential for our analysis (remember, numbers tell more than a graph), it is useful to plot some graphs to get an idea how the strategy evolves, in what periods it does better or worse, and how it moves with respect to the benchmark.

We will plot the following plots (and save them in the process):

  • NAVs of our different strategies together with the benchmark
  • stacked area plot to assess the evolution of the weights
  • a scatter plot of different strategies and the benchmark together with the OLS regression line
# %% Visualizations

## NAV Plot
df_NAVs = summary.NAVs # saving a df of the NAVs we calculated in the class "port_an"
df_NAVs.plot() # calling a simple plot
# adding a title with a dynamic variable if one decides to add/reduce strategies
plt.title(f"NAV Comparison of {len(strategy_names)} momentum strategies") 
plt.xlabel('Date') # adding x and y-axes labels
plt.ylabel('Cumulative NAVs')
plt.savefig(f"plots/NAVs.png")
#if we want to display the final plot, displays the plot with the propiertes we defined above.
plt.show() 

## Weights stacked plot
# source: https://stackoverflow.com/questions/52872938/stacked-area-plot-in-python-with-positive-and-negative-values

for strategy, name in zip(dict_scaled_weights, strategy_names):
    warnings.filterwarnings('ignore')
    fig, ax = plt.subplots()
    # split dataframe df into negative only and positive only values
    df_neg, df_pos = dict_scaled_weights[strategy].clip(upper=0), dict_scaled_weights[strategy].clip(lower=0)
    # stacked area plot of positive values
    df_pos.plot.area(ax=ax, stacked=True, linewidth=0.)
    # reset the color cycle
    ax.set_prop_cycle(None)
    # stacked area plot of negative values, prepend column names with '_' such that they don't appear in the legend
    df_neg.rename(columns=lambda x: '_' + x).plot.area(ax=ax, stacked=True, linewidth=0.)
    # rescale the y axis
    ax.set_ylim([df_neg.sum(axis=1).min(), df_pos.sum(axis=1).max()])
    plt.title(f"Weights of {name} strategy")
    plt.savefig(f"plots/{name}_weights.png")
    plt.show()


## Scatter plot
for strategy, name in zip(dict_portfolio_returns, strategy_names):
    sns.regplot(x = df_BM, y = dict_portfolio_returns[strategy])
    plt.title(f"Scatter plot of {name} strategy against benchmark")
    # create a folder called "plots" in the working directory for saving the plots
    plt.savefig(f"plots/{name}_scatter.png")
    plt.show()

NAVs plot

Scatter plot

Weights plot

NO INVESTMENT ADVICE

Nothing in the Site constitutes professional and/or financial advice, nor does any information on the Site constitute a comprehensive or complete statement of the matters discussed or the law relating thereto. Finance Club of the University of Zurich is not a fiduciary by virtue of any person's use of or access to the Site or Content.