Skip to content

bkadirbeyoglu/bist-pullback-scanner

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

BIST Pullback Scanner

Scans Borsa İstanbul (BIST) index constituents for a trend-pullback setup and accumulates the matches into a two-file signal/outcome log so you can study what happened after past signals.

The scanner surfaces raw data only — no buy/sell language, no edge labels, no tier ratings. All judgement happens downstream, in analysis. Matches are pre-specified hypotheses, not confirmed edges.

🇹🇷 Türkçe: see README.tr.md


The setup

All four conditions are evaluated on the latest completed daily bar:

# Condition Meaning
1 Close > SMA(50) long-term uptrend intact
2 |Close − EMA(20)| / EMA(20) ≤ band% (default 2.5) price pulling back near the 20 EMA
3 40 ≤ RSI(14) ≤ 50 momentum cooled, not yet weak
4 AvgVolume(N) > min_vol (default N=30, 1,000,000) liquidity floor

RSI and ATR use Wilder smoothing. Condition 2 is a proximity band on either side of EMA20: empirically, an uptrend pullback that cools RSI into 40–50 lands the close just below EMA20, so a strict Close ≥ EMA20 test is near-empty on a given day.

The discretionary "Phase-4" trigger — today's candle breaking above yesterday's high — is surfaced as the boolean column broke_prev_high. It is not a filter by default; pass --require-breakout to filter on it.

Output columns

ticker, yf_symbol, date, close, sma50, ema20, rsi14, avg_vol_N, sma50_gap_pct, ema20_gap_pct, atr14, atr_pct, ema20_gap_atr, prev_high, high, low, broke_prev_high

ema20_gap_atr is the ATR-normalized distance from EMA20 (volatility-adjusted pullback depth).

Two-file tracking

Each index keeps its own pair of files (e.g. pullback_signals_xu100.csv, pullback_signals_xu500.csv), selected automatically by -i/--index.

File Role
pullback_signals_{index}.csv Logged matches, keyed on (signal_date, ticker). A snapshot of the geometry at signal time, plus the config that produced it (ema20_band, vol_window). Last write wins: re-logging the same date+ticker overwrites the prior row, so an EOD run cleanly supersedes any intraday one.
pullback_outcomes_{index}.csv Derived each run from the signal log + price series: d1..dN raw return (dN_ret) and market-relative return (dN_rel = stock − XU100), plus a status column (open / partial / complete / no_data / date_not_found).

Returns are computed from the current price series for both the reference and each dN (split-consistent), referenced to the signal-day (d0) close. The outcomes file is fully regenerated each run, so it is safe to delete and rebuild; the signal log is the source of truth.

Logging is ON by default. Use --no-log for intraday test runs. Because each match is logged with its signal_date (the bar date, not the run date), only run on the settled EOD bar — yfinance daily bars for BIST finalize ~3–3.5h after the 18:00 TRT close (≈21:30–21:45 TRT). Intraday runs reflect an unsettled candle and churn heavily.

Install

pip install yfinance pandas

You also need the constituent files xu100.csv / xu500.csv. Generate them with the bundled helper:

python update_index.py -i xu100   # -> xu100.csv
python update_index.py -i xu500   # -> xu500.csv

Usage

python bist_pullback_scanner.py                 # XU100, band ±2.5%, scans AND logs
python bist_pullback_scanner.py -i xu500        # XU500
python bist_pullback_scanner.py --no-log        # scan only (test run, no logging)
python bist_pullback_scanner.py --diagnose      # condition funnel, no logging
python bist_pullback_scanner.py --resolve-only  # backfill d1–d5 outcomes, no scan
python bist_pullback_scanner.py --ema20-band 3 --require-breakout

Typical daily flow: run once after the bar settles (≈21:45 TRT). It prints the match table, upserts pullback_signals_<index>.csv, and refreshes pullback_outcomes_<index>.csv.

Options

Flag Default Purpose
-i, --index xu100 universe (xu100 / xu500)
--ema20-band 2.5 EMA20 proximity band, % either side
--max-ema20-gap off extra upper cap on % above EMA20
--rsi-low / --rsi-high 40 / 50 RSI band
--vol-window 30 average-volume window (days)
--min-vol 1,000,000 minimum average volume (shares)
--require-breakout off keep only high > prev_high rows
--diagnose off print condition funnel and exit
--period 6mo yfinance history window
--chunk / --pause 50 / 1.0 download batch size / batch pause (s)
--retries 2 retry passes for tickers that return no data (transient yfinance failures)
--no-log off disable logging for this run
--resolve-only off recompute outcomes only, no scan
--horizon 5 forward trading days tracked
--index-symbol XU100.IS index level for rel returns
--signals-file / --outcomes-file pullback_signals_{index}.csv / pullback_outcomes_{index}.csv per-index log paths

Notes & caveats

  • Pre-specified hypothesis, not a confirmed edge. The scanner is neutral; use the outcome log to test the setup across regimes before trusting it.
  • rel index symbol. Defaults to XU100.IS. If that does not resolve on your data source, the rel columns stay blank (with a warning) — override with --index-symbol.
  • Entry reference. Returns are measured from the d0 close. Real-world entry on the d1 open will differ.
  • Liquidity. Names sitting just above the min_vol floor are thin; raise --min-vol for cleaner fills.

Files

bist_pullback_scanner.py   the scanner
update_index.py            constituent-list generator (KAP, Midas fallback)
xu100.csv / xu500.csv      index constituents (ticker, yf_symbol)
pullback_signals_{index}.csv    generated: append/upsert signal log (one per index)
pullback_outcomes_{index}.csv   generated: derived d1–dN outcomes (one per index)

About

Raw-data scanner for trend-pullback setups on Borsa İstanbul (BIST), with two-file signal/outcome tracking for forward-return analysis.

Topics

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages