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,534 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Tests for theme_classifier module.
4
+
5
+ Covers cross-sector theme matching, vertical (single-sector) theme detection,
6
+ sector weight calculation, and edge cases.
7
+ """
8
+
9
+ from calculators.industry_ranker import rank_industries
10
+ from calculators.theme_classifier import (
11
+ classify_themes,
12
+ get_theme_sector_weights,
13
+ get_matched_industry_names,
14
+ )
15
+
16
+ # ---------------------------------------------------------------------------
17
+ # Sample themes_config for testing
18
+ # ---------------------------------------------------------------------------
19
+
20
+ SAMPLE_THEMES_CONFIG = {
21
+ "cross_sector": [
22
+ {
23
+ "theme_name": "AI & Automation",
24
+ "matching_keywords": ["Semiconductor", "Software - Application", "IT Services"],
25
+ "proxy_etfs": ["BOTZ", "ROBO"],
26
+ "static_stocks": ["NVDA", "MSFT"],
27
+ },
28
+ {
29
+ "theme_name": "Green Energy Transition",
30
+ "matching_keywords": ["Solar", "Utilities - Renewable", "Auto Manufacturers"],
31
+ "proxy_etfs": ["ICLN", "TAN"],
32
+ "static_stocks": ["ENPH", "TSLA"],
33
+ },
34
+ {
35
+ "theme_name": "Infrastructure Boom",
36
+ "matching_keywords": ["Building Materials", "Engineering & Construction", "Steel"],
37
+ "proxy_etfs": ["PAVE", "IFRA"],
38
+ "static_stocks": ["VMC", "NUE"],
39
+ },
40
+ ],
41
+ "vertical_min_industries": 3,
42
+ "cross_sector_min_matches": 2,
43
+ }
44
+
45
+
46
+ # ---------------------------------------------------------------------------
47
+ # Helpers
48
+ # ---------------------------------------------------------------------------
49
+
50
+
51
+ def _make_ranked_industry(name, weighted_return, momentum_score, direction, rank, sector=None):
52
+ """Build a ranked industry dict (simulates output of rank_industries)."""
53
+ entry = {
54
+ "name": name,
55
+ "perf_1w": 0.0,
56
+ "perf_1m": 0.0,
57
+ "perf_3m": 0.0,
58
+ "perf_6m": 0.0,
59
+ "weighted_return": weighted_return,
60
+ "momentum_score": momentum_score,
61
+ "direction": direction,
62
+ "rank": rank,
63
+ }
64
+ if sector is not None:
65
+ entry["sector"] = sector
66
+ return entry
67
+
68
+
69
+ # ---------------------------------------------------------------------------
70
+ # Cross-sector theme matching
71
+ # ---------------------------------------------------------------------------
72
+
73
+
74
+ class TestCrossSectorThemes:
75
+ """Test cross-sector theme detection (min 2 matching industries)."""
76
+
77
+ def test_ai_theme_detected_with_two_matches(self):
78
+ """Two matching industries from AI theme -> theme detected."""
79
+ ranked = [
80
+ _make_ranked_industry("Semiconductor", 15.0, 82.0, "bullish", 1, "Technology"),
81
+ _make_ranked_industry("Software - Application", 12.0, 75.0, "bullish", 2, "Technology"),
82
+ _make_ranked_industry("Banks - Regional", 8.0, 65.0, "bullish", 3, "Financial Services"),
83
+ ]
84
+ themes = classify_themes(ranked, SAMPLE_THEMES_CONFIG)
85
+ theme_names = [t["theme_name"] for t in themes]
86
+ assert "AI & Automation" in theme_names
87
+
88
+ def test_ai_theme_not_detected_with_one_match(self):
89
+ """Only one matching industry -> theme NOT detected."""
90
+ ranked = [
91
+ _make_ranked_industry("Semiconductor", 15.0, 82.0, "bullish", 1, "Technology"),
92
+ _make_ranked_industry("Banks - Regional", 8.0, 65.0, "bullish", 2, "Financial Services"),
93
+ _make_ranked_industry("Oil & Gas E&P", 5.0, 55.0, "bullish", 3, "Energy"),
94
+ ]
95
+ themes = classify_themes(ranked, SAMPLE_THEMES_CONFIG)
96
+ theme_names = [t["theme_name"] for t in themes]
97
+ assert "AI & Automation" not in theme_names
98
+
99
+ def test_all_three_matches(self):
100
+ """All three keywords match -> theme detected with all three matching."""
101
+ ranked = [
102
+ _make_ranked_industry("Semiconductor", 15.0, 82.0, "bullish", 1, "Technology"),
103
+ _make_ranked_industry("Software - Application", 12.0, 75.0, "bullish", 2, "Technology"),
104
+ _make_ranked_industry("IT Services", 10.0, 70.0, "bullish", 3, "Technology"),
105
+ ]
106
+ themes = classify_themes(ranked, SAMPLE_THEMES_CONFIG)
107
+ ai_theme = [t for t in themes if t["theme_name"] == "AI & Automation"][0]
108
+ assert len(ai_theme["matching_industries"]) == 3
109
+
110
+ def test_theme_direction_bullish(self):
111
+ """Theme direction is bullish when matching industries are bullish."""
112
+ ranked = [
113
+ _make_ranked_industry("Semiconductor", 15.0, 82.0, "bullish", 1, "Technology"),
114
+ _make_ranked_industry("Software - Application", 12.0, 75.0, "bullish", 2, "Technology"),
115
+ ]
116
+ themes = classify_themes(ranked, SAMPLE_THEMES_CONFIG)
117
+ ai_theme = [t for t in themes if t["theme_name"] == "AI & Automation"][0]
118
+ assert ai_theme["direction"] == "bullish"
119
+
120
+ def test_theme_direction_bearish(self):
121
+ """Theme direction is bearish when matching industries are bearish."""
122
+ ranked = [
123
+ _make_ranked_industry("Semiconductor", -15.0, 82.0, "bearish", 1, "Technology"),
124
+ _make_ranked_industry("Software - Application", -12.0, 75.0, "bearish", 2, "Technology"),
125
+ ]
126
+ themes = classify_themes(ranked, SAMPLE_THEMES_CONFIG)
127
+ ai_theme = [t for t in themes if t["theme_name"] == "AI & Automation"][0]
128
+ assert ai_theme["direction"] == "bearish"
129
+
130
+ def test_theme_direction_mixed(self):
131
+ """Mixed directions -> majority determines theme direction."""
132
+ ranked = [
133
+ _make_ranked_industry("Semiconductor", 15.0, 82.0, "bullish", 1, "Technology"),
134
+ _make_ranked_industry("Software - Application", -5.0, 50.0, "bearish", 5, "Technology"),
135
+ _make_ranked_industry("IT Services", 8.0, 65.0, "bullish", 3, "Technology"),
136
+ ]
137
+ themes = classify_themes(ranked, SAMPLE_THEMES_CONFIG)
138
+ ai_theme = [t for t in themes if t["theme_name"] == "AI & Automation"][0]
139
+ # 2 bullish vs 1 bearish -> bullish
140
+ assert ai_theme["direction"] == "bullish"
141
+
142
+ def test_proxy_etfs_and_static_stocks_included(self):
143
+ """Theme result includes proxy_etfs and static_stocks from config."""
144
+ ranked = [
145
+ _make_ranked_industry("Semiconductor", 15.0, 82.0, "bullish", 1, "Technology"),
146
+ _make_ranked_industry("Software - Application", 12.0, 75.0, "bullish", 2, "Technology"),
147
+ ]
148
+ themes = classify_themes(ranked, SAMPLE_THEMES_CONFIG)
149
+ ai_theme = [t for t in themes if t["theme_name"] == "AI & Automation"][0]
150
+ assert ai_theme["proxy_etfs"] == ["BOTZ", "ROBO"]
151
+ assert ai_theme["static_stocks"] == ["NVDA", "MSFT"]
152
+
153
+ def test_multiple_themes_detected(self):
154
+ """Multiple themes can be detected simultaneously."""
155
+ ranked = [
156
+ _make_ranked_industry("Semiconductor", 15.0, 82.0, "bullish", 1, "Technology"),
157
+ _make_ranked_industry("Software - Application", 12.0, 75.0, "bullish", 2, "Technology"),
158
+ _make_ranked_industry("Solar", 18.0, 85.0, "bullish", 3, "Energy"),
159
+ _make_ranked_industry("Auto Manufacturers", 10.0, 70.0, "bullish", 4, "Consumer Cyclical"),
160
+ ]
161
+ themes = classify_themes(ranked, SAMPLE_THEMES_CONFIG)
162
+ theme_names = [t["theme_name"] for t in themes]
163
+ assert "AI & Automation" in theme_names
164
+ assert "Green Energy Transition" in theme_names
165
+
166
+
167
+ # ---------------------------------------------------------------------------
168
+ # Vertical (single-sector) theme detection
169
+ # ---------------------------------------------------------------------------
170
+
171
+
172
+ class TestVerticalThemes:
173
+ """Test vertical theme detection (min 3 same-sector industries in top/bottom)."""
174
+
175
+ def test_vertical_theme_detected_with_three_same_sector(self):
176
+ """3+ industries from same sector in top ranks -> vertical theme detected."""
177
+ ranked = [
178
+ _make_ranked_industry("Semiconductor", 20.0, 90.0, "bullish", 1, "Technology"),
179
+ _make_ranked_industry("Software - Application", 18.0, 85.0, "bullish", 2, "Technology"),
180
+ _make_ranked_industry("IT Services", 15.0, 82.0, "bullish", 3, "Technology"),
181
+ _make_ranked_industry("Banks - Regional", 12.0, 75.0, "bullish", 4, "Financial Services"),
182
+ _make_ranked_industry("Oil & Gas E&P", 10.0, 70.0, "bullish", 5, "Energy"),
183
+ ]
184
+ themes = classify_themes(ranked, SAMPLE_THEMES_CONFIG)
185
+ vertical_names = [t["theme_name"] for t in themes if "Sector" in t["theme_name"]]
186
+ assert any("Technology" in n for n in vertical_names)
187
+
188
+ def test_vertical_theme_not_detected_with_two_same_sector(self):
189
+ """Only 2 same-sector industries -> no vertical theme."""
190
+ ranked = [
191
+ _make_ranked_industry("Semiconductor", 20.0, 90.0, "bullish", 1, "Technology"),
192
+ _make_ranked_industry("Software - Application", 18.0, 85.0, "bullish", 2, "Technology"),
193
+ _make_ranked_industry("Banks - Regional", 12.0, 75.0, "bullish", 3, "Financial Services"),
194
+ _make_ranked_industry("Oil & Gas E&P", 10.0, 70.0, "bullish", 4, "Energy"),
195
+ _make_ranked_industry("Gold", 8.0, 65.0, "bullish", 5, "Basic Materials"),
196
+ ]
197
+ themes = classify_themes(ranked, SAMPLE_THEMES_CONFIG)
198
+ vertical_names = [t["theme_name"] for t in themes if "Sector" in t["theme_name"]]
199
+ assert not any("Technology" in n for n in vertical_names)
200
+
201
+ def test_vertical_bearish_bottom_sector(self):
202
+ """3+ same-sector industries at bottom -> bearish vertical theme."""
203
+ ranked = [
204
+ _make_ranked_industry("Banks - Regional", 12.0, 75.0, "bullish", 1, "Financial Services"),
205
+ _make_ranked_industry("Oil & Gas E&P", 10.0, 70.0, "bullish", 2, "Energy"),
206
+ # Bottom 3 are all Technology bearish
207
+ _make_ranked_industry("IT Services", -10.0, 70.0, "bearish", 3, "Technology"),
208
+ _make_ranked_industry("Software - Application", -15.0, 82.0, "bearish", 4, "Technology"),
209
+ _make_ranked_industry("Semiconductor", -20.0, 90.0, "bearish", 5, "Technology"),
210
+ ]
211
+ themes = classify_themes(ranked, SAMPLE_THEMES_CONFIG)
212
+ vertical_themes = [t for t in themes if "Sector" in t["theme_name"]]
213
+ tech_vertical = [t for t in vertical_themes if "Technology" in t["theme_name"]]
214
+ assert len(tech_vertical) >= 1
215
+ assert tech_vertical[0]["direction"] == "bearish"
216
+
217
+ def test_no_sector_field_skips_vertical(self):
218
+ """Industries without sector field don't contribute to vertical themes."""
219
+ ranked = [
220
+ _make_ranked_industry("Semiconductor", 20.0, 90.0, "bullish", 1),
221
+ _make_ranked_industry("Software - Application", 18.0, 85.0, "bullish", 2),
222
+ _make_ranked_industry("IT Services", 15.0, 82.0, "bullish", 3),
223
+ ]
224
+ themes = classify_themes(ranked, SAMPLE_THEMES_CONFIG)
225
+ vertical_themes = [t for t in themes if "Sector" in t["theme_name"]]
226
+ assert len(vertical_themes) == 0
227
+
228
+
229
+ # ---------------------------------------------------------------------------
230
+ # get_theme_sector_weights
231
+ # ---------------------------------------------------------------------------
232
+
233
+
234
+ class TestGetThemeSectorWeights:
235
+ """Test sector weight calculation for a theme."""
236
+
237
+ def test_single_sector(self):
238
+ """All matching industries from one sector -> 100% weight."""
239
+ theme = {
240
+ "matching_industries": [
241
+ {"name": "Semiconductor", "sector": "Technology"},
242
+ {"name": "Software - Application", "sector": "Technology"},
243
+ ]
244
+ }
245
+ weights = get_theme_sector_weights(theme)
246
+ assert weights == {"Technology": 1.0}
247
+
248
+ def test_two_sectors_equal(self):
249
+ """Two sectors with equal industries -> 50/50."""
250
+ theme = {
251
+ "matching_industries": [
252
+ {"name": "Semiconductor", "sector": "Technology"},
253
+ {"name": "Solar", "sector": "Energy"},
254
+ ]
255
+ }
256
+ weights = get_theme_sector_weights(theme)
257
+ assert abs(weights["Technology"] - 0.5) < 0.01
258
+ assert abs(weights["Energy"] - 0.5) < 0.01
259
+
260
+ def test_three_sectors_uneven(self):
261
+ """3 industries: 2 Tech, 1 Energy -> Tech=0.67, Energy=0.33."""
262
+ theme = {
263
+ "matching_industries": [
264
+ {"name": "Semiconductor", "sector": "Technology"},
265
+ {"name": "Software - Application", "sector": "Technology"},
266
+ {"name": "Solar", "sector": "Energy"},
267
+ ]
268
+ }
269
+ weights = get_theme_sector_weights(theme)
270
+ assert abs(weights["Technology"] - 2 / 3) < 0.01
271
+ assert abs(weights["Energy"] - 1 / 3) < 0.01
272
+
273
+ def test_empty_matching_industries(self):
274
+ """No matching industries -> empty weights."""
275
+ theme = {"matching_industries": []}
276
+ weights = get_theme_sector_weights(theme)
277
+ assert weights == {}
278
+
279
+ def test_weights_sum_to_one(self):
280
+ """Sector weights always sum to 1.0."""
281
+ theme = {
282
+ "matching_industries": [
283
+ {"name": "A", "sector": "Tech"},
284
+ {"name": "B", "sector": "Energy"},
285
+ {"name": "C", "sector": "Tech"},
286
+ {"name": "D", "sector": "Healthcare"},
287
+ ]
288
+ }
289
+ weights = get_theme_sector_weights(theme)
290
+ total = sum(weights.values())
291
+ assert abs(total - 1.0) < 0.001
292
+
293
+ def test_missing_sector_field_skipped(self):
294
+ """Industries without sector field are excluded from weight calc."""
295
+ theme = {
296
+ "matching_industries": [
297
+ {"name": "A", "sector": "Tech"},
298
+ {"name": "B"}, # no sector
299
+ ]
300
+ }
301
+ weights = get_theme_sector_weights(theme)
302
+ assert weights == {"Tech": 1.0}
303
+
304
+
305
+ # ---------------------------------------------------------------------------
306
+ # Edge cases
307
+ # ---------------------------------------------------------------------------
308
+
309
+
310
+ class TestClassifyThemesEdgeCases:
311
+ """Edge cases for classify_themes."""
312
+
313
+ def test_empty_ranked_list(self):
314
+ """Empty ranked list -> no themes detected."""
315
+ themes = classify_themes([], SAMPLE_THEMES_CONFIG)
316
+ assert themes == []
317
+
318
+ def test_empty_themes_config(self):
319
+ """Empty config -> no cross-sector themes but vertical may still detect."""
320
+ ranked = [
321
+ _make_ranked_industry("Semiconductor", 20.0, 90.0, "bullish", 1, "Technology"),
322
+ _make_ranked_industry("Software - Application", 18.0, 85.0, "bullish", 2, "Technology"),
323
+ _make_ranked_industry("IT Services", 15.0, 82.0, "bullish", 3, "Technology"),
324
+ ]
325
+ config = {"cross_sector": [], "vertical_min_industries": 3, "cross_sector_min_matches": 2}
326
+ themes = classify_themes(ranked, config)
327
+ # Only vertical themes possible
328
+ theme_names = [t["theme_name"] for t in themes]
329
+ assert any("Technology" in n for n in theme_names)
330
+
331
+ def test_theme_result_has_sector_weights(self):
332
+ """Each theme result includes sector_weights dict."""
333
+ ranked = [
334
+ _make_ranked_industry("Semiconductor", 15.0, 82.0, "bullish", 1, "Technology"),
335
+ _make_ranked_industry("Software - Application", 12.0, 75.0, "bullish", 2, "Technology"),
336
+ ]
337
+ themes = classify_themes(ranked, SAMPLE_THEMES_CONFIG)
338
+ ai_theme = [t for t in themes if t["theme_name"] == "AI & Automation"][0]
339
+ assert "sector_weights" in ai_theme
340
+ assert isinstance(ai_theme["sector_weights"], dict)
341
+
342
+ def test_integration_with_rank_industries(self):
343
+ """Test full pipeline: raw industries -> ranked -> classified."""
344
+ raw_industries = [
345
+ {"name": "Semiconductor", "perf_1w": 5, "perf_1m": 10, "perf_3m": 20, "perf_6m": 25, "sector": "Technology"},
346
+ {"name": "Software - Application", "perf_1w": 4, "perf_1m": 8, "perf_3m": 15, "perf_6m": 20, "sector": "Technology"},
347
+ {"name": "Banks - Regional", "perf_1w": 2, "perf_1m": 5, "perf_3m": 8, "perf_6m": 10, "sector": "Financial Services"},
348
+ ]
349
+ ranked = rank_industries(raw_industries)
350
+ themes = classify_themes(ranked, SAMPLE_THEMES_CONFIG)
351
+ theme_names = [t["theme_name"] for t in themes]
352
+ assert "AI & Automation" in theme_names
353
+
354
+
355
+ # ---------------------------------------------------------------------------
356
+ # top_n filtering
357
+ # ---------------------------------------------------------------------------
358
+
359
+
360
+ class TestTopNFiltering:
361
+ """Test that classify_themes only considers top/bottom N industries."""
362
+
363
+ def test_middle_industries_excluded_with_small_top_n(self):
364
+ """With 100 industries and top_n=5, middle industries are excluded."""
365
+ # Build 100 industries: top 5 include AI keywords, bottom 5 include Green keywords
366
+ # Middle 90 include Infrastructure keywords that should NOT match
367
+ ranked = []
368
+
369
+ # Top 5 (highest momentum)
370
+ ranked.append(
371
+ _make_ranked_industry("Semiconductor", 20.0, 95.0, "bullish", 1, "Technology"))
372
+ ranked.append(
373
+ _make_ranked_industry("Software - Application", 18.0, 90.0, "bullish", 2, "Technology"))
374
+ ranked.append(
375
+ _make_ranked_industry("IT Services", 16.0, 88.0, "bullish", 3, "Technology"))
376
+ ranked.append(
377
+ _make_ranked_industry("Banks - Diversified", 15.0, 85.0, "bullish", 4, "Financial"))
378
+ ranked.append(
379
+ _make_ranked_industry("Gold", 14.0, 82.0, "bullish", 5, "Basic Materials"))
380
+
381
+ # Middle 90: include Building Materials and Steel (Infrastructure keywords)
382
+ for i in range(6, 96):
383
+ if i == 50:
384
+ ranked.append(
385
+ _make_ranked_industry("Building Materials", 0.0, 50.0, "bullish", i, "Industrials"))
386
+ elif i == 51:
387
+ ranked.append(
388
+ _make_ranked_industry("Steel", -0.1, 49.0, "bearish", i, "Basic Materials"))
389
+ elif i == 52:
390
+ ranked.append(
391
+ _make_ranked_industry("Engineering & Construction", 0.1, 50.5, "bullish", i, "Industrials"))
392
+ else:
393
+ ranked.append(
394
+ _make_ranked_industry(f"Industry_{i}", 10.0 - i * 0.2, 80.0 - i * 0.3, "bullish", i, "Other"))
395
+
396
+ # Bottom 5 (lowest momentum)
397
+ ranked.append(
398
+ _make_ranked_industry("Solar", -18.0, 10.0, "bearish", 96, "Energy"))
399
+ ranked.append(
400
+ _make_ranked_industry("Auto Manufacturers", -19.0, 8.0, "bearish", 97, "Consumer Cyclical"))
401
+ ranked.append(
402
+ _make_ranked_industry("Utilities - Renewable", -20.0, 5.0, "bearish", 98, "Utilities"))
403
+ ranked.append(
404
+ _make_ranked_industry("Retail_A", -21.0, 4.0, "bearish", 99, "Consumer Cyclical"))
405
+ ranked.append(
406
+ _make_ranked_industry("Retail_B", -22.0, 3.0, "bearish", 100, "Consumer Cyclical"))
407
+
408
+ themes = classify_themes(ranked, SAMPLE_THEMES_CONFIG, top_n=5)
409
+ theme_names = [t["theme_name"] for t in themes]
410
+
411
+ # AI keywords (Semiconductor, Software - App) are in top 5 -> AI detected
412
+ assert "AI & Automation" in theme_names
413
+
414
+ # Green keywords (Solar, Auto Manufacturers) are in bottom 5 -> Green detected
415
+ assert "Green Energy Transition" in theme_names
416
+
417
+ # Infrastructure keywords (Building Materials, Steel, E&C) are in middle -> NOT detected
418
+ assert "Infrastructure Boom" not in theme_names
419
+
420
+ def test_top_n_default_30(self):
421
+ """Default top_n=30 works for small datasets (< 30 items)."""
422
+ ranked = [
423
+ _make_ranked_industry("Semiconductor", 15.0, 82.0, "bullish", 1, "Technology"),
424
+ _make_ranked_industry("Software - Application", 12.0, 75.0, "bullish", 2, "Technology"),
425
+ ]
426
+ # Default top_n=30, with only 2 items, all should be considered
427
+ themes = classify_themes(ranked, SAMPLE_THEMES_CONFIG)
428
+ theme_names = [t["theme_name"] for t in themes]
429
+ assert "AI & Automation" in theme_names
430
+
431
+ def test_vertical_theme_only_in_top_bottom(self):
432
+ """Vertical themes only detected from top/bottom N, not middle."""
433
+ ranked = []
434
+ # Top 3: mixed sectors
435
+ ranked.append(
436
+ _make_ranked_industry("Gold", 20.0, 95.0, "bullish", 1, "Basic Materials"))
437
+ ranked.append(
438
+ _make_ranked_industry("Banks - Regional", 18.0, 90.0, "bullish", 2, "Financial"))
439
+ ranked.append(
440
+ _make_ranked_industry("Solar", 16.0, 88.0, "bullish", 3, "Energy"))
441
+
442
+ # Middle: 3 Technology industries (would trigger vertical if not filtered)
443
+ for i, name in enumerate(["Semiconductor", "Software - Application", "IT Services"], 4):
444
+ ranked.append(
445
+ _make_ranked_industry(name, 5.0, 50.0, "bullish", i, "Technology"))
446
+
447
+ # Bottom 3: mixed sectors
448
+ ranked.append(
449
+ _make_ranked_industry("Oil & Gas E&P", -15.0, 10.0, "bearish", 7, "Energy"))
450
+ ranked.append(
451
+ _make_ranked_industry("Retail_A", -18.0, 8.0, "bearish", 8, "Consumer Cyclical"))
452
+ ranked.append(
453
+ _make_ranked_industry("Retail_B", -20.0, 5.0, "bearish", 9, "Consumer Cyclical"))
454
+
455
+ themes = classify_themes(ranked, SAMPLE_THEMES_CONFIG, top_n=3)
456
+ vertical_names = [t["theme_name"] for t in themes if "Sector" in t["theme_name"]]
457
+ # Technology industries are in middle -> no Technology vertical theme
458
+ assert not any("Technology" in n for n in vertical_names)
459
+
460
+
461
+ # ---------------------------------------------------------------------------
462
+ # get_matched_industry_names
463
+ # ---------------------------------------------------------------------------
464
+
465
+
466
+ class TestGetMatchedIndustryNames:
467
+ """Test extraction of matched industry names from classified themes."""
468
+
469
+ def test_returns_matched_names(self):
470
+ themes = [
471
+ {
472
+ "theme_name": "AI",
473
+ "matching_industries": [
474
+ {"name": "Semiconductors"},
475
+ {"name": "Software - Application"},
476
+ ],
477
+ },
478
+ ]
479
+ names = get_matched_industry_names(themes)
480
+ assert names == {"Semiconductors", "Software - Application"}
481
+
482
+ def test_multiple_themes_combined(self):
483
+ themes = [
484
+ {"theme_name": "A", "matching_industries": [{"name": "X"}]},
485
+ {"theme_name": "B", "matching_industries": [{"name": "Y"}, {"name": "Z"}]},
486
+ ]
487
+ names = get_matched_industry_names(themes)
488
+ assert names == {"X", "Y", "Z"}
489
+
490
+ def test_empty_themes_returns_empty_set(self):
491
+ assert get_matched_industry_names([]) == set()
492
+
493
+ def test_no_matching_industries_key(self):
494
+ themes = [{"theme_name": "A"}]
495
+ assert get_matched_industry_names(themes) == set()
496
+
497
+ def test_deduplicates_across_themes(self):
498
+ themes = [
499
+ {"theme_name": "A", "matching_industries": [{"name": "X"}, {"name": "Y"}]},
500
+ {"theme_name": "B", "matching_industries": [{"name": "Y"}, {"name": "Z"}]},
501
+ ]
502
+ names = get_matched_industry_names(themes)
503
+ assert names == {"X", "Y", "Z"}
504
+
505
+
506
+ # ---------------------------------------------------------------------------
507
+ # theme_origin and name_confidence
508
+ # ---------------------------------------------------------------------------
509
+
510
+
511
+ class TestThemeOrigin:
512
+ """Test that theme_origin and name_confidence are set correctly."""
513
+
514
+ def test_cross_sector_has_seed_origin(self):
515
+ ranked = [
516
+ _make_ranked_industry("Semiconductor", 15.0, 82.0, "bullish", 1, "Technology"),
517
+ _make_ranked_industry("Software - Application", 12.0, 75.0, "bullish", 2, "Technology"),
518
+ ]
519
+ themes = classify_themes(ranked, SAMPLE_THEMES_CONFIG)
520
+ ai_theme = [t for t in themes if t["theme_name"] == "AI & Automation"][0]
521
+ assert ai_theme["theme_origin"] == "seed"
522
+ assert ai_theme["name_confidence"] == "high"
523
+
524
+ def test_vertical_has_vertical_origin(self):
525
+ ranked = [
526
+ _make_ranked_industry("Semiconductor", 20.0, 90.0, "bullish", 1, "Technology"),
527
+ _make_ranked_industry("Software - Application", 18.0, 85.0, "bullish", 2, "Technology"),
528
+ _make_ranked_industry("IT Services", 15.0, 82.0, "bullish", 3, "Technology"),
529
+ ]
530
+ themes = classify_themes(ranked, SAMPLE_THEMES_CONFIG)
531
+ vertical = [t for t in themes if "Sector" in t["theme_name"]]
532
+ assert len(vertical) >= 1
533
+ assert vertical[0]["theme_origin"] == "vertical"
534
+ assert vertical[0]["name_confidence"] == "high"