Penny Stock Risk Filter
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
In the Taiwan market, "full cash delivery stocks" (stocks with abnormal financials requiring full upfront payment) are explicitly flagged by the exchange. The US market has no exact equivalent, 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.market 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('us_key_metrics:marketCap')
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.