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
package/.claude/skills/dividend-growth-pullback-screener/scripts/screen_dividend_growth_rsi.py
ADDED
|
@@ -0,0 +1,1155 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Dividend Growth Pullback Screener using FINVIZ + Financial Modeling Prep API
|
|
4
|
+
|
|
5
|
+
Two-stage screening approach:
|
|
6
|
+
1. FINVIZ Elite API: Pre-screen stocks with dividend growth + RSI criteria (fast, cost-effective)
|
|
7
|
+
2. FMP API: Detailed analysis of pre-screened candidates (comprehensive)
|
|
8
|
+
|
|
9
|
+
Screens for high-quality dividend growth stocks (12%+ dividend CAGR, 1.5%+ yield)
|
|
10
|
+
that are experiencing temporary pullbacks identified by RSI oversold conditions (RSI ≤40).
|
|
11
|
+
|
|
12
|
+
Usage:
|
|
13
|
+
# Two-stage screening with FINVIZ (RECOMMENDED)
|
|
14
|
+
python3 screen_dividend_growth_rsi.py --use-finviz
|
|
15
|
+
|
|
16
|
+
# FMP-only screening (original method)
|
|
17
|
+
python3 screen_dividend_growth_rsi.py
|
|
18
|
+
|
|
19
|
+
Environment variables:
|
|
20
|
+
export FMP_API_KEY=your_fmp_key_here
|
|
21
|
+
export FINVIZ_API_KEY=your_finviz_key_here # Required for --use-finviz
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
import argparse
|
|
25
|
+
import csv
|
|
26
|
+
import io
|
|
27
|
+
import json
|
|
28
|
+
import os
|
|
29
|
+
import sys
|
|
30
|
+
import time
|
|
31
|
+
from datetime import datetime, date
|
|
32
|
+
from typing import Dict, List, Optional, Tuple, Set
|
|
33
|
+
import requests
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class FINVIZClient:
|
|
37
|
+
"""Client for FINVIZ Elite API"""
|
|
38
|
+
|
|
39
|
+
BASE_URL = "https://elite.finviz.com/export.ashx"
|
|
40
|
+
|
|
41
|
+
def __init__(self, api_key: str):
|
|
42
|
+
self.api_key = api_key
|
|
43
|
+
self.session = requests.Session()
|
|
44
|
+
|
|
45
|
+
def screen_stocks(self) -> Set[str]:
|
|
46
|
+
"""
|
|
47
|
+
Screen stocks using FINVIZ Elite API with predefined criteria
|
|
48
|
+
|
|
49
|
+
Criteria for dividend growth pullback opportunities (Balanced):
|
|
50
|
+
- Market cap: Mid-cap or higher
|
|
51
|
+
- Dividend yield: 0.5-3% (captures dividend growers without REITs/utilities)
|
|
52
|
+
- Dividend growth (3Y): 10%+ (we'll verify 12%+ with FMP)
|
|
53
|
+
- EPS growth (3Y): 5%+ (positive earnings momentum)
|
|
54
|
+
- Sales growth (3Y): 5%+ (positive revenue momentum)
|
|
55
|
+
- RSI (14): Under 40 (oversold/pullback)
|
|
56
|
+
- Geography: USA
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
Set of stock symbols
|
|
60
|
+
"""
|
|
61
|
+
# Build filter string in FINVIZ format: key_value,key_value,...
|
|
62
|
+
# Balanced criteria: Div Growth 10%+, EPS/Sales Growth 5%+ (30-40 candidates expected)
|
|
63
|
+
filters = 'cap_midover,fa_div_0.5to3,fa_divgrowth_3yo10,fa_eps3years_o5,fa_sales3years_o5,geo_usa,ta_rsi_os40'
|
|
64
|
+
|
|
65
|
+
params = {
|
|
66
|
+
'v': '151', # View type
|
|
67
|
+
'f': filters, # Filter conditions
|
|
68
|
+
'ft': '4', # File type: CSV export
|
|
69
|
+
'auth': self.api_key
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
try:
|
|
73
|
+
print(f"Fetching pre-screened stocks from FINVIZ Elite API...", file=sys.stderr)
|
|
74
|
+
print(f"FINVIZ Filters: Div Yield 0.5-3%, Div Growth 10%+, EPS Growth 5%+, Sales Growth 5%+, RSI <40", file=sys.stderr)
|
|
75
|
+
response = self.session.get(self.BASE_URL, params=params, timeout=30)
|
|
76
|
+
|
|
77
|
+
if response.status_code == 200:
|
|
78
|
+
# Parse CSV response
|
|
79
|
+
csv_content = response.content.decode('utf-8')
|
|
80
|
+
reader = csv.DictReader(io.StringIO(csv_content))
|
|
81
|
+
|
|
82
|
+
symbols = set()
|
|
83
|
+
for row in reader:
|
|
84
|
+
# FINVIZ CSV has 'Ticker' column
|
|
85
|
+
ticker = row.get('Ticker', '').strip()
|
|
86
|
+
if ticker:
|
|
87
|
+
symbols.add(ticker)
|
|
88
|
+
|
|
89
|
+
print(f"✅ FINVIZ returned {len(symbols)} pre-screened stocks", file=sys.stderr)
|
|
90
|
+
return symbols
|
|
91
|
+
|
|
92
|
+
elif response.status_code == 401 or response.status_code == 403:
|
|
93
|
+
print(f"ERROR: FINVIZ API authentication failed. Check your API key.", file=sys.stderr)
|
|
94
|
+
print(f"Status code: {response.status_code}", file=sys.stderr)
|
|
95
|
+
return set()
|
|
96
|
+
else:
|
|
97
|
+
print(f"ERROR: FINVIZ API request failed: {response.status_code}", file=sys.stderr)
|
|
98
|
+
return set()
|
|
99
|
+
|
|
100
|
+
except requests.exceptions.RequestException as e:
|
|
101
|
+
print(f"ERROR: FINVIZ request exception: {e}", file=sys.stderr)
|
|
102
|
+
return set()
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class FMPClient:
|
|
106
|
+
"""Financial Modeling Prep API client with rate limiting."""
|
|
107
|
+
|
|
108
|
+
BASE_URL = "https://financialmodelingprep.com/api/v3"
|
|
109
|
+
|
|
110
|
+
def __init__(self, api_key: str):
|
|
111
|
+
self.api_key = api_key
|
|
112
|
+
self.session = requests.Session()
|
|
113
|
+
self.rate_limit_reached = False
|
|
114
|
+
self.retry_count = 0
|
|
115
|
+
|
|
116
|
+
def _get(self, endpoint: str, params: Optional[Dict] = None) -> Optional[Dict]:
|
|
117
|
+
"""Execute GET request with rate limiting and error handling."""
|
|
118
|
+
if self.rate_limit_reached:
|
|
119
|
+
return None
|
|
120
|
+
|
|
121
|
+
if params is None:
|
|
122
|
+
params = {}
|
|
123
|
+
params['apikey'] = self.api_key
|
|
124
|
+
|
|
125
|
+
url = f"{self.BASE_URL}/{endpoint}"
|
|
126
|
+
|
|
127
|
+
try:
|
|
128
|
+
response = self.session.get(url, params=params, timeout=30)
|
|
129
|
+
|
|
130
|
+
if response.status_code == 200:
|
|
131
|
+
self.retry_count = 0
|
|
132
|
+
time.sleep(0.3) # Rate limiting: 0.3s between requests
|
|
133
|
+
return response.json()
|
|
134
|
+
elif response.status_code == 429:
|
|
135
|
+
self.retry_count += 1
|
|
136
|
+
if self.retry_count <= 1:
|
|
137
|
+
print(f"WARNING: Rate limit exceeded. Waiting 60 seconds...", file=sys.stderr)
|
|
138
|
+
time.sleep(60)
|
|
139
|
+
return self._get(endpoint, params)
|
|
140
|
+
else:
|
|
141
|
+
print(f"ERROR: Daily API rate limit reached. Stopping analysis.", file=sys.stderr)
|
|
142
|
+
self.rate_limit_reached = True
|
|
143
|
+
return None
|
|
144
|
+
else:
|
|
145
|
+
print(f"WARNING: API request failed ({response.status_code}): {url}", file=sys.stderr)
|
|
146
|
+
return None
|
|
147
|
+
except Exception as e:
|
|
148
|
+
print(f"ERROR: Request failed for {url}: {e}", file=sys.stderr)
|
|
149
|
+
return None
|
|
150
|
+
|
|
151
|
+
def screen_stocks(self, min_market_cap: int = 2000000000, exchange: str = None) -> List[Dict]:
|
|
152
|
+
"""Screen stocks by market cap and exchange."""
|
|
153
|
+
params = {'marketCapMoreThan': min_market_cap}
|
|
154
|
+
if exchange:
|
|
155
|
+
params['exchange'] = exchange
|
|
156
|
+
|
|
157
|
+
result = self._get('stock-screener', params)
|
|
158
|
+
return result if result else []
|
|
159
|
+
|
|
160
|
+
def get_historical_prices(self, symbol: str, days: int = 30) -> Optional[List[Dict]]:
|
|
161
|
+
"""Get historical daily prices."""
|
|
162
|
+
result = self._get(f'historical-price-full/{symbol}', {'timeseries': days})
|
|
163
|
+
if result and 'historical' in result:
|
|
164
|
+
return result['historical']
|
|
165
|
+
return None
|
|
166
|
+
|
|
167
|
+
def get_dividend_history(self, symbol: str) -> Optional[Dict]:
|
|
168
|
+
"""Get historical dividend payments."""
|
|
169
|
+
result = self._get(f'historical-price-full/stock_dividend/{symbol}')
|
|
170
|
+
return result
|
|
171
|
+
|
|
172
|
+
def get_income_statement(self, symbol: str, limit: int = 5) -> Optional[List[Dict]]:
|
|
173
|
+
"""Get income statement data."""
|
|
174
|
+
result = self._get(f'income-statement/{symbol}', {'limit': limit})
|
|
175
|
+
return result if result else []
|
|
176
|
+
|
|
177
|
+
def get_balance_sheet(self, symbol: str, limit: int = 5) -> Optional[List[Dict]]:
|
|
178
|
+
"""Get balance sheet data."""
|
|
179
|
+
result = self._get(f'balance-sheet-statement/{symbol}', {'limit': limit})
|
|
180
|
+
return result if result else []
|
|
181
|
+
|
|
182
|
+
def get_cash_flow(self, symbol: str, limit: int = 5) -> Optional[List[Dict]]:
|
|
183
|
+
"""Get cash flow statement data."""
|
|
184
|
+
result = self._get(f'cash-flow-statement/{symbol}', {'limit': limit})
|
|
185
|
+
return result if result else []
|
|
186
|
+
|
|
187
|
+
def get_key_metrics(self, symbol: str, limit: int = 5) -> Optional[List[Dict]]:
|
|
188
|
+
"""Get key financial metrics."""
|
|
189
|
+
result = self._get(f'key-metrics/{symbol}', {'limit': limit})
|
|
190
|
+
return result if result else []
|
|
191
|
+
|
|
192
|
+
def get_company_profile(self, symbol: str) -> Optional[Dict]:
|
|
193
|
+
"""Get company profile including sector information."""
|
|
194
|
+
result = self._get(f'profile/{symbol}')
|
|
195
|
+
if result and isinstance(result, list) and len(result) > 0:
|
|
196
|
+
return result[0]
|
|
197
|
+
return None
|
|
198
|
+
|
|
199
|
+
def get_quote_with_profile(self, symbol: str) -> Optional[Dict]:
|
|
200
|
+
"""
|
|
201
|
+
Get quote data merged with profile data to include sector information.
|
|
202
|
+
|
|
203
|
+
Returns:
|
|
204
|
+
Dict with quote data + sector/companyName from profile, or None on error
|
|
205
|
+
"""
|
|
206
|
+
# First get quote data
|
|
207
|
+
quote = self._get(f'quote/{symbol}')
|
|
208
|
+
if not quote or not isinstance(quote, list) or len(quote) == 0:
|
|
209
|
+
return None
|
|
210
|
+
|
|
211
|
+
quote_data = quote[0].copy()
|
|
212
|
+
|
|
213
|
+
# Then get profile for sector information
|
|
214
|
+
profile = self.get_company_profile(symbol)
|
|
215
|
+
if profile:
|
|
216
|
+
# Merge profile data into quote (profile has more accurate sector/companyName)
|
|
217
|
+
quote_data['sector'] = profile.get('sector', 'Unknown')
|
|
218
|
+
quote_data['companyName'] = profile.get('companyName', quote_data.get('name', ''))
|
|
219
|
+
quote_data['industry'] = profile.get('industry', '')
|
|
220
|
+
else:
|
|
221
|
+
# Fallback if profile fetch fails
|
|
222
|
+
quote_data['sector'] = quote_data.get('sector', 'Unknown')
|
|
223
|
+
quote_data['companyName'] = quote_data.get('name', quote_data.get('companyName', ''))
|
|
224
|
+
|
|
225
|
+
return quote_data
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
class RSICalculator:
|
|
229
|
+
"""Calculate Relative Strength Index (RSI) from price data."""
|
|
230
|
+
|
|
231
|
+
@staticmethod
|
|
232
|
+
def calculate_rsi(prices: List[float], period: int = 14) -> Optional[float]:
|
|
233
|
+
"""
|
|
234
|
+
Calculate RSI using standard formula.
|
|
235
|
+
|
|
236
|
+
Args:
|
|
237
|
+
prices: List of closing prices (oldest first)
|
|
238
|
+
period: RSI period (default 14)
|
|
239
|
+
|
|
240
|
+
Returns:
|
|
241
|
+
RSI value (0-100) or None if insufficient data
|
|
242
|
+
"""
|
|
243
|
+
if len(prices) < period + 1:
|
|
244
|
+
return None
|
|
245
|
+
|
|
246
|
+
# Calculate price changes
|
|
247
|
+
changes = [prices[i] - prices[i-1] for i in range(1, len(prices))]
|
|
248
|
+
|
|
249
|
+
# Separate gains and losses
|
|
250
|
+
gains = [change if change > 0 else 0 for change in changes]
|
|
251
|
+
losses = [-change if change < 0 else 0 for change in changes]
|
|
252
|
+
|
|
253
|
+
# Calculate initial average gain and loss
|
|
254
|
+
avg_gain = sum(gains[:period]) / period
|
|
255
|
+
avg_loss = sum(losses[:period]) / period
|
|
256
|
+
|
|
257
|
+
# Calculate smoothed averages for remaining periods
|
|
258
|
+
for i in range(period, len(gains)):
|
|
259
|
+
avg_gain = (avg_gain * (period - 1) + gains[i]) / period
|
|
260
|
+
avg_loss = (avg_loss * (period - 1) + losses[i]) / period
|
|
261
|
+
|
|
262
|
+
# Calculate RSI
|
|
263
|
+
if avg_loss == 0:
|
|
264
|
+
return 100.0
|
|
265
|
+
|
|
266
|
+
rs = avg_gain / avg_loss
|
|
267
|
+
rsi = 100 - (100 / (1 + rs))
|
|
268
|
+
|
|
269
|
+
return round(rsi, 2)
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
class StockAnalyzer:
|
|
273
|
+
"""Analyze stock fundamentals and dividend growth."""
|
|
274
|
+
|
|
275
|
+
@staticmethod
|
|
276
|
+
def calculate_cagr(start_value: float, end_value: float, years: int) -> Optional[float]:
|
|
277
|
+
"""Calculate Compound Annual Growth Rate."""
|
|
278
|
+
if start_value <= 0 or end_value <= 0 or years <= 0:
|
|
279
|
+
return None
|
|
280
|
+
return round(((end_value / start_value) ** (1 / years) - 1) * 100, 2)
|
|
281
|
+
|
|
282
|
+
@staticmethod
|
|
283
|
+
def analyze_dividend_growth(dividend_history: List[Dict]) -> Tuple[Optional[float], bool, Optional[float], int]:
|
|
284
|
+
"""
|
|
285
|
+
Analyze dividend growth rate (3-year CAGR and consistency) and return latest annual dividend.
|
|
286
|
+
|
|
287
|
+
Returns:
|
|
288
|
+
Tuple of (CAGR%, consistent_growth, latest_annual_dividend, years_of_growth)
|
|
289
|
+
"""
|
|
290
|
+
if not dividend_history or 'historical' not in dividend_history:
|
|
291
|
+
return None, False, None, 0
|
|
292
|
+
|
|
293
|
+
dividends = dividend_history['historical']
|
|
294
|
+
if len(dividends) < 4:
|
|
295
|
+
return None, False, None, 0
|
|
296
|
+
|
|
297
|
+
# Sort by date and aggregate by year
|
|
298
|
+
dividends = sorted(dividends, key=lambda x: x['date'])
|
|
299
|
+
annual_dividends = {}
|
|
300
|
+
for div in dividends:
|
|
301
|
+
year = div['date'][:4]
|
|
302
|
+
annual_dividends[year] = annual_dividends.get(year, 0) + div.get('dividend', 0)
|
|
303
|
+
|
|
304
|
+
if len(annual_dividends) < 4:
|
|
305
|
+
return None, False, None, 0
|
|
306
|
+
|
|
307
|
+
# Get all available years sorted (oldest first)
|
|
308
|
+
all_years = sorted(annual_dividends.keys())
|
|
309
|
+
all_div_values = [annual_dividends[y] for y in all_years]
|
|
310
|
+
|
|
311
|
+
# Get last 4 years for CAGR calculation
|
|
312
|
+
years = all_years[-4:]
|
|
313
|
+
div_values = [annual_dividends[y] for y in years]
|
|
314
|
+
|
|
315
|
+
# Calculate 3-year CAGR
|
|
316
|
+
cagr = StockAnalyzer.calculate_cagr(div_values[0], div_values[-1], 3)
|
|
317
|
+
|
|
318
|
+
# Check consistency (no significant cuts)
|
|
319
|
+
consistent = all(div_values[i] >= div_values[i-1] * 0.95 for i in range(1, len(div_values)))
|
|
320
|
+
|
|
321
|
+
# Count consecutive years of growth (from most recent going back)
|
|
322
|
+
years_of_growth = 0
|
|
323
|
+
for i in range(len(all_div_values) - 1, 0, -1):
|
|
324
|
+
if all_div_values[i] >= all_div_values[i-1] * 0.95: # Allow 5% tolerance
|
|
325
|
+
years_of_growth += 1
|
|
326
|
+
else:
|
|
327
|
+
break
|
|
328
|
+
|
|
329
|
+
# Latest annual dividend
|
|
330
|
+
latest_annual_dividend = div_values[-1]
|
|
331
|
+
|
|
332
|
+
return cagr, consistent, latest_annual_dividend, years_of_growth
|
|
333
|
+
|
|
334
|
+
@staticmethod
|
|
335
|
+
def is_reit(stock_data: Dict) -> bool:
|
|
336
|
+
"""
|
|
337
|
+
Determine if a stock is a REIT based on sector/industry.
|
|
338
|
+
|
|
339
|
+
Args:
|
|
340
|
+
stock_data: Dict containing sector and/or industry fields
|
|
341
|
+
|
|
342
|
+
Returns:
|
|
343
|
+
True if the stock is likely a REIT
|
|
344
|
+
"""
|
|
345
|
+
sector = stock_data.get('sector', '').lower()
|
|
346
|
+
industry = stock_data.get('industry', '').lower()
|
|
347
|
+
|
|
348
|
+
# Check for Real Estate sector or REIT in industry
|
|
349
|
+
if 'real estate' in sector:
|
|
350
|
+
return True
|
|
351
|
+
if 'reit' in industry:
|
|
352
|
+
return True
|
|
353
|
+
|
|
354
|
+
return False
|
|
355
|
+
|
|
356
|
+
@staticmethod
|
|
357
|
+
def calculate_ffo(cash_flows: List[Dict]) -> Optional[float]:
|
|
358
|
+
"""
|
|
359
|
+
Calculate Funds From Operations (FFO) for REITs.
|
|
360
|
+
|
|
361
|
+
FFO = Net Income + Depreciation & Amortization
|
|
362
|
+
(Simplified formula - does not include gains/losses on property sales)
|
|
363
|
+
|
|
364
|
+
Args:
|
|
365
|
+
cash_flows: List of cash flow statements (newest first)
|
|
366
|
+
|
|
367
|
+
Returns:
|
|
368
|
+
FFO value or None if data is missing
|
|
369
|
+
"""
|
|
370
|
+
if not cash_flows:
|
|
371
|
+
return None
|
|
372
|
+
|
|
373
|
+
latest_cf = cash_flows[0]
|
|
374
|
+
net_income = latest_cf.get('netIncome', 0)
|
|
375
|
+
depreciation = latest_cf.get('depreciationAndAmortization', 0)
|
|
376
|
+
|
|
377
|
+
if net_income == 0 and depreciation == 0:
|
|
378
|
+
return None
|
|
379
|
+
|
|
380
|
+
return net_income + depreciation
|
|
381
|
+
|
|
382
|
+
@staticmethod
|
|
383
|
+
def calculate_ffo_payout_ratio(cash_flows: List[Dict]) -> Optional[float]:
|
|
384
|
+
"""
|
|
385
|
+
Calculate FFO payout ratio for REITs.
|
|
386
|
+
|
|
387
|
+
FFO Payout Ratio = Dividends Paid / FFO
|
|
388
|
+
|
|
389
|
+
Args:
|
|
390
|
+
cash_flows: List of cash flow statements (newest first)
|
|
391
|
+
|
|
392
|
+
Returns:
|
|
393
|
+
FFO payout ratio as percentage, or None if calculation fails
|
|
394
|
+
"""
|
|
395
|
+
if not cash_flows:
|
|
396
|
+
return None
|
|
397
|
+
|
|
398
|
+
ffo = StockAnalyzer.calculate_ffo(cash_flows)
|
|
399
|
+
if not ffo or ffo <= 0:
|
|
400
|
+
return None
|
|
401
|
+
|
|
402
|
+
latest_cf = cash_flows[0]
|
|
403
|
+
dividends_paid = abs(latest_cf.get('dividendsPaid', 0))
|
|
404
|
+
|
|
405
|
+
if dividends_paid <= 0:
|
|
406
|
+
return None
|
|
407
|
+
|
|
408
|
+
return round((dividends_paid / ffo) * 100, 1)
|
|
409
|
+
|
|
410
|
+
@staticmethod
|
|
411
|
+
def calculate_payout_ratios(income_stmts: List[Dict], cash_flows: List[Dict], is_reit: bool = False) -> Dict:
|
|
412
|
+
"""
|
|
413
|
+
Calculate payout ratios using dividendsPaid from cash flow statement.
|
|
414
|
+
|
|
415
|
+
For REITs, uses FFO-based payout ratio instead of net income-based.
|
|
416
|
+
|
|
417
|
+
Args:
|
|
418
|
+
income_stmts: List of income statements (newest first)
|
|
419
|
+
cash_flows: List of cash flow statements (newest first)
|
|
420
|
+
is_reit: Whether the stock is a REIT (uses FFO for payout calculation)
|
|
421
|
+
|
|
422
|
+
Returns:
|
|
423
|
+
Dict with payout_ratio and fcf_payout_ratio (as percentages)
|
|
424
|
+
"""
|
|
425
|
+
result = {
|
|
426
|
+
'payout_ratio': None,
|
|
427
|
+
'fcf_payout_ratio': None
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
if not cash_flows:
|
|
431
|
+
return result
|
|
432
|
+
|
|
433
|
+
latest_cf = cash_flows[0]
|
|
434
|
+
dividends_paid = abs(latest_cf.get('dividendsPaid', 0))
|
|
435
|
+
fcf = latest_cf.get('freeCashFlow', 0)
|
|
436
|
+
|
|
437
|
+
# For REITs, use FFO-based payout ratio
|
|
438
|
+
if is_reit:
|
|
439
|
+
result['payout_ratio'] = StockAnalyzer.calculate_ffo_payout_ratio(cash_flows)
|
|
440
|
+
else:
|
|
441
|
+
# For non-REITs, use traditional net income-based payout ratio
|
|
442
|
+
if income_stmts:
|
|
443
|
+
latest_income = income_stmts[0]
|
|
444
|
+
net_income = latest_income.get('netIncome', 0)
|
|
445
|
+
|
|
446
|
+
if net_income > 0 and dividends_paid > 0:
|
|
447
|
+
result['payout_ratio'] = round((dividends_paid / net_income) * 100, 1)
|
|
448
|
+
|
|
449
|
+
# Calculate FCF payout ratio (same for both REIT and non-REIT)
|
|
450
|
+
if fcf > 0 and dividends_paid > 0:
|
|
451
|
+
result['fcf_payout_ratio'] = round((dividends_paid / fcf) * 100, 1)
|
|
452
|
+
|
|
453
|
+
return result
|
|
454
|
+
|
|
455
|
+
@staticmethod
|
|
456
|
+
def get_payout_ratio_from_metrics(key_metrics: List[Dict]) -> Optional[float]:
|
|
457
|
+
"""
|
|
458
|
+
Get payout ratio directly from key_metrics as fallback.
|
|
459
|
+
|
|
460
|
+
Args:
|
|
461
|
+
key_metrics: List of key metrics (newest first)
|
|
462
|
+
|
|
463
|
+
Returns:
|
|
464
|
+
Payout ratio as percentage, or None if not available
|
|
465
|
+
"""
|
|
466
|
+
if not key_metrics:
|
|
467
|
+
return None
|
|
468
|
+
|
|
469
|
+
latest = key_metrics[0]
|
|
470
|
+
payout_ratio = latest.get('payoutRatio')
|
|
471
|
+
|
|
472
|
+
if payout_ratio is not None:
|
|
473
|
+
# payoutRatio from FMP is a decimal (e.g., 0.316 = 31.6%)
|
|
474
|
+
return round(payout_ratio * 100, 1)
|
|
475
|
+
|
|
476
|
+
return None
|
|
477
|
+
|
|
478
|
+
@staticmethod
|
|
479
|
+
def analyze_financial_health(balance_sheet: List[Dict]) -> Dict:
|
|
480
|
+
"""Analyze financial health metrics."""
|
|
481
|
+
if not balance_sheet:
|
|
482
|
+
return {}
|
|
483
|
+
|
|
484
|
+
latest = balance_sheet[0]
|
|
485
|
+
|
|
486
|
+
total_debt = latest.get('totalDebt', 0)
|
|
487
|
+
total_equity = latest.get('totalStockholdersEquity', 0)
|
|
488
|
+
current_assets = latest.get('totalCurrentAssets', 0)
|
|
489
|
+
current_liabilities = latest.get('totalCurrentLiabilities', 0)
|
|
490
|
+
|
|
491
|
+
debt_to_equity = round(total_debt / total_equity, 2) if total_equity > 0 else None
|
|
492
|
+
current_ratio = round(current_assets / current_liabilities, 2) if current_liabilities > 0 else None
|
|
493
|
+
|
|
494
|
+
financially_healthy = (
|
|
495
|
+
(debt_to_equity is None or debt_to_equity < 2.0) and
|
|
496
|
+
(current_ratio is None or current_ratio > 1.0)
|
|
497
|
+
)
|
|
498
|
+
|
|
499
|
+
return {
|
|
500
|
+
'debt_to_equity': debt_to_equity,
|
|
501
|
+
'current_ratio': current_ratio,
|
|
502
|
+
'financially_healthy': financially_healthy
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
@staticmethod
|
|
506
|
+
def analyze_growth_metrics(income_stmts: List[Dict]) -> Dict:
|
|
507
|
+
"""Analyze revenue and EPS growth trends."""
|
|
508
|
+
if not income_stmts or len(income_stmts) < 4:
|
|
509
|
+
return {'revenue_cagr_3y': None, 'eps_cagr_3y': None}
|
|
510
|
+
|
|
511
|
+
# Sort by date (newest first from API)
|
|
512
|
+
revenue_3y_ago = income_stmts[3].get('revenue', 0)
|
|
513
|
+
revenue_latest = income_stmts[0].get('revenue', 0)
|
|
514
|
+
|
|
515
|
+
eps_3y_ago = income_stmts[3].get('eps', 0)
|
|
516
|
+
eps_latest = income_stmts[0].get('eps', 0)
|
|
517
|
+
|
|
518
|
+
revenue_cagr = StockAnalyzer.calculate_cagr(revenue_3y_ago, revenue_latest, 3)
|
|
519
|
+
eps_cagr = StockAnalyzer.calculate_cagr(eps_3y_ago, eps_latest, 3)
|
|
520
|
+
|
|
521
|
+
return {
|
|
522
|
+
'revenue_cagr_3y': revenue_cagr,
|
|
523
|
+
'eps_cagr_3y': eps_cagr
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
@staticmethod
|
|
527
|
+
def calculate_composite_score(stock_data: Dict) -> float:
|
|
528
|
+
"""
|
|
529
|
+
Calculate composite score (0-100) based on:
|
|
530
|
+
- Dividend Growth (40%): Reward higher CAGR
|
|
531
|
+
- Financial Quality (30%): ROE, profit margins, debt levels
|
|
532
|
+
- Technical Setup (20%): Lower RSI = better entry opportunity
|
|
533
|
+
- Valuation (10%): P/E and P/B for context
|
|
534
|
+
"""
|
|
535
|
+
score = 0.0
|
|
536
|
+
|
|
537
|
+
# Dividend Growth Score (40 points max)
|
|
538
|
+
div_cagr = stock_data.get('dividend_cagr_3y', 0)
|
|
539
|
+
if div_cagr >= 20:
|
|
540
|
+
score += 40
|
|
541
|
+
elif div_cagr >= 15:
|
|
542
|
+
score += 35
|
|
543
|
+
elif div_cagr >= 12:
|
|
544
|
+
score += 30
|
|
545
|
+
else:
|
|
546
|
+
score += 20
|
|
547
|
+
|
|
548
|
+
# Add bonus for consistency
|
|
549
|
+
if stock_data.get('dividend_consistent', False):
|
|
550
|
+
score += 5
|
|
551
|
+
|
|
552
|
+
# Financial Quality Score (30 points max)
|
|
553
|
+
roe = stock_data.get('roe', 0)
|
|
554
|
+
profit_margin = stock_data.get('profit_margin', 0)
|
|
555
|
+
debt_to_equity = stock_data.get('debt_to_equity', 999)
|
|
556
|
+
|
|
557
|
+
if roe >= 20:
|
|
558
|
+
score += 12
|
|
559
|
+
elif roe >= 15:
|
|
560
|
+
score += 10
|
|
561
|
+
elif roe >= 10:
|
|
562
|
+
score += 7
|
|
563
|
+
else:
|
|
564
|
+
score += 3
|
|
565
|
+
|
|
566
|
+
if profit_margin >= 20:
|
|
567
|
+
score += 10
|
|
568
|
+
elif profit_margin >= 15:
|
|
569
|
+
score += 8
|
|
570
|
+
elif profit_margin >= 10:
|
|
571
|
+
score += 6
|
|
572
|
+
else:
|
|
573
|
+
score += 3
|
|
574
|
+
|
|
575
|
+
if debt_to_equity < 0.5:
|
|
576
|
+
score += 8
|
|
577
|
+
elif debt_to_equity < 1.0:
|
|
578
|
+
score += 6
|
|
579
|
+
elif debt_to_equity < 2.0:
|
|
580
|
+
score += 3
|
|
581
|
+
|
|
582
|
+
# Technical Setup Score (20 points max) - Lower RSI = Higher score
|
|
583
|
+
rsi = stock_data.get('rsi', 50)
|
|
584
|
+
if rsi <= 25:
|
|
585
|
+
score += 20 # Extreme oversold
|
|
586
|
+
elif rsi <= 30:
|
|
587
|
+
score += 18
|
|
588
|
+
elif rsi <= 35:
|
|
589
|
+
score += 15
|
|
590
|
+
elif rsi <= 40:
|
|
591
|
+
score += 12
|
|
592
|
+
else:
|
|
593
|
+
score += 5
|
|
594
|
+
|
|
595
|
+
# Valuation Score (10 points max) - Context only, not exclusionary
|
|
596
|
+
pe_ratio = stock_data.get('pe_ratio', 999)
|
|
597
|
+
pb_ratio = stock_data.get('pb_ratio', 999)
|
|
598
|
+
|
|
599
|
+
if pe_ratio < 15:
|
|
600
|
+
score += 5
|
|
601
|
+
elif pe_ratio < 25:
|
|
602
|
+
score += 3
|
|
603
|
+
|
|
604
|
+
if pb_ratio < 3:
|
|
605
|
+
score += 5
|
|
606
|
+
elif pb_ratio < 5:
|
|
607
|
+
score += 3
|
|
608
|
+
|
|
609
|
+
return round(min(score, 100), 1)
|
|
610
|
+
|
|
611
|
+
|
|
612
|
+
def screen_dividend_growth_pullbacks(
|
|
613
|
+
api_key: str,
|
|
614
|
+
min_yield: float = 1.5,
|
|
615
|
+
min_div_growth: float = 12.0,
|
|
616
|
+
rsi_max: float = 40.0,
|
|
617
|
+
max_candidates: int = None,
|
|
618
|
+
finviz_symbols: Optional[Set[str]] = None
|
|
619
|
+
) -> List[Dict]:
|
|
620
|
+
"""
|
|
621
|
+
Main screening function.
|
|
622
|
+
|
|
623
|
+
Args:
|
|
624
|
+
api_key: FMP API key
|
|
625
|
+
min_yield: Minimum dividend yield % (default 1.5%)
|
|
626
|
+
min_div_growth: Minimum 3-year dividend CAGR % (default 12%)
|
|
627
|
+
rsi_max: Maximum RSI value (default 40)
|
|
628
|
+
max_candidates: Maximum number of candidates to analyze (None = all)
|
|
629
|
+
finviz_symbols: Optional set of symbols from FINVIZ pre-screening
|
|
630
|
+
|
|
631
|
+
Returns:
|
|
632
|
+
List of qualified stocks with full analysis
|
|
633
|
+
"""
|
|
634
|
+
client = FMPClient(api_key)
|
|
635
|
+
analyzer = StockAnalyzer()
|
|
636
|
+
rsi_calc = RSICalculator()
|
|
637
|
+
|
|
638
|
+
print(f"\n{'='*80}", file=sys.stderr)
|
|
639
|
+
print(f"Dividend Growth Pullback Screener", file=sys.stderr)
|
|
640
|
+
print(f"{'='*80}", file=sys.stderr)
|
|
641
|
+
print(f"\nCriteria:", file=sys.stderr)
|
|
642
|
+
print(f" - Dividend Yield ≥ {min_yield}%", file=sys.stderr)
|
|
643
|
+
print(f" - Dividend Growth (3Y CAGR) ≥ {min_div_growth}%", file=sys.stderr)
|
|
644
|
+
print(f" - RSI ≤ {rsi_max}", file=sys.stderr)
|
|
645
|
+
print(f" - Market Cap ≥ $2B", file=sys.stderr)
|
|
646
|
+
print(f" - Exchange: NYSE, NASDAQ", file=sys.stderr)
|
|
647
|
+
print(f"\n{'='*80}\n", file=sys.stderr)
|
|
648
|
+
|
|
649
|
+
# Step 1: Get candidate list
|
|
650
|
+
if finviz_symbols:
|
|
651
|
+
print(f"Step 1: Using FINVIZ pre-screened symbols ({len(finviz_symbols)} stocks)...", file=sys.stderr)
|
|
652
|
+
# Convert FINVIZ symbols to candidate format for FMP analysis
|
|
653
|
+
# We'll fetch quote data with profile to get sector information
|
|
654
|
+
candidates = []
|
|
655
|
+
print("Fetching quote and profile data from FMP for FINVIZ symbols...", file=sys.stderr)
|
|
656
|
+
for symbol in finviz_symbols:
|
|
657
|
+
stock_data = client.get_quote_with_profile(symbol)
|
|
658
|
+
if stock_data:
|
|
659
|
+
candidates.append(stock_data)
|
|
660
|
+
|
|
661
|
+
if client.rate_limit_reached:
|
|
662
|
+
print(f"⚠️ FMP rate limit reached while fetching quotes. Using {len(candidates)} symbols.", file=sys.stderr)
|
|
663
|
+
break
|
|
664
|
+
|
|
665
|
+
print(f"Retrieved quote and profile data for {len(candidates)} symbols from FMP", file=sys.stderr)
|
|
666
|
+
else:
|
|
667
|
+
print("Step 1: Initial screening using FMP Stock Screener...", file=sys.stderr)
|
|
668
|
+
candidates = client.screen_stocks(min_market_cap=2000000000)
|
|
669
|
+
print(f"Found {len(candidates)} initial candidates", file=sys.stderr)
|
|
670
|
+
|
|
671
|
+
if not candidates:
|
|
672
|
+
print("ERROR: No candidates found or API error", file=sys.stderr)
|
|
673
|
+
return []
|
|
674
|
+
|
|
675
|
+
# Limit candidates if specified
|
|
676
|
+
if max_candidates and not finviz_symbols:
|
|
677
|
+
candidates = candidates[:max_candidates]
|
|
678
|
+
print(f"Limiting analysis to first {max_candidates} candidates", file=sys.stderr)
|
|
679
|
+
|
|
680
|
+
print(f"\nStep 2: Detailed analysis of candidates...", file=sys.stderr)
|
|
681
|
+
print(f"Note: Analysis will continue until API rate limit is reached\n", file=sys.stderr)
|
|
682
|
+
|
|
683
|
+
results = []
|
|
684
|
+
|
|
685
|
+
for i, stock in enumerate(candidates, 1):
|
|
686
|
+
symbol = stock.get('symbol', '')
|
|
687
|
+
company_name = stock.get('companyName', '')
|
|
688
|
+
|
|
689
|
+
print(f"[{i}/{len(candidates)}] Analyzing {symbol} - {company_name}...", file=sys.stderr)
|
|
690
|
+
|
|
691
|
+
# Check rate limit
|
|
692
|
+
if client.rate_limit_reached:
|
|
693
|
+
print(f"\n⚠️ API rate limit reached after analyzing {i-1} stocks.", file=sys.stderr)
|
|
694
|
+
print(f"Returning results collected so far: {len(results)} qualified stocks", file=sys.stderr)
|
|
695
|
+
break
|
|
696
|
+
|
|
697
|
+
# Get current price
|
|
698
|
+
current_price = stock.get('price', 0)
|
|
699
|
+
if current_price <= 0:
|
|
700
|
+
print(f" ⚠️ No valid price data", file=sys.stderr)
|
|
701
|
+
continue
|
|
702
|
+
|
|
703
|
+
# Fetch dividend history
|
|
704
|
+
dividend_history = client.get_dividend_history(symbol)
|
|
705
|
+
if client.rate_limit_reached:
|
|
706
|
+
break
|
|
707
|
+
|
|
708
|
+
if not dividend_history:
|
|
709
|
+
print(f" ⚠️ No dividend history", file=sys.stderr)
|
|
710
|
+
continue
|
|
711
|
+
|
|
712
|
+
# Analyze dividend growth
|
|
713
|
+
div_cagr, div_consistent, annual_dividend, div_years_of_growth = analyzer.analyze_dividend_growth(dividend_history)
|
|
714
|
+
if not div_cagr or div_cagr < min_div_growth:
|
|
715
|
+
print(f" ⚠️ Dividend CAGR {div_cagr}% < {min_div_growth}%", file=sys.stderr)
|
|
716
|
+
continue
|
|
717
|
+
|
|
718
|
+
if not annual_dividend:
|
|
719
|
+
print(f" ⚠️ Cannot determine annual dividend", file=sys.stderr)
|
|
720
|
+
continue
|
|
721
|
+
|
|
722
|
+
# Calculate actual dividend yield
|
|
723
|
+
actual_dividend_yield = (annual_dividend / current_price) * 100
|
|
724
|
+
|
|
725
|
+
if actual_dividend_yield < min_yield:
|
|
726
|
+
print(f" ⚠️ Dividend yield {actual_dividend_yield:.2f}% < {min_yield}%", file=sys.stderr)
|
|
727
|
+
continue
|
|
728
|
+
|
|
729
|
+
print(f" ✓ Dividend: {actual_dividend_yield:.2f}% yield, {div_cagr}% CAGR", file=sys.stderr)
|
|
730
|
+
|
|
731
|
+
# Fetch historical prices for RSI
|
|
732
|
+
historical_prices = client.get_historical_prices(symbol, days=30)
|
|
733
|
+
if client.rate_limit_reached:
|
|
734
|
+
break
|
|
735
|
+
|
|
736
|
+
if not historical_prices or len(historical_prices) < 20:
|
|
737
|
+
print(f" ⚠️ Insufficient price data for RSI calculation", file=sys.stderr)
|
|
738
|
+
continue
|
|
739
|
+
|
|
740
|
+
# Calculate RSI
|
|
741
|
+
prices = [p['close'] for p in reversed(historical_prices)] # Oldest first
|
|
742
|
+
rsi = rsi_calc.calculate_rsi(prices, period=14)
|
|
743
|
+
|
|
744
|
+
if rsi is None:
|
|
745
|
+
print(f" ⚠️ RSI calculation failed", file=sys.stderr)
|
|
746
|
+
continue
|
|
747
|
+
|
|
748
|
+
if rsi > rsi_max:
|
|
749
|
+
print(f" ⚠️ RSI {rsi} > {rsi_max}", file=sys.stderr)
|
|
750
|
+
continue
|
|
751
|
+
|
|
752
|
+
print(f" ✓ RSI: {rsi} (oversold)", file=sys.stderr)
|
|
753
|
+
|
|
754
|
+
# Fetch additional fundamental data
|
|
755
|
+
income_stmts = client.get_income_statement(symbol, limit=5)
|
|
756
|
+
if client.rate_limit_reached:
|
|
757
|
+
break
|
|
758
|
+
|
|
759
|
+
balance_sheet = client.get_balance_sheet(symbol, limit=5)
|
|
760
|
+
if client.rate_limit_reached:
|
|
761
|
+
break
|
|
762
|
+
|
|
763
|
+
cash_flow = client.get_cash_flow(symbol, limit=5)
|
|
764
|
+
if client.rate_limit_reached:
|
|
765
|
+
break
|
|
766
|
+
|
|
767
|
+
key_metrics = client.get_key_metrics(symbol, limit=1)
|
|
768
|
+
if client.rate_limit_reached:
|
|
769
|
+
break
|
|
770
|
+
|
|
771
|
+
# Analyze growth metrics
|
|
772
|
+
growth_metrics = analyzer.analyze_growth_metrics(income_stmts if income_stmts else [])
|
|
773
|
+
|
|
774
|
+
# Check for positive revenue and EPS growth
|
|
775
|
+
revenue_cagr = growth_metrics.get('revenue_cagr_3y')
|
|
776
|
+
eps_cagr = growth_metrics.get('eps_cagr_3y')
|
|
777
|
+
|
|
778
|
+
if revenue_cagr is not None and revenue_cagr < 0:
|
|
779
|
+
print(f" ⚠️ Negative revenue growth", file=sys.stderr)
|
|
780
|
+
continue
|
|
781
|
+
|
|
782
|
+
if eps_cagr is not None and eps_cagr < 0:
|
|
783
|
+
print(f" ⚠️ Negative EPS growth", file=sys.stderr)
|
|
784
|
+
continue
|
|
785
|
+
|
|
786
|
+
# Analyze financial health
|
|
787
|
+
health_metrics = analyzer.analyze_financial_health(balance_sheet if balance_sheet else [])
|
|
788
|
+
|
|
789
|
+
if not health_metrics.get('financially_healthy', False):
|
|
790
|
+
print(f" ⚠️ Financial health concerns", file=sys.stderr)
|
|
791
|
+
continue
|
|
792
|
+
|
|
793
|
+
# Extract additional metrics
|
|
794
|
+
latest_income = income_stmts[0] if income_stmts else {}
|
|
795
|
+
latest_metrics = key_metrics[0] if key_metrics else {}
|
|
796
|
+
|
|
797
|
+
# Check if this is a REIT (uses different payout ratio calculation)
|
|
798
|
+
is_reit = analyzer.is_reit(stock)
|
|
799
|
+
|
|
800
|
+
# Calculate payout ratios using the new method
|
|
801
|
+
payout_ratios = analyzer.calculate_payout_ratios(
|
|
802
|
+
income_stmts if income_stmts else [],
|
|
803
|
+
cash_flow if cash_flow else [],
|
|
804
|
+
is_reit=is_reit
|
|
805
|
+
)
|
|
806
|
+
payout_ratio = payout_ratios['payout_ratio']
|
|
807
|
+
fcf_payout_ratio = payout_ratios['fcf_payout_ratio']
|
|
808
|
+
|
|
809
|
+
# Fallback to key_metrics if calculation failed (only for non-REITs)
|
|
810
|
+
if payout_ratio is None and not is_reit:
|
|
811
|
+
payout_ratio = analyzer.get_payout_ratio_from_metrics(key_metrics if key_metrics else [])
|
|
812
|
+
|
|
813
|
+
# Determine dividend sustainability
|
|
814
|
+
# Sustainable if payout ratio < 80% and FCF covers dividends
|
|
815
|
+
dividend_sustainable = False
|
|
816
|
+
if payout_ratio and fcf_payout_ratio:
|
|
817
|
+
dividend_sustainable = (payout_ratio < 80 and fcf_payout_ratio < 100)
|
|
818
|
+
elif payout_ratio:
|
|
819
|
+
dividend_sustainable = payout_ratio < 80
|
|
820
|
+
|
|
821
|
+
# Build result object
|
|
822
|
+
result = {
|
|
823
|
+
'symbol': symbol,
|
|
824
|
+
'company_name': company_name,
|
|
825
|
+
'sector': stock.get('sector', 'Unknown'),
|
|
826
|
+
'market_cap': stock.get('marketCap', 0),
|
|
827
|
+
'price': current_price,
|
|
828
|
+
'dividend_yield': round(actual_dividend_yield, 2),
|
|
829
|
+
'annual_dividend': round(annual_dividend, 2),
|
|
830
|
+
'dividend_cagr_3y': div_cagr,
|
|
831
|
+
'dividend_consistent': div_consistent,
|
|
832
|
+
'rsi': rsi,
|
|
833
|
+
'pe_ratio': latest_metrics.get('peRatio', 0),
|
|
834
|
+
'pb_ratio': latest_metrics.get('pbRatio', 0),
|
|
835
|
+
'revenue_cagr_3y': revenue_cagr,
|
|
836
|
+
'eps_cagr_3y': eps_cagr,
|
|
837
|
+
'payout_ratio': payout_ratio,
|
|
838
|
+
'fcf_payout_ratio': fcf_payout_ratio,
|
|
839
|
+
'dividend_sustainable': dividend_sustainable,
|
|
840
|
+
'dividend_years_of_growth': div_years_of_growth,
|
|
841
|
+
'debt_to_equity': health_metrics.get('debt_to_equity'),
|
|
842
|
+
'current_ratio': health_metrics.get('current_ratio'),
|
|
843
|
+
'financially_healthy': health_metrics.get('financially_healthy', False),
|
|
844
|
+
'roe': latest_metrics.get('roe', 0),
|
|
845
|
+
'profit_margin': latest_metrics.get('netProfitMargin', 0)
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
# Calculate composite score
|
|
849
|
+
result['composite_score'] = analyzer.calculate_composite_score(result)
|
|
850
|
+
|
|
851
|
+
results.append(result)
|
|
852
|
+
print(f" ✅ QUALIFIED - Score: {result['composite_score']}", file=sys.stderr)
|
|
853
|
+
|
|
854
|
+
# Sort by composite score
|
|
855
|
+
results.sort(key=lambda x: x['composite_score'], reverse=True)
|
|
856
|
+
|
|
857
|
+
print(f"\n{'='*80}", file=sys.stderr)
|
|
858
|
+
print(f"Screening Complete!", file=sys.stderr)
|
|
859
|
+
print(f"Qualified Stocks: {len(results)}", file=sys.stderr)
|
|
860
|
+
print(f"{'='*80}\n", file=sys.stderr)
|
|
861
|
+
|
|
862
|
+
return results
|
|
863
|
+
|
|
864
|
+
|
|
865
|
+
def generate_markdown_report(results: List[Dict], criteria: Dict, output_path: str):
|
|
866
|
+
"""Generate human-readable markdown report."""
|
|
867
|
+
|
|
868
|
+
report = f"""# Dividend Growth Pullback Screening Report
|
|
869
|
+
|
|
870
|
+
**Generated:** {datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S')} UTC
|
|
871
|
+
**Data Source:** Financial Modeling Prep API
|
|
872
|
+
|
|
873
|
+
## Executive Summary
|
|
874
|
+
|
|
875
|
+
**Total Qualified Stocks:** {len(results)}
|
|
876
|
+
|
|
877
|
+
### Screening Criteria
|
|
878
|
+
|
|
879
|
+
- **Dividend Yield:** ≥ {criteria['dividend_yield_min']}%
|
|
880
|
+
- **Dividend Growth (3Y CAGR):** ≥ {criteria['dividend_cagr_min']}%
|
|
881
|
+
- **RSI:** ≤ {criteria['rsi_max']} (oversold/pullback)
|
|
882
|
+
- **Market Cap:** ≥ $2 billion
|
|
883
|
+
- **Financial Health:** Positive revenue/EPS growth, D/E < 2.0, Current Ratio > 1.0
|
|
884
|
+
|
|
885
|
+
---
|
|
886
|
+
|
|
887
|
+
"""
|
|
888
|
+
|
|
889
|
+
if not results:
|
|
890
|
+
report += """## No Stocks Qualified
|
|
891
|
+
|
|
892
|
+
**Possible Reasons:**
|
|
893
|
+
- Strong bull market with few oversold stocks
|
|
894
|
+
- Dividend growth criteria (12%+) is very selective
|
|
895
|
+
- RSI threshold may be too strict for current market conditions
|
|
896
|
+
|
|
897
|
+
**Recommendations:**
|
|
898
|
+
- Relax RSI threshold to ≤45 for early pullback phase
|
|
899
|
+
- Lower dividend growth to ≥10% for more candidates
|
|
900
|
+
- Check back during market corrections or sector rotations
|
|
901
|
+
|
|
902
|
+
"""
|
|
903
|
+
else:
|
|
904
|
+
for i, stock in enumerate(results, 1):
|
|
905
|
+
rsi_interpretation = (
|
|
906
|
+
"Extreme Oversold" if stock['rsi'] < 30
|
|
907
|
+
else "Strong Oversold" if stock['rsi'] < 35
|
|
908
|
+
else "Early Pullback"
|
|
909
|
+
)
|
|
910
|
+
|
|
911
|
+
report += f"""## {i}. {stock['symbol']} - {stock['company_name']}
|
|
912
|
+
|
|
913
|
+
**Sector:** {stock['sector']}
|
|
914
|
+
**Market Cap:** ${stock['market_cap'] / 1e9:.1f}B
|
|
915
|
+
**Current Price:** ${stock['price']:.2f}
|
|
916
|
+
**Composite Score:** {stock['composite_score']}/100
|
|
917
|
+
|
|
918
|
+
### Dividend Growth Profile
|
|
919
|
+
|
|
920
|
+
| Metric | Value | Assessment |
|
|
921
|
+
|--------|-------|------------|
|
|
922
|
+
| Dividend Yield | **{stock['dividend_yield']:.2f}%** | {'✓ Above 2%' if stock['dividend_yield'] >= 2 else '⚠ Below 2%'} |
|
|
923
|
+
| Annual Dividend | ${stock['annual_dividend']:.2f} | |
|
|
924
|
+
| 3Y Dividend CAGR | **{stock['dividend_cagr_3y']:.2f}%** | {'🔥 Exceptional' if stock['dividend_cagr_3y'] >= 20 else '✓ Excellent' if stock['dividend_cagr_3y'] >= 15 else '✓ Strong'} |
|
|
925
|
+
| Dividend Consistency | {'Yes' if stock['dividend_consistent'] else 'No'} | {'✓' if stock['dividend_consistent'] else '⚠'} |
|
|
926
|
+
| Payout Ratio | {f"{stock['payout_ratio']:.1f}%" if stock['payout_ratio'] else 'N/A'} | {'✓ Sustainable' if stock['payout_ratio'] and stock['payout_ratio'] < 70 else '⚠ High' if stock['payout_ratio'] and stock['payout_ratio'] < 100 else '❌ Risk' if stock['payout_ratio'] else 'N/A'} |
|
|
927
|
+
|
|
928
|
+
### Technical Setup
|
|
929
|
+
|
|
930
|
+
| Metric | Value | Interpretation |
|
|
931
|
+
|--------|-------|----------------|
|
|
932
|
+
| RSI (14-period) | **{stock['rsi']:.1f}** | {rsi_interpretation} |
|
|
933
|
+
| Entry Timing | {'Immediate - Scale in 50%' if stock['rsi'] < 30 else 'Good - Full position OK' if stock['rsi'] < 35 else 'Conservative - High conviction'} | |
|
|
934
|
+
| Stop Loss Suggestion | {f"{((stock['rsi'] - 30) / 2 + 3):.0f}% below entry" if stock['rsi'] >= 30 else "8% below entry"} | |
|
|
935
|
+
|
|
936
|
+
**RSI Context:** {
|
|
937
|
+
'Extreme oversold reading suggests panic selling or negative news. Wait for RSI to turn up (>30) before entry to confirm stabilization.' if stock['rsi'] < 30
|
|
938
|
+
else 'Strong oversold in uptrend. Normal correction creating entry opportunity. Can initiate position with standard risk management.' if stock['rsi'] < 35
|
|
939
|
+
else 'Early pullback in uptrend. Conservative entry point with lower risk of further decline. Suitable for high-conviction additions.'
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
### Business Fundamentals
|
|
943
|
+
|
|
944
|
+
| Metric | Value | Status |
|
|
945
|
+
|--------|-------|--------|
|
|
946
|
+
| Revenue CAGR (3Y) | {f"{stock['revenue_cagr_3y']:.2f}%" if stock['revenue_cagr_3y'] else 'N/A'} | {'✓' if stock['revenue_cagr_3y'] and stock['revenue_cagr_3y'] > 0 else '⚠'} |
|
|
947
|
+
| EPS CAGR (3Y) | {f"{stock['eps_cagr_3y']:.2f}%" if stock['eps_cagr_3y'] else 'N/A'} | {'✓' if stock['eps_cagr_3y'] and stock['eps_cagr_3y'] > 0 else '⚠'} |
|
|
948
|
+
| ROE | {f"{stock['roe']:.1f}%" if stock['roe'] else 'N/A'} | {'✓ Excellent' if stock['roe'] and stock['roe'] >= 20 else '✓ Good' if stock['roe'] and stock['roe'] >= 15 else '⚠ Moderate' if stock['roe'] else 'N/A'} |
|
|
949
|
+
| Net Profit Margin | {f"{stock['profit_margin']:.1f}%" if stock['profit_margin'] else 'N/A'} | {'✓' if stock['profit_margin'] and stock['profit_margin'] >= 10 else '⚠'} |
|
|
950
|
+
|
|
951
|
+
### Financial Health
|
|
952
|
+
|
|
953
|
+
| Metric | Value | Status |
|
|
954
|
+
|--------|-------|--------|
|
|
955
|
+
| Debt-to-Equity | {f"{stock['debt_to_equity']:.2f}" if stock['debt_to_equity'] is not None else 'N/A'} | {'✓ Very Low' if stock['debt_to_equity'] and stock['debt_to_equity'] < 0.5 else '✓ Low' if stock['debt_to_equity'] and stock['debt_to_equity'] < 1.0 else '⚠ Moderate' if stock['debt_to_equity'] else 'N/A'} |
|
|
956
|
+
| Current Ratio | {f"{stock['current_ratio']:.2f}" if stock['current_ratio'] else 'N/A'} | {'✓ Healthy' if stock['current_ratio'] and stock['current_ratio'] > 1.2 else '⚠ Adequate' if stock['current_ratio'] else 'N/A'} |
|
|
957
|
+
|
|
958
|
+
### Investment Thesis
|
|
959
|
+
|
|
960
|
+
**10-Year Dividend Projection ({stock['dividend_cagr_3y']:.0f}% CAGR):**
|
|
961
|
+
- Current Yield on Cost: {stock['dividend_yield']:.2f}%
|
|
962
|
+
- Year 5 Yield on Cost: {stock['dividend_yield'] * (1 + stock['dividend_cagr_3y']/100)**5:.2f}%
|
|
963
|
+
- Year 10 Yield on Cost: {stock['dividend_yield'] * (1 + stock['dividend_cagr_3y']/100)**10:.2f}%
|
|
964
|
+
|
|
965
|
+
**Entry Strategy:**
|
|
966
|
+
{f"- RSI {stock['rsi']:.0f} indicates {rsi_interpretation.lower()} condition"}
|
|
967
|
+
- {'Scale in with 50% position now, add remaining on RSI >30 confirmation' if stock['rsi'] < 30 else f"Full position acceptable with stop loss {((stock['rsi'] - 30) / 2 + 3):.0f}% below entry" if stock['rsi'] < 35 else 'Conservative entry for high-conviction add with 3-5% stop loss'}
|
|
968
|
+
- Time horizon: 6-12 months minimum (long-term dividend growth play)
|
|
969
|
+
|
|
970
|
+
**Risk Factors:**
|
|
971
|
+
{f"- Payout ratio {stock['payout_ratio']:.0f}% limits dividend growth runway" if stock['payout_ratio'] and stock['payout_ratio'] > 70 else "- Monitor payout ratio sustainability"}
|
|
972
|
+
{f"- Debt-to-equity {stock['debt_to_equity']:.1f} requires monitoring" if stock['debt_to_equity'] and stock['debt_to_equity'] > 1.0 else ""}
|
|
973
|
+
- RSI can remain oversold in downtrends - watch for reversal confirmation
|
|
974
|
+
- Dividend growth may slow if business growth moderates
|
|
975
|
+
|
|
976
|
+
---
|
|
977
|
+
|
|
978
|
+
"""
|
|
979
|
+
|
|
980
|
+
report += f"""
|
|
981
|
+
## Methodology
|
|
982
|
+
|
|
983
|
+
This screening combines fundamental dividend analysis with technical timing indicators:
|
|
984
|
+
|
|
985
|
+
1. **Fundamental Filter:** Dividend yield ≥{criteria['dividend_yield_min']}%, dividend CAGR ≥{criteria['dividend_cagr_min']}%, positive business growth
|
|
986
|
+
2. **Technical Filter:** RSI ≤{criteria['rsi_max']} identifies temporary pullbacks in quality stocks
|
|
987
|
+
3. **Quality Filter:** Financial health checks (debt, liquidity, profitability)
|
|
988
|
+
4. **Ranking:** Composite score balancing dividend growth (40%), quality (30%), technical setup (20%), valuation (10%)
|
|
989
|
+
|
|
990
|
+
**Investment Philosophy:**
|
|
991
|
+
High dividend growth stocks (12%+ CAGR) compound wealth through rising dividends rather than high current yield. A 1.5% yielding stock growing dividends at 15%/year becomes a 4% yielder in 6 years and 9% yielder in 12 years - far superior to a 4% yielder growing at 3%/year. Buying during RSI oversold conditions (≤40) enhances returns by entering at technical support levels.
|
|
992
|
+
|
|
993
|
+
---
|
|
994
|
+
|
|
995
|
+
**Disclaimer:** This report is for informational purposes only. Past dividend growth does not guarantee future performance. RSI oversold conditions do not guarantee price reversals. Conduct thorough due diligence and consult a financial advisor before making investment decisions.
|
|
996
|
+
|
|
997
|
+
**Report Generated:** {datetime.utcnow().isoformat()}Z
|
|
998
|
+
"""
|
|
999
|
+
|
|
1000
|
+
# Write report
|
|
1001
|
+
with open(output_path, 'w') as f:
|
|
1002
|
+
f.write(report)
|
|
1003
|
+
|
|
1004
|
+
print(f"✅ Markdown report saved: {output_path}", file=sys.stderr)
|
|
1005
|
+
|
|
1006
|
+
|
|
1007
|
+
def main():
|
|
1008
|
+
parser = argparse.ArgumentParser(
|
|
1009
|
+
description='Screen dividend growth stocks with RSI oversold using FINVIZ + FMP API (two-stage approach)',
|
|
1010
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
1011
|
+
epilog='''
|
|
1012
|
+
Examples:
|
|
1013
|
+
# Two-stage screening: FINVIZ pre-screen + FMP detailed analysis (RECOMMENDED)
|
|
1014
|
+
python3 screen_dividend_growth_rsi.py --use-finviz
|
|
1015
|
+
|
|
1016
|
+
# FMP-only screening (original method)
|
|
1017
|
+
python3 screen_dividend_growth_rsi.py
|
|
1018
|
+
|
|
1019
|
+
# Provide API keys as arguments
|
|
1020
|
+
python3 screen_dividend_growth_rsi.py --use-finviz --fmp-api-key YOUR_FMP_KEY --finviz-api-key YOUR_FINVIZ_KEY
|
|
1021
|
+
|
|
1022
|
+
# Custom parameters
|
|
1023
|
+
python3 screen_dividend_growth_rsi.py --use-finviz --min-yield 2.0 --min-div-growth 15.0 --rsi-max 35
|
|
1024
|
+
|
|
1025
|
+
Environment Variables:
|
|
1026
|
+
FMP_API_KEY - Financial Modeling Prep API key
|
|
1027
|
+
FINVIZ_API_KEY - FINVIZ Elite API key (required for --use-finviz)
|
|
1028
|
+
'''
|
|
1029
|
+
)
|
|
1030
|
+
|
|
1031
|
+
parser.add_argument(
|
|
1032
|
+
'--fmp-api-key',
|
|
1033
|
+
type=str,
|
|
1034
|
+
help='FMP API key (or set FMP_API_KEY environment variable)'
|
|
1035
|
+
)
|
|
1036
|
+
parser.add_argument(
|
|
1037
|
+
'--finviz-api-key',
|
|
1038
|
+
type=str,
|
|
1039
|
+
help='FINVIZ Elite API key (or set FINVIZ_API_KEY environment variable)'
|
|
1040
|
+
)
|
|
1041
|
+
parser.add_argument(
|
|
1042
|
+
'--use-finviz',
|
|
1043
|
+
action='store_true',
|
|
1044
|
+
help='Use FINVIZ Elite API for pre-screening (recommended to reduce FMP API calls)'
|
|
1045
|
+
)
|
|
1046
|
+
parser.add_argument(
|
|
1047
|
+
'--min-yield',
|
|
1048
|
+
type=float,
|
|
1049
|
+
default=1.5,
|
|
1050
|
+
help='Minimum dividend yield %% (default: 1.5)'
|
|
1051
|
+
)
|
|
1052
|
+
parser.add_argument(
|
|
1053
|
+
'--min-div-growth',
|
|
1054
|
+
type=float,
|
|
1055
|
+
default=12.0,
|
|
1056
|
+
help='Minimum 3-year dividend CAGR %% (default: 12.0)'
|
|
1057
|
+
)
|
|
1058
|
+
parser.add_argument(
|
|
1059
|
+
'--rsi-max',
|
|
1060
|
+
type=float,
|
|
1061
|
+
default=40.0,
|
|
1062
|
+
help='Maximum RSI value (default: 40.0)'
|
|
1063
|
+
)
|
|
1064
|
+
parser.add_argument(
|
|
1065
|
+
'--max-candidates',
|
|
1066
|
+
type=int,
|
|
1067
|
+
default=None,
|
|
1068
|
+
help='Maximum candidates to analyze (default: all, only applies to FMP-only mode)'
|
|
1069
|
+
)
|
|
1070
|
+
|
|
1071
|
+
args = parser.parse_args()
|
|
1072
|
+
|
|
1073
|
+
# Get FMP API key
|
|
1074
|
+
fmp_api_key = args.fmp_api_key or os.environ.get('FMP_API_KEY')
|
|
1075
|
+
if not fmp_api_key:
|
|
1076
|
+
print("ERROR: FMP API key required. Provide via --fmp-api-key or FMP_API_KEY environment variable", file=sys.stderr)
|
|
1077
|
+
sys.exit(1)
|
|
1078
|
+
|
|
1079
|
+
# FINVIZ pre-screening (optional)
|
|
1080
|
+
finviz_symbols = None
|
|
1081
|
+
if args.use_finviz:
|
|
1082
|
+
finviz_api_key = args.finviz_api_key or os.environ.get('FINVIZ_API_KEY')
|
|
1083
|
+
if not finviz_api_key:
|
|
1084
|
+
print("ERROR: FINVIZ API key required when using --use-finviz. Provide via --finviz-api-key or FINVIZ_API_KEY environment variable", file=sys.stderr)
|
|
1085
|
+
sys.exit(1)
|
|
1086
|
+
|
|
1087
|
+
print(f"\n{'='*80}", file=sys.stderr)
|
|
1088
|
+
print("DIVIDEND GROWTH PULLBACK SCREENER (TWO-STAGE)", file=sys.stderr)
|
|
1089
|
+
print(f"{'='*80}\n", file=sys.stderr)
|
|
1090
|
+
|
|
1091
|
+
finviz_client = FINVIZClient(finviz_api_key)
|
|
1092
|
+
finviz_symbols = finviz_client.screen_stocks()
|
|
1093
|
+
|
|
1094
|
+
if not finviz_symbols:
|
|
1095
|
+
print("ERROR: No stocks found in FINVIZ pre-screening", file=sys.stderr)
|
|
1096
|
+
sys.exit(1)
|
|
1097
|
+
|
|
1098
|
+
print(f"\n{'='*80}\n", file=sys.stderr)
|
|
1099
|
+
|
|
1100
|
+
# Run screening
|
|
1101
|
+
results = screen_dividend_growth_pullbacks(
|
|
1102
|
+
api_key=fmp_api_key,
|
|
1103
|
+
min_yield=args.min_yield,
|
|
1104
|
+
min_div_growth=args.min_div_growth,
|
|
1105
|
+
rsi_max=args.rsi_max,
|
|
1106
|
+
max_candidates=args.max_candidates,
|
|
1107
|
+
finviz_symbols=finviz_symbols
|
|
1108
|
+
)
|
|
1109
|
+
|
|
1110
|
+
# Prepare metadata
|
|
1111
|
+
criteria = {
|
|
1112
|
+
'dividend_yield_min': args.min_yield,
|
|
1113
|
+
'dividend_cagr_min': args.min_div_growth,
|
|
1114
|
+
'rsi_max': args.rsi_max,
|
|
1115
|
+
'revenue_trend': 'positive over 3 years',
|
|
1116
|
+
'eps_trend': 'positive over 3 years'
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
# Generate outputs
|
|
1120
|
+
today = date.today().isoformat()
|
|
1121
|
+
|
|
1122
|
+
# Determine output directory (project root logs/ folder)
|
|
1123
|
+
script_dir = os.path.dirname(os.path.abspath(__file__))
|
|
1124
|
+
# Navigate from .claude/skills/dividend-growth-pullback-screener/scripts to project root
|
|
1125
|
+
project_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(script_dir))))
|
|
1126
|
+
logs_dir = os.path.join(project_root, 'logs')
|
|
1127
|
+
os.makedirs(logs_dir, exist_ok=True)
|
|
1128
|
+
|
|
1129
|
+
# JSON output
|
|
1130
|
+
json_output = {
|
|
1131
|
+
'metadata': {
|
|
1132
|
+
'generated_at': datetime.utcnow().isoformat() + 'Z',
|
|
1133
|
+
'criteria': criteria,
|
|
1134
|
+
'total_results': len(results)
|
|
1135
|
+
},
|
|
1136
|
+
'stocks': results
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
json_path = os.path.join(logs_dir, f'dividend_growth_pullback_results_{today}.json')
|
|
1140
|
+
with open(json_path, 'w') as f:
|
|
1141
|
+
json.dump(json_output, f, indent=2)
|
|
1142
|
+
|
|
1143
|
+
print(f"✅ JSON results saved: {json_path}", file=sys.stderr)
|
|
1144
|
+
|
|
1145
|
+
# Markdown report
|
|
1146
|
+
md_path = os.path.join(logs_dir, f'dividend_growth_pullback_screening_{today}.md')
|
|
1147
|
+
generate_markdown_report(results, criteria, md_path)
|
|
1148
|
+
|
|
1149
|
+
print(f"\n{'='*80}", file=sys.stderr)
|
|
1150
|
+
print(f"Screening complete! Found {len(results)} qualified stocks.", file=sys.stderr)
|
|
1151
|
+
print(f"{'='*80}\n", file=sys.stderr)
|
|
1152
|
+
|
|
1153
|
+
|
|
1154
|
+
if __name__ == '__main__':
|
|
1155
|
+
main()
|