diff --git a/crates/aura-ingest/examples/shared/breakout_real.rs b/crates/aura-ingest/examples/shared/breakout_real.rs index 3303e96..4e4466a 100644 --- a/crates/aura-ingest/examples/shared/breakout_real.rs +++ b/crates/aura-ingest/examples/shared/breakout_real.rs @@ -40,8 +40,8 @@ use std::sync::mpsc; use aura_core::{NodeSchema, Scalar, ScalarKind, Timestamp}; use aura_engine::{ - summarize, Composite, Edge, FlatGraph, GraphBuilder, Harness, RunManifest, RunReport, - SourceSpec, Target, + join_on_ts, summarize, Composite, Edge, FlatGraph, GraphBuilder, Harness, RunManifest, + RunReport, SourceSpec, Target, }; use aura_std::{And, Delay, EqConst, Gt, Latch, Recorder, Resample, Session, SimBroker}; use chrono::TimeZone; @@ -352,30 +352,19 @@ pub struct BarTrace { /// (the sinks fire on different cardinalities — see [`BarTrace`]). Reads only /// recorded sink output (C8/C18) — never engine internals. pub fn drain_trace(taps: &Taps) -> Vec { - use std::collections::HashMap; - let equity: Vec<(Timestamp, Vec)> = taps.equity.try_iter().collect(); let held: Vec<(Timestamp, Vec)> = taps.held.try_iter().collect(); let bars: Vec<(Timestamp, Vec)> = taps.bars.try_iter().collect(); let breakout: Vec<(Timestamp, Vec)> = taps.breakout.try_iter().collect(); - // held shares the equity spine (same node-firing cadence); align by index is - // safe between those two, but we still fuse the side taps by ts. - let held_by_ts: HashMap = - held.iter().map(|(t, r)| (t.0, as_f64(&r[0]))).collect(); - let bars_by_ts: HashMap = - bars.iter().map(|(t, r)| (t.0, as_i64(&r[0]))).collect(); - let breakout_by_ts: HashMap = - breakout.iter().map(|(t, r)| (t.0, as_bool(&r[0]))).collect(); - - equity - .iter() - .map(|(ts, row)| BarTrace { - ts: *ts, - equity: as_f64(&row[0]), - held: held_by_ts.get(&ts.0).copied().unwrap_or(0.0), - bars_since_open: bars_by_ts.get(&ts.0).copied().unwrap_or(-1), - breakout: breakout_by_ts.get(&ts.0).copied().unwrap_or(false), + join_on_ts(&equity, &[&held, &bars, &breakout]) + .into_iter() + .map(|j| BarTrace { + ts: j.ts, + equity: as_f64(&j.spine[0]), + held: j.sides[0].as_ref().map_or(0.0, |r| as_f64(&r[0])), + bars_since_open: j.sides[1].as_ref().map_or(-1, |r| as_i64(&r[0])), + breakout: j.sides[2].as_ref().is_some_and(|r| as_bool(&r[0])), }) .collect() } diff --git a/crates/aura-std/src/recorder.rs b/crates/aura-std/src/recorder.rs index c81729d..796b1c2 100644 --- a/crates/aura-std/src/recorder.rs +++ b/crates/aura-std/src/recorder.rs @@ -14,6 +14,13 @@ use std::sync::mpsc::Sender; /// the newest value of every column and sends the row to `tx`; it returns `None` /// (records, forwards nothing) and `None` during warm-up until all columns have a /// value. +/// +/// Multiple taps fire at their own node's cadence, so their recorded streams have +/// different lengths and different firing instants. To fuse several taps into one +/// trace, join on the recorded timestamp — never by positional index, which +/// misaligns the columns or panics. The engine's report layer ships a `join_on_ts` +/// helper (in `aura-engine`) for exactly this; it is named in prose rather than +/// linked because `aura-std` does not depend on `aura-engine`. pub struct Recorder { kinds: Vec, tx: Sender<(Timestamp, Vec)>,