Small Cap Screener
Build a screener that layers AskEdgar dilution data on top of traditional market filters to find trade setups other screeners miss.
Every screener can filter by market cap, float, and volume. None of them can tell you whether a company has an active shelf registration, is burning through its last 3 months of cash, or has a Nasdaq compliance deadline approaching. That's the gap this example fills.
By combining the AskEdgar screener endpoint with dilution ratings, compliance data, and float details, you can build a screener that surfaces setups based on the full capital structure picture — not just price and volume.
What you're building
A screener that supports queries like:
- Low float + high short interest + no active shelf = potential squeeze setup
- High cash burn + under 6 months runway + Nasdaq non-compliant = likely offering incoming
- Recent IPO + low tradable float + high insider ownership = lockup expiration play
- Small cap + high offering frequency + declining float = chronic diluter to avoid or short
The data sources
| Endpoint | What it gives you |
|---|---|
/v1/screener | Market cap, price, float, short interest, volume, sector, gains, and more |
/v1/dilution-rating | Offering risk, cash burn, cash runway, offering frequency, Reg SHO status |
/v1/nasdaq-compliance | Active deficiencies and compliance status |
/v1/float-outstanding | Float, outstanding, insider/institutional/affiliate ownership percentages |
Step 1: Run the base screen
The screener endpoint handles the heavy lifting for traditional filters. It supports dozens of parameters — here's a query for small-cap, low-float stocks with elevated short interest:
import requests
API_KEY = "your_api_key"
BASE_URL = "https://api.askedgar.com"
HEADERS = {"API-KEY": API_KEY}
def run_screen(filters, limit=50):
"""Run a screener query with arbitrary filters."""
params = {"limit": limit, **filters}
resp = requests.get(
f"{BASE_URL}/v1/screener",
params=params,
headers=HEADERS,
)
resp.raise_for_status()
return resp.json()["results"]Some useful filter combinations:
# Low float, high short interest small caps
low_float_short_squeeze = {
"max_market_cap": 500_000_000,
"max_float": 10_000_000,
"min_short_float": 15.0,
"isactivelytrading": True,
}
# Recent runners that gapped up
recent_runners = {
"max_market_cap": 300_000_000,
"min_gain_7_day": 30.0,
"min_averagevolume": 500_000,
}
# Nano caps with very low float
nano_cap_low_float = {
"max_market_cap": 50_000_000,
"max_float": 5_000_000,
"min_price": 0.50,
"isactivelytrading": True,
}
results = run_screen(low_float_short_squeeze)Step 2: Layer on dilution data
The screener gives you the market data. Now enrich each result with the dilution picture — this is what separates your screener from everything else out there.
def get_dilution_rating(ticker):
resp = requests.get(
f"{BASE_URL}/v1/dilution-rating",
params={"ticker": ticker},
headers=HEADERS,
)
resp.raise_for_status()
results = resp.json()["results"]
return results[0] if results else None
def get_compliance(ticker):
resp = requests.get(
f"{BASE_URL}/v1/nasdaq-compliance",
params={"ticker": ticker},
headers=HEADERS,
)
resp.raise_for_status()
return resp.json()["results"]
def enrich_with_dilution(screener_results):
enriched = []
for stock in screener_results:
ticker = stock["ticker"]
rating = get_dilution_rating(ticker)
compliance = get_compliance(ticker)
stock["dilution"] = {
"overall_risk": rating["overall_offering_risk"] if rating else None,
"offering_ability": rating["offering_ability"] if rating else None,
"offering_frequency": rating["offering_frequency"] if rating else None,
"cash_remaining_months": rating.get("cash_remaining_months") if rating else None,
"cash_burn": rating.get("cash_burn") if rating else None,
"estimated_cash": rating.get("estimated_cash") if rating else None,
"regsho": rating.get("regsho") if rating else None,
}
stock["compliance_issues"] = [
c["deficiency"] for c in compliance if c.get("deficiency")
]
enriched.append(stock)
return enrichedStep 3: Score and rank
With both market data and dilution data on each stock, you can build composite scores for different strategies. Here are two examples — one bullish, one bearish:
def score_squeeze_candidate(stock):
"""Higher score = more interesting squeeze setup."""
score = 0
d = stock.get("dilution", {})
# Bullish factors
short_float = stock.get("short_float") or 0
if short_float > 20:
score += 3
elif short_float > 10:
score += 1
if (stock.get("float") or float("inf")) < 5_000_000:
score += 2
if stock.get("vol_change") and stock["vol_change"] > 50:
score += 1
# Dilution risk reduces the score
if d.get("offering_ability") == "High":
score -= 3 # Active shelf = they can dilute at any time
if d.get("offering_ability") == "Low":
score += 2 # No shelf = safer squeeze setup
if d.get("overall_risk") == "High":
score -= 2
if d.get("overall_risk") == "Low":
score += 1
if stock.get("compliance_issues"):
score -= 1 # Compliance pressure = might need to raise cash
return score
def score_dilution_short(stock):
"""Higher score = more likely to dilute soon. Useful for short setups."""
score = 0
d = stock.get("dilution", {})
if d.get("overall_risk") == "High":
score += 3
elif d.get("overall_risk") == "Medium":
score += 1
if d.get("offering_ability") == "High":
score += 2
if d.get("offering_frequency") == "High":
score += 2
cash_months = d.get("cash_remaining_months")
if cash_months is not None:
if cash_months < 3:
score += 3
elif cash_months < 6:
score += 2
elif cash_months < 12:
score += 1
if stock.get("compliance_issues"):
score += 2
if d.get("regsho"):
score += 1
return scorePutting it together
A full pipeline that screens, enriches, scores, and ranks:
def run_dilution_screener(filters, strategy="squeeze", top_n=10):
# Step 1: Base screen
results = run_screen(filters)
print(f"Base screen returned {len(results)} stocks")
# Step 2: Enrich with dilution data
enriched = enrich_with_dilution(results)
# Step 3: Score and rank
score_fn = {
"squeeze": score_squeeze_candidate,
"dilution_short": score_dilution_short,
}[strategy]
for stock in enriched:
stock["score"] = score_fn(stock)
ranked = sorted(enriched, key=lambda s: s["score"], reverse=True)
return ranked[:top_n]Example: Finding squeeze candidates
candidates = run_dilution_screener(
filters={
"max_market_cap": 500_000_000,
"max_float": 10_000_000,
"min_short_float": 10.0,
"isactivelytrading": True,
},
strategy="squeeze",
top_n=5,
)
for s in candidates:
d = s["dilution"]
print(
f"{s['ticker']:6s} | Score: {s['score']:+d} | "
f"Float: {s.get('float', 0):>12,.0f} | "
f"SI: {s.get('short_float', 0):>5.1f}% | "
f"Offering Risk: {d.get('overall_risk', 'N/A'):>6s} | "
f"Cash: {d.get('cash_remaining_months', '?')} months"
)Example output:
ABCD | Score: +6 | Float: 2,500,000 | SI: 25.3% | Offering Risk: Low | Cash: 18.5 months
EFGH | Score: +4 | Float: 4,100,000 | SI: 18.7% | Offering Risk: Medium | Cash: 12.0 months
IJKL | Score: +3 | Float: 8,200,000 | SI: 22.1% | Offering Risk: Low | Cash: 30.2 months
MNOP | Score: +1 | Float: 3,800,000 | SI: 15.4% | Offering Risk: High | Cash: 4.0 months
QRST | Score: -1 | Float: 1,200,000 | SI: 35.0% | Offering Risk: High | Cash: 2.1 months
Notice how QRST has the highest short interest and lowest float — a traditional screener would rank it #1. But with the dilution overlay, you can see it has high offering risk and only 2 months of cash. That "squeeze candidate" is actually a dilution trap.
Pre-built filter recipes
Here are a few more filter combinations to get you started:
# Stocks likely to dilute soon — short candidates
likely_diluters = {
"max_market_cap": 300_000_000,
"max_price": 5.00,
"isactivelytrading": True,
}
# Then use strategy="dilution_short" and look for high scores
# Recent IPOs with low float — potential volatility
recent_ipos = {
"max_market_cap": 200_000_000,
"ipodate_from": "2024-06-01",
"max_float": 5_000_000,
}
# Beaten-down stocks bouncing — check if dilution risk is clearing
bounce_candidates = {
"max_market_cap": 500_000_000,
"min_gain_7_day": 20.0,
"max_gain_30_day": -20.0, # Was down 20%+ over 30 days
}Cost considerations
The base screener call is a single request regardless of how many results it returns. The enrichment step adds 2 API calls per stock (dilution rating + compliance). For a 50-result screen, that's about 100 enrichment calls. To keep costs down:
- Start with tight filters to reduce the base result set
- Cache dilution ratings — they don't change intraday
- Only enrich the top N results after an initial sort by your primary market data criteria
Use the cost estimate endpoint to project costs before running large screens.
Next steps
Updated about 3 hours ago