use super::hilbert::HilbertPhaseState;
use super::to_internal;
use crate::{
common::validate_finite_value, validate_finite_slice, validate_input_len, validate_output_len,
CompactOutput, Float, IndicatorConfig, OutputRange, PreparedBatchRunner, Result,
StreamingComputation, TalibError,
};
#[cfg(not(feature = "std"))]
use alloc::{vec, vec::Vec};
#[cfg(feature = "std")]
use std::{vec, vec::Vec};
const HISTORY_SIZE: usize = 50;
const DEGREE_TO_RADIAN: f64 = core::f64::consts::PI / 180.0;
pub const HT_TRENDMODE_LOOKBACK: usize = 63;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum TrendMode {
Cycle,
Trend,
}
#[derive(Debug, Clone, Copy, PartialEq)]
struct HilbertTrendModeState {
phase: HilbertPhaseState,
raw_price: [f64; HISTORY_SIZE],
raw_price_index: usize,
trend_average_1: f64,
trend_average_2: f64,
trend_average_3: f64,
days_in_trend: usize,
previous_phase: f64,
sine: f64,
lead_sine: f64,
}
impl Default for HilbertTrendModeState {
fn default() -> Self {
Self {
phase: HilbertPhaseState::default(),
raw_price: [0.0; HISTORY_SIZE],
raw_price_index: 0,
trend_average_1: 0.0,
trend_average_2: 0.0,
trend_average_3: 0.0,
days_in_trend: 0,
previous_phase: 0.0,
sine: 0.0,
lead_sine: 0.0,
}
}
}
impl HilbertTrendModeState {
#[inline]
const fn observations(&self) -> usize {
self.phase.observations()
}
#[inline]
fn reset(&mut self) {
*self = Self::default();
}
#[inline]
fn next(&mut self, input: f64) -> Option<TrendMode> {
self.raw_price[self.raw_price_index] = input;
self.raw_price_index = (self.raw_price_index + 1) % HISTORY_SIZE;
let transition = self.phase.next_phase_transition(input)?;
let previous_phase = self.previous_phase;
self.previous_phase = transition.phase;
let previous_sine = self.sine;
let previous_lead_sine = self.lead_sine;
self.sine = (transition.phase * DEGREE_TO_RADIAN).sin();
self.lead_sine = ((transition.phase + 45.0) * DEGREE_TO_RADIAN).sin();
let period = (transition.smooth_period + 0.5) as usize;
let mut average = 0.0;
let mut index = (self.raw_price_index + HISTORY_SIZE - 1) % HISTORY_SIZE;
for _ in 0..period {
average += self.raw_price[index];
index = (index + HISTORY_SIZE - 1) % HISTORY_SIZE;
}
if period != 0 {
average /= period as f64;
}
let trendline = (4.0 * average
+ 3.0 * self.trend_average_1
+ 2.0 * self.trend_average_2
+ self.trend_average_3)
/ 10.0;
self.trend_average_3 = self.trend_average_2;
self.trend_average_2 = self.trend_average_1;
self.trend_average_1 = average;
let crossed = (self.sine > self.lead_sine && previous_sine <= previous_lead_sine)
|| (self.sine < self.lead_sine && previous_sine >= previous_lead_sine);
let mut mode = TrendMode::Trend;
if crossed {
self.days_in_trend = 0;
mode = TrendMode::Cycle;
}
self.days_in_trend += 1;
if (self.days_in_trend as f64) < 0.5 * transition.smooth_period {
mode = TrendMode::Cycle;
}
let phase_change = transition.phase - previous_phase;
let expected_change = 360.0 / transition.smooth_period;
if phase_change > 0.67 * expected_change && phase_change < 1.5 * expected_change {
mode = TrendMode::Cycle;
}
if trendline != 0.0 && ((transition.smoothed_value - trendline) / trendline).abs() >= 0.015
{
mode = TrendMode::Trend;
}
(transition.today >= HT_TRENDMODE_LOOKBACK).then_some(mode)
}
}
#[inline]
fn validate_input(real: &[Float]) -> Result<usize> {
validate_finite_slice("real", real)?;
validate_input_len(real.len(), HT_TRENDMODE_LOOKBACK)
}
#[inline]
fn compute_validated(real: &[Float], output: &mut [TrendMode]) -> OutputRange {
let mut state = HilbertTrendModeState::default();
let mut output_index = 0;
for input in real.iter().copied() {
if let Some(mode) = state.next(to_internal(input)) {
output[output_index] = mode;
output_index += 1;
}
}
if output_index == 0 {
OutputRange::empty()
} else {
OutputRange::new(HT_TRENDMODE_LOOKBACK, output_index)
}
}
#[allow(non_snake_case)]
pub fn HT_TRENDMODE(real: &[Float], output: &mut [TrendMode]) -> Result<OutputRange> {
let count = validate_input(real)?;
validate_output_len("HT_TRENDMODE", output.len(), count)?;
Ok(compute_validated(real, output))
}
#[allow(non_camel_case_types)]
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
pub struct HT_TRENDMODEConfig;
impl HT_TRENDMODEConfig {
#[inline]
pub const fn new() -> Self {
Self
}
}
impl crate::traits::sealed::Sealed for HT_TRENDMODEConfig {}
impl IndicatorConfig for HT_TRENDMODEConfig {
type Input<'a> = &'a [Float];
type Output = Vec<TrendMode>;
type OutputMut<'a> = &'a mut [TrendMode];
type BatchRunner = HT_TRENDMODEBatchRunner;
type Stream = HT_TRENDMODEStream;
#[inline]
fn lookback(&self) -> usize {
HT_TRENDMODE_LOOKBACK
}
fn compute<'a>(&self, input: Self::Input<'a>) -> Result<CompactOutput<Self::Output>> {
let count = validate_input(input)?;
let mut values = vec![TrendMode::Cycle; count];
let range = compute_validated(input, values.as_mut_slice());
CompactOutput::new(input.len(), range, values)
}
#[inline]
fn compute_into<'a>(
&self,
input: Self::Input<'a>,
output: Self::OutputMut<'a>,
) -> Result<OutputRange> {
HT_TRENDMODE(input, output)
}
#[inline]
fn prepare_batch(&self, max_input_len: usize) -> Result<Self::BatchRunner> {
Ok(HT_TRENDMODEBatchRunner { max_input_len })
}
#[inline]
fn stream(&self) -> Result<Self::Stream> {
Ok(HT_TRENDMODEStream::default())
}
}
#[allow(non_camel_case_types)]
#[derive(Debug, Clone)]
pub struct HT_TRENDMODEBatchRunner {
max_input_len: usize,
}
impl crate::traits::sealed::Sealed for HT_TRENDMODEBatchRunner {}
impl PreparedBatchRunner<HT_TRENDMODEConfig> for HT_TRENDMODEBatchRunner {
#[inline]
fn max_input_len(&self) -> usize {
self.max_input_len
}
#[inline]
fn compute_into<'a>(
&mut self,
input: <HT_TRENDMODEConfig as IndicatorConfig>::Input<'a>,
output: <HT_TRENDMODEConfig as IndicatorConfig>::OutputMut<'a>,
) -> Result<OutputRange>
where
HT_TRENDMODEConfig: 'a,
{
if input.len() > self.max_input_len {
return Err(TalibError::prepared_capacity_exceeded(
self.max_input_len,
input.len(),
));
}
HT_TRENDMODE(input, output)
}
}
#[allow(non_camel_case_types)]
#[derive(Debug, Clone, Copy, Default, PartialEq)]
pub struct HT_TRENDMODEStream {
state: HilbertTrendModeState,
}
impl crate::traits::sealed::Sealed for HT_TRENDMODEStream {}
impl StreamingComputation<HT_TRENDMODEConfig> for HT_TRENDMODEStream {
type Tick = Float;
type TickOutput = TrendMode;
#[inline]
fn next(&mut self, input: Self::Tick) -> Result<Option<Self::TickOutput>> {
validate_finite_value("input", self.state.observations(), input)?;
Ok(self.state.next(to_internal(input)))
}
#[inline]
fn reset(&mut self) {
self.state.reset();
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn compact_count_matches_fixed_lookback() {
assert_eq!(
crate::output_count(HT_TRENDMODE_LOOKBACK, HT_TRENDMODE_LOOKBACK),
0
);
assert_eq!(
crate::output_count(HT_TRENDMODE_LOOKBACK + 1, HT_TRENDMODE_LOOKBACK),
1
);
}
}