Skip to main content

Crate pond_rs

Crate pond_rs 

Source
Expand description

§pond-rs

Time series analytics for Rust: rolling windows, time and calendar buckets (time-zone and daylight-saving aware), joins, per-entity partitions, 109 technical-analysis studies, trading calendars, and live series that update as events arrive. Arrow and JSON in and out.

pond-rs is the Rust implementation of pond. The TypeScript packages (pond-ts, @pond-ts/financial) and this crate give the same results, bit for bit: a parity harness checks every operation against output written by pond-ts. Pick the language that fits the program; the numbers do not change.

[dependencies]
pond-rs = "0.1"

§Where to start

You want to…Start at
Build a series, or read one from Arrow or JSONTimeSeries::new, TimeSeries::read_ipc, TimeSeries::from_json
Roll a window over itTimeSeries::rolling, TimeSeries::rolling_with
Bucket it by minute, day, month… in a time zoneTimeSeries::aggregate with Sequence::every or Sequence::calendar
Do the same per host, symbol or userTimeSeries::partition_by
Line several series upTimeSeries::join, TimeSeries::align, TimeSeries::materialize
Compute RSI, Bollinger bands, VWAP…financial
Process events as they arrivelive (the live feature)

§What is in the crate

Every call returns Result; a PondError names the call and the column, so the input can be fixed without reading pond-rs source.

§Cargo features

FeatureDefaultWhat it does
parallelonIndependent output columns, partitions and loess rows run on several cores (rayon). Results are the same bits either way. Does nothing on wasm32.
liveonThe live module.

With default-features = false you get the batch core and financial, on one thread.

§Threads and memory

Threads. With the parallel feature (on by default; not on wasm32), the output columns of one rolling or aggregate call are computed on several cores (rayon’s pool; RAYON_NUM_THREADS sets its size). Each column is still computed by one thread, in order, so results are the same bits on any number of threads. Partitions of a PartitionedSeries run on several cores the same way: each partition is computed by one thread. Loess fits each row on its own, so its rows are spread over the cores.

Memory. Columns hold their values in Arrow buffers. Reading a gap-free Float64 field, a Utf8 / Dictionary<Int32, Utf8> field or a Boolean field from a one-batch Arrow file uses the file’s memory (or a Decimal128 field) uses the file’s memory without copying (the file’s bytes stay in memory while the column lives), and writing hands each column’s buffer to Arrow.

§Example

use pond_rs::{Agg, Column, Reducer, Sequence, TimeSeries, parse_time_in};

let series = TimeSeries::new(
    "cpu",
    vec![0, 20_000, 40_000, 60_000, 80_000],
    vec![Column::optional("load", [Some(0.5), Some(0.7), None, Some(0.9), Some(0.4)])],
)?;
let per_minute = series.aggregate(
    &Sequence::every("1m")?,
    &[
        Agg::new("load_avg", "load", Reducer::Avg),
        Agg::new("load_n", "load", Reducer::Count),
    ],
)?;
assert_eq!(per_minute.len(), 2);
assert_eq!(per_minute.column("load_n")?.numbers()?.get(0), Some(2.0)); // the gap is not counted

// Calendar days in New York: the day clocks spring forward is 23 hours.
let days = Sequence::calendar("day", "America/New_York")?;
let day = parse_time_in("2026-03-08", "America/New_York")?; // its start, as epoch ms
let keys = days.buckets_overlapping(day, day)?;
assert_eq!(keys.end[0] - keys.begin[0], 23 * 3_600_000);

§Results that are not a series

Latest state per entity. A ranked table (one row per symbol, sorted by score) is not a TimeSeries, which is always in time order with its key first. Reduce each partition’s last window with TimeSeries::tail and TimeSeries::reduce, then write the table with the re-exported arrow_array and arrow_ipc:

use std::sync::Arc;
use pond_rs::arrow_array::{ArrayRef, Float64Array, RecordBatch, StringArray};
use pond_rs::{Agg, Column, Reducer, TimeSeries, Value};

let bars = TimeSeries::new(
    "bars",
    vec![0, 0, 60_000, 60_000, 120_000, 120_000],
    vec![
        Column::required("symbol", vec!["A", "B", "A", "B", "A", "B"]),
        Column::required("close", vec![10.0, 50.0, 11.0, 50.0, 14.0, 49.0]),
    ],
)?;
let mut scores: Vec<(String, f64)> = Vec::new();
for (key, part) in bars.partition_by(&["symbol"])?.iter() {
    let symbol = key[0].as_ref().and_then(Value::as_str).unwrap_or("");
    let last = part.tail("2m")?.reduce(&[
        Agg::new("mean", "close", Reducer::Avg),
        Agg::new("close", "close", Reducer::Last),
    ])?;
    if let (Some(mean), Some(close)) = (last.number("mean")?, last.number("close")?) {
        scores.push((symbol.to_string(), close - mean));
    }
}
scores.sort_by(|a, b| b.1.abs().total_cmp(&a.1.abs()));
let table = RecordBatch::try_from_iter([
    ("symbol", Arc::new(StringArray::from_iter_values(scores.iter().map(|s| &s.0))) as ArrayRef),
    ("score", Arc::new(scores.iter().map(|s| s.1).collect::<Float64Array>()) as ArrayRef),
])
.expect("columns of one length");
assert_eq!(table.num_rows(), 2);
// tail("2m") keeps rows after 0: A's mean is 12.5, its close 14.
assert_eq!(scores[0], ("A".to_string(), 1.5));
// Write it with arrow_ipc::writer::FileWriter, as TimeSeries::write_ipc does.

