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,311 @@
1
+ """Tests for post_ftd_monitor.py — distribution counting, invalidation, quality score."""
2
+
3
+ from helpers import make_bar, make_rally_history
4
+ from post_ftd_monitor import (
5
+ assess_post_ftd_health,
6
+ calculate_ftd_quality_score,
7
+ check_ftd_invalidation,
8
+ count_post_ftd_distribution,
9
+ )
10
+
11
+ # ─── Helpers ──────────────────────────────────────────────────────────────────
12
+
13
+
14
+ def _build_post_ftd_bars(ftd_close=100.0, ftd_low=99.5, ftd_volume=1_000_000, post_days=None):
15
+ """
16
+ Build a minimal history around an FTD day for post-FTD testing.
17
+
18
+ Args:
19
+ post_days: List of (close, volume) tuples for days after FTD.
20
+
21
+ Returns (history, ftd_idx).
22
+ """
23
+ if post_days is None:
24
+ post_days = []
25
+
26
+ bars = []
27
+ # Pre-FTD bar (needed for volume comparison)
28
+ bars.append(
29
+ make_bar(ftd_close * 0.99, volume=ftd_volume, date="pre-ftd", low=ftd_close * 0.985)
30
+ )
31
+ # FTD bar
32
+ bars.append(make_bar(ftd_close, volume=ftd_volume, date="ftd-day", low=ftd_low))
33
+ ftd_idx = 1
34
+
35
+ # Post-FTD bars
36
+ for i, (close, volume) in enumerate(post_days):
37
+ bars.append(make_bar(close, volume=volume, date=f"post-{i + 1}", low=close * 0.997))
38
+
39
+ return bars, ftd_idx
40
+
41
+
42
+ # ─── Test 1: Distribution day counting ───────────────────────────────────────
43
+
44
+
45
+ class TestCountPostFTDDistribution:
46
+ def test_distribution_day_detected(self):
47
+ """Day 1 post-FTD: down on higher volume -> count=1, earliest=1."""
48
+ bars, ftd_idx = _build_post_ftd_bars(
49
+ ftd_close=100,
50
+ ftd_volume=1_000_000,
51
+ post_days=[
52
+ (99.5, 1_200_000), # Day 1: -0.5% on higher volume = distribution
53
+ ],
54
+ )
55
+ result = count_post_ftd_distribution(bars, ftd_idx)
56
+ assert result["distribution_count"] == 1
57
+ assert result["earliest_distribution_day"] == 1
58
+ assert result["days_monitored"] == 1
59
+
60
+ def test_no_distribution_clean_days(self):
61
+ """5 clean days (up or down on lower volume) -> count=0."""
62
+ bars, ftd_idx = _build_post_ftd_bars(
63
+ ftd_close=100,
64
+ ftd_volume=1_000_000,
65
+ post_days=[
66
+ (100.5, 900_000), # Day 1: up, lower vol
67
+ (101.0, 950_000), # Day 2: up, lower vol
68
+ (100.8, 800_000), # Day 3: down but lower volume
69
+ (101.5, 850_000), # Day 4: up, lower vol
70
+ (102.0, 900_000), # Day 5: up, lower vol
71
+ ],
72
+ )
73
+ result = count_post_ftd_distribution(bars, ftd_idx)
74
+ assert result["distribution_count"] == 0
75
+ assert result["days_monitored"] == 5
76
+ assert result["earliest_distribution_day"] is None
77
+
78
+ def test_multiple_distribution_days(self):
79
+ """Days 2 and 4 are distribution -> count=2, earliest=2."""
80
+ bars, ftd_idx = _build_post_ftd_bars(
81
+ ftd_close=100,
82
+ ftd_volume=1_000_000,
83
+ post_days=[
84
+ (100.5, 900_000), # Day 1: up
85
+ (99.0, 1_100_000), # Day 2: down on higher vol = dist
86
+ (99.5, 800_000), # Day 3: up, lower vol
87
+ (98.5, 1_300_000), # Day 4: down on higher vol = dist
88
+ (99.0, 900_000), # Day 5: up, lower vol
89
+ ],
90
+ )
91
+ result = count_post_ftd_distribution(bars, ftd_idx)
92
+ assert result["distribution_count"] == 2
93
+ assert result["earliest_distribution_day"] == 2
94
+
95
+ def test_ftd_at_end_of_history(self):
96
+ """FTD on last day of history -> 0 days monitored."""
97
+ bars, ftd_idx = _build_post_ftd_bars(ftd_close=100, post_days=[])
98
+ result = count_post_ftd_distribution(bars, ftd_idx)
99
+ assert result["distribution_count"] == 0
100
+ assert result["days_monitored"] == 0
101
+
102
+
103
+ # ─── Test 2: FTD invalidation ────────────────────────────────────────────────
104
+
105
+
106
+ class TestCheckFTDInvalidation:
107
+ def test_invalidated_close_below_ftd_low(self):
108
+ """Close below FTD day's low -> invalidated=True."""
109
+ bars, ftd_idx = _build_post_ftd_bars(
110
+ ftd_close=100,
111
+ ftd_low=99.0,
112
+ post_days=[
113
+ (100.5, 1_000_000), # Day 1: above
114
+ (98.5, 1_000_000), # Day 2: below FTD low 99.0
115
+ ],
116
+ )
117
+ result = check_ftd_invalidation(bars, ftd_idx)
118
+ assert result["invalidated"] is True
119
+ assert result["days_after_ftd"] == 2
120
+
121
+ def test_not_invalidated_holds_above_ftd_low(self):
122
+ """Price stays above FTD low -> invalidated=False."""
123
+ bars, ftd_idx = _build_post_ftd_bars(
124
+ ftd_close=100,
125
+ ftd_low=99.0,
126
+ post_days=[
127
+ (100.5, 1_000_000),
128
+ (99.5, 1_000_000), # Above FTD low
129
+ (101.0, 1_000_000),
130
+ ],
131
+ )
132
+ result = check_ftd_invalidation(bars, ftd_idx)
133
+ assert result["invalidated"] is False
134
+ assert result["days_since_ftd"] == 3
135
+
136
+
137
+ # ─── Test 3: Fix 3 verification — data missing -> +0 not +10 ─────────────────
138
+
139
+
140
+ class TestQualityScoreDataMissing:
141
+ def test_empty_post_ftd_gives_zero_not_ten(self):
142
+ """
143
+ Fix 3 verification: When post_ftd_distribution is empty {},
144
+ days_monitored defaults to 0 -> score adjustment should be +0.
145
+ """
146
+ market_state = {
147
+ "sp500": {
148
+ "ftd": {
149
+ "ftd_detected": True,
150
+ "ftd_day_number": 5,
151
+ "gain_pct": 1.6,
152
+ "volume_above_avg": True,
153
+ },
154
+ },
155
+ "nasdaq": {"ftd": {}},
156
+ "dual_confirmation": False,
157
+ "post_ftd_distribution": {}, # Empty — no data yet
158
+ }
159
+ result = calculate_ftd_quality_score(market_state)
160
+ breakdown = result["breakdown"]
161
+
162
+ # Must say "not yet available" and +0
163
+ assert "+0" in breakdown["post_ftd"]
164
+ assert "not yet available" in breakdown["post_ftd"].lower()
165
+
166
+ def test_monitored_days_with_no_distribution_gives_ten(self):
167
+ """When days_monitored > 0 and dist_count == 0, should give +10."""
168
+ market_state = {
169
+ "sp500": {
170
+ "ftd": {
171
+ "ftd_detected": True,
172
+ "ftd_day_number": 5,
173
+ "gain_pct": 1.6,
174
+ "volume_above_avg": True,
175
+ },
176
+ },
177
+ "nasdaq": {"ftd": {}},
178
+ "dual_confirmation": False,
179
+ "post_ftd_distribution": {
180
+ "distribution_count": 0,
181
+ "days_monitored": 3,
182
+ "earliest_distribution_day": None,
183
+ },
184
+ }
185
+ result = calculate_ftd_quality_score(market_state)
186
+ breakdown = result["breakdown"]
187
+ assert "+10" in breakdown["post_ftd"]
188
+ assert "3 days clean" in breakdown["post_ftd"]
189
+
190
+
191
+ # ─── Test 4: Quality score full ──────────────────────────────────────────────
192
+
193
+
194
+ class TestQualityScoreFull:
195
+ def test_maximum_score(self):
196
+ """Day 5 + 2.0% gain + above avg vol + dual + clean 5 days -> 100."""
197
+ market_state = {
198
+ "sp500": {
199
+ "ftd": {
200
+ "ftd_detected": True,
201
+ "ftd_day_number": 5,
202
+ "gain_pct": 2.0,
203
+ "volume_above_avg": True,
204
+ },
205
+ },
206
+ "nasdaq": {
207
+ "ftd": {
208
+ "ftd_detected": True,
209
+ "ftd_day_number": 6,
210
+ "gain_pct": 1.8,
211
+ "volume_above_avg": True,
212
+ },
213
+ },
214
+ "dual_confirmation": True,
215
+ "post_ftd_distribution": {
216
+ "distribution_count": 0,
217
+ "days_monitored": 5,
218
+ "earliest_distribution_day": None,
219
+ },
220
+ }
221
+ result = calculate_ftd_quality_score(market_state)
222
+ # 60 (base) + 15 (gain) + 10 (volume) + 15 (dual) + 10 (clean) = 110 -> clamped to 100
223
+ assert result["total_score"] == 100
224
+
225
+ def test_minimum_score(self):
226
+ """Day 10 + 1.25% gain + below avg vol + single + dist day 2 -> low score."""
227
+ market_state = {
228
+ "sp500": {
229
+ "ftd": {
230
+ "ftd_detected": True,
231
+ "ftd_day_number": 10,
232
+ "gain_pct": 1.25,
233
+ "volume_above_avg": False,
234
+ },
235
+ },
236
+ "nasdaq": {"ftd": {}},
237
+ "dual_confirmation": False,
238
+ "post_ftd_distribution": {
239
+ "distribution_count": 1,
240
+ "days_monitored": 2,
241
+ "earliest_distribution_day": 2,
242
+ },
243
+ }
244
+ result = calculate_ftd_quality_score(market_state)
245
+ # 50 (base day 10) + 5 (gain 1.25%) + 0 (vol below) + 0 (single) - 30 (dist day 2) = 25
246
+ assert result["total_score"] == 25
247
+
248
+ def test_no_ftd_returns_zero(self):
249
+ """No FTD detected -> score 0."""
250
+ market_state = {
251
+ "sp500": {"ftd": {}},
252
+ "nasdaq": {"ftd": {}},
253
+ "dual_confirmation": False,
254
+ }
255
+ result = calculate_ftd_quality_score(market_state)
256
+ assert result["total_score"] == 0
257
+ assert result["signal"] == "No FTD"
258
+
259
+
260
+ # ─── Test 5: Fix 2 verification — dual-index consistency ─────────────────────
261
+
262
+
263
+ class TestDualIndexConsistency:
264
+ def test_assess_uses_first_ftd_index_only(self):
265
+ """
266
+ Fix 2 verification: When both indices have confirmed FTDs,
267
+ assess_post_ftd_health should use S&P 500 (first in loop) and
268
+ NOT overwrite with NASDAQ data.
269
+ """
270
+ # Build separate histories for each index
271
+ sp_bars, sp_swing, _, _ = make_rally_history(
272
+ peak=100,
273
+ decline_pct=5.0,
274
+ down_days=5,
275
+ rally_days=8,
276
+ ftd_day=5,
277
+ ftd_gain_pct=1.6,
278
+ ftd_volume_mult=1.5,
279
+ )
280
+ nq_bars, nq_swing, _, _ = make_rally_history(
281
+ peak=200,
282
+ decline_pct=6.0,
283
+ down_days=5,
284
+ rally_days=8,
285
+ ftd_day=6,
286
+ ftd_gain_pct=2.0,
287
+ ftd_volume_mult=1.8,
288
+ )
289
+
290
+ # Use get_market_state to build initial state (expects reversed/API order)
291
+ from rally_tracker import get_market_state
292
+
293
+ sp_reversed = list(reversed(sp_bars))
294
+ nq_reversed = list(reversed(nq_bars))
295
+ market_state = get_market_state(sp_reversed, nq_reversed)
296
+
297
+ # Both should have FTD
298
+ assert market_state["sp500"]["ftd"]["ftd_detected"] is True
299
+ assert market_state["nasdaq"]["ftd"]["ftd_detected"] is True
300
+
301
+ # Run post-FTD assessment
302
+ enriched = assess_post_ftd_health(market_state, sp_bars, nq_bars)
303
+
304
+ # The quality score should use S&P 500 as the source (first in loop)
305
+ qs = enriched.get("quality_score", {})
306
+ assert qs.get("ftd_source") == "S&P 500"
307
+
308
+ # post_ftd_distribution should be based on S&P 500 history
309
+ # (If Fix 2 not applied, NASDAQ would overwrite this)
310
+ dist = enriched.get("post_ftd_distribution", {})
311
+ assert "days_monitored" in dist
@@ -0,0 +1,302 @@
1
+ """Tests for rally_tracker.py — swing low, rally attempt, FTD detection."""
2
+
3
+ from helpers import make_bar, make_correction_history, make_rally_history
4
+ from rally_tracker import (
5
+ MIN_CORRECTION_PCT,
6
+ MarketState,
7
+ analyze_single_index,
8
+ detect_ftd,
9
+ find_swing_low,
10
+ get_market_state,
11
+ track_rally_attempt,
12
+ )
13
+
14
+ # ─── Test 1: Swing low detection ─────────────────────────────────────────────
15
+
16
+
17
+ class TestFindSwingLow:
18
+ def test_detects_qualifying_swing_low(self):
19
+ """5% decline + 5 down days -> swing low detected."""
20
+ bars, _ = make_correction_history(peak=100, decline_pct=5.0, down_days=5)
21
+ result = find_swing_low(bars)
22
+ assert result is not None
23
+ assert result["decline_pct"] <= -MIN_CORRECTION_PCT
24
+ assert result["down_days"] >= 3
25
+
26
+ def test_no_swing_low_shallow_decline(self):
27
+ """2% decline -> no qualifying swing low."""
28
+ bars, _ = make_correction_history(peak=100, decline_pct=2.0, down_days=4)
29
+ result = find_swing_low(bars)
30
+ assert result is None
31
+
32
+ def test_no_swing_low_insufficient_down_days(self):
33
+ """5% decline but only 2 down days -> no qualifying swing low."""
34
+ bars, _ = make_correction_history(peak=100, decline_pct=5.0, down_days=2)
35
+ result = find_swing_low(bars)
36
+ assert result is None
37
+
38
+ def test_swing_low_returns_correct_fields(self):
39
+ """Verify all expected fields are present in result."""
40
+ bars, _ = make_correction_history(peak=100, decline_pct=6.0, down_days=5)
41
+ result = find_swing_low(bars)
42
+ assert result is not None
43
+ expected_keys = [
44
+ "swing_low_idx",
45
+ "swing_low_price",
46
+ "swing_low_date",
47
+ "swing_low_low",
48
+ "recent_high_price",
49
+ "recent_high_idx",
50
+ "recent_high_date",
51
+ "decline_pct",
52
+ "down_days",
53
+ ]
54
+ for key in expected_keys:
55
+ assert key in result, f"Missing key: {key}"
56
+
57
+
58
+ # ─── Test 2-5: Rally tracking ────────────────────────────────────────────────
59
+
60
+
61
+ class TestTrackRallyAttempt:
62
+ def test_day1_detected_on_up_close(self):
63
+ """First up close after swing low sets day1_idx."""
64
+ bars, swing_low_idx, day1_idx, _ = make_rally_history(
65
+ decline_pct=5.0,
66
+ down_days=5,
67
+ rally_days=3,
68
+ )
69
+ result = track_rally_attempt(bars, swing_low_idx)
70
+ assert result["day1_idx"] is not None
71
+ assert result["day1_date"] is not None
72
+ assert result["current_day_count"] >= 1
73
+
74
+ def test_rally_invalidated_close_below_swing_low(self):
75
+ """Close below swing low invalidates the rally."""
76
+ bars, _ = make_correction_history(peak=100, decline_pct=5.0, down_days=5)
77
+ swing_low_idx = len(bars) - 1
78
+ swing_low_close = bars[swing_low_idx]["close"]
79
+
80
+ # Add a day that closes below swing low
81
+ bars.append(
82
+ make_bar(
83
+ swing_low_close * 0.99,
84
+ date=f"day-{len(bars):03d}",
85
+ )
86
+ )
87
+ result = track_rally_attempt(bars, swing_low_idx)
88
+ assert result["invalidated"] is True
89
+ assert "below swing low" in result["invalidation_reason"]
90
+
91
+ def test_day1_low_breach_invalidates(self):
92
+ """Day 2 close below Day 1 low (but above swing low) invalidates the rally."""
93
+ bars, _ = make_correction_history(peak=100, decline_pct=5.0, down_days=5)
94
+ swing_low_idx = len(bars) - 1
95
+ swing_low_close = bars[swing_low_idx]["close"]
96
+
97
+ # Day 1: strong up close with a high intraday low (well above swing low)
98
+ day1_close = swing_low_close * 1.03 # 3% up from swing low
99
+ day1_low = swing_low_close * 1.015 # Day 1 low well above swing low
100
+ bars.append(
101
+ make_bar(
102
+ day1_close,
103
+ date=f"day-{len(bars):03d}",
104
+ low=day1_low,
105
+ )
106
+ )
107
+
108
+ # Day 2: close below Day 1 low but still above swing low
109
+ day2_close = swing_low_close * 1.005 # Above swing low
110
+ assert day2_close < day1_low, "Day 2 close must be below Day 1 low"
111
+ assert day2_close > swing_low_close, "Day 2 close must be above swing low"
112
+ bars.append(
113
+ make_bar(
114
+ day2_close,
115
+ date=f"day-{len(bars):03d}",
116
+ )
117
+ )
118
+
119
+ result = track_rally_attempt(bars, swing_low_idx)
120
+ assert result["invalidated"] is True
121
+ assert "Day 1 low" in result["invalidation_reason"]
122
+
123
+ def test_no_day1_if_no_up_close(self):
124
+ """If all post-swing-low days are down, day1_idx is None."""
125
+ bars, _ = make_correction_history(peak=100, decline_pct=5.0, down_days=5)
126
+ swing_low_idx = len(bars) - 1
127
+ last_close = bars[-1]["close"]
128
+
129
+ # Add 3 down days
130
+ for _i in range(3):
131
+ last_close *= 0.998
132
+ bars.append(make_bar(last_close, date=f"day-{len(bars):03d}"))
133
+
134
+ result = track_rally_attempt(bars, swing_low_idx)
135
+ # Either day1 is None or it got invalidated because it went below swing low
136
+ assert result["day1_idx"] is None or result["invalidated"]
137
+
138
+
139
+ # ─── Test 6-8: FTD detection ─────────────────────────────────────────────────
140
+
141
+
142
+ class TestDetectFTD:
143
+ def test_ftd_detected_day4_qualifying(self):
144
+ """Day 4: 1.5% gain + higher volume -> ftd_detected=True."""
145
+ bars, swing_low_idx, _, _ = make_rally_history(
146
+ decline_pct=5.0,
147
+ down_days=5,
148
+ rally_days=6,
149
+ ftd_day=4,
150
+ ftd_gain_pct=1.5,
151
+ ftd_volume_mult=1.5,
152
+ )
153
+ rally = track_rally_attempt(bars, swing_low_idx)
154
+ assert not rally["invalidated"]
155
+ ftd = detect_ftd(bars, rally)
156
+ assert ftd["ftd_detected"] is True
157
+ assert ftd["ftd_day_number"] == 4
158
+ assert ftd["gain_pct"] >= 1.25
159
+
160
+ def test_ftd_not_detected_low_volume(self):
161
+ """Gain OK but volume lower than previous day -> no FTD."""
162
+ bars, swing_low_idx, _, _ = make_rally_history(
163
+ decline_pct=5.0,
164
+ down_days=5,
165
+ rally_days=6,
166
+ ftd_day=4,
167
+ ftd_gain_pct=1.5,
168
+ ftd_volume_mult=0.8, # Volume LOWER than previous
169
+ )
170
+ rally = track_rally_attempt(bars, swing_low_idx)
171
+ ftd = detect_ftd(bars, rally)
172
+ assert ftd["ftd_detected"] is False
173
+
174
+ def test_ftd_window_expired(self):
175
+ """Rally goes past day 10 without qualifying FTD -> RALLY_FAILED."""
176
+ bars, swing_low_idx, _, _ = make_rally_history(
177
+ decline_pct=5.0,
178
+ down_days=5,
179
+ rally_days=12,
180
+ rally_gain_per_day=0.2, # Small gains, no FTD
181
+ )
182
+ result = analyze_single_index(bars, "Test")
183
+ # Should be RALLY_FAILED or FTD_WINDOW, not FTD_CONFIRMED
184
+ assert result["state"] != MarketState.FTD_CONFIRMED.value
185
+
186
+ def test_ftd_gain_tier_strong(self):
187
+ """2.0%+ gain -> strong tier."""
188
+ bars, swing_low_idx, _, _ = make_rally_history(
189
+ decline_pct=5.0,
190
+ down_days=5,
191
+ rally_days=6,
192
+ ftd_day=5,
193
+ ftd_gain_pct=2.5,
194
+ ftd_volume_mult=1.5,
195
+ )
196
+ rally = track_rally_attempt(bars, swing_low_idx)
197
+ ftd = detect_ftd(bars, rally)
198
+ if ftd["ftd_detected"]:
199
+ assert ftd["gain_tier"] == "strong"
200
+
201
+
202
+ # ─── Test 9: Fix 1 verification — price recovery near highs ──────────────────
203
+
204
+
205
+ class TestPriceRecoveryFix:
206
+ def test_ftd_tracked_despite_recovery_near_highs(self):
207
+ """
208
+ Fix 1 verification: After correction, if price rallies back near
209
+ the lookback high (within 3%), FTD tracking must NOT be killed.
210
+
211
+ Old behavior: correction_depth > -3% -> NO_SIGNAL (wrong).
212
+ New behavior: swing_low found -> continue tracking.
213
+ """
214
+ bars, swing_low_idx, _, _ = make_rally_history(
215
+ peak=100,
216
+ decline_pct=5.0,
217
+ down_days=5,
218
+ rally_days=8,
219
+ rally_gain_per_day=0.6,
220
+ ftd_day=5,
221
+ ftd_gain_pct=1.8,
222
+ ftd_volume_mult=1.5,
223
+ )
224
+
225
+ # The rally should have brought price back near the peak
226
+ current_price = bars[-1]["close"]
227
+ peak = 100.0
228
+ recovery_pct = (current_price - peak) / peak * 100
229
+
230
+ result = analyze_single_index(bars, "Test Recovery")
231
+
232
+ # Must NOT be NO_SIGNAL — should track the FTD
233
+ assert result["state"] != MarketState.NO_SIGNAL.value, (
234
+ f"Price recovered to {recovery_pct:+.1f}% from peak but state is "
235
+ f"NO_SIGNAL. Fix 1 regression: early return on shallow correction_depth."
236
+ )
237
+
238
+ def test_no_signal_when_no_swing_low(self):
239
+ """No qualifying swing low -> NO_SIGNAL (correct behavior)."""
240
+ # Flat market with 1% variation — no swing low
241
+ bars = []
242
+ for i in range(30):
243
+ price = 100 + (i % 3) * 0.3 # Tiny oscillation
244
+ bars.append(make_bar(price, date=f"day-{i:03d}"))
245
+
246
+ result = analyze_single_index(bars, "Flat Market")
247
+ assert result["state"] == MarketState.NO_SIGNAL.value
248
+
249
+
250
+ # ─── Test 10: Dual-index merge ───────────────────────────────────────────────
251
+
252
+
253
+ class TestDualIndexMerge:
254
+ def test_dual_confirmation_both_ftd(self):
255
+ """Both S&P 500 and NASDAQ have FTD -> dual_confirmation=True."""
256
+ bars_sp, _, _, _ = make_rally_history(
257
+ peak=100,
258
+ decline_pct=5.0,
259
+ down_days=5,
260
+ rally_days=7,
261
+ ftd_day=5,
262
+ ftd_gain_pct=1.6,
263
+ ftd_volume_mult=1.5,
264
+ )
265
+ bars_nq, _, _, _ = make_rally_history(
266
+ peak=200,
267
+ decline_pct=6.0,
268
+ down_days=5,
269
+ rally_days=7,
270
+ ftd_day=6,
271
+ ftd_gain_pct=1.8,
272
+ ftd_volume_mult=1.5,
273
+ )
274
+
275
+ # get_market_state expects API format (most recent first)
276
+ sp_reversed = list(reversed(bars_sp))
277
+ nq_reversed = list(reversed(bars_nq))
278
+
279
+ result = get_market_state(sp_reversed, nq_reversed)
280
+ assert result["combined_state"] == MarketState.FTD_CONFIRMED.value
281
+ assert result["dual_confirmation"] is True
282
+
283
+ def test_single_ftd_no_dual(self):
284
+ """Only S&P 500 has FTD -> dual_confirmation=False."""
285
+ bars_sp, _, _, _ = make_rally_history(
286
+ peak=100,
287
+ decline_pct=5.0,
288
+ down_days=5,
289
+ rally_days=7,
290
+ ftd_day=5,
291
+ ftd_gain_pct=1.6,
292
+ ftd_volume_mult=1.5,
293
+ )
294
+ # NASDAQ: flat market, no swing low
295
+ bars_nq = [make_bar(200 + i * 0.1, date=f"day-{i:03d}") for i in range(30)]
296
+
297
+ sp_reversed = list(reversed(bars_sp))
298
+ nq_reversed = list(reversed(bars_nq))
299
+
300
+ result = get_market_state(sp_reversed, nq_reversed)
301
+ assert result["combined_state"] == MarketState.FTD_CONFIRMED.value
302
+ assert result["dual_confirmation"] is False