feat: signed candlestick directional ±1 encoding (Doji signed mode) (#111)
* feat(core): add signed dragonfly/gravestone encoding to Doji Doji gains an opt-in `.signed()` mode that classifies a detected Doji by the position of its body within the bar range: dragonfly (long lower shadow) emits +1.0 (bullish), gravestone (long upper shadow) emits -1.0 (bearish), and a long-legged/standard Doji emits 0.0. The default detection-flag behaviour (+1.0/0.0) is unchanged, so existing callers are unaffected. The other 14 candlestick patterns already emit the uniform +1 bull / -1 bear / 0 none convention; document that explicitly with a "Signed +-1 encoding" section on each so the whole family is a consistent drop-in ML feature. * feat(bindings): expose Doji signed mode in python, node, wasm Hand-write the Doji binding in all three language bindings (instead of the shared candle-pattern macro) so it accepts an opt-in `signed` flag and exposes an `is_signed`/`isSigned` accessor: - Python: `Doji(signed=False)` keyword argument - Node: `new Doji(signed?)` optional constructor argument (index.d.ts/.js regenerated via napi build) - WASM: `new Doji(signed?)` optional constructor argument The default construction is unchanged, so existing callers keep the direction-less +1/0 detection flag. * test(bindings,fuzz): cover Doji signed dragonfly/gravestone encoding - python: dragonfly(+1)/gravestone(-1)/neutral(0) and default-flag cases in test_known_values - node: equivalent signed/default assertions in indicators.test.js - fuzz: drive a signed Doji alongside the default in indicator_update_candle * docs: document signed candlestick convention and Doji signed mode README gains a candlestick sign-convention note; CHANGELOG records the new opt-in Doji signed dragonfly/gravestone encoding under [Unreleased].
This commit is contained in:
@@ -11433,8 +11433,9 @@ impl PyOpeningRange {
|
||||
// ============================== Candlestick Patterns ==============================
|
||||
//
|
||||
// All 15 patterns take Candles and emit a signed f64 signal per bar:
|
||||
// +1.0 bullish, -1.0 bearish, 0.0 no pattern. Doji is direction-less, so it
|
||||
// uses +1.0 / 0.0 only.
|
||||
// +1.0 bullish, -1.0 bearish, 0.0 no pattern. Doji is direction-less by
|
||||
// default (+1.0 / 0.0); construct it with `signed=True` for the
|
||||
// dragonfly/gravestone signed +-1 encoding.
|
||||
|
||||
macro_rules! candle_pattern_no_param {
|
||||
($name:ident, $inner:ty, $repr:expr) => {
|
||||
@@ -11505,7 +11506,86 @@ macro_rules! candle_pattern_no_param {
|
||||
};
|
||||
}
|
||||
|
||||
candle_pattern_no_param!(PyDoji, wc::Doji, "Doji");
|
||||
// Doji is the one pattern with an opt-in signed mode, so it is hand-written
|
||||
// rather than generated by `candle_pattern_no_param!`.
|
||||
#[pyclass(name = "Doji", module = "wickra._wickra", skip_from_py_object)]
|
||||
#[derive(Clone)]
|
||||
struct PyDoji {
|
||||
inner: wc::Doji,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyDoji {
|
||||
#[new]
|
||||
#[pyo3(signature = (signed = false))]
|
||||
fn new(signed: bool) -> Self {
|
||||
let inner = if signed {
|
||||
wc::Doji::new().signed()
|
||||
} else {
|
||||
wc::Doji::new()
|
||||
};
|
||||
Self { inner }
|
||||
}
|
||||
fn update(&mut self, candle: &Bound<'_, PyAny>) -> PyResult<Option<f64>> {
|
||||
let c = extract_candle(candle)?;
|
||||
Ok(self.inner.update(c))
|
||||
}
|
||||
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, PyArray1<f64>>> {
|
||||
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",
|
||||
));
|
||||
}
|
||||
let mut out = Vec::with_capacity(o.len());
|
||||
for i in 0..o.len() {
|
||||
let candle = wc::Candle::new(o[i], h[i], l[i], c[i], 0.0, 0).map_err(map_err)?;
|
||||
out.push(self.inner.update(candle).unwrap_or(f64::NAN));
|
||||
}
|
||||
Ok(out.into_pyarray(py))
|
||||
}
|
||||
fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
fn is_ready(&self) -> bool {
|
||||
self.inner.is_ready()
|
||||
}
|
||||
fn warmup_period(&self) -> usize {
|
||||
self.inner.warmup_period()
|
||||
}
|
||||
fn is_signed(&self) -> bool {
|
||||
self.inner.is_signed()
|
||||
}
|
||||
fn __repr__(&self) -> String {
|
||||
format!(
|
||||
"Doji(signed={})",
|
||||
if self.inner.is_signed() {
|
||||
"True"
|
||||
} else {
|
||||
"False"
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
candle_pattern_no_param!(PyHammer, wc::Hammer, "Hammer");
|
||||
candle_pattern_no_param!(PyInvertedHammer, wc::InvertedHammer, "InvertedHammer");
|
||||
candle_pattern_no_param!(PyHangingMan, wc::HangingMan, "HangingMan");
|
||||
|
||||
Reference in New Issue
Block a user