This chapter focuses on refining the investment universe for short-selling by filtering out ideas that are theoretically attractive but practically dangerous. Short-selling is asymmetric by nature: liquidity evaporates; crowding creates explosive short squeeze risks, and corporate actions can distort prices for long periods. As a result, success on the short side depends less on finding "good stories" and more on weeding out bad setups. This chapter introduces the fundamental and risk-based filters required to build a robust, tradable short universe.
By the end of this chapter, you will be able to systematically narrow a broad equity universe into a short-ready subset, avoid structural short-selling hazards, interpret valuation and beta signals in context, and understand how these filters support more resilient long/short portfolio construction.
In this chapter, we will cover the following topics:
The way to approach liquidity on the short-side is radically different. On the long-side, liquidity increases as more investors are drawn to rising prices. Early birds end up selling to a much larger pool of market participants. Market participants can afford to build positions over several days of trading volume. The bid/ask spread tends to narrow on the way up and widen on the way down.
On the short side, when investors liquidate their positions, it is a one-way street. After a beating, they don't come back for round two. Nothing captures the emotional journey of the Long market participants more faithfully than the Kübler-Ross model. Market participants grieve their losses. Each phase even has its distinctive signature on the markets. Interest wanes, and as the pool of market participants shrinks, so does liquidity. Early bear markets are a lot more liquid than later-stage ones. Short-sellers exit into thinner liquidity than they entered.
Add short squeezes to the mix, and you've got a recipe for an explosive cocktail. The last thing you want is to be caught in a short squeeze with a large position that you cannot cover without serious market impact. On the short side, the way to approach liquidity and market impact is not about how long it takes to build a position, but about how easy it is to get out.
This limits the investment universe to issues where liquidity is not a problem. Average daily trading value should be well above 1 million dollars. When risk is "on", small-mid caps are where the action is happening. Meanwhile, on the short side, boring blue chips go out of style.
When risk is "off", market participants gravitate toward larger capitalizations, where liquidity remains abundant. Reverting the Long small/Short large caps trade is not easy. Shorting small caps is a bloody sport. Liquidity evaporates, such that you can dock a supertanker in the bid/ask spread. So, gains can be wiped out at the first squeeze.
We will be working with the pandas, numpy, yfinance, and matplotlib libraries. So, please remember to import them first:
import pandas as pdimport numpy as npimport requestsfrom io import StringIOimport arcticdb as adbfrom arcticdb import LibraryOptionsimport yfinance as yf%matplotlib inlineimport matplotlib.pyplot as plt Now that we have imported the libraries, let's proceed to download the information for all constituents of the S&P 500 index.
First, we download the list of the current constituents of the S&P 500 via Wikipedia. Note that it has survivorship bias. Constituents added and deleted are not featured. So, this is purely for educational purposes.
For better data integrity, no survivorship bias, and yet still affordable, I would personally recommend Norgate (). I have no affiliation with the company, yet they have some of the best quality data. For now, let's proceed with the free data sources Wikipedia and yfinance:
url = 'https://en.wikipedia.org/wiki/List_of_S%26P_500_companies' headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'} response= requests.get(url, headers=headers) df_SP500 = pd.read_html(StringIO(response.text))[0] df_SP500 =df_SP500.rename(columns={'GICS Sector':'sector'}) df_SP500.set_index('Symbol', inplace=True) bm_ticker = '^GSPC' tickers = [bm_ticker] + df_SP500.index.tolist() Once we have a dataframe with the tickers for all the constituents, we can proceed to extract data for individual securities.
'GICS Sector' is renamed 'sector' and sets ticker symbols as index.Now that we have published the function, let's download the information for each current constituent of the S&P 500 index.
sp500_info_dict ={} for symbol in tickers: try: ticker = yf.Ticker(symbol) info = ticker.info sp500_info_dict[symbol] = info except Exception as e: print(f"Error fetching info for {symbol}: {e}") sp500_info_dict[symbol] = None sp500_info_df = pd.DataFrame.from_dict(sp500_info_dict, orient='index') Let's go over the logic step by step:
yf.Ticker(symbol).info to get company information (market cap, PE ratio, dividends, short interest, etc.).Now that we have created a dataframe with all the information available, let's narrow down to the fields useful to our purpose.
# Market Cap correction sp500_info_df['marketCap_calculated'] = sp500_info_df['sharesOutstanding'].mul(sp500_info_df['currentPrice']) sp500_info_df['market_Cap'] = np.where(sp500_info_df['marketCap'] == 0, sp500_info_df['marketCap_calculated'], sp500_info_df['marketCap']) sp500_info_df['market_Cap'] = round(sp500_info_df['market_Cap'] *1e-6) # Average Liquidity calculation value_traded = round(sp500_info_df['averageVolume'].mul(sp500_info_df['currentPrice']) *1e-6) value_traded_avg10 = round(sp500_info_df['averageDailyVolume10Day'].mul(sp500_info_df['currentPrice']) *1e-6) sp500_info_df['value_traded'] = np.where(~pd.isna(value_traded_avg10), value_traded_avg10, value_traded) # Crowded shorts sp500_info_df['shortfloat'] = round(sp500_info_df['sharesShort'].div(sp500_info_df['floatShares']),3) sp500_info_df['shortPctFloat'] = np.where(~pd.isna(sp500_info_df['shortfloat']), sp500_info_df['shortfloat'], sp500_info_df['shortPercentOfFloat']) sp500_info_df[['exDividendDate', 'dividendDate']] = sp500_info_df[['exDividendDate', 'dividendDate']].apply(lambda x: pd.to_datetime(x).dt.strftime('%Y-%m-%d')) sp500_info_df[['sector','exchange','market_Cap', 'value_traded', 'currentPrice',#'sharesOutstanding', 'averageDailyVolume10Day', 'averageVolume', 'shortPctFloat', 'shortRatio', #'sharesShort', 'floatShares', 'shortPercentOfFloat', 'exDividendDate', 'dividendDate','dividendYield', 'fiveYearAvgDividendYield', 'payoutRatio', 'trailingPE', 'forwardPE', 'priceToBook', ]] Let's go through the code:
Liquidity is the currency of bear markets. Market capitalisation is calculated as share price times number of outstanding shares. Logically speaking, the larger the number of shares available for trading, the more liquid.
There may be endless potential shorts in the small cap universe. Market efficiency is a direct function of liquidity. The more illiquid, the longer it takes to arbitrage an inefficiency. In other words, market impact on the way in and out is not worth the price of inefficiencies. During bear markets, the market capitalizations of small caps melt like butter in the sun. And yet, they are particularly difficult to short. Selling short exacerbates the selling pressure while absence of demand drags prices down.
Downstream from market capitalization is liquidity. As a general rule of thumb, only short issues with an average trading value well above $1 million. Anything below is unlikely to have decent borrow available.
Next, we will avoid a common tourist trap: crowded shorts.
"Elvis has left the building"
Back in 2007, I developed a little weapon of mass short destruction (WMSD) eponymously named "squeeze box." The mechanics might have been counterintuitive, but it had a surreal accuracy rate to predict potential short squeezes. All it took was a long-only manager to take a minuscule speculative long position. Since the buy/sell equilibrium was already out of balance, this would push the price up and flush the "tourists" out. They would frantically cover, which would rapidly morph into a short squeeze. At this point, the long-only manager would leisurely exit his long position with a comfortable profit. When I realized that this program directly hurt my friends back in the hedge fund world, this WMSD was permanently dismantled.
Moral of the story: Eliminate all crowded shorts—issues where borrow utilization exceeds 50%—from your investment universe. They should not even be on your radar screen. The way to make money on the short side is to find what institutional investors are liquidating and ride their tails. Good short stories usually make up for bad short trades. Every bit of information has a tick price tag attached to it. Do not wait for all the pieces of the puzzle to fit. By the time a story has deteriorated enough to become an obvious short candidate, institutional investors will have liquidated their long positions. The only people left will be short-sellers fighting over a dry bone. This leads us to a simple and powerful supply and demand indicator called borrow utilization.
Borrow utilization is the ratio of borrow taken out for short-selling divided by the supply of shares available for borrow. Large institutions have lending programs wherein they lend a portion of their long holdings for a fee. When they trim those positions, supply dries up.
Meanwhile, as shorts gain popularity, demand for borrow increases. Demand on the numerator and supply on the denominator is what makes borrow utilization the most effective measure of both institutional ownership and popularity. Institutions are considered stable long-term shareholders. They engage in stock lending programs to earn interest and juice up their returns. For example, borrow utilization north of 50% simply means that the shorting appetite exceeds institutional lending programs. Since institutions are the major players in the stock lending markets, the risk/reward ratio evidently deteriorates.
Borrow utilization can vary significantly based on a company's ownership structure. Some shareholders do not participate in lending programs. For example, tightly held companies have few shareholders, who, for inscrutable reasons, often fail to appreciate their babies being the targets of vicious bear raids. Unfortunately yfinance does not feature borrow utilisation. Yahoo has two fields.
Short ratio is the number of days it takes to cover the existing shares shorted. It is calculated as the number of shares shorted divided by the average volume. The longer the duration to cover, the more painful and protracted a short squeeze would be. The rationale would be that short sellers' frantic short cover would push prices up and force other participants to cover as well. In theory, it makes sense. In practice, it might fuel and exacerbate a short squeeze for one or two days and then things would revert back to normal.
short_ratio = sp500_info_df['shortRatio'].rank(pct=True).sort_values( ascending=False).index[:10].tolist() sp500_info_df.loc[high_short_ratio, ['shortName','sector','shortPctFloat', 'shortRatio']].round(3).head(10) Let's explain this snippet:
This gives the following table at the time of writing:

