quantwise 1.2.0 → 1.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/README.md +80 -0
- package/.claude/skills/backtest-expert/SKILL.md +206 -0
- package/.claude/skills/backtest-expert/references/failed_tests.md +236 -0
- package/.claude/skills/backtest-expert/references/methodology.md +227 -0
- package/.claude/skills/breadth-chart-analyst/SKILL.md +583 -0
- package/.claude/skills/breadth-chart-analyst/assets/SP500_Breadth_Index_200MA_8MA.jpeg +0 -0
- package/.claude/skills/breadth-chart-analyst/assets/US_Stock_Market_Uptrend_Ratio.jpeg +0 -0
- package/.claude/skills/breadth-chart-analyst/assets/breadth_analysis_template.md +558 -0
- package/.claude/skills/breadth-chart-analyst/references/breadth_chart_methodology.md +590 -0
- package/.claude/skills/canslim-screener/SKILL.md +599 -0
- package/.claude/skills/canslim-screener/references/canslim_methodology.md +606 -0
- package/.claude/skills/canslim-screener/references/fmp_api_endpoints.md +707 -0
- package/.claude/skills/canslim-screener/references/interpretation_guide.md +516 -0
- package/.claude/skills/canslim-screener/references/scoring_system.md +597 -0
- package/.claude/skills/canslim-screener/scripts/calculators/earnings_calculator.py +343 -0
- package/.claude/skills/canslim-screener/scripts/calculators/growth_calculator.py +334 -0
- package/.claude/skills/canslim-screener/scripts/calculators/institutional_calculator.py +347 -0
- package/.claude/skills/canslim-screener/scripts/calculators/leadership_calculator.py +380 -0
- package/.claude/skills/canslim-screener/scripts/calculators/market_calculator.py +244 -0
- package/.claude/skills/canslim-screener/scripts/calculators/new_highs_calculator.py +194 -0
- package/.claude/skills/canslim-screener/scripts/calculators/supply_demand_calculator.py +221 -0
- package/.claude/skills/canslim-screener/scripts/finviz_stock_client.py +227 -0
- package/.claude/skills/canslim-screener/scripts/fmp_client.py +393 -0
- package/.claude/skills/canslim-screener/scripts/report_generator.py +405 -0
- package/.claude/skills/canslim-screener/scripts/scorer.py +625 -0
- package/.claude/skills/canslim-screener/scripts/screen_canslim.py +361 -0
- package/.claude/skills/canslim-screener/scripts/test_institutional_endpoint.py +109 -0
- package/.claude/skills/chart/SKILL.md +20 -0
- package/.claude/skills/dividend-growth-pullback-screener/SKILL.md +322 -0
- package/.claude/skills/dividend-growth-pullback-screener/references/dividend_growth_compounding.md +400 -0
- package/.claude/skills/dividend-growth-pullback-screener/references/fmp_api_guide.md +642 -0
- package/.claude/skills/dividend-growth-pullback-screener/references/rsi_oversold_strategy.md +333 -0
- package/.claude/skills/dividend-growth-pullback-screener/scripts/screen_dividend_growth_rsi.py +1155 -0
- package/.claude/skills/earnings-calendar/SKILL.md +721 -0
- package/.claude/skills/earnings-calendar/assets/earnings_report_template.md +102 -0
- package/.claude/skills/earnings-calendar/references/fmp_api_guide.md +590 -0
- package/.claude/skills/earnings-calendar/scripts/fetch_earnings_fmp.py +443 -0
- package/.claude/skills/earnings-calendar/scripts/generate_report.py +366 -0
- package/.claude/skills/economic-calendar-fetcher/SKILL.md +365 -0
- package/.claude/skills/economic-calendar-fetcher/references/fmp_api_documentation.md +345 -0
- package/.claude/skills/economic-calendar-fetcher/scripts/get_economic_calendar.py +267 -0
- package/.claude/skills/ftd-detector/SKILL.md +147 -0
- package/.claude/skills/ftd-detector/references/ftd_methodology.md +188 -0
- package/.claude/skills/ftd-detector/references/post_ftd_guide.md +185 -0
- package/.claude/skills/ftd-detector/scripts/fmp_client.py +158 -0
- package/.claude/skills/ftd-detector/scripts/ftd_detector.py +280 -0
- package/.claude/skills/ftd-detector/scripts/post_ftd_monitor.py +404 -0
- package/.claude/skills/ftd-detector/scripts/rally_tracker.py +508 -0
- package/.claude/skills/ftd-detector/scripts/report_generator.py +341 -0
- package/.claude/skills/ftd-detector/scripts/tests/conftest.py +9 -0
- package/.claude/skills/ftd-detector/scripts/tests/helpers.py +107 -0
- package/.claude/skills/ftd-detector/scripts/tests/test_post_ftd_monitor.py +311 -0
- package/.claude/skills/ftd-detector/scripts/tests/test_rally_tracker.py +302 -0
- package/.claude/skills/institutional-flow-tracker/README.md +362 -0
- package/.claude/skills/institutional-flow-tracker/SKILL.md +357 -0
- package/.claude/skills/institutional-flow-tracker/references/13f_filings_guide.md +383 -0
- package/.claude/skills/institutional-flow-tracker/references/institutional_investor_types.md +580 -0
- package/.claude/skills/institutional-flow-tracker/references/interpretation_framework.md +573 -0
- package/.claude/skills/institutional-flow-tracker/scripts/analyze_single_stock.py +457 -0
- package/.claude/skills/institutional-flow-tracker/scripts/track_institution_portfolio.py +108 -0
- package/.claude/skills/institutional-flow-tracker/scripts/track_institutional_flow.py +450 -0
- package/.claude/skills/macro-regime-detector/SKILL.md +86 -0
- package/.claude/skills/macro-regime-detector/references/historical_regimes.md +124 -0
- package/.claude/skills/macro-regime-detector/references/indicator_interpretation_guide.md +144 -0
- package/.claude/skills/macro-regime-detector/references/regime_detection_methodology.md +138 -0
- package/.claude/skills/macro-regime-detector/scripts/calculators/__init__.py +1 -0
- package/.claude/skills/macro-regime-detector/scripts/calculators/concentration_calculator.py +165 -0
- package/.claude/skills/macro-regime-detector/scripts/calculators/credit_conditions_calculator.py +124 -0
- package/.claude/skills/macro-regime-detector/scripts/calculators/equity_bond_calculator.py +198 -0
- package/.claude/skills/macro-regime-detector/scripts/calculators/sector_rotation_calculator.py +123 -0
- package/.claude/skills/macro-regime-detector/scripts/calculators/size_factor_calculator.py +131 -0
- package/.claude/skills/macro-regime-detector/scripts/calculators/utils.py +347 -0
- package/.claude/skills/macro-regime-detector/scripts/calculators/yield_curve_calculator.py +279 -0
- package/.claude/skills/macro-regime-detector/scripts/fmp_client.py +134 -0
- package/.claude/skills/macro-regime-detector/scripts/macro_regime_detector.py +278 -0
- package/.claude/skills/macro-regime-detector/scripts/report_generator.py +327 -0
- package/.claude/skills/macro-regime-detector/scripts/scorer.py +574 -0
- package/.claude/skills/macro-regime-detector/scripts/tests/conftest.py +9 -0
- package/.claude/skills/macro-regime-detector/scripts/tests/test_concentration.py +78 -0
- package/.claude/skills/macro-regime-detector/scripts/tests/test_credit_conditions.py +59 -0
- package/.claude/skills/macro-regime-detector/scripts/tests/test_equity_bond.py +74 -0
- package/.claude/skills/macro-regime-detector/scripts/tests/test_helpers.py +90 -0
- package/.claude/skills/macro-regime-detector/scripts/tests/test_scorer.py +439 -0
- package/.claude/skills/macro-regime-detector/scripts/tests/test_sector_rotation.py +78 -0
- package/.claude/skills/macro-regime-detector/scripts/tests/test_size_factor.py +59 -0
- package/.claude/skills/macro-regime-detector/scripts/tests/test_utils.py +126 -0
- package/.claude/skills/macro-regime-detector/scripts/tests/test_yield_curve.py +64 -0
- package/.claude/skills/market-breadth-analyzer/SKILL.md +121 -0
- package/.claude/skills/market-breadth-analyzer/references/breadth_analysis_methodology.md +168 -0
- package/.claude/skills/market-breadth-analyzer/scripts/calculators/__init__.py +1 -0
- package/.claude/skills/market-breadth-analyzer/scripts/calculators/bearish_signal_calculator.py +150 -0
- package/.claude/skills/market-breadth-analyzer/scripts/calculators/cycle_calculator.py +168 -0
- package/.claude/skills/market-breadth-analyzer/scripts/calculators/divergence_calculator.py +119 -0
- package/.claude/skills/market-breadth-analyzer/scripts/calculators/historical_context_calculator.py +120 -0
- package/.claude/skills/market-breadth-analyzer/scripts/calculators/ma_crossover_calculator.py +115 -0
- package/.claude/skills/market-breadth-analyzer/scripts/calculators/trend_level_calculator.py +103 -0
- package/.claude/skills/market-breadth-analyzer/scripts/csv_client.py +225 -0
- package/.claude/skills/market-breadth-analyzer/scripts/market_breadth_analyzer.py +307 -0
- package/.claude/skills/market-breadth-analyzer/scripts/report_generator.py +330 -0
- package/.claude/skills/market-breadth-analyzer/scripts/scorer.py +271 -0
- package/.claude/skills/market-environment-analysis/SKILL.md +139 -0
- package/.claude/skills/market-environment-analysis/references/analysis_patterns.md +124 -0
- package/.claude/skills/market-environment-analysis/references/indicators.md +99 -0
- package/.claude/skills/market-environment-analysis/scripts/market_utils.py +127 -0
- package/.claude/skills/market-news-analyst/SKILL.md +714 -0
- package/.claude/skills/market-news-analyst/references/corporate_news_impact.md +446 -0
- package/.claude/skills/market-news-analyst/references/geopolitical_commodity_correlations.md +499 -0
- package/.claude/skills/market-news-analyst/references/market_event_patterns.md +393 -0
- package/.claude/skills/market-news-analyst/references/trusted_news_sources.md +510 -0
- package/.claude/skills/market-top-detector/SKILL.md +159 -0
- package/.claude/skills/market-top-detector/references/distribution_day_guide.md +100 -0
- package/.claude/skills/market-top-detector/references/historical_tops.md +142 -0
- package/.claude/skills/market-top-detector/references/market_top_methodology.md +167 -0
- package/.claude/skills/market-top-detector/scripts/calculators/__init__.py +17 -0
- package/.claude/skills/market-top-detector/scripts/calculators/breadth_calculator.py +116 -0
- package/.claude/skills/market-top-detector/scripts/calculators/defensive_rotation_calculator.py +127 -0
- package/.claude/skills/market-top-detector/scripts/calculators/distribution_day_calculator.py +161 -0
- package/.claude/skills/market-top-detector/scripts/calculators/index_technical_calculator.py +254 -0
- package/.claude/skills/market-top-detector/scripts/calculators/leading_stock_calculator.py +198 -0
- package/.claude/skills/market-top-detector/scripts/calculators/sentiment_calculator.py +213 -0
- package/.claude/skills/market-top-detector/scripts/fmp_client.py +158 -0
- package/.claude/skills/market-top-detector/scripts/market_top_detector.py +349 -0
- package/.claude/skills/market-top-detector/scripts/report_generator.py +314 -0
- package/.claude/skills/market-top-detector/scripts/scorer.py +473 -0
- package/.claude/skills/market-top-detector/scripts/tests/conftest.py +9 -0
- package/.claude/skills/market-top-detector/scripts/tests/helpers.py +49 -0
- package/.claude/skills/market-top-detector/scripts/tests/test_breadth.py +62 -0
- package/.claude/skills/market-top-detector/scripts/tests/test_defensive_rotation.py +56 -0
- package/.claude/skills/market-top-detector/scripts/tests/test_distribution_day.py +92 -0
- package/.claude/skills/market-top-detector/scripts/tests/test_index_technical.py +73 -0
- package/.claude/skills/market-top-detector/scripts/tests/test_leading_stock.py +57 -0
- package/.claude/skills/market-top-detector/scripts/tests/test_scorer.py +180 -0
- package/.claude/skills/market-top-detector/scripts/tests/test_sentiment.py +64 -0
- package/.claude/skills/options-strategy-advisor/README.md +469 -0
- package/.claude/skills/options-strategy-advisor/SKILL.md +959 -0
- package/.claude/skills/options-strategy-advisor/scripts/black_scholes.py +495 -0
- package/.claude/skills/pair-trade-screener/README.md +389 -0
- package/.claude/skills/pair-trade-screener/SKILL.md +622 -0
- package/.claude/skills/pair-trade-screener/references/cointegration_guide.md +745 -0
- package/.claude/skills/pair-trade-screener/references/methodology.md +853 -0
- package/.claude/skills/pair-trade-screener/scripts/analyze_spread.py +394 -0
- package/.claude/skills/pair-trade-screener/scripts/find_pairs.py +535 -0
- package/.claude/skills/portfolio-manager/README.md +394 -0
- package/.claude/skills/portfolio-manager/SKILL.md +750 -0
- package/.claude/skills/portfolio-manager/references/alpaca-mcp-setup.md +367 -0
- package/.claude/skills/portfolio-manager/references/asset-allocation.md +502 -0
- package/.claude/skills/portfolio-manager/references/diversification-principles.md +553 -0
- package/.claude/skills/portfolio-manager/references/portfolio-risk-metrics.md +603 -0
- package/.claude/skills/portfolio-manager/references/position-evaluation.md +477 -0
- package/.claude/skills/portfolio-manager/references/rebalancing-strategies.md +715 -0
- package/.claude/skills/portfolio-manager/references/risk-profile-questionnaire.md +608 -0
- package/.claude/skills/portfolio-manager/references/target-allocations.md +558 -0
- package/.claude/skills/portfolio-manager/scripts/test_alpaca_connection.py +286 -0
- package/.claude/skills/scenario-analyzer/SKILL.md +317 -0
- package/.claude/skills/scenario-analyzer/references/headline_event_patterns.md +264 -0
- package/.claude/skills/scenario-analyzer/references/scenario_playbooks.md +320 -0
- package/.claude/skills/scenario-analyzer/references/sector_sensitivity_matrix.md +217 -0
- package/.claude/skills/sector-analyst/SKILL.md +206 -0
- package/.claude/skills/sector-analyst/assets/industory_performance_1.jpeg +0 -0
- package/.claude/skills/sector-analyst/assets/industory_performance_2.jpeg +0 -0
- package/.claude/skills/sector-analyst/assets/sector_performance.jpeg +0 -0
- package/.claude/skills/sector-analyst/references/sector_rotation.md +170 -0
- package/.claude/skills/stanley-druckenmiller-investment/SKILL.md +84 -0
- package/.claude/skills/stanley-druckenmiller-investment/references/case-studies.md +148 -0
- package/.claude/skills/stanley-druckenmiller-investment/references/investment-philosophy.md +80 -0
- package/.claude/skills/stanley-druckenmiller-investment/references/market-analysis-guide.md +146 -0
- package/.claude/skills/stock/NOTION_SETUP.md +33 -0
- package/.claude/skills/stock/SKILL.md +38 -0
- package/.claude/skills/technical-analyst/SKILL.md +238 -0
- package/.claude/skills/technical-analyst/assets/analysis_template.md +183 -0
- package/.claude/skills/technical-analyst/references/technical_analysis_framework.md +282 -0
- package/.claude/skills/theme-detector/SKILL.md +320 -0
- package/.claude/skills/theme-detector/assets/report_template.md +155 -0
- package/.claude/skills/theme-detector/references/cross_sector_themes.md +252 -0
- package/.claude/skills/theme-detector/references/finviz_industry_codes.md +403 -0
- package/.claude/skills/theme-detector/references/thematic_etf_catalog.md +333 -0
- package/.claude/skills/theme-detector/references/theme_detection_methodology.md +430 -0
- package/.claude/skills/theme-detector/scripts/calculators/__init__.py +1 -0
- package/.claude/skills/theme-detector/scripts/calculators/heat_calculator.py +123 -0
- package/.claude/skills/theme-detector/scripts/calculators/industry_ranker.py +98 -0
- package/.claude/skills/theme-detector/scripts/calculators/lifecycle_calculator.py +172 -0
- package/.claude/skills/theme-detector/scripts/calculators/theme_classifier.py +195 -0
- package/.claude/skills/theme-detector/scripts/calculators/theme_discoverer.py +280 -0
- package/.claude/skills/theme-detector/scripts/config_loader.py +142 -0
- package/.claude/skills/theme-detector/scripts/default_theme_config.py +254 -0
- package/.claude/skills/theme-detector/scripts/etf_scanner.py +609 -0
- package/.claude/skills/theme-detector/scripts/finviz_performance_client.py +131 -0
- package/.claude/skills/theme-detector/scripts/report_generator.py +490 -0
- package/.claude/skills/theme-detector/scripts/representative_stock_selector.py +673 -0
- package/.claude/skills/theme-detector/scripts/scorer.py +87 -0
- package/.claude/skills/theme-detector/scripts/tests/README.md +21 -0
- package/.claude/skills/theme-detector/scripts/tests/conftest.py +9 -0
- package/.claude/skills/theme-detector/scripts/tests/test_config_loader.py +239 -0
- package/.claude/skills/theme-detector/scripts/tests/test_etf_scanner.py +810 -0
- package/.claude/skills/theme-detector/scripts/tests/test_heat_calculator.py +245 -0
- package/.claude/skills/theme-detector/scripts/tests/test_industry_ranker.py +256 -0
- package/.claude/skills/theme-detector/scripts/tests/test_lifecycle_calculator.py +301 -0
- package/.claude/skills/theme-detector/scripts/tests/test_report_generator.py +624 -0
- package/.claude/skills/theme-detector/scripts/tests/test_representative_stock_selector.py +898 -0
- package/.claude/skills/theme-detector/scripts/tests/test_scorer.py +185 -0
- package/.claude/skills/theme-detector/scripts/tests/test_theme_classifier.py +534 -0
- package/.claude/skills/theme-detector/scripts/tests/test_theme_detector_e2e.py +467 -0
- package/.claude/skills/theme-detector/scripts/tests/test_theme_discoverer.py +458 -0
- package/.claude/skills/theme-detector/scripts/tests/test_uptrend_client.py +76 -0
- package/.claude/skills/theme-detector/scripts/theme_detector.py +815 -0
- package/.claude/skills/theme-detector/scripts/themes.yaml +168 -0
- package/.claude/skills/theme-detector/scripts/uptrend_client.py +241 -0
- package/.claude/skills/uptrend-analyzer/SKILL.md +108 -0
- package/.claude/skills/uptrend-analyzer/references/uptrend_methodology.md +215 -0
- package/.claude/skills/uptrend-analyzer/scripts/calculators/__init__.py +1 -0
- package/.claude/skills/uptrend-analyzer/scripts/calculators/historical_context_calculator.py +122 -0
- package/.claude/skills/uptrend-analyzer/scripts/calculators/market_breadth_calculator.py +145 -0
- package/.claude/skills/uptrend-analyzer/scripts/calculators/momentum_calculator.py +183 -0
- package/.claude/skills/uptrend-analyzer/scripts/calculators/sector_participation_calculator.py +204 -0
- package/.claude/skills/uptrend-analyzer/scripts/calculators/sector_rotation_calculator.py +218 -0
- package/.claude/skills/uptrend-analyzer/scripts/data_fetcher.py +236 -0
- package/.claude/skills/uptrend-analyzer/scripts/report_generator.py +329 -0
- package/.claude/skills/uptrend-analyzer/scripts/scorer.py +276 -0
- package/.claude/skills/uptrend-analyzer/scripts/uptrend_analyzer.py +219 -0
- package/.claude/skills/us-market-bubble-detector/CHANGELOG.md +118 -0
- package/.claude/skills/us-market-bubble-detector/SKILL.md +545 -0
- package/.claude/skills/us-market-bubble-detector/references/bubble_framework.md +335 -0
- package/.claude/skills/us-market-bubble-detector/references/historical_cases.md +327 -0
- package/.claude/skills/us-market-bubble-detector/references/implementation_guide.md +473 -0
- package/.claude/skills/us-market-bubble-detector/references/quick_reference.md +354 -0
- package/.claude/skills/us-market-bubble-detector/references/quick_reference_en.md +342 -0
- package/.claude/skills/us-market-bubble-detector/scripts/bubble_scorer.py +309 -0
- package/.claude/skills/us-stock-analysis/SKILL.md +294 -0
- package/.claude/skills/us-stock-analysis/references/financial-metrics.md +172 -0
- package/.claude/skills/us-stock-analysis/references/fundamental-analysis.md +129 -0
- package/.claude/skills/us-stock-analysis/references/report-template.md +207 -0
- package/.claude/skills/us-stock-analysis/references/technical-analysis.md +93 -0
- package/.claude/skills/value-dividend-screener/SKILL.md +562 -0
- package/.claude/skills/value-dividend-screener/references/fmp_api_guide.md +348 -0
- package/.claude/skills/value-dividend-screener/references/screening_methodology.md +315 -0
- package/.claude/skills/value-dividend-screener/scripts/screen_dividend_stocks.py +1138 -0
- package/.claude/skills/vcp-screener/SKILL.md +79 -0
- package/.claude/skills/vcp-screener/references/fmp_api_endpoints.md +45 -0
- package/.claude/skills/vcp-screener/references/scoring_system.md +154 -0
- package/.claude/skills/vcp-screener/references/vcp_methodology.md +124 -0
- package/.claude/skills/vcp-screener/scripts/calculators/__init__.py +1 -0
- package/.claude/skills/vcp-screener/scripts/calculators/pivot_proximity_calculator.py +139 -0
- package/.claude/skills/vcp-screener/scripts/calculators/relative_strength_calculator.py +161 -0
- package/.claude/skills/vcp-screener/scripts/calculators/trend_template_calculator.py +228 -0
- package/.claude/skills/vcp-screener/scripts/calculators/vcp_pattern_calculator.py +322 -0
- package/.claude/skills/vcp-screener/scripts/calculators/volume_pattern_calculator.py +121 -0
- package/.claude/skills/vcp-screener/scripts/fmp_client.py +162 -0
- package/.claude/skills/vcp-screener/scripts/report_generator.py +317 -0
- package/.claude/skills/vcp-screener/scripts/scorer.py +155 -0
- package/.claude/skills/vcp-screener/scripts/screen_vcp.py +536 -0
- package/.claude/skills/vcp-screener/scripts/tests/__init__.py +0 -0
- package/.claude/skills/vcp-screener/scripts/tests/conftest.py +9 -0
- package/.claude/skills/vcp-screener/scripts/tests/test_vcp_screener.py +834 -0
- package/.claude/skills/weekly-trade-strategy/.claude/agents/druckenmiller-strategy-planner.md +300 -0
- package/.claude/skills/weekly-trade-strategy/.claude/agents/market-news-analyzer.md +239 -0
- package/.claude/skills/weekly-trade-strategy/.claude/agents/technical-market-analyst.md +187 -0
- package/.claude/skills/weekly-trade-strategy/.claude/agents/us-market-analyst.md +218 -0
- package/.claude/skills/weekly-trade-strategy/.claude/agents/weekly-trade-blog-writer.md +318 -0
- package/.claude/skills/weekly-trade-strategy/.claude/skills/breadth-chart-analyst/SKILL.md +662 -0
- package/.claude/skills/weekly-trade-strategy/.claude/skills/breadth-chart-analyst/assets/SP500_Breadth_Index_200MA_8MA.jpeg +0 -0
- package/.claude/skills/weekly-trade-strategy/.claude/skills/breadth-chart-analyst/assets/US_Stock_Market_Uptrend_Ratio.jpeg +0 -0
- package/.claude/skills/weekly-trade-strategy/.claude/skills/breadth-chart-analyst/assets/breadth_analysis_template.md +558 -0
- package/.claude/skills/weekly-trade-strategy/.claude/skills/breadth-chart-analyst/references/breadth_chart_methodology.md +590 -0
- package/.claude/skills/weekly-trade-strategy/.claude/skills/earnings-calendar/SKILL.md +721 -0
- package/.claude/skills/weekly-trade-strategy/.claude/skills/earnings-calendar/assets/earnings_report_template.md +102 -0
- package/.claude/skills/weekly-trade-strategy/.claude/skills/earnings-calendar/earnings_calendar_2025-11-02.md +447 -0
- package/.claude/skills/weekly-trade-strategy/.claude/skills/earnings-calendar/references/fmp_api_guide.md +590 -0
- package/.claude/skills/weekly-trade-strategy/.claude/skills/earnings-calendar/scripts/fetch_earnings_fmp.py +443 -0
- package/.claude/skills/weekly-trade-strategy/.claude/skills/earnings-calendar/scripts/generate_report.py +366 -0
- package/.claude/skills/weekly-trade-strategy/.claude/skills/economic-calendar-fetcher/SKILL.md +365 -0
- package/.claude/skills/weekly-trade-strategy/.claude/skills/economic-calendar-fetcher/references/fmp_api_documentation.md +345 -0
- package/.claude/skills/weekly-trade-strategy/.claude/skills/economic-calendar-fetcher/scripts/get_economic_calendar.py +267 -0
- package/.claude/skills/weekly-trade-strategy/.claude/skills/market-environment-analysis/SKILL.md +139 -0
- package/.claude/skills/weekly-trade-strategy/.claude/skills/market-environment-analysis/references/analysis_patterns.md +124 -0
- package/.claude/skills/weekly-trade-strategy/.claude/skills/market-environment-analysis/references/indicators.md +99 -0
- package/.claude/skills/weekly-trade-strategy/.claude/skills/market-environment-analysis/scripts/market_utils.py +127 -0
- package/.claude/skills/weekly-trade-strategy/.claude/skills/market-news-analyst/SKILL.md +714 -0
- package/.claude/skills/weekly-trade-strategy/.claude/skills/market-news-analyst/references/corporate_news_impact.md +446 -0
- package/.claude/skills/weekly-trade-strategy/.claude/skills/market-news-analyst/references/geopolitical_commodity_correlations.md +499 -0
- package/.claude/skills/weekly-trade-strategy/.claude/skills/market-news-analyst/references/market_event_patterns.md +393 -0
- package/.claude/skills/weekly-trade-strategy/.claude/skills/market-news-analyst/references/trusted_news_sources.md +510 -0
- package/.claude/skills/weekly-trade-strategy/.claude/skills/sector-analyst/SKILL.md +206 -0
- package/.claude/skills/weekly-trade-strategy/.claude/skills/sector-analyst/assets/industory_performance_1.jpeg +0 -0
- package/.claude/skills/weekly-trade-strategy/.claude/skills/sector-analyst/assets/industory_performance_2.jpeg +0 -0
- package/.claude/skills/weekly-trade-strategy/.claude/skills/sector-analyst/assets/sector_performance.jpeg +0 -0
- package/.claude/skills/weekly-trade-strategy/.claude/skills/sector-analyst/references/sector_rotation.md +170 -0
- package/.claude/skills/weekly-trade-strategy/.claude/skills/stanley-druckenmiller-investment/SKILL.md +84 -0
- package/.claude/skills/weekly-trade-strategy/.claude/skills/stanley-druckenmiller-investment/references/case-studies.md +148 -0
- package/.claude/skills/weekly-trade-strategy/.claude/skills/stanley-druckenmiller-investment/references/investment-philosophy.md +80 -0
- package/.claude/skills/weekly-trade-strategy/.claude/skills/stanley-druckenmiller-investment/references/market-analysis-guide.md +146 -0
- package/.claude/skills/weekly-trade-strategy/.claude/skills/technical-analyst/SKILL.md +238 -0
- package/.claude/skills/weekly-trade-strategy/.claude/skills/technical-analyst/assets/analysis_template.md +183 -0
- package/.claude/skills/weekly-trade-strategy/.claude/skills/technical-analyst/references/technical_analysis_framework.md +282 -0
- package/.claude/skills/weekly-trade-strategy/.claude/skills/us-market-bubble-detector/CHANGELOG.md +118 -0
- package/.claude/skills/weekly-trade-strategy/.claude/skills/us-market-bubble-detector/SKILL.md +545 -0
- package/.claude/skills/weekly-trade-strategy/.claude/skills/us-market-bubble-detector/references/bubble_framework.md +335 -0
- package/.claude/skills/weekly-trade-strategy/.claude/skills/us-market-bubble-detector/references/historical_cases.md +327 -0
- package/.claude/skills/weekly-trade-strategy/.claude/skills/us-market-bubble-detector/references/implementation_guide.md +473 -0
- package/.claude/skills/weekly-trade-strategy/.claude/skills/us-market-bubble-detector/references/quick_reference.md +354 -0
- package/.claude/skills/weekly-trade-strategy/.claude/skills/us-market-bubble-detector/references/quick_reference_en.md +342 -0
- package/.claude/skills/weekly-trade-strategy/.claude/skills/us-market-bubble-detector/scripts/bubble_scorer.py +309 -0
- package/.claude/skills/weekly-trade-strategy/.claude/skills/us-stock-analysis/SKILL.md +294 -0
- package/.claude/skills/weekly-trade-strategy/.claude/skills/us-stock-analysis/references/financial-metrics.md +172 -0
- package/.claude/skills/weekly-trade-strategy/.claude/skills/us-stock-analysis/references/fundamental-analysis.md +129 -0
- package/.claude/skills/weekly-trade-strategy/.claude/skills/us-stock-analysis/references/report-template.md +207 -0
- package/.claude/skills/weekly-trade-strategy/.claude/skills/us-stock-analysis/references/technical-analysis.md +93 -0
- package/.claude/skills/weekly-trade-strategy/CLAUDE.md +454 -0
- package/.claude/skills/weekly-trade-strategy/README.md +287 -0
- package/.claude/skills/weekly-trade-strategy/blogs/.gitkeep +0 -0
- package/.claude/skills/weekly-trade-strategy/charts/.gitkeep +0 -0
- package/.claude/skills/weekly-trade-strategy/earnings_data.json +10054 -0
- package/.claude/skills/weekly-trade-strategy/skills/breadth-chart-analyst/SKILL.md +662 -0
- package/.claude/skills/weekly-trade-strategy/skills/breadth-chart-analyst/assets/SP500_Breadth_Index_200MA_8MA.jpeg +0 -0
- package/.claude/skills/weekly-trade-strategy/skills/breadth-chart-analyst/assets/US_Stock_Market_Uptrend_Ratio.jpeg +0 -0
- package/.claude/skills/weekly-trade-strategy/skills/breadth-chart-analyst/assets/breadth_analysis_template.md +558 -0
- package/.claude/skills/weekly-trade-strategy/skills/breadth-chart-analyst/references/breadth_chart_methodology.md +590 -0
- package/.claude/skills/weekly-trade-strategy/skills/earnings-calendar/SKILL.md +721 -0
- package/.claude/skills/weekly-trade-strategy/skills/earnings-calendar/assets/earnings_report_template.md +102 -0
- package/.claude/skills/weekly-trade-strategy/skills/earnings-calendar/earnings_calendar_2025-11-02.md +447 -0
- package/.claude/skills/weekly-trade-strategy/skills/earnings-calendar/references/fmp_api_guide.md +590 -0
- package/.claude/skills/weekly-trade-strategy/skills/earnings-calendar/scripts/fetch_earnings_fmp.py +443 -0
- package/.claude/skills/weekly-trade-strategy/skills/earnings-calendar/scripts/generate_report.py +366 -0
- package/.claude/skills/weekly-trade-strategy/skills/economic-calendar-fetcher/SKILL.md +365 -0
- package/.claude/skills/weekly-trade-strategy/skills/economic-calendar-fetcher/references/fmp_api_documentation.md +345 -0
- package/.claude/skills/weekly-trade-strategy/skills/economic-calendar-fetcher/scripts/get_economic_calendar.py +267 -0
- package/.claude/skills/weekly-trade-strategy/skills/market-environment-analysis/SKILL.md +139 -0
- package/.claude/skills/weekly-trade-strategy/skills/market-environment-analysis/references/analysis_patterns.md +124 -0
- package/.claude/skills/weekly-trade-strategy/skills/market-environment-analysis/references/indicators.md +99 -0
- package/.claude/skills/weekly-trade-strategy/skills/market-environment-analysis/scripts/market_utils.py +127 -0
- package/.claude/skills/weekly-trade-strategy/skills/market-news-analyst/SKILL.md +714 -0
- package/.claude/skills/weekly-trade-strategy/skills/market-news-analyst/references/corporate_news_impact.md +446 -0
- package/.claude/skills/weekly-trade-strategy/skills/market-news-analyst/references/geopolitical_commodity_correlations.md +499 -0
- package/.claude/skills/weekly-trade-strategy/skills/market-news-analyst/references/market_event_patterns.md +393 -0
- package/.claude/skills/weekly-trade-strategy/skills/market-news-analyst/references/trusted_news_sources.md +510 -0
- package/.claude/skills/weekly-trade-strategy/skills/sector-analyst/SKILL.md +206 -0
- package/.claude/skills/weekly-trade-strategy/skills/sector-analyst/assets/industory_performance_1.jpeg +0 -0
- package/.claude/skills/weekly-trade-strategy/skills/sector-analyst/assets/industory_performance_2.jpeg +0 -0
- package/.claude/skills/weekly-trade-strategy/skills/sector-analyst/assets/sector_performance.jpeg +0 -0
- package/.claude/skills/weekly-trade-strategy/skills/sector-analyst/references/sector_rotation.md +170 -0
- package/.claude/skills/weekly-trade-strategy/skills/stanley-druckenmiller-investment/SKILL.md +84 -0
- package/.claude/skills/weekly-trade-strategy/skills/stanley-druckenmiller-investment/references/case-studies.md +148 -0
- package/.claude/skills/weekly-trade-strategy/skills/stanley-druckenmiller-investment/references/investment-philosophy.md +80 -0
- package/.claude/skills/weekly-trade-strategy/skills/stanley-druckenmiller-investment/references/market-analysis-guide.md +146 -0
- package/.claude/skills/weekly-trade-strategy/skills/technical-analyst/SKILL.md +238 -0
- package/.claude/skills/weekly-trade-strategy/skills/technical-analyst/assets/analysis_template.md +183 -0
- package/.claude/skills/weekly-trade-strategy/skills/technical-analyst/references/technical_analysis_framework.md +282 -0
- package/.claude/skills/weekly-trade-strategy/skills/us-market-bubble-detector/CHANGELOG.md +118 -0
- package/.claude/skills/weekly-trade-strategy/skills/us-market-bubble-detector/SKILL.md +545 -0
- package/.claude/skills/weekly-trade-strategy/skills/us-market-bubble-detector/references/bubble_framework.md +335 -0
- package/.claude/skills/weekly-trade-strategy/skills/us-market-bubble-detector/references/historical_cases.md +327 -0
- package/.claude/skills/weekly-trade-strategy/skills/us-market-bubble-detector/references/implementation_guide.md +473 -0
- package/.claude/skills/weekly-trade-strategy/skills/us-market-bubble-detector/references/quick_reference.md +354 -0
- package/.claude/skills/weekly-trade-strategy/skills/us-market-bubble-detector/references/quick_reference_en.md +342 -0
- package/.claude/skills/weekly-trade-strategy/skills/us-market-bubble-detector/scripts/bubble_scorer.py +309 -0
- package/.claude/skills/weekly-trade-strategy/skills/us-stock-analysis/SKILL.md +294 -0
- package/.claude/skills/weekly-trade-strategy/skills/us-stock-analysis/references/financial-metrics.md +172 -0
- package/.claude/skills/weekly-trade-strategy/skills/us-stock-analysis/references/fundamental-analysis.md +129 -0
- package/.claude/skills/weekly-trade-strategy/skills/us-stock-analysis/references/report-template.md +207 -0
- package/.claude/skills/weekly-trade-strategy/skills/us-stock-analysis/references/technical-analysis.md +93 -0
- package/.mcp.json +3 -0
- package/cli.mjs +16 -16
- package/package.json +4 -2
|
@@ -0,0 +1,815 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Theme Detector - Main Orchestrator
|
|
4
|
+
|
|
5
|
+
Detects trending market themes (bullish and bearish) by combining:
|
|
6
|
+
- FINVIZ industry/sector performance data
|
|
7
|
+
- yfinance ETF volume and stock metrics
|
|
8
|
+
- Monty's Uptrend Ratio Dashboard data
|
|
9
|
+
|
|
10
|
+
Outputs: JSON + Markdown report saved to reports/ directory.
|
|
11
|
+
|
|
12
|
+
Usage:
|
|
13
|
+
python3 theme_detector.py --output-dir reports/
|
|
14
|
+
python3 theme_detector.py --fmp-api-key $FMP_API_KEY --finviz-api-key $FINVIZ_API_KEY
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import argparse
|
|
18
|
+
import json
|
|
19
|
+
import os
|
|
20
|
+
import sys
|
|
21
|
+
import time
|
|
22
|
+
from datetime import datetime
|
|
23
|
+
from typing import Dict, List, Optional, Tuple
|
|
24
|
+
|
|
25
|
+
# Ensure scripts directory is on the path
|
|
26
|
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
27
|
+
|
|
28
|
+
from calculators.industry_ranker import rank_industries, get_top_bottom_industries
|
|
29
|
+
from calculators.theme_classifier import classify_themes
|
|
30
|
+
from calculators.heat_calculator import (
|
|
31
|
+
momentum_strength_score,
|
|
32
|
+
volume_intensity_score,
|
|
33
|
+
uptrend_signal_score,
|
|
34
|
+
breadth_signal_score,
|
|
35
|
+
calculate_theme_heat,
|
|
36
|
+
)
|
|
37
|
+
from calculators.lifecycle_calculator import (
|
|
38
|
+
estimate_duration_score,
|
|
39
|
+
extremity_clustering_score,
|
|
40
|
+
price_extreme_saturation_score,
|
|
41
|
+
valuation_premium_score,
|
|
42
|
+
etf_proliferation_score,
|
|
43
|
+
classify_stage,
|
|
44
|
+
calculate_lifecycle_maturity,
|
|
45
|
+
)
|
|
46
|
+
from scorer import (
|
|
47
|
+
score_theme,
|
|
48
|
+
calculate_confidence,
|
|
49
|
+
determine_data_mode,
|
|
50
|
+
)
|
|
51
|
+
from report_generator import generate_json_report, generate_markdown_report, save_reports
|
|
52
|
+
|
|
53
|
+
# Heavy-dependency modules (pandas/numpy/yfinance/finvizfinance) are imported
|
|
54
|
+
# lazily inside main() to allow lightweight helpers like _get_representative_stocks
|
|
55
|
+
# to be imported without triggering sys.exit(1) when pandas is absent.
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
# ---------------------------------------------------------------------------
|
|
59
|
+
# Industry-to-sector mapping (FINVIZ industry names -> sector)
|
|
60
|
+
# ---------------------------------------------------------------------------
|
|
61
|
+
INDUSTRY_TO_SECTOR: Dict[str, str] = {
|
|
62
|
+
# Technology
|
|
63
|
+
"Semiconductors": "Technology",
|
|
64
|
+
"Semiconductor Equipment & Materials": "Technology",
|
|
65
|
+
"Software - Application": "Technology",
|
|
66
|
+
"Software - Infrastructure": "Technology",
|
|
67
|
+
"Information Technology Services": "Technology",
|
|
68
|
+
"Computer Hardware": "Technology",
|
|
69
|
+
"Electronic Components": "Technology",
|
|
70
|
+
"Electronics & Computer Distribution": "Technology",
|
|
71
|
+
"Scientific & Technical Instruments": "Technology",
|
|
72
|
+
"Communication Equipment": "Technology",
|
|
73
|
+
"Consumer Electronics": "Technology",
|
|
74
|
+
# Healthcare
|
|
75
|
+
"Biotechnology": "Healthcare",
|
|
76
|
+
"Drug Manufacturers - General": "Healthcare",
|
|
77
|
+
"Drug Manufacturers - Specialty & Generic": "Healthcare",
|
|
78
|
+
"Medical Devices": "Healthcare",
|
|
79
|
+
"Medical Instruments & Supplies": "Healthcare",
|
|
80
|
+
"Diagnostics & Research": "Healthcare",
|
|
81
|
+
"Healthcare Plans": "Healthcare",
|
|
82
|
+
"Health Information Services": "Healthcare",
|
|
83
|
+
"Medical Care Facilities": "Healthcare",
|
|
84
|
+
"Pharmaceutical Retailers": "Healthcare",
|
|
85
|
+
"Medical Distribution": "Healthcare",
|
|
86
|
+
# Financial
|
|
87
|
+
"Banks - Diversified": "Financial",
|
|
88
|
+
"Banks - Regional": "Financial",
|
|
89
|
+
"Insurance - Life": "Financial",
|
|
90
|
+
"Insurance - Property & Casualty": "Financial",
|
|
91
|
+
"Insurance - Diversified": "Financial",
|
|
92
|
+
"Insurance - Specialty": "Financial",
|
|
93
|
+
"Insurance Brokers": "Financial",
|
|
94
|
+
"Asset Management": "Financial",
|
|
95
|
+
"Capital Markets": "Financial",
|
|
96
|
+
"Financial Data & Stock Exchanges": "Financial",
|
|
97
|
+
"Credit Services": "Financial",
|
|
98
|
+
"Mortgage Finance": "Financial",
|
|
99
|
+
"Financial Conglomerates": "Financial",
|
|
100
|
+
"Shell Companies": "Financial",
|
|
101
|
+
# Consumer Cyclical
|
|
102
|
+
"Auto Manufacturers": "Consumer Cyclical",
|
|
103
|
+
"Auto Parts": "Consumer Cyclical",
|
|
104
|
+
"Auto & Truck Dealerships": "Consumer Cyclical",
|
|
105
|
+
"Recreational Vehicles": "Consumer Cyclical",
|
|
106
|
+
"Furnishings, Fixtures & Appliances": "Consumer Cyclical",
|
|
107
|
+
"Residential Construction": "Consumer Cyclical",
|
|
108
|
+
"Textile Manufacturing": "Consumer Cyclical",
|
|
109
|
+
"Apparel Manufacturing": "Consumer Cyclical",
|
|
110
|
+
"Footwear & Accessories": "Consumer Cyclical",
|
|
111
|
+
"Packaging & Containers": "Consumer Cyclical",
|
|
112
|
+
"Personal Services": "Consumer Cyclical",
|
|
113
|
+
"Restaurants": "Consumer Cyclical",
|
|
114
|
+
"Apparel Retail": "Consumer Cyclical",
|
|
115
|
+
"Department Stores": "Consumer Cyclical",
|
|
116
|
+
"Home Improvement Retail": "Consumer Cyclical",
|
|
117
|
+
"Luxury Goods": "Consumer Cyclical",
|
|
118
|
+
"Internet Retail": "Consumer Cyclical",
|
|
119
|
+
"Specialty Retail": "Consumer Cyclical",
|
|
120
|
+
"Gambling": "Consumer Cyclical",
|
|
121
|
+
"Leisure": "Consumer Cyclical",
|
|
122
|
+
"Lodging": "Consumer Cyclical",
|
|
123
|
+
"Resorts & Casinos": "Consumer Cyclical",
|
|
124
|
+
"Travel Services": "Consumer Cyclical",
|
|
125
|
+
# Consumer Defensive
|
|
126
|
+
"Beverages - Non-Alcoholic": "Consumer Defensive",
|
|
127
|
+
"Beverages - Brewers": "Consumer Defensive",
|
|
128
|
+
"Beverages - Wineries & Distilleries": "Consumer Defensive",
|
|
129
|
+
"Confectioners": "Consumer Defensive",
|
|
130
|
+
"Farm Products": "Consumer Defensive",
|
|
131
|
+
"Household & Personal Products": "Consumer Defensive",
|
|
132
|
+
"Packaged Foods": "Consumer Defensive",
|
|
133
|
+
"Education & Training Services": "Consumer Defensive",
|
|
134
|
+
"Discount Stores": "Consumer Defensive",
|
|
135
|
+
"Food Distribution": "Consumer Defensive",
|
|
136
|
+
"Grocery Stores": "Consumer Defensive",
|
|
137
|
+
"Tobacco": "Consumer Defensive",
|
|
138
|
+
# Industrials
|
|
139
|
+
"Aerospace & Defense": "Industrials",
|
|
140
|
+
"Airlines": "Industrials",
|
|
141
|
+
"Building Products & Equipment": "Industrials",
|
|
142
|
+
"Business Equipment & Supplies": "Industrials",
|
|
143
|
+
"Conglomerates": "Industrials",
|
|
144
|
+
"Consulting Services": "Industrials",
|
|
145
|
+
"Electrical Equipment & Parts": "Industrials",
|
|
146
|
+
"Engineering & Construction": "Industrials",
|
|
147
|
+
"Farm & Heavy Construction Machinery": "Industrials",
|
|
148
|
+
"Industrial Distribution": "Industrials",
|
|
149
|
+
"Infrastructure Operations": "Industrials",
|
|
150
|
+
"Integrated Freight & Logistics": "Industrials",
|
|
151
|
+
"Marine Shipping": "Industrials",
|
|
152
|
+
"Metal Fabrication": "Industrials",
|
|
153
|
+
"Pollution & Treatment Controls": "Industrials",
|
|
154
|
+
"Railroads": "Industrials",
|
|
155
|
+
"Rental & Leasing Services": "Industrials",
|
|
156
|
+
"Security & Protection Services": "Industrials",
|
|
157
|
+
"Specialty Business Services": "Industrials",
|
|
158
|
+
"Specialty Industrial Machinery": "Industrials",
|
|
159
|
+
"Staffing & Employment Services": "Industrials",
|
|
160
|
+
"Tools & Accessories": "Industrials",
|
|
161
|
+
"Trucking": "Industrials",
|
|
162
|
+
"Waste Management": "Industrials",
|
|
163
|
+
# Energy
|
|
164
|
+
"Oil & Gas E&P": "Energy",
|
|
165
|
+
"Oil & Gas Equipment & Services": "Energy",
|
|
166
|
+
"Oil & Gas Integrated": "Energy",
|
|
167
|
+
"Oil & Gas Midstream": "Energy",
|
|
168
|
+
"Oil & Gas Refining & Marketing": "Energy",
|
|
169
|
+
"Oil & Gas Drilling": "Energy",
|
|
170
|
+
"Thermal Coal": "Energy",
|
|
171
|
+
"Uranium": "Energy",
|
|
172
|
+
"Solar": "Energy",
|
|
173
|
+
# Basic Materials
|
|
174
|
+
"Gold": "Basic Materials",
|
|
175
|
+
"Silver": "Basic Materials",
|
|
176
|
+
"Aluminum": "Basic Materials",
|
|
177
|
+
"Copper": "Basic Materials",
|
|
178
|
+
"Steel": "Basic Materials",
|
|
179
|
+
"Other Industrial Metals & Mining": "Basic Materials",
|
|
180
|
+
"Other Precious Metals & Mining": "Basic Materials",
|
|
181
|
+
"Coking Coal": "Basic Materials",
|
|
182
|
+
"Lumber & Wood Production": "Basic Materials",
|
|
183
|
+
"Paper & Paper Products": "Basic Materials",
|
|
184
|
+
"Chemicals": "Basic Materials",
|
|
185
|
+
"Specialty Chemicals": "Basic Materials",
|
|
186
|
+
"Agricultural Inputs": "Basic Materials",
|
|
187
|
+
"Building Materials": "Basic Materials",
|
|
188
|
+
# Communication Services
|
|
189
|
+
"Telecom Services": "Communication Services",
|
|
190
|
+
"Advertising Agencies": "Communication Services",
|
|
191
|
+
"Publishing": "Communication Services",
|
|
192
|
+
"Broadcasting": "Communication Services",
|
|
193
|
+
"Entertainment": "Communication Services",
|
|
194
|
+
"Internet Content & Information": "Communication Services",
|
|
195
|
+
"Electronic Gaming & Multimedia": "Communication Services",
|
|
196
|
+
# Real Estate
|
|
197
|
+
"REIT - Diversified": "Real Estate",
|
|
198
|
+
"REIT - Healthcare Facilities": "Real Estate",
|
|
199
|
+
"REIT - Hotel & Motel": "Real Estate",
|
|
200
|
+
"REIT - Industrial": "Real Estate",
|
|
201
|
+
"REIT - Mortgage": "Real Estate",
|
|
202
|
+
"REIT - Office": "Real Estate",
|
|
203
|
+
"REIT - Residential": "Real Estate",
|
|
204
|
+
"REIT - Retail": "Real Estate",
|
|
205
|
+
"REIT - Specialty": "Real Estate",
|
|
206
|
+
"Real Estate - Development": "Real Estate",
|
|
207
|
+
"Real Estate - Diversified": "Real Estate",
|
|
208
|
+
"Real Estate Services": "Real Estate",
|
|
209
|
+
# Utilities
|
|
210
|
+
"Utilities - Diversified": "Utilities",
|
|
211
|
+
"Utilities - Independent Power Producers": "Utilities",
|
|
212
|
+
"Utilities - Regulated Electric": "Utilities",
|
|
213
|
+
"Utilities - Regulated Gas": "Utilities",
|
|
214
|
+
"Utilities - Regulated Water": "Utilities",
|
|
215
|
+
"Utilities - Renewable": "Utilities",
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
# ---------------------------------------------------------------------------
|
|
220
|
+
# Theme configuration is loaded from YAML / inline fallback via config_loader.
|
|
221
|
+
# DEFAULT_THEMES_CONFIG and ETF_CATALOG live in default_theme_config.py
|
|
222
|
+
# to avoid circular imports.
|
|
223
|
+
# ---------------------------------------------------------------------------
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
# ---------------------------------------------------------------------------
|
|
227
|
+
# CLI argument parsing
|
|
228
|
+
# ---------------------------------------------------------------------------
|
|
229
|
+
def parse_args() -> argparse.Namespace:
|
|
230
|
+
parser = argparse.ArgumentParser(
|
|
231
|
+
description="Detect trending market themes from FINVIZ industry data"
|
|
232
|
+
)
|
|
233
|
+
parser.add_argument(
|
|
234
|
+
"--fmp-api-key",
|
|
235
|
+
default=os.environ.get("FMP_API_KEY"),
|
|
236
|
+
help="Financial Modeling Prep API key (env: FMP_API_KEY)",
|
|
237
|
+
)
|
|
238
|
+
parser.add_argument(
|
|
239
|
+
"--finviz-api-key",
|
|
240
|
+
default=os.environ.get("FINVIZ_API_KEY"),
|
|
241
|
+
help="FINVIZ Elite API key (env: FINVIZ_API_KEY)",
|
|
242
|
+
)
|
|
243
|
+
parser.add_argument(
|
|
244
|
+
"--finviz-mode",
|
|
245
|
+
choices=["public", "elite"],
|
|
246
|
+
default=None,
|
|
247
|
+
help="FINVIZ mode (auto-detected if not specified)",
|
|
248
|
+
)
|
|
249
|
+
parser.add_argument(
|
|
250
|
+
"--output-dir",
|
|
251
|
+
default="reports/",
|
|
252
|
+
help="Output directory for reports (default: reports/)",
|
|
253
|
+
)
|
|
254
|
+
parser.add_argument(
|
|
255
|
+
"--top",
|
|
256
|
+
type=int,
|
|
257
|
+
default=3,
|
|
258
|
+
help="Number of top themes to show in detail (default: 3)",
|
|
259
|
+
)
|
|
260
|
+
parser.add_argument(
|
|
261
|
+
"--max-themes",
|
|
262
|
+
type=int,
|
|
263
|
+
default=10,
|
|
264
|
+
help="Maximum themes to analyze (default: 10)",
|
|
265
|
+
)
|
|
266
|
+
parser.add_argument(
|
|
267
|
+
"--max-stocks-per-theme",
|
|
268
|
+
type=int,
|
|
269
|
+
default=10,
|
|
270
|
+
help="Maximum stocks per theme (default: 10)",
|
|
271
|
+
)
|
|
272
|
+
parser.add_argument(
|
|
273
|
+
"--themes-config",
|
|
274
|
+
default=None,
|
|
275
|
+
help="Path to custom themes.yaml (default: bundled)",
|
|
276
|
+
)
|
|
277
|
+
parser.add_argument(
|
|
278
|
+
"--discover-themes",
|
|
279
|
+
action="store_true",
|
|
280
|
+
default=False,
|
|
281
|
+
help="Enable automatic theme discovery for unmatched industries",
|
|
282
|
+
)
|
|
283
|
+
parser.add_argument(
|
|
284
|
+
"--dynamic-stocks",
|
|
285
|
+
action="store_true",
|
|
286
|
+
default=False,
|
|
287
|
+
help="Enable dynamic stock selection via FINVIZ screener (default: off)",
|
|
288
|
+
)
|
|
289
|
+
parser.add_argument(
|
|
290
|
+
"--dynamic-min-cap",
|
|
291
|
+
choices=["micro", "small", "mid"],
|
|
292
|
+
default="small",
|
|
293
|
+
help="Minimum market cap for dynamic stock selection (default: small=$300mln+)",
|
|
294
|
+
)
|
|
295
|
+
return parser.parse_args()
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
# ---------------------------------------------------------------------------
|
|
299
|
+
# Helpers
|
|
300
|
+
# ---------------------------------------------------------------------------
|
|
301
|
+
def _add_sector_info(industries: List[Dict]) -> List[Dict]:
|
|
302
|
+
"""Add sector field to each industry dict from INDUSTRY_TO_SECTOR mapping."""
|
|
303
|
+
for ind in industries:
|
|
304
|
+
name = ind.get("name", "")
|
|
305
|
+
ind["sector"] = INDUSTRY_TO_SECTOR.get(name, "Unknown")
|
|
306
|
+
return industries
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def _convert_perf_to_pct(industries: List[Dict]) -> List[Dict]:
|
|
310
|
+
"""Convert FINVIZ decimal performance values to percentage.
|
|
311
|
+
|
|
312
|
+
finvizfinance returns 0.05 for 5%. Our calculators expect 5.0.
|
|
313
|
+
"""
|
|
314
|
+
perf_keys = ["perf_1w", "perf_1m", "perf_3m", "perf_6m", "perf_1y", "perf_ytd"]
|
|
315
|
+
for ind in industries:
|
|
316
|
+
for key in perf_keys:
|
|
317
|
+
val = ind.get(key)
|
|
318
|
+
if val is not None:
|
|
319
|
+
ind[key] = val * 100.0
|
|
320
|
+
return industries
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def _get_representative_stocks(
|
|
324
|
+
theme: Dict,
|
|
325
|
+
selector, # Optional[RepresentativeStockSelector]
|
|
326
|
+
max_stocks: int,
|
|
327
|
+
) -> Tuple[List[str], List[Dict]]:
|
|
328
|
+
"""Get representative stocks and metadata for a theme.
|
|
329
|
+
|
|
330
|
+
When selector is provided (--dynamic-stocks), uses FINVIZ/FMP fallback chain.
|
|
331
|
+
Otherwise falls back to static_stocks from config.
|
|
332
|
+
|
|
333
|
+
Returns:
|
|
334
|
+
(tickers, stock_details)
|
|
335
|
+
tickers: ["NVDA", "AVGO", ...] (backward compatible)
|
|
336
|
+
stock_details: [{symbol, source, market_cap, matched_industries,
|
|
337
|
+
reasons, composite_score}, ...]
|
|
338
|
+
"""
|
|
339
|
+
if selector is not None:
|
|
340
|
+
details = selector.select_stocks(theme, max_stocks)
|
|
341
|
+
tickers = [d["symbol"] for d in details]
|
|
342
|
+
return tickers, details
|
|
343
|
+
|
|
344
|
+
# Static fallback (selector is None = --dynamic-stocks not set)
|
|
345
|
+
static = theme.get("static_stocks", [])[:max_stocks]
|
|
346
|
+
details = [
|
|
347
|
+
{
|
|
348
|
+
"symbol": s,
|
|
349
|
+
"source": "static",
|
|
350
|
+
"market_cap": 0,
|
|
351
|
+
"matched_industries": [],
|
|
352
|
+
"reasons": ["Static config"],
|
|
353
|
+
"composite_score": 0,
|
|
354
|
+
}
|
|
355
|
+
for s in static
|
|
356
|
+
]
|
|
357
|
+
return static, details
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def _calculate_breadth_ratio(theme: Dict) -> Optional[float]:
|
|
361
|
+
"""Estimate breadth ratio from theme's matching industries.
|
|
362
|
+
|
|
363
|
+
For bullish: ratio of industries with positive weighted_return.
|
|
364
|
+
For bearish: ratio of industries with negative weighted_return.
|
|
365
|
+
"""
|
|
366
|
+
industries = theme.get("matching_industries", [])
|
|
367
|
+
if not industries:
|
|
368
|
+
return None
|
|
369
|
+
|
|
370
|
+
is_bearish = theme.get("direction") == "bearish"
|
|
371
|
+
if is_bearish:
|
|
372
|
+
count = sum(1 for ind in industries if ind.get("weighted_return", 0) < 0)
|
|
373
|
+
else:
|
|
374
|
+
count = sum(1 for ind in industries if ind.get("weighted_return", 0) > 0)
|
|
375
|
+
|
|
376
|
+
return count / len(industries)
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def _get_theme_uptrend_data(
|
|
380
|
+
theme: Dict, sector_uptrend: Dict
|
|
381
|
+
) -> List[Dict]:
|
|
382
|
+
"""Build sector_data for uptrend_signal_score from theme's sector weights.
|
|
383
|
+
|
|
384
|
+
Maps theme sectors to uptrend data with weights.
|
|
385
|
+
"""
|
|
386
|
+
sector_weights = theme.get("sector_weights", {})
|
|
387
|
+
if not sector_weights or not sector_uptrend:
|
|
388
|
+
return []
|
|
389
|
+
|
|
390
|
+
sector_data = []
|
|
391
|
+
for sector_name, weight in sector_weights.items():
|
|
392
|
+
uptrend_entry = sector_uptrend.get(sector_name)
|
|
393
|
+
if uptrend_entry and uptrend_entry.get("ratio") is not None:
|
|
394
|
+
sector_data.append({
|
|
395
|
+
"sector": sector_name,
|
|
396
|
+
"ratio": uptrend_entry["ratio"],
|
|
397
|
+
"ma_10": uptrend_entry.get("ma_10") or 0,
|
|
398
|
+
"slope": uptrend_entry.get("slope") or 0,
|
|
399
|
+
"weight": weight,
|
|
400
|
+
})
|
|
401
|
+
|
|
402
|
+
return sector_data
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
def _get_theme_weighted_return(theme: Dict) -> float:
|
|
406
|
+
"""Calculate aggregate weighted return for a theme from its industries."""
|
|
407
|
+
industries = theme.get("matching_industries", [])
|
|
408
|
+
if not industries:
|
|
409
|
+
return 0.0
|
|
410
|
+
|
|
411
|
+
returns = [ind.get("weighted_return", 0.0) for ind in industries]
|
|
412
|
+
return sum(returns) / len(returns)
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
# ---------------------------------------------------------------------------
|
|
416
|
+
# Main orchestrator
|
|
417
|
+
# ---------------------------------------------------------------------------
|
|
418
|
+
def main():
|
|
419
|
+
# Lazy imports: these modules depend on pandas/numpy/yfinance/finvizfinance
|
|
420
|
+
# and are only needed at runtime, not when importing helpers for testing.
|
|
421
|
+
from finviz_performance_client import get_industry_performance
|
|
422
|
+
from etf_scanner import ETFScanner
|
|
423
|
+
from uptrend_client import fetch_sector_uptrend_data, is_data_stale
|
|
424
|
+
from config_loader import load_themes_config
|
|
425
|
+
|
|
426
|
+
args = parse_args()
|
|
427
|
+
|
|
428
|
+
# -----------------------------------------------------------------------
|
|
429
|
+
# Step 0: Load theme configuration (YAML or inline fallback)
|
|
430
|
+
# -----------------------------------------------------------------------
|
|
431
|
+
themes_config, etf_catalog = load_themes_config(args.themes_config)
|
|
432
|
+
start_time = time.time()
|
|
433
|
+
|
|
434
|
+
# Determine data mode
|
|
435
|
+
finviz_mode = args.finviz_mode
|
|
436
|
+
if finviz_mode is None:
|
|
437
|
+
finviz_mode = "elite" if args.finviz_api_key else "public"
|
|
438
|
+
fmp_available = args.fmp_api_key is not None
|
|
439
|
+
data_mode = determine_data_mode(fmp_available, finviz_mode == "elite")
|
|
440
|
+
|
|
441
|
+
print("Theme Detector starting...", file=sys.stderr)
|
|
442
|
+
print(f" Data mode: {data_mode}", file=sys.stderr)
|
|
443
|
+
print(f" FINVIZ mode: {finviz_mode}", file=sys.stderr)
|
|
444
|
+
print(f" FMP API: {'available' if fmp_available else 'not available'}",
|
|
445
|
+
file=sys.stderr)
|
|
446
|
+
print(f" Max themes: {args.max_themes}", file=sys.stderr)
|
|
447
|
+
print(f" Max stocks/theme: {args.max_stocks_per_theme}", file=sys.stderr)
|
|
448
|
+
|
|
449
|
+
metadata = {
|
|
450
|
+
"generated_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
|
451
|
+
"data_mode": data_mode,
|
|
452
|
+
"finviz_mode": finviz_mode,
|
|
453
|
+
"fmp_available": fmp_available,
|
|
454
|
+
"max_themes": args.max_themes,
|
|
455
|
+
"max_stocks_per_theme": args.max_stocks_per_theme,
|
|
456
|
+
"data_sources": {},
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
# -----------------------------------------------------------------------
|
|
460
|
+
# Step 1: Fetch FINVIZ industry performance
|
|
461
|
+
# -----------------------------------------------------------------------
|
|
462
|
+
print("Fetching FINVIZ industry performance...", file=sys.stderr)
|
|
463
|
+
raw_industries = get_industry_performance()
|
|
464
|
+
if not raw_industries:
|
|
465
|
+
print("ERROR: No industry data from FINVIZ. Exiting.", file=sys.stderr)
|
|
466
|
+
sys.exit(1)
|
|
467
|
+
|
|
468
|
+
metadata["data_sources"]["finviz_industries"] = len(raw_industries)
|
|
469
|
+
print(f" Got {len(raw_industries)} industries", file=sys.stderr)
|
|
470
|
+
|
|
471
|
+
# Convert decimal to percentage and add sector info
|
|
472
|
+
industries = _convert_perf_to_pct(raw_industries)
|
|
473
|
+
industries = _add_sector_info(industries)
|
|
474
|
+
|
|
475
|
+
# -----------------------------------------------------------------------
|
|
476
|
+
# Step 2: Rank industries by momentum
|
|
477
|
+
# -----------------------------------------------------------------------
|
|
478
|
+
print("Ranking industries by momentum...", file=sys.stderr)
|
|
479
|
+
ranked = rank_industries(industries)
|
|
480
|
+
industry_rankings = get_top_bottom_industries(ranked, n=15)
|
|
481
|
+
print(f" Top: {ranked[0]['name']} ({ranked[0]['momentum_score']})" if ranked else "",
|
|
482
|
+
file=sys.stderr)
|
|
483
|
+
|
|
484
|
+
# -----------------------------------------------------------------------
|
|
485
|
+
# Step 3: Classify themes
|
|
486
|
+
# -----------------------------------------------------------------------
|
|
487
|
+
print("Classifying themes...", file=sys.stderr)
|
|
488
|
+
themes = classify_themes(ranked, themes_config)
|
|
489
|
+
print(f" Detected {len(themes)} themes (seed + vertical)", file=sys.stderr)
|
|
490
|
+
|
|
491
|
+
if not themes:
|
|
492
|
+
print("WARNING: No themes detected. Generating empty report.",
|
|
493
|
+
file=sys.stderr)
|
|
494
|
+
|
|
495
|
+
# Step 3.5: Discover new themes from unmatched industries
|
|
496
|
+
if args.discover_themes:
|
|
497
|
+
from calculators.theme_classifier import get_matched_industry_names
|
|
498
|
+
from calculators.theme_discoverer import discover_themes
|
|
499
|
+
matched_names = get_matched_industry_names(themes)
|
|
500
|
+
discovered = discover_themes(ranked, matched_names, themes, top_n=30)
|
|
501
|
+
themes.extend(discovered)
|
|
502
|
+
metadata["data_sources"]["discovered_themes"] = len(discovered)
|
|
503
|
+
print(f" Discovered {len(discovered)} new themes", file=sys.stderr)
|
|
504
|
+
|
|
505
|
+
# Step 3.9: Limit to max_themes using composite priority (size + strength)
|
|
506
|
+
def _theme_priority(t):
|
|
507
|
+
inds = t.get("matching_industries", [])
|
|
508
|
+
n_industries = len(inds)
|
|
509
|
+
avg_strength = (
|
|
510
|
+
sum(abs(ind.get("weighted_return", 0)) for ind in inds) / max(len(inds), 1)
|
|
511
|
+
)
|
|
512
|
+
size_norm = min(n_industries / 10.0, 1.0)
|
|
513
|
+
strength_norm = min(avg_strength / 30.0, 1.0)
|
|
514
|
+
return size_norm * 0.5 + strength_norm * 0.5
|
|
515
|
+
|
|
516
|
+
themes.sort(key=_theme_priority, reverse=True)
|
|
517
|
+
themes = themes[:args.max_themes]
|
|
518
|
+
|
|
519
|
+
# -----------------------------------------------------------------------
|
|
520
|
+
# Step 4: Collect all stock symbols for batch download
|
|
521
|
+
# -----------------------------------------------------------------------
|
|
522
|
+
print("Selecting representative stocks...", file=sys.stderr)
|
|
523
|
+
|
|
524
|
+
# Create dynamic selector if requested
|
|
525
|
+
selector = None
|
|
526
|
+
if args.dynamic_stocks:
|
|
527
|
+
from representative_stock_selector import RepresentativeStockSelector
|
|
528
|
+
selector = RepresentativeStockSelector(
|
|
529
|
+
finviz_elite_key=args.finviz_api_key,
|
|
530
|
+
fmp_api_key=args.fmp_api_key,
|
|
531
|
+
finviz_mode=finviz_mode,
|
|
532
|
+
rate_limit_sec=1.0,
|
|
533
|
+
min_cap=args.dynamic_min_cap,
|
|
534
|
+
)
|
|
535
|
+
print(" Dynamic stock selection: ON", file=sys.stderr)
|
|
536
|
+
|
|
537
|
+
# Use index-based keys to avoid collisions when multiple themes share
|
|
538
|
+
# the same name (e.g. two "{Sector} Sector Concentration" themes for
|
|
539
|
+
# top and bottom, or duplicate auto-names from the discoverer).
|
|
540
|
+
theme_stocks: Dict[int, List[str]] = {}
|
|
541
|
+
theme_stock_details: Dict[int, List[Dict]] = {}
|
|
542
|
+
all_symbols = set()
|
|
543
|
+
|
|
544
|
+
for idx, theme in enumerate(themes):
|
|
545
|
+
tickers, stock_details = _get_representative_stocks(
|
|
546
|
+
theme, selector, args.max_stocks_per_theme
|
|
547
|
+
)
|
|
548
|
+
theme_stocks[idx] = tickers
|
|
549
|
+
theme_stock_details[idx] = stock_details
|
|
550
|
+
all_symbols.update(tickers)
|
|
551
|
+
|
|
552
|
+
all_symbols_list = sorted(all_symbols)
|
|
553
|
+
print(f" Total unique stocks: {len(all_symbols_list)}", file=sys.stderr)
|
|
554
|
+
|
|
555
|
+
if selector:
|
|
556
|
+
print(f" Dynamic stock queries: {selector.query_count}", file=sys.stderr)
|
|
557
|
+
print(f" Dynamic stock failures: {selector.failure_count}", file=sys.stderr)
|
|
558
|
+
print(f" Dynamic stock status: {selector.status}", file=sys.stderr)
|
|
559
|
+
metadata["data_sources"]["dynamic_stocks_status"] = selector.status
|
|
560
|
+
metadata["data_sources"]["dynamic_stocks_queries"] = selector.query_count
|
|
561
|
+
metadata["data_sources"]["dynamic_stocks_failures"] = selector.failure_count
|
|
562
|
+
metadata["data_sources"]["dynamic_stocks_source_states"] = {
|
|
563
|
+
name: {"disabled": s.disabled, "failures": s.total_failures}
|
|
564
|
+
for name, s in selector.source_states.items()
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
# -----------------------------------------------------------------------
|
|
568
|
+
# Step 5: Batch fetch stock metrics (yfinance)
|
|
569
|
+
# -----------------------------------------------------------------------
|
|
570
|
+
stock_metrics_map: Dict[str, Dict] = {}
|
|
571
|
+
scanner = ETFScanner(fmp_api_key=args.fmp_api_key)
|
|
572
|
+
|
|
573
|
+
if all_symbols_list:
|
|
574
|
+
print(f"Batch downloading {len(all_symbols_list)} stocks...", file=sys.stderr)
|
|
575
|
+
all_metrics = scanner.batch_stock_metrics(all_symbols_list)
|
|
576
|
+
for m in all_metrics:
|
|
577
|
+
stock_metrics_map[m["symbol"]] = m
|
|
578
|
+
# Backward compatible key (1 release coexistence)
|
|
579
|
+
metadata["data_sources"]["yfinance_stocks"] = len(all_metrics)
|
|
580
|
+
print(f" Got metrics for {len(all_metrics)} stocks", file=sys.stderr)
|
|
581
|
+
|
|
582
|
+
# -----------------------------------------------------------------------
|
|
583
|
+
# Step 6: Fetch ETF volume ratios for each theme's proxy ETFs
|
|
584
|
+
# -----------------------------------------------------------------------
|
|
585
|
+
print("Fetching ETF volume data...", file=sys.stderr)
|
|
586
|
+
etf_volume_map: Dict[str, Dict] = {}
|
|
587
|
+
all_etfs = set()
|
|
588
|
+
for theme in themes:
|
|
589
|
+
for etf in theme.get("proxy_etfs", []):
|
|
590
|
+
all_etfs.add(etf)
|
|
591
|
+
|
|
592
|
+
etf_volume_map = scanner.batch_etf_volume_ratios(sorted(all_etfs))
|
|
593
|
+
|
|
594
|
+
metadata["data_sources"]["etf_volume"] = len(etf_volume_map)
|
|
595
|
+
|
|
596
|
+
# Capture backend stats after all scanner calls (stock + ETF)
|
|
597
|
+
scanner_stats = scanner.backend_stats()
|
|
598
|
+
metadata["data_sources"]["scanner_backend"] = scanner_stats
|
|
599
|
+
print(f" Scanner: FMP {scanner_stats['fmp_calls']} calls "
|
|
600
|
+
f"({scanner_stats['fmp_failures']} failures), "
|
|
601
|
+
f"yfinance: {scanner_stats['yf_calls']} calls "
|
|
602
|
+
f"({scanner_stats['yf_fallbacks']} fallbacks)",
|
|
603
|
+
file=sys.stderr)
|
|
604
|
+
|
|
605
|
+
# -----------------------------------------------------------------------
|
|
606
|
+
# Step 7: Fetch uptrend-dashboard data
|
|
607
|
+
# -----------------------------------------------------------------------
|
|
608
|
+
print("Fetching uptrend ratio data...", file=sys.stderr)
|
|
609
|
+
sector_uptrend = fetch_sector_uptrend_data()
|
|
610
|
+
stale_data = False
|
|
611
|
+
|
|
612
|
+
if sector_uptrend:
|
|
613
|
+
# Check freshness from any sector's latest_date
|
|
614
|
+
any_sector = next(iter(sector_uptrend.values()), {})
|
|
615
|
+
latest_date = any_sector.get("latest_date", "")
|
|
616
|
+
stale_data = is_data_stale(latest_date, threshold_bdays=2)
|
|
617
|
+
if stale_data:
|
|
618
|
+
print(f" WARNING: Uptrend data is stale (latest: {latest_date})",
|
|
619
|
+
file=sys.stderr)
|
|
620
|
+
metadata["data_sources"]["uptrend_sectors"] = len(sector_uptrend)
|
|
621
|
+
metadata["data_sources"]["uptrend_stale"] = stale_data
|
|
622
|
+
else:
|
|
623
|
+
print(" WARNING: Uptrend data unavailable", file=sys.stderr)
|
|
624
|
+
metadata["data_sources"]["uptrend_error"] = "fetch failed"
|
|
625
|
+
|
|
626
|
+
# -----------------------------------------------------------------------
|
|
627
|
+
# Step 8: Score each theme
|
|
628
|
+
# -----------------------------------------------------------------------
|
|
629
|
+
print("Scoring themes...", file=sys.stderr)
|
|
630
|
+
scored_themes = []
|
|
631
|
+
|
|
632
|
+
for idx, theme in enumerate(themes):
|
|
633
|
+
theme_name = theme["theme_name"]
|
|
634
|
+
direction = theme["direction"]
|
|
635
|
+
is_bearish = direction == "bearish"
|
|
636
|
+
stocks = theme_stocks.get(idx, [])
|
|
637
|
+
|
|
638
|
+
# --- Theme Heat ---
|
|
639
|
+
# Momentum: average weighted_return of matching industries
|
|
640
|
+
theme_wr = _get_theme_weighted_return(theme)
|
|
641
|
+
momentum = momentum_strength_score(theme_wr)
|
|
642
|
+
|
|
643
|
+
# Volume: average ETF volume ratio
|
|
644
|
+
etf_vol_ratios = []
|
|
645
|
+
for etf_sym in theme.get("proxy_etfs", []):
|
|
646
|
+
vol = etf_volume_map.get(etf_sym, {})
|
|
647
|
+
if vol.get("vol_20d") is not None and vol.get("vol_60d") is not None:
|
|
648
|
+
etf_vol_ratios.append((vol["vol_20d"], vol["vol_60d"]))
|
|
649
|
+
|
|
650
|
+
if etf_vol_ratios:
|
|
651
|
+
avg_20d = sum(r[0] for r in etf_vol_ratios) / len(etf_vol_ratios)
|
|
652
|
+
avg_60d = sum(r[1] for r in etf_vol_ratios) / len(etf_vol_ratios)
|
|
653
|
+
volume = volume_intensity_score(avg_20d, avg_60d)
|
|
654
|
+
else:
|
|
655
|
+
volume = None # defaults to 50
|
|
656
|
+
|
|
657
|
+
# Uptrend signal
|
|
658
|
+
sector_data = _get_theme_uptrend_data(theme, sector_uptrend)
|
|
659
|
+
if sector_data:
|
|
660
|
+
uptrend = uptrend_signal_score(sector_data, is_bearish)
|
|
661
|
+
else:
|
|
662
|
+
uptrend = None # defaults to 50
|
|
663
|
+
|
|
664
|
+
# Breadth signal
|
|
665
|
+
breadth_ratio = _calculate_breadth_ratio(theme)
|
|
666
|
+
breadth = breadth_signal_score(breadth_ratio)
|
|
667
|
+
|
|
668
|
+
heat = calculate_theme_heat(momentum, volume, uptrend, breadth)
|
|
669
|
+
|
|
670
|
+
heat_breakdown = {
|
|
671
|
+
"momentum_strength": round(momentum, 2),
|
|
672
|
+
"volume_intensity": round(volume, 2) if volume is not None else 50.0,
|
|
673
|
+
"uptrend_signal": round(uptrend, 2) if uptrend is not None else 50.0,
|
|
674
|
+
"breadth_signal": round(breadth, 2),
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
# --- Lifecycle Maturity ---
|
|
678
|
+
# Get stock-level metrics for this theme
|
|
679
|
+
theme_stock_metrics = [
|
|
680
|
+
stock_metrics_map[s] for s in stocks if s in stock_metrics_map
|
|
681
|
+
]
|
|
682
|
+
|
|
683
|
+
# Remap keys: rsi_14 -> rsi (lifecycle_calculator expects "rsi")
|
|
684
|
+
for sm in theme_stock_metrics:
|
|
685
|
+
if "rsi_14" in sm:
|
|
686
|
+
sm["rsi"] = sm["rsi_14"]
|
|
687
|
+
|
|
688
|
+
# Duration: from industry performance timeframes
|
|
689
|
+
avg_perfs = _average_industry_perfs(theme.get("matching_industries", []))
|
|
690
|
+
duration = estimate_duration_score(
|
|
691
|
+
avg_perfs.get("perf_1m"),
|
|
692
|
+
avg_perfs.get("perf_3m"),
|
|
693
|
+
avg_perfs.get("perf_6m"),
|
|
694
|
+
avg_perfs.get("perf_1y"),
|
|
695
|
+
is_bearish,
|
|
696
|
+
)
|
|
697
|
+
|
|
698
|
+
# Extremity clustering
|
|
699
|
+
extremity = extremity_clustering_score(theme_stock_metrics, is_bearish)
|
|
700
|
+
|
|
701
|
+
# Price extreme saturation
|
|
702
|
+
price_extreme = price_extreme_saturation_score(theme_stock_metrics, is_bearish)
|
|
703
|
+
|
|
704
|
+
# Valuation premium
|
|
705
|
+
valuation = valuation_premium_score(theme_stock_metrics)
|
|
706
|
+
|
|
707
|
+
# ETF proliferation
|
|
708
|
+
etf_count = etf_catalog.get(theme_name, 0)
|
|
709
|
+
etf_prolif = etf_proliferation_score(etf_count)
|
|
710
|
+
|
|
711
|
+
maturity = calculate_lifecycle_maturity(
|
|
712
|
+
duration, extremity, price_extreme, valuation, etf_prolif
|
|
713
|
+
)
|
|
714
|
+
stage = classify_stage(maturity)
|
|
715
|
+
|
|
716
|
+
maturity_breakdown = {
|
|
717
|
+
"duration_estimate": round(duration, 2),
|
|
718
|
+
"extremity_clustering": round(extremity, 2),
|
|
719
|
+
"price_extreme_saturation": round(price_extreme, 2),
|
|
720
|
+
"valuation_premium": round(valuation, 2),
|
|
721
|
+
"etf_proliferation": round(etf_prolif, 2),
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
# --- Confidence ---
|
|
725
|
+
quant_confirmed = momentum > 50
|
|
726
|
+
breadth_confirmed = (uptrend is not None and uptrend > 55) if uptrend else False
|
|
727
|
+
narrative_confirmed = False # Pending Claude WebSearch
|
|
728
|
+
confidence = calculate_confidence(
|
|
729
|
+
quant_confirmed, breadth_confirmed, narrative_confirmed, stale_data
|
|
730
|
+
)
|
|
731
|
+
|
|
732
|
+
# --- Score theme ---
|
|
733
|
+
score = score_theme(
|
|
734
|
+
round(heat, 2),
|
|
735
|
+
round(maturity, 2),
|
|
736
|
+
stage,
|
|
737
|
+
direction,
|
|
738
|
+
confidence,
|
|
739
|
+
data_mode,
|
|
740
|
+
)
|
|
741
|
+
|
|
742
|
+
# Build full theme result
|
|
743
|
+
scored_theme = {
|
|
744
|
+
"name": theme_name,
|
|
745
|
+
"direction": direction,
|
|
746
|
+
"heat": round(heat, 2),
|
|
747
|
+
"maturity": round(maturity, 2),
|
|
748
|
+
"stage": stage,
|
|
749
|
+
"confidence": confidence,
|
|
750
|
+
"heat_label": score["heat_label"],
|
|
751
|
+
"heat_breakdown": heat_breakdown,
|
|
752
|
+
"maturity_breakdown": maturity_breakdown,
|
|
753
|
+
"representative_stocks": stocks,
|
|
754
|
+
"stock_details": theme_stock_details.get(idx, []),
|
|
755
|
+
"proxy_etfs": theme.get("proxy_etfs", []),
|
|
756
|
+
"industries": [ind.get("name", "") for ind in
|
|
757
|
+
theme.get("matching_industries", [])],
|
|
758
|
+
"sector_weights": theme.get("sector_weights", {}),
|
|
759
|
+
"stock_data": "available" if theme_stock_metrics else "unavailable",
|
|
760
|
+
"data_mode": data_mode,
|
|
761
|
+
"stale_data_penalty": stale_data,
|
|
762
|
+
"theme_origin": theme.get("theme_origin", "seed"),
|
|
763
|
+
"name_confidence": theme.get("name_confidence", "high"),
|
|
764
|
+
}
|
|
765
|
+
scored_themes.append(scored_theme)
|
|
766
|
+
|
|
767
|
+
# Sort by heat descending
|
|
768
|
+
scored_themes.sort(key=lambda t: t["heat"], reverse=True)
|
|
769
|
+
|
|
770
|
+
# -----------------------------------------------------------------------
|
|
771
|
+
# Step 9: Generate reports
|
|
772
|
+
# -----------------------------------------------------------------------
|
|
773
|
+
print("Generating reports...", file=sys.stderr)
|
|
774
|
+
|
|
775
|
+
json_report = generate_json_report(
|
|
776
|
+
scored_themes, industry_rankings, sector_uptrend, metadata
|
|
777
|
+
)
|
|
778
|
+
md_report = generate_markdown_report(json_report, top_n_detail=args.top)
|
|
779
|
+
|
|
780
|
+
# Resolve output directory relative to repo root if relative
|
|
781
|
+
output_dir = args.output_dir
|
|
782
|
+
if not os.path.isabs(output_dir):
|
|
783
|
+
# Look for reports/ relative to repo root
|
|
784
|
+
repo_root = os.path.dirname(os.path.dirname(os.path.dirname(
|
|
785
|
+
os.path.dirname(os.path.abspath(__file__))
|
|
786
|
+
)))
|
|
787
|
+
output_dir = os.path.join(repo_root, output_dir)
|
|
788
|
+
|
|
789
|
+
paths = save_reports(json_report, md_report, output_dir)
|
|
790
|
+
|
|
791
|
+
elapsed = time.time() - start_time
|
|
792
|
+
print(f"\nDone in {elapsed:.1f}s", file=sys.stderr)
|
|
793
|
+
print(f" JSON: {paths['json']}", file=sys.stderr)
|
|
794
|
+
print(f" Markdown: {paths['markdown']}", file=sys.stderr)
|
|
795
|
+
print(f" Themes: {len(scored_themes)}", file=sys.stderr)
|
|
796
|
+
|
|
797
|
+
# Print JSON to stdout for programmatic consumption
|
|
798
|
+
print(json.dumps(json_report, indent=2, default=str))
|
|
799
|
+
|
|
800
|
+
|
|
801
|
+
def _average_industry_perfs(industries: List[Dict]) -> Dict:
|
|
802
|
+
"""Average performance across theme's matching industries."""
|
|
803
|
+
if not industries:
|
|
804
|
+
return {}
|
|
805
|
+
|
|
806
|
+
perf_keys = ["perf_1m", "perf_3m", "perf_6m", "perf_1y"]
|
|
807
|
+
result = {}
|
|
808
|
+
for key in perf_keys:
|
|
809
|
+
vals = [ind.get(key) for ind in industries if ind.get(key) is not None]
|
|
810
|
+
result[key] = sum(vals) / len(vals) if vals else None
|
|
811
|
+
return result
|
|
812
|
+
|
|
813
|
+
|
|
814
|
+
if __name__ == "__main__":
|
|
815
|
+
main()
|