
Theme Detector
- 870 installs
- 2.5k repo stars
- Updated July 26, 2026
- tradermonty/claude-trading-skills
theme-detector is a Claude trading skill that automatically detects and ranks emerging market themes from stock data for developers evaluating what product or feature directions align with capital flows.
About
theme-detector is a skill from tradermonty/claude-trading-skills that scans stock universes and industries to surface ranked emerging market themes before product or investment decisions. Output follows a Theme Detection Report template with a dashboard table scoring each theme on direction (LEAD/LAG), heat on a 0-100 scale, lifecycle stage (Emerging, Accelerating, Trending, Mature, Exhausting), and confidence. Reports log themes analyzed, industries scanned, total stocks reviewed, uptrend data status, and execution time. Quantitative traders, fintech engineers, and product strategists invoke theme-detector when they need data-backed theme rankings rather than anecdotal sector narratives to guide feature bets or market-facing positioning.
- Generates ranked Theme Dashboard with direction, heat, lifecycle and confidence scores
- Delivers detailed bullish theme reports including momentum, volume, breadth and narrative assessment
- Analyzes industries, top movers, ETFs and relative P/E ratios per theme
- Outputs structured markdown reports with Handlebars templating for consistent delivery
- Scans thousands of stocks across multiple industries in a single run
Theme Detector by the numbers
- 870 all-time installs (skills.sh)
- +49 installs in the week ending Jul 28, 2026 (Skillselion tracking)
- Ranked #1,208 of 16,659 AI & Agent Building skills by installs in the Skillselion catalog
- Security screen: HIGH risk (skills.sh audit)
- Data as of Jul 28, 2026 (Skillselion catalog sync)
npx skills add https://github.com/tradermonty/claude-trading-skills --skill theme-detectorAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 870 |
|---|---|
| repo stars | ★ 2.5k |
| Security audit | 2 / 3 scanners passed |
| Last updated | July 26, 2026 |
| Repository | tradermonty/claude-trading-skills ↗ |
How do you detect emerging market themes from stocks?
Automatically detect and rank emerging market themes from stock data before deciding what product or feature to build next.
Who is it for?
Fintech engineers and product strategists who rank equity-market themes by heat and lifecycle before prioritizing feature or investment bets.
Skip if: Developers seeking fundamental company valuation models without cross-industry thematic scanning and ranked heat scoring.
When should I use this skill?
Stock data is available and the user wants ranked emerging themes with heat scores, lifecycle stages, and confidence before product decisions.
What you get
Theme Detection Report with ranked dashboard, heat scores, lifecycle labels, direction flags, and scan metadata.
- Theme Detection Report
- ranked theme dashboard
By the numbers
- Scores theme heat on a 0-100 scale
- Tracks 5 lifecycle stages: Emerging, Accelerating, Trending, Mature, Exhausting
Files
Theme Detector
Overview
This skill detects and ranks trending market themes by analyzing cross-sector momentum, volume, and breadth signals. It identifies both bullish (upward momentum) and bearish (downward pressure) themes, assesses lifecycle maturity (Emerging/Accelerating/Trending/Mature/Exhausting), and provides a confidence score combining quantitative data with narrative analysis.
3-Dimensional Scoring Model: 1. Theme Heat (0-100): Direction-neutral strength of the theme (momentum, volume, uptrend ratio, breadth) 2. Lifecycle Maturity: Stage classification (Emerging / Accelerating / Trending / Mature / Exhausting) based on duration, extremity clustering, valuation, and ETF proliferation 3. Confidence (Low / Medium / High): Reliability of the detection, combining quantitative breadth with narrative confirmation. Script output is capped at Medium; Claude's WebSearch narrative confirmation step can elevate to High.
Key Features:
- Cross-sector theme detection using FINVIZ industry data
- Direction-aware scoring (bullish and bearish themes)
- Lifecycle maturity assessment to identify crowded vs. emerging trades
- ETF proliferation scoring (more ETFs = more mature/crowded theme)
- Integration with uptrend-dashboard for 3-point evaluation
- Dual-mode operation: FINVIZ Elite (fast) or public scraping (slower, limited)
- WebSearch-based narrative confirmation for top themes
---
When to Use This Skill
Explicit Triggers:
- "What market themes are trending right now?"
- "Which sectors are hot/cold?"
- "Detect current market themes"
- "What are the strongest bullish/bearish narratives?"
- "Is AI/clean energy/defense still a strong theme?"
- "Where is sector rotation heading?"
- "Show me thematic investing opportunities"
Implicit Triggers:
- User wants to understand broad market narrative shifts
- User is looking for thematic ETF or sector allocation ideas
- User asks about crowded trades or late-cycle themes
- User wants to know which themes are emerging vs. exhausted
When NOT to Use:
- Individual stock analysis (use us-stock-analysis instead)
- Specific sector deep-dive with chart reading (use sector-analyst instead)
- Portfolio rebalancing (use portfolio-manager instead)
- Dividend/income investing (use value-dividend-screener instead)
---
Prerequisites
Required:
- Python 3.10+ with core dependencies. The scripts use modern type-hint syntax such as
dict | None, so Python 3.9 and older can fail at import time even when dependencies are installed.
pip install requests beautifulsoup4 lxml pandas numpy yfinanceCron / mixed-Python fallback: If the active python3 is older than 3.10, or a newer Hermes venv lacks the data-science dependencies, run the detector through uv with an explicit modern interpreter and temporary dependencies instead of editing the environment mid-cron:
uv run --python 3.12 \
--with requests --with beautifulsoup4 --with lxml \
--with pandas --with numpy --with yfinance \
--with finvizfinance --with PyYAML \
python skills/theme-detector/scripts/theme_detector.py \
--finviz-api-key "$FINVIZ_API_KEY" \
--fmp-api-key "$FMP_API_KEY" \
--output-dir reports/Use this as a setup workaround, not as evidence that the detector is broken; still report FINVIZ/FMP/API-data caveats separately.
Optional API Keys:
FINVIZ Elite (recommended for full industry coverage and speed):
export FINVIZ_API_KEY=your_finviz_elite_api_key_hereFMP API (optional, for P/E ratio valuation data):
export FMP_API_KEY=your_fmp_api_key_hereOptional Python packages:
finvizfinance- Required for FINVIZ Elite modePyYAML- Required for--themes-configcustom themes
Without FINVIZ Elite, the skill uses public FINVIZ scraping (limited to ~20 stocks per industry, slower rate limits).
---
Workflow
Step 1: Verify Environment
Check that API keys are configured (see Prerequisites):
# Verify FINVIZ Elite API key (optional but recommended)
echo $FINVIZ_API_KEY
# Verify FMP API key (optional)
echo $FMP_API_KEYStep 2: Execute Theme Detection Script
Run the main detection script:
python3 skills/theme-detector/scripts/theme_detector.py \
--output-dir reports/Script Options:
# Full run (public FINVIZ mode, no API key required)
python3 skills/theme-detector/scripts/theme_detector.py \
--output-dir reports/
# With FINVIZ Elite API key
python3 skills/theme-detector/scripts/theme_detector.py \
--finviz-api-key $FINVIZ_API_KEY \
--output-dir reports/
# With FMP API key for enhanced stock data
python3 skills/theme-detector/scripts/theme_detector.py \
--fmp-api-key $FMP_API_KEY \
--output-dir reports/
# Custom limits
python3 skills/theme-detector/scripts/theme_detector.py \
--max-themes 5 \
--max-stocks-per-theme 10 \
--output-dir reports/
# Explicit FINVIZ mode
python3 skills/theme-detector/scripts/theme_detector.py \
--finviz-mode public \
--output-dir reports/Expected Execution Time:
- FINVIZ Elite mode: ~2-3 minutes (14+ themes)
- Public FINVIZ mode: ~5-8 minutes (rate-limited scraping)
Step 3: Read and Parse Detection Results
The script generates two output files:
theme_detector_YYYY-MM-DD_HHMMSS.json- Structured data for programmatic usetheme_detector_YYYY-MM-DD_HHMMSS.md- Human-readable report
Read the JSON output to understand quantitative results:
# Find the latest report
ls -lt reports/theme_detector_*.json | head -1
# Read the JSON output
cat reports/theme_detector_YYYY-MM-DD_HHMMSS.jsonStep 4: Perform Narrative Confirmation via WebSearch
For the top 5 themes (by Theme Heat score), execute WebSearch queries to confirm narrative strength:
Search Pattern:
"[theme name] stocks market [current month] [current year]"
"[theme name] sector momentum [current month] [current year]"Evaluate narrative signals:
- Strong narrative: Multiple major outlets covering the theme, analyst upgrades, policy catalysts
- Moderate narrative: Some coverage, mixed sentiment, no clear catalyst
- Weak narrative: Little coverage, or predominantly contrarian/skeptical tone
Update Confidence levels based on findings:
- Quantitative High + Narrative Strong = High confidence
- Quantitative High + Narrative Weak = Medium confidence (possible momentum divergence)
- Quantitative Low + Narrative Strong = Medium confidence (narrative may lead price)
- Quantitative Low + Narrative Weak = Low confidence
Step 5: Analyze Results and Provide Recommendations
Cross-reference detection results with knowledge bases:
Reference Documents to Consult: 1. references/cross_sector_themes.md - Theme definitions and constituent industries 2. references/thematic_etf_catalog.md - ETF exposure options by theme 3. references/theme_detection_methodology.md - Scoring model details 4. references/finviz_industry_codes.md - Industry classification reference
Analysis Framework:
For Hot Bullish Themes (Heat >= 70, Direction = Bullish):
- Identify lifecycle stage (Emerging = opportunity, Mature/Exhausting = caution)
- List top-performing industries within the theme
- Recommend proxy ETFs for exposure
- Flag if ETF proliferation is high (crowded trade warning)
For Hot Bearish Themes (Heat >= 70, Direction = Bearish):
- Identify industries under pressure
- Assess if bearish momentum is accelerating or decelerating
- Recommend hedging strategies or sectors to avoid
- Note potential mean-reversion opportunities if lifecycle is Mature/Exhausting
For Emerging Themes (Heat 40-69, Lifecycle = Emerging):
- These may represent early rotation signals
- Recommend monitoring with watchlist
- Identify catalyst events that could accelerate the theme
For Exhausted Themes (Heat >= 60, Lifecycle = Exhausting):
- Warn about crowded trade risk
- High ETF count confirms excessive retail participation
- Consider contrarian positioning or reducing exposure
Step 6: Generate Final Report
Present the final report to the user using the report template structure:
# Theme Detection Report
**Date:** YYYY-MM-DD
**Mode:** FINVIZ Elite / Public
**Themes Analyzed:** N
**Data Quality:** [note any limitations]
## Theme Dashboard
[Top themes table with Heat, Direction, Lifecycle, Confidence]
## Bullish Themes Detail
[Detailed analysis of bullish themes sorted by Heat]
## Bearish Themes Detail
[Detailed analysis of bearish themes sorted by Heat]
## All Themes Summary
[Complete theme ranking table]
## Industry Rankings
[Top performing and worst performing industries]
## Sector Uptrend Ratios
[Sector-level aggregation if uptrend data available]
## Methodology Notes
[Brief explanation of scoring model]Save the report to reports/ directory.
---
Output
The skill generates two output files in the reports/ directory:
JSON Output (theme_detector_YYYY-MM-DD_HHMMSS.json):
{
"report_type": "theme_detector",
"generated_at": "2026-04-18 10:30:00",
"metadata": {
"generated_at": "2026-04-18 10:30:00",
"data_mode": "full",
"finviz_mode": "elite",
"fmp_available": true,
"max_themes": 14,
"max_stocks_per_theme": 5,
"data_sources": {
"finviz_industries": 152,
"yfinance_stocks": 68,
"etf_volume": 24
}
},
"summary": {
"total_themes": 14,
"bullish_count": 8,
"bearish_count": 6,
"top_bullish": "AI & Machine Learning",
"top_bearish": "Regional Banks"
},
"themes": {
"all": [
{
"name": "AI & Machine Learning",
"direction": "bullish",
"heat": 85.3,
"maturity": 42.1,
"stage": "Accelerating",
"confidence": "Medium",
"heat_label": "Hot",
"industries": ["Software - Infrastructure", "Semiconductors"],
"representative_stocks": [{"symbol": "NVDA"}, {"symbol": "MSFT"}],
"proxy_etfs": ["BOTZ", "ROBO"],
"theme_origin": "seed"
}
],
"bullish": [...],
"bearish": [...]
},
"industry_rankings": {
"top": [...],
"bottom": [...]
},
"sector_uptrend": {...},
"data_quality": {...}
}Markdown Report (theme_detector_YYYY-MM-DD_HHMMSS.md):
- Theme Dashboard with sortable rankings
- Bullish/Bearish theme detail sections
- Industry performance rankings
- Sector uptrend ratio summary
- Methodology notes
Key Output Fields (per theme):
| Field | Description |
|---|---|
heat | 0-100 direction-neutral theme strength |
direction | "bullish" (LEAD) or "bearish" (LAG) |
stage | Emerging / Accelerating / Trending / Mature / Exhausting |
confidence | Low / Medium / High (script caps at Medium; WebSearch can elevate) |
representative_stocks | Top stocks for the theme (list of objects with symbol and metrics) |
proxy_etfs | Thematic ETF tickers (length = ETF count; higher = more crowded) |
theme_origin | "seed" (from YAML config) or "discovered" (auto-clustered) |
---
Resources
Scripts Directory (scripts/)
Main Scripts:
theme_detector.py- Main orchestrator script- Coordinates industry data collection, theme classification, and scoring
- Generates JSON + Markdown output
- Usage:
python3 theme_detector.py [options]
theme_classifier.py- Maps industries to cross-sector themes- Reads theme definitions from
cross_sector_themes.md - Calculates theme-level aggregated scores
- Determines direction (bullish/bearish) from constituent industries
- Display mapping: "bullish" → "LEAD", "bearish" → "LAG" (see report_generator.py::_direction_label())
finviz_industry_scanner.py- FINVIZ industry data collection- Elite mode: CSV export with full stock data per industry
- Public mode: Web scraping with rate limiting
- Extracts: performance, volume, change%, avg volume, market cap
calculators/lifecycle_calculator.py- Lifecycle maturity assessment- Duration scoring, extremity clustering, valuation analysis
- ETF proliferation scoring from thematic_etf_catalog.md
- Stage classification: Emerging / Accelerating / Trending / Mature / Exhausting
report_generator.py- Report output generation- Markdown report from template
- JSON structured output
- Theme dashboard formatting
References Directory (references/)
Knowledge Bases:
cross_sector_themes.md- Theme definitions with industries, ETFs, stocks, and matching criteriathematic_etf_catalog.md- Comprehensive thematic ETF catalog with counts per themefinviz_industry_codes.md- Complete FINVIZ industry-to-filter-code mappingtheme_detection_methodology.md- Technical documentation of the 3D scoring model
Assets Directory (assets/)
report_template.md- Markdown template for report generation with placeholder format
---
Important Notes
FINVIZ Mode Differences
| Feature | Elite Mode | Public Mode |
|---|---|---|
| Industry coverage | All ~145 industries | All ~145 industries |
| Stocks per industry | Full universe | ~20 stocks (page 1) |
| Rate limiting | 0.5s between requests | 2.0s between requests |
| Data freshness | Real-time | 15-min delayed |
| API key required | Yes ($39.50/mo) | No |
| Execution time | ~2-3 minutes | ~5-8 minutes |
Direction Detection Logic
Theme direction is determined by majority vote of constituent industries' relative rank:
1. Industry ranking: All ~145 industries are ranked by multi-timeframe momentum score 2. Rank-based direction: Industries in the top half of the ranked list are classified as "bullish"; bottom half as "bearish" 3. Theme majority vote: _majority_direction() counts bullish vs. bearish industries within each theme; the majority wins
Display mapping: "bullish" → LEAD, "bearish" → LAG (see report_generator.py::_direction_label())
A LEAD theme indicates relative outperformance of its constituent industries. A LAG theme may still have positive absolute returns — it indicates relative underperformance, not a short signal.
Known Limitations
1. Survivorship bias: Only analyzes currently listed stocks and ETFs 2. Lag: FINVIZ data may lag intraday moves by 15 minutes (public mode) 3. Theme boundaries: Some stocks fit multiple themes; classification uses primary industry 4. ETF proliferation: Catalog is static and may not capture very new ETFs 5. Narrative scoring: WebSearch-based and inherently subjective 6. Public mode limitation: ~20 stocks per industry may miss small-cap signals
Disclaimer
This analysis is for educational and informational purposes only.
- Not investment advice
- Past thematic trends do not guarantee future performance
- Theme detection identifies momentum, not fundamental value
- Conduct your own research before making investment decisions
---
Version: 1.0 Last Updated: 2026-02-16 API Requirements: FINVIZ Elite (recommended) or public mode (free); FMP API optional Execution Time: ~2-8 minutes depending on mode Output Formats: JSON + Markdown Themes Covered: 14+ cross-sector themes
Theme Detection Report
Date: {{date}} Mode: {{mode}} Themes Analyzed: {{themes_analyzed}} Industries Scanned: {{industries_scanned}} Total Stocks: {{total_stocks}} Uptrend Data: {{uptrend_data_status}} Execution Time: {{execution_time}}
---
1. Theme Dashboard
| Rank | Theme | Direction | Heat | Lifecycle | Confidence |
|---|
{{#each top_themes}} | {{rank}} | {{name}} | {{direction_emoji}} {{direction}} | {{heat}}/100 | {{lifecycle}} | {{confidence}} | {{/each}}
Legend: Direction: LEAD/LAG (Dashboard), ^/v (Summary) | Heat: 0-100 scale | Lifecycle: Emerging/Accelerating/Trending/Mature/Exhausting
---
2. Bullish Themes Detail
{{#each bullish_themes}}
{{name}} - Heat: {{heat}}/100
Direction: Bullish ({{direction_strength}}) | Lifecycle: {{lifecycle}} | Confidence: {{confidence}}
Heat Components:
- Momentum: {{momentum}}/100
- Volume: {{volume}}/100
- Uptrend: {{uptrend}}/100
- Breadth: {{breadth}}/100
Lifecycle Factors:
- Duration: {{duration_weeks}} weeks
- Extremity clustering: {{extremity_pct}}%
- Relative P/E: {{relative_pe}}x
- ETF proliferation: {{etf_count}} ETFs (score: {{etf_score}}/100)
- Narrative: {{narrative_assessment}}
Top Industries: {{#each industries}}
- {{name}}: {{change_pct}}% ({{stock_count}} stocks, uptrend {{uptrend_ratio}}%)
{{/each}}
Top Movers: {{#each top_stocks}}
- {{ticker}}: {{change_pct}}% | Vol: {{relative_volume}}x
{{/each}}
Proxy ETFs: {{proxy_etfs}}
Assessment: {{assessment}}
--- {{/each}}
3. Bearish Themes Detail
{{#each bearish_themes}}
{{name}} - Heat: {{heat}}/100
Direction: Bearish ({{direction_strength}}) | Lifecycle: {{lifecycle}} | Confidence: {{confidence}}
Heat Components:
- Momentum: {{momentum}}/100
- Volume: {{volume}}/100
- Downtrend: {{downtrend}}/100
- Breadth: {{breadth}}/100
Top Declining Industries: {{#each industries}}
- {{name}}: {{change_pct}}% ({{stock_count}} stocks, downtrend {{downtrend_ratio}}%)
{{/each}}
Worst Performers: {{#each bottom_stocks}}
- {{ticker}}: {{change_pct}}% | Vol: {{relative_volume}}x
{{/each}}
Assessment: {{assessment}}
--- {{/each}}
4. All Themes Summary
| Theme | Direction | Heat | Momentum | Volume | Uptrend | Breadth | Lifecycle | Confidence |
|---|
{{#each all_themes}} | {{name}} | {{direction}} | {{heat}} | {{momentum}} | {{volume}} | {{uptrend}} | {{breadth}} | {{lifecycle}} | {{confidence}} | {{/each}}
---
5. Industry Rankings
Top 10 Industries (by performance)
| Rank | Industry | Sector | Change % | Rel. Volume | Uptrend Ratio | Theme |
|---|
{{#each top_industries}} | {{rank}} | {{name}} | {{sector}} | {{change_pct}}% | {{rel_volume}}x | {{uptrend_ratio}}% | {{theme}} | {{/each}}
Bottom 10 Industries (by performance)
| Rank | Industry | Sector | Change % | Rel. Volume | Downtrend Ratio | Theme |
|---|
{{#each bottom_industries}} | {{rank}} | {{name}} | {{sector}} | {{change_pct}}% | {{rel_volume}}x | {{downtrend_ratio}}% | {{theme}} | {{/each}}
---
6. Sector Uptrend Ratios
| Sector | Uptrend Ratio | Avg Change % | Dominant Theme | Direction |
|---|
{{#each sectors}} | {{name}} | {{uptrend_ratio}}% | {{avg_change_pct}}% | {{dominant_theme}} | {{direction}} | {{/each}}
---
7. Methodology Notes
Scoring Model: 3-Dimensional (Heat x Lifecycle x Confidence)
- Theme Heat (0-100): Momentum (30%) + Volume (25%) + Uptrend (25%) + Breadth (20%)
- Lifecycle: Duration + Extremity clustering + Valuation + ETF proliferation + Narrative saturation
- Confidence: Quantitative breadth + Cross-sector confirmation + Narrative verification
Data Sources:
- FINVIZ {{mode}} (industry performance, volume, 52-week data)
{{#if uptrend_data}}
- uptrend-dashboard (3-point technical evaluation)
{{/if}} {{#if fmp_data}}
- FMP API (P/E ratios for valuation analysis)
{{/if}}
- WebSearch (narrative confirmation for top themes)
Limitations:
- Theme definitions are static (predefined in cross_sector_themes.md)
- {{mode_limitation}}
- Duration tracking requires historical baseline data
- Narrative assessment is subjective
---
Generated by Theme Detector v1.0 | {{date}} {{time}}
Cross-Sector Theme Definitions
This reference defines market themes by their constituent FINVIZ industries, sectors, proxy ETFs, and representative stocks. The theme_classifier.py script reads these definitions to map industry-level data into theme-level aggregations.
Usage Notes:
- Industry names must exactly match FINVIZ industry names (see
finviz_industry_codes.md) min_matching_industries: Design-intent minimum for each theme. In practice, theme detection uses the globalcross_sector_min_matchessetting (default: 2, configurable in themes.yaml) as the threshold. Per-theme values below are informational and not individually enforced in codestatic_stocks: Fallback representative stocks used when industry-level data is insufficientproxy_etfs: Used for quick volume/momentum checks and as user-facing exposure recommendations
---
AI & Semiconductors
- Direction bias: Bullish (typically)
- Industries: Semiconductors, Software - Application, Software - Infrastructure, Information Technology Services, Electronic Components, Computer Hardware, Scientific & Technical Instruments
- Sectors: Technology (primary), Communication Services, Industrials
- Proxy ETFs: SMH, SOXX, AIQ, BOTZ, CHAT
- Static stocks: NVDA, AVGO, AMD, INTC, QCOM, MRVL, AMAT, LRCX, KLAC, TSM, MU, ARM, SNPS, CDNS, MCHP
- Min matching industries: 2
---
Clean Energy & EV
- Direction bias: Bullish (typically)
- Industries: Solar, Utilities - Renewable, Auto Manufacturers, Auto Parts, Electrical Equipment & Parts, Specialty Chemicals
- Sectors: Utilities, Consumer Cyclical, Industrials, Basic Materials
- Proxy ETFs: ICLN, QCLN, TAN, DRIV, LIT
- Static stocks: ENPH, SEDG, FSLR, RUN, TSLA, RIVN, LCID, NIO, PLUG, BE, CHPT, ALB, SQM, LAC, LTHM
- Min matching industries: 2
---
Cybersecurity
- Direction bias: Bullish (typically)
- Industries: Software - Infrastructure, Information Technology Services, Software - Application, Communication Equipment
- Sectors: Technology (primary)
- Proxy ETFs: CIBR, HACK, BUG
- Static stocks: CRWD, PANW, FTNT, ZS, NET, S, OKTA, CYBR, QLYS, RPD, TENB, VRNS, SAIL, MNDT, DDOG
- Min matching industries: 2
Note: Cybersecurity overlaps with broader software industries. Theme classification uses proxy ETF volume and static stock performance to differentiate from general software themes.
---
Cloud Computing & SaaS
- Direction bias: Bullish (typically)
- Industries: Software - Application, Software - Infrastructure, Information Technology Services
- Sectors: Technology (primary), Communication Services
- Proxy ETFs: SKYY, WCLD, CLOU
- Static stocks: CRM, NOW, SNOW, DDOG, TEAM, MDB, ESTC, NET, ZS, HUBS, BILL, TTD, PLTR, DOCN, DT
- Min matching industries: 2
Note: Cloud/SaaS overlaps significantly with Cybersecurity and AI themes. When multiple themes share the same industries, proxy ETF performance differentiates them.
---
Biotech & Genomics
- Direction bias: Bullish (typically, but highly volatile)
- Industries: Biotechnology, Drug Manufacturers - Specialty & Generic, Medical Devices, Diagnostics & Research, Drug Manufacturers - General
- Sectors: Healthcare (primary)
- Proxy ETFs: XBI, IBB, ARKG, GNOM
- Static stocks: AMGN, GILD, VRTX, REGN, MRNA, BIIB, ILMN, CRSP, NTLA, BEAM, EDIT, EXAS, TWST, SGEN, BMRN
- Min matching industries: 2
---
Infrastructure & Construction
- Direction bias: Bullish (typically, policy-driven)
- Industries: Engineering & Construction, Building Materials, Industrial Distribution, Farm & Heavy Construction Machinery, Steel, Specialty Industrial Machinery, Railroads, Waste Management
- Sectors: Industrials (primary), Basic Materials
- Proxy ETFs: PAVE, IFRA, SIMS
- Static stocks: CAT, DE, VMC, MLM, URI, PWR, EME, MTZ, GVA, AECOM, STRL, GBX, NUE, CLF, RS
- Min matching industries: 3
---
Gold & Precious Metals
- Direction bias: Bullish (typically in risk-off or inflation)
- Industries: Gold, Silver, Other Precious Metals & Mining
- Sectors: Basic Materials (primary)
- Proxy ETFs: GDX, GDXJ, RING, SIL
- Static stocks: NEM, GOLD, AEM, FNV, WPM, RGLD, KGC, AGI, AU, HMY, PAAS, CDE, HL, MAG, EQX
- Min matching industries: 2
---
Oil & Gas (Energy Sector)
- Direction bias: Varies (cyclical)
- Industries: Oil & Gas E&P, Oil & Gas Equipment & Services, Oil & Gas Midstream, Oil & Gas Refining & Marketing, Oil & Gas Integrated, Oil & Gas Drilling
- Sectors: Energy (primary)
- Proxy ETFs: XLE, XOP, OIH
- Static stocks: XOM, CVX, COP, EOG, SLB, HAL, PXD, DVN, MPC, VLO, PSX, OXY, FANG, HES, WMB
- Min matching industries: 2
---
Financial Services & Banks
- Direction bias: Varies (rate-sensitive)
- Industries: Banks - Diversified, Banks - Regional, Capital Markets, Insurance - Diversified, Insurance - Property & Casualty, Financial Data & Stock Exchanges, Credit Services, Asset Management, Insurance Brokers, Mortgage Finance
- Sectors: Financial Services (primary)
- Proxy ETFs: XLF, KBE, KRE, IAI
- Static stocks: JPM, BAC, WFC, GS, MS, C, SCHW, BLK, AXP, ICE, CME, MCO, SPGI, BX, KKR
- Min matching industries: 3
---
Healthcare & Pharma
- Direction bias: Varies (defensive in downturns)
- Industries: Drug Manufacturers - General, Health Care Plans, Medical Care Facilities, Health Information Services, Medical Distribution, Medical Instruments & Supplies, Pharmaceutical Retailers
- Sectors: Healthcare (primary)
- Proxy ETFs: XLV, IHE, IHI
- Static stocks: UNH, JNJ, LLY, PFE, ABT, TMO, DHR, MDT, ISRG, SYK, BSX, EW, HCA, CVS, CI
- Min matching industries: 3
Note: Healthcare & Pharma is distinct from Biotech & Genomics. Healthcare focuses on established pharma, insurance, and medical devices; Biotech focuses on emerging drug development and genomics.
---
Defense & Aerospace
- Direction bias: Bullish (typically in geopolitical tension)
- Industries: Aerospace & Defense, Airlines, Security & Protection Services
- Sectors: Industrials (primary)
- Proxy ETFs: ITA, PPA, ROKT, ARKX
- Static stocks: LMT, RTX, NOC, BA, GD, LHX, HII, TDG, HWM, AXON, LDOS, BWXT, KTOS, RKLB, SPR
- Min matching industries: 2
---
Real Estate & REITs
- Direction bias: Varies (rate-sensitive)
- Industries: REIT - Residential, REIT - Industrial, REIT - Retail, REIT - Office, REIT - Healthcare Facilities, REIT - Diversified, REIT - Hotel & Motel, REIT - Specialty, Real Estate Services, Real Estate - Diversified, Real Estate - Development
- Sectors: Real Estate (primary)
- Proxy ETFs: VNQ, XLRE, IYR
- Static stocks: PLD, AMT, CCI, EQIX, SPG, O, WELL, DLR, PSA, VICI, EXR, AVB, ARE, MAA, IRM
- Min matching industries: 3
---
Retail & Consumer
- Direction bias: Varies (consumer sentiment driven)
- Industries: Internet Retail, Specialty Retail, Apparel Retail, Home Improvement Retail, Department Stores, Discount Stores, Luxury Goods, Restaurants, Leisure, Resorts & Casinos, Gambling, Apparel Manufacturing, Footwear & Accessories
- Sectors: Consumer Cyclical (primary), Consumer Defensive
- Proxy ETFs: XLY, XRT, XLP, IBUY
- Static stocks: AMZN, HD, LOW, TJX, COST, WMT, TGT, NKE, SBUX, MCD, DPZ, LULU, ROST, BURL, DECK
- Min matching industries: 3
---
Crypto & Blockchain
- Direction bias: Bullish (typically in risk-on)
- Industries: Capital Markets, Software - Application, Financial Data & Stock Exchanges, Information Technology Services
- Sectors: Financial Services, Technology
- Proxy ETFs: BITO, BLOK, BITQ, IBIT, DAPP
- Static stocks: COIN, MSTR, MARA, RIOT, CLSK, HUT, BITF, SQ, PYPL, HOOD, CIFR, IREN, HIVE, CORZ, BTBT
- Min matching industries: 2
Note: Crypto theme uses proxy ETFs and static stocks as primary signals rather than industry classification, since blockchain companies span multiple traditional industries.
---
Nuclear Energy
- Direction bias: Bullish (policy-driven, AI data center demand)
- Industries: Uranium, Utilities - Independent Power Producers, Specialty Industrial Machinery, Electrical Equipment & Parts
- Sectors: Energy, Utilities, Industrials
- Proxy ETFs: URA, URNM, NLR, NUKZ
- Static stocks: CCJ, UEC, NXE, DNN, UUUU, LEU, SMR, OKLO, BWX, GEV, CEG, VST, TLN, NRG, BWXT
- Min matching industries: 2
---
Uranium
- Direction bias: Bullish (supply deficit narrative)
- Industries: Uranium, Other Industrial Metals & Mining
- Sectors: Energy (primary), Basic Materials
- Proxy ETFs: URA, URNM, URNJ
- Static stocks: CCJ, UEC, NXE, DNN, UUUU, LEU, URG, GLATF, EU, PALAF, SRUUF, LTBR, FCUUF, AEC, WSTRF
- Min matching industries: 1
Note: Uranium is a sub-theme of Nuclear Energy but tracked separately due to its distinct commodity-driven dynamics. Requires only 1 matching industry due to narrow sector focus.
---
Obesity & GLP-1
- Direction bias: Bullish (medical innovation)
- Industries: Drug Manufacturers - General, Drug Manufacturers - Specialty & Generic, Medical Devices, Biotechnology
- Sectors: Healthcare (primary)
- Proxy ETFs: SLIM, HRTS
- Static stocks: LLY, NVO, AMGN, VKTX, ALT, GPCR, SMLR, RVMD, PTGX, IVA
- Min matching industries: 2
Note: Obesity/GLP-1 is a narrow theme that overlaps with Healthcare & Pharma. It is differentiated primarily through proxy ETF volume and static stock performance rather than industry classification.
---
Summary Table
| Theme | Industries | Sectors | Proxy ETFs | Static Stocks | Min Industries |
|---|---|---|---|---|---|
| AI & Semiconductors | 7 | 3 | 5 | 15 | 2 |
| Clean Energy & EV | 6 | 4 | 5 | 15 | 2 |
| Cybersecurity | 4 | 1 | 3 | 15 | 2 |
| Cloud Computing & SaaS | 3 | 2 | 3 | 15 | 2 |
| Biotech & Genomics | 5 | 1 | 4 | 15 | 2 |
| Infrastructure & Construction | 8 | 2 | 3 | 15 | 3 |
| Gold & Precious Metals | 3 | 1 | 4 | 15 | 2 |
| Oil & Gas (Energy) | 6 | 1 | 3 | 15 | 2 |
| Financial Services & Banks | 10 | 1 | 4 | 15 | 3 |
| Healthcare & Pharma | 7 | 1 | 3 | 15 | 3 |
| Defense & Aerospace | 3 | 1 | 4 | 15 | 2 |
| Real Estate & REITs | 11 | 1 | 3 | 15 | 3 |
| Retail & Consumer | 13 | 2 | 4 | 15 | 3 |
| Crypto & Blockchain | 4 | 2 | 5 | 15 | 2 |
| Nuclear Energy | 4 | 3 | 4 | 15 | 2 |
| Uranium | 2 | 2 | 3 | 15 | 1 |
| Obesity & GLP-1 | 4 | 1 | 2 | 10 | 2 |
Total: 17 themes covering all major market narratives.
---
Theme Overlap Matrix
Some industries contribute to multiple themes. When scoring, each industry's data is used in all applicable themes:
| Industry | Themes |
|---|---|
| Software - Application | AI, Cybersecurity, Cloud, Crypto |
| Software - Infrastructure | AI, Cybersecurity, Cloud |
| Drug Manufacturers - General | Healthcare, Biotech, Obesity |
| Biotechnology | Biotech, Obesity |
| Capital Markets | Financial Services, Crypto |
| Electrical Equipment & Parts | Clean Energy, Nuclear |
| Uranium | Nuclear, Uranium |
This overlap is intentional - a strong software industry may boost multiple themes simultaneously, reflecting the interconnected nature of market narratives.
FINVIZ Industry Filter Codes
This reference maps all FINVIZ industry names to their corresponding filter codes used in the FINVIZ Screener API and Elite CSV export URLs.
URL Pattern (Elite CSV Export):
https://elite.finviz.com/export.ashx?v=151&f=ind_{code},cap_smallover&auth={API_KEY}URL Pattern (Public Screener):
https://finviz.com/screener.ashx?v=151&f=ind_{code},cap_smalloverFilter Code Convention: ind_ + lowercase industry name with spaces, hyphens, ampersands, and special characters removed.
---
Aerospace & Defense
| Industry Name | Filter Code |
|---|---|
| Aerospace & Defense | ind_aerospacedefense |
Auto & Transport
| Industry Name | Filter Code |
|---|---|
| Airlines | ind_airlines |
| Auto Manufacturers | ind_automanufacturers |
| Auto Parts | ind_autoparts |
| Railroads | ind_railroads |
| Recreational Vehicles | ind_recreationalvehicles |
| Trucking | ind_trucking |
| Marine Shipping | ind_marineshipping |
Basic Materials
| Industry Name | Filter Code |
|---|---|
| Aluminum | ind_aluminum |
| Building Materials | ind_buildingmaterials |
| Chemicals | ind_chemicals |
| Coking Coal | ind_cokingcoal |
| Copper | ind_copper |
| Gold | ind_gold |
| Lumber & Wood Production | ind_lumberwoodproduction |
| Other Industrial Metals & Mining | ind_otherindustrialmetalsmining |
| Other Precious Metals & Mining | ind_otherpreciousmetalsmining |
| Paper & Paper Products | ind_paperpaperproducts |
| Silver | ind_silver |
| Specialty Chemicals | ind_specialtychemicals |
| Steel | ind_steel |
| Uranium | ind_uranium |
Communication Services
| Industry Name | Filter Code |
|---|---|
| Advertising Agencies | ind_advertisingagencies |
| Broadcasting | ind_broadcasting |
| Electronic Gaming & Multimedia | ind_electronicgamingmultimedia |
| Entertainment | ind_entertainment |
| Internet Content & Information | ind_internetcontentinformation |
| Publishing | ind_publishing |
| Telecom Services | ind_telecomservices |
Consumer Cyclical
| Industry Name | Filter Code |
|---|---|
| Apparel Manufacturing | ind_apparelmanufacturing |
| Apparel Retail | ind_apparelretail |
| Auto & Truck Dealerships | ind_autotruckdealerships |
| Department Stores | ind_departmentstores |
| Discount Stores | ind_discountstores |
| Footwear & Accessories | ind_footwearaccessories |
| Furnishings, Fixtures & Appliances | ind_furnishingsfixturesappliances |
| Gambling | ind_gambling |
| Home Improvement Retail | ind_homeimprovementretail |
| Internet Retail | ind_internetretail |
| Leisure | ind_leisure |
| Lodging | ind_lodging |
| Luxury Goods | ind_luxurygoods |
| Packaging & Containers | ind_packagingcontainers |
| Personal Services | ind_personalservices |
| Residential Construction | ind_residentialconstruction |
| Resorts & Casinos | ind_resortscasinos |
| Restaurants | ind_restaurants |
| Specialty Retail | ind_specialtyretail |
| Textile Manufacturing | ind_textilemanufacturing |
| Travel Services | ind_travelservices |
Consumer Defensive
| Industry Name | Filter Code |
|---|---|
| Beverages - Brewers | ind_beveragesbrewers |
| Beverages - Non-Alcoholic | ind_beveragesnonalcoholic |
| Beverages - Wineries & Distilleries | ind_beverageswineriesanddistilleries |
| Confectioners | ind_confectioners |
| Discount Stores | ind_discountstores |
| Education & Training Services | ind_educationtrainingservices |
| Farm Products | ind_farmproducts |
| Food Distribution | ind_fooddistribution |
| Grocery Stores | ind_grocerystores |
| Household & Personal Products | ind_householdpersonalproducts |
| Packaged Foods | ind_packagedfoods |
| Tobacco | ind_tobacco |
Energy
| Industry Name | Filter Code |
|---|---|
| Oil & Gas Drilling | ind_oilgasdrilling |
| Oil & Gas E&P | ind_oilgasep |
| Oil & Gas Equipment & Services | ind_oilgasequipmentservices |
| Oil & Gas Integrated | ind_oilgasintegrated |
| Oil & Gas Midstream | ind_oilgasmidstream |
| Oil & Gas Refining & Marketing | ind_oilgasrefiningmarketing |
| Thermal Coal | ind_thermalcoal |
| Uranium | ind_uranium |
Financial Services
| Industry Name | Filter Code |
|---|---|
| Asset Management | ind_assetmanagement |
| Banks - Diversified | ind_banksdiversified |
| Banks - Regional | ind_banksregional |
| Capital Markets | ind_capitalmarkets |
| Credit Services | ind_creditservices |
| Financial Conglomerates | ind_financialconglomerates |
| Financial Data & Stock Exchanges | ind_financialdatastockexchanges |
| Insurance - Diversified | ind_insurancediversified |
| Insurance - Life | ind_insurancelife |
| Insurance - Property & Casualty | ind_insurancepropertycasualty |
| Insurance - Reinsurance | ind_insurancereinsurance |
| Insurance - Specialty | ind_insurancespecialty |
| Insurance Brokers | ind_insurancebrokers |
| Mortgage Finance | ind_mortgagefinance |
| Shell Companies | ind_shellcompanies |
Healthcare
| Industry Name | Filter Code |
|---|---|
| Biotechnology | ind_biotechnology |
| Diagnostics & Research | ind_diagnosticsresearch |
| Drug Manufacturers - General | ind_drugmanufacturersgeneral |
| Drug Manufacturers - Specialty & Generic | ind_drugmanufacturersspecialtygeneric |
| Health Information Services | ind_healthinformationservices |
| Health Care Plans | ind_healthcareplans |
| Medical Care Facilities | ind_medicalcarefacilities |
| Medical Devices | ind_medicaldevices |
| Medical Distribution | ind_medicaldistribution |
| Medical Instruments & Supplies | ind_medicalinstrumentssupplies |
| Pharmaceutical Retailers | ind_pharmaceuticalretailers |
Industrials
| Industry Name | Filter Code |
|---|---|
| Aerospace & Defense | ind_aerospacedefense |
| Business Equipment & Supplies | ind_businessequipmentsupplies |
| Conglomerates | ind_conglomerates |
| Consulting Services | ind_consultingservices |
| Electrical Equipment & Parts | ind_electricalequipmentparts |
| Engineering & Construction | ind_engineeringconstruction |
| Farm & Heavy Construction Machinery | ind_farmheavyconstructionmachinery |
| Industrial Distribution | ind_industrialdistribution |
| Infrastructure Operations | ind_infrastructureoperations |
| Integrated Freight & Logistics | ind_integratedfreightlogistics |
| Metal Fabrication | ind_metalfabrication |
| Pollution & Treatment Controls | ind_pollutiontreatmentcontrols |
| Rental & Leasing Services | ind_rentalleasingservices |
| Security & Protection Services | ind_securityprotectionservices |
| Specialty Business Services | ind_specialtybusinessservices |
| Specialty Industrial Machinery | ind_specialtyindustrialmachinery |
| Staffing & Employment Services | ind_staffingemploymentservices |
| Tools & Accessories | ind_toolsaccessories |
| Waste Management | ind_wastemanagement |
Real Estate
| Industry Name | Filter Code |
|---|---|
| Real Estate - Development | ind_realestatedevelopment |
| Real Estate - Diversified | ind_realestatediversified |
| Real Estate Services | ind_realestateservices |
| REIT - Diversified | ind_reitdiversified |
| REIT - Healthcare Facilities | ind_reithealthcarefacilities |
| REIT - Hotel & Motel | ind_reithotelandmotel |
| REIT - Industrial | ind_reitindustrial |
| REIT - Mortgage | ind_reitmortgage |
| REIT - Office | ind_reitoffice |
| REIT - Residential | ind_reitresidential |
| REIT - Retail | ind_reitretail |
| REIT - Specialty | ind_reitspecialty |
Technology
| Industry Name | Filter Code |
|---|---|
| Communication Equipment | ind_communicationequipment |
| Computer Hardware | ind_computerhardware |
| Consumer Electronics | ind_consumerelectronics |
| Electronic Components | ind_electroniccomponents |
| Electronics & Computer Distribution | ind_electronicscomputerdistribution |
| Information Technology Services | ind_informationtechnologyservices |
| Scientific & Technical Instruments | ind_scientifictechnicalinstruments |
| Semiconductor Equipment & Materials | ind_semiconductorequipmentmaterials |
| Semiconductors | ind_semiconductors |
| Software - Application | ind_softwareapplication |
| Software - Infrastructure | ind_softwareinfrastructure |
| Solar | ind_solar |
Utilities
| Industry Name | Filter Code |
|---|---|
| Utilities - Diversified | ind_utilitiesdiversified |
| Utilities - Independent Power Producers | ind_utilitiesindependentpowerproducers |
| Utilities - Regulated Electric | ind_utilitiesregulatedelectric |
| Utilities - Regulated Gas | ind_utilitiesregulatedgas |
| Utilities - Regulated Water | ind_utilitiesregulatedwater |
| Utilities - Renewable | ind_utilitiesrenewable |
---
Complete Alphabetical Index
This flat list provides all industry filter codes in alphabetical order for quick lookup:
| # | Industry Name | Filter Code |
|---|---|---|
| 1 | Advertising Agencies | ind_advertisingagencies |
| 2 | Aerospace & Defense | ind_aerospacedefense |
| 3 | Airlines | ind_airlines |
| 4 | Aluminum | ind_aluminum |
| 5 | Apparel Manufacturing | ind_apparelmanufacturing |
| 6 | Apparel Retail | ind_apparelretail |
| 7 | Asset Management | ind_assetmanagement |
| 8 | Auto & Truck Dealerships | ind_autotruckdealerships |
| 9 | Auto Manufacturers | ind_automanufacturers |
| 10 | Auto Parts | ind_autoparts |
| 11 | Banks - Diversified | ind_banksdiversified |
| 12 | Banks - Regional | ind_banksregional |
| 13 | Beverages - Brewers | ind_beveragesbrewers |
| 14 | Beverages - Non-Alcoholic | ind_beveragesnonalcoholic |
| 15 | Beverages - Wineries & Distilleries | ind_beverageswineriesanddistilleries |
| 16 | Biotechnology | ind_biotechnology |
| 17 | Broadcasting | ind_broadcasting |
| 18 | Building Materials | ind_buildingmaterials |
| 19 | Business Equipment & Supplies | ind_businessequipmentsupplies |
| 20 | Capital Markets | ind_capitalmarkets |
| 21 | Chemicals | ind_chemicals |
| 22 | Coking Coal | ind_cokingcoal |
| 23 | Communication Equipment | ind_communicationequipment |
| 24 | Computer Hardware | ind_computerhardware |
| 25 | Confectioners | ind_confectioners |
| 26 | Conglomerates | ind_conglomerates |
| 27 | Consulting Services | ind_consultingservices |
| 28 | Consumer Electronics | ind_consumerelectronics |
| 29 | Copper | ind_copper |
| 30 | Credit Services | ind_creditservices |
| 31 | Department Stores | ind_departmentstores |
| 32 | Diagnostics & Research | ind_diagnosticsresearch |
| 33 | Discount Stores | ind_discountstores |
| 34 | Drug Manufacturers - General | ind_drugmanufacturersgeneral |
| 35 | Drug Manufacturers - Specialty & Generic | ind_drugmanufacturersspecialtygeneric |
| 36 | Education & Training Services | ind_educationtrainingservices |
| 37 | Electrical Equipment & Parts | ind_electricalequipmentparts |
| 38 | Electronic Components | ind_electroniccomponents |
| 39 | Electronic Gaming & Multimedia | ind_electronicgamingmultimedia |
| 40 | Electronics & Computer Distribution | ind_electronicscomputerdistribution |
| 41 | Engineering & Construction | ind_engineeringconstruction |
| 42 | Entertainment | ind_entertainment |
| 43 | Farm & Heavy Construction Machinery | ind_farmheavyconstructionmachinery |
| 44 | Farm Products | ind_farmproducts |
| 45 | Financial Conglomerates | ind_financialconglomerates |
| 46 | Financial Data & Stock Exchanges | ind_financialdatastockexchanges |
| 47 | Food Distribution | ind_fooddistribution |
| 48 | Footwear & Accessories | ind_footwearaccessories |
| 49 | Furnishings, Fixtures & Appliances | ind_furnishingsfixturesappliances |
| 50 | Gambling | ind_gambling |
| 51 | Gold | ind_gold |
| 52 | Grocery Stores | ind_grocerystores |
| 53 | Health Care Plans | ind_healthcareplans |
| 54 | Health Information Services | ind_healthinformationservices |
| 55 | Home Improvement Retail | ind_homeimprovementretail |
| 56 | Household & Personal Products | ind_householdpersonalproducts |
| 57 | Industrial Distribution | ind_industrialdistribution |
| 58 | Infrastructure Operations | ind_infrastructureoperations |
| 59 | Information Technology Services | ind_informationtechnologyservices |
| 60 | Insurance - Diversified | ind_insurancediversified |
| 61 | Insurance - Life | ind_insurancelife |
| 62 | Insurance - Property & Casualty | ind_insurancepropertycasualty |
| 63 | Insurance - Reinsurance | ind_insurancereinsurance |
| 64 | Insurance - Specialty | ind_insurancespecialty |
| 65 | Insurance Brokers | ind_insurancebrokers |
| 66 | Integrated Freight & Logistics | ind_integratedfreightlogistics |
| 67 | Internet Content & Information | ind_internetcontentinformation |
| 68 | Internet Retail | ind_internetretail |
| 69 | Leisure | ind_leisure |
| 70 | Lodging | ind_lodging |
| 71 | Lumber & Wood Production | ind_lumberwoodproduction |
| 72 | Luxury Goods | ind_luxurygoods |
| 73 | Marine Shipping | ind_marineshipping |
| 74 | Medical Care Facilities | ind_medicalcarefacilities |
| 75 | Medical Devices | ind_medicaldevices |
| 76 | Medical Distribution | ind_medicaldistribution |
| 77 | Medical Instruments & Supplies | ind_medicalinstrumentssupplies |
| 78 | Metal Fabrication | ind_metalfabrication |
| 79 | Mortgage Finance | ind_mortgagefinance |
| 80 | Oil & Gas Drilling | ind_oilgasdrilling |
| 81 | Oil & Gas E&P | ind_oilgasep |
| 82 | Oil & Gas Equipment & Services | ind_oilgasequipmentservices |
| 83 | Oil & Gas Integrated | ind_oilgasintegrated |
| 84 | Oil & Gas Midstream | ind_oilgasmidstream |
| 85 | Oil & Gas Refining & Marketing | ind_oilgasrefiningmarketing |
| 86 | Other Industrial Metals & Mining | ind_otherindustrialmetalsmining |
| 87 | Other Precious Metals & Mining | ind_otherpreciousmetalsmining |
| 88 | Packaged Foods | ind_packagedfoods |
| 89 | Packaging & Containers | ind_packagingcontainers |
| 90 | Paper & Paper Products | ind_paperpaperproducts |
| 91 | Personal Services | ind_personalservices |
| 92 | Pharmaceutical Retailers | ind_pharmaceuticalretailers |
| 93 | Pollution & Treatment Controls | ind_pollutiontreatmentcontrols |
| 94 | Publishing | ind_publishing |
| 95 | Railroads | ind_railroads |
| 96 | Real Estate - Development | ind_realestatedevelopment |
| 97 | Real Estate - Diversified | ind_realestatediversified |
| 98 | Real Estate Services | ind_realestateservices |
| 99 | Recreational Vehicles | ind_recreationalvehicles |
| 100 | REIT - Diversified | ind_reitdiversified |
| 101 | REIT - Healthcare Facilities | ind_reithealthcarefacilities |
| 102 | REIT - Hotel & Motel | ind_reithotelandmotel |
| 103 | REIT - Industrial | ind_reitindustrial |
| 104 | REIT - Mortgage | ind_reitmortgage |
| 105 | REIT - Office | ind_reitoffice |
| 106 | REIT - Residential | ind_reitresidential |
| 107 | REIT - Retail | ind_reitretail |
| 108 | REIT - Specialty | ind_reitspecialty |
| 109 | Rental & Leasing Services | ind_rentalleasingservices |
| 110 | Residential Construction | ind_residentialconstruction |
| 111 | Resorts & Casinos | ind_resortscasinos |
| 112 | Restaurants | ind_restaurants |
| 113 | Scientific & Technical Instruments | ind_scientifictechnicalinstruments |
| 114 | Security & Protection Services | ind_securityprotectionservices |
| 115 | Semiconductor Equipment & Materials | ind_semiconductorequipmentmaterials |
| 116 | Semiconductors | ind_semiconductors |
| 117 | Shell Companies | ind_shellcompanies |
| 118 | Silver | ind_silver |
| 119 | Software - Application | ind_softwareapplication |
| 120 | Software - Infrastructure | ind_softwareinfrastructure |
| 121 | Solar | ind_solar |
| 122 | Specialty Business Services | ind_specialtybusinessservices |
| 123 | Specialty Chemicals | ind_specialtychemicals |
| 124 | Specialty Industrial Machinery | ind_specialtyindustrialmachinery |
| 125 | Specialty Retail | ind_specialtyretail |
| 126 | Staffing & Employment Services | ind_staffingemploymentservices |
| 127 | Steel | ind_steel |
| 128 | Telecom Services | ind_telecomservices |
| 129 | Textile Manufacturing | ind_textilemanufacturing |
| 130 | Thermal Coal | ind_thermalcoal |
| 131 | Tobacco | ind_tobacco |
| 132 | Tools & Accessories | ind_toolsaccessories |
| 133 | Travel Services | ind_travelservices |
| 134 | Trucking | ind_trucking |
| 135 | Uranium | ind_uranium |
| 136 | Utilities - Diversified | ind_utilitiesdiversified |
| 137 | Utilities - Independent Power Producers | ind_utilitiesindependentpowerproducers |
| 138 | Utilities - Regulated Electric | ind_utilitiesregulatedelectric |
| 139 | Utilities - Regulated Gas | ind_utilitiesregulatedgas |
| 140 | Utilities - Regulated Water | ind_utilitiesregulatedwater |
| 141 | Utilities - Renewable | ind_utilitiesrenewable |
| 142 | Waste Management | ind_wastemanagement |
Total: 142 industries
---
Notes
1. Filter code derivation: Remove spaces, hyphens, ampersands (&), and other special characters from the industry name, then lowercase the result. Prefix with ind_.
2. Cross-sector industries: Some industries (e.g., Uranium) appear under multiple FINVIZ sectors (Energy and Basic Materials). The filter code is the same regardless of which sector view you navigate from.
3. Industry changes: FINVIZ occasionally adds, removes, or renames industries. This list was compiled as of February 2026. If a filter code returns no results, verify the industry name on FINVIZ directly.
4. Combining filters: Multiple industry filters can be combined in FINVIZ URLs using comma separation:
f=ind_semiconductors,ind_softwareapplication,cap_smallover5. Market cap filter: For theme detection, always combine with cap_smallover (market cap > $300M) to filter out micro-caps that may distort aggregated metrics.
6. Special cases:
- "E&P" becomes "ep" (ampersand removed)
- "REIT - " prefix becomes "reit" (hyphen and spaces removed)
- "Wineries & Distilleries" becomes "wineriesanddistilleries" (ampersand becomes "and" in this case)
- Verify edge cases against FINVIZ directly if results seem incorrect
Thematic ETF Catalog
This catalog maps market themes to their associated ETFs. The ETF count per theme is used for the etf_proliferation_score in lifecycle maturity assessment.
ETF Proliferation Scoring:
| ETF Count | Score |
|---|---|
| 0 | 0 |
| 1 | 20 |
| 2-3 | 40 |
| 4-6 | 60 |
| 7-10 | 80 |
| >10 | 100 |
Higher ETF proliferation indicates more retail/institutional interest and a potentially more mature (crowded) theme.
---
AI & Semiconductors
ETF Count: 11 | Proliferation Score: 100
| Ticker | Name | Focus | AUM Tier |
|---|---|---|---|
| SMH | VanEck Semiconductor ETF | Semiconductor equities | Large |
| SOXX | iShares Semiconductor ETF | Semiconductor equities | Large |
| SOXQ | Invesco PHLX Semiconductor ETF | Semiconductor equities | Medium |
| AIQ | Global X Artificial Intelligence & Technology ETF | AI & big data | Medium |
| BOTZ | Global X Robotics & Artificial Intelligence ETF | AI & robotics | Medium |
| ROBT | First Trust Nasdaq Artificial Intelligence & Robotics ETF | AI & robotics | Small |
| IRBO | iShares Robotics and Artificial Intelligence Multisector ETF | AI multi-sector | Small |
| CHAT | Roundhill Generative AI & Technology ETF | Generative AI | Small |
| WTAI | WisdomTree Artificial Intelligence and Innovation Fund | AI innovation | Small |
| IGPT | North Shore AI Powered Large Cap Growth ETF | AI large-cap | Small |
| AIVI | WisdomTree International AI Enhanced Value Fund | AI value | Small |
---
Clean Energy & EV
ETF Count: 9 | Proliferation Score: 80
| Ticker | Name | Focus | AUM Tier |
|---|---|---|---|
| ICLN | iShares Global Clean Energy ETF | Global clean energy | Large |
| QCLN | First Trust NASDAQ Clean Edge Green Energy ETF | Clean energy technology | Medium |
| TAN | Invesco Solar ETF | Solar energy | Medium |
| FAN | First Trust Global Wind Energy ETF | Wind energy | Small |
| LIT | Global X Lithium & Battery Tech ETF | Lithium & batteries | Medium |
| DRIV | Global X Autonomous & Electric Vehicles ETF | EVs & autonomous driving | Medium |
| IDRV | iShares Self-Driving EV and Tech ETF | EVs & self-driving tech | Small |
| ACES | ALPS Clean Energy ETF | US clean energy | Small |
| PBW | Invesco WilderHill Clean Energy ETF | Clean energy innovation | Small |
---
Cybersecurity
ETF Count: 4 | Proliferation Score: 60
| Ticker | Name | Focus | AUM Tier |
|---|---|---|---|
| CIBR | First Trust NASDAQ Cybersecurity ETF | Cybersecurity equities | Large |
| HACK | ETFMG Prime Cyber Security ETF | Cybersecurity equities | Medium |
| BUG | Global X Cybersecurity ETF | Cybersecurity equities | Small |
| IHAK | iShares Cybersecurity and Tech ETF | Cybersecurity & tech | Small |
---
Cloud Computing & SaaS
ETF Count: 4 | Proliferation Score: 60
| Ticker | Name | Focus | AUM Tier |
|---|---|---|---|
| SKYY | First Trust Cloud Computing ETF | Cloud computing | Medium |
| WCLD | WisdomTree Cloud Computing Fund | Cloud software | Small |
| CLOU | Global X Cloud Computing ETF | Cloud computing | Small |
| CLDL | Direxion Daily Cloud Computing Bull 2X Shares | Cloud computing (leveraged) | Small |
---
Biotech & Genomics
ETF Count: 6 | Proliferation Score: 60
| Ticker | Name | Focus | AUM Tier |
|---|---|---|---|
| XBI | SPDR S&P Biotech ETF | Biotech equities (equal-weight) | Large |
| IBB | iShares Biotechnology ETF | Biotech equities (cap-weight) | Large |
| ARKG | ARK Genomic Revolution ETF | Genomics & biotech | Medium |
| GNOM | Global X Genomics & Biotechnology ETF | Genomics | Small |
| IDNA | iShares Genomics Immunology and Healthcare ETF | Genomics & immunology | Small |
| LABU | Direxion Daily S&P Biotech Bull 3X Shares | Biotech (leveraged) | Medium |
---
Infrastructure & Construction
ETF Count: 3 | Proliferation Score: 40
| Ticker | Name | Focus | AUM Tier |
|---|---|---|---|
| PAVE | Global X U.S. Infrastructure Development ETF | US infrastructure | Large |
| IFRA | iShares U.S. Infrastructure ETF | US infrastructure equities | Medium |
| SIMS | SPDR S&P Kensho Intelligent Structures ETF | Smart infrastructure | Small |
---
Cannabis
ETF Count: 3 | Proliferation Score: 40
| Ticker | Name | Focus | AUM Tier |
|---|---|---|---|
| MSOS | AdvisorShares Pure US Cannabis ETF | US cannabis operators | Medium |
| MJ | ETFMG Alternative Harvest ETF | Global cannabis | Small |
| THCX | The Cannabis ETF | Cannabis equities | Small |
---
Blockchain & Crypto
ETF Count: 5 | Proliferation Score: 60
| Ticker | Name | Focus | AUM Tier |
|---|---|---|---|
| BITO | ProShares Bitcoin Strategy ETF | Bitcoin futures | Large |
| BLOK | Amplify Transformational Data Sharing ETF | Blockchain companies | Medium |
| BITQ | Bitwise Crypto Industry Innovators ETF | Crypto industry equities | Small |
| DAPP | VanEck Digital Transformation ETF | Digital assets ecosystem | Small |
| IBIT | iShares Bitcoin Trust ETF | Spot Bitcoin | Large |
---
Defense & Aerospace
ETF Count: 5 | Proliferation Score: 60
| Ticker | Name | Focus | AUM Tier |
|---|---|---|---|
| ITA | iShares U.S. Aerospace & Defense ETF | Aerospace & defense equities | Large |
| PPA | Invesco Aerospace & Defense ETF | Aerospace & defense equities | Medium |
| ROKT | SPDR S&P Kensho Final Frontiers ETF | Space & deep sea exploration | Small |
| UFO | Procure Space ETF | Space industry | Small |
| ARKX | ARK Space Exploration & Innovation ETF | Space exploration & innovation | Small |
---
Obesity & GLP-1
ETF Count: 2 | Proliferation Score: 40
| Ticker | Name | Focus | AUM Tier |
|---|---|---|---|
| SLIM | Themes Obesity & Cardiometabolic ETF | Obesity & cardiometabolic drugs | Small |
| HRTS | Tema Cardiovascular and Metabolic ETF | Cardiovascular & metabolic health | Small |
---
Nuclear Energy
ETF Count: 3 | Proliferation Score: 40
| Ticker | Name | Focus | AUM Tier |
|---|---|---|---|
| NLR | VanEck Uranium+Nuclear Energy ETF | Uranium & nuclear energy | Medium |
| NUKZ | Range Nuclear Renaissance Index ETF | Nuclear renaissance | Small |
| NUCL | Horizons Global Uranium Index ETF | Global uranium | Small |
---
Water Resources
ETF Count: 3 | Proliferation Score: 40
| Ticker | Name | Focus | AUM Tier |
|---|---|---|---|
| PHO | Invesco Water Resources ETF | Water utilities & infrastructure | Medium |
| FIW | First Trust Water ETF | Water industry | Medium |
| CGW | Invesco S&P Global Water Index ETF | Global water equities | Small |
---
Robotics & Automation
ETF Count: 3 | Proliferation Score: 40
| Ticker | Name | Focus | AUM Tier |
|---|---|---|---|
| ROBO | Robo Global Robotics and Automation Index ETF | Robotics & automation | Medium |
| ARKQ | ARK Autonomous Technology & Robotics ETF | Autonomous tech & robotics | Medium |
| IRBO | iShares Robotics and Artificial Intelligence Multisector ETF | Robotics & AI (also in AI theme) | Small |
---
Lithium & Battery Technology
ETF Count: 2 | Proliferation Score: 40
| Ticker | Name | Focus | AUM Tier |
|---|---|---|---|
| LIT | Global X Lithium & Battery Tech ETF | Lithium mining & battery tech (also in Clean Energy) | Medium |
| BATT | Amplify Lithium & Battery Technology ETF | Battery technology value chain | Small |
---
Gold & Precious Metals
ETF Count: 5 | Proliferation Score: 60
| Ticker | Name | Focus | AUM Tier |
|---|---|---|---|
| GDX | VanEck Gold Miners ETF | Gold mining equities | Large |
| GDXJ | VanEck Junior Gold Miners ETF | Junior gold miners | Large |
| RING | iShares MSCI Global Gold Miners ETF | Global gold miners | Medium |
| SGDM | Sprott Gold Miners ETF | Gold miners (rules-based) | Small |
| SIL | Global X Silver Miners ETF | Silver miners | Medium |
---
Uranium
ETF Count: 3 | Proliferation Score: 40
| Ticker | Name | Focus | AUM Tier |
|---|---|---|---|
| URA | Global X Uranium ETF | Uranium mining & nuclear components | Medium |
| URNM | Sprott Uranium Miners ETF | Pure-play uranium miners | Medium |
| URNJ | Sprott Junior Uranium Miners ETF | Junior uranium miners | Small |
---
Oil & Gas (Energy)
ETF Count: 6 | Proliferation Score: 60
| Ticker | Name | Focus | AUM Tier |
|---|---|---|---|
| XLE | Energy Select Sector SPDR Fund | US energy sector | Large |
| XOP | SPDR S&P Oil & Gas Exploration & Production ETF | Oil & gas E&P | Large |
| OIH | VanEck Oil Services ETF | Oil services equities | Medium |
| AMLP | Alerian MLP ETF | Energy MLPs | Medium |
| FCG | First Trust Natural Gas ETF | Natural gas equities | Small |
| CRAK | VanEck Oil Refiners ETF | Oil refiners | Small |
---
Financial Services & Banks
ETF Count: 5 | Proliferation Score: 60
| Ticker | Name | Focus | AUM Tier |
|---|---|---|---|
| XLF | Financial Select Sector SPDR Fund | US financial sector | Large |
| KBE | SPDR S&P Bank ETF | US bank equities | Medium |
| KRE | SPDR S&P Regional Banking ETF | Regional bank equities | Large |
| IAI | iShares U.S. Broker-Dealers & Securities Exchanges ETF | Broker-dealers | Small |
| KBWB | Invesco KBW Bank ETF | Large-cap bank equities | Medium |
---
Healthcare & Pharma
ETF Count: 5 | Proliferation Score: 60
| Ticker | Name | Focus | AUM Tier |
|---|---|---|---|
| XLV | Health Care Select Sector SPDR Fund | US healthcare sector | Large |
| IHE | iShares U.S. Pharmaceuticals ETF | US pharmaceutical equities | Medium |
| XHS | SPDR S&P Health Care Services ETF | Healthcare services | Small |
| IHI | iShares U.S. Medical Devices ETF | Medical devices | Medium |
| PPH | VanEck Pharmaceutical ETF | Pharmaceutical equities | Small |
---
Real Estate & REITs
ETF Count: 4 | Proliferation Score: 60
| Ticker | Name | Focus | AUM Tier |
|---|---|---|---|
| VNQ | Vanguard Real Estate ETF | US REITs | Large |
| XLRE | Real Estate Select Sector SPDR Fund | US real estate sector | Large |
| IYR | iShares U.S. Real Estate ETF | US real estate equities | Large |
| MORT | VanEck Mortgage REIT Income ETF | Mortgage REITs | Small |
---
Retail & Consumer
ETF Count: 4 | Proliferation Score: 60
| Ticker | Name | Focus | AUM Tier |
|---|---|---|---|
| XLY | Consumer Discretionary Select Sector SPDR Fund | Consumer discretionary | Large |
| XRT | SPDR S&P Retail ETF | Retail equities | Medium |
| XLP | Consumer Staples Select Sector SPDR Fund | Consumer staples | Large |
| IBUY | Amplify Online Retail ETF | Online retail equities | Small |
---
Summary Table
| Theme | ETF Count | Proliferation Score |
|---|---|---|
| AI & Semiconductors | 11 | 100 |
| Clean Energy & EV | 9 | 80 |
| Biotech & Genomics | 6 | 60 |
| Oil & Gas (Energy) | 6 | 60 |
| Gold & Precious Metals | 5 | 60 |
| Defense & Aerospace | 5 | 60 |
| Blockchain & Crypto | 5 | 60 |
| Financial Services & Banks | 5 | 60 |
| Healthcare & Pharma | 5 | 60 |
| Cloud Computing & SaaS | 4 | 60 |
| Cybersecurity | 4 | 60 |
| Real Estate & REITs | 4 | 60 |
| Retail & Consumer | 4 | 60 |
| Infrastructure & Construction | 3 | 40 |
| Cannabis | 3 | 40 |
| Nuclear Energy | 3 | 40 |
| Water Resources | 3 | 40 |
| Robotics & Automation | 3 | 40 |
| Uranium | 3 | 40 |
| Obesity & GLP-1 | 2 | 40 |
| Lithium & Battery Technology | 2 | 40 |
Notes:
- Some ETFs appear in multiple themes (e.g., LIT in both Clean Energy and Lithium, IRBO in both AI and Robotics)
- AUM Tier: Large (>$1B), Medium ($100M-$1B), Small (<$100M)
- Leveraged ETFs (LABU, CLDL) included for completeness but not recommended for theme exposure measurement
- Catalog should be updated quarterly to capture new ETF launches and closures
Theme Detection Methodology
Overview
The Theme Detector uses a 3-Dimensional Scoring Model to identify, rank, and assess market themes. Unlike single-score ranking systems, this approach separates the intensity of a theme (heat), its maturity stage (lifecycle), and the reliability of the signal (confidence) into independent dimensions.
---
Dimension 1: Theme Heat (0-100)
Theme Heat measures the direction-neutral strength of a theme. A high heat score means the theme is generating significant market activity, regardless of whether it is bullish or bearish.
Components
1.1 Momentum Strength (weight: 35%)
Measures the direction-neutral momentum strength using a log-sigmoid function on the absolute weighted return.
Formula:
weighted_return = perf_1w * 0.10 + perf_1m * 0.25 + perf_3m * 0.35 + perf_6m * 0.30
momentum_score = 100 / (1 + exp(-2.0 * (ln(1 + |weighted_return|) - ln(16))))Midpoint at |15%| weighted return. Log transform compresses extreme values for better mid-range separation. Absolute value is used so both strong bullish and strong bearish moves generate high heat.
Data Source: FINVIZ industry performance (1W, 1M, 3M, 6M change %)
Example Scores (continuous, not stepped): | |Weighted Return| | Score | |---------------------|-------| | 0% | ~3 | | 5% | ~27 | | 15% | 50 (midpoint) | | 30% | ~73 | | 50% | ~86 |
1.2 Volume Intensity (weight: 20%)
Measures abnormal volume using sqrt scaling on the 20-day/60-day volume ratio.
Formula:
ratio = vol_20d / vol_60d
volume_score = min(100, sqrt(max(0, ratio - 0.8)) / sqrt(1.2) * 100)Returns 50.0 if either volume input is None or zero.
Data Source: FINVIZ relative volume (volume / avg volume ratio)
Example Scores:
| Volume Ratio | Score |
|---|---|
| 1.0 | ~37 |
| 1.2 | ~58 |
| 1.5 | ~76 |
| 2.0 | 100 (ceiling) |
1.3 Uptrend Signal (weight: 25%)
Continuous score from sector uptrend data with base + bonus structure.
Formula per sector:
base = min(80, ratio * 100) # continuous 0-80
ma_bonus = 10 if ratio > ma_10 # MA above bonus
slope_bonus = 10 if slope > 0 # positive slope bonus
sector_score = base + ma_bonus + slope_bonus # 0-100Final score = weighted average of sector scores. For bearish themes, result is inverted (100 - score).
Data Source: uptrend-dashboard output (ratio, ma_10, slope per sector)
Returns 50.0 if no sector data available.
1.4 Breadth Signal (weight: 20%)
Measures directionally-aligned industry participation using a power curve with industry count bonus.
Formula:
breadth_score = min(100, ratio^2.5 * 80 + count_bonus)
count_bonus = min(20, industry_count * 2)The ratio is the fraction of matched industries with directionally-aligned weighted returns (positive for LEAD themes, negative for LAG themes). Power curve (exponent 2.5) suppresses low ratios and amplifies high ones.
Data Source: Industry-level weighted returns (not stock-level)
Example Scores (no count bonus):
| Breadth Ratio | Score |
|---|---|
| 0.5 | ~14 |
| 0.7 | ~33 |
| 0.9 | ~61 |
| 1.0 | 80 |
Returns 50.0 if ratio is None.
Theme Heat Composite
theme_heat = (momentum * 0.35) + (volume * 0.20) + (uptrend * 0.25) + (breadth * 0.20)Any None sub-score defaults to 50.0. Result clamped to 0-100.
---
Dimension 2: Lifecycle Maturity
Lifecycle assessment classifies a theme into one of five stages: Emerging, Accelerating, Trending, Mature, or Exhausting. This is critical for distinguishing emerging opportunities from crowded trades.
Components
2.1 Duration Score (weight: 25%)
How long the theme has been active (consecutive weeks of elevated heat).
Measurement: Count weeks where theme_heat >= 40.
| Duration | Stage Signal |
|---|---|
| 1-3 weeks | Emerging |
| 4-8 weeks | Accelerating |
| 9-12 weeks | Trending |
| 13-16 weeks | Mature |
| > 16 weeks | Exhausting |
Limitation: Duration tracking requires historical data. On first run, duration defaults to "Unknown" and lifecycle uses other factors only.
2.2 Extremity Clustering Score (weight: 25%)
Percentage of stocks in the theme near 52-week highs or lows.
Formula:
extremity_pct = count(within_5pct_of_52wk_high_or_low) / count(total_stocks)| Extremity % | Stage Signal |
|---|---|
| < 15% | Emerging |
| 15-30% | Accelerating |
| 30-45% | Trending |
| 45-60% | Mature |
| > 60% | Exhausting |
Data Source: FINVIZ 52-week high/low data
2.3 Price Extreme Saturation Score (weight: 25%)
Proportion of stocks near 52-week extremes (within 5%).
Formula:
bullish: pct = count(dist_from_52w_high <= 0.05) / total
bearish: pct = count(dist_from_52w_low <= 0.05) / total
score = min(100, pct * 200)2.4 Valuation Score (weight: 15%)
Average P/E ratio of theme constituents relative to S&P 500 P/E.
Formula:
relative_pe = avg_theme_pe / sp500_pe| Relative P/E | Stage Signal |
|---|---|
| < 0.8 | Emerging (undervalued) |
| 0.8-1.0 | Accelerating (fair value) |
| 1.0-1.4 | Trending (slightly elevated) |
| 1.4-2.0 | Mature (overvalued) |
| > 2.0 | Exhausting (extreme) |
Data Source: FMP API for P/E ratios (optional; uses FINVIZ forward P/E as fallback)
2.5 ETF Proliferation Score (weight: 10%)
Number of thematic ETFs tracking the theme. More ETFs indicate greater retail/institutional attention.
Source: thematic_etf_catalog.md (static reference)
| ETF Count | Score | Stage Signal |
|---|---|---|
| 0 | 0 | Emerging |
| 1 | 20 | Emerging |
| 2-3 | 40 | Accelerating |
| 4-6 | 60 | Trending |
| 7-10 | 80 | Mature |
| > 10 | 100 | Exhausting |
Lifecycle Maturity Composite
maturity = (duration * 0.25) + (extremity * 0.25) + (price_extreme * 0.25) + (valuation * 0.15) + (etf_proliferation * 0.10)Lifecycle Stage Classification
The lifecycle stage is classified from the maturity score:
| Maturity Score | Stage |
|---|---|
| 0-20 | Emerging |
| 20-40 | Accelerating |
| 40-60 | Trending |
| 60-80 | Mature |
| 80-100 | Exhausting |
Note: Media/Narrative Saturation is not included in the automated maturity calculation. Claude's WebSearch narrative confirmation can be used to qualitatively adjust the lifecycle assessment.
---
Dimension 3: Confidence (Low / Medium / High)
Confidence measures how reliable the theme detection is, based on data quality and confirmation signals.
Layers
3.1 Quantitative Layer (base)
Based on data breadth and consistency:
| Condition | Level |
|---|---|
| >= 4 industries matching, >= 20 stocks analyzed | High |
| 2-3 industries matching, >= 10 stocks analyzed | Medium |
| 1 industry matching or < 10 stocks | Low |
3.2 Breadth Layer (modifier)
Cross-sector participation adds confidence:
| Condition | Modifier |
|---|---|
| Theme spans 3+ sectors | +1 level (cap at High) |
| Theme spans 2 sectors | No change |
| Theme in 1 sector only | -1 level (floor at Low) |
3.3 Narrative Layer (modifier, applied in Step 4)
WebSearch confirmation adjusts confidence:
| Narrative Finding | Modifier |
|---|---|
| Strong confirmation (multiple sources, clear catalysts) | +1 level |
| Mixed signals | No change |
| Contradictory narrative (bearish articles for bullish theme) | -1 level |
Final Confidence
confidence = apply_modifiers(quantitative_base, breadth_modifier, narrative_modifier)
confidence = clamp(confidence, Low, High)---
Direction Detection
Theme direction (leading vs. lagging) is determined by relative rank, not absolute price change:
Algorithm
1. Each industry gets a rank_direction based on its position in the momentum-ranked list: top half = "bullish" (leading), bottom half = "bearish" (lagging). 2. Theme direction is the majority vote of its constituent industries' rank_direction.
# Industry-level (industry_ranker.py)
rank_direction = "bullish" if rank <= len(industries) // 2 else "bearish"
# Theme-level (theme_classifier.py)
direction = majority_vote([ind.rank_direction for ind in theme.industries])Important: Relative, Not Absolute
LEAD/LAG direction is relative. A "lagging" theme may still have positive absolute returns — it simply underperforms relative to other themes. This means:
- LEAD themes: Suitable for overweight / new positions
- LAG themes: Candidates for underweight reduction, not short signals
---
Data Sources
Primary: FINVIZ
Elite Mode (recommended):
- CSV export endpoint:
https://elite.finviz.com/export.ashx?v=151&f=ind_{code},cap_smallover,...&auth=KEY - Provides: ticker, company, sector, industry, market cap, P/E, change%, volume, avg volume, 52wk high/low, RSI, SMA20/50/200
- Rate limit: 0.5s between requests
- Coverage: Full stock universe per industry
Public Mode (fallback):
- HTML scraping:
https://finviz.com/screener.ashx?v=151&f=ind_{code},cap_smallover - Provides: Same fields but limited to page 1 (~20 stocks per industry)
- Rate limit: 2.0s between requests (aggressive scraping may trigger blocks)
- Coverage: Top 20 stocks per industry by market cap
Secondary: FMP API (optional)
- P/E ratios for valuation analysis
- Not required; FINVIZ forward P/E used as fallback
- Useful for more granular valuation metrics
Tertiary: uptrend-dashboard (optional)
- CSV output from the uptrend-dashboard skill
- Provides 3-point technical evaluation per stock
- Significantly improves uptrend ratio accuracy
- If unavailable, FINVIZ price-vs-SMA200 is used as proxy
Quaternary: WebSearch (narrative layer)
- Used for narrative confirmation in Step 4
- Not automated; Claude performs searches during workflow
- Subjective assessment of media coverage and analyst sentiment
---
Integration with uptrend-dashboard
When uptrend-dashboard data is available, the theme detector uses it for enhanced 3-point evaluation:
3-Point Evaluation Criteria: 1. Price > 50-day SMA (short-term trend) 2. 50-day SMA > 200-day SMA (medium-term trend, golden/death cross) 3. 200-day SMA is rising (long-term trend confirmation)
Stocks meeting all 3 points = in confirmed uptrend Stocks meeting 0 points = in confirmed downtrend
This provides more accurate uptrend ratios than simple price-above-SMA200 proxy.
---
Output Schema
JSON Output Structure
{
"metadata": {
"date": "2026-02-16",
"mode": "elite",
"themes_analyzed": 14,
"industries_scanned": 145,
"total_stocks": 5200,
"uptrend_data_available": true,
"execution_time_seconds": 150
},
"themes": [
{
"name": "AI & Semiconductors",
"direction": "Bullish",
"direction_strength": "Strong",
"theme_heat": 82,
"heat_components": {
"momentum": 90,
"volume": 75,
"uptrend": 85,
"breadth": 70
},
"lifecycle": {
"stage": "Mature",
"duration_weeks": 12,
"extremity_pct": 45,
"relative_pe": 1.8,
"etf_proliferation_score": 100,
"etf_count": 11
},
"confidence": "High",
"confidence_components": {
"quantitative": "High",
"breadth_modifier": "+1",
"narrative_modifier": "pending"
},
"industries": [
{
"name": "Semiconductors",
"change_pct": 4.2,
"avg_relative_volume": 1.8,
"uptrend_ratio": 0.75,
"stock_count": 35
}
],
"top_stocks": [
{"ticker": "NVDA", "change_pct": 6.5, "relative_volume": 2.1},
{"ticker": "AVGO", "change_pct": 4.8, "relative_volume": 1.9}
],
"proxy_etfs": ["SMH", "SOXX", "AIQ", "BOTZ"]
}
],
"industry_rankings": {
"top_10": [...],
"bottom_10": [...]
},
"sector_summary": {
"Technology": {"uptrend_ratio": 0.65, "avg_change_pct": 2.1},
"Energy": {"uptrend_ratio": 0.40, "avg_change_pct": -1.5}
}
}---
Known Limitations
1. Static theme definitions: Cross-sector themes are predefined in cross_sector_themes.md. New themes (e.g., a sudden meme-stock theme) are not automatically detected.
2. Industry granularity: FINVIZ industry classification may not perfectly map to investment themes. Some industries span multiple themes.
3. Single-stock dominance: Large-cap stocks (e.g., NVDA in AI) can skew theme-level metrics. Market-cap weighting amplifies this effect.
4. Temporal lag: Weekly performance data does not capture intraday or same-day momentum shifts.
5. ETF catalog staleness: The thematic ETF catalog is manually maintained and may not reflect recent ETF launches or closures.
6. Public mode data limits: Only ~20 stocks per industry are captured, which may underrepresent small/mid-cap participation.
7. Duration tracking: First-run analysis cannot assess theme duration without historical baseline data.
8. Narrative subjectivity: Confidence adjustment from WebSearch is inherently subjective and depends on search result quality.
9. Survivorship bias: Analysis only covers currently listed stocks and active ETFs, missing delisted or closed instruments.
10. FINVIZ data delays: Public FINVIZ data is 15-minute delayed; Elite provides real-time during market hours.
# Theme Detector calculators package
"""
Theme Heat Calculator (0-100)
ThemeHeat = momentum_strength * 0.35
+ volume_intensity * 0.20
+ uptrend_signal * 0.25
+ breadth_signal * 0.20
All sub-scores are direction-neutral (0-100 for "strength").
Formulas are designed to spread scores across the full 0-100 range,
avoiding ceiling effects that compress most themes into 80+.
"""
import math
from typing import Optional
HEAT_WEIGHTS = {
"momentum": 0.35,
"volume": 0.20,
"uptrend": 0.25,
"breadth": 0.20,
}
def momentum_strength_score(weighted_return_pct: float) -> float:
"""Log-sigmoid score based on absolute weighted return.
Formula: 100 / (1 + exp(-2.0 * (ln(1 + |wr|) - ln(16))))
Midpoint at |wr| = 15% (typical strong industry weighted return).
Log transform compresses extreme values for better mid-range separation.
Examples:
|0%| -> ~3
|5%| -> ~27
|15%| -> 50 (midpoint)
|30%| -> ~73
|50%| -> ~86
"""
x = abs(weighted_return_pct)
log_x = math.log(1.0 + x)
log_mid = math.log(16.0)
return 100.0 / (1.0 + math.exp(-2.0 * (log_x - log_mid)))
def volume_intensity_score(vol_20d: Optional[float], vol_60d: Optional[float]) -> float:
"""Score based on short-term vs long-term volume ratio using sqrt scaling.
Formula: min(100, sqrt(max(0, ratio - 0.8)) / sqrt(1.2) * 100)
Ceiling at ratio=2.0 instead of 1.2, with better mid-range separation.
Examples:
ratio=1.0 -> ~37
ratio=1.2 -> ~58
ratio=1.5 -> ~76
ratio=2.0 -> 100
Returns 50.0 if either input is None or vol_60d == 0.
"""
if vol_20d is None or vol_60d is None or vol_60d == 0:
return 50.0
ratio = vol_20d / vol_60d
raw = max(0.0, ratio - 0.8)
return min(100.0, math.sqrt(raw) / math.sqrt(1.2) * 100.0)
def uptrend_signal_score(sector_data: list[dict], is_bearish: bool) -> float:
"""Continuous score from sector uptrend data.
Each sector entry: {"sector", "ratio", "ma_10", "slope", "weight"}
Scoring per sector:
base = min(80, ratio * 100) # continuous 0-80
+10 if ratio > ma_10 # MA above bonus
+10 if slope > 0 # positive slope bonus
Total: 0-100 continuous
If is_bearish: result = 100 - weighted_average
Returns 50.0 if empty.
"""
if not sector_data:
return 50.0
total_weight = 0.0
weighted_sum = 0.0
for entry in sector_data:
ratio = entry.get("ratio") or 0
ma_10 = entry.get("ma_10") or 0
slope = entry.get("slope") or 0
weight = entry.get("weight") or 1.0
base = min(80.0, ratio * 100.0)
ma_bonus = 10.0 if ratio > ma_10 else 0.0
slope_bonus = 10.0 if slope > 0 else 0.0
direction_score = base + ma_bonus + slope_bonus
weighted_sum += direction_score * weight
total_weight += weight
if total_weight == 0:
return 50.0
result = weighted_sum / total_weight
if is_bearish:
result = 100.0 - result
return result
def breadth_signal_score(positive_ratio: Optional[float], industry_count: int = 0) -> float:
"""Score based on breadth ratio (0-1) with power curve and industry count bonus.
Formula: min(100, ratio^2.5 * 80 + count_bonus)
count_bonus = min(20, industry_count * 2)
Power curve (exponent 2.5) suppresses low ratios and amplifies high ones,
creating better separation in the 0.5-1.0 range.
Examples (no bonus):
ratio=0.5 -> ~14
ratio=0.7 -> ~33
ratio=0.9 -> ~61
ratio=1.0 -> 80
Returns 50.0 if None.
"""
if positive_ratio is None:
return 50.0
count_bonus = min(20.0, industry_count * 2.0)
raw = math.pow(max(0.0, positive_ratio), 2.5) * 80.0 + count_bonus
return min(100.0, max(0.0, raw))
def calculate_theme_heat(
momentum: Optional[float],
volume: Optional[float],
uptrend: Optional[float],
breadth: Optional[float],
) -> float:
"""Weighted sum of sub-scores, clamped 0-100.
Any None input defaults to 50.0.
"""
m = momentum if momentum is not None else 50.0
v = volume if volume is not None else 50.0
u = uptrend if uptrend is not None else 50.0
b = breadth if breadth is not None else 50.0
raw = (
m * HEAT_WEIGHTS["momentum"]
+ v * HEAT_WEIGHTS["volume"]
+ u * HEAT_WEIGHTS["uptrend"]
+ b * HEAT_WEIGHTS["breadth"]
)
return float(min(100.0, max(0.0, raw)))
#!/usr/bin/env python3
"""
Industry Ranker - Momentum scoring and ranking for industries.
Calculates direction-neutral momentum strength using a sigmoid function,
then ranks industries by weighted multi-timeframe returns.
Timeframe Weights:
- 1W: 10%
- 1M: 25%
- 3M: 35%
- 6M: 30%
Total: 100%
"""
import math
TIMEFRAME_WEIGHTS: dict[str, float] = {
"perf_1w": 0.10,
"perf_1m": 0.25,
"perf_3m": 0.35,
"perf_6m": 0.30,
}
def momentum_strength_score(weighted_return_pct: float) -> float:
"""
Direction-neutral sigmoid momentum score.
Formula: 100 / (1 + exp(-0.15 * (abs(weighted_return) - 5.0)))
Returns a score in [0, 100] where:
|0%| -> ~32
|5%| -> 50 (midpoint)
|10%| -> ~68
|15%| -> ~82
|20%| -> ~90
"""
x = abs(weighted_return_pct)
return 100.0 / (1.0 + math.exp(-0.15 * (x - 5.0)))
def rank_industries(industries: list[dict]) -> list[dict]:
"""
Rank industries by momentum strength score.
Each input dict must have keys: name, perf_1w, perf_1m, perf_3m, perf_6m.
Additional keys are preserved.
Adds fields: weighted_return, momentum_score, direction, rank.
Returns list sorted by momentum_score descending.
"""
if not industries:
return []
scored = []
for ind in industries:
weighted_return = sum(
ind.get(key, 0.0) * weight for key, weight in TIMEFRAME_WEIGHTS.items()
)
score = momentum_strength_score(weighted_return)
direction = "bullish" if weighted_return > 0 else "bearish"
entry = dict(ind)
entry["weighted_return"] = round(weighted_return, 4)
entry["momentum_score"] = round(score, 2)
entry["direction"] = direction
scored.append(entry)
scored.sort(key=lambda x: x["momentum_score"], reverse=True)
mid = len(scored) // 2
for i, entry in enumerate(scored, start=1):
entry["rank"] = i
entry["rank_direction"] = "bullish" if i <= mid else "bearish"
return scored
def get_top_bottom_industries(ranked: list[dict], n: int = 5) -> dict[str, list[dict]]:
"""
Extract top N and bottom N industries from a ranked list.
Args:
ranked: List sorted by momentum_score descending (output of rank_industries).
n: Number of industries for each group.
Returns:
{"top": [...], "bottom": [...]}
"""
if not ranked:
return {"top": [], "bottom": []}
top = ranked[:n]
bottom = ranked[-n:] if len(ranked) >= n else ranked[:]
return {"top": top, "bottom": bottom}
"""
Lifecycle Maturity Calculator (0-100)
Maturity = duration * 0.25
+ extremity * 0.25
+ price_extreme * 0.25
+ valuation * 0.15
+ etf_proliferation * 0.10
All sub-scores are direction-aware.
"""
import statistics
from typing import Optional
LIFECYCLE_WEIGHTS = {
"duration": 0.25,
"extremity": 0.25,
"price_extreme": 0.25,
"valuation": 0.15,
"etf_proliferation": 0.10,
}
def estimate_duration_score(
perf_1m: Optional[float],
perf_3m: Optional[float],
perf_6m: Optional[float],
perf_1y: Optional[float],
is_bearish: bool,
) -> float:
"""Count horizons where trend is active. Each active = 25 points.
Bullish: perf > 2%
Bearish: perf < -2%
None values treated as inactive.
"""
horizons = [perf_1m, perf_3m, perf_6m, perf_1y]
count = 0
for p in horizons:
if p is None:
continue
if is_bearish and p < -2.0:
count += 1
elif not is_bearish and p > 2.0:
count += 1
return float(count * 25)
def extremity_clustering_score(stock_metrics: list[dict], is_bearish: bool) -> float:
"""Proportion of stocks at RSI extremes.
Bullish: count RSI > 70
Bearish: count RSI < 30
Formula: min(100, pct * 200)
Returns 50.0 if empty.
"""
if not stock_metrics:
return 50.0
valid = [s for s in stock_metrics if s.get("rsi") is not None]
if not valid:
return 50.0
if is_bearish:
extreme_count = sum(1 for s in valid if s["rsi"] < 30)
else:
extreme_count = sum(1 for s in valid if s["rsi"] > 70)
pct = extreme_count / len(valid)
return min(100.0, pct * 200.0)
def price_extreme_saturation_score(stock_metrics: list[dict], is_bearish: bool) -> float:
"""Proportion of stocks near 52-week extremes.
Bullish: dist_from_52w_high <= 0.05
Bearish: dist_from_52w_low <= 0.05
Formula: min(100, pct * 200)
Returns 50.0 if empty.
"""
if not stock_metrics:
return 50.0
key = "dist_from_52w_low" if is_bearish else "dist_from_52w_high"
valid = [s for s in stock_metrics if s.get(key) is not None]
if not valid:
return 50.0
near_count = sum(1 for s in valid if s[key] <= 0.05)
pct = near_count / len(valid)
return min(100.0, pct * 200.0)
def valuation_premium_score(stock_metrics: list[dict]) -> float:
"""Score based on median P/E relative to market average (22).
premium_ratio = median_PE / 22.0
Score: min(100, max(0, (premium_ratio - 0.5) * 32))
Needs 3+ valid P/E values, else returns 50.0.
"""
valid_pe = [
s["pe_ratio"] for s in stock_metrics if s.get("pe_ratio") is not None and s["pe_ratio"] > 0
]
if len(valid_pe) < 3:
return 50.0
median_pe = statistics.median(valid_pe)
premium_ratio = median_pe / 22.0
return min(100.0, max(0.0, (premium_ratio - 0.5) * 32.0))
def etf_proliferation_score(etf_count: int) -> float:
"""Score based on number of theme-related ETFs.
0 => 0, 1 => 20, <=3 => 40, <=6 => 60, <=10 => 80, >10 => 100
"""
if etf_count == 0:
return 0.0
elif etf_count == 1:
return 20.0
elif etf_count <= 3:
return 40.0
elif etf_count <= 6:
return 60.0
elif etf_count <= 10:
return 80.0
else:
return 100.0
def has_sufficient_lifecycle_data(
extremity: Optional[float], price_extreme: Optional[float], valuation: Optional[float]
) -> bool:
"""Check whether stock-derived lifecycle sub-scores have real data.
Returns False if all three stock-based sub-scores are None (indicating
no stock metrics were available). Duration and etf_proliferation are
industry-level scores and always available.
"""
return not (extremity is None and price_extreme is None and valuation is None)
def classify_stage(maturity: float) -> str:
"""Classify lifecycle stage from maturity score.
0-20: Emerging, 20-40: Accelerating, 40-60: Trending,
60-80: Mature, 80-100: Exhausting
"""
if maturity < 20:
return "Emerging"
elif maturity < 40:
return "Accelerating"
elif maturity < 60:
return "Trending"
elif maturity < 80:
return "Mature"
else:
return "Exhausting"
def calculate_lifecycle_maturity(
duration: Optional[float],
extremity: Optional[float],
price_extreme: Optional[float],
valuation: Optional[float],
etf_prolif: Optional[float],
) -> float:
"""Weighted sum of lifecycle sub-scores, clamped 0-100.
Any None input defaults to 50.0.
"""
d = duration if duration is not None else 50.0
e = extremity if extremity is not None else 50.0
p = price_extreme if price_extreme is not None else 50.0
v = valuation if valuation is not None else 50.0
et = etf_prolif if etf_prolif is not None else 50.0
raw = (
d * LIFECYCLE_WEIGHTS["duration"]
+ e * LIFECYCLE_WEIGHTS["extremity"]
+ p * LIFECYCLE_WEIGHTS["price_extreme"]
+ v * LIFECYCLE_WEIGHTS["valuation"]
+ et * LIFECYCLE_WEIGHTS["etf_proliferation"]
)
return float(min(100.0, max(0.0, raw)))
#!/usr/bin/env python3
"""
Theme Classifier - Detect cross-sector and vertical themes from ranked industries.
Cross-sector themes match industries against keyword templates (min 2 matches).
Vertical themes detect sector concentration (min 3 same-sector industries in top/bottom).
themes_config format:
{
"cross_sector": [
{
"theme_name": str,
"matching_keywords": [str, ...],
"proxy_etfs": [str, ...],
"static_stocks": [str, ...],
},
...
],
"vertical_min_industries": int, # default 3
"cross_sector_min_matches": int, # default 2
}
"""
from collections import Counter
def classify_themes(
ranked_industries: list[dict],
themes_config: dict,
top_n: int = 30,
) -> list[dict]:
"""
Match ranked industries to cross-sector and vertical themes.
Only the top N and bottom N industries (by momentum rank) are considered
for theme matching. This prevents themes from always matching when using
the full ~145 industry universe.
Args:
ranked_industries: Output of rank_industries (sorted by momentum_score desc).
themes_config: Theme definitions with cross_sector templates and thresholds.
top_n: Number of top/bottom industries to consider (default 30).
Returns:
List of theme result dicts, each with:
theme_name, direction, matching_industries, sector_weights,
proxy_etfs, static_stocks
"""
if not ranked_industries:
return []
cross_sector_min = themes_config.get("cross_sector_min_matches", 2)
vertical_min = themes_config.get("vertical_min_industries", 3)
cross_sector_defs = themes_config.get("cross_sector", [])
# Build active set from top N + bottom N (deduplicated)
top = ranked_industries[:top_n]
bottom = ranked_industries[-top_n:] if len(ranked_industries) > top_n else []
active_set: dict[str, dict] = {ind["name"]: ind for ind in top}
for ind in bottom:
if ind["name"] not in active_set:
active_set[ind["name"]] = ind
themes = []
# 1. Cross-sector theme matching (active set only)
for theme_def in cross_sector_defs:
keywords = theme_def.get("matching_keywords", [])
matches = [kw for kw in keywords if kw in active_set]
if len(matches) >= cross_sector_min:
matching_inds = [active_set[m] for m in matches]
direction = _majority_direction(matching_inds)
sector_weights = get_theme_sector_weights({"matching_industries": matching_inds})
themes.append(
{
"theme_name": theme_def["theme_name"],
"direction": direction,
"matching_industries": matching_inds,
"sector_weights": sector_weights,
"proxy_etfs": theme_def.get("proxy_etfs", []),
"static_stocks": theme_def.get("static_stocks", []),
"theme_origin": "seed",
"name_confidence": "high",
}
)
# 2. Vertical (single-sector) theme detection
# Count industries per sector in top N and bottom N separately
top_set = set(ind["name"] for ind in top)
# Top N sector groups
top_sector_groups: dict[str, list[dict]] = {}
for ind in top:
sector = ind.get("sector")
if sector is None:
continue
top_sector_groups.setdefault(sector, []).append(ind)
for sector, inds in top_sector_groups.items():
if len(inds) >= vertical_min:
direction = _majority_direction(inds)
sector_weights = get_theme_sector_weights({"matching_industries": inds})
themes.append(
{
"theme_name": f"{sector} Sector Concentration",
"direction": direction,
"matching_industries": inds,
"sector_weights": sector_weights,
"proxy_etfs": [],
"static_stocks": [],
"theme_origin": "vertical",
"name_confidence": "high",
}
)
# Bottom N sector groups (excluding industries already in top N)
bottom_sector_groups: dict[str, list[dict]] = {}
for ind in bottom:
if ind["name"] in top_set:
continue
sector = ind.get("sector")
if sector is None:
continue
bottom_sector_groups.setdefault(sector, []).append(ind)
for sector, inds in bottom_sector_groups.items():
if len(inds) >= vertical_min:
direction = _majority_direction(inds)
sector_weights = get_theme_sector_weights({"matching_industries": inds})
themes.append(
{
"theme_name": f"{sector} Sector Concentration",
"direction": direction,
"matching_industries": inds,
"sector_weights": sector_weights,
"proxy_etfs": [],
"static_stocks": [],
"theme_origin": "vertical",
"name_confidence": "high",
}
)
return themes
def get_matched_industry_names(themes: list[dict]) -> set[str]:
"""Return the set of all matched industry names across classified themes.
Args:
themes: Output of classify_themes().
Returns:
Set of industry name strings.
"""
names: set[str] = set()
for theme in themes:
for ind in theme.get("matching_industries", []):
name = ind.get("name", "")
if name:
names.add(name)
return names
def get_theme_sector_weights(theme: dict) -> dict[str, float]:
"""
Calculate sector weight distribution for a theme's matching industries.
Args:
theme: Dict with "matching_industries" key containing industry dicts
(each may have a "sector" field).
Returns:
Dict mapping sector name to its proportion (0.0-1.0).
Industries without a sector field are excluded.
"""
matching = theme.get("matching_industries", [])
sectors = [ind["sector"] for ind in matching if "sector" in ind]
if not sectors:
return {}
counts = Counter(sectors)
total = sum(counts.values())
return {sector: count / total for sector, count in counts.items()}
# Sector-to-representative-stocks mapping for vertical theme enrichment
SECTOR_REPRESENTATIVE_STOCKS: dict[str, list[str]] = {
"Technology": ["AAPL", "MSFT", "NVDA", "AVGO", "CRM"],
"Consumer Cyclical": ["AMZN", "TSLA", "HD", "MCD", "NKE"],
"Consumer Defensive": ["PG", "KO", "PEP", "WMT", "COST"],
"Industrials": ["CAT", "HON", "UPS", "GE", "RTX"],
"Healthcare": ["UNH", "JNJ", "LLY", "PFE", "ABBV"],
"Financial": ["JPM", "BAC", "GS", "V", "MA"],
"Energy": ["XOM", "CVX", "COP", "SLB", "EOG"],
"Basic Materials": ["LIN", "APD", "NEM", "FCX", "NUE"],
"Communication Services": ["META", "GOOGL", "NFLX", "DIS", "TMUS"],
"Real Estate": ["PLD", "AMT", "EQIX", "SPG", "O"],
"Utilities": ["NEE", "DUK", "SO", "D", "AEP"],
}
# Sector-to-ETF mapping for vertical theme enrichment
SECTOR_ETFS: dict[str, list[str]] = {
"Energy": ["XLE"],
"Technology": ["XLK"],
"Basic Materials": ["XLB"],
"Industrials": ["XLI"],
"Consumer Cyclical": ["XLY"],
"Consumer Defensive": ["XLP"],
"Healthcare": ["XLV"],
"Financial": ["XLF"],
"Communication Services": ["XLC"],
"Real Estate": ["XLRE"],
"Utilities": ["XLU"],
}
def _industry_overlap_ratio(theme_a: dict, theme_b: dict) -> float:
"""Calculate industry name overlap ratio between two themes.
Returns the Jaccard-like ratio: |intersection| / |smaller set|.
"""
names_a = {ind.get("name") for ind in theme_a.get("matching_industries", [])}
names_b = {ind.get("name") for ind in theme_b.get("matching_industries", [])}
if not names_a or not names_b:
return 0.0
intersection = names_a & names_b
smaller = min(len(names_a), len(names_b))
return len(intersection) / smaller if smaller > 0 else 0.0
def enrich_vertical_themes(themes: list[dict]) -> None:
"""Add ETFs and stocks to vertical themes from overlapping seeds or sector mapping.
Mutates vertical themes in place:
1. If a seed theme shares >= 50% industry overlap, inherit its ETFs/stocks.
2. Otherwise, assign sector ETF from SECTOR_ETFS mapping.
"""
seed_themes = [t for t in themes if t.get("theme_origin") == "seed"]
for theme in themes:
if theme.get("theme_origin") != "vertical":
continue
if theme.get("proxy_etfs"):
continue # already has ETFs
# Try inheriting from overlapping seed theme
best_seed = None
best_overlap = 0.0
for seed in seed_themes:
overlap = _industry_overlap_ratio(theme, seed)
if overlap > best_overlap:
best_overlap = overlap
best_seed = seed
if best_seed and best_overlap >= 0.5:
theme["proxy_etfs"] = list(best_seed.get("proxy_etfs", []))
theme["static_stocks"] = list(best_seed.get("static_stocks", []))
continue
# Sector ETF fallback
sector_weights = theme.get("sector_weights", {})
if sector_weights:
primary_sector = max(sector_weights, key=sector_weights.get)
etfs = SECTOR_ETFS.get(primary_sector, [])
if etfs:
theme["proxy_etfs"] = list(etfs)
else:
# Infer sector from matching industries
sectors = [
ind.get("sector")
for ind in theme.get("matching_industries", [])
if ind.get("sector")
]
if sectors:
primary = max(set(sectors), key=sectors.count)
etfs = SECTOR_ETFS.get(primary, [])
if etfs:
theme["proxy_etfs"] = list(etfs)
# Fill empty static_stocks for vertical themes from sector mapping
for theme in themes:
if theme.get("theme_origin") != "vertical":
continue
if theme.get("static_stocks"):
continue # already has stocks
sector_weights = theme.get("sector_weights", {})
if sector_weights:
primary_sector = max(sector_weights, key=sector_weights.get)
else:
sectors = [
ind.get("sector")
for ind in theme.get("matching_industries", [])
if ind.get("sector")
]
primary_sector = max(set(sectors), key=sectors.count) if sectors else None
if primary_sector:
stocks = SECTOR_REPRESENTATIVE_STOCKS.get(primary_sector, [])
if stocks:
theme["static_stocks"] = list(stocks)
def deduplicate_themes(themes: list[dict], overlap_threshold: float = 0.5) -> list[dict]:
"""Remove vertical themes that duplicate seed themes.
A vertical theme is removed if:
- Same direction as a seed theme
- Industry overlap ratio >= overlap_threshold
Seed themes are always kept. Returns a new list.
"""
seed_themes = [t for t in themes if t.get("theme_origin") == "seed"]
result = list(seed_themes)
for theme in themes:
if theme.get("theme_origin") == "seed":
continue
is_duplicate = False
for seed in seed_themes:
if theme.get("direction") != seed.get("direction"):
continue
overlap = _industry_overlap_ratio(theme, seed)
if overlap >= overlap_threshold:
is_duplicate = True
break
if not is_duplicate:
result.append(theme)
return result
def _majority_direction(industries: list[dict]) -> str:
"""Determine majority direction from rank_direction (falls back to direction)."""
bullish = sum(
1 for ind in industries if ind.get("rank_direction", ind.get("direction")) == "bullish"
)
bearish = len(industries) - bullish
return "bullish" if bullish > bearish else "bearish"
"""Theme Discoverer - Automatic theme detection from unmatched industries.
Finds clusters of unmatched industries with similar performance patterns
and generates new theme entries compatible with classify_themes() output.
Algorithm:
1. Extract top_n and bottom_n from ranked_industries, exclude matched names.
2. Separate into bullish (top) and bearish (bottom) groups.
3. Sort each group by weighted_return.
4. Cluster adjacent industries that satisfy BOTH:
a. weighted_return gap <= gap_threshold
b. perf vector distance <= vector_threshold (normalized Euclidean)
5. Filter clusters by min_cluster_size.
6. Exclude clusters that overlap significantly with existing themes.
7. Auto-name clusters from industry name tokens.
"""
import math
from collections import Counter
from calculators.theme_classifier import get_theme_sector_weights
# ---------------------------------------------------------------------------
# Constants
# ---------------------------------------------------------------------------
_GAP_THRESHOLD_PCT = 3.0
_VECTOR_THRESHOLD = 0.5
_MIN_CLUSTER_SIZE = 2
_OVERLAP_THRESHOLD = 0.5
_STOP_WORDS = {
"Services",
"Products",
"Equipment",
"Materials",
"General",
"Other",
"Specialty",
"Diversified",
"Regulated",
"Independent",
"&",
"-",
"and",
}
# ---------------------------------------------------------------------------
# Public API
# ---------------------------------------------------------------------------
def discover_themes(
ranked_industries: list[dict],
matched_names: set[str],
existing_themes: list[dict],
top_n: int = 30,
gap_threshold: float = _GAP_THRESHOLD_PCT,
vector_threshold: float = _VECTOR_THRESHOLD,
min_cluster_size: int = _MIN_CLUSTER_SIZE,
) -> list[dict]:
"""Discover new themes from unmatched industries.
Args:
ranked_industries: Full ranked list (sorted by momentum_score desc).
matched_names: Set of industry names already matched by seed/vertical.
existing_themes: List of existing theme dicts (for overlap detection).
top_n: Number of top/bottom industries to consider.
gap_threshold: Max weighted_return gap between adjacent industries.
vector_threshold: Max normalized Euclidean distance for perf vectors.
min_cluster_size: Minimum industries to form a cluster.
Returns:
List of theme dicts compatible with classify_themes() output,
each with theme_origin="discovered" and name_confidence="medium".
"""
bullish, bearish = _get_unmatched_industries(ranked_industries, matched_names, top_n)
discovered = []
for group, direction in [(bullish, "bullish"), (bearish, "bearish")]:
clusters = _cluster_by_proximity(group, gap_threshold, vector_threshold)
for cluster in clusters:
if len(cluster) < min_cluster_size:
continue
if _is_duplicate_of_existing(cluster, direction, existing_themes):
continue
name = _auto_name_cluster(cluster)
theme = _build_theme_dict(name, cluster)
discovered.append(theme)
return discovered
# ---------------------------------------------------------------------------
# Internal helpers
# ---------------------------------------------------------------------------
def _get_unmatched_industries(
ranked: list[dict],
matched_names: set[str],
top_n: int,
) -> tuple[list[dict], list[dict]]:
"""Extract unmatched industries from top N and bottom N.
Returns:
(bullish_unmatched, bearish_unmatched)
"""
top = ranked[:top_n]
bottom = ranked[-top_n:] if len(ranked) > top_n else []
# Deduplicate: industries in both top and bottom only appear in top
top_names = {ind["name"] for ind in top}
bullish = [
ind for ind in top if ind["name"] not in matched_names and ind.get("direction") == "bullish"
]
bearish = [
ind
for ind in bottom
if ind["name"] not in matched_names
and ind["name"] not in top_names
and ind.get("direction") == "bearish"
]
return bullish, bearish
def _cluster_by_proximity(
industries: list[dict],
gap_threshold: float,
vector_threshold: float,
) -> list[list[dict]]:
"""Cluster industries by weighted_return proximity and perf vector distance.
Industries are sorted by weighted_return. Adjacent pairs are joined into
the same cluster if BOTH conditions are met:
1. abs(weighted_return difference) <= gap_threshold
2. perf_vector_distance <= vector_threshold
Returns:
List of clusters (each cluster is a list of industry dicts).
Only clusters with >= 2 members are returned.
"""
if not industries:
return []
sorted_inds = sorted(industries, key=lambda x: x.get("weighted_return", 0), reverse=True)
ranges = _compute_ranges(sorted_inds)
clusters: list[list[dict]] = [[sorted_inds[0]]]
for i in range(1, len(sorted_inds)):
prev = sorted_inds[i - 1]
curr = sorted_inds[i]
gap = abs(prev.get("weighted_return", 0) - curr.get("weighted_return", 0))
vec_dist = _perf_vector_distance(prev, curr, ranges)
if gap <= gap_threshold and vec_dist <= vector_threshold:
clusters[-1].append(curr)
else:
clusters.append([curr])
# Filter to min size 2
return [c for c in clusters if len(c) >= 2]
def _perf_vector_distance(a: dict, b: dict, ranges: dict) -> float:
"""Normalized Euclidean distance between perf vectors (1W, 1M, 3M).
Each timeframe difference is normalized by the range (max - min) across
all industries in the group. If range is 0, that dimension is ignored.
Returns:
0.0 (identical) to ~1.7 (maximally different across all 3 dimensions).
"""
keys = ["perf_1w", "perf_1m", "perf_3m"]
total = 0.0
for key in keys:
r = ranges.get(key, 0)
if r == 0:
continue
diff = (a.get(key, 0) - b.get(key, 0)) / r
total += diff * diff
return math.sqrt(total)
def _compute_ranges(industries: list[dict]) -> dict[str, float]:
"""Compute value ranges for perf normalization."""
keys = ["perf_1w", "perf_1m", "perf_3m"]
ranges = {}
for key in keys:
vals = [ind.get(key, 0) for ind in industries]
if vals:
ranges[key] = max(vals) - min(vals)
else:
ranges[key] = 0
return ranges
def _auto_name_cluster(industries: list[dict]) -> str:
"""Generate a descriptive name for a cluster from industry name tokens.
Tokenizes all industry names, removes stop words, picks the top 2
most frequent tokens and joins them.
"""
tokens: list[str] = []
for ind in industries:
name = ind.get("name", "")
for token in name.split():
cleaned = token.strip("(),")
if cleaned and cleaned not in _STOP_WORDS:
tokens.append(cleaned)
if not tokens:
return "Unknown Cluster"
counts = Counter(tokens)
top = counts.most_common(3)
if len(top) >= 2:
return f"{top[0][0]} & {top[1][0]} Related"
elif len(top) == 1:
return f"{top[0][0]} Related"
return "Unknown Cluster"
def _is_duplicate_of_existing(
cluster_industries: list[dict],
cluster_direction: str,
existing_themes: list[dict],
overlap_threshold: float = _OVERLAP_THRESHOLD,
) -> bool:
"""Check if a discovered cluster duplicates an existing theme.
A cluster is considered duplicate if:
1. Direction matches an existing theme, AND
2. Jaccard similarity >= threshold OR overlap coefficient >= threshold.
The overlap coefficient (intersection / min(|A|, |B|)) catches cases
where a small cluster is a subset of a large existing theme, which
Jaccard alone would miss because the union denominator dilutes the ratio.
"""
cluster_names = {ind.get("name", "") for ind in cluster_industries}
if not cluster_names:
return False
for theme in existing_themes:
if theme.get("direction") != cluster_direction:
continue
theme_names = {ind.get("name", "") for ind in theme.get("matching_industries", [])}
if not theme_names:
continue
intersection = cluster_names & theme_names
union = cluster_names | theme_names
jaccard = len(intersection) / len(union) if union else 0
min_size = min(len(cluster_names), len(theme_names))
overlap_coeff = len(intersection) / min_size if min_size else 0
if jaccard >= overlap_threshold or overlap_coeff >= overlap_threshold:
return True
return False
def _build_theme_dict(name: str, industries: list[dict]) -> dict:
"""Build a theme dict compatible with classify_themes() output.
Discovered themes have proxy_etfs=[], static_stocks=[],
theme_origin="discovered", name_confidence="medium".
"""
# Determine direction from majority
bullish = sum(1 for ind in industries if ind.get("direction") == "bullish")
bearish = len(industries) - bullish
direction = "bullish" if bullish > bearish else "bearish"
sector_weights = get_theme_sector_weights({"matching_industries": industries})
return {
"theme_name": name,
"direction": direction,
"matching_industries": industries,
"sector_weights": sector_weights,
"proxy_etfs": [],
"static_stocks": [],
"theme_origin": "discovered",
"name_confidence": "medium",
}
"""Theme configuration loader with YAML support and inline fallback.
Loads cross-sector theme definitions from:
1. User-specified YAML file (--themes-config)
2. Bundled themes.yaml (same directory as this module)
3. Inline DEFAULT_THEMES_CONFIG (fallback when YAML is unavailable)
Error handling:
- Explicit yaml_path: fail-fast on any error (FileNotFoundError, yaml.YAMLError)
- No yaml_path: bundled YAML -> inline fallback (safe degradation)
"""
import copy
import os
import sys
from typing import Optional
# Build tuple of catchable YAML errors (yaml may not be installed)
_YAML_ERRORS: tuple = ()
try:
import yaml
_YAML_ERRORS = (yaml.YAMLError,)
except ImportError:
pass
def load_themes_config(
yaml_path: Optional[str] = None,
) -> tuple[dict, dict[str, int]]:
"""Load themes config and ETF catalog.
Args:
yaml_path: Path to custom themes YAML. If None, tries bundled then inline.
Returns:
(themes_config, etf_catalog) where themes_config has etf_count stripped.
Raises:
FileNotFoundError: If explicit yaml_path doesn't exist.
yaml.YAMLError: If explicit yaml_path contains invalid YAML.
ValueError: If config fails validation.
"""
if yaml_path is not None:
# Explicit path -> fail-fast
raw = _load_yaml(yaml_path)
_validate_config(raw)
catalog = _extract_etf_catalog(raw)
config = _strip_etf_count(raw)
return config, catalog
# No path specified -> try bundled YAML, then inline fallback
try:
bundled = _get_bundled_yaml_path()
raw = _load_yaml(bundled)
_validate_config(raw)
catalog = _extract_etf_catalog(raw)
config = _strip_etf_count(raw)
return config, catalog
except (FileNotFoundError, ValueError, RuntimeError, ImportError, *_YAML_ERRORS) as exc:
print(f"WARNING: YAML load failed ({exc}), using inline config", file=sys.stderr)
from default_theme_config import DEFAULT_THEMES_CONFIG, ETF_CATALOG
return copy.deepcopy(DEFAULT_THEMES_CONFIG), dict(ETF_CATALOG)
def _load_yaml(path: str) -> dict:
"""Load and parse a YAML file.
Raises:
FileNotFoundError: If file doesn't exist.
RuntimeError: If PyYAML is not installed.
yaml.YAMLError: If YAML is malformed.
"""
if not os.path.exists(path):
raise FileNotFoundError(f"YAML config not found: {path}")
try:
import yaml
except ImportError:
raise RuntimeError(
"PyYAML is required but not installed. Install with: pip install pyyaml>=6.0"
)
with open(path) as f:
return yaml.safe_load(f)
def _validate_config(config: dict) -> None:
"""Validate required keys in themes config.
Raises:
ValueError: If required keys are missing.
"""
if not isinstance(config, dict):
raise ValueError("Config must be a dict")
if "cross_sector" not in config:
raise ValueError("Config missing required key: cross_sector")
for i, theme in enumerate(config["cross_sector"]):
if "theme_name" not in theme:
raise ValueError(f"Theme at index {i} missing required key: theme_name")
if "matching_keywords" not in theme:
raise ValueError(
f"Theme '{theme.get('theme_name', i)}' missing required key: matching_keywords"
)
def _extract_etf_catalog(config: dict) -> dict[str, int]:
"""Extract ETF counts from config into a catalog dict.
Each theme may have an `etf_count` field. Missing etf_count defaults to 0.
Returns:
{theme_name: etf_count}
"""
catalog = {}
for theme in config.get("cross_sector", []):
name = theme.get("theme_name", "")
catalog[name] = theme.get("etf_count", 0)
return catalog
def _strip_etf_count(config: dict) -> dict:
"""Return a copy of config with etf_count removed from each theme.
The etf_count is only used for ETF catalog extraction and should not
be passed to classify_themes().
"""
result = copy.deepcopy(config)
for theme in result.get("cross_sector", []):
theme.pop("etf_count", None)
return result
def _get_bundled_yaml_path() -> str:
"""Return the absolute path to the bundled themes.yaml."""
return os.path.join(os.path.dirname(os.path.abspath(__file__)), "themes.yaml")
"""Default inline theme configuration (fallback when YAML is unavailable).
This module holds DEFAULT_THEMES_CONFIG and ETF_CATALOG constants extracted
from theme_detector.py to avoid circular imports between theme_detector.py
and config_loader.py.
"""
DEFAULT_THEMES_CONFIG: dict = {
"cross_sector_min_matches": 2,
"vertical_min_industries": 3,
"cross_sector": [
{
"theme_name": "AI & Semiconductors",
"matching_keywords": [
"Semiconductors",
"Semiconductor Equipment & Materials",
"Software - Application",
"Software - Infrastructure",
"Information Technology Services",
"Computer Hardware",
"Electronic Components",
],
"proxy_etfs": ["SMH", "SOXX", "AIQ", "BOTZ"],
"static_stocks": [
"NVDA",
"AVGO",
"AMD",
"INTC",
"QCOM",
"MRVL",
"AMAT",
"LRCX",
"KLAC",
"TSM",
],
},
{
"theme_name": "Clean Energy & EV",
"matching_keywords": [
"Solar",
"Utilities - Renewable",
"Auto Manufacturers",
"Electrical Equipment & Parts",
"Specialty Chemicals",
],
"proxy_etfs": ["ICLN", "QCLN", "TAN", "LIT"],
"static_stocks": [
"ENPH",
"SEDG",
"FSLR",
"RUN",
"TSLA",
"RIVN",
"ALB",
"PLUG",
"BE",
"NEE",
],
},
{
"theme_name": "Cybersecurity",
"matching_keywords": [
"Software - Infrastructure",
"Software - Application",
"Information Technology Services",
],
"proxy_etfs": ["CIBR", "HACK", "BUG"],
"static_stocks": [
"CRWD",
"PANW",
"FTNT",
"ZS",
"S",
"OKTA",
"NET",
"CYBR",
"QLYS",
"RPD",
],
},
{
"theme_name": "Cloud Computing & SaaS",
"matching_keywords": [
"Software - Application",
"Software - Infrastructure",
"Information Technology Services",
],
"proxy_etfs": ["SKYY", "WCLD", "CLOU"],
"static_stocks": [
"CRM",
"NOW",
"SNOW",
"DDOG",
"MDB",
"NET",
"ZS",
"HUBS",
"WDAY",
"TEAM",
],
},
{
"theme_name": "Biotech & Genomics",
"matching_keywords": [
"Biotechnology",
"Drug Manufacturers - General",
"Drug Manufacturers - Specialty & Generic",
"Diagnostics & Research",
],
"proxy_etfs": ["XBI", "IBB", "ARKG"],
"static_stocks": [
"AMGN",
"GILD",
"VRTX",
"REGN",
"MRNA",
"BIIB",
"ILMN",
"SGEN",
"ALNY",
"BMRN",
],
},
{
"theme_name": "Infrastructure & Construction",
"matching_keywords": [
"Engineering & Construction",
"Building Products & Equipment",
"Building Materials",
"Specialty Industrial Machinery",
"Farm & Heavy Construction Machinery",
"Infrastructure Operations",
],
"proxy_etfs": ["PAVE", "IFRA"],
"static_stocks": [
"CAT",
"DE",
"VMC",
"MLM",
"URI",
"PWR",
"EME",
"J",
"ACM",
"FAST",
],
},
{
"theme_name": "Gold & Precious Metals",
"matching_keywords": [
"Gold",
"Silver",
"Other Precious Metals & Mining",
],
"proxy_etfs": ["GDX", "GDXJ", "SLV", "GLD"],
"static_stocks": [
"NEM",
"GOLD",
"AEM",
"FNV",
"WPM",
"RGLD",
"KGC",
"AGI",
"HL",
"PAAS",
],
},
{
"theme_name": "Oil & Gas (Energy)",
"matching_keywords": [
"Oil & Gas E&P",
"Oil & Gas Equipment & Services",
"Oil & Gas Integrated",
"Oil & Gas Midstream",
"Oil & Gas Refining & Marketing",
"Oil & Gas Drilling",
],
"proxy_etfs": ["XLE", "XOP", "OIH"],
"static_stocks": [
"XOM",
"CVX",
"COP",
"EOG",
"SLB",
"PXD",
"MPC",
"PSX",
"OXY",
"DVN",
],
},
{
"theme_name": "Financial Services & Banks",
"matching_keywords": [
"Banks - Diversified",
"Banks - Regional",
"Capital Markets",
"Insurance - Diversified",
"Financial Data & Stock Exchanges",
"Asset Management",
],
"proxy_etfs": ["XLF", "KBE", "KRE"],
"static_stocks": [
"JPM",
"BAC",
"GS",
"MS",
"WFC",
"C",
"BLK",
"SCHW",
"ICE",
"CME",
],
},
{
"theme_name": "Healthcare & Pharma",
"matching_keywords": [
"Drug Manufacturers - General",
"Medical Devices",
"Medical Instruments & Supplies",
"Healthcare Plans",
"Medical Care Facilities",
],
"proxy_etfs": ["XLV", "IHI", "XBI"],
"static_stocks": [
"UNH",
"JNJ",
"LLY",
"PFE",
"ABT",
"TMO",
"DHR",
"SYK",
"ISRG",
"MDT",
],
},
{
"theme_name": "Defense & Aerospace",
"matching_keywords": [
"Aerospace & Defense",
"Communication Equipment",
"Scientific & Technical Instruments",
],
"proxy_etfs": ["ITA", "PPA", "XAR"],
"static_stocks": [
"LMT",
"RTX",
"NOC",
"GD",
"BA",
"LHX",
"HII",
"TDG",
"HWM",
"AXON",
],
},
{
"theme_name": "Real Estate & REITs",
"matching_keywords": [
"REIT - Diversified",
"REIT - Industrial",
"REIT - Residential",
"REIT - Retail",
"REIT - Specialty",
"REIT - Office",
"Real Estate - Development",
"Real Estate Services",
],
"proxy_etfs": ["VNQ", "IYR", "XLRE"],
"static_stocks": [
"PLD",
"AMT",
"EQIX",
"CCI",
"SPG",
"O",
"WELL",
"DLR",
"PSA",
"AVB",
],
},
{
"theme_name": "Retail & Consumer",
"matching_keywords": [
"Internet Retail",
"Specialty Retail",
"Apparel Retail",
"Discount Stores",
"Home Improvement Retail",
"Department Stores",
],
"proxy_etfs": ["XRT", "XLY"],
"static_stocks": [
"AMZN",
"HD",
"LOW",
"TJX",
"COST",
"WMT",
"TGT",
"ROST",
"BURL",
"LULU",
],
},
{
"theme_name": "Crypto & Blockchain",
"matching_keywords": [
"Capital Markets",
"Software - Infrastructure",
"Financial Data & Stock Exchanges",
],
"proxy_etfs": ["BITO", "BLOK", "BITQ"],
"static_stocks": [
"COIN",
"MSTR",
"MARA",
"RIOT",
"CLSK",
"HUT",
"BITF",
"HIVE",
"SQ",
"PYPL",
],
},
{
"theme_name": "Nuclear & Uranium",
"matching_keywords": [
"Uranium",
"Utilities - Independent Power Producers",
"Utilities - Regulated Electric",
],
"proxy_etfs": ["URA", "URNM", "NLR"],
"static_stocks": [
"CCJ",
"UEC",
"NXE",
"DNN",
"LEU",
"SMR",
"UUUU",
"URG",
"OKLO",
"VST",
],
},
],
}
ETF_CATALOG: dict[str, int] = {
"AI & Semiconductors": 8,
"Clean Energy & EV": 7,
"Cybersecurity": 4,
"Cloud Computing & SaaS": 4,
"Biotech & Genomics": 5,
"Infrastructure & Construction": 3,
"Gold & Precious Metals": 4,
"Oil & Gas (Energy)": 5,
"Financial Services & Banks": 5,
"Healthcare & Pharma": 5,
"Defense & Aerospace": 5,
"Real Estate & REITs": 4,
"Retail & Consumer": 3,
"Crypto & Blockchain": 4,
"Nuclear & Uranium": 3,
}
"""
Theme Detector - Scoring, Labeling & Confidence
Combines theme heat and lifecycle maturity into a final theme score dict.
Follows the pattern from macro-regime-detector/scripts/scorer.py.
Design Note on Confidence Levels:
narrative_confirmed is intentionally set to False in the script output.
This is by design: Claude performs WebSearch-based narrative confirmation
as a post-processing step. Script-only output therefore has a maximum
confidence of "Medium" (quant_confirmed + breadth_confirmed = 2 layers).
After Claude confirms narrative alignment, confidence can reach "High".
"""
HEAT_LABELS = {
80: "Hot",
60: "Warm",
40: "Neutral",
20: "Cool",
0: "Cold",
}
def get_heat_label(heat_score: float) -> str:
"""Map heat score to label: Hot/Warm/Neutral/Cool/Cold."""
if heat_score >= 80:
return "Hot"
elif heat_score >= 60:
return "Warm"
elif heat_score >= 40:
return "Neutral"
elif heat_score >= 20:
return "Cool"
else:
return "Cold"
def calculate_confidence(
quant_confirmed: bool,
breadth_confirmed: bool,
narrative_confirmed: bool,
stale_data_penalty: bool,
) -> str:
"""Determine confidence level from confirmation layers.
3 layers confirmed => High
2 layers confirmed => Medium
1 or 0 layers => Low
stale_data_penalty downgrades by 1 level (High->Medium, Medium->Low).
"""
confirmed_count = sum([quant_confirmed, breadth_confirmed, narrative_confirmed])
if confirmed_count >= 3:
level = "High"
elif confirmed_count >= 2:
level = "Medium"
else:
level = "Low"
if stale_data_penalty and level != "Low":
level = "Medium" if level == "High" else "Low"
return level
def determine_data_mode(fmp_available: bool, finviz_elite: bool) -> str:
"""Describe available data sources."""
if fmp_available and finviz_elite:
return "FINVIZ-Elite+FMP"
elif fmp_available and not finviz_elite:
return "FMP-only"
elif not fmp_available and finviz_elite:
return "FINVIZ-Elite"
else:
return "FINVIZ-Public"
def score_theme(
theme_heat: float,
lifecycle_maturity: float,
lifecycle_stage: str,
direction: str,
confidence: str,
data_mode: str,
) -> dict:
"""Combine all dimensions into final theme score dict."""
return {
"theme_heat": theme_heat,
"heat_label": get_heat_label(theme_heat),
"lifecycle_maturity": lifecycle_maturity,
"lifecycle_stage": lifecycle_stage,
"direction": direction,
"confidence": confidence,
"data_mode": data_mode,
}
"""Shared fixtures for Theme Detector 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__))
Theme Detector Tests
Unit Tests (Phase 1)
- test_industry_ranker.py
- test_theme_classifier.py
- test_heat_calculator.py
- test_lifecycle_calculator.py
- test_scorer.py
- test_report_generator.py
- test_uptrend_client.py
Unit Tests (Phase 2)
- test_representative_stock_selector.py (dynamic stock selection, FINVIZ/FMP fallback, circuit breaker)
Integration Tests
- test_theme_detector_e2e.py (full pipeline, mocked I/O, no network required)
Network Integration Tests (Phase 2 TODO)
- test_finviz_performance_client.py (requires network)
- test_etf_scanner.py (requires network)
- test_uptrend_client_integration.py (requires network)
Related skills
How it compares
Pick theme-detector over single-stock analysis skills when the goal is cross-industry thematic ranking with heat and lifecycle scoring rather than one-ticker fundamentals.
FAQ
What metrics does theme-detector rank themes by?
theme-detector ranks themes using direction (LEAD/LAG), heat on a 0-100 scale, lifecycle stage (Emerging through Exhausting), and a confidence score. The Theme Dashboard table summarizes top themes in a single report view.
What does a theme-detector report include?
theme-detector produces a Theme Detection Report listing themes analyzed, industries scanned, total stocks, uptrend data status, and execution time alongside the ranked theme dashboard.
Is Theme Detector safe to install?
skills.sh reports 2 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.