Skip to main content

Crate volas

Crate volas 

Source
Expand description

§volas

English | 简体中文

A Rust-backed, OHLCV-shaped DataFrame for candlestick / market time-series, with a technical-indicator directive engine.

volas is intentionally narrow — not a general-purpose DataFrame. It targets live OHLCV pipelines: compute an indicator by naming its directive ("ma:20", "macd.signal", "close > open"), and resample / cumulate bars to a coarser time frame.

[dependencies]
volas = "1"

There is also a Python package named volas — the same Rust kernels exposed via PyO3. See the volas Python project on GitHub and volas on PyPI. It is a separate distribution from this crate; the directive vocabulary is identical across both.

§Quick start

use volas::{Column, DataFrame};
use volas::directive::{execute, parse};

// Build a frame from named columns (here just `close`).
let df = DataFrame::new(
    vec!["close".to_string()],
    vec![Column::f64(vec![1.0, 2.0, 3.0, 4.0])],
    None,
).unwrap();

// Compute an indicator by its directive string. `ma:2` is the 2-period SMA.
let directive = parse("ma:2").unwrap();
let ma = execute(&df, &directive).unwrap();

assert!(!ma.is_valid(0));             // the warm-up row is NA (no value yet)
assert_eq!(ma.to_f64_vec()[3], 3.5);  // (3.0 + 4.0) / 2

§The data model

A DataFrame is a set of equal-length, named, typed Columns over a shared row Index. A Column is a contiguous typed buffer; any cell can be NA (missing) independently of its value — see Missing values below.

use volas::{Column, DataFrame, DType};

let df = DataFrame::new(
    vec!["open".to_string(), "close".to_string()],
    vec![
        Column::f64(vec![10.0, 11.0, 12.0]),
        Column::f64(vec![10.5, 10.5, 13.0]),
    ],
    None, // None => a default 0..n RangeIndex
).unwrap();

assert_eq!(df.height(), 3);
assert_eq!(df.width(), 2);
assert_eq!(df.names(), &["open".to_string(), "close".to_string()]);

let close = df.column("close").unwrap();   // -> &Column
assert_eq!(close.dtype(), DType::F64);
assert_eq!(close.to_f64_vec()[2], 13.0);    // owned Vec<f64>
assert_eq!(close.as_f64(), Some(&[10.5, 10.5, 13.0][..])); // borrowed slice (F64 only)

Column constructors: Column::f64, Column::i64, Column::bool, Column::str. Read values with Column::to_f64_vec (owned), Column::as_f64 (borrowed, None for non-f64), Column::is_valid (per-cell NA check), Column::len, and Column::dtype.

§Missing values (NA)

A cell can be missing independently of its value. Float columns use NaN as the NA marker; integer / bool / string columns carry an explicit validity mask. Check missingness per cell with is_valid, and count it with null_count.

use volas::Column;
use volas::core::Validity;

// Float columns: NaN is the missing marker.
let prices = Column::f64(vec![1.0, f64::NAN, 3.0]);
assert_eq!(prices.null_count(), 1);
assert!(prices.is_valid(0));
assert!(!prices.is_valid(1));         // cell 1 is NA
assert!(prices.get_f64(1).is_nan());  // reading an NA f64 yields NaN

// Integer / bool / string columns carry a validity mask instead of a sentinel.
let volume = Column::i64_with(
    vec![100, 0, 300],
    Validity::from_valid_iter(3, [true, false, true]), // cell 1 is NA
);
assert_eq!(volume.null_count(), 1);
assert!(!volume.is_valid(1));

Indicator warm-up rows are NA too — e.g. a 2-period SMA has one NA row at the head:

use volas::{Column, DataFrame};
use volas::directive::{execute, parse};

let df = DataFrame::new(
    vec!["close".to_string()],
    vec![Column::f64(vec![1.0, 2.0, 3.0])],
    None,
).unwrap();
let ma = execute(&df, &parse("ma:2").unwrap()).unwrap();

assert!(!ma.is_valid(0));        // the warm-up row is NA
assert_eq!(ma.null_count(), 1);

§Indicators via directives

The directive engine is the primary way to compute indicators: parse a directive string into an Ast, then execute it against a DataFrame to get a Column. This covers every built-in indicator uniformly — see the full reference of all directives in INDICATORS.md.

use volas::{Column, DataFrame};
use volas::directive::{execute, parse, stringify};

let n = 40;
let df = DataFrame::new(
    vec!["open".to_string(), "close".to_string()],
    vec![
        Column::f64((1..=n).map(|x| x as f64).collect()),
        Column::f64((1..=n).map(|x| x as f64 + 0.5).collect()),
    ],
    None,
).unwrap();

// Single-output directive -> one Column.
let rsi = execute(&df, &parse("rsi:14").unwrap()).unwrap();
assert_eq!(rsi.len(), 40);

// Multi-output indicator: address each line by its sub-command.
let macd   = execute(&df, &parse("macd").unwrap()).unwrap();
let signal = execute(&df, &parse("macd.signal").unwrap()).unwrap();
let hist   = execute(&df, &parse("macd.histogram").unwrap()).unwrap();
assert_eq!((macd.len(), signal.len(), hist.len()), (40, 40, 40));

