Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9a98e9bf55 | |||
| d4b3f9dbd1 | |||
| f37eedd44e | |||
| 93097db482 |
+11
-1
@@ -7,6 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [0.4.5] - 2026-06-02
|
||||
|
||||
### Added
|
||||
|
||||
- **Anchored RSI** — a cumulative Relative Strength Index whose averaging begins at a runtime-chosen anchor bar (`set_anchor`), the momentum counterpart to Anchored VWAP. Every up- and down-move since the anchor is weighted equally, so it reports the RSI of the entire move since the anchor point. Scalar input, Momentum Oscillators family; available in Rust, Python, Node and WASM.
|
||||
- **Volume Profile** — the full per-bin volume distribution over a rolling window, exposing the raw histogram (price bounds plus per-bin volume) that Value Area reduces to POC/VAH/VAL. Market Profile family; candle input, available in Rust, Python, Node and WASM.
|
||||
- **TPO Profile** — the Time-Price-Opportunity (market-profile letter) distribution: a volume-agnostic count of how many periods traded at each price level over a rolling window. Market Profile family; candle input, available in Rust, Python, Node and WASM.
|
||||
- **Alt-Chart Bars** — a new `BarBuilder` trait and family of price-driven chart constructors that emit a variable number of completed bars per candle (so they are deliberately not `Indicator`s): **Renko** (fixed box-size bricks with the 2-box reversal rule), **Kagi** (reversal-amount line segments), and **Point & Figure** (box-size X/O columns with an N-box reversal). Available in Rust, Python, Node and WASM.
|
||||
|
||||
## [0.4.4] - 2026-06-02
|
||||
|
||||
### Added
|
||||
@@ -1075,7 +1084,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
optional Binance live feed.
|
||||
- Bindings for Python, Node.js, and WebAssembly.
|
||||
|
||||
[Unreleased]: https://github.com/wickra-lib/wickra/compare/v0.4.4...HEAD
|
||||
[Unreleased]: https://github.com/wickra-lib/wickra/compare/v0.4.5...HEAD
|
||||
[0.4.5]: https://github.com/wickra-lib/wickra/compare/v0.4.4...v0.4.5
|
||||
[0.4.4]: https://github.com/wickra-lib/wickra/compare/v0.4.3...v0.4.4
|
||||
[0.4.3]: https://github.com/wickra-lib/wickra/compare/v0.4.2...v0.4.3
|
||||
[0.4.2]: https://github.com/wickra-lib/wickra/compare/v0.4.1...v0.4.2
|
||||
|
||||
Generated
+6
-6
@@ -1867,7 +1867,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wickra"
|
||||
version = "0.4.4"
|
||||
version = "0.4.5"
|
||||
dependencies = [
|
||||
"approx",
|
||||
"criterion",
|
||||
@@ -1878,7 +1878,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wickra-core"
|
||||
version = "0.4.4"
|
||||
version = "0.4.5"
|
||||
dependencies = [
|
||||
"approx",
|
||||
"proptest",
|
||||
@@ -1888,7 +1888,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wickra-data"
|
||||
version = "0.4.4"
|
||||
version = "0.4.5"
|
||||
dependencies = [
|
||||
"approx",
|
||||
"csv",
|
||||
@@ -1915,7 +1915,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wickra-node"
|
||||
version = "0.4.4"
|
||||
version = "0.4.5"
|
||||
dependencies = [
|
||||
"napi",
|
||||
"napi-build",
|
||||
@@ -1925,7 +1925,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wickra-python"
|
||||
version = "0.4.4"
|
||||
version = "0.4.5"
|
||||
dependencies = [
|
||||
"numpy",
|
||||
"pyo3",
|
||||
@@ -1934,7 +1934,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wickra-wasm"
|
||||
version = "0.4.4"
|
||||
version = "0.4.5"
|
||||
dependencies = [
|
||||
"console_error_panic_hook",
|
||||
"js-sys",
|
||||
|
||||
+2
-2
@@ -12,7 +12,7 @@ members = [
|
||||
exclude = ["fuzz"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.4.4"
|
||||
version = "0.4.5"
|
||||
authors = ["kingchenc <support@wickra.org>"]
|
||||
edition = "2021"
|
||||
rust-version = "1.86"
|
||||
@@ -24,7 +24,7 @@ keywords = ["finance", "trading", "indicators", "technical-analysis", "ta"]
|
||||
categories = ["finance", "mathematics", "science"]
|
||||
|
||||
[workspace.dependencies]
|
||||
wickra-core = { path = "crates/wickra-core", version = "0.4.4" }
|
||||
wickra-core = { path = "crates/wickra-core", version = "0.4.5" }
|
||||
|
||||
thiserror = "2"
|
||||
rayon = "1.10"
|
||||
|
||||
@@ -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=289" 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=295" 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 289 indicators; start at the
|
||||
every one of the 295 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
|
||||
|
||||
289 streaming-first indicators across eighteen families. Every one passes the
|
||||
295 streaming-first indicators across nineteen 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).
|
||||
@@ -143,7 +143,7 @@ warmup) at [docs.wickra.org](https://docs.wickra.org/Indicators-Overview).
|
||||
| Family | Indicators |
|
||||
|--------|-----------|
|
||||
| Moving Averages | SMA, EMA, WMA, DEMA, TEMA, HMA, KAMA, SMMA, TRIMA, ZLEMA, T3, VWMA, ALMA, McGinley Dynamic, FRAMA, VIDYA, JMA, Alligator, EVWMA |
|
||||
| Momentum Oscillators | RSI (Wilder), Stochastic, CCI, ROC, Williams %R, MFI, Awesome Oscillator, MOM, CMO, TSI, PMO, StochRSI, Ultimate Oscillator, RVI, PGO, KST, SMI, Laguerre RSI, Connors RSI, Inertia |
|
||||
| Momentum Oscillators | RSI (Wilder), Anchored RSI, Stochastic, CCI, ROC, Williams %R, MFI, Awesome Oscillator, MOM, CMO, TSI, PMO, StochRSI, Ultimate Oscillator, RVI, PGO, KST, SMI, Laguerre RSI, Connors RSI, Inertia |
|
||||
| Trend & Directional | MACD, ADX (+DI/-DI), ADXR, Aroon, TRIX, Aroon Oscillator, Vortex, Random Walk Index, Trend Intensity Index, Wave Trend Oscillator, Mass Index, Choppiness Index, Vertical Horizontal Filter |
|
||||
| Price Oscillators | PPO, DPO, Coppock, Accelerator Oscillator, Balance of Power, APO, AO Histogram, CFO, Zero-Lag MACD, Elder Impulse, STC |
|
||||
| Volatility & Bands | ATR, Bollinger Bands, Keltner Channels, Donchian Channels, NATR, StdDev, Ulcer Index, Historical Volatility, Bollinger Bandwidth, %B, True Range, Chaikin Volatility, RVI (Relative Volatility Index), Parkinson Volatility, Garman-Klass Volatility, Rogers-Satchell Volatility, Yang-Zhang Volatility |
|
||||
@@ -155,10 +155,11 @@ warmup) at [docs.wickra.org](https://docs.wickra.org/Indicators-Overview).
|
||||
| Pivots & S/R | Classic Pivots, Fibonacci Pivots, Camarilla, Woodie Pivots, DeMark Pivots, Williams Fractals, ZigZag |
|
||||
| DeMark | TD Setup, TD Sequential, TD DeMarker, TD REI, TD Pressure, TD Combo, TD Countdown, TD Lines, TD Range Projection, TD Differential, TD Open, TD Risk Level |
|
||||
| Ichimoku & Charts | Ichimoku Kinko Hyo (Tenkan, Kijun, Senkou A/B, Chikou), Heikin-Ashi |
|
||||
| Alt-Chart Bars | Renko (box-size bricks), Kagi (reversal-amount lines), Point & Figure (X/O columns) |
|
||||
| Candlestick Patterns | Doji, Hammer, Inverted Hammer, Hanging Man, Shooting Star, Engulfing, Harami, Morning/Evening Star, Three White Soldiers/Black Crows, Piercing Line/Dark Cloud Cover, Marubozu, Tweezer, Spinning Top, Three Inside Up/Down, Three Outside Up/Down, Two Crows, Upside Gap Two Crows, Identical Three Crows, Three Line Strike, Three Stars in the South, Abandoned Baby, Advance Block, Belt-hold, Breakaway, Counterattack, Doji Star, Dragonfly Doji, Gravestone Doji, Long-Legged Doji, Rickshaw Man, Evening Doji Star, Morning Doji Star, Gap Side-by-Side White, High-Wave, Hikkake, Modified Hikkake, Homing Pigeon, On-Neck, In-Neck, Thrusting, Separating Lines, Kicking, Kicking by Length, Ladder Bottom, Mat Hold, Matching Low, Long Line, Short Line, Rising Three Methods, Falling Three Methods, Upside Gap Three Methods, Downside Gap Three Methods, Stalled Pattern, Stick Sandwich, Takuri, Closing Marubozu, Opening Marubozu, Tasuki Gap, Unique Three River, Concealing Baby Swallow |
|
||||
| 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), Initial Balance, Opening Range |
|
||||
| Market Profile | Value Area (POC / VAH / VAL), Volume Profile (histogram), TPO Profile, Initial Balance, Opening Range |
|
||||
| Risk / Performance | Sharpe Ratio, Sortino Ratio, Calmar Ratio, Omega Ratio, Max Drawdown, Average Drawdown, Drawdown Duration, Pain Index, Value at Risk, Conditional Value at Risk (CVaR), Profit Factor, Gain/Loss Ratio, Recovery Factor, Kelly Criterion, Treynor Ratio, Information Ratio, Alpha (Jensen) |
|
||||
|
||||
Every candlestick pattern emits a signed per-bar value — `+1.0` bullish,
|
||||
@@ -238,7 +239,7 @@ A Python live-trading example using the public `websockets` package lives at
|
||||
```
|
||||
wickra/
|
||||
├── crates/
|
||||
│ ├── wickra-core/ core engine + all 289 indicators
|
||||
│ ├── wickra-core/ core engine + all 295 indicators
|
||||
│ ├── wickra/ top-level facade crate (publishes on crates.io) + benches/
|
||||
│ └── wickra-data/ CSV reader, tick aggregator, live exchange feeds
|
||||
├── bindings/
|
||||
|
||||
@@ -8,16 +8,25 @@ const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const wickra = require('..');
|
||||
|
||||
// Bar builders (Renko / Kagi / Point & Figure) implement the `BarBuilder`
|
||||
// contract, not `Indicator`: they emit a variable number of completed bars per
|
||||
// candle and have no fixed warmup or ready state. They expose update/batch/reset
|
||||
// but intentionally not isReady/warmupPeriod, so they are excluded from the
|
||||
// Indicator completeness contract below (their interface is covered by the
|
||||
// dedicated bar-builder tests).
|
||||
const BAR_BUILDERS = new Set(['RenkoBars', 'KagiBars', 'PointAndFigureBars']);
|
||||
|
||||
// An "indicator class" is an exported constructor whose prototype carries the
|
||||
// streaming `update` method. This excludes `version` (a plain function) and any
|
||||
// non-indicator export.
|
||||
// streaming `update` method. This excludes `version` (a plain function), the bar
|
||||
// builders, and any non-indicator export.
|
||||
function indicatorClasses() {
|
||||
return Object.keys(wickra).filter((name) => {
|
||||
const value = wickra[name];
|
||||
return (
|
||||
typeof value === 'function' &&
|
||||
value.prototype &&
|
||||
typeof value.prototype.update === 'function'
|
||||
typeof value.prototype.update === 'function' &&
|
||||
!BAR_BUILDERS.has(name)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ const scalarFactories = {
|
||||
EMA: () => new wickra.EMA(14),
|
||||
WMA: () => new wickra.WMA(14),
|
||||
RSI: () => new wickra.RSI(14),
|
||||
AnchoredRSI: () => new wickra.AnchoredRSI(),
|
||||
DEMA: () => new wickra.DEMA(10),
|
||||
TEMA: () => new wickra.TEMA(10),
|
||||
HMA: () => new wickra.HMA(9),
|
||||
@@ -1245,3 +1246,63 @@ test('basis rejects bad input', () => {
|
||||
assert.throws(() => new wickra.TermStructureBasis().update(100, 0));
|
||||
assert.throws(() => new wickra.CalendarSpread().update(100, 0));
|
||||
});
|
||||
|
||||
test('VolumeProfile exposes the full histogram', () => {
|
||||
// bar0 single-print at 10 vol 100; bar1 spans 10..14 vol 80 over 4 bins.
|
||||
const vp = new wickra.VolumeProfile(2, 4);
|
||||
assert.equal(vp.update(10, 10, 100), null);
|
||||
const out = vp.update(14, 10, 80);
|
||||
assert.ok(out !== null);
|
||||
assert.ok(Math.abs(out.priceLow - 10) < 1e-9);
|
||||
assert.ok(Math.abs(out.priceHigh - 14) < 1e-9);
|
||||
assert.deepEqual(out.bins.length, 4);
|
||||
assert.ok(Math.abs(out.bins[0] - 120) < 1e-9);
|
||||
for (let i = 1; i < 4; i++) {
|
||||
assert.ok(Math.abs(out.bins[i] - 20) < 1e-9);
|
||||
}
|
||||
});
|
||||
|
||||
test('TpoProfile counts time at price, volume-agnostic', () => {
|
||||
// bar0 spans 10..14 (+1 each bin); bar1 spans 11..12 (+1 bins 1,2).
|
||||
const tpo = new wickra.TpoProfile(2, 4);
|
||||
assert.equal(tpo.update(14, 10), null);
|
||||
const out = tpo.update(12, 11);
|
||||
assert.ok(out !== null);
|
||||
assert.ok(Math.abs(out.priceLow - 10) < 1e-9);
|
||||
assert.ok(Math.abs(out.priceHigh - 14) < 1e-9);
|
||||
assert.deepEqual(out.counts, [1, 2, 2, 1]);
|
||||
});
|
||||
|
||||
test('RenkoBars prints aligned bricks and reverses on two boxes', () => {
|
||||
const r = new wickra.RenkoBars(1.0);
|
||||
assert.deepEqual(r.update(10), []); // seed
|
||||
const up = r.update(13);
|
||||
assert.equal(up.length, 3);
|
||||
assert.ok(Math.abs(up[0].open - 10) < 1e-9 && Math.abs(up[0].close - 11) < 1e-9);
|
||||
assert.ok(up.every((b) => b.direction === 1));
|
||||
const down = r.update(10);
|
||||
assert.equal(down.length, 2);
|
||||
assert.ok(down.every((b) => b.direction === -1));
|
||||
});
|
||||
|
||||
test('KagiBars closes a segment on a reversal', () => {
|
||||
const k = new wickra.KagiBars(2.0);
|
||||
k.update(10);
|
||||
k.update(11);
|
||||
k.update(15);
|
||||
const seg = k.update(12);
|
||||
assert.equal(seg.length, 1);
|
||||
assert.equal(seg[0].direction, 1);
|
||||
assert.ok(Math.abs(seg[0].start - 10) < 1e-9 && Math.abs(seg[0].end - 15) < 1e-9);
|
||||
});
|
||||
|
||||
test('PointAndFigureBars closes a column on a 3-box reversal', () => {
|
||||
const pnf = new wickra.PointAndFigureBars(1.0, 3);
|
||||
pnf.update(10);
|
||||
pnf.update(13);
|
||||
pnf.update(15);
|
||||
const col = pnf.update(12);
|
||||
assert.equal(col.length, 1);
|
||||
assert.equal(col[0].direction, 1);
|
||||
assert.ok(Math.abs(col[0].high - 15) < 1e-9 && Math.abs(col[0].low - 10) < 1e-9);
|
||||
});
|
||||
|
||||
Vendored
+78
@@ -270,6 +270,16 @@ export interface ValueAreaValue {
|
||||
vah: number
|
||||
val: number
|
||||
}
|
||||
export interface VolumeProfileValue {
|
||||
priceLow: number
|
||||
priceHigh: number
|
||||
bins: Array<number>
|
||||
}
|
||||
export interface TpoProfileValue {
|
||||
priceLow: number
|
||||
priceHigh: number
|
||||
counts: Array<number>
|
||||
}
|
||||
export interface InitialBalanceValue {
|
||||
high: number
|
||||
low: number
|
||||
@@ -300,6 +310,21 @@ export interface LiquidationFeaturesValue {
|
||||
total: number
|
||||
imbalance: number
|
||||
}
|
||||
export interface RenkoBrickValue {
|
||||
open: number
|
||||
close: number
|
||||
direction: number
|
||||
}
|
||||
export interface KagiSegmentValue {
|
||||
start: number
|
||||
end: number
|
||||
direction: number
|
||||
}
|
||||
export interface PnfColumnValue {
|
||||
direction: number
|
||||
high: number
|
||||
low: number
|
||||
}
|
||||
export type SmaNode = SMA
|
||||
export declare class SMA {
|
||||
constructor(period: number)
|
||||
@@ -1224,6 +1249,16 @@ export declare class WilliamsAD {
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type AnchoredRsiNode = AnchoredRSI
|
||||
export declare class AnchoredRSI {
|
||||
constructor()
|
||||
setAnchor(): void
|
||||
update(value: number): number | null
|
||||
batch(prices: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type AnchoredVwapNode = AnchoredVWAP
|
||||
export declare class AnchoredVWAP {
|
||||
constructor()
|
||||
@@ -2054,6 +2089,24 @@ export declare class ValueArea {
|
||||
update(high: number, low: number, volume: number): ValueAreaValue | null
|
||||
batch(high: Array<number>, low: Array<number>, volume: Array<number>): Array<number>
|
||||
}
|
||||
export type VolumeProfileNode = VolumeProfile
|
||||
export declare class VolumeProfile {
|
||||
constructor(period: number, binCount: number)
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
update(high: number, low: number, volume: number): VolumeProfileValue | null
|
||||
batch(high: Array<number>, low: Array<number>, volume: Array<number>): Array<number>
|
||||
}
|
||||
export type TpoProfileNode = TpoProfile
|
||||
export declare class TpoProfile {
|
||||
constructor(period: number, binCount: number)
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
update(high: number, low: number): TpoProfileValue | null
|
||||
batch(high: Array<number>, low: Array<number>): Array<number>
|
||||
}
|
||||
export type InitialBalanceNode = InitialBalance
|
||||
export declare class InitialBalance {
|
||||
constructor(period: number)
|
||||
@@ -2984,6 +3037,31 @@ export declare class InformationRatio {
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type RenkoBarsNode = RenkoBars
|
||||
export declare class RenkoBars {
|
||||
constructor(boxSize: number)
|
||||
update(close: number): Array<RenkoBrickValue>
|
||||
batch(close: Array<number>): Array<RenkoBrickValue>
|
||||
boxSize(): number
|
||||
reset(): void
|
||||
}
|
||||
export type KagiBarsNode = KagiBars
|
||||
export declare class KagiBars {
|
||||
constructor(reversal: number)
|
||||
update(close: number): Array<KagiSegmentValue>
|
||||
batch(close: Array<number>): Array<KagiSegmentValue>
|
||||
reversal(): number
|
||||
reset(): void
|
||||
}
|
||||
export type PointAndFigureBarsNode = PointAndFigureBars
|
||||
export declare class PointAndFigureBars {
|
||||
constructor(boxSize: number, reversal: number)
|
||||
update(close: number): Array<PnfColumnValue>
|
||||
batch(close: Array<number>): Array<PnfColumnValue>
|
||||
boxSize(): number
|
||||
reversal(): number
|
||||
reset(): void
|
||||
}
|
||||
export type AlphaNode = Alpha
|
||||
export declare class Alpha {
|
||||
constructor(period: number, riskFree: number)
|
||||
|
||||
@@ -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, Autocorrelation, HurstExponent, PearsonCorrelation, Beta, PairwiseBeta, SpearmanCorrelation, PairSpreadZScore, LeadLagCrossCorrelation, Cointegration, RelativeStrengthAB, MACD, BollingerBands, ATR, 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, 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, HilbertDominantCycle, AdaptiveCycle, SineWave, MAMA, FAMA, Ichimoku, HeikinAshi, ValueArea, 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, SharpeRatio, SortinoRatio, CalmarRatio, OmegaRatio, MaxDrawdown, AverageDrawdown, DrawdownDuration, PainIndex, ValueAtRisk, ConditionalValueAtRisk, ProfitFactor, GainLossRatio, RecoveryFactor, KellyCriterion, TreynorRatio, InformationRatio, 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, Autocorrelation, HurstExponent, PearsonCorrelation, Beta, PairwiseBeta, SpearmanCorrelation, PairSpreadZScore, LeadLagCrossCorrelation, Cointegration, RelativeStrengthAB, MACD, BollingerBands, ATR, 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, 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, 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
|
||||
@@ -410,6 +410,7 @@ module.exports.PVI = PVI
|
||||
module.exports.VolumeOscillator = VolumeOscillator
|
||||
module.exports.KVO = KVO
|
||||
module.exports.WilliamsAD = WilliamsAD
|
||||
module.exports.AnchoredRSI = AnchoredRSI
|
||||
module.exports.AnchoredVWAP = AnchoredVWAP
|
||||
module.exports.DemandIndex = DemandIndex
|
||||
module.exports.TSV = TSV
|
||||
@@ -498,6 +499,8 @@ module.exports.FAMA = FAMA
|
||||
module.exports.Ichimoku = Ichimoku
|
||||
module.exports.HeikinAshi = HeikinAshi
|
||||
module.exports.ValueArea = ValueArea
|
||||
module.exports.VolumeProfile = VolumeProfile
|
||||
module.exports.TpoProfile = TpoProfile
|
||||
module.exports.InitialBalance = InitialBalance
|
||||
module.exports.OpeningRange = OpeningRange
|
||||
module.exports.Doji = Doji
|
||||
@@ -601,4 +604,7 @@ module.exports.RecoveryFactor = RecoveryFactor
|
||||
module.exports.KellyCriterion = KellyCriterion
|
||||
module.exports.TreynorRatio = TreynorRatio
|
||||
module.exports.InformationRatio = InformationRatio
|
||||
module.exports.RenkoBars = RenkoBars
|
||||
module.exports.KagiBars = KagiBars
|
||||
module.exports.PointAndFigureBars = PointAndFigureBars
|
||||
module.exports.Alpha = Alpha
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "wickra-darwin-arm64",
|
||||
"version": "0.4.4",
|
||||
"version": "0.4.5",
|
||||
"description": "Native binding for wickra (macOS Apple Silicon). Installed automatically as an optional dependency of wickra on matching platforms.",
|
||||
"main": "wickra.darwin-arm64.node",
|
||||
"files": [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "wickra-darwin-x64",
|
||||
"version": "0.4.4",
|
||||
"version": "0.4.5",
|
||||
"description": "Native binding for wickra (macOS Intel). Installed automatically as an optional dependency of wickra on matching platforms.",
|
||||
"main": "wickra.darwin-x64.node",
|
||||
"files": [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "wickra-linux-arm64-gnu",
|
||||
"version": "0.4.4",
|
||||
"version": "0.4.5",
|
||||
"description": "Native binding for wickra (linux arm64 GNU). Installed automatically as an optional dependency of wickra on matching platforms.",
|
||||
"main": "wickra.linux-arm64-gnu.node",
|
||||
"files": [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "wickra-linux-x64-gnu",
|
||||
"version": "0.4.4",
|
||||
"version": "0.4.5",
|
||||
"description": "Native binding for wickra (linux x64 GNU). Installed automatically as an optional dependency of wickra on matching platforms.",
|
||||
"main": "wickra.linux-x64-gnu.node",
|
||||
"files": [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "wickra-win32-arm64-msvc",
|
||||
"version": "0.4.4",
|
||||
"version": "0.4.5",
|
||||
"description": "Native binding for wickra (Windows arm64 MSVC). Installed automatically as an optional dependency of wickra on matching platforms.",
|
||||
"main": "wickra.win32-arm64-msvc.node",
|
||||
"files": [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "wickra-win32-x64-msvc",
|
||||
"version": "0.4.4",
|
||||
"version": "0.4.5",
|
||||
"description": "Native binding for wickra (Windows x64 MSVC). Installed automatically as an optional dependency of wickra on matching platforms.",
|
||||
"main": "wickra.win32-x64-msvc.node",
|
||||
"files": [
|
||||
|
||||
Generated
+20
-20
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "wickra",
|
||||
"version": "0.4.4",
|
||||
"version": "0.4.5",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "wickra",
|
||||
"version": "0.4.4",
|
||||
"version": "0.4.5",
|
||||
"license": "PolyForm-Noncommercial-1.0.0",
|
||||
"devDependencies": {
|
||||
"@napi-rs/cli": "^2.18.0"
|
||||
@@ -15,12 +15,12 @@
|
||||
"node": ">= 18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"wickra-darwin-arm64": "0.4.4",
|
||||
"wickra-darwin-x64": "0.4.4",
|
||||
"wickra-linux-arm64-gnu": "0.4.4",
|
||||
"wickra-linux-x64-gnu": "0.4.4",
|
||||
"wickra-win32-arm64-msvc": "0.4.4",
|
||||
"wickra-win32-x64-msvc": "0.4.4"
|
||||
"wickra-darwin-arm64": "0.4.5",
|
||||
"wickra-darwin-x64": "0.4.5",
|
||||
"wickra-linux-arm64-gnu": "0.4.5",
|
||||
"wickra-linux-x64-gnu": "0.4.5",
|
||||
"wickra-win32-arm64-msvc": "0.4.5",
|
||||
"wickra-win32-x64-msvc": "0.4.5"
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/cli": {
|
||||
@@ -41,8 +41,8 @@
|
||||
}
|
||||
},
|
||||
"node_modules/wickra-darwin-arm64": {
|
||||
"version": "0.4.4",
|
||||
"resolved": "https://registry.npmjs.org/wickra-darwin-arm64/-/wickra-darwin-arm64-0.4.4.tgz",
|
||||
"version": "0.4.5",
|
||||
"resolved": "https://registry.npmjs.org/wickra-darwin-arm64/-/wickra-darwin-arm64-0.4.5.tgz",
|
||||
"integrity": "sha512-4eZiBR/yGUdr4nzhEUFy2i69XgNx64iI2ax/LPamsThgylC0KpHOZKK19QzJ2d9KbK4C8nMjME5FLuR+4GNEwQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
@@ -57,8 +57,8 @@
|
||||
}
|
||||
},
|
||||
"node_modules/wickra-darwin-x64": {
|
||||
"version": "0.4.4",
|
||||
"resolved": "https://registry.npmjs.org/wickra-darwin-x64/-/wickra-darwin-x64-0.4.4.tgz",
|
||||
"version": "0.4.5",
|
||||
"resolved": "https://registry.npmjs.org/wickra-darwin-x64/-/wickra-darwin-x64-0.4.5.tgz",
|
||||
"integrity": "sha512-6hf8zI3QPjTFp4zCpmgUwDvNtu6jHqNUHKD5e55POo0CgA52HkpyxSPtVm8TGTIZDI7kPjlbOdBM8CJ76mmXwA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
@@ -73,8 +73,8 @@
|
||||
}
|
||||
},
|
||||
"node_modules/wickra-linux-arm64-gnu": {
|
||||
"version": "0.4.4",
|
||||
"resolved": "https://registry.npmjs.org/wickra-linux-arm64-gnu/-/wickra-linux-arm64-gnu-0.4.4.tgz",
|
||||
"version": "0.4.5",
|
||||
"resolved": "https://registry.npmjs.org/wickra-linux-arm64-gnu/-/wickra-linux-arm64-gnu-0.4.5.tgz",
|
||||
"integrity": "sha512-kSe6y0xBMSiqdPLXNjwop5WZdHtvdBNKSEBCwZ4hFq33p4apW25/wrlzv9/oDuyD4kuPabJEhCCnFOplh58CUg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
@@ -89,8 +89,8 @@
|
||||
}
|
||||
},
|
||||
"node_modules/wickra-linux-x64-gnu": {
|
||||
"version": "0.4.4",
|
||||
"resolved": "https://registry.npmjs.org/wickra-linux-x64-gnu/-/wickra-linux-x64-gnu-0.4.4.tgz",
|
||||
"version": "0.4.5",
|
||||
"resolved": "https://registry.npmjs.org/wickra-linux-x64-gnu/-/wickra-linux-x64-gnu-0.4.5.tgz",
|
||||
"integrity": "sha512-tWBWS4qz7hxM4xnpFb59bhf6TaLwXq0Z3jEa/2l7r8PiHA94g8r8S53NRMiT+4yiL5hSWe/nUiC/YXdRrhEZ4g==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
@@ -105,8 +105,8 @@
|
||||
}
|
||||
},
|
||||
"node_modules/wickra-win32-arm64-msvc": {
|
||||
"version": "0.4.4",
|
||||
"resolved": "https://registry.npmjs.org/wickra-win32-arm64-msvc/-/wickra-win32-arm64-msvc-0.4.4.tgz",
|
||||
"version": "0.4.5",
|
||||
"resolved": "https://registry.npmjs.org/wickra-win32-arm64-msvc/-/wickra-win32-arm64-msvc-0.4.5.tgz",
|
||||
"integrity": "sha512-EXIckHxAtF75PUGDKRzXyqMe9ldP0JjSdu68WFN6iJfp+McYrGu6h40TEJlQ/oUEIoPqiZB/xhVyo/el5Lg7zw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
@@ -121,8 +121,8 @@
|
||||
}
|
||||
},
|
||||
"node_modules/wickra-win32-x64-msvc": {
|
||||
"version": "0.4.4",
|
||||
"resolved": "https://registry.npmjs.org/wickra-win32-x64-msvc/-/wickra-win32-x64-msvc-0.4.4.tgz",
|
||||
"version": "0.4.5",
|
||||
"resolved": "https://registry.npmjs.org/wickra-win32-x64-msvc/-/wickra-win32-x64-msvc-0.4.5.tgz",
|
||||
"integrity": "sha512-Yfsqq1Xwp6hdxMyLze411vNdo7BDwI6+lPSe7A9XdqyPecNDbtKwYLpsal2r8EHbNzqM+R8XnuRtUaEQS5VlUQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "wickra",
|
||||
"version": "0.4.4",
|
||||
"version": "0.4.5",
|
||||
"description": "Streaming-first technical indicators: incremental, fast, install-free. Node bindings powered by Rust.",
|
||||
"author": "kingchenc <support@wickra.org>",
|
||||
"main": "index.js",
|
||||
@@ -47,12 +47,12 @@
|
||||
"node": ">= 18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"wickra-linux-x64-gnu": "0.4.4",
|
||||
"wickra-linux-arm64-gnu": "0.4.4",
|
||||
"wickra-darwin-x64": "0.4.4",
|
||||
"wickra-darwin-arm64": "0.4.4",
|
||||
"wickra-win32-x64-msvc": "0.4.4",
|
||||
"wickra-win32-arm64-msvc": "0.4.4"
|
||||
"wickra-linux-x64-gnu": "0.4.5",
|
||||
"wickra-linux-arm64-gnu": "0.4.5",
|
||||
"wickra-darwin-x64": "0.4.5",
|
||||
"wickra-darwin-arm64": "0.4.5",
|
||||
"wickra-win32-x64-msvc": "0.4.5",
|
||||
"wickra-win32-arm64-msvc": "0.4.5"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "napi build --platform --release",
|
||||
|
||||
+374
-1
@@ -16,7 +16,7 @@ use napi::Error as NapiError;
|
||||
use napi::Status;
|
||||
use napi_derive::napi;
|
||||
use wickra_core as wc;
|
||||
use wickra_core::{BatchExt, Indicator};
|
||||
use wickra_core::{BarBuilder, BatchExt, Indicator};
|
||||
|
||||
fn map_err(e: wc::Error) -> NapiError {
|
||||
NapiError::new(Status::InvalidArg, e.to_string())
|
||||
@@ -3179,6 +3179,48 @@ impl AdOscillatorNode {
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== Anchored RSI ==============================
|
||||
|
||||
#[napi(js_name = "AnchoredRSI")]
|
||||
pub struct AnchoredRsiNode {
|
||||
inner: wc::AnchoredRsi,
|
||||
}
|
||||
|
||||
#[napi]
|
||||
impl AnchoredRsiNode {
|
||||
#[napi(constructor)]
|
||||
#[allow(clippy::new_without_default)]
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
inner: wc::AnchoredRsi::new(),
|
||||
}
|
||||
}
|
||||
#[napi(js_name = "setAnchor")]
|
||||
pub fn set_anchor(&mut self) {
|
||||
self.inner.set_anchor();
|
||||
}
|
||||
#[napi]
|
||||
pub fn update(&mut self, value: f64) -> Option<f64> {
|
||||
self.inner.update(value)
|
||||
}
|
||||
#[napi]
|
||||
pub fn batch(&mut self, prices: Vec<f64>) -> Vec<f64> {
|
||||
flatten(self.inner.batch(&prices))
|
||||
}
|
||||
#[napi]
|
||||
pub fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
#[napi(js_name = "isReady")]
|
||||
pub fn is_ready(&self) -> bool {
|
||||
self.inner.is_ready()
|
||||
}
|
||||
#[napi(js_name = "warmupPeriod")]
|
||||
pub fn warmup_period(&self) -> u32 {
|
||||
self.inner.warmup_period() as u32
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== Anchored VWAP ==============================
|
||||
|
||||
#[napi(js_name = "AnchoredVWAP")]
|
||||
@@ -8434,6 +8476,153 @@ impl ValueAreaNode {
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== VolumeProfile ==============================
|
||||
|
||||
#[napi(object)]
|
||||
pub struct VolumeProfileValue {
|
||||
pub price_low: f64,
|
||||
pub price_high: f64,
|
||||
pub bins: Vec<f64>,
|
||||
}
|
||||
|
||||
#[napi(js_name = "VolumeProfile")]
|
||||
pub struct VolumeProfileNode {
|
||||
inner: wc::VolumeProfile,
|
||||
}
|
||||
#[napi]
|
||||
impl VolumeProfileNode {
|
||||
#[napi(constructor)]
|
||||
pub fn new(period: u32, bin_count: u32) -> napi::Result<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::VolumeProfile::new(period as usize, bin_count as usize).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
#[napi]
|
||||
pub fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
#[napi(js_name = "isReady")]
|
||||
pub fn is_ready(&self) -> bool {
|
||||
self.inner.is_ready()
|
||||
}
|
||||
#[napi(js_name = "warmupPeriod")]
|
||||
pub fn warmup_period(&self) -> u32 {
|
||||
self.inner.warmup_period() as u32
|
||||
}
|
||||
#[napi]
|
||||
pub fn update(
|
||||
&mut self,
|
||||
high: f64,
|
||||
low: f64,
|
||||
volume: f64,
|
||||
) -> napi::Result<Option<VolumeProfileValue>> {
|
||||
let mid = f64::midpoint(high, low);
|
||||
let candle = wc::Candle::new(mid, high, low, mid, volume, 0).map_err(map_err)?;
|
||||
Ok(self.inner.update(candle).map(|o| VolumeProfileValue {
|
||||
price_low: o.price_low,
|
||||
price_high: o.price_high,
|
||||
bins: o.bins,
|
||||
}))
|
||||
}
|
||||
#[napi]
|
||||
pub fn batch(
|
||||
&mut self,
|
||||
high: Vec<f64>,
|
||||
low: Vec<f64>,
|
||||
volume: Vec<f64>,
|
||||
) -> napi::Result<Vec<f64>> {
|
||||
if high.len() != low.len() || low.len() != volume.len() {
|
||||
return Err(NapiError::from_reason(
|
||||
"high, low, volume must be equal length".to_string(),
|
||||
));
|
||||
}
|
||||
let k = self.inner.params().1 + 2;
|
||||
let n = high.len();
|
||||
let mut out = vec![f64::NAN; n * k];
|
||||
for i in 0..n {
|
||||
let mid = f64::midpoint(high[i], low[i]);
|
||||
let candle =
|
||||
wc::Candle::new(mid, high[i], low[i], mid, volume[i], 0).map_err(map_err)?;
|
||||
if let Some(o) = self.inner.update(candle) {
|
||||
out[i * k] = o.price_low;
|
||||
out[i * k + 1] = o.price_high;
|
||||
for (j, b) in o.bins.iter().enumerate() {
|
||||
out[i * k + 2 + j] = *b;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== TpoProfile ==============================
|
||||
|
||||
#[napi(object)]
|
||||
pub struct TpoProfileValue {
|
||||
pub price_low: f64,
|
||||
pub price_high: f64,
|
||||
pub counts: Vec<f64>,
|
||||
}
|
||||
|
||||
#[napi(js_name = "TpoProfile")]
|
||||
pub struct TpoProfileNode {
|
||||
inner: wc::TpoProfile,
|
||||
}
|
||||
#[napi]
|
||||
impl TpoProfileNode {
|
||||
#[napi(constructor)]
|
||||
pub fn new(period: u32, bin_count: u32) -> napi::Result<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::TpoProfile::new(period as usize, bin_count as usize).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
#[napi]
|
||||
pub fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
#[napi(js_name = "isReady")]
|
||||
pub fn is_ready(&self) -> bool {
|
||||
self.inner.is_ready()
|
||||
}
|
||||
#[napi(js_name = "warmupPeriod")]
|
||||
pub fn warmup_period(&self) -> u32 {
|
||||
self.inner.warmup_period() as u32
|
||||
}
|
||||
#[napi]
|
||||
pub fn update(&mut self, high: f64, low: f64) -> napi::Result<Option<TpoProfileValue>> {
|
||||
let mid = f64::midpoint(high, low);
|
||||
let candle = wc::Candle::new(mid, high, low, mid, 1.0, 0).map_err(map_err)?;
|
||||
Ok(self.inner.update(candle).map(|o| TpoProfileValue {
|
||||
price_low: o.price_low,
|
||||
price_high: o.price_high,
|
||||
counts: o.counts,
|
||||
}))
|
||||
}
|
||||
#[napi]
|
||||
pub fn batch(&mut self, high: Vec<f64>, low: Vec<f64>) -> napi::Result<Vec<f64>> {
|
||||
if high.len() != low.len() {
|
||||
return Err(NapiError::from_reason(
|
||||
"high, low must be equal length".to_string(),
|
||||
));
|
||||
}
|
||||
let k = self.inner.params().1 + 2;
|
||||
let n = high.len();
|
||||
let mut out = vec![f64::NAN; n * k];
|
||||
for i in 0..n {
|
||||
let mid = f64::midpoint(high[i], low[i]);
|
||||
let candle = wc::Candle::new(mid, high[i], low[i], mid, 1.0, 0).map_err(map_err)?;
|
||||
if let Some(o) = self.inner.update(candle) {
|
||||
out[i * k] = o.price_low;
|
||||
out[i * k + 1] = o.price_high;
|
||||
for (j, count) in o.counts.iter().enumerate() {
|
||||
out[i * k + 2 + j] = *count;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== InitialBalance ==============================
|
||||
|
||||
#[napi(object)]
|
||||
@@ -10871,6 +11060,190 @@ impl InformationRatioNode {
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== Alt-Chart Bars ==============================
|
||||
//
|
||||
// Bar builders consume close prices and emit a variable number of completed bars
|
||||
// per input. `update(close)` returns the bars finished on that close; `batch`
|
||||
// returns all completed bars concatenated.
|
||||
|
||||
#[napi(object)]
|
||||
pub struct RenkoBrickValue {
|
||||
pub open: f64,
|
||||
pub close: f64,
|
||||
pub direction: i32,
|
||||
}
|
||||
|
||||
#[napi(js_name = "RenkoBars")]
|
||||
pub struct RenkoBarsNode {
|
||||
inner: wc::RenkoBars,
|
||||
}
|
||||
#[napi]
|
||||
impl RenkoBarsNode {
|
||||
#[napi(constructor)]
|
||||
pub fn new(box_size: f64) -> napi::Result<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::RenkoBars::new(box_size).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
#[napi]
|
||||
pub fn update(&mut self, close: f64) -> napi::Result<Vec<RenkoBrickValue>> {
|
||||
let candle = wc::Candle::new(close, close, close, close, 1.0, 0).map_err(map_err)?;
|
||||
Ok(self
|
||||
.inner
|
||||
.update(candle)
|
||||
.into_iter()
|
||||
.map(|b| RenkoBrickValue {
|
||||
open: b.open,
|
||||
close: b.close,
|
||||
direction: i32::from(b.direction),
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
#[napi]
|
||||
pub fn batch(&mut self, close: Vec<f64>) -> napi::Result<Vec<RenkoBrickValue>> {
|
||||
let mut out = Vec::new();
|
||||
for price in close {
|
||||
let candle = wc::Candle::new(price, price, price, price, 1.0, 0).map_err(map_err)?;
|
||||
for b in self.inner.update(candle) {
|
||||
out.push(RenkoBrickValue {
|
||||
open: b.open,
|
||||
close: b.close,
|
||||
direction: i32::from(b.direction),
|
||||
});
|
||||
}
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
#[napi(js_name = "boxSize")]
|
||||
pub fn box_size(&self) -> f64 {
|
||||
self.inner.box_size()
|
||||
}
|
||||
#[napi]
|
||||
pub fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
}
|
||||
|
||||
#[napi(object)]
|
||||
pub struct KagiSegmentValue {
|
||||
pub start: f64,
|
||||
pub end: f64,
|
||||
pub direction: i32,
|
||||
}
|
||||
|
||||
#[napi(js_name = "KagiBars")]
|
||||
pub struct KagiBarsNode {
|
||||
inner: wc::KagiBars,
|
||||
}
|
||||
#[napi]
|
||||
impl KagiBarsNode {
|
||||
#[napi(constructor)]
|
||||
pub fn new(reversal: f64) -> napi::Result<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::KagiBars::new(reversal).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
#[napi]
|
||||
pub fn update(&mut self, close: f64) -> napi::Result<Vec<KagiSegmentValue>> {
|
||||
let candle = wc::Candle::new(close, close, close, close, 1.0, 0).map_err(map_err)?;
|
||||
Ok(self
|
||||
.inner
|
||||
.update(candle)
|
||||
.into_iter()
|
||||
.map(|b| KagiSegmentValue {
|
||||
start: b.start,
|
||||
end: b.end,
|
||||
direction: i32::from(b.direction),
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
#[napi]
|
||||
pub fn batch(&mut self, close: Vec<f64>) -> napi::Result<Vec<KagiSegmentValue>> {
|
||||
let mut out = Vec::new();
|
||||
for price in close {
|
||||
let candle = wc::Candle::new(price, price, price, price, 1.0, 0).map_err(map_err)?;
|
||||
for b in self.inner.update(candle) {
|
||||
out.push(KagiSegmentValue {
|
||||
start: b.start,
|
||||
end: b.end,
|
||||
direction: i32::from(b.direction),
|
||||
});
|
||||
}
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
#[napi]
|
||||
pub fn reversal(&self) -> f64 {
|
||||
self.inner.reversal()
|
||||
}
|
||||
#[napi]
|
||||
pub fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
}
|
||||
|
||||
#[napi(object)]
|
||||
pub struct PnfColumnValue {
|
||||
pub direction: i32,
|
||||
pub high: f64,
|
||||
pub low: f64,
|
||||
}
|
||||
|
||||
#[napi(js_name = "PointAndFigureBars")]
|
||||
pub struct PointAndFigureBarsNode {
|
||||
inner: wc::PointAndFigureBars,
|
||||
}
|
||||
#[napi]
|
||||
impl PointAndFigureBarsNode {
|
||||
#[napi(constructor)]
|
||||
pub fn new(box_size: f64, reversal: u32) -> napi::Result<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::PointAndFigureBars::new(box_size, reversal as usize).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
#[napi]
|
||||
pub fn update(&mut self, close: f64) -> napi::Result<Vec<PnfColumnValue>> {
|
||||
let candle = wc::Candle::new(close, close, close, close, 1.0, 0).map_err(map_err)?;
|
||||
Ok(self
|
||||
.inner
|
||||
.update(candle)
|
||||
.into_iter()
|
||||
.map(|col| PnfColumnValue {
|
||||
direction: i32::from(col.direction),
|
||||
high: col.high,
|
||||
low: col.low,
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
#[napi]
|
||||
pub fn batch(&mut self, close: Vec<f64>) -> napi::Result<Vec<PnfColumnValue>> {
|
||||
let mut out = Vec::new();
|
||||
for price in close {
|
||||
let candle = wc::Candle::new(price, price, price, price, 1.0, 0).map_err(map_err)?;
|
||||
for col in self.inner.update(candle) {
|
||||
out.push(PnfColumnValue {
|
||||
direction: i32::from(col.direction),
|
||||
high: col.high,
|
||||
low: col.low,
|
||||
});
|
||||
}
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
#[napi(js_name = "boxSize")]
|
||||
pub fn box_size(&self) -> f64 {
|
||||
self.inner.box_size()
|
||||
}
|
||||
#[napi]
|
||||
pub fn reversal(&self) -> u32 {
|
||||
self.inner.reversal() as u32
|
||||
}
|
||||
#[napi]
|
||||
pub fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
}
|
||||
|
||||
#[napi(js_name = "Alpha")]
|
||||
pub struct AlphaNode {
|
||||
inner: wc::Alpha,
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "maturin"
|
||||
|
||||
[project]
|
||||
name = "wickra"
|
||||
version = "0.4.4"
|
||||
version = "0.4.5"
|
||||
description = "Streaming-first technical indicators: incremental, fast, install-free."
|
||||
readme = "README.md"
|
||||
license = { text = "PolyForm-Noncommercial-1.0.0 with additional personal-account permissions; see LICENSE" }
|
||||
|
||||
@@ -47,6 +47,7 @@ from ._wickra import (
|
||||
EVWMA,
|
||||
# Momentum
|
||||
RSI,
|
||||
AnchoredRSI,
|
||||
MACD,
|
||||
Stochastic,
|
||||
CCI,
|
||||
@@ -222,8 +223,14 @@ from ._wickra import (
|
||||
HeikinAshi,
|
||||
# Market Profile
|
||||
ValueArea,
|
||||
VolumeProfile,
|
||||
TpoProfile,
|
||||
InitialBalance,
|
||||
OpeningRange,
|
||||
# Alt-Chart Bars
|
||||
RenkoBars,
|
||||
KagiBars,
|
||||
PointAndFigureBars,
|
||||
# Candlestick patterns
|
||||
Doji,
|
||||
Hammer,
|
||||
@@ -359,6 +366,7 @@ __all__ = [
|
||||
"EVWMA",
|
||||
# Momentum
|
||||
"RSI",
|
||||
"AnchoredRSI",
|
||||
"MACD",
|
||||
"Stochastic",
|
||||
"CCI",
|
||||
@@ -534,8 +542,14 @@ __all__ = [
|
||||
"HeikinAshi",
|
||||
# Market Profile
|
||||
"ValueArea",
|
||||
"VolumeProfile",
|
||||
"TpoProfile",
|
||||
"InitialBalance",
|
||||
"OpeningRange",
|
||||
# Alt-Chart Bars
|
||||
"RenkoBars",
|
||||
"KagiBars",
|
||||
"PointAndFigureBars",
|
||||
# Candlestick patterns
|
||||
"Doji",
|
||||
"Hammer",
|
||||
|
||||
+435
-1
@@ -17,7 +17,7 @@ use pyo3::exceptions::{PyTypeError, PyValueError};
|
||||
use pyo3::prelude::*;
|
||||
use pyo3::types::PyDict;
|
||||
use wickra_core as wc;
|
||||
use wickra_core::{BatchExt, Indicator};
|
||||
use wickra_core::{BarBuilder, BatchExt, Indicator};
|
||||
|
||||
fn map_err(e: wc::Error) -> PyErr {
|
||||
match e {
|
||||
@@ -5052,6 +5052,58 @@ impl PyAdOscillator {
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== Anchored RSI ==============================
|
||||
|
||||
#[pyclass(name = "AnchoredRSI", module = "wickra._wickra", skip_from_py_object)]
|
||||
#[derive(Clone)]
|
||||
struct PyAnchoredRsi {
|
||||
inner: wc::AnchoredRsi,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyAnchoredRsi {
|
||||
#[new]
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
inner: wc::AnchoredRsi::new(),
|
||||
}
|
||||
}
|
||||
fn update(&mut self, value: f64) -> Option<f64> {
|
||||
self.inner.update(value)
|
||||
}
|
||||
/// Re-anchor the cumulative window at the next bar that arrives.
|
||||
fn set_anchor(&mut self) {
|
||||
self.inner.set_anchor();
|
||||
}
|
||||
/// Batch over a close-price numpy column.
|
||||
fn batch<'py>(
|
||||
&mut self,
|
||||
py: Python<'py>,
|
||||
prices: PyReadonlyArray1<'py, f64>,
|
||||
) -> PyResult<Bound<'py, PyArray1<f64>>> {
|
||||
let slice = prices
|
||||
.as_slice()
|
||||
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||
Ok(flatten(self.inner.batch(slice)).into_pyarray(py))
|
||||
}
|
||||
#[getter]
|
||||
fn value(&self) -> Option<f64> {
|
||||
self.inner.value()
|
||||
}
|
||||
fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
fn is_ready(&self) -> bool {
|
||||
self.inner.is_ready()
|
||||
}
|
||||
fn warmup_period(&self) -> usize {
|
||||
self.inner.warmup_period()
|
||||
}
|
||||
fn __repr__(&self) -> String {
|
||||
"AnchoredRSI()".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== Anchored VWAP ==============================
|
||||
|
||||
#[pyclass(name = "AnchoredVWAP", module = "wickra._wickra", skip_from_py_object)]
|
||||
@@ -11274,6 +11326,183 @@ impl PyValueArea {
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== VolumeProfile ==============================
|
||||
|
||||
/// Streaming profile output: `(price_low, price_high, per_bin_values)`, or `None`
|
||||
/// during warmup. Shared by `VolumeProfile` (volume bins) and `TpoProfile` (TPO
|
||||
/// counts).
|
||||
type ProfileHistogram<'py> = Option<(f64, f64, Bound<'py, PyArray1<f64>>)>;
|
||||
|
||||
#[pyclass(name = "VolumeProfile", module = "wickra._wickra", skip_from_py_object)]
|
||||
#[derive(Clone)]
|
||||
struct PyVolumeProfile {
|
||||
inner: wc::VolumeProfile,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyVolumeProfile {
|
||||
#[new]
|
||||
#[pyo3(signature = (period=20, bin_count=50))]
|
||||
fn new(period: usize, bin_count: usize) -> PyResult<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::VolumeProfile::new(period, bin_count).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
/// Streaming update. Returns `(price_low, price_high, bins)` once warm, else `None`.
|
||||
fn update<'py>(
|
||||
&mut self,
|
||||
py: Python<'py>,
|
||||
candle: &Bound<'_, PyAny>,
|
||||
) -> PyResult<ProfileHistogram<'py>> {
|
||||
let c = extract_candle(candle)?;
|
||||
Ok(self
|
||||
.inner
|
||||
.update(c)
|
||||
.map(|o| (o.price_low, o.price_high, o.bins.into_pyarray(py))))
|
||||
}
|
||||
/// Batch over numpy columns high, low, volume. Returns shape `(n, bin_count + 2)`
|
||||
/// with columns `[price_low, price_high, bin_0, ..., bin_{k-1}]`; warmup rows are `NaN`.
|
||||
fn batch<'py>(
|
||||
&mut self,
|
||||
py: Python<'py>,
|
||||
high: PyReadonlyArray1<'py, f64>,
|
||||
low: PyReadonlyArray1<'py, f64>,
|
||||
volume: PyReadonlyArray1<'py, f64>,
|
||||
) -> PyResult<Bound<'py, PyArray2<f64>>> {
|
||||
let h = high
|
||||
.as_slice()
|
||||
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||
let l = low
|
||||
.as_slice()
|
||||
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||
let v = volume
|
||||
.as_slice()
|
||||
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||
if h.len() != l.len() || l.len() != v.len() {
|
||||
return Err(PyValueError::new_err(
|
||||
"high, low, volume must be equal length",
|
||||
));
|
||||
}
|
||||
let k = self.inner.params().1 + 2;
|
||||
let n = h.len();
|
||||
let mut out = vec![f64::NAN; n * k];
|
||||
for i in 0..n {
|
||||
let mid = f64::midpoint(h[i], l[i]);
|
||||
let candle = wc::Candle::new(mid, h[i], l[i], mid, v[i], 0).map_err(map_err)?;
|
||||
if let Some(o) = self.inner.update(candle) {
|
||||
out[i * k] = o.price_low;
|
||||
out[i * k + 1] = o.price_high;
|
||||
for (j, b) in o.bins.iter().enumerate() {
|
||||
out[i * k + 2 + j] = *b;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(numpy::ndarray::Array2::from_shape_vec((n, k), out)
|
||||
.expect("shape consistent")
|
||||
.into_pyarray(py))
|
||||
}
|
||||
#[getter]
|
||||
fn params(&self) -> (usize, usize) {
|
||||
self.inner.params()
|
||||
}
|
||||
fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
fn is_ready(&self) -> bool {
|
||||
self.inner.is_ready()
|
||||
}
|
||||
fn warmup_period(&self) -> usize {
|
||||
self.inner.warmup_period()
|
||||
}
|
||||
fn __repr__(&self) -> String {
|
||||
let (period, bin_count) = self.inner.params();
|
||||
format!("VolumeProfile(period={period}, bin_count={bin_count})")
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== TpoProfile ==============================
|
||||
|
||||
#[pyclass(name = "TpoProfile", module = "wickra._wickra", skip_from_py_object)]
|
||||
#[derive(Clone)]
|
||||
struct PyTpoProfile {
|
||||
inner: wc::TpoProfile,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyTpoProfile {
|
||||
#[new]
|
||||
#[pyo3(signature = (period=30, bin_count=50))]
|
||||
fn new(period: usize, bin_count: usize) -> PyResult<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::TpoProfile::new(period, bin_count).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
/// Streaming update. Returns `(price_low, price_high, counts)` once warm, else `None`.
|
||||
fn update<'py>(
|
||||
&mut self,
|
||||
py: Python<'py>,
|
||||
candle: &Bound<'_, PyAny>,
|
||||
) -> PyResult<ProfileHistogram<'py>> {
|
||||
let c = extract_candle(candle)?;
|
||||
Ok(self
|
||||
.inner
|
||||
.update(c)
|
||||
.map(|o| (o.price_low, o.price_high, o.counts.into_pyarray(py))))
|
||||
}
|
||||
/// Batch over numpy columns high, low. Returns shape `(n, bin_count + 2)`
|
||||
/// with columns `[price_low, price_high, count_0, ..., count_{k-1}]`; warmup rows are `NaN`.
|
||||
fn batch<'py>(
|
||||
&mut self,
|
||||
py: Python<'py>,
|
||||
high: PyReadonlyArray1<'py, f64>,
|
||||
low: PyReadonlyArray1<'py, f64>,
|
||||
) -> PyResult<Bound<'py, PyArray2<f64>>> {
|
||||
let h = high
|
||||
.as_slice()
|
||||
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||
let l = low
|
||||
.as_slice()
|
||||
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||
if h.len() != l.len() {
|
||||
return Err(PyValueError::new_err("high, low must be equal length"));
|
||||
}
|
||||
let k = self.inner.params().1 + 2;
|
||||
let n = h.len();
|
||||
let mut out = vec![f64::NAN; n * k];
|
||||
for i in 0..n {
|
||||
let mid = f64::midpoint(h[i], l[i]);
|
||||
let candle = wc::Candle::new(mid, h[i], l[i], mid, 1.0, 0).map_err(map_err)?;
|
||||
if let Some(o) = self.inner.update(candle) {
|
||||
out[i * k] = o.price_low;
|
||||
out[i * k + 1] = o.price_high;
|
||||
for (j, count) in o.counts.iter().enumerate() {
|
||||
out[i * k + 2 + j] = *count;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(numpy::ndarray::Array2::from_shape_vec((n, k), out)
|
||||
.expect("shape consistent")
|
||||
.into_pyarray(py))
|
||||
}
|
||||
#[getter]
|
||||
fn params(&self) -> (usize, usize) {
|
||||
self.inner.params()
|
||||
}
|
||||
fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
fn is_ready(&self) -> bool {
|
||||
self.inner.is_ready()
|
||||
}
|
||||
fn warmup_period(&self) -> usize {
|
||||
self.inner.warmup_period()
|
||||
}
|
||||
fn __repr__(&self) -> String {
|
||||
let (period, bin_count) = self.inner.params();
|
||||
format!("TpoProfile(period={period}, bin_count={bin_count})")
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== InitialBalance ==============================
|
||||
|
||||
#[pyclass(
|
||||
@@ -13981,6 +14210,205 @@ impl PyAlpha {
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== Alt-Chart Bars ==============================
|
||||
//
|
||||
// Bar builders consume close prices and emit a variable number of completed bars
|
||||
// per input. `update(close)` returns the bars finished on that close; `batch`
|
||||
// returns a `(k, 3)` array of all completed bars concatenated.
|
||||
|
||||
#[pyclass(name = "RenkoBars", module = "wickra._wickra", skip_from_py_object)]
|
||||
#[derive(Clone)]
|
||||
struct PyRenkoBars {
|
||||
inner: wc::RenkoBars,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyRenkoBars {
|
||||
#[new]
|
||||
fn new(box_size: f64) -> PyResult<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::RenkoBars::new(box_size).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
/// Feed one close; returns bricks completed on it as `(open, close, direction)`.
|
||||
fn update(&mut self, close: f64) -> PyResult<Vec<(f64, f64, i64)>> {
|
||||
let candle = wc::Candle::new(close, close, close, close, 1.0, 0).map_err(map_err)?;
|
||||
Ok(self
|
||||
.inner
|
||||
.update(candle)
|
||||
.into_iter()
|
||||
.map(|b| (b.open, b.close, i64::from(b.direction)))
|
||||
.collect())
|
||||
}
|
||||
/// Batch over a close column. Returns shape `(k, 3)` of `[open, close, direction]`.
|
||||
fn batch<'py>(
|
||||
&mut self,
|
||||
py: Python<'py>,
|
||||
close: PyReadonlyArray1<'py, f64>,
|
||||
) -> PyResult<Bound<'py, PyArray2<f64>>> {
|
||||
let prices = close
|
||||
.as_slice()
|
||||
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||
let mut rows: Vec<f64> = Vec::new();
|
||||
let mut k = 0usize;
|
||||
for &price in prices {
|
||||
let candle = wc::Candle::new(price, price, price, price, 1.0, 0).map_err(map_err)?;
|
||||
for b in self.inner.update(candle) {
|
||||
rows.push(b.open);
|
||||
rows.push(b.close);
|
||||
rows.push(f64::from(b.direction));
|
||||
k += 1;
|
||||
}
|
||||
}
|
||||
Ok(numpy::ndarray::Array2::from_shape_vec((k, 3), rows)
|
||||
.expect("shape consistent")
|
||||
.into_pyarray(py))
|
||||
}
|
||||
#[getter]
|
||||
fn box_size(&self) -> f64 {
|
||||
self.inner.box_size()
|
||||
}
|
||||
fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
fn __repr__(&self) -> String {
|
||||
format!("RenkoBars(box_size={})", self.inner.box_size())
|
||||
}
|
||||
}
|
||||
|
||||
#[pyclass(name = "KagiBars", module = "wickra._wickra", skip_from_py_object)]
|
||||
#[derive(Clone)]
|
||||
struct PyKagiBars {
|
||||
inner: wc::KagiBars,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyKagiBars {
|
||||
#[new]
|
||||
fn new(reversal: f64) -> PyResult<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::KagiBars::new(reversal).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
/// Feed one close; returns completed segments as `(start, end, direction)`.
|
||||
fn update(&mut self, close: f64) -> PyResult<Vec<(f64, f64, i64)>> {
|
||||
let candle = wc::Candle::new(close, close, close, close, 1.0, 0).map_err(map_err)?;
|
||||
Ok(self
|
||||
.inner
|
||||
.update(candle)
|
||||
.into_iter()
|
||||
.map(|b| (b.start, b.end, i64::from(b.direction)))
|
||||
.collect())
|
||||
}
|
||||
/// Batch over a close column. Returns shape `(k, 3)` of `[start, end, direction]`.
|
||||
fn batch<'py>(
|
||||
&mut self,
|
||||
py: Python<'py>,
|
||||
close: PyReadonlyArray1<'py, f64>,
|
||||
) -> PyResult<Bound<'py, PyArray2<f64>>> {
|
||||
let prices = close
|
||||
.as_slice()
|
||||
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||
let mut rows: Vec<f64> = Vec::new();
|
||||
let mut k = 0usize;
|
||||
for &price in prices {
|
||||
let candle = wc::Candle::new(price, price, price, price, 1.0, 0).map_err(map_err)?;
|
||||
for b in self.inner.update(candle) {
|
||||
rows.push(b.start);
|
||||
rows.push(b.end);
|
||||
rows.push(f64::from(b.direction));
|
||||
k += 1;
|
||||
}
|
||||
}
|
||||
Ok(numpy::ndarray::Array2::from_shape_vec((k, 3), rows)
|
||||
.expect("shape consistent")
|
||||
.into_pyarray(py))
|
||||
}
|
||||
#[getter]
|
||||
fn reversal(&self) -> f64 {
|
||||
self.inner.reversal()
|
||||
}
|
||||
fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
fn __repr__(&self) -> String {
|
||||
format!("KagiBars(reversal={})", self.inner.reversal())
|
||||
}
|
||||
}
|
||||
|
||||
#[pyclass(
|
||||
name = "PointAndFigureBars",
|
||||
module = "wickra._wickra",
|
||||
skip_from_py_object
|
||||
)]
|
||||
#[derive(Clone)]
|
||||
struct PyPointAndFigureBars {
|
||||
inner: wc::PointAndFigureBars,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyPointAndFigureBars {
|
||||
#[new]
|
||||
#[pyo3(signature = (box_size, reversal=3))]
|
||||
fn new(box_size: f64, reversal: usize) -> PyResult<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::PointAndFigureBars::new(box_size, reversal).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
/// Feed one close; returns completed columns as `(direction, high, low)`.
|
||||
fn update(&mut self, close: f64) -> PyResult<Vec<(i64, f64, f64)>> {
|
||||
let candle = wc::Candle::new(close, close, close, close, 1.0, 0).map_err(map_err)?;
|
||||
Ok(self
|
||||
.inner
|
||||
.update(candle)
|
||||
.into_iter()
|
||||
.map(|c| (i64::from(c.direction), c.high, c.low))
|
||||
.collect())
|
||||
}
|
||||
/// Batch over a close column. Returns shape `(k, 3)` of `[direction, high, low]`.
|
||||
fn batch<'py>(
|
||||
&mut self,
|
||||
py: Python<'py>,
|
||||
close: PyReadonlyArray1<'py, f64>,
|
||||
) -> PyResult<Bound<'py, PyArray2<f64>>> {
|
||||
let prices = close
|
||||
.as_slice()
|
||||
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||
let mut rows: Vec<f64> = Vec::new();
|
||||
let mut k = 0usize;
|
||||
for &price in prices {
|
||||
let candle = wc::Candle::new(price, price, price, price, 1.0, 0).map_err(map_err)?;
|
||||
for col in self.inner.update(candle) {
|
||||
rows.push(f64::from(col.direction));
|
||||
rows.push(col.high);
|
||||
rows.push(col.low);
|
||||
k += 1;
|
||||
}
|
||||
}
|
||||
Ok(numpy::ndarray::Array2::from_shape_vec((k, 3), rows)
|
||||
.expect("shape consistent")
|
||||
.into_pyarray(py))
|
||||
}
|
||||
#[getter]
|
||||
fn box_size(&self) -> f64 {
|
||||
self.inner.box_size()
|
||||
}
|
||||
#[getter]
|
||||
fn reversal(&self) -> usize {
|
||||
self.inner.reversal()
|
||||
}
|
||||
fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
fn __repr__(&self) -> String {
|
||||
format!(
|
||||
"PointAndFigureBars(box_size={}, reversal={})",
|
||||
self.inner.box_size(),
|
||||
self.inner.reversal()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== Module ==============================
|
||||
|
||||
#[pymodule]
|
||||
@@ -14058,6 +14486,7 @@ fn _wickra(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||
m.add_class::<PyNvi>()?;
|
||||
m.add_class::<PyPvi>()?;
|
||||
m.add_class::<PyAdOscillator>()?;
|
||||
m.add_class::<PyAnchoredRsi>()?;
|
||||
m.add_class::<PyAnchoredVwap>()?;
|
||||
m.add_class::<PyDemandIndex>()?;
|
||||
m.add_class::<PyTsv>()?;
|
||||
@@ -14175,6 +14604,11 @@ fn _wickra(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||
m.add_class::<PyRelativeStrengthAB>()?;
|
||||
m.add_class::<PySpearmanCorrelation>()?;
|
||||
m.add_class::<PyValueArea>()?;
|
||||
m.add_class::<PyVolumeProfile>()?;
|
||||
m.add_class::<PyTpoProfile>()?;
|
||||
m.add_class::<PyRenkoBars>()?;
|
||||
m.add_class::<PyKagiBars>()?;
|
||||
m.add_class::<PyPointAndFigureBars>()?;
|
||||
m.add_class::<PyInitialBalance>()?;
|
||||
m.add_class::<PyOpeningRange>()?;
|
||||
// Candlestick patterns.
|
||||
|
||||
@@ -66,6 +66,14 @@ def test_rsi_wilder_textbook_first_value():
|
||||
assert math.isclose(out[14], 70.464, abs_tol=0.05)
|
||||
|
||||
|
||||
def test_anchored_rsi_cumulative_reference():
|
||||
"""Cumulative anchored RSI: 10 -> 11 (+1) -> 9 (-2) -> 12 (+3)."""
|
||||
out = ta.AnchoredRSI().batch(np.array([10.0, 11.0, 9.0, 12.0]))
|
||||
assert math.isclose(out[1], 100.0, abs_tol=1e-9)
|
||||
assert math.isclose(out[2], 100.0 - 100.0 / 1.5, abs_tol=1e-6)
|
||||
assert math.isclose(out[3], 100.0 - 100.0 / 3.0, abs_tol=1e-6)
|
||||
|
||||
|
||||
def test_inertia_constant_rvi_passes_through_linreg():
|
||||
# Every bar identical (open, high, low, close) = (10, 11, 9, 10.5):
|
||||
# RVI = (c-o) / (h-l) = 0.5 / 2 = 0.25 every bar. LinReg of a constant
|
||||
|
||||
@@ -12,6 +12,7 @@ SCALAR_INDICATORS = [
|
||||
(ta.EMA, (14,)),
|
||||
(ta.WMA, (14,)),
|
||||
(ta.RSI, (14,)),
|
||||
(ta.AnchoredRSI, ()),
|
||||
(ta.MACD, ()),
|
||||
(ta.BollingerBands, ()),
|
||||
]
|
||||
@@ -42,6 +43,7 @@ def test_reset_returns_to_initial_state(cls, args):
|
||||
(ta.EMA, (14,), 14),
|
||||
(ta.WMA, (14,), 14),
|
||||
(ta.RSI, (14,), 15),
|
||||
(ta.AnchoredRSI, (), 2),
|
||||
(ta.BollingerBands, (20, 2.0), 20),
|
||||
],
|
||||
)
|
||||
|
||||
@@ -1010,6 +1010,64 @@ def test_opening_range_shape_and_streaming(ohlcv):
|
||||
assert _eq_nan(batch, np.array(rows, dtype=np.float64))
|
||||
|
||||
|
||||
def test_volume_profile_reference():
|
||||
# bar0 single-print at 10 vol 100; bar1 spans 10..14 vol 80 over 4 bins.
|
||||
vp = ta.VolumeProfile(2, 4)
|
||||
assert vp.update((10.0, 10.0, 10.0, 10.0, 100.0, 0)) is None
|
||||
out = vp.update((10.0, 14.0, 10.0, 12.0, 80.0, 1))
|
||||
assert out is not None
|
||||
price_low, price_high, bins = out
|
||||
assert price_low == pytest.approx(10.0)
|
||||
assert price_high == pytest.approx(14.0)
|
||||
np.testing.assert_allclose(bins, [120.0, 20.0, 20.0, 20.0])
|
||||
|
||||
|
||||
def test_volume_profile_streaming_matches_batch(ohlcv):
|
||||
high, low, close, volume = ohlcv
|
||||
batch = ta.VolumeProfile(10, 8).batch(high, low, volume)
|
||||
assert batch.shape == (high.size, 10)
|
||||
streamer = ta.VolumeProfile(10, 8)
|
||||
for i in range(high.size):
|
||||
mid = float((high[i] + low[i]) / 2)
|
||||
out = streamer.update((mid, float(high[i]), float(low[i]), mid, float(volume[i]), i))
|
||||
if out is None:
|
||||
assert np.isnan(batch[i]).all()
|
||||
else:
|
||||
pl, ph, bins = out
|
||||
assert pl == pytest.approx(batch[i][0])
|
||||
assert ph == pytest.approx(batch[i][1])
|
||||
np.testing.assert_allclose(bins, batch[i][2:])
|
||||
|
||||
|
||||
def test_tpo_profile_reference():
|
||||
# bar0 spans 10..14 (+1 to all 4 bins); bar1 spans 11..12 (+1 to bins 1,2).
|
||||
tpo = ta.TpoProfile(2, 4)
|
||||
assert tpo.update((12.0, 14.0, 10.0, 12.0, 5.0, 0)) is None
|
||||
out = tpo.update((11.5, 12.0, 11.0, 11.5, 999.0, 1))
|
||||
assert out is not None
|
||||
price_low, price_high, counts = out
|
||||
assert price_low == pytest.approx(10.0)
|
||||
assert price_high == pytest.approx(14.0)
|
||||
np.testing.assert_allclose(counts, [1.0, 2.0, 2.0, 1.0])
|
||||
|
||||
|
||||
def test_tpo_profile_streaming_matches_batch(ohlcv):
|
||||
high, low, close, volume = ohlcv
|
||||
batch = ta.TpoProfile(10, 8).batch(high, low)
|
||||
assert batch.shape == (high.size, 10)
|
||||
streamer = ta.TpoProfile(10, 8)
|
||||
for i in range(high.size):
|
||||
mid = float((high[i] + low[i]) / 2)
|
||||
out = streamer.update((mid, float(high[i]), float(low[i]), mid, 1.0, i))
|
||||
if out is None:
|
||||
assert np.isnan(batch[i]).all()
|
||||
else:
|
||||
pl, ph, counts = out
|
||||
assert pl == pytest.approx(batch[i][0])
|
||||
assert ph == pytest.approx(batch[i][1])
|
||||
np.testing.assert_allclose(counts, batch[i][2:])
|
||||
|
||||
|
||||
# --- TD Pressure (OHLCV-input) -------------------------------------------
|
||||
|
||||
|
||||
@@ -1193,6 +1251,42 @@ def test_anchored_vwap_set_anchor_clears_window():
|
||||
assert v == pytest.approx(100.0)
|
||||
|
||||
|
||||
def test_anchored_rsi_reference():
|
||||
# prices 10 -> 11 (+1) -> 9 (-2) -> 12 (+3); cumulative anchored RSI.
|
||||
# bar2: sum_gain=1, sum_loss=2 -> rs=0.5 -> 100 - 100/1.5 = 33.3333
|
||||
# bar3: sum_gain=4, sum_loss=2 -> rs=2.0 -> 100 - 100/3 = 66.6667
|
||||
rsi = ta.AnchoredRSI()
|
||||
out = rsi.batch(np.array([10.0, 11.0, 9.0, 12.0]))
|
||||
assert np.isnan(out[0])
|
||||
assert out[1] == pytest.approx(100.0)
|
||||
assert out[2] == pytest.approx(33.333333, abs=1e-4)
|
||||
assert out[3] == pytest.approx(66.666666, abs=1e-4)
|
||||
|
||||
|
||||
def test_anchored_rsi_set_anchor_clears_window():
|
||||
# Downtrend reads 0; after re-anchor an uptrend must read a fresh 100.
|
||||
rsi = ta.AnchoredRSI()
|
||||
for p in (20.0, 19.0, 18.0, 17.0):
|
||||
rsi.update(p)
|
||||
assert rsi.is_ready()
|
||||
assert rsi.value == pytest.approx(0.0)
|
||||
rsi.set_anchor()
|
||||
assert rsi.update(50.0) is None
|
||||
assert rsi.update(51.0) == pytest.approx(100.0)
|
||||
|
||||
|
||||
def test_anchored_rsi_streaming_matches_batch():
|
||||
prices = np.array([100.0 + np.sin(i * 0.4) * 8.0 for i in range(60)])
|
||||
batched = ta.AnchoredRSI().batch(prices)
|
||||
streamer = ta.AnchoredRSI()
|
||||
streamed = [streamer.update(float(p)) for p in prices]
|
||||
for b, s in zip(batched, streamed):
|
||||
if np.isnan(b):
|
||||
assert s is None
|
||||
else:
|
||||
assert s == pytest.approx(b)
|
||||
|
||||
|
||||
def test_tsv_reference():
|
||||
# closes = [10, 11, 13, 12, 14, 15]
|
||||
# volumes = [50, 100, 200, 150, 50, 200]
|
||||
@@ -2555,3 +2649,44 @@ def test_basis_indicators_streaming_equals_batch():
|
||||
[streamer.update(futures[i], mark[i]) for i in range(n)], dtype=np.float64
|
||||
)
|
||||
assert _eq_nan(batch, streamed)
|
||||
|
||||
|
||||
# --- Alt-Chart Bars ------------------------------------------------------
|
||||
|
||||
|
||||
def test_renko_bars_reference():
|
||||
r = ta.RenkoBars(1.0)
|
||||
assert r.update(10.0) == [] # seed
|
||||
assert r.update(13.0) == [(10.0, 11.0, 1), (11.0, 12.0, 1), (12.0, 13.0, 1)]
|
||||
assert r.update(10.0) == [(12.0, 11.0, -1), (11.0, 10.0, -1)] # 2-box reversal
|
||||
|
||||
|
||||
def test_renko_bars_batch_shape():
|
||||
r = ta.RenkoBars(1.0)
|
||||
out = r.batch(np.array([10.0, 11.0, 12.0, 13.0]))
|
||||
assert out.shape == (3, 3)
|
||||
np.testing.assert_allclose(out[:, 2], [1.0, 1.0, 1.0])
|
||||
|
||||
|
||||
def test_kagi_bars_reference():
|
||||
k = ta.KagiBars(2.0)
|
||||
assert k.update(10.0) == [] # seed
|
||||
assert k.update(11.0) == [] # establishes up
|
||||
assert k.update(15.0) == [] # extends
|
||||
assert k.update(12.0) == [(10.0, 15.0, 1)] # reversal closes up segment
|
||||
|
||||
|
||||
def test_point_and_figure_bars_reference():
|
||||
pnf = ta.PointAndFigureBars(1.0, 3)
|
||||
assert pnf.update(10.0) == [] # seed
|
||||
assert pnf.update(13.0) == [] # starts X column
|
||||
assert pnf.update(15.0) == [] # extends up
|
||||
assert pnf.update(12.0) == [(1, 15.0, 10.0)] # 3-box reversal closes X column
|
||||
|
||||
|
||||
def test_bar_builders_reset():
|
||||
r = ta.RenkoBars(1.0)
|
||||
r.update(10.0)
|
||||
r.update(15.0)
|
||||
r.reset()
|
||||
assert r.update(50.0) == [] # re-seeds after reset
|
||||
|
||||
+323
-1
@@ -12,7 +12,7 @@
|
||||
use js_sys::{Array, Float64Array, Object, Reflect};
|
||||
use wasm_bindgen::prelude::*;
|
||||
use wickra_core as wc;
|
||||
use wickra_core::{BatchExt, Indicator};
|
||||
use wickra_core::{BarBuilder, BatchExt, Indicator};
|
||||
|
||||
fn map_err(e: wc::Error) -> JsError {
|
||||
JsError::new(&e.to_string())
|
||||
@@ -1682,6 +1682,44 @@ impl WasmAdOscillator {
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = AnchoredRSI)]
|
||||
pub struct WasmAnchoredRsi {
|
||||
inner: wc::AnchoredRsi,
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_class = AnchoredRSI)]
|
||||
impl WasmAnchoredRsi {
|
||||
#[wasm_bindgen(constructor)]
|
||||
#[allow(clippy::new_without_default)]
|
||||
pub fn new() -> WasmAnchoredRsi {
|
||||
Self {
|
||||
inner: wc::AnchoredRsi::new(),
|
||||
}
|
||||
}
|
||||
#[wasm_bindgen(js_name = setAnchor)]
|
||||
pub fn set_anchor(&mut self) {
|
||||
self.inner.set_anchor();
|
||||
}
|
||||
pub fn update(&mut self, value: f64) -> Option<f64> {
|
||||
self.inner.update(value)
|
||||
}
|
||||
pub fn batch(&mut self, prices: &[f64]) -> Float64Array {
|
||||
let out = flatten(self.inner.batch(prices));
|
||||
Float64Array::from(out.as_slice())
|
||||
}
|
||||
pub fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
#[wasm_bindgen(js_name = isReady)]
|
||||
pub fn is_ready(&self) -> bool {
|
||||
self.inner.is_ready()
|
||||
}
|
||||
#[wasm_bindgen(js_name = warmupPeriod)]
|
||||
pub fn warmup_period(&self) -> usize {
|
||||
self.inner.warmup_period()
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = AnchoredVWAP)]
|
||||
pub struct WasmAnchoredVwap {
|
||||
inner: wc::AnchoredVwap,
|
||||
@@ -6032,6 +6070,135 @@ impl WasmValueArea {
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = VolumeProfile)]
|
||||
pub struct WasmVolumeProfile {
|
||||
inner: wc::VolumeProfile,
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_class = VolumeProfile)]
|
||||
impl WasmVolumeProfile {
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(period: usize, bin_count: usize) -> Result<WasmVolumeProfile, JsError> {
|
||||
Ok(Self {
|
||||
inner: wc::VolumeProfile::new(period, bin_count).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
pub fn batch(
|
||||
&mut self,
|
||||
high: &[f64],
|
||||
low: &[f64],
|
||||
volume: &[f64],
|
||||
) -> Result<Float64Array, JsError> {
|
||||
if high.len() != low.len() || low.len() != volume.len() {
|
||||
return Err(JsError::new("high, low, volume must be equal length"));
|
||||
}
|
||||
let k = self.inner.params().1 + 2;
|
||||
let n = high.len();
|
||||
let mut out = vec![f64::NAN; n * k];
|
||||
for i in 0..n {
|
||||
let mid = f64::midpoint(high[i], low[i]);
|
||||
let c = wc::Candle::new(mid, high[i], low[i], mid, volume[i], 0).map_err(map_err)?;
|
||||
if let Some(o) = self.inner.update(c) {
|
||||
out[i * k] = o.price_low;
|
||||
out[i * k + 1] = o.price_high;
|
||||
for (j, b) in o.bins.iter().enumerate() {
|
||||
out[i * k + 2 + j] = *b;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(Float64Array::from(out.as_slice()))
|
||||
}
|
||||
/// Streaming update. Returns `{ priceLow, priceHigh, bins }` once warm, else `null`.
|
||||
pub fn update(&mut self, high: f64, low: f64, volume: f64) -> Result<JsValue, JsError> {
|
||||
let mid = f64::midpoint(high, low);
|
||||
let c = wc::Candle::new(mid, high, low, mid, volume, 0).map_err(map_err)?;
|
||||
Ok(match self.inner.update(c) {
|
||||
Some(o) => {
|
||||
let obj = Object::new();
|
||||
Reflect::set(&obj, &"priceLow".into(), &o.price_low.into()).ok();
|
||||
Reflect::set(&obj, &"priceHigh".into(), &o.price_high.into()).ok();
|
||||
let bins = Float64Array::from(o.bins.as_slice());
|
||||
Reflect::set(&obj, &"bins".into(), &bins).ok();
|
||||
obj.into()
|
||||
}
|
||||
None => JsValue::NULL,
|
||||
})
|
||||
}
|
||||
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 = TpoProfile)]
|
||||
pub struct WasmTpoProfile {
|
||||
inner: wc::TpoProfile,
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_class = TpoProfile)]
|
||||
impl WasmTpoProfile {
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(period: usize, bin_count: usize) -> Result<WasmTpoProfile, JsError> {
|
||||
Ok(Self {
|
||||
inner: wc::TpoProfile::new(period, bin_count).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
pub fn batch(&mut self, high: &[f64], low: &[f64]) -> Result<Float64Array, JsError> {
|
||||
if high.len() != low.len() {
|
||||
return Err(JsError::new("high, low must be equal length"));
|
||||
}
|
||||
let k = self.inner.params().1 + 2;
|
||||
let n = high.len();
|
||||
let mut out = vec![f64::NAN; n * k];
|
||||
for i in 0..n {
|
||||
let mid = f64::midpoint(high[i], low[i]);
|
||||
let c = wc::Candle::new(mid, high[i], low[i], mid, 1.0, 0).map_err(map_err)?;
|
||||
if let Some(o) = self.inner.update(c) {
|
||||
out[i * k] = o.price_low;
|
||||
out[i * k + 1] = o.price_high;
|
||||
for (j, count) in o.counts.iter().enumerate() {
|
||||
out[i * k + 2 + j] = *count;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(Float64Array::from(out.as_slice()))
|
||||
}
|
||||
/// Streaming update. Returns `{ priceLow, priceHigh, counts }` once warm, else `null`.
|
||||
pub fn update(&mut self, high: f64, low: f64) -> Result<JsValue, JsError> {
|
||||
let mid = f64::midpoint(high, low);
|
||||
let c = wc::Candle::new(mid, high, low, mid, 1.0, 0).map_err(map_err)?;
|
||||
Ok(match self.inner.update(c) {
|
||||
Some(o) => {
|
||||
let obj = Object::new();
|
||||
Reflect::set(&obj, &"priceLow".into(), &o.price_low.into()).ok();
|
||||
Reflect::set(&obj, &"priceHigh".into(), &o.price_high.into()).ok();
|
||||
let counts = Float64Array::from(o.counts.as_slice());
|
||||
Reflect::set(&obj, &"counts".into(), &counts).ok();
|
||||
obj.into()
|
||||
}
|
||||
None => JsValue::NULL,
|
||||
})
|
||||
}
|
||||
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 = InitialBalance)]
|
||||
pub struct WasmInitialBalance {
|
||||
inner: wc::InitialBalance,
|
||||
@@ -8350,6 +8517,161 @@ impl WasmInformationRatio {
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== Alt-Chart Bars ==============================
|
||||
//
|
||||
// Bar builders consume close prices and emit a variable number of completed bars
|
||||
// per input. `update(close)` returns a JS array of the bars finished on that
|
||||
// close; `batch` returns all completed bars concatenated.
|
||||
|
||||
#[wasm_bindgen(js_name = RenkoBars)]
|
||||
pub struct WasmRenkoBars {
|
||||
inner: wc::RenkoBars,
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_class = RenkoBars)]
|
||||
impl WasmRenkoBars {
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(box_size: f64) -> Result<WasmRenkoBars, JsError> {
|
||||
Ok(Self {
|
||||
inner: wc::RenkoBars::new(box_size).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
/// Returns an array of `{ open, close, direction }` bricks completed on this close.
|
||||
pub fn update(&mut self, close: f64) -> Result<Array, JsError> {
|
||||
let candle = wc::Candle::new(close, close, close, close, 1.0, 0).map_err(map_err)?;
|
||||
let arr = Array::new();
|
||||
for b in self.inner.update(candle) {
|
||||
let obj = Object::new();
|
||||
Reflect::set(&obj, &"open".into(), &b.open.into()).ok();
|
||||
Reflect::set(&obj, &"close".into(), &b.close.into()).ok();
|
||||
Reflect::set(&obj, &"direction".into(), &f64::from(b.direction).into()).ok();
|
||||
arr.push(&obj);
|
||||
}
|
||||
Ok(arr)
|
||||
}
|
||||
pub fn batch(&mut self, close: &[f64]) -> Result<Array, JsError> {
|
||||
let arr = Array::new();
|
||||
for &price in close {
|
||||
let candle = wc::Candle::new(price, price, price, price, 1.0, 0).map_err(map_err)?;
|
||||
for b in self.inner.update(candle) {
|
||||
let obj = Object::new();
|
||||
Reflect::set(&obj, &"open".into(), &b.open.into()).ok();
|
||||
Reflect::set(&obj, &"close".into(), &b.close.into()).ok();
|
||||
Reflect::set(&obj, &"direction".into(), &f64::from(b.direction).into()).ok();
|
||||
arr.push(&obj);
|
||||
}
|
||||
}
|
||||
Ok(arr)
|
||||
}
|
||||
#[wasm_bindgen(js_name = boxSize)]
|
||||
pub fn box_size(&self) -> f64 {
|
||||
self.inner.box_size()
|
||||
}
|
||||
pub fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = KagiBars)]
|
||||
pub struct WasmKagiBars {
|
||||
inner: wc::KagiBars,
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_class = KagiBars)]
|
||||
impl WasmKagiBars {
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(reversal: f64) -> Result<WasmKagiBars, JsError> {
|
||||
Ok(Self {
|
||||
inner: wc::KagiBars::new(reversal).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
/// Returns an array of `{ start, end, direction }` segments completed on this close.
|
||||
pub fn update(&mut self, close: f64) -> Result<Array, JsError> {
|
||||
let candle = wc::Candle::new(close, close, close, close, 1.0, 0).map_err(map_err)?;
|
||||
let arr = Array::new();
|
||||
for b in self.inner.update(candle) {
|
||||
let obj = Object::new();
|
||||
Reflect::set(&obj, &"start".into(), &b.start.into()).ok();
|
||||
Reflect::set(&obj, &"end".into(), &b.end.into()).ok();
|
||||
Reflect::set(&obj, &"direction".into(), &f64::from(b.direction).into()).ok();
|
||||
arr.push(&obj);
|
||||
}
|
||||
Ok(arr)
|
||||
}
|
||||
pub fn batch(&mut self, close: &[f64]) -> Result<Array, JsError> {
|
||||
let arr = Array::new();
|
||||
for &price in close {
|
||||
let candle = wc::Candle::new(price, price, price, price, 1.0, 0).map_err(map_err)?;
|
||||
for b in self.inner.update(candle) {
|
||||
let obj = Object::new();
|
||||
Reflect::set(&obj, &"start".into(), &b.start.into()).ok();
|
||||
Reflect::set(&obj, &"end".into(), &b.end.into()).ok();
|
||||
Reflect::set(&obj, &"direction".into(), &f64::from(b.direction).into()).ok();
|
||||
arr.push(&obj);
|
||||
}
|
||||
}
|
||||
Ok(arr)
|
||||
}
|
||||
pub fn reversal(&self) -> f64 {
|
||||
self.inner.reversal()
|
||||
}
|
||||
pub fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = PointAndFigureBars)]
|
||||
pub struct WasmPointAndFigureBars {
|
||||
inner: wc::PointAndFigureBars,
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_class = PointAndFigureBars)]
|
||||
impl WasmPointAndFigureBars {
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(box_size: f64, reversal: usize) -> Result<WasmPointAndFigureBars, JsError> {
|
||||
Ok(Self {
|
||||
inner: wc::PointAndFigureBars::new(box_size, reversal).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
/// Returns an array of `{ direction, high, low }` columns completed on this close.
|
||||
pub fn update(&mut self, close: f64) -> Result<Array, JsError> {
|
||||
let candle = wc::Candle::new(close, close, close, close, 1.0, 0).map_err(map_err)?;
|
||||
let arr = Array::new();
|
||||
for col in self.inner.update(candle) {
|
||||
let obj = Object::new();
|
||||
Reflect::set(&obj, &"direction".into(), &f64::from(col.direction).into()).ok();
|
||||
Reflect::set(&obj, &"high".into(), &col.high.into()).ok();
|
||||
Reflect::set(&obj, &"low".into(), &col.low.into()).ok();
|
||||
arr.push(&obj);
|
||||
}
|
||||
Ok(arr)
|
||||
}
|
||||
pub fn batch(&mut self, close: &[f64]) -> Result<Array, JsError> {
|
||||
let arr = Array::new();
|
||||
for &price in close {
|
||||
let candle = wc::Candle::new(price, price, price, price, 1.0, 0).map_err(map_err)?;
|
||||
for col in self.inner.update(candle) {
|
||||
let obj = Object::new();
|
||||
Reflect::set(&obj, &"direction".into(), &f64::from(col.direction).into()).ok();
|
||||
Reflect::set(&obj, &"high".into(), &col.high.into()).ok();
|
||||
Reflect::set(&obj, &"low".into(), &col.low.into()).ok();
|
||||
arr.push(&obj);
|
||||
}
|
||||
}
|
||||
Ok(arr)
|
||||
}
|
||||
#[wasm_bindgen(js_name = boxSize)]
|
||||
pub fn box_size(&self) -> f64 {
|
||||
self.inner.box_size()
|
||||
}
|
||||
pub fn reversal(&self) -> usize {
|
||||
self.inner.reversal()
|
||||
}
|
||||
pub fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = Alpha)]
|
||||
pub struct WasmAlpha {
|
||||
inner: wc::Alpha,
|
||||
|
||||
@@ -0,0 +1,284 @@
|
||||
//! Anchored Relative Strength Index.
|
||||
|
||||
use crate::traits::Indicator;
|
||||
|
||||
/// Anchored RSI — a cumulative Relative Strength Index whose averaging begins at
|
||||
/// a user-chosen anchor bar rather than over a fixed Wilder period.
|
||||
///
|
||||
/// Where [`crate::Rsi`] uses Wilder's `period`-length smoothing, Anchored RSI
|
||||
/// accumulates *every* up- and down-move since the anchor with equal weight, so
|
||||
/// it answers "what is the RSI of the entire move since the anchor point?". The
|
||||
/// running relative strength is `Σ gains / Σ losses` over all bars in the
|
||||
/// current anchor window (the bar count cancels, so this equals
|
||||
/// `avg_gain / avg_loss`):
|
||||
///
|
||||
/// ```text
|
||||
/// RSI_t = 100 - 100 / (1 + Σ_{i ≥ anchor} gain_i / Σ_{i ≥ anchor} loss_i)
|
||||
/// ```
|
||||
///
|
||||
/// As with [`crate::AnchoredVwap`], the anchor is chosen at runtime:
|
||||
/// [`AnchoredRsi::set_anchor`] re-anchors at the **next** bar that arrives,
|
||||
/// clearing the running sums. Because RSI needs a price *change*, the first bar
|
||||
/// of a fresh anchor window only seeds the previous close and emits `None`; the
|
||||
/// first value follows on the second bar (warmup period 2).
|
||||
///
|
||||
/// Saturation follows the standard convention: a window with no losses yet (and
|
||||
/// at least one gain) reads 100, no gains yet reads 0, and a perfectly flat
|
||||
/// window reads the neutral 50. Non-finite inputs are ignored, leaving the last
|
||||
/// value unchanged.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use wickra_core::{AnchoredRsi, Indicator};
|
||||
///
|
||||
/// let mut indicator = AnchoredRsi::new();
|
||||
/// let mut last = None;
|
||||
/// for i in 0..80 {
|
||||
/// let price = 100.0 + (f64::from(i) * 0.5).sin() * 5.0;
|
||||
/// // Re-anchor at bar 40 (e.g. a major swing low).
|
||||
/// if i == 40 {
|
||||
/// indicator.set_anchor();
|
||||
/// }
|
||||
/// last = indicator.update(price);
|
||||
/// }
|
||||
/// assert!(last.is_some());
|
||||
/// ```
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct AnchoredRsi {
|
||||
prev_close: Option<f64>,
|
||||
sum_gain: f64,
|
||||
sum_loss: f64,
|
||||
last_value: Option<f64>,
|
||||
pending_anchor: bool,
|
||||
}
|
||||
|
||||
impl AnchoredRsi {
|
||||
/// Construct a fresh Anchored RSI. The first bar to arrive is the anchor.
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
prev_close: None,
|
||||
sum_gain: 0.0,
|
||||
sum_loss: 0.0,
|
||||
last_value: None,
|
||||
pending_anchor: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Mark a re-anchor: the **next** [`Indicator::update`] call clears the
|
||||
/// running sums and previous close before folding in its own bar, starting
|
||||
/// a fresh anchored window.
|
||||
pub fn set_anchor(&mut self) {
|
||||
self.pending_anchor = true;
|
||||
}
|
||||
|
||||
/// Current anchored RSI value if at least one price change has been
|
||||
/// observed in the current anchor window.
|
||||
pub const fn value(&self) -> Option<f64> {
|
||||
self.last_value
|
||||
}
|
||||
|
||||
fn rsi_from_sums(sum_gain: f64, sum_loss: f64) -> f64 {
|
||||
if sum_loss == 0.0 {
|
||||
if sum_gain == 0.0 {
|
||||
// No movement at all -> RSI undefined; standard convention returns 50.
|
||||
50.0
|
||||
} else {
|
||||
100.0
|
||||
}
|
||||
} else {
|
||||
let rs = sum_gain / sum_loss;
|
||||
100.0 - 100.0 / (1.0 + rs)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Indicator for AnchoredRsi {
|
||||
type Input = f64;
|
||||
type Output = f64;
|
||||
|
||||
fn update(&mut self, input: f64) -> Option<f64> {
|
||||
if !input.is_finite() {
|
||||
return self.last_value;
|
||||
}
|
||||
|
||||
if self.pending_anchor {
|
||||
self.prev_close = None;
|
||||
self.sum_gain = 0.0;
|
||||
self.sum_loss = 0.0;
|
||||
self.last_value = None;
|
||||
self.pending_anchor = false;
|
||||
}
|
||||
|
||||
let Some(prev) = self.prev_close else {
|
||||
self.prev_close = Some(input);
|
||||
return None;
|
||||
};
|
||||
self.prev_close = Some(input);
|
||||
|
||||
let diff = input - prev;
|
||||
if diff > 0.0 {
|
||||
self.sum_gain += diff;
|
||||
} else if diff < 0.0 {
|
||||
self.sum_loss -= diff;
|
||||
}
|
||||
|
||||
let value = Self::rsi_from_sums(self.sum_gain, self.sum_loss);
|
||||
self.last_value = Some(value);
|
||||
Some(value)
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.prev_close = None;
|
||||
self.sum_gain = 0.0;
|
||||
self.sum_loss = 0.0;
|
||||
self.last_value = None;
|
||||
self.pending_anchor = false;
|
||||
}
|
||||
|
||||
fn warmup_period(&self) -> usize {
|
||||
2
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.last_value.is_some()
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"AnchoredRSI"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::traits::BatchExt;
|
||||
use approx::assert_relative_eq;
|
||||
|
||||
#[test]
|
||||
fn accessors_and_metadata() {
|
||||
let indicator = AnchoredRsi::new();
|
||||
assert_eq!(indicator.name(), "AnchoredRSI");
|
||||
assert_eq!(indicator.warmup_period(), 2);
|
||||
assert_eq!(indicator.value(), None);
|
||||
assert!(!indicator.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn first_bar_seeds_and_returns_none() {
|
||||
let mut indicator = AnchoredRsi::new();
|
||||
assert_eq!(indicator.update(100.0), None);
|
||||
assert!(!indicator.is_ready());
|
||||
// Second bar produces the first value.
|
||||
assert!(indicator.update(101.0).is_some());
|
||||
assert!(indicator.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pure_uptrend_saturates_at_100() {
|
||||
let mut indicator = AnchoredRsi::new();
|
||||
let out = indicator.batch(&[10.0, 11.0, 12.0, 13.0]);
|
||||
assert_relative_eq!(out[3].unwrap(), 100.0, epsilon = 1e-12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pure_downtrend_saturates_at_0() {
|
||||
let mut indicator = AnchoredRsi::new();
|
||||
let out = indicator.batch(&[13.0, 12.0, 11.0, 10.0]);
|
||||
assert_relative_eq!(out[3].unwrap(), 0.0, epsilon = 1e-12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn flat_window_reads_50() {
|
||||
let mut indicator = AnchoredRsi::new();
|
||||
let out = indicator.batch(&[42.0, 42.0, 42.0]);
|
||||
assert_relative_eq!(out[2].unwrap(), 50.0, epsilon = 1e-12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cumulative_reference_values() {
|
||||
// prices 10 -> 11 (+1) -> 9 (-2) -> 12 (+3)
|
||||
// after bar2: sum_gain=1, sum_loss=2 -> rs=0.5 -> 100 - 100/1.5 = 33.3333
|
||||
// after bar3: sum_gain=4, sum_loss=2 -> rs=2.0 -> 100 - 100/3 = 66.6667
|
||||
let mut indicator = AnchoredRsi::new();
|
||||
let out = indicator.batch(&[10.0, 11.0, 9.0, 12.0]);
|
||||
assert_relative_eq!(out[1].unwrap(), 100.0, epsilon = 1e-9);
|
||||
assert_relative_eq!(out[2].unwrap(), 33.333_333_333, epsilon = 1e-6);
|
||||
assert_relative_eq!(out[3].unwrap(), 66.666_666_666, epsilon = 1e-6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_anchor_clears_old_window() {
|
||||
// Downtrend, then re-anchor and pump an uptrend: the new window must
|
||||
// read 100, not the blended value.
|
||||
let mut indicator = AnchoredRsi::new();
|
||||
indicator.batch(&[20.0, 19.0, 18.0, 17.0]);
|
||||
assert_relative_eq!(indicator.value().unwrap(), 0.0, epsilon = 1e-12);
|
||||
indicator.set_anchor();
|
||||
// First bar after anchor re-seeds (None), second bar emits.
|
||||
assert_eq!(indicator.update(50.0), None);
|
||||
let after = indicator.update(51.0).unwrap();
|
||||
assert_relative_eq!(after, 100.0, epsilon = 1e-12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_anchor_before_first_bar_acts_as_normal_start() {
|
||||
let mut indicator = AnchoredRsi::new();
|
||||
indicator.set_anchor();
|
||||
assert_eq!(indicator.update(10.0), None);
|
||||
assert_relative_eq!(indicator.update(11.0).unwrap(), 100.0, epsilon = 1e-12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ignores_non_finite_input() {
|
||||
let mut indicator = AnchoredRsi::new();
|
||||
indicator.batch(&[10.0, 11.0, 12.0]);
|
||||
let before = indicator.value();
|
||||
assert!(before.is_some());
|
||||
assert_eq!(indicator.update(f64::NAN), before);
|
||||
assert_eq!(indicator.update(f64::INFINITY), before);
|
||||
assert_eq!(indicator.value(), before);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn non_finite_before_any_bar_returns_none() {
|
||||
let mut indicator = AnchoredRsi::new();
|
||||
assert_eq!(indicator.update(f64::NAN), None);
|
||||
assert!(!indicator.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut indicator = AnchoredRsi::new();
|
||||
indicator.batch(&[10.0, 11.0, 12.0]);
|
||||
assert!(indicator.is_ready());
|
||||
indicator.reset();
|
||||
assert!(!indicator.is_ready());
|
||||
assert_eq!(indicator.value(), None);
|
||||
assert_eq!(indicator.update(50.0), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stays_in_0_100_range() {
|
||||
let prices: Vec<f64> = (0..200)
|
||||
.map(|i| 100.0 + (f64::from(i) * 0.7).sin() * 10.0)
|
||||
.collect();
|
||||
let mut indicator = AnchoredRsi::new();
|
||||
for value in indicator.batch(&prices).into_iter().flatten() {
|
||||
assert!((0.0..=100.0).contains(&value), "RSI out of range: {value}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_equals_streaming() {
|
||||
let prices: Vec<f64> = (1..=40)
|
||||
.map(|i| (f64::from(i) * 0.3).sin() * 5.0 + f64::from(i))
|
||||
.collect();
|
||||
let mut a = AnchoredRsi::new();
|
||||
let mut b = AnchoredRsi::new();
|
||||
assert_eq!(
|
||||
a.batch(&prices),
|
||||
prices.iter().map(|p| b.update(*p)).collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,283 @@
|
||||
//! Kagi bar builder — reversal-amount line segments on close prices.
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
use crate::ohlcv::Candle;
|
||||
use crate::traits::BarBuilder;
|
||||
|
||||
/// One completed Kagi line segment (the vertical run between two reversals).
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct KagiBar {
|
||||
/// Price where the segment began (the previous reversal point).
|
||||
pub start: f64,
|
||||
/// Extreme price the segment reached before reversing.
|
||||
pub end: f64,
|
||||
/// `+1` for a rising segment, `-1` for a falling segment.
|
||||
pub direction: i8,
|
||||
}
|
||||
|
||||
/// Kagi bar builder using the fixed reversal-amount method on close prices.
|
||||
///
|
||||
/// A Kagi chart is one continuous line that extends in its current direction as
|
||||
/// long as price makes new extremes, and turns when price retraces by at least
|
||||
/// `reversal` from the latest extreme. This builder emits the **completed
|
||||
/// segment** each time the line turns:
|
||||
///
|
||||
/// - The first candle seeds the start price; the first subsequent move (of any
|
||||
/// size) sets the initial direction.
|
||||
/// - While the trend holds, new extremes extend the current segment silently.
|
||||
/// - A retracement of `>= reversal` closes the current segment (returned from
|
||||
/// [`BarBuilder::update`]) and starts a new one in the opposite direction.
|
||||
///
|
||||
/// At most one segment completes per candle, so `update` returns either an empty
|
||||
/// vector or a single [`KagiBar`].
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use wickra_core::{BarBuilder, Candle, KagiBars};
|
||||
///
|
||||
/// let flat = |price: f64| Candle::new(price, price, price, price, 1.0, 0).unwrap();
|
||||
/// let mut kagi = KagiBars::new(2.0).unwrap();
|
||||
/// kagi.update(flat(10.0)); // seed
|
||||
/// kagi.update(flat(15.0)); // rise to 15
|
||||
/// let bars = kagi.update(flat(12.0)); // retrace >= 2 -> closes the up segment
|
||||
/// assert_eq!(bars.len(), 1);
|
||||
/// assert_eq!(bars[0].direction, 1);
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct KagiBars {
|
||||
reversal: f64,
|
||||
dir: i8,
|
||||
extreme: Option<f64>,
|
||||
segment_start: f64,
|
||||
}
|
||||
|
||||
impl KagiBars {
|
||||
/// Construct a Kagi builder with the given reversal amount.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`Error::InvalidPeriod`] if `reversal` is not finite and positive.
|
||||
pub fn new(reversal: f64) -> Result<Self> {
|
||||
if !reversal.is_finite() || reversal <= 0.0 {
|
||||
return Err(Error::InvalidPeriod {
|
||||
message: "reversal must be finite and positive",
|
||||
});
|
||||
}
|
||||
Ok(Self {
|
||||
reversal,
|
||||
dir: 0,
|
||||
extreme: None,
|
||||
segment_start: 0.0,
|
||||
})
|
||||
}
|
||||
|
||||
/// Configured reversal amount.
|
||||
pub const fn reversal(&self) -> f64 {
|
||||
self.reversal
|
||||
}
|
||||
|
||||
/// Current extreme price (or the seed price before any move).
|
||||
pub const fn extreme(&self) -> Option<f64> {
|
||||
self.extreme
|
||||
}
|
||||
}
|
||||
|
||||
impl BarBuilder for KagiBars {
|
||||
type Bar = KagiBar;
|
||||
|
||||
fn update(&mut self, candle: Candle) -> Vec<KagiBar> {
|
||||
let close = candle.close;
|
||||
let Some(mut ext) = self.extreme else {
|
||||
self.extreme = Some(close);
|
||||
self.segment_start = close;
|
||||
return Vec::new();
|
||||
};
|
||||
let mut bars = Vec::new();
|
||||
match self.dir {
|
||||
0 => {
|
||||
if close > ext {
|
||||
self.dir = 1;
|
||||
ext = close;
|
||||
} else if close < ext {
|
||||
self.dir = -1;
|
||||
ext = close;
|
||||
}
|
||||
}
|
||||
1 => {
|
||||
if close > ext {
|
||||
ext = close;
|
||||
} else if close <= ext - self.reversal {
|
||||
bars.push(KagiBar {
|
||||
start: self.segment_start,
|
||||
end: ext,
|
||||
direction: 1,
|
||||
});
|
||||
self.segment_start = ext;
|
||||
self.dir = -1;
|
||||
ext = close;
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
if close < ext {
|
||||
ext = close;
|
||||
} else if close >= ext + self.reversal {
|
||||
bars.push(KagiBar {
|
||||
start: self.segment_start,
|
||||
end: ext,
|
||||
direction: -1,
|
||||
});
|
||||
self.segment_start = ext;
|
||||
self.dir = 1;
|
||||
ext = close;
|
||||
}
|
||||
}
|
||||
}
|
||||
self.extreme = Some(ext);
|
||||
bars
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.dir = 0;
|
||||
self.extreme = None;
|
||||
self.segment_start = 0.0;
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"KagiBars"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use approx::assert_relative_eq;
|
||||
|
||||
fn flat(price: f64) -> Candle {
|
||||
Candle::new(price, price, price, price, 1.0, 0).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_invalid_reversal() {
|
||||
assert!(matches!(
|
||||
KagiBars::new(0.0),
|
||||
Err(Error::InvalidPeriod { .. })
|
||||
));
|
||||
assert!(matches!(
|
||||
KagiBars::new(-2.0),
|
||||
Err(Error::InvalidPeriod { .. })
|
||||
));
|
||||
assert!(matches!(
|
||||
KagiBars::new(f64::INFINITY),
|
||||
Err(Error::InvalidPeriod { .. })
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accessors_and_metadata() {
|
||||
let kagi = KagiBars::new(2.0).unwrap();
|
||||
assert_eq!(kagi.name(), "KagiBars");
|
||||
assert_relative_eq!(kagi.reversal(), 2.0, epsilon = 1e-12);
|
||||
assert_eq!(kagi.extreme(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn seeds_then_establishes_up_direction() {
|
||||
let mut kagi = KagiBars::new(2.0).unwrap();
|
||||
assert!(kagi.update(flat(10.0)).is_empty()); // seed
|
||||
assert_eq!(kagi.extreme(), Some(10.0));
|
||||
assert!(kagi.update(flat(11.0)).is_empty()); // first move sets dir up
|
||||
assert_eq!(kagi.extreme(), Some(11.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn establishes_down_direction_from_seed() {
|
||||
let mut kagi = KagiBars::new(2.0).unwrap();
|
||||
kagi.update(flat(10.0));
|
||||
assert!(kagi.update(flat(9.0)).is_empty()); // first move sets dir down
|
||||
assert_eq!(kagi.extreme(), Some(9.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extends_without_emitting() {
|
||||
let mut kagi = KagiBars::new(2.0).unwrap();
|
||||
kagi.update(flat(10.0));
|
||||
kagi.update(flat(11.0));
|
||||
assert!(kagi.update(flat(15.0)).is_empty()); // new high, extend
|
||||
assert_eq!(kagi.extreme(), Some(15.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reversal_closes_up_segment() {
|
||||
let mut kagi = KagiBars::new(2.0).unwrap();
|
||||
kagi.update(flat(10.0));
|
||||
kagi.update(flat(11.0));
|
||||
kagi.update(flat(15.0));
|
||||
let bars = kagi.update(flat(12.0)); // retrace 3 >= 2
|
||||
assert_eq!(bars.len(), 1);
|
||||
assert_eq!(bars[0].direction, 1);
|
||||
assert_relative_eq!(bars[0].start, 10.0, epsilon = 1e-12);
|
||||
assert_relative_eq!(bars[0].end, 15.0, epsilon = 1e-12);
|
||||
assert_eq!(kagi.extreme(), Some(12.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reversal_closes_down_segment() {
|
||||
let mut kagi = KagiBars::new(2.0).unwrap();
|
||||
kagi.update(flat(10.0));
|
||||
kagi.update(flat(11.0));
|
||||
kagi.update(flat(15.0));
|
||||
kagi.update(flat(12.0)); // now dir down, segment_start 15, extreme 12
|
||||
let bars = kagi.update(flat(20.0)); // rise 8 >= 2 -> closes down segment
|
||||
assert_eq!(bars.len(), 1);
|
||||
assert_eq!(bars[0].direction, -1);
|
||||
assert_relative_eq!(bars[0].start, 15.0, epsilon = 1e-12);
|
||||
assert_relative_eq!(bars[0].end, 12.0, epsilon = 1e-12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn small_pullback_does_not_reverse() {
|
||||
let mut kagi = KagiBars::new(2.0).unwrap();
|
||||
kagi.update(flat(10.0));
|
||||
kagi.update(flat(11.0));
|
||||
kagi.update(flat(15.0));
|
||||
assert!(kagi.update(flat(14.0)).is_empty()); // retrace 1 < 2
|
||||
assert_eq!(kagi.extreme(), Some(15.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn down_trend_small_bounce_does_not_reverse() {
|
||||
let mut kagi = KagiBars::new(2.0).unwrap();
|
||||
kagi.update(flat(10.0));
|
||||
kagi.update(flat(9.0)); // dir down
|
||||
kagi.update(flat(5.0)); // extreme 5
|
||||
assert!(kagi.update(flat(6.0)).is_empty()); // bounce 1 < 2
|
||||
assert_eq!(kagi.extreme(), Some(5.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut kagi = KagiBars::new(2.0).unwrap();
|
||||
kagi.update(flat(10.0));
|
||||
kagi.update(flat(15.0));
|
||||
kagi.reset();
|
||||
assert_eq!(kagi.extreme(), None);
|
||||
assert!(kagi.update(flat(99.0)).is_empty());
|
||||
assert_eq!(kagi.extreme(), Some(99.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_collects_completed_segments() {
|
||||
let mut kagi = KagiBars::new(2.0).unwrap();
|
||||
let candles = [
|
||||
flat(10.0),
|
||||
flat(15.0),
|
||||
flat(12.0), // closes up segment
|
||||
flat(20.0), // closes down segment
|
||||
];
|
||||
let bars = kagi.batch(&candles);
|
||||
assert_eq!(bars.len(), 2);
|
||||
assert_eq!(bars[0].direction, 1);
|
||||
assert_eq!(bars[1].direction, -1);
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,7 @@ mod adxr;
|
||||
mod alligator;
|
||||
mod alma;
|
||||
mod alpha;
|
||||
mod anchored_rsi;
|
||||
mod anchored_vwap;
|
||||
mod apo;
|
||||
mod aroon;
|
||||
@@ -122,6 +123,7 @@ mod instantaneous_trendline;
|
||||
mod inverse_fisher_transform;
|
||||
mod inverted_hammer;
|
||||
mod jma;
|
||||
mod kagi_bars;
|
||||
mod kama;
|
||||
mod kelly_criterion;
|
||||
mod keltner;
|
||||
@@ -182,6 +184,7 @@ mod percentage_trailing_stop;
|
||||
mod pgo;
|
||||
mod piercing_dark_cloud;
|
||||
mod pmo;
|
||||
mod point_and_figure_bars;
|
||||
mod ppo;
|
||||
mod profit_factor;
|
||||
mod psar;
|
||||
@@ -191,6 +194,7 @@ mod r_squared;
|
||||
mod realized_spread;
|
||||
mod recovery_factor;
|
||||
mod relative_strength_ab;
|
||||
mod renko_bars;
|
||||
mod renko_trailing_stop;
|
||||
mod rickshaw_man;
|
||||
mod rising_three_methods;
|
||||
@@ -251,6 +255,7 @@ mod three_soldiers_or_crows;
|
||||
mod three_stars_in_south;
|
||||
mod thrusting;
|
||||
mod tii;
|
||||
mod tpo_profile;
|
||||
mod trade_imbalance;
|
||||
mod treynor_ratio;
|
||||
mod trima;
|
||||
@@ -274,6 +279,7 @@ mod vertical_horizontal_filter;
|
||||
mod vidya;
|
||||
mod volty_stop;
|
||||
mod volume_oscillator;
|
||||
mod volume_profile;
|
||||
mod vortex;
|
||||
mod vpt;
|
||||
mod vwap;
|
||||
@@ -305,6 +311,7 @@ pub use adxr::Adxr;
|
||||
pub use alligator::{Alligator, AlligatorOutput};
|
||||
pub use alma::Alma;
|
||||
pub use alpha::Alpha;
|
||||
pub use anchored_rsi::AnchoredRsi;
|
||||
pub use anchored_vwap::AnchoredVwap;
|
||||
pub use apo::Apo;
|
||||
pub use aroon::{Aroon, AroonOutput};
|
||||
@@ -411,6 +418,7 @@ pub use instantaneous_trendline::InstantaneousTrendline;
|
||||
pub use inverse_fisher_transform::InverseFisherTransform;
|
||||
pub use inverted_hammer::InvertedHammer;
|
||||
pub use jma::Jma;
|
||||
pub use kagi_bars::{KagiBar, KagiBars};
|
||||
pub use kama::Kama;
|
||||
pub use kelly_criterion::KellyCriterion;
|
||||
pub use keltner::{Keltner, KeltnerOutput};
|
||||
@@ -471,6 +479,7 @@ pub use percentage_trailing_stop::PercentageTrailingStop;
|
||||
pub use pgo::Pgo;
|
||||
pub use piercing_dark_cloud::PiercingDarkCloud;
|
||||
pub use pmo::Pmo;
|
||||
pub use point_and_figure_bars::{PnfColumn, PointAndFigureBars};
|
||||
pub use ppo::Ppo;
|
||||
pub use profit_factor::ProfitFactor;
|
||||
pub use psar::Psar;
|
||||
@@ -480,6 +489,7 @@ pub use r_squared::RSquared;
|
||||
pub use realized_spread::RealizedSpread;
|
||||
pub use recovery_factor::RecoveryFactor;
|
||||
pub use relative_strength_ab::{RelativeStrengthAB, RelativeStrengthOutput};
|
||||
pub use renko_bars::{RenkoBars, RenkoBrick};
|
||||
pub use renko_trailing_stop::RenkoTrailingStop;
|
||||
pub use rickshaw_man::RickshawMan;
|
||||
pub use rising_three_methods::RisingThreeMethods;
|
||||
@@ -540,6 +550,7 @@ pub use three_soldiers_or_crows::ThreeSoldiersOrCrows;
|
||||
pub use three_stars_in_south::ThreeStarsInSouth;
|
||||
pub use thrusting::Thrusting;
|
||||
pub use tii::Tii;
|
||||
pub use tpo_profile::{TpoProfile, TpoProfileOutput};
|
||||
pub use trade_imbalance::TradeImbalance;
|
||||
pub use treynor_ratio::TreynorRatio;
|
||||
pub use trima::Trima;
|
||||
@@ -563,6 +574,7 @@ pub use vertical_horizontal_filter::VerticalHorizontalFilter;
|
||||
pub use vidya::Vidya;
|
||||
pub use volty_stop::VoltyStop;
|
||||
pub use volume_oscillator::VolumeOscillator;
|
||||
pub use volume_profile::{VolumeProfile, VolumeProfileOutput};
|
||||
pub use vortex::{Vortex, VortexOutput};
|
||||
pub use vpt::VolumePriceTrend;
|
||||
pub use vwap::{RollingVwap, Vwap};
|
||||
@@ -617,6 +629,7 @@ pub const FAMILIES: &[(&str, &[&str])] = &[
|
||||
"Momentum Oscillators",
|
||||
&[
|
||||
"Rsi",
|
||||
"AnchoredRsi",
|
||||
"Stochastic",
|
||||
"Cci",
|
||||
"Roc",
|
||||
@@ -929,7 +942,13 @@ pub const FAMILIES: &[(&str, &[&str])] = &[
|
||||
),
|
||||
(
|
||||
"Market Profile",
|
||||
&["ValueArea", "InitialBalance", "OpeningRange"],
|
||||
&[
|
||||
"ValueArea",
|
||||
"InitialBalance",
|
||||
"OpeningRange",
|
||||
"VolumeProfile",
|
||||
"TpoProfile",
|
||||
],
|
||||
),
|
||||
(
|
||||
"Risk / Performance",
|
||||
@@ -953,6 +972,10 @@ pub const FAMILIES: &[(&str, &[&str])] = &[
|
||||
"Alpha",
|
||||
],
|
||||
),
|
||||
(
|
||||
"Alt-Chart Bars",
|
||||
&["RenkoBars", "KagiBars", "PointAndFigureBars"],
|
||||
),
|
||||
];
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -981,6 +1004,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, 284, "FAMILIES total drifted from indicator count");
|
||||
assert_eq!(total, 290, "FAMILIES total drifted from indicator count");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,296 @@
|
||||
//! Point-and-Figure bar builder — box-size columns with an N-box reversal.
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
use crate::ohlcv::Candle;
|
||||
use crate::traits::BarBuilder;
|
||||
|
||||
/// One completed Point-and-Figure column.
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct PnfColumn {
|
||||
/// `+1` for a rising (X) column, `-1` for a falling (O) column.
|
||||
pub direction: i8,
|
||||
/// Upper box edge of the column.
|
||||
pub high: f64,
|
||||
/// Lower box edge of the column.
|
||||
pub low: f64,
|
||||
}
|
||||
|
||||
/// Point-and-Figure bar builder using the fixed box-size, N-box reversal method.
|
||||
///
|
||||
/// Price is quantised to a `box_size` grid (each close maps to the box that
|
||||
/// contains it). An X column extends upward while price makes new box highs; an
|
||||
/// O column extends downward while price makes new box lows. A reversal needs
|
||||
/// price to move `reversal` boxes against the column, at which point the current
|
||||
/// column is closed (returned from [`BarBuilder::update`]) and a new column
|
||||
/// starts one box offset from the prior extreme.
|
||||
///
|
||||
/// - The first candle seeds the grid box and prints no column.
|
||||
/// - The first one-box move sets the initial column direction.
|
||||
/// - At most one column completes per candle, so `update` returns an empty
|
||||
/// vector or a single [`PnfColumn`].
|
||||
///
|
||||
/// Closes are mapped to their containing box via `floor(close / box_size)` for
|
||||
/// both directions, so the construction is fully deterministic.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use wickra_core::{BarBuilder, Candle, PointAndFigureBars};
|
||||
///
|
||||
/// let flat = |price: f64| Candle::new(price, price, price, price, 1.0, 0).unwrap();
|
||||
/// let mut pnf = PointAndFigureBars::new(1.0, 3).unwrap();
|
||||
/// pnf.update(flat(10.0)); // seed
|
||||
/// pnf.update(flat(15.0)); // X column up to 15
|
||||
/// let cols = pnf.update(flat(12.0)); // 3-box reversal closes the X column
|
||||
/// assert_eq!(cols.len(), 1);
|
||||
/// assert_eq!(cols[0].direction, 1);
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PointAndFigureBars {
|
||||
box_size: f64,
|
||||
reversal: usize,
|
||||
dir: i8,
|
||||
col_top: f64,
|
||||
col_bottom: f64,
|
||||
seeded: bool,
|
||||
}
|
||||
|
||||
impl PointAndFigureBars {
|
||||
/// Construct a Point-and-Figure builder with the given box size and reversal
|
||||
/// (in boxes).
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`Error::InvalidPeriod`] if `box_size` is not finite and positive,
|
||||
/// and [`Error::PeriodZero`] if `reversal` is zero.
|
||||
pub fn new(box_size: f64, reversal: usize) -> Result<Self> {
|
||||
if !box_size.is_finite() || box_size <= 0.0 {
|
||||
return Err(Error::InvalidPeriod {
|
||||
message: "box_size must be finite and positive",
|
||||
});
|
||||
}
|
||||
if reversal == 0 {
|
||||
return Err(Error::PeriodZero);
|
||||
}
|
||||
Ok(Self {
|
||||
box_size,
|
||||
reversal,
|
||||
dir: 0,
|
||||
col_top: 0.0,
|
||||
col_bottom: 0.0,
|
||||
seeded: false,
|
||||
})
|
||||
}
|
||||
|
||||
/// Configured box size.
|
||||
pub const fn box_size(&self) -> f64 {
|
||||
self.box_size
|
||||
}
|
||||
|
||||
/// Configured reversal, in boxes.
|
||||
pub const fn reversal(&self) -> usize {
|
||||
self.reversal
|
||||
}
|
||||
|
||||
fn floor_box(&self, price: f64) -> f64 {
|
||||
(price / self.box_size).floor() * self.box_size
|
||||
}
|
||||
}
|
||||
|
||||
impl BarBuilder for PointAndFigureBars {
|
||||
type Bar = PnfColumn;
|
||||
|
||||
fn update(&mut self, candle: Candle) -> Vec<PnfColumn> {
|
||||
let box_price = self.floor_box(candle.close);
|
||||
if !self.seeded {
|
||||
self.seeded = true;
|
||||
self.col_top = box_price;
|
||||
self.col_bottom = box_price;
|
||||
return Vec::new();
|
||||
}
|
||||
let box_size = self.box_size;
|
||||
let reversal = self.reversal as f64 * box_size;
|
||||
let mut cols = Vec::new();
|
||||
match self.dir {
|
||||
0 => {
|
||||
if box_price >= self.col_top + box_size {
|
||||
self.dir = 1;
|
||||
self.col_top = box_price;
|
||||
} else if box_price <= self.col_bottom - box_size {
|
||||
self.dir = -1;
|
||||
self.col_bottom = box_price;
|
||||
}
|
||||
}
|
||||
1 => {
|
||||
if box_price > self.col_top {
|
||||
self.col_top = box_price;
|
||||
} else if box_price <= self.col_top - reversal {
|
||||
cols.push(PnfColumn {
|
||||
direction: 1,
|
||||
high: self.col_top,
|
||||
low: self.col_bottom,
|
||||
});
|
||||
self.dir = -1;
|
||||
self.col_top -= box_size;
|
||||
self.col_bottom = box_price;
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
if box_price < self.col_bottom {
|
||||
self.col_bottom = box_price;
|
||||
} else if box_price >= self.col_bottom + reversal {
|
||||
cols.push(PnfColumn {
|
||||
direction: -1,
|
||||
high: self.col_top,
|
||||
low: self.col_bottom,
|
||||
});
|
||||
self.dir = 1;
|
||||
self.col_bottom += box_size;
|
||||
self.col_top = box_price;
|
||||
}
|
||||
}
|
||||
}
|
||||
cols
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.dir = 0;
|
||||
self.col_top = 0.0;
|
||||
self.col_bottom = 0.0;
|
||||
self.seeded = false;
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"PointAndFigureBars"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use approx::assert_relative_eq;
|
||||
|
||||
fn flat(price: f64) -> Candle {
|
||||
Candle::new(price, price, price, price, 1.0, 0).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_invalid_box_size() {
|
||||
assert!(matches!(
|
||||
PointAndFigureBars::new(0.0, 3),
|
||||
Err(Error::InvalidPeriod { .. })
|
||||
));
|
||||
assert!(matches!(
|
||||
PointAndFigureBars::new(f64::NAN, 3),
|
||||
Err(Error::InvalidPeriod { .. })
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_zero_reversal() {
|
||||
assert!(matches!(
|
||||
PointAndFigureBars::new(1.0, 0),
|
||||
Err(Error::PeriodZero)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accessors_and_metadata() {
|
||||
let pnf = PointAndFigureBars::new(0.5, 3).unwrap();
|
||||
assert_eq!(pnf.name(), "PointAndFigureBars");
|
||||
assert_relative_eq!(pnf.box_size(), 0.5, epsilon = 1e-12);
|
||||
assert_eq!(pnf.reversal(), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn first_candle_seeds_without_column() {
|
||||
let mut pnf = PointAndFigureBars::new(1.0, 3).unwrap();
|
||||
assert!(pnf.update(flat(10.0)).is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn establishes_up_then_extends() {
|
||||
let mut pnf = PointAndFigureBars::new(1.0, 3).unwrap();
|
||||
pnf.update(flat(10.0));
|
||||
assert!(pnf.update(flat(13.0)).is_empty()); // start X column
|
||||
assert!(pnf.update(flat(15.0)).is_empty()); // extend up, no completed column
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn establishes_down_direction() {
|
||||
let mut pnf = PointAndFigureBars::new(1.0, 3).unwrap();
|
||||
pnf.update(flat(10.0));
|
||||
assert!(pnf.update(flat(7.0)).is_empty()); // start O column
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reversal_closes_x_column() {
|
||||
let mut pnf = PointAndFigureBars::new(1.0, 3).unwrap();
|
||||
pnf.update(flat(10.0));
|
||||
pnf.update(flat(13.0));
|
||||
pnf.update(flat(15.0));
|
||||
let cols = pnf.update(flat(12.0)); // 3-box drop from 15
|
||||
assert_eq!(cols.len(), 1);
|
||||
assert_eq!(cols[0].direction, 1);
|
||||
assert_relative_eq!(cols[0].high, 15.0, epsilon = 1e-12);
|
||||
assert_relative_eq!(cols[0].low, 10.0, epsilon = 1e-12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reversal_closes_o_column() {
|
||||
let mut pnf = PointAndFigureBars::new(1.0, 3).unwrap();
|
||||
pnf.update(flat(10.0));
|
||||
pnf.update(flat(13.0));
|
||||
pnf.update(flat(15.0));
|
||||
pnf.update(flat(12.0)); // now O column from 14 down
|
||||
pnf.update(flat(10.0)); // extend O down to 10
|
||||
let cols = pnf.update(flat(15.0)); // 3-box rise -> closes O column
|
||||
assert_eq!(cols.len(), 1);
|
||||
assert_eq!(cols[0].direction, -1);
|
||||
assert_relative_eq!(cols[0].low, 10.0, epsilon = 1e-12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn small_move_prints_nothing() {
|
||||
let mut pnf = PointAndFigureBars::new(1.0, 3).unwrap();
|
||||
pnf.update(flat(10.0));
|
||||
pnf.update(flat(13.0));
|
||||
pnf.update(flat(15.0));
|
||||
assert!(pnf.update(flat(14.0)).is_empty()); // 1-box pullback < 3
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn down_column_small_bounce_prints_nothing() {
|
||||
let mut pnf = PointAndFigureBars::new(1.0, 3).unwrap();
|
||||
pnf.update(flat(10.0));
|
||||
pnf.update(flat(7.0)); // O column
|
||||
pnf.update(flat(5.0)); // extend down
|
||||
assert!(pnf.update(flat(6.0)).is_empty()); // 1-box bounce < 3
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut pnf = PointAndFigureBars::new(1.0, 3).unwrap();
|
||||
pnf.update(flat(10.0));
|
||||
pnf.update(flat(15.0));
|
||||
pnf.reset();
|
||||
assert!(pnf.update(flat(99.0)).is_empty()); // re-seeds
|
||||
assert!(pnf.update(flat(100.0)).is_empty()); // first move after reseed
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_collects_completed_columns() {
|
||||
let mut pnf = PointAndFigureBars::new(1.0, 3).unwrap();
|
||||
let candles = [
|
||||
flat(10.0),
|
||||
flat(15.0), // X column
|
||||
flat(12.0), // reversal -> closes X
|
||||
flat(9.0), // extend O
|
||||
flat(15.0), // reversal -> closes O
|
||||
];
|
||||
let cols = pnf.batch(&candles);
|
||||
assert_eq!(cols.len(), 2);
|
||||
assert_eq!(cols[0].direction, 1);
|
||||
assert_eq!(cols[1].direction, -1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,267 @@
|
||||
//! Renko bar builder — fixed box-size bricks with the classic reversal rule.
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
use crate::ohlcv::Candle;
|
||||
use crate::traits::BarBuilder;
|
||||
|
||||
/// One completed Renko brick.
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct RenkoBrick {
|
||||
/// Price at the brick's origin edge.
|
||||
pub open: f64,
|
||||
/// Price at the brick's far edge (`open ± box_size`).
|
||||
pub close: f64,
|
||||
/// `+1` for an up brick, `-1` for a down brick.
|
||||
pub direction: i8,
|
||||
}
|
||||
|
||||
/// Renko bar builder using the fixed box-size method on close prices.
|
||||
///
|
||||
/// Construction follows the classic Renko rules:
|
||||
///
|
||||
/// - The first candle seeds the reference level and prints no brick.
|
||||
/// - While the trend continues, every additional `box_size` of close movement
|
||||
/// prints one more brick in the trend direction.
|
||||
/// - A reversal requires `2 * box_size` against the trend (one box to unwind the
|
||||
/// last brick's body, one to print the first opposite brick); thereafter each
|
||||
/// further `box_size` prints another brick.
|
||||
///
|
||||
/// A single candle whose close gaps several boxes prints all the bricks it
|
||||
/// completes in one [`BarBuilder::update`] call. Bricks are perfectly aligned to
|
||||
/// the `box_size` grid relative to the seed price.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use wickra_core::{BarBuilder, Candle, RenkoBars};
|
||||
///
|
||||
/// let flat = |price: f64| Candle::new(price, price, price, price, 1.0, 0).unwrap();
|
||||
/// let mut renko = RenkoBars::new(1.0).unwrap();
|
||||
/// assert!(renko.update(flat(10.0)).is_empty()); // seed
|
||||
/// let bricks = renko.update(flat(13.0)); // +3 boxes
|
||||
/// assert_eq!(bricks.len(), 3);
|
||||
/// assert!(bricks.iter().all(|b| b.direction == 1));
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RenkoBars {
|
||||
box_size: f64,
|
||||
level: Option<f64>,
|
||||
dir: i8,
|
||||
}
|
||||
|
||||
impl RenkoBars {
|
||||
/// Construct a Renko builder with the given brick size.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`Error::InvalidPeriod`] if `box_size` is not finite and positive.
|
||||
pub fn new(box_size: f64) -> Result<Self> {
|
||||
if !box_size.is_finite() || box_size <= 0.0 {
|
||||
return Err(Error::InvalidPeriod {
|
||||
message: "box_size must be finite and positive",
|
||||
});
|
||||
}
|
||||
Ok(Self {
|
||||
box_size,
|
||||
level: None,
|
||||
dir: 0,
|
||||
})
|
||||
}
|
||||
|
||||
/// Configured brick size.
|
||||
pub const fn box_size(&self) -> f64 {
|
||||
self.box_size
|
||||
}
|
||||
|
||||
/// Current reference level (the close of the last completed brick, or the
|
||||
/// seed price before any brick has formed).
|
||||
pub const fn level(&self) -> Option<f64> {
|
||||
self.level
|
||||
}
|
||||
}
|
||||
|
||||
impl BarBuilder for RenkoBars {
|
||||
type Bar = RenkoBrick;
|
||||
|
||||
fn update(&mut self, candle: Candle) -> Vec<RenkoBrick> {
|
||||
let close = candle.close;
|
||||
let Some(mut level) = self.level else {
|
||||
self.level = Some(close);
|
||||
return Vec::new();
|
||||
};
|
||||
let box_size = self.box_size;
|
||||
let two = 2.0 * box_size;
|
||||
let mut bricks = Vec::new();
|
||||
loop {
|
||||
if self.dir >= 0 && close >= level + box_size {
|
||||
bricks.push(RenkoBrick {
|
||||
open: level,
|
||||
close: level + box_size,
|
||||
direction: 1,
|
||||
});
|
||||
level += box_size;
|
||||
self.dir = 1;
|
||||
} else if self.dir <= 0 && close <= level - box_size {
|
||||
bricks.push(RenkoBrick {
|
||||
open: level,
|
||||
close: level - box_size,
|
||||
direction: -1,
|
||||
});
|
||||
level -= box_size;
|
||||
self.dir = -1;
|
||||
} else if self.dir > 0 && close <= level - two {
|
||||
bricks.push(RenkoBrick {
|
||||
open: level - box_size,
|
||||
close: level - two,
|
||||
direction: -1,
|
||||
});
|
||||
level -= two;
|
||||
self.dir = -1;
|
||||
} else if self.dir < 0 && close >= level + two {
|
||||
bricks.push(RenkoBrick {
|
||||
open: level + box_size,
|
||||
close: level + two,
|
||||
direction: 1,
|
||||
});
|
||||
level += two;
|
||||
self.dir = 1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
self.level = Some(level);
|
||||
bricks
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.level = None;
|
||||
self.dir = 0;
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"RenkoBars"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use approx::assert_relative_eq;
|
||||
|
||||
fn flat(price: f64) -> Candle {
|
||||
Candle::new(price, price, price, price, 1.0, 0).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_invalid_box_size() {
|
||||
assert!(matches!(
|
||||
RenkoBars::new(0.0),
|
||||
Err(Error::InvalidPeriod { .. })
|
||||
));
|
||||
assert!(matches!(
|
||||
RenkoBars::new(-1.0),
|
||||
Err(Error::InvalidPeriod { .. })
|
||||
));
|
||||
assert!(matches!(
|
||||
RenkoBars::new(f64::NAN),
|
||||
Err(Error::InvalidPeriod { .. })
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accessors_and_metadata() {
|
||||
let renko = RenkoBars::new(2.5).unwrap();
|
||||
assert_eq!(renko.name(), "RenkoBars");
|
||||
assert_relative_eq!(renko.box_size(), 2.5, epsilon = 1e-12);
|
||||
assert_eq!(renko.level(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn first_candle_seeds_without_brick() {
|
||||
let mut renko = RenkoBars::new(1.0).unwrap();
|
||||
assert!(renko.update(flat(10.0)).is_empty());
|
||||
assert_eq!(renko.level(), Some(10.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn up_trend_prints_aligned_bricks() {
|
||||
let mut renko = RenkoBars::new(1.0).unwrap();
|
||||
renko.update(flat(10.0));
|
||||
let bricks = renko.update(flat(13.0));
|
||||
assert_eq!(bricks.len(), 3);
|
||||
assert_relative_eq!(bricks[0].open, 10.0, epsilon = 1e-12);
|
||||
assert_relative_eq!(bricks[0].close, 11.0, epsilon = 1e-12);
|
||||
assert_relative_eq!(bricks[2].close, 13.0, epsilon = 1e-12);
|
||||
assert!(bricks.iter().all(|b| b.direction == 1));
|
||||
assert_eq!(renko.level(), Some(13.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn down_trend_prints_aligned_bricks() {
|
||||
let mut renko = RenkoBars::new(1.0).unwrap();
|
||||
renko.update(flat(10.0));
|
||||
let bricks = renko.update(flat(7.0));
|
||||
assert_eq!(bricks.len(), 3);
|
||||
assert!(bricks.iter().all(|b| b.direction == -1));
|
||||
assert_relative_eq!(bricks[2].close, 7.0, epsilon = 1e-12);
|
||||
assert_eq!(renko.level(), Some(7.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reversal_down_needs_two_boxes() {
|
||||
let mut renko = RenkoBars::new(1.0).unwrap();
|
||||
renko.update(flat(10.0));
|
||||
renko.update(flat(13.0)); // level 13, dir up
|
||||
let bricks = renko.update(flat(10.0)); // drop of 3 -> reversal eats one box
|
||||
assert_eq!(bricks.len(), 2);
|
||||
assert!(bricks.iter().all(|b| b.direction == -1));
|
||||
assert_relative_eq!(bricks[0].open, 12.0, epsilon = 1e-12);
|
||||
assert_relative_eq!(bricks[0].close, 11.0, epsilon = 1e-12);
|
||||
assert_relative_eq!(bricks[1].close, 10.0, epsilon = 1e-12);
|
||||
assert_eq!(renko.level(), Some(10.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reversal_up_needs_two_boxes() {
|
||||
let mut renko = RenkoBars::new(1.0).unwrap();
|
||||
renko.update(flat(10.0));
|
||||
renko.update(flat(7.0)); // level 7, dir down
|
||||
let bricks = renko.update(flat(10.0)); // rise of 3 -> reversal
|
||||
assert_eq!(bricks.len(), 2);
|
||||
assert!(bricks.iter().all(|b| b.direction == 1));
|
||||
assert_relative_eq!(bricks[0].open, 8.0, epsilon = 1e-12);
|
||||
assert_relative_eq!(bricks[0].close, 9.0, epsilon = 1e-12);
|
||||
assert_relative_eq!(bricks[1].close, 10.0, epsilon = 1e-12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn small_move_prints_nothing() {
|
||||
let mut renko = RenkoBars::new(1.0).unwrap();
|
||||
renko.update(flat(10.0));
|
||||
renko.update(flat(13.0));
|
||||
assert!(renko.update(flat(12.5)).is_empty()); // less than a reversal
|
||||
assert_eq!(renko.level(), Some(13.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut renko = RenkoBars::new(1.0).unwrap();
|
||||
renko.update(flat(10.0));
|
||||
renko.update(flat(13.0));
|
||||
renko.reset();
|
||||
assert_eq!(renko.level(), None);
|
||||
// After reset the next candle seeds again.
|
||||
assert!(renko.update(flat(50.0)).is_empty());
|
||||
assert_eq!(renko.level(), Some(50.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_concatenates_completed_bricks() {
|
||||
let mut renko = RenkoBars::new(1.0).unwrap();
|
||||
let candles = [flat(10.0), flat(12.0), flat(13.0)];
|
||||
let bricks = renko.batch(&candles);
|
||||
// seed at 10, then +2 then +1 => 3 up bricks total.
|
||||
assert_eq!(bricks.len(), 3);
|
||||
assert!(bricks.iter().all(|b| b.direction == 1));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,314 @@
|
||||
//! TPO Profile — the Time-Price-Opportunity (market-profile letter) distribution.
|
||||
|
||||
use std::collections::VecDeque;
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
use crate::ohlcv::Candle;
|
||||
use crate::traits::Indicator;
|
||||
|
||||
/// TPO Profile output: the price domain plus the per-bin time-period counts.
|
||||
///
|
||||
/// `counts[i]` is the number of periods in the rolling window whose `[low, high]`
|
||||
/// range touched the price bucket `[price_low + i * w, price_low + (i + 1) * w)`
|
||||
/// where `w = (price_high - price_low) / counts.len()`. This is the classic
|
||||
/// market-profile "letter" count: one Time-Price-Opportunity per period per
|
||||
/// price level it traded at, independent of volume.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct TpoProfileOutput {
|
||||
/// Lowest price in the window — the lower edge of bin 0.
|
||||
pub price_low: f64,
|
||||
/// Highest price in the window — the upper edge of the last bin.
|
||||
pub price_high: f64,
|
||||
/// Per-bin TPO count, lowest price bucket first. Length equals `bin_count`.
|
||||
pub counts: Vec<f64>,
|
||||
}
|
||||
|
||||
/// Rolling TPO (Time Price Opportunity) Profile over the last `period` candles.
|
||||
///
|
||||
/// Where [`crate::VolumeProfile`] distributes each bar's *volume* across the
|
||||
/// bins it touches, the TPO profile counts *time*: every period that trades at a
|
||||
/// price level contributes exactly one TPO mark there, regardless of how much
|
||||
/// volume it carried. The result highlights the prices the market spent the most
|
||||
/// time at — the market-profile bell curve. Each touched bin receives a full
|
||||
/// `+1` per period (no sharing), so a wide-range bar marks every level it spans.
|
||||
///
|
||||
/// A window whose bars are all single-print at one price (`price_high == price_low`)
|
||||
/// is degenerate: every period's mark lands in bin 0 and both edges collapse to
|
||||
/// that price.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use wickra_core::{Candle, Indicator, TpoProfile};
|
||||
///
|
||||
/// let mut tpo = TpoProfile::new(5, 10).unwrap();
|
||||
/// let mut last = None;
|
||||
/// for i in 0..10 {
|
||||
/// let base = 100.0 + f64::from(i);
|
||||
/// let candle =
|
||||
/// Candle::new(base, base + 2.0, base - 2.0, base, 10.0, i64::from(i)).unwrap();
|
||||
/// last = tpo.update(candle);
|
||||
/// }
|
||||
/// let profile = last.unwrap();
|
||||
/// assert_eq!(profile.counts.len(), 10);
|
||||
/// ```
|
||||
#[allow(clippy::struct_field_names)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TpoProfile {
|
||||
period: usize,
|
||||
bin_count: usize,
|
||||
window: VecDeque<Candle>,
|
||||
last: Option<TpoProfileOutput>,
|
||||
}
|
||||
|
||||
impl TpoProfile {
|
||||
/// Construct a TPO Profile indicator.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`Error::PeriodZero`] if `period` or `bin_count` is zero.
|
||||
pub fn new(period: usize, bin_count: usize) -> Result<Self> {
|
||||
if period == 0 || bin_count == 0 {
|
||||
return Err(Error::PeriodZero);
|
||||
}
|
||||
Ok(Self {
|
||||
period,
|
||||
bin_count,
|
||||
window: VecDeque::with_capacity(period),
|
||||
last: None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Classic TPO Profile: 30-bar rolling window, 50 bins.
|
||||
pub fn classic() -> Self {
|
||||
Self::new(30, 50).expect("classic TpoProfile params are valid")
|
||||
}
|
||||
|
||||
/// Configured `(period, bin_count)`.
|
||||
pub const fn params(&self) -> (usize, usize) {
|
||||
(self.period, self.bin_count)
|
||||
}
|
||||
|
||||
/// Most recent profile if available.
|
||||
pub fn value(&self) -> Option<&TpoProfileOutput> {
|
||||
self.last.as_ref()
|
||||
}
|
||||
|
||||
fn price_to_bin(&self, price: f64, win_low: f64, bin_width: f64) -> usize {
|
||||
let raw = ((price - win_low) / bin_width).floor();
|
||||
let max = (self.bin_count - 1) as f64;
|
||||
raw.clamp(0.0, max) as usize
|
||||
}
|
||||
|
||||
fn compute(&self) -> TpoProfileOutput {
|
||||
let mut win_low = f64::INFINITY;
|
||||
let mut win_high = f64::NEG_INFINITY;
|
||||
for candle in &self.window {
|
||||
if candle.low < win_low {
|
||||
win_low = candle.low;
|
||||
}
|
||||
if candle.high > win_high {
|
||||
win_high = candle.high;
|
||||
}
|
||||
}
|
||||
let span = win_high - win_low;
|
||||
let mut counts = vec![0.0_f64; self.bin_count];
|
||||
|
||||
if span <= 0.0 {
|
||||
// All bars are single-print at the same price: every period marks bin 0.
|
||||
counts[0] = self.window.len() as f64;
|
||||
return TpoProfileOutput {
|
||||
price_low: win_low,
|
||||
price_high: win_low,
|
||||
counts,
|
||||
};
|
||||
}
|
||||
|
||||
let bin_width = span / self.bin_count as f64;
|
||||
for candle in &self.window {
|
||||
if candle.high <= candle.low {
|
||||
let idx = self.price_to_bin(candle.low, win_low, bin_width);
|
||||
counts[idx] += 1.0;
|
||||
continue;
|
||||
}
|
||||
let lo_idx = self.price_to_bin(candle.low, win_low, bin_width);
|
||||
let hi_idx = self.price_to_bin(candle.high, win_low, bin_width);
|
||||
for count in counts.iter_mut().take(hi_idx + 1).skip(lo_idx) {
|
||||
*count += 1.0;
|
||||
}
|
||||
}
|
||||
|
||||
TpoProfileOutput {
|
||||
price_low: win_low,
|
||||
price_high: win_high,
|
||||
counts,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Indicator for TpoProfile {
|
||||
type Input = Candle;
|
||||
type Output = TpoProfileOutput;
|
||||
|
||||
fn update(&mut self, candle: Candle) -> Option<TpoProfileOutput> {
|
||||
if self.window.len() == self.period {
|
||||
self.window.pop_front();
|
||||
}
|
||||
self.window.push_back(candle);
|
||||
if self.window.len() < self.period {
|
||||
return None;
|
||||
}
|
||||
let out = self.compute();
|
||||
self.last = Some(out.clone());
|
||||
Some(out)
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.window.clear();
|
||||
self.last = None;
|
||||
}
|
||||
|
||||
fn warmup_period(&self) -> usize {
|
||||
self.period
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.last.is_some()
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"TpoProfile"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::traits::BatchExt;
|
||||
use approx::assert_relative_eq;
|
||||
|
||||
fn c(open: f64, high: f64, low: f64, close: f64, volume: f64, ts: i64) -> Candle {
|
||||
Candle::new(open, high, low, close, volume, ts).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_zero_period() {
|
||||
assert!(matches!(TpoProfile::new(0, 50), Err(Error::PeriodZero)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_zero_bin_count() {
|
||||
assert!(matches!(TpoProfile::new(20, 0), Err(Error::PeriodZero)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accessors_and_metadata() {
|
||||
let tpo = TpoProfile::new(30, 50).unwrap();
|
||||
assert_eq!(tpo.name(), "TpoProfile");
|
||||
assert_eq!(tpo.warmup_period(), 30);
|
||||
assert_eq!(tpo.params(), (30, 50));
|
||||
assert!(tpo.value().is_none());
|
||||
assert!(!tpo.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn classic_params() {
|
||||
let tpo = TpoProfile::classic();
|
||||
assert_eq!(tpo.params(), (30, 50));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn warms_up_over_period() {
|
||||
let mut tpo = TpoProfile::new(3, 4).unwrap();
|
||||
assert!(tpo.update(c(10.0, 11.0, 9.0, 10.0, 5.0, 0)).is_none());
|
||||
assert!(tpo.update(c(10.0, 11.0, 9.0, 10.0, 5.0, 1)).is_none());
|
||||
assert!(tpo.update(c(10.0, 11.0, 9.0, 10.0, 5.0, 2)).is_some());
|
||||
assert!(tpo.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reference_counts() {
|
||||
// Window of 2 candles, 4 bins, domain 10..14, width 1.
|
||||
// bar0: 10..14 touches bins 0,1,2,3 -> +1 each.
|
||||
// bar1: 11..12 touches bins 1,2 -> +1 each.
|
||||
// counts = [1, 2, 2, 1]. TPO is volume-agnostic.
|
||||
let mut tpo = TpoProfile::new(2, 4).unwrap();
|
||||
assert!(tpo.update(c(10.0, 14.0, 10.0, 12.0, 5.0, 0)).is_none());
|
||||
let out = tpo.update(c(11.0, 12.0, 11.0, 11.5, 999.0, 1)).unwrap();
|
||||
assert_relative_eq!(out.price_low, 10.0, epsilon = 1e-12);
|
||||
assert_relative_eq!(out.price_high, 14.0, epsilon = 1e-12);
|
||||
assert_eq!(out.counts.len(), 4);
|
||||
assert_relative_eq!(out.counts[0], 1.0, epsilon = 1e-12);
|
||||
assert_relative_eq!(out.counts[1], 2.0, epsilon = 1e-12);
|
||||
assert_relative_eq!(out.counts[2], 2.0, epsilon = 1e-12);
|
||||
assert_relative_eq!(out.counts[3], 1.0, epsilon = 1e-12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn volume_independent() {
|
||||
// Identical ranges with wildly different volumes give identical TPO counts.
|
||||
let mut a = TpoProfile::new(2, 4).unwrap();
|
||||
let mut b = TpoProfile::new(2, 4).unwrap();
|
||||
a.update(c(10.0, 14.0, 10.0, 12.0, 1.0, 0));
|
||||
let out_a = a.update(c(10.0, 14.0, 10.0, 12.0, 1.0, 1)).unwrap();
|
||||
b.update(c(10.0, 14.0, 10.0, 12.0, 9_999.0, 0));
|
||||
let out_b = b.update(c(10.0, 14.0, 10.0, 12.0, 9_999.0, 1)).unwrap();
|
||||
assert_eq!(out_a.counts, out_b.counts);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn degenerate_single_price_window() {
|
||||
let mut tpo = TpoProfile::new(3, 4).unwrap();
|
||||
tpo.update(c(50.0, 50.0, 50.0, 50.0, 10.0, 0));
|
||||
tpo.update(c(50.0, 50.0, 50.0, 50.0, 20.0, 1));
|
||||
let out = tpo.update(c(50.0, 50.0, 50.0, 50.0, 30.0, 2)).unwrap();
|
||||
assert_relative_eq!(out.price_low, 50.0, epsilon = 1e-12);
|
||||
assert_relative_eq!(out.price_high, 50.0, epsilon = 1e-12);
|
||||
assert_relative_eq!(out.counts[0], 3.0, epsilon = 1e-12);
|
||||
assert_relative_eq!(out.counts[1], 0.0, epsilon = 1e-12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn single_print_bar_marks_one_bin() {
|
||||
// A single-print bar inside a wider domain marks exactly its own bin.
|
||||
let mut tpo = TpoProfile::new(2, 4).unwrap();
|
||||
tpo.update(c(10.0, 14.0, 10.0, 12.0, 5.0, 0)); // domain setter
|
||||
let out = tpo.update(c(13.0, 13.0, 13.0, 13.0, 5.0, 1)).unwrap();
|
||||
// domain 10..14, width 1; price 13 -> bin 3.
|
||||
assert_relative_eq!(out.counts[3], 2.0, epsilon = 1e-12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut tpo = TpoProfile::new(2, 4).unwrap();
|
||||
tpo.update(c(10.0, 11.0, 9.0, 10.0, 5.0, 0));
|
||||
tpo.update(c(10.0, 11.0, 9.0, 10.0, 5.0, 1));
|
||||
assert!(tpo.is_ready());
|
||||
tpo.reset();
|
||||
assert!(!tpo.is_ready());
|
||||
assert!(tpo.value().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_equals_streaming() {
|
||||
let candles: Vec<Candle> = (0..30)
|
||||
.map(|i| {
|
||||
let base = 100.0 + f64::from(i % 7);
|
||||
c(
|
||||
base,
|
||||
base + 2.0,
|
||||
base - 2.0,
|
||||
base,
|
||||
10.0 + f64::from(i),
|
||||
i64::from(i),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
let mut a = TpoProfile::new(10, 16).unwrap();
|
||||
let mut b = TpoProfile::new(10, 16).unwrap();
|
||||
assert_eq!(
|
||||
a.batch(&candles),
|
||||
candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,333 @@
|
||||
//! Volume Profile — the full per-bin volume distribution over a rolling window.
|
||||
|
||||
use std::collections::VecDeque;
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
use crate::ohlcv::Candle;
|
||||
use crate::traits::Indicator;
|
||||
|
||||
/// Volume Profile output: the price domain plus the per-bin volume histogram.
|
||||
///
|
||||
/// `bins[i]` holds the volume attributed to the price bucket
|
||||
/// `[price_low + i * w, price_low + (i + 1) * w)` where
|
||||
/// `w = (price_high - price_low) / bins.len()`. The histogram sums to the total
|
||||
/// volume in the rolling window (within floating-point tolerance).
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct VolumeProfileOutput {
|
||||
/// Lowest price in the window — the lower edge of bin 0.
|
||||
pub price_low: f64,
|
||||
/// Highest price in the window — the upper edge of the last bin.
|
||||
pub price_high: f64,
|
||||
/// Per-bin volume, lowest price bucket first. Length equals `bin_count`.
|
||||
pub bins: Vec<f64>,
|
||||
}
|
||||
|
||||
/// Rolling Volume Profile over the last `period` candles.
|
||||
///
|
||||
/// Where [`crate::ValueArea`] reduces the same volume distribution to its
|
||||
/// summary levels (Point of Control, Value Area High / Low), Volume Profile
|
||||
/// exposes the **full histogram** so callers can inspect, render or post-process
|
||||
/// the raw distribution. Each candle's volume is spread uniformly across the
|
||||
/// bins its `[low, high]` range touches; a single-print bar (`low == high`)
|
||||
/// drops its whole volume into one bin. The histogram domain spans the window's
|
||||
/// lowest low to its highest high.
|
||||
///
|
||||
/// A window whose bars are all single-print at one price (`price_high == price_low`)
|
||||
/// is degenerate: the entire volume lands in bin 0 and both edges collapse to
|
||||
/// that price.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use wickra_core::{Candle, Indicator, VolumeProfile};
|
||||
///
|
||||
/// let mut vp = VolumeProfile::new(5, 10).unwrap();
|
||||
/// let mut last = None;
|
||||
/// for i in 0..10 {
|
||||
/// let base = 100.0 + f64::from(i);
|
||||
/// let candle =
|
||||
/// Candle::new(base, base + 2.0, base - 2.0, base, 10.0, i64::from(i)).unwrap();
|
||||
/// last = vp.update(candle);
|
||||
/// }
|
||||
/// let profile = last.unwrap();
|
||||
/// assert_eq!(profile.bins.len(), 10);
|
||||
/// ```
|
||||
#[allow(clippy::struct_field_names)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct VolumeProfile {
|
||||
period: usize,
|
||||
bin_count: usize,
|
||||
window: VecDeque<Candle>,
|
||||
last: Option<VolumeProfileOutput>,
|
||||
}
|
||||
|
||||
impl VolumeProfile {
|
||||
/// Construct a Volume Profile indicator.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`Error::PeriodZero`] if `period` or `bin_count` is zero.
|
||||
pub fn new(period: usize, bin_count: usize) -> Result<Self> {
|
||||
if period == 0 || bin_count == 0 {
|
||||
return Err(Error::PeriodZero);
|
||||
}
|
||||
Ok(Self {
|
||||
period,
|
||||
bin_count,
|
||||
window: VecDeque::with_capacity(period),
|
||||
last: None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Classic Volume Profile: 20-bar rolling window, 50 bins.
|
||||
pub fn classic() -> Self {
|
||||
Self::new(20, 50).expect("classic VolumeProfile params are valid")
|
||||
}
|
||||
|
||||
/// Configured `(period, bin_count)`.
|
||||
pub const fn params(&self) -> (usize, usize) {
|
||||
(self.period, self.bin_count)
|
||||
}
|
||||
|
||||
/// Most recent profile if available.
|
||||
pub fn value(&self) -> Option<&VolumeProfileOutput> {
|
||||
self.last.as_ref()
|
||||
}
|
||||
|
||||
fn price_to_bin(&self, price: f64, win_low: f64, bin_width: f64) -> usize {
|
||||
let raw = ((price - win_low) / bin_width).floor();
|
||||
let max = (self.bin_count - 1) as f64;
|
||||
raw.clamp(0.0, max) as usize
|
||||
}
|
||||
|
||||
fn compute(&self) -> VolumeProfileOutput {
|
||||
let mut win_low = f64::INFINITY;
|
||||
let mut win_high = f64::NEG_INFINITY;
|
||||
for candle in &self.window {
|
||||
if candle.low < win_low {
|
||||
win_low = candle.low;
|
||||
}
|
||||
if candle.high > win_high {
|
||||
win_high = candle.high;
|
||||
}
|
||||
}
|
||||
let span = win_high - win_low;
|
||||
let mut bins = vec![0.0_f64; self.bin_count];
|
||||
|
||||
if span <= 0.0 {
|
||||
// All bars are single-print at the same price.
|
||||
let total: f64 = self.window.iter().map(|candle| candle.volume).sum();
|
||||
bins[0] = total;
|
||||
return VolumeProfileOutput {
|
||||
price_low: win_low,
|
||||
price_high: win_low,
|
||||
bins,
|
||||
};
|
||||
}
|
||||
|
||||
let bin_width = span / self.bin_count as f64;
|
||||
for candle in &self.window {
|
||||
if candle.volume == 0.0 {
|
||||
continue;
|
||||
}
|
||||
if candle.high <= candle.low {
|
||||
let idx = self.price_to_bin(candle.low, win_low, bin_width);
|
||||
bins[idx] += candle.volume;
|
||||
continue;
|
||||
}
|
||||
let lo_idx = self.price_to_bin(candle.low, win_low, bin_width);
|
||||
let hi_idx = self.price_to_bin(candle.high, win_low, bin_width);
|
||||
let touched = hi_idx - lo_idx + 1;
|
||||
let share = candle.volume / touched as f64;
|
||||
for bin in bins.iter_mut().take(hi_idx + 1).skip(lo_idx) {
|
||||
*bin += share;
|
||||
}
|
||||
}
|
||||
|
||||
VolumeProfileOutput {
|
||||
price_low: win_low,
|
||||
price_high: win_high,
|
||||
bins,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Indicator for VolumeProfile {
|
||||
type Input = Candle;
|
||||
type Output = VolumeProfileOutput;
|
||||
|
||||
fn update(&mut self, candle: Candle) -> Option<VolumeProfileOutput> {
|
||||
if self.window.len() == self.period {
|
||||
self.window.pop_front();
|
||||
}
|
||||
self.window.push_back(candle);
|
||||
if self.window.len() < self.period {
|
||||
return None;
|
||||
}
|
||||
let out = self.compute();
|
||||
self.last = Some(out.clone());
|
||||
Some(out)
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.window.clear();
|
||||
self.last = None;
|
||||
}
|
||||
|
||||
fn warmup_period(&self) -> usize {
|
||||
self.period
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.last.is_some()
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"VolumeProfile"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::traits::BatchExt;
|
||||
use approx::assert_relative_eq;
|
||||
|
||||
fn c(open: f64, high: f64, low: f64, close: f64, volume: f64, ts: i64) -> Candle {
|
||||
Candle::new(open, high, low, close, volume, ts).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_zero_period() {
|
||||
assert!(matches!(VolumeProfile::new(0, 50), Err(Error::PeriodZero)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_zero_bin_count() {
|
||||
assert!(matches!(VolumeProfile::new(20, 0), Err(Error::PeriodZero)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accessors_and_metadata() {
|
||||
let vp = VolumeProfile::new(20, 50).unwrap();
|
||||
assert_eq!(vp.name(), "VolumeProfile");
|
||||
assert_eq!(vp.warmup_period(), 20);
|
||||
assert_eq!(vp.params(), (20, 50));
|
||||
assert!(vp.value().is_none());
|
||||
assert!(!vp.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn classic_params() {
|
||||
let vp = VolumeProfile::classic();
|
||||
assert_eq!(vp.params(), (20, 50));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn warms_up_over_period() {
|
||||
let mut vp = VolumeProfile::new(3, 4).unwrap();
|
||||
assert!(vp.update(c(10.0, 11.0, 9.0, 10.0, 5.0, 0)).is_none());
|
||||
assert!(vp.update(c(10.0, 11.0, 9.0, 10.0, 5.0, 1)).is_none());
|
||||
assert!(vp.update(c(10.0, 11.0, 9.0, 10.0, 5.0, 2)).is_some());
|
||||
assert!(vp.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reference_distribution() {
|
||||
// Window of 2 candles, 4 bins.
|
||||
// bar0: single print at 10, vol 100 -> bin 0 gets 100.
|
||||
// bar1: 10..14, vol 80, spans 4 bins -> 20 each.
|
||||
// domain: low=10, high=14, width=1 -> bins = [120, 20, 20, 20].
|
||||
let mut vp = VolumeProfile::new(2, 4).unwrap();
|
||||
assert!(vp.update(c(10.0, 10.0, 10.0, 10.0, 100.0, 0)).is_none());
|
||||
let out = vp.update(c(10.0, 14.0, 10.0, 12.0, 80.0, 1)).unwrap();
|
||||
assert_relative_eq!(out.price_low, 10.0, epsilon = 1e-12);
|
||||
assert_relative_eq!(out.price_high, 14.0, epsilon = 1e-12);
|
||||
assert_eq!(out.bins.len(), 4);
|
||||
assert_relative_eq!(out.bins[0], 120.0, epsilon = 1e-9);
|
||||
assert_relative_eq!(out.bins[1], 20.0, epsilon = 1e-9);
|
||||
assert_relative_eq!(out.bins[2], 20.0, epsilon = 1e-9);
|
||||
assert_relative_eq!(out.bins[3], 20.0, epsilon = 1e-9);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn conserves_total_volume() {
|
||||
let mut vp = VolumeProfile::new(4, 8).unwrap();
|
||||
let candles = [
|
||||
c(10.0, 12.0, 9.0, 11.0, 30.0, 0),
|
||||
c(11.0, 13.0, 10.0, 12.0, 40.0, 1),
|
||||
c(12.0, 14.0, 11.0, 13.0, 50.0, 2),
|
||||
c(13.0, 15.0, 12.0, 14.0, 60.0, 3),
|
||||
];
|
||||
let out = vp.batch(&candles).pop().unwrap().unwrap();
|
||||
let total: f64 = out.bins.iter().sum();
|
||||
assert_relative_eq!(total, 180.0, epsilon = 1e-9);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn degenerate_single_price_window() {
|
||||
// All bars single-print at 50 -> domain collapses, all volume in bin 0.
|
||||
let mut vp = VolumeProfile::new(2, 4).unwrap();
|
||||
vp.update(c(50.0, 50.0, 50.0, 50.0, 10.0, 0));
|
||||
let out = vp.update(c(50.0, 50.0, 50.0, 50.0, 20.0, 1)).unwrap();
|
||||
assert_relative_eq!(out.price_low, 50.0, epsilon = 1e-12);
|
||||
assert_relative_eq!(out.price_high, 50.0, epsilon = 1e-12);
|
||||
assert_relative_eq!(out.bins[0], 30.0, epsilon = 1e-9);
|
||||
assert_relative_eq!(out.bins[1], 0.0, epsilon = 1e-12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zero_volume_bars_are_skipped() {
|
||||
let mut vp = VolumeProfile::new(2, 4).unwrap();
|
||||
vp.update(c(10.0, 14.0, 10.0, 12.0, 0.0, 0));
|
||||
let out = vp.update(c(10.0, 14.0, 10.0, 12.0, 40.0, 1)).unwrap();
|
||||
let total: f64 = out.bins.iter().sum();
|
||||
assert_relative_eq!(total, 40.0, epsilon = 1e-9);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rolling_window_drops_oldest() {
|
||||
let mut vp = VolumeProfile::new(2, 4).unwrap();
|
||||
vp.update(c(100.0, 100.0, 100.0, 100.0, 99.0, 0));
|
||||
vp.update(c(10.0, 14.0, 10.0, 12.0, 40.0, 1));
|
||||
// Third bar evicts the price-100 bar; domain is now 10..14 only.
|
||||
let out = vp.update(c(10.0, 14.0, 10.0, 12.0, 40.0, 2)).unwrap();
|
||||
assert_relative_eq!(out.price_high, 14.0, epsilon = 1e-12);
|
||||
let total: f64 = out.bins.iter().sum();
|
||||
assert_relative_eq!(total, 80.0, epsilon = 1e-9);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut vp = VolumeProfile::new(2, 4).unwrap();
|
||||
vp.update(c(10.0, 11.0, 9.0, 10.0, 5.0, 0));
|
||||
vp.update(c(10.0, 11.0, 9.0, 10.0, 5.0, 1));
|
||||
assert!(vp.is_ready());
|
||||
vp.reset();
|
||||
assert!(!vp.is_ready());
|
||||
assert!(vp.value().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_equals_streaming() {
|
||||
let candles: Vec<Candle> = (0..30)
|
||||
.map(|i| {
|
||||
let base = 100.0 + f64::from(i % 7);
|
||||
c(
|
||||
base,
|
||||
base + 2.0,
|
||||
base - 2.0,
|
||||
base,
|
||||
10.0 + f64::from(i),
|
||||
i64::from(i),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
let mut a = VolumeProfile::new(10, 16).unwrap();
|
||||
let mut b = VolumeProfile::new(10, 16).unwrap();
|
||||
assert_eq!(
|
||||
a.batch(&candles),
|
||||
candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -55,8 +55,8 @@ pub use error::{Error, Result};
|
||||
pub use indicators::{
|
||||
AbandonedBaby, AccelerationBands, AccelerationBandsOutput, AcceleratorOscillator, AdOscillator,
|
||||
AdaptiveCycle, Adl, AdvanceBlock, Adx, AdxOutput, Adxr, Alligator, AlligatorOutput, Alma,
|
||||
Alpha, AnchoredVwap, Apo, Aroon, AroonOscillator, AroonOutput, Atr, AtrBands, AtrBandsOutput,
|
||||
AtrTrailingStop, Autocorrelation, AverageDrawdown, AwesomeOscillator,
|
||||
Alpha, AnchoredRsi, AnchoredVwap, Apo, Aroon, AroonOscillator, AroonOutput, Atr, AtrBands,
|
||||
AtrBandsOutput, AtrTrailingStop, Autocorrelation, AverageDrawdown, AwesomeOscillator,
|
||||
AwesomeOscillatorHistogram, BalanceOfPower, BeltHold, Beta, BollingerBands, BollingerBandwidth,
|
||||
BollingerOutput, Breakaway, CalendarSpread, CalmarRatio, Camarilla, CamarillaPivotsOutput, Cci,
|
||||
CenterOfGravity, Cfo, ChaikinMoneyFlow, ChaikinOscillator, ChaikinVolatility, ChandeKrollStop,
|
||||
@@ -76,43 +76,51 @@ pub use indicators::{
|
||||
HighWave, Hikkake, HikkakeModified, HilbertDominantCycle, HistoricalVolatility, Hma,
|
||||
HomingPigeon, HurstChannel, HurstChannelOutput, HurstExponent, Ichimoku, IchimokuOutput,
|
||||
IdenticalThreeCrows, InNeck, Inertia, InformationRatio, InitialBalance, InitialBalanceOutput,
|
||||
InstantaneousTrendline, InverseFisherTransform, InvertedHammer, Jma, Kama, KellyCriterion,
|
||||
Keltner, KeltnerOutput, Kicking, KickingByLength, Kst, KstOutput, Kurtosis, Kvo, KylesLambda,
|
||||
LadderBottom, LaguerreRsi, LeadLagCrossCorrelation, LeadLagCrossCorrelationOutput, LinRegAngle,
|
||||
LinRegChannel, LinRegChannelOutput, LinRegSlope, LinearRegression, LiquidationFeatures,
|
||||
LiquidationFeaturesOutput, LongLeggedDoji, LongLine, LongShortRatio, MaEnvelope,
|
||||
MaEnvelopeOutput, MacdIndicator, MacdOutput, Mama, MamaOutput, MarketFacilitationIndex,
|
||||
Marubozu, MassIndex, MatHold, MatchingLow, MaxDrawdown, McGinleyDynamic,
|
||||
MedianAbsoluteDeviation, MedianPrice, Mfi, Microprice, Mom, MorningDojiStar,
|
||||
InstantaneousTrendline, InverseFisherTransform, InvertedHammer, Jma, KagiBars, Kama,
|
||||
KellyCriterion, Keltner, KeltnerOutput, Kicking, KickingByLength, Kst, KstOutput, Kurtosis,
|
||||
Kvo, KylesLambda, LadderBottom, LaguerreRsi, LeadLagCrossCorrelation,
|
||||
LeadLagCrossCorrelationOutput, LinRegAngle, LinRegChannel, LinRegChannelOutput, LinRegSlope,
|
||||
LinearRegression, LiquidationFeatures, LiquidationFeaturesOutput, LongLeggedDoji, LongLine,
|
||||
LongShortRatio, MaEnvelope, MaEnvelopeOutput, MacdIndicator, MacdOutput, Mama, MamaOutput,
|
||||
MarketFacilitationIndex, Marubozu, MassIndex, MatHold, MatchingLow, MaxDrawdown,
|
||||
McGinleyDynamic, MedianAbsoluteDeviation, MedianPrice, Mfi, Microprice, Mom, MorningDojiStar,
|
||||
MorningEveningStar, Natr, Nvi, OIPriceDivergence, OIWeighted, Obv, OmegaRatio, OnNeck,
|
||||
OpenInterestDelta, OpeningMarubozu, OpeningRange, OpeningRangeOutput, OrderBookImbalanceFull,
|
||||
OrderBookImbalanceTop1, OrderBookImbalanceTopN, PainIndex, PairSpreadZScore, PairwiseBeta,
|
||||
ParkinsonVolatility, PearsonCorrelation, PercentB, PercentageTrailingStop, Pgo,
|
||||
PiercingDarkCloud, Pmo, Ppo, ProfitFactor, Psar, Pvi, QuotedSpread, RSquared, RealizedSpread,
|
||||
RecoveryFactor, RelativeStrengthAB, RelativeStrengthOutput, RenkoTrailingStop, RickshawMan,
|
||||
RisingThreeMethods, Roc, RogersSatchellVolatility, RollingVwap, RoofingFilter, Rsi, Rvi,
|
||||
RviVolatility, Rwi, RwiOutput, SeparatingLines, SharpeRatio, ShootingStar, ShortLine,
|
||||
SignedVolume, SineWave, Skewness, Sma, Smi, Smma, SortinoRatio, SpearmanCorrelation,
|
||||
SpinningTop, 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, TradeImbalance, TreynorRatio, Trima, Trix, TrueRange, Tsi, Tsv, TtmSqueeze,
|
||||
PiercingDarkCloud, Pmo, PointAndFigureBars, Ppo, ProfitFactor, Psar, Pvi, QuotedSpread,
|
||||
RSquared, RealizedSpread, RecoveryFactor, RelativeStrengthAB, RelativeStrengthOutput,
|
||||
RenkoBars, RenkoTrailingStop, RickshawMan, RisingThreeMethods, Roc, RogersSatchellVolatility,
|
||||
RollingVwap, RoofingFilter, Rsi, Rvi, RviVolatility, Rwi, RwiOutput, SeparatingLines,
|
||||
SharpeRatio, ShootingStar, ShortLine, SignedVolume, SineWave, Skewness, Sma, Smi, Smma,
|
||||
SortinoRatio, SpearmanCorrelation, SpinningTop, 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, Tsi, Tsv, TtmSqueeze,
|
||||
TtmSqueezeOutput, Tweezer, TwoCrows, TypicalPrice, UlcerIndex, UltimateOscillator,
|
||||
UniqueThreeRiver, UpsideGapThreeMethods, UpsideGapTwoCrows, ValueArea, ValueAreaOutput,
|
||||
ValueAtRisk, Variance, VerticalHorizontalFilter, Vidya, VoltyStop, VolumeOscillator,
|
||||
VolumePriceTrend, 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,
|
||||
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
|
||||
// strips only `*Output` companions) does not count it as a separate indicator.
|
||||
pub use indicators::FootprintLevel;
|
||||
// Bar element types for the alt-chart builders, re-exported on their own lines so
|
||||
// the indicator-count tooling (which scans only the braced block above) does not
|
||||
// count them as separate indicators.
|
||||
pub use indicators::KagiBar;
|
||||
pub use indicators::PnfColumn;
|
||||
pub use indicators::RenkoBrick;
|
||||
pub use microstructure::{Level, OrderBook, Side, Trade, TradeQuote};
|
||||
pub use ohlcv::{Candle, Tick};
|
||||
pub use traits::{BatchExt, Chain, Indicator};
|
||||
pub use traits::{BarBuilder, BatchExt, Chain, Indicator};
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
//! Core traits: the [`Indicator`] state machine and the [`BatchExt`] blanket extension.
|
||||
|
||||
use crate::ohlcv::Candle;
|
||||
|
||||
/// A streaming technical indicator.
|
||||
///
|
||||
/// Every indicator in Wickra implements this trait. The contract is:
|
||||
@@ -88,6 +90,58 @@ pub trait BatchExt: Indicator {
|
||||
|
||||
impl<T: Indicator> BatchExt for T {}
|
||||
|
||||
/// A streaming *bar builder* — an alternative-chart constructor (Renko, Kagi,
|
||||
/// Point-and-Figure) that turns a candle stream into a stream of price-driven
|
||||
/// bars.
|
||||
///
|
||||
/// Bar builders are deliberately **not** [`Indicator`]s: a single input candle
|
||||
/// may complete zero, one, or many bars (a large move can print several Renko
|
||||
/// bricks at once), which breaks the `update -> Option<Output>` one-in-one-out
|
||||
/// contract and the `batch == repeated update` length invariant. They get their
|
||||
/// own trait instead, returning a `Vec` of freshly completed bars per candle.
|
||||
///
|
||||
/// The contract is:
|
||||
///
|
||||
/// - [`update`](BarBuilder::update) ingests one candle and returns every bar it
|
||||
/// *completed* on that candle, in chronological order. An empty vector means
|
||||
/// the move was not large enough to finish a bar yet.
|
||||
/// - [`reset`](BarBuilder::reset) clears all state, returning the builder to the
|
||||
/// configuration it had immediately after construction.
|
||||
/// - [`batch`](BarBuilder::batch) concatenates the bars from replaying `update`
|
||||
/// over a slice; the flattened length is data-dependent, not the input length.
|
||||
///
|
||||
/// Bar builders cannot participate in [`Chain`] (which requires
|
||||
/// `Indicator<Input = f64, Output = f64>`); feed a downstream indicator from the
|
||||
/// bars' close prices manually if you need to chain off them.
|
||||
///
|
||||
/// ```text
|
||||
/// let mut renko = RenkoBars::new(1.0).unwrap();
|
||||
/// let bricks = renko.update(candle); // Vec<RenkoBrick>: 0..n completed bricks
|
||||
/// ```
|
||||
pub trait BarBuilder {
|
||||
/// Type of one completed bar.
|
||||
type Bar;
|
||||
|
||||
/// Feed one candle and return every bar completed on it (possibly none).
|
||||
fn update(&mut self, candle: Candle) -> Vec<Self::Bar>;
|
||||
|
||||
/// Reset all internal state to the freshly-constructed configuration.
|
||||
fn reset(&mut self);
|
||||
|
||||
/// Stable, human-readable builder name.
|
||||
fn name(&self) -> &'static str;
|
||||
|
||||
/// Replay `update` over a slice, concatenating all completed bars. The
|
||||
/// result length is data-dependent (not the input length).
|
||||
fn batch(&mut self, candles: &[Candle]) -> Vec<Self::Bar> {
|
||||
let mut out = Vec::new();
|
||||
for candle in candles {
|
||||
out.extend(self.update(*candle));
|
||||
}
|
||||
out
|
||||
}
|
||||
}
|
||||
|
||||
/// Chain two indicators so the output of the first becomes the input of the second.
|
||||
///
|
||||
/// Both indicators must agree on `f64` as the bridging type, which is the common
|
||||
|
||||
@@ -32,16 +32,17 @@
|
||||
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion, Throughput};
|
||||
use std::hint::black_box;
|
||||
use wickra::{
|
||||
Adx, Atr, Autocorrelation, BatchExt, BollingerBands, BollingerOutput, CalmarRatio, Candle, Cci,
|
||||
ClassicPivots, ConnorsRsi, DepthSlope, DerivativesTick, EffectiveSpread, Ema,
|
||||
Adx, AnchoredRsi, Atr, Autocorrelation, BatchExt, BollingerBands, BollingerOutput, CalmarRatio,
|
||||
Candle, Cci, ClassicPivots, ConnorsRsi, DepthSlope, DerivativesTick, EffectiveSpread, Ema,
|
||||
EmpiricalModeDecomposition, Engulfing, Frama, FundingRate, FundingRateZScore,
|
||||
HilbertDominantCycle, HurstExponent, Ichimoku, IchimokuOutput, Indicator, Jma, KylesLambda,
|
||||
Level, LinearRegression, MacdIndicator, MacdOutput, Mama, MamaOutput, MaxDrawdown, Microprice,
|
||||
Obv, OrderBook, OrderBookImbalanceFull, OrderBookImbalanceTop1, ParkinsonVolatility, Ppo, Psar,
|
||||
RollingVwap, Rsi, SharpeRatio, Side, SignedVolume, Sma, Stc, SuperTrend, SuperTrendOutput,
|
||||
TdSequential, TdSequentialOutput, Trade, TradeImbalance, TradeQuote, TtmSqueeze,
|
||||
TtmSqueezeOutput, ValueArea, ValueAreaOutput, ValueAtRisk, Vwap, VwapStdDevBands,
|
||||
VwapStdDevBandsOutput, WaveTrend, YangZhangVolatility, T3,
|
||||
TdSequential, TdSequentialOutput, TpoProfile, TpoProfileOutput, Trade, TradeImbalance,
|
||||
TradeQuote, TtmSqueeze, TtmSqueezeOutput, ValueArea, ValueAreaOutput, ValueAtRisk,
|
||||
VolumeProfile, VolumeProfileOutput, Vwap, VwapStdDevBands, VwapStdDevBandsOutput, WaveTrend,
|
||||
YangZhangVolatility, T3,
|
||||
};
|
||||
use wickra_data::csv::CandleReader;
|
||||
|
||||
@@ -246,6 +247,7 @@ fn benches(c: &mut Criterion) {
|
||||
// === Family 02 — Momentum Oscillators ===
|
||||
// Rsi: textbook baseline; ConnorsRsi: three-component composite.
|
||||
bench_scalar(c, "rsi", &closes, || Rsi::new(14).unwrap());
|
||||
bench_scalar(c, "anchored_rsi", &closes, AnchoredRsi::new);
|
||||
bench_candle_input(c, "cci", &candles, || Cci::new(20).unwrap());
|
||||
bench_scalar(c, "connors_rsi", &closes, ConnorsRsi::classic);
|
||||
|
||||
@@ -347,6 +349,12 @@ fn benches(c: &mut Criterion) {
|
||||
bench_candle_input::<_, _, ValueAreaOutput>(c, "value_area", &candles, || {
|
||||
ValueArea::new(20, 50, 0.70).unwrap()
|
||||
});
|
||||
bench_candle_input::<_, _, VolumeProfileOutput>(c, "volume_profile", &candles, || {
|
||||
VolumeProfile::new(20, 50).unwrap()
|
||||
});
|
||||
bench_candle_input::<_, _, TpoProfileOutput>(c, "tpo_profile", &candles, || {
|
||||
TpoProfile::new(20, 50).unwrap()
|
||||
});
|
||||
|
||||
// === Family 16 — Risk / Performance Metrics ===
|
||||
// Close-prices stand in for the equity curve / return stream; absolute
|
||||
|
||||
+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 **289 indicators** across
|
||||
- A per-indicator deep dive for every one of the **295 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 &
|
||||
|
||||
Generated
+7
-7
@@ -17,7 +17,7 @@
|
||||
},
|
||||
"../../bindings/node": {
|
||||
"name": "wickra",
|
||||
"version": "0.4.4",
|
||||
"version": "0.4.5",
|
||||
"license": "PolyForm-Noncommercial-1.0.0",
|
||||
"devDependencies": {
|
||||
"@napi-rs/cli": "^2.18.0"
|
||||
@@ -26,12 +26,12 @@
|
||||
"node": ">= 18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"wickra-darwin-arm64": "0.4.4",
|
||||
"wickra-darwin-x64": "0.4.4",
|
||||
"wickra-linux-arm64-gnu": "0.4.4",
|
||||
"wickra-linux-x64-gnu": "0.4.4",
|
||||
"wickra-win32-arm64-msvc": "0.4.4",
|
||||
"wickra-win32-x64-msvc": "0.4.4"
|
||||
"wickra-darwin-arm64": "0.4.5",
|
||||
"wickra-darwin-x64": "0.4.5",
|
||||
"wickra-linux-arm64-gnu": "0.4.5",
|
||||
"wickra-linux-x64-gnu": "0.4.5",
|
||||
"wickra-win32-arm64-msvc": "0.4.5",
|
||||
"wickra-win32-x64-msvc": "0.4.5"
|
||||
}
|
||||
},
|
||||
"node_modules/wickra": {
|
||||
|
||||
@@ -86,3 +86,10 @@ path = "fuzz_targets/tick_aggregator.rs"
|
||||
test = false
|
||||
doc = false
|
||||
bench = false
|
||||
|
||||
[[bin]]
|
||||
name = "bar_builder_update_candle"
|
||||
path = "fuzz_targets/bar_builder_update_candle.rs"
|
||||
test = false
|
||||
doc = false
|
||||
bench = false
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
#![no_main]
|
||||
//! Fuzz the alt-chart bar builders (Renko, Kagi, Point-and-Figure) with arbitrary
|
||||
//! candle streams.
|
||||
//!
|
||||
//! Each fuzz iteration runs the same candle sequence through every bar builder
|
||||
//! twice — once as a streaming `update` loop, once as a full `batch` call — and
|
||||
//! asserts neither path panics. Renko's brick count per candle is proportional
|
||||
//! to `(price_move / box_size)`, so the candle magnitudes are bounded here (a
|
||||
//! sensibly scaled `box_size` relative to the instrument price is a documented
|
||||
//! precondition); the fuzzer still freely varies ordering, gaps, equal prices
|
||||
//! and zero ranges within that band.
|
||||
|
||||
use libfuzzer_sys::fuzz_target;
|
||||
use wickra_core::{BarBuilder, Candle, KagiBars, PointAndFigureBars, RenkoBars};
|
||||
|
||||
/// Reinterpret the fuzz bytes as `[open, high, low, close, volume]` groups,
|
||||
/// keeping only structurally-valid candles whose magnitudes stay in a band that
|
||||
/// keeps Renko's brick count bounded.
|
||||
fn candles_from(data: &[f64]) -> Vec<Candle> {
|
||||
data.chunks_exact(5)
|
||||
.enumerate()
|
||||
.filter_map(|(i, ch)| Candle::new(ch[0], ch[1], ch[2], ch[3], ch[4], i as i64).ok())
|
||||
.filter(|candle| candle.high.abs() < 1.0e4 && candle.low.abs() < 1.0e4)
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[inline(never)]
|
||||
fn drive<B: BarBuilder>(mut builder: B, candles: &[Candle]) {
|
||||
for candle in candles {
|
||||
let _ = builder.update(*candle);
|
||||
}
|
||||
}
|
||||
|
||||
fuzz_target!(|data: Vec<f64>| {
|
||||
let candles = candles_from(&data);
|
||||
if candles.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
drive(RenkoBars::new(5.0).unwrap(), &candles);
|
||||
drive(KagiBars::new(5.0).unwrap(), &candles);
|
||||
drive(PointAndFigureBars::new(5.0, 3).unwrap(), &candles);
|
||||
|
||||
let _ = RenkoBars::new(5.0).unwrap().batch(&candles);
|
||||
let _ = KagiBars::new(5.0).unwrap().batch(&candles);
|
||||
let _ = PointAndFigureBars::new(5.0, 3).unwrap().batch(&candles);
|
||||
});
|
||||
@@ -15,7 +15,8 @@
|
||||
|
||||
use libfuzzer_sys::fuzz_target;
|
||||
use wickra_core::{
|
||||
AdaptiveCycle, Alma, Apo, Autocorrelation, AverageDrawdown, BatchExt, Beta, BollingerBands,
|
||||
AdaptiveCycle, Alma, AnchoredRsi, Apo, Autocorrelation, AverageDrawdown, BatchExt, Beta,
|
||||
BollingerBands,
|
||||
CalmarRatio, CenterOfGravity, Cfo, Cmo, CoefficientOfVariation, ConditionalValueAtRisk,
|
||||
ConnorsRsi, Coppock, CyberneticCycle, Decycler, DecyclerOscillator, Dema, DetrendedStdDev,
|
||||
DoubleBollinger, Dpo, DrawdownDuration, EhlersStochastic, ElderImpulse, Ema,
|
||||
@@ -54,6 +55,7 @@ fuzz_target!(|data: Vec<f64>| {
|
||||
drive(|| Ema::new(20).unwrap(), &data);
|
||||
drive(|| Wma::new(14).unwrap(), &data);
|
||||
drive(|| Rsi::new(14).unwrap(), &data);
|
||||
drive(AnchoredRsi::new, &data);
|
||||
drive(|| Dema::new(14).unwrap(), &data);
|
||||
drive(|| Tema::new(14).unwrap(), &data);
|
||||
drive(|| Hma::new(14).unwrap(), &data);
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
|
||||
use libfuzzer_sys::fuzz_target;
|
||||
use wickra_core::{
|
||||
AbandonedBaby, AccelerationBands, AcceleratorOscillator, AdOscillator, Adl, AdvanceBlock, Adx, Adxr, Alligator, AnchoredVwap, Aroon, AroonOscillator, Atr, AtrBands, AtrTrailingStop, AwesomeOscillator, AwesomeOscillatorHistogram, BalanceOfPower, BatchExt, BeltHold, Breakaway, Camarilla, Candle, Cci, ChaikinMoneyFlow, ChaikinOscillator, ChaikinVolatility, ChandeKrollStop, ChandelierExit, ChoppinessIndex, ClassicPivots, ClosingMarubozu, ConcealingBabySwallow, Counterattack, DemandIndex, DemarkPivots, Doji, DojiStar, Donchian, DonchianStop, DownsideGapThreeMethods, DragonflyDoji, EaseOfMovement, Engulfing, EveningDojiStar, Evwma, FallingThreeMethods, FibonacciPivots, ForceIndex, FractalChaosBands, GapSideBySideWhite, GarmanKlassVolatility, GravestoneDoji, Hammer, HangingMan, Harami, HeikinAshi, HiLoActivator, HighWave, Hikkake, HikkakeModified, HomingPigeon, HurstChannel, Ichimoku, IdenticalThreeCrows, InNeck, Indicator, Inertia, InitialBalance, InvertedHammer, Keltner, Kicking, KickingByLength, Kvo, LadderBottom, LongLeggedDoji, LongLine, MarketFacilitationIndex, Marubozu, MassIndex, MatHold, MatchingLow, MedianPrice, Mfi, MorningDojiStar, MorningEveningStar, Natr, Nvi, Obv, OnNeck, OpeningMarubozu, OpeningRange, ParkinsonVolatility, Pgo, PiercingDarkCloud, Psar, Pvi, RickshawMan, RisingThreeMethods, RogersSatchellVolatility, RollingVwap, Rvi, Rwi, SeparatingLines, ShootingStar, ShortLine, Smi, SpinningTop, StalledPattern, StarcBands, StickSandwich, Stochastic, SuperTrend, Takuri, TasukiGap, TdCombo, TdCountdown, TdDeMarker, TdDifferential, TdLines, TdOpen, TdPressure, TdRangeProjection, TdRei, TdRiskLevel, TdSequential, TdSetup, ThreeInside, ThreeLineStrike, ThreeOutside, ThreeSoldiersOrCrows, ThreeStarsInSouth, Thrusting, TrueRange, Tsv, TtmSqueeze, Tweezer, TwoCrows, TypicalPrice, UltimateOscillator, UniqueThreeRiver, UpsideGapThreeMethods, UpsideGapTwoCrows, ValueArea, VoltyStop, VolumeOscillator, VolumePriceTrend, Vortex, Vwap, VwapStdDevBands, Vwma, Vzo, WaveTrend, WeightedClose, WilliamsFractals, WilliamsR, WoodiePivots, YangZhangVolatility, YoyoExit, ZigZag
|
||||
AbandonedBaby, AccelerationBands, AcceleratorOscillator, AdOscillator, Adl, AdvanceBlock, Adx, Adxr, Alligator, AnchoredVwap, Aroon, AroonOscillator, Atr, AtrBands, AtrTrailingStop, AwesomeOscillator, AwesomeOscillatorHistogram, BalanceOfPower, BatchExt, BeltHold, Breakaway, Camarilla, Candle, Cci, ChaikinMoneyFlow, ChaikinOscillator, ChaikinVolatility, ChandeKrollStop, ChandelierExit, ChoppinessIndex, ClassicPivots, ClosingMarubozu, ConcealingBabySwallow, Counterattack, DemandIndex, DemarkPivots, Doji, DojiStar, Donchian, DonchianStop, DownsideGapThreeMethods, DragonflyDoji, EaseOfMovement, Engulfing, EveningDojiStar, Evwma, FallingThreeMethods, FibonacciPivots, ForceIndex, FractalChaosBands, GapSideBySideWhite, GarmanKlassVolatility, GravestoneDoji, Hammer, HangingMan, Harami, HeikinAshi, HiLoActivator, HighWave, Hikkake, HikkakeModified, HomingPigeon, HurstChannel, Ichimoku, IdenticalThreeCrows, InNeck, Indicator, Inertia, InitialBalance, InvertedHammer, Keltner, Kicking, KickingByLength, Kvo, LadderBottom, LongLeggedDoji, LongLine, MarketFacilitationIndex, Marubozu, MassIndex, MatHold, MatchingLow, MedianPrice, Mfi, MorningDojiStar, MorningEveningStar, Natr, Nvi, Obv, OnNeck, OpeningMarubozu, OpeningRange, ParkinsonVolatility, Pgo, PiercingDarkCloud, Psar, Pvi, RickshawMan, RisingThreeMethods, RogersSatchellVolatility, RollingVwap, Rvi, Rwi, SeparatingLines, ShootingStar, ShortLine, Smi, SpinningTop, StalledPattern, StarcBands, StickSandwich, Stochastic, SuperTrend, Takuri, TasukiGap, TdCombo, TdCountdown, TdDeMarker, TdDifferential, TdLines, TdOpen, TdPressure, TdRangeProjection, TdRei, TdRiskLevel, TdSequential, TdSetup, ThreeInside, ThreeLineStrike, ThreeOutside, ThreeSoldiersOrCrows, ThreeStarsInSouth, Thrusting, TpoProfile, TrueRange, Tsv, TtmSqueeze, Tweezer, TwoCrows, TypicalPrice, UltimateOscillator, UniqueThreeRiver, UpsideGapThreeMethods, UpsideGapTwoCrows, ValueArea, VoltyStop, VolumeOscillator, VolumePriceTrend, VolumeProfile, Vortex, Vwap, VwapStdDevBands, Vwma, Vzo, WaveTrend, WeightedClose, WilliamsFractals, WilliamsR, WoodiePivots, YangZhangVolatility, YoyoExit, ZigZag
|
||||
};
|
||||
|
||||
/// Convert a flat `f64` stream into a `Vec<Candle>` by chunking it into
|
||||
@@ -155,6 +155,8 @@ fuzz_target!(|data: Vec<f64>| {
|
||||
|
||||
// --- Market Profile (multi-output) ---
|
||||
drive(|| ValueArea::new(20, 50, 0.70).unwrap(), &candles);
|
||||
drive(|| VolumeProfile::new(20, 50).unwrap(), &candles);
|
||||
drive(|| TpoProfile::new(20, 50).unwrap(), &candles);
|
||||
drive(|| InitialBalance::new(12).unwrap(), &candles);
|
||||
drive(|| OpeningRange::new(6).unwrap(), &candles);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user