Re-exports§

pub use arrow_array;
pub use arrow_ipc;
pub use arrow_schema;
pub use serde_json;

Modules§

financial
Technical-analysis studies for price bars (pond-ts @pond-ts/financial): moving averages and the studies built on them (MACD, price oscillator, Guppy, rainbow, TRIX, Schaff, Coppock, KST, …), bands (Bollinger, Keltner, STARC, ATR, Donchian, high-low, prime number, envelope), momentum (RSI, stochastics, Williams %R, Chande momentum, momentum, percent change, intraday momentum, awesome and ultimate oscillators, CCI, Fisher transform, psychological line, pretty good and prime number oscillators, QStick, relative vigor, centre of gravity, Chande forecast, true strength), trend (directional movement, Aroon, vortex, vertical horizontal filter, random walk index, SuperTrend, parabolic SAR, ATR trailing stop, ZigZag, Ichimoku, Elder impulse and ray, trend intensity, RAVI, swing index and its running total), rolling statistics and the z-score so far, each a method on TimeSeries that adds its number columns and keeps every row. Also trading calendars (TradingCalendar, SessionRules): the sessions an exchange trades, their lookups, and session tagging.
livelive
Live series: events pushed one batch at a time, and nodes built on them that update as events arrive (pond-ts LiveSeries, LiveView, LiveAggregation).

Structs§

Agg
One mapping entry: write reducer(source) to the output column output.
Align
Options for TimeSeries::align. Align::default() is pond-ts’s default: hold, sample each bucket at its start, over the series’ own time range.
ArrayColumn
A column of arrays (pond-ts kind: 'array'): each row holds a list of numbers, text and true/false values, mixed freely, or is a gap. An empty list is a value, not a gap. Numbers must be finite.
ArrayOptions
Where TimeSeries::array_aggregate and TimeSeries::array_explode write, and what kind of column they write (pond-ts’s { as, kind }). ArrayOptions::default() replaces the array column in place and works the kind out from the values.
BooleanColumn
A column of true/false values (pond-ts kind: 'boolean'). A gap is an Arrow null. Held as an Arrow Boolean array (one bit per row).
Change
Options for TimeSeries::diff_with, TimeSeries::rate_with and TimeSeries::pct_change_with. Change::default() is pond-ts’s default: the first row kept, each named column replaced by its change.
Column
A named value column: numbers, text or true/false values. Handed to TimeSeries::new, and read back with TimeSeries::column.
ColumnDef
One schema entry. required: false means the column may have gaps.
Decimal
One exact decimal: value × 10^-scale, so Decimal { value: 125, scale: 2 } is 1.25. Prints as a decimal (1.25). Equality compares value and scale as stored, so 1.25 at scale 2 is not equal to 1.250 at scale 3 (a column has one scale, so within a column this never arises).
DecimalColumn
A column of exact decimals: Arrow Decimal128(precision, scale). Each value is a whole number (i128) and the column has one scale, so 1.25 at scale 2 is stored as 125. A gap is an Arrow null.
EventKey
One key: a point in time, a time range or a labelled interval, in epoch milliseconds, as pond-ts Time / TimeRange / Interval. Used to look rows up (TimeSeries::bisect and friends) and given by Row::key. A plain i64 converts to a point in time. (A series’ whole key column is a Key.)
FillLimits
Caps on which runs of gaps TimeSeries::fill fills. A run that breaks a cap is left entirely unfilled, never partly (pond-ts { limit, maxGap }).
Float64Column
A column of f64 values where any non-finite value (NaN, ±inf) is a gap. Reading a gap gives None.
IntervalKeys
Interval keys: row i covers [begin[i], end[i]) and is labelled label[i] (for buckets made by a sequence, the label is the start).
Join
Options for TimeSeries::join and TimeSeries::join_many.
JsonOptions
Options for TimeSeries::from_json.
Materialize
Options for TimeSeries::materialize. Materialize::default() is pond-ts’s default: the last row of each bucket, the bucket’s start as the row’s time, over the series’ own time range.
PartitionedSeries
A series split into partitions by the values of one or more columns: TimeSeries::partition_by. Operations run on each partition separately (on several cores with the parallel feature) and return another PartitionedSeries; PartitionedSeries::collect puts the rows back into one series.
Pivot
How TimeSeries::pivot_by_group treats rows that share a time and a group, and which groups become columns.
Reduced
What TimeSeries::reduce gives: one value per output, read by name with the same calls as a Row, or as a Value with Reduced::get. A name not among the outputs, or an output of another kind, is an error; Ok(None) is a gap.
Rolling
Options for TimeSeries::rolling_with and TimeSeries::rolling_rows. Rolling::default() is pond-ts’s default: a trailing window, every row given a value.
RollingOver
Options for TimeSeries::rolling_over. RollingOver::default() is pond-ts’s default: a trailing window ending at each bucket’s start, over the series’ own time range, every bucket given a value.
Row
One row of a series, handed to TimeSeries::filter’s closure and yielded by TimeSeries::rows. Values are read by column name; a name the series does not have, or a column of another kind, is an error.
Sequence
An unbounded grid of buckets. Realised over a concrete range by Sequence::buckets_overlapping. Build one with Sequence::every or Sequence::calendar; the constructors are the only way in, so every Sequence is valid (a positive step, a known zone, a week start 1–7).
Smooth
How TimeSeries::smooth smooths a column, with its options. Build one with Smooth::ema, Smooth::ema_span, Smooth::moving_average or Smooth::loess, then add options:
StringColumn
A column of text values (pond-ts kind: 'string'). A gap is an Arrow null; an empty string is a value, not a gap.
Table
Columns of equal length without a key: what TimeSeries::by_column and TimeSeries::rolling_by_column give (pond-ts gives an array of records). Read a column by name, as on a series.
TimeRangeKeys
Time-range keys (pond-ts timeRange): row i spans begin[i] to end[i], epoch milliseconds.
TimeSeries
A time series: a name, a key column and value columns of equal length.
ValueSeries
A series keyed by a number axis instead of time (pond-ts ValueSeries): a cumulative distance, a strike price, a frequency. The axis has no gaps, every value is finite, and it never goes down (repeats are allowed). The value columns are as in a TimeSeries.

