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,92 @@
|
|
|
1
|
+
"""Tests for Distribution Day Calculator"""
|
|
2
|
+
|
|
3
|
+
from calculators.distribution_day_calculator import (
|
|
4
|
+
_count_distribution_days,
|
|
5
|
+
_score_distribution_days,
|
|
6
|
+
calculate_distribution_days,
|
|
7
|
+
)
|
|
8
|
+
from helpers import make_history, make_history_with_volumes
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class TestScoreDistributionDays:
|
|
12
|
+
"""Boundary tests for scoring thresholds."""
|
|
13
|
+
|
|
14
|
+
def test_zero_days(self):
|
|
15
|
+
assert _score_distribution_days(0) == 0
|
|
16
|
+
|
|
17
|
+
def test_one_day(self):
|
|
18
|
+
assert _score_distribution_days(1) == 15
|
|
19
|
+
|
|
20
|
+
def test_two_days(self):
|
|
21
|
+
assert _score_distribution_days(2) == 30
|
|
22
|
+
|
|
23
|
+
def test_three_days(self):
|
|
24
|
+
assert _score_distribution_days(3) == 55
|
|
25
|
+
|
|
26
|
+
def test_four_days_oneil_threshold(self):
|
|
27
|
+
assert _score_distribution_days(4) == 75
|
|
28
|
+
|
|
29
|
+
def test_five_days(self):
|
|
30
|
+
assert _score_distribution_days(5) == 90
|
|
31
|
+
|
|
32
|
+
def test_six_plus_days(self):
|
|
33
|
+
assert _score_distribution_days(6) == 100
|
|
34
|
+
assert _score_distribution_days(8) == 100
|
|
35
|
+
|
|
36
|
+
def test_fractional_stalling(self):
|
|
37
|
+
"""Stalling days count as 0.5"""
|
|
38
|
+
assert _score_distribution_days(3.5) == 55 # 3.5 >= 3
|
|
39
|
+
assert _score_distribution_days(4.5) == 75 # 4.5 >= 4
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class TestCountDistributionDays:
|
|
43
|
+
"""Tests for distribution day detection logic."""
|
|
44
|
+
|
|
45
|
+
def test_empty_history(self):
|
|
46
|
+
result = _count_distribution_days([], "TEST")
|
|
47
|
+
assert result["distribution_days"] == 0
|
|
48
|
+
|
|
49
|
+
def test_single_day(self):
|
|
50
|
+
result = _count_distribution_days([{"close": 100, "volume": 1000}], "TEST")
|
|
51
|
+
assert result["distribution_days"] == 0
|
|
52
|
+
|
|
53
|
+
def test_distribution_day_detected(self):
|
|
54
|
+
"""Price drop >= 0.2% on higher volume = distribution day."""
|
|
55
|
+
history = make_history_with_volumes(
|
|
56
|
+
[
|
|
57
|
+
(99.0, 1200000), # Today: -1% drop, higher volume → distribution
|
|
58
|
+
(100.0, 1000000), # Yesterday
|
|
59
|
+
(100.5, 800000), # Two days ago: lower volume than yesterday
|
|
60
|
+
]
|
|
61
|
+
)
|
|
62
|
+
result = _count_distribution_days(history, "TEST")
|
|
63
|
+
assert result["distribution_days"] >= 1
|
|
64
|
+
|
|
65
|
+
def test_stalling_day_detected(self):
|
|
66
|
+
"""Volume increases but price gain < 0.1% = stalling day."""
|
|
67
|
+
history = make_history_with_volumes(
|
|
68
|
+
[
|
|
69
|
+
(100.05, 1200000), # Today: +0.05%, higher volume
|
|
70
|
+
(100.0, 1000000), # Yesterday
|
|
71
|
+
(99.5, 900000), # Two days ago
|
|
72
|
+
]
|
|
73
|
+
)
|
|
74
|
+
result = _count_distribution_days(history, "TEST")
|
|
75
|
+
assert result["stalling_days"] >= 1
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class TestCalculateDistributionDays:
|
|
79
|
+
"""Integration tests."""
|
|
80
|
+
|
|
81
|
+
def test_uses_higher_count(self):
|
|
82
|
+
"""Should use the worse of S&P 500 / NASDAQ."""
|
|
83
|
+
# Create histories where NASDAQ has more distribution days
|
|
84
|
+
sp_history = make_history([100] * 30)
|
|
85
|
+
# NASDAQ with distribution pattern
|
|
86
|
+
nasdaq_cv = [(99.0 - i * 0.3, 1200000 + i * 10000) for i in range(15)]
|
|
87
|
+
nasdaq_cv += [(100.0 + i * 0.1, 1000000) for i in range(15)]
|
|
88
|
+
nasdaq_history = make_history_with_volumes(nasdaq_cv)
|
|
89
|
+
|
|
90
|
+
result = calculate_distribution_days(sp_history, nasdaq_history)
|
|
91
|
+
assert "score" in result
|
|
92
|
+
assert "effective_count" in result
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""Tests for Index Technical Calculator"""
|
|
2
|
+
|
|
3
|
+
from calculators.index_technical_calculator import (
|
|
4
|
+
_evaluate_index,
|
|
5
|
+
calculate_index_technical,
|
|
6
|
+
)
|
|
7
|
+
from helpers import make_history
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class TestEvaluateIndex:
|
|
11
|
+
"""Test single index evaluation."""
|
|
12
|
+
|
|
13
|
+
def test_insufficient_data(self):
|
|
14
|
+
"""Less than 21 days → data_available=False."""
|
|
15
|
+
result = _evaluate_index("TEST", [{"close": 100}] * 10)
|
|
16
|
+
assert result["data_available"] is False
|
|
17
|
+
assert result["raw_score"] == 0
|
|
18
|
+
|
|
19
|
+
def test_sufficient_data(self):
|
|
20
|
+
"""21+ days → data_available=True."""
|
|
21
|
+
history = make_history([100 - i * 0.1 for i in range(50)])
|
|
22
|
+
result = _evaluate_index("TEST", history)
|
|
23
|
+
assert result["data_available"] is True
|
|
24
|
+
|
|
25
|
+
def test_below_21ema_adds_8(self):
|
|
26
|
+
"""Price significantly below 21 EMA should trigger flag."""
|
|
27
|
+
# Create downtrend: recent prices well below earlier ones
|
|
28
|
+
closes = [90] * 5 + [100 + i * 0.5 for i in range(45)]
|
|
29
|
+
history = make_history(closes)
|
|
30
|
+
result = _evaluate_index("TEST", history)
|
|
31
|
+
assert any("21 EMA" in f for f in result["flags"])
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class TestCalculateIndexTechnical:
|
|
35
|
+
"""Test composite index technical score."""
|
|
36
|
+
|
|
37
|
+
def test_both_available(self):
|
|
38
|
+
"""Both S&P and NASDAQ have data → average them."""
|
|
39
|
+
sp = make_history([100 - i * 0.1 for i in range(50)])
|
|
40
|
+
nq = make_history([200 - i * 0.2 for i in range(50)])
|
|
41
|
+
result = calculate_index_technical(sp, nq)
|
|
42
|
+
assert result["data_available"] is True
|
|
43
|
+
assert result["sp500"]["data_available"] is True
|
|
44
|
+
assert result["nasdaq"]["data_available"] is True
|
|
45
|
+
|
|
46
|
+
def test_nasdaq_missing_uses_sp_only(self):
|
|
47
|
+
"""NASDAQ insufficient data → use S&P 500 only, no halving."""
|
|
48
|
+
sp = make_history([100 - i * 0.1 for i in range(50)])
|
|
49
|
+
nq_short = [{"close": 200}] * 5 # Too short
|
|
50
|
+
|
|
51
|
+
calculate_index_technical(sp, sp) # Both = S&P (warm up)
|
|
52
|
+
result_sp_only = calculate_index_technical(sp, nq_short)
|
|
53
|
+
|
|
54
|
+
# S&P-only score should equal S&P's raw_score (not halved)
|
|
55
|
+
sp_raw = result_sp_only["sp500"]["raw_score"]
|
|
56
|
+
assert result_sp_only["score"] == round(min(100, max(0, sp_raw)))
|
|
57
|
+
assert result_sp_only["nasdaq"]["data_available"] is False
|
|
58
|
+
|
|
59
|
+
def test_both_missing_returns_50(self):
|
|
60
|
+
"""Both insufficient → score=50, data_available=False."""
|
|
61
|
+
short = [{"close": 100}] * 5
|
|
62
|
+
result = calculate_index_technical(short, short)
|
|
63
|
+
assert result["score"] == 50
|
|
64
|
+
assert result["data_available"] is False
|
|
65
|
+
assert "NO DATA" in result["signal"]
|
|
66
|
+
|
|
67
|
+
def test_sp_missing_nasdaq_available(self):
|
|
68
|
+
"""S&P insufficient, NASDAQ available → use NASDAQ only."""
|
|
69
|
+
sp_short = [{"close": 100}] * 5
|
|
70
|
+
nq = make_history([200 - i * 0.2 for i in range(50)])
|
|
71
|
+
result = calculate_index_technical(sp_short, nq)
|
|
72
|
+
nq_raw = result["nasdaq"]["raw_score"]
|
|
73
|
+
assert result["score"] == round(min(100, max(0, nq_raw)))
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""Tests for Leading Stock Calculator"""
|
|
2
|
+
|
|
3
|
+
from calculators.leading_stock_calculator import (
|
|
4
|
+
_evaluate_etf,
|
|
5
|
+
calculate_leading_stock_health,
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class TestEvaluateETF:
|
|
10
|
+
"""Test individual ETF evaluation."""
|
|
11
|
+
|
|
12
|
+
def test_near_highs_healthy(self):
|
|
13
|
+
quote = {"price": 100, "yearHigh": 102, "yearLow": 80}
|
|
14
|
+
history = [{"close": 100 - i * 0.1, "volume": 1000000} for i in range(60)]
|
|
15
|
+
result = _evaluate_etf("TEST", quote, history)
|
|
16
|
+
assert result["deterioration_score"] <= 20
|
|
17
|
+
|
|
18
|
+
def test_deep_correction(self):
|
|
19
|
+
quote = {"price": 70, "yearHigh": 100, "yearLow": 65}
|
|
20
|
+
history = [{"close": 70 + i * 0.3, "volume": 1000000} for i in range(60)]
|
|
21
|
+
result = _evaluate_etf("TEST", quote, history)
|
|
22
|
+
assert result["deterioration_score"] >= 30
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class TestCalculateLeadingStockHealth:
|
|
26
|
+
"""Test composite leading stock calculation."""
|
|
27
|
+
|
|
28
|
+
def test_no_data_returns_50(self):
|
|
29
|
+
"""No quotes → score 50 (neutral), data_available=False."""
|
|
30
|
+
result = calculate_leading_stock_health({}, {})
|
|
31
|
+
assert result["score"] == 50
|
|
32
|
+
assert result["etfs_evaluated"] == 0
|
|
33
|
+
assert result["data_available"] is False
|
|
34
|
+
|
|
35
|
+
def test_healthy_leaders(self):
|
|
36
|
+
quotes = {}
|
|
37
|
+
historical = {}
|
|
38
|
+
for sym in ["ARKK", "WCLD", "IGV"]:
|
|
39
|
+
quotes[sym] = {"price": 100, "yearHigh": 102, "yearLow": 80}
|
|
40
|
+
historical[sym] = [{"close": 100 - i * 0.05, "volume": 1000000} for i in range(60)]
|
|
41
|
+
result = calculate_leading_stock_health(quotes, historical)
|
|
42
|
+
assert result["score"] <= 30 # Healthy
|
|
43
|
+
assert result["data_available"] is True
|
|
44
|
+
|
|
45
|
+
def test_amplification_at_60pct(self):
|
|
46
|
+
"""60%+ ETFs deteriorating triggers 1.3x amplification."""
|
|
47
|
+
quotes = {}
|
|
48
|
+
historical = {}
|
|
49
|
+
# All ETFs in deep correction: -30% from high, below 50DMA
|
|
50
|
+
for sym in ["ARKK", "WCLD", "IGV", "XBI", "SOXX", "SMH", "KWEB", "TAN"]:
|
|
51
|
+
quotes[sym] = {"price": 70, "yearHigh": 100, "yearLow": 60}
|
|
52
|
+
# Create declining history so price is well below MAs
|
|
53
|
+
historical[sym] = [
|
|
54
|
+
{"close": 70 + i * 0.5, "high": 72 + i * 0.5, "volume": 1000000} for i in range(60)
|
|
55
|
+
]
|
|
56
|
+
result = calculate_leading_stock_health(quotes, historical)
|
|
57
|
+
assert result["amplified"] is True
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
"""Tests for Composite Scorer and FTD Detection"""
|
|
2
|
+
|
|
3
|
+
from scorer import (
|
|
4
|
+
COMPONENT_WEIGHTS,
|
|
5
|
+
calculate_composite_score,
|
|
6
|
+
detect_follow_through_day,
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class TestCompositeScore:
|
|
11
|
+
"""Test composite scoring."""
|
|
12
|
+
|
|
13
|
+
def test_all_zeros(self):
|
|
14
|
+
scores = {k: 0 for k in COMPONENT_WEIGHTS}
|
|
15
|
+
result = calculate_composite_score(scores)
|
|
16
|
+
assert result["composite_score"] == 0
|
|
17
|
+
assert "Green" in result["zone"]
|
|
18
|
+
|
|
19
|
+
def test_all_100(self):
|
|
20
|
+
scores = {k: 100 for k in COMPONENT_WEIGHTS}
|
|
21
|
+
result = calculate_composite_score(scores)
|
|
22
|
+
assert result["composite_score"] == 100.0
|
|
23
|
+
assert "Critical" in result["zone"]
|
|
24
|
+
|
|
25
|
+
def test_moderate_risk(self):
|
|
26
|
+
"""Calibration target: ~48-50 for moderate risk."""
|
|
27
|
+
scores = {
|
|
28
|
+
"distribution_days": 45,
|
|
29
|
+
"leading_stocks": 52,
|
|
30
|
+
"defensive_rotation": 82,
|
|
31
|
+
"breadth_divergence": 20,
|
|
32
|
+
"index_technical": 42,
|
|
33
|
+
"sentiment": 62,
|
|
34
|
+
}
|
|
35
|
+
result = calculate_composite_score(scores)
|
|
36
|
+
assert 40 <= result["composite_score"] <= 55
|
|
37
|
+
|
|
38
|
+
def test_weights_sum_to_1(self):
|
|
39
|
+
total = sum(COMPONENT_WEIGHTS.values())
|
|
40
|
+
assert abs(total - 1.0) < 0.001
|
|
41
|
+
|
|
42
|
+
def test_strongest_weakest(self):
|
|
43
|
+
scores = {
|
|
44
|
+
"distribution_days": 90,
|
|
45
|
+
"leading_stocks": 10,
|
|
46
|
+
"defensive_rotation": 50,
|
|
47
|
+
"breadth_divergence": 50,
|
|
48
|
+
"index_technical": 50,
|
|
49
|
+
"sentiment": 50,
|
|
50
|
+
}
|
|
51
|
+
result = calculate_composite_score(scores)
|
|
52
|
+
assert result["strongest_warning"]["component"] == "distribution_days"
|
|
53
|
+
assert result["weakest_warning"]["component"] == "leading_stocks"
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class TestDataQuality:
|
|
57
|
+
"""Test data quality tracking."""
|
|
58
|
+
|
|
59
|
+
def test_all_available(self):
|
|
60
|
+
scores = {k: 50 for k in COMPONENT_WEIGHTS}
|
|
61
|
+
avail = {k: True for k in COMPONENT_WEIGHTS}
|
|
62
|
+
result = calculate_composite_score(scores, avail)
|
|
63
|
+
dq = result["data_quality"]
|
|
64
|
+
assert dq["available_count"] == 6
|
|
65
|
+
assert "Complete" in dq["label"]
|
|
66
|
+
assert dq["missing_components"] == []
|
|
67
|
+
|
|
68
|
+
def test_some_missing(self):
|
|
69
|
+
scores = {k: 50 for k in COMPONENT_WEIGHTS}
|
|
70
|
+
avail = {k: True for k in COMPONENT_WEIGHTS}
|
|
71
|
+
avail["breadth_divergence"] = False
|
|
72
|
+
avail["sentiment"] = False
|
|
73
|
+
result = calculate_composite_score(scores, avail)
|
|
74
|
+
dq = result["data_quality"]
|
|
75
|
+
assert dq["available_count"] == 4
|
|
76
|
+
assert "Partial" in dq["label"]
|
|
77
|
+
assert len(dq["missing_components"]) == 2
|
|
78
|
+
|
|
79
|
+
def test_many_missing(self):
|
|
80
|
+
scores = {k: 50 for k in COMPONENT_WEIGHTS}
|
|
81
|
+
avail = {k: False for k in COMPONENT_WEIGHTS}
|
|
82
|
+
avail["distribution_days"] = True
|
|
83
|
+
avail["leading_stocks"] = True
|
|
84
|
+
result = calculate_composite_score(scores, avail)
|
|
85
|
+
dq = result["data_quality"]
|
|
86
|
+
assert dq["available_count"] == 2
|
|
87
|
+
assert "Limited" in dq["label"]
|
|
88
|
+
|
|
89
|
+
def test_backward_compat_no_availability(self):
|
|
90
|
+
"""Without data_availability, all assumed available."""
|
|
91
|
+
scores = {k: 50 for k in COMPONENT_WEIGHTS}
|
|
92
|
+
result = calculate_composite_score(scores)
|
|
93
|
+
dq = result["data_quality"]
|
|
94
|
+
assert dq["available_count"] == 6
|
|
95
|
+
assert "Complete" in dq["label"]
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class TestFollowThroughDay:
|
|
99
|
+
"""Test O'Neil-strict FTD detection."""
|
|
100
|
+
|
|
101
|
+
def test_not_applicable_below_40(self):
|
|
102
|
+
result = detect_follow_through_day([], 30.0)
|
|
103
|
+
assert result["applicable"] is False
|
|
104
|
+
|
|
105
|
+
def test_insufficient_data(self):
|
|
106
|
+
result = detect_follow_through_day([{"close": 100}] * 5, 50.0)
|
|
107
|
+
assert result["ftd_detected"] is False
|
|
108
|
+
|
|
109
|
+
def test_no_swing_low(self):
|
|
110
|
+
"""Flat market → no swing low found."""
|
|
111
|
+
history = [{"close": 100, "volume": 1000000, "date": f"day-{i}"} for i in range(30)]
|
|
112
|
+
result = detect_follow_through_day(history, 50.0)
|
|
113
|
+
assert result["ftd_detected"] is False
|
|
114
|
+
|
|
115
|
+
def test_ftd_detected_on_clear_pattern(self):
|
|
116
|
+
"""
|
|
117
|
+
Create a clear pattern:
|
|
118
|
+
- Days 0-9: prices rise from 100 to 109 (establishing high)
|
|
119
|
+
- Days 10-14: decline from 109 to 100 (5+ down days, ~8% drop)
|
|
120
|
+
- Day 15: swing low at 100
|
|
121
|
+
- Day 16: rally day 1 (up from 100 to 101)
|
|
122
|
+
- Day 17-18: rally continues (101.5, 102)
|
|
123
|
+
- Day 19 (rally day 4): FTD - big gain +2% on higher volume
|
|
124
|
+
|
|
125
|
+
History is most-recent-first, so reversed.
|
|
126
|
+
"""
|
|
127
|
+
bars = []
|
|
128
|
+
# Build chronologically, then reverse
|
|
129
|
+
# Days 0-9: uptrend
|
|
130
|
+
for i in range(10):
|
|
131
|
+
bars.append({"close": 100 + i, "volume": 1000000, "date": f"2026-01-{i + 1:02d}"})
|
|
132
|
+
|
|
133
|
+
# Days 10-14: decline (5 down days from 109)
|
|
134
|
+
decline_prices = [107, 105, 103, 101, 100]
|
|
135
|
+
for i, p in enumerate(decline_prices):
|
|
136
|
+
bars.append({"close": p, "volume": 1100000, "date": f"2026-01-{11 + i:02d}"})
|
|
137
|
+
|
|
138
|
+
# Day 15: swing low
|
|
139
|
+
bars.append({"close": 99.5, "volume": 1200000, "date": "2026-01-16"})
|
|
140
|
+
|
|
141
|
+
# Day 16: rally day 1
|
|
142
|
+
bars.append({"close": 101, "volume": 1000000, "date": "2026-01-17"})
|
|
143
|
+
|
|
144
|
+
# Day 17-18: rally continues
|
|
145
|
+
bars.append({"close": 101.5, "volume": 1000000, "date": "2026-01-18"})
|
|
146
|
+
bars.append({"close": 102, "volume": 1000000, "date": "2026-01-19"})
|
|
147
|
+
|
|
148
|
+
# Day 19: FTD - +2% gain on higher volume
|
|
149
|
+
bars.append({"close": 104.04, "volume": 1500000, "date": "2026-01-20"})
|
|
150
|
+
|
|
151
|
+
# Reverse to most-recent-first
|
|
152
|
+
history = list(reversed(bars))
|
|
153
|
+
|
|
154
|
+
result = detect_follow_through_day(history, 55.0)
|
|
155
|
+
assert result["ftd_detected"] is True
|
|
156
|
+
assert result["rally_day_count"] >= 4
|
|
157
|
+
|
|
158
|
+
def test_rally_reset_below_swing_low(self):
|
|
159
|
+
"""Rally resets if price drops below swing low."""
|
|
160
|
+
bars = []
|
|
161
|
+
# Uptrend
|
|
162
|
+
for i in range(10):
|
|
163
|
+
bars.append({"close": 100 + i, "volume": 1000000, "date": f"d-{i}"})
|
|
164
|
+
# Decline
|
|
165
|
+
for i, p in enumerate([107, 105, 103, 101, 100]):
|
|
166
|
+
bars.append({"close": p, "volume": 1100000, "date": f"d-{10 + i}"})
|
|
167
|
+
# Swing low
|
|
168
|
+
bars.append({"close": 99, "volume": 1200000, "date": "d-15"})
|
|
169
|
+
# Brief rally
|
|
170
|
+
bars.append({"close": 100, "volume": 1000000, "date": "d-16"})
|
|
171
|
+
# Drop below swing low → reset
|
|
172
|
+
bars.append({"close": 98, "volume": 1000000, "date": "d-17"})
|
|
173
|
+
# New rally from lower base
|
|
174
|
+
bars.append({"close": 99, "volume": 1000000, "date": "d-18"})
|
|
175
|
+
bars.append({"close": 99.5, "volume": 1000000, "date": "d-19"})
|
|
176
|
+
|
|
177
|
+
history = list(reversed(bars))
|
|
178
|
+
result = detect_follow_through_day(history, 55.0)
|
|
179
|
+
# FTD should NOT be detected (rally reset, new rally too short)
|
|
180
|
+
assert result["ftd_detected"] is False
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""Tests for Sentiment Calculator"""
|
|
2
|
+
|
|
3
|
+
from calculators.sentiment_calculator import calculate_sentiment
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class TestSentimentMissingData:
|
|
7
|
+
"""Test missing data handling."""
|
|
8
|
+
|
|
9
|
+
def test_all_none_returns_50(self):
|
|
10
|
+
"""All inputs None → score=50, data_available=False."""
|
|
11
|
+
result = calculate_sentiment()
|
|
12
|
+
assert result["score"] == 50
|
|
13
|
+
assert result["data_available"] is False
|
|
14
|
+
assert "NO DATA" in result["signal"]
|
|
15
|
+
|
|
16
|
+
def test_partial_data_is_available(self):
|
|
17
|
+
"""At least one input → data_available=True."""
|
|
18
|
+
result = calculate_sentiment(vix_level=15.0)
|
|
19
|
+
assert result["data_available"] is True
|
|
20
|
+
|
|
21
|
+
def test_vix_only(self):
|
|
22
|
+
result = calculate_sentiment(vix_level=15.0)
|
|
23
|
+
assert result["score"] == 10 # VIX 15 → +10pt
|
|
24
|
+
|
|
25
|
+
def test_put_call_only(self):
|
|
26
|
+
result = calculate_sentiment(put_call_ratio=0.65)
|
|
27
|
+
assert result["score"] == 30 # PC < 0.70 → +30pt
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class TestSentimentScoring:
|
|
31
|
+
"""Boundary tests for sentiment scoring."""
|
|
32
|
+
|
|
33
|
+
def test_extreme_complacency(self):
|
|
34
|
+
"""Low VIX + low P/C + steep contango = max score."""
|
|
35
|
+
result = calculate_sentiment(
|
|
36
|
+
vix_level=11.0,
|
|
37
|
+
put_call_ratio=0.55,
|
|
38
|
+
vix_term_structure="steep_contango",
|
|
39
|
+
)
|
|
40
|
+
assert result["score"] == 100 # 30+40+30=100
|
|
41
|
+
|
|
42
|
+
def test_high_fear(self):
|
|
43
|
+
"""High VIX + high P/C + backwardation = 0 or negative."""
|
|
44
|
+
result = calculate_sentiment(
|
|
45
|
+
vix_level=30.0,
|
|
46
|
+
put_call_ratio=0.95,
|
|
47
|
+
vix_term_structure="backwardation",
|
|
48
|
+
)
|
|
49
|
+
assert result["score"] == 0 # -10+0+(-10) = -20 → clamped to 0
|
|
50
|
+
|
|
51
|
+
def test_moderate_sentiment(self):
|
|
52
|
+
result = calculate_sentiment(
|
|
53
|
+
vix_level=15.0,
|
|
54
|
+
put_call_ratio=0.75,
|
|
55
|
+
vix_term_structure="contango",
|
|
56
|
+
)
|
|
57
|
+
# VIX 15→10, PC 0.75→15, contango→15 = 40
|
|
58
|
+
assert result["score"] == 40
|
|
59
|
+
|
|
60
|
+
def test_margin_debt_not_scored(self):
|
|
61
|
+
"""Margin debt is context only, should not affect score."""
|
|
62
|
+
without = calculate_sentiment(vix_level=15.0)
|
|
63
|
+
with_margin = calculate_sentiment(vix_level=15.0, margin_debt_yoy_pct=40.0)
|
|
64
|
+
assert without["score"] == with_margin["score"]
|