# Intraday Watchlist-Trigger Reconfirmation — invoked automatically by
# check_positions.sh, once per newly-crossed trigger from
# check_watchlist_triggers.py. Not run by hand; not a separate cron entry.

---

**Why this exists (added 2026-08-28):** `check_watchlist_triggers.py` has always
been deliberately mechanical — it flags that ONE numeric level crossed and
explicitly does NOT re-confirm the setup, deferring the real look to
tomorrow's Step 1c. That gap let KXS run from its $157.47 trigger to $178.76
(+13.5%) over a single session with nothing but a passive alert firing. The
gap wasn't that data was unavailable — Questrade's own `get_quotes` and
`get_historical_data` had the real numbers the whole time — it's that nobody
asked Questrade for them until asked to. This flow closes that gap: it uses
Questrade as the authoritative data source (no web search, no waiting on a
flaky screener page) to actually re-score the ticker the moment its trigger
crosses, same-day, and executes if it genuinely qualifies.

**This does NOT replace tomorrow's Step 1c re-evaluation.** That still runs
in full, multi-source, regardless of what happens here — this is a same-day
capture check, not a substitute for the real daily re-confirmation.

You are given one `TICKER` (the bare symbol, e.g. `KXS`) whose
`trigger_price_cad` was just crossed. Read the most recent
`/media/raid/rshare/SwingTrader/daily_snapshots/swing_trade_*.json` and find
`TICKER`'s entry in `signals.watchlist` for its `waiting_for` text,
`questrade_symbol`, and `trigger_price_cad`/`trigger_direction`.

**Step 0 — Tool-availability check (same pattern as `daily_scan_prompt.md`
Step 6 point 0).** If `mcp__Questrade__list_accounts` (or any other
`mcp__Questrade__*` tool) is not available in your toolset, you are still the
real unattended invocation — `check_positions.sh` allow-lists these tools for
exactly this call. Do not conclude "this must be interactive" and stop
silently. Instead call `ntfy_alert.send_alert()` with `priority='urgent'`
naming `TICKER` and stating the Questrade connector was unavailable for this
intraday reconfirm check, then stop. Do not attempt anything below.

**Step 1 — Pyramid check.** If `TICKER` is already in `positions.json`'s
`open_positions`, stop — this is an ADD/pyramid case, out of scope here same
as `daily_scan_prompt.md` Step 6 point 1 and `place_trade_prompt.md` Step 2.

**Step 2 — Resolve the instrument.** Use `questrade_symbol` from the
watchlist entry if present. If null, call `search_symbols` (ticker, then
company name if noisy), filter to `listingMarket == "TSX"`. Zero or 2+
matches → alert (`priority='high'`) that the ticker crossed its trigger but
the Questrade symbol couldn't be resolved for reconfirmation, and stop —
never guess a suffix.

**Step 3 — Pull live price.** Call `get_quotes` for the resolved symbol. This
is the same-day confirmed price — use it, don't wait for a web search to
find one.

**Step 4 — Pull enough history to actually score the setup.**
Call `get_historical_data` with `granularity: "1d"`, `to` = today. It caps at
roughly 40 bars per call and truncates silently (`candlesTruncated: true`) —
if you need more (200d EMA wants ~200+ bars for a trustworthy read; RSI/MACD/
ATR/volume-ratio need far fewer), make repeated calls, each time setting `to`
to one day before the earliest `start` date you got back, and concatenate
oldest-first. Don't over-fetch: 60-80 bars is enough for everything except a
genuinely reliable 200d EMA — if the ticker's `waiting_for` condition doesn't
hinge on the 200 EMA specifically (it was already confirmed once when the
ticker was first sourced), 60-80 bars is fine.

Append today's still-forming bar using the live quote from Step 3:
`{"date": today, "open": openPrice, "high": highPrice, "low": lowPrice,
"close": lastPrice, "volume": volume}` (all fields come straight off the
`get_quotes` response).

