# Manual Trade Execution — invoked interactively, not by cron.

# Trigger phrases: "place the trade for TICKER" (entry) / "exit TICKER" or
# "sell N% of TICKER" (exit). This doc is NOT read by run_daily_scan.sh —
# order placement stays human-initiated until a separate, explicit decision
# changes that.

---

There are two human approval gates in this flow, and neither is optional:
1. **You show the user the concrete computed order** (ticker/qty/price/$ risk) and wait
   for explicit confirmation before calling `create_order_instruction`.
2. **Questrade's `create_order_instruction` itself never executes a trade** — it sends a
   push-to-approve request to the user's phone and blocks until they approve, deny, or it
   times out. Nothing reaches the market without that approval.

Do not skip gate 1 by treating gate 2 as sufficient — the user asked for a system that
shows its work, not one that fires and hopes.

## Entry flow — "place the trade for TICKER"

**Step 1 — Resolve the trade's numbers.**
Read the most recent `/media/raid/rshare/SwingTrader/daily_snapshots/swing_trade_*.json` and
find `TICKER` in `signals.new_buys`. Take `entry_zone_cad`, `stop_cad`, `t2_cad`,
`setup_score`, `company`, `sector`, `questrade_symbol` from there.
If `TICKER` isn't in today's (or the most recent) scan's `new_buys`, stop and ask the user
to supply entry zone / stop / target / setup score explicitly — do not guess numbers that
weren't screened.

**Step 1a — Resolve the Questrade instrument symbol (added 2026-08-27, bug fix).**
The bare tickers used everywhere in this system are not always Questrade's own symbol —
`CAR.UN` needed `CAR.UN.TO` on 2026-08-27, discovered only when `preview_order_instruction`
was about to be called with the wrong symbol. If `questrade_symbol` came through from Step 1,
use it. If it's missing or null (older snapshot, or the scan couldn't resolve it), call
`search_symbols` with the ticker as typed, or the company name if the bare ticker returns
noise (a search for `"CAR"` alone matches unrelated US securities), and filter results to
`listingMarket == "TSX"`. Exactly one TSX match → use its `symbol` field as the instrument for
every Questrade call below. Zero or more than one TSX match → stop and ask the user which
symbol to use — never guess a suffix on a real order.

**Step 2 — Block pyramiding.**
Read `/media/raid/rshare/SwingTrader/positions.json`. If `TICKER` is already in
`open_positions`, this is an ADD/pyramid case (`screen_criteria.json` →
`aggressive_overrides.trade_management.pyramid_rule`) — **out of scope for this workflow.**
Stop and tell the user to size and place that add manually; do not attempt to compute a
pyramid tranche here.

**Step 3 — Resolve the account.**
Call `list_accounts`. There should be exactly one account with `supportTrading: true`. If
there's more than one, stop and ask the user which account to use — never guess.

**Step 4 — Pull live state. This is ground truth; `positions.json` is not.**
Call `get_balances(accountId)` for live CAD and USD cash/buying power/equity, and
`get_positions(accountId)` for live holdings.