Figure 7.1: Top 10 short ratios
The second ratio is the shares shorted as a percentage of the free float. This is more interesting than the short ratio. Free float is the number of a company's shares that are available for public trading, excluding shares held by insiders, governments, or long-term strategic holders.
short_float = sp500_info_df['shortPercentOfFloat'].rank(pct=True).sort_values(ascending=False).index[:10].tolist() sp500_info_df.loc[high_short_float, ['shortName','sector','shortPercentOfFloat', 'shortRatio']].round(3).head(10) This gives us the following table:

Figure 7.2: Top 10 short as a percentage of the free float
The most heavily shorted stocks are less than 1/5 of the free float. Anything beyond the top 10 represents less than 10% of the float. This means that short sellers do not constitute a material threat to share prices. Let's illustrate this further by calculating a sector average.
sp500_info_df.groupby('sector')[['shortPctFloat','shortRatio']].mean().sort_values(by='shortPctFloat', ascending=False).round(3) This gives us the following table:

Figure 7.3: Top 10 short as a percentage of the free float
It is high time we put an enduring myth to rest. Short sellers do not cause share prices to melt. Shorts represent an average of 3.5% of the free float of the S&P 500. It would take 3.4 days to cover the entire short selling across the most liquid equity market on the planet. That proves beyond the shadow of a doubt that even with unfriendly intentions, foul language, short sellers still could not tank share prices.
Short-selling is an expensive sport. Short-sellers are charged borrowing fees. These range from general collateral (GC) on easy-to-borrow issues to prohibitive rates on hard-to-borrow or crowded shorts. As borrow gradually gets taken out, the quality of the remaining pool deteriorates. Adding to this, hard-to-borrow issues do not only fetch usury rates, they are sometimes callable. This means the lenders have the option of withdrawing their lending (shares lent) back on short notice. This is referred to as "recall."
When recalls happen, short-sellers are left to either locate borrows elsewhere or close their positions. Recalls sometimes cause short squeezes.
The last tourist short-sellers to get on board are often too excited to think twice about borrowing callable stocks. When a recall happens, they cannot locate borrow and are forced to close. Since selling pressure has already reached a climax, the new buying pressure creates an imbalance in the supply/demand. Share price rises effortlessly. This triggers other short-sellers to stop losses, and they proceed to cover. This rapidly snowballs into a full-blown short squeeze that flushes out not only tourists but the more seasoned short-sellers as well. Bottom line: stay away from issues where other short-sellers tap into low quality borrow.
Crowded shorts tend to have lower sensitivity to the market than the more popular issues. When the market hits a periodic air pocket, crowded shorts barely move. Meanwhile, high flying stocks on the long side nosedive. In relative terms, this means that crowded shorts outperform and longs underperform. One possible explanation for this phenomenon is the lack of participation from institutional and retail investors. There is no institutional long money in crowded shorts, so when a periodic pullback happens, there is no one to sell. Bottom line, crowded shorts do not edge long positions when it really matters. Simply said, when everything else tanks, crowded shorts do not drop as much. It may sound counterintuitive, but crowded shorts relatively outperform the markets during pullbacks.
Conclusion: Just like dieters need to remove ice cream and potato chips from reach, weed out crowded shorts from your investment universe altogether.
Next, we will look for shorts hiding behind high dividend yields.
Ex-growth companies often increase their dividend yield to bring dividend support to their share price. They realize they underperform the benchmark. So, they compensate lackluster returns with generous dividend policies. This effectively deters short-sellers from engaging in bear campaigns. While a high dividend yield might stem immediate short-selling pressure, it does not prevent share price from falling long term. This is what makes the high-yielding stock universe fertile ground for profitable, peaceful short-selling.
Let's prove that point using the sp500_info_df. In , we defined defensive and cyclical sectors. Cyclical sectors lead the charge while defensive sectors trail the benchmark during bull markets. Let's calculate the average dividend yield by sector.
sp500_info_df.groupby('sector')[['dividendYield', 'fiveYearAvgDividendYield', 'payoutRatio']].mean().sort_values(by='fiveYearAvgDividendYield', ascending=False).round(2) This is how we did it:
dividendYield, fiveYearAvgDividendYield, and payoutRatio per sectorThis gives us the following table:

