90e298d3b4
First fieldtest of the sum combinators. A standalone downstream-consumer crate
(fieldtests/cycle-0008-sum-combinators/) path-depends on the engine crates and
exercises the post-0008 surface from the public interface only (rustdoc + ledger
+ glossary + project-layout, never crates/*/src).
Primary axis empirically met: the north-star two-signal combine move now uses the
shipped Add (Add::new() dropped in exactly where the 0007 fixture hand-authored
an Add2) — the 0007 boilerplate is retired, end to end through SimBroker to a
deterministic recorded pip curve.
Findings: 0 bugs, 1 friction, 2 spec_gaps, 3 working.
- working ×3: Add2 retired; LinComb weighted/variadic sums exact (<1e-12);
warm-up barrier + empty-weights panic as documented.
- spec_gap: Add/LinComb per-input firing policy (Firing::Any / mode-A as-of
join) absent from the public surface — same class as the 0007 SimBroker gap.
- spec_gap: a fired combinator emits one row per *cycle*, so a heterogeneous
same-timestamp multi-source trace can carry >1 row per timestamp (correct per
C4/C5, undocumented at this surface).
- friction: nested consumer crate still needs the empty [workspace] table
(carried from 0006/0007; #9 closed the docs half; folds into aura new).
Spec feeds the next plan as reference.
150 lines
5.8 KiB
Rust
150 lines
5.8 KiB
Rust
//! Example 1 — the north-star "combine one signal with another" move (C10),
|
|
//! expressed with the SHIPPED `Add` node (no hand-authored combinator).
|
|
//!
|
|
//! This is the demonstration that the cycle-0007 fieldtest boilerplate (a
|
|
//! project-local `Add2`) is retired: aura-std now ships `Add`, so two MA-cross
|
|
//! spreads can be summed into one exposure from shipped blocks alone.
|
|
//!
|
|
//! price ─┬─> Sma(2) ─┐
|
|
//! ├─> Sma(4) ─┴─> Sub (fast spread) ─┐
|
|
//! ├─> Sma(3) ─┐ ├─> Add ─> Exposure ─> SimBroker ─> Recorder
|
|
//! ├─> Sma(6) ─┴─> Sub (slow spread) ─┘ ^
|
|
//! └────────────────────────────────────────────────────────────┘ (price)
|
|
|
|
mod recorder;
|
|
|
|
use std::cell::RefCell;
|
|
use std::rc::Rc;
|
|
|
|
use aura_core::{Scalar, Timestamp};
|
|
use aura_engine::{Edge, Harness, SourceSpec, Target};
|
|
use aura_core::ScalarKind;
|
|
use aura_std::{Add, Exposure, SimBroker, Sma, Sub};
|
|
|
|
use recorder::{Recorder, Tape};
|
|
|
|
fn main() {
|
|
// Node indices.
|
|
const SMA2: usize = 0;
|
|
const SMA4: usize = 1;
|
|
const SMA3: usize = 2;
|
|
const SMA6: usize = 3;
|
|
const SUB_FAST: usize = 4;
|
|
const SUB_SLOW: usize = 5;
|
|
const ADD: usize = 6;
|
|
const EXPOSURE: usize = 7;
|
|
const BROKER: usize = 8;
|
|
const REC: usize = 9;
|
|
|
|
let tape: Tape = Rc::new(RefCell::new(Vec::new()));
|
|
|
|
let nodes: Vec<Box<dyn aura_core::Node>> = vec![
|
|
Box::new(Sma::new(2)),
|
|
Box::new(Sma::new(4)),
|
|
Box::new(Sma::new(3)),
|
|
Box::new(Sma::new(6)),
|
|
Box::new(Sub::new()),
|
|
Box::new(Sub::new()),
|
|
Box::new(Add::new()),
|
|
Box::new(Exposure::new(0.5)),
|
|
Box::new(SimBroker::new(1.0)),
|
|
Box::new(Recorder::new(tape.clone())),
|
|
];
|
|
|
|
// One price source, fed into all four SMAs (slot 0) and the broker price (slot 1).
|
|
let sources = vec![SourceSpec {
|
|
kind: ScalarKind::F64,
|
|
targets: vec![
|
|
Target { node: SMA2, slot: 0 },
|
|
Target { node: SMA4, slot: 0 },
|
|
Target { node: SMA3, slot: 0 },
|
|
Target { node: SMA6, slot: 0 },
|
|
Target { node: BROKER, slot: 1 },
|
|
],
|
|
}];
|
|
|
|
let edges = vec![
|
|
// fast spread = Sma(2) - Sma(4)
|
|
Edge { from: SMA2, to: SUB_FAST, slot: 0, from_field: 0 },
|
|
Edge { from: SMA4, to: SUB_FAST, slot: 1, from_field: 0 },
|
|
// slow spread = Sma(3) - Sma(6)
|
|
Edge { from: SMA3, to: SUB_SLOW, slot: 0, from_field: 0 },
|
|
Edge { from: SMA6, to: SUB_SLOW, slot: 1, from_field: 0 },
|
|
// combine the two spreads with the SHIPPED Add
|
|
Edge { from: SUB_FAST, to: ADD, slot: 0, from_field: 0 },
|
|
Edge { from: SUB_SLOW, to: ADD, slot: 1, from_field: 0 },
|
|
// sum -> exposure -> broker exposure slot (0)
|
|
Edge { from: ADD, to: EXPOSURE, slot: 0, from_field: 0 },
|
|
Edge { from: EXPOSURE, to: BROKER, slot: 0, from_field: 0 },
|
|
// broker equity -> recorder
|
|
Edge { from: BROKER, to: REC, slot: 0, from_field: 0 },
|
|
];
|
|
|
|
let mut harness = Harness::bootstrap(nodes, sources, edges)
|
|
.expect("bootstrap should succeed for an acyclic, kind-consistent graph");
|
|
|
|
// A simple rising-then-falling price path, 1 tick per ns.
|
|
let prices: &[f64] = &[
|
|
100.0, 101.0, 102.0, 103.0, 104.0, 105.0, 106.0, 107.0, // rising
|
|
106.0, 104.0, 102.0, 100.0, 98.0, 96.0, 94.0, 92.0, // falling
|
|
];
|
|
let stream: Vec<(Timestamp, Scalar)> = prices
|
|
.iter()
|
|
.enumerate()
|
|
.map(|(i, &p)| (Timestamp(i as i64), Scalar::F64(p)))
|
|
.collect();
|
|
|
|
harness.run(vec![stream]);
|
|
|
|
let tape = tape.borrow();
|
|
println!("recorded {} pip-equity rows (one per price cycle)", tape.len());
|
|
for (ts, eq) in tape.iter() {
|
|
println!(" t={:>3} equity={:+.4}", ts.0, eq);
|
|
}
|
|
|
|
// Determinism check (C1): same input -> identical run.
|
|
let tape2: Tape = Rc::new(RefCell::new(Vec::new()));
|
|
let nodes2: Vec<Box<dyn aura_core::Node>> = vec![
|
|
Box::new(Sma::new(2)),
|
|
Box::new(Sma::new(4)),
|
|
Box::new(Sma::new(3)),
|
|
Box::new(Sma::new(6)),
|
|
Box::new(Sub::new()),
|
|
Box::new(Sub::new()),
|
|
Box::new(Add::new()),
|
|
Box::new(Exposure::new(0.5)),
|
|
Box::new(SimBroker::new(1.0)),
|
|
Box::new(Recorder::new(tape2.clone())),
|
|
];
|
|
let sources2 = vec![SourceSpec {
|
|
kind: ScalarKind::F64,
|
|
targets: vec![
|
|
Target { node: SMA2, slot: 0 },
|
|
Target { node: SMA4, slot: 0 },
|
|
Target { node: SMA3, slot: 0 },
|
|
Target { node: SMA6, slot: 0 },
|
|
Target { node: BROKER, slot: 1 },
|
|
],
|
|
}];
|
|
let edges2 = vec![
|
|
Edge { from: SMA2, to: SUB_FAST, slot: 0, from_field: 0 },
|
|
Edge { from: SMA4, to: SUB_FAST, slot: 1, from_field: 0 },
|
|
Edge { from: SMA3, to: SUB_SLOW, slot: 0, from_field: 0 },
|
|
Edge { from: SMA6, to: SUB_SLOW, slot: 1, from_field: 0 },
|
|
Edge { from: SUB_FAST, to: ADD, slot: 0, from_field: 0 },
|
|
Edge { from: SUB_SLOW, to: ADD, slot: 1, from_field: 0 },
|
|
Edge { from: ADD, to: EXPOSURE, slot: 0, from_field: 0 },
|
|
Edge { from: EXPOSURE, to: BROKER, slot: 0, from_field: 0 },
|
|
Edge { from: BROKER, to: REC, slot: 0, from_field: 0 },
|
|
];
|
|
let stream2: Vec<(Timestamp, Scalar)> = prices
|
|
.iter()
|
|
.enumerate()
|
|
.map(|(i, &p)| (Timestamp(i as i64), Scalar::F64(p)))
|
|
.collect();
|
|
let mut harness2 = Harness::bootstrap(nodes2, sources2, edges2).unwrap();
|
|
harness2.run(vec![stream2]);
|
|
assert_eq!(*tape, *tape2.borrow(), "C1: two runs must be bit-identical");
|
|
println!("determinism: two runs bit-identical = OK");
|
|
}
|