1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
//! # 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](https://pond-ts.org). 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.
//!
//! ```toml
//! [dependencies]
//! pond-rs = "0.1"
//! ```
//!
//! ## Where to start
//!
//! | You want to… | Start at |
//! |---|---|
//! | Build a series, or read one from Arrow or JSON | [`TimeSeries::new`], [`TimeSeries::read_ipc`], [`TimeSeries::from_json`] |
//! | Roll a window over it | [`TimeSeries::rolling`], [`TimeSeries::rolling_with`] |
//! | Bucket it by minute, day, month… in a time zone | [`TimeSeries::aggregate`] with [`Sequence::every`] or [`Sequence::calendar`] |
//! | Do the same per host, symbol or user | [`TimeSeries::partition_by`] |
//! | Line several series up | [`TimeSeries::join`], [`TimeSeries::align`], [`TimeSeries::materialize`] |
//! | Compute RSI, Bollinger bands, VWAP… | [`financial`] |
//! | Process events as they arrive | [`live`] (the `live` feature) |
//!
//! ## What is in the crate
//!
//! - **Series.** A [`TimeSeries`] is keyed by time, by time ranges or by
//! intervals labelled with numbers or text. Value columns hold numbers,
//! exact decimals, text, true/false values or arrays, and may have gaps
//! ([`Column`], [`ColumnData`]). Keys are always in order.
//! - **Windows and buckets.** [`TimeSeries::rolling`] over a span of time or
//! a count of rows (trailing, leading or centred), [`TimeSeries::aggregate`]
//! into fixed-length or calendar buckets, [`TimeSeries::reduce`] over the
//! whole series, with the built-in [`Reducer`]s (count, sum, avg, min,
//! max, percentiles, stdev, unique, top, …).
//! - **Row by row.** [`TimeSeries::diff`], `rate`, `pct_change`, `shift`,
//! `cumulative`, `fill`; [`TimeSeries::smooth`] (EMA, moving average,
//! loess), [`TimeSeries::baseline`] and `outliers`, a fold of your own
//! with [`TimeSeries::scan`].
//! - **Picking rows.** [`TimeSeries::filter`], `slice`, `within`, `trim`,
//! `before`, `after`, `tail`, and lookups by key: [`TimeSeries::bisect`],
//! `at_or_before`, `nearest`, `find`.
//! - **Reshaping.** [`TimeSeries::join`] / `join_many`, `concat`,
//! `pivot_by_group`, `collapse`, `dedupe`, `align`, `materialize`;
//! histograms with [`TimeSeries::by_column`]; rows keyed by a number
//! column instead of time with [`TimeSeries::by_value`] ([`ValueSeries`]).
//! - **Per entity.** [`TimeSeries::partition_by`] runs any of the above per
//! group, on several cores.
//! - **In and out.** Arrow IPC files and record batches
//! ([`TimeSeries::read_ipc`], [`TimeSeries::write_ipc`],
//! [`TimeSeries::to_arrow`]); pond's JSON formats (rows, columns,
//! chart points); times written as text with [`parse_time`] /
//! [`parse_time_in`].
//! - **Finance.** [`financial`]: 109 studies (moving averages, bands,
//! momentum, trend, volatility, volume, statistics, price, per session),
//! each checked against a pandas / TA-Lib oracle, trading calendars, and a
//! [`catalog`](financial::catalog) describing every study for menus and
//! agents.
//! - **Live.** [`live`]: a [`LiveSeries`](live::LiveSeries) takes events as
//! they arrive and keeps filters, rolling windows, bucket aggregations and
//! per-entity partitions up to date, returning what each push caused.
//!
//! 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
//!
//! | Feature | Default | What it does |
//! |---|---|---|
//! | `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. |
//! | `live` | on | The [`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);
//! # Ok::<(), pond_rs::PondError>(())
//! ```
//!
//! ## 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.
//! # Ok::<(), pond_rs::PondError>(())
//! ```
pub use Reduced;
pub use ArrayOptions;
pub use ArrowKey;
pub use ;
pub use ;
pub use parse_duration;
pub use ;
pub use ;
pub use ;
pub use ;
pub use ;
pub use Pivot;
pub use ;
pub use ;
pub use At;
pub use ;
pub use Row;
pub use ;
pub use ;
pub use ;
pub use ;
pub use ;
pub use ValueSeries;
// The README's example, run as a doctest so it cannot drift.
;
/// The version of this crate.
pub const VERSION: &str = env!;
/// The Arrow crates pond-rs reads and writes, re-exported so callers use the
/// same versions.
pub use ;