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
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.
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).
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.
pip install yfinance pandasYou 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.csvpython 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-breakoutTypical 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.
| 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 |
- Pre-specified hypothesis, not a confirmed edge. The scanner is neutral; use the outcome log to test the setup across regimes before trusting it.
relindex symbol. Defaults toXU100.IS. If that does not resolve on your data source, therelcolumns 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_volfloor are thin; raise--min-volfor cleaner fills.
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)