From 45a056abd044fa1adb10b48127f8d83f768fc84f Mon Sep 17 00:00:00 2001 From: gord chung Date: Wed, 15 May 2024 11:15:28 -0400 Subject: [PATCH] more indicators - true range - trend intensity index - trade volume index - triple exponential average - typical price index - supertrend - linreg smooth --- src/indicator.rs | 137 ++++++++++++++++++++++++++-- src/main.rs | 8 +- src/smooth.rs | 23 +++++ tests/indicator_test.rs | 192 ++++++++++++++++++++++++++++++++++++++++ tests/ma_test.rs | 30 +++++++ 5 files changed, 383 insertions(+), 7 deletions(-) diff --git a/src/indicator.rs b/src/indicator.rs index b37e52f..4e91a98 100644 --- a/src/indicator.rs +++ b/src/indicator.rs @@ -451,12 +451,7 @@ pub fn ultimate( /// pretty good oscillator /// https://library.tradingtechnologies.com/trade/chrt-ti-pretty-good-oscillator.html pub fn pgo(high: &[f64], low: &[f64], close: &[f64], window: u8) -> Vec { - let atr = smooth::ewma( - &izip!(&high[1..], &low[1..], &close[..close.len() - 1]) - .map(|(h, l, prevc)| (h - l).max(f64::abs(h - prevc)).max(f64::abs(l - prevc))) - .collect::>(), - window, - ); + let atr = smooth::ewma(&_true_range(high, low, close).collect::>(), window); let sma_close = smooth::sma(close, window); izip!( &close[close.len() - atr.len()..], @@ -539,3 +534,133 @@ pub fn ulcer(data: &[f64], window: u8) -> Vec { .map(|x| x.sqrt()) .collect::>() } + +fn _true_range<'a>( + high: &'a [f64], + low: &'a [f64], + close: &'a [f64], +) -> impl Iterator + 'a { + izip!(&high[1..], &low[1..], &close[..close.len() - 1]) + .map(|(h, l, prevc)| (h - l).max(f64::abs(h - prevc)).max(f64::abs(l - prevc))) + .into_iter() +} + +/// true range +/// https://www.investopedia.com/terms/a/atr.asp +pub fn tr(high: &[f64], low: &[f64], close: &[f64]) -> Vec { + _true_range(high, low, close).collect::>() +} + +/// typical price +/// https://www.fidelity.com/learning-center/trading-investing/technical-analysis/technical-indicator-guide/typical-price +pub fn hlc3(high: &[f64], low: &[f64], close: &[f64], window: u8) -> Vec { + smooth::sma( + &izip!(high, low, close) + .map(|(h, l, c)| (h + l + c) / 3.0) + .collect::>(), + window, + ) +} + +/// Triple Exponential Average +/// https://www.investopedia.com/terms/t/trix.asp +pub fn trix(close: &[f64], window: u8) -> Vec { + let ema3 = smooth::ewma(&smooth::ewma(&smooth::ewma(&close, window), window), window); + ema3[..ema3.len() - 1] + .iter() + .zip(&ema3[1..]) + .map(|(prev, curr)| 100.0 * (curr - prev) / prev) + .collect::>() +} + +/// trend intensity index +/// https://www.marketvolume.com/technicalanalysis/trendintensityindex.asp +pub fn tii(data: &[f64], window: u8) -> Vec { + smooth::sma(data, window) + .iter() + .zip(&data[(window - 1) as usize..]) + .map(|(avg, actual)| { + let dev: f64 = actual - avg; + let pos_dev = if dev > 0.0 { dev } else { 0.0 }; + let neg_dev = if dev < 0.0 { dev.abs() } else { 0.0 }; + (pos_dev, neg_dev) + }) + .collect::>() + .windows(u8::div_ceil(window, 2).into()) + .map(|w| { + let mut sd_pos = 0.0; + let mut sd_neg = 0.0; + for (pos_dev, neg_dev) in w { + sd_pos += pos_dev; + sd_neg += neg_dev; + } + 100.0 * sd_pos / (sd_pos + sd_neg) + }) + .collect::>() +} + +/// 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)| { + let direction = if curr - prev > min_tick { + 1 + } else if prev - curr > min_tick { + -1 + } else { + state.0 + }; + let tvi = state.1 + direction as f64 * vol; + *state = (direction, tvi); + Some(tvi) + }) + .collect::>() +} + +/// supertrend +/// https://www.tradingview.com/support/solutions/43000634738-supertrend/ +/// https://www.investopedia.com/supertrend-indicator-7976167 +pub fn supertrend( + high: &[f64], + low: &[f64], + close: &[f64], + window: u8, + multiplier: f64, +) -> Vec { + let atr = smooth::wilder(&_true_range(high, low, close).collect::>(), window); + izip!( + &high[window.into()..], + &low[window.into()..], + &close[window.into()..], + &atr + ) + .scan( + (f64::NAN, f64::NAN, f64::MIN_POSITIVE, 1), + |state, (h, l, c, tr)| { + let (prevlower, prevupper, prevc, prevdir) = state; + let mut lower = (h + l) / 2.0 - multiplier * tr; + let mut upper = (h + l) / 2.0 + multiplier * tr; + if prevc > prevlower && *prevlower > lower { + lower = *prevlower; + } + if prevc < prevupper && *prevupper < upper { + upper = *prevupper; + } + let dir = if c > prevupper { + 1 + } else if c < prevlower { + -1 + } else { + *prevdir + }; + *state = (lower, upper, *c, dir); + if dir > 0 { + Some(lower) + } else { + Some(upper) + } + }, + ) + .collect::>() +} diff --git a/src/main.rs b/src/main.rs index 9d8151c..584a718 100644 --- a/src/main.rs +++ b/src/main.rs @@ -16,5 +16,11 @@ fn main() { let data = fs::read_to_string("./tests/rddt.input").expect("Unable to read file"); let stats: SecStats = serde_json::from_str(&data).expect("JSON does not have correct format."); - dbg!(indicator::ulcer(&stats.close, 8)); + dbg!(indicator::supertrend( + &stats.high, + &stats.low, + &stats.close, + 16, + 3.0 + )); } diff --git a/src/smooth.rs b/src/smooth.rs index f7245f3..07cf118 100644 --- a/src/smooth.rs +++ b/src/smooth.rs @@ -126,3 +126,26 @@ pub fn vma(data: &[f64], window: u8) -> Vec { .skip((u8::max(9, window) - 9).into()) .collect::>() } + +/// Linear Regression Forecast +/// aka Time series forecast +/// https://quantstrategy.io/blog/what-is-tsf-understanding-time-series-forecast-indicator/ +pub fn lrf(data: &[f64], window: u16) -> Vec { + let x_sum = (window * (window + 1)) as f64 / 2.0; + let x2_sum: f64 = x_sum * (2 * window + 1) as f64 / 3.0; + let divisor = window as f64 * x2_sum - x_sum.powi(2); + + data.windows(window.into()) + .map(|w| { + let mut y_sum = 0.0; + let mut xy_sum = 0.0; + for (count, val) in w.iter().enumerate() { + y_sum += val; + xy_sum += (count + 1) as f64 * val; + } + let m = (window as f64 * xy_sum - x_sum * y_sum) / divisor; + let b = (y_sum * x2_sum - x_sum * xy_sum) / divisor; + m * window as f64 + b + }) + .collect::>() +} diff --git a/tests/indicator_test.rs b/tests/indicator_test.rs index 92cd700..ceb6774 100644 --- a/tests/indicator_test.rs +++ b/tests/indicator_test.rs @@ -874,3 +874,195 @@ fn test_ulcer() { result ); } + +#[test] +fn test_tr() { + let stats = common::test_data(); + let result = indicator::tr(&stats.high, &stats.low, &stats.close); + assert_eq!( + vec![ + 15.939998626708984, + 15.10000228881836, + 9.490001678466797, + 8.650001525878906, + 4.924999237060547, + 7.349998474121094, + 4.69000244140625, + 3.3300018310546875, + 3.6100006103515625, + 4.1399993896484375, + 2.5900001525878906, + 3.279998779296875, + 4.340000152587891, + 2.3699989318847656, + 2.5900001525878906, + 2.8199996948242188, + 2.4399986267089844, + 4.780002593994141, + 3.660003662109375, + 2.0600013732910156, + 2.2380027770996094, + 1.5200004577636719, + 2.3000030517578125, + 3.7490005493164063, + 3.450000762939453, + 2.604999542236328, + 3.2600021362304688, + 3.918998718261726, + 2.4599990844726563, + 3.229999542236328, + 2.9300003051757813, + 5.759998321533203, + 3.1500015258789063, + ], + result + ); +} + +#[test] +fn test_hlc3() { + let stats = common::test_data(); + let result = indicator::hlc3(&stats.high, &stats.low, &stats.close, 16); + assert_eq!( + vec![ + 48.82131250699361, + 48.41006247202555, + 47.38204161326091, + 45.67329160372416, + 44.58475001653036, + 43.98891671498617, + 43.760937531789146, + 43.422812620798744, + 43.03031253814698, + 42.92550015449524, + 42.935500065485634, + 42.83081253369649, + 42.84727080663045, + 43.15249999364217, + 43.33354179064433, + 43.67937509218853, + 44.2075002193451, + 44.90875029563905, + 45.53729192415874, + ], + result + ); +} + +#[test] +fn test_trix() { + let stats = common::test_data(); + let result = indicator::trix(&stats.close, 7); + assert_eq!( + vec![ + -1.7609812348121436, + -1.58700358125189, + -1.3595873824994853, + -1.1099628496419054, + -0.8955729429209658, + -0.6091528694171965, + -0.2949587326281726, + -0.07920030554104754, + 0.1135712345224751, + 0.32952700679764474, + 0.4769166955410566, + 0.6219613185170387, + 0.7718438325710452, + 0.9560958810856369, + 1.0335513961870586, + ], + result + ); +} + +#[test] +fn test_tii_even() { + let stats = common::test_data(); + let result = indicator::tii(&stats.close, 16); + assert_eq!( + vec![ + 0.0, + 0.0, + 12.365592243625398, + 36.838871014658466, + 53.81832516603384, + 77.25510120423154, + 91.83894521268712, + 97.2706485470137, + 98.02691957272636, + 100.0, + 100.0, + 100.0, + ], + result + ); +} + +#[test] +fn test_tii_odd() { + let stats = common::test_data(); + let result = indicator::tii(&stats.close, 15); + assert_eq!( + vec![ + 0.0, + 0.6686354433953655, + 0.9316341618307428, + 17.111743788068527, + 44.82345282880815, + 61.21209452789354, + 83.24057181663895, + 96.05752982533481, + 98.58952713255982, + 98.83515680783523, + 100.0, + 100.0, + 100.0, + ], + result + ); +} + +#[test] +fn test_tvi() { + let stats = common::test_data(); + let result = indicator::tvi(&stats.close, &stats.volume, 0.5); + assert_eq!( + vec![ + 24398800.0, 59729800.0, 40971500.0, 28363400.0, 15375500.0, 24818400.0, 19995500.0, + 15377100.0, 18304100.0, 15642600.0, 13365300.0, 9015200.0, 13278200.0, 11264800.0, + 7816300.0, 9515300.0, 7361500.0, 9645600.0, 7117500.0, 8603900.0, 10560400.0, + 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 + ); +} + +#[test] +fn test_supertrend() { + let stats = common::test_data(); + let result = indicator::supertrend(&stats.high, &stats.low, &stats.close, 16, 3.0); + assert_eq!( + vec![ + 22.87718629837036, + 22.87718629837036, + 22.87718629837036, + 25.36115028045606, + 25.5548271386142, + 27.535275836318306, + 28.482134516844127, + 28.482134516844127, + 30.372852510605856, + 33.54470377638434, + 33.54470377638434, + 33.54470377638434, + 34.55477737989572, + 34.70432339242238, + 35.73655158775987, + 36.52801923545023, + 39.78407957956028, + 39.78407957956028, + ], + result + ); +} diff --git a/tests/ma_test.rs b/tests/ma_test.rs index 71aa106..dfe3963 100644 --- a/tests/ma_test.rs +++ b/tests/ma_test.rs @@ -480,3 +480,33 @@ fn test_wilder_odd() { results ); } + +#[test] +fn test_lrf() { + let stats = common::test_data(); + let result = smooth::lrf(&stats.close, 16); + assert_eq!( + vec![ + 40.43308849895702, + 38.47007355970494, + 37.890220025006464, + 39.01044091056375, + 39.53514696570004, + 39.64264704199398, + 39.66977935678818, + 40.40161794774673, + 40.70242705064662, + 41.63139747170841, + 42.98433842378505, + 43.769852946786315, + 44.77757378185497, + 45.816985719344196, + 46.79602951162002, + 47.817573743707996, + 48.68058875027825, + 49.934633058660175, + 50.23628669626572, + ], + result + ); +}