Skip to content

Screener Functions

RSI Divergence Screening

rsi_divergence

RSI Divergence screener for NASDAQ stocks.

Screens stocks for RSI divergences (bullish and bearish) with configurable detection parameters, breakout filtering, and flexible pivot detection methods.

RSIDivergenceResult dataclass

Result from RSI divergence screening.

Attributes:

Name Type Description
ticker str

Stock symbol.

company_name str

Company name from NASDAQ listing.

close_price float

Most recent close price.

rsi float

Current RSI value.

divergence_type str

Type detected ('bullish', 'bearish', or 'both').

bullish_divergence bool

True if bullish divergence detected.

bearish_divergence bool

True if bearish divergence detected.

details str

Human-readable description of divergence.

bullish_indices Optional[tuple]

Tuple of (p1_idx, p2_idx, r1_idx, r2_idx) for bullish.

bearish_indices Optional[tuple]

Tuple of (p1_idx, p2_idx, r1_idx, r2_idx) for bearish.

Source code in src/stockcharts/screener/rsi_divergence.py
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
@dataclass
class RSIDivergenceResult:
    """Result from RSI divergence screening.

    Attributes:
        ticker: Stock symbol.
        company_name: Company name from NASDAQ listing.
        close_price: Most recent close price.
        rsi: Current RSI value.
        divergence_type: Type detected ('bullish', 'bearish', or 'both').
        bullish_divergence: True if bullish divergence detected.
        bearish_divergence: True if bearish divergence detected.
        details: Human-readable description of divergence.
        bullish_indices: Tuple of (p1_idx, p2_idx, r1_idx, r2_idx) for bullish.
        bearish_indices: Tuple of (p1_idx, p2_idx, r1_idx, r2_idx) for bearish.
    """

    ticker: str
    company_name: str
    close_price: float
    rsi: float
    divergence_type: str  # 'bullish', 'bearish', or 'both'
    bullish_divergence: bool
    bearish_divergence: bool
    details: str
    bullish_indices: Optional[tuple] = None  # (p1_idx, p2_idx, r1_idx, r2_idx)
    bearish_indices: Optional[tuple] = None  # (p1_idx, p2_idx, r1_idx, r2_idx)

screen_rsi_divergence(tickers=None, period='3mo', interval='1d', rsi_period=14, divergence_type='all', min_price=None, max_price=None, min_volume=None, swing_window=5, lookback=60, start=None, end=None, exclude_breakouts=False, breakout_threshold=0.05, exclude_failed_breakouts=False, failed_lookback_window=10, failed_attempt_threshold=0.03, failed_reversal_threshold=0.01, min_swing_points=2, index_proximity_factor=2, sequence_tolerance_pct=0.002, rsi_sequence_tolerance=0.0, pivot_method='swing', zigzag_pct=0.03, zigzag_atr_mult=2.0, zigzag_atr_period=14, ema_price_span=5, ema_rsi_span=5, use_sequence_scoring=False, min_sequence_score=1.0, max_bar_gap=10, min_magnitude_atr_mult=0.5, atr_period=14, batch_size=50, verbose=True)

Screen stocks for RSI divergences.

Parameters:

Name Type Description Default
tickers list[str] | None

List of ticker symbols (if None, uses all NASDAQ)

None
period str

Historical period breadth for yfinance (e.g. '1mo', '3mo', '6mo', '1y') ignored if both start & end provided.

'3mo'
interval str

Candle aggregation interval ('1d','1wk','1mo').

'1d'
start Optional[str]

Start date for historical data (YYYY-MM-DD format).

None
end Optional[str]

End date for historical data (YYYY-MM-DD format).

None
rsi_period int

RSI calculation period (default: 14)

14
divergence_type str

Type to screen for ('bullish', 'bearish', or 'all')

'all'
min_price Optional[float]

Minimum stock price filter

None
max_price Optional[float]

Maximum stock price filter

None
min_volume Optional[float]

Minimum average daily volume filter

None
swing_window int

Window for swing point detection (default: 5)

5
lookback int

Bars to look back for divergence (default: 60)

60
exclude_breakouts bool

If True, filter out divergences where breakout already occurred

False
breakout_threshold float

% move required to consider breakout complete (default: 0.05 = 5%)

0.05
exclude_failed_breakouts bool

If True, filter out divergences with failed breakout attempts

False
failed_lookback_window int

Bars to check for failed breakout (default: 10)

10
failed_attempt_threshold float