**Step 5 — Compute indicators.** Pipe the combined bar list (oldest-first
JSON array) into `python3 /media/raid/rshare/SwingTrader/technical_indicators.py`.
This gives deterministic `rsi14`, `macd_histogram` (with `macd_error` set if
there wasn't enough history to trust it — treat a non-null `macd_error` as
"MACD not reconfirmed today," not as a fresh disproof of whatever reading the
last scan recorded), `ema20`/`ema50`/`ema200` (with
`insufficient_history_for_ema200` set if you didn't fetch enough bars — in
that case, use the 200 EMA figure the last scan already recorded instead;
that number moves very little day to day, so a real earlier reading is still
good enough), `volume_ratio_vs_20d_avg`, and
`price_above_recent_high_20d`/`recent_high_20d_excl_latest` (needed for
setup_B_breakout's resistance-level check).

**Step 6 — Re-score against `screen_criteria.json` → `entry_setups`,** using
whichever setup the `waiting_for` text implies (pullback / breakout /
oversold reversal), same rubric as `daily_scan_prompt.md` Step 4:
- RSI and MACD: use today's Step 5 numbers where reliable
  (`macd_error` null); otherwise fall back to the last scan's recorded
  reading for that one signal only, and say so explicitly in the eventual
  alert/log — don't silently substitute without noting it.
- Price vs 200 EMA, price vs 20/50 EMA proximity, and volume ratio: always
  use today's Step 5 numbers — this is exactly the same-day confirmation
  that was missing before.
- Resistance level (setup_B): use `price_above_recent_high_20d` from Step 5.

**A crossed `trigger_price_cad` is not itself a passing score.** Score
honestly against all 5 points of whichever setup fits — a stock can cross a
200 EMA trigger (a loose long-term filter) while sitting mid-range in a
recent consolidation, extended above its 20 EMA, and below its actual
resistance level, which is not an actionable setup regardless of the
trigger firing. If the honest score is below 4, do not force it.

**Step 7 — Branch on the result:**

- **Score 4-5, all data confirmed (no unresolved `macd_error` on the
  decisive point):** treat this exactly like a `daily_scan_prompt.md` Step 6
  `new_buys` candidate. Compute `stop_cad` = `entry (today's live price) -
  2.5 * atr14` (from Step 5), `t1_cad`/`t2_cad` using the active mode's R:R
  ratios from `screen_criteria.json` (aggressive: 1:1.5 / 1:3). Then follow
  `daily_scan_prompt.md` Step 6 points 2-10 exactly (resolve account, pull
  live state, reconcile, size via `position_sizer.py`, guardrail check,
  preview, submit via `create_order_instruction`, verify via
  `get_order_history`/`get_positions`, log via `POST
  http://localhost:8282/api/positions`, `ntfy_alert.send_alert()` on every
  outcome). Note in the position's `notes` field that this was an intraday
  trigger-reconfirm auto-placement, not the morning scan, and cite the score
  and which numbers were freshly Questrade-confirmed vs. carried forward.
- **Score below 4, or a decisive signal (RSI/MACD/volume/resistance) still
  can't be confirmed even with Questrade's data:** do NOT execute. Send
  `ntfy_alert.send_alert()` with `priority='default'`, stating plainly:
  the ticker, the confirmed live price, which specific numbers are now
  confirmed (list them), the resulting score, and why it doesn't qualify
  (e.g. "3.5% above 20 EMA — too extended for a pullback entry; still below
  its $181.81 20-day high — not yet a confirmed breakout"). This replaces
  the old silent "wait for tomorrow" — the user gets real numbers today even
  when the answer is "not yet," and tomorrow's Step 1c still runs regardless.
- **A hard or soft guardrail fails during sizing:** same treatment as
  `daily_scan_prompt.md` Step 6 point 6 — skip, alert with the guardrail
  detail, no override (nobody's present to type one).

**Step 8 — Log the reconfirmation itself**, regardless of outcome, by
appending one line to `daily_run.log` in the same format
`fetch_prices.py`/`check_positions.sh` use, e.g.:
`[watchlist_reconfirm] TICKER: score N/5 (setup_type), price $X.XX, order_status Y`
— this makes the outcome auditable from the log the same way Step 6's
`order_status` field does for the morning scan, without needing a second
persistent output file.
