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.
- package/.claude/skills/README.md +80 -0
- package/.claude/skills/backtest-expert/SKILL.md +206 -0
- package/.claude/skills/backtest-expert/references/failed_tests.md +236 -0
- package/.claude/skills/backtest-expert/references/methodology.md +227 -0
- package/.claude/skills/breadth-chart-analyst/SKILL.md +583 -0
- package/.claude/skills/breadth-chart-analyst/assets/SP500_Breadth_Index_200MA_8MA.jpeg +0 -0
- package/.claude/skills/breadth-chart-analyst/assets/US_Stock_Market_Uptrend_Ratio.jpeg +0 -0
- package/.claude/skills/breadth-chart-analyst/assets/breadth_analysis_template.md +558 -0
- package/.claude/skills/breadth-chart-analyst/references/breadth_chart_methodology.md +590 -0
- package/.claude/skills/canslim-screener/SKILL.md +599 -0
- package/.claude/skills/canslim-screener/references/canslim_methodology.md +606 -0
- package/.claude/skills/canslim-screener/references/fmp_api_endpoints.md +707 -0
- package/.claude/skills/canslim-screener/references/interpretation_guide.md +516 -0
- package/.claude/skills/canslim-screener/references/scoring_system.md +597 -0
- package/.claude/skills/canslim-screener/scripts/calculators/earnings_calculator.py +343 -0
- package/.claude/skills/canslim-screener/scripts/calculators/growth_calculator.py +334 -0
- package/.claude/skills/canslim-screener/scripts/calculators/institutional_calculator.py +347 -0
- package/.claude/skills/canslim-screener/scripts/calculators/leadership_calculator.py +380 -0
- package/.claude/skills/canslim-screener/scripts/calculators/market_calculator.py +244 -0
- package/.claude/skills/canslim-screener/scripts/calculators/new_highs_calculator.py +194 -0
- package/.claude/skills/canslim-screener/scripts/calculators/supply_demand_calculator.py +221 -0
- package/.claude/skills/canslim-screener/scripts/finviz_stock_client.py +227 -0
- package/.claude/skills/canslim-screener/scripts/fmp_client.py +393 -0
- package/.claude/skills/canslim-screener/scripts/report_generator.py +405 -0
- package/.claude/skills/canslim-screener/scripts/scorer.py +625 -0
- package/.claude/skills/canslim-screener/scripts/screen_canslim.py +361 -0
- package/.claude/skills/canslim-screener/scripts/test_institutional_endpoint.py +109 -0
- package/.claude/skills/chart/SKILL.md +20 -0
- package/.claude/skills/dividend-growth-pullback-screener/SKILL.md +322 -0
- package/.claude/skills/dividend-growth-pullback-screener/references/dividend_growth_compounding.md +400 -0
- package/.claude/skills/dividend-growth-pullback-screener/references/fmp_api_guide.md +642 -0
- package/.claude/skills/dividend-growth-pullback-screener/references/rsi_oversold_strategy.md +333 -0
- package/.claude/skills/dividend-growth-pullback-screener/scripts/screen_dividend_growth_rsi.py +1155 -0
- package/.claude/skills/earnings-calendar/SKILL.md +721 -0
- package/.claude/skills/earnings-calendar/assets/earnings_report_template.md +102 -0
- package/.claude/skills/earnings-calendar/references/fmp_api_guide.md +590 -0
- package/.claude/skills/earnings-calendar/scripts/fetch_earnings_fmp.py +443 -0
- package/.claude/skills/earnings-calendar/scripts/generate_report.py +366 -0
- package/.claude/skills/economic-calendar-fetcher/SKILL.md +365 -0
- package/.claude/skills/economic-calendar-fetcher/references/fmp_api_documentation.md +345 -0
- package/.claude/skills/economic-calendar-fetcher/scripts/get_economic_calendar.py +267 -0
- package/.claude/skills/ftd-detector/SKILL.md +147 -0
- package/.claude/skills/ftd-detector/references/ftd_methodology.md +188 -0
- package/.claude/skills/ftd-detector/references/post_ftd_guide.md +185 -0
- package/.claude/skills/ftd-detector/scripts/fmp_client.py +158 -0
- package/.claude/skills/ftd-detector/scripts/ftd_detector.py +280 -0
- package/.claude/skills/ftd-detector/scripts/post_ftd_monitor.py +404 -0
- package/.claude/skills/ftd-detector/scripts/rally_tracker.py +508 -0
- package/.claude/skills/ftd-detector/scripts/report_generator.py +341 -0
- package/.claude/skills/ftd-detector/scripts/tests/conftest.py +9 -0
- package/.claude/skills/ftd-detector/scripts/tests/helpers.py +107 -0
- package/.claude/skills/ftd-detector/scripts/tests/test_post_ftd_monitor.py +311 -0
- package/.claude/skills/ftd-detector/scripts/tests/test_rally_tracker.py +302 -0
- package/.claude/skills/institutional-flow-tracker/README.md +362 -0
- package/.claude/skills/institutional-flow-tracker/SKILL.md +357 -0
- package/.claude/skills/institutional-flow-tracker/references/13f_filings_guide.md +383 -0
- package/.claude/skills/institutional-flow-tracker/references/institutional_investor_types.md +580 -0
- package/.claude/skills/institutional-flow-tracker/references/interpretation_framework.md +573 -0
- package/.claude/skills/institutional-flow-tracker/scripts/analyze_single_stock.py +457 -0
- package/.claude/skills/institutional-flow-tracker/scripts/track_institution_portfolio.py +108 -0
- package/.claude/skills/institutional-flow-tracker/scripts/track_institutional_flow.py +450 -0
- package/.claude/skills/macro-regime-detector/SKILL.md +86 -0
- package/.claude/skills/macro-regime-detector/references/historical_regimes.md +124 -0
- package/.claude/skills/macro-regime-detector/references/indicator_interpretation_guide.md +144 -0
- package/.claude/skills/macro-regime-detector/references/regime_detection_methodology.md +138 -0
- package/.claude/skills/macro-regime-detector/scripts/calculators/__init__.py +1 -0
- package/.claude/skills/macro-regime-detector/scripts/calculators/concentration_calculator.py +165 -0
- package/.claude/skills/macro-regime-detector/scripts/calculators/credit_conditions_calculator.py +124 -0
- package/.claude/skills/macro-regime-detector/scripts/calculators/equity_bond_calculator.py +198 -0
- package/.claude/skills/macro-regime-detector/scripts/calculators/sector_rotation_calculator.py +123 -0
- package/.claude/skills/macro-regime-detector/scripts/calculators/size_factor_calculator.py +131 -0
- package/.claude/skills/macro-regime-detector/scripts/calculators/utils.py +347 -0
- package/.claude/skills/macro-regime-detector/scripts/calculators/yield_curve_calculator.py +279 -0
- package/.claude/skills/macro-regime-detector/scripts/fmp_client.py +134 -0
- package/.claude/skills/macro-regime-detector/scripts/macro_regime_detector.py +278 -0
- package/.claude/skills/macro-regime-detector/scripts/report_generator.py +327 -0
- package/.claude/skills/macro-regime-detector/scripts/scorer.py +574 -0
- package/.claude/skills/macro-regime-detector/scripts/tests/conftest.py +9 -0
- package/.claude/skills/macro-regime-detector/scripts/tests/test_concentration.py +78 -0
- package/.claude/skills/macro-regime-detector/scripts/tests/test_credit_conditions.py +59 -0
- package/.claude/skills/macro-regime-detector/scripts/tests/test_equity_bond.py +74 -0
- package/.claude/skills/macro-regime-detector/scripts/tests/test_helpers.py +90 -0
- package/.claude/skills/macro-regime-detector/scripts/tests/test_scorer.py +439 -0
- package/.claude/skills/macro-regime-detector/scripts/tests/test_sector_rotation.py +78 -0
- package/.claude/skills/macro-regime-detector/scripts/tests/test_size_factor.py +59 -0
- package/.claude/skills/macro-regime-detector/scripts/tests/test_utils.py +126 -0
- package/.claude/skills/macro-regime-detector/scripts/tests/test_yield_curve.py +64 -0
- package/.claude/skills/market-breadth-analyzer/SKILL.md +121 -0
- package/.claude/skills/market-breadth-analyzer/references/breadth_analysis_methodology.md +168 -0
- package/.claude/skills/market-breadth-analyzer/scripts/calculators/__init__.py +1 -0
- package/.claude/skills/market-breadth-analyzer/scripts/calculators/bearish_signal_calculator.py +150 -0
- package/.claude/skills/market-breadth-analyzer/scripts/calculators/cycle_calculator.py +168 -0
- package/.claude/skills/market-breadth-analyzer/scripts/calculators/divergence_calculator.py +119 -0
- package/.claude/skills/market-breadth-analyzer/scripts/calculators/historical_context_calculator.py +120 -0
- package/.claude/skills/market-breadth-analyzer/scripts/calculators/ma_crossover_calculator.py +115 -0
- package/.claude/skills/market-breadth-analyzer/scripts/calculators/trend_level_calculator.py +103 -0
- package/.claude/skills/market-breadth-analyzer/scripts/csv_client.py +225 -0
- package/.claude/skills/market-breadth-analyzer/scripts/market_breadth_analyzer.py +307 -0
- package/.claude/skills/market-breadth-analyzer/scripts/report_generator.py +330 -0
- package/.claude/skills/market-breadth-analyzer/scripts/scorer.py +271 -0
- package/.claude/skills/market-environment-analysis/SKILL.md +139 -0
- package/.claude/skills/market-environment-analysis/references/analysis_patterns.md +124 -0
- package/.claude/skills/market-environment-analysis/references/indicators.md +99 -0
- package/.claude/skills/market-environment-analysis/scripts/market_utils.py +127 -0
- package/.claude/skills/market-news-analyst/SKILL.md +714 -0
- package/.claude/skills/market-news-analyst/references/corporate_news_impact.md +446 -0
- package/.claude/skills/market-news-analyst/references/geopolitical_commodity_correlations.md +499 -0
- package/.claude/skills/market-news-analyst/references/market_event_patterns.md +393 -0
- package/.claude/skills/market-news-analyst/references/trusted_news_sources.md +510 -0
- package/.claude/skills/market-top-detector/SKILL.md +159 -0
- package/.claude/skills/market-top-detector/references/distribution_day_guide.md +100 -0
- package/.claude/skills/market-top-detector/references/historical_tops.md +142 -0
- package/.claude/skills/market-top-detector/references/market_top_methodology.md +167 -0
- package/.claude/skills/market-top-detector/scripts/calculators/__init__.py +17 -0
- package/.claude/skills/market-top-detector/scripts/calculators/breadth_calculator.py +116 -0
- package/.claude/skills/market-top-detector/scripts/calculators/defensive_rotation_calculator.py +127 -0
- package/.claude/skills/market-top-detector/scripts/calculators/distribution_day_calculator.py +161 -0
- package/.claude/skills/market-top-detector/scripts/calculators/index_technical_calculator.py +254 -0
- package/.claude/skills/market-top-detector/scripts/calculators/leading_stock_calculator.py +198 -0
- package/.claude/skills/market-top-detector/scripts/calculators/sentiment_calculator.py +213 -0
- package/.claude/skills/market-top-detector/scripts/fmp_client.py +158 -0
- package/.claude/skills/market-top-detector/scripts/market_top_detector.py +349 -0
- package/.claude/skills/market-top-detector/scripts/report_generator.py +314 -0
- package/.claude/skills/market-top-detector/scripts/scorer.py +473 -0
- package/.claude/skills/market-top-detector/scripts/tests/conftest.py +9 -0
- package/.claude/skills/market-top-detector/scripts/tests/helpers.py +49 -0
- package/.claude/skills/market-top-detector/scripts/tests/test_breadth.py +62 -0
- package/.claude/skills/market-top-detector/scripts/tests/test_defensive_rotation.py +56 -0
- package/.claude/skills/market-top-detector/scripts/tests/test_distribution_day.py +92 -0
- package/.claude/skills/market-top-detector/scripts/tests/test_index_technical.py +73 -0
- package/.claude/skills/market-top-detector/scripts/tests/test_leading_stock.py +57 -0
- package/.claude/skills/market-top-detector/scripts/tests/test_scorer.py +180 -0
- package/.claude/skills/market-top-detector/scripts/tests/test_sentiment.py +64 -0
- package/.claude/skills/options-strategy-advisor/README.md +469 -0
- package/.claude/skills/options-strategy-advisor/SKILL.md +959 -0
- package/.claude/skills/options-strategy-advisor/scripts/black_scholes.py +495 -0
- package/.claude/skills/pair-trade-screener/README.md +389 -0
- package/.claude/skills/pair-trade-screener/SKILL.md +622 -0
- package/.claude/skills/pair-trade-screener/references/cointegration_guide.md +745 -0
- package/.claude/skills/pair-trade-screener/references/methodology.md +853 -0
- package/.claude/skills/pair-trade-screener/scripts/analyze_spread.py +394 -0
- package/.claude/skills/pair-trade-screener/scripts/find_pairs.py +535 -0
- package/.claude/skills/portfolio-manager/README.md +394 -0
- package/.claude/skills/portfolio-manager/SKILL.md +750 -0
- package/.claude/skills/portfolio-manager/references/alpaca-mcp-setup.md +367 -0
- package/.claude/skills/portfolio-manager/references/asset-allocation.md +502 -0
- package/.claude/skills/portfolio-manager/references/diversification-principles.md +553 -0
- package/.claude/skills/portfolio-manager/references/portfolio-risk-metrics.md +603 -0
- package/.claude/skills/portfolio-manager/references/position-evaluation.md +477 -0
- package/.claude/skills/portfolio-manager/references/rebalancing-strategies.md +715 -0
- package/.claude/skills/portfolio-manager/references/risk-profile-questionnaire.md +608 -0
- package/.claude/skills/portfolio-manager/references/target-allocations.md +558 -0
- package/.claude/skills/portfolio-manager/scripts/test_alpaca_connection.py +286 -0
- package/.claude/skills/scenario-analyzer/SKILL.md +317 -0
- package/.claude/skills/scenario-analyzer/references/headline_event_patterns.md +264 -0
- package/.claude/skills/scenario-analyzer/references/scenario_playbooks.md +320 -0
- package/.claude/skills/scenario-analyzer/references/sector_sensitivity_matrix.md +217 -0
- package/.claude/skills/sector-analyst/SKILL.md +206 -0
- package/.claude/skills/sector-analyst/assets/industory_performance_1.jpeg +0 -0
- package/.claude/skills/sector-analyst/assets/industory_performance_2.jpeg +0 -0
- package/.claude/skills/sector-analyst/assets/sector_performance.jpeg +0 -0
- package/.claude/skills/sector-analyst/references/sector_rotation.md +170 -0
- package/.claude/skills/stanley-druckenmiller-investment/SKILL.md +84 -0
- package/.claude/skills/stanley-druckenmiller-investment/references/case-studies.md +148 -0
- package/.claude/skills/stanley-druckenmiller-investment/references/investment-philosophy.md +80 -0
- package/.claude/skills/stanley-druckenmiller-investment/references/market-analysis-guide.md +146 -0
- package/.claude/skills/stock/NOTION_SETUP.md +33 -0
- package/.claude/skills/stock/SKILL.md +38 -0
- package/.claude/skills/technical-analyst/SKILL.md +238 -0
- package/.claude/skills/technical-analyst/assets/analysis_template.md +183 -0
- package/.claude/skills/technical-analyst/references/technical_analysis_framework.md +282 -0
- package/.claude/skills/theme-detector/SKILL.md +320 -0
- package/.claude/skills/theme-detector/assets/report_template.md +155 -0
- package/.claude/skills/theme-detector/references/cross_sector_themes.md +252 -0
- package/.claude/skills/theme-detector/references/finviz_industry_codes.md +403 -0
- package/.claude/skills/theme-detector/references/thematic_etf_catalog.md +333 -0
- package/.claude/skills/theme-detector/references/theme_detection_methodology.md +430 -0
- package/.claude/skills/theme-detector/scripts/calculators/__init__.py +1 -0
- package/.claude/skills/theme-detector/scripts/calculators/heat_calculator.py +123 -0
- package/.claude/skills/theme-detector/scripts/calculators/industry_ranker.py +98 -0
- package/.claude/skills/theme-detector/scripts/calculators/lifecycle_calculator.py +172 -0
- package/.claude/skills/theme-detector/scripts/calculators/theme_classifier.py +195 -0
- package/.claude/skills/theme-detector/scripts/calculators/theme_discoverer.py +280 -0
- package/.claude/skills/theme-detector/scripts/config_loader.py +142 -0
- package/.claude/skills/theme-detector/scripts/default_theme_config.py +254 -0
- package/.claude/skills/theme-detector/scripts/etf_scanner.py +609 -0
- package/.claude/skills/theme-detector/scripts/finviz_performance_client.py +131 -0
- package/.claude/skills/theme-detector/scripts/report_generator.py +490 -0
- package/.claude/skills/theme-detector/scripts/representative_stock_selector.py +673 -0
- package/.claude/skills/theme-detector/scripts/scorer.py +87 -0
- package/.claude/skills/theme-detector/scripts/tests/README.md +21 -0
- package/.claude/skills/theme-detector/scripts/tests/conftest.py +9 -0
- package/.claude/skills/theme-detector/scripts/tests/test_config_loader.py +239 -0
- package/.claude/skills/theme-detector/scripts/tests/test_etf_scanner.py +810 -0
- package/.claude/skills/theme-detector/scripts/tests/test_heat_calculator.py +245 -0
- package/.claude/skills/theme-detector/scripts/tests/test_industry_ranker.py +256 -0
- package/.claude/skills/theme-detector/scripts/tests/test_lifecycle_calculator.py +301 -0
- package/.claude/skills/theme-detector/scripts/tests/test_report_generator.py +624 -0
- package/.claude/skills/theme-detector/scripts/tests/test_representative_stock_selector.py +898 -0
- package/.claude/skills/theme-detector/scripts/tests/test_scorer.py +185 -0
- package/.claude/skills/theme-detector/scripts/tests/test_theme_classifier.py +534 -0
- package/.claude/skills/theme-detector/scripts/tests/test_theme_detector_e2e.py +467 -0
- package/.claude/skills/theme-detector/scripts/tests/test_theme_discoverer.py +458 -0
- package/.claude/skills/theme-detector/scripts/tests/test_uptrend_client.py +76 -0
- package/.claude/skills/theme-detector/scripts/theme_detector.py +815 -0
- package/.claude/skills/theme-detector/scripts/themes.yaml +168 -0
- package/.claude/skills/theme-detector/scripts/uptrend_client.py +241 -0
- package/.claude/skills/uptrend-analyzer/SKILL.md +108 -0
- package/.claude/skills/uptrend-analyzer/references/uptrend_methodology.md +215 -0
- package/.claude/skills/uptrend-analyzer/scripts/calculators/__init__.py +1 -0
- package/.claude/skills/uptrend-analyzer/scripts/calculators/historical_context_calculator.py +122 -0
- package/.claude/skills/uptrend-analyzer/scripts/calculators/market_breadth_calculator.py +145 -0
- package/.claude/skills/uptrend-analyzer/scripts/calculators/momentum_calculator.py +183 -0
- package/.claude/skills/uptrend-analyzer/scripts/calculators/sector_participation_calculator.py +204 -0
- package/.claude/skills/uptrend-analyzer/scripts/calculators/sector_rotation_calculator.py +218 -0
- package/.claude/skills/uptrend-analyzer/scripts/data_fetcher.py +236 -0
- package/.claude/skills/uptrend-analyzer/scripts/report_generator.py +329 -0
- package/.claude/skills/uptrend-analyzer/scripts/scorer.py +276 -0
- package/.claude/skills/uptrend-analyzer/scripts/uptrend_analyzer.py +219 -0
- package/.claude/skills/us-market-bubble-detector/CHANGELOG.md +118 -0
- package/.claude/skills/us-market-bubble-detector/SKILL.md +545 -0
- package/.claude/skills/us-market-bubble-detector/references/bubble_framework.md +335 -0
- package/.claude/skills/us-market-bubble-detector/references/historical_cases.md +327 -0
- package/.claude/skills/us-market-bubble-detector/references/implementation_guide.md +473 -0
- package/.claude/skills/us-market-bubble-detector/references/quick_reference.md +354 -0
- package/.claude/skills/us-market-bubble-detector/references/quick_reference_en.md +342 -0
- package/.claude/skills/us-market-bubble-detector/scripts/bubble_scorer.py +309 -0
- package/.claude/skills/us-stock-analysis/SKILL.md +294 -0
- package/.claude/skills/us-stock-analysis/references/financial-metrics.md +172 -0
- package/.claude/skills/us-stock-analysis/references/fundamental-analysis.md +129 -0
- package/.claude/skills/us-stock-analysis/references/report-template.md +207 -0
- package/.claude/skills/us-stock-analysis/references/technical-analysis.md +93 -0
- package/.claude/skills/value-dividend-screener/SKILL.md +562 -0
- package/.claude/skills/value-dividend-screener/references/fmp_api_guide.md +348 -0
- package/.claude/skills/value-dividend-screener/references/screening_methodology.md +315 -0
- package/.claude/skills/value-dividend-screener/scripts/screen_dividend_stocks.py +1138 -0
- package/.claude/skills/vcp-screener/SKILL.md +79 -0
- package/.claude/skills/vcp-screener/references/fmp_api_endpoints.md +45 -0
- package/.claude/skills/vcp-screener/references/scoring_system.md +154 -0
- package/.claude/skills/vcp-screener/references/vcp_methodology.md +124 -0
- package/.claude/skills/vcp-screener/scripts/calculators/__init__.py +1 -0
- package/.claude/skills/vcp-screener/scripts/calculators/pivot_proximity_calculator.py +139 -0
- package/.claude/skills/vcp-screener/scripts/calculators/relative_strength_calculator.py +161 -0
- package/.claude/skills/vcp-screener/scripts/calculators/trend_template_calculator.py +228 -0
- package/.claude/skills/vcp-screener/scripts/calculators/vcp_pattern_calculator.py +322 -0
- package/.claude/skills/vcp-screener/scripts/calculators/volume_pattern_calculator.py +121 -0
- package/.claude/skills/vcp-screener/scripts/fmp_client.py +162 -0
- package/.claude/skills/vcp-screener/scripts/report_generator.py +317 -0
- package/.claude/skills/vcp-screener/scripts/scorer.py +155 -0
- package/.claude/skills/vcp-screener/scripts/screen_vcp.py +536 -0
- package/.claude/skills/vcp-screener/scripts/tests/__init__.py +0 -0
- package/.claude/skills/vcp-screener/scripts/tests/conftest.py +9 -0
- package/.claude/skills/vcp-screener/scripts/tests/test_vcp_screener.py +834 -0
- package/.claude/skills/weekly-trade-strategy/.claude/agents/druckenmiller-strategy-planner.md +300 -0
- package/.claude/skills/weekly-trade-strategy/.claude/agents/market-news-analyzer.md +239 -0
- package/.claude/skills/weekly-trade-strategy/.claude/agents/technical-market-analyst.md +187 -0
- package/.claude/skills/weekly-trade-strategy/.claude/agents/us-market-analyst.md +218 -0
- package/.claude/skills/weekly-trade-strategy/.claude/agents/weekly-trade-blog-writer.md +318 -0
- package/.claude/skills/weekly-trade-strategy/.claude/skills/breadth-chart-analyst/SKILL.md +662 -0
- package/.claude/skills/weekly-trade-strategy/.claude/skills/breadth-chart-analyst/assets/SP500_Breadth_Index_200MA_8MA.jpeg +0 -0
- package/.claude/skills/weekly-trade-strategy/.claude/skills/breadth-chart-analyst/assets/US_Stock_Market_Uptrend_Ratio.jpeg +0 -0
- package/.claude/skills/weekly-trade-strategy/.claude/skills/breadth-chart-analyst/assets/breadth_analysis_template.md +558 -0
- package/.claude/skills/weekly-trade-strategy/.claude/skills/breadth-chart-analyst/references/breadth_chart_methodology.md +590 -0
- package/.claude/skills/weekly-trade-strategy/.claude/skills/earnings-calendar/SKILL.md +721 -0
- package/.claude/skills/weekly-trade-strategy/.claude/skills/earnings-calendar/assets/earnings_report_template.md +102 -0
- package/.claude/skills/weekly-trade-strategy/.claude/skills/earnings-calendar/earnings_calendar_2025-11-02.md +447 -0
- package/.claude/skills/weekly-trade-strategy/.claude/skills/earnings-calendar/references/fmp_api_guide.md +590 -0
- package/.claude/skills/weekly-trade-strategy/.claude/skills/earnings-calendar/scripts/fetch_earnings_fmp.py +443 -0
- package/.claude/skills/weekly-trade-strategy/.claude/skills/earnings-calendar/scripts/generate_report.py +366 -0
- package/.claude/skills/weekly-trade-strategy/.claude/skills/economic-calendar-fetcher/SKILL.md +365 -0
- package/.claude/skills/weekly-trade-strategy/.claude/skills/economic-calendar-fetcher/references/fmp_api_documentation.md +345 -0
- package/.claude/skills/weekly-trade-strategy/.claude/skills/economic-calendar-fetcher/scripts/get_economic_calendar.py +267 -0
- package/.claude/skills/weekly-trade-strategy/.claude/skills/market-environment-analysis/SKILL.md +139 -0
- package/.claude/skills/weekly-trade-strategy/.claude/skills/market-environment-analysis/references/analysis_patterns.md +124 -0
- package/.claude/skills/weekly-trade-strategy/.claude/skills/market-environment-analysis/references/indicators.md +99 -0
- package/.claude/skills/weekly-trade-strategy/.claude/skills/market-environment-analysis/scripts/market_utils.py +127 -0
- package/.claude/skills/weekly-trade-strategy/.claude/skills/market-news-analyst/SKILL.md +714 -0
- package/.claude/skills/weekly-trade-strategy/.claude/skills/market-news-analyst/references/corporate_news_impact.md +446 -0
- package/.claude/skills/weekly-trade-strategy/.claude/skills/market-news-analyst/references/geopolitical_commodity_correlations.md +499 -0
- package/.claude/skills/weekly-trade-strategy/.claude/skills/market-news-analyst/references/market_event_patterns.md +393 -0
- package/.claude/skills/weekly-trade-strategy/.claude/skills/market-news-analyst/references/trusted_news_sources.md +510 -0
- package/.claude/skills/weekly-trade-strategy/.claude/skills/sector-analyst/SKILL.md +206 -0
- package/.claude/skills/weekly-trade-strategy/.claude/skills/sector-analyst/assets/industory_performance_1.jpeg +0 -0
- package/.claude/skills/weekly-trade-strategy/.claude/skills/sector-analyst/assets/industory_performance_2.jpeg +0 -0
- package/.claude/skills/weekly-trade-strategy/.claude/skills/sector-analyst/assets/sector_performance.jpeg +0 -0
- package/.claude/skills/weekly-trade-strategy/.claude/skills/sector-analyst/references/sector_rotation.md +170 -0
- package/.claude/skills/weekly-trade-strategy/.claude/skills/stanley-druckenmiller-investment/SKILL.md +84 -0
- package/.claude/skills/weekly-trade-strategy/.claude/skills/stanley-druckenmiller-investment/references/case-studies.md +148 -0
- package/.claude/skills/weekly-trade-strategy/.claude/skills/stanley-druckenmiller-investment/references/investment-philosophy.md +80 -0
- package/.claude/skills/weekly-trade-strategy/.claude/skills/stanley-druckenmiller-investment/references/market-analysis-guide.md +146 -0
- package/.claude/skills/weekly-trade-strategy/.claude/skills/technical-analyst/SKILL.md +238 -0
- package/.claude/skills/weekly-trade-strategy/.claude/skills/technical-analyst/assets/analysis_template.md +183 -0
- package/.claude/skills/weekly-trade-strategy/.claude/skills/technical-analyst/references/technical_analysis_framework.md +282 -0
- package/.claude/skills/weekly-trade-strategy/.claude/skills/us-market-bubble-detector/CHANGELOG.md +118 -0
- package/.claude/skills/weekly-trade-strategy/.claude/skills/us-market-bubble-detector/SKILL.md +545 -0
- package/.claude/skills/weekly-trade-strategy/.claude/skills/us-market-bubble-detector/references/bubble_framework.md +335 -0
- package/.claude/skills/weekly-trade-strategy/.claude/skills/us-market-bubble-detector/references/historical_cases.md +327 -0
- package/.claude/skills/weekly-trade-strategy/.claude/skills/us-market-bubble-detector/references/implementation_guide.md +473 -0
- package/.claude/skills/weekly-trade-strategy/.claude/skills/us-market-bubble-detector/references/quick_reference.md +354 -0
- package/.claude/skills/weekly-trade-strategy/.claude/skills/us-market-bubble-detector/references/quick_reference_en.md +342 -0
- package/.claude/skills/weekly-trade-strategy/.claude/skills/us-market-bubble-detector/scripts/bubble_scorer.py +309 -0
- package/.claude/skills/weekly-trade-strategy/.claude/skills/us-stock-analysis/SKILL.md +294 -0
- package/.claude/skills/weekly-trade-strategy/.claude/skills/us-stock-analysis/references/financial-metrics.md +172 -0
- package/.claude/skills/weekly-trade-strategy/.claude/skills/us-stock-analysis/references/fundamental-analysis.md +129 -0
- package/.claude/skills/weekly-trade-strategy/.claude/skills/us-stock-analysis/references/report-template.md +207 -0
- package/.claude/skills/weekly-trade-strategy/.claude/skills/us-stock-analysis/references/technical-analysis.md +93 -0
- package/.claude/skills/weekly-trade-strategy/CLAUDE.md +454 -0
- package/.claude/skills/weekly-trade-strategy/README.md +287 -0
- package/.claude/skills/weekly-trade-strategy/blogs/.gitkeep +0 -0
- package/.claude/skills/weekly-trade-strategy/charts/.gitkeep +0 -0
- package/.claude/skills/weekly-trade-strategy/earnings_data.json +10054 -0
- package/.claude/skills/weekly-trade-strategy/skills/breadth-chart-analyst/SKILL.md +662 -0
- package/.claude/skills/weekly-trade-strategy/skills/breadth-chart-analyst/assets/SP500_Breadth_Index_200MA_8MA.jpeg +0 -0
- package/.claude/skills/weekly-trade-strategy/skills/breadth-chart-analyst/assets/US_Stock_Market_Uptrend_Ratio.jpeg +0 -0
- package/.claude/skills/weekly-trade-strategy/skills/breadth-chart-analyst/assets/breadth_analysis_template.md +558 -0
- package/.claude/skills/weekly-trade-strategy/skills/breadth-chart-analyst/references/breadth_chart_methodology.md +590 -0
- package/.claude/skills/weekly-trade-strategy/skills/earnings-calendar/SKILL.md +721 -0
- package/.claude/skills/weekly-trade-strategy/skills/earnings-calendar/assets/earnings_report_template.md +102 -0
- package/.claude/skills/weekly-trade-strategy/skills/earnings-calendar/earnings_calendar_2025-11-02.md +447 -0
- package/.claude/skills/weekly-trade-strategy/skills/earnings-calendar/references/fmp_api_guide.md +590 -0
- package/.claude/skills/weekly-trade-strategy/skills/earnings-calendar/scripts/fetch_earnings_fmp.py +443 -0
- package/.claude/skills/weekly-trade-strategy/skills/earnings-calendar/scripts/generate_report.py +366 -0
- package/.claude/skills/weekly-trade-strategy/skills/economic-calendar-fetcher/SKILL.md +365 -0
- package/.claude/skills/weekly-trade-strategy/skills/economic-calendar-fetcher/references/fmp_api_documentation.md +345 -0
- package/.claude/skills/weekly-trade-strategy/skills/economic-calendar-fetcher/scripts/get_economic_calendar.py +267 -0
- package/.claude/skills/weekly-trade-strategy/skills/market-environment-analysis/SKILL.md +139 -0
- package/.claude/skills/weekly-trade-strategy/skills/market-environment-analysis/references/analysis_patterns.md +124 -0
- package/.claude/skills/weekly-trade-strategy/skills/market-environment-analysis/references/indicators.md +99 -0
- package/.claude/skills/weekly-trade-strategy/skills/market-environment-analysis/scripts/market_utils.py +127 -0
- package/.claude/skills/weekly-trade-strategy/skills/market-news-analyst/SKILL.md +714 -0
- package/.claude/skills/weekly-trade-strategy/skills/market-news-analyst/references/corporate_news_impact.md +446 -0
- package/.claude/skills/weekly-trade-strategy/skills/market-news-analyst/references/geopolitical_commodity_correlations.md +499 -0
- package/.claude/skills/weekly-trade-strategy/skills/market-news-analyst/references/market_event_patterns.md +393 -0
- package/.claude/skills/weekly-trade-strategy/skills/market-news-analyst/references/trusted_news_sources.md +510 -0
- package/.claude/skills/weekly-trade-strategy/skills/sector-analyst/SKILL.md +206 -0
- package/.claude/skills/weekly-trade-strategy/skills/sector-analyst/assets/industory_performance_1.jpeg +0 -0
- package/.claude/skills/weekly-trade-strategy/skills/sector-analyst/assets/industory_performance_2.jpeg +0 -0
- package/.claude/skills/weekly-trade-strategy/skills/sector-analyst/assets/sector_performance.jpeg +0 -0
- package/.claude/skills/weekly-trade-strategy/skills/sector-analyst/references/sector_rotation.md +170 -0
- package/.claude/skills/weekly-trade-strategy/skills/stanley-druckenmiller-investment/SKILL.md +84 -0
- package/.claude/skills/weekly-trade-strategy/skills/stanley-druckenmiller-investment/references/case-studies.md +148 -0
- package/.claude/skills/weekly-trade-strategy/skills/stanley-druckenmiller-investment/references/investment-philosophy.md +80 -0
- package/.claude/skills/weekly-trade-strategy/skills/stanley-druckenmiller-investment/references/market-analysis-guide.md +146 -0
- package/.claude/skills/weekly-trade-strategy/skills/technical-analyst/SKILL.md +238 -0
- package/.claude/skills/weekly-trade-strategy/skills/technical-analyst/assets/analysis_template.md +183 -0
- package/.claude/skills/weekly-trade-strategy/skills/technical-analyst/references/technical_analysis_framework.md +282 -0
- package/.claude/skills/weekly-trade-strategy/skills/us-market-bubble-detector/CHANGELOG.md +118 -0
- package/.claude/skills/weekly-trade-strategy/skills/us-market-bubble-detector/SKILL.md +545 -0
- package/.claude/skills/weekly-trade-strategy/skills/us-market-bubble-detector/references/bubble_framework.md +335 -0
- package/.claude/skills/weekly-trade-strategy/skills/us-market-bubble-detector/references/historical_cases.md +327 -0
- package/.claude/skills/weekly-trade-strategy/skills/us-market-bubble-detector/references/implementation_guide.md +473 -0
- package/.claude/skills/weekly-trade-strategy/skills/us-market-bubble-detector/references/quick_reference.md +354 -0
- package/.claude/skills/weekly-trade-strategy/skills/us-market-bubble-detector/references/quick_reference_en.md +342 -0
- package/.claude/skills/weekly-trade-strategy/skills/us-market-bubble-detector/scripts/bubble_scorer.py +309 -0
- package/.claude/skills/weekly-trade-strategy/skills/us-stock-analysis/SKILL.md +294 -0
- package/.claude/skills/weekly-trade-strategy/skills/us-stock-analysis/references/financial-metrics.md +172 -0
- package/.claude/skills/weekly-trade-strategy/skills/us-stock-analysis/references/fundamental-analysis.md +129 -0
- package/.claude/skills/weekly-trade-strategy/skills/us-stock-analysis/references/report-template.md +207 -0
- package/.claude/skills/weekly-trade-strategy/skills/us-stock-analysis/references/technical-analysis.md +93 -0
- package/.mcp.json +3 -0
- package/cli.mjs +16 -16
- package/package.json +4 -2
|
@@ -0,0 +1,473 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Market Top Detector - Composite Scoring Engine
|
|
4
|
+
|
|
5
|
+
Combines 6 component scores into a weighted composite (0-100).
|
|
6
|
+
|
|
7
|
+
Component Weights:
|
|
8
|
+
1. Distribution Day Count: 25%
|
|
9
|
+
2. Leading Stock Health: 20%
|
|
10
|
+
3. Defensive Sector Rotation: 15%
|
|
11
|
+
4. Market Breadth Divergence: 15%
|
|
12
|
+
5. Index Technical Condition: 15%
|
|
13
|
+
6. Sentiment & Speculation: 10%
|
|
14
|
+
Total: 100%
|
|
15
|
+
|
|
16
|
+
Risk Zone Mapping:
|
|
17
|
+
0-20: Green (Normal) - Risk Budget: 100%
|
|
18
|
+
21-40: Yellow (Early Warning) - Risk Budget: 80-90%
|
|
19
|
+
41-60: Orange (Elevated Risk) - Risk Budget: 60-75%
|
|
20
|
+
61-80: Red (High Probability Top)- Risk Budget: 40-55%
|
|
21
|
+
81-100:Critical(Top Formation) - Risk Budget: 20-35%
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from typing import Dict, List, Optional
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
COMPONENT_WEIGHTS = {
|
|
28
|
+
"distribution_days": 0.25,
|
|
29
|
+
"leading_stocks": 0.20,
|
|
30
|
+
"defensive_rotation": 0.15,
|
|
31
|
+
"breadth_divergence": 0.15,
|
|
32
|
+
"index_technical": 0.15,
|
|
33
|
+
"sentiment": 0.10,
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
COMPONENT_LABELS = {
|
|
37
|
+
"distribution_days": "Distribution Day Count",
|
|
38
|
+
"leading_stocks": "Leading Stock Health",
|
|
39
|
+
"defensive_rotation": "Defensive Sector Rotation",
|
|
40
|
+
"breadth_divergence": "Market Breadth Divergence",
|
|
41
|
+
"index_technical": "Index Technical Condition",
|
|
42
|
+
"sentiment": "Sentiment & Speculation",
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def calculate_composite_score(component_scores: Dict[str, float],
|
|
47
|
+
data_availability: Optional[Dict[str, bool]] = None) -> Dict:
|
|
48
|
+
"""
|
|
49
|
+
Calculate weighted composite market top probability score.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
component_scores: Dict with keys matching COMPONENT_WEIGHTS,
|
|
53
|
+
each value 0-100
|
|
54
|
+
data_availability: Optional dict mapping component key -> bool indicating
|
|
55
|
+
if data was actually available (vs neutral default)
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
Dict with composite_score, zone, risk_budget, guidance,
|
|
59
|
+
weakest/strongest components, component breakdown, and data_quality
|
|
60
|
+
"""
|
|
61
|
+
if data_availability is None:
|
|
62
|
+
data_availability = {}
|
|
63
|
+
|
|
64
|
+
# Calculate weighted composite
|
|
65
|
+
composite = 0.0
|
|
66
|
+
for key, weight in COMPONENT_WEIGHTS.items():
|
|
67
|
+
score = component_scores.get(key, 0)
|
|
68
|
+
composite += score * weight
|
|
69
|
+
|
|
70
|
+
composite = round(composite, 1)
|
|
71
|
+
|
|
72
|
+
# Identify strongest and weakest warning signals
|
|
73
|
+
valid_scores = {k: v for k, v in component_scores.items()
|
|
74
|
+
if k in COMPONENT_WEIGHTS}
|
|
75
|
+
|
|
76
|
+
if valid_scores:
|
|
77
|
+
strongest_warning = max(valid_scores, key=valid_scores.get)
|
|
78
|
+
weakest_warning = min(valid_scores, key=valid_scores.get)
|
|
79
|
+
else:
|
|
80
|
+
strongest_warning = "N/A"
|
|
81
|
+
weakest_warning = "N/A"
|
|
82
|
+
|
|
83
|
+
# Get zone interpretation
|
|
84
|
+
zone_info = _interpret_zone(composite)
|
|
85
|
+
|
|
86
|
+
# Calculate data quality
|
|
87
|
+
available_count = sum(
|
|
88
|
+
1 for k in COMPONENT_WEIGHTS
|
|
89
|
+
if data_availability.get(k, True) # Default True for backward compat
|
|
90
|
+
)
|
|
91
|
+
total_components = len(COMPONENT_WEIGHTS)
|
|
92
|
+
missing_components = [
|
|
93
|
+
COMPONENT_LABELS[k] for k in COMPONENT_WEIGHTS
|
|
94
|
+
if not data_availability.get(k, True)
|
|
95
|
+
]
|
|
96
|
+
|
|
97
|
+
if available_count == total_components:
|
|
98
|
+
quality_label = f"Complete ({available_count}/{total_components} components)"
|
|
99
|
+
elif available_count >= total_components - 2:
|
|
100
|
+
quality_label = (f"Partial ({available_count}/{total_components} components)"
|
|
101
|
+
" - interpret with caution")
|
|
102
|
+
else:
|
|
103
|
+
quality_label = (f"Limited ({available_count}/{total_components} components)"
|
|
104
|
+
" - low confidence")
|
|
105
|
+
|
|
106
|
+
data_quality = {
|
|
107
|
+
"available_count": available_count,
|
|
108
|
+
"total_components": total_components,
|
|
109
|
+
"label": quality_label,
|
|
110
|
+
"missing_components": missing_components,
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
"composite_score": composite,
|
|
115
|
+
"zone": zone_info["zone"],
|
|
116
|
+
"zone_color": zone_info["color"],
|
|
117
|
+
"risk_budget": zone_info["risk_budget"],
|
|
118
|
+
"guidance": zone_info["guidance"],
|
|
119
|
+
"actions": zone_info["actions"],
|
|
120
|
+
"strongest_warning": {
|
|
121
|
+
"component": strongest_warning,
|
|
122
|
+
"label": COMPONENT_LABELS.get(strongest_warning, strongest_warning),
|
|
123
|
+
"score": valid_scores.get(strongest_warning, 0),
|
|
124
|
+
},
|
|
125
|
+
"weakest_warning": {
|
|
126
|
+
"component": weakest_warning,
|
|
127
|
+
"label": COMPONENT_LABELS.get(weakest_warning, weakest_warning),
|
|
128
|
+
"score": valid_scores.get(weakest_warning, 0),
|
|
129
|
+
},
|
|
130
|
+
"data_quality": data_quality,
|
|
131
|
+
"component_scores": {
|
|
132
|
+
k: {
|
|
133
|
+
"score": component_scores.get(k, 0),
|
|
134
|
+
"weight": w,
|
|
135
|
+
"weighted_contribution": round(component_scores.get(k, 0) * w, 1),
|
|
136
|
+
"label": COMPONENT_LABELS[k],
|
|
137
|
+
}
|
|
138
|
+
for k, w in COMPONENT_WEIGHTS.items()
|
|
139
|
+
},
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _interpret_zone(composite: float) -> Dict:
|
|
144
|
+
"""Map composite score to risk zone"""
|
|
145
|
+
if composite <= 20:
|
|
146
|
+
return {
|
|
147
|
+
"zone": "Green (Normal)",
|
|
148
|
+
"color": "green",
|
|
149
|
+
"risk_budget": "100%",
|
|
150
|
+
"guidance": "Normal market conditions. Maintain standard position management.",
|
|
151
|
+
"actions": [
|
|
152
|
+
"Normal position sizing",
|
|
153
|
+
"Standard stop-loss levels",
|
|
154
|
+
"New position entries allowed",
|
|
155
|
+
],
|
|
156
|
+
}
|
|
157
|
+
elif composite <= 40:
|
|
158
|
+
return {
|
|
159
|
+
"zone": "Yellow (Early Warning)",
|
|
160
|
+
"color": "yellow",
|
|
161
|
+
"risk_budget": "80-90%",
|
|
162
|
+
"guidance": "Early warning signs detected. Tighten stops and reduce new entries.",
|
|
163
|
+
"actions": [
|
|
164
|
+
"Tighten stop-losses by 10-20%",
|
|
165
|
+
"Reduce new position sizes by 25-50%",
|
|
166
|
+
"Review weakest positions for exits",
|
|
167
|
+
"Monitor distribution days closely",
|
|
168
|
+
],
|
|
169
|
+
}
|
|
170
|
+
elif composite <= 60:
|
|
171
|
+
return {
|
|
172
|
+
"zone": "Orange (Elevated Risk)",
|
|
173
|
+
"color": "orange",
|
|
174
|
+
"risk_budget": "60-75%",
|
|
175
|
+
"guidance": "Elevated risk of correction. Begin profit-taking on weaker positions.",
|
|
176
|
+
"actions": [
|
|
177
|
+
"Take profits on weakest 25-30% of positions",
|
|
178
|
+
"No new momentum entries",
|
|
179
|
+
"Only quality stocks near support",
|
|
180
|
+
"Raise cash allocation",
|
|
181
|
+
"Watch for Follow-Through Day if market pulls back",
|
|
182
|
+
],
|
|
183
|
+
}
|
|
184
|
+
elif composite <= 80:
|
|
185
|
+
return {
|
|
186
|
+
"zone": "Red (High Probability Top)",
|
|
187
|
+
"color": "red",
|
|
188
|
+
"risk_budget": "40-55%",
|
|
189
|
+
"guidance": "High probability of market top. Aggressive profit-taking recommended.",
|
|
190
|
+
"actions": [
|
|
191
|
+
"Aggressive profit-taking (sell 40-50% of positions)",
|
|
192
|
+
"Maximum cash allocation",
|
|
193
|
+
"Only hold strongest leaders",
|
|
194
|
+
"Consider hedges (put options, inverse ETFs)",
|
|
195
|
+
"Prepare short watchlist",
|
|
196
|
+
],
|
|
197
|
+
}
|
|
198
|
+
else:
|
|
199
|
+
return {
|
|
200
|
+
"zone": "Critical (Top Formation)",
|
|
201
|
+
"color": "critical",
|
|
202
|
+
"risk_budget": "20-35%",
|
|
203
|
+
"guidance": "Top formation in progress. Maximum defensive posture.",
|
|
204
|
+
"actions": [
|
|
205
|
+
"Sell most positions (keep only 20-35% invested)",
|
|
206
|
+
"Full hedge implementation",
|
|
207
|
+
"Short positions on weakest leaders",
|
|
208
|
+
"Preserve capital as primary objective",
|
|
209
|
+
"Watch for capitulation/Follow-Through Day for re-entry",
|
|
210
|
+
],
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def detect_follow_through_day(index_history: List[Dict],
|
|
215
|
+
composite_score: float) -> Dict:
|
|
216
|
+
"""
|
|
217
|
+
Detect Follow-Through Day (FTD) signal for bottom confirmation.
|
|
218
|
+
Only relevant when composite > 40 (Orange zone or worse).
|
|
219
|
+
|
|
220
|
+
O'Neil's Strict FTD Rules:
|
|
221
|
+
1. Identify swing low: significant decline (3+ down days, -3%+ from recent high)
|
|
222
|
+
2. Rally Day 1: first up day (close > previous close) after swing low
|
|
223
|
+
3. FTD: Day 4-7 of rally, gain >= 1.5% on volume higher than previous day
|
|
224
|
+
4. Rally resets if price closes below swing low
|
|
225
|
+
|
|
226
|
+
Args:
|
|
227
|
+
index_history: Daily OHLCV (most recent first)
|
|
228
|
+
composite_score: Current composite score
|
|
229
|
+
|
|
230
|
+
Returns:
|
|
231
|
+
Dict with ftd_detected, rally_day_count, details
|
|
232
|
+
"""
|
|
233
|
+
if composite_score < 40:
|
|
234
|
+
return {
|
|
235
|
+
"ftd_detected": False,
|
|
236
|
+
"applicable": False,
|
|
237
|
+
"reason": "Composite < 40 (Green/Yellow zone) - FTD monitoring not needed",
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if not index_history or len(index_history) < 10:
|
|
241
|
+
return {
|
|
242
|
+
"ftd_detected": False,
|
|
243
|
+
"applicable": True,
|
|
244
|
+
"reason": "Insufficient data for FTD analysis",
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
# Work in chronological order (oldest first)
|
|
248
|
+
history = list(reversed(index_history))
|
|
249
|
+
n = len(history)
|
|
250
|
+
|
|
251
|
+
# Only look at the most recent 40 trading days
|
|
252
|
+
lookback = min(40, n)
|
|
253
|
+
history = history[n - lookback:]
|
|
254
|
+
n = len(history)
|
|
255
|
+
|
|
256
|
+
# Step 1: Find swing low within the lookback window
|
|
257
|
+
# Swing low = lowest close after a decline of 3%+ from a recent high
|
|
258
|
+
swing_low_idx = None
|
|
259
|
+
swing_low_price = None
|
|
260
|
+
|
|
261
|
+
# Scan for the most recent swing low
|
|
262
|
+
for i in range(n - 1, 2, -1): # from recent to old
|
|
263
|
+
low_close = history[i].get("close", 0)
|
|
264
|
+
if low_close <= 0:
|
|
265
|
+
continue
|
|
266
|
+
|
|
267
|
+
# Look back from this point for a recent high (within 20 days)
|
|
268
|
+
search_start = max(0, i - 20)
|
|
269
|
+
recent_high = 0
|
|
270
|
+
for j in range(search_start, i):
|
|
271
|
+
c = history[j].get("close", 0)
|
|
272
|
+
if c > recent_high:
|
|
273
|
+
recent_high = c
|
|
274
|
+
|
|
275
|
+
if recent_high <= 0:
|
|
276
|
+
continue
|
|
277
|
+
|
|
278
|
+
decline_pct = (low_close - recent_high) / recent_high * 100
|
|
279
|
+
|
|
280
|
+
# Check for 3%+ decline from recent high
|
|
281
|
+
if decline_pct <= -3.0:
|
|
282
|
+
# Verify at least 3 down days in the decline
|
|
283
|
+
down_days = 0
|
|
284
|
+
for j in range(search_start + 1, i + 1):
|
|
285
|
+
prev_c = history[j - 1].get("close", 0)
|
|
286
|
+
curr_c = history[j].get("close", 0)
|
|
287
|
+
if prev_c > 0 and curr_c < prev_c:
|
|
288
|
+
down_days += 1
|
|
289
|
+
|
|
290
|
+
if down_days >= 3:
|
|
291
|
+
# Check this is actually a local low
|
|
292
|
+
is_local_low = True
|
|
293
|
+
if i > 0 and history[i - 1].get("close", 0) < low_close:
|
|
294
|
+
is_local_low = False
|
|
295
|
+
if i + 1 < n and history[i + 1].get("close", 0) < low_close:
|
|
296
|
+
is_local_low = False
|
|
297
|
+
|
|
298
|
+
if is_local_low:
|
|
299
|
+
swing_low_idx = i
|
|
300
|
+
swing_low_price = low_close
|
|
301
|
+
break
|
|
302
|
+
|
|
303
|
+
if swing_low_idx is None:
|
|
304
|
+
return {
|
|
305
|
+
"ftd_detected": False,
|
|
306
|
+
"applicable": True,
|
|
307
|
+
"reason": "No qualifying swing low found (need 3%+ decline with 3+ down days)",
|
|
308
|
+
"rally_day_count": 0,
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
# Step 2: Find Rally Day 1 (first up day after swing low)
|
|
312
|
+
rally_day_1_idx = None
|
|
313
|
+
for i in range(swing_low_idx + 1, n):
|
|
314
|
+
curr_close = history[i].get("close", 0)
|
|
315
|
+
prev_close = history[i - 1].get("close", 0)
|
|
316
|
+
if prev_close > 0 and curr_close > prev_close:
|
|
317
|
+
rally_day_1_idx = i
|
|
318
|
+
break
|
|
319
|
+
|
|
320
|
+
if rally_day_1_idx is None:
|
|
321
|
+
return {
|
|
322
|
+
"ftd_detected": False,
|
|
323
|
+
"applicable": True,
|
|
324
|
+
"reason": "No rally attempt started after swing low",
|
|
325
|
+
"rally_day_count": 0,
|
|
326
|
+
"swing_low_date": history[swing_low_idx].get("date", "N/A"),
|
|
327
|
+
"swing_low_price": swing_low_price,
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
# Step 3: Count rally days and check for FTD / reset
|
|
331
|
+
rally_day_count = 0
|
|
332
|
+
ftd_detected = False
|
|
333
|
+
ftd_day = None
|
|
334
|
+
rally_reset = False
|
|
335
|
+
|
|
336
|
+
for i in range(rally_day_1_idx, n):
|
|
337
|
+
curr_close = history[i].get("close", 0)
|
|
338
|
+
|
|
339
|
+
# Check for rally reset: price closes below swing low
|
|
340
|
+
if curr_close < swing_low_price:
|
|
341
|
+
rally_reset = True
|
|
342
|
+
# Try to find a new swing low from here
|
|
343
|
+
swing_low_idx = i
|
|
344
|
+
swing_low_price = curr_close
|
|
345
|
+
rally_day_count = 0
|
|
346
|
+
ftd_detected = False
|
|
347
|
+
rally_reset = False
|
|
348
|
+
# Look for new rally day 1
|
|
349
|
+
continue
|
|
350
|
+
|
|
351
|
+
prev_close = history[i - 1].get("close", 0) if i > 0 else 0
|
|
352
|
+
|
|
353
|
+
if prev_close > 0 and curr_close > prev_close:
|
|
354
|
+
rally_day_count += 1
|
|
355
|
+
elif prev_close > 0 and curr_close <= prev_close:
|
|
356
|
+
# Down day during rally - still count toward rally days
|
|
357
|
+
rally_day_count += 1
|
|
358
|
+
|
|
359
|
+
# Check FTD on days 4-7
|
|
360
|
+
if 4 <= rally_day_count <= 7 and prev_close > 0:
|
|
361
|
+
gain_pct = (curr_close - prev_close) / prev_close * 100
|
|
362
|
+
curr_volume = history[i].get("volume", 0)
|
|
363
|
+
prev_volume = history[i - 1].get("volume", 0)
|
|
364
|
+
|
|
365
|
+
if gain_pct >= 1.5 and prev_volume > 0 and curr_volume > prev_volume:
|
|
366
|
+
ftd_detected = True
|
|
367
|
+
ftd_day = history[i].get("date", f"day-{i}")
|
|
368
|
+
break
|
|
369
|
+
|
|
370
|
+
# Cap rally_day_count for reporting
|
|
371
|
+
days_since_rally_start = n - rally_day_1_idx
|
|
372
|
+
rally_day_count = min(rally_day_count, days_since_rally_start)
|
|
373
|
+
|
|
374
|
+
swing_low_date = history[swing_low_idx].get("date", "N/A")
|
|
375
|
+
|
|
376
|
+
if ftd_detected:
|
|
377
|
+
reason = f"Follow-Through Day detected on {ftd_day} (Day {rally_day_count} of rally from {swing_low_date} low)"
|
|
378
|
+
elif rally_day_count >= 7:
|
|
379
|
+
reason = (f"Rally attempt: Day {rally_day_count} from {swing_low_date} low - "
|
|
380
|
+
"FTD window (Day 4-7) passed without qualifying day")
|
|
381
|
+
else:
|
|
382
|
+
reason = (f"Rally attempt: Day {rally_day_count} from {swing_low_date} low "
|
|
383
|
+
f"(FTD requires Day 4-7 with >= 1.5% gain on higher volume)")
|
|
384
|
+
|
|
385
|
+
return {
|
|
386
|
+
"ftd_detected": ftd_detected,
|
|
387
|
+
"applicable": True,
|
|
388
|
+
"rally_day_count": rally_day_count,
|
|
389
|
+
"ftd_day": ftd_day,
|
|
390
|
+
"swing_low_date": swing_low_date,
|
|
391
|
+
"swing_low_price": swing_low_price,
|
|
392
|
+
"reason": reason,
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
# Testing
|
|
397
|
+
if __name__ == "__main__":
|
|
398
|
+
print("Testing Market Top Scorer...\n")
|
|
399
|
+
|
|
400
|
+
# Test 1: Moderate risk scenario (calibration target: ~50)
|
|
401
|
+
test_scores = {
|
|
402
|
+
"distribution_days": 45,
|
|
403
|
+
"leading_stocks": 52,
|
|
404
|
+
"defensive_rotation": 82,
|
|
405
|
+
"breadth_divergence": 20,
|
|
406
|
+
"index_technical": 42,
|
|
407
|
+
"sentiment": 62,
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
result = calculate_composite_score(test_scores)
|
|
411
|
+
print(f"Test 1 - Moderate Risk:")
|
|
412
|
+
print(f" Composite: {result['composite_score']}/100")
|
|
413
|
+
print(f" Zone: {result['zone']}")
|
|
414
|
+
print(f" Risk Budget: {result['risk_budget']}")
|
|
415
|
+
print(f" Strongest Warning: {result['strongest_warning']['label']} "
|
|
416
|
+
f"({result['strongest_warning']['score']})")
|
|
417
|
+
print()
|
|
418
|
+
|
|
419
|
+
# Test 2: Healthy market
|
|
420
|
+
healthy = {
|
|
421
|
+
"distribution_days": 0,
|
|
422
|
+
"leading_stocks": 10,
|
|
423
|
+
"defensive_rotation": 0,
|
|
424
|
+
"breadth_divergence": 10,
|
|
425
|
+
"index_technical": 5,
|
|
426
|
+
"sentiment": 15,
|
|
427
|
+
}
|
|
428
|
+
result2 = calculate_composite_score(healthy)
|
|
429
|
+
print(f"Test 2 - Healthy Market:")
|
|
430
|
+
print(f" Composite: {result2['composite_score']}/100")
|
|
431
|
+
print(f" Zone: {result2['zone']}")
|
|
432
|
+
print()
|
|
433
|
+
|
|
434
|
+
# Test 3: Crisis
|
|
435
|
+
crisis = {
|
|
436
|
+
"distribution_days": 100,
|
|
437
|
+
"leading_stocks": 90,
|
|
438
|
+
"defensive_rotation": 85,
|
|
439
|
+
"breadth_divergence": 80,
|
|
440
|
+
"index_technical": 75,
|
|
441
|
+
"sentiment": 70,
|
|
442
|
+
}
|
|
443
|
+
result3 = calculate_composite_score(crisis)
|
|
444
|
+
print(f"Test 3 - Crisis:")
|
|
445
|
+
print(f" Composite: {result3['composite_score']}/100")
|
|
446
|
+
print(f" Zone: {result3['zone']}")
|
|
447
|
+
print()
|
|
448
|
+
|
|
449
|
+
# Test 4: Data quality tracking
|
|
450
|
+
partial_scores = {
|
|
451
|
+
"distribution_days": 45,
|
|
452
|
+
"leading_stocks": 52,
|
|
453
|
+
"defensive_rotation": 50,
|
|
454
|
+
"breadth_divergence": 50,
|
|
455
|
+
"index_technical": 42,
|
|
456
|
+
"sentiment": 50,
|
|
457
|
+
}
|
|
458
|
+
partial_availability = {
|
|
459
|
+
"distribution_days": True,
|
|
460
|
+
"leading_stocks": True,
|
|
461
|
+
"defensive_rotation": False,
|
|
462
|
+
"breadth_divergence": False,
|
|
463
|
+
"index_technical": True,
|
|
464
|
+
"sentiment": False,
|
|
465
|
+
}
|
|
466
|
+
result4 = calculate_composite_score(partial_scores, partial_availability)
|
|
467
|
+
print(f"Test 4 - Partial Data:")
|
|
468
|
+
print(f" Composite: {result4['composite_score']}/100")
|
|
469
|
+
print(f" Data Quality: {result4['data_quality']['label']}")
|
|
470
|
+
print(f" Missing: {result4['data_quality']['missing_components']}")
|
|
471
|
+
print()
|
|
472
|
+
|
|
473
|
+
print("All tests completed.")
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""Shared fixtures for Market Top Detector tests"""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
# Add scripts directory to path so calculators can be imported
|
|
7
|
+
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
|
8
|
+
# Add tests directory to path so helpers can be imported
|
|
9
|
+
sys.path.insert(0, os.path.dirname(__file__))
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""Importable test helpers for Market Top Detector tests"""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def make_daily_bar(close, volume=1000000, date="2026-01-15", open_=None, high=None, low=None):
|
|
5
|
+
"""Helper to create a daily OHLCV bar dict."""
|
|
6
|
+
if open_ is None:
|
|
7
|
+
open_ = close
|
|
8
|
+
if high is None:
|
|
9
|
+
high = close * 1.005
|
|
10
|
+
if low is None:
|
|
11
|
+
low = close * 0.995
|
|
12
|
+
return {
|
|
13
|
+
"date": date,
|
|
14
|
+
"open": open_,
|
|
15
|
+
"high": high,
|
|
16
|
+
"low": low,
|
|
17
|
+
"close": close,
|
|
18
|
+
"adjClose": close,
|
|
19
|
+
"volume": volume,
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def make_history(closes, base_volume=1000000, start_date="2026-01-15"):
|
|
24
|
+
"""Create a list of daily bars from a list of closes (most recent first)."""
|
|
25
|
+
bars = []
|
|
26
|
+
for i, close in enumerate(closes):
|
|
27
|
+
vol = base_volume + (i * 1000) # Slight volume variation
|
|
28
|
+
bars.append(
|
|
29
|
+
make_daily_bar(
|
|
30
|
+
close=close,
|
|
31
|
+
volume=vol,
|
|
32
|
+
date=f"day-{i}",
|
|
33
|
+
)
|
|
34
|
+
)
|
|
35
|
+
return bars
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def make_history_with_volumes(close_volume_pairs, start_date="2026-01-15"):
|
|
39
|
+
"""Create bars from list of (close, volume) tuples (most recent first)."""
|
|
40
|
+
bars = []
|
|
41
|
+
for i, (close, volume) in enumerate(close_volume_pairs):
|
|
42
|
+
bars.append(
|
|
43
|
+
make_daily_bar(
|
|
44
|
+
close=close,
|
|
45
|
+
volume=volume,
|
|
46
|
+
date=f"day-{i}",
|
|
47
|
+
)
|
|
48
|
+
)
|
|
49
|
+
return bars
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""Tests for Breadth Calculator"""
|
|
2
|
+
|
|
3
|
+
from calculators.breadth_calculator import (
|
|
4
|
+
_score_200dma_breadth,
|
|
5
|
+
calculate_breadth_divergence,
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class TestScore200dmaBreadth:
|
|
10
|
+
"""Boundary tests for 200DMA scoring."""
|
|
11
|
+
|
|
12
|
+
def test_critical_below_40(self):
|
|
13
|
+
assert _score_200dma_breadth(35) == 100
|
|
14
|
+
|
|
15
|
+
def test_healthy_above_70(self):
|
|
16
|
+
assert _score_200dma_breadth(75) == 5
|
|
17
|
+
|
|
18
|
+
def test_calibration_62_26(self):
|
|
19
|
+
"""62.26% breadth should score ~24 (healthy)."""
|
|
20
|
+
score = _score_200dma_breadth(62.26)
|
|
21
|
+
assert 18 <= score <= 30 # Reasonable range around 24
|
|
22
|
+
|
|
23
|
+
def test_boundary_50(self):
|
|
24
|
+
score = _score_200dma_breadth(50)
|
|
25
|
+
assert score == 55
|
|
26
|
+
|
|
27
|
+
def test_boundary_60(self):
|
|
28
|
+
score = _score_200dma_breadth(60)
|
|
29
|
+
assert score == 30
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class TestCalculateBreadthDivergence:
|
|
33
|
+
"""Integration tests."""
|
|
34
|
+
|
|
35
|
+
def test_missing_breadth_returns_50(self):
|
|
36
|
+
"""None 200DMA → score 50, data_available=False."""
|
|
37
|
+
result = calculate_breadth_divergence(None, None, -2.0)
|
|
38
|
+
assert result["score"] == 50
|
|
39
|
+
assert result["data_available"] is False
|
|
40
|
+
|
|
41
|
+
def test_available_data_has_flag(self):
|
|
42
|
+
"""Valid data → data_available=True."""
|
|
43
|
+
result = calculate_breadth_divergence(65.0, 55.0, -1.0)
|
|
44
|
+
assert result["data_available"] is True
|
|
45
|
+
|
|
46
|
+
def test_near_highs_divergence(self):
|
|
47
|
+
"""Index near highs + weak breadth → high score."""
|
|
48
|
+
result = calculate_breadth_divergence(45.0, 30.0, -2.0)
|
|
49
|
+
assert result["score"] >= 70
|
|
50
|
+
assert result["divergence_detected"] is True
|
|
51
|
+
|
|
52
|
+
def test_not_near_highs_halved(self):
|
|
53
|
+
"""Index NOT near highs → score halved."""
|
|
54
|
+
near = calculate_breadth_divergence(45.0, None, -2.0)
|
|
55
|
+
far = calculate_breadth_divergence(45.0, None, -8.0)
|
|
56
|
+
assert far["score"] < near["score"]
|
|
57
|
+
|
|
58
|
+
def test_50dma_supplement_boost(self):
|
|
59
|
+
"""Low 50DMA breadth at highs adds +10."""
|
|
60
|
+
base = calculate_breadth_divergence(55.0, 80.0, -2.0)
|
|
61
|
+
boosted = calculate_breadth_divergence(55.0, 25.0, -2.0)
|
|
62
|
+
assert boosted["score"] > base["score"]
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""Tests for Defensive Rotation Calculator"""
|
|
2
|
+
|
|
3
|
+
from calculators.defensive_rotation_calculator import (
|
|
4
|
+
_score_rotation,
|
|
5
|
+
calculate_defensive_rotation,
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class TestScoreRotation:
|
|
10
|
+
"""Boundary tests for rotation scoring."""
|
|
11
|
+
|
|
12
|
+
def test_growth_leading(self):
|
|
13
|
+
"""Negative relative = growth leading = healthy."""
|
|
14
|
+
assert _score_rotation(-3.0) == 0
|
|
15
|
+
assert _score_rotation(-1.0) <= 15
|
|
16
|
+
|
|
17
|
+
def test_slight_defensive_tilt(self):
|
|
18
|
+
assert 20 <= _score_rotation(0.3) <= 40
|
|
19
|
+
|
|
20
|
+
def test_strong_defensive_rotation(self):
|
|
21
|
+
assert _score_rotation(5.0) == 100
|
|
22
|
+
|
|
23
|
+
def test_moderate_rotation(self):
|
|
24
|
+
assert 60 <= _score_rotation(2.0) <= 80
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class TestCalculateDefensiveRotation:
|
|
28
|
+
"""Integration tests."""
|
|
29
|
+
|
|
30
|
+
def test_missing_data_returns_50(self):
|
|
31
|
+
"""No ETF data → score 50 (neutral), data_available=False."""
|
|
32
|
+
result = calculate_defensive_rotation({})
|
|
33
|
+
assert result["score"] == 50
|
|
34
|
+
assert result["data_available"] is False
|
|
35
|
+
|
|
36
|
+
def test_partial_data_still_computes(self):
|
|
37
|
+
"""At least 1 defensive + 1 offensive ETF should compute."""
|
|
38
|
+
historical = {
|
|
39
|
+
"XLU": [{"close": 70 + i * 0.1, "volume": 500000} for i in range(25)],
|
|
40
|
+
"XLK": [{"close": 200 - i * 0.5, "volume": 2000000} for i in range(25)],
|
|
41
|
+
}
|
|
42
|
+
result = calculate_defensive_rotation(historical)
|
|
43
|
+
assert result["data_available"] is True
|
|
44
|
+
assert "score" in result
|
|
45
|
+
|
|
46
|
+
def test_growth_leading_scenario(self):
|
|
47
|
+
"""Offensive outperforming defensive → low score."""
|
|
48
|
+
historical = {}
|
|
49
|
+
for sym in ["XLU", "XLP", "XLV", "VNQ"]:
|
|
50
|
+
# Defensive: flat
|
|
51
|
+
historical[sym] = [{"close": 50, "volume": 500000} for _ in range(25)]
|
|
52
|
+
for sym in ["XLK", "XLC", "XLY", "QQQ"]:
|
|
53
|
+
# Offensive: +5% over 20 days
|
|
54
|
+
historical[sym] = [{"close": 105 - i * 0.25, "volume": 2000000} for i in range(25)]
|
|
55
|
+
result = calculate_defensive_rotation(historical)
|
|
56
|
+
assert result["score"] <= 20
|