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

# The journal

The risk engine's append-only, checksum-chained journal, what each line holds, when a line is written, and what it can and cannot detect.

The risk engine remembers its halts, its peak, its daily window and its open positions in a journal on disk. This is `PersistentRisk` in `crates/zunder-exec/src/risk_store.rs`, true today and used by Zunder's testnet runner.

## One line per change

Each line is one JSON record. Lines are appended, never rewritten. Each line holds the engine's **complete** state after the change, so loading needs only the last line (after checking all of them).

A line, shortened:

```json
{"check":"9f2c…e41a","format":"zunder-risk-journal","version":2,"seq":17,
 "at":1791000000000,"event":{"kind":"updated"},
 "state":{"state":"active","peak":"2140.5","day":20729,"day_start":"2101.2",
          "last":"2098.7","positions":[{"symbol":"BTC","side":"buy","qty":"0.03144",
          "entry":"60000","stop":"58800","mark":"60210"}]},
 "prev":"1b07…aa90"}
```

| Field | Meaning |
|---|---|
| `check` | SHA3-256, in hex, of the exact bytes of the rest of the line |
| `format`, `version` | `zunder-risk-journal`, version 2. A newer version is refused, never guessed at |
| `seq` | 1, 2, 3, … without gaps |
| `at` | when the change happened, UTC milliseconds |
| `event` | why the line was written (below) |
| `state` | the engine's state after the change |
| `prev` | the previous line's `check`; empty for the first |

Because each line includes the previous line's checksum, the lines form a chain. Changing, removing or reordering a line in the middle breaks it.

## Events

| `event.kind` | Written by | When |
|---|---|---|
| `initialised` | a person | the first line only: the limits, the network and the account |
| `updated` | the engine | the state, peak, day, day start or positions changed |
| `resumed_after_review` | a person | after a drawdown halt, with a note |
| `repaired` | a person | a torn last line was cut off, with a note |
| `scoped` | a person | once, to name the network and account of an old journal |

A falling equity alone is not written. The last equity matters only as the next day's starting point, and a starting point that is too high makes that day's loss look larger. So a missing write can only make the engine stricter.

## What loading refuses

The journal opens only if every line checks out. It refuses a journal that is:

- missing, unreadable, or torn (the last line has no newline: a write was cut short);
- damaged or reordered (a checksum or the chain does not match);
- impossible (a step the engine cannot take: a lower peak, a drawdown halt cleared without a review, a daily halt cleared on the day it fired);
- marked broken (a failed write leaves `<journal>.broken` next to it);
- written under other limits, or for another network or account.

Each of these stops trading until a person has looked. Only a person starts a journal, cuts a torn line, removes a broken marker, or resumes. Changing the limits, even tightening them, starts a new journal with a new peak.

The writer runs the same checks before it appends, and never writes a line its reader would refuse. Each line is synced to disk before anything acts on it: a halt is on disk before a single position is closed. The journal is locked: a second process cannot open it.

## What it cannot detect

- **An older copy.** If someone replaces the journal with an earlier, complete copy, or cuts it back to an earlier complete line, the file itself looks fine. Nothing inside the file can tell.
- **Anything outside the engine.** It records risk state, not every order. Zunder's runner keeps a separate decision log for that.

:::note[Planned]
Guard keeps the same journal, opened for your network and account. A monitor page will show it read-only. Anchoring the chain's latest checksum somewhere outside your machine (to detect an older copy) is an idea, not a plan yet.
:::
