<!-- https://zunder-design-preview.pages.dev/docs/concepts/circuit-breakers · Markdown version of the page -->

# Daily loss stop and drawdown halt

The two circuit breakers of the risk engine, how they are measured, when they fire and clear, and why only a person can resume after a drawdown halt.

The risk engine has two circuit breakers. Both refuse new entries. Neither ever blocks an exit. Both are in `RiskEngine::observe` (`crates/zunder-risk/src/engine.rs`) and run today in Zunder's testnet runner.

| | Daily loss stop | Drawdown halt |
|---|---|---|
| Measured from | equity at the start of the UTC day | the highest equity seen |
| Default | 6% | 25% |
| Fires when | `(day start − equity) / day start ≥ 6%` | `(peak − equity) / peak ≥ 25%` |
| State | `halted_for_day` | `stopped` |
| Clears | by itself, at the first observation of the next UTC day | only when a person resumes |
| Then | flatten, refuse entries | flatten, refuse entries |

## Try it

Adjust the day’s loss and the fall from the peak to see which halt binds. The figure uses the browser engine; it sends no order.

Move the losses and see which breaker fires. The real engine judges each position of the sliders.

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

## How the engine observes

Every few seconds, and always before sizing, the caller tells the engine the account's equity. The engine then:

1. Rolls the day forward if a new UTC day has begun. The new day starts from the **last equity of the old day**, so a gap at midnight counts as the new day's loss. A late observation from an earlier day never clears today's halt.
2. Raises the peak if equity is higher.
3. Checks the drawdown first, then the day's loss.

When the state is no longer active, the caller has to flatten. Zunder's runner closes every position and cancels every order that could open one. The halt is written to the [journal](https://zunder-design-preview.pages.dev/docs/concepts/journal) **before** anything is closed.

## Example: the daily loss stop

Equity at 00:00 UTC: 2,000. Default 6%, so the stop fires at a loss of 120.

| Time (UTC) | Equity | Day's loss | State |
|---|---|---|---|
| 09:00 | 1,950 | 2.5% | active |
| 13:00 | 1,890 | 5.5% | active |
| 15:30 | 1,880 | 6.0% | **halted_for_day** |
| 18:00 | 1,910 | 4.5% | still halted |
| next day 00:00 | 1,910 | new day starts at 1,910 | active |

A recovery during the day does not clear the halt. The next day starts from 1,910, not from 2,000.

## Example: the drawdown halt

The peak was 2,600. Default 25%, so the halt fires at 1,950.

```text
(2,600 − 1,950) / 2,600 = 650 / 2,600 = 25%  →  stopped
```

From here nothing opens, today or any later day, until a person resumes.

## Only a person resumes

`RiskEngine::resume_after_review` is the only way out of a drawdown halt. It does nothing unless the engine is stopped. It restarts the peak from the current equity, so the next halt is measured from there.

Nothing in Zunder calls it automatically. That is a hard rule of the project (`CLAUDE.md`, hard rule 2): "never call it automatically". In Zunder's runner, a person runs `zunder-runner journal-resume --note "..."` with the runner stopped, and the note goes into the journal.

:::note[Planned]
In Guard: stop Guard, then `zunder-guard journal-resume --mode testnet --note "..."` (or `--mode paper`, `--mode mainnet`) on the machine that runs Guard, and start it again. The note goes into the journal. The MCP server, the monitor and the relay have no resume tool: an agent or a web page cannot resume. A restart does not resume either.
:::

**Example.** The halt fired at 1,950. You look at the trades, find a bug in the bot, fix it, and resume at 1,940. The new peak is 1,940. The next halt fires at 1,940 × 0.75 = 1,455.

## Deposits and withdrawals

The stops measure the account's equity, and a withdrawal lowers it as a loss would. **Withdraw while Guard is stopped, or expect a halt.** A withdrawal of 6% or more of the day's start halts the day; one of 25% or more of the peak stops Guard until a person resumes it. This never loosens anything: the worst it does is stop trading, and resuming is a person's decision. A deposit raises the equity and the peak, and can mask that day's earlier losses until the next day. Keeping deposits and withdrawals out of the stops is planned.

## Restarts do not clear anything

The engine's state is kept in the [journal](https://zunder-design-preview.pages.dev/docs/concepts/journal). A restart restores it. A test (400 random paths with random restarts) shows that a restored engine is never less strict than one that kept running (`docs/decisions.md`, 5 Oct 2026, "The risk engine persists and tracks positions").

## Limits

- **They measure equity, not intentions.** A loss inside an open position counts as soon as equity shows it. Zunder's runner reads equity from the venue on every poll.
- **They act after the fact.** The stop fires at the first observation at or past the threshold. A fast move between two observations can overshoot it. The stops resting on the venue are what limits each position in between.
- **Withdrawals count as losses.** Taking money off the account lowers equity. Stop the bot first, or expect a halt.