Figure 7.4: Average dividend yield and payout ratio by sector
Utilities and Consumer Defensive sectors have 5-year average dividend yields at 3.34% and 2.78%, respectively. Those are quintessentially defensive sectors that proudly lag the index. Meanwhile, consumer cyclicals and technology sport 5-year average yields of 2.12.33% and 1.41.58%, respectively. Growth stocks often reinvest their earnings into the business. Apple, once the largest market cap in the multiverse, has long sported a stingy dividend policy as a badge of honor.
The good news about dividends is that they are predictable corporate events. There are a few events that short-sellers need to be mindful of. The first two are record and payment dates. Short-sellers need to curb their enthusiasm in these moments. Companies sometimes decide to raise dividends around the time they make earnings announcements. Articulate your trading around those dates, and you will have a surprisingly pleasant time. Dividend record dates are also called ex-dates. Short-sellers are liable for dividends when holding a short position through a record date. Keep a dividend calendar close to your short positions.
You will need to convert the Unix date into YY-mm-dd format when using the yfinance library. Here is a quick snippet of code:
sp500_info_df[['exDividendDate', 'dividendDate']] = sp500_info_df[['exDividendDate', 'dividendDate']].apply(lambda x: pd.to_datetime(x).dt.strftime('%Y-%m-%d')) sp500_info_df[['exDividendDate', 'dividendDate']] Another way of reframing high-yield stocks as viable short candidates is to focus on the underlying reason for the dividend being so high in the first place. Growth stocks usually have miserable dividend yields. They need their cash to reinvest in their businesses. Stable, mature companies have excess cash but unsexy long-term prospects. A little bit of dividend goes a long way to court investors.
Next, we will look at a legal price control scheme.
"A man generally has two reasons for doing one thing. One that sounds good and the real one"
– JP Morgan
Conventional wisdom has it that corporates will buy back their shares on the open market when they are undervalued. In practice, They are just as clueless as sell-side analysts when it comes to timing undervaluation. Share buy-backs peaked in late 2007 and bottomed out in March 2009.
Proponents for share buy-backs argue that there are no better use of their cash and cheap credit than buying back their shares. This creates a trickle-down effect and benefits the economy at large. This shareholder supremacy dates back to a school of thought pioneered by Milton Friedman in the 80s'. Corporates have become increasingly big players on the markets through share buy-back programs. The rally that followed President Trump tax cut was primarily driven by corporates buying back their own stock.
Adversaries argue that cash is siphoned from stakeholders: employees, customers, R&D and suppliers, then channeled toward shareholders. Simply said, the coach skimps on the players but spreads largesse onto the fans. The majority of top executives' compensation comes from stock options. They therefore have a direct incentive to prop up share prices to pad their stock option plans. The most powerful way is to buy back stock. This reduces liquidity, inflates earnings per share, and drives share price up.
The Corona pandemic of 2020 shed a crude light on this hypocrisy and settled the debate once and for all. Share buybacks siphon the life out of companies. This obsessive focus on short-term market impact is not aligned with the long-term interests of companies.
This is, however, besides the point for short-sellers. Corporates have pockets deep enough to artificially levitate share prices. Short-sellers should therefore refrain from shorting companies that engage in share buyback programs.
The good news is that share buybacks are highly correlated to market gyrations. Share buybacks evaporate when most needed, during corrections.
Next, we will finally talk about fundamental analysis.
"Time is on my side"
– Mick Jagger
Fundamental analysis gives its letters of nobility to stock market analysis. Few endeavors are as intellectually stimulating as stock analysis. Regime definition can greatly simplify the work of fundamental analysts.
When fundamental analysts do not consider market regime, they essentially try to answer a theoretical question. They try to figure out why a stock should go down. There are multitudes of reasons. They may eventually be vindicated. Yet they take on massive timing, reputational, and ultimately business risk. They hope markets will agree with their thesis before investors lose patience.
On the other hand, subjecting fundamental analysis to market regimes tries to answer a practical question: why is this stock going down? The good news is the answer generally falls in only three buckets.
On the short side, conventional fundamental analysis is difficult to do. Asymmetry of information is something short-sellers must contend with. Companies rarely volunteer bad news. Analysts keep their "buy" ratings until the fateful day when they happen to read in the press that companies have filed for bankruptcy the night before.
Sell-side cheerleaders may however have some involuntary marginal utility. In the permafrost tundra "Buy rating", the code word for underperformance is "Buy for the long-term investors". Look for stocks where ratings are not refreshed, earnings models gather dust, where even maintenance research is not properly maintained. One phone call to confirm that "those are stocks for long-term investors" and you are in business.
Next, we will consider valuations. This is far from trivial.
Once you have distilled your investment universe, it is time to pull the trigger. Next, let's take a look at average sector valuations:
sp500_info_df.groupby('sector')[['trailingPE', 'forwardPE', 'priceToBook']].median().sort_values(by='trailingPE', ascending=False).round(1) How it works:
trailingPE, forwardPE, and priceToBook to avoid outlier distortion.This gives the following table:

