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:
@@ -6,6 +6,20 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
- **TICK Index** — instantaneous net advancing-minus-declining issues (`TICK_INDEX`).
|
||||||
|
- **Absolute Breadth Index** — absolute value of net advancing-minus-declining issues (`ABSOLUTE_BREADTH_INDEX`).
|
||||||
|
- **Cumulative Volume Index** — running total of volume-normalised net advancing volume (`CUMULATIVE_VOLUME_INDEX`).
|
||||||
|
- **Bullish Percent Index** — percentage of the universe on a point-and-figure buy signal (`BULLISH_PERCENT_INDEX`).
|
||||||
|
- **Up/Down Volume Ratio** — advancing volume divided by declining volume (`UP_DOWN_VOLUME_RATIO`).
|
||||||
|
- **Percent Above Moving Average** — percentage of the universe trading above its reference moving average (`PERCENT_ABOVE_MA`).
|
||||||
|
- **High-Low Index** — moving average of the record-high percentage (`HIGH_LOW_INDEX`).
|
||||||
|
- **New Highs - New Lows** — net count of new period highs minus new period lows (`NEW_HIGHS_NEW_LOWS`).
|
||||||
|
- **Breadth Thrust** — moving average of the advancing-issues share (Zweig) (`BREADTH_THRUST`).
|
||||||
|
- **TRIN / Arms Index** — advance-decline ratio divided by the up-down volume ratio (`TRIN`).
|
||||||
|
- **McClellan Summation Index** — running cumulative total of the McClellan Oscillator (`MCCLELLAN_SUMMATION_INDEX`).
|
||||||
|
- **McClellan Oscillator** — spread between a 19- and 39-period EMA of ratio-adjusted net advances (`MCCLELLAN_OSCILLATOR`).
|
||||||
|
- **Advance/Decline Volume Line** — cumulative net advancing-minus-declining volume across the universe (`AD_VOLUME_LINE`).
|
||||||
|
- **Advance/Decline Ratio** — advancing issues divided by declining issues across the universe (`ADVANCE_DECLINE_RATIO`).
|
||||||
|
|
||||||
## [0.4.7] - 2026-06-03
|
## [0.4.7] - 2026-06-03
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://wickra.org"><img src="https://raw.githubusercontent.com/wickra-lib/.github/main/profile/wickra-banner.webp?v=325" alt="Wickra — streaming-first technical indicators" width="100%"></a>
|
<a href="https://wickra.org"><img src="https://raw.githubusercontent.com/wickra-lib/.github/main/profile/wickra-banner.webp?v=339" alt="Wickra — streaming-first technical indicators" width="100%"></a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
[](https://github.com/wickra-lib/wickra/actions/workflows/ci.yml)
|
[](https://github.com/wickra-lib/wickra/actions/workflows/ci.yml)
|
||||||
@@ -47,7 +47,7 @@ Full documentation lives at **[docs.wickra.org](https://docs.wickra.org)**:
|
|||||||
[Node](https://docs.wickra.org/Quickstart-Node),
|
[Node](https://docs.wickra.org/Quickstart-Node),
|
||||||
[WASM](https://docs.wickra.org/Quickstart-WASM).
|
[WASM](https://docs.wickra.org/Quickstart-WASM).
|
||||||
- **Indicators** — a per-indicator deep dive (formula, parameters, warmup) for
|
- **Indicators** — a per-indicator deep dive (formula, parameters, warmup) for
|
||||||
every one of the 325 indicators; start at the
|
every one of the 339 indicators; start at the
|
||||||
[indicators overview](https://docs.wickra.org/Indicators-Overview).
|
[indicators overview](https://docs.wickra.org/Indicators-Overview).
|
||||||
- **Reference** — [warmup periods](https://docs.wickra.org/Warmup-Periods),
|
- **Reference** — [warmup periods](https://docs.wickra.org/Warmup-Periods),
|
||||||
[streaming vs batch](https://docs.wickra.org/Streaming-vs-Batch),
|
[streaming vs batch](https://docs.wickra.org/Streaming-vs-Batch),
|
||||||
@@ -135,7 +135,7 @@ python -m benchmarks.compare_libraries
|
|||||||
|
|
||||||
## Indicators
|
## Indicators
|
||||||
|
|
||||||
325 streaming-first indicators across twenty families. Every one passes the
|
339 streaming-first indicators across twenty families. Every one passes the
|
||||||
`batch == streaming` equivalence test, reference-value tests, and reset
|
`batch == streaming` equivalence test, reference-value tests, and reset
|
||||||
semantics tests. Each has a per-indicator deep dive (formula, parameters,
|
semantics tests. Each has a per-indicator deep dive (formula, parameters,
|
||||||
warmup) at [docs.wickra.org](https://docs.wickra.org/Indicators-Overview).
|
warmup) at [docs.wickra.org](https://docs.wickra.org/Indicators-Overview).
|
||||||
@@ -160,7 +160,7 @@ warmup) at [docs.wickra.org](https://docs.wickra.org/Indicators-Overview).
|
|||||||
| Microstructure | Order-Book Imbalance (Top-1 / Top-N / Full), Microprice, Quoted Spread, Depth Slope, Signed Volume, Cumulative Volume Delta, Trade Imbalance, Effective Spread, Realized Spread, Kyle's Lambda, Footprint |
|
| Microstructure | Order-Book Imbalance (Top-1 / Top-N / Full), Microprice, Quoted Spread, Depth Slope, Signed Volume, Cumulative Volume Delta, Trade Imbalance, Effective Spread, Realized Spread, Kyle's Lambda, Footprint |
|
||||||
| Derivatives | Funding Rate, Funding Rate Mean, Funding Rate Z-Score, Funding Basis, Open-Interest Delta, OI / Price Divergence, OI-Weighted Price, Long/Short Ratio, Taker Buy/Sell Ratio, Liquidation Features, Term-Structure Basis, Calendar Spread |
|
| Derivatives | Funding Rate, Funding Rate Mean, Funding Rate Z-Score, Funding Basis, Open-Interest Delta, OI / Price Divergence, OI-Weighted Price, Long/Short Ratio, Taker Buy/Sell Ratio, Liquidation Features, Term-Structure Basis, Calendar Spread |
|
||||||
| Market Profile | Value Area (POC / VAH / VAL), Volume Profile (histogram), TPO Profile, Initial Balance, Opening Range |
|
| Market Profile | Value Area (POC / VAH / VAL), Volume Profile (histogram), TPO Profile, Initial Balance, Opening Range |
|
||||||
| Market Breadth | Advance/Decline Line |
|
| Market Breadth | Advance/Decline Line, Advance/Decline Ratio, Advance/Decline Volume Line, McClellan Oscillator, McClellan Summation Index, TRIN / Arms Index, Breadth Thrust, New Highs - New Lows, High-Low Index, Percent Above Moving Average, Up/Down Volume Ratio, Bullish Percent Index, Cumulative Volume Index, Absolute Breadth Index, TICK Index |
|
||||||
| 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) |
|
| 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,
|
Every candlestick pattern emits a signed per-bar value — `+1.0` bullish,
|
||||||
@@ -240,7 +240,7 @@ A Python live-trading example using the public `websockets` package lives at
|
|||||||
```
|
```
|
||||||
wickra/
|
wickra/
|
||||||
├── crates/
|
├── crates/
|
||||||
│ ├── wickra-core/ core engine + all 325 indicators
|
│ ├── wickra-core/ core engine + all 339 indicators
|
||||||
│ ├── wickra/ top-level facade crate (publishes on crates.io) + benches/
|
│ ├── wickra/ top-level facade crate (publishes on crates.io) + benches/
|
||||||
│ └── wickra-data/ CSV reader, tick aggregator, live exchange feeds
|
│ └── wickra-data/ CSV reader, tick aggregator, live exchange feeds
|
||||||
├── bindings/
|
├── bindings/
|
||||||
|
|||||||
@@ -1284,6 +1284,112 @@ test('market breadth: AdvanceDecline rejects ragged universe', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('market breadth: 14 indicators reference values + batch parity', () => {
|
||||||
|
const flags4 = [false, false, false, false];
|
||||||
|
|
||||||
|
// Advance/Decline Ratio: 3/1 = 3 ; 0 advancers -> 0.
|
||||||
|
const adr = new wickra.AdvanceDeclineRatio();
|
||||||
|
assert.equal(adr.update([1, 1, 1, -1], [10, 10, 10, 10], flags4, flags4), 3.0);
|
||||||
|
assert.equal(adr.update([-1, -1, -1, -1], [10, 10, 10, 10], flags4, flags4), 0.0);
|
||||||
|
assert.deepEqual(
|
||||||
|
Array.from(
|
||||||
|
new wickra.AdvanceDeclineRatio().batch(
|
||||||
|
[[1, 1, 1, -1], [-1, -1, -1, -1]],
|
||||||
|
[[10, 10, 10, 10], [10, 10, 10, 10]],
|
||||||
|
[flags4, flags4],
|
||||||
|
[flags4, flags4],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
[3.0, 0.0],
|
||||||
|
);
|
||||||
|
|
||||||
|
// AD Volume Line: cumulative net advancing volume.
|
||||||
|
const adv = new wickra.AdVolumeLine();
|
||||||
|
assert.equal(adv.update([1, -1], [150, 50], [false, false], [false, false]), 100.0);
|
||||||
|
assert.equal(adv.update([1, -1], [60, 60], [false, false], [false, false]), 100.0);
|
||||||
|
|
||||||
|
// McClellan Oscillator + Summation: seed 0, then -50.
|
||||||
|
const osc = new wickra.McClellanOscillator();
|
||||||
|
assert.ok(Math.abs(osc.update([1, 1, 1, -1], [10, 10, 10, 10], flags4, flags4)) < 1e-9);
|
||||||
|
assert.ok(Math.abs(osc.update([-1, -1, -1, 1], [10, 10, 10, 10], flags4, flags4) - -50.0) < 1e-9);
|
||||||
|
const msi = new wickra.McClellanSummationIndex();
|
||||||
|
assert.ok(Math.abs(msi.update([1, 1, 1, -1], [10, 10, 10, 10], flags4, flags4)) < 1e-9);
|
||||||
|
assert.ok(Math.abs(msi.update([-1, -1, -1, 1], [10, 10, 10, 10], flags4, flags4) - -50.0) < 1e-9);
|
||||||
|
|
||||||
|
// TRIN: balanced breadth -> 1.
|
||||||
|
assert.ok(
|
||||||
|
Math.abs(new wickra.Trin().update([1, 1, 1, -1], [50, 50, 50, 50], flags4, flags4) - 1.0) < 1e-9,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Breadth Thrust(2): warmup null, then SMA(2) of [0.8, 0.6] = 0.7.
|
||||||
|
const bt = new wickra.BreadthThrust(2);
|
||||||
|
const up10 = Array(10).fill(false);
|
||||||
|
assert.equal(bt.update([...Array(8).fill(1), -1, -1], Array(10).fill(10), up10, up10), null);
|
||||||
|
assert.ok(
|
||||||
|
Math.abs(bt.update([...Array(6).fill(1), -1, -1, -1, -1], Array(10).fill(10), up10, up10) - 0.7) < 1e-9,
|
||||||
|
);
|
||||||
|
|
||||||
|
// New Highs - New Lows: 2 - 1 = 1.
|
||||||
|
assert.equal(
|
||||||
|
new wickra.NewHighsNewLows().update([1, 1, -1], [10, 10, 10], [true, true, false], [false, false, true]),
|
||||||
|
1.0,
|
||||||
|
);
|
||||||
|
|
||||||
|
// High-Low Index(2): warmup null, then SMA(2) of [80, 60] = 70.
|
||||||
|
const hli = new wickra.HighLowIndex(2);
|
||||||
|
assert.equal(
|
||||||
|
hli.update(Array(10).fill(1), Array(10).fill(10), [...Array(8).fill(true), false, false], [...Array(8).fill(false), true, true]),
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
assert.ok(
|
||||||
|
Math.abs(
|
||||||
|
hli.update(Array(10).fill(1), Array(10).fill(10), [...Array(6).fill(true), false, false, false, false], [...Array(6).fill(false), true, true, true, true]) - 70.0,
|
||||||
|
) < 1e-9,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Percent Above MA: 3/4 -> 75 (5-array update with aboveMa).
|
||||||
|
assert.equal(
|
||||||
|
new wickra.PercentAboveMa().update([1, 1, 1, -1], [10, 10, 10, 10], flags4, flags4, [true, true, true, false]),
|
||||||
|
75.0,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Up/Down Volume Ratio: 150/50 = 3.
|
||||||
|
assert.equal(
|
||||||
|
new wickra.UpDownVolumeRatio().update([1, -1], [150, 50], [false, false], [false, false]),
|
||||||
|
3.0,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Bullish Percent Index: 2/4 -> 50 (5-array update with onBuySignal).
|
||||||
|
assert.equal(
|
||||||
|
new wickra.BullishPercentIndex().update([1, 1, -1, -1], [10, 10, 10, 10], flags4, flags4, [true, true, false, false]),
|
||||||
|
50.0,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Cumulative Volume Index: (100/200) -> 0.5.
|
||||||
|
assert.ok(
|
||||||
|
Math.abs(new wickra.CumulativeVolumeIndex().update([1, -1], [150, 50], [false, false], [false, false]) - 0.5) < 1e-9,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Absolute Breadth Index: |2 - 3| = 1.
|
||||||
|
assert.equal(
|
||||||
|
new wickra.AbsoluteBreadthIndex().update([1, 1, -1, -1, -1], Array(5).fill(10), Array(5).fill(false), Array(5).fill(false)),
|
||||||
|
1.0,
|
||||||
|
);
|
||||||
|
|
||||||
|
// TICK Index: 2 - 3 = -1.
|
||||||
|
assert.equal(
|
||||||
|
new wickra.TickIndex().update([1, 1, -1, -1, -1], Array(5).fill(10), Array(5).fill(false), Array(5).fill(false)),
|
||||||
|
-1.0,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('market breadth: rejects ragged universe', () => {
|
||||||
|
assert.throws(() => new wickra.Trin().update([1, -1], [10], [false, false], [false, false]));
|
||||||
|
assert.throws(() =>
|
||||||
|
new wickra.PercentAboveMa().update([1, -1], [10, 10], [false, false], [false, false], [true]),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
test('OI / flow / liquidation indicators reference values', () => {
|
test('OI / flow / liquidation indicators reference values', () => {
|
||||||
// OI +10% while price flat -> divergence +0.1.
|
// OI +10% while price flat -> divergence +0.1.
|
||||||
const div = new wickra.OIPriceDivergence(1);
|
const div = new wickra.OIPriceDivergence(1);
|
||||||
|
|||||||
Vendored
+126
@@ -3253,6 +3253,132 @@ export declare class AdvanceDecline {
|
|||||||
isReady(): boolean
|
isReady(): boolean
|
||||||
warmupPeriod(): number
|
warmupPeriod(): number
|
||||||
}
|
}
|
||||||
|
export type AdvanceDeclineRatioNode = AdvanceDeclineRatio
|
||||||
|
export declare class AdvanceDeclineRatio {
|
||||||
|
constructor()
|
||||||
|
update(change: Array<number>, volume: Array<number>, newHigh: Array<boolean>, newLow: Array<boolean>): number | null
|
||||||
|
batch(change: Array<Array<number>>, volume: Array<Array<number>>, newHigh: Array<Array<boolean>>, newLow: Array<Array<boolean>>): Array<number>
|
||||||
|
reset(): void
|
||||||
|
isReady(): boolean
|
||||||
|
warmupPeriod(): number
|
||||||
|
}
|
||||||
|
export type AdVolumeLineNode = AdVolumeLine
|
||||||
|
export declare class AdVolumeLine {
|
||||||
|
constructor()
|
||||||
|
update(change: Array<number>, volume: Array<number>, newHigh: Array<boolean>, newLow: Array<boolean>): number | null
|
||||||
|
batch(change: Array<Array<number>>, volume: Array<Array<number>>, newHigh: Array<Array<boolean>>, newLow: Array<Array<boolean>>): Array<number>
|
||||||
|
reset(): void
|
||||||
|
isReady(): boolean
|
||||||
|
warmupPeriod(): number
|
||||||
|
}
|
||||||
|
export type McClellanOscillatorNode = McClellanOscillator
|
||||||
|
export declare class McClellanOscillator {
|
||||||
|
constructor()
|
||||||
|
update(change: Array<number>, volume: Array<number>, newHigh: Array<boolean>, newLow: Array<boolean>): number | null
|
||||||
|
batch(change: Array<Array<number>>, volume: Array<Array<number>>, newHigh: Array<Array<boolean>>, newLow: Array<Array<boolean>>): Array<number>
|
||||||
|
reset(): void
|
||||||
|
isReady(): boolean
|
||||||
|
warmupPeriod(): number
|
||||||
|
}
|
||||||
|
export type McClellanSummationIndexNode = McClellanSummationIndex
|
||||||
|
export declare class McClellanSummationIndex {
|
||||||
|
constructor()
|
||||||
|
update(change: Array<number>, volume: Array<number>, newHigh: Array<boolean>, newLow: Array<boolean>): number | null
|
||||||
|
batch(change: Array<Array<number>>, volume: Array<Array<number>>, newHigh: Array<Array<boolean>>, newLow: Array<Array<boolean>>): Array<number>
|
||||||
|
reset(): void
|
||||||
|
isReady(): boolean
|
||||||
|
warmupPeriod(): number
|
||||||
|
}
|
||||||
|
export type TrinNode = Trin
|
||||||
|
export declare class Trin {
|
||||||
|
constructor()
|
||||||
|
update(change: Array<number>, volume: Array<number>, newHigh: Array<boolean>, newLow: Array<boolean>): number | null
|
||||||
|
batch(change: Array<Array<number>>, volume: Array<Array<number>>, newHigh: Array<Array<boolean>>, newLow: Array<Array<boolean>>): Array<number>
|
||||||
|
reset(): void
|
||||||
|
isReady(): boolean
|
||||||
|
warmupPeriod(): number
|
||||||
|
}
|
||||||
|
export type BreadthThrustNode = BreadthThrust
|
||||||
|
export declare class BreadthThrust {
|
||||||
|
constructor(period: number)
|
||||||
|
update(change: Array<number>, volume: Array<number>, newHigh: Array<boolean>, newLow: Array<boolean>): number | null
|
||||||
|
batch(change: Array<Array<number>>, volume: Array<Array<number>>, newHigh: Array<Array<boolean>>, newLow: Array<Array<boolean>>): Array<number>
|
||||||
|
reset(): void
|
||||||
|
isReady(): boolean
|
||||||
|
warmupPeriod(): number
|
||||||
|
}
|
||||||
|
export type NewHighsNewLowsNode = NewHighsNewLows
|
||||||
|
export declare class NewHighsNewLows {
|
||||||
|
constructor()
|
||||||
|
update(change: Array<number>, volume: Array<number>, newHigh: Array<boolean>, newLow: Array<boolean>): number | null
|
||||||
|
batch(change: Array<Array<number>>, volume: Array<Array<number>>, newHigh: Array<Array<boolean>>, newLow: Array<Array<boolean>>): Array<number>
|
||||||
|
reset(): void
|
||||||
|
isReady(): boolean
|
||||||
|
warmupPeriod(): number
|
||||||
|
}
|
||||||
|
export type HighLowIndexNode = HighLowIndex
|
||||||
|
export declare class HighLowIndex {
|
||||||
|
constructor(period: number)
|
||||||
|
update(change: Array<number>, volume: Array<number>, newHigh: Array<boolean>, newLow: Array<boolean>): number | null
|
||||||
|
batch(change: Array<Array<number>>, volume: Array<Array<number>>, newHigh: Array<Array<boolean>>, newLow: Array<Array<boolean>>): Array<number>
|
||||||
|
reset(): void
|
||||||
|
isReady(): boolean
|
||||||
|
warmupPeriod(): number
|
||||||
|
}
|
||||||
|
export type PercentAboveMaNode = PercentAboveMa
|
||||||
|
export declare class PercentAboveMa {
|
||||||
|
constructor()
|
||||||
|
update(change: Array<number>, volume: Array<number>, newHigh: Array<boolean>, newLow: Array<boolean>, aboveMa: Array<boolean>): number | null
|
||||||
|
batch(change: Array<Array<number>>, volume: Array<Array<number>>, newHigh: Array<Array<boolean>>, newLow: Array<Array<boolean>>, aboveMa: Array<Array<boolean>>): Array<number>
|
||||||
|
reset(): void
|
||||||
|
isReady(): boolean
|
||||||
|
warmupPeriod(): number
|
||||||
|
}
|
||||||
|
export type UpDownVolumeRatioNode = UpDownVolumeRatio
|
||||||
|
export declare class UpDownVolumeRatio {
|
||||||
|
constructor()
|
||||||
|
update(change: Array<number>, volume: Array<number>, newHigh: Array<boolean>, newLow: Array<boolean>): number | null
|
||||||
|
batch(change: Array<Array<number>>, volume: Array<Array<number>>, newHigh: Array<Array<boolean>>, newLow: Array<Array<boolean>>): Array<number>
|
||||||
|
reset(): void
|
||||||
|
isReady(): boolean
|
||||||
|
warmupPeriod(): number
|
||||||
|
}
|
||||||
|
export type BullishPercentIndexNode = BullishPercentIndex
|
||||||
|
export declare class BullishPercentIndex {
|
||||||
|
constructor()
|
||||||
|
update(change: Array<number>, volume: Array<number>, newHigh: Array<boolean>, newLow: Array<boolean>, onBuySignal: Array<boolean>): number | null
|
||||||
|
batch(change: Array<Array<number>>, volume: Array<Array<number>>, newHigh: Array<Array<boolean>>, newLow: Array<Array<boolean>>, onBuySignal: Array<Array<boolean>>): Array<number>
|
||||||
|
reset(): void
|
||||||
|
isReady(): boolean
|
||||||
|
warmupPeriod(): number
|
||||||
|
}
|
||||||
|
export type CumulativeVolumeIndexNode = CumulativeVolumeIndex
|
||||||
|
export declare class CumulativeVolumeIndex {
|
||||||
|
constructor()
|
||||||
|
update(change: Array<number>, volume: Array<number>, newHigh: Array<boolean>, newLow: Array<boolean>): number | null
|
||||||
|
batch(change: Array<Array<number>>, volume: Array<Array<number>>, newHigh: Array<Array<boolean>>, newLow: Array<Array<boolean>>): Array<number>
|
||||||
|
reset(): void
|
||||||
|
isReady(): boolean
|
||||||
|
warmupPeriod(): number
|
||||||
|
}
|
||||||
|
export type AbsoluteBreadthIndexNode = AbsoluteBreadthIndex
|
||||||
|
export declare class AbsoluteBreadthIndex {
|
||||||
|
constructor()
|
||||||
|
update(change: Array<number>, volume: Array<number>, newHigh: Array<boolean>, newLow: Array<boolean>): number | null
|
||||||
|
batch(change: Array<Array<number>>, volume: Array<Array<number>>, newHigh: Array<Array<boolean>>, newLow: Array<Array<boolean>>): Array<number>
|
||||||
|
reset(): void
|
||||||
|
isReady(): boolean
|
||||||
|
warmupPeriod(): number
|
||||||
|
}
|
||||||
|
export type TickIndexNode = TickIndex
|
||||||
|
export declare class TickIndex {
|
||||||
|
constructor()
|
||||||
|
update(change: Array<number>, volume: Array<number>, newHigh: Array<boolean>, newLow: Array<boolean>): number | null
|
||||||
|
batch(change: Array<Array<number>>, volume: Array<Array<number>>, newHigh: Array<Array<boolean>>, newLow: Array<Array<boolean>>): Array<number>
|
||||||
|
reset(): void
|
||||||
|
isReady(): boolean
|
||||||
|
warmupPeriod(): number
|
||||||
|
}
|
||||||
export type SharpeRatioNode = SharpeRatio
|
export type SharpeRatioNode = SharpeRatio
|
||||||
export declare class SharpeRatio {
|
export declare class SharpeRatio {
|
||||||
constructor(period: number, riskFree: number)
|
constructor(period: number, riskFree: number)
|
||||||
|
|||||||
+15
-1
@@ -310,7 +310,7 @@ if (!nativeBinding) {
|
|||||||
throw new Error(`Failed to load native binding`)
|
throw new Error(`Failed to load native binding`)
|
||||||
}
|
}
|
||||||
|
|
||||||
const { version, SMA, EMA, WMA, RSI, DEMA, TEMA, HMA, ROC, TRIX, SMMA, TRIMA, ZLEMA, MOM, CMO, DPO, StdDev, UlcerIndex, VerticalHorizontalFilter, ZScore, McGinleyDynamic, FRAMA, SuperSmoother, FisherTransform, Decycler, CenterOfGravity, CyberneticCycle, InstantaneousTrendline, EhlersStochastic, RVIVolatility, Variance, CoefficientOfVariation, Skewness, Kurtosis, StandardError, DetrendedStdDev, RSquared, MedianAbsoluteDeviation, MIDPOINT, ROCP, ROCR, ROCR100, LINEARREG_INTERCEPT, TSF, Autocorrelation, HurstExponent, PearsonCorrelation, Beta, PairwiseBeta, SpearmanCorrelation, RollingCorrelation, RollingCovariance, OuHalfLife, SpreadHurst, DistanceSsd, BetaNeutralSpread, PairSpreadZScore, LeadLagCrossCorrelation, Cointegration, RelativeStrengthAB, VarianceRatio, GrangerCausality, KalmanHedgeRatio, SpreadBollingerBands, MACD, MACDFIX, MACDEXT, BollingerBands, ATR, PLUS_DM, MINUS_DM, PLUS_DI, MINUS_DI, DX, MIDPRICE, AVGPRICE, SAREXT, HT_PHASOR, Stochastic, OBV, ADX, ADXR, CCI, WilliamsR, MFI, PSAR, Keltner, Donchian, VWAP, RollingVWAP, AwesomeOscillator, Aroon, Inertia, ConnorsRSI, LaguerreRSI, SMI, KST, PGO, RVI, AwesomeOscillatorHistogram, STC, ElderImpulse, ZeroLagMACD, CFO, APO, KAMA, EVWMA, Alligator, JMA, VIDYA, ALMA, T3, TSI, PMO, TII, ADL, VolumePriceTrend, ChaikinMoneyFlow, ChaikinOscillator, ForceIndex, NVI, PVI, VolumeOscillator, KVO, WilliamsAD, AnchoredRSI, AnchoredVWAP, DemandIndex, TSV, VZO, MarketFacilitationIndex, EaseOfMovement, SuperTrend, ChandelierExit, ChandeKrollStop, AtrTrailingStop, HiLoActivator, VoltyStop, YoyoExit, DonchianStop, PercentageTrailingStop, StepTrailingStop, RenkoTrailingStop, TypicalPrice, MedianPrice, WeightedClose, LinearRegression, LinRegSlope, AcceleratorOscillator, BalanceOfPower, ChoppinessIndex, TrueRange, ChaikinVolatility, YangZhangVolatility, RogersSatchellVolatility, GarmanKlassVolatility, ParkinsonVolatility, LinRegAngle, BollingerBandwidth, PercentB, NATR, HistoricalVolatility, AroonOscillator, WaveTrend, RWI, Vortex, MassIndex, StochRSI, UltimateOscillator, PPO, Coppock, VWMA, MaEnvelope, AccelerationBands, StarcBands, AtrBands, HurstChannel, LinRegChannel, StandardErrorBands, DoubleBollinger, TtmSqueeze, FractalChaosBands, VwapStdDevBands, ClassicPivots, FibonacciPivots, Camarilla, WoodiePivots, DemarkPivots, WilliamsFractals, ZigZag, TDSetup, TDSequential, TDDeMarker, TDREI, TDPressure, TDCombo, TDCountdown, TDLines, TDRangeProjection, TDDifferential, TDOpen, TDRiskLevel, InverseFisherTransform, DecyclerOscillator, RoofingFilter, EmpiricalModeDecomposition, HT_DCPHASE, HT_TRENDMODE, HilbertDominantCycle, AdaptiveCycle, SineWave, MAMA, FAMA, Ichimoku, HeikinAshi, ValueArea, VolumeProfile, TpoProfile, InitialBalance, OpeningRange, Doji, Hammer, InvertedHammer, HangingMan, ShootingStar, Engulfing, Harami, MorningEveningStar, ThreeSoldiersOrCrows, PiercingDarkCloud, Marubozu, Tweezer, SpinningTop, ThreeInside, ThreeOutside, TwoCrows, UpsideGapTwoCrows, IdenticalThreeCrows, ThreeLineStrike, ThreeStarsInSouth, AbandonedBaby, AdvanceBlock, BeltHold, Breakaway, Counterattack, DojiStar, DragonflyDoji, GravestoneDoji, LongLeggedDoji, RickshawMan, EveningDojiStar, MorningDojiStar, GapSideBySideWhite, HighWave, Hikkake, HikkakeModified, HomingPigeon, OnNeck, InNeck, Thrusting, SeparatingLines, Kicking, KickingByLength, LadderBottom, MatHold, MatchingLow, LongLine, ShortLine, RisingThreeMethods, FallingThreeMethods, UpsideGapThreeMethods, DownsideGapThreeMethods, StalledPattern, StickSandwich, Takuri, ClosingMarubozu, OpeningMarubozu, TasukiGap, UniqueThreeRiver, ConcealingBabySwallow, OrderBookImbalanceTop1, OrderBookImbalanceFull, Microprice, QuotedSpread, DepthSlope, OrderBookImbalanceTopN, SignedVolume, CumulativeVolumeDelta, TradeImbalance, EffectiveSpread, RealizedSpread, KylesLambda, Footprint, FundingRate, FundingRateMean, FundingRateZScore, FundingBasis, OpenInterestDelta, OIPriceDivergence, OIWeighted, LongShortRatio, TakerBuySellRatio, LiquidationFeatures, TermStructureBasis, CalendarSpread, AdvanceDecline, SharpeRatio, SortinoRatio, CalmarRatio, OmegaRatio, MaxDrawdown, AverageDrawdown, DrawdownDuration, PainIndex, ValueAtRisk, ConditionalValueAtRisk, ProfitFactor, GainLossRatio, RecoveryFactor, KellyCriterion, TreynorRatio, InformationRatio, RenkoBars, KagiBars, PointAndFigureBars, Alpha } = nativeBinding
|
const { version, SMA, EMA, WMA, RSI, DEMA, TEMA, HMA, ROC, TRIX, SMMA, TRIMA, ZLEMA, MOM, CMO, DPO, StdDev, UlcerIndex, VerticalHorizontalFilter, ZScore, McGinleyDynamic, FRAMA, SuperSmoother, FisherTransform, Decycler, CenterOfGravity, CyberneticCycle, InstantaneousTrendline, EhlersStochastic, RVIVolatility, Variance, CoefficientOfVariation, Skewness, Kurtosis, StandardError, DetrendedStdDev, RSquared, MedianAbsoluteDeviation, MIDPOINT, ROCP, ROCR, ROCR100, LINEARREG_INTERCEPT, TSF, Autocorrelation, HurstExponent, PearsonCorrelation, Beta, PairwiseBeta, SpearmanCorrelation, RollingCorrelation, RollingCovariance, OuHalfLife, SpreadHurst, DistanceSsd, BetaNeutralSpread, PairSpreadZScore, LeadLagCrossCorrelation, Cointegration, RelativeStrengthAB, VarianceRatio, GrangerCausality, KalmanHedgeRatio, SpreadBollingerBands, MACD, MACDFIX, MACDEXT, BollingerBands, ATR, PLUS_DM, MINUS_DM, PLUS_DI, MINUS_DI, DX, MIDPRICE, AVGPRICE, SAREXT, HT_PHASOR, Stochastic, OBV, ADX, ADXR, CCI, WilliamsR, MFI, PSAR, Keltner, Donchian, VWAP, RollingVWAP, AwesomeOscillator, Aroon, Inertia, ConnorsRSI, LaguerreRSI, SMI, KST, PGO, RVI, AwesomeOscillatorHistogram, STC, ElderImpulse, ZeroLagMACD, CFO, APO, KAMA, EVWMA, Alligator, JMA, VIDYA, ALMA, T3, TSI, PMO, TII, ADL, VolumePriceTrend, ChaikinMoneyFlow, ChaikinOscillator, ForceIndex, NVI, PVI, VolumeOscillator, KVO, WilliamsAD, AnchoredRSI, AnchoredVWAP, DemandIndex, TSV, VZO, MarketFacilitationIndex, EaseOfMovement, SuperTrend, ChandelierExit, ChandeKrollStop, AtrTrailingStop, HiLoActivator, VoltyStop, YoyoExit, DonchianStop, PercentageTrailingStop, StepTrailingStop, RenkoTrailingStop, TypicalPrice, MedianPrice, WeightedClose, LinearRegression, LinRegSlope, AcceleratorOscillator, BalanceOfPower, ChoppinessIndex, TrueRange, ChaikinVolatility, YangZhangVolatility, RogersSatchellVolatility, GarmanKlassVolatility, ParkinsonVolatility, LinRegAngle, BollingerBandwidth, PercentB, NATR, HistoricalVolatility, AroonOscillator, WaveTrend, RWI, Vortex, MassIndex, StochRSI, UltimateOscillator, PPO, Coppock, VWMA, MaEnvelope, AccelerationBands, StarcBands, AtrBands, HurstChannel, LinRegChannel, StandardErrorBands, DoubleBollinger, TtmSqueeze, FractalChaosBands, VwapStdDevBands, ClassicPivots, FibonacciPivots, Camarilla, WoodiePivots, DemarkPivots, WilliamsFractals, ZigZag, TDSetup, TDSequential, TDDeMarker, TDREI, TDPressure, TDCombo, TDCountdown, TDLines, TDRangeProjection, TDDifferential, TDOpen, TDRiskLevel, InverseFisherTransform, DecyclerOscillator, RoofingFilter, EmpiricalModeDecomposition, HT_DCPHASE, HT_TRENDMODE, HilbertDominantCycle, AdaptiveCycle, SineWave, MAMA, FAMA, Ichimoku, HeikinAshi, ValueArea, VolumeProfile, TpoProfile, InitialBalance, OpeningRange, Doji, Hammer, InvertedHammer, HangingMan, ShootingStar, Engulfing, Harami, MorningEveningStar, ThreeSoldiersOrCrows, PiercingDarkCloud, Marubozu, Tweezer, SpinningTop, ThreeInside, ThreeOutside, TwoCrows, UpsideGapTwoCrows, IdenticalThreeCrows, ThreeLineStrike, ThreeStarsInSouth, AbandonedBaby, AdvanceBlock, BeltHold, Breakaway, Counterattack, DojiStar, DragonflyDoji, GravestoneDoji, LongLeggedDoji, RickshawMan, EveningDojiStar, MorningDojiStar, GapSideBySideWhite, HighWave, Hikkake, HikkakeModified, HomingPigeon, OnNeck, InNeck, Thrusting, SeparatingLines, Kicking, KickingByLength, LadderBottom, MatHold, MatchingLow, LongLine, ShortLine, RisingThreeMethods, FallingThreeMethods, UpsideGapThreeMethods, DownsideGapThreeMethods, StalledPattern, StickSandwich, Takuri, ClosingMarubozu, OpeningMarubozu, TasukiGap, UniqueThreeRiver, ConcealingBabySwallow, OrderBookImbalanceTop1, OrderBookImbalanceFull, Microprice, QuotedSpread, DepthSlope, OrderBookImbalanceTopN, SignedVolume, CumulativeVolumeDelta, TradeImbalance, EffectiveSpread, RealizedSpread, KylesLambda, Footprint, FundingRate, FundingRateMean, FundingRateZScore, FundingBasis, OpenInterestDelta, OIPriceDivergence, OIWeighted, LongShortRatio, TakerBuySellRatio, LiquidationFeatures, TermStructureBasis, CalendarSpread, AdvanceDecline, AdvanceDeclineRatio, AdVolumeLine, McClellanOscillator, McClellanSummationIndex, Trin, BreadthThrust, NewHighsNewLows, HighLowIndex, PercentAboveMa, UpDownVolumeRatio, BullishPercentIndex, CumulativeVolumeIndex, AbsoluteBreadthIndex, TickIndex, SharpeRatio, SortinoRatio, CalmarRatio, OmegaRatio, MaxDrawdown, AverageDrawdown, DrawdownDuration, PainIndex, ValueAtRisk, ConditionalValueAtRisk, ProfitFactor, GainLossRatio, RecoveryFactor, KellyCriterion, TreynorRatio, InformationRatio, RenkoBars, KagiBars, PointAndFigureBars, Alpha } = nativeBinding
|
||||||
|
|
||||||
module.exports.version = version
|
module.exports.version = version
|
||||||
module.exports.SMA = SMA
|
module.exports.SMA = SMA
|
||||||
@@ -618,6 +618,20 @@ module.exports.LiquidationFeatures = LiquidationFeatures
|
|||||||
module.exports.TermStructureBasis = TermStructureBasis
|
module.exports.TermStructureBasis = TermStructureBasis
|
||||||
module.exports.CalendarSpread = CalendarSpread
|
module.exports.CalendarSpread = CalendarSpread
|
||||||
module.exports.AdvanceDecline = AdvanceDecline
|
module.exports.AdvanceDecline = AdvanceDecline
|
||||||
|
module.exports.AdvanceDeclineRatio = AdvanceDeclineRatio
|
||||||
|
module.exports.AdVolumeLine = AdVolumeLine
|
||||||
|
module.exports.McClellanOscillator = McClellanOscillator
|
||||||
|
module.exports.McClellanSummationIndex = McClellanSummationIndex
|
||||||
|
module.exports.Trin = Trin
|
||||||
|
module.exports.BreadthThrust = BreadthThrust
|
||||||
|
module.exports.NewHighsNewLows = NewHighsNewLows
|
||||||
|
module.exports.HighLowIndex = HighLowIndex
|
||||||
|
module.exports.PercentAboveMa = PercentAboveMa
|
||||||
|
module.exports.UpDownVolumeRatio = UpDownVolumeRatio
|
||||||
|
module.exports.BullishPercentIndex = BullishPercentIndex
|
||||||
|
module.exports.CumulativeVolumeIndex = CumulativeVolumeIndex
|
||||||
|
module.exports.AbsoluteBreadthIndex = AbsoluteBreadthIndex
|
||||||
|
module.exports.TickIndex = TickIndex
|
||||||
module.exports.SharpeRatio = SharpeRatio
|
module.exports.SharpeRatio = SharpeRatio
|
||||||
module.exports.SortinoRatio = SortinoRatio
|
module.exports.SortinoRatio = SortinoRatio
|
||||||
module.exports.CalmarRatio = CalmarRatio
|
module.exports.CalmarRatio = CalmarRatio
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -352,6 +352,20 @@ from ._wickra import (
|
|||||||
TermStructureBasis,
|
TermStructureBasis,
|
||||||
CalendarSpread,
|
CalendarSpread,
|
||||||
# Market Breadth
|
# Market Breadth
|
||||||
|
TickIndex,
|
||||||
|
AbsoluteBreadthIndex,
|
||||||
|
CumulativeVolumeIndex,
|
||||||
|
BullishPercentIndex,
|
||||||
|
UpDownVolumeRatio,
|
||||||
|
PercentAboveMa,
|
||||||
|
HighLowIndex,
|
||||||
|
NewHighsNewLows,
|
||||||
|
BreadthThrust,
|
||||||
|
Trin,
|
||||||
|
McClellanSummationIndex,
|
||||||
|
McClellanOscillator,
|
||||||
|
AdVolumeLine,
|
||||||
|
AdvanceDeclineRatio,
|
||||||
AdvanceDecline,
|
AdvanceDecline,
|
||||||
# Risk / Performance
|
# Risk / Performance
|
||||||
SharpeRatio,
|
SharpeRatio,
|
||||||
@@ -702,6 +716,20 @@ __all__ = [
|
|||||||
"TermStructureBasis",
|
"TermStructureBasis",
|
||||||
"CalendarSpread",
|
"CalendarSpread",
|
||||||
# Market Breadth
|
# Market Breadth
|
||||||
|
"TickIndex",
|
||||||
|
"AbsoluteBreadthIndex",
|
||||||
|
"CumulativeVolumeIndex",
|
||||||
|
"BullishPercentIndex",
|
||||||
|
"UpDownVolumeRatio",
|
||||||
|
"PercentAboveMa",
|
||||||
|
"HighLowIndex",
|
||||||
|
"NewHighsNewLows",
|
||||||
|
"BreadthThrust",
|
||||||
|
"Trin",
|
||||||
|
"McClellanSummationIndex",
|
||||||
|
"McClellanOscillator",
|
||||||
|
"AdVolumeLine",
|
||||||
|
"AdvanceDeclineRatio",
|
||||||
"AdvanceDecline",
|
"AdvanceDecline",
|
||||||
# Risk / Performance
|
# Risk / Performance
|
||||||
"SharpeRatio",
|
"SharpeRatio",
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -2783,6 +2783,190 @@ def test_advance_decline_rejects_ragged_universe():
|
|||||||
ad.update([1.0, -1.0], [10.0], [False, False], [False, False])
|
ad.update([1.0, -1.0], [10.0], [False, False], [False, False])
|
||||||
|
|
||||||
|
|
||||||
|
def _breadth_streaming_equals_batch(indicator, change, volume, new_high, new_low):
|
||||||
|
"""Assert a 4-array breadth indicator's batch matches its streaming output."""
|
||||||
|
batch = indicator().batch(change, volume, new_high, new_low)
|
||||||
|
streamer = indicator()
|
||||||
|
streamed = np.array(
|
||||||
|
[
|
||||||
|
streamer.update(change[i], volume[i], new_high[i], new_low[i])
|
||||||
|
for i in range(len(change))
|
||||||
|
],
|
||||||
|
dtype=np.float64,
|
||||||
|
)
|
||||||
|
assert batch.shape == (len(change),)
|
||||||
|
assert _eq_nan(batch, streamed)
|
||||||
|
return batch
|
||||||
|
|
||||||
|
|
||||||
|
def test_advance_decline_ratio_breadth():
|
||||||
|
change = [[1.0, 1.0, 1.0, -1.0], [1.0, 0.0, 0.0, 0.0], [-1.0, -1.0, -1.0, -1.0]]
|
||||||
|
volume = [[10.0] * 4 for _ in range(3)]
|
||||||
|
flags = [[False] * 4 for _ in range(3)]
|
||||||
|
batch = _breadth_streaming_equals_batch(ta.AdvanceDeclineRatio, change, volume, flags, flags)
|
||||||
|
# 3/1 = 3 ; 1/max(0,1) = 1 ; 0/3 = 0.
|
||||||
|
assert list(batch) == [3.0, 1.0, 0.0]
|
||||||
|
|
||||||
|
|
||||||
|
def test_ad_volume_line_breadth():
|
||||||
|
change = [[1.0, -1.0], [1.0, -1.0], [1.0, 0.0]]
|
||||||
|
volume = [[150.0, 50.0], [60.0, 60.0], [30.0, 0.0]]
|
||||||
|
flags = [[False] * 2 for _ in range(3)]
|
||||||
|
batch = _breadth_streaming_equals_batch(ta.AdVolumeLine, change, volume, flags, flags)
|
||||||
|
# net +100 -> 100 ; net 0 -> 100 ; net +30 -> 130.
|
||||||
|
assert list(batch) == [100.0, 100.0, 130.0]
|
||||||
|
|
||||||
|
|
||||||
|
def test_mcclellan_oscillator_breadth():
|
||||||
|
change = [[1.0, 1.0, 1.0, -1.0], [-1.0, -1.0, -1.0, 1.0], [1.0, 1.0, -1.0, -1.0]]
|
||||||
|
volume = [[10.0] * 4 for _ in range(3)]
|
||||||
|
flags = [[False] * 4 for _ in range(3)]
|
||||||
|
batch = _breadth_streaming_equals_batch(ta.McClellanOscillator, change, volume, flags, flags)
|
||||||
|
# seed 0 ; -50 ; -67.5.
|
||||||
|
assert abs(batch[0]) < 1e-9
|
||||||
|
assert abs(batch[1] - (-50.0)) < 1e-9
|
||||||
|
assert abs(batch[2] - (-67.5)) < 1e-9
|
||||||
|
|
||||||
|
|
||||||
|
def test_mcclellan_summation_index_breadth():
|
||||||
|
change = [[1.0, 1.0, 1.0, -1.0], [-1.0, -1.0, -1.0, 1.0], [1.0, 1.0, -1.0, -1.0]]
|
||||||
|
volume = [[10.0] * 4 for _ in range(3)]
|
||||||
|
flags = [[False] * 4 for _ in range(3)]
|
||||||
|
batch = _breadth_streaming_equals_batch(ta.McClellanSummationIndex, change, volume, flags, flags)
|
||||||
|
# 0 ; -50 ; -117.5.
|
||||||
|
assert abs(batch[0]) < 1e-9
|
||||||
|
assert abs(batch[1] - (-50.0)) < 1e-9
|
||||||
|
assert abs(batch[2] - (-117.5)) < 1e-9
|
||||||
|
|
||||||
|
|
||||||
|
def test_trin_breadth():
|
||||||
|
change = [[1.0, 1.0, 1.0, -1.0], [1.0, 1.0, -1.0, -1.0]]
|
||||||
|
volume = [[50.0, 50.0, 50.0, 50.0], [10.0, 10.0, 40.0, 40.0]]
|
||||||
|
flags = [[False] * 4 for _ in range(2)]
|
||||||
|
batch = _breadth_streaming_equals_batch(ta.Trin, change, volume, flags, flags)
|
||||||
|
# (3/1)/(150/50) = 1 ; (2/2)/(20/80) = 4.
|
||||||
|
assert abs(batch[0] - 1.0) < 1e-9
|
||||||
|
assert abs(batch[1] - 4.0) < 1e-9
|
||||||
|
|
||||||
|
|
||||||
|
def test_breadth_thrust_breadth():
|
||||||
|
change = [[1.0] * 8 + [-1.0] * 2, [1.0] * 6 + [-1.0] * 4]
|
||||||
|
volume = [[10.0] * 10 for _ in range(2)]
|
||||||
|
flags = [[False] * 10 for _ in range(2)]
|
||||||
|
batch = ta.BreadthThrust(2).batch(change, volume, flags, flags)
|
||||||
|
streamer = ta.BreadthThrust(2)
|
||||||
|
streamed = np.array(
|
||||||
|
[streamer.update(change[i], volume[i], flags[i], flags[i]) for i in range(2)],
|
||||||
|
dtype=np.float64,
|
||||||
|
)
|
||||||
|
assert _eq_nan(batch, streamed)
|
||||||
|
# 0.8 (warmup -> NaN) ; SMA(2) of [0.8, 0.6] = 0.7.
|
||||||
|
assert math.isnan(batch[0])
|
||||||
|
assert abs(batch[1] - 0.7) < 1e-9
|
||||||
|
|
||||||
|
|
||||||
|
def test_new_highs_new_lows_breadth():
|
||||||
|
change = [[1.0, 1.0, -1.0], [1.0, -1.0, -1.0]]
|
||||||
|
volume = [[10.0] * 3 for _ in range(2)]
|
||||||
|
new_high = [[True, True, False], [True, False, False]]
|
||||||
|
new_low = [[False, False, True], [False, True, True]]
|
||||||
|
batch = _breadth_streaming_equals_batch(ta.NewHighsNewLows, change, volume, new_high, new_low)
|
||||||
|
# 2 - 1 = 1 ; 1 - 2 = -1.
|
||||||
|
assert list(batch) == [1.0, -1.0]
|
||||||
|
|
||||||
|
|
||||||
|
def test_high_low_index_breadth():
|
||||||
|
change = [[1.0] * 10, [1.0] * 10]
|
||||||
|
volume = [[10.0] * 10 for _ in range(2)]
|
||||||
|
new_high = [[True] * 8 + [False] * 2, [True] * 6 + [False] * 4]
|
||||||
|
new_low = [[False] * 8 + [True] * 2, [False] * 6 + [True] * 4]
|
||||||
|
batch = ta.HighLowIndex(2).batch(change, volume, new_high, new_low)
|
||||||
|
streamer = ta.HighLowIndex(2)
|
||||||
|
streamed = np.array(
|
||||||
|
[streamer.update(change[i], volume[i], new_high[i], new_low[i]) for i in range(2)],
|
||||||
|
dtype=np.float64,
|
||||||
|
)
|
||||||
|
assert _eq_nan(batch, streamed)
|
||||||
|
# 80% (warmup) ; SMA(2) of [80, 60] = 70.
|
||||||
|
assert math.isnan(batch[0])
|
||||||
|
assert abs(batch[1] - 70.0) < 1e-9
|
||||||
|
|
||||||
|
|
||||||
|
def test_percent_above_ma_breadth():
|
||||||
|
change = [[1.0, 1.0, 1.0, -1.0], [1.0, 1.0, -1.0, -1.0]]
|
||||||
|
volume = [[10.0] * 4 for _ in range(2)]
|
||||||
|
flags = [[False] * 4 for _ in range(2)]
|
||||||
|
above_ma = [[True, True, True, False], [True, False, False, False]]
|
||||||
|
batch = ta.PercentAboveMa().batch(change, volume, flags, flags, above_ma)
|
||||||
|
streamer = ta.PercentAboveMa()
|
||||||
|
streamed = np.array(
|
||||||
|
[streamer.update(change[i], volume[i], flags[i], flags[i], above_ma[i]) for i in range(2)],
|
||||||
|
dtype=np.float64,
|
||||||
|
)
|
||||||
|
assert _eq_nan(batch, streamed)
|
||||||
|
# 3/4 -> 75 ; 1/4 -> 25.
|
||||||
|
assert list(batch) == [75.0, 25.0]
|
||||||
|
|
||||||
|
|
||||||
|
def test_up_down_volume_ratio_breadth():
|
||||||
|
change = [[1.0, -1.0], [1.0, 0.0]]
|
||||||
|
volume = [[150.0, 50.0], [100.0, 0.0]]
|
||||||
|
flags = [[False] * 2 for _ in range(2)]
|
||||||
|
batch = _breadth_streaming_equals_batch(ta.UpDownVolumeRatio, change, volume, flags, flags)
|
||||||
|
# 150/50 = 3 ; 100/max(0,1) = 100.
|
||||||
|
assert list(batch) == [3.0, 100.0]
|
||||||
|
|
||||||
|
|
||||||
|
def test_bullish_percent_index_breadth():
|
||||||
|
change = [[1.0, 1.0, -1.0, -1.0], [1.0, 1.0, 1.0, 1.0]]
|
||||||
|
volume = [[10.0] * 4 for _ in range(2)]
|
||||||
|
flags = [[False] * 4 for _ in range(2)]
|
||||||
|
on_buy = [[True, True, False, False], [True, True, True, True]]
|
||||||
|
batch = ta.BullishPercentIndex().batch(change, volume, flags, flags, on_buy)
|
||||||
|
streamer = ta.BullishPercentIndex()
|
||||||
|
streamed = np.array(
|
||||||
|
[streamer.update(change[i], volume[i], flags[i], flags[i], on_buy[i]) for i in range(2)],
|
||||||
|
dtype=np.float64,
|
||||||
|
)
|
||||||
|
assert _eq_nan(batch, streamed)
|
||||||
|
# 2/4 -> 50 ; 4/4 -> 100.
|
||||||
|
assert list(batch) == [50.0, 100.0]
|
||||||
|
|
||||||
|
|
||||||
|
def test_cumulative_volume_index_breadth():
|
||||||
|
change = [[1.0, -1.0], [1.0, -1.0], [0.0]]
|
||||||
|
volume = [[150.0, 50.0], [60.0, 60.0], [0.0]]
|
||||||
|
new_high = [[False, False], [False, False], [False]]
|
||||||
|
new_low = [[False, False], [False, False], [False]]
|
||||||
|
batch = ta.CumulativeVolumeIndex().batch(change, volume, new_high, new_low)
|
||||||
|
streamer = ta.CumulativeVolumeIndex()
|
||||||
|
streamed = np.array(
|
||||||
|
[streamer.update(change[i], volume[i], new_high[i], new_low[i]) for i in range(3)],
|
||||||
|
dtype=np.float64,
|
||||||
|
)
|
||||||
|
assert _eq_nan(batch, streamed)
|
||||||
|
# (100/200) -> 0.5 ; net 0 -> 0.5 ; zero-volume tick -> 0.5.
|
||||||
|
assert list(batch) == [0.5, 0.5, 0.5]
|
||||||
|
|
||||||
|
|
||||||
|
def test_absolute_breadth_index_breadth():
|
||||||
|
change = [[1.0, 1.0, -1.0, -1.0, -1.0], [1.0, 1.0, 1.0, -1.0, -1.0]]
|
||||||
|
volume = [[10.0] * 5 for _ in range(2)]
|
||||||
|
flags = [[False] * 5 for _ in range(2)]
|
||||||
|
batch = _breadth_streaming_equals_batch(ta.AbsoluteBreadthIndex, change, volume, flags, flags)
|
||||||
|
# |2 - 3| = 1 ; |3 - 2| = 1.
|
||||||
|
assert list(batch) == [1.0, 1.0]
|
||||||
|
|
||||||
|
|
||||||
|
def test_tick_index_breadth():
|
||||||
|
change = [[1.0, 1.0, -1.0, -1.0, -1.0], [1.0, 1.0, 1.0, -1.0, -1.0]]
|
||||||
|
volume = [[10.0] * 5 for _ in range(2)]
|
||||||
|
flags = [[False] * 5 for _ in range(2)]
|
||||||
|
batch = _breadth_streaming_equals_batch(ta.TickIndex, change, volume, flags, flags)
|
||||||
|
# 2 - 3 = -1 ; 3 - 2 = 1.
|
||||||
|
assert list(batch) == [-1.0, 1.0]
|
||||||
|
|
||||||
|
|
||||||
def test_funding_basis_streaming_equals_batch():
|
def test_funding_basis_streaming_equals_batch():
|
||||||
n = 40
|
n = 40
|
||||||
index = np.array([100.0 + 0.5 * math.sin(i * 0.2) for i in range(n)], dtype=np.float64)
|
index = np.array([100.0 + 0.5 * math.sin(i * 0.2) for i in range(n)], dtype=np.float64)
|
||||||
|
|||||||
@@ -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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
# Proper nouns that appear in indicator documentation. They are real names,
|
# Proper nouns that appear in indicator documentation. They are real names,
|
||||||
# not code identifiers, so `clippy::doc_markdown` must not demand backticks.
|
# not code identifiers, so `clippy::doc_markdown` must not demand backticks.
|
||||||
# `..` keeps clippy's built-in default identifier list in addition to these.
|
# `..` keeps clippy's built-in default identifier list in addition to these.
|
||||||
doc-valid-idents = ["LeBeau", ".."]
|
doc-valid-idents = ["LeBeau", "McClellan", ".."]
|
||||||
|
|||||||
@@ -9,9 +9,10 @@
|
|||||||
//!
|
//!
|
||||||
//! Each [`Member`] precomputes the per-symbol signals the breadth indicators
|
//! Each [`Member`] precomputes the per-symbol signals the breadth indicators
|
||||||
//! need — a signed price `change` (whose sign classifies the symbol as
|
//! need — a signed price `change` (whose sign classifies the symbol as
|
||||||
//! advancing, declining or unchanged), the period `volume`, and the
|
//! advancing, declining or unchanged), the period `volume`, the
|
||||||
//! `new_high` / `new_low` extreme flags — so the indicators stay stateless per
|
//! `new_high` / `new_low` extreme flags, and the `above_ma` / `on_buy_signal`
|
||||||
//! tick and never have to track per-symbol history.
|
//! state flags — so the indicators stay stateless per tick and never have to
|
||||||
|
//! track per-symbol history.
|
||||||
//!
|
//!
|
||||||
//! [`DerivativesTick`]: crate::DerivativesTick
|
//! [`DerivativesTick`]: crate::DerivativesTick
|
||||||
//! [`OrderBook`]: crate::OrderBook
|
//! [`OrderBook`]: crate::OrderBook
|
||||||
@@ -28,9 +29,16 @@ use crate::error::{Error, Result};
|
|||||||
/// - `volume` is finite and non-negative.
|
/// - `volume` is finite and non-negative.
|
||||||
///
|
///
|
||||||
/// `new_high` / `new_low` are caller-supplied flags marking whether the symbol
|
/// `new_high` / `new_low` are caller-supplied flags marking whether the symbol
|
||||||
/// printed a new period extreme; they carry no numeric invariant.
|
/// printed a new period extreme; `above_ma` / `on_buy_signal` are caller-supplied
|
||||||
|
/// per-symbol state signals (whether the symbol trades above its reference moving
|
||||||
|
/// average, and whether it is on a point-and-figure buy signal). None of the four
|
||||||
|
/// flags carries a numeric invariant.
|
||||||
#[non_exhaustive]
|
#[non_exhaustive]
|
||||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
|
#[allow(
|
||||||
|
clippy::struct_excessive_bools,
|
||||||
|
reason = "the four flags are independent per-symbol breadth signals, not a state machine"
|
||||||
|
)]
|
||||||
pub struct Member {
|
pub struct Member {
|
||||||
/// Price change versus the previous close. Sign classifies the symbol:
|
/// Price change versus the previous close. Sign classifies the symbol:
|
||||||
/// positive is advancing, negative is declining, zero is unchanged.
|
/// positive is advancing, negative is declining, zero is unchanged.
|
||||||
@@ -41,10 +49,17 @@ pub struct Member {
|
|||||||
pub new_high: bool,
|
pub new_high: bool,
|
||||||
/// Whether the symbol printed a new period low.
|
/// Whether the symbol printed a new period low.
|
||||||
pub new_low: bool,
|
pub new_low: bool,
|
||||||
|
/// Whether the symbol is trading above its reference moving average
|
||||||
|
/// (consumed by the `% Above Moving Average` breadth indicator).
|
||||||
|
pub above_ma: bool,
|
||||||
|
/// Whether the symbol is on a point-and-figure buy signal
|
||||||
|
/// (consumed by the `Bullish Percent Index` breadth indicator).
|
||||||
|
pub on_buy_signal: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Member {
|
impl Member {
|
||||||
/// Assemble a cross-section member.
|
/// Assemble a cross-section member from its core signals, leaving the
|
||||||
|
/// extended per-symbol state flags (`above_ma`, `on_buy_signal`) cleared.
|
||||||
///
|
///
|
||||||
/// The field invariants documented on [`Member`] are validated centrally by
|
/// The field invariants documented on [`Member`] are validated centrally by
|
||||||
/// [`CrossSection::new`] when the member is placed into a tick; this
|
/// [`CrossSection::new`] when the member is placed into a tick; this
|
||||||
@@ -57,6 +72,37 @@ impl Member {
|
|||||||
volume,
|
volume,
|
||||||
new_high,
|
new_high,
|
||||||
new_low,
|
new_low,
|
||||||
|
above_ma: false,
|
||||||
|
on_buy_signal: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Assemble a cross-section member including the extended per-symbol state
|
||||||
|
/// signals `above_ma` and `on_buy_signal`.
|
||||||
|
///
|
||||||
|
/// Use this constructor for the breadth indicators that read per-symbol
|
||||||
|
/// state (`% Above Moving Average`, `Bullish Percent Index`); [`new`](Member::new)
|
||||||
|
/// is the shorthand that leaves both flags `false`.
|
||||||
|
#[must_use]
|
||||||
|
#[allow(
|
||||||
|
clippy::fn_params_excessive_bools,
|
||||||
|
reason = "mirrors the four independent per-symbol flag fields of Member"
|
||||||
|
)]
|
||||||
|
pub const fn with_signals(
|
||||||
|
change: f64,
|
||||||
|
volume: f64,
|
||||||
|
new_high: bool,
|
||||||
|
new_low: bool,
|
||||||
|
above_ma: bool,
|
||||||
|
on_buy_signal: bool,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
change,
|
||||||
|
volume,
|
||||||
|
new_high,
|
||||||
|
new_low,
|
||||||
|
above_ma,
|
||||||
|
on_buy_signal,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -126,6 +172,56 @@ impl CrossSection {
|
|||||||
pub fn decliners(&self) -> usize {
|
pub fn decliners(&self) -> usize {
|
||||||
self.members.iter().filter(|m| m.change < 0.0).count()
|
self.members.iter().filter(|m| m.change < 0.0).count()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Total volume traded by advancing symbols (those with positive `change`).
|
||||||
|
#[must_use]
|
||||||
|
pub fn advancing_volume(&self) -> f64 {
|
||||||
|
self.members
|
||||||
|
.iter()
|
||||||
|
.filter(|m| m.change > 0.0)
|
||||||
|
.map(|m| m.volume)
|
||||||
|
.sum()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Total volume traded by declining symbols (those with negative `change`).
|
||||||
|
#[must_use]
|
||||||
|
pub fn declining_volume(&self) -> f64 {
|
||||||
|
self.members
|
||||||
|
.iter()
|
||||||
|
.filter(|m| m.change < 0.0)
|
||||||
|
.map(|m| m.volume)
|
||||||
|
.sum()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Total volume traded across the whole universe.
|
||||||
|
#[must_use]
|
||||||
|
pub fn total_volume(&self) -> f64 {
|
||||||
|
self.members.iter().map(|m| m.volume).sum()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Number of symbols that printed a new period high.
|
||||||
|
#[must_use]
|
||||||
|
pub fn new_highs(&self) -> usize {
|
||||||
|
self.members.iter().filter(|m| m.new_high).count()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Number of symbols that printed a new period low.
|
||||||
|
#[must_use]
|
||||||
|
pub fn new_lows(&self) -> usize {
|
||||||
|
self.members.iter().filter(|m| m.new_low).count()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Number of symbols trading above their reference moving average.
|
||||||
|
#[must_use]
|
||||||
|
pub fn above_ma_count(&self) -> usize {
|
||||||
|
self.members.iter().filter(|m| m.above_ma).count()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Number of symbols on a point-and-figure buy signal.
|
||||||
|
#[must_use]
|
||||||
|
pub fn on_buy_signal_count(&self) -> usize {
|
||||||
|
self.members.iter().filter(|m| m.on_buy_signal).count()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -223,4 +319,69 @@ mod tests {
|
|||||||
assert_eq!(cs.advancers(), 0);
|
assert_eq!(cs.advancers(), 0);
|
||||||
assert_eq!(cs.decliners(), 0);
|
assert_eq!(cs.decliners(), 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn new_leaves_extended_flags_cleared() {
|
||||||
|
let m = Member::new(1.0, 10.0, true, false);
|
||||||
|
assert!(!m.above_ma);
|
||||||
|
assert!(!m.on_buy_signal);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn with_signals_assembles_all_fields() {
|
||||||
|
let m = Member::with_signals(2.0, 10.0, true, false, true, true);
|
||||||
|
assert_eq!(m.change, 2.0);
|
||||||
|
assert_eq!(m.volume, 10.0);
|
||||||
|
assert!(m.new_high);
|
||||||
|
assert!(!m.new_low);
|
||||||
|
assert!(m.above_ma);
|
||||||
|
assert!(m.on_buy_signal);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn volume_helpers_bucket_by_change_sign() {
|
||||||
|
let cs = CrossSection::new(
|
||||||
|
vec![
|
||||||
|
Member::new(1.5, 100.0, false, false), // advancing
|
||||||
|
Member::new(2.0, 40.0, false, false), // advancing
|
||||||
|
Member::new(-0.5, 50.0, false, false), // declining
|
||||||
|
Member::new(0.0, 7.0, false, false), // unchanged
|
||||||
|
],
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(cs.advancing_volume(), 140.0);
|
||||||
|
assert_eq!(cs.declining_volume(), 50.0);
|
||||||
|
assert_eq!(cs.total_volume(), 197.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn high_low_helpers_count_flags() {
|
||||||
|
let cs = CrossSection::new(
|
||||||
|
vec![
|
||||||
|
Member::new(1.0, 1.0, true, false),
|
||||||
|
Member::new(1.0, 1.0, true, false),
|
||||||
|
Member::new(-1.0, 1.0, false, true),
|
||||||
|
],
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(cs.new_highs(), 2);
|
||||||
|
assert_eq!(cs.new_lows(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn state_helpers_count_extended_flags() {
|
||||||
|
let cs = CrossSection::new(
|
||||||
|
vec![
|
||||||
|
Member::with_signals(1.0, 1.0, false, false, true, true),
|
||||||
|
Member::with_signals(1.0, 1.0, false, false, true, false),
|
||||||
|
Member::with_signals(-1.0, 1.0, false, false, false, true),
|
||||||
|
],
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(cs.above_ma_count(), 2);
|
||||||
|
assert_eq!(cs.on_buy_signal_count(), 2);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,144 @@
|
|||||||
|
//! Absolute Breadth Index — the magnitude of net advancing-minus-declining issues.
|
||||||
|
|
||||||
|
use crate::cross_section::CrossSection;
|
||||||
|
use crate::traits::Indicator;
|
||||||
|
|
||||||
|
/// Absolute Breadth Index (ABI) — the absolute value of net advancing issues,
|
||||||
|
/// `|advancers - decliners|`.
|
||||||
|
///
|
||||||
|
/// The ABI ignores the *direction* of breadth and measures only its *magnitude*:
|
||||||
|
/// a high reading means the universe moved decisively one way or the other (high
|
||||||
|
/// internal activity / volatility), while a low reading means advances and
|
||||||
|
/// declines were nearly balanced (a quiet, directionless market). It is sometimes
|
||||||
|
/// called a "market thermometer" because elevated readings often cluster around
|
||||||
|
/// turning points.
|
||||||
|
///
|
||||||
|
/// `Input = CrossSection`, `Output = f64`, `warmup_period == 1`.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use wickra_core::{AbsoluteBreadthIndex, CrossSection, Indicator, Member};
|
||||||
|
///
|
||||||
|
/// let mut abi = AbsoluteBreadthIndex::new();
|
||||||
|
/// // 2 advancers, 5 decliners -> |2 - 5| = 3.
|
||||||
|
/// let tick = CrossSection::new(
|
||||||
|
/// vec![
|
||||||
|
/// Member::new(1.0, 10.0, false, false),
|
||||||
|
/// Member::new(1.0, 10.0, false, false),
|
||||||
|
/// Member::new(-1.0, 10.0, false, false),
|
||||||
|
/// Member::new(-1.0, 10.0, false, false),
|
||||||
|
/// Member::new(-1.0, 10.0, false, false),
|
||||||
|
/// Member::new(-1.0, 10.0, false, false),
|
||||||
|
/// Member::new(-1.0, 10.0, false, false),
|
||||||
|
/// ],
|
||||||
|
/// 0,
|
||||||
|
/// )
|
||||||
|
/// .unwrap();
|
||||||
|
/// assert_eq!(abi.update(tick), Some(3.0));
|
||||||
|
/// ```
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
pub struct AbsoluteBreadthIndex {
|
||||||
|
has_emitted: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AbsoluteBreadthIndex {
|
||||||
|
/// Construct a new Absolute Breadth Index indicator.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn new() -> Self {
|
||||||
|
Self { has_emitted: false }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Indicator for AbsoluteBreadthIndex {
|
||||||
|
type Input = CrossSection;
|
||||||
|
type Output = f64;
|
||||||
|
|
||||||
|
fn update(&mut self, section: CrossSection) -> Option<f64> {
|
||||||
|
let net = section.advancers() as f64 - section.decliners() as f64;
|
||||||
|
self.has_emitted = true;
|
||||||
|
Some(net.abs())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reset(&mut self) {
|
||||||
|
self.has_emitted = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn warmup_period(&self) -> usize {
|
||||||
|
1
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_ready(&self) -> bool {
|
||||||
|
self.has_emitted
|
||||||
|
}
|
||||||
|
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
"AbsoluteBreadthIndex"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::cross_section::Member;
|
||||||
|
use crate::traits::BatchExt;
|
||||||
|
|
||||||
|
fn section(up: usize, down: usize) -> CrossSection {
|
||||||
|
let mut members = Vec::new();
|
||||||
|
for _ in 0..up {
|
||||||
|
members.push(Member::new(1.0, 10.0, false, false));
|
||||||
|
}
|
||||||
|
for _ in 0..down {
|
||||||
|
members.push(Member::new(-1.0, 10.0, false, false));
|
||||||
|
}
|
||||||
|
members.push(Member::new(0.0, 10.0, false, false));
|
||||||
|
CrossSection::new(members, 0).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn accessors_and_metadata() {
|
||||||
|
let abi = AbsoluteBreadthIndex::new();
|
||||||
|
assert_eq!(abi.name(), "AbsoluteBreadthIndex");
|
||||||
|
assert_eq!(abi.warmup_period(), 1);
|
||||||
|
assert!(!abi.is_ready());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn magnitude_ignores_direction() {
|
||||||
|
let mut abi = AbsoluteBreadthIndex::new();
|
||||||
|
assert_eq!(abi.update(section(2, 5)), Some(3.0));
|
||||||
|
// Same magnitude with the direction reversed.
|
||||||
|
let mut abi2 = AbsoluteBreadthIndex::new();
|
||||||
|
assert_eq!(abi2.update(section(5, 2)), Some(3.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn balanced_universe_yields_zero() {
|
||||||
|
let mut abi = AbsoluteBreadthIndex::new();
|
||||||
|
assert_eq!(abi.update(section(3, 3)), Some(0.0));
|
||||||
|
assert!(abi.is_ready());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn reset_clears_state() {
|
||||||
|
let mut abi = AbsoluteBreadthIndex::new();
|
||||||
|
abi.update(section(2, 5));
|
||||||
|
assert!(abi.is_ready());
|
||||||
|
abi.reset();
|
||||||
|
assert!(!abi.is_ready());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn batch_equals_streaming() {
|
||||||
|
let sections = vec![section(2, 5), section(5, 2), section(3, 3)];
|
||||||
|
let mut a = AbsoluteBreadthIndex::new();
|
||||||
|
let mut b = AbsoluteBreadthIndex::new();
|
||||||
|
assert_eq!(
|
||||||
|
a.batch(§ions),
|
||||||
|
sections
|
||||||
|
.iter()
|
||||||
|
.map(|s| b.update(s.clone()))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,157 @@
|
|||||||
|
//! Advance/Decline Volume Line — cumulative net advancing-minus-declining volume.
|
||||||
|
|
||||||
|
use crate::cross_section::CrossSection;
|
||||||
|
use crate::traits::Indicator;
|
||||||
|
|
||||||
|
/// Advance/Decline Volume Line (AD Volume Line) — the running cumulative sum of
|
||||||
|
/// net advancing volume across a universe.
|
||||||
|
///
|
||||||
|
/// On each [`CrossSection`] tick the net is `advancing volume - declining volume`,
|
||||||
|
/// where advancing volume is the total volume of symbols with a positive change
|
||||||
|
/// and declining volume the total volume of symbols with a negative change. The
|
||||||
|
/// line accumulates this net over time, so a rising line means volume is flowing
|
||||||
|
/// into advancing issues (healthy participation) while a falling line warns that
|
||||||
|
/// declining issues are carrying the volume — the volume-weighted analogue of the
|
||||||
|
/// plain Advance/Decline Line.
|
||||||
|
///
|
||||||
|
/// `Input = CrossSection`, `Output = f64`, `warmup_period == 1` (defined from the
|
||||||
|
/// first tick).
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use wickra_core::{AdVolumeLine, CrossSection, Indicator, Member};
|
||||||
|
///
|
||||||
|
/// let mut adv = AdVolumeLine::new();
|
||||||
|
/// // advancing volume 150, declining volume 50 -> net +100.
|
||||||
|
/// let tick = CrossSection::new(
|
||||||
|
/// vec![
|
||||||
|
/// Member::new(1.0, 150.0, false, false),
|
||||||
|
/// Member::new(-1.0, 50.0, false, false),
|
||||||
|
/// ],
|
||||||
|
/// 0,
|
||||||
|
/// )
|
||||||
|
/// .unwrap();
|
||||||
|
/// assert_eq!(adv.update(tick), Some(100.0));
|
||||||
|
/// ```
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
pub struct AdVolumeLine {
|
||||||
|
line: f64,
|
||||||
|
has_emitted: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AdVolumeLine {
|
||||||
|
/// Construct a new Advance/Decline Volume Line indicator.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
line: 0.0,
|
||||||
|
has_emitted: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Indicator for AdVolumeLine {
|
||||||
|
type Input = CrossSection;
|
||||||
|
type Output = f64;
|
||||||
|
|
||||||
|
fn update(&mut self, section: CrossSection) -> Option<f64> {
|
||||||
|
let net = section.advancing_volume() - section.declining_volume();
|
||||||
|
self.line += net;
|
||||||
|
self.has_emitted = true;
|
||||||
|
Some(self.line)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reset(&mut self) {
|
||||||
|
self.line = 0.0;
|
||||||
|
self.has_emitted = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn warmup_period(&self) -> usize {
|
||||||
|
1
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_ready(&self) -> bool {
|
||||||
|
self.has_emitted
|
||||||
|
}
|
||||||
|
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
"AdVolumeLine"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::cross_section::Member;
|
||||||
|
use crate::traits::BatchExt;
|
||||||
|
|
||||||
|
fn tick(items: &[(f64, f64)]) -> CrossSection {
|
||||||
|
CrossSection::new(
|
||||||
|
items
|
||||||
|
.iter()
|
||||||
|
.map(|&(change, volume)| Member::new(change, volume, false, false))
|
||||||
|
.collect(),
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn accessors_and_metadata() {
|
||||||
|
let adv = AdVolumeLine::new();
|
||||||
|
assert_eq!(adv.name(), "AdVolumeLine");
|
||||||
|
assert_eq!(adv.warmup_period(), 1);
|
||||||
|
assert!(!adv.is_ready());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn first_tick_emits_net_volume() {
|
||||||
|
let mut adv = AdVolumeLine::new();
|
||||||
|
assert_eq!(adv.update(tick(&[(1.0, 150.0), (-1.0, 50.0)])), Some(100.0));
|
||||||
|
assert!(adv.is_ready());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn line_accumulates_across_ticks() {
|
||||||
|
let mut adv = AdVolumeLine::new();
|
||||||
|
assert_eq!(adv.update(tick(&[(1.0, 150.0), (-1.0, 50.0)])), Some(100.0));
|
||||||
|
assert_eq!(adv.update(tick(&[(1.0, 60.0), (-1.0, 60.0)])), Some(100.0));
|
||||||
|
assert_eq!(adv.update(tick(&[(1.0, 30.0)])), Some(130.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn unchanged_volume_is_ignored() {
|
||||||
|
let mut adv = AdVolumeLine::new();
|
||||||
|
// Unchanged symbols (zero change) contribute to neither bucket.
|
||||||
|
assert_eq!(adv.update(tick(&[(0.0, 1000.0), (1.0, 10.0)])), Some(10.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn reset_clears_state() {
|
||||||
|
let mut adv = AdVolumeLine::new();
|
||||||
|
adv.update(tick(&[(1.0, 100.0)]));
|
||||||
|
assert!(adv.is_ready());
|
||||||
|
adv.reset();
|
||||||
|
assert!(!adv.is_ready());
|
||||||
|
assert_eq!(adv.update(tick(&[(1.0, 20.0)])), Some(20.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn batch_equals_streaming() {
|
||||||
|
let sections = vec![
|
||||||
|
tick(&[(1.0, 150.0), (-1.0, 50.0)]),
|
||||||
|
tick(&[(1.0, 60.0), (-1.0, 60.0)]),
|
||||||
|
tick(&[(1.0, 30.0)]),
|
||||||
|
];
|
||||||
|
let mut a = AdVolumeLine::new();
|
||||||
|
let mut b = AdVolumeLine::new();
|
||||||
|
assert_eq!(
|
||||||
|
a.batch(§ions),
|
||||||
|
sections
|
||||||
|
.iter()
|
||||||
|
.map(|s| b.update(s.clone()))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,151 @@
|
|||||||
|
//! Advance/Decline Ratio — advancing issues divided by declining issues.
|
||||||
|
|
||||||
|
use crate::cross_section::CrossSection;
|
||||||
|
use crate::traits::Indicator;
|
||||||
|
|
||||||
|
/// Advance/Decline Ratio (ADR) — the number of advancing symbols divided by the
|
||||||
|
/// number of declining symbols across a universe.
|
||||||
|
///
|
||||||
|
/// On each [`CrossSection`] tick the ratio is `advancers / decliners`: a reading
|
||||||
|
/// above one means advancing issues outnumber declining ones (broad strength),
|
||||||
|
/// while a reading below one signals broad weakness. Because it is a ratio rather
|
||||||
|
/// than a difference, the ADR is comparable across universes of different sizes.
|
||||||
|
///
|
||||||
|
/// When a tick has no declining symbols the denominator is floored to one, so the
|
||||||
|
/// ratio degrades gracefully to the advancer count instead of dividing by zero.
|
||||||
|
///
|
||||||
|
/// `Input = CrossSection`, `Output = f64`. The ratio is defined from the first
|
||||||
|
/// tick, so `warmup_period == 1` and the indicator is ready after one update.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use wickra_core::{AdvanceDeclineRatio, CrossSection, Indicator, Member};
|
||||||
|
///
|
||||||
|
/// let mut adr = AdvanceDeclineRatio::new();
|
||||||
|
/// // 3 advancers, 1 decliner -> ratio 3.0.
|
||||||
|
/// let tick = CrossSection::new(
|
||||||
|
/// vec![
|
||||||
|
/// Member::new(1.0, 10.0, false, false),
|
||||||
|
/// Member::new(0.5, 10.0, false, false),
|
||||||
|
/// Member::new(2.0, 10.0, false, false),
|
||||||
|
/// Member::new(-1.0, 10.0, false, false),
|
||||||
|
/// ],
|
||||||
|
/// 0,
|
||||||
|
/// )
|
||||||
|
/// .unwrap();
|
||||||
|
/// assert_eq!(adr.update(tick), Some(3.0));
|
||||||
|
/// ```
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
pub struct AdvanceDeclineRatio {
|
||||||
|
has_emitted: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AdvanceDeclineRatio {
|
||||||
|
/// Construct a new Advance/Decline Ratio indicator.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn new() -> Self {
|
||||||
|
Self { has_emitted: false }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Indicator for AdvanceDeclineRatio {
|
||||||
|
type Input = CrossSection;
|
||||||
|
type Output = f64;
|
||||||
|
|
||||||
|
fn update(&mut self, section: CrossSection) -> Option<f64> {
|
||||||
|
let advancers = section.advancers() as f64;
|
||||||
|
let decliners = section.decliners().max(1) as f64;
|
||||||
|
self.has_emitted = true;
|
||||||
|
Some(advancers / decliners)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reset(&mut self) {
|
||||||
|
self.has_emitted = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn warmup_period(&self) -> usize {
|
||||||
|
1
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_ready(&self) -> bool {
|
||||||
|
self.has_emitted
|
||||||
|
}
|
||||||
|
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
"AdvanceDeclineRatio"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::cross_section::Member;
|
||||||
|
use crate::traits::BatchExt;
|
||||||
|
|
||||||
|
fn section(up: usize, down: usize) -> CrossSection {
|
||||||
|
let mut members = Vec::new();
|
||||||
|
for _ in 0..up {
|
||||||
|
members.push(Member::new(1.0, 10.0, false, false));
|
||||||
|
}
|
||||||
|
for _ in 0..down {
|
||||||
|
members.push(Member::new(-1.0, 10.0, false, false));
|
||||||
|
}
|
||||||
|
// A non-empty unchanged member guarantees a valid universe when both
|
||||||
|
// counts are zero.
|
||||||
|
members.push(Member::new(0.0, 10.0, false, false));
|
||||||
|
CrossSection::new(members, 0).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn accessors_and_metadata() {
|
||||||
|
let adr = AdvanceDeclineRatio::new();
|
||||||
|
assert_eq!(adr.name(), "AdvanceDeclineRatio");
|
||||||
|
assert_eq!(adr.warmup_period(), 1);
|
||||||
|
assert!(!adr.is_ready());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn first_tick_emits_ratio() {
|
||||||
|
let mut adr = AdvanceDeclineRatio::new();
|
||||||
|
assert_eq!(adr.update(section(3, 1)), Some(3.0));
|
||||||
|
assert!(adr.is_ready());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn zero_decliners_floors_denominator() {
|
||||||
|
let mut adr = AdvanceDeclineRatio::new();
|
||||||
|
// 4 advancers, 0 decliners -> 4 / max(0, 1) = 4.0.
|
||||||
|
assert_eq!(adr.update(section(4, 0)), Some(4.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn no_advancers_yields_zero() {
|
||||||
|
let mut adr = AdvanceDeclineRatio::new();
|
||||||
|
assert_eq!(adr.update(section(0, 5)), Some(0.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn reset_clears_state() {
|
||||||
|
let mut adr = AdvanceDeclineRatio::new();
|
||||||
|
adr.update(section(3, 1));
|
||||||
|
assert!(adr.is_ready());
|
||||||
|
adr.reset();
|
||||||
|
assert!(!adr.is_ready());
|
||||||
|
assert_eq!(adr.update(section(2, 1)), Some(2.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn batch_equals_streaming() {
|
||||||
|
let sections = vec![section(3, 1), section(4, 0), section(0, 5), section(2, 2)];
|
||||||
|
let mut a = AdvanceDeclineRatio::new();
|
||||||
|
let mut b = AdvanceDeclineRatio::new();
|
||||||
|
assert_eq!(
|
||||||
|
a.batch(§ions),
|
||||||
|
sections
|
||||||
|
.iter()
|
||||||
|
.map(|s| b.update(s.clone()))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,165 @@
|
|||||||
|
//! Breadth Thrust (Zweig) — a moving average of the advancing-issues share.
|
||||||
|
|
||||||
|
use crate::cross_section::CrossSection;
|
||||||
|
use crate::error::Result;
|
||||||
|
use crate::traits::Indicator;
|
||||||
|
use crate::Sma;
|
||||||
|
|
||||||
|
/// Breadth Thrust (Zweig) — a simple moving average of the advancing-issues
|
||||||
|
/// share, `advancers / (advancers + decliners)`.
|
||||||
|
///
|
||||||
|
/// Martin Zweig's breadth thrust smooths the fraction of participating issues
|
||||||
|
/// that are advancing over a short window (the classic period is 10). A "thrust"
|
||||||
|
/// fires when this average climbs from below ~0.40 (oversold, washed-out breadth)
|
||||||
|
/// to above ~0.615 within about ten sessions — historically a rare, reliable
|
||||||
|
/// signal that a powerful new advance has begun with broad participation.
|
||||||
|
///
|
||||||
|
/// Each tick's share floors the participating count to one, so a tick with no
|
||||||
|
/// advancing or declining issues contributes a defined `0.0` instead of dividing
|
||||||
|
/// by zero. The reading is `None` until `period` ticks have been seen.
|
||||||
|
///
|
||||||
|
/// `Input = CrossSection`, `Output = f64` (a share in `0..=1`),
|
||||||
|
/// `warmup_period == period`.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use wickra_core::{BreadthThrust, CrossSection, Indicator, Member};
|
||||||
|
///
|
||||||
|
/// let mut bt = BreadthThrust::new(2).unwrap();
|
||||||
|
/// let up = CrossSection::new(vec![Member::new(1.0, 1.0, false, false)], 0).unwrap();
|
||||||
|
/// assert_eq!(bt.update(up.clone()), None); // warming up
|
||||||
|
/// assert_eq!(bt.update(up), Some(1.0)); // both ticks 100% advancing
|
||||||
|
/// ```
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct BreadthThrust {
|
||||||
|
sma: Sma,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BreadthThrust {
|
||||||
|
/// Construct a new Breadth Thrust over the given window length.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns [`Error::PeriodZero`](crate::Error::PeriodZero) if `period == 0`.
|
||||||
|
pub fn new(period: usize) -> Result<Self> {
|
||||||
|
Ok(Self {
|
||||||
|
sma: Sma::new(period)?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Configured window length.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn period(&self) -> usize {
|
||||||
|
self.sma.period()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Indicator for BreadthThrust {
|
||||||
|
type Input = CrossSection;
|
||||||
|
type Output = f64;
|
||||||
|
|
||||||
|
fn update(&mut self, section: CrossSection) -> Option<f64> {
|
||||||
|
let advancers = section.advancers();
|
||||||
|
let decliners = section.decliners();
|
||||||
|
let participating = (advancers + decliners).max(1) as f64;
|
||||||
|
let share = advancers as f64 / participating;
|
||||||
|
self.sma.update(share)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reset(&mut self) {
|
||||||
|
self.sma.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn warmup_period(&self) -> usize {
|
||||||
|
self.sma.period()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_ready(&self) -> bool {
|
||||||
|
self.sma.value().is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
"BreadthThrust"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::cross_section::Member;
|
||||||
|
use crate::error::Error;
|
||||||
|
use crate::traits::BatchExt;
|
||||||
|
|
||||||
|
fn section(up: usize, down: usize) -> CrossSection {
|
||||||
|
let mut members = Vec::new();
|
||||||
|
for _ in 0..up {
|
||||||
|
members.push(Member::new(1.0, 10.0, false, false));
|
||||||
|
}
|
||||||
|
for _ in 0..down {
|
||||||
|
members.push(Member::new(-1.0, 10.0, false, false));
|
||||||
|
}
|
||||||
|
members.push(Member::new(0.0, 10.0, false, false));
|
||||||
|
CrossSection::new(members, 0).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn accessors_and_metadata() {
|
||||||
|
let bt = BreadthThrust::new(10).unwrap();
|
||||||
|
assert_eq!(bt.name(), "BreadthThrust");
|
||||||
|
assert_eq!(bt.warmup_period(), 10);
|
||||||
|
assert_eq!(bt.period(), 10);
|
||||||
|
assert!(!bt.is_ready());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rejects_zero_period() {
|
||||||
|
assert!(matches!(BreadthThrust::new(0), Err(Error::PeriodZero)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn averages_the_advancing_share() {
|
||||||
|
let mut bt = BreadthThrust::new(2).unwrap();
|
||||||
|
// share = 8 / 10 = 0.8 ; window not full yet.
|
||||||
|
assert_eq!(bt.update(section(8, 2)), None);
|
||||||
|
// share = 6 / 10 = 0.6 ; SMA(2) = (0.8 + 0.6) / 2 = 0.7.
|
||||||
|
let value = bt.update(section(6, 4)).unwrap();
|
||||||
|
assert!((value - 0.7).abs() < 1e-9);
|
||||||
|
assert!(bt.is_ready());
|
||||||
|
// share = 5 / 10 = 0.5 ; SMA(2) = (0.6 + 0.5) / 2 = 0.55.
|
||||||
|
let value = bt.update(section(5, 5)).unwrap();
|
||||||
|
assert!((value - 0.55).abs() < 1e-9);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn empty_participation_floors_to_zero_share() {
|
||||||
|
let mut bt = BreadthThrust::new(1).unwrap();
|
||||||
|
// No advancers or decliners -> 0 / max(0, 1) = 0.0.
|
||||||
|
assert_eq!(bt.update(section(0, 0)), Some(0.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn reset_clears_state() {
|
||||||
|
let mut bt = BreadthThrust::new(2).unwrap();
|
||||||
|
bt.update(section(8, 2));
|
||||||
|
bt.update(section(6, 4));
|
||||||
|
assert!(bt.is_ready());
|
||||||
|
bt.reset();
|
||||||
|
assert!(!bt.is_ready());
|
||||||
|
assert_eq!(bt.update(section(8, 2)), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn batch_equals_streaming() {
|
||||||
|
let sections = vec![section(8, 2), section(6, 4), section(5, 5), section(0, 0)];
|
||||||
|
let mut a = BreadthThrust::new(2).unwrap();
|
||||||
|
let mut b = BreadthThrust::new(2).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
a.batch(§ions),
|
||||||
|
sections
|
||||||
|
.iter()
|
||||||
|
.map(|s| b.update(s.clone()))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,147 @@
|
|||||||
|
//! Bullish Percent Index — share of a universe on a point-and-figure buy signal.
|
||||||
|
|
||||||
|
use crate::cross_section::CrossSection;
|
||||||
|
use crate::traits::Indicator;
|
||||||
|
|
||||||
|
/// Bullish Percent Index (BPI) — the percentage of symbols in a universe that are
|
||||||
|
/// currently on a point-and-figure buy signal.
|
||||||
|
///
|
||||||
|
/// On each [`CrossSection`] tick the value is `100 * on_buy_signal_count /
|
||||||
|
/// universe size`, read from the per-symbol `on_buy_signal` flag (the caller
|
||||||
|
/// evaluates each symbol's point-and-figure chart when it builds the tick). It is
|
||||||
|
/// a bounded `0..=100` gauge of how many issues are in a confirmed uptrend.
|
||||||
|
/// Readings above 70 are considered overbought (broad strength, but a crowded
|
||||||
|
/// market) and below 30 oversold; reversals from those zones are classic BPI
|
||||||
|
/// buy/sell triggers.
|
||||||
|
///
|
||||||
|
/// `Input = CrossSection`, `Output = f64` (a percentage in `0..=100`),
|
||||||
|
/// `warmup_period == 1`. The universe is non-empty by construction, so the share
|
||||||
|
/// is always defined.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use wickra_core::{BullishPercentIndex, CrossSection, Indicator, Member};
|
||||||
|
///
|
||||||
|
/// let mut bpi = BullishPercentIndex::new();
|
||||||
|
/// // 2 of 4 symbols on a buy signal -> 50%.
|
||||||
|
/// let tick = CrossSection::new(
|
||||||
|
/// vec![
|
||||||
|
/// Member::with_signals(1.0, 10.0, false, false, false, true),
|
||||||
|
/// Member::with_signals(1.0, 10.0, false, false, false, true),
|
||||||
|
/// Member::with_signals(-1.0, 10.0, false, false, false, false),
|
||||||
|
/// Member::with_signals(-1.0, 10.0, false, false, false, false),
|
||||||
|
/// ],
|
||||||
|
/// 0,
|
||||||
|
/// )
|
||||||
|
/// .unwrap();
|
||||||
|
/// assert_eq!(bpi.update(tick), Some(50.0));
|
||||||
|
/// ```
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
pub struct BullishPercentIndex {
|
||||||
|
has_emitted: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BullishPercentIndex {
|
||||||
|
/// Construct a new Bullish Percent Index indicator.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn new() -> Self {
|
||||||
|
Self { has_emitted: false }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Indicator for BullishPercentIndex {
|
||||||
|
type Input = CrossSection;
|
||||||
|
type Output = f64;
|
||||||
|
|
||||||
|
fn update(&mut self, section: CrossSection) -> Option<f64> {
|
||||||
|
let bullish = section.on_buy_signal_count() as f64;
|
||||||
|
let total = section.members.len() as f64;
|
||||||
|
self.has_emitted = true;
|
||||||
|
Some(100.0 * bullish / total)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reset(&mut self) {
|
||||||
|
self.has_emitted = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn warmup_period(&self) -> usize {
|
||||||
|
1
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_ready(&self) -> bool {
|
||||||
|
self.has_emitted
|
||||||
|
}
|
||||||
|
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
"BullishPercentIndex"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::cross_section::Member;
|
||||||
|
use crate::traits::BatchExt;
|
||||||
|
|
||||||
|
fn tick(bullish: usize, bearish: usize) -> CrossSection {
|
||||||
|
let mut members = Vec::new();
|
||||||
|
for _ in 0..bullish {
|
||||||
|
members.push(Member::with_signals(1.0, 10.0, false, false, false, true));
|
||||||
|
}
|
||||||
|
for _ in 0..bearish {
|
||||||
|
members.push(Member::with_signals(-1.0, 10.0, false, false, false, false));
|
||||||
|
}
|
||||||
|
CrossSection::new(members, 0).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn accessors_and_metadata() {
|
||||||
|
let bpi = BullishPercentIndex::new();
|
||||||
|
assert_eq!(bpi.name(), "BullishPercentIndex");
|
||||||
|
assert_eq!(bpi.warmup_period(), 1);
|
||||||
|
assert!(!bpi.is_ready());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn first_tick_emits_percentage() {
|
||||||
|
let mut bpi = BullishPercentIndex::new();
|
||||||
|
assert_eq!(bpi.update(tick(2, 2)), Some(50.0));
|
||||||
|
assert!(bpi.is_ready());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn all_bullish_is_one_hundred() {
|
||||||
|
let mut bpi = BullishPercentIndex::new();
|
||||||
|
assert_eq!(bpi.update(tick(5, 0)), Some(100.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn none_bullish_is_zero() {
|
||||||
|
let mut bpi = BullishPercentIndex::new();
|
||||||
|
assert_eq!(bpi.update(tick(0, 4)), Some(0.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn reset_clears_state() {
|
||||||
|
let mut bpi = BullishPercentIndex::new();
|
||||||
|
bpi.update(tick(2, 2));
|
||||||
|
assert!(bpi.is_ready());
|
||||||
|
bpi.reset();
|
||||||
|
assert!(!bpi.is_ready());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn batch_equals_streaming() {
|
||||||
|
let sections = vec![tick(2, 2), tick(5, 0), tick(0, 4)];
|
||||||
|
let mut a = BullishPercentIndex::new();
|
||||||
|
let mut b = BullishPercentIndex::new();
|
||||||
|
assert_eq!(
|
||||||
|
a.batch(§ions),
|
||||||
|
sections
|
||||||
|
.iter()
|
||||||
|
.map(|s| b.update(s.clone()))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,163 @@
|
|||||||
|
//! Cumulative Volume Index — running total of volume-normalised net advancing volume.
|
||||||
|
|
||||||
|
use crate::cross_section::CrossSection;
|
||||||
|
use crate::traits::Indicator;
|
||||||
|
|
||||||
|
/// Cumulative Volume Index (CVI) — the running total of *volume-normalised* net
|
||||||
|
/// advancing volume across a universe.
|
||||||
|
///
|
||||||
|
/// On each [`CrossSection`] tick the increment is `(advancing volume - declining
|
||||||
|
/// volume) / total volume`: the share of the tick's total volume that flowed,
|
||||||
|
/// net, into advancing issues. The index accumulates this share over time. Where
|
||||||
|
/// the raw [`AdVolumeLine`](crate::AdVolumeLine) sums *absolute* net volume — and
|
||||||
|
/// so drifts with secular growth in trading activity — the CVI normalises each
|
||||||
|
/// tick by its own total volume, so a one-share-net day in a thin market counts
|
||||||
|
/// the same as in a heavy one. This keeps the index comparable across regimes of
|
||||||
|
/// very different volume.
|
||||||
|
///
|
||||||
|
/// When a tick has zero total volume the net is necessarily zero too, so the
|
||||||
|
/// increment is zero and the index is unchanged (the divisor is floored to the
|
||||||
|
/// smallest positive `f64` purely to keep the division defined).
|
||||||
|
///
|
||||||
|
/// `Input = CrossSection`, `Output = f64`, `warmup_period == 1`.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use wickra_core::{CrossSection, CumulativeVolumeIndex, Indicator, Member};
|
||||||
|
///
|
||||||
|
/// let mut cvi = CumulativeVolumeIndex::new();
|
||||||
|
/// // adv vol 150, dec vol 50, total 200 -> (150 - 50) / 200 = 0.5.
|
||||||
|
/// let tick = CrossSection::new(
|
||||||
|
/// vec![
|
||||||
|
/// Member::new(1.0, 150.0, false, false),
|
||||||
|
/// Member::new(-1.0, 50.0, false, false),
|
||||||
|
/// ],
|
||||||
|
/// 0,
|
||||||
|
/// )
|
||||||
|
/// .unwrap();
|
||||||
|
/// assert_eq!(cvi.update(tick), Some(0.5));
|
||||||
|
/// ```
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
pub struct CumulativeVolumeIndex {
|
||||||
|
index: f64,
|
||||||
|
has_emitted: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CumulativeVolumeIndex {
|
||||||
|
/// Construct a new Cumulative Volume Index indicator.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
index: 0.0,
|
||||||
|
has_emitted: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Indicator for CumulativeVolumeIndex {
|
||||||
|
type Input = CrossSection;
|
||||||
|
type Output = f64;
|
||||||
|
|
||||||
|
fn update(&mut self, section: CrossSection) -> Option<f64> {
|
||||||
|
let net = section.advancing_volume() - section.declining_volume();
|
||||||
|
let total = section.total_volume().max(f64::MIN_POSITIVE);
|
||||||
|
self.index += net / total;
|
||||||
|
self.has_emitted = true;
|
||||||
|
Some(self.index)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reset(&mut self) {
|
||||||
|
self.index = 0.0;
|
||||||
|
self.has_emitted = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn warmup_period(&self) -> usize {
|
||||||
|
1
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_ready(&self) -> bool {
|
||||||
|
self.has_emitted
|
||||||
|
}
|
||||||
|
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
"CumulativeVolumeIndex"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::cross_section::Member;
|
||||||
|
use crate::traits::BatchExt;
|
||||||
|
|
||||||
|
fn tick(items: &[(f64, f64)]) -> CrossSection {
|
||||||
|
CrossSection::new(
|
||||||
|
items
|
||||||
|
.iter()
|
||||||
|
.map(|&(change, volume)| Member::new(change, volume, false, false))
|
||||||
|
.collect(),
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn accessors_and_metadata() {
|
||||||
|
let cvi = CumulativeVolumeIndex::new();
|
||||||
|
assert_eq!(cvi.name(), "CumulativeVolumeIndex");
|
||||||
|
assert_eq!(cvi.warmup_period(), 1);
|
||||||
|
assert!(!cvi.is_ready());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn first_tick_emits_normalised_net() {
|
||||||
|
let mut cvi = CumulativeVolumeIndex::new();
|
||||||
|
assert_eq!(cvi.update(tick(&[(1.0, 150.0), (-1.0, 50.0)])), Some(0.5));
|
||||||
|
assert!(cvi.is_ready());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn index_accumulates_normalised_shares() {
|
||||||
|
let mut cvi = CumulativeVolumeIndex::new();
|
||||||
|
assert_eq!(cvi.update(tick(&[(1.0, 150.0), (-1.0, 50.0)])), Some(0.5));
|
||||||
|
// adv 60, dec 60, total 120 -> net 0 -> index unchanged.
|
||||||
|
assert_eq!(cvi.update(tick(&[(1.0, 60.0), (-1.0, 60.0)])), Some(0.5));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn zero_total_volume_leaves_index_unchanged() {
|
||||||
|
let mut cvi = CumulativeVolumeIndex::new();
|
||||||
|
cvi.update(tick(&[(1.0, 150.0), (-1.0, 50.0)]));
|
||||||
|
// A tick with no volume at all: net 0 / floored divisor -> 0 increment.
|
||||||
|
assert_eq!(cvi.update(tick(&[(0.0, 0.0)])), Some(0.5));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn reset_clears_state() {
|
||||||
|
let mut cvi = CumulativeVolumeIndex::new();
|
||||||
|
cvi.update(tick(&[(1.0, 150.0), (-1.0, 50.0)]));
|
||||||
|
assert!(cvi.is_ready());
|
||||||
|
cvi.reset();
|
||||||
|
assert!(!cvi.is_ready());
|
||||||
|
assert_eq!(cvi.update(tick(&[(1.0, 100.0)])), Some(1.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn batch_equals_streaming() {
|
||||||
|
let sections = vec![
|
||||||
|
tick(&[(1.0, 150.0), (-1.0, 50.0)]),
|
||||||
|
tick(&[(1.0, 60.0), (-1.0, 60.0)]),
|
||||||
|
tick(&[(0.0, 0.0)]),
|
||||||
|
];
|
||||||
|
let mut a = CumulativeVolumeIndex::new();
|
||||||
|
let mut b = CumulativeVolumeIndex::new();
|
||||||
|
assert_eq!(
|
||||||
|
a.batch(§ions),
|
||||||
|
sections
|
||||||
|
.iter()
|
||||||
|
.map(|s| b.update(s.clone()))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,162 @@
|
|||||||
|
//! High-Low Index — a moving average of the record-high percentage.
|
||||||
|
|
||||||
|
use crate::cross_section::CrossSection;
|
||||||
|
use crate::error::Result;
|
||||||
|
use crate::traits::Indicator;
|
||||||
|
use crate::Sma;
|
||||||
|
|
||||||
|
/// High-Low Index — a simple moving average of the *record high percent*,
|
||||||
|
/// `100 * new_highs / (new_highs + new_lows)`.
|
||||||
|
///
|
||||||
|
/// The record high percent is the share of new-extreme issues that are new
|
||||||
|
/// *highs* rather than new *lows*; smoothing it over a window (the classic period
|
||||||
|
/// is 10) gives the High-Low Index. Readings above 50 mean new highs dominate
|
||||||
|
/// (a healthy, broadening trend), readings below 50 mean new lows dominate. The
|
||||||
|
/// 30 and 70 lines are watched as oversold / overbought breadth thresholds.
|
||||||
|
///
|
||||||
|
/// Each tick floors the new-extreme count to one, so a tick with no new highs or
|
||||||
|
/// lows contributes a defined `0.0` instead of dividing by zero. The reading is
|
||||||
|
/// `None` until `period` ticks have been seen.
|
||||||
|
///
|
||||||
|
/// `Input = CrossSection`, `Output = f64` (a percentage in `0..=100`),
|
||||||
|
/// `warmup_period == period`.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use wickra_core::{CrossSection, HighLowIndex, Indicator, Member};
|
||||||
|
///
|
||||||
|
/// let mut hli = HighLowIndex::new(2).unwrap();
|
||||||
|
/// let highs = CrossSection::new(vec![Member::new(1.0, 1.0, true, false)], 0).unwrap();
|
||||||
|
/// assert_eq!(hli.update(highs.clone()), None); // warming up
|
||||||
|
/// assert_eq!(hli.update(highs), Some(100.0)); // all new highs
|
||||||
|
/// ```
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct HighLowIndex {
|
||||||
|
sma: Sma,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HighLowIndex {
|
||||||
|
/// Construct a new High-Low Index over the given window length.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns [`Error::PeriodZero`](crate::Error::PeriodZero) if `period == 0`.
|
||||||
|
pub fn new(period: usize) -> Result<Self> {
|
||||||
|
Ok(Self {
|
||||||
|
sma: Sma::new(period)?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Configured window length.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn period(&self) -> usize {
|
||||||
|
self.sma.period()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Indicator for HighLowIndex {
|
||||||
|
type Input = CrossSection;
|
||||||
|
type Output = f64;
|
||||||
|
|
||||||
|
fn update(&mut self, section: CrossSection) -> Option<f64> {
|
||||||
|
let new_highs = section.new_highs();
|
||||||
|
let new_lows = section.new_lows();
|
||||||
|
let extremes = (new_highs + new_lows).max(1) as f64;
|
||||||
|
let record_high_percent = 100.0 * new_highs as f64 / extremes;
|
||||||
|
self.sma.update(record_high_percent)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reset(&mut self) {
|
||||||
|
self.sma.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn warmup_period(&self) -> usize {
|
||||||
|
self.sma.period()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_ready(&self) -> bool {
|
||||||
|
self.sma.value().is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
"HighLowIndex"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::cross_section::Member;
|
||||||
|
use crate::error::Error;
|
||||||
|
use crate::traits::BatchExt;
|
||||||
|
|
||||||
|
fn flags(highs: usize, lows: usize) -> CrossSection {
|
||||||
|
let mut members = Vec::new();
|
||||||
|
for _ in 0..highs {
|
||||||
|
members.push(Member::new(1.0, 10.0, true, false));
|
||||||
|
}
|
||||||
|
for _ in 0..lows {
|
||||||
|
members.push(Member::new(-1.0, 10.0, false, true));
|
||||||
|
}
|
||||||
|
members.push(Member::new(0.0, 10.0, false, false));
|
||||||
|
CrossSection::new(members, 0).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn accessors_and_metadata() {
|
||||||
|
let hli = HighLowIndex::new(10).unwrap();
|
||||||
|
assert_eq!(hli.name(), "HighLowIndex");
|
||||||
|
assert_eq!(hli.warmup_period(), 10);
|
||||||
|
assert_eq!(hli.period(), 10);
|
||||||
|
assert!(!hli.is_ready());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rejects_zero_period() {
|
||||||
|
assert!(matches!(HighLowIndex::new(0), Err(Error::PeriodZero)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn averages_the_record_high_percent() {
|
||||||
|
let mut hli = HighLowIndex::new(2).unwrap();
|
||||||
|
// 8 highs / 10 extremes -> 80% ; window not full.
|
||||||
|
assert_eq!(hli.update(flags(8, 2)), None);
|
||||||
|
// 6 highs / 10 extremes -> 60% ; SMA(2) = (80 + 60) / 2 = 70.
|
||||||
|
let value = hli.update(flags(6, 4)).unwrap();
|
||||||
|
assert!((value - 70.0).abs() < 1e-9);
|
||||||
|
assert!(hli.is_ready());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn no_extremes_floors_to_zero_percent() {
|
||||||
|
let mut hli = HighLowIndex::new(1).unwrap();
|
||||||
|
// No new highs or lows -> 0 / max(0, 1) -> 0%.
|
||||||
|
assert_eq!(hli.update(flags(0, 0)), Some(0.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn reset_clears_state() {
|
||||||
|
let mut hli = HighLowIndex::new(2).unwrap();
|
||||||
|
hli.update(flags(8, 2));
|
||||||
|
hli.update(flags(6, 4));
|
||||||
|
assert!(hli.is_ready());
|
||||||
|
hli.reset();
|
||||||
|
assert!(!hli.is_ready());
|
||||||
|
assert_eq!(hli.update(flags(8, 2)), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn batch_equals_streaming() {
|
||||||
|
let sections = vec![flags(8, 2), flags(6, 4), flags(3, 7), flags(0, 0)];
|
||||||
|
let mut a = HighLowIndex::new(2).unwrap();
|
||||||
|
let mut b = HighLowIndex::new(2).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
a.batch(§ions),
|
||||||
|
sections
|
||||||
|
.iter()
|
||||||
|
.map(|s| b.update(s.clone()))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,204 @@
|
|||||||
|
//! McClellan Oscillator — the spread between a fast and slow EMA of breadth.
|
||||||
|
|
||||||
|
use crate::cross_section::CrossSection;
|
||||||
|
use crate::traits::Indicator;
|
||||||
|
|
||||||
|
/// Fast EMA smoothing constant — the classic McClellan 19-period weight
|
||||||
|
/// `2 / (19 + 1)`.
|
||||||
|
const ALPHA_FAST: f64 = 0.1;
|
||||||
|
/// Slow EMA smoothing constant — the classic McClellan 39-period weight
|
||||||
|
/// `2 / (39 + 1)`.
|
||||||
|
const ALPHA_SLOW: f64 = 0.05;
|
||||||
|
/// Scale applied to the ratio-adjusted net advances so readings land on the
|
||||||
|
/// familiar McClellan amplitude.
|
||||||
|
const RANA_SCALE: f64 = 1000.0;
|
||||||
|
|
||||||
|
/// McClellan Oscillator — the difference between a 19-period and a 39-period
|
||||||
|
/// exponential moving average of *ratio-adjusted net advances*.
|
||||||
|
///
|
||||||
|
/// Each tick's breadth is reduced to ratio-adjusted net advances (RANA),
|
||||||
|
/// `(advancers - decliners) / (advancers + decliners) * 1000`. Dividing by the
|
||||||
|
/// number of participating issues makes the reading independent of universe size,
|
||||||
|
/// so the oscillator stays comparable as the universe grows or shrinks. The
|
||||||
|
/// oscillator is then the fast EMA minus the slow EMA of that series, using the
|
||||||
|
/// classic McClellan smoothing constants `0.10` (19-period) and `0.05`
|
||||||
|
/// (39-period). Both EMAs are seeded from the first tick's RANA, so the
|
||||||
|
/// oscillator is defined from the first update (`warmup_period == 1`); it starts
|
||||||
|
/// at `0.0` and crosses zero as breadth momentum shifts.
|
||||||
|
///
|
||||||
|
/// A tick with no advancing or declining issues yields a RANA of `0.0` (the
|
||||||
|
/// participating count is floored to one).
|
||||||
|
///
|
||||||
|
/// `Input = CrossSection`, `Output = f64`.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use wickra_core::{CrossSection, Indicator, McClellanOscillator, Member};
|
||||||
|
///
|
||||||
|
/// let mut osc = McClellanOscillator::new();
|
||||||
|
/// let tick = CrossSection::new(
|
||||||
|
/// vec![
|
||||||
|
/// Member::new(1.0, 10.0, false, false),
|
||||||
|
/// Member::new(1.0, 10.0, false, false),
|
||||||
|
/// Member::new(1.0, 10.0, false, false),
|
||||||
|
/// Member::new(-1.0, 10.0, false, false),
|
||||||
|
/// ],
|
||||||
|
/// 0,
|
||||||
|
/// )
|
||||||
|
/// .unwrap();
|
||||||
|
/// // First tick seeds both EMAs to the same value -> oscillator 0.
|
||||||
|
/// assert_eq!(osc.update(tick), Some(0.0));
|
||||||
|
/// ```
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
pub struct McClellanOscillator {
|
||||||
|
ema_fast: f64,
|
||||||
|
ema_slow: f64,
|
||||||
|
seeded: bool,
|
||||||
|
has_emitted: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl McClellanOscillator {
|
||||||
|
/// Construct a new McClellan Oscillator with the classic 19/39 smoothing.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
ema_fast: 0.0,
|
||||||
|
ema_slow: 0.0,
|
||||||
|
seeded: false,
|
||||||
|
has_emitted: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Feed a cross-section tick and return the oscillator value, which is defined
|
||||||
|
/// on every tick. Shared with [`McClellanSummationIndex`] so the summation
|
||||||
|
/// index can accumulate the oscillator without an `Option` round-trip.
|
||||||
|
///
|
||||||
|
/// [`McClellanSummationIndex`]: crate::McClellanSummationIndex
|
||||||
|
pub(crate) fn step(&mut self, section: &CrossSection) -> f64 {
|
||||||
|
let advancers = section.advancers();
|
||||||
|
let decliners = section.decliners();
|
||||||
|
let net = advancers as f64 - decliners as f64;
|
||||||
|
let participating = (advancers + decliners).max(1) as f64;
|
||||||
|
let rana = net / participating * RANA_SCALE;
|
||||||
|
if self.seeded {
|
||||||
|
self.ema_fast += ALPHA_FAST * (rana - self.ema_fast);
|
||||||
|
self.ema_slow += ALPHA_SLOW * (rana - self.ema_slow);
|
||||||
|
} else {
|
||||||
|
self.ema_fast = rana;
|
||||||
|
self.ema_slow = rana;
|
||||||
|
self.seeded = true;
|
||||||
|
}
|
||||||
|
self.has_emitted = true;
|
||||||
|
self.ema_fast - self.ema_slow
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Indicator for McClellanOscillator {
|
||||||
|
type Input = CrossSection;
|
||||||
|
type Output = f64;
|
||||||
|
|
||||||
|
fn update(&mut self, section: CrossSection) -> Option<f64> {
|
||||||
|
Some(self.step(§ion))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reset(&mut self) {
|
||||||
|
self.ema_fast = 0.0;
|
||||||
|
self.ema_slow = 0.0;
|
||||||
|
self.seeded = false;
|
||||||
|
self.has_emitted = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn warmup_period(&self) -> usize {
|
||||||
|
1
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_ready(&self) -> bool {
|
||||||
|
self.has_emitted
|
||||||
|
}
|
||||||
|
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
"McClellanOscillator"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::cross_section::Member;
|
||||||
|
use crate::traits::BatchExt;
|
||||||
|
|
||||||
|
fn section(up: usize, down: usize) -> CrossSection {
|
||||||
|
let mut members = Vec::new();
|
||||||
|
for _ in 0..up {
|
||||||
|
members.push(Member::new(1.0, 10.0, false, false));
|
||||||
|
}
|
||||||
|
for _ in 0..down {
|
||||||
|
members.push(Member::new(-1.0, 10.0, false, false));
|
||||||
|
}
|
||||||
|
members.push(Member::new(0.0, 10.0, false, false));
|
||||||
|
CrossSection::new(members, 0).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn accessors_and_metadata() {
|
||||||
|
let osc = McClellanOscillator::new();
|
||||||
|
assert_eq!(osc.name(), "McClellanOscillator");
|
||||||
|
assert_eq!(osc.warmup_period(), 1);
|
||||||
|
assert!(!osc.is_ready());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn seeds_to_zero_on_first_tick() {
|
||||||
|
let mut osc = McClellanOscillator::new();
|
||||||
|
// RANA = (3 - 1) / 4 * 1000 = 500 ; both EMAs seed to 500 -> spread 0.
|
||||||
|
assert_eq!(osc.update(section(3, 1)), Some(0.0));
|
||||||
|
assert!(osc.is_ready());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tracks_breadth_momentum_after_seeding() {
|
||||||
|
let mut osc = McClellanOscillator::new();
|
||||||
|
osc.update(section(3, 1)); // seed at RANA 500
|
||||||
|
// RANA = (1 - 3) / 4 * 1000 = -500.
|
||||||
|
// fast = 500 + 0.1 * (-1000) = 400 ; slow = 500 + 0.05 * (-1000) = 450.
|
||||||
|
let value = osc.update(section(1, 3)).unwrap();
|
||||||
|
assert!((value - (-50.0)).abs() < 1e-9);
|
||||||
|
// RANA = 0. fast = 400 + 0.1 * (-400) = 360 ; slow = 450 + 0.05 * (-450) = 427.5.
|
||||||
|
let value = osc.update(section(2, 2)).unwrap();
|
||||||
|
assert!((value - (-67.5)).abs() < 1e-9);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn empty_participation_yields_zero_rana() {
|
||||||
|
let mut osc = McClellanOscillator::new();
|
||||||
|
// No advancers or decliners -> RANA 0 ; seeds both EMAs to 0 -> spread 0.
|
||||||
|
assert_eq!(osc.update(section(0, 0)), Some(0.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn reset_clears_state() {
|
||||||
|
let mut osc = McClellanOscillator::new();
|
||||||
|
osc.update(section(3, 1));
|
||||||
|
osc.update(section(1, 3));
|
||||||
|
assert!(osc.is_ready());
|
||||||
|
osc.reset();
|
||||||
|
assert!(!osc.is_ready());
|
||||||
|
// After reset the next tick re-seeds to spread 0.
|
||||||
|
assert_eq!(osc.update(section(1, 3)), Some(0.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn batch_equals_streaming() {
|
||||||
|
let sections = vec![section(3, 1), section(1, 3), section(2, 2), section(0, 0)];
|
||||||
|
let mut a = McClellanOscillator::new();
|
||||||
|
let mut b = McClellanOscillator::new();
|
||||||
|
assert_eq!(
|
||||||
|
a.batch(§ions),
|
||||||
|
sections
|
||||||
|
.iter()
|
||||||
|
.map(|s| b.update(s.clone()))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,160 @@
|
|||||||
|
//! McClellan Summation Index — the running total of the McClellan Oscillator.
|
||||||
|
|
||||||
|
use crate::cross_section::CrossSection;
|
||||||
|
use crate::indicators::mcclellan_oscillator::McClellanOscillator;
|
||||||
|
use crate::traits::Indicator;
|
||||||
|
|
||||||
|
/// McClellan Summation Index — the running cumulative sum of the
|
||||||
|
/// [`McClellanOscillator`].
|
||||||
|
///
|
||||||
|
/// Where the oscillator measures the *momentum* of breadth, the summation index
|
||||||
|
/// integrates it into a longer-term breadth trend: it rises while the oscillator
|
||||||
|
/// is positive and falls while it is negative, so it behaves like a slow,
|
||||||
|
/// smoothed advance/decline line. Sustained readings far above or below zero mark
|
||||||
|
/// strong bull or bear breadth regimes, and crosses of the zero line are read as
|
||||||
|
/// major trend changes.
|
||||||
|
///
|
||||||
|
/// The index embeds a [`McClellanOscillator`] and adds its value on every tick.
|
||||||
|
/// Because the oscillator seeds to `0.0` on the first tick, the summation index
|
||||||
|
/// also starts at `0.0` and is defined from the first update
|
||||||
|
/// (`warmup_period == 1`).
|
||||||
|
///
|
||||||
|
/// `Input = CrossSection`, `Output = f64`.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use wickra_core::{CrossSection, Indicator, McClellanSummationIndex, Member};
|
||||||
|
///
|
||||||
|
/// let mut msi = McClellanSummationIndex::new();
|
||||||
|
/// let tick = CrossSection::new(
|
||||||
|
/// vec![
|
||||||
|
/// Member::new(1.0, 10.0, false, false),
|
||||||
|
/// Member::new(-1.0, 10.0, false, false),
|
||||||
|
/// ],
|
||||||
|
/// 0,
|
||||||
|
/// )
|
||||||
|
/// .unwrap();
|
||||||
|
/// // First tick: oscillator seeds to 0, so the summation index is 0.
|
||||||
|
/// assert_eq!(msi.update(tick), Some(0.0));
|
||||||
|
/// ```
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
pub struct McClellanSummationIndex {
|
||||||
|
oscillator: McClellanOscillator,
|
||||||
|
sum: f64,
|
||||||
|
has_emitted: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl McClellanSummationIndex {
|
||||||
|
/// Construct a new McClellan Summation Index.
|
||||||
|
#[must_use]
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
oscillator: McClellanOscillator::new(),
|
||||||
|
sum: 0.0,
|
||||||
|
has_emitted: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Indicator for McClellanSummationIndex {
|
||||||
|
type Input = CrossSection;
|
||||||
|
type Output = f64;
|
||||||
|
|
||||||
|
fn update(&mut self, section: CrossSection) -> Option<f64> {
|
||||||
|
let oscillator = self.oscillator.step(§ion);
|
||||||
|
self.sum += oscillator;
|
||||||
|
self.has_emitted = true;
|
||||||
|
Some(self.sum)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reset(&mut self) {
|
||||||
|
self.oscillator.reset();
|
||||||
|
self.sum = 0.0;
|
||||||
|
self.has_emitted = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn warmup_period(&self) -> usize {
|
||||||
|
1
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_ready(&self) -> bool {
|
||||||
|
self.has_emitted
|
||||||
|
}
|
||||||
|
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
"McClellanSummationIndex"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::cross_section::Member;
|
||||||
|
use crate::traits::BatchExt;
|
||||||
|
|
||||||
|
fn section(up: usize, down: usize) -> CrossSection {
|
||||||
|
let mut members = Vec::new();
|
||||||
|
for _ in 0..up {
|
||||||
|
members.push(Member::new(1.0, 10.0, false, false));
|
||||||
|
}
|
||||||
|
for _ in 0..down {
|
||||||
|
members.push(Member::new(-1.0, 10.0, false, false));
|
||||||
|
}
|
||||||
|
members.push(Member::new(0.0, 10.0, false, false));
|
||||||
|
CrossSection::new(members, 0).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn accessors_and_metadata() {
|
||||||
|
let msi = McClellanSummationIndex::new();
|
||||||
|
assert_eq!(msi.name(), "McClellanSummationIndex");
|
||||||
|
assert_eq!(msi.warmup_period(), 1);
|
||||||
|
assert!(!msi.is_ready());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn first_tick_starts_at_zero() {
|
||||||
|
let mut msi = McClellanSummationIndex::new();
|
||||||
|
assert_eq!(msi.update(section(3, 1)), Some(0.0));
|
||||||
|
assert!(msi.is_ready());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn accumulates_the_oscillator() {
|
||||||
|
let mut msi = McClellanSummationIndex::new();
|
||||||
|
assert_eq!(msi.update(section(3, 1)), Some(0.0)); // osc 0 -> sum 0
|
||||||
|
// osc -50 -> sum -50.
|
||||||
|
let value = msi.update(section(1, 3)).unwrap();
|
||||||
|
assert!((value - (-50.0)).abs() < 1e-9);
|
||||||
|
// osc -67.5 -> sum -117.5.
|
||||||
|
let value = msi.update(section(2, 2)).unwrap();
|
||||||
|
assert!((value - (-117.5)).abs() < 1e-9);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn reset_clears_state() {
|
||||||
|
let mut msi = McClellanSummationIndex::new();
|
||||||
|
msi.update(section(3, 1));
|
||||||
|
msi.update(section(1, 3));
|
||||||
|
assert!(msi.is_ready());
|
||||||
|
msi.reset();
|
||||||
|
assert!(!msi.is_ready());
|
||||||
|
// Oscillator re-seeds, so the summation index restarts at 0.
|
||||||
|
assert_eq!(msi.update(section(1, 3)), Some(0.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn batch_equals_streaming() {
|
||||||
|
let sections = vec![section(3, 1), section(1, 3), section(2, 2)];
|
||||||
|
let mut a = McClellanSummationIndex::new();
|
||||||
|
let mut b = McClellanSummationIndex::new();
|
||||||
|
assert_eq!(
|
||||||
|
a.batch(§ions),
|
||||||
|
sections
|
||||||
|
.iter()
|
||||||
|
.map(|s| b.update(s.clone()))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,13 +5,16 @@
|
|||||||
//! from the crate root for convenience.
|
//! from the crate root for convenience.
|
||||||
|
|
||||||
mod abandoned_baby;
|
mod abandoned_baby;
|
||||||
|
mod absolute_breadth_index;
|
||||||
mod acceleration_bands;
|
mod acceleration_bands;
|
||||||
mod accelerator_oscillator;
|
mod accelerator_oscillator;
|
||||||
mod ad_oscillator;
|
mod ad_oscillator;
|
||||||
|
mod ad_volume_line;
|
||||||
mod adaptive_cycle;
|
mod adaptive_cycle;
|
||||||
mod adl;
|
mod adl;
|
||||||
mod advance_block;
|
mod advance_block;
|
||||||
mod advance_decline;
|
mod advance_decline;
|
||||||
|
mod advance_decline_ratio;
|
||||||
mod adx;
|
mod adx;
|
||||||
mod adxr;
|
mod adxr;
|
||||||
mod alligator;
|
mod alligator;
|
||||||
@@ -36,7 +39,9 @@ mod beta;
|
|||||||
mod beta_neutral_spread;
|
mod beta_neutral_spread;
|
||||||
mod bollinger;
|
mod bollinger;
|
||||||
mod bollinger_bandwidth;
|
mod bollinger_bandwidth;
|
||||||
|
mod breadth_thrust;
|
||||||
mod breakaway;
|
mod breakaway;
|
||||||
|
mod bullish_percent_index;
|
||||||
mod calendar_spread;
|
mod calendar_spread;
|
||||||
mod calmar_ratio;
|
mod calmar_ratio;
|
||||||
mod camarilla_pivots;
|
mod camarilla_pivots;
|
||||||
@@ -59,6 +64,7 @@ mod conditional_value_at_risk;
|
|||||||
mod connors_rsi;
|
mod connors_rsi;
|
||||||
mod coppock;
|
mod coppock;
|
||||||
mod counterattack;
|
mod counterattack;
|
||||||
|
mod cumulative_volume_index;
|
||||||
mod cvd;
|
mod cvd;
|
||||||
mod cybernetic_cycle;
|
mod cybernetic_cycle;
|
||||||
mod decycler;
|
mod decycler;
|
||||||
@@ -109,6 +115,7 @@ mod hammer;
|
|||||||
mod hanging_man;
|
mod hanging_man;
|
||||||
mod harami;
|
mod harami;
|
||||||
mod heikin_ashi;
|
mod heikin_ashi;
|
||||||
|
mod high_low_index;
|
||||||
mod high_wave;
|
mod high_wave;
|
||||||
mod hikkake;
|
mod hikkake;
|
||||||
mod hikkake_modified;
|
mod hikkake_modified;
|
||||||
@@ -166,6 +173,8 @@ mod mass_index;
|
|||||||
mod mat_hold;
|
mod mat_hold;
|
||||||
mod matching_low;
|
mod matching_low;
|
||||||
mod max_drawdown;
|
mod max_drawdown;
|
||||||
|
mod mcclellan_oscillator;
|
||||||
|
mod mcclellan_summation_index;
|
||||||
mod mcginley_dynamic;
|
mod mcginley_dynamic;
|
||||||
mod median_absolute_deviation;
|
mod median_absolute_deviation;
|
||||||
mod median_price;
|
mod median_price;
|
||||||
@@ -179,6 +188,7 @@ mod mom;
|
|||||||
mod morning_doji_star;
|
mod morning_doji_star;
|
||||||
mod morning_evening_star;
|
mod morning_evening_star;
|
||||||
mod natr;
|
mod natr;
|
||||||
|
mod new_highs_new_lows;
|
||||||
mod nvi;
|
mod nvi;
|
||||||
mod ob_imbalance_full;
|
mod ob_imbalance_full;
|
||||||
mod ob_imbalance_top1;
|
mod ob_imbalance_top1;
|
||||||
@@ -197,6 +207,7 @@ mod pair_spread_zscore;
|
|||||||
mod pairwise_beta;
|
mod pairwise_beta;
|
||||||
mod parkinson;
|
mod parkinson;
|
||||||
mod pearson_correlation;
|
mod pearson_correlation;
|
||||||
|
mod percent_above_ma;
|
||||||
mod percent_b;
|
mod percent_b;
|
||||||
mod percentage_trailing_stop;
|
mod percentage_trailing_stop;
|
||||||
mod pgo;
|
mod pgo;
|
||||||
@@ -282,11 +293,13 @@ mod three_outside;
|
|||||||
mod three_soldiers_or_crows;
|
mod three_soldiers_or_crows;
|
||||||
mod three_stars_in_south;
|
mod three_stars_in_south;
|
||||||
mod thrusting;
|
mod thrusting;
|
||||||
|
mod tick_index;
|
||||||
mod tii;
|
mod tii;
|
||||||
mod tpo_profile;
|
mod tpo_profile;
|
||||||
mod trade_imbalance;
|
mod trade_imbalance;
|
||||||
mod treynor_ratio;
|
mod treynor_ratio;
|
||||||
mod trima;
|
mod trima;
|
||||||
|
mod trin;
|
||||||
mod trix;
|
mod trix;
|
||||||
mod true_range;
|
mod true_range;
|
||||||
mod tsf;
|
mod tsf;
|
||||||
@@ -299,6 +312,7 @@ mod typical_price;
|
|||||||
mod ulcer_index;
|
mod ulcer_index;
|
||||||
mod ultimate_oscillator;
|
mod ultimate_oscillator;
|
||||||
mod unique_three_river;
|
mod unique_three_river;
|
||||||
|
mod up_down_volume_ratio;
|
||||||
mod upside_gap_three_methods;
|
mod upside_gap_three_methods;
|
||||||
mod upside_gap_two_crows;
|
mod upside_gap_two_crows;
|
||||||
mod value_area;
|
mod value_area;
|
||||||
@@ -330,13 +344,16 @@ mod zig_zag;
|
|||||||
mod zlema;
|
mod zlema;
|
||||||
|
|
||||||
pub use abandoned_baby::AbandonedBaby;
|
pub use abandoned_baby::AbandonedBaby;
|
||||||
|
pub use absolute_breadth_index::AbsoluteBreadthIndex;
|
||||||
pub use acceleration_bands::{AccelerationBands, AccelerationBandsOutput};
|
pub use acceleration_bands::{AccelerationBands, AccelerationBandsOutput};
|
||||||
pub use accelerator_oscillator::AcceleratorOscillator;
|
pub use accelerator_oscillator::AcceleratorOscillator;
|
||||||
pub use ad_oscillator::AdOscillator;
|
pub use ad_oscillator::AdOscillator;
|
||||||
|
pub use ad_volume_line::AdVolumeLine;
|
||||||
pub use adaptive_cycle::AdaptiveCycle;
|
pub use adaptive_cycle::AdaptiveCycle;
|
||||||
pub use adl::Adl;
|
pub use adl::Adl;
|
||||||
pub use advance_block::AdvanceBlock;
|
pub use advance_block::AdvanceBlock;
|
||||||
pub use advance_decline::AdvanceDecline;
|
pub use advance_decline::AdvanceDecline;
|
||||||
|
pub use advance_decline_ratio::AdvanceDeclineRatio;
|
||||||
pub use adx::{Adx, AdxOutput};
|
pub use adx::{Adx, AdxOutput};
|
||||||
pub use adxr::Adxr;
|
pub use adxr::Adxr;
|
||||||
pub use alligator::{Alligator, AlligatorOutput};
|
pub use alligator::{Alligator, AlligatorOutput};
|
||||||
@@ -361,7 +378,9 @@ pub use beta::Beta;
|
|||||||
pub use beta_neutral_spread::BetaNeutralSpread;
|
pub use beta_neutral_spread::BetaNeutralSpread;
|
||||||
pub use bollinger::{BollingerBands, BollingerOutput};
|
pub use bollinger::{BollingerBands, BollingerOutput};
|
||||||
pub use bollinger_bandwidth::BollingerBandwidth;
|
pub use bollinger_bandwidth::BollingerBandwidth;
|
||||||
|
pub use breadth_thrust::BreadthThrust;
|
||||||
pub use breakaway::Breakaway;
|
pub use breakaway::Breakaway;
|
||||||
|
pub use bullish_percent_index::BullishPercentIndex;
|
||||||
pub use calendar_spread::CalendarSpread;
|
pub use calendar_spread::CalendarSpread;
|
||||||
pub use calmar_ratio::CalmarRatio;
|
pub use calmar_ratio::CalmarRatio;
|
||||||
pub use camarilla_pivots::{Camarilla, CamarillaPivotsOutput};
|
pub use camarilla_pivots::{Camarilla, CamarillaPivotsOutput};
|
||||||
@@ -384,6 +403,7 @@ pub use conditional_value_at_risk::ConditionalValueAtRisk;
|
|||||||
pub use connors_rsi::ConnorsRsi;
|
pub use connors_rsi::ConnorsRsi;
|
||||||
pub use coppock::Coppock;
|
pub use coppock::Coppock;
|
||||||
pub use counterattack::Counterattack;
|
pub use counterattack::Counterattack;
|
||||||
|
pub use cumulative_volume_index::CumulativeVolumeIndex;
|
||||||
pub use cvd::CumulativeVolumeDelta;
|
pub use cvd::CumulativeVolumeDelta;
|
||||||
pub use cybernetic_cycle::CyberneticCycle;
|
pub use cybernetic_cycle::CyberneticCycle;
|
||||||
pub use decycler::Decycler;
|
pub use decycler::Decycler;
|
||||||
@@ -434,6 +454,7 @@ pub use hammer::Hammer;
|
|||||||
pub use hanging_man::HangingMan;
|
pub use hanging_man::HangingMan;
|
||||||
pub use harami::Harami;
|
pub use harami::Harami;
|
||||||
pub use heikin_ashi::{HeikinAshi, HeikinAshiOutput};
|
pub use heikin_ashi::{HeikinAshi, HeikinAshiOutput};
|
||||||
|
pub use high_low_index::HighLowIndex;
|
||||||
pub use high_wave::HighWave;
|
pub use high_wave::HighWave;
|
||||||
pub use hikkake::Hikkake;
|
pub use hikkake::Hikkake;
|
||||||
pub use hikkake_modified::HikkakeModified;
|
pub use hikkake_modified::HikkakeModified;
|
||||||
@@ -491,6 +512,8 @@ pub use mass_index::MassIndex;
|
|||||||
pub use mat_hold::MatHold;
|
pub use mat_hold::MatHold;
|
||||||
pub use matching_low::MatchingLow;
|
pub use matching_low::MatchingLow;
|
||||||
pub use max_drawdown::MaxDrawdown;
|
pub use max_drawdown::MaxDrawdown;
|
||||||
|
pub use mcclellan_oscillator::McClellanOscillator;
|
||||||
|
pub use mcclellan_summation_index::McClellanSummationIndex;
|
||||||
pub use mcginley_dynamic::McGinleyDynamic;
|
pub use mcginley_dynamic::McGinleyDynamic;
|
||||||
pub use median_absolute_deviation::MedianAbsoluteDeviation;
|
pub use median_absolute_deviation::MedianAbsoluteDeviation;
|
||||||
pub use median_price::MedianPrice;
|
pub use median_price::MedianPrice;
|
||||||
@@ -504,6 +527,7 @@ pub use mom::Mom;
|
|||||||
pub use morning_doji_star::MorningDojiStar;
|
pub use morning_doji_star::MorningDojiStar;
|
||||||
pub use morning_evening_star::MorningEveningStar;
|
pub use morning_evening_star::MorningEveningStar;
|
||||||
pub use natr::Natr;
|
pub use natr::Natr;
|
||||||
|
pub use new_highs_new_lows::NewHighsNewLows;
|
||||||
pub use nvi::Nvi;
|
pub use nvi::Nvi;
|
||||||
pub use ob_imbalance_full::OrderBookImbalanceFull;
|
pub use ob_imbalance_full::OrderBookImbalanceFull;
|
||||||
pub use ob_imbalance_top1::OrderBookImbalanceTop1;
|
pub use ob_imbalance_top1::OrderBookImbalanceTop1;
|
||||||
@@ -522,6 +546,7 @@ pub use pair_spread_zscore::PairSpreadZScore;
|
|||||||
pub use pairwise_beta::PairwiseBeta;
|
pub use pairwise_beta::PairwiseBeta;
|
||||||
pub use parkinson::ParkinsonVolatility;
|
pub use parkinson::ParkinsonVolatility;
|
||||||
pub use pearson_correlation::PearsonCorrelation;
|
pub use pearson_correlation::PearsonCorrelation;
|
||||||
|
pub use percent_above_ma::PercentAboveMa;
|
||||||
pub use percent_b::PercentB;
|
pub use percent_b::PercentB;
|
||||||
pub use percentage_trailing_stop::PercentageTrailingStop;
|
pub use percentage_trailing_stop::PercentageTrailingStop;
|
||||||
pub use pgo::Pgo;
|
pub use pgo::Pgo;
|
||||||
@@ -607,11 +632,13 @@ pub use three_outside::ThreeOutside;
|
|||||||
pub use three_soldiers_or_crows::ThreeSoldiersOrCrows;
|
pub use three_soldiers_or_crows::ThreeSoldiersOrCrows;
|
||||||
pub use three_stars_in_south::ThreeStarsInSouth;
|
pub use three_stars_in_south::ThreeStarsInSouth;
|
||||||
pub use thrusting::Thrusting;
|
pub use thrusting::Thrusting;
|
||||||
|
pub use tick_index::TickIndex;
|
||||||
pub use tii::Tii;
|
pub use tii::Tii;
|
||||||
pub use tpo_profile::{TpoProfile, TpoProfileOutput};
|
pub use tpo_profile::{TpoProfile, TpoProfileOutput};
|
||||||
pub use trade_imbalance::TradeImbalance;
|
pub use trade_imbalance::TradeImbalance;
|
||||||
pub use treynor_ratio::TreynorRatio;
|
pub use treynor_ratio::TreynorRatio;
|
||||||
pub use trima::Trima;
|
pub use trima::Trima;
|
||||||
|
pub use trin::Trin;
|
||||||
pub use trix::Trix;
|
pub use trix::Trix;
|
||||||
pub use true_range::TrueRange;
|
pub use true_range::TrueRange;
|
||||||
pub use tsf::Tsf;
|
pub use tsf::Tsf;
|
||||||
@@ -624,6 +651,7 @@ pub use typical_price::TypicalPrice;
|
|||||||
pub use ulcer_index::UlcerIndex;
|
pub use ulcer_index::UlcerIndex;
|
||||||
pub use ultimate_oscillator::UltimateOscillator;
|
pub use ultimate_oscillator::UltimateOscillator;
|
||||||
pub use unique_three_river::UniqueThreeRiver;
|
pub use unique_three_river::UniqueThreeRiver;
|
||||||
|
pub use up_down_volume_ratio::UpDownVolumeRatio;
|
||||||
pub use upside_gap_three_methods::UpsideGapThreeMethods;
|
pub use upside_gap_three_methods::UpsideGapThreeMethods;
|
||||||
pub use upside_gap_two_crows::UpsideGapTwoCrows;
|
pub use upside_gap_two_crows::UpsideGapTwoCrows;
|
||||||
pub use value_area::{ValueArea, ValueAreaOutput};
|
pub use value_area::{ValueArea, ValueAreaOutput};
|
||||||
@@ -1070,7 +1098,26 @@ pub const FAMILIES: &[(&str, &[&str])] = &[
|
|||||||
"Alt-Chart Bars",
|
"Alt-Chart Bars",
|
||||||
&["RenkoBars", "KagiBars", "PointAndFigureBars"],
|
&["RenkoBars", "KagiBars", "PointAndFigureBars"],
|
||||||
),
|
),
|
||||||
("Market Breadth", &["AdvanceDecline"]),
|
(
|
||||||
|
"Market Breadth",
|
||||||
|
&[
|
||||||
|
"AdvanceDecline",
|
||||||
|
"AdvanceDeclineRatio",
|
||||||
|
"AdVolumeLine",
|
||||||
|
"McClellanOscillator",
|
||||||
|
"McClellanSummationIndex",
|
||||||
|
"Trin",
|
||||||
|
"BreadthThrust",
|
||||||
|
"NewHighsNewLows",
|
||||||
|
"HighLowIndex",
|
||||||
|
"PercentAboveMa",
|
||||||
|
"UpDownVolumeRatio",
|
||||||
|
"BullishPercentIndex",
|
||||||
|
"CumulativeVolumeIndex",
|
||||||
|
"AbsoluteBreadthIndex",
|
||||||
|
"TickIndex",
|
||||||
|
],
|
||||||
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -1099,6 +1146,6 @@ mod family_tests {
|
|||||||
// the actual indicator count is the early-warning signal that an
|
// the actual indicator count is the early-warning signal that an
|
||||||
// indicator was added without being assigned a family.
|
// indicator was added without being assigned a family.
|
||||||
let total: usize = FAMILIES.iter().map(|(_, ns)| ns.len()).sum();
|
let total: usize = FAMILIES.iter().map(|(_, ns)| ns.len()).sum();
|
||||||
assert_eq!(total, 325, "FAMILIES total drifted from indicator count");
|
assert_eq!(total, 339, "FAMILIES total drifted from indicator count");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,142 @@
|
|||||||
|
//! New Highs − New Lows — net count of fresh period extremes across a universe.
|
||||||
|
|
||||||
|
use crate::cross_section::CrossSection;
|
||||||
|
use crate::traits::Indicator;
|
||||||
|
|
||||||
|
/// New Highs − New Lows — the number of symbols printing a new period high minus
|
||||||
|
/// the number printing a new period low across a universe.
|
||||||
|
///
|
||||||
|
/// On each [`CrossSection`] tick the value is `new_highs - new_lows`, read from the
|
||||||
|
/// per-symbol `new_high` / `new_low` flags. A persistently positive reading means
|
||||||
|
/// fresh leadership is broad (many names making new highs); a negative reading
|
||||||
|
/// during an index advance is a classic breadth divergence warning that the rally
|
||||||
|
/// is narrowing.
|
||||||
|
///
|
||||||
|
/// `Input = CrossSection`, `Output = f64`, `warmup_period == 1`.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use wickra_core::{CrossSection, Indicator, Member, NewHighsNewLows};
|
||||||
|
///
|
||||||
|
/// let mut nhnl = NewHighsNewLows::new();
|
||||||
|
/// // 2 new highs, 1 new low -> net +1.
|
||||||
|
/// let tick = CrossSection::new(
|
||||||
|
/// vec![
|
||||||
|
/// Member::new(1.0, 10.0, true, false),
|
||||||
|
/// Member::new(1.0, 10.0, true, false),
|
||||||
|
/// Member::new(-1.0, 10.0, false, true),
|
||||||
|
/// ],
|
||||||
|
/// 0,
|
||||||
|
/// )
|
||||||
|
/// .unwrap();
|
||||||
|
/// assert_eq!(nhnl.update(tick), Some(1.0));
|
||||||
|
/// ```
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
pub struct NewHighsNewLows {
|
||||||
|
has_emitted: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NewHighsNewLows {
|
||||||
|
/// Construct a new New Highs − New Lows indicator.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn new() -> Self {
|
||||||
|
Self { has_emitted: false }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Indicator for NewHighsNewLows {
|
||||||
|
type Input = CrossSection;
|
||||||
|
type Output = f64;
|
||||||
|
|
||||||
|
fn update(&mut self, section: CrossSection) -> Option<f64> {
|
||||||
|
let net = section.new_highs() as f64 - section.new_lows() as f64;
|
||||||
|
self.has_emitted = true;
|
||||||
|
Some(net)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reset(&mut self) {
|
||||||
|
self.has_emitted = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn warmup_period(&self) -> usize {
|
||||||
|
1
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_ready(&self) -> bool {
|
||||||
|
self.has_emitted
|
||||||
|
}
|
||||||
|
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
"NewHighsNewLows"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::cross_section::Member;
|
||||||
|
use crate::traits::BatchExt;
|
||||||
|
|
||||||
|
fn flags(highs: usize, lows: usize) -> CrossSection {
|
||||||
|
let mut members = Vec::new();
|
||||||
|
for _ in 0..highs {
|
||||||
|
members.push(Member::new(1.0, 10.0, true, false));
|
||||||
|
}
|
||||||
|
for _ in 0..lows {
|
||||||
|
members.push(Member::new(-1.0, 10.0, false, true));
|
||||||
|
}
|
||||||
|
members.push(Member::new(0.0, 10.0, false, false));
|
||||||
|
CrossSection::new(members, 0).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn accessors_and_metadata() {
|
||||||
|
let nhnl = NewHighsNewLows::new();
|
||||||
|
assert_eq!(nhnl.name(), "NewHighsNewLows");
|
||||||
|
assert_eq!(nhnl.warmup_period(), 1);
|
||||||
|
assert!(!nhnl.is_ready());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn first_tick_emits_net_extremes() {
|
||||||
|
let mut nhnl = NewHighsNewLows::new();
|
||||||
|
assert_eq!(nhnl.update(flags(5, 2)), Some(3.0));
|
||||||
|
assert!(nhnl.is_ready());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn more_lows_than_highs_is_negative() {
|
||||||
|
let mut nhnl = NewHighsNewLows::new();
|
||||||
|
assert_eq!(nhnl.update(flags(1, 4)), Some(-3.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn no_extremes_yields_zero() {
|
||||||
|
let mut nhnl = NewHighsNewLows::new();
|
||||||
|
assert_eq!(nhnl.update(flags(0, 0)), Some(0.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn reset_clears_state() {
|
||||||
|
let mut nhnl = NewHighsNewLows::new();
|
||||||
|
nhnl.update(flags(3, 1));
|
||||||
|
assert!(nhnl.is_ready());
|
||||||
|
nhnl.reset();
|
||||||
|
assert!(!nhnl.is_ready());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn batch_equals_streaming() {
|
||||||
|
let sections = vec![flags(5, 2), flags(1, 4), flags(0, 0)];
|
||||||
|
let mut a = NewHighsNewLows::new();
|
||||||
|
let mut b = NewHighsNewLows::new();
|
||||||
|
assert_eq!(
|
||||||
|
a.batch(§ions),
|
||||||
|
sections
|
||||||
|
.iter()
|
||||||
|
.map(|s| b.update(s.clone()))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,146 @@
|
|||||||
|
//! Percent Above Moving Average — share of a universe trading above its MA.
|
||||||
|
|
||||||
|
use crate::cross_section::CrossSection;
|
||||||
|
use crate::traits::Indicator;
|
||||||
|
|
||||||
|
/// Percent Above Moving Average — the percentage of symbols in a universe that
|
||||||
|
/// are trading above their reference moving average.
|
||||||
|
///
|
||||||
|
/// On each [`CrossSection`] tick the value is `100 * above_ma_count / universe
|
||||||
|
/// size`, read from the per-symbol `above_ma` flag (the caller decides which MA —
|
||||||
|
/// 50-day, 200-day — when it builds the tick). It is a bounded `0..=100` breadth
|
||||||
|
/// gauge: readings near 100 mean almost the whole universe is in an uptrend
|
||||||
|
/// (broad participation, but also a potential overbought extreme), readings near
|
||||||
|
/// zero mark washouts. Crosses of the 50 line are read as bull/bear regime flips.
|
||||||
|
///
|
||||||
|
/// `Input = CrossSection`, `Output = f64` (a percentage in `0..=100`),
|
||||||
|
/// `warmup_period == 1`. The universe is non-empty by construction, so the share
|
||||||
|
/// is always defined.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use wickra_core::{CrossSection, Indicator, Member, PercentAboveMa};
|
||||||
|
///
|
||||||
|
/// let mut pct = PercentAboveMa::new();
|
||||||
|
/// // 3 of 4 symbols above their MA -> 75%.
|
||||||
|
/// let tick = CrossSection::new(
|
||||||
|
/// vec![
|
||||||
|
/// Member::with_signals(1.0, 10.0, false, false, true, false),
|
||||||
|
/// Member::with_signals(1.0, 10.0, false, false, true, false),
|
||||||
|
/// Member::with_signals(-1.0, 10.0, false, false, true, false),
|
||||||
|
/// Member::with_signals(-1.0, 10.0, false, false, false, false),
|
||||||
|
/// ],
|
||||||
|
/// 0,
|
||||||
|
/// )
|
||||||
|
/// .unwrap();
|
||||||
|
/// assert_eq!(pct.update(tick), Some(75.0));
|
||||||
|
/// ```
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
pub struct PercentAboveMa {
|
||||||
|
has_emitted: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PercentAboveMa {
|
||||||
|
/// Construct a new Percent Above Moving Average indicator.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn new() -> Self {
|
||||||
|
Self { has_emitted: false }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Indicator for PercentAboveMa {
|
||||||
|
type Input = CrossSection;
|
||||||
|
type Output = f64;
|
||||||
|
|
||||||
|
fn update(&mut self, section: CrossSection) -> Option<f64> {
|
||||||
|
let above = section.above_ma_count() as f64;
|
||||||
|
let total = section.members.len() as f64;
|
||||||
|
self.has_emitted = true;
|
||||||
|
Some(100.0 * above / total)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reset(&mut self) {
|
||||||
|
self.has_emitted = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn warmup_period(&self) -> usize {
|
||||||
|
1
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_ready(&self) -> bool {
|
||||||
|
self.has_emitted
|
||||||
|
}
|
||||||
|
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
"PercentAboveMa"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::cross_section::Member;
|
||||||
|
use crate::traits::BatchExt;
|
||||||
|
|
||||||
|
fn tick(above: usize, below: usize) -> CrossSection {
|
||||||
|
let mut members = Vec::new();
|
||||||
|
for _ in 0..above {
|
||||||
|
members.push(Member::with_signals(1.0, 10.0, false, false, true, false));
|
||||||
|
}
|
||||||
|
for _ in 0..below {
|
||||||
|
members.push(Member::with_signals(-1.0, 10.0, false, false, false, false));
|
||||||
|
}
|
||||||
|
CrossSection::new(members, 0).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn accessors_and_metadata() {
|
||||||
|
let pct = PercentAboveMa::new();
|
||||||
|
assert_eq!(pct.name(), "PercentAboveMa");
|
||||||
|
assert_eq!(pct.warmup_period(), 1);
|
||||||
|
assert!(!pct.is_ready());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn first_tick_emits_percentage() {
|
||||||
|
let mut pct = PercentAboveMa::new();
|
||||||
|
assert_eq!(pct.update(tick(3, 1)), Some(75.0));
|
||||||
|
assert!(pct.is_ready());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn all_above_is_one_hundred() {
|
||||||
|
let mut pct = PercentAboveMa::new();
|
||||||
|
assert_eq!(pct.update(tick(4, 0)), Some(100.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn none_above_is_zero() {
|
||||||
|
let mut pct = PercentAboveMa::new();
|
||||||
|
assert_eq!(pct.update(tick(0, 5)), Some(0.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn reset_clears_state() {
|
||||||
|
let mut pct = PercentAboveMa::new();
|
||||||
|
pct.update(tick(3, 1));
|
||||||
|
assert!(pct.is_ready());
|
||||||
|
pct.reset();
|
||||||
|
assert!(!pct.is_ready());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn batch_equals_streaming() {
|
||||||
|
let sections = vec![tick(3, 1), tick(4, 0), tick(0, 5)];
|
||||||
|
let mut a = PercentAboveMa::new();
|
||||||
|
let mut b = PercentAboveMa::new();
|
||||||
|
assert_eq!(
|
||||||
|
a.batch(§ions),
|
||||||
|
sections
|
||||||
|
.iter()
|
||||||
|
.map(|s| b.update(s.clone()))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,148 @@
|
|||||||
|
//! TICK Index — instantaneous net advancing-minus-declining issues.
|
||||||
|
|
||||||
|
use crate::cross_section::CrossSection;
|
||||||
|
use crate::traits::Indicator;
|
||||||
|
|
||||||
|
/// TICK Index — the instantaneous net of advancing minus declining issues across
|
||||||
|
/// a universe, `advancers - decliners`.
|
||||||
|
///
|
||||||
|
/// Unlike the cumulative [`AdvanceDecline`](crate::AdvanceDecline) line, the TICK
|
||||||
|
/// is *not* accumulated: each tick reports the breadth of that snapshot alone. It
|
||||||
|
/// oscillates around zero — strongly positive readings mean a broad surge of
|
||||||
|
/// upticks (often an intraday overbought extreme), strongly negative readings a
|
||||||
|
/// broad flush. Traders fade extremes and watch the zero line for intraday bias.
|
||||||
|
///
|
||||||
|
/// `Input = CrossSection`, `Output = f64`, `warmup_period == 1`.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use wickra_core::{CrossSection, Indicator, Member, TickIndex};
|
||||||
|
///
|
||||||
|
/// let mut tick = TickIndex::new();
|
||||||
|
/// // 2 advancers, 5 decliners -> net -3.
|
||||||
|
/// let snapshot = CrossSection::new(
|
||||||
|
/// vec![
|
||||||
|
/// Member::new(1.0, 10.0, false, false),
|
||||||
|
/// Member::new(1.0, 10.0, false, false),
|
||||||
|
/// Member::new(-1.0, 10.0, false, false),
|
||||||
|
/// Member::new(-1.0, 10.0, false, false),
|
||||||
|
/// Member::new(-1.0, 10.0, false, false),
|
||||||
|
/// Member::new(-1.0, 10.0, false, false),
|
||||||
|
/// Member::new(-1.0, 10.0, false, false),
|
||||||
|
/// ],
|
||||||
|
/// 0,
|
||||||
|
/// )
|
||||||
|
/// .unwrap();
|
||||||
|
/// assert_eq!(tick.update(snapshot), Some(-3.0));
|
||||||
|
/// ```
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
pub struct TickIndex {
|
||||||
|
has_emitted: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TickIndex {
|
||||||
|
/// Construct a new TICK Index indicator.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn new() -> Self {
|
||||||
|
Self { has_emitted: false }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Indicator for TickIndex {
|
||||||
|
type Input = CrossSection;
|
||||||
|
type Output = f64;
|
||||||
|
|
||||||
|
fn update(&mut self, section: CrossSection) -> Option<f64> {
|
||||||
|
let net = section.advancers() as f64 - section.decliners() as f64;
|
||||||
|
self.has_emitted = true;
|
||||||
|
Some(net)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reset(&mut self) {
|
||||||
|
self.has_emitted = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn warmup_period(&self) -> usize {
|
||||||
|
1
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_ready(&self) -> bool {
|
||||||
|
self.has_emitted
|
||||||
|
}
|
||||||
|
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
"TickIndex"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::cross_section::Member;
|
||||||
|
use crate::traits::BatchExt;
|
||||||
|
|
||||||
|
fn section(up: usize, down: usize) -> CrossSection {
|
||||||
|
let mut members = Vec::new();
|
||||||
|
for _ in 0..up {
|
||||||
|
members.push(Member::new(1.0, 10.0, false, false));
|
||||||
|
}
|
||||||
|
for _ in 0..down {
|
||||||
|
members.push(Member::new(-1.0, 10.0, false, false));
|
||||||
|
}
|
||||||
|
members.push(Member::new(0.0, 10.0, false, false));
|
||||||
|
CrossSection::new(members, 0).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn accessors_and_metadata() {
|
||||||
|
let tick = TickIndex::new();
|
||||||
|
assert_eq!(tick.name(), "TickIndex");
|
||||||
|
assert_eq!(tick.warmup_period(), 1);
|
||||||
|
assert!(!tick.is_ready());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn positive_when_advancers_lead() {
|
||||||
|
let mut tick = TickIndex::new();
|
||||||
|
assert_eq!(tick.update(section(5, 2)), Some(3.0));
|
||||||
|
assert!(tick.is_ready());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn negative_when_decliners_lead() {
|
||||||
|
let mut tick = TickIndex::new();
|
||||||
|
assert_eq!(tick.update(section(2, 5)), Some(-3.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn does_not_accumulate() {
|
||||||
|
let mut tick = TickIndex::new();
|
||||||
|
// Each tick is independent — the second reading does not carry the first.
|
||||||
|
assert_eq!(tick.update(section(3, 0)), Some(3.0));
|
||||||
|
assert_eq!(tick.update(section(0, 1)), Some(-1.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn reset_clears_state() {
|
||||||
|
let mut tick = TickIndex::new();
|
||||||
|
tick.update(section(3, 0));
|
||||||
|
assert!(tick.is_ready());
|
||||||
|
tick.reset();
|
||||||
|
assert!(!tick.is_ready());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn batch_equals_streaming() {
|
||||||
|
let sections = vec![section(5, 2), section(2, 5), section(3, 0), section(0, 1)];
|
||||||
|
let mut a = TickIndex::new();
|
||||||
|
let mut b = TickIndex::new();
|
||||||
|
assert_eq!(
|
||||||
|
a.batch(§ions),
|
||||||
|
sections
|
||||||
|
.iter()
|
||||||
|
.map(|s| b.update(s.clone()))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,171 @@
|
|||||||
|
//! TRIN / Arms Index — the advance-decline ratio over the up-down volume ratio.
|
||||||
|
|
||||||
|
use crate::cross_section::CrossSection;
|
||||||
|
use crate::traits::Indicator;
|
||||||
|
|
||||||
|
/// TRIN (Arms Index) — `(advancers / decliners) / (advancing volume / declining
|
||||||
|
/// volume)`.
|
||||||
|
///
|
||||||
|
/// The TRIN compares the breadth of a move in *issues* to the breadth of the move
|
||||||
|
/// in *volume*. A value near `1.0` means advancing issues and advancing volume are
|
||||||
|
/// in balance; a value below `1.0` is bullish (volume is concentrated in advancing
|
||||||
|
/// issues relative to their count); a value above `1.0` is bearish (declining
|
||||||
|
/// issues are absorbing disproportionate volume).
|
||||||
|
///
|
||||||
|
/// To stay finite on degenerate ticks the decliner count is floored to one and
|
||||||
|
/// both volume sums are floored to `1.0`, so a tick with no declining issues or no
|
||||||
|
/// volume on one side still yields a defined reading instead of a division by
|
||||||
|
/// zero.
|
||||||
|
///
|
||||||
|
/// `Input = CrossSection`, `Output = f64`, `warmup_period == 1`.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use wickra_core::{CrossSection, Indicator, Member, Trin};
|
||||||
|
///
|
||||||
|
/// let mut trin = Trin::new();
|
||||||
|
/// // 3 advancers / 1 decliner = 3; adv vol 150 / dec vol 50 = 3; TRIN = 1.0.
|
||||||
|
/// let tick = CrossSection::new(
|
||||||
|
/// vec![
|
||||||
|
/// Member::new(1.0, 50.0, false, false),
|
||||||
|
/// Member::new(1.0, 50.0, false, false),
|
||||||
|
/// Member::new(1.0, 50.0, false, false),
|
||||||
|
/// Member::new(-1.0, 50.0, false, false),
|
||||||
|
/// ],
|
||||||
|
/// 0,
|
||||||
|
/// )
|
||||||
|
/// .unwrap();
|
||||||
|
/// assert_eq!(trin.update(tick), Some(1.0));
|
||||||
|
/// ```
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
pub struct Trin {
|
||||||
|
has_emitted: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Trin {
|
||||||
|
/// Construct a new TRIN / Arms Index indicator.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn new() -> Self {
|
||||||
|
Self { has_emitted: false }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Indicator for Trin {
|
||||||
|
type Input = CrossSection;
|
||||||
|
type Output = f64;
|
||||||
|
|
||||||
|
fn update(&mut self, section: CrossSection) -> Option<f64> {
|
||||||
|
let advancers = section.advancers() as f64;
|
||||||
|
let decliners = section.decliners().max(1) as f64;
|
||||||
|
let advancing_volume = section.advancing_volume().max(1.0);
|
||||||
|
let declining_volume = section.declining_volume().max(1.0);
|
||||||
|
let ad_ratio = advancers / decliners;
|
||||||
|
let volume_ratio = advancing_volume / declining_volume;
|
||||||
|
self.has_emitted = true;
|
||||||
|
Some(ad_ratio / volume_ratio)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reset(&mut self) {
|
||||||
|
self.has_emitted = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn warmup_period(&self) -> usize {
|
||||||
|
1
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_ready(&self) -> bool {
|
||||||
|
self.has_emitted
|
||||||
|
}
|
||||||
|
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
"Trin"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::cross_section::Member;
|
||||||
|
use crate::traits::BatchExt;
|
||||||
|
|
||||||
|
fn tick(items: &[(f64, f64)]) -> CrossSection {
|
||||||
|
CrossSection::new(
|
||||||
|
items
|
||||||
|
.iter()
|
||||||
|
.map(|&(change, volume)| Member::new(change, volume, false, false))
|
||||||
|
.collect(),
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn accessors_and_metadata() {
|
||||||
|
let trin = Trin::new();
|
||||||
|
assert_eq!(trin.name(), "Trin");
|
||||||
|
assert_eq!(trin.warmup_period(), 1);
|
||||||
|
assert!(!trin.is_ready());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn balanced_breadth_yields_one() {
|
||||||
|
let mut trin = Trin::new();
|
||||||
|
let value = trin
|
||||||
|
.update(tick(&[(1.0, 50.0), (1.0, 50.0), (1.0, 50.0), (-1.0, 50.0)]))
|
||||||
|
.unwrap();
|
||||||
|
assert!((value - 1.0).abs() < 1e-9);
|
||||||
|
assert!(trin.is_ready());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn zero_decliners_and_volume_are_floored() {
|
||||||
|
let mut trin = Trin::new();
|
||||||
|
// 2 advancers, 0 decliners, adv vol 100, dec vol 0.
|
||||||
|
// ad_ratio = 2 / max(0,1) = 2; volume_ratio = 100 / max(0,1) = 100; TRIN = 0.02.
|
||||||
|
let value = trin.update(tick(&[(1.0, 50.0), (1.0, 50.0)])).unwrap();
|
||||||
|
assert!((value - 0.02).abs() < 1e-9);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn heavy_declining_volume_pushes_above_one() {
|
||||||
|
let mut trin = Trin::new();
|
||||||
|
// 2 adv / 2 dec = 1; adv vol 20 / dec vol 80 = 0.25; TRIN = 4.0.
|
||||||
|
let value = trin
|
||||||
|
.update(tick(&[
|
||||||
|
(1.0, 10.0),
|
||||||
|
(1.0, 10.0),
|
||||||
|
(-1.0, 40.0),
|
||||||
|
(-1.0, 40.0),
|
||||||
|
]))
|
||||||
|
.unwrap();
|
||||||
|
assert!((value - 4.0).abs() < 1e-9);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn reset_clears_state() {
|
||||||
|
let mut trin = Trin::new();
|
||||||
|
trin.update(tick(&[(1.0, 10.0), (-1.0, 10.0)]));
|
||||||
|
assert!(trin.is_ready());
|
||||||
|
trin.reset();
|
||||||
|
assert!(!trin.is_ready());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn batch_equals_streaming() {
|
||||||
|
let sections = vec![
|
||||||
|
tick(&[(1.0, 50.0), (1.0, 50.0), (1.0, 50.0), (-1.0, 50.0)]),
|
||||||
|
tick(&[(1.0, 50.0), (1.0, 50.0)]),
|
||||||
|
tick(&[(1.0, 10.0), (1.0, 10.0), (-1.0, 40.0), (-1.0, 40.0)]),
|
||||||
|
];
|
||||||
|
let mut a = Trin::new();
|
||||||
|
let mut b = Trin::new();
|
||||||
|
assert_eq!(
|
||||||
|
a.batch(§ions),
|
||||||
|
sections
|
||||||
|
.iter()
|
||||||
|
.map(|s| b.update(s.clone()))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,143 @@
|
|||||||
|
//! Up/Down Volume Ratio — advancing volume divided by declining volume.
|
||||||
|
|
||||||
|
use crate::cross_section::CrossSection;
|
||||||
|
use crate::traits::Indicator;
|
||||||
|
|
||||||
|
/// Up/Down Volume Ratio — total advancing volume divided by total declining
|
||||||
|
/// volume across a universe.
|
||||||
|
///
|
||||||
|
/// On each [`CrossSection`] tick the ratio is `advancing volume / declining
|
||||||
|
/// volume`. A reading above one means more volume is trading in advancing issues
|
||||||
|
/// than declining ones (accumulation); a reading below one means distribution.
|
||||||
|
/// Sustained extremes are used to flag breadth thrusts and washout bottoms.
|
||||||
|
///
|
||||||
|
/// When a tick has no declining volume the denominator is floored to `1.0`, so the
|
||||||
|
/// ratio stays finite (it degrades to the advancing-volume total) instead of
|
||||||
|
/// dividing by zero.
|
||||||
|
///
|
||||||
|
/// `Input = CrossSection`, `Output = f64`, `warmup_period == 1`.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use wickra_core::{CrossSection, Indicator, Member, UpDownVolumeRatio};
|
||||||
|
///
|
||||||
|
/// let mut udv = UpDownVolumeRatio::new();
|
||||||
|
/// // advancing volume 150, declining volume 50 -> ratio 3.0.
|
||||||
|
/// let tick = CrossSection::new(
|
||||||
|
/// vec![
|
||||||
|
/// Member::new(1.0, 150.0, false, false),
|
||||||
|
/// Member::new(-1.0, 50.0, false, false),
|
||||||
|
/// ],
|
||||||
|
/// 0,
|
||||||
|
/// )
|
||||||
|
/// .unwrap();
|
||||||
|
/// assert_eq!(udv.update(tick), Some(3.0));
|
||||||
|
/// ```
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
pub struct UpDownVolumeRatio {
|
||||||
|
has_emitted: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UpDownVolumeRatio {
|
||||||
|
/// Construct a new Up/Down Volume Ratio indicator.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn new() -> Self {
|
||||||
|
Self { has_emitted: false }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Indicator for UpDownVolumeRatio {
|
||||||
|
type Input = CrossSection;
|
||||||
|
type Output = f64;
|
||||||
|
|
||||||
|
fn update(&mut self, section: CrossSection) -> Option<f64> {
|
||||||
|
let advancing_volume = section.advancing_volume();
|
||||||
|
let declining_volume = section.declining_volume().max(1.0);
|
||||||
|
self.has_emitted = true;
|
||||||
|
Some(advancing_volume / declining_volume)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reset(&mut self) {
|
||||||
|
self.has_emitted = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn warmup_period(&self) -> usize {
|
||||||
|
1
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_ready(&self) -> bool {
|
||||||
|
self.has_emitted
|
||||||
|
}
|
||||||
|
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
"UpDownVolumeRatio"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::cross_section::Member;
|
||||||
|
use crate::traits::BatchExt;
|
||||||
|
|
||||||
|
fn tick(items: &[(f64, f64)]) -> CrossSection {
|
||||||
|
CrossSection::new(
|
||||||
|
items
|
||||||
|
.iter()
|
||||||
|
.map(|&(change, volume)| Member::new(change, volume, false, false))
|
||||||
|
.collect(),
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn accessors_and_metadata() {
|
||||||
|
let udv = UpDownVolumeRatio::new();
|
||||||
|
assert_eq!(udv.name(), "UpDownVolumeRatio");
|
||||||
|
assert_eq!(udv.warmup_period(), 1);
|
||||||
|
assert!(!udv.is_ready());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn first_tick_emits_ratio() {
|
||||||
|
let mut udv = UpDownVolumeRatio::new();
|
||||||
|
assert_eq!(udv.update(tick(&[(1.0, 150.0), (-1.0, 50.0)])), Some(3.0));
|
||||||
|
assert!(udv.is_ready());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn zero_declining_volume_floors_denominator() {
|
||||||
|
let mut udv = UpDownVolumeRatio::new();
|
||||||
|
// advancing volume 100, declining volume 0 -> 100 / max(0, 1) = 100.0.
|
||||||
|
assert_eq!(udv.update(tick(&[(1.0, 100.0)])), Some(100.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn reset_clears_state() {
|
||||||
|
let mut udv = UpDownVolumeRatio::new();
|
||||||
|
udv.update(tick(&[(1.0, 10.0), (-1.0, 10.0)]));
|
||||||
|
assert!(udv.is_ready());
|
||||||
|
udv.reset();
|
||||||
|
assert!(!udv.is_ready());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn batch_equals_streaming() {
|
||||||
|
let sections = vec![
|
||||||
|
tick(&[(1.0, 150.0), (-1.0, 50.0)]),
|
||||||
|
tick(&[(1.0, 100.0)]),
|
||||||
|
tick(&[(1.0, 20.0), (-1.0, 80.0)]),
|
||||||
|
];
|
||||||
|
let mut a = UpDownVolumeRatio::new();
|
||||||
|
let mut b = UpDownVolumeRatio::new();
|
||||||
|
assert_eq!(
|
||||||
|
a.batch(§ions),
|
||||||
|
sections
|
||||||
|
.iter()
|
||||||
|
.map(|s| b.update(s.clone()))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -55,20 +55,21 @@ pub use cross_section::{CrossSection, Member};
|
|||||||
pub use derivatives::DerivativesTick;
|
pub use derivatives::DerivativesTick;
|
||||||
pub use error::{Error, Result};
|
pub use error::{Error, Result};
|
||||||
pub use indicators::{
|
pub use indicators::{
|
||||||
AbandonedBaby, AccelerationBands, AccelerationBandsOutput, AcceleratorOscillator, AdOscillator,
|
AbandonedBaby, AbsoluteBreadthIndex, AccelerationBands, AccelerationBandsOutput,
|
||||||
AdaptiveCycle, Adl, AdvanceBlock, AdvanceDecline, Adx, AdxOutput, Adxr, Alligator,
|
AcceleratorOscillator, AdOscillator, AdVolumeLine, AdaptiveCycle, Adl, AdvanceBlock,
|
||||||
AlligatorOutput, Alma, Alpha, AnchoredRsi, AnchoredVwap, Apo, Aroon, AroonOscillator,
|
AdvanceDecline, AdvanceDeclineRatio, Adx, AdxOutput, Adxr, Alligator, AlligatorOutput, Alma,
|
||||||
AroonOutput, Atr, AtrBands, AtrBandsOutput, AtrTrailingStop, Autocorrelation, AverageDrawdown,
|
Alpha, AnchoredRsi, AnchoredVwap, Apo, Aroon, AroonOscillator, AroonOutput, Atr, AtrBands,
|
||||||
AvgPrice, AwesomeOscillator, AwesomeOscillatorHistogram, BalanceOfPower, BeltHold, Beta,
|
AtrBandsOutput, AtrTrailingStop, Autocorrelation, AverageDrawdown, AvgPrice, AwesomeOscillator,
|
||||||
BetaNeutralSpread, BollingerBands, BollingerBandwidth, BollingerOutput, Breakaway,
|
AwesomeOscillatorHistogram, BalanceOfPower, BeltHold, Beta, BetaNeutralSpread, BollingerBands,
|
||||||
|
BollingerBandwidth, BollingerOutput, BreadthThrust, Breakaway, BullishPercentIndex,
|
||||||
CalendarSpread, CalmarRatio, Camarilla, CamarillaPivotsOutput, Cci, CenterOfGravity, Cfo,
|
CalendarSpread, CalmarRatio, Camarilla, CamarillaPivotsOutput, Cci, CenterOfGravity, Cfo,
|
||||||
ChaikinMoneyFlow, ChaikinOscillator, ChaikinVolatility, ChandeKrollStop, ChandeKrollStopOutput,
|
ChaikinMoneyFlow, ChaikinOscillator, ChaikinVolatility, ChandeKrollStop, ChandeKrollStopOutput,
|
||||||
ChandelierExit, ChandelierExitOutput, ChoppinessIndex, ClassicPivots, ClassicPivotsOutput,
|
ChandelierExit, ChandelierExitOutput, ChoppinessIndex, ClassicPivots, ClassicPivotsOutput,
|
||||||
ClosingMarubozu, Cmo, CoefficientOfVariation, Cointegration, CointegrationOutput,
|
ClosingMarubozu, Cmo, CoefficientOfVariation, Cointegration, CointegrationOutput,
|
||||||
ConcealingBabySwallow, ConditionalValueAtRisk, ConnorsRsi, Coppock, Counterattack,
|
ConcealingBabySwallow, ConditionalValueAtRisk, ConnorsRsi, Coppock, Counterattack,
|
||||||
CumulativeVolumeDelta, CyberneticCycle, Decycler, DecyclerOscillator, Dema, DemandIndex,
|
CumulativeVolumeDelta, CumulativeVolumeIndex, CyberneticCycle, Decycler, DecyclerOscillator,
|
||||||
DemarkPivots, DemarkPivotsOutput, DepthSlope, DetrendedStdDev, DistanceSsd, Doji, DojiStar,
|
Dema, DemandIndex, DemarkPivots, DemarkPivotsOutput, DepthSlope, DetrendedStdDev, DistanceSsd,
|
||||||
Donchian, DonchianOutput, DonchianStop, DonchianStopOutput, DoubleBollinger,
|
Doji, DojiStar, Donchian, DonchianOutput, DonchianStop, DonchianStopOutput, DoubleBollinger,
|
||||||
DoubleBollingerOutput, DownsideGapThreeMethods, Dpo, DragonflyDoji, DrawdownDuration, Dx,
|
DoubleBollingerOutput, DownsideGapThreeMethods, Dpo, DragonflyDoji, DrawdownDuration, Dx,
|
||||||
EaseOfMovement, EffectiveSpread, EhlersStochastic, ElderImpulse, Ema,
|
EaseOfMovement, EffectiveSpread, EhlersStochastic, ElderImpulse, Ema,
|
||||||
EmpiricalModeDecomposition, Engulfing, EveningDojiStar, Evwma, FallingThreeMethods, Fama,
|
EmpiricalModeDecomposition, Engulfing, EveningDojiStar, Evwma, FallingThreeMethods, Fama,
|
||||||
@@ -76,47 +77,48 @@ pub use indicators::{
|
|||||||
ForceIndex, FractalChaosBands, FractalChaosBandsOutput, Frama, FundingBasis, FundingRate,
|
ForceIndex, FractalChaosBands, FractalChaosBandsOutput, Frama, FundingBasis, FundingRate,
|
||||||
FundingRateMean, FundingRateZScore, GainLossRatio, GapSideBySideWhite, GarmanKlassVolatility,
|
FundingRateMean, FundingRateZScore, GainLossRatio, GapSideBySideWhite, GarmanKlassVolatility,
|
||||||
GrangerCausality, GravestoneDoji, Hammer, HangingMan, Harami, HeikinAshi, HeikinAshiOutput,
|
GrangerCausality, GravestoneDoji, Hammer, HangingMan, Harami, HeikinAshi, HeikinAshiOutput,
|
||||||
HiLoActivator, HighWave, Hikkake, HikkakeModified, HilbertDominantCycle, HistoricalVolatility,
|
HiLoActivator, HighLowIndex, HighWave, Hikkake, HikkakeModified, HilbertDominantCycle,
|
||||||
Hma, HomingPigeon, HtDcPhase, HtPhasor, HtPhasorOutput, HtTrendMode, HurstChannel,
|
HistoricalVolatility, Hma, HomingPigeon, HtDcPhase, HtPhasor, HtPhasorOutput, HtTrendMode,
|
||||||
HurstChannelOutput, HurstExponent, Ichimoku, IchimokuOutput, IdenticalThreeCrows, InNeck,
|
HurstChannel, HurstChannelOutput, HurstExponent, Ichimoku, IchimokuOutput, IdenticalThreeCrows,
|
||||||
Inertia, InformationRatio, InitialBalance, InitialBalanceOutput, InstantaneousTrendline,
|
InNeck, Inertia, InformationRatio, InitialBalance, InitialBalanceOutput,
|
||||||
InverseFisherTransform, InvertedHammer, Jma, KagiBars, KalmanHedgeRatio,
|
InstantaneousTrendline, InverseFisherTransform, InvertedHammer, Jma, KagiBars,
|
||||||
KalmanHedgeRatioOutput, Kama, KellyCriterion, Keltner, KeltnerOutput, Kicking, KickingByLength,
|
KalmanHedgeRatio, KalmanHedgeRatioOutput, Kama, KellyCriterion, Keltner, KeltnerOutput,
|
||||||
Kst, KstOutput, Kurtosis, Kvo, KylesLambda, LadderBottom, LaguerreRsi, LeadLagCrossCorrelation,
|
Kicking, KickingByLength, Kst, KstOutput, Kurtosis, Kvo, KylesLambda, LadderBottom,
|
||||||
LeadLagCrossCorrelationOutput, LinRegAngle, LinRegChannel, LinRegChannelOutput,
|
LaguerreRsi, LeadLagCrossCorrelation, LeadLagCrossCorrelationOutput, LinRegAngle,
|
||||||
LinRegIntercept, LinRegSlope, LinearRegression, LiquidationFeatures, LiquidationFeaturesOutput,
|
LinRegChannel, LinRegChannelOutput, LinRegIntercept, LinRegSlope, LinearRegression,
|
||||||
LongLeggedDoji, LongLine, LongShortRatio, MaEnvelope, MaEnvelopeOutput, MacdExt, MacdFix,
|
LiquidationFeatures, LiquidationFeaturesOutput, LongLeggedDoji, LongLine, LongShortRatio,
|
||||||
MacdIndicator, MacdOutput, Mama, MamaOutput, MarketFacilitationIndex, Marubozu, MassIndex,
|
MaEnvelope, MaEnvelopeOutput, MacdExt, MacdFix, MacdIndicator, MacdOutput, Mama, MamaOutput,
|
||||||
MatHold, MatchingLow, MaxDrawdown, McGinleyDynamic, MedianAbsoluteDeviation, MedianPrice, Mfi,
|
MarketFacilitationIndex, Marubozu, MassIndex, MatHold, MatchingLow, MaxDrawdown,
|
||||||
Microprice, MidPoint, MidPrice, MinusDi, MinusDm, Mom, MorningDojiStar, MorningEveningStar,
|
McClellanOscillator, McClellanSummationIndex, McGinleyDynamic, MedianAbsoluteDeviation,
|
||||||
Natr, Nvi, OIPriceDivergence, OIWeighted, Obv, OmegaRatio, OnNeck, OpenInterestDelta,
|
MedianPrice, Mfi, Microprice, MidPoint, MidPrice, MinusDi, MinusDm, Mom, MorningDojiStar,
|
||||||
OpeningMarubozu, OpeningRange, OpeningRangeOutput, OrderBookImbalanceFull,
|
MorningEveningStar, Natr, NewHighsNewLows, Nvi, OIPriceDivergence, OIWeighted, Obv, OmegaRatio,
|
||||||
OrderBookImbalanceTop1, OrderBookImbalanceTopN, OuHalfLife, PainIndex, PairSpreadZScore,
|
OnNeck, OpenInterestDelta, OpeningMarubozu, OpeningRange, OpeningRangeOutput,
|
||||||
PairwiseBeta, ParkinsonVolatility, PearsonCorrelation, PercentB, PercentageTrailingStop, Pgo,
|
OrderBookImbalanceFull, OrderBookImbalanceTop1, OrderBookImbalanceTopN, OuHalfLife, PainIndex,
|
||||||
PiercingDarkCloud, PlusDi, PlusDm, Pmo, PointAndFigureBars, Ppo, ProfitFactor, Psar, Pvi,
|
PairSpreadZScore, PairwiseBeta, ParkinsonVolatility, PearsonCorrelation, PercentAboveMa,
|
||||||
QuotedSpread, RSquared, RealizedSpread, RecoveryFactor, RelativeStrengthAB,
|
PercentB, PercentageTrailingStop, Pgo, PiercingDarkCloud, PlusDi, PlusDm, Pmo,
|
||||||
RelativeStrengthOutput, RenkoBars, RenkoTrailingStop, RickshawMan, RisingThreeMethods, Roc,
|
PointAndFigureBars, Ppo, ProfitFactor, Psar, Pvi, QuotedSpread, RSquared, RealizedSpread,
|
||||||
Rocp, Rocr, Rocr100, RogersSatchellVolatility, RollingCorrelation, RollingCovariance,
|
RecoveryFactor, RelativeStrengthAB, RelativeStrengthOutput, RenkoBars, RenkoTrailingStop,
|
||||||
RollingVwap, RoofingFilter, Rsi, Rvi, RviVolatility, Rwi, RwiOutput, SarExt, SeparatingLines,
|
RickshawMan, RisingThreeMethods, Roc, Rocp, Rocr, Rocr100, RogersSatchellVolatility,
|
||||||
SharpeRatio, ShootingStar, ShortLine, SignedVolume, SineWave, Skewness, Sma, Smi, Smma,
|
RollingCorrelation, RollingCovariance, RollingVwap, RoofingFilter, Rsi, Rvi, RviVolatility,
|
||||||
SortinoRatio, SpearmanCorrelation, SpinningTop, SpreadBollingerBands,
|
Rwi, RwiOutput, SarExt, SeparatingLines, SharpeRatio, ShootingStar, ShortLine, SignedVolume,
|
||||||
SpreadBollingerBandsOutput, SpreadHurst, StalledPattern, StandardError, StandardErrorBands,
|
SineWave, Skewness, Sma, Smi, Smma, SortinoRatio, SpearmanCorrelation, SpinningTop,
|
||||||
StandardErrorBandsOutput, StarcBands, StarcBandsOutput, Stc, StdDev, StepTrailingStop,
|
SpreadBollingerBands, SpreadBollingerBandsOutput, SpreadHurst, StalledPattern, StandardError,
|
||||||
StickSandwich, StochRsi, Stochastic, StochasticOutput, SuperSmoother, SuperTrend,
|
StandardErrorBands, StandardErrorBandsOutput, StarcBands, StarcBandsOutput, Stc, StdDev,
|
||||||
SuperTrendOutput, TakerBuySellRatio, Takuri, TasukiGap, TdCombo, TdCountdown, TdDeMarker,
|
StepTrailingStop, StickSandwich, StochRsi, Stochastic, StochasticOutput, SuperSmoother,
|
||||||
TdDifferential, TdLines, TdLinesOutput, TdOpen, TdPressure, TdRangeProjection,
|
SuperTrend, SuperTrendOutput, TakerBuySellRatio, Takuri, TasukiGap, TdCombo, TdCountdown,
|
||||||
|
TdDeMarker, TdDifferential, TdLines, TdLinesOutput, TdOpen, TdPressure, TdRangeProjection,
|
||||||
TdRangeProjectionOutput, TdRei, TdRiskLevel, TdRiskLevelOutput, TdSequential,
|
TdRangeProjectionOutput, TdRei, TdRiskLevel, TdRiskLevelOutput, TdSequential,
|
||||||
TdSequentialOutput, TdSetup, Tema, TermStructureBasis, ThreeInside, ThreeLineStrike,
|
TdSequentialOutput, TdSetup, Tema, TermStructureBasis, ThreeInside, ThreeLineStrike,
|
||||||
ThreeOutside, ThreeSoldiersOrCrows, ThreeStarsInSouth, Thrusting, Tii, TpoProfile,
|
ThreeOutside, ThreeSoldiersOrCrows, ThreeStarsInSouth, Thrusting, TickIndex, Tii, TpoProfile,
|
||||||
TpoProfileOutput, TradeImbalance, TreynorRatio, Trima, Trix, TrueRange, Tsf, Tsi, Tsv,
|
TpoProfileOutput, TradeImbalance, TreynorRatio, Trima, Trin, Trix, TrueRange, Tsf, Tsi, Tsv,
|
||||||
TtmSqueeze, TtmSqueezeOutput, Tweezer, TwoCrows, TypicalPrice, UlcerIndex, UltimateOscillator,
|
TtmSqueeze, TtmSqueezeOutput, Tweezer, TwoCrows, TypicalPrice, UlcerIndex, UltimateOscillator,
|
||||||
UniqueThreeRiver, UpsideGapThreeMethods, UpsideGapTwoCrows, ValueArea, ValueAreaOutput,
|
UniqueThreeRiver, UpDownVolumeRatio, UpsideGapThreeMethods, UpsideGapTwoCrows, ValueArea,
|
||||||
ValueAtRisk, Variance, VarianceRatio, VerticalHorizontalFilter, Vidya, VoltyStop,
|
ValueAreaOutput, ValueAtRisk, Variance, VarianceRatio, VerticalHorizontalFilter, Vidya,
|
||||||
VolumeOscillator, VolumePriceTrend, VolumeProfile, VolumeProfileOutput, Vortex, VortexOutput,
|
VoltyStop, VolumeOscillator, VolumePriceTrend, VolumeProfile, VolumeProfileOutput, Vortex,
|
||||||
Vwap, VwapStdDevBands, VwapStdDevBandsOutput, Vwma, Vzo, WaveTrend, WaveTrendOutput,
|
VortexOutput, Vwap, VwapStdDevBands, VwapStdDevBandsOutput, Vwma, Vzo, WaveTrend,
|
||||||
WeightedClose, WilliamsFractals, WilliamsFractalsOutput, WilliamsR, Wma, WoodiePivots,
|
WaveTrendOutput, WeightedClose, WilliamsFractals, WilliamsFractalsOutput, WilliamsR, Wma,
|
||||||
WoodiePivotsOutput, YangZhangVolatility, YoyoExit, ZScore, ZeroLagMacd, ZeroLagMacdOutput,
|
WoodiePivots, WoodiePivotsOutput, YangZhangVolatility, YoyoExit, ZScore, ZeroLagMacd,
|
||||||
ZigZag, ZigZagOutput, Zlema, FAMILIES, T3,
|
ZeroLagMacdOutput, ZigZag, ZigZagOutput, Zlema, FAMILIES, T3,
|
||||||
};
|
};
|
||||||
// `FootprintLevel` is a row element of `FootprintOutput`, re-exported on its own
|
// `FootprintLevel` is a row element of `FootprintOutput`, re-exported on its own
|
||||||
// line so the indicator-count tooling (which scans the braced block above and
|
// line so the indicator-count tooling (which scans the braced block above and
|
||||||
|
|||||||
+1
-1
@@ -8,7 +8,7 @@ That includes:
|
|||||||
[Python](https://docs.wickra.org/Quickstart-Python),
|
[Python](https://docs.wickra.org/Quickstart-Python),
|
||||||
[Node](https://docs.wickra.org/Quickstart-Node), and
|
[Node](https://docs.wickra.org/Quickstart-Node), and
|
||||||
[WASM](https://docs.wickra.org/Quickstart-WASM).
|
[WASM](https://docs.wickra.org/Quickstart-WASM).
|
||||||
- A per-indicator deep dive for every one of the **325 indicators** across
|
- A per-indicator deep dive for every one of the **339 indicators** across
|
||||||
the sixteen families (Moving Averages, Momentum Oscillators, Trend &
|
the sixteen families (Moving Averages, Momentum Oscillators, Trend &
|
||||||
Directional, Price Oscillators, Volatility & Bands, Bands & Channels,
|
Directional, Price Oscillators, Volatility & Bands, Bands & Channels,
|
||||||
Trailing Stops, Volume, Price Statistics, Ehlers / Cycle DSP, Pivots &
|
Trailing Stops, Volume, Price Statistics, Ehlers / Cycle DSP, Pivots &
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
//! streaming or batched.
|
//! streaming or batched.
|
||||||
|
|
||||||
use libfuzzer_sys::fuzz_target;
|
use libfuzzer_sys::fuzz_target;
|
||||||
use wickra_core::{AdvanceDecline, BatchExt, CrossSection, Indicator, Member};
|
use wickra_core::{AbsoluteBreadthIndex, AdVolumeLine, AdvanceDecline, AdvanceDeclineRatio, BatchExt, BreadthThrust, BullishPercentIndex, CrossSection, CumulativeVolumeIndex, HighLowIndex, Indicator, McClellanOscillator, McClellanSummationIndex, Member, NewHighsNewLows, PercentAboveMa, TickIndex, Trin, UpDownVolumeRatio};
|
||||||
|
|
||||||
#[inline(never)]
|
#[inline(never)]
|
||||||
fn drive<I>(make: impl Fn() -> I, sections: &[CrossSection])
|
fn drive<I>(make: impl Fn() -> I, sections: &[CrossSection])
|
||||||
@@ -44,4 +44,18 @@ fuzz_target!(|data: &[u8]| {
|
|||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
drive(AdvanceDecline::new, §ions);
|
drive(AdvanceDecline::new, §ions);
|
||||||
|
drive(AdvanceDeclineRatio::new, §ions);
|
||||||
|
drive(AdVolumeLine::new, §ions);
|
||||||
|
drive(McClellanOscillator::new, §ions);
|
||||||
|
drive(McClellanSummationIndex::new, §ions);
|
||||||
|
drive(Trin::new, §ions);
|
||||||
|
drive(|| BreadthThrust::new(10).unwrap(), §ions);
|
||||||
|
drive(NewHighsNewLows::new, §ions);
|
||||||
|
drive(|| HighLowIndex::new(10).unwrap(), §ions);
|
||||||
|
drive(PercentAboveMa::new, §ions);
|
||||||
|
drive(UpDownVolumeRatio::new, §ions);
|
||||||
|
drive(BullishPercentIndex::new, §ions);
|
||||||
|
drive(CumulativeVolumeIndex::new, §ions);
|
||||||
|
drive(AbsoluteBreadthIndex::new, §ions);
|
||||||
|
drive(TickIndex::new, §ions);
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user