
Breadth Chart Analyst
- 950 installs
- 2.5k repo stars
- Updated July 26, 2026
- tradermonty/claude-trading-skills
breadth-chart-analyst is a trading analysis skill that generates structured S&P 500 market-breadth reports from breadth charts for developers and quantitative traders making strategy and positioning decisions.
About
breadth-chart-analyst is a claude-trading-skills agent skill that produces structured market breadth analysis reports from S&P 500 chart inputs. Reports include executive summary, strategic and tactical outlook labels, overall recommendation tiers from Aggressive Long through Short, and dedicated sections for Chart 1 (S&P 500 Breadth Index on 200-day MA) with metric tables for current readings and interpretation. Traders and quant developers reach for breadth-chart-analyst when they upload breadth charts and need a consistent narrative template instead of free-form commentary. The output is a dated analysis document suitable for journaling, alerts, or downstream automation in trading workflows.
- Produces standardized Market Breadth Analysis Report with executive summary, strategic/tactical outlooks and overall rec
- Analyzes both 8-Day MA and 200-Day MA readings with distance from 73% peak and 23% trough thresholds
- Focuses on rightmost 3-5 data points for latest trend trajectory with week-by-week breakdown
- Delivers bullish/bearish/neutral ratings plus concrete trade recommendations (Aggressive Long through Short)
- Supports single-chart or dual-chart analysis modes
Breadth Chart Analyst by the numbers
- 950 all-time installs (skills.sh)
- +47 installs in the week ending Jul 28, 2026 (Skillselion tracking)
- Ranked #149 of 1,136 Finance & Trading skills by installs in the Skillselion catalog
- Security screen: MEDIUM risk (skills.sh audit)
- Data as of Jul 28, 2026 (Skillselion catalog sync)
npx skills add https://github.com/tradermonty/claude-trading-skills --skill breadth-chart-analystAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 950 |
|---|---|
| repo stars | ★ 2.5k |
| Security audit | 2 / 3 scanners passed |
| Last updated | July 26, 2026 |
| Repository | tradermonty/claude-trading-skills ↗ |
How do you analyze S&P 500 breadth charts systematically?
Generate structured market-breadth analysis reports from S&P 500 charts for trading strategy decisions.
Who is it for?
Quantitative traders reviewing S&P 500 200-day MA breadth charts who want repeatable structured reports.
Skip if: Long-term investors ignoring market breadth, or developers building unrelated application frontends.
When should I use this skill?
The user supplies S&P 500 breadth charts and wants a structured breadth analysis or trading outlook report.
What you get
Market breadth analysis report with executive summary, metric tables, strategic/tactical outlook, and trade recommendation.
- Market breadth analysis report
- Outlook and recommendation summary
Files
Breadth Chart Analyst
Overview
This skill enables specialized analysis of two complementary market breadth charts that provide strategic (medium to long-term) and tactical (short-term) market perspectives. Analyze breadth chart images to assess market health, identify trading signals based on backtested strategies, and develop positioning recommendations. All thinking and output are conducted exclusively in English.
When to Use
Use this skill when:
- User provides S&P 500 Breadth Index (200-Day MA based) chart images for analysis
- User provides US Stock Market Uptrend Stock Ratio chart images for analysis
- User requests market breadth assessment or market health evaluation
- User asks about medium-term strategic positioning based on breadth indicators
- User needs short-term tactical timing signals for swing trading
- User wants combined strategic and tactical market outlook
- User requests breadth analysis WITHOUT providing chart images (CSV data mode)
Do NOT use this skill when:
- User asks about individual stock analysis (use
us-stock-analysisskill instead) - User needs sector rotation analysis without breadth charts (use
sector-analystskill instead) - User wants news-based market analysis (use
market-news-analystskill instead)
Prerequisites
- Chart Images Optional: CSV data from public sources is the PRIMARY data source; chart images provide supplementary visual context
- No API Keys Required: CSV data is fetched from public GitHub Pages; no external API subscriptions needed
- Language: All analysis and output conducted in English
Output
This skill generates markdown analysis reports saved to the reports/ directory:
- Chart 1 only:
breadth_200ma_analysis_[YYYY-MM-DD].md - Chart 2 only:
uptrend_ratio_analysis_[YYYY-MM-DD].md - Both charts:
breadth_combined_analysis_[YYYY-MM-DD].md
Reports include executive summaries, current readings, signal identification, scenario analysis with probabilities, and actionable positioning recommendations for different trader types.
Core Principles
1. Dual-Timeframe Analysis: Combine strategic (Chart 1: 200MA Breadth) and tactical (Chart 2: Uptrend Ratio) perspectives 2. Backtested Strategy Focus: Apply proven systematic strategies based on historical patterns 3. Objective Signal Identification: Focus on clearly defined thresholds, transitions, and markers 4. English Communication: Conduct all analysis and generate all reports in English 5. Actionable Recommendations: Provide specific positioning guidance for different investor types
Chart Types and Purposes
Chart 1: S&P 500 Breadth Index (200-Day MA Based)
Purpose: Medium to long-term strategic market positioning
Key Elements:
- 8-Day MA (Orange Line): Short-term breadth trend, primary entry signal generator
- 200-Day MA (Green Line): Long-term breadth trend, primary exit signal generator
- Red Dashed Line (73%): Average peak level - market overheating threshold
- Blue Dashed Line (23%): Average 8MA trough level - extreme oversold, excellent buying opportunity
- Triangles:
- Purple ▼ = 8MA troughs (buy signal when reverses)
- Blue ▼ = 200MA troughs (major cycle lows)
- Red ▲ = 200MA peaks (sell signal)
- Pink Background: Downtrend periods
Backtested Strategy:
- BUY: When 8MA reverses from a trough (especially below 23%)
- SELL: When 200MA forms a peak (typically near/above 73%)
- Result: Historically high performance, avoids bear markets
Chart 2: US Stock Market - Uptrend Stock Ratio
Purpose: Short-term tactical timing and swing trading
Key Elements:
- Uptrend Stock Definition: Stocks above 200MA/50MA/20MA with positive 1-month performance
- Green Regions: Market in uptrend phase
- Red Regions: Market in downtrend phase
- ~10% Level (Lower Orange Dashed): Short-term bottom, extreme oversold
- ~40% Level (Upper Orange Dashed): Short-term top, market overheating
Swing Trading Strategy:
- ENTER LONG: When color changes from red to green (especially from <10-15% levels)
- EXIT LONG: When color changes from green to red (especially from >35-40% levels)
- Timeframe: Days to weeks
Analysis Workflow
Step 0: Fetch CSV Data (PRIMARY SOURCE - MANDATORY)
CRITICAL: CSV data is the PRIMARY source for all Breadth values. This step MUST be executed BEFORE any image analysis.
python3 skills/breadth-chart-analyst/scripts/fetch_breadth_csv.pyWhy CSV is PRIMARY:
- OpenCV image detection is fragile -- chart format changes cause catastrophic failures (Issue #7)
- CSV provides exact numerical values directly from the data source
- Image analysis is SUPPLEMENTARY only (for visual trend context)
Data Sources: 1. Market Breadth: tradermonty.github.io/market-breadth-analysis/market_breadth_data.csv
- Provides: 200-Day MA, 8-Day MA, Trend, Dead Cross status
2. Uptrend Ratio: tradermonty/uptrend-dashboard/data/uptrend_ratio_timeseries.csv
- Provides: Current ratio, 10MA, slope, trend (UP/DOWN), color (GREEN/RED)
3. Sector Summary: tradermonty/uptrend-dashboard/data/sector_summary.csv
- Provides: Per-sector ratio, trend, status (overbought/oversold)
Data Source Priority:
| Priority | Source | Use For | Reliability |
|---|---|---|---|
| 1 (PRIMARY) | CSV Data | All numerical values, dead cross status, color | HIGH |
| 2 (SUPPLEMENTARY) | Chart Image | Visual trend context, pattern confirmation | MEDIUM |
| 3 (DEPRECATED) | ~~OpenCV detect_breadth_values.py~~ | ~~Breadth detection~~ | UNRELIABLE |
| 4 (LAST RESORT) | ~~LLM visual reading~~ | ~~Emergency only~~ | LOW |
Expected Output:
============================================================
Breadth Data (CSV) - 2026-02-13
============================================================
--- Market Breadth (S&P 500) ---
200-Day MA: 62.26% (healthy (>=60%))
8-Day MA: 67.56% (healthy_bullish (60-73%))
8MA vs 200MA: +5.30pt (8MA ABOVE -- NO dead cross)
Trend: UPTREND
--- Uptrend Ratio (All Markets) ---
Current: 33.03% GREEN (neutral_bullish)
10MA: 32.65%, Slope: +0.0055, Trend: UP
--- Sector Summary ---
...
============================================================Validation: After running CSV fetch, verify:
- [ ] CSV data retrieved successfully
- [ ] 200-Day MA value recorded
- [ ] 8-Day MA value recorded
- [ ] Dead cross status determined (8MA < 200MA = dead cross)
- [ ] Uptrend Ratio value + color + trend recorded
- [ ] Use these CSV values as the authoritative source for all subsequent analysis
---
Step 1: Receive Chart Images and Prepare Analysis
When the user provides breadth chart images for analysis:
1. Confirm receipt of chart image(s) 2. Identify which chart(s) are provided:
- Chart 1 only (200MA Breadth)
- Chart 2 only (Uptrend Ratio)
- Both charts
3. Note any specific focus areas or questions from the user 4. CRITICAL: Extract right edge of chart images before analysis (Step 1.5)
If NO chart images are provided: Skip Steps 1, 1.5, and image-based analysis. Use CSV data from Step 0 as the sole data source and proceed directly to the analysis and report generation steps.
Language Note: All subsequent thinking, analysis, and output will be in English.
Step 1.5: Two-Stage Chart Analysis (MANDATORY when charts provided)
CRITICAL: Use a two-stage analysis approach to prevent misreading historical data as current values.
Stage 1: Full Chart Analysis (Historical Context)
First, analyze the FULL chart image to understand:
- Overall historical trend and cycles
- Key historical events (troughs, peaks, recoveries)
- Long-term patterns and context
Stage 2: Right Edge Focused Analysis (Current Values)
Then, extract and analyze the rightmost 25% of the chart to accurately determine CURRENT values.
Execute the Python script to extract the right edge:
python3 skills/breadth-chart-analyst/scripts/extract_chart_right_edge.py <image_path> --percent 25Why Two-Stage Analysis is Mandatory
| Stage | Purpose | What to Extract |
|---|---|---|
| Stage 1 (Full) | Historical context, trend cycles | Overall patterns, past troughs/peaks |
| Stage 2 (Right Edge) | Current values (CRITICAL) | 8MA value, 200MA value, current color, current slope |
Common Error This Prevents:
- LLM reads 2025 mid-year dip (8MA ~25-30%) instead of current value (8MA ~60-65%)
- By isolating the right edge, the "current" data is unambiguous
Analysis Protocol
1. Read full chart → Document historical context 2. Run extraction script → Generate right edge image 3. Read right edge image → Document current values with HIGH CONFIDENCE 4. Cross-check: If Stage 1 and Stage 2 values differ significantly, Stage 2 (right edge) takes precedence 5. Report both: Include Stage 1 context AND Stage 2 current values in analysis
Step 2: Load Breadth Chart Methodology
Before beginning analysis, read the comprehensive breadth chart methodology:
Read: references/breadth_chart_methodology.mdThis reference contains detailed guidance on:
- Chart 1: 200MA-based breadth index interpretation and strategy
- Chart 2: Uptrend stock ratio interpretation and strategy
- Signal identification and threshold significance
- Strategy rules and risk management
- Combining both charts for optimal decision-making
- Common pitfalls to avoid
Step 3: Examine Sample Charts (First Time or for Reference)
To understand the chart format and visual elements, review the sample charts included in this skill:
View: assets/SP500_Breadth_Index_200MA_8MA.jpeg
View: assets/US_Stock_Market_Uptrend_Ratio.jpegThese samples demonstrate:
- Visual appearance and structure of each chart type
- How signals and thresholds are displayed
- Color coding and marker systems
- Historical patterns and cycles
Step 4: Analyze Chart 1 (200MA-Based Breadth Index)
If Chart 1 is provided, conduct systematic analysis:
4.1 Extract Current Readings
From the chart image, identify:
- Current 8MA level (orange line): Specific percentage
- Current 200MA level (green line): Specific percentage
- 8MA slope: Rising, falling, or flat
- 200MA slope: Rising, falling, or flat
- Distance from 73% threshold: How close to overheating
- Distance from 23% threshold: How close to extreme oversold
- Most recent date visible on the chart
4.1.5 CRITICAL: Latest Data Point Detailed Trend Analysis
This step is MANDATORY to avoid misreading recent trend changes.
CRITICAL WARNING: Charts can be deceptive. The MAJORITY of analysis errors occur because the analyst: 1. Confuses the 8MA (orange) with the 200MA (green) 2. Reads historical trends instead of the CURRENT rightmost data points 3. Misidentifies which line is rising vs falling
BEFORE analyzing trend direction, FIRST confirm line colors:
- ✓ 8MA = ORANGE line (fast-moving, more volatile)
- ✓ 200MA = GREEN line (slow-moving, smoother)
- If unsure which is which, STOP and re-examine the chart legend/colors
Focus intensively on the rightmost 3-5 data points of the chart (most recent weeks):
For 8MA (Orange Line) - Analyze the very latest trajectory: 1. Identify the absolute latest position: Where is the 8MA at the rightmost edge of the chart? 2. Trace back 3-5 data points (approximately 3-5 weeks):
- What was the 8MA level 1 week ago?
- What was the 8MA level 2 weeks ago?
- What was the 8MA level 3 weeks ago?
3. Calculate the directional change:
- Is the latest value HIGHER or LOWER than 1 week ago?
- Is the latest value HIGHER or LOWER than 2 weeks ago?
- What is the trend: consistently rising, consistently falling, or mixed?
4. Determine the CURRENT slope (not historical slope):
- Rising: Latest data point is higher than previous 2-3 points AND shows upward curvature
- Falling: Latest data point is lower than previous 2-3 points AND shows downward curvature
- Flat: Latest data point is approximately equal to previous points (within 2-3%)
Critical Questions to Answer:
- [ ] Is the 8MA currently moving UP or DOWN at the rightmost edge?
- [ ] If there was a recent trough, has the 8MA sustained the upward move, or has it rolled over?
- [ ] Count consecutive periods of increase: How many consecutive periods has 8MA risen? (Need 2-3 for confirmation)
- [ ] Count consecutive periods of decrease: How many consecutive periods has 8MA fallen? (Indicates failed reversal if declining after trough)
For 200MA (Green Line) - Analyze the very latest trajectory: 1. Identify the absolute latest position: Where is the 200MA at the rightmost edge? 2. Trace back 4-6 weeks:
- What was the 200MA level 2 weeks ago?
- What was the 200MA level 4 weeks ago?
3. Determine the CURRENT slope:
- Is it rising, falling, or flat in the most recent period?
Failed Reversal Detection (CRITICAL): If an 8MA trough (purple ▼) was recently identified:
- [ ] Did the 8MA rise for only 1-2 periods and then turn back down?
- [ ] Did the 8MA fail to reach 60% before turning down?
- [ ] Is the 8MA currently declining again after the bounce?
- If YES to any: This is a FAILED REVERSAL - DO NOT ENTER, signal is INVALID
Example Analysis Format:
Latest 8MA Data Points (rightmost to left):
- Current (Week 0): 48%
- 1 week ago: 52%
- 2 weeks ago: 55%
- 3 weeks ago: 50%
Analysis: 8MA is FALLING. It rose from 50% to 55% (weeks 3-2), but has since declined to 48%.
This shows a failed reversal pattern - bounce was temporary, downtrend has resumed.
SLOPE: Falling (not rising!)MANDATORY CROSS-CHECK (to catch misreadings): After determining the trend, ask yourself:
- [ ] "If I claimed 8MA is RISING, but it's actually been FALLING for weeks, what would that look like?"
- Answer: The rightmost data points would be LOWER than previous points (confirming FALLING)
- [ ] "Does my analysis match the visual slope of the orange line at the rightmost edge?"
- If orange line visually slopes DOWN at the right edge → It's FALLING
- If orange line visually slopes UP at the right edge → It's RISING
- [ ] "Is there any pink background shading (downtrend) near the rightmost edge?"
- If YES → This confirms downtrend conditions, 8MA is likely FALLING
- [ ] "Are the 8MA and 200MA converging (getting closer) or diverging?"
- Converging from below → Potential death cross forming → BEARISH
- Converging from above → Potential golden cross forming → BULLISH
4.2 Identify Signal Markers
Look for and document:
- Most recent 8MA trough (purple ▼): Date and level
- Most recent 200MA trough (blue ▼): Date and level (if visible in timeframe)
- Most recent 200MA peak (red ▲): Date and level
- Days/weeks since most recent signals
- Any pink background shading (downtrend periods)
4.3 Assess Market Regime
Based on readings and patterns, classify the current market as:
- Healthy Bull Market
- Overheated Bull Market
- Market Top/Distribution Phase
- Bear Market/Correction
- Capitulation/Extreme Oversold
- Early Recovery
Support the classification with specific evidence from the chart.
4.4 Determine Strategy Position
Apply the backtested strategy rules with STRICT confirmation requirements:
Check for BUY signal (ALL criteria must be met): 1. ✓ Trough Formation: Has 8MA formed a clear trough (purple ▼)? 2. ✓ Reversal Initiated: Has 8MA begun to move upward from the trough? 3. ✓ Confirmation Achieved: Has 8MA risen for 2-3 CONSECUTIVE periods after the trough? 4. ✓ No Recent Reversal: Based on Step 4.1.5 analysis, is 8MA CURRENTLY rising (not falling)? 5. ✓ Sustained Move: Has 8MA maintained the upward trajectory without rolling over? 6. ⭐ Optional but Strong: Is 8MA below or near 23% (extreme oversold) at trough?
BUY Signal Status:
- CONFIRMED: All 5 required criteria met → ENTER LONG
- DEVELOPING: Trough formed, but < 2-3 consecutive increases → WAIT, MONITOR
- FAILED: Trough formed, but 8MA has rolled over and is declining → DO NOT ENTER, WAIT FOR NEXT TROUGH
- NO SIGNAL: No trough formed → WAIT
Check for SELL signal:
- Has 200MA formed a peak (red ▲)?
- Is 200MA near or above 73%?
- Is this an active sell signal requiring position exit?
Current position determination:
- Long: BUY signal confirmed, position entered and held
- Preparing to Enter: BUY signal developing (trough formed, watching for confirmation)
- WAIT / Flat: No valid signal OR failed reversal detected
- Preparing to Exit: SELL signal developing (200MA approaching peak)
4.5 Develop Scenarios
Create 2-3 scenarios with probability estimates:
- Base case scenario (highest probability)
- Alternative scenario(s)
- Each scenario includes: description, supporting factors, strategy implications, key levels
Step 5: Analyze Chart 2 (Uptrend Stock Ratio)
If Chart 2 is provided, conduct systematic analysis:
5.0 ~~MANDATORY~~ DEPRECATED: Uptrend Ratio Detection Script (Superseded by Step 0 CSV Fetch)
NOTE (Issue #7): This OpenCV detection step is DEPRECATED. Use CSV data from Step 0 as the PRIMARY source. The OpenCV script may be run for supplementary validation only, but CSV values take precedence in all cases.
python3 skills/breadth-chart-analyst/scripts/detect_uptrend_ratio.py <image_path> [--debug]5.1 Extract Current Readings
From the chart image, identify:
- Current uptrend stock ratio: Specific percentage
- Current color: Green (uptrend) or Red (downtrend)
- Ratio slope: Rising, falling, or flat
- Distance from 10% threshold: How close to extreme oversold
- Distance from 40% threshold: How close to overbought
- Most recent date visible on the chart
5.2 Identify Trend Transitions
Look for and document:
- Most recent red-to-green transition: Date and ratio level at transition
- Most recent green-to-red transition: Date and ratio level at transition
- Duration of current color phase: How long in current trend
- Days/weeks since most recent transition
5.3 Assess Market Condition
Based on current ratio and color, classify as:
- Extreme Oversold (<10%)
- Moderate Bearish (10-20%, red)
- Neutral/Transitional (20-30%)
- Moderate Bullish (30-37%, green)
- Extreme Overbought (>37-40%)
Support the classification with specific evidence from the chart.
5.4 Determine Trading Position
Apply the swing trading strategy rules:
Check for ENTER LONG signal:
- Has color changed from red to green?
- Was the transition from an oversold level (<15%)?
- Is the transition confirmed (2-3 days of green)?
Check for EXIT LONG signal:
- Has color changed from green to red?
- Was the transition from an overbought level (>35%)?
- Is momentum weakening?
Current position: Long, Flat, Preparing to Enter, or Preparing to Exit
5.5 Develop Scenarios
Create 2-3 scenarios with probability estimates:
- Base case scenario (highest probability)
- Alternative scenario(s)
- Each scenario includes: description, supporting factors, trading implications, key levels
Step 6: Combined Analysis (When Both Charts Provided)
If both charts are provided, integrate the strategic and tactical perspectives:
6.1 Alignment Assessment
Create a positioning matrix:
- Chart 1 (Strategic): Bullish / Bearish / Neutral + signal status
- Chart 2 (Tactical): Bullish / Bearish / Neutral + signal status
- Combined Implication: How do they align or conflict?
6.2 Scenario Classification
Determine which of the four scenarios applies:
Scenario 1: Both Bullish
- Chart 1: 8MA rising, 200MA not yet peaked
- Chart 2: Green (uptrend), ratio rising from oversold
- Implication: Maximum bullish stance, aggressive positioning
Scenario 2: Strategic Bullish, Tactical Bearish
- Chart 1: 8MA rising, 200MA not yet peaked
- Chart 2: Red (downtrend), ratio falling or elevated
- Implication: Hold core long positions, wait for tactical entry
Scenario 3: Strategic Bearish, Tactical Bullish
- Chart 1: 200MA peaked or declining
- Chart 2: Green (uptrend), ratio rising
- Implication: Short-term tactical trades only, tight stops
Scenario 4: Both Bearish
- Chart 1: Both MAs declining
- Chart 2: Red (downtrend), ratio falling
- Implication: Defensive positioning, cash or shorts
6.3 Unified Recommendation
Provide integrated positioning guidance for:
- Long-term investors (based primarily on Chart 1)
- Swing traders (based primarily on Chart 2)
- Active tactical traders (based on combination)
Address any conflicts between charts and explain resolution.
Step 7: Generate Analysis Report in English
Create a comprehensive markdown report using the template structure:
Read and use as template: assets/breadth_analysis_template.mdIMPORTANT: All analysis and output must be in English.
The report structure varies based on which chart(s) are analyzed:
If Chart 1 only:
- Executive Summary
- Chart 1 full analysis sections
- Summary and Conclusion
- Omit Chart 2 and Combined Analysis sections
If Chart 2 only:
- Executive Summary
- Chart 2 full analysis sections
- Summary and Conclusion
- Omit Chart 1 and Combined Analysis sections
If Both Charts:
- Executive Summary
- Chart 1 full analysis sections
- Chart 2 full analysis sections
- Combined Analysis section (mandatory)
- Summary and Conclusion
File Naming Convention: Save each analysis as:
- Chart 1 only:
breadth_200ma_analysis_[YYYY-MM-DD].md - Chart 2 only:
uptrend_ratio_analysis_[YYYY-MM-DD].md - Both charts:
breadth_combined_analysis_[YYYY-MM-DD].md
Step 8: Quality Assurance
Before finalizing the report, verify:
1. ✓ Language: All content is in English (thinking and output) 2. ✓ Line Color Verification: Explicitly confirmed 8MA = ORANGE, 200MA = GREEN before trend analysis 3. ✓ Latest Data Trend Analysis: Step 4.1.5 was thoroughly completed - the most recent 3-5 data points were analyzed to determine CURRENT trend direction 4. ✓ Trend Direction Accuracy: The stated 8MA slope (Rising/Falling/Flat) accurately reflects the RIGHTMOST data points, not historical movement 5. ✓ Cross-Check Completed: MANDATORY CROSS-CHECK questions were answered to confirm trend direction matches visual slope 6. ✓ Death/Golden Cross Check: If 8MA and 200MA are converging, explicitly identified whether death cross or golden cross is forming 7. ✓ Failed Reversal Check: If a trough was identified, explicitly verified whether the reversal sustained or failed by analyzing latest trajectory 8. ✓ Specific Values: All readings include specific percentages/levels, not vague descriptions 9. ✓ Signal Status: Clear identification of active signals (CONFIRMED BUY / DEVELOPING / FAILED / SELL / WAIT) 10. ✓ Strategy Alignment: Recommendations align with backtested strategies and confirmation requirements 11. ✓ Probabilities: Scenario probabilities sum to 100% 12. ✓ Actionable: Clear positioning recommendations for different trader types 13. ✓ Context: Historical comparison and reference to similar past situations 14. ✓ Risk Management: Invalidation levels and risk factors clearly stated
FINAL SANITY CHECK:
- [ ] If report claims "BUY signal" or "8MA rising", confirm this doesn't contradict user's chart showing death cross or downtrend
- [ ] If report claims "bullish", confirm there's no pink background shading or death cross visible
- [ ] If unsure about ANY trend direction, explicitly state uncertainty rather than guessing
Quality Standards
Objectivity Requirements
- Base all analysis strictly on observable chart data
- Do not incorporate external information (news, fundamentals) unless specifically relevant
- Use precise technical terminology consistently
- Distinguish clearly between factual observations and probabilistic forecasts
- Acknowledge uncertainty when signals are ambiguous
Completeness Requirements
- Address all relevant sections of the analysis template
- Provide specific numerical values for all key metrics
- Justify probability estimates with technical factors
- Include invalidation levels for each scenario
- Compare current readings to historical patterns visible on the charts
Clarity Requirements
- Write in professional, analytical English
- Use clear section headings and structure
- Present information in tables where appropriate
- Make recommendations specific and actionable
- Avoid jargon without explanation
Strategy Adherence Requirements
- Apply backtested strategy rules correctly
- Distinguish between strategic (Chart 1) and tactical (Chart 2) signals
- Provide clear position status (Long/Flat/Entering/Exiting)
- Include specific entry/exit levels when applicable
- Address risk management (stop losses, position sizing)
Common Analysis Errors and How to Avoid Them
Error 1: Confusing 8MA with 200MA
Symptom: Report claims 8MA is rising when it's actually the 200MA that's rising
Prevention:
- ALWAYS verify: 8MA = ORANGE, 200MA = GREEN
- Check line volatility: 8MA moves faster with more volatility
- If unsure, state: "Based on line color, I identify the [orange/green] line as [8MA/200MA]"
Error 2: Reading Historical Trends Instead of Current Direction
Symptom: Report describes what happened 1-2 months ago, not what's happening NOW
Prevention:
- Focus ONLY on rightmost 3-5 data points
- Explicitly write: "At the CURRENT rightmost edge, 8MA is at X% and is [rising/falling]"
- Ignore what happened in September if we're analyzing November
Error 3: Missing Death Cross or Golden Cross Formation
Symptom: Report is bullish when 8MA and 200MA are about to death cross (bearish)
Prevention:
- ALWAYS check: "Are 8MA and 200MA getting closer (converging) or further apart (diverging)?"
- If converging with 8MA above 200MA → Potential death cross → BEARISH
- If converging with 8MA below 200MA → Potential golden cross → BULLISH
- Explicitly state: "8MA is currently [above/below] 200MA, and they are [converging/diverging]"
Error 4: Ignoring Pink Background Shading
Symptom: Report claims bullish setup when chart shows pink downtrend background
Prevention:
- Check for pink shading at the rightmost edge
- Pink background = Downtrend period = Bearish conditions
- If pink shading present, report MUST acknowledge bearish conditions
Error 5: Claiming Reversal Too Early
Symptom: Report says "BUY signal confirmed" after only 1 week of increase
Prevention:
- Require 2-3 CONSECUTIVE weeks of 8MA increase for confirmation
- If only 1 week: Signal is "DEVELOPING", not "CONFIRMED"
- If 8MA rises then falls again: Signal is "FAILED", not valid
Example Usage Scenarios
Example 1: CSV-Only Analysis (No Charts)
User: "breadth分析して" (Analyze breadth)
[No chart images provided]
Breadth Analyst:
1. Executes fetch_breadth_csv.py to get latest CSV data
2. Reads breadth_chart_methodology.md
3. Analyzes CSV data:
- 200MA: 62.26% (healthy)
- 8MA: 67.56% (healthy_bullish)
- 8MA vs 200MA: +5.30pt (NO dead cross)
- Uptrend Ratio: 33.03% GREEN (neutral_bullish)
- Sector summary with overbought/oversold sectors
4. Assesses market regime based on CSV values
5. Generates report: breadth_combined_analysis_2026-03-16.mdExample 2: Strategic Breadth Analysis (Chart 1 Only)
User: "Please analyze this S&P 500 breadth chart and tell me where we are in the market cycle."
[Provides Chart 1 image: 200MA Breadth Index]
Breadth Analyst (thinking in English):
1. Executes CSV fetch for authoritative numerical values
2. Confirms receipt of Chart 1 (200MA-based breadth index)
3. Reads breadth_chart_methodology.md for Chart 1 guidance
4. Two-stage analysis: full chart → right edge extraction
5. Cross-checks CSV values with chart readings
6. Generates comprehensive report in EnglishExample 3: Combined Strategic + Tactical Analysis (Both Charts)
User: "Analyze both of these breadth charts and give me your overall market view."
[Provides both Chart 1 and Chart 2 images]
Breadth Analyst (thinking in English):
1. Executes CSV fetch as PRIMARY source
2. Confirms receipt of both charts
3. Reads full breadth_chart_methodology.md
4. Two-stage analysis for each chart
5. Cross-checks all values against CSV data
6. Combined assessment and unified recommendation
7. Generates comprehensive combined reportResources
This skill includes the following bundled resources:
references/breadth_chart_methodology.md
Comprehensive methodology covering:
- Chart 1 (200MA Breadth): Components, interpretation, market regimes, backtested strategy, analysis checklist
- Chart 2 (Uptrend Ratio): Components, interpretation, market conditions, swing trading strategy, analysis checklist
- Combined Analysis: Alignment scenarios, integrated decision-making
- Common Pitfalls: Mistakes to avoid for each chart type
Usage: Read this file before conducting any breadth chart analysis to ensure systematic, accurate interpretation.
assets/breadth_analysis_template.md
Structured template for breadth analysis reports in English.
Usage: Use this template structure for every analysis report.
assets/SP500_Breadth_Index_200MA_8MA.jpeg
Sample Chart 1 image for format reference.
assets/US_Stock_Market_Uptrend_Ratio.jpeg
Sample Chart 2 image for format reference.
scripts/fetch_breadth_csv.py
PRIMARY data source. Fetches market breadth, uptrend ratio, and sector summary data from public CSV sources. Uses only stdlib (urllib + csv) -- no external dependencies.
python3 skills/breadth-chart-analyst/scripts/fetch_breadth_csv.py # Human-readable
python3 skills/breadth-chart-analyst/scripts/fetch_breadth_csv.py --json # JSON outputscripts/extract_chart_right_edge.py
Extracts the rightmost portion of chart images to help focus on latest data points. Requires PIL/Pillow.
python3 skills/breadth-chart-analyst/scripts/extract_chart_right_edge.py <image_path> --percent 25scripts/detect_uptrend_ratio.py (DEPRECATED)
OpenCV-based uptrend ratio detection. Superseded by CSV fetch. Requires opencv-python + numpy.
scripts/detect_breadth_values.py (DEPRECATED)
OpenCV-based breadth value detection. Superseded by CSV fetch. Requires opencv-python + numpy.
Special Notes
Language Requirement
CRITICAL: All analysis, thinking, and output MUST be in English. This includes:
- Internal analysis and reasoning
- Report generation
- Tables and data presentation
- Scenario descriptions
- Recommendations
Do not translate or use any other language. The user expects English output exclusively.
Strategy Focus
This skill emphasizes backtested, systematic strategies rather than discretionary interpretation. Always:
- Apply the strategy rules as documented
- Reference historical performance patterns
- Provide specific entry/exit criteria
- Include risk management guidance
Practical Application
The goal is actionable intelligence. Every analysis should answer:
- Strategic: Should I be long, flat, or short the market over the medium term?
- Tactical: Should I enter or exit positions in the short term?
- Timing: When should I act, and at what levels?
- Risk: What would invalidate this view, and where should stops be?
Market Breadth Analysis Report
Analysis Date: [YYYY-MM-DD] Analyst: Claude Breadth Chart Specialist Charts Analyzed: [Chart 1 only / Chart 2 only / Both Charts]
---
Executive Summary
[Brief 2-4 sentence overview of the key findings from the breadth analysis]
Strategic Outlook: [Bullish / Bearish / Neutral] Tactical Outlook: [Bullish / Bearish / Neutral] Overall Recommendation: [Aggressive Long / Moderate Long / Neutral / Defensive / Short]
---
Chart 1: S&P 500 Breadth Index (200-Day MA Based)
Current Readings
Date of Latest Data: [Latest date visible on chart]
Key Metrics
| Metric | Current Value | Interpretation |
|---|---|---|
| 8-Day MA (Orange) | [X.XX / XX%] | [Above/Below/At threshold] |
| 200-Day MA (Green) | [X.XX / XX%] | [Above/Below/At threshold] |
| Distance from 73% Peak Threshold | [+/- XX%] | [Overheated / Normal / Oversold] |
| Distance from 23% Trough Threshold | [+/- XX%] | [Extreme Oversold / Normal / Elevated] |
Trend Direction
- 8MA Slope: [Rising / Falling / Flat] - [Brief description of trajectory]
- 200MA Slope: [Rising / Falling / Flat] - [Brief description of trajectory]
- Trend Alignment: [Both Rising / Both Falling / Diverging / Mixed]
Latest Data Point Trend Analysis (CRITICAL)
Focus: Rightmost 3-5 Data Points
8MA Recent Trajectory (Week-by-Week from trough/recent period to present):
- Current Level (Week 0): [XX%]
- 1 Week Ago (Week -1): [XX%] - Change: [+/-XX%]
- 2 Weeks Ago (Week -2): [XX%] - Change: [+/-XX%]
- 3 Weeks Ago (Week -3): [XX%] - Change: [+/-XX%]
Consecutive Period Analysis:
- Consecutive Increases: [X periods]
- Consecutive Decreases: [X periods]
- Current Momentum: [Accelerating Up / Steady Up / Decelerating / Turning Down / Falling]
CURRENT Slope Determination (based on latest edge):
- [ ] Rising: Latest value higher than previous 2-3 points with upward curvature
- [ ] Falling: Latest value lower than previous 2-3 points with downward curvature
- [ ] Flat: Latest value approximately equal to previous points (±2-3%)
Failed Reversal Check (if trough recently formed):
- [ ] Has 8MA rolled over after initial bounce?
- [ ] Has 8MA declined for 1-2+ consecutive periods after rising?
- [ ] Has 8MA failed to reach 55-60% before turning down?
Conclusion: The 8MA is [CURRENTLY RISING / CURRENTLY FALLING / FLAT] based on the rightmost data points.
Signal Status
Recent Signal Markers
8MA Troughs (Purple ▼):
- Most Recent: [Date] at [XX%]
- Status: [X days/weeks since trough]
- Reversal Confirmed: [Yes / No / Developing]
200MA Troughs (Blue ▼):
- Most Recent: [Date] at [XX%]
- Significance: [Major cycle low / Historical context]
200MA Peaks (Red ▲):
- Most Recent: [Date] at [XX%]
- Status: [X days/weeks since peak]
- Implications: [Exit signal active / Historical peak]
Active Signals
Current Strategy Signal: [CONFIRMED BUY / DEVELOPING BUY / FAILED REVERSAL / SELL / HOLD / WAIT]
Signal Classification:
- CONFIRMED BUY: Trough formed + 2-3 consecutive increases + CURRENTLY rising → ENTER LONG
- DEVELOPING BUY: Trough formed + < 2 consecutive increases → WAIT, MONITOR
- FAILED REVERSAL: Trough formed + initially rose + now declining → DO NOT ENTER
- SELL: 200MA peak formed → EXIT LONG
- HOLD: Already long, no exit signal → MAINTAIN POSITION
- WAIT: No trough, or waiting for new trough after failed reversal → STAY FLAT
[Detailed explanation of why this signal status is assigned, including specific reference to Latest Data Point Trend Analysis findings]
Market Regime Assessment
Current Regime: [Healthy Bull Market / Overheated Bull Market / Market Top/Distribution / Bear Market/Correction / Capitulation/Extreme Oversold / Early Recovery]
Regime Characteristics
[Detailed description of the current market regime, including:]
- Key observations supporting this classification
- How current readings align with historical patterns
- Typical duration and behavior in this regime
Supporting Evidence
1. [First piece of evidence] 2. [Second piece of evidence] 3. [Third piece of evidence]
Historical Context
Comparison to Previous Cycles
[Analysis comparing current readings to previous similar situations shown on the chart]
Similar Historical Periods:
- [Date Range 1]: [Brief description and outcome]
- [Date Range 2]: [Brief description and outcome]
- [Date Range 3]: [Brief description and outcome]
Distance from Extremes
- Time Since Last 8MA Trough: [X weeks/months]
- Time Since Last 200MA Peak: [X weeks/months]
- Current Reading vs Historical Average: [Above/Below by XX%]
Strategy Implications
Backtested Strategy Positioning
Current Position: [Long / Flat / Preparing to Enter / Preparing to Exit]
Entry Analysis
Entry Signal Status: [Active / Not Active / Pending]
[If active or pending:]
- Entry Trigger: [Description of what triggered or would trigger entry]
- Confirmation Criteria: [What needs to happen for confirmation]
- Optimal Entry Zone: [Price/breadth level range]
- Entry Timing: [Immediate / Wait for X / Already entered]
[If not active:]
- What Would Trigger Entry: [Specific conditions needed]
- Estimated Distance to Entry: [How far from entry conditions]
Exit Analysis
Exit Signal Status: [Active / Not Active / Monitoring]
[If active or monitoring:]
- Exit Trigger: [Description of what triggered or would trigger exit]
- Key Level to Watch: [Specific breadth level or 200MA behavior]
- Exit Timing: [Immediate / Prepare to exit / Not yet]
[If not active:]
- What Would Trigger Exit: [Specific conditions needed]
- Estimated Distance to Exit: [How far from exit conditions]
Risk Management
Current Risk Level: [Low / Moderate / High / Extreme]
Risk Factors:
- [Risk factor 1]
- [Risk factor 2]
- [Risk factor 3]
Invalidation Levels:
- Bullish Case Invalidation: [Specific level or condition]
- Bearish Case Invalidation: [Specific level or condition]
Key Levels to Monitor
Critical Thresholds
1. [Level 1]: [Description and significance] 2. [Level 2]: [Description and significance] 3. [Level 3]: [Description and significance]
Upcoming Milestones
- Near-term (Next 1-2 weeks): [What to watch for]
- Medium-term (Next 1-2 months): [What to watch for]
- Long-term (Next 3-6 months): [What to watch for]
Scenario Analysis
Base Case Scenario: [Title] - [XX]% Probability
Description: [What this scenario entails]
Supporting Factors:
- [Factor 1]
- [Factor 2]
- [Factor 3]
Implications:
- Strategy Position: [Long/Flat/Short]
- Expected Duration: [Timeframe]
- Key Levels: [Targets or invalidation levels]
---
Alternative Scenario: [Title] - [XX]% Probability
Description: [What this scenario entails]
Supporting Factors:
- [Factor 1]
- [Factor 2]
Implications:
- Strategy Position: [Long/Flat/Short]
- Expected Duration: [Timeframe]
- Key Levels: [Targets or invalidation levels]
---
Chart 2: US Stock Market - Uptrend Stock Ratio
Current Readings
Date of Latest Data: [Latest date visible on chart]
Key Metrics
| Metric | Current Value | Interpretation |
|---|---|---|
| Uptrend Stock Ratio | [XX.X%] | [Oversold / Normal / Overbought] |
| Current Trend Color | [Green / Red] | [Uptrend / Downtrend] |
| Distance from 10% Bottom | [+/- XX%] | [Extreme Oversold / Normal / Elevated] |
| Distance from 40% Top | [+/- XX%] | [Overbought / Normal / Oversold] |
Trend Direction
- Ratio Slope: [Rising / Falling / Flat] - [Brief description of trajectory]
- Trend Duration: [X days/weeks] in current [Green/Red] phase
- Momentum: [Accelerating / Decelerating / Steady]
Signal Status
Recent Trend Transitions
Most Recent Red-to-Green Transition (Downtrend to Uptrend):
- Date: [Date]
- Ratio at Transition: [XX%]
- Days Since Transition: [X days]
- Performance Since: [Result if applicable]
Most Recent Green-to-Red Transition (Uptrend to Downtrend):
- Date: [Date]
- Ratio at Transition: [XX%]
- Days Since Transition: [X days]
- Performance Since: [Result if applicable]
Active Signals
Current Trading Signal: [ENTER LONG / EXIT LONG / HOLD LONG / STAY FLAT / PREPARE TO ENTER]
[Detailed explanation of why this signal is active]
Market Condition Assessment
Current Condition: [Extreme Oversold / Moderate Bearish / Neutral/Transitional / Moderate Bullish / Extreme Overbought]
Condition Characteristics
[Detailed description of the current market condition, including:]
- Participation breadth analysis
- Short-term momentum assessment
- Risk/reward considerations at current levels
Supporting Evidence
1. [First piece of evidence] 2. [Second piece of evidence] 3. [Third piece of evidence]
Historical Context
Comparison to Recent Extremes
Recent Oversold Extremes (<15%):
- [Date]: [Ratio %] - [Outcome]
- [Date]: [Ratio %] - [Outcome]
Recent Overbought Extremes (>35%):
- [Date]: [Ratio %] - [Outcome]
- [Date]: [Ratio %] - [Outcome]
Typical Behavior at Current Level
[Analysis of how the market typically behaves when the ratio is at the current level]
Trading Strategy Implications
Swing Trading Strategy Positioning
Current Position: [Long / Flat / Preparing to Enter / Preparing to Exit]
Entry Analysis
Entry Signal Status: [Active / Not Active / Pending Confirmation]
[If active or pending:]
- Entry Trigger: [Red-to-green transition / Oversold reversal / etc.]
- Confirmation Status: [Confirmed / Needs X more days / Partial confirmation]
- Optimal Entry Level: [Current / Wait for ratio to reach X%]
- Entry Timing: [Immediate / Next X days / Already entered]
- Risk/Reward: [Assessment of risk/reward at current or pending entry]
[If not active:]
- What Would Trigger Entry: [Specific color change or level needed]
- Current Phase: [Waiting for oversold / Mid-trend / Overbought]
Exit Analysis
Exit Signal Status: [Active / Not Active / Monitoring / Tighten Stops]
[If active or monitoring:]
- Exit Trigger: [Green-to-red transition / Overbought level / etc.]
- Exit Timing: [Immediate / When ratio reaches X% / When color changes]
- Profit Status: [Up X% from entry / Down / Flat]
[If not active:]
- What Would Trigger Exit: [Specific color change or level needed]
- Current Phase: [Early in trend / Mid-trend / Late in trend]
Position Sizing Recommendations
Suggested Position Size: [Full / 75% / 50% / 25% / Minimal / Zero]
Rationale: [Explanation based on current ratio level, trend status, and risk/reward]
Risk Management
Current Risk Level: [Low / Moderate / High / Extreme]
Risk Factors:
- [Risk factor 1]
- [Risk factor 2]
- [Risk factor 3]
Stop Loss Level: [Specific ratio level or condition]
Profit Target: [Specific ratio level or condition]
Key Levels to Monitor
Critical Thresholds
1. 10% Level (Oversold Bottom): [Distance from current, significance] 2. 40% Level (Overbought Top): [Distance from current, significance] 3. [Other Key Level]: [Description and significance]
Timing Considerations
- Short-term (Next few days): [What to watch for]
- Swing Trade Horizon (Next 1-3 weeks): [Expected development]
Scenario Analysis
Scenario 1: [Title] - [XX]% Probability
Description: [What this scenario entails]
Supporting Factors:
- [Factor 1]
- [Factor 2]
Implications:
- Trading Action: [Enter/Hold/Exit]
- Expected Ratio Movement: [Direction and potential range]
- Timeframe: [Days to weeks]
---
Scenario 2: [Title] - [XX]% Probability
Description: [What this scenario entails]
Supporting Factors:
- [Factor 1]
- [Factor 2]
Implications:
- Trading Action: [Enter/Hold/Exit]
- Expected Ratio Movement: [Direction and potential range]
- Timeframe: [Days to weeks]
---
Combined Analysis: Strategic & Tactical Integration
[This section is included when both charts are analyzed together]
Alignment Assessment
Current Positioning Matrix
| Timeframe | Chart | Status | Signal | Implication |
|---|---|---|---|---|
| Strategic (Medium/Long) | 200MA Breadth | [Bullish/Bearish/Neutral] | [Buy/Sell/Hold/Wait] | [Brief implication] |
| Tactical (Short-term) | Uptrend Ratio | [Bullish/Bearish/Neutral] | [Enter/Exit/Hold/Wait] | [Brief implication] |
Scenario Classification
Current Scenario: [Scenario 1 / Scenario 2 / Scenario 3 / Scenario 4]
Scenario 1: Both Bullish
- Chart 1: [Status]
- Chart 2: [Status]
- Combined Implication: [Maximum bullish / Strategic opportunity]
Scenario 2: Strategic Bullish, Tactical Bearish
- Chart 1: [Status]
- Chart 2: [Status]
- Combined Implication: [Hold core, wait for tactical entry]
Scenario 3: Strategic Bearish, Tactical Bullish
- Chart 1: [Status]
- Chart 2: [Status]
- Combined Implication: [Short-term trade only, tight stops]
Scenario 4: Both Bearish
- Chart 1: [Status]
- Chart 2: [Status]
- Combined Implication: [Defensive positioning]
Unified Recommendation
Overall Market Assessment
Strategic Stance (3-12+ months): [Bullish / Bearish / Neutral]
Tactical Stance (Days to weeks): [Bullish / Bearish / Neutral]
Conviction Level: [High / Medium / Low]
Recommended Actions
For Long-Term Investors: 1. [Action 1 based on Chart 1] 2. [Action 2 based on Chart 1] 3. [Action 3 - position sizing]
For Swing Traders: 1. [Action 1 based on Chart 2] 2. [Action 2 based on Chart 2] 3. [Action 3 - timing and entry/exit levels]
For Active Tactical Traders: 1. [Action 1 combining both charts] 2. [Action 2 - nimble positioning] 3. [Action 3 - risk management]
Optimal Strategy
Current Market Environment Best Suited For: [Strategic positioning / Tactical trading / Both / Neither - stay flat]
[Detailed explanation of why this is the optimal approach given the current breadth readings]
Risk and Opportunity Matrix
Primary Opportunities
1. [Opportunity 1]: [Description and how to capitalize] 2. [Opportunity 2]: [Description and how to capitalize]
Primary Risks
1. [Risk 1]: [Description and how to mitigate] 2. [Risk 2]: [Description and how to mitigate]
Conflicts and Considerations
Chart Divergences
[If there are conflicts between Chart 1 and Chart 2 signals:]
Divergence Identified: [Description of the conflict]
Resolution: [How to interpret and act on the divergence]
Precedence: [Which chart takes priority in this situation and why]
---
Summary and Conclusion
Key Takeaways
1. [Most Important Finding]: [Brief explanation] 2. [Second Most Important Finding]: [Brief explanation] 3. [Third Most Important Finding]: [Brief explanation]
Action Items
Immediate Actions (Now):
- [ ] [Action item 1]
- [ ] [Action item 2]
Pending Actions (Contingent on market movement):
- [ ] If [condition], then [action]
- [ ] If [condition], then [action]
Monitoring Plan
Daily Monitoring Required: [Yes / No]
Key Updates to Watch For: 1. [Update 1 - e.g., new 8MA trough formation] 2. [Update 2 - e.g., color transition in Chart 2] 3. [Update 3 - e.g., breach of key threshold]
Next Analysis Recommended: [Timeframe - daily / every 3 days / weekly]
Confidence and Limitations
Analysis Confidence Level: [High / Medium / Low]
Reasoning for Confidence Level: [Explanation]
Limitations and Caveats:
- [Limitation 1 - e.g., signals work best in trending markets]
- [Limitation 2 - e.g., external shocks can override technical signals]
- [Limitation 3 - e.g., breadth lags price in fast-moving conditions]
---
Disclaimer
This analysis is based purely on market breadth technical indicators and does not consider fundamental factors, macroeconomic data, geopolitical events, or other external factors that may influence market behavior. The strategies referenced are based on historical backtesting and may not perform identically in future market conditions. This analysis represents a probabilistic assessment of potential scenarios and is not a guarantee of future performance or a recommendation to buy or sell securities.
All probabilities are estimates based on technical factors and historical patterns, and are subject to change as new data emerges.
---
End of Breadth Analysis Report
---
Appendix: Chart Images
[If chart images are to be included in the report]
Chart 1: S&P 500 Breadth Index
[Chart image or reference]
Chart 2: Uptrend Stock Ratio
[Chart image or reference]
Breadth Chart Analysis Methodology
This reference provides comprehensive methodology for analyzing two types of market breadth charts that provide complementary views of market health and trend status.
Overview
Market breadth indicators measure the participation of stocks in market moves, providing insights into the strength and sustainability of trends that price indices alone cannot reveal. This skill focuses on two specific breadth measurements:
1. 200MA-Based Breadth Index: Medium to long-term trend assessment tool 2. Uptrend Stock Ratio: Short-term trend and momentum indicator
These two charts work together to provide both strategic (medium/long-term) and tactical (short-term) market perspectives.
---
Chart 1: S&P 500 Breadth Index (200-Day MA Based)
Purpose and Use Case
This chart measures the percentage of S&P 500 stocks trading above their 200-day moving average, providing insight into the medium to long-term health of the market. It serves as a strategic positioning tool for determining major market cycles.
Chart Components
Primary Lines
8-Day Moving Average (8MA) - Orange Line
- Short-term smoothing of the breadth index
- More responsive to rapid market changes
- Key for identifying reversal points
- Used as the primary signal generator for entries
200-Day Moving Average (200MA) - Green Line
- Long-term trend of market breadth
- Slower to respond, filters out noise
- Represents the underlying structural health of the market
- Used as the primary signal generator for exits
Reference Levels
Red Dashed Line (0.73 / 73%)
- Average Peak Level: Historical average of 200MA tops
- Significance: When the breadth index exceeds this level, the market is in an overheated state
- Interpretation:
- Above 73%: Market is extremely bullish but approaching exhaustion
- Significantly above 73%: Elevated risk of correction or consolidation
- Rarely sustainable for extended periods
Blue Dashed Line (0.23 / 23%)
- Average 8MA Trough Level: Historical average of 8MA bottoms
- Significance: When the 8MA falls below this level, the market is in extreme oversold territory
- Interpretation:
- Below 23%: Severe market stress, capitulation-level selling
- High probability of reversal and recovery
- Historically represents excellent buying opportunities
- Extreme fear and pessimism typically present
Signal Markers (Triangles)
Purple/Magenta Downward Triangles (▼)
- 8MA Troughs: Mark the bottoms of the 8-day moving average
- Significance: Potential reversal points, especially when near or below the 23% threshold
- Use: Primary buy signal when 8MA begins to turn up from a trough
Blue Downward Triangles (▼)
- 200MA Troughs: Mark the bottoms of the 200-day moving average
- Significance: Major market cycle lows, extremely rare
- Use: Indicates the beginning of a new long-term bull market
Red Upward Triangles (▲)
- 200MA Peaks: Mark the tops of the 200-day moving average
- Significance: Major market cycle highs indicating trend exhaustion
- Use: Primary sell signal for the tested strategy
Background Shading
Pink Background Regions
- Downtrend Periods: Time periods when the market is in a confirmed downtrend
- Significance: Higher risk environment, defensive positioning warranted
- Use: Visual aid to identify unfavorable market conditions
Interpretation Framework
Market Regimes
Healthy Bull Market
- 8MA and 200MA both rising
- Breadth index between 40% and 73%
- Regular pullbacks to 50-60% range that hold above 8MA
- Wide participation across stocks
Overheated Bull Market
- Breadth index persistently above 73%
- 8MA and 200MA both very elevated
- High risk of near-term correction
- Market vulnerable to negative catalysts
Market Top/Distribution Phase
- 200MA forming a peak (red ▲)
- 8MA begins to roll over even if 200MA hasn't peaked yet
- Breadth index declining from elevated levels
- Signal to exit long positions
Bear Market/Correction
- Both 8MA and 200MA declining
- Pink background shading present
- Breadth index below 50%
- High volatility typical
Capitulation/Extreme Oversold
- 8MA below 23% (blue dashed line)
- 8MA forming a trough (purple ▼)
- Maximum pessimism and fear
- Historically excellent buying opportunities
Early Recovery
- 8MA turning up from a trough
- 200MA may still be declining (lagging)
- Breadth index beginning to rise from extreme lows
- Initial phase of new uptrend
Backtested Strategy
Based on historical analysis, the following strategy has demonstrated strong performance:
Entry Rules
BUY Signal: 8MA Trough Reversal
- Wait for 8MA to form a clear bottom (purple ▼)
- Especially strong when 8MA trough is below 23% (extreme oversold)
- Enter long position in S&P 500 index when 8MA begins to turn upward
- Confirmation: 8MA crosses above previous recent high or shows 2-3 consecutive days of increase
Detailed Reversal Confirmation Criteria
CRITICAL: Do not enter prematurely. A trough formation alone is NOT sufficient for entry. The reversal must be CONFIRMED.
Required Steps for Confirmation:
1. Trough Identification (Step 1):
- 8MA forms a clear bottom marked by purple ▼ triangle
- Trough level should ideally be below 40%, preferably below 30%
- Extreme troughs below 23% provide highest probability setups
2. Initial Reversal (Step 2):
- 8MA begins to move upward from the trough
- First 1-2 periods of increase observed
- WARNING: This is NOT yet a confirmed signal - DO NOT ENTER
3. Confirmation Period (Step 3 - MANDATORY):
- 8MA continues to rise for 2-3 CONSECUTIVE periods after the trough
- Each period should show clear upward progress (at least 2-5% increase)
- No significant pullbacks or reversals during confirmation period
4. Latest Data Verification (Step 4 - CRITICAL):
- Analyze the RIGHTMOST 3-5 data points on the chart
- Verify that the 8MA is CURRENTLY rising (at the latest edge of the chart)
- Confirm no rollover or downward turn has occurred recently
- This step prevents entry on failed reversals
5. Resistance Breakout (Step 5 - Optional but Ideal):
- 8MA crosses above 55-60% level
- This confirms strong breadth improvement
- Provides higher confidence in sustainability
Confirmation Status Classification:
- CONFIRMED (Green Light): Steps 1-4 complete, 8MA rising for 2-3+ consecutive periods and CURRENTLY rising → ENTER
- DEVELOPING (Yellow Light): Steps 1-2 complete, but < 2 consecutive increases → WAIT, MONITOR CLOSELY
- FAILED (Red Light): Steps 1-2 occurred, but 8MA has rolled over and is declining → DO NOT ENTER, signal invalidated
- NO SIGNAL (Red Light): No trough formed → WAIT
Time-Based Confirmation:
- Typical confirmation period: 2-4 weeks after trough
- If confirmation takes > 6 weeks, signal strength decreases
- If 8MA fails to reach 55% within 6 weeks of trough, reassess validity
Failed Reversal Patterns (WARNING SIGNS)
What is a Failed Reversal?
A failed reversal occurs when the 8MA forms a trough and begins to rise, but then rolls over and declines again before reaching sustained momentum. This invalidates the buy signal and often leads to deeper corrections.
Identifying Failed Reversals:
1. Weak Bounce Pattern:
- 8MA rises for only 1-2 periods after trough
- 8MA fails to reach 50-55% level
- 8MA then turns downward again
- Result: False signal, do NOT enter
2. Premature Rollover:
- 8MA rises to 50-60% range
- 8MA fails to sustain the rise, begins declining
- 8MA shows consistent downward trajectory at latest data points
- Result: Reversal failed, exit if already entered or do not enter
3. Double Bottom Formation:
- First trough forms, 8MA rises briefly
- 8MA declines to form second, lower trough
- Result: First trough was not the final bottom, wait for second trough to confirm
Failed Reversal Action Protocol:
- Immediately classify signal as INVALID
- Do NOT enter position
- Wait for 8MA to form a NEW, LOWER trough (often below 23%)
- Reset analysis and wait for fresh confirmation from the new trough
Historical Failed Reversal Examples (refer to chart):
- Failed reversals are less common but occur during severe corrections
- They typically precede deeper declines to extreme oversold (<23%)
- After failed reversal, the eventual confirmed reversal from lower levels often provides excellent entries
Latest Data Point Analysis Protocol
Why Latest Data Points Matter Most:
The most recent 3-5 data points on the chart (rightmost edge) represent the CURRENT market condition. Historical movement is context, but the latest trajectory determines whether a signal is active, developing, or failed.
Protocol:
1. Always Start with the Rightmost Data Point:
- This is the absolute latest reading
- This determines the CURRENT slope of the 8MA
2. Trace Backward 3-5 Data Points:
- Compare current level to 1 week ago
- Compare current level to 2 weeks ago
- Compare current level to 3 weeks ago
- Determine if the trend is: Rising, Falling, or Flat
3. Calculate Consecutive Periods:
- Count how many consecutive periods 8MA has risen
- Count how many consecutive periods 8MA has fallen
- Confirmation requires 2-3 consecutive UP periods
- 1-2 consecutive DOWN periods after a trough = failed reversal warning
4. Visual Slope Assessment:
- Visually trace the 8MA line at the rightmost edge
- Is the line sloping UP, DOWN, or FLAT?
- Do NOT rely on earlier parts of the chart - focus on the edge
Example Analysis:
Scenario: 8MA formed trough at 30% three weeks ago
Week-by-Week Analysis (from trough to present):
- Week 1 after trough: 30% → 40% (UP 10%)
- Week 2 after trough: 40% → 52% (UP 12%)
- Week 3 after trough: 52% → 48% (DOWN 4%)
Current Slope: FALLING
Consecutive Increases: 0 (last period was DOWN)
Status: FAILED REVERSAL - 8MA rolled over after initial bounce
Action: DO NOT ENTER. Signal is invalid. Wait for new trough formation.Scenario: 8MA formed trough at 25% four weeks ago
Week-by-Week Analysis (from trough to present):
- Week 1 after trough: 25% → 35% (UP 10%)
- Week 2 after trough: 35% → 45% (UP 10%)
- Week 3 after trough: 45% → 55% (UP 10%)
- Week 4 after trough: 55% → 60% (UP 5%)
Current Slope: RISING
Consecutive Increases: 4 (all periods UP)
Status: CONFIRMED - Strong sustained reversal
Action: ENTER LONG. Signal is confirmed and robust.Exit Rules
SELL Signal: 200MA Peak Detection
- Exit long position when 200MA forms a peak (red ▲)
- 200MA peaks typically occur when breadth index is near or above 73%
- Do not wait for 200MA to turn down; exit at or near the peak
- This signals the end of the bull market phase
Risk Management
- Stop Loss: Consider exiting if 8MA breaks decisively below its most recent trough
- Partial Profits: Consider taking partial profits when breadth index exceeds 73% (overheated)
- Re-entry: Do not re-enter during the same cycle unless a new 8MA trough forms
Strategy Performance Characteristics
- Win Rate: Historically high due to buying extreme oversold and selling into strength
- Hold Period: Typically several months to over a year
- Drawdown Management: Strategy avoids holding through major bear markets
- False Signals: Rare due to the significant smoothing of the indicators
Analysis Checklist
When analyzing this chart, systematically evaluate:
1. ✓ Current 8MA Level: Where is the 8MA relative to historical troughs and the 23% threshold? 2. ✓ Current 200MA Level: Where is the 200MA relative to historical peaks and the 73% threshold? 3. ✓ 8MA Slope: Is the 8MA rising, falling, or flat? 4. ✓ 200MA Slope: Is the 200MA rising, falling, or flat? 5. ✓ Recent Signal Markers: Any recent triangles indicating troughs or peaks? 6. ✓ Market Regime: Which regime best describes the current state? 7. ✓ Strategy Position: Based on the backtested strategy, should one be long, flat, or preparing to enter/exit? 8. ✓ Distance from Thresholds: How close is the breadth index to key levels (23%, 73%)?
---
Chart 2: US Stock Market - Uptrend Stock Ratio
Purpose and Use Case
This chart measures the percentage of stocks in confirmed uptrends across the US stock market, providing a real-time view of market momentum and participation. It serves as a tactical tool for short-term swing trading and confirming the strength of market moves.
Definition of "Uptrend Stock"
A stock is classified as being in an uptrend when it meets ALL of the following criteria:
1. Price above 200-day MA: Stock is in a long-term uptrend 2. Price above 50-day MA: Stock is in an intermediate-term uptrend 3. Price above 20-day MA: Stock is in a short-term uptrend 4. Positive 1-month performance: Stock has gained value over the past month
This multi-factor definition ensures that only stocks with confirmed momentum across multiple timeframes are counted as "uptrend stocks."
Chart Components
Primary Visualization
Color-Coded Trend Identification
Green Regions
- Represent periods when the market is in an uptrend
- Uptrend stock ratio is generally rising
- Positive market momentum
- Favorable environment for long positions
Red Regions
- Represent periods when the market is in a downtrend
- Uptrend stock ratio is generally falling
- Negative market momentum
- Unfavorable for long positions, defensive posture appropriate
Color Transitions
- Red-to-Green Transition: Downtrend ending, uptrend beginning (BUY signal)
- Green-to-Red Transition: Uptrend ending, downtrend beginning (SELL signal)
Reference Levels
Lower Orange Dashed Line (~10%)
- Short-Term Market Bottom: When the uptrend ratio falls to approximately 10%
- Significance: Extreme oversold condition
- Interpretation:
- Below 10%: Maximum bearish sentiment, capitulation phase
- High probability of near-term reversal
- Short-term bottom often forms at this level
- Excellent risk/reward for initiating long positions
Upper Orange Dashed Line (~37-40%)
- Short-Term Market Top: When the uptrend ratio approaches approximately 40%
- Significance: Overbought condition, market overheating
- Interpretation:
- Near 40%: Market is short-term overbought
- High probability of pullback or consolidation
- Reversal often begins at this level
- Consider taking profits or tightening stops on long positions
Interpretation Framework
Market Conditions
Extreme Oversold (Ratio < 10%)
- Very few stocks participating in uptrend
- Market-wide selling pressure
- High fear and pessimism
- Contrarian buying opportunity
- Often coincides with VIX spikes
- Watch for red-to-green transition
Moderate Bearish (Ratio 10-20%)
- Below-average market participation
- Downtrend still in effect (red)
- Selective stock strength only
- Wait for confirmation before entering
Neutral/Transitional (Ratio 20-30%)
- Average market participation
- Can be either early uptrend or late downtrend
- Color (green vs red) is critical for context
- Watch for trend changes
Moderate Bullish (Ratio 30-37%)
- Healthy uptrend participation
- Green color dominant
- Sustainable momentum
- Favorable for long positions
Extreme Overbought (Ratio > 37-40%)
- Very high participation in uptrend
- Market short-term overheated
- Vulnerable to pullback
- Watch for green-to-red transition
- Consider profit-taking
Trend Transitions
Red-to-Green (Downtrend to Uptrend)
- Entry Signal: Marks the beginning of a new uptrend phase
- Characteristics:
- Uptrend ratio bottoms and begins to rise
- Often occurs from extreme oversold levels (<10-15%)
- Increasing number of stocks breaking above MAs
- Momentum shifting from bearish to bullish
- Action: Initiate long positions in index or strong individual stocks
Green-to-Red (Uptrend to Downtrend)
- Exit Signal: Marks the end of an uptrend phase
- Characteristics:
- Uptrend ratio peaks and begins to fall
- Often occurs from overbought levels (>35-40%)
- Stocks breaking below key moving averages
- Momentum shifting from bullish to bearish
- Action: Exit long positions, move to cash or defensive positions
Trading Strategy
Short-Term Swing Trading Strategy
Entry Rules
1. Wait for Oversold Condition:
- Uptrend ratio declines to 10-15% range
- Market in red (downtrend) phase
2. Identify Trend Reversal:
- Color changes from red to green
- Uptrend ratio begins to rise from lows
- Confirmation: 2-3 consecutive increases in the ratio
3. Enter Long Position:
- Buy S&P 500 index (SPY, ES futures) or basket of leading stocks
- Position size based on risk tolerance
Exit Rules
1. Watch for Overbought Condition:
- Uptrend ratio approaches 37-40% level
- Market has been green for extended period
2. Identify Trend Exhaustion:
- Color changes from green to red
- Uptrend ratio begins to fall from highs
- Momentum waning
3. Exit Long Position:
- Sell index position
- Move to cash or defensive assets
Risk Management
- Stop Loss: Exit if uptrend ratio breaks below recent lows while still in supposed uptrend
- Partial Exits: Consider scaling out as ratio approaches 35-37%
- Position Sizing: Larger positions when entering from extreme oversold (<10%), smaller when entering from moderate levels
- False Signals: Quick reversals (red-green-red or green-red-green within days) may be false; wait for confirmation
Strategy Performance Characteristics
- Timeframe: Days to weeks (swing trading)
- Win Rate: Moderate to high, depending on confirmation criteria
- Hold Period: Typically 1-4 weeks
- Best Performance: When entries align with extreme oversold conditions (<10%)
- Risk: Whipsaws during choppy, trendless markets
Combining Both Charts
For optimal decision-making, use both breadth charts together:
Strategic Positioning (Chart 1: 200MA Breadth)
- Determines overall market regime (bull, bear, neutral)
- Guides strategic asset allocation
- Identifies major entry/exit points for core positions
- Longer holding periods (months to year+)
Tactical Positioning (Chart 2: Uptrend Ratio)
- Determines short-term momentum and timing
- Guides tactical trades and position sizing
- Identifies short-term entry/exit points for swing trades
- Shorter holding periods (days to weeks)
Alignment Scenarios
Scenario 1: Both Bullish
- Chart 1: 8MA rising, 200MA rising or flat, breadth < 73%
- Chart 2: Green (uptrend), ratio rising from oversold
- Action: Maximum bullish stance, aggressive long positioning
Scenario 2: Strategic Bullish, Tactical Bearish
- Chart 1: 8MA rising, 200MA not yet peaked
- Chart 2: Red (downtrend), ratio elevated or falling
- Action: Hold core long positions, avoid new tactical longs, wait for oversold
Scenario 3: Strategic Bearish, Tactical Bullish
- Chart 1: 200MA peaked or declining
- Chart 2: Green (uptrend), ratio rising from extreme oversold
- Action: Short-term tactical longs only, tight stops, prepare for strategic exit
Scenario 4: Both Bearish
- Chart 1: Both MAs declining, pink background
- Chart 2: Red (downtrend), ratio falling
- Action: Defensive positioning, cash or short positions, wait for 8MA trough
Analysis Checklist for Chart 2
When analyzing the uptrend stock ratio chart, systematically evaluate:
1. ✓ Current Ratio Level: What is the current uptrend stock percentage? 2. ✓ Current Color: Is the market in green (uptrend) or red (downtrend)? 3. ✓ Proximity to Thresholds: How close is the ratio to 10% (bottom) or 40% (top)? 4. ✓ Trend Direction: Is the ratio rising, falling, or flat? 5. ✓ Recent Transitions: Any recent color changes from red-to-green or green-to-red? 6. ✓ Duration of Current Trend: How long has the current color phase lasted? 7. ✓ Strategy Position: Based on the swing trading strategy, should one be long, flat, or preparing to enter/exit? 8. ✓ Historical Context: How does the current reading compare to recent extremes?
---
Output Requirements
Language and Style
- Analysis Language: Conduct all thinking and analysis in English
- Output Format: Generate all reports in English using markdown format
- Tone: Professional, objective, analytical
- Terminology: Use precise technical terms consistently
Analysis Structure
Each breadth chart analysis report should include:
1. Current Readings: Specific values for all key metrics 2. Signal Status: Clear identification of any active signals (troughs, peaks, transitions) 3. Market Regime Assessment: Classification of the current market environment 4. Strategy Implications: Specific positioning recommendations based on the backtested strategies 5. Key Levels to Watch: Upcoming thresholds or levels that would change the analysis 6. Risk Considerations: Potential invalidation scenarios or alternative interpretations 7. Combined Analysis (when both charts analyzed): Integration of strategic and tactical perspectives
Objectivity Standards
- Base all analysis strictly on observable chart data
- Avoid incorporating external information (news, fundamentals) unless specifically relevant to breadth interpretation
- Clearly distinguish between factual observations and probabilistic forecasts
- Acknowledge uncertainty when signals are ambiguous
- Present both bullish and bearish scenarios when appropriate
---
Common Pitfalls to Avoid
Chart 1 (200MA Breadth) Pitfalls
- Misreading Recent Trend Direction (MOST COMMON ERROR): Analyzing historical chart movement instead of focusing on the LATEST 3-5 data points at the rightmost edge. This leads to stating "8MA is rising" when it's actually falling at the current edge, or vice versa. ALWAYS analyze the rightmost data points to determine CURRENT slope.
- Premature Entry: Buying before clear 8MA reversal is confirmed (2-3 consecutive periods of increase). Entering on just a trough formation without confirmation often results in losses from failed reversals.
- Ignoring Failed Reversals: Not recognizing when an 8MA has rolled over after an initial bounce from a trough. If the 8MA rises briefly then turns down again, the signal is INVALID.
- Late Exit: Waiting for 200MA to decline instead of exiting at the peak formation. The peak itself is the sell signal - do not wait for confirmation of decline.
- Ignoring Thresholds: Not recognizing significance of 23% and 73% levels. These thresholds provide critical context for entry and exit timing.
- Overtrading: Trying to trade every wiggle in the 8MA instead of waiting for clear, confirmed trough reversals. Patience is essential for this strategy.
Chart 2 (Uptrend Ratio) Pitfalls
- Chasing: Entering after the ratio has already risen substantially from oversold
- Ignoring Color: Focusing only on the ratio level without considering trend direction (color)
- False Breakouts: Not waiting for confirmation of color transitions
- Missing Extremes: Not recognizing when the ratio reaches <10% or >40%
General Pitfalls
- Analyzing in Isolation: Using only one chart without considering the other
- Over-Complicating: Adding too many additional indicators that cloud the core signals
- Recency Bias: Giving too much weight to the most recent move
- Ignoring Historical Context: Not comparing current readings to historical patterns shown on the charts
#!/usr/bin/env python3
"""
Breadth Chart Value Detector (OpenCV Version)
Automatically detects the 200-Day MA and 8-Day MA values from S&P 500 Breadth Index charts
using OpenCV image processing for high-accuracy detection.
This script analyzes chart images to:
1. Calibrate Y-axis using reference lines (0.73 red dashed, 0.23 blue dashed)
2. Detect green line (200-Day MA) position at the right edge
3. Detect orange line (8-Day MA) position at the right edge
4. Convert pixel positions to percentage values
Requirements:
pip install opencv-python numpy
Usage:
python detect_breadth_values.py <image_path>
python detect_breadth_values.py charts/2025-12-22/IMG_5499.jpeg
python detect_breadth_values.py charts/2025-12-22/IMG_5499.jpeg --debug
Output:
JSON with detected values:
{
"200ma": 0.60,
"8ma": 0.63,
"confidence": "high"
}
"""
import argparse
import json
import os
import sys
from typing import Any, Optional
import cv2
import numpy as np
class BreadthChartDetector:
"""
Detects 200-Day MA and 8-Day MA values from S&P 500 Breadth Index charts.
The chart has:
- Green line: 200-Day Moving Average of stocks above their 200-Day MA
- Orange line: 8-Day Moving Average (short-term indicator)
- Red dashed line: Upper reference at 0.73 (73%)
- Blue dashed line: Lower reference at 0.23 (23%)
- Pink background: Indicates downtrend periods
"""
# Reference values for Y-axis calibration
RED_LINE_VALUE = 0.73 # Upper reference line
BLUE_LINE_VALUE = 0.23 # Lower reference line
# HSV color ranges for OpenCV (H: 0-179, S: 0-255, V: 0-255)
# OpenCV uses H/2 (0-179 instead of 0-360)
# Green line (200MA): Hue ~60-80 in OpenCV scale
GREEN_HSV_LOW = np.array([35, 60, 60])
GREEN_HSV_HIGH = np.array([85, 255, 255])
# Orange line (8MA): Hue ~10-25 in OpenCV scale
ORANGE_HSV_LOW = np.array([5, 100, 100])
ORANGE_HSV_HIGH = np.array([25, 255, 255])
# Red reference line: Hue ~0-10 or 170-179
RED_HSV_LOW1 = np.array([0, 80, 80])
RED_HSV_HIGH1 = np.array([10, 255, 255])
RED_HSV_LOW2 = np.array([170, 80, 80])
RED_HSV_HIGH2 = np.array([179, 255, 255])
# Blue reference line: Hue ~100-130
BLUE_HSV_LOW = np.array([95, 60, 60])
BLUE_HSV_HIGH = np.array([135, 255, 255])
def __init__(self, image_path: str, debug: bool = False):
"""
Initialize the detector with an image path.
Args:
image_path: Path to the breadth chart image
debug: If True, saves debug images showing detection results
"""
self.image_path = image_path
self.debug = debug
# Read image with OpenCV (BGR format)
self.img_bgr = cv2.imread(image_path)
if self.img_bgr is None:
raise ValueError(f"Could not read image: {image_path}")
self.height, self.width = self.img_bgr.shape[:2]
# Convert to HSV for color detection
self.img_hsv = cv2.cvtColor(self.img_bgr, cv2.COLOR_BGR2HSV)
# Calibration results
self.red_line_y: Optional[int] = None
self.blue_line_y: Optional[int] = None
self.y_scale: Optional[float] = None
self.y_offset: Optional[float] = None
# Debug info
self.debug_info: dict[str, Any] = {}
def _create_color_mask(
self,
hsv_low: np.ndarray,
hsv_high: np.ndarray,
hsv_low2: np.ndarray = None,
hsv_high2: np.ndarray = None,
) -> np.ndarray:
"""
Create a binary mask for pixels within the specified HSV range.
Args:
hsv_low: Lower HSV bounds
hsv_high: Upper HSV bounds
hsv_low2: Optional second lower bounds (for wraparound colors like red)
hsv_high2: Optional second upper bounds
Returns:
Binary mask (255 where color matches, 0 elsewhere)
"""
mask = cv2.inRange(self.img_hsv, hsv_low, hsv_high)
if hsv_low2 is not None and hsv_high2 is not None:
mask2 = cv2.inRange(self.img_hsv, hsv_low2, hsv_high2)
mask = cv2.bitwise_or(mask, mask2)
return mask
def _find_horizontal_line_y(
self, mask: np.ndarray, min_width_ratio: float = 0.15
) -> Optional[int]:
"""
Find the Y coordinate of a horizontal line in the mask.
Uses horizontal projection (sum of pixels per row) to find lines.
Args:
mask: Binary mask of the target color
min_width_ratio: Minimum ratio of image width the line should span
Returns:
Y coordinate of the detected line, or None
"""
min_width = int(self.width * min_width_ratio)
# Calculate horizontal projection (sum of white pixels per row)
h_projection = np.sum(mask > 0, axis=1)
# Find rows with significant horizontal extent
candidate_rows = np.where(h_projection >= min_width)[0]
if len(candidate_rows) == 0:
return None
# Group consecutive rows and find the strongest group
# (handles thick lines)
groups = []
current_group = [candidate_rows[0]]
for i in range(1, len(candidate_rows)):
if candidate_rows[i] - candidate_rows[i - 1] <= 3: # Allow 3px gaps
current_group.append(candidate_rows[i])
else:
groups.append(current_group)
current_group = [candidate_rows[i]]
groups.append(current_group)
# Find the group with the maximum total projection value
best_group = max(groups, key=lambda g: sum(h_projection[y] for y in g))
# Return the center of the best group
return int(np.mean(best_group))
def _detect_reference_lines(self) -> tuple[bool, bool]:
"""
Detect the red (0.73) and blue (0.23) reference lines for Y-axis calibration.
Returns:
Tuple of (red_detected, blue_detected)
"""
# Create masks for reference lines
red_mask = self._create_color_mask(
self.RED_HSV_LOW1, self.RED_HSV_HIGH1, self.RED_HSV_LOW2, self.RED_HSV_HIGH2
)
blue_mask = self._create_color_mask(self.BLUE_HSV_LOW, self.BLUE_HSV_HIGH)
if self.debug:
self.debug_info["red_mask"] = red_mask
self.debug_info["blue_mask"] = blue_mask
# Find line positions
self.red_line_y = self._find_horizontal_line_y(red_mask)
self.blue_line_y = self._find_horizontal_line_y(blue_mask)
return (self.red_line_y is not None, self.blue_line_y is not None)
def _calibrate_y_axis(self) -> bool:
"""
Calibrate Y-axis using detected reference lines.
The Y-axis is inverted in images (0 at top), so:
- Red line (0.73) should be at lower Y pixel value (higher in image)
- Blue line (0.23) should be at higher Y pixel value (lower in image)
Returns:
True if calibration succeeded, False otherwise
"""
red_detected, blue_detected = self._detect_reference_lines()
if not red_detected or not blue_detected:
# Fallback: estimate based on image dimensions
print("Warning: Could not detect reference lines. Using estimated calibration.")
chart_top = int(self.height * 0.1)
chart_bottom = int(self.height * 0.85)
# Estimate reference line positions
if not red_detected:
self.red_line_y = chart_top + int((chart_bottom - chart_top) * (1 - 0.73))
if not blue_detected:
self.blue_line_y = chart_top + int((chart_bottom - chart_top) * (1 - 0.23))
# Ensure red line is above blue line (lower Y value = higher position)
if self.red_line_y > self.blue_line_y:
self.red_line_y, self.blue_line_y = self.blue_line_y, self.red_line_y
# Calculate linear transformation: value = scale * pixel_y + offset
pixel_diff = self.blue_line_y - self.red_line_y
value_diff = self.BLUE_LINE_VALUE - self.RED_LINE_VALUE # Negative (0.23 - 0.73)
if pixel_diff == 0:
return False
self.y_scale = value_diff / pixel_diff
self.y_offset = self.RED_LINE_VALUE - (self.y_scale * self.red_line_y)
return True
def _pixel_y_to_value(self, pixel_y: int) -> float:
"""Convert pixel Y coordinate to percentage value (0-1)."""
if self.y_scale is None or self.y_offset is None:
raise ValueError("Y-axis not calibrated. Call _calibrate_y_axis first.")
return self.y_scale * pixel_y + self.y_offset
def _detect_line_at_right_edge(
self, mask: np.ndarray, right_percent: float = 0.15, line_name: str = "line"
) -> Optional[float]:
"""
Detect a colored line's Y position at the right edge of the chart.
Uses weighted centroid calculation for sub-pixel accuracy.
Args:
mask: Binary mask of the target line color
right_percent: Percentage of image width to analyze from right edge
line_name: Name for debug logging
Returns:
Detected value (0-1), or None if not detected
"""
right_start = int(self.width * (1 - right_percent))
# Extract the right edge region
right_region = mask[:, right_start:]
# Find all white (line) pixels in the region
y_coords, x_coords = np.where(right_region > 0)
if len(y_coords) == 0:
return None
# Weight by x position (prefer rightmost pixels for current value)
# Pixels closer to right edge (higher x) get more weight
weights = (x_coords - x_coords.min() + 1).astype(float)
weights = weights**2 # Quadratic weighting for more emphasis on rightmost
# Calculate weighted Y centroid
weighted_y = np.average(y_coords, weights=weights)
# Also calculate the mode (most common Y) for validation
y_bins = np.bincount(y_coords, minlength=self.height)
mode_y = np.argmax(y_bins)
# Use weighted average if it's reasonably close to mode
if abs(weighted_y - mode_y) > 20:
# Large discrepancy - use mode for robustness
final_y = mode_y
else:
final_y = weighted_y
if self.debug:
self.debug_info[f"{line_name}_weighted_y"] = weighted_y
self.debug_info[f"{line_name}_mode_y"] = mode_y
self.debug_info[f"{line_name}_final_y"] = final_y
self.debug_info[f"{line_name}_pixel_count"] = len(y_coords)
# Convert to actual value
value = self._pixel_y_to_value(int(final_y))
# Clamp to reasonable range
value = max(0.0, min(1.0, value))
return round(value, 3)
def _detect_green_line(self) -> Optional[float]:
"""Detect the 200-Day MA (green) line value."""
green_mask = self._create_color_mask(self.GREEN_HSV_LOW, self.GREEN_HSV_HIGH)
if self.debug:
self.debug_info["green_mask"] = green_mask
return self._detect_line_at_right_edge(green_mask, line_name="green")
def _detect_orange_line(self) -> Optional[float]:
"""Detect the 8-Day MA (orange) line value."""
orange_mask = self._create_color_mask(self.ORANGE_HSV_LOW, self.ORANGE_HSV_HIGH)
if self.debug:
self.debug_info["orange_mask"] = orange_mask
return self._detect_line_at_right_edge(orange_mask, line_name="orange")
def _assess_confidence(self, value_200ma: Optional[float], value_8ma: Optional[float]) -> str:
"""
Assess confidence level of the detection.
Returns:
'high', 'medium', or 'low'
"""
if value_200ma is None and value_8ma is None:
return "failed"
# Check if reference lines were detected (not estimated)
red_detected, blue_detected = (
(
self.debug_info.get("red_mask") is not None
and self._find_horizontal_line_y(
self.debug_info.get("red_mask", np.zeros((1, 1), dtype=np.uint8))
)
is not None,
self.debug_info.get("blue_mask") is not None
and self._find_horizontal_line_y(
self.debug_info.get("blue_mask", np.zeros((1, 1), dtype=np.uint8))
)
is not None,
)
if self.debug
else (self.red_line_y is not None, self.blue_line_y is not None)
)
if not (red_detected and blue_detected):
base_confidence = "low"
else:
base_confidence = "high"
# Degrade confidence if only one value detected
if value_200ma is None or value_8ma is None:
if base_confidence == "high":
return "medium"
return "low"
# Check for reasonable value range
if not (0.1 <= value_200ma <= 0.9) or not (0.1 <= value_8ma <= 0.9):
return "low"
# Check pixel counts if available
if self.debug:
green_count = self.debug_info.get("green_pixel_count", 0)
orange_count = self.debug_info.get("orange_pixel_count", 0)
if green_count < 20 or orange_count < 20:
if base_confidence == "high":
return "medium"
return base_confidence
def analyze(self) -> dict[str, Any]:
"""
Perform full analysis of the breadth chart.
Returns:
Dictionary with detected values and metadata
"""
# Calibrate Y-axis
calibrated = self._calibrate_y_axis()
# Detect line values
value_200ma = self._detect_green_line()
value_8ma = self._detect_orange_line()
# Assess confidence
confidence = self._assess_confidence(value_200ma, value_8ma)
# Build result
result = {
"200ma": value_200ma,
"200ma_percent": f"{value_200ma * 100:.1f}%" if value_200ma else None,
"8ma": value_8ma,
"8ma_percent": f"{value_8ma * 100:.1f}%" if value_8ma else None,
"confidence": confidence,
"calibration": {
"red_line_y": self.red_line_y,
"blue_line_y": self.blue_line_y,
"calibrated": calibrated,
"y_scale": self.y_scale,
"y_offset": self.y_offset,
},
"image": {"path": self.image_path, "width": self.width, "height": self.height},
}
# Add interpretation
if value_200ma is not None:
if value_200ma >= 0.60:
result["200ma_interpretation"] = "healthy (>=60%)"
elif value_200ma >= 0.50:
result["200ma_interpretation"] = "narrow_rally (50-60%)"
elif value_200ma >= 0.40:
result["200ma_interpretation"] = "caution (40-50%)"
else:
result["200ma_interpretation"] = "weak (<40%)"
if value_8ma is not None:
if value_8ma >= 0.73:
result["8ma_interpretation"] = "overbought (>=73%)"
elif value_8ma >= 0.60:
result["8ma_interpretation"] = "healthy_bullish (60-73%)"
elif value_8ma >= 0.40:
result["8ma_interpretation"] = "neutral (40-60%)"
elif value_8ma >= 0.23:
result["8ma_interpretation"] = "weak (23-40%)"
else:
result["8ma_interpretation"] = "oversold (<23%)"
if self.debug:
result["debug"] = {
"green_pixel_count": self.debug_info.get("green_pixel_count"),
"orange_pixel_count": self.debug_info.get("orange_pixel_count"),
"green_weighted_y": self.debug_info.get("green_weighted_y"),
"orange_weighted_y": self.debug_info.get("orange_weighted_y"),
}
self._save_debug_image(value_200ma, value_8ma)
return result
def _save_debug_image(self, value_200ma: Optional[float], value_8ma: Optional[float]):
"""Save a debug image with detected values marked."""
debug_img = self.img_bgr.copy()
# Draw reference lines
if self.red_line_y:
cv2.line(
debug_img, (0, self.red_line_y), (self.width, self.red_line_y), (0, 0, 255), 2
) # Red in BGR
cv2.putText(
debug_img,
f"0.73 ref (y={self.red_line_y})",
(10, self.red_line_y - 10),
cv2.FONT_HERSHEY_SIMPLEX,
0.5,
(0, 0, 255),
1,
)
if self.blue_line_y:
cv2.line(
debug_img, (0, self.blue_line_y), (self.width, self.blue_line_y), (255, 0, 0), 2
) # Blue in BGR
cv2.putText(
debug_img,
f"0.23 ref (y={self.blue_line_y})",
(10, self.blue_line_y + 20),
cv2.FONT_HERSHEY_SIMPLEX,
0.5,
(255, 0, 0),
1,
)
# Mark detected values on right edge
right_x = self.width - 200
if value_200ma is not None and self.y_scale is not None:
y_200ma = int((value_200ma - self.y_offset) / self.y_scale)
cv2.circle(debug_img, (right_x, y_200ma), 15, (0, 255, 0), 3) # Green
cv2.putText(
debug_img,
f"200MA: {value_200ma:.3f} ({value_200ma * 100:.1f}%)",
(right_x - 180, y_200ma - 20),
cv2.FONT_HERSHEY_SIMPLEX,
0.6,
(0, 255, 0),
2,
)
if value_8ma is not None and self.y_scale is not None:
y_8ma = int((value_8ma - self.y_offset) / self.y_scale)
cv2.circle(debug_img, (right_x + 40, y_8ma), 15, (0, 165, 255), 3) # Orange in BGR
cv2.putText(
debug_img,
f"8MA: {value_8ma:.3f} ({value_8ma * 100:.1f}%)",
(right_x - 180, y_8ma + 30),
cv2.FONT_HERSHEY_SIMPLEX,
0.6,
(0, 165, 255),
2,
)
# Save color masks as well
base, ext = os.path.splitext(self.image_path)
debug_path = f"{base}_debug_detection{ext}"
cv2.imwrite(debug_path, debug_img)
print(f"Debug detection image saved to: {debug_path}")
# Save individual masks if available
if "green_mask" in self.debug_info:
mask_path = f"{base}_debug_green_mask.png"
cv2.imwrite(mask_path, self.debug_info["green_mask"])
print(f"Green mask saved to: {mask_path}")
if "orange_mask" in self.debug_info:
mask_path = f"{base}_debug_orange_mask.png"
cv2.imwrite(mask_path, self.debug_info["orange_mask"])
print(f"Orange mask saved to: {mask_path}")
def format_result_for_human(result: dict[str, Any]) -> str:
"""Format the detection result for human-readable output."""
lines = []
lines.append("=" * 60)
lines.append("Breadth Chart Detection Results (OpenCV)")
lines.append("=" * 60)
lines.append(f"\nImage: {result['image']['path']}")
lines.append(f"Confidence: {result['confidence'].upper()}")
lines.append("\n--- Detected Values ---")
if result["200ma"] is not None:
lines.append(
f"200-Day MA: {result['200ma_percent']} ({result.get('200ma_interpretation', 'N/A')})"
)
else:
lines.append("200-Day MA: NOT DETECTED")
if result["8ma"] is not None:
lines.append(
f"8-Day MA: {result['8ma_percent']} ({result.get('8ma_interpretation', 'N/A')})"
)
else:
lines.append("8-Day MA: NOT DETECTED")
lines.append("\n--- Calibration ---")
cal = result["calibration"]
lines.append(f"Red line (0.73) Y-pixel: {cal['red_line_y']}")
lines.append(f"Blue line (0.23) Y-pixel: {cal['blue_line_y']}")
lines.append(f"Y-scale: {cal.get('y_scale', 'N/A')}")
lines.append(f"Calibration successful: {cal['calibrated']}")
if "debug" in result:
lines.append("\n--- Debug Info ---")
lines.append(f"Green pixels detected: {result['debug'].get('green_pixel_count', 'N/A')}")
lines.append(f"Orange pixels detected: {result['debug'].get('orange_pixel_count', 'N/A')}")
lines.append("\n" + "=" * 60)
return "\n".join(lines)
def main():
parser = argparse.ArgumentParser(
description="Detect 200MA and 8MA values from S&P 500 Breadth Index charts (OpenCV version).",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
python detect_breadth_values.py charts/2025-12-22/IMG_5499.jpeg
python detect_breadth_values.py charts/2025-12-22/IMG_5499.jpeg --debug
python detect_breadth_values.py charts/2025-12-22/IMG_5499.jpeg --json
""",
)
parser.add_argument("image_path", help="Path to the breadth chart image")
parser.add_argument(
"--debug", action="store_true", help="Save debug images showing detection process"
)
parser.add_argument("--json", action="store_true", help="Output results as JSON only")
args = parser.parse_args()
if not os.path.isfile(args.image_path):
print(f"Error: File not found: {args.image_path}", file=sys.stderr)
sys.exit(1)
try:
detector = BreadthChartDetector(args.image_path, debug=args.debug)
result = detector.analyze()
if args.json:
print(json.dumps(result, indent=2))
else:
print(format_result_for_human(result))
print("\nJSON output:")
print(json.dumps(result, indent=2))
# Exit with error code if detection failed
if result["confidence"] == "failed":
sys.exit(1)
except Exception as e:
print(f"Error: {str(e)}", file=sys.stderr)
if args.debug:
import traceback
traceback.print_exc()
sys.exit(1)
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""
Uptrend Stock Ratio Chart Detector (OpenCV Version)
Automatically detects the current value, color (GREEN/RED), and trend direction
from US Stock Market Uptrend Ratio charts using OpenCV image processing.
This script analyzes chart images to:
1. Calibrate Y-axis using orange reference lines (0.40 upper, 0.10 lower)
2. Detect the rightmost filled area (GREEN = uptrend, RED = downtrend)
3. Find the top edge of the filled area to get the current percentage
4. Determine trend direction from recent data points
Requirements:
pip install opencv-python numpy
Usage:
python detect_uptrend_ratio.py <image_path>
python detect_uptrend_ratio.py charts/2026-01-05/uptrend_ratio.jpeg
python detect_uptrend_ratio.py charts/2026-01-05/uptrend_ratio.jpeg --debug
Output:
{
"current_value": 0.23,
"current_color": "RED",
"trend_direction": "FALLING",
"confidence": "HIGH",
"interpretation": "neutral_bearish"
}
"""
import argparse
import json
import os
import sys
from pathlib import Path
from typing import Any, Optional
import numpy as np
try:
import cv2
except ImportError:
cv2 = None
class UptrendRatioDetector:
"""
Detects current value and trend from US Stock Market Uptrend Ratio charts.
The chart has:
- GREEN filled area: Stocks in uptrend (above their moving average)
- RED filled area: Stocks in downtrend (below their moving average)
- Orange dashed lines: Reference at 0.40 (40%) upper and 0.10 (10%) lower
"""
# Reference values for Y-axis calibration
# NOTE: The actual chart uses 37% (not 40%) for the upper reference line
UPPER_REFERENCE = 0.37 # Upper orange reference line (37%)
LOWER_REFERENCE = 0.10 # Lower orange reference line (10%)
# HSV color ranges for OpenCV (H: 0-179, S: 0-255, V: 0-255)
# OpenCV uses H/2 (0-179 instead of 0-360)
# Green area: Hue ~60-80 in OpenCV scale (bright green)
# Saturation: 40+ to capture both bright and slightly faded greens
GREEN_HSV_LOW = np.array([35, 40, 40])
GREEN_HSV_HIGH = np.array([85, 255, 255])
# Red area: Hue ~0-10 or 170-179 (red wraps around)
# Chart uses coral/salmon red - moderate saturation to catch both bright and faded reds
# Value (V) threshold at 60 to capture filled areas but not dark shadows
RED_HSV_LOW1 = np.array([0, 70, 60])
RED_HSV_HIGH1 = np.array([12, 255, 255])
RED_HSV_LOW2 = np.array([168, 70, 60])
RED_HSV_HIGH2 = np.array([179, 255, 255])
# Orange reference lines: Hue ~10-25 (between red and yellow)
# Lower saturation range for the beige/tan moving average line
ORANGE_HSV_LOW = np.array([8, 50, 100])
ORANGE_HSV_HIGH = np.array([25, 200, 255])
# Gray/beige moving average line to EXCLUDE from detection
# This line has low saturation and can interfere with color detection
GRAY_HSV_LOW = np.array([0, 0, 100])
GRAY_HSV_HIGH = np.array([30, 60, 220])
def __init__(self, image_path: str):
"""
Initialize the detector with an image path.
Args:
image_path: Path to the Uptrend Ratio chart image
"""
self.image_path = image_path
# Image data (loaded lazily)
self.img_bgr = None
self.img_hsv = None
self.height = 0
self.width = 0
# Calibration results
self.upper_ref_y: Optional[int] = None
self.lower_ref_y: Optional[int] = None
self.y_scale: Optional[float] = None
self.y_offset: Optional[float] = None
self.calibration_success = False
self._ref_lines_detected: tuple[bool, bool] = (
False,
False,
) # Cache for reference line detection
# Detection results
self._current_value: Optional[float] = None
self._current_color: Optional[str] = None
self._trend_direction: Optional[str] = None
self._confidence: str = "FAILED"
# Debug info
self.debug_info: dict[str, Any] = {}
def _load_image(self) -> bool:
"""
Load and validate the image.
Returns:
True if image loaded successfully
Raises:
FileNotFoundError: If image file doesn't exist
"""
if not os.path.isfile(self.image_path):
raise FileNotFoundError(f"Image file not found: {self.image_path}")
if cv2 is None:
self._confidence = "FAILED"
self.debug_info["error"] = "OpenCV not installed"
return False
self.img_bgr = cv2.imread(self.image_path)
if self.img_bgr is None:
self._confidence = "FAILED"
self.debug_info["error"] = f"Could not read image: {self.image_path}"
return False
self.height, self.width = self.img_bgr.shape[:2]
self.img_hsv = cv2.cvtColor(self.img_bgr, cv2.COLOR_BGR2HSV)
return True
def _create_color_mask(
self,
hsv_low: np.ndarray,
hsv_high: np.ndarray,
hsv_low2: np.ndarray = None,
hsv_high2: np.ndarray = None,
) -> np.ndarray:
"""
Create a binary mask for pixels within the specified HSV range.
Args:
hsv_low: Lower HSV bounds
hsv_high: Upper HSV bounds
hsv_low2: Optional second lower bounds (for wraparound colors like red)
hsv_high2: Optional second upper bounds
Returns:
Binary mask (255 where color matches, 0 elsewhere)
"""
mask = cv2.inRange(self.img_hsv, hsv_low, hsv_high)
if hsv_low2 is not None and hsv_high2 is not None:
mask2 = cv2.inRange(self.img_hsv, hsv_low2, hsv_high2)
mask = cv2.bitwise_or(mask, mask2)
return mask
def _find_horizontal_line_y(
self, mask: np.ndarray, min_width_ratio: float = 0.15, y_range: tuple[int, int] = None
) -> Optional[int]:
"""
Find the Y coordinate of a horizontal line in the mask.
Args:
mask: Binary mask of the target color
min_width_ratio: Minimum ratio of image width the line should span
y_range: Optional (y_min, y_max) to restrict search area
Returns:
Y coordinate of the detected line, or None
"""
min_width = int(self.width * min_width_ratio)
# Restrict to y_range if specified
if y_range:
search_mask = np.zeros_like(mask)
search_mask[y_range[0] : y_range[1], :] = mask[y_range[0] : y_range[1], :]
mask = search_mask
# Calculate horizontal projection (sum of white pixels per row)
h_projection = np.sum(mask > 0, axis=1)
# Find rows with significant horizontal extent
candidate_rows = np.where(h_projection >= min_width)[0]
if len(candidate_rows) == 0:
return None
# Group consecutive rows and find the strongest group
groups = []
current_group = [candidate_rows[0]]
for i in range(1, len(candidate_rows)):
if candidate_rows[i] - candidate_rows[i - 1] <= 5: # Allow 5px gaps
current_group.append(candidate_rows[i])
else:
groups.append(current_group)
current_group = [candidate_rows[i]]
groups.append(current_group)
# Find the group with the maximum total projection value
best_group = max(groups, key=lambda g: sum(h_projection[y] for y in g))
# Return the center of the best group
return int(np.mean(best_group))
def _detect_reference_lines(self) -> tuple[bool, bool]:
"""
Detect the orange reference lines (40% and 10%) for Y-axis calibration.
Returns:
Tuple of (upper_detected, lower_detected)
"""
orange_mask = self._create_color_mask(self.ORANGE_HSV_LOW, self.ORANGE_HSV_HIGH)
if self.debug_info.get("save_masks"):
self.debug_info["orange_mask"] = orange_mask
# Find both reference lines
# Upper line (40%) should be in upper half of image
# Lower line (10%) should be in lower half of image
mid_y = self.height // 2
upper_y = self._find_horizontal_line_y(orange_mask, y_range=(0, mid_y))
lower_y = self._find_horizontal_line_y(orange_mask, y_range=(mid_y, self.height))
# Fallback: find top two lines if split search fails
if upper_y is None or lower_y is None:
# Try full search
h_projection = np.sum(orange_mask > 0, axis=1)
min_width = int(self.width * 0.12)
candidate_rows = np.where(h_projection >= min_width)[0]
if len(candidate_rows) > 0:
# Group into clusters
groups = []
current_group = [candidate_rows[0]]
for i in range(1, len(candidate_rows)):
if candidate_rows[i] - candidate_rows[i - 1] <= 10:
current_group.append(candidate_rows[i])
else:
groups.append(current_group)
current_group = [candidate_rows[i]]
groups.append(current_group)
# Sort groups by y position and take top 2
group_centers = sorted([int(np.mean(g)) for g in groups if len(g) > 2])
if len(group_centers) >= 2:
upper_y = group_centers[0]
lower_y = group_centers[-1]
elif len(group_centers) == 1:
# Only one line found - determine which one
y = group_centers[0]
if y < mid_y:
upper_y = y
else:
lower_y = y
self.upper_ref_y = upper_y
self.lower_ref_y = lower_y
return (upper_y is not None, lower_y is not None)
def _calibrate_y_axis(self) -> bool:
"""
Calibrate Y-axis using detected reference lines.
The Y-axis is inverted in images (0 at top), so:
- Upper line (0.40) should be at lower Y pixel value (higher in image)
- Lower line (0.10) should be at higher Y pixel value (lower in image)
Returns:
True if calibration succeeded
"""
upper_detected, lower_detected = self._detect_reference_lines()
self._ref_lines_detected = (upper_detected, lower_detected) # Cache result
# Fallback estimation if reference lines not detected
if not upper_detected or not lower_detected:
# Estimate based on typical chart layout
chart_top = int(self.height * 0.12)
chart_bottom = int(self.height * 0.85)
chart_range = chart_bottom - chart_top
if not upper_detected:
# 40% line should be ~20% from top of chart area (since range is 0-50%)
self.upper_ref_y = chart_top + int(chart_range * 0.20)
if not lower_detected:
# 10% line should be ~80% from top of chart area
self.lower_ref_y = chart_top + int(chart_range * 0.80)
# Validate: upper should have lower Y than lower (images are Y-inverted)
if self.upper_ref_y is not None and self.lower_ref_y is not None:
if self.upper_ref_y > self.lower_ref_y:
# Swap if reversed
self.upper_ref_y, self.lower_ref_y = self.lower_ref_y, self.upper_ref_y
# Calculate linear transformation: value = scale * pixel_y + offset
if self.upper_ref_y is None or self.lower_ref_y is None:
self.calibration_success = False
return False
pixel_diff = self.lower_ref_y - self.upper_ref_y
value_diff = self.LOWER_REFERENCE - self.UPPER_REFERENCE # 0.10 - 0.40 = -0.30
if pixel_diff == 0:
self.calibration_success = False
return False
self.y_scale = value_diff / pixel_diff
self.y_offset = self.UPPER_REFERENCE - (self.y_scale * self.upper_ref_y)
self.calibration_success = True
return True
def _pixel_to_value(self, pixel_y: int) -> float:
"""Convert pixel Y coordinate to percentage value (0-1)."""
if self.y_scale is None or self.y_offset is None:
return 0.0
return self.y_scale * pixel_y + self.y_offset
def _value_to_pixel(self, value: float) -> int:
"""Convert percentage value (0-1) to pixel Y coordinate."""
if self.y_scale is None or self.y_offset is None:
return 0
return int((value - self.y_offset) / self.y_scale)
def _detect_current_value_and_color(self) -> tuple[Optional[float], Optional[str]]:
"""
Detect the current value and color at the right edge of the chart.
Returns:
Tuple of (value, color) where color is 'GREEN', 'RED', or 'UNKNOWN'
"""
# Create masks for green and red areas
green_mask = self._create_color_mask(self.GREEN_HSV_LOW, self.GREEN_HSV_HIGH)
red_mask = self._create_color_mask(
self.RED_HSV_LOW1, self.RED_HSV_HIGH1, self.RED_HSV_LOW2, self.RED_HSV_HIGH2
)
# Create gray exclusion mask (moving average line)
gray_mask = self._create_color_mask(self.GRAY_HSV_LOW, self.GRAY_HSV_HIGH)
# Remove gray areas from green and red masks to avoid detecting the MA line
green_mask = cv2.bitwise_and(green_mask, cv2.bitwise_not(gray_mask))
red_mask = cv2.bitwise_and(red_mask, cv2.bitwise_not(gray_mask))
if self.debug_info.get("save_masks"):
self.debug_info["green_mask"] = green_mask
self.debug_info["red_mask"] = red_mask
self.debug_info["gray_mask"] = gray_mask
# Strategy: Find the EXACT rightmost column with data and analyze ONLY that column
# The chart is filled from bottom, so we need to find where colored pixels END (top edge)
# Find the rightmost column that has colored pixels
combined_mask = cv2.bitwise_or(green_mask, red_mask)
# FIX (Issue #5): Collect ALL colored columns, then select the TRUE rightmost
# Previous bug: Early break selected FIRST column with >10 pixels, not the actual rightmost
# This caused GREEN (4-8px) to be skipped and RED (29px) in an earlier column to be selected
# FIX (Issue #5b): Lower threshold from 10 to 3 pixels to detect thin lines
# Real case: 6px GREEN (true rightmost) was excluded, 29px RED (older data) was selected
colored_cols = []
for col in range(self.width - 1, max(0, self.width - 150), -1):
col_pixels = np.sum(combined_mask[:, col] > 0)
if col_pixels >= 3: # At least 3 pixels of colored data (lowered from 10)
colored_cols.append(col)
# Select the absolute rightmost column (maximum index)
if colored_cols:
rightmost_col = max(colored_cols)
self.debug_info["colored_cols_found"] = len(colored_cols)
self.debug_info["colored_cols_range"] = (min(colored_cols), max(colored_cols))
else:
# Fallback: use last 5% of width
rightmost_col = int(self.width * 0.95)
self.debug_info["colored_cols_found"] = 0
self.debug_info["rightmost_col"] = rightmost_col
# Analyze ONLY the rightmost column (not a strip!) to get the current value
# Using a strip would include older data points from columns to the left
col_green = green_mask[:, rightmost_col]
col_red = red_mask[:, rightmost_col]
# Count pixels in just this column
green_col_count = np.sum(col_green > 0)
red_col_count = np.sum(col_red > 0)
self.debug_info["green_pixel_count_right"] = green_col_count
self.debug_info["red_pixel_count_right"] = red_col_count
# Determine color at the rightmost point
if red_col_count > green_col_count:
dominant_color = "RED"
col_mask = col_red
elif green_col_count > red_col_count:
dominant_color = "GREEN"
col_mask = col_green
elif red_col_count > 0:
dominant_color = "RED"
col_mask = col_red
elif green_col_count > 0:
dominant_color = "GREEN"
col_mask = col_green
else:
return None, "UNKNOWN"
# Find the top edge (minimum Y value where there are colored pixels)
colored_rows = np.where(col_mask > 0)[0]
if len(colored_rows) == 0:
return None, dominant_color
# The top edge (minimum Y) represents the current value
# This is where the filled area ends (chart fills from bottom)
top_edge_y = colored_rows.min()
bottom_edge_y = colored_rows.max()
self.debug_info["top_edge_y"] = top_edge_y
self.debug_info["bottom_edge_y"] = bottom_edge_y
# Convert pixel position to value
value = self._pixel_to_value(top_edge_y)
# Clamp to reasonable range (0-0.55 for Uptrend Ratio)
value = max(0.0, min(0.55, value))
self.debug_info["detected_value"] = value
return round(value, 3), dominant_color
def _detect_trend_direction(self) -> str:
"""
Detect the trend direction based on recent values.
Analyzes multiple points along the right portion of the chart
to determine if the value is rising, falling, or flat.
Returns:
'RISING', 'FALLING', 'FLAT', or 'UNKNOWN'
"""
# Create combined color mask with gray exclusion (Issue #4 fix)
green_mask = self._create_color_mask(self.GREEN_HSV_LOW, self.GREEN_HSV_HIGH)
red_mask = self._create_color_mask(
self.RED_HSV_LOW1, self.RED_HSV_HIGH1, self.RED_HSV_LOW2, self.RED_HSV_HIGH2
)
# Exclude gray MA line to prevent false detection
gray_mask = self._create_color_mask(self.GRAY_HSV_LOW, self.GRAY_HSV_HIGH)
green_mask = cv2.bitwise_and(green_mask, cv2.bitwise_not(gray_mask))
red_mask = cv2.bitwise_and(red_mask, cv2.bitwise_not(gray_mask))
combined_mask = cv2.bitwise_or(green_mask, red_mask)
# Sample points from right 30% of image
sample_start = int(self.width * 0.70)
sample_end = int(self.width * 0.98)
num_samples = 5
sample_values = []
sample_xs = np.linspace(sample_start, sample_end, num_samples, dtype=int)
for x in sample_xs:
# Get column slice (+/- 3 pixels for stability)
col_start = max(0, x - 3)
col_end = min(self.width, x + 4)
col_slice = combined_mask[:, col_start:col_end]
colored_rows = np.where(np.any(col_slice > 0, axis=1))[0]
if len(colored_rows) > 0:
top_y = colored_rows.min()
value = self._pixel_to_value(top_y)
sample_values.append(value)
if len(sample_values) < 3:
return "UNKNOWN"
self.debug_info["trend_samples"] = sample_values
# Calculate trend: compare first half vs second half averages
mid = len(sample_values) // 2
early_avg = np.mean(sample_values[:mid])
late_avg = np.mean(sample_values[mid:])
diff = late_avg - early_avg
# Determine direction based on difference
# Use 2% threshold for significance
if diff > 0.02:
return "RISING"
elif diff < -0.02:
return "FALLING"
else:
return "FLAT"
def _assess_confidence(self) -> str:
"""
Assess confidence level of the detection.
Returns:
'HIGH', 'MEDIUM', 'LOW', or 'FAILED'
"""
if self._current_value is None:
return "FAILED"
confidence_score = 0
# Check calibration quality
if self.calibration_success:
confidence_score += 2
# Extra point if both reference lines were detected (not estimated)
# Use cached result to avoid re-computation and side effects
upper_detected, lower_detected = self._ref_lines_detected
if upper_detected and lower_detected:
confidence_score += 1
# Check if value is in reasonable range
if 0.05 <= self._current_value <= 0.50:
confidence_score += 1
# Check pixel count
green_count = self.debug_info.get("green_pixel_count_right", 0)
red_count = self.debug_info.get("red_pixel_count_right", 0)
total_count = green_count + red_count
if total_count > 1000:
confidence_score += 2
elif total_count > 200:
confidence_score += 1
# Check color clarity
if green_count > 0 and red_count > 0:
ratio = max(green_count, red_count) / min(green_count, red_count)
if ratio > 5: # Clear color dominance
confidence_score += 1
# Map score to confidence level
if confidence_score >= 5:
return "HIGH"
elif confidence_score >= 3:
return "MEDIUM"
elif confidence_score >= 1:
return "LOW"
else:
return "FAILED"
def _interpret_value(self, value: float) -> str:
"""
Interpret the Uptrend Ratio value.
Args:
value: The detected value (0.0-1.0)
Returns:
Interpretation string
"""
if value >= 0.40:
return "overbought"
elif value >= 0.25:
return "neutral_bullish"
elif value >= 0.15:
return "neutral_bearish"
else:
return "oversold"
def detect(self, debug: bool = False, debug_output_dir: str = None) -> dict[str, Any]:
"""
Perform full detection on the Uptrend Ratio chart.
Args:
debug: If True, save debug images
debug_output_dir: Directory for debug output (defaults to same as input image)
Returns:
Dictionary with detection results
"""
self.debug_info["save_masks"] = debug
# Load image
try:
if not self._load_image():
return self._build_result()
except FileNotFoundError as e:
raise e
# Calibrate Y-axis
self._calibrate_y_axis()
# Only proceed with detection if calibration succeeded
# This prevents false positives when calibration fails (Issue #2 fix)
if self.calibration_success:
# Detect current value and color
self._current_value, self._current_color = self._detect_current_value_and_color()
# Detect trend direction
self._trend_direction = self._detect_trend_direction()
# Assess confidence (handles calibration failure internally)
self._confidence = self._assess_confidence()
# Save debug images if requested
if debug:
self._save_debug_images(debug_output_dir)
return self._build_result()
def _build_result(self) -> dict[str, Any]:
"""Build the result dictionary."""
# Use 'is not None' check to properly handle 0.0 as a valid value (Issue #3 fix)
result = {
"current_value": self._current_value,
"current_value_percent": f"{self._current_value * 100:.1f}%"
if self._current_value is not None
else None,
"current_color": self._current_color or "UNKNOWN",
"trend_direction": self._trend_direction or "UNKNOWN",
"confidence": self._confidence,
"interpretation": self._interpret_value(self._current_value)
if self._current_value is not None
else None,
"calibration": {
"upper_ref_y": self.upper_ref_y,
"lower_ref_y": self.lower_ref_y,
"y_scale": self.y_scale,
"calibration_success": self.calibration_success,
},
"image": {"path": self.image_path, "width": self.width, "height": self.height},
"debug_info": self.debug_info, # Include debug info for testing and troubleshooting
}
return result
def _save_debug_images(self, output_dir: str = None):
"""Save debug visualization images."""
if output_dir is None:
output_dir = os.path.dirname(self.image_path) or "."
base_name = Path(self.image_path).stem
# Create annotated image
debug_img = self.img_bgr.copy()
# Draw reference lines
if self.upper_ref_y:
cv2.line(
debug_img, (0, self.upper_ref_y), (self.width, self.upper_ref_y), (0, 165, 255), 2
) # Orange in BGR
cv2.putText(
debug_img,
f"{self.UPPER_REFERENCE:.0%} ref (y={self.upper_ref_y})",
(10, self.upper_ref_y - 10),
cv2.FONT_HERSHEY_SIMPLEX,
0.5,
(0, 165, 255),
1,
)
if self.lower_ref_y:
cv2.line(
debug_img, (0, self.lower_ref_y), (self.width, self.lower_ref_y), (0, 165, 255), 2
) # Orange in BGR
cv2.putText(
debug_img,
f"{self.LOWER_REFERENCE:.0%} ref (y={self.lower_ref_y})",
(10, self.lower_ref_y + 15),
cv2.FONT_HERSHEY_SIMPLEX,
0.5,
(0, 165, 255),
1,
)
# Mark detected value
if self._current_value is not None:
value_y = self._value_to_pixel(self._current_value)
color = (0, 255, 0) if self._current_color == "GREEN" else (0, 0, 255)
cv2.circle(debug_img, (self.width - 100, value_y), 15, color, 3)
cv2.putText(
debug_img,
f"{self._current_value * 100:.1f}% {self._current_color}",
(self.width - 250, value_y - 20),
cv2.FONT_HERSHEY_SIMPLEX,
0.7,
color,
2,
)
# Save debug image
debug_path = os.path.join(output_dir, f"{base_name}_debug_detection.jpeg")
cv2.imwrite(debug_path, debug_img)
print(f"Debug image saved to: {debug_path}")
# Save masks if available
if "green_mask" in self.debug_info:
mask_path = os.path.join(output_dir, f"{base_name}_debug_green_mask.png")
cv2.imwrite(mask_path, self.debug_info["green_mask"])
if "red_mask" in self.debug_info:
mask_path = os.path.join(output_dir, f"{base_name}_debug_red_mask.png")
cv2.imwrite(mask_path, self.debug_info["red_mask"])
def format_result_for_human(result: dict[str, Any]) -> str:
"""Format the detection result for human-readable output."""
lines = []
lines.append("=" * 60)
lines.append("Uptrend Ratio Detection Results (OpenCV)")
lines.append("=" * 60)
lines.append(f"\nImage: {result['image']['path']}")
lines.append(f"Confidence: {result['confidence']}")
lines.append("\n--- Detected Values ---")
if result["current_value"] is not None:
lines.append(
f"Current Value: {result['current_value_percent']} ({result['interpretation']})"
)
lines.append(f"Current Color: {result['current_color']}")
lines.append(f"Trend Direction: {result['trend_direction']}")
else:
lines.append("Current Value: NOT DETECTED")
lines.append("\n--- Calibration ---")
cal = result["calibration"]
lines.append(
f"Upper reference ({UptrendRatioDetector.UPPER_REFERENCE:.0%}) Y-pixel: {cal['upper_ref_y']}"
)
lines.append(
f"Lower reference ({UptrendRatioDetector.LOWER_REFERENCE:.0%}) Y-pixel: {cal['lower_ref_y']}"
)
lines.append(f"Calibration successful: {cal['calibration_success']}")
lines.append("=" * 60)
return "\n".join(lines)
def main():
parser = argparse.ArgumentParser(
description="Detect current value and trend from US Stock Market Uptrend Ratio charts.",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
python detect_uptrend_ratio.py charts/2026-01-05/uptrend_ratio.jpeg
python detect_uptrend_ratio.py charts/2026-01-05/uptrend_ratio.jpeg --debug
python detect_uptrend_ratio.py charts/2026-01-05/uptrend_ratio.jpeg --json
""",
)
parser.add_argument("image_path", help="Path to the Uptrend Ratio chart image")
parser.add_argument(
"--debug", action="store_true", help="Save debug images showing detection process"
)
parser.add_argument("--json", action="store_true", help="Output results as JSON only")
args = parser.parse_args()
if not os.path.isfile(args.image_path):
print(f"Error: File not found: {args.image_path}", file=sys.stderr)
sys.exit(1)
try:
detector = UptrendRatioDetector(args.image_path)
result = detector.detect(debug=args.debug)
if args.json:
print(json.dumps(result, indent=2))
else:
print(format_result_for_human(result))
# Exit with error code if detection failed
if result["confidence"] == "FAILED":
sys.exit(1)
except FileNotFoundError as e:
print(f"Error: {str(e)}", file=sys.stderr)
sys.exit(1)
except Exception as e:
print(f"Error: {str(e)}", file=sys.stderr)
if args.debug:
import traceback
traceback.print_exc()
sys.exit(1)
if __name__ == "__main__":
main()
# Tests for breadth-chart-analyst skill
"""
Pytest fixtures for breadth-chart-analyst tests.
"""
import sys
from pathlib import Path
import pytest
# Add scripts directory to path for imports
scripts_dir = Path(__file__).parent.parent / "scripts"
sys.path.insert(0, str(scripts_dir))
@pytest.fixture
def sample_uptrend_chart():
"""Path to the sample Uptrend Ratio chart in assets."""
chart_path = Path(__file__).parent.parent / "assets" / "US_Stock_Market_Uptrend_Ratio.jpeg"
if not chart_path.exists():
pytest.skip(f"Sample chart not found: {chart_path}")
return str(chart_path)
@pytest.fixture
def sample_breadth_chart():
"""Path to the sample S&P 500 Breadth Index chart in assets."""
chart_path = Path(__file__).parent.parent / "assets" / "SP500_Breadth_Index_200MA_8MA.jpeg"
if not chart_path.exists():
pytest.skip(f"Sample chart not found: {chart_path}")
return str(chart_path)
@pytest.fixture
def test_data_dir():
"""Directory for test data files."""
return Path(__file__).parent / "test_data" / "uptrend_ratio_samples"
@pytest.fixture
def nonexistent_image():
"""Path to a file that doesn't exist (for error handling tests)."""
return "/nonexistent/path/to/image.jpeg"
@pytest.fixture
def scripts_dir():
"""Path to the scripts directory."""
return Path(__file__).parent.parent / "scripts"
Related skills
FAQ
What charts does breadth-chart-analyst support?
breadth-chart-analyst structures reports around S&P 500 breadth charts, including Chart 1 (200-day MA Breadth Index) and an optional second chart. The analyst notes whether Chart 1 only, Chart 2 only, or both were analyzed.
What recommendations does breadth-chart-analyst output?
breadth-chart-analyst labels strategic outlook, tactical outlook, and an overall recommendation such as Aggressive Long, Moderate Long, Neutral, Defensive, or Short. Metric tables capture current breadth readings with interpretation columns.
Is Breadth Chart Analyst safe to install?
skills.sh reports 2 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.