A platform for distributed quantitative computing. It ships with working pricing and risk workers, but the product is the two contracts underneath them: write a function that takes JSON and returns JSON, and it becomes a capability your whole firm can call.
What It Is
DistributeX is three things:
- A server that routes calculations to worker processes, authenticates callers, and enforces the concurrency each worker is allowed.
- Two contracts — one for workers, one for clients — both small enough to fit on this page.
- A set of built-in workers and clients: twenty-five pricing and risk calculations, and six client languages. These are a working starting point and a reference implementation. They are not the boundary of the system.
The last point is the one that matters. Most firms’ most valuable quantitative code is already written, in whichever language it was written in, and it is not going to be rewritten to join a platform. So the extension path is the product:
A worker is a table of functions. Each takes JSON and returns JSON.
A client is three HTTP calls.
That is the whole surface. Everything below shows it in real code.
The Worker Contract
A worker process loads plugins. A plugin exposes named methods. A method takes parameters as JSON and returns a result as JSON. That is all a plugin ever sees — no queues, no sockets, no threading.
The same shape exists in five languages:
| Language | The contract |
|---|---|
| C++ | DISTRIBUTEX_PLUGIN_METHODS({"name", fn}) over json(const json&) |
| Python | get_methods() -> {"name": callable(dict) -> dict} |
| Java | Map<String, Function<JsonNode, JsonNode>> getMethods() |
| C# | Dictionary<string, Func<JsonNode, JsonNode>> GetMethods() |
| VBA | GetMethods() -> Scripting.Dictionary of name → procedure |
No IDL, no code generation, no schema compiler. A model already written in one of those languages joins the cluster by being wrapped, not ported.
C++ — a complete plugin
#include <cmath>
#include <nlohmann/json.hpp>
#include "../worker_plugin_interface.h"
json price_zero_coupon(const json& params) {
try {
// Required inputs use .at(), so a missing one raises and becomes an
// error result — it never takes the worker down.
const double notional = params.at("notional").get<double>();
const double rate = params.at("rate").get<double>();
const double years = params.at("years").get<double>();
// Optional inputs get a documented default.
const double spread = params.value("spread", 0.0);
const double df = std::exp(-(rate + spread) * years);
return {{"pv", notional * df}, {"discount_factor", df}};
} catch (const std::exception& e) {
// A failed calculation is a RESULT. Return it, don't throw past the host.
return {{"error", std::string("price_zero_coupon: ") + e.what()}};
}
}
// The entire registration step.
DISTRIBUTEX_PLUGIN_METHODS(
{"price_zero_coupon", price_zero_coupon}
)
Build it as a shared module and drop it in the worker’s plugin directory:
add_library(my_curve MODULE cpp/worker/plugins/my_curve.cpp)
target_link_libraries(my_curve PRIVATE nlohmann_json::nlohmann_json)
set_target_properties(my_curve PROPERTIES PREFIX "" OUTPUT_NAME "my_curve")
sync_binary(my_curve "workers/cpp/plugins")
Link whatever you like into it — QuantLib, your firm’s in-house library, a vendor .so. The worker does not care what is behind the function.
Python — for anything numpy does better
"""my_analytics.py — drop into the worker's plugins directory."""
from typing import Any, Dict
import numpy as np
def portfolio_beta(params: Dict[str, Any]) -> Dict[str, Any]:
asset = params.get("asset_returns")
market = params.get("market_returns")
# Validate and return the reason. The caller can act on a message;
# it cannot act on a stack trace.
if not isinstance(asset, list) or not isinstance(market, list):
return {"error": "'asset_returns' and 'market_returns' must be lists"}
if len(asset) != len(market):
return {"error": f"length mismatch: {len(asset)} vs {len(market)}"}
if len(asset) < 2:
return {"error": "need at least two observations"}
a, m = np.asarray(asset, float), np.asarray(market, float)
# Both estimators use the same ddof. np.cov defaults to ddof=1 and np.var
# to ddof=0, so mixing the defaults inflates beta by n/(n-1) — 25% on five
# observations. Exactly the kind of quiet error worth being deliberate about.
variance = float(np.var(m, ddof=1))
if variance == 0.0:
return {"error": "market returns have zero variance; beta is undefined"}
return {
"beta": float(np.cov(a, m, ddof=1)[0, 1] / variance),
"observations": len(a),
}
def get_methods():
return {"portfolio_beta": portfolio_beta}
No build step. The worker discovers get_methods() and registers everything in it.
Java — for a model that already lives on the JVM
public class CreditAnalyticsPlugin implements WorkerPlugin {
private static final JsonNodeFactory F = JsonNodeFactory.instance;
@Override
public Map<String, Function<JsonNode, JsonNode>> getMethods() {
Map<String, Function<JsonNode, JsonNode>> m = new HashMap<>();
m.put("survival_probability", CreditAnalyticsPlugin::survivalProbability);
return m;
}
private static JsonNode survivalProbability(JsonNode params) {
try {
double hazard = required(params, "hazard_rate");
double years = required(params, "years");
ObjectNode r = F.objectNode();
r.put("survival_probability", Math.exp(-hazard * years));
r.put("default_probability", 1.0 - Math.exp(-hazard * years));
return r;
} catch (Exception e) {
return F.objectNode().put("error", e.getMessage());
}
}
private static double required(JsonNode p, String key) {
JsonNode v = p.get(key);
if (v == null || !v.isNumber()) {
throw new IllegalArgumentException("'" + key + "' is required and must be a number");
}
return v.asDouble();
}
}
Build to a JAR, drop it in the plugins folder. The host scans jars, instantiates anything implementing WorkerPlugin, and merges the maps.
C# — for a .NET analytics library
public class RatesPlugin : IWorkerPlugin {
public Dictionary<string, Func<JsonNode, JsonNode>> GetMethods() {
return new Dictionary<string, Func<JsonNode, JsonNode>> {
["forward_rate"] = Guarded(ForwardRate),
};
}
private static JsonNode ForwardRate(JsonNode p) {
double near = Required(p, "near_rate"), farR = Required(p, "far_rate");
double t1 = Required(p, "near_years"), t2 = Required(p, "far_years");
if (t2 <= t1) throw new ArgumentException("far_years must exceed near_years");
return new JsonObject {
["forward_rate"] = (farR * t2 - near * t1) / (t2 - t1),
["tenor_years"] = t2 - t1,
};
}
// Turns a throw into an error result, so the contract is kept by the
// plugin rather than relying on the host to catch for it.
private static Func<JsonNode, JsonNode> Guarded(Func<JsonNode, JsonNode> body) =>
input => {
try { return body(input); }
catch (Exception e) { return new JsonObject { ["error"] = e.Message }; }
};
private static double Required(JsonNode p, string key) =>
p is JsonObject o && o.TryGetPropertyValue(key, out var n)
&& n is JsonValue v && v.TryGetValue<double>(out var d)
? d : throw new ArgumentException($"'{key}' is required and must be a number");
}
One note that saves an afternoon: publish a C# plugin to its own subdirectory and do not give it its own System.Text.Json package reference. The framework ships that assembly, and a second copy beside your plugin loads into a separate context — which makes the JsonNode in your signature a different type from the host’s, and the runtime then tells you your GetMethods “does not have an implementation”.
VBA — because some models live in a spreadsheet
Public Function GetMethods() As Object
Dim d As Object
Set d = CreateObject("Scripting.Dictionary")
d.Add "desk_markup", "MyDeskPlugin.DeskMarkup"
Set GetMethods = d
End Function
Public Function DeskMarkup(ByVal paramsJson As String) As String
On Error GoTo Failed
Dim p As Object, res As Object
Set p = JsonConverter.ParseJson(paramsJson)
Set res = CreateObject("Scripting.Dictionary")
res.Add "marked_price", CDbl(p("mid")) * (1 + CDbl(p("markup_bp")) / 10000)
DeskMarkup = JsonConverter.ConvertToJson(res)
Exit Function
Failed:
Set res = CreateObject("Scripting.Dictionary")
res.Add "error", Err.Description
DeskMarkup = JsonConverter.ConvertToJson(res)
End Function
A model that only exists in a workbook can serve the rest of the firm without being rewritten first. It will be slower than the others — that is a fair trade for not rewriting it.
The two rules every worker follows
Answer every call, including the failures. A bad input comes back as {"error": "..."} that the caller can read. A client should never be left waiting out a timeout, unable to distinguish a broken calculation from a missing worker. All five examples above do this, and it is the single most important habit.
Declare whether your calculations are thread-safe. One line in the worker’s config:
{
"topic": "curves",
"execution": { "thread_safety": "safe", "concurrency": "pooled", "threads": 4 }
}
Say safe and one worker runs several of your calculations at once. Say unsafe — the right answer for anything touching QuantLib or any library with global state — and the server guarantees it is never entered twice at once. This is enforced, not advisory: a worker declaring unsafe and then asking for concurrency refuses to start.
Deploying it
- Drop the plugin in the worker’s plugin directory. That directory decides which plugins share a process, and therefore which execution mode applies to them — keep thread-unsafe engines in a directory of their own.
- Give the topic a worker identity in the registry so it can authenticate.
- Start the worker. Its banner lists the methods it loaded and the mode it is running in.
One rule to know before you start a second worker on the same topic: every worker on a topic must implement every method on it. Work is distributed across them, so a worker missing one method fails a fraction of requests rather than failing cleanly. Either load the same plugins everywhere on a topic, or give the new capability its own topic.
The Client Contract
Three HTTP calls. Any language that can make an HTTP request and parse JSON can be a client — there is no SDK you are obliged to use.
# 1. Authenticate once. Reuse the token; a login is deliberately expensive.
TOKEN=$(curl -s -X POST $DX/auth/client/login \
-H 'Content-Type: application/json' \
-d '{"id":"risk_desk","credential":"..."}' | jq -r .token)
# 2. Submit. Returns immediately with an id.
REQ=$(curl -s -X POST $DX/push-pull/request \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"topic":"curves","method":"price_zero_coupon",
"params":{"notional":1000000,"rate":0.04,"years":5}}' | jq -r .request_id)
# 3. Poll. 202 while pending, 200 with the result when done.
curl -s $DX/push-pull/result/$REQ -H "Authorization: Bearer $TOKEN"
# {"status":"success","result":{"pv":818730.75,"discount_factor":0.81873}}
Notice that nothing in those three calls knows which language the worker was written in, or which machine it ran on. Your new price_zero_coupon is now callable from every client below without one of them being changed or redeployed.
A client in twenty lines
Standard library only — no dependencies:
import json, time, urllib.request
DX = "http://localhost:8080"
def _post(path, body, token=None):
r = urllib.request.Request(
DX + path, data=json.dumps(body).encode(), method="POST",
headers={"Content-Type": "application/json",
**({"Authorization": "Bearer " + token} if token else {})})
with urllib.request.urlopen(r) as res:
return json.loads(res.read() or b"{}")
def _get(path, token):
r = urllib.request.Request(DX + path, headers={"Authorization": "Bearer " + token})
with urllib.request.urlopen(r) as res:
raw = res.read()
# On a POLL, 202 means "not ready yet" and carries nothing useful.
return {"status": "pending"} if res.status == 202 or not raw else json.loads(raw)
def login(client_id, credential):
return _post("/auth/client/login", {"id": client_id, "credential": credential})["token"]
def call(token, topic, method, params, timeout=60):
# Submit answers 202 as well — with the id in the body. Read the body
# regardless of the status here; only the poll treats 202 as "pending".
request_id = _post("/push-pull/request",
{"topic": topic, "method": method, "params": params},
token)["request_id"]
deadline, delay = time.monotonic() + timeout, 0.025
while True:
body = _get(f"/push-pull/result/{request_id}", token)
if body.get("status") in ("success", "completed"):
return body["result"]
if body.get("status") == "error":
raise RuntimeError(body["error"])
if time.monotonic() >= deadline:
raise TimeoutError(f"no result after {timeout}s")
time.sleep(delay)
delay = min(0.5, delay * 2) # back off; see below
Three things a client should get right
Both POSTs answer 202. Submitting returns 202 Accepted with the request
id in the body, and polling returns 202 with no body while the calculation
is still running. A client that treats every 202 as “nothing to see here” will
drop the id it just asked for. Read the body on submit; treat 202 as pending
only on the poll.
Back off between polls. Start around 25 ms and grow to a ceiling. Most calculations return in well under a millisecond, so a flat half-second poll is the cost of a request. We measured a batch go from 19.5 to 172 requests per second on this change alone, with no change to the cluster. Check your client before you buy hardware.
Retry a failed poll, not a failed calculation. A poll is an idempotent GET, so a reset connection is indistinguishable from “not ready yet” — treat it as pending. A {"status":"error"} body is a different thing entirely: that is the calculation telling you what was wrong with the arguments, and retrying gets the same answer.
Clients you can start from
Six are included, and each is a worked example of the contract above rather than a black box:
| Language | Shape |
|---|---|
| Python | client.run(topic, method, params), plus a thread pool for a whole book |
| JavaScript | One dependency-free module; runs under Node and in a browser |
| C++ | Native client with a batch mode for a whole portfolio |
| C# and Java | HTTP and WebSocket clients |
| Excel / VBA | Worksheet functions — the pattern below |
The Excel one is worth seeing, because it shows how far a client can be from the server’s idea of a client:
=DX_BOND_DV01(100, 0.04, 0.035, 2, 10) ' 0.0858
=DX_OPTION_GREEK(100, 105, 0.01, 0.2, "2027-06-30", "call", "delta")
Errors arrive as readable text in the cell — DX error: 'yield' is required and must be a number — not as #VALUE!. Wrapping your own method in a worksheet function is a dozen lines of the same pattern.
Your Methods Become Agent Tools
DistributeX exposes calculations to an LLM agent as tools over the Model Context Protocol, so a researcher can ask in English and have the model compute against your real library rather than recall a formula. Add a method and a catalogue entry, and it is available to the agent alongside everything else.
Ask about a three-leg option book and you get six pricing calls and this, in under a minute:
Net delta = −12.33 shares. The book is essentially delta-flat. Net vega = +$482.99 per vol point.
Rather than Taylor-expand, I repriced all three legs at spot 103. Net P&L = −$10.57. For reference the delta-gamma approximation gives −$9.93; the $0.64 gap is third-order and confirms the full reprice is the number to use.
The honest summary: the delta and the spot move are both near-irrelevant to this book. It is a long-vega position, and that is where the risk should be managed.
The question asked about delta. The answer priced the book, checked its own approximation against a full reprice, and then pointed out that delta was not the risk.
One boundary worth being direct about: the agent has no filesystem, no shell, and no capability other than the calculations you have given it. It authenticates as an ordinary client, so it is subject to the same permissions and audit trail as a human user — you scope what it can reach by editing its topic list, not by trusting a prompt.
What Ships In The Box
The built-in workers are a reference set, and a useful one: twenty-five calculations across the instruments most firms need on day one.
| Area | Included |
|---|---|
| Equity options | European and American pricing, full Greek set from one call |
| FX options | Pricing, implied volatility, spot × volatility risk grids |
| Bonds | Price, yield, duration, convexity, key rate durations, effective risk |
| Rates | Swap NPV, par rate, annuity, DV01 |
| Credit | CDS pricing, CS01, recovery sensitivity, credit curve bootstrapping |
| Portfolio risk | Historical, parametric and Monte Carlo VaR with Expected Shortfall; component attribution; stress scenarios; scenario revaluation; P&L attribution |
Two conventions they follow, worth copying in your own: one option call returns the whole Greek set, so you never make five calls for five Greeks; and the uncertainty comes back with the number — Monte Carlo VaR returns its standard error, and P&L attribution returns the unexplained residual rather than only the part it could explain.
Scaling It
Capacity is worker processes. Add them where the load is, on one machine or across many, and the server distributes work to whichever are free.
- Per topic. Put six workers on the option book and two on overnight VaR; change the ratio without touching a client.
- Per class. Thread-unsafe workers scale by process count. Thread-safe workers scale by process count and by threads inside each.
- Across machines. Nothing ties a worker to the server’s host. Add a box, point its workers at the server, they join the rotation.
- Without downtime. Several workers on a topic means you can restart one.
What the numbers actually say
Measured on a single machine: 54,120 option repricings took 10.07 s with one pricing worker and 7.94 s with three — 1.27×, not 3×.
That is a statement about the test, not a ceiling on the architecture. A Black-Scholes reprice here is sub-millisecond, and the test drove everything from one client process; the client saturated long before three workers did. Adding workers to a cluster already answering faster than one caller can ask changes nothing.
Worker count shows where real load lives: genuinely heavy calculations, and many clients at once rather than one in a loop. And if a run turns out to be round-trip-bound rather than compute-bound, more hardware is the wrong answer — send fewer, larger requests instead. An FX risk grid prices an entire spot-by-volatility surface in one call rather than one call per point.
We would rather tell you which of those you are before you buy anything, which is the sort of thing the consulting engagement is for.
Where It Runs
On your own infrastructure. A single machine for one desk; worker processes across as many machines as the load needs, for a firm.
No cloud dependency, no pricing data leaving your network perimeter, and no third-party data processing agreement for pricing workloads. The same principle behind everything else we build: the software runs where your data already lives.
What It Does Not Do
The honest edges, because a platform you cannot check is a platform you should not build on.
Nothing is persisted. A server restart loses in-flight requests. Calculations are pure functions of their arguments, so a client can resubmit — but plan for that rather than expecting a durable queue.
TLS terminates in front of it. There is no TLS on the client-facing surface itself; put a reverse proxy there and do not expose the port.
A plugin is code running in your worker process. Loading one is arbitrary code execution by design — that is what makes the platform extensible. Treat the plugin directory with the same care as any other place your firm’s binaries live.
A thread-safe declaration is a promise you make. The server enforces the concurrency your declaration implies, and it will hold you to serial execution if you ask for it — but it cannot read your code and tell you which answer is true. Declaring a stateful plugin safe is the one mistake the design cannot catch for you.
Getting Started
The fastest way to know whether this fits is to wrap one model you already have. A single method, in whichever language it is already written in, called from a spreadsheet by the desk that owns it — that is an afternoon, and it tells you more than any evaluation document.
Deploying the platform properly is an infrastructure decision as much as a software one: which hardware, which models move first, how it fits the systems you already run, and what a compliance review needs to see. That is what our enterprise AI consulting is for. Get in touch and we will scope it against your environment.