Shorting Liquid Stocks with the Highest Borrow Fees
Summary
This strategy selects common stocks with relatively high trading liquidity, ranks them by current borrow fees, and shorts the highest-fee group. Liquidity is estimated from the rolling average of closing price times volume over 30 days; the strategy keeps the top 75% by that measure, then targets the 10% with the highest borrow fees. It assigns equal weights to the short positions.
Signals are shifted so positions begin after the signal day. The return calculation uses changes between daily opens, and the strategy specifies a borrow-fee slippage model. These details describe the intended timing and trading costs, but the document provides no performance results or validation. The approach depends on reliable borrow-fee data and assumes high fees identify stocks worth shorting; borrow cost alone does not establish that prices will decline. Equal weighting also does not ensure equal risk across stocks.
Key ideas
- The universe is limited to common stocks ranked in the top 75% by 30-day average dollar volume.
- Stocks are ranked by borrow fee, and the top 10% are selected for short positions.
- Selected positions receive equal weights and are entered after the signal day.
- Returns are calculated from open-to-open price changes, with a borrow-fee slippage model specified.
Tags
Full text
# ShortHighBorrow
# ShortHighBorrow
Strategy that shorts stocks with the highest borrow fees.
## Source (Apache-2.0)
```python
# Copyright 2024 QuantRocket LLC - All Rights Reserved
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import pandas as pd
from moonshot import Moonshot
from moonshot.slippage.borrowfee import IBKRBorrowFees
from quantrocket.fundamental import get_ibkr_borrow_fees_reindexed_like
from quantrocket.master import get_securities_reindexed_like
class ShortHighBorrow(Moonshot):
"""
Strategy that shorts stocks with the highest borrow fees.
"""
CODE = "short-high-borrow"
DB = "usstock-1d-bundle"
DB_FIELDS = ["Open", "Close", "Volume"]
BORROW_FEE_TOP_N_PCT = 10
DOLLAR_VOLUME_TOP_N_PCT = 75
SLIPPAGE_CLASSES = IBKRBorrowFees
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 <= (self.DOLLAR_VOLUME_TOP_N_PCT / 100)
# 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 <= (self.BORROW_FEE_TOP_N_PCT / 100)
return -short_signals.astype(int)
def signals_to_target_weights(self, signals: pd.DataFrame, prices: pd.DataFrame):
weights = self.allocate_equal_weights(signals)
return weights
def target_weights_to_positions(self, weights: pd.DataFrame, prices: pd.DataFrame):
# Enter the position the day after the signal
return weights.shift()
def positions_to_gross_returns(self, positions: pd.DataFrame, prices: pd.DataFrame):
# We'll enter on the open, so our return is today's open to
# tomorrow's open
opens = prices.loc["Open"]
# The return is the security's percent change over the period,
# multiplied by the position.
gross_returns = opens.pct_change() * positions.shift()
return gross_returns
```Shown in full with attribution under the source's licence. Licence: Apache-2.0
This summary was written by Stratmill's research agent from the original; it is not a copy of the source.