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,245 @@
1
+ """Tests for heat_calculator.py - Theme Heat Score (0-100)"""
2
+
3
+ import pytest
4
+ from calculators.heat_calculator import (
5
+ momentum_strength_score,
6
+ volume_intensity_score,
7
+ uptrend_signal_score,
8
+ breadth_signal_score,
9
+ calculate_theme_heat,
10
+ )
11
+
12
+
13
+ # ── momentum_strength_score ──────────────────────────────────────────
14
+
15
+ class TestMomentumStrengthScore:
16
+ """sigmoid: 100 / (1 + exp(-0.15 * (abs(wr%) - 5.0)))"""
17
+
18
+ def test_zero_return(self):
19
+ # abs(0) - 5 = -5 => sigmoid(0.75) ≈ 32
20
+ score = momentum_strength_score(0.0)
21
+ assert 30 <= score <= 35
22
+
23
+ def test_five_percent(self):
24
+ # abs(5) - 5 = 0 => sigmoid(0) = 50
25
+ score = momentum_strength_score(5.0)
26
+ assert score == pytest.approx(50.0, abs=0.1)
27
+
28
+ def test_negative_five_percent(self):
29
+ # abs(-5) - 5 = 0 => 50
30
+ score = momentum_strength_score(-5.0)
31
+ assert score == pytest.approx(50.0, abs=0.1)
32
+
33
+ def test_fifteen_percent(self):
34
+ # abs(15) - 5 = 10 => sigmoid(-1.5) ≈ 82
35
+ score = momentum_strength_score(15.0)
36
+ assert 80 <= score <= 85
37
+
38
+ def test_twenty_percent(self):
39
+ # abs(20) - 5 = 15 => sigmoid(-2.25) ≈ 90
40
+ score = momentum_strength_score(20.0)
41
+ assert 88 <= score <= 92
42
+
43
+ def test_negative_twenty_percent(self):
44
+ score = momentum_strength_score(-20.0)
45
+ assert 88 <= score <= 92
46
+
47
+ def test_returns_float(self):
48
+ assert isinstance(momentum_strength_score(3.0), float)
49
+
50
+
51
+ # ── volume_intensity_score ───────────────────────────────────────────
52
+
53
+ class TestVolumeIntensityScore:
54
+ """min(100, max(0, (vol_20d/vol_60d - 0.8) * 250))"""
55
+
56
+ def test_ratio_0_8_returns_zero(self):
57
+ # (0.8 - 0.8) * 250 = 0
58
+ assert volume_intensity_score(80.0, 100.0) == pytest.approx(0.0)
59
+
60
+ def test_ratio_1_0_returns_50(self):
61
+ # (1.0 - 0.8) * 250 = 50
62
+ assert volume_intensity_score(100.0, 100.0) == pytest.approx(50.0)
63
+
64
+ def test_ratio_1_2_returns_100(self):
65
+ # (1.2 - 0.8) * 250 = 100
66
+ assert volume_intensity_score(120.0, 100.0) == pytest.approx(100.0)
67
+
68
+ def test_ratio_above_cap_clamped_to_100(self):
69
+ # (2.0 - 0.8) * 250 = 300 => clamped 100
70
+ assert volume_intensity_score(200.0, 100.0) == pytest.approx(100.0)
71
+
72
+ def test_ratio_below_floor_clamped_to_0(self):
73
+ # (0.5 - 0.8) * 250 = -75 => clamped 0
74
+ assert volume_intensity_score(50.0, 100.0) == pytest.approx(0.0)
75
+
76
+ def test_none_vol_20d(self):
77
+ assert volume_intensity_score(None, 100.0) == pytest.approx(50.0)
78
+
79
+ def test_none_vol_60d(self):
80
+ assert volume_intensity_score(100.0, None) == pytest.approx(50.0)
81
+
82
+ def test_zero_vol_60d(self):
83
+ assert volume_intensity_score(100.0, 0.0) == pytest.approx(50.0)
84
+
85
+
86
+ # ── uptrend_signal_score ─────────────────────────────────────────────
87
+
88
+ class TestUptrendSignalScore:
89
+
90
+ def _make_sector(self, ratio, ma_10, slope, weight=1.0):
91
+ return {
92
+ "sector": "test",
93
+ "ratio": ratio,
94
+ "ma_10": ma_10,
95
+ "slope": slope,
96
+ "weight": weight,
97
+ }
98
+
99
+ def test_both_positive_gives_80(self):
100
+ # ratio>ma_10 AND slope>0 => 80
101
+ data = [self._make_sector(ratio=50, ma_10=40, slope=0.5)]
102
+ score = uptrend_signal_score(data, is_bearish=False)
103
+ assert score == pytest.approx(80.0)
104
+
105
+ def test_ratio_only_gives_60(self):
106
+ # ratio>ma_10 but slope<=0 => 60
107
+ data = [self._make_sector(ratio=50, ma_10=40, slope=-0.1)]
108
+ score = uptrend_signal_score(data, is_bearish=False)
109
+ assert score == pytest.approx(60.0)
110
+
111
+ def test_slope_only_gives_60(self):
112
+ # ratio<=ma_10 but slope>0 => 60
113
+ data = [self._make_sector(ratio=30, ma_10=40, slope=0.5)]
114
+ score = uptrend_signal_score(data, is_bearish=False)
115
+ assert score == pytest.approx(60.0)
116
+
117
+ def test_neither_gives_20(self):
118
+ # ratio<=ma_10 AND slope<=0 => 20
119
+ data = [self._make_sector(ratio=30, ma_10=40, slope=-0.1)]
120
+ score = uptrend_signal_score(data, is_bearish=False)
121
+ assert score == pytest.approx(20.0)
122
+
123
+ def test_weighted_average(self):
124
+ # sector A: both positive => 80, weight 2
125
+ # sector B: neither => 20, weight 1
126
+ # weighted = (80*2 + 20*1) / 3 = 60
127
+ data = [
128
+ self._make_sector(ratio=50, ma_10=40, slope=0.5, weight=2.0),
129
+ self._make_sector(ratio=30, ma_10=40, slope=-0.1, weight=1.0),
130
+ ]
131
+ score = uptrend_signal_score(data, is_bearish=False)
132
+ assert score == pytest.approx(60.0)
133
+
134
+ def test_bearish_inversion(self):
135
+ # both positive => 80, bearish => 100-80 = 20
136
+ data = [self._make_sector(ratio=50, ma_10=40, slope=0.5)]
137
+ score = uptrend_signal_score(data, is_bearish=True)
138
+ assert score == pytest.approx(20.0)
139
+
140
+ def test_empty_list(self):
141
+ assert uptrend_signal_score([], is_bearish=False) == pytest.approx(50.0)
142
+
143
+ def test_equal_ratio_and_ma10(self):
144
+ # ratio == ma_10 => not > => slope<=0 => 20
145
+ data = [self._make_sector(ratio=40, ma_10=40, slope=0)]
146
+ score = uptrend_signal_score(data, is_bearish=False)
147
+ assert score == pytest.approx(20.0)
148
+
149
+
150
+ # ── breadth_signal_score ─────────────────────────────────────────────
151
+
152
+ class TestBreadthSignalScore:
153
+
154
+ def test_zero(self):
155
+ assert breadth_signal_score(0.0) == pytest.approx(0.0)
156
+
157
+ def test_half(self):
158
+ assert breadth_signal_score(0.5) == pytest.approx(50.0)
159
+
160
+ def test_full(self):
161
+ assert breadth_signal_score(1.0) == pytest.approx(100.0)
162
+
163
+ def test_above_one_clamped(self):
164
+ assert breadth_signal_score(1.5) == pytest.approx(100.0)
165
+
166
+ def test_negative_clamped(self):
167
+ assert breadth_signal_score(-0.3) == pytest.approx(0.0)
168
+
169
+ def test_none(self):
170
+ assert breadth_signal_score(None) == pytest.approx(50.0)
171
+
172
+
173
+ # ── calculate_theme_heat ─────────────────────────────────────────────
174
+
175
+ class TestCalculateThemeHeat:
176
+
177
+ def test_weighted_sum(self):
178
+ # 80*0.40 + 60*0.25 + 70*0.20 + 50*0.15
179
+ # = 32 + 15 + 14 + 7.5 = 68.5
180
+ result = calculate_theme_heat(80.0, 60.0, 70.0, 50.0)
181
+ assert result == pytest.approx(68.5)
182
+
183
+ def test_all_100(self):
184
+ result = calculate_theme_heat(100.0, 100.0, 100.0, 100.0)
185
+ assert result == pytest.approx(100.0)
186
+
187
+ def test_all_zero(self):
188
+ result = calculate_theme_heat(0.0, 0.0, 0.0, 0.0)
189
+ assert result == pytest.approx(0.0)
190
+
191
+ def test_none_defaults_to_50(self):
192
+ # All None => 50*0.40 + 50*0.25 + 50*0.20 + 50*0.15 = 50
193
+ result = calculate_theme_heat(None, None, None, None)
194
+ assert result == pytest.approx(50.0)
195
+
196
+ def test_partial_none(self):
197
+ # 80*0.40 + 50*0.25 + 50*0.20 + 50*0.15
198
+ # = 32 + 12.5 + 10 + 7.5 = 62.0
199
+ result = calculate_theme_heat(80.0, None, None, None)
200
+ assert result == pytest.approx(62.0)
201
+
202
+ def test_clamped_above_100(self):
203
+ result = calculate_theme_heat(200.0, 200.0, 200.0, 200.0)
204
+ assert result == 100.0
205
+
206
+ def test_clamped_below_0(self):
207
+ result = calculate_theme_heat(-50.0, -50.0, -50.0, -50.0)
208
+ assert result == 0.0
209
+
210
+ def test_returns_float(self):
211
+ assert isinstance(calculate_theme_heat(50, 50, 50, 50), float)
212
+
213
+
214
+ # ── uptrend_signal_score with None values ────────────────────────────
215
+
216
+ class TestUptrendSignalNoneValues:
217
+ """Ensure None values in sector_data don't cause TypeError."""
218
+
219
+ def test_none_ma_10(self):
220
+ """ma_10=None should not crash (treated as 0)."""
221
+ data = [{"sector": "Tech", "ratio": 0.5, "ma_10": None, "slope": 0.01, "weight": 1.0}]
222
+ score = uptrend_signal_score(data, is_bearish=False)
223
+ # ratio(0.5) > ma_10(0) AND slope(0.01) > 0 => 80
224
+ assert score == pytest.approx(80.0)
225
+
226
+ def test_none_slope(self):
227
+ """slope=None should not crash (treated as 0)."""
228
+ data = [{"sector": "Tech", "ratio": 0.5, "ma_10": 0.3, "slope": None, "weight": 1.0}]
229
+ score = uptrend_signal_score(data, is_bearish=False)
230
+ # ratio(0.5) > ma_10(0.3) but slope(0) not > 0 => 60
231
+ assert score == pytest.approx(60.0)
232
+
233
+ def test_none_ratio(self):
234
+ """ratio=None should not crash (treated as 0)."""
235
+ data = [{"sector": "Tech", "ratio": None, "ma_10": 0.3, "slope": 0.01, "weight": 1.0}]
236
+ score = uptrend_signal_score(data, is_bearish=False)
237
+ # ratio(0) not > ma_10(0.3) but slope(0.01) > 0 => 60
238
+ assert score == pytest.approx(60.0)
239
+
240
+ def test_all_none(self):
241
+ """All values None should not crash."""
242
+ data = [{"sector": "Tech", "ratio": None, "ma_10": None, "slope": None, "weight": 1.0}]
243
+ score = uptrend_signal_score(data, is_bearish=False)
244
+ # ratio(0) not > ma_10(0), slope(0) not > 0 => 20
245
+ assert score == pytest.approx(20.0)
@@ -0,0 +1,256 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Tests for industry_ranker module.
4
+
5
+ Covers momentum_strength_score sigmoid, weighted return calculation,
6
+ ranking, direction classification, and top/bottom extraction.
7
+ """
8
+
9
+ import math
10
+
11
+ from calculators.industry_ranker import (
12
+ TIMEFRAME_WEIGHTS,
13
+ get_top_bottom_industries,
14
+ momentum_strength_score,
15
+ rank_industries,
16
+ )
17
+
18
+ # ---------------------------------------------------------------------------
19
+ # momentum_strength_score tests
20
+ # ---------------------------------------------------------------------------
21
+
22
+
23
+ class TestMomentumStrengthScore:
24
+ """Test the direction-neutral sigmoid scoring function."""
25
+
26
+ def test_zero_return_gives_about_32(self):
27
+ """abs(0%) -> ~32 (sigmoid at -5.0 midpoint)."""
28
+ score = momentum_strength_score(0.0)
29
+ assert 30 <= score <= 34, f"Expected ~32, got {score}"
30
+
31
+ def test_five_pct_gives_50(self):
32
+ """abs(5%) is the midpoint -> exactly 50."""
33
+ score = momentum_strength_score(5.0)
34
+ assert abs(score - 50.0) < 0.1, f"Expected 50, got {score}"
35
+
36
+ def test_ten_pct_gives_about_68(self):
37
+ """abs(10%) -> ~68."""
38
+ score = momentum_strength_score(10.0)
39
+ assert 66 <= score <= 70, f"Expected ~68, got {score}"
40
+
41
+ def test_fifteen_pct_gives_about_82(self):
42
+ """abs(15%) -> ~82."""
43
+ score = momentum_strength_score(15.0)
44
+ assert 80 <= score <= 84, f"Expected ~82, got {score}"
45
+
46
+ def test_twenty_pct_gives_about_90(self):
47
+ """abs(20%) -> ~90."""
48
+ score = momentum_strength_score(20.0)
49
+ assert 88 <= score <= 92, f"Expected ~90, got {score}"
50
+
51
+ def test_negative_return_same_as_positive(self):
52
+ """Negative and positive returns with same magnitude give same score."""
53
+ pos = momentum_strength_score(12.0)
54
+ neg = momentum_strength_score(-12.0)
55
+ assert abs(pos - neg) < 0.01
56
+
57
+ def test_large_return_caps_near_100(self):
58
+ """Very large returns approach but don't exceed 100."""
59
+ score = momentum_strength_score(50.0)
60
+ assert 99 <= score <= 100
61
+
62
+ def test_score_always_between_0_and_100(self):
63
+ """Score is bounded [0, 100] for any input."""
64
+ for ret in [-100, -50, -10, 0, 10, 50, 100]:
65
+ score = momentum_strength_score(ret)
66
+ assert 0 <= score <= 100, f"Out of bounds for {ret}: {score}"
67
+
68
+ def test_monotonically_increasing_with_abs(self):
69
+ """Higher absolute return -> higher score."""
70
+ s1 = momentum_strength_score(5.0)
71
+ s2 = momentum_strength_score(10.0)
72
+ s3 = momentum_strength_score(20.0)
73
+ assert s1 < s2 < s3
74
+
75
+
76
+ # ---------------------------------------------------------------------------
77
+ # TIMEFRAME_WEIGHTS validation
78
+ # ---------------------------------------------------------------------------
79
+
80
+
81
+ class TestTimeframeWeights:
82
+ """Verify timeframe weights are correct and sum to 1.0."""
83
+
84
+ def test_weights_sum_to_one(self):
85
+ total = sum(TIMEFRAME_WEIGHTS.values())
86
+ assert abs(total - 1.0) < 0.001
87
+
88
+ def test_expected_keys(self):
89
+ expected = {"perf_1w", "perf_1m", "perf_3m", "perf_6m"}
90
+ assert set(TIMEFRAME_WEIGHTS.keys()) == expected
91
+
92
+ def test_expected_values(self):
93
+ assert TIMEFRAME_WEIGHTS["perf_1w"] == 0.10
94
+ assert TIMEFRAME_WEIGHTS["perf_1m"] == 0.25
95
+ assert TIMEFRAME_WEIGHTS["perf_3m"] == 0.35
96
+ assert TIMEFRAME_WEIGHTS["perf_6m"] == 0.30
97
+
98
+
99
+ # ---------------------------------------------------------------------------
100
+ # rank_industries tests
101
+ # ---------------------------------------------------------------------------
102
+
103
+
104
+ def _make_industry(name, perf_1w=0.0, perf_1m=0.0, perf_3m=0.0, perf_6m=0.0):
105
+ """Helper to build a synthetic industry dict."""
106
+ return {
107
+ "name": name,
108
+ "perf_1w": perf_1w,
109
+ "perf_1m": perf_1m,
110
+ "perf_3m": perf_3m,
111
+ "perf_6m": perf_6m,
112
+ }
113
+
114
+
115
+ class TestRankIndustries:
116
+ """Test rank_industries function."""
117
+
118
+ def test_adds_required_fields(self):
119
+ """Each result dict gets momentum_score, weighted_return, direction, rank."""
120
+ industries = [_make_industry("Tech", 5, 10, 15, 20)]
121
+ ranked = rank_industries(industries)
122
+ assert len(ranked) == 1
123
+ item = ranked[0]
124
+ assert "momentum_score" in item
125
+ assert "weighted_return" in item
126
+ assert "direction" in item
127
+ assert "rank" in item
128
+
129
+ def test_weighted_return_calculation(self):
130
+ """Verify weighted return = sum(perf_x * weight_x)."""
131
+ ind = _make_industry("Test", perf_1w=10.0, perf_1m=20.0, perf_3m=30.0, perf_6m=40.0)
132
+ ranked = rank_industries([ind])
133
+ expected = 10 * 0.10 + 20 * 0.25 + 30 * 0.35 + 40 * 0.30
134
+ assert abs(ranked[0]["weighted_return"] - expected) < 0.01
135
+
136
+ def test_sorted_by_momentum_score_desc(self):
137
+ """Industries are sorted by momentum_score descending."""
138
+ industries = [
139
+ _make_industry("Low", perf_1w=1, perf_1m=1, perf_3m=1, perf_6m=1),
140
+ _make_industry("High", perf_1w=20, perf_1m=30, perf_3m=40, perf_6m=50),
141
+ _make_industry("Mid", perf_1w=5, perf_1m=10, perf_3m=10, perf_6m=10),
142
+ ]
143
+ ranked = rank_industries(industries)
144
+ assert ranked[0]["name"] == "High"
145
+ assert ranked[-1]["name"] == "Low"
146
+ # Verify descending order
147
+ scores = [r["momentum_score"] for r in ranked]
148
+ assert scores == sorted(scores, reverse=True)
149
+
150
+ def test_rank_starts_at_one(self):
151
+ """Rank should be 1-based."""
152
+ industries = [
153
+ _make_industry("A", perf_3m=10),
154
+ _make_industry("B", perf_3m=20),
155
+ ]
156
+ ranked = rank_industries(industries)
157
+ assert ranked[0]["rank"] == 1
158
+ assert ranked[1]["rank"] == 2
159
+
160
+ def test_bullish_direction_for_positive_return(self):
161
+ """Positive weighted return -> direction='bullish'."""
162
+ ranked = rank_industries([_make_industry("Up", perf_3m=15, perf_6m=10)])
163
+ assert ranked[0]["direction"] == "bullish"
164
+
165
+ def test_bearish_direction_for_negative_return(self):
166
+ """Negative weighted return -> direction='bearish'."""
167
+ ranked = rank_industries([_make_industry("Down", perf_3m=-15, perf_6m=-10)])
168
+ assert ranked[0]["direction"] == "bearish"
169
+
170
+ def test_zero_return_direction(self):
171
+ """Zero weighted return -> direction='bearish' (non-positive)."""
172
+ ranked = rank_industries([_make_industry("Flat")])
173
+ assert ranked[0]["direction"] == "bearish"
174
+
175
+ def test_empty_list_returns_empty(self):
176
+ """Empty input returns empty list."""
177
+ assert rank_industries([]) == []
178
+
179
+ def test_preserves_original_fields(self):
180
+ """Original industry dict fields are preserved in output."""
181
+ ind = _make_industry("Test", perf_1w=5, perf_1m=10, perf_3m=15, perf_6m=20)
182
+ ind["sector"] = "Technology" # extra field
183
+ ranked = rank_industries([ind])
184
+ assert ranked[0]["name"] == "Test"
185
+ assert ranked[0]["perf_1w"] == 5
186
+ assert ranked[0]["sector"] == "Technology"
187
+
188
+ def test_mixed_bullish_bearish(self):
189
+ """Mix of positive and negative returns handled correctly."""
190
+ industries = [
191
+ _make_industry("Bull", perf_1w=10, perf_1m=15, perf_3m=20, perf_6m=25),
192
+ _make_industry("Bear", perf_1w=-10, perf_1m=-15, perf_3m=-20, perf_6m=-25),
193
+ ]
194
+ ranked = rank_industries(industries)
195
+ # Both should have same momentum_score (direction-neutral)
196
+ assert abs(ranked[0]["momentum_score"] - ranked[1]["momentum_score"]) < 0.01
197
+ # But different directions
198
+ directions = {r["name"]: r["direction"] for r in ranked}
199
+ assert directions["Bull"] == "bullish"
200
+ assert directions["Bear"] == "bearish"
201
+
202
+
203
+ # ---------------------------------------------------------------------------
204
+ # get_top_bottom_industries tests
205
+ # ---------------------------------------------------------------------------
206
+
207
+
208
+ class TestGetTopBottomIndustries:
209
+ """Test top/bottom extraction."""
210
+
211
+ def test_top_and_bottom_n(self):
212
+ """Returns correct top N and bottom N."""
213
+ industries = [
214
+ _make_industry(f"Ind{i}", perf_3m=20 - i * 2, perf_6m=15 - i)
215
+ for i in range(10)
216
+ ]
217
+ ranked = rank_industries(industries)
218
+ result = get_top_bottom_industries(ranked, n=3)
219
+ assert len(result["top"]) == 3
220
+ assert len(result["bottom"]) == 3
221
+ # Top should have rank 1,2,3
222
+ assert result["top"][0]["rank"] == 1
223
+ assert result["top"][2]["rank"] == 3
224
+ # Bottom should have rank 8,9,10
225
+ assert result["bottom"][0]["rank"] == 8
226
+ assert result["bottom"][2]["rank"] == 10
227
+
228
+ def test_n_greater_than_half(self):
229
+ """When n > len/2, top and bottom may overlap; should handle gracefully."""
230
+ industries = [_make_industry(f"I{i}", perf_3m=i) for i in range(4)]
231
+ ranked = rank_industries(industries)
232
+ result = get_top_bottom_industries(ranked, n=3)
233
+ assert len(result["top"]) == 3
234
+ assert len(result["bottom"]) == 3
235
+
236
+ def test_empty_ranked_list(self):
237
+ """Empty input returns empty top and bottom."""
238
+ result = get_top_bottom_industries([], n=5)
239
+ assert result["top"] == []
240
+ assert result["bottom"] == []
241
+
242
+ def test_n_exceeds_list_length(self):
243
+ """n > len(ranked) returns all available."""
244
+ industries = [_make_industry(f"I{i}", perf_3m=i * 5) for i in range(3)]
245
+ ranked = rank_industries(industries)
246
+ result = get_top_bottom_industries(ranked, n=10)
247
+ assert len(result["top"]) == 3
248
+ assert len(result["bottom"]) == 3
249
+
250
+ def test_default_n_is_5(self):
251
+ """Default n should be 5."""
252
+ industries = [_make_industry(f"I{i}", perf_3m=i * 3) for i in range(20)]
253
+ ranked = rank_industries(industries)
254
+ result = get_top_bottom_industries(ranked)
255
+ assert len(result["top"]) == 5
256
+ assert len(result["bottom"]) == 5