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,458 @@
1
+ """Tests for theme_discoverer module.
2
+
3
+ Covers unmatched industry extraction, proximity clustering with perf vectors,
4
+ auto-naming, duplicate detection, and full discover_themes flow.
5
+ """
6
+
7
+ import pytest
8
+
9
+ from calculators.theme_discoverer import (
10
+ discover_themes,
11
+ _get_unmatched_industries,
12
+ _cluster_by_proximity,
13
+ _perf_vector_distance,
14
+ _compute_ranges,
15
+ _auto_name_cluster,
16
+ _is_duplicate_of_existing,
17
+ _build_theme_dict,
18
+ )
19
+
20
+
21
+ # ---------------------------------------------------------------------------
22
+ # Helpers
23
+ # ---------------------------------------------------------------------------
24
+
25
+ def _ind(name, weighted_return, direction="bullish", sector="Tech",
26
+ perf_1w=0.0, perf_1m=0.0, perf_3m=0.0):
27
+ return {
28
+ "name": name,
29
+ "weighted_return": weighted_return,
30
+ "direction": direction,
31
+ "sector": sector,
32
+ "perf_1w": perf_1w,
33
+ "perf_1m": perf_1m,
34
+ "perf_3m": perf_3m,
35
+ }
36
+
37
+
38
+ # ---------------------------------------------------------------------------
39
+ # TestGetUnmatchedIndustries
40
+ # ---------------------------------------------------------------------------
41
+
42
+
43
+ class TestGetUnmatchedIndustries:
44
+
45
+ def test_excludes_matched_names(self):
46
+ ranked = [
47
+ _ind("A", 10.0, "bullish"), _ind("B", 8.0, "bullish"), _ind("C", 6.0, "bullish"),
48
+ _ind("D", -5.0, "bearish"), _ind("E", -8.0, "bearish"), _ind("F", -10.0, "bearish"),
49
+ ]
50
+ matched = {"A", "E"}
51
+ bull, bear = _get_unmatched_industries(ranked, matched, top_n=3)
52
+ bull_names = {i["name"] for i in bull}
53
+ bear_names = {i["name"] for i in bear}
54
+ assert "A" not in bull_names
55
+ assert "E" not in bear_names
56
+ assert "B" in bull_names
57
+ assert "F" in bear_names
58
+
59
+ def test_separates_bullish_and_bearish(self):
60
+ ranked = [
61
+ _ind("A", 10.0, "bullish"), _ind("B", 8.0, "bullish"),
62
+ _ind("C", -5.0, "bearish"), _ind("D", -10.0, "bearish"),
63
+ ]
64
+ bull, bear = _get_unmatched_industries(ranked, set(), top_n=2)
65
+ assert all(i["direction"] == "bullish" for i in bull)
66
+ assert all(i["direction"] == "bearish" for i in bear)
67
+
68
+ def test_respects_top_n(self):
69
+ ranked = [_ind(f"I{i}", 20.0 - i) for i in range(20)]
70
+ bull, bear = _get_unmatched_industries(ranked, set(), top_n=5)
71
+ # Top 5 + bottom 5 from 20 items
72
+ assert len(bull) + len(bear) <= 10
73
+
74
+ def test_empty_matched_returns_all(self):
75
+ ranked = [_ind("A", 10.0), _ind("B", -10.0)]
76
+ bull, bear = _get_unmatched_industries(ranked, set(), top_n=30)
77
+ assert len(bull) + len(bear) == 2
78
+
79
+
80
+ # ---------------------------------------------------------------------------
81
+ # TestClusterByProximity
82
+ # ---------------------------------------------------------------------------
83
+
84
+
85
+ class TestClusterByProximity:
86
+
87
+ def test_adjacent_within_both_thresholds_grouped(self):
88
+ # Use 3+ items so range is wider than pairwise diff
89
+ industries = [
90
+ _ind("A", 10.0, perf_1w=5.0, perf_1m=10.0, perf_3m=15.0),
91
+ _ind("B", 11.5, perf_1w=6.0, perf_1m=11.0, perf_3m=16.0),
92
+ _ind("C", 13.0, perf_1w=15.0, perf_1m=20.0, perf_3m=25.0),
93
+ ]
94
+ # A-B gap=1.5 within 3.0, C far in perf space; A&B should cluster
95
+ clusters = _cluster_by_proximity(industries, gap_threshold=3.0,
96
+ vector_threshold=0.5)
97
+ assert len(clusters) >= 1
98
+ cluster_names = [set(i["name"] for i in c) for c in clusters]
99
+ assert {"A", "B"} in cluster_names or any("A" in c and "B" in c for c in cluster_names)
100
+
101
+ def test_gap_exceeds_splits(self):
102
+ industries = [
103
+ _ind("A", 10.0, perf_1w=5.0, perf_1m=10.0, perf_3m=15.0),
104
+ _ind("B", 20.0, perf_1w=6.0, perf_1m=11.0, perf_3m=16.0),
105
+ ]
106
+ clusters = _cluster_by_proximity(industries, gap_threshold=3.0,
107
+ vector_threshold=0.5)
108
+ # Gap is 10.0 > 3.0, so separate clusters; each < min_size so empty
109
+ assert len(clusters) == 0
110
+
111
+ def test_vector_distance_exceeds_splits(self):
112
+ """Different perf patterns should prevent clustering."""
113
+ industries = [
114
+ _ind("A", 10.0, perf_1w=20.0, perf_1m=5.0, perf_3m=1.0),
115
+ _ind("B", 11.0, perf_1w=1.0, perf_1m=5.0, perf_3m=20.0),
116
+ ]
117
+ clusters = _cluster_by_proximity(industries, gap_threshold=3.0,
118
+ vector_threshold=0.3)
119
+ # Vector distance should be high despite close weighted_return
120
+ assert len(clusters) == 0
121
+
122
+ def test_single_industry_not_a_cluster(self):
123
+ industries = [_ind("A", 10.0)]
124
+ clusters = _cluster_by_proximity(industries, gap_threshold=3.0,
125
+ vector_threshold=0.5)
126
+ assert len(clusters) == 0
127
+
128
+ def test_all_within_thresholds_single_cluster(self):
129
+ # Add a distant outlier to widen the range, so close items normalize well
130
+ industries = [
131
+ _ind("A", 10.0, perf_1w=5.0, perf_1m=10.0, perf_3m=15.0),
132
+ _ind("B", 11.0, perf_1w=5.5, perf_1m=10.5, perf_3m=15.5),
133
+ _ind("C", 12.0, perf_1w=6.0, perf_1m=11.0, perf_3m=16.0),
134
+ _ind("Far", 30.0, perf_1w=50.0, perf_1m=60.0, perf_3m=70.0),
135
+ ]
136
+ clusters = _cluster_by_proximity(industries, gap_threshold=3.0,
137
+ vector_threshold=0.5)
138
+ # A, B, C should form a single cluster (close in both gap and perf)
139
+ abc_cluster = [c for c in clusters if len(c) >= 3]
140
+ assert len(abc_cluster) >= 1
141
+ names = {i["name"] for i in abc_cluster[0]}
142
+ assert {"A", "B", "C"}.issubset(names)
143
+
144
+ def test_empty_input_returns_empty(self):
145
+ clusters = _cluster_by_proximity([], gap_threshold=3.0,
146
+ vector_threshold=0.5)
147
+ assert clusters == []
148
+
149
+
150
+ # ---------------------------------------------------------------------------
151
+ # TestPerfVectorDistance
152
+ # ---------------------------------------------------------------------------
153
+
154
+
155
+ class TestPerfVectorDistance:
156
+
157
+ def test_identical_returns_zero(self):
158
+ a = _ind("A", 10.0, perf_1w=5.0, perf_1m=10.0, perf_3m=15.0)
159
+ b = _ind("B", 10.0, perf_1w=5.0, perf_1m=10.0, perf_3m=15.0)
160
+ ranges = {"perf_1w": 20.0, "perf_1m": 20.0, "perf_3m": 20.0}
161
+ assert _perf_vector_distance(a, b, ranges) == 0.0
162
+
163
+ def test_different_patterns_high_distance(self):
164
+ a = _ind("A", 10.0, perf_1w=20.0, perf_1m=0.0, perf_3m=0.0)
165
+ b = _ind("B", 10.0, perf_1w=0.0, perf_1m=0.0, perf_3m=20.0)
166
+ ranges = {"perf_1w": 20.0, "perf_1m": 20.0, "perf_3m": 20.0}
167
+ dist = _perf_vector_distance(a, b, ranges)
168
+ assert dist > 1.0 # sqrt(1^2 + 0 + 1^2) = sqrt(2) ~= 1.414
169
+
170
+
171
+ # ---------------------------------------------------------------------------
172
+ # TestAutoNameCluster
173
+ # ---------------------------------------------------------------------------
174
+
175
+
176
+ class TestAutoNameCluster:
177
+
178
+ def test_top_two_tokens(self):
179
+ industries = [
180
+ _ind("Oil & Gas E&P", 10.0),
181
+ _ind("Oil & Gas Integrated", 9.0),
182
+ _ind("Oil & Gas Midstream", 8.0),
183
+ ]
184
+ name = _auto_name_cluster(industries)
185
+ assert "Oil" in name
186
+ assert "Gas" in name
187
+
188
+ def test_stop_words_excluded(self):
189
+ industries = [
190
+ _ind("General Services", 10.0),
191
+ _ind("Specialty Services", 9.0),
192
+ ]
193
+ name = _auto_name_cluster(industries)
194
+ # "General", "Specialty", "Services" are stop words
195
+ # Should still produce a name
196
+ assert len(name) > 0
197
+
198
+ def test_single_token_adds_related(self):
199
+ industries = [
200
+ _ind("Gold", 10.0),
201
+ _ind("Silver", 9.0),
202
+ ]
203
+ name = _auto_name_cluster(industries)
204
+ assert len(name) > 0
205
+
206
+ def test_oil_gas_example(self):
207
+ industries = [
208
+ _ind("Oil & Gas E&P", 10.0),
209
+ _ind("Oil & Gas Drilling", 9.0),
210
+ ]
211
+ name = _auto_name_cluster(industries)
212
+ assert "Oil" in name or "Gas" in name
213
+
214
+ def test_metals_mining_example(self):
215
+ industries = [
216
+ _ind("Other Industrial Metals & Mining", 10.0),
217
+ _ind("Steel", 9.0),
218
+ _ind("Copper", 8.0),
219
+ ]
220
+ name = _auto_name_cluster(industries)
221
+ assert len(name) > 0
222
+
223
+
224
+ # ---------------------------------------------------------------------------
225
+ # TestDiscoverThemes
226
+ # ---------------------------------------------------------------------------
227
+
228
+
229
+ class TestDiscoverThemes:
230
+
231
+ def test_discovers_cross_sector_cluster(self):
232
+ ranked = [
233
+ _ind("UnmatchedA", 15.0, "bullish", "Tech",
234
+ perf_1w=5.0, perf_1m=10.0, perf_3m=15.0),
235
+ _ind("UnmatchedB", 14.0, "bullish", "Industrials",
236
+ perf_1w=5.0, perf_1m=10.0, perf_3m=15.0),
237
+ _ind("MatchedC", 13.0, "bullish"),
238
+ _ind("Bottom1", -10.0, "bearish"),
239
+ _ind("Bottom2", -11.0, "bearish"),
240
+ ]
241
+ matched = {"MatchedC"}
242
+ existing = []
243
+ discovered = discover_themes(ranked, matched, existing, top_n=5)
244
+ assert len(discovered) >= 1
245
+
246
+ def test_single_sector_with_no_vertical_overlap_kept(self):
247
+ """Single-sector cluster not overlapping vertical should be kept."""
248
+ ranked = [
249
+ _ind("Drug Manufacturers - General", 15.0, "bullish", "Healthcare",
250
+ perf_1w=5.0, perf_1m=10.0, perf_3m=15.0),
251
+ _ind("Drug Manufacturers - Specialty & Generic", 14.0, "bullish", "Healthcare",
252
+ perf_1w=5.0, perf_1m=10.0, perf_3m=15.0),
253
+ ] + [_ind(f"Filler{i}", -10.0 - i, "bearish") for i in range(8)]
254
+ matched = set()
255
+ existing_vertical = [{
256
+ "theme_name": "Healthcare Sector Concentration",
257
+ "direction": "bullish",
258
+ "matching_industries": [
259
+ {"name": "Medical Devices"},
260
+ {"name": "Healthcare Plans"},
261
+ {"name": "Medical Care Facilities"},
262
+ ],
263
+ }]
264
+ discovered = discover_themes(ranked, matched, existing_vertical, top_n=5)
265
+ # GLP-1 like cluster (Drug Manufacturers) should not overlap Healthcare Vertical
266
+ if discovered:
267
+ names = {i["name"] for d in discovered for i in d.get("matching_industries", [])}
268
+ assert "Drug Manufacturers - General" in names or len(discovered) == 0
269
+
270
+ def test_single_sector_with_vertical_overlap_excluded(self):
271
+ """Single-sector cluster overlapping existing vertical should be excluded."""
272
+ ranked = [
273
+ _ind("Medical Devices", 15.0, "bullish", "Healthcare",
274
+ perf_1w=5.0, perf_1m=10.0, perf_3m=15.0),
275
+ _ind("Healthcare Plans", 14.0, "bullish", "Healthcare",
276
+ perf_1w=5.0, perf_1m=10.0, perf_3m=15.0),
277
+ ] + [_ind(f"Filler{i}", -10.0 - i, "bearish") for i in range(8)]
278
+ matched = set()
279
+ existing_vertical = [{
280
+ "theme_name": "Healthcare Sector Concentration",
281
+ "direction": "bullish",
282
+ "matching_industries": [
283
+ {"name": "Medical Devices"},
284
+ {"name": "Healthcare Plans"},
285
+ {"name": "Medical Care Facilities"},
286
+ ],
287
+ }]
288
+ discovered = discover_themes(ranked, matched, existing_vertical, top_n=5)
289
+ # Should be excluded due to high overlap with Healthcare Vertical
290
+ for d in discovered:
291
+ d_names = {i["name"] for i in d.get("matching_industries", [])}
292
+ overlap = d_names & {"Medical Devices", "Healthcare Plans", "Medical Care Facilities"}
293
+ # If cluster is detected, it shouldn't overlap significantly
294
+ if d.get("direction") == "bullish":
295
+ assert len(overlap) / max(len(d_names), 1) < 0.5
296
+
297
+ def test_small_cluster_rejected(self):
298
+ """Cluster with only 1 industry should be rejected."""
299
+ ranked = [
300
+ _ind("LoneIndustry", 15.0, "bullish", "Tech",
301
+ perf_1w=5.0, perf_1m=10.0, perf_3m=15.0),
302
+ _ind("FarAway", 50.0, "bullish", "Other",
303
+ perf_1w=30.0, perf_1m=40.0, perf_3m=50.0),
304
+ ] + [_ind(f"Bottom{i}", -10.0 - i, "bearish") for i in range(8)]
305
+ discovered = discover_themes(ranked, set(), [], top_n=5)
306
+ # Single-industry clusters don't form (min_cluster_size=2)
307
+ for d in discovered:
308
+ assert len(d.get("matching_industries", [])) >= 2
309
+
310
+ def test_sets_theme_origin_discovered(self):
311
+ ranked = [
312
+ _ind("A", 15.0, "bullish", "Tech",
313
+ perf_1w=5.0, perf_1m=10.0, perf_3m=15.0),
314
+ _ind("B", 14.0, "bullish", "Tech",
315
+ perf_1w=5.0, perf_1m=10.0, perf_3m=15.0),
316
+ ] + [_ind(f"Bottom{i}", -10.0 - i, "bearish") for i in range(8)]
317
+ discovered = discover_themes(ranked, set(), [], top_n=5)
318
+ for d in discovered:
319
+ assert d["theme_origin"] == "discovered"
320
+
321
+ def test_sets_proxy_etfs_empty(self):
322
+ ranked = [
323
+ _ind("A", 15.0, "bullish", "Tech",
324
+ perf_1w=5.0, perf_1m=10.0, perf_3m=15.0),
325
+ _ind("B", 14.0, "bullish", "Tech",
326
+ perf_1w=5.0, perf_1m=10.0, perf_3m=15.0),
327
+ ] + [_ind(f"Bottom{i}", -10.0 - i, "bearish") for i in range(8)]
328
+ discovered = discover_themes(ranked, set(), [], top_n=5)
329
+ for d in discovered:
330
+ assert d["proxy_etfs"] == []
331
+
332
+ def test_name_confidence_is_medium(self):
333
+ ranked = [
334
+ _ind("A", 15.0, "bullish", "Tech",
335
+ perf_1w=5.0, perf_1m=10.0, perf_3m=15.0),
336
+ _ind("B", 14.0, "bullish", "Tech",
337
+ perf_1w=5.0, perf_1m=10.0, perf_3m=15.0),
338
+ ] + [_ind(f"Bottom{i}", -10.0 - i, "bearish") for i in range(8)]
339
+ discovered = discover_themes(ranked, set(), [], top_n=5)
340
+ for d in discovered:
341
+ assert d["name_confidence"] == "medium"
342
+
343
+ def test_no_unmatched_returns_empty(self):
344
+ ranked = [_ind("A", 10.0), _ind("B", -10.0)]
345
+ matched = {"A", "B"}
346
+ discovered = discover_themes(ranked, matched, [], top_n=30)
347
+ assert discovered == []
348
+
349
+ def test_bearish_clusters_detected(self):
350
+ ranked = [
351
+ _ind("TopFiller", 20.0, "bullish"),
352
+ ] + [
353
+ _ind(f"BearInd{i}", -10.0 - i * 0.5, "bearish", "Consumer",
354
+ perf_1w=-3.0 - i, perf_1m=-5.0 - i, perf_3m=-8.0 - i)
355
+ for i in range(5)
356
+ ]
357
+ discovered = discover_themes(ranked, set(), [], top_n=5)
358
+ bearish = [d for d in discovered if d["direction"] == "bearish"]
359
+ if bearish:
360
+ assert bearish[0]["direction"] == "bearish"
361
+
362
+
363
+ # ---------------------------------------------------------------------------
364
+ # TestIsDuplicateOfExisting
365
+ # ---------------------------------------------------------------------------
366
+
367
+
368
+ class TestIsDuplicateOfExisting:
369
+
370
+ def test_high_overlap_same_direction_is_duplicate(self):
371
+ cluster = [{"name": "A"}, {"name": "B"}]
372
+ existing = [{
373
+ "direction": "bullish",
374
+ "matching_industries": [{"name": "A"}, {"name": "B"}, {"name": "C"}],
375
+ }]
376
+ assert _is_duplicate_of_existing(cluster, "bullish", existing, 0.5) is True
377
+
378
+ def test_high_overlap_different_direction_not_duplicate(self):
379
+ cluster = [{"name": "A"}, {"name": "B"}]
380
+ existing = [{
381
+ "direction": "bearish",
382
+ "matching_industries": [{"name": "A"}, {"name": "B"}, {"name": "C"}],
383
+ }]
384
+ assert _is_duplicate_of_existing(cluster, "bullish", existing, 0.5) is False
385
+
386
+ def test_low_overlap_same_direction_not_duplicate(self):
387
+ cluster = [{"name": "X"}, {"name": "Y"}]
388
+ existing = [{
389
+ "direction": "bullish",
390
+ "matching_industries": [{"name": "A"}, {"name": "B"}, {"name": "C"}],
391
+ }]
392
+ assert _is_duplicate_of_existing(cluster, "bullish", existing, 0.5) is False
393
+
394
+ def test_glp1_not_duplicate_of_healthcare_vertical(self):
395
+ """GLP-1 (Drug Manufacturers) should not be duplicate of Healthcare Vertical."""
396
+ cluster = [
397
+ {"name": "Drug Manufacturers - General"},
398
+ {"name": "Drug Manufacturers - Specialty & Generic"},
399
+ ]
400
+ existing = [{
401
+ "direction": "bullish",
402
+ "matching_industries": [
403
+ {"name": "Medical Devices"},
404
+ {"name": "Healthcare Plans"},
405
+ {"name": "Medical Care Facilities"},
406
+ ],
407
+ }]
408
+ assert _is_duplicate_of_existing(cluster, "bullish", existing, 0.5) is False
409
+
410
+ def test_subset_detected_by_overlap_coefficient(self):
411
+ """Small cluster that is a subset of a large theme should be detected.
412
+
413
+ Jaccard alone misses this: intersection=2, union=12 → Jaccard=0.17.
414
+ Overlap coefficient catches it: intersection=2, min(2,12)=2 → coeff=1.0.
415
+ """
416
+ cluster = [{"name": "A"}, {"name": "B"}]
417
+ existing = [{
418
+ "direction": "bullish",
419
+ "matching_industries": [
420
+ {"name": n} for n in "ABCDEFGHIJKL"
421
+ ],
422
+ }]
423
+ # Jaccard = 2/12 = 0.17 < 0.5 → would PASS Jaccard-only check
424
+ # Overlap coeff = 2/2 = 1.0 >= 0.5 → caught by overlap coefficient
425
+ assert _is_duplicate_of_existing(cluster, "bullish", existing, 0.5) is True
426
+
427
+ def test_partial_overlap_below_both_thresholds_passes(self):
428
+ """Cluster with low overlap on both metrics should not be duplicate."""
429
+ cluster = [{"name": "A"}, {"name": "X"}, {"name": "Y"}, {"name": "Z"}]
430
+ existing = [{
431
+ "direction": "bullish",
432
+ "matching_industries": [
433
+ {"name": n} for n in "ABCDEFGHIJ"
434
+ ],
435
+ }]
436
+ # Jaccard = 1/13 = 0.077 < 0.5
437
+ # Overlap coeff = 1/4 = 0.25 < 0.5
438
+ assert _is_duplicate_of_existing(cluster, "bullish", existing, 0.5) is False
439
+
440
+
441
+ # ---------------------------------------------------------------------------
442
+ # TestBuildThemeDict
443
+ # ---------------------------------------------------------------------------
444
+
445
+
446
+ class TestBuildThemeDict:
447
+
448
+ def test_has_required_fields(self):
449
+ industries = [_ind("A", 10.0, "bullish"), _ind("B", 9.0, "bullish")]
450
+ result = _build_theme_dict("Test Cluster", industries)
451
+ assert result["theme_name"] == "Test Cluster"
452
+ assert result["direction"] == "bullish"
453
+ assert result["proxy_etfs"] == []
454
+ assert result["static_stocks"] == []
455
+ assert result["theme_origin"] == "discovered"
456
+ assert result["name_confidence"] == "medium"
457
+ assert len(result["matching_industries"]) == 2
458
+ assert "sector_weights" in result
@@ -0,0 +1,76 @@
1
+ """Tests for uptrend_client.is_data_stale (business day logic)."""
2
+
3
+ from datetime import datetime
4
+ from unittest.mock import patch
5
+
6
+ import pytest
7
+
8
+ from uptrend_client import is_data_stale
9
+
10
+
11
+ class TestIsDataStale:
12
+ """is_data_stale should count business days, not calendar days."""
13
+
14
+ def _mock_now(self, year, month, day, hour=12):
15
+ return datetime(year, month, day, hour, 0, 0)
16
+
17
+ def test_friday_to_sunday_not_stale(self):
18
+ """Friday data checked on Sunday: 0 business days -> not stale."""
19
+ with patch("uptrend_client.datetime") as mock_dt:
20
+ mock_dt.strptime = datetime.strptime
21
+ mock_dt.now.return_value = self._mock_now(2026, 2, 15) # Sunday
22
+ assert is_data_stale("2026-02-13") is False # Friday
23
+
24
+ def test_friday_to_monday_not_stale(self):
25
+ """Friday data checked on Monday: 1 business day -> not stale (threshold=2)."""
26
+ with patch("uptrend_client.datetime") as mock_dt:
27
+ mock_dt.strptime = datetime.strptime
28
+ mock_dt.now.return_value = self._mock_now(2026, 2, 16) # Monday
29
+ assert is_data_stale("2026-02-13") is False # Friday
30
+
31
+ def test_friday_to_tuesday_not_stale(self):
32
+ """Friday data checked on Tuesday: 2 business days -> not stale (threshold=2)."""
33
+ with patch("uptrend_client.datetime") as mock_dt:
34
+ mock_dt.strptime = datetime.strptime
35
+ mock_dt.now.return_value = self._mock_now(2026, 2, 17) # Tuesday
36
+ assert is_data_stale("2026-02-13") is False # Friday
37
+
38
+ def test_friday_to_wednesday_stale(self):
39
+ """Friday data checked on Wednesday: 3 business days -> stale (threshold=2)."""
40
+ with patch("uptrend_client.datetime") as mock_dt:
41
+ mock_dt.strptime = datetime.strptime
42
+ mock_dt.now.return_value = self._mock_now(2026, 2, 18) # Wednesday
43
+ assert is_data_stale("2026-02-13") is True # Friday
44
+
45
+ def test_monday_to_wednesday_not_stale(self):
46
+ """Monday data checked on Wednesday: 2 business days -> not stale."""
47
+ with patch("uptrend_client.datetime") as mock_dt:
48
+ mock_dt.strptime = datetime.strptime
49
+ mock_dt.now.return_value = self._mock_now(2026, 2, 18) # Wednesday
50
+ assert is_data_stale("2026-02-16") is False # Monday
51
+
52
+ def test_monday_to_thursday_stale(self):
53
+ """Monday data checked on Thursday: 3 business days -> stale."""
54
+ with patch("uptrend_client.datetime") as mock_dt:
55
+ mock_dt.strptime = datetime.strptime
56
+ mock_dt.now.return_value = self._mock_now(2026, 2, 19) # Thursday
57
+ assert is_data_stale("2026-02-16") is True # Monday
58
+
59
+ def test_same_day_not_stale(self):
60
+ """Same day data -> 0 business days -> not stale."""
61
+ with patch("uptrend_client.datetime") as mock_dt:
62
+ mock_dt.strptime = datetime.strptime
63
+ mock_dt.now.return_value = self._mock_now(2026, 2, 16) # Monday
64
+ assert is_data_stale("2026-02-16") is False
65
+
66
+ def test_invalid_date_returns_true(self):
67
+ """Invalid date string -> stale (safe default)."""
68
+ assert is_data_stale("not-a-date") is True
69
+
70
+ def test_custom_threshold(self):
71
+ """Custom threshold_bdays works correctly."""
72
+ with patch("uptrend_client.datetime") as mock_dt:
73
+ mock_dt.strptime = datetime.strptime
74
+ mock_dt.now.return_value = self._mock_now(2026, 2, 17) # Tuesday
75
+ # Friday to Tuesday = 2 bdays, threshold=1 -> stale
76
+ assert is_data_stale("2026-02-13", threshold_bdays=1) is True