kestrel.markets
Version:
A typed, token-efficient language + runtime for agentic trading: agents author bounded plans, the runtime fires them at the tick. CLI + typed library + MCP server.
200 lines (151 loc) • 9.11 kB
Markdown
# Kestrel examples
A handful of things you can run in a minute, all on generic tickers (SPX / SPY). Each
is a real invocation of the shipped CLI or library — copy, paste, run.
Every command is self-documenting: `kestrel --help` lists the commands, the global
flags, and the [exit-code taxonomy](#exit-codes); `kestrel <command> --help` (e.g.
`kestrel run --help`, `kestrel runs list --help`) prints that one command's own usage
and flags.
## Setup
**From a clone of this repo** (build once, then use `node dist/cli.js` as the CLI):
```bash
bun install && bun run build
```
**As an installed dependency** — `npm i kestrel.markets` gives you the `kestrel`
binary (swap `node dist/cli.js` for `kestrel` / `npx kestrel` below) and the
`kestrel.markets` / `kestrel.markets/lang` library imports.
---
## 1. Round-trip a plan — `momentum-breakout.kestrel`
Parse a plan into the typed object model and print it back in canonical form. The
output is byte-stable: `print(parse(text))` is a fixed point (ADR-0004).
```bash
node dist/cli.js print examples/momentum-breakout.kestrel
```
Prints the same plan back, canonicalized. `parse` (or its alias `validate`) checks it
without printing.
## 2. Watch it fail closed — `reject-exit-on-mark.kestrel`
An active `EXIT` may not condition on the mark — a quote is a health signal, never a
value (ADR-0005). The parser refuses this at parse time and exits nonzero, instead of
arming a strategy that trusts a lying mark. This holds whether the mark is spelled with
the exchange jargon (`mid`/`bid`/`ask`/`last`) or with the plain-English words a written
playbook uses for the option quote (`mark`/`premium`, as in "close at 2x premium") —
condition the exit on `fair`, `spot`, or a structural level instead.
```bash
node dist/cli.js parse examples/reject-exit-on-mark.kestrel; echo "exit=$?"
```
```
error code=PARSE message=EXIT may not condition on the mark `mid`: marks lie ... (line 9, col 19)
exit=2
```
## 3. See the market as text — `briefing.json` + `wake.json`
Render a **Frame** — the same market picture you'd see on a terminal, laid out for a
context window instead of a screen. The tape is vertical (one row per candle, newest
last) so an agent that watches all day pays only for the new bar.
```bash
node dist/cli.js frame examples/briefing.json
```
```
KESTREL · OPEN briefing · T-92m to close · regular · 13:24 ET
...
tape 5m · axis 505.90→521.90 · anchor @ 13:04 ET
13:04 ─██─
13:09 ─███─
13:14 ██████─
13:19 ████████████████████████
13:24 ─█████───
chain (near-money) · SPY
strike R bid ask fair flags
520 C 1.70 2.05 1.84 b76 nLiq=6 —
```
Add `--json` to get the ASCII transported under a `kestrel.frame/v1` schema.
### The two frame-input kinds
`frame` reads a single JSON object (a path argument, `--input <p>`, or `-` / stdin) and
infers its **kind** from the shape, then renders it. `--kind briefing|wake` overrides
the inference.
- **`briefing`** — the OPEN keyframe, a full market picture (`examples/briefing.json`).
- **`wake`** — a WAKE delta: everything **since the last vantage** — the tape buckets
since last, the current levels/kernel, and the wake's own identity. It is inferred
whenever the object carries a `wakeIndex` (`examples/wake.json`):
```bash
node dist/cli.js frame examples/wake.json --json | jq .kind # "wake"
```
The two shapes share a `market` pane (`instrument`, `levels`, `tape`, `tapeBucketMin`,
`chain`) and an acting `kernel` (`positions`, `resting`, `fillsSinceLast`, optional
`budget`, `plans`). A `briefing` adds `timeToCloseMin` + `instruments`; a `wake` adds
`wakeIndex`, `minutesSinceLast`, and an optional `wakeReason`. The full field list is
`src/frame/types.ts` (`BriefingInput` / `WakeDeltaInput`).
Two invariants hold across both kinds:
- **Date-blind by construction.** There is no `session_date`, day-of-week, or
epoch-millisecond field anywhere. Time is carried only as `timeToCloseMin` (relative),
`wakeIndex` + `minutesSinceLast`, and HH:MM `clockET` strings — never an absolute date.
- **Unknowns are explicit.** A genuinely unavailable number is `null` (or an omitted
optional) and renders as an explicit `—`; the renderer never guesses or defaults a value.
A non-object input, or malformed JSON, is a loud `USAGE` (exit 2); a missing input file
is `NOT_FOUND` (exit 3) — see [exit codes](#exit-codes).
## 4. Use the library — `roundtrip.mjs`
The text DSL and the typed object model are the same language: `parse` text to
objects, `print` objects back to byte-identical text.
```bash
node examples/roundtrip.mjs
```
```js
import { parse, print } from "kestrel.markets/lang";
const doc = parse(source);
print(parse(print(doc))) === print(doc); // true
```
## 5. Drive the managed API — `client-mint-sim-proof.mjs`
The SDK face (`kestrel.markets/client`, one of the four equal faces — ADR-0004). Walks
the day-one flow — **mint** a trial capability, **validate**, **sim** (streaming the
operation to a certified Blotter), **grade** it, then hit the **paid boundary** and
watch the 402/Offer arrive as data with the free proof already earned. Runs offline
against the in-process contract server the client ships — no secrets.
```bash
node examples/client-mint-sim-proof.mjs # in-process contract mock (default)
KESTREL_API=https://api.kestrel.markets node examples/client-mint-sim-proof.mjs # live
```
```
1. minted trial capability kind=trial scopes=[data, sim, grade]
3. sim operation=op_sim_1 state=open
events=[operation.started → operation.artifact → operation.completed]
blotter (receipt) sessionId=bl_mock_1
4. grade subject=dh_mock metrics: bankable_ev=27.3 fill_support=0.61
5. paid boundary 402/Offer (surfaced as data, not a redirect)
amount=25.00 USDC proof: https://kestrel.markets/proof/op_sim_1
```
## 6. Adopt a held leg across a wake — `plan-acquire-open.kestrel` + `plan-manage-adopt.kestrel` + `adopt-cross.jsonl`
The exit escape hatch, end to end. `adopt-cross.jsonl` is a short SPY bus whose spot crosses
**100** in the morning (so `acquire` fills a 100C) then **114** in the afternoon (so the adopted
`EXIT` rests). The opener stages as the day's plan; the adopter is the wake reply that supersedes
it and carries the held leg over with `ARM … foreach held leg`, so its covered sell can close the
position the replacement plan never opened. `kestrel card adopt` walks the whole loop.
```bash
mkdir -p ./run
cp examples/plan-acquire-open.kestrel ./run/plans-0.kestrel
cp examples/plan-manage-adopt.kestrel ./run/revision-0.kestrel
bun dist/cli.js day --bus examples/adopt-cross.jsonl --dir ./run --fill strict-cross-v1 --r-usd 1000 --wakes 12:00
```
Ends `plans_armed=2 … orders_filled=2` — `acquire` buys the 100C, `manage` adopts it at basis
0.90 and its covered `EXIT` sells it: fill → adopt → exit, with no naked hole at any step.
---
## Exit codes
Every command fails **closed and loud**: on error it writes a structured line to
**stderr** (stdout stays a pure payload channel) and returns a nonzero exit code. The
error carries a stable `code` string — agents match on the `code`, not on prose, and the
codes are stable across releases. This taxonomy mirrors `EXIT` in `src/cli/errors.ts`;
`kestrel --help` prints it too.
| exit | `code` | when |
| ---: | --------------------- | ------------------------------------------------------------------------------------- |
| 0 | `OK` | the command succeeded |
| 1 | `GENERIC` | an unexpected/uncaught error (`KESTREL_DEBUG=1` also dumps the stack) |
| 2 | `USAGE` | bad/unknown flag, missing required flag, bad `--format`, or a parse failure (`PARSE`) |
| 3 | `NOT_FOUND` | `runs show <id>` / `lineage <name>` / an input file is absent |
| 4 | `RUNTIME_UNAVAILABLE` | bun/chdb is needed but could not be loaded |
| 5 | `PAYMENT_REQUIRED` | a remote verb returned a 402/Offer (surfaced as data) |
| 130 | `SIGINT` | interrupted (Ctrl-C) |
A parse failure raised by the language layer surfaces the `code: "PARSE"` inside the
`USAGE` exit bucket (exit 2) — see example 2 above. Under `--json`, the error is a
`{ "error": { "code", "message", "hint"? } }` object on stderr; under `text`, a
`error\tcode=…\tmessage=…` line.
---
What each feature actually does today — graded per capability across **syntax ·
runtime · evidence · access**, with source and test receipts — is tracked honestly in
[`../docs/public/status.md`](../docs/public/status.md).