Skip to content

Penny Stock Risk Filter (US Market)

A revenue momentum strategy combined with volume and price filters to avoid buying illiquid, low-quality, or financially distressed stocks. In the US market, penny stocks, micro-caps, and low-volume names carry outsized risks that can silently erode strategy performance.

Why Filter Risky Stocks

US exchanges do not flag a category of financially distressed stocks (Taiwan, for example, flags "full cash delivery" stocks that require full upfront payment), but similar risks exist in different forms:

Penny Stocks (SEC Definition: Price < $5)

The U.S. Securities and Exchange Commission defines penny stocks as securities trading below $5. These stocks are characterized by:

  • Wide bid-ask spreads that inflate transaction costs
  • Low institutional ownership and analyst coverage
  • Higher susceptibility to pump-and-dump manipulation
  • Frequent delisting risk

Low-Volume Stocks

Stocks with average daily volume below 100,000 shares present execution risk -- large orders can move the price significantly, and positions may be difficult to exit quickly during market stress.

Micro-Cap Stocks

Companies with market capitalization below $300 million are more likely to have:

  • Volatile and unpredictable earnings
  • Limited financial disclosure quality
  • Higher bankruptcy risk
  • Survivorship bias in backtests (many delisted stocks were once small-caps)

Revenue Momentum Strategy with Filters

The core strategy identifies stocks with strong revenue momentum (trailing 3-month cumulative revenue at a 2-year high). The filters then remove penny stocks, low-volume names, and micro-caps from the selection.

from finlab import data
from finlab.backtest import sim
from finlab.markets.us import USMarket

data.set_market('us')

close = data.get('price:adj_close')
close = close[close.index.dayofweek < 5]  # remove weekends

# Revenue momentum: quarterly revenue at 2-year high
revenue = data.get('us_income_statement:revenue')
rev_trailing_3q = revenue.rolling(3).sum()
cond_rev = rev_trailing_3q == rev_trailing_3q.rolling(8, min_periods=4).max()

# Revenue growth rate for ranking
rev_growth = revenue / revenue.shift(4) - 1

# --- Risk Filters ---

# Filter 1: Remove penny stocks (price < $5)
cond_price = close > 5

# Filter 2: Volume filter (average daily volume > 100K shares)
volume = data.get('price:volume')
volume = volume[volume.index.dayofweek < 5]
avg_volume = volume.rolling(20).mean()
cond_volume = avg_volume > 100000

# Filter 3: Market cap filter (> $300M)
market_cap = data.get('etl:us_market_value')
cond_mcap = market_cap > 300000000

# Combine all conditions
cond_all = cond_rev & cond_price & cond_volume & cond_mcap

# Rank by revenue growth, select top 10
result = rev_growth * cond_all
position = result[result > 0].is_largest(10)

report = sim(position, resample='M', market=USMarket(), fee_ratio=0.001, tax_ratio=0)
report.display()

Filter Breakdown

Price Filter: close > 5

Removes all stocks trading below $5. This single filter eliminates the vast majority of pump-and-dump candidates, shell companies, and near-delisting stocks. It also improves backtest realism, since historical penny stock data often suffers from survivorship bias.

Volume Filter: avg_volume > 100000

Uses a 20-day rolling average of daily share volume. The 100,000-share threshold ensures that a strategy allocating reasonable capital (e.g., \(100K-\)1M per position) can enter and exit without significant market impact.

Market Cap Filter: market_cap > 300000000

A $300 million market cap floor removes micro-caps that are disproportionately represented in backtested alpha signals. Many academic anomalies (e.g., value, momentum) derive much of their historical alpha from micro-caps that are practically untradeable at scale.

Key Parameters

  • revenue.rolling(3).sum(): Trailing 3-quarter cumulative revenue smooths out single-quarter noise while staying responsive to recent trends.
  • rolling(8, min_periods=4).max(): A 2-year (8-quarter) rolling maximum identifies revenue breakout signals.
  • rev_growth = revenue / revenue.shift(4) - 1: Year-over-year quarterly growth removes seasonality and provides a clean ranking metric.
  • is_largest(10): Selects the top 10 stocks by revenue growth rate among those passing all filters.
  • resample='M': Monthly rebalancing balances the quarterly cadence of revenue updates with the need to respond to price and volume changes.
  • fee_ratio=0.001: Commission rate of 0.1%.
  • tax_ratio=0: No transaction tax on US stock trades.

Practical Notes

  • These filters are additive -- you can apply them to any existing strategy by adding & cond_price & cond_volume & cond_mcap to your final position condition.
  • The specific thresholds ($5, 100K shares, $300M) are reasonable defaults but can be adjusted based on your capital size and risk tolerance.
  • For larger portfolios, consider raising the market cap floor to $1B and the volume floor to 500K shares.