
Finviz Screener
- 938 installs
- 2.5k repo stars
- Updated July 26, 2026
- tradermonty/claude-trading-skills
finviz-screener is a Claude Code trading skill that instantly translates natural language stock screening requests into precise FinViz filter codes and ready-to-use screener URLs for developers who automate equity resear
About
finviz-screener is a reference skill from tradermonty/claude-trading-skills that maps natural-language stock screening intent to FinViz filter codes and fully formed screener URLs. It documents public and Elite URL patterns with view, filter, order, and signal parameters, plus English and Japanese keyword mappings for each filter code. Developers and quant-adjacent engineers use it when they want Claude to emit valid `finviz.com/screener.ashx` or `elite.finviz.com` links instead of guessing filter syntax. Reach for it during daily screen refreshes, strategy prototyping, or chat-driven equity discovery.
- Maps 200+ FinViz filter codes to plain English and Japanese keywords
- Translates user intent like "profitable small-cap tech stocks with low P/E" into valid URL parameters
- Explains range syntax such as fa_pe_10to20 and ta_beta_0.5to1.5
- Supports both free public and Elite paid FinViz screener URLs
- Includes view types, sort orders, signal filters and ticker lists
Finviz Screener by the numbers
- 938 all-time installs (skills.sh)
- +51 installs in the week ending Jul 28, 2026 (Skillselion tracking)
- Ranked #152 of 1,136 Finance & Trading skills by installs in the Skillselion catalog
- Security screen: LOW risk (skills.sh audit)
- Data as of Jul 28, 2026 (Skillselion catalog sync)
npx skills add https://github.com/tradermonty/claude-trading-skills --skill finviz-screenerAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 938 |
|---|---|
| repo stars | ★ 2.5k |
| Security audit | 3 / 3 scanners passed |
| Last updated | July 26, 2026 |
| Repository | tradermonty/claude-trading-skills ↗ |
How do you build FinViz screener URLs from plain English?
Let Claude instantly translate natural language stock screening requests into precise FinViz filter codes and ready-to-use screener URLs.
Who is it for?
Developers and traders who screen US equities in FinViz and want Claude to output correct filter codes and URLs from conversational queries.
Skip if: Skip finviz-screener when you need live API screening or Volatility Contraction Pattern scans instead of FinViz URL generation.
When should I use this skill?
User describes stock filters in natural language and needs FinViz screener links or filter codes.
What you get
Valid FinViz filter code strings and public or Elite screener URLs with view, order, and signal parameters
- FinViz filter code string
- Public or Elite screener URL
By the numbers
- Documents public and Elite FinViz screener URL parameter formats
- Filter reference includes English and Japanese natural-language keyword mappings
Files
FinViz Screener
Overview
Translate natural-language stock screening requests into FinViz screener filter codes, build the URL, and open it in Chrome. No API key required for public screener; FINVIZ Elite is auto-detected from $FINVIZ_API_KEY for enhanced functionality.
Key Features:
- Natural language → filter code mapping (Japanese + English)
- URL construction with view type and sort order selection
- Elite/Public auto-detection (environment variable or explicit flag)
- Chrome-first browser opening with OS-appropriate fallbacks
- Strict filter validation to prevent URL injection
---
When to Use This Skill
Explicit Triggers:
- "高配当で成長している小型株を探したい"
- "Find oversold large caps near 52-week lows"
- "テクノロジーセクターの割安株をスクリーニングしたい"
- "Screen for stocks with insider buying"
- "FinVizでブレイクアウト候補を表示して"
- "Show me high-growth small caps on FinViz"
- "配当利回り5%以上でROE15%以上の銘柄を探して"
Implicit Triggers:
- User describes stock screening criteria using fundamental or technical terms
- User mentions FinViz screener or stock filtering
- User asks to find stocks matching specific financial characteristics
When NOT to Use:
- Deep fundamental analysis of a specific stock (use us-stock-analysis)
- Portfolio review with holdings (use portfolio-manager)
- Chart pattern analysis on images (use technical-analyst)
- Earnings-based screening (use earnings-trade-analyzer or pead-screener)
---
Workflow
Step 1: Load Filter Reference
Read the filter knowledge base:
cat references/finviz_screener_filters.mdStep 2: Interpret User Request
Map the user's natural-language request to FinViz filter codes. Use the Common Concept Mapping table below for quick translation, and reference the full filter list for precise code selection.
Note: For range criteria (e.g., "dividend 3-8%", "P/E between 10 and 20"), use the {from}to{to} range syntax as a single filter token (e.g., fa_div_3to8, fa_pe_10to20) instead of combining separate _o and _u filters.
Common Concept Mapping:
| User Concept (EN) | User Concept (JP) | Filter Codes |
|---|---|---|
| High dividend | 高配当 | fa_div_o3 or fa_div_o5 |
| Small cap | 小型株 | cap_small |
| Mid cap | 中型株 | cap_mid |
| Large cap | 大型株 | cap_large |
| Mega cap | 超大型株 | cap_mega |
| Value / cheap | 割安 | fa_pe_u20,fa_pb_u2 |
| Growth stock | 成長株 | fa_epsqoq_o25,fa_salesqoq_o15 |
| Oversold | 売られすぎ | ta_rsi_os30 |
| Overbought | 買われすぎ | ta_rsi_ob70 |
| Near 52W high | 52週高値付近 | ta_highlow52w_b0to5h |
| Near 52W low | 52週安値付近 | ta_highlow52w_a0to5l |
| Breakout | ブレイクアウト | ta_highlow52w_b0to5h,sh_relvol_o1.5 |
| Technology | テクノロジー | sec_technology |
| Healthcare | ヘルスケア | sec_healthcare |
| Energy | エネルギー | sec_energy |
| Financial | 金融 | sec_financial |
| Semiconductors | 半導体 | ind_semiconductors |
| Biotechnology | バイオテク | ind_biotechnology |
| US stocks | 米国株 | geo_usa |
| Profitable | 黒字 | fa_pe_profitable |
| High ROE | 高ROE | fa_roe_o15 or fa_roe_o20 |
| Low debt | 低負債 | fa_debteq_u0.5 |
| Insider buying | インサイダー買い | sh_insidertrans_verypos |
| Short squeeze | ショートスクイーズ | sh_short_o20,sh_relvol_o2 |
| Dividend growth | 増配 | fa_divgrowth_3yo10 |
| Deep value | ディープバリュー | fa_pb_u1,fa_pe_u10 |
| Momentum | モメンタム | ta_perf_13wup,ta_sma50_pa,ta_sma200_pa |
| Defensive | ディフェンシブ | ta_beta_u0.5 or sec_utilities,sec_consumerdefensive |
| Liquid / high volume | 高出来高 | sh_avgvol_o500 or sh_avgvol_o1000 |
| Pullback from high | 高値からの押し目 | ta_highlow52w_10to30-bhx |
| Near 52W low reversal | 安値圏リバーサル | ta_highlow52w_10to30-alx |
| Fallen angel | 急落後反発 | ta_highlow52w_b20to30h,ta_rsi_os40 |
| AI theme | AIテーマ | --themes "artificialintelligence" |
| Cybersecurity theme | サイバーセキュリティ | --themes "cybersecurity" |
| AI + Cybersecurity | AI&サイバーセキュリティ | --themes "artificialintelligence,cybersecurity" |
| AI Cloud sub-theme | AIクラウド | --subthemes "aicloud" |
| AI Compute sub-theme | AI半導体 | --subthemes "aicompute" |
| Yield 3-8% (trap excluded) | 配当3-8%(トラップ除外) | fa_div_3to8 |
| Mid-range P/E | 適正PER帯 | fa_pe_10to20 |
| EV undervalued | EV割安 | fa_evebitda_u10 |
| Earnings next week | 来週決算 | earningsdate_nextweek |
| IPO recent | 直近IPO | ipodate_thismonth |
| Target price above | 目標株価以上 | targetprice_a20 |
| Recent news | 最新ニュースあり | news_date_today |
| High institutional | 機関保有率高 | sh_instown_o60 |
| Low float | 浮動株少 | sh_float_u20 |
| Near all-time high | 史上最高値付近 | ta_alltime_b0to5h |
| High ATR | 高ボラティリティ | ta_averagetruerange_o1.5 |
Step 3: Present Filter Selection
Before executing, present the selected filters in a table for user confirmation:
| Type | Value | Meaning |
|---|---|---|
| Theme | artificialintelligence | Artificial Intelligence |
| Sub-theme | aicloud | AI - Cloud & Infrastructure |
| Filter | cap_small | Small Cap ($300M–$2B) |
| Filter | fa_div_o3 | Dividend Yield > 3% |
| Filter | fa_pe_u20 | P/E < 20 |
| Filter | geo_usa | USA |
View: Overview (v=111)
Mode: Public / Elite (auto-detected)Ask the user to confirm or adjust before proceeding.
Step 4: Execute Script
Run the screener script to build the URL and open Chrome:
python3 scripts/open_finviz_screener.py \
--filters "cap_small,fa_div_o3,fa_pe_u20,geo_usa" \
--view overview
# Theme-only screening (no --filters required)
python3 scripts/open_finviz_screener.py \
--themes "artificialintelligence,cybersecurity" \
--url-only
# Theme + sub-theme + filters combined
python3 scripts/open_finviz_screener.py \
--themes "artificialintelligence" \
--subthemes "aicloud,aicompute" \
--filters "cap_midover" \
--url-onlyScript arguments:
--filters(optional): Comma-separated filter codes. Note:theme_*andsubtheme_*tokens are not allowed here — use--themes/--subthemesinstead.--themes(optional): Comma-separated theme slugs (e.g.,artificialintelligence,cybersecurity). Accepts bare slugs ortheme_-prefixed values.--subthemes(optional): Comma-separated sub-theme slugs (e.g.,aicloud,aicompute). Accepts bare slugs orsubtheme_-prefixed values.--elite: Force Elite mode (auto-detected from$FINVIZ_API_KEYif not set)--view: View type — overview, valuation, financial, technical, ownership, performance, custom--order: Sort order (e.g.,-marketcap,dividendyield,-change)--url-only: Print URL without opening browser
At least one of --filters, --themes, or --subthemes must be provided.
Step 5: Report Results
After opening the screener, report: 1. The constructed URL 2. Elite or Public mode used 3. Summary of applied filters 4. Suggested next steps (e.g., "Sort by dividend yield", "Switch to Financial view for detailed ratios")
---
Usage Recipes
Real-world screening patterns distilled from repeated use. Each recipe includes a starter filter set, recommended view, and tips for iterative refinement.
Recipe 1: High-Dividend Growth Stocks (Kanchi-Style)
Goal: High yield + dividend growth + earnings growth, excluding yield traps.
--filters "fa_div_3to8,fa_sales5years_pos,fa_eps5years_pos,fa_divgrowth_5ypos,fa_payoutratio_u60,geo_usa"
--view financial| Filter Code | Purpose |
|---|---|
fa_div_3to8 | Yield 3-8% (caps high-yield traps) |
fa_sales5years_pos | Positive 5Y revenue growth |
fa_eps5years_pos | Positive 5Y EPS growth |
fa_divgrowth_5ypos | Positive 5Y dividend growth |
fa_payoutratio_u60 | Payout ratio < 60% (sustainability) |
geo_usa | US-listed stocks |
Iterative refinement: Start broad with fa_div_o3 → review results → add fa_div_3to8 to cap yield → add fa_payoutratio_u60 to exclude traps → switch to financial view for payout and growth columns.
Recipe 2: Minervini Trend Template + VCP
Goal: Stocks in a Stage 2 uptrend with volatility contraction (VCP setup).
--filters "ta_sma50_pa,ta_sma200_pa,ta_sma200_sb50,ta_highlow52w_0to25-bhx,ta_perf_26wup,sh_avgvol_o300,cap_midover"
--view technical| Filter Code | Purpose |
|---|---|
ta_sma50_pa | Price above 50-day SMA |
ta_sma200_pa | Price above 200-day SMA |
ta_sma200_sb50 | 200 SMA below 50 SMA (uptrend) |
ta_highlow52w_0to25-bhx | Within 25% of 52W high |
ta_perf_26wup | Positive 26-week performance |
sh_avgvol_o300 | Avg volume > 300K |
cap_midover | Mid cap and above |
VCP tightening filters (add to narrow): ta_volatility_wo3,ta_highlow20d_b0to5h,sh_relvol_u1 — low weekly volatility, near 20-day high, below-average relative volume (contraction signal).
Recipe 3: Unfairly Sold-Off Growth Stocks
Goal: Fundamentally strong companies with recent sharp declines — potential mean reversion candidates.
--filters "fa_sales5years_o5,fa_eps5years_o10,fa_roe_o15,fa_salesqoq_pos,fa_epsqoq_pos,ta_perf_13wdown,ta_highlow52w_10to30-bhx,cap_large,sh_avgvol_o200"
--view overview| Filter Code | Purpose |
|---|---|
fa_sales5years_o5 | 5Y sales growth > 5% |
fa_eps5years_o10 | 5Y EPS growth > 10% |
fa_roe_o15 | ROE > 15% |
fa_salesqoq_pos | Positive QoQ sales growth |
fa_epsqoq_pos | Positive QoQ EPS growth |
ta_perf_13wdown | Negative 13-week performance |
ta_highlow52w_10to30-bhx | 10-30% below 52W high |
cap_large | Large cap |
sh_avgvol_o200 | Avg volume > 200K |
After review: Switch to valuation view to check P/E and P/S for entry attractiveness.
Recipe 4: Turnaround Stocks
Goal: Companies with previously declining earnings now showing recovery — bottom-fishing with fundamental confirmation.
--filters "fa_eps5years_neg,fa_epsqoq_pos,fa_salesqoq_pos,ta_highlow52w_b30h,ta_perf_13wup,cap_smallover,sh_avgvol_o200"
--view performance| Filter Code | Purpose |
|---|---|
fa_eps5years_neg | Negative 5Y EPS growth (prior decline) |
fa_epsqoq_pos | Positive QoQ EPS growth (recovery) |
fa_salesqoq_pos | Positive QoQ sales growth (recovery) |
ta_highlow52w_b30h | Within 30% of 52W high (not at bottom) |
ta_perf_13wup | Positive 13-week performance |
cap_smallover | Small cap and above |
sh_avgvol_o200 | Avg volume > 200K |
Recipe 5: Momentum Trade Candidates
Goal: Short-term momentum leaders near 52W highs with increasing volume.
--filters "ta_sma50_pa,ta_sma200_pa,ta_highlow52w_b0to3h,ta_perf_4wup,sh_relvol_o1.5,sh_avgvol_o1000,cap_midover"
--view technical| Filter Code | Purpose |
|---|---|
ta_sma50_pa | Price above 50-day SMA |
ta_sma200_pa | Price above 200-day SMA |
ta_highlow52w_b0to3h | Within 3% of 52W high |
ta_perf_4wup | Positive 4-week performance |
sh_relvol_o1.5 | Relative volume > 1.5x |
sh_avgvol_o1000 | Avg volume > 1M |
cap_midover | Mid cap and above |
Recipe 6: Theme Screening (AI + Sub-theme Drill-Down)
Goal: Find mid-cap+ AI stocks focused on cloud infrastructure and compute acceleration.
--themes "artificialintelligence"
--subthemes "aicloud,aicompute"
--filters "cap_midover"
--view overview| Type | Value | Purpose |
|---|---|---|
| Theme | artificialintelligence | AI theme universe |
| Sub-theme | aicloud | Cloud & Infrastructure vertical |
| Sub-theme | aicompute | Compute & Acceleration vertical |
| Filter | cap_midover | Mid cap and above |
Multi-theme example: --themes "artificialintelligence,cybersecurity" selects stocks tagged with either theme (OR logic via | grouping).
Tips: Iterative Refinement Pattern
Screening works best as a dialogue, not a one-shot query:
1. Start broad — use 3-4 core filters to get an initial result set 2. Review count — if too many results (>100), add tightening filters; if too few (<5), relax constraints 3. Switch views — start with overview for a quick scan, then switch to financial or valuation for deeper inspection 4. Layer in technicals — after confirming fundamental quality, add ta_ filters to time entries 5. Save and iterate — bookmark the URL, then adjust one filter at a time to understand its impact
---
Resources
references/finviz_screener_filters.md— Complete filter code reference with natural language keywords (includes industry code examples; full 142-code list is in the Industry Codes section)scripts/open_finviz_screener.py— URL builder and Chrome opener
#!/usr/bin/env python3
"""Build a FinViz screener URL from filter codes and open it in Chrome.
Usage:
python3 open_finviz_screener.py --filters "cap_small,fa_div_o3,fa_pe_u20" --url-only
python3 open_finviz_screener.py --themes "artificialintelligence,cybersecurity" --url-only
python3 open_finviz_screener.py --themes "artificialintelligence" --subthemes "aicloud" --filters "cap_midover" --url-only
python3 open_finviz_screener.py --filters "cap_small,fa_div_o3" --view valuation
python3 open_finviz_screener.py --filters "cap_small,fa_div_o3" --elite
"""
from __future__ import annotations
import argparse
import os
import re
import shutil
import subprocess
import sys
import webbrowser
from urllib.parse import quote
# View type name -> code mapping
VIEW_CODES: dict[str, str] = {
"overview": "111",
"valuation": "121",
"ownership": "131",
"performance": "141",
"custom": "152",
"financial": "161",
"technical": "171",
}
# Known filter prefixes (warning only if unknown)
KNOWN_PREFIXES: set[str] = {
"an_",
"cap_",
"earningsdate_",
"etf_",
"exch_",
"fa_",
"geo_",
"idx_",
"ind_",
"ipodate_",
"news_",
"sec_",
"sh_",
"subtheme_",
"ta_",
"targetprice_",
"theme_",
}
# Strict token pattern: lowercase letters, digits, underscore, dot, hyphen only
_TOKEN_RE = re.compile(r"^[a-z0-9_.\-]+$")
# Slug pattern for theme/sub-theme values: lowercase letters and digits only
_SLUG_RE = re.compile(r"^[a-z0-9]+$")
# Order parameter pattern: optional leading dash, then lowercase letters/digits/underscore
_ORDER_RE = re.compile(r"^-?[a-z0-9_]+$")
def normalize_grouped_slug(value: str, prefix: str) -> str:
"""Strip optional prefix and whitespace from a theme/sub-theme slug."""
value = value.strip()
full_prefix = f"{prefix}_"
if value.startswith(full_prefix):
value = value[len(full_prefix) :]
return value
def validate_grouped_slugs(raw: str, prefix: str) -> list[str]:
"""Validate comma-separated theme or sub-theme slugs.
Args:
raw: Comma-separated slug values (with or without prefix).
prefix: ``"theme"`` or ``"subtheme"``.
Returns:
Deduplicated list of bare slugs in input order.
Raises:
SystemExit: If any slug is empty or contains invalid characters.
"""
parts = [normalize_grouped_slug(p, prefix) for p in raw.split(",") if p.strip()]
if not parts:
print(f"Error: --{prefix}s must contain at least one slug.", file=sys.stderr)
sys.exit(1)
validated: list[str] = []
for slug in parts:
if not _SLUG_RE.match(slug):
print(
f"Error: Invalid {prefix} slug '{slug}'. "
"Only lowercase letters and digits are allowed (no underscores, dots, or hyphens).",
file=sys.stderr,
)
sys.exit(1)
validated.append(slug)
# Deduplicate preserving order
return list(dict.fromkeys(validated))
def build_filter_parts(
filters: list[str],
themes: list[str] | None = None,
subthemes: list[str] | None = None,
) -> list[str]:
"""Assemble filter parts in canonical order: theme → subtheme → plain filters."""
parts: list[str] = []
if themes:
parts.append("theme_" + "|".join(themes))
if subthemes:
parts.append("subtheme_" + "|".join(subthemes))
parts.extend(filters)
return parts
def validate_filters(raw_filters: str) -> list[str]:
"""Validate and return a list of filter tokens.
Each token must match ``^[a-z0-9_.\\-]+$`` (lowercase, digits, underscore,
dot, hyphen). Tokens containing ``&``, ``=``, spaces, or other characters
are rejected to prevent URL injection.
Unknown prefixes generate a warning to stderr but are not rejected.
Returns:
Sorted list of valid filter tokens.
Raises:
SystemExit: If any token fails the character-class check.
"""
tokens: list[str] = [t.strip() for t in raw_filters.split(",") if t.strip()]
if not tokens:
print("Error: --filters must contain at least one filter code.", file=sys.stderr)
sys.exit(1)
validated: list[str] = []
for token in tokens:
# Check for theme/subtheme BEFORE regex validation
if token.startswith("theme_") or token.startswith("subtheme_"):
suggested = "--themes" if token.startswith("theme_") else "--subthemes"
print(
f"Error: '{token}' detected in --filters. "
f"Use {suggested} instead for theme/sub-theme filtering.",
file=sys.stderr,
)
sys.exit(1)
if not _TOKEN_RE.match(token):
print(
f"Error: Invalid filter token '{token}'. "
"Only lowercase letters, digits, underscores, and dots are allowed.",
file=sys.stderr,
)
sys.exit(1)
# Warn on unknown prefix (not an error)
prefix = token.split("_", 1)[0] + "_" if "_" in token else ""
if prefix and prefix not in KNOWN_PREFIXES:
print(f"Warning: Unknown filter prefix '{prefix}' in '{token}'.", file=sys.stderr)
validated.append(token)
return validated
def validate_order(order: str) -> str:
"""Validate the sort order parameter.
Must match ``^-?[a-z0-9_]+$`` (optional leading dash, then lowercase
letters, digits, underscores). Rejects characters that could inject
additional URL parameters.
Returns:
The validated order string.
Raises:
SystemExit: If the order value fails validation.
"""
if not _ORDER_RE.match(order):
print(
f"Error: Invalid order value '{order}'. "
"Only lowercase letters, digits, underscores, and an optional leading dash are allowed.",
file=sys.stderr,
)
sys.exit(1)
return order
def detect_elite(args: argparse.Namespace) -> bool:
"""Determine whether to use elite.finviz.com.
Priority:
1. ``--elite`` flag → True
2. ``$FINVIZ_API_KEY`` environment variable present → True
3. Otherwise → False (public)
"""
if args.elite:
return True
if os.environ.get("FINVIZ_API_KEY"):
return True
return False
def build_url(
filters: list[str],
*,
elite: bool = False,
view: str = "overview",
order: str | None = None,
themes: list[str] | None = None,
subthemes: list[str] | None = None,
) -> str:
"""Build the full FinViz screener URL.
Args:
filters: List of validated filter tokens.
elite: Use elite.finviz.com if True.
view: View type name (overview, valuation, etc.).
order: Optional sort order code (e.g., ``-marketcap``).
themes: Optional list of theme slugs.
subthemes: Optional list of sub-theme slugs.
Returns:
Complete URL string.
"""
host = "elite.finviz.com" if elite else "finviz.com"
view_code = VIEW_CODES.get(view, VIEW_CODES["overview"])
parts = build_filter_parts(filters, themes, subthemes)
filter_str = ",".join(parts)
encoded_filter_str = quote(filter_str, safe=",_.-")
url = f"https://{host}/screener.ashx?v={view_code}&f={encoded_filter_str}"
if order:
url += f"&o={order}"
return url
def open_browser(url: str) -> None:
"""Open *url* in Google Chrome, with fallbacks.
macOS: ``open -a "Google Chrome"`` → ``open``
Linux: ``google-chrome`` → ``chromium-browser`` → ``xdg-open``
Final fallback: ``webbrowser.open()``.
"""
if sys.platform == "darwin":
# Try Chrome first
try:
subprocess.run( # nosec B607 — macOS open is a known local tool
["open", "-a", "Google Chrome", url],
check=True,
capture_output=True,
)
return
except (subprocess.CalledProcessError, FileNotFoundError):
pass
# Fallback to default browser on macOS
try:
subprocess.run(["open", url], check=True, capture_output=True) # nosec B607
return
except (subprocess.CalledProcessError, FileNotFoundError):
pass
elif sys.platform.startswith("linux"):
for cmd in ("google-chrome", "chromium-browser", "xdg-open"):
if shutil.which(cmd):
try:
subprocess.run([cmd, url], check=True, capture_output=True)
return
except (subprocess.CalledProcessError, FileNotFoundError):
continue
# Final fallback
webbrowser.open(url)
def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Build a FinViz screener URL and open it in Chrome.",
)
parser.add_argument(
"--filters",
required=False,
default=None,
help="(optional) Comma-separated FinViz filter codes (e.g., cap_small,fa_div_o3,fa_pe_u20).",
)
parser.add_argument(
"--themes",
default=None,
help="Comma-separated theme slugs (e.g., artificialintelligence,cybersecurity).",
)
parser.add_argument(
"--subthemes",
default=None,
help="Comma-separated sub-theme slugs (e.g., aicloud,aicompute).",
)
parser.add_argument(
"--elite",
action="store_true",
default=False,
help="Use elite.finviz.com (auto-detected from $FINVIZ_API_KEY if not set).",
)
parser.add_argument(
"--view",
default="overview",
choices=sorted(VIEW_CODES.keys()),
help="Screener view type (default: overview).",
)
parser.add_argument(
"--order",
default=None,
help="Sort order code (e.g., -marketcap, dividendyield). Prefix - for descending.",
)
parser.add_argument(
"--url-only",
action="store_true",
default=False,
help="Print the URL without opening a browser.",
)
return parser.parse_args(argv)
def main(argv: list[str] | None = None) -> None:
args = parse_args(argv)
# Validate inputs
filters: list[str] = []
themes: list[str] | None = None
subthemes: list[str] | None = None
if args.filters is not None:
filters = validate_filters(args.filters)
if args.themes is not None:
themes = validate_grouped_slugs(args.themes, "theme")
if args.subthemes is not None:
subthemes = validate_grouped_slugs(args.subthemes, "subtheme")
if not filters and not themes and not subthemes:
print(
"Error: At least one of --filters, --themes, or --subthemes is required.",
file=sys.stderr,
)
sys.exit(1)
order = validate_order(args.order) if args.order else None
elite = detect_elite(args)
url = build_url(
filters, elite=elite, view=args.view, order=order, themes=themes, subthemes=subthemes
)
mode = "Elite" if elite else "Public"
print(f"[{mode}] {url}")
if not args.url_only:
open_browser(url)
if __name__ == "__main__":
main()
"""Shared fixtures for FinViz Screener tests"""
import os
import sys
# Add scripts directory to path so modules can be imported
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
# Add tests directory to path so helpers can be imported
sys.path.insert(0, os.path.dirname(__file__))
"""Tests for open_finviz_screener.py"""
from __future__ import annotations
import argparse
import subprocess
from unittest import mock
import pytest
from open_finviz_screener import (
KNOWN_PREFIXES,
VIEW_CODES,
build_filter_parts,
build_url,
detect_elite,
open_browser,
validate_filters,
validate_grouped_slugs,
validate_order,
)
# ---------------------------------------------------------------------------
# TestValidateGroupedSlugs
# ---------------------------------------------------------------------------
class TestValidateGroupedSlugs:
"""Grouped slug validation for themes and sub-themes."""
def test_single_theme_slug(self):
result = validate_grouped_slugs("artificialintelligence", "theme")
assert result == ["artificialintelligence"]
def test_multi_theme_slugs(self):
result = validate_grouped_slugs("artificialintelligence,cybersecurity", "theme")
assert result == ["artificialintelligence", "cybersecurity"]
def test_prefix_stripping_theme(self):
result = validate_grouped_slugs("theme_artificialintelligence", "theme")
assert result == ["artificialintelligence"]
def test_prefix_stripping_subtheme(self):
result = validate_grouped_slugs("subtheme_aicloud", "subtheme")
assert result == ["aicloud"]
def test_mixed_prefix_no_prefix(self):
result = validate_grouped_slugs("theme_artificialintelligence,cybersecurity", "theme")
assert result == ["artificialintelligence", "cybersecurity"]
def test_duplicate_removal(self):
result = validate_grouped_slugs(
"artificialintelligence,cybersecurity,artificialintelligence", "theme"
)
assert result == ["artificialintelligence", "cybersecurity"]
def test_reject_empty(self):
with pytest.raises(SystemExit):
validate_grouped_slugs("", "theme")
def test_reject_invalid_chars(self):
with pytest.raises(SystemExit):
validate_grouped_slugs("ai_cloud", "subtheme")
def test_reject_uppercase(self):
with pytest.raises(SystemExit):
validate_grouped_slugs("ArtificialIntelligence", "theme")
def test_whitespace_trimming(self):
result = validate_grouped_slugs(" artificialintelligence , cybersecurity ", "theme")
assert result == ["artificialintelligence", "cybersecurity"]
# ---------------------------------------------------------------------------
# TestBuildFilterParts
# ---------------------------------------------------------------------------
class TestBuildFilterParts:
"""Filter parts assembly from themes, sub-themes, and plain filters."""
def test_theme_only(self):
result = build_filter_parts([], ["artificialintelligence"], None)
assert result == ["theme_artificialintelligence"]
def test_multi_theme_pipe(self):
result = build_filter_parts([], ["artificialintelligence", "cybersecurity"], None)
assert result == ["theme_artificialintelligence|cybersecurity"]
def test_subtheme_only(self):
result = build_filter_parts([], None, ["aicloud"])
assert result == ["subtheme_aicloud"]
def test_multi_subtheme_pipe(self):
result = build_filter_parts([], None, ["aicloud", "aicompute"])
assert result == ["subtheme_aicloud|aicompute"]
def test_all_combined(self):
result = build_filter_parts(
["cap_midover", "ta_perf_13wup"],
["artificialintelligence"],
["aicloud"],
)
assert result == [
"theme_artificialintelligence",
"subtheme_aicloud",
"cap_midover",
"ta_perf_13wup",
]
# ---------------------------------------------------------------------------
# TestBuildUrl
# ---------------------------------------------------------------------------
class TestBuildUrl:
"""URL construction for various configurations."""
def test_public_overview_default(self):
url = build_url(["cap_small", "fa_div_o3"])
assert url == "https://finviz.com/screener.ashx?v=111&f=cap_small,fa_div_o3"
def test_elite_url(self):
url = build_url(["cap_small", "fa_div_o3"], elite=True)
assert url.startswith("https://elite.finviz.com/screener.ashx")
assert "f=cap_small,fa_div_o3" in url
def test_valuation_view(self):
url = build_url(["fa_pe_u20"], view="valuation")
assert "v=121" in url
def test_financial_view(self):
url = build_url(["fa_roe_o15"], view="financial")
assert "v=161" in url
def test_technical_view(self):
url = build_url(["ta_rsi_os30"], view="technical")
assert "v=171" in url
def test_ownership_view(self):
url = build_url(["fa_insttown_o50"], view="ownership")
assert "v=131" in url
def test_performance_view(self):
url = build_url(["ta_perf_13wup"], view="performance")
assert "v=141" in url
def test_custom_view(self):
url = build_url(["cap_large"], view="custom")
assert "v=152" in url
def test_order_parameter(self):
url = build_url(["cap_small"], order="-marketcap")
assert "&o=-marketcap" in url
def test_no_order_parameter(self):
url = build_url(["cap_small"])
assert "&o=" not in url
def test_unknown_view_falls_back_to_overview(self):
url = build_url(["cap_small"], view="nonexistent")
assert "v=111" in url
def test_all_view_codes_covered(self):
expected = {"111", "121", "131", "141", "152", "161", "171"}
assert set(VIEW_CODES.values()) == expected
def test_single_theme_url(self):
url = build_url([], themes=["artificialintelligence"])
assert "theme_artificialintelligence" in url
def test_multi_theme_url_encoded(self):
url = build_url([], themes=["artificialintelligence", "cybersecurity"])
assert "%7C" in url
assert "theme_artificialintelligence%7Ccybersecurity" in url
def test_theme_subtheme_filters_url(self):
url = build_url(
["cap_midover"],
themes=["artificialintelligence"],
subthemes=["aicloud"],
)
assert "theme_artificialintelligence" in url
assert "subtheme_aicloud" in url
assert "cap_midover" in url
# ---------------------------------------------------------------------------
# TestValidateFilters
# ---------------------------------------------------------------------------
class TestValidateFilters:
"""Filter token validation."""
# --- Valid tokens ---
def test_single_filter(self):
result = validate_filters("cap_small")
assert result == ["cap_small"]
def test_multiple_filters(self):
result = validate_filters("cap_small,fa_div_o3,fa_pe_u20")
assert result == ["cap_small", "fa_div_o3", "fa_pe_u20"]
def test_whitespace_trimming(self):
result = validate_filters(" cap_small , fa_div_o3 ")
assert result == ["cap_small", "fa_div_o3"]
def test_filter_with_dot(self):
result = validate_filters("fa_ltdebteq_u0.5")
assert result == ["fa_ltdebteq_u0.5"]
def test_long_prefix_earningsdate(self):
result = validate_filters("earningsdate_thisweek")
assert result == ["earningsdate_thisweek"]
def test_filter_with_hyphen(self):
"""Filters like fa_grossmargin_u-10 contain hyphens."""
result = validate_filters("fa_grossmargin_u-10")
assert result == ["fa_grossmargin_u-10"]
def test_filter_with_hyphen_negative_value(self):
result = validate_filters("fa_roa_u-50,sh_insidertrans_u-90")
assert result == ["fa_roa_u-50", "sh_insidertrans_u-90"]
def test_known_prefixes_accepted(self):
# One filter from each known prefix category (theme_ and subtheme_ excluded — use --themes/--subthemes)
tokens = [
"an_recom_strongbuy",
"cap_large",
"earningsdate_thisweek",
"etf_return_1yo10",
"exch_nasd",
"fa_pe_u20",
"geo_usa",
"idx_sp500",
"ind_semiconductors",
"ipodate_prevweek",
"news_date_today",
"sec_technology",
"sh_avgvol_o200",
"ta_rsi_os30",
"targetprice_a20",
]
result = validate_filters(",".join(tokens))
assert len(result) == len(tokens)
# --- Range pattern tokens ---
def test_filter_with_range_pattern(self):
"""Range filter like fa_div_3to8 should pass validation."""
result = validate_filters("fa_div_3to8")
assert result == ["fa_div_3to8"]
def test_filter_with_decimal_range(self):
"""Decimal range filter like ta_beta_0.5to1.5 should pass validation."""
result = validate_filters("ta_beta_0.5to1.5")
assert result == ["ta_beta_0.5to1.5"]
def test_filter_with_custom_highlow_below_high(self):
"""Custom 52-week below-high range like ta_highlow52w_10to30-bhx should pass."""
result = validate_filters("ta_highlow52w_10to30-bhx")
assert result == ["ta_highlow52w_10to30-bhx"]
def test_filter_with_custom_highlow_above_low(self):
"""Custom 52-week above-low range like ta_highlow52w_10to30-alx should pass."""
result = validate_filters("ta_highlow52w_10to30-alx")
assert result == ["ta_highlow52w_10to30-alx"]
# --- Rejected tokens (URL injection) ---
def test_reject_ampersand(self):
with pytest.raises(SystemExit):
validate_filters("cap_small&evil=1")
def test_reject_equals(self):
with pytest.raises(SystemExit):
validate_filters("cap_small=bad")
def test_reject_space_in_token(self):
with pytest.raises(SystemExit):
validate_filters("cap small")
def test_reject_url_encoded(self):
with pytest.raises(SystemExit):
validate_filters("cap%20small")
def test_reject_uppercase(self):
with pytest.raises(SystemExit):
validate_filters("Cap_Small")
def test_reject_semicolon(self):
with pytest.raises(SystemExit):
validate_filters("cap_small;drop")
def test_reject_slash(self):
with pytest.raises(SystemExit):
validate_filters("cap_small/evil")
def test_reject_question_mark(self):
with pytest.raises(SystemExit):
validate_filters("cap_small?v=111")
# --- Empty input ---
def test_reject_empty_string(self):
with pytest.raises(SystemExit):
validate_filters("")
def test_reject_only_commas(self):
with pytest.raises(SystemExit):
validate_filters(",,,")
# --- Unknown prefix warning (stderr, no error) ---
def test_unknown_prefix_warns(self, capsys):
result = validate_filters("xyz_something")
assert result == ["xyz_something"]
captured = capsys.readouterr()
assert "Warning: Unknown filter prefix" in captured.err
def test_known_prefix_no_warning(self, capsys):
validate_filters("fa_pe_u20")
captured = capsys.readouterr()
assert "Warning" not in captured.err
def test_known_prefixes_count(self):
"""All 17 FinViz filter prefixes are registered."""
assert len(KNOWN_PREFIXES) == 17
def test_known_prefixes_include_theme(self):
assert "theme_" in KNOWN_PREFIXES
def test_known_prefixes_include_subtheme(self):
assert "subtheme_" in KNOWN_PREFIXES
def test_conflict_theme_in_filters(self, capsys):
with pytest.raises(SystemExit):
validate_filters("theme_artificialintelligence,cap_small")
captured = capsys.readouterr()
assert "--themes" in captured.err
def test_conflict_subtheme_in_filters(self, capsys):
with pytest.raises(SystemExit):
validate_filters("subtheme_aicloud,cap_small")
captured = capsys.readouterr()
assert "--subthemes" in captured.err
def test_conflict_theme_pipe_in_filters(self, capsys):
"""Pipe-containing theme token must be caught before _TOKEN_RE rejects it."""
with pytest.raises(SystemExit):
validate_filters("theme_artificialintelligence|cybersecurity")
captured = capsys.readouterr()
assert "--themes" in captured.err
# ---------------------------------------------------------------------------
# TestValidateOrder
# ---------------------------------------------------------------------------
class TestValidateOrder:
"""Order parameter validation."""
# --- Valid orders ---
def test_simple_order(self):
assert validate_order("marketcap") == "marketcap"
def test_descending_order(self):
assert validate_order("-marketcap") == "-marketcap"
def test_order_with_underscore(self):
assert validate_order("dividend_yield") == "dividend_yield"
def test_order_with_digits(self):
assert validate_order("sma200") == "sma200"
# --- Rejected orders (URL injection) ---
def test_reject_ampersand(self):
with pytest.raises(SystemExit):
validate_order("-marketcap&evil=1")
def test_reject_equals(self):
with pytest.raises(SystemExit):
validate_order("order=bad")
def test_reject_space(self):
with pytest.raises(SystemExit):
validate_order("market cap")
def test_reject_url_encoded(self):
with pytest.raises(SystemExit):
validate_order("-market%20cap")
def test_reject_semicolon(self):
with pytest.raises(SystemExit):
validate_order("-marketcap;drop")
def test_reject_uppercase(self):
with pytest.raises(SystemExit):
validate_order("-MarketCap")
def test_reject_dot(self):
with pytest.raises(SystemExit):
validate_order("market.cap")
# ---------------------------------------------------------------------------
# TestEliteDetection
# ---------------------------------------------------------------------------
class TestEliteDetection:
"""Elite vs Public detection logic."""
def _make_args(self, elite: bool = False) -> argparse.Namespace:
return argparse.Namespace(elite=elite)
def test_elite_flag_explicit(self):
args = self._make_args(elite=True)
assert detect_elite(args) is True
def test_env_var_present(self):
args = self._make_args(elite=False)
with mock.patch.dict(
"os.environ",
{"FINVIZ_API_KEY": "test_key_123"}, # pragma: allowlist secret
):
assert detect_elite(args) is True
def test_no_flag_no_env(self):
args = self._make_args(elite=False)
with mock.patch.dict("os.environ", {}, clear=True):
assert detect_elite(args) is False
def test_elite_flag_overrides_missing_env(self):
args = self._make_args(elite=True)
with mock.patch.dict("os.environ", {}, clear=True):
assert detect_elite(args) is True
def test_empty_env_var_not_detected(self):
args = self._make_args(elite=False)
with mock.patch.dict("os.environ", {"FINVIZ_API_KEY": ""}):
assert detect_elite(args) is False
# ---------------------------------------------------------------------------
# TestOpenBrowser
# ---------------------------------------------------------------------------
class TestOpenBrowser:
"""Browser opening with OS-specific fallbacks."""
@mock.patch("open_finviz_screener.sys")
@mock.patch("open_finviz_screener.subprocess.run")
def test_macos_chrome(self, mock_run, mock_sys):
mock_sys.platform = "darwin"
mock_run.return_value = mock.MagicMock(returncode=0)
open_browser("https://finviz.com/screener.ashx?v=111&f=cap_small")
mock_run.assert_called_once_with(
["open", "-a", "Google Chrome", "https://finviz.com/screener.ashx?v=111&f=cap_small"],
check=True,
capture_output=True,
)
@mock.patch("open_finviz_screener.sys")
@mock.patch("open_finviz_screener.subprocess.run")
def test_macos_fallback_to_open(self, mock_run, mock_sys):
mock_sys.platform = "darwin"
# First call (Chrome) fails, second call (open) succeeds
mock_run.side_effect = [
subprocess.CalledProcessError(1, "open"),
mock.MagicMock(returncode=0),
]
open_browser("https://finviz.com/screener.ashx?v=111&f=cap_small")
assert mock_run.call_count == 2
assert mock_run.call_args_list[1][0][0] == [
"open",
"https://finviz.com/screener.ashx?v=111&f=cap_small",
]
@mock.patch("open_finviz_screener.sys")
@mock.patch("open_finviz_screener.shutil.which")
@mock.patch("open_finviz_screener.subprocess.run")
def test_linux_chrome(self, mock_run, mock_which, mock_sys):
mock_sys.platform = "linux"
mock_which.return_value = "/usr/bin/google-chrome"
mock_run.return_value = mock.MagicMock(returncode=0)
open_browser("https://finviz.com/screener.ashx?v=111&f=cap_small")
mock_run.assert_called_once()
assert mock_run.call_args[0][0][0] == "google-chrome"
@mock.patch("open_finviz_screener.webbrowser.open")
@mock.patch("open_finviz_screener.subprocess.run")
def test_fallback_to_webbrowser(self, mock_run, mock_wb_open):
# Both macOS calls fail → webbrowser fallback
mock_run.side_effect = [
subprocess.CalledProcessError(1, "open"),
subprocess.CalledProcessError(1, "open"),
]
url = "https://finviz.com/screener.ashx?v=111&f=cap_small"
open_browser(url)
mock_wb_open.assert_called_once_with(url)
# ---------------------------------------------------------------------------
# TestMainIntegration
# ---------------------------------------------------------------------------
class TestMainIntegration:
"""Integration tests via parse_args + main logic."""
def test_url_only_prints_url(self, capsys):
from open_finviz_screener import main
with mock.patch.dict("os.environ", {}, clear=True):
main(["--filters", "cap_small,fa_div_o3", "--url-only"])
captured = capsys.readouterr()
assert "https://finviz.com/screener.ashx" in captured.out
assert "cap_small,fa_div_o3" in captured.out
assert "[Public]" in captured.out
def test_elite_flag_in_output(self, capsys):
from open_finviz_screener import main
with mock.patch.dict("os.environ", {}, clear=True):
main(["--filters", "cap_small", "--elite", "--url-only"])
captured = capsys.readouterr()
assert "[Elite]" in captured.out
assert "elite.finviz.com" in captured.out
def test_view_selection(self, capsys):
from open_finviz_screener import main
with mock.patch.dict("os.environ", {}, clear=True):
main(["--filters", "fa_pe_u20", "--view", "valuation", "--url-only"])
captured = capsys.readouterr()
assert "v=121" in captured.out
def test_order_in_output(self, capsys):
from open_finviz_screener import main
with mock.patch.dict("os.environ", {}, clear=True):
# Use = syntax because argparse treats -marketcap as a flag
main(["--filters", "cap_small", "--order=-marketcap", "--url-only"])
captured = capsys.readouterr()
assert "o=-marketcap" in captured.out
def test_themes_only_no_filters(self, capsys):
from open_finviz_screener import main
with mock.patch.dict("os.environ", {}, clear=True):
main(["--themes", "artificialintelligence", "--url-only"])
captured = capsys.readouterr()
assert "theme_artificialintelligence" in captured.out
def test_themes_and_filters(self, capsys):
from open_finviz_screener import main
with mock.patch.dict("os.environ", {}, clear=True):
main(["--themes", "artificialintelligence", "--filters", "cap_midover", "--url-only"])
captured = capsys.readouterr()
assert "theme_artificialintelligence" in captured.out
assert "cap_midover" in captured.out
def test_subthemes_cli(self, capsys):
from open_finviz_screener import main
with mock.patch.dict("os.environ", {}, clear=True):
main(["--subthemes", "aicloud,aicompute", "--url-only"])
captured = capsys.readouterr()
assert "%7C" in captured.out or "|" in captured.out
assert "subtheme_" in captured.out
def test_empty_themes_string(self, capsys):
"""--themes '' should produce a specific error, not the generic 'at least one' error."""
from open_finviz_screener import main
with pytest.raises(SystemExit):
main(["--themes", "", "--url-only"])
captured = capsys.readouterr()
assert "--themes must contain at least one slug" in captured.err
def test_no_input_at_all(self):
from open_finviz_screener import main
with pytest.raises(SystemExit):
main(["--url-only"])
Related skills
How it compares
Use finviz-screener for FinViz URL and filter translation; use vcp-screener for live Volatility Contraction Pattern scans via FMP API.
FAQ
What URL format does finviz-screener generate?
finviz-screener builds FinViz screener URLs such as finviz.com/screener.ashx with v view, f comma-separated filters, o sort order, and s signal parameters. Elite subscribers use the elite.finviz.com host.
Does finviz-screener support non-English filter requests?
finviz-screener includes natural-language keyword mappings in English and Japanese for FinViz filter codes. Claude uses that reference to translate intent into valid f parameter values.
Is Finviz Screener safe to install?
skills.sh reports 3 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.