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).
|
||||
|
||||
## [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
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<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>
|
||||
|
||||
[](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),
|
||||
[WASM](https://docs.wickra.org/Quickstart-WASM).
|
||||
- **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).
|
||||
- **Reference** — [warmup periods](https://docs.wickra.org/Warmup-Periods),
|
||||
[streaming vs batch](https://docs.wickra.org/Streaming-vs-Batch),
|
||||
@@ -135,7 +135,7 @@ python -m benchmarks.compare_libraries
|
||||
|
||||
## 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
|
||||
semantics tests. Each has a per-indicator deep dive (formula, parameters,
|
||||
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 |
|
||||
| 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 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) |
|
||||
|
||||
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/
|
||||
├── 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-data/ CSV reader, tick aggregator, live exchange feeds
|
||||
├── 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', () => {
|
||||
// OI +10% while price flat -> divergence +0.1.
|
||||
const div = new wickra.OIPriceDivergence(1);
|
||||
|
||||
Vendored
+126
@@ -3253,6 +3253,132 @@ export declare class AdvanceDecline {
|
||||
isReady(): boolean
|
||||
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 declare class SharpeRatio {
|
||||
constructor(period: number, riskFree: number)
|
||||
|
||||
+15
-1
@@ -310,7 +310,7 @@ if (!nativeBinding) {
|
||||
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.SMA = SMA
|
||||
@@ -618,6 +618,20 @@ module.exports.LiquidationFeatures = LiquidationFeatures
|
||||
module.exports.TermStructureBasis = TermStructureBasis
|
||||
module.exports.CalendarSpread = CalendarSpread
|
||||
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.SortinoRatio = SortinoRatio
|
||||
module.exports.CalmarRatio = CalmarRatio
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -352,6 +352,20 @@ from ._wickra import (
|
||||
TermStructureBasis,
|
||||
CalendarSpread,
|
||||
# Market Breadth
|
||||
TickIndex,
|
||||
AbsoluteBreadthIndex,
|
||||
CumulativeVolumeIndex,
|
||||
BullishPercentIndex,
|
||||
UpDownVolumeRatio,
|
||||
PercentAboveMa,
|
||||
HighLowIndex,
|
||||
NewHighsNewLows,
|
||||
BreadthThrust,
|
||||
Trin,
|
||||
McClellanSummationIndex,
|
||||
McClellanOscillator,
|
||||
AdVolumeLine,
|
||||
AdvanceDeclineRatio,
|
||||
AdvanceDecline,
|
||||
# Risk / Performance
|
||||
SharpeRatio,
|
||||
@@ -702,6 +716,20 @@ __all__ = [
|
||||
"TermStructureBasis",
|
||||
"CalendarSpread",
|
||||
# Market Breadth
|
||||
"TickIndex",
|
||||
"AbsoluteBreadthIndex",
|
||||
"CumulativeVolumeIndex",
|
||||
"BullishPercentIndex",
|
||||
"UpDownVolumeRatio",
|
||||
"PercentAboveMa",
|
||||
"HighLowIndex",
|
||||
"NewHighsNewLows",
|
||||
"BreadthThrust",
|
||||
"Trin",
|
||||
"McClellanSummationIndex",
|
||||
"McClellanOscillator",
|
||||
"AdVolumeLine",
|
||||
"AdvanceDeclineRatio",
|
||||
"AdvanceDecline",
|
||||
# Risk / Performance
|
||||
"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])
|
||||
|
||||
|
||||
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():
|
||||
n = 40
|
||||
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)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
# Proper nouns that appear in indicator documentation. They are real names,
|
||||
# not code identifiers, so `clippy::doc_markdown` must not demand backticks.
|
||||
# `..` 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
|
||||
//! need — a signed price `change` (whose sign classifies the symbol as
|
||||
//! advancing, declining or unchanged), the period `volume`, and the
|
||||
//! `new_high` / `new_low` extreme flags — so the indicators stay stateless per
|
||||
//! tick and never have to track per-symbol history.
|
||||
//! advancing, declining or unchanged), the period `volume`, the
|
||||
//! `new_high` / `new_low` extreme flags, and the `above_ma` / `on_buy_signal`
|
||||
//! state flags — so the indicators stay stateless per tick and never have to
|
||||
//! track per-symbol history.
|
||||
//!
|
||||
//! [`DerivativesTick`]: crate::DerivativesTick
|
||||
//! [`OrderBook`]: crate::OrderBook
|
||||
@@ -28,9 +29,16 @@ use crate::error::{Error, Result};
|
||||
/// - `volume` is finite and non-negative.
|
||||
///
|
||||
/// `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]
|
||||
#[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 {
|
||||
/// Price change versus the previous close. Sign classifies the symbol:
|
||||
/// positive is advancing, negative is declining, zero is unchanged.
|
||||
@@ -41,10 +49,17 @@ pub struct Member {
|
||||
pub new_high: bool,
|
||||
/// Whether the symbol printed a new period low.
|
||||
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 {
|
||||
/// 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
|
||||
/// [`CrossSection::new`] when the member is placed into a tick; this
|
||||
@@ -57,6 +72,37 @@ impl Member {
|
||||
volume,
|
||||
new_high,
|
||||
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 {
|
||||
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)]
|
||||
@@ -223,4 +319,69 @@ mod tests {
|
||||
assert_eq!(cs.advancers(), 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.
|
||||
|
||||
mod abandoned_baby;
|
||||
mod absolute_breadth_index;
|
||||
mod acceleration_bands;
|
||||
mod accelerator_oscillator;
|
||||
mod ad_oscillator;
|
||||
mod ad_volume_line;
|
||||
mod adaptive_cycle;
|
||||
mod adl;
|
||||
mod advance_block;
|
||||
mod advance_decline;
|
||||
mod advance_decline_ratio;
|
||||
mod adx;
|
||||
mod adxr;
|
||||
mod alligator;
|
||||
@@ -36,7 +39,9 @@ mod beta;
|
||||
mod beta_neutral_spread;
|
||||
mod bollinger;
|
||||
mod bollinger_bandwidth;
|
||||
mod breadth_thrust;
|
||||
mod breakaway;
|
||||
mod bullish_percent_index;
|
||||
mod calendar_spread;
|
||||
mod calmar_ratio;
|
||||
mod camarilla_pivots;
|
||||
@@ -59,6 +64,7 @@ mod conditional_value_at_risk;
|
||||
mod connors_rsi;
|
||||
mod coppock;
|
||||
mod counterattack;
|
||||
mod cumulative_volume_index;
|
||||
mod cvd;
|
||||
mod cybernetic_cycle;
|
||||
mod decycler;
|
||||
@@ -109,6 +115,7 @@ mod hammer;
|
||||
mod hanging_man;
|
||||
mod harami;
|
||||
mod heikin_ashi;
|
||||
mod high_low_index;
|
||||
mod high_wave;
|
||||
mod hikkake;
|
||||
mod hikkake_modified;
|
||||
@@ -166,6 +173,8 @@ mod mass_index;
|
||||
mod mat_hold;
|
||||
mod matching_low;
|
||||
mod max_drawdown;
|
||||
mod mcclellan_oscillator;
|
||||
mod mcclellan_summation_index;
|
||||
mod mcginley_dynamic;
|
||||
mod median_absolute_deviation;
|
||||
mod median_price;
|
||||
@@ -179,6 +188,7 @@ mod mom;
|
||||
mod morning_doji_star;
|
||||
mod morning_evening_star;
|
||||
mod natr;
|
||||
mod new_highs_new_lows;
|
||||
mod nvi;
|
||||
mod ob_imbalance_full;
|
||||
mod ob_imbalance_top1;
|
||||
@@ -197,6 +207,7 @@ mod pair_spread_zscore;
|
||||
mod pairwise_beta;
|
||||
mod parkinson;
|
||||
mod pearson_correlation;
|
||||
mod percent_above_ma;
|
||||
mod percent_b;
|
||||
mod percentage_trailing_stop;
|
||||
mod pgo;
|
||||
@@ -282,11 +293,13 @@ mod three_outside;
|
||||
mod three_soldiers_or_crows;
|
||||
mod three_stars_in_south;
|
||||
mod thrusting;
|
||||
mod tick_index;
|
||||
mod tii;
|
||||
mod tpo_profile;
|
||||
mod trade_imbalance;
|
||||
mod treynor_ratio;
|
||||
mod trima;
|
||||
mod trin;
|
||||
mod trix;
|
||||
mod true_range;
|
||||
mod tsf;
|
||||
@@ -299,6 +312,7 @@ mod typical_price;
|
||||
mod ulcer_index;
|
||||
mod ultimate_oscillator;
|
||||
mod unique_three_river;
|
||||
mod up_down_volume_ratio;
|
||||
mod upside_gap_three_methods;
|
||||
mod upside_gap_two_crows;
|
||||
mod value_area;
|
||||
@@ -330,13 +344,16 @@ mod zig_zag;
|
||||
mod zlema;
|
||||
|
||||
pub use abandoned_baby::AbandonedBaby;
|
||||
pub use absolute_breadth_index::AbsoluteBreadthIndex;
|
||||
pub use acceleration_bands::{AccelerationBands, AccelerationBandsOutput};
|
||||
pub use accelerator_oscillator::AcceleratorOscillator;
|
||||
pub use ad_oscillator::AdOscillator;
|
||||
pub use ad_volume_line::AdVolumeLine;
|
||||
pub use adaptive_cycle::AdaptiveCycle;
|
||||
pub use adl::Adl;
|
||||
pub use advance_block::AdvanceBlock;
|
||||
pub use advance_decline::AdvanceDecline;
|
||||
pub use advance_decline_ratio::AdvanceDeclineRatio;
|
||||
pub use adx::{Adx, AdxOutput};
|
||||
pub use adxr::Adxr;
|
||||
pub use alligator::{Alligator, AlligatorOutput};
|
||||
@@ -361,7 +378,9 @@ pub use beta::Beta;
|
||||
pub use beta_neutral_spread::BetaNeutralSpread;
|
||||
pub use bollinger::{BollingerBands, BollingerOutput};
|
||||
pub use bollinger_bandwidth::BollingerBandwidth;
|
||||
pub use breadth_thrust::BreadthThrust;
|
||||
pub use breakaway::Breakaway;
|
||||
pub use bullish_percent_index::BullishPercentIndex;
|
||||
pub use calendar_spread::CalendarSpread;
|
||||
pub use calmar_ratio::CalmarRatio;
|
||||
pub use camarilla_pivots::{Camarilla, CamarillaPivotsOutput};
|
||||
@@ -384,6 +403,7 @@ pub use conditional_value_at_risk::ConditionalValueAtRisk;
|
||||
pub use connors_rsi::ConnorsRsi;
|
||||
pub use coppock::Coppock;
|
||||
pub use counterattack::Counterattack;
|
||||
pub use cumulative_volume_index::CumulativeVolumeIndex;
|
||||
pub use cvd::CumulativeVolumeDelta;
|
||||
pub use cybernetic_cycle::CyberneticCycle;
|
||||
pub use decycler::Decycler;
|
||||
@@ -434,6 +454,7 @@ pub use hammer::Hammer;
|
||||
pub use hanging_man::HangingMan;
|
||||
pub use harami::Harami;
|
||||
pub use heikin_ashi::{HeikinAshi, HeikinAshiOutput};
|
||||
pub use high_low_index::HighLowIndex;
|
||||
pub use high_wave::HighWave;
|
||||
pub use hikkake::Hikkake;
|
||||
pub use hikkake_modified::HikkakeModified;
|
||||
@@ -491,6 +512,8 @@ pub use mass_index::MassIndex;
|
||||
pub use mat_hold::MatHold;
|
||||
pub use matching_low::MatchingLow;
|
||||
pub use max_drawdown::MaxDrawdown;
|
||||
pub use mcclellan_oscillator::McClellanOscillator;
|
||||
pub use mcclellan_summation_index::McClellanSummationIndex;
|
||||
pub use mcginley_dynamic::McGinleyDynamic;
|
||||
pub use median_absolute_deviation::MedianAbsoluteDeviation;
|
||||
pub use median_price::MedianPrice;
|
||||
@@ -504,6 +527,7 @@ pub use mom::Mom;
|
||||
pub use morning_doji_star::MorningDojiStar;
|
||||
pub use morning_evening_star::MorningEveningStar;
|
||||
pub use natr::Natr;
|
||||
pub use new_highs_new_lows::NewHighsNewLows;
|
||||
pub use nvi::Nvi;
|
||||
pub use ob_imbalance_full::OrderBookImbalanceFull;
|
||||
pub use ob_imbalance_top1::OrderBookImbalanceTop1;
|
||||
@@ -522,6 +546,7 @@ pub use pair_spread_zscore::PairSpreadZScore;
|
||||
pub use pairwise_beta::PairwiseBeta;
|
||||
pub use parkinson::ParkinsonVolatility;
|
||||
pub use pearson_correlation::PearsonCorrelation;
|
||||
pub use percent_above_ma::PercentAboveMa;
|
||||
pub use percent_b::PercentB;
|
||||
pub use percentage_trailing_stop::PercentageTrailingStop;
|
||||
pub use pgo::Pgo;
|
||||
@@ -607,11 +632,13 @@ pub use three_outside::ThreeOutside;
|
||||
pub use three_soldiers_or_crows::ThreeSoldiersOrCrows;
|
||||
pub use three_stars_in_south::ThreeStarsInSouth;
|
||||
pub use thrusting::Thrusting;
|
||||
pub use tick_index::TickIndex;
|
||||
pub use tii::Tii;
|
||||
pub use tpo_profile::{TpoProfile, TpoProfileOutput};
|
||||
pub use trade_imbalance::TradeImbalance;
|
||||
pub use treynor_ratio::TreynorRatio;
|
||||
pub use trima::Trima;
|
||||
pub use trin::Trin;
|
||||
pub use trix::Trix;
|
||||
pub use true_range::TrueRange;
|
||||
pub use tsf::Tsf;
|
||||
@@ -624,6 +651,7 @@ pub use typical_price::TypicalPrice;
|
||||
pub use ulcer_index::UlcerIndex;
|
||||
pub use ultimate_oscillator::UltimateOscillator;
|
||||
pub use unique_three_river::UniqueThreeRiver;
|
||||
pub use up_down_volume_ratio::UpDownVolumeRatio;
|
||||
pub use upside_gap_three_methods::UpsideGapThreeMethods;
|
||||
pub use upside_gap_two_crows::UpsideGapTwoCrows;
|
||||
pub use value_area::{ValueArea, ValueAreaOutput};
|
||||
@@ -1070,7 +1098,26 @@ pub const FAMILIES: &[(&str, &[&str])] = &[
|
||||
"Alt-Chart Bars",
|
||||
&["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)]
|
||||
@@ -1099,6 +1146,6 @@ mod family_tests {
|
||||
// the actual indicator count is the early-warning signal that an
|
||||
// indicator was added without being assigned a family.
|
||||
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 error::{Error, Result};
|
||||
pub use indicators::{
|
||||
AbandonedBaby, AccelerationBands, AccelerationBandsOutput, AcceleratorOscillator, AdOscillator,
|
||||
AdaptiveCycle, Adl, AdvanceBlock, AdvanceDecline, Adx, AdxOutput, Adxr, Alligator,
|
||||
AlligatorOutput, Alma, Alpha, AnchoredRsi, AnchoredVwap, Apo, Aroon, AroonOscillator,
|
||||
AroonOutput, Atr, AtrBands, AtrBandsOutput, AtrTrailingStop, Autocorrelation, AverageDrawdown,
|
||||
AvgPrice, AwesomeOscillator, AwesomeOscillatorHistogram, BalanceOfPower, BeltHold, Beta,
|
||||
BetaNeutralSpread, BollingerBands, BollingerBandwidth, BollingerOutput, Breakaway,
|
||||
AbandonedBaby, AbsoluteBreadthIndex, AccelerationBands, AccelerationBandsOutput,
|
||||
AcceleratorOscillator, AdOscillator, AdVolumeLine, AdaptiveCycle, Adl, AdvanceBlock,
|
||||
AdvanceDecline, AdvanceDeclineRatio, Adx, AdxOutput, Adxr, Alligator, AlligatorOutput, Alma,
|
||||
Alpha, AnchoredRsi, AnchoredVwap, Apo, Aroon, AroonOscillator, AroonOutput, Atr, AtrBands,
|
||||
AtrBandsOutput, AtrTrailingStop, Autocorrelation, AverageDrawdown, AvgPrice, AwesomeOscillator,
|
||||
AwesomeOscillatorHistogram, BalanceOfPower, BeltHold, Beta, BetaNeutralSpread, BollingerBands,
|
||||
BollingerBandwidth, BollingerOutput, BreadthThrust, Breakaway, BullishPercentIndex,
|
||||
CalendarSpread, CalmarRatio, Camarilla, CamarillaPivotsOutput, Cci, CenterOfGravity, Cfo,
|
||||
ChaikinMoneyFlow, ChaikinOscillator, ChaikinVolatility, ChandeKrollStop, ChandeKrollStopOutput,
|
||||
ChandelierExit, ChandelierExitOutput, ChoppinessIndex, ClassicPivots, ClassicPivotsOutput,
|
||||
ClosingMarubozu, Cmo, CoefficientOfVariation, Cointegration, CointegrationOutput,
|
||||
ConcealingBabySwallow, ConditionalValueAtRisk, ConnorsRsi, Coppock, Counterattack,
|
||||
CumulativeVolumeDelta, CyberneticCycle, Decycler, DecyclerOscillator, Dema, DemandIndex,
|
||||
DemarkPivots, DemarkPivotsOutput, DepthSlope, DetrendedStdDev, DistanceSsd, Doji, DojiStar,
|
||||
Donchian, DonchianOutput, DonchianStop, DonchianStopOutput, DoubleBollinger,
|
||||
CumulativeVolumeDelta, CumulativeVolumeIndex, CyberneticCycle, Decycler, DecyclerOscillator,
|
||||
Dema, DemandIndex, DemarkPivots, DemarkPivotsOutput, DepthSlope, DetrendedStdDev, DistanceSsd,
|
||||
Doji, DojiStar, Donchian, DonchianOutput, DonchianStop, DonchianStopOutput, DoubleBollinger,
|
||||
DoubleBollingerOutput, DownsideGapThreeMethods, Dpo, DragonflyDoji, DrawdownDuration, Dx,
|
||||
EaseOfMovement, EffectiveSpread, EhlersStochastic, ElderImpulse, Ema,
|
||||
EmpiricalModeDecomposition, Engulfing, EveningDojiStar, Evwma, FallingThreeMethods, Fama,
|
||||
@@ -76,47 +77,48 @@ pub use indicators::{
|
||||
ForceIndex, FractalChaosBands, FractalChaosBandsOutput, Frama, FundingBasis, FundingRate,
|
||||
FundingRateMean, FundingRateZScore, GainLossRatio, GapSideBySideWhite, GarmanKlassVolatility,
|
||||
GrangerCausality, GravestoneDoji, Hammer, HangingMan, Harami, HeikinAshi, HeikinAshiOutput,
|
||||
HiLoActivator, HighWave, Hikkake, HikkakeModified, HilbertDominantCycle, HistoricalVolatility,
|
||||
Hma, HomingPigeon, HtDcPhase, HtPhasor, HtPhasorOutput, HtTrendMode, HurstChannel,
|
||||
HurstChannelOutput, HurstExponent, Ichimoku, IchimokuOutput, IdenticalThreeCrows, InNeck,
|
||||
Inertia, InformationRatio, InitialBalance, InitialBalanceOutput, InstantaneousTrendline,
|
||||
InverseFisherTransform, InvertedHammer, Jma, KagiBars, KalmanHedgeRatio,
|
||||
KalmanHedgeRatioOutput, Kama, KellyCriterion, Keltner, KeltnerOutput, Kicking, KickingByLength,
|
||||
Kst, KstOutput, Kurtosis, Kvo, KylesLambda, LadderBottom, LaguerreRsi, LeadLagCrossCorrelation,
|
||||
LeadLagCrossCorrelationOutput, LinRegAngle, LinRegChannel, LinRegChannelOutput,
|
||||
LinRegIntercept, LinRegSlope, LinearRegression, LiquidationFeatures, LiquidationFeaturesOutput,
|
||||
LongLeggedDoji, LongLine, LongShortRatio, MaEnvelope, MaEnvelopeOutput, MacdExt, MacdFix,
|
||||
MacdIndicator, MacdOutput, Mama, MamaOutput, MarketFacilitationIndex, Marubozu, MassIndex,
|
||||
MatHold, MatchingLow, MaxDrawdown, McGinleyDynamic, MedianAbsoluteDeviation, MedianPrice, Mfi,
|
||||
Microprice, MidPoint, MidPrice, MinusDi, MinusDm, Mom, MorningDojiStar, MorningEveningStar,
|
||||
Natr, Nvi, OIPriceDivergence, OIWeighted, Obv, OmegaRatio, OnNeck, OpenInterestDelta,
|
||||
OpeningMarubozu, OpeningRange, OpeningRangeOutput, OrderBookImbalanceFull,
|
||||
OrderBookImbalanceTop1, OrderBookImbalanceTopN, OuHalfLife, PainIndex, PairSpreadZScore,
|
||||
PairwiseBeta, ParkinsonVolatility, PearsonCorrelation, PercentB, PercentageTrailingStop, Pgo,
|
||||
PiercingDarkCloud, PlusDi, PlusDm, Pmo, PointAndFigureBars, Ppo, ProfitFactor, Psar, Pvi,
|
||||
QuotedSpread, RSquared, RealizedSpread, RecoveryFactor, RelativeStrengthAB,
|
||||
RelativeStrengthOutput, RenkoBars, RenkoTrailingStop, RickshawMan, RisingThreeMethods, Roc,
|
||||
Rocp, Rocr, Rocr100, RogersSatchellVolatility, RollingCorrelation, RollingCovariance,
|
||||
RollingVwap, RoofingFilter, Rsi, Rvi, RviVolatility, Rwi, RwiOutput, SarExt, SeparatingLines,
|
||||
SharpeRatio, ShootingStar, ShortLine, SignedVolume, SineWave, Skewness, Sma, Smi, Smma,
|
||||
SortinoRatio, SpearmanCorrelation, SpinningTop, SpreadBollingerBands,
|
||||
SpreadBollingerBandsOutput, SpreadHurst, StalledPattern, StandardError, StandardErrorBands,
|
||||
StandardErrorBandsOutput, StarcBands, StarcBandsOutput, Stc, StdDev, StepTrailingStop,
|
||||
StickSandwich, StochRsi, Stochastic, StochasticOutput, SuperSmoother, SuperTrend,
|
||||
SuperTrendOutput, TakerBuySellRatio, Takuri, TasukiGap, TdCombo, TdCountdown, TdDeMarker,
|
||||
TdDifferential, TdLines, TdLinesOutput, TdOpen, TdPressure, TdRangeProjection,
|
||||
HiLoActivator, HighLowIndex, HighWave, Hikkake, HikkakeModified, HilbertDominantCycle,
|
||||
HistoricalVolatility, Hma, HomingPigeon, HtDcPhase, HtPhasor, HtPhasorOutput, HtTrendMode,
|
||||
HurstChannel, HurstChannelOutput, HurstExponent, Ichimoku, IchimokuOutput, IdenticalThreeCrows,
|
||||
InNeck, Inertia, InformationRatio, InitialBalance, InitialBalanceOutput,
|
||||
InstantaneousTrendline, InverseFisherTransform, InvertedHammer, Jma, KagiBars,
|
||||
KalmanHedgeRatio, KalmanHedgeRatioOutput, Kama, KellyCriterion, Keltner, KeltnerOutput,
|
||||
Kicking, KickingByLength, Kst, KstOutput, Kurtosis, Kvo, KylesLambda, LadderBottom,
|
||||
LaguerreRsi, LeadLagCrossCorrelation, LeadLagCrossCorrelationOutput, LinRegAngle,
|
||||
LinRegChannel, LinRegChannelOutput, LinRegIntercept, LinRegSlope, LinearRegression,
|
||||
LiquidationFeatures, LiquidationFeaturesOutput, LongLeggedDoji, LongLine, LongShortRatio,
|
||||
MaEnvelope, MaEnvelopeOutput, MacdExt, MacdFix, MacdIndicator, MacdOutput, Mama, MamaOutput,
|
||||
MarketFacilitationIndex, Marubozu, MassIndex, MatHold, MatchingLow, MaxDrawdown,
|
||||
McClellanOscillator, McClellanSummationIndex, McGinleyDynamic, MedianAbsoluteDeviation,
|
||||
MedianPrice, Mfi, Microprice, MidPoint, MidPrice, MinusDi, MinusDm, Mom, MorningDojiStar,
|
||||
MorningEveningStar, Natr, NewHighsNewLows, Nvi, OIPriceDivergence, OIWeighted, Obv, OmegaRatio,
|
||||
OnNeck, OpenInterestDelta, OpeningMarubozu, OpeningRange, OpeningRangeOutput,
|
||||
OrderBookImbalanceFull, OrderBookImbalanceTop1, OrderBookImbalanceTopN, OuHalfLife, PainIndex,
|
||||
PairSpreadZScore, PairwiseBeta, ParkinsonVolatility, PearsonCorrelation, PercentAboveMa,
|
||||
PercentB, PercentageTrailingStop, Pgo, PiercingDarkCloud, PlusDi, PlusDm, Pmo,
|
||||
PointAndFigureBars, Ppo, ProfitFactor, Psar, Pvi, QuotedSpread, RSquared, RealizedSpread,
|
||||
RecoveryFactor, RelativeStrengthAB, RelativeStrengthOutput, RenkoBars, RenkoTrailingStop,
|
||||
RickshawMan, RisingThreeMethods, Roc, Rocp, Rocr, Rocr100, RogersSatchellVolatility,
|
||||
RollingCorrelation, RollingCovariance, RollingVwap, RoofingFilter, Rsi, Rvi, RviVolatility,
|
||||
Rwi, RwiOutput, SarExt, SeparatingLines, SharpeRatio, ShootingStar, ShortLine, SignedVolume,
|
||||
SineWave, Skewness, Sma, Smi, Smma, SortinoRatio, SpearmanCorrelation, SpinningTop,
|
||||
SpreadBollingerBands, SpreadBollingerBandsOutput, SpreadHurst, StalledPattern, StandardError,
|
||||
StandardErrorBands, StandardErrorBandsOutput, StarcBands, StarcBandsOutput, Stc, StdDev,
|
||||
StepTrailingStop, StickSandwich, StochRsi, Stochastic, StochasticOutput, SuperSmoother,
|
||||
SuperTrend, SuperTrendOutput, TakerBuySellRatio, Takuri, TasukiGap, TdCombo, TdCountdown,
|
||||
TdDeMarker, TdDifferential, TdLines, TdLinesOutput, TdOpen, TdPressure, TdRangeProjection,
|
||||
TdRangeProjectionOutput, TdRei, TdRiskLevel, TdRiskLevelOutput, TdSequential,
|
||||
TdSequentialOutput, TdSetup, Tema, TermStructureBasis, ThreeInside, ThreeLineStrike,
|
||||
ThreeOutside, ThreeSoldiersOrCrows, ThreeStarsInSouth, Thrusting, Tii, TpoProfile,
|
||||
TpoProfileOutput, TradeImbalance, TreynorRatio, Trima, Trix, TrueRange, Tsf, Tsi, Tsv,
|
||||
ThreeOutside, ThreeSoldiersOrCrows, ThreeStarsInSouth, Thrusting, TickIndex, Tii, TpoProfile,
|
||||
TpoProfileOutput, TradeImbalance, TreynorRatio, Trima, Trin, Trix, TrueRange, Tsf, Tsi, Tsv,
|
||||
TtmSqueeze, TtmSqueezeOutput, Tweezer, TwoCrows, TypicalPrice, UlcerIndex, UltimateOscillator,
|
||||
UniqueThreeRiver, UpsideGapThreeMethods, UpsideGapTwoCrows, ValueArea, ValueAreaOutput,
|
||||
ValueAtRisk, Variance, VarianceRatio, VerticalHorizontalFilter, Vidya, VoltyStop,
|
||||
VolumeOscillator, VolumePriceTrend, VolumeProfile, VolumeProfileOutput, Vortex, VortexOutput,
|
||||
Vwap, VwapStdDevBands, VwapStdDevBandsOutput, Vwma, Vzo, WaveTrend, WaveTrendOutput,
|
||||
WeightedClose, WilliamsFractals, WilliamsFractalsOutput, WilliamsR, Wma, WoodiePivots,
|
||||
WoodiePivotsOutput, YangZhangVolatility, YoyoExit, ZScore, ZeroLagMacd, ZeroLagMacdOutput,
|
||||
ZigZag, ZigZagOutput, Zlema, FAMILIES, T3,
|
||||
UniqueThreeRiver, UpDownVolumeRatio, UpsideGapThreeMethods, UpsideGapTwoCrows, ValueArea,
|
||||
ValueAreaOutput, ValueAtRisk, Variance, VarianceRatio, VerticalHorizontalFilter, Vidya,
|
||||
VoltyStop, VolumeOscillator, VolumePriceTrend, VolumeProfile, VolumeProfileOutput, Vortex,
|
||||
VortexOutput, Vwap, VwapStdDevBands, VwapStdDevBandsOutput, Vwma, Vzo, WaveTrend,
|
||||
WaveTrendOutput, WeightedClose, WilliamsFractals, WilliamsFractalsOutput, WilliamsR, Wma,
|
||||
WoodiePivots, WoodiePivotsOutput, YangZhangVolatility, YoyoExit, ZScore, ZeroLagMacd,
|
||||
ZeroLagMacdOutput, ZigZag, ZigZagOutput, Zlema, FAMILIES, T3,
|
||||
};
|
||||
// `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
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@ That includes:
|
||||
[Python](https://docs.wickra.org/Quickstart-Python),
|
||||
[Node](https://docs.wickra.org/Quickstart-Node), and
|
||||
[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 &
|
||||
Directional, Price Oscillators, Volatility & Bands, Bands & Channels,
|
||||
Trailing Stops, Volume, Price Statistics, Ehlers / Cycle DSP, Pivots &
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
//! streaming or batched.
|
||||
|
||||
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)]
|
||||
fn drive<I>(make: impl Fn() -> I, sections: &[CrossSection])
|
||||
@@ -44,4 +44,18 @@ fuzz_target!(|data: &[u8]| {
|
||||
.collect();
|
||||
|
||||
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