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,1138 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Value Dividend Stock Screener using FINVIZ + Financial Modeling Prep API
|
|
4
|
+
|
|
5
|
+
Two-stage screening approach:
|
|
6
|
+
1. FINVIZ Elite API: Pre-screen stocks with basic criteria (fast, cost-effective)
|
|
7
|
+
2. FMP API: Detailed analysis of pre-screened candidates (comprehensive)
|
|
8
|
+
|
|
9
|
+
Screens US stocks based on:
|
|
10
|
+
- Dividend yield >= 3.5%
|
|
11
|
+
- P/E ratio <= 20
|
|
12
|
+
- P/B ratio <= 2
|
|
13
|
+
- Dividend CAGR >= 5% (3-year)
|
|
14
|
+
- Revenue growth: positive trend over 3 years
|
|
15
|
+
- EPS growth: positive trend over 3 years
|
|
16
|
+
- Additional analysis: dividend sustainability, financial health, quality scores
|
|
17
|
+
|
|
18
|
+
Outputs top 20 stocks ranked by composite score.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
import argparse
|
|
22
|
+
import csv
|
|
23
|
+
import io
|
|
24
|
+
import json
|
|
25
|
+
import os
|
|
26
|
+
import sys
|
|
27
|
+
from typing import Dict, List, Optional, Tuple, Set
|
|
28
|
+
from datetime import datetime
|
|
29
|
+
import time
|
|
30
|
+
|
|
31
|
+
try:
|
|
32
|
+
import requests
|
|
33
|
+
except ImportError:
|
|
34
|
+
print("ERROR: requests library not found. Install with: pip install requests", file=sys.stderr)
|
|
35
|
+
sys.exit(1)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class FINVIZClient:
|
|
39
|
+
"""Client for FINVIZ Elite API"""
|
|
40
|
+
|
|
41
|
+
BASE_URL = "https://elite.finviz.com/export.ashx"
|
|
42
|
+
|
|
43
|
+
def __init__(self, api_key: str):
|
|
44
|
+
self.api_key = api_key
|
|
45
|
+
self.session = requests.Session()
|
|
46
|
+
|
|
47
|
+
def screen_stocks(self) -> Set[str]:
|
|
48
|
+
"""
|
|
49
|
+
Screen stocks using FINVIZ Elite API with predefined criteria
|
|
50
|
+
|
|
51
|
+
Criteria:
|
|
52
|
+
- Market cap: Mid-cap or higher
|
|
53
|
+
- Dividend yield: 3%+
|
|
54
|
+
- Dividend growth (3Y): 5%+
|
|
55
|
+
- EPS growth (3Y): Positive
|
|
56
|
+
- P/B: Under 2
|
|
57
|
+
- P/E: Under 20
|
|
58
|
+
- Sales growth (3Y): Positive
|
|
59
|
+
- Geography: USA
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
Set of stock symbols
|
|
63
|
+
"""
|
|
64
|
+
# Build filter string in FINVIZ format: key_value,key_value,...
|
|
65
|
+
filters = 'cap_midover,fa_div_o3,fa_divgrowth_3yo5,fa_eps3years_pos,fa_pb_u2,fa_pe_u20,fa_sales3years_pos,geo_usa'
|
|
66
|
+
|
|
67
|
+
params = {
|
|
68
|
+
'v': '151', # View type
|
|
69
|
+
'f': filters, # Filter conditions
|
|
70
|
+
'ft': '4', # File type: CSV export
|
|
71
|
+
'auth': self.api_key
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
try:
|
|
75
|
+
print(f"Fetching pre-screened stocks from FINVIZ Elite API...", file=sys.stderr)
|
|
76
|
+
response = self.session.get(self.BASE_URL, params=params, timeout=30)
|
|
77
|
+
|
|
78
|
+
if response.status_code == 200:
|
|
79
|
+
# Parse CSV response
|
|
80
|
+
csv_content = response.content.decode('utf-8')
|
|
81
|
+
reader = csv.DictReader(io.StringIO(csv_content))
|
|
82
|
+
|
|
83
|
+
symbols = set()
|
|
84
|
+
for row in reader:
|
|
85
|
+
# FINVIZ CSV has 'Ticker' column
|
|
86
|
+
ticker = row.get('Ticker', '').strip()
|
|
87
|
+
if ticker:
|
|
88
|
+
symbols.add(ticker)
|
|
89
|
+
|
|
90
|
+
print(f"✅ FINVIZ returned {len(symbols)} pre-screened stocks", file=sys.stderr)
|
|
91
|
+
return symbols
|
|
92
|
+
|
|
93
|
+
elif response.status_code == 401 or response.status_code == 403:
|
|
94
|
+
print(f"ERROR: FINVIZ API authentication failed. Check your API key.", file=sys.stderr)
|
|
95
|
+
print(f"Status code: {response.status_code}", file=sys.stderr)
|
|
96
|
+
return set()
|
|
97
|
+
else:
|
|
98
|
+
print(f"ERROR: FINVIZ API request failed: {response.status_code}", file=sys.stderr)
|
|
99
|
+
return set()
|
|
100
|
+
|
|
101
|
+
except requests.exceptions.RequestException as e:
|
|
102
|
+
print(f"ERROR: FINVIZ request exception: {e}", file=sys.stderr)
|
|
103
|
+
return set()
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class FMPClient:
|
|
107
|
+
"""Client for Financial Modeling Prep API"""
|
|
108
|
+
|
|
109
|
+
BASE_URL = "https://financialmodelingprep.com/api/v3"
|
|
110
|
+
|
|
111
|
+
def __init__(self, api_key: str):
|
|
112
|
+
self.api_key = api_key
|
|
113
|
+
self.session = requests.Session()
|
|
114
|
+
self.rate_limit_reached = False
|
|
115
|
+
self.retry_count = 0
|
|
116
|
+
|
|
117
|
+
def _get(self, endpoint: str, params: Optional[Dict] = None) -> Optional[Dict]:
|
|
118
|
+
"""Make GET request with rate limiting and error handling"""
|
|
119
|
+
if self.rate_limit_reached:
|
|
120
|
+
return None
|
|
121
|
+
|
|
122
|
+
if params is None:
|
|
123
|
+
params = {}
|
|
124
|
+
params['apikey'] = self.api_key
|
|
125
|
+
|
|
126
|
+
url = f"{self.BASE_URL}/{endpoint}"
|
|
127
|
+
|
|
128
|
+
try:
|
|
129
|
+
response = self.session.get(url, params=params, timeout=30)
|
|
130
|
+
time.sleep(0.3) # Rate limiting: ~3 requests/second
|
|
131
|
+
|
|
132
|
+
if response.status_code == 200:
|
|
133
|
+
self.retry_count = 0 # Reset retry count on success
|
|
134
|
+
return response.json()
|
|
135
|
+
elif response.status_code == 429:
|
|
136
|
+
self.retry_count += 1
|
|
137
|
+
if self.retry_count <= 1: # Only retry once
|
|
138
|
+
print(f"WARNING: Rate limit exceeded. Waiting 60 seconds...", file=sys.stderr)
|
|
139
|
+
time.sleep(60)
|
|
140
|
+
return self._get(endpoint, params)
|
|
141
|
+
else:
|
|
142
|
+
print(f"ERROR: Daily API rate limit reached. Stopping analysis.", file=sys.stderr)
|
|
143
|
+
self.rate_limit_reached = True
|
|
144
|
+
return None
|
|
145
|
+
else:
|
|
146
|
+
print(f"ERROR: API request failed: {response.status_code} - {response.text}", file=sys.stderr)
|
|
147
|
+
return None
|
|
148
|
+
except requests.exceptions.RequestException as e:
|
|
149
|
+
print(f"ERROR: Request exception: {e}", file=sys.stderr)
|
|
150
|
+
return None
|
|
151
|
+
|
|
152
|
+
def screen_stocks(self, dividend_yield_min: float, pe_max: float, pb_max: float,
|
|
153
|
+
market_cap_min: float = 2_000_000_000) -> List[Dict]:
|
|
154
|
+
"""Screen stocks using Stock Screener API"""
|
|
155
|
+
params = {
|
|
156
|
+
'dividendYieldMoreThan': dividend_yield_min,
|
|
157
|
+
'priceEarningRatioLowerThan': pe_max,
|
|
158
|
+
'priceToBookRatioLowerThan': pb_max,
|
|
159
|
+
'marketCapMoreThan': market_cap_min,
|
|
160
|
+
'exchange': 'NASDAQ,NYSE',
|
|
161
|
+
'limit': 1000
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
data = self._get('stock-screener', params)
|
|
165
|
+
return data if data else []
|
|
166
|
+
|
|
167
|
+
def get_income_statement(self, symbol: str, limit: int = 5) -> List[Dict]:
|
|
168
|
+
"""Get income statement"""
|
|
169
|
+
return self._get(f'income-statement/{symbol}', {'limit': limit}) or []
|
|
170
|
+
|
|
171
|
+
def get_balance_sheet(self, symbol: str, limit: int = 5) -> List[Dict]:
|
|
172
|
+
"""Get balance sheet"""
|
|
173
|
+
return self._get(f'balance-sheet-statement/{symbol}', {'limit': limit}) or []
|
|
174
|
+
|
|
175
|
+
def get_cash_flow(self, symbol: str, limit: int = 5) -> List[Dict]:
|
|
176
|
+
"""Get cash flow statement"""
|
|
177
|
+
return self._get(f'cash-flow-statement/{symbol}', {'limit': limit}) or []
|
|
178
|
+
|
|
179
|
+
def get_key_metrics(self, symbol: str, limit: int = 5) -> List[Dict]:
|
|
180
|
+
"""Get key metrics"""
|
|
181
|
+
return self._get(f'key-metrics/{symbol}', {'limit': limit}) or []
|
|
182
|
+
|
|
183
|
+
def get_dividend_history(self, symbol: str) -> List[Dict]:
|
|
184
|
+
"""Get dividend history"""
|
|
185
|
+
return self._get(f'historical-price-full/stock_dividend/{symbol}') or {}
|
|
186
|
+
|
|
187
|
+
def get_company_profile(self, symbol: str) -> Optional[Dict]:
|
|
188
|
+
"""Get company profile including sector information."""
|
|
189
|
+
result = self._get(f'profile/{symbol}')
|
|
190
|
+
if result and isinstance(result, list) and len(result) > 0:
|
|
191
|
+
return result[0]
|
|
192
|
+
return None
|
|
193
|
+
|
|
194
|
+
def get_historical_prices(self, symbol: str, days: int = 30) -> List[Dict]:
|
|
195
|
+
"""Get historical daily prices for RSI calculation."""
|
|
196
|
+
result = self._get(f'historical-price-full/{symbol}', {'serietype': 'line'})
|
|
197
|
+
if result and 'historical' in result:
|
|
198
|
+
# Return most recent 'days' entries
|
|
199
|
+
return result['historical'][:days]
|
|
200
|
+
return []
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
class RSICalculator:
|
|
204
|
+
"""Calculate RSI (Relative Strength Index)"""
|
|
205
|
+
|
|
206
|
+
@staticmethod
|
|
207
|
+
def calculate_rsi(prices: List[float], period: int = 14) -> Optional[float]:
|
|
208
|
+
"""
|
|
209
|
+
Calculate RSI from price data.
|
|
210
|
+
|
|
211
|
+
Args:
|
|
212
|
+
prices: List of closing prices (oldest to newest)
|
|
213
|
+
period: RSI period (default 14)
|
|
214
|
+
|
|
215
|
+
Returns:
|
|
216
|
+
RSI value (0-100) or None if insufficient data
|
|
217
|
+
"""
|
|
218
|
+
if len(prices) < period + 1:
|
|
219
|
+
return None
|
|
220
|
+
|
|
221
|
+
# Calculate price changes
|
|
222
|
+
changes = [prices[i] - prices[i - 1] for i in range(1, len(prices))]
|
|
223
|
+
|
|
224
|
+
# Separate gains and losses
|
|
225
|
+
gains = [max(0, change) for change in changes]
|
|
226
|
+
losses = [abs(min(0, change)) for change in changes]
|
|
227
|
+
|
|
228
|
+
# Calculate initial average gain/loss
|
|
229
|
+
avg_gain = sum(gains[:period]) / period
|
|
230
|
+
avg_loss = sum(losses[:period]) / period
|
|
231
|
+
|
|
232
|
+
# Smooth using Wilder's method for remaining periods
|
|
233
|
+
for i in range(period, len(changes)):
|
|
234
|
+
avg_gain = (avg_gain * (period - 1) + gains[i]) / period
|
|
235
|
+
avg_loss = (avg_loss * (period - 1) + losses[i]) / period
|
|
236
|
+
|
|
237
|
+
# Calculate RSI
|
|
238
|
+
if avg_loss == 0:
|
|
239
|
+
return 100.0
|
|
240
|
+
|
|
241
|
+
rs = avg_gain / avg_loss
|
|
242
|
+
rsi = 100 - (100 / (1 + rs))
|
|
243
|
+
|
|
244
|
+
return round(rsi, 1)
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
class StockAnalyzer:
|
|
248
|
+
"""Analyzes stock data and calculates scores"""
|
|
249
|
+
|
|
250
|
+
@staticmethod
|
|
251
|
+
def is_reit(stock_data: Dict) -> bool:
|
|
252
|
+
"""
|
|
253
|
+
Determine if a stock is a REIT based on sector/industry.
|
|
254
|
+
|
|
255
|
+
Args:
|
|
256
|
+
stock_data: Dict containing sector and/or industry fields
|
|
257
|
+
|
|
258
|
+
Returns:
|
|
259
|
+
True if the stock is likely a REIT
|
|
260
|
+
"""
|
|
261
|
+
sector = stock_data.get('sector', '') or ''
|
|
262
|
+
industry = stock_data.get('industry', '') or ''
|
|
263
|
+
|
|
264
|
+
sector_lower = sector.lower()
|
|
265
|
+
industry_lower = industry.lower()
|
|
266
|
+
|
|
267
|
+
if 'real estate' in sector_lower:
|
|
268
|
+
return True
|
|
269
|
+
if 'reit' in industry_lower:
|
|
270
|
+
return True
|
|
271
|
+
|
|
272
|
+
return False
|
|
273
|
+
|
|
274
|
+
@staticmethod
|
|
275
|
+
def calculate_ffo(cash_flows: List[Dict]) -> Optional[float]:
|
|
276
|
+
"""
|
|
277
|
+
Calculate Funds From Operations (FFO) for REITs.
|
|
278
|
+
|
|
279
|
+
FFO = Net Income + Depreciation & Amortization
|
|
280
|
+
|
|
281
|
+
Args:
|
|
282
|
+
cash_flows: List of cash flow statements (newest first)
|
|
283
|
+
|
|
284
|
+
Returns:
|
|
285
|
+
FFO value or None if data is missing
|
|
286
|
+
"""
|
|
287
|
+
if not cash_flows:
|
|
288
|
+
return None
|
|
289
|
+
|
|
290
|
+
latest_cf = cash_flows[0]
|
|
291
|
+
net_income = latest_cf.get('netIncome', 0)
|
|
292
|
+
depreciation = latest_cf.get('depreciationAndAmortization', 0)
|
|
293
|
+
|
|
294
|
+
if net_income == 0 and depreciation == 0:
|
|
295
|
+
return None
|
|
296
|
+
|
|
297
|
+
return net_income + depreciation
|
|
298
|
+
|
|
299
|
+
@staticmethod
|
|
300
|
+
def calculate_ffo_payout_ratio(cash_flows: List[Dict]) -> Optional[float]:
|
|
301
|
+
"""
|
|
302
|
+
Calculate FFO payout ratio for REITs.
|
|
303
|
+
|
|
304
|
+
FFO Payout Ratio = Dividends Paid / FFO
|
|
305
|
+
|
|
306
|
+
Args:
|
|
307
|
+
cash_flows: List of cash flow statements (newest first)
|
|
308
|
+
|
|
309
|
+
Returns:
|
|
310
|
+
FFO payout ratio as percentage, or None if calculation fails
|
|
311
|
+
"""
|
|
312
|
+
if not cash_flows:
|
|
313
|
+
return None
|
|
314
|
+
|
|
315
|
+
ffo = StockAnalyzer.calculate_ffo(cash_flows)
|
|
316
|
+
if not ffo or ffo <= 0:
|
|
317
|
+
return None
|
|
318
|
+
|
|
319
|
+
latest_cf = cash_flows[0]
|
|
320
|
+
dividends_paid = abs(latest_cf.get('dividendsPaid', 0))
|
|
321
|
+
|
|
322
|
+
if dividends_paid <= 0:
|
|
323
|
+
return None
|
|
324
|
+
|
|
325
|
+
return round((dividends_paid / ffo) * 100, 1)
|
|
326
|
+
|
|
327
|
+
@staticmethod
|
|
328
|
+
def calculate_cagr(start_value: float, end_value: float, years: int) -> Optional[float]:
|
|
329
|
+
"""Calculate Compound Annual Growth Rate"""
|
|
330
|
+
if start_value <= 0 or end_value <= 0 or years <= 0:
|
|
331
|
+
return None
|
|
332
|
+
return (pow(end_value / start_value, 1 / years) - 1) * 100
|
|
333
|
+
|
|
334
|
+
@staticmethod
|
|
335
|
+
def check_positive_trend(values: List[float]) -> bool:
|
|
336
|
+
"""Check if values show positive trend (allow one dip)"""
|
|
337
|
+
if len(values) < 3:
|
|
338
|
+
return False
|
|
339
|
+
|
|
340
|
+
# Check overall trend: first < last
|
|
341
|
+
if values[0] >= values[-1]:
|
|
342
|
+
return False
|
|
343
|
+
|
|
344
|
+
# Allow one dip but overall upward trend
|
|
345
|
+
dips = sum(1 for i in range(1, len(values)) if values[i] < values[i-1])
|
|
346
|
+
return dips <= 1
|
|
347
|
+
|
|
348
|
+
@staticmethod
|
|
349
|
+
def analyze_dividend_growth(dividend_history: List[Dict]) -> Tuple[Optional[float], bool, Optional[float]]:
|
|
350
|
+
"""Analyze dividend growth rate (3-year CAGR and consistency) and return latest annual dividend"""
|
|
351
|
+
if not dividend_history or 'historical' not in dividend_history:
|
|
352
|
+
return None, False, None
|
|
353
|
+
|
|
354
|
+
dividends = dividend_history['historical']
|
|
355
|
+
if len(dividends) < 4: # Need at least 4 years
|
|
356
|
+
return None, False, None
|
|
357
|
+
|
|
358
|
+
# Sort by date
|
|
359
|
+
dividends = sorted(dividends, key=lambda x: x['date'])
|
|
360
|
+
|
|
361
|
+
# Get annual dividends for last 4 years
|
|
362
|
+
annual_dividends = {}
|
|
363
|
+
for div in dividends:
|
|
364
|
+
year = div['date'][:4]
|
|
365
|
+
annual_dividends[year] = annual_dividends.get(year, 0) + div.get('dividend', 0)
|
|
366
|
+
|
|
367
|
+
if len(annual_dividends) < 4:
|
|
368
|
+
return None, False, None
|
|
369
|
+
|
|
370
|
+
years = sorted(annual_dividends.keys())[-4:]
|
|
371
|
+
div_values = [annual_dividends[y] for y in years]
|
|
372
|
+
|
|
373
|
+
# Calculate 3-year CAGR
|
|
374
|
+
cagr = StockAnalyzer.calculate_cagr(div_values[0], div_values[-1], 3)
|
|
375
|
+
|
|
376
|
+
# Check for consistency (no dividend cuts)
|
|
377
|
+
consistent = all(div_values[i] >= div_values[i-1] * 0.95 for i in range(1, len(div_values)))
|
|
378
|
+
|
|
379
|
+
# Get latest annual dividend (most recent year)
|
|
380
|
+
latest_annual_dividend = div_values[-1]
|
|
381
|
+
|
|
382
|
+
return cagr, consistent, latest_annual_dividend
|
|
383
|
+
|
|
384
|
+
@staticmethod
|
|
385
|
+
def analyze_revenue_growth(income_statements: List[Dict]) -> Tuple[bool, Optional[float]]:
|
|
386
|
+
"""Analyze revenue growth trend"""
|
|
387
|
+
if len(income_statements) < 4:
|
|
388
|
+
return False, None
|
|
389
|
+
|
|
390
|
+
revenues = [stmt.get('revenue', 0) for stmt in income_statements[:4]]
|
|
391
|
+
revenues.reverse() # Oldest to newest
|
|
392
|
+
|
|
393
|
+
positive_trend = StockAnalyzer.check_positive_trend(revenues)
|
|
394
|
+
cagr = StockAnalyzer.calculate_cagr(revenues[0], revenues[-1], 3) if revenues[0] > 0 else None
|
|
395
|
+
|
|
396
|
+
return positive_trend, cagr
|
|
397
|
+
|
|
398
|
+
@staticmethod
|
|
399
|
+
def analyze_eps_growth(income_statements: List[Dict]) -> Tuple[bool, Optional[float]]:
|
|
400
|
+
"""Analyze EPS growth trend"""
|
|
401
|
+
if len(income_statements) < 4:
|
|
402
|
+
return False, None
|
|
403
|
+
|
|
404
|
+
eps_values = [stmt.get('eps', 0) for stmt in income_statements[:4]]
|
|
405
|
+
eps_values.reverse() # Oldest to newest
|
|
406
|
+
|
|
407
|
+
positive_trend = StockAnalyzer.check_positive_trend(eps_values)
|
|
408
|
+
cagr = StockAnalyzer.calculate_cagr(eps_values[0], eps_values[-1], 3) if eps_values[0] > 0 else None
|
|
409
|
+
|
|
410
|
+
return positive_trend, cagr
|
|
411
|
+
|
|
412
|
+
@staticmethod
|
|
413
|
+
def analyze_dividend_sustainability(income_statements: List[Dict], cash_flows: List[Dict], is_reit: bool = False) -> Dict:
|
|
414
|
+
"""
|
|
415
|
+
Analyze dividend sustainability.
|
|
416
|
+
|
|
417
|
+
For REITs, uses FFO-based payout ratio instead of net income-based.
|
|
418
|
+
|
|
419
|
+
Args:
|
|
420
|
+
income_statements: List of income statements (newest first)
|
|
421
|
+
cash_flows: List of cash flow statements (newest first)
|
|
422
|
+
is_reit: Whether the stock is a REIT (uses FFO for payout calculation)
|
|
423
|
+
|
|
424
|
+
Returns:
|
|
425
|
+
Dict with payout_ratio, fcf_payout_ratio, and sustainable flag
|
|
426
|
+
"""
|
|
427
|
+
result = {
|
|
428
|
+
'payout_ratio': None,
|
|
429
|
+
'fcf_payout_ratio': None,
|
|
430
|
+
'sustainable': False
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
if not cash_flows:
|
|
434
|
+
return result
|
|
435
|
+
|
|
436
|
+
latest_cf = cash_flows[0]
|
|
437
|
+
dividends_paid = abs(latest_cf.get('dividendsPaid', 0))
|
|
438
|
+
|
|
439
|
+
# For REITs, use FFO-based payout ratio
|
|
440
|
+
if is_reit:
|
|
441
|
+
ffo_payout = StockAnalyzer.calculate_ffo_payout_ratio(cash_flows)
|
|
442
|
+
if ffo_payout is not None:
|
|
443
|
+
result['payout_ratio'] = ffo_payout
|
|
444
|
+
else:
|
|
445
|
+
# For non-REITs, use traditional net income-based payout ratio
|
|
446
|
+
if income_statements:
|
|
447
|
+
latest_income = income_statements[0]
|
|
448
|
+
net_income = latest_income.get('netIncome', 0)
|
|
449
|
+
|
|
450
|
+
if net_income > 0 and dividends_paid > 0:
|
|
451
|
+
result['payout_ratio'] = (dividends_paid / net_income) * 100
|
|
452
|
+
|
|
453
|
+
# FCF payout ratio (same for both REIT and non-REIT)
|
|
454
|
+
operating_cf = latest_cf.get('operatingCashFlow', 0)
|
|
455
|
+
capex = abs(latest_cf.get('capitalExpenditure', 0))
|
|
456
|
+
fcf = operating_cf - capex
|
|
457
|
+
|
|
458
|
+
if fcf > 0 and dividends_paid > 0:
|
|
459
|
+
result['fcf_payout_ratio'] = (dividends_paid / fcf) * 100
|
|
460
|
+
|
|
461
|
+
# Sustainable if payout ratio < 80% and FCF covers dividends
|
|
462
|
+
if result['payout_ratio'] and result['fcf_payout_ratio']:
|
|
463
|
+
result['sustainable'] = (result['payout_ratio'] < 80 and result['fcf_payout_ratio'] < 100)
|
|
464
|
+
elif result['payout_ratio']:
|
|
465
|
+
# If FCF payout is not available, just check payout ratio
|
|
466
|
+
result['sustainable'] = result['payout_ratio'] < 80
|
|
467
|
+
|
|
468
|
+
return result
|
|
469
|
+
|
|
470
|
+
@staticmethod
|
|
471
|
+
def analyze_financial_health(balance_sheets: List[Dict]) -> Dict:
|
|
472
|
+
"""Analyze financial health metrics"""
|
|
473
|
+
result = {
|
|
474
|
+
'debt_to_equity': None,
|
|
475
|
+
'current_ratio': None,
|
|
476
|
+
'healthy': False
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
if not balance_sheets:
|
|
480
|
+
return result
|
|
481
|
+
|
|
482
|
+
latest_bs = balance_sheets[0]
|
|
483
|
+
|
|
484
|
+
# Debt-to-Equity ratio
|
|
485
|
+
total_debt = latest_bs.get('totalDebt', 0)
|
|
486
|
+
shareholders_equity = latest_bs.get('totalStockholdersEquity', 0)
|
|
487
|
+
|
|
488
|
+
if shareholders_equity > 0:
|
|
489
|
+
result['debt_to_equity'] = total_debt / shareholders_equity
|
|
490
|
+
|
|
491
|
+
# Current ratio
|
|
492
|
+
current_assets = latest_bs.get('totalCurrentAssets', 0)
|
|
493
|
+
current_liabilities = latest_bs.get('totalCurrentLiabilities', 0)
|
|
494
|
+
|
|
495
|
+
if current_liabilities > 0:
|
|
496
|
+
result['current_ratio'] = current_assets / current_liabilities
|
|
497
|
+
|
|
498
|
+
# Healthy if D/E < 2.0 and Current Ratio > 1.0
|
|
499
|
+
if result['debt_to_equity'] is not None and result['current_ratio'] is not None:
|
|
500
|
+
result['healthy'] = (result['debt_to_equity'] < 2.0 and result['current_ratio'] > 1.0)
|
|
501
|
+
|
|
502
|
+
return result
|
|
503
|
+
|
|
504
|
+
@staticmethod
|
|
505
|
+
def analyze_dividend_stability(dividend_history: Dict) -> Dict:
|
|
506
|
+
"""
|
|
507
|
+
Analyze dividend stability and growth consistency.
|
|
508
|
+
|
|
509
|
+
Evaluates:
|
|
510
|
+
- Year-over-year dividend growth
|
|
511
|
+
- Volatility (variation in annual dividends)
|
|
512
|
+
- Consecutive years of growth
|
|
513
|
+
|
|
514
|
+
Args:
|
|
515
|
+
dividend_history: Dict with 'historical' key containing dividend records
|
|
516
|
+
|
|
517
|
+
Returns:
|
|
518
|
+
Dict with is_stable, is_growing, volatility_pct, years_of_growth
|
|
519
|
+
"""
|
|
520
|
+
result = {
|
|
521
|
+
'is_stable': False,
|
|
522
|
+
'is_growing': False,
|
|
523
|
+
'volatility_pct': None,
|
|
524
|
+
'years_of_growth': 0,
|
|
525
|
+
'annual_dividends': {}
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
if not dividend_history or 'historical' not in dividend_history:
|
|
529
|
+
return result
|
|
530
|
+
|
|
531
|
+
dividends = dividend_history['historical']
|
|
532
|
+
if len(dividends) < 4:
|
|
533
|
+
return result
|
|
534
|
+
|
|
535
|
+
# Calculate annual dividends
|
|
536
|
+
annual_dividends = {}
|
|
537
|
+
for div in dividends:
|
|
538
|
+
year = div.get('date', '')[:4]
|
|
539
|
+
if year:
|
|
540
|
+
annual_dividends[year] = annual_dividends.get(year, 0) + div.get('dividend', 0)
|
|
541
|
+
|
|
542
|
+
if len(annual_dividends) < 3:
|
|
543
|
+
return result
|
|
544
|
+
|
|
545
|
+
result['annual_dividends'] = annual_dividends
|
|
546
|
+
|
|
547
|
+
# Get sorted years (newest to oldest for analysis)
|
|
548
|
+
years = sorted(annual_dividends.keys(), reverse=True)
|
|
549
|
+
div_values = [annual_dividends[y] for y in years]
|
|
550
|
+
|
|
551
|
+
# Calculate volatility (coefficient of variation)
|
|
552
|
+
if len(div_values) >= 2:
|
|
553
|
+
avg_div = sum(div_values) / len(div_values)
|
|
554
|
+
if avg_div > 0:
|
|
555
|
+
max_div = max(div_values)
|
|
556
|
+
min_div = min(div_values)
|
|
557
|
+
# Volatility as percentage variation from average
|
|
558
|
+
volatility = ((max_div - min_div) / avg_div) * 100
|
|
559
|
+
result['volatility_pct'] = round(volatility, 1)
|
|
560
|
+
|
|
561
|
+
# Stable if volatility < 50% (allowing some variation)
|
|
562
|
+
result['is_stable'] = volatility < 50
|
|
563
|
+
|
|
564
|
+
# Count consecutive years of growth (from oldest to newest)
|
|
565
|
+
years_oldest_first = sorted(annual_dividends.keys())
|
|
566
|
+
div_values_oldest_first = [annual_dividends[y] for y in years_oldest_first]
|
|
567
|
+
|
|
568
|
+
years_of_growth = 0
|
|
569
|
+
for i in range(1, len(div_values_oldest_first)):
|
|
570
|
+
# Allow small decrease (5%) as "no cut"
|
|
571
|
+
if div_values_oldest_first[i] >= div_values_oldest_first[i-1] * 0.95:
|
|
572
|
+
years_of_growth += 1
|
|
573
|
+
else:
|
|
574
|
+
years_of_growth = 0 # Reset on dividend cut
|
|
575
|
+
|
|
576
|
+
result['years_of_growth'] = years_of_growth
|
|
577
|
+
|
|
578
|
+
# Growing if at least 2 consecutive years of growth and overall uptrend
|
|
579
|
+
if len(div_values_oldest_first) >= 3:
|
|
580
|
+
overall_growth = div_values_oldest_first[-1] > div_values_oldest_first[0]
|
|
581
|
+
result['is_growing'] = years_of_growth >= 2 and overall_growth
|
|
582
|
+
|
|
583
|
+
return result
|
|
584
|
+
|
|
585
|
+
@staticmethod
|
|
586
|
+
def analyze_revenue_trend(income_statements: List[Dict]) -> Dict:
|
|
587
|
+
"""
|
|
588
|
+
Analyze revenue trend for year-over-year growth.
|
|
589
|
+
|
|
590
|
+
Args:
|
|
591
|
+
income_statements: List of income statements (newest first)
|
|
592
|
+
|
|
593
|
+
Returns:
|
|
594
|
+
Dict with is_uptrend, years_of_growth, cagr
|
|
595
|
+
"""
|
|
596
|
+
result = {
|
|
597
|
+
'is_uptrend': False,
|
|
598
|
+
'years_of_growth': 0,
|
|
599
|
+
'cagr': None
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
if len(income_statements) < 3:
|
|
603
|
+
return result
|
|
604
|
+
|
|
605
|
+
# Get revenues (newest first in input, reverse for analysis)
|
|
606
|
+
revenues = [stmt.get('revenue', 0) for stmt in income_statements[:4]]
|
|
607
|
+
revenues_oldest_first = list(reversed(revenues))
|
|
608
|
+
|
|
609
|
+
# Count years of growth
|
|
610
|
+
years_of_growth = 0
|
|
611
|
+
for i in range(1, len(revenues_oldest_first)):
|
|
612
|
+
if revenues_oldest_first[i] > revenues_oldest_first[i-1]:
|
|
613
|
+
years_of_growth += 1
|
|
614
|
+
else:
|
|
615
|
+
# Allow one dip but don't reset completely
|
|
616
|
+
pass
|
|
617
|
+
|
|
618
|
+
result['years_of_growth'] = years_of_growth
|
|
619
|
+
|
|
620
|
+
# Calculate CAGR
|
|
621
|
+
if revenues_oldest_first[0] > 0 and revenues_oldest_first[-1] > 0:
|
|
622
|
+
years = len(revenues_oldest_first) - 1
|
|
623
|
+
if years > 0:
|
|
624
|
+
cagr = (pow(revenues_oldest_first[-1] / revenues_oldest_first[0], 1 / years) - 1) * 100
|
|
625
|
+
result['cagr'] = round(cagr, 2)
|
|
626
|
+
|
|
627
|
+
# Uptrend if overall growth and at least 2 years of growth
|
|
628
|
+
overall_growth = revenues_oldest_first[-1] > revenues_oldest_first[0]
|
|
629
|
+
result['is_uptrend'] = overall_growth and years_of_growth >= 2
|
|
630
|
+
|
|
631
|
+
return result
|
|
632
|
+
|
|
633
|
+
@staticmethod
|
|
634
|
+
def analyze_earnings_trend(income_statements: List[Dict]) -> Dict:
|
|
635
|
+
"""
|
|
636
|
+
Analyze earnings/profit trend for year-over-year growth.
|
|
637
|
+
|
|
638
|
+
Args:
|
|
639
|
+
income_statements: List of income statements (newest first)
|
|
640
|
+
|
|
641
|
+
Returns:
|
|
642
|
+
Dict with is_uptrend, years_of_growth, cagr
|
|
643
|
+
"""
|
|
644
|
+
result = {
|
|
645
|
+
'is_uptrend': False,
|
|
646
|
+
'years_of_growth': 0,
|
|
647
|
+
'cagr': None
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
if len(income_statements) < 3:
|
|
651
|
+
return result
|
|
652
|
+
|
|
653
|
+
# Get net income (newest first in input, reverse for analysis)
|
|
654
|
+
earnings = [stmt.get('netIncome', 0) for stmt in income_statements[:4]]
|
|
655
|
+
earnings_oldest_first = list(reversed(earnings))
|
|
656
|
+
|
|
657
|
+
# Check for negative earnings (not a good sign)
|
|
658
|
+
if any(e <= 0 for e in earnings_oldest_first):
|
|
659
|
+
return result
|
|
660
|
+
|
|
661
|
+
# Count years of growth
|
|
662
|
+
years_of_growth = 0
|
|
663
|
+
for i in range(1, len(earnings_oldest_first)):
|
|
664
|
+
if earnings_oldest_first[i] > earnings_oldest_first[i-1]:
|
|
665
|
+
years_of_growth += 1
|
|
666
|
+
|
|
667
|
+
result['years_of_growth'] = years_of_growth
|
|
668
|
+
|
|
669
|
+
# Calculate CAGR
|
|
670
|
+
if earnings_oldest_first[0] > 0 and earnings_oldest_first[-1] > 0:
|
|
671
|
+
years = len(earnings_oldest_first) - 1
|
|
672
|
+
if years > 0:
|
|
673
|
+
cagr = (pow(earnings_oldest_first[-1] / earnings_oldest_first[0], 1 / years) - 1) * 100
|
|
674
|
+
result['cagr'] = round(cagr, 2)
|
|
675
|
+
|
|
676
|
+
# Uptrend if overall growth and at least 2 years of growth
|
|
677
|
+
overall_growth = earnings_oldest_first[-1] > earnings_oldest_first[0]
|
|
678
|
+
result['is_uptrend'] = overall_growth and years_of_growth >= 2
|
|
679
|
+
|
|
680
|
+
return result
|
|
681
|
+
|
|
682
|
+
@staticmethod
|
|
683
|
+
def calculate_stability_score(stability: Dict) -> float:
|
|
684
|
+
"""
|
|
685
|
+
Calculate a stability score based on dividend stability metrics.
|
|
686
|
+
|
|
687
|
+
Args:
|
|
688
|
+
stability: Dict from analyze_dividend_stability
|
|
689
|
+
|
|
690
|
+
Returns:
|
|
691
|
+
Score from 0-100
|
|
692
|
+
"""
|
|
693
|
+
score = 0
|
|
694
|
+
|
|
695
|
+
# Stability bonus (max 40 points)
|
|
696
|
+
if stability.get('is_stable'):
|
|
697
|
+
score += 40
|
|
698
|
+
elif stability.get('volatility_pct') is not None:
|
|
699
|
+
# Partial credit for lower volatility
|
|
700
|
+
volatility = stability['volatility_pct']
|
|
701
|
+
if volatility < 100:
|
|
702
|
+
score += max(0, 40 - (volatility * 0.4))
|
|
703
|
+
|
|
704
|
+
# Growth bonus (max 30 points)
|
|
705
|
+
if stability.get('is_growing'):
|
|
706
|
+
score += 30
|
|
707
|
+
|
|
708
|
+
# Years of growth bonus (max 30 points, 10 per year)
|
|
709
|
+
years = stability.get('years_of_growth', 0)
|
|
710
|
+
score += min(years * 10, 30)
|
|
711
|
+
|
|
712
|
+
return round(score, 1)
|
|
713
|
+
|
|
714
|
+
@staticmethod
|
|
715
|
+
def calculate_quality_score(key_metrics: List[Dict], income_statements: List[Dict]) -> Dict:
|
|
716
|
+
"""Calculate quality scores (ROE, Profit Margin)"""
|
|
717
|
+
result = {
|
|
718
|
+
'roe': None,
|
|
719
|
+
'profit_margin': None,
|
|
720
|
+
'quality_score': 0
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
if not key_metrics or not income_statements:
|
|
724
|
+
return result
|
|
725
|
+
|
|
726
|
+
# ROE (Return on Equity)
|
|
727
|
+
latest_metrics = key_metrics[0]
|
|
728
|
+
result['roe'] = latest_metrics.get('roe')
|
|
729
|
+
|
|
730
|
+
# Profit Margin
|
|
731
|
+
latest_income = income_statements[0]
|
|
732
|
+
revenue = latest_income.get('revenue', 0)
|
|
733
|
+
net_income = latest_income.get('netIncome', 0)
|
|
734
|
+
|
|
735
|
+
if revenue > 0:
|
|
736
|
+
result['profit_margin'] = (net_income / revenue) * 100
|
|
737
|
+
|
|
738
|
+
# Quality score (0-100)
|
|
739
|
+
score = 0
|
|
740
|
+
if result['roe']:
|
|
741
|
+
roe_pct = result['roe'] * 100
|
|
742
|
+
score += min(roe_pct / 20 * 50, 50) # Max 50 points for 20%+ ROE
|
|
743
|
+
|
|
744
|
+
if result['profit_margin']:
|
|
745
|
+
score += min(result['profit_margin'] / 15 * 50, 50) # Max 50 points for 15%+ margin
|
|
746
|
+
|
|
747
|
+
result['quality_score'] = round(score, 1)
|
|
748
|
+
|
|
749
|
+
return result
|
|
750
|
+
|
|
751
|
+
|
|
752
|
+
def screen_value_dividend_stocks(fmp_api_key: str, top_n: int = 20,
|
|
753
|
+
finviz_symbols: Optional[Set[str]] = None) -> List[Dict]:
|
|
754
|
+
"""
|
|
755
|
+
Main screening function
|
|
756
|
+
|
|
757
|
+
Args:
|
|
758
|
+
fmp_api_key: Financial Modeling Prep API key
|
|
759
|
+
top_n: Number of top stocks to return
|
|
760
|
+
finviz_symbols: Optional set of symbols from FINVIZ pre-screening
|
|
761
|
+
|
|
762
|
+
Returns:
|
|
763
|
+
List of stocks with detailed analysis, sorted by composite score
|
|
764
|
+
"""
|
|
765
|
+
client = FMPClient(fmp_api_key)
|
|
766
|
+
analyzer = StockAnalyzer()
|
|
767
|
+
rsi_calc = RSICalculator()
|
|
768
|
+
|
|
769
|
+
# Step 1: Get candidate list
|
|
770
|
+
if finviz_symbols:
|
|
771
|
+
print(f"Step 1: Using FINVIZ pre-screened symbols ({len(finviz_symbols)} stocks)...", file=sys.stderr)
|
|
772
|
+
# Convert FINVIZ symbols to candidate format for FMP analysis
|
|
773
|
+
# Fetch quote + profile to get sector information
|
|
774
|
+
candidates = []
|
|
775
|
+
print("Fetching quote and profile data from FMP for FINVIZ symbols...", file=sys.stderr)
|
|
776
|
+
for symbol in finviz_symbols:
|
|
777
|
+
quote = client._get(f'quote/{symbol}')
|
|
778
|
+
if quote and isinstance(quote, list) and len(quote) > 0:
|
|
779
|
+
stock_data = quote[0].copy()
|
|
780
|
+
# Fetch profile for sector information
|
|
781
|
+
profile = client.get_company_profile(symbol)
|
|
782
|
+
if profile:
|
|
783
|
+
stock_data['sector'] = profile.get('sector', 'N/A')
|
|
784
|
+
stock_data['industry'] = profile.get('industry', '')
|
|
785
|
+
stock_data['companyName'] = profile.get('companyName', stock_data.get('name', ''))
|
|
786
|
+
candidates.append(stock_data)
|
|
787
|
+
|
|
788
|
+
if client.rate_limit_reached:
|
|
789
|
+
print(f"⚠️ FMP rate limit reached while fetching quotes. Using {len(candidates)} symbols.", file=sys.stderr)
|
|
790
|
+
break
|
|
791
|
+
|
|
792
|
+
print(f"Retrieved quote and profile data for {len(candidates)} symbols from FMP", file=sys.stderr)
|
|
793
|
+
else:
|
|
794
|
+
print("Step 1: Initial screening using FMP Stock Screener (Dividend Yield >= 3.0%, P/E <= 20, P/B <= 2)...", file=sys.stderr)
|
|
795
|
+
print("Criteria: Div Yield >= 3.0%, Div Growth >= 4.0% CAGR", file=sys.stderr)
|
|
796
|
+
candidates = client.screen_stocks(dividend_yield_min=3.0, pe_max=20, pb_max=2)
|
|
797
|
+
print(f"Found {len(candidates)} initial candidates", file=sys.stderr)
|
|
798
|
+
|
|
799
|
+
if not candidates:
|
|
800
|
+
print("No stocks found matching initial criteria", file=sys.stderr)
|
|
801
|
+
return []
|
|
802
|
+
|
|
803
|
+
results = []
|
|
804
|
+
|
|
805
|
+
print(f"\nStep 2: Detailed analysis of candidates...", file=sys.stderr)
|
|
806
|
+
print(f"Note: Analysis will continue until API rate limit is reached", file=sys.stderr)
|
|
807
|
+
|
|
808
|
+
for i, stock in enumerate(candidates, 1): # Analyze all candidates until rate limit
|
|
809
|
+
symbol = stock.get('symbol', '')
|
|
810
|
+
company_name = stock.get('name', stock.get('companyName', ''))
|
|
811
|
+
|
|
812
|
+
print(f"[{i}/{len(candidates)}] Analyzing {symbol} - {company_name}...", file=sys.stderr)
|
|
813
|
+
|
|
814
|
+
# Check if rate limit reached
|
|
815
|
+
if client.rate_limit_reached:
|
|
816
|
+
print(f"\n⚠️ API rate limit reached after analyzing {i-1} stocks.", file=sys.stderr)
|
|
817
|
+
print(f"Returning results collected so far: {len(results)} qualified stocks", file=sys.stderr)
|
|
818
|
+
break
|
|
819
|
+
|
|
820
|
+
# Fetch detailed data
|
|
821
|
+
income_stmts = client.get_income_statement(symbol, limit=5)
|
|
822
|
+
if client.rate_limit_reached:
|
|
823
|
+
break
|
|
824
|
+
|
|
825
|
+
balance_sheets = client.get_balance_sheet(symbol, limit=5)
|
|
826
|
+
if client.rate_limit_reached:
|
|
827
|
+
break
|
|
828
|
+
|
|
829
|
+
cash_flows = client.get_cash_flow(symbol, limit=5)
|
|
830
|
+
if client.rate_limit_reached:
|
|
831
|
+
break
|
|
832
|
+
|
|
833
|
+
key_metrics = client.get_key_metrics(symbol, limit=5)
|
|
834
|
+
if client.rate_limit_reached:
|
|
835
|
+
break
|
|
836
|
+
|
|
837
|
+
dividend_history = client.get_dividend_history(symbol)
|
|
838
|
+
if client.rate_limit_reached:
|
|
839
|
+
break
|
|
840
|
+
|
|
841
|
+
# Fetch historical prices for RSI calculation
|
|
842
|
+
historical_prices = client.get_historical_prices(symbol, days=30)
|
|
843
|
+
if client.rate_limit_reached:
|
|
844
|
+
break
|
|
845
|
+
|
|
846
|
+
# Calculate RSI for technical analysis
|
|
847
|
+
rsi = None
|
|
848
|
+
if historical_prices and len(historical_prices) >= 20:
|
|
849
|
+
# Prices come newest first, reverse for RSI calculation
|
|
850
|
+
prices = [p.get('close', 0) for p in reversed(historical_prices)]
|
|
851
|
+
rsi = rsi_calc.calculate_rsi(prices, period=14)
|
|
852
|
+
|
|
853
|
+
if rsi is None:
|
|
854
|
+
print(f" ⚠️ RSI calculation failed (insufficient price data)", file=sys.stderr)
|
|
855
|
+
continue
|
|
856
|
+
|
|
857
|
+
# Store RSI for later filtering
|
|
858
|
+
stock['_rsi'] = rsi
|
|
859
|
+
|
|
860
|
+
# Fetch profile for sector information if not already present
|
|
861
|
+
if not stock.get('sector') or stock.get('sector') == 'N/A':
|
|
862
|
+
profile = client.get_company_profile(symbol)
|
|
863
|
+
if profile:
|
|
864
|
+
stock['sector'] = profile.get('sector', 'N/A')
|
|
865
|
+
stock['industry'] = profile.get('industry', '')
|
|
866
|
+
if client.rate_limit_reached:
|
|
867
|
+
break
|
|
868
|
+
|
|
869
|
+
# Skip if insufficient data
|
|
870
|
+
if len(income_stmts) < 4:
|
|
871
|
+
print(f" ⚠️ Insufficient income statement data", file=sys.stderr)
|
|
872
|
+
continue
|
|
873
|
+
|
|
874
|
+
# Analyze dividend growth and get latest annual dividend
|
|
875
|
+
div_cagr, div_consistent, annual_dividend = analyzer.analyze_dividend_growth(dividend_history)
|
|
876
|
+
if not div_cagr or div_cagr < 4.0:
|
|
877
|
+
print(f" ⚠️ Dividend CAGR < 4% (or no data)", file=sys.stderr)
|
|
878
|
+
continue
|
|
879
|
+
|
|
880
|
+
# Calculate actual dividend yield
|
|
881
|
+
current_price = stock.get('price', 0)
|
|
882
|
+
if current_price <= 0 or not annual_dividend:
|
|
883
|
+
print(f" ⚠️ Cannot calculate dividend yield (price or dividend data missing)", file=sys.stderr)
|
|
884
|
+
continue
|
|
885
|
+
|
|
886
|
+
actual_dividend_yield = (annual_dividend / current_price) * 100
|
|
887
|
+
|
|
888
|
+
# Verify dividend yield >= 3.0%
|
|
889
|
+
if actual_dividend_yield < 3.0:
|
|
890
|
+
print(f" ⚠️ Dividend yield {actual_dividend_yield:.2f}% < 3.0%", file=sys.stderr)
|
|
891
|
+
continue
|
|
892
|
+
|
|
893
|
+
# Analyze revenue growth
|
|
894
|
+
revenue_positive, revenue_cagr = analyzer.analyze_revenue_growth(income_stmts)
|
|
895
|
+
if not revenue_positive:
|
|
896
|
+
print(f" ⚠️ Revenue trend not positive", file=sys.stderr)
|
|
897
|
+
continue
|
|
898
|
+
|
|
899
|
+
# Analyze EPS growth
|
|
900
|
+
eps_positive, eps_cagr = analyzer.analyze_eps_growth(income_stmts)
|
|
901
|
+
if not eps_positive:
|
|
902
|
+
print(f" ⚠️ EPS trend not positive", file=sys.stderr)
|
|
903
|
+
continue
|
|
904
|
+
|
|
905
|
+
# NEW: Check dividend stability - filter out highly volatile dividends
|
|
906
|
+
dividend_stability = analyzer.analyze_dividend_stability(dividend_history)
|
|
907
|
+
if dividend_stability['volatility_pct'] and dividend_stability['volatility_pct'] > 100:
|
|
908
|
+
# Allow if consistently growing despite volatility
|
|
909
|
+
if not dividend_stability['is_growing'] or dividend_stability['years_of_growth'] < 3:
|
|
910
|
+
print(f" ⚠️ Dividend too volatile ({dividend_stability['volatility_pct']:.1f}%) and not consistently growing", file=sys.stderr)
|
|
911
|
+
continue
|
|
912
|
+
|
|
913
|
+
# Check if this is a REIT (uses different payout ratio calculation)
|
|
914
|
+
is_reit = analyzer.is_reit(stock)
|
|
915
|
+
|
|
916
|
+
# Additional analysis
|
|
917
|
+
sustainability = analyzer.analyze_dividend_sustainability(income_stmts, cash_flows, is_reit=is_reit)
|
|
918
|
+
financial_health = analyzer.analyze_financial_health(balance_sheets)
|
|
919
|
+
quality = analyzer.calculate_quality_score(key_metrics, income_stmts)
|
|
920
|
+
|
|
921
|
+
# Calculate stability score (dividend_stability already analyzed above)
|
|
922
|
+
stability_score = analyzer.calculate_stability_score(dividend_stability)
|
|
923
|
+
|
|
924
|
+
# NEW: Analyze revenue and earnings trends
|
|
925
|
+
revenue_trend = analyzer.analyze_revenue_trend(income_stmts)
|
|
926
|
+
earnings_trend = analyzer.analyze_earnings_trend(income_stmts)
|
|
927
|
+
|
|
928
|
+
# Calculate composite score (updated to include stability)
|
|
929
|
+
composite_score = 0
|
|
930
|
+
composite_score += min(div_cagr / 10 * 15, 15) # Max 15 points for 10%+ div growth
|
|
931
|
+
composite_score += stability_score * 0.2 # Max 20 points from stability (100 * 0.2)
|
|
932
|
+
composite_score += min((revenue_cagr or 0) / 10 * 10, 10) # Max 10 points for revenue
|
|
933
|
+
composite_score += min((eps_cagr or 0) / 15 * 10, 10) # Max 10 points for EPS
|
|
934
|
+
composite_score += 10 if sustainability['sustainable'] else 0
|
|
935
|
+
composite_score += 10 if financial_health['healthy'] else 0
|
|
936
|
+
composite_score += quality['quality_score'] * 0.25 # Max 25 points from quality
|
|
937
|
+
|
|
938
|
+
result = {
|
|
939
|
+
'symbol': symbol,
|
|
940
|
+
'company_name': company_name,
|
|
941
|
+
'sector': stock.get('sector', 'N/A'),
|
|
942
|
+
'market_cap': stock.get('marketCap', 0),
|
|
943
|
+
'price': stock.get('price', 0),
|
|
944
|
+
'dividend_yield': round(actual_dividend_yield, 2),
|
|
945
|
+
'annual_dividend': round(annual_dividend, 2),
|
|
946
|
+
'pe_ratio': stock.get('pe', 0),
|
|
947
|
+
'pb_ratio': stock.get('priceToBook', 0),
|
|
948
|
+
'rsi': rsi,
|
|
949
|
+
'dividend_cagr_3y': round(div_cagr, 2),
|
|
950
|
+
'dividend_consistent': div_consistent,
|
|
951
|
+
'dividend_stable': dividend_stability['is_stable'],
|
|
952
|
+
'dividend_growing': dividend_stability['is_growing'],
|
|
953
|
+
'dividend_volatility_pct': dividend_stability['volatility_pct'],
|
|
954
|
+
'dividend_years_of_growth': dividend_stability['years_of_growth'],
|
|
955
|
+
'revenue_cagr_3y': round(revenue_cagr, 2) if revenue_cagr else None,
|
|
956
|
+
'revenue_uptrend': revenue_trend['is_uptrend'],
|
|
957
|
+
'revenue_years_of_growth': revenue_trend['years_of_growth'],
|
|
958
|
+
'eps_cagr_3y': round(eps_cagr, 2) if eps_cagr else None,
|
|
959
|
+
'earnings_uptrend': earnings_trend['is_uptrend'],
|
|
960
|
+
'earnings_years_of_growth': earnings_trend['years_of_growth'],
|
|
961
|
+
'payout_ratio': round(sustainability['payout_ratio'], 1) if sustainability['payout_ratio'] else None,
|
|
962
|
+
'fcf_payout_ratio': round(sustainability['fcf_payout_ratio'], 1) if sustainability['fcf_payout_ratio'] else None,
|
|
963
|
+
'dividend_sustainable': sustainability['sustainable'],
|
|
964
|
+
'debt_to_equity': round(financial_health['debt_to_equity'], 2) if financial_health['debt_to_equity'] else None,
|
|
965
|
+
'current_ratio': round(financial_health['current_ratio'], 2) if financial_health['current_ratio'] else None,
|
|
966
|
+
'financially_healthy': financial_health['healthy'],
|
|
967
|
+
'roe': round(key_metrics[0].get('roe', 0) * 100, 1) if key_metrics else None,
|
|
968
|
+
'profit_margin': round(quality['profit_margin'], 1) if quality['profit_margin'] else None,
|
|
969
|
+
'quality_score': quality['quality_score'],
|
|
970
|
+
'stability_score': stability_score,
|
|
971
|
+
'composite_score': round(composite_score, 1)
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
results.append(result)
|
|
975
|
+
print(f" ✅ Passed all criteria (RSI: {rsi:.1f}, Score: {result['composite_score']})", file=sys.stderr)
|
|
976
|
+
|
|
977
|
+
# Step 3: Filter by RSI
|
|
978
|
+
# Prefer RSI <= 40 (oversold), but if none found, return lowest RSI stocks
|
|
979
|
+
oversold_results = [r for r in results if r['rsi'] <= 40]
|
|
980
|
+
|
|
981
|
+
if oversold_results:
|
|
982
|
+
print(f"\nStep 3: Found {len(oversold_results)} oversold stocks (RSI <= 40)", file=sys.stderr)
|
|
983
|
+
# Sort oversold stocks by composite score
|
|
984
|
+
oversold_results.sort(key=lambda x: x['composite_score'], reverse=True)
|
|
985
|
+
results = oversold_results[:top_n]
|
|
986
|
+
else:
|
|
987
|
+
print(f"\nStep 3: No oversold stocks found (RSI <= 40). Returning lowest RSI stocks.", file=sys.stderr)
|
|
988
|
+
# Sort by RSI (lowest first), then by composite score
|
|
989
|
+
results.sort(key=lambda x: (x['rsi'], -x['composite_score']))
|
|
990
|
+
results = results[:top_n]
|
|
991
|
+
|
|
992
|
+
return results
|
|
993
|
+
|
|
994
|
+
|
|
995
|
+
def main():
|
|
996
|
+
parser = argparse.ArgumentParser(
|
|
997
|
+
description='Screen value dividend stocks using FINVIZ + FMP API (two-stage approach)',
|
|
998
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
999
|
+
epilog='''
|
|
1000
|
+
Examples:
|
|
1001
|
+
# Two-stage screening: FINVIZ pre-screen + FMP detailed analysis (RECOMMENDED)
|
|
1002
|
+
python3 screen_dividend_stocks.py --use-finviz
|
|
1003
|
+
|
|
1004
|
+
# FMP-only screening (original method)
|
|
1005
|
+
python3 screen_dividend_stocks.py
|
|
1006
|
+
|
|
1007
|
+
# Provide API keys as arguments
|
|
1008
|
+
python3 screen_dividend_stocks.py --use-finviz --fmp-api-key YOUR_FMP_KEY --finviz-api-key YOUR_FINVIZ_KEY
|
|
1009
|
+
|
|
1010
|
+
# Custom output location
|
|
1011
|
+
python3 screen_dividend_stocks.py --use-finviz --output /path/to/results.json
|
|
1012
|
+
|
|
1013
|
+
# Get top 50 stocks
|
|
1014
|
+
python3 screen_dividend_stocks.py --use-finviz --top 50
|
|
1015
|
+
|
|
1016
|
+
Environment Variables:
|
|
1017
|
+
FMP_API_KEY - Financial Modeling Prep API key
|
|
1018
|
+
FINVIZ_API_KEY - FINVIZ Elite API key (required for --use-finviz)
|
|
1019
|
+
'''
|
|
1020
|
+
)
|
|
1021
|
+
|
|
1022
|
+
parser.add_argument(
|
|
1023
|
+
'--fmp-api-key',
|
|
1024
|
+
type=str,
|
|
1025
|
+
help='FMP API key (or set FMP_API_KEY environment variable)'
|
|
1026
|
+
)
|
|
1027
|
+
|
|
1028
|
+
parser.add_argument(
|
|
1029
|
+
'--finviz-api-key',
|
|
1030
|
+
type=str,
|
|
1031
|
+
help='FINVIZ Elite API key (or set FINVIZ_API_KEY environment variable)'
|
|
1032
|
+
)
|
|
1033
|
+
|
|
1034
|
+
parser.add_argument(
|
|
1035
|
+
'--use-finviz',
|
|
1036
|
+
action='store_true',
|
|
1037
|
+
help='Use FINVIZ Elite API for pre-screening (recommended to reduce FMP API calls)'
|
|
1038
|
+
)
|
|
1039
|
+
|
|
1040
|
+
# Determine default output directory (project root logs/ folder)
|
|
1041
|
+
script_dir = os.path.dirname(os.path.abspath(__file__))
|
|
1042
|
+
# Navigate from .claude/skills/value-dividend-screener/scripts to project root
|
|
1043
|
+
project_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(script_dir))))
|
|
1044
|
+
logs_dir = os.path.join(project_root, 'logs')
|
|
1045
|
+
default_output = os.path.join(logs_dir, 'dividend_screener_results.json')
|
|
1046
|
+
|
|
1047
|
+
parser.add_argument(
|
|
1048
|
+
'--output',
|
|
1049
|
+
type=str,
|
|
1050
|
+
default=default_output,
|
|
1051
|
+
help=f'Output JSON file path (default: {default_output})'
|
|
1052
|
+
)
|
|
1053
|
+
|
|
1054
|
+
parser.add_argument(
|
|
1055
|
+
'--top',
|
|
1056
|
+
type=int,
|
|
1057
|
+
default=20,
|
|
1058
|
+
help='Number of top stocks to return (default: 20)'
|
|
1059
|
+
)
|
|
1060
|
+
|
|
1061
|
+
args = parser.parse_args()
|
|
1062
|
+
|
|
1063
|
+
# Get FMP API key
|
|
1064
|
+
fmp_api_key = args.fmp_api_key or os.environ.get('FMP_API_KEY')
|
|
1065
|
+
if not fmp_api_key:
|
|
1066
|
+
print("ERROR: FMP API key required. Provide via --fmp-api-key or FMP_API_KEY environment variable", file=sys.stderr)
|
|
1067
|
+
sys.exit(1)
|
|
1068
|
+
|
|
1069
|
+
# FINVIZ pre-screening (optional)
|
|
1070
|
+
finviz_symbols = None
|
|
1071
|
+
if args.use_finviz:
|
|
1072
|
+
finviz_api_key = args.finviz_api_key or os.environ.get('FINVIZ_API_KEY')
|
|
1073
|
+
if not finviz_api_key:
|
|
1074
|
+
print("ERROR: FINVIZ API key required when using --use-finviz. Provide via --finviz-api-key or FINVIZ_API_KEY environment variable", file=sys.stderr)
|
|
1075
|
+
sys.exit(1)
|
|
1076
|
+
|
|
1077
|
+
print(f"\n{'='*60}", file=sys.stderr)
|
|
1078
|
+
print("VALUE DIVIDEND STOCK SCREENER (TWO-STAGE)", file=sys.stderr)
|
|
1079
|
+
print(f"{'='*60}\n", file=sys.stderr)
|
|
1080
|
+
|
|
1081
|
+
finviz_client = FINVIZClient(finviz_api_key)
|
|
1082
|
+
finviz_symbols = finviz_client.screen_stocks()
|
|
1083
|
+
|
|
1084
|
+
if not finviz_symbols:
|
|
1085
|
+
print("ERROR: FINVIZ pre-screening failed or returned no results", file=sys.stderr)
|
|
1086
|
+
sys.exit(1)
|
|
1087
|
+
else:
|
|
1088
|
+
print(f"\n{'='*60}", file=sys.stderr)
|
|
1089
|
+
print("VALUE DIVIDEND STOCK SCREENER (FMP ONLY)", file=sys.stderr)
|
|
1090
|
+
print(f"{'='*60}\n", file=sys.stderr)
|
|
1091
|
+
|
|
1092
|
+
# Run detailed screening
|
|
1093
|
+
results = screen_value_dividend_stocks(fmp_api_key, top_n=args.top, finviz_symbols=finviz_symbols)
|
|
1094
|
+
|
|
1095
|
+
if not results:
|
|
1096
|
+
print("\nNo stocks found matching all criteria.", file=sys.stderr)
|
|
1097
|
+
sys.exit(1)
|
|
1098
|
+
|
|
1099
|
+
# Add metadata
|
|
1100
|
+
output_data = {
|
|
1101
|
+
'metadata': {
|
|
1102
|
+
'generated_at': datetime.utcnow().isoformat() + 'Z',
|
|
1103
|
+
'criteria': {
|
|
1104
|
+
'dividend_yield_min': 3.0,
|
|
1105
|
+
'pe_ratio_max': 20,
|
|
1106
|
+
'pb_ratio_max': 2,
|
|
1107
|
+
'dividend_cagr_min': 4.0,
|
|
1108
|
+
'dividend_stability': 'low volatility, year-over-year growth',
|
|
1109
|
+
'revenue_trend': 'positive over 3 years',
|
|
1110
|
+
'eps_trend': 'positive over 3 years'
|
|
1111
|
+
},
|
|
1112
|
+
'scoring': {
|
|
1113
|
+
'dividend_growth': 'max 15 points (10%+ CAGR)',
|
|
1114
|
+
'dividend_stability': 'max 20 points (stable, growing)',
|
|
1115
|
+
'revenue_growth': 'max 10 points (10%+ CAGR)',
|
|
1116
|
+
'eps_growth': 'max 10 points (15%+ CAGR)',
|
|
1117
|
+
'dividend_sustainable': '10 points',
|
|
1118
|
+
'financial_health': '10 points',
|
|
1119
|
+
'quality_score': 'max 25 points'
|
|
1120
|
+
},
|
|
1121
|
+
'total_results': len(results)
|
|
1122
|
+
},
|
|
1123
|
+
'stocks': results
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
# Write to file
|
|
1127
|
+
os.makedirs(os.path.dirname(args.output), exist_ok=True)
|
|
1128
|
+
with open(args.output, 'w', encoding='utf-8') as f:
|
|
1129
|
+
json.dump(output_data, f, indent=2, ensure_ascii=False)
|
|
1130
|
+
|
|
1131
|
+
print(f"\n{'='*60}", file=sys.stderr)
|
|
1132
|
+
print(f"✅ Screening complete! Found {len(results)} stocks.", file=sys.stderr)
|
|
1133
|
+
print(f"📄 Results saved to: {args.output}", file=sys.stderr)
|
|
1134
|
+
print(f"{'='*60}\n", file=sys.stderr)
|
|
1135
|
+
|
|
1136
|
+
|
|
1137
|
+
if __name__ == '__main__':
|
|
1138
|
+
main()
|