feat: init the repo
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
use crate::validation;
|
||||
use numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};
|
||||
use pyo3::prelude::*;
|
||||
|
||||
/// Beta: regression of *real1* daily returns on *real0* daily returns over a
|
||||
/// rolling window of *timeperiod* return pairs.
|
||||
///
|
||||
/// Matches TA-Lib's algorithm:
|
||||
/// - For bar *i* (output index *i*): use `timeperiod` pairs of consecutive
|
||||
/// price returns from the window ending at bar *i*.
|
||||
/// - Return for bar t: r_x[t] = x[t]/x[t-1] - 1 (similarly for y).
|
||||
/// - beta = Cov(r_y, r_x) / Var(r_x) (sample, divided by timeperiod).
|
||||
/// - First valid output is at index `timeperiod` (needs `timeperiod+1` bars).
|
||||
#[pyfunction]
|
||||
#[pyo3(signature = (real0, real1, timeperiod = 5))]
|
||||
pub fn beta<'py>(
|
||||
py: Python<'py>,
|
||||
real0: PyReadonlyArray1<'py, f64>,
|
||||
real1: PyReadonlyArray1<'py, f64>,
|
||||
timeperiod: usize,
|
||||
) -> PyResult<Bound<'py, PyArray1<f64>>> {
|
||||
validation::validate_timeperiod(timeperiod, "timeperiod", 1)?;
|
||||
let x = real0.as_slice()?;
|
||||
let y = real1.as_slice()?;
|
||||
let n = x.len();
|
||||
validation::validate_equal_length(&[(n, "real0"), (y.len(), "real1")])?;
|
||||
let mut result = vec![f64::NAN; n];
|
||||
// Need at least timeperiod+1 bars to compute timeperiod return pairs
|
||||
#[allow(clippy::needless_range_loop)]
|
||||
for i in timeperiod..n {
|
||||
// returns from bar (i - timeperiod) to bar i => timeperiod pairs
|
||||
let start = i - timeperiod;
|
||||
let mut rx = vec![0.0_f64; timeperiod];
|
||||
let mut ry = vec![0.0_f64; timeperiod];
|
||||
for k in 0..timeperiod {
|
||||
let prev = start + k;
|
||||
let curr = start + k + 1;
|
||||
rx[k] = if x[prev] != 0.0 {
|
||||
x[curr] / x[prev] - 1.0
|
||||
} else {
|
||||
f64::NAN
|
||||
};
|
||||
ry[k] = if y[prev] != 0.0 {
|
||||
y[curr] / y[prev] - 1.0
|
||||
} else {
|
||||
f64::NAN
|
||||
};
|
||||
}
|
||||
let mean_x: f64 = rx.iter().sum::<f64>() / timeperiod as f64;
|
||||
let mean_y: f64 = ry.iter().sum::<f64>() / timeperiod as f64;
|
||||
let cov: f64 = rx
|
||||
.iter()
|
||||
.zip(ry.iter())
|
||||
.map(|(&xi, &yi)| (xi - mean_x) * (yi - mean_y))
|
||||
.sum::<f64>()
|
||||
/ timeperiod as f64;
|
||||
let var_x: f64 =
|
||||
rx.iter().map(|&xi| (xi - mean_x).powi(2)).sum::<f64>() / timeperiod as f64;
|
||||
result[i] = if var_x != 0.0 { cov / var_x } else { f64::NAN };
|
||||
}
|
||||
Ok(result.into_pyarray(py))
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
/// Rolling linear regression: returns (slope, intercept) for the given window.
|
||||
pub(super) fn linreg(window: &[f64]) -> (f64, f64) {
|
||||
let n = window.len() as f64;
|
||||
let sum_x: f64 = (0..window.len()).map(|i| i as f64).sum();
|
||||
let sum_y: f64 = window.iter().sum();
|
||||
let sum_xy: f64 = window.iter().enumerate().map(|(i, &y)| i as f64 * y).sum();
|
||||
let sum_x2: f64 = (0..window.len()).map(|i| (i as f64).powi(2)).sum();
|
||||
let denom = n * sum_x2 - sum_x * sum_x;
|
||||
let slope = if denom != 0.0 {
|
||||
(n * sum_xy - sum_x * sum_y) / denom
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
let intercept = (sum_y - slope * sum_x) / n;
|
||||
(slope, intercept)
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
use crate::validation;
|
||||
use numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};
|
||||
use pyo3::prelude::*;
|
||||
|
||||
/// Pearson correlation coefficient between two series over the rolling window.
|
||||
#[pyfunction]
|
||||
#[pyo3(signature = (real0, real1, timeperiod = 30))]
|
||||
pub fn correl<'py>(
|
||||
py: Python<'py>,
|
||||
real0: PyReadonlyArray1<'py, f64>,
|
||||
real1: PyReadonlyArray1<'py, f64>,
|
||||
timeperiod: usize,
|
||||
) -> PyResult<Bound<'py, PyArray1<f64>>> {
|
||||
validation::validate_timeperiod(timeperiod, "timeperiod", 1)?;
|
||||
let x = real0.as_slice()?;
|
||||
let y = real1.as_slice()?;
|
||||
let n = x.len();
|
||||
validation::validate_equal_length(&[(n, "real0"), (y.len(), "real1")])?;
|
||||
let mut result = vec![f64::NAN; n];
|
||||
for i in (timeperiod - 1)..n {
|
||||
let wx = &x[(i + 1 - timeperiod)..=i];
|
||||
let wy = &y[(i + 1 - timeperiod)..=i];
|
||||
let mean_x: f64 = wx.iter().sum::<f64>() / timeperiod as f64;
|
||||
let mean_y: f64 = wy.iter().sum::<f64>() / timeperiod as f64;
|
||||
let cov: f64 = wx
|
||||
.iter()
|
||||
.zip(wy.iter())
|
||||
.map(|(&xi, &yi)| (xi - mean_x) * (yi - mean_y))
|
||||
.sum::<f64>();
|
||||
let std_x: f64 = (wx.iter().map(|&xi| (xi - mean_x).powi(2)).sum::<f64>()).sqrt();
|
||||
let std_y: f64 = (wy.iter().map(|&yi| (yi - mean_y).powi(2)).sum::<f64>()).sqrt();
|
||||
let denom = std_x * std_y;
|
||||
result[i] = if denom != 0.0 { cov / denom } else { f64::NAN };
|
||||
}
|
||||
Ok(result.into_pyarray(py))
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
use super::common::linreg;
|
||||
use crate::validation;
|
||||
use numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};
|
||||
use pyo3::prelude::*;
|
||||
use std::f64::consts::PI;
|
||||
|
||||
/// Linear regression fitted value at the last point of the window.
|
||||
#[pyfunction]
|
||||
#[pyo3(signature = (close, timeperiod = 14))]
|
||||
pub fn linearreg<'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 - 1)..n {
|
||||
let window = &prices[(i + 1 - timeperiod)..=i];
|
||||
let (slope, intercept) = linreg(window);
|
||||
result[i] = intercept + slope * (timeperiod - 1) as f64;
|
||||
}
|
||||
Ok(result.into_pyarray(py))
|
||||
}
|
||||
|
||||
/// Slope of the rolling linear regression line.
|
||||
#[pyfunction]
|
||||
#[pyo3(signature = (close, timeperiod = 14))]
|
||||
pub fn linearreg_slope<'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 - 1)..n {
|
||||
let window = &prices[(i + 1 - timeperiod)..=i];
|
||||
let (slope, _) = linreg(window);
|
||||
result[i] = slope;
|
||||
}
|
||||
Ok(result.into_pyarray(py))
|
||||
}
|
||||
|
||||
/// Intercept of the rolling linear regression line.
|
||||
#[pyfunction]
|
||||
#[pyo3(signature = (close, timeperiod = 14))]
|
||||
pub fn linearreg_intercept<'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 - 1)..n {
|
||||
let window = &prices[(i + 1 - timeperiod)..=i];
|
||||
let (_, intercept) = linreg(window);
|
||||
result[i] = intercept;
|
||||
}
|
||||
Ok(result.into_pyarray(py))
|
||||
}
|
||||
|
||||
/// Angle of the regression line in degrees (atan(slope) * 180/π).
|
||||
#[pyfunction]
|
||||
#[pyo3(signature = (close, timeperiod = 14))]
|
||||
pub fn linearreg_angle<'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 - 1)..n {
|
||||
let window = &prices[(i + 1 - timeperiod)..=i];
|
||||
let (slope, _) = linreg(window);
|
||||
result[i] = slope.atan() * 180.0 / PI;
|
||||
}
|
||||
Ok(result.into_pyarray(py))
|
||||
}
|
||||
|
||||
/// Time series forecast: linear regression extrapolated one period ahead.
|
||||
#[pyfunction]
|
||||
#[pyo3(signature = (close, timeperiod = 14))]
|
||||
pub fn tsf<'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 - 1)..n {
|
||||
let window = &prices[(i + 1 - timeperiod)..=i];
|
||||
let (slope, intercept) = linreg(window);
|
||||
// Forecast one period ahead of the last point in the window
|
||||
result[i] = intercept + slope * timeperiod as f64;
|
||||
}
|
||||
Ok(result.into_pyarray(py))
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
//! Statistic functions — rolling window statistical operations on price data.
|
||||
//! Each function (or closely related group) lives in its own file.
|
||||
|
||||
mod beta;
|
||||
mod common;
|
||||
mod correl;
|
||||
mod linearreg;
|
||||
mod stddev;
|
||||
mod var;
|
||||
|
||||
use pyo3::prelude::*;
|
||||
|
||||
pub fn register(m: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||
m.add_function(pyo3::wrap_pyfunction!(self::stddev::stddev, m)?)?;
|
||||
m.add_function(pyo3::wrap_pyfunction!(self::var::var, m)?)?;
|
||||
m.add_function(pyo3::wrap_pyfunction!(self::linearreg::linearreg, m)?)?;
|
||||
m.add_function(pyo3::wrap_pyfunction!(self::linearreg::linearreg_slope, m)?)?;
|
||||
m.add_function(pyo3::wrap_pyfunction!(
|
||||
self::linearreg::linearreg_intercept,
|
||||
m
|
||||
)?)?;
|
||||
m.add_function(pyo3::wrap_pyfunction!(self::linearreg::linearreg_angle, m)?)?;
|
||||
m.add_function(pyo3::wrap_pyfunction!(self::linearreg::tsf, m)?)?;
|
||||
m.add_function(pyo3::wrap_pyfunction!(self::beta::beta, m)?)?;
|
||||
m.add_function(pyo3::wrap_pyfunction!(self::correl::correl, m)?)?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
use crate::validation;
|
||||
use numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};
|
||||
use pyo3::exceptions::PyValueError;
|
||||
use pyo3::prelude::*;
|
||||
use ta::indicators::StandardDeviation;
|
||||
use ta::Next;
|
||||
|
||||
/// Standard deviation over a rolling window; scaled by nbdev (default 1.0).
|
||||
#[pyfunction]
|
||||
#[pyo3(signature = (close, timeperiod = 5, nbdev = 1.0))]
|
||||
pub fn stddev<'py>(
|
||||
py: Python<'py>,
|
||||
close: PyReadonlyArray1<'py, f64>,
|
||||
timeperiod: usize,
|
||||
nbdev: f64,
|
||||
) -> PyResult<Bound<'py, PyArray1<f64>>> {
|
||||
validation::validate_timeperiod(timeperiod, "timeperiod", 1)?;
|
||||
let prices = close.as_slice()?;
|
||||
let n = prices.len();
|
||||
let mut indicator =
|
||||
StandardDeviation::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 + 1 >= timeperiod {
|
||||
result[i] = val * nbdev;
|
||||
}
|
||||
}
|
||||
Ok(result.into_pyarray(py))
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
use crate::validation;
|
||||
use numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};
|
||||
use pyo3::prelude::*;
|
||||
|
||||
/// Rolling variance; scaled by nbdev².
|
||||
#[pyfunction]
|
||||
#[pyo3(signature = (close, timeperiod = 5, nbdev = 1.0))]
|
||||
pub fn var<'py>(
|
||||
py: Python<'py>,
|
||||
close: PyReadonlyArray1<'py, f64>,
|
||||
timeperiod: usize,
|
||||
nbdev: f64,
|
||||
) -> 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 - 1)..n {
|
||||
let window = &prices[(i + 1 - timeperiod)..=i];
|
||||
let mean: f64 = window.iter().sum::<f64>() / timeperiod as f64;
|
||||
let variance: f64 =
|
||||
window.iter().map(|x| (x - mean).powi(2)).sum::<f64>() / timeperiod as f64;
|
||||
result[i] = variance * nbdev * nbdev;
|
||||
}
|
||||
Ok(result.into_pyarray(py))
|
||||
}
|
||||
Reference in New Issue
Block a user