% move to consider breakout "attempted" (default: 0.03 = 3%)

0.03
failed_reversal_threshold float

% from divergence to consider "failed" (default: 0.01 = 1%)

0.01
min_swing_points int

Minimum swing points required (2 or 3, default: 2)

2
index_proximity_factor int

Multiplier for swing_window to allow bar index gap (default: 2)

2
sequence_tolerance_pct float

Relative tolerance for 3-point price sequences (default: 0.002 = 0.2%)

0.002
rsi_sequence_tolerance float

Extra RSI tolerance in points for 3-point sequences (default: 0.0)

0.0
pivot_method str

Pivot detection method - 'swing' or 'ema-deriv' (default: 'swing')

'swing'
zigzag_pct float

Percentage threshold for zigzag-pct method (default: 0.03 = 3%) [DEPRECATED]

0.03
zigzag_atr_mult float

ATR multiplier for zigzag-atr method (default: 2.0) [DEPRECATED]

2.0
zigzag_atr_period int

ATR period for zigzag-atr method (default: 14) [DEPRECATED]

14
ema_price_span int

EMA smoothing span for price when using ema-deriv (default: 5)

5
ema_rsi_span int

EMA smoothing span for RSI when using ema-deriv (default: 5)

5
use_sequence_scoring bool

Enable ATR-normalized 3-point scoring (default: False)

False
min_sequence_score float

Minimum score to accept a 3-point sequence (default: 1.0)

1.0
max_bar_gap int

Max bar distance between price and RSI pivots for scoring (default: 10)

10
min_magnitude_atr_mult float

Min price move as ATR multiple for scoring (default: 0.5)

0.5
atr_period int

ATR period for magnitude filtering (default: 14)

14
batch_size int | None

Number of tickers to download per batch (default: 50). Uses yfinance's built-in threading for parallel downloads. Set to None for legacy sequential download mode.

50
verbose bool

Print progress messages (default: True)

True

Returns:

Type Description
list[RSIDivergenceResult]

List of RSIDivergenceResult objects

