//! Folded sink reductions are byte-for-byte equal to the raw-recorder path. //! Drives the folding sinks node-by-node (the `stage1_r_e2e.rs` direct-node //! style) over a synthetic dense R-record + equity/exposure series, and asserts //! `summarize_r` over `GatedRecorder`'s emitted rows equals `summarize_r` over //! the full record, and a `SeriesReducer` summary equals `summarize`'s fields. use aura_core::{AnyColumn, Ctx, Node, Scalar, ScalarKind, Timestamp}; use aura_engine::{summarize, summarize_r}; use aura_std::{GatedRecorder, SeriesReducer, PM_RECORD_KINDS, PM_WIDTH}; use std::sync::mpsc; const CLOSED: usize = 0; const REALIZED_R: usize = 1; const OPEN: usize = 11; const UNREALIZED_R: usize = 12; const ENTRY_PRICE: usize = 6; const STOP_PRICE: usize = 7; /// The kind-correct zero for a dense-record column. The PM record is /// heterogeneous (`PM_RECORD_KINDS`: Bool/I64/Timestamp slots beside the f64s), /// so each unset slot must default to a scalar of its column's kind — a blanket /// f64 zero would be rejected by the kind-typed `AnyColumn::push`. fn zero_of(kind: ScalarKind) -> Scalar { match kind { ScalarKind::F64 => Scalar::f64(0.0), ScalarKind::I64 => Scalar::i64(0), ScalarKind::Bool => Scalar::bool(false), ScalarKind::Timestamp => Scalar::ts(Timestamp(0)), } } /// One dense R-record row. `closed`/`open` set the gate + the open-at-end flag; /// the priced fields make `summarize_r`'s R arithmetic well-defined. fn pm_row(closed: bool, realized: f64, open: bool, entry: f64, stop: f64) -> Vec { let mut v: Vec = PM_RECORD_KINDS.iter().map(|&k| zero_of(k)).collect(); debug_assert_eq!(v.len(), PM_WIDTH); v[CLOSED] = Scalar::bool(closed); v[REALIZED_R] = Scalar::f64(realized); v[OPEN] = Scalar::bool(open); v[UNREALIZED_R] = Scalar::f64(if open { realized } else { 0.0 }); v[ENTRY_PRICE] = Scalar::f64(entry); v[STOP_PRICE] = Scalar::f64(stop); v } #[test] fn gated_recorder_then_summarize_r_equals_raw_summarize_r() { // a realistic mix: holds, closed wins/losses, a hold tail, then an open-at-end row. let full: Vec<(Timestamp, Vec)> = [ pm_row(false, 0.0, false, 0.0, 0.0), pm_row(true, 1.5, false, 100.0, 95.0), pm_row(false, 0.0, false, 0.0, 0.0), pm_row(true, -1.0, false, 100.0, 95.0), pm_row(false, 0.0, false, 0.0, 0.0), pm_row(false, 0.7, true, 100.0, 95.0), // open at end ] .into_iter() .enumerate() .map(|(i, r)| (Timestamp(i as i64 + 1), r)) .collect(); // RAW: summarize_r over the whole dense record. let raw = summarize_r(&full, 0.0); // FOLDED: drive GatedRecorder (gate = CLOSED), collect its emitted rows, fold. let (tx, rx) = mpsc::channel(); let mut g = GatedRecorder::new(&PM_RECORD_KINDS, CLOSED, tx); let mut cols: Vec = PM_RECORD_KINDS.iter().map(|&k| AnyColumn::with_capacity(k, 1)).collect(); for (ts, row) in &full { for (i, s) in row.iter().enumerate() { cols[i].push(*s).unwrap(); } assert_eq!(g.eval(Ctx::new(&cols, *ts)), None); } g.finalize(); let folded_rows: Vec<(Timestamp, Vec)> = rx.try_iter().collect(); let folded = summarize_r(&folded_rows, 0.0); assert_eq!(folded, raw, "GatedRecorder+summarize_r must equal raw summarize_r"); } #[test] fn series_reducer_equals_summarize_pip_fields() { let equity: Vec<(Timestamp, f64)> = [10.0, 12.0, 8.0, 9.0, 7.0].iter().enumerate().map(|(i, &v)| (Timestamp(i as i64), v)).collect(); let exposure: Vec<(Timestamp, f64)> = [1.0, 1.0, -1.0, 0.0, 1.0].iter().enumerate().map(|(i, &v)| (Timestamp(i as i64), v)).collect(); let raw = summarize(&equity, &exposure); // FOLDED: one SeriesReducer per series; read pip fields from their summaries. let fold_series = |series: &[(Timestamp, f64)]| -> Vec { let (tx, rx) = mpsc::channel(); let mut r = SeriesReducer::new(tx); let mut col = vec![AnyColumn::with_capacity(ScalarKind::F64, 1)]; for &(ts, v) in series { col[0].push(Scalar::f64(v)).unwrap(); r.eval(Ctx::new(&col, ts)); } r.finalize(); rx.try_iter().next().expect("one summary row").1 }; let eqs = fold_series(&equity); let exs = fold_series(&exposure); assert_eq!(eqs[0].as_f64(), raw.total_pips, "total_pips"); assert_eq!(eqs[1].as_f64(), raw.max_drawdown, "max_drawdown"); assert_eq!(exs[2].as_i64() as u64, raw.bias_sign_flips, "bias_sign_flips"); }