Skip to content

Event schema for the monitor

A WebSocket on Guard’s own address, local only: ws://127.0.0.1:8547/zunder/events. One JSON object per message. Read-only: nothing sent to it changes Guard. Only the kill switch has a write path (planned, separate, and it can only pull, never release).

Every event has:

FieldTypeMeaning
schemastring"zunder-guard-event"
versioninteger0 while this is a draft
seqinteger1, 2, 3, … per Guard process; a gap means you missed events
atintegerUTC epoch milliseconds
networkstring"paper", "testnet" or "mainnet"
kindstringone of the kinds below

Money is a decimal string, never a JSON number: "0.03144", not 0.03144.

One per order that opens, grows or flips a position.

{"schema":"zunder-guard-event","version":0,"seq":42,"at":1791000000000,"network":"paper",
"kind":"decision","client":"freqtrade-1","coin":"BTC","side":"buy","effect":"open",
"asked_qty":"0.5","stop":"58800",
"verdict":{"kind":"resize","qty":"0.03144","notional":"1886.4","rule":"max_loss_per_trade",
"source":"engine","reason":"a stop-out may lose at most 2% of equity"},
"rules":[{"rule":"market_allowlist","source":"site","status":"passed"},
{"rule":"protective_stop","source":"site","status":"passed"},
{"rule":"max_leverage","source":"engine","status":"passed"},
{"rule":"min_liquidation_distance","source":"site","status":"passed"},
{"rule":"max_open_risk","source":"engine","status":"passed"},
{"rule":"max_position_size","source":"site","status":"passed"},
{"rule":"max_loss_per_trade","source":"engine","status":"resized"}]}
  • effect: open, increase, flip (as in the website judge).
  • verdict.kind: allow, resize (with qty, notional, rule) or veto (with code, rule). Codes: Veto and reason codes.
  • rules: every rule checked, in order, up to the first refusal; status is passed, resized, vetoed, assumed or not_judged.
  • source says where the rule lives: engine, session or site (policy). The name site comes from the website judge and may be renamed policy before version 1.

An order that reduces or closes. Never refused, so no verdict: coin, side, qty, client.

What was sent to Hyperliquid and what came back: coin, side, qty, price, role (entry, stop, exit, flatten), status (filled, resting, rejected), venue_message.

placed, moved (tighter), ignored (looser, stays), fired. Fields: coin, from, to.

Whenever the engine’s state changes, and once a minute: state (active, halted_for_day, stopped), equity, peak, day_start, open_risk, open_notional.

A daily loss stop or drawdown halt fired; a person resumed (note); the kill switch was pulled (reason, by: cli, mcp, telegram, monitor).

Guard’s record of positions and the venue’s disagree. discrepancy is one of not_in_book, not_on_venue, side_differs, qty_differs, stop_differs (PositionDiscrepancy in crates/zunder-risk/src/book.rs, exists today).

context, message. Never contains a key; addresses only.

seq 41 risk active, equity 2000, peak 2000
seq 42 decision BTC buy 0.5 → resize 0.03144 (max_loss_per_trade)
seq 43 order entry filled 0.03144 @ 60000
seq 44 stop placed BTC 58800
seq 45 decision HYPE buy → veto coin_not_allowed
seq 46 stop ignored BTC 58800 → 58000
seq 47 stop fired BTC 58800
seq 48 risk active, equity 1960