<!-- https://zunderlabs.com/docs/concepts/sizing · Markdown version of the page -->

# Sizing from the stop

How Guard turns a stop price into a position size, with the exact formula from zunder-risk and hand-worked examples.

Guard does not ask "how big is this order?". It asks "how much does this order lose if the stop is hit?". The size follows from that.

This is true today: it is `RiskEngine::size_entry` in `crates/zunder-risk/src/engine.rs`, the same code Zunder's own runner uses on Hyperliquid testnet.

## The formula

For one new entry, with equity `E`:

```text
risk per unit   = |entry − stop| + round-trip cost per unit
trade budget    = E × risk_per_trade                       (default 2%)
open budget     = E × max_open_risk − open risk already    (default 6%)
risk budget     = min(trade budget, open budget)
notional room   = E × max_leverage − open position value   (default 5x)

quantity = min( risk budget / risk per unit , notional room / entry )
           rounded down to the venue's lot size
```

Then:

- If the stop is not on the losing side of the entry (below it for a buy, above it for a sell), the entry is refused: `stop_on_wrong_side`.
- If the risk budget is zero or less, refused: `open_risk_exhausted`.
- If the notional room is zero or less, refused: `leverage_exhausted`.
- If the rounded quantity is zero, or worth less than the venue's minimum order value, refused: `below_minimum`.
- The quantity is never rounded up.

**Which equity.** The engine uses the smallest of three numbers: the equity in the request, the last equity it observed itself, and the equity cap if one is set ([Equity cap](https://zunderlabs.com/docs/concepts/equity-cap)). A wrong or stale number can only make the size smaller.

**Which open risk.** The engine keeps its own record of open positions. It uses the larger of its record and the venue's, symbol by symbol. A position without a stop has no bounded risk, so nothing is sized next to it: `unprotected_position`.

**Open risk of one position** is `quantity × distance from the current price to its stop`. Its value is `quantity × current price` (`crates/zunder-risk/src/book.rs`).

## Try it

Drag the stop and watch the size. The real engine sizes it, in your browser, with your rules from this site or the defaults.

_On the web page: an interactive figure for this rule._

## Example 1: the plain case

Equity 2,000 USDC. Defaults: 2% per trade, 6% open risk, 5x. No open positions.

A long on BTC: entry 60,000, stop 58,800. Round-trip costs 12 basis points of the price, so 60,000 × 0.0012 = 72 per BTC. Lot size 0.00001 BTC (an assumption for the example).

```text
risk per unit  = 1,200 + 72                 = 1,272
trade budget   = 2,000 × 0.02               = 40
open budget    = 2,000 × 0.06 − 0           = 120
risk budget    = min(40, 120)               = 40
notional room  = 2,000 × 5 − 0              = 10,000

by risk        = 40 / 1,272                 = 0.031446…
by leverage    = 10,000 / 60,000            = 0.1666…
quantity       = 0.031446… → 0.03144 BTC
```

Check: at the stop, 0.03144 × 1,272 = 39.99 USDC, just under 2% of 2,000. The position is worth 0.03144 × 60,000 = 1,886.40 USDC, under 1x.

The 12 basis points are the measured round-trip cost for the most liquid perps at fee tier 0 (`docs/decisions.md`, 5 Oct 2026, "Maker execution measured; taker cost assumption"). Your costs may differ.

## Example 2: open risk binds

Same account, but two positions already risk 100 USDC at their stops (5%).

```text
open budget    = 120 − 100                  = 20
risk budget    = min(40, 20)                = 20
quantity       = 20 / 1,272 = 0.015723… → 0.01572 BTC
```

The new trade gets half the size. All stops together may still lose at most 6% (19.996 + 100 ≤ 120).

## Example 3: leverage binds

Equity 2,000, no positions. Entry 100, stop 99.8, costs 0.12 per unit.

```text
risk per unit  = 0.2 + 0.12                 = 0.32
by risk        = 40 / 0.32                  = 125 units   (worth 12,500)
by leverage    = 10,000 / 100               = 100 units   (worth 10,000)
quantity       = 100 units
```

A very tight stop asks for a large position. The leverage cap wins. This trade risks only 100 × 0.32 = 32 USDC (1.6%).

## Example 4: the stop on the wrong side

A buy at 100 with a stop at 101. The distance is −1. Refused: `stop_on_wrong_side`. Nothing is sized.

## Example 5: the numbers from the latency benchmark

The benchmark in `crates/zunder-exec/src/hyperliquid/signing.rs` sizes this request 20,000 times:

```text
equity 2,000 · buy · entry 3,101.5 · stop 3,040 · cost 2.8 per unit
open risk 35 · open value 1,800 · lot 0.0001 · minimum value 10

risk per unit  = 61.5 + 2.8                 = 64.3
trade budget   = 40
open budget    = 120 − 35                   = 85
risk budget    = 40
notional room  = 10,000 − 1,800             = 8,200
by risk        = 40 / 64.3                  = 0.622083…
by leverage    = 8,200 / 3,101.5            = 2.643881…
quantity       = 0.6220
```

At the stop: 0.6220 × 64.3 = 39.99 USDC.

## What Guard does with your bot's size

:::note[Planned]
Guard compares the size your bot asked for with the size above and sends the **smaller** one. A bot that asks for less than the limit gets exactly what it asked for. A bot that asks for more is resized, and its reply names the rule that bound the size. Zunder's own `Session` already does the same: it sizes every entry again from the venue's own numbers and sends the smaller quantity (`crates/zunder-exec/src/session.rs`).
:::

## Two limits of the method

- **Gaps.** A stop can fill beyond its trigger. Zunder's session allows a protective stop to fill up to 10% beyond its trigger (`stop_slippage`, the same as Hyperliquid's own market stops). A gap through the stop costs more than the budget.
- **Costs are an estimate.** The round-trip cost is a number you give. If it is too low, a stop-out costs a little more than 2%.
