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,834 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Tests for VCP Screener modules.
4
+
5
+ Covers boundary conditions for VCP pattern detection, contraction validation,
6
+ Trend Template criteria, volume patterns, pivot proximity, and scoring.
7
+ """
8
+
9
+ import json
10
+ import os
11
+ import tempfile
12
+
13
+ from calculators.pivot_proximity_calculator import calculate_pivot_proximity
14
+ from calculators.relative_strength_calculator import calculate_relative_strength
15
+ from calculators.trend_template_calculator import calculate_trend_template
16
+ from calculators.vcp_pattern_calculator import _validate_vcp
17
+ from calculators.volume_pattern_calculator import calculate_volume_pattern
18
+ from report_generator import generate_json_report, generate_markdown_report
19
+ from scorer import calculate_composite_score
20
+ from screen_vcp import analyze_stock, compute_entry_ready, is_stale_price
21
+
22
+ # ---------------------------------------------------------------------------
23
+ # Helpers
24
+ # ---------------------------------------------------------------------------
25
+
26
+
27
+ def _make_prices(n, start=100.0, daily_change=0.0, volume=1000000):
28
+ """Generate synthetic price data (most-recent-first)."""
29
+ prices = []
30
+ p = start
31
+ for i in range(n):
32
+ p_day = p * (1 + daily_change * (n - i)) # linear drift
33
+ prices.append(
34
+ {
35
+ "date": f"2025-{(i // 22) + 1:02d}-{(i % 22) + 1:02d}",
36
+ "open": round(p_day, 2),
37
+ "high": round(p_day * 1.01, 2),
38
+ "low": round(p_day * 0.99, 2),
39
+ "close": round(p_day, 2),
40
+ "adjClose": round(p_day, 2),
41
+ "volume": volume,
42
+ }
43
+ )
44
+ return prices
45
+
46
+
47
+ def _make_vcp_contractions(depths, high_price=100.0):
48
+ """Build contraction dicts for _validate_vcp testing."""
49
+ contractions = []
50
+ hp = high_price
51
+ for i, depth in enumerate(depths):
52
+ lp = hp * (1 - depth / 100)
53
+ contractions.append(
54
+ {
55
+ "label": f"T{i + 1}",
56
+ "high_idx": i * 20,
57
+ "high_price": round(hp, 2),
58
+ "high_date": f"2025-01-{i * 20 + 1:02d}",
59
+ "low_idx": i * 20 + 10,
60
+ "low_price": round(lp, 2),
61
+ "low_date": f"2025-01-{i * 20 + 11:02d}",
62
+ "depth_pct": round(depth, 2),
63
+ }
64
+ )
65
+ hp = hp * 0.99 # next high slightly lower
66
+ return contractions
67
+
68
+
69
+ # ===========================================================================
70
+ # VCP Pattern Validation Tests (Fix 1: contraction ratio 0.75 rule)
71
+ # ===========================================================================
72
+
73
+
74
+ class TestVCPValidation:
75
+ """Test the strict 75% contraction ratio rule."""
76
+
77
+ def test_valid_tight_contractions(self):
78
+ """T1=20%, T2=10%, T3=5% -> ratios 0.50, 0.50 -> valid"""
79
+ contractions = _make_vcp_contractions([20, 10, 5])
80
+ result = _validate_vcp(contractions, total_days=120)
81
+ assert result["valid"] is True
82
+
83
+ def test_invalid_loose_contractions(self):
84
+ """T1=20%, T2=18% -> ratio 0.90 > 0.75 -> invalid"""
85
+ contractions = _make_vcp_contractions([20, 18])
86
+ result = _validate_vcp(contractions, total_days=120)
87
+ assert result["valid"] is False
88
+ assert any("0.75" in issue for issue in result["issues"])
89
+
90
+ def test_borderline_ratio_075(self):
91
+ """T1=20%, T2=15% -> ratio 0.75 -> valid (exactly at threshold)"""
92
+ contractions = _make_vcp_contractions([20, 15])
93
+ result = _validate_vcp(contractions, total_days=120)
94
+ assert result["valid"] is True
95
+
96
+ def test_ratio_076_invalid(self):
97
+ """T1=20%, T2=15.2% -> ratio 0.76 -> invalid"""
98
+ contractions = _make_vcp_contractions([20, 15.2])
99
+ result = _validate_vcp(contractions, total_days=120)
100
+ assert result["valid"] is False
101
+
102
+ def test_expanding_contractions_invalid(self):
103
+ """T1=10%, T2=15% -> ratio 1.5 -> invalid"""
104
+ contractions = _make_vcp_contractions([10, 15])
105
+ result = _validate_vcp(contractions, total_days=120)
106
+ assert result["valid"] is False
107
+
108
+ def test_single_contraction_too_few(self):
109
+ """Single contraction is not enough for VCP."""
110
+ contractions = _make_vcp_contractions([20])
111
+ result = _validate_vcp(contractions, total_days=120)
112
+ assert result["valid"] is False
113
+
114
+ def test_t1_too_shallow(self):
115
+ """T1=5% is below 8% minimum -> invalid"""
116
+ contractions = _make_vcp_contractions([5, 3])
117
+ result = _validate_vcp(contractions, total_days=120)
118
+ assert result["valid"] is False
119
+
120
+ def test_four_progressive_contractions(self):
121
+ """T1=30%, T2=15%, T3=7%, T4=3% -> valid textbook"""
122
+ contractions = _make_vcp_contractions([30, 15, 7, 3])
123
+ result = _validate_vcp(contractions, total_days=120)
124
+ assert result["valid"] is True
125
+
126
+
127
+ # ===========================================================================
128
+ # Stale Price (Acquisition) Filter Tests
129
+ # ===========================================================================
130
+
131
+
132
+ class TestStalePrice:
133
+ """Test is_stale_price() - detects acquired/pinned stocks."""
134
+
135
+ def test_stale_flat_price(self):
136
+ """Daily range < 1% for 10 days -> stale."""
137
+ prices = []
138
+ for i in range(20):
139
+ prices.append({
140
+ "date": f"2026-01-{20-i:02d}",
141
+ "open": 14.31, "high": 14.35, "low": 14.28,
142
+ "close": 14.31, "volume": 500000,
143
+ })
144
+ assert is_stale_price(prices) is True
145
+
146
+ def test_normal_price_action(self):
147
+ """Normal volatility -> not stale."""
148
+ prices = []
149
+ for i in range(20):
150
+ base = 100.0 + i * 0.5
151
+ prices.append({
152
+ "date": f"2026-01-{20-i:02d}",
153
+ "open": base, "high": base * 1.02, "low": base * 0.98,
154
+ "close": base + 0.3, "volume": 1000000,
155
+ })
156
+ assert is_stale_price(prices) is False
157
+
158
+ def test_insufficient_data(self):
159
+ """Less than lookback days -> not stale (let other filters handle)."""
160
+ prices = [{"date": "2026-01-01", "high": 10, "low": 10, "close": 10}]
161
+ assert is_stale_price(prices) is False
162
+
163
+
164
+ # ===========================================================================
165
+ # Trend Template Tests (Fix 5: C3 conservative with limited data)
166
+ # ===========================================================================
167
+
168
+
169
+ class TestTrendTemplate:
170
+ """Test Trend Template scoring."""
171
+
172
+ def test_insufficient_data(self):
173
+ prices = _make_prices(30)
174
+ quote = {"price": 100, "yearHigh": 110, "yearLow": 50}
175
+ result = calculate_trend_template(prices, quote)
176
+ assert result["score"] == 0
177
+ assert result["passed"] is False
178
+
179
+ def test_c3_fails_with_200_days(self):
180
+ """With exactly 200 days, C3 should fail (cannot verify 22d SMA200 trend)."""
181
+ prices = _make_prices(210, start=100, daily_change=0.001)
182
+ quote = {"price": 120, "yearHigh": 125, "yearLow": 80}
183
+ result = calculate_trend_template(prices, quote, rs_rank=85)
184
+ c3 = result["criteria"].get("c3_sma200_trending_up", {})
185
+ assert c3["passed"] is False
186
+
187
+ def test_c3_passes_with_222_days(self):
188
+ """With 222+ days and uptrend, C3 should pass."""
189
+ prices = _make_prices(250, start=80, daily_change=0.001)
190
+ quote = {"price": 120, "yearHigh": 125, "yearLow": 70}
191
+ result = calculate_trend_template(prices, quote, rs_rank=85)
192
+ # C3 should be evaluated (may pass or fail depending on synthetic data)
193
+ c3 = result["criteria"].get("c3_sma200_trending_up", {})
194
+ assert "Cannot verify" not in c3.get("detail", "")
195
+
196
+
197
+ # ===========================================================================
198
+ # Volume Pattern Tests
199
+ # ===========================================================================
200
+
201
+
202
+ class TestVolumePattern:
203
+ def test_insufficient_data(self):
204
+ result = calculate_volume_pattern([])
205
+ assert result["score"] == 0
206
+ assert "Insufficient" in result["error"]
207
+
208
+ def test_low_dry_up_ratio(self):
209
+ """Recent volume much lower than 50d avg -> high score."""
210
+ prices = _make_prices(60, volume=1000000)
211
+ # Override recent 10 bars with low volume
212
+ for i in range(10):
213
+ prices[i]["volume"] = 200000
214
+ result = calculate_volume_pattern(prices)
215
+ assert result["dry_up_ratio"] < 0.3
216
+ assert result["score"] >= 80
217
+
218
+
219
+ # ===========================================================================
220
+ # Pivot Proximity Tests
221
+ # ===========================================================================
222
+
223
+
224
+ class TestPivotProximity:
225
+ def test_no_pivot(self):
226
+ result = calculate_pivot_proximity(100.0, None)
227
+ assert result["score"] == 0
228
+
229
+ def test_breakout_confirmed(self):
230
+ """0-3% above with volume -> base 90 + bonus 10 = 100, BREAKOUT CONFIRMED."""
231
+ result = calculate_pivot_proximity(
232
+ 102.0, 100.0, last_contraction_low=95.0, breakout_volume=True
233
+ )
234
+ assert result["score"] == 100
235
+ assert result["trade_status"] == "BREAKOUT CONFIRMED"
236
+
237
+ def test_at_pivot(self):
238
+ result = calculate_pivot_proximity(99.0, 100.0, last_contraction_low=95.0)
239
+ assert result["score"] == 90
240
+ assert "AT PIVOT" in result["trade_status"]
241
+
242
+ def test_far_below_pivot(self):
243
+ result = calculate_pivot_proximity(80.0, 100.0)
244
+ assert result["score"] == 10
245
+
246
+ def test_below_stop_level(self):
247
+ result = calculate_pivot_proximity(90.0, 100.0, last_contraction_low=95.0)
248
+ assert "BELOW STOP LEVEL" in result["trade_status"]
249
+
250
+ def test_extended_above_pivot_7pct(self):
251
+ """7% above pivot (no volume) -> score=50, High chase risk."""
252
+ result = calculate_pivot_proximity(107.0, 100.0, last_contraction_low=95.0)
253
+ assert result["score"] == 50
254
+ assert "High chase risk" in result["trade_status"]
255
+
256
+ def test_extended_above_pivot_25pct(self):
257
+ """25% above pivot -> score=20, OVEREXTENDED."""
258
+ result = calculate_pivot_proximity(125.0, 100.0, last_contraction_low=95.0)
259
+ assert result["score"] == 20
260
+ assert "OVEREXTENDED" in result["trade_status"]
261
+
262
+ def test_near_above_pivot_2pct(self):
263
+ """2% above pivot (no volume) -> score=90, ABOVE PIVOT."""
264
+ result = calculate_pivot_proximity(102.0, 100.0, last_contraction_low=95.0)
265
+ assert result["score"] == 90
266
+ assert "ABOVE PIVOT" in result["trade_status"]
267
+
268
+ # --- New distance-priority tests ---
269
+
270
+ def test_breakout_volume_no_override_at_33pct(self):
271
+ """+33.5% above, volume=True -> score=20 (distance priority, no bonus >5%)."""
272
+ result = calculate_pivot_proximity(
273
+ 133.5, 100.0, last_contraction_low=95.0, breakout_volume=True
274
+ )
275
+ assert result["score"] == 20
276
+ assert "OVEREXTENDED" in result["trade_status"]
277
+
278
+ def test_breakout_volume_bonus_at_2pct(self):
279
+ """+2% above, volume=True -> base 90 + bonus 10 = 100."""
280
+ result = calculate_pivot_proximity(
281
+ 102.0, 100.0, last_contraction_low=95.0, breakout_volume=True
282
+ )
283
+ assert result["score"] == 100
284
+ assert result["trade_status"] == "BREAKOUT CONFIRMED"
285
+
286
+ def test_breakout_volume_bonus_at_4pct(self):
287
+ """+4% above, volume=True -> base 65 + bonus 10 = 75."""
288
+ result = calculate_pivot_proximity(
289
+ 104.0, 100.0, last_contraction_low=95.0, breakout_volume=True
290
+ )
291
+ assert result["score"] == 75
292
+ assert "vol confirmed" in result["trade_status"]
293
+
294
+ def test_breakout_volume_no_bonus_at_7pct(self):
295
+ """+7% above, volume=True -> score=50 (no bonus >5%)."""
296
+ result = calculate_pivot_proximity(
297
+ 107.0, 100.0, last_contraction_low=95.0, breakout_volume=True
298
+ )
299
+ assert result["score"] == 50
300
+ assert "High chase risk" in result["trade_status"]
301
+
302
+
303
+ # ===========================================================================
304
+ # Relative Strength Tests
305
+ # ===========================================================================
306
+
307
+
308
+ class TestRelativeStrength:
309
+ def test_insufficient_stock_data(self):
310
+ result = calculate_relative_strength([], [])
311
+ assert result["score"] == 0
312
+
313
+ def test_outperformer(self):
314
+ # Stock up 30%, SP500 up 5% over 3 months
315
+ stock = _make_prices(70, start=77, daily_change=0.003)
316
+ sp500 = _make_prices(70, start=95, daily_change=0.0005)
317
+ result = calculate_relative_strength(stock, sp500)
318
+ assert result["score"] >= 60 # should outperform
319
+
320
+
321
+ # ===========================================================================
322
+ # Entry Ready Tests
323
+ # ===========================================================================
324
+
325
+
326
+ class TestEntryReady:
327
+ """Test compute_entry_ready() from screen_vcp module."""
328
+
329
+ def _make_result(
330
+ self,
331
+ valid_vcp=True,
332
+ distance_from_pivot_pct=-1.0,
333
+ dry_up_ratio=0.5,
334
+ risk_pct=5.0,
335
+ ):
336
+ """Build a minimal analysis result dict for compute_entry_ready()."""
337
+ return {
338
+ "valid_vcp": valid_vcp,
339
+ "distance_from_pivot_pct": distance_from_pivot_pct,
340
+ "volume_pattern": {"dry_up_ratio": dry_up_ratio},
341
+ "pivot_proximity": {"risk_pct": risk_pct},
342
+ }
343
+
344
+ def test_entry_ready_ideal_candidate(self):
345
+ """valid_vcp=True, distance=-1%, dry_up=0.5, risk=5% -> True."""
346
+ result = self._make_result(
347
+ valid_vcp=True, distance_from_pivot_pct=-1.0,
348
+ dry_up_ratio=0.5, risk_pct=5.0,
349
+ )
350
+ assert compute_entry_ready(result) is True
351
+
352
+ def test_entry_ready_false_extended(self):
353
+ """valid_vcp=True, distance=+15% -> False (too far above pivot)."""
354
+ result = self._make_result(
355
+ valid_vcp=True, distance_from_pivot_pct=15.0,
356
+ dry_up_ratio=0.5, risk_pct=5.0,
357
+ )
358
+ assert compute_entry_ready(result) is False
359
+
360
+ def test_entry_ready_false_invalid_vcp(self):
361
+ """valid_vcp=False -> False regardless of distance."""
362
+ result = self._make_result(
363
+ valid_vcp=False, distance_from_pivot_pct=-1.0,
364
+ dry_up_ratio=0.5, risk_pct=5.0,
365
+ )
366
+ assert compute_entry_ready(result) is False
367
+
368
+ def test_entry_ready_false_high_risk(self):
369
+ """valid_vcp=True, distance=-1%, risk=20% -> False (risk too high)."""
370
+ result = self._make_result(
371
+ valid_vcp=True, distance_from_pivot_pct=-1.0,
372
+ dry_up_ratio=0.5, risk_pct=20.0,
373
+ )
374
+ assert compute_entry_ready(result) is False
375
+
376
+ def test_entry_ready_custom_max_above_pivot(self):
377
+ """CLI --max-above-pivot=5.0 allows +4% above pivot."""
378
+ result = self._make_result(distance_from_pivot_pct=4.0)
379
+ assert compute_entry_ready(result, max_above_pivot=5.0) is True
380
+ assert compute_entry_ready(result, max_above_pivot=3.0) is False
381
+
382
+ def test_entry_ready_custom_max_risk(self):
383
+ """CLI --max-risk=10.0 rejects risk=12%."""
384
+ result = self._make_result(risk_pct=12.0)
385
+ assert compute_entry_ready(result, max_risk=15.0) is True
386
+ assert compute_entry_ready(result, max_risk=10.0) is False
387
+
388
+ def test_entry_ready_no_require_valid_vcp(self):
389
+ """CLI --no-require-valid-vcp allows invalid VCP."""
390
+ result = self._make_result(valid_vcp=False)
391
+ assert compute_entry_ready(result, require_valid_vcp=True) is False
392
+ assert compute_entry_ready(result, require_valid_vcp=False) is True
393
+
394
+
395
+ # ===========================================================================
396
+ # Scorer Tests
397
+ # ===========================================================================
398
+
399
+
400
+ class TestScorer:
401
+ def test_textbook_rating(self):
402
+ result = calculate_composite_score(100, 100, 100, 100, 100)
403
+ assert result["composite_score"] == 100
404
+ assert result["rating"] == "Textbook VCP"
405
+
406
+ def test_no_vcp_rating(self):
407
+ result = calculate_composite_score(0, 0, 0, 0, 0)
408
+ assert result["composite_score"] == 0
409
+ assert result["rating"] == "No VCP"
410
+
411
+ def test_weights_sum_to_100(self):
412
+ """Verify component weights sum to 1.0"""
413
+ from scorer import COMPONENT_WEIGHTS
414
+
415
+ total = sum(COMPONENT_WEIGHTS.values())
416
+ assert abs(total - 1.0) < 0.001
417
+
418
+ def test_valid_vcp_false_caps_rating(self):
419
+ """valid_vcp=False with composite>=70 -> rating capped to 'Developing VCP'."""
420
+ # Scores: 80*0.25 + 70*0.25 + 70*0.20 + 70*0.15 + 70*0.15 = 72.5
421
+ result = calculate_composite_score(80, 70, 70, 70, 70, valid_vcp=False)
422
+ assert result["composite_score"] >= 70
423
+ assert result["rating"] == "Developing VCP"
424
+ assert "not confirmed" in result["rating_description"].lower()
425
+ assert result["valid_vcp"] is False
426
+
427
+ def test_valid_vcp_true_no_cap(self):
428
+ """valid_vcp=True with composite>=70 -> normal rating (Good VCP)."""
429
+ result = calculate_composite_score(80, 70, 70, 70, 70, valid_vcp=True)
430
+ assert result["composite_score"] >= 70
431
+ assert result["rating"] == "Good VCP"
432
+ assert result["valid_vcp"] is True
433
+
434
+ def test_valid_vcp_false_low_score_no_effect(self):
435
+ """valid_vcp=False with composite<70 -> no cap needed, normal rating."""
436
+ # Scores: 60*0.25 + 50*0.25 + 50*0.20 + 50*0.15 + 50*0.15 = 52.5
437
+ result = calculate_composite_score(60, 50, 50, 50, 50, valid_vcp=False)
438
+ assert result["composite_score"] < 70
439
+ assert result["rating"] == "Weak VCP"
440
+ assert result["valid_vcp"] is False
441
+
442
+
443
+ # ===========================================================================
444
+ # Report Generator Tests (Fix 2: market_cap=None, Fix 3/4: summary counts)
445
+ # ===========================================================================
446
+
447
+
448
+ class TestReportGenerator:
449
+ def _make_stock(self, symbol="TEST", score=75.0, market_cap=50e9, rating=None):
450
+ if rating is None:
451
+ if score >= 90:
452
+ rating = "Textbook VCP"
453
+ elif score >= 80:
454
+ rating = "Strong VCP"
455
+ elif score >= 70:
456
+ rating = "Good VCP"
457
+ elif score >= 60:
458
+ rating = "Developing VCP"
459
+ elif score >= 50:
460
+ rating = "Weak VCP"
461
+ else:
462
+ rating = "No VCP"
463
+ return {
464
+ "symbol": symbol,
465
+ "company_name": f"{symbol} Corp",
466
+ "sector": "Technology",
467
+ "price": 150.0,
468
+ "market_cap": market_cap,
469
+ "composite_score": score,
470
+ "rating": rating,
471
+ "rating_description": "Solid VCP",
472
+ "guidance": "Buy on volume confirmation",
473
+ "weakest_component": "Volume",
474
+ "weakest_score": 40,
475
+ "strongest_component": "Trend",
476
+ "strongest_score": 100,
477
+ "trend_template": {"score": 100, "criteria_passed": 7},
478
+ "vcp_pattern": {
479
+ "score": 70,
480
+ "num_contractions": 2,
481
+ "contractions": [],
482
+ "pivot_price": 145.0,
483
+ },
484
+ "volume_pattern": {"score": 40, "dry_up_ratio": 0.8},
485
+ "pivot_proximity": {
486
+ "score": 75,
487
+ "distance_from_pivot_pct": -3.0,
488
+ "stop_loss_price": 140.0,
489
+ "risk_pct": 7.0,
490
+ "trade_status": "NEAR PIVOT",
491
+ },
492
+ "relative_strength": {"score": 80, "rs_rank_estimate": 80, "weighted_rs": 15.0},
493
+ }
494
+
495
+ def test_market_cap_none(self):
496
+ """market_cap=None should not crash."""
497
+ with tempfile.TemporaryDirectory() as tmpdir:
498
+ stock = self._make_stock(market_cap=None)
499
+ md_file = os.path.join(tmpdir, "test.md")
500
+ metadata = {
501
+ "generated_at": "2026-01-01",
502
+ "universe_description": "Test",
503
+ "funnel": {},
504
+ "api_stats": {},
505
+ }
506
+ generate_markdown_report([stock], metadata, md_file)
507
+ with open(md_file) as f:
508
+ content = f.read()
509
+ assert "N/A" in content # market cap should show N/A
510
+
511
+ def test_summary_uses_all_results(self):
512
+ """Summary should count all candidates, not just top N."""
513
+ all_results = [self._make_stock(f"S{i}", score=90 - i * 5) for i in range(10)]
514
+ top_results = all_results[:3]
515
+ metadata = {
516
+ "generated_at": "2026-01-01",
517
+ "universe_description": "Test",
518
+ "funnel": {"vcp_candidates": 10},
519
+ "api_stats": {},
520
+ }
521
+ with tempfile.TemporaryDirectory() as tmpdir:
522
+ json_file = os.path.join(tmpdir, "test.json")
523
+ generate_json_report(top_results, metadata, json_file, all_results=all_results)
524
+ with open(json_file) as f:
525
+ data = json.load(f)
526
+ assert data["summary"]["total"] == 10
527
+ assert len(data["results"]) == 3
528
+
529
+ def test_market_cap_zero(self):
530
+ """market_cap=0 should show N/A."""
531
+ with tempfile.TemporaryDirectory() as tmpdir:
532
+ stock = self._make_stock(market_cap=0)
533
+ md_file = os.path.join(tmpdir, "test.md")
534
+ metadata = {
535
+ "generated_at": "2026-01-01",
536
+ "universe_description": "Test",
537
+ "funnel": {},
538
+ "api_stats": {},
539
+ }
540
+ generate_markdown_report([stock], metadata, md_file)
541
+ with open(md_file) as f:
542
+ content = f.read()
543
+ assert "N/A" in content
544
+
545
+ def test_top_greater_than_20(self):
546
+ """--top=25 should produce 25 entries in Markdown, not capped at 20."""
547
+ stocks = [self._make_stock(f"S{i:02d}", score=95 - i) for i in range(25)]
548
+ metadata = {
549
+ "generated_at": "2026-01-01",
550
+ "universe_description": "Test",
551
+ "funnel": {"vcp_candidates": 25},
552
+ "api_stats": {},
553
+ }
554
+ with tempfile.TemporaryDirectory() as tmpdir:
555
+ md_file = os.path.join(tmpdir, "test.md")
556
+ generate_markdown_report(stocks, metadata, md_file)
557
+ with open(md_file) as f:
558
+ content = f.read()
559
+ # All 25 stocks should appear in Section A or B
560
+ assert "Section A:" in content or "Section B:" in content
561
+ for i in range(25):
562
+ assert f"S{i:02d}" in content
563
+
564
+ def test_report_two_sections(self):
565
+ """Report splits into Pre-Breakout Watchlist and Extended sections."""
566
+ entry_ready_stock = self._make_stock("READY", score=80.0, rating="Strong VCP")
567
+ entry_ready_stock["entry_ready"] = True
568
+ entry_ready_stock["distance_from_pivot_pct"] = -1.0
569
+
570
+ extended_stock = self._make_stock("EXTENDED", score=75.0, rating="Good VCP")
571
+ extended_stock["entry_ready"] = False
572
+ extended_stock["distance_from_pivot_pct"] = 15.0
573
+
574
+ results = [entry_ready_stock, extended_stock]
575
+ metadata = {
576
+ "generated_at": "2026-01-01",
577
+ "universe_description": "Test",
578
+ "funnel": {"vcp_candidates": 2},
579
+ "api_stats": {},
580
+ }
581
+ with tempfile.TemporaryDirectory() as tmpdir:
582
+ md_file = os.path.join(tmpdir, "test.md")
583
+ generate_markdown_report(results, metadata, md_file)
584
+ with open(md_file) as f:
585
+ content = f.read()
586
+ assert "Pre-Breakout Watchlist" in content
587
+ assert "Extended / Quality VCP" in content
588
+ assert "READY" in content
589
+ assert "EXTENDED" in content
590
+
591
+ def test_summary_counts_by_rating_not_score(self):
592
+ """Summary should use rating field, not composite_score.
593
+
594
+ A stock with composite=72 but rating='Developing VCP' (valid_vcp cap)
595
+ must count as developing, not good.
596
+ """
597
+ from report_generator import _generate_summary
598
+
599
+ results = [
600
+ # Normal: composite=75, rating=Good VCP
601
+ self._make_stock("GOOD1", score=75.0, rating="Good VCP"),
602
+ # Capped: composite=72 but valid_vcp=False -> Developing VCP
603
+ self._make_stock("CAPPED", score=72.0, rating="Developing VCP"),
604
+ # Normal developing
605
+ self._make_stock("DEV1", score=65.0, rating="Developing VCP"),
606
+ # Weak
607
+ self._make_stock("WEAK1", score=55.0, rating="Weak VCP"),
608
+ ]
609
+
610
+ summary = _generate_summary(results)
611
+ assert summary["total"] == 4
612
+ assert summary["good"] == 1 # only GOOD1
613
+ assert summary["developing"] == 2 # CAPPED + DEV1
614
+ assert summary["weak"] == 1 # WEAK1
615
+ assert summary["textbook"] == 0
616
+ assert summary["strong"] == 0
617
+
618
+
619
+ # ===========================================================================
620
+ # SMA50 Extended Penalty Tests
621
+ # ===========================================================================
622
+
623
+
624
+ class TestSMA50ExtendedPenalty:
625
+ """Test extended penalty applied to trend template score."""
626
+
627
+ def _make_stage2_prices(self, n=250, sma50_target=100.0, price=None):
628
+ """Build synthetic prices where SMA50 ≈ sma50_target.
629
+
630
+ All prices are constant at sma50_target so SMA50 = sma50_target exactly.
631
+ The quote price is set separately to control distance.
632
+ """
633
+ prices = []
634
+ for i in range(n):
635
+ prices.append({
636
+ "date": f"2025-{(i // 22) + 1:02d}-{(i % 22) + 1:02d}",
637
+ "open": sma50_target,
638
+ "high": sma50_target * 1.005,
639
+ "low": sma50_target * 0.995,
640
+ "close": sma50_target,
641
+ "adjClose": sma50_target,
642
+ "volume": 1000000,
643
+ })
644
+ return prices
645
+
646
+ def _run_tt(self, distance_pct, ext_threshold=8.0):
647
+ """Run calculate_trend_template with a given SMA50 distance %.
648
+
649
+ Returns the result dict.
650
+ """
651
+ sma50_target = 100.0
652
+ price = sma50_target * (1 + distance_pct / 100)
653
+ prices = self._make_stage2_prices(n=250, sma50_target=sma50_target)
654
+ quote = {
655
+ "price": price,
656
+ "yearHigh": price * 1.05,
657
+ "yearLow": sma50_target * 0.6,
658
+ }
659
+ return calculate_trend_template(
660
+ prices, quote, rs_rank=85, ext_threshold=ext_threshold,
661
+ )
662
+
663
+ # --- Penalty calculation ---
664
+
665
+ def test_no_penalty_within_8pct(self):
666
+ result = self._run_tt(5.0)
667
+ assert result["extended_penalty"] == 0
668
+
669
+ def test_penalty_at_10pct_distance(self):
670
+ result = self._run_tt(10.0)
671
+ assert result["extended_penalty"] == -5
672
+
673
+ def test_penalty_at_15pct_distance(self):
674
+ result = self._run_tt(15.0)
675
+ assert result["extended_penalty"] == -10
676
+
677
+ def test_penalty_at_20pct_distance(self):
678
+ result = self._run_tt(20.0)
679
+ assert result["extended_penalty"] == -15
680
+
681
+ def test_penalty_at_30pct_distance(self):
682
+ result = self._run_tt(30.0)
683
+ assert result["extended_penalty"] == -20
684
+
685
+ def test_penalty_floor_at_zero(self):
686
+ """Penalty cannot make score negative (max(0, raw + penalty))."""
687
+ # Recent 50 at 80, older 200 at 120 → SMA50=80, SMA150≈107, SMA200≈110
688
+ # Price=105: above SMA50 by ~31% (penalty=-20) but below SMA150 (C1 fail)
689
+ # Only C4 passes → raw_score=14.3, 14.3+(-20)=-5.7 → floor to 0
690
+ n = 250
691
+ prices = []
692
+ for i in range(n):
693
+ close = 80.0 if i < 50 else 120.0
694
+ prices.append({
695
+ "date": f"2025-{(i // 22) + 1:02d}-{(i % 22) + 1:02d}",
696
+ "open": close, "high": close * 1.005,
697
+ "low": close * 0.995, "close": close,
698
+ "adjClose": close, "volume": 1000000,
699
+ })
700
+ quote = {"price": 105.0, "yearHigh": 200.0, "yearLow": 100.0}
701
+ result = calculate_trend_template(prices, quote, rs_rank=10)
702
+ assert result["extended_penalty"] == -20
703
+ assert result["raw_score"] <= 14.3
704
+ assert result["score"] == 0
705
+
706
+ def test_price_below_sma50_no_penalty(self):
707
+ result = self._run_tt(-5.0)
708
+ assert result["extended_penalty"] == 0
709
+
710
+ # --- Boundary tests (R1-4) ---
711
+
712
+ def test_boundary_exactly_8pct(self):
713
+ result = self._run_tt(8.0)
714
+ assert result["extended_penalty"] == -5
715
+
716
+ def test_boundary_exactly_12pct(self):
717
+ result = self._run_tt(12.0)
718
+ assert result["extended_penalty"] == -10
719
+
720
+ def test_boundary_exactly_18pct(self):
721
+ result = self._run_tt(18.0)
722
+ assert result["extended_penalty"] == -15
723
+
724
+ def test_boundary_exactly_25pct(self):
725
+ result = self._run_tt(25.0)
726
+ assert result["extended_penalty"] == -20
727
+
728
+ # --- Gate separation (R1-1: most important) ---
729
+
730
+ def test_passed_uses_raw_score_not_adjusted(self):
731
+ """raw >= 85, ext < 0 -> passed=True (raw >= 85), score < raw."""
732
+ # Build uptrending data (most-recent-first) so most criteria pass
733
+ n = 250
734
+ prices = []
735
+ for i in range(n):
736
+ # index 0 = newest (highest), index 249 = oldest (lowest)
737
+ base = 120 - 40 * i / (n - 1) # 120 → 80
738
+ prices.append({
739
+ "date": f"2025-{(i // 22) + 1:02d}-{(i % 22) + 1:02d}",
740
+ "open": base, "high": base * 1.005,
741
+ "low": base * 0.995, "close": base,
742
+ "adjClose": base, "volume": 1000000,
743
+ })
744
+ # SMA50 ≈ avg of newest 50 prices (120 down to ~112)
745
+ sma50_approx = sum(p["close"] for p in prices[:50]) / 50
746
+ price = sma50_approx * 1.20 # 20% above SMA50
747
+ quote = {
748
+ "price": price,
749
+ "yearHigh": price * 1.02,
750
+ "yearLow": 60.0,
751
+ }
752
+ result = calculate_trend_template(prices, quote, rs_rank=85)
753
+ assert result["raw_score"] >= 85
754
+ assert result["passed"] is True
755
+ assert result["extended_penalty"] < 0
756
+ assert result["score"] < result["raw_score"]
757
+
758
+ def test_raw_score_in_result(self):
759
+ result = self._run_tt(10.0)
760
+ assert "raw_score" in result
761
+
762
+ def test_score_is_adjusted(self):
763
+ result = self._run_tt(15.0)
764
+ assert result["score"] == max(0, result["raw_score"] + result["extended_penalty"])
765
+
766
+ # --- Output fields ---
767
+
768
+ def test_sma50_distance_in_result(self):
769
+ result = self._run_tt(10.0)
770
+ assert "sma50_distance_pct" in result
771
+ assert result["sma50_distance_pct"] is not None
772
+ assert abs(result["sma50_distance_pct"] - 10.0) < 0.5
773
+
774
+ def test_extended_penalty_in_result(self):
775
+ result = self._run_tt(10.0)
776
+ assert "extended_penalty" in result
777
+
778
+ # --- Custom threshold (R1-3) ---
779
+
780
+ def test_custom_threshold_5pct(self):
781
+ result = self._run_tt(6.0, ext_threshold=5.0)
782
+ assert result["extended_penalty"] == -5
783
+
784
+ def test_custom_threshold_15pct(self):
785
+ result = self._run_tt(10.0, ext_threshold=15.0)
786
+ assert result["extended_penalty"] == 0
787
+
788
+
789
+ # ===========================================================================
790
+ # E2E Threshold Passthrough Test (R2-7)
791
+ # ===========================================================================
792
+
793
+
794
+ class TestExtThresholdE2E:
795
+ """Test that ext_threshold passes through analyze_stock to trend_template."""
796
+
797
+ def test_ext_threshold_passes_through_to_trend_template(self):
798
+ """analyze_stock(ext_threshold=15) uses 15% threshold for penalty."""
799
+ sma50_target = 100.0
800
+ n = 250
801
+ prices = []
802
+ for i in range(n):
803
+ prices.append({
804
+ "date": f"2025-{(i // 22) + 1:02d}-{(i % 22) + 1:02d}",
805
+ "open": sma50_target,
806
+ "high": sma50_target * 1.005,
807
+ "low": sma50_target * 0.995,
808
+ "close": sma50_target,
809
+ "adjClose": sma50_target,
810
+ "volume": 1000000,
811
+ })
812
+ # Price is 12% above SMA50
813
+ price = sma50_target * 1.12
814
+ quote = {
815
+ "price": price,
816
+ "yearHigh": price * 1.05,
817
+ "yearLow": sma50_target * 0.6,
818
+ }
819
+ sp500 = _make_prices(n, start=95, daily_change=0.0005)
820
+
821
+ # Default threshold=8 -> 12% distance -> penalty=-10
822
+ result_default = analyze_stock(
823
+ "TEST", prices, quote, sp500, "Tech", "Test Corp",
824
+ )
825
+ tt_default = result_default["trend_template"]
826
+ assert tt_default["extended_penalty"] == -10
827
+
828
+ # Custom threshold=15 -> 12% distance -> no penalty
829
+ result_custom = analyze_stock(
830
+ "TEST", prices, quote, sp500, "Tech", "Test Corp",
831
+ ext_threshold=15.0,
832
+ )
833
+ tt_custom = result_custom["trend_template"]
834
+ assert tt_custom["extended_penalty"] == 0