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_mcapto 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.