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
Section titled “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:
{"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
Section titled “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
Section titled “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>.brokennext 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
Section titled “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.
This page as plain Markdown, for people and LLMs: /docs/concepts/journal.md