Enums§

AlignMethod
How TimeSeries::align fills each grid point.
Alignment
Where a rolling window sits around each row (pond-ts alignment).
ArrowKey
Which fields of an Arrow table hold the key, for TimeSeries::from_arrow. A plain &str or String means a time key of that name, so from_arrow(name, batches, "time") reads a time-keyed table.
At
Where TimeSeries::as_time places each row’s new time within its interval (pond-ts asTime({ at })).
Bins
How TimeSeries::by_column cuts a number column into bins (pond-ts { width, origin } or { edges, inclusive }).
CalendarUnit
A calendar unit for Sequence::calendar.
ColumnData
What a value column holds: numbers, exact decimals, text, true/false values, or arrays.
ColumnKind
What a schema column holds.
Fill
How TimeSeries::fill fills a run of gaps.
FirstRow
Whether TimeSeries::diff, TimeSeries::rate and TimeSeries::pct_change keep the first row, which has no row before it (pond-ts { drop }).
JoinType
Which rows a TimeSeries::join keeps (pond-ts type).
Keep
What TimeSeries::dedupe does with rows that share a key.
Key
The key column.
KeyOrder
What TimeSeries::from_json does when a row’s key sorts before the row above it, as pond-ts fromJSON({ ordering }).
Label
One interval label: a number or text, as pond-ts allows. (A whole column of them is Labels.)
Labels
Interval labels: numbers or text, one kind for the whole key, as in pond-ts. Every row has a label (no gaps). Build them with .into() from Vec<f64>, Vec<i64>, Vec<&str> or Vec<String>.
OnConflict
What to do when a value column name appears in more than one series (pond-ts onConflict).
Pick
Which row TimeSeries::materialize takes from a bucket with several.
PondError
What went wrong. Every message names the call and, where there is one, the column, so the caller can fix the input without reading pond-rs source.
Reducer
A built-in reducer.
RowFormat
How TimeSeries::to_json writes each row, as pond-ts toJSON({ rowFormat }).
Sample
How TimeSeries::sample picks rows.
Scalar
One element of an array cell: a number, text or a true/false value, as pond-ts’s ScalarValue. Numbers must be finite (checked when a series is built).
Value
One value of any column kind: a Fill::Value literal, or one part of a partition key (PartitionedSeries), where None stands for a gap.

Constants§

MAX_TIME_MS
Largest key time, in epoch milliseconds, either side of 1970: the range of a JavaScript Date (so of pond-ts), 100 million days. Keeping keys in it leaves room for any window or step arithmetic in i64. Checked on every series built from outside data; a derived series can go one bucket past it (aggregate of a row near the limit gives a bucket whose end is beyond it), as pond-ts’s does.
VERSION
The version of this crate.

Functions§

parse_duration
Parse a duration string to whole milliseconds.
parse_time
A time written as text, as epoch milliseconds, read the way pond-ts’s Time.parse(text) reads it. Same as parse_time_in with "UTC".
parse_time_in
A time written as text, as epoch milliseconds, read the way pond-ts’s Time.parse(text, { timeZone }) reads it:
tzdb_version
The version of the tz database bundled into pond-rs, e.g. "2026e".

Type Aliases§

PartitionKey
A partition key: one entry per partition column, None for a gap.
Result
Shorthand for Result<T, PondError>.