feat: init the repo

This commit is contained in:
Pratik Bhadane
2026-03-23 23:34:28 +05:30
commit 7a5a220dfe
344 changed files with 75728 additions and 0 deletions
+62
View File
@@ -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))
}
+16
View File
@@ -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)
}
+36
View File
@@ -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))
}
+106
View File
@@ -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))
}
+27
View File
@@ -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(())
}
+30
View File
@@ -0,0 +1,30 @@
use crate::validation;
use numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};
use pyo3::exceptions::PyValueError;
use pyo3::prelude::*;
use ta::indicators::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))
}
+26
View File
@@ -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))
}