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,815 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Theme Detector - Main Orchestrator
4
+
5
+ Detects trending market themes (bullish and bearish) by combining:
6
+ - FINVIZ industry/sector performance data
7
+ - yfinance ETF volume and stock metrics
8
+ - Monty's Uptrend Ratio Dashboard data
9
+
10
+ Outputs: JSON + Markdown report saved to reports/ directory.
11
+
12
+ Usage:
13
+ python3 theme_detector.py --output-dir reports/
14
+ python3 theme_detector.py --fmp-api-key $FMP_API_KEY --finviz-api-key $FINVIZ_API_KEY
15
+ """
16
+
17
+ import argparse
18
+ import json
19
+ import os
20
+ import sys
21
+ import time
22
+ from datetime import datetime
23
+ from typing import Dict, List, Optional, Tuple
24
+
25
+ # Ensure scripts directory is on the path
26
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
27
+
28
+ from calculators.industry_ranker import rank_industries, get_top_bottom_industries
29
+ from calculators.theme_classifier import classify_themes
30
+ from calculators.heat_calculator import (
31
+ momentum_strength_score,
32
+ volume_intensity_score,
33
+ uptrend_signal_score,
34
+ breadth_signal_score,
35
+ calculate_theme_heat,
36
+ )
37
+ from calculators.lifecycle_calculator import (
38
+ estimate_duration_score,
39
+ extremity_clustering_score,
40
+ price_extreme_saturation_score,
41
+ valuation_premium_score,
42
+ etf_proliferation_score,
43
+ classify_stage,
44
+ calculate_lifecycle_maturity,
45
+ )
46
+ from scorer import (
47
+ score_theme,
48
+ calculate_confidence,
49
+ determine_data_mode,
50
+ )
51
+ from report_generator import generate_json_report, generate_markdown_report, save_reports
52
+
53
+ # Heavy-dependency modules (pandas/numpy/yfinance/finvizfinance) are imported
54
+ # lazily inside main() to allow lightweight helpers like _get_representative_stocks
55
+ # to be imported without triggering sys.exit(1) when pandas is absent.
56
+
57
+
58
+ # ---------------------------------------------------------------------------
59
+ # Industry-to-sector mapping (FINVIZ industry names -> sector)
60
+ # ---------------------------------------------------------------------------
61
+ INDUSTRY_TO_SECTOR: Dict[str, str] = {
62
+ # Technology
63
+ "Semiconductors": "Technology",
64
+ "Semiconductor Equipment & Materials": "Technology",
65
+ "Software - Application": "Technology",
66
+ "Software - Infrastructure": "Technology",
67
+ "Information Technology Services": "Technology",
68
+ "Computer Hardware": "Technology",
69
+ "Electronic Components": "Technology",
70
+ "Electronics & Computer Distribution": "Technology",
71
+ "Scientific & Technical Instruments": "Technology",
72
+ "Communication Equipment": "Technology",
73
+ "Consumer Electronics": "Technology",
74
+ # Healthcare
75
+ "Biotechnology": "Healthcare",
76
+ "Drug Manufacturers - General": "Healthcare",
77
+ "Drug Manufacturers - Specialty & Generic": "Healthcare",
78
+ "Medical Devices": "Healthcare",
79
+ "Medical Instruments & Supplies": "Healthcare",
80
+ "Diagnostics & Research": "Healthcare",
81
+ "Healthcare Plans": "Healthcare",
82
+ "Health Information Services": "Healthcare",
83
+ "Medical Care Facilities": "Healthcare",
84
+ "Pharmaceutical Retailers": "Healthcare",
85
+ "Medical Distribution": "Healthcare",
86
+ # Financial
87
+ "Banks - Diversified": "Financial",
88
+ "Banks - Regional": "Financial",
89
+ "Insurance - Life": "Financial",
90
+ "Insurance - Property & Casualty": "Financial",
91
+ "Insurance - Diversified": "Financial",
92
+ "Insurance - Specialty": "Financial",
93
+ "Insurance Brokers": "Financial",
94
+ "Asset Management": "Financial",
95
+ "Capital Markets": "Financial",
96
+ "Financial Data & Stock Exchanges": "Financial",
97
+ "Credit Services": "Financial",
98
+ "Mortgage Finance": "Financial",
99
+ "Financial Conglomerates": "Financial",
100
+ "Shell Companies": "Financial",
101
+ # Consumer Cyclical
102
+ "Auto Manufacturers": "Consumer Cyclical",
103
+ "Auto Parts": "Consumer Cyclical",
104
+ "Auto & Truck Dealerships": "Consumer Cyclical",
105
+ "Recreational Vehicles": "Consumer Cyclical",
106
+ "Furnishings, Fixtures & Appliances": "Consumer Cyclical",
107
+ "Residential Construction": "Consumer Cyclical",
108
+ "Textile Manufacturing": "Consumer Cyclical",
109
+ "Apparel Manufacturing": "Consumer Cyclical",
110
+ "Footwear & Accessories": "Consumer Cyclical",
111
+ "Packaging & Containers": "Consumer Cyclical",
112
+ "Personal Services": "Consumer Cyclical",
113
+ "Restaurants": "Consumer Cyclical",
114
+ "Apparel Retail": "Consumer Cyclical",
115
+ "Department Stores": "Consumer Cyclical",
116
+ "Home Improvement Retail": "Consumer Cyclical",
117
+ "Luxury Goods": "Consumer Cyclical",
118
+ "Internet Retail": "Consumer Cyclical",
119
+ "Specialty Retail": "Consumer Cyclical",
120
+ "Gambling": "Consumer Cyclical",
121
+ "Leisure": "Consumer Cyclical",
122
+ "Lodging": "Consumer Cyclical",
123
+ "Resorts & Casinos": "Consumer Cyclical",
124
+ "Travel Services": "Consumer Cyclical",
125
+ # Consumer Defensive
126
+ "Beverages - Non-Alcoholic": "Consumer Defensive",
127
+ "Beverages - Brewers": "Consumer Defensive",
128
+ "Beverages - Wineries & Distilleries": "Consumer Defensive",
129
+ "Confectioners": "Consumer Defensive",
130
+ "Farm Products": "Consumer Defensive",
131
+ "Household & Personal Products": "Consumer Defensive",
132
+ "Packaged Foods": "Consumer Defensive",
133
+ "Education & Training Services": "Consumer Defensive",
134
+ "Discount Stores": "Consumer Defensive",
135
+ "Food Distribution": "Consumer Defensive",
136
+ "Grocery Stores": "Consumer Defensive",
137
+ "Tobacco": "Consumer Defensive",
138
+ # Industrials
139
+ "Aerospace & Defense": "Industrials",
140
+ "Airlines": "Industrials",
141
+ "Building Products & Equipment": "Industrials",
142
+ "Business Equipment & Supplies": "Industrials",
143
+ "Conglomerates": "Industrials",
144
+ "Consulting Services": "Industrials",
145
+ "Electrical Equipment & Parts": "Industrials",
146
+ "Engineering & Construction": "Industrials",
147
+ "Farm & Heavy Construction Machinery": "Industrials",
148
+ "Industrial Distribution": "Industrials",
149
+ "Infrastructure Operations": "Industrials",
150
+ "Integrated Freight & Logistics": "Industrials",
151
+ "Marine Shipping": "Industrials",
152
+ "Metal Fabrication": "Industrials",
153
+ "Pollution & Treatment Controls": "Industrials",
154
+ "Railroads": "Industrials",
155
+ "Rental & Leasing Services": "Industrials",
156
+ "Security & Protection Services": "Industrials",
157
+ "Specialty Business Services": "Industrials",
158
+ "Specialty Industrial Machinery": "Industrials",
159
+ "Staffing & Employment Services": "Industrials",
160
+ "Tools & Accessories": "Industrials",
161
+ "Trucking": "Industrials",
162
+ "Waste Management": "Industrials",
163
+ # Energy
164
+ "Oil & Gas E&P": "Energy",
165
+ "Oil & Gas Equipment & Services": "Energy",
166
+ "Oil & Gas Integrated": "Energy",
167
+ "Oil & Gas Midstream": "Energy",
168
+ "Oil & Gas Refining & Marketing": "Energy",
169
+ "Oil & Gas Drilling": "Energy",
170
+ "Thermal Coal": "Energy",
171
+ "Uranium": "Energy",
172
+ "Solar": "Energy",
173
+ # Basic Materials
174
+ "Gold": "Basic Materials",
175
+ "Silver": "Basic Materials",
176
+ "Aluminum": "Basic Materials",
177
+ "Copper": "Basic Materials",
178
+ "Steel": "Basic Materials",
179
+ "Other Industrial Metals & Mining": "Basic Materials",
180
+ "Other Precious Metals & Mining": "Basic Materials",
181
+ "Coking Coal": "Basic Materials",
182
+ "Lumber & Wood Production": "Basic Materials",
183
+ "Paper & Paper Products": "Basic Materials",
184
+ "Chemicals": "Basic Materials",
185
+ "Specialty Chemicals": "Basic Materials",
186
+ "Agricultural Inputs": "Basic Materials",
187
+ "Building Materials": "Basic Materials",
188
+ # Communication Services
189
+ "Telecom Services": "Communication Services",
190
+ "Advertising Agencies": "Communication Services",
191
+ "Publishing": "Communication Services",
192
+ "Broadcasting": "Communication Services",
193
+ "Entertainment": "Communication Services",
194
+ "Internet Content & Information": "Communication Services",
195
+ "Electronic Gaming & Multimedia": "Communication Services",
196
+ # Real Estate
197
+ "REIT - Diversified": "Real Estate",
198
+ "REIT - Healthcare Facilities": "Real Estate",
199
+ "REIT - Hotel & Motel": "Real Estate",
200
+ "REIT - Industrial": "Real Estate",
201
+ "REIT - Mortgage": "Real Estate",
202
+ "REIT - Office": "Real Estate",
203
+ "REIT - Residential": "Real Estate",
204
+ "REIT - Retail": "Real Estate",
205
+ "REIT - Specialty": "Real Estate",
206
+ "Real Estate - Development": "Real Estate",
207
+ "Real Estate - Diversified": "Real Estate",
208
+ "Real Estate Services": "Real Estate",
209
+ # Utilities
210
+ "Utilities - Diversified": "Utilities",
211
+ "Utilities - Independent Power Producers": "Utilities",
212
+ "Utilities - Regulated Electric": "Utilities",
213
+ "Utilities - Regulated Gas": "Utilities",
214
+ "Utilities - Regulated Water": "Utilities",
215
+ "Utilities - Renewable": "Utilities",
216
+ }
217
+
218
+
219
+ # ---------------------------------------------------------------------------
220
+ # Theme configuration is loaded from YAML / inline fallback via config_loader.
221
+ # DEFAULT_THEMES_CONFIG and ETF_CATALOG live in default_theme_config.py
222
+ # to avoid circular imports.
223
+ # ---------------------------------------------------------------------------
224
+
225
+
226
+ # ---------------------------------------------------------------------------
227
+ # CLI argument parsing
228
+ # ---------------------------------------------------------------------------
229
+ def parse_args() -> argparse.Namespace:
230
+ parser = argparse.ArgumentParser(
231
+ description="Detect trending market themes from FINVIZ industry data"
232
+ )
233
+ parser.add_argument(
234
+ "--fmp-api-key",
235
+ default=os.environ.get("FMP_API_KEY"),
236
+ help="Financial Modeling Prep API key (env: FMP_API_KEY)",
237
+ )
238
+ parser.add_argument(
239
+ "--finviz-api-key",
240
+ default=os.environ.get("FINVIZ_API_KEY"),
241
+ help="FINVIZ Elite API key (env: FINVIZ_API_KEY)",
242
+ )
243
+ parser.add_argument(
244
+ "--finviz-mode",
245
+ choices=["public", "elite"],
246
+ default=None,
247
+ help="FINVIZ mode (auto-detected if not specified)",
248
+ )
249
+ parser.add_argument(
250
+ "--output-dir",
251
+ default="reports/",
252
+ help="Output directory for reports (default: reports/)",
253
+ )
254
+ parser.add_argument(
255
+ "--top",
256
+ type=int,
257
+ default=3,
258
+ help="Number of top themes to show in detail (default: 3)",
259
+ )
260
+ parser.add_argument(
261
+ "--max-themes",
262
+ type=int,
263
+ default=10,
264
+ help="Maximum themes to analyze (default: 10)",
265
+ )
266
+ parser.add_argument(
267
+ "--max-stocks-per-theme",
268
+ type=int,
269
+ default=10,
270
+ help="Maximum stocks per theme (default: 10)",
271
+ )
272
+ parser.add_argument(
273
+ "--themes-config",
274
+ default=None,
275
+ help="Path to custom themes.yaml (default: bundled)",
276
+ )
277
+ parser.add_argument(
278
+ "--discover-themes",
279
+ action="store_true",
280
+ default=False,
281
+ help="Enable automatic theme discovery for unmatched industries",
282
+ )
283
+ parser.add_argument(
284
+ "--dynamic-stocks",
285
+ action="store_true",
286
+ default=False,
287
+ help="Enable dynamic stock selection via FINVIZ screener (default: off)",
288
+ )
289
+ parser.add_argument(
290
+ "--dynamic-min-cap",
291
+ choices=["micro", "small", "mid"],
292
+ default="small",
293
+ help="Minimum market cap for dynamic stock selection (default: small=$300mln+)",
294
+ )
295
+ return parser.parse_args()
296
+
297
+
298
+ # ---------------------------------------------------------------------------
299
+ # Helpers
300
+ # ---------------------------------------------------------------------------
301
+ def _add_sector_info(industries: List[Dict]) -> List[Dict]:
302
+ """Add sector field to each industry dict from INDUSTRY_TO_SECTOR mapping."""
303
+ for ind in industries:
304
+ name = ind.get("name", "")
305
+ ind["sector"] = INDUSTRY_TO_SECTOR.get(name, "Unknown")
306
+ return industries
307
+
308
+
309
+ def _convert_perf_to_pct(industries: List[Dict]) -> List[Dict]:
310
+ """Convert FINVIZ decimal performance values to percentage.
311
+
312
+ finvizfinance returns 0.05 for 5%. Our calculators expect 5.0.
313
+ """
314
+ perf_keys = ["perf_1w", "perf_1m", "perf_3m", "perf_6m", "perf_1y", "perf_ytd"]
315
+ for ind in industries:
316
+ for key in perf_keys:
317
+ val = ind.get(key)
318
+ if val is not None:
319
+ ind[key] = val * 100.0
320
+ return industries
321
+
322
+
323
+ def _get_representative_stocks(
324
+ theme: Dict,
325
+ selector, # Optional[RepresentativeStockSelector]
326
+ max_stocks: int,
327
+ ) -> Tuple[List[str], List[Dict]]:
328
+ """Get representative stocks and metadata for a theme.
329
+
330
+ When selector is provided (--dynamic-stocks), uses FINVIZ/FMP fallback chain.
331
+ Otherwise falls back to static_stocks from config.
332
+
333
+ Returns:
334
+ (tickers, stock_details)
335
+ tickers: ["NVDA", "AVGO", ...] (backward compatible)
336
+ stock_details: [{symbol, source, market_cap, matched_industries,
337
+ reasons, composite_score}, ...]
338
+ """
339
+ if selector is not None:
340
+ details = selector.select_stocks(theme, max_stocks)
341
+ tickers = [d["symbol"] for d in details]
342
+ return tickers, details
343
+
344
+ # Static fallback (selector is None = --dynamic-stocks not set)
345
+ static = theme.get("static_stocks", [])[:max_stocks]
346
+ details = [
347
+ {
348
+ "symbol": s,
349
+ "source": "static",
350
+ "market_cap": 0,
351
+ "matched_industries": [],
352
+ "reasons": ["Static config"],
353
+ "composite_score": 0,
354
+ }
355
+ for s in static
356
+ ]
357
+ return static, details
358
+
359
+
360
+ def _calculate_breadth_ratio(theme: Dict) -> Optional[float]:
361
+ """Estimate breadth ratio from theme's matching industries.
362
+
363
+ For bullish: ratio of industries with positive weighted_return.
364
+ For bearish: ratio of industries with negative weighted_return.
365
+ """
366
+ industries = theme.get("matching_industries", [])
367
+ if not industries:
368
+ return None
369
+
370
+ is_bearish = theme.get("direction") == "bearish"
371
+ if is_bearish:
372
+ count = sum(1 for ind in industries if ind.get("weighted_return", 0) < 0)
373
+ else:
374
+ count = sum(1 for ind in industries if ind.get("weighted_return", 0) > 0)
375
+
376
+ return count / len(industries)
377
+
378
+
379
+ def _get_theme_uptrend_data(
380
+ theme: Dict, sector_uptrend: Dict
381
+ ) -> List[Dict]:
382
+ """Build sector_data for uptrend_signal_score from theme's sector weights.
383
+
384
+ Maps theme sectors to uptrend data with weights.
385
+ """
386
+ sector_weights = theme.get("sector_weights", {})
387
+ if not sector_weights or not sector_uptrend:
388
+ return []
389
+
390
+ sector_data = []
391
+ for sector_name, weight in sector_weights.items():
392
+ uptrend_entry = sector_uptrend.get(sector_name)
393
+ if uptrend_entry and uptrend_entry.get("ratio") is not None:
394
+ sector_data.append({
395
+ "sector": sector_name,
396
+ "ratio": uptrend_entry["ratio"],
397
+ "ma_10": uptrend_entry.get("ma_10") or 0,
398
+ "slope": uptrend_entry.get("slope") or 0,
399
+ "weight": weight,
400
+ })
401
+
402
+ return sector_data
403
+
404
+
405
+ def _get_theme_weighted_return(theme: Dict) -> float:
406
+ """Calculate aggregate weighted return for a theme from its industries."""
407
+ industries = theme.get("matching_industries", [])
408
+ if not industries:
409
+ return 0.0
410
+
411
+ returns = [ind.get("weighted_return", 0.0) for ind in industries]
412
+ return sum(returns) / len(returns)
413
+
414
+
415
+ # ---------------------------------------------------------------------------
416
+ # Main orchestrator
417
+ # ---------------------------------------------------------------------------
418
+ def main():
419
+ # Lazy imports: these modules depend on pandas/numpy/yfinance/finvizfinance
420
+ # and are only needed at runtime, not when importing helpers for testing.
421
+ from finviz_performance_client import get_industry_performance
422
+ from etf_scanner import ETFScanner
423
+ from uptrend_client import fetch_sector_uptrend_data, is_data_stale
424
+ from config_loader import load_themes_config
425
+
426
+ args = parse_args()
427
+
428
+ # -----------------------------------------------------------------------
429
+ # Step 0: Load theme configuration (YAML or inline fallback)
430
+ # -----------------------------------------------------------------------
431
+ themes_config, etf_catalog = load_themes_config(args.themes_config)
432
+ start_time = time.time()
433
+
434
+ # Determine data mode
435
+ finviz_mode = args.finviz_mode
436
+ if finviz_mode is None:
437
+ finviz_mode = "elite" if args.finviz_api_key else "public"
438
+ fmp_available = args.fmp_api_key is not None
439
+ data_mode = determine_data_mode(fmp_available, finviz_mode == "elite")
440
+
441
+ print("Theme Detector starting...", file=sys.stderr)
442
+ print(f" Data mode: {data_mode}", file=sys.stderr)
443
+ print(f" FINVIZ mode: {finviz_mode}", file=sys.stderr)
444
+ print(f" FMP API: {'available' if fmp_available else 'not available'}",
445
+ file=sys.stderr)
446
+ print(f" Max themes: {args.max_themes}", file=sys.stderr)
447
+ print(f" Max stocks/theme: {args.max_stocks_per_theme}", file=sys.stderr)
448
+
449
+ metadata = {
450
+ "generated_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
451
+ "data_mode": data_mode,
452
+ "finviz_mode": finviz_mode,
453
+ "fmp_available": fmp_available,
454
+ "max_themes": args.max_themes,
455
+ "max_stocks_per_theme": args.max_stocks_per_theme,
456
+ "data_sources": {},
457
+ }
458
+
459
+ # -----------------------------------------------------------------------
460
+ # Step 1: Fetch FINVIZ industry performance
461
+ # -----------------------------------------------------------------------
462
+ print("Fetching FINVIZ industry performance...", file=sys.stderr)
463
+ raw_industries = get_industry_performance()
464
+ if not raw_industries:
465
+ print("ERROR: No industry data from FINVIZ. Exiting.", file=sys.stderr)
466
+ sys.exit(1)
467
+
468
+ metadata["data_sources"]["finviz_industries"] = len(raw_industries)
469
+ print(f" Got {len(raw_industries)} industries", file=sys.stderr)
470
+
471
+ # Convert decimal to percentage and add sector info
472
+ industries = _convert_perf_to_pct(raw_industries)
473
+ industries = _add_sector_info(industries)
474
+
475
+ # -----------------------------------------------------------------------
476
+ # Step 2: Rank industries by momentum
477
+ # -----------------------------------------------------------------------
478
+ print("Ranking industries by momentum...", file=sys.stderr)
479
+ ranked = rank_industries(industries)
480
+ industry_rankings = get_top_bottom_industries(ranked, n=15)
481
+ print(f" Top: {ranked[0]['name']} ({ranked[0]['momentum_score']})" if ranked else "",
482
+ file=sys.stderr)
483
+
484
+ # -----------------------------------------------------------------------
485
+ # Step 3: Classify themes
486
+ # -----------------------------------------------------------------------
487
+ print("Classifying themes...", file=sys.stderr)
488
+ themes = classify_themes(ranked, themes_config)
489
+ print(f" Detected {len(themes)} themes (seed + vertical)", file=sys.stderr)
490
+
491
+ if not themes:
492
+ print("WARNING: No themes detected. Generating empty report.",
493
+ file=sys.stderr)
494
+
495
+ # Step 3.5: Discover new themes from unmatched industries
496
+ if args.discover_themes:
497
+ from calculators.theme_classifier import get_matched_industry_names
498
+ from calculators.theme_discoverer import discover_themes
499
+ matched_names = get_matched_industry_names(themes)
500
+ discovered = discover_themes(ranked, matched_names, themes, top_n=30)
501
+ themes.extend(discovered)
502
+ metadata["data_sources"]["discovered_themes"] = len(discovered)
503
+ print(f" Discovered {len(discovered)} new themes", file=sys.stderr)
504
+
505
+ # Step 3.9: Limit to max_themes using composite priority (size + strength)
506
+ def _theme_priority(t):
507
+ inds = t.get("matching_industries", [])
508
+ n_industries = len(inds)
509
+ avg_strength = (
510
+ sum(abs(ind.get("weighted_return", 0)) for ind in inds) / max(len(inds), 1)
511
+ )
512
+ size_norm = min(n_industries / 10.0, 1.0)
513
+ strength_norm = min(avg_strength / 30.0, 1.0)
514
+ return size_norm * 0.5 + strength_norm * 0.5
515
+
516
+ themes.sort(key=_theme_priority, reverse=True)
517
+ themes = themes[:args.max_themes]
518
+
519
+ # -----------------------------------------------------------------------
520
+ # Step 4: Collect all stock symbols for batch download
521
+ # -----------------------------------------------------------------------
522
+ print("Selecting representative stocks...", file=sys.stderr)
523
+
524
+ # Create dynamic selector if requested
525
+ selector = None
526
+ if args.dynamic_stocks:
527
+ from representative_stock_selector import RepresentativeStockSelector
528
+ selector = RepresentativeStockSelector(
529
+ finviz_elite_key=args.finviz_api_key,
530
+ fmp_api_key=args.fmp_api_key,
531
+ finviz_mode=finviz_mode,
532
+ rate_limit_sec=1.0,
533
+ min_cap=args.dynamic_min_cap,
534
+ )
535
+ print(" Dynamic stock selection: ON", file=sys.stderr)
536
+
537
+ # Use index-based keys to avoid collisions when multiple themes share
538
+ # the same name (e.g. two "{Sector} Sector Concentration" themes for
539
+ # top and bottom, or duplicate auto-names from the discoverer).
540
+ theme_stocks: Dict[int, List[str]] = {}
541
+ theme_stock_details: Dict[int, List[Dict]] = {}
542
+ all_symbols = set()
543
+
544
+ for idx, theme in enumerate(themes):
545
+ tickers, stock_details = _get_representative_stocks(
546
+ theme, selector, args.max_stocks_per_theme
547
+ )
548
+ theme_stocks[idx] = tickers
549
+ theme_stock_details[idx] = stock_details
550
+ all_symbols.update(tickers)
551
+
552
+ all_symbols_list = sorted(all_symbols)
553
+ print(f" Total unique stocks: {len(all_symbols_list)}", file=sys.stderr)
554
+
555
+ if selector:
556
+ print(f" Dynamic stock queries: {selector.query_count}", file=sys.stderr)
557
+ print(f" Dynamic stock failures: {selector.failure_count}", file=sys.stderr)
558
+ print(f" Dynamic stock status: {selector.status}", file=sys.stderr)
559
+ metadata["data_sources"]["dynamic_stocks_status"] = selector.status
560
+ metadata["data_sources"]["dynamic_stocks_queries"] = selector.query_count
561
+ metadata["data_sources"]["dynamic_stocks_failures"] = selector.failure_count
562
+ metadata["data_sources"]["dynamic_stocks_source_states"] = {
563
+ name: {"disabled": s.disabled, "failures": s.total_failures}
564
+ for name, s in selector.source_states.items()
565
+ }
566
+
567
+ # -----------------------------------------------------------------------
568
+ # Step 5: Batch fetch stock metrics (yfinance)
569
+ # -----------------------------------------------------------------------
570
+ stock_metrics_map: Dict[str, Dict] = {}
571
+ scanner = ETFScanner(fmp_api_key=args.fmp_api_key)
572
+
573
+ if all_symbols_list:
574
+ print(f"Batch downloading {len(all_symbols_list)} stocks...", file=sys.stderr)
575
+ all_metrics = scanner.batch_stock_metrics(all_symbols_list)
576
+ for m in all_metrics:
577
+ stock_metrics_map[m["symbol"]] = m
578
+ # Backward compatible key (1 release coexistence)
579
+ metadata["data_sources"]["yfinance_stocks"] = len(all_metrics)
580
+ print(f" Got metrics for {len(all_metrics)} stocks", file=sys.stderr)
581
+
582
+ # -----------------------------------------------------------------------
583
+ # Step 6: Fetch ETF volume ratios for each theme's proxy ETFs
584
+ # -----------------------------------------------------------------------
585
+ print("Fetching ETF volume data...", file=sys.stderr)
586
+ etf_volume_map: Dict[str, Dict] = {}
587
+ all_etfs = set()
588
+ for theme in themes:
589
+ for etf in theme.get("proxy_etfs", []):
590
+ all_etfs.add(etf)
591
+
592
+ etf_volume_map = scanner.batch_etf_volume_ratios(sorted(all_etfs))
593
+
594
+ metadata["data_sources"]["etf_volume"] = len(etf_volume_map)
595
+
596
+ # Capture backend stats after all scanner calls (stock + ETF)
597
+ scanner_stats = scanner.backend_stats()
598
+ metadata["data_sources"]["scanner_backend"] = scanner_stats
599
+ print(f" Scanner: FMP {scanner_stats['fmp_calls']} calls "
600
+ f"({scanner_stats['fmp_failures']} failures), "
601
+ f"yfinance: {scanner_stats['yf_calls']} calls "
602
+ f"({scanner_stats['yf_fallbacks']} fallbacks)",
603
+ file=sys.stderr)
604
+
605
+ # -----------------------------------------------------------------------
606
+ # Step 7: Fetch uptrend-dashboard data
607
+ # -----------------------------------------------------------------------
608
+ print("Fetching uptrend ratio data...", file=sys.stderr)
609
+ sector_uptrend = fetch_sector_uptrend_data()
610
+ stale_data = False
611
+
612
+ if sector_uptrend:
613
+ # Check freshness from any sector's latest_date
614
+ any_sector = next(iter(sector_uptrend.values()), {})
615
+ latest_date = any_sector.get("latest_date", "")
616
+ stale_data = is_data_stale(latest_date, threshold_bdays=2)
617
+ if stale_data:
618
+ print(f" WARNING: Uptrend data is stale (latest: {latest_date})",
619
+ file=sys.stderr)
620
+ metadata["data_sources"]["uptrend_sectors"] = len(sector_uptrend)
621
+ metadata["data_sources"]["uptrend_stale"] = stale_data
622
+ else:
623
+ print(" WARNING: Uptrend data unavailable", file=sys.stderr)
624
+ metadata["data_sources"]["uptrend_error"] = "fetch failed"
625
+
626
+ # -----------------------------------------------------------------------
627
+ # Step 8: Score each theme
628
+ # -----------------------------------------------------------------------
629
+ print("Scoring themes...", file=sys.stderr)
630
+ scored_themes = []
631
+
632
+ for idx, theme in enumerate(themes):
633
+ theme_name = theme["theme_name"]
634
+ direction = theme["direction"]
635
+ is_bearish = direction == "bearish"
636
+ stocks = theme_stocks.get(idx, [])
637
+
638
+ # --- Theme Heat ---
639
+ # Momentum: average weighted_return of matching industries
640
+ theme_wr = _get_theme_weighted_return(theme)
641
+ momentum = momentum_strength_score(theme_wr)
642
+
643
+ # Volume: average ETF volume ratio
644
+ etf_vol_ratios = []
645
+ for etf_sym in theme.get("proxy_etfs", []):
646
+ vol = etf_volume_map.get(etf_sym, {})
647
+ if vol.get("vol_20d") is not None and vol.get("vol_60d") is not None:
648
+ etf_vol_ratios.append((vol["vol_20d"], vol["vol_60d"]))
649
+
650
+ if etf_vol_ratios:
651
+ avg_20d = sum(r[0] for r in etf_vol_ratios) / len(etf_vol_ratios)
652
+ avg_60d = sum(r[1] for r in etf_vol_ratios) / len(etf_vol_ratios)
653
+ volume = volume_intensity_score(avg_20d, avg_60d)
654
+ else:
655
+ volume = None # defaults to 50
656
+
657
+ # Uptrend signal
658
+ sector_data = _get_theme_uptrend_data(theme, sector_uptrend)
659
+ if sector_data:
660
+ uptrend = uptrend_signal_score(sector_data, is_bearish)
661
+ else:
662
+ uptrend = None # defaults to 50
663
+
664
+ # Breadth signal
665
+ breadth_ratio = _calculate_breadth_ratio(theme)
666
+ breadth = breadth_signal_score(breadth_ratio)
667
+
668
+ heat = calculate_theme_heat(momentum, volume, uptrend, breadth)
669
+
670
+ heat_breakdown = {
671
+ "momentum_strength": round(momentum, 2),
672
+ "volume_intensity": round(volume, 2) if volume is not None else 50.0,
673
+ "uptrend_signal": round(uptrend, 2) if uptrend is not None else 50.0,
674
+ "breadth_signal": round(breadth, 2),
675
+ }
676
+
677
+ # --- Lifecycle Maturity ---
678
+ # Get stock-level metrics for this theme
679
+ theme_stock_metrics = [
680
+ stock_metrics_map[s] for s in stocks if s in stock_metrics_map
681
+ ]
682
+
683
+ # Remap keys: rsi_14 -> rsi (lifecycle_calculator expects "rsi")
684
+ for sm in theme_stock_metrics:
685
+ if "rsi_14" in sm:
686
+ sm["rsi"] = sm["rsi_14"]
687
+
688
+ # Duration: from industry performance timeframes
689
+ avg_perfs = _average_industry_perfs(theme.get("matching_industries", []))
690
+ duration = estimate_duration_score(
691
+ avg_perfs.get("perf_1m"),
692
+ avg_perfs.get("perf_3m"),
693
+ avg_perfs.get("perf_6m"),
694
+ avg_perfs.get("perf_1y"),
695
+ is_bearish,
696
+ )
697
+
698
+ # Extremity clustering
699
+ extremity = extremity_clustering_score(theme_stock_metrics, is_bearish)
700
+
701
+ # Price extreme saturation
702
+ price_extreme = price_extreme_saturation_score(theme_stock_metrics, is_bearish)
703
+
704
+ # Valuation premium
705
+ valuation = valuation_premium_score(theme_stock_metrics)
706
+
707
+ # ETF proliferation
708
+ etf_count = etf_catalog.get(theme_name, 0)
709
+ etf_prolif = etf_proliferation_score(etf_count)
710
+
711
+ maturity = calculate_lifecycle_maturity(
712
+ duration, extremity, price_extreme, valuation, etf_prolif
713
+ )
714
+ stage = classify_stage(maturity)
715
+
716
+ maturity_breakdown = {
717
+ "duration_estimate": round(duration, 2),
718
+ "extremity_clustering": round(extremity, 2),
719
+ "price_extreme_saturation": round(price_extreme, 2),
720
+ "valuation_premium": round(valuation, 2),
721
+ "etf_proliferation": round(etf_prolif, 2),
722
+ }
723
+
724
+ # --- Confidence ---
725
+ quant_confirmed = momentum > 50
726
+ breadth_confirmed = (uptrend is not None and uptrend > 55) if uptrend else False
727
+ narrative_confirmed = False # Pending Claude WebSearch
728
+ confidence = calculate_confidence(
729
+ quant_confirmed, breadth_confirmed, narrative_confirmed, stale_data
730
+ )
731
+
732
+ # --- Score theme ---
733
+ score = score_theme(
734
+ round(heat, 2),
735
+ round(maturity, 2),
736
+ stage,
737
+ direction,
738
+ confidence,
739
+ data_mode,
740
+ )
741
+
742
+ # Build full theme result
743
+ scored_theme = {
744
+ "name": theme_name,
745
+ "direction": direction,
746
+ "heat": round(heat, 2),
747
+ "maturity": round(maturity, 2),
748
+ "stage": stage,
749
+ "confidence": confidence,
750
+ "heat_label": score["heat_label"],
751
+ "heat_breakdown": heat_breakdown,
752
+ "maturity_breakdown": maturity_breakdown,
753
+ "representative_stocks": stocks,
754
+ "stock_details": theme_stock_details.get(idx, []),
755
+ "proxy_etfs": theme.get("proxy_etfs", []),
756
+ "industries": [ind.get("name", "") for ind in
757
+ theme.get("matching_industries", [])],
758
+ "sector_weights": theme.get("sector_weights", {}),
759
+ "stock_data": "available" if theme_stock_metrics else "unavailable",
760
+ "data_mode": data_mode,
761
+ "stale_data_penalty": stale_data,
762
+ "theme_origin": theme.get("theme_origin", "seed"),
763
+ "name_confidence": theme.get("name_confidence", "high"),
764
+ }
765
+ scored_themes.append(scored_theme)
766
+
767
+ # Sort by heat descending
768
+ scored_themes.sort(key=lambda t: t["heat"], reverse=True)
769
+
770
+ # -----------------------------------------------------------------------
771
+ # Step 9: Generate reports
772
+ # -----------------------------------------------------------------------
773
+ print("Generating reports...", file=sys.stderr)
774
+
775
+ json_report = generate_json_report(
776
+ scored_themes, industry_rankings, sector_uptrend, metadata
777
+ )
778
+ md_report = generate_markdown_report(json_report, top_n_detail=args.top)
779
+
780
+ # Resolve output directory relative to repo root if relative
781
+ output_dir = args.output_dir
782
+ if not os.path.isabs(output_dir):
783
+ # Look for reports/ relative to repo root
784
+ repo_root = os.path.dirname(os.path.dirname(os.path.dirname(
785
+ os.path.dirname(os.path.abspath(__file__))
786
+ )))
787
+ output_dir = os.path.join(repo_root, output_dir)
788
+
789
+ paths = save_reports(json_report, md_report, output_dir)
790
+
791
+ elapsed = time.time() - start_time
792
+ print(f"\nDone in {elapsed:.1f}s", file=sys.stderr)
793
+ print(f" JSON: {paths['json']}", file=sys.stderr)
794
+ print(f" Markdown: {paths['markdown']}", file=sys.stderr)
795
+ print(f" Themes: {len(scored_themes)}", file=sys.stderr)
796
+
797
+ # Print JSON to stdout for programmatic consumption
798
+ print(json.dumps(json_report, indent=2, default=str))
799
+
800
+
801
+ def _average_industry_perfs(industries: List[Dict]) -> Dict:
802
+ """Average performance across theme's matching industries."""
803
+ if not industries:
804
+ return {}
805
+
806
+ perf_keys = ["perf_1m", "perf_3m", "perf_6m", "perf_1y"]
807
+ result = {}
808
+ for key in perf_keys:
809
+ vals = [ind.get(key) for ind in industries if ind.get(key) is not None]
810
+ result[key] = sum(vals) / len(vals) if vals else None
811
+ return result
812
+
813
+
814
+ if __name__ == "__main__":
815
+ main()