# Risk sizing: from risk % to a real, affordable order Every trading script computes a position size in passes, never one: 1. **Risk-based sizing** (`positionQty` / `fractionalPositionQty` / `cashSecuredPutContracts`) — "how big should this position be, given the strategy's risk-per-trade % and the distance to the stop-loss?" This is sized off **equity**, because that's the number the risk-per-trade % is meant to be a fraction of. 2. **Buying-power clamp** (`clampToBuyingPower` / `clampFractionalToBuyingPower`, `src/risk/rules.ts`) — "can the account actually afford that many shares/contracts/coins *right now*?" This is checked against **buying power** (or, for wheel-v1's cash-secured puts, `options_buying_power`), because equity and spendable cash are not the same number on an account that already holds positions. 3. **Per-buy hard caps** (`clampPerBuy`, `src/risk/rules.ts`) — the account's own fixed limits on ONE buy: **max value per buy** (`maxNotionalPerBuy`, a money amount in the account's currency) and **max shares per buy** (`maxSharesPerTrade`, skipped for crypto). Both live on every strategy's per-trade risk settings (unset in every base config = no cap) and are set per account on the Accounts page, for any strategy. Every trading script — Alpaca swing-dip, intraday, QSR, RSI-2, crypto, and T212 QSR (with a GBP→USD conversion for US stocks) — calls this one function, and so does the Accounts page's worked example, so the cap can't be enforced in one place and forgotten in another. Until 2026-10-02 the value cap existed only for QSR and was only enforced on T212. wheel-v1 doesn't use it: it sizes contracts against collateral. Passes 2 and 3 only ever shrink what pass 1 decided — they never size something up. If buying power comfortably covers the risk-sized amount and no cap is set, both are no-ops. ## Why this exists Found live on `live-1` (the GBP live-launch account): equity **$137**, `buying_power` **$13.72** — most of the account's value was already committed to open positions. Risk-based sizing alone had no way to see that gap; it would happily compute a qty the account couldn't actually pay for, which Alpaca would then reject (or worse, partially fill in a way nothing downstream expected). ## Worked examples | Script | Function | Risk-sized amount | Price / collateral per unit | Buying power available | Clamped result | What the script logs | |---|---|---:|---:|---:|---:|---| | `trade.ts` (swing-dip-v1), well-capitalized account | `clampToBuyingPower` | 12 shares | $180.00 | $54,040 | **12 shares** (unchanged) | nothing — buying power ($54,040) covers the full $2,160 cost, clamp is a no-op | | `trade.ts`, thin account (live-1-shaped) | `clampToBuyingPower` | 3 shares | $8.00 | $13.72 | **1 share** | `risk sizing wanted 3 shares, buying power only covers 1 — using 1` | | `trade.ts`, thin account, pricier symbol | `clampToBuyingPower` | 2 shares | $52.00 | $13.72 | **0 shares** | `candidate but only 0 share(s) affordable (buying power) vs 2 risk-sized — skip` | | `qsrTrade.ts`, anchor+runner tranches | `clampToBuyingPower` | 40 shares (pre-split) | $25.00 | $600 | **24 shares** | `risk sizing wanted 40 shares, buying power only covers 24 — using 24` — then split into tranches as usual | | `intradayWatch.ts` (orb-v1, after `sizeMultiplier`) | `clampToBuyingPower` | 15 shares | $60.00 | $700 | **11 shares** | `risk sizing wanted 15 shares, buying power only covers 11 — using 11` | | `cryptoTrade.ts`, BTC breakout | `clampFractionalToBuyingPower` | 0.05000 BTC | $65,000.00 | $2,000 | **0.030769 BTC** | `risk sizing wanted 0.05, buying power only covers 0.030769 — using 0.030769` | | `wheelTrade.ts`, cash-secured put | `clampToBuyingPower` (against `options_buying_power`) | 3 contracts | $6,600 collateral/contract ($66 strike × 100) | $8,000 | **1 contract** | `pool sizing wanted 3 contracts, options buying power only covers 1 — using 1` | | `wheelTrade.ts`, covered call | *(not clamped)* | 2 contracts | — | — | **2 contracts** (unchanged) | covered calls are collateralized by shares already held, not cash — no buying-power check applies | ## The math, in full ``` clampToBuyingPower(qty, price, buyingPower): if price <= 0 or buyingPower <= 0: return 0 return min(qty, floor(buyingPower / price)) clampFractionalToBuyingPower(qty, price, buyingPower, precision = 6): if price <= 0 or buyingPower <= 0: return 0 factor = 10^precision maxAffordable = floor((buyingPower / price) * factor) / factor return min(qty, maxAffordable) ``` Applying the BTC example above by hand: `buyingPower / price = 2000 / 65000 = 0.0307692...`, truncated to 6 decimal places → `0.030769`, then `min(0.05, 0.030769) = 0.030769`. ## Where the running balance is tracked Each script keeps a local `buyingPower` (or `optionsBuyingPower` for wheel-v1) variable, fetched once per run from `getAccount()`, and decrements it after every entry that actually fires — same pattern as the pre-existing `slots -= 1` / `exposed.add(symbol)` bookkeeping. This stops a second candidate later in the same run from being sized against cash the first candidate already spent. See CLAUDE.md §16 for the full incident writeup and the file-by-file list of what changed.