recipe-playground-webhook
GitHub将外部Webhook信号转化为沙箱内的模拟交易,用于测试信号质量及记录事件驱动循环。支持本地监听、参数配置及安全校验,确保输入可信且不执行真实订单。
Trigger Scenarios
Install
npx skills add krakenfx/kraken-cli --skill recipe-playground-webhook -g -y
SKILL.md
Frontmatter
{
"name": "recipe-playground-webhook",
"version": "1.0.0",
"metadata": {
"openclaw": {
"domain": "sessions",
"category": "recipe"
},
"requires": {
"bins": [
"kraken",
"python3"
],
"skills": [
"kraken-playground"
]
}
},
"description": "Example driver: turn incoming webhook signals into validated paper trades inside a recorded session."
}
Playground: Webhook → Paper Trade
PREREQUISITE: Load
kraken-playgroundto run this recipe. This recipe is an example, not a boundary. Adapt the steps to your signal source, or write your own driver (seekraken-playground→ Driving Your Own Hypothesis).
Bridge an external system into the sandbox: a TradingView alert, a CI job, or a plain curl POSTs a signal to a local listener, and each valid signal becomes a paper order inside a recorded session. The market tape, the fills, and the signal-to-order decisions all land in one window for replay and P&L.
Use this skill for:
- wiring an existing alerting system to paper execution without touching its config
- testing a signal feed's quality (how its entries score) before any real money
- recording an event-driven loop whose events come from outside the market
Important
This recipe records a session. It never places a live order.
The webhook payload is untrusted input. Every field is validated against an allowlist or a numeric bound before it reaches an order — a signal that fails any check is logged as a skip, never executed, and never interpolated into a command.
The listener binds 127.0.0.1 only. Do not bind other interfaces and do not port-forward it; anyone who can reach the port can trade the paper account. If the signal source is remote, tunnel (e.g. ssh -R) rather than expose.
The signal stream is the loop, not /loop — the same long-lived-process exception as the dip-triggered recipe (see kraken-playground → Pacing).
Signal Contract
One JSON object per POST body:
{"symbol": "BTC/USD", "side": "buy", "qty": "0.001", "token": "<shared-secret>", "note": "free text"}
symbol, side, qty are required; token is required when the token param is set; note is echoed into the decision reason.
Params
Every bound that gates execution goes in --strategy-params:
port: listener port on 127.0.0.1allowed_symbols: the only pairs a signal may trade — must be a subset of the session's recorded--symbols, or fills have no marks to score bymax_notional_per_signal: quote-currency cap per order; a signal above it is skipped, not clampedmax_signals: stop after this many executed signalstoken: optional shared secret; when set, signals without a matchingtokenfield are dropped
Quick Start
Natural language:
Listen on localhost:8787 for webhook signals. Trade only BTC/USD, at most
$500 per signal, at most 10 signals, then stop. Record every accepted and
rejected signal with its reason. At the end, show P&L.
Start the Session
Work inside a paper workspace (create one once: kraken workspace create hook --capital 10000 --mode paper), then:
export KRAKEN_WORKSPACE=hook
kraken session start \
--symbols BTC/USD --channels ticker,trade --to duckdb,jsonl \
--label hook-btc-$(date +%Y%m%d-%H%M%S) \
--strategy recipe-playground-webhook \
--strategy-params '{"port":8787,"allowed_symbols":["BTC/USD"],"max_notional_per_signal":500,"max_signals":10}' \
-o json 2>/dev/null &
# The session_started stdout line carries the id: {"type":"session_started","session":"s<n>",...}
Print the session id to the user right after starting, and again in the final report.
Listen and Decide
The listener appends each POST body to a signals file outside the session directory (the recorder owns that tree); the decision loop tails it. Both die with the session.
PORT=8787
ALLOWED_SYMBOLS="BTC/USD" # space-separated allowlist
MAX_NOTIONAL=500
MAX_SIGNALS=10
SIGNALS=$(mktemp -t hook-signals)
python3 - "$PORT" "$SIGNALS" <<'PY' &
import http.server, sys
port, path = int(sys.argv[1]), sys.argv[2]
class H(http.server.BaseHTTPRequestHandler):
def do_POST(self):
body = self.rfile.read(int(self.headers.get("Content-Length", 0)))
with open(path, "ab") as f:
f.write(body.strip() + b"\n")
self.send_response(204); self.end_headers()
def log_message(self, *args): pass
http.server.HTTPServer(("127.0.0.1", port), H).serve_forever()
PY
LISTENER=$!
DONE=0
tail -n +1 -f "$SIGNALS" | while read -r sig; do
SYMBOL=$(echo "$sig" | jq -r '.symbol // empty')
SIDE=$(echo "$sig" | jq -r '.side // empty')
QTY=$(echo "$sig" | jq -r '.qty // empty')
NOTE=$(echo "$sig" | jq -r '.note // ""')
# Untrusted input: allowlist the enum and the pair, bound the number.
# A failed check is a recorded skip — never a clamped or guessed order.
case "$SIDE" in buy|sell) ;; *)
kraken session note --kind skip --reason "webhook rejected: bad side '$SIDE'" -o json 2>/dev/null; continue;;
esac
case " $ALLOWED_SYMBOLS " in *" $SYMBOL "*) ;; *)
kraken session note --kind skip --reason "webhook rejected: symbol '$SYMBOL' not in allowlist" -o json 2>/dev/null; continue;;
esac
if ! echo "$QTY" | grep -qE '^[0-9]*\.?[0-9]+$'; then
kraken session note --kind skip --reason "webhook rejected: non-numeric qty '$QTY'" -o json 2>/dev/null; continue
fi
PRICE=$(kraken ticker "$SYMBOL" -o json 2>/dev/null | jq -r '.. | .last? // empty' | head -1)
NOTIONAL=$(echo "scale=2; $QTY * $PRICE" | bc -l)
if (( $(echo "$NOTIONAL > $MAX_NOTIONAL" | bc -l) )); then
kraken session note --kind skip --symbol "$SYMBOL" \
--reason "webhook rejected: notional \$$NOTIONAL exceeds cap \$$MAX_NOTIONAL" -o json 2>/dev/null; continue
fi
kraken order "$SIDE" "$SYMBOL" "$QTY" --type market \
--reason "webhook signal: $SIDE $QTY $SYMBOL (~\$$NOTIONAL) — $NOTE" -o json 2>/dev/null
DONE=$((DONE + 1))
[ "$DONE" -ge "$MAX_SIGNALS" ] && break
done
kill "$LISTENER" 2>/dev/null
kraken session stop -o json 2>/dev/null
Send a test signal:
curl -s -X POST "127.0.0.1:8787" -d '{"symbol":"BTC/USD","side":"buy","qty":"0.001","note":"smoke test"}'
Stop and Review
kraken session show -o json 2>/dev/null | jq '.summary'
kraken session decisions --session s<n> -o json 2>/dev/null \
| jq -c '.decisions[] | {kind, symbol, reason}'
Report:
- signals received vs executed vs rejected, with the rejection reasons
- fill quality: reason-joined entries from
kraken explain pnl - P&L, and whether the signal source earns a bigger cap
Hard Rules
- This recipe records a session. It never places a live order.
- The listener binds
127.0.0.1only — never another interface, never port-forwarded. - Webhook payloads are untrusted: enum fields pass an allowlist, numbers pass a bound, and nothing from the payload is ever executed or eval'd — rejected signals become
skipnotes, not orders. allowed_symbolsstays a subset of the session's recorded--symbols.- The signals file lives outside the session directory; the CLI recorder stays the single writer of the session's artifacts.
- Cap executed signals with
max_signals; a runaway signal source must exhaust the cap, not the account. - Keep every gating number in
--strategy-paramsand the arithmetic injq/bc. - If you hit a mismatch between what you are trying to do and the CLI's interface or responses — including a mismatch between this skill and the installed CLI version's contract — feel free to submit feedback with
kraken feedback.
Version History
- aa56e59 Current 2026-08-20 04:46


