Reamer Stream — Documentation

Wire Protocol

This is the actual interop contract: everything an external tool needs to write its own client, in any language, without linking Reamer Stream's C++ engine directly. reamer_py links that engine directly too, as an in-process embedding for its own backtest engine's speed — that's an implementation detail of reamer_py, not a second, undocumented way to reach the same data. Everyone else — a Databento connector, an Alpaca connector, a research engine that isn't reamer_py — uses this protocol.

v1 scope: historical query/ingestion, plus live pub/sub

Paste into Claude, GPT, or any AI assistant to give it full Reamer Stream context for writing a client.

Security model

Same-machine trust boundary; TLS doesn't apply. A Unix domain socket is local IPC, not a network protocol — there is no listening TCP port, no routable address, no network hop for a packet to ever traverse. The socket file on disk is the entire attack surface, not a network endpoint.

Two independent layers, both worth understanding before writing a client:

Socket file permissions

reamer-stream sets the socket file to 0600 (owner read/write only) immediately after bind() — not left to whatever the process's umask happens to produce. This restricts access between users: only the account the server runs as (and root) can even open the socket.

Optional shared-secret token

File permissions alone don't restrict access between processes running as the same user. Setting the REAMER_STREAM_AUTH_TOKEN environment variable before starting reamer-stream closes that gap: every new connection must then send an Auth message with a matching token as its first frame — anything else sent first gets the connection closed immediately, no response. Unset (the default): no auth required. There is no --auth-token=<...> CLI flag deliberately — a secret value passed via argv is visible to any other process on the machine (/proc/<pid>/cmdline, ps), which would defeat the token's own purpose.

What this does and doesn't cover. 0600 permissions stop a different OS user from ever opening the socket. The optional token additionally stops a different process running as the same user. Neither stops a full compromise of the account the server itself runs as. There's no encryption of data in flight, because there's no network hop for it to cross.

At rest. .bin files, .reamer result files, and audit.log are all stored unencrypted. Encrypting them — e.g. filesystem-level encryption at the deploying firm's own OS layer — is explicitly the deploying firm's responsibility, not something Reamer Stream does itself.

Transport & configuration

A Unix domain socket, same machine only — no TCP/remote option exists in v1. Default path: /tmp/reamer-stream.sock, overridable via reamer-stream --socket-path=<path>.

FlagDefaultNotes
--data-dir=<path>REAMER_STREAM_DATA_DIR env var, else ./dataWhere .bin files are read/written. If you also run reamer_py against the same data, point it at this same directory (reamer_py.set_data_dir()).
--socket-path=<path>/tmp/reamer-stream.sockThe socket external tools connect to.

Precedence for the data directory: --data-dir flag, then REAMER_STREAM_DATA_DIR, then ./data.

The server also caps concurrent connections at 256 (not configurable) — a same-machine ceiling, not sized for internet-facing exposure. A connection opened past the cap is accepted then immediately closed with no data sent.

Framing

FieldSizeNotes
version1 byteMust be 0x01. Server closes the connection immediately on any other value.
length4 bytes, unsigned, big-endianByte length of payload that follows.
payloadlength bytesMessage-type-specific.

There is no pagination in either direction — a HistoricalResponse can be arbitrarily large, and an IngestCsv/IngestBars request is capped only at a generous 128 MB. Read exactly length bytes before attempting to parse the payload; a large message may arrive across multiple reads.

All multi-byte integers are big-endian. All floating-point fields are IEEE 754 binary64, also big-endian — exactly what struct.pack('>d', x) does in Python. Never treat the wire bytes as directly castable to a C struct, even on a little-endian machine. A string field is length-prefixed: 2 bytes unsigned big-endian length, then that many raw UTF-8 bytes, no null terminator.

QueryHistorical / HistoricalResponse

Client → server: QueryHistorical

FieldSizeNotes
message_type1 byte0x00
tickerstringExact ticker name, case-sensitive.
has_start1 byte0x00 or 0x01.
start_ts8 bytes, signedEpoch seconds (UTC), inclusive. Meaningful only when has_start=1.
has_end1 byte0x00 or 0x01.
end_ts8 bytes, signedEpoch seconds (UTC), inclusive. Meaningful only when has_end=1.

has_start=0 means "from the first bar available"; has_end=0 means "through the last bar available." Both 0 returns the ticker's entire series.

Server → client: HistoricalResponse

FieldSizeNotes
message_type1 byte0x00
found1 byte0x01 if the ticker is known, 0x00 if not.
bar_count4 bytes, unsignedNumber of bar records that follow. 0 when found=0, and legitimately also 0 when the ticker is known but nothing falls in range — check found first.
barsbar_count × 64 bytesSee below.

Each bar record, in order (64 bytes total — an explicit field-by-field big-endian encoding, not a raw copy of the in-memory struct):

FieldSize
ts8 bytes, signed (epoch seconds, UTC)
open, high, low, close, volume, notional8 bytes each, double
tick_count8 bytes, signed (-1 = not supplied)

Bars are returned in ascending ts order.

IngestBars & IngestCsv

IngestBars is for a connector that already has structured bars — a Databento or Alpaca connector pulling from a vendor API that returns typed records directly. IngestCsv is for anything with a CSV file and no structured-bar step of its own — the server parses it using the same auto-detecting parser every other Reamer Stream ingestion path uses.

FieldSizeNotes
message_type1 byte0x01 (IngestBars) or 0x02 (IngestCsv)
tickerstringOverwrites any existing data under the same ticker.
bar_count / csv_length4 bytes, unsignedNumber of bar records, or byte length of csv_bytes.
bars / csv_bytesvariableSame 64-byte bar layout as HistoricalResponse, or the raw CSV file content.

