This notebook explores the percentage of listings that are shortable by country, above a given dollar volume threshold.
from quantrocket import get_prices
from quantrocket.fundamental import download_ibkr_shortable_shares, NoFundamentalData
from quantrocket.account import download_exchange_rates
from quantrocket.master import get_securities_reindexed_like
import pandas as pd
import io
Get exchange rates (for converting non-USD stocks to USD dollar volume):
f = io.StringIO()
download_exchange_rates(f, latest=True, base_currencies="USD")
rates = pd.read_csv(f)
rates = rates.set_index("QuoteCurrency").Rate.to_dict()
def get_sids(db, start_date, min_dollar_volume=0, max_dollar_volume=None, universes=None):
"""
Return the list of sids for the db, optionally with average dollar volume above or below the threshold(s).
"""
prices = get_prices(
db,
start_date=start_date,
universes=universes,
fields=["Close","Volume"])
closes = prices.loc["Close"]
volumes = prices.loc["Volume"]
securities = get_securities_reindexed_like(closes, fields=["Currency","Multiplier","PriceMagnifier"])
currencies = securities.loc["Currency"]
multipliers = securities.loc["Multiplier"]
magnifiers = securities.loc["PriceMagnifier"]
exchange_rates = currencies.applymap(lambda x: rates[x])
exchange_rates = exchange_rates.reindex(closes.index, method="ffill")
closes = closes * multipliers.fillna(1) / magnifiers.fillna(1)
dollar_volumes = closes.div(exchange_rates) * volumes
avg_dollar_volumes = dollar_volumes.mean()
if min_dollar_volume:
avg_dollar_volumes = avg_dollar_volumes[avg_dollar_volumes >= min_dollar_volume]
if max_dollar_volume:
avg_dollar_volumes = avg_dollar_volumes[avg_dollar_volumes <= max_dollar_volume]
sids = list(avg_dollar_volumes.index)
return sids
def get_shortable_universe_size(sids, start_date):
"""
Queries shortable shares and counts the unique sids.
"""
f = io.StringIO()
try:
download_ibkr_shortable_shares(f, sids=sids, start_date=start_date)
except NoFundamentalData:
return 0
else:
return len(pd.read_csv(f).Sid.unique())
def get_shortable_ratio(db, min_dollar_volume=None, max_dollar_volume=None):
"""
Divides count of shortable sids by total sids in the last 90 days.
"""
start_date = pd.Timestamp.today() - pd.Timedelta('90D')
start_date = start_date.date().isoformat()
sids = get_sids(db, start_date, min_dollar_volume=min_dollar_volume, max_dollar_volume=max_dollar_volume)
shortable_size = get_shortable_universe_size(sids, start_date)
return (shortable_size/len(sids)) * 100
Countries for which numbers should be run:
countries = (
# North America
("usa-stk-1d", "United States"),
("canada-stk-1d", "Canada"),
("mexico-stk-1d", "Mexico"),
# Asia
("australia-stk-1d", "Australia"),
("japan-stk-1d", "Japan"),
("hongkong-stk-1d", "Hong Kong"),
("singapore-stk-1d", "Singapore"),
# Europe
("spain-stk-1d", "Spain"),
("netherlands-stk-1d", "Netherlands"),
("uk-stk-1d", "United Kingdom"),
("germany-stk-1d", "Germany"),
("france-stk-1d", "France"),
("belgium-stk-1d", "Belgium"),
("swiss-stk-1d", "Switzerland"),
("austria-stk-1d", "Austria"),
("sweden-stk-1d", "Sweden"),
("italy-stk-1d", "Italy"),
)
Run numbers for each country, first with no dollar volume filter:
all_countries = {}
for db, country in countries:
print(country)
all_countries[country] = get_shortable_ratio(db, min_dollar_volume=0)
shortable_ratios = pd.Series(all_countries, name="shortable").to_frame()
shortable_ratios["not shortable"] = 100 - shortable_ratios
import seaborn as sns
import matplotlib.pyplot as plt
sns.set(palette=sns.color_palette("Reds_r", n_colors=2))
ax = shortable_ratios.sort_index().plot(kind="bar", stacked=True, title="Percentage of stocks that are shortable", figsize=(16,6))
ax.set_ylabel("Percentage of total")
plt.legend(loc='center left', bbox_to_anchor=(1, 0.5))
Repeat with a dollar volume threshold of $1M USD.
all_countries = {}
for db, country in countries:
print(country)
all_countries[country] = get_shortable_ratio(db, min_dollar_volume=1000000)
shortable_ratios = pd.Series(all_countries, name="shortable").to_frame()
shortable_ratios["not shortable"] = 100 - shortable_ratios
ax = shortable_ratios.sort_index().plot(kind="bar", stacked=True, title="Percentage of stocks with average daily dollar volume above $1M that are shortable", figsize=(16,6))
ax.set_ylabel("Percentage of total")
plt.legend(loc='center left', bbox_to_anchor=(1, 0.5))
Limit to stocks below $1M USD dollar volume:
all_countries = {}
for db, country in countries:
print(country)
all_countries[country] = get_shortable_ratio(db, max_dollar_volume=1000000)
shortable_ratios = pd.Series(all_countries, name="shortable").to_frame()
shortable_ratios["not shortable"] = 100 - shortable_ratios
ax = shortable_ratios.sort_index().plot(kind="bar", stacked=True, title="Percentage of stocks with average daily dollar volume below $1M that are shortable", figsize=(16,6))
ax.set_ylabel("Percentage of total")
plt.legend(loc='center left', bbox_to_anchor=(1, 0.5))