diff --git a/CHANGELOG.md b/CHANGELOG.md index a95ee419..a95b5116 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added +- **Signed Doji encoding.** `Doji` gains an opt-in `.signed()` mode + (`Doji(signed=True)` in Python, `new Doji(true)` in Node and WASM) that + classifies a detected Doji by the position of its body within the bar range — + a dragonfly (long lower shadow) emits `+1.0` (bullish), a gravestone (long + upper shadow) emits `−1.0` (bearish), and a long-legged / standard Doji emits + `0.0` (neutral). The default construction is unchanged — a direction-less + `+1.0` / `0.0` detection flag — so existing callers are unaffected. This + completes the uniform `+1` bull / `−1` bear / `0` none sign convention across + every candlestick pattern, making the family a drop-in machine-learning + feature where bullish and bearish instances share a single dimension. + ## [0.4.1] - 2026-06-01 ### Added diff --git a/README.md b/README.md index cbc3b9d8..d11b42db 100644 --- a/README.md +++ b/README.md @@ -159,6 +159,12 @@ warmup) at [docs.wickra.org](https://docs.wickra.org/Indicators-Overview). | Market Profile | Value Area (POC / VAH / VAL), Initial Balance, Opening Range | | Risk / Performance | Sharpe Ratio, Sortino Ratio, Calmar Ratio, Omega Ratio, Max Drawdown, Average Drawdown, Drawdown Duration, Pain Index, Value at Risk, Conditional Value at Risk (CVaR), Profit Factor, Gain/Loss Ratio, Recovery Factor, Kelly Criterion, Treynor Ratio, Information Ratio, Alpha (Jensen) | +Every candlestick pattern emits a signed per-bar value — `+1.0` bullish, +`−1.0` bearish, `0.0` none — so the family drops straight into a feature matrix +as one column each. `Doji` is direction-less by default (`+1.0` / `0.0`); +construct it in signed mode (`Doji::new().signed()`, `Doji(signed=True)`, +`new Doji(true)`) for a dragonfly / gravestone `±1` reading. + Adding a new indicator means implementing one trait in Rust; all four bindings inherit it automatically. diff --git a/bindings/node/__tests__/indicators.test.js b/bindings/node/__tests__/indicators.test.js index 937797df..f9914d4b 100644 --- a/bindings/node/__tests__/indicators.test.js +++ b/bindings/node/__tests__/indicators.test.js @@ -896,3 +896,19 @@ test('ALMA(3, 0.85, 6) reference value on [10, 20, 30]', () => { // simple mean of 20. assert.ok(out[2] > 20); }); + +test('Doji signed mode encodes dragonfly/gravestone/neutral direction', () => { + // Default: direction-less detection flag (+1 doji / 0 otherwise). + const flag = new wickra.Doji(); + assert.equal(flag.isSigned(), false); + assert.equal(flag.update(10, 11, 9, 10), 1); // body 0, range 2 -> doji + assert.equal(flag.update(10, 12, 10, 12), 0); // body == range -> not a doji + + // Signed: classify a detected doji by its body position within the range. + const d = new wickra.Doji(true); + assert.equal(d.isSigned(), true); + assert.equal(d.update(10, 10.05, 6, 10), 1); // dragonfly -> bullish +1 + assert.equal(d.update(10, 14, 9.95, 10), -1); // gravestone -> bearish -1 + assert.equal(d.update(10, 12, 8, 10), 0); // long-legged -> neutral 0 + assert.equal(d.update(10, 12, 10, 12), 0); // not a doji -> 0 +}); diff --git a/bindings/node/index.d.ts b/bindings/node/index.d.ts index 5e8e3229..57818e71 100644 --- a/bindings/node/index.d.ts +++ b/bindings/node/index.d.ts @@ -2055,12 +2055,13 @@ export declare class OpeningRange { } export type DojiNode = Doji export declare class Doji { - constructor() + constructor(signed?: boolean | undefined | null) update(open: number, high: number, low: number, close: number): number | null batch(open: Array, high: Array, low: Array, close: Array): Array reset(): void isReady(): boolean warmupPeriod(): number + isSigned(): boolean } export type HammerNode = Hammer export declare class Hammer { diff --git a/bindings/node/src/lib.rs b/bindings/node/src/lib.rs index d39d6e1f..69848931 100644 --- a/bindings/node/src/lib.rs +++ b/bindings/node/src/lib.rs @@ -8581,7 +8581,8 @@ impl OpeningRangeNode { // // All 15 patterns take Candles (open, high, low, close) and emit a signed f64 // signal per bar: +1.0 bullish, -1.0 bearish, 0.0 no pattern. Doji is -// direction-less and emits 0/+1 only. +// direction-less by default (0/+1); pass `signed = true` to its constructor for +// the dragonfly/gravestone signed +-1 encoding. macro_rules! node_candle_pattern { ($node:ident, $inner:ty, $js:literal) => { @@ -8652,7 +8653,80 @@ macro_rules! node_candle_pattern { }; } -node_candle_pattern!(DojiNode, wc::Doji, "Doji"); +// Doji is the one pattern with an opt-in signed mode, so it is hand-written +// rather than generated by `node_candle_pattern!`. +#[napi(js_name = "Doji")] +pub struct DojiNode { + inner: wc::Doji, +} + +impl Default for DojiNode { + fn default() -> Self { + Self::new(None) + } +} + +#[napi] +impl DojiNode { + #[napi(constructor)] + pub fn new(signed: Option) -> Self { + let inner = if signed.unwrap_or(false) { + wc::Doji::new().signed() + } else { + wc::Doji::new() + }; + Self { inner } + } + #[napi] + pub fn update( + &mut self, + open: f64, + high: f64, + low: f64, + close: f64, + ) -> napi::Result> { + let candle = wc::Candle::new(open, high, low, close, 0.0, 0).map_err(map_err)?; + Ok(self.inner.update(candle)) + } + #[napi] + pub fn batch( + &mut self, + open: Vec, + high: Vec, + low: Vec, + close: Vec, + ) -> napi::Result> { + if open.len() != high.len() || high.len() != low.len() || low.len() != close.len() { + return Err(NapiError::from_reason( + "open, high, low, close must be equal length".to_string(), + )); + } + let mut out = Vec::with_capacity(open.len()); + for i in 0..open.len() { + let candle = + wc::Candle::new(open[i], high[i], low[i], close[i], 0.0, 0).map_err(map_err)?; + out.push(self.inner.update(candle).unwrap_or(f64::NAN)); + } + Ok(out) + } + #[napi] + pub fn reset(&mut self) { + self.inner.reset(); + } + #[napi(js_name = "isReady")] + pub fn is_ready(&self) -> bool { + self.inner.is_ready() + } + #[napi(js_name = "warmupPeriod")] + pub fn warmup_period(&self) -> u32 { + self.inner.warmup_period() as u32 + } + #[napi(js_name = "isSigned")] + pub fn is_signed(&self) -> bool { + self.inner.is_signed() + } +} + node_candle_pattern!(HammerNode, wc::Hammer, "Hammer"); node_candle_pattern!(InvertedHammerNode, wc::InvertedHammer, "InvertedHammer"); node_candle_pattern!(HangingManNode, wc::HangingMan, "HangingMan"); diff --git a/bindings/python/src/lib.rs b/bindings/python/src/lib.rs index ed5296b2..5e1613b0 100644 --- a/bindings/python/src/lib.rs +++ b/bindings/python/src/lib.rs @@ -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> { + 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>> { + 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"); diff --git a/bindings/python/tests/test_known_values.py b/bindings/python/tests/test_known_values.py index 46e84a78..3f9242a8 100644 --- a/bindings/python/tests/test_known_values.py +++ b/bindings/python/tests/test_known_values.py @@ -823,3 +823,27 @@ def test_yang_zhang_zero_movement_yields_zero(): ready = out[~np.isnan(out)] assert ready.size > 0 np.testing.assert_allclose(ready, 0.0, atol=1e-12) + + +def test_doji_default_is_directionless_flag(): + # Default Doji is a direction-less detection flag: +1 on a doji, 0 else. + d = ta.Doji() + assert d.is_signed() is False + # body 0, range 2 -> doji. + assert d.update((10.0, 11.0, 9.0, 10.0, 1.0, 0)) == pytest.approx(1.0) + # body 2 == range -> not a doji. + assert d.update((10.0, 12.0, 10.0, 12.0, 1.0, 1)) == pytest.approx(0.0) + + +def test_doji_signed_dragonfly_gravestone_neutral(): + # Signed Doji classifies by body position within the range. + d = ta.Doji(signed=True) + assert d.is_signed() is True + # Dragonfly: body at the top, long lower shadow -> bullish +1. + assert d.update((10.0, 10.05, 6.0, 10.0, 1.0, 0)) == pytest.approx(1.0) + # Gravestone: body at the bottom, long upper shadow -> bearish -1. + assert d.update((10.0, 14.0, 9.95, 10.0, 1.0, 1)) == pytest.approx(-1.0) + # Long-legged: body centred, symmetric shadows -> neutral 0. + assert d.update((10.0, 12.0, 8.0, 10.0, 1.0, 2)) == pytest.approx(0.0) + # A large body is not a doji at all -> 0 regardless of position. + assert d.update((10.0, 12.0, 10.0, 12.0, 1.0, 3)) == pytest.approx(0.0) diff --git a/bindings/wasm/src/lib.rs b/bindings/wasm/src/lib.rs index b749ba18..daf70c27 100644 --- a/bindings/wasm/src/lib.rs +++ b/bindings/wasm/src/lib.rs @@ -6167,7 +6167,8 @@ impl WasmOpeningRange { // // All 15 patterns take Candles (open, high, low, close) and emit a signed f64 // signal per bar: +1.0 bullish, -1.0 bearish, 0.0 no pattern. Doji is -// direction-less and emits 0/+1 only. +// direction-less by default (0/+1); pass `signed = true` to its constructor for +// the dragonfly/gravestone signed +-1 encoding. macro_rules! wasm_candle_pattern { ($wasm:ident, $inner:ty, $js:ident) => { @@ -6234,7 +6235,75 @@ macro_rules! wasm_candle_pattern { }; } -wasm_candle_pattern!(WasmDoji, wc::Doji, Doji); +// Doji is the one pattern with an opt-in signed mode, so it is hand-written +// rather than generated by `wasm_candle_pattern!`. +#[wasm_bindgen(js_name = Doji)] +pub struct WasmDoji { + inner: wc::Doji, +} + +impl Default for WasmDoji { + fn default() -> Self { + Self::new(None) + } +} + +#[wasm_bindgen(js_class = Doji)] +impl WasmDoji { + #[wasm_bindgen(constructor)] + pub fn new(signed: Option) -> WasmDoji { + let inner = if signed.unwrap_or(false) { + wc::Doji::new().signed() + } else { + wc::Doji::new() + }; + Self { inner } + } + pub fn update( + &mut self, + open: f64, + high: f64, + low: f64, + close: f64, + ) -> Result, JsError> { + let c = wc::Candle::new(open, high, low, close, 0.0, 0).map_err(map_err)?; + Ok(self.inner.update(c)) + } + pub fn batch( + &mut self, + open: &[f64], + high: &[f64], + low: &[f64], + close: &[f64], + ) -> Result { + let n = open.len(); + if high.len() != n || low.len() != n || close.len() != n { + return Err(JsError::new("open, high, low, close must be equal length")); + } + let mut out = Vec::with_capacity(n); + for i in 0..n { + let c = wc::Candle::new(open[i], high[i], low[i], close[i], 0.0, 0).map_err(map_err)?; + out.push(self.inner.update(c).unwrap_or(f64::NAN)); + } + Ok(Float64Array::from(out.as_slice())) + } + pub fn reset(&mut self) { + self.inner.reset(); + } + #[wasm_bindgen(js_name = isReady)] + pub fn is_ready(&self) -> bool { + self.inner.is_ready() + } + #[wasm_bindgen(js_name = warmupPeriod)] + pub fn warmup_period(&self) -> usize { + self.inner.warmup_period() + } + #[wasm_bindgen(js_name = isSigned)] + pub fn is_signed(&self) -> bool { + self.inner.is_signed() + } +} + wasm_candle_pattern!(WasmHammer, wc::Hammer, Hammer); wasm_candle_pattern!(WasmInvertedHammer, wc::InvertedHammer, InvertedHammer); wasm_candle_pattern!(WasmHangingMan, wc::HangingMan, HangingMan); diff --git a/crates/wickra-core/src/indicators/doji.rs b/crates/wickra-core/src/indicators/doji.rs index ff32f2bd..77704e0f 100644 --- a/crates/wickra-core/src/indicators/doji.rs +++ b/crates/wickra-core/src/indicators/doji.rs @@ -16,22 +16,44 @@ use crate::traits::Indicator; /// doji = body <= body_threshold * range /// ``` /// -/// The output is `+1.0` when a Doji is detected and `0.0` otherwise. Doji is -/// directionless — no `−1.0` is emitted. Pattern-shape check only — no trend -/// filter is applied; combine with a trend indicator for actionable signals. +/// # Signed ±1 encoding +/// +/// By default the output is `+1.0` when a Doji is detected and `0.0` +/// otherwise — a direction-less detection flag. For a drop-in machine-learning +/// feature where every candlestick pattern shares the same sign convention +/// (`+1.0` bullish, `−1.0` bearish, `0.0` none), switch the detector into +/// signed mode with [`Doji::signed`]. A detected Doji is then classified by +/// where its (negligible) body sits within the bar's range: +/// +/// ```text +/// pos = (0.5 * (open + close) − low) / (high − low) +/// pos > 2/3 -> +1.0 dragonfly (long lower shadow, bullish) +/// pos < 1/3 -> −1.0 gravestone (long upper shadow, bearish) +/// else -> 0.0 long-legged / standard (neutral) +/// ``` +/// +/// Pattern-shape check only — no trend filter is applied; combine with a trend +/// indicator for actionable signals. /// /// # Example /// /// ``` /// use wickra_core::{Candle, Doji, Indicator}; /// +/// // Default: direction-less detection flag. /// let mut indicator = Doji::default(); /// let candle = Candle::new(10.0, 11.0, 9.0, 10.0, 1.0, 0).unwrap(); /// assert_eq!(indicator.update(candle), Some(1.0)); +/// +/// // Signed: a dragonfly Doji (body at the top, long lower shadow) is bullish. +/// let mut signed = Doji::new().signed(); +/// let dragonfly = Candle::new(10.0, 10.05, 6.0, 10.0, 1.0, 0).unwrap(); +/// assert_eq!(signed.update(dragonfly), Some(1.0)); /// ``` #[derive(Debug, Clone)] pub struct Doji { body_threshold: f64, + signed: bool, has_emitted: bool, } @@ -46,6 +68,7 @@ impl Doji { pub const fn new() -> Self { Self { body_threshold: 0.1, + signed: false, has_emitted: false, } } @@ -61,14 +84,32 @@ impl Doji { } Ok(Self { body_threshold, + signed: false, has_emitted: false, }) } + /// Switch to the signed dragonfly / gravestone encoding (consuming builder). + /// + /// In signed mode a detected Doji emits `+1.0` (dragonfly, bullish), + /// `−1.0` (gravestone, bearish) or `0.0` (long-legged / neutral) instead of + /// the default direction-less `+1.0` detection flag. See the type-level + /// docs for the exact classification rule. + #[must_use] + pub fn signed(mut self) -> Self { + self.signed = true; + self + } + /// Configured body / range threshold. pub fn body_threshold(&self) -> f64 { self.body_threshold } + + /// Whether this detector emits the signed dragonfly / gravestone encoding. + pub fn is_signed(&self) -> bool { + self.signed + } } impl Indicator for Doji { @@ -82,11 +123,23 @@ impl Indicator for Doji { return Some(0.0); } let body = (candle.close - candle.open).abs(); - Some(if body <= self.body_threshold * range { - 1.0 + if body > self.body_threshold * range { + return Some(0.0); + } + if !self.signed { + return Some(1.0); + } + // Signed mode: classify the Doji by where its (negligible) body sits + // within the high–low range. + let body_mid = 0.5 * (candle.open + candle.close); + let pos = (body_mid - candle.low) / range; + if pos > 2.0 / 3.0 { + Some(1.0) + } else if pos < 1.0 / 3.0 { + Some(-1.0) } else { - 0.0 - }) + Some(0.0) + } } fn reset(&mut self) { @@ -134,6 +187,7 @@ mod tests { assert_eq!(d.name(), "Doji"); assert_eq!(d.warmup_period(), 1); assert!(!d.is_ready()); + assert!(!d.is_signed()); assert!((d.body_threshold() - 0.1).abs() < 1e-12); } @@ -182,4 +236,81 @@ mod tests { d.reset(); assert!(!d.is_ready()); } + + #[test] + fn signed_accessor_and_builder() { + let d = Doji::new().signed(); + assert!(d.is_signed()); + // The consuming builder composes with `with_threshold`. + let t = Doji::with_threshold(0.05).unwrap().signed(); + assert!(t.is_signed()); + assert!((t.body_threshold() - 0.05).abs() < 1e-12); + } + + #[test] + fn signed_dragonfly_is_plus_one() { + // Body at the top of the range, long lower shadow -> bullish. + let mut d = Doji::new().signed(); + assert_eq!(d.update(c(10.0, 10.05, 6.0, 10.0, 0)), Some(1.0)); + } + + #[test] + fn signed_gravestone_is_minus_one() { + // Body at the bottom of the range, long upper shadow -> bearish. + let mut d = Doji::new().signed(); + assert_eq!(d.update(c(10.0, 14.0, 9.95, 10.0, 0)), Some(-1.0)); + } + + #[test] + fn signed_long_legged_is_zero() { + // Body centred, symmetric shadows -> neutral. + let mut d = Doji::new().signed(); + assert_eq!(d.update(c(10.0, 12.0, 8.0, 10.0, 0)), Some(0.0)); + } + + #[test] + fn signed_non_doji_is_zero() { + // A large body is not a Doji at all -> 0 regardless of position. + let mut d = Doji::new().signed(); + assert_eq!(d.update(c(10.0, 12.0, 10.0, 12.0, 0)), Some(0.0)); + } + + #[test] + fn signed_zero_range_is_zero() { + let mut d = Doji::new().signed(); + assert_eq!(d.update(c(10.0, 10.0, 10.0, 10.0, 0)), Some(0.0)); + } + + #[test] + fn signed_batch_equals_streaming() { + let candles: Vec = (0..40) + .map(|i| { + let base = 100.0 + i as f64; + // Alternate dragonfly / gravestone / centred Doji shapes. + match i % 3 { + 0 => c(base, base + 0.05, base - 4.0, base, i), + 1 => c(base, base + 4.0, base - 0.05, base, i), + _ => c(base, base + 2.0, base - 2.0, base, i), + } + }) + .collect(); + let mut a = Doji::new().signed(); + let mut b = Doji::new().signed(); + assert_eq!( + a.batch(&candles), + candles.iter().map(|x| b.update(*x)).collect::>() + ); + } + + #[test] + fn signed_survives_reset() { + let mut d = Doji::new().signed(); + d.update(c(10.0, 10.05, 6.0, 10.0, 0)); + assert!(d.is_ready()); + d.reset(); + assert!(!d.is_ready()); + // `reset` clears only the streaming state, not the signed configuration. + assert!(d.is_signed()); + assert_eq!(d.update(c(10.0, 10.05, 6.0, 10.0, 1)), Some(1.0)); + } } diff --git a/crates/wickra-core/src/indicators/engulfing.rs b/crates/wickra-core/src/indicators/engulfing.rs index 21e0f019..c92b1b69 100644 --- a/crates/wickra-core/src/indicators/engulfing.rs +++ b/crates/wickra-core/src/indicators/engulfing.rs @@ -22,6 +22,13 @@ use crate::traits::Indicator; /// body exists to engulf. Pattern-shape check only — no trend filter is /// applied; combine with a trend indicator for actionable signals. /// +/// # Signed ±1 encoding +/// +/// This detector already emits the uniform candlestick sign convention shared +/// across the pattern family — `+1.0` bullish, `−1.0` bearish, `0.0` no +/// pattern — so it drops straight into a machine-learning feature matrix where +/// the bullish and bearish variants of the pattern occupy a single dimension. +/// /// # Example /// /// ``` diff --git a/crates/wickra-core/src/indicators/hammer.rs b/crates/wickra-core/src/indicators/hammer.rs index 8add55c2..916831f6 100644 --- a/crates/wickra-core/src/indicators/hammer.rs +++ b/crates/wickra-core/src/indicators/hammer.rs @@ -22,6 +22,14 @@ use crate::traits::Indicator; /// check only — no trend filter is applied; combine with a trend indicator /// for actionable signals. /// +/// # Signed ±1 encoding +/// +/// A Hammer is bullish by definition, so under the uniform candlestick sign +/// convention (`+1.0` bullish, `−1.0` bearish, `0.0` none) it emits `+1.0` +/// when the shape matches and `0.0` otherwise — it never emits `−1.0`. The +/// same geometry read at the top of an uptrend is the bearish `HangingMan`, +/// which carries the opposite sign. +/// /// # Example /// /// ``` diff --git a/crates/wickra-core/src/indicators/hanging_man.rs b/crates/wickra-core/src/indicators/hanging_man.rs index f8ab1ace..8e8c2675 100644 --- a/crates/wickra-core/src/indicators/hanging_man.rs +++ b/crates/wickra-core/src/indicators/hanging_man.rs @@ -22,6 +22,14 @@ use crate::traits::Indicator; /// check only — no trend filter is applied; combine with a trend indicator /// for actionable signals. /// +/// # Signed ±1 encoding +/// +/// A Hanging Man is bearish by definition, so under the uniform candlestick +/// sign convention (`+1.0` bullish, `−1.0` bearish, `0.0` none) it emits +/// `−1.0` when the shape matches and `0.0` otherwise — it never emits `+1.0`. +/// The same geometry read at the bottom of a downtrend is the bullish +/// `Hammer`, which carries the opposite sign. +/// /// # Example /// /// ``` diff --git a/crates/wickra-core/src/indicators/harami.rs b/crates/wickra-core/src/indicators/harami.rs index 89e6e2df..cc9276d6 100644 --- a/crates/wickra-core/src/indicators/harami.rs +++ b/crates/wickra-core/src/indicators/harami.rs @@ -23,6 +23,13 @@ use crate::traits::Indicator; /// no trend filter is applied; combine with a trend indicator for actionable /// signals. /// +/// # Signed ±1 encoding +/// +/// This detector already emits the uniform candlestick sign convention shared +/// across the pattern family — `+1.0` bullish, `−1.0` bearish, `0.0` no +/// pattern — so it drops straight into a machine-learning feature matrix where +/// the bullish and bearish variants of the pattern occupy a single dimension. +/// /// # Example /// /// ``` diff --git a/crates/wickra-core/src/indicators/inverted_hammer.rs b/crates/wickra-core/src/indicators/inverted_hammer.rs index 0928c739..dd6492b8 100644 --- a/crates/wickra-core/src/indicators/inverted_hammer.rs +++ b/crates/wickra-core/src/indicators/inverted_hammer.rs @@ -22,6 +22,14 @@ use crate::traits::Indicator; /// check only — no trend filter is applied; combine with a trend indicator /// for actionable signals. /// +/// # Signed ±1 encoding +/// +/// An Inverted Hammer is bullish by definition, so under the uniform +/// candlestick sign convention (`+1.0` bullish, `−1.0` bearish, `0.0` none) it +/// emits `+1.0` when the shape matches and `0.0` otherwise — it never emits +/// `−1.0`. The same geometry read at the top of an uptrend is the bearish +/// `ShootingStar`, which carries the opposite sign. +/// /// # Example /// /// ``` diff --git a/crates/wickra-core/src/indicators/marubozu.rs b/crates/wickra-core/src/indicators/marubozu.rs index 65519470..debdda06 100644 --- a/crates/wickra-core/src/indicators/marubozu.rs +++ b/crates/wickra-core/src/indicators/marubozu.rs @@ -22,6 +22,13 @@ use crate::traits::Indicator; /// `shadow_tolerance` defaults to `0.05` (5 % of the bar range allowed on each /// side) and must lie in `[0, 1)`. /// +/// # Signed ±1 encoding +/// +/// This detector already emits the uniform candlestick sign convention shared +/// across the pattern family — `+1.0` bullish, `−1.0` bearish, `0.0` no +/// pattern — so it drops straight into a machine-learning feature matrix where +/// the bullish and bearish variants of the pattern occupy a single dimension. +/// /// # Example /// /// ``` diff --git a/crates/wickra-core/src/indicators/morning_evening_star.rs b/crates/wickra-core/src/indicators/morning_evening_star.rs index 1469ba50..6710c965 100644 --- a/crates/wickra-core/src/indicators/morning_evening_star.rs +++ b/crates/wickra-core/src/indicators/morning_evening_star.rs @@ -18,6 +18,13 @@ use crate::traits::Indicator; /// trend filter is applied; combine with a trend indicator for actionable /// signals. /// +/// # Signed ±1 encoding +/// +/// This detector already emits the uniform candlestick sign convention shared +/// across the pattern family — `+1.0` bullish, `−1.0` bearish, `0.0` no +/// pattern — so it drops straight into a machine-learning feature matrix where +/// the bullish and bearish variants of the pattern occupy a single dimension. +/// /// # Example /// /// ``` diff --git a/crates/wickra-core/src/indicators/piercing_dark_cloud.rs b/crates/wickra-core/src/indicators/piercing_dark_cloud.rs index 6b314627..70c685c8 100644 --- a/crates/wickra-core/src/indicators/piercing_dark_cloud.rs +++ b/crates/wickra-core/src/indicators/piercing_dark_cloud.rs @@ -26,6 +26,13 @@ use crate::traits::Indicator; /// only — no trend filter is applied; combine with a trend indicator for /// actionable signals. /// +/// # Signed ±1 encoding +/// +/// This detector already emits the uniform candlestick sign convention shared +/// across the pattern family — `+1.0` bullish, `−1.0` bearish, `0.0` no +/// pattern — so it drops straight into a machine-learning feature matrix where +/// the bullish and bearish variants of the pattern occupy a single dimension. +/// /// # Example /// /// ``` diff --git a/crates/wickra-core/src/indicators/shooting_star.rs b/crates/wickra-core/src/indicators/shooting_star.rs index 3d5201d5..5dacec42 100644 --- a/crates/wickra-core/src/indicators/shooting_star.rs +++ b/crates/wickra-core/src/indicators/shooting_star.rs @@ -22,6 +22,14 @@ use crate::traits::Indicator; /// check only — no trend filter is applied; combine with a trend indicator /// for actionable signals. /// +/// # Signed ±1 encoding +/// +/// A Shooting Star is bearish by definition, so under the uniform candlestick +/// sign convention (`+1.0` bullish, `−1.0` bearish, `0.0` none) it emits +/// `−1.0` when the shape matches and `0.0` otherwise — it never emits `+1.0`. +/// The same geometry read at the bottom of a downtrend is the bullish +/// `InvertedHammer`, which carries the opposite sign. +/// /// # Example /// /// ``` diff --git a/crates/wickra-core/src/indicators/spinning_top.rs b/crates/wickra-core/src/indicators/spinning_top.rs index cfe7c69d..227498d2 100644 --- a/crates/wickra-core/src/indicators/spinning_top.rs +++ b/crates/wickra-core/src/indicators/spinning_top.rs @@ -24,6 +24,13 @@ use crate::traits::Indicator; /// /// `body_threshold` defaults to `0.3` and must lie in `(0, 1]`. /// +/// # Signed ±1 encoding +/// +/// This detector already emits the uniform candlestick sign convention shared +/// across the pattern family — `+1.0` bullish, `−1.0` bearish, `0.0` no +/// pattern — so it drops straight into a machine-learning feature matrix where +/// the bullish and bearish variants of the pattern occupy a single dimension. +/// /// # Example /// /// ``` diff --git a/crates/wickra-core/src/indicators/three_inside.rs b/crates/wickra-core/src/indicators/three_inside.rs index 583018e0..3ee471cc 100644 --- a/crates/wickra-core/src/indicators/three_inside.rs +++ b/crates/wickra-core/src/indicators/three_inside.rs @@ -18,6 +18,13 @@ use crate::traits::Indicator; /// Pattern-shape check only — no trend filter is applied; combine with a trend /// indicator for actionable signals. /// +/// # Signed ±1 encoding +/// +/// This detector already emits the uniform candlestick sign convention shared +/// across the pattern family — `+1.0` bullish, `−1.0` bearish, `0.0` no +/// pattern — so it drops straight into a machine-learning feature matrix where +/// the bullish and bearish variants of the pattern occupy a single dimension. +/// /// # Example /// /// ``` diff --git a/crates/wickra-core/src/indicators/three_outside.rs b/crates/wickra-core/src/indicators/three_outside.rs index e8ef1041..fe084aba 100644 --- a/crates/wickra-core/src/indicators/three_outside.rs +++ b/crates/wickra-core/src/indicators/three_outside.rs @@ -17,6 +17,13 @@ use crate::traits::Indicator; /// Pattern-shape check only — no trend filter is applied; combine with a trend /// indicator for actionable signals. /// +/// # Signed ±1 encoding +/// +/// This detector already emits the uniform candlestick sign convention shared +/// across the pattern family — `+1.0` bullish, `−1.0` bearish, `0.0` no +/// pattern — so it drops straight into a machine-learning feature matrix where +/// the bullish and bearish variants of the pattern occupy a single dimension. +/// /// # Example /// /// ``` diff --git a/crates/wickra-core/src/indicators/three_soldiers_or_crows.rs b/crates/wickra-core/src/indicators/three_soldiers_or_crows.rs index c9178f0b..917da815 100644 --- a/crates/wickra-core/src/indicators/three_soldiers_or_crows.rs +++ b/crates/wickra-core/src/indicators/three_soldiers_or_crows.rs @@ -20,6 +20,13 @@ use crate::traits::Indicator; /// Pattern-shape check only — no trend filter is applied; combine with a trend /// indicator for actionable signals. /// +/// # Signed ±1 encoding +/// +/// This detector already emits the uniform candlestick sign convention shared +/// across the pattern family — `+1.0` bullish, `−1.0` bearish, `0.0` no +/// pattern — so it drops straight into a machine-learning feature matrix where +/// the bullish and bearish variants of the pattern occupy a single dimension. +/// /// # Example /// /// ``` diff --git a/crates/wickra-core/src/indicators/tweezer.rs b/crates/wickra-core/src/indicators/tweezer.rs index 57057cc3..4aeb155f 100644 --- a/crates/wickra-core/src/indicators/tweezer.rs +++ b/crates/wickra-core/src/indicators/tweezer.rs @@ -22,6 +22,13 @@ use crate::traits::Indicator; /// Pattern-shape check only — no trend filter is applied; combine with a trend /// indicator for actionable signals. /// +/// # Signed ±1 encoding +/// +/// This detector already emits the uniform candlestick sign convention shared +/// across the pattern family — `+1.0` bullish, `−1.0` bearish, `0.0` no +/// pattern — so it drops straight into a machine-learning feature matrix where +/// the bullish and bearish variants of the pattern occupy a single dimension. +/// /// # Example /// /// ``` diff --git a/fuzz/fuzz_targets/indicator_update_candle.rs b/fuzz/fuzz_targets/indicator_update_candle.rs index 1bb6a11c..38b1b76b 100644 --- a/fuzz/fuzz_targets/indicator_update_candle.rs +++ b/fuzz/fuzz_targets/indicator_update_candle.rs @@ -295,6 +295,7 @@ fuzz_target!(|data: Vec| { // --- Candlestick Patterns (family 14) --- drive(Doji::new, &candles); + drive(|| Doji::new().signed(), &candles); drive(Hammer::new, &candles); drive(InvertedHammer::new, &candles); drive(HangingMan::new, &candles);