Skip to content
All library documents

Shorting Liquid Stocks After Large One-Day Drops

Article Strategy library · Author: QuantRocket

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.