ExeQute
An Elixir client for KDB+, the high-performance time-series database used in financial markets. ExeQute handles the KDB+ IPC wire protocol, type system, and pub/sub — letting you query and subscribe to KDB+ instances from any Elixir application.
The name is a play on Exir + Q (the KDB+ query language) + execute.
Installation
def deps do
[
{:exe_qute, "~> 0.1.0"}
]
endQuerying
One-shot query
Opens a connection, runs the query, closes the connection. Good for infrequent or one-off queries.
{:ok, result} = ExeQute.query("select from trade", host: "kdb-host", port: 5010)Persistent connection
{:ok, conn} = ExeQute.connect(host: "kdb-host", port: 5010)
{:ok, result} = ExeQute.query(conn, "select from trade")
{:ok, result} = ExeQute.query(conn, "select from trade where sym=`AAPL")
ExeQute.disconnect(conn)Named connections
Register a connection under an atom so any part of your application can use it without passing the pid around.
ExeQute.connect(host: "kdb-host", port: 5010, name: :trades)
{:ok, result} = ExeQute.query(:trades, "select from trade")Parameterized queries
Pass typed arguments directly rather than interpolating them into strings. Arguments are encoded as KDB+ types on the wire.
{:ok, result} = ExeQute.query(conn, "{x + y}", [1, 2])
{:ok, result} = ExeQute.query(conn, ".myns.getquotes", ["USD/JPY", ~D[2024-01-01]])Publishing (fire-and-forget)
Send data to a KDB+ function asynchronously — no response is expected. Useful for writing to feed handlers or triggering side-effects.
:ok = ExeQute.publish(conn, ".feed.upd", ["trade", rows])Connection options
| Option | Default | Description |
|---|---|---|
:host | "localhost" | KDB+ server hostname or IP |
:port | 5001 | KDB+ server port |
:username | nil | Username (omit for unauthenticated servers) |
:password | nil | Password |
:timeout | 5000 | Connection and query timeout in ms |
:encoding | "utf8" | Character encoding for string data |
:name | nil | Register connection under this atom name |
Type mapping
Decoding (KDB+ → Elixir)
| KDB+ type | Elixir type |
|---|---|
| boolean | true / false |
| short, int, long | integer() |
| real, float | float() |
| char | <<byte>> |
| symbol | String.t() |
| timestamp | %DateTime{} (UTC, microsecond precision) |
| date | %Date{} |
| time, minute, second | %Time{} |
| timespan | integer() (nanoseconds) |
| list | [term()] |
| dictionary | %{term() => term()} |
| table | [%{String.t() => term()}] (list of row maps) |
| keyed table | [%{String.t() => term()}] (key and value columns merged) |
| null values | nil |
infinity (0Wf, -0Wf) | :infinity / :neg_infinity |
Encoding (Elixir → KDB+)
| Elixir value | KDB+ type |
|---|---|
true / false | boolean |
integer() | long (64-bit) |
float() | float (64-bit) |
String.t() | char vector |
atom() |
symbol (e.g. :AAPL → `AAPL) |
%DateTime{} | timestamp |
%Date{} | date |
%Time{} |
time (e.g. ~T[17:00:00]) |
[term()] | generic list |
%{term() => term()} | dictionary |
Types not listed (char atom, short/int atoms, timespan, minute, second, guid) cannot be sent as typed parameters. Use an inline q expression string for those cases.
Pub/Sub
ExeQute supports KDB+ tickerplant subscriptions. One TCP connection to the tickerplant is shared across any number of subscribing processes in your application.
Process-based subscriptions
Each subscribing process receives {:exe_qute, table, data} messages in its own mailbox. This works naturally in LiveViews, GenServers, or any handle_info-capable process.
defmodule MyApp.TradeHandler do
use GenServer
def start_link(opts), do: GenServer.start_link(__MODULE__, opts, name: __MODULE__)
def subscribe do
ExeQute.subscribe("trade", host: "tp-host", port: 5010)
ExeQute.subscribe("quote", host: "tp-host", port: 5010)
end
def unsubscribe do
ExeQute.unsubscribe("trade", host: "tp-host", port: 5010)
ExeQute.unsubscribe("quote", host: "tp-host", port: 5010)
end
@impl true
def init(_opts), do: {:ok, %{}}
@impl true
def handle_info({:exe_qute, "trade", data}, state) do
IO.inspect(data, label: "trade")
{:noreply, state}
end
def handle_info({:exe_qute, "quote", data}, state) do
IO.inspect(data, label: "quote")
{:noreply, state}
end
end
{:ok, _pid} = MyApp.TradeHandler.start_link([])
MyApp.TradeHandler.subscribe()
Process.sleep(30_000)
MyApp.TradeHandler.unsubscribe()Named subscriber
For applications that want explicit control over the subscriber lifecycle — for example, to start it in a supervision tree.
defmodule MyApp.TradeHandler do
use GenServer
def start_link(opts), do: GenServer.start_link(__MODULE__, opts, name: __MODULE__)
def subscribe do
ExeQute.subscribe(:tp, "trade")
ExeQute.subscribe(:tp, "quote")
end
def unsubscribe do
ExeQute.unsubscribe(:tp, "trade")
ExeQute.unsubscribe(:tp, "quote")
end
@impl true
def init(_opts), do: {:ok, %{}}
@impl true
def handle_info({:exe_qute, table, data}, state) do
IO.inspect({table, data})
{:noreply, state}
end
end
ExeQute.Subscriber.start_link(host: "tp-host", port: 5010, name: :tp)
{:ok, _pid} = MyApp.TradeHandler.start_link([])
MyApp.TradeHandler.subscribe()
Process.sleep(30_000)
MyApp.TradeHandler.unsubscribe()Callback-based subscriptions
For cases where you want a function called directly rather than receiving mailbox messages.
ExeQute.Subscriber.start_link(host: "tp-host", port: 5010, name: :tp)
{:ok, trade_ref} = ExeQute.subscribe(:tp, "trade", fn {table, data} ->
IO.inspect({table, data})
end)
{:ok, quote_ref} = ExeQute.subscribe(:tp, "quote", ["AAPL", "MSFT"], fn {_table, data} ->
IO.inspect(data)
end)
Process.sleep(30_000)
ExeQute.unsubscribe(:tp, trade_ref)
ExeQute.unsubscribe(:tp, quote_ref)Multiple subscribers, one connection
Multiple processes can subscribe to the same table. Only one .u.sub is sent to the tickerplant regardless of how many local processes subscribe. When the last subscriber unsubscribes, .u.unsub is sent automatically.
# All three processes receive {:exe_qute, "trade", data} independently
ExeQute.subscribe(:tp, "trade") # from LiveView A
ExeQute.subscribe(:tp, "trade") # from LiveView B
ExeQute.subscribe(:tp, "trade") # from a GenServerStarting the subscriber in a supervision tree
defmodule MyApp.Application do
use Application
def start(_type, _args) do
children = [
{ExeQute.Subscriber, host: "tp-host", port: 5010, name: :tp},
MyApp.TradeHandler
]
Supervisor.start_link(children, strategy: :one_for_one)
end
endIntrospection
KDB+ instances often accumulate years of q functions across many namespaces. ExeQute lets you explore a live instance programmatically — useful for building admin dashboards, documentation generators, or dynamic query builders that adapt to whatever functions a given server exposes.
Results are cached per-connection after the first call, so repeated introspection is free. The cache is tied to the connection process and clears automatically when it dies. Call ExeQute.refresh_introspection/1 to force a fresh fetch after deploying new code to KDB+.
Namespaces
{:ok, ns} = ExeQute.namespaces(conn)
#=> [".myns", ".feed", ".util", ".q", ".Q", ".h"]Functions
Returns each function’s name, parameter list, and source body. The body is the verbatim q source — useful for displaying what a function does without leaving Elixir, or for building lightweight documentation tooling around a KDB+ instance.
{:ok, fns} = ExeQute.functions(conn, ".util")
#=> [
#=> %{
#=> "name" => ".util.getquotes",
#=> "params" => ["sym", "start", "end"],
#=> "body" => "{[sym;start;end] select from quote where sym=sym, date within (start;end)}"
#=> },
#=> %{
#=> "name" => ".util.lasttrade",
#=> "params" => ["sym"],
#=> "body" => "{[sym] last select from trade where sym=sym}"
#=> }
#=> ]Omit the namespace argument to list functions in the root namespace:
{:ok, fns} = ExeQute.functions(conn)Variables and tables
{:ok, vars} = ExeQute.variables(conn, ".myns")
{:ok, tables} = ExeQute.tables(conn)Refreshing the cache
ExeQute.refresh_introspection(conn)Livebook integration (work in progress)
Interactive explorer
ExeQute.Explorer renders a QStudio-style widget inside a Livebook cell — connect to a server, browse namespaces, inspect tables and functions, run ad-hoc queries, and see results as tables or charts, all without leaving the notebook.
ExeQute.Explorer.new(host: "kdb-host", port: 5010)Results can be captured and used in subsequent cells:
ExeQute.Explorer.new()
# ... interact in the UI, assign result to "my_data" ...
my_data = ExeQute.Explorer.get("my_data")Live chart widget
ExeQute.EChart is a low-overhead Apache ECharts widget for Livebook, designed for high-frequency streaming data. It is the rendering backend used by the KDB+ Chart smart cell and can be driven directly when building custom subscription callbacks.
chart = ExeQute.EChart.new(height: 400)
ExeQute.EChart.render(chart, initial_options)
ExeQute.subscribe(:tp, "trade", fn {_table, raw} ->
rows = ExeQute.to_rows(raw)
cfg = %{x_field: "time", x_type: :temporal, y_field: "price",
y_type: :quantitative, color_field: "", chart_type: :line, window: 500}
{buf, _} = ExeQute.EChart.update_buffer({[], %{}}, rows, cfg)
ExeQute.EChart.push(chart, ExeQute.EChart.options_from_buffer(cfg, {buf, %{}}))
end)Note: Both modules are functional but not yet fully polished — APIs may change in future releases.
Error handling
All public functions return tagged tuples — no exceptions reach your code under normal operation.
case ExeQute.query(:trades, "select from trade") do
{:ok, result} -> handle(result)
{:error, :not_connected} -> reconnect()
{:error, :timeout} -> retry()
{:error, reason} -> Logger.error(inspect(reason))
end
Errors returned by KDB+ itself (e.g. 'type) are returned as {:error, {:kdb_error, "type"}}.