Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 13c8250488 | |||
| e5305ffa94 | |||
| 46be7a54ea | |||
| bca61322b5 |
+23
-1
@@ -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
@@ -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
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
[](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 **11–56×**
|
||||
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 |
|
||||
|------------------|-------------|-------------|-----------------------------|-----------:|--------|
|
||||
| **★ Wickra**| **clean** | **yes, O(1)** | **Python · Node · WASM · Rust** | **498** | **yes** |
|
||||
| **★ 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)
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
Vendored
+185
@@ -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
File diff suppressed because one or more lines are too long
@@ -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,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,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": [
|
||||
|
||||
Generated
+20
-20
@@ -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"
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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² / Modigliani–Modigliani measure — Sharpe expressed in benchmark return units.
|
||||
|
||||
use std::collections::VecDeque;
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
use crate::traits::Indicator;
|
||||
|
||||
/// M² (Modigliani–Modigliani) 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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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 &
|
||||
|
||||
Generated
+7
-7
@@ -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);
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user