In this chapter, we will be comparing methodologies, with a particular focus on absolute versus relative series. We will move from a scarcity to an abundance of ideas. We will find it easy to shop for potential Short and Long candidates via sector rotation. Absolute series are the Open High Low Close (OHLC) prices you can see on any website or platform. They often come either adjusted for dividends, stock splits, and other corporate actions or as adjusted close price. Relative series are the above absolute series divided by the closing price of the benchmark, adjusted for currency.
We will demonstrate the weakness of the absolute method, and the strength of the relative weakness method, which will define our methodology for the rest of the book. Our objective is to broaden the investment universe from the rare stocks tanking in absolute to half the constituents that underperform the index at any point in time.
We will cover the following topics along the way:
You can access color versions of all images in this chapter via the following link: .
You can also access the source code for this chapter via the book's GitHub repository. To access the repository link, follow the steps in the Download the example code files section in the Preface.
The following code, and indeed most of the code in the book, first requires the installation of the yfinance package:
pip install yfinance yfinance can then be imported:
import yfinance as yf For this chapter and the rest of the book, we will also be working with pandas, numpy, and matplotlib. So, please remember to import them first:
# Import Libraries import pandas as pd import numpy as np %matplotlib inline import matplotlib.pyplot as plt import pathlib import datetime from dateutil.relativedelta import relativedelta import requests from io import StringIO Now that we have imported the libraries, let's proceed with the absolute method.
"But he hasn't got anything on!" a little child said. "Did you ever hear such innocent prattle?" said its father. And one person whispered to another what the child had said, "He hasn't anything on. A child says he hasn't anything on."
– Hans Christian Andersen
The absolute method makes intuitive sense: buy stocks that go up, short stocks that go down. There is a one-to-one relationship between data coming from various providers, price charts on the screen, and what goes into the portfolio. Everybody speaks the same language. Investors, market commentators, and various other market participants talk about the same prices and valuation levels. Shorting stocks that go down in absolute value generates cash that can be used to buy additional stocks on the long side and increase leverage.
There is only one small problem: the product does not do what it says on the tin. The following sections will consider why.
Investors explicitly pay premium fees for uncorrelated returns. In execution trader English, investors want their money to grow, regardless of Mr. Market's mood swings. Let's look at why the absolute method fails to deliver on this promise.
In bull markets, the tide lifts all boats. Few stocks go down, but when they do, they attract a lot of attention. Long holders exit the stock while short sellers pile on. These stocks become crowded shorts, issues with elevated borrow utilization ratio. Popular shorts are notoriously difficult to trade—they are illiquid and volatile. Getting in and out of these stocks has some adverse market impact, borrow fees are expensive, and they are also prone to short squeezes, those violent rallies at the end of a bearish descent. Since no one wants to be too cornered through a short squeeze, this puts a natural cap on bet sizes, leading to atrophied short books.
On the other hand, long books are overdeveloped in bull markets. This results in high structural positive net exposures, which is an explicit bet on upward market direction. In financial creole, those are known as directional hedge funds. These fund managers talk a good game about ambidextrous and agile management, but as soon as the market turns bearish, they seek refuge under the comfort of their desks. Conversely, in bear markets, few stocks tend to go up. Short ideas are plentiful, but in practice, exposures rarely cross into negative territory. This means that investors cushion the blow but still lose money.
Figure 3.1 shows the S&P 500 index and a count of all the constituents in either the bull or bear regime, defined as 252 days breakout/breakdown, using the absolute price series. This count does not adjust the weight of each component in the index. For example, Google and the smallest market cap in the index are both counted as 1 or -1. In financial creole, this is referred to as equal weight as opposed to market cap weighted.
Secondly, we download the latest constituents of the S&P 500. We do not go back in time and adjust for the past composition of the index. Stocks deleted from the index are not featured in the dataset. In finance parlance, this is referred to as survivorship bias. This gives a built-in bullish bias to the index. Current constituents have survived the test of time while losers have been deleted. For example, Tesla joined the S&P 500 index in December 2020. Running a backtest including TSLA for a long period would result in a bullish bias. The good news is that survivorship bias negatively affects short sellers. If we run backtests on surviving constituents, we benchmark our survival against the fittest issues.
We will use the popular 1 year high/low as a proxy for bullishness/bearishness. When the price hits a 252-day high, it is deemed bullish. Conversely, when the price drops to a 252-day low, it enters bearish territory. The regime stays unchanged until it prints a 252-day high or low in the other direction, at which point it reverts.

