feat: add 7 alt-chart bar builders (B19) (#220)
Adds seven information-driven bar builders to the **Alt-Chart Bars** family, the final batch of the family-deepening run. Indicator count **507 → 514**. ## Builders All implement the `BarBuilder` trait (`update(Candle) -> Vec<Bar>`), emitting a data-dependent number of completed bars per candle. | Builder | Driver | Bar fields | |---------|--------|-----------| | `RangeBars` | close | open, close, direction | | `TickBars` | OHLCV | open, high, low, close, volume | | `VolumeBars` | OHLCV | open, high, low, close, volume | | `DollarBars` (Lopez de Prado) | OHLCV | + dollar | | `ImbalanceBars` | OHLC | + imbalance, direction | | `RunBars` | OHLC | + length, direction | | `ThreeLineBreakBars` | close | open, close, direction | ## Touchpoints Seven core modules (each with full unit tests), `mod.rs`/`lib.rs` (builders counted, bar element types on their own re-export lines), README family rows, Python/Node/WASM hand-written bindings for the variable-length output (Python tuples + `(k, N)` ndarray; Node `Vec<object>`; WASM array of objects), the `bar_builder_update_candle` fuzz target, dedicated Python + Node tests, the `BAR_BUILDERS` completeness exclusion, and CHANGELOG. ## Verification - `cargo test -p wickra-core --lib` — 4207 passed - `cargo test -p wickra-core --doc` — 464 passed - `cargo clippy --workspace --all-targets --all-features -- -D warnings` — clean - `npm test` (node) — 584 passed - `pytest` (python) — 957 passed
This commit is contained in:
@@ -370,6 +370,13 @@ from ._wickra import (
|
||||
InitialBalance,
|
||||
OpeningRange,
|
||||
# Alt-Chart Bars
|
||||
ThreeLineBreakBars,
|
||||
RunBars,
|
||||
ImbalanceBars,
|
||||
DollarBars,
|
||||
VolumeBars,
|
||||
TickBars,
|
||||
RangeBars,
|
||||
RenkoBars,
|
||||
KagiBars,
|
||||
PointAndFigureBars,
|
||||
@@ -907,6 +914,13 @@ __all__ = [
|
||||
"InitialBalance",
|
||||
"OpeningRange",
|
||||
# Alt-Chart Bars
|
||||
"ThreeLineBreakBars",
|
||||
"RunBars",
|
||||
"ImbalanceBars",
|
||||
"DollarBars",
|
||||
"VolumeBars",
|
||||
"TickBars",
|
||||
"RangeBars",
|
||||
"RenkoBars",
|
||||
"KagiBars",
|
||||
"PointAndFigureBars",
|
||||
|
||||
@@ -38,6 +38,67 @@ fn map_err(e: wc::Error) -> PyErr {
|
||||
/// 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)";
|
||||
|
||||
/// Borrowed `(open, high, low, close)` columns for candle-driven bar builders.
|
||||
type OhlcCols<'a> = (&'a [f64], &'a [f64], &'a [f64], &'a [f64]);
|
||||
/// Borrowed `(open, high, low, close, volume)` columns for candle-driven bar builders.
|
||||
type OhlcvCols<'a> = (&'a [f64], &'a [f64], &'a [f64], &'a [f64], &'a [f64]);
|
||||
|
||||
/// `(open, high, low, close, volume)` rows from Tick/Volume bar builders.
|
||||
type OhlcvBarRows = Vec<(f64, f64, f64, f64, f64)>;
|
||||
/// `(open, high, low, close, volume, dollar)` rows from the Dollar bar builder.
|
||||
type DollarBarRows = Vec<(f64, f64, f64, f64, f64, f64)>;
|
||||
/// `(open, high, low, close, imbalance, direction)` rows from the Imbalance bar builder.
|
||||
type ImbalanceBarRows = Vec<(f64, f64, f64, f64, f64, i64)>;
|
||||
/// `(open, high, low, close, length, direction)` rows from the Run bar builder.
|
||||
type RunBarRows = Vec<(f64, f64, f64, f64, i64, i64)>;
|
||||
|
||||
/// Extract four equal-length OHLC slices, erroring on non-contiguous or mismatched input.
|
||||
fn ohlc_slices<'a, 'py>(
|
||||
open: &'a PyReadonlyArray1<'py, f64>,
|
||||
high: &'a PyReadonlyArray1<'py, f64>,
|
||||
low: &'a PyReadonlyArray1<'py, f64>,
|
||||
close: &'a PyReadonlyArray1<'py, f64>,
|
||||
) -> PyResult<OhlcCols<'a>> {
|
||||
let o = open
|
||||
.as_slice()
|
||||
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||
let h = high
|
||||
.as_slice()
|
||||
.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 o.len() != h.len() || h.len() != l.len() || l.len() != c.len() {
|
||||
return Err(PyValueError::new_err(
|
||||
"open, high, low, close must be equal length",
|
||||
));
|
||||
}
|
||||
Ok((o, h, l, c))
|
||||
}
|
||||
|
||||
/// Extract five equal-length OHLCV slices, erroring on non-contiguous or mismatched input.
|
||||
fn ohlcv_slices<'a, 'py>(
|
||||
open: &'a PyReadonlyArray1<'py, f64>,
|
||||
high: &'a PyReadonlyArray1<'py, f64>,
|
||||
low: &'a PyReadonlyArray1<'py, f64>,
|
||||
close: &'a PyReadonlyArray1<'py, f64>,
|
||||
volume: &'a PyReadonlyArray1<'py, f64>,
|
||||
) -> PyResult<OhlcvCols<'a>> {
|
||||
let (o, h, l, c) = ohlc_slices(open, high, low, close)?;
|
||||
let v = volume
|
||||
.as_slice()
|
||||
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||
if v.len() != o.len() {
|
||||
return Err(PyValueError::new_err(
|
||||
"open, high, low, close, volume must be equal length",
|
||||
));
|
||||
}
|
||||
Ok((o, h, l, c, v))
|
||||
}
|
||||
|
||||
/// `(pp, r1, r2, r3, s1, s2, s3)` pivot levels returned by Classic/Fibonacci pivots.
|
||||
type PivotLevels = (f64, f64, f64, f64, f64, f64, f64);
|
||||
/// The five Fibonacci-extension levels returned by `FibExtension`.
|
||||
@@ -23459,6 +23520,486 @@ impl PyPointAndFigureBars {
|
||||
}
|
||||
}
|
||||
|
||||
#[pyclass(name = "RangeBars", module = "wickra._wickra", skip_from_py_object)]
|
||||
#[derive(Clone)]
|
||||
struct PyRangeBars {
|
||||
inner: wc::RangeBars,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyRangeBars {
|
||||
#[new]
|
||||
fn new(range: f64) -> PyResult<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::RangeBars::new(range).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
/// Feed one close; returns bars completed on it as `(open, close, direction)`.
|
||||
fn update(&mut self, close: f64) -> PyResult<Vec<(f64, f64, i64)>> {
|
||||
let candle = wc::Candle::new(close, close, close, close, 1.0, 0).map_err(map_err)?;
|
||||
Ok(self
|
||||
.inner
|
||||
.update(candle)
|
||||
.into_iter()
|
||||
.map(|b| (b.open, b.close, i64::from(b.direction)))
|
||||
.collect())
|
||||
}
|
||||
/// Batch over a close column. Returns shape `(k, 3)` of `[open, close, direction]`.
|
||||
fn batch<'py>(
|
||||
&mut self,
|
||||
py: Python<'py>,
|
||||
close: PyReadonlyArray1<'py, f64>,
|
||||
) -> PyResult<Bound<'py, PyArray2<f64>>> {
|
||||
let prices = close
|
||||
.as_slice()
|
||||
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||
let mut rows: Vec<f64> = Vec::new();
|
||||
let mut k = 0usize;
|
||||
for &price in prices {
|
||||
let candle = wc::Candle::new(price, price, price, price, 1.0, 0).map_err(map_err)?;
|
||||
for b in self.inner.update(candle) {
|
||||
rows.push(b.open);
|
||||
rows.push(b.close);
|
||||
rows.push(f64::from(b.direction));
|
||||
k += 1;
|
||||
}
|
||||
}
|
||||
Ok(numpy::ndarray::Array2::from_shape_vec((k, 3), rows)
|
||||
.expect("shape consistent")
|
||||
.into_pyarray(py))
|
||||
}
|
||||
#[getter]
|
||||
fn range(&self) -> f64 {
|
||||
self.inner.range()
|
||||
}
|
||||
fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
fn __repr__(&self) -> String {
|
||||
format!("RangeBars(range={})", self.inner.range())
|
||||
}
|
||||
}
|
||||
|
||||
#[pyclass(name = "TickBars", module = "wickra._wickra", skip_from_py_object)]
|
||||
#[derive(Clone)]
|
||||
struct PyTickBars {
|
||||
inner: wc::TickBars,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyTickBars {
|
||||
#[new]
|
||||
fn new(ticks: usize) -> PyResult<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::TickBars::new(ticks).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
/// Feed one candle; returns bars completed as `(open, high, low, close, volume)`.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn update(
|
||||
&mut self,
|
||||
open: f64,
|
||||
high: f64,
|
||||
low: f64,
|
||||
close: f64,
|
||||
volume: f64,
|
||||
) -> PyResult<OhlcvBarRows> {
|
||||
let candle = wc::Candle::new(open, high, low, close, volume, 0).map_err(map_err)?;
|
||||
Ok(self
|
||||
.inner
|
||||
.update(candle)
|
||||
.into_iter()
|
||||
.map(|b| (b.open, b.high, b.low, b.close, b.volume))
|
||||
.collect())
|
||||
}
|
||||
/// Batch over OHLCV columns. Returns shape `(k, 5)` of `[open, high, low, close, volume]`.
|
||||
fn batch<'py>(
|
||||
&mut self,
|
||||
py: Python<'py>,
|
||||
open: PyReadonlyArray1<'py, f64>,
|
||||
high: PyReadonlyArray1<'py, f64>,
|
||||
low: PyReadonlyArray1<'py, f64>,
|
||||
close: PyReadonlyArray1<'py, f64>,
|
||||
volume: PyReadonlyArray1<'py, f64>,
|
||||
) -> PyResult<Bound<'py, PyArray2<f64>>> {
|
||||
let (o, h, l, c, v) = ohlcv_slices(&open, &high, &low, &close, &volume)?;
|
||||
let mut rows: Vec<f64> = Vec::new();
|
||||
let mut k = 0usize;
|
||||
for i in 0..o.len() {
|
||||
let candle = wc::Candle::new(o[i], h[i], l[i], c[i], v[i], 0).map_err(map_err)?;
|
||||
for b in self.inner.update(candle) {
|
||||
rows.extend_from_slice(&[b.open, b.high, b.low, b.close, b.volume]);
|
||||
k += 1;
|
||||
}
|
||||
}
|
||||
Ok(numpy::ndarray::Array2::from_shape_vec((k, 5), rows)
|
||||
.expect("shape consistent")
|
||||
.into_pyarray(py))
|
||||
}
|
||||
#[getter]
|
||||
fn ticks(&self) -> usize {
|
||||
self.inner.ticks()
|
||||
}
|
||||
fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
fn __repr__(&self) -> String {
|
||||
format!("TickBars(ticks={})", self.inner.ticks())
|
||||
}
|
||||
}
|
||||
|
||||
#[pyclass(name = "VolumeBars", module = "wickra._wickra", skip_from_py_object)]
|
||||
#[derive(Clone)]
|
||||
struct PyVolumeBars {
|
||||
inner: wc::VolumeBars,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyVolumeBars {
|
||||
#[new]
|
||||
fn new(volume_per_bar: f64) -> PyResult<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::VolumeBars::new(volume_per_bar).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
/// Feed one candle; returns bars completed as `(open, high, low, close, volume)`.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn update(
|
||||
&mut self,
|
||||
open: f64,
|
||||
high: f64,
|
||||
low: f64,
|
||||
close: f64,
|
||||
volume: f64,
|
||||
) -> PyResult<OhlcvBarRows> {
|
||||
let candle = wc::Candle::new(open, high, low, close, volume, 0).map_err(map_err)?;
|
||||
Ok(self
|
||||
.inner
|
||||
.update(candle)
|
||||
.into_iter()
|
||||
.map(|b| (b.open, b.high, b.low, b.close, b.volume))
|
||||
.collect())
|
||||
}
|
||||
/// Batch over OHLCV columns. Returns shape `(k, 5)` of `[open, high, low, close, volume]`.
|
||||
fn batch<'py>(
|
||||
&mut self,
|
||||
py: Python<'py>,
|
||||
open: PyReadonlyArray1<'py, f64>,
|
||||
high: PyReadonlyArray1<'py, f64>,
|
||||
low: PyReadonlyArray1<'py, f64>,
|
||||
close: PyReadonlyArray1<'py, f64>,
|
||||
volume: PyReadonlyArray1<'py, f64>,
|
||||
) -> PyResult<Bound<'py, PyArray2<f64>>> {
|
||||
let (o, h, l, c, v) = ohlcv_slices(&open, &high, &low, &close, &volume)?;
|
||||
let mut rows: Vec<f64> = Vec::new();
|
||||
let mut k = 0usize;
|
||||
for i in 0..o.len() {
|
||||
let candle = wc::Candle::new(o[i], h[i], l[i], c[i], v[i], 0).map_err(map_err)?;
|
||||
for b in self.inner.update(candle) {
|
||||
rows.extend_from_slice(&[b.open, b.high, b.low, b.close, b.volume]);
|
||||
k += 1;
|
||||
}
|
||||
}
|
||||
Ok(numpy::ndarray::Array2::from_shape_vec((k, 5), rows)
|
||||
.expect("shape consistent")
|
||||
.into_pyarray(py))
|
||||
}
|
||||
#[getter]
|
||||
fn volume_per_bar(&self) -> f64 {
|
||||
self.inner.volume_per_bar()
|
||||
}
|
||||
fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
fn __repr__(&self) -> String {
|
||||
format!("VolumeBars(volume_per_bar={})", self.inner.volume_per_bar())
|
||||
}
|
||||
}
|
||||
|
||||
#[pyclass(name = "DollarBars", module = "wickra._wickra", skip_from_py_object)]
|
||||
#[derive(Clone)]
|
||||
struct PyDollarBars {
|
||||
inner: wc::DollarBars,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyDollarBars {
|
||||
#[new]
|
||||
fn new(dollar_per_bar: f64) -> PyResult<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::DollarBars::new(dollar_per_bar).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
/// Feed one candle; returns bars completed as `(open, high, low, close, volume, dollar)`.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn update(
|
||||
&mut self,
|
||||
open: f64,
|
||||
high: f64,
|
||||
low: f64,
|
||||
close: f64,
|
||||
volume: f64,
|
||||
) -> PyResult<DollarBarRows> {
|
||||
let candle = wc::Candle::new(open, high, low, close, volume, 0).map_err(map_err)?;
|
||||
Ok(self
|
||||
.inner
|
||||
.update(candle)
|
||||
.into_iter()
|
||||
.map(|b| (b.open, b.high, b.low, b.close, b.volume, b.dollar))
|
||||
.collect())
|
||||
}
|
||||
/// Batch over OHLCV columns. Returns shape `(k, 6)` of `[open, high, low, close, volume, dollar]`.
|
||||
fn batch<'py>(
|
||||
&mut self,
|
||||
py: Python<'py>,
|
||||
open: PyReadonlyArray1<'py, f64>,
|
||||
high: PyReadonlyArray1<'py, f64>,
|
||||
low: PyReadonlyArray1<'py, f64>,
|
||||
close: PyReadonlyArray1<'py, f64>,
|
||||
volume: PyReadonlyArray1<'py, f64>,
|
||||
) -> PyResult<Bound<'py, PyArray2<f64>>> {
|
||||
let (o, h, l, c, v) = ohlcv_slices(&open, &high, &low, &close, &volume)?;
|
||||
let mut rows: Vec<f64> = Vec::new();
|
||||
let mut k = 0usize;
|
||||
for i in 0..o.len() {
|
||||
let candle = wc::Candle::new(o[i], h[i], l[i], c[i], v[i], 0).map_err(map_err)?;
|
||||
for b in self.inner.update(candle) {
|
||||
rows.extend_from_slice(&[b.open, b.high, b.low, b.close, b.volume, b.dollar]);
|
||||
k += 1;
|
||||
}
|
||||
}
|
||||
Ok(numpy::ndarray::Array2::from_shape_vec((k, 6), rows)
|
||||
.expect("shape consistent")
|
||||
.into_pyarray(py))
|
||||
}
|
||||
#[getter]
|
||||
fn dollar_per_bar(&self) -> f64 {
|
||||
self.inner.dollar_per_bar()
|
||||
}
|
||||
fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
fn __repr__(&self) -> String {
|
||||
format!("DollarBars(dollar_per_bar={})", self.inner.dollar_per_bar())
|
||||
}
|
||||
}
|
||||
|
||||
#[pyclass(name = "ImbalanceBars", module = "wickra._wickra", skip_from_py_object)]
|
||||
#[derive(Clone)]
|
||||
struct PyImbalanceBars {
|
||||
inner: wc::ImbalanceBars,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyImbalanceBars {
|
||||
#[new]
|
||||
fn new(threshold: f64) -> PyResult<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::ImbalanceBars::new(threshold).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
/// Feed one candle; returns bars completed as `(open, high, low, close, imbalance, direction)`.
|
||||
fn update(&mut self, open: f64, high: f64, low: f64, close: f64) -> PyResult<ImbalanceBarRows> {
|
||||
let candle = wc::Candle::new(open, high, low, close, 1.0, 0).map_err(map_err)?;
|
||||
Ok(self
|
||||
.inner
|
||||
.update(candle)
|
||||
.into_iter()
|
||||
.map(|b| {
|
||||
(
|
||||
b.open,
|
||||
b.high,
|
||||
b.low,
|
||||
b.close,
|
||||
b.imbalance,
|
||||
i64::from(b.direction),
|
||||
)
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
/// Batch over OHLC columns. Returns shape `(k, 6)` of `[open, high, low, close, imbalance, direction]`.
|
||||
fn batch<'py>(
|
||||
&mut self,
|
||||
py: Python<'py>,
|
||||
open: PyReadonlyArray1<'py, f64>,
|
||||
high: PyReadonlyArray1<'py, f64>,
|
||||
low: PyReadonlyArray1<'py, f64>,
|
||||
close: PyReadonlyArray1<'py, f64>,
|
||||
) -> PyResult<Bound<'py, PyArray2<f64>>> {
|
||||
let (o, h, l, c) = ohlc_slices(&open, &high, &low, &close)?;
|
||||
let mut rows: Vec<f64> = Vec::new();
|
||||
let mut k = 0usize;
|
||||
for i in 0..o.len() {
|
||||
let candle = wc::Candle::new(o[i], h[i], l[i], c[i], 1.0, 0).map_err(map_err)?;
|
||||
for b in self.inner.update(candle) {
|
||||
rows.extend_from_slice(&[
|
||||
b.open,
|
||||
b.high,
|
||||
b.low,
|
||||
b.close,
|
||||
b.imbalance,
|
||||
f64::from(b.direction),
|
||||
]);
|
||||
k += 1;
|
||||
}
|
||||
}
|
||||
Ok(numpy::ndarray::Array2::from_shape_vec((k, 6), rows)
|
||||
.expect("shape consistent")
|
||||
.into_pyarray(py))
|
||||
}
|
||||
#[getter]
|
||||
fn threshold(&self) -> f64 {
|
||||
self.inner.threshold()
|
||||
}
|
||||
fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
fn __repr__(&self) -> String {
|
||||
format!("ImbalanceBars(threshold={})", self.inner.threshold())
|
||||
}
|
||||
}
|
||||
|
||||
#[pyclass(name = "RunBars", module = "wickra._wickra", skip_from_py_object)]
|
||||
#[derive(Clone)]
|
||||
struct PyRunBars {
|
||||
inner: wc::RunBars,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyRunBars {
|
||||
#[new]
|
||||
fn new(run_length: usize) -> PyResult<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::RunBars::new(run_length).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
/// Feed one candle; returns bars completed as `(open, high, low, close, length, direction)`.
|
||||
fn update(&mut self, open: f64, high: f64, low: f64, close: f64) -> PyResult<RunBarRows> {
|
||||
let candle = wc::Candle::new(open, high, low, close, 1.0, 0).map_err(map_err)?;
|
||||
Ok(self
|
||||
.inner
|
||||
.update(candle)
|
||||
.into_iter()
|
||||
.map(|b| {
|
||||
(
|
||||
b.open,
|
||||
b.high,
|
||||
b.low,
|
||||
b.close,
|
||||
i64::try_from(b.length).unwrap_or(i64::MAX),
|
||||
i64::from(b.direction),
|
||||
)
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
/// Batch over OHLC columns. Returns shape `(k, 6)` of `[open, high, low, close, length, direction]`.
|
||||
fn batch<'py>(
|
||||
&mut self,
|
||||
py: Python<'py>,
|
||||
open: PyReadonlyArray1<'py, f64>,
|
||||
high: PyReadonlyArray1<'py, f64>,
|
||||
low: PyReadonlyArray1<'py, f64>,
|
||||
close: PyReadonlyArray1<'py, f64>,
|
||||
) -> PyResult<Bound<'py, PyArray2<f64>>> {
|
||||
let (o, h, l, c) = ohlc_slices(&open, &high, &low, &close)?;
|
||||
let mut rows: Vec<f64> = Vec::new();
|
||||
let mut k = 0usize;
|
||||
for i in 0..o.len() {
|
||||
let candle = wc::Candle::new(o[i], h[i], l[i], c[i], 1.0, 0).map_err(map_err)?;
|
||||
for b in self.inner.update(candle) {
|
||||
#[allow(clippy::cast_precision_loss)]
|
||||
rows.extend_from_slice(&[
|
||||
b.open,
|
||||
b.high,
|
||||
b.low,
|
||||
b.close,
|
||||
b.length as f64,
|
||||
f64::from(b.direction),
|
||||
]);
|
||||
k += 1;
|
||||
}
|
||||
}
|
||||
Ok(numpy::ndarray::Array2::from_shape_vec((k, 6), rows)
|
||||
.expect("shape consistent")
|
||||
.into_pyarray(py))
|
||||
}
|
||||
#[getter]
|
||||
fn run_length(&self) -> usize {
|
||||
self.inner.run_length()
|
||||
}
|
||||
fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
fn __repr__(&self) -> String {
|
||||
format!("RunBars(run_length={})", self.inner.run_length())
|
||||
}
|
||||
}
|
||||
|
||||
#[pyclass(
|
||||
name = "ThreeLineBreakBars",
|
||||
module = "wickra._wickra",
|
||||
skip_from_py_object
|
||||
)]
|
||||
#[derive(Clone)]
|
||||
struct PyThreeLineBreakBars {
|
||||
inner: wc::ThreeLineBreakBars,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyThreeLineBreakBars {
|
||||
#[new]
|
||||
#[pyo3(signature = (lines=3))]
|
||||
fn new(lines: usize) -> PyResult<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::ThreeLineBreakBars::new(lines).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
/// Feed one close; returns bars completed on it as `(open, close, direction)`.
|
||||
fn update(&mut self, close: f64) -> PyResult<Vec<(f64, f64, i64)>> {
|
||||
let candle = wc::Candle::new(close, close, close, close, 1.0, 0).map_err(map_err)?;
|
||||
Ok(self
|
||||
.inner
|
||||
.update(candle)
|
||||
.into_iter()
|
||||
.map(|b| (b.open, b.close, i64::from(b.direction)))
|
||||
.collect())
|
||||
}
|
||||
/// Batch over a close column. Returns shape `(k, 3)` of `[open, close, direction]`.
|
||||
fn batch<'py>(
|
||||
&mut self,
|
||||
py: Python<'py>,
|
||||
close: PyReadonlyArray1<'py, f64>,
|
||||
) -> PyResult<Bound<'py, PyArray2<f64>>> {
|
||||
let prices = close
|
||||
.as_slice()
|
||||
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||
let mut rows: Vec<f64> = Vec::new();
|
||||
let mut k = 0usize;
|
||||
for &price in prices {
|
||||
let candle = wc::Candle::new(price, price, price, price, 1.0, 0).map_err(map_err)?;
|
||||
for b in self.inner.update(candle) {
|
||||
rows.push(b.open);
|
||||
rows.push(b.close);
|
||||
rows.push(f64::from(b.direction));
|
||||
k += 1;
|
||||
}
|
||||
}
|
||||
Ok(numpy::ndarray::Array2::from_shape_vec((k, 3), rows)
|
||||
.expect("shape consistent")
|
||||
.into_pyarray(py))
|
||||
}
|
||||
#[getter]
|
||||
fn lines(&self) -> usize {
|
||||
self.inner.lines()
|
||||
}
|
||||
fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
fn __repr__(&self) -> String {
|
||||
format!("ThreeLineBreakBars(lines={})", self.inner.lines())
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== Module ==============================
|
||||
|
||||
// ====================== Seasonality & Session (full-candle) ======================
|
||||
@@ -26074,6 +26615,13 @@ fn _wickra(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||
m.add_class::<PyRenkoBars>()?;
|
||||
m.add_class::<PyKagiBars>()?;
|
||||
m.add_class::<PyPointAndFigureBars>()?;
|
||||
m.add_class::<PyRangeBars>()?;
|
||||
m.add_class::<PyTickBars>()?;
|
||||
m.add_class::<PyVolumeBars>()?;
|
||||
m.add_class::<PyDollarBars>()?;
|
||||
m.add_class::<PyImbalanceBars>()?;
|
||||
m.add_class::<PyRunBars>()?;
|
||||
m.add_class::<PyThreeLineBreakBars>()?;
|
||||
m.add_class::<PyInitialBalance>()?;
|
||||
m.add_class::<PyOpeningRange>()?;
|
||||
m.add_class::<PyNakedPoc>()?;
|
||||
|
||||
@@ -4237,3 +4237,83 @@ def test_bar_builders_reset():
|
||||
r.update(15.0)
|
||||
r.reset()
|
||||
assert r.update(50.0) == [] # re-seeds after reset
|
||||
|
||||
|
||||
def test_range_bars_reference():
|
||||
rb = ta.RangeBars(1.0)
|
||||
assert rb.update(10.0) == [] # seed
|
||||
assert rb.update(13.0) == [(10.0, 11.0, 1), (11.0, 12.0, 1), (12.0, 13.0, 1)]
|
||||
|
||||
|
||||
def test_range_bars_batch_shape():
|
||||
rb = ta.RangeBars(1.0)
|
||||
out = rb.batch(np.array([10.0, 11.0, 12.0, 13.0]))
|
||||
assert out.shape == (3, 3)
|
||||
np.testing.assert_allclose(out[:, 2], [1.0, 1.0, 1.0])
|
||||
|
||||
|
||||
def test_tick_bars_reference():
|
||||
tb = ta.TickBars(2)
|
||||
assert tb.update(10.0, 11.0, 9.0, 10.5, 100.0) == []
|
||||
out = tb.update(10.5, 12.0, 10.0, 11.0, 150.0)
|
||||
assert len(out) == 1
|
||||
assert out[0] == (10.0, 12.0, 9.0, 11.0, 250.0)
|
||||
|
||||
|
||||
def test_tick_bars_batch_shape():
|
||||
tb = ta.TickBars(2)
|
||||
col = np.array([10.0, 10.0, 10.0, 10.0])
|
||||
vol = np.array([1.0, 1.0, 1.0, 1.0])
|
||||
out = tb.batch(col, col, col, col, vol)
|
||||
assert out.shape == (2, 5)
|
||||
|
||||
|
||||
def test_volume_bars_reference():
|
||||
vb = ta.VolumeBars(100.0)
|
||||
assert vb.update(10.0, 10.0, 10.0, 10.0, 60.0) == []
|
||||
out = vb.update(10.5, 10.5, 10.5, 10.5, 60.0)
|
||||
assert len(out) == 1
|
||||
assert out[0][4] == 120.0 # accumulated volume
|
||||
|
||||
|
||||
def test_dollar_bars_reference():
|
||||
db = ta.DollarBars(1000.0)
|
||||
assert db.update(10.0, 10.0, 10.0, 10.0, 60.0) == [] # 600
|
||||
out = db.update(10.0, 10.0, 10.0, 10.0, 60.0) # 1200 >= 1000
|
||||
assert len(out) == 1
|
||||
assert out[0][4] == 120.0 # volume
|
||||
assert out[0][5] == 1200.0 # traded value
|
||||
|
||||
|
||||
def test_imbalance_bars_reference():
|
||||
ib = ta.ImbalanceBars(3.0)
|
||||
assert ib.update(10.0, 10.0, 10.0, 10.0) == [] # seed
|
||||
ib.update(11.0, 11.0, 11.0, 11.0) # +1
|
||||
ib.update(12.0, 12.0, 12.0, 12.0) # +2
|
||||
out = ib.update(13.0, 13.0, 13.0, 13.0) # +3 -> close
|
||||
assert len(out) == 1
|
||||
assert out[0][4] == 3.0 # imbalance
|
||||
assert out[0][5] == 1 # direction
|
||||
|
||||
|
||||
def test_run_bars_reference():
|
||||
rb = ta.RunBars(3)
|
||||
assert rb.update(10.0, 10.0, 10.0, 10.0) == [] # seed
|
||||
rb.update(11.0, 11.0, 11.0, 11.0) # run 1
|
||||
rb.update(12.0, 12.0, 12.0, 12.0) # run 2
|
||||
out = rb.update(13.0, 13.0, 13.0, 13.0) # run 3 -> close
|
||||
assert len(out) == 1
|
||||
assert out[0][4] == 3 # length
|
||||
assert out[0][5] == 1 # direction
|
||||
|
||||
|
||||
def test_three_line_break_bars_reference():
|
||||
tlb = ta.ThreeLineBreakBars(3)
|
||||
assert tlb.update(10.0) == [] # seed
|
||||
assert tlb.update(11.0) == [(10.0, 11.0, 1)]
|
||||
|
||||
|
||||
def test_three_line_break_bars_batch_shape():
|
||||
tlb = ta.ThreeLineBreakBars(3)
|
||||
out = tlb.batch(np.array([10.0, 11.0, 12.0, 13.0]))
|
||||
assert out.shape[1] == 3
|
||||
|
||||
Reference in New Issue
Block a user