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:
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user