The previous notebook revealed that stocks with high borrow fees tend to decline. On the one hand, this tendency makes such stocks attractive to short. On the other hand, their high borrow fees make them unattractive to short. In the next two notebooks, we pit these competing forces against each other by using Moonshot to backtest a strategy that shorts the top decile of stocks by borrow fee, while also incurring the borrowing costs of those short positions.
The strategy is implemented in short-high-borrow.py.
Execute the following cell to move the strategy file to the /codeload/moonshot
directory:
# make directory if doesn't exist
!mkdir -p /codeload/moonshot
!mv short-high-borrow.py /codeload/moonshot/
The prices_to_signals
method of the Moonshot strategy replicates much of the logic from our Pipeline.
prices_to_signals
method, we use get_ibkr_borrow_fees_reindexed_like(...)
to pull borrow fees into the strategy. We then use rank(axis=1...)
to rank stocks daily by borrow fee, then select the top decile.are_common_stocks
and have_adequate_dollar_volumes
). These DataFrames are passed to Pandas' where()
method before ranking (are_common_stocks
is applied before ranking by dollar volume, and have_adequate_dollar_volumes
is applied before ranking by borrow fees). This approach of using where()
, which is typical in Moonshot, sets the dollar volumes or borrow fees, respectively, to NaN
for stocks that don't belong in the universe and thereby excludes such stocks from the ranking, as rank(...)
ignores NaN
s.from quantrocket.fundamental import get_ibkr_borrow_fees_reindexed_like
from quantrocket.master import get_securities_reindexed_like
...
class ShortHighBorrow(Moonshot):
...
def prices_to_signals(self, prices: pd.DataFrame):
closes = prices.loc["Close"]
volumes = prices.loc["Volume"]
# limit to common stocks...
sec_types = get_securities_reindexed_like(closes, "usstock_SecurityType2").loc["usstock_SecurityType2"]
are_common_stocks = sec_types == "Common Stock"
# ...in the top 75% by dollar volume
avg_dollar_volumes = (closes * volumes).rolling(30).mean()
dollar_volume_pct_ranks = avg_dollar_volumes.where(are_common_stocks).rank(axis=1, ascending=False, pct=True)
have_adequate_dollar_volumes = dollar_volume_pct_ranks <= 0.75
# rank by borrow fee and short the 10% with the highest fees
borrow_fees = get_ibkr_borrow_fees_reindexed_like(closes)
borrow_fee_ranks = borrow_fees.where(have_adequate_dollar_volumes).rank(axis=1, ascending=False, pct=True)
short_signals = borrow_fee_ranks <= 0.10
return -short_signals.astype(int)
Modeling borrowing costs for short positions is simple in Moonshot. Import the IBKRBorrowFees
slippage class and assign it to the SLIPPAGE_CLASSES
attribute of the Moonshot strategy:
from moonshot.slippage.borrowfee import IBKRBorrowFees
class ShortHighBorrow(Moonshot):
...
SLIPPAGE_CLASSES = IBKRBorrowFees