chore: release v1.0.2
This commit is contained in:
+117
-33
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user