This document defines the communication protocol for Tier 2 process strategies in SpringBot.
Tier 2 strategies run as managed subprocesses of the SpringBot daemon. Instead of compiling to WASM, you write a standalone program in any language (Python, Node.js, Go, Rust, Java, etc.) that communicates with the daemon over stdin/stdout using newline-delimited JSON (JSON Lines).
Advantages over Tier 1 (WASM):
- Use any language and any library
- Full I/O access: load ML models, read files, call external APIs
- Use existing ecosystems (NumPy, TensorFlow, scikit-learn, etc.)
- Easier debugging with stderr logging
Tradeoffs:
- No sandbox -- the process runs with the same OS permissions as the daemon
- Local only -- process strategies cannot be distributed through the marketplace
- Slightly higher latency per evaluation (JSON serialization over pipes vs. direct memory access)
- Resource consumption is not bounded by the engine
The daemon and strategy communicate using newline-delimited JSON (JSON Lines) over the process's stdin and stdout:
- stdin (daemon writes, strategy reads): The daemon sends request messages
- stdout (strategy writes, daemon reads): The strategy sends response messages
- stderr (strategy writes): Captured by the daemon and logged with the strategy ID as prefix. Use this for debugging output.
Each message is a single JSON object on one line, terminated by \n. No pretty-printing. No blank lines between messages.
Sent once immediately after the process starts. The strategy must respond with its manifest.
{"type": "manifest"}Sent on each tick with the current market data snapshot. The data field contains an EvaluateInput object (see data-types.md) sliced to only the fields your strategy declared in its manifest requires.
{"type": "evaluate", "data": {"candles": [...], "ticker": {...}}}Health check. The strategy must respond with a pong.
{"type": "ping"}Sent when the bot is stopping. The strategy should clean up resources and exit. After sending this message, the daemon waits up to 5 seconds for the process to exit before killing it.
{"type": "shutdown"}Response to a manifest request. The data field contains the strategy's manifest.
{"type": "manifest", "data": {"id": "my-strategy", "name": "My Strategy", "version": "1.0.0", "requires": {"candles": {"timeframe": "5m", "lookback": 50}, "ticker": true}}}See data-types.md for the full manifest schema.
Response to an evaluate request. The data field contains the trading signal.
{"type": "signal", "data": {"direction": "BUY", "confidence": 0.85}}Valid directions: "BUY", "SELL", "HOLD" (case-sensitive).
Confidence must be a float between 0.0 and 1.0 (inclusive).
Response to a ping request.
{"type": "pong"}-
One JSON object per line. Do not pretty-print. Do not split a message across multiple lines. Each message must be a single line terminated by
\n. -
Always flush stdout after writing. Many languages buffer stdout by default when writing to a pipe. If you don't flush, the daemon will never see your response and the evaluate call will time out. Examples:
- Python:
sys.stdout.flush()after eachprint(), or useprint(..., flush=True) - Node.js:
process.stdout.write()flushes automatically - Go:
bufio.Writerneeds explicitFlush() - Java:
System.out.flush()or usePrintStreamwith auto-flush
- Python:
-
The process is long-lived. The daemon spawns your process once when the bot starts and keeps it alive across all ticks until the bot stops. Do not exit after handling a single message. Read from stdin in a loop.
-
Respond to every message. The daemon expects a response for
manifest,evaluate, andpingmessages. If you don't respond, the call will time out. The only exception isshutdown, which does not expect a response. -
Handle
shutdowngracefully. When you receive a shutdown message, clean up any resources (close files, connections, etc.) and exit with code 0. If you don't exit within 5 seconds, the daemon will kill the process. -
Don't write non-protocol data to stdout. The daemon reads stdout as JSON Lines. If you write debug output, log messages, or anything else to stdout, it will be parsed as (invalid) JSON and cause errors. Use stderr for all non-protocol output.
The daemon uses a 30-second default deadline for each evaluate call. If your strategy does not respond within 30 seconds, the evaluate is treated as a failure. The supervision layer will:
- Log the timeout
- Return a
HOLDsignal with confidence0.0for that tick - Increment the failure counter
After 3 consecutive failures, the circuit breaker opens and the strategy returns HOLD for a 30-second backoff period. After the backoff, the daemon will attempt to use the strategy again.
The manifest request uses a 10-second timeout.
Process strategies must include a manifest.json file in their directory. This file is read by the daemon to determine how to launch the process. It is separate from the protocol manifest response (which the running process sends over stdout).
{
"id": "my-ml-strategy",
"name": "My ML Strategy",
"version": "1.0.0",
"runtime": "process",
"command": ["python3", "main.py"],
"author": "yourname",
"description": "An ML-based trading strategy using scikit-learn",
"requires": {
"candles": { "timeframe": "15m", "lookback": 100 },
"ticker": true,
"portfolio": true
}
}| Field | Type | Description |
|---|---|---|
id |
string |
Unique strategy slug (e.g. "my-ml-strategy"). Must be unique across all installed strategies. |
name |
string |
Human-readable display name. |
version |
string |
Semver version string (e.g. "1.0.0"). |
runtime |
string |
Must be "process". |
command |
string[] |
Argv array for spawning the process. The first element is the executable, the rest are arguments. Executed relative to the strategy directory. |
requires |
object |
Data requirements. Same schema as the manifest requires field (see data-types.md). |
| Field | Type | Description |
|---|---|---|
author |
string |
Strategy author name. |
description |
string |
Human-readable description of what the strategy does. |
Install a process strategy from a local directory:
springbot strategies install --path ./my-strategy/The directory must contain a valid manifest.json. The daemon copies the entire directory to its strategy storage location. The command in the manifest is executed relative to the installed directory.
Make sure the command is available on the system. For example, if your strategy uses python3, ensure Python 3 is installed and on the PATH of the user running the daemon.
#!/usr/bin/env python3
import sys
import json
def handle_manifest():
return {
"type": "manifest",
"data": {
"id": "simple-momentum",
"name": "Simple Momentum",
"version": "1.0.0",
"requires": {
"candles": {"timeframe": "5m", "lookback": 20},
"ticker": True
}
}
}
def handle_evaluate(data):
candles = data.get("candles", [])
if len(candles) < 10:
return {"type": "signal", "data": {"direction": "HOLD", "confidence": 0.0}}
# Simple momentum: compare current close to close 10 candles ago
current = candles[-1]["close"]
previous = candles[-10]["close"]
change = (current - previous) / previous
if change > 0.02:
return {"type": "signal", "data": {"direction": "BUY", "confidence": min(abs(change) * 10, 1.0)}}
elif change < -0.02:
return {"type": "signal", "data": {"direction": "SELL", "confidence": min(abs(change) * 10, 1.0)}}
else:
return {"type": "signal", "data": {"direction": "HOLD", "confidence": 0.0}}
def main():
for line in sys.stdin:
line = line.strip()
if not line:
continue
msg = json.loads(line)
msg_type = msg["type"]
if msg_type == "manifest":
response = handle_manifest()
elif msg_type == "evaluate":
response = handle_evaluate(msg.get("data", {}))
elif msg_type == "ping":
response = {"type": "pong"}
elif msg_type == "shutdown":
break
else:
continue
print(json.dumps(response), flush=True)
if __name__ == "__main__":
main()