<!-- https://zunderlabs.com/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

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://zunderlabs.com/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: `zunder-guard resume --note "..."` on the machine that runs Guard. The MCP server, the monitor and the relay will 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.

## Restarts do not clear anything

The engine's state is kept in the [journal](https://zunderlabs.com/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.