Source code in src/stockcharts/screener/rsi_divergence.py
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
def screen_rsi_divergence(
    tickers: list[str] | None = None,
    period: str = "3mo",  # Historical lookback e.g. '3mo', '6mo', '1y'
    interval: str = "1d",  # Candle interval '1d','1wk','1mo'
    rsi_period: int = 14,
    divergence_type: str = "all",  # 'bullish', 'bearish', or 'all'
    min_price: Optional[float] = None,
    max_price: Optional[float] = None,
    min_volume: Optional[float] = None,
    swing_window: int = 5,
    lookback: int = 60,
    start: Optional[str] = None,
    end: Optional[str] = None,
    exclude_breakouts: bool = False,
    breakout_threshold: float = 0.05,
    exclude_failed_breakouts: bool = False,
    failed_lookback_window: int = 10,
    failed_attempt_threshold: float = 0.03,
    failed_reversal_threshold: float = 0.01,
    min_swing_points: int = 2,
    index_proximity_factor: int = 2,
    sequence_tolerance_pct: float = 0.002,
    rsi_sequence_tolerance: float = 0.0,
    pivot_method: str = "swing",
    zigzag_pct: float = 0.03,
    zigzag_atr_mult: float = 2.0,
    zigzag_atr_period: int = 14,
    ema_price_span: int = 5,
    ema_rsi_span: int = 5,
    use_sequence_scoring: bool = False,
    min_sequence_score: float = 1.0,
    max_bar_gap: int = 10,
    min_magnitude_atr_mult: float = 0.5,
    atr_period: int = 14,
    batch_size: int | None = 50,
    verbose: bool = True,
) -> list[RSIDivergenceResult]:
    """Screen stocks for RSI divergences.

    Args:
        tickers: List of ticker symbols (if None, uses all NASDAQ)
        period: Historical period breadth for yfinance (e.g. '1mo', '3mo', '6mo', '1y') ignored if both start & end provided.
        interval: Candle aggregation interval ('1d','1wk','1mo').
        start: Start date for historical data (YYYY-MM-DD format).
        end: End date for historical data (YYYY-MM-DD format).
        rsi_period: RSI calculation period (default: 14)
        divergence_type: Type to screen for ('bullish', 'bearish', or 'all')
        min_price: Minimum stock price filter
        max_price: Maximum stock price filter
        min_volume: Minimum average daily volume filter
        swing_window: Window for swing point detection (default: 5)
        lookback: Bars to look back for divergence (default: 60)
        exclude_breakouts: If True, filter out divergences where breakout already occurred
        breakout_threshold: % move required to consider breakout complete (default: 0.05 = 5%)
        exclude_failed_breakouts: If True, filter out divergences with failed breakout attempts
        failed_lookback_window: Bars to check for failed breakout (default: 10)
        failed_attempt_threshold: % move to consider breakout "attempted" (default: 0.03 = 3%)
        failed_reversal_threshold: % from divergence to consider "failed" (default: 0.01 = 1%)
        min_swing_points: Minimum swing points required (2 or 3, default: 2)
        index_proximity_factor: Multiplier for swing_window to allow bar index gap (default: 2)
        sequence_tolerance_pct: Relative tolerance for 3-point price sequences (default: 0.002 = 0.2%)
        rsi_sequence_tolerance: Extra RSI tolerance in points for 3-point sequences (default: 0.0)
        pivot_method: Pivot detection method - 'swing' or 'ema-deriv' (default: 'swing')
        zigzag_pct: Percentage threshold for zigzag-pct method (default: 0.03 = 3%) [DEPRECATED]
        zigzag_atr_mult: ATR multiplier for zigzag-atr method (default: 2.0) [DEPRECATED]
        zigzag_atr_period: ATR period for zigzag-atr method (default: 14) [DEPRECATED]
        ema_price_span: EMA smoothing span for price when using ema-deriv (default: 5)
        ema_rsi_span: EMA smoothing span for RSI when using ema-deriv (default: 5)
        use_sequence_scoring: Enable ATR-normalized 3-point scoring (default: False)
        min_sequence_score: Minimum score to accept a 3-point sequence (default: 1.0)
        max_bar_gap: Max bar distance between price and RSI pivots for scoring (default: 10)
        min_magnitude_atr_mult: Min price move as ATR multiple for scoring (default: 0.5)
        atr_period: ATR period for magnitude filtering (default: 14)
        batch_size: Number of tickers to download per batch (default: 50).
            Uses yfinance's built-in threading for parallel downloads.
            Set to None for legacy sequential download mode.
        verbose: Print progress messages (default: True)

    Returns:
        List of RSIDivergenceResult objects
    """
    if tickers is None:
        if verbose:
            print("Fetching NASDAQ ticker list...")
        tickers = get_nasdaq_tickers()
        if verbose:
            print(f"Found {len(tickers)} tickers to screen")

    # Normalize tickers to list of strings
    ticker_list = []
    ticker_names = {}
    for ticker_info in tickers:
        if isinstance(ticker_info, tuple):
            ticker, company_name = ticker_info
            ticker_list.append(ticker)
            ticker_names[ticker] = company_name
        else:
            ticker = str(ticker_info)
            ticker_list.append(ticker)
            ticker_names[ticker] = ticker

    # Build divergence detection kwargs
    divergence_kwargs = {
        "price_col": "Close",
        "rsi_col": "RSI",
        "window": swing_window,
        "lookback": lookback,
        "min_swing_points": min_swing_points,
        "index_proximity_factor": index_proximity_factor,
        "sequence_tolerance_pct": sequence_tolerance_pct,
        "rsi_sequence_tolerance": rsi_sequence_tolerance,
        "pivot_method": pivot_method,
        "zigzag_pct": zigzag_pct,
        "zigzag_atr_mult": zigzag_atr_mult,
        "zigzag_atr_period": zigzag_atr_period,
        "ema_price_span": ema_price_span,
        "ema_rsi_span": ema_rsi_span,
        "use_sequence_scoring": use_sequence_scoring,
        "min_sequence_score": min_sequence_score,
        "max_bar_gap": max_bar_gap,
        "min_magnitude_atr_mult": min_magnitude_atr_mult,
        "atr_period": atr_period,
    }

    # Use batch or sequential mode
    if batch_size is not None and batch_size > 0:
        results = _screen_batch_mode(
            ticker_list=ticker_list,
            ticker_names=ticker_names,
            interval=interval,
            period=period,
            start=start,
            end=end,
            rsi_period=rsi_period,
            divergence_type=divergence_type,
            min_price=min_price,
            max_price=max_price,
            min_volume=min_volume,
            exclude_breakouts=exclude_breakouts,
            breakout_threshold=breakout_threshold,
            exclude_failed_breakouts=exclude_failed_breakouts,
            failed_lookback_window=failed_lookback_window,
            failed_attempt_threshold=failed_attempt_threshold,
            failed_reversal_threshold=failed_reversal_threshold,
            divergence_kwargs=divergence_kwargs,
            batch_size=batch_size,
            verbose=verbose,
        )
    else:
        results = _screen_sequential_mode(
            ticker_list=ticker_list,
            ticker_names=ticker_names,
            interval=interval,
            period=period,
            start=start,
            end=end,
            rsi_period=rsi_period,
            divergence_type=divergence_type,
            min_price=min_price,
            max_price=max_price,
            min_volume=min_volume,
            exclude_breakouts=exclude_breakouts,
            breakout_threshold=breakout_threshold,
            exclude_failed_breakouts=exclude_failed_breakouts,
            failed_lookback_window=failed_lookback_window,
            failed_attempt_threshold=failed_attempt_threshold,
            failed_reversal_threshold=failed_reversal_threshold,
            divergence_kwargs=divergence_kwargs,
            verbose=verbose,
        )

    if verbose:
        print(f"\nScreening complete. Found {len(results)} stocks with divergences.")
    return results

