feat: init the repo
This commit is contained in:
@@ -0,0 +1,187 @@
|
||||
// Shared Hilbert Transform Core
|
||||
// Based on John Ehlers' Discrete Hilbert Transform as implemented in TA-Lib.
|
||||
// Reference: "Cybernetic Analysis for Stocks and Futures" by J.F. Ehlers
|
||||
//
|
||||
// All HT functions share a 63-bar lookback period.
|
||||
|
||||
use std::f64::consts::PI;
|
||||
|
||||
pub(super) const HT_LOOKBACK: usize = 63;
|
||||
|
||||
/// Shared output from the core Hilbert Transform computation.
|
||||
pub(super) struct HtCore {
|
||||
pub(super) trendline: Vec<f64>,
|
||||
pub(super) dc_period: Vec<f64>,
|
||||
pub(super) dc_phase: Vec<f64>,
|
||||
pub(super) inphase: Vec<f64>,
|
||||
pub(super) quadrature: Vec<f64>,
|
||||
pub(super) trend_mode: Vec<i32>,
|
||||
}
|
||||
|
||||
/// Run the full Hilbert Transform pipeline on a slice of close prices.
|
||||
pub(super) fn compute_ht_core(prices: &[f64]) -> HtCore {
|
||||
let n = prices.len();
|
||||
|
||||
let mut trendline = vec![f64::NAN; n];
|
||||
let mut dc_period = vec![f64::NAN; n];
|
||||
let mut dc_phase = vec![f64::NAN; n];
|
||||
let mut inphase = vec![f64::NAN; n];
|
||||
let mut quadrature = vec![f64::NAN; n];
|
||||
let mut trend_mode = vec![0i32; n];
|
||||
|
||||
if n <= HT_LOOKBACK {
|
||||
return HtCore {
|
||||
trendline,
|
||||
dc_period,
|
||||
dc_phase,
|
||||
inphase,
|
||||
quadrature,
|
||||
trend_mode,
|
||||
};
|
||||
}
|
||||
|
||||
// Step 1: Smooth the price series (4-bar weighted average)
|
||||
let mut smooth = vec![0.0f64; n];
|
||||
for i in 0..n {
|
||||
smooth[i] = if i >= 3 {
|
||||
(4.0 * prices[i] + 3.0 * prices[i - 1] + 2.0 * prices[i - 2] + prices[i - 3]) / 10.0
|
||||
} else {
|
||||
prices[i]
|
||||
};
|
||||
}
|
||||
|
||||
// Step 2: Full Hilbert Transform pipeline
|
||||
let mut detrender = vec![0.0f64; n];
|
||||
let mut q1 = vec![0.0f64; n];
|
||||
let mut i1 = vec![0.0f64; n];
|
||||
let mut ji = vec![0.0f64; n];
|
||||
let mut jq = vec![0.0f64; n];
|
||||
let mut i2 = vec![0.0f64; n];
|
||||
let mut q2 = vec![0.0f64; n];
|
||||
let mut re = vec![0.0f64; n];
|
||||
let mut im = vec![0.0f64; n];
|
||||
let mut period = vec![0.0f64; n];
|
||||
let mut smooth_period = vec![0.0f64; n];
|
||||
let mut phase = vec![0.0f64; n];
|
||||
|
||||
for i in 6..n {
|
||||
let prev_period = period[i - 1];
|
||||
// Alpha coefficient for HT filters depends on the current period estimate
|
||||
let alpha = 0.075 * prev_period + 0.54;
|
||||
|
||||
// Discrete Hilbert Transform of smooth price (detrender)
|
||||
detrender[i] = (0.0962 * smooth[i] + 0.5769 * smooth[i - 2]
|
||||
- 0.5769 * smooth[i - 4]
|
||||
- 0.0962 * smooth[i - 6])
|
||||
* alpha;
|
||||
|
||||
// Q1: HT of detrender
|
||||
if i >= 12 {
|
||||
q1[i] = (0.0962 * detrender[i] + 0.5769 * detrender[i - 2]
|
||||
- 0.5769 * detrender[i - 4]
|
||||
- 0.0962 * detrender[i - 6])
|
||||
* alpha;
|
||||
}
|
||||
|
||||
// I1: delayed detrender
|
||||
if i >= 9 {
|
||||
i1[i] = detrender[i - 3];
|
||||
}
|
||||
|
||||
// jI: HT of I1
|
||||
if i >= 15 {
|
||||
ji[i] = (0.0962 * i1[i] + 0.5769 * i1[i - 2] - 0.5769 * i1[i - 4] - 0.0962 * i1[i - 6])
|
||||
* alpha;
|
||||
}
|
||||
|
||||
// jQ: HT of Q1
|
||||
if i >= 18 {
|
||||
jq[i] = (0.0962 * q1[i] + 0.5769 * q1[i - 2] - 0.5769 * q1[i - 4] - 0.0962 * q1[i - 6])
|
||||
* alpha;
|
||||
}
|
||||
|
||||
// Phase components
|
||||
let i2_raw = i1[i] - jq[i];
|
||||
let q2_raw = q1[i] + ji[i];
|
||||
|
||||
// EMA smoothing of I2 and Q2
|
||||
let i2_prev = i2[i - 1];
|
||||
let q2_prev = q2[i - 1];
|
||||
i2[i] = 0.2 * i2_raw + 0.8 * i2_prev;
|
||||
q2[i] = 0.2 * q2_raw + 0.8 * q2_prev;
|
||||
|
||||
// Cross-product for period estimation
|
||||
let re_raw = i2[i] * i2_prev + q2[i] * q2_prev;
|
||||
let im_raw = i2[i] * q2_prev - q2[i] * i2_prev;
|
||||
|
||||
// EMA smoothing of Re and Im
|
||||
re[i] = 0.2 * re_raw + 0.8 * re[i - 1];
|
||||
im[i] = 0.2 * im_raw + 0.8 * im[i - 1];
|
||||
|
||||
// Compute period from cross-product of consecutive phasors.
|
||||
// Uses atan(Im/Re) per Ehlers' convention; guard against negative Re
|
||||
// which would flip the sign of the period estimate.
|
||||
let mut p = if re[i] != 0.0 && im[i] != 0.0 && re[i] > 0.0 {
|
||||
2.0 * PI / (im[i] / re[i]).atan()
|
||||
} else {
|
||||
prev_period
|
||||
};
|
||||
|
||||
// Clamp period relative to previous
|
||||
if prev_period > 0.0 {
|
||||
if p > 1.5 * prev_period {
|
||||
p = 1.5 * prev_period;
|
||||
}
|
||||
if p < 0.67 * prev_period {
|
||||
p = 0.67 * prev_period;
|
||||
}
|
||||
}
|
||||
// Hard clamp to [6, 50] bars
|
||||
p = p.clamp(6.0, 50.0);
|
||||
|
||||
// EMA smooth the period
|
||||
period[i] = 0.2 * p + 0.8 * prev_period;
|
||||
|
||||
// Smooth the smoothed period once more
|
||||
smooth_period[i] = 0.33 * period[i] + 0.67 * smooth_period[i - 1];
|
||||
|
||||
// Phase from I1 and Q1
|
||||
phase[i] = if i1[i] != 0.0 {
|
||||
q1[i].atan2(i1[i]) * 180.0 / PI
|
||||
} else if q1[i] > 0.0 {
|
||||
90.0
|
||||
} else if q1[i] < 0.0 {
|
||||
-90.0
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
|
||||
// Write outputs once past lookback
|
||||
if i >= HT_LOOKBACK {
|
||||
dc_period[i] = smooth_period[i];
|
||||
dc_phase[i] = phase[i];
|
||||
inphase[i] = i1[i];
|
||||
quadrature[i] = q1[i];
|
||||
|
||||
// Trend mode: cycle when SmoothPeriod >= 20, trend when < 20
|
||||
trend_mode[i] = if smooth_period[i] < 20.0 { 1 } else { 0 };
|
||||
}
|
||||
}
|
||||
|
||||
// Trendline: average over the current dominant cycle period
|
||||
for i in HT_LOOKBACK..n {
|
||||
let sp = smooth_period[i];
|
||||
let dc = (sp.round() as usize).max(1).min(i + 1);
|
||||
let sum: f64 = (0..dc).map(|j| smooth[i - j]).sum();
|
||||
trendline[i] = sum / dc as f64;
|
||||
}
|
||||
|
||||
HtCore {
|
||||
trendline,
|
||||
dc_period,
|
||||
dc_phase,
|
||||
inphase,
|
||||
quadrature,
|
||||
trend_mode,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
use super::common::compute_ht_core;
|
||||
use numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};
|
||||
use pyo3::prelude::*;
|
||||
|
||||
/// Hilbert Transform Dominant Cycle Period in bars.
|
||||
#[pyfunction]
|
||||
pub fn ht_dcperiod<'py>(
|
||||
py: Python<'py>,
|
||||
close: PyReadonlyArray1<'py, f64>,
|
||||
) -> PyResult<Bound<'py, PyArray1<f64>>> {
|
||||
let prices = close.as_slice()?;
|
||||
let core = compute_ht_core(prices);
|
||||
Ok(core.dc_period.into_pyarray(py))
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
use super::common::compute_ht_core;
|
||||
use numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};
|
||||
use pyo3::prelude::*;
|
||||
|
||||
/// Hilbert Transform Dominant Cycle Phase in degrees.
|
||||
#[pyfunction]
|
||||
pub fn ht_dcphase<'py>(
|
||||
py: Python<'py>,
|
||||
close: PyReadonlyArray1<'py, f64>,
|
||||
) -> PyResult<Bound<'py, PyArray1<f64>>> {
|
||||
let prices = close.as_slice()?;
|
||||
let core = compute_ht_core(prices);
|
||||
Ok(core.dc_phase.into_pyarray(py))
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
use super::common::compute_ht_core;
|
||||
use numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};
|
||||
use pyo3::prelude::*;
|
||||
|
||||
/// Hilbert Transform Phasor components. Returns (inphase, quadrature) tuple.
|
||||
#[pyfunction]
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub fn ht_phasor<'py>(
|
||||
py: Python<'py>,
|
||||
close: PyReadonlyArray1<'py, f64>,
|
||||
) -> PyResult<(Bound<'py, PyArray1<f64>>, Bound<'py, PyArray1<f64>>)> {
|
||||
let prices = close.as_slice()?;
|
||||
let core = compute_ht_core(prices);
|
||||
Ok((
|
||||
core.inphase.into_pyarray(py),
|
||||
core.quadrature.into_pyarray(py),
|
||||
))
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
use super::common::{compute_ht_core, HT_LOOKBACK};
|
||||
use numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};
|
||||
use pyo3::prelude::*;
|
||||
use std::f64::consts::PI;
|
||||
|
||||
/// Hilbert Transform SineWave. Returns (sine, leadsine) where leadsine leads sine by 45°.
|
||||
#[pyfunction]
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub fn ht_sine<'py>(
|
||||
py: Python<'py>,
|
||||
close: PyReadonlyArray1<'py, f64>,
|
||||
) -> PyResult<(Bound<'py, PyArray1<f64>>, Bound<'py, PyArray1<f64>>)> {
|
||||
let prices = close.as_slice()?;
|
||||
let n = prices.len();
|
||||
let core = compute_ht_core(prices);
|
||||
|
||||
let mut sine = vec![f64::NAN; n];
|
||||
let mut lead_sine = vec![f64::NAN; n];
|
||||
|
||||
for i in HT_LOOKBACK..n {
|
||||
if !core.dc_phase[i].is_nan() {
|
||||
let phase_rad = core.dc_phase[i] * PI / 180.0;
|
||||
sine[i] = phase_rad.sin();
|
||||
lead_sine[i] = (phase_rad + PI / 4.0).sin(); // 45-degree lead
|
||||
}
|
||||
}
|
||||
|
||||
Ok((sine.into_pyarray(py), lead_sine.into_pyarray(py)))
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
use super::common::compute_ht_core;
|
||||
use numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};
|
||||
use pyo3::prelude::*;
|
||||
|
||||
/// Hilbert Transform Instantaneous Trendline (Ehlers). Smooths price over the dominant cycle period.
|
||||
#[pyfunction]
|
||||
pub fn ht_trendline<'py>(
|
||||
py: Python<'py>,
|
||||
close: PyReadonlyArray1<'py, f64>,
|
||||
) -> PyResult<Bound<'py, PyArray1<f64>>> {
|
||||
let prices = close.as_slice()?;
|
||||
let core = compute_ht_core(prices);
|
||||
Ok(core.trendline.into_pyarray(py))
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
use super::common::compute_ht_core;
|
||||
use numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};
|
||||
use pyo3::prelude::*;
|
||||
|
||||
/// Hilbert Transform Trend vs Cycle Mode: 1 = trending, 0 = cycling.
|
||||
#[pyfunction]
|
||||
pub fn ht_trendmode<'py>(
|
||||
py: Python<'py>,
|
||||
close: PyReadonlyArray1<'py, f64>,
|
||||
) -> PyResult<Bound<'py, PyArray1<i32>>> {
|
||||
let prices = close.as_slice()?;
|
||||
let core = compute_ht_core(prices);
|
||||
Ok(core.trend_mode.into_pyarray(py))
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
//! Cycle indicators — Hilbert Transform-based cycle analysis (Ehlers).
|
||||
//! The shared HT core computation lives in `common.rs`; each indicator has its own file.
|
||||
//!
|
||||
//! All functions use a 63-bar lookback period (first 63 values are NaN).
|
||||
|
||||
mod common;
|
||||
|
||||
mod ht_dcperiod;
|
||||
mod ht_dcphase;
|
||||
mod ht_phasor;
|
||||
mod ht_sine;
|
||||
mod ht_trendline;
|
||||
mod ht_trendmode;
|
||||
|
||||
use pyo3::prelude::*;
|
||||
|
||||
pub fn register(m: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||
m.add_function(pyo3::wrap_pyfunction!(self::ht_trendline::ht_trendline, m)?)?;
|
||||
m.add_function(pyo3::wrap_pyfunction!(self::ht_dcperiod::ht_dcperiod, m)?)?;
|
||||
m.add_function(pyo3::wrap_pyfunction!(self::ht_dcphase::ht_dcphase, m)?)?;
|
||||
m.add_function(pyo3::wrap_pyfunction!(self::ht_phasor::ht_phasor, m)?)?;
|
||||
m.add_function(pyo3::wrap_pyfunction!(self::ht_sine::ht_sine, m)?)?;
|
||||
m.add_function(pyo3::wrap_pyfunction!(self::ht_trendmode::ht_trendmode, m)?)?;
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user