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.
Files changed (362) hide show
  1. package/.claude/skills/README.md +80 -0
  2. package/.claude/skills/backtest-expert/SKILL.md +206 -0
  3. package/.claude/skills/backtest-expert/references/failed_tests.md +236 -0
  4. package/.claude/skills/backtest-expert/references/methodology.md +227 -0
  5. package/.claude/skills/breadth-chart-analyst/SKILL.md +583 -0
  6. package/.claude/skills/breadth-chart-analyst/assets/SP500_Breadth_Index_200MA_8MA.jpeg +0 -0
  7. package/.claude/skills/breadth-chart-analyst/assets/US_Stock_Market_Uptrend_Ratio.jpeg +0 -0
  8. package/.claude/skills/breadth-chart-analyst/assets/breadth_analysis_template.md +558 -0
  9. package/.claude/skills/breadth-chart-analyst/references/breadth_chart_methodology.md +590 -0
  10. package/.claude/skills/canslim-screener/SKILL.md +599 -0
  11. package/.claude/skills/canslim-screener/references/canslim_methodology.md +606 -0
  12. package/.claude/skills/canslim-screener/references/fmp_api_endpoints.md +707 -0
  13. package/.claude/skills/canslim-screener/references/interpretation_guide.md +516 -0
  14. package/.claude/skills/canslim-screener/references/scoring_system.md +597 -0
  15. package/.claude/skills/canslim-screener/scripts/calculators/earnings_calculator.py +343 -0
  16. package/.claude/skills/canslim-screener/scripts/calculators/growth_calculator.py +334 -0
  17. package/.claude/skills/canslim-screener/scripts/calculators/institutional_calculator.py +347 -0
  18. package/.claude/skills/canslim-screener/scripts/calculators/leadership_calculator.py +380 -0
  19. package/.claude/skills/canslim-screener/scripts/calculators/market_calculator.py +244 -0
  20. package/.claude/skills/canslim-screener/scripts/calculators/new_highs_calculator.py +194 -0
  21. package/.claude/skills/canslim-screener/scripts/calculators/supply_demand_calculator.py +221 -0
  22. package/.claude/skills/canslim-screener/scripts/finviz_stock_client.py +227 -0
  23. package/.claude/skills/canslim-screener/scripts/fmp_client.py +393 -0
  24. package/.claude/skills/canslim-screener/scripts/report_generator.py +405 -0
  25. package/.claude/skills/canslim-screener/scripts/scorer.py +625 -0
  26. package/.claude/skills/canslim-screener/scripts/screen_canslim.py +361 -0
  27. package/.claude/skills/canslim-screener/scripts/test_institutional_endpoint.py +109 -0
  28. package/.claude/skills/chart/SKILL.md +20 -0
  29. package/.claude/skills/dividend-growth-pullback-screener/SKILL.md +322 -0
  30. package/.claude/skills/dividend-growth-pullback-screener/references/dividend_growth_compounding.md +400 -0
  31. package/.claude/skills/dividend-growth-pullback-screener/references/fmp_api_guide.md +642 -0
  32. package/.claude/skills/dividend-growth-pullback-screener/references/rsi_oversold_strategy.md +333 -0
  33. package/.claude/skills/dividend-growth-pullback-screener/scripts/screen_dividend_growth_rsi.py +1155 -0
  34. package/.claude/skills/earnings-calendar/SKILL.md +721 -0
  35. package/.claude/skills/earnings-calendar/assets/earnings_report_template.md +102 -0
  36. package/.claude/skills/earnings-calendar/references/fmp_api_guide.md +590 -0
  37. package/.claude/skills/earnings-calendar/scripts/fetch_earnings_fmp.py +443 -0
  38. package/.claude/skills/earnings-calendar/scripts/generate_report.py +366 -0
  39. package/.claude/skills/economic-calendar-fetcher/SKILL.md +365 -0
  40. package/.claude/skills/economic-calendar-fetcher/references/fmp_api_documentation.md +345 -0
  41. package/.claude/skills/economic-calendar-fetcher/scripts/get_economic_calendar.py +267 -0
  42. package/.claude/skills/ftd-detector/SKILL.md +147 -0
  43. package/.claude/skills/ftd-detector/references/ftd_methodology.md +188 -0
  44. package/.claude/skills/ftd-detector/references/post_ftd_guide.md +185 -0
  45. package/.claude/skills/ftd-detector/scripts/fmp_client.py +158 -0
  46. package/.claude/skills/ftd-detector/scripts/ftd_detector.py +280 -0
  47. package/.claude/skills/ftd-detector/scripts/post_ftd_monitor.py +404 -0
  48. package/.claude/skills/ftd-detector/scripts/rally_tracker.py +508 -0
  49. package/.claude/skills/ftd-detector/scripts/report_generator.py +341 -0
  50. package/.claude/skills/ftd-detector/scripts/tests/conftest.py +9 -0
  51. package/.claude/skills/ftd-detector/scripts/tests/helpers.py +107 -0
  52. package/.claude/skills/ftd-detector/scripts/tests/test_post_ftd_monitor.py +311 -0
  53. package/.claude/skills/ftd-detector/scripts/tests/test_rally_tracker.py +302 -0
  54. package/.claude/skills/institutional-flow-tracker/README.md +362 -0
  55. package/.claude/skills/institutional-flow-tracker/SKILL.md +357 -0
  56. package/.claude/skills/institutional-flow-tracker/references/13f_filings_guide.md +383 -0
  57. package/.claude/skills/institutional-flow-tracker/references/institutional_investor_types.md +580 -0
  58. package/.claude/skills/institutional-flow-tracker/references/interpretation_framework.md +573 -0
  59. package/.claude/skills/institutional-flow-tracker/scripts/analyze_single_stock.py +457 -0
  60. package/.claude/skills/institutional-flow-tracker/scripts/track_institution_portfolio.py +108 -0
  61. package/.claude/skills/institutional-flow-tracker/scripts/track_institutional_flow.py +450 -0
  62. package/.claude/skills/macro-regime-detector/SKILL.md +86 -0
  63. package/.claude/skills/macro-regime-detector/references/historical_regimes.md +124 -0
  64. package/.claude/skills/macro-regime-detector/references/indicator_interpretation_guide.md +144 -0
  65. package/.claude/skills/macro-regime-detector/references/regime_detection_methodology.md +138 -0
  66. package/.claude/skills/macro-regime-detector/scripts/calculators/__init__.py +1 -0
  67. package/.claude/skills/macro-regime-detector/scripts/calculators/concentration_calculator.py +165 -0
  68. package/.claude/skills/macro-regime-detector/scripts/calculators/credit_conditions_calculator.py +124 -0
  69. package/.claude/skills/macro-regime-detector/scripts/calculators/equity_bond_calculator.py +198 -0
  70. package/.claude/skills/macro-regime-detector/scripts/calculators/sector_rotation_calculator.py +123 -0
  71. package/.claude/skills/macro-regime-detector/scripts/calculators/size_factor_calculator.py +131 -0
  72. package/.claude/skills/macro-regime-detector/scripts/calculators/utils.py +347 -0
  73. package/.claude/skills/macro-regime-detector/scripts/calculators/yield_curve_calculator.py +279 -0
  74. package/.claude/skills/macro-regime-detector/scripts/fmp_client.py +134 -0
  75. package/.claude/skills/macro-regime-detector/scripts/macro_regime_detector.py +278 -0
  76. package/.claude/skills/macro-regime-detector/scripts/report_generator.py +327 -0
  77. package/.claude/skills/macro-regime-detector/scripts/scorer.py +574 -0
  78. package/.claude/skills/macro-regime-detector/scripts/tests/conftest.py +9 -0
  79. package/.claude/skills/macro-regime-detector/scripts/tests/test_concentration.py +78 -0
  80. package/.claude/skills/macro-regime-detector/scripts/tests/test_credit_conditions.py +59 -0
  81. package/.claude/skills/macro-regime-detector/scripts/tests/test_equity_bond.py +74 -0
  82. package/.claude/skills/macro-regime-detector/scripts/tests/test_helpers.py +90 -0
  83. package/.claude/skills/macro-regime-detector/scripts/tests/test_scorer.py +439 -0
  84. package/.claude/skills/macro-regime-detector/scripts/tests/test_sector_rotation.py +78 -0
  85. package/.claude/skills/macro-regime-detector/scripts/tests/test_size_factor.py +59 -0
  86. package/.claude/skills/macro-regime-detector/scripts/tests/test_utils.py +126 -0
  87. package/.claude/skills/macro-regime-detector/scripts/tests/test_yield_curve.py +64 -0
  88. package/.claude/skills/market-breadth-analyzer/SKILL.md +121 -0
  89. package/.claude/skills/market-breadth-analyzer/references/breadth_analysis_methodology.md +168 -0
  90. package/.claude/skills/market-breadth-analyzer/scripts/calculators/__init__.py +1 -0
  91. package/.claude/skills/market-breadth-analyzer/scripts/calculators/bearish_signal_calculator.py +150 -0
  92. package/.claude/skills/market-breadth-analyzer/scripts/calculators/cycle_calculator.py +168 -0
  93. package/.claude/skills/market-breadth-analyzer/scripts/calculators/divergence_calculator.py +119 -0
  94. package/.claude/skills/market-breadth-analyzer/scripts/calculators/historical_context_calculator.py +120 -0
  95. package/.claude/skills/market-breadth-analyzer/scripts/calculators/ma_crossover_calculator.py +115 -0
  96. package/.claude/skills/market-breadth-analyzer/scripts/calculators/trend_level_calculator.py +103 -0
  97. package/.claude/skills/market-breadth-analyzer/scripts/csv_client.py +225 -0
  98. package/.claude/skills/market-breadth-analyzer/scripts/market_breadth_analyzer.py +307 -0
  99. package/.claude/skills/market-breadth-analyzer/scripts/report_generator.py +330 -0
  100. package/.claude/skills/market-breadth-analyzer/scripts/scorer.py +271 -0
  101. package/.claude/skills/market-environment-analysis/SKILL.md +139 -0
  102. package/.claude/skills/market-environment-analysis/references/analysis_patterns.md +124 -0
  103. package/.claude/skills/market-environment-analysis/references/indicators.md +99 -0
  104. package/.claude/skills/market-environment-analysis/scripts/market_utils.py +127 -0
  105. package/.claude/skills/market-news-analyst/SKILL.md +714 -0
  106. package/.claude/skills/market-news-analyst/references/corporate_news_impact.md +446 -0
  107. package/.claude/skills/market-news-analyst/references/geopolitical_commodity_correlations.md +499 -0
  108. package/.claude/skills/market-news-analyst/references/market_event_patterns.md +393 -0
  109. package/.claude/skills/market-news-analyst/references/trusted_news_sources.md +510 -0
  110. package/.claude/skills/market-top-detector/SKILL.md +159 -0
  111. package/.claude/skills/market-top-detector/references/distribution_day_guide.md +100 -0
  112. package/.claude/skills/market-top-detector/references/historical_tops.md +142 -0
  113. package/.claude/skills/market-top-detector/references/market_top_methodology.md +167 -0
  114. package/.claude/skills/market-top-detector/scripts/calculators/__init__.py +17 -0
  115. package/.claude/skills/market-top-detector/scripts/calculators/breadth_calculator.py +116 -0
  116. package/.claude/skills/market-top-detector/scripts/calculators/defensive_rotation_calculator.py +127 -0
  117. package/.claude/skills/market-top-detector/scripts/calculators/distribution_day_calculator.py +161 -0
  118. package/.claude/skills/market-top-detector/scripts/calculators/index_technical_calculator.py +254 -0
  119. package/.claude/skills/market-top-detector/scripts/calculators/leading_stock_calculator.py +198 -0
  120. package/.claude/skills/market-top-detector/scripts/calculators/sentiment_calculator.py +213 -0
  121. package/.claude/skills/market-top-detector/scripts/fmp_client.py +158 -0
  122. package/.claude/skills/market-top-detector/scripts/market_top_detector.py +349 -0
  123. package/.claude/skills/market-top-detector/scripts/report_generator.py +314 -0
  124. package/.claude/skills/market-top-detector/scripts/scorer.py +473 -0
  125. package/.claude/skills/market-top-detector/scripts/tests/conftest.py +9 -0
  126. package/.claude/skills/market-top-detector/scripts/tests/helpers.py +49 -0
  127. package/.claude/skills/market-top-detector/scripts/tests/test_breadth.py +62 -0
  128. package/.claude/skills/market-top-detector/scripts/tests/test_defensive_rotation.py +56 -0
  129. package/.claude/skills/market-top-detector/scripts/tests/test_distribution_day.py +92 -0
  130. package/.claude/skills/market-top-detector/scripts/tests/test_index_technical.py +73 -0
  131. package/.claude/skills/market-top-detector/scripts/tests/test_leading_stock.py +57 -0
  132. package/.claude/skills/market-top-detector/scripts/tests/test_scorer.py +180 -0
  133. package/.claude/skills/market-top-detector/scripts/tests/test_sentiment.py +64 -0
  134. package/.claude/skills/options-strategy-advisor/README.md +469 -0
  135. package/.claude/skills/options-strategy-advisor/SKILL.md +959 -0
  136. package/.claude/skills/options-strategy-advisor/scripts/black_scholes.py +495 -0
  137. package/.claude/skills/pair-trade-screener/README.md +389 -0
  138. package/.claude/skills/pair-trade-screener/SKILL.md +622 -0
  139. package/.claude/skills/pair-trade-screener/references/cointegration_guide.md +745 -0
  140. package/.claude/skills/pair-trade-screener/references/methodology.md +853 -0
  141. package/.claude/skills/pair-trade-screener/scripts/analyze_spread.py +394 -0
  142. package/.claude/skills/pair-trade-screener/scripts/find_pairs.py +535 -0
  143. package/.claude/skills/portfolio-manager/README.md +394 -0
  144. package/.claude/skills/portfolio-manager/SKILL.md +750 -0
  145. package/.claude/skills/portfolio-manager/references/alpaca-mcp-setup.md +367 -0
  146. package/.claude/skills/portfolio-manager/references/asset-allocation.md +502 -0
  147. package/.claude/skills/portfolio-manager/references/diversification-principles.md +553 -0
  148. package/.claude/skills/portfolio-manager/references/portfolio-risk-metrics.md +603 -0
  149. package/.claude/skills/portfolio-manager/references/position-evaluation.md +477 -0
  150. package/.claude/skills/portfolio-manager/references/rebalancing-strategies.md +715 -0
  151. package/.claude/skills/portfolio-manager/references/risk-profile-questionnaire.md +608 -0
  152. package/.claude/skills/portfolio-manager/references/target-allocations.md +558 -0
  153. package/.claude/skills/portfolio-manager/scripts/test_alpaca_connection.py +286 -0
  154. package/.claude/skills/scenario-analyzer/SKILL.md +317 -0
  155. package/.claude/skills/scenario-analyzer/references/headline_event_patterns.md +264 -0
  156. package/.claude/skills/scenario-analyzer/references/scenario_playbooks.md +320 -0
  157. package/.claude/skills/scenario-analyzer/references/sector_sensitivity_matrix.md +217 -0
  158. package/.claude/skills/sector-analyst/SKILL.md +206 -0
  159. package/.claude/skills/sector-analyst/assets/industory_performance_1.jpeg +0 -0
  160. package/.claude/skills/sector-analyst/assets/industory_performance_2.jpeg +0 -0
  161. package/.claude/skills/sector-analyst/assets/sector_performance.jpeg +0 -0
  162. package/.claude/skills/sector-analyst/references/sector_rotation.md +170 -0
  163. package/.claude/skills/stanley-druckenmiller-investment/SKILL.md +84 -0
  164. package/.claude/skills/stanley-druckenmiller-investment/references/case-studies.md +148 -0
  165. package/.claude/skills/stanley-druckenmiller-investment/references/investment-philosophy.md +80 -0
  166. package/.claude/skills/stanley-druckenmiller-investment/references/market-analysis-guide.md +146 -0
  167. package/.claude/skills/stock/NOTION_SETUP.md +33 -0
  168. package/.claude/skills/stock/SKILL.md +38 -0
  169. package/.claude/skills/technical-analyst/SKILL.md +238 -0
  170. package/.claude/skills/technical-analyst/assets/analysis_template.md +183 -0
  171. package/.claude/skills/technical-analyst/references/technical_analysis_framework.md +282 -0
  172. package/.claude/skills/theme-detector/SKILL.md +320 -0
  173. package/.claude/skills/theme-detector/assets/report_template.md +155 -0
  174. package/.claude/skills/theme-detector/references/cross_sector_themes.md +252 -0
  175. package/.claude/skills/theme-detector/references/finviz_industry_codes.md +403 -0
  176. package/.claude/skills/theme-detector/references/thematic_etf_catalog.md +333 -0
  177. package/.claude/skills/theme-detector/references/theme_detection_methodology.md +430 -0
  178. package/.claude/skills/theme-detector/scripts/calculators/__init__.py +1 -0
  179. package/.claude/skills/theme-detector/scripts/calculators/heat_calculator.py +123 -0
  180. package/.claude/skills/theme-detector/scripts/calculators/industry_ranker.py +98 -0
  181. package/.claude/skills/theme-detector/scripts/calculators/lifecycle_calculator.py +172 -0
  182. package/.claude/skills/theme-detector/scripts/calculators/theme_classifier.py +195 -0
  183. package/.claude/skills/theme-detector/scripts/calculators/theme_discoverer.py +280 -0
  184. package/.claude/skills/theme-detector/scripts/config_loader.py +142 -0
  185. package/.claude/skills/theme-detector/scripts/default_theme_config.py +254 -0
  186. package/.claude/skills/theme-detector/scripts/etf_scanner.py +609 -0
  187. package/.claude/skills/theme-detector/scripts/finviz_performance_client.py +131 -0
  188. package/.claude/skills/theme-detector/scripts/report_generator.py +490 -0
  189. package/.claude/skills/theme-detector/scripts/representative_stock_selector.py +673 -0
  190. package/.claude/skills/theme-detector/scripts/scorer.py +87 -0
  191. package/.claude/skills/theme-detector/scripts/tests/README.md +21 -0
  192. package/.claude/skills/theme-detector/scripts/tests/conftest.py +9 -0
  193. package/.claude/skills/theme-detector/scripts/tests/test_config_loader.py +239 -0
  194. package/.claude/skills/theme-detector/scripts/tests/test_etf_scanner.py +810 -0
  195. package/.claude/skills/theme-detector/scripts/tests/test_heat_calculator.py +245 -0
  196. package/.claude/skills/theme-detector/scripts/tests/test_industry_ranker.py +256 -0
  197. package/.claude/skills/theme-detector/scripts/tests/test_lifecycle_calculator.py +301 -0
  198. package/.claude/skills/theme-detector/scripts/tests/test_report_generator.py +624 -0
  199. package/.claude/skills/theme-detector/scripts/tests/test_representative_stock_selector.py +898 -0
  200. package/.claude/skills/theme-detector/scripts/tests/test_scorer.py +185 -0
  201. package/.claude/skills/theme-detector/scripts/tests/test_theme_classifier.py +534 -0
  202. package/.claude/skills/theme-detector/scripts/tests/test_theme_detector_e2e.py +467 -0
  203. package/.claude/skills/theme-detector/scripts/tests/test_theme_discoverer.py +458 -0
  204. package/.claude/skills/theme-detector/scripts/tests/test_uptrend_client.py +76 -0
  205. package/.claude/skills/theme-detector/scripts/theme_detector.py +815 -0
  206. package/.claude/skills/theme-detector/scripts/themes.yaml +168 -0
  207. package/.claude/skills/theme-detector/scripts/uptrend_client.py +241 -0
  208. package/.claude/skills/uptrend-analyzer/SKILL.md +108 -0
  209. package/.claude/skills/uptrend-analyzer/references/uptrend_methodology.md +215 -0
  210. package/.claude/skills/uptrend-analyzer/scripts/calculators/__init__.py +1 -0
  211. package/.claude/skills/uptrend-analyzer/scripts/calculators/historical_context_calculator.py +122 -0
  212. package/.claude/skills/uptrend-analyzer/scripts/calculators/market_breadth_calculator.py +145 -0
  213. package/.claude/skills/uptrend-analyzer/scripts/calculators/momentum_calculator.py +183 -0
  214. package/.claude/skills/uptrend-analyzer/scripts/calculators/sector_participation_calculator.py +204 -0
  215. package/.claude/skills/uptrend-analyzer/scripts/calculators/sector_rotation_calculator.py +218 -0
  216. package/.claude/skills/uptrend-analyzer/scripts/data_fetcher.py +236 -0
  217. package/.claude/skills/uptrend-analyzer/scripts/report_generator.py +329 -0
  218. package/.claude/skills/uptrend-analyzer/scripts/scorer.py +276 -0
  219. package/.claude/skills/uptrend-analyzer/scripts/uptrend_analyzer.py +219 -0
  220. package/.claude/skills/us-market-bubble-detector/CHANGELOG.md +118 -0
  221. package/.claude/skills/us-market-bubble-detector/SKILL.md +545 -0
  222. package/.claude/skills/us-market-bubble-detector/references/bubble_framework.md +335 -0
  223. package/.claude/skills/us-market-bubble-detector/references/historical_cases.md +327 -0
  224. package/.claude/skills/us-market-bubble-detector/references/implementation_guide.md +473 -0
  225. package/.claude/skills/us-market-bubble-detector/references/quick_reference.md +354 -0
  226. package/.claude/skills/us-market-bubble-detector/references/quick_reference_en.md +342 -0
  227. package/.claude/skills/us-market-bubble-detector/scripts/bubble_scorer.py +309 -0
  228. package/.claude/skills/us-stock-analysis/SKILL.md +294 -0
  229. package/.claude/skills/us-stock-analysis/references/financial-metrics.md +172 -0
  230. package/.claude/skills/us-stock-analysis/references/fundamental-analysis.md +129 -0
  231. package/.claude/skills/us-stock-analysis/references/report-template.md +207 -0
  232. package/.claude/skills/us-stock-analysis/references/technical-analysis.md +93 -0
  233. package/.claude/skills/value-dividend-screener/SKILL.md +562 -0
  234. package/.claude/skills/value-dividend-screener/references/fmp_api_guide.md +348 -0
  235. package/.claude/skills/value-dividend-screener/references/screening_methodology.md +315 -0
  236. package/.claude/skills/value-dividend-screener/scripts/screen_dividend_stocks.py +1138 -0
  237. package/.claude/skills/vcp-screener/SKILL.md +79 -0
  238. package/.claude/skills/vcp-screener/references/fmp_api_endpoints.md +45 -0
  239. package/.claude/skills/vcp-screener/references/scoring_system.md +154 -0
  240. package/.claude/skills/vcp-screener/references/vcp_methodology.md +124 -0
  241. package/.claude/skills/vcp-screener/scripts/calculators/__init__.py +1 -0
  242. package/.claude/skills/vcp-screener/scripts/calculators/pivot_proximity_calculator.py +139 -0
  243. package/.claude/skills/vcp-screener/scripts/calculators/relative_strength_calculator.py +161 -0
  244. package/.claude/skills/vcp-screener/scripts/calculators/trend_template_calculator.py +228 -0
  245. package/.claude/skills/vcp-screener/scripts/calculators/vcp_pattern_calculator.py +322 -0
  246. package/.claude/skills/vcp-screener/scripts/calculators/volume_pattern_calculator.py +121 -0
  247. package/.claude/skills/vcp-screener/scripts/fmp_client.py +162 -0
  248. package/.claude/skills/vcp-screener/scripts/report_generator.py +317 -0
  249. package/.claude/skills/vcp-screener/scripts/scorer.py +155 -0
  250. package/.claude/skills/vcp-screener/scripts/screen_vcp.py +536 -0
  251. package/.claude/skills/vcp-screener/scripts/tests/__init__.py +0 -0
  252. package/.claude/skills/vcp-screener/scripts/tests/conftest.py +9 -0
  253. package/.claude/skills/vcp-screener/scripts/tests/test_vcp_screener.py +834 -0
  254. package/.claude/skills/weekly-trade-strategy/.claude/agents/druckenmiller-strategy-planner.md +300 -0
  255. package/.claude/skills/weekly-trade-strategy/.claude/agents/market-news-analyzer.md +239 -0
  256. package/.claude/skills/weekly-trade-strategy/.claude/agents/technical-market-analyst.md +187 -0
  257. package/.claude/skills/weekly-trade-strategy/.claude/agents/us-market-analyst.md +218 -0
  258. package/.claude/skills/weekly-trade-strategy/.claude/agents/weekly-trade-blog-writer.md +318 -0
  259. package/.claude/skills/weekly-trade-strategy/.claude/skills/breadth-chart-analyst/SKILL.md +662 -0
  260. package/.claude/skills/weekly-trade-strategy/.claude/skills/breadth-chart-analyst/assets/SP500_Breadth_Index_200MA_8MA.jpeg +0 -0
  261. package/.claude/skills/weekly-trade-strategy/.claude/skills/breadth-chart-analyst/assets/US_Stock_Market_Uptrend_Ratio.jpeg +0 -0
  262. package/.claude/skills/weekly-trade-strategy/.claude/skills/breadth-chart-analyst/assets/breadth_analysis_template.md +558 -0
  263. package/.claude/skills/weekly-trade-strategy/.claude/skills/breadth-chart-analyst/references/breadth_chart_methodology.md +590 -0
  264. package/.claude/skills/weekly-trade-strategy/.claude/skills/earnings-calendar/SKILL.md +721 -0
  265. package/.claude/skills/weekly-trade-strategy/.claude/skills/earnings-calendar/assets/earnings_report_template.md +102 -0
  266. package/.claude/skills/weekly-trade-strategy/.claude/skills/earnings-calendar/earnings_calendar_2025-11-02.md +447 -0
  267. package/.claude/skills/weekly-trade-strategy/.claude/skills/earnings-calendar/references/fmp_api_guide.md +590 -0
  268. package/.claude/skills/weekly-trade-strategy/.claude/skills/earnings-calendar/scripts/fetch_earnings_fmp.py +443 -0
  269. package/.claude/skills/weekly-trade-strategy/.claude/skills/earnings-calendar/scripts/generate_report.py +366 -0
  270. package/.claude/skills/weekly-trade-strategy/.claude/skills/economic-calendar-fetcher/SKILL.md +365 -0
  271. package/.claude/skills/weekly-trade-strategy/.claude/skills/economic-calendar-fetcher/references/fmp_api_documentation.md +345 -0
  272. package/.claude/skills/weekly-trade-strategy/.claude/skills/economic-calendar-fetcher/scripts/get_economic_calendar.py +267 -0
  273. package/.claude/skills/weekly-trade-strategy/.claude/skills/market-environment-analysis/SKILL.md +139 -0
  274. package/.claude/skills/weekly-trade-strategy/.claude/skills/market-environment-analysis/references/analysis_patterns.md +124 -0
  275. package/.claude/skills/weekly-trade-strategy/.claude/skills/market-environment-analysis/references/indicators.md +99 -0
  276. package/.claude/skills/weekly-trade-strategy/.claude/skills/market-environment-analysis/scripts/market_utils.py +127 -0
  277. package/.claude/skills/weekly-trade-strategy/.claude/skills/market-news-analyst/SKILL.md +714 -0
  278. package/.claude/skills/weekly-trade-strategy/.claude/skills/market-news-analyst/references/corporate_news_impact.md +446 -0
  279. package/.claude/skills/weekly-trade-strategy/.claude/skills/market-news-analyst/references/geopolitical_commodity_correlations.md +499 -0
  280. package/.claude/skills/weekly-trade-strategy/.claude/skills/market-news-analyst/references/market_event_patterns.md +393 -0
  281. package/.claude/skills/weekly-trade-strategy/.claude/skills/market-news-analyst/references/trusted_news_sources.md +510 -0
  282. package/.claude/skills/weekly-trade-strategy/.claude/skills/sector-analyst/SKILL.md +206 -0
  283. package/.claude/skills/weekly-trade-strategy/.claude/skills/sector-analyst/assets/industory_performance_1.jpeg +0 -0
  284. package/.claude/skills/weekly-trade-strategy/.claude/skills/sector-analyst/assets/industory_performance_2.jpeg +0 -0
  285. package/.claude/skills/weekly-trade-strategy/.claude/skills/sector-analyst/assets/sector_performance.jpeg +0 -0
  286. package/.claude/skills/weekly-trade-strategy/.claude/skills/sector-analyst/references/sector_rotation.md +170 -0
  287. package/.claude/skills/weekly-trade-strategy/.claude/skills/stanley-druckenmiller-investment/SKILL.md +84 -0
  288. package/.claude/skills/weekly-trade-strategy/.claude/skills/stanley-druckenmiller-investment/references/case-studies.md +148 -0
  289. package/.claude/skills/weekly-trade-strategy/.claude/skills/stanley-druckenmiller-investment/references/investment-philosophy.md +80 -0
  290. package/.claude/skills/weekly-trade-strategy/.claude/skills/stanley-druckenmiller-investment/references/market-analysis-guide.md +146 -0
  291. package/.claude/skills/weekly-trade-strategy/.claude/skills/technical-analyst/SKILL.md +238 -0
  292. package/.claude/skills/weekly-trade-strategy/.claude/skills/technical-analyst/assets/analysis_template.md +183 -0
  293. package/.claude/skills/weekly-trade-strategy/.claude/skills/technical-analyst/references/technical_analysis_framework.md +282 -0
  294. package/.claude/skills/weekly-trade-strategy/.claude/skills/us-market-bubble-detector/CHANGELOG.md +118 -0
  295. package/.claude/skills/weekly-trade-strategy/.claude/skills/us-market-bubble-detector/SKILL.md +545 -0
  296. package/.claude/skills/weekly-trade-strategy/.claude/skills/us-market-bubble-detector/references/bubble_framework.md +335 -0
  297. package/.claude/skills/weekly-trade-strategy/.claude/skills/us-market-bubble-detector/references/historical_cases.md +327 -0
  298. package/.claude/skills/weekly-trade-strategy/.claude/skills/us-market-bubble-detector/references/implementation_guide.md +473 -0
  299. package/.claude/skills/weekly-trade-strategy/.claude/skills/us-market-bubble-detector/references/quick_reference.md +354 -0
  300. package/.claude/skills/weekly-trade-strategy/.claude/skills/us-market-bubble-detector/references/quick_reference_en.md +342 -0
  301. package/.claude/skills/weekly-trade-strategy/.claude/skills/us-market-bubble-detector/scripts/bubble_scorer.py +309 -0
  302. package/.claude/skills/weekly-trade-strategy/.claude/skills/us-stock-analysis/SKILL.md +294 -0
  303. package/.claude/skills/weekly-trade-strategy/.claude/skills/us-stock-analysis/references/financial-metrics.md +172 -0
  304. package/.claude/skills/weekly-trade-strategy/.claude/skills/us-stock-analysis/references/fundamental-analysis.md +129 -0
  305. package/.claude/skills/weekly-trade-strategy/.claude/skills/us-stock-analysis/references/report-template.md +207 -0
  306. package/.claude/skills/weekly-trade-strategy/.claude/skills/us-stock-analysis/references/technical-analysis.md +93 -0
  307. package/.claude/skills/weekly-trade-strategy/CLAUDE.md +454 -0
  308. package/.claude/skills/weekly-trade-strategy/README.md +287 -0
  309. package/.claude/skills/weekly-trade-strategy/blogs/.gitkeep +0 -0
  310. package/.claude/skills/weekly-trade-strategy/charts/.gitkeep +0 -0
  311. package/.claude/skills/weekly-trade-strategy/earnings_data.json +10054 -0
  312. package/.claude/skills/weekly-trade-strategy/skills/breadth-chart-analyst/SKILL.md +662 -0
  313. package/.claude/skills/weekly-trade-strategy/skills/breadth-chart-analyst/assets/SP500_Breadth_Index_200MA_8MA.jpeg +0 -0
  314. package/.claude/skills/weekly-trade-strategy/skills/breadth-chart-analyst/assets/US_Stock_Market_Uptrend_Ratio.jpeg +0 -0
  315. package/.claude/skills/weekly-trade-strategy/skills/breadth-chart-analyst/assets/breadth_analysis_template.md +558 -0
  316. package/.claude/skills/weekly-trade-strategy/skills/breadth-chart-analyst/references/breadth_chart_methodology.md +590 -0
  317. package/.claude/skills/weekly-trade-strategy/skills/earnings-calendar/SKILL.md +721 -0
  318. package/.claude/skills/weekly-trade-strategy/skills/earnings-calendar/assets/earnings_report_template.md +102 -0
  319. package/.claude/skills/weekly-trade-strategy/skills/earnings-calendar/earnings_calendar_2025-11-02.md +447 -0
  320. package/.claude/skills/weekly-trade-strategy/skills/earnings-calendar/references/fmp_api_guide.md +590 -0
  321. package/.claude/skills/weekly-trade-strategy/skills/earnings-calendar/scripts/fetch_earnings_fmp.py +443 -0
  322. package/.claude/skills/weekly-trade-strategy/skills/earnings-calendar/scripts/generate_report.py +366 -0
  323. package/.claude/skills/weekly-trade-strategy/skills/economic-calendar-fetcher/SKILL.md +365 -0
  324. package/.claude/skills/weekly-trade-strategy/skills/economic-calendar-fetcher/references/fmp_api_documentation.md +345 -0
  325. package/.claude/skills/weekly-trade-strategy/skills/economic-calendar-fetcher/scripts/get_economic_calendar.py +267 -0
  326. package/.claude/skills/weekly-trade-strategy/skills/market-environment-analysis/SKILL.md +139 -0
  327. package/.claude/skills/weekly-trade-strategy/skills/market-environment-analysis/references/analysis_patterns.md +124 -0
  328. package/.claude/skills/weekly-trade-strategy/skills/market-environment-analysis/references/indicators.md +99 -0
  329. package/.claude/skills/weekly-trade-strategy/skills/market-environment-analysis/scripts/market_utils.py +127 -0
  330. package/.claude/skills/weekly-trade-strategy/skills/market-news-analyst/SKILL.md +714 -0
  331. package/.claude/skills/weekly-trade-strategy/skills/market-news-analyst/references/corporate_news_impact.md +446 -0
  332. package/.claude/skills/weekly-trade-strategy/skills/market-news-analyst/references/geopolitical_commodity_correlations.md +499 -0
  333. package/.claude/skills/weekly-trade-strategy/skills/market-news-analyst/references/market_event_patterns.md +393 -0
  334. package/.claude/skills/weekly-trade-strategy/skills/market-news-analyst/references/trusted_news_sources.md +510 -0
  335. package/.claude/skills/weekly-trade-strategy/skills/sector-analyst/SKILL.md +206 -0
  336. package/.claude/skills/weekly-trade-strategy/skills/sector-analyst/assets/industory_performance_1.jpeg +0 -0
  337. package/.claude/skills/weekly-trade-strategy/skills/sector-analyst/assets/industory_performance_2.jpeg +0 -0
  338. package/.claude/skills/weekly-trade-strategy/skills/sector-analyst/assets/sector_performance.jpeg +0 -0
  339. package/.claude/skills/weekly-trade-strategy/skills/sector-analyst/references/sector_rotation.md +170 -0
  340. package/.claude/skills/weekly-trade-strategy/skills/stanley-druckenmiller-investment/SKILL.md +84 -0
  341. package/.claude/skills/weekly-trade-strategy/skills/stanley-druckenmiller-investment/references/case-studies.md +148 -0
  342. package/.claude/skills/weekly-trade-strategy/skills/stanley-druckenmiller-investment/references/investment-philosophy.md +80 -0
  343. package/.claude/skills/weekly-trade-strategy/skills/stanley-druckenmiller-investment/references/market-analysis-guide.md +146 -0
  344. package/.claude/skills/weekly-trade-strategy/skills/technical-analyst/SKILL.md +238 -0
  345. package/.claude/skills/weekly-trade-strategy/skills/technical-analyst/assets/analysis_template.md +183 -0
  346. package/.claude/skills/weekly-trade-strategy/skills/technical-analyst/references/technical_analysis_framework.md +282 -0
  347. package/.claude/skills/weekly-trade-strategy/skills/us-market-bubble-detector/CHANGELOG.md +118 -0
  348. package/.claude/skills/weekly-trade-strategy/skills/us-market-bubble-detector/SKILL.md +545 -0
  349. package/.claude/skills/weekly-trade-strategy/skills/us-market-bubble-detector/references/bubble_framework.md +335 -0
  350. package/.claude/skills/weekly-trade-strategy/skills/us-market-bubble-detector/references/historical_cases.md +327 -0
  351. package/.claude/skills/weekly-trade-strategy/skills/us-market-bubble-detector/references/implementation_guide.md +473 -0
  352. package/.claude/skills/weekly-trade-strategy/skills/us-market-bubble-detector/references/quick_reference.md +354 -0
  353. package/.claude/skills/weekly-trade-strategy/skills/us-market-bubble-detector/references/quick_reference_en.md +342 -0
  354. package/.claude/skills/weekly-trade-strategy/skills/us-market-bubble-detector/scripts/bubble_scorer.py +309 -0
  355. package/.claude/skills/weekly-trade-strategy/skills/us-stock-analysis/SKILL.md +294 -0
  356. package/.claude/skills/weekly-trade-strategy/skills/us-stock-analysis/references/financial-metrics.md +172 -0
  357. package/.claude/skills/weekly-trade-strategy/skills/us-stock-analysis/references/fundamental-analysis.md +129 -0
  358. package/.claude/skills/weekly-trade-strategy/skills/us-stock-analysis/references/report-template.md +207 -0
  359. package/.claude/skills/weekly-trade-strategy/skills/us-stock-analysis/references/technical-analysis.md +93 -0
  360. package/.mcp.json +3 -0
  361. package/cli.mjs +16 -16
  362. package/package.json +4 -2
@@ -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()