chore: release v1.0.2

This commit is contained in:
Pratik Bhadane
2026-03-24 02:02:10 +05:30
parent 9011250f99
commit 2d5000262f
47 changed files with 3821 additions and 422 deletions
+117 -33
View File
@@ -2,6 +2,45 @@ use crate::validation;
use numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};
use pyo3::prelude::*;
fn price_return(curr: f64, prev: f64) -> f64 {
if prev != 0.0 {
curr / prev - 1.0
} else {
f64::NAN
}
}
fn beta_fallback(x: &[f64], y: &[f64], timeperiod: usize) -> Vec<f64> {
let n = x.len();
let mut result = vec![f64::NAN; n];
for end in timeperiod..n {
let start = end - timeperiod;
let mut rx = vec![0.0_f64; timeperiod];
let mut ry = vec![0.0_f64; timeperiod];
for offset in 0..timeperiod {
let prev = start + offset;
let curr = prev + 1;
rx[offset] = price_return(x[curr], x[prev]);
ry[offset] = price_return(y[curr], y[prev]);
}
let mean_x = rx.iter().sum::<f64>() / timeperiod as f64;
let mean_y = ry.iter().sum::<f64>() / timeperiod as f64;
let cov = rx
.iter()
.zip(ry.iter())
.map(|(&lhs, &rhs)| (lhs - mean_x) * (rhs - mean_y))
.sum::<f64>()
/ timeperiod as f64;
let var_x = rx
.iter()
.map(|&value| (value - mean_x).powi(2))
.sum::<f64>()
/ timeperiod as f64;
result[end] = if var_x != 0.0 { cov / var_x } else { f64::NAN };
}
result
}
/// Beta: regression of *real1* daily returns on *real0* daily returns over a
/// rolling window of *timeperiod* return pairs.
///
@@ -24,39 +63,84 @@ pub fn beta<'py>(
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 };
if x.iter().any(|value| !value.is_finite()) || y.iter().any(|value| !value.is_finite()) {
return Ok(beta_fallback(x, y, timeperiod).into_pyarray(py));
}
let mut result = vec![f64::NAN; n];
if n <= timeperiod {
return Ok(result.into_pyarray(py));
}
let rx: Vec<f64> = x
.windows(2)
.map(|window| price_return(window[1], window[0]))
.collect();
let ry: Vec<f64> = y
.windows(2)
.map(|window| price_return(window[1], window[0]))
.collect();
let period = timeperiod as f64;
let mut invalid_pairs = 0_usize;
let mut sum_rx = 0.0_f64;
let mut sum_ry = 0.0_f64;
let mut sum_rx2 = 0.0_f64;
let mut sum_rxry = 0.0_f64;
for idx in 0..timeperiod {
let ret_x = rx[idx];
let ret_y = ry[idx];
if ret_x.is_finite() && ret_y.is_finite() {
sum_rx += ret_x;
sum_ry += ret_y;
sum_rx2 += ret_x * ret_x;
sum_rxry += ret_x * ret_y;
} else {
invalid_pairs += 1;
}
}
for end in timeperiod..n {
result[end] = if invalid_pairs == 0 {
let denom = period * sum_rx2 - sum_rx * sum_rx;
if denom != 0.0 {
(period * sum_rxry - sum_rx * sum_ry) / denom
} else {
f64::NAN
}
} else {
f64::NAN
};
if end + 1 < n {
let outgoing = end - timeperiod;
let incoming = end;
let outgoing_x = rx[outgoing];
let outgoing_y = ry[outgoing];
if outgoing_x.is_finite() && outgoing_y.is_finite() {
sum_rx -= outgoing_x;
sum_ry -= outgoing_y;
sum_rx2 -= outgoing_x * outgoing_x;
sum_rxry -= outgoing_x * outgoing_y;
} else {
invalid_pairs -= 1;
}
let incoming_x = rx[incoming];
let incoming_y = ry[incoming];
if incoming_x.is_finite() && incoming_y.is_finite() {
sum_rx += incoming_x;
sum_ry += incoming_y;
sum_rx2 += incoming_x * incoming_x;
sum_rxry += incoming_x * incoming_y;
} else {
invalid_pairs += 1;
}
}
}
Ok(result.into_pyarray(py))
}
+54
View File
@@ -14,3 +14,57 @@ pub(super) fn linreg(window: &[f64]) -> (f64, f64) {
let intercept = (sum_y - slope * sum_x) / n;
(slope, intercept)
}
pub(crate) fn rolling_linreg_apply<F>(prices: &[f64], timeperiod: usize, mut map: F) -> Vec<f64>
where
F: FnMut(f64, f64) -> f64,
{
let n = prices.len();
let mut result = vec![f64::NAN; n];
if timeperiod == 0 || n < timeperiod {
return result;
}
if prices.iter().any(|value| !value.is_finite()) {
for end in (timeperiod - 1)..n {
let window = &prices[(end + 1 - timeperiod)..=end];
let (slope, intercept) = linreg(window);
result[end] = map(slope, intercept);
}
return result;
}
let period = timeperiod as f64;
let last_x = (timeperiod - 1) as f64;
let sum_x = last_x * period / 2.0;
let sum_x2 = last_x * period * (2.0 * period - 1.0) / 6.0;
let denom = period * sum_x2 - sum_x * sum_x;
let mut sum_y = prices[..timeperiod].iter().sum::<f64>();
let mut sum_xy = prices[..timeperiod]
.iter()
.enumerate()
.map(|(idx, &value)| idx as f64 * value)
.sum::<f64>();
for end in (timeperiod - 1)..n {
let slope = if denom != 0.0 {
(period * sum_xy - sum_x * sum_y) / denom
} else {
0.0
};
let intercept = (sum_y - slope * sum_x) / period;
result[end] = map(slope, intercept);
if end + 1 < n {
let outgoing = prices[end + 1 - timeperiod];
let incoming = prices[end + 1];
let prev_sum_y = sum_y;
sum_y = prev_sum_y - outgoing + incoming;
sum_xy = sum_xy - (prev_sum_y - outgoing) + last_x * incoming;
}
}
result
}
+79 -14
View File
@@ -2,6 +2,35 @@ use crate::validation;
use numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};
use pyo3::prelude::*;
fn correl_fallback(x: &[f64], y: &[f64], timeperiod: usize) -> Vec<f64> {
let n = x.len();
let mut result = vec![f64::NAN; n];
for end in (timeperiod - 1)..n {
let wx = &x[(end + 1 - timeperiod)..=end];
let wy = &y[(end + 1 - timeperiod)..=end];
let mean_x = wx.iter().sum::<f64>() / timeperiod as f64;
let mean_y = wy.iter().sum::<f64>() / timeperiod as f64;
let cov = wx
.iter()
.zip(wy.iter())
.map(|(&xi, &yi)| (xi - mean_x) * (yi - mean_y))
.sum::<f64>();
let std_x = wx
.iter()
.map(|&xi| (xi - mean_x).powi(2))
.sum::<f64>()
.sqrt();
let std_y = wy
.iter()
.map(|&yi| (yi - mean_y).powi(2))
.sum::<f64>()
.sqrt();
let denom = std_x * std_y;
result[end] = if denom != 0.0 { cov / denom } else { f64::NAN };
}
result
}
/// Pearson correlation coefficient between two series over the rolling window.
#[pyfunction]
#[pyo3(signature = (real0, real1, timeperiod = 30))]
@@ -16,21 +45,57 @@ pub fn correl<'py>(
let y = real1.as_slice()?;
let n = x.len();
validation::validate_equal_length(&[(n, "real0"), (y.len(), "real1")])?;
if x.iter().any(|value| !value.is_finite()) || y.iter().any(|value| !value.is_finite()) {
return Ok(correl_fallback(x, y, timeperiod).into_pyarray(py));
}
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 };
if n < timeperiod {
return Ok(result.into_pyarray(py));
}
let period = timeperiod as f64;
let mut sum_x = x[..timeperiod].iter().sum::<f64>();
let mut sum_y = y[..timeperiod].iter().sum::<f64>();
let mut sum_x2 = x[..timeperiod]
.iter()
.map(|value| value * value)
.sum::<f64>();
let mut sum_y2 = y[..timeperiod]
.iter()
.map(|value| value * value)
.sum::<f64>();
let mut sum_xy = x[..timeperiod]
.iter()
.zip(y[..timeperiod].iter())
.map(|(&lhs, &rhs)| lhs * rhs)
.sum::<f64>();
for end in (timeperiod - 1)..n {
let denom_x = period * sum_x2 - sum_x * sum_x;
let denom_y = period * sum_y2 - sum_y * sum_y;
result[end] = if denom_x > 0.0 && denom_y > 0.0 {
(period * sum_xy - sum_x * sum_y) / (denom_x * denom_y).sqrt()
} else {
f64::NAN
};
if end + 1 < n {
let outgoing = end + 1 - timeperiod;
let incoming = end + 1;
let outgoing_x = x[outgoing];
let outgoing_y = y[outgoing];
let incoming_x = x[incoming];
let incoming_y = y[incoming];
sum_x += incoming_x - outgoing_x;
sum_y += incoming_y - outgoing_y;
sum_x2 += incoming_x * incoming_x - outgoing_x * outgoing_x;
sum_y2 += incoming_y * incoming_y - outgoing_y * outgoing_y;
sum_xy += incoming_x * incoming_y - outgoing_x * outgoing_y;
}
}
Ok(result.into_pyarray(py))
}
+12 -37
View File
@@ -1,4 +1,4 @@
use super::common::linreg;
use super::common::rolling_linreg_apply;
use crate::validation;
use numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};
use pyo3::prelude::*;
@@ -14,13 +14,10 @@ pub fn linearreg<'py>(
) -> 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;
}
let last_x = (timeperiod - 1) as f64;
let result = rolling_linreg_apply(prices, timeperiod, |slope, intercept| {
intercept + slope * last_x
});
Ok(result.into_pyarray(py))
}
@@ -34,13 +31,7 @@ pub fn linearreg_slope<'py>(
) -> 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;
}
let result = rolling_linreg_apply(prices, timeperiod, |slope, _| slope);
Ok(result.into_pyarray(py))
}
@@ -54,13 +45,7 @@ pub fn linearreg_intercept<'py>(
) -> 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;
}
let result = rolling_linreg_apply(prices, timeperiod, |_, intercept| intercept);
Ok(result.into_pyarray(py))
}
@@ -74,13 +59,7 @@ pub fn linearreg_angle<'py>(
) -> 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;
}
let result = rolling_linreg_apply(prices, timeperiod, |slope, _| slope.atan() * 180.0 / PI);
Ok(result.into_pyarray(py))
}
@@ -94,13 +73,9 @@ pub fn tsf<'py>(
) -> 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;
}
let forecast_x = timeperiod as f64;
let result = rolling_linreg_apply(prices, timeperiod, |slope, intercept| {
intercept + slope * forecast_x
});
Ok(result.into_pyarray(py))
}
+1 -1
View File
@@ -2,7 +2,7 @@
//! Each function (or closely related group) lives in its own file.
mod beta;
mod common;
pub(crate) mod common;
mod correl;
mod linearreg;
mod stddev;