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:
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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
|
||||
});
|
||||
|
||||
Vendored
+2
-1
@@ -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<number>, high: Array<number>, low: Array<number>, close: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
isSigned(): boolean
|
||||
}
|
||||
export type HammerNode = Hammer
|
||||
export declare class Hammer {
|
||||
|
||||
@@ -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<bool>) -> 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<Option<f64>> {
|
||||
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<f64>,
|
||||
high: Vec<f64>,
|
||||
low: Vec<f64>,
|
||||
close: Vec<f64>,
|
||||
) -> napi::Result<Vec<f64>> {
|
||||
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");
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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<bool>) -> 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<Option<f64>, 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<Float64Array, JsError> {
|
||||
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);
|
||||
|
||||
@@ -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<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
|
||||
///
|
||||
/// ```
|
||||
|
||||
@@ -295,6 +295,7 @@ fuzz_target!(|data: Vec<f64>| {
|
||||
|
||||
// --- 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);
|
||||
|
||||
Reference in New Issue
Block a user