Figure 3.1: Percentage of stocks in bullish/bearish territory and S&P 500
The green line represents the percentage of stocks in bullish territory and the red line those in bearish space. The black line shows the index.
The first obvious observation is: the absolute series is highly correlated with the index. When roughly 80-90% of the issues are in a bullish regime during a bull market, the remaining 10-20% of bearish stocks naturally stand out. They attract a lot of attention from all the short sellers around. The cost of borrow gets bid up. Those issues are notoriously volatile, hard to trade, and ultimately hardly profitable. Those stocks will rapidly turn into crowded shorts.
The preceding chart, and others comparing this method with the relative weakness method (which we will come back to in the Consistent supply of fresh ideas on both sides section), can be produced by executing the following source code:
url = 'https://en.wikipedia.org/wiki/List_of_S%26P_500_companies' headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)}'} response = requests.get(url, headers=headers) tables = pd.read_html(io.StringIO(response.text)) tables = pd.read_html(io.StringIO(response.text)) df_SP500 = tables[0] df_SP500 = df_SP500.rename(columns={'Symbol':'ticker', 'Security': 'name', 'GICS Sector':'sector','GICS Sub-Industry':'sub-industry'}) df_SP500['ticker'] = df_SP500['ticker'].str.replace('.', '-', regex =False) df_SP500 = df_SP500.sort_values(by = ['sector','name']).set_index('ticker') bm_ticker = '^GSPC' tickers_list = [bm_ticker] + list(df_SP500.index)[:] df_SP500.groupby('sector').count() We displayed the chart and published the corresponding code. Let's explain the code step-by-step:
We will now download the current constituents of the S&P 500 index:
read_html is an efficient way to convert tables from the internet into a dataframe. In this case, we need to use requests to scrape the internet.[0].yfinance library: .str.replace('.', '‑', regex =False).method.groupby(). Tables and webpages get updated regularly so check the shape of the df to make sure you have downloaded the correct table.
Now that we have a list of tickers, we will download historical prices using the yfinance library. We define a function to download the historical "Close" prices in batches.
def batch_px_df(tickers_list,batch_size, start, end,show_batch = False): px_df = pd.DataFrame() loop_size = int(len(tickers_list) // batch_size) + 2 for t in range(1,loop_size): # Batch download try: m = (t - 1) * batch_size n = t * batch_size batch_list = tickers_list[m:n] if show_batch: print(batch_list,m,n) batch_download = yf.download(tickers= batch_list,start= start, end = end, interval = "1d", group_by = 'column',auto_adjust = True, prepost = True)['Close'] px_df = px_df.join(batch_download, how='outer') except: pass return px_df batch_size = 51 ; start= '2001-01-01' ; end = None df_abs = batch_px_df(tickers_list,batch_size, start, end,show_batch = False).round(0) df_rel = df_abs.dropna(subset=[bm_ticker], axis=0) df_rel = round(df_abs.divide(df_abs[bm_ticker], axis=0).mul(df_abs[df_abs[bm_ticker].notna()].iloc[0,list(df_abs.columns). index(bm_ticker)]),2) df_abs.shape, df_rel.shape We defined a function to download historical prices. We will download historical prices for all the constituents in the S&P 500, build a dataframe of absolute prices, and then another one relative to the index.
We will define a function to download historical "Close" prices in batch. We will then generate two dataframes df_abs and df_rel for absolute and relative series, respectively.
def batch_px_df (): tickers_list: we need to make sure all tickers are compatible with the yfinance library.batch_size: data is processed in batches. Big downloads sometimes generate errors, so we break it down into smaller batchesstart, end: self-explanatory. yfinance has also the period method: 1Y, 5Y etcshow_batch = True, shows tickers as they are downloaded. This adds a few milliseconds, so default to False for production. Loop to download ['Close'] prices in batchespx_df by joining each batch and return px_df.df_abs.df_rel of relative prices by dividing the df_abs by the benchmark ticker and rebase to the first non-null value of the benchmark.th absolute and relative df. window = 252 # 1 year rolling window bullbear = pd.DataFrame() bullbear['benchmark'] = df_abs[bm_ticker].copy() bullbear['bm_returns'] = np.exp(np.log(bullbear/bullbear.shift()).cumsum()) - 1 bo_abs = pd.DataFrame(data= np.where(df_abs >= df_abs.rolling(window).max(),1, np.where(df_abs<=df_abs.rolling(window).min(),-1,np.nan)), index= df_abs.index,columns= df_abs.columns).ffill() bo_abs = bo_abs.dropna(how = 'all', axis=0) bo_rel = pd.DataFrame(data = np.where(df_rel >= df_rel.rolling(window).max(),1, np.where(df_rel <= df_rel.rolling(window).min(),-1,np.nan)), index= df_rel.index,columns= df_rel.columns).ffill() bo_rel = bo_rel.dropna(how = 'all', axis=0) bo_abs_count = bo_abs.count(axis=1) bo_rel_count = bo_rel.count(axis=1) bullbear['bulls_absolute'] = bo_abs[bo_abs > 0].count(axis=1).div(bo_abs_count,fill_value=0) * 100 bullbear['bears_absolute'] = bo_abs[bo_abs < 0].count(axis=1).div(bo_abs_count,fill_value=0) * 100 bullbear['bulls_relative'] = bo_rel[bo_rel > 0].count(axis=1).div(bo_rel_count,fill_value=0) * 100 bullbear['bears_relative'] = bo_rel[bo_rel < 0].count(axis=1).div(bo_rel_count,fill_value=0) * 100 bullbear['bullbear_absolute_avg'] = bo_abs.mean(axis=1) bullbear['bullbear_relative_avg'] = bo_rel.mean(axis=1) bullbear = round(bullbear,2).ffill() bullbear[280:][['bm_returns','bulls_absolute', 'bears_absolute']].plot(figsize=(15,4), style=['k','g+-', 'r--'],grid=True,secondary_y=['bm_returns'], markevery= 100,markersize= 10, title = 'S&P500: 1 year High/Low count, absolute series') We are going to manipulate data to produce the chart we saw in Figure 3.1: Percentage of stocks in bullish/bearish territory and S&P 500.
We use a classic one-year high/low as a proxy for the bullish/bearish regime. Stocks that have printed a 252-day high are deemed bullish. They are expected to remain strong. Conversely, stocks that have printed a one-year low are expected to exhibit continued weakness.
bullbear df. We copy the benchmark price for df_abs and calculate cumulative returns.dataframes from absolute and relative price series: np.where price hits a 252 day high, assign 1 for bullishnp.where price hits a 252 day low, assign -1 for bearishAnd voila! We have all the bullish/bearish counts for the absolute and relative series:

Figure 3.2: Percentage of stocks in bullish/bearish territory and S&P 500
Absolute series are highly correlated to the benchmark. As the above chart shows, it is not uncommon to have above 80% of the constituents be in either bullish or bearish territory. When the market goes up, the number of bullish stocks rises. When the market turns sideways or bearish, the number of stocks in bearish territory rises. This demonstrates that the absolute method is by definition correlated to the markets. It should suffice to look at the track record of long/short funds to conclude that the absolute method has failed to deliver uncorrelated attractive returns.
The next few sections will consider a few weaknesses of the absolute method.
Since the short side has fewer ideas than the long one, the way to balance exposures is to supersize the more concentrated book. This translates into a diluted, relatively low volatility long portfolio and a few concentrated structural short bets. Crowded shorts are illiquid and therefore more prone to volatility spikes. There is no shortage of sellers, but courageous buyers are scarce.
Volatility on the short side drives the entire portfolio. Underwhelming performance divided by high residual volatility only yields unattractive volatility-adjusted returns.
The absolute method might post positive returns during bull markets, but every time the market tanks, performance hits a soft patch.
During the Great Financial Crisis (GFC), one would have expected net exposures to drop below zero. After all, the financial pundits were talking of financial Armageddon. If long/short players were as comfortable shorting as they claimed to be, net exposures should have dropped below 0. The numbers told a different story. With net beta hovering around +0.5, market participants were residually bullish. They might have been talking a defense game, but they were still playing offense.
The way absolute method practitioners reduced their net exposure was to reduce their long exposure, and hoard cash. When it came down to pulling the trigger on the short side, they froze. Years of trading in a bull market had atrophied their short selling muscle.
"Who needs a circus when bad drivers give us a chance to watch the biggest show on the road?"
The absolute method has failed to deliver on its promise. Long/short 1.0 is neither a sophisticated nor a safe investment vehicle. It is a lesser vehicle in every single respect. It makes less money than mutual funds and index funds in bull markets. It only loses less money than the index in bear markets. After fees, investors compound less money with the absolute method than they would with low-tech plain-vanilla index funds. There is less transparency and less liquidity than in classical mutual funds. It is therefore little surprise that those funds have failed to attract and retain pension money looking for stable, uncorrelated returns. Absolute long/shorts funds are in the business of making money for themselves and occasionally for their investors.
It is time we added grave insult to fatal injury.
"La gravité est le bouclier des sots [Gravity is the shield of fools]."
– Montesquieu, French philosopher
Stocks do not simply fall out of the sky. They show signs of weakness along the way:
The bottom line is, by the time those stocks pop up on people's radar screens, they have already shed a lot of value. In an industry where creative, colorful insults are an occupational pastime, few zingers sting as much as being called a laggard indicator. Yet it seems to be the dominant business model in the long/short business.
Long/short market participants are most certainly not intellectually challenged. They are highly educated, dedicated, and hard-working people. Going 'long on strength' and 'short on weakness' is the right idea—the absolute series is just the wrong data set. As an alternative, the next section will discuss the benefits of using the relative series over absolute prices.
"Truth is by nature self-evident. As soon as you remove the cobwebs of ignorance that surround it, it shines clear."
– Mahatma Gandhi
Indices such as S&P 500, Nasdaq 100, FTSE 100, and Topix are the market capitalization weighted average of their constituents. Roughly half the stocks will do better and the rest worse than the index over any timeframe. There are many more stocks to pick from the large contingent of relative underperformers than the few and far between stocks that drop in absolute value.
When we plot the percentage of bullish/bearish stocks in the S&P 500 using the relative series we calculated in the previous section, we obtain something like this:

Figure 3.3: Percentage of stocks in bullish/bearish territory and S&P 500
At any point in time, there are roughly as many underperformers as outperformers. Simply said, somewhere between 40 to 60% of stocks are either in relative bullish or bearish territory at any point in time. The potential short universe expands from the rarefied atmosphere of crowded shorts to roughly half the index.
Let's plot the absolute and relative series side by side to get an idea of the difference in size of the investment universes.

Figure 3.4: Percentage of stocks in bullish/bearish territory, absolute and relative series, S&P 500
The percentage of stocks in bearish territory does not go to extremes (20% to 80%) when using the relative series. It gently oscillates around the mean. This has several important implications:
def rel_fx(df,_o,_h,_l,_c, bm_df, bm, ccy_df, ccy, start, end,rebase = True, mult = 1000): df[ccy] = ccy_df.loc[start:end,ccy].copy().ffill() df[bm] = bm_df.loc[start:end,bm].copy().ffill() for i in [_o,_h,_l,_c]: df[f'r{i}'] = df[i].div(df[ccy]) if rebase == True: df[f'r{i}'] = df[f'r{i}'].div(df[bm]).mul(df[df[bm].notna()].iloc[0,list(df.columns).index(bm) ]) else: df[f'r{i}'] = df[f'r{i}'].div(df[bm]) * mult return df We calculated the relative series simply by dividing the df_abs by the index. We will now define a proper function to calculate the relative series.
This function converts absolute local price series into fund currency relative to a benchmark. It is a 3-step process:
bm_df and ccy_df, respectively.rebase = True. This rebases the multiplier to the beginning of the series. This works best when the start of the series is a fixed date.rebase = False. This is better suited for continuous series or moving windows such as rolling 1, 2, 3 yearsNow that we have defined the relative function, we will quickly define a few utilities functions.
def dict_swap(dct): dict_swap = {value:key for key,value in dct.items()} return dict_swapdef df_from_dict(batch_size,dct,start,end): tickers_list = list(dct.keys()) df = batch_px_df(tickers_list,batch_size, start, end,show_batch = False) df = df.rename(columns = dct) df = df.tz_localize(None).ffill() return dfdef rohlc(df,relative = False): if relative==True: rel = 'r' else: rel= '' if 'Open' in df.columns: _o,_h,_l,_c = f'{rel}Open',f'{rel}High',f'{rel}Low',f'{rel}Close' elif 'open' in df.columns: _o,_h,_l,_c = f'{rel}open',f'{rel}high',f'{rel}low',f'{rel}close' else: _o=_h=_l=_c= np.nan return _o,_h,_l,_cdef yf_droplevel(batch_download,ticker): df = batch_download.iloc[:, batch_download.columns.get_level_values(1) == ticker] df.columns = df.columns.droplevel(1) df = df.dropna() return df Those utilities functions are quite useful. They will be recycled throughout the rest of the book. There is no need to explain the code; their purpose will suffice:
dict_swap(dct): this dictionary comprehension swaps keys and valuesdf_from_dict(batch_size, dct,start,end): this function downloads historical prices from dictionary keys, and swaps column names with valuesrohlc(df,relative = False): this verbose function instantiates _o,_h,_l,_cdef yf_droplevel(batch_download,ticker): drops multiindex df into single ticker dfNext, we will take this function for a spin. Softbank (9984.T) is a company listed on the Tokyo Stock Exchange (TSE). It trades in Japanese yen (JPY). The company has been a major player in the US tech industry for almost three decades. Softbank will therefore be benchmarked against Nasdaq in USD:
dict_bm = {'^GSPC': 'SP500', '^DJI': 'DOW30', '^IXIC': 'Nasdaq100', '^NYA': 'NYSE ', '^XAX': 'Nyse_Amex', '^RUT': 'Russell2000', '^VIX': 'VIX', '^BUK100P': 'CBOEUK100', '^FTSE': 'FTSE100', '^GDAXI': 'DAX', '^FCHI': 'CAC40', '^STOXX50E': 'EuroStocks50', '^N100': 'Euronext100', '^BFX': 'BEL20', 'IMOEX.ME': 'MOEX_Russia', '^N225': 'Nikkei225', '^HSI': 'HangSeng', '000001.SS': 'SSE', '399001.SZ': 'Shenzhen', '^STI': 'STI', '^AXJO': 'ASX200', '^AORD': 'All_Ordinaries', '^BSESN': 'Sensex', '^JKSE': 'Jakarta','^KLSE': 'FTSE_KLCI', '^JKSE': 'Jakarta', '^KLSE': 'FTSE_KLCI', '^NZ50': 'NZX50', '^KS11': 'Kospi', '^TWII': 'TSEC', '^GSPTSE': 'TSX', '^BVSP': 'Bovespa', '^MXX': 'IPC_Mexico', '^MERV': 'Merval', '^TA125.TA': 'TA125', '^IRX': '3M_US_Yield', '^FVX': '5Y_US_Yield', '^TNX': '10Y_US_Yield', '^TYX': '30Y_US_Yield'}bm_df = df_from_dict(batch_size,dict_bm,start,end)bm_df['absolute'] = 1dict_ccy = {'EURUSD=X': 'EURUSD', 'JPY=X': 'USDJPY', 'GBPUSD=X': 'GBPUSD', 'AUDUSD=X': 'AUDUSD', 'NZDUSD=X': 'NZDUSD', 'USDCAD=X': 'USDCAD', 'EURJPY=X': 'EURJPY', 'GBPJPY=X': 'GBPJPY', 'EURGBP=X': 'EURGBP', 'EURCAD=X': 'EURCAD', 'EURSEK=X': 'EURSEK', 'EURCHF=X': 'EURCHF', 'EURHUF=X': 'EURHUF','CNY=X': 'USDCNY', 'HKD=X': 'USDHKD','SGD=X': 'USDSGD','INR=X': 'USDINR', 'KRW=X': 'USDKRW','TWD=X': 'USDTWD', 'MXN=X': 'USDMXN', 'PHP=X': 'USDPHP', 'IDR=X': 'USDIDR', 'THB=X': 'USDTHB', 'MYR=X': 'USDMYR', 'ZAR=X': 'USDZAR', 'RUB=X': 'USDRUB', 'BRL=X': 'USDBRL'}ccy_df = df_from_dict(batch_size,dict_ccy,start,end)ccy_df['local'] = 1bm = 'Nasdaq100' ; ccy = 'USDJPY' ; dgt = 2ticker = '9984.T' # Softbankdf = yf.download(tickers= ticker,start= start, end = end, interval = "1d",group_by = 'column', auto_adjust = True, prepost = True)df = round(yf_droplevel(df,ticker),2).tz_localize(None).ffill() _o,_h,_l,_c = rohlc(df,relative = False) df = rel_fx(df,_o,_h,_l,_c, bm_df, bm, ccy_df, ccy, start, end,rebase = True, mult = 1000) df[-1300:][['Close','rClose']].plot(figsize=(15,4),grid=True, secondary_y = ['rClose'],title= 'Softbank Absolute in JPY vs relative to Nasdaq in USD') This code calculates the relative series rebased to the beginning of the dataframe compared to the absolute series, using Softbank as an example. Let's go through each of the steps:
'absolute' and 'local' with 1 values for absolute series and local currency series. Those bm_df and ccy_df will be re-purposed later. Those two dataframes bm_df and ccy_df can be re-purposed for any benchmark or currency._o,_h,_l,_c, calculate relative prices and plot both absolute in local currency and relative in USD. We only plot the last 1300 datapoints for visibility's sake.
Figure 3.5: Softbank in absolute JPY versus relative to Nasdaq in USD
Technology works in mysterious ways. When telephones lost their cord, people carried them everywhere and gradually enchained themselves to their devices. Softbank has been a powerhouse on the TSE since portable phones have enslaved mankind. Yet, Softbank has been a lackluster performer when benchmarked against the Nasdaq index and labeled in USD. For the rest of the book, we will be using Softbank series in absolute value and JPY simply for demonstration purposes.
The concept is simple: buy the outperformers, sell short the underperformers, make money on the spread. The idea of focusing on the excess returns over the index is nothing new. Mutual fund managers are assessed on their excess returns over the benchmark. When focusing on only the long side, the mission is to overweight outperformers and underweight underperformers. The difference in weight over the benchmark is called active money.
The relative weakness method takes a similar approach. A long/short portfolio is the net sum of two relative books. The long side is a classic mutual fund-type long book. The short side is composed of underperformers benchmarked to the inverse of the index. The only difference with a mutual fund is that instead of staying away from underperformers, managers take active bets on the short side. Performance comes from the spread between outperformance on the long side and underperformance on the short. Below is a simple example. General Electric, General Motors, and Goldman Sachs are in three different industries. Yet, they have two things in common. Firstly, they are constituents of the same S&P 500 index. Secondly, when combined, their tickers have this lovely acronym: GEMS.
The code below shows price series in absolute value and then returns relative to the benchmark:
benchmark = ['^GSPC'] ; tickers_list = ['GE','GM','GS'] gems = yf.download(tickers,start, end, interval = "1d",group_by = 'column',auto_adjust = True, prepost = True)['Close'] gems = gems.tz_localize(None).ffill().dropna(how='any',axis=0) gems_rel = gems[tickers_list].div(gems[bm_ticker], axis=0).mul(gems.iloc[0,list(gems.columns).index(bm_ticker)]) gems_rel.columns = ['rel_'+i for i in tickers_list] gems = round(pd.concat([gems,gems_rel],axis=1),2) gems_returns = np.exp(np.log(gems/gems.shift()).cumsum()) - 1 gems_returns.plot(figsize=(15,4),grid=True,style = ['k','b','g','r','b-.','g-.','r-.'], secondary_y = list(gems_rel.columns), title = 'GEMS: GE, GM, GS, Absolute & Relative returns v. S&P500') The code takes the following steps:
This code downloads historical price data for selected stocks (GE, GM, GS) and the S&P 500 benchmark (^GSPC), calculates their absolute and relative returns, and plots them. It builds on previously defined variables like `start`, `end`, and `bm_ticker` from earlier cells.
`benchmark = ['^GSPC']`: List containing the S&P 500 ticker.`start` to `end` dates. Extracts only the 'Close' prices. `start` and `end` are defined in CELL INDEX: 7.'rel_' prefix to relative column names (e.g., 'rel_GE').`gems_returns.plot(...)`: Plots cumulative returns with specified styles, secondary y-axis for relative series, grid, and title. Styles differentiate absolute (solid lines) and relative (dashed lines) returns.The result shows the absolute prices along with the relative series on dotted lines of General Electric (GE), General Motors (GM), and Goldman Sachs (GS):

Figure 3.6: Absolute versus Relative prices of General Electric (GE), General Motors (GM), and Goldman Sachs (GS)
The chart illustrates that relative series are a bit more indicative of a stock's relative strength compared to the market. Prices are restarted versus the benchmark at the beginning of the period. They provide uniform series across sectors. At first, it may seem unnatural to enter a short position on a rising stock. Losing money does not come naturally to absolute return players. To understand the concept, let's reframe it by picturing what happens in a bear market. When the big bear pounds the market, the only thing that goes up is correlation. Everything takes it on the chin, albeit some less than others. This means that you'll be looking to buy long defensive stocks that hold their ground or fall slower than the broader index. You will be losing less than the market on the long side. Typically, you would be buying non-cyclical utilities, railways, or food stocks.
Conversely, you will be looking to short stocks that drop faster than the benchmark. Those tend to be cyclical stocks for which performance ebbs and flows with the economic cycle, such as airline companies. You can keep the darlings of the previous bull markets in your portfolio, just remember to switch sides. Leaders of bull markets tend to attract late-cycle momentum players. Those are the weakest hands, late to the game, with no real game plan. They bail as soon as the going gets tough, which leads to sudden performance disgorgement. The relative series opens a whole new world of possibilities compared with the classic absolute method. The upcoming sections will take us through the 10 reasons that assert the superiority of the relative weakness method.
Indices are generally the market cap weighted average of all their constituents. This means that roughly half the issues will outperform while the other half trail the market. At first, this plethora of ideas appears disconcerting to market participants who have consistently focused on absolute performance. Market participants can be tempted to continue to trade the long side using the absolute prices and the short one using the relative series. Once we switch to the relative series, we must use the same set of prices and signals for the long and short sides. The long side must be a mirror image of the type of strategy and series deployed on the short side.
Let's revisit the script we wrote in the Ineffective at decreasing correlation with the benchmark section about the number of stocks in bullish and bearish territory. As we mentioned there, the code produces a few charts. We originally published a lateral count using the absolute series, which is also shown here:

Figure 3.7: Stocks in the S&P 500 in bullish or bearish territory using the absolute series
It illustrates that the S&P 500 has been in bullish territory since the end of the GFC, with a few hiccups along the way. Overall, there are many more stocks in bullish than bearish territory. Short candidates are few and far between. As the adage goes, the tide lifts all boats.
However, let's compare this with the relative series. This is a simple lateral count on daily bars. In comparison, the chart below shows the number of stocks in bullish and bearish territory using both absolute and relative series side by side, with the price divided by the close of the index:

Figure 3.8: Percentage of stocks in bullish or bearish territory using the absolute and relative series
This eye-opening chart was produced with the following block of code:
bullbear[280:][['bm_returns','bulls_relative',bullbear[280:][['bm_returns','bulls_relative', 'bears_relative']].plot(figsize=(15,4), style= ['k','g^-', 'rv-'],grid=True,secondary_y=['bm_returns'], markevery= 100,markersize= 10,bullbear[280:][['bm_returns','bulls_absolute', 'bears_absolute', 'bulls_relative', 'bears_relative']].plot(figsize=(15,4), style= ['k','g+-', 'r--','g^-', 'rv-'],grid=True,secondary_y=['bm_returns'], markevery= 100,markersize= 10, title = 'S&P500: 1 year Bullish/Bearish count, absolute & relative series')bullbear[280:][['bm_returns','bullbear_absolute_avg','bullbear_relative_avg']].plot(figsize=(15,4), style=['k', 'b', 'm'],grid=True,secondary_y=['bm_returns'], title = 'S&P500: 1 year Bullish/Bearish average absolute & relative series') The dotted lines represent the number of stocks in either bullish or bearish territory relative to the index. Unsurprisingly, there are roughly half the stocks in the bull camp and the others in bearish mode at any point in time. This provides a plethora of ideas to choose from on both sides.
Survivorship bias: The list of constituents does not feature historical inclusions/deletions. Stocks that perform poorly, go bankrupt, or are absorbed are deleted from the index. This creates an artificial upward drift referred to as survivorship bias. Survivorship bias does not affect short selling strategies as their validity is measured only against the strongest stocks. Promising issues gradually dethrone the weaker ones.
There is a consistent supply of fresh ideas, regardless of the index. The absolute series has massive fluctuations depending on the market's ever-changing mood. Meanwhile, the number of stocks in bullish or bearish territory remains fairly constant using the relative series. This implies there are always plenty of ideas on both sides of the book at any given time. As we can see in the graph below, the lines between bullish and bearish are much closer to one another.

Figure 3.9: Percentage of stocks in the S&P 500 in bullish or bearish territory using the relative series
As the breadth narrows, the number of underperformers may even exceed that of outperformers. That directly contradicts the widely held belief that short ideas are few and far between. In reality, it is not rare to have more underperformers than outperformers. For example, tech megacaps have been driving the market for a few years. If a few megacaps drive returns, this implies that the majority of otherwise well-managed companies have underperformed. This explains the downward drift of the net outperformers/underperformers in the next graph.

Figure 3.10: Average bullishness/bearishness of the absolute and relative series v. S&P 500
The average score of the absolute and relative series is another visualization of the same idea.
The index is the market capitalization weighted average of its constituents. In sideways or turbulent times, smaller capitalizations tend to be rudely reminded of Newtonian physics. Gravity hits hard on illiquid stocks.
In the absolute method, the objective is to time the top and the bottom of the market. Every seasoned market participant will admit timing peaks and troughs is a futile exercise. When restating everything in relative series, the objective is to focus on sector rotation. As we saw in the above chart (see Figure 3.10), the number of stocks in either territory does not fluctuate much over time. This does not mean the same stocks underperform for eternity. The market may go up for years, but it will reward different industries, sectors, and even market caps over time. Using the relative weakness method, this means buying nascent outperformers and shorting early underperformers. Imagine the impact on clients when you tell them after a successful bear campaign it is now time to switch sides and go long early cyclicals and small caps amidst the ambient doomsday gloom.
We previously looked at the broad market number of bullish/bearish issues in Step 3: Bullish/Bearish regime for the absolute/relative series. Next, let's drill down and look at what happens at the sector level.
We will join the sector column from the table dataframe we originally built from the internet with the historical regime df. We build a multi-index df, and then calculate the average by sector using the pivot_table method.
def sector_avg_df(df_table, target_col, df_historical): """ Calculate the average absolute bullish/bearish count for each sector. """ df_historical = df_historical.copy().dropna(how='all', axis=1) df_historical.columns.names = ['ticker'] sector_df = pd.concat([df_table[[target_col]], df_historical.T], axis=1) sector_avg_df = pd.pivot_table(sector_df, values=list(df_historical.index), index=[target_col], aggfunc="mean").T return sector_avg_dfsector_avg_abs = round(sector_avg_df(df_SP500, 'sector', bo_abs).round(2),2)sector_avg_rel = round(sector_avg_df(df_SP500, 'sector', bo_rel).round(2),2)bullbear = pd.concat([bullbear, sector_avg_abs.add_suffix('_abs'), sector_avg_rel.add_suffix('_rel')], axis=1) Let's explain how the function works.
We will now concatenate the df_SP500 with the historical prices to group by sectors.
'ticker'.df_historical.pivot_table, then transpose.bullbear df adding the suffix "_abs" and "_rel". sectors = df_SP500['sector'].unique().tolist() sector_avg_abs_cols = [i+'_abs' for i in sectors] sector_avg_rel_cols = [i+'_rel' for i in sectors] bullbear[['bm_returns', 'bullbear_absolute_avg']+sector_avg_abs_cols].plot(figsize= (15,4),secondary_y =['bm_returns'],style = ['k','b*'], title = 'S&P500, sectors in absolute') bullbear[['bm_returns', 'bullbear_relative_avg']+sector_avg_rel_cols].plot(figsize= (15,4),secondary_y =['bm_returns'],style = ['k','g*'], title = 'S&P500, sectors in relative') Let's explain the code below.
We will now plot the averages by sector for the absolute and relative series.
df_SP500.sector_avg_abs_cols and sector_avg_rel_cols using a list comprehension.bullbear averages in absolute and relative.
Figure 3.11: Bullish/bearish sectors and benchmark using the absolute series
A rising tide lifts all boats. All sectors are correlated with the index. When everything moves synchronously, it is objectively difficult to pick the best and worst performing stocks. Market participants must apply additional filters. Some work sometimes; none works all the time. The default filter is valuations. It is important to understand that valuation metrics change over the course of a bull/bear cycle. Example: buy machinery stocks when their price to book ratio (PBR) are expensive and sell them when they are cheap. Machinery stocks rise and fall on their order books. When their order books are empty and filling up, valuations are expensive. When their order books are full and decreasing, valuations are cheap. So, sell machinery stocks when they look cheap on PBR. On the other hand, retail stocks are correlated with year-on-year change in monthly sales. There is no one size fits all for valuations. Knowing the driver for each of them, keeping abreast of them and knowing when to switch in and out is near impossible using the absolute series.
Fortunately, there is a simple way to find out when to allocate to each sector. We simply let the market do the heavy lifting for us. Next, let's look at the relative series.

Figure 3.12: Bullish/bearish sectors and benchmark using the relative series
The relative charts apparently look all over the place. In quants trader English, there is low correlation between the index and the sectors. This is good news. This means that some sectors get rewarded or punished depending on where we are in the economic cycle. Early cyclicals tend to outperform in the early stage of a bull market, and fade as the cycle matures, etc. A thorough discussion on market cycles is beyond the scope of this book. For this, read Mastering the Market Cycle by Howard Marks.
For us Long/Short practitioners, this is good news. We don't need Google satellite views of auto manufacturers' parking lots to trade stocks in the steel or rubber sector. We don't need to track Walmart shipping lanes from China to gauge US retail sales either. All we have to do is keep track of which sectors start to outperform and underperform. In financial analyst English, this is called sector rotation. This is extremely powerful. Focusing on sector rotation will accelerate your learning curve. For example, suppose social media stocks start to roll over. You are left with a choice. Either you fight the tape and go the way of the dodo. Or you try to understand and look for clues. Focusing on sector rotation is simply the most efficient and intellectually rewarding way to build a broad knowledge base across the market.
Imagine a conversation with an investor about how you switched a sector from the long to the short side. Imagine dropping a few negative catalysts when the entire news flow is euphoric.
Next, let's see what happens when we group sectors together.
Earlier on in this chapter, we alluded to defensive and cyclical sectors when we were looking at the GEMS. Now, let's build them.
Let's simplify things a little bit. Some sectors are sensitive to the economic cycle. They expand and contract along with the economy. Those are traditionally referred to as cyclicals. They can be further subdivided into early, mid, or late cyclicals. For the sake of simplicity, we will take the two emblematic sectors of the cyclicals: consumer discretionary and technology.
Defensive sectors are impervious to economic recessions. No matter how bad the economy might get, you will still need to feed yourself and pay your utility bills. Consumer staples like food and utilities are emblematic defensive sectors.
Cyclical stocks are sensitive to the economy. Consumers' discretionary spending expands and contracts along with the economy. Those are also tech stocks. When the economy is doing fine, investors have a bigger appetite to finance riskier businesses. Without further ado, let's code:
defensives_list = ['Consumer Staples_rel', 'Consumer Staples_abs','Utilities_rel', 'Utilities_abs'] bullbear['defensives_rel'] = bullbear[ ['Consumer Staples_rel','Utilities_rel']].mean(axis=1) bullbear['defensives_abs'] = bullbear[['Consumer Staples_abs','Utilities_abs']].mean(axis=1) defensives_plot =['benchmark'] + defensives_list +['defensives_rel','defensives_abs'] cyclicals_list = ['Consumer Discretionary_rel', 'Consumer Discretionary_abs', 'Information Technology_rel', 'Information Technology_abs',] bullbear['cyclicals_abs'] = bullbear[[ 'Consumer Discretionary_abs', 'Information Technology_abs']].mean(axis=1) bullbear['cyclicals_rel'] = bullbear[[ 'Consumer Discretionary_rel', 'Information Technology_rel']].mean(axis=1) cyclicals_plot = ['benchmark'] + cyclicals_list + ['cyclicals_rel','cyclicals_abs'] bullbear['rotation_rel'] = bullbear['cyclicals_rel'].sub(bullbear['defensives_rel'], fill_value =0) bullbear['rotation_abs'] = bullbear['cyclicals_abs'].sub(bullbear['defensives_abs'], fill_value =0) So, this code calculates sector-based averages and rotations for defensive and cyclical sectors in the bullbear dataframe.
The code is fairly simple:
The absolute series is highly correlated with the index. The lows mirror the bottoms of the index. First, let's publish the code and then the charts
bullbear[window:][defensives_plot].plot(figsize=(15,4),grid=True,secondary_y=['benchmark'], style = ['k','b','b:','g','g:','co-','c+-'],title = 'Defensive sectors in absolute & relative')bullbear[window:][cyclicals_plot].plot(figsize=(15,4),grid=True,secondary_y=['benchmark'], style = ['k','m','m:','r','r:','y.-','y--'],title = 'Cyclical sectors in absolute & relative') The following chart shows the sectors that constitute the cyclical indices. We then over impose the cyclicals both in absolute and relative to the index.

Figure 3.13: Cyclical sectors and benchmark equal-weight returns
On the other hand, some sectors are impervious to the economic cycle. People do not eat twice as much or refrigerate their houses simply because the GDP has grown by 1%. Those sectors also tend to be resilient in economic downturns. They are referred to as defensives. We will also take the two emblematic defensives: consumer staples and utilities. Similarly, we plot the defensive sectors and then over impose the defensive indices in absolute and relative to the index.

Figure 3.14: Defensive sectors and benchmark equal weight returns
The defensives are inversely correlated to the markets in relative terms. Since they are resilient to economic downturns, they do not drop as much. They therefore outperform the benchmark in relative terms.
This leads us to the next indicator: rotation. This is defined as cyclicals minus defensives.
bullbear[window:][['benchmark', 'rotation_abs', 'cyclicals_abs','defensives_abs' ]].plot(figsize=(15,4),grid=True,secondary_y=['benchmark'], style = ['k','r:','y:','c:+'],title = str('benchmark, rotation_abs, cyclicals_abs, defensives_abs') ) bullbear[window:][['benchmark', 'rotation_rel','rotation_abs']].plot(figsize=(15,4),grid=True,secondary_y=['benchmark'], style = ['k','r','r:'],title = str('benchmark, rotation_rel, rotation_abs') ) Let's start with the absolute series.

Figure 3.15: Absolute rotation indicator and benchmark
The rotation indicator took off before the GFC. This reverse indicator would have put any righteous market participant out of business. This indicator tends to bottom out after the broader index. Being called a lagging indicator is an insult in the industry. We can reasonably conclude this rotation index in absolute is somewhat of a lagging indicator.
Next, let's look at the rotation indicator using the relative series.

Figure 3.16: Relative rotation indicator and benchmark
This rotation indicator using the relative series seems to be doing a better job at catching major inflexions in the market. It peaked in 2007. The GFC also started in the summer of 2007 when cross-sectional volatility ticked up while volume was slowly drying up. It bottomed out before the broader market in early 2009. It tends to peak as the broader market reaches plateaus. There are two ways to use this indicator. Major peaks and troughs slightly lead or coincide with tops and bottoms of the market. Secondly, when the rotation indicator turns negative, it is a clear indication that market participants should adopt a more defensive positioning.
It intuitively makes sense too. Defensives do not fluctuate much. They usually offer generous dividends to compensate for their lacklustre growth. This high dividend yield creates "dividend support", a safety net that prevents abrupt price descents. Every dip attracts patient investors looking for high dividends. Conversely, cyclicals sectors trade water before leading the charge on the way down. Growth stocks generally do not have dividend support to cushion the fall.
Finally, let's plot the rotation index in absolute and relative.

Figure 3.17: Absolute and relative rotation indicator and benchmark
This indicator probably needs a little more work to be production grade, but it looks promising. We will revisit this indicator when we talk about Beta in subsequent chapters. For now, suffice it to say that you can broadly keep the same stocks, just swap sides when the market turns from bullish to bearish.
Dividing everything by the benchmark strips the effect of the underlying index. This mechanically removes correlation to the index. The focus is now on excess returns over the benchmark. It may come across as repetitive, but it drives the point home. The absolute series has large fluctuations in the number of names in either regime depending on the state of the market.
Conversely, the relative series has a fairly consistent number of names on either side. Market gyrations do not greatly affect the size of the pool of candidates, as demonstrated in the Consistent supply of fresh ideas on both sides section. This is also basic arithmetic. Dividing the absolute series by the index strips the effect of the benchmark across the entire population. The only two factors remaining are currency impact and stock-specific performance.
This is a direct consequence of the abundance of underperformers. When there is a plethora of short candidates to choose from, market participants enjoy greater diversification in their portfolios. They also have lower concentration. This reduces volatility.
Crowded shorts are notoriously expensive to borrow. Institutional shareholders rarely abide by an old maritime code of honor. They do not sink with the ship. They liquidate their positions when underperformance persists. This makes borrowing even harder to locate over time.
Lending fees on the brokerage side are a direct function of supply of shares from long holders and demand for borrow from short-sellers. If institutional investors liquidate and short sellers appetite increases, the lending fees will mechanically rise. Besides, the quality of borrow diminishes. Lending desks tap into short term lending pools referred to as callable stocks. As a short-selling practitioner, it is perilous to remain in short positions when tourists tap into the callable inventory. Here is why.
Callable stocks are the borrow of last resort, when everything else has been tapped. This means that all the short-sellers are already positioned. All the long holders have already lent their stock. All it takes is one lender to recall his stock for the short sellers to scramble to score new borrow. This is the genesis of a short squeeze. Bottom line, whenever you hear that the only inventory available is expensive or callable stock, play the short squeeze as a quick long trade.
On the other hand, underperformers still benefit from some inertia. Borrowing is close to general collateral—in other words, cheap and plentiful. When bad news sinks in and absolute players lurk around, relative short sellers can pass the baton to their less sophisticated counterparts. The former can move to cheap, easy-to-borrow shorts, while the latter are left to chew on expensive, dry, hard-to-borrow names.
This approach dovetails nicely with the earlier sector rotation. You will be able to score cheap and plentiful borrow, and capture some relative alpha before the hordes of absolutists flatten the landscape.
One of the problems with the absolute method is its lack of scalability. Stocks that go down in absolute value are usually popular. Crowded shorts are volatile and illiquid. It is difficult to build large profitable positions.
In 2007, quantitative funds, often referred to as quants, learned the hard way that there was a limit to short selling crowded issues. When there is an ample supply of ideas on both sides, concentration can be kept low as Assets Under Management (AUM) continue to grow.
The relative method is non-confrontational. Relative short sellers do not need to make things personal. Management of defensive companies understand that their stocks are bound to underperform in bull markets. They do not take offense when their stocks are either sold short or bought long on this premise. Conversely, high-flying tech entrepreneurs know that bear markets are not exactly IPO season.
Managers with regional or global mandates juggle multiple currencies and indices. Converting everything into fund currency relative to a global or regional benchmark puts all stocks on the same playfield.
There is no need for an additional layer of macro-view, currency hedging, or risk management. Everything is rebased in the same currency and benchmark adjusted unit. For example, the Japanese market soared as its currency was devalued by 40%. Managers operating in JPY did well in local currency; their counterparts denominated in USD fared poorly despite having the exact same stocks in their portfolios. The depreciation of the JPY ate all gains away.
But managing a currency-adjusted relative portfolio is not intuitive. Everything from stock selection to portfolio management must be adjusted for the benchmark and fund currency. Decisions such as entry, exit, and position-sizing need to be made in currency-adjusted relative prices. Charts in absolute and local currency provided by most data vendors answer questions that relative market participants should never be asking themselves. Comparing stocks' absolute value with currencies is like comparing apples and oranges. Converting the entire investment universe into relative series is a bit more involved up front, but a lot easier to manipulate thereafter.
Let's revisit the GEMS initiative. This time let's broaden the universe to global stocks. Let's look at automotive manufacturers around the world: Toyota, Volkswagen, Renault, Ford, Tesla, and Hyundai Motors.
The relative method is particularly useful when comparing global stocks in the same sectors. This is a three-step process:
Let's code the autos dictionary. We will be revisiting this global sector throughout the book.
fx_dict = {'7203.T': 'USDJPY','VOW3.DE' :'EURUSD', 'RNO.PA':'EURUSD','F':'local' ,'TSLA':'local' ,'GM':'local' ,'005380.KS':'USDKRW' } tickers = list(fx_dict.keys()) ; bm = 'SP500' ; start = '2021-12-30' px_df = yf.download(tickers,start, end, interval = "1d", group_by = 'column',auto_adjust = True, prepost = True)['Close'] px_df[bm] = bm_df[start:][[bm]].copy() px_df = round(px_df.tz_localize(None).ffill(),2) Now, let's go through the steps.
We download historical prices using the yfinance library.
bm_df we built earlier in the heading Long/Short 2.0: the relative weakness method.The yen has definitely hit a permanently low plateau. Let's calculate the returns with the following code:
daily_log_returns_abs = np.log(px_df/px_df.shift()) cumul_returns_abs = daily_log_returns_abs.expanding().sum().apply(np.exp) - 1 daily_log_returns_rel = daily_log_returns_abs.sub(daily_log_returns_abs[bm], axis=0) cumul_returns_rel = daily_log_returns_rel.expanding().sum().apply(np.exp) - 1 cumul_returns_abs.plot(figsize=(15,4),grid=True,secondary_y = [bm],style = ['k','b','g','r','c','m'], title = 'Cumulative returns and S&P500 in LOCAL currency, absolute series') cumul_returns_rel.plot(figsize=(15,4),grid=True,secondary_y = [bm],style = ['k','b','g','r','c','m'], title = 'Cumulative returns relative to S&P500 in LOCAL currency') np.exp to calculate the cumulative returns.expanding() instead of cumsum() to circumvent potential missing values. We apply numpy exponential to calculate cumulative returns in local currency.
Figure 3.18: Cumulative returns in local currency, absolute series

Figure 3.19: Cumulative returns relative to S&P 500 in local currency
px_dict_usd = {key:px_df[key].div(ccy_df[value]).ffill() for key,value in fx_dict.items()} px_df_usd = pd.DataFrame(px_dict_usd) px_df_usd[bm] = bm_df[start:][[bm]].copy() px_df_usd = round(px_df_usd.tz_localize(None).ffill(),2) px_df_usd = px_df_usd.dropna(subset = fx_dict.keys()) px_df_usd = px_df_usd[px_df.columns] Let's go through the preceding snippet of code.
We create a dictionary comprehension of prices converted to USD.
px_df_usd.px_df_usd, drop NaN and re-order columns.daily_log_returns_abs_usd = np.log(px_df_usd/px_df_usd.shift()) daily_log_returns_abs_usd = np.log(px_df_usd/px_df_usd.shift()) cumul_returns_abs_usd = daily_log_returns_abs_usd.expanding().sum().apply(np.exp) - 1 daily_log_returns_rel_usd = daily_log_returns_abs_usd.sub(daily_log_returns_abs_usd[bm], axis=0) cumul_returns_rel_usd = round(daily_log_returns_rel_usd.expanding().sum().apply(np.exp) - 1,4) cumul_returns_abs_usd.plot(figsize=(15,4),secondary_y = [bm],style = ['k','b','g','r','c','m'], grid= True, title = 'cumulative return in absolute, in USD') cumul_returns_rel_usd.plot(figsize=(15,4),secondary_y = [bm],style = ['k','b','g','r','c','m'],grid=True, title = 'cumulative returns relative to S&P 500, in USD') We repeat the same process that we did with local currency. We calculate cumulative returns in USD
We calculate the cumulative returns in USD:
expanding() instead of cumsum() to circumvent potential missing values. We apply numpy exponential to calculate cumulative returns.
Figure 3.20: Cumulative returns in absolute and USD
Toyota (7203.T) was clearly leading the pack in local currency. When converted to US dollars, Toyota sinks. Next, let's plot the returns in USD relative to the index.

Figure 3.21: Cumulative returns relative to S&P 500 and USD
This restatement in USD versus the S&P clarifies the situation. Toyota does not look like the clear winner anymore. The JPY has suffered a continuous devaluation. Renault leads the pack. Tesla has bottomed out. Ford, GM, and Hyundai Motors could be potential short candidates. This exercise shows how deceiving big or small the returns in local currency and absolute terms can look. When looking at securities across different markets, first convert into local currency.
Next, let's build a heatmap of returns:
def returns_table(daily_log_returns, range_days): daily_log_returns.index = pd.to_datetime(daily_log_returns.index) dt_today = daily_log_returns.index.max() rtrns_df = pd.DataFrame() rtrns_df['1D'] = daily_log_returns.ffill()[-1:].sum() rtrns_df['1W'] = daily_log_returns[dt_today + relativedelta(weeks = - 1):].sum() rtrns_df['1M'] = daily_log_returns[dt_today + relativedelta(months = - 1):].sum() rtrns_df['3M'] = daily_log_returns[dt_today + relativedelta(months = - 3):].sum() rtrns_df['6M'] = daily_log_returns[dt_today + relativedelta(months = - 6):].sum() rtrns_df['1Y'] = daily_log_returns[dt_today + relativedelta(years = -1):].sum() rtrns_df['2Y'] = daily_log_returns[dt_today + relativedelta(years = -2):].sum() rtrns_df['3Y'] = daily_log_returns[dt_today + relativedelta(years = -3):].sum() rtrns_df['Max'] = daily_log_returns.sum() rtrns_df = round((np.exp(rtrns_df)-1)*100,1) cumsum = daily_log_returns[dt_today + relativedelta(days = - range_days):].cumsum() rng = round((cumsum.tail(1) - cumsum.min()) / (cumsum.max() - cumsum.min()),2) rtrns_df = pd.concat([rtrns_df,rng.T],axis=1) rtrns_df =rtrns_df.rename(columns={rtrns_df.columns[-1]: f'range {range_days}d'}) return rtrns_df def heatmap(df, subset_cols, clrmap = 'RdYlGn', prec =1): try: return df.style.background_gradient(subset= subset_cols,cmap= clrmap).format(precision = prec) except: return df.style.background_gradient(subset= subset_cols,cmap= clrmap).set_precision(prec) returns_table_abs = returns_table(daily_log_returns_abs, range_days=364) returns_table_rel_usd = returns_table(daily_log_returns_rel_usd, range_days=364) Let's go through the code.
We build a table of returns for a universe of securities over various time periods.
daily_log_returns to time. Today's date is logically the last row.relativedelta from today's date to calculate the cumulative sum of the returns.np.exp and subtract 1 to convert log returns into geometric returns.Heatmaps are great visualization tools.
Style.background.gradient syntax changed from Python 3.9 to 3.13.We generate two returns tables, one in absolute local currency and one in USD relative to the benchmark. We sort by a period of our choice.
sort_period = ['6M'] heatmap(returns_table_abs.sort_values(by=sort_period, ascending = False), subset_cols = returns_table_abs.columns, clrmap = 'RdYlGn', prec =1) This produces the following table:

Figure 3.22: Returns in absolute and local currency
We are going to produce a similar table with the relative series:
heatmap(returns_table_rel_usd.sort_values(by=sort_period, ascending = False), subset_cols = returns_table_rel_usd.columns, clrmap = 'RdYlGn', prec =1) This generates the table below:

Figure 3.23: Returns relative to S&P 500 in USD
Heatmaps are powerful tools. They bypass the slow process of the left brain. It would take 10 minutes to process rows and columns of numbers. Ideally, we would like to simplify the table to 1 number. When we paint a heatmap, it takes less than 1 second to mentally sort out the winners from the losers. We even have the capacity to process a lot more information.
This is not the end of the research process, only the beginning. This simply triages the upcoming work:
One humble word of advice from a scarred veteran: the market is always right. Sometimes, we haven't figured out why just yet.
For now, we used relative returns to triage stocks into the bullish, bearish, and indecisive buckets. In the next chapter on regime definition, we will introduce more systematic ways to reclassify markets.
Market participants often complain that they are triggering their stop loss, or they work their orders. Market participants tend to place stop losses at support/resistance levels or round numbers. Some algorithms are specifically designed to game resting orders. When all the price levels are calculated relative to the index and adjusted for currency, those sniper algorithms are ineffective. The series that relative participants react to are different from the absolute levels.
The flipside is that stop losses must be actively managed. In execution trader English, orders that do not match prices coming from the exchange cannot be filled.
As we noted when we first introduced the relative method, stocks do not drop in absolute value out of nowhere. Underperformance starts with their competitors and extends to the industry, sector, and broader market before tumbling down in absolute value. Entering a short on a relative basis may be mildly painful for a while. It will, however, appear prescient to those fixated on absolute prices.
A picture is worth a thousand words. The following chart plots both the relative and absolute performance of Disney (DIS):
ticker = 'DIS' ; bm = 'SP500'; ccy = 'local' ; df = yf.download(ticker,start = '2017-12-30', end = None ,interval = "1d",group_by = 'column',auto_adjust = True) df = yf_droplevel(df,ticker) start = '2019-12-30'; end= None df1 = rel_fx(df,_o,_h,_l,_c, bm_df, bm, ccy_df, ccy, start, end,rebase = True, mult = 1) df2 = rel_fx(df,_o,_h,_l,_c, bm_df, bm, ccy_df, ccy, start, end,rebase = False, mult = 2000) df1 = rel_fx(df,_o,_h,_l,_c, bm_df, bm, ccy_df, ccy, start, end,rebase = True, mult = 100) df1[[bm,'Close','rClose']].plot(figsize=(15,4),grid=True, secondary_y = [bm], style = ['grey','k', 'r' ],title= f'{ticker} Absolute vs Relative series rebased') df2 = rel_fx(df,_o,_h,_l,_c, bm_df, bm, ccy_df, ccy, start, end,rebase = False, mult = 2000) df2[[bm,'Close','rClose']].plot(figsize=(15,4),grid=True, secondary_y = [bm], style = ['grey','k', 'r' ],title= f'{ticker} Absolute vs Relative series continuous') We are quite familiar with the two-step process by now: 1) We download data and 2) process the relative function. We will focus on the difference between rebased and continuous series.
This produces the following chart. The first one is rebased at the beginning of the series. The relative and absolute prices start at the same price. The latter one uses continuous series and a multiplier of 2,000. The relative series is much lower than the absolute prices.

Figure 3.24: S&P 500, Disney in absolute and relative rebased to beginning
The absolute series is in black, the relative in red and the benchmark in gray. The absolute series outperformed the market during the Corona pandemic. However, the price flatlined from mid-2021 to early 2022. Meanwhile, the index continued to rise.
The Disney price chart starts in January 2018. The relative series and benchmark start 2 years later. The rebased relative series is tethered to the price on the first date of the chart. If you are going to use a fixed date for all your calculations going forward, then set the Boolean flag rebase to True.
Let's go through the pros and cons of each method for a better understanding of rebasing:
This produces the following charts.

Figure 3.25: S&P 500, Disney in absolute and relative rebased to beginning
This is the same source data, but we calculate the relative series using a constant multiplier instead of the price on a specific date. If you are going to use a rolling window such as 1, 3, or 5 years for instance, then set the Boolean rebase flag to False.
There is no right or wrong. It really depends on where you are in your journey. If your system is production worthy, thoroughly tested, and you feel confident, then go with a rolling window. If you need to develop, test, tweak, and build confidence, then give yourself the memory of a long runway. Now, let's dive into what the charts are telling us.
The Force was strong with Disney after their acquisitions of the Marvel Comic Universe and the Star Wars franchise. They clocked box office hits with metronomic regularity. They then swapped narratives and characters in established franchises with untested archetypes. This new editorial line alienated their traditional fan base while failing to attract a new crowd. Super productions releases have been relegated to the outer rim of the cinematographic galaxy since then. Now, imagine you had started to short before the share price decided to be reunited with Little Mermaid in her natural habitat called the abyss. Is there a better selling point to all those investors who are so desperate to find one good short seller? There is no better marketing pitch than a chart in absolute value plotting the trades you took according to changes relative to the index. Imagine anchoring short positions in all the Enrons, Lehmans, GMs, and Kodaks of this world long before they even made the news.
Getting in ahead of everyone gives a margin of safety. Positions are less vulnerable to bear market rallies. This is, however, only half of the equation. Getting out on the short side can be messy. Volume thins out as shorts gain in popularity. This is where the relative method offers a decisive advantage. At some point, selling pressure will be done. Relative short sellers have ample time to cover their shorts, while their absolute counterparts double down with their vitriolic comments. Stocks begin to outperform, imperceptibly at first, stubbornly then, before rising defiantly. This invariably catches absolute fundamental short sellers who bank on a double-dip that never comes.
In this chapter, we introduced and compared two contrasting methodologies for constructing long/short portfolios: the absolute method and the relative strength method. The absolute method, while intuitive and widely used, suffers from significant limitations. We analyzed the shortcomings of the absolute method: its high correlation with the benchmark, limited downside protection, volatility-driven concentration, and reliance on lagging indicators. As such, it often fails to deliver uncorrelated, attractive returns.
In contrast, the relative strength method offers a more robust and scalable approach. This method expands the investment universe by normalizing prices relative to a benchmark and adjusting currency. It consistently offers fresh opportunities on both the long and short sides. Unlike the absolute approach, the relative method reduces correlation to the broader market and enhances diversification. This results in lower portfolio volatility and greater flexibility. The objective is to identify outperformers and underperformers sectors dynamically through sector rotation and capitalize on evolving economic cycles.
We explored technical implementations, including converting absolute OHLC prices to relative series. We visualized the advantages of the relative method through charts, heatmaps, and performance metrics. These tools help you generate ideas, reduce borrowing costs, and navigate markets with a clearer understanding of sector dynamics and global currency impacts.
Ultimately, this chapter equips you with the foundational knowledge to transition from lagging absolute strategies to a forward-looking relative framework. It emphasizes why adopting the relative weakness method is not just a tactical shift but a strategic advantage, enabling practitioners to lead the market, mitigate risks, and achieve consistent, uncorrelated returns. Absolute participants are laggard indicators while relative players lead the pack.
In the next chapter, we will look at regime definition. We will put stocks in 3 buckets: bullish, bearish, and inconclusive.
Scan the QR code (or go to ). Search for this book by name, confirm the edition, and then follow the steps on the page.


Note: Keep your invoice handy. Purchases made directly from Packt don't require an invoice.