pond_rs/lib.rs
1//! # pond-rs
2//!
3//! Time series analytics for Rust: rolling windows, time and calendar
4//! buckets (time-zone and daylight-saving aware), joins, per-entity
5//! partitions, 109 technical-analysis studies, trading calendars, and live
6//! series that update as events arrive. Arrow and JSON in and out.
7//!
8//! pond-rs is the Rust implementation of [pond](https://pond-ts.org). The
9//! TypeScript packages (`pond-ts`, `@pond-ts/financial`) and this crate
10//! give the same results, bit for bit: a parity harness checks every
11//! operation against output written by pond-ts. Pick the language that
12//! fits the program; the numbers do not change.
13//!
14//! ```toml
15//! [dependencies]
16//! pond-rs = "0.1"
17//! ```
18//!
19//! ## Where to start
20//!
21//! | You want to… | Start at |
22//! |---|---|
23//! | Build a series, or read one from Arrow or JSON | [`TimeSeries::new`], [`TimeSeries::read_ipc`], [`TimeSeries::from_json`] |
24//! | Roll a window over it | [`TimeSeries::rolling`], [`TimeSeries::rolling_with`] |
25//! | Bucket it by minute, day, month… in a time zone | [`TimeSeries::aggregate`] with [`Sequence::every`] or [`Sequence::calendar`] |
26//! | Do the same per host, symbol or user | [`TimeSeries::partition_by`] |
27//! | Line several series up | [`TimeSeries::join`], [`TimeSeries::align`], [`TimeSeries::materialize`] |
28//! | Compute RSI, Bollinger bands, VWAP… | [`financial`] |
29//! | Process events as they arrive | [`live`] (the `live` feature) |
30//!
31//! ## What is in the crate
32//!
33//! - **Series.** A [`TimeSeries`] is keyed by time, by time ranges or by
34//! intervals labelled with numbers or text. Value columns hold numbers,
35//! exact decimals, text, true/false values or arrays, and may have gaps
36//! ([`Column`], [`ColumnData`]). Keys are always in order.
37//! - **Windows and buckets.** [`TimeSeries::rolling`] over a span of time or
38//! a count of rows (trailing, leading or centred), [`TimeSeries::aggregate`]
39//! into fixed-length or calendar buckets, [`TimeSeries::reduce`] over the
40//! whole series, with the built-in [`Reducer`]s (count, sum, avg, min,
41//! max, percentiles, stdev, unique, top, …).
42//! - **Row by row.** [`TimeSeries::diff`], `rate`, `pct_change`, `shift`,
43//! `cumulative`, `fill`; [`TimeSeries::smooth`] (EMA, moving average,
44//! loess), [`TimeSeries::baseline`] and `outliers`, a fold of your own
45//! with [`TimeSeries::scan`].
46//! - **Picking rows.** [`TimeSeries::filter`], `slice`, `within`, `trim`,
47//! `before`, `after`, `tail`, and lookups by key: [`TimeSeries::bisect`],
48//! `at_or_before`, `nearest`, `find`.
49//! - **Reshaping.** [`TimeSeries::join`] / `join_many`, `concat`,
50//! `pivot_by_group`, `collapse`, `dedupe`, `align`, `materialize`;
51//! histograms with [`TimeSeries::by_column`]; rows keyed by a number
52//! column instead of time with [`TimeSeries::by_value`] ([`ValueSeries`]).
53//! - **Per entity.** [`TimeSeries::partition_by`] runs any of the above per
54//! group, on several cores.
55//! - **In and out.** Arrow IPC files and record batches
56//! ([`TimeSeries::read_ipc`], [`TimeSeries::write_ipc`],
57//! [`TimeSeries::to_arrow`]); pond's JSON formats (rows, columns,
58//! chart points); times written as text with [`parse_time`] /
59//! [`parse_time_in`].
60//! - **Finance.** [`financial`]: 109 studies (moving averages, bands,
61//! momentum, trend, volatility, volume, statistics, price, per session),
62//! each checked against a pandas / TA-Lib oracle, trading calendars, and a
63//! [`catalog`](financial::catalog) describing every study for menus and
64//! agents.
65//! - **Live.** [`live`]: a [`LiveSeries`](live::LiveSeries) takes events as
66//! they arrive and keeps filters, rolling windows, bucket aggregations and
67//! per-entity partitions up to date, returning what each push caused.
68//!
69//! Every call returns [`Result`]; a [`PondError`] names the call and the
70//! column, so the input can be fixed without reading pond-rs source.
71//!
72//! ## Cargo features
73//!
74//! | Feature | Default | What it does |
75//! |---|---|---|
76//! | `parallel` | on | Independent output columns, partitions and loess rows run on several cores (rayon). Results are the same bits either way. Does nothing on wasm32. |
77//! | `live` | on | The [`live`] module. |
78//!
79//! With `default-features = false` you get the batch core and
80//! [`financial`], on one thread.
81//!
82//! ## Threads and memory
83//!
84//! **Threads.** With the `parallel` feature (on by default; not on wasm32),
85//! the output columns of one `rolling` or `aggregate` call are computed on
86//! several cores (rayon's pool; `RAYON_NUM_THREADS` sets its size). Each
87//! column is still computed by one thread, in order, so results are the same
88//! bits on any number of threads. Partitions of a [`PartitionedSeries`] run
89//! on several cores the same way: each partition is computed by one thread.
90//! Loess fits each row on its own, so its rows are spread over the cores.
91//!
92//! **Memory.** Columns hold their values in Arrow buffers. Reading a
93//! gap-free `Float64` field, a `Utf8` / `Dictionary<Int32, Utf8>` field or a
94//! `Boolean` field from a one-batch Arrow file uses the file's memory
95//! (or a `Decimal128` field) uses the file's memory without copying (the
96//! file's bytes stay in memory while the column lives), and writing hands
97//! each column's buffer to Arrow.
98//!
99//! ## Example
100//!
101//! ```
102//! use pond_rs::{Agg, Column, Reducer, Sequence, TimeSeries, parse_time_in};
103//!
104//! let series = TimeSeries::new(
105//! "cpu",
106//! vec![0, 20_000, 40_000, 60_000, 80_000],
107//! vec![Column::optional("load", [Some(0.5), Some(0.7), None, Some(0.9), Some(0.4)])],
108//! )?;
109//! let per_minute = series.aggregate(
110//! &Sequence::every("1m")?,
111//! &[
112//! Agg::new("load_avg", "load", Reducer::Avg),
113//! Agg::new("load_n", "load", Reducer::Count),
114//! ],
115//! )?;
116//! assert_eq!(per_minute.len(), 2);
117//! assert_eq!(per_minute.column("load_n")?.numbers()?.get(0), Some(2.0)); // the gap is not counted
118//!
119//! // Calendar days in New York: the day clocks spring forward is 23 hours.
120//! let days = Sequence::calendar("day", "America/New_York")?;
121//! let day = parse_time_in("2026-03-08", "America/New_York")?; // its start, as epoch ms
122//! let keys = days.buckets_overlapping(day, day)?;
123//! assert_eq!(keys.end[0] - keys.begin[0], 23 * 3_600_000);
124//! # Ok::<(), pond_rs::PondError>(())
125//! ```
126//!
127//! ## Results that are not a series
128//!
129//! **Latest state per entity.** A
130//! ranked table (one row per symbol, sorted by score) is not a
131//! [`TimeSeries`], which is always in time order with its key first. Reduce
132//! each partition's last window with [`TimeSeries::tail`] and
133//! [`TimeSeries::reduce`], then write the table with the re-exported
134//! [`arrow_array`] and [`arrow_ipc`]:
135//!
136//! ```
137//! use std::sync::Arc;
138//! use pond_rs::arrow_array::{ArrayRef, Float64Array, RecordBatch, StringArray};
139//! use pond_rs::{Agg, Column, Reducer, TimeSeries, Value};
140//!
141//! let bars = TimeSeries::new(
142//! "bars",
143//! vec![0, 0, 60_000, 60_000, 120_000, 120_000],
144//! vec![
145//! Column::required("symbol", vec!["A", "B", "A", "B", "A", "B"]),
146//! Column::required("close", vec![10.0, 50.0, 11.0, 50.0, 14.0, 49.0]),
147//! ],
148//! )?;
149//! let mut scores: Vec<(String, f64)> = Vec::new();
150//! for (key, part) in bars.partition_by(&["symbol"])?.iter() {
151//! let symbol = key[0].as_ref().and_then(Value::as_str).unwrap_or("");
152//! let last = part.tail("2m")?.reduce(&[
153//! Agg::new("mean", "close", Reducer::Avg),
154//! Agg::new("close", "close", Reducer::Last),
155//! ])?;
156//! if let (Some(mean), Some(close)) = (last.number("mean")?, last.number("close")?) {
157//! scores.push((symbol.to_string(), close - mean));
158//! }
159//! }
160//! scores.sort_by(|a, b| b.1.abs().total_cmp(&a.1.abs()));
161//! let table = RecordBatch::try_from_iter([
162//! ("symbol", Arc::new(StringArray::from_iter_values(scores.iter().map(|s| &s.0))) as ArrayRef),
163//! ("score", Arc::new(scores.iter().map(|s| s.1).collect::<Float64Array>()) as ArrayRef),
164//! ])
165//! .expect("columns of one length");
166//! assert_eq!(table.num_rows(), 2);
167//! // tail("2m") keeps rows after 0: A's mean is 12.5, its close 14.
168//! assert_eq!(scores[0], ("A".to_string(), 1.5));
169//! // Write it with arrow_ipc::writer::FileWriter, as TimeSeries::write_ipc does.
170//! # Ok::<(), pond_rs::PondError>(())
171//! ```
172
173#![warn(missing_docs)]
174#![cfg_attr(docsrs, feature(doc_cfg))]
175
176mod aggregate;
177mod array;
178mod arrow;
179mod bycolumn;
180mod column;
181mod decimal;
182mod duration;
183mod elements;
184mod error;
185pub mod financial;
186mod grid;
187mod join;
188mod json;
189mod json_columns;
190mod json_text;
191#[cfg(feature = "live")]
192pub mod live;
193mod par;
194mod partition;
195mod pivot;
196mod query;
197mod reducer;
198mod reshape;
199mod rolling;
200mod rows;
201mod rowwise;
202mod sequence;
203mod series;
204mod smooth;
205mod span;
206mod time_parse;
207mod value_series;
208
209pub use aggregate::Reduced;
210pub use array::ArrayOptions;
211pub use arrow::ArrowKey;
212pub use bycolumn::{Bins, Table};
213pub use column::{
214 ArrayColumn, BooleanColumn, ColumnData, Decimal, DecimalColumn, Float64Column, Scalar,
215 StringColumn, Value,
216};
217pub use duration::parse_duration;
218pub use error::{PondError, Result};
219pub use grid::{Align, AlignMethod, Keep, Materialize, Pick};
220pub use join::{Join, JoinType, OnConflict};
221pub use json::{JsonOptions, KeyOrder, RowFormat};
222pub use partition::{PartitionKey, PartitionedSeries};
223pub use pivot::Pivot;
224pub use query::{EventKey, Label};
225pub use reducer::{Agg, Reducer};
226pub use reshape::At;
227pub use rolling::{Alignment, Rolling, RollingOver};
228pub use rows::Row;
229pub use rowwise::{Change, Fill, FillLimits, FirstRow};
230pub use sequence::{CalendarUnit, Sequence, tzdb_version};
231pub use series::{
232 Column, ColumnDef, ColumnKind, IntervalKeys, Key, Labels, MAX_TIME_MS, TimeRangeKeys,
233 TimeSeries,
234};
235pub use smooth::{Sample, Smooth};
236pub use time_parse::{parse_time, parse_time_in};
237pub use value_series::ValueSeries;
238
239// The README's example, run as a doctest so it cannot drift.
240#[cfg(doctest)]
241#[doc = include_str!("../README.md")]
242struct ReadmeDoctests;
243
244/// The version of this crate.
245pub const VERSION: &str = env!("CARGO_PKG_VERSION");
246
247/// The Arrow crates pond-rs reads and writes, re-exported so callers use the
248/// same versions.
249pub use {arrow_array, arrow_ipc, arrow_schema, serde_json};