feat: add 7 alt-chart bar builders (B19) (#220)
Adds seven information-driven bar builders to the **Alt-Chart Bars** family, the final batch of the family-deepening run. Indicator count **507 → 514**. ## Builders All implement the `BarBuilder` trait (`update(Candle) -> Vec<Bar>`), emitting a data-dependent number of completed bars per candle. | Builder | Driver | Bar fields | |---------|--------|-----------| | `RangeBars` | close | open, close, direction | | `TickBars` | OHLCV | open, high, low, close, volume | | `VolumeBars` | OHLCV | open, high, low, close, volume | | `DollarBars` (Lopez de Prado) | OHLCV | + dollar | | `ImbalanceBars` | OHLC | + imbalance, direction | | `RunBars` | OHLC | + length, direction | | `ThreeLineBreakBars` | close | open, close, direction | ## Touchpoints Seven core modules (each with full unit tests), `mod.rs`/`lib.rs` (builders counted, bar element types on their own re-export lines), README family rows, Python/Node/WASM hand-written bindings for the variable-length output (Python tuples + `(k, N)` ndarray; Node `Vec<object>`; WASM array of objects), the `bar_builder_update_candle` fuzz target, dedicated Python + Node tests, the `BAR_BUILDERS` completeness exclusion, and CHANGELOG. ## Verification - `cargo test -p wickra-core --lib` — 4207 passed - `cargo test -p wickra-core --doc` — 464 passed - `cargo clippy --workspace --all-targets --all-features -- -D warnings` — clean - `npm test` (node) — 584 passed - `pytest` (python) — 957 passed
This commit is contained in:
@@ -6,6 +6,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [Unreleased]
|
||||
- **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`).
|
||||
|
||||
@@ -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=507" 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 507 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.** 507 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 507 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** | **507** | **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
|
||||
|
||||
507 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 507 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
|
||||
|
||||
@@ -1769,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
+104
@@ -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
|
||||
@@ -5028,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)
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -17818,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,
|
||||
|
||||
@@ -370,6 +370,13 @@ from ._wickra import (
|
||||
InitialBalance,
|
||||
OpeningRange,
|
||||
# Alt-Chart Bars
|
||||
ThreeLineBreakBars,
|
||||
RunBars,
|
||||
ImbalanceBars,
|
||||
DollarBars,
|
||||
VolumeBars,
|
||||
TickBars,
|
||||
RangeBars,
|
||||
RenkoBars,
|
||||
KagiBars,
|
||||
PointAndFigureBars,
|
||||
@@ -907,6 +914,13 @@ __all__ = [
|
||||
"InitialBalance",
|
||||
"OpeningRange",
|
||||
# Alt-Chart Bars
|
||||
"ThreeLineBreakBars",
|
||||
"RunBars",
|
||||
"ImbalanceBars",
|
||||
"DollarBars",
|
||||
"VolumeBars",
|
||||
"TickBars",
|
||||
"RangeBars",
|
||||
"RenkoBars",
|
||||
"KagiBars",
|
||||
"PointAndFigureBars",
|
||||
|
||||
@@ -38,6 +38,67 @@ fn map_err(e: wc::Error) -> PyErr {
|
||||
/// Raised instead of panicking when a `NumPy` input is not C-contiguous.
|
||||
const NON_CONTIGUOUS: &str = "array must be C-contiguous; pass np.ascontiguousarray(arr)";
|
||||
|
||||
/// Borrowed `(open, high, low, close)` columns for candle-driven bar builders.
|
||||
type OhlcCols<'a> = (&'a [f64], &'a [f64], &'a [f64], &'a [f64]);
|
||||
/// Borrowed `(open, high, low, close, volume)` columns for candle-driven bar builders.
|
||||
type OhlcvCols<'a> = (&'a [f64], &'a [f64], &'a [f64], &'a [f64], &'a [f64]);
|
||||
|
||||
/// `(open, high, low, close, volume)` rows from Tick/Volume bar builders.
|
||||
type OhlcvBarRows = Vec<(f64, f64, f64, f64, f64)>;
|
||||
/// `(open, high, low, close, volume, dollar)` rows from the Dollar bar builder.
|
||||
type DollarBarRows = Vec<(f64, f64, f64, f64, f64, f64)>;
|
||||
/// `(open, high, low, close, imbalance, direction)` rows from the Imbalance bar builder.
|
||||
type ImbalanceBarRows = Vec<(f64, f64, f64, f64, f64, i64)>;
|
||||
/// `(open, high, low, close, length, direction)` rows from the Run bar builder.
|
||||
type RunBarRows = Vec<(f64, f64, f64, f64, i64, i64)>;
|
||||
|
||||
/// Extract four equal-length OHLC slices, erroring on non-contiguous or mismatched input.
|
||||
fn ohlc_slices<'a, 'py>(
|
||||
open: &'a PyReadonlyArray1<'py, f64>,
|
||||
high: &'a PyReadonlyArray1<'py, f64>,
|
||||
low: &'a PyReadonlyArray1<'py, f64>,
|
||||
close: &'a PyReadonlyArray1<'py, f64>,
|
||||
) -> PyResult<OhlcCols<'a>> {
|
||||
let o = open
|
||||
.as_slice()
|
||||
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||
let h = high
|
||||
.as_slice()
|
||||
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||
let l = low
|
||||
.as_slice()
|
||||
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||
let c = close
|
||||
.as_slice()
|
||||
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||
if o.len() != h.len() || h.len() != l.len() || l.len() != c.len() {
|
||||
return Err(PyValueError::new_err(
|
||||
"open, high, low, close must be equal length",
|
||||
));
|
||||
}
|
||||
Ok((o, h, l, c))
|
||||
}
|
||||
|
||||
/// Extract five equal-length OHLCV slices, erroring on non-contiguous or mismatched input.
|
||||
fn ohlcv_slices<'a, 'py>(
|
||||
open: &'a PyReadonlyArray1<'py, f64>,
|
||||
high: &'a PyReadonlyArray1<'py, f64>,
|
||||
low: &'a PyReadonlyArray1<'py, f64>,
|
||||
close: &'a PyReadonlyArray1<'py, f64>,
|
||||
volume: &'a PyReadonlyArray1<'py, f64>,
|
||||
) -> PyResult<OhlcvCols<'a>> {
|
||||
let (o, h, l, c) = ohlc_slices(open, high, low, close)?;
|
||||
let v = volume
|
||||
.as_slice()
|
||||
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||
if v.len() != o.len() {
|
||||
return Err(PyValueError::new_err(
|
||||
"open, high, low, close, volume must be equal length",
|
||||
));
|
||||
}
|
||||
Ok((o, h, l, c, v))
|
||||
}
|
||||
|
||||
/// `(pp, r1, r2, r3, s1, s2, s3)` pivot levels returned by Classic/Fibonacci pivots.
|
||||
type PivotLevels = (f64, f64, f64, f64, f64, f64, f64);
|
||||
/// The five Fibonacci-extension levels returned by `FibExtension`.
|
||||
@@ -23459,6 +23520,486 @@ impl PyPointAndFigureBars {
|
||||
}
|
||||
}
|
||||
|
||||
#[pyclass(name = "RangeBars", module = "wickra._wickra", skip_from_py_object)]
|
||||
#[derive(Clone)]
|
||||
struct PyRangeBars {
|
||||
inner: wc::RangeBars,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyRangeBars {
|
||||
#[new]
|
||||
fn new(range: f64) -> PyResult<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::RangeBars::new(range).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
/// Feed one close; returns bars completed on it as `(open, close, direction)`.
|
||||
fn update(&mut self, close: f64) -> PyResult<Vec<(f64, f64, i64)>> {
|
||||
let candle = wc::Candle::new(close, close, close, close, 1.0, 0).map_err(map_err)?;
|
||||
Ok(self
|
||||
.inner
|
||||
.update(candle)
|
||||
.into_iter()
|
||||
.map(|b| (b.open, b.close, i64::from(b.direction)))
|
||||
.collect())
|
||||
}
|
||||
/// Batch over a close column. Returns shape `(k, 3)` of `[open, close, direction]`.
|
||||
fn batch<'py>(
|
||||
&mut self,
|
||||
py: Python<'py>,
|
||||
close: PyReadonlyArray1<'py, f64>,
|
||||
) -> PyResult<Bound<'py, PyArray2<f64>>> {
|
||||
let prices = close
|
||||
.as_slice()
|
||||
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||
let mut rows: Vec<f64> = Vec::new();
|
||||
let mut k = 0usize;
|
||||
for &price in prices {
|
||||
let candle = wc::Candle::new(price, price, price, price, 1.0, 0).map_err(map_err)?;
|
||||
for b in self.inner.update(candle) {
|
||||
rows.push(b.open);
|
||||
rows.push(b.close);
|
||||
rows.push(f64::from(b.direction));
|
||||
k += 1;
|
||||
}
|
||||
}
|
||||
Ok(numpy::ndarray::Array2::from_shape_vec((k, 3), rows)
|
||||
.expect("shape consistent")
|
||||
.into_pyarray(py))
|
||||
}
|
||||
#[getter]
|
||||
fn range(&self) -> f64 {
|
||||
self.inner.range()
|
||||
}
|
||||
fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
fn __repr__(&self) -> String {
|
||||
format!("RangeBars(range={})", self.inner.range())
|
||||
}
|
||||
}
|
||||
|
||||
#[pyclass(name = "TickBars", module = "wickra._wickra", skip_from_py_object)]
|
||||
#[derive(Clone)]
|
||||
struct PyTickBars {
|
||||
inner: wc::TickBars,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyTickBars {
|
||||
#[new]
|
||||
fn new(ticks: usize) -> PyResult<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::TickBars::new(ticks).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
/// Feed one candle; returns bars completed as `(open, high, low, close, volume)`.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn update(
|
||||
&mut self,
|
||||
open: f64,
|
||||
high: f64,
|
||||
low: f64,
|
||||
close: f64,
|
||||
volume: f64,
|
||||
) -> PyResult<OhlcvBarRows> {
|
||||
let candle = wc::Candle::new(open, high, low, close, volume, 0).map_err(map_err)?;
|
||||
Ok(self
|
||||
.inner
|
||||
.update(candle)
|
||||
.into_iter()
|
||||
.map(|b| (b.open, b.high, b.low, b.close, b.volume))
|
||||
.collect())
|
||||
}
|
||||
/// Batch over OHLCV columns. Returns shape `(k, 5)` of `[open, high, low, close, volume]`.
|
||||
fn batch<'py>(
|
||||
&mut self,
|
||||
py: Python<'py>,
|
||||
open: PyReadonlyArray1<'py, f64>,
|
||||
high: PyReadonlyArray1<'py, f64>,
|
||||
low: PyReadonlyArray1<'py, f64>,
|
||||
close: PyReadonlyArray1<'py, f64>,
|
||||
volume: PyReadonlyArray1<'py, f64>,
|
||||
) -> PyResult<Bound<'py, PyArray2<f64>>> {
|
||||
let (o, h, l, c, v) = ohlcv_slices(&open, &high, &low, &close, &volume)?;
|
||||
let mut rows: Vec<f64> = Vec::new();
|
||||
let mut k = 0usize;
|
||||
for i in 0..o.len() {
|
||||
let candle = wc::Candle::new(o[i], h[i], l[i], c[i], v[i], 0).map_err(map_err)?;
|
||||
for b in self.inner.update(candle) {
|
||||
rows.extend_from_slice(&[b.open, b.high, b.low, b.close, b.volume]);
|
||||
k += 1;
|
||||
}
|
||||
}
|
||||
Ok(numpy::ndarray::Array2::from_shape_vec((k, 5), rows)
|
||||
.expect("shape consistent")
|
||||
.into_pyarray(py))
|
||||
}
|
||||
#[getter]
|
||||
fn ticks(&self) -> usize {
|
||||
self.inner.ticks()
|
||||
}
|
||||
fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
fn __repr__(&self) -> String {
|
||||
format!("TickBars(ticks={})", self.inner.ticks())
|
||||
}
|
||||
}
|
||||
|
||||
#[pyclass(name = "VolumeBars", module = "wickra._wickra", skip_from_py_object)]
|
||||
#[derive(Clone)]
|
||||
struct PyVolumeBars {
|
||||
inner: wc::VolumeBars,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyVolumeBars {
|
||||
#[new]
|
||||
fn new(volume_per_bar: f64) -> PyResult<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::VolumeBars::new(volume_per_bar).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
/// Feed one candle; returns bars completed as `(open, high, low, close, volume)`.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn update(
|
||||
&mut self,
|
||||
open: f64,
|
||||
high: f64,
|
||||
low: f64,
|
||||
close: f64,
|
||||
volume: f64,
|
||||
) -> PyResult<OhlcvBarRows> {
|
||||
let candle = wc::Candle::new(open, high, low, close, volume, 0).map_err(map_err)?;
|
||||
Ok(self
|
||||
.inner
|
||||
.update(candle)
|
||||
.into_iter()
|
||||
.map(|b| (b.open, b.high, b.low, b.close, b.volume))
|
||||
.collect())
|
||||
}
|
||||
/// Batch over OHLCV columns. Returns shape `(k, 5)` of `[open, high, low, close, volume]`.
|
||||
fn batch<'py>(
|
||||
&mut self,
|
||||
py: Python<'py>,
|
||||
open: PyReadonlyArray1<'py, f64>,
|
||||
high: PyReadonlyArray1<'py, f64>,
|
||||
low: PyReadonlyArray1<'py, f64>,
|
||||
close: PyReadonlyArray1<'py, f64>,
|
||||
volume: PyReadonlyArray1<'py, f64>,
|
||||
) -> PyResult<Bound<'py, PyArray2<f64>>> {
|
||||
let (o, h, l, c, v) = ohlcv_slices(&open, &high, &low, &close, &volume)?;
|
||||
let mut rows: Vec<f64> = Vec::new();
|
||||
let mut k = 0usize;
|
||||
for i in 0..o.len() {
|
||||
let candle = wc::Candle::new(o[i], h[i], l[i], c[i], v[i], 0).map_err(map_err)?;
|
||||
for b in self.inner.update(candle) {
|
||||
rows.extend_from_slice(&[b.open, b.high, b.low, b.close, b.volume]);
|
||||
k += 1;
|
||||
}
|
||||
}
|
||||
Ok(numpy::ndarray::Array2::from_shape_vec((k, 5), rows)
|
||||
.expect("shape consistent")
|
||||
.into_pyarray(py))
|
||||
}
|
||||
#[getter]
|
||||
fn volume_per_bar(&self) -> f64 {
|
||||
self.inner.volume_per_bar()
|
||||
}
|
||||
fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
fn __repr__(&self) -> String {
|
||||
format!("VolumeBars(volume_per_bar={})", self.inner.volume_per_bar())
|
||||
}
|
||||
}
|
||||
|
||||
#[pyclass(name = "DollarBars", module = "wickra._wickra", skip_from_py_object)]
|
||||
#[derive(Clone)]
|
||||
struct PyDollarBars {
|
||||
inner: wc::DollarBars,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyDollarBars {
|
||||
#[new]
|
||||
fn new(dollar_per_bar: f64) -> PyResult<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::DollarBars::new(dollar_per_bar).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
/// Feed one candle; returns bars completed as `(open, high, low, close, volume, dollar)`.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn update(
|
||||
&mut self,
|
||||
open: f64,
|
||||
high: f64,
|
||||
low: f64,
|
||||
close: f64,
|
||||
volume: f64,
|
||||
) -> PyResult<DollarBarRows> {
|
||||
let candle = wc::Candle::new(open, high, low, close, volume, 0).map_err(map_err)?;
|
||||
Ok(self
|
||||
.inner
|
||||
.update(candle)
|
||||
.into_iter()
|
||||
.map(|b| (b.open, b.high, b.low, b.close, b.volume, b.dollar))
|
||||
.collect())
|
||||
}
|
||||
/// Batch over OHLCV columns. Returns shape `(k, 6)` of `[open, high, low, close, volume, dollar]`.
|
||||
fn batch<'py>(
|
||||
&mut self,
|
||||
py: Python<'py>,
|
||||
open: PyReadonlyArray1<'py, f64>,
|
||||
high: PyReadonlyArray1<'py, f64>,
|
||||
low: PyReadonlyArray1<'py, f64>,
|
||||
close: PyReadonlyArray1<'py, f64>,
|
||||
volume: PyReadonlyArray1<'py, f64>,
|
||||
) -> PyResult<Bound<'py, PyArray2<f64>>> {
|
||||
let (o, h, l, c, v) = ohlcv_slices(&open, &high, &low, &close, &volume)?;
|
||||
let mut rows: Vec<f64> = Vec::new();
|
||||
let mut k = 0usize;
|
||||
for i in 0..o.len() {
|
||||
let candle = wc::Candle::new(o[i], h[i], l[i], c[i], v[i], 0).map_err(map_err)?;
|
||||
for b in self.inner.update(candle) {
|
||||
rows.extend_from_slice(&[b.open, b.high, b.low, b.close, b.volume, b.dollar]);
|
||||
k += 1;
|
||||
}
|
||||
}
|
||||
Ok(numpy::ndarray::Array2::from_shape_vec((k, 6), rows)
|
||||
.expect("shape consistent")
|
||||
.into_pyarray(py))
|
||||
}
|
||||
#[getter]
|
||||
fn dollar_per_bar(&self) -> f64 {
|
||||
self.inner.dollar_per_bar()
|
||||
}
|
||||
fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
fn __repr__(&self) -> String {
|
||||
format!("DollarBars(dollar_per_bar={})", self.inner.dollar_per_bar())
|
||||
}
|
||||
}
|
||||
|
||||
#[pyclass(name = "ImbalanceBars", module = "wickra._wickra", skip_from_py_object)]
|
||||
#[derive(Clone)]
|
||||
struct PyImbalanceBars {
|
||||
inner: wc::ImbalanceBars,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyImbalanceBars {
|
||||
#[new]
|
||||
fn new(threshold: f64) -> PyResult<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::ImbalanceBars::new(threshold).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
/// Feed one candle; returns bars completed as `(open, high, low, close, imbalance, direction)`.
|
||||
fn update(&mut self, open: f64, high: f64, low: f64, close: f64) -> PyResult<ImbalanceBarRows> {
|
||||
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| {
|
||||
(
|
||||
b.open,
|
||||
b.high,
|
||||
b.low,
|
||||
b.close,
|
||||
b.imbalance,
|
||||
i64::from(b.direction),
|
||||
)
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
/// Batch over OHLC columns. Returns shape `(k, 6)` of `[open, high, low, close, imbalance, direction]`.
|
||||
fn batch<'py>(
|
||||
&mut self,
|
||||
py: Python<'py>,
|
||||
open: PyReadonlyArray1<'py, f64>,
|
||||
high: PyReadonlyArray1<'py, f64>,
|
||||
low: PyReadonlyArray1<'py, f64>,
|
||||
close: PyReadonlyArray1<'py, f64>,
|
||||
) -> PyResult<Bound<'py, PyArray2<f64>>> {
|
||||
let (o, h, l, c) = ohlc_slices(&open, &high, &low, &close)?;
|
||||
let mut rows: Vec<f64> = Vec::new();
|
||||
let mut k = 0usize;
|
||||
for i in 0..o.len() {
|
||||
let candle = wc::Candle::new(o[i], h[i], l[i], c[i], 1.0, 0).map_err(map_err)?;
|
||||
for b in self.inner.update(candle) {
|
||||
rows.extend_from_slice(&[
|
||||
b.open,
|
||||
b.high,
|
||||
b.low,
|
||||
b.close,
|
||||
b.imbalance,
|
||||
f64::from(b.direction),
|
||||
]);
|
||||
k += 1;
|
||||
}
|
||||
}
|
||||
Ok(numpy::ndarray::Array2::from_shape_vec((k, 6), rows)
|
||||
.expect("shape consistent")
|
||||
.into_pyarray(py))
|
||||
}
|
||||
#[getter]
|
||||
fn threshold(&self) -> f64 {
|
||||
self.inner.threshold()
|
||||
}
|
||||
fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
fn __repr__(&self) -> String {
|
||||
format!("ImbalanceBars(threshold={})", self.inner.threshold())
|
||||
}
|
||||
}
|
||||
|
||||
#[pyclass(name = "RunBars", module = "wickra._wickra", skip_from_py_object)]
|
||||
#[derive(Clone)]
|
||||
struct PyRunBars {
|
||||
inner: wc::RunBars,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyRunBars {
|
||||
#[new]
|
||||
fn new(run_length: usize) -> PyResult<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::RunBars::new(run_length).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
/// Feed one candle; returns bars completed as `(open, high, low, close, length, direction)`.
|
||||
fn update(&mut self, open: f64, high: f64, low: f64, close: f64) -> PyResult<RunBarRows> {
|
||||
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| {
|
||||
(
|
||||
b.open,
|
||||
b.high,
|
||||
b.low,
|
||||
b.close,
|
||||
i64::try_from(b.length).unwrap_or(i64::MAX),
|
||||
i64::from(b.direction),
|
||||
)
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
/// Batch over OHLC columns. Returns shape `(k, 6)` of `[open, high, low, close, length, direction]`.
|
||||
fn batch<'py>(
|
||||
&mut self,
|
||||
py: Python<'py>,
|
||||
open: PyReadonlyArray1<'py, f64>,
|
||||
high: PyReadonlyArray1<'py, f64>,
|
||||
low: PyReadonlyArray1<'py, f64>,
|
||||
close: PyReadonlyArray1<'py, f64>,
|
||||
) -> PyResult<Bound<'py, PyArray2<f64>>> {
|
||||
let (o, h, l, c) = ohlc_slices(&open, &high, &low, &close)?;
|
||||
let mut rows: Vec<f64> = Vec::new();
|
||||
let mut k = 0usize;
|
||||
for i in 0..o.len() {
|
||||
let candle = wc::Candle::new(o[i], h[i], l[i], c[i], 1.0, 0).map_err(map_err)?;
|
||||
for b in self.inner.update(candle) {
|
||||
#[allow(clippy::cast_precision_loss)]
|
||||
rows.extend_from_slice(&[
|
||||
b.open,
|
||||
b.high,
|
||||
b.low,
|
||||
b.close,
|
||||
b.length as f64,
|
||||
f64::from(b.direction),
|
||||
]);
|
||||
k += 1;
|
||||
}
|
||||
}
|
||||
Ok(numpy::ndarray::Array2::from_shape_vec((k, 6), rows)
|
||||
.expect("shape consistent")
|
||||
.into_pyarray(py))
|
||||
}
|
||||
#[getter]
|
||||
fn run_length(&self) -> usize {
|
||||
self.inner.run_length()
|
||||
}
|
||||
fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
fn __repr__(&self) -> String {
|
||||
format!("RunBars(run_length={})", self.inner.run_length())
|
||||
}
|
||||
}
|
||||
|
||||
#[pyclass(
|
||||
name = "ThreeLineBreakBars",
|
||||
module = "wickra._wickra",
|
||||
skip_from_py_object
|
||||
)]
|
||||
#[derive(Clone)]
|
||||
struct PyThreeLineBreakBars {
|
||||
inner: wc::ThreeLineBreakBars,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyThreeLineBreakBars {
|
||||
#[new]
|
||||
#[pyo3(signature = (lines=3))]
|
||||
fn new(lines: usize) -> PyResult<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::ThreeLineBreakBars::new(lines).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
/// Feed one close; returns bars completed on it as `(open, close, direction)`.
|
||||
fn update(&mut self, close: f64) -> PyResult<Vec<(f64, f64, i64)>> {
|
||||
let candle = wc::Candle::new(close, close, close, close, 1.0, 0).map_err(map_err)?;
|
||||
Ok(self
|
||||
.inner
|
||||
.update(candle)
|
||||
.into_iter()
|
||||
.map(|b| (b.open, b.close, i64::from(b.direction)))
|
||||
.collect())
|
||||
}
|
||||
/// Batch over a close column. Returns shape `(k, 3)` of `[open, close, direction]`.
|
||||
fn batch<'py>(
|
||||
&mut self,
|
||||
py: Python<'py>,
|
||||
close: PyReadonlyArray1<'py, f64>,
|
||||
) -> PyResult<Bound<'py, PyArray2<f64>>> {
|
||||
let prices = close
|
||||
.as_slice()
|
||||
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||
let mut rows: Vec<f64> = Vec::new();
|
||||
let mut k = 0usize;
|
||||
for &price in prices {
|
||||
let candle = wc::Candle::new(price, price, price, price, 1.0, 0).map_err(map_err)?;
|
||||
for b in self.inner.update(candle) {
|
||||
rows.push(b.open);
|
||||
rows.push(b.close);
|
||||
rows.push(f64::from(b.direction));
|
||||
k += 1;
|
||||
}
|
||||
}
|
||||
Ok(numpy::ndarray::Array2::from_shape_vec((k, 3), rows)
|
||||
.expect("shape consistent")
|
||||
.into_pyarray(py))
|
||||
}
|
||||
#[getter]
|
||||
fn lines(&self) -> usize {
|
||||
self.inner.lines()
|
||||
}
|
||||
fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
fn __repr__(&self) -> String {
|
||||
format!("ThreeLineBreakBars(lines={})", self.inner.lines())
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== Module ==============================
|
||||
|
||||
// ====================== Seasonality & Session (full-candle) ======================
|
||||
@@ -26074,6 +26615,13 @@ fn _wickra(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||
m.add_class::<PyRenkoBars>()?;
|
||||
m.add_class::<PyKagiBars>()?;
|
||||
m.add_class::<PyPointAndFigureBars>()?;
|
||||
m.add_class::<PyRangeBars>()?;
|
||||
m.add_class::<PyTickBars>()?;
|
||||
m.add_class::<PyVolumeBars>()?;
|
||||
m.add_class::<PyDollarBars>()?;
|
||||
m.add_class::<PyImbalanceBars>()?;
|
||||
m.add_class::<PyRunBars>()?;
|
||||
m.add_class::<PyThreeLineBreakBars>()?;
|
||||
m.add_class::<PyInitialBalance>()?;
|
||||
m.add_class::<PyOpeningRange>()?;
|
||||
m.add_class::<PyNakedPoc>()?;
|
||||
|
||||
@@ -4237,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
|
||||
|
||||
@@ -13163,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,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,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));
|
||||
}
|
||||
}
|
||||
@@ -112,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;
|
||||
@@ -204,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;
|
||||
@@ -331,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;
|
||||
@@ -358,6 +361,7 @@ mod rolling_quantile;
|
||||
mod roofing_filter;
|
||||
mod rsi;
|
||||
mod rsx;
|
||||
mod run_bars;
|
||||
mod rvi;
|
||||
mod rvi_volatility;
|
||||
mod rwi;
|
||||
@@ -431,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;
|
||||
@@ -485,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;
|
||||
@@ -619,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};
|
||||
@@ -711,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;
|
||||
@@ -838,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;
|
||||
@@ -865,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};
|
||||
@@ -938,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;
|
||||
@@ -992,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};
|
||||
@@ -1573,7 +1587,18 @@ pub const FAMILIES: &[(&str, &[&str])] = &[
|
||||
),
|
||||
(
|
||||
"Alt-Chart Bars",
|
||||
&["RenkoBars", "KagiBars", "PointAndFigureBars"],
|
||||
&[
|
||||
"RenkoBars",
|
||||
"KagiBars",
|
||||
"PointAndFigureBars",
|
||||
"RangeBars",
|
||||
"TickBars",
|
||||
"VolumeBars",
|
||||
"DollarBars",
|
||||
"ImbalanceBars",
|
||||
"RunBars",
|
||||
"ThreeLineBreakBars",
|
||||
],
|
||||
),
|
||||
(
|
||||
"Market Breadth",
|
||||
@@ -1681,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, 507, "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,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,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,
|
||||
@@ -77,8 +84,8 @@ pub use indicators::{
|
||||
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,
|
||||
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,
|
||||
@@ -98,13 +105,13 @@ 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, KRatio, 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,
|
||||
@@ -124,15 +131,15 @@ 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,
|
||||
@@ -142,23 +149,24 @@ pub use indicators::{
|
||||
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,
|
||||
UpsidePotentialRatio, 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, VwapStdDevBandsOutput, Vwma, Vzo, Wad, WavePm, WaveTrend,
|
||||
WaveTrendOutput, Wedge, WeightedClose, WickRatio, WilliamsFractals, WilliamsFractalsOutput,
|
||||
WilliamsR, WinRate, Wma, WoodiePivots, WoodiePivotsOutput, YangZhangVolatility, YoyoExit,
|
||||
ZScore, ZeroLagMacd, ZeroLagMacdOutput, ZigZag, ZigZagOutput, Zlema, FAMILIES, T3,
|
||||
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,
|
||||
ZeroLagMacdOutput, ZigZag, ZigZagOutput, Zlema, FAMILIES, T3,
|
||||
};
|
||||
// `FootprintLevel` is a row element of `FootprintOutput`, re-exported on its own
|
||||
// line so the indicator-count tooling (which scans the braced block above and
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@ That includes:
|
||||
[Python](https://docs.wickra.org/Quickstart-Python),
|
||||
[Node](https://docs.wickra.org/Quickstart-Node), and
|
||||
[WASM](https://docs.wickra.org/Quickstart-WASM).
|
||||
- A per-indicator deep dive for every one of the **507 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 &
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user