diff --git a/bindings/csharp/Wickra.Tests/GoldenTests.cs b/bindings/csharp/Wickra.Tests/GoldenTests.cs
new file mode 100644
index 00000000..e4360e6f
--- /dev/null
+++ b/bindings/csharp/Wickra.Tests/GoldenTests.cs
@@ -0,0 +1,162 @@
+using System.Globalization;
+using System.Runtime.CompilerServices;
+using Wickra;
+using Xunit;
+
+namespace Wickra.Tests;
+
+///
+/// Golden-fixture parity: replay the shared testdata/golden input series
+/// through the C# FFI and assert every value matches the Rust reference output.
+/// Where the archetype tests only check finiteness, this pins exact values, so a
+/// wiring bug (swapped parameter, wrong multi-output field) is caught.
+/// Fixtures are generated by cargo run -p wickra-examples --bin gen_golden.
+///
+public class GoldenTests
+{
+ private const double Tol = 1e-6;
+
+ private static string GoldenDir([CallerFilePath] string file = "") =>
+ Path.GetFullPath(Path.Combine(Path.GetDirectoryName(file)!, "..", "..", "..", "testdata", "golden"));
+
+ private static List ReadCsv(string name)
+ {
+ var path = Path.Combine(GoldenDir(), name + ".csv");
+ return File.ReadAllLines(path)
+ .Skip(1) // header
+ .Where(l => l.Length > 0)
+ .Select(l => l.Split(','))
+ .ToList();
+ }
+
+ private static double[][] Input()
+ {
+ return ReadCsv("input")
+ .Select(r => r.Select(c => double.Parse(c, CultureInfo.InvariantCulture)).ToArray())
+ .ToArray();
+ }
+
+ private static double Cell(string s) =>
+ s == "nan" ? double.NaN : double.Parse(s, CultureInfo.InvariantCulture);
+
+ private static void AssertClose(double got, double want, int row, string field)
+ {
+ if (double.IsNaN(want))
+ {
+ Assert.True(double.IsNaN(got), $"row {row} {field}: expected warmup/NaN, got {got}");
+ return;
+ }
+ var tol = Tol * Math.Max(1.0, Math.Abs(want));
+ Assert.True(Math.Abs(got - want) <= tol, $"row {row} {field}: got {got}, want {want}");
+ }
+
+ // --- scalar (close-driven) ------------------------------------------------
+
+ [Theory]
+ [InlineData("sma")]
+ [InlineData("ema")]
+ [InlineData("rsi")]
+ public void Scalar_MatchesGolden(string name)
+ {
+ var input = Input();
+ var expected = ReadCsv(name);
+ using var ind = (IDisposable)(name switch
+ {
+ "sma" => new Sma(14),
+ "ema" => new Ema(14),
+ "rsi" => new Rsi(14),
+ _ => throw new ArgumentOutOfRangeException(nameof(name)),
+ });
+ for (var i = 0; i < input.Length; i++)
+ {
+ var close = input[i][3];
+ double got = ind switch
+ {
+ Sma s => s.Update(close),
+ Ema e => e.Update(close),
+ Rsi r => r.Update(close),
+ _ => double.NaN,
+ };
+ AssertClose(got, Cell(expected[i][0]), i, name);
+ }
+ }
+
+ // --- candle, single output ------------------------------------------------
+
+ [Fact]
+ public void Candle_Atr_MatchesGolden()
+ {
+ var input = Input();
+ var expected = ReadCsv("atr");
+ using var atr = new Atr(14);
+ for (var i = 0; i < input.Length; i++)
+ {
+ var (o, h, l, c, v) = (input[i][0], input[i][1], input[i][2], input[i][3], input[i][4]);
+ AssertClose(atr.Update(o, h, l, c, v, i), Cell(expected[i][0]), i, "atr");
+ }
+ }
+
+ // --- pairwise -------------------------------------------------------------
+
+ [Fact]
+ public void Pairwise_Beta_MatchesGolden()
+ {
+ var input = Input();
+ var expected = ReadCsv("beta");
+ using var beta = new Beta(20);
+ for (var i = 0; i < input.Length; i++)
+ {
+ // generator fed (close, open)
+ AssertClose(beta.Update(input[i][3], input[i][0]), Cell(expected[i][0]), i, "beta");
+ }
+ }
+
+ // --- scalar multi-output: MACD -------------------------------------------
+
+ [Fact]
+ public void MultiOutput_Macd_MatchesGolden()
+ {
+ var input = Input();
+ var expected = ReadCsv("macd");
+ using var macd = new MacdIndicator(12, 26, 9);
+ for (var i = 0; i < input.Length; i++)
+ {
+ MacdOutput? got = macd.Update(input[i][3]);
+ var e = expected[i];
+ if (e[0] == "nan")
+ {
+ Assert.Null(got);
+ continue;
+ }
+ Assert.NotNull(got);
+ AssertClose(got!.Value.Macd, Cell(e[0]), i, "macd.macd");
+ AssertClose(got.Value.Signal, Cell(e[1]), i, "macd.signal");
+ AssertClose(got.Value.Histogram, Cell(e[2]), i, "macd.histogram");
+ }
+ }
+
+ // --- candle multi-output: ADX --------------------------------------------
+
+ [Fact]
+ public void MultiOutput_Adx_MatchesGolden()
+ {
+ var input = Input();
+ var expected = ReadCsv("adx");
+ using var adx = new Adx(14);
+ for (var i = 0; i < input.Length; i++)
+ {
+ var (o, h, l, c, v) = (input[i][0], input[i][1], input[i][2], input[i][3], input[i][4]);
+ AdxOutput? got = adx.Update(o, h, l, c, v, i);
+ var e = expected[i];
+ if (e[0] == "nan")
+ {
+ Assert.Null(got);
+ continue;
+ }
+ Assert.NotNull(got);
+ AssertClose(got!.Value.PlusDi, Cell(e[0]), i, "adx.plus_di");
+ AssertClose(got.Value.MinusDi, Cell(e[1]), i, "adx.minus_di");
+ AssertClose(got.Value.Adx, Cell(e[2]), i, "adx.adx");
+ }
+ }
+}
diff --git a/bindings/go/golden_test.go b/bindings/go/golden_test.go
new file mode 100644
index 00000000..b5ed3b84
--- /dev/null
+++ b/bindings/go/golden_test.go
@@ -0,0 +1,191 @@
+package wickra
+
+import (
+ "bufio"
+ "math"
+ "os"
+ "strconv"
+ "strings"
+ "testing"
+)
+
+// Golden-fixture parity: replay the shared testdata/golden input series through
+// the Go FFI and assert every value matches the Rust reference output. Where the
+// archetype test only checks finiteness, this pins exact values, catching wiring
+// bugs (swapped params, wrong multi-output field). Fixtures are generated by
+// `cargo run -p wickra-examples --bin gen_golden`.
+
+const goldenTol = 1e-6
+
+func readGolden(t *testing.T, name string) [][]string {
+ t.Helper()
+ f, err := os.Open("../../testdata/golden/" + name + ".csv")
+ if err != nil {
+ t.Fatalf("open %s: %v", name, err)
+ }
+ defer f.Close()
+ var rows [][]string
+ sc := bufio.NewScanner(f)
+ first := true
+ for sc.Scan() {
+ line := sc.Text()
+ if first {
+ first = false
+ continue
+ }
+ if line == "" {
+ continue
+ }
+ rows = append(rows, strings.Split(line, ","))
+ }
+ return rows
+}
+
+func goldenCell(s string) float64 {
+ if s == "nan" {
+ return math.NaN()
+ }
+ v, _ := strconv.ParseFloat(s, 64)
+ return v
+}
+
+func goldenInput(t *testing.T) [][]float64 {
+ rows := readGolden(t, "input")
+ out := make([][]float64, len(rows))
+ for i, r := range rows {
+ vals := make([]float64, len(r))
+ for j, c := range r {
+ vals[j] = goldenCell(c)
+ }
+ out[i] = vals
+ }
+ return out
+}
+
+func assertGoldenClose(t *testing.T, got, want float64, row int, field string) {
+ t.Helper()
+ if math.IsNaN(want) {
+ if !math.IsNaN(got) {
+ t.Errorf("row %d %s: expected warmup/NaN, got %v", row, field, got)
+ }
+ return
+ }
+ tol := goldenTol * math.Max(1.0, math.Abs(want))
+ if math.Abs(got-want) > tol {
+ t.Errorf("row %d %s: got %v want %v", row, field, got, want)
+ }
+}
+
+func TestGoldenScalar(t *testing.T) {
+ input := goldenInput(t)
+ sma, err := NewSma(14)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer sma.Close()
+ ema, err := NewEma(14)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer ema.Close()
+ rsi, err := NewRsi(14)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer rsi.Close()
+
+ cases := []struct {
+ name string
+ upd func(close float64) float64
+ }{
+ {"sma", sma.Update},
+ {"ema", ema.Update},
+ {"rsi", rsi.Update},
+ }
+ for _, tc := range cases {
+ exp := readGolden(t, tc.name)
+ for i := range input {
+ assertGoldenClose(t, tc.upd(input[i][3]), goldenCell(exp[i][0]), i, tc.name)
+ }
+ }
+}
+
+func TestGoldenAtr(t *testing.T) {
+ input := goldenInput(t)
+ exp := readGolden(t, "atr")
+ atr, err := NewAtr(14)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer atr.Close()
+ for i := range input {
+ got := atr.Update(input[i][0], input[i][1], input[i][2], input[i][3], input[i][4], int64(i))
+ assertGoldenClose(t, got, goldenCell(exp[i][0]), i, "atr")
+ }
+}
+
+func TestGoldenBeta(t *testing.T) {
+ input := goldenInput(t)
+ exp := readGolden(t, "beta")
+ beta, err := NewBeta(20)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer beta.Close()
+ for i := range input {
+ // generator fed (close, open)
+ assertGoldenClose(t, beta.Update(input[i][3], input[i][0]), goldenCell(exp[i][0]), i, "beta")
+ }
+}
+
+func TestGoldenMacd(t *testing.T) {
+ input := goldenInput(t)
+ exp := readGolden(t, "macd")
+ macd, err := NewMacdIndicator(12, 26, 9)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer macd.Close()
+ for i := range input {
+ out, ok := macd.Update(input[i][3])
+ if exp[i][0] == "nan" {
+ if ok {
+ t.Errorf("row %d macd: expected warmup, got %+v", i, out)
+ }
+ continue
+ }
+ if !ok {
+ t.Errorf("row %d macd: expected value, got warmup", i)
+ continue
+ }
+ assertGoldenClose(t, out.Macd, goldenCell(exp[i][0]), i, "macd.macd")
+ assertGoldenClose(t, out.Signal, goldenCell(exp[i][1]), i, "macd.signal")
+ assertGoldenClose(t, out.Histogram, goldenCell(exp[i][2]), i, "macd.histogram")
+ }
+}
+
+func TestGoldenAdx(t *testing.T) {
+ input := goldenInput(t)
+ exp := readGolden(t, "adx")
+ adx, err := NewAdx(14)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer adx.Close()
+ for i := range input {
+ out, ok := adx.Update(input[i][0], input[i][1], input[i][2], input[i][3], input[i][4], int64(i))
+ if exp[i][0] == "nan" {
+ if ok {
+ t.Errorf("row %d adx: expected warmup, got %+v", i, out)
+ }
+ continue
+ }
+ if !ok {
+ t.Errorf("row %d adx: expected value, got warmup", i)
+ continue
+ }
+ assertGoldenClose(t, out.PlusDi, goldenCell(exp[i][0]), i, "adx.plus_di")
+ assertGoldenClose(t, out.MinusDi, goldenCell(exp[i][1]), i, "adx.minus_di")
+ assertGoldenClose(t, out.Adx, goldenCell(exp[i][2]), i, "adx.adx")
+ }
+}
diff --git a/bindings/java/src/test/java/org/wickra/GoldenTests.java b/bindings/java/src/test/java/org/wickra/GoldenTests.java
new file mode 100644
index 00000000..def1d374
--- /dev/null
+++ b/bindings/java/src/test/java/org/wickra/GoldenTests.java
@@ -0,0 +1,160 @@
+package org.wickra;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.File;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Golden-fixture parity: replay the shared {@code testdata/golden} input series
+ * through the Java FFI and assert every value matches the Rust reference output.
+ * Where the archetype tests only check finiteness, this pins exact values, so a
+ * wiring bug (swapped parameter, wrong multi-output field) is caught. Fixtures
+ * are generated by {@code cargo run -p wickra-examples --bin gen_golden}.
+ */
+class GoldenTests {
+ private static final double TOL = 1e-6;
+
+ private static Path goldenDir() {
+ File d = new File("").getAbsoluteFile();
+ while (d != null) {
+ File g = new File(d, "testdata/golden");
+ if (g.isDirectory()) {
+ return g.toPath();
+ }
+ d = d.getParentFile();
+ }
+ throw new IllegalStateException("testdata/golden not found from " + new File("").getAbsolutePath());
+ }
+
+ private static List readCsv(String name) throws Exception {
+ List lines = Files.readAllLines(goldenDir().resolve(name + ".csv"));
+ List rows = new ArrayList<>();
+ for (int i = 1; i < lines.size(); i++) { // skip header
+ if (!lines.get(i).isEmpty()) {
+ rows.add(lines.get(i).split(","));
+ }
+ }
+ return rows;
+ }
+
+ private static double cell(String s) {
+ return s.equals("nan") ? Double.NaN : Double.parseDouble(s);
+ }
+
+ private static double[][] input() throws Exception {
+ List rows = readCsv("input");
+ double[][] out = new double[rows.size()][];
+ for (int i = 0; i < rows.size(); i++) {
+ String[] r = rows.get(i);
+ double[] v = new double[r.length];
+ for (int j = 0; j < r.length; j++) {
+ v[j] = Double.parseDouble(r[j]);
+ }
+ out[i] = v;
+ }
+ return out;
+ }
+
+ private static void close(double got, double want, int row, String field) {
+ if (Double.isNaN(want)) {
+ assertTrue(Double.isNaN(got), "row " + row + " " + field + ": expected warmup/NaN, got " + got);
+ return;
+ }
+ double tol = TOL * Math.max(1.0, Math.abs(want));
+ assertTrue(Math.abs(got - want) <= tol, "row " + row + " " + field + ": got " + got + " want " + want);
+ }
+
+ @Test
+ void scalarMatchesGolden() throws Exception {
+ double[][] in = input();
+ try (Sma sma = new Sma(14)) {
+ List e = readCsv("sma");
+ for (int i = 0; i < in.length; i++) {
+ close(sma.update(in[i][3]), cell(e.get(i)[0]), i, "sma");
+ }
+ }
+ try (Ema ema = new Ema(14)) {
+ List e = readCsv("ema");
+ for (int i = 0; i < in.length; i++) {
+ close(ema.update(in[i][3]), cell(e.get(i)[0]), i, "ema");
+ }
+ }
+ try (Rsi rsi = new Rsi(14)) {
+ List e = readCsv("rsi");
+ for (int i = 0; i < in.length; i++) {
+ close(rsi.update(in[i][3]), cell(e.get(i)[0]), i, "rsi");
+ }
+ }
+ }
+
+ @Test
+ void candleAtrMatchesGolden() throws Exception {
+ double[][] in = input();
+ List e = readCsv("atr");
+ try (Atr atr = new Atr(14)) {
+ for (int i = 0; i < in.length; i++) {
+ close(atr.update(in[i][0], in[i][1], in[i][2], in[i][3], in[i][4], i), cell(e.get(i)[0]), i, "atr");
+ }
+ }
+ }
+
+ @Test
+ void pairwiseBetaMatchesGolden() throws Exception {
+ double[][] in = input();
+ List e = readCsv("beta");
+ try (Beta beta = new Beta(20)) {
+ for (int i = 0; i < in.length; i++) {
+ // generator fed (close, open)
+ close(beta.update(in[i][3], in[i][0]), cell(e.get(i)[0]), i, "beta");
+ }
+ }
+ }
+
+ @Test
+ void macdMatchesGolden() throws Exception {
+ double[][] in = input();
+ List e = readCsv("macd");
+ try (MacdIndicator macd = new MacdIndicator(12, 26, 9)) {
+ for (int i = 0; i < in.length; i++) {
+ MacdOutput o = macd.update(in[i][3]);
+ String[] row = e.get(i);
+ if (row[0].equals("nan")) {
+ assertNull(o, "row " + i + " macd: expected warmup");
+ continue;
+ }
+ assertNotNull(o, "row " + i + " macd: expected value");
+ close(o.macd(), cell(row[0]), i, "macd.macd");
+ close(o.signal(), cell(row[1]), i, "macd.signal");
+ close(o.histogram(), cell(row[2]), i, "macd.histogram");
+ }
+ }
+ }
+
+ @Test
+ void adxMatchesGolden() throws Exception {
+ double[][] in = input();
+ List e = readCsv("adx");
+ try (Adx adx = new Adx(14)) {
+ for (int i = 0; i < in.length; i++) {
+ AdxOutput o = adx.update(in[i][0], in[i][1], in[i][2], in[i][3], in[i][4], i);
+ String[] row = e.get(i);
+ if (row[0].equals("nan")) {
+ assertNull(o, "row " + i + " adx: expected warmup");
+ continue;
+ }
+ assertNotNull(o, "row " + i + " adx: expected value");
+ close(o.plusDi(), cell(row[0]), i, "adx.plus_di");
+ close(o.minusDi(), cell(row[1]), i, "adx.minus_di");
+ close(o.adx(), cell(row[2]), i, "adx.adx");
+ }
+ }
+ }
+}
diff --git a/bindings/r/tests/testthat/test-golden.R b/bindings/r/tests/testthat/test-golden.R
new file mode 100644
index 00000000..055d5b7c
--- /dev/null
+++ b/bindings/r/tests/testthat/test-golden.R
@@ -0,0 +1,98 @@
+# Golden-fixture parity: replay the shared testdata/golden input series through
+# the R FFI and assert every value matches the Rust reference output. Where the
+# archetype test only checks finiteness, this pins exact values, catching wiring
+# bugs (swapped params, wrong multi-output field). Fixtures are generated by
+# `cargo run -p wickra-examples --bin gen_golden`.
+
+golden_dir <- local({
+ d <- normalizePath(getwd(), winslash = "/", mustWork = FALSE)
+ repeat {
+ g <- file.path(d, "testdata", "golden")
+ if (dir.exists(g)) return(g)
+ parent <- dirname(d)
+ if (identical(parent, d)) stop("testdata/golden not found from ", getwd())
+ d <- parent
+ }
+})
+
+read_golden <- function(name) {
+ read.csv(file.path(golden_dir, paste0(name, ".csv")),
+ colClasses = "character", check.names = FALSE)
+}
+
+gcell <- function(s) if (identical(s, "nan")) NA_real_ else as.numeric(s)
+
+expect_close <- function(got, want, row, field) {
+ if (is.na(want)) {
+ expect_true(is.na(got), info = paste("row", row, field, "expected warmup/NA"))
+ } else {
+ tol <- 1e-6 * max(1, abs(want))
+ expect_lte(abs(got - want), tol, label = paste("row", row, field, "got", got, "want", want))
+ }
+}
+
+golden_input <- read.csv(file.path(golden_dir, "input.csv"))
+
+test_that("scalar indicators match golden", {
+ specs <- list(c("sma", 14), c("ema", 14), c("rsi", 14))
+ for (spec in specs) {
+ name <- spec[[1]]
+ ind <- switch(name, sma = Sma(14), ema = Ema(14), rsi = Rsi(14))
+ exp <- read_golden(name)
+ for (i in seq_len(nrow(golden_input))) {
+ got <- update(ind, golden_input$close[i])
+ expect_close(got, gcell(exp[i, 1]), i, name)
+ }
+ }
+})
+
+test_that("candle Atr matches golden", {
+ atr <- Atr(14)
+ exp <- read_golden("atr")
+ for (i in seq_len(nrow(golden_input))) {
+ got <- update(atr, golden_input$open[i], golden_input$high[i], golden_input$low[i],
+ golden_input$close[i], golden_input$volume[i], i - 1)
+ expect_close(got, gcell(exp[i, 1]), i, "atr")
+ }
+})
+
+test_that("pairwise Beta matches golden", {
+ beta <- Beta(20)
+ exp <- read_golden("beta")
+ for (i in seq_len(nrow(golden_input))) {
+ # generator fed (close, open)
+ got <- update(beta, golden_input$close[i], golden_input$open[i])
+ expect_close(got, gcell(exp[i, 1]), i, "beta")
+ }
+})
+
+test_that("multi-output MACD matches golden", {
+ macd <- MacdIndicator(12, 26, 9)
+ exp <- read_golden("macd")
+ for (i in seq_len(nrow(golden_input))) {
+ out <- update(macd, golden_input$close[i])
+ if (identical(exp[i, "macd"], "nan")) {
+ expect_true(all(is.na(out)), info = paste("row", i, "macd warmup"))
+ } else {
+ expect_close(out[["macd"]], gcell(exp[i, "macd"]), i, "macd.macd")
+ expect_close(out[["signal"]], gcell(exp[i, "signal"]), i, "macd.signal")
+ expect_close(out[["histogram"]], gcell(exp[i, "histogram"]), i, "macd.histogram")
+ }
+ }
+})
+
+test_that("multi-output ADX matches golden", {
+ adx <- Adx(14)
+ exp <- read_golden("adx")
+ for (i in seq_len(nrow(golden_input))) {
+ out <- update(adx, golden_input$open[i], golden_input$high[i], golden_input$low[i],
+ golden_input$close[i], golden_input$volume[i], i - 1)
+ if (identical(exp[i, "plus_di"], "nan")) {
+ expect_true(all(is.na(out)), info = paste("row", i, "adx warmup"))
+ } else {
+ expect_close(out[["plus_di"]], gcell(exp[i, "plus_di"]), i, "adx.plus_di")
+ expect_close(out[["minus_di"]], gcell(exp[i, "minus_di"]), i, "adx.minus_di")
+ expect_close(out[["adx"]], gcell(exp[i, "adx"]), i, "adx.adx")
+ }
+ }
+})
diff --git a/examples/rust/src/bin/gen_golden.rs b/examples/rust/src/bin/gen_golden.rs
new file mode 100644
index 00000000..ccfb2b65
--- /dev/null
+++ b/examples/rust/src/bin/gen_golden.rs
@@ -0,0 +1,138 @@
+//! Generate the language-neutral golden fixtures consumed by the per-binding
+//! parity runners (`bindings/{csharp,go,java,r}`).
+//!
+//! Run from the repo root: `cargo run -p wickra-examples --bin gen_golden`.
+//! It writes a deterministic OHLCV input series plus the reference outputs of a
+//! curated set of indicators spanning the FFI archetypes (scalar, candle,
+//! scalar multi-output, candle multi-output, pairwise), computed by the Rust
+//! core. Each binding runner replays the same input through its own FFI and
+//! checks it matches these values — catching wiring bugs (swapped params,
+//! wrong multi-output index) that the math-only core tests cannot see.
+//!
+//! `nan` marks a warmup slot where the indicator returned `None`.
+
+use std::fmt::Write as _;
+use std::fs;
+use std::path::Path;
+
+use wickra::{Adx, Atr, Beta, Candle, Ema, Indicator, MacdIndicator, Rsi, Sma};
+
+const N: usize = 80;
+
+/// Deterministic OHLCV bar `i`: a varied, non-degenerate path so every
+/// indicator gets real movement (no constant returns, no zero ranges).
+fn bar(i: usize) -> (f64, f64, f64, f64, f64) {
+ let t = i as f64;
+ let close = 100.0 + 10.0 * (t * 0.3).sin() + 0.5 * t;
+ let open = close - (t * 0.5).cos();
+ let span = 1.0 + 0.5 * (t * 0.7).sin().abs();
+ let high = close.max(open) + span;
+ let low = close.min(open) - span;
+ let volume = 1000.0 + 100.0 * (t * 0.2).sin().abs();
+ (open, high, low, close, volume)
+}
+
+fn cell(v: Option) -> String {
+ match v {
+ Some(x) => format!("{x}"),
+ None => "nan".to_owned(),
+ }
+}
+
+fn write_csv(dir: &Path, name: &str, header: &str, rows: &[String]) {
+ let mut out = String::new();
+ let _ = writeln!(out, "{header}");
+ for r in rows {
+ let _ = writeln!(out, "{r}");
+ }
+ let path = dir.join(format!("{name}.csv"));
+ fs::write(&path, out).expect("write fixture");
+ println!("wrote {}", path.display());
+}
+
+fn main() {
+ let dir = Path::new("testdata/golden");
+ fs::create_dir_all(dir).expect("create testdata/golden");
+
+ let candles: Vec = (0..N)
+ .map(|i| {
+ let (o, h, l, c, v) = bar(i);
+ Candle::new(o, h, l, c, v, i as i64).expect("valid candle")
+ })
+ .collect();
+ let closes: Vec = candles.iter().map(|c| c.close).collect();
+
+ // input.csv — the single shared series every runner reads.
+ let mut input = vec![String::from("open,high,low,close,volume")];
+ for c in &candles {
+ input.push(format!(
+ "{},{},{},{},{}",
+ c.open, c.high, c.low, c.close, c.volume
+ ));
+ }
+ let input_header = input.remove(0);
+ write_csv(dir, "input", &input_header, &input);
+
+ // scalar (close-driven), single output. Each is a distinct type, so write
+ // them out separately rather than from a heterogeneous collection.
+ {
+ let mut sma = Sma::new(14).unwrap();
+ let rows: Vec = closes.iter().map(|&c| cell(sma.update(c))).collect();
+ write_csv(dir, "sma", "sma", &rows);
+ }
+ {
+ let mut ema = Ema::new(14).unwrap();
+ let rows: Vec = closes.iter().map(|&c| cell(ema.update(c))).collect();
+ write_csv(dir, "ema", "ema", &rows);
+ }
+ {
+ let mut rsi = Rsi::new(14).unwrap();
+ let rows: Vec = closes.iter().map(|&c| cell(rsi.update(c))).collect();
+ write_csv(dir, "rsi", "rsi", &rows);
+ }
+
+ // candle, single output.
+ {
+ let mut atr = Atr::new(14).unwrap();
+ let rows: Vec = candles.iter().map(|&c| cell(atr.update(c))).collect();
+ write_csv(dir, "atr", "atr", &rows);
+ }
+
+ // scalar multi-output: MACD(12,26,9).
+ {
+ let mut macd = MacdIndicator::new(12, 26, 9).unwrap();
+ let rows: Vec = closes
+ .iter()
+ .map(|&c| match macd.update(c) {
+ Some(o) => format!("{},{},{}", o.macd, o.signal, o.histogram),
+ None => "nan,nan,nan".to_owned(),
+ })
+ .collect();
+ write_csv(dir, "macd", "macd,signal,histogram", &rows);
+ }
+
+ // candle multi-output: ADX(14).
+ {
+ let mut adx = Adx::new(14).unwrap();
+ let rows: Vec = candles
+ .iter()
+ .map(|&c| match adx.update(c) {
+ Some(o) => format!("{},{},{}", o.plus_di, o.minus_di, o.adx),
+ None => "nan,nan,nan".to_owned(),
+ })
+ .collect();
+ write_csv(dir, "adx", "plus_di,minus_di,adx", &rows);
+ }
+
+ // pairwise: Beta(20) over (close, open).
+ {
+ let mut beta = Beta::new(20).unwrap();
+ let rows: Vec = candles
+ .iter()
+ .map(|&c| cell(beta.update((c.close, c.open))))
+ .collect();
+ write_csv(dir, "beta", "beta", &rows);
+ }
+
+ println!("golden fixtures written to {}", dir.display());
+}
diff --git a/testdata/golden/adx.csv b/testdata/golden/adx.csv
new file mode 100644
index 00000000..35cc0959
--- /dev/null
+++ b/testdata/golden/adx.csv
@@ -0,0 +1,81 @@
+plus_di,minus_di,adx
+nan,nan,nan
+nan,nan,nan
+nan,nan,nan
+nan,nan,nan
+nan,nan,nan
+nan,nan,nan
+nan,nan,nan
+nan,nan,nan
+nan,nan,nan
+nan,nan,nan
+nan,nan,nan
+nan,nan,nan
+nan,nan,nan
+nan,nan,nan
+nan,nan,nan
+nan,nan,nan
+nan,nan,nan
+nan,nan,nan
+nan,nan,nan
+nan,nan,nan
+nan,nan,nan
+nan,nan,nan
+nan,nan,nan
+nan,nan,nan
+nan,nan,nan
+nan,nan,nan
+nan,nan,nan
+39.5757019682593,10.51553170206367,30.912276792040817
+37.536261624936586,10.906948307963743,32.63069750889788
+35.215165462446194,12.883060473369223,33.61637662813429
+32.86410166086221,15.606658308025656,33.758337048896145
+30.70333902684003,18.50628500756431,33.11744953242373
+28.583754140794692,21.990805693589266,31.683067176177246
+26.55934622279741,25.269437743189002,29.597761510222966
+24.849750313093512,27.34663879718962,27.82532441495794
+23.505186308008465,28.419972295720065,26.513882262566277
+22.24831894567969,28.255382095825222,25.469626589930424
+21.991214452481188,26.24217101526044,24.279889509686143
+23.0167314852192,24.206077112686202,22.725510472977113
+25.270075861089534,22.49633154108633,21.51703781909767
+28.167925277844184,20.797978949255484,21.055190823958306
+32.199512135355,19.002819811263755,21.39222118044018
+36.67556284467224,17.08189161927664,22.467654445091764
+39.66767810504092,15.312333278446436,24.027003737037383
+41.25710485757308,13.864063299710923,25.86050681733179
+41.90255145462447,12.778588478409224,27.8177163877859
+42.395628716412624,11.937994453509367,29.834786399099126
+41.39315313749411,11.291691494482587,31.784798356703554
+38.98369312390027,10.942263455121635,33.52631517717672
+36.80520205991526,11.790866997951634,34.808291852450445
+34.33471861125097,14.165199105275695,35.29246448522293
+31.575896425438806,17.056390539549085,34.90412360474046
+29.011655429985023,19.499990165743316,33.81146999165209
+26.941362744011517,21.41861333571675,32.21208520671275
+25.293388618220902,23.528371409730532,30.16945244226847
+23.70108159294569,26.085794270270323,28.356623123061247
+21.9818825371455,26.753559469925122,27.030505691607747
+20.4556018343849,25.575479039846915,25.894230469505597
+19.315305653157523,24.149775639951073,24.839117763267886
+20.15346427198841,22.836532180755725,23.510691119817217
+22.621623373749483,21.279561359099215,22.049713637827605
+26.102172398824052,19.564946484191104,21.497230627213543
+29.5794011460886,17.914862734831026,21.71599192763469
+33.103914522491884,16.421886804841026,22.570814571368153
+36.988461875248944,14.998303176938437,23.980008725527608
+39.75301194225335,13.75986552582622,25.73669586768207
+41.37315229671887,12.777991517838071,27.67023180773477
+42.48135161688847,11.981093863498268,29.693955639647385
+42.62294720176041,11.257736731480932,31.730983563876975
+40.47684671425082,10.451870979556956,33.67554918447296
+37.62379013468778,11.104415435913893,35.1575133843429
+35.445073236866044,12.840589537363048,35.99012453099969
+33.361147221464314,15.58241905716853,36.014040954862004
+30.784559395666363,20.10967847345435,34.9397977135121
+28.101022653590128,23.891333482064333,33.02243689823779
+25.724805235133402,26.22784634752717,30.732853434649453
+23.898483996930594,27.16675703876092,28.994806129209827
+22.507592062957475,27.59047661188655,27.648453495608493
+21.169522519356338,26.35196828602853,26.452526680749255
+21.224977830554703,24.775436644747092,25.11436900225873
diff --git a/testdata/golden/atr.csv b/testdata/golden/atr.csv
new file mode 100644
index 00000000..1b38d50d
--- /dev/null
+++ b/testdata/golden/atr.csv
@@ -0,0 +1,81 @@
+atr
+nan
+nan
+nan
+nan
+nan
+nan
+nan
+nan
+nan
+nan
+nan
+nan
+nan
+3.917642936098542
+3.871756865753318
+3.8256550237571303
+3.775585219414474
+3.7359112048078584
+3.751806385408204
+3.844730678370346
+3.9663267478375337
+4.065925146592011
+4.103990051025662
+4.122277884333844
+4.120405500552875
+4.1099068422738645
+4.067219182386907
+3.965656623010464
+3.883731209815142
+3.8454426000158177
+3.827649810828608
+3.8056867488416217
+3.7971798302715882
+3.795930185925992
+3.768312827006575
+3.700099728786484
+3.6306300896424184
+3.63084862338228
+3.6560272497167245
+3.6536783654804816
+3.670527021756155
+3.731163563648225
+3.855191519830406
+3.994403105040647
+4.097289247845801
+4.128368657741341
+4.103844480606131
+4.0291518193306395
+3.972922779511551
+3.9077919971718877
+3.8900842871217334
+3.9281940607734454
+3.970355102352287
+3.970344885666312
+3.9271768651715058
+3.891868629198634
+3.896746818914544
+3.888594808948336
+3.8241554941261624
+3.755340593345057
+3.742402894117046
+3.779811441067645
+3.833276428397188
+3.8832337640008086
+3.9482678364884123
+3.9963590513965954
+3.9961613679725096
+3.957618048219805
+3.9111378825809897
+3.9118754285284236
+3.9079875306228926
+3.8519602816708205
+3.8003043443211753
+3.824279210324508
+3.89030736805505
+3.946176568396809
+3.944383618517461
+3.889018639116889
+3.8395240928112067
+3.792174559624793
diff --git a/testdata/golden/beta.csv b/testdata/golden/beta.csv
new file mode 100644
index 00000000..b93714a9
--- /dev/null
+++ b/testdata/golden/beta.csv
@@ -0,0 +1,81 @@
+beta
+nan
+nan
+nan
+nan
+nan
+nan
+nan
+nan
+nan
+nan
+nan
+nan
+nan
+nan
+nan
+nan
+nan
+nan
+nan
+0.9195261318785413
+0.922546057952995
+0.9270670303012646
+0.9360823119678988
+0.9520494101604974
+0.9734365033467753
+0.995093627699459
+1.0111583960294632
+1.018359059410051
+1.0174553154888744
+1.0121204526718954
+1.0067713468887387
+1.0049777031298115
+1.0087588074727343
+1.0182494903866188
+1.031286972171161
+1.0431058848104742
+1.0473159800278413
+1.0395185963505182
+1.0218520432290943
+1.0027356441997792
+0.989852175809267
+0.9830850966851594
+0.9757086459066595
+0.9639743320260056
+0.9518863451049362
+0.945167696468917
+0.9459710447944064
+0.9530453598334451
+0.9634788824656162
+0.973948405670025
+0.9815754255825168
+0.9846435781849213
+0.9830670028068416
+0.9783981708129912
+0.9733920844663336
+0.9713725848572005
+0.9755741087472364
+0.988273819307749
+1.0091885908656193
+1.0332355593672604
+1.0510340548483572
+1.057262107302167
+1.058580145340488
+1.0645933328050556
+1.0735088672694193
+1.0743308164692502
+1.0612002349944065
+1.0392533178013483
+1.0174419588972756
+1.0016230577141623
+0.9934855658174216
+0.9919410932606791
+0.9943630990130676
+0.9973488461009995
+0.9974248103764315
+0.9919840212101835
+0.9802751136245323
+0.9638604974880948
+0.9461133999713321
+0.9309659374652403
diff --git a/testdata/golden/ema.csv b/testdata/golden/ema.csv
new file mode 100644
index 00000000..27287680
--- /dev/null
+++ b/testdata/golden/ema.csv
@@ -0,0 +1,81 @@
+ema
+nan
+nan
+nan
+nan
+nan
+nan
+nan
+nan
+nan
+nan
+nan
+nan
+nan
+107.082865308575
+105.90971557088022
+104.81838000454273
+103.91437652548925
+103.29137341232037
+103.02217097393634
+103.15163412061464
+103.6921955736008
+104.62232136443316
+105.88806700052659
+107.40757775297398
+109.07812453770966
+110.78504123504803
+112.4117601975411
+113.85004525232907
+115.00950442946956
+115.8255294789832
+116.26495019544112
+116.32889606739172
+116.05260755010886
+115.50221201839392
+114.76875083248335
+113.95998970819002
+113.19074277367604
+112.57257361958058
+112.2037924360836
+112.16064199938042
+112.49045917546245
+113.20742223024638
+114.29126332917505
+115.68906080413531
+117.3199507165269
+119.08233652305883
+120.86295254590979
+122.54697139145239
+124.02825224132444
+125.2188143840051
+126.05668958634725
+126.51145173900844
+126.58692971020658
+126.32086097368011
+125.78151686199148
+125.06160082390045
+124.26996466927172
+123.5218827065933
+122.92875250508669
+122.58814223802023
+122.57507361058863
+122.93531900967452
+123.68131159273575
+124.79103363078006
+126.20998238510259
+127.85603789338172
+129.62679782427364
+131.408724185901
+133.0872848279711
+134.5571838169111
+135.7317668260377
+136.550761377433
+136.98566051598186
+137.04226900211742
+136.7601845743594
+136.20926061449748
+135.48336619117217
+134.6920008752563
+133.95051334772793
+133.369797540168
diff --git a/testdata/golden/input.csv b/testdata/golden/input.csv
new file mode 100644
index 00000000..78765d19
--- /dev/null
+++ b/testdata/golden/input.csv
@@ -0,0 +1,81 @@
+open,high,low,close,volume
+99,101,98,100,1000
+102.57761950472302,104.77731091023225,101.25551066110417,103.4552020666134,1019.8669330795061
+106.10612242808222,108.13914959894458,104.61339756308799,106.64642473395035,1038.941834230865
+109.26253189460714,110.76487377959927,107.8309272112827,109.33326909627483,1056.4642473395036
+111.7365376962194,112.90403177129735,110.1528967845943,111.32039085967226,1071.7356090899523
+113.27609348158748,114.45148509543229,111.29955825219574,112.47494986604055,1084.1470984807897
+113.7284688053824,115.1642566915892,111.30268842257516,112.73847630878196,1093.2039085967226
+113.06855035377954,114.55977666009171,110.64086736017657,112.13209366648874,1098.544972998846
+111.40827542637513,112.72390874531129,109.43899848657534,110.75463180551151,1099.9573603041506
+108.98459460176909,109.99300155201126,107.76539185209613,108.7737988023383,1097.3847630878195
+106.12753789513545,107.73969337995807,104.79904459577605,106.41120008059868,1090.929742682568
+103.21387328427625,105.41662717550601,101.71978916733775,103.92254305856751,1080.849640381959
+100.61462528040111,103.00209502109563,99.18732582635697,101.57479556705148,1067.546318055115
+98.64575078243223,100.78188758933494,97.48620160125755,99.62233840816026,1051.5501371821465
+97.53034002152081,99.46748184049008,96.34710045689485,98.28424227586412,1033.4988150155905
+97.378063505514,99.16454670333486,95.93821562552817,97.72469882334903,1014.1120008059867
+98.18385394545021,99.67344281002588,96.54876504706593,98.0383539116416,1005.837414342758
+99.8438650794075,101.15293363552601,97.93278462060415,99.24185317672267,1025.5541102026832
+102.1834853863248,103.20029690993537,100.25554360082955,101.27235512444012,1044.2520443294852
+104.99031673022002,106.32510161131832,102.65835969292533,103.99314457402363,1061.185789094272
+108.04491654708718,109.54022022493461,105.7105413401633,107.20584501801073,1075.680249530793
+111.14367593283949,112.56654934841096,109.24526558927202,110.6681390048435,1087.157577241359
+114.11098793714572,115.26697281350663,112.95942875877287,114.11541363513378,1095.1602073889517
+116.80109288512898,118.47543335247398,115.61005717653698,117.28439764388199,1099.3691003633464
+119.09282467975903,121.38046215528227,117.64904116296829,119.93667863849153,1099.616460883584
+120.88220148856881,123.36781277048146,119.39438848583472,121.87999976774739,1095.8924274663138
+122.07798667229586,124.2878498649492,120.77557026109272,122.98543345374605,1088.3454655720152
+122.60397744514097,124.22410945235427,121.57876610123756,123.19889810845086,1077.2764487555987
+122.40925186267498,123.88697089091687,121.06827005264091,122.5459890808828,1063.126663787232
+121.48461656761053,122.98099977052848,119.63330909790388,121.12969230082183,1046.4602179413757
+119.8808727652764,121.29920058454442,117.70285703314954,119.12118485241757,1027.9415498198925
+117.72299769788953,118.8665240235534,115.60101790940676,116.74454423507063,1008.3089402817496
+115.21439166809358,116.4131695096543,113.05795434620948,114.2567321877702,1011.6549204850494
+112.6270381197495,114.07463180365934,110.47704737833695,111.92464106224679,1031.154136351338
+110.27641646211617,111.76231568498811,108.51535390119264,110.00125312406458,1049.4113351138608
+108.48360243707184,109.99872116521585,107.18792367213928,108.7030424002833,1065.6986598718788
+107.530320991091,109.22424173559783,106.49671695482826,108.19063769933508,1079.3667863849153
+107.61494922421186,109.90155645208623,106.26786689008574,108.55447411796011,1089.8708095811626
+108.81801012516657,111.30403713029216,107.32068773822765,109.80671474335324,1096.7919672031487
+111.08434919099572,113.29382811110736,109.67068524069803,111.88016416080967,1099.8543345374605
+114.22618875818226,115.76972371414958,113.09073586402833,114.63427081999565,1098.9358246623383
+117.94724565362051,119.15370942824079,116.6612183117217,117.86768208634197,1094.0730556679773
+121.88395973243564,123.33523703654073,119.88495316810628,121.33623047221137,1085.459890808828
+125.65653611891825,127.14038418498515,123.29089632531003,124.77474439137693,1073.4397097874114
+128.92069597346688,130.20955349568973,126.63187762484937,127.92073514707224,1058.4917192891762
+131.41114890560974,132.45313613345562,129.49585703767033,130.5378442655162,1041.2118485241756
+132.96978971477444,134.3228744433646,131.08387196585088,132.43695669444105,1022.2889914100246
+133.5549991814736,135.053120145851,131.99497292310176,133.49309388747918,1002.4775425453357
+133.2323987581558,135.065460892756,131.8235156308926,133.65657776549278,1017.4326781222982
+132.1510588175171,134.08480947785137,131.0237176510951,132.95746831142935,1036.647912925193
+130.5116755897077,132.71696973631924,129.29758425495962,131.50287840157117,1054.402111088937
+128.53409061924222,130.9222390672229,127.07925728332546,129.46740573130614,1069.9874687593544
+126.4306172006658,128.5591966352313,124.94895708842891,127.07753652299444,1082.7826469085653
+124.38927906587094,125.87336879788326,123.1073244542458,124.59141418625812,1092.2775421612807
+122.56791894474918,123.61827749324543,121.2254215875191,122.27578013601534,1098.093623006649
+121.09703354508844,122.45602084147426,119.02315927992295,120.38214657630877,1099.9990206550704
+120.08693553049852,121.58571423995242,117.62555095473105,119.12432966418496,1097.9177729151318
+119.63399522490424,121.03798192673773,117.25536324735009,118.65934994918358,1091.9328525664675
+119.82146372498268,120.94065716085713,117.95421275941922,119.07340619529367,1082.2828594968707
+120.7124947130592,121.93415307848436,119.152517136663,120.37417550208815,1069.3525084777123
+122.33587608239566,123.94838830624106,120.87761530843784,122.49012753228324,1053.6572918000436
+124.66785812762632,126.75625008370868,123.18852214765032,125.27691410373268,1035.8229282236828
+127.61552102482923,129.80523536741393,126.34054904004905,128.53026338263376,1016.5604175448309
+131.00775897051096,133.06294250948164,129.94904333909741,132.0042268780681,1003.3623047221139
+134.59892592769242,136.79793765689536,133.234137558996,135.43314928819893,1023.1509825101539
+138.08766685694894,140.0546941334019,136.58837142074304,138.555398697196,1042.016703682664
+141.1500141222941,142.5489901806554,139.73776131670974,141.13673737507105,1059.2073514707224
+143.48228026040917,144.5932922665057,141.8802335303824,142.99124553647894,1074.037588995245
+144.84649927621132,146.0756620034572,142.76876627418082,143.9979290014267,1085.9161814856498
+145.1098734724699,146.57143212166807,142.649968595823,144.11152724502116,1094.3695669444105
+144.27024859045207,145.7471249168318,141.88968005898082,143.36655638536055,1099.060735569487
+142.46200877506277,143.72992039092952,140.60631434563564,141.8742259615024,1099.8026652716362
+139.9404686061768,141.00752471999962,138.7454488027266,139.81250491654941,1096.5657776549278
+137.04703875358243,138.78071046739933,135.67655244818158,137.41022416199849,1089.4791172140503
+134.16122174198705,136.42630679247307,132.66155074344638,134.9266357939324,1078.8252067375315
+131.6480122345848,134.02210748888697,130.25415962109273,132.6282548753949,1065.0287840157116
+129.80997879551026,131.86785162975886,128.70717960530897,130.76505243955756,1048.6398688853799
+128.85205701193928,130.78472862032595,127.6154547134165,129.54812632180318,1030.3118356745701
+128.8642014864336,130.59557044803222,127.39947545719488,129.13084441879352,1010.7753652299442
+129.82321172551153,131.29749357237068,128.12086294416926,129.59514479102842,1009.1906850227682
diff --git a/testdata/golden/macd.csv b/testdata/golden/macd.csv
new file mode 100644
index 00000000..7ad5a00c
--- /dev/null
+++ b/testdata/golden/macd.csv
@@ -0,0 +1,81 @@
+macd,signal,histogram
+nan,nan,nan
+nan,nan,nan
+nan,nan,nan
+nan,nan,nan
+nan,nan,nan
+nan,nan,nan
+nan,nan,nan
+nan,nan,nan
+nan,nan,nan
+nan,nan,nan
+nan,nan,nan
+nan,nan,nan
+nan,nan,nan
+nan,nan,nan
+nan,nan,nan
+nan,nan,nan
+nan,nan,nan
+nan,nan,nan
+nan,nan,nan
+nan,nan,nan
+nan,nan,nan
+nan,nan,nan
+nan,nan,nan
+nan,nan,nan
+nan,nan,nan
+nan,nan,nan
+nan,nan,nan
+nan,nan,nan
+nan,nan,nan
+nan,nan,nan
+nan,nan,nan
+nan,nan,nan
+nan,nan,nan
+3.553825374871522,4.7567646581597245,-1.2029392832882024
+2.8208654161048656,4.369584809748753,-1.5487193936438874
+2.1109014543067843,3.9178481386603594,-1.806946684353575
+1.4897304373886868,3.4322245984060253,-1.9424941610173385
+1.0151049910054155,2.9488006769259036,-1.9336956859204881
+0.7315731385597957,2.5053551692526823,-1.7737820306928866
+0.6664991548332893,2.137583966368804,-1.4710848115355146
+0.8276206486027746,1.8755913028155982,-1.0479706542128235
+1.2023598043004569,1.74094500311257,-0.5385851988121131
+1.7589500704087442,1.744546016571805,0.014404053836939212
+2.449277456832604,1.885492304623965,0.563785152208639
+3.2131824784813148,2.1510303393954353,1.0621521390858795
+3.9838383370451567,2.5175919389253796,1.466246398119777
+4.693724820711168,2.9528185152825377,1.7409063054286302
+5.280664208546995,3.418387653935429,1.8622765546115656
+5.693379944743711,3.8733861120970854,1.8199938326466256
+5.896081491642136,4.277925188006096,1.6181563036360398
+5.871665770714699,4.596673304547816,1.274992466166883
+5.623249186369037,4.801988480912061,0.821260705456976
+5.17389335937969,4.876369456605587,0.29752390277410345
+4.564549053715282,4.814005376027526,-0.2494563223122439
+3.8504019492156516,4.621284690665152,-0.7708827414495003
+3.0959466751243525,4.316217087556993,-1.2202704124326402
+2.369229123514458,3.926819494748486,-1.5575903712340278
+1.735771359392487,3.488609867677286,-1.7528385082847988
+1.252721799050505,3.0414322539519296,-1.7887104549014246
+0.9637532073126067,2.6258964446240656,-1.6621432373114589
+0.8951642651140048,2.2797500087220537,-1.3845857436080489
+1.0535329484060725,2.0345065966588574,-0.9809736482527849
+1.4251313413431745,1.9126315455957208,-0.48750020425254625
+1.9771541634642489,1.9255360691694265,0.051618094294822336
+2.6606512771767967,2.072559110770901,0.5880921664058958
+3.414902230493496,2.34102773471542,1.073874495778076
+4.17284207727586,2.707390603227508,1.4654514740483515
+4.867053809684606,3.139323244518928,1.727730565165678
+5.4357921236225195,3.5986170203396464,1.8371751032828731
+5.828500438890558,4.044593704049829,1.7839067348407287
+6.010328361604678,4.437740635560799,1.5725877260438788
+5.965246063689932,4.743241721186626,1.2220043425033058
+5.697477387222705,4.934088854393842,0.7633885328288628
+5.231123664416998,4.9934958163984735,0.23762784801852455
+4.608011861752942,4.916399025469367,-0.30838716371642505
+3.883959272298,4.709911074835094,-0.8259518025370935
+3.123788425025225,4.39268654487312,-1.2688981198478952
+2.395537519050208,3.993256739708538,-1.59771922065833
+1.764383551803462,3.547482102127523,-1.7830985503240608
+1.286820974089835,3.0953498765199856,-1.8085289024301505
diff --git a/testdata/golden/rsi.csv b/testdata/golden/rsi.csv
new file mode 100644
index 00000000..de46fdff
--- /dev/null
+++ b/testdata/golden/rsi.csv
@@ -0,0 +1,81 @@
+rsi
+nan
+nan
+nan
+nan
+nan
+nan
+nan
+nan
+nan
+nan
+nan
+nan
+nan
+nan
+46.84518809898699
+45.82961265424322
+46.5293990131247
+49.23903870201344
+53.518768674122796
+58.56047460456064
+63.58342577996474
+68.07459111891804
+71.80327260156207
+74.72559789038102
+76.88488357198254
+78.34471541432595
+79.15131540353094
+79.31156608926268
+77.35312513119456
+73.13439349750769
+67.51119167055117
+61.48673113238304
+55.86675382432369
+51.14733221714866
+47.57753664027756
+45.28036590056008
+44.36975889174273
+45.212282377402985
+48.12434665867438
+52.61537254620642
+57.83678169397702
+62.992663568874654
+67.57346293490359
+71.3583982125327
+74.31265622918312
+76.4855340725887
+77.9435594091973
+78.73329989554925
+78.85948376472163
+76.76197022138767
+72.44443440532005
+66.78373698621982
+60.77877328002407
+55.21662487911296
+50.57413251002351
+47.087563552178636
+44.87458874458458
+44.050409895373384
+45.01878876311954
+48.0601943425273
+52.64880490307121
+57.92140340103244
+63.088714334306665
+67.65616719708338
+71.41589642993111
+74.34074717674663
+76.4832369420561
+77.91026866443379
+78.66702552588708
+78.75547116040536
+76.51507736151031
+72.09088140590961
+66.3803507173797
+60.378854393296976
+54.856911707280055
+50.27461841837877
+46.857548898064
+44.719625963574536
+43.978649539628535
+45.06927522550655
diff --git a/testdata/golden/sma.csv b/testdata/golden/sma.csv
new file mode 100644
index 00000000..340eb8bd
--- /dev/null
+++ b/testdata/golden/sma.csv
@@ -0,0 +1,81 @@
+sma
+nan
+nan
+nan
+nan
+nan
+nan
+nan
+nan
+nan
+nan
+nan
+nan
+nan
+107.082865308575
+106.96031118542241
+106.55098952518925
+105.93612732359576
+105.21531190077062
+104.49759506253974
+103.89175182739567
+103.49656387805487
+103.39199568793734
+103.63205153291037
+104.23995145016349
+105.20605706144154
+106.48873254066868
+108.01806381828972
+109.7021037968819
+111.4350857115261
+113.10687095991703
+114.61278745568676
+115.86297967414018
+116.79043517866376
+117.35697064210828
+117.55664264968355
+117.41627860650068
+116.99308032537222
+116.36951435923494
+115.64594550958222
+114.93167153765809
+114.33515992096163
+113.9543587765253
+113.86794744733447
+114.12830831094557
+114.75684761770661
+115.742083334167
+117.04067079892921
+118.58127457216008
+120.2709406179764
+122.00339961162967
+123.66855966178939
+125.16234049131411
+126.39597061843133
+127.30391704882051
+127.84973914282192
+128.0293437492481
+127.87135083438908
+127.43453694566097
+126.80258487767678
+126.07660853743191
+125.36612074013492
+124.7792507555816
+124.41308544252024
+124.34499676870873
+124.62573040346784
+125.27487275817427
+126.27910139046546
+127.59337505833837
+129.14495712015346
+130.83991288220435
+132.57150050514548
+134.2297059345968
+135.71107012897224
+136.92793074753726
+137.81625276622648
+138.34134853563094
+138.50097632541122
+138.3255405713923
+137.87537593786334
+137.23535780170852