feat(breadth): complete the Market Breadth family (14 indicators) (#157)

Completes expansion-roadmap block **A2 — Market Breadth**: the 14 indicators that remained after the `AdvanceDecline` bootstrap, all built on the existing `CrossSection` input.

## Indicators (all scalar `Indicator<Input = CrossSection, Output = f64>`)

| Indicator | Reading |
|-----------|---------|
| `AdvanceDeclineRatio` | advancers / decliners |
| `AdVolumeLine` | cumulative net advancing volume |
| `McClellanOscillator` | 19/39 EMAs of ratio-adjusted net advances |
| `McClellanSummationIndex` | running total of the oscillator |
| `Trin` (Arms Index) | A/D ratio over up/down volume ratio |
| `BreadthThrust` (Zweig) | SMA of the advancing-issues share |
| `NewHighsNewLows` | new highs − new lows |
| `HighLowIndex` | SMA of the record-high percent |
| `PercentAboveMa` | % of the universe above its MA |
| `UpDownVolumeRatio` | advancing / declining volume |
| `BullishPercentIndex` | % on a point-and-figure buy signal |
| `CumulativeVolumeIndex` | volume-normalised cumulative net advancing volume |
| `AbsoluteBreadthIndex` | \|advancers − decliners\| |
| `TickIndex` | instantaneous net advancers − decliners |

## Input model

`AdVolumeLine` and `CumulativeVolumeIndex` are kept distinct (the latter normalises each tick's net advancing volume by total volume, so it stays comparable across volume regimes). `PercentAboveMa` and `BullishPercentIndex` need a per-symbol state signal that `Member` did not carry, so `Member` gains two additive flags (`above_ma`, `on_buy_signal`) via a new `Member::with_signals` constructor; the 4-arg `Member::new` leaves both cleared, so every existing caller and binding is unchanged. `CrossSection` gains volume / new-extreme / state aggregation helpers.

## Wiring

Fully wired across the Rust core, the python/node/wasm bindings, the cross-section fuzz target, the README + docs indicator counters (325 → 339), and dedicated python/node streaming-vs-batch tests. `fmt` / `test --workspace --all-features` / `clippy --workspace -D warnings` / node build+test / pytest all green locally.
This commit is contained in:
kingchenc
2026-06-03 17:24:33 +02:00
committed by GitHub
parent c44f625e69
commit c096943bdf
30 changed files with 5648 additions and 63 deletions
+658
View File
@@ -8437,6 +8437,664 @@ impl WasmAdvanceDecline {
}
}
fn build_cross_section_above_ma(
change: &[f64],
volume: &[f64],
new_high: &[f64],
new_low: &[f64],
above_ma: &[f64],
) -> Result<wc::CrossSection, JsError> {
if change.len() != volume.len()
|| change.len() != new_high.len()
|| change.len() != new_low.len()
|| change.len() != above_ma.len()
{
return Err(JsError::new(
"change, volume, newHigh, newLow and aboveMa must be equal length",
));
}
let members = (0..change.len())
.map(|i| {
wc::Member::with_signals(
change[i],
volume[i],
new_high[i] != 0.0,
new_low[i] != 0.0,
above_ma[i] != 0.0,
false,
)
})
.collect();
wc::CrossSection::new(members, 0).map_err(map_err)
}
fn build_cross_section_buy(
change: &[f64],
volume: &[f64],
new_high: &[f64],
new_low: &[f64],
on_buy_signal: &[f64],
) -> Result<wc::CrossSection, JsError> {
if change.len() != volume.len()
|| change.len() != new_high.len()
|| change.len() != new_low.len()
|| change.len() != on_buy_signal.len()
{
return Err(JsError::new(
"change, volume, newHigh, newLow and onBuySignal must be equal length",
));
}
let members = (0..change.len())
.map(|i| {
wc::Member::with_signals(
change[i],
volume[i],
new_high[i] != 0.0,
new_low[i] != 0.0,
false,
on_buy_signal[i] != 0.0,
)
})
.collect();
wc::CrossSection::new(members, 0).map_err(map_err)
}
#[wasm_bindgen(js_name = AdvanceDeclineRatio)]
pub struct WasmAdvanceDeclineRatio {
inner: wc::AdvanceDeclineRatio,
}
impl Default for WasmAdvanceDeclineRatio {
fn default() -> Self {
Self::new()
}
}
#[wasm_bindgen(js_class = AdvanceDeclineRatio)]
impl WasmAdvanceDeclineRatio {
#[wasm_bindgen(constructor)]
pub fn new() -> WasmAdvanceDeclineRatio {
Self {
inner: wc::AdvanceDeclineRatio::new(),
}
}
pub fn update(
&mut self,
change: Vec<f64>,
volume: Vec<f64>,
new_high: Vec<f64>,
new_low: Vec<f64>,
) -> Result<Option<f64>, JsError> {
Ok(self
.inner
.update(build_cross_section(&change, &volume, &new_high, &new_low)?))
}
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 = AdVolumeLine)]
pub struct WasmAdVolumeLine {
inner: wc::AdVolumeLine,
}
impl Default for WasmAdVolumeLine {
fn default() -> Self {
Self::new()
}
}
#[wasm_bindgen(js_class = AdVolumeLine)]
impl WasmAdVolumeLine {
#[wasm_bindgen(constructor)]
pub fn new() -> WasmAdVolumeLine {
Self {
inner: wc::AdVolumeLine::new(),
}
}
pub fn update(
&mut self,
change: Vec<f64>,
volume: Vec<f64>,
new_high: Vec<f64>,
new_low: Vec<f64>,
) -> Result<Option<f64>, JsError> {
Ok(self
.inner
.update(build_cross_section(&change, &volume, &new_high, &new_low)?))
}
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 = McClellanOscillator)]
pub struct WasmMcClellanOscillator {
inner: wc::McClellanOscillator,
}
impl Default for WasmMcClellanOscillator {
fn default() -> Self {
Self::new()
}
}
#[wasm_bindgen(js_class = McClellanOscillator)]
impl WasmMcClellanOscillator {
#[wasm_bindgen(constructor)]
pub fn new() -> WasmMcClellanOscillator {
Self {
inner: wc::McClellanOscillator::new(),
}
}
pub fn update(
&mut self,
change: Vec<f64>,
volume: Vec<f64>,
new_high: Vec<f64>,
new_low: Vec<f64>,
) -> Result<Option<f64>, JsError> {
Ok(self
.inner
.update(build_cross_section(&change, &volume, &new_high, &new_low)?))
}
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 = McClellanSummationIndex)]
pub struct WasmMcClellanSummationIndex {
inner: wc::McClellanSummationIndex,
}
impl Default for WasmMcClellanSummationIndex {
fn default() -> Self {
Self::new()
}
}
#[wasm_bindgen(js_class = McClellanSummationIndex)]
impl WasmMcClellanSummationIndex {
#[wasm_bindgen(constructor)]
pub fn new() -> WasmMcClellanSummationIndex {
Self {
inner: wc::McClellanSummationIndex::new(),
}
}
pub fn update(
&mut self,
change: Vec<f64>,
volume: Vec<f64>,
new_high: Vec<f64>,
new_low: Vec<f64>,
) -> Result<Option<f64>, JsError> {
Ok(self
.inner
.update(build_cross_section(&change, &volume, &new_high, &new_low)?))
}
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 = Trin)]
pub struct WasmTrin {
inner: wc::Trin,
}
impl Default for WasmTrin {
fn default() -> Self {
Self::new()
}
}
#[wasm_bindgen(js_class = Trin)]
impl WasmTrin {
#[wasm_bindgen(constructor)]
pub fn new() -> WasmTrin {
Self {
inner: wc::Trin::new(),
}
}
pub fn update(
&mut self,
change: Vec<f64>,
volume: Vec<f64>,
new_high: Vec<f64>,
new_low: Vec<f64>,
) -> Result<Option<f64>, JsError> {
Ok(self
.inner
.update(build_cross_section(&change, &volume, &new_high, &new_low)?))
}
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 = BreadthThrust)]
pub struct WasmBreadthThrust {
inner: wc::BreadthThrust,
}
#[wasm_bindgen(js_class = BreadthThrust)]
impl WasmBreadthThrust {
#[wasm_bindgen(constructor)]
pub fn new(period: usize) -> Result<WasmBreadthThrust, JsError> {
Ok(WasmBreadthThrust {
inner: wc::BreadthThrust::new(period).map_err(map_err)?,
})
}
pub fn update(
&mut self,
change: Vec<f64>,
volume: Vec<f64>,
new_high: Vec<f64>,
new_low: Vec<f64>,
) -> Result<Option<f64>, JsError> {
Ok(self
.inner
.update(build_cross_section(&change, &volume, &new_high, &new_low)?))
}
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 = NewHighsNewLows)]
pub struct WasmNewHighsNewLows {
inner: wc::NewHighsNewLows,
}
impl Default for WasmNewHighsNewLows {
fn default() -> Self {
Self::new()
}
}
#[wasm_bindgen(js_class = NewHighsNewLows)]
impl WasmNewHighsNewLows {
#[wasm_bindgen(constructor)]
pub fn new() -> WasmNewHighsNewLows {
Self {
inner: wc::NewHighsNewLows::new(),
}
}
pub fn update(
&mut self,
change: Vec<f64>,
volume: Vec<f64>,
new_high: Vec<f64>,
new_low: Vec<f64>,
) -> Result<Option<f64>, JsError> {
Ok(self
.inner
.update(build_cross_section(&change, &volume, &new_high, &new_low)?))
}
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 = HighLowIndex)]
pub struct WasmHighLowIndex {
inner: wc::HighLowIndex,
}
#[wasm_bindgen(js_class = HighLowIndex)]
impl WasmHighLowIndex {
#[wasm_bindgen(constructor)]
pub fn new(period: usize) -> Result<WasmHighLowIndex, JsError> {
Ok(WasmHighLowIndex {
inner: wc::HighLowIndex::new(period).map_err(map_err)?,
})
}
pub fn update(
&mut self,
change: Vec<f64>,
volume: Vec<f64>,
new_high: Vec<f64>,
new_low: Vec<f64>,
) -> Result<Option<f64>, JsError> {
Ok(self
.inner
.update(build_cross_section(&change, &volume, &new_high, &new_low)?))
}
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 = PercentAboveMa)]
pub struct WasmPercentAboveMa {
inner: wc::PercentAboveMa,
}
impl Default for WasmPercentAboveMa {
fn default() -> Self {
Self::new()
}
}
#[wasm_bindgen(js_class = PercentAboveMa)]
impl WasmPercentAboveMa {
#[wasm_bindgen(constructor)]
pub fn new() -> WasmPercentAboveMa {
Self {
inner: wc::PercentAboveMa::new(),
}
}
pub fn update(
&mut self,
change: Vec<f64>,
volume: Vec<f64>,
new_high: Vec<f64>,
new_low: Vec<f64>,
above_ma: Vec<f64>,
) -> Result<Option<f64>, JsError> {
Ok(self.inner.update(build_cross_section_above_ma(
&change, &volume, &new_high, &new_low, &above_ma,
)?))
}
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 = UpDownVolumeRatio)]
pub struct WasmUpDownVolumeRatio {
inner: wc::UpDownVolumeRatio,
}
impl Default for WasmUpDownVolumeRatio {
fn default() -> Self {
Self::new()
}
}
#[wasm_bindgen(js_class = UpDownVolumeRatio)]
impl WasmUpDownVolumeRatio {
#[wasm_bindgen(constructor)]
pub fn new() -> WasmUpDownVolumeRatio {
Self {
inner: wc::UpDownVolumeRatio::new(),
}
}
pub fn update(
&mut self,
change: Vec<f64>,
volume: Vec<f64>,
new_high: Vec<f64>,
new_low: Vec<f64>,
) -> Result<Option<f64>, JsError> {
Ok(self
.inner
.update(build_cross_section(&change, &volume, &new_high, &new_low)?))
}
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 = BullishPercentIndex)]
pub struct WasmBullishPercentIndex {
inner: wc::BullishPercentIndex,
}
impl Default for WasmBullishPercentIndex {
fn default() -> Self {
Self::new()
}
}
#[wasm_bindgen(js_class = BullishPercentIndex)]
impl WasmBullishPercentIndex {
#[wasm_bindgen(constructor)]
pub fn new() -> WasmBullishPercentIndex {
Self {
inner: wc::BullishPercentIndex::new(),
}
}
pub fn update(
&mut self,
change: Vec<f64>,
volume: Vec<f64>,
new_high: Vec<f64>,
new_low: Vec<f64>,
on_buy_signal: Vec<f64>,
) -> Result<Option<f64>, JsError> {
Ok(self.inner.update(build_cross_section_buy(
&change,
&volume,
&new_high,
&new_low,
&on_buy_signal,
)?))
}
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 = CumulativeVolumeIndex)]
pub struct WasmCumulativeVolumeIndex {
inner: wc::CumulativeVolumeIndex,
}
impl Default for WasmCumulativeVolumeIndex {
fn default() -> Self {
Self::new()
}
}
#[wasm_bindgen(js_class = CumulativeVolumeIndex)]
impl WasmCumulativeVolumeIndex {
#[wasm_bindgen(constructor)]
pub fn new() -> WasmCumulativeVolumeIndex {
Self {
inner: wc::CumulativeVolumeIndex::new(),
}
}
pub fn update(
&mut self,
change: Vec<f64>,
volume: Vec<f64>,
new_high: Vec<f64>,
new_low: Vec<f64>,
) -> Result<Option<f64>, JsError> {
Ok(self
.inner
.update(build_cross_section(&change, &volume, &new_high, &new_low)?))
}
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 = AbsoluteBreadthIndex)]
pub struct WasmAbsoluteBreadthIndex {
inner: wc::AbsoluteBreadthIndex,
}
impl Default for WasmAbsoluteBreadthIndex {
fn default() -> Self {
Self::new()
}
}
#[wasm_bindgen(js_class = AbsoluteBreadthIndex)]
impl WasmAbsoluteBreadthIndex {
#[wasm_bindgen(constructor)]
pub fn new() -> WasmAbsoluteBreadthIndex {
Self {
inner: wc::AbsoluteBreadthIndex::new(),
}
}
pub fn update(
&mut self,
change: Vec<f64>,
volume: Vec<f64>,
new_high: Vec<f64>,
new_low: Vec<f64>,
) -> Result<Option<f64>, JsError> {
Ok(self
.inner
.update(build_cross_section(&change, &volume, &new_high, &new_low)?))
}
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 = TickIndex)]
pub struct WasmTickIndex {
inner: wc::TickIndex,
}
impl Default for WasmTickIndex {
fn default() -> Self {
Self::new()
}
}
#[wasm_bindgen(js_class = TickIndex)]
impl WasmTickIndex {
#[wasm_bindgen(constructor)]
pub fn new() -> WasmTickIndex {
Self {
inner: wc::TickIndex::new(),
}
}
pub fn update(
&mut self,
change: Vec<f64>,
volume: Vec<f64>,
new_high: Vec<f64>,
new_low: Vec<f64>,
) -> Result<Option<f64>, JsError> {
Ok(self
.inner
.update(build_cross_section(&change, &volume, &new_high, &new_low)?))
}
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()
}
}
#[cfg(test)]
mod tests {
use super::*;