🚀 Wingfoil
Wingfoil is a blazingly fast, highly scalable stream processing framework designed for latency-critical use cases such as electronic trading and real-time AI systems. You define a graph of transformations over streams; Wingfoil drives their execution in a tightly scheduled DAG, either against live data or replayed history.
The Rust engine does the heavy lifting; this wingfoil package gives you the same graph
model, operators, and production-ready I/O adapters from Python.
Table of Contents
- Features
- Installation
- Quick Start
- Core Concepts
- Stream Operators
- Composing Streams:
Graph,bimap,CustomStream - Backtesting with Historical Mode
- Pandas Integration
- I/O Adapters
- Build from Source
- Release Status & Feedback
✨ Features
- Fast — ultra-low latency and high throughput with an efficient DAG execution engine written in Rust.
- Simple and obvious — define your graph with fluent operators; Wingfoil manages scheduling and data propagation.
- Backtesting out of the box — switch from real-time to historical replay by flipping a single flag.
- Production I/O adapters — CSV, KDB+, etcd, ZeroMQ, iceoryx2, FIX 4.4 (incl. TLS), Prometheus, and OTLP — ready to plug into your graph.
- Multi-language — Rust crate and Python package today, WASM/JS/TS planned.
📦 Installation
Wingfoil wheels are published for Linux, macOS, and Windows on CPython 3.8+.
Optional adapters require the matching server/library (KDB+, etcd, iceoryx2, a FIX counterparty, OTLP collector, etc.) but no additional Python dependencies — the adapter clients are compiled into the wheel.
⚡ Quick Start
[INFO wingfoil] 0.000_092 >> hello, world 1
[INFO wingfoil] 1.008_038 >> hello, world 2
[INFO wingfoil] 2.012_219 >> hello, world 3
run() blocks until the stop condition is reached. Pass any of:
| Argument | Type | Meaning |
|---|---|---|
realtime |
bool |
True uses wall-clock time; False is historical replay. |
start |
float | datetime |
Historical start (Unix-seconds float or UTC datetime). |
duration |
float | timedelta |
Stop after this many seconds of graph time. |
cycles |
int |
Stop after this many engine cycles. |
🧠 Core Concepts
- Stream — a time-stamped channel of values. Streams are produced by sources
(
ticker,constant, I/O adapters) and transformed with operators (.map,.filter,.distinct, ...). Every operator returns a newStream. - Node — anything schedulable. A
Streamis aNodethat also carries a value; pure side-effect sinks (.for_each,.csv_write,.zmq_pub, ...) return aNode. - Graph — a bundle of roots that share one engine run. Use
Graph([...])when you have several independent stream branches that must execute together (e.g. publisher + subscriber + monitoring). - Active vs. passive upstreams — an active upstream triggers downstream
execution on tick; a passive upstream is read but does not trigger. Most built-in
operators use active inputs;
.sample(trigger)is the common way to fire a stream from a different clock.
Run Modes
realtime=True— the engine tracks wall-clock time. Use with live inputs (sockets, brokers, iceoryx2, etc.).realtime=False— historical replay, driven by event timestamps. Ideal for backtests and deterministic tests. In historical mode the graph runs as fast as the CPU allows; time advances purely from source events.
🧰 Stream Operators
All methods are available on Stream instances. Examples assume
from wingfoil import ticker, constant, bimap, Graph.
Source operators
| Operator | Description |
|---|---|
ticker(period) |
Emit once every period seconds. Returns a Node. |
constant(value) |
Emit value once on the first cycle. |
Transforming values
| Operator | Description |
|---|---|
.map(f) |
Apply f(value) to each tick. |
.filter(pred) |
Drop values where pred(value) is false. |
.distinct() |
Drop consecutive duplicates. |
.difference() |
Emit current - previous. |
.delay(seconds) |
Replay values delayed by seconds. |
getattr(s, 'not')() |
Logical/arithmetic negation (the literal method name is not; invoke via getattr because not is a Python keyword). |
.limit(n) |
Pass through at most n values, then stop. |
.sample(trigger) |
Re-emit the current value on each trigger tick. |
Aggregation
| Operator | Description |
|---|---|
.count() |
Emit tick count: 1, 2, 3, ... |
.sum() |
Running sum (values must be numeric). |
.average() |
Running mean (values must be numeric). |
.buffer(n) |
Tumbling window of size n. |
.collect() |
Accumulate every value into a list emitted each cycle. |
.with_time() |
Pair each value with graph-time as (seconds, value). |
.dataframe() |
Collect [(time, value), ...] for pandas (see below). |
Observing and sinking
| Operator | Description |
|---|---|
.inspect(f) |
Call f(value) and pass the value through. |
.logged("label") |
INFO-log each value and pass it through. |
.for_each(f) |
Terminal sink: f(value, time) on every tick. |
getattr(s, 'finally')(f) |
Terminal sink: f(final_value) called once at shutdown (literal method name finally collides with Python's keyword, so use getattr). |
.peek_value() |
After run(), inspect the last emitted value. |
Execution
| Operator | Description |
|---|---|
.run(realtime, start=, duration=, cycles=) |
Build and run a one-root graph. |
Graph([...]).run(...) |
Build and run a multi-root graph. |
Example: most operators in one pipeline
=
🧱 Composing Streams: Graph, bimap, CustomStream
Graph — run several roots together
=
=
bimap — fuse two streams
= # 1, 2, 3, ...
= # 0.5 on every tick
CustomStream — write your own operator in Python
Subclass CustomStream and implement cycle():
"""Sum of upstream[i] * 10**i."""
=
return True
=
🕰️ Backtesting with Historical Mode
Pass realtime=False to drive the graph from source timestamps rather than the
wall clock. Add start= if your sources require a specific epoch start (such as
kdb_read), and cap the replay with duration= or cycles=.
=
# [1, 2, 3, 4, 5]
Historical mode is deterministic — it's the right mode for unit tests and strategy backtests.
🐼 Pandas Integration
wingfoil ships with two pandas helpers:
stream.dataframe()— collects(time, value)pairs into a list; combine withwingfoil.to_dataframeto materialise apandas.DataFrame.wingfoil.build_dataframe({"col": stream, ...})— aligns several.dataframe()streams by graph time.
=
=
=
=
# time price qty
# 0 0.0e+00 101 10
# 1 1.0e-02 102 10
# ...
A single-stream variant using to_dataframe:
=
=
🔌 I/O Adapters
All adapters are exposed from the top-level wingfoil module. Every write
method (csv_write, kdb_write, etcd_pub, zmq_pub, iceoryx2_pub,
otlp_push) returns a Node — drive it by calling .run(...).
CSV
Read a CSV file into a stream of dicts (keys = column headers, values = strings). The file must have a header row and a timestamp column encoded as integer nanoseconds since the Unix epoch.
=
# [{'time_ns': '...', 'sym': 'AAPL', ...}, ...]
Write a stream of dicts to CSV. Headers are inferred from the first dict; a
time column with graph-time nanoseconds is prepended automatically.
KDB+
Start a q process (q -p 5000) and create the target table:
test_trades:([]time:`timestamp$();sym:`symbol$();price:`float$();qty:`long$())
, , = , 5000,
# Write: each dict becomes one row; "columns" names the non-time columns.
# Read: time-sliced query; returns a stream of dicts.
# `start` and `duration` bound the replay window against the KDB time column.
=
Supported kdb_write column types: "symbol", "float", "long", "int",
"bool".
etcd
Start etcd (docker run --rm -p 2379:2379 gcr.io/etcd-development/etcd:v3.5.0).
=
# Publish: each dict = {"key": str, "value": bytes}, or a list of them per tick.
# Subscribe: snapshot + watch events under a prefix; each tick = list[event].
=
Event dicts have shape:
{"kind": "put"|"delete", "key": str, "value": bytes, "revision": int}.
ZeroMQ
Cross-language compatible — the Rust publisher/subscriber inter-operate with Python on both sides.
Direct mode — hard-coded address, no discovery infrastructure:
# zmq_pub.py
# zmq_sub.py
, =
=
=
zmq_sub returns (data_stream, status_stream). Each data_stream tick yields
list[bytes] of messages received that cycle. status_stream yields
"connected" / "disconnected".
etcd discovery — publishers register under a service name; subscribers look it up. Useful for dynamic deployments. Requires a running etcd.
# publisher
\
\
# subscriber
, =
For multi-host deployments where 127.0.0.1 isn't routable, use
zmq_pub_etcd_on(name, address, port, endpoint).
iceoryx2 (shared memory)
Zero-copy pub/sub over shared memory. Requires building wingfoil with the
iceoryx2 feature (opt-in; see Build from Source).
=
=
=
=
Both ends accept variant (Ipc for cross-process, Local for same-process),
history_size, and publisher-side initial_max_slice_len.
FIX protocol
FIX 4.4 initiator, TLS initiator, and acceptor. All return
(data_stream, status_stream); TLS additionally returns a sender object
for sending outbound messages.
, =
=
=
Each data tick yields a list[dict] where every dict is
{"msg_type": str, "seq_num": int, "fields": [(tag, value), ...]}.
Status values are "disconnected" | "logging_in" | "logged_in" or a dict
{"status": "logged_out"|"error", "reason"|"message": str}.
TLS initiator (e.g. LMAX) with a sender:
, , =
# Send a FIX message on the session:
Acceptor:
, =
Prometheus
Expose any stream as a gauge metric on a Prometheus-compatible /metrics
endpoint.
=
# bind and start the HTTP server
=
=
Scrape with curl http://localhost:9091/metrics.
OpenTelemetry OTLP
Push any stream's value to an OTLP HTTP collector as a gauge metric.
🛠️ Build from Source
Most users should pip install wingfoil. To build locally (e.g. to enable the
iceoryx2 feature or develop against the bindings), see
build.md.
📢 Release Status & Feedback
The Wingfoil Python module is currently a beta release. APIs are stabilising and we would love your input — especially if you:
- are interested in contributing,
- know of a project Wingfoil is a good fit for,
- want to request a feature, or
- have any feedback.
Email us at hello@wingfoil.io, open a GitHub discussion, or browse the issue tracker.
More resources:
- 📚 Python examples: https://github.com/wingfoil-io/wingfoil/tree/main/wingfoil-python/examples
- 🦀 Rust crate docs: https://docs.rs/wingfoil