B9: raise ValueError instead of panicking on non-contiguous arrays
Every Python batch() did prices.as_slice().expect("contiguous"), so a
non-contiguous NumPy input (e.g. a strided view) aborted with a Rust
panic instead of a catchable exception. as_slice() failures now map to a
PyValueError pointing at np.ascontiguousarray; the scalar / MACD /
Bollinger batch methods that returned a bare array were lifted to
PyResult so the error can propagate. Adds input-validation tests
(non-contiguous arrays, unequal-length candle batches, ROC/TRIX
defaults). All 60 Python tests pass against the freshly built wheel.
This commit is contained in:
+176
-75
@@ -31,6 +31,9 @@ fn flatten(values: Vec<Option<f64>>) -> Vec<f64> {
|
|||||||
values.into_iter().map(opt_to_nan).collect()
|
values.into_iter().map(opt_to_nan).collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Raised instead of panicking when a NumPy input is not C-contiguous.
|
||||||
|
const NON_CONTIGUOUS: &str = "array must be C-contiguous; pass np.ascontiguousarray(arr)";
|
||||||
|
|
||||||
// ============================== SMA ==============================
|
// ============================== SMA ==============================
|
||||||
|
|
||||||
#[pyclass(name = "SMA", module = "wickra._wickra")]
|
#[pyclass(name = "SMA", module = "wickra._wickra")]
|
||||||
@@ -54,9 +57,11 @@ impl PySma {
|
|||||||
&mut self,
|
&mut self,
|
||||||
py: Python<'py>,
|
py: Python<'py>,
|
||||||
prices: PyReadonlyArray1<'py, f64>,
|
prices: PyReadonlyArray1<'py, f64>,
|
||||||
) -> Bound<'py, PyArray1<f64>> {
|
) -> PyResult<Bound<'py, PyArray1<f64>>> {
|
||||||
let slice = prices.as_slice().expect("contiguous numpy array");
|
let slice = prices
|
||||||
flatten(self.inner.batch(slice)).into_pyarray_bound(py)
|
.as_slice()
|
||||||
|
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||||
|
Ok(flatten(self.inner.batch(slice)).into_pyarray_bound(py))
|
||||||
}
|
}
|
||||||
#[getter]
|
#[getter]
|
||||||
fn period(&self) -> usize {
|
fn period(&self) -> usize {
|
||||||
@@ -103,9 +108,11 @@ impl PyEma {
|
|||||||
&mut self,
|
&mut self,
|
||||||
py: Python<'py>,
|
py: Python<'py>,
|
||||||
prices: PyReadonlyArray1<'py, f64>,
|
prices: PyReadonlyArray1<'py, f64>,
|
||||||
) -> Bound<'py, PyArray1<f64>> {
|
) -> PyResult<Bound<'py, PyArray1<f64>>> {
|
||||||
let slice = prices.as_slice().expect("contiguous numpy array");
|
let slice = prices
|
||||||
flatten(self.inner.batch(slice)).into_pyarray_bound(py)
|
.as_slice()
|
||||||
|
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||||
|
Ok(flatten(self.inner.batch(slice)).into_pyarray_bound(py))
|
||||||
}
|
}
|
||||||
#[getter]
|
#[getter]
|
||||||
fn period(&self) -> usize {
|
fn period(&self) -> usize {
|
||||||
@@ -156,9 +163,11 @@ impl PyWma {
|
|||||||
&mut self,
|
&mut self,
|
||||||
py: Python<'py>,
|
py: Python<'py>,
|
||||||
prices: PyReadonlyArray1<'py, f64>,
|
prices: PyReadonlyArray1<'py, f64>,
|
||||||
) -> Bound<'py, PyArray1<f64>> {
|
) -> PyResult<Bound<'py, PyArray1<f64>>> {
|
||||||
let slice = prices.as_slice().expect("contiguous numpy array");
|
let slice = prices
|
||||||
flatten(self.inner.batch(slice)).into_pyarray_bound(py)
|
.as_slice()
|
||||||
|
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||||
|
Ok(flatten(self.inner.batch(slice)).into_pyarray_bound(py))
|
||||||
}
|
}
|
||||||
#[getter]
|
#[getter]
|
||||||
fn period(&self) -> usize {
|
fn period(&self) -> usize {
|
||||||
@@ -206,9 +215,11 @@ impl PyRsi {
|
|||||||
&mut self,
|
&mut self,
|
||||||
py: Python<'py>,
|
py: Python<'py>,
|
||||||
prices: PyReadonlyArray1<'py, f64>,
|
prices: PyReadonlyArray1<'py, f64>,
|
||||||
) -> Bound<'py, PyArray1<f64>> {
|
) -> PyResult<Bound<'py, PyArray1<f64>>> {
|
||||||
let slice = prices.as_slice().expect("contiguous numpy array");
|
let slice = prices
|
||||||
flatten(self.inner.batch(slice)).into_pyarray_bound(py)
|
.as_slice()
|
||||||
|
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||||
|
Ok(flatten(self.inner.batch(slice)).into_pyarray_bound(py))
|
||||||
}
|
}
|
||||||
#[getter]
|
#[getter]
|
||||||
fn period(&self) -> usize {
|
fn period(&self) -> usize {
|
||||||
@@ -261,8 +272,10 @@ impl PyMacd {
|
|||||||
&mut self,
|
&mut self,
|
||||||
py: Python<'py>,
|
py: Python<'py>,
|
||||||
prices: PyReadonlyArray1<'py, f64>,
|
prices: PyReadonlyArray1<'py, f64>,
|
||||||
) -> Bound<'py, PyArray2<f64>> {
|
) -> PyResult<Bound<'py, PyArray2<f64>>> {
|
||||||
let slice = prices.as_slice().expect("contiguous numpy array");
|
let slice = prices
|
||||||
|
.as_slice()
|
||||||
|
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||||
let n = slice.len();
|
let n = slice.len();
|
||||||
let mut out = vec![f64::NAN; n * 3];
|
let mut out = vec![f64::NAN; n * 3];
|
||||||
for (i, p) in slice.iter().enumerate() {
|
for (i, p) in slice.iter().enumerate() {
|
||||||
@@ -272,9 +285,9 @@ impl PyMacd {
|
|||||||
out[i * 3 + 2] = o.histogram;
|
out[i * 3 + 2] = o.histogram;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
numpy::ndarray::Array2::from_shape_vec((n, 3), out)
|
Ok(numpy::ndarray::Array2::from_shape_vec((n, 3), out)
|
||||||
.expect("shape consistent")
|
.expect("shape consistent")
|
||||||
.into_pyarray_bound(py)
|
.into_pyarray_bound(py))
|
||||||
}
|
}
|
||||||
#[getter]
|
#[getter]
|
||||||
fn periods(&self) -> (usize, usize, usize) {
|
fn periods(&self) -> (usize, usize, usize) {
|
||||||
@@ -323,8 +336,10 @@ impl PyBb {
|
|||||||
&mut self,
|
&mut self,
|
||||||
py: Python<'py>,
|
py: Python<'py>,
|
||||||
prices: PyReadonlyArray1<'py, f64>,
|
prices: PyReadonlyArray1<'py, f64>,
|
||||||
) -> Bound<'py, PyArray2<f64>> {
|
) -> PyResult<Bound<'py, PyArray2<f64>>> {
|
||||||
let slice = prices.as_slice().expect("contiguous numpy array");
|
let slice = prices
|
||||||
|
.as_slice()
|
||||||
|
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||||
let n = slice.len();
|
let n = slice.len();
|
||||||
let mut out = vec![f64::NAN; n * 4];
|
let mut out = vec![f64::NAN; n * 4];
|
||||||
for (i, p) in slice.iter().enumerate() {
|
for (i, p) in slice.iter().enumerate() {
|
||||||
@@ -335,9 +350,9 @@ impl PyBb {
|
|||||||
out[i * 4 + 3] = o.stddev;
|
out[i * 4 + 3] = o.stddev;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
numpy::ndarray::Array2::from_shape_vec((n, 4), out)
|
Ok(numpy::ndarray::Array2::from_shape_vec((n, 4), out)
|
||||||
.expect("shape consistent")
|
.expect("shape consistent")
|
||||||
.into_pyarray_bound(py)
|
.into_pyarray_bound(py))
|
||||||
}
|
}
|
||||||
#[getter]
|
#[getter]
|
||||||
fn period(&self) -> usize {
|
fn period(&self) -> usize {
|
||||||
@@ -426,9 +441,15 @@ impl PyAtr {
|
|||||||
low: PyReadonlyArray1<'py, f64>,
|
low: PyReadonlyArray1<'py, f64>,
|
||||||
close: PyReadonlyArray1<'py, f64>,
|
close: PyReadonlyArray1<'py, f64>,
|
||||||
) -> PyResult<Bound<'py, PyArray1<f64>>> {
|
) -> PyResult<Bound<'py, PyArray1<f64>>> {
|
||||||
let h = high.as_slice().expect("contiguous");
|
let h = high
|
||||||
let l = low.as_slice().expect("contiguous");
|
.as_slice()
|
||||||
let c = close.as_slice().expect("contiguous");
|
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||||
|
let l = low
|
||||||
|
.as_slice()
|
||||||
|
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||||
|
let c = close
|
||||||
|
.as_slice()
|
||||||
|
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||||
if h.len() != l.len() || l.len() != c.len() {
|
if h.len() != l.len() || l.len() != c.len() {
|
||||||
return Err(PyValueError::new_err(
|
return Err(PyValueError::new_err(
|
||||||
"high, low, close must be equal length",
|
"high, low, close must be equal length",
|
||||||
@@ -488,9 +509,15 @@ impl PyStoch {
|
|||||||
low: PyReadonlyArray1<'py, f64>,
|
low: PyReadonlyArray1<'py, f64>,
|
||||||
close: PyReadonlyArray1<'py, f64>,
|
close: PyReadonlyArray1<'py, f64>,
|
||||||
) -> PyResult<Bound<'py, PyArray2<f64>>> {
|
) -> PyResult<Bound<'py, PyArray2<f64>>> {
|
||||||
let h = high.as_slice().expect("contiguous");
|
let h = high
|
||||||
let l = low.as_slice().expect("contiguous");
|
.as_slice()
|
||||||
let c = close.as_slice().expect("contiguous");
|
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||||
|
let l = low
|
||||||
|
.as_slice()
|
||||||
|
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||||
|
let c = close
|
||||||
|
.as_slice()
|
||||||
|
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||||
if h.len() != l.len() || l.len() != c.len() {
|
if h.len() != l.len() || l.len() != c.len() {
|
||||||
return Err(PyValueError::new_err(
|
return Err(PyValueError::new_err(
|
||||||
"high, low, close must be equal length",
|
"high, low, close must be equal length",
|
||||||
@@ -555,8 +582,12 @@ impl PyObv {
|
|||||||
close: PyReadonlyArray1<'py, f64>,
|
close: PyReadonlyArray1<'py, f64>,
|
||||||
volume: PyReadonlyArray1<'py, f64>,
|
volume: PyReadonlyArray1<'py, f64>,
|
||||||
) -> PyResult<Bound<'py, PyArray1<f64>>> {
|
) -> PyResult<Bound<'py, PyArray1<f64>>> {
|
||||||
let c = close.as_slice().expect("contiguous");
|
let c = close
|
||||||
let v = volume.as_slice().expect("contiguous");
|
.as_slice()
|
||||||
|
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||||
|
let v = volume
|
||||||
|
.as_slice()
|
||||||
|
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||||
if c.len() != v.len() {
|
if c.len() != v.len() {
|
||||||
return Err(PyValueError::new_err(
|
return Err(PyValueError::new_err(
|
||||||
"close and volume must be equal length",
|
"close and volume must be equal length",
|
||||||
@@ -610,9 +641,11 @@ impl PyDema {
|
|||||||
&mut self,
|
&mut self,
|
||||||
py: Python<'py>,
|
py: Python<'py>,
|
||||||
prices: PyReadonlyArray1<'py, f64>,
|
prices: PyReadonlyArray1<'py, f64>,
|
||||||
) -> Bound<'py, PyArray1<f64>> {
|
) -> PyResult<Bound<'py, PyArray1<f64>>> {
|
||||||
let s = prices.as_slice().expect("contiguous");
|
let s = prices
|
||||||
flatten(self.inner.batch(s)).into_pyarray_bound(py)
|
.as_slice()
|
||||||
|
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||||
|
Ok(flatten(self.inner.batch(s)).into_pyarray_bound(py))
|
||||||
}
|
}
|
||||||
#[getter]
|
#[getter]
|
||||||
fn period(&self) -> usize {
|
fn period(&self) -> usize {
|
||||||
@@ -655,9 +688,11 @@ impl PyTema {
|
|||||||
&mut self,
|
&mut self,
|
||||||
py: Python<'py>,
|
py: Python<'py>,
|
||||||
prices: PyReadonlyArray1<'py, f64>,
|
prices: PyReadonlyArray1<'py, f64>,
|
||||||
) -> Bound<'py, PyArray1<f64>> {
|
) -> PyResult<Bound<'py, PyArray1<f64>>> {
|
||||||
let s = prices.as_slice().expect("contiguous");
|
let s = prices
|
||||||
flatten(self.inner.batch(s)).into_pyarray_bound(py)
|
.as_slice()
|
||||||
|
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||||
|
Ok(flatten(self.inner.batch(s)).into_pyarray_bound(py))
|
||||||
}
|
}
|
||||||
#[getter]
|
#[getter]
|
||||||
fn period(&self) -> usize {
|
fn period(&self) -> usize {
|
||||||
@@ -700,9 +735,11 @@ impl PyHma {
|
|||||||
&mut self,
|
&mut self,
|
||||||
py: Python<'py>,
|
py: Python<'py>,
|
||||||
prices: PyReadonlyArray1<'py, f64>,
|
prices: PyReadonlyArray1<'py, f64>,
|
||||||
) -> Bound<'py, PyArray1<f64>> {
|
) -> PyResult<Bound<'py, PyArray1<f64>>> {
|
||||||
let s = prices.as_slice().expect("contiguous");
|
let s = prices
|
||||||
flatten(self.inner.batch(s)).into_pyarray_bound(py)
|
.as_slice()
|
||||||
|
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||||
|
Ok(flatten(self.inner.batch(s)).into_pyarray_bound(py))
|
||||||
}
|
}
|
||||||
#[getter]
|
#[getter]
|
||||||
fn period(&self) -> usize {
|
fn period(&self) -> usize {
|
||||||
@@ -746,9 +783,11 @@ impl PyKama {
|
|||||||
&mut self,
|
&mut self,
|
||||||
py: Python<'py>,
|
py: Python<'py>,
|
||||||
prices: PyReadonlyArray1<'py, f64>,
|
prices: PyReadonlyArray1<'py, f64>,
|
||||||
) -> Bound<'py, PyArray1<f64>> {
|
) -> PyResult<Bound<'py, PyArray1<f64>>> {
|
||||||
let s = prices.as_slice().expect("contiguous");
|
let s = prices
|
||||||
flatten(self.inner.batch(s)).into_pyarray_bound(py)
|
.as_slice()
|
||||||
|
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||||
|
Ok(flatten(self.inner.batch(s)).into_pyarray_bound(py))
|
||||||
}
|
}
|
||||||
fn reset(&mut self) {
|
fn reset(&mut self) {
|
||||||
self.inner.reset();
|
self.inner.reset();
|
||||||
@@ -792,9 +831,15 @@ impl PyCci {
|
|||||||
low: PyReadonlyArray1<'py, f64>,
|
low: PyReadonlyArray1<'py, f64>,
|
||||||
close: PyReadonlyArray1<'py, f64>,
|
close: PyReadonlyArray1<'py, f64>,
|
||||||
) -> PyResult<Bound<'py, PyArray1<f64>>> {
|
) -> PyResult<Bound<'py, PyArray1<f64>>> {
|
||||||
let h = high.as_slice().expect("contiguous");
|
let h = high
|
||||||
let l = low.as_slice().expect("contiguous");
|
.as_slice()
|
||||||
let c = close.as_slice().expect("contiguous");
|
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||||
|
let l = low
|
||||||
|
.as_slice()
|
||||||
|
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||||
|
let c = close
|
||||||
|
.as_slice()
|
||||||
|
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||||
if h.len() != l.len() || l.len() != c.len() {
|
if h.len() != l.len() || l.len() != c.len() {
|
||||||
return Err(PyValueError::new_err(
|
return Err(PyValueError::new_err(
|
||||||
"high, low, close must be equal length",
|
"high, low, close must be equal length",
|
||||||
@@ -849,9 +894,11 @@ impl PyRoc {
|
|||||||
&mut self,
|
&mut self,
|
||||||
py: Python<'py>,
|
py: Python<'py>,
|
||||||
prices: PyReadonlyArray1<'py, f64>,
|
prices: PyReadonlyArray1<'py, f64>,
|
||||||
) -> Bound<'py, PyArray1<f64>> {
|
) -> PyResult<Bound<'py, PyArray1<f64>>> {
|
||||||
let s = prices.as_slice().expect("contiguous");
|
let s = prices
|
||||||
flatten(self.inner.batch(s)).into_pyarray_bound(py)
|
.as_slice()
|
||||||
|
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||||
|
Ok(flatten(self.inner.batch(s)).into_pyarray_bound(py))
|
||||||
}
|
}
|
||||||
#[getter]
|
#[getter]
|
||||||
fn period(&self) -> usize {
|
fn period(&self) -> usize {
|
||||||
@@ -899,9 +946,15 @@ impl PyWilliamsR {
|
|||||||
low: PyReadonlyArray1<'py, f64>,
|
low: PyReadonlyArray1<'py, f64>,
|
||||||
close: PyReadonlyArray1<'py, f64>,
|
close: PyReadonlyArray1<'py, f64>,
|
||||||
) -> PyResult<Bound<'py, PyArray1<f64>>> {
|
) -> PyResult<Bound<'py, PyArray1<f64>>> {
|
||||||
let h = high.as_slice().expect("contiguous");
|
let h = high
|
||||||
let l = low.as_slice().expect("contiguous");
|
.as_slice()
|
||||||
let c = close.as_slice().expect("contiguous");
|
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||||
|
let l = low
|
||||||
|
.as_slice()
|
||||||
|
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||||
|
let c = close
|
||||||
|
.as_slice()
|
||||||
|
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||||
if h.len() != l.len() || l.len() != c.len() {
|
if h.len() != l.len() || l.len() != c.len() {
|
||||||
return Err(PyValueError::new_err(
|
return Err(PyValueError::new_err(
|
||||||
"high, low, close must be equal length",
|
"high, low, close must be equal length",
|
||||||
@@ -955,9 +1008,15 @@ impl PyAdx {
|
|||||||
low: PyReadonlyArray1<'py, f64>,
|
low: PyReadonlyArray1<'py, f64>,
|
||||||
close: PyReadonlyArray1<'py, f64>,
|
close: PyReadonlyArray1<'py, f64>,
|
||||||
) -> PyResult<Bound<'py, PyArray2<f64>>> {
|
) -> PyResult<Bound<'py, PyArray2<f64>>> {
|
||||||
let h = high.as_slice().expect("contiguous");
|
let h = high
|
||||||
let l = low.as_slice().expect("contiguous");
|
.as_slice()
|
||||||
let c = close.as_slice().expect("contiguous");
|
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||||
|
let l = low
|
||||||
|
.as_slice()
|
||||||
|
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||||
|
let c = close
|
||||||
|
.as_slice()
|
||||||
|
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||||
if h.len() != l.len() || l.len() != c.len() {
|
if h.len() != l.len() || l.len() != c.len() {
|
||||||
return Err(PyValueError::new_err(
|
return Err(PyValueError::new_err(
|
||||||
"high, low, close must be equal length",
|
"high, low, close must be equal length",
|
||||||
@@ -1017,10 +1076,18 @@ impl PyMfi {
|
|||||||
close: PyReadonlyArray1<'py, f64>,
|
close: PyReadonlyArray1<'py, f64>,
|
||||||
volume: PyReadonlyArray1<'py, f64>,
|
volume: PyReadonlyArray1<'py, f64>,
|
||||||
) -> PyResult<Bound<'py, PyArray1<f64>>> {
|
) -> PyResult<Bound<'py, PyArray1<f64>>> {
|
||||||
let h = high.as_slice().expect("contiguous");
|
let h = high
|
||||||
let l = low.as_slice().expect("contiguous");
|
.as_slice()
|
||||||
let c = close.as_slice().expect("contiguous");
|
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||||
let v = volume.as_slice().expect("contiguous");
|
let l = low
|
||||||
|
.as_slice()
|
||||||
|
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||||
|
let c = close
|
||||||
|
.as_slice()
|
||||||
|
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||||
|
let v = volume
|
||||||
|
.as_slice()
|
||||||
|
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||||
if h.len() != l.len() || l.len() != c.len() || c.len() != v.len() {
|
if h.len() != l.len() || l.len() != c.len() || c.len() != v.len() {
|
||||||
return Err(PyValueError::new_err(
|
return Err(PyValueError::new_err(
|
||||||
"high, low, close, volume must be equal length",
|
"high, low, close, volume must be equal length",
|
||||||
@@ -1068,9 +1135,11 @@ impl PyTrix {
|
|||||||
&mut self,
|
&mut self,
|
||||||
py: Python<'py>,
|
py: Python<'py>,
|
||||||
prices: PyReadonlyArray1<'py, f64>,
|
prices: PyReadonlyArray1<'py, f64>,
|
||||||
) -> Bound<'py, PyArray1<f64>> {
|
) -> PyResult<Bound<'py, PyArray1<f64>>> {
|
||||||
let s = prices.as_slice().expect("contiguous");
|
let s = prices
|
||||||
flatten(self.inner.batch(s)).into_pyarray_bound(py)
|
.as_slice()
|
||||||
|
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||||
|
Ok(flatten(self.inner.batch(s)).into_pyarray_bound(py))
|
||||||
}
|
}
|
||||||
fn reset(&mut self) {
|
fn reset(&mut self) {
|
||||||
self.inner.reset();
|
self.inner.reset();
|
||||||
@@ -1111,9 +1180,15 @@ impl PyPsar {
|
|||||||
low: PyReadonlyArray1<'py, f64>,
|
low: PyReadonlyArray1<'py, f64>,
|
||||||
close: PyReadonlyArray1<'py, f64>,
|
close: PyReadonlyArray1<'py, f64>,
|
||||||
) -> PyResult<Bound<'py, PyArray1<f64>>> {
|
) -> PyResult<Bound<'py, PyArray1<f64>>> {
|
||||||
let h = high.as_slice().expect("contiguous");
|
let h = high
|
||||||
let l = low.as_slice().expect("contiguous");
|
.as_slice()
|
||||||
let c = close.as_slice().expect("contiguous");
|
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||||
|
let l = low
|
||||||
|
.as_slice()
|
||||||
|
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||||
|
let c = close
|
||||||
|
.as_slice()
|
||||||
|
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||||
if h.len() != l.len() || l.len() != c.len() {
|
if h.len() != l.len() || l.len() != c.len() {
|
||||||
return Err(PyValueError::new_err(
|
return Err(PyValueError::new_err(
|
||||||
"high, low, close must be equal length",
|
"high, low, close must be equal length",
|
||||||
@@ -1167,9 +1242,15 @@ impl PyKeltner {
|
|||||||
low: PyReadonlyArray1<'py, f64>,
|
low: PyReadonlyArray1<'py, f64>,
|
||||||
close: PyReadonlyArray1<'py, f64>,
|
close: PyReadonlyArray1<'py, f64>,
|
||||||
) -> PyResult<Bound<'py, PyArray2<f64>>> {
|
) -> PyResult<Bound<'py, PyArray2<f64>>> {
|
||||||
let h = high.as_slice().expect("contiguous");
|
let h = high
|
||||||
let l = low.as_slice().expect("contiguous");
|
.as_slice()
|
||||||
let c = close.as_slice().expect("contiguous");
|
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||||
|
let l = low
|
||||||
|
.as_slice()
|
||||||
|
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||||
|
let c = close
|
||||||
|
.as_slice()
|
||||||
|
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||||
if h.len() != l.len() || l.len() != c.len() {
|
if h.len() != l.len() || l.len() != c.len() {
|
||||||
return Err(PyValueError::new_err(
|
return Err(PyValueError::new_err(
|
||||||
"high, low, close must be equal length",
|
"high, low, close must be equal length",
|
||||||
@@ -1227,8 +1308,12 @@ impl PyDonchian {
|
|||||||
high: PyReadonlyArray1<'py, f64>,
|
high: PyReadonlyArray1<'py, f64>,
|
||||||
low: PyReadonlyArray1<'py, f64>,
|
low: PyReadonlyArray1<'py, f64>,
|
||||||
) -> PyResult<Bound<'py, PyArray2<f64>>> {
|
) -> PyResult<Bound<'py, PyArray2<f64>>> {
|
||||||
let h = high.as_slice().expect("contiguous");
|
let h = high
|
||||||
let l = low.as_slice().expect("contiguous");
|
.as_slice()
|
||||||
|
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||||
|
let l = low
|
||||||
|
.as_slice()
|
||||||
|
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||||
if h.len() != l.len() {
|
if h.len() != l.len() {
|
||||||
return Err(PyValueError::new_err("high and low must be equal length"));
|
return Err(PyValueError::new_err("high and low must be equal length"));
|
||||||
}
|
}
|
||||||
@@ -1285,10 +1370,18 @@ impl PyVwap {
|
|||||||
close: PyReadonlyArray1<'py, f64>,
|
close: PyReadonlyArray1<'py, f64>,
|
||||||
volume: PyReadonlyArray1<'py, f64>,
|
volume: PyReadonlyArray1<'py, f64>,
|
||||||
) -> PyResult<Bound<'py, PyArray1<f64>>> {
|
) -> PyResult<Bound<'py, PyArray1<f64>>> {
|
||||||
let h = high.as_slice().expect("contiguous");
|
let h = high
|
||||||
let l = low.as_slice().expect("contiguous");
|
.as_slice()
|
||||||
let c = close.as_slice().expect("contiguous");
|
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||||
let v = volume.as_slice().expect("contiguous");
|
let l = low
|
||||||
|
.as_slice()
|
||||||
|
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||||
|
let c = close
|
||||||
|
.as_slice()
|
||||||
|
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||||
|
let v = volume
|
||||||
|
.as_slice()
|
||||||
|
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||||
if h.len() != l.len() || l.len() != c.len() || c.len() != v.len() {
|
if h.len() != l.len() || l.len() != c.len() || c.len() != v.len() {
|
||||||
return Err(PyValueError::new_err(
|
return Err(PyValueError::new_err(
|
||||||
"high, low, close, volume must be equal length",
|
"high, low, close, volume must be equal length",
|
||||||
@@ -1339,8 +1432,12 @@ impl PyAo {
|
|||||||
high: PyReadonlyArray1<'py, f64>,
|
high: PyReadonlyArray1<'py, f64>,
|
||||||
low: PyReadonlyArray1<'py, f64>,
|
low: PyReadonlyArray1<'py, f64>,
|
||||||
) -> PyResult<Bound<'py, PyArray1<f64>>> {
|
) -> PyResult<Bound<'py, PyArray1<f64>>> {
|
||||||
let h = high.as_slice().expect("contiguous");
|
let h = high
|
||||||
let l = low.as_slice().expect("contiguous");
|
.as_slice()
|
||||||
|
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||||
|
let l = low
|
||||||
|
.as_slice()
|
||||||
|
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||||
if h.len() != l.len() {
|
if h.len() != l.len() {
|
||||||
return Err(PyValueError::new_err("high and low must be equal length"));
|
return Err(PyValueError::new_err("high and low must be equal length"));
|
||||||
}
|
}
|
||||||
@@ -1389,8 +1486,12 @@ impl PyAroon {
|
|||||||
high: PyReadonlyArray1<'py, f64>,
|
high: PyReadonlyArray1<'py, f64>,
|
||||||
low: PyReadonlyArray1<'py, f64>,
|
low: PyReadonlyArray1<'py, f64>,
|
||||||
) -> PyResult<Bound<'py, PyArray2<f64>>> {
|
) -> PyResult<Bound<'py, PyArray2<f64>>> {
|
||||||
let h = high.as_slice().expect("contiguous");
|
let h = high
|
||||||
let l = low.as_slice().expect("contiguous");
|
.as_slice()
|
||||||
|
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||||
|
let l = low
|
||||||
|
.as_slice()
|
||||||
|
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||||
if h.len() != l.len() {
|
if h.len() != l.len() {
|
||||||
return Err(PyValueError::new_err("high and low must be equal length"));
|
return Err(PyValueError::new_err("high and low must be equal length"));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
"""Input-validation tests: malformed NumPy inputs raise ValueError, not panics."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
import wickra as ta
|
||||||
|
|
||||||
|
|
||||||
|
def test_non_contiguous_array_raises_value_error():
|
||||||
|
# A strided view is not C-contiguous; batch() must reject it cleanly.
|
||||||
|
base = np.linspace(1.0, 100.0, 60)
|
||||||
|
non_contiguous = base[::2]
|
||||||
|
assert not non_contiguous.flags["C_CONTIGUOUS"]
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
ta.SMA(5).batch(non_contiguous)
|
||||||
|
|
||||||
|
|
||||||
|
def test_ascontiguousarray_recovers():
|
||||||
|
base = np.linspace(1.0, 100.0, 60)
|
||||||
|
fixed = np.ascontiguousarray(base[::2])
|
||||||
|
out = ta.SMA(5).batch(fixed)
|
||||||
|
assert out.shape == fixed.shape
|
||||||
|
|
||||||
|
|
||||||
|
def test_unequal_length_candle_batch_raises(ohlc_series):
|
||||||
|
high, low, close = ohlc_series
|
||||||
|
short = low[:-1]
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
ta.ATR(14).batch(high, short, close)
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
ta.WilliamsR(14).batch(high, short, close)
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
ta.Aroon(14).batch(high, short)
|
||||||
|
|
||||||
|
|
||||||
|
def test_roc_and_trix_have_default_periods():
|
||||||
|
# ROC/TRIX gained constructor defaults matching the TA-Lib convention.
|
||||||
|
assert ta.ROC().period == 10
|
||||||
|
assert ta.TRIX() is not None
|
||||||
Reference in New Issue
Block a user