Shorting Liquid Stocks After Large One-Day Drops
Summary
This strategy shorts eligible stocks after a one-day decline of at least 10%. Eligibility requires average dollar volume over a rolling 22-session window to meet a market-specific threshold; it also filters for equities and can restrict securities by currency. The strategy assigns fixed position weights capped at 10% per position and shifts target weights forward one day before entry. Returns are calculated from the next session’s open to close.
The document provides implementations for several markets, with different liquidity thresholds and currencies. It also describes an optional constraint based on shares reported as shortable, shifted to align with the planned entry. No performance results are included. The rules do not specify a distinct exit signal beyond the daily position handling, and the example’s modeled returns use open-to-close price changes. Short availability, transaction costs, slippage, and the possibility of continued declines or rebounds may affect live results.
Key ideas
- The signal shorts equities after a daily close-to-close loss of at least 10%.\nEligibility depends on a rolling average dollar-volume threshold and can be limited by currency.\nTarget weights are fixed and capped at 10% per position, with entry delayed by one session.\nAn optional shortability constraint uses reported shortable shares aligned to the planned entry.\nThe example calculates gross returns from the open to the close and supplies no performance evidence.
Tags
Full text
# DeadCatDrop
# DeadCatDrop
## Source (Apache-2.0)
```python
# Copyright 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.commission import PercentageCommission
from quantrocket.fundamental import get_ibkr_shortable_shares_reindexed_like, get_ibkr_borrow_fees_reindexed_like
from quantrocket.master import get_securities_reindexed_like
class DeadCatDrop(Moonshot):
CODE = "dead-cat-drop"
DB = None
DB_FIELDS = ["Open","Close","Volume"]
MIN_DOLLAR_VOLUME = 1000000
MAX_WEIGHT_PER_POSITION = 0.1
LIMIT_TO_CURRENCY = None
COMMISSION_CLASS = None
MIN_BORROW_FEE = None
POSITIONS_CLOSED_DAILY = True # see https://www.quantrocket.com/docs/#moonshot-commissions-and-slippage-for-intraday-positions
CONSTRAIN_SHORTABLE = False # whether to limit position sizes by shortable shares
def prices_to_signals(self, prices: pd.DataFrame):
closes = prices.loc["Close"]
# Compute dollar volume mask
dollar_volumes = prices.loc["Volume"] * closes
avg_dollar_volumes = dollar_volumes.rolling(window=22).mean()
are_eligible = avg_dollar_volumes >= self.MIN_DOLLAR_VOLUME
sectypes = get_securities_reindexed_like(
closes, "edi_SecTypeCode").loc["edi_SecTypeCode"]
are_eligible &= sectypes == "EQS"
if self.LIMIT_TO_CURRENCY:
currencies = get_securities_reindexed_like(
closes, "Currency").loc["Currency"]
are_eligible &= currencies == self.LIMIT_TO_CURRENCY
if self.MIN_BORROW_FEE:
borrow_fees = get_ibkr_borrow_fees_reindexed_like(closes)
are_eligible &= borrow_fees >= self.MIN_BORROW_FEE
# Compute big losers mask
prior_returns = (closes - closes.shift()) / closes.shift()
big_losers = prior_returns <= -0.10
short_signals = big_losers & are_eligible
return -short_signals.astype(int)
def signals_to_target_weights(self, signals: pd.DataFrame, prices: pd.DataFrame):
weights = self.allocate_fixed_weights_capped(signals, weight=self.MAX_WEIGHT_PER_POSITION)
return weights
def limit_position_sizes(self, prices: pd.DataFrame):
max_shares_for_shorts = None
if self.CONSTRAIN_SHORTABLE:
t = f"09:00:00 {self.TIMEZONE}"
shortable_shares = get_ibkr_shortable_shares_reindexed_like(prices.loc["Close"], time=t)
# constrain today's target weights by tomorrow's shortable shares, when the position will be entered
max_shares_for_shorts = shortable_shares.shift(-1)
return None, max_shares_for_shorts
def target_weights_to_positions(self, weights: pd.DataFrame, prices: pd.DataFrame):
# enter next day
positions = weights.shift()
return positions
def positions_to_gross_returns(self, positions: pd.DataFrame, prices: pd.DataFrame):
closes = prices.loc["Close"]
opens = prices.loc["Open"]
pct_changes = (closes - opens) / opens.where(opens > 0)
gross_returns = pct_changes * positions
return gross_returns
# Canada
class DeadCatDropCanada(DeadCatDrop):
CODE = "dead-cat-drop-canada"
DB = "edi-canada-1d"
TIMEZONE = "America/Toronto"
MIN_DOLLAR_VOLUME = 1000000.0
LIMIT_TO_CURRENCY = "CAD"
# Eurozone
class DeadCatDropEurozone(DeadCatDrop):
CODE = "dead-cat-drop-eurozone"
DB = ['edi-belgium-1d', 'edi-france-1d',
'edi-germany-1d', 'edi-netherlands-1d']
TIMEZONE = "Europe/Paris"
MIN_DOLLAR_VOLUME = 1000000.0
LIMIT_TO_CURRENCY = "EUR"
class DeadCatDropHongkong(DeadCatDrop):
CODE = "dead-cat-drop-hongkong"
DB = "edi-hongkong-1d"
TIMEZONE = "Asia/Hong_Kong"
MIN_DOLLAR_VOLUME = 8000000.0
LIMIT_TO_CURRENCY = "HKD"
# Japan
class JapanStockTieredCommission(PercentageCommission):
BROKER_COMMISSION_RATE = 0.0005 # 0.05% of trade value
EXCHANGE_FEE_RATE = 0.00002 + 0.000004 # 0.002% Tokyo Stock Exchange fee + 0.0004% clearing fee
MIN_COMMISSION = 80.00 # JPY
class DeadCatDropJapan(DeadCatDrop):
CODE = "dead-cat-drop-japan"
DB = "edi-japan-1d"
TIMEZONE = "Japan"
MIN_DOLLAR_VOLUME = 100000000.0
LIMIT_TO_CURRENCY = "JPY"
# Sweden
class DeadCatDropSweden(DeadCatDrop):
CODE = "dead-cat-drop-sweden"
DB = "edi-sweden-1d"
TIMEZONE = "Europe/Stockholm"
MIN_DOLLAR_VOLUME = 8000000.0
LIMIT_TO_CURRENCY = "SEK"
# Switzerland
class DeadCatDropSwitzerland(DeadCatDrop):
CODE = "dead-cat-drop-switzerland"
DB = "edi-switzerland-1d"
TIMEZONE = "Europe/Zurich"
MIN_DOLLAR_VOLUME = 1000000.0
LIMIT_TO_CURRENCY = "CHF"
# UK
class DeadCatDropUK(DeadCatDrop):
CODE = "dead-cat-drop-uk"
DB = "edi-uk-1d"
TIMEZONE = "Europe/London"
MIN_DOLLAR_VOLUME = 100000000.0
LIMIT_TO_CURRENCY = "GBX"
```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.