Sizing from the stop
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
Section titled “The formula”For one new entry, with equity E:
risk per unit = |entry − stop| + round-trip cost per unittrade 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 sizeThen:
- 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). 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).
Example 1: the plain case
Section titled “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).
risk per unit = 1,200 + 72 = 1,272trade budget = 2,000 × 0.02 = 40open budget = 2,000 × 0.06 − 0 = 120risk budget = min(40, 120) = 40notional 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 BTCCheck: 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
Section titled “Example 2: open risk binds”Same account, but two positions already risk 100 USDC at their stops (5%).
open budget = 120 − 100 = 20risk budget = min(40, 20) = 20quantity = 20 / 1,272 = 0.015723… → 0.01572 BTCThe new trade gets half the size. All stops together may still lose at most 6% (19.996 + 100 ≤ 120).
Example 3: leverage binds
Section titled “Example 3: leverage binds”Equity 2,000, no positions. Entry 100, stop 99.8, costs 0.12 per unit.
risk per unit = 0.2 + 0.12 = 0.32by risk = 40 / 0.32 = 125 units (worth 12,500)by leverage = 10,000 / 100 = 100 units (worth 10,000)quantity = 100 unitsA 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
Section titled “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
Section titled “Example 5: the numbers from the latency benchmark”The benchmark in crates/zunder-exec/src/hyperliquid/signing.rs sizes this request 20,000 times:
equity 2,000 · buy · entry 3,101.5 · stop 3,040 · cost 2.8 per unitopen risk 35 · open value 1,800 · lot 0.0001 · minimum value 10
risk per unit = 61.5 + 2.8 = 64.3trade budget = 40open budget = 120 − 35 = 85risk budget = 40notional room = 10,000 − 1,800 = 8,200by risk = 40 / 64.3 = 0.622083…by leverage = 8,200 / 3,101.5 = 2.643881…quantity = 0.6220At the stop: 0.6220 × 64.3 = 39.99 USDC.
What Guard does with your bot’s size
Section titled “What Guard does with your bot’s size”Two limits of the method
Section titled “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%.