diff --git a/benches/traquer.rs b/benches/traquer.rs index 21bee05..26cc5a4 100644 --- a/benches/traquer.rs +++ b/benches/traquer.rs @@ -31,30 +31,23 @@ fn criterion_benchmark(c: &mut Criterion) { c.bench_function("sig-qstick", |b| { b.iter(|| black_box(indicator::qstick(&stats.open, &stats.close, 8))) }); - c.bench_function("sig-twiggs", |b| { + c.bench_function("sig-volume-twiggs", |b| { b.iter(|| { - black_box(volume::twiggs( - &stats.high, - &stats.low, - &stats.close, - &stats.volume, - 16, - )) + black_box( + volume::twiggs(&stats.high, &stats.low, &stats.close, &stats.volume, 16) + .collect::>(), + ) }) }); c.bench_function("sig-rsi", |b| { b.iter(|| black_box(indicator::rsi(&stats.close, 16))) }); - c.bench_function("sig-kvo", |b| { + c.bench_function("sig-volume-kvo", |b| { b.iter(|| { - black_box(volume::kvo( - &stats.high, - &stats.low, - &stats.close, - &stats.volume, - 10, - 16, - )) + black_box( + volume::kvo(&stats.high, &stats.low, &stats.close, &stats.volume, 10, 16) + .collect::>(), + ) }) }); c.bench_function("sig-macd", |b| { @@ -86,49 +79,47 @@ fn criterion_benchmark(c: &mut Criterion) { )) }) }); - c.bench_function("sig-elder_force", |b| { - b.iter(|| black_box(volume::elder_force(&stats.close, &stats.volume, 16))) + c.bench_function("sig-volume-elder_force", |b| { + b.iter(|| { + black_box(volume::elder_force(&stats.close, &stats.volume, 16).collect::>()) + }) }); - c.bench_function("sig-mfi", |b| { + c.bench_function("sig-volume-mfi", |b| { b.iter(|| { - black_box(volume::mfi( - &stats.high, - &stats.low, - &stats.close, - &stats.volume, - 16, - )) + black_box( + volume::mfi(&stats.high, &stats.low, &stats.close, &stats.volume, 16) + .collect::>(), + ) }) }); - c.bench_function("sig-ad", |b| { + c.bench_function("sig-volume-ad", |b| { b.iter(|| { - black_box(volume::ad( - &stats.high, - &stats.low, - &stats.close, - &stats.volume, - )) + black_box( + volume::ad(&stats.high, &stats.low, &stats.close, &stats.volume, None) + .collect::>(), + ) }) }); - c.bench_function("sig-ad_yahoo", |b| { + c.bench_function("sig-volume-ad_yahoo", |b| { b.iter(|| { - black_box(volume::ad_yahoo( - &stats.high, - &stats.low, - &stats.close, - &stats.volume, - )) + black_box( + volume::ad( + &stats.high, + &stats.low, + &stats.close, + &stats.volume, + Some(true), + ) + .collect::>(), + ) }) }); - c.bench_function("sig-cmf", |b| { + c.bench_function("sig-volume-cmf", |b| { b.iter(|| { - black_box(volume::cmf( - &stats.high, - &stats.low, - &stats.close, - &stats.volume, - 16, - )) + black_box( + volume::cmf(&stats.high, &stats.low, &stats.close, &stats.volume, 16) + .collect::>(), + ) }) }); c.bench_function("sig-cvi", |b| { @@ -207,8 +198,8 @@ fn criterion_benchmark(c: &mut Criterion) { c.bench_function("sig-tii", |b| { b.iter(|| black_box(indicator::tii(&stats.close, 16))) }); - c.bench_function("sig-tvi", |b| { - b.iter(|| black_box(volume::tvi(&stats.close, &stats.volume, 0.5))) + c.bench_function("sig-volume-tvi", |b| { + b.iter(|| black_box(volume::tvi(&stats.close, &stats.volume, 0.5).collect::>())) }); c.bench_function("sig-supertrend", |b| { b.iter(|| { @@ -282,14 +273,20 @@ fn criterion_benchmark(c: &mut Criterion) { c.bench_function("sig-gri", |b| { b.iter(|| black_box(indicator::gri(&stats.high, &stats.low, 16))) }); - c.bench_function("sig-bw_mfi", |b| { - b.iter(|| black_box(volume::bw_mfi(&stats.high, &stats.low, &stats.volume))) + c.bench_function("sig-volume-bw_mfi", |b| { + b.iter(|| { + black_box(volume::bw_mfi(&stats.high, &stats.low, &stats.volume).collect::>()) + }) }); - c.bench_function("sig-ease", |b| { - b.iter(|| black_box(volume::ease(&stats.high, &stats.low, &stats.volume, 16))) + c.bench_function("sig-volume-ease", |b| { + b.iter(|| { + black_box( + volume::ease(&stats.high, &stats.low, &stats.volume, 16).collect::>(), + ) + }) }); - c.bench_function("sig-obv", |b| { - b.iter(|| black_box(volume::obv(&stats.close, &stats.volume))) + c.bench_function("sig-volume-obv", |b| { + b.iter(|| black_box(volume::obv(&stats.close, &stats.volume).collect::>())) }); c.bench_function("ma-ewma", |b| { b.iter(|| black_box(smooth::ewma(&stats.close, 16).collect::>())) diff --git a/src/volume.rs b/src/volume.rs index 3f3e684..a07f6d8 100644 --- a/src/volume.rs +++ b/src/volume.rs @@ -2,9 +2,15 @@ use itertools::izip; use crate::smooth; -fn vforce(high: &[f64], low: &[f64], close: &[f64], volume: &[f64]) -> Vec { - izip!(&high[1..], &low[1..], &close[1..], &volume[1..]) - .scan((high[0], low[0], close[0], 99), |state, (h, l, c, v)| { +fn vforce<'a>( + high: &'a [f64], + low: &'a [f64], + close: &'a [f64], + volume: &'a [f64], +) -> impl Iterator + 'a { + izip!(&high[1..], &low[1..], &close[1..], &volume[1..]).scan( + (high[0], low[0], close[0], 99), + |state, (h, l, c, v)| { let trend: i8 = { if h + l + c > state.0 + state.1 + state.2 { 1 @@ -14,21 +20,21 @@ fn vforce(high: &[f64], low: &[f64], close: &[f64], volume: &[f64]) -> Vec }; *state = (*h, *l, *c, trend); Some(v * trend as f64) - }) - .collect::>() + }, + ) } /// klinger volume oscillator /// different from formula defined by https://www.investopedia.com/terms/k/klingeroscillator.asp -pub fn kvo( - high: &[f64], - low: &[f64], - close: &[f64], - volume: &[f64], +pub fn kvo<'a>( + high: &'a [f64], + low: &'a [f64], + close: &'a [f64], + volume: &'a [f64], short: usize, long: usize, -) -> Vec { - let vf = vforce(high, low, close, volume); +) -> impl Iterator + 'a { + let vf = vforce(high, low, close, volume).collect::>(); let short_ma = smooth::ewma(&vf, short); let long_ma = smooth::ewma(&vf, long); short_ma @@ -36,6 +42,7 @@ pub fn kvo( .zip(long_ma) .map(|(x, y)| x - y) .collect::>() + .into_iter() } fn wilder_sum(data: &[f64], window: usize) -> impl Iterator + '_ { @@ -50,7 +57,13 @@ fn wilder_sum(data: &[f64], window: usize) -> impl Iterator + '_ { /// twiggs money flow /// https://www.marketvolume.com/technicalanalysis/twiggsmoneyflow.asp /// https://www.incrediblecharts.com/indicators/twiggs_money_flow.php -pub fn twiggs(high: &[f64], low: &[f64], close: &[f64], volume: &[f64], window: usize) -> Vec { +pub fn twiggs<'a>( + high: &'a [f64], + low: &'a [f64], + close: &'a [f64], + volume: &'a [f64], + window: usize, +) -> impl Iterator + 'a { let data = izip!(&high[1..], &low[1..], &close[1..], &volume[1..]); // not using wilder moving average to minimise drift caused by floating point math wilder_sum( @@ -67,44 +80,58 @@ pub fn twiggs(high: &[f64], low: &[f64], close: &[f64], volume: &[f64], window: ) .zip(wilder_sum(&volume[1..], window)) .map(|(range, vol)| range / vol) - .collect() + .collect::>() + .into_iter() } /// accumulation/distribution /// https://www.investopedia.com/terms/a/accumulationdistribution.asp -pub fn ad(high: &[f64], low: &[f64], close: &[f64], volume: &[f64]) -> Vec { - izip!(high, low, close, volume) - .scan(0.0, |state, (h, l, c, vol)| { - let mfm = ((c - l) - (h - c)) / (h - l); - let mfv = mfm * vol; - let adl = *state + mfv; - *state = adl; - Some(adl) - }) - .collect::>() -} - -/// accumulation/distribution -/// like yahoo -pub fn ad_yahoo(high: &[f64], low: &[f64], close: &[f64], volume: &[f64]) -> Vec { - izip!(&high[1..], &low[1..], &close[1..], &volume[1..]) - .scan((close[0], 0.0), |state, (h, l, c, vol)| { - let mfm = if *c > state.0 { - c - f64::min(*l, state.0) - } else { - c - f64::max(*h, state.0) - }; - let mfv = mfm * vol; - let adl = state.1 + mfv; - *state = (*c, adl); - Some(adl) - }) - .collect::>() +/// supports alternate logic to consider prior close like yahoo +pub fn ad<'a>( + high: &'a [f64], + low: &'a [f64], + close: &'a [f64], + volume: &'a [f64], + alt: Option, +) -> Box + 'a> { + if !alt.unwrap_or(false) { + Box::new( + izip!(high, low, close, volume).scan(0.0, |state, (h, l, c, vol)| { + let mfm = ((c - l) - (h - c)) / (h - l); + let mfv = mfm * vol; + let adl = *state + mfv; + *state = adl; + Some(adl) + }), + ) + } else { + // alternate logic to consider prior close like yahoo + Box::new( + izip!(&high[1..], &low[1..], &close[1..], &volume[1..]).scan( + (close[0], 0.0), + |state, (h, l, c, vol)| { + let mfm = if *c > state.0 { + c - f64::min(*l, state.0) + } else { + c - f64::max(*h, state.0) + }; + let mfv = mfm * vol; + let adl = state.1 + mfv; + *state = (*c, adl); + Some(adl) + }, + ), + ) + } } /// elder force index /// https://www.investopedia.com/articles/trading/03/031203.asp -pub fn elder_force(close: &[f64], volume: &[f64], window: usize) -> Vec { +pub fn elder_force<'a>( + close: &'a [f64], + volume: &'a [f64], + window: usize, +) -> impl Iterator + 'a { smooth::ewma( &izip!(&close[..close.len() - 1], &close[1..], &volume[1..]) .map(|(prev, curr, vol)| (curr - prev) * vol) @@ -112,11 +139,18 @@ pub fn elder_force(close: &[f64], volume: &[f64], window: usize) -> Vec { window, ) .collect::>() + .into_iter() } /// money flow index /// https://www.investopedia.com/terms/m/mfi.asp -pub fn mfi(high: &[f64], low: &[f64], close: &[f64], volume: &[f64], window: usize) -> Vec { +pub fn mfi<'a>( + high: &'a [f64], + low: &'a [f64], + close: &'a [f64], + volume: &'a [f64], + window: usize, +) -> impl Iterator + 'a { let (pos_mf, neg_mf): (Vec, Vec) = izip!(&high[1..], &low[1..], &close[1..], &volume[1..]) .scan( @@ -137,11 +171,18 @@ pub fn mfi(high: &[f64], low: &[f64], close: &[f64], volume: &[f64], window: usi 100.0 - (100.0 / (1.0 + pos.iter().sum::() / neg.iter().sum::())) }) .collect::>() + .into_iter() } /// chaikin money flow /// https://corporatefinanceinstitute.com/resources/equities/chaikin-money-flow-cmf/ -pub fn cmf(high: &[f64], low: &[f64], close: &[f64], volume: &[f64], window: usize) -> Vec { +pub fn cmf<'a>( + high: &'a [f64], + low: &'a [f64], + close: &'a [f64], + volume: &'a [f64], + window: usize, +) -> impl Iterator + 'a { izip!(high, low, close, volume) .map(|(h, l, c, vol)| vol * ((c - l) - (h - c)) / (h - l)) .collect::>() @@ -149,13 +190,19 @@ pub fn cmf(high: &[f64], low: &[f64], close: &[f64], volume: &[f64], window: usi .zip(volume.windows(window)) .map(|(mfv_win, v_win)| mfv_win.iter().sum::() / v_win.iter().sum::()) .collect::>() + .into_iter() } -/// trade volume index +/// Trade Volume Index /// https://www.investopedia.com/terms/t/tradevolumeindex.asp -pub fn tvi(close: &[f64], volume: &[f64], min_tick: f64) -> Vec { - izip!(&close[..close.len() - 1], &close[1..], &volume[1..],) - .scan((1, 0.0), |state, (prev, curr, vol)| { +pub fn tvi<'a>( + close: &'a [f64], + volume: &'a [f64], + min_tick: f64, +) -> impl Iterator + 'a { + izip!(&close[..close.len() - 1], &close[1..], &volume[1..],).scan( + (1, 0.0), + move |state, (prev, curr, vol)| { let direction = if curr - prev > min_tick { 1 } else if prev - curr > min_tick { @@ -166,13 +213,18 @@ pub fn tvi(close: &[f64], volume: &[f64], min_tick: f64) -> Vec { let tvi = state.1 + direction as f64 * vol; *state = (direction, tvi); Some(tvi) - }) - .collect::>() + }, + ) } /// Ease of Movement /// https://www.investopedia.com/terms/e/easeofmovement.asp -pub fn ease(high: &[f64], low: &[f64], volume: &[f64], window: usize) -> Vec { +pub fn ease<'a>( + high: &'a [f64], + low: &'a [f64], + volume: &'a [f64], + window: usize, +) -> impl Iterator + 'a { smooth::sma( &(1..high.len()) .map(|i| { @@ -184,27 +236,27 @@ pub fn ease(high: &[f64], low: &[f64], volume: &[f64], window: usize) -> Vec>() + .into_iter() } /// On-Balance Volume /// https://www.investopedia.com/terms/o/onbalancevolume.asp -pub fn obv(close: &[f64], volume: &[f64]) -> Vec { - close - .windows(2) - .enumerate() - .scan(0.0, |state, (i, pairs)| { - *state += (pairs[1] - pairs[0]).signum() * volume[i + 1]; - Some(*state) - }) - .collect::>() +pub fn obv<'a>(close: &'a [f64], volume: &'a [f64]) -> impl Iterator + 'a { + close.windows(2).enumerate().scan(0.0, |state, (i, pairs)| { + *state += (pairs[1] - pairs[0]).signum() * volume[i + 1]; + Some(*state) + }) } /// Market Facilitation Index /// https://www.metatrader5.com/en/terminal/help/indicators/bw_indicators/market_facilitation -pub fn bw_mfi(high: &[f64], low: &[f64], volume: &[f64]) -> Vec { +pub fn bw_mfi<'a>( + high: &'a [f64], + low: &'a [f64], + volume: &'a [f64], +) -> impl Iterator + 'a { high.iter() .zip(low) .zip(volume) .map(|((h, l), vol)| (h - l) / vol * (10.0_f64).powi(6)) - .collect::>() } diff --git a/tests/volume_test.rs b/tests/volume_test.rs index 867d7a3..01c1365 100644 --- a/tests/volume_test.rs +++ b/tests/volume_test.rs @@ -27,7 +27,7 @@ fn test_twiggs() { -0.14855035203055256, -0.15469018900758857, ], - result + result.collect::>() ); } @@ -56,7 +56,7 @@ fn test_kvo() { 1045791.2002935112, 491703.71571153356, ], - result + result.collect::>() ); } @@ -85,7 +85,7 @@ fn test_elder_force() { 6967489.387947403, 5483494.728818839, ], - result + result.collect::>() ); } #[test] @@ -113,14 +113,20 @@ fn test_mfi() { 87.34401989374501, 85.51147984689622, ], - result, + result.collect::>(), ); } #[test] fn test_ad() { let stats = common::test_data(); - let result = volume::ad(&stats.high, &stats.low, &stats.close, &stats.volume); + let result = volume::ad( + &stats.high, + &stats.low, + &stats.close, + &stats.volume, + Some(false), + ); assert_eq!( vec![ -12220014.53999535, @@ -158,14 +164,32 @@ fn test_ad() { -55780759.43377636, -56496069.02735695, ], - result + result.collect::>() + ); + assert_eq!( + volume::ad(&stats.high, &stats.low, &stats.close, &stats.volume, None) + .collect::>(), + volume::ad( + &stats.high, + &stats.low, + &stats.close, + &stats.volume, + Some(false) + ) + .collect::>() ); } #[test] fn test_ad_yahoo() { let stats = common::test_data(); - let result = volume::ad_yahoo(&stats.high, &stats.low, &stats.close, &stats.volume); + let result = volume::ad( + &stats.high, + &stats.low, + &stats.close, + &stats.volume, + Some(true), + ); assert_eq!( vec![ 336703421.3851929, @@ -202,7 +226,7 @@ fn test_ad_yahoo() { 304462333.9931488, 298815868.7785034, ], - result + result.collect::>() ); } @@ -232,7 +256,7 @@ fn test_cmf() { -0.1013140453290804, -0.09237139625802167, ], - result + result.collect::>() ); } @@ -248,7 +272,7 @@ fn test_tvi() { 11944400.0, 10458600.0, 13222800.0, 15901100.0, 14148200.0, 15746300.0, 18111300.0, 16923000.0, 19039700.0, 25281400.0, 38772900.0, 36090498.0, ], - result + result.collect::>() ); } @@ -277,7 +301,7 @@ fn test_ease() { 76.98413524618097, 64.64598529287485, ], - result + result.collect::>() ); } @@ -293,7 +317,7 @@ fn test_obv() { 11944400.0, 10458600.0, 13222800.0, 15901100.0, 14148200.0, 15746300.0, 18111300.0, 16923000.0, 19039700.0, 25281400.0, 38772900.0, 36090498.0, ], - result + result.collect::>() ); } @@ -338,6 +362,6 @@ fn test_bw_mfi() { 0.4046991872269693, 1.0624799989416553, ], - result + result.collect::>() ); }