TBBO Data

TickData columns (bid/ask price/size, spread, volume, delta), data.col() free-form access, DayPrep, OHLC bars.

TickData

The per-day tick struct passed to prepare() and calculate().

All slices have the same length (data.len()); index i corresponds to the same tick across all arrays. Optional columns (volume, delta, vwap) may be empty if the source parquet lacks them. Always check has_volume(), has_delta(), has_vwap() or use col(name) for safe lookup.

Parameters

  • timestamp (Vec<i64>): Tick timestamp in milliseconds
  • bid (Vec<f64>): Best-bid price per tick
  • ask (Vec<f64>): Best-ask price per tick
  • mid (Vec<f64>): (bid + ask) / 2 per tick
  • spread (Vec<f64>): ask - bid per tick
  • bid_size (Vec<f64>): Best-bid size per tick
  • ask_size (Vec<f64>): Best-ask size per tick
  • volume (Vec<f64>): Trade volume per tick (may be empty)
  • delta (Vec<f64>): Signed buyer-seller delta per tick (may be empty)
  • vwap (Vec<f64>): Session VWAP at this tick (may be empty)
  • extras (HashMap<String, Vec<f64>>): Any extra columns the parquet emitted

Example

// Direct field access (always safe for core fields):
for i in 0..data.len() {
    let micro = (data.bid[i] * data.ask_size[i] + data.ask[i] * data.bid_size[i])
              / (data.bid_size[i] + data.ask_size[i] + 1e-9);
    // ...
}

data.col(name)

data.col(name)fn col(&self, name: &str) -> Option<&[f64]>

Free-form by-name column access. Covers core fields, optional fields, and arbitrary extras.

Returns None when a column is absent or empty (the optional ones may not exist on every parquet). Pair with unwrap_or(&[]) or use col_or(name, fallback) for a default.

Name aliases: "bid_size" / "bidSize" / "bid_vol" all return the bid-size slice; "ask_size" / "askSize" / "ask_vol" all return the ask-size slice; "mid" and "close" both return the mid slice.

Parameters

  • name (&str, default required): Column name

Returns

Option<&[f64]>

Example

// Safe access to optional columns
let volume = data.col("volume").unwrap_or(&[]);
let delta = data.col("delta").unwrap_or(&[]);

// Custom column from the parquet (e.g. a feature engineered upstream)
if let Some(feat) = data.col("my_feature") {
    // use feat[i]
}

// With a fallback in one call
let vols = data.col_or("volume", &[]);

has_volume / has_delta / has_vwap

Boolean checks for the optional columns.

Example

if data.has_volume() {
    let cum_vol: f64 = data.volume.iter().sum();
    qc_log!("total volume = {:.0}", cum_vol);
}

data.ohlc_bars(bucket_ms)

data.ohlc_bars(bucket_ms)fn ohlc_bars(&self, bucket_ms: i64) -> Vec<OhlcBar>

Aggregate ticks into fixed-size OHLC time buckets on the mid-price axis.

Useful when an indicator needs bar-level context (e.g. session-anchored buckets, day-of-week stats, intraday regime detection). bucket_ms must be > 0 (60_000 = 1m, 300_000 = 5m, 3_600_000 = 1h).

Returns Vec<OhlcBar> with start_ms, end_ms, open, high, low, close, volume, tick_count. When volume is absent on the source data, falls back to bid_size + ask_size per tick.

Parameters

  • bucket_ms (i64, default required): Bucket size in milliseconds

Returns

Vec

Example

let bars_5m = data.ohlc_bars(5 * 60_000);
qc_log!("got {} 5-minute bars", bars_5m.len());

// Use in prepare() for hoisted bar-level context
fn prepare(data: &TickData) -> DayPrep {
    let mut prep = DayPrep::empty();
    let bars = data.ohlc_bars(60_000);
    let closes: Vec<f64> = bars.iter().map(|b| b.close).collect();
    prep.insert_f64("bar_closes_1m", closes);
    prep
}

DayPrep

Storage for hoisted per-day arrays. Returned by prepare(), read in calculate().

Four typed maps: f64_arrays, bool_arrays, i64_arrays, scalars. Insert with insert_f64/insert_bool/insert_i64/insert_scalar; read with f64/bool_arr/i64_arr/scalar (each returns Option).

Use this for any computation that does not depend on swept parameters: full-series TA arrays, rolling baselines, day-level summary stats, regime masks.

Example

fn prepare(data: &TickData) -> DayPrep {
    let mut prep = DayPrep::empty();
    let atr = ta::atr_bid_ask(&data.bid, &data.ask, 30);
    let baseline = rolling_mean(&data.spread, 200);
    let day_open = data.mid.first().copied().unwrap_or(f64::NAN);

    prep.insert_f64("atr", atr);
    prep.insert_f64("spread_baseline", baseline);
    prep.insert_scalar("day_open", day_open);
    prep
}

fn calculate(&self, data: &TickData, prep: &DayPrep) -> SignalOutput {
    let atr = prep.f64("atr").unwrap_or(&[]);
    let baseline = prep.f64("spread_baseline").unwrap_or(&[]);
    let day_open = prep.scalar("day_open").unwrap_or(0.0);
    // ...
}

qc_log!

qc_log!qc_log!("fmt {}", arg)

Macro: write a debug line to the in-app terminal.

Forwards eprintln! through the runner's stderr to the renderer, surfaced in the Terminal pane.

Example

qc_log!("loaded {} ticks, has_volume={}", data.len(), data.has_volume());
qc_log!("avg spread = {:.4}", data.spread.iter().sum::<f64>() / data.len() as f64);