Skip to main content

Finance

For quantitative research, backtesting and live trading across many instruments: equities, futures, foreign exchange, crypto and rates.

Your data, as Delve reads it​

You haveSendNotes
Trades with exchange or receive timestamps in nanosecondsBars built from trades, with time_unit: "ns"Times are kept exactly as sent; JSON carries them as strings
Vendor OHLC barsThe bars, as they areKeep the vendor's bar boundaries
QuotesBars of the mid, or of the bid and the ask as two series
A second, minute or session with no tradeNothing: a gapNever a flat filler bar
Spreads or returns that cross zeroThe series plus a fixed offsetRemove it when you use the segments
HorizonBar width that usually fits
Intraday execution and signals1 second to 1 minute
Swing and daily5 minutes to 1 hour
Research across years1 day

The plan for the job​

UsePlan
Research and backtestsProduction, with jobs reading Parquet or Arrow from your storage
Live signals feeding a trading systemMission Critical, gRPC extensions, callers in the jurisdiction's region or connected privately
A fund at full scale, or one whose regulator treats Patternode as a critical ICT third partyDedicated

Register your venues' trading hours as blackout periods, so Patternode changes nothing while you trade. Size capacity for the open and the close, when every instrument trades at once.

Backtest and live agree​

Pin the profile version (standard@2026-11). The same bars with the same version give the same segments, bit for bit, on every transport, region and day, for at least 24 months. So a backtest's segments are the segments you would have had live, and a model validated on them stays valid until you choose to move version.

Example​

examples/industries/finance/trades_to_delve.py reads invented trades for two instruments with nanosecond times, builds one-second bars (a second with no trade is a gap), backtests the day as a finished series and checks a second run is identical, then goes live from the backtest's edge, extending every instrument in one request as each second closes. The edges stay in your store.

python mock/mock_patternode.py &
PATTERNODE_API_KEY=pnk_live_BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB python examples/industries/finance/trades_to_delve.py
examples/industries/finance/trades_to_delve.py
"""Finance: trades into Delve, with nanosecond times, live and in backtest.

PATTERNODE_API_KEY=pnk_live_... python examples/industries/finance/trades_to_delve.py

Reads trades (symbol, time_ns, price, size) as a market data feed or a tick store gives them.
For each instrument it:

1. Builds one-second bars from trades, open and close the first and last trade, high and low
the extremes. A second with no trade is a gap, not a flat bar.
2. Keeps nanosecond times end to end (time_unit "ns", sent as strings in JSON).
3. Backtests: segments the whole day as a finished series with a pinned profile version, and
checks a second run gives the same segments, bit for bit.
4. Goes live from where the backtest ended: the finished series returns its edge, and as
each second closes one request extends every instrument from its stored edge. Patternode
keeps nothing; the edges live in your store. At full scale use the gRPC transport
(Mission Critical): the same operation on one connection.

The instruments and trades are invented.
"""

import csv
import sys
from collections import defaultdict
from pathlib import Path

HERE = Path(__file__).resolve().parent
sys.path.insert(0, str(HERE.parents[1] / "python"))

from patternode_client import Patternode # noqa: E402
from segment_store import SegmentStore # noqa: E402

BAR_NS = 1_000_000_000
PROFILE = "standard@2026-11" # pinned: a backtest must reproduce


def bars_from_trades(trades: list[tuple[int, float]]) -> list[list]:
buckets = defaultdict(list)
for t, px in trades:
buckets[t // BAR_NS].append((t, px))
bars = []
for _, ts in sorted(buckets.items()):
prices = [p for _, p in ts]
bars.append([str(ts[0][0]), str(ts[-1][0]), prices[0], max(prices), min(prices), prices[-1]])
return bars


def main(path: Path = HERE / "trades.csv", store_root: Path = Path("out")) -> None:
trades = defaultdict(list)
with path.open() as f:
for row in csv.DictReader(f):
trades[row["symbol"]].append((int(row["time_ns"]), float(row["price"])))
pn = Patternode()
store = SegmentStore(store_root / "fund")

live = {}
for symbol, ts in trades.items():
bars = bars_from_trades(ts)
history, tail = bars[:-20], bars[-20:]
name = f"eq/{symbol.lower()}/1s"
body = {"series": name, "profile": PROFILE, "time_unit": "ns", "bars": history, "return_edge": True}
a = pn.post("/v1/segmentations", body)
b = pn.post("/v1/segmentations", body)
same = a["segments"] == b["segments"] and a["edge"] == b["edge"]
print(f"{symbol}: {len(ts)} trades -> {a['bars']} bars -> {len(a['segments'])} segments, "
f"{a['profile_version']}, reproducible: {same}")
if not same:
raise SystemExit("two runs of one profile version differed")
store.apply_segmentation(name, a)
live[name] = tail

closed = 0
for i in range(20): # each second closes
names = list(live)
edges = [store.edge(n) for n in names]
items = [{"series": n, "profile": e.profile_version, "edge": e.token, "bars": [live[n][i]]}
for n, e in zip(names, edges)]
out = pn.post("/v1/segmentations/extend/batch", {"time_unit": "ns", "items": items})
for n, e, r in zip(names, edges, out["results"]):
if r["status"] != 200:
raise SystemExit(r["error"])
store.apply_extension(n, r["result"], expected_version=e.version)
closed += len(r["result"]["closed"])
print(f"live: {closed} segments closed across {len(live)} instruments; edges kept in your store")


if __name__ == "__main__":
main(Path(sys.argv[1]) if len(sys.argv) > 1 else HERE / "trades.csv")

What you will be asked​

QuestionAnswer
Tail latency?p99 10 ms on gRPC, Mission Critical, at the edge (Service levels)
Timestamp precision?Nanoseconds, untouched (Time)
Reproducible for model validation?Yes, per profile version (Determinism)
Change during trading hours?None, inside your blackout periods (Change control)
DORA and outsourcing terms?In the order form, with an exit plan (Compliance)
Does Patternode see our positions or strategies?No: only bars under names you choose, and it keeps none of them (Data boundary)