feat(indicators): A5b Fibonacci tools (geometric) (#172)
Completes the **Fibonacci** family with the four geometric/time tools (catalogue 373 -> 377). All extend the internal `pattern_swing` ZigZag tracker with a per-pivot bar index and a current-bar counter (additive — the chart/harmonic detectors are unaffected), and emit `Candle -> struct` outputs via custom Python/Node/WASM bindings. | Tool | Output | |------|--------| | `FibFan` | three trendlines fanning from a swing start through its 38.2/50/61.8% retracement levels, extended to the current bar | | `FibArcs` | semicircular retracement levels centred on the swing end, normalised by the leg's bar-width (chart-scale-free) | | `FibChannel` | a sloped base trendline plus parallel lines at Fibonacci multiples of the channel width | | `FibTimeZones` | markers at Fibonacci bar-distances (1/2/3/5/8/...) from the latest swing pivot | The geometric tools are novel as streaming indicators; each normalises its geometry to the swing leg's bar-width so the output is chart-scale-free. Formulas are documented in each module and deep-dive. Fully wired: core (100% unit-tested branches incl. the new `pattern_swing` bar tracking), Python/Node/WASM struct bindings, fuzz, reference + streaming-vs-batch tests. Verification: `cargo test --workspace` green, clippy `-D warnings` clean, node 454 tests, python 768 tests.
This commit is contained in:
@@ -6,6 +6,10 @@ 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]
|
||||
- **Fibonacci Time Zones** — vertical markers at Fibonacci bar-distances (1/2/3/5/8/...) from the latest swing pivot (`FIB_TIME_ZONES`).
|
||||
- **Fibonacci Channel** — a sloped base trendline plus parallel lines at Fibonacci multiples of the channel width (`FIB_CHANNEL`).
|
||||
- **Fibonacci Arcs** — semicircular retracement levels centred on the swing end, normalised by leg bar-width (`FIB_ARCS`).
|
||||
- **Fibonacci Fan** — three trendlines fanning from a swing start through its 38.2/50/61.8% retracement levels (`FIB_FAN`).
|
||||
- **Fibonacci Confluence** — densest cluster of retracement levels across recent swing legs (price + strength) (`FIB_CONFLUENCE`).
|
||||
- **Golden Pocket** — the 0.618-0.65 optimal-trade-entry band of the most recent swing leg (`GOLDEN_POCKET`).
|
||||
- **Auto-Fibonacci** — retracement anchored on the dominant (largest-magnitude) leg among recent swings (`AUTO_FIB`).
|
||||
|
||||
@@ -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=373" 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=377" 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 373 indicators; start at the
|
||||
every one of the 377 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),
|
||||
@@ -136,7 +136,7 @@ python -m benchmarks.compare_libraries
|
||||
|
||||
## Indicators
|
||||
|
||||
373 streaming-first indicators across twenty-four families. Every one passes the
|
||||
377 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).
|
||||
@@ -160,7 +160,7 @@ warmup) at [docs.wickra.org](https://docs.wickra.org/Indicators-Overview).
|
||||
| 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 |
|
||||
| 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 |
|
||||
| Fibonacci | Fibonacci Retracement, Fibonacci Extension, Fibonacci Projection, Auto-Fibonacci, Golden Pocket, Fibonacci Confluence |
|
||||
| Fibonacci | Fibonacci Retracement, Fibonacci Extension, Fibonacci Projection, Auto-Fibonacci, Golden Pocket, Fibonacci Confluence, Fibonacci Fan, Fibonacci Arcs, Fibonacci Channel, Fibonacci Time Zones |
|
||||
| Microstructure | Order-Book Imbalance (Top-1 / Top-N / Full), Microprice, Quoted Spread, Depth Slope, Signed Volume, Cumulative Volume Delta, Trade Imbalance, Effective Spread, Realized Spread, Kyle's Lambda, Footprint |
|
||||
| Derivatives | Funding Rate, Funding Rate Mean, Funding Rate Z-Score, Funding Basis, Open-Interest Delta, OI / Price Divergence, OI-Weighted Price, Long/Short Ratio, Taker Buy/Sell Ratio, Liquidation Features, Term-Structure Basis, Calendar Spread |
|
||||
| Market Profile | Value Area (POC / VAH / VAL), Volume Profile (histogram), TPO Profile, Initial Balance, Opening Range |
|
||||
@@ -245,7 +245,7 @@ A Python live-trading example using the public `websockets` package lives at
|
||||
```
|
||||
wickra/
|
||||
├── crates/
|
||||
│ ├── wickra-core/ core engine + all 373 indicators
|
||||
│ ├── wickra-core/ core engine + all 377 indicators
|
||||
│ ├── wickra/ top-level facade crate (publishes on crates.io) + benches/
|
||||
│ └── wickra-data/ CSV reader, tick aggregator, live exchange feeds
|
||||
├── bindings/
|
||||
|
||||
@@ -391,6 +391,10 @@ const multi = {
|
||||
AutoFib: { make: () => new wickra.AutoFib(), fields: ['level0', 'level236', 'level382', 'level500', 'level618', 'level786', 'level1000'], step: (ind, i) => ind.update(high[i], low[i]), batch: (ind) => ind.batch(high, low) },
|
||||
GoldenPocket: { make: () => new wickra.GoldenPocket(), fields: ['low', 'mid', 'high'], step: (ind, i) => ind.update(high[i], low[i]), batch: (ind) => ind.batch(high, low) },
|
||||
FibConfluence: { make: () => new wickra.FibConfluence(), fields: ['price', 'strength'], step: (ind, i) => ind.update(high[i], low[i]), batch: (ind) => ind.batch(high, low) },
|
||||
FibFan: { make: () => new wickra.FibFan(), fields: ['fan382', 'fan500', 'fan618'], step: (ind, i) => ind.update(high[i], low[i]), batch: (ind) => ind.batch(high, low) },
|
||||
FibArcs: { make: () => new wickra.FibArcs(), fields: ['arc382', 'arc500', 'arc618'], step: (ind, i) => ind.update(high[i], low[i]), batch: (ind) => ind.batch(high, low) },
|
||||
FibChannel: { make: () => new wickra.FibChannel(), fields: ['base', 'level618', 'level1000', 'level1618'], step: (ind, i) => ind.update(high[i], low[i]), batch: (ind) => ind.batch(high, low) },
|
||||
FibTimeZones: { make: () => new wickra.FibTimeZones(), fields: ['onZone', 'barsToNext'], step: (ind, i) => ind.update(high[i], low[i]), batch: (ind) => ind.batch(high, low) },
|
||||
};
|
||||
|
||||
for (const [name, d] of Object.entries(multi)) {
|
||||
|
||||
Vendored
+56
@@ -402,6 +402,26 @@ export interface FibConfluenceValue {
|
||||
price: number
|
||||
strength: number
|
||||
}
|
||||
export interface FibFanValue {
|
||||
fan382: number
|
||||
fan500: number
|
||||
fan618: number
|
||||
}
|
||||
export interface FibArcsValue {
|
||||
arc382: number
|
||||
arc500: number
|
||||
arc618: number
|
||||
}
|
||||
export interface FibChannelValue {
|
||||
base: number
|
||||
level618: number
|
||||
level1000: number
|
||||
level1618: number
|
||||
}
|
||||
export interface FibTimeZonesValue {
|
||||
onZone: number
|
||||
barsToNext: number
|
||||
}
|
||||
export type SmaNode = SMA
|
||||
export declare class SMA {
|
||||
constructor(period: number)
|
||||
@@ -3926,3 +3946,39 @@ export declare class FibConfluence {
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type FibFanNode = FibFan
|
||||
export declare class FibFan {
|
||||
constructor()
|
||||
update(high: number, low: number): FibFanValue | null
|
||||
batch(high: Array<number>, low: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type FibArcsNode = FibArcs
|
||||
export declare class FibArcs {
|
||||
constructor()
|
||||
update(high: number, low: number): FibArcsValue | null
|
||||
batch(high: Array<number>, low: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type FibChannelNode = FibChannel
|
||||
export declare class FibChannel {
|
||||
constructor()
|
||||
update(high: number, low: number): FibChannelValue | null
|
||||
batch(high: Array<number>, low: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type FibTimeZonesNode = FibTimeZones
|
||||
export declare class FibTimeZones {
|
||||
constructor()
|
||||
update(high: number, low: number): FibTimeZonesValue | null
|
||||
batch(high: Array<number>, low: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -14482,3 +14482,279 @@ impl Default for FibConfluenceNode {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[napi(object)]
|
||||
pub struct FibFanValue {
|
||||
pub fan_382: f64,
|
||||
pub fan_500: f64,
|
||||
pub fan_618: f64,
|
||||
}
|
||||
|
||||
#[napi(js_name = "FibFan")]
|
||||
pub struct FibFanNode {
|
||||
inner: wc::FibFan,
|
||||
}
|
||||
|
||||
#[napi]
|
||||
impl FibFanNode {
|
||||
#[napi(constructor)]
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
inner: wc::FibFan::new(),
|
||||
}
|
||||
}
|
||||
#[napi]
|
||||
pub fn update(&mut self, high: f64, low: f64) -> napi::Result<Option<FibFanValue>> {
|
||||
Ok(self
|
||||
.inner
|
||||
.update(swing_cnd(high, low)?)
|
||||
.map(|o| FibFanValue {
|
||||
fan_382: o.fan_382,
|
||||
fan_500: o.fan_500,
|
||||
fan_618: o.fan_618,
|
||||
}))
|
||||
}
|
||||
#[napi]
|
||||
pub fn batch(&mut self, high: Vec<f64>, low: Vec<f64>) -> napi::Result<Vec<f64>> {
|
||||
if high.len() != low.len() {
|
||||
return Err(NapiError::from_reason(
|
||||
"high and low must be equal length".to_string(),
|
||||
));
|
||||
}
|
||||
let n = high.len();
|
||||
let mut out = vec![f64::NAN; n * 3];
|
||||
for i in 0..n {
|
||||
if let Some(o) = self.inner.update(swing_cnd(high[i], low[i])?) {
|
||||
out[i * 3] = o.fan_382;
|
||||
out[i * 3 + 1] = o.fan_500;
|
||||
out[i * 3 + 2] = o.fan_618;
|
||||
}
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
#[napi]
|
||||
pub fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
#[napi(js_name = "isReady")]
|
||||
pub fn is_ready(&self) -> bool {
|
||||
self.inner.is_ready()
|
||||
}
|
||||
#[napi(js_name = "warmupPeriod")]
|
||||
pub fn warmup_period(&self) -> u32 {
|
||||
self.inner.warmup_period() as u32
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for FibFanNode {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[napi(object)]
|
||||
pub struct FibArcsValue {
|
||||
pub arc_382: f64,
|
||||
pub arc_500: f64,
|
||||
pub arc_618: f64,
|
||||
}
|
||||
|
||||
#[napi(js_name = "FibArcs")]
|
||||
pub struct FibArcsNode {
|
||||
inner: wc::FibArcs,
|
||||
}
|
||||
|
||||
#[napi]
|
||||
impl FibArcsNode {
|
||||
#[napi(constructor)]
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
inner: wc::FibArcs::new(),
|
||||
}
|
||||
}
|
||||
#[napi]
|
||||
pub fn update(&mut self, high: f64, low: f64) -> napi::Result<Option<FibArcsValue>> {
|
||||
Ok(self
|
||||
.inner
|
||||
.update(swing_cnd(high, low)?)
|
||||
.map(|o| FibArcsValue {
|
||||
arc_382: o.arc_382,
|
||||
arc_500: o.arc_500,
|
||||
arc_618: o.arc_618,
|
||||
}))
|
||||
}
|
||||
#[napi]
|
||||
pub fn batch(&mut self, high: Vec<f64>, low: Vec<f64>) -> napi::Result<Vec<f64>> {
|
||||
if high.len() != low.len() {
|
||||
return Err(NapiError::from_reason(
|
||||
"high and low must be equal length".to_string(),
|
||||
));
|
||||
}
|
||||
let n = high.len();
|
||||
let mut out = vec![f64::NAN; n * 3];
|
||||
for i in 0..n {
|
||||
if let Some(o) = self.inner.update(swing_cnd(high[i], low[i])?) {
|
||||
out[i * 3] = o.arc_382;
|
||||
out[i * 3 + 1] = o.arc_500;
|
||||
out[i * 3 + 2] = o.arc_618;
|
||||
}
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
#[napi]
|
||||
pub fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
#[napi(js_name = "isReady")]
|
||||
pub fn is_ready(&self) -> bool {
|
||||
self.inner.is_ready()
|
||||
}
|
||||
#[napi(js_name = "warmupPeriod")]
|
||||
pub fn warmup_period(&self) -> u32 {
|
||||
self.inner.warmup_period() as u32
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for FibArcsNode {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[napi(object)]
|
||||
pub struct FibChannelValue {
|
||||
pub base: f64,
|
||||
pub level_618: f64,
|
||||
pub level_1000: f64,
|
||||
pub level_1618: f64,
|
||||
}
|
||||
|
||||
#[napi(js_name = "FibChannel")]
|
||||
pub struct FibChannelNode {
|
||||
inner: wc::FibChannel,
|
||||
}
|
||||
|
||||
#[napi]
|
||||
impl FibChannelNode {
|
||||
#[napi(constructor)]
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
inner: wc::FibChannel::new(),
|
||||
}
|
||||
}
|
||||
#[napi]
|
||||
pub fn update(&mut self, high: f64, low: f64) -> napi::Result<Option<FibChannelValue>> {
|
||||
Ok(self
|
||||
.inner
|
||||
.update(swing_cnd(high, low)?)
|
||||
.map(|o| FibChannelValue {
|
||||
base: o.base,
|
||||
level_618: o.level_618,
|
||||
level_1000: o.level_1000,
|
||||
level_1618: o.level_1618,
|
||||
}))
|
||||
}
|
||||
#[napi]
|
||||
pub fn batch(&mut self, high: Vec<f64>, low: Vec<f64>) -> napi::Result<Vec<f64>> {
|
||||
if high.len() != low.len() {
|
||||
return Err(NapiError::from_reason(
|
||||
"high and low must be equal length".to_string(),
|
||||
));
|
||||
}
|
||||
let n = high.len();
|
||||
let mut out = vec![f64::NAN; n * 4];
|
||||
for i in 0..n {
|
||||
if let Some(o) = self.inner.update(swing_cnd(high[i], low[i])?) {
|
||||
out[i * 4] = o.base;
|
||||
out[i * 4 + 1] = o.level_618;
|
||||
out[i * 4 + 2] = o.level_1000;
|
||||
out[i * 4 + 3] = o.level_1618;
|
||||
}
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
#[napi]
|
||||
pub fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
#[napi(js_name = "isReady")]
|
||||
pub fn is_ready(&self) -> bool {
|
||||
self.inner.is_ready()
|
||||
}
|
||||
#[napi(js_name = "warmupPeriod")]
|
||||
pub fn warmup_period(&self) -> u32 {
|
||||
self.inner.warmup_period() as u32
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for FibChannelNode {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[napi(object)]
|
||||
pub struct FibTimeZonesValue {
|
||||
pub on_zone: f64,
|
||||
pub bars_to_next: f64,
|
||||
}
|
||||
|
||||
#[napi(js_name = "FibTimeZones")]
|
||||
pub struct FibTimeZonesNode {
|
||||
inner: wc::FibTimeZones,
|
||||
}
|
||||
|
||||
#[napi]
|
||||
impl FibTimeZonesNode {
|
||||
#[napi(constructor)]
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
inner: wc::FibTimeZones::new(),
|
||||
}
|
||||
}
|
||||
#[napi]
|
||||
pub fn update(&mut self, high: f64, low: f64) -> napi::Result<Option<FibTimeZonesValue>> {
|
||||
Ok(self
|
||||
.inner
|
||||
.update(swing_cnd(high, low)?)
|
||||
.map(|o| FibTimeZonesValue {
|
||||
on_zone: o.on_zone,
|
||||
bars_to_next: o.bars_to_next,
|
||||
}))
|
||||
}
|
||||
#[napi]
|
||||
pub fn batch(&mut self, high: Vec<f64>, low: Vec<f64>) -> napi::Result<Vec<f64>> {
|
||||
if high.len() != low.len() {
|
||||
return Err(NapiError::from_reason(
|
||||
"high and low must be equal length".to_string(),
|
||||
));
|
||||
}
|
||||
let n = high.len();
|
||||
let mut out = vec![f64::NAN; n * 2];
|
||||
for i in 0..n {
|
||||
if let Some(o) = self.inner.update(swing_cnd(high[i], low[i])?) {
|
||||
out[i * 2] = o.on_zone;
|
||||
out[i * 2 + 1] = o.bars_to_next;
|
||||
}
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
#[napi]
|
||||
pub fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
#[napi(js_name = "isReady")]
|
||||
pub fn is_ready(&self) -> bool {
|
||||
self.inner.is_ready()
|
||||
}
|
||||
#[napi(js_name = "warmupPeriod")]
|
||||
pub fn warmup_period(&self) -> u32 {
|
||||
self.inner.warmup_period() as u32
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for FibTimeZonesNode {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -340,6 +340,10 @@ from ._wickra import (
|
||||
Gartley,
|
||||
Abcd,
|
||||
# Fibonacci
|
||||
FibTimeZones,
|
||||
FibChannel,
|
||||
FibArcs,
|
||||
FibFan,
|
||||
FibConfluence,
|
||||
GoldenPocket,
|
||||
AutoFib,
|
||||
@@ -742,6 +746,10 @@ __all__ = [
|
||||
"Gartley",
|
||||
"Abcd",
|
||||
# Fibonacci
|
||||
"FibTimeZones",
|
||||
"FibChannel",
|
||||
"FibArcs",
|
||||
"FibFan",
|
||||
"FibConfluence",
|
||||
"GoldenPocket",
|
||||
"AutoFib",
|
||||
|
||||
@@ -18293,6 +18293,275 @@ impl PyFibConfluence {
|
||||
}
|
||||
}
|
||||
|
||||
#[pyclass(name = "FibFan", module = "wickra._wickra", skip_from_py_object)]
|
||||
#[derive(Clone)]
|
||||
struct PyFibFan {
|
||||
inner: wc::FibFan,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyFibFan {
|
||||
#[new]
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
inner: wc::FibFan::new(),
|
||||
}
|
||||
}
|
||||
/// Returns `(fan_382, fan_500, fan_618)` at the current bar, or None.
|
||||
fn update(&mut self, candle: &Bound<'_, PyAny>) -> PyResult<Option<(f64, f64, f64)>> {
|
||||
let c = extract_candle(candle)?;
|
||||
Ok(self
|
||||
.inner
|
||||
.update(c)
|
||||
.map(|o| (o.fan_382, o.fan_500, o.fan_618)))
|
||||
}
|
||||
/// Batch over numpy columns high, low. Returns shape `(n, 3)`.
|
||||
fn batch<'py>(
|
||||
&mut self,
|
||||
py: Python<'py>,
|
||||
high: PyReadonlyArray1<'py, f64>,
|
||||
low: PyReadonlyArray1<'py, f64>,
|
||||
) -> PyResult<Bound<'py, PyArray2<f64>>> {
|
||||
let h = high
|
||||
.as_slice()
|
||||
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||
let l = low
|
||||
.as_slice()
|
||||
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||
if h.len() != l.len() {
|
||||
return Err(PyValueError::new_err("high and low must be equal length"));
|
||||
}
|
||||
let n = h.len();
|
||||
let mut out = vec![f64::NAN; n * 3];
|
||||
for i in 0..n {
|
||||
if let Some(o) = self
|
||||
.inner
|
||||
.update(swing_candle(h[i], l[i]).map_err(map_err)?)
|
||||
{
|
||||
out[i * 3] = o.fan_382;
|
||||
out[i * 3 + 1] = o.fan_500;
|
||||
out[i * 3 + 2] = o.fan_618;
|
||||
}
|
||||
}
|
||||
Ok(numpy::ndarray::Array2::from_shape_vec((n, 3), out)
|
||||
.expect("shape consistent")
|
||||
.into_pyarray(py))
|
||||
}
|
||||
fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
fn is_ready(&self) -> bool {
|
||||
self.inner.is_ready()
|
||||
}
|
||||
fn warmup_period(&self) -> usize {
|
||||
self.inner.warmup_period()
|
||||
}
|
||||
fn __repr__(&self) -> String {
|
||||
"FibFan()".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
#[pyclass(name = "FibArcs", module = "wickra._wickra", skip_from_py_object)]
|
||||
#[derive(Clone)]
|
||||
struct PyFibArcs {
|
||||
inner: wc::FibArcs,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyFibArcs {
|
||||
#[new]
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
inner: wc::FibArcs::new(),
|
||||
}
|
||||
}
|
||||
/// Returns `(arc_382, arc_500, arc_618)` at the current bar, or None.
|
||||
fn update(&mut self, candle: &Bound<'_, PyAny>) -> PyResult<Option<(f64, f64, f64)>> {
|
||||
let c = extract_candle(candle)?;
|
||||
Ok(self
|
||||
.inner
|
||||
.update(c)
|
||||
.map(|o| (o.arc_382, o.arc_500, o.arc_618)))
|
||||
}
|
||||
/// Batch over numpy columns high, low. Returns shape `(n, 3)`.
|
||||
fn batch<'py>(
|
||||
&mut self,
|
||||
py: Python<'py>,
|
||||
high: PyReadonlyArray1<'py, f64>,
|
||||
low: PyReadonlyArray1<'py, f64>,
|
||||
) -> PyResult<Bound<'py, PyArray2<f64>>> {
|
||||
let h = high
|
||||
.as_slice()
|
||||
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||
let l = low
|
||||
.as_slice()
|
||||
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||
if h.len() != l.len() {
|
||||
return Err(PyValueError::new_err("high and low must be equal length"));
|
||||
}
|
||||
let n = h.len();
|
||||
let mut out = vec![f64::NAN; n * 3];
|
||||
for i in 0..n {
|
||||
if let Some(o) = self
|
||||
.inner
|
||||
.update(swing_candle(h[i], l[i]).map_err(map_err)?)
|
||||
{
|
||||
out[i * 3] = o.arc_382;
|
||||
out[i * 3 + 1] = o.arc_500;
|
||||
out[i * 3 + 2] = o.arc_618;
|
||||
}
|
||||
}
|
||||
Ok(numpy::ndarray::Array2::from_shape_vec((n, 3), out)
|
||||
.expect("shape consistent")
|
||||
.into_pyarray(py))
|
||||
}
|
||||
fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
fn is_ready(&self) -> bool {
|
||||
self.inner.is_ready()
|
||||
}
|
||||
fn warmup_period(&self) -> usize {
|
||||
self.inner.warmup_period()
|
||||
}
|
||||
fn __repr__(&self) -> String {
|
||||
"FibArcs()".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
#[pyclass(name = "FibChannel", module = "wickra._wickra", skip_from_py_object)]
|
||||
#[derive(Clone)]
|
||||
struct PyFibChannel {
|
||||
inner: wc::FibChannel,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyFibChannel {
|
||||
#[new]
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
inner: wc::FibChannel::new(),
|
||||
}
|
||||
}
|
||||
/// Returns `(base, level_618, level_1000, level_1618)` at the current bar, or None.
|
||||
fn update(&mut self, candle: &Bound<'_, PyAny>) -> PyResult<Option<(f64, f64, f64, f64)>> {
|
||||
let c = extract_candle(candle)?;
|
||||
Ok(self
|
||||
.inner
|
||||
.update(c)
|
||||
.map(|o| (o.base, o.level_618, o.level_1000, o.level_1618)))
|
||||
}
|
||||
/// Batch over numpy columns high, low. Returns shape `(n, 4)`.
|
||||
fn batch<'py>(
|
||||
&mut self,
|
||||
py: Python<'py>,
|
||||
high: PyReadonlyArray1<'py, f64>,
|
||||
low: PyReadonlyArray1<'py, f64>,
|
||||
) -> PyResult<Bound<'py, PyArray2<f64>>> {
|
||||
let h = high
|
||||
.as_slice()
|
||||
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||
let l = low
|
||||
.as_slice()
|
||||
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||
if h.len() != l.len() {
|
||||
return Err(PyValueError::new_err("high and low must be equal length"));
|
||||
}
|
||||
let n = h.len();
|
||||
let mut out = vec![f64::NAN; n * 4];
|
||||
for i in 0..n {
|
||||
if let Some(o) = self
|
||||
.inner
|
||||
.update(swing_candle(h[i], l[i]).map_err(map_err)?)
|
||||
{
|
||||
out[i * 4] = o.base;
|
||||
out[i * 4 + 1] = o.level_618;
|
||||
out[i * 4 + 2] = o.level_1000;
|
||||
out[i * 4 + 3] = o.level_1618;
|
||||
}
|
||||
}
|
||||
Ok(numpy::ndarray::Array2::from_shape_vec((n, 4), out)
|
||||
.expect("shape consistent")
|
||||
.into_pyarray(py))
|
||||
}
|
||||
fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
fn is_ready(&self) -> bool {
|
||||
self.inner.is_ready()
|
||||
}
|
||||
fn warmup_period(&self) -> usize {
|
||||
self.inner.warmup_period()
|
||||
}
|
||||
fn __repr__(&self) -> String {
|
||||
"FibChannel()".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
#[pyclass(name = "FibTimeZones", module = "wickra._wickra", skip_from_py_object)]
|
||||
#[derive(Clone)]
|
||||
struct PyFibTimeZones {
|
||||
inner: wc::FibTimeZones,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyFibTimeZones {
|
||||
#[new]
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
inner: wc::FibTimeZones::new(),
|
||||
}
|
||||
}
|
||||
/// Returns `(on_zone, bars_to_next)` at the current bar, or None during warmup.
|
||||
fn update(&mut self, candle: &Bound<'_, PyAny>) -> PyResult<Option<(f64, f64)>> {
|
||||
let c = extract_candle(candle)?;
|
||||
Ok(self.inner.update(c).map(|o| (o.on_zone, o.bars_to_next)))
|
||||
}
|
||||
/// Batch over numpy columns high, low. Returns shape `(n, 2)`.
|
||||
fn batch<'py>(
|
||||
&mut self,
|
||||
py: Python<'py>,
|
||||
high: PyReadonlyArray1<'py, f64>,
|
||||
low: PyReadonlyArray1<'py, f64>,
|
||||
) -> PyResult<Bound<'py, PyArray2<f64>>> {
|
||||
let h = high
|
||||
.as_slice()
|
||||
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||
let l = low
|
||||
.as_slice()
|
||||
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||
if h.len() != l.len() {
|
||||
return Err(PyValueError::new_err("high and low must be equal length"));
|
||||
}
|
||||
let n = h.len();
|
||||
let mut out = vec![f64::NAN; n * 2];
|
||||
for i in 0..n {
|
||||
if let Some(o) = self
|
||||
.inner
|
||||
.update(swing_candle(h[i], l[i]).map_err(map_err)?)
|
||||
{
|
||||
out[i * 2] = o.on_zone;
|
||||
out[i * 2 + 1] = o.bars_to_next;
|
||||
}
|
||||
}
|
||||
Ok(numpy::ndarray::Array2::from_shape_vec((n, 2), out)
|
||||
.expect("shape consistent")
|
||||
.into_pyarray(py))
|
||||
}
|
||||
fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
fn is_ready(&self) -> bool {
|
||||
self.inner.is_ready()
|
||||
}
|
||||
fn warmup_period(&self) -> usize {
|
||||
self.inner.warmup_period()
|
||||
}
|
||||
fn __repr__(&self) -> String {
|
||||
"FibTimeZones()".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
#[pymodule]
|
||||
#[allow(clippy::too_many_lines)]
|
||||
fn _wickra(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||
@@ -18682,5 +18951,9 @@ fn _wickra(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||
m.add_class::<PyAutoFib>()?;
|
||||
m.add_class::<PyGoldenPocket>()?;
|
||||
m.add_class::<PyFibConfluence>()?;
|
||||
m.add_class::<PyFibFan>()?;
|
||||
m.add_class::<PyFibArcs>()?;
|
||||
m.add_class::<PyFibChannel>()?;
|
||||
m.add_class::<PyFibTimeZones>()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -860,6 +860,26 @@ def test_candle_scalar_streaming_matches_batch(name, ohlcv):
|
||||
# --- Candle-input, multi-output indicators --------------------------------
|
||||
|
||||
MULTI = {
|
||||
"FibFan": (
|
||||
lambda: ta.FibFan(),
|
||||
lambda ind, h, l, c, v: ind.batch(h, l),
|
||||
3,
|
||||
),
|
||||
"FibArcs": (
|
||||
lambda: ta.FibArcs(),
|
||||
lambda ind, h, l, c, v: ind.batch(h, l),
|
||||
3,
|
||||
),
|
||||
"FibChannel": (
|
||||
lambda: ta.FibChannel(),
|
||||
lambda ind, h, l, c, v: ind.batch(h, l),
|
||||
4,
|
||||
),
|
||||
"FibTimeZones": (
|
||||
lambda: ta.FibTimeZones(),
|
||||
lambda ind, h, l, c, v: ind.batch(h, l),
|
||||
2,
|
||||
),
|
||||
"FibRetracement": (
|
||||
lambda: ta.FibRetracement(),
|
||||
lambda ind, h, l, c, v: ind.batch(h, l),
|
||||
@@ -2652,6 +2672,41 @@ def test_fib_confluence_reference():
|
||||
assert t.update((101.0, 160.0, 101.0, 101.0, 1.0, 2)) is None
|
||||
assert t.update((144.0, 158.4, 144.0, 144.0, 1.0, 3)) == pytest.approx((137.64, 2.0))
|
||||
|
||||
|
||||
def test_fib_fan_reference():
|
||||
t = ta.FibFan()
|
||||
assert t.update((199.0, 200.0, 199.0, 199.0, 1.0, 0)) is None
|
||||
assert t.update((160.0, 190.0, 160.0, 160.0, 1.0, 1)) is None
|
||||
assert t.update((100.0, 150.0, 100.0, 100.0, 1.0, 2)) is None
|
||||
assert t.update((105.0, 110.0, 105.0, 105.0, 1.0, 3)) == pytest.approx((142.7, 125.0, 107.3))
|
||||
|
||||
|
||||
def test_fib_arcs_reference():
|
||||
t = ta.FibArcs()
|
||||
assert t.update((199.0, 200.0, 199.0, 199.0, 1.0, 0)) is None
|
||||
assert t.update((160.0, 190.0, 160.0, 160.0, 1.0, 1)) is None
|
||||
assert t.update((100.0, 150.0, 100.0, 100.0, 1.0, 2)) is None
|
||||
assert t.update((105.0, 110.0, 105.0, 105.0, 1.0, 3)) == pytest.approx((133.082181, 143.30127, 153.52037))
|
||||
|
||||
|
||||
def test_fib_channel_reference():
|
||||
t = ta.FibChannel()
|
||||
assert t.update((199.0, 200.0, 199.0, 199.0, 1.0, 0)) is None
|
||||
assert t.update((100.0, 190.0, 100.0, 100.0, 1.0, 1)) is None
|
||||
assert t.update((108.0, 110.0, 108.0, 108.0, 1.0, 2)) is None
|
||||
assert t.update((210.0, 220.0, 210.0, 210.0, 1.0, 3)) is None
|
||||
assert t.update((150.0, 200.0, 150.0, 150.0, 1.0, 4)) == pytest.approx((226.666667, 160.746667, 120.0, 54.08))
|
||||
|
||||
|
||||
def test_fib_time_zones_reference():
|
||||
t = ta.FibTimeZones()
|
||||
assert t.update((199.0, 200.0, 199.0, 199.0, 1.0, 0)) is None
|
||||
assert t.update((150.0, 190.0, 150.0, 150.0, 1.0, 1)) == pytest.approx((1.0, 1.0))
|
||||
assert t.update((151.0, 155.0, 151.0, 151.0, 1.0, 2)) == pytest.approx((1.0, 1.0))
|
||||
assert t.update((151.0, 155.0, 151.0, 151.0, 1.0, 3)) == pytest.approx((1.0, 2.0))
|
||||
assert t.update((151.0, 155.0, 151.0, 151.0, 1.0, 4)) == pytest.approx((0.0, 1.0))
|
||||
assert t.update((151.0, 155.0, 151.0, 151.0, 1.0, 5)) == pytest.approx((1.0, 3.0))
|
||||
|
||||
# --- Lifecycle ------------------------------------------------------------
|
||||
|
||||
|
||||
|
||||
@@ -11017,3 +11017,243 @@ impl WasmFibConfluence {
|
||||
self.inner.warmup_period()
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = FibFan)]
|
||||
pub struct WasmFibFan {
|
||||
inner: wc::FibFan,
|
||||
}
|
||||
|
||||
impl Default for WasmFibFan {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_class = FibFan)]
|
||||
impl WasmFibFan {
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new() -> WasmFibFan {
|
||||
Self {
|
||||
inner: wc::FibFan::new(),
|
||||
}
|
||||
}
|
||||
pub fn update(&mut self, high: f64, low: f64) -> Result<JsValue, JsError> {
|
||||
let c = swing_make_candle(high, low)?;
|
||||
Ok(match self.inner.update(c) {
|
||||
Some(o) => {
|
||||
let obj = Object::new();
|
||||
Reflect::set(&obj, &"fan382".into(), &o.fan_382.into()).ok();
|
||||
Reflect::set(&obj, &"fan500".into(), &o.fan_500.into()).ok();
|
||||
Reflect::set(&obj, &"fan618".into(), &o.fan_618.into()).ok();
|
||||
obj.into()
|
||||
}
|
||||
None => JsValue::NULL,
|
||||
})
|
||||
}
|
||||
pub fn batch(&mut self, high: &[f64], low: &[f64]) -> Result<Float64Array, JsError> {
|
||||
if high.len() != low.len() {
|
||||
return Err(JsError::new("high and low must be equal length"));
|
||||
}
|
||||
let n = high.len();
|
||||
let mut out = vec![f64::NAN; n * 3];
|
||||
for i in 0..n {
|
||||
if let Some(o) = self.inner.update(swing_make_candle(high[i], low[i])?) {
|
||||
out[i * 3] = o.fan_382;
|
||||
out[i * 3 + 1] = o.fan_500;
|
||||
out[i * 3 + 2] = o.fan_618;
|
||||
}
|
||||
}
|
||||
Ok(Float64Array::from(out.as_slice()))
|
||||
}
|
||||
pub fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
#[wasm_bindgen(js_name = isReady)]
|
||||
pub fn is_ready(&self) -> bool {
|
||||
self.inner.is_ready()
|
||||
}
|
||||
#[wasm_bindgen(js_name = warmupPeriod)]
|
||||
pub fn warmup_period(&self) -> usize {
|
||||
self.inner.warmup_period()
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = FibArcs)]
|
||||
pub struct WasmFibArcs {
|
||||
inner: wc::FibArcs,
|
||||
}
|
||||
|
||||
impl Default for WasmFibArcs {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_class = FibArcs)]
|
||||
impl WasmFibArcs {
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new() -> WasmFibArcs {
|
||||
Self {
|
||||
inner: wc::FibArcs::new(),
|
||||
}
|
||||
}
|
||||
pub fn update(&mut self, high: f64, low: f64) -> Result<JsValue, JsError> {
|
||||
let c = swing_make_candle(high, low)?;
|
||||
Ok(match self.inner.update(c) {
|
||||
Some(o) => {
|
||||
let obj = Object::new();
|
||||
Reflect::set(&obj, &"arc382".into(), &o.arc_382.into()).ok();
|
||||
Reflect::set(&obj, &"arc500".into(), &o.arc_500.into()).ok();
|
||||
Reflect::set(&obj, &"arc618".into(), &o.arc_618.into()).ok();
|
||||
obj.into()
|
||||
}
|
||||
None => JsValue::NULL,
|
||||
})
|
||||
}
|
||||
pub fn batch(&mut self, high: &[f64], low: &[f64]) -> Result<Float64Array, JsError> {
|
||||
if high.len() != low.len() {
|
||||
return Err(JsError::new("high and low must be equal length"));
|
||||
}
|
||||
let n = high.len();
|
||||
let mut out = vec![f64::NAN; n * 3];
|
||||
for i in 0..n {
|
||||
if let Some(o) = self.inner.update(swing_make_candle(high[i], low[i])?) {
|
||||
out[i * 3] = o.arc_382;
|
||||
out[i * 3 + 1] = o.arc_500;
|
||||
out[i * 3 + 2] = o.arc_618;
|
||||
}
|
||||
}
|
||||
Ok(Float64Array::from(out.as_slice()))
|
||||
}
|
||||
pub fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
#[wasm_bindgen(js_name = isReady)]
|
||||
pub fn is_ready(&self) -> bool {
|
||||
self.inner.is_ready()
|
||||
}
|
||||
#[wasm_bindgen(js_name = warmupPeriod)]
|
||||
pub fn warmup_period(&self) -> usize {
|
||||
self.inner.warmup_period()
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = FibChannel)]
|
||||
pub struct WasmFibChannel {
|
||||
inner: wc::FibChannel,
|
||||
}
|
||||
|
||||
impl Default for WasmFibChannel {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_class = FibChannel)]
|
||||
impl WasmFibChannel {
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new() -> WasmFibChannel {
|
||||
Self {
|
||||
inner: wc::FibChannel::new(),
|
||||
}
|
||||
}
|
||||
pub fn update(&mut self, high: f64, low: f64) -> Result<JsValue, JsError> {
|
||||
let c = swing_make_candle(high, low)?;
|
||||
Ok(match self.inner.update(c) {
|
||||
Some(o) => {
|
||||
let obj = Object::new();
|
||||
Reflect::set(&obj, &"base".into(), &o.base.into()).ok();
|
||||
Reflect::set(&obj, &"level618".into(), &o.level_618.into()).ok();
|
||||
Reflect::set(&obj, &"level1000".into(), &o.level_1000.into()).ok();
|
||||
Reflect::set(&obj, &"level1618".into(), &o.level_1618.into()).ok();
|
||||
obj.into()
|
||||
}
|
||||
None => JsValue::NULL,
|
||||
})
|
||||
}
|
||||
pub fn batch(&mut self, high: &[f64], low: &[f64]) -> Result<Float64Array, JsError> {
|
||||
if high.len() != low.len() {
|
||||
return Err(JsError::new("high and low must be equal length"));
|
||||
}
|
||||
let n = high.len();
|
||||
let mut out = vec![f64::NAN; n * 4];
|
||||
for i in 0..n {
|
||||
if let Some(o) = self.inner.update(swing_make_candle(high[i], low[i])?) {
|
||||
out[i * 4] = o.base;
|
||||
out[i * 4 + 1] = o.level_618;
|
||||
out[i * 4 + 2] = o.level_1000;
|
||||
out[i * 4 + 3] = o.level_1618;
|
||||
}
|
||||
}
|
||||
Ok(Float64Array::from(out.as_slice()))
|
||||
}
|
||||
pub fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
#[wasm_bindgen(js_name = isReady)]
|
||||
pub fn is_ready(&self) -> bool {
|
||||
self.inner.is_ready()
|
||||
}
|
||||
#[wasm_bindgen(js_name = warmupPeriod)]
|
||||
pub fn warmup_period(&self) -> usize {
|
||||
self.inner.warmup_period()
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = FibTimeZones)]
|
||||
pub struct WasmFibTimeZones {
|
||||
inner: wc::FibTimeZones,
|
||||
}
|
||||
|
||||
impl Default for WasmFibTimeZones {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_class = FibTimeZones)]
|
||||
impl WasmFibTimeZones {
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new() -> WasmFibTimeZones {
|
||||
Self {
|
||||
inner: wc::FibTimeZones::new(),
|
||||
}
|
||||
}
|
||||
pub fn update(&mut self, high: f64, low: f64) -> Result<JsValue, JsError> {
|
||||
let c = swing_make_candle(high, low)?;
|
||||
Ok(match self.inner.update(c) {
|
||||
Some(o) => {
|
||||
let obj = Object::new();
|
||||
Reflect::set(&obj, &"onZone".into(), &o.on_zone.into()).ok();
|
||||
Reflect::set(&obj, &"barsToNext".into(), &o.bars_to_next.into()).ok();
|
||||
obj.into()
|
||||
}
|
||||
None => JsValue::NULL,
|
||||
})
|
||||
}
|
||||
pub fn batch(&mut self, high: &[f64], low: &[f64]) -> Result<Float64Array, JsError> {
|
||||
if high.len() != low.len() {
|
||||
return Err(JsError::new("high and low must be equal length"));
|
||||
}
|
||||
let n = high.len();
|
||||
let mut out = vec![f64::NAN; n * 2];
|
||||
for i in 0..n {
|
||||
if let Some(o) = self.inner.update(swing_make_candle(high[i], low[i])?) {
|
||||
out[i * 2] = o.on_zone;
|
||||
out[i * 2 + 1] = o.bars_to_next;
|
||||
}
|
||||
}
|
||||
Ok(Float64Array::from(out.as_slice()))
|
||||
}
|
||||
pub fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
#[wasm_bindgen(js_name = isReady)]
|
||||
pub fn is_ready(&self) -> bool {
|
||||
self.inner.is_ready()
|
||||
}
|
||||
#[wasm_bindgen(js_name = warmupPeriod)]
|
||||
pub fn warmup_period(&self) -> usize {
|
||||
self.inner.warmup_period()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,198 @@
|
||||
//! Fibonacci Arcs — semicircular retracement levels centred on the swing end,
|
||||
//! decaying back toward it as time elapses.
|
||||
|
||||
use crate::indicators::pattern_swing::{SwingTracker, SWING_THRESHOLD};
|
||||
use crate::ohlcv::Candle;
|
||||
use crate::traits::Indicator;
|
||||
|
||||
/// The three arc ratios drawn (38.2% / 50% / 61.8%).
|
||||
const RATIOS: [f64; 3] = [0.382, 0.5, 0.618];
|
||||
|
||||
/// Fibonacci Arc prices evaluated at the current bar.
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct FibArcsOutput {
|
||||
/// Price of the 38.2% arc at the current bar.
|
||||
pub arc_382: f64,
|
||||
/// Price of the 50% arc at the current bar.
|
||||
pub arc_500: f64,
|
||||
/// Price of the 61.8% arc at the current bar.
|
||||
pub arc_618: f64,
|
||||
}
|
||||
|
||||
/// Fibonacci Arcs (`FibArcs`).
|
||||
///
|
||||
/// Three arcs centred on the end of the most recent confirmed swing leg. Time is
|
||||
/// normalised by the leg's bar-width so the construction is chart-scale-free: at
|
||||
/// the leg's end bar each arc sits exactly on its retracement level, and as time
|
||||
/// elapses the arc curves back toward the swing-end price, reaching it one leg
|
||||
/// width later.
|
||||
///
|
||||
/// ```text
|
||||
/// u = (cur - end_bar) / (end_bar - start_bar)
|
||||
/// arc(r) = end + (start - end) * r * sqrt(max(0, 1 - u^2))
|
||||
/// ```
|
||||
///
|
||||
/// Parameter-free; construction is infallible. Returns `None` until the first
|
||||
/// leg is complete.
|
||||
///
|
||||
/// See `crates/wickra-core/src/indicators/fib_arcs.rs`.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct FibArcs {
|
||||
swing: SwingTracker,
|
||||
}
|
||||
|
||||
impl FibArcs {
|
||||
/// Construct a new Fibonacci Arcs tracker.
|
||||
#[must_use]
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
swing: SwingTracker::new(SWING_THRESHOLD, 2),
|
||||
}
|
||||
}
|
||||
|
||||
fn arcs(&self) -> Option<FibArcsOutput> {
|
||||
let pivots = self.swing.pivots();
|
||||
let start = pivots.first()?;
|
||||
let end = pivots.get(1)?;
|
||||
// Consecutive pivots occur at strictly increasing bars → span >= 1 bar.
|
||||
let span_bars = (end.bar - start.bar) as f64;
|
||||
let u = (self.swing.current_bar() - end.bar) as f64 / span_bars;
|
||||
let curve = (1.0 - u * u).max(0.0).sqrt();
|
||||
let arc = |r: f64| end.price + (start.price - end.price) * r * curve;
|
||||
Some(FibArcsOutput {
|
||||
arc_382: arc(RATIOS[0]),
|
||||
arc_500: arc(RATIOS[1]),
|
||||
arc_618: arc(RATIOS[2]),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for FibArcs {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl Indicator for FibArcs {
|
||||
type Input = Candle;
|
||||
type Output = FibArcsOutput;
|
||||
|
||||
fn update(&mut self, candle: Candle) -> Option<FibArcsOutput> {
|
||||
self.swing.update(candle);
|
||||
self.arcs()
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.swing.reset();
|
||||
}
|
||||
|
||||
fn warmup_period(&self) -> usize {
|
||||
2
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.swing.pivots().len() >= 2
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"FibArcs"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::traits::BatchExt;
|
||||
use approx::assert_relative_eq;
|
||||
|
||||
fn c(high: f64, low: f64, ts: i64) -> Candle {
|
||||
Candle::new(low, high, low, low, 1.0, ts).unwrap()
|
||||
}
|
||||
|
||||
/// Leg start=200 (bar 0) -> end=100 (bar 2), confirmed at bar 3 so the arc is
|
||||
/// first reported with `u = (3 - 2) / (2 - 0) = 0.5`.
|
||||
fn down_leg() -> Vec<Candle> {
|
||||
vec![
|
||||
c(200.0, 199.0, 0),
|
||||
c(190.0, 160.0, 1), // confirm high @200
|
||||
c(150.0, 100.0, 2), // extend low to 100 (bar 2)
|
||||
c(110.0, 105.0, 3), // confirm low @100 -> two pivots
|
||||
]
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accessors_and_metadata() {
|
||||
let indicator = FibArcs::new();
|
||||
assert_eq!(indicator.name(), "FibArcs");
|
||||
assert_eq!(indicator.warmup_period(), 2);
|
||||
assert!(!indicator.is_ready());
|
||||
assert!(!FibArcs::default().is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_output_before_two_pivots() {
|
||||
let mut indicator = FibArcs::new();
|
||||
let outputs: Vec<_> = [c(200.0, 199.0, 0), c(190.0, 150.0, 1)]
|
||||
.into_iter()
|
||||
.map(|x| indicator.update(x))
|
||||
.collect();
|
||||
assert!(outputs.iter().all(Option::is_none));
|
||||
assert!(!indicator.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn arcs_curve_back_toward_the_swing_end() {
|
||||
let mut indicator = FibArcs::new();
|
||||
let mut last = None;
|
||||
for candle in down_leg() {
|
||||
last = indicator.update(candle);
|
||||
}
|
||||
let v = last.unwrap();
|
||||
assert!(indicator.is_ready());
|
||||
// u = 0.5 → curve = sqrt(0.75); arc(r) = 100 + 100 * r * curve.
|
||||
let curve = 0.75_f64.sqrt();
|
||||
assert_relative_eq!(v.arc_382, 100.0 + 100.0 * 0.382 * curve);
|
||||
assert_relative_eq!(v.arc_500, 100.0 + 100.0 * 0.5 * curve);
|
||||
assert_relative_eq!(v.arc_618, 100.0 + 100.0 * 0.618 * curve);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn arc_clamps_to_zero_beyond_one_leg_width() {
|
||||
// Extend far past the end pivot so u > 1; the curve clamps to 0 and the
|
||||
// arcs collapse onto the swing-end price.
|
||||
let mut indicator = FibArcs::new();
|
||||
for candle in down_leg() {
|
||||
let _ = indicator.update(candle);
|
||||
}
|
||||
// Feed flat bars that neither extend nor confirm a new pivot.
|
||||
let mut last = None;
|
||||
for ts in 4..12 {
|
||||
last = indicator.update(c(108.0, 106.0, ts));
|
||||
}
|
||||
let v = last.unwrap();
|
||||
assert_relative_eq!(v.arc_382, 100.0);
|
||||
assert_relative_eq!(v.arc_618, 100.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut indicator = FibArcs::new();
|
||||
for candle in down_leg() {
|
||||
let _ = indicator.update(candle);
|
||||
}
|
||||
indicator.reset();
|
||||
assert!(!indicator.is_ready());
|
||||
assert!(indicator.update(c(100.0, 99.5, 0)).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_equals_streaming() {
|
||||
let candles = down_leg();
|
||||
let mut a = FibArcs::new();
|
||||
let mut b = FibArcs::new();
|
||||
assert_eq!(
|
||||
a.batch(&candles),
|
||||
candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
//! Fibonacci Channel — a sloped base trendline plus parallel lines offset by
|
||||
//! Fibonacci multiples of the channel width.
|
||||
|
||||
use crate::indicators::pattern_swing::{SwingTracker, SWING_THRESHOLD};
|
||||
use crate::ohlcv::Candle;
|
||||
use crate::traits::Indicator;
|
||||
|
||||
/// The parallel-line ratios above the base (61.8% / 100% / 161.8% of the width).
|
||||
const RATIOS: [f64; 3] = [0.618, 1.0, 1.618];
|
||||
|
||||
/// Fibonacci Channel line prices evaluated at the current bar.
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct FibChannelOutput {
|
||||
/// The base trendline price at the current bar.
|
||||
pub base: f64,
|
||||
/// Base + 61.8% of the channel width.
|
||||
pub level_618: f64,
|
||||
/// Base + 100% of the width — the opposite channel boundary.
|
||||
pub level_1000: f64,
|
||||
/// Base + 161.8% of the width.
|
||||
pub level_1618: f64,
|
||||
}
|
||||
|
||||
/// Fibonacci Channel (`FibChannel`).
|
||||
///
|
||||
/// From the last three confirmed pivots, the two same-direction outer pivots
|
||||
/// define a sloped base trendline and the opposite middle pivot sets the channel
|
||||
/// width (its signed distance from the base line). Parallel lines are then offset
|
||||
/// by Fibonacci multiples of that width and reported at the current bar.
|
||||
///
|
||||
/// ```text
|
||||
/// slope = (p2 - p0) / (bar2 - bar0)
|
||||
/// base(bar) = p0 + slope * (bar - bar0)
|
||||
/// width = p1 - base(bar1)
|
||||
/// level(r) = base(cur) + r * width
|
||||
/// ```
|
||||
///
|
||||
/// Parameter-free; construction is infallible. Returns `None` until three pivots
|
||||
/// have confirmed.
|
||||
///
|
||||
/// See `crates/wickra-core/src/indicators/fib_channel.rs`.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct FibChannel {
|
||||
swing: SwingTracker,
|
||||
}
|
||||
|
||||
impl FibChannel {
|
||||
/// Construct a new Fibonacci Channel tracker.
|
||||
#[must_use]
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
swing: SwingTracker::new(SWING_THRESHOLD, 3),
|
||||
}
|
||||
}
|
||||
|
||||
fn channel(&self) -> Option<FibChannelOutput> {
|
||||
let pivots = self.swing.pivots();
|
||||
let p0 = pivots.first()?;
|
||||
let p1 = pivots.get(1)?;
|
||||
let p2 = pivots.get(2)?;
|
||||
// p0 and p2 are the same-direction outer pivots; their bars differ
|
||||
// strictly, so the slope denominator is non-zero.
|
||||
let slope = (p2.price - p0.price) / (p2.bar - p0.bar) as f64;
|
||||
let base_at = |bar: usize| p0.price + slope * (bar - p0.bar) as f64;
|
||||
let width = p1.price - base_at(p1.bar);
|
||||
let base = base_at(self.swing.current_bar());
|
||||
Some(FibChannelOutput {
|
||||
base,
|
||||
level_618: base + RATIOS[0] * width,
|
||||
level_1000: base + RATIOS[1] * width,
|
||||
level_1618: base + RATIOS[2] * width,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for FibChannel {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl Indicator for FibChannel {
|
||||
type Input = Candle;
|
||||
type Output = FibChannelOutput;
|
||||
|
||||
fn update(&mut self, candle: Candle) -> Option<FibChannelOutput> {
|
||||
self.swing.update(candle);
|
||||
self.channel()
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.swing.reset();
|
||||
}
|
||||
|
||||
fn warmup_period(&self) -> usize {
|
||||
3
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.swing.pivots().len() >= 3
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"FibChannel"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::traits::BatchExt;
|
||||
use approx::assert_relative_eq;
|
||||
|
||||
fn c(high: f64, low: f64, ts: i64) -> Candle {
|
||||
Candle::new(low, high, low, low, 1.0, ts).unwrap()
|
||||
}
|
||||
|
||||
/// Pivots: high 200 (bar 0), low 100 (bar 1), high 220 (bar 3); confirmed at
|
||||
/// bar 4 so the channel is first reported at current bar 4.
|
||||
fn three_pivots() -> Vec<Candle> {
|
||||
vec![
|
||||
c(200.0, 199.0, 0),
|
||||
c(190.0, 100.0, 1), // confirm high @200, low candidate @100
|
||||
c(110.0, 108.0, 2), // confirm low @100, high candidate @110
|
||||
c(220.0, 210.0, 3), // extend high to 220 (bar 3)
|
||||
c(200.0, 150.0, 4), // confirm high @220 -> three pivots
|
||||
]
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accessors_and_metadata() {
|
||||
let indicator = FibChannel::new();
|
||||
assert_eq!(indicator.name(), "FibChannel");
|
||||
assert_eq!(indicator.warmup_period(), 3);
|
||||
assert!(!indicator.is_ready());
|
||||
assert!(!FibChannel::default().is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_output_before_three_pivots() {
|
||||
let mut indicator = FibChannel::new();
|
||||
let outputs: Vec<_> = [c(200.0, 199.0, 0), c(190.0, 100.0, 1), c(110.0, 108.0, 2)]
|
||||
.into_iter()
|
||||
.map(|x| indicator.update(x))
|
||||
.collect();
|
||||
// Only two pivots confirm within these three bars.
|
||||
assert!(outputs.iter().all(Option::is_none));
|
||||
assert!(!indicator.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn channel_levels_from_three_pivots() {
|
||||
let mut indicator = FibChannel::new();
|
||||
let mut last = None;
|
||||
for candle in three_pivots() {
|
||||
last = indicator.update(candle);
|
||||
}
|
||||
let v = last.unwrap();
|
||||
assert!(indicator.is_ready());
|
||||
// Base through highs (0,200) and (3,220); width from low (1,100); cur = 4.
|
||||
let slope = (220.0 - 200.0) / 3.0;
|
||||
let base_cur = 200.0 + slope * 4.0;
|
||||
let width = 100.0 - (200.0 + slope * 1.0);
|
||||
assert_relative_eq!(v.base, base_cur);
|
||||
assert_relative_eq!(v.level_1000, base_cur + width);
|
||||
assert_relative_eq!(v.level_618, base_cur + 0.618 * width);
|
||||
assert_relative_eq!(v.level_1618, base_cur + 1.618 * width);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut indicator = FibChannel::new();
|
||||
for candle in three_pivots() {
|
||||
let _ = indicator.update(candle);
|
||||
}
|
||||
assert!(indicator.is_ready());
|
||||
indicator.reset();
|
||||
assert!(!indicator.is_ready());
|
||||
assert!(indicator.update(c(100.0, 99.5, 0)).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_equals_streaming() {
|
||||
let candles = three_pivots();
|
||||
let mut a = FibChannel::new();
|
||||
let mut b = FibChannel::new();
|
||||
assert_eq!(
|
||||
a.batch(&candles),
|
||||
candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
//! Fibonacci Fan — trendlines fanning from a swing start through the
|
||||
//! retracement levels at the swing end, extended to the current bar.
|
||||
|
||||
use crate::indicators::pattern_swing::{SwingTracker, SWING_THRESHOLD};
|
||||
use crate::ohlcv::Candle;
|
||||
use crate::traits::Indicator;
|
||||
|
||||
/// The three fan ratios drawn (38.2% / 50% / 61.8%).
|
||||
const RATIOS: [f64; 3] = [0.382, 0.5, 0.618];
|
||||
|
||||
/// Fibonacci Fan line prices evaluated at the current bar.
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct FibFanOutput {
|
||||
/// Price of the 38.2% fan line at the current bar.
|
||||
pub fan_382: f64,
|
||||
/// Price of the 50% fan line at the current bar.
|
||||
pub fan_500: f64,
|
||||
/// Price of the 61.8% fan line at the current bar.
|
||||
pub fan_618: f64,
|
||||
}
|
||||
|
||||
/// Fibonacci Fan (`FibFan`).
|
||||
///
|
||||
/// Anchored at the start of the most recent confirmed swing leg, three lines fan
|
||||
/// out through the 38.2% / 50% / 61.8% retracement levels located at the leg's
|
||||
/// end bar, then extend to the current bar. Each line's price is reported as the
|
||||
/// fan opens with elapsed time.
|
||||
///
|
||||
/// ```text
|
||||
/// line(r) = start + r * (end - start) * (cur - start_bar) / (end_bar - start_bar)
|
||||
/// ```
|
||||
///
|
||||
/// Parameter-free; construction is infallible. Returns `None` until the first
|
||||
/// leg is complete.
|
||||
///
|
||||
/// See `crates/wickra-core/src/indicators/fib_fan.rs`.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct FibFan {
|
||||
swing: SwingTracker,
|
||||
}
|
||||
|
||||
impl FibFan {
|
||||
/// Construct a new Fibonacci Fan tracker.
|
||||
#[must_use]
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
swing: SwingTracker::new(SWING_THRESHOLD, 2),
|
||||
}
|
||||
}
|
||||
|
||||
fn fan(&self) -> Option<FibFanOutput> {
|
||||
let pivots = self.swing.pivots();
|
||||
let start = pivots.first()?;
|
||||
let end = pivots.get(1)?;
|
||||
// Consecutive pivots occur at strictly increasing bars, so the span is
|
||||
// always at least one bar — no division by zero.
|
||||
let span_bars = (end.bar - start.bar) as f64;
|
||||
let elapsed = (self.swing.current_bar() - start.bar) as f64;
|
||||
let progress = elapsed / span_bars;
|
||||
let line = |r: f64| start.price + r * (end.price - start.price) * progress;
|
||||
Some(FibFanOutput {
|
||||
fan_382: line(RATIOS[0]),
|
||||
fan_500: line(RATIOS[1]),
|
||||
fan_618: line(RATIOS[2]),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for FibFan {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl Indicator for FibFan {
|
||||
type Input = Candle;
|
||||
type Output = FibFanOutput;
|
||||
|
||||
fn update(&mut self, candle: Candle) -> Option<FibFanOutput> {
|
||||
self.swing.update(candle);
|
||||
self.fan()
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.swing.reset();
|
||||
}
|
||||
|
||||
fn warmup_period(&self) -> usize {
|
||||
2
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.swing.pivots().len() >= 2
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"FibFan"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::traits::BatchExt;
|
||||
use approx::assert_relative_eq;
|
||||
|
||||
fn c(high: f64, low: f64, ts: i64) -> Candle {
|
||||
Candle::new(low, high, low, low, 1.0, ts).unwrap()
|
||||
}
|
||||
|
||||
/// Drive a leg start=200 (bar 0) -> end=100 (bar 2), confirmed at bar 3, so
|
||||
/// the fan is first reported at bar 3 with `progress = 3 / 2 = 1.5`.
|
||||
fn down_leg() -> Vec<Candle> {
|
||||
vec![
|
||||
c(200.0, 199.0, 0), // bootstrap high @200 (bar 0)
|
||||
c(190.0, 160.0, 1), // confirm high @200, low candidate @160
|
||||
c(150.0, 100.0, 2), // extend low to 100 (bar 2)
|
||||
c(110.0, 105.0, 3), // confirm low @100 -> two pivots
|
||||
]
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accessors_and_metadata() {
|
||||
let indicator = FibFan::new();
|
||||
assert_eq!(indicator.name(), "FibFan");
|
||||
assert_eq!(indicator.warmup_period(), 2);
|
||||
assert!(!indicator.is_ready());
|
||||
assert!(!FibFan::default().is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_output_before_two_pivots() {
|
||||
let mut indicator = FibFan::new();
|
||||
// Only the high confirms here; no end pivot yet.
|
||||
let outputs: Vec<_> = [c(200.0, 199.0, 0), c(190.0, 150.0, 1)]
|
||||
.into_iter()
|
||||
.map(|x| indicator.update(x))
|
||||
.collect();
|
||||
assert!(outputs.iter().all(Option::is_none));
|
||||
assert!(!indicator.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fan_lines_open_with_elapsed_time() {
|
||||
let mut indicator = FibFan::new();
|
||||
let mut last = None;
|
||||
for candle in down_leg() {
|
||||
last = indicator.update(candle);
|
||||
}
|
||||
let v = last.unwrap();
|
||||
assert!(indicator.is_ready());
|
||||
// progress = (3 - 0) / (2 - 0) = 1.5; line(r) = 200 + r*(-100)*1.5.
|
||||
assert_relative_eq!(v.fan_382, 200.0 - 0.382 * 150.0);
|
||||
assert_relative_eq!(v.fan_500, 125.0);
|
||||
assert_relative_eq!(v.fan_618, 200.0 - 0.618 * 150.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut indicator = FibFan::new();
|
||||
for candle in down_leg() {
|
||||
let _ = indicator.update(candle);
|
||||
}
|
||||
assert!(indicator.is_ready());
|
||||
indicator.reset();
|
||||
assert!(!indicator.is_ready());
|
||||
assert!(indicator.update(c(100.0, 99.5, 0)).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_equals_streaming() {
|
||||
let candles = down_leg();
|
||||
let mut a = FibFan::new();
|
||||
let mut b = FibFan::new();
|
||||
assert_eq!(
|
||||
a.batch(&candles),
|
||||
candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
//! Fibonacci Time Zones — vertical markers at Fibonacci bar-distances from the
|
||||
//! most recent swing pivot.
|
||||
|
||||
use crate::indicators::pattern_swing::{SwingTracker, SWING_THRESHOLD};
|
||||
use crate::ohlcv::Candle;
|
||||
use crate::traits::Indicator;
|
||||
|
||||
/// Where the current bar sits relative to the Fibonacci time-zone grid anchored
|
||||
/// on the most recent confirmed pivot.
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct FibTimeZonesOutput {
|
||||
/// `1.0` when the current bar lands on a Fibonacci time zone (a bar distance
|
||||
/// of 1, 2, 3, 5, 8, 13, … from the anchor pivot), otherwise `0.0`.
|
||||
pub on_zone: f64,
|
||||
/// Number of bars until the next Fibonacci time zone (`0` is never returned —
|
||||
/// when on a zone this is the gap to the following one).
|
||||
pub bars_to_next: f64,
|
||||
}
|
||||
|
||||
/// Fibonacci Time Zones (`FibTimeZones`).
|
||||
///
|
||||
/// Anchored on the most recent confirmed swing pivot, the Fibonacci sequence
|
||||
/// `1, 2, 3, 5, 8, 13, …` marks bars at which trend changes are classically
|
||||
/// anticipated. Reports whether the current bar is on a zone and how many bars
|
||||
/// remain until the next one.
|
||||
///
|
||||
/// Parameter-free; construction is infallible. Returns `None` until the first
|
||||
/// pivot has confirmed.
|
||||
///
|
||||
/// See `crates/wickra-core/src/indicators/fib_time_zones.rs`.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct FibTimeZones {
|
||||
swing: SwingTracker,
|
||||
}
|
||||
|
||||
impl FibTimeZones {
|
||||
/// Construct a new Fibonacci Time Zones tracker.
|
||||
#[must_use]
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
swing: SwingTracker::new(SWING_THRESHOLD, 2),
|
||||
}
|
||||
}
|
||||
|
||||
fn zones(&self) -> Option<FibTimeZonesOutput> {
|
||||
let anchor = self.swing.pivots().last()?;
|
||||
let distance = self.swing.current_bar() - anchor.bar;
|
||||
// Walk the time-zone sequence 1, 2, 3, 5, 8, … : `lo` advances through the
|
||||
// members, `on_zone` records a hit, and the loop exits with `lo` holding
|
||||
// the smallest member strictly greater than `distance`.
|
||||
let (mut lo, mut hi) = (1usize, 2usize);
|
||||
let mut on_zone = false;
|
||||
while lo <= distance {
|
||||
if lo == distance {
|
||||
on_zone = true;
|
||||
}
|
||||
let next = lo + hi;
|
||||
lo = hi;
|
||||
hi = next;
|
||||
}
|
||||
Some(FibTimeZonesOutput {
|
||||
on_zone: f64::from(u8::from(on_zone)),
|
||||
bars_to_next: (lo - distance) as f64,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for FibTimeZones {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl Indicator for FibTimeZones {
|
||||
type Input = Candle;
|
||||
type Output = FibTimeZonesOutput;
|
||||
|
||||
fn update(&mut self, candle: Candle) -> Option<FibTimeZonesOutput> {
|
||||
self.swing.update(candle);
|
||||
self.zones()
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.swing.reset();
|
||||
}
|
||||
|
||||
fn warmup_period(&self) -> usize {
|
||||
2
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
!self.swing.pivots().is_empty()
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"FibTimeZones"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::traits::BatchExt;
|
||||
use approx::assert_relative_eq;
|
||||
|
||||
fn c(high: f64, low: f64, ts: i64) -> Candle {
|
||||
Candle::new(low, high, low, low, 1.0, ts).unwrap()
|
||||
}
|
||||
|
||||
/// One pivot confirms at bar 0 (high @200, confirmed at bar 1); subsequent
|
||||
/// flat bars neither extend nor confirm, so the anchor stays at bar 0 and the
|
||||
/// distance equals the current bar index.
|
||||
fn anchored_run() -> Vec<Candle> {
|
||||
let mut bars = vec![c(200.0, 199.0, 0), c(190.0, 150.0, 1)];
|
||||
for ts in 2..=5 {
|
||||
bars.push(c(155.0, 151.0, ts));
|
||||
}
|
||||
bars
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accessors_and_metadata() {
|
||||
let indicator = FibTimeZones::new();
|
||||
assert_eq!(indicator.name(), "FibTimeZones");
|
||||
assert_eq!(indicator.warmup_period(), 2);
|
||||
assert!(!indicator.is_ready());
|
||||
assert!(!FibTimeZones::default().is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_output_before_first_pivot() {
|
||||
let mut indicator = FibTimeZones::new();
|
||||
// The bootstrap bar confirms nothing.
|
||||
assert!(indicator.update(c(200.0, 199.0, 0)).is_none());
|
||||
assert!(!indicator.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn flags_zones_and_counts_to_next() {
|
||||
let mut indicator = FibTimeZones::new();
|
||||
let out: Vec<_> = anchored_run()
|
||||
.into_iter()
|
||||
.map(|x| indicator.update(x))
|
||||
.collect();
|
||||
assert!(out[0].is_none()); // bootstrap, no pivot yet
|
||||
assert!(indicator.is_ready());
|
||||
// out[i] is reported at current bar i; anchor at bar 0 → distance = i.
|
||||
let d1 = out[1].unwrap(); // distance 1 → a zone
|
||||
assert_relative_eq!(d1.on_zone, 1.0);
|
||||
assert_relative_eq!(d1.bars_to_next, 1.0); // next zone at 2
|
||||
let d4 = out[4].unwrap(); // distance 4 → not a zone
|
||||
assert_relative_eq!(d4.on_zone, 0.0);
|
||||
assert_relative_eq!(d4.bars_to_next, 1.0); // next zone at 5
|
||||
let d5 = out[5].unwrap(); // distance 5 → a zone
|
||||
assert_relative_eq!(d5.on_zone, 1.0);
|
||||
assert_relative_eq!(d5.bars_to_next, 3.0); // next zone at 8
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut indicator = FibTimeZones::new();
|
||||
for candle in anchored_run() {
|
||||
let _ = indicator.update(candle);
|
||||
}
|
||||
assert!(indicator.is_ready());
|
||||
indicator.reset();
|
||||
assert!(!indicator.is_ready());
|
||||
assert!(indicator.update(c(100.0, 99.5, 0)).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_equals_streaming() {
|
||||
let candles = anchored_run();
|
||||
let mut a = FibTimeZones::new();
|
||||
let mut b = FibTimeZones::new();
|
||||
assert_eq!(
|
||||
a.batch(&candles),
|
||||
candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -111,10 +111,14 @@ mod evening_doji_star;
|
||||
mod evwma;
|
||||
mod falling_three_methods;
|
||||
mod fama;
|
||||
mod fib_arcs;
|
||||
mod fib_channel;
|
||||
mod fib_confluence;
|
||||
mod fib_extension;
|
||||
mod fib_fan;
|
||||
mod fib_projection;
|
||||
mod fib_retracement;
|
||||
mod fib_time_zones;
|
||||
mod fibonacci_pivots;
|
||||
mod fisher_transform;
|
||||
mod flag_pennant;
|
||||
@@ -484,10 +488,14 @@ pub use evening_doji_star::EveningDojiStar;
|
||||
pub use evwma::Evwma;
|
||||
pub use falling_three_methods::FallingThreeMethods;
|
||||
pub use fama::Fama;
|
||||
pub use fib_arcs::{FibArcs, FibArcsOutput};
|
||||
pub use fib_channel::{FibChannel, FibChannelOutput};
|
||||
pub use fib_confluence::{FibConfluence, FibConfluenceOutput};
|
||||
pub use fib_extension::{FibExtension, FibExtensionOutput};
|
||||
pub use fib_fan::{FibFan, FibFanOutput};
|
||||
pub use fib_projection::{FibProjection, FibProjectionOutput};
|
||||
pub use fib_retracement::{FibRetracement, FibRetracementOutput};
|
||||
pub use fib_time_zones::{FibTimeZones, FibTimeZonesOutput};
|
||||
pub use fibonacci_pivots::{FibonacciPivots, FibonacciPivotsOutput};
|
||||
pub use fisher_transform::FisherTransform;
|
||||
pub use flag_pennant::FlagPennant;
|
||||
@@ -1243,6 +1251,10 @@ pub const FAMILIES: &[(&str, &[&str])] = &[
|
||||
"AutoFib",
|
||||
"GoldenPocket",
|
||||
"FibConfluence",
|
||||
"FibFan",
|
||||
"FibArcs",
|
||||
"FibChannel",
|
||||
"FibTimeZones",
|
||||
],
|
||||
),
|
||||
];
|
||||
@@ -1273,6 +1285,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, 373, "FAMILIES total drifted from indicator count");
|
||||
assert_eq!(total, 377, "FAMILIES total drifted from indicator count");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,14 +27,19 @@ pub(crate) const SWING_THRESHOLD: f64 = 0.05;
|
||||
/// flat boundary of a rectangle.
|
||||
pub(crate) const LEVEL_TOLERANCE: f64 = 0.03;
|
||||
|
||||
/// A confirmed swing pivot: the extreme price the swing turned from and its
|
||||
/// direction (`+1.0` for a swing high, `-1.0` for a swing low).
|
||||
/// A confirmed swing pivot: the extreme price the swing turned from, its
|
||||
/// direction (`+1.0` for a swing high, `-1.0` for a swing low) and the bar index
|
||||
/// at which that extreme occurred.
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub(crate) struct Pivot {
|
||||
/// Price of the confirmed swing extreme.
|
||||
pub price: f64,
|
||||
/// `+1.0` if the pivot is a swing high, `-1.0` if it is a swing low.
|
||||
pub direction: f64,
|
||||
/// Zero-based index of the candle at which the swing extreme occurred (not
|
||||
/// the later bar that confirmed it). Used by the geometric Fibonacci tools
|
||||
/// (fan, arcs, channel, time zones) to place trendlines and time offsets.
|
||||
pub bar: usize,
|
||||
}
|
||||
|
||||
/// Non-repainting percent-threshold swing tracker with a bounded pivot history.
|
||||
@@ -50,6 +55,8 @@ pub(crate) struct Pivot {
|
||||
pub(crate) struct SwingTracker {
|
||||
threshold: f64,
|
||||
cap: usize,
|
||||
/// Number of candles fed so far; the current bar index is `bars_seen - 1`.
|
||||
bars_seen: usize,
|
||||
state: Option<State>,
|
||||
pivots: Vec<Pivot>,
|
||||
}
|
||||
@@ -61,6 +68,8 @@ struct State {
|
||||
direction: f64,
|
||||
/// The running candidate extreme price.
|
||||
extreme: f64,
|
||||
/// Bar index at which the running extreme was last set.
|
||||
extreme_bar: usize,
|
||||
}
|
||||
|
||||
impl SwingTracker {
|
||||
@@ -75,6 +84,7 @@ impl SwingTracker {
|
||||
Self {
|
||||
threshold,
|
||||
cap,
|
||||
bars_seen: 0,
|
||||
state: None,
|
||||
pivots: Vec::new(),
|
||||
}
|
||||
@@ -82,11 +92,14 @@ impl SwingTracker {
|
||||
|
||||
/// Feed one candle. Returns `true` when a new pivot was confirmed this bar.
|
||||
pub(crate) fn update(&mut self, candle: Candle) -> bool {
|
||||
let bar = self.bars_seen;
|
||||
self.bars_seen += 1;
|
||||
let Some(s) = self.state else {
|
||||
// Bootstrap: seed an uptrend tracking the first candle's high.
|
||||
self.state = Some(State {
|
||||
direction: 1.0,
|
||||
extreme: candle.high,
|
||||
extreme_bar: bar,
|
||||
});
|
||||
return false;
|
||||
};
|
||||
@@ -97,6 +110,7 @@ impl SwingTracker {
|
||||
self.state = Some(State {
|
||||
direction: 1.0,
|
||||
extreme: candle.high,
|
||||
extreme_bar: bar,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
@@ -105,10 +119,12 @@ impl SwingTracker {
|
||||
self.push(Pivot {
|
||||
price: s.extreme,
|
||||
direction: 1.0,
|
||||
bar: s.extreme_bar,
|
||||
});
|
||||
self.state = Some(State {
|
||||
direction: -1.0,
|
||||
extreme: candle.low,
|
||||
extreme_bar: bar,
|
||||
});
|
||||
return true;
|
||||
}
|
||||
@@ -119,6 +135,7 @@ impl SwingTracker {
|
||||
self.state = Some(State {
|
||||
direction: -1.0,
|
||||
extreme: candle.low,
|
||||
extreme_bar: bar,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
@@ -127,10 +144,12 @@ impl SwingTracker {
|
||||
self.push(Pivot {
|
||||
price: s.extreme,
|
||||
direction: -1.0,
|
||||
bar: s.extreme_bar,
|
||||
});
|
||||
self.state = Some(State {
|
||||
direction: 1.0,
|
||||
extreme: candle.high,
|
||||
extreme_bar: bar,
|
||||
});
|
||||
return true;
|
||||
}
|
||||
@@ -138,6 +157,12 @@ impl SwingTracker {
|
||||
}
|
||||
}
|
||||
|
||||
/// Zero-based index of the most recently fed candle. Saturates at `0` before
|
||||
/// any candle has been seen.
|
||||
pub(crate) fn current_bar(&self) -> usize {
|
||||
self.bars_seen.saturating_sub(1)
|
||||
}
|
||||
|
||||
fn push(&mut self, pivot: Pivot) {
|
||||
self.pivots.push(pivot);
|
||||
if self.pivots.len() > self.cap {
|
||||
@@ -152,6 +177,7 @@ impl SwingTracker {
|
||||
|
||||
/// Clear all state, returning the tracker to its just-constructed condition.
|
||||
pub(crate) fn reset(&mut self) {
|
||||
self.bars_seen = 0;
|
||||
self.state = None;
|
||||
self.pivots.clear();
|
||||
}
|
||||
@@ -312,9 +338,11 @@ mod tests {
|
||||
assert!(t.update(c_hl(101.0, 100.0, 2)));
|
||||
assert_eq!(
|
||||
t.pivots().last().copied(),
|
||||
// The high extreme was set at bar 1, confirmed at bar 2.
|
||||
Some(Pivot {
|
||||
price: 120.0,
|
||||
direction: 1.0,
|
||||
bar: 1,
|
||||
})
|
||||
);
|
||||
// Now in a downtrend: a lower low extends the candidate low.
|
||||
@@ -323,9 +351,11 @@ mod tests {
|
||||
assert!(t.update(c_hl(100.0, 99.0, 4)));
|
||||
assert_eq!(
|
||||
t.pivots().last().copied(),
|
||||
// The low extreme was set at bar 3, confirmed at bar 4.
|
||||
Some(Pivot {
|
||||
price: 90.0,
|
||||
direction: -1.0,
|
||||
bar: 3,
|
||||
})
|
||||
);
|
||||
}
|
||||
@@ -382,18 +412,22 @@ mod tests {
|
||||
Pivot {
|
||||
price: 100.0,
|
||||
direction: -1.0,
|
||||
bar: 0,
|
||||
},
|
||||
Pivot {
|
||||
price: 120.0,
|
||||
direction: 1.0,
|
||||
bar: 0,
|
||||
},
|
||||
Pivot {
|
||||
price: 110.0,
|
||||
direction: -1.0,
|
||||
bar: 0,
|
||||
},
|
||||
Pivot {
|
||||
price: 121.0,
|
||||
direction: 1.0,
|
||||
bar: 0,
|
||||
},
|
||||
];
|
||||
assert_eq!(recent_legs(&ending_high), (120.0, 121.0, 100.0, 110.0));
|
||||
@@ -402,18 +436,22 @@ mod tests {
|
||||
Pivot {
|
||||
price: 120.0,
|
||||
direction: 1.0,
|
||||
bar: 0,
|
||||
},
|
||||
Pivot {
|
||||
price: 100.0,
|
||||
direction: -1.0,
|
||||
bar: 0,
|
||||
},
|
||||
Pivot {
|
||||
price: 110.0,
|
||||
direction: 1.0,
|
||||
bar: 0,
|
||||
},
|
||||
Pivot {
|
||||
price: 99.0,
|
||||
direction: -1.0,
|
||||
bar: 0,
|
||||
},
|
||||
];
|
||||
assert_eq!(recent_legs(&ending_low), (120.0, 110.0, 100.0, 99.0));
|
||||
@@ -425,26 +463,32 @@ mod tests {
|
||||
Pivot {
|
||||
price: 50.0,
|
||||
direction: 1.0,
|
||||
bar: 0,
|
||||
},
|
||||
Pivot {
|
||||
price: 100.0,
|
||||
direction: -1.0,
|
||||
bar: 0,
|
||||
}, // X
|
||||
Pivot {
|
||||
price: 140.0,
|
||||
direction: 1.0,
|
||||
bar: 0,
|
||||
}, // A
|
||||
Pivot {
|
||||
price: 115.0,
|
||||
direction: -1.0,
|
||||
bar: 0,
|
||||
}, // B
|
||||
Pivot {
|
||||
price: 128.0,
|
||||
direction: 1.0,
|
||||
bar: 0,
|
||||
}, // C
|
||||
Pivot {
|
||||
price: 108.0,
|
||||
direction: -1.0,
|
||||
bar: 0,
|
||||
}, // D (low → bullish)
|
||||
];
|
||||
let p = xabcd(&pivots);
|
||||
@@ -483,4 +527,16 @@ mod tests {
|
||||
assert!(approx_equal(0.0, 0.0, 0.01)); // both zero
|
||||
assert!(approx_equal(-50.0, -49.0, 0.05)); // negative magnitudes
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tracks_extreme_bar_and_current_bar() {
|
||||
let mut t = SwingTracker::new(0.10, 6);
|
||||
// Before any candle, the current bar saturates at 0.
|
||||
assert_eq!(t.current_bar(), 0);
|
||||
let _ = t.update(c_hl(100.0, 99.5, 0));
|
||||
let _ = t.update(c_hl(120.0, 119.5, 1)); // candidate high at bar 1
|
||||
let _ = t.update(c_hl(101.0, 90.0, 2)); // confirm high @120 (extreme bar 1)
|
||||
assert_eq!(t.current_bar(), 2);
|
||||
assert_eq!(t.pivots().last().unwrap().bar, 1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,8 +76,9 @@ pub use indicators::{
|
||||
DoubleTopBottom, DownsideGapThreeMethods, Dpo, DragonflyDoji, DrawdownDuration, Dx,
|
||||
EaseOfMovement, EffectiveSpread, EhlersStochastic, ElderImpulse, Ema,
|
||||
EmpiricalModeDecomposition, Engulfing, EveningDojiStar, Evwma, FallingThreeMethods, Fama,
|
||||
FibConfluence, FibConfluenceOutput, FibExtension, FibExtensionOutput, FibProjection,
|
||||
FibProjectionOutput, FibRetracement, FibRetracementOutput, FibonacciPivots,
|
||||
FibArcs, FibArcsOutput, FibChannel, FibChannelOutput, FibConfluence, FibConfluenceOutput,
|
||||
FibExtension, FibExtensionOutput, FibFan, FibFanOutput, FibProjection, FibProjectionOutput,
|
||||
FibRetracement, FibRetracementOutput, FibTimeZones, FibTimeZonesOutput, FibonacciPivots,
|
||||
FibonacciPivotsOutput, FisherTransform, FlagPennant, Footprint, FootprintOutput, ForceIndex,
|
||||
FractalChaosBands, FractalChaosBandsOutput, Frama, FundingBasis, FundingRate, FundingRateMean,
|
||||
FundingRateZScore, GainLossRatio, GapSideBySideWhite, GarmanKlassVolatility, Gartley,
|
||||
|
||||
+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 **373 indicators** across
|
||||
- A per-indicator deep dive for every one of the **377 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 &
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
//! WeightedClose.
|
||||
|
||||
use libfuzzer_sys::fuzz_target;
|
||||
use wickra_core::{AbandonedBaby, Abcd, AccelerationBands, AcceleratorOscillator, AdOscillator, Adl, AdvanceBlock, Adx, Adxr, Alligator, AnchoredVwap, Aroon, AroonOscillator, Atr, AtrBands, AtrTrailingStop, AutoFib, AverageDailyRange, AwesomeOscillator, AwesomeOscillatorHistogram, BalanceOfPower, Bat, BatchExt, BeltHold, Breakaway, Butterfly, Camarilla, Candle, Cci, ChaikinMoneyFlow, ChaikinOscillator, ChaikinVolatility, ChandeKrollStop, ChandelierExit, ChoppinessIndex, ClassicPivots, ClosingMarubozu, ConcealingBabySwallow, Counterattack, Crab, CupAndHandle, Cypher, DayOfWeekProfile, DemandIndex, DemarkPivots, Doji, DojiStar, Donchian, DonchianStop, DoubleTopBottom, DownsideGapThreeMethods, DragonflyDoji, Dx, EaseOfMovement, Engulfing, EveningDojiStar, Evwma, FallingThreeMethods, FibConfluence, FibExtension, FibProjection, FibRetracement, FibonacciPivots, FlagPennant, ForceIndex, FractalChaosBands, GapSideBySideWhite, GarmanKlassVolatility, Gartley, GoldenPocket, GravestoneDoji, Hammer, HangingMan, Harami, HeadAndShoulders, HeikinAshi, HiLoActivator, HighWave, Hikkake, HikkakeModified, HomingPigeon, HurstChannel, Ichimoku, IdenticalThreeCrows, InNeck, Indicator, Inertia, InitialBalance, IntradayVolatilityProfile, InvertedHammer, Keltner, Kicking, KickingByLength, Kvo, LadderBottom, LongLeggedDoji, LongLine, MarketFacilitationIndex, Marubozu, MassIndex, MatHold, MatchingLow, AvgPrice, MedianPrice, Mfi, MidPrice, MinusDi, MinusDm, MorningDojiStar, MorningEveningStar, Natr, Nvi, Obv, OnNeck, OpeningMarubozu, OpeningRange, OvernightGap, OvernightIntradayReturn, ParkinsonVolatility, Pgo, PiercingDarkCloud, PlusDi, PlusDm, Psar, Pvi, RectangleRange, RickshawMan, RisingThreeMethods, RogersSatchellVolatility, RollingVwap, Rvi, Rwi, SarExt, SeasonalZScore, SeparatingLines, SessionHighLow, SessionRange, SessionVwap, Shark, ShootingStar, ShortLine, Smi, SpinningTop, StalledPattern, StarcBands, StickSandwich, Stochastic, SuperTrend, Takuri, TasukiGap, TdCombo, TdCountdown, TdDeMarker, TdDifferential, TdLines, TdOpen, TdPressure, TdRangeProjection, TdRei, TdRiskLevel, TdSequential, TdSetup, ThreeDrives, ThreeInside, ThreeLineStrike, ThreeOutside, ThreeSoldiersOrCrows, ThreeStarsInSouth, Thrusting, TimeOfDayReturnProfile, TpoProfile, Triangle, TripleTopBottom, TrueRange, Tsv, TtmSqueeze, TurnOfMonth, Tweezer, TwoCrows, TypicalPrice, UltimateOscillator, UniqueThreeRiver, UpsideGapThreeMethods, UpsideGapTwoCrows, ValueArea, VoltyStop, VolumeByTimeProfile, VolumeOscillator, VolumePriceTrend, VolumeProfile, Vortex, Vwap, VwapStdDevBands, Vwma, Vzo, WaveTrend, Wedge, WeightedClose, WilliamsFractals, WilliamsR, WoodiePivots, YangZhangVolatility, YoyoExit, ZigZag};
|
||||
use wickra_core::{AbandonedBaby, Abcd, AccelerationBands, AcceleratorOscillator, AdOscillator, Adl, AdvanceBlock, Adx, Adxr, Alligator, AnchoredVwap, Aroon, AroonOscillator, Atr, AtrBands, AtrTrailingStop, AutoFib, AverageDailyRange, AwesomeOscillator, AwesomeOscillatorHistogram, BalanceOfPower, Bat, BatchExt, BeltHold, Breakaway, Butterfly, Camarilla, Candle, Cci, ChaikinMoneyFlow, ChaikinOscillator, ChaikinVolatility, ChandeKrollStop, ChandelierExit, ChoppinessIndex, ClassicPivots, ClosingMarubozu, ConcealingBabySwallow, Counterattack, Crab, CupAndHandle, Cypher, DayOfWeekProfile, DemandIndex, DemarkPivots, Doji, DojiStar, Donchian, DonchianStop, DoubleTopBottom, DownsideGapThreeMethods, DragonflyDoji, Dx, EaseOfMovement, Engulfing, EveningDojiStar, Evwma, FallingThreeMethods, FibArcs, FibChannel, FibConfluence, FibExtension, FibFan, FibProjection, FibRetracement, FibTimeZones, FibonacciPivots, FlagPennant, ForceIndex, FractalChaosBands, GapSideBySideWhite, GarmanKlassVolatility, Gartley, GoldenPocket, GravestoneDoji, Hammer, HangingMan, Harami, HeadAndShoulders, HeikinAshi, HiLoActivator, HighWave, Hikkake, HikkakeModified, HomingPigeon, HurstChannel, Ichimoku, IdenticalThreeCrows, InNeck, Indicator, Inertia, InitialBalance, IntradayVolatilityProfile, InvertedHammer, Keltner, Kicking, KickingByLength, Kvo, LadderBottom, LongLeggedDoji, LongLine, MarketFacilitationIndex, Marubozu, MassIndex, MatHold, MatchingLow, AvgPrice, MedianPrice, Mfi, MidPrice, MinusDi, MinusDm, MorningDojiStar, MorningEveningStar, Natr, Nvi, Obv, OnNeck, OpeningMarubozu, OpeningRange, OvernightGap, OvernightIntradayReturn, ParkinsonVolatility, Pgo, PiercingDarkCloud, PlusDi, PlusDm, Psar, Pvi, RectangleRange, RickshawMan, RisingThreeMethods, RogersSatchellVolatility, RollingVwap, Rvi, Rwi, SarExt, SeasonalZScore, SeparatingLines, SessionHighLow, SessionRange, SessionVwap, Shark, ShootingStar, ShortLine, Smi, SpinningTop, StalledPattern, StarcBands, StickSandwich, Stochastic, SuperTrend, Takuri, TasukiGap, TdCombo, TdCountdown, TdDeMarker, TdDifferential, TdLines, TdOpen, TdPressure, TdRangeProjection, TdRei, TdRiskLevel, TdSequential, TdSetup, ThreeDrives, ThreeInside, ThreeLineStrike, ThreeOutside, ThreeSoldiersOrCrows, ThreeStarsInSouth, Thrusting, TimeOfDayReturnProfile, TpoProfile, Triangle, TripleTopBottom, TrueRange, Tsv, TtmSqueeze, TurnOfMonth, Tweezer, TwoCrows, TypicalPrice, UltimateOscillator, UniqueThreeRiver, UpsideGapThreeMethods, UpsideGapTwoCrows, ValueArea, VoltyStop, VolumeByTimeProfile, VolumeOscillator, VolumePriceTrend, VolumeProfile, Vortex, Vwap, VwapStdDevBands, Vwma, Vzo, WaveTrend, Wedge, WeightedClose, WilliamsFractals, WilliamsR, WoodiePivots, YangZhangVolatility, YoyoExit, ZigZag};
|
||||
|
||||
/// Convert a flat `f64` stream into a `Vec<Candle>` by chunking it into
|
||||
/// `[open, high, low, close, volume]` groups. Tuples that fail OHLCV
|
||||
@@ -393,6 +393,10 @@ fuzz_target!(|data: Vec<f64>| {
|
||||
drive(Abcd::new, &candles);
|
||||
|
||||
// --- Fibonacci (multi-output) ---
|
||||
drive(FibTimeZones::new, &candles);
|
||||
drive(FibChannel::new, &candles);
|
||||
drive(FibArcs::new, &candles);
|
||||
drive(FibFan::new, &candles);
|
||||
drive(FibConfluence::new, &candles);
|
||||
drive(GoldenPocket::new, &candles);
|
||||
drive(AutoFib::new, &candles);
|
||||
|
||||
Reference in New Issue
Block a user