feat: init the repo
This commit is contained in:
@@ -0,0 +1,290 @@
|
||||
//! Tick / Trade Aggregation Pipeline — Rust implementations.
|
||||
//!
|
||||
//! Aggregates raw tick/trade data into OHLCV bars:
|
||||
//! - **time bars** — fixed duration buckets (label-based via Python timestamps)
|
||||
//! - **volume bars** — fixed volume threshold per bar
|
||||
//! - **tick bars** — fixed number of ticks per bar
|
||||
//!
|
||||
//! The Python layer (ferro_ta.aggregation) provides the timestamp bucketing
|
||||
//! for time bars; this module handles the compute-intensive OHLCV accumulation.
|
||||
|
||||
use numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};
|
||||
use pyo3::exceptions::PyValueError;
|
||||
use pyo3::prelude::*;
|
||||
|
||||
/// Return type for functions that return five OHLCV 1-D arrays.
|
||||
type Ohlcv5<'py> = (
|
||||
Bound<'py, PyArray1<f64>>,
|
||||
Bound<'py, PyArray1<f64>>,
|
||||
Bound<'py, PyArray1<f64>>,
|
||||
Bound<'py, PyArray1<f64>>,
|
||||
Bound<'py, PyArray1<f64>>,
|
||||
);
|
||||
|
||||
/// Return type for time bars: five OHLCV arrays plus labels.
|
||||
type Ohlcv5AndLabels<'py> = (
|
||||
Bound<'py, PyArray1<f64>>,
|
||||
Bound<'py, PyArray1<f64>>,
|
||||
Bound<'py, PyArray1<f64>>,
|
||||
Bound<'py, PyArray1<f64>>,
|
||||
Bound<'py, PyArray1<f64>>,
|
||||
Bound<'py, PyArray1<i64>>,
|
||||
);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// aggregate_tick_bars
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Aggregate tick/trade data into tick bars (every N ticks become one bar).
|
||||
///
|
||||
/// Parameters
|
||||
/// ----------
|
||||
/// price, size : 1-D float64 arrays (equal length, one entry per trade/tick)
|
||||
/// ticks_per_bar : int — number of ticks per bar (must be >= 1)
|
||||
///
|
||||
/// Returns
|
||||
/// -------
|
||||
/// Tuple of five 1-D arrays: (open, high, low, close, volume)
|
||||
/// where volume = sum of sizes in each bar.
|
||||
#[pyfunction]
|
||||
#[pyo3(signature = (price, size, ticks_per_bar))]
|
||||
pub fn aggregate_tick_bars<'py>(
|
||||
py: Python<'py>,
|
||||
price: PyReadonlyArray1<'py, f64>,
|
||||
size: PyReadonlyArray1<'py, f64>,
|
||||
ticks_per_bar: usize,
|
||||
) -> PyResult<Ohlcv5<'py>> {
|
||||
if ticks_per_bar == 0 {
|
||||
return Err(PyValueError::new_err("ticks_per_bar must be >= 1"));
|
||||
}
|
||||
let p = price.as_slice()?;
|
||||
let s = size.as_slice()?;
|
||||
let n = p.len();
|
||||
if n == 0 || s.len() != n {
|
||||
return Err(PyValueError::new_err(
|
||||
"price and size must be non-empty and equal length",
|
||||
));
|
||||
}
|
||||
|
||||
let n_bars = n.div_ceil(ticks_per_bar);
|
||||
let mut out_open = Vec::with_capacity(n_bars);
|
||||
let mut out_high = Vec::with_capacity(n_bars);
|
||||
let mut out_low = Vec::with_capacity(n_bars);
|
||||
let mut out_close = Vec::with_capacity(n_bars);
|
||||
let mut out_vol = Vec::with_capacity(n_bars);
|
||||
|
||||
let mut i = 0;
|
||||
while i < n {
|
||||
let end = (i + ticks_per_bar).min(n);
|
||||
let bar_p = &p[i..end];
|
||||
let bar_s = &s[i..end];
|
||||
let bar_open = bar_p[0];
|
||||
let bar_high = bar_p.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
|
||||
let bar_low = bar_p.iter().cloned().fold(f64::INFINITY, f64::min);
|
||||
let bar_close = *bar_p.last().unwrap();
|
||||
let bar_vol: f64 = bar_s.iter().sum();
|
||||
out_open.push(bar_open);
|
||||
out_high.push(bar_high);
|
||||
out_low.push(bar_low);
|
||||
out_close.push(bar_close);
|
||||
out_vol.push(bar_vol);
|
||||
i = end;
|
||||
}
|
||||
|
||||
Ok((
|
||||
out_open.into_pyarray(py),
|
||||
out_high.into_pyarray(py),
|
||||
out_low.into_pyarray(py),
|
||||
out_close.into_pyarray(py),
|
||||
out_vol.into_pyarray(py),
|
||||
))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// aggregate_volume_bars_ticks
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Aggregate tick data into volume bars (fixed volume threshold).
|
||||
///
|
||||
/// Accumulates ticks until cumulative size >= `volume_threshold`, then emits
|
||||
/// a bar.
|
||||
///
|
||||
/// Parameters
|
||||
/// ----------
|
||||
/// price, size : 1-D float64 arrays (equal length)
|
||||
/// volume_threshold : float — cumulative size threshold per bar (must be > 0)
|
||||
///
|
||||
/// Returns
|
||||
/// -------
|
||||
/// Tuple of five 1-D arrays: (open, high, low, close, volume)
|
||||
#[pyfunction]
|
||||
#[pyo3(signature = (price, size, volume_threshold))]
|
||||
pub fn aggregate_volume_bars_ticks<'py>(
|
||||
py: Python<'py>,
|
||||
price: PyReadonlyArray1<'py, f64>,
|
||||
size: PyReadonlyArray1<'py, f64>,
|
||||
volume_threshold: f64,
|
||||
) -> PyResult<Ohlcv5<'py>> {
|
||||
if volume_threshold <= 0.0 {
|
||||
return Err(PyValueError::new_err("volume_threshold must be > 0"));
|
||||
}
|
||||
let p = price.as_slice()?;
|
||||
let s = size.as_slice()?;
|
||||
let n = p.len();
|
||||
if n == 0 || s.len() != n {
|
||||
return Err(PyValueError::new_err(
|
||||
"price and size must be non-empty and equal length",
|
||||
));
|
||||
}
|
||||
|
||||
let mut out_open: Vec<f64> = Vec::new();
|
||||
let mut out_high: Vec<f64> = Vec::new();
|
||||
let mut out_low: Vec<f64> = Vec::new();
|
||||
let mut out_close: Vec<f64> = Vec::new();
|
||||
let mut out_vol: Vec<f64> = Vec::new();
|
||||
|
||||
let mut bar_open = p[0];
|
||||
let mut bar_high = p[0];
|
||||
let mut bar_low = p[0];
|
||||
let mut bar_close = p[0];
|
||||
let mut bar_vol = s[0];
|
||||
|
||||
for i in 1..n {
|
||||
bar_high = bar_high.max(p[i]);
|
||||
bar_low = bar_low.min(p[i]);
|
||||
bar_close = p[i];
|
||||
bar_vol += s[i];
|
||||
|
||||
if bar_vol >= volume_threshold {
|
||||
out_open.push(bar_open);
|
||||
out_high.push(bar_high);
|
||||
out_low.push(bar_low);
|
||||
out_close.push(bar_close);
|
||||
out_vol.push(bar_vol);
|
||||
if i + 1 < n {
|
||||
bar_open = p[i + 1];
|
||||
bar_high = p[i + 1];
|
||||
bar_low = p[i + 1];
|
||||
bar_close = p[i + 1];
|
||||
bar_vol = s[i + 1];
|
||||
} else {
|
||||
bar_vol = 0.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Push remaining partial bar
|
||||
if bar_vol > 0.0 {
|
||||
out_open.push(bar_open);
|
||||
out_high.push(bar_high);
|
||||
out_low.push(bar_low);
|
||||
out_close.push(bar_close);
|
||||
out_vol.push(bar_vol);
|
||||
}
|
||||
|
||||
Ok((
|
||||
out_open.into_pyarray(py),
|
||||
out_high.into_pyarray(py),
|
||||
out_low.into_pyarray(py),
|
||||
out_close.into_pyarray(py),
|
||||
out_vol.into_pyarray(py),
|
||||
))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// aggregate_time_bars
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Aggregate tick data into time bars using pre-computed integer bucket labels.
|
||||
///
|
||||
/// Each tick is assigned a `label` (e.g. unix_ts // period_secs). Ticks with
|
||||
/// the same label are accumulated into one bar. Labels must be non-decreasing.
|
||||
///
|
||||
/// Parameters
|
||||
/// ----------
|
||||
/// price, size : 1-D float64 arrays
|
||||
/// labels : 1-D int64 array — bucket label per tick (non-decreasing)
|
||||
///
|
||||
/// Returns
|
||||
/// -------
|
||||
/// Tuple of five 1-D arrays: (open, high, low, close, volume)
|
||||
/// and a 1-D int64 array of unique labels (one per bar).
|
||||
#[pyfunction]
|
||||
#[pyo3(signature = (price, size, labels))]
|
||||
pub fn aggregate_time_bars<'py>(
|
||||
py: Python<'py>,
|
||||
price: PyReadonlyArray1<'py, f64>,
|
||||
size: PyReadonlyArray1<'py, f64>,
|
||||
labels: PyReadonlyArray1<'py, i64>,
|
||||
) -> PyResult<Ohlcv5AndLabels<'py>> {
|
||||
let p = price.as_slice()?;
|
||||
let s = size.as_slice()?;
|
||||
let lbl = labels.as_slice()?;
|
||||
let n = p.len();
|
||||
if n == 0 || s.len() != n || lbl.len() != n {
|
||||
return Err(PyValueError::new_err(
|
||||
"price, size, and labels must be non-empty and equal length",
|
||||
));
|
||||
}
|
||||
|
||||
let mut out_open: Vec<f64> = Vec::new();
|
||||
let mut out_high: Vec<f64> = Vec::new();
|
||||
let mut out_low: Vec<f64> = Vec::new();
|
||||
let mut out_close: Vec<f64> = Vec::new();
|
||||
let mut out_vol: Vec<f64> = Vec::new();
|
||||
let mut out_labels: Vec<i64> = Vec::new();
|
||||
|
||||
let mut cur_label = lbl[0];
|
||||
let mut bar_open = p[0];
|
||||
let mut bar_high = p[0];
|
||||
let mut bar_low = p[0];
|
||||
let mut bar_close = p[0];
|
||||
let mut bar_vol = s[0];
|
||||
|
||||
for i in 1..n {
|
||||
if lbl[i] != cur_label {
|
||||
out_open.push(bar_open);
|
||||
out_high.push(bar_high);
|
||||
out_low.push(bar_low);
|
||||
out_close.push(bar_close);
|
||||
out_vol.push(bar_vol);
|
||||
out_labels.push(cur_label);
|
||||
cur_label = lbl[i];
|
||||
bar_open = p[i];
|
||||
bar_high = p[i];
|
||||
bar_low = p[i];
|
||||
bar_close = p[i];
|
||||
bar_vol = s[i];
|
||||
} else {
|
||||
bar_high = bar_high.max(p[i]);
|
||||
bar_low = bar_low.min(p[i]);
|
||||
bar_close = p[i];
|
||||
bar_vol += s[i];
|
||||
}
|
||||
}
|
||||
out_open.push(bar_open);
|
||||
out_high.push(bar_high);
|
||||
out_low.push(bar_low);
|
||||
out_close.push(bar_close);
|
||||
out_vol.push(bar_vol);
|
||||
out_labels.push(cur_label);
|
||||
|
||||
Ok((
|
||||
out_open.into_pyarray(py),
|
||||
out_high.into_pyarray(py),
|
||||
out_low.into_pyarray(py),
|
||||
out_close.into_pyarray(py),
|
||||
out_vol.into_pyarray(py),
|
||||
out_labels.into_pyarray(py),
|
||||
))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Register
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
pub fn register(m: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||
m.add_function(wrap_pyfunction!(aggregate_tick_bars, m)?)?;
|
||||
m.add_function(wrap_pyfunction!(aggregate_volume_bars_ticks, m)?)?;
|
||||
m.add_function(wrap_pyfunction!(aggregate_time_bars, m)?)?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
//! Alerts — condition evaluation helpers.
|
||||
//!
|
||||
//! These Rust functions evaluate conditions over price/indicator series and
|
||||
//! return boolean or integer arrays indicating where conditions fire. They
|
||||
//! are designed to be called once per batch (backtest) or per bar (live) and
|
||||
//! return the full history of firings.
|
||||
//!
|
||||
//! Functions
|
||||
//! ---------
|
||||
//! - `check_threshold` — fires when a series crosses above/below a level
|
||||
//! - `check_cross` — fires when *fast* crosses above or below *slow*
|
||||
//! - `collect_alert_bars` — returns indices of bars where a bool mask is True
|
||||
|
||||
use numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};
|
||||
use pyo3::exceptions::PyValueError;
|
||||
use pyo3::prelude::*;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// check_threshold
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Fire an alert when *series* crosses a threshold level.
|
||||
///
|
||||
/// Parameters
|
||||
/// ----------
|
||||
/// series : 1-D float64 array — indicator values (e.g. RSI)
|
||||
/// level : float — threshold value
|
||||
/// direction : int
|
||||
/// ``1`` → fire when series crosses **above** *level* (value goes from
|
||||
/// ≤ level to > level).
|
||||
/// ``-1`` → fire when series crosses **below** *level* (value goes from
|
||||
/// ≥ level to < level).
|
||||
///
|
||||
/// Returns
|
||||
/// -------
|
||||
/// 1-D int8 array — 1 at the bar where the crossing occurs, 0 elsewhere.
|
||||
/// Element 0 is always 0 (no crossing possible without a prior bar).
|
||||
#[pyfunction]
|
||||
pub fn check_threshold<'py>(
|
||||
py: Python<'py>,
|
||||
series: PyReadonlyArray1<'py, f64>,
|
||||
level: f64,
|
||||
direction: i32,
|
||||
) -> PyResult<Bound<'py, PyArray1<i8>>> {
|
||||
if direction != 1 && direction != -1 {
|
||||
return Err(PyValueError::new_err(
|
||||
"direction must be 1 (cross above) or -1 (cross below)",
|
||||
));
|
||||
}
|
||||
let s = series.as_slice()?;
|
||||
let n = s.len();
|
||||
let mut out = vec![0i8; n];
|
||||
if n < 2 {
|
||||
return Ok(out.into_pyarray(py));
|
||||
}
|
||||
for i in 1..n {
|
||||
let prev = s[i - 1];
|
||||
let curr = s[i];
|
||||
if prev.is_nan() || curr.is_nan() {
|
||||
continue;
|
||||
}
|
||||
if direction == 1 {
|
||||
// cross above: was at or below level, now above
|
||||
if prev <= level && curr > level {
|
||||
out[i] = 1;
|
||||
}
|
||||
} else {
|
||||
// cross below: was at or above level, now below
|
||||
if prev >= level && curr < level {
|
||||
out[i] = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(out.into_pyarray(py))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// check_cross
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Detect cross-over / cross-under events between two series.
|
||||
///
|
||||
/// Parameters
|
||||
/// ----------
|
||||
/// fast : 1-D float64 array — the "fast" series (e.g. short SMA)
|
||||
/// slow : 1-D float64 array — the "slow" series (e.g. long SMA)
|
||||
///
|
||||
/// Returns
|
||||
/// -------
|
||||
/// 1-D int8 array:
|
||||
/// ``1`` at bars where *fast* crosses **above** *slow* (bullish cross)
|
||||
/// ``-1`` at bars where *fast* crosses **below** *slow* (bearish cross)
|
||||
/// ``0`` elsewhere
|
||||
/// Element 0 is always 0.
|
||||
#[pyfunction]
|
||||
pub fn check_cross<'py>(
|
||||
py: Python<'py>,
|
||||
fast: PyReadonlyArray1<'py, f64>,
|
||||
slow: PyReadonlyArray1<'py, f64>,
|
||||
) -> PyResult<Bound<'py, PyArray1<i8>>> {
|
||||
let f = fast.as_slice()?;
|
||||
let s = slow.as_slice()?;
|
||||
let n = f.len();
|
||||
if n != s.len() {
|
||||
return Err(PyValueError::new_err(
|
||||
"fast and slow must have the same length",
|
||||
));
|
||||
}
|
||||
let mut out = vec![0i8; n];
|
||||
if n < 2 {
|
||||
return Ok(out.into_pyarray(py));
|
||||
}
|
||||
for i in 1..n {
|
||||
let fp = f[i - 1];
|
||||
let fc = f[i];
|
||||
let sp = s[i - 1];
|
||||
let sc = s[i];
|
||||
if fp.is_nan() || fc.is_nan() || sp.is_nan() || sc.is_nan() {
|
||||
continue;
|
||||
}
|
||||
// Bullish: fast was below slow, now above
|
||||
if fp <= sp && fc > sc {
|
||||
out[i] = 1;
|
||||
}
|
||||
// Bearish: fast was above slow, now below
|
||||
else if fp >= sp && fc < sc {
|
||||
out[i] = -1;
|
||||
}
|
||||
}
|
||||
Ok(out.into_pyarray(py))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// collect_alert_bars
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Collect bar indices where *mask* is non-zero (i.e. condition fired).
|
||||
///
|
||||
/// Parameters
|
||||
/// ----------
|
||||
/// mask : 1-D int8 array (output of ``check_threshold`` or ``check_cross``)
|
||||
///
|
||||
/// Returns
|
||||
/// -------
|
||||
/// 1-D int64 array — indices of fired bars (ascending order)
|
||||
#[pyfunction]
|
||||
pub fn collect_alert_bars<'py>(
|
||||
py: Python<'py>,
|
||||
mask: PyReadonlyArray1<'py, i8>,
|
||||
) -> PyResult<Bound<'py, PyArray1<i64>>> {
|
||||
let m = mask.as_slice()?;
|
||||
let indices: Vec<i64> = m
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, &v)| v != 0)
|
||||
.map(|(i, _)| i as i64)
|
||||
.collect();
|
||||
Ok(indices.into_pyarray(py))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Register
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
pub fn register(m: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||
m.add_function(wrap_pyfunction!(check_threshold, m)?)?;
|
||||
m.add_function(wrap_pyfunction!(check_cross, m)?)?;
|
||||
m.add_function(wrap_pyfunction!(collect_alert_bars, m)?)?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
//! Performance attribution and trade analysis.
|
||||
//!
|
||||
//! Functions
|
||||
//! ---------
|
||||
//! - `trade_stats` — compute win rate, avg win/loss, hold time,
|
||||
//! profit factor from a list of trade PnLs and hold durations.
|
||||
//! - `monthly_contribution` — group bar returns by month index and sum, for
|
||||
//! time-based performance attribution.
|
||||
//! - `signal_attribution` — given signal labels per bar and bar returns,
|
||||
//! compute the PnL contribution of each signal.
|
||||
|
||||
use numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};
|
||||
use pyo3::exceptions::PyValueError;
|
||||
use pyo3::prelude::*;
|
||||
use std::collections::HashMap;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// trade_stats
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Compute trade-level statistics from trade PnL and hold durations.
|
||||
///
|
||||
/// Parameters
|
||||
/// ----------
|
||||
/// pnl : 1-D float64 array — per-trade profit/loss (positive = win)
|
||||
/// hold_bars : 1-D float64 array — hold duration in bars for each trade
|
||||
/// (same length as *pnl*)
|
||||
///
|
||||
/// Returns
|
||||
/// -------
|
||||
/// tuple of 5 floats:
|
||||
/// ``(win_rate, avg_win, avg_loss, profit_factor, avg_hold_bars)``
|
||||
///
|
||||
/// - **win_rate** : fraction of trades with PnL > 0
|
||||
/// - **avg_win** : mean PnL of winning trades (or 0 if none)
|
||||
/// - **avg_loss** : mean PnL of losing trades (negative; or 0 if none)
|
||||
/// - **profit_factor** : gross profit / |gross loss| (inf if no losses)
|
||||
/// - **avg_hold_bars** : mean hold duration across all trades
|
||||
#[pyfunction]
|
||||
pub fn trade_stats(
|
||||
pnl: PyReadonlyArray1<'_, f64>,
|
||||
hold_bars: PyReadonlyArray1<'_, f64>,
|
||||
) -> PyResult<(f64, f64, f64, f64, f64)> {
|
||||
let p = pnl.as_slice()?;
|
||||
let h = hold_bars.as_slice()?;
|
||||
let n = p.len();
|
||||
if n == 0 {
|
||||
return Err(PyValueError::new_err("pnl must be non-empty"));
|
||||
}
|
||||
if n != h.len() {
|
||||
return Err(PyValueError::new_err(
|
||||
"pnl and hold_bars must have the same length",
|
||||
));
|
||||
}
|
||||
|
||||
let mut wins: Vec<f64> = Vec::new();
|
||||
let mut losses: Vec<f64> = Vec::new();
|
||||
for &v in p.iter() {
|
||||
if v > 0.0 {
|
||||
wins.push(v);
|
||||
} else if v < 0.0 {
|
||||
losses.push(v);
|
||||
}
|
||||
}
|
||||
|
||||
let win_rate = wins.len() as f64 / n as f64;
|
||||
let avg_win = if wins.is_empty() {
|
||||
0.0
|
||||
} else {
|
||||
wins.iter().sum::<f64>() / wins.len() as f64
|
||||
};
|
||||
let avg_loss = if losses.is_empty() {
|
||||
0.0
|
||||
} else {
|
||||
losses.iter().sum::<f64>() / losses.len() as f64
|
||||
};
|
||||
|
||||
let gross_profit: f64 = wins.iter().sum();
|
||||
let gross_loss: f64 = losses.iter().map(|v| v.abs()).sum();
|
||||
let profit_factor = if gross_loss == 0.0 {
|
||||
f64::INFINITY
|
||||
} else {
|
||||
gross_profit / gross_loss
|
||||
};
|
||||
|
||||
let avg_hold = h.iter().sum::<f64>() / n as f64;
|
||||
|
||||
Ok((win_rate, avg_win, avg_loss, profit_factor, avg_hold))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// monthly_contribution
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Group per-bar returns by month index and sum each month's contribution.
|
||||
///
|
||||
/// The ``month_index`` array assigns each bar to a month bucket (0-based
|
||||
/// integer, e.g. 0 = January year 1, 1 = February year 1, …). The function
|
||||
/// returns the **unique sorted month indices** and the corresponding
|
||||
/// **total return** for each month.
|
||||
///
|
||||
/// Parameters
|
||||
/// ----------
|
||||
/// bar_returns : 1-D float64 array — per-bar strategy returns
|
||||
/// month_index : 1-D int64 array — month bucket for each bar (same length)
|
||||
///
|
||||
/// Returns
|
||||
/// -------
|
||||
/// tuple ``(months, contributions)``:
|
||||
/// - ``months`` : 1-D int64 array — sorted unique month indices
|
||||
/// - ``contributions`` : 1-D float64 array — summed return per month
|
||||
#[pyfunction]
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub fn monthly_contribution<'py>(
|
||||
py: Python<'py>,
|
||||
bar_returns: PyReadonlyArray1<'py, f64>,
|
||||
month_index: PyReadonlyArray1<'py, i64>,
|
||||
) -> PyResult<(Bound<'py, PyArray1<i64>>, Bound<'py, PyArray1<f64>>)> {
|
||||
let ret = bar_returns.as_slice()?;
|
||||
let mi = month_index.as_slice()?;
|
||||
let n = ret.len();
|
||||
if n != mi.len() {
|
||||
return Err(PyValueError::new_err(
|
||||
"bar_returns and month_index must have the same length",
|
||||
));
|
||||
}
|
||||
|
||||
// Accumulate contributions by month
|
||||
let mut map: HashMap<i64, f64> = HashMap::new();
|
||||
for i in 0..n {
|
||||
if !ret[i].is_nan() {
|
||||
*map.entry(mi[i]).or_insert(0.0) += ret[i];
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by month index
|
||||
let mut months: Vec<i64> = map.keys().copied().collect();
|
||||
months.sort_unstable();
|
||||
let contributions: Vec<f64> = months.iter().map(|m| map[m]).collect();
|
||||
|
||||
Ok((months.into_pyarray(py), contributions.into_pyarray(py)))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// signal_attribution
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Attribute per-bar returns to each signal label.
|
||||
///
|
||||
/// Each bar has a *signal_label* (integer) indicating which signal or rule
|
||||
/// triggered the trade. ``-1`` means "no signal / flat". The function sums
|
||||
/// bar returns per signal label.
|
||||
///
|
||||
/// Parameters
|
||||
/// ----------
|
||||
/// bar_returns : 1-D float64 array — per-bar strategy returns
|
||||
/// signal_labels : 1-D int64 array — signal label per bar (same length)
|
||||
///
|
||||
/// Returns
|
||||
/// -------
|
||||
/// tuple ``(labels, contributions)``:
|
||||
/// - ``labels`` : 1-D int64 array — sorted unique signal labels
|
||||
/// - ``contributions`` : 1-D float64 array — summed return per label
|
||||
#[pyfunction]
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub fn signal_attribution<'py>(
|
||||
py: Python<'py>,
|
||||
bar_returns: PyReadonlyArray1<'py, f64>,
|
||||
signal_labels: PyReadonlyArray1<'py, i64>,
|
||||
) -> PyResult<(Bound<'py, PyArray1<i64>>, Bound<'py, PyArray1<f64>>)> {
|
||||
let ret = bar_returns.as_slice()?;
|
||||
let lbl = signal_labels.as_slice()?;
|
||||
let n = ret.len();
|
||||
if n != lbl.len() {
|
||||
return Err(PyValueError::new_err(
|
||||
"bar_returns and signal_labels must have the same length",
|
||||
));
|
||||
}
|
||||
|
||||
let mut map: HashMap<i64, f64> = HashMap::new();
|
||||
for i in 0..n {
|
||||
if !ret[i].is_nan() {
|
||||
*map.entry(lbl[i]).or_insert(0.0) += ret[i];
|
||||
}
|
||||
}
|
||||
|
||||
let mut labels: Vec<i64> = map.keys().copied().collect();
|
||||
labels.sort_unstable();
|
||||
let contributions: Vec<f64> = labels.iter().map(|l| map[l]).collect();
|
||||
|
||||
Ok((labels.into_pyarray(py), contributions.into_pyarray(py)))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Register
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
pub fn register(m: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||
m.add_function(wrap_pyfunction!(trade_stats, m)?)?;
|
||||
m.add_function(wrap_pyfunction!(monthly_contribution, m)?)?;
|
||||
m.add_function(wrap_pyfunction!(signal_attribution, m)?)?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,410 @@
|
||||
//! Rust-side batch execution — run SMA/EMA/RSI over all columns of a 2-D
|
||||
//! array in a **single GIL release**, avoiding per-column Python round-trips.
|
||||
//!
|
||||
//! Python shapes: `(n_samples, n_series)` — C-contiguous row-major.
|
||||
//! Rust iterates over columns (series) and rows (time) inside native code.
|
||||
//!
|
||||
//! When `parallel = true` (default), columns are processed in parallel via
|
||||
//! [Rayon](https://docs.rs/rayon) after releasing the GIL. For small inputs
|
||||
//! the sequential path (`parallel = false`) may be faster due to thread-pool
|
||||
//! overhead.
|
||||
|
||||
use ndarray::Array2;
|
||||
use numpy::{IntoPyArray, PyArray2, PyReadonlyArray2};
|
||||
use pyo3::exceptions::PyValueError;
|
||||
use pyo3::prelude::*;
|
||||
use rayon::prelude::*;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// batch_sma
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Batch Simple Moving Average — applies SMA to every column of a 2-D array.
|
||||
///
|
||||
/// Parameters
|
||||
/// ----------
|
||||
/// data : numpy array, shape (n_samples, n_series), dtype float64
|
||||
/// timeperiod : int
|
||||
/// parallel : bool, default True
|
||||
/// When True, columns are processed in parallel via Rayon (GIL released).
|
||||
///
|
||||
/// Returns
|
||||
/// -------
|
||||
/// numpy array, shape (n_samples, n_series), dtype float64
|
||||
/// Same shape as input; first ``timeperiod-1`` rows are NaN.
|
||||
#[pyfunction]
|
||||
#[pyo3(signature = (data, timeperiod = 30, parallel = true))]
|
||||
pub fn batch_sma<'py>(
|
||||
py: Python<'py>,
|
||||
data: PyReadonlyArray2<'py, f64>,
|
||||
timeperiod: usize,
|
||||
parallel: bool,
|
||||
) -> PyResult<Bound<'py, PyArray2<f64>>> {
|
||||
if timeperiod == 0 {
|
||||
return Err(PyValueError::new_err("timeperiod must be >= 1"));
|
||||
}
|
||||
let arr = data.as_array();
|
||||
let (n_samples, n_series) = arr.dim();
|
||||
log::debug!(
|
||||
"batch_sma: timeperiod={timeperiod}, shape=({n_samples}, {n_series}), parallel={parallel}"
|
||||
);
|
||||
|
||||
// Extract columns to owned Vecs so we can release the GIL for parallel work.
|
||||
let columns: Vec<Vec<f64>> = (0..n_series)
|
||||
.map(|j| (0..n_samples).map(|i| arr[[i, j]]).collect())
|
||||
.collect();
|
||||
|
||||
let process_col = |col: &Vec<f64>| -> Vec<f64> { ferro_ta_core::overlap::sma(col, timeperiod) };
|
||||
|
||||
let col_results: Vec<Vec<f64>> = py.allow_threads(|| {
|
||||
if parallel {
|
||||
columns.par_iter().map(process_col).collect()
|
||||
} else {
|
||||
columns.iter().map(process_col).collect()
|
||||
}
|
||||
});
|
||||
|
||||
let mut result = Array2::<f64>::from_elem((n_samples, n_series), f64::NAN);
|
||||
for (j, col_result) in col_results.iter().enumerate() {
|
||||
for (i, &val) in col_result.iter().enumerate() {
|
||||
result[[i, j]] = val;
|
||||
}
|
||||
}
|
||||
Ok(result.into_pyarray(py))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// batch_ema
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Batch Exponential Moving Average — applies EMA to every column.
|
||||
///
|
||||
/// Parameters
|
||||
/// ----------
|
||||
/// data : numpy array, shape (n_samples, n_series), dtype float64
|
||||
/// timeperiod : int
|
||||
/// parallel : bool, default True
|
||||
/// When True, columns are processed in parallel via Rayon (GIL released).
|
||||
///
|
||||
/// Returns
|
||||
/// -------
|
||||
/// numpy array, shape (n_samples, n_series), dtype float64
|
||||
#[pyfunction]
|
||||
#[pyo3(signature = (data, timeperiod = 30, parallel = true))]
|
||||
pub fn batch_ema<'py>(
|
||||
py: Python<'py>,
|
||||
data: PyReadonlyArray2<'py, f64>,
|
||||
timeperiod: usize,
|
||||
parallel: bool,
|
||||
) -> PyResult<Bound<'py, PyArray2<f64>>> {
|
||||
if timeperiod == 0 {
|
||||
return Err(PyValueError::new_err("timeperiod must be >= 1"));
|
||||
}
|
||||
let arr = data.as_array();
|
||||
let (n_samples, n_series) = arr.dim();
|
||||
log::debug!(
|
||||
"batch_ema: timeperiod={timeperiod}, shape=({n_samples}, {n_series}), parallel={parallel}"
|
||||
);
|
||||
|
||||
let columns: Vec<Vec<f64>> = (0..n_series)
|
||||
.map(|j| (0..n_samples).map(|i| arr[[i, j]]).collect())
|
||||
.collect();
|
||||
|
||||
let process_col = |col: &Vec<f64>| -> Vec<f64> { ferro_ta_core::overlap::ema(col, timeperiod) };
|
||||
|
||||
let col_results: Vec<Vec<f64>> = py.allow_threads(|| {
|
||||
if parallel {
|
||||
columns.par_iter().map(process_col).collect()
|
||||
} else {
|
||||
columns.iter().map(process_col).collect()
|
||||
}
|
||||
});
|
||||
|
||||
let mut result = Array2::<f64>::from_elem((n_samples, n_series), f64::NAN);
|
||||
for (j, col_result) in col_results.iter().enumerate() {
|
||||
for (i, &val) in col_result.iter().enumerate() {
|
||||
result[[i, j]] = val;
|
||||
}
|
||||
}
|
||||
Ok(result.into_pyarray(py))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// batch_rsi
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Batch RSI — applies RSI (Wilder seeding) to every column.
|
||||
///
|
||||
/// Parameters
|
||||
/// ----------
|
||||
/// data : numpy array, shape (n_samples, n_series), dtype float64
|
||||
/// timeperiod : int
|
||||
/// parallel : bool, default True
|
||||
/// When True, columns are processed in parallel via Rayon (GIL released).
|
||||
///
|
||||
/// Returns
|
||||
/// -------
|
||||
/// numpy array, shape (n_samples, n_series), dtype float64
|
||||
/// Values in [0, 100]; NaN during warmup.
|
||||
#[pyfunction]
|
||||
#[pyo3(signature = (data, timeperiod = 14, parallel = true))]
|
||||
pub fn batch_rsi<'py>(
|
||||
py: Python<'py>,
|
||||
data: PyReadonlyArray2<'py, f64>,
|
||||
timeperiod: usize,
|
||||
parallel: bool,
|
||||
) -> PyResult<Bound<'py, PyArray2<f64>>> {
|
||||
if timeperiod == 0 {
|
||||
return Err(PyValueError::new_err("timeperiod must be >= 1"));
|
||||
}
|
||||
let arr = data.as_array();
|
||||
let (n_samples, n_series) = arr.dim();
|
||||
log::debug!(
|
||||
"batch_rsi: timeperiod={timeperiod}, shape=({n_samples}, {n_series}), parallel={parallel}"
|
||||
);
|
||||
|
||||
let columns: Vec<Vec<f64>> = (0..n_series)
|
||||
.map(|j| (0..n_samples).map(|i| arr[[i, j]]).collect())
|
||||
.collect();
|
||||
|
||||
let period_f = timeperiod as f64;
|
||||
let process_col = |col: &Vec<f64>| -> Vec<f64> {
|
||||
let mut col_result = vec![f64::NAN; n_samples];
|
||||
if n_samples <= timeperiod {
|
||||
return col_result;
|
||||
}
|
||||
let mut avg_gain = 0.0_f64;
|
||||
let mut avg_loss = 0.0_f64;
|
||||
for i in 1..=timeperiod {
|
||||
let delta = col[i] - col[i - 1];
|
||||
if delta > 0.0 {
|
||||
avg_gain += delta;
|
||||
} else {
|
||||
avg_loss += -delta;
|
||||
}
|
||||
}
|
||||
avg_gain /= period_f;
|
||||
avg_loss /= period_f;
|
||||
let rs = if avg_loss == 0.0 {
|
||||
f64::MAX
|
||||
} else {
|
||||
avg_gain / avg_loss
|
||||
};
|
||||
col_result[timeperiod] = 100.0 - 100.0 / (1.0 + rs);
|
||||
for i in (timeperiod + 1)..n_samples {
|
||||
let delta = col[i] - col[i - 1];
|
||||
let (gain, loss) = if delta > 0.0 {
|
||||
(delta, 0.0)
|
||||
} else {
|
||||
(0.0, -delta)
|
||||
};
|
||||
avg_gain = (avg_gain * (period_f - 1.0) + gain) / period_f;
|
||||
avg_loss = (avg_loss * (period_f - 1.0) + loss) / period_f;
|
||||
let rs = if avg_loss == 0.0 {
|
||||
f64::MAX
|
||||
} else {
|
||||
avg_gain / avg_loss
|
||||
};
|
||||
col_result[i] = 100.0 - 100.0 / (1.0 + rs);
|
||||
}
|
||||
col_result
|
||||
};
|
||||
|
||||
let col_results: Vec<Vec<f64>> = py.allow_threads(|| {
|
||||
if parallel {
|
||||
columns.par_iter().map(process_col).collect()
|
||||
} else {
|
||||
columns.iter().map(process_col).collect()
|
||||
}
|
||||
});
|
||||
|
||||
let mut result = Array2::<f64>::from_elem((n_samples, n_series), f64::NAN);
|
||||
for (j, col_result) in col_results.iter().enumerate() {
|
||||
for (i, &val) in col_result.iter().enumerate() {
|
||||
result[[i, j]] = val;
|
||||
}
|
||||
}
|
||||
Ok(result.into_pyarray(py))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// batch_atr
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[pyfunction]
|
||||
#[pyo3(signature = (high, low, close, timeperiod = 14, parallel = true))]
|
||||
pub fn batch_atr<'py>(
|
||||
py: Python<'py>,
|
||||
high: PyReadonlyArray2<'py, f64>,
|
||||
low: PyReadonlyArray2<'py, f64>,
|
||||
close: PyReadonlyArray2<'py, f64>,
|
||||
timeperiod: usize,
|
||||
parallel: bool,
|
||||
) -> PyResult<Bound<'py, PyArray2<f64>>> {
|
||||
if timeperiod == 0 {
|
||||
return Err(PyValueError::new_err("timeperiod must be >= 1"));
|
||||
}
|
||||
let arr_h = high.as_array();
|
||||
let arr_l = low.as_array();
|
||||
let arr_c = close.as_array();
|
||||
let (n_samples, n_series) = arr_h.dim();
|
||||
|
||||
let cols_h: Vec<Vec<f64>> = (0..n_series)
|
||||
.map(|j| (0..n_samples).map(|i| arr_h[[i, j]]).collect())
|
||||
.collect();
|
||||
let cols_l: Vec<Vec<f64>> = (0..n_series)
|
||||
.map(|j| (0..n_samples).map(|i| arr_l[[i, j]]).collect())
|
||||
.collect();
|
||||
let cols_c: Vec<Vec<f64>> = (0..n_series)
|
||||
.map(|j| (0..n_samples).map(|i| arr_c[[i, j]]).collect())
|
||||
.collect();
|
||||
|
||||
let col_results: Vec<Vec<f64>> = py.allow_threads(|| {
|
||||
let process_col = |j: usize| -> Vec<f64> {
|
||||
ferro_ta_core::volatility::atr(&cols_h[j], &cols_l[j], &cols_c[j], timeperiod)
|
||||
};
|
||||
if parallel {
|
||||
(0..n_series).into_par_iter().map(process_col).collect()
|
||||
} else {
|
||||
(0..n_series).map(process_col).collect()
|
||||
}
|
||||
});
|
||||
|
||||
let mut result = Array2::<f64>::from_elem((n_samples, n_series), f64::NAN);
|
||||
for (j, col_result) in col_results.iter().enumerate() {
|
||||
for (i, &val) in col_result.iter().enumerate() {
|
||||
result[[i, j]] = val;
|
||||
}
|
||||
}
|
||||
Ok(result.into_pyarray(py))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// batch_stoch
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Stoch batch result type (slowk, slowd arrays).
|
||||
type StochBatchResult<'py> = (Bound<'py, PyArray2<f64>>, Bound<'py, PyArray2<f64>>);
|
||||
|
||||
#[pyfunction]
|
||||
#[pyo3(signature = (high, low, close, fastk_period = 5, slowk_period = 3, slowd_period = 3, parallel = true))]
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn batch_stoch<'py>(
|
||||
py: Python<'py>,
|
||||
high: PyReadonlyArray2<'py, f64>,
|
||||
low: PyReadonlyArray2<'py, f64>,
|
||||
close: PyReadonlyArray2<'py, f64>,
|
||||
fastk_period: usize,
|
||||
slowk_period: usize,
|
||||
slowd_period: usize,
|
||||
parallel: bool,
|
||||
) -> PyResult<StochBatchResult<'py>> {
|
||||
let arr_h = high.as_array();
|
||||
let arr_l = low.as_array();
|
||||
let arr_c = close.as_array();
|
||||
let (n_samples, n_series) = arr_h.dim();
|
||||
|
||||
let cols_h: Vec<Vec<f64>> = (0..n_series)
|
||||
.map(|j| (0..n_samples).map(|i| arr_h[[i, j]]).collect())
|
||||
.collect();
|
||||
let cols_l: Vec<Vec<f64>> = (0..n_series)
|
||||
.map(|j| (0..n_samples).map(|i| arr_l[[i, j]]).collect())
|
||||
.collect();
|
||||
let cols_c: Vec<Vec<f64>> = (0..n_series)
|
||||
.map(|j| (0..n_samples).map(|i| arr_c[[i, j]]).collect())
|
||||
.collect();
|
||||
|
||||
let col_results: Vec<(Vec<f64>, Vec<f64>)> = py.allow_threads(|| {
|
||||
let process_col = |j: usize| -> (Vec<f64>, Vec<f64>) {
|
||||
ferro_ta_core::momentum::stoch(
|
||||
&cols_h[j],
|
||||
&cols_l[j],
|
||||
&cols_c[j],
|
||||
fastk_period,
|
||||
slowk_period,
|
||||
slowd_period,
|
||||
)
|
||||
};
|
||||
if parallel {
|
||||
(0..n_series).into_par_iter().map(process_col).collect()
|
||||
} else {
|
||||
(0..n_series).map(process_col).collect()
|
||||
}
|
||||
});
|
||||
|
||||
let mut result_k = Array2::<f64>::from_elem((n_samples, n_series), f64::NAN);
|
||||
let mut result_d = Array2::<f64>::from_elem((n_samples, n_series), f64::NAN);
|
||||
for (j, (k_col, d_col)) in col_results.iter().enumerate() {
|
||||
for i in 0..n_samples {
|
||||
result_k[[i, j]] = k_col[i];
|
||||
result_d[[i, j]] = d_col[i];
|
||||
}
|
||||
}
|
||||
Ok((result_k.into_pyarray(py), result_d.into_pyarray(py)))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// batch_adx
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[pyfunction]
|
||||
#[pyo3(signature = (high, low, close, timeperiod = 14, parallel = true))]
|
||||
pub fn batch_adx<'py>(
|
||||
py: Python<'py>,
|
||||
high: PyReadonlyArray2<'py, f64>,
|
||||
low: PyReadonlyArray2<'py, f64>,
|
||||
close: PyReadonlyArray2<'py, f64>,
|
||||
timeperiod: usize,
|
||||
parallel: bool,
|
||||
) -> PyResult<Bound<'py, PyArray2<f64>>> {
|
||||
if timeperiod == 0 {
|
||||
return Err(PyValueError::new_err("timeperiod must be >= 1"));
|
||||
}
|
||||
let arr_h = high.as_array();
|
||||
let arr_l = low.as_array();
|
||||
let arr_c = close.as_array();
|
||||
let (n_samples, n_series) = arr_h.dim();
|
||||
|
||||
let cols_h: Vec<Vec<f64>> = (0..n_series)
|
||||
.map(|j| (0..n_samples).map(|i| arr_h[[i, j]]).collect())
|
||||
.collect();
|
||||
let cols_l: Vec<Vec<f64>> = (0..n_series)
|
||||
.map(|j| (0..n_samples).map(|i| arr_l[[i, j]]).collect())
|
||||
.collect();
|
||||
let cols_c: Vec<Vec<f64>> = (0..n_series)
|
||||
.map(|j| (0..n_samples).map(|i| arr_c[[i, j]]).collect())
|
||||
.collect();
|
||||
|
||||
let col_results: Vec<Vec<f64>> = py.allow_threads(|| {
|
||||
let process_col = |j: usize| -> Vec<f64> {
|
||||
ferro_ta_core::momentum::adx(&cols_h[j], &cols_l[j], &cols_c[j], timeperiod)
|
||||
};
|
||||
if parallel {
|
||||
(0..n_series).into_par_iter().map(process_col).collect()
|
||||
} else {
|
||||
(0..n_series).map(process_col).collect()
|
||||
}
|
||||
});
|
||||
|
||||
let mut result = Array2::<f64>::from_elem((n_samples, n_series), f64::NAN);
|
||||
for (j, col_result) in col_results.iter().enumerate() {
|
||||
for (i, &val) in col_result.iter().enumerate() {
|
||||
result[[i, j]] = val;
|
||||
}
|
||||
}
|
||||
Ok(result.into_pyarray(py))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// register
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
pub fn register(m: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||
m.add_function(pyo3::wrap_pyfunction!(batch_sma, m)?)?;
|
||||
m.add_function(pyo3::wrap_pyfunction!(batch_ema, m)?)?;
|
||||
m.add_function(pyo3::wrap_pyfunction!(batch_rsi, m)?)?;
|
||||
m.add_function(pyo3::wrap_pyfunction!(batch_atr, m)?)?;
|
||||
m.add_function(pyo3::wrap_pyfunction!(batch_stoch, m)?)?;
|
||||
m.add_function(pyo3::wrap_pyfunction!(batch_adx, m)?)?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
//! Chunked / out-of-core execution helpers.
|
||||
//!
|
||||
//! These functions support running indicators on data that is too large for
|
||||
//! memory by processing it in chunks. The caller splits a large series into
|
||||
//! overlapping chunks (overlap = indicator warm-up period), runs an indicator
|
||||
//! on each chunk, and then stitches the results by trimming the overlap from
|
||||
//! the front of each chunk's output.
|
||||
//!
|
||||
//! Functions
|
||||
//! ---------
|
||||
//! - `trim_overlap` — remove the first *overlap* elements from an array
|
||||
//! (to strip the warm-up from a chunk's indicator output).
|
||||
//! - `stitch_chunks` — concatenate trimmed chunk results into one array.
|
||||
//! - `make_chunk_ranges` — compute start/end indices for a series given chunk
|
||||
//! size and overlap, for use by the Python caller.
|
||||
|
||||
use numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};
|
||||
use pyo3::exceptions::PyValueError;
|
||||
use pyo3::prelude::*;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// trim_overlap
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Remove the first *overlap* elements from an array.
|
||||
///
|
||||
/// After running an indicator on a chunk that includes a warm-up prefix, the
|
||||
/// first *overlap* output values are unreliable (NaN or influenced by
|
||||
/// artificial padding). This function discards them.
|
||||
///
|
||||
/// Parameters
|
||||
/// ----------
|
||||
/// chunk_out : 1-D float64 array — indicator output for a chunk
|
||||
/// overlap : int — number of leading elements to discard
|
||||
///
|
||||
/// Returns
|
||||
/// -------
|
||||
/// 1-D float64 array — the trailing ``len(chunk_out) - overlap`` elements
|
||||
#[pyfunction]
|
||||
pub fn trim_overlap<'py>(
|
||||
py: Python<'py>,
|
||||
chunk_out: PyReadonlyArray1<'py, f64>,
|
||||
overlap: usize,
|
||||
) -> PyResult<Bound<'py, PyArray1<f64>>> {
|
||||
let s = chunk_out.as_slice()?;
|
||||
let n = s.len();
|
||||
if overlap > n {
|
||||
return Err(PyValueError::new_err(format!(
|
||||
"overlap ({overlap}) must be <= chunk length ({n})"
|
||||
)));
|
||||
}
|
||||
let out = s[overlap..].to_vec();
|
||||
Ok(out.into_pyarray(py))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// stitch_chunks
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Concatenate a list of trimmed chunk results into a single output array.
|
||||
///
|
||||
/// Parameters
|
||||
/// ----------
|
||||
/// chunks : list of 1-D float64 arrays — trimmed outputs from each chunk
|
||||
///
|
||||
/// Returns
|
||||
/// -------
|
||||
/// 1-D float64 array — concatenated result
|
||||
#[pyfunction]
|
||||
pub fn stitch_chunks<'py>(
|
||||
py: Python<'py>,
|
||||
chunks: Vec<PyReadonlyArray1<'py, f64>>,
|
||||
) -> PyResult<Bound<'py, PyArray1<f64>>> {
|
||||
let mut out: Vec<f64> = Vec::new();
|
||||
for chunk in &chunks {
|
||||
out.extend_from_slice(chunk.as_slice()?);
|
||||
}
|
||||
Ok(out.into_pyarray(py))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// make_chunk_ranges
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Compute the (start, end) index pairs for chunked processing.
|
||||
///
|
||||
/// Each range ``[start, end)`` specifies a slice of the input series that the
|
||||
/// caller should pass to the indicator function. The first *overlap* elements
|
||||
/// of each range (except the very first range) are the warm-up prefix from the
|
||||
/// previous chunk.
|
||||
///
|
||||
/// Parameters
|
||||
/// ----------
|
||||
/// n : int — total length of the series
|
||||
/// chunk_size : int — desired number of *output* bars per chunk (>= 1)
|
||||
/// overlap : int — number of warm-up bars prepended to each chunk (>= 0)
|
||||
///
|
||||
/// Returns
|
||||
/// -------
|
||||
/// list of (start: int, end: int) pairs as a flattened 1-D int64 array of
|
||||
/// length 2 × n_chunks. Caller unpacks with ``ranges.reshape(-1, 2)``.
|
||||
///
|
||||
/// Example
|
||||
/// -------
|
||||
/// For n=10, chunk_size=4, overlap=2 the ranges would cover:
|
||||
/// [0, 4), [2, 8), [6, 10) (start of chunk 2 = end of prev chunk - overlap)
|
||||
#[pyfunction]
|
||||
pub fn make_chunk_ranges<'py>(
|
||||
py: Python<'py>,
|
||||
n: usize,
|
||||
chunk_size: usize,
|
||||
overlap: usize,
|
||||
) -> PyResult<Bound<'py, PyArray1<i64>>> {
|
||||
if chunk_size == 0 {
|
||||
return Err(PyValueError::new_err("chunk_size must be >= 1"));
|
||||
}
|
||||
let mut ranges: Vec<i64> = Vec::new();
|
||||
if n == 0 {
|
||||
return Ok(ranges.into_pyarray(py));
|
||||
}
|
||||
let mut start: usize = 0;
|
||||
loop {
|
||||
let end = (start + chunk_size + overlap).min(n);
|
||||
ranges.push(start as i64);
|
||||
ranges.push(end as i64);
|
||||
if end >= n {
|
||||
break;
|
||||
}
|
||||
// Next chunk starts at end - overlap (so the next chunk has its overlap prefix)
|
||||
start = end.saturating_sub(overlap);
|
||||
}
|
||||
Ok(ranges.into_pyarray(py))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Register
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
pub fn register(m: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||
m.add_function(wrap_pyfunction!(trim_overlap, m)?)?;
|
||||
m.add_function(wrap_pyfunction!(stitch_chunks, m)?)?;
|
||||
m.add_function(wrap_pyfunction!(make_chunk_ranges, m)?)?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
//! Crypto and 24/7 market helpers.
|
||||
//!
|
||||
//! Functions designed for continuous (24/7) markets such as crypto:
|
||||
//!
|
||||
//! - `funding_cumulative_pnl` — cumulative PnL from periodic funding rate
|
||||
//! payments, given a constant position size.
|
||||
//! - `continuous_bar_labels` — assign a sequential integer label to each bar
|
||||
//! based on a fixed period size (e.g. daily UTC buckets).
|
||||
//! - `mark_session_boundaries` — return indices where the session rolls over
|
||||
//! (useful for calendar-free resampling on continuous data).
|
||||
|
||||
use numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};
|
||||
use pyo3::exceptions::PyValueError;
|
||||
use pyo3::prelude::*;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// funding_cumulative_pnl
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Compute the cumulative PnL from funding rate payments.
|
||||
///
|
||||
/// Crypto perpetual contracts charge a periodic funding rate to the holder.
|
||||
/// If you hold ``position_size`` contracts and the funding rate is ``rate[i]``,
|
||||
/// the PnL at period *i* is ``-position_size * rate[i]`` (longs pay when rate
|
||||
/// is positive). This function returns the **cumulative** funding PnL.
|
||||
///
|
||||
/// Parameters
|
||||
/// ----------
|
||||
/// position_size : 1-D float64 array — signed position size per funding period
|
||||
/// (positive = long, negative = short). Must have the same length as
|
||||
/// ``funding_rate``.
|
||||
/// funding_rate : 1-D float64 array — periodic funding rate (decimal, e.g.
|
||||
/// 0.0001 = 0.01%).
|
||||
///
|
||||
/// Returns
|
||||
/// -------
|
||||
/// 1-D float64 array — cumulative funding PnL (same length as inputs).
|
||||
#[pyfunction]
|
||||
pub fn funding_cumulative_pnl<'py>(
|
||||
py: Python<'py>,
|
||||
position_size: PyReadonlyArray1<'py, f64>,
|
||||
funding_rate: PyReadonlyArray1<'py, f64>,
|
||||
) -> PyResult<Bound<'py, PyArray1<f64>>> {
|
||||
let pos = position_size.as_slice()?;
|
||||
let rate = funding_rate.as_slice()?;
|
||||
let n = pos.len();
|
||||
if n != rate.len() {
|
||||
return Err(PyValueError::new_err(
|
||||
"position_size and funding_rate must have the same length",
|
||||
));
|
||||
}
|
||||
let mut out = vec![0.0_f64; n];
|
||||
let mut cumulative = 0.0_f64;
|
||||
for i in 0..n {
|
||||
// Longs pay when rate > 0; shorts receive
|
||||
cumulative += -pos[i] * rate[i];
|
||||
out[i] = cumulative;
|
||||
}
|
||||
Ok(out.into_pyarray(py))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// continuous_bar_labels
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Assign a sequential integer label per bar based on a fixed-size period.
|
||||
///
|
||||
/// For 24/7 data (no session gaps), this groups bars into equal-sized buckets:
|
||||
/// bars 0…(period_bars-1) get label 0, bars period_bars…(2*period_bars-1) get
|
||||
/// label 1, etc. Useful for resampling continuous data (e.g. group every 24
|
||||
/// one-hour bars into a "day").
|
||||
///
|
||||
/// Parameters
|
||||
/// ----------
|
||||
/// n_bars : int — total number of bars
|
||||
/// period_bars: int — number of bars per period (must be >= 1)
|
||||
///
|
||||
/// Returns
|
||||
/// -------
|
||||
/// 1-D int64 array of length *n_bars* — period labels (0-based).
|
||||
#[pyfunction]
|
||||
pub fn continuous_bar_labels<'py>(
|
||||
py: Python<'py>,
|
||||
n_bars: usize,
|
||||
period_bars: usize,
|
||||
) -> PyResult<Bound<'py, PyArray1<i64>>> {
|
||||
if period_bars == 0 {
|
||||
return Err(PyValueError::new_err("period_bars must be >= 1"));
|
||||
}
|
||||
let out: Vec<i64> = (0..n_bars).map(|i| (i / period_bars) as i64).collect();
|
||||
Ok(out.into_pyarray(py))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// mark_session_boundaries
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Return bar indices where a new session begins.
|
||||
///
|
||||
/// Given UTC timestamps in nanoseconds (int64), marks boundaries at the start
|
||||
/// of each new UTC day (midnight). Returns the bar indices where the UTC day
|
||||
/// changes. Bar 0 is always included as the first boundary.
|
||||
///
|
||||
/// Parameters
|
||||
/// ----------
|
||||
/// timestamps_ns : 1-D int64 array — UTC timestamps in nanoseconds
|
||||
/// (e.g. from ``pandas.DatetimeIndex.astype('int64')``).
|
||||
///
|
||||
/// Returns
|
||||
/// -------
|
||||
/// 1-D int64 array — indices of bars at the start of each new UTC day.
|
||||
#[pyfunction]
|
||||
pub fn mark_session_boundaries<'py>(
|
||||
py: Python<'py>,
|
||||
timestamps_ns: PyReadonlyArray1<'py, i64>,
|
||||
) -> PyResult<Bound<'py, PyArray1<i64>>> {
|
||||
let ts = timestamps_ns.as_slice()?;
|
||||
let n = ts.len();
|
||||
if n == 0 {
|
||||
return Ok(Vec::<i64>::new().into_pyarray(py));
|
||||
}
|
||||
// Nanoseconds per day
|
||||
const NS_PER_DAY: i64 = 86_400_000_000_000;
|
||||
let mut out = vec![0i64]; // bar 0 is always a boundary
|
||||
let mut prev_day = ts[0].div_euclid(NS_PER_DAY);
|
||||
for (i, &t) in ts.iter().enumerate().skip(1) {
|
||||
let day = t.div_euclid(NS_PER_DAY);
|
||||
if day != prev_day {
|
||||
out.push(i as i64);
|
||||
prev_day = day;
|
||||
}
|
||||
}
|
||||
Ok(out.into_pyarray(py))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Register
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
pub fn register(m: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||
m.add_function(wrap_pyfunction!(funding_cumulative_pnl, m)?)?;
|
||||
m.add_function(wrap_pyfunction!(continuous_bar_labels, m)?)?;
|
||||
m.add_function(wrap_pyfunction!(mark_session_boundaries, m)?)?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
@@ -0,0 +1,822 @@
|
||||
//! Extended Indicators — Rust implementations of indicators not in TA-Lib.
|
||||
//!
|
||||
//! All compute-heavy work (sequential loops, rolling windows) is done in Rust.
|
||||
//! Python wrappers in `python/ferro_ta/extended.py` are thin call-throughs that
|
||||
//! handle input conversion and pandas/polars wrapping.
|
||||
|
||||
#![allow(clippy::type_complexity)]
|
||||
#![allow(clippy::too_many_arguments)]
|
||||
|
||||
use std::collections::VecDeque;
|
||||
|
||||
use crate::validation;
|
||||
use numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};
|
||||
use pyo3::exceptions::PyValueError;
|
||||
use pyo3::prelude::*;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Internal helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Compute ATR array using Wilder smoothing (same as src/volatility/atr.rs).
|
||||
fn compute_atr(high: &[f64], low: &[f64], close: &[f64], timeperiod: usize) -> Vec<f64> {
|
||||
let n = high.len();
|
||||
let mut result = vec![f64::NAN; n];
|
||||
if n <= timeperiod {
|
||||
return result;
|
||||
}
|
||||
// Seed: SMA of first `timeperiod` true range values
|
||||
let mut seed_sum = high[0] - low[0]; // first TR has no prev_close
|
||||
for i in 1..timeperiod {
|
||||
let hl = high[i] - low[i];
|
||||
let hc = (high[i] - close[i - 1]).abs();
|
||||
let lc = (low[i] - close[i - 1]).abs();
|
||||
seed_sum += hl.max(hc).max(lc);
|
||||
}
|
||||
let mut atr = seed_sum / timeperiod as f64;
|
||||
result[timeperiod - 1] = atr;
|
||||
let pf = (timeperiod - 1) as f64;
|
||||
for i in timeperiod..n {
|
||||
let hl = high[i] - low[i];
|
||||
let hc = (high[i] - close[i - 1]).abs();
|
||||
let lc = (low[i] - close[i - 1]).abs();
|
||||
let tr = hl.max(hc).max(lc);
|
||||
atr = (atr * pf + tr) / timeperiod as f64;
|
||||
result[i] = atr;
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
/// Compute EMA using ferro_ta_core (SMA-seeded, matches Python EMA).
|
||||
fn compute_ema_ta(prices: &[f64], timeperiod: usize) -> Vec<f64> {
|
||||
ferro_ta_core::overlap::ema(prices, timeperiod)
|
||||
}
|
||||
|
||||
/// Compute WMA array using ferro_ta_core O(n) implementation.
|
||||
fn compute_wma(prices: &[f64], timeperiod: usize) -> Vec<f64> {
|
||||
ferro_ta_core::overlap::wma(prices, timeperiod)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// VWAP
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Volume Weighted Average Price (cumulative or rolling).
|
||||
///
|
||||
/// Parameters
|
||||
/// ----------
|
||||
/// high, low, close, volume : 1-D float64 arrays (equal length)
|
||||
/// timeperiod : 0 = cumulative from bar 0; >= 1 = rolling window
|
||||
///
|
||||
/// Returns
|
||||
/// -------
|
||||
/// 1-D float64 array of VWAP values.
|
||||
#[pyfunction]
|
||||
#[pyo3(signature = (high, low, close, volume, timeperiod = 0))]
|
||||
pub fn vwap<'py>(
|
||||
py: Python<'py>,
|
||||
high: PyReadonlyArray1<'py, f64>,
|
||||
low: PyReadonlyArray1<'py, f64>,
|
||||
close: PyReadonlyArray1<'py, f64>,
|
||||
volume: PyReadonlyArray1<'py, f64>,
|
||||
timeperiod: usize,
|
||||
) -> PyResult<Bound<'py, PyArray1<f64>>> {
|
||||
let h = high.as_slice()?;
|
||||
let lo = low.as_slice()?;
|
||||
let c = close.as_slice()?;
|
||||
let v = volume.as_slice()?;
|
||||
let n = h.len();
|
||||
validation::validate_equal_length(&[
|
||||
(n, "high"),
|
||||
(lo.len(), "low"),
|
||||
(c.len(), "close"),
|
||||
(v.len(), "volume"),
|
||||
])?;
|
||||
|
||||
let mut result = vec![f64::NAN; n];
|
||||
let mut cum_tpv = 0.0f64;
|
||||
let mut cum_vol = 0.0f64;
|
||||
|
||||
if timeperiod == 0 {
|
||||
for i in 0..n {
|
||||
let tp = (h[i] + lo[i] + c[i]) / 3.0;
|
||||
cum_tpv += tp * v[i];
|
||||
cum_vol += v[i];
|
||||
result[i] = if cum_vol != 0.0 {
|
||||
cum_tpv / cum_vol
|
||||
} else {
|
||||
f64::NAN
|
||||
};
|
||||
}
|
||||
} else {
|
||||
// Pre-compute cumulative sums for O(n) rolling window
|
||||
let mut cum_tpv_arr = vec![0.0f64; n];
|
||||
let mut cum_vol_arr = vec![0.0f64; n];
|
||||
for i in 0..n {
|
||||
let tp = (h[i] + lo[i] + c[i]) / 3.0;
|
||||
let tpv = tp * v[i];
|
||||
cum_tpv_arr[i] = tpv + if i > 0 { cum_tpv_arr[i - 1] } else { 0.0 };
|
||||
cum_vol_arr[i] = v[i] + if i > 0 { cum_vol_arr[i - 1] } else { 0.0 };
|
||||
}
|
||||
for i in (timeperiod - 1)..n {
|
||||
let prev_tpv = if i >= timeperiod {
|
||||
cum_tpv_arr[i - timeperiod]
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
let prev_vol = if i >= timeperiod {
|
||||
cum_vol_arr[i - timeperiod]
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
let w_tpv = cum_tpv_arr[i] - prev_tpv;
|
||||
let w_vol = cum_vol_arr[i] - prev_vol;
|
||||
result[i] = if w_vol != 0.0 {
|
||||
w_tpv / w_vol
|
||||
} else {
|
||||
f64::NAN
|
||||
};
|
||||
}
|
||||
}
|
||||
Ok(result.into_pyarray(py))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// VWMA
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Volume Weighted Moving Average.
|
||||
///
|
||||
/// VWMA = sum(close * volume, n) / sum(volume, n)
|
||||
#[pyfunction]
|
||||
#[pyo3(signature = (close, volume, timeperiod = 20))]
|
||||
pub fn vwma<'py>(
|
||||
py: Python<'py>,
|
||||
close: PyReadonlyArray1<'py, f64>,
|
||||
volume: PyReadonlyArray1<'py, f64>,
|
||||
timeperiod: usize,
|
||||
) -> PyResult<Bound<'py, PyArray1<f64>>> {
|
||||
validation::validate_timeperiod(timeperiod, "timeperiod", 1)?;
|
||||
let c = close.as_slice()?;
|
||||
let v = volume.as_slice()?;
|
||||
let n = c.len();
|
||||
validation::validate_equal_length(&[(n, "close"), (v.len(), "volume")])?;
|
||||
|
||||
let mut cum_cv = vec![0.0f64; n];
|
||||
let mut cum_v = vec![0.0f64; n];
|
||||
for i in 0..n {
|
||||
cum_cv[i] = c[i] * v[i] + if i > 0 { cum_cv[i - 1] } else { 0.0 };
|
||||
cum_v[i] = v[i] + if i > 0 { cum_v[i - 1] } else { 0.0 };
|
||||
}
|
||||
|
||||
let mut result = vec![f64::NAN; n];
|
||||
for i in (timeperiod - 1)..n {
|
||||
let prev_cv = if i >= timeperiod {
|
||||
cum_cv[i - timeperiod]
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
let prev_v = if i >= timeperiod {
|
||||
cum_v[i - timeperiod]
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
let w_cv = cum_cv[i] - prev_cv;
|
||||
let w_v = cum_v[i] - prev_v;
|
||||
result[i] = if w_v != 0.0 { w_cv / w_v } else { f64::NAN };
|
||||
}
|
||||
Ok(result.into_pyarray(py))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// SUPERTREND
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// ATR-based Supertrend indicator.
|
||||
///
|
||||
/// Returns (supertrend_line, direction) where direction is an int8 array:
|
||||
/// 1 = uptrend, -1 = downtrend, 0 = warmup.
|
||||
#[pyfunction]
|
||||
#[pyo3(signature = (high, low, close, timeperiod = 7, multiplier = 3.0))]
|
||||
pub fn supertrend<'py>(
|
||||
py: Python<'py>,
|
||||
high: PyReadonlyArray1<'py, f64>,
|
||||
low: PyReadonlyArray1<'py, f64>,
|
||||
close: PyReadonlyArray1<'py, f64>,
|
||||
timeperiod: usize,
|
||||
multiplier: f64,
|
||||
) -> PyResult<(Bound<'py, PyArray1<f64>>, Bound<'py, PyArray1<i8>>)> {
|
||||
validation::validate_timeperiod(timeperiod, "timeperiod", 1)?;
|
||||
let h = high.as_slice()?;
|
||||
let lo = low.as_slice()?;
|
||||
let c = close.as_slice()?;
|
||||
let n = h.len();
|
||||
validation::validate_equal_length(&[(n, "high"), (lo.len(), "low"), (c.len(), "close")])?;
|
||||
|
||||
let atr = compute_atr(h, lo, c, timeperiod);
|
||||
|
||||
let mut supertrend_out = vec![f64::NAN; n];
|
||||
let mut direction = vec![0i8; n];
|
||||
let mut upper_band = vec![f64::NAN; n];
|
||||
let mut lower_band = vec![f64::NAN; n];
|
||||
|
||||
let first_valid = timeperiod - 1;
|
||||
if first_valid >= n || atr[first_valid].is_nan() {
|
||||
return Ok((supertrend_out.into_pyarray(py), direction.into_pyarray(py)));
|
||||
}
|
||||
|
||||
// Compute basic bands
|
||||
let mut upper_basic = vec![f64::NAN; n];
|
||||
let mut lower_basic = vec![f64::NAN; n];
|
||||
for i in 0..n {
|
||||
if !atr[i].is_nan() {
|
||||
let hl2 = (h[i] + lo[i]) / 2.0;
|
||||
upper_basic[i] = hl2 + multiplier * atr[i];
|
||||
lower_basic[i] = hl2 - multiplier * atr[i];
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize band state at first valid ATR bar
|
||||
upper_band[first_valid] = upper_basic[first_valid];
|
||||
lower_band[first_valid] = lower_basic[first_valid];
|
||||
// Keep supertrend_out NaN and direction 0 for indices 0..timeperiod (warmup)
|
||||
|
||||
for i in (first_valid + 1)..n {
|
||||
if atr[i].is_nan() {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Adjust lower band
|
||||
lower_band[i] = if lower_basic[i] > lower_band[i - 1] || c[i - 1] < lower_band[i - 1] {
|
||||
lower_basic[i]
|
||||
} else {
|
||||
lower_band[i - 1]
|
||||
};
|
||||
|
||||
// Adjust upper band
|
||||
upper_band[i] = if upper_basic[i] < upper_band[i - 1] || c[i - 1] > upper_band[i - 1] {
|
||||
upper_basic[i]
|
||||
} else {
|
||||
upper_band[i - 1]
|
||||
};
|
||||
|
||||
// Direction and output only from index timeperiod (warmup = 0, NaN)
|
||||
if i >= timeperiod {
|
||||
let prev_dir = direction[i - 1];
|
||||
direction[i] = if prev_dir == 0 {
|
||||
// First output bar: bootstrap with -1 (downtrend) or 1 from price vs band
|
||||
if c[i] > upper_band[i] {
|
||||
1
|
||||
} else {
|
||||
-1
|
||||
}
|
||||
} else if prev_dir == -1 {
|
||||
if c[i] > upper_band[i] {
|
||||
1
|
||||
} else {
|
||||
-1
|
||||
}
|
||||
} else if c[i] < lower_band[i] {
|
||||
-1
|
||||
} else {
|
||||
1
|
||||
};
|
||||
supertrend_out[i] = if direction[i] == 1 {
|
||||
lower_band[i]
|
||||
} else {
|
||||
upper_band[i]
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Ok((supertrend_out.into_pyarray(py), direction.into_pyarray(py)))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// DONCHIAN
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Donchian Channels — rolling highest high / lowest low.
|
||||
///
|
||||
/// Returns (upper, middle, lower) arrays.
|
||||
#[pyfunction]
|
||||
#[pyo3(signature = (high, low, timeperiod = 20))]
|
||||
pub fn donchian<'py>(
|
||||
py: Python<'py>,
|
||||
high: PyReadonlyArray1<'py, f64>,
|
||||
low: PyReadonlyArray1<'py, f64>,
|
||||
timeperiod: usize,
|
||||
) -> PyResult<(
|
||||
Bound<'py, PyArray1<f64>>,
|
||||
Bound<'py, PyArray1<f64>>,
|
||||
Bound<'py, PyArray1<f64>>,
|
||||
)> {
|
||||
validation::validate_timeperiod(timeperiod, "timeperiod", 1)?;
|
||||
let h = high.as_slice()?;
|
||||
let lo = low.as_slice()?;
|
||||
let n = h.len();
|
||||
validation::validate_equal_length(&[(n, "high"), (lo.len(), "low")])?;
|
||||
|
||||
let mut upper = vec![f64::NAN; n];
|
||||
let mut lower = vec![f64::NAN; n];
|
||||
let mut middle = vec![f64::NAN; n];
|
||||
|
||||
// Use monotonic deque for O(n) sliding max / min
|
||||
let mut max_dq: VecDeque<usize> = VecDeque::new();
|
||||
let mut min_dq: VecDeque<usize> = VecDeque::new();
|
||||
|
||||
for i in 0..n {
|
||||
// Remove out-of-window indices
|
||||
while max_dq
|
||||
.front()
|
||||
.map(|&j| j + timeperiod <= i)
|
||||
.unwrap_or(false)
|
||||
{
|
||||
max_dq.pop_front();
|
||||
}
|
||||
while min_dq
|
||||
.front()
|
||||
.map(|&j| j + timeperiod <= i)
|
||||
.unwrap_or(false)
|
||||
{
|
||||
min_dq.pop_front();
|
||||
}
|
||||
// Maintain decreasing deque for max
|
||||
while max_dq.back().map(|&j| h[j] <= h[i]).unwrap_or(false) {
|
||||
max_dq.pop_back();
|
||||
}
|
||||
max_dq.push_back(i);
|
||||
// Maintain increasing deque for min
|
||||
while min_dq.back().map(|&j| lo[j] >= lo[i]).unwrap_or(false) {
|
||||
min_dq.pop_back();
|
||||
}
|
||||
min_dq.push_back(i);
|
||||
|
||||
if i + 1 >= timeperiod {
|
||||
upper[i] = h[*max_dq.front().unwrap()];
|
||||
lower[i] = lo[*min_dq.front().unwrap()];
|
||||
middle[i] = (upper[i] + lower[i]) / 2.0;
|
||||
}
|
||||
}
|
||||
|
||||
Ok((
|
||||
upper.into_pyarray(py),
|
||||
middle.into_pyarray(py),
|
||||
lower.into_pyarray(py),
|
||||
))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// CHOPPINESS_INDEX
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Choppiness Index — measures market choppiness vs trending.
|
||||
///
|
||||
/// Values near 100 → choppy; near 0 → trending.
|
||||
/// Leading `timeperiod` values are NaN.
|
||||
#[pyfunction]
|
||||
#[pyo3(signature = (high, low, close, timeperiod = 14))]
|
||||
pub fn choppiness_index<'py>(
|
||||
py: Python<'py>,
|
||||
high: PyReadonlyArray1<'py, f64>,
|
||||
low: PyReadonlyArray1<'py, f64>,
|
||||
close: PyReadonlyArray1<'py, f64>,
|
||||
timeperiod: usize,
|
||||
) -> PyResult<Bound<'py, PyArray1<f64>>> {
|
||||
validation::validate_timeperiod(timeperiod, "timeperiod", 1)?;
|
||||
let h = high.as_slice()?;
|
||||
let lo = low.as_slice()?;
|
||||
let c = close.as_slice()?;
|
||||
let n = h.len();
|
||||
validation::validate_equal_length(&[(n, "high"), (lo.len(), "low"), (c.len(), "close")])?;
|
||||
|
||||
// ATR(1) = True Range per bar
|
||||
let mut tr = vec![0.0f64; n];
|
||||
tr[0] = h[0] - lo[0];
|
||||
for i in 1..n {
|
||||
let hl = h[i] - lo[i];
|
||||
let hc = (h[i] - c[i - 1]).abs();
|
||||
let lc = (lo[i] - c[i - 1]).abs();
|
||||
tr[i] = hl.max(hc).max(lc);
|
||||
}
|
||||
|
||||
// Cumulative TR for rolling sum
|
||||
let mut cum_tr = vec![0.0f64; n];
|
||||
cum_tr[0] = tr[0];
|
||||
for i in 1..n {
|
||||
cum_tr[i] = cum_tr[i - 1] + tr[i];
|
||||
}
|
||||
|
||||
let log_n = (timeperiod as f64).log10();
|
||||
let mut result = vec![f64::NAN; n];
|
||||
|
||||
// Rolling max and min using monotonic deques
|
||||
let mut max_dq: VecDeque<usize> = VecDeque::new();
|
||||
let mut min_dq: VecDeque<usize> = VecDeque::new();
|
||||
|
||||
for i in 0..n {
|
||||
while max_dq
|
||||
.front()
|
||||
.map(|&j| j + timeperiod <= i)
|
||||
.unwrap_or(false)
|
||||
{
|
||||
max_dq.pop_front();
|
||||
}
|
||||
while min_dq
|
||||
.front()
|
||||
.map(|&j| j + timeperiod <= i)
|
||||
.unwrap_or(false)
|
||||
{
|
||||
min_dq.pop_front();
|
||||
}
|
||||
while max_dq.back().map(|&j| h[j] <= h[i]).unwrap_or(false) {
|
||||
max_dq.pop_back();
|
||||
}
|
||||
max_dq.push_back(i);
|
||||
while min_dq.back().map(|&j| lo[j] >= lo[i]).unwrap_or(false) {
|
||||
min_dq.pop_back();
|
||||
}
|
||||
min_dq.push_back(i);
|
||||
|
||||
if i + 1 > timeperiod {
|
||||
let prev_cum = if i >= timeperiod {
|
||||
cum_tr[i - timeperiod]
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
let sum_tr = cum_tr[i] - prev_cum;
|
||||
let hh = h[*max_dq.front().unwrap()];
|
||||
let ll = lo[*min_dq.front().unwrap()];
|
||||
let hl_range = hh - ll;
|
||||
if hl_range > 0.0 && log_n > 0.0 {
|
||||
result[i] = 100.0 * (sum_tr / hl_range).log10() / log_n;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(result.into_pyarray(py))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// KELTNER_CHANNELS
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Keltner Channels — EMA ± (multiplier × ATR).
|
||||
///
|
||||
/// Returns (upper, middle, lower) arrays.
|
||||
#[pyfunction]
|
||||
#[pyo3(signature = (high, low, close, timeperiod = 20, atr_period = 10, multiplier = 2.0))]
|
||||
pub fn keltner_channels<'py>(
|
||||
py: Python<'py>,
|
||||
high: PyReadonlyArray1<'py, f64>,
|
||||
low: PyReadonlyArray1<'py, f64>,
|
||||
close: PyReadonlyArray1<'py, f64>,
|
||||
timeperiod: usize,
|
||||
atr_period: usize,
|
||||
multiplier: f64,
|
||||
) -> PyResult<(
|
||||
Bound<'py, PyArray1<f64>>,
|
||||
Bound<'py, PyArray1<f64>>,
|
||||
Bound<'py, PyArray1<f64>>,
|
||||
)> {
|
||||
validation::validate_timeperiod(timeperiod, "timeperiod", 1)?;
|
||||
validation::validate_timeperiod(atr_period, "atr_period", 1)?;
|
||||
let h = high.as_slice()?;
|
||||
let lo = low.as_slice()?;
|
||||
let c = close.as_slice()?;
|
||||
let n = h.len();
|
||||
validation::validate_equal_length(&[(n, "high"), (lo.len(), "low"), (c.len(), "close")])?;
|
||||
|
||||
let middle = compute_ema_ta(c, timeperiod);
|
||||
let atr = compute_atr(h, lo, c, atr_period);
|
||||
|
||||
let mut upper = vec![f64::NAN; n];
|
||||
let mut lower = vec![f64::NAN; n];
|
||||
for i in 0..n {
|
||||
if !middle[i].is_nan() && !atr[i].is_nan() {
|
||||
let band = multiplier * atr[i];
|
||||
upper[i] = middle[i] + band;
|
||||
lower[i] = middle[i] - band;
|
||||
}
|
||||
}
|
||||
|
||||
Ok((
|
||||
upper.into_pyarray(py),
|
||||
middle.into_pyarray(py),
|
||||
lower.into_pyarray(py),
|
||||
))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// HULL_MA
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Hull Moving Average (HMA).
|
||||
///
|
||||
/// Formula: HMA(n) = WMA(2 * WMA(n/2) - WMA(n), sqrt(n))
|
||||
#[pyfunction]
|
||||
#[pyo3(signature = (close, timeperiod = 16))]
|
||||
pub fn hull_ma<'py>(
|
||||
py: Python<'py>,
|
||||
close: PyReadonlyArray1<'py, f64>,
|
||||
timeperiod: usize,
|
||||
) -> PyResult<Bound<'py, PyArray1<f64>>> {
|
||||
validation::validate_timeperiod(timeperiod, "timeperiod", 1)?;
|
||||
let c = close.as_slice()?;
|
||||
let n = c.len();
|
||||
|
||||
let half = (timeperiod / 2).max(1);
|
||||
let sqrt_p = ((timeperiod as f64).sqrt().round() as usize).max(1);
|
||||
|
||||
let wma_full = compute_wma(c, timeperiod);
|
||||
let wma_half = compute_wma(c, half);
|
||||
|
||||
// raw = 2 * wma_half - wma_full
|
||||
let mut raw = vec![f64::NAN; n];
|
||||
for i in 0..n {
|
||||
if !wma_full[i].is_nan() && !wma_half[i].is_nan() {
|
||||
raw[i] = 2.0 * wma_half[i] - wma_full[i];
|
||||
}
|
||||
}
|
||||
|
||||
// Find first valid index in raw
|
||||
let first_valid = raw.iter().position(|x| !x.is_nan()).unwrap_or(n);
|
||||
let mut hull = vec![f64::NAN; n];
|
||||
if first_valid < n {
|
||||
let raw_valid = &raw[first_valid..];
|
||||
let hma_slice = compute_wma(raw_valid, sqrt_p);
|
||||
for (k, &v) in hma_slice.iter().enumerate() {
|
||||
hull[first_valid + k] = v;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(hull.into_pyarray(py))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// CHANDELIER_EXIT
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Chandelier Exit — ATR-based trailing stop levels.
|
||||
///
|
||||
/// Returns (long_exit, short_exit) arrays.
|
||||
#[pyfunction]
|
||||
#[pyo3(signature = (high, low, close, timeperiod = 22, multiplier = 3.0))]
|
||||
pub fn chandelier_exit<'py>(
|
||||
py: Python<'py>,
|
||||
high: PyReadonlyArray1<'py, f64>,
|
||||
low: PyReadonlyArray1<'py, f64>,
|
||||
close: PyReadonlyArray1<'py, f64>,
|
||||
timeperiod: usize,
|
||||
multiplier: f64,
|
||||
) -> PyResult<(Bound<'py, PyArray1<f64>>, Bound<'py, PyArray1<f64>>)> {
|
||||
validation::validate_timeperiod(timeperiod, "timeperiod", 1)?;
|
||||
let h = high.as_slice()?;
|
||||
let lo = low.as_slice()?;
|
||||
let c = close.as_slice()?;
|
||||
let n = h.len();
|
||||
validation::validate_equal_length(&[(n, "high"), (lo.len(), "low"), (c.len(), "close")])?;
|
||||
|
||||
let atr = compute_atr(h, lo, c, timeperiod);
|
||||
|
||||
// Rolling max/min using monotonic deques
|
||||
let mut max_dq: VecDeque<usize> = VecDeque::new();
|
||||
let mut min_dq: VecDeque<usize> = VecDeque::new();
|
||||
let mut highest_high = vec![f64::NAN; n];
|
||||
let mut lowest_low = vec![f64::NAN; n];
|
||||
|
||||
for i in 0..n {
|
||||
while max_dq
|
||||
.front()
|
||||
.map(|&j| j + timeperiod <= i)
|
||||
.unwrap_or(false)
|
||||
{
|
||||
max_dq.pop_front();
|
||||
}
|
||||
while min_dq
|
||||
.front()
|
||||
.map(|&j| j + timeperiod <= i)
|
||||
.unwrap_or(false)
|
||||
{
|
||||
min_dq.pop_front();
|
||||
}
|
||||
while max_dq.back().map(|&j| h[j] <= h[i]).unwrap_or(false) {
|
||||
max_dq.pop_back();
|
||||
}
|
||||
max_dq.push_back(i);
|
||||
while min_dq.back().map(|&j| lo[j] >= lo[i]).unwrap_or(false) {
|
||||
min_dq.pop_back();
|
||||
}
|
||||
min_dq.push_back(i);
|
||||
|
||||
if i + 1 >= timeperiod {
|
||||
highest_high[i] = h[*max_dq.front().unwrap()];
|
||||
lowest_low[i] = lo[*min_dq.front().unwrap()];
|
||||
}
|
||||
}
|
||||
|
||||
let mut long_exit = vec![f64::NAN; n];
|
||||
let mut short_exit = vec![f64::NAN; n];
|
||||
for i in 0..n {
|
||||
if !highest_high[i].is_nan() && !atr[i].is_nan() {
|
||||
long_exit[i] = highest_high[i] - multiplier * atr[i];
|
||||
short_exit[i] = lowest_low[i] + multiplier * atr[i];
|
||||
}
|
||||
}
|
||||
|
||||
Ok((long_exit.into_pyarray(py), short_exit.into_pyarray(py)))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ICHIMOKU
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Ichimoku Cloud (Ichimoku Kinko Hyo).
|
||||
///
|
||||
/// Returns (tenkan, kijun, senkou_a, senkou_b, chikou) arrays.
|
||||
#[pyfunction]
|
||||
#[pyo3(signature = (high, low, close, tenkan_period = 9, kijun_period = 26, senkou_b_period = 52, displacement = 26))]
|
||||
pub fn ichimoku<'py>(
|
||||
py: Python<'py>,
|
||||
high: PyReadonlyArray1<'py, f64>,
|
||||
low: PyReadonlyArray1<'py, f64>,
|
||||
close: PyReadonlyArray1<'py, f64>,
|
||||
tenkan_period: usize,
|
||||
kijun_period: usize,
|
||||
senkou_b_period: usize,
|
||||
displacement: usize,
|
||||
) -> PyResult<(
|
||||
Bound<'py, PyArray1<f64>>,
|
||||
Bound<'py, PyArray1<f64>>,
|
||||
Bound<'py, PyArray1<f64>>,
|
||||
Bound<'py, PyArray1<f64>>,
|
||||
Bound<'py, PyArray1<f64>>,
|
||||
)> {
|
||||
validation::validate_timeperiod(tenkan_period, "tenkan_period", 1)?;
|
||||
validation::validate_timeperiod(kijun_period, "kijun_period", 1)?;
|
||||
validation::validate_timeperiod(senkou_b_period, "senkou_b_period", 1)?;
|
||||
let h = high.as_slice()?;
|
||||
let lo = low.as_slice()?;
|
||||
let c = close.as_slice()?;
|
||||
let n = h.len();
|
||||
validation::validate_equal_length(&[(n, "high"), (lo.len(), "low"), (c.len(), "close")])?;
|
||||
|
||||
// Helper: rolling (H+L)/2 using monotonic deques
|
||||
let midpoint_rolling = |period: usize| -> Vec<f64> {
|
||||
let mut result = vec![f64::NAN; n];
|
||||
let mut max_dq: VecDeque<usize> = VecDeque::new();
|
||||
let mut min_dq: VecDeque<usize> = VecDeque::new();
|
||||
for i in 0..n {
|
||||
while max_dq.front().map(|&j| j + period <= i).unwrap_or(false) {
|
||||
max_dq.pop_front();
|
||||
}
|
||||
while min_dq.front().map(|&j| j + period <= i).unwrap_or(false) {
|
||||
min_dq.pop_front();
|
||||
}
|
||||
while max_dq.back().map(|&j| h[j] <= h[i]).unwrap_or(false) {
|
||||
max_dq.pop_back();
|
||||
}
|
||||
max_dq.push_back(i);
|
||||
while min_dq.back().map(|&j| lo[j] >= lo[i]).unwrap_or(false) {
|
||||
min_dq.pop_back();
|
||||
}
|
||||
min_dq.push_back(i);
|
||||
if i + 1 >= period {
|
||||
result[i] = (h[*max_dq.front().unwrap()] + lo[*min_dq.front().unwrap()]) / 2.0;
|
||||
}
|
||||
}
|
||||
result
|
||||
};
|
||||
|
||||
let tenkan = midpoint_rolling(tenkan_period);
|
||||
let kijun = midpoint_rolling(kijun_period);
|
||||
let raw_b = midpoint_rolling(senkou_b_period);
|
||||
|
||||
// Senkou A: (tenkan + kijun) / 2 shifted back `displacement` bars
|
||||
let mut senkou_a = vec![f64::NAN; n];
|
||||
if n > displacement {
|
||||
for i in displacement..n {
|
||||
if !tenkan[i].is_nan() && !kijun[i].is_nan() {
|
||||
senkou_a[i - displacement] = (tenkan[i] + kijun[i]) / 2.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Senkou B: raw_b shifted back `displacement` bars
|
||||
let mut senkou_b = vec![f64::NAN; n];
|
||||
if n > displacement {
|
||||
senkou_b[..n - displacement].copy_from_slice(&raw_b[displacement..]);
|
||||
}
|
||||
|
||||
// Chikou: close shifted forward `displacement` bars
|
||||
let mut chikou = vec![f64::NAN; n];
|
||||
if n > displacement {
|
||||
chikou[displacement..].copy_from_slice(&c[..n - displacement]);
|
||||
}
|
||||
|
||||
Ok((
|
||||
tenkan.into_pyarray(py),
|
||||
kijun.into_pyarray(py),
|
||||
senkou_a.into_pyarray(py),
|
||||
senkou_b.into_pyarray(py),
|
||||
chikou.into_pyarray(py),
|
||||
))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// PIVOT_POINTS
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Pivot Points — support / resistance levels computed from previous bar.
|
||||
///
|
||||
/// method: "classic" | "fibonacci" | "camarilla"
|
||||
/// Returns (pivot, r1, s1, r2, s2) arrays.
|
||||
#[pyfunction]
|
||||
#[pyo3(signature = (high, low, close, method = "classic"))]
|
||||
pub fn pivot_points<'py>(
|
||||
py: Python<'py>,
|
||||
high: PyReadonlyArray1<'py, f64>,
|
||||
low: PyReadonlyArray1<'py, f64>,
|
||||
close: PyReadonlyArray1<'py, f64>,
|
||||
method: &str,
|
||||
) -> PyResult<(
|
||||
Bound<'py, PyArray1<f64>>,
|
||||
Bound<'py, PyArray1<f64>>,
|
||||
Bound<'py, PyArray1<f64>>,
|
||||
Bound<'py, PyArray1<f64>>,
|
||||
Bound<'py, PyArray1<f64>>,
|
||||
)> {
|
||||
let h = high.as_slice()?;
|
||||
let lo = low.as_slice()?;
|
||||
let c = close.as_slice()?;
|
||||
let n = h.len();
|
||||
validation::validate_equal_length(&[(n, "high"), (lo.len(), "low"), (c.len(), "close")])?;
|
||||
|
||||
let mut pivot = vec![f64::NAN; n];
|
||||
let mut r1 = vec![f64::NAN; n];
|
||||
let mut s1 = vec![f64::NAN; n];
|
||||
let mut r2 = vec![f64::NAN; n];
|
||||
let mut s2 = vec![f64::NAN; n];
|
||||
|
||||
let method_lower = method.to_lowercase();
|
||||
for i in 1..n {
|
||||
let ph = h[i - 1];
|
||||
let pl = lo[i - 1];
|
||||
let pc = c[i - 1];
|
||||
let hl = ph - pl;
|
||||
let p = (ph + pl + pc) / 3.0;
|
||||
pivot[i] = p;
|
||||
match method_lower.as_str() {
|
||||
"classic" => {
|
||||
r1[i] = 2.0 * p - pl;
|
||||
s1[i] = 2.0 * p - ph;
|
||||
r2[i] = p + hl;
|
||||
s2[i] = p - hl;
|
||||
}
|
||||
"fibonacci" => {
|
||||
r1[i] = p + 0.382 * hl;
|
||||
s1[i] = p - 0.382 * hl;
|
||||
r2[i] = p + 0.618 * hl;
|
||||
s2[i] = p - 0.618 * hl;
|
||||
}
|
||||
"camarilla" => {
|
||||
r1[i] = pc + 1.1 * hl / 12.0;
|
||||
s1[i] = pc - 1.1 * hl / 12.0;
|
||||
r2[i] = pc + 1.1 * hl / 6.0;
|
||||
s2[i] = pc - 1.1 * hl / 6.0;
|
||||
}
|
||||
_ => {
|
||||
return Err(PyValueError::new_err(format!(
|
||||
"Unknown pivot method '{}'. Use 'classic', 'fibonacci', or 'camarilla'.",
|
||||
method
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok((
|
||||
pivot.into_pyarray(py),
|
||||
r1.into_pyarray(py),
|
||||
s1.into_pyarray(py),
|
||||
r2.into_pyarray(py),
|
||||
s2.into_pyarray(py),
|
||||
))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// register
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
pub fn register(m: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||
m.add_function(pyo3::wrap_pyfunction!(vwap, m)?)?;
|
||||
m.add_function(pyo3::wrap_pyfunction!(vwma, m)?)?;
|
||||
m.add_function(pyo3::wrap_pyfunction!(supertrend, m)?)?;
|
||||
m.add_function(pyo3::wrap_pyfunction!(donchian, m)?)?;
|
||||
m.add_function(pyo3::wrap_pyfunction!(choppiness_index, m)?)?;
|
||||
m.add_function(pyo3::wrap_pyfunction!(keltner_channels, m)?)?;
|
||||
m.add_function(pyo3::wrap_pyfunction!(hull_ma, m)?)?;
|
||||
m.add_function(pyo3::wrap_pyfunction!(chandelier_exit, m)?)?;
|
||||
m.add_function(pyo3::wrap_pyfunction!(ichimoku, m)?)?;
|
||||
m.add_function(pyo3::wrap_pyfunction!(pivot_points, m)?)?;
|
||||
Ok(())
|
||||
}
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
pub mod aggregation;
|
||||
pub mod alerts;
|
||||
pub mod attribution;
|
||||
pub mod batch;
|
||||
pub mod chunked;
|
||||
pub mod crypto;
|
||||
pub mod cycle;
|
||||
pub mod extended;
|
||||
pub mod math_ops;
|
||||
pub mod momentum;
|
||||
pub mod overlap;
|
||||
pub mod pattern;
|
||||
pub mod portfolio;
|
||||
pub mod price_transform;
|
||||
pub mod regime;
|
||||
pub mod resampling;
|
||||
pub mod signals;
|
||||
pub mod statistic;
|
||||
pub mod streaming;
|
||||
pub mod validation;
|
||||
pub mod volatility;
|
||||
pub mod volume;
|
||||
|
||||
use pyo3::prelude::*;
|
||||
|
||||
/// ferro_ta — A fast Technical Analysis library powered by Rust.
|
||||
///
|
||||
/// Indicators are organized into modules matching the TA-Lib category structure:
|
||||
/// - **overlap** : Overlap Studies (SMA, EMA, WMA, DEMA, TEMA, TRIMA, KAMA, T3, MACD, BBANDS, SAR, MA, MAVP, MAMA, SAREXT, MACDEXT, …)
|
||||
/// - **momentum** : Momentum Indicators (RSI, STOCH, ADX, CCI, WILLR, AROON, MFI, …)
|
||||
/// - **volume** : Volume Indicators (AD, ADOSC, OBV)
|
||||
/// - **volatility** : Volatility Indicators (ATR, NATR, TRANGE)
|
||||
/// - **statistic** : Statistic Functions (STDDEV, VAR, LINEARREG, BETA, CORREL, …)
|
||||
/// - **price_transform**: Price Transformations (AVGPRICE, MEDPRICE, TYPPRICE, WCLPRICE)
|
||||
/// - **pattern** : Pattern Recognition (CDLDOJI, CDLENGULFING, CDLHAMMER, …)
|
||||
/// - **cycle** : Cycle Indicators (HT_TRENDLINE, HT_DCPERIOD, HT_DCPHASE, HT_PHASOR, HT_SINE, HT_TRENDMODE)
|
||||
/// - **batch** : Batch Execution (batch_sma, batch_ema, batch_rsi — 2-D array input)
|
||||
/// - **streaming** : Streaming Indicators (StreamingSMA, StreamingEMA, … — bar-by-bar PyO3 classes)
|
||||
/// - **extended** : Extended Indicators (VWAP, SUPERTREND, DONCHIAN, ICHIMOKU, …)
|
||||
/// - **math_ops** : Rolling Math Operators (rolling_sum, rolling_max, rolling_min, …)
|
||||
/// - **resampling** : OHLCV resampling helpers (volume_bars, ohlcv_agg)
|
||||
/// - **aggregation** : Tick/trade aggregation pipeline (aggregate_tick_bars, aggregate_volume_bars_ticks, aggregate_time_bars)
|
||||
/// - **portfolio** : Portfolio analytics (portfolio_volatility, beta_full, rolling_beta, drawdown_series, correlation_matrix, relative_strength, spread, zscore_series, compose_weighted)
|
||||
/// - **signals** : Signal helpers (rank_series, top_n_indices, bottom_n_indices)
|
||||
#[pymodule]
|
||||
fn _ferro_ta(m: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||
pyo3_log::init();
|
||||
overlap::register(m)?;
|
||||
momentum::register(m)?;
|
||||
volume::register(m)?;
|
||||
volatility::register(m)?;
|
||||
statistic::register(m)?;
|
||||
price_transform::register(m)?;
|
||||
pattern::register(m)?;
|
||||
cycle::register(m)?;
|
||||
batch::register(m)?;
|
||||
streaming::register(m)?;
|
||||
extended::register(m)?;
|
||||
math_ops::register(m)?;
|
||||
resampling::register(m)?;
|
||||
aggregation::register(m)?;
|
||||
portfolio::register(m)?;
|
||||
signals::register(m)?;
|
||||
alerts::register(m)?;
|
||||
crypto::register(m)?;
|
||||
chunked::register(m)?;
|
||||
regime::register(m)?;
|
||||
attribution::register(m)?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
//! Rust rolling math operators — O(n) sliding window using monotonic deques.
|
||||
//!
|
||||
//! Functions exposed to Python:
|
||||
//! rolling_sum — Rolling sum over `timeperiod` bars
|
||||
//! rolling_max — Rolling maximum (O(n) via monotonic deque)
|
||||
//! rolling_min — Rolling minimum (O(n) via monotonic deque)
|
||||
//! rolling_maxindex — Index of rolling maximum
|
||||
//! rolling_minindex — Index of rolling minimum
|
||||
|
||||
use std::collections::VecDeque;
|
||||
|
||||
use crate::validation;
|
||||
use numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};
|
||||
use pyo3::prelude::*;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// rolling_sum
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Rolling sum over `timeperiod` bars.
|
||||
///
|
||||
/// Uses a prefix-sum array for O(n) computation.
|
||||
/// Leading `timeperiod - 1` values are NaN.
|
||||
#[pyfunction]
|
||||
#[pyo3(signature = (real, timeperiod = 30))]
|
||||
pub fn rolling_sum<'py>(
|
||||
py: Python<'py>,
|
||||
real: PyReadonlyArray1<'py, f64>,
|
||||
timeperiod: usize,
|
||||
) -> PyResult<Bound<'py, PyArray1<f64>>> {
|
||||
validation::validate_timeperiod(timeperiod, "timeperiod", 1)?;
|
||||
let prices = real.as_slice()?;
|
||||
let n = prices.len();
|
||||
let mut result = vec![f64::NAN; n];
|
||||
if n < timeperiod {
|
||||
return Ok(result.into_pyarray(py));
|
||||
}
|
||||
// Prefix sum
|
||||
let mut cs = vec![0.0f64; n + 1];
|
||||
for i in 0..n {
|
||||
cs[i + 1] = cs[i] + prices[i];
|
||||
}
|
||||
for i in (timeperiod - 1)..n {
|
||||
result[i] = cs[i + 1] - cs[i + 1 - timeperiod];
|
||||
}
|
||||
Ok(result.into_pyarray(py))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// rolling_max
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Rolling maximum over `timeperiod` bars (O(n) monotonic deque).
|
||||
///
|
||||
/// Leading `timeperiod - 1` values are NaN.
|
||||
#[pyfunction]
|
||||
#[pyo3(signature = (real, timeperiod = 30))]
|
||||
pub fn rolling_max<'py>(
|
||||
py: Python<'py>,
|
||||
real: PyReadonlyArray1<'py, f64>,
|
||||
timeperiod: usize,
|
||||
) -> PyResult<Bound<'py, PyArray1<f64>>> {
|
||||
validation::validate_timeperiod(timeperiod, "timeperiod", 1)?;
|
||||
let prices = real.as_slice()?;
|
||||
let n = prices.len();
|
||||
let mut result = vec![f64::NAN; n];
|
||||
let mut dq: VecDeque<usize> = VecDeque::new();
|
||||
|
||||
for i in 0..n {
|
||||
// Remove indices out of the window
|
||||
while dq.front().map(|&j| j + timeperiod <= i).unwrap_or(false) {
|
||||
dq.pop_front();
|
||||
}
|
||||
// Maintain decreasing deque
|
||||
while dq.back().map(|&j| prices[j] <= prices[i]).unwrap_or(false) {
|
||||
dq.pop_back();
|
||||
}
|
||||
dq.push_back(i);
|
||||
if i + 1 >= timeperiod {
|
||||
result[i] = prices[*dq.front().unwrap()];
|
||||
}
|
||||
}
|
||||
Ok(result.into_pyarray(py))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// rolling_min
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Rolling minimum over `timeperiod` bars (O(n) monotonic deque).
|
||||
///
|
||||
/// Leading `timeperiod - 1` values are NaN.
|
||||
#[pyfunction]
|
||||
#[pyo3(signature = (real, timeperiod = 30))]
|
||||
pub fn rolling_min<'py>(
|
||||
py: Python<'py>,
|
||||
real: PyReadonlyArray1<'py, f64>,
|
||||
timeperiod: usize,
|
||||
) -> PyResult<Bound<'py, PyArray1<f64>>> {
|
||||
validation::validate_timeperiod(timeperiod, "timeperiod", 1)?;
|
||||
let prices = real.as_slice()?;
|
||||
let n = prices.len();
|
||||
let mut result = vec![f64::NAN; n];
|
||||
let mut dq: VecDeque<usize> = VecDeque::new();
|
||||
|
||||
for i in 0..n {
|
||||
while dq.front().map(|&j| j + timeperiod <= i).unwrap_or(false) {
|
||||
dq.pop_front();
|
||||
}
|
||||
// Maintain increasing deque
|
||||
while dq.back().map(|&j| prices[j] >= prices[i]).unwrap_or(false) {
|
||||
dq.pop_back();
|
||||
}
|
||||
dq.push_back(i);
|
||||
if i + 1 >= timeperiod {
|
||||
result[i] = prices[*dq.front().unwrap()];
|
||||
}
|
||||
}
|
||||
Ok(result.into_pyarray(py))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// rolling_maxindex
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Index of rolling maximum over `timeperiod` bars (O(n) monotonic deque).
|
||||
///
|
||||
/// Returns the 0-based index into the input array. During the warmup window
|
||||
/// the value is `-1` (not valid — mask with warmup period if needed).
|
||||
#[pyfunction]
|
||||
#[pyo3(signature = (real, timeperiod = 30))]
|
||||
pub fn rolling_maxindex<'py>(
|
||||
py: Python<'py>,
|
||||
real: PyReadonlyArray1<'py, f64>,
|
||||
timeperiod: usize,
|
||||
) -> PyResult<Bound<'py, PyArray1<i64>>> {
|
||||
validation::validate_timeperiod(timeperiod, "timeperiod", 1)?;
|
||||
let prices = real.as_slice()?;
|
||||
let n = prices.len();
|
||||
let mut result = vec![-1i64; n];
|
||||
let mut dq: VecDeque<usize> = VecDeque::new();
|
||||
|
||||
for i in 0..n {
|
||||
while dq.front().map(|&j| j + timeperiod <= i).unwrap_or(false) {
|
||||
dq.pop_front();
|
||||
}
|
||||
while dq.back().map(|&j| prices[j] <= prices[i]).unwrap_or(false) {
|
||||
dq.pop_back();
|
||||
}
|
||||
dq.push_back(i);
|
||||
if i + 1 >= timeperiod {
|
||||
result[i] = *dq.front().unwrap() as i64;
|
||||
}
|
||||
}
|
||||
Ok(result.into_pyarray(py))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// rolling_minindex
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Index of rolling minimum over `timeperiod` bars (O(n) monotonic deque).
|
||||
///
|
||||
/// Returns the 0-based index into the input array. During the warmup window
|
||||
/// the value is `-1` (not valid — mask with warmup period if needed).
|
||||
#[pyfunction]
|
||||
#[pyo3(signature = (real, timeperiod = 30))]
|
||||
pub fn rolling_minindex<'py>(
|
||||
py: Python<'py>,
|
||||
real: PyReadonlyArray1<'py, f64>,
|
||||
timeperiod: usize,
|
||||
) -> PyResult<Bound<'py, PyArray1<i64>>> {
|
||||
validation::validate_timeperiod(timeperiod, "timeperiod", 1)?;
|
||||
let prices = real.as_slice()?;
|
||||
let n = prices.len();
|
||||
let mut result = vec![-1i64; n];
|
||||
let mut dq: VecDeque<usize> = VecDeque::new();
|
||||
|
||||
for i in 0..n {
|
||||
while dq.front().map(|&j| j + timeperiod <= i).unwrap_or(false) {
|
||||
dq.pop_front();
|
||||
}
|
||||
while dq.back().map(|&j| prices[j] >= prices[i]).unwrap_or(false) {
|
||||
dq.pop_back();
|
||||
}
|
||||
dq.push_back(i);
|
||||
if i + 1 >= timeperiod {
|
||||
result[i] = *dq.front().unwrap() as i64;
|
||||
}
|
||||
}
|
||||
Ok(result.into_pyarray(py))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// register
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
pub fn register(m: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||
m.add_function(pyo3::wrap_pyfunction!(rolling_sum, m)?)?;
|
||||
m.add_function(pyo3::wrap_pyfunction!(rolling_max, m)?)?;
|
||||
m.add_function(pyo3::wrap_pyfunction!(rolling_min, m)?)?;
|
||||
m.add_function(pyo3::wrap_pyfunction!(rolling_maxindex, m)?)?;
|
||||
m.add_function(pyo3::wrap_pyfunction!(rolling_minindex, m)?)?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
//! ADX family: PLUS_DM, MINUS_DM, +DI, -DI, DX, ADX, ADXR.
|
||||
//! Thin wrappers that delegate to ferro_ta_core::momentum.
|
||||
|
||||
use crate::validation;
|
||||
use numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};
|
||||
use pyo3::prelude::*;
|
||||
|
||||
/// Plus Directional Movement (Wilder smoothing).
|
||||
#[pyfunction]
|
||||
#[pyo3(signature = (high, low, timeperiod = 14))]
|
||||
pub fn plus_dm<'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()?;
|
||||
validation::validate_equal_length(&[(highs.len(), "high"), (lows.len(), "low")])?;
|
||||
let result = ferro_ta_core::momentum::plus_dm(highs, lows, timeperiod);
|
||||
Ok(result.into_pyarray(py))
|
||||
}
|
||||
|
||||
/// Minus Directional Movement (Wilder smoothing).
|
||||
#[pyfunction]
|
||||
#[pyo3(signature = (high, low, timeperiod = 14))]
|
||||
pub fn minus_dm<'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()?;
|
||||
validation::validate_equal_length(&[(highs.len(), "high"), (lows.len(), "low")])?;
|
||||
let result = ferro_ta_core::momentum::minus_dm(highs, lows, timeperiod);
|
||||
Ok(result.into_pyarray(py))
|
||||
}
|
||||
|
||||
/// Plus Directional Indicator (Wilder smoothing).
|
||||
#[pyfunction]
|
||||
#[pyo3(signature = (high, low, close, timeperiod = 14))]
|
||||
pub fn plus_di<'py>(
|
||||
py: Python<'py>,
|
||||
high: PyReadonlyArray1<'py, f64>,
|
||||
low: PyReadonlyArray1<'py, f64>,
|
||||
close: 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 closes = close.as_slice()?;
|
||||
let n = highs.len();
|
||||
validation::validate_equal_length(&[
|
||||
(n, "high"),
|
||||
(lows.len(), "low"),
|
||||
(closes.len(), "close"),
|
||||
])?;
|
||||
let result = ferro_ta_core::momentum::plus_di(highs, lows, closes, timeperiod);
|
||||
Ok(result.into_pyarray(py))
|
||||
}
|
||||
|
||||
/// Minus Directional Indicator (Wilder smoothing).
|
||||
#[pyfunction]
|
||||
#[pyo3(signature = (high, low, close, timeperiod = 14))]
|
||||
pub fn minus_di<'py>(
|
||||
py: Python<'py>,
|
||||
high: PyReadonlyArray1<'py, f64>,
|
||||
low: PyReadonlyArray1<'py, f64>,
|
||||
close: 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 closes = close.as_slice()?;
|
||||
let n = highs.len();
|
||||
validation::validate_equal_length(&[
|
||||
(n, "high"),
|
||||
(lows.len(), "low"),
|
||||
(closes.len(), "close"),
|
||||
])?;
|
||||
let result = ferro_ta_core::momentum::minus_di(highs, lows, closes, timeperiod);
|
||||
Ok(result.into_pyarray(py))
|
||||
}
|
||||
|
||||
/// Directional Movement Index: 100 * |+DI - -DI| / (+DI + -DI).
|
||||
#[pyfunction]
|
||||
#[pyo3(signature = (high, low, close, timeperiod = 14))]
|
||||
pub fn dx<'py>(
|
||||
py: Python<'py>,
|
||||
high: PyReadonlyArray1<'py, f64>,
|
||||
low: PyReadonlyArray1<'py, f64>,
|
||||
close: 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 closes = close.as_slice()?;
|
||||
let n = highs.len();
|
||||
validation::validate_equal_length(&[
|
||||
(n, "high"),
|
||||
(lows.len(), "low"),
|
||||
(closes.len(), "close"),
|
||||
])?;
|
||||
let result = ferro_ta_core::momentum::dx(highs, lows, closes, timeperiod);
|
||||
Ok(result.into_pyarray(py))
|
||||
}
|
||||
|
||||
/// Average Directional Movement Index (Wilder smoothing of DX).
|
||||
#[pyfunction]
|
||||
#[pyo3(signature = (high, low, close, timeperiod = 14))]
|
||||
pub fn adx<'py>(
|
||||
py: Python<'py>,
|
||||
high: PyReadonlyArray1<'py, f64>,
|
||||
low: PyReadonlyArray1<'py, f64>,
|
||||
close: 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 closes = close.as_slice()?;
|
||||
let n = highs.len();
|
||||
validation::validate_equal_length(&[
|
||||
(n, "high"),
|
||||
(lows.len(), "low"),
|
||||
(closes.len(), "close"),
|
||||
])?;
|
||||
let result = ferro_ta_core::momentum::adx(highs, lows, closes, timeperiod);
|
||||
Ok(result.into_pyarray(py))
|
||||
}
|
||||
|
||||
/// ADX Rating: (ADX[i] + ADX[i - timeperiod]) / 2.
|
||||
#[pyfunction]
|
||||
#[pyo3(signature = (high, low, close, timeperiod = 14))]
|
||||
pub fn adxr<'py>(
|
||||
py: Python<'py>,
|
||||
high: PyReadonlyArray1<'py, f64>,
|
||||
low: PyReadonlyArray1<'py, f64>,
|
||||
close: 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 closes = close.as_slice()?;
|
||||
let n = highs.len();
|
||||
validation::validate_equal_length(&[
|
||||
(n, "high"),
|
||||
(lows.len(), "low"),
|
||||
(closes.len(), "close"),
|
||||
])?;
|
||||
let result = ferro_ta_core::momentum::adxr(highs, lows, closes, timeperiod);
|
||||
Ok(result.into_pyarray(py))
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/// Absolute Price Oscillator: fast EMA - slow EMA.
|
||||
#[pyfunction]
|
||||
#[pyo3(signature = (close, fastperiod = 12, slowperiod = 26))]
|
||||
pub fn apo<'py>(
|
||||
py: Python<'py>,
|
||||
close: PyReadonlyArray1<'py, f64>,
|
||||
fastperiod: usize,
|
||||
slowperiod: usize,
|
||||
) -> PyResult<Bound<'py, PyArray1<f64>>> {
|
||||
validation::validate_timeperiod(fastperiod, "fastperiod", 1)?;
|
||||
validation::validate_timeperiod(slowperiod, "slowperiod", 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 mut fast_ema = ExponentialMovingAverage::new(fastperiod)
|
||||
.map_err(|e| PyValueError::new_err(e.to_string()))?;
|
||||
let mut slow_ema = ExponentialMovingAverage::new(slowperiod)
|
||||
.map_err(|e| PyValueError::new_err(e.to_string()))?;
|
||||
let warmup = slowperiod - 1;
|
||||
let mut result = vec![f64::NAN; n];
|
||||
for (i, &price) in prices.iter().enumerate() {
|
||||
let fast = fast_ema.next(price);
|
||||
let slow = slow_ema.next(price);
|
||||
if i >= warmup {
|
||||
result[i] = fast - slow;
|
||||
}
|
||||
}
|
||||
Ok(result.into_pyarray(py))
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
use crate::validation;
|
||||
use numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};
|
||||
use pyo3::prelude::*;
|
||||
|
||||
/// Aroon. Returns (aroon_down, aroon_up) tuple. Leading timeperiod values are NaN.
|
||||
#[pyfunction]
|
||||
#[pyo3(signature = (high, low, timeperiod = 14))]
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub fn aroon<'py>(
|
||||
py: Python<'py>,
|
||||
high: PyReadonlyArray1<'py, f64>,
|
||||
low: PyReadonlyArray1<'py, f64>,
|
||||
timeperiod: usize,
|
||||
) -> PyResult<(Bound<'py, PyArray1<f64>>, 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 aroon_down = vec![f64::NAN; n];
|
||||
let mut aroon_up = vec![f64::NAN; n];
|
||||
let period_f = timeperiod as f64;
|
||||
|
||||
for i in timeperiod..n {
|
||||
let window_size = timeperiod + 1;
|
||||
let start = i + 1 - window_size;
|
||||
let mut max_val = highs[start];
|
||||
let mut min_val = lows[start];
|
||||
let mut max_idx = 0usize;
|
||||
let mut min_idx = 0usize;
|
||||
for j in 0..window_size {
|
||||
if highs[start + j] >= max_val {
|
||||
max_val = highs[start + j];
|
||||
max_idx = j;
|
||||
}
|
||||
if lows[start + j] <= min_val {
|
||||
min_val = lows[start + j];
|
||||
min_idx = j;
|
||||
}
|
||||
}
|
||||
aroon_up[i] = 100.0 * (max_idx as f64) / period_f;
|
||||
aroon_down[i] = 100.0 * (min_idx as f64) / period_f;
|
||||
}
|
||||
Ok((aroon_down.into_pyarray(py), aroon_up.into_pyarray(py)))
|
||||
}
|
||||
|
||||
/// Aroon Oscillator: aroon_up - aroon_down. Leading timeperiod values are NaN.
|
||||
#[pyfunction]
|
||||
#[pyo3(signature = (high, low, timeperiod = 14))]
|
||||
pub fn aroonosc<'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 result = vec![f64::NAN; n];
|
||||
let period_f = timeperiod as f64;
|
||||
|
||||
#[allow(clippy::needless_range_loop)]
|
||||
for i in timeperiod..n {
|
||||
let window_size = timeperiod + 1;
|
||||
let start = i + 1 - window_size;
|
||||
let mut max_val = highs[start];
|
||||
let mut min_val = lows[start];
|
||||
let mut max_idx = 0usize;
|
||||
let mut min_idx = 0usize;
|
||||
for j in 0..window_size {
|
||||
if highs[start + j] >= max_val {
|
||||
max_val = highs[start + j];
|
||||
max_idx = j;
|
||||
}
|
||||
if lows[start + j] <= min_val {
|
||||
min_val = lows[start + j];
|
||||
min_idx = j;
|
||||
}
|
||||
}
|
||||
let up = 100.0 * (max_idx as f64) / period_f;
|
||||
let down = 100.0 * (min_idx as f64) / period_f;
|
||||
result[i] = up - down;
|
||||
}
|
||||
Ok(result.into_pyarray(py))
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
use crate::validation;
|
||||
use numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};
|
||||
use pyo3::prelude::*;
|
||||
|
||||
/// Balance Of Power: (close - open) / (high - low). Zero when range is zero.
|
||||
#[pyfunction]
|
||||
pub fn bop<'py>(
|
||||
py: Python<'py>,
|
||||
open: PyReadonlyArray1<'py, f64>,
|
||||
high: PyReadonlyArray1<'py, f64>,
|
||||
low: PyReadonlyArray1<'py, f64>,
|
||||
close: PyReadonlyArray1<'py, f64>,
|
||||
) -> PyResult<Bound<'py, PyArray1<f64>>> {
|
||||
let opens = open.as_slice()?;
|
||||
let highs = high.as_slice()?;
|
||||
let lows = low.as_slice()?;
|
||||
let closes = close.as_slice()?;
|
||||
let n = opens.len();
|
||||
validation::validate_equal_length(&[
|
||||
(n, "open"),
|
||||
(highs.len(), "high"),
|
||||
(lows.len(), "low"),
|
||||
(closes.len(), "close"),
|
||||
])?;
|
||||
let mut result = vec![f64::NAN; n];
|
||||
for i in 0..n {
|
||||
let range = highs[i] - lows[i];
|
||||
if range != 0.0 {
|
||||
result[i] = (closes[i] - opens[i]) / range;
|
||||
} else {
|
||||
result[i] = 0.0;
|
||||
}
|
||||
}
|
||||
Ok(result.into_pyarray(py))
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
use crate::validation;
|
||||
use numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};
|
||||
use pyo3::prelude::*;
|
||||
|
||||
/// Commodity Channel Index (TA-Lib–compatible): (typical_price - SMA) / (0.015 * MAD).
|
||||
#[pyfunction]
|
||||
#[pyo3(signature = (high, low, close, timeperiod = 14))]
|
||||
pub fn cci<'py>(
|
||||
py: Python<'py>,
|
||||
high: PyReadonlyArray1<'py, f64>,
|
||||
low: PyReadonlyArray1<'py, f64>,
|
||||
close: 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 closes = close.as_slice()?;
|
||||
let n = highs.len();
|
||||
validation::validate_equal_length(&[
|
||||
(n, "high"),
|
||||
(lows.len(), "low"),
|
||||
(closes.len(), "close"),
|
||||
])?;
|
||||
let tp: Vec<f64> = highs
|
||||
.iter()
|
||||
.zip(lows.iter())
|
||||
.zip(closes.iter())
|
||||
.map(|((&h, &l), &c)| (h + l + c) / 3.0)
|
||||
.collect();
|
||||
let mut result = vec![f64::NAN; n];
|
||||
for i in (timeperiod - 1)..n {
|
||||
let window = &tp[(i + 1 - timeperiod)..=i];
|
||||
let mean: f64 = window.iter().sum::<f64>() / timeperiod as f64;
|
||||
let mad: f64 = window.iter().map(|&x| (x - mean).abs()).sum::<f64>() / timeperiod as f64;
|
||||
result[i] = if mad != 0.0 {
|
||||
(tp[i] - mean) / (0.015 * mad)
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
}
|
||||
Ok(result.into_pyarray(py))
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
use crate::validation;
|
||||
use numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};
|
||||
use pyo3::prelude::*;
|
||||
|
||||
/// Chande Momentum Oscillator: 100 * (sum of gains - sum of losses) / (sum of gains + sum of losses) over window.
|
||||
#[pyfunction]
|
||||
#[pyo3(signature = (close, timeperiod = 14))]
|
||||
pub fn cmo<'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 result = vec![f64::NAN; n];
|
||||
|
||||
if n < timeperiod + 1 {
|
||||
return Ok(result.into_pyarray(py));
|
||||
}
|
||||
|
||||
let changes: Vec<f64> = prices.windows(2).map(|w| w[1] - w[0]).collect();
|
||||
|
||||
#[allow(clippy::needless_range_loop)]
|
||||
for i in timeperiod..n {
|
||||
let mut ups = 0.0_f64;
|
||||
let mut downs = 0.0_f64;
|
||||
for ch in &changes[(i - timeperiod)..i] {
|
||||
if *ch > 0.0 {
|
||||
ups += ch;
|
||||
} else {
|
||||
downs -= ch;
|
||||
}
|
||||
}
|
||||
let denom = ups + downs;
|
||||
result[i] = if denom != 0.0 {
|
||||
100.0 * (ups - downs) / denom
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
}
|
||||
Ok(result.into_pyarray(py))
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
use crate::validation;
|
||||
use numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};
|
||||
use pyo3::prelude::*;
|
||||
|
||||
/// Money Flow Index: volume-weighted RSI (typical price * volume). Leading timeperiod values are NaN.
|
||||
#[pyfunction]
|
||||
#[pyo3(signature = (high, low, close, volume, timeperiod = 14))]
|
||||
pub fn mfi<'py>(
|
||||
py: Python<'py>,
|
||||
high: PyReadonlyArray1<'py, f64>,
|
||||
low: PyReadonlyArray1<'py, f64>,
|
||||
close: PyReadonlyArray1<'py, f64>,
|
||||
volume: 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 closes = close.as_slice()?;
|
||||
let vols = volume.as_slice()?;
|
||||
let n = highs.len();
|
||||
validation::validate_equal_length(&[
|
||||
(n, "high"),
|
||||
(lows.len(), "low"),
|
||||
(closes.len(), "close"),
|
||||
(vols.len(), "volume"),
|
||||
])?;
|
||||
log::debug!("MFI: timeperiod={timeperiod}, n={n}");
|
||||
let result = ferro_ta_core::volume::mfi(highs, lows, closes, vols, timeperiod);
|
||||
Ok(result.into_pyarray(py))
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
//! Momentum indicators — RSI, stochastics, ADX, CCI, etc.
|
||||
//! Each indicator (or small group) lives in its own file for maintainability.
|
||||
|
||||
mod adx;
|
||||
mod apo;
|
||||
mod aroon;
|
||||
mod bop;
|
||||
mod cci;
|
||||
mod cmo;
|
||||
mod mfi;
|
||||
mod mom;
|
||||
mod ppo;
|
||||
mod roc;
|
||||
mod rsi;
|
||||
mod stoch;
|
||||
mod stochf;
|
||||
mod stochrsi;
|
||||
mod trix;
|
||||
mod ultosc;
|
||||
mod willr;
|
||||
|
||||
use pyo3::prelude::*;
|
||||
|
||||
pub fn register(m: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||
m.add_function(pyo3::wrap_pyfunction!(self::rsi::rsi, m)?)?;
|
||||
m.add_function(pyo3::wrap_pyfunction!(self::mom::mom, m)?)?;
|
||||
m.add_function(pyo3::wrap_pyfunction!(self::roc::roc, m)?)?;
|
||||
m.add_function(pyo3::wrap_pyfunction!(self::roc::rocp, m)?)?;
|
||||
m.add_function(pyo3::wrap_pyfunction!(self::roc::rocr, m)?)?;
|
||||
m.add_function(pyo3::wrap_pyfunction!(self::roc::rocr100, m)?)?;
|
||||
m.add_function(pyo3::wrap_pyfunction!(self::willr::willr, m)?)?;
|
||||
m.add_function(pyo3::wrap_pyfunction!(self::aroon::aroon, m)?)?;
|
||||
m.add_function(pyo3::wrap_pyfunction!(self::aroon::aroonosc, m)?)?;
|
||||
m.add_function(pyo3::wrap_pyfunction!(self::cci::cci, m)?)?;
|
||||
m.add_function(pyo3::wrap_pyfunction!(self::mfi::mfi, m)?)?;
|
||||
m.add_function(pyo3::wrap_pyfunction!(self::bop::bop, m)?)?;
|
||||
m.add_function(pyo3::wrap_pyfunction!(self::stochf::stochf, m)?)?;
|
||||
m.add_function(pyo3::wrap_pyfunction!(self::stoch::stoch, m)?)?;
|
||||
m.add_function(pyo3::wrap_pyfunction!(self::stochrsi::stochrsi, m)?)?;
|
||||
m.add_function(pyo3::wrap_pyfunction!(self::apo::apo, m)?)?;
|
||||
m.add_function(pyo3::wrap_pyfunction!(self::ppo::ppo, m)?)?;
|
||||
m.add_function(pyo3::wrap_pyfunction!(self::cmo::cmo, m)?)?;
|
||||
m.add_function(pyo3::wrap_pyfunction!(self::adx::plus_dm, m)?)?;
|
||||
m.add_function(pyo3::wrap_pyfunction!(self::adx::minus_dm, m)?)?;
|
||||
m.add_function(pyo3::wrap_pyfunction!(self::adx::plus_di, m)?)?;
|
||||
m.add_function(pyo3::wrap_pyfunction!(self::adx::minus_di, m)?)?;
|
||||
m.add_function(pyo3::wrap_pyfunction!(self::adx::dx, m)?)?;
|
||||
m.add_function(pyo3::wrap_pyfunction!(self::adx::adx, m)?)?;
|
||||
m.add_function(pyo3::wrap_pyfunction!(self::adx::adxr, m)?)?;
|
||||
m.add_function(pyo3::wrap_pyfunction!(self::trix::trix, m)?)?;
|
||||
m.add_function(pyo3::wrap_pyfunction!(self::ultosc::ultosc, m)?)?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
use crate::validation;
|
||||
use numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};
|
||||
use pyo3::prelude::*;
|
||||
|
||||
/// Momentum: close[i] - close[i - timeperiod]. Leading timeperiod values are NaN.
|
||||
#[pyfunction]
|
||||
#[pyo3(signature = (close, timeperiod = 10))]
|
||||
pub fn mom<'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 result = vec![f64::NAN; n];
|
||||
for i in timeperiod..n {
|
||||
result[i] = prices[i] - prices[i - timeperiod];
|
||||
}
|
||||
Ok(result.into_pyarray(py))
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
use crate::validation;
|
||||
use numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};
|
||||
use pyo3::exceptions::PyValueError;
|
||||
use pyo3::prelude::*;
|
||||
use ta::indicators::PercentagePriceOscillator;
|
||||
use ta::Next;
|
||||
|
||||
/// Percentage Price Oscillator. Returns (ppo_line, signal_line, histogram).
|
||||
#[pyfunction]
|
||||
#[pyo3(signature = (close, fastperiod = 12, slowperiod = 26, signalperiod = 9))]
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub fn ppo<'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()?;
|
||||
let n = prices.len();
|
||||
let mut indicator = PercentagePriceOscillator::new(fastperiod, slowperiod, signalperiod)
|
||||
.map_err(|e| PyValueError::new_err(e.to_string()))?;
|
||||
let warmup = slowperiod + signalperiod - 2;
|
||||
let mut ppo_line = vec![f64::NAN; n];
|
||||
let mut signal_line = vec![f64::NAN; n];
|
||||
let mut hist = vec![f64::NAN; n];
|
||||
for (i, &price) in prices.iter().enumerate() {
|
||||
let out = indicator.next(price);
|
||||
if i >= warmup {
|
||||
ppo_line[i] = out.ppo;
|
||||
signal_line[i] = out.signal;
|
||||
hist[i] = out.histogram;
|
||||
}
|
||||
}
|
||||
Ok((
|
||||
ppo_line.into_pyarray(py),
|
||||
signal_line.into_pyarray(py),
|
||||
hist.into_pyarray(py),
|
||||
))
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
use crate::validation;
|
||||
use numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};
|
||||
use pyo3::exceptions::PyValueError;
|
||||
use pyo3::prelude::*;
|
||||
use ta::indicators::RateOfChange;
|
||||
use ta::Next;
|
||||
|
||||
/// Rate of Change: (price - prev) / prev * 100. Leading timeperiod values are NaN.
|
||||
#[pyfunction]
|
||||
#[pyo3(signature = (close, timeperiod = 10))]
|
||||
pub fn roc<'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 indicator =
|
||||
RateOfChange::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 val = indicator.next(price);
|
||||
if i >= timeperiod {
|
||||
result[i] = val;
|
||||
}
|
||||
}
|
||||
Ok(result.into_pyarray(py))
|
||||
}
|
||||
|
||||
/// Rate of Change Percentage: (price - prev) / prev. Leading timeperiod values are NaN.
|
||||
#[pyfunction]
|
||||
#[pyo3(signature = (close, timeperiod = 10))]
|
||||
pub fn rocp<'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 result = vec![f64::NAN; n];
|
||||
for i in timeperiod..n {
|
||||
let prev = prices[i - timeperiod];
|
||||
if prev != 0.0 {
|
||||
result[i] = (prices[i] - prev) / prev;
|
||||
}
|
||||
}
|
||||
Ok(result.into_pyarray(py))
|
||||
}
|
||||
|
||||
/// Rate of Change Ratio: price / prev. Leading timeperiod values are NaN.
|
||||
#[pyfunction]
|
||||
#[pyo3(signature = (close, timeperiod = 10))]
|
||||
pub fn rocr<'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 result = vec![f64::NAN; n];
|
||||
for i in timeperiod..n {
|
||||
let prev = prices[i - timeperiod];
|
||||
if prev != 0.0 {
|
||||
result[i] = prices[i] / prev;
|
||||
}
|
||||
}
|
||||
Ok(result.into_pyarray(py))
|
||||
}
|
||||
|
||||
/// Rate of Change Ratio × 100: (price / prev) * 100. Leading timeperiod values are NaN.
|
||||
#[pyfunction]
|
||||
#[pyo3(signature = (close, timeperiod = 10))]
|
||||
pub fn rocr100<'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 result = vec![f64::NAN; n];
|
||||
for i in timeperiod..n {
|
||||
let prev = prices[i - timeperiod];
|
||||
if prev != 0.0 {
|
||||
result[i] = (prices[i] / prev) * 100.0;
|
||||
}
|
||||
}
|
||||
Ok(result.into_pyarray(py))
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
use crate::validation;
|
||||
use numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};
|
||||
use pyo3::prelude::*;
|
||||
|
||||
/// Relative Strength Index. Uses TA-Lib–compatible Wilder smoothing seed:
|
||||
/// seed = SMA of first `timeperiod` gains (or losses), then Wilder EMA.
|
||||
/// Returns NaN for the first `timeperiod` bars.
|
||||
#[pyfunction]
|
||||
#[pyo3(signature = (close, timeperiod = 14))]
|
||||
pub fn rsi<'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 result = ferro_ta_core::momentum::rsi(prices, timeperiod);
|
||||
Ok(result.into_pyarray(py))
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
use crate::validation;
|
||||
use numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};
|
||||
use pyo3::prelude::*;
|
||||
|
||||
/// Slow Stochastic. Returns (slowk, slowd). Matches TA-Lib: Fast %K raw, Slow %K = SMA(fast %K, slowk_period), Slow %D = SMA(slow %K, slowd_period).
|
||||
/// Uses O(n) sliding max/min via monotonic deques.
|
||||
#[pyfunction]
|
||||
#[pyo3(signature = (high, low, close, fastk_period = 5, slowk_period = 3, slowd_period = 3))]
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub fn stoch<'py>(
|
||||
py: Python<'py>,
|
||||
high: PyReadonlyArray1<'py, f64>,
|
||||
low: PyReadonlyArray1<'py, f64>,
|
||||
close: PyReadonlyArray1<'py, f64>,
|
||||
fastk_period: usize,
|
||||
slowk_period: usize,
|
||||
slowd_period: usize,
|
||||
) -> PyResult<(Bound<'py, PyArray1<f64>>, Bound<'py, PyArray1<f64>>)> {
|
||||
validation::validate_timeperiod(fastk_period, "fastk_period", 1)?;
|
||||
validation::validate_timeperiod(slowk_period, "slowk_period", 1)?;
|
||||
validation::validate_timeperiod(slowd_period, "slowd_period", 1)?;
|
||||
let highs = high.as_slice()?;
|
||||
let lows = low.as_slice()?;
|
||||
let closes = close.as_slice()?;
|
||||
let n = highs.len();
|
||||
validation::validate_equal_length(&[
|
||||
(n, "high"),
|
||||
(lows.len(), "low"),
|
||||
(closes.len(), "close"),
|
||||
])?;
|
||||
let (slowk, slowd) = ferro_ta_core::momentum::stoch(
|
||||
highs,
|
||||
lows,
|
||||
closes,
|
||||
fastk_period,
|
||||
slowk_period,
|
||||
slowd_period,
|
||||
);
|
||||
Ok((slowk.into_pyarray(py), slowd.into_pyarray(py)))
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
use crate::validation;
|
||||
use numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};
|
||||
use pyo3::exceptions::PyValueError;
|
||||
use pyo3::prelude::*;
|
||||
use ta::indicators::{ExponentialMovingAverage, FastStochastic};
|
||||
use ta::{DataItem, Next};
|
||||
|
||||
/// Fast Stochastic. Returns (fastk, fastd). %K from high-low range; %D is EMA of %K.
|
||||
#[pyfunction]
|
||||
#[pyo3(signature = (high, low, close, fastk_period = 5, fastd_period = 3))]
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub fn stochf<'py>(
|
||||
py: Python<'py>,
|
||||
high: PyReadonlyArray1<'py, f64>,
|
||||
low: PyReadonlyArray1<'py, f64>,
|
||||
close: PyReadonlyArray1<'py, f64>,
|
||||
fastk_period: usize,
|
||||
fastd_period: usize,
|
||||
) -> PyResult<(Bound<'py, PyArray1<f64>>, Bound<'py, PyArray1<f64>>)> {
|
||||
validation::validate_timeperiod(fastk_period, "fastk_period", 1)?;
|
||||
validation::validate_timeperiod(fastd_period, "fastd_period", 1)?;
|
||||
let highs = high.as_slice()?;
|
||||
let lows = low.as_slice()?;
|
||||
let closes = close.as_slice()?;
|
||||
let n = highs.len();
|
||||
validation::validate_equal_length(&[
|
||||
(n, "high"),
|
||||
(lows.len(), "low"),
|
||||
(closes.len(), "close"),
|
||||
])?;
|
||||
|
||||
let mut fast_stoch =
|
||||
FastStochastic::new(fastk_period).map_err(|e| PyValueError::new_err(e.to_string()))?;
|
||||
let mut d_ema = ExponentialMovingAverage::new(fastd_period)
|
||||
.map_err(|e| PyValueError::new_err(e.to_string()))?;
|
||||
|
||||
let warmup_k = fastk_period - 1;
|
||||
let warmup_d = warmup_k + fastd_period - 1;
|
||||
|
||||
let mut fastk = vec![f64::NAN; n];
|
||||
let mut fastd = vec![f64::NAN; n];
|
||||
|
||||
for (i, ((&h, &l), &c)) in highs.iter().zip(lows.iter()).zip(closes.iter()).enumerate() {
|
||||
let bar = DataItem::builder()
|
||||
.high(h)
|
||||
.low(l)
|
||||
.close(c)
|
||||
.open(c)
|
||||
.volume(0.0)
|
||||
.build()
|
||||
.map_err(|e| PyValueError::new_err(e.to_string()))?;
|
||||
let k = fast_stoch.next(&bar);
|
||||
if i >= warmup_k {
|
||||
fastk[i] = k;
|
||||
let d = d_ema.next(k);
|
||||
if i >= warmup_d {
|
||||
fastd[i] = d;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok((fastk.into_pyarray(py), fastd.into_pyarray(py)))
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
use crate::validation;
|
||||
use numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};
|
||||
use pyo3::prelude::*;
|
||||
|
||||
fn compute_rsi_talib(prices: &[f64], period: usize) -> Vec<f64> {
|
||||
let n = prices.len();
|
||||
let mut result = vec![f64::NAN; n];
|
||||
if n <= period || period == 0 {
|
||||
return result;
|
||||
}
|
||||
let mut avg_gain = 0.0_f64;
|
||||
let mut avg_loss = 0.0_f64;
|
||||
for i in 1..=period {
|
||||
let delta = prices[i] - prices[i - 1];
|
||||
if delta > 0.0 {
|
||||
avg_gain += delta;
|
||||
} else {
|
||||
avg_loss += -delta;
|
||||
}
|
||||
}
|
||||
avg_gain /= period as f64;
|
||||
avg_loss /= period as f64;
|
||||
let rs = if avg_loss == 0.0 {
|
||||
f64::MAX
|
||||
} else {
|
||||
avg_gain / avg_loss
|
||||
};
|
||||
result[period] = 100.0 - 100.0 / (1.0 + rs);
|
||||
let period_f = period as f64;
|
||||
for i in (period + 1)..n {
|
||||
let delta = prices[i] - prices[i - 1];
|
||||
let (gain, loss) = if delta > 0.0 {
|
||||
(delta, 0.0)
|
||||
} else {
|
||||
(0.0, -delta)
|
||||
};
|
||||
avg_gain = (avg_gain * (period_f - 1.0) + gain) / period_f;
|
||||
avg_loss = (avg_loss * (period_f - 1.0) + loss) / period_f;
|
||||
let rs = if avg_loss == 0.0 {
|
||||
f64::MAX
|
||||
} else {
|
||||
avg_gain / avg_loss
|
||||
};
|
||||
result[i] = 100.0 - 100.0 / (1.0 + rs);
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
/// Stochastic RSI (TA-Lib–compatible): stochastic applied to RSI. Returns (fastk, fastd).
|
||||
#[pyfunction]
|
||||
#[pyo3(signature = (close, timeperiod = 14, fastk_period = 5, fastd_period = 3))]
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub fn stochrsi<'py>(
|
||||
py: Python<'py>,
|
||||
close: PyReadonlyArray1<'py, f64>,
|
||||
timeperiod: usize,
|
||||
fastk_period: usize,
|
||||
fastd_period: usize,
|
||||
) -> PyResult<(Bound<'py, PyArray1<f64>>, Bound<'py, PyArray1<f64>>)> {
|
||||
validation::validate_timeperiod(timeperiod, "timeperiod", 1)?;
|
||||
validation::validate_timeperiod(fastk_period, "fastk_period", 1)?;
|
||||
validation::validate_timeperiod(fastd_period, "fastd_period", 1)?;
|
||||
let prices = close.as_slice()?;
|
||||
let n = prices.len();
|
||||
|
||||
let rsi_vals = compute_rsi_talib(prices, timeperiod);
|
||||
|
||||
let rsi_warmup = timeperiod;
|
||||
let k_warmup = rsi_warmup + fastk_period - 1;
|
||||
let d_warmup = k_warmup + fastd_period - 1;
|
||||
|
||||
let mut fastk = vec![f64::NAN; n];
|
||||
let mut fastd = vec![f64::NAN; n];
|
||||
|
||||
for i in k_warmup..n {
|
||||
if rsi_vals[i].is_nan() {
|
||||
continue;
|
||||
}
|
||||
let start = i + 1 - fastk_period;
|
||||
if (start..=i).any(|j| rsi_vals[j].is_nan()) {
|
||||
continue;
|
||||
}
|
||||
let mx = rsi_vals[start..=i]
|
||||
.iter()
|
||||
.cloned()
|
||||
.fold(f64::NEG_INFINITY, f64::max);
|
||||
let mn = rsi_vals[start..=i]
|
||||
.iter()
|
||||
.cloned()
|
||||
.fold(f64::INFINITY, f64::min);
|
||||
fastk[i] = if mx != mn {
|
||||
100.0 * (rsi_vals[i] - mn) / (mx - mn)
|
||||
} else {
|
||||
50.0
|
||||
};
|
||||
}
|
||||
|
||||
for i in d_warmup..n {
|
||||
let start = i + 1 - fastd_period;
|
||||
let window = &fastk[start..=i];
|
||||
if window.iter().all(|v| !v.is_nan()) {
|
||||
fastd[i] = window.iter().sum::<f64>() / fastd_period as f64;
|
||||
}
|
||||
}
|
||||
Ok((fastk.into_pyarray(py), fastd.into_pyarray(py)))
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
use crate::validation;
|
||||
use numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};
|
||||
use pyo3::exceptions::PyValueError;
|
||||
use pyo3::prelude::*;
|
||||
use ta::indicators::ExponentialMovingAverage;
|
||||
use ta::Next;
|
||||
|
||||
/// TRIX: 1-period rate of change of triple-smoothed EMA.
|
||||
#[pyfunction]
|
||||
#[pyo3(signature = (close, timeperiod = 30))]
|
||||
pub fn trix<'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 warmup = 3 * (timeperiod - 1);
|
||||
let mut ema3_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 >= timeperiod - 1 {
|
||||
let v2 = ema2.next(v1);
|
||||
if i >= 2 * (timeperiod - 1) {
|
||||
let v3 = ema3.next(v2);
|
||||
if i >= warmup {
|
||||
ema3_vals[i] = v3;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for i in (warmup + 1)..n {
|
||||
let prev = ema3_vals[i - 1];
|
||||
if !ema3_vals[i].is_nan() && !prev.is_nan() && prev != 0.0 {
|
||||
result[i] = (ema3_vals[i] - prev) / prev * 100.0;
|
||||
}
|
||||
}
|
||||
Ok(result.into_pyarray(py))
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
use crate::validation;
|
||||
use numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};
|
||||
use pyo3::prelude::*;
|
||||
|
||||
/// Ultimate Oscillator: weighted sum of buying pressure over three periods (7, 14, 28).
|
||||
#[pyfunction]
|
||||
#[pyo3(signature = (high, low, close, timeperiod1 = 7, timeperiod2 = 14, timeperiod3 = 28))]
|
||||
pub fn ultosc<'py>(
|
||||
py: Python<'py>,
|
||||
high: PyReadonlyArray1<'py, f64>,
|
||||
low: PyReadonlyArray1<'py, f64>,
|
||||
close: PyReadonlyArray1<'py, f64>,
|
||||
timeperiod1: usize,
|
||||
timeperiod2: usize,
|
||||
timeperiod3: usize,
|
||||
) -> PyResult<Bound<'py, PyArray1<f64>>> {
|
||||
validation::validate_timeperiod(timeperiod1, "timeperiod1", 1)?;
|
||||
validation::validate_timeperiod(timeperiod2, "timeperiod2", 1)?;
|
||||
validation::validate_timeperiod(timeperiod3, "timeperiod3", 1)?;
|
||||
let highs = high.as_slice()?;
|
||||
let lows = low.as_slice()?;
|
||||
let closes = close.as_slice()?;
|
||||
let n = highs.len();
|
||||
validation::validate_equal_length(&[
|
||||
(n, "high"),
|
||||
(lows.len(), "low"),
|
||||
(closes.len(), "close"),
|
||||
])?;
|
||||
|
||||
let max_period = timeperiod1.max(timeperiod2).max(timeperiod3);
|
||||
let mut result = vec![f64::NAN; n];
|
||||
|
||||
let mut bp = vec![0.0_f64; n];
|
||||
let mut tr = vec![0.0_f64; n];
|
||||
for i in 1..n {
|
||||
let true_low = lows[i].min(closes[i - 1]);
|
||||
let true_high = highs[i].max(closes[i - 1]);
|
||||
bp[i] = closes[i] - true_low;
|
||||
tr[i] = true_high - true_low;
|
||||
}
|
||||
|
||||
for i in max_period..n {
|
||||
let raw1 = {
|
||||
let sum_bp: f64 = bp[(i + 1 - timeperiod1)..=i].iter().sum();
|
||||
let sum_tr: f64 = tr[(i + 1 - timeperiod1)..=i].iter().sum();
|
||||
if sum_tr != 0.0 {
|
||||
sum_bp / sum_tr
|
||||
} else {
|
||||
0.0
|
||||
}
|
||||
};
|
||||
let raw2 = {
|
||||
let sum_bp: f64 = bp[(i + 1 - timeperiod2)..=i].iter().sum();
|
||||
let sum_tr: f64 = tr[(i + 1 - timeperiod2)..=i].iter().sum();
|
||||
if sum_tr != 0.0 {
|
||||
sum_bp / sum_tr
|
||||
} else {
|
||||
0.0
|
||||
}
|
||||
};
|
||||
let raw3 = {
|
||||
let sum_bp: f64 = bp[(i + 1 - timeperiod3)..=i].iter().sum();
|
||||
let sum_tr: f64 = tr[(i + 1 - timeperiod3)..=i].iter().sum();
|
||||
if sum_tr != 0.0 {
|
||||
sum_bp / sum_tr
|
||||
} else {
|
||||
0.0
|
||||
}
|
||||
};
|
||||
result[i] = 100.0 * (4.0 * raw1 + 2.0 * raw2 + raw3) / 7.0;
|
||||
}
|
||||
Ok(result.into_pyarray(py))
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
use crate::validation;
|
||||
use numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};
|
||||
use pyo3::exceptions::PyValueError;
|
||||
use pyo3::prelude::*;
|
||||
use ta::indicators::{Maximum, Minimum};
|
||||
use ta::Next;
|
||||
|
||||
/// Williams' %R: -100 * (highest high - close) / (highest high - lowest low) over the window.
|
||||
#[pyfunction]
|
||||
#[pyo3(signature = (high, low, close, timeperiod = 14))]
|
||||
pub fn willr<'py>(
|
||||
py: Python<'py>,
|
||||
high: PyReadonlyArray1<'py, f64>,
|
||||
low: PyReadonlyArray1<'py, f64>,
|
||||
close: 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 closes = close.as_slice()?;
|
||||
let n = highs.len();
|
||||
validation::validate_equal_length(&[
|
||||
(n, "high"),
|
||||
(lows.len(), "low"),
|
||||
(closes.len(), "close"),
|
||||
])?;
|
||||
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, ((&h, &l), &c)) in highs.iter().zip(lows.iter()).zip(closes.iter()).enumerate() {
|
||||
let highest = max_ind.next(h);
|
||||
let lowest = min_ind.next(l);
|
||||
if i + 1 >= timeperiod {
|
||||
let range = highest - lowest;
|
||||
if range != 0.0 {
|
||||
result[i] = -100.0 * (highest - c) / range;
|
||||
} else {
|
||||
result[i] = -50.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(result.into_pyarray(py))
|
||||
}
|
||||
@@ -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),
|
||||
))
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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 0–7 (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))
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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 0–7). 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),
|
||||
))
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
use numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};
|
||||
use pyo3::prelude::*;
|
||||
|
||||
/// MESA Adaptive Moving Average. Returns (mama, fama). Uses Hilbert Transform–based 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)))
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
use numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};
|
||||
use pyo3::prelude::*;
|
||||
|
||||
use super::common::*;
|
||||
|
||||
#[pyfunction]
|
||||
pub fn cdl2crows<'py>(
|
||||
py: Python<'py>,
|
||||
open: PyReadonlyArray1<'py, f64>,
|
||||
high: PyReadonlyArray1<'py, f64>,
|
||||
low: PyReadonlyArray1<'py, f64>,
|
||||
close: PyReadonlyArray1<'py, f64>,
|
||||
) -> PyResult<Bound<'py, PyArray1<i32>>> {
|
||||
let opens = open.as_slice()?;
|
||||
let highs = high.as_slice()?;
|
||||
let lows = low.as_slice()?;
|
||||
let closes = close.as_slice()?;
|
||||
let n = opens.len();
|
||||
super::common::validate_ohlc_length(n, highs.len(), lows.len(), closes.len())?;
|
||||
let mut result = vec![0i32; n];
|
||||
for i in 2..n {
|
||||
let (o1, _h1, _l1, c1) = (opens[i - 2], highs[i - 2], lows[i - 2], closes[i - 2]);
|
||||
let (o2, _h2, _l2, c2) = (opens[i - 1], highs[i - 1], lows[i - 1], closes[i - 1]);
|
||||
let (o3, _h3, _l3, c3) = (opens[i], highs[i], lows[i], closes[i]);
|
||||
|
||||
// Two Crows:
|
||||
// 1. First candle is a long white (bullish) candle
|
||||
// 2. Second candle gaps up (opens above first close) and closes lower but still above first close
|
||||
// 3. Third candle opens within second body and closes within first body
|
||||
if is_bullish(o1, c1)
|
||||
&& is_bearish(o2, c2)
|
||||
&& o2 > c1 // gap up
|
||||
&& c2 > c1 // second still closes above first close
|
||||
&& is_bearish(o3, c3)
|
||||
&& o3 < o2 && o3 > c2 // opens within second body
|
||||
&& c3 > o1 && c3 < c1
|
||||
// closes within first body
|
||||
{
|
||||
result[i] = -100;
|
||||
}
|
||||
}
|
||||
Ok(result.into_pyarray(py))
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
use numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};
|
||||
use pyo3::prelude::*;
|
||||
|
||||
use super::common::*;
|
||||
|
||||
#[pyfunction]
|
||||
pub fn cdl3blackcrows<'py>(
|
||||
py: Python<'py>,
|
||||
open: PyReadonlyArray1<'py, f64>,
|
||||
high: PyReadonlyArray1<'py, f64>,
|
||||
low: PyReadonlyArray1<'py, f64>,
|
||||
close: PyReadonlyArray1<'py, f64>,
|
||||
) -> PyResult<Bound<'py, PyArray1<i32>>> {
|
||||
let opens = open.as_slice()?;
|
||||
let highs = high.as_slice()?;
|
||||
let lows = low.as_slice()?;
|
||||
let closes = close.as_slice()?;
|
||||
let n = opens.len();
|
||||
super::common::validate_ohlc_length(n, highs.len(), lows.len(), closes.len())?;
|
||||
let mut result = vec![0i32; n];
|
||||
for i in 2..n {
|
||||
let (o1, h1, l1, c1) = (opens[i - 2], highs[i - 2], lows[i - 2], closes[i - 2]);
|
||||
let (o2, h2, l2, c2) = (opens[i - 1], highs[i - 1], lows[i - 1], closes[i - 1]);
|
||||
let (o3, h3, l3, c3) = (opens[i], highs[i], lows[i], closes[i]);
|
||||
|
||||
let body1 = body_size(o1, c1);
|
||||
let body2 = body_size(o2, c2);
|
||||
let body3 = body_size(o3, c3);
|
||||
let range1 = candle_range(h1, l1);
|
||||
let range2 = candle_range(h2, l2);
|
||||
let range3 = candle_range(h3, l3);
|
||||
|
||||
// All three candles must be bearish with large bodies
|
||||
let long_body1 = range1 > 0.0 && body1 >= range1 * 0.6;
|
||||
let long_body2 = range2 > 0.0 && body2 >= range2 * 0.5;
|
||||
let long_body3 = range3 > 0.0 && body3 >= range3 * 0.5;
|
||||
|
||||
// Each opens within the previous candle's body and closes lower
|
||||
let open2_in_body1 = o2 < o1 && o2 > c1;
|
||||
let open3_in_body2 = o3 < o2 && o3 > c2;
|
||||
|
||||
// Small upper shadows (closes near the low)
|
||||
let small_upper1 = upper_shadow(o1, h1, c1) <= body1 * 0.3;
|
||||
let small_upper2 = upper_shadow(o2, h2, c2) <= body2 * 0.3;
|
||||
let small_upper3 = upper_shadow(o3, h3, c3) <= body3 * 0.3;
|
||||
|
||||
if is_bearish(o1, c1)
|
||||
&& is_bearish(o2, c2)
|
||||
&& is_bearish(o3, c3)
|
||||
&& long_body1
|
||||
&& long_body2
|
||||
&& long_body3
|
||||
&& open2_in_body1
|
||||
&& open3_in_body2
|
||||
&& small_upper1
|
||||
&& small_upper2
|
||||
&& small_upper3
|
||||
&& c2 < c1
|
||||
&& c3 < c2
|
||||
&& l3 < l2
|
||||
&& l2 < l1
|
||||
{
|
||||
result[i] = -100;
|
||||
}
|
||||
}
|
||||
Ok(result.into_pyarray(py))
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
use numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};
|
||||
use pyo3::prelude::*;
|
||||
|
||||
use super::common::*;
|
||||
|
||||
#[pyfunction]
|
||||
pub fn cdl3inside<'py>(
|
||||
py: Python<'py>,
|
||||
open: PyReadonlyArray1<'py, f64>,
|
||||
high: PyReadonlyArray1<'py, f64>,
|
||||
low: PyReadonlyArray1<'py, f64>,
|
||||
close: PyReadonlyArray1<'py, f64>,
|
||||
) -> PyResult<Bound<'py, PyArray1<i32>>> {
|
||||
let opens = open.as_slice()?;
|
||||
let highs = high.as_slice()?;
|
||||
let lows = low.as_slice()?;
|
||||
let closes = close.as_slice()?;
|
||||
let n = opens.len();
|
||||
super::common::validate_ohlc_length(n, highs.len(), lows.len(), closes.len())?;
|
||||
let mut result = vec![0i32; n];
|
||||
for i in 2..n {
|
||||
let (o1, h1, l1, c1) = (opens[i - 2], highs[i - 2], lows[i - 2], closes[i - 2]);
|
||||
let (o2, _h2, _l2, c2) = (opens[i - 1], highs[i - 1], lows[i - 1], closes[i - 1]);
|
||||
let (_o3, _h3, _l3, c3) = (opens[i], highs[i], lows[i], closes[i]);
|
||||
|
||||
let body1 = body_size(o1, c1);
|
||||
let body2 = body_size(o2, c2);
|
||||
let range1 = candle_range(h1, l1);
|
||||
|
||||
let large_body1 = range1 > 0.0 && body1 >= range1 * 0.5;
|
||||
|
||||
// Candle 2 body is inside candle 1 body (harami condition)
|
||||
let body2_high = o2.max(c2);
|
||||
let body2_low = o2.min(c2);
|
||||
let body1_high = o1.max(c1);
|
||||
let body1_low = o1.min(c1);
|
||||
let inside = body2_high <= body1_high && body2_low >= body1_low && body2 < body1 * 0.5;
|
||||
|
||||
// Three Inside Up: C1 bearish, C2 bullish harami, C3 closes above C2 close
|
||||
if is_bearish(o1, c1) && large_body1 && inside && is_bullish(o2, c2) && c3 > c2 {
|
||||
result[i] = 100;
|
||||
}
|
||||
// Three Inside Down: C1 bullish, C2 bearish harami, C3 closes below C2 close
|
||||
else if is_bullish(o1, c1) && large_body1 && inside && is_bearish(o2, c2) && c3 < c2 {
|
||||
result[i] = -100;
|
||||
}
|
||||
}
|
||||
Ok(result.into_pyarray(py))
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
use numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};
|
||||
use pyo3::prelude::*;
|
||||
|
||||
use super::common::*;
|
||||
|
||||
#[pyfunction]
|
||||
pub fn cdl3linestrike<'py>(
|
||||
py: Python<'py>,
|
||||
open: PyReadonlyArray1<'py, f64>,
|
||||
high: PyReadonlyArray1<'py, f64>,
|
||||
low: PyReadonlyArray1<'py, f64>,
|
||||
close: PyReadonlyArray1<'py, f64>,
|
||||
) -> PyResult<Bound<'py, PyArray1<i32>>> {
|
||||
let opens = open.as_slice()?;
|
||||
let highs = high.as_slice()?;
|
||||
let lows = low.as_slice()?;
|
||||
let closes = close.as_slice()?;
|
||||
let n = opens.len();
|
||||
super::common::validate_ohlc_length(n, highs.len(), lows.len(), closes.len())?;
|
||||
let mut result = vec![0i32; n];
|
||||
for i in 3..n {
|
||||
let (o0, c0) = (opens[i - 3], closes[i - 3]);
|
||||
let (o1, c1) = (opens[i - 2], closes[i - 2]);
|
||||
let (o2, c2) = (opens[i - 1], closes[i - 1]);
|
||||
let (o3, c3) = (opens[i], closes[i]);
|
||||
if is_bearish(o0, c0)
|
||||
&& is_bearish(o1, c1)
|
||||
&& is_bearish(o2, c2)
|
||||
&& c1 < c0
|
||||
&& c2 < c1
|
||||
&& is_bullish(o3, c3)
|
||||
&& o3 < c2
|
||||
&& c3 > o0
|
||||
{
|
||||
result[i] = 100;
|
||||
} else if is_bullish(o0, c0)
|
||||
&& is_bullish(o1, c1)
|
||||
&& is_bullish(o2, c2)
|
||||
&& c1 > c0
|
||||
&& c2 > c1
|
||||
&& is_bearish(o3, c3)
|
||||
&& o3 > c2
|
||||
&& c3 < o0
|
||||
{
|
||||
result[i] = -100;
|
||||
}
|
||||
}
|
||||
Ok(result.into_pyarray(py))
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
use numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};
|
||||
use pyo3::prelude::*;
|
||||
|
||||
use super::common::*;
|
||||
|
||||
#[pyfunction]
|
||||
pub fn cdl3outside<'py>(
|
||||
py: Python<'py>,
|
||||
open: PyReadonlyArray1<'py, f64>,
|
||||
high: PyReadonlyArray1<'py, f64>,
|
||||
low: PyReadonlyArray1<'py, f64>,
|
||||
close: PyReadonlyArray1<'py, f64>,
|
||||
) -> PyResult<Bound<'py, PyArray1<i32>>> {
|
||||
let opens = open.as_slice()?;
|
||||
let highs = high.as_slice()?;
|
||||
let lows = low.as_slice()?;
|
||||
let closes = close.as_slice()?;
|
||||
let n = opens.len();
|
||||
super::common::validate_ohlc_length(n, highs.len(), lows.len(), closes.len())?;
|
||||
let mut result = vec![0i32; n];
|
||||
for i in 2..n {
|
||||
let (o1, _h1, _l1, c1) = (opens[i - 2], highs[i - 2], lows[i - 2], closes[i - 2]);
|
||||
let (o2, _h2, _l2, c2) = (opens[i - 1], highs[i - 1], lows[i - 1], closes[i - 1]);
|
||||
let (_o3, _h3, _l3, c3) = (opens[i], highs[i], lows[i], closes[i]);
|
||||
|
||||
let body1_high = o1.max(c1);
|
||||
let body1_low = o1.min(c1);
|
||||
let body2_high = o2.max(c2);
|
||||
let body2_low = o2.min(c2);
|
||||
|
||||
// Engulfing: candle 2 body completely covers candle 1 body
|
||||
let engulfs = body2_high > body1_high && body2_low < body1_low;
|
||||
|
||||
// Three Outside Up: C1 bearish, C2 bullish engulfing, C3 closes above C2
|
||||
if is_bearish(o1, c1) && is_bullish(o2, c2) && engulfs && c3 > c2 {
|
||||
result[i] = 100;
|
||||
}
|
||||
// Three Outside Down: C1 bullish, C2 bearish engulfing, C3 closes below C2
|
||||
else if is_bullish(o1, c1) && is_bearish(o2, c2) && engulfs && c3 < c2 {
|
||||
result[i] = -100;
|
||||
}
|
||||
}
|
||||
Ok(result.into_pyarray(py))
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
use numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};
|
||||
use pyo3::prelude::*;
|
||||
|
||||
use super::common::*;
|
||||
|
||||
#[pyfunction]
|
||||
pub fn cdl3starsinsouth<'py>(
|
||||
py: Python<'py>,
|
||||
open: PyReadonlyArray1<'py, f64>,
|
||||
high: PyReadonlyArray1<'py, f64>,
|
||||
low: PyReadonlyArray1<'py, f64>,
|
||||
close: PyReadonlyArray1<'py, f64>,
|
||||
) -> PyResult<Bound<'py, PyArray1<i32>>> {
|
||||
let opens = open.as_slice()?;
|
||||
let highs = high.as_slice()?;
|
||||
let lows = low.as_slice()?;
|
||||
let closes = close.as_slice()?;
|
||||
let n = opens.len();
|
||||
super::common::validate_ohlc_length(n, highs.len(), lows.len(), closes.len())?;
|
||||
let mut result = vec![0i32; n];
|
||||
for i in 2..n {
|
||||
let (o0, h0, l0, c0) = (opens[i - 2], highs[i - 2], lows[i - 2], closes[i - 2]);
|
||||
let (o1, h1, l1, c1) = (opens[i - 1], highs[i - 1], lows[i - 1], closes[i - 1]);
|
||||
let (o2, h2, l2, c2) = (opens[i], highs[i], lows[i], closes[i]);
|
||||
if is_bearish(o0, c0)
|
||||
&& is_bearish(o1, c1)
|
||||
&& is_bearish(o2, c2)
|
||||
&& h1 <= h0
|
||||
&& l1 >= l0
|
||||
&& h2 <= h1
|
||||
&& l2 >= l1
|
||||
&& body_size(o2, c2) <= body_size(o1, c1) * 0.6
|
||||
&& upper_shadow(o2, h2, c2) <= body_size(o2, c2) * 0.2
|
||||
{
|
||||
result[i] = 100;
|
||||
}
|
||||
}
|
||||
Ok(result.into_pyarray(py))
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
use numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};
|
||||
use pyo3::prelude::*;
|
||||
|
||||
use super::common::*;
|
||||
|
||||
#[pyfunction]
|
||||
pub fn cdl3whitesoldiers<'py>(
|
||||
py: Python<'py>,
|
||||
open: PyReadonlyArray1<'py, f64>,
|
||||
high: PyReadonlyArray1<'py, f64>,
|
||||
low: PyReadonlyArray1<'py, f64>,
|
||||
close: PyReadonlyArray1<'py, f64>,
|
||||
) -> PyResult<Bound<'py, PyArray1<i32>>> {
|
||||
let opens = open.as_slice()?;
|
||||
let highs = high.as_slice()?;
|
||||
let lows = low.as_slice()?;
|
||||
let closes = close.as_slice()?;
|
||||
let n = opens.len();
|
||||
super::common::validate_ohlc_length(n, highs.len(), lows.len(), closes.len())?;
|
||||
let mut result = vec![0i32; n];
|
||||
for i in 2..n {
|
||||
let (o1, h1, l1, c1) = (opens[i - 2], highs[i - 2], lows[i - 2], closes[i - 2]);
|
||||
let (o2, h2, l2, c2) = (opens[i - 1], highs[i - 1], lows[i - 1], closes[i - 1]);
|
||||
let (o3, h3, l3, c3) = (opens[i], highs[i], lows[i], closes[i]);
|
||||
|
||||
let body1 = body_size(o1, c1);
|
||||
let body2 = body_size(o2, c2);
|
||||
let body3 = body_size(o3, c3);
|
||||
let range1 = candle_range(h1, l1);
|
||||
let range2 = candle_range(h2, l2);
|
||||
let range3 = candle_range(h3, l3);
|
||||
|
||||
let long_body1 = range1 > 0.0 && body1 >= range1 * 0.6;
|
||||
let long_body2 = range2 > 0.0 && body2 >= range2 * 0.5;
|
||||
let long_body3 = range3 > 0.0 && body3 >= range3 * 0.5;
|
||||
|
||||
let open2_in_body1 = o2 > o1 && o2 < c1;
|
||||
let open3_in_body2 = o3 > o2 && o3 < c2;
|
||||
|
||||
let small_lower1 = lower_shadow(o1, l1, c1) <= body1 * 0.3;
|
||||
let small_lower2 = lower_shadow(o2, l2, c2) <= body2 * 0.3;
|
||||
let small_lower3 = lower_shadow(o3, l3, c3) <= body3 * 0.3;
|
||||
|
||||
if is_bullish(o1, c1)
|
||||
&& is_bullish(o2, c2)
|
||||
&& is_bullish(o3, c3)
|
||||
&& long_body1
|
||||
&& long_body2
|
||||
&& long_body3
|
||||
&& open2_in_body1
|
||||
&& open3_in_body2
|
||||
&& small_lower1
|
||||
&& small_lower2
|
||||
&& small_lower3
|
||||
&& c2 > c1
|
||||
&& c3 > c2
|
||||
&& h3 > h2
|
||||
&& h2 > h1
|
||||
{
|
||||
result[i] = 100;
|
||||
}
|
||||
}
|
||||
Ok(result.into_pyarray(py))
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
use numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};
|
||||
use pyo3::prelude::*;
|
||||
|
||||
use super::common::*;
|
||||
|
||||
#[pyfunction]
|
||||
pub fn cdlabandonedbaby<'py>(
|
||||
py: Python<'py>,
|
||||
open: PyReadonlyArray1<'py, f64>,
|
||||
high: PyReadonlyArray1<'py, f64>,
|
||||
low: PyReadonlyArray1<'py, f64>,
|
||||
close: PyReadonlyArray1<'py, f64>,
|
||||
) -> PyResult<Bound<'py, PyArray1<i32>>> {
|
||||
let opens = open.as_slice()?;
|
||||
let highs = high.as_slice()?;
|
||||
let lows = low.as_slice()?;
|
||||
let closes = close.as_slice()?;
|
||||
let n = opens.len();
|
||||
super::common::validate_ohlc_length(n, highs.len(), lows.len(), closes.len())?;
|
||||
let mut result = vec![0i32; n];
|
||||
for i in 2..n {
|
||||
let (o0, h0, l0, c0) = (opens[i - 2], highs[i - 2], lows[i - 2], closes[i - 2]);
|
||||
let (o1, h1, l1, c1) = (opens[i - 1], highs[i - 1], lows[i - 1], closes[i - 1]);
|
||||
let (o2, h2, l2, c2) = (opens[i], highs[i], lows[i], closes[i]);
|
||||
let range0 = candle_range(h0, l0);
|
||||
let range1 = candle_range(h1, l1);
|
||||
let range2 = candle_range(h2, l2);
|
||||
let body0 = body_size(o0, c0);
|
||||
let body1 = body_size(o1, c1);
|
||||
let body2 = body_size(o2, c2);
|
||||
let is_doji1 = range1 > 0.0 && body1 / range1 <= 0.1;
|
||||
if is_bearish(o0, c0)
|
||||
&& range0 > 0.0
|
||||
&& body0 >= range0 * 0.5
|
||||
&& is_doji1
|
||||
&& h1 < l0
|
||||
&& is_bullish(o2, c2)
|
||||
&& range2 > 0.0
|
||||
&& body2 >= range2 * 0.5
|
||||
&& l2 > h1
|
||||
{
|
||||
result[i] = 100;
|
||||
} else if is_bullish(o0, c0)
|
||||
&& range0 > 0.0
|
||||
&& body0 >= range0 * 0.5
|
||||
&& is_doji1
|
||||
&& l1 > h0
|
||||
&& is_bearish(o2, c2)
|
||||
&& range2 > 0.0
|
||||
&& body2 >= range2 * 0.5
|
||||
&& h2 < l1
|
||||
{
|
||||
result[i] = -100;
|
||||
}
|
||||
}
|
||||
Ok(result.into_pyarray(py))
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
use numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};
|
||||
use pyo3::prelude::*;
|
||||
|
||||
use super::common::*;
|
||||
|
||||
#[pyfunction]
|
||||
pub fn cdladvanceblock<'py>(
|
||||
py: Python<'py>,
|
||||
open: PyReadonlyArray1<'py, f64>,
|
||||
high: PyReadonlyArray1<'py, f64>,
|
||||
low: PyReadonlyArray1<'py, f64>,
|
||||
close: PyReadonlyArray1<'py, f64>,
|
||||
) -> PyResult<Bound<'py, PyArray1<i32>>> {
|
||||
let opens = open.as_slice()?;
|
||||
let highs = high.as_slice()?;
|
||||
let lows = low.as_slice()?;
|
||||
let closes = close.as_slice()?;
|
||||
let n = opens.len();
|
||||
super::common::validate_ohlc_length(n, highs.len(), lows.len(), closes.len())?;
|
||||
let mut result = vec![0i32; n];
|
||||
for i in 2..n {
|
||||
let (o0, h0, _l0, c0) = (opens[i - 2], highs[i - 2], lows[i - 2], closes[i - 2]);
|
||||
let (o1, h1, _l1, c1) = (opens[i - 1], highs[i - 1], lows[i - 1], closes[i - 1]);
|
||||
let (o2, h2, _l2, c2) = (opens[i], highs[i], lows[i], closes[i]);
|
||||
let body0 = body_size(o0, c0);
|
||||
let body1 = body_size(o1, c1);
|
||||
let body2 = body_size(o2, c2);
|
||||
let us0 = upper_shadow(o0, h0, c0);
|
||||
let us1 = upper_shadow(o1, h1, c1);
|
||||
let us2 = upper_shadow(o2, h2, c2);
|
||||
if is_bullish(o0, c0)
|
||||
&& is_bullish(o1, c1)
|
||||
&& is_bullish(o2, c2)
|
||||
&& c1 > c0
|
||||
&& c2 > c1
|
||||
&& o1 >= o0
|
||||
&& o1 <= c0
|
||||
&& o2 >= o1
|
||||
&& o2 <= c1
|
||||
&& (body1 < body0 || body2 < body1 || us2 > us1 || us1 > us0)
|
||||
{
|
||||
result[i] = -100;
|
||||
}
|
||||
}
|
||||
Ok(result.into_pyarray(py))
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
use numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};
|
||||
use pyo3::prelude::*;
|
||||
|
||||
use super::common::*;
|
||||
|
||||
#[pyfunction]
|
||||
pub fn cdlbelthold<'py>(
|
||||
py: Python<'py>,
|
||||
open: PyReadonlyArray1<'py, f64>,
|
||||
high: PyReadonlyArray1<'py, f64>,
|
||||
low: PyReadonlyArray1<'py, f64>,
|
||||
close: PyReadonlyArray1<'py, f64>,
|
||||
) -> PyResult<Bound<'py, PyArray1<i32>>> {
|
||||
let opens = open.as_slice()?;
|
||||
let highs = high.as_slice()?;
|
||||
let lows = low.as_slice()?;
|
||||
let closes = close.as_slice()?;
|
||||
let n = opens.len();
|
||||
super::common::validate_ohlc_length(n, highs.len(), lows.len(), closes.len())?;
|
||||
let mut result = vec![0i32; n];
|
||||
for i in 0..n {
|
||||
let (o, h, l, c) = (opens[i], highs[i], lows[i], closes[i]);
|
||||
let range = candle_range(h, l);
|
||||
let body = body_size(o, c);
|
||||
if range == 0.0 {
|
||||
continue;
|
||||
}
|
||||
let long_body = body >= range * 0.6;
|
||||
if is_bullish(o, c) && long_body && (o - l).abs() <= range * 0.01 {
|
||||
result[i] = 100;
|
||||
} else if is_bearish(o, c) && long_body && (h - o).abs() <= range * 0.01 {
|
||||
result[i] = -100;
|
||||
}
|
||||
}
|
||||
Ok(result.into_pyarray(py))
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
use numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};
|
||||
use pyo3::prelude::*;
|
||||
|
||||
use super::common::*;
|
||||
|
||||
#[pyfunction]
|
||||
pub fn cdlbreakaway<'py>(
|
||||
py: Python<'py>,
|
||||
open: PyReadonlyArray1<'py, f64>,
|
||||
high: PyReadonlyArray1<'py, f64>,
|
||||
low: PyReadonlyArray1<'py, f64>,
|
||||
close: PyReadonlyArray1<'py, f64>,
|
||||
) -> PyResult<Bound<'py, PyArray1<i32>>> {
|
||||
let opens = open.as_slice()?;
|
||||
let highs = high.as_slice()?;
|
||||
let lows = low.as_slice()?;
|
||||
let closes = close.as_slice()?;
|
||||
let n = opens.len();
|
||||
super::common::validate_ohlc_length(n, highs.len(), lows.len(), closes.len())?;
|
||||
let mut result = vec![0i32; n];
|
||||
for i in 4..n {
|
||||
let (o0, h0, l0, c0) = (opens[i - 4], highs[i - 4], lows[i - 4], closes[i - 4]);
|
||||
let c1 = closes[i - 3];
|
||||
let c2 = closes[i - 2];
|
||||
let c3 = closes[i - 1];
|
||||
let _l3 = lows[i - 1];
|
||||
let _h3 = highs[i - 1];
|
||||
let (o4, c4) = (opens[i], closes[i]);
|
||||
let range0 = candle_range(h0, l0);
|
||||
let body0 = body_size(o0, c0);
|
||||
if is_bearish(o0, c0)
|
||||
&& range0 > 0.0
|
||||
&& body0 >= range0 * 0.4
|
||||
&& c1 < l0
|
||||
&& c2 < c1
|
||||
&& c3 < c2
|
||||
&& is_bullish(o4, c4)
|
||||
&& c4 > c1
|
||||
&& c4 < c0
|
||||
{
|
||||
result[i] = 100;
|
||||
} else if is_bullish(o0, c0)
|
||||
&& range0 > 0.0
|
||||
&& body0 >= range0 * 0.4
|
||||
&& c1 > h0
|
||||
&& c2 > c1
|
||||
&& c3 > c2
|
||||
&& is_bearish(o4, c4)
|
||||
&& c4 < c1
|
||||
&& c4 > c0
|
||||
{
|
||||
result[i] = -100;
|
||||
}
|
||||
}
|
||||
Ok(result.into_pyarray(py))
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
use numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};
|
||||
use pyo3::prelude::*;
|
||||
|
||||
use super::common::*;
|
||||
|
||||
#[pyfunction]
|
||||
pub fn cdlclosingmarubozu<'py>(
|
||||
py: Python<'py>,
|
||||
open: PyReadonlyArray1<'py, f64>,
|
||||
high: PyReadonlyArray1<'py, f64>,
|
||||
low: PyReadonlyArray1<'py, f64>,
|
||||
close: PyReadonlyArray1<'py, f64>,
|
||||
) -> PyResult<Bound<'py, PyArray1<i32>>> {
|
||||
let opens = open.as_slice()?;
|
||||
let highs = high.as_slice()?;
|
||||
let lows = low.as_slice()?;
|
||||
let closes = close.as_slice()?;
|
||||
let n = opens.len();
|
||||
super::common::validate_ohlc_length(n, highs.len(), lows.len(), closes.len())?;
|
||||
let mut result = vec![0i32; n];
|
||||
for i in 0..n {
|
||||
let (o, h, l, c) = (opens[i], highs[i], lows[i], closes[i]);
|
||||
let range = candle_range(h, l);
|
||||
if range == 0.0 {
|
||||
continue;
|
||||
}
|
||||
let body = body_size(o, c);
|
||||
if body < range * 0.4 {
|
||||
continue;
|
||||
}
|
||||
if is_bullish(o, c) && (h - c).abs() <= range * 0.01 {
|
||||
result[i] = 100;
|
||||
} else if is_bearish(o, c) && (c - l).abs() <= range * 0.01 {
|
||||
result[i] = -100;
|
||||
}
|
||||
}
|
||||
Ok(result.into_pyarray(py))
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
use numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};
|
||||
use pyo3::prelude::*;
|
||||
|
||||
use super::common::*;
|
||||
|
||||
#[pyfunction]
|
||||
pub fn cdlconcealbabyswall<'py>(
|
||||
py: Python<'py>,
|
||||
open: PyReadonlyArray1<'py, f64>,
|
||||
high: PyReadonlyArray1<'py, f64>,
|
||||
low: PyReadonlyArray1<'py, f64>,
|
||||
close: PyReadonlyArray1<'py, f64>,
|
||||
) -> PyResult<Bound<'py, PyArray1<i32>>> {
|
||||
let opens = open.as_slice()?;
|
||||
let highs = high.as_slice()?;
|
||||
let lows = low.as_slice()?;
|
||||
let closes = close.as_slice()?;
|
||||
let n = opens.len();
|
||||
super::common::validate_ohlc_length(n, highs.len(), lows.len(), closes.len())?;
|
||||
let mut result = vec![0i32; n];
|
||||
for i in 3..n {
|
||||
let (o0, h0, l0, c0) = (opens[i - 3], highs[i - 3], lows[i - 3], closes[i - 3]);
|
||||
let (o1, h1, l1, c1) = (opens[i - 2], highs[i - 2], lows[i - 2], closes[i - 2]);
|
||||
let (o2, h2, l2, c2) = (opens[i - 1], highs[i - 1], lows[i - 1], closes[i - 1]);
|
||||
let (o3, h3, l3, c3) = (opens[i], highs[i], lows[i], closes[i]);
|
||||
let range0 = candle_range(h0, l0);
|
||||
let range1 = candle_range(h1, l1);
|
||||
let maru0 = range0 > 0.0
|
||||
&& upper_shadow(o0, h0, c0) <= range0 * 0.02
|
||||
&& lower_shadow(o0, l0, c0) <= range0 * 0.02;
|
||||
let maru1 = range1 > 0.0
|
||||
&& upper_shadow(o1, h1, c1) <= range1 * 0.02
|
||||
&& lower_shadow(o1, l1, c1) <= range1 * 0.02;
|
||||
let gap_down = o2 < c1;
|
||||
let shadow_into = h2 >= c1;
|
||||
let engulfs = o3 >= o2 && c3 <= c2 && h3 >= h2 && l3 <= l2;
|
||||
if is_bearish(o0, c0)
|
||||
&& is_bearish(o1, c1)
|
||||
&& maru0
|
||||
&& maru1
|
||||
&& is_bearish(o2, c2)
|
||||
&& gap_down
|
||||
&& shadow_into
|
||||
&& is_bearish(o3, c3)
|
||||
&& engulfs
|
||||
{
|
||||
result[i] = 100;
|
||||
}
|
||||
}
|
||||
Ok(result.into_pyarray(py))
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
use numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};
|
||||
use pyo3::prelude::*;
|
||||
|
||||
use super::common::*;
|
||||
|
||||
#[pyfunction]
|
||||
pub fn cdlcounterattack<'py>(
|
||||
py: Python<'py>,
|
||||
open: PyReadonlyArray1<'py, f64>,
|
||||
high: PyReadonlyArray1<'py, f64>,
|
||||
low: PyReadonlyArray1<'py, f64>,
|
||||
close: PyReadonlyArray1<'py, f64>,
|
||||
) -> PyResult<Bound<'py, PyArray1<i32>>> {
|
||||
let opens = open.as_slice()?;
|
||||
let highs = high.as_slice()?;
|
||||
let lows = low.as_slice()?;
|
||||
let closes = close.as_slice()?;
|
||||
let n = opens.len();
|
||||
super::common::validate_ohlc_length(n, highs.len(), lows.len(), closes.len())?;
|
||||
let mut result = vec![0i32; n];
|
||||
for i in 1..n {
|
||||
let (o0, h0, l0, c0) = (opens[i - 1], highs[i - 1], lows[i - 1], closes[i - 1]);
|
||||
let (o1, h1, l1, c1) = (opens[i], highs[i], lows[i], closes[i]);
|
||||
let range0 = candle_range(h0, l0);
|
||||
let range1 = candle_range(h1, l1);
|
||||
let body0 = body_size(o0, c0);
|
||||
let body1 = body_size(o1, c1);
|
||||
let long0 = range0 > 0.0 && body0 >= range0 * 0.5;
|
||||
let long1 = range1 > 0.0 && body1 >= range1 * 0.5;
|
||||
let same_close = (c1 - c0).abs() <= range0 * 0.02;
|
||||
if is_bearish(o0, c0) && long0 && is_bullish(o1, c1) && long1 && same_close {
|
||||
result[i] = 100;
|
||||
} else if is_bullish(o0, c0) && long0 && is_bearish(o1, c1) && long1 && same_close {
|
||||
result[i] = -100;
|
||||
}
|
||||
}
|
||||
Ok(result.into_pyarray(py))
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
use numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};
|
||||
use pyo3::prelude::*;
|
||||
|
||||
use super::common::*;
|
||||
|
||||
#[pyfunction]
|
||||
pub fn cdldarkcloudcover<'py>(
|
||||
py: Python<'py>,
|
||||
open: PyReadonlyArray1<'py, f64>,
|
||||
high: PyReadonlyArray1<'py, f64>,
|
||||
low: PyReadonlyArray1<'py, f64>,
|
||||
close: PyReadonlyArray1<'py, f64>,
|
||||
) -> PyResult<Bound<'py, PyArray1<i32>>> {
|
||||
let opens = open.as_slice()?;
|
||||
let highs = high.as_slice()?;
|
||||
let lows = low.as_slice()?;
|
||||
let closes = close.as_slice()?;
|
||||
let n = opens.len();
|
||||
super::common::validate_ohlc_length(n, highs.len(), lows.len(), closes.len())?;
|
||||
let mut result = vec![0i32; n];
|
||||
for i in 1..n {
|
||||
let (o0, h0, l0, c0) = (opens[i - 1], highs[i - 1], lows[i - 1], closes[i - 1]);
|
||||
let (o1, _h1, _l1, c1) = (opens[i], highs[i], lows[i], closes[i]);
|
||||
let body0 = body_size(o0, c0);
|
||||
let range0 = candle_range(h0, l0);
|
||||
let midpoint0 = (o0 + c0) / 2.0;
|
||||
if is_bullish(o0, c0)
|
||||
&& range0 > 0.0
|
||||
&& body0 >= range0 * 0.5
|
||||
&& is_bearish(o1, c1)
|
||||
&& o1 > h0
|
||||
&& c1 < midpoint0
|
||||
&& c1 > o0
|
||||
{
|
||||
result[i] = -100;
|
||||
}
|
||||
}
|
||||
Ok(result.into_pyarray(py))
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
use numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};
|
||||
use pyo3::prelude::*;
|
||||
|
||||
use super::common::*;
|
||||
|
||||
#[pyfunction]
|
||||
pub fn cdldoji<'py>(
|
||||
py: Python<'py>,
|
||||
open: PyReadonlyArray1<'py, f64>,
|
||||
high: PyReadonlyArray1<'py, f64>,
|
||||
low: PyReadonlyArray1<'py, f64>,
|
||||
close: PyReadonlyArray1<'py, f64>,
|
||||
) -> PyResult<Bound<'py, PyArray1<i32>>> {
|
||||
let opens = open.as_slice()?;
|
||||
let highs = high.as_slice()?;
|
||||
let lows = low.as_slice()?;
|
||||
let closes = close.as_slice()?;
|
||||
let n = opens.len();
|
||||
super::common::validate_ohlc_length(n, highs.len(), lows.len(), closes.len())?;
|
||||
let mut result = vec![0i32; n];
|
||||
for i in 0..n {
|
||||
let body = body_size(opens[i], closes[i]);
|
||||
let range = candle_range(highs[i], lows[i]);
|
||||
// Doji: body is very small relative to range
|
||||
if range > 0.0 && body / range <= 0.1 {
|
||||
result[i] = 100;
|
||||
}
|
||||
}
|
||||
Ok(result.into_pyarray(py))
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
use numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};
|
||||
use pyo3::prelude::*;
|
||||
|
||||
use super::common::*;
|
||||
|
||||
#[pyfunction]
|
||||
pub fn cdldojistar<'py>(
|
||||
py: Python<'py>,
|
||||
open: PyReadonlyArray1<'py, f64>,
|
||||
high: PyReadonlyArray1<'py, f64>,
|
||||
low: PyReadonlyArray1<'py, f64>,
|
||||
close: PyReadonlyArray1<'py, f64>,
|
||||
) -> PyResult<Bound<'py, PyArray1<i32>>> {
|
||||
let opens = open.as_slice()?;
|
||||
let highs = high.as_slice()?;
|
||||
let lows = low.as_slice()?;
|
||||
let closes = close.as_slice()?;
|
||||
let n = opens.len();
|
||||
super::common::validate_ohlc_length(n, highs.len(), lows.len(), closes.len())?;
|
||||
let mut result = vec![0i32; n];
|
||||
for i in 1..n {
|
||||
let (o1, h1, l1, c1) = (opens[i - 1], highs[i - 1], lows[i - 1], closes[i - 1]);
|
||||
let (o2, h2, l2, c2) = (opens[i], highs[i], lows[i], closes[i]);
|
||||
|
||||
let body1 = body_size(o1, c1);
|
||||
let body2 = body_size(o2, c2);
|
||||
let range1 = candle_range(h1, l1);
|
||||
let range2 = candle_range(h2, l2);
|
||||
|
||||
let large_body1 = range1 > 0.0 && body1 >= range1 * 0.6;
|
||||
// Doji: body <= 10% of range
|
||||
let is_doji2 = range2 > 0.0 && body2 / range2 <= 0.1;
|
||||
|
||||
// Bullish Doji Star: prior bearish large candle, doji opens/closes below prior low
|
||||
let gap_down = o2.max(c2) < l1;
|
||||
if is_bearish(o1, c1) && large_body1 && is_doji2 && gap_down {
|
||||
result[i] = 100;
|
||||
}
|
||||
// Bearish Doji Star: prior bullish large candle, doji opens/closes above prior high
|
||||
let gap_up = o2.min(c2) > h1;
|
||||
if is_bullish(o1, c1) && large_body1 && is_doji2 && gap_up {
|
||||
result[i] = -100;
|
||||
}
|
||||
}
|
||||
Ok(result.into_pyarray(py))
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
use numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};
|
||||
use pyo3::prelude::*;
|
||||
|
||||
use super::common::*;
|
||||
|
||||
#[pyfunction]
|
||||
pub fn cdldragonflydoji<'py>(
|
||||
py: Python<'py>,
|
||||
open: PyReadonlyArray1<'py, f64>,
|
||||
high: PyReadonlyArray1<'py, f64>,
|
||||
low: PyReadonlyArray1<'py, f64>,
|
||||
close: PyReadonlyArray1<'py, f64>,
|
||||
) -> PyResult<Bound<'py, PyArray1<i32>>> {
|
||||
let opens = open.as_slice()?;
|
||||
let highs = high.as_slice()?;
|
||||
let lows = low.as_slice()?;
|
||||
let closes = close.as_slice()?;
|
||||
let n = opens.len();
|
||||
super::common::validate_ohlc_length(n, highs.len(), lows.len(), closes.len())?;
|
||||
let mut result = vec![0i32; n];
|
||||
for i in 0..n {
|
||||
let (o, h, l, c) = (opens[i], highs[i], lows[i], closes[i]);
|
||||
let range = candle_range(h, l);
|
||||
if range == 0.0 {
|
||||
continue;
|
||||
}
|
||||
let body = body_size(o, c);
|
||||
let us = upper_shadow(o, h, c);
|
||||
let ls = lower_shadow(o, l, c);
|
||||
if body / range <= 0.1 && us / range <= 0.1 && ls >= range * 0.6 {
|
||||
result[i] = 100;
|
||||
}
|
||||
}
|
||||
Ok(result.into_pyarray(py))
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
use numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};
|
||||
use pyo3::prelude::*;
|
||||
|
||||
use super::common::*;
|
||||
|
||||
#[pyfunction]
|
||||
pub fn cdlengulfing<'py>(
|
||||
py: Python<'py>,
|
||||
open: PyReadonlyArray1<'py, f64>,
|
||||
high: PyReadonlyArray1<'py, f64>,
|
||||
low: PyReadonlyArray1<'py, f64>,
|
||||
close: PyReadonlyArray1<'py, f64>,
|
||||
) -> PyResult<Bound<'py, PyArray1<i32>>> {
|
||||
let opens = open.as_slice()?;
|
||||
let highs = high.as_slice()?;
|
||||
let lows = low.as_slice()?;
|
||||
let closes = close.as_slice()?;
|
||||
let n = opens.len();
|
||||
super::common::validate_ohlc_length(n, highs.len(), lows.len(), closes.len())?;
|
||||
let mut result = vec![0i32; n];
|
||||
for i in 1..n {
|
||||
let prev_o = opens[i - 1];
|
||||
let prev_c = closes[i - 1];
|
||||
let curr_o = opens[i];
|
||||
let curr_c = closes[i];
|
||||
|
||||
let prev_body_high = prev_o.max(prev_c);
|
||||
let prev_body_low = prev_o.min(prev_c);
|
||||
let curr_body_high = curr_o.max(curr_c);
|
||||
let curr_body_low = curr_o.min(curr_c);
|
||||
|
||||
// Bullish engulfing: prev is bearish, current is bullish and engulfs
|
||||
if is_bearish(prev_o, prev_c)
|
||||
&& is_bullish(curr_o, curr_c)
|
||||
&& curr_body_high > prev_body_high
|
||||
&& curr_body_low < prev_body_low
|
||||
{
|
||||
result[i] = 100;
|
||||
}
|
||||
// Bearish engulfing: prev is bullish, current is bearish and engulfs
|
||||
else if is_bullish(prev_o, prev_c)
|
||||
&& is_bearish(curr_o, curr_c)
|
||||
&& curr_body_high > prev_body_high
|
||||
&& curr_body_low < prev_body_low
|
||||
{
|
||||
result[i] = -100;
|
||||
}
|
||||
}
|
||||
Ok(result.into_pyarray(py))
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
use numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};
|
||||
use pyo3::prelude::*;
|
||||
|
||||
use super::common::*;
|
||||
|
||||
#[pyfunction]
|
||||
pub fn cdleveningdojistar<'py>(
|
||||
py: Python<'py>,
|
||||
open: PyReadonlyArray1<'py, f64>,
|
||||
high: PyReadonlyArray1<'py, f64>,
|
||||
low: PyReadonlyArray1<'py, f64>,
|
||||
close: PyReadonlyArray1<'py, f64>,
|
||||
) -> PyResult<Bound<'py, PyArray1<i32>>> {
|
||||
let opens = open.as_slice()?;
|
||||
let highs = high.as_slice()?;
|
||||
let lows = low.as_slice()?;
|
||||
let closes = close.as_slice()?;
|
||||
let n = opens.len();
|
||||
super::common::validate_ohlc_length(n, highs.len(), lows.len(), closes.len())?;
|
||||
let mut result = vec![0i32; n];
|
||||
for i in 2..n {
|
||||
let (o1, h1, l1, c1) = (opens[i - 2], highs[i - 2], lows[i - 2], closes[i - 2]);
|
||||
let (o2, _h2, _l2, c2) = (opens[i - 1], highs[i - 1], lows[i - 1], closes[i - 1]);
|
||||
let (o3, h3, l3, c3) = (opens[i], highs[i], lows[i], closes[i]);
|
||||
|
||||
let body1 = body_size(o1, c1);
|
||||
let body2 = body_size(o2, c2);
|
||||
let body3 = body_size(o3, c3);
|
||||
let range1 = candle_range(h1, l1);
|
||||
let range2 = candle_range(o2.min(c2) - DOJI_BODY_EPSILON, o2.max(c2));
|
||||
let range3 = candle_range(h3, l3);
|
||||
|
||||
let large_body1 = range1 > 0.0 && body1 >= range1 * 0.6;
|
||||
let is_doji2 = range2 > 0.0 && body2 / range2 <= 0.1;
|
||||
let large_body3 = range3 > 0.0 && body3 >= range3 * 0.6;
|
||||
|
||||
if is_bullish(o1, c1)
|
||||
&& large_body1
|
||||
&& is_doji2
|
||||
&& is_bearish(o3, c3)
|
||||
&& large_body3
|
||||
&& c3 < (o1 + c1) / 2.0
|
||||
{
|
||||
result[i] = -100;
|
||||
}
|
||||
}
|
||||
Ok(result.into_pyarray(py))
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
use numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};
|
||||
use pyo3::prelude::*;
|
||||
|
||||
use super::common::*;
|
||||
|
||||
#[pyfunction]
|
||||
pub fn cdleveningstar<'py>(
|
||||
py: Python<'py>,
|
||||
open: PyReadonlyArray1<'py, f64>,
|
||||
high: PyReadonlyArray1<'py, f64>,
|
||||
low: PyReadonlyArray1<'py, f64>,
|
||||
close: PyReadonlyArray1<'py, f64>,
|
||||
) -> PyResult<Bound<'py, PyArray1<i32>>> {
|
||||
let opens = open.as_slice()?;
|
||||
let highs = high.as_slice()?;
|
||||
let lows = low.as_slice()?;
|
||||
let closes = close.as_slice()?;
|
||||
let n = opens.len();
|
||||
super::common::validate_ohlc_length(n, highs.len(), lows.len(), closes.len())?;
|
||||
let mut result = vec![0i32; n];
|
||||
for i in 2..n {
|
||||
let (o1, h1, l1, c1) = (opens[i - 2], highs[i - 2], lows[i - 2], closes[i - 2]);
|
||||
let (o2, _h2, _l2, c2) = (opens[i - 1], highs[i - 1], lows[i - 1], closes[i - 1]);
|
||||
let (o3, h3, l3, c3) = (opens[i], highs[i], lows[i], closes[i]);
|
||||
|
||||
let body1 = body_size(o1, c1);
|
||||
let body2 = body_size(o2, c2);
|
||||
let body3 = body_size(o3, c3);
|
||||
let range1 = candle_range(h1, l1);
|
||||
let range3 = candle_range(h3, l3);
|
||||
|
||||
// Evening star conditions:
|
||||
// 1. First candle is a large bullish candle
|
||||
// 2. Second candle is a star (small body) gapping above first
|
||||
// 3. Third candle is a large bearish candle
|
||||
let large_body1 = range1 > 0.0 && body1 >= range1 * 0.6;
|
||||
let small_body2 = range1 > 0.0 && body2 < body1 * 0.3;
|
||||
let large_body3 = range3 > 0.0 && body3 >= range3 * 0.6;
|
||||
|
||||
if is_bullish(o1, c1)
|
||||
&& large_body1
|
||||
&& small_body2
|
||||
&& is_bearish(o3, c3)
|
||||
&& large_body3
|
||||
&& c3 < (o1 + c1) / 2.0
|
||||
{
|
||||
result[i] = -100;
|
||||
}
|
||||
}
|
||||
Ok(result.into_pyarray(py))
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
use numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};
|
||||
use pyo3::prelude::*;
|
||||
|
||||
use super::common::*;
|
||||
|
||||
#[pyfunction]
|
||||
pub fn cdlgapsidesidewhite<'py>(
|
||||
py: Python<'py>,
|
||||
open: PyReadonlyArray1<'py, f64>,
|
||||
high: PyReadonlyArray1<'py, f64>,
|
||||
low: PyReadonlyArray1<'py, f64>,
|
||||
close: PyReadonlyArray1<'py, f64>,
|
||||
) -> PyResult<Bound<'py, PyArray1<i32>>> {
|
||||
let opens = open.as_slice()?;
|
||||
let highs = high.as_slice()?;
|
||||
let lows = low.as_slice()?;
|
||||
let closes = close.as_slice()?;
|
||||
let n = opens.len();
|
||||
super::common::validate_ohlc_length(n, highs.len(), lows.len(), closes.len())?;
|
||||
let mut result = vec![0i32; n];
|
||||
for i in 2..n {
|
||||
let (o0, _h0, _l0, c0) = (opens[i - 2], highs[i - 2], lows[i - 2], closes[i - 2]);
|
||||
let (o1, _h1, _l1, c1) = (opens[i - 1], highs[i - 1], lows[i - 1], closes[i - 1]);
|
||||
let (o2, _h2, _l2, c2) = (opens[i], highs[i], lows[i], closes[i]);
|
||||
let body1 = body_size(o1, c1);
|
||||
let body2 = body_size(o2, c2);
|
||||
let both_bullish = is_bullish(o1, c1) && is_bullish(o2, c2);
|
||||
let similar_size = body1 > 0.0 && (body2 - body1).abs() / body1 <= 0.3;
|
||||
let similar_open = body1 > 0.0 && (o2 - o1).abs() / body1 <= 0.3;
|
||||
if is_bullish(o0, c0) && both_bullish && similar_size && similar_open && o1 > c0 {
|
||||
result[i] = 100;
|
||||
} else if is_bearish(o0, c0) && both_bullish && similar_size && similar_open && c1 < o0 {
|
||||
result[i] = -100;
|
||||
}
|
||||
}
|
||||
Ok(result.into_pyarray(py))
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
use numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};
|
||||
use pyo3::prelude::*;
|
||||
|
||||
use super::common::*;
|
||||
|
||||
#[pyfunction]
|
||||
pub fn cdlgravestonedoji<'py>(
|
||||
py: Python<'py>,
|
||||
open: PyReadonlyArray1<'py, f64>,
|
||||
high: PyReadonlyArray1<'py, f64>,
|
||||
low: PyReadonlyArray1<'py, f64>,
|
||||
close: PyReadonlyArray1<'py, f64>,
|
||||
) -> PyResult<Bound<'py, PyArray1<i32>>> {
|
||||
let opens = open.as_slice()?;
|
||||
let highs = high.as_slice()?;
|
||||
let lows = low.as_slice()?;
|
||||
let closes = close.as_slice()?;
|
||||
let n = opens.len();
|
||||
super::common::validate_ohlc_length(n, highs.len(), lows.len(), closes.len())?;
|
||||
let mut result = vec![0i32; n];
|
||||
for i in 0..n {
|
||||
let (o, h, l, c) = (opens[i], highs[i], lows[i], closes[i]);
|
||||
let range = candle_range(h, l);
|
||||
if range == 0.0 {
|
||||
continue;
|
||||
}
|
||||
let body = body_size(o, c);
|
||||
let us = upper_shadow(o, h, c);
|
||||
let ls = lower_shadow(o, l, c);
|
||||
if body / range <= 0.1 && ls / range <= 0.1 && us >= range * 0.6 {
|
||||
result[i] = -100;
|
||||
}
|
||||
}
|
||||
Ok(result.into_pyarray(py))
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
use numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};
|
||||
use pyo3::prelude::*;
|
||||
|
||||
use super::common::*;
|
||||
|
||||
#[pyfunction]
|
||||
pub fn cdlhammer<'py>(
|
||||
py: Python<'py>,
|
||||
open: PyReadonlyArray1<'py, f64>,
|
||||
high: PyReadonlyArray1<'py, f64>,
|
||||
low: PyReadonlyArray1<'py, f64>,
|
||||
close: PyReadonlyArray1<'py, f64>,
|
||||
) -> PyResult<Bound<'py, PyArray1<i32>>> {
|
||||
let opens = open.as_slice()?;
|
||||
let highs = high.as_slice()?;
|
||||
let lows = low.as_slice()?;
|
||||
let closes = close.as_slice()?;
|
||||
let n = opens.len();
|
||||
super::common::validate_ohlc_length(n, highs.len(), lows.len(), closes.len())?;
|
||||
let mut result = vec![0i32; n];
|
||||
for i in 0..n {
|
||||
let body = body_size(opens[i], closes[i]);
|
||||
let range = candle_range(highs[i], lows[i]);
|
||||
let lower = lower_shadow(opens[i], lows[i], closes[i]);
|
||||
let upper = upper_shadow(opens[i], highs[i], closes[i]);
|
||||
|
||||
// Hammer: small body (< 1/3 range), long lower shadow (>= 2x body), small upper shadow
|
||||
if range > 0.0 && body > 0.0 && body <= range / 3.0 && lower >= 2.0 * body && upper <= body
|
||||
{
|
||||
result[i] = 100;
|
||||
}
|
||||
}
|
||||
Ok(result.into_pyarray(py))
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
use numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};
|
||||
use pyo3::prelude::*;
|
||||
|
||||
use super::common::*;
|
||||
|
||||
#[pyfunction]
|
||||
pub fn cdlhangingman<'py>(
|
||||
py: Python<'py>,
|
||||
open: PyReadonlyArray1<'py, f64>,
|
||||
high: PyReadonlyArray1<'py, f64>,
|
||||
low: PyReadonlyArray1<'py, f64>,
|
||||
close: PyReadonlyArray1<'py, f64>,
|
||||
) -> PyResult<Bound<'py, PyArray1<i32>>> {
|
||||
let opens = open.as_slice()?;
|
||||
let highs = high.as_slice()?;
|
||||
let lows = low.as_slice()?;
|
||||
let closes = close.as_slice()?;
|
||||
let n = opens.len();
|
||||
super::common::validate_ohlc_length(n, highs.len(), lows.len(), closes.len())?;
|
||||
let mut result = vec![0i32; n];
|
||||
for i in 0..n {
|
||||
let (o, h, l, c) = (opens[i], highs[i], lows[i], closes[i]);
|
||||
let range = candle_range(h, l);
|
||||
if range == 0.0 {
|
||||
continue;
|
||||
}
|
||||
let body = body_size(o, c);
|
||||
let us = upper_shadow(o, h, c);
|
||||
let ls = lower_shadow(o, l, c);
|
||||
if range > 0.0 && body > 0.0 && ls >= body * 2.0 && us <= body && body / range <= 0.4 {
|
||||
result[i] = -100;
|
||||
}
|
||||
}
|
||||
Ok(result.into_pyarray(py))
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
use numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};
|
||||
use pyo3::prelude::*;
|
||||
|
||||
use super::common::*;
|
||||
|
||||
#[pyfunction]
|
||||
pub fn cdlharami<'py>(
|
||||
py: Python<'py>,
|
||||
open: PyReadonlyArray1<'py, f64>,
|
||||
high: PyReadonlyArray1<'py, f64>,
|
||||
low: PyReadonlyArray1<'py, f64>,
|
||||
close: PyReadonlyArray1<'py, f64>,
|
||||
) -> PyResult<Bound<'py, PyArray1<i32>>> {
|
||||
let opens = open.as_slice()?;
|
||||
let highs = high.as_slice()?;
|
||||
let lows = low.as_slice()?;
|
||||
let closes = close.as_slice()?;
|
||||
let n = opens.len();
|
||||
super::common::validate_ohlc_length(n, highs.len(), lows.len(), closes.len())?;
|
||||
let mut result = vec![0i32; n];
|
||||
for i in 1..n {
|
||||
let (o1, h1, l1, c1) = (opens[i - 1], highs[i - 1], lows[i - 1], closes[i - 1]);
|
||||
let (o2, _h2, _l2, c2) = (opens[i], highs[i], lows[i], closes[i]);
|
||||
|
||||
let body1 = body_size(o1, c1);
|
||||
let body2 = body_size(o2, c2);
|
||||
let range1 = candle_range(h1, l1);
|
||||
|
||||
let large_body1 = range1 > 0.0 && body1 >= range1 * 0.5;
|
||||
|
||||
// Current candle body is inside prior candle body
|
||||
let body1_high = o1.max(c1);
|
||||
let body1_low = o1.min(c1);
|
||||
let body2_high = o2.max(c2);
|
||||
let body2_low = o2.min(c2);
|
||||
|
||||
let inside = body2_high <= body1_high && body2_low >= body1_low && body2 < body1 * 0.6;
|
||||
|
||||
// Bullish Harami: prior bearish, current bullish inside
|
||||
if is_bearish(o1, c1) && large_body1 && inside && is_bullish(o2, c2) {
|
||||
result[i] = 100;
|
||||
}
|
||||
// Bearish Harami: prior bullish, current bearish inside
|
||||
else if is_bullish(o1, c1) && large_body1 && inside && is_bearish(o2, c2) {
|
||||
result[i] = -100;
|
||||
}
|
||||
}
|
||||
Ok(result.into_pyarray(py))
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
use numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};
|
||||
use pyo3::prelude::*;
|
||||
|
||||
use super::common::*;
|
||||
|
||||
#[pyfunction]
|
||||
pub fn cdlharamicross<'py>(
|
||||
py: Python<'py>,
|
||||
open: PyReadonlyArray1<'py, f64>,
|
||||
high: PyReadonlyArray1<'py, f64>,
|
||||
low: PyReadonlyArray1<'py, f64>,
|
||||
close: PyReadonlyArray1<'py, f64>,
|
||||
) -> PyResult<Bound<'py, PyArray1<i32>>> {
|
||||
let opens = open.as_slice()?;
|
||||
let highs = high.as_slice()?;
|
||||
let lows = low.as_slice()?;
|
||||
let closes = close.as_slice()?;
|
||||
let n = opens.len();
|
||||
super::common::validate_ohlc_length(n, highs.len(), lows.len(), closes.len())?;
|
||||
let mut result = vec![0i32; n];
|
||||
for i in 1..n {
|
||||
let (o1, h1, l1, c1) = (opens[i - 1], highs[i - 1], lows[i - 1], closes[i - 1]);
|
||||
let (o2, h2, l2, c2) = (opens[i], highs[i], lows[i], closes[i]);
|
||||
|
||||
let body1 = body_size(o1, c1);
|
||||
let body2 = body_size(o2, c2);
|
||||
let range1 = candle_range(h1, l1);
|
||||
let range2 = candle_range(h2, l2);
|
||||
|
||||
let large_body1 = range1 > 0.0 && body1 >= range1 * 0.5;
|
||||
|
||||
// Second candle must be a doji
|
||||
let is_doji2 = range2 > 0.0 && body2 / range2 <= 0.1;
|
||||
|
||||
// Doji body must be inside prior body
|
||||
let body1_high = o1.max(c1);
|
||||
let body1_low = o1.min(c1);
|
||||
let doji_mid = (o2 + c2) / 2.0;
|
||||
let inside = doji_mid <= body1_high && doji_mid >= body1_low;
|
||||
|
||||
// Bullish Harami Cross: prior bearish large candle, doji inside
|
||||
if is_bearish(o1, c1) && large_body1 && is_doji2 && inside {
|
||||
result[i] = 100;
|
||||
}
|
||||
// Bearish Harami Cross: prior bullish large candle, doji inside
|
||||
else if is_bullish(o1, c1) && large_body1 && is_doji2 && inside {
|
||||
result[i] = -100;
|
||||
}
|
||||
}
|
||||
Ok(result.into_pyarray(py))
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
use numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};
|
||||
use pyo3::prelude::*;
|
||||
|
||||
use super::common::*;
|
||||
|
||||
#[pyfunction]
|
||||
pub fn cdlhighwave<'py>(
|
||||
py: Python<'py>,
|
||||
open: PyReadonlyArray1<'py, f64>,
|
||||
high: PyReadonlyArray1<'py, f64>,
|
||||
low: PyReadonlyArray1<'py, f64>,
|
||||
close: PyReadonlyArray1<'py, f64>,
|
||||
) -> PyResult<Bound<'py, PyArray1<i32>>> {
|
||||
let opens = open.as_slice()?;
|
||||
let highs = high.as_slice()?;
|
||||
let lows = low.as_slice()?;
|
||||
let closes = close.as_slice()?;
|
||||
let n = opens.len();
|
||||
super::common::validate_ohlc_length(n, highs.len(), lows.len(), closes.len())?;
|
||||
let mut result = vec![0i32; n];
|
||||
for i in 0..n {
|
||||
let (o, h, l, c) = (opens[i], highs[i], lows[i], closes[i]);
|
||||
let range = candle_range(h, l);
|
||||
if range == 0.0 {
|
||||
continue;
|
||||
}
|
||||
let body = body_size(o, c);
|
||||
let us = upper_shadow(o, h, c);
|
||||
let ls = lower_shadow(o, l, c);
|
||||
if body / range <= 0.3 && us >= range * 0.3 && ls >= range * 0.3 {
|
||||
if is_bullish(o, c) {
|
||||
result[i] = 100;
|
||||
} else {
|
||||
result[i] = -100;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(result.into_pyarray(py))
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
use numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};
|
||||
use pyo3::prelude::*;
|
||||
|
||||
use super::common::*;
|
||||
|
||||
#[pyfunction]
|
||||
pub fn cdlhikkake<'py>(
|
||||
py: Python<'py>,
|
||||
open: PyReadonlyArray1<'py, f64>,
|
||||
high: PyReadonlyArray1<'py, f64>,
|
||||
low: PyReadonlyArray1<'py, f64>,
|
||||
close: PyReadonlyArray1<'py, f64>,
|
||||
) -> PyResult<Bound<'py, PyArray1<i32>>> {
|
||||
let opens = open.as_slice()?;
|
||||
let highs = high.as_slice()?;
|
||||
let lows = low.as_slice()?;
|
||||
let closes = close.as_slice()?;
|
||||
let n = opens.len();
|
||||
super::common::validate_ohlc_length(n, highs.len(), lows.len(), closes.len())?;
|
||||
let mut result = vec![0i32; n];
|
||||
for i in 2..n {
|
||||
let (o0, h0, l0, c0) = (opens[i - 2], highs[i - 2], lows[i - 2], closes[i - 2]);
|
||||
let h1 = highs[i - 1];
|
||||
let l1 = lows[i - 1];
|
||||
let h2 = highs[i];
|
||||
let l2 = lows[i];
|
||||
let inside = h1 <= h0 && l1 >= l0;
|
||||
if !inside {
|
||||
continue;
|
||||
}
|
||||
if is_bearish(o0, c0) && h2 > h1 && l2 > l1 {
|
||||
result[i] = 100;
|
||||
} else if is_bullish(o0, c0) && l2 < l1 && h2 < h1 {
|
||||
result[i] = -100;
|
||||
}
|
||||
}
|
||||
Ok(result.into_pyarray(py))
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
use numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};
|
||||
use pyo3::prelude::*;
|
||||
|
||||
use super::common::*;
|
||||
|
||||
#[pyfunction]
|
||||
pub fn cdlhikkakemod<'py>(
|
||||
py: Python<'py>,
|
||||
open: PyReadonlyArray1<'py, f64>,
|
||||
high: PyReadonlyArray1<'py, f64>,
|
||||
low: PyReadonlyArray1<'py, f64>,
|
||||
close: PyReadonlyArray1<'py, f64>,
|
||||
) -> PyResult<Bound<'py, PyArray1<i32>>> {
|
||||
let opens = open.as_slice()?;
|
||||
let highs = high.as_slice()?;
|
||||
let lows = low.as_slice()?;
|
||||
let closes = close.as_slice()?;
|
||||
let n = opens.len();
|
||||
super::common::validate_ohlc_length(n, highs.len(), lows.len(), closes.len())?;
|
||||
let mut result = vec![0i32; n];
|
||||
for i in 3..n {
|
||||
let (o0, h0, l0, c0) = (opens[i - 3], highs[i - 3], lows[i - 3], closes[i - 3]);
|
||||
let h1 = highs[i - 2];
|
||||
let l1 = lows[i - 2];
|
||||
let h2 = highs[i - 1];
|
||||
let l2 = lows[i - 1];
|
||||
let h3 = highs[i];
|
||||
let l3 = lows[i];
|
||||
let inside = h1 <= h0 && l1 >= l0;
|
||||
if !inside {
|
||||
continue;
|
||||
}
|
||||
if is_bearish(o0, c0) && l2 < l1 && h3 > h1 && l3 > l1 {
|
||||
result[i] = 100;
|
||||
} else if is_bullish(o0, c0) && h2 > h1 && l3 < l1 && h3 < h1 {
|
||||
result[i] = -100;
|
||||
}
|
||||
}
|
||||
Ok(result.into_pyarray(py))
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
use numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};
|
||||
use pyo3::prelude::*;
|
||||
|
||||
use super::common::*;
|
||||
|
||||
#[pyfunction]
|
||||
pub fn cdlhomingpigeon<'py>(
|
||||
py: Python<'py>,
|
||||
open: PyReadonlyArray1<'py, f64>,
|
||||
high: PyReadonlyArray1<'py, f64>,
|
||||
low: PyReadonlyArray1<'py, f64>,
|
||||
close: PyReadonlyArray1<'py, f64>,
|
||||
) -> PyResult<Bound<'py, PyArray1<i32>>> {
|
||||
let opens = open.as_slice()?;
|
||||
let highs = high.as_slice()?;
|
||||
let lows = low.as_slice()?;
|
||||
let closes = close.as_slice()?;
|
||||
let n = opens.len();
|
||||
super::common::validate_ohlc_length(n, highs.len(), lows.len(), closes.len())?;
|
||||
let mut result = vec![0i32; n];
|
||||
for i in 1..n {
|
||||
let (o0, _h0, _l0, c0) = (opens[i - 1], highs[i - 1], lows[i - 1], closes[i - 1]);
|
||||
let (o1, _h1, _l1, c1) = (opens[i], highs[i], lows[i], closes[i]);
|
||||
let body0_high = o0.max(c0);
|
||||
let body0_low = o0.min(c0);
|
||||
let body1_high = o1.max(c1);
|
||||
let body1_low = o1.min(c1);
|
||||
if is_bearish(o0, c0)
|
||||
&& is_bearish(o1, c1)
|
||||
&& body1_high <= body0_high
|
||||
&& body1_low >= body0_low
|
||||
{
|
||||
result[i] = 100;
|
||||
}
|
||||
}
|
||||
Ok(result.into_pyarray(py))
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
use numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};
|
||||
use pyo3::prelude::*;
|
||||
|
||||
use super::common::*;
|
||||
|
||||
#[pyfunction]
|
||||
pub fn cdlidentical3crows<'py>(
|
||||
py: Python<'py>,
|
||||
open: PyReadonlyArray1<'py, f64>,
|
||||
high: PyReadonlyArray1<'py, f64>,
|
||||
low: PyReadonlyArray1<'py, f64>,
|
||||
close: PyReadonlyArray1<'py, f64>,
|
||||
) -> PyResult<Bound<'py, PyArray1<i32>>> {
|
||||
let opens = open.as_slice()?;
|
||||
let highs = high.as_slice()?;
|
||||
let lows = low.as_slice()?;
|
||||
let closes = close.as_slice()?;
|
||||
let n = opens.len();
|
||||
super::common::validate_ohlc_length(n, highs.len(), lows.len(), closes.len())?;
|
||||
let mut result = vec![0i32; n];
|
||||
for i in 2..n {
|
||||
let (o0, h0, l0, c0) = (opens[i - 2], highs[i - 2], lows[i - 2], closes[i - 2]);
|
||||
let (o1, h1, l1, c1) = (opens[i - 1], highs[i - 1], lows[i - 1], closes[i - 1]);
|
||||
let (o2, h2, l2, c2) = (opens[i], highs[i], lows[i], closes[i]);
|
||||
let range0 = candle_range(h0, l0);
|
||||
let range1 = candle_range(h1, l1);
|
||||
let tol0 = range0 * 0.03;
|
||||
let tol1 = range1 * 0.03;
|
||||
if is_bearish(o0, c0)
|
||||
&& is_bearish(o1, c1)
|
||||
&& is_bearish(o2, c2)
|
||||
&& c1 < c0
|
||||
&& c2 < c1
|
||||
&& (o1 - c0).abs() <= tol0
|
||||
&& (o2 - c1).abs() <= tol1
|
||||
&& range0 > 0.0
|
||||
&& range1 > 0.0
|
||||
&& candle_range(h2, l2) > 0.0
|
||||
{
|
||||
result[i] = -100;
|
||||
}
|
||||
}
|
||||
Ok(result.into_pyarray(py))
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
use numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};
|
||||
use pyo3::prelude::*;
|
||||
|
||||
use super::common::*;
|
||||
|
||||
#[pyfunction]
|
||||
pub fn cdlinneck<'py>(
|
||||
py: Python<'py>,
|
||||
open: PyReadonlyArray1<'py, f64>,
|
||||
high: PyReadonlyArray1<'py, f64>,
|
||||
low: PyReadonlyArray1<'py, f64>,
|
||||
close: PyReadonlyArray1<'py, f64>,
|
||||
) -> PyResult<Bound<'py, PyArray1<i32>>> {
|
||||
let opens = open.as_slice()?;
|
||||
let highs = high.as_slice()?;
|
||||
let lows = low.as_slice()?;
|
||||
let closes = close.as_slice()?;
|
||||
let n = opens.len();
|
||||
super::common::validate_ohlc_length(n, highs.len(), lows.len(), closes.len())?;
|
||||
let mut result = vec![0i32; n];
|
||||
for i in 1..n {
|
||||
let (o0, h0, l0, c0) = (opens[i - 1], highs[i - 1], lows[i - 1], closes[i - 1]);
|
||||
let (o1, _h1, _l1, c1) = (opens[i], highs[i], lows[i], closes[i]);
|
||||
let range0 = candle_range(h0, l0);
|
||||
let body0 = body_size(o0, c0);
|
||||
if is_bearish(o0, c0)
|
||||
&& range0 > 0.0
|
||||
&& body0 >= range0 * 0.4
|
||||
&& is_bullish(o1, c1)
|
||||
&& o1 < l0
|
||||
&& (c1 - c0).abs() <= range0 * 0.03
|
||||
{
|
||||
result[i] = -100;
|
||||
}
|
||||
}
|
||||
Ok(result.into_pyarray(py))
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
use numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};
|
||||
use pyo3::prelude::*;
|
||||
|
||||
use super::common::*;
|
||||
|
||||
#[pyfunction]
|
||||
pub fn cdlinvertedhammer<'py>(
|
||||
py: Python<'py>,
|
||||
open: PyReadonlyArray1<'py, f64>,
|
||||
high: PyReadonlyArray1<'py, f64>,
|
||||
low: PyReadonlyArray1<'py, f64>,
|
||||
close: PyReadonlyArray1<'py, f64>,
|
||||
) -> PyResult<Bound<'py, PyArray1<i32>>> {
|
||||
let opens = open.as_slice()?;
|
||||
let highs = high.as_slice()?;
|
||||
let lows = low.as_slice()?;
|
||||
let closes = close.as_slice()?;
|
||||
let n = opens.len();
|
||||
super::common::validate_ohlc_length(n, highs.len(), lows.len(), closes.len())?;
|
||||
let mut result = vec![0i32; n];
|
||||
for i in 0..n {
|
||||
let (o, h, l, c) = (opens[i], highs[i], lows[i], closes[i]);
|
||||
let range = candle_range(h, l);
|
||||
if range == 0.0 {
|
||||
continue;
|
||||
}
|
||||
let body = body_size(o, c);
|
||||
let us = upper_shadow(o, h, c);
|
||||
let ls = lower_shadow(o, l, c);
|
||||
if body > 0.0 && us >= body * 2.0 && ls <= body && body / range <= 0.4 {
|
||||
result[i] = 100;
|
||||
}
|
||||
}
|
||||
Ok(result.into_pyarray(py))
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
use numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};
|
||||
use pyo3::prelude::*;
|
||||
|
||||
use super::common::*;
|
||||
|
||||
#[pyfunction]
|
||||
pub fn cdlkicking<'py>(
|
||||
py: Python<'py>,
|
||||
open: PyReadonlyArray1<'py, f64>,
|
||||
high: PyReadonlyArray1<'py, f64>,
|
||||
low: PyReadonlyArray1<'py, f64>,
|
||||
close: PyReadonlyArray1<'py, f64>,
|
||||
) -> PyResult<Bound<'py, PyArray1<i32>>> {
|
||||
let opens = open.as_slice()?;
|
||||
let highs = high.as_slice()?;
|
||||
let lows = low.as_slice()?;
|
||||
let closes = close.as_slice()?;
|
||||
let n = opens.len();
|
||||
super::common::validate_ohlc_length(n, highs.len(), lows.len(), closes.len())?;
|
||||
let mut result = vec![0i32; n];
|
||||
for i in 1..n {
|
||||
let (o0, h0, l0, c0) = (opens[i - 1], highs[i - 1], lows[i - 1], closes[i - 1]);
|
||||
let (o1, h1, l1, c1) = (opens[i], highs[i], lows[i], closes[i]);
|
||||
let range0 = candle_range(h0, l0);
|
||||
let range1 = candle_range(h1, l1);
|
||||
let maru0 = range0 > 0.0
|
||||
&& upper_shadow(o0, h0, c0) <= range0 * 0.02
|
||||
&& lower_shadow(o0, l0, c0) <= range0 * 0.02;
|
||||
let maru1 = range1 > 0.0
|
||||
&& upper_shadow(o1, h1, c1) <= range1 * 0.02
|
||||
&& lower_shadow(o1, l1, c1) <= range1 * 0.02;
|
||||
if is_bearish(o0, c0) && maru0 && is_bullish(o1, c1) && maru1 && o1 > o0 {
|
||||
result[i] = 100;
|
||||
} else if is_bullish(o0, c0) && maru0 && is_bearish(o1, c1) && maru1 && o1 < o0 {
|
||||
result[i] = -100;
|
||||
}
|
||||
}
|
||||
Ok(result.into_pyarray(py))
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
use numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};
|
||||
use pyo3::prelude::*;
|
||||
|
||||
use super::common::*;
|
||||
|
||||
#[pyfunction]
|
||||
pub fn cdlkickingbylength<'py>(
|
||||
py: Python<'py>,
|
||||
open: PyReadonlyArray1<'py, f64>,
|
||||
high: PyReadonlyArray1<'py, f64>,
|
||||
low: PyReadonlyArray1<'py, f64>,
|
||||
close: PyReadonlyArray1<'py, f64>,
|
||||
) -> PyResult<Bound<'py, PyArray1<i32>>> {
|
||||
let opens = open.as_slice()?;
|
||||
let highs = high.as_slice()?;
|
||||
let lows = low.as_slice()?;
|
||||
let closes = close.as_slice()?;
|
||||
let n = opens.len();
|
||||
super::common::validate_ohlc_length(n, highs.len(), lows.len(), closes.len())?;
|
||||
let mut result = vec![0i32; n];
|
||||
for i in 1..n {
|
||||
let (o0, h0, l0, c0) = (opens[i - 1], highs[i - 1], lows[i - 1], closes[i - 1]);
|
||||
let (o1, h1, l1, c1) = (opens[i], highs[i], lows[i], closes[i]);
|
||||
let range0 = candle_range(h0, l0);
|
||||
let range1 = candle_range(h1, l1);
|
||||
let maru0 = range0 > 0.0
|
||||
&& upper_shadow(o0, h0, c0) <= range0 * 0.02
|
||||
&& lower_shadow(o0, l0, c0) <= range0 * 0.02;
|
||||
let maru1 = range1 > 0.0
|
||||
&& upper_shadow(o1, h1, c1) <= range1 * 0.02
|
||||
&& lower_shadow(o1, l1, c1) <= range1 * 0.02;
|
||||
let opposite = (is_bearish(o0, c0) && is_bullish(o1, c1))
|
||||
|| (is_bullish(o0, c0) && is_bearish(o1, c1));
|
||||
let has_gap = (o1 - c0).abs() > 0.0;
|
||||
if maru0 && maru1 && opposite && has_gap {
|
||||
if range1 >= range0 {
|
||||
if is_bullish(o1, c1) {
|
||||
result[i] = 100;
|
||||
} else {
|
||||
result[i] = -100;
|
||||
}
|
||||
} else if is_bullish(o0, c0) {
|
||||
result[i] = 100;
|
||||
} else {
|
||||
result[i] = -100;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(result.into_pyarray(py))
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
use numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};
|
||||
use pyo3::prelude::*;
|
||||
|
||||
use super::common::*;
|
||||
|
||||
#[pyfunction]
|
||||
pub fn cdlladderbottom<'py>(
|
||||
py: Python<'py>,
|
||||
open: PyReadonlyArray1<'py, f64>,
|
||||
high: PyReadonlyArray1<'py, f64>,
|
||||
low: PyReadonlyArray1<'py, f64>,
|
||||
close: PyReadonlyArray1<'py, f64>,
|
||||
) -> PyResult<Bound<'py, PyArray1<i32>>> {
|
||||
let opens = open.as_slice()?;
|
||||
let highs = high.as_slice()?;
|
||||
let lows = low.as_slice()?;
|
||||
let closes = close.as_slice()?;
|
||||
let n = opens.len();
|
||||
super::common::validate_ohlc_length(n, highs.len(), lows.len(), closes.len())?;
|
||||
let mut result = vec![0i32; n];
|
||||
for i in 4..n {
|
||||
let (o0, _h0, _l0, c0) = (opens[i - 4], highs[i - 4], lows[i - 4], closes[i - 4]);
|
||||
let (o1, _h1, _l1, c1) = (opens[i - 3], highs[i - 3], lows[i - 3], closes[i - 3]);
|
||||
let (o2, _h2, _l2, c2) = (opens[i - 2], highs[i - 2], lows[i - 2], closes[i - 2]);
|
||||
let (o3, h3, _l3, c3) = (opens[i - 1], highs[i - 1], lows[i - 1], closes[i - 1]);
|
||||
let (o4, h4, l4, c4) = (opens[i], highs[i], lows[i], closes[i]);
|
||||
let three_bear = is_bearish(o0, c0) && is_bearish(o1, c1) && is_bearish(o2, c2);
|
||||
let descend = c1 < c0 && c2 < c1;
|
||||
let us3 = upper_shadow(o3, h3, c3);
|
||||
let body3 = body_size(o3, c3);
|
||||
let inv_hammer = us3 >= body3 * 1.5;
|
||||
let range4 = candle_range(h4, l4);
|
||||
let body4 = body_size(o4, c4);
|
||||
let large_bull = is_bullish(o4, c4) && range4 > 0.0 && body4 >= range4 * 0.5;
|
||||
if three_bear && descend && inv_hammer && large_bull && c4 > c2 {
|
||||
result[i] = 100;
|
||||
}
|
||||
}
|
||||
Ok(result.into_pyarray(py))
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
use numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};
|
||||
use pyo3::prelude::*;
|
||||
|
||||
use super::common::*;
|
||||
|
||||
#[pyfunction]
|
||||
pub fn cdllongleggeddoji<'py>(
|
||||
py: Python<'py>,
|
||||
open: PyReadonlyArray1<'py, f64>,
|
||||
high: PyReadonlyArray1<'py, f64>,
|
||||
low: PyReadonlyArray1<'py, f64>,
|
||||
close: PyReadonlyArray1<'py, f64>,
|
||||
) -> PyResult<Bound<'py, PyArray1<i32>>> {
|
||||
let opens = open.as_slice()?;
|
||||
let highs = high.as_slice()?;
|
||||
let lows = low.as_slice()?;
|
||||
let closes = close.as_slice()?;
|
||||
let n = opens.len();
|
||||
super::common::validate_ohlc_length(n, highs.len(), lows.len(), closes.len())?;
|
||||
let mut result = vec![0i32; n];
|
||||
for i in 0..n {
|
||||
let (o, h, l, c) = (opens[i], highs[i], lows[i], closes[i]);
|
||||
let range = candle_range(h, l);
|
||||
if range == 0.0 {
|
||||
continue;
|
||||
}
|
||||
let body = body_size(o, c);
|
||||
let us = upper_shadow(o, h, c);
|
||||
let ls = lower_shadow(o, l, c);
|
||||
if body / range <= 0.1 && us >= range * 0.3 && ls >= range * 0.3 {
|
||||
result[i] = 100;
|
||||
}
|
||||
}
|
||||
Ok(result.into_pyarray(py))
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
use numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};
|
||||
use pyo3::prelude::*;
|
||||
|
||||
use super::common::*;
|
||||
|
||||
#[pyfunction]
|
||||
pub fn cdllongline<'py>(
|
||||
py: Python<'py>,
|
||||
open: PyReadonlyArray1<'py, f64>,
|
||||
high: PyReadonlyArray1<'py, f64>,
|
||||
low: PyReadonlyArray1<'py, f64>,
|
||||
close: PyReadonlyArray1<'py, f64>,
|
||||
) -> PyResult<Bound<'py, PyArray1<i32>>> {
|
||||
let opens = open.as_slice()?;
|
||||
let highs = high.as_slice()?;
|
||||
let lows = low.as_slice()?;
|
||||
let closes = close.as_slice()?;
|
||||
let n = opens.len();
|
||||
super::common::validate_ohlc_length(n, highs.len(), lows.len(), closes.len())?;
|
||||
let mut result = vec![0i32; n];
|
||||
for i in 0..n {
|
||||
let (o, h, l, c) = (opens[i], highs[i], lows[i], closes[i]);
|
||||
let range = candle_range(h, l);
|
||||
if range == 0.0 {
|
||||
continue;
|
||||
}
|
||||
let body = body_size(o, c);
|
||||
if body >= range * 0.7 {
|
||||
if is_bullish(o, c) {
|
||||
result[i] = 100;
|
||||
} else {
|
||||
result[i] = -100;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(result.into_pyarray(py))
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
use numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};
|
||||
use pyo3::prelude::*;
|
||||
|
||||
use super::common::*;
|
||||
|
||||
#[pyfunction]
|
||||
pub fn cdlmarubozu<'py>(
|
||||
py: Python<'py>,
|
||||
open: PyReadonlyArray1<'py, f64>,
|
||||
high: PyReadonlyArray1<'py, f64>,
|
||||
low: PyReadonlyArray1<'py, f64>,
|
||||
close: PyReadonlyArray1<'py, f64>,
|
||||
) -> PyResult<Bound<'py, PyArray1<i32>>> {
|
||||
let opens = open.as_slice()?;
|
||||
let highs = high.as_slice()?;
|
||||
let lows = low.as_slice()?;
|
||||
let closes = close.as_slice()?;
|
||||
let n = opens.len();
|
||||
super::common::validate_ohlc_length(n, highs.len(), lows.len(), closes.len())?;
|
||||
let mut result = vec![0i32; n];
|
||||
for i in 0..n {
|
||||
let body = body_size(opens[i], closes[i]);
|
||||
let range = candle_range(highs[i], lows[i]);
|
||||
let lower = lower_shadow(opens[i], lows[i], closes[i]);
|
||||
let upper = upper_shadow(opens[i], highs[i], closes[i]);
|
||||
|
||||
// Marubozu: body is >= 95% of range, tiny or no shadows
|
||||
if range > 0.0 && body >= range * 0.95 && upper <= range * 0.025 && lower <= range * 0.025 {
|
||||
if is_bullish(opens[i], closes[i]) {
|
||||
result[i] = 100;
|
||||
} else {
|
||||
result[i] = -100;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(result.into_pyarray(py))
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
use numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};
|
||||
use pyo3::prelude::*;
|
||||
|
||||
use super::common::*;
|
||||
|
||||
#[pyfunction]
|
||||
pub fn cdlmatchinglow<'py>(
|
||||
py: Python<'py>,
|
||||
open: PyReadonlyArray1<'py, f64>,
|
||||
high: PyReadonlyArray1<'py, f64>,
|
||||
low: PyReadonlyArray1<'py, f64>,
|
||||
close: PyReadonlyArray1<'py, f64>,
|
||||
) -> PyResult<Bound<'py, PyArray1<i32>>> {
|
||||
let opens = open.as_slice()?;
|
||||
let highs = high.as_slice()?;
|
||||
let lows = low.as_slice()?;
|
||||
let closes = close.as_slice()?;
|
||||
let n = opens.len();
|
||||
super::common::validate_ohlc_length(n, highs.len(), lows.len(), closes.len())?;
|
||||
let mut result = vec![0i32; n];
|
||||
for i in 1..n {
|
||||
let (o0, h0, l0, c0) = (opens[i - 1], highs[i - 1], lows[i - 1], closes[i - 1]);
|
||||
let (o1, _h1, _l1, c1) = (opens[i], highs[i], lows[i], closes[i]);
|
||||
let range0 = candle_range(h0, l0);
|
||||
let tol = range0 * 0.02;
|
||||
if is_bearish(o0, c0) && is_bearish(o1, c1) && (c1 - c0).abs() <= tol {
|
||||
result[i] = 100;
|
||||
}
|
||||
}
|
||||
Ok(result.into_pyarray(py))
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
use numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};
|
||||
use pyo3::prelude::*;
|
||||
|
||||
use super::common::*;
|
||||
|
||||
#[pyfunction]
|
||||
pub fn cdlmathold<'py>(
|
||||
py: Python<'py>,
|
||||
open: PyReadonlyArray1<'py, f64>,
|
||||
high: PyReadonlyArray1<'py, f64>,
|
||||
low: PyReadonlyArray1<'py, f64>,
|
||||
close: PyReadonlyArray1<'py, f64>,
|
||||
) -> PyResult<Bound<'py, PyArray1<i32>>> {
|
||||
let opens = open.as_slice()?;
|
||||
let highs = high.as_slice()?;
|
||||
let lows = low.as_slice()?;
|
||||
let closes = close.as_slice()?;
|
||||
let n = opens.len();
|
||||
super::common::validate_ohlc_length(n, highs.len(), lows.len(), closes.len())?;
|
||||
let mut result = vec![0i32; n];
|
||||
for i in 4..n {
|
||||
let (o0, h0, l0, c0) = (opens[i - 4], highs[i - 4], lows[i - 4], closes[i - 4]);
|
||||
let (o1, _h1, l1, c1) = (opens[i - 3], highs[i - 3], lows[i - 3], closes[i - 3]);
|
||||
let (o2, _h2, l2, c2) = (opens[i - 2], highs[i - 2], lows[i - 2], closes[i - 2]);
|
||||
let (o3, _h3, l3, c3) = (opens[i - 1], highs[i - 1], lows[i - 1], closes[i - 1]);
|
||||
let (o4, h4, l4, c4) = (opens[i], highs[i], lows[i], closes[i]);
|
||||
let range0 = candle_range(h0, l0);
|
||||
let body0 = body_size(o0, c0);
|
||||
let range4 = candle_range(h4, l4);
|
||||
let body4 = body_size(o4, c4);
|
||||
let large_bull0 = is_bullish(o0, c0) && range0 > 0.0 && body0 >= range0 * 0.5;
|
||||
let small_bears = is_bearish(o1, c1) && is_bearish(o2, c2) && is_bearish(o3, c3);
|
||||
let stay_above = l1 >= o0 && l2 >= o0 && l3 >= o0;
|
||||
let large_bull4 = is_bullish(o4, c4) && range4 > 0.0 && body4 >= range4 * 0.5 && c4 > c0;
|
||||
if large_bull0 && small_bears && stay_above && large_bull4 {
|
||||
result[i] = 100;
|
||||
}
|
||||
}
|
||||
Ok(result.into_pyarray(py))
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
use numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};
|
||||
use pyo3::prelude::*;
|
||||
|
||||
use super::common::*;
|
||||
|
||||
#[pyfunction]
|
||||
pub fn cdlmorningdojistar<'py>(
|
||||
py: Python<'py>,
|
||||
open: PyReadonlyArray1<'py, f64>,
|
||||
high: PyReadonlyArray1<'py, f64>,
|
||||
low: PyReadonlyArray1<'py, f64>,
|
||||
close: PyReadonlyArray1<'py, f64>,
|
||||
) -> PyResult<Bound<'py, PyArray1<i32>>> {
|
||||
let opens = open.as_slice()?;
|
||||
let highs = high.as_slice()?;
|
||||
let lows = low.as_slice()?;
|
||||
let closes = close.as_slice()?;
|
||||
let n = opens.len();
|
||||
super::common::validate_ohlc_length(n, highs.len(), lows.len(), closes.len())?;
|
||||
let mut result = vec![0i32; n];
|
||||
for i in 2..n {
|
||||
let (o1, h1, l1, c1) = (opens[i - 2], highs[i - 2], lows[i - 2], closes[i - 2]);
|
||||
let (o2, _h2, _l2, c2) = (opens[i - 1], highs[i - 1], lows[i - 1], closes[i - 1]);
|
||||
let (o3, h3, l3, c3) = (opens[i], highs[i], lows[i], closes[i]);
|
||||
|
||||
let body1 = body_size(o1, c1);
|
||||
let body2 = body_size(o2, c2);
|
||||
let body3 = body_size(o3, c3);
|
||||
let range1 = candle_range(h1, l1);
|
||||
let range2 = candle_range(o2.min(c2) - DOJI_BODY_EPSILON, o2.max(c2)); // avoid div-by-zero
|
||||
let range3 = candle_range(h3, l3);
|
||||
|
||||
let large_body1 = range1 > 0.0 && body1 >= range1 * 0.6;
|
||||
// Middle candle must be a doji
|
||||
let is_doji2 = range2 > 0.0 && body2 / range2 <= 0.1;
|
||||
let large_body3 = range3 > 0.0 && body3 >= range3 * 0.6;
|
||||
|
||||
if is_bearish(o1, c1)
|
||||
&& large_body1
|
||||
&& is_doji2
|
||||
&& is_bullish(o3, c3)
|
||||
&& large_body3
|
||||
&& c3 > (o1 + c1) / 2.0
|
||||
{
|
||||
result[i] = 100;
|
||||
}
|
||||
}
|
||||
Ok(result.into_pyarray(py))
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
use numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};
|
||||
use pyo3::prelude::*;
|
||||
|
||||
use super::common::*;
|
||||
|
||||
#[pyfunction]
|
||||
pub fn cdlmorningstar<'py>(
|
||||
py: Python<'py>,
|
||||
open: PyReadonlyArray1<'py, f64>,
|
||||
high: PyReadonlyArray1<'py, f64>,
|
||||
low: PyReadonlyArray1<'py, f64>,
|
||||
close: PyReadonlyArray1<'py, f64>,
|
||||
) -> PyResult<Bound<'py, PyArray1<i32>>> {
|
||||
let opens = open.as_slice()?;
|
||||
let highs = high.as_slice()?;
|
||||
let lows = low.as_slice()?;
|
||||
let closes = close.as_slice()?;
|
||||
let n = opens.len();
|
||||
super::common::validate_ohlc_length(n, highs.len(), lows.len(), closes.len())?;
|
||||
let mut result = vec![0i32; n];
|
||||
for i in 2..n {
|
||||
let (o1, h1, l1, c1) = (opens[i - 2], highs[i - 2], lows[i - 2], closes[i - 2]);
|
||||
let (o2, _h2, _l2, c2) = (opens[i - 1], highs[i - 1], lows[i - 1], closes[i - 1]);
|
||||
let (o3, h3, l3, c3) = (opens[i], highs[i], lows[i], closes[i]);
|
||||
|
||||
let body1 = body_size(o1, c1);
|
||||
let body2 = body_size(o2, c2);
|
||||
let body3 = body_size(o3, c3);
|
||||
let range1 = candle_range(h1, l1);
|
||||
let range3 = candle_range(h3, l3);
|
||||
|
||||
// Morning star conditions:
|
||||
// 1. First candle is a large bearish candle
|
||||
// 2. Second candle is a star (small body) gapping below first
|
||||
// 3. Third candle is a large bullish candle
|
||||
let large_body1 = range1 > 0.0 && body1 >= range1 * 0.6;
|
||||
let small_body2 = range1 > 0.0 && body2 < body1 * 0.3;
|
||||
let large_body3 = range3 > 0.0 && body3 >= range3 * 0.6;
|
||||
|
||||
if is_bearish(o1, c1)
|
||||
&& large_body1
|
||||
&& small_body2
|
||||
&& is_bullish(o3, c3)
|
||||
&& large_body3
|
||||
&& c3 > (o1 + c1) / 2.0
|
||||
{
|
||||
result[i] = 100;
|
||||
}
|
||||
}
|
||||
Ok(result.into_pyarray(py))
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
use numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};
|
||||
use pyo3::prelude::*;
|
||||
|
||||
use super::common::*;
|
||||
|
||||
#[pyfunction]
|
||||
pub fn cdlonneck<'py>(
|
||||
py: Python<'py>,
|
||||
open: PyReadonlyArray1<'py, f64>,
|
||||
high: PyReadonlyArray1<'py, f64>,
|
||||
low: PyReadonlyArray1<'py, f64>,
|
||||
close: PyReadonlyArray1<'py, f64>,
|
||||
) -> PyResult<Bound<'py, PyArray1<i32>>> {
|
||||
let opens = open.as_slice()?;
|
||||
let highs = high.as_slice()?;
|
||||
let lows = low.as_slice()?;
|
||||
let closes = close.as_slice()?;
|
||||
let n = opens.len();
|
||||
super::common::validate_ohlc_length(n, highs.len(), lows.len(), closes.len())?;
|
||||
let mut result = vec![0i32; n];
|
||||
for i in 1..n {
|
||||
let (o0, h0, l0, c0) = (opens[i - 1], highs[i - 1], lows[i - 1], closes[i - 1]);
|
||||
let (o1, _h1, _l1, c1) = (opens[i], highs[i], lows[i], closes[i]);
|
||||
let range0 = candle_range(h0, l0);
|
||||
let body0 = body_size(o0, c0);
|
||||
if is_bearish(o0, c0)
|
||||
&& range0 > 0.0
|
||||
&& body0 >= range0 * 0.4
|
||||
&& is_bullish(o1, c1)
|
||||
&& o1 < l0
|
||||
&& (c1 - l0).abs() <= range0 * 0.03
|
||||
{
|
||||
result[i] = -100;
|
||||
}
|
||||
}
|
||||
Ok(result.into_pyarray(py))
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
use numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};
|
||||
use pyo3::prelude::*;
|
||||
|
||||
use super::common::*;
|
||||
|
||||
#[pyfunction]
|
||||
pub fn cdlpiercing<'py>(
|
||||
py: Python<'py>,
|
||||
open: PyReadonlyArray1<'py, f64>,
|
||||
high: PyReadonlyArray1<'py, f64>,
|
||||
low: PyReadonlyArray1<'py, f64>,
|
||||
close: PyReadonlyArray1<'py, f64>,
|
||||
) -> PyResult<Bound<'py, PyArray1<i32>>> {
|
||||
let opens = open.as_slice()?;
|
||||
let highs = high.as_slice()?;
|
||||
let lows = low.as_slice()?;
|
||||
let closes = close.as_slice()?;
|
||||
let n = opens.len();
|
||||
super::common::validate_ohlc_length(n, highs.len(), lows.len(), closes.len())?;
|
||||
let mut result = vec![0i32; n];
|
||||
for i in 1..n {
|
||||
let (o0, h0, l0, c0) = (opens[i - 1], highs[i - 1], lows[i - 1], closes[i - 1]);
|
||||
let (o1, _h1, _l1, c1) = (opens[i], highs[i], lows[i], closes[i]);
|
||||
let body0 = body_size(o0, c0);
|
||||
let range0 = candle_range(h0, l0);
|
||||
let midpoint0 = (o0 + c0) / 2.0;
|
||||
if is_bearish(o0, c0)
|
||||
&& range0 > 0.0
|
||||
&& body0 >= range0 * 0.4
|
||||
&& is_bullish(o1, c1)
|
||||
&& o1 < l0
|
||||
&& c1 > midpoint0
|
||||
&& c1 < o0
|
||||
{
|
||||
result[i] = 100;
|
||||
}
|
||||
}
|
||||
Ok(result.into_pyarray(py))
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
use numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};
|
||||
use pyo3::prelude::*;
|
||||
|
||||
use super::common::*;
|
||||
|
||||
#[pyfunction]
|
||||
pub fn cdlrickshawman<'py>(
|
||||
py: Python<'py>,
|
||||
open: PyReadonlyArray1<'py, f64>,
|
||||
high: PyReadonlyArray1<'py, f64>,
|
||||
low: PyReadonlyArray1<'py, f64>,
|
||||
close: PyReadonlyArray1<'py, f64>,
|
||||
) -> PyResult<Bound<'py, PyArray1<i32>>> {
|
||||
let opens = open.as_slice()?;
|
||||
let highs = high.as_slice()?;
|
||||
let lows = low.as_slice()?;
|
||||
let closes = close.as_slice()?;
|
||||
let n = opens.len();
|
||||
super::common::validate_ohlc_length(n, highs.len(), lows.len(), closes.len())?;
|
||||
let mut result = vec![0i32; n];
|
||||
for i in 0..n {
|
||||
let (o, h, l, c) = (opens[i], highs[i], lows[i], closes[i]);
|
||||
let range = candle_range(h, l);
|
||||
if range == 0.0 {
|
||||
continue;
|
||||
}
|
||||
let body = body_size(o, c);
|
||||
let us = upper_shadow(o, h, c);
|
||||
let ls = lower_shadow(o, l, c);
|
||||
let body_mid = (o + c) / 2.0;
|
||||
let range_mid = (h + l) / 2.0;
|
||||
let is_doji = body / range <= 0.1;
|
||||
let long_shadows = us >= range * 0.3 && ls >= range * 0.3;
|
||||
let near_center = (body_mid - range_mid).abs() <= range * 0.15;
|
||||
if is_doji && long_shadows && near_center {
|
||||
result[i] = 100;
|
||||
}
|
||||
}
|
||||
Ok(result.into_pyarray(py))
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user