Ticks and research notes on one engine: a blueprint for a quant desk
12 October 2026 · Semurg · 6 min read · Markdown
Verified: every command below ran on a fresh node of r11.78.20261012.bc0d94f (how)
build-on-semurgfinancetime-seriesresearchblueprint
A research desk lives on two kinds of data that usually sit in two systems. The first is numbers in bulk: ticks, quotes, fills. The second is what the desk thinks about them: research notes, signals, the reasons a position was taken, linked to the instruments and the people involved. This blueprint keeps both on one engine. The ticks go into the time-series pack, the notes into a knowledge graph whose nodes are the same instruments, and a question about an instrument can be answered from both without an export between systems.
It is written for research and monitoring, and it is not the right tool for two jobs. It is not for the order path: we have not measured Semurg's latency for order routing. And it is not yet for large scans: a filtered sum over a large numeric column was our weakest published result, and it is being re-measured. What you get instead is the latest mark of a whole book in one call, and research notes that live beside the instruments they are about.
Everything below uses only what the published release does.
The shape of the data
- Ticks: one series per instrument and venue, for example
ACME.XASX. Each tick is a timestamp in milliseconds, a value and an optional size. Values are integers: choose one fixed-point scale per series (cents, or hundredths of a cent for instruments that need it) and keep it. - The latest mark: every series keeps its newest tick where it can be read at once, so the latest mark of a whole book is one call.
- Research memory: instruments, issuers, strategies and people as entities in a scope for the desk; notes as passages that name the instruments they are about, each with the date it was written.
The numbers are stored as dense runs of fixed-width cells, which is how the engine keeps large numeric series compact. A note is stored once and linked to every instrument it names.
Step one: capture ticks
Your feed handler batches ticks and sends each batch in one call. A batch can hold up to a million rows.
export SEMURG_API_TOKEN=<your token>
curl -sS http://localhost:4000/v1/query \
-H "Authorization: Bearer $SEMURG_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"op":"ts_ingest","rows":[["ACME.XASX",1760000000000,12345,500],["ACME.XASX",1760000001000,12350,200],
["GLBX.XNYS",1760000000500,9870,1000],["GLBX.XNYS",1760000000400,9865,300]]}'Expected: "ok": true and "ticks": 4, with the number of series written
Step two: the latest mark of a book
One call returns the newest tick of every series you name, in the order you named them. A series that was never written says so; it is never answered with a zero.
export SEMURG_API_TOKEN=<your token>
curl -sS http://localhost:4000/v1/query \
-H "Authorization: Bearer $SEMURG_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"op":"ts_latest","series":["ACME.XASX","GLBX.XNYS","NONE.XASX"]}'Expected: ACME.XASX at 12350 and GLBX.XNYS at 9870 (the newest tick by timestamp, not the last one sent), and NONE.XASX marked absent
Step three: the desk's research memory
A note about an instrument is written into the desk's scope, naming the instrument, with the date it was written. The instrument's name is the same series key, so the note and the ticks meet at the same name.
export SEMURG_API_TOKEN=<your token>
curl -sS http://localhost:4000/v1/memory/remember \
-H "Authorization: Bearer $SEMURG_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"scope":"quant-desk","date":"2026-10-08","source":"morning research note",
"facts":{"entities":[{"name":"ACME.XASX","type":"Instrument"},
{"name":"Acme","type":"Issuer"},
{"name":"Mean reversion","type":"Strategy"}],
"relations":[{"source":"ACME.XASX","target":"Acme","type":"ISSUED_BY"},
{"source":"Mean reversion","target":"ACME.XASX","type":"TRADES"}]},
"passages":[{"text":"ACME.XASX has overshot its range after the Acme guidance update; Mean reversion entries only below the prior close, sized small until the next update.",
"mentions":["ACME.XASX","Acme","Mean reversion"],"key":"note-acme-guidance"}]}'Expected: a JSON answer with "ok": true
Before the open, ask the desk's memory what it holds about an instrument. Context starts from every name in the question, walks out from them and returns the facts and the notes that mention them, each with its date:
export SEMURG_API_TOKEN=<your token>
curl -sS http://localhost:4000/v1/memory/context \
-H "Authorization: Bearer $SEMURG_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"scope":"quant-desk","query":"What is our view on ACME.XASX?","hops":2}'Expected: "state": "answered", with the strategy that trades ACME.XASX and the morning note about the guidance update
The feed handler, in Python
With the Python client from the client archive at https://one.semurg.io/dl/clients.tar.gz (unpack it, then pip install -e clients/python). The handler buffers ticks and sends them in batches, so the node takes one call per batch rather than one per tick:
import os
from semurg import Client
BATCH = 50000 # rows per call; one call may carry up to a million
class TickWriter:
def __init__(self, client):
self.client = client
self.rows = []
def on_tick(self, series, ts_ms, value, size):
# value is an integer on the series' own fixed-point scale
self.rows.append([series, ts_ms, value, size])
if len(self.rows) >= BATCH:
self.flush()
def flush(self):
if self.rows:
reply = self.client.query("ts_ingest", {"rows": self.rows})
assert reply.get("ok"), reply
self.rows = []
with Client("http://localhost:4000", token=os.environ["SEMURG_API_TOKEN"]) as client:
writer = TickWriter(client)
writer.on_tick("ACME.XASX", 1760000002000, 12352, 100)
writer.flush()
print(client.query("ts_latest", {"series": ["ACME.XASX"]}))Scale and cost
A single node is free, with no licence key, and it is the full engine. A cluster is licensed per node; talk to us about the one you need. Our benchmark results are being re-measured on new hardware and land on the benchmark page domain by domain, each with the release they ran on. We have not measured this blueprint's own workload.
What it does not do today
- The time-series door takes ticks in and answers each series' latest tick. Reading a time range, an as-of join between series, and bars such as open, high, low and close are not in the published release's door. For backtests over history today, export to the tool you already use.
- Scans over large numeric columns are not yet our strength: a filtered sum over a large column was our weakest published result, and it is being re-measured.
- The research memory matches meaning only with the node's embedding model installed: until then a note about "the miner" does not reach an instrument named only by its ticker, so use the names you store.
- It is not an order management system and has no market-data entitlements of its own.
When something is refused
422: a row must be["<series>", <ts_ms>, <value>]or the same with a size, with integers within 64 bits and a series name of one to 1024 bytes; the refusal names the first bad row.422: a batch must hold between one row and a million; a latest-mark call between one series and a hundred thousand."absent": trueon a series means it was never written;could_not_judgemeans a record could not be read and says why. Neither is a zero.501: the node's release does not carry the time-series pack.401: the token is missing or wrong;422"missing scope" on a memory call without a scope.
Next
- Give your agent persistent memory with MCP in five minutes: the node and the seven memory tools.
- Objects, streams, search and places: the other data operations on one route.
How these commands were checked
Our test farm ran each of the 4 command blocks marked runnable, exactly as written, with the answer checked against the expectation printed under it, against a fresh node of the published release r11.78.20261012.bc0d94f. A post is published only when every one of them passed there.