Coil Scan — dual-timeframe %R Trend Exhaustion
Mirrors the "%R Trend Exhaustion [upslidedown]" TradingView indicator (dual timeframe 21 + 112). Finds stocks that are both deeply oversold AND just starting to reverse.
The indicator math
- %R(n) = Williams %R =
100 * (close - highestHigh_n) / (highestHigh_n - lowestLow_n), range −100…0. - Fast leg = EMA(7) of %R(21).
- Slow leg = EMA(3) of %R(112).
- Threshold (TH) = −80.
- In the coil: both fast AND slow ≤ −80. The short-term AND the structural picture are both deeply oversold. This is the stalk zone — a name can sit here for days/weeks before the trigger fires.
- Triggered out: fast crossed back above −80 while it was recently (within ~20 bars) coiled on both legs. The slow leg still lagging confirms the structural exhaustion hasn't reset — this is the bullish reversal signal.
Two output buckets
⚡ TRIGGERED OUT (≤3 days ago)
Fast just crossed back above −80. These are actionable reversal candidates.
A sanity check filters false-positives:
- If the close is already >5% above the price on the trigger date → tagged "already ran — not fresh" and downranked.
- If the close is now above EMA21 → same tag.
The canonical false-positive this catches: SLB crossed up but had already run +4% to just under its 21EMA by the time the scan ran — that's a chase, not an entry. It lands in the sanity-filtered section, not the fresh list.
🟢 IN THE COIL (stalk candidates)
Both legs ≤ −80, no trigger yet. Sorted deepest-first (most negative slow leg = most structurally exhausted).
The days-in-coil counter comes from the persistent state file
(state/coil_watch.json), so the scan can say "IREN — day 6 in the coil."
A name stays in the store until it triggers or exits the coil entirely.
Per-name display
SYM fast%R slow%R depth close day-in-coil vs SMA200 vs EMA21
IREN -92.3 -88.1 -8.1 $7.42 day 3 below SMA200 below EMA21
- depth:
min(fast, slow) − (−80). Negative = deeper in the hole. - above/below SMA200 + EMA21: from live technicals endpoint.
How to run
From the repo root or anywhere under it:
python .claude/skills/coil-scan/scripts/coil_scan.py
Options:
# smoke test against a short list
python coil_scan.py --universe IREN,RGTI,SMCI,SLB,FIG,GLD,ASTS
# limit universe size
python coil_scan.py --limit 50
# write JSON output
python coil_scan.py --json /tmp/coil_2026-07-19.json
# custom universe file (one ticker per line, or comma-separated)
python coil_scan.py --universe /tmp/my_watchlist.txt
Universe defaults to: all live TDPro screeners + stock-recap persistent watch
list (falls back to the most recent stock-recap/runs/*/raw/screener_*.json
if the live API is unavailable). Aim: ~150–200 names.
Persistence
state/coil_watch.json is keyed by ticker and tracks:
firstSeen— date first seen in the coildaysInCoil— rolling counter updated each runentryClose/lastClose— price anchorstriggered/triggerDate— when the cross fired
On each run: newly-coiled names are added, existing names increment their counter, triggered names are flagged, and names that left the coil without triggering are dropped.
Data sources
- Bars:
GET /api/agent/ticker/{SYMBOL}/chart-data?days=365(TraderDaddy Pro Railway API; Bearer token from.env_agent_api). Needs ≥115 bars for the slow %R(112) leg. - Live technicals:
GET /api/agent/ticker/{SYMBOL}(close, ema21, sma200, atr, rsi — used for sanity check + display). - Universe:
GET /api/screeners+GET /api/screeners/{id}/run× 10.
Interpreting results for Michael
- Empty IN THE COIL list: tape is not oversold broadly — that's fine, nothing to stalk beats a list of knives.
- Triggered + already ran: it moved without you. Note it for the next pullback but don't chase.
- Triggered fresh + below EMA21: highest-quality entry zone. Fast is turning, structure is still washed out, price hasn't reclaimed the mean yet.
- In coil day 10+: structural exhaustion is deep. The longer it coils, the more compressed the spring. Watch for the fast leg to tick back above −80.
The scan is a data layer — Michael confirms setups on Ghost Flow (the %R EXHAUST panel is visible there) before sizing.
Trigger A/B harness (coil_ab.py)
A sibling script runs two trigger rules side by side so we can settle whether tightening the trigger helps:
- CURRENT (
find_trigger): both legs were ≤ −80, then fast crosses > −80. Slow is not required to still be under −80 at the cross. - STRICT: fast crosses > −80 while slow is still ≤ −80 at that same bar ("fast breaks while slow still under"). A subset of CURRENT.
A 2026-07-21 backtest found STRICT is worse — the crosses it drops (slow already recovered = both legs lifting together = structural confirmation) outperformed at every horizon (3/5/10/20d). Fast-leads-while-slow-buried is the earlier but weaker (knife) signal. So the harness is a live forward test to confirm that before deciding.
python coil_ab.py # log today's fresh triggers under both rules
python coil_ab.py --report # re-price the ledger, print STRICT vs CURRENT-ONLY
Ledger: state/ab_ledger.json (deduped per symbol|trig_date). Logs daily via
weekday 22:00-UTC cron → state/ab_cron.log. Core coil_scan.py is untouched.