Figure 7.5: Average trailing and forward-looking PER, PBR
Technology and internet stocks have been sporting gravity-defying valuations and yet they have spearheaded this bull market. Note that the forward-looking price to earnings (PE) appear reasonable. This means that earnings momentum is still strong. Those valuations are rich, but they deliver solid earnings growth. This gap between trailing PE, PE calculated on past earnings per share (EPS), and forward-looking PE, calculated on estimates, justifies high expectations and high valuations. Should earnings disappoint, the market will not fail to teach some table manners to those insolent stocks. Meanwhile, the valuations of defensive sectors are below market average. Those stocks have committed the ultimate crime on the markets. They are boring. There is no story to write about. No-one gets excited about a food stock baking bread. Consequently, the lack of demand for those sectors in a strong bull market pushes valuations down.
Now, let's go back to the growth premium for a bit. Growth stocks often sport insolent valuations. In "The Little Book That Beats the Market", Joel Greenblatt used Microsoft to illustrate a simple point: a great company can still produce poor investment returns if you pay too high a price for its earnings. In the early 90s, Microsoft insolently traded at insolently rich valuations, sometimes 50–60× earnings or more. Yet, the company delivered year after year. Microsoft was an exceptional business: high growth, high return on capital, dominant products. In the late 90s, growth started to slow down. Valuations compressed and the stock went nowhere for years. Bottom line, do not short stocks with rich valuations alone. Short high valuations only when earnings momentum decelerates. In financial creole, this is called "multiples compression". For example, this means a PE of 35 will come down to 20, because the growth premium disappears. Joel Greenblatt showed that even if Microsoft continued growing strongly, those already priced-in expectations made the future returns unattractive.
Price to Book Ratio (PBR) is a reflection on the general health of the balance sheet. Expensive PBR means that book per share (equity + retained earnings) is expensive relative to the current share price. In other words, growth and investments have been financed either by issuance of shares or by debt. In accounting parlance, this means investing cash flow has been financed by the financing cash flow instead of operating cash flow. In any case, this fragilizes the balance sheet. When the music stops, highly leveraged companies often have a rude encounter with Newtonian physics. Never keep highly leveraged companies on the long side of the book through a bear market.
Next, we looked at sectors. In Chapter 3, we assigned sectors labels such as defensive or cyclicals. Let's take a look at beta and see if we can build a mathematical foundation.
"CAPM and beta imply that buying riskier stocks should give you higher returns, but the data says the opposite".
Joel Greenblatt
Beta measures how much a stock moves relative to its benchmark. High beta means stocks move "faster" than the market and vice versa for low beta stocks. For example, a beta of 1.2 means that when the benchmark moves by 1%, the issue would move by 1.2%. Market participants really need to understand how beta works and more specifically how to balance the Long/Short net beta exposure.
In the following subsections, we will download historical prices since January 2015. We define and calculate monthly beta. We perform some sector analysis. We define high and low beta custom indices. We calculate daily log and cumulative returns. We then drill down to the highest and lowest betas' returns.
Without further ado, let's download the historical prices. We recycle the arctic database we created in Chapter 5. We start by defining the functions.
def initialise_adb_library_local(uri_path, library_name): uri = f"lmdb://{uri_path}" # this will set up the storage using the local file system ac = adb.Arctic(uri) library = ac.get_library(library_name, create_if_missing=True,)# library_options=LibraryOptions(dynamic_schema=True)) return library def adb_concat_single_column(library, symbols, column_name): symbols_list = [symbol for symbol in symbols if symbol in library.list_symbols()] comp_list_from_adb = [library.read(symbol, columns=[column_name]).data.rename(columns={column_name: symbol}) for symbol in symbols_list] return pd.concat(comp_list_from_adb, axis=1) Then we download Close prices in a df called px_df.
library_SP500 = initialise_adb_library_local('data', 'SP500') px_df = adb_concat_single_column(library_SP500, library_SP500.list_symbols(), 'Close').round(2) px_df_rel = round(px_df.divide(px_df[bm_ticker], axis=0).mul(px_df.iloc[0,list(px_df.columns).index(bm_ticker)]),1) px_df_rel = px_df_rel.round(2) Accessing local Arctic database to retrieve closing prices for all S&P 500 stocks and compute relative performance versus the benchmark.
library_SP500: Create or connect to the SP500 library in the local LMDB database at the data folder.adb_concat_single_column() to extract Close column for all symbols, concatenate into wide-format DataFrame. We round absolute prices to 2 decimals (px_df).px_df_rel).This produces two DataFrames: px_df contains absolute closing prices, while px_df_rel shows each stock's relative performance indexed to the benchmark.
Next, we will define beta and calculate monthly betas.
def beta(df,bm_col): log_returns = np.log(df/df.shift(1)) var = log_returns[bm_col].var() cov = log_returns.cov() beta = round(cov.loc[:,bm_col] / var,2) return beta In high school parlance, beta is a covariance matrix. The formula is as elegant as it is mathematically simple. We calculate a covariance matrix with the benchmark and then divide it by the variance matrix to measure the dispersion.
Here is how we coded it:
Next, we will calculate the monthly beta. In theory, we could use the method apply to calculate a rolling daily beta. This would, however, take time. Beta does not move much from one month to the next in regular market conditions. So, we will resample the px_df dataframe to month ends and calculate beta as a multiple of 12 months.
This is what the code looks like:
bm_col = '^GSPC' window = 12 * 3 def monthly_beta(df, bm_col, window): month_end_list = df.resample('ME', closed='left', label='right').last().index.to_list() beta_df = pd.DataFrame( ) for t,dt in enumerate(month_end_list): beta_monthly_df = beta(df[dt:month_end_list[t + window]],bm_col) beta_df = pd.concat([beta_df,beta_monthly_df],axis =1) beta_df = beta_df.rename(columns={bm_col: month_end_list[t + window]}) if month_end_list[t + window] == month_end_list[-1]: print(t,dt,month_end_list[t+window]) break return beta_df beta_df = monthly_beta(px_df, bm_col, window) beta_df.tail() Computing monthly updated betas using a 3-year rolling window to track how systematic risk evolves over time.
This generates a dataFrame with betas for each stock (rows) calculated at each month-end (columns), showing how risk profiles change across market cycles.
Once we have calculated beta for the entire market, we will update the arctic database.
monthly_beta_df = beta_df.T for ticker in library_SP500.list_symbols(): df = library_SP500.read(ticker).data df = df.dropna(subset =['Close','High','Low','Open' ]) df = pd.concat([df,monthly_beta_df[ticker]],axis =1) df = df.rename(columns={ticker:'beta'}) df['beta'] = df['beta'].ffill() df = df.loc[:, ~df.columns.duplicated(keep='last')] library_SP500.write(ticker, df) Writing rolling 36-month beta values to Arctic database for all S&P 500 tickers, ensuring each stock record includes a monthly-updated systematic risk metric.
beta_df: we convert from ticker-indexed to date-indexed format with tickers as columns (monthly_beta_df).library_SP500.list_symbols(), read complete ticker data from the database.monthly_beta_df[ticker] column with ticker DataFrame on matching dates (axis=1). We rename the concatenated column to beta; apply forward-fill to propagate values through data gaps.AllS&P500stockrecordsinArcticdatabasenowcontainmonthly-rollingbetavalues forward-filled values ensure continuous coverage from the first available beta date forward.
Calculating average beta by sector
Next, we will concatenate the beta_df with the sector column to the sp500_info_df. We will perform some sector analysis.
df_SP500_beta = pd.DataFrame() df_SP500_beta = pd.concat([sp500_info_df['sector'], beta_df], axis=1) sector_beta_pivot = pd.pivot_table(df_SP500_beta, values=list(beta_df.columns), index= ['sector'], aggfunc="mean").T sector_beta_pivot.plot(figsize=(15,4),title = 'Beta by Sector') plt.legend(loc='lower left') Aggregating individual stock betas to the sector level and plotting time series can help identify which sectors have high or low beta.
This is how we do it:
This produces a line chart revealing which sectors exhibit higher or lower (e.g., Technology, Utilities) systematic risk and how sector betas shift during bull and bear markets.

