The nine rules
Exact definitions, formulas, defaults and bounds of Guard's nine rules, where each lives in the code, and what Backtest and Watch can and cannot judge.
Every order that opens or grows a position passes nine rules, in this order. The first refusal wins. If nothing refuses, the smallest allowed size wins. Orders that reduce or close a position pass without a check: exits are never blocked.
Where the rules live today:
- Engine rules are
zunder-risk’sRiskEngine, the code Zunder trades with on testnet. The website runs the same code, compiled to WebAssembly (crates/zunder-risk-wasm). - Policy rules are not in the engine. Today they exist in the website’s judge (
crates/zunder-risk-wasm/src/judge.rs). In Guard they will live in the policy file.
Your rules
Section titled “Your rules”| Rule | Your setting |
|---|---|
| Max leverage | 5x |
| Max loss at the stop | 2% |
| Without a stop | Guard sets one, 2% away |
| Min distance to liquidation | 10% |
| Max position size | 200% of equity |
| Max open risk | 6% |
| Daily loss stop | 6% |
| Drawdown halt | 25% |
| Markets | all |
filled in from your settings ·
In the tables below, E is account equity and percentages are fractions of it. In the config, fractions are written as fractions: 0.02 means 2%. In a rules code, they are written as percent: 2 means 2%.
(a) Market allowlist
Section titled “(a) Market allowlist”Definition. An entry in a coin that is not on your list is refused.
| Formula | coin ∈ markets, or the list is “all” |
| Default | all markets |
| Bounds | at most 1,000 coins |
| Refusal | coin_not_allowed |
| Lives in | policy (SiteRules::allowed_coins) |
Example. Your list is BTC and ETH. A bot buys HYPE. Refused: “HYPE is not on the market allowlist”.
Backtest: judged. Watch: judged.
(b) Protective stop
Section titled “(b) Protective stop”Definition. Every entry needs a stop that protects the whole position on the venue. If the bot sends none, Guard sets one defaultStopDistancePct away from the entry and sizes the order from it (stopPolicy = attach, the default), or refuses the entry (stopPolicy = refuse). A stop counts if it is a stop trigger order (not a take-profit) on the closing side, reduce-only or attached to the position, below the price for a long (above for a short), and together with other stops covers the whole position size. Of several, the loosest counts.
| Formula | stop exists ∧ stop on the losing side ∧ Σ stop sizes ≥ position size |
| Default | stopPolicy = attach, stop 2% away |
| Bounds | attach or refuse; the attached stop 0.1% to 25% away |
| Refusal | no_protective_stop (refuse policy only) |
| Lives in | policy; Zunder’s Session enforces the same for its own bot (every position gets a stop on the venue, or is closed at once) |
Example. A bot buys 1 ETH and places no stop. With the default, Guard sets a stop 2% below the entry and sizes the order so the loss there is your max loss at the stop. With stopPolicy = refuse: refused, “no stop order protects the ETH position”.
Backtest: judged from the account’s order history. A stop placed up to 60 s after the entry still counts (a bot places it once the entry fills). Entries older than the order history are kept as traded, not judged. Watch: judged; a stop placed up to 5 s after the fill counts.
(c) Max leverage
Section titled “(c) Max leverage”Definition. The value of all open positions plus the new entry may be at most max_leverage times equity. This is exposure, not Hyperliquid’s margin setting: a 3x position next to a 4x position is 7x here.
| Formula | open value + entry quantity × entry price ≤ E × max_leverage |
| Default | 5 |
| Bounds | above 0, at most 100 |
| Outcome | resize, or leverage_exhausted when no room is left |
| Lives in | engine (RiskEngine::size_entry) |
Example. Equity 2,000, positions worth 9,000. Room left: 2,000 × 5 − 9,000 = 1,000. A buy worth 3,000 is cut to 1,000.
Backtest: judged; other positions are valued at the last fill price seen in their coin. Watch: judged from the account’s live positions.
(d) Minimum distance to liquidation
Section titled “(d) Minimum distance to liquidation”Definition. After the trade, the liquidation price must be at least this far from the trade price.
| Formula | |price − liquidation price| / price ≥ min_liquidation_distance |
| Default | 10% |
| Bounds | 0 (off) up to, but not including, 100% |
| Refusal | liquidation_too_close |
| Lives in | policy |
No liquidation price passes: Hyperliquid reports none when a position cannot be liquidated at any positive price.
Example. A long at 100 with liquidation at 93 is 7% away. Refused: “liquidation is 7% from the price; the minimum is 10%”.
Backtest: not judged. Past liquidation prices are not in the public history. Watch: judged, from the liquidation price Hyperliquid reports after the trade.
(e) Max open risk
Section titled “(e) Max open risk”Definition. All stops together may lose at most this share of equity, the new entry included. A position without a stop has unbounded risk, so nothing new opens next to it.
| Formula | Σ qty × |price − stop| over open positions + entry risk ≤ E × max_open_risk |
| Default | 6% |
| Bounds | above 0, at most 100%; never below max loss at the stop |
| Outcome | resize, or open_risk_exhausted; unprotected_position when a position has no stop or its price is at or through it |
| Lives in | engine (RiskEngine::size_entry, combined_exposure) |
Example. Equity 2,000, open stops risk 100. Budget left: 120 − 100 = 20. A trade that would risk 40 is halved (Example 2).
Backtest: judged. Watch: judged; an account-level warning fires when open risk is already over the cap.
(f) Max position size
Section titled “(f) Max position size”Definition. One coin’s whole position, after the trade, may be worth at most this multiple of equity.
| Formula | (held quantity + entry quantity) × price ≤ E × max_position_fraction |
| Default | 200% (2× equity) |
| Bounds | above 0, at most 10,000% (100×) |
| Outcome | resize; position_cap_reached when the room left is below the venue’s minimum order |
| Lives in | policy |
Example. Equity 2,000, cap 200%: at most 4,000 in one coin. Holding 3,500 of SOL, a buy of 1,000 is cut to 500.
Backtest: judged. Watch: judged.
(g) Max loss at the stop
Section titled “(g) Max loss at the stop”Definition. If the stop is hit, this trade may lose at most this share of equity, round-trip costs included. This is the rule that sizes most trades.
| Formula | quantity × (|entry − stop| + round-trip cost per unit) ≤ E × risk_per_trade |
| Default | 2% |
| Bounds | above 0, at most 100%; at most max open risk |
| Outcome | resize; stop_on_wrong_side, below_minimum |
| Lives in | engine (RiskEngine::size_entry) |
The round-trip cost defaults to 12 basis points of the price (why), bounded to 0–500.
Example. Example 1 on Sizing: 0.5 BTC becomes 0.03144 BTC.
Backtest: judged where the entry had a stop. Without a stop, rule (b) decides first. Watch: judged.
(h) Daily loss stop
Section titled “(h) Daily loss stop”Definition. Once today’s loss reaches this share of the equity at the start of the UTC day, no entry opens until the next UTC day.
| Formula | (day start − equity) / day start ≥ daily_loss_stop (with an equity cap: divided by min(day start, cap)) |
| Default | 6% |
| Bounds | above 0, at most 100% |
| Refusal | halted_for_day |
| Lives in | engine (RiskEngine::observe) |
Details and examples: Daily loss stop and drawdown halt.
Backtest: judged on realised equity (closed PnL, fees, funding); an open loss counts when the trade closes. Watch: judged from Hyperliquid’s PnL history, read every five minutes.
(i) Drawdown halt
Section titled “(i) Drawdown halt”Definition. Once equity has fallen this far below its peak, no entry opens until a person has reviewed and resumed.
| Formula | (peak − equity) / peak ≥ drawdown_stop (with a cap: divided by min(peak, cap)) |
| Default | 25% |
| Bounds | above 0, at most 100% |
| Refusal | stopped |
| Lives in | engine (RiskEngine::observe, resume_after_review) |
Backtest: judged on realised equity; once halted, the guarded account opens nothing again. Watch: judged from the PnL history.
What no browser tool can judge
Section titled “What no browser tool can judge”Two of Guard’s promises cannot be seen from outside:
- Stops only tighten. A public account can loosen its own stops; Backtest and Watch show the result, not a refusal.
- Only a person resumes. A public account just keeps trading.
Deposits and withdrawals are left out of the equity history in both tools, so a withdrawal does not look like a loss.
Where the bounds come from
Section titled “Where the bounds come from”The bounds are what validate() accepts (RiskLimits::validate in crates/zunder-risk/src/limits.rs, SiteRules::validate in judge.rs). They stop nonsense such as 25 instead of 0.25. They are not advice: a 100% loss at the stop is a valid setting and a bad idea.
This page as plain Markdown, for people and LLMs: /docs/concepts/rules.md