**Step 5 — Reconcile.**
Compare live `get_positions` against `positions.json`'s `open_positions`. If they disagree
(a position exists at the broker that isn't logged, or vice versa), stop and surface the
mismatch to the user — ask which is correct before proceeding. This is the same failure
mode that let CNQ.TO go unmanaged for two months; don't let a silent second source of truth
happen again.

**Step 6 — Size the trade.**
Run `python3 position_sizer.py --json` with:
- `--equity` = live equity from Step 4 (never `positions.json`'s `account.total_value_cad`)
- `--score` = `setup_score` from Step 1
- `--entry` = midpoint of `entry_zone_cad` (or the user's explicit override)
- `--stop` = `stop_cad`
- `--target` = `t2_cad`
- `--open-positions` = live open-position count from Step 4
- `--cash-available` = live CAD buying power from Step 4
- `--open-risk` (added 2026-08-12) = sum of `(entry_price_cad - stop_loss_cad) * shares` across
  `positions.json`'s `open_positions` (only entries where both are set; skip the rest — this is
  the same "Capital at Risk" figure the dashboard already shows). This is the originally-planned
  risk budget per position, not a live-quote-dependent figure, so `positions.json`'s values are
  fine here (unlike equity/cash, which must be live).
- `--realized-pl` (added 2026-08-12) = `retrospective.json`'s `summary.realized_pl_cad`. Note
  this file only regenerates once daily (2:05pm MDT cron) — if it's stale from before today's
  closes, say so, but use it anyway; there's no live equivalent to pull instead.

Read the result:
- Any guardrail with `"hard": true` and `"pass": false` → **stop.** Show the failure
  reason(s). Do not present an order to the user. `drawdown_circuit_breaker` failing means
  cumulative realized losses have crossed `max_drawdown_halt_pct` — this one is intentionally
  unoverridable; tell the user new entries are halted and this needs a deliberate review, not
  a resize.
- Any guardrail with `"hard": false` and `"pass": false` → show the warning and require the
  user to type an explicit override (e.g. "override cash_reserve, proceed with 8 shares")
  before continuing. Do not treat silence or a vague "sure" as an override. `portfolio_risk_cap`
  failing means shares were already reduced to fit the remaining risk budget (see the
  `detail` field for the capped count) — the override, if given, is for proceeding at the
  now-larger requested size anyway, not for restoring it silently.

**Step 7 — Dry-run validate.**
Call `preview_order_instruction` with the computed order (symbol=`questrade_symbol` from
Step 1a, **never** the bare `TICKER`, side=Buy, type=Limit, qty=`shares` from Step 6,
limitPrice=entry from Step 6). If it flags an issue
(odd lot, insufficient buying power, not fractional-eligible at this qty), treat that as a
hard stop too — fix the inputs or report to the user, don't proceed to gate 1 with an order
Questrade will reject anyway.

**Step 8 — Human gate 1: present the order.**
Show, in one place: ticker, side (Buy), qty, order type (Limit), limit price, estimated
cost, dollar risk, risk % of equity, CAD cash available vs cost (explicit call-out if CAD
is short — check this every time, don't assume it's fine because it was fine yesterday).
Wait for the user to explicitly confirm. An unclear response is not a yes.

**Step 9 — Human gate 2: submit.**
Call `create_order_instruction(operation="create", accountId=..., instrument=<questrade_symbol
from Step 1a>, side="buy", type="limit", qty=<from Step 6>, limitPrice=<from Step 8>,
duration="day")`. This blocks pending the user's phone approval.

**Step 10 — Verify before writing anything. Do not trust the reported outcome alone.**
Regardless of what `create_order_instruction` returns, call `get_order_history` and/or
`get_positions` to confirm actual state — a phone approval can land right at the tool's
timeout boundary, and a limit order can partial-fill for less than the requested qty.
- **Filled:** get the *actual* fill price and qty from order history (not the requested
  values). `POST http://localhost:8282/api/positions` with
  `{"action":"add","ticker":TICKER,"entry_price_cad":<actual fill>,"shares":<actual filled>,
  "company":..., "sector":..., "entry_date":today, "questrade_symbol":<from Step 1a>,
  "notes":"setup_score N, order <id>"}`. Include `questrade_symbol` so a later exit never has
  to re-resolve it.
  If the POST fails (connection refused), **stop and tell the user to start `server.py`** —
  do not fall back to editing `positions.json` directly; that recreates the exact
  two-writers problem this endpoint exists to prevent.
  Ask the user if they also want the protective stop placed now, as a separate
  approval-gated `create_order_instruction` call (type=Stop, stopPrice=`stop_cad`).
  Call `ntfy_alert.send_alert()` confirming the trade is logged.
- **Rejected:** report to the user. No writes to `positions.json`. Offer to retry with an
  adjusted price if they want.
- **Timed out:** re-verify per above before concluding anything. Only treat it as "nothing
  happened" once you've confirmed via `get_order_history`/`get_positions` that it's
  genuinely unfilled — if it turns out something did fill despite the timeout report,
  follow the Filled path instead.

## Exit flow — "exit TICKER" / "sell N% of TICKER"

**Step 1** — Look up `TICKER` in `positions.json`'s `open_positions`. Not found → stop.
Take `questrade_symbol` from that entry if present.

**Step 1a** — Resolve the Questrade instrument symbol, same rule as the entry flow's Step 1a:
if `questrade_symbol` is missing from the position (an older entry logged before this field
existed), call `search_symbols` (ticker, then company name if noisy), filter to
`listingMarket == "TSX"`, exactly one match required — stop and ask the user if it's ambiguous
or empty. Use this resolved symbol for every Questrade call below, never the bare `TICKER`.

**Step 2** — Call `get_positions` to verify the live held quantity. Disagreement with
`positions.json`'s `shares` → surface it, ask the user which is correct before proceeding.

**Step 3** — Determine quantity: full position by default (matches `signals.exits`'
`"EXIT full position"` semantics). If the user specified a fraction (e.g. tiered T1/T2/T3 —
`check_positions.py`'s alert language is "sell 20%"/"sell 30%"), use that instead.
**Note:** `server.py`'s `remove` action only supports closing a position fully — see the
Known Gap below before doing a partial sell.

**Step 4** — Order type defaults to **Market** (exits are typically urgency-driven — stop
hit, time stop, "sell now"). Offer Limit as an alternative if the user asks for one.

**Step 5** — Same sequence as the entry flow: human gate 1 (present the order) →
`preview_order_instruction` → human gate 2 (`create_order_instruction`, side="sell") →
re-verify via `get_order_history`/`get_positions` before writing anything.

**Step 6 — On a confirmed full exit:**
`POST http://localhost:8282/api/positions` with
`{"action":"remove","ticker":TICKER,"exit_price_cad":<actual fill>}`.

**Step 7 — Known gap: partial exits.**
`server.py`'s `remove` action closes a position fully; `modify`'s negative-share-delta path
doesn't record an `exit_price_cad` for the sold tranche, so a T1/T2/T3 partial sell routed
through it won't show up correctly in `generate_retrospective.py`'s P/L totals. If the user
wants a partial exit, tell them this explicitly and let them log it by hand via the
dashboard's Position Manager instead of silently mis-recording it here.

## Rules

- Never call `create_order_instruction` without having shown the user the concrete order
  first (gate 1) — gate 2's phone approval is a second check, not a replacement for it.
- Never write to `positions.json` directly from this workflow. Always go through
  `server.py`'s `POST /api/positions` so the dashboard and file stay one source of truth.
- Never size a trade off `positions.json`'s `account.total_value_cad` — it's a stale
  placeholder. Always pull live equity/cash via `get_balances` in the same turn you size
  the trade.
- Never proceed past a hard-failed guardrail from `position_sizer.py`.
- Never attempt a pyramid/ADD trade through this workflow — flag it and stop.
- Never pass a bare ticker as `instrument` to `preview_order_instruction` or
  `create_order_instruction` — always resolve `questrade_symbol` first (Step 1a in both
  flows). Questrade's own symbol table doesn't always match this system's bare tickers
  (`CAR.UN` vs `CAR.UN.TO`), and guessing risks trading the wrong instrument.