Figure 7.6: Average beta by sector
The above graph sets mathematical foundations on something we named in . Cyclical sectors such as technology and consumer cyclicals are also often referred to as high beta. Conversely, defensive sectors, such as utilities and consumer defensive, trail the benchmark with sub-1 betas.
This paints an interesting dichotomy of the market. Go short defensive sectors in a bull market and go long cyclicals. This echoes something we saw in . Defensive sectors such as consumer staples and utilities are impervious to the economic cycle. They do not go down during downturns on the basis that people need food and electricity. They do not participate in economic upturns either. Meanwhile, cyclical sectors, such as consumer cyclicals and technology, move with the economic cycles.
Next, let's calculate the daily log and cumulative returns across sectors. Note that we will calculate a simple lateral average, otherwise referred to as equal weight.
We have calculated the average beta by sector. Next, we will calculate returns in absolute terms and relative to the index for all the constituents. We will then calculate the average by sector. Let's start with the daily log and then calculate the cumulative returns.
daily_log_returns = np.log(px_df/px_df.shift(1)) daily_rel_log_returns = daily_log_returns.sub(daily_log_returns[bm_col],axis=0) cum_returns_abs = daily_log_returns.cumsum().apply(np.exp)-1 cum_returns_rel = daily_rel_log_returns.cumsum().apply(np.exp)-1 Computing daily log returns in both absolute and benchmark-relative terms for performance attribution.
Here is how it works:
This results in four DataFrames, enabling analysis of both absolute performance and alpha generation—excess returns over the S&P 500 index.
Next we will calculate and chart absolute and relative returns by sector.
sector_abs_returns = pd.concat([sp500_info_df['sector'],cum_returns_abs.T],axis=1) sector_avg_abs_returns = sector_abs_returns.groupby('sector').mean().T sector_avg_abs_returns.plot(figsize=(15,4), title='Sector Average Absolute Returns vs S&P 500', grid=True) plt.legend(loc='upper left') plt.show() sector_rel_returns = pd.concat([sp500_info_df['sector'],cum_returns_rel.T],axis=1) sector_avg_rel_returns = sector_rel_returns.groupby('sector').mean().T sector_avg_rel_returns.plot(figsize=(15,4), title='Sector Average Relative Returns vs S&P 500', grid=True) plt.legend(loc='upper left') plt.show() Comparing sector-level absolute and relative returns to identify which sectors out/underperform the S&P 500 benchmark.
Here are the steps:
This produces two visualizations showing sector performance trends—absolute chart reveals total returns while relative chart highlights which sectors generated alpha (outperformance) versus the benchmark.

