mod common;
use common::*;
use indicators::indicator::Indicator;
use indicators::volume::vwap::{Vwap, VwapParams};
use indicators::volume::adl::Adl;
use indicators::volume::chaikin_money_flow::{ChaikinMoneyFlow, CmfParams};
const EPS: f64 = 1e-7;
#[test]
fn vwap_cumulative_single_bar() {
let out = Vwap::cumulative().calculate(&ref_candles()).unwrap();
let vals = out.get("VWAP").unwrap();
assert_close(vals[0], 100.1666_6666_667, EPS, "VWAP_cum[0]");
}
#[test]
fn vwap_cumulative_last_value() {
let out = Vwap::cumulative().calculate(&ref_candles()).unwrap();
let vals = out.get("VWAP").unwrap();
assert_close(vals[29], 110.5357_5102_88, 1e-6, "VWAP_cum[29]");
}
#[test]
fn vwap_cumulative_is_non_decreasing_on_monotone_price() {
let bars: Vec<(f64, f64, f64, f64)> = vec![(12.0, 8.0, 10.0, 100.0); 10];
let out = Vwap::cumulative().calculate(&make_candles(&bars)).unwrap();
let tp = (12.0 + 8.0 + 10.0) / 3.0;
let vals = out.get("VWAP").unwrap();
for (i, &v) in vals.iter().enumerate() {
assert_close(v, tp, EPS, &format!("VWAP_cum const[{i}]"));
}
}
#[test]
fn vwap_cumulative_no_leading_nans() {
let out = Vwap::cumulative().calculate(&ref_candles()).unwrap();
let vals = out.get("VWAP").unwrap();
assert_no_nans_from(vals, 0, "VWAP_cum");
}
#[test]
fn vwap_cumulative_output_key_is_vwap() {
let out = Vwap::cumulative().calculate(&ref_candles()).unwrap();
assert!(out.get("VWAP").is_some());
assert!(out.get("VWAP_10").is_none());
}
#[test]
fn vwap_rolling10_first_valid() {
let out = Vwap::rolling(10).calculate(&ref_candles()).unwrap();
let vals = out.get("VWAP_10").unwrap();
assert_close(vals[9], 103.7780_3738_32, 1e-6, "VWAP_roll10[9]");
}
#[test]
fn vwap_rolling10_last_value() {
let out = Vwap::rolling(10).calculate(&ref_candles()).unwrap();
let vals = out.get("VWAP_10").unwrap();
assert_close(vals[29], 117.0662_1004_57, 1e-6, "VWAP_roll10[29]");
}
#[test]
fn vwap_rolling_leading_nans() {
let out = Vwap::rolling(10).calculate(&ref_candles()).unwrap();
let vals = out.get("VWAP_10").unwrap();
assert_leading_nans(vals, 9, "VWAP_roll10");
assert_no_nans_from(vals, 9, "VWAP_roll10");
}
#[test]
fn vwap_rolling_output_key_includes_period() {
let out = Vwap::rolling(10).calculate(&ref_candles()).unwrap();
assert!(out.get("VWAP_10").is_some());
assert!(out.get("VWAP").is_none());
}
#[test]
fn vwap_rolling_equals_cumulative_on_one_bar() {
let out = Vwap::rolling(1).calculate(&ref_candles()).unwrap();
let vals = out.get("VWAP_1").unwrap();
let tp0 = (BARS[0].0 + BARS[0].1 + BARS[0].2) / 3.0;
assert_close(vals[0], tp0, EPS, "VWAP_roll1[0] vs tp");
}
#[test]
fn adl_first_bar_known_value() {
let out = Adl::new().calculate(&ref_candles()).unwrap();
let vals = out.get("ADL").unwrap();
assert_close(vals[0], -200.0, EPS, "ADL[0]");
}
#[test]
fn adl_last_value() {
let out = Adl::new().calculate(&ref_candles()).unwrap();
let vals = out.get("ADL").unwrap();
assert_close(vals[29], -505.7142_8571_43, 1e-6, "ADL[29]");
}
#[test]
fn adl_is_strictly_cumulative() {
let bars = vec![(10.0_f64, 8.0, 9.0, 100.0); 3];
let out = Adl::new().calculate(&make_candles(&bars)).unwrap();
let vals = out.get("ADL").unwrap();
assert_close(vals[1], 2.0 * vals[0], EPS, "ADL cumulative");
assert_close(vals[2], 3.0 * vals[0], EPS, "ADL cumulative x3");
}
#[test]
fn adl_full_positive_bar_mfm_one() {
let bars = vec![(10.0_f64, 8.0, 10.0, 500.0)];
let out = Adl::new().calculate(&make_candles(&bars)).unwrap();
assert_close(out.get("ADL").unwrap()[0], 500.0, EPS, "ADL full bull");
}
#[test]
fn adl_full_negative_bar_mfm_neg_one() {
let bars = vec![(10.0_f64, 8.0, 8.0, 500.0)];
let out = Adl::new().calculate(&make_candles(&bars)).unwrap();
assert_close(out.get("ADL").unwrap()[0], -500.0, EPS, "ADL full bear");
}
#[test]
fn adl_zero_range_contributes_zero() {
let bars = vec![(5.0_f64, 5.0, 5.0, 1000.0), (10.0, 8.0, 10.0, 200.0)];
let out = Adl::new().calculate(&make_candles(&bars)).unwrap();
let vals = out.get("ADL").unwrap();
assert_close(vals[0], 0.0, EPS, "ADL zero range");
assert_close(vals[1], 200.0, EPS, "ADL after zero range");
}
#[test]
fn adl_no_leading_nans() {
let out = Adl::new().calculate(&ref_candles()).unwrap();
assert_no_nans_from(out.get("ADL").unwrap(), 0, "ADL");
}
#[test]
fn cmf_14_first_valid() {
let out = ChaikinMoneyFlow::with_period(14).calculate(&ref_candles()).unwrap();
let vals = out.get("CMF_14").unwrap();
assert_close(vals[13], 0.0108660914, 1e-8, "CMF(14)[13]");
}
#[test]
fn cmf_14_last_value() {
let out = ChaikinMoneyFlow::with_period(14).calculate(&ref_candles()).unwrap();
let vals = out.get("CMF_14").unwrap();
assert_close(vals[29], -0.0242622951, 1e-8, "CMF(14)[29]");
}
#[test]
fn cmf_always_in_neg1_to_pos1() {
let out = ChaikinMoneyFlow::with_period(14).calculate(&ref_candles()).unwrap();
assert_all_non_nan(out.get("CMF_14").unwrap(), |v| (-1.0..=1.0).contains(&v), "CMF range");
}
#[test]
fn cmf_leading_nans() {
let out = ChaikinMoneyFlow::with_period(14).calculate(&ref_candles()).unwrap();
assert_leading_nans(out.get("CMF_14").unwrap(), 13, "CMF(14)");
}
#[test]
fn cmf_all_up_bars_is_positive() {
let bars: Vec<(f64, f64, f64, f64)> =
(0..20).map(|i| (10.0 + i as f64, 8.0 + i as f64, 10.0 + i as f64, 100.0)).collect();
let out = ChaikinMoneyFlow::with_period(5).calculate(&make_candles(&bars)).unwrap();
assert_all_non_nan(out.get("CMF_5").unwrap(), |v| v > 0.0, "CMF all up");
}
#[test]
fn cmf_all_down_bars_is_negative() {
let bars: Vec<(f64, f64, f64, f64)> =
(0..20).map(|i| (10.0 + i as f64, 8.0 + i as f64, 8.0 + i as f64, 100.0)).collect();
let out = ChaikinMoneyFlow::with_period(5).calculate(&make_candles(&bars)).unwrap();
assert_all_non_nan(out.get("CMF_5").unwrap(), |v| v < 0.0, "CMF all down");
}
#[test]
fn cmf_output_key_includes_period() {
let out = ChaikinMoneyFlow::with_period(20).calculate(&ref_candles()).unwrap();
assert!(out.get("CMF_20").is_some());
}