// A comparison directive -> a Bool column (usable as a row mask).
let bullish = execute(&df, &parse("close > open").unwrap()).unwrap();
assert_eq!(bullish.len(), 40);

// `@`-suffix picks input columns; canonical form drops a default input.
let ast = parse("ma:20@close").unwrap();
assert_eq!(stringify(&ast), "ma:20");

A directive has the shape name:arg0,arg1,...@col0,col1,...name is the indicator, : arguments are its parameters, and @ columns are the inputs (each with a sensible default, e.g. close). Multi-output indicators expose each line as name.subcommand (macd.signal, boll.upper, kdj.k). The exact arguments, defaults, and sub-commands for all directives are in INDICATORS.md.

Warm-up length (rows before the first valid value) is available without computing:

use volas::directive::{lookback, parse};

let ast = parse("ma:20").unwrap();
assert_eq!(lookback(&ast), 19); // a 20-period SMA has 19 warm-up rows

§Raw kernels (advanced)

The directive engine is the recommended API. For direct, DataFrame-free access, the compute module exposes the numeric kernels as pure functions over slices (&[f64] -> Vec<f64>); most users should prefer directives, which are stable, complete, and identical to the Python surface.

§Reading CSV

use volas::{read_csv, ReadCsvOptions};

// Defaults: comma-separated, the first row is the header, standard NA tokens.
let df = read_csv("ohlcv.csv", &ReadCsvOptions::default()).unwrap();
println!("{} rows x {} cols", df.height(), df.width());

path is any AsRef<Path> (&str, String, PathBuf). Tune parsing via ReadCsvOptions (delimiter, has_header, na_values, keep_default_na).

§Time-frame cumulation (OHLCV resampling)

Aggregate finer bars into a coarser TimeFrame (the source must have a DatetimeIndex). The default OHLCV spec is open=first, high=max, low=min, close=last, volume=sum.

use volas::{Column, DataFrame, TimeFrame};
use volas::time::{cumulate, AggSpec};

// In practice `df` has a 1-minute DatetimeIndex; this is a placeholder frame.
let df = DataFrame::new(vec!["close".to_string()], vec![Column::f64(vec![1.0])], None).unwrap();

// Resample to 5-minute bars.
let five_min = cumulate(&df, TimeFrame::Min5, &AggSpec::ohlcv()).unwrap();
let _ = five_min;

TimeFrame is an enum (Min1, Min5, Hour1, Day1, …) and also parses a label via TimeFrame::from_label("5m").

§Error handling

Fallible functions return Result<_, VolasError> (a single error enum) — no panics on bad input. An unknown or malformed directive is reported at parse:

use volas::VolasError;
use volas::directive::parse;

let ok = parse("ma:2");
assert!(ok.is_ok());

let bad: Result<_, VolasError> = parse("definitely_not_an_indicator:5");
assert!(bad.is_err());

§Crate layout

  • top level — the data model (DataFrame, Series, Column, Index, DType, Scalar, Tz, Result, VolasError), plus read_csv and TimeFrame;
  • directiveparse / execute / stringify / lookback, and the Ast;
  • compute — numeric kernels and technical indicators (pure functions);
  • time — time-frame cumulation (OHLCV resampling);
  • core — the full volas-core surface, for the less-common types.

This crate is a thin facade re-exporting the volas workspace (volas-core, volas-compute, volas-directive, volas-time, volas-io) behind one dependency.

§License

MIT

Re-exports§

pub use volas_core as core;

Modules§

compute
Numeric kernels and technical indicators — pure functions over slices, usable without a DataFrame.
directive
The directive engine: parse a directive string (e.g. "ma:20", "macd.signal", "close > open") into an Ast, then [execute] it against a DataFrame to get a Column.
time
Time-frame cumulation (OHLCV resampling): aggregate finer bars into a coarser TimeFrame.

Structs§

DataFrame
A 2-D, column-oriented, time-indexed table. All columns share one index and have equal length (height).
Index
Row labels plus an optional name.
ReadCsvOptions
Options controlling read_csv.
Series
A one-dimensional, named, indexed column.

Enums§

Column
A typed, contiguous column of values. The value buffer is a Buffer — owned (Arc-shared, cheap clone, copy-on-write mutation) or a zero-copy borrow of foreign (Arrow / NumPy) memory.
DType
The logical data type of a crate::Column.
IndexKind
The label storage backing an Index.
Label
A single row-index label: an integer / datetime (I64, ns) or a string (Str). It decouples label lookup from the index’s storage so the same .loc / .at / .loc[a:b] / drop paths serve every index kind.
Scalar
The result of a dtype-preserving scalar reduction (Column::sum etc.), carrying the value in its pandas result dtype so the binding can box it as the matching numpy scalar (np.int64 / np.float64 / np.bool_).
TimeFrame
An OHLCV sampling period.
Tz
The timezone attached to a DatetimeIndex. Storage is always UTC epoch-ns; this drives wall-clock conversion only.
VolasError
Errors raised by the volas core. The Python binding maps each variant to a typed exception (see volas-python).

Functions§

read_csv
Read a CSV file into a DataFrame, inferring each column’s dtype (i64 -> f64 -> bool -> str).

Type Aliases§

Result
Convenience result alias used throughout the core.