chore: prepare v1.1.0 release
Update version numbers across Rust, Python, and documentation files to 1.1.0. Enhance the .gitignore to include macOS dSYM files and plans directory. Introduce new dependencies in the Rust core library and update the README to reflect recent performance benchmarks and backtesting engine capabilities. Add new artifacts to the benchmarks manifest and improve documentation for the backtesting engine API.
This commit is contained in:
@@ -1,187 +0,0 @@
|
||||
// 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,
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
use super::common::compute_ht_core;
|
||||
use numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};
|
||||
use pyo3::prelude::*;
|
||||
|
||||
@@ -8,7 +7,6 @@ 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))
|
||||
let result = ferro_ta_core::cycle::ht_dcperiod(close.as_slice()?);
|
||||
Ok(result.into_pyarray(py))
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
use super::common::compute_ht_core;
|
||||
use numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};
|
||||
use pyo3::prelude::*;
|
||||
|
||||
@@ -8,7 +7,6 @@ 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))
|
||||
let result = ferro_ta_core::cycle::ht_dcphase(close.as_slice()?);
|
||||
Ok(result.into_pyarray(py))
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
use super::common::compute_ht_core;
|
||||
use numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};
|
||||
use pyo3::prelude::*;
|
||||
|
||||
@@ -9,10 +8,6 @@ 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),
|
||||
))
|
||||
let (inphase, quadrature) = ferro_ta_core::cycle::ht_phasor(close.as_slice()?);
|
||||
Ok((inphase.into_pyarray(py), quadrature.into_pyarray(py)))
|
||||
}
|
||||
|
||||
+1
-17
@@ -1,7 +1,5 @@
|
||||
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]
|
||||
@@ -10,20 +8,6 @@ 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
|
||||
}
|
||||
}
|
||||
|
||||
let (sine, lead_sine) = ferro_ta_core::cycle::ht_sine(close.as_slice()?);
|
||||
Ok((sine.into_pyarray(py), lead_sine.into_pyarray(py)))
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
use super::common::compute_ht_core;
|
||||
use numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};
|
||||
use pyo3::prelude::*;
|
||||
|
||||
@@ -8,7 +7,6 @@ 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))
|
||||
let result = ferro_ta_core::cycle::ht_trendline(close.as_slice()?);
|
||||
Ok(result.into_pyarray(py))
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
use super::common::compute_ht_core;
|
||||
use numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};
|
||||
use pyo3::prelude::*;
|
||||
|
||||
@@ -8,7 +7,6 @@ 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))
|
||||
let result = ferro_ta_core::cycle::ht_trendmode(close.as_slice()?);
|
||||
Ok(result.into_pyarray(py))
|
||||
}
|
||||
|
||||
@@ -3,8 +3,6 @@
|
||||
//!
|
||||
//! All functions use a 63-bar lookback period (first 63 values are NaN).
|
||||
|
||||
mod common;
|
||||
|
||||
mod ht_dcperiod;
|
||||
mod ht_dcphase;
|
||||
mod ht_phasor;
|
||||
|
||||
Reference in New Issue
Block a user