use crate::indicators::market_structure::Bias;
use crate::traits::Next;
use serde::Deserialize;
use std::fs;
use std::path::Path;
#[derive(Debug, Deserialize)]
pub struct TestCase {
pub input: Vec<f64>,
pub expected: Vec<Option<f64>>,
}
#[derive(Debug, Deserialize)]
pub struct TestCaseVec {
pub input: Vec<f64>,
pub expected: Vec<Vec<Option<f64>>>,
}
#[derive(Debug, Deserialize)]
pub struct TestCaseLoops {
pub input: Vec<(f64, f64)>,
pub expected: Vec<(Option<f64>, Option<f64>)>,
}
#[derive(Debug, Deserialize)]
pub struct TestCaseOC {
pub input: Vec<(f64, f64)>,
pub expected: Vec<Option<f64>>,
}
#[derive(Debug, Deserialize)]
pub struct TestCaseTuple {
pub input: Vec<f64>,
pub expected: Vec<(Option<f64>, Option<f64>)>,
}
pub fn load_gold_standard(name: &str) -> TestCase {
let path = get_gold_standard_path(name);
let content = fs::read_to_string(&path)
.expect(&format!("Failed to read gold standard file at {:?}", path));
serde_json::from_str(&content).expect("Failed to parse gold standard JSON")
}
pub fn load_gold_standard_vec(name: &str) -> TestCaseVec {
let path = get_gold_standard_path(name);
let content = fs::read_to_string(&path)
.expect(&format!("Failed to read gold standard file at {:?}", path));
serde_json::from_str(&content).expect("Failed to parse gold standard JSON")
}
pub fn load_gold_standard_loops(name: &str) -> TestCaseLoops {
let path = get_gold_standard_path(name);
let content = fs::read_to_string(&path)
.expect(&format!("Failed to read gold standard file at {:?}", path));
serde_json::from_str(&content).expect("Failed to parse gold standard JSON")
}
fn get_gold_standard_path(name: &str) -> std::path::PathBuf {
let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR not set");
let manifest_path = Path::new(&manifest_dir);
let path = manifest_path
.join("tests/gold_standard")
.join(format!("{}.json", name));
if path.exists() {
path
} else {
manifest_path
.parent()
.unwrap()
.join("tests/gold_standard")
.join(format!("{}.json", name))
}
}
pub fn assert_indicator_parity<I>(mut indicator: I, input: &[f64], expected: &[Option<f64>])
where
I: Next<f64, Output = f64>,
{
for (i, &val) in input.iter().enumerate() {
let result = indicator.next(val);
match expected[i] {
Some(exp) => approx::assert_relative_eq!(result, exp, epsilon = 1e-6),
None => assert!(result.is_nan(), "Expected NaN at index {}", i),
}
}
}
pub fn assert_indicator_parity_vec<I>(mut indicator: I, input: &[f64], expected: &[Vec<Option<f64>>])
where
I: Next<f64, Output = Vec<f64>>,
{
for (i, &val) in input.iter().enumerate() {
let result = indicator.next(val);
for (j, &v) in result.iter().enumerate() {
match expected[i][j] {
Some(exp) => approx::assert_relative_eq!(v, exp, epsilon = 1e-6),
None => assert!(v.is_nan(), "Expected NaN at index {},{}", i, j),
}
}
}
}
pub fn assert_indicator_parity_loops<I>(mut indicator: I, input: &[(f64, f64)], expected: &[(Option<f64>, Option<f64>)])
where
I: Next<(f64, f64), Output = (f64, f64)>,
{
for (i, &val) in input.iter().enumerate() {
let result = indicator.next(val);
match expected[i].0 {
Some(exp) => approx::assert_relative_eq!(result.0, exp, epsilon = 1e-6),
None => assert!(result.0.is_nan(), "Expected NaN at index {}.0", i),
}
match expected[i].1 {
Some(exp) => approx::assert_relative_eq!(result.1, exp, epsilon = 1e-6),
None => assert!(result.1.is_nan(), "Expected NaN at index {}.1", i),
}
}
}
pub fn load_gold_standard_oc(name: &str) -> TestCaseOC {
let path = get_gold_standard_path(name);
let content = fs::read_to_string(&path)
.expect(&format!("Failed to read gold standard file at {:?}", path));
serde_json::from_str(&content).expect("Failed to parse gold standard JSON")
}
pub fn load_gold_standard_tuple(name: &str) -> TestCaseTuple {
let path = get_gold_standard_path(name);
let content = fs::read_to_string(&path)
.expect(&format!("Failed to read gold standard file at {:?}", path));
serde_json::from_str(&content).expect("Failed to parse gold standard JSON")
}
pub fn assert_indicator_parity_oc<I>(mut indicator: I, input: &[(f64, f64)], expected: &[Option<f64>])
where
I: Next<(f64, f64), Output = f64>,
{
for (i, &val) in input.iter().enumerate() {
let result = indicator.next(val);
match expected[i] {
Some(exp) => approx::assert_relative_eq!(result, exp, epsilon = 1e-6),
None => assert!(result.is_nan(), "Expected NaN at index {}", i),
}
}
}
pub fn assert_indicator_parity_tuple<I>(mut indicator: I, input: &[f64], expected: &[(Option<f64>, Option<f64>)])
where
I: Next<f64, Output = (f64, f64)>,
{
for (i, &val) in input.iter().enumerate() {
let result = indicator.next(val);
match expected[i].0 {
Some(exp) => approx::assert_relative_eq!(result.0, exp, epsilon = 1e-6),
None => assert!(result.0.is_nan(), "Expected NaN at index {}.0", i),
}
match expected[i].1 {
Some(exp) => approx::assert_relative_eq!(result.1, exp, epsilon = 1e-6),
None => assert!(result.1.is_nan(), "Expected NaN at index {}.1", i),
}
}
}
pub fn check_batch_streaming_parity<I, F>(input: Vec<f64>, mut indicator: I, batch_fn: F)
where
I: Next<f64, Output = f64>,
F: FnOnce(Vec<f64>) -> Vec<f64>,
{
let batch_results = batch_fn(input.clone());
let mut streaming_results = Vec::with_capacity(input.len());
for val in input {
streaming_results.push(indicator.next(val));
}
for (_i, (&s, &b)) in streaming_results
.iter()
.zip(batch_results.iter())
.enumerate()
{
approx::assert_relative_eq!(s, b, epsilon = 1e-6, max_relative = 1e-6);
}
}
#[derive(Debug, Clone)]
pub struct SyntheticStructureCase {
pub data: Vec<(f64, f64)>,
pub expected_flips: Vec<(usize, bool)>, pub final_bias: Bias,
pub description: &'static str,
}
pub fn generate_bullish_structure_confirmed_flip(_swing_strength: usize, noise_std: f64, seed: u64) -> SyntheticStructureCase {
let mut rng_state = seed;
let mut next_rand = || {
rng_state = rng_state.wrapping_mul(6364136223846793005).wrapping_add(1);
((rng_state >> 32) as f64 / u32::MAX as f64 - 0.5) * noise_std
};
let base: Vec<f64> = (0..40).map(|i| 100.0 + (i as f64 * 0.8)).collect();
let mut highs: Vec<f64> = base.iter().map(|&p| p + 0.5 + next_rand()).collect();
let lows: Vec<f64> = base.iter().map(|&p| p - 0.5 + next_rand()).collect();
let flip_bar = 25usize;
if flip_bar < highs.len() {
highs[flip_bar] = highs[flip_bar - 3] - 1.5; }
let data: Vec<(f64, f64)> = highs.into_iter().zip(lows).map(|(h, l)| (h.max(l), l.min(h))).collect();
SyntheticStructureCase {
data,
expected_flips: vec![(flip_bar, true)], final_bias: Bias::Bearish,
description: "bullish HL/HH sequence + confirmed LH flip after bias>=2 (Part 21 rule)",
}
}
pub fn generate_bearish_structure_confirmed_flip(_swing_strength: usize, noise_std: f64, seed: u64) -> SyntheticStructureCase {
let mut rng_state = seed;
let mut next_rand = || {
rng_state = rng_state.wrapping_mul(6364136223846793005).wrapping_add(1);
((rng_state >> 32) as f64 / u32::MAX as f64 - 0.5) * noise_std
};
let base: Vec<f64> = (0..40).map(|i| 120.0 - (i as f64 * 0.7)).collect();
let highs: Vec<f64> = base.iter().map(|&p| p + 0.4 + next_rand()).collect();
let mut lows: Vec<f64> = base.iter().map(|&p| p - 0.4 + next_rand()).collect();
let flip_bar = 28usize;
if flip_bar < lows.len() {
lows[flip_bar] = lows[flip_bar - 3] + 1.2; }
let data: Vec<(f64, f64)> = highs.into_iter().zip(lows).map(|(h, l)| (h.max(l), l.min(h))).collect();
SyntheticStructureCase {
data,
expected_flips: vec![(flip_bar, false)],
final_bias: Bias::Bullish,
description: "bearish LL/LH sequence + confirmed HL flip after bias>=2 (Part 21)",
}
}
#[derive(Debug, Clone)]
pub struct SyntheticGeometricCase {
pub data: Vec<(f64, f64)>,
pub expected_flags: Vec<FlagPatternGroundTruth>,
pub expected_hs: Vec<HsPatternGroundTruth>,
pub description: &'static str,
}
#[derive(Debug, Clone, PartialEq)]
pub struct FlagPatternGroundTruth {
pub is_bull: bool,
pub pole_length_atr_min: f64, pub max_retrace_observed: f64, pub pullbacks_gt_pushes: bool,
pub breakout_bar_approx: usize,
}
#[derive(Debug, Clone, PartialEq)]
pub struct HsPatternGroundTruth {
pub is_bearish: bool,
pub head_dominance: bool,
pub shoulder_symmetry: f64, pub score_min: f64, pub height_atr_min: f64, }
pub fn generate_clean_bull_flag(_swing_strength: usize, atr_proxy: f64) -> SyntheticGeometricCase {
let mut highs = vec![100.0; 30];
let mut lows = vec![99.0; 30];
for i in 5..8 {
highs[i] = 100.0 + (i as f64 - 4.0) * 1.8 * atr_proxy;
lows[i] = 100.0 + (i as f64 - 5.0) * 0.9 * atr_proxy;
}
let pole_end = 7;
let pole_high = highs[pole_end];
let pole_low = lows[5];
let pole_len = pole_high - pole_low;
let max_extreme = pole_high - pole_len * 0.55; for i in 8..15 {
let t = (i - 8) as f64;
highs[i] = pole_high - t * 0.04 * atr_proxy;
lows[i] = max_extreme + t * 0.01 * atr_proxy;
}
highs[15] = pole_high + 0.5 * atr_proxy;
lows[15] = highs[15] - 0.2 * atr_proxy;
for i in 16..30 {
highs[i] = highs[15];
lows[i] = lows[15];
}
let data: Vec<_> = highs.into_iter().zip(lows).map(|(h,l)| (h.max(l), l.min(h))).collect();
SyntheticGeometricCase {
data,
expected_flags: vec![FlagPatternGroundTruth {
is_bull: true,
pole_length_atr_min: 1.2,
max_retrace_observed: 0.55,
pullbacks_gt_pushes: true,
breakout_bar_approx: 16,
}],
expected_hs: vec![],
description: "clean bull flag per Part69: 3-bar impulse pole, pullbacks>pushes, retrace<61.8, breakout at pole high (r46a invariants)",
}
}
pub fn generate_perfect_bear_hs(_atr_proxy: f64) -> SyntheticGeometricCase {
let mut highs = vec![100.0; 45];
let mut lows = vec![99.0; 45];
let ls_bar=10; let neck1=12; let head=20; let neck2=28; let rs_bar=35;
highs[ls_bar] = 102.0; lows[neck1] = 100.0; highs[head] = 108.0; lows[neck2] = 100.2; highs[rs_bar] = 102.1;
for i in (ls_bar-3)..(rs_bar+3) {
if i < highs.len() {
if highs[i] < 101.0 { highs[i] = 101.0 + (i as f64 % 3.0) * 0.1; }
}
}
let data: Vec<_> = highs.into_iter().zip(lows).map(|(h,l)| (h.max(l), l.min(h))).collect();
SyntheticGeometricCase {
data,
expected_flags: vec![],
expected_hs: vec![HsPatternGroundTruth {
is_bearish: true,
head_dominance: true,
shoulder_symmetry: 0.001, score_min: 85.0,
height_atr_min: 1.6,
}],
description: "perfect flat-neck bear H&S (Part66/bfg): head>shoulders, sym<=0.02, height>1.5ATR, slope~0, high time/price score via ComputePatternScore",
}
}
pub fn generate_flag_violation_retrace_too_deep() -> SyntheticGeometricCase {
let mut case = generate_clean_bull_flag(2, 1.0);
if case.data.len() > 14 {
let (_, pole_low) = case.data[5];
case.data[12].1 = pole_low - 2.0;
}
case.description = "flag violation: retrace >61.8% of pole — must be rejected per r46a ValidateFlagConsolidation";
case.expected_flags.clear(); case
}