Figure 7.7: Sector cumulative average absolute returns
As Warren Buffet famously said: "A rising tide lifts all ships." All sectors have gone up in tandem. Apart from technology, which has dwarfed the pack, it is quite difficult to sort the winners from the losers.
Next, we will display the equal-weight cumulative relative returns by sector.

Figure 7.8: Sector average relative returns
Relative returns paint a better picture of the performance of sectors over time. Next, let's build custom indices by beta.
Now that we have calculated returns and beta by sector, let's put all the pieces together and build custom indices: high and low beta. High beta index will be the simple average (equal weight) of the two highest beta sectors: "technology" and "consumer cyclical." Conversely, low beta will be the simple average (equal weight) of the two lowest beta sectors: "utilities" and "consumer defensive."
Let's publish the code:
high_beta_sectors = ['Consumer Cyclical', 'Technology'] low_beta_sectors = ['Consumer Defensive', 'Utilities'] high_beta_rel_returns = sector_rel_returns.loc[sector_rel_returns[ 'sector'].isin(high_beta_sectors)].drop(columns=['sector']) low_beta_rel_returns = sector_rel_returns.loc[sector_rel_returns[ 'sector'].isin(low_beta_sectors)].drop(columns=['sector']) high_beta_abs_returns = sector_abs_returns.loc[sector_abs_returns[ 'sector'].isin(high_beta_sectors)].drop(columns=['sector']) low_beta_abs_returns = sector_abs_returns.loc[sector_abs_returns[ 'sector'].isin(low_beta_sectors)].drop(columns=['sector']) beta_cum_returns = pd.DataFrame() beta_cum_returns[bm_col]= cum_returns_abs[bm_col].copy() beta_cum_returns['high_beta_rel_avg'] = high_beta_rel_returns.mean(axis=0) beta_cum_returns['low_beta_rel_avg'] = low_beta_rel_returns.mean(axis=0) beta_cum_returns['high_low_beta_rel_avg'] = beta_cum_returns['high_beta_rel_avg'].sub( beta_cum_returns['low_beta_rel_avg'] ) beta_cum_returns['high_beta_abs_avg'] = high_beta_abs_returns.mean(axis=0) beta_cum_returns['low_beta_abs_avg'] = low_beta_abs_returns.mean(axis=0) beta_cum_returns['high_low_beta_abs_avg'] = beta_cum_returns['high_beta_abs_avg'].sub( beta_cum_returns['low_beta_abs_avg'] ) Analyzing performance differences between cyclical high-beta sectors (Technology, Consumer Cyclical) and defensive low-beta sectors (Utilities, Consumer Defensive).
How it works step by step:
sector_abs_returns.Next, let's plot the relative and absolute returns:
beta_cum_returns[[bm_col,'high_beta_rel_avg','low_beta_rel_avg', 'high_low_beta_rel_avg']].plot( grid = True, figsize= (15,4), title = 'High Low Beta Sectors Relative Returns') beta_cum_returns[[bm_col,'high_beta_abs_avg','low_beta_abs_avg', 'high_low_beta_abs_avg']].plot( grid = True, figsize= (15,4), title = 'High Low Beta Sectors Absolute Returns') Comparing high-beta versus low-beta sector performance.
The absolute chart shows the returns without adjustment for the benchmark returns. This is why it looks so much better. The relative chart shows the excess returns over the benchmark.
Let's start with the absolute returns:

