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:
kingchenc
2026-06-01 14:37:20 +02:00
committed by GitHub
parent 4631519885
commit 0479191b66
24 changed files with 531 additions and 15 deletions
+138 -7
View File
@@ -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 highlow 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<Candle> = (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::<Vec<_>>()
);
}
#[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));
}
}
@@ -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
///
/// ```
@@ -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
///
/// ```
@@ -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
///
/// ```
@@ -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
///
/// ```
@@ -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
///
/// ```
@@ -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
///
/// ```
@@ -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
///
/// ```
@@ -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
///
/// ```
@@ -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
///
/// ```
@@ -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
///
/// ```
@@ -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
///
/// ```
@@ -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
///
/// ```
@@ -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
///
/// ```
@@ -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
///
/// ```