Server → client: IngestAck

FieldSizeNotes
message_type1 byte0x01
success1 byte0x01 if written and now queryable, 0x00 otherwise.
reasonstringEmpty on success; a human-readable failure description otherwise.

Ingested data is immediately queryable via QueryHistorical — including on the same connection, right after the IngestAck.

Beyond the 128 MB single-request cap, IngestBars/IngestCsv also share a process-wide 512 MB ceiling on combined ingestion memory in flight at once. A request that would push the total over that ceiling gets IngestAck{success=false} with a reason describing the budget exceeded, rather than proceeding and risking an OOM. A single request never trips this on its own.

Subscribe / Unsubscribe / Publish

A stream name is a plain string with no relationship to a ticker unless a caller chooses to reuse the same string for both.

MessageFields
Subscribe / Unsubscribemessage_type (0x03/0x04), stream (string)
Publishmessage_type (0x05), stream (string), bar (64 bytes)
SubscriptionAck / PublishAckmessage_type (0x02/0x03), success (1 byte), reason (string)

Subscribing twice to the same stream on the same connection is harmless (idempotent). Publishing to a stream with zero current subscribers is not a failure — PublishAck{success=true} either way; the record is simply dropped. No persistence: a subscriber not listening at publish time never sees that record.

Server → client: PushedRecord (unsolicited)

FieldSizeNotes
message_type1 byte0x04
streamstringThe stream name this record was published to.
bar64 bytesSame 8-field layout as HistoricalResponse's bars.
This is the one message type that arrives unsolicited — sent the moment any connection publishes to a stream this one is subscribed to. A client can no longer assume "one frame in, one frame out" — read the message_type byte of every incoming frame and route PushedRecords separately from whatever response is currently pending.

Auth

MessageFields
Client → server: Authmessage_type (0x06), token (string — must exactly match REAMER_STREAM_AUTH_TOKEN)
Server → client: AuthAck (success only)message_type (0x05), success (always 0x01), reason (always empty)

A wrong token gets no AuthAck at all — the connection is simply closed, so a peer can't distinguish "wrong token" from "malformed frame" from the outside.

Concurrency semantics

reamer-stream is single-threaded: StreamServer::poll() is one epoll_wait loop, and every accepted event — including the blocking StreamEngine call inside it — runs to full completion before the loop returns to epoll_wait for the next one. There is no thread pool, no per-connection thread.

A query on one connection can never observe a torn/partial view of an ingest happening on another connection. IngestBars/IngestCsv replace the engine's entire bar index in one step, and because the server never runs two requests at once, a QueryHistorical on connection B while an IngestBars is in flight on connection A always sees either the complete pre-ingest data or the complete post-ingest data — never a mix.

What this does not claim: no ordering guarantee between two connections' requests that both arrive before the server gets to either one — which one epoll_wait happens to report first is unspecified. The guarantee is about atomicity of each individual request, not about which one wins a race to be first.

This is a property of the single-threaded design, not a lock: StreamEngine itself has no internal synchronization and is not safe to call concurrently with no external locking — reamer-stream's own single-threaded dispatch is what makes the guarantee above hold.

Failure modes

A client can tell which of two categories a failed request falls into from the response shape alone: silence (connection closes, nothing sent back) always means the request itself couldn't be trusted enough to interpret; an explicit success=false/found=false field always means the request was understood and something at the data layer said no.

Connection closed, no response sent

TriggerNotes
Wrong protocol version byteAny value other than 0x01.
Oversized payloadDeclared length exceeds the 128 MB request cap.
Truncated/malformed payload fieldChecked identically for every request type.
Unrecognized message_type byteNot one of the eight message types this protocol defines.
Non-Auth message before authenticatingOnly when REAMER_STREAM_AUTH_TOKEN is configured.
Wrong Auth tokenNo AuthAck sent either way.
Connection count at the 256-connection capAccepted then immediately closed, zero bytes sent.

Normal ack/response with a failure field, connection stays open

TriggerClient-observable result
Unknown ticker on QueryHistoricalHistoricalResponse{found=false, bar_count=0} — not an error.
Known ticker, empty result rangeHistoricalResponse{found=true, bar_count=0} — check found first.
Ingest budget ceiling exceededIngestAck{success=false, reason=<budget message>}
IngestCsv parse failureIngestAck{success=false, reason=<parser message>}
Ingest write failureIngestAck{success=false, reason=<error message>}
Publish to a stream with no subscribersPublishAck{success=true} — not a failure.
Redundant Subscribe/UnsubscribeSubscriptionAck{success=true} — idempotent.

Worked example

Request: QueryHistorical for ticker "AAPL", no bounds:

Frame header: 01 00 00 00 19 Payload (25 bytes): 00 message_type = QueryHistorical 00 04 ticker length = 4 41 41 50 4C "AAPL" 00 has_start = false 00 00 00 00 00 00 00 00 start_ts (unused) 00 has_end = false 00 00 00 00 00 00 00 00 end_ts (unused)

Response: ticker found, one bar (ts=1700000000, all price/volume fields 0.0, tick_count=1):

Frame header: 01 00 00 00 46 Payload (70 bytes): 00 message_type = HistoricalResponse 01 found = true 00 00 00 01 bar_count = 1 00 00 00 00 65 53 F1 00 ts = 1700000000 00 00 00 00 00 00 00 00 open = 0.0 00 00 00 00 00 00 00 00 high = 0.0 00 00 00 00 00 00 00 00 low = 0.0 00 00 00 00 00 00 00 00 close = 0.0 00 00 00 00 00 00 00 00 volume = 0.0 00 00 00 00 00 00 00 00 notional = 0.0 00 00 00 00 00 00 00 01 tick_count = 1