Figure 7.9: High and Low Beta v. S&P 500 absolute returns
The high beta index has really outperformed the low beta one. Drawdowns, however, are not commercially viable. Investors will not sit through routine –20~40%.
Next, let's publish the relative returns:

Figure 7.10: High and Low Beta versus S&P 500 relative returns
On the surface, it may seem like the net or high beta index underperforms the S&P 500 index. Those are the excess returns over the index. If the high and net beta underperformed the index, this would be negative, like the low beta index. Bottom line, these are promising excess returns. It is important to understand that high betas will take returns to exhilarating highs but also gut-wrenching lows.
Before we start loading the truck on high beta indiscriminately, we should probably be aware of the low beta anomaly. According to the Capital Asset Pricing Model (CAPM), risk is positively correlated with returns. This means high beta stocks carry inherently more risk and should generate high returns. In practice, low beta stocks tend to generate high returns in long-only portfolios over long durations.
In a long short context, however, things tend to move differently. When markets are in bullish territory, go long high beta and short low beta. In bearish markets, keep the same stocks, switch sides. The trick is to negotiate stock changes in regime and sideways markets. The solution may not be stock selection but risk management as we will see in .
Next, let's see what happens when we pick the top and bottom 5 betas.
In the following block of code, we drill down to the individual stock level. We select the top and bottom 5 betas at the end of every month. We calculate the average returns for the following month. The portfolio is rebalanced every month. We do not factor in transaction costs.
pos_count = 5 top_beta_returns_list = [] bottom_beta_returns_list = [] beta_returns_df = pd.DataFrame() for t in range(1,len(beta_df.columns)): period_start = beta_df.columns[t-1] period_end = beta_df.columns[t] top_beta = beta_df[period_start].nlargest(n = pos_count).index bottom_beta = beta_df[period_start].nsmallest(n = pos_count).index top_beta_returns = daily_rel_log_returns.loc[period_start:period_end, top_beta].iloc[1:].mean(axis=1) bottom_beta_returns = daily_rel_log_returns.loc[period_start:period_end, bottom_beta].iloc[1:].mean(axis=1) top_beta_returns_list.append(top_beta_returns) bottom_beta_returns_list.append(bottom_beta_returns) top_beta_returns_df = pd.concat(top_beta_returns_list) bottom_beta_returns_df = pd.concat(bottom_beta_returns_list) beta_returns_df[f'top_{pos_count}_beta'] = top_beta_returns_df beta_returns_df[f'bottom_{pos_count}_beta'] = bottom_beta_returns_df beta_returns_df[f'net_{pos_count}_beta'] = beta_returns_df[f'top_{pos_count}_beta'].sub(beta_returns_df[f'bottom_{pos_count}_beta']) beta_returns_df[bm_col] = daily_log_returns[bm_col] beta_returns_df = beta_returns_df.cumsum() beta_returns_df.plot(figsize=(15,4), grid = True, title=f'Top vs Bottom Beta {pos_count} Stocks Cumulative Returns' ) Testing monthly rebalanced strategy: long top-beta stocks, compare to bottom-beta stocks to measure beta factor premium.
This is how we do it step by step:
This code produces the following chart:

