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,172 @@
1
+ """
2
+ Lifecycle Maturity Calculator (0-100)
3
+
4
+ Maturity = duration * 0.25
5
+ + extremity * 0.25
6
+ + price_extreme * 0.25
7
+ + valuation * 0.15
8
+ + etf_proliferation * 0.10
9
+
10
+ All sub-scores are direction-aware.
11
+ """
12
+
13
+ import statistics
14
+ from typing import List, Dict, Optional
15
+
16
+
17
+ LIFECYCLE_WEIGHTS = {
18
+ "duration": 0.25,
19
+ "extremity": 0.25,
20
+ "price_extreme": 0.25,
21
+ "valuation": 0.15,
22
+ "etf_proliferation": 0.10,
23
+ }
24
+
25
+
26
+ def estimate_duration_score(perf_1m: Optional[float],
27
+ perf_3m: Optional[float],
28
+ perf_6m: Optional[float],
29
+ perf_1y: Optional[float],
30
+ is_bearish: bool) -> float:
31
+ """Count horizons where trend is active. Each active = 25 points.
32
+
33
+ Bullish: perf > 2%
34
+ Bearish: perf < -2%
35
+ None values treated as inactive.
36
+ """
37
+ horizons = [perf_1m, perf_3m, perf_6m, perf_1y]
38
+ count = 0
39
+ for p in horizons:
40
+ if p is None:
41
+ continue
42
+ if is_bearish and p < -2.0:
43
+ count += 1
44
+ elif not is_bearish and p > 2.0:
45
+ count += 1
46
+ return float(count * 25)
47
+
48
+
49
+ def extremity_clustering_score(stock_metrics: List[Dict],
50
+ is_bearish: bool) -> float:
51
+ """Proportion of stocks at RSI extremes.
52
+
53
+ Bullish: count RSI > 70
54
+ Bearish: count RSI < 30
55
+ Formula: min(100, pct * 200)
56
+ Returns 50.0 if empty.
57
+ """
58
+ if not stock_metrics:
59
+ return 50.0
60
+
61
+ valid = [s for s in stock_metrics if s.get("rsi") is not None]
62
+ if not valid:
63
+ return 50.0
64
+
65
+ if is_bearish:
66
+ extreme_count = sum(1 for s in valid if s["rsi"] < 30)
67
+ else:
68
+ extreme_count = sum(1 for s in valid if s["rsi"] > 70)
69
+
70
+ pct = extreme_count / len(valid)
71
+ return min(100.0, pct * 200.0)
72
+
73
+
74
+ def price_extreme_saturation_score(stock_metrics: List[Dict],
75
+ is_bearish: bool) -> float:
76
+ """Proportion of stocks near 52-week extremes.
77
+
78
+ Bullish: dist_from_52w_high <= 0.05
79
+ Bearish: dist_from_52w_low <= 0.05
80
+ Formula: min(100, pct * 200)
81
+ Returns 50.0 if empty.
82
+ """
83
+ if not stock_metrics:
84
+ return 50.0
85
+
86
+ key = "dist_from_52w_low" if is_bearish else "dist_from_52w_high"
87
+ valid = [s for s in stock_metrics if s.get(key) is not None]
88
+ if not valid:
89
+ return 50.0
90
+
91
+ near_count = sum(1 for s in valid if s[key] <= 0.05)
92
+ pct = near_count / len(valid)
93
+ return min(100.0, pct * 200.0)
94
+
95
+
96
+ def valuation_premium_score(stock_metrics: List[Dict]) -> float:
97
+ """Score based on median P/E relative to market average (22).
98
+
99
+ premium_ratio = median_PE / 22.0
100
+ Score: min(100, max(0, (premium_ratio - 0.5) * 32))
101
+ Needs 3+ valid P/E values, else returns 50.0.
102
+ """
103
+ valid_pe = [s["pe_ratio"] for s in stock_metrics
104
+ if s.get("pe_ratio") is not None and s["pe_ratio"] > 0]
105
+
106
+ if len(valid_pe) < 3:
107
+ return 50.0
108
+
109
+ median_pe = statistics.median(valid_pe)
110
+ premium_ratio = median_pe / 22.0
111
+ return min(100.0, max(0.0, (premium_ratio - 0.5) * 32.0))
112
+
113
+
114
+ def etf_proliferation_score(etf_count: int) -> float:
115
+ """Score based on number of theme-related ETFs.
116
+
117
+ 0 => 0, 1 => 20, <=3 => 40, <=6 => 60, <=10 => 80, >10 => 100
118
+ """
119
+ if etf_count == 0:
120
+ return 0.0
121
+ elif etf_count == 1:
122
+ return 20.0
123
+ elif etf_count <= 3:
124
+ return 40.0
125
+ elif etf_count <= 6:
126
+ return 60.0
127
+ elif etf_count <= 10:
128
+ return 80.0
129
+ else:
130
+ return 100.0
131
+
132
+
133
+ def classify_stage(maturity: float) -> str:
134
+ """Classify lifecycle stage from maturity score.
135
+
136
+ 0-20: Emerging, 20-40: Accelerating, 40-60: Trending,
137
+ 60-80: Mature, 80-100: Exhausting
138
+ """
139
+ if maturity < 20:
140
+ return "Emerging"
141
+ elif maturity < 40:
142
+ return "Accelerating"
143
+ elif maturity < 60:
144
+ return "Trending"
145
+ elif maturity < 80:
146
+ return "Mature"
147
+ else:
148
+ return "Exhausting"
149
+
150
+
151
+ def calculate_lifecycle_maturity(duration: Optional[float],
152
+ extremity: Optional[float],
153
+ price_extreme: Optional[float],
154
+ valuation: Optional[float],
155
+ etf_prolif: Optional[float]) -> float:
156
+ """Weighted sum of lifecycle sub-scores, clamped 0-100.
157
+
158
+ Any None input defaults to 50.0.
159
+ """
160
+ d = duration if duration is not None else 50.0
161
+ e = extremity if extremity is not None else 50.0
162
+ p = price_extreme if price_extreme is not None else 50.0
163
+ v = valuation if valuation is not None else 50.0
164
+ et = etf_prolif if etf_prolif is not None else 50.0
165
+
166
+ raw = (d * LIFECYCLE_WEIGHTS["duration"]
167
+ + e * LIFECYCLE_WEIGHTS["extremity"]
168
+ + p * LIFECYCLE_WEIGHTS["price_extreme"]
169
+ + v * LIFECYCLE_WEIGHTS["valuation"]
170
+ + et * LIFECYCLE_WEIGHTS["etf_proliferation"])
171
+
172
+ return float(min(100.0, max(0.0, raw)))
@@ -0,0 +1,195 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Theme Classifier - Detect cross-sector and vertical themes from ranked industries.
4
+
5
+ Cross-sector themes match industries against keyword templates (min 2 matches).
6
+ Vertical themes detect sector concentration (min 3 same-sector industries in top/bottom).
7
+
8
+ themes_config format:
9
+ {
10
+ "cross_sector": [
11
+ {
12
+ "theme_name": str,
13
+ "matching_keywords": [str, ...],
14
+ "proxy_etfs": [str, ...],
15
+ "static_stocks": [str, ...],
16
+ },
17
+ ...
18
+ ],
19
+ "vertical_min_industries": int, # default 3
20
+ "cross_sector_min_matches": int, # default 2
21
+ }
22
+ """
23
+
24
+ from collections import Counter
25
+ from typing import Dict, List, Set
26
+
27
+
28
+ def classify_themes(
29
+ ranked_industries: List[Dict],
30
+ themes_config: Dict,
31
+ top_n: int = 30,
32
+ ) -> List[Dict]:
33
+ """
34
+ Match ranked industries to cross-sector and vertical themes.
35
+
36
+ Only the top N and bottom N industries (by momentum rank) are considered
37
+ for theme matching. This prevents themes from always matching when using
38
+ the full ~145 industry universe.
39
+
40
+ Args:
41
+ ranked_industries: Output of rank_industries (sorted by momentum_score desc).
42
+ themes_config: Theme definitions with cross_sector templates and thresholds.
43
+ top_n: Number of top/bottom industries to consider (default 30).
44
+
45
+ Returns:
46
+ List of theme result dicts, each with:
47
+ theme_name, direction, matching_industries, sector_weights,
48
+ proxy_etfs, static_stocks
49
+ """
50
+ if not ranked_industries:
51
+ return []
52
+
53
+ cross_sector_min = themes_config.get("cross_sector_min_matches", 2)
54
+ vertical_min = themes_config.get("vertical_min_industries", 3)
55
+ cross_sector_defs = themes_config.get("cross_sector", [])
56
+
57
+ # Build active set from top N + bottom N (deduplicated)
58
+ top = ranked_industries[:top_n]
59
+ bottom = ranked_industries[-top_n:] if len(ranked_industries) > top_n else []
60
+ active_set: Dict[str, Dict] = {ind["name"]: ind for ind in top}
61
+ for ind in bottom:
62
+ if ind["name"] not in active_set:
63
+ active_set[ind["name"]] = ind
64
+
65
+ themes = []
66
+
67
+ # 1. Cross-sector theme matching (active set only)
68
+ for theme_def in cross_sector_defs:
69
+ keywords = theme_def.get("matching_keywords", [])
70
+ matches = [kw for kw in keywords if kw in active_set]
71
+
72
+ if len(matches) >= cross_sector_min:
73
+ matching_inds = [active_set[m] for m in matches]
74
+ direction = _majority_direction(matching_inds)
75
+ sector_weights = get_theme_sector_weights(
76
+ {"matching_industries": matching_inds}
77
+ )
78
+
79
+ themes.append({
80
+ "theme_name": theme_def["theme_name"],
81
+ "direction": direction,
82
+ "matching_industries": matching_inds,
83
+ "sector_weights": sector_weights,
84
+ "proxy_etfs": theme_def.get("proxy_etfs", []),
85
+ "static_stocks": theme_def.get("static_stocks", []),
86
+ "theme_origin": "seed",
87
+ "name_confidence": "high",
88
+ })
89
+
90
+ # 2. Vertical (single-sector) theme detection
91
+ # Count industries per sector in top N and bottom N separately
92
+ top_set = set(ind["name"] for ind in top)
93
+
94
+ # Top N sector groups
95
+ top_sector_groups: Dict[str, List[Dict]] = {}
96
+ for ind in top:
97
+ sector = ind.get("sector")
98
+ if sector is None:
99
+ continue
100
+ top_sector_groups.setdefault(sector, []).append(ind)
101
+
102
+ for sector, inds in top_sector_groups.items():
103
+ if len(inds) >= vertical_min:
104
+ direction = _majority_direction(inds)
105
+ sector_weights = get_theme_sector_weights(
106
+ {"matching_industries": inds}
107
+ )
108
+ themes.append({
109
+ "theme_name": f"{sector} Sector Concentration",
110
+ "direction": direction,
111
+ "matching_industries": inds,
112
+ "sector_weights": sector_weights,
113
+ "proxy_etfs": [],
114
+ "static_stocks": [],
115
+ "theme_origin": "vertical",
116
+ "name_confidence": "high",
117
+ })
118
+
119
+ # Bottom N sector groups (excluding industries already in top N)
120
+ bottom_sector_groups: Dict[str, List[Dict]] = {}
121
+ for ind in bottom:
122
+ if ind["name"] in top_set:
123
+ continue
124
+ sector = ind.get("sector")
125
+ if sector is None:
126
+ continue
127
+ bottom_sector_groups.setdefault(sector, []).append(ind)
128
+
129
+ for sector, inds in bottom_sector_groups.items():
130
+ if len(inds) >= vertical_min:
131
+ direction = _majority_direction(inds)
132
+ sector_weights = get_theme_sector_weights(
133
+ {"matching_industries": inds}
134
+ )
135
+ themes.append({
136
+ "theme_name": f"{sector} Sector Concentration",
137
+ "direction": direction,
138
+ "matching_industries": inds,
139
+ "sector_weights": sector_weights,
140
+ "proxy_etfs": [],
141
+ "static_stocks": [],
142
+ "theme_origin": "vertical",
143
+ "name_confidence": "high",
144
+ })
145
+
146
+ return themes
147
+
148
+
149
+ def get_matched_industry_names(themes: List[Dict]) -> Set[str]:
150
+ """Return the set of all matched industry names across classified themes.
151
+
152
+ Args:
153
+ themes: Output of classify_themes().
154
+
155
+ Returns:
156
+ Set of industry name strings.
157
+ """
158
+ names: Set[str] = set()
159
+ for theme in themes:
160
+ for ind in theme.get("matching_industries", []):
161
+ name = ind.get("name", "")
162
+ if name:
163
+ names.add(name)
164
+ return names
165
+
166
+
167
+ def get_theme_sector_weights(theme: Dict) -> Dict[str, float]:
168
+ """
169
+ Calculate sector weight distribution for a theme's matching industries.
170
+
171
+ Args:
172
+ theme: Dict with "matching_industries" key containing industry dicts
173
+ (each may have a "sector" field).
174
+
175
+ Returns:
176
+ Dict mapping sector name to its proportion (0.0-1.0).
177
+ Industries without a sector field are excluded.
178
+ """
179
+ matching = theme.get("matching_industries", [])
180
+ sectors = [ind["sector"] for ind in matching if "sector" in ind]
181
+
182
+ if not sectors:
183
+ return {}
184
+
185
+ counts = Counter(sectors)
186
+ total = sum(counts.values())
187
+
188
+ return {sector: count / total for sector, count in counts.items()}
189
+
190
+
191
+ def _majority_direction(industries: List[Dict]) -> str:
192
+ """Determine majority direction from a list of industries."""
193
+ bullish = sum(1 for ind in industries if ind.get("direction") == "bullish")
194
+ bearish = len(industries) - bullish
195
+ return "bullish" if bullish > bearish else "bearish"
@@ -0,0 +1,280 @@
1
+ """Theme Discoverer - Automatic theme detection from unmatched industries.
2
+
3
+ Finds clusters of unmatched industries with similar performance patterns
4
+ and generates new theme entries compatible with classify_themes() output.
5
+
6
+ Algorithm:
7
+ 1. Extract top_n and bottom_n from ranked_industries, exclude matched names.
8
+ 2. Separate into bullish (top) and bearish (bottom) groups.
9
+ 3. Sort each group by weighted_return.
10
+ 4. Cluster adjacent industries that satisfy BOTH:
11
+ a. weighted_return gap <= gap_threshold
12
+ b. perf vector distance <= vector_threshold (normalized Euclidean)
13
+ 5. Filter clusters by min_cluster_size.
14
+ 6. Exclude clusters that overlap significantly with existing themes.
15
+ 7. Auto-name clusters from industry name tokens.
16
+ """
17
+
18
+ import math
19
+ from collections import Counter
20
+ from typing import Dict, List, Set, Tuple
21
+
22
+ from calculators.theme_classifier import get_theme_sector_weights
23
+
24
+ # ---------------------------------------------------------------------------
25
+ # Constants
26
+ # ---------------------------------------------------------------------------
27
+
28
+ _GAP_THRESHOLD_PCT = 3.0
29
+ _VECTOR_THRESHOLD = 0.5
30
+ _MIN_CLUSTER_SIZE = 2
31
+ _OVERLAP_THRESHOLD = 0.5
32
+
33
+ _STOP_WORDS = {
34
+ "Services", "Products", "Equipment", "Materials", "General",
35
+ "Other", "Specialty", "Diversified", "Regulated", "Independent",
36
+ "&", "-", "and",
37
+ }
38
+
39
+
40
+ # ---------------------------------------------------------------------------
41
+ # Public API
42
+ # ---------------------------------------------------------------------------
43
+
44
+ def discover_themes(
45
+ ranked_industries: List[Dict],
46
+ matched_names: Set[str],
47
+ existing_themes: List[Dict],
48
+ top_n: int = 30,
49
+ gap_threshold: float = _GAP_THRESHOLD_PCT,
50
+ vector_threshold: float = _VECTOR_THRESHOLD,
51
+ min_cluster_size: int = _MIN_CLUSTER_SIZE,
52
+ ) -> List[Dict]:
53
+ """Discover new themes from unmatched industries.
54
+
55
+ Args:
56
+ ranked_industries: Full ranked list (sorted by momentum_score desc).
57
+ matched_names: Set of industry names already matched by seed/vertical.
58
+ existing_themes: List of existing theme dicts (for overlap detection).
59
+ top_n: Number of top/bottom industries to consider.
60
+ gap_threshold: Max weighted_return gap between adjacent industries.
61
+ vector_threshold: Max normalized Euclidean distance for perf vectors.
62
+ min_cluster_size: Minimum industries to form a cluster.
63
+
64
+ Returns:
65
+ List of theme dicts compatible with classify_themes() output,
66
+ each with theme_origin="discovered" and name_confidence="medium".
67
+ """
68
+ bullish, bearish = _get_unmatched_industries(ranked_industries, matched_names, top_n)
69
+
70
+ discovered = []
71
+ for group, direction in [(bullish, "bullish"), (bearish, "bearish")]:
72
+ clusters = _cluster_by_proximity(group, gap_threshold, vector_threshold)
73
+ for cluster in clusters:
74
+ if len(cluster) < min_cluster_size:
75
+ continue
76
+ if _is_duplicate_of_existing(cluster, direction, existing_themes):
77
+ continue
78
+ name = _auto_name_cluster(cluster)
79
+ theme = _build_theme_dict(name, cluster)
80
+ discovered.append(theme)
81
+
82
+ return discovered
83
+
84
+
85
+ # ---------------------------------------------------------------------------
86
+ # Internal helpers
87
+ # ---------------------------------------------------------------------------
88
+
89
+ def _get_unmatched_industries(
90
+ ranked: List[Dict],
91
+ matched_names: Set[str],
92
+ top_n: int,
93
+ ) -> Tuple[List[Dict], List[Dict]]:
94
+ """Extract unmatched industries from top N and bottom N.
95
+
96
+ Returns:
97
+ (bullish_unmatched, bearish_unmatched)
98
+ """
99
+ top = ranked[:top_n]
100
+ bottom = ranked[-top_n:] if len(ranked) > top_n else []
101
+
102
+ # Deduplicate: industries in both top and bottom only appear in top
103
+ top_names = {ind["name"] for ind in top}
104
+
105
+ bullish = [
106
+ ind for ind in top
107
+ if ind["name"] not in matched_names and ind.get("direction") == "bullish"
108
+ ]
109
+ bearish = [
110
+ ind for ind in bottom
111
+ if ind["name"] not in matched_names
112
+ and ind["name"] not in top_names
113
+ and ind.get("direction") == "bearish"
114
+ ]
115
+
116
+ return bullish, bearish
117
+
118
+
119
+ def _cluster_by_proximity(
120
+ industries: List[Dict],
121
+ gap_threshold: float,
122
+ vector_threshold: float,
123
+ ) -> List[List[Dict]]:
124
+ """Cluster industries by weighted_return proximity and perf vector distance.
125
+
126
+ Industries are sorted by weighted_return. Adjacent pairs are joined into
127
+ the same cluster if BOTH conditions are met:
128
+ 1. abs(weighted_return difference) <= gap_threshold
129
+ 2. perf_vector_distance <= vector_threshold
130
+
131
+ Returns:
132
+ List of clusters (each cluster is a list of industry dicts).
133
+ Only clusters with >= 2 members are returned.
134
+ """
135
+ if not industries:
136
+ return []
137
+
138
+ sorted_inds = sorted(industries, key=lambda x: x.get("weighted_return", 0), reverse=True)
139
+ ranges = _compute_ranges(sorted_inds)
140
+
141
+ clusters: List[List[Dict]] = [[sorted_inds[0]]]
142
+
143
+ for i in range(1, len(sorted_inds)):
144
+ prev = sorted_inds[i - 1]
145
+ curr = sorted_inds[i]
146
+
147
+ gap = abs(prev.get("weighted_return", 0) - curr.get("weighted_return", 0))
148
+ vec_dist = _perf_vector_distance(prev, curr, ranges)
149
+
150
+ if gap <= gap_threshold and vec_dist <= vector_threshold:
151
+ clusters[-1].append(curr)
152
+ else:
153
+ clusters.append([curr])
154
+
155
+ # Filter to min size 2
156
+ return [c for c in clusters if len(c) >= 2]
157
+
158
+
159
+ def _perf_vector_distance(a: Dict, b: Dict, ranges: Dict) -> float:
160
+ """Normalized Euclidean distance between perf vectors (1W, 1M, 3M).
161
+
162
+ Each timeframe difference is normalized by the range (max - min) across
163
+ all industries in the group. If range is 0, that dimension is ignored.
164
+
165
+ Returns:
166
+ 0.0 (identical) to ~1.7 (maximally different across all 3 dimensions).
167
+ """
168
+ keys = ["perf_1w", "perf_1m", "perf_3m"]
169
+ total = 0.0
170
+
171
+ for key in keys:
172
+ r = ranges.get(key, 0)
173
+ if r == 0:
174
+ continue
175
+ diff = (a.get(key, 0) - b.get(key, 0)) / r
176
+ total += diff * diff
177
+
178
+ return math.sqrt(total)
179
+
180
+
181
+ def _compute_ranges(industries: List[Dict]) -> Dict[str, float]:
182
+ """Compute value ranges for perf normalization."""
183
+ keys = ["perf_1w", "perf_1m", "perf_3m"]
184
+ ranges = {}
185
+ for key in keys:
186
+ vals = [ind.get(key, 0) for ind in industries]
187
+ if vals:
188
+ ranges[key] = max(vals) - min(vals)
189
+ else:
190
+ ranges[key] = 0
191
+ return ranges
192
+
193
+
194
+ def _auto_name_cluster(industries: List[Dict]) -> str:
195
+ """Generate a descriptive name for a cluster from industry name tokens.
196
+
197
+ Tokenizes all industry names, removes stop words, picks the top 2
198
+ most frequent tokens and joins them.
199
+ """
200
+ tokens: List[str] = []
201
+ for ind in industries:
202
+ name = ind.get("name", "")
203
+ for token in name.split():
204
+ cleaned = token.strip("(),")
205
+ if cleaned and cleaned not in _STOP_WORDS:
206
+ tokens.append(cleaned)
207
+
208
+ if not tokens:
209
+ return "Unknown Cluster"
210
+
211
+ counts = Counter(tokens)
212
+ top = counts.most_common(3)
213
+
214
+ if len(top) >= 2:
215
+ return f"{top[0][0]} & {top[1][0]} Related"
216
+ elif len(top) == 1:
217
+ return f"{top[0][0]} Related"
218
+ return "Unknown Cluster"
219
+
220
+
221
+ def _is_duplicate_of_existing(
222
+ cluster_industries: List[Dict],
223
+ cluster_direction: str,
224
+ existing_themes: List[Dict],
225
+ overlap_threshold: float = _OVERLAP_THRESHOLD,
226
+ ) -> bool:
227
+ """Check if a discovered cluster duplicates an existing theme.
228
+
229
+ A cluster is considered duplicate if:
230
+ 1. Direction matches an existing theme, AND
231
+ 2. Jaccard similarity >= threshold OR overlap coefficient >= threshold.
232
+
233
+ The overlap coefficient (intersection / min(|A|, |B|)) catches cases
234
+ where a small cluster is a subset of a large existing theme, which
235
+ Jaccard alone would miss because the union denominator dilutes the ratio.
236
+ """
237
+ cluster_names = {ind.get("name", "") for ind in cluster_industries}
238
+ if not cluster_names:
239
+ return False
240
+
241
+ for theme in existing_themes:
242
+ if theme.get("direction") != cluster_direction:
243
+ continue
244
+ theme_names = {ind.get("name", "") for ind in theme.get("matching_industries", [])}
245
+ if not theme_names:
246
+ continue
247
+ intersection = cluster_names & theme_names
248
+ union = cluster_names | theme_names
249
+ jaccard = len(intersection) / len(union) if union else 0
250
+ min_size = min(len(cluster_names), len(theme_names))
251
+ overlap_coeff = len(intersection) / min_size if min_size else 0
252
+ if jaccard >= overlap_threshold or overlap_coeff >= overlap_threshold:
253
+ return True
254
+
255
+ return False
256
+
257
+
258
+ def _build_theme_dict(name: str, industries: List[Dict]) -> Dict:
259
+ """Build a theme dict compatible with classify_themes() output.
260
+
261
+ Discovered themes have proxy_etfs=[], static_stocks=[],
262
+ theme_origin="discovered", name_confidence="medium".
263
+ """
264
+ # Determine direction from majority
265
+ bullish = sum(1 for ind in industries if ind.get("direction") == "bullish")
266
+ bearish = len(industries) - bullish
267
+ direction = "bullish" if bullish > bearish else "bearish"
268
+
269
+ sector_weights = get_theme_sector_weights({"matching_industries": industries})
270
+
271
+ return {
272
+ "theme_name": name,
273
+ "direction": direction,
274
+ "matching_industries": industries,
275
+ "sector_weights": sector_weights,
276
+ "proxy_etfs": [],
277
+ "static_stocks": [],
278
+ "theme_origin": "discovered",
279
+ "name_confidence": "medium",
280
+ }