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,609 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Theme Detector - ETF & Stock Metrics Scanner
4
+
5
+ Uses FMP API (preferred) with yfinance fallback for batch downloading
6
+ stock/ETF data and computing technical metrics: RSI-14, 52-week distance,
7
+ PE ratio, volume ratios.
8
+
9
+ FMP API key is optional; without it, yfinance is used exclusively.
10
+ """
11
+
12
+ import sys
13
+ import time
14
+ from typing import Any, Dict, List, Optional, Tuple
15
+
16
+ try:
17
+ import pandas as pd
18
+ import numpy as np
19
+ except ImportError:
20
+ print("ERROR: pandas/numpy not found. Install with: pip install pandas numpy",
21
+ file=sys.stderr)
22
+ sys.exit(1)
23
+
24
+ try:
25
+ import yfinance as yf
26
+ HAS_YFINANCE = True
27
+ except ImportError:
28
+ HAS_YFINANCE = False
29
+
30
+ try:
31
+ import requests as _requests_lib
32
+ HAS_REQUESTS = True
33
+ except ImportError:
34
+ HAS_REQUESTS = False
35
+
36
+
37
+ # ---------------------------------------------------------------------------
38
+ # FMP endpoint definitions (R2-1: callable URL builders for stable/v3)
39
+ # ---------------------------------------------------------------------------
40
+
41
+ def _stable_quote_url(base: str, symbols_str: str, params: Dict) -> Tuple[str, Dict]:
42
+ """stable/quote?symbol=A,B&apikey=..."""
43
+ params["symbol"] = symbols_str
44
+ return base, params
45
+
46
+
47
+ def _v3_quote_url(base: str, symbols_str: str, params: Dict) -> Tuple[str, Dict]:
48
+ """api/v3/quote/A,B?apikey=..."""
49
+ return f"{base}/{symbols_str}", params
50
+
51
+
52
+ def _stable_hist_url(base: str, symbols_str: str, params: Dict) -> Tuple[str, Dict]:
53
+ """stable/historical-price-full?symbol=A,B&..."""
54
+ params["symbol"] = symbols_str
55
+ return base, params
56
+
57
+
58
+ def _v3_hist_url(base: str, symbols_str: str, params: Dict) -> Tuple[str, Dict]:
59
+ """api/v3/historical-price-full/A,B?..."""
60
+ return f"{base}/{symbols_str}", params
61
+
62
+
63
+ _FMP_ENDPOINTS = {
64
+ "quote": [
65
+ ("https://financialmodelingprep.com/stable/quote", _stable_quote_url),
66
+ ("https://financialmodelingprep.com/api/v3/quote", _v3_quote_url),
67
+ ],
68
+ "historical": [
69
+ ("https://financialmodelingprep.com/stable/historical-price-full", _stable_hist_url),
70
+ ("https://financialmodelingprep.com/api/v3/historical-price-full", _v3_hist_url),
71
+ ],
72
+ }
73
+
74
+
75
+ class ETFScanner:
76
+ """Scans ETFs and stocks for volume ratios and technical metrics.
77
+
78
+ Supports FMP API (preferred) with automatic yfinance fallback.
79
+ """
80
+
81
+ FMP_HIST_BATCH_SIZE = 5
82
+ FMP_QUOTE_BATCH_SIZE = 50
83
+
84
+ def __init__(self, fmp_api_key: Optional[str] = None, rate_limit_sec: float = 0.3):
85
+ self._cache: Dict[str, pd.DataFrame] = {}
86
+ self._fmp_api_key = fmp_api_key
87
+ self._rate_limit_sec = rate_limit_sec
88
+ self._last_request_time = 0.0
89
+ self._fmp_quote_cache: Dict[str, Dict] = {} # normalized_symbol -> quote dict
90
+ self._stats = {
91
+ "fmp_calls": 0,
92
+ "fmp_failures": 0,
93
+ "yf_calls": 0,
94
+ "yf_fallbacks": 0,
95
+ }
96
+
97
+ def backend_stats(self) -> Dict[str, int]:
98
+ """Return a copy of backend usage statistics."""
99
+ return dict(self._stats)
100
+
101
+ # -------------------------------------------------------------------
102
+ # Symbol normalization (R2-4)
103
+ # -------------------------------------------------------------------
104
+ @staticmethod
105
+ def _normalize_symbol_for_fmp(symbol: str) -> str:
106
+ """Normalize symbol for FMP API (e.g., BRK-B -> BRK.B)."""
107
+ return symbol.replace("-", ".")
108
+
109
+ # -------------------------------------------------------------------
110
+ # FMP infrastructure (R2-1: callable URL builder)
111
+ # -------------------------------------------------------------------
112
+ def _fmp_rate_limit(self):
113
+ now = time.time()
114
+ elapsed = now - self._last_request_time
115
+ if elapsed < self._rate_limit_sec:
116
+ time.sleep(self._rate_limit_sec - elapsed)
117
+ self._last_request_time = time.time()
118
+
119
+ def _fmp_request(
120
+ self, endpoint_key: str, symbols_str: str, extra_params: Optional[Dict] = None
121
+ ) -> Optional[Any]:
122
+ """Try each endpoint (stable -> v3) with correct URL format.
123
+
124
+ Args:
125
+ endpoint_key: "quote" or "historical"
126
+ symbols_str: comma-separated symbols (already normalized)
127
+ extra_params: e.g. {"timeseries": 20}
128
+
129
+ Returns:
130
+ Parsed JSON or None on all failures.
131
+ """
132
+ if not HAS_REQUESTS or not self._fmp_api_key:
133
+ return None
134
+
135
+ params = {"apikey": self._fmp_api_key}
136
+ if extra_params:
137
+ params.update(extra_params)
138
+
139
+ for base_url, url_builder in _FMP_ENDPOINTS[endpoint_key]:
140
+ url, final_params = url_builder(base_url, symbols_str, dict(params))
141
+ self._fmp_rate_limit()
142
+ self._stats["fmp_calls"] += 1
143
+ try:
144
+ resp = _requests_lib.get(url, params=final_params, timeout=15)
145
+ if resp.status_code == 200:
146
+ data = resp.json()
147
+ if data:
148
+ return data
149
+ except Exception:
150
+ pass
151
+ self._stats["fmp_failures"] += 1
152
+ return None
153
+
154
+ # -------------------------------------------------------------------
155
+ # FMP quote fetch (R2-4: normalized cache)
156
+ # -------------------------------------------------------------------
157
+ def _fetch_fmp_quotes(self, symbols: List[str]) -> Dict[str, Dict]:
158
+ """Batch fetch quotes. Returns {original_symbol: quote_dict}."""
159
+ result: Dict[str, Dict] = {}
160
+ uncached = []
161
+ for s in symbols:
162
+ norm = self._normalize_symbol_for_fmp(s)
163
+ if norm not in self._fmp_quote_cache:
164
+ uncached.append(s)
165
+
166
+ for i in range(0, len(uncached), self.FMP_QUOTE_BATCH_SIZE):
167
+ batch = uncached[i:i + self.FMP_QUOTE_BATCH_SIZE]
168
+ normalized = [self._normalize_symbol_for_fmp(s) for s in batch]
169
+ data = self._fmp_request("quote", ",".join(normalized))
170
+ if isinstance(data, list):
171
+ for item in data:
172
+ sym = self._normalize_symbol_for_fmp(item.get("symbol", ""))
173
+ self._fmp_quote_cache[sym] = item
174
+
175
+ # Per-symbol retry for missing: try original symbol if different
176
+ for s in uncached:
177
+ norm = self._normalize_symbol_for_fmp(s)
178
+ if norm in self._fmp_quote_cache:
179
+ continue
180
+ if norm != s:
181
+ data = self._fmp_request("quote", s)
182
+ if isinstance(data, list):
183
+ for item in data:
184
+ sym = self._normalize_symbol_for_fmp(item.get("symbol", ""))
185
+ self._fmp_quote_cache[sym] = item
186
+
187
+ for s in symbols:
188
+ norm = self._normalize_symbol_for_fmp(s)
189
+ cached = self._fmp_quote_cache.get(norm)
190
+ if cached:
191
+ result[s] = cached
192
+ return result
193
+
194
+ # -------------------------------------------------------------------
195
+ # FMP historical fetch (R2-2: per-symbol retry)
196
+ # -------------------------------------------------------------------
197
+ def _fetch_fmp_historical(
198
+ self, symbols: List[str], timeseries: int = 20
199
+ ) -> Dict[str, List[Dict]]:
200
+ """Batch fetch historical prices with per-symbol retry on partial failure."""
201
+ result: Dict[str, List[Dict]] = {}
202
+ extra = {"timeseries": timeseries}
203
+
204
+ # Phase 1: batch fetch
205
+ for i in range(0, len(symbols), self.FMP_HIST_BATCH_SIZE):
206
+ batch = symbols[i:i + self.FMP_HIST_BATCH_SIZE]
207
+ normalized = [self._normalize_symbol_for_fmp(s) for s in batch]
208
+ data = self._fmp_request("historical", ",".join(normalized), extra)
209
+ if data is None:
210
+ continue
211
+ self._parse_historical_response(data, result)
212
+
213
+ # Phase 2: per-symbol retry for missing symbols
214
+ # Try normalized form first, then original if different
215
+ missing = [s for s in symbols if self._normalize_symbol_for_fmp(s) not in result]
216
+ for s in missing:
217
+ norm = self._normalize_symbol_for_fmp(s)
218
+ data = self._fmp_request("historical", norm, extra)
219
+ if data is not None:
220
+ self._parse_historical_response(data, result)
221
+ continue
222
+ # Retry with original symbol if normalization changed it
223
+ if norm != s:
224
+ data = self._fmp_request("historical", s, extra)
225
+ if data is not None:
226
+ self._parse_historical_response(data, result)
227
+
228
+ # Map normalized keys back to original symbols
229
+ mapped: Dict[str, List[Dict]] = {}
230
+ for s in symbols:
231
+ norm = self._normalize_symbol_for_fmp(s)
232
+ if norm in result:
233
+ mapped[s] = result[norm]
234
+ return mapped
235
+
236
+ def _parse_historical_response(self, data: Any, result: Dict) -> None:
237
+ """Parse FMP historical response (batch or single format).
238
+
239
+ Keys in result are always normalized (e.g., BRK.B not BRK-B).
240
+ """
241
+ if isinstance(data, dict):
242
+ if "historicalStockList" in data:
243
+ for entry in data["historicalStockList"]:
244
+ sym = entry.get("symbol", "")
245
+ if sym and "historical" in entry:
246
+ result[self._normalize_symbol_for_fmp(sym)] = entry["historical"]
247
+ elif "historical" in data:
248
+ sym = data.get("symbol", "")
249
+ if sym:
250
+ result[self._normalize_symbol_for_fmp(sym)] = data["historical"]
251
+
252
+ # -------------------------------------------------------------------
253
+ # FMP-based stock metrics
254
+ # -------------------------------------------------------------------
255
+ def _batch_stock_metrics_fmp(self, symbols: List[str]) -> List[Dict]:
256
+ """Compute stock metrics using FMP quote + historical data."""
257
+ quotes = self._fetch_fmp_quotes(symbols)
258
+ historical = self._fetch_fmp_historical(symbols, timeseries=20)
259
+
260
+ results = []
261
+ for s in symbols:
262
+ entry = {
263
+ "symbol": s, "rsi_14": None,
264
+ "dist_from_52w_high": None, "dist_from_52w_low": None,
265
+ "pe_ratio": None,
266
+ }
267
+
268
+ # PE and 52w from quote
269
+ q = quotes.get(s, {})
270
+ pe = q.get("pe")
271
+ if pe is not None:
272
+ entry["pe_ratio"] = pe
273
+
274
+ price = q.get("price")
275
+ year_high = q.get("yearHigh")
276
+ year_low = q.get("yearLow")
277
+ if price and year_high and year_high > 0:
278
+ entry["dist_from_52w_high"] = round(
279
+ (year_high - price) / year_high, 4
280
+ )
281
+ if price and year_low is not None and price > 0:
282
+ entry["dist_from_52w_low"] = round(
283
+ (price - year_low) / price, 4
284
+ )
285
+
286
+ # RSI from historical close
287
+ hist = historical.get(s, [])
288
+ if hist:
289
+ closes = [d.get("close") for d in hist if d.get("close") is not None]
290
+ if len(closes) >= 15:
291
+ # Historical comes newest-first from FMP; reverse for RSI
292
+ prices_series = pd.Series(list(reversed(closes)))
293
+ entry["rsi_14"] = self._calculate_rsi(prices_series, period=14)
294
+
295
+ results.append(entry)
296
+ return results
297
+
298
+ # -------------------------------------------------------------------
299
+ # FMP-based ETF volume ratios
300
+ # -------------------------------------------------------------------
301
+ def _batch_etf_volume_ratios_fmp(self, symbols: List[str]) -> Dict[str, Dict]:
302
+ """Compute ETF volume ratios using FMP historical data."""
303
+ historical = self._fetch_fmp_historical(symbols, timeseries=60)
304
+
305
+ result: Dict[str, Dict] = {}
306
+ for s in symbols:
307
+ entry = {"symbol": s, "vol_20d": None, "vol_60d": None, "vol_ratio": None}
308
+ hist = historical.get(s, [])
309
+ volumes = [d.get("volume") for d in hist if d.get("volume") is not None]
310
+
311
+ if len(volumes) >= 20:
312
+ # Historical comes newest-first
313
+ vol_20d = float(np.mean(volumes[:20]))
314
+ vol_60d = float(np.mean(volumes[:60])) if len(volumes) >= 60 else float(np.mean(volumes))
315
+ entry["vol_20d"] = vol_20d
316
+ entry["vol_60d"] = vol_60d
317
+ entry["vol_ratio"] = vol_20d / vol_60d if vol_60d > 0 else None
318
+
319
+ result[s] = entry
320
+ return result
321
+
322
+ # -------------------------------------------------------------------
323
+ # yfinance-based methods (original implementations)
324
+ # -------------------------------------------------------------------
325
+ def _get_etf_volume_ratio_yfinance(self, symbol: str) -> Dict:
326
+ """Get 20-day / 60-day average volume ratio via yfinance."""
327
+ result = {"symbol": symbol, "vol_20d": None, "vol_60d": None,
328
+ "vol_ratio": None}
329
+
330
+ if not HAS_YFINANCE:
331
+ print("WARNING: yfinance not installed.", file=sys.stderr)
332
+ return result
333
+
334
+ try:
335
+ data = self._get_cached(symbol, period="6mo")
336
+ if data is None or data.empty or "Volume" not in data.columns:
337
+ return result
338
+
339
+ volume = data["Volume"].dropna()
340
+ if len(volume) < 20:
341
+ return result
342
+
343
+ vol_20d = float(volume.tail(20).mean())
344
+ vol_60d = float(volume.tail(60).mean()) if len(volume) >= 60 else float(volume.mean())
345
+
346
+ result["vol_20d"] = vol_20d
347
+ result["vol_60d"] = vol_60d
348
+ result["vol_ratio"] = vol_20d / vol_60d if vol_60d > 0 else None
349
+
350
+ except Exception as e:
351
+ print(f"WARNING: Volume ratio failed for {symbol}: {e}",
352
+ file=sys.stderr)
353
+
354
+ return result
355
+
356
+ def _batch_stock_metrics_yfinance(self, symbols: List[str]) -> List[Dict]:
357
+ """Batch-download stock data and compute metrics via yfinance."""
358
+ if not HAS_YFINANCE:
359
+ print("WARNING: yfinance not installed.", file=sys.stderr)
360
+ return [{"symbol": s, "rsi_14": None, "dist_from_52w_high": None,
361
+ "dist_from_52w_low": None, "pe_ratio": None}
362
+ for s in symbols]
363
+
364
+ try:
365
+ data = yf.download(
366
+ symbols,
367
+ period="1y",
368
+ group_by="ticker",
369
+ threads=True,
370
+ progress=False,
371
+ )
372
+ except Exception as e:
373
+ print(f"WARNING: Batch download failed: {e}", file=sys.stderr)
374
+ return [{"symbol": s, "rsi_14": None, "dist_from_52w_high": None,
375
+ "dist_from_52w_low": None, "pe_ratio": None}
376
+ for s in symbols]
377
+
378
+ results = []
379
+ for symbol in symbols:
380
+ entry = {"symbol": symbol, "rsi_14": None,
381
+ "dist_from_52w_high": None, "dist_from_52w_low": None,
382
+ "pe_ratio": None}
383
+ try:
384
+ if len(symbols) == 1:
385
+ sym_data = data
386
+ else:
387
+ sym_data = data[symbol]
388
+
389
+ if sym_data is None or sym_data.empty:
390
+ results.append(entry)
391
+ continue
392
+
393
+ close = sym_data["Close"].dropna()
394
+ high = sym_data["High"].dropna()
395
+ low = sym_data["Low"].dropna()
396
+
397
+ if len(close) < 2:
398
+ results.append(entry)
399
+ continue
400
+
401
+ entry["rsi_14"] = self._calculate_rsi(close, period=14)
402
+
403
+ distances = self._calculate_52w_distances(close, high, low)
404
+ entry["dist_from_52w_high"] = distances["dist_from_52w_high"]
405
+ entry["dist_from_52w_low"] = distances["dist_from_52w_low"]
406
+
407
+ entry["pe_ratio"] = self._get_pe_ratio(symbol)
408
+
409
+ except Exception as e:
410
+ print(f"WARNING: Metrics failed for {symbol}: {e}",
411
+ file=sys.stderr)
412
+
413
+ results.append(entry)
414
+
415
+ return results
416
+
417
+ # -------------------------------------------------------------------
418
+ # Public methods (FMP -> yfinance symbol-level fallback)
419
+ # -------------------------------------------------------------------
420
+ def get_etf_volume_ratio(self, symbol: str) -> Dict:
421
+ """Get 20-day / 60-day average volume ratio for an ETF.
422
+
423
+ Args:
424
+ symbol: Ticker symbol (e.g., "XLK")
425
+
426
+ Returns:
427
+ Dict with keys: symbol, vol_20d, vol_60d, vol_ratio.
428
+ Values are None if data unavailable.
429
+ """
430
+ if self._fmp_api_key and HAS_REQUESTS:
431
+ fmp_result = self._batch_etf_volume_ratios_fmp([symbol])
432
+ data = fmp_result.get(symbol, {})
433
+ if data.get("vol_20d") is not None:
434
+ return data
435
+
436
+ return self._get_etf_volume_ratio_yfinance(symbol)
437
+
438
+ def batch_etf_volume_ratios(self, symbols: List[str]) -> Dict[str, Dict]:
439
+ """Batch fetch ETF volume ratios with FMP -> yfinance fallback.
440
+
441
+ Args:
442
+ symbols: List of ETF ticker symbols
443
+
444
+ Returns:
445
+ Dict mapping symbol -> {symbol, vol_20d, vol_60d, vol_ratio}
446
+ """
447
+ if not symbols:
448
+ return {}
449
+
450
+ # Phase 1: Try FMP batch
451
+ fmp_results: Dict[str, Dict] = {}
452
+ fmp_attempted = self._fmp_api_key and HAS_REQUESTS
453
+ if fmp_attempted:
454
+ fmp_results = self._batch_etf_volume_ratios_fmp(symbols)
455
+
456
+ # Phase 2: yfinance for missing/empty
457
+ result: Dict[str, Dict] = {}
458
+ missing_for_yf = []
459
+ for sym in symbols:
460
+ fmp_data = fmp_results.get(sym, {})
461
+ if fmp_data.get("vol_20d") is not None:
462
+ result[sym] = fmp_data
463
+ else:
464
+ missing_for_yf.append(sym)
465
+
466
+ if missing_for_yf:
467
+ if fmp_attempted:
468
+ self._stats["yf_fallbacks"] += 1
469
+ for sym in missing_for_yf:
470
+ self._stats["yf_calls"] += 1
471
+ result[sym] = self._get_etf_volume_ratio_yfinance(sym)
472
+
473
+ return result
474
+
475
+ def batch_stock_metrics(self, symbols: List[str]) -> List[Dict]:
476
+ """Batch compute stock metrics with FMP -> yfinance symbol-level fallback.
477
+
478
+ Args:
479
+ symbols: List of ticker symbols
480
+
481
+ Returns:
482
+ List of dicts with keys: symbol, rsi_14, dist_from_52w_high,
483
+ dist_from_52w_low, pe_ratio. Values are None if unavailable.
484
+ """
485
+ if not symbols:
486
+ return []
487
+
488
+ # Phase 1: Try FMP
489
+ fmp_results: Dict[str, Dict] = {}
490
+ fmp_attempted = self._fmp_api_key and HAS_REQUESTS
491
+ if fmp_attempted:
492
+ fmp_list = self._batch_stock_metrics_fmp(symbols)
493
+ for m in fmp_list:
494
+ sym = m["symbol"]
495
+ if m.get("pe_ratio") is not None or m.get("rsi_14") is not None:
496
+ fmp_results[sym] = m
497
+
498
+ # Phase 2: yfinance for missing symbols only
499
+ missing = [s for s in symbols if s not in fmp_results]
500
+ yf_results: Dict[str, Dict] = {}
501
+ if missing:
502
+ if fmp_attempted:
503
+ self._stats["yf_fallbacks"] += 1
504
+ yf_list = self._batch_stock_metrics_yfinance(missing)
505
+ self._stats["yf_calls"] += 1
506
+ for m in yf_list:
507
+ yf_results[m["symbol"]] = m
508
+
509
+ # Merge: FMP takes priority, yfinance fills gaps
510
+ results = []
511
+ for s in symbols:
512
+ if s in fmp_results:
513
+ results.append(fmp_results[s])
514
+ elif s in yf_results:
515
+ results.append(yf_results[s])
516
+ else:
517
+ results.append({
518
+ "symbol": s, "rsi_14": None,
519
+ "dist_from_52w_high": None,
520
+ "dist_from_52w_low": None, "pe_ratio": None,
521
+ })
522
+ return results
523
+
524
+ # -------------------------------------------------------------------
525
+ # Shared utilities (unchanged)
526
+ # -------------------------------------------------------------------
527
+ @staticmethod
528
+ def _calculate_rsi(prices: pd.Series, period: int = 14) -> Optional[float]:
529
+ """Calculate RSI using Wilder's smoothing method."""
530
+ if prices is None or len(prices) < period + 1:
531
+ return None
532
+
533
+ deltas = prices.diff()
534
+
535
+ gains = deltas.where(deltas > 0, 0.0)
536
+ losses = (-deltas).where(deltas < 0, 0.0)
537
+
538
+ first_avg_gain = gains.iloc[1:period + 1].mean()
539
+ first_avg_loss = losses.iloc[1:period + 1].mean()
540
+
541
+ avg_gain = first_avg_gain
542
+ avg_loss = first_avg_loss
543
+
544
+ for i in range(period + 1, len(prices)):
545
+ avg_gain = (avg_gain * (period - 1) + gains.iloc[i]) / period
546
+ avg_loss = (avg_loss * (period - 1) + losses.iloc[i]) / period
547
+
548
+ if avg_loss == 0:
549
+ return 100.0
550
+
551
+ rs = avg_gain / avg_loss
552
+ rsi = 100.0 - (100.0 / (1.0 + rs))
553
+ return round(rsi, 2)
554
+
555
+ @staticmethod
556
+ def _calculate_52w_distances(close: pd.Series,
557
+ high: pd.Series,
558
+ low: pd.Series) -> Dict:
559
+ """Calculate distance from 52-week high and low."""
560
+ result = {"dist_from_52w_high": None, "dist_from_52w_low": None}
561
+
562
+ if close.empty:
563
+ return result
564
+
565
+ current = float(close.iloc[-1])
566
+ if current <= 0:
567
+ return result
568
+
569
+ high_52w = float(high.max())
570
+ low_52w = float(low.min())
571
+
572
+ if high_52w > 0:
573
+ result["dist_from_52w_high"] = round(
574
+ (high_52w - current) / high_52w, 4
575
+ )
576
+
577
+ if low_52w >= 0:
578
+ result["dist_from_52w_low"] = round(
579
+ (current - low_52w) / current, 4
580
+ ) if current > 0 else None
581
+
582
+ return result
583
+
584
+ def _get_pe_ratio(self, symbol: str) -> Optional[float]:
585
+ """Get trailing P/E ratio for a symbol via yfinance info."""
586
+ try:
587
+ ticker = yf.Ticker(symbol)
588
+ info = ticker.info
589
+ pe = info.get("trailingPE")
590
+ if pe is not None:
591
+ return round(float(pe), 2)
592
+ except Exception:
593
+ pass
594
+ return None
595
+
596
+ def _get_cached(self, symbol: str, period: str = "6mo") -> Optional[pd.DataFrame]:
597
+ """Get cached download or fetch new data."""
598
+ cache_key = f"{symbol}_{period}"
599
+ if cache_key in self._cache:
600
+ return self._cache[cache_key]
601
+
602
+ try:
603
+ data = yf.download(symbol, period=period, progress=False)
604
+ self._cache[cache_key] = data
605
+ return data
606
+ except Exception as e:
607
+ print(f"WARNING: Download failed for {symbol}: {e}",
608
+ file=sys.stderr)
609
+ return None