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,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()