Compare commits

..

4 Commits

Author SHA1 Message Date
kingchenc 13c8250488 release: bump 0.7.3 -> 0.7.4 (#221)
Version bump **0.7.3 → 0.7.4** for the B19 Alt-Chart Bars batch (7 new bar builders, 507 → 514).

Bumps `Cargo.toml` (+ `wickra-core` dep), `Cargo.lock`, `pyproject.toml`, the Node `package.json` and its six platform packages, both `package-lock.json` files, and the CHANGELOG (`[Unreleased]` → `[0.7.4]` with compare URLs).
2026-06-08 14:33:43 +02:00
kingchenc e5305ffa94 feat: add 7 alt-chart bar builders (B19) (#220)
Adds seven information-driven bar builders to the **Alt-Chart Bars** family, the final batch of the family-deepening run. Indicator count **507 → 514**.

## Builders
All implement the `BarBuilder` trait (`update(Candle) -> Vec<Bar>`), emitting a data-dependent number of completed bars per candle.

| Builder | Driver | Bar fields |
|---------|--------|-----------|
| `RangeBars` | close | open, close, direction |
| `TickBars` | OHLCV | open, high, low, close, volume |
| `VolumeBars` | OHLCV | open, high, low, close, volume |
| `DollarBars` (Lopez de Prado) | OHLCV | + dollar |
| `ImbalanceBars` | OHLC | + imbalance, direction |
| `RunBars` | OHLC | + length, direction |
| `ThreeLineBreakBars` | close | open, close, direction |

## Touchpoints
Seven core modules (each with full unit tests), `mod.rs`/`lib.rs` (builders counted, bar element types on their own re-export lines), README family rows, Python/Node/WASM hand-written bindings for the variable-length output (Python tuples + `(k, N)` ndarray; Node `Vec<object>`; WASM array of objects), the `bar_builder_update_candle` fuzz target, dedicated Python + Node tests, the `BAR_BUILDERS` completeness exclusion, and CHANGELOG.

## Verification
- `cargo test -p wickra-core --lib` — 4207 passed
- `cargo test -p wickra-core --doc` — 464 passed
- `cargo clippy --workspace --all-targets --all-features -- -D warnings` — clean
- `npm test` (node) — 584 passed
- `pytest` (python) — 957 passed
2026-06-08 14:32:40 +02:00
kingchenc 46be7a54ea release: bump 0.7.2 -> 0.7.3 (#219)
Version bump **0.7.2 → 0.7.3** for the B18 Risk / Performance batch (9 new indicators, 498 → 507).

Bumps `Cargo.toml` (+ `wickra-core` dep), `Cargo.lock`, `pyproject.toml`, the Node `package.json` and its six platform packages, both `package-lock.json` files, and the CHANGELOG (`[Unreleased]` → `[0.7.3]` with compare URLs).
2026-06-08 13:29:39 +02:00
kingchenc bca61322b5 feat: add 9 Risk / Performance indicators (B18) (#218)
Adds nine risk/performance metrics to the existing **Risk / Performance** family, all consuming a per-period return series (`f64` in, `f64` out). Indicator count **498 → 507**.

## Indicators

Single-param (`new(period)`, macro bindings):
- **SterlingRatio** — mean return over average drawdown of the equity curve.
- **BurkeRatio** — return over root-sum-squared drawdowns.
- **MartinRatio** — Ulcer Performance Index; return over RMS percentage drawdown.
- **TailRatio** — 95th percentile over the absolute 5th percentile return.
- **KRatio** — Kestner; equity-curve OLS slope over the standard error of that slope.
- **CommonSenseRatio** — tail ratio times gain-to-pain.
- **GainToPainRatio** — sum of returns over the sum of absolute losses.

Multi-param (hand-written Python/Node bindings, variadic WASM macro):
- **UpsidePotentialRatio** — `new(period, mar)`; upside mean over downside deviation (Sortino philosophy).
- **M2Measure** — `new(period, risk_free, benchmark_stddev)`; Modigliani M², Sharpe rescaled into benchmark return units.

## Touchpoints
Core modules + unit tests, `mod.rs`/`lib.rs` wiring, Python/Node/WASM bindings (`index.d.ts`/`index.js` regenerated), fuzz drive lines, Python `SCALAR` registry + Node factories, CHANGELOG, and the indicator counters.

## Verification
- `cargo test -p wickra-core --lib` — 4149 passed
- `cargo test -p wickra-core --doc` — 457 passed
- `cargo clippy --workspace --all-targets --all-features -- -D warnings` — clean
- `npm test` (node) — 577 passed
- `pytest` (python) — 947 passed
2026-06-08 13:23:01 +02:00
44 changed files with 6362 additions and 132 deletions
+23 -1
View File
@@ -7,6 +7,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
## [0.7.4] - 2026-06-08
- **Three-Line Break** — Three-line-break bars (reversal needs N-line break) (`THREE_LINE_BREAK_BARS`).
- **Run** — Run bars (consecutive same-direction tick runs) (`RUN_BARS`).
- **Imbalance** — Imbalance bars (tick-rule signed imbalance threshold) (`IMBALANCE_BARS`).
- **Dollar** — Dollar bars (fixed traded value per bar, Lopez de Prado) (`DOLLAR_BARS`).
- **Volume** — Volume bars (fixed traded volume per bar) (`VOLUME_BARS`).
- **Tick** — Tick bars (fixed candle count per bar) (`TICK_BARS`).
- **Range** — Range bars (fixed price-range bricks) (`RANGE_BARS`).
## [0.7.3] - 2026-06-08
- **M2Measure** — M2 measure (Modigliani; Sharpe expressed in benchmark return units) (`M2Measure`).
- **UpsidePotentialRatio** — Upside Potential Ratio (upside mean over downside deviation) (`UpsidePotentialRatio`).
- **GainToPainRatio** — Gain-to-Pain Ratio (sum of returns over sum of losses) (`GainToPainRatio`).
- **CommonSenseRatio** — Common Sense Ratio (tail ratio times gain-to-pain) (`CommonSenseRatio`).
- **KRatio** — K-Ratio (Kestner; equity-curve slope over its standard error) (`KRatio`).
- **TailRatio** — Tail Ratio (95th over absolute 5th return percentile) (`TailRatio`).
- **MartinRatio** — Martin Ratio (Ulcer Performance Index; return over RMS drawdown) (`MartinRatio`).
- **BurkeRatio** — Burke Ratio (return over root-sum-squared drawdowns) (`BurkeRatio`).
- **SterlingRatio** — Sterling Ratio (mean return over average drawdown) (`SterlingRatio`).
## [0.7.2] - 2026-06-08
- **Composite Profile** — multi-session composite volume profile exposing POC, VAH and VAL (`CompositeProfile`).
- **High/Low Volume Nodes** — highest- and lowest-volume price nodes in the profile (`HighLowVolumeNodes`).
@@ -1396,7 +1416,9 @@ 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.7.2...HEAD
[Unreleased]: https://github.com/wickra-lib/wickra/compare/v0.7.4...HEAD
[0.7.4]: https://github.com/wickra-lib/wickra/compare/v0.7.3...v0.7.4
[0.7.3]: https://github.com/wickra-lib/wickra/compare/v0.7.2...v0.7.3
[0.7.2]: https://github.com/wickra-lib/wickra/compare/v0.7.1...v0.7.2
[0.7.1]: https://github.com/wickra-lib/wickra/compare/v0.7.0...v0.7.1
[0.7.0]: https://github.com/wickra-lib/wickra/compare/v0.6.9...v0.7.0
Generated
+8 -8
View File
@@ -1944,7 +1944,7 @@ dependencies = [
[[package]]
name = "wickra"
version = "0.7.2"
version = "0.7.4"
dependencies = [
"approx",
"criterion",
@@ -1955,7 +1955,7 @@ dependencies = [
[[package]]
name = "wickra-bench"
version = "0.7.2"
version = "0.7.4"
dependencies = [
"criterion",
"kand",
@@ -1967,7 +1967,7 @@ dependencies = [
[[package]]
name = "wickra-core"
version = "0.7.2"
version = "0.7.4"
dependencies = [
"approx",
"proptest",
@@ -1977,7 +1977,7 @@ dependencies = [
[[package]]
name = "wickra-data"
version = "0.7.2"
version = "0.7.4"
dependencies = [
"approx",
"csv",
@@ -1994,7 +1994,7 @@ dependencies = [
[[package]]
name = "wickra-examples"
version = "0.7.2"
version = "0.7.4"
dependencies = [
"serde_json",
"tokio",
@@ -2004,7 +2004,7 @@ dependencies = [
[[package]]
name = "wickra-node"
version = "0.7.2"
version = "0.7.4"
dependencies = [
"napi",
"napi-build",
@@ -2014,7 +2014,7 @@ dependencies = [
[[package]]
name = "wickra-python"
version = "0.7.2"
version = "0.7.4"
dependencies = [
"numpy",
"pyo3",
@@ -2023,7 +2023,7 @@ dependencies = [
[[package]]
name = "wickra-wasm"
version = "0.7.2"
version = "0.7.4"
dependencies = [
"console_error_panic_hook",
"js-sys",
+2 -2
View File
@@ -13,7 +13,7 @@ members = [
exclude = ["fuzz"]
[workspace.package]
version = "0.7.2"
version = "0.7.4"
authors = ["kingchenc <support@wickra.org>"]
edition = "2021"
rust-version = "1.86"
@@ -25,7 +25,7 @@ keywords = ["finance", "trading", "indicators", "technical-analysis", "ta"]
categories = ["finance", "mathematics", "science"]
[workspace.dependencies]
wickra-core = { path = "crates/wickra-core", version = "0.7.2" }
wickra-core = { path = "crates/wickra-core", version = "0.7.4" }
thiserror = "2"
rayon = "1.10"
+8 -8
View File
@@ -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=498" 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=514" alt="Wickra — streaming-first technical indicators" width="100%"></a>
</p>
[![CI](https://github.com/wickra-lib/wickra/actions/workflows/ci.yml/badge.svg)](https://github.com/wickra-lib/wickra/actions/workflows/ci.yml)
@@ -48,7 +48,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 498 indicators; start at the
every one of the 514 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),
@@ -66,7 +66,7 @@ an afterthought — **live, tick-by-tick data** — without giving up the breadt
a full batch library, and without making you reimplement your indicators four
times to get there.
- **The biggest streaming-native catalogue, period.** 498 indicators across 24
- **The biggest streaming-native catalogue, period.** 514 indicators across 24
families — candlesticks, harmonic & chart patterns, market profile, market
breadth, Renko/Kagi/Point&Figure bars, Ehlers DSP cycles, risk/performance
metrics — every single one updating in **O(1) per tick**. TA-Lib ships ~150 and
@@ -77,7 +77,7 @@ times to get there.
- **Correct by construction, not by hope.** Every `update` validates its input,
runs a real warmup, and returns an `Option` so a single bad tick can't silently
poison state. `batch == streaming` is **bit-exact, fuzzed and 100 %-line-covered
for all 498 indicators**.
for all 514 indicators**.
- **Orders of magnitude faster where it counts.** In streaming Wickra is **1156×**
faster than the only other incremental peer and **thousands of times** faster
than recompute-on-every-tick libraries. On batch it wins several rows outright
@@ -95,7 +95,7 @@ Every other library forces one of those compromises. Wickra doesn't:
| Library | Install | Streaming | Languages | Indicators | Active |
|------------------|-------------|-------------|-----------------------------|-----------:|--------|
| **★&nbsp;Wickra**| **clean** | **yes, O(1)** | **Python · Node · WASM · Rust** | **498** | **yes** |
| **★&nbsp;Wickra**| **clean** | **yes, O(1)** | **Python · Node · WASM · Rust** | **514** | **yes** |
| kand | clean | yes | Python · WASM · Rust | ~60 | yes |
| ta-rs | clean | yes | Rust only | ~30 | stale |
| yata | clean | partial | Rust only | ~35 | yes |
@@ -128,7 +128,7 @@ Full tables (Rust + Python, streaming + batch) and how to reproduce them live in
## Indicators
498 streaming-first indicators across twenty-four families. Every one passes the
514 streaming-first indicators across twenty-four 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).
@@ -148,7 +148,7 @@ 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, Central Pivot Range, Murrey Math Lines, Andrews Pitchfork, Volume-Weighted Support/Resistance, Pivot Reversal |
| 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, TD Camouflage, TD Clop, TD Clopwin, TD Propulsion, TD Trap, TD D-Wave, TD Moving Averages |
| Ichimoku & Charts | Ichimoku Kinko Hyo (Tenkan, Kijun, Senkou A/B, Chikou), Heikin-Ashi, Heikin-Ashi Oscillator, Three Line Break, Smoothed Heikin-Ashi, Equivolume, CandleVolume |
| Alt-Chart Bars | Renko (box-size bricks), Kagi (reversal-amount lines), Point & Figure (X/O columns) |
| Alt-Chart Bars | Renko (box-size bricks), Kagi (reversal-amount lines), Point & Figure (X/O columns), Range, Tick, Volume, Dollar, Imbalance, Run, Three-Line Break |
| 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, Tristar, Harami Cross, Tower Top/Bottom, Dumpling Top, New Price Lines, Frying Pan Bottom |
| Chart Patterns | Double Top / Bottom, Triple Top / Bottom, Head and Shoulders, Triangle (asc/desc/sym), Wedge (rising/falling), Flag / Pennant, Rectangle / Range, Cup and Handle |
| Harmonic Patterns | AB=CD, Gartley, Butterfly, Bat, Crab, Shark, Cypher, Three Drives |
@@ -237,7 +237,7 @@ A Python live-trading example using the public `websockets` package lives at
```
wickra/
├── crates/
│ ├── wickra-core/ core engine + all 498 indicators
│ ├── wickra-core/ core engine + all 514 indicators
│ ├── wickra/ top-level facade crate (publishes on crates.io) + benches/
│ ├── wickra-data/ CSV reader, tick aggregator, live exchange feeds
│ └── wickra-bench/ internal cross-library benchmark harness (not published)
+12 -1
View File
@@ -14,7 +14,18 @@ const wickra = require('..');
// 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']);
const BAR_BUILDERS = new Set([
'RenkoBars',
'KagiBars',
'PointAndFigureBars',
'RangeBars',
'TickBars',
'VolumeBars',
'DollarBars',
'ImbalanceBars',
'RunBars',
'ThreeLineBreakBars',
]);
// An "indicator class" is an exported constructor whose prototype carries the
// streaming `update` method. This excludes `version` (a plain function), the bar
@@ -28,6 +28,15 @@ function num(v) {
// --- Scalar indicators: update(value) vs batch(prices) ---
const scalarFactories = {
M2Measure: () => new wickra.M2Measure(20, 0.0, 0.02),
UpsidePotentialRatio: () => new wickra.UpsidePotentialRatio(20, 0.0),
GainToPainRatio: () => new wickra.GainToPainRatio(12),
CommonSenseRatio: () => new wickra.CommonSenseRatio(20),
KRatio: () => new wickra.KRatio(30),
TailRatio: () => new wickra.TailRatio(20),
MartinRatio: () => new wickra.MartinRatio(14),
BurkeRatio: () => new wickra.BurkeRatio(12),
SterlingRatio: () => new wickra.SterlingRatio(12),
AUTOCORRPGRAM: () => new wickra.AUTOCORRPGRAM(10, 48),
EVENBETTERSINE: () => new wickra.EVENBETTERSINE(40, 10),
BANDPASS: () => new wickra.BANDPASS(20, 0.3),
@@ -1760,3 +1769,72 @@ test('PointAndFigureBars closes a column on a 3-box reversal', () => {
assert.equal(col[0].direction, 1);
assert.ok(Math.abs(col[0].high - 15) < 1e-9 && Math.abs(col[0].low - 10) < 1e-9);
});
test('RangeBars prints aligned bars on an up move', () => {
const rb = new wickra.RangeBars(1.0);
assert.deepEqual(rb.update(10), []); // seed
const up = rb.update(13);
assert.equal(up.length, 3);
assert.ok(Math.abs(up[0].open - 10) < 1e-9 && Math.abs(up[2].close - 13) < 1e-9);
assert.ok(up.every((b) => b.direction === 1));
});
test('TickBars groups a fixed number of candles', () => {
const tb = new wickra.TickBars(2);
assert.deepEqual(tb.update(10, 11, 9, 10.5, 100), []);
const out = tb.update(10.5, 12, 10, 11, 150);
assert.equal(out.length, 1);
assert.ok(Math.abs(out[0].open - 10) < 1e-9);
assert.ok(Math.abs(out[0].high - 12) < 1e-9);
assert.ok(Math.abs(out[0].low - 9) < 1e-9);
assert.ok(Math.abs(out[0].close - 11) < 1e-9);
assert.ok(Math.abs(out[0].volume - 250) < 1e-9);
});
test('VolumeBars closes when accumulated volume crosses the threshold', () => {
const vb = new wickra.VolumeBars(100);
assert.deepEqual(vb.update(10, 10, 10, 10, 60), []);
const out = vb.update(10.5, 10.5, 10.5, 10.5, 60);
assert.equal(out.length, 1);
assert.ok(Math.abs(out[0].volume - 120) < 1e-9);
});
test('DollarBars closes when traded value crosses the threshold', () => {
const db = new wickra.DollarBars(1000);
assert.deepEqual(db.update(10, 10, 10, 10, 60), []);
const out = db.update(10, 10, 10, 10, 60);
assert.equal(out.length, 1);
assert.ok(Math.abs(out[0].dollar - 1200) < 1e-9);
assert.ok(Math.abs(out[0].volume - 120) < 1e-9);
});
test('ImbalanceBars closes a buy bar at the threshold', () => {
const ib = new wickra.ImbalanceBars(3.0);
ib.update(10, 10, 10, 10);
ib.update(11, 11, 11, 11);
ib.update(12, 12, 12, 12);
const out = ib.update(13, 13, 13, 13);
assert.equal(out.length, 1);
assert.equal(out[0].direction, 1);
assert.ok(Math.abs(out[0].imbalance - 3) < 1e-9);
});
test('RunBars closes a buy run at the run length', () => {
const rb = new wickra.RunBars(3);
rb.update(10, 10, 10, 10);
rb.update(11, 11, 11, 11);
rb.update(12, 12, 12, 12);
const out = rb.update(13, 13, 13, 13);
assert.equal(out.length, 1);
assert.equal(out[0].direction, 1);
assert.equal(out[0].length, 3);
});
test('ThreeLineBreakBars draws a rising line', () => {
const tlb = new wickra.ThreeLineBreakBars(3);
assert.deepEqual(tlb.update(10), []); // seed
const out = tlb.update(11);
assert.equal(out.length, 1);
assert.equal(out[0].direction, 1);
assert.ok(Math.abs(out[0].open - 10) < 1e-9 && Math.abs(out[0].close - 11) < 1e-9);
});
+185
View File
@@ -468,6 +468,54 @@ export interface PnfColumnValue {
high: number
low: number
}
export interface RangeBarValue {
open: number
close: number
direction: number
}
export interface TickBarValue {
open: number
high: number
low: number
close: number
volume: number
}
export interface VolumeBarValue {
open: number
high: number
low: number
close: number
volume: number
}
export interface DollarBarValue {
open: number
high: number
low: number
close: number
volume: number
dollar: number
}
export interface ImbalanceBarValue {
open: number
high: number
low: number
close: number
imbalance: number
direction: number
}
export interface RunBarValue {
open: number
high: number
low: number
close: number
length: number
direction: number
}
export interface LineBreakBarValue {
open: number
close: number
direction: number
}
export interface SessionHighLowValue {
high: number
low: number
@@ -1176,6 +1224,87 @@ export declare class UNIVERSALOSC {
isReady(): boolean
warmupPeriod(): number
}
export type SterlingRatioNode = SterlingRatio
export declare class SterlingRatio {
constructor(period: number)
update(value: number): number | null
batch(prices: Array<number>): Array<number>
reset(): void
isReady(): boolean
warmupPeriod(): number
}
export type BurkeRatioNode = BurkeRatio
export declare class BurkeRatio {
constructor(period: number)
update(value: number): number | null
batch(prices: Array<number>): Array<number>
reset(): void
isReady(): boolean
warmupPeriod(): number
}
export type MartinRatioNode = MartinRatio
export declare class MartinRatio {
constructor(period: number)
update(value: number): number | null
batch(prices: Array<number>): Array<number>
reset(): void
isReady(): boolean
warmupPeriod(): number
}
export type TailRatioNode = TailRatio
export declare class TailRatio {
constructor(period: number)
update(value: number): number | null
batch(prices: Array<number>): Array<number>
reset(): void
isReady(): boolean
warmupPeriod(): number
}
export type KRatioNode = KRatio
export declare class KRatio {
constructor(period: number)
update(value: number): number | null
batch(prices: Array<number>): Array<number>
reset(): void
isReady(): boolean
warmupPeriod(): number
}
export type CommonSenseRatioNode = CommonSenseRatio
export declare class CommonSenseRatio {
constructor(period: number)
update(value: number): number | null
batch(prices: Array<number>): Array<number>
reset(): void
isReady(): boolean
warmupPeriod(): number
}
export type GainToPainRatioNode = GainToPainRatio
export declare class GainToPainRatio {
constructor(period: number)
update(value: number): number | null
batch(prices: Array<number>): Array<number>
reset(): void
isReady(): boolean
warmupPeriod(): number
}
export type UpsidePotentialRatioNode = UpsidePotentialRatio
export declare class UpsidePotentialRatio {
constructor(period: number, mar: number)
update(value: number): number | null
batch(prices: Array<number>): Array<number>
reset(): void
isReady(): boolean
warmupPeriod(): number
}
export type M2MeasureNode = M2Measure
export declare class M2Measure {
constructor(period: number, riskFree: number, benchmarkStddev: number)
update(value: number): number | null
batch(prices: Array<number>): Array<number>
reset(): void
isReady(): boolean
warmupPeriod(): number
}
export type BandpassFilterNode = BANDPASS
export declare class BANDPASS {
constructor(period: number, bandwidth: number)
@@ -4947,6 +5076,62 @@ export declare class PointAndFigureBars {
reversal(): number
reset(): void
}
export type RangeBarsNode = RangeBars
export declare class RangeBars {
constructor(range: number)
update(close: number): Array<RangeBarValue>
batch(close: Array<number>): Array<RangeBarValue>
range(): number
reset(): void
}
export type TickBarsNode = TickBars
export declare class TickBars {
constructor(ticks: number)
update(open: number, high: number, low: number, close: number, volume: number): Array<TickBarValue>
batch(open: Array<number>, high: Array<number>, low: Array<number>, close: Array<number>, volume: Array<number>): Array<TickBarValue>
ticks(): number
reset(): void
}
export type VolumeBarsNode = VolumeBars
export declare class VolumeBars {
constructor(volumePerBar: number)
update(open: number, high: number, low: number, close: number, volume: number): Array<VolumeBarValue>
batch(open: Array<number>, high: Array<number>, low: Array<number>, close: Array<number>, volume: Array<number>): Array<VolumeBarValue>
volumePerBar(): number
reset(): void
}
export type DollarBarsNode = DollarBars
export declare class DollarBars {
constructor(dollarPerBar: number)
update(open: number, high: number, low: number, close: number, volume: number): Array<DollarBarValue>
batch(open: Array<number>, high: Array<number>, low: Array<number>, close: Array<number>, volume: Array<number>): Array<DollarBarValue>
dollarPerBar(): number
reset(): void
}
export type ImbalanceBarsNode = ImbalanceBars
export declare class ImbalanceBars {
constructor(threshold: number)
update(open: number, high: number, low: number, close: number): Array<ImbalanceBarValue>
batch(open: Array<number>, high: Array<number>, low: Array<number>, close: Array<number>): Array<ImbalanceBarValue>
threshold(): number
reset(): void
}
export type RunBarsNode = RunBars
export declare class RunBars {
constructor(runLength: number)
update(open: number, high: number, low: number, close: number): Array<RunBarValue>
batch(open: Array<number>, high: Array<number>, low: Array<number>, close: Array<number>): Array<RunBarValue>
runLength(): number
reset(): void
}
export type ThreeLineBreakBarsNode = ThreeLineBreakBars
export declare class ThreeLineBreakBars {
constructor(lines: number)
update(close: number): Array<LineBreakBarValue>
batch(close: Array<number>): Array<LineBreakBarValue>
lines(): number
reset(): void
}
export type AlphaNode = Alpha
export declare class Alpha {
constructor(period: number, riskFree: number)
+17 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "wickra-darwin-arm64",
"version": "0.7.2",
"version": "0.7.4",
"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 -1
View File
@@ -1,6 +1,6 @@
{
"name": "wickra-darwin-x64",
"version": "0.7.2",
"version": "0.7.4",
"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.7.2",
"version": "0.7.4",
"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 -1
View File
@@ -1,6 +1,6 @@
{
"name": "wickra-linux-x64-gnu",
"version": "0.7.2",
"version": "0.7.4",
"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.7.2",
"version": "0.7.4",
"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.7.2",
"version": "0.7.4",
"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": [
+20 -20
View File
@@ -1,12 +1,12 @@
{
"name": "wickra",
"version": "0.7.2",
"version": "0.7.4",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "wickra",
"version": "0.7.2",
"version": "0.7.4",
"license": "MIT OR Apache-2.0",
"devDependencies": {
"@napi-rs/cli": "^2.18.0"
@@ -15,12 +15,12 @@
"node": ">= 18"
},
"optionalDependencies": {
"wickra-darwin-arm64": "0.7.2",
"wickra-darwin-x64": "0.7.2",
"wickra-linux-arm64-gnu": "0.7.2",
"wickra-linux-x64-gnu": "0.7.2",
"wickra-win32-arm64-msvc": "0.7.2",
"wickra-win32-x64-msvc": "0.7.2"
"wickra-darwin-arm64": "0.7.4",
"wickra-darwin-x64": "0.7.4",
"wickra-linux-arm64-gnu": "0.7.4",
"wickra-linux-x64-gnu": "0.7.4",
"wickra-win32-arm64-msvc": "0.7.4",
"wickra-win32-x64-msvc": "0.7.4"
}
},
"node_modules/@napi-rs/cli": {
@@ -41,8 +41,8 @@
}
},
"node_modules/wickra-darwin-arm64": {
"version": "0.7.2",
"resolved": "https://registry.npmjs.org/wickra-darwin-arm64/-/wickra-darwin-arm64-0.7.2.tgz",
"version": "0.7.4",
"resolved": "https://registry.npmjs.org/wickra-darwin-arm64/-/wickra-darwin-arm64-0.7.4.tgz",
"integrity": "sha512-4eZiBR/yGUdr4nzhEUFy2i69XgNx64iI2ax/LPamsThgylC0KpHOZKK19QzJ2d9KbK4C8nMjME5FLuR+4GNEwQ==",
"cpu": [
"arm64"
@@ -57,8 +57,8 @@
}
},
"node_modules/wickra-darwin-x64": {
"version": "0.7.2",
"resolved": "https://registry.npmjs.org/wickra-darwin-x64/-/wickra-darwin-x64-0.7.2.tgz",
"version": "0.7.4",
"resolved": "https://registry.npmjs.org/wickra-darwin-x64/-/wickra-darwin-x64-0.7.4.tgz",
"integrity": "sha512-6hf8zI3QPjTFp4zCpmgUwDvNtu6jHqNUHKD5e55POo0CgA52HkpyxSPtVm8TGTIZDI7kPjlbOdBM8CJ76mmXwA==",
"cpu": [
"x64"
@@ -73,8 +73,8 @@
}
},
"node_modules/wickra-linux-arm64-gnu": {
"version": "0.7.2",
"resolved": "https://registry.npmjs.org/wickra-linux-arm64-gnu/-/wickra-linux-arm64-gnu-0.7.2.tgz",
"version": "0.7.4",
"resolved": "https://registry.npmjs.org/wickra-linux-arm64-gnu/-/wickra-linux-arm64-gnu-0.7.4.tgz",
"integrity": "sha512-kSe6y0xBMSiqdPLXNjwop5WZdHtvdBNKSEBCwZ4hFq33p4apW25/wrlzv9/oDuyD4kuPabJEhCCnFOplh58CUg==",
"cpu": [
"arm64"
@@ -89,8 +89,8 @@
}
},
"node_modules/wickra-linux-x64-gnu": {
"version": "0.7.2",
"resolved": "https://registry.npmjs.org/wickra-linux-x64-gnu/-/wickra-linux-x64-gnu-0.7.2.tgz",
"version": "0.7.4",
"resolved": "https://registry.npmjs.org/wickra-linux-x64-gnu/-/wickra-linux-x64-gnu-0.7.4.tgz",
"integrity": "sha512-tWBWS4qz7hxM4xnpFb59bhf6TaLwXq0Z3jEa/2l7r8PiHA94g8r8S53NRMiT+4yiL5hSWe/nUiC/YXdRrhEZ4g==",
"cpu": [
"x64"
@@ -105,8 +105,8 @@
}
},
"node_modules/wickra-win32-arm64-msvc": {
"version": "0.7.2",
"resolved": "https://registry.npmjs.org/wickra-win32-arm64-msvc/-/wickra-win32-arm64-msvc-0.7.2.tgz",
"version": "0.7.4",
"resolved": "https://registry.npmjs.org/wickra-win32-arm64-msvc/-/wickra-win32-arm64-msvc-0.7.4.tgz",
"integrity": "sha512-EXIckHxAtF75PUGDKRzXyqMe9ldP0JjSdu68WFN6iJfp+McYrGu6h40TEJlQ/oUEIoPqiZB/xhVyo/el5Lg7zw==",
"cpu": [
"arm64"
@@ -121,8 +121,8 @@
}
},
"node_modules/wickra-win32-x64-msvc": {
"version": "0.7.2",
"resolved": "https://registry.npmjs.org/wickra-win32-x64-msvc/-/wickra-win32-x64-msvc-0.7.2.tgz",
"version": "0.7.4",
"resolved": "https://registry.npmjs.org/wickra-win32-x64-msvc/-/wickra-win32-x64-msvc-0.7.4.tgz",
"integrity": "sha512-Yfsqq1Xwp6hdxMyLze411vNdo7BDwI6+lPSe7A9XdqyPecNDbtKwYLpsal2r8EHbNzqM+R8XnuRtUaEQS5VlUQ==",
"cpu": [
"x64"
+7 -7
View File
@@ -1,6 +1,6 @@
{
"name": "wickra",
"version": "0.7.2",
"version": "0.7.4",
"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.7.2",
"wickra-linux-arm64-gnu": "0.7.2",
"wickra-darwin-x64": "0.7.2",
"wickra-darwin-arm64": "0.7.2",
"wickra-win32-x64-msvc": "0.7.2",
"wickra-win32-arm64-msvc": "0.7.2"
"wickra-linux-x64-gnu": "0.7.4",
"wickra-linux-arm64-gnu": "0.7.4",
"wickra-darwin-x64": "0.7.4",
"wickra-darwin-arm64": "0.7.4",
"wickra-win32-x64-msvc": "0.7.4",
"wickra-win32-arm64-msvc": "0.7.4"
},
"scripts": {
"build": "napi build --platform --release",
+635
View File
@@ -245,9 +245,91 @@ node_scalar_indicator!(
"UNIVERSALOSC",
wc::UniversalOscillator
);
node_scalar_indicator!(SterlingRatioNode, "SterlingRatio", wc::SterlingRatio);
node_scalar_indicator!(BurkeRatioNode, "BurkeRatio", wc::BurkeRatio);
node_scalar_indicator!(MartinRatioNode, "MartinRatio", wc::MartinRatio);
node_scalar_indicator!(TailRatioNode, "TailRatio", wc::TailRatio);
node_scalar_indicator!(KRatioNode, "KRatio", wc::KRatio);
node_scalar_indicator!(
CommonSenseRatioNode,
"CommonSenseRatio",
wc::CommonSenseRatio
);
node_scalar_indicator!(GainToPainRatioNode, "GainToPainRatio", wc::GainToPainRatio);
// Multi-arg Ehlers scalars: hand-written (node_scalar_indicator! is single-period).
#[napi(js_name = "UpsidePotentialRatio")]
pub struct UpsidePotentialRatioNode {
inner: wc::UpsidePotentialRatio,
}
#[napi]
impl UpsidePotentialRatioNode {
#[napi(constructor)]
pub fn new(period: u32, mar: f64) -> napi::Result<Self> {
Ok(Self {
inner: wc::UpsidePotentialRatio::new(period as usize, mar).map_err(map_err)?,
})
}
#[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
}
}
#[napi(js_name = "M2Measure")]
pub struct M2MeasureNode {
inner: wc::M2Measure,
}
#[napi]
impl M2MeasureNode {
#[napi(constructor)]
pub fn new(period: u32, risk_free: f64, benchmark_stddev: f64) -> napi::Result<Self> {
Ok(Self {
inner: wc::M2Measure::new(period as usize, risk_free, benchmark_stddev)
.map_err(map_err)?,
})
}
#[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
}
}
#[napi(js_name = "BANDPASS")]
pub struct BandpassFilterNode {
inner: wc::BandpassFilter,
@@ -17736,6 +17818,559 @@ impl PointAndFigureBarsNode {
}
}
#[napi(object)]
pub struct RangeBarValue {
pub open: f64,
pub close: f64,
pub direction: i32,
}
#[napi(js_name = "RangeBars")]
pub struct RangeBarsNode {
inner: wc::RangeBars,
}
#[napi]
impl RangeBarsNode {
#[napi(constructor)]
pub fn new(range: f64) -> napi::Result<Self> {
Ok(Self {
inner: wc::RangeBars::new(range).map_err(map_err)?,
})
}
#[napi]
pub fn update(&mut self, close: f64) -> napi::Result<Vec<RangeBarValue>> {
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| RangeBarValue {
open: b.open,
close: b.close,
direction: i32::from(b.direction),
})
.collect())
}
#[napi]
pub fn batch(&mut self, close: Vec<f64>) -> napi::Result<Vec<RangeBarValue>> {
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(RangeBarValue {
open: b.open,
close: b.close,
direction: i32::from(b.direction),
});
}
}
Ok(out)
}
#[napi]
pub fn range(&self) -> f64 {
self.inner.range()
}
#[napi]
pub fn reset(&mut self) {
self.inner.reset();
}
}
#[napi(object)]
pub struct TickBarValue {
pub open: f64,
pub high: f64,
pub low: f64,
pub close: f64,
pub volume: f64,
}
#[napi(js_name = "TickBars")]
pub struct TickBarsNode {
inner: wc::TickBars,
}
#[napi]
impl TickBarsNode {
#[napi(constructor)]
pub fn new(ticks: u32) -> napi::Result<Self> {
Ok(Self {
inner: wc::TickBars::new(ticks as usize).map_err(map_err)?,
})
}
#[napi]
pub fn update(
&mut self,
open: f64,
high: f64,
low: f64,
close: f64,
volume: f64,
) -> napi::Result<Vec<TickBarValue>> {
let candle = wc::Candle::new(open, high, low, close, volume, 0).map_err(map_err)?;
Ok(self
.inner
.update(candle)
.into_iter()
.map(|b| TickBarValue {
open: b.open,
high: b.high,
low: b.low,
close: b.close,
volume: b.volume,
})
.collect())
}
#[napi]
pub fn batch(
&mut self,
open: Vec<f64>,
high: Vec<f64>,
low: Vec<f64>,
close: Vec<f64>,
volume: Vec<f64>,
) -> napi::Result<Vec<TickBarValue>> {
if open.len() != high.len()
|| high.len() != low.len()
|| low.len() != close.len()
|| close.len() != volume.len()
{
return Err(NapiError::from_reason(
"open, high, low, close, volume must be equal length".to_string(),
));
}
let mut out = Vec::new();
for i in 0..open.len() {
let candle = wc::Candle::new(open[i], high[i], low[i], close[i], volume[i], 0)
.map_err(map_err)?;
for b in self.inner.update(candle) {
out.push(TickBarValue {
open: b.open,
high: b.high,
low: b.low,
close: b.close,
volume: b.volume,
});
}
}
Ok(out)
}
#[napi]
pub fn ticks(&self) -> u32 {
self.inner.ticks() as u32
}
#[napi]
pub fn reset(&mut self) {
self.inner.reset();
}
}
#[napi(object)]
pub struct VolumeBarValue {
pub open: f64,
pub high: f64,
pub low: f64,
pub close: f64,
pub volume: f64,
}
#[napi(js_name = "VolumeBars")]
pub struct VolumeBarsNode {
inner: wc::VolumeBars,
}
#[napi]
impl VolumeBarsNode {
#[napi(constructor)]
pub fn new(volume_per_bar: f64) -> napi::Result<Self> {
Ok(Self {
inner: wc::VolumeBars::new(volume_per_bar).map_err(map_err)?,
})
}
#[napi]
pub fn update(
&mut self,
open: f64,
high: f64,
low: f64,
close: f64,
volume: f64,
) -> napi::Result<Vec<VolumeBarValue>> {
let candle = wc::Candle::new(open, high, low, close, volume, 0).map_err(map_err)?;
Ok(self
.inner
.update(candle)
.into_iter()
.map(|b| VolumeBarValue {
open: b.open,
high: b.high,
low: b.low,
close: b.close,
volume: b.volume,
})
.collect())
}
#[napi]
pub fn batch(
&mut self,
open: Vec<f64>,
high: Vec<f64>,
low: Vec<f64>,
close: Vec<f64>,
volume: Vec<f64>,
) -> napi::Result<Vec<VolumeBarValue>> {
if open.len() != high.len()
|| high.len() != low.len()
|| low.len() != close.len()
|| close.len() != volume.len()
{
return Err(NapiError::from_reason(
"open, high, low, close, volume must be equal length".to_string(),
));
}
let mut out = Vec::new();
for i in 0..open.len() {
let candle = wc::Candle::new(open[i], high[i], low[i], close[i], volume[i], 0)
.map_err(map_err)?;
for b in self.inner.update(candle) {
out.push(VolumeBarValue {
open: b.open,
high: b.high,
low: b.low,
close: b.close,
volume: b.volume,
});
}
}
Ok(out)
}
#[napi(js_name = "volumePerBar")]
pub fn volume_per_bar(&self) -> f64 {
self.inner.volume_per_bar()
}
#[napi]
pub fn reset(&mut self) {
self.inner.reset();
}
}
#[napi(object)]
pub struct DollarBarValue {
pub open: f64,
pub high: f64,
pub low: f64,
pub close: f64,
pub volume: f64,
pub dollar: f64,
}
#[napi(js_name = "DollarBars")]
pub struct DollarBarsNode {
inner: wc::DollarBars,
}
#[napi]
impl DollarBarsNode {
#[napi(constructor)]
pub fn new(dollar_per_bar: f64) -> napi::Result<Self> {
Ok(Self {
inner: wc::DollarBars::new(dollar_per_bar).map_err(map_err)?,
})
}
#[napi]
pub fn update(
&mut self,
open: f64,
high: f64,
low: f64,
close: f64,
volume: f64,
) -> napi::Result<Vec<DollarBarValue>> {
let candle = wc::Candle::new(open, high, low, close, volume, 0).map_err(map_err)?;
Ok(self
.inner
.update(candle)
.into_iter()
.map(|b| DollarBarValue {
open: b.open,
high: b.high,
low: b.low,
close: b.close,
volume: b.volume,
dollar: b.dollar,
})
.collect())
}
#[napi]
pub fn batch(
&mut self,
open: Vec<f64>,
high: Vec<f64>,
low: Vec<f64>,
close: Vec<f64>,
volume: Vec<f64>,
) -> napi::Result<Vec<DollarBarValue>> {
if open.len() != high.len()
|| high.len() != low.len()
|| low.len() != close.len()
|| close.len() != volume.len()
{
return Err(NapiError::from_reason(
"open, high, low, close, volume must be equal length".to_string(),
));
}
let mut out = Vec::new();
for i in 0..open.len() {
let candle = wc::Candle::new(open[i], high[i], low[i], close[i], volume[i], 0)
.map_err(map_err)?;
for b in self.inner.update(candle) {
out.push(DollarBarValue {
open: b.open,
high: b.high,
low: b.low,
close: b.close,
volume: b.volume,
dollar: b.dollar,
});
}
}
Ok(out)
}
#[napi(js_name = "dollarPerBar")]
pub fn dollar_per_bar(&self) -> f64 {
self.inner.dollar_per_bar()
}
#[napi]
pub fn reset(&mut self) {
self.inner.reset();
}
}
#[napi(object)]
pub struct ImbalanceBarValue {
pub open: f64,
pub high: f64,
pub low: f64,
pub close: f64,
pub imbalance: f64,
pub direction: i32,
}
#[napi(js_name = "ImbalanceBars")]
pub struct ImbalanceBarsNode {
inner: wc::ImbalanceBars,
}
#[napi]
impl ImbalanceBarsNode {
#[napi(constructor)]
pub fn new(threshold: f64) -> napi::Result<Self> {
Ok(Self {
inner: wc::ImbalanceBars::new(threshold).map_err(map_err)?,
})
}
#[napi]
pub fn update(
&mut self,
open: f64,
high: f64,
low: f64,
close: f64,
) -> napi::Result<Vec<ImbalanceBarValue>> {
let candle = wc::Candle::new(open, high, low, close, 1.0, 0).map_err(map_err)?;
Ok(self
.inner
.update(candle)
.into_iter()
.map(|b| ImbalanceBarValue {
open: b.open,
high: b.high,
low: b.low,
close: b.close,
imbalance: b.imbalance,
direction: i32::from(b.direction),
})
.collect())
}
#[napi]
pub fn batch(
&mut self,
open: Vec<f64>,
high: Vec<f64>,
low: Vec<f64>,
close: Vec<f64>,
) -> napi::Result<Vec<ImbalanceBarValue>> {
if open.len() != high.len() || high.len() != low.len() || low.len() != close.len() {
return Err(NapiError::from_reason(
"open, high, low, close must be equal length".to_string(),
));
}
let mut out = Vec::new();
for i in 0..open.len() {
let candle =
wc::Candle::new(open[i], high[i], low[i], close[i], 1.0, 0).map_err(map_err)?;
for b in self.inner.update(candle) {
out.push(ImbalanceBarValue {
open: b.open,
high: b.high,
low: b.low,
close: b.close,
imbalance: b.imbalance,
direction: i32::from(b.direction),
});
}
}
Ok(out)
}
#[napi]
pub fn threshold(&self) -> f64 {
self.inner.threshold()
}
#[napi]
pub fn reset(&mut self) {
self.inner.reset();
}
}
#[napi(object)]
pub struct RunBarValue {
pub open: f64,
pub high: f64,
pub low: f64,
pub close: f64,
pub length: u32,
pub direction: i32,
}
#[napi(js_name = "RunBars")]
pub struct RunBarsNode {
inner: wc::RunBars,
}
#[napi]
impl RunBarsNode {
#[napi(constructor)]
pub fn new(run_length: u32) -> napi::Result<Self> {
Ok(Self {
inner: wc::RunBars::new(run_length as usize).map_err(map_err)?,
})
}
#[napi]
pub fn update(
&mut self,
open: f64,
high: f64,
low: f64,
close: f64,
) -> napi::Result<Vec<RunBarValue>> {
let candle = wc::Candle::new(open, high, low, close, 1.0, 0).map_err(map_err)?;
Ok(self
.inner
.update(candle)
.into_iter()
.map(|b| RunBarValue {
open: b.open,
high: b.high,
low: b.low,
close: b.close,
length: b.length as u32,
direction: i32::from(b.direction),
})
.collect())
}
#[napi]
pub fn batch(
&mut self,
open: Vec<f64>,
high: Vec<f64>,
low: Vec<f64>,
close: Vec<f64>,
) -> napi::Result<Vec<RunBarValue>> {
if open.len() != high.len() || high.len() != low.len() || low.len() != close.len() {
return Err(NapiError::from_reason(
"open, high, low, close must be equal length".to_string(),
));
}
let mut out = Vec::new();
for i in 0..open.len() {
let candle =
wc::Candle::new(open[i], high[i], low[i], close[i], 1.0, 0).map_err(map_err)?;
for b in self.inner.update(candle) {
out.push(RunBarValue {
open: b.open,
high: b.high,
low: b.low,
close: b.close,
length: b.length as u32,
direction: i32::from(b.direction),
});
}
}
Ok(out)
}
#[napi(js_name = "runLength")]
pub fn run_length(&self) -> u32 {
self.inner.run_length() as u32
}
#[napi]
pub fn reset(&mut self) {
self.inner.reset();
}
}
#[napi(object)]
pub struct LineBreakBarValue {
pub open: f64,
pub close: f64,
pub direction: i32,
}
#[napi(js_name = "ThreeLineBreakBars")]
pub struct ThreeLineBreakBarsNode {
inner: wc::ThreeLineBreakBars,
}
#[napi]
impl ThreeLineBreakBarsNode {
#[napi(constructor)]
pub fn new(lines: u32) -> napi::Result<Self> {
Ok(Self {
inner: wc::ThreeLineBreakBars::new(lines as usize).map_err(map_err)?,
})
}
#[napi]
pub fn update(&mut self, close: f64) -> napi::Result<Vec<LineBreakBarValue>> {
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| LineBreakBarValue {
open: b.open,
close: b.close,
direction: i32::from(b.direction),
})
.collect())
}
#[napi]
pub fn batch(&mut self, close: Vec<f64>) -> napi::Result<Vec<LineBreakBarValue>> {
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(LineBreakBarValue {
open: b.open,
close: b.close,
direction: i32::from(b.direction),
});
}
}
Ok(out)
}
#[napi]
pub fn lines(&self) -> u32 {
self.inner.lines() as u32
}
#[napi]
pub fn reset(&mut self) {
self.inner.reset();
}
}
#[napi(js_name = "Alpha")]
pub struct AlphaNode {
inner: wc::Alpha,
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "maturin"
[project]
name = "wickra"
version = "0.7.2"
version = "0.7.4"
description = "Streaming-first technical indicators: incremental, fast, install-free."
readme = "README.md"
license = "MIT OR Apache-2.0"
+32
View File
@@ -25,6 +25,15 @@ from __future__ import annotations
from ._wickra import (
__version__,
M2Measure,
UpsidePotentialRatio,
GainToPainRatio,
CommonSenseRatio,
KRatio,
TailRatio,
MartinRatio,
BurkeRatio,
SterlingRatio,
AUTOCORRPGRAM,
EVENBETTERSINE,
BANDPASS,
@@ -361,6 +370,13 @@ from ._wickra import (
InitialBalance,
OpeningRange,
# Alt-Chart Bars
ThreeLineBreakBars,
RunBars,
ImbalanceBars,
DollarBars,
VolumeBars,
TickBars,
RangeBars,
RenkoBars,
KagiBars,
PointAndFigureBars,
@@ -552,6 +568,15 @@ from ._wickra import (
)
__all__ = [
"M2Measure",
"UpsidePotentialRatio",
"GainToPainRatio",
"CommonSenseRatio",
"KRatio",
"TailRatio",
"MartinRatio",
"BurkeRatio",
"SterlingRatio",
"AUTOCORRPGRAM",
"EVENBETTERSINE",
"BANDPASS",
@@ -889,6 +914,13 @@ __all__ = [
"InitialBalance",
"OpeningRange",
# Alt-Chart Bars
"ThreeLineBreakBars",
"RunBars",
"ImbalanceBars",
"DollarBars",
"VolumeBars",
"TickBars",
"RangeBars",
"RenkoBars",
"KagiBars",
"PointAndFigureBars",
File diff suppressed because it is too large Load Diff
@@ -45,6 +45,15 @@ def ohlcv() -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
# --- Scalar (f64 -> f64) indicators ---------------------------------------
SCALAR = [
(ta.M2Measure, (20, 0.0, 0.02)),
(ta.UpsidePotentialRatio, (20, 0.0)),
(ta.GainToPainRatio, (12,)),
(ta.CommonSenseRatio, (20,)),
(ta.KRatio, (30,)),
(ta.TailRatio, (20,)),
(ta.MartinRatio, (14,)),
(ta.BurkeRatio, (12,)),
(ta.SterlingRatio, (12,)),
(ta.AUTOCORRPGRAM, (10, 48)),
(ta.EVENBETTERSINE, (40, 10)),
(ta.BANDPASS, (20, 0.3)),
@@ -4228,3 +4237,83 @@ def test_bar_builders_reset():
r.update(15.0)
r.reset()
assert r.update(50.0) == [] # re-seeds after reset
def test_range_bars_reference():
rb = ta.RangeBars(1.0)
assert rb.update(10.0) == [] # seed
assert rb.update(13.0) == [(10.0, 11.0, 1), (11.0, 12.0, 1), (12.0, 13.0, 1)]
def test_range_bars_batch_shape():
rb = ta.RangeBars(1.0)
out = rb.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_tick_bars_reference():
tb = ta.TickBars(2)
assert tb.update(10.0, 11.0, 9.0, 10.5, 100.0) == []
out = tb.update(10.5, 12.0, 10.0, 11.0, 150.0)
assert len(out) == 1
assert out[0] == (10.0, 12.0, 9.0, 11.0, 250.0)
def test_tick_bars_batch_shape():
tb = ta.TickBars(2)
col = np.array([10.0, 10.0, 10.0, 10.0])
vol = np.array([1.0, 1.0, 1.0, 1.0])
out = tb.batch(col, col, col, col, vol)
assert out.shape == (2, 5)
def test_volume_bars_reference():
vb = ta.VolumeBars(100.0)
assert vb.update(10.0, 10.0, 10.0, 10.0, 60.0) == []
out = vb.update(10.5, 10.5, 10.5, 10.5, 60.0)
assert len(out) == 1
assert out[0][4] == 120.0 # accumulated volume
def test_dollar_bars_reference():
db = ta.DollarBars(1000.0)
assert db.update(10.0, 10.0, 10.0, 10.0, 60.0) == [] # 600
out = db.update(10.0, 10.0, 10.0, 10.0, 60.0) # 1200 >= 1000
assert len(out) == 1
assert out[0][4] == 120.0 # volume
assert out[0][5] == 1200.0 # traded value
def test_imbalance_bars_reference():
ib = ta.ImbalanceBars(3.0)
assert ib.update(10.0, 10.0, 10.0, 10.0) == [] # seed
ib.update(11.0, 11.0, 11.0, 11.0) # +1
ib.update(12.0, 12.0, 12.0, 12.0) # +2
out = ib.update(13.0, 13.0, 13.0, 13.0) # +3 -> close
assert len(out) == 1
assert out[0][4] == 3.0 # imbalance
assert out[0][5] == 1 # direction
def test_run_bars_reference():
rb = ta.RunBars(3)
assert rb.update(10.0, 10.0, 10.0, 10.0) == [] # seed
rb.update(11.0, 11.0, 11.0, 11.0) # run 1
rb.update(12.0, 12.0, 12.0, 12.0) # run 2
out = rb.update(13.0, 13.0, 13.0, 13.0) # run 3 -> close
assert len(out) == 1
assert out[0][4] == 3 # length
assert out[0][5] == 1 # direction
def test_three_line_break_bars_reference():
tlb = ta.ThreeLineBreakBars(3)
assert tlb.update(10.0) == [] # seed
assert tlb.update(11.0) == [(10.0, 11.0, 1)]
def test_three_line_break_bars_batch_shape():
tlb = ta.ThreeLineBreakBars(3)
out = tlb.batch(np.array([10.0, 11.0, 12.0, 13.0]))
assert out.shape[1] == 3
+313
View File
@@ -12755,6 +12755,15 @@ wasm_scalar_indicator!(WasmUniversalOscillator, "UNIVERSALOSC", wc::UniversalOsc
wasm_scalar_indicator!(WasmBandpassFilter, "BANDPASS", wc::BandpassFilter, period: usize, bandwidth: f64);
wasm_scalar_indicator!(WasmEvenBetterSinewave, "EVENBETTERSINE", wc::EvenBetterSinewave, hp_period: usize, ssf_length: usize);
wasm_scalar_indicator!(WasmAutocorrelationPeriodogram, "AUTOCORRPGRAM", wc::AutocorrelationPeriodogram, min_period: usize, max_period: usize);
wasm_scalar_indicator!(WasmSterlingRatio, "SterlingRatio", wc::SterlingRatio, period: usize);
wasm_scalar_indicator!(WasmBurkeRatio, "BurkeRatio", wc::BurkeRatio, period: usize);
wasm_scalar_indicator!(WasmMartinRatio, "MartinRatio", wc::MartinRatio, period: usize);
wasm_scalar_indicator!(WasmTailRatio, "TailRatio", wc::TailRatio, period: usize);
wasm_scalar_indicator!(WasmKRatio, "KRatio", wc::KRatio, period: usize);
wasm_scalar_indicator!(WasmCommonSenseRatio, "CommonSenseRatio", wc::CommonSenseRatio, period: usize);
wasm_scalar_indicator!(WasmGainToPainRatio, "GainToPainRatio", wc::GainToPainRatio, period: usize);
wasm_scalar_indicator!(WasmUpsidePotentialRatio, "UpsidePotentialRatio", wc::UpsidePotentialRatio, period: usize, mar: f64);
wasm_scalar_indicator!(WasmM2Measure, "M2Measure", wc::M2Measure, period: usize, risk_free: f64, benchmark_stddev: f64);
// --- VolatilityCone: Candle in, struct out (current/min/median/max/percentile) ---
@@ -13154,6 +13163,310 @@ impl WasmPointAndFigureBars {
}
}
#[wasm_bindgen(js_name = RangeBars)]
pub struct WasmRangeBars {
inner: wc::RangeBars,
}
#[wasm_bindgen(js_class = RangeBars)]
impl WasmRangeBars {
#[wasm_bindgen(constructor)]
pub fn new(range: f64) -> Result<WasmRangeBars, JsError> {
Ok(Self {
inner: wc::RangeBars::new(range).map_err(map_err)?,
})
}
/// Returns an array of `{ open, close, direction }` bars 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)
}
pub fn range(&self) -> f64 {
self.inner.range()
}
pub fn reset(&mut self) {
self.inner.reset();
}
}
#[wasm_bindgen(js_name = TickBars)]
pub struct WasmTickBars {
inner: wc::TickBars,
}
#[wasm_bindgen(js_class = TickBars)]
impl WasmTickBars {
#[wasm_bindgen(constructor)]
pub fn new(ticks: usize) -> Result<WasmTickBars, JsError> {
Ok(Self {
inner: wc::TickBars::new(ticks).map_err(map_err)?,
})
}
/// Returns an array of `{ open, high, low, close, volume }` bars completed on this candle.
pub fn update(
&mut self,
open: f64,
high: f64,
low: f64,
close: f64,
volume: f64,
) -> Result<Array, JsError> {
let candle = wc::Candle::new(open, high, low, close, volume, 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, &"high".into(), &b.high.into()).ok();
Reflect::set(&obj, &"low".into(), &b.low.into()).ok();
Reflect::set(&obj, &"close".into(), &b.close.into()).ok();
Reflect::set(&obj, &"volume".into(), &b.volume.into()).ok();
arr.push(&obj);
}
Ok(arr)
}
pub fn ticks(&self) -> usize {
self.inner.ticks()
}
pub fn reset(&mut self) {
self.inner.reset();
}
}
#[wasm_bindgen(js_name = VolumeBars)]
pub struct WasmVolumeBars {
inner: wc::VolumeBars,
}
#[wasm_bindgen(js_class = VolumeBars)]
impl WasmVolumeBars {
#[wasm_bindgen(constructor)]
pub fn new(volume_per_bar: f64) -> Result<WasmVolumeBars, JsError> {
Ok(Self {
inner: wc::VolumeBars::new(volume_per_bar).map_err(map_err)?,
})
}
/// Returns an array of `{ open, high, low, close, volume }` bars completed on this candle.
pub fn update(
&mut self,
open: f64,
high: f64,
low: f64,
close: f64,
volume: f64,
) -> Result<Array, JsError> {
let candle = wc::Candle::new(open, high, low, close, volume, 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, &"high".into(), &b.high.into()).ok();
Reflect::set(&obj, &"low".into(), &b.low.into()).ok();
Reflect::set(&obj, &"close".into(), &b.close.into()).ok();
Reflect::set(&obj, &"volume".into(), &b.volume.into()).ok();
arr.push(&obj);
}
Ok(arr)
}
#[wasm_bindgen(js_name = volumePerBar)]
pub fn volume_per_bar(&self) -> f64 {
self.inner.volume_per_bar()
}
pub fn reset(&mut self) {
self.inner.reset();
}
}
#[wasm_bindgen(js_name = DollarBars)]
pub struct WasmDollarBars {
inner: wc::DollarBars,
}
#[wasm_bindgen(js_class = DollarBars)]
impl WasmDollarBars {
#[wasm_bindgen(constructor)]
pub fn new(dollar_per_bar: f64) -> Result<WasmDollarBars, JsError> {
Ok(Self {
inner: wc::DollarBars::new(dollar_per_bar).map_err(map_err)?,
})
}
/// Returns an array of `{ open, high, low, close, volume, dollar }` bars completed on this candle.
pub fn update(
&mut self,
open: f64,
high: f64,
low: f64,
close: f64,
volume: f64,
) -> Result<Array, JsError> {
let candle = wc::Candle::new(open, high, low, close, volume, 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, &"high".into(), &b.high.into()).ok();
Reflect::set(&obj, &"low".into(), &b.low.into()).ok();
Reflect::set(&obj, &"close".into(), &b.close.into()).ok();
Reflect::set(&obj, &"volume".into(), &b.volume.into()).ok();
Reflect::set(&obj, &"dollar".into(), &b.dollar.into()).ok();
arr.push(&obj);
}
Ok(arr)
}
#[wasm_bindgen(js_name = dollarPerBar)]
pub fn dollar_per_bar(&self) -> f64 {
self.inner.dollar_per_bar()
}
pub fn reset(&mut self) {
self.inner.reset();
}
}
#[wasm_bindgen(js_name = ImbalanceBars)]
pub struct WasmImbalanceBars {
inner: wc::ImbalanceBars,
}
#[wasm_bindgen(js_class = ImbalanceBars)]
impl WasmImbalanceBars {
#[wasm_bindgen(constructor)]
pub fn new(threshold: f64) -> Result<WasmImbalanceBars, JsError> {
Ok(Self {
inner: wc::ImbalanceBars::new(threshold).map_err(map_err)?,
})
}
/// Returns an array of `{ open, high, low, close, imbalance, direction }` bars completed on this candle.
pub fn update(&mut self, open: f64, high: f64, low: f64, close: f64) -> Result<Array, JsError> {
let candle = wc::Candle::new(open, high, low, 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, &"high".into(), &b.high.into()).ok();
Reflect::set(&obj, &"low".into(), &b.low.into()).ok();
Reflect::set(&obj, &"close".into(), &b.close.into()).ok();
Reflect::set(&obj, &"imbalance".into(), &b.imbalance.into()).ok();
Reflect::set(&obj, &"direction".into(), &f64::from(b.direction).into()).ok();
arr.push(&obj);
}
Ok(arr)
}
pub fn threshold(&self) -> f64 {
self.inner.threshold()
}
pub fn reset(&mut self) {
self.inner.reset();
}
}
#[wasm_bindgen(js_name = RunBars)]
pub struct WasmRunBars {
inner: wc::RunBars,
}
#[wasm_bindgen(js_class = RunBars)]
impl WasmRunBars {
#[wasm_bindgen(constructor)]
pub fn new(run_length: usize) -> Result<WasmRunBars, JsError> {
Ok(Self {
inner: wc::RunBars::new(run_length).map_err(map_err)?,
})
}
/// Returns an array of `{ open, high, low, close, length, direction }` bars completed on this candle.
pub fn update(&mut self, open: f64, high: f64, low: f64, close: f64) -> Result<Array, JsError> {
let candle = wc::Candle::new(open, high, low, 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, &"high".into(), &b.high.into()).ok();
Reflect::set(&obj, &"low".into(), &b.low.into()).ok();
Reflect::set(&obj, &"close".into(), &b.close.into()).ok();
#[allow(clippy::cast_precision_loss)]
Reflect::set(&obj, &"length".into(), &(b.length as f64).into()).ok();
Reflect::set(&obj, &"direction".into(), &f64::from(b.direction).into()).ok();
arr.push(&obj);
}
Ok(arr)
}
#[wasm_bindgen(js_name = runLength)]
pub fn run_length(&self) -> usize {
self.inner.run_length()
}
pub fn reset(&mut self) {
self.inner.reset();
}
}
#[wasm_bindgen(js_name = ThreeLineBreakBars)]
pub struct WasmThreeLineBreakBars {
inner: wc::ThreeLineBreakBars,
}
#[wasm_bindgen(js_class = ThreeLineBreakBars)]
impl WasmThreeLineBreakBars {
#[wasm_bindgen(constructor)]
pub fn new(lines: usize) -> Result<WasmThreeLineBreakBars, JsError> {
Ok(Self {
inner: wc::ThreeLineBreakBars::new(lines).map_err(map_err)?,
})
}
/// Returns an array of `{ open, close, direction }` bars 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)
}
pub fn lines(&self) -> usize {
self.inner.lines()
}
pub fn reset(&mut self) {
self.inner.reset();
}
}
#[wasm_bindgen(js_name = Alpha)]
pub struct WasmAlpha {
inner: wc::Alpha,
@@ -0,0 +1,218 @@
//! Burke Ratio — mean return over the square root of the summed squared drawdowns.
use std::collections::VecDeque;
use crate::error::{Error, Result};
use crate::traits::Indicator;
/// Burke Ratio over a trailing window of `period` returns.
///
/// ```text
/// equity_t = Π_{i<=t} (1 + return_i) (compounded curve)
/// peak_t = max_{s<=t} equity_s
/// dd_t = (peak_t equity_t) / peak_t (fractional drawdown, >= 0)
/// Burke = mean(returns) / sqrt( Σ dd_t² )
/// ```
///
/// The Burke Ratio divides the average per-period return by the **Euclidean norm of
/// the drawdowns** — the square root of the *sum* of squared drawdowns. Squaring
/// penalises deep drawdowns far more than shallow ones, and summing (rather than
/// averaging) means the denominator grows with both the depth and the *number* of
/// drawdowns. This makes Burke the most outlier-sensitive of Wickra's three
/// drawdown ratios: where the [`SterlingRatio`](crate::SterlingRatio) averages raw
/// drawdowns and shrugs off a single crater, Burke makes that crater dominate.
/// The [`MartinRatio`](crate::MartinRatio) sits between them with a root-*mean*
/// square of percentage drawdowns. A window that never draws down has a zero
/// denominator and the indicator reports `0.0`.
///
/// The first value lands after `period` returns; each `update` rebuilds the equity
/// curve over the window (O(period)), which is O(1) in the length of the overall
/// series.
///
/// # Example
///
/// ```
/// use wickra_core::{Indicator, BurkeRatio};
///
/// let mut indicator = BurkeRatio::new(12).unwrap();
/// let mut last = None;
/// for i in 0..24 {
/// last = indicator.update((f64::from(i) * 0.5).sin() * 0.05);
/// }
/// assert!(last.is_some());
/// ```
#[derive(Debug, Clone)]
pub struct BurkeRatio {
period: usize,
window: VecDeque<f64>,
}
impl BurkeRatio {
/// Construct a Burke Ratio over `period` returns.
///
/// # Errors
///
/// Returns [`Error::InvalidPeriod`] if `period < 2`.
pub fn new(period: usize) -> Result<Self> {
if period < 2 {
return Err(Error::InvalidPeriod {
message: "burke ratio needs period >= 2",
});
}
Ok(Self {
period,
window: VecDeque::with_capacity(period),
})
}
/// Configured window of returns.
pub const fn period(&self) -> usize {
self.period
}
fn compute(&self) -> f64 {
#[allow(clippy::cast_precision_loss)]
let length = self.window.len() as f64;
let mut sum_return = 0.0;
let mut sum_drawdown_sq = 0.0;
let mut equity = 1.0;
let mut peak: f64 = 1.0;
for ret in &self.window {
sum_return += *ret;
equity *= 1.0 + *ret;
peak = peak.max(equity);
let drawdown = (peak - equity) / peak;
sum_drawdown_sq += drawdown * drawdown;
}
let denom = sum_drawdown_sq.sqrt();
if denom > 0.0 {
(sum_return / length) / denom
} else {
0.0
}
}
}
impl Indicator for BurkeRatio {
type Input = f64;
type Output = f64;
fn update(&mut self, ret: f64) -> Option<f64> {
if !ret.is_finite() {
return None;
}
if self.window.len() == self.period {
self.window.pop_front();
}
self.window.push_back(ret);
if self.window.len() < self.period {
return None;
}
Some(self.compute())
}
fn reset(&mut self) {
self.window.clear();
}
fn warmup_period(&self) -> usize {
self.period
}
fn is_ready(&self) -> bool {
self.window.len() == self.period
}
fn name(&self) -> &'static str {
"BurkeRatio"
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::traits::BatchExt;
use approx::assert_relative_eq;
#[test]
fn rejects_period_less_than_two() {
assert!(matches!(
BurkeRatio::new(1),
Err(Error::InvalidPeriod { .. })
));
}
#[test]
fn accessors_and_metadata() {
let br = BurkeRatio::new(12).unwrap();
assert_eq!(br.period(), 12);
assert_eq!(br.warmup_period(), 12);
assert_eq!(br.name(), "BurkeRatio");
assert!(!br.is_ready());
}
#[test]
fn reference_value() {
// returns [0.1, -0.1, 0.1]: dd = [0, 0.1, 0.01].
// Σ dd² = 0.01 + 0.0001 = 0.0101; denom = sqrt(0.0101).
// Burke = (0.1/3) / sqrt(0.0101).
let mut br = BurkeRatio::new(3).unwrap();
let out = br.batch(&[0.1, -0.1, 0.1]);
let expected = (0.1_f64 / 3.0) / (0.0101_f64).sqrt();
assert_relative_eq!(out[2].unwrap(), expected, epsilon = 1e-9);
}
#[test]
fn no_drawdown_is_zero() {
let mut br = BurkeRatio::new(3).unwrap();
let last = br
.batch(&[0.01, 0.02, 0.03])
.into_iter()
.flatten()
.last()
.unwrap();
assert_relative_eq!(last, 0.0, epsilon = 1e-12);
}
#[test]
fn losing_window_is_negative() {
let mut br = BurkeRatio::new(3).unwrap();
let last = br
.batch(&[-0.05, -0.02, -0.03])
.into_iter()
.flatten()
.last()
.unwrap();
assert!(last < 0.0);
}
#[test]
fn ignores_non_finite_input() {
let mut br = BurkeRatio::new(3).unwrap();
assert_eq!(br.update(0.1), None);
assert_eq!(br.update(f64::NAN), None);
assert_eq!(br.update(-0.1), None);
assert!(br.update(0.1).is_some());
}
#[test]
fn reset_clears_state() {
let mut br = BurkeRatio::new(3).unwrap();
br.batch(&[0.1, -0.1, 0.1]);
assert!(br.is_ready());
br.reset();
assert!(!br.is_ready());
assert_eq!(br.update(0.1), None);
}
#[test]
fn batch_equals_streaming() {
let rets: Vec<f64> = (0..60)
.map(|i| (f64::from(i) * 0.25).sin() * 0.05)
.collect();
let batch = BurkeRatio::new(12).unwrap().batch(&rets);
let mut streamer = BurkeRatio::new(12).unwrap();
let streamed: Vec<_> = rets.iter().map(|r| streamer.update(*r)).collect();
assert_eq!(batch, streamed);
}
}
@@ -0,0 +1,248 @@
//! Common Sense Ratio (Schwager / Carver) — profit factor multiplied by the tail ratio.
use std::collections::VecDeque;
use crate::error::{Error, Result};
use crate::traits::Indicator;
/// Common Sense Ratio over a trailing window of `period` returns.
///
/// ```text
/// ProfitFactor = Σ gains / Σ |losses| over the window
/// TailRatio = P95(returns) / |P5(returns)| over the window
/// CSR = ProfitFactor · TailRatio
/// ```
///
/// The Common Sense Ratio fuses two views of a return series into one number. The
/// [profit factor](crate::ProfitFactor) captures the *body* of the distribution —
/// how much you make per unit you lose on the average bar. The
/// [`TailRatio`](crate::TailRatio) captures the *extremes* — whether the largest
/// gains outweigh the largest losses. Multiplying them produces a ratio that is
/// only comfortably above `1.0` when a strategy wins on both fronts: a respectable
/// profit factor can still hide catastrophic left-tail risk, and a fat right tail
/// means little if the body bleeds. Above `1.0` the strategy is sound on a
/// common-sense basis; below `1.0` something — body or tail — is working against it.
///
/// Percentiles use linear interpolation over the sorted window. A window with no
/// losses (zero profit-factor denominator) or no left tail (zero P5) reports `0.0`
/// rather than dividing by zero.
///
/// The first value lands after `period` returns; each `update` re-sorts the window
/// (O(period log period)), which is O(1) in the length of the overall series.
///
/// # Example
///
/// ```
/// use wickra_core::{Indicator, CommonSenseRatio};
///
/// let mut indicator = CommonSenseRatio::new(20).unwrap();
/// let mut last = None;
/// for i in 0..40 {
/// last = indicator.update((f64::from(i) * 0.3).sin() * 0.02);
/// }
/// assert!(last.is_some());
/// ```
#[derive(Debug, Clone)]
pub struct CommonSenseRatio {
period: usize,
window: VecDeque<f64>,
}
impl CommonSenseRatio {
/// Construct a Common Sense Ratio over `period` returns.
///
/// # Errors
///
/// Returns [`Error::InvalidPeriod`] if `period < 2` (percentiles need at least
/// two observations).
pub fn new(period: usize) -> Result<Self> {
if period < 2 {
return Err(Error::InvalidPeriod {
message: "common sense ratio needs period >= 2",
});
}
Ok(Self {
period,
window: VecDeque::with_capacity(period),
})
}
/// Configured window of returns.
pub const fn period(&self) -> usize {
self.period
}
fn compute(&self) -> f64 {
let mut gains = 0.0;
let mut losses = 0.0;
for ret in &self.window {
gains += ret.max(0.0);
losses += (-ret).max(0.0);
}
if losses <= 0.0 {
return 0.0;
}
let mut sorted: Vec<f64> = self.window.iter().copied().collect();
sorted.sort_unstable_by(f64::total_cmp);
let lower_tail = percentile(&sorted, 5.0).abs();
if lower_tail <= 0.0 {
return 0.0;
}
let profit_factor = gains / losses;
let tail_ratio = percentile(&sorted, 95.0) / lower_tail;
profit_factor * tail_ratio
}
}
/// Linear-interpolation percentile of an ascending, non-empty slice.
fn percentile(sorted: &[f64], pct: f64) -> f64 {
let last_index = sorted.len() - 1;
#[allow(clippy::cast_precision_loss)]
let rank = pct / 100.0 * last_index as f64;
let floor = rank.floor();
// `rank` lies in `[0, last_index]`, so its floor is a valid in-bounds index.
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
let lower = floor as usize;
if lower >= last_index {
return sorted[last_index];
}
let frac = rank - floor;
sorted[lower] + frac * (sorted[lower + 1] - sorted[lower])
}
impl Indicator for CommonSenseRatio {
type Input = f64;
type Output = f64;
fn update(&mut self, ret: f64) -> Option<f64> {
if !ret.is_finite() {
return None;
}
if self.window.len() == self.period {
self.window.pop_front();
}
self.window.push_back(ret);
if self.window.len() < self.period {
return None;
}
Some(self.compute())
}
fn reset(&mut self) {
self.window.clear();
}
fn warmup_period(&self) -> usize {
self.period
}
fn is_ready(&self) -> bool {
self.window.len() == self.period
}
fn name(&self) -> &'static str {
"CommonSenseRatio"
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::traits::BatchExt;
use approx::assert_relative_eq;
#[test]
fn rejects_period_less_than_two() {
assert!(matches!(
CommonSenseRatio::new(1),
Err(Error::InvalidPeriod { .. })
));
}
#[test]
fn accessors_and_metadata() {
let csr = CommonSenseRatio::new(20).unwrap();
assert_eq!(csr.period(), 20);
assert_eq!(csr.warmup_period(), 20);
assert_eq!(csr.name(), "CommonSenseRatio");
assert!(!csr.is_ready());
}
#[test]
fn reference_value() {
// window [-0.04, -0.02, 0.0, 0.02, 0.04].
// gains = 0.06, losses = 0.06 -> profit factor 1.0.
// P95 = 0.036, |P5| = 0.036 -> tail ratio 1.0. CSR = 1.0.
let mut csr = CommonSenseRatio::new(5).unwrap();
let out = csr.batch(&[-0.04, -0.02, 0.0, 0.02, 0.04]);
assert_relative_eq!(out[4].unwrap(), 1.0, epsilon = 1e-9);
}
#[test]
fn no_losses_is_zero() {
let mut csr = CommonSenseRatio::new(3).unwrap();
let last = csr
.batch(&[0.01, 0.02, 0.03])
.into_iter()
.flatten()
.last()
.unwrap();
assert_relative_eq!(last, 0.0, epsilon = 1e-12);
}
#[test]
fn flat_window_is_zero() {
// All zeros: no losses denominator -> zero (the gains/losses guard fires).
let mut csr = CommonSenseRatio::new(4).unwrap();
let last = csr.batch(&[0.0; 4]).into_iter().flatten().last().unwrap();
assert_relative_eq!(last, 0.0, epsilon = 1e-12);
}
#[test]
fn ignores_non_finite_input() {
let mut csr = CommonSenseRatio::new(3).unwrap();
assert_eq!(csr.update(0.01), None);
assert_eq!(csr.update(f64::NAN), None);
assert_eq!(csr.update(-0.02), None);
assert!(csr.update(0.03).is_some());
}
#[test]
fn reset_clears_state() {
let mut csr = CommonSenseRatio::new(3).unwrap();
csr.batch(&[-0.01, 0.0, 0.02]);
assert!(csr.is_ready());
csr.reset();
assert!(!csr.is_ready());
assert_eq!(csr.update(0.01), None);
}
#[test]
fn batch_equals_streaming() {
let rets: Vec<f64> = (0..60)
.map(|i| (f64::from(i) * 0.25).sin() * 0.02)
.collect();
let batch = CommonSenseRatio::new(15).unwrap().batch(&rets);
let mut streamer = CommonSenseRatio::new(15).unwrap();
let streamed: Vec<_> = rets.iter().map(|r| streamer.update(*r)).collect();
assert_eq!(batch, streamed);
}
#[test]
fn percentile_at_top_returns_last() {
// The rank floor reaching the final index returns the largest element.
assert_relative_eq!(percentile(&[1.0, 2.0, 3.0], 100.0), 3.0, epsilon = 1e-12);
}
#[test]
fn zero_lower_tail_is_zero() {
// One loss but a 5th percentile of exactly zero: the tail term collapses
// and the indicator reports 0.0 rather than dividing by zero. With period
// 21 the 5% rank lands on sorted index 1, which is 0.0 here.
let mut returns = vec![0.0; 21];
returns[0] = -0.1;
let mut csr = CommonSenseRatio::new(21).unwrap();
let last = csr.batch(&returns).into_iter().flatten().last().unwrap();
assert_relative_eq!(last, 0.0, epsilon = 1e-12);
}
}
@@ -0,0 +1,224 @@
//! Dollar bar builder — close a bar each time accumulated traded value reaches a threshold.
use crate::error::{Error, Result};
use crate::ohlcv::Candle;
use crate::traits::BarBuilder;
/// One completed dollar bar (an OHLC aggregate spanning ~`dollar_per_bar` of traded value).
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct DollarBar {
/// Open of the first candle in the bar.
pub open: f64,
/// Highest high across the bar.
pub high: f64,
/// Lowest low across the bar.
pub low: f64,
/// Close of the candle that closed the bar.
pub close: f64,
/// Summed volume across the bar.
pub volume: f64,
/// Accumulated traded value (`Σ close · volume`, `>= dollar_per_bar`).
pub dollar: f64,
}
/// Dollar bar builder — emits a bar each time accumulated traded value
/// (`price × volume`) reaches `dollar_per_bar`.
///
/// Dollar bars are the most drift-robust of the information-driven bar types. Where
/// [`VolumeBars`](crate::VolumeBars) close on a fixed *quantity* of shares/contracts,
/// dollar bars close on a fixed *value*: each candle contributes `close × volume` to
/// the running total. As a market's price level rises over years, a fixed share
/// count buys ever more value and volume bars drift in meaning; dollar bars stay
/// economically comparable across the whole history, which is why they are the
/// preferred sampling for long backtests and machine-learning features.
///
/// The bar is candle-granular: at most one bar closes per candle, and the candle
/// that crosses the threshold closes the bar with its overshoot included.
/// [`BarBuilder::update`] returns either an empty vector or a single [`DollarBar`].
///
/// # Example
///
/// ```
/// use wickra_core::{BarBuilder, Candle, DollarBars};
///
/// let c = |cl, v| Candle::new(cl, cl, cl, cl, v, 0).unwrap();
/// let mut bars = DollarBars::new(1000.0).unwrap();
/// assert!(bars.update(c(10.0, 60.0)).is_empty()); // 600
/// let out = bars.update(c(10.0, 60.0)); // 1200 >= 1000 -> close
/// assert_eq!(out.len(), 1);
/// assert_eq!(out[0].dollar, 1200.0);
/// ```
#[derive(Debug, Clone)]
pub struct DollarBars {
dollar_per_bar: f64,
count: usize,
open: f64,
high: f64,
low: f64,
close: f64,
volume: f64,
dollar: f64,
}
impl DollarBars {
/// Construct a dollar-bar builder with the given traded-value threshold.
///
/// # Errors
///
/// Returns [`Error::InvalidPeriod`] if `dollar_per_bar` is not finite and positive.
pub fn new(dollar_per_bar: f64) -> Result<Self> {
if !dollar_per_bar.is_finite() || dollar_per_bar <= 0.0 {
return Err(Error::InvalidPeriod {
message: "dollar_per_bar must be finite and positive",
});
}
Ok(Self {
dollar_per_bar,
count: 0,
open: 0.0,
high: 0.0,
low: 0.0,
close: 0.0,
volume: 0.0,
dollar: 0.0,
})
}
/// Configured traded-value threshold per bar.
pub const fn dollar_per_bar(&self) -> f64 {
self.dollar_per_bar
}
/// Traded value accumulated into the in-progress bar.
pub const fn accumulated(&self) -> f64 {
self.dollar
}
}
impl BarBuilder for DollarBars {
type Bar = DollarBar;
fn update(&mut self, candle: Candle) -> Vec<DollarBar> {
if self.count == 0 {
self.open = candle.open;
self.high = candle.high;
self.low = candle.low;
self.volume = 0.0;
} else {
self.high = self.high.max(candle.high);
self.low = self.low.min(candle.low);
}
self.close = candle.close;
self.volume += candle.volume;
self.dollar += candle.close * candle.volume;
self.count += 1;
if self.dollar < self.dollar_per_bar {
return Vec::new();
}
let bar = DollarBar {
open: self.open,
high: self.high,
low: self.low,
close: self.close,
volume: self.volume,
dollar: self.dollar,
};
self.count = 0;
self.dollar = 0.0;
vec![bar]
}
fn reset(&mut self) {
self.count = 0;
self.volume = 0.0;
self.dollar = 0.0;
}
fn name(&self) -> &'static str {
"DollarBars"
}
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_relative_eq;
fn candle(open: f64, high: f64, low: f64, close: f64, volume: f64) -> Candle {
Candle::new(open, high, low, close, volume, 0).unwrap()
}
#[test]
fn rejects_invalid_threshold() {
assert!(matches!(
DollarBars::new(0.0),
Err(Error::InvalidPeriod { .. })
));
assert!(matches!(
DollarBars::new(-1000.0),
Err(Error::InvalidPeriod { .. })
));
assert!(matches!(
DollarBars::new(f64::NAN),
Err(Error::InvalidPeriod { .. })
));
}
#[test]
fn accessors_and_metadata() {
let bars = DollarBars::new(50_000.0).unwrap();
assert_relative_eq!(bars.dollar_per_bar(), 50_000.0, epsilon = 1e-6);
assert_relative_eq!(bars.accumulated(), 0.0, epsilon = 1e-12);
assert_eq!(bars.name(), "DollarBars");
}
#[test]
fn closes_when_value_reached() {
let mut bars = DollarBars::new(1000.0).unwrap();
assert!(bars.update(candle(10.0, 10.0, 10.0, 10.0, 60.0)).is_empty()); // 600
let out = bars.update(candle(10.0, 10.0, 10.0, 10.0, 60.0)); // 1200
assert_eq!(out.len(), 1);
assert_relative_eq!(out[0].dollar, 1200.0, epsilon = 1e-9);
assert_relative_eq!(out[0].volume, 120.0, epsilon = 1e-12);
}
#[test]
fn aggregates_ohlc() {
let mut bars = DollarBars::new(1000.0).unwrap();
bars.update(candle(10.0, 11.0, 9.0, 10.0, 50.0)); // 500
let out = bars.update(candle(10.0, 12.0, 9.5, 11.0, 60.0)); // 500 + 660 = 1160
assert_relative_eq!(out[0].open, 10.0, epsilon = 1e-12);
assert_relative_eq!(out[0].high, 12.0, epsilon = 1e-12);
assert_relative_eq!(out[0].low, 9.0, epsilon = 1e-12);
assert_relative_eq!(out[0].close, 11.0, epsilon = 1e-12);
}
#[test]
fn below_threshold_emits_nothing() {
let mut bars = DollarBars::new(1000.0).unwrap();
bars.update(candle(10.0, 10.0, 10.0, 10.0, 30.0)); // 300
assert_relative_eq!(bars.accumulated(), 300.0, epsilon = 1e-9);
}
#[test]
fn reset_clears_state() {
let mut bars = DollarBars::new(1000.0).unwrap();
bars.update(candle(10.0, 10.0, 10.0, 10.0, 60.0));
bars.reset();
assert_relative_eq!(bars.accumulated(), 0.0, epsilon = 1e-12);
assert!(bars.update(candle(20.0, 20.0, 20.0, 20.0, 10.0)).is_empty());
}
#[test]
fn batch_concatenates_completed_bars() {
let mut bars = DollarBars::new(1000.0).unwrap();
let candles = [
candle(10.0, 10.0, 10.0, 10.0, 60.0),
candle(10.0, 10.0, 10.0, 10.0, 60.0),
candle(10.0, 10.0, 10.0, 10.0, 60.0),
candle(10.0, 10.0, 10.0, 10.0, 60.0),
];
let out = bars.batch(&candles);
assert_eq!(out.len(), 2);
}
}
@@ -0,0 +1,229 @@
//! Gain-to-Pain Ratio (Schwager) — sum of returns over the sum of losses.
use std::collections::VecDeque;
use crate::error::{Error, Result};
use crate::traits::Indicator;
/// Gain-to-Pain Ratio — Jack Schwager's measure of return per unit of downside:
/// the sum of all returns divided by the sum of the absolute *negative* returns.
///
/// ```text
/// GPR = Σ returns / Σ |negative returns| over the window
/// ```
///
/// Where the [`GainLossRatio`](crate::GainLossRatio) compares *average* win to
/// *average* loss and the [`ProfitFactor`](crate::ProfitFactor) compares gross
/// profit to gross loss, the Gain-to-Pain Ratio puts the **net** result over the
/// total pain endured to earn it. Schwager treats a GPR above `1.0` as good and
/// above `2.0` as excellent for a monthly return series: the strategy made more
/// than it lost on the way, and twice as much when GPR is `2`. A flat series, or
/// one with no losses, has no measurable pain and reports `0` (undefined).
///
/// The output is unbounded and may be negative (a net-losing window). The first
/// value lands after `period` returns; each `update` is O(1).
///
/// # Example
///
/// ```
/// use wickra_core::{Indicator, GainToPainRatio};
///
/// let mut indicator = GainToPainRatio::new(12).unwrap();
/// let mut last = None;
/// for i in 0..24 {
/// last = indicator.update((f64::from(i) * 0.5).sin() * 0.02);
/// }
/// assert!(last.is_some());
/// ```
#[derive(Debug, Clone)]
pub struct GainToPainRatio {
period: usize,
window: VecDeque<f64>,
sum_all: f64,
sum_pain: f64,
}
impl GainToPainRatio {
/// Construct a Gain-to-Pain Ratio over `period` returns.
///
/// # Errors
///
/// Returns [`Error::PeriodZero`] if `period == 0`.
pub fn new(period: usize) -> Result<Self> {
if period == 0 {
return Err(Error::PeriodZero);
}
Ok(Self {
period,
window: VecDeque::with_capacity(period),
sum_all: 0.0,
sum_pain: 0.0,
})
}
/// Configured window of returns.
pub const fn period(&self) -> usize {
self.period
}
}
impl Indicator for GainToPainRatio {
type Input = f64;
type Output = f64;
fn update(&mut self, ret: f64) -> Option<f64> {
if !ret.is_finite() {
return if self.window.len() == self.period {
Some(self.compute())
} else {
None
};
}
if self.window.len() == self.period {
let old = self.window.pop_front().expect("non-empty");
self.sum_all -= old;
if old < 0.0 {
self.sum_pain -= -old;
}
}
self.window.push_back(ret);
self.sum_all += ret;
if ret < 0.0 {
self.sum_pain += -ret;
}
if self.window.len() < self.period {
return None;
}
Some(self.compute())
}
fn reset(&mut self) {
self.window.clear();
self.sum_all = 0.0;
self.sum_pain = 0.0;
}
fn warmup_period(&self) -> usize {
self.period
}
fn is_ready(&self) -> bool {
self.window.len() == self.period
}
fn name(&self) -> &'static str {
"GainToPainRatio"
}
}
impl GainToPainRatio {
fn compute(&self) -> f64 {
if self.sum_pain > 0.0 {
self.sum_all / self.sum_pain
} else {
0.0
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::traits::BatchExt;
use approx::assert_relative_eq;
#[test]
fn rejects_zero_period() {
assert!(matches!(GainToPainRatio::new(0), Err(Error::PeriodZero)));
}
#[test]
fn accessors_and_metadata() {
let g = GainToPainRatio::new(12).unwrap();
assert_eq!(g.period(), 12);
assert_eq!(g.warmup_period(), 12);
assert_eq!(g.name(), "GainToPainRatio");
assert!(!g.is_ready());
}
#[test]
fn first_emission_at_warmup_period() {
let mut g = GainToPainRatio::new(4).unwrap();
let out = g.batch(&[0.01, -0.01, 0.02, -0.01, 0.03]);
for v in out.iter().take(3) {
assert!(v.is_none());
}
assert!(out[3].is_some());
}
#[test]
fn reference_value() {
// returns: +0.04, -0.02 -> sum_all = 0.02, pain = 0.02 -> GPR = 1.0.
let mut g = GainToPainRatio::new(2).unwrap();
let out = g.batch(&[0.04, -0.02]);
assert_relative_eq!(out[1].unwrap(), 1.0, epsilon = 1e-9);
}
#[test]
fn net_losing_window_is_negative() {
let mut g = GainToPainRatio::new(3).unwrap();
let last = g
.batch(&[-0.03, 0.01, -0.02])
.into_iter()
.flatten()
.last()
.unwrap();
assert!(last < 0.0);
}
#[test]
fn no_pain_is_zero() {
let mut g = GainToPainRatio::new(3).unwrap();
let last = g
.batch(&[0.01, 0.02, 0.03])
.into_iter()
.flatten()
.last()
.unwrap();
assert_relative_eq!(last, 0.0, epsilon = 1e-12);
}
#[test]
fn ignores_non_finite() {
let mut g = GainToPainRatio::new(2).unwrap();
let ready = g
.batch(&[0.04, -0.02])
.into_iter()
.flatten()
.last()
.unwrap();
assert_eq!(g.update(f64::NAN), Some(ready));
}
#[test]
fn non_finite_before_ready_is_none() {
// A non-finite value arriving before the window fills yields None.
let mut g = GainToPainRatio::new(3).unwrap();
assert_eq!(g.update(0.02), None);
assert_eq!(g.update(f64::NAN), None);
}
#[test]
fn reset_clears_state() {
let mut g = GainToPainRatio::new(2).unwrap();
g.batch(&[0.04, -0.02]);
assert!(g.is_ready());
g.reset();
assert!(!g.is_ready());
assert_eq!(g.update(0.01), None);
}
#[test]
fn batch_equals_streaming() {
let rets: Vec<f64> = (0..60).map(|i| (f64::from(i) * 0.3).sin() * 0.02).collect();
let batch = GainToPainRatio::new(12).unwrap().batch(&rets);
let mut b = GainToPainRatio::new(12).unwrap();
let streamed: Vec<_> = rets.iter().map(|r| b.update(*r)).collect();
assert_eq!(batch, streamed);
}
}
@@ -0,0 +1,263 @@
//! Tick-imbalance bar builder (simplified López de Prado) — sample on cumulative signed order flow.
use crate::error::{Error, Result};
use crate::ohlcv::Candle;
use crate::traits::BarBuilder;
/// One completed imbalance bar.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct ImbalanceBar {
/// Open of the first candle in the bar.
pub open: f64,
/// Highest high across the bar.
pub high: f64,
/// Lowest low across the bar.
pub low: f64,
/// Close of the candle that closed the bar.
pub close: f64,
/// Signed cumulative tick imbalance at the close (`Σ sign`).
pub imbalance: f64,
/// `+1` if buy-side imbalance closed the bar, `-1` if sell-side.
pub direction: i8,
}
/// Tick-imbalance bar builder — a **simplified** form of López de Prado's
/// imbalance bars.
///
/// Each candle is assigned a tick sign by the tick rule: `+1` if its close is above
/// the previous close, `-1` if below, and the previous sign is carried on an
/// unchanged close. The signed imbalance `θ = Σ sign` accumulates until its absolute
/// value reaches a fixed `threshold`, at which point a bar closes. Imbalance bars
/// therefore sample the market when order flow becomes *one-sided* — a burst of
/// persistent buying or selling — rather than on time, count, or volume. This makes
/// them sensitive to informed, directional trading.
///
/// **Simplification.** The full method estimates a *dynamic* threshold
/// `E[T] · |2P 1|` from an EWMA of the expected bar length `E[T]` and the buy-tick
/// probability `P`, and can weight each sign by volume (volume-imbalance bars) or
/// traded value (dollar-imbalance bars). This builder uses a **fixed** threshold on
/// the unweighted tick imbalance. For the adaptive estimator and the volume/dollar
/// variants, see López de Prado (2018), ch. 2.
///
/// At most one bar closes per candle, so [`BarBuilder::update`] returns either an
/// empty vector or a single [`ImbalanceBar`].
///
/// # Example
///
/// ```
/// use wickra_core::{BarBuilder, Candle, ImbalanceBars};
///
/// let flat = |price: f64| Candle::new(price, price, price, price, 1.0, 0).unwrap();
/// let mut bars = ImbalanceBars::new(3.0).unwrap();
/// bars.update(flat(10.0)); // seed, no sign
/// bars.update(flat(11.0)); // +1
/// bars.update(flat(12.0)); // +2
/// let out = bars.update(flat(13.0)); // +3 -> close
/// assert_eq!(out.len(), 1);
/// assert_eq!(out[0].direction, 1);
/// ```
#[derive(Debug, Clone)]
pub struct ImbalanceBars {
threshold: f64,
count: usize,
open: f64,
high: f64,
low: f64,
close: f64,
prev_close: Option<f64>,
last_sign: i8,
theta: f64,
}
impl ImbalanceBars {
/// Construct an imbalance-bar builder with the given absolute imbalance threshold.
///
/// # Errors
///
/// Returns [`Error::InvalidPeriod`] if `threshold` is not finite and positive.
pub fn new(threshold: f64) -> Result<Self> {
if !threshold.is_finite() || threshold <= 0.0 {
return Err(Error::InvalidPeriod {
message: "threshold must be finite and positive",
});
}
Ok(Self {
threshold,
count: 0,
open: 0.0,
high: 0.0,
low: 0.0,
close: 0.0,
prev_close: None,
last_sign: 0,
theta: 0.0,
})
}
/// Configured absolute imbalance threshold.
pub const fn threshold(&self) -> f64 {
self.threshold
}
/// Signed imbalance accumulated into the in-progress bar.
pub const fn imbalance(&self) -> f64 {
self.theta
}
}
impl BarBuilder for ImbalanceBars {
type Bar = ImbalanceBar;
fn update(&mut self, candle: Candle) -> Vec<ImbalanceBar> {
if self.count == 0 {
self.open = candle.open;
self.high = candle.high;
self.low = candle.low;
} else {
self.high = self.high.max(candle.high);
self.low = self.low.min(candle.low);
}
self.close = candle.close;
self.count += 1;
if let Some(prev) = self.prev_close {
let sign = if candle.close > prev {
1
} else if candle.close < prev {
-1
} else {
self.last_sign
};
self.last_sign = sign;
self.theta += f64::from(sign);
}
self.prev_close = Some(candle.close);
if self.theta.abs() < self.threshold {
return Vec::new();
}
let direction = if self.theta > 0.0 { 1 } else { -1 };
let bar = ImbalanceBar {
open: self.open,
high: self.high,
low: self.low,
close: self.close,
imbalance: self.theta,
direction,
};
self.count = 0;
self.theta = 0.0;
vec![bar]
}
fn reset(&mut self) {
self.count = 0;
self.prev_close = None;
self.last_sign = 0;
self.theta = 0.0;
}
fn name(&self) -> &'static str {
"ImbalanceBars"
}
}
#[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_threshold() {
assert!(matches!(
ImbalanceBars::new(0.0),
Err(Error::InvalidPeriod { .. })
));
assert!(matches!(
ImbalanceBars::new(-3.0),
Err(Error::InvalidPeriod { .. })
));
assert!(matches!(
ImbalanceBars::new(f64::NAN),
Err(Error::InvalidPeriod { .. })
));
}
#[test]
fn accessors_and_metadata() {
let bars = ImbalanceBars::new(10.0).unwrap();
assert_relative_eq!(bars.threshold(), 10.0, epsilon = 1e-12);
assert_relative_eq!(bars.imbalance(), 0.0, epsilon = 1e-12);
assert_eq!(bars.name(), "ImbalanceBars");
}
#[test]
fn buy_imbalance_closes_up_bar() {
let mut bars = ImbalanceBars::new(3.0).unwrap();
bars.update(flat(10.0)); // seed
bars.update(flat(11.0)); // +1
bars.update(flat(12.0)); // +2
let out = bars.update(flat(13.0)); // +3
assert_eq!(out.len(), 1);
assert_eq!(out[0].direction, 1);
assert_relative_eq!(out[0].imbalance, 3.0, epsilon = 1e-12);
}
#[test]
fn sell_imbalance_closes_down_bar() {
let mut bars = ImbalanceBars::new(3.0).unwrap();
bars.update(flat(10.0));
bars.update(flat(9.0)); // -1
bars.update(flat(8.0)); // -2
let out = bars.update(flat(7.0)); // -3
assert_eq!(out.len(), 1);
assert_eq!(out[0].direction, -1);
}
#[test]
fn flat_tick_carries_previous_sign() {
let mut bars = ImbalanceBars::new(3.0).unwrap();
bars.update(flat(10.0));
bars.update(flat(11.0)); // +1
bars.update(flat(11.0)); // flat -> carries +1 -> +2
assert_relative_eq!(bars.imbalance(), 2.0, epsilon = 1e-12);
}
#[test]
fn oscillation_does_not_reach_threshold() {
let mut bars = ImbalanceBars::new(3.0).unwrap();
bars.update(flat(10.0));
bars.update(flat(11.0)); // +1
bars.update(flat(10.0)); // -1 -> theta 0
assert!(bars.update(flat(11.0)).is_empty()); // +1
assert_relative_eq!(bars.imbalance(), 1.0, epsilon = 1e-12);
}
#[test]
fn reset_clears_state() {
let mut bars = ImbalanceBars::new(3.0).unwrap();
bars.update(flat(10.0));
bars.update(flat(11.0));
bars.reset();
assert_relative_eq!(bars.imbalance(), 0.0, epsilon = 1e-12);
// After reset the next candle re-seeds (no previous close).
assert!(bars.update(flat(50.0)).is_empty());
}
#[test]
fn batch_concatenates_completed_bars() {
let mut bars = ImbalanceBars::new(2.0).unwrap();
let candles = [
flat(10.0),
flat(11.0), // +1
flat(12.0), // +2 -> close
flat(13.0), // +1
flat(14.0), // +2 -> close
];
let out = bars.batch(&candles);
assert_eq!(out.len(), 2);
assert!(out.iter().all(|b| b.direction == 1));
}
}
@@ -0,0 +1,239 @@
//! K-Ratio (Kestner) — slope of the cumulative-return curve over the standard error of that slope.
use std::collections::VecDeque;
use crate::error::{Error, Result};
use crate::traits::Indicator;
/// K-Ratio over a trailing window of `period` returns.
///
/// Lars Kestner's K-Ratio measures the *consistency* of an equity curve, not just
/// its return. It builds the cumulative-return curve over the window, fits an
/// ordinary-least-squares trend line through it against time, and divides the
/// fitted slope by the standard error of that slope:
///
/// ```text
/// equity_t = Σ_{i<=t} return_i (cumulative curve, t = 1..period)
/// slope, intercept = OLS(equity_t ~ t)
/// SE(slope) = sqrt( (Σ residual² / (period 2)) / Σ(t t̄)² )
/// K-Ratio = slope / SE(slope)
/// ```
///
/// A high K-Ratio means the equity curve climbs *steadily* — a steep slope with
/// little scatter around the trend. A strategy that earns the same total return in
/// a few lucky jumps scores lower because its residual scatter inflates the
/// standard error. This is the original 1996 form; later Kestner revisions scale by
/// the number of periods (`slope / (SE · period)` in 2003, `slope / (SE · √period)`
/// in 2013) — apply that scaling downstream if you need to compare across window
/// lengths.
///
/// A perfectly straight window (e.g. constant returns) has zero residual scatter,
/// so the slope's standard error is zero and the K-Ratio is undefined; the
/// indicator reports `0.0` in that degenerate case. The statistic therefore needs
/// some dispersion in the returns to be meaningful.
///
/// The first value lands after `period` returns; each `update` re-fits the line
/// over the window (O(period)), which is O(1) in the length of the overall series.
///
/// # Example
///
/// ```
/// use wickra_core::{Indicator, KRatio};
///
/// let mut indicator = KRatio::new(30).unwrap();
/// let mut last = None;
/// for i in 0..60 {
/// last = indicator.update(0.001 + (f64::from(i) * 0.3).sin() * 0.01);
/// }
/// assert!(last.is_some());
/// ```
#[derive(Debug, Clone)]
pub struct KRatio {
period: usize,
window: VecDeque<f64>,
}
impl KRatio {
/// Construct a K-Ratio over `period` returns.
///
/// # Errors
///
/// Returns [`Error::InvalidPeriod`] if `period < 3` (the slope's standard error
/// divides by `period 2`).
pub fn new(period: usize) -> Result<Self> {
if period < 3 {
return Err(Error::InvalidPeriod {
message: "k-ratio needs period >= 3",
});
}
Ok(Self {
period,
window: VecDeque::with_capacity(period),
})
}
/// Configured window of returns.
pub const fn period(&self) -> usize {
self.period
}
fn compute(&self) -> f64 {
let count = self.window.len();
#[allow(clippy::cast_precision_loss)]
let length = count as f64;
// Build the cumulative-equity curve and its mean.
let mut equity = 0.0;
let mut curve: Vec<f64> = Vec::with_capacity(count);
let mut sum_equity = 0.0;
for ret in &self.window {
equity += *ret;
curve.push(equity);
sum_equity += equity;
}
// Times are 1..=count, so Σt = count(count+1)/2 in closed form.
let mean_time = f64::midpoint(length, 1.0);
let mean_equity = sum_equity / length;
let mut sxx = 0.0;
let mut sxy = 0.0;
for (index, value) in curve.iter().enumerate() {
#[allow(clippy::cast_precision_loss)]
let time = (index + 1) as f64;
let dt = time - mean_time;
sxx += dt * dt;
sxy += dt * (value - mean_equity);
}
// sxx > 0 for count >= 2 (distinct integer times), guaranteed by period >= 3.
let slope = sxy / sxx;
let intercept = mean_equity - slope * mean_time;
let mut sse = 0.0;
for (index, value) in curve.iter().enumerate() {
#[allow(clippy::cast_precision_loss)]
let time = (index + 1) as f64;
let residual = value - (intercept + slope * time);
sse += residual * residual;
}
if sse <= 0.0 {
return 0.0;
}
let se_slope = (sse / (length - 2.0) / sxx).sqrt();
slope / se_slope
}
}
impl Indicator for KRatio {
type Input = f64;
type Output = f64;
fn update(&mut self, ret: f64) -> Option<f64> {
if !ret.is_finite() {
return None;
}
if self.window.len() == self.period {
self.window.pop_front();
}
self.window.push_back(ret);
if self.window.len() < self.period {
return None;
}
Some(self.compute())
}
fn reset(&mut self) {
self.window.clear();
}
fn warmup_period(&self) -> usize {
self.period
}
fn is_ready(&self) -> bool {
self.window.len() == self.period
}
fn name(&self) -> &'static str {
"KRatio"
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::traits::BatchExt;
use approx::assert_relative_eq;
#[test]
fn rejects_period_less_than_three() {
assert!(matches!(KRatio::new(2), Err(Error::InvalidPeriod { .. })));
assert!(matches!(KRatio::new(0), Err(Error::InvalidPeriod { .. })));
}
#[test]
fn accessors_and_metadata() {
let kr = KRatio::new(30).unwrap();
assert_eq!(kr.period(), 30);
assert_eq!(kr.warmup_period(), 30);
assert_eq!(kr.name(), "KRatio");
assert!(!kr.is_ready());
}
#[test]
fn reference_value() {
// returns [0.01, 0.02, 0.03] -> equity curve [0.01, 0.03, 0.06].
// slope = 0.025, SE(slope) = sqrt((1/60000)/1/2) = 1/sqrt(120000).
// K-Ratio = 0.025 * sqrt(120000) = 5*sqrt(3) ≈ 8.660254.
let mut kr = KRatio::new(3).unwrap();
let out = kr.batch(&[0.01, 0.02, 0.03]);
let expected = 0.025_f64 / (1.0_f64 / 120_000.0).sqrt();
assert_relative_eq!(out[2].unwrap(), expected, epsilon = 1e-6);
}
#[test]
fn constant_returns_are_degenerate_zero() {
// A perfectly linear equity curve has zero residual scatter -> undefined.
let mut kr = KRatio::new(4).unwrap();
let last = kr.batch(&[0.01; 4]).into_iter().flatten().last().unwrap();
assert_relative_eq!(last, 0.0, epsilon = 1e-12);
}
#[test]
fn rising_curve_is_positive() {
let mut kr = KRatio::new(5).unwrap();
let last = kr
.batch(&[0.01, 0.012, 0.009, 0.011, 0.013])
.into_iter()
.flatten()
.last()
.unwrap();
assert!(last > 0.0);
}
#[test]
fn ignores_non_finite_input() {
let mut kr = KRatio::new(3).unwrap();
assert_eq!(kr.update(0.01), None);
assert_eq!(kr.update(f64::NAN), None);
assert_eq!(kr.update(0.02), None);
assert!(kr.update(0.03).is_some());
}
#[test]
fn reset_clears_state() {
let mut kr = KRatio::new(3).unwrap();
kr.batch(&[0.01, 0.02, 0.03]);
assert!(kr.is_ready());
kr.reset();
assert!(!kr.is_ready());
assert_eq!(kr.update(0.01), None);
}
#[test]
fn batch_equals_streaming() {
let rets: Vec<f64> = (0..60)
.map(|i| 0.001 + (f64::from(i) * 0.25).sin() * 0.01)
.collect();
let batch = KRatio::new(20).unwrap().batch(&rets);
let mut streamer = KRatio::new(20).unwrap();
let streamed: Vec<_> = rets.iter().map(|r| streamer.update(*r)).collect();
assert_eq!(batch, streamed);
}
}
@@ -0,0 +1,232 @@
//! M² / ModiglianiModigliani measure — Sharpe expressed in benchmark return units.
use std::collections::VecDeque;
use crate::error::{Error, Result};
use crate::traits::Indicator;
/// M² (ModiglianiModigliani) measure over a trailing window of `period` returns.
///
/// ```text
/// Sharpe = (mean(returns) risk_free) / stddev(returns)
/// M² = risk_free + Sharpe · benchmark_stddev
/// ```
///
/// The [`SharpeRatio`](crate::SharpeRatio) is dimensionless, which makes it hard to
/// communicate: "0.8" means little to a client. M² rescales the Sharpe ratio back
/// into *return units* by levering (or de-levering) the portfolio to the
/// benchmark's volatility. The result answers a concrete question: "if this
/// strategy had run at the market's risk level, what return would it have
/// produced?" Two portfolios can then be ranked on the same risk-adjusted scale,
/// and M² preserves the Sharpe ordering while being quoted as a percentage.
///
/// `stddev` is the sample standard deviation (Bessel's `n 1`).
/// `risk_free` is the per-period risk-free rate and `benchmark_stddev` the
/// per-period volatility of the benchmark, both supplied by the caller at the
/// return frequency. A flat window has zero volatility and the Sharpe ratio is
/// undefined; the indicator returns `0.0` in that case rather than producing `NaN`.
///
/// Each `update` is O(1) — running sums maintain `Σr` and `Σr²` as the window slides.
///
/// # Example
///
/// ```
/// use wickra_core::{Indicator, M2Measure};
///
/// let mut indicator = M2Measure::new(20, 0.0, 0.02).unwrap();
/// let mut last = None;
/// for i in 0..40 {
/// last = indicator.update(0.001 + (f64::from(i) * 0.1).sin() * 0.01);
/// }
/// assert!(last.is_some());
/// ```
#[derive(Debug, Clone)]
pub struct M2Measure {
period: usize,
risk_free: f64,
benchmark_stddev: f64,
window: VecDeque<f64>,
sum: f64,
sum_sq: f64,
}
impl M2Measure {
/// Construct an M² measure over `period` returns with the given per-period
/// risk-free rate and benchmark standard deviation.
///
/// # Errors
///
/// Returns [`Error::InvalidPeriod`] if `period < 2`, or
/// [`Error::InvalidParameter`] if `risk_free` is not finite or
/// `benchmark_stddev` is negative or not finite.
pub fn new(period: usize, risk_free: f64, benchmark_stddev: f64) -> Result<Self> {
if period < 2 {
return Err(Error::InvalidPeriod {
message: "m2 measure needs period >= 2",
});
}
if !risk_free.is_finite() || !benchmark_stddev.is_finite() || benchmark_stddev < 0.0 {
return Err(Error::InvalidParameter {
message: "risk_free must be finite and benchmark_stddev finite and non-negative",
});
}
Ok(Self {
period,
risk_free,
benchmark_stddev,
window: VecDeque::with_capacity(period),
sum: 0.0,
sum_sq: 0.0,
})
}
/// Configured window of returns.
pub const fn period(&self) -> usize {
self.period
}
/// Configured per-period risk-free rate.
pub const fn risk_free(&self) -> f64 {
self.risk_free
}
/// Configured per-period benchmark standard deviation.
pub const fn benchmark_stddev(&self) -> f64 {
self.benchmark_stddev
}
}
impl Indicator for M2Measure {
type Input = f64;
type Output = f64;
fn update(&mut self, ret: f64) -> Option<f64> {
if !ret.is_finite() {
return None;
}
if self.window.len() == self.period {
let old = self.window.pop_front().expect("non-empty");
self.sum -= old;
self.sum_sq -= old * old;
}
self.window.push_back(ret);
self.sum += ret;
self.sum_sq += ret * ret;
if self.window.len() < self.period {
return None;
}
let n = self.period as f64;
let mean = self.sum / n;
let var = (self.sum_sq - n * mean * mean).max(0.0) / (n - 1.0);
let sd = var.sqrt();
if sd == 0.0 {
return Some(0.0);
}
let sharpe = (mean - self.risk_free) / sd;
Some(self.risk_free + sharpe * self.benchmark_stddev)
}
fn reset(&mut self) {
self.window.clear();
self.sum = 0.0;
self.sum_sq = 0.0;
}
fn warmup_period(&self) -> usize {
self.period
}
fn is_ready(&self) -> bool {
self.window.len() == self.period
}
fn name(&self) -> &'static str {
"M2Measure"
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::traits::BatchExt;
use approx::assert_relative_eq;
#[test]
fn rejects_period_less_than_two() {
assert!(matches!(
M2Measure::new(1, 0.0, 0.02),
Err(Error::InvalidPeriod { .. })
));
}
#[test]
fn rejects_invalid_benchmark_stddev() {
assert!(matches!(
M2Measure::new(10, 0.0, -0.01),
Err(Error::InvalidParameter { .. })
));
assert!(matches!(
M2Measure::new(10, f64::NAN, 0.02),
Err(Error::InvalidParameter { .. })
));
}
#[test]
fn accessors_and_metadata() {
let m2 = M2Measure::new(20, 0.001, 0.02).unwrap();
assert_eq!(m2.period(), 20);
assert_relative_eq!(m2.risk_free(), 0.001, epsilon = 1e-12);
assert_relative_eq!(m2.benchmark_stddev(), 0.02, epsilon = 1e-12);
assert_eq!(m2.warmup_period(), 20);
assert_eq!(m2.name(), "M2Measure");
}
#[test]
fn reference_value() {
// returns [0.01, 0.02, 0.03, 0.04], rf = 0, benchmark_stddev = 0.02.
// mean = 0.025, sd = sqrt(0.000166666...), Sharpe = 0.025 / sd.
// M2 = 0 + Sharpe * 0.02.
let mut m2 = M2Measure::new(4, 0.0, 0.02).unwrap();
let out = m2.batch(&[0.01, 0.02, 0.03, 0.04]);
let sharpe = 0.025_f64 / (0.000_166_666_666_666_666_67_f64).sqrt();
assert_relative_eq!(out[3].unwrap(), sharpe * 0.02, epsilon = 1e-9);
}
#[test]
fn constant_returns_yield_zero() {
let mut m2 = M2Measure::new(5, 0.0, 0.02).unwrap();
for v in m2.batch(&[0.01; 10]).into_iter().flatten() {
assert_relative_eq!(v, 0.0, epsilon = 1e-12);
}
}
#[test]
fn ignores_non_finite_input() {
let mut m2 = M2Measure::new(3, 0.0, 0.02).unwrap();
assert_eq!(m2.update(0.01), None);
assert_eq!(m2.update(f64::NAN), None);
assert_eq!(m2.update(0.02), None);
assert!(m2.update(0.03).is_some());
}
#[test]
fn reset_clears_state() {
let mut m2 = M2Measure::new(3, 0.0, 0.02).unwrap();
m2.batch(&[0.01, 0.02, 0.03]);
assert!(m2.is_ready());
m2.reset();
assert!(!m2.is_ready());
assert_eq!(m2.update(0.01), None);
}
#[test]
fn batch_equals_streaming() {
let rets: Vec<f64> = (0..50)
.map(|i| 0.001 + (f64::from(i) * 0.2).sin() * 0.01)
.collect();
let batch = M2Measure::new(10, 0.0, 0.02).unwrap().batch(&rets);
let mut streamer = M2Measure::new(10, 0.0, 0.02).unwrap();
let streamed: Vec<_> = rets.iter().map(|r| streamer.update(*r)).collect();
assert_eq!(batch, streamed);
}
}
@@ -0,0 +1,220 @@
//! Martin Ratio (Ulcer Performance Index) — mean return over the Ulcer Index.
use std::collections::VecDeque;
use crate::error::{Error, Result};
use crate::traits::Indicator;
/// Martin Ratio — also called the Ulcer Performance Index (UPI) — over a trailing
/// window of `period` returns.
///
/// ```text
/// equity_t = Π_{i<=t} (1 + return_i) (compounded curve)
/// peak_t = max_{s<=t} equity_s
/// dd_t% = 100 · (peak_t equity_t) / peak_t (percentage drawdown)
/// UlcerIdx = sqrt( mean( dd_t%² ) )
/// Martin = mean(returns) / UlcerIdx
/// ```
///
/// The Martin Ratio divides the average per-period return by the **Ulcer Index** —
/// the root-mean-square of the *percentage* drawdowns. The Ulcer Index, by
/// construction, measures the depth *and* duration of the time spent under water:
/// a long shallow slump and a short deep one can score the same. Compared to
/// Wickra's other drawdown ratios, Martin uses the RMS (not the average as in the
/// [`SterlingRatio`](crate::SterlingRatio), nor the un-normalised sum-norm as in the
/// [`BurkeRatio`](crate::BurkeRatio)) and expresses drawdowns in **percent**, so its
/// denominator is on a `0..100` scale and its output is numerically smaller than
/// the fractional-drawdown ratios. A window that never draws down has an Ulcer Index
/// of zero and the indicator reports `0.0`.
///
/// The first value lands after `period` returns; each `update` rebuilds the equity
/// curve over the window (O(period)), which is O(1) in the length of the overall
/// series.
///
/// # Example
///
/// ```
/// use wickra_core::{Indicator, MartinRatio};
///
/// let mut indicator = MartinRatio::new(14).unwrap();
/// let mut last = None;
/// for i in 0..28 {
/// last = indicator.update((f64::from(i) * 0.5).sin() * 0.05);
/// }
/// assert!(last.is_some());
/// ```
#[derive(Debug, Clone)]
pub struct MartinRatio {
period: usize,
window: VecDeque<f64>,
}
impl MartinRatio {
/// Construct a Martin Ratio over `period` returns.
///
/// # Errors
///
/// Returns [`Error::InvalidPeriod`] if `period < 2`.
pub fn new(period: usize) -> Result<Self> {
if period < 2 {
return Err(Error::InvalidPeriod {
message: "martin ratio needs period >= 2",
});
}
Ok(Self {
period,
window: VecDeque::with_capacity(period),
})
}
/// Configured window of returns.
pub const fn period(&self) -> usize {
self.period
}
fn compute(&self) -> f64 {
#[allow(clippy::cast_precision_loss)]
let length = self.window.len() as f64;
let mut sum_return = 0.0;
let mut sum_drawdown_pct_sq = 0.0;
let mut equity = 1.0;
let mut peak: f64 = 1.0;
for ret in &self.window {
sum_return += *ret;
equity *= 1.0 + *ret;
peak = peak.max(equity);
let drawdown_pct = 100.0 * (peak - equity) / peak;
sum_drawdown_pct_sq += drawdown_pct * drawdown_pct;
}
let ulcer_index = (sum_drawdown_pct_sq / length).sqrt();
if ulcer_index > 0.0 {
(sum_return / length) / ulcer_index
} else {
0.0
}
}
}
impl Indicator for MartinRatio {
type Input = f64;
type Output = f64;
fn update(&mut self, ret: f64) -> Option<f64> {
if !ret.is_finite() {
return None;
}
if self.window.len() == self.period {
self.window.pop_front();
}
self.window.push_back(ret);
if self.window.len() < self.period {
return None;
}
Some(self.compute())
}
fn reset(&mut self) {
self.window.clear();
}
fn warmup_period(&self) -> usize {
self.period
}
fn is_ready(&self) -> bool {
self.window.len() == self.period
}
fn name(&self) -> &'static str {
"MartinRatio"
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::traits::BatchExt;
use approx::assert_relative_eq;
#[test]
fn rejects_period_less_than_two() {
assert!(matches!(
MartinRatio::new(1),
Err(Error::InvalidPeriod { .. })
));
}
#[test]
fn accessors_and_metadata() {
let mr = MartinRatio::new(14).unwrap();
assert_eq!(mr.period(), 14);
assert_eq!(mr.warmup_period(), 14);
assert_eq!(mr.name(), "MartinRatio");
assert!(!mr.is_ready());
}
#[test]
fn reference_value() {
// returns [0.1, -0.1, 0.1]: drawdowns% = [0, 10, 1].
// Ulcer Index = sqrt((0 + 100 + 1)/3) = sqrt(101/3).
// Martin = (0.1/3) / sqrt(101/3).
let mut mr = MartinRatio::new(3).unwrap();
let out = mr.batch(&[0.1, -0.1, 0.1]);
let expected = (0.1_f64 / 3.0) / (101.0_f64 / 3.0).sqrt();
assert_relative_eq!(out[2].unwrap(), expected, epsilon = 1e-9);
}
#[test]
fn no_drawdown_is_zero() {
let mut mr = MartinRatio::new(3).unwrap();
let last = mr
.batch(&[0.01, 0.02, 0.03])
.into_iter()
.flatten()
.last()
.unwrap();
assert_relative_eq!(last, 0.0, epsilon = 1e-12);
}
#[test]
fn losing_window_is_negative() {
let mut mr = MartinRatio::new(3).unwrap();
let last = mr
.batch(&[-0.05, -0.02, -0.03])
.into_iter()
.flatten()
.last()
.unwrap();
assert!(last < 0.0);
}
#[test]
fn ignores_non_finite_input() {
let mut mr = MartinRatio::new(3).unwrap();
assert_eq!(mr.update(0.1), None);
assert_eq!(mr.update(f64::NAN), None);
assert_eq!(mr.update(-0.1), None);
assert!(mr.update(0.1).is_some());
}
#[test]
fn reset_clears_state() {
let mut mr = MartinRatio::new(3).unwrap();
mr.batch(&[0.1, -0.1, 0.1]);
assert!(mr.is_ready());
mr.reset();
assert!(!mr.is_ready());
assert_eq!(mr.update(0.1), None);
}
#[test]
fn batch_equals_streaming() {
let rets: Vec<f64> = (0..60)
.map(|i| (f64::from(i) * 0.25).sin() * 0.05)
.collect();
let batch = MartinRatio::new(14).unwrap().batch(&rets);
let mut streamer = MartinRatio::new(14).unwrap();
let streamed: Vec<_> = rets.iter().map(|r| streamer.update(*r)).collect();
assert_eq!(batch, streamed);
}
}
+54 -2
View File
@@ -63,6 +63,7 @@ mod bomar_bands;
mod breadth_thrust;
mod breakaway;
mod bullish_percent_index;
mod burke_ratio;
mod butterfly;
mod calendar_spread;
mod calmar_ratio;
@@ -84,6 +85,7 @@ mod cmf;
mod cmo;
mod coefficient_of_variation;
mod cointegration;
mod common_sense_ratio;
mod composite_profile;
mod concealing_baby_swallow;
mod conditional_value_at_risk;
@@ -110,6 +112,7 @@ mod disparity_index;
mod distance_ssd;
mod doji;
mod doji_star;
mod dollar_bars;
mod donchian;
mod donchian_stop;
mod double_bollinger;
@@ -163,6 +166,7 @@ mod funding_rate;
mod funding_rate_mean;
mod funding_rate_zscore;
mod gain_loss_ratio;
mod gain_to_pain_ratio;
mod gap_side_by_side_white;
mod garch11;
mod garman_klass;
@@ -201,6 +205,7 @@ mod hurst_channel;
mod hurst_exponent;
mod ichimoku;
mod identical_three_crows;
mod imbalance_bars;
mod in_neck;
mod inertia;
mod information_ratio;
@@ -214,6 +219,7 @@ mod inverted_hammer;
mod jarque_bera;
mod jma;
mod jump_indicator;
mod k_ratio;
mod kagi_bars;
mod kalman_hedge_ratio;
mod kama;
@@ -241,6 +247,7 @@ mod log_return;
mod long_legged_doji;
mod long_line;
mod long_short_ratio;
mod m2_measure;
mod ma_envelope;
mod macd;
mod macd_ext;
@@ -248,6 +255,7 @@ mod macd_fix;
mod macd_histogram;
mod mama;
mod market_facilitation_index;
mod martin_ratio;
mod marubozu;
mod mass_index;
mod mat_hold;
@@ -325,6 +333,7 @@ mod qstick;
mod quartile_bands;
mod quoted_spread;
mod r_squared;
mod range_bars;
mod realized_spread;
mod realized_volatility;
mod recovery_factor;
@@ -352,6 +361,7 @@ mod rolling_quantile;
mod roofing_filter;
mod rsi;
mod rsx;
mod run_bars;
mod rvi;
mod rvi_volatility;
mod rwi;
@@ -389,6 +399,7 @@ mod starc_bands;
mod stc;
mod std_dev;
mod step_trailing_stop;
mod sterling_ratio;
mod stick_sandwich;
mod stoch_rsi;
mod stochastic;
@@ -396,6 +407,7 @@ mod stochastic_cci;
mod super_smoother;
mod super_trend;
mod t3;
mod tail_ratio;
mod taker_buy_sell_ratio;
mod takuri;
mod tasuki_gap;
@@ -423,11 +435,13 @@ mod term_structure_basis;
mod three_drives;
mod three_inside;
mod three_line_break;
mod three_line_break_bars;
mod three_line_strike;
mod three_outside;
mod three_soldiers_or_crows;
mod three_stars_in_south;
mod thrusting;
mod tick_bars;
mod tick_index;
mod tii;
mod time_based_stop;
@@ -466,6 +480,7 @@ mod universal_oscillator;
mod up_down_volume_ratio;
mod upside_gap_three_methods;
mod upside_gap_two_crows;
mod upside_potential_ratio;
mod value_area;
mod value_at_risk;
mod variance;
@@ -476,6 +491,7 @@ mod volatility_cone;
mod volatility_of_volatility;
mod volatility_ratio;
mod volty_stop;
mod volume_bars;
mod volume_by_time_profile;
mod volume_oscillator;
mod volume_profile;
@@ -561,6 +577,7 @@ pub use bomar_bands::{BomarBands, BomarBandsOutput};
pub use breadth_thrust::BreadthThrust;
pub use breakaway::Breakaway;
pub use bullish_percent_index::BullishPercentIndex;
pub use burke_ratio::BurkeRatio;
pub use butterfly::Butterfly;
pub use calendar_spread::CalendarSpread;
pub use calmar_ratio::CalmarRatio;
@@ -582,6 +599,7 @@ pub use cmf::ChaikinMoneyFlow;
pub use cmo::Cmo;
pub use coefficient_of_variation::CoefficientOfVariation;
pub use cointegration::{Cointegration, CointegrationOutput};
pub use common_sense_ratio::CommonSenseRatio;
pub use composite_profile::{CompositeProfile, CompositeProfileOutput};
pub use concealing_baby_swallow::ConcealingBabySwallow;
pub use conditional_value_at_risk::ConditionalValueAtRisk;
@@ -608,6 +626,7 @@ pub use disparity_index::DisparityIndex;
pub use distance_ssd::DistanceSsd;
pub use doji::Doji;
pub use doji_star::DojiStar;
pub use dollar_bars::{DollarBar, DollarBars};
pub use donchian::{Donchian, DonchianOutput};
pub use donchian_stop::{DonchianStop, DonchianStopOutput};
pub use double_bollinger::{DoubleBollinger, DoubleBollingerOutput};
@@ -661,6 +680,7 @@ pub use funding_rate::FundingRate;
pub use funding_rate_mean::FundingRateMean;
pub use funding_rate_zscore::FundingRateZScore;
pub use gain_loss_ratio::GainLossRatio;
pub use gain_to_pain_ratio::GainToPainRatio;
pub use gap_side_by_side_white::GapSideBySideWhite;
pub use garch11::Garch11;
pub use garman_klass::GarmanKlassVolatility;
@@ -699,6 +719,7 @@ pub use hurst_channel::{HurstChannel, HurstChannelOutput};
pub use hurst_exponent::HurstExponent;
pub use ichimoku::{Ichimoku, IchimokuOutput};
pub use identical_three_crows::IdenticalThreeCrows;
pub use imbalance_bars::{ImbalanceBar, ImbalanceBars};
pub use in_neck::InNeck;
pub use inertia::Inertia;
pub use information_ratio::InformationRatio;
@@ -712,6 +733,7 @@ pub use inverted_hammer::InvertedHammer;
pub use jarque_bera::JarqueBera;
pub use jma::Jma;
pub use jump_indicator::JumpIndicator;
pub use k_ratio::KRatio;
pub use kagi_bars::{KagiBar, KagiBars};
pub use kalman_hedge_ratio::{KalmanHedgeRatio, KalmanHedgeRatioOutput};
pub use kama::Kama;
@@ -739,6 +761,7 @@ pub use log_return::LogReturn;
pub use long_legged_doji::LongLeggedDoji;
pub use long_line::LongLine;
pub use long_short_ratio::LongShortRatio;
pub use m2_measure::M2Measure;
pub use ma_envelope::{MaEnvelope, MaEnvelopeOutput};
pub use macd::{MacdIndicator, MacdOutput};
pub use macd_ext::{MaType, MacdExt};
@@ -746,6 +769,7 @@ pub use macd_fix::MacdFix;
pub use macd_histogram::MacdHistogram;
pub use mama::{Mama, MamaOutput};
pub use market_facilitation_index::MarketFacilitationIndex;
pub use martin_ratio::MartinRatio;
pub use marubozu::Marubozu;
pub use mass_index::MassIndex;
pub use mat_hold::MatHold;
@@ -823,6 +847,7 @@ pub use qstick::Qstick;
pub use quartile_bands::{QuartileBands, QuartileBandsOutput};
pub use quoted_spread::QuotedSpread;
pub use r_squared::RSquared;
pub use range_bars::{RangeBar, RangeBars};
pub use realized_spread::RealizedSpread;
pub use realized_volatility::RealizedVolatility;
pub use recovery_factor::RecoveryFactor;
@@ -850,6 +875,7 @@ pub use rolling_quantile::RollingQuantile;
pub use roofing_filter::RoofingFilter;
pub use rsi::Rsi;
pub use rsx::Rsx;
pub use run_bars::{RunBar, RunBars};
pub use rvi::Rvi;
pub use rvi_volatility::RviVolatility;
pub use rwi::{Rwi, RwiOutput};
@@ -887,6 +913,7 @@ pub use starc_bands::{StarcBands, StarcBandsOutput};
pub use stc::Stc;
pub use std_dev::StdDev;
pub use step_trailing_stop::StepTrailingStop;
pub use sterling_ratio::SterlingRatio;
pub use stick_sandwich::StickSandwich;
pub use stoch_rsi::StochRsi;
pub use stochastic::{Stochastic, StochasticOutput};
@@ -894,6 +921,7 @@ pub use stochastic_cci::StochasticCci;
pub use super_smoother::SuperSmoother;
pub use super_trend::{SuperTrend, SuperTrendOutput};
pub use t3::T3;
pub use tail_ratio::TailRatio;
pub use taker_buy_sell_ratio::TakerBuySellRatio;
pub use takuri::Takuri;
pub use tasuki_gap::TasukiGap;
@@ -921,11 +949,13 @@ pub use term_structure_basis::TermStructureBasis;
pub use three_drives::ThreeDrives;
pub use three_inside::ThreeInside;
pub use three_line_break::ThreeLineBreak;
pub use three_line_break_bars::{LineBreakBar, ThreeLineBreakBars};
pub use three_line_strike::ThreeLineStrike;
pub use three_outside::ThreeOutside;
pub use three_soldiers_or_crows::ThreeSoldiersOrCrows;
pub use three_stars_in_south::ThreeStarsInSouth;
pub use thrusting::Thrusting;
pub use tick_bars::{TickBar, TickBars};
pub use tick_index::TickIndex;
pub use tii::Tii;
pub use time_based_stop::TimeBasedStop;
@@ -964,6 +994,7 @@ pub use universal_oscillator::UniversalOscillator;
pub use up_down_volume_ratio::UpDownVolumeRatio;
pub use upside_gap_three_methods::UpsideGapThreeMethods;
pub use upside_gap_two_crows::UpsideGapTwoCrows;
pub use upside_potential_ratio::UpsidePotentialRatio;
pub use value_area::{ValueArea, ValueAreaOutput};
pub use value_at_risk::ValueAtRisk;
pub use variance::Variance;
@@ -974,6 +1005,7 @@ pub use volatility_cone::{VolatilityCone, VolatilityConeOutput};
pub use volatility_of_volatility::VolatilityOfVolatility;
pub use volatility_ratio::VolatilityRatio;
pub use volty_stop::VoltyStop;
pub use volume_bars::{VolumeBar, VolumeBars};
pub use volume_by_time_profile::{VolumeByTimeProfile, VolumeByTimeProfileOutput};
pub use volume_oscillator::VolumeOscillator;
pub use volume_profile::{VolumeProfile, VolumeProfileOutput};
@@ -1542,11 +1574,31 @@ pub const FAMILIES: &[(&str, &[&str])] = &[
"Alpha",
"WinRate",
"Expectancy",
"SterlingRatio",
"BurkeRatio",
"MartinRatio",
"TailRatio",
"KRatio",
"CommonSenseRatio",
"GainToPainRatio",
"UpsidePotentialRatio",
"M2Measure",
],
),
(
"Alt-Chart Bars",
&["RenkoBars", "KagiBars", "PointAndFigureBars"],
&[
"RenkoBars",
"KagiBars",
"PointAndFigureBars",
"RangeBars",
"TickBars",
"VolumeBars",
"DollarBars",
"ImbalanceBars",
"RunBars",
"ThreeLineBreakBars",
],
),
(
"Market Breadth",
@@ -1654,6 +1706,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, 498, "FAMILIES total drifted from indicator count");
assert_eq!(total, 514, "FAMILIES total drifted from indicator count");
}
}
@@ -0,0 +1,227 @@
//! Range bar builder — fixed price-range bars with no reversal penalty.
use crate::error::{Error, Result};
use crate::ohlcv::Candle;
use crate::traits::BarBuilder;
/// One completed range bar.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct RangeBar {
/// Price at the bar's origin edge.
pub open: f64,
/// Price at the bar's far edge (`open ± range`).
pub close: f64,
/// `+1` for an up bar, `-1` for a down bar.
pub direction: i8,
}
/// Range bar builder using a fixed price increment on close prices.
///
/// A range bar completes every time price travels a fixed `range` from the current
/// anchor, in *either* direction. This is the key difference from
/// [`RenkoBars`](crate::RenkoBars): Renko imposes a `2 * box_size` penalty to
/// reverse direction, so it filters out small oscillations; range bars have **no
/// reversal penalty** — a move of exactly `range` against the trend prints a bar
/// immediately. Range bars therefore track every leg of price movement, while Renko
/// smooths them.
///
/// Construction rules:
///
/// - The first candle seeds the anchor and prints no bar.
/// - Each subsequent candle prints one bar for every `range` of close movement away
/// from the anchor; a candle that gaps several ranges prints them all in one
/// [`BarBuilder::update`] call.
/// - Bars are aligned to the `range` grid relative to the seed price.
///
/// # Example
///
/// ```
/// use wickra_core::{BarBuilder, Candle, RangeBars};
///
/// let flat = |price: f64| Candle::new(price, price, price, price, 1.0, 0).unwrap();
/// let mut bars = RangeBars::new(1.0).unwrap();
/// assert!(bars.update(flat(10.0)).is_empty()); // seed
/// let up = bars.update(flat(12.0)); // +2 ranges
/// assert_eq!(up.len(), 2);
/// let down = bars.update(flat(11.0)); // -1 range, no penalty
/// assert_eq!(down.len(), 1);
/// ```
#[derive(Debug, Clone)]
pub struct RangeBars {
range: f64,
anchor: Option<f64>,
}
impl RangeBars {
/// Construct a range-bar builder with the given price increment.
///
/// # Errors
///
/// Returns [`Error::InvalidPeriod`] if `range` is not finite and positive.
pub fn new(range: f64) -> Result<Self> {
if !range.is_finite() || range <= 0.0 {
return Err(Error::InvalidPeriod {
message: "range must be finite and positive",
});
}
Ok(Self {
range,
anchor: None,
})
}
/// Configured price range.
pub const fn range(&self) -> f64 {
self.range
}
/// Current anchor level (the close of the last completed bar, or the seed
/// price before any bar has formed).
pub const fn anchor(&self) -> Option<f64> {
self.anchor
}
}
impl BarBuilder for RangeBars {
type Bar = RangeBar;
fn update(&mut self, candle: Candle) -> Vec<RangeBar> {
let close = candle.close;
let Some(mut anchor) = self.anchor else {
self.anchor = Some(close);
return Vec::new();
};
let range = self.range;
let mut bars = Vec::new();
while close >= anchor + range {
bars.push(RangeBar {
open: anchor,
close: anchor + range,
direction: 1,
});
anchor += range;
}
while close <= anchor - range {
bars.push(RangeBar {
open: anchor,
close: anchor - range,
direction: -1,
});
anchor -= range;
}
self.anchor = Some(anchor);
bars
}
fn reset(&mut self) {
self.anchor = None;
}
fn name(&self) -> &'static str {
"RangeBars"
}
}
#[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_range() {
assert!(matches!(
RangeBars::new(0.0),
Err(Error::InvalidPeriod { .. })
));
assert!(matches!(
RangeBars::new(-1.0),
Err(Error::InvalidPeriod { .. })
));
assert!(matches!(
RangeBars::new(f64::NAN),
Err(Error::InvalidPeriod { .. })
));
}
#[test]
fn accessors_and_metadata() {
let bars = RangeBars::new(2.5).unwrap();
assert_eq!(bars.name(), "RangeBars");
assert_relative_eq!(bars.range(), 2.5, epsilon = 1e-12);
assert_eq!(bars.anchor(), None);
}
#[test]
fn first_candle_seeds_without_bar() {
let mut bars = RangeBars::new(1.0).unwrap();
assert!(bars.update(flat(10.0)).is_empty());
assert_eq!(bars.anchor(), Some(10.0));
}
#[test]
fn up_move_prints_aligned_bars() {
let mut bars = RangeBars::new(1.0).unwrap();
bars.update(flat(10.0));
let up = bars.update(flat(13.0));
assert_eq!(up.len(), 3);
assert_relative_eq!(up[0].open, 10.0, epsilon = 1e-12);
assert_relative_eq!(up[2].close, 13.0, epsilon = 1e-12);
assert!(up.iter().all(|b| b.direction == 1));
assert_eq!(bars.anchor(), Some(13.0));
}
#[test]
fn down_move_prints_aligned_bars() {
let mut bars = RangeBars::new(1.0).unwrap();
bars.update(flat(10.0));
let down = bars.update(flat(7.0));
assert_eq!(down.len(), 3);
assert!(down.iter().all(|b| b.direction == -1));
assert_relative_eq!(down[2].close, 7.0, epsilon = 1e-12);
}
#[test]
fn reversal_needs_only_one_range() {
// Unlike Renko, a single-range move against the trend prints immediately.
let mut bars = RangeBars::new(1.0).unwrap();
bars.update(flat(10.0));
bars.update(flat(12.0)); // anchor 12, up
let down = bars.update(flat(11.0)); // drop of exactly one range
assert_eq!(down.len(), 1);
assert_eq!(down[0].direction, -1);
assert_relative_eq!(down[0].close, 11.0, epsilon = 1e-12);
assert_eq!(bars.anchor(), Some(11.0));
}
#[test]
fn small_move_prints_nothing() {
let mut bars = RangeBars::new(1.0).unwrap();
bars.update(flat(10.0));
assert!(bars.update(flat(10.5)).is_empty());
assert_eq!(bars.anchor(), Some(10.0));
}
#[test]
fn reset_clears_state() {
let mut bars = RangeBars::new(1.0).unwrap();
bars.update(flat(10.0));
bars.update(flat(13.0));
bars.reset();
assert_eq!(bars.anchor(), None);
assert!(bars.update(flat(50.0)).is_empty());
assert_eq!(bars.anchor(), Some(50.0));
}
#[test]
fn batch_concatenates_completed_bars() {
let mut bars = RangeBars::new(1.0).unwrap();
let candles = [flat(10.0), flat(12.0), flat(13.0)];
let out = bars.batch(&candles);
assert_eq!(out.len(), 3);
assert!(out.iter().all(|b| b.direction == 1));
}
}
@@ -0,0 +1,257 @@
//! Run bar builder (simplified López de Prado) — sample on runs of same-signed ticks.
use crate::error::{Error, Result};
use crate::ohlcv::Candle;
use crate::traits::BarBuilder;
/// One completed run bar.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct RunBar {
/// Open of the first candle in the bar.
pub open: f64,
/// Highest high across the bar.
pub high: f64,
/// Lowest low across the bar.
pub low: f64,
/// Close of the candle that closed the bar.
pub close: f64,
/// Length of the run that closed the bar (`== run_length`).
pub length: usize,
/// `+1` if a buy run closed the bar, `-1` if a sell run.
pub direction: i8,
}
/// Run bar builder — a **simplified** form of López de Prado's run bars.
///
/// A *run* is an uninterrupted sequence of same-signed ticks: a streak of up-ticks
/// (a buy run) or down-ticks (a sell run), with unchanged closes extending the
/// current run. This builder counts the current run's length and closes a bar when
/// it reaches `run_length`; a tick in the opposite direction restarts the run from
/// one. Where [`ImbalanceBars`](crate::ImbalanceBars) sample on the *net* signed
/// imbalance (which oscillating flow can cancel back to zero), run bars sample on
/// *persistence*: they fire only when the market pushes the same way without
/// interruption, making them a cleaner sequential-trend detector.
///
/// **Simplification.** The full method estimates a *dynamic* expected run length
/// from an EWMA and can weight runs by volume or traded value. This builder uses a
/// **fixed** run-length threshold on unweighted ticks. See López de Prado (2018),
/// ch. 2, for the adaptive estimator and weighted variants.
///
/// At most one bar closes per candle, so [`BarBuilder::update`] returns either an
/// empty vector or a single [`RunBar`].
///
/// # Example
///
/// ```
/// use wickra_core::{BarBuilder, Candle, RunBars};
///
/// let flat = |price: f64| Candle::new(price, price, price, price, 1.0, 0).unwrap();
/// let mut bars = RunBars::new(3).unwrap();
/// bars.update(flat(10.0)); // seed
/// bars.update(flat(11.0)); // run 1
/// bars.update(flat(12.0)); // run 2
/// let out = bars.update(flat(13.0)); // run 3 -> close
/// assert_eq!(out.len(), 1);
/// assert_eq!(out[0].direction, 1);
/// ```
#[derive(Debug, Clone)]
pub struct RunBars {
run_length: usize,
count: usize,
open: f64,
high: f64,
low: f64,
close: f64,
prev_close: Option<f64>,
run_sign: i8,
run_len: usize,
}
impl RunBars {
/// Construct a run-bar builder that closes a bar on a run of `run_length` ticks.
///
/// # Errors
///
/// Returns [`Error::PeriodZero`] if `run_length == 0`.
pub fn new(run_length: usize) -> Result<Self> {
if run_length == 0 {
return Err(Error::PeriodZero);
}
Ok(Self {
run_length,
count: 0,
open: 0.0,
high: 0.0,
low: 0.0,
close: 0.0,
prev_close: None,
run_sign: 0,
run_len: 0,
})
}
/// Configured run length that closes a bar.
pub const fn run_length(&self) -> usize {
self.run_length
}
/// Length of the in-progress run.
pub const fn run(&self) -> usize {
self.run_len
}
}
impl BarBuilder for RunBars {
type Bar = RunBar;
fn update(&mut self, candle: Candle) -> Vec<RunBar> {
if self.count == 0 {
self.open = candle.open;
self.high = candle.high;
self.low = candle.low;
} else {
self.high = self.high.max(candle.high);
self.low = self.low.min(candle.low);
}
self.close = candle.close;
self.count += 1;
if let Some(prev) = self.prev_close {
let directional = if candle.close > prev {
1
} else if candle.close < prev {
-1
} else {
0
};
if directional == 0 {
// A flat tick extends the current run (if one is under way).
if self.run_sign != 0 {
self.run_len += 1;
}
} else if directional == self.run_sign {
self.run_len += 1;
} else {
self.run_sign = directional;
self.run_len = 1;
}
}
self.prev_close = Some(candle.close);
if self.run_sign == 0 || self.run_len < self.run_length {
return Vec::new();
}
let bar = RunBar {
open: self.open,
high: self.high,
low: self.low,
close: self.close,
length: self.run_len,
direction: self.run_sign,
};
self.count = 0;
self.run_sign = 0;
self.run_len = 0;
vec![bar]
}
fn reset(&mut self) {
self.count = 0;
self.prev_close = None;
self.run_sign = 0;
self.run_len = 0;
}
fn name(&self) -> &'static str {
"RunBars"
}
}
#[cfg(test)]
mod tests {
use super::*;
fn flat(price: f64) -> Candle {
Candle::new(price, price, price, price, 1.0, 0).unwrap()
}
#[test]
fn rejects_zero_run_length() {
assert!(matches!(RunBars::new(0), Err(Error::PeriodZero)));
}
#[test]
fn accessors_and_metadata() {
let bars = RunBars::new(5).unwrap();
assert_eq!(bars.run_length(), 5);
assert_eq!(bars.run(), 0);
assert_eq!(bars.name(), "RunBars");
}
#[test]
fn buy_run_closes_up_bar() {
let mut bars = RunBars::new(3).unwrap();
bars.update(flat(10.0)); // seed
bars.update(flat(11.0)); // run 1
bars.update(flat(12.0)); // run 2
let out = bars.update(flat(13.0)); // run 3
assert_eq!(out.len(), 1);
assert_eq!(out[0].direction, 1);
assert_eq!(out[0].length, 3);
}
#[test]
fn sell_run_closes_down_bar() {
let mut bars = RunBars::new(3).unwrap();
bars.update(flat(10.0));
bars.update(flat(9.0)); // run 1
bars.update(flat(8.0)); // run 2
let out = bars.update(flat(7.0)); // run 3
assert_eq!(out.len(), 1);
assert_eq!(out[0].direction, -1);
}
#[test]
fn opposite_tick_restarts_run() {
let mut bars = RunBars::new(3).unwrap();
bars.update(flat(10.0));
bars.update(flat(11.0)); // up run 1
bars.update(flat(12.0)); // up run 2
bars.update(flat(11.0)); // down -> run restarts at 1
assert_eq!(bars.run(), 1);
}
#[test]
fn flat_tick_extends_run() {
let mut bars = RunBars::new(3).unwrap();
bars.update(flat(10.0));
bars.update(flat(11.0)); // run 1
bars.update(flat(11.0)); // flat -> run 2
let out = bars.update(flat(12.0)); // run 3
assert_eq!(out.len(), 1);
assert_eq!(out[0].direction, 1);
}
#[test]
fn reset_clears_state() {
let mut bars = RunBars::new(3).unwrap();
bars.update(flat(10.0));
bars.update(flat(11.0));
bars.reset();
assert_eq!(bars.run(), 0);
assert!(bars.update(flat(50.0)).is_empty());
}
#[test]
fn batch_concatenates_completed_bars() {
let mut bars = RunBars::new(2).unwrap();
let candles = [
flat(10.0),
flat(11.0), // run 1
flat(12.0), // run 2 -> close
flat(13.0), // run 1
flat(14.0), // run 2 -> close
];
let out = bars.batch(&candles);
assert_eq!(out.len(), 2);
assert!(out.iter().all(|b| b.direction == 1));
}
}
@@ -0,0 +1,216 @@
//! Sterling Ratio — mean return over the average drawdown of the equity curve.
use std::collections::VecDeque;
use crate::error::{Error, Result};
use crate::traits::Indicator;
/// Sterling Ratio over a trailing window of `period` returns.
///
/// ```text
/// equity_t = Π_{i<=t} (1 + return_i) (compounded curve)
/// peak_t = max_{s<=t} equity_s
/// dd_t = (peak_t equity_t) / peak_t (fractional drawdown, >= 0)
/// Sterling = mean(returns) / mean(dd_t)
/// ```
///
/// The Sterling Ratio rewards return per unit of *typical* pain: it divides the
/// average per-period return by the **average drawdown** experienced along the
/// compounded equity curve. Of the three drawdown-based ratios Wickra ships it is
/// the gentlest on outliers — averaging the drawdowns means one deep crater does
/// not dominate the way it does in the [`BurkeRatio`](crate::BurkeRatio) (which
/// sums squared drawdowns) or the [`MartinRatio`](crate::MartinRatio) (which uses
/// the root-mean-square percentage drawdown). A window that never draws down has
/// zero average drawdown and the indicator reports `0.0`.
///
/// The first value lands after `period` returns; each `update` rebuilds the equity
/// curve over the window (O(period)), which is O(1) in the length of the overall
/// series.
///
/// # Example
///
/// ```
/// use wickra_core::{Indicator, SterlingRatio};
///
/// let mut indicator = SterlingRatio::new(12).unwrap();
/// let mut last = None;
/// for i in 0..24 {
/// last = indicator.update((f64::from(i) * 0.5).sin() * 0.05);
/// }
/// assert!(last.is_some());
/// ```
#[derive(Debug, Clone)]
pub struct SterlingRatio {
period: usize,
window: VecDeque<f64>,
}
impl SterlingRatio {
/// Construct a Sterling Ratio over `period` returns.
///
/// # Errors
///
/// Returns [`Error::InvalidPeriod`] if `period < 2`.
pub fn new(period: usize) -> Result<Self> {
if period < 2 {
return Err(Error::InvalidPeriod {
message: "sterling ratio needs period >= 2",
});
}
Ok(Self {
period,
window: VecDeque::with_capacity(period),
})
}
/// Configured window of returns.
pub const fn period(&self) -> usize {
self.period
}
fn compute(&self) -> f64 {
#[allow(clippy::cast_precision_loss)]
let length = self.window.len() as f64;
let mut sum_return = 0.0;
let mut sum_drawdown = 0.0;
let mut equity = 1.0;
let mut peak: f64 = 1.0;
for ret in &self.window {
sum_return += *ret;
equity *= 1.0 + *ret;
peak = peak.max(equity);
sum_drawdown += (peak - equity) / peak;
}
let avg_drawdown = sum_drawdown / length;
if avg_drawdown > 0.0 {
(sum_return / length) / avg_drawdown
} else {
0.0
}
}
}
impl Indicator for SterlingRatio {
type Input = f64;
type Output = f64;
fn update(&mut self, ret: f64) -> Option<f64> {
if !ret.is_finite() {
return None;
}
if self.window.len() == self.period {
self.window.pop_front();
}
self.window.push_back(ret);
if self.window.len() < self.period {
return None;
}
Some(self.compute())
}
fn reset(&mut self) {
self.window.clear();
}
fn warmup_period(&self) -> usize {
self.period
}
fn is_ready(&self) -> bool {
self.window.len() == self.period
}
fn name(&self) -> &'static str {
"SterlingRatio"
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::traits::BatchExt;
use approx::assert_relative_eq;
#[test]
fn rejects_period_less_than_two() {
assert!(matches!(
SterlingRatio::new(1),
Err(Error::InvalidPeriod { .. })
));
}
#[test]
fn accessors_and_metadata() {
let sr = SterlingRatio::new(12).unwrap();
assert_eq!(sr.period(), 12);
assert_eq!(sr.warmup_period(), 12);
assert_eq!(sr.name(), "SterlingRatio");
assert!(!sr.is_ready());
}
#[test]
fn reference_value() {
// returns [0.1, -0.1, 0.1]:
// equity 1.1, 0.99, 1.089; peak stays 1.1.
// dd = [0, 0.1, 0.01]; avg_dd = 0.11/3; mean_return = 0.1/3.
// Sterling = (0.1/3) / (0.11/3) = 0.1/0.11.
let mut sr = SterlingRatio::new(3).unwrap();
let out = sr.batch(&[0.1, -0.1, 0.1]);
assert_relative_eq!(out[2].unwrap(), 0.1_f64 / 0.11, epsilon = 1e-9);
}
#[test]
fn no_drawdown_is_zero() {
// Monotonically rising equity never draws down.
let mut sr = SterlingRatio::new(3).unwrap();
let last = sr
.batch(&[0.01, 0.02, 0.03])
.into_iter()
.flatten()
.last()
.unwrap();
assert_relative_eq!(last, 0.0, epsilon = 1e-12);
}
#[test]
fn losing_window_is_negative() {
let mut sr = SterlingRatio::new(3).unwrap();
let last = sr
.batch(&[-0.05, -0.02, -0.03])
.into_iter()
.flatten()
.last()
.unwrap();
assert!(last < 0.0);
}
#[test]
fn ignores_non_finite_input() {
let mut sr = SterlingRatio::new(3).unwrap();
assert_eq!(sr.update(0.1), None);
assert_eq!(sr.update(f64::NAN), None);
assert_eq!(sr.update(-0.1), None);
assert!(sr.update(0.1).is_some());
}
#[test]
fn reset_clears_state() {
let mut sr = SterlingRatio::new(3).unwrap();
sr.batch(&[0.1, -0.1, 0.1]);
assert!(sr.is_ready());
sr.reset();
assert!(!sr.is_ready());
assert_eq!(sr.update(0.1), None);
}
#[test]
fn batch_equals_streaming() {
let rets: Vec<f64> = (0..60)
.map(|i| (f64::from(i) * 0.25).sin() * 0.05)
.collect();
let batch = SterlingRatio::new(12).unwrap().batch(&rets);
let mut streamer = SterlingRatio::new(12).unwrap();
let streamed: Vec<_> = rets.iter().map(|r| streamer.update(*r)).collect();
assert_eq!(batch, streamed);
}
}
@@ -0,0 +1,224 @@
//! Tail Ratio — the right tail (95th percentile) over the absolute left tail (5th percentile).
use std::collections::VecDeque;
use crate::error::{Error, Result};
use crate::traits::Indicator;
/// Tail Ratio over a trailing window of `period` returns.
///
/// ```text
/// TailRatio = P95(returns) / |P5(returns)|
/// ```
///
/// The Tail Ratio contrasts the magnitude of the best outcomes against the worst:
/// the 95th percentile of the return distribution divided by the absolute value of
/// the 5th percentile. A value above `1.0` means the right tail (upside surprises)
/// is fatter than the left tail (downside surprises); below `1.0` means crashes are
/// larger than rallies. It is a distribution-shape statistic, distinct from the
/// average-based [`SharpeRatio`](crate::SharpeRatio): two series with the same mean
/// and variance can have very different tail ratios.
///
/// Percentiles are computed by linear interpolation over the sorted window
/// (the same rule `NumPy` uses by default). A window whose 5th percentile is exactly
/// zero has no measurable left tail and the indicator reports `0.0` rather than
/// dividing by zero.
///
/// The first value lands after `period` returns; each `update` re-sorts the window
/// (O(period log period)), which is O(1) in the length of the overall series.
///
/// # Example
///
/// ```
/// use wickra_core::{Indicator, TailRatio};
///
/// let mut indicator = TailRatio::new(20).unwrap();
/// let mut last = None;
/// for i in 0..40 {
/// last = indicator.update((f64::from(i) * 0.3).sin() * 0.02);
/// }
/// assert!(last.is_some());
/// ```
#[derive(Debug, Clone)]
pub struct TailRatio {
period: usize,
window: VecDeque<f64>,
}
impl TailRatio {
/// Construct a Tail Ratio over `period` returns.
///
/// # Errors
///
/// Returns [`Error::InvalidPeriod`] if `period < 2` (percentiles need at least
/// two observations to interpolate).
pub fn new(period: usize) -> Result<Self> {
if period < 2 {
return Err(Error::InvalidPeriod {
message: "tail ratio needs period >= 2",
});
}
Ok(Self {
period,
window: VecDeque::with_capacity(period),
})
}
/// Configured window of returns.
pub const fn period(&self) -> usize {
self.period
}
fn compute(&self) -> f64 {
let mut sorted: Vec<f64> = self.window.iter().copied().collect();
sorted.sort_unstable_by(f64::total_cmp);
let upper = percentile(&sorted, 95.0);
let lower = percentile(&sorted, 5.0).abs();
if lower > 0.0 {
upper / lower
} else {
0.0
}
}
}
/// Linear-interpolation percentile of an ascending, non-empty slice.
fn percentile(sorted: &[f64], pct: f64) -> f64 {
let last_index = sorted.len() - 1;
#[allow(clippy::cast_precision_loss)]
let rank = pct / 100.0 * last_index as f64;
let floor = rank.floor();
// `rank` lies in `[0, last_index]`, so its floor is a valid in-bounds index.
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
let lower = floor as usize;
if lower >= last_index {
return sorted[last_index];
}
let frac = rank - floor;
sorted[lower] + frac * (sorted[lower + 1] - sorted[lower])
}
impl Indicator for TailRatio {
type Input = f64;
type Output = f64;
fn update(&mut self, ret: f64) -> Option<f64> {
if !ret.is_finite() {
return None;
}
if self.window.len() == self.period {
self.window.pop_front();
}
self.window.push_back(ret);
if self.window.len() < self.period {
return None;
}
Some(self.compute())
}
fn reset(&mut self) {
self.window.clear();
}
fn warmup_period(&self) -> usize {
self.period
}
fn is_ready(&self) -> bool {
self.window.len() == self.period
}
fn name(&self) -> &'static str {
"TailRatio"
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::traits::BatchExt;
use approx::assert_relative_eq;
#[test]
fn rejects_period_less_than_two() {
assert!(matches!(
TailRatio::new(1),
Err(Error::InvalidPeriod { .. })
));
assert!(matches!(
TailRatio::new(0),
Err(Error::InvalidPeriod { .. })
));
}
#[test]
fn accessors_and_metadata() {
let tr = TailRatio::new(20).unwrap();
assert_eq!(tr.period(), 20);
assert_eq!(tr.warmup_period(), 20);
assert_eq!(tr.name(), "TailRatio");
assert!(!tr.is_ready());
}
#[test]
fn reference_value() {
// sorted window [-0.04, -0.02, 0.0, 0.02, 0.04], last_index = 4.
// P95: rank 3.8 -> 0.02 + 0.8*(0.04-0.02) = 0.036.
// P5: rank 0.2 -> -0.04 + 0.2*(0.02) = -0.036, abs 0.036.
// ratio = 0.036 / 0.036 = 1.0.
let mut tr = TailRatio::new(5).unwrap();
let out = tr.batch(&[-0.04, -0.02, 0.0, 0.02, 0.04]);
assert_relative_eq!(out[4].unwrap(), 1.0, epsilon = 1e-9);
}
#[test]
fn fatter_right_tail_exceeds_one() {
let mut tr = TailRatio::new(5).unwrap();
let out = tr.batch(&[-0.01, 0.0, 0.01, 0.02, 0.10]);
assert!(out[4].unwrap() > 1.0);
}
#[test]
fn flat_window_is_zero() {
let mut tr = TailRatio::new(4).unwrap();
let last = tr.batch(&[0.0; 4]).into_iter().flatten().last().unwrap();
assert_relative_eq!(last, 0.0, epsilon = 1e-12);
}
#[test]
fn ignores_non_finite_input() {
let mut tr = TailRatio::new(3).unwrap();
assert_eq!(tr.update(0.01), None);
assert_eq!(tr.update(f64::NAN), None);
assert_eq!(tr.update(0.02), None);
assert!(tr.update(0.03).is_some());
}
#[test]
fn reset_clears_state() {
let mut tr = TailRatio::new(3).unwrap();
tr.batch(&[-0.01, 0.0, 0.02]);
assert!(tr.is_ready());
tr.reset();
assert!(!tr.is_ready());
assert_eq!(tr.update(0.01), None);
}
#[test]
fn batch_equals_streaming() {
let rets: Vec<f64> = (0..60)
.map(|i| (f64::from(i) * 0.25).sin() * 0.02)
.collect();
let batch = TailRatio::new(15).unwrap().batch(&rets);
let mut streamer = TailRatio::new(15).unwrap();
let streamed: Vec<_> = rets.iter().map(|r| streamer.update(*r)).collect();
assert_eq!(batch, streamed);
}
#[test]
fn percentile_at_top_returns_last() {
// When the rank floor reaches the final index (the 100th percentile), the
// helper returns the largest element without interpolating past the end.
assert_relative_eq!(percentile(&[1.0, 2.0, 3.0], 100.0), 3.0, epsilon = 1e-12);
}
}
@@ -0,0 +1,305 @@
//! Three-Line-Break bar builder — line-break chart segments driven by close prices.
use std::collections::VecDeque;
use crate::error::{Error, Result};
use crate::ohlcv::Candle;
use crate::traits::BarBuilder;
/// One completed line-break line.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct LineBreakBar {
/// Price where the line began (the previous line's far edge).
pub open: f64,
/// Price where the line ended (the new close that drew it).
pub close: f64,
/// `+1` for a rising line, `-1` for a falling line.
pub direction: i8,
}
/// Three-Line-Break bar builder using the classic close-based reversal rule.
///
/// A line-break chart draws a new line in the trend direction whenever the close
/// makes a new extreme, and only reverses when the close breaks the extreme of the
/// previous `lines` lines (three by default — hence "three-line break"). This filters
/// minor noise: a pullback that fails to exceed the last three lines is ignored
/// entirely, so the chart isolates meaningful reversals.
///
/// This is the **bar-builder** counterpart of the
/// [`ThreeLineBreak`](crate::ThreeLineBreak) indicator: the indicator reports the
/// current line *state* as a streaming value, whereas this builder emits each
/// completed line as a [`LineBreakBar`] so you can reconstruct the full line-break
/// chart. At most one line forms per candle, so [`BarBuilder::update`] returns either
/// an empty vector or a single bar.
///
/// Construction rules:
///
/// - The first candle seeds a reference close and prints nothing.
/// - The first subsequent move (up or down) draws the first line.
/// - In an up-trend a close above the last line's top extends it (a new up line); a
/// close below the lowest low of the last `lines` lines reverses to a down line.
/// The down-trend is symmetric.
///
/// # Example
///
/// ```
/// use wickra_core::{BarBuilder, Candle, ThreeLineBreakBars};
///
/// let flat = |price: f64| Candle::new(price, price, price, price, 1.0, 0).unwrap();
/// let mut bars = ThreeLineBreakBars::new(3).unwrap();
/// bars.update(flat(10.0)); // seed
/// let first = bars.update(flat(11.0)); // first up line
/// assert_eq!(first.len(), 1);
/// assert_eq!(first[0].direction, 1);
/// ```
#[derive(Debug, Clone)]
pub struct ThreeLineBreakBars {
lines: usize,
seed: Option<f64>,
recent: VecDeque<LineBreakBar>,
}
impl ThreeLineBreakBars {
/// Construct a line-break builder that reverses on a break of the last `lines`
/// lines (3 for the classic three-line break).
///
/// # Errors
///
/// Returns [`Error::PeriodZero`] if `lines == 0`.
pub fn new(lines: usize) -> Result<Self> {
if lines == 0 {
return Err(Error::PeriodZero);
}
Ok(Self {
lines,
seed: None,
recent: VecDeque::with_capacity(lines),
})
}
/// Configured number of lines a reversal must break.
pub const fn lines(&self) -> usize {
self.lines
}
/// Number of recent lines currently tracked for the reversal test.
pub fn tracked(&self) -> usize {
self.recent.len()
}
fn push_line(&mut self, bar: LineBreakBar) {
if self.recent.len() == self.lines {
self.recent.pop_front();
}
self.recent.push_back(bar);
}
fn lowest_low(&self) -> f64 {
self.recent
.iter()
.map(|bar| bar.open.min(bar.close))
.fold(f64::INFINITY, f64::min)
}
fn highest_high(&self) -> f64 {
self.recent
.iter()
.map(|bar| bar.open.max(bar.close))
.fold(f64::NEG_INFINITY, f64::max)
}
}
impl BarBuilder for ThreeLineBreakBars {
type Bar = LineBreakBar;
fn update(&mut self, candle: Candle) -> Vec<LineBreakBar> {
let close = candle.close;
let Some(last) = self.recent.back().copied() else {
// No line yet: seed, then draw the first line on the first move.
let Some(seed) = self.seed else {
self.seed = Some(close);
return Vec::new();
};
let bar = if close > seed {
LineBreakBar {
open: seed,
close,
direction: 1,
}
} else if close < seed {
LineBreakBar {
open: seed,
close,
direction: -1,
}
} else {
return Vec::new();
};
self.push_line(bar);
return vec![bar];
};
let new_bar = if last.direction > 0 {
if close > last.close {
Some(LineBreakBar {
open: last.close,
close,
direction: 1,
})
} else if close < self.lowest_low() {
Some(LineBreakBar {
open: last.close,
close,
direction: -1,
})
} else {
None
}
} else if close < last.close {
Some(LineBreakBar {
open: last.close,
close,
direction: -1,
})
} else if close > self.highest_high() {
Some(LineBreakBar {
open: last.close,
close,
direction: 1,
})
} else {
None
};
if let Some(bar) = new_bar {
self.push_line(bar);
vec![bar]
} else {
Vec::new()
}
}
fn reset(&mut self) {
self.seed = None;
self.recent.clear();
}
fn name(&self) -> &'static str {
"ThreeLineBreakBars"
}
}
#[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_zero_lines() {
assert!(matches!(ThreeLineBreakBars::new(0), Err(Error::PeriodZero)));
}
#[test]
fn accessors_and_metadata() {
let bars = ThreeLineBreakBars::new(3).unwrap();
assert_eq!(bars.lines(), 3);
assert_eq!(bars.tracked(), 0);
assert_eq!(bars.name(), "ThreeLineBreakBars");
}
#[test]
fn seed_then_first_line() {
let mut bars = ThreeLineBreakBars::new(3).unwrap();
assert!(bars.update(flat(10.0)).is_empty()); // seed
let first = bars.update(flat(11.0));
assert_eq!(first.len(), 1);
assert_eq!(first[0].direction, 1);
assert_relative_eq!(first[0].open, 10.0, epsilon = 1e-12);
assert_relative_eq!(first[0].close, 11.0, epsilon = 1e-12);
}
#[test]
fn new_high_extends_up_line() {
let mut bars = ThreeLineBreakBars::new(3).unwrap();
bars.update(flat(10.0));
bars.update(flat(11.0)); // line 1 up
let cont = bars.update(flat(12.0)); // new high -> extend
assert_eq!(cont.len(), 1);
assert_eq!(cont[0].direction, 1);
assert_relative_eq!(cont[0].open, 11.0, epsilon = 1e-12);
}
#[test]
fn small_pullback_prints_nothing() {
let mut bars = ThreeLineBreakBars::new(3).unwrap();
bars.update(flat(10.0));
bars.update(flat(11.0)); // line 1
bars.update(flat(12.0)); // line 2
bars.update(flat(13.0)); // line 3, lows are 10/11/12
assert!(bars.update(flat(10.5)).is_empty()); // not > 13, not < 10
}
#[test]
fn reversal_breaks_three_lines() {
let mut bars = ThreeLineBreakBars::new(3).unwrap();
bars.update(flat(10.0));
bars.update(flat(11.0)); // line 1, low 10
bars.update(flat(12.0)); // line 2, low 11
bars.update(flat(13.0)); // line 3, low 12
let rev = bars.update(flat(9.0)); // 9 < lowest low 10 -> reverse
assert_eq!(rev.len(), 1);
assert_eq!(rev[0].direction, -1);
assert_relative_eq!(rev[0].open, 13.0, epsilon = 1e-12);
assert_relative_eq!(rev[0].close, 9.0, epsilon = 1e-12);
}
#[test]
fn reset_clears_state() {
let mut bars = ThreeLineBreakBars::new(3).unwrap();
bars.update(flat(10.0));
bars.update(flat(11.0));
bars.reset();
assert_eq!(bars.tracked(), 0);
assert!(bars.update(flat(50.0)).is_empty()); // re-seeds
}
#[test]
fn flat_first_move_prints_nothing() {
let mut bars = ThreeLineBreakBars::new(3).unwrap();
assert!(bars.update(flat(10.0)).is_empty()); // seed
assert!(bars.update(flat(10.0)).is_empty()); // equal to seed -> no line
}
#[test]
fn first_line_down_then_down_trend_and_reversal() {
let mut bars = ThreeLineBreakBars::new(3).unwrap();
assert!(bars.update(flat(10.0)).is_empty()); // seed
let first = bars.update(flat(9.0)); // first line down
assert_eq!(first.len(), 1);
assert_eq!(first[0].direction, -1);
assert_relative_eq!(first[0].open, 10.0, epsilon = 1e-12);
assert_relative_eq!(first[0].close, 9.0, epsilon = 1e-12);
let cont = bars.update(flat(8.0)); // new low extends the down line
assert_eq!(cont.len(), 1);
assert_eq!(cont[0].direction, -1);
assert_relative_eq!(cont[0].open, 9.0, epsilon = 1e-12);
bars.update(flat(7.0)); // third down line; highs are 10/9/8
assert!(bars.update(flat(7.5)).is_empty()); // not < 7, not > highest high 10
let rev = bars.update(flat(11.0)); // > highest high 10 -> reverse up
assert_eq!(rev.len(), 1);
assert_eq!(rev[0].direction, 1);
assert_relative_eq!(rev[0].open, 7.0, epsilon = 1e-12);
}
#[test]
fn batch_concatenates_completed_lines() {
let mut bars = ThreeLineBreakBars::new(3).unwrap();
let candles = [flat(10.0), flat(11.0), flat(12.0), flat(13.0)];
let out = bars.batch(&candles);
// seed at 10, then three rising lines.
assert_eq!(out.len(), 3);
assert!(out.iter().all(|b| b.direction == 1));
}
}
@@ -0,0 +1,209 @@
//! Tick bar builder — aggregate a fixed number of candles into one OHLCV bar.
use crate::error::{Error, Result};
use crate::ohlcv::Candle;
use crate::traits::BarBuilder;
/// One completed tick bar (an OHLCV aggregate of `ticks` input candles).
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct TickBar {
/// Open of the first candle in the group.
pub open: f64,
/// Highest high across the group.
pub high: f64,
/// Lowest low across the group.
pub low: f64,
/// Close of the last candle in the group.
pub close: f64,
/// Summed volume across the group.
pub volume: f64,
}
/// Tick bar builder — emits one OHLCV bar for every `ticks` input candles.
///
/// Classic time bars (1-minute, 1-hour) sample the market on a clock; tick bars
/// sample it on *activity* by grouping a fixed number of trades — here modelled as a
/// fixed number of input candles. In fast markets a tick bar closes quickly; in
/// quiet markets it takes longer, so each bar carries roughly equal information
/// content. This is the simplest of the information-driven bar types; the
/// [`VolumeBars`](crate::VolumeBars) and [`DollarBars`](crate::DollarBars) builders
/// extend the idea to equal traded volume and equal traded value respectively.
///
/// The open is the first candle's open, the high and low are the extremes across the
/// group, the close is the last candle's close, and the volume is the group sum.
/// Exactly one bar completes every `ticks` candles, so [`BarBuilder::update`]
/// returns either an empty vector or a single [`TickBar`].
///
/// # Example
///
/// ```
/// use wickra_core::{BarBuilder, Candle, TickBars};
///
/// let c = |o, h, l, cl, v| Candle::new(o, h, l, cl, v, 0).unwrap();
/// let mut bars = TickBars::new(3).unwrap();
/// assert!(bars.update(c(10.0, 11.0, 9.0, 10.5, 100.0)).is_empty());
/// assert!(bars.update(c(10.5, 12.0, 10.0, 11.0, 150.0)).is_empty());
/// let out = bars.update(c(11.0, 11.5, 10.8, 11.2, 120.0));
/// assert_eq!(out.len(), 1);
/// assert_eq!(out[0].volume, 370.0);
/// ```
#[derive(Debug, Clone)]
pub struct TickBars {
ticks: usize,
count: usize,
open: f64,
high: f64,
low: f64,
close: f64,
volume: f64,
}
impl TickBars {
/// Construct a tick-bar builder that groups `ticks` candles per bar.
///
/// # Errors
///
/// Returns [`Error::PeriodZero`] if `ticks == 0`.
pub fn new(ticks: usize) -> Result<Self> {
if ticks == 0 {
return Err(Error::PeriodZero);
}
Ok(Self {
ticks,
count: 0,
open: 0.0,
high: 0.0,
low: 0.0,
close: 0.0,
volume: 0.0,
})
}
/// Configured number of candles per bar.
pub const fn ticks(&self) -> usize {
self.ticks
}
/// Number of candles accumulated into the in-progress bar.
pub const fn count(&self) -> usize {
self.count
}
}
impl BarBuilder for TickBars {
type Bar = TickBar;
fn update(&mut self, candle: Candle) -> Vec<TickBar> {
if self.count == 0 {
self.open = candle.open;
self.high = candle.high;
self.low = candle.low;
self.volume = 0.0;
} else {
self.high = self.high.max(candle.high);
self.low = self.low.min(candle.low);
}
self.close = candle.close;
self.volume += candle.volume;
self.count += 1;
if self.count < self.ticks {
return Vec::new();
}
self.count = 0;
vec![TickBar {
open: self.open,
high: self.high,
low: self.low,
close: self.close,
volume: self.volume,
}]
}
fn reset(&mut self) {
self.count = 0;
self.volume = 0.0;
}
fn name(&self) -> &'static str {
"TickBars"
}
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_relative_eq;
fn candle(open: f64, high: f64, low: f64, close: f64, volume: f64) -> Candle {
Candle::new(open, high, low, close, volume, 0).unwrap()
}
#[test]
fn rejects_zero_ticks() {
assert!(matches!(TickBars::new(0), Err(Error::PeriodZero)));
}
#[test]
fn accessors_and_metadata() {
let bars = TickBars::new(5).unwrap();
assert_eq!(bars.ticks(), 5);
assert_eq!(bars.count(), 0);
assert_eq!(bars.name(), "TickBars");
}
#[test]
fn emits_every_n_candles() {
let mut bars = TickBars::new(2).unwrap();
assert!(bars.update(candle(10.0, 10.0, 10.0, 10.0, 1.0)).is_empty());
assert_eq!(bars.update(candle(10.0, 10.0, 10.0, 10.0, 1.0)).len(), 1);
assert!(bars.update(candle(10.0, 10.0, 10.0, 10.0, 1.0)).is_empty());
assert_eq!(bars.update(candle(10.0, 10.0, 10.0, 10.0, 1.0)).len(), 1);
}
#[test]
fn aggregates_ohlcv() {
let mut bars = TickBars::new(3).unwrap();
bars.update(candle(10.0, 11.0, 9.0, 10.5, 100.0));
bars.update(candle(10.5, 12.0, 10.0, 11.0, 150.0));
let out = bars.update(candle(11.0, 11.5, 10.8, 11.2, 120.0));
assert_eq!(out.len(), 1);
assert_relative_eq!(out[0].open, 10.0, epsilon = 1e-12);
assert_relative_eq!(out[0].high, 12.0, epsilon = 1e-12);
assert_relative_eq!(out[0].low, 9.0, epsilon = 1e-12);
assert_relative_eq!(out[0].close, 11.2, epsilon = 1e-12);
assert_relative_eq!(out[0].volume, 370.0, epsilon = 1e-12);
}
#[test]
fn partial_group_emits_nothing() {
let mut bars = TickBars::new(4).unwrap();
bars.update(candle(10.0, 10.0, 10.0, 10.0, 1.0));
bars.update(candle(10.0, 10.0, 10.0, 10.0, 1.0));
assert_eq!(bars.count(), 2);
}
#[test]
fn reset_clears_state() {
let mut bars = TickBars::new(3).unwrap();
bars.update(candle(10.0, 10.0, 10.0, 10.0, 1.0));
bars.update(candle(10.0, 10.0, 10.0, 10.0, 1.0));
bars.reset();
assert_eq!(bars.count(), 0);
// After reset the next candle starts a fresh group.
assert!(bars.update(candle(20.0, 20.0, 20.0, 20.0, 5.0)).is_empty());
assert_eq!(bars.count(), 1);
}
#[test]
fn batch_concatenates_completed_bars() {
let mut bars = TickBars::new(2).unwrap();
let candles = [
candle(10.0, 10.0, 10.0, 10.0, 1.0),
candle(10.0, 10.0, 10.0, 10.0, 1.0),
candle(10.0, 10.0, 10.0, 10.0, 1.0),
candle(10.0, 10.0, 10.0, 10.0, 1.0),
];
let out = bars.batch(&candles);
assert_eq!(out.len(), 2);
}
}
@@ -0,0 +1,226 @@
//! Upside Potential Ratio (Sortino, van der Meer & Plantinga) — upside mean over downside deviation.
use std::collections::VecDeque;
use crate::error::{Error, Result};
use crate::traits::Indicator;
/// Upside Potential Ratio over a trailing window of `period` returns, measured
/// relative to a minimal acceptable return (`mar`).
///
/// ```text
/// upside = mean( max(r mar, 0) ) over the window
/// downside = sqrt( mean( min(r mar, 0)² ) ) over the window
/// UPR = upside / downside
/// ```
///
/// Where the [`SharpeRatio`](crate::SharpeRatio) divides excess return by *total*
/// volatility (penalising upside and downside symmetrically), the Upside Potential
/// Ratio rewards only the average outperformance above the threshold while
/// penalising solely the downside deviation below it. It is the purest expression
/// of the Sortino philosophy: investors do not dislike upside variance, only
/// shortfall risk.
///
/// `mar` (minimal acceptable return) is the per-period hurdle the caller supplies
/// (e.g. `0.0` for break-even, or a target rate matching the return frequency). A
/// window that never breaches the threshold has zero downside deviation; the
/// indicator then reports `0.0` rather than dividing by zero.
///
/// Each `update` is O(1) — running sums maintain the upside total and the
/// downside sum-of-squares as the window slides.
///
/// # Example
///
/// ```
/// use wickra_core::{Indicator, UpsidePotentialRatio};
///
/// let mut indicator = UpsidePotentialRatio::new(20, 0.0).unwrap();
/// let mut last = None;
/// for i in 0..40 {
/// last = indicator.update((f64::from(i) * 0.3).sin() * 0.02);
/// }
/// assert!(last.is_some());
/// ```
#[derive(Debug, Clone)]
pub struct UpsidePotentialRatio {
period: usize,
mar: f64,
window: VecDeque<f64>,
sum_upside: f64,
sum_downside_sq: f64,
}
impl UpsidePotentialRatio {
/// Construct an Upside Potential Ratio over `period` returns with minimal
/// acceptable return `mar`.
///
/// # Errors
///
/// Returns [`Error::InvalidPeriod`] if `period < 2`, or
/// [`Error::InvalidParameter`] if `mar` is not finite.
pub fn new(period: usize, mar: f64) -> Result<Self> {
if period < 2 {
return Err(Error::InvalidPeriod {
message: "upside potential ratio needs period >= 2",
});
}
if !mar.is_finite() {
return Err(Error::InvalidParameter {
message: "mar must be finite",
});
}
Ok(Self {
period,
mar,
window: VecDeque::with_capacity(period),
sum_upside: 0.0,
sum_downside_sq: 0.0,
})
}
/// Configured window of returns.
pub const fn period(&self) -> usize {
self.period
}
/// Configured minimal acceptable return.
pub const fn mar(&self) -> f64 {
self.mar
}
}
impl Indicator for UpsidePotentialRatio {
type Input = f64;
type Output = f64;
fn update(&mut self, ret: f64) -> Option<f64> {
if !ret.is_finite() {
return None;
}
if self.window.len() == self.period {
let old = self.window.pop_front().expect("non-empty");
let excess = old - self.mar;
self.sum_upside -= excess.max(0.0);
self.sum_downside_sq -= excess.min(0.0).powi(2);
}
let excess = ret - self.mar;
self.sum_upside += excess.max(0.0);
self.sum_downside_sq += excess.min(0.0).powi(2);
self.window.push_back(ret);
if self.window.len() < self.period {
return None;
}
let n = self.period as f64;
let upside_mean = self.sum_upside / n;
let downside_dev = (self.sum_downside_sq / n).sqrt();
if downside_dev > 0.0 {
Some(upside_mean / downside_dev)
} else {
Some(0.0)
}
}
fn reset(&mut self) {
self.window.clear();
self.sum_upside = 0.0;
self.sum_downside_sq = 0.0;
}
fn warmup_period(&self) -> usize {
self.period
}
fn is_ready(&self) -> bool {
self.window.len() == self.period
}
fn name(&self) -> &'static str {
"UpsidePotentialRatio"
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::traits::BatchExt;
use approx::assert_relative_eq;
#[test]
fn rejects_period_less_than_two() {
assert!(matches!(
UpsidePotentialRatio::new(1, 0.0),
Err(Error::InvalidPeriod { .. })
));
}
#[test]
fn rejects_non_finite_mar() {
assert!(matches!(
UpsidePotentialRatio::new(10, f64::NAN),
Err(Error::InvalidParameter { .. })
));
}
#[test]
fn accessors_and_metadata() {
let upr = UpsidePotentialRatio::new(20, 0.001).unwrap();
assert_eq!(upr.period(), 20);
assert_relative_eq!(upr.mar(), 0.001, epsilon = 1e-12);
assert_eq!(upr.warmup_period(), 20);
assert_eq!(upr.name(), "UpsidePotentialRatio");
}
#[test]
fn reference_value() {
// returns [0.02, -0.01, 0.03, -0.02], mar = 0.
// upside = (0.02 + 0 + 0.03 + 0)/4 = 0.0125.
// downside = sqrt((0 + 0.0001 + 0 + 0.0004)/4) = sqrt(0.000125).
// UPR = 0.0125 / sqrt(0.000125).
let mut upr = UpsidePotentialRatio::new(4, 0.0).unwrap();
let out = upr.batch(&[0.02, -0.01, 0.03, -0.02]);
let expected = 0.0125_f64 / (0.000_125_f64).sqrt();
assert_relative_eq!(out[3].unwrap(), expected, epsilon = 1e-9);
}
#[test]
fn no_downside_is_zero() {
let mut upr = UpsidePotentialRatio::new(3, 0.0).unwrap();
let last = upr
.batch(&[0.01, 0.02, 0.03])
.into_iter()
.flatten()
.last()
.unwrap();
assert_relative_eq!(last, 0.0, epsilon = 1e-12);
}
#[test]
fn ignores_non_finite_input() {
let mut upr = UpsidePotentialRatio::new(3, 0.0).unwrap();
assert_eq!(upr.update(0.01), None);
assert_eq!(upr.update(f64::INFINITY), None);
assert_eq!(upr.update(-0.02), None);
assert!(upr.update(0.03).is_some());
}
#[test]
fn reset_clears_state() {
let mut upr = UpsidePotentialRatio::new(2, 0.0).unwrap();
upr.batch(&[0.02, -0.01]);
assert!(upr.is_ready());
upr.reset();
assert!(!upr.is_ready());
assert_eq!(upr.update(0.01), None);
}
#[test]
fn batch_equals_streaming() {
let rets: Vec<f64> = (0..60)
.map(|i| (f64::from(i) * 0.25).sin() * 0.02)
.collect();
let batch = UpsidePotentialRatio::new(12, 0.0).unwrap().batch(&rets);
let mut streamer = UpsidePotentialRatio::new(12, 0.0).unwrap();
let streamed: Vec<_> = rets.iter().map(|r| streamer.update(*r)).collect();
assert_eq!(batch, streamed);
}
}
@@ -0,0 +1,217 @@
//! Volume bar builder — close a bar each time accumulated volume reaches a threshold.
use crate::error::{Error, Result};
use crate::ohlcv::Candle;
use crate::traits::BarBuilder;
/// One completed volume bar (an OHLCV aggregate spanning ~`volume_per_bar` of volume).
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct VolumeBar {
/// Open of the first candle in the bar.
pub open: f64,
/// Highest high across the bar.
pub high: f64,
/// Lowest low across the bar.
pub low: f64,
/// Close of the candle that closed the bar.
pub close: f64,
/// Accumulated volume in the bar (`>= volume_per_bar`; the crossing candle's
/// overshoot is kept in the bar that closes).
pub volume: f64,
}
/// Volume bar builder — emits a bar each time accumulated volume reaches
/// `volume_per_bar`.
///
/// Where [`TickBars`](crate::TickBars) sample on trade *count*, volume bars sample on
/// traded *quantity*: a bar closes once the candles fed into it have accumulated at
/// least `volume_per_bar` of volume. This gives each bar roughly equal participation,
/// which de-emphasises quiet periods and resolves bursts of heavy trading into more
/// bars. The companion [`DollarBars`](crate::DollarBars) builder uses traded *value*
/// (`price × volume`) instead, which is more robust to price-level drift over long
/// histories.
///
/// The bar is candle-granular: at most one bar closes per candle, and the candle
/// that crosses the threshold closes the bar with its overshoot included (the next
/// bar starts fresh). [`BarBuilder::update`] therefore returns either an empty vector
/// or a single [`VolumeBar`].
///
/// # Example
///
/// ```
/// use wickra_core::{BarBuilder, Candle, VolumeBars};
///
/// let c = |cl, v| Candle::new(cl, cl, cl, cl, v, 0).unwrap();
/// let mut bars = VolumeBars::new(100.0).unwrap();
/// assert!(bars.update(c(10.0, 60.0)).is_empty());
/// let out = bars.update(c(10.5, 60.0)); // 120 >= 100 -> close
/// assert_eq!(out.len(), 1);
/// assert_eq!(out[0].volume, 120.0);
/// ```
#[derive(Debug, Clone)]
pub struct VolumeBars {
volume_per_bar: f64,
count: usize,
open: f64,
high: f64,
low: f64,
close: f64,
accumulated: f64,
}
impl VolumeBars {
/// Construct a volume-bar builder with the given volume threshold.
///
/// # Errors
///
/// Returns [`Error::InvalidPeriod`] if `volume_per_bar` is not finite and positive.
pub fn new(volume_per_bar: f64) -> Result<Self> {
if !volume_per_bar.is_finite() || volume_per_bar <= 0.0 {
return Err(Error::InvalidPeriod {
message: "volume_per_bar must be finite and positive",
});
}
Ok(Self {
volume_per_bar,
count: 0,
open: 0.0,
high: 0.0,
low: 0.0,
close: 0.0,
accumulated: 0.0,
})
}
/// Configured volume threshold per bar.
pub const fn volume_per_bar(&self) -> f64 {
self.volume_per_bar
}
/// Volume accumulated into the in-progress bar.
pub const fn accumulated(&self) -> f64 {
self.accumulated
}
}
impl BarBuilder for VolumeBars {
type Bar = VolumeBar;
fn update(&mut self, candle: Candle) -> Vec<VolumeBar> {
if self.count == 0 {
self.open = candle.open;
self.high = candle.high;
self.low = candle.low;
} else {
self.high = self.high.max(candle.high);
self.low = self.low.min(candle.low);
}
self.close = candle.close;
self.accumulated += candle.volume;
self.count += 1;
if self.accumulated < self.volume_per_bar {
return Vec::new();
}
let bar = VolumeBar {
open: self.open,
high: self.high,
low: self.low,
close: self.close,
volume: self.accumulated,
};
self.count = 0;
self.accumulated = 0.0;
vec![bar]
}
fn reset(&mut self) {
self.count = 0;
self.accumulated = 0.0;
}
fn name(&self) -> &'static str {
"VolumeBars"
}
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_relative_eq;
fn candle(open: f64, high: f64, low: f64, close: f64, volume: f64) -> Candle {
Candle::new(open, high, low, close, volume, 0).unwrap()
}
#[test]
fn rejects_invalid_threshold() {
assert!(matches!(
VolumeBars::new(0.0),
Err(Error::InvalidPeriod { .. })
));
assert!(matches!(
VolumeBars::new(-100.0),
Err(Error::InvalidPeriod { .. })
));
assert!(matches!(
VolumeBars::new(f64::INFINITY),
Err(Error::InvalidPeriod { .. })
));
}
#[test]
fn accessors_and_metadata() {
let bars = VolumeBars::new(1000.0).unwrap();
assert_relative_eq!(bars.volume_per_bar(), 1000.0, epsilon = 1e-12);
assert_relative_eq!(bars.accumulated(), 0.0, epsilon = 1e-12);
assert_eq!(bars.name(), "VolumeBars");
}
#[test]
fn closes_when_threshold_reached() {
let mut bars = VolumeBars::new(100.0).unwrap();
assert!(bars.update(candle(10.0, 10.0, 10.0, 10.0, 60.0)).is_empty());
let out = bars.update(candle(10.5, 10.5, 10.5, 10.5, 60.0));
assert_eq!(out.len(), 1);
assert_relative_eq!(out[0].volume, 120.0, epsilon = 1e-12);
}
#[test]
fn aggregates_ohlc() {
let mut bars = VolumeBars::new(100.0).unwrap();
bars.update(candle(10.0, 11.0, 9.0, 10.5, 50.0));
let out = bars.update(candle(10.5, 12.0, 10.0, 11.0, 60.0));
assert_relative_eq!(out[0].open, 10.0, epsilon = 1e-12);
assert_relative_eq!(out[0].high, 12.0, epsilon = 1e-12);
assert_relative_eq!(out[0].low, 9.0, epsilon = 1e-12);
assert_relative_eq!(out[0].close, 11.0, epsilon = 1e-12);
}
#[test]
fn below_threshold_emits_nothing() {
let mut bars = VolumeBars::new(100.0).unwrap();
bars.update(candle(10.0, 10.0, 10.0, 10.0, 30.0));
assert_relative_eq!(bars.accumulated(), 30.0, epsilon = 1e-12);
}
#[test]
fn reset_clears_state() {
let mut bars = VolumeBars::new(100.0).unwrap();
bars.update(candle(10.0, 10.0, 10.0, 10.0, 60.0));
bars.reset();
assert_relative_eq!(bars.accumulated(), 0.0, epsilon = 1e-12);
assert!(bars.update(candle(20.0, 20.0, 20.0, 20.0, 60.0)).is_empty());
}
#[test]
fn batch_concatenates_completed_bars() {
let mut bars = VolumeBars::new(100.0).unwrap();
let candles = [
candle(10.0, 10.0, 10.0, 10.0, 60.0),
candle(10.0, 10.0, 10.0, 10.0, 60.0),
candle(10.0, 10.0, 10.0, 10.0, 60.0),
candle(10.0, 10.0, 10.0, 10.0, 60.0),
];
let out = bars.batch(&candles);
assert_eq!(out.len(), 2);
}
}
+74 -65
View File
@@ -55,6 +55,13 @@ pub mod indicators;
pub use cross_section::{CrossSection, Member};
pub use derivatives::DerivativesTick;
pub use error::{Error, Result};
pub use indicators::DollarBar;
pub use indicators::ImbalanceBar;
pub use indicators::LineBreakBar;
pub use indicators::RangeBar;
pub use indicators::RunBar;
pub use indicators::TickBar;
pub use indicators::VolumeBar;
pub use indicators::{
AbandonedBaby, Abcd, AbsoluteBreadthIndex, AccelerationBands, AccelerationBandsOutput,
AcceleratorOscillator, AdOscillator, AdVolumeLine, AdaptiveCci, AdaptiveCycle,
@@ -66,30 +73,31 @@ pub use indicators::{
AverageDrawdown, AvgPrice, AwesomeOscillator, AwesomeOscillatorHistogram, BalanceOfPower,
BandpassFilter, Bat, BeltHold, Beta, BetaNeutralSpread, BetterVolume, BipowerVariation,
BodySizePct, BollingerBands, BollingerBandwidth, BollingerOutput, BomarBands, BomarBandsOutput,
BreadthThrust, Breakaway, BullishPercentIndex, Butterfly, CalendarSpread, CalmarRatio,
Camarilla, CamarillaPivotsOutput, CandleVolume, CandleVolumeOutput, Cci, CenterOfGravity,
CentralPivotRange, CentralPivotRangeOutput, Cfo, ChaikinMoneyFlow, ChaikinOscillator,
ChaikinVolatility, ChandeKrollStop, ChandeKrollStopOutput, ChandelierExit,
BreadthThrust, Breakaway, BullishPercentIndex, BurkeRatio, Butterfly, CalendarSpread,
CalmarRatio, Camarilla, CamarillaPivotsOutput, CandleVolume, CandleVolumeOutput, Cci,
CenterOfGravity, CentralPivotRange, CentralPivotRangeOutput, Cfo, ChaikinMoneyFlow,
ChaikinOscillator, ChaikinVolatility, ChandeKrollStop, ChandeKrollStopOutput, ChandelierExit,
ChandelierExitOutput, ChoppinessIndex, ClassicPivots, ClassicPivotsOutput, CloseVsOpen,
ClosingMarubozu, Cmo, CoefficientOfVariation, Cointegration, CointegrationOutput,
CompositeProfile, CompositeProfileOutput, ConcealingBabySwallow, ConditionalValueAtRisk,
ConnorsRsi, Coppock, CorrelationTrendIndicator, Counterattack, Crab, CumulativeVolumeDelta,
CumulativeVolumeIndex, CupAndHandle, CyberneticCycle, Cypher, DayOfWeekProfile,
DayOfWeekProfileOutput, Decycler, DecyclerOscillator, Dema, DemandIndex, DemarkPivots,
DemarkPivotsOutput, DepthSlope, DerivativeOscillator, DetrendedStdDev, DisparityIndex,
DistanceSsd, Doji, DojiStar, Donchian, DonchianOutput, DonchianStop, DonchianStopOutput,
DoubleBollinger, DoubleBollingerOutput, DoubleTopBottom, DownsideGapThreeMethods, Dpo,
DragonflyDoji, DrawdownDuration, DumplingTop, Dx, DynamicMomentumIndex, EaseOfMovement,
EffectiveSpread, EhlersStochastic, Ehma, ElderImpulse, ElderRay, ElderRayOutput, ElderSafeZone,
ElderSafeZoneOutput, Ema, EmpiricalModeDecomposition, Engulfing, Equivolume, EquivolumeOutput,
EstimatedLeverageRatio, EvenBetterSinewave, EveningDojiStar, Evwma, EwmaVolatility, Expectancy,
FallingThreeMethods, Fama, FibArcs, FibArcsOutput, FibChannel, FibChannelOutput, FibConfluence,
FibConfluenceOutput, FibExtension, FibExtensionOutput, FibFan, FibFanOutput, FibProjection,
FibProjectionOutput, FibRetracement, FibRetracementOutput, FibTimeZones, FibTimeZonesOutput,
FibonacciPivots, FibonacciPivotsOutput, FisherRsi, FisherTransform, FlagPennant, Footprint,
FootprintOutput, ForceIndex, FractalChaosBands, FractalChaosBandsOutput, Frama, FryPanBottom,
FundingBasis, FundingImpliedApr, FundingRate, FundingRateMean, FundingRateZScore,
GainLossRatio, GapSideBySideWhite, Garch11, GarmanKlassVolatility, Gartley, GatorOscillator,
CommonSenseRatio, CompositeProfile, CompositeProfileOutput, ConcealingBabySwallow,
ConditionalValueAtRisk, ConnorsRsi, Coppock, CorrelationTrendIndicator, Counterattack, Crab,
CumulativeVolumeDelta, CumulativeVolumeIndex, CupAndHandle, CyberneticCycle, Cypher,
DayOfWeekProfile, DayOfWeekProfileOutput, Decycler, DecyclerOscillator, Dema, DemandIndex,
DemarkPivots, DemarkPivotsOutput, DepthSlope, DerivativeOscillator, DetrendedStdDev,
DisparityIndex, DistanceSsd, Doji, DojiStar, DollarBars, Donchian, DonchianOutput,
DonchianStop, DonchianStopOutput, DoubleBollinger, DoubleBollingerOutput, DoubleTopBottom,
DownsideGapThreeMethods, Dpo, DragonflyDoji, DrawdownDuration, DumplingTop, Dx,
DynamicMomentumIndex, EaseOfMovement, EffectiveSpread, EhlersStochastic, Ehma, ElderImpulse,
ElderRay, ElderRayOutput, ElderSafeZone, ElderSafeZoneOutput, Ema, EmpiricalModeDecomposition,
Engulfing, Equivolume, EquivolumeOutput, EstimatedLeverageRatio, EvenBetterSinewave,
EveningDojiStar, Evwma, EwmaVolatility, Expectancy, FallingThreeMethods, Fama, FibArcs,
FibArcsOutput, FibChannel, FibChannelOutput, FibConfluence, FibConfluenceOutput, FibExtension,
FibExtensionOutput, FibFan, FibFanOutput, FibProjection, FibProjectionOutput, FibRetracement,
FibRetracementOutput, FibTimeZones, FibTimeZonesOutput, FibonacciPivots, FibonacciPivotsOutput,
FisherRsi, FisherTransform, FlagPennant, Footprint, FootprintOutput, ForceIndex,
FractalChaosBands, FractalChaosBandsOutput, Frama, FryPanBottom, FundingBasis,
FundingImpliedApr, FundingRate, FundingRateMean, FundingRateZScore, GainLossRatio,
GainToPainRatio, GapSideBySideWhite, Garch11, GarmanKlassVolatility, Gartley, GatorOscillator,
GatorOscillatorOutput, GeneralizedDema, GeometricMa, GoldenPocket, GoldenPocketOutput,
GrangerCausality, GravestoneDoji, Hammer, HangingMan, Harami, HaramiCross,
HasbrouckInformationShare, HeadAndShoulders, HeikinAshi, HeikinAshiOscillator,
@@ -97,25 +105,25 @@ pub use indicators::{
HighLowVolumeNodesOutput, HighWave, HighpassFilter, Hikkake, HikkakeModified,
HilbertDominantCycle, HistoricalVolatility, Hma, HoltWinters, HomingPigeon, HtDcPhase,
HtPhasor, HtPhasorOutput, HtTrendMode, HurstChannel, HurstChannelOutput, HurstExponent,
Ichimoku, IchimokuOutput, IdenticalThreeCrows, InNeck, Inertia, InformationRatio,
InitialBalance, InitialBalanceOutput, InstantaneousTrendline, IntradayIntensity,
IntradayMomentumIndex, IntradayVolatilityProfile, IntradayVolatilityProfileOutput,
InverseFisherTransform, InvertedHammer, JarqueBera, Jma, JumpIndicator, KagiBars,
KalmanHedgeRatio, KalmanHedgeRatioOutput, Kama, KaseDevStop, KaseDevStopOutput,
KasePermissionStochastic, KasePermissionStochasticOutput, KellyCriterion, Keltner,
KeltnerOutput, KendallTau, Kicking, KickingByLength, Kst, KstOutput, Kurtosis, Kvo,
Ichimoku, IchimokuOutput, IdenticalThreeCrows, ImbalanceBars, InNeck, Inertia,
InformationRatio, InitialBalance, InitialBalanceOutput, InstantaneousTrendline,
IntradayIntensity, IntradayMomentumIndex, IntradayVolatilityProfile,
IntradayVolatilityProfileOutput, InverseFisherTransform, InvertedHammer, JarqueBera, Jma,
JumpIndicator, KRatio, KagiBars, KalmanHedgeRatio, KalmanHedgeRatioOutput, Kama, KaseDevStop,
KaseDevStopOutput, KasePermissionStochastic, KasePermissionStochasticOutput, KellyCriterion,
Keltner, KeltnerOutput, KendallTau, Kicking, KickingByLength, Kst, KstOutput, Kurtosis, Kvo,
KylesLambda, LadderBottom, LaguerreRsi, LeadLagCrossCorrelation, LeadLagCrossCorrelationOutput,
LinRegAngle, LinRegChannel, LinRegChannelOutput, LinRegIntercept, LinRegSlope,
LinearRegression, LiquidationFeatures, LiquidationFeaturesOutput, LogReturn, LongLeggedDoji,
LongLine, LongShortRatio, MaEnvelope, MaEnvelopeOutput, MacdExt, MacdFix, MacdHistogram,
MacdIndicator, MacdOutput, Mama, MamaOutput, MarketFacilitationIndex, Marubozu, MassIndex,
MatHold, MatchingLow, MaxDrawdown, McClellanOscillator, McClellanSummationIndex,
McGinleyDynamic, MedianAbsoluteDeviation, MedianChannel, MedianChannelOutput, MedianMa,
MedianPrice, Mfi, Microprice, MidPoint, MidPrice, MinusDi, MinusDm, ModifiedMaStop,
ModifiedMaStopOutput, Mom, MorningDojiStar, MorningEveningStar, MurreyMathLines,
MurreyMathLinesOutput, NakedPoc, Natr, NewHighsNewLows, NewPriceLines, Nrtr, NrtrOutput, Nvi,
OIPriceDivergence, OIWeighted, Obv, OiToVolumeRatio, OmegaRatio, OnNeck, OpenInterestDelta,
OpenInterestMomentum, OpeningMarubozu, OpeningRange, OpeningRangeOutput,
LongLine, LongShortRatio, M2Measure, MaEnvelope, MaEnvelopeOutput, MacdExt, MacdFix,
MacdHistogram, MacdIndicator, MacdOutput, Mama, MamaOutput, MarketFacilitationIndex,
MartinRatio, Marubozu, MassIndex, MatHold, MatchingLow, MaxDrawdown, McClellanOscillator,
McClellanSummationIndex, McGinleyDynamic, MedianAbsoluteDeviation, MedianChannel,
MedianChannelOutput, MedianMa, MedianPrice, Mfi, Microprice, MidPoint, MidPrice, MinusDi,
MinusDm, ModifiedMaStop, ModifiedMaStopOutput, Mom, MorningDojiStar, MorningEveningStar,
MurreyMathLines, MurreyMathLinesOutput, NakedPoc, Natr, NewHighsNewLows, NewPriceLines, Nrtr,
NrtrOutput, Nvi, OIPriceDivergence, OIWeighted, Obv, OiToVolumeRatio, OmegaRatio, OnNeck,
OpenInterestDelta, OpenInterestMomentum, OpeningMarubozu, OpeningRange, OpeningRangeOutput,
OrderBookImbalanceFull, OrderBookImbalanceTop1, OrderBookImbalanceTopN, OrderFlowImbalance,
OuHalfLife, OvernightGap, OvernightIntradayReturn, OvernightIntradayReturnOutput, PainIndex,
PairSpreadZScore, PairwiseBeta, ParkinsonVolatility, PearsonCorrelation, PercentAboveMa,
@@ -123,37 +131,38 @@ pub use indicators::{
PivotReversal, PlusDi, PlusDm, Pmo, PointAndFigureBars, PolarizedFractalEfficiency, Ppo,
PpoHistogram, ProfileShape, ProfitFactor, ProjectionBands, ProjectionBandsOutput,
ProjectionOscillator, Psar, Pvi, Qqe, QqeOutput, Qstick, QuartileBands, QuartileBandsOutput,
QuotedSpread, RSquared, RealizedSpread, RealizedVolatility, RecoveryFactor, RectangleRange,
Reflex, RegimeLabel, RelativeStrengthAB, RelativeStrengthOutput, RenkoBars, RenkoTrailingStop,
RickshawMan, RisingThreeMethods, Rmi, Roc, Rocp, Rocr, Rocr100, RogersSatchellVolatility,
RollMeasure, RollingCorrelation, RollingCovariance, RollingIqr, RollingMinMaxScaler,
RollingPercentileRank, RollingQuantile, RollingVwap, RoofingFilter, Rsi, Rsx, Rvi,
RviVolatility, Rwi, RwiOutput, SampleEntropy, SarExt, SeasonalZScore, SeparatingLines,
SessionHighLow, SessionHighLowOutput, SessionRange, SessionRangeOutput, SessionVwap,
ShannonEntropy, Shark, SharpeRatio, ShootingStar, ShortLine, SignedVolume, SineWave,
SineWeightedMa, SinglePrints, Skewness, Sma, Smi, Smma, SmoothedHeikinAshi,
QuotedSpread, RSquared, RangeBars, RealizedSpread, RealizedVolatility, RecoveryFactor,
RectangleRange, Reflex, RegimeLabel, RelativeStrengthAB, RelativeStrengthOutput, RenkoBars,
RenkoTrailingStop, RickshawMan, RisingThreeMethods, Rmi, Roc, Rocp, Rocr, Rocr100,
RogersSatchellVolatility, RollMeasure, RollingCorrelation, RollingCovariance, RollingIqr,
RollingMinMaxScaler, RollingPercentileRank, RollingQuantile, RollingVwap, RoofingFilter, Rsi,
Rsx, RunBars, Rvi, RviVolatility, Rwi, RwiOutput, SampleEntropy, SarExt, SeasonalZScore,
SeparatingLines, SessionHighLow, SessionHighLowOutput, SessionRange, SessionRangeOutput,
SessionVwap, ShannonEntropy, Shark, SharpeRatio, ShootingStar, ShortLine, SignedVolume,
SineWave, SineWeightedMa, SinglePrints, Skewness, Sma, Smi, Smma, SmoothedHeikinAshi,
SmoothedHeikinAshiOutput, SortinoRatio, SpearmanCorrelation, SpinningTop, SpreadAr1Coefficient,
SpreadBollingerBands, SpreadBollingerBandsOutput, SpreadHurst, StalledPattern, StandardError,
StandardErrorBands, StandardErrorBandsOutput, StarcBands, StarcBandsOutput, Stc, StdDev,
StepTrailingStop, StickSandwich, StochRsi, Stochastic, StochasticCci, StochasticOutput,
SuperSmoother, SuperTrend, SuperTrendOutput, TakerBuySellRatio, Takuri, TasukiGap,
TdCamouflage, TdClop, TdClopwin, TdCombo, TdCountdown, TdDWave, TdDeMarker, TdDifferential,
TdLines, TdLinesOutput, TdMovingAverage, TdMovingAverageOutput, TdOpen, TdPressure,
TdPropulsion, TdRangeProjection, TdRangeProjectionOutput, TdRei, TdRiskLevel,
StepTrailingStop, SterlingRatio, StickSandwich, StochRsi, Stochastic, StochasticCci,
StochasticOutput, SuperSmoother, SuperTrend, SuperTrendOutput, TailRatio, TakerBuySellRatio,
Takuri, TasukiGap, TdCamouflage, TdClop, TdClopwin, TdCombo, TdCountdown, TdDWave, TdDeMarker,
TdDifferential, TdLines, TdLinesOutput, TdMovingAverage, TdMovingAverageOutput, TdOpen,
TdPressure, TdPropulsion, TdRangeProjection, TdRangeProjectionOutput, TdRei, TdRiskLevel,
TdRiskLevelOutput, TdSequential, TdSequentialOutput, TdSetup, TdTrap, Tema, TermStructureBasis,
ThreeDrives, ThreeInside, ThreeLineBreak, ThreeLineStrike, ThreeOutside, ThreeSoldiersOrCrows,
ThreeStarsInSouth, Thrusting, TickIndex, Tii, TimeBasedStop, TimeOfDayReturnProfile,
TimeOfDayReturnProfileOutput, TowerTopBottom, TpoProfile, TpoProfileOutput, TradeImbalance,
TradeSignAutocorrelation, TradeVolumeIndex, TrendLabel, TrendStrengthIndex, Trendflex,
TreynorRatio, Triangle, Trima, Trin, TripleTopBottom, Tristar, Trix, TrueRange, Tsf,
TsfOscillator, Tsi, Tsv, TtmSqueeze, TtmSqueezeOutput, TtmTrend, TurnOfMonth, Tweezer,
TwiggsMoneyFlow, TwoCrows, TypicalPrice, UlcerIndex, UltimateOscillator, UniqueThreeRiver,
UniversalOscillator, UpDownVolumeRatio, UpsideGapThreeMethods, UpsideGapTwoCrows, ValueArea,
ValueAreaOutput, ValueAtRisk, Variance, VarianceRatio, VerticalHorizontalFilter, Vidya,
VolatilityCone, VolatilityConeOutput, VolatilityOfVolatility, VolatilityRatio, VoltyStop,
VolumeByTimeProfile, VolumeByTimeProfileOutput, VolumeOscillator, VolumePriceTrend,
VolumeProfile, VolumeProfileOutput, VolumeRsi, VolumeWeightedMacd, VolumeWeightedMacdOutput,
VolumeWeightedSr, VolumeWeightedSrOutput, Vortex, VortexOutput, Vpin, Vwap, VwapStdDevBands,
ThreeDrives, ThreeInside, ThreeLineBreak, ThreeLineBreakBars, ThreeLineStrike, ThreeOutside,
ThreeSoldiersOrCrows, ThreeStarsInSouth, Thrusting, TickBars, TickIndex, Tii, TimeBasedStop,
TimeOfDayReturnProfile, TimeOfDayReturnProfileOutput, TowerTopBottom, TpoProfile,
TpoProfileOutput, TradeImbalance, TradeSignAutocorrelation, TradeVolumeIndex, TrendLabel,
TrendStrengthIndex, Trendflex, TreynorRatio, Triangle, Trima, Trin, TripleTopBottom, Tristar,
Trix, TrueRange, Tsf, TsfOscillator, Tsi, Tsv, TtmSqueeze, TtmSqueezeOutput, TtmTrend,
TurnOfMonth, Tweezer, TwiggsMoneyFlow, TwoCrows, TypicalPrice, UlcerIndex, UltimateOscillator,
UniqueThreeRiver, UniversalOscillator, UpDownVolumeRatio, UpsideGapThreeMethods,
UpsideGapTwoCrows, UpsidePotentialRatio, ValueArea, ValueAreaOutput, ValueAtRisk, Variance,
VarianceRatio, VerticalHorizontalFilter, Vidya, VolatilityCone, VolatilityConeOutput,
VolatilityOfVolatility, VolatilityRatio, VoltyStop, VolumeBars, VolumeByTimeProfile,
VolumeByTimeProfileOutput, VolumeOscillator, VolumePriceTrend, VolumeProfile,
VolumeProfileOutput, VolumeRsi, VolumeWeightedMacd, VolumeWeightedMacdOutput, VolumeWeightedSr,
VolumeWeightedSrOutput, Vortex, VortexOutput, Vpin, Vwap, VwapStdDevBands,
VwapStdDevBandsOutput, Vwma, Vzo, Wad, WavePm, WaveTrend, WaveTrendOutput, Wedge,
WeightedClose, WickRatio, WilliamsFractals, WilliamsFractalsOutput, WilliamsR, WinRate, Wma,
WoodiePivots, WoodiePivotsOutput, YangZhangVolatility, YoyoExit, ZScore, ZeroLagMacd,
+1 -1
View File
@@ -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 **498 indicators** across
- A per-indicator deep dive for every one of the **514 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 &
+7 -7
View File
@@ -17,7 +17,7 @@
},
"../../bindings/node": {
"name": "wickra",
"version": "0.7.2",
"version": "0.7.4",
"license": "MIT OR Apache-2.0",
"devDependencies": {
"@napi-rs/cli": "^2.18.0"
@@ -26,12 +26,12 @@
"node": ">= 18"
},
"optionalDependencies": {
"wickra-darwin-arm64": "0.7.2",
"wickra-darwin-x64": "0.7.2",
"wickra-linux-arm64-gnu": "0.7.2",
"wickra-linux-x64-gnu": "0.7.2",
"wickra-win32-arm64-msvc": "0.7.2",
"wickra-win32-x64-msvc": "0.7.2"
"wickra-darwin-arm64": "0.7.4",
"wickra-darwin-x64": "0.7.4",
"wickra-linux-arm64-gnu": "0.7.4",
"wickra-linux-x64-gnu": "0.7.4",
"wickra-win32-arm64-msvc": "0.7.4",
"wickra-win32-x64-msvc": "0.7.4"
}
},
"node_modules/wickra": {
@@ -11,7 +11,7 @@
//! and zero ranges within that band.
use libfuzzer_sys::fuzz_target;
use wickra_core::{BarBuilder, Candle, KagiBars, PointAndFigureBars, RenkoBars};
use wickra_core::{BarBuilder, Candle, DollarBars, ImbalanceBars, KagiBars, PointAndFigureBars, RangeBars, RenkoBars, RunBars, ThreeLineBreakBars, TickBars, VolumeBars};
/// Reinterpret the fuzz bytes as `[open, high, low, close, volume]` groups,
/// keeping only structurally-valid candles whose magnitudes stay in a band that
@@ -40,6 +40,13 @@ fuzz_target!(|data: Vec<f64>| {
drive(RenkoBars::new(5.0).unwrap(), &candles);
drive(KagiBars::new(5.0).unwrap(), &candles);
drive(PointAndFigureBars::new(5.0, 3).unwrap(), &candles);
drive(RangeBars::new(2.0).unwrap(), &candles);
drive(TickBars::new(5).unwrap(), &candles);
drive(VolumeBars::new(100.0).unwrap(), &candles);
drive(DollarBars::new(10000.0).unwrap(), &candles);
drive(ImbalanceBars::new(5.0).unwrap(), &candles);
drive(RunBars::new(3).unwrap(), &candles);
drive(ThreeLineBreakBars::new(3).unwrap(), &candles);
let _ = RenkoBars::new(5.0).unwrap().batch(&candles);
let _ = KagiBars::new(5.0).unwrap().batch(&candles);
+10 -1
View File
@@ -14,7 +14,7 @@
//! `Ema(20)`. This target now covers every scalar indicator in the catalogue.
use libfuzzer_sys::fuzz_target;
use wickra_core::{AdaptiveCycle, AdaptiveLaguerreFilter, AdaptiveRsi, Alma, AnchoredRsi, Apo, Autocorrelation, AutocorrelationPeriodogram, AverageDrawdown, BandpassFilter, BatchExt, Beta, BipowerVariation, BollingerBands, BomarBands, CalmarRatio, CenterOfGravity, Cfo, Cmo, CoefficientOfVariation, ConditionalValueAtRisk, ConnorsRsi, Coppock, CorrelationTrendIndicator, CyberneticCycle, Decycler, DecyclerOscillator, Dema, DerivativeOscillator, DetrendedStdDev, DisparityIndex, DoubleBollinger, Dpo, DrawdownDuration, DynamicMomentumIndex, EhlersStochastic, Ehma, ElderImpulse, Ema, EmpiricalModeDecomposition, EvenBetterSinewave, EwmaVolatility, Expectancy, Fama, FisherRsi, FisherTransform, Frama, GainLossRatio, Garch11, GeneralizedDema, GeometricMa, HighpassFilter, HilbertDominantCycle, HistoricalVolatility, Hma, HoltWinters, HtDcPhase, HtPhasor, HtTrendMode, HurstExponent, Indicator, InstantaneousTrendline, InverseFisherTransform, JarqueBera, Jma, JumpIndicator, Kama, KellyCriterion, Kst, Kurtosis, LaguerreRsi, LinRegAngle, LinRegChannel, LinRegIntercept, LinRegSlope, LinearRegression, LogReturn, MaEnvelope, MaType, MacdExt, MacdFix, MacdHistogram, MacdIndicator, Mama, MaxDrawdown, McGinleyDynamic, MedianAbsoluteDeviation, MedianChannel, MedianMa, MidPoint, Mom, OmegaRatio, PainIndex, PearsonCorrelation, PercentageTrailingStop, Pmo, PolarizedFractalEfficiency, Ppo, PpoHistogram, ProfitFactor, Qqe, QuartileBands, RSquared, RealizedVolatility, RecoveryFactor, Reflex, RegimeLabel, RenkoTrailingStop, Rmi, Roc, Rocp, Rocr, Rocr100, RollingIqr, RollingMinMaxScaler, RollingPercentileRank, RollingQuantile, RoofingFilter, Rsi, Rsx, RviVolatility, SampleEntropy, ShannonEntropy, SharpeRatio, SineWave, SineWeightedMa, Skewness, Sma, Smma, SortinoRatio, SpearmanCorrelation, StandardError, StandardErrorBands, Stc, StdDev, StepTrailingStop, StochRsi, SuperSmoother, Tema, Tii, TrendLabel, TrendStrengthIndex, Trendflex, Trima, Trix, Tsf, TsfOscillator, Tsi, UlcerIndex, UniversalOscillator, ValueAtRisk, Variance, VerticalHorizontalFilter, Vidya, VolatilityOfVolatility, WavePm, WinRate, Wma, ZScore, ZeroLagMacd, Zlema, T3};
use wickra_core::{AdaptiveCycle, AdaptiveLaguerreFilter, AdaptiveRsi, Alma, AnchoredRsi, Apo, Autocorrelation, AutocorrelationPeriodogram, AverageDrawdown, BandpassFilter, BatchExt, Beta, BipowerVariation, BollingerBands, BomarBands, BurkeRatio, CalmarRatio, CenterOfGravity, Cfo, Cmo, CoefficientOfVariation, CommonSenseRatio, ConditionalValueAtRisk, ConnorsRsi, Coppock, CorrelationTrendIndicator, CyberneticCycle, Decycler, DecyclerOscillator, Dema, DerivativeOscillator, DetrendedStdDev, DisparityIndex, DoubleBollinger, Dpo, DrawdownDuration, DynamicMomentumIndex, EhlersStochastic, Ehma, ElderImpulse, Ema, EmpiricalModeDecomposition, EvenBetterSinewave, EwmaVolatility, Expectancy, Fama, FisherRsi, FisherTransform, Frama, GainLossRatio, GainToPainRatio, Garch11, GeneralizedDema, GeometricMa, HighpassFilter, HilbertDominantCycle, HistoricalVolatility, Hma, HoltWinters, HtDcPhase, HtPhasor, HtTrendMode, HurstExponent, Indicator, InstantaneousTrendline, InverseFisherTransform, JarqueBera, Jma, JumpIndicator, KRatio, Kama, KellyCriterion, Kst, Kurtosis, LaguerreRsi, LinRegAngle, LinRegChannel, LinRegIntercept, LinRegSlope, LinearRegression, LogReturn, M2Measure, MaEnvelope, MaType, MacdExt, MacdFix, MacdHistogram, MacdIndicator, Mama, MartinRatio, MaxDrawdown, McGinleyDynamic, MedianAbsoluteDeviation, MedianChannel, MedianMa, MidPoint, Mom, OmegaRatio, PainIndex, PearsonCorrelation, PercentageTrailingStop, Pmo, PolarizedFractalEfficiency, Ppo, PpoHistogram, ProfitFactor, Qqe, QuartileBands, RSquared, RealizedVolatility, RecoveryFactor, Reflex, RegimeLabel, RenkoTrailingStop, Rmi, Roc, Rocp, Rocr, Rocr100, RollingIqr, RollingMinMaxScaler, RollingPercentileRank, RollingQuantile, RoofingFilter, Rsi, Rsx, RviVolatility, SampleEntropy, ShannonEntropy, SharpeRatio, SineWave, SineWeightedMa, Skewness, Sma, Smma, SortinoRatio, SpearmanCorrelation, StandardError, StandardErrorBands, Stc, StdDev, StepTrailingStop, SterlingRatio, StochRsi, SuperSmoother, TailRatio, Tema, Tii, TrendLabel, TrendStrengthIndex, Trendflex, Trima, Trix, Tsf, TsfOscillator, Tsi, UlcerIndex, UniversalOscillator, UpsidePotentialRatio, ValueAtRisk, Variance, VerticalHorizontalFilter, Vidya, VolatilityOfVolatility, WavePm, WinRate, Wma, ZScore, ZeroLagMacd, Zlema, T3};
/// Drive a single streaming + batch run through one scalar indicator. Marked
/// `#[inline(never)]` so a panic backtrace pin-points the specific indicator.
@@ -211,6 +211,15 @@ fuzz_target!(|data: Vec<f64>| {
drive(|| KellyCriterion::new(20).unwrap(), &data);
drive(|| WinRate::new(20).unwrap(), &data);
drive(|| Expectancy::new(20).unwrap(), &data);
drive(|| SterlingRatio::new(12).unwrap(), &data);
drive(|| BurkeRatio::new(12).unwrap(), &data);
drive(|| MartinRatio::new(14).unwrap(), &data);
drive(|| TailRatio::new(20).unwrap(), &data);
drive(|| KRatio::new(30).unwrap(), &data);
drive(|| CommonSenseRatio::new(20).unwrap(), &data);
drive(|| GainToPainRatio::new(12).unwrap(), &data);
drive(|| UpsidePotentialRatio::new(20, 0.0).unwrap(), &data);
drive(|| M2Measure::new(20, 0.0, 0.02).unwrap(), &data);
// RecoveryFactor and DrawdownDuration produce non-`f64` outputs / have
// no `period` knob, so they cannot use the `drive` helper directly.