save_results_to_csv(results, filename='rsi_divergence_results.csv')

Save screening results to CSV file.

Parameters:

Name Type Description Default
results list[RSIDivergenceResult]

List of RSIDivergenceResult objects to save.

required
filename str

Output CSV file path.

'rsi_divergence_results.csv'

Returns:

Type Description
None

None. Prints status message and writes file to disk.

Source code in src/stockcharts/screener/rsi_divergence.py
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
def save_results_to_csv(
    results: list[RSIDivergenceResult], filename: str = "rsi_divergence_results.csv"
) -> None:
    """Save screening results to CSV file.

    Args:
        results: List of RSIDivergenceResult objects to save.
        filename: Output CSV file path.

    Returns:
        None. Prints status message and writes file to disk.
    """
    if not results:
        print("No results to save.")
        return

    import json

    def serialize_indices(indices: list | tuple | None) -> list[str] | None:
        """Convert timestamp indices to ISO format strings for JSON serialization.

        Args:
            indices: Tuple of timestamp indices or None.

        Returns:
            List of ISO format date strings or None.
        """
        if indices is None:
            return None
        return [idx.isoformat() for idx in indices]

    df = pd.DataFrame(
        [
            {
                "Ticker": r.ticker,
                "Company": r.company_name,
                "Price": r.close_price,
                "RSI": r.rsi,
                "Divergence Type": r.divergence_type,
                "Bullish": r.bullish_divergence,
                "Bearish": r.bearish_divergence,
                "Details": r.details,
                "Bullish_Indices": (
                    json.dumps(serialize_indices(r.bullish_indices)) if r.bullish_indices else ""
                ),
                "Bearish_Indices": (
                    json.dumps(serialize_indices(r.bearish_indices)) if r.bearish_indices else ""
                ),
            }
            for r in results
        ]
    )

    df.to_csv(filename, index=False)
    print(f"Results saved to {filename}")

Heiken Ashi Screening

screener

NASDAQ Heiken Ashi screener module.

Screens NASDAQ stocks for bullish (green) or bearish (red) Heiken Ashi patterns.

ScreenResult dataclass

Result for a single ticker Heiken Ashi screening.

Attributes:

Name Type Description
ticker str

Stock symbol.

color Literal['green', 'red']

Current candle color ('green' = bullish, 'red' = bearish).

previous_color Literal['green', 'red']

Previous candle color.

color_changed bool

True if color changed from previous candle.

ha_open float

Heiken Ashi open price.

ha_close float

Heiken Ashi close price.

last_date str

Date of the most recent candle.

interval str

Time interval used ('1d', '1wk', '1mo').

avg_volume float

Average trading volume over recent period.

run_length int

Consecutive candles of same color.

run_percentile float

Percentile rank of run length (0-100).

Source code in src/stockcharts/screener/screener.py
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
@dataclass
class ScreenResult:
    """Result for a single ticker Heiken Ashi screening.

    Attributes:
        ticker: Stock symbol.
        color: Current candle color ('green' = bullish, 'red' = bearish).
        previous_color: Previous candle color.
        color_changed: True if color changed from previous candle.
        ha_open: Heiken Ashi open price.
        ha_close: Heiken Ashi close price.
        last_date: Date of the most recent candle.
        interval: Time interval used ('1d', '1wk', '1mo').
        avg_volume: Average trading volume over recent period.
        run_length: Consecutive candles of same color.
        run_percentile: Percentile rank of run length (0-100).
    """

    ticker: str
    color: Literal["green", "red"]
    previous_color: Literal["green", "red"]
    color_changed: bool
    ha_open: float
    ha_close: float
    last_date: str
    interval: str
    avg_volume: float
    run_length: int
    run_percentile: float

