feat: init the repo

This commit is contained in:
Pratik Bhadane
2026-03-23 23:34:28 +05:30
commit 7a5a220dfe
344 changed files with 75728 additions and 0 deletions
+30
View File
@@ -0,0 +1,30 @@
use crate::validation;
use numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};
use pyo3::prelude::*;
/// Bollinger Bands. Returns (upper, middle, lower). Middle is SMA; bands are ± nbdev * stddev.
#[pyfunction]
#[pyo3(signature = (close, timeperiod = 5, nbdevup = 2.0, nbdevdn = 2.0))]
#[allow(clippy::type_complexity)]
pub fn bbands<'py>(
py: Python<'py>,
close: PyReadonlyArray1<'py, f64>,
timeperiod: usize,
nbdevup: f64,
nbdevdn: f64,
) -> PyResult<(
Bound<'py, PyArray1<f64>>,
Bound<'py, PyArray1<f64>>,
Bound<'py, PyArray1<f64>>,
)> {
validation::validate_timeperiod(timeperiod, "timeperiod", 1)?;
let prices = close.as_slice()?;
log::debug!("BBANDS: timeperiod={timeperiod}, n={}", prices.len());
let (upper, middle, lower) =
ferro_ta_core::overlap::bbands(prices, timeperiod, nbdevup, nbdevdn);
Ok((
upper.into_pyarray(py),
middle.into_pyarray(py),
lower.into_pyarray(py),
))
}
+40
View File
@@ -0,0 +1,40 @@
use crate::validation;
use numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};
use pyo3::exceptions::PyValueError;
use pyo3::prelude::*;
use ta::indicators::ExponentialMovingAverage;
use ta::Next;
/// Double Exponential Moving Average. Converges after ~2*(timeperiod-1) bars.
#[pyfunction]
#[pyo3(signature = (close, timeperiod = 30))]
pub fn dema<'py>(
py: Python<'py>,
close: PyReadonlyArray1<'py, f64>,
timeperiod: usize,
) -> PyResult<Bound<'py, PyArray1<f64>>> {
validation::validate_timeperiod(timeperiod, "timeperiod", 1)?;
let prices = close.as_slice()?;
let n = prices.len();
let mut ema1 = ExponentialMovingAverage::new(timeperiod)
.map_err(|e| PyValueError::new_err(e.to_string()))?;
let mut ema2 = ExponentialMovingAverage::new(timeperiod)
.map_err(|e| PyValueError::new_err(e.to_string()))?;
let warmup = 2 * (timeperiod - 1);
let mut ema1_vals = vec![f64::NAN; n];
let mut result = vec![f64::NAN; n];
for (i, &price) in prices.iter().enumerate() {
let v1 = ema1.next(price);
if i + 1 >= timeperiod {
ema1_vals[i] = v1;
let v2 = ema2.next(v1);
if i >= warmup {
result[i] = 2.0 * v1 - v2;
}
}
}
Ok(result.into_pyarray(py))
}
+19
View File
@@ -0,0 +1,19 @@
use crate::validation;
use numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};
use pyo3::prelude::*;
/// Exponential Moving Average. Leading timeperiod-1 values are NaN.
#[pyfunction]
#[pyo3(signature = (close, timeperiod = 30))]
pub fn ema<'py>(
py: Python<'py>,
close: PyReadonlyArray1<'py, f64>,
timeperiod: usize,
) -> PyResult<Bound<'py, PyArray1<f64>>> {
validation::validate_timeperiod(timeperiod, "timeperiod", 1)?;
let prices = close.as_slice()?;
let n = prices.len();
log::debug!("EMA: timeperiod={timeperiod}, n={n}");
let result = ferro_ta_core::overlap::ema(prices, timeperiod);
Ok(result.into_pyarray(py))
}
+43
View File
@@ -0,0 +1,43 @@
use crate::validation;
use numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};
use pyo3::prelude::*;
/// Kaufman Adaptive Moving Average. First value at index timeperiod-1.
#[pyfunction]
#[pyo3(signature = (close, timeperiod = 30))]
pub fn kama<'py>(
py: Python<'py>,
close: PyReadonlyArray1<'py, f64>,
timeperiod: usize,
) -> PyResult<Bound<'py, PyArray1<f64>>> {
validation::validate_timeperiod(timeperiod, "timeperiod", 1)?;
let prices = close.as_slice()?;
let n = prices.len();
if n < timeperiod {
return Ok(vec![f64::NAN; n].into_pyarray(py));
}
let fast_sc = 2.0 / (2.0 + 1.0_f64);
let slow_sc = 2.0 / (30.0 + 1.0_f64);
let mut result = vec![f64::NAN; n];
let mut kama_val = prices[timeperiod - 1];
result[timeperiod - 1] = kama_val;
for i in timeperiod..n {
let direction = (prices[i] - prices[i - timeperiod]).abs();
let mut volatility = 0.0_f64;
for j in 1..=timeperiod {
volatility += (prices[i - j + 1] - prices[i - j]).abs();
}
let er = if volatility > 0.0 {
direction / volatility
} else {
0.0
};
let sc = (er * (fast_sc - slow_sc) + slow_sc).powi(2);
kama_val += sc * (prices[i] - kama_val);
result[i] = kama_val;
}
Ok(result.into_pyarray(py))
}
+58
View File
@@ -0,0 +1,58 @@
use crate::validation;
use numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};
use pyo3::exceptions::PyValueError;
use pyo3::prelude::*;
use super::{dema, ema, kama, sma, t3, tema, trima, wma};
/// Generic Moving Average. matype: 0=SMA, 1=EMA, 2=WMA, 3=DEMA, 4=TEMA, 5=TRIMA, 6=KAMA, 7=T3.
#[pyfunction]
#[pyo3(signature = (close, timeperiod = 30, matype = 0))]
pub fn ma<'py>(
py: Python<'py>,
close: PyReadonlyArray1<'py, f64>,
timeperiod: usize,
matype: u8,
) -> PyResult<Bound<'py, PyArray1<f64>>> {
validation::validate_timeperiod(timeperiod, "timeperiod", 1)?;
match matype {
0 => sma::sma_inner(py, close, timeperiod),
1 => ema::ema(py, close, timeperiod),
2 => wma::wma(py, close, timeperiod),
3 => dema::dema(py, close, timeperiod),
4 => tema::tema(py, close, timeperiod),
5 => trima::trima(py, close, timeperiod),
6 => kama::kama(py, close, timeperiod),
7 => t3::t3(py, close, timeperiod, 0.7),
_ => Err(PyValueError::new_err(
"matype must be 07 (SMA/EMA/WMA/DEMA/TEMA/TRIMA/KAMA/T3)",
)),
}
}
/// Moving Average with variable period per bar (SMA over period from periods array).
#[pyfunction]
#[pyo3(signature = (close, periods, minperiod = 2, maxperiod = 30))]
pub fn mavp<'py>(
py: Python<'py>,
close: PyReadonlyArray1<'py, f64>,
periods: PyReadonlyArray1<'py, f64>,
minperiod: usize,
maxperiod: usize,
) -> PyResult<Bound<'py, PyArray1<f64>>> {
let prices = close.as_slice()?;
let per = periods.as_slice()?;
let n = prices.len();
validation::validate_equal_length(&[(n, "close"), (per.len(), "periods")])?;
validation::validate_timeperiod(minperiod, "minperiod", 1)?;
validation::validate_timeperiod(maxperiod, "maxperiod", minperiod)?;
let mut result = vec![f64::NAN; n];
for i in 0..n {
let p = (per[i].round() as usize).clamp(minperiod, maxperiod);
if i + 1 >= p {
let sum: f64 = prices[(i + 1 - p)..=i].iter().sum();
result[i] = sum / p as f64;
}
}
Ok(result.into_pyarray(py))
}
+57
View File
@@ -0,0 +1,57 @@
use crate::validation;
use numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};
use pyo3::exceptions::PyValueError;
use pyo3::prelude::*;
/// MACD (EMA-based). Returns (macd_line, signal_line, histogram).
#[pyfunction]
#[pyo3(signature = (close, fastperiod = 12, slowperiod = 26, signalperiod = 9))]
#[allow(clippy::type_complexity)]
pub fn macd<'py>(
py: Python<'py>,
close: PyReadonlyArray1<'py, f64>,
fastperiod: usize,
slowperiod: usize,
signalperiod: usize,
) -> PyResult<(
Bound<'py, PyArray1<f64>>,
Bound<'py, PyArray1<f64>>,
Bound<'py, PyArray1<f64>>,
)> {
validation::validate_timeperiod(fastperiod, "fastperiod", 1)?;
validation::validate_timeperiod(slowperiod, "slowperiod", 1)?;
validation::validate_timeperiod(signalperiod, "signalperiod", 1)?;
if fastperiod >= slowperiod {
return Err(PyValueError::new_err(
"fastperiod must be less than slowperiod",
));
}
let prices = close.as_slice()?;
log::debug!(
"MACD: fast={fastperiod}, slow={slowperiod}, signal={signalperiod}, n={}",
prices.len()
);
let (macd_line, signal_line, histogram) =
ferro_ta_core::overlap::macd(prices, fastperiod, slowperiod, signalperiod);
Ok((
macd_line.into_pyarray(py),
signal_line.into_pyarray(py),
histogram.into_pyarray(py),
))
}
/// MACD with fixed 12/26 periods. Returns (macd_line, signal_line, histogram).
#[pyfunction]
#[pyo3(signature = (close, signalperiod = 9))]
#[allow(clippy::type_complexity)]
pub fn macdfix<'py>(
py: Python<'py>,
close: PyReadonlyArray1<'py, f64>,
signalperiod: usize,
) -> PyResult<(
Bound<'py, PyArray1<f64>>,
Bound<'py, PyArray1<f64>>,
Bound<'py, PyArray1<f64>>,
)> {
macd(py, close, 12, 26, signalperiod)
}
+116
View File
@@ -0,0 +1,116 @@
use crate::validation;
use numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};
use pyo3::exceptions::PyValueError;
use pyo3::prelude::*;
fn compute_ma_slice(prices: &[f64], period: usize, matype: u8) -> Vec<f64> {
let n = prices.len();
match matype {
1 => {
if period == 0 {
return vec![f64::NAN; n];
}
let k = 2.0 / (period as f64 + 1.0);
let mut result = vec![f64::NAN; n];
let mut ema_val = prices[period - 1];
result[period - 1] = ema_val;
for i in period..n {
ema_val = prices[i] * k + ema_val * (1.0 - k);
result[i] = ema_val;
}
result
}
2 => {
if period == 0 {
return vec![f64::NAN; n];
}
let weight_sum = (period * (period + 1) / 2) as f64;
let mut result = vec![f64::NAN; n];
for i in (period - 1)..n {
let val: f64 = (0..period)
.map(|j| prices[i - j] * (period - j) as f64)
.sum();
result[i] = val / weight_sum;
}
result
}
_ => {
if period == 0 {
return vec![f64::NAN; n];
}
let mut result = vec![f64::NAN; n];
for i in (period - 1)..n {
let sum: f64 = prices[(i + 1 - period)..=i].iter().sum();
result[i] = sum / period as f64;
}
result
}
}
}
/// MACD with configurable MA types for fast/slow/signal (matype 07). Returns (macd_line, signal_line, histogram).
#[pyfunction]
#[pyo3(signature = (close, fastperiod = 12, fastmatype = 1, slowperiod = 26, slowmatype = 1, signalperiod = 9, signalmatype = 1))]
#[allow(clippy::type_complexity, clippy::too_many_arguments)]
pub fn macdext<'py>(
py: Python<'py>,
close: PyReadonlyArray1<'py, f64>,
fastperiod: usize,
fastmatype: u8,
slowperiod: usize,
slowmatype: u8,
signalperiod: usize,
signalmatype: u8,
) -> PyResult<(
Bound<'py, PyArray1<f64>>,
Bound<'py, PyArray1<f64>>,
Bound<'py, PyArray1<f64>>,
)> {
validation::validate_timeperiod(fastperiod, "fastperiod", 1)?;
validation::validate_timeperiod(slowperiod, "slowperiod", 1)?;
validation::validate_timeperiod(signalperiod, "signalperiod", 1)?;
if fastperiod >= slowperiod {
return Err(PyValueError::new_err(
"fastperiod must be less than slowperiod",
));
}
let prices = close.as_slice()?;
let n = prices.len();
let fast_ma = compute_ma_slice(prices, fastperiod, fastmatype);
let slow_ma = compute_ma_slice(prices, slowperiod, slowmatype);
let mut macd_line = vec![f64::NAN; n];
let macd_start = slowperiod - 1;
for i in macd_start..n {
if !fast_ma[i].is_nan() && !slow_ma[i].is_nan() {
macd_line[i] = fast_ma[i] - slow_ma[i];
}
}
let macd_valid: Vec<f64> = macd_line[macd_start..].to_vec();
let signal_slice = compute_ma_slice(&macd_valid, signalperiod, signalmatype);
let mut signal_line = vec![f64::NAN; n];
let warmup = macd_start + signalperiod - 1;
#[allow(clippy::needless_range_loop)]
for i in warmup..n {
let j = i - macd_start;
if j < signal_slice.len() && !signal_slice[j].is_nan() {
signal_line[i] = signal_slice[j];
}
}
let mut histogram = vec![f64::NAN; n];
for i in 0..n {
if !macd_line[i].is_nan() && !signal_line[i].is_nan() {
histogram[i] = macd_line[i] - signal_line[i];
}
}
Ok((
macd_line.into_pyarray(py),
signal_line.into_pyarray(py),
histogram.into_pyarray(py),
))
}
+138
View File
@@ -0,0 +1,138 @@
use numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};
use pyo3::prelude::*;
/// MESA Adaptive Moving Average. Returns (mama, fama). Uses Hilbert Transformbased period.
#[pyfunction]
#[pyo3(signature = (close, fastlimit = 0.5, slowlimit = 0.05))]
#[allow(clippy::type_complexity)]
pub fn mama<'py>(
py: Python<'py>,
close: PyReadonlyArray1<'py, f64>,
fastlimit: f64,
slowlimit: f64,
) -> PyResult<(Bound<'py, PyArray1<f64>>, Bound<'py, PyArray1<f64>>)> {
let prices = close.as_slice()?;
let n = prices.len();
let lookback = 32;
let mut mama_arr = vec![f64::NAN; n];
let mut fama_arr = vec![f64::NAN; n];
if n <= lookback {
return Ok((mama_arr.into_pyarray(py), fama_arr.into_pyarray(py)));
}
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]
};
}
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 phase = vec![0.0f64; n];
let mut mama_val = prices[0];
let mut fama_val = prices[0];
for i in 6..n {
let prev_period = period[i - 1].max(1.0);
let alpha = 0.075 * prev_period + 0.54;
detrender[i] = (0.0962 * smooth[i] + 0.5769 * smooth[i - 2]
- 0.5769 * smooth[i - 4]
- 0.0962 * smooth[i - 6])
* alpha;
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;
}
if i >= 9 {
i1[i] = detrender[i - 3];
}
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;
}
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;
}
let i2_raw = i1[i] - jq[i];
let q2_raw = q1[i] + ji[i];
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;
let re_raw = i2[i] * i2_prev + q2[i] * q2_prev;
let im_raw = i2[i] * q2_prev - q2[i] * i2_prev;
re[i] = 0.2 * re_raw + 0.8 * re[i - 1];
im[i] = 0.2 * im_raw + 0.8 * im[i - 1];
let mut p = if re[i] != 0.0 && im[i] != 0.0 && re[i] > 0.0 {
std::f64::consts::PI * 2.0 / (im[i] / re[i]).atan()
} else {
prev_period
};
if p > 1.5 * prev_period {
p = 1.5 * prev_period;
}
if p < 0.67 * prev_period {
p = 0.67 * prev_period;
}
p = p.clamp(6.0, 50.0);
period[i] = 0.2 * p + 0.8 * prev_period;
let prev_phase = phase[i - 1];
phase[i] = if i1[i] != 0.0 {
q1[i].atan2(i1[i]) * 180.0 / std::f64::consts::PI
} else if q1[i] > 0.0 {
90.0
} else if q1[i] < 0.0 {
-90.0
} else {
0.0
};
let mut delta_phase = prev_phase - phase[i];
if delta_phase < 1.0 {
delta_phase = 1.0;
}
let adaptive_alpha = fastlimit / delta_phase;
let adaptive_alpha = adaptive_alpha.clamp(slowlimit, fastlimit);
if i >= lookback {
mama_val = adaptive_alpha * prices[i] + (1.0 - adaptive_alpha) * mama_val;
fama_val = 0.5 * adaptive_alpha * mama_val + (1.0 - 0.5 * adaptive_alpha) * fama_val;
mama_arr[i] = mama_val;
fama_arr[i] = fama_val;
} else {
mama_val = prices[i];
fama_val = prices[i];
}
}
Ok((mama_arr.into_pyarray(py), fama_arr.into_pyarray(py)))
}
+30
View File
@@ -0,0 +1,30 @@
use crate::validation;
use numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};
use pyo3::exceptions::PyValueError;
use pyo3::prelude::*;
use ta::indicators::{Maximum, Minimum};
use ta::Next;
/// Midpoint: (max(close) + min(close)) / 2 over the rolling window.
#[pyfunction]
#[pyo3(signature = (close, timeperiod = 14))]
pub fn midpoint<'py>(
py: Python<'py>,
close: PyReadonlyArray1<'py, f64>,
timeperiod: usize,
) -> PyResult<Bound<'py, PyArray1<f64>>> {
validation::validate_timeperiod(timeperiod, "timeperiod", 1)?;
let prices = close.as_slice()?;
let n = prices.len();
let mut max_ind = Maximum::new(timeperiod).map_err(|e| PyValueError::new_err(e.to_string()))?;
let mut min_ind = Minimum::new(timeperiod).map_err(|e| PyValueError::new_err(e.to_string()))?;
let mut result = vec![f64::NAN; n];
for (i, &price) in prices.iter().enumerate() {
let mx = max_ind.next(price);
let mn = min_ind.next(price);
if i + 1 >= timeperiod {
result[i] = (mx + mn) / 2.0;
}
}
Ok(result.into_pyarray(py))
}
+34
View File
@@ -0,0 +1,34 @@
use crate::validation;
use numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};
use pyo3::prelude::*;
use ta::indicators::{Maximum, Minimum};
use ta::Next;
/// MidPrice: (highest high + lowest low) / 2 over the rolling window.
#[pyfunction]
#[pyo3(signature = (high, low, timeperiod = 14))]
pub fn midprice<'py>(
py: Python<'py>,
high: PyReadonlyArray1<'py, f64>,
low: PyReadonlyArray1<'py, f64>,
timeperiod: usize,
) -> PyResult<Bound<'py, PyArray1<f64>>> {
validation::validate_timeperiod(timeperiod, "timeperiod", 1)?;
let highs = high.as_slice()?;
let lows = low.as_slice()?;
let n = highs.len();
validation::validate_equal_length(&[(n, "high"), (lows.len(), "low")])?;
let mut max_ind = Maximum::new(timeperiod)
.map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?;
let mut min_ind = Minimum::new(timeperiod)
.map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?;
let mut result = vec![f64::NAN; n];
for (i, (&h, &l)) in highs.iter().zip(lows.iter()).enumerate() {
let mx = max_ind.next(h);
let mn = min_ind.next(l);
if i + 1 >= timeperiod {
result[i] = (mx + mn) / 2.0;
}
}
Ok(result.into_pyarray(py))
}
+47
View File
@@ -0,0 +1,47 @@
//! Overlap studies — moving averages and trend indicators.
//! Each indicator lives in its own file for maintainability.
mod bbands;
mod dema;
mod ema;
mod kama;
mod ma_mavp;
mod macd;
mod macdext;
mod mama;
mod midpoint;
mod midprice;
mod sar;
mod sarext;
mod sma;
mod t3;
mod tema;
mod trima;
mod wma;
pub use ma_mavp::{ma, mavp};
use pyo3::prelude::*;
pub fn register(m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_function(pyo3::wrap_pyfunction!(self::sma::sma, m)?)?;
m.add_function(pyo3::wrap_pyfunction!(self::ema::ema, m)?)?;
m.add_function(pyo3::wrap_pyfunction!(self::wma::wma, m)?)?;
m.add_function(pyo3::wrap_pyfunction!(self::dema::dema, m)?)?;
m.add_function(pyo3::wrap_pyfunction!(self::tema::tema, m)?)?;
m.add_function(pyo3::wrap_pyfunction!(self::trima::trima, m)?)?;
m.add_function(pyo3::wrap_pyfunction!(self::kama::kama, m)?)?;
m.add_function(pyo3::wrap_pyfunction!(self::t3::t3, m)?)?;
m.add_function(pyo3::wrap_pyfunction!(self::bbands::bbands, m)?)?;
m.add_function(pyo3::wrap_pyfunction!(self::macd::macd, m)?)?;
m.add_function(pyo3::wrap_pyfunction!(self::macd::macdfix, m)?)?;
m.add_function(pyo3::wrap_pyfunction!(self::sar::sar, m)?)?;
m.add_function(pyo3::wrap_pyfunction!(self::midpoint::midpoint, m)?)?;
m.add_function(pyo3::wrap_pyfunction!(self::midprice::midprice, m)?)?;
m.add_function(pyo3::wrap_pyfunction!(ma, m)?)?;
m.add_function(pyo3::wrap_pyfunction!(mavp, m)?)?;
m.add_function(pyo3::wrap_pyfunction!(self::mama::mama, m)?)?;
m.add_function(pyo3::wrap_pyfunction!(self::sarext::sarext, m)?)?;
m.add_function(pyo3::wrap_pyfunction!(self::macdext::macdext, m)?)?;
Ok(())
}
+70
View File
@@ -0,0 +1,70 @@
use crate::validation;
use numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};
use pyo3::prelude::*;
/// Parabolic SAR. Same shape as TA-Lib; reversal history may differ slightly.
#[pyfunction]
#[pyo3(signature = (high, low, acceleration = 0.02, maximum = 0.2))]
pub fn sar<'py>(
py: Python<'py>,
high: PyReadonlyArray1<'py, f64>,
low: PyReadonlyArray1<'py, f64>,
acceleration: f64,
maximum: f64,
) -> PyResult<Bound<'py, PyArray1<f64>>> {
let highs = high.as_slice()?;
let lows = low.as_slice()?;
let n = highs.len();
validation::validate_equal_length(&[(n, "high"), (lows.len(), "low")])?;
if n < 2 {
return Ok(vec![f64::NAN; n].into_pyarray(py));
}
let mut result = vec![f64::NAN; n];
let mut is_rising = highs[1] >= highs[0];
let mut af = acceleration;
let mut ep: f64;
let mut sar_val: f64;
if is_rising {
sar_val = lows[0];
ep = highs[1];
} else {
sar_val = highs[0];
ep = lows[1];
}
result[1] = sar_val;
for i in 2..n {
let prev_sar = sar_val;
sar_val = prev_sar + af * (ep - prev_sar);
if is_rising {
sar_val = sar_val.min(lows[i - 1]).min(lows[i - 2]);
if lows[i] < sar_val {
is_rising = false;
sar_val = ep;
ep = lows[i];
af = acceleration;
} else if highs[i] > ep {
ep = highs[i];
af = (af + acceleration).min(maximum);
}
} else {
sar_val = sar_val.max(highs[i - 1]).max(highs[i - 2]);
if highs[i] > sar_val {
is_rising = true;
sar_val = ep;
ep = highs[i];
af = acceleration;
} else if lows[i] < ep {
ep = lows[i];
af = (af + acceleration).min(maximum);
}
}
result[i] = sar_val;
}
Ok(result.into_pyarray(py))
}
+103
View File
@@ -0,0 +1,103 @@
use crate::validation;
use numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};
use pyo3::prelude::*;
/// Parabolic SAR Extended: SAR with configurable start value and long/short acceleration.
#[pyfunction]
#[pyo3(signature = (high, low, startvalue = 0.0, offsetonreverse = 0.0, accelerationinitlong = 0.02, accelerationlong = 0.02, accelerationmaxlong = 0.2, accelerationinitshort = 0.02, accelerationshort = 0.02, accelerationmaxshort = 0.2))]
#[allow(clippy::too_many_arguments)]
pub fn sarext<'py>(
py: Python<'py>,
high: PyReadonlyArray1<'py, f64>,
low: PyReadonlyArray1<'py, f64>,
startvalue: f64,
offsetonreverse: f64,
accelerationinitlong: f64,
accelerationlong: f64,
accelerationmaxlong: f64,
accelerationinitshort: f64,
accelerationshort: f64,
accelerationmaxshort: f64,
) -> PyResult<Bound<'py, PyArray1<f64>>> {
let highs = high.as_slice()?;
let lows = low.as_slice()?;
let n = highs.len();
validation::validate_equal_length(&[(n, "high"), (lows.len(), "low")])?;
if n < 2 {
return Ok(vec![f64::NAN; n].into_pyarray(py));
}
let mut result = vec![f64::NAN; n];
let mut is_rising = highs[1] >= highs[0];
let (mut af, af_step, af_max) = if is_rising {
(accelerationinitlong, accelerationlong, accelerationmaxlong)
} else {
(
accelerationinitshort,
accelerationshort,
accelerationmaxshort,
)
};
let mut ep: f64;
let mut sar_val: f64;
if is_rising {
sar_val = if startvalue != 0.0 {
startvalue
} else {
lows[0]
};
ep = highs[1];
} else {
sar_val = if startvalue != 0.0 {
-startvalue
} else {
highs[0]
};
ep = lows[1];
}
result[1] = sar_val;
let mut af_step_cur = af_step;
let mut af_max_cur = af_max;
for i in 2..n {
let prev_sar = sar_val;
sar_val = prev_sar + af * (ep - prev_sar);
if is_rising {
sar_val = sar_val.min(lows[i - 1]).min(lows[i - 2]);
if lows[i] < sar_val {
is_rising = false;
sar_val = ep + sar_val.abs() * offsetonreverse;
ep = lows[i];
af = accelerationinitshort;
af_step_cur = accelerationshort;
af_max_cur = accelerationmaxshort;
} else if highs[i] > ep {
ep = highs[i];
af = (af + af_step_cur).min(af_max_cur);
}
} else {
sar_val = sar_val.max(highs[i - 1]).max(highs[i - 2]);
if highs[i] > sar_val {
is_rising = true;
sar_val = ep - sar_val.abs() * offsetonreverse;
ep = highs[i];
af = accelerationinitlong;
af_step_cur = accelerationlong;
af_max_cur = accelerationmaxlong;
} else if lows[i] < ep {
ep = lows[i];
af = (af + af_step_cur).min(af_max_cur);
}
}
result[i] = sar_val;
}
Ok(result.into_pyarray(py))
}
+29
View File
@@ -0,0 +1,29 @@
use crate::validation;
use numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};
use pyo3::prelude::*;
/// Inner SMA implementation (timeperiod already validated as usize).
/// Used by the PyO3 sma() and by ma() when matype=0.
pub fn sma_inner<'py>(
py: Python<'py>,
close: PyReadonlyArray1<'py, f64>,
timeperiod: usize,
) -> PyResult<Bound<'py, PyArray1<f64>>> {
let prices = close.as_slice()?;
let n = prices.len();
log::debug!("SMA: timeperiod={timeperiod}, n={n}");
let result = ferro_ta_core::overlap::sma(prices, timeperiod);
Ok(result.into_pyarray(py))
}
/// Simple Moving Average. Leading timeperiod-1 values are NaN.
#[pyfunction]
#[pyo3(signature = (close, timeperiod = 30))]
pub fn sma<'py>(
py: Python<'py>,
close: PyReadonlyArray1<'py, f64>,
timeperiod: i64,
) -> PyResult<Bound<'py, PyArray1<f64>>> {
let timeperiod = validation::parse_timeperiod(timeperiod, "timeperiod", 1)?;
sma_inner(py, close, timeperiod)
}
+46
View File
@@ -0,0 +1,46 @@
use crate::validation;
use numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};
use pyo3::prelude::*;
/// Tillson T3 (triple smoothed EMA). Converges after ~6*(timeperiod-1) bars.
#[pyfunction]
#[pyo3(signature = (close, timeperiod = 5, vfactor = 0.7))]
pub fn t3<'py>(
py: Python<'py>,
close: PyReadonlyArray1<'py, f64>,
timeperiod: usize,
vfactor: f64,
) -> PyResult<Bound<'py, PyArray1<f64>>> {
validation::validate_timeperiod(timeperiod, "timeperiod", 1)?;
let prices = close.as_slice()?;
let n = prices.len();
let mut e = [0.0_f64; 6];
let k = 2.0 / (timeperiod as f64 + 1.0);
let v = vfactor;
let c1 = -(v * v * v);
let c2 = 3.0 * v * v + 3.0 * v * v * v;
let c3 = -6.0 * v * v - 3.0 * v - 3.0 * v * v * v;
let c4 = 1.0 + 3.0 * v + v * v * v + 3.0 * v * v;
let warmup = 6 * (timeperiod - 1);
let mut result = vec![f64::NAN; n];
for (i, &price) in prices.iter().enumerate() {
if i == 0 {
for ej in e.iter_mut() {
*ej = price;
}
} else {
e[0] += k * (price - e[0]);
for j in 1..6 {
e[j] += k * (e[j - 1] - e[j]);
}
}
if i >= warmup {
result[i] = c1 * e[5] + c2 * e[4] + c3 * e[3] + c4 * e[2];
}
}
Ok(result.into_pyarray(py))
}
+45
View File
@@ -0,0 +1,45 @@
use crate::validation;
use numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};
use pyo3::exceptions::PyValueError;
use pyo3::prelude::*;
use ta::indicators::ExponentialMovingAverage;
use ta::Next;
/// Triple Exponential Moving Average. Converges after ~3*(timeperiod-1) bars.
#[pyfunction]
#[pyo3(signature = (close, timeperiod = 30))]
pub fn tema<'py>(
py: Python<'py>,
close: PyReadonlyArray1<'py, f64>,
timeperiod: usize,
) -> PyResult<Bound<'py, PyArray1<f64>>> {
validation::validate_timeperiod(timeperiod, "timeperiod", 1)?;
let prices = close.as_slice()?;
let n = prices.len();
let mut ema1 = ExponentialMovingAverage::new(timeperiod)
.map_err(|e| PyValueError::new_err(e.to_string()))?;
let mut ema2 = ExponentialMovingAverage::new(timeperiod)
.map_err(|e| PyValueError::new_err(e.to_string()))?;
let mut ema3 = ExponentialMovingAverage::new(timeperiod)
.map_err(|e| PyValueError::new_err(e.to_string()))?;
let warmup1 = timeperiod - 1;
let warmup2 = 2 * (timeperiod - 1);
let warmup3 = 3 * (timeperiod - 1);
let mut result = vec![f64::NAN; n];
for (i, &price) in prices.iter().enumerate() {
let v1 = ema1.next(price);
if i >= warmup1 {
let v2 = ema2.next(v1);
if i >= warmup2 {
let v3 = ema3.next(v2);
if i >= warmup3 {
result[i] = 3.0 * v1 - 3.0 * v2 + v3;
}
}
}
}
Ok(result.into_pyarray(py))
}
+34
View File
@@ -0,0 +1,34 @@
use crate::validation;
use numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};
use pyo3::prelude::*;
/// Triangular Moving Average (triangle-weighted). Leading timeperiod-1 values are NaN.
#[pyfunction]
#[pyo3(signature = (close, timeperiod = 30))]
pub fn trima<'py>(
py: Python<'py>,
close: PyReadonlyArray1<'py, f64>,
timeperiod: usize,
) -> PyResult<Bound<'py, PyArray1<f64>>> {
validation::validate_timeperiod(timeperiod, "timeperiod", 1)?;
let prices = close.as_slice()?;
let n = prices.len();
let mut weights = Vec::with_capacity(timeperiod);
let half = timeperiod.div_ceil(2);
for i in 1..=timeperiod {
let w = if i <= half { i } else { timeperiod + 1 - i };
weights.push(w as f64);
}
let weight_sum: f64 = weights.iter().sum();
let mut result = vec![f64::NAN; n];
for i in (timeperiod - 1)..n {
let mut val = 0.0_f64;
for (j, &w) in weights.iter().enumerate() {
val += prices[i - (timeperiod - 1 - j)] * w;
}
result[i] = val / weight_sum;
}
Ok(result.into_pyarray(py))
}
+19
View File
@@ -0,0 +1,19 @@
use crate::validation;
use numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};
use pyo3::prelude::*;
/// Weighted Moving Average (linear weights). Leading timeperiod-1 values are NaN.
#[pyfunction]
#[pyo3(signature = (close, timeperiod = 30))]
pub fn wma<'py>(
py: Python<'py>,
close: PyReadonlyArray1<'py, f64>,
timeperiod: usize,
) -> PyResult<Bound<'py, PyArray1<f64>>> {
validation::validate_timeperiod(timeperiod, "timeperiod", 1)?;
let prices = close.as_slice()?;
let n = prices.len();
log::debug!("WMA: timeperiod={timeperiod}, n={n}");
let result = ferro_ta_core::overlap::wma(prices, timeperiod);
Ok(result.into_pyarray(py))
}