Figure 7.11: Top/Bottom 5 Betas versus S&P 500 cumulative relative returns
That tame low volatility line? This is just the S&P 500. To say that this strategy is rock n' roll would be the understatement of the year. Going long top betas and shorting the bottom ones will deliver spectacular results in strong bull markets. It will however be devastating in any other times. This illustrates the low beta anomaly mentioned earlier in this chapter. High beta is positively correlated with risk, but not always returns.
Top and bottom betas may be too volatile or too defensive. Perhaps lower and higher decile betas would have better return profiles. But for now, let's see what happens when we select for the biggest rises and drops in betas.
Going long top and short bottom betas is not a commercially viable strategy. The volatility is too high to be an investable product. Let's take a different approach. see how rising and falling beta momentum would fare. In the next block of code, we will select the top 5 largest increase and decrease in beta, calculate the average return and rebalance the portfolio on a monthly basis. The objective is to see if returns are positively correlated with rising or falling beta.
First, we publish the code then we will explain it step by step:
beta_diff = beta_df.pct_change(axis=1, fill_method = None).dropna(axis=1, how = 'all') top_beta_diff_returns_list = [] bottom_beta_diff_returns_list = [] beta_returns_diff = pd.DataFrame() for t in range(1,len(beta_diff.columns)): period_start = beta_diff.columns[t-1] period_end = beta_diff.columns[t] top_beta = beta_diff[period_start].nlargest(n = pos_count).index bottom_beta = beta_diff[period_start].nsmallest(n = pos_count).index top_beta_diff_returns = daily_rel_log_returns.loc[period_start:period_end, top_beta].iloc[1:].mean(axis=1)#.sum() bottom_beta_diff_returns = daily_rel_log_returns.loc[period_start:period_end, bottom_beta].iloc[1:].mean(axis=1)#.sum() top_beta_diff_returns_list.append(top_beta_diff_returns) bottom_beta_diff_returns_list.append(bottom_beta_diff_returns) top_beta_diff_returns_df = pd.concat(top_beta_diff_returns_list) bottom_beta_diff_returns_df = pd.concat(bottom_beta_diff_returns_list) beta_returns_diff[f'rising_{pos_count}_beta'] = top_beta_diff_returns_df beta_returns_diff[f'falling_{pos_count}_beta'] = bottom_beta_diff_returns_df beta_returns_diff[f'net_{pos_count}_beta'] = beta_returns_diff[f'falling_{pos_count}_beta'].sub(beta_returns_diff[f'rising_{pos_count}_beta']) beta_returns_diff[bm_col] = daily_log_returns[bm_col] beta_returns_diff = beta_returns_diff.cumsum() beta_returns_diff.plot(figsize=(15,4), grid = True, title=f'{pos_count} Fallling V. Rising Betas Cumulative Returns' ) Backtesting beta momentum strategy (rising versus falling betas). This code tests a strategy based on beta changes rather than absolute beta levels, targeting stocks with rising or falling systematic risk.
Let's go through the code step by step:
Result: Chart showing high-beta stocks typically outperform in bull markets while low-beta stocks provide downside protection in bear markets.
Let's publish the chart:

Figure 7.12: Beta momentum versus S&P 500 cumulative relative returns
Fast rising beta stocks look like the kind of stocks that would be featured in Jim Cramer's popular show Mad Money. They have a compelling story in theory but tend to be gravitationally challenged in practice.
Rising beta stands out as being inversely correlated with returns. The faster beta rises, the worse the returns. This is a counterintuitive finding. It flies in the face of CAPM. Classic financial education postulates that returns should be correlated with risk. Rising risk is inversely correlated with returns.
Conversely, falling betas are positively correlated with excess returns. This means that issues whose sensitivity to the market decreases tend to outperform over time. In other words, once issues fade from the limelight, volatility abates, and they perform better. This rudimentary beta momentum analysis has inherent flaws such as survivorship bias. It is by no means an exhaustive study. It is, however, worth digging deeper and extending to other markets.
In summary, beta is a double-edged sword. Market participants who fail to understand it can find themselves on the wrong side of a trade. The easiest way to manage the risk associated with beta is to size positions accordingly. Inverse beta is a popular method that equalizes risk. For example, let's take two stocks A and B with betas of 1.25 and 0.8 (0.8 is the inverse of 1.25), then assign them raw weights of 0.5% fixed risk. If we want to neutralize risk using the inverse beta, their respective size would look like this:
Now, let's assume we would want a net beta-neutral portfolio with A on the long side and B on the short one.
If we want to neutralize the beta, i.e. a net beta neutral portfolio, we need to have a bigger short book. This mechanically leads to a negative net exposure of -0.225%. This explains why seemingly market neutral portfolios, with net exposure hovering around 0, often have a residual bullish tilt. We are now touching on an important concept that we will revisit in the next chapter on the Long/Short toolbox: net exposure versus net beta. For now, let's summarize what we have learned in this chapter.
This chapter provided a structured process for boiling down a broad investment universe to a practical list of securities suitable for short selling. Because of the inherent asymmetrical risk in short selling, the chapter emphasized strict filtering based on liquidity, crowding, corporate actions, fundamentals, valuations, and beta.
The first filter is liquidity, described as the currency of the bear markets. Liquidity often evaporates during selloffs, so short positions must be chosen with the exit in mind before entering. Small-cap stocks, despite offering many theoretical opportunities, are generally excluded due to thin trading volume, limited borrow availability, and painful short squeeze risk. A minimum of roughly $1 million in daily trading value is a practical threshold.
Next, the chapter highlights "crowded shorts" as perilous. When borrow utilization rises above about 50%, the risk of recalls and forced short cover increases sharply, while downside sensitivity to market declines weakens. Short ratio and short as a percentage of float are briefly described.
The chapter then examines dividend and buyback policies. High‑dividend stocks are sometimes defensive underperformers and are potential short candidates, since elevated yields often signal ex-growth fundamentals. In contrast, companies actively conducting share buybacks will artificially support their stock price, making them unattractive for shorts.
The fundamental analysis section explains that stocks fall for three main reasons: mispricing, sector rotation, or company-specific deterioration. The focus is on identifying value traps: firms with superficially cheap valuations but declining profitability or weakening balance sheets.
Finally, the chapter details valuation and beta effects. High valuations alone are insufficient for shorting; the key warning sign is expensive multiples paired with slowing earnings momentum. Beta analysis shows that sector betas vary widely across regimes. Rising betas tend to precede underperformance, while falling betas often precede outperformance, helping guide long/short portfolio construction.
In the next chapter, we will expand on beta and revisit concepts such as gross and net exposure that we saw in on position sizing.
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: Have your invoice handy. Purchases made directly from the Packt website don't require an invoice.