get_candle_color(ha_df, index=-1)

Determine if a Heiken Ashi candle is green or red.

Parameters:

Name Type Description Default
ha_df DataFrame

Heiken Ashi DataFrame with columns HA_Open, HA_Close.

required
index int

Index of the candle to check (-1 for most recent, -2 for previous).

-1

Returns:

Type Description
Literal['green', 'red']

'green' if HA_Close >= HA_Open (bullish), 'red' otherwise (bearish).

Source code in src/stockcharts/screener/screener.py
51
52
53
54
55
56
57
58
59
60
61
62
def get_candle_color(ha_df: pd.DataFrame, index: int = -1) -> Literal["green", "red"]:
    """Determine if a Heiken Ashi candle is green or red.

    Args:
        ha_df: Heiken Ashi DataFrame with columns HA_Open, HA_Close.
        index: Index of the candle to check (-1 for most recent, -2 for previous).

    Returns:
        'green' if HA_Close >= HA_Open (bullish), 'red' otherwise (bearish).
    """
    row = ha_df.iloc[index]
    return "green" if row["HA_Close"] >= row["HA_Open"] else "red"

screen_ticker(ticker, period='1d', lookback=None, start=None, end=None, debug=False)

Screen a single ticker for its latest Heiken Ashi candle color.

Parameters:

Name Type Description Default
ticker str

Stock symbol.

required
period str

Aggregation period ('1d', '1wk', '1mo').

'1d'
lookback str | None

How far back to fetch ('1mo', '3mo', '6mo', '1y', '2y', '5y', etc.).

None
start str | None

Start date YYYY-MM-DD.

None
end str | None

End date YYYY-MM-DD.

None
debug bool

When True, prints detailed error information during screening.

False

Returns:

Type Description
ScreenResult | None

ScreenResult or None if data unavailable or error occurs.

Source code in src/stockcharts/screener/screener.py
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
def screen_ticker(
    ticker: str,
    period: str = "1d",
    lookback: str | None = None,
    start: str | None = None,
    end: str | None = None,
    debug: bool = False,
) -> ScreenResult | None:
    """Screen a single ticker for its latest Heiken Ashi candle color.

    Args:
        ticker: Stock symbol.
        period: Aggregation period ('1d', '1wk', '1mo').
        lookback: How far back to fetch ('1mo', '3mo', '6mo', '1y', '2y', '5y', etc.).
        start: Start date YYYY-MM-DD.
        end: End date YYYY-MM-DD.
        debug: When True, prints detailed error information during screening.

    Returns:
        ScreenResult or None if data unavailable or error occurs.
    """
    try:
        # Fetch recent data
        df = fetch_ohlc(ticker, interval=period, lookback=lookback, start=start, end=end)
        return _process_ticker_dataframe(ticker, df, period, debug=debug)
    except Exception as e:
        if debug:
            print(f"  DEBUG: Error screening {ticker}: {type(e).__name__}: {e}")
        return None

screen_nasdaq(color_filter='all', period='1d', limit=None, delay=0.5, verbose=True, changed_only=False, lookback=None, start=None, end=None, debug=False, min_volume=None, min_price=None, min_run_percentile=None, max_run_percentile=None, ticker_filter=None, batch_size=50)

Screen NASDAQ stocks for Heiken Ashi candle colors.

Parameters:

Name Type Description Default
color_filter Literal['green', 'red', 'all']

Filter results by current candle color ('green', 'red', 'all').

'all'
period str

Aggregation period ('1d', '1wk', '1mo').

'1d'
limit int | None

Maximum number of tickers to screen (for testing). None = all.

None
delay float

Delay in seconds between API calls. Only used when batch_size=None (sequential mode). Ignored when using batch downloads.

0.5
verbose bool

Print progress messages.

True
changed_only bool

If True, only return tickers where the candle color just changed.

False
lookback str | None

How far back to fetch ('1mo', '3mo', '6mo', '1y', '2y', '5y', etc.).

None
start str | None

Start date YYYY-MM-DD. Cannot be used with lookback.

None
end str | None

End date YYYY-MM-DD. Cannot be used with lookback.

None
debug bool

When True, enables debug output in underlying ticker screening.

False
min_volume float | None

Minimum average daily volume (in shares).

None
min_price float | None

Minimum stock price (in dollars).

None
min_run_percentile float | None

Minimum run percentile (0-100). Find rare long runs.

None
max_run_percentile float | None

Maximum run percentile (0-100). Find common short runs.

None
ticker_filter list[str] | None

Optional list of ticker symbols to screen instead of all NASDAQ.

None
batch_size int | None

Tickers per batch for parallel download (default: 50). Set to None for legacy sequential mode with delay.

50

Returns:

Type Description
list[ScreenResult]

List of ScreenResult objects matching filters, sorted by ticker.

Source code in src/stockcharts/screener/screener.py
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
def screen_nasdaq(
    color_filter: Literal["green", "red", "all"] = "all",
    period: str = "1d",
    limit: int | None = None,
    delay: float = 0.5,
    verbose: bool = True,
    changed_only: bool = False,
    lookback: str | None = None,
    start: str | None = None,
    end: str | None = None,
    debug: bool = False,
    min_volume: float | None = None,
    min_price: float | None = None,
    min_run_percentile: float | None = None,
    max_run_percentile: float | None = None,
    ticker_filter: list[str] | None = None,
    batch_size: int | None = 50,
) -> list[ScreenResult]:
    """Screen NASDAQ stocks for Heiken Ashi candle colors.

    Args:
        color_filter: Filter results by current candle color ('green', 'red', 'all').
        period: Aggregation period ('1d', '1wk', '1mo').
        limit: Maximum number of tickers to screen (for testing). None = all.
        delay: Delay in seconds between API calls. Only used when batch_size=None
            (sequential mode). Ignored when using batch downloads.
        verbose: Print progress messages.
        changed_only: If True, only return tickers where the candle color just changed.
        lookback: How far back to fetch ('1mo', '3mo', '6mo', '1y', '2y', '5y', etc.).
        start: Start date YYYY-MM-DD. Cannot be used with lookback.
        end: End date YYYY-MM-DD. Cannot be used with lookback.
        debug: When True, enables debug output in underlying ticker screening.
        min_volume: Minimum average daily volume (in shares).
        min_price: Minimum stock price (in dollars).
        min_run_percentile: Minimum run percentile (0-100). Find rare long runs.
        max_run_percentile: Maximum run percentile (0-100). Find common short runs.
        ticker_filter: Optional list of ticker symbols to screen instead of all NASDAQ.
        batch_size: Tickers per batch for parallel download (default: 50).
            Set to None for legacy sequential mode with delay.

    Returns:
        List of ScreenResult objects matching filters, sorted by ticker.
    """
    # Use provided ticker filter or fetch all NASDAQ tickers
    if ticker_filter is not None:
        tickers = ticker_filter
        if limit is not None:
            tickers = tickers[:limit]
    else:
        tickers = get_nasdaq_tickers(limit=limit)

    results: list[ScreenResult] = []

    change_msg = " that just changed color" if changed_only else ""
    filter_msg = " (filtered list)" if ticker_filter is not None else ""
    mode_msg = f"batch size {batch_size}" if batch_size else "sequential"

    if verbose:
        print(
            f"Screening {len(tickers)} tickers{filter_msg} for {color_filter} "
            f"Heiken Ashi candles{change_msg} ({period} period, {mode_msg})..."
        )
        print("-" * 70)

    # Use batch download mode if batch_size is specified
    if batch_size is not None and batch_size > 0:
        results = _screen_batch_mode(
            tickers=tickers,
            period=period,
            lookback=lookback,
            start=start,
            end=end,
            batch_size=batch_size,
            color_filter=color_filter,
            changed_only=changed_only,
            min_volume=min_volume,
            min_price=min_price,
            min_run_percentile=min_run_percentile,
            max_run_percentile=max_run_percentile,
            verbose=verbose,
            debug=debug,
        )
    else:
        # Legacy sequential mode
        results = _screen_sequential_mode(
            tickers=tickers,
            period=period,
            lookback=lookback,
            start=start,
            end=end,
            delay=delay,
            color_filter=color_filter,
            changed_only=changed_only,
            min_volume=min_volume,
            min_price=min_price,
            min_run_percentile=min_run_percentile,
            max_run_percentile=max_run_percentile,
            verbose=verbose,
            debug=debug,
        )

    if verbose:
        print("-" * 70)
        print(f"Screening complete: {len(results)} {color_filter} candles{change_msg} found")

    return sorted(results, key=lambda x: x.ticker)