feat(indicators): A5a Fibonacci tools (price-level) (#171)

Adds the six price-level Fibonacci tools as a new **Fibonacci** family (catalogue 367 -> 373, twenty-four families). All build on the internal `pattern_swing` ZigZag tracker, are parameter-free (baked 5% swing threshold), and emit `Candle -> struct` outputs via custom Python/Node/WASM bindings.

| Tool | Output |
|------|--------|
| `FibRetracement` | seven levels (0/23.6/38.2/50/61.8/78.6/100%) of the last swing leg |
| `FibExtension` | five extension ratios (127.2/141.4/161.8/200/261.8%) projected beyond the leg |
| `FibProjection` | A-B-C measured-move target zone (61.8/100/161.8/261.8%) |
| `AutoFib` | retracement anchored on the dominant (largest-magnitude) recent leg |
| `GoldenPocket` | the 0.618-0.65 optimal-trade-entry band (low/mid/high) |
| `FibConfluence` | densest cluster of retracement levels across recent legs (price + strength) |

Fully wired: core (100% unit-tested branches), Python/Node/WASM struct bindings, fuzz driver, reference + streaming-vs-batch tests, README/docs counter. The four geometric/time tools (Fan, Arcs, Channel, Time Zones) follow in A5b.

Verification: `cargo test --workspace` green, clippy `-D warnings` clean, node 450 tests, python 760 tests.
This commit is contained in:
kingchenc
2026-06-04 00:47:00 +02:00
committed by GitHub
parent 8115d3b33d
commit 716eb40206
20 changed files with 2615 additions and 18 deletions
@@ -385,6 +385,12 @@ const multi = {
// Family 13: Ichimoku & alternative charts
Ichimoku: { make: () => new wickra.Ichimoku(9, 26, 52, 26), fields: ['tenkan', 'kijun', 'senkouA', 'senkouB', 'chikou'], step: (ind, i) => ind.update(high[i], low[i], close[i]), batch: (ind) => ind.batch(high, low, close) },
HeikinAshi: { make: () => new wickra.HeikinAshi(), fields: ['open', 'high', 'low', 'close'], step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
FibRetracement: { make: () => new wickra.FibRetracement(), fields: ['level0', 'level236', 'level382', 'level500', 'level618', 'level786', 'level1000'], step: (ind, i) => ind.update(high[i], low[i]), batch: (ind) => ind.batch(high, low) },
FibExtension: { make: () => new wickra.FibExtension(), fields: ['level1272', 'level1414', 'level1618', 'level2000', 'level2618'], step: (ind, i) => ind.update(high[i], low[i]), batch: (ind) => ind.batch(high, low) },
FibProjection: { make: () => new wickra.FibProjection(), fields: ['level618', 'level1000', 'level1618', 'level2618'], step: (ind, i) => ind.update(high[i], low[i]), batch: (ind) => ind.batch(high, low) },
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) },
};
for (const [name, d] of Object.entries(multi)) {
+94
View File
@@ -362,6 +362,46 @@ export interface OvernightIntradayReturnValue {
overnight: number
intraday: number
}
export interface FibRetracementValue {
level0: number
level236: number
level382: number
level500: number
level618: number
level786: number
level1000: number
}
export interface FibExtensionValue {
level1272: number
level1414: number
level1618: number
level2000: number
level2618: number
}
export interface FibProjectionValue {
level618: number
level1000: number
level1618: number
level2618: number
}
export interface AutoFibValue {
level0: number
level236: number
level382: number
level500: number
level618: number
level786: number
level1000: number
}
export interface GoldenPocketValue {
low: number
mid: number
high: number
}
export interface FibConfluenceValue {
price: number
strength: number
}
export type SmaNode = SMA
export declare class SMA {
constructor(period: number)
@@ -3832,3 +3872,57 @@ export declare class OvernightIntradayReturn {
isReady(): boolean
warmupPeriod(): number
}
export type FibRetracementNode = FibRetracement
export declare class FibRetracement {
constructor()
update(high: number, low: number): FibRetracementValue | null
batch(high: Array<number>, low: Array<number>): Array<number>
reset(): void
isReady(): boolean
warmupPeriod(): number
}
export type FibExtensionNode = FibExtension
export declare class FibExtension {
constructor()
update(high: number, low: number): FibExtensionValue | null
batch(high: Array<number>, low: Array<number>): Array<number>
reset(): void
isReady(): boolean
warmupPeriod(): number
}
export type FibProjectionNode = FibProjection
export declare class FibProjection {
constructor()
update(high: number, low: number): FibProjectionValue | null
batch(high: Array<number>, low: Array<number>): Array<number>
reset(): void
isReady(): boolean
warmupPeriod(): number
}
export type AutoFibNode = AutoFib
export declare class AutoFib {
constructor()
update(high: number, low: number): AutoFibValue | null
batch(high: Array<number>, low: Array<number>): Array<number>
reset(): void
isReady(): boolean
warmupPeriod(): number
}
export type GoldenPocketNode = GoldenPocket
export declare class GoldenPocket {
constructor()
update(high: number, low: number): GoldenPocketValue | null
batch(high: Array<number>, low: Array<number>): Array<number>
reset(): void
isReady(): boolean
warmupPeriod(): number
}
export type FibConfluenceNode = FibConfluence
export declare class FibConfluence {
constructor()
update(high: number, low: number): FibConfluenceValue | 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
+452
View File
@@ -14030,3 +14030,455 @@ impl OvernightIntradayReturnNode {
self.inner.warmup_period() as u32
}
}
// ============================== Fibonacci ==============================
/// Build a candle for the swing-based Fibonacci tools from a `high`/`low` pair;
/// only the high and low drive the swing tracker, so open/close are the midpoint.
fn swing_cnd(high: f64, low: f64) -> napi::Result<wc::Candle> {
cnd(high, low, f64::midpoint(high, low), 0.0)
}
#[napi(object)]
pub struct FibRetracementValue {
pub level_0: f64,
pub level_236: f64,
pub level_382: f64,
pub level_500: f64,
pub level_618: f64,
pub level_786: f64,
pub level_1000: f64,
}
#[napi(js_name = "FibRetracement")]
pub struct FibRetracementNode {
inner: wc::FibRetracement,
}
#[napi]
impl FibRetracementNode {
#[napi(constructor)]
pub fn new() -> Self {
Self {
inner: wc::FibRetracement::new(),
}
}
#[napi]
pub fn update(&mut self, high: f64, low: f64) -> napi::Result<Option<FibRetracementValue>> {
Ok(self
.inner
.update(swing_cnd(high, low)?)
.map(|o| FibRetracementValue {
level_0: o.level_0,
level_236: o.level_236,
level_382: o.level_382,
level_500: o.level_500,
level_618: o.level_618,
level_786: o.level_786,
level_1000: o.level_1000,
}))
}
#[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 * 7];
for i in 0..n {
if let Some(o) = self.inner.update(swing_cnd(high[i], low[i])?) {
out[i * 7] = o.level_0;
out[i * 7 + 1] = o.level_236;
out[i * 7 + 2] = o.level_382;
out[i * 7 + 3] = o.level_500;
out[i * 7 + 4] = o.level_618;
out[i * 7 + 5] = o.level_786;
out[i * 7 + 6] = o.level_1000;
}
}
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
}
}
#[napi(object)]
pub struct FibExtensionValue {
pub level_1272: f64,
pub level_1414: f64,
pub level_1618: f64,
pub level_2000: f64,
pub level_2618: f64,
}
#[napi(js_name = "FibExtension")]
pub struct FibExtensionNode {
inner: wc::FibExtension,
}
#[napi]
impl FibExtensionNode {
#[napi(constructor)]
pub fn new() -> Self {
Self {
inner: wc::FibExtension::new(),
}
}
#[napi]
pub fn update(&mut self, high: f64, low: f64) -> napi::Result<Option<FibExtensionValue>> {
Ok(self
.inner
.update(swing_cnd(high, low)?)
.map(|o| FibExtensionValue {
level_1272: o.level_1272,
level_1414: o.level_1414,
level_1618: o.level_1618,
level_2000: o.level_2000,
level_2618: o.level_2618,
}))
}
#[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 * 5];
for i in 0..n {
if let Some(o) = self.inner.update(swing_cnd(high[i], low[i])?) {
out[i * 5] = o.level_1272;
out[i * 5 + 1] = o.level_1414;
out[i * 5 + 2] = o.level_1618;
out[i * 5 + 3] = o.level_2000;
out[i * 5 + 4] = o.level_2618;
}
}
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
}
}
#[napi(object)]
pub struct FibProjectionValue {
pub level_618: f64,
pub level_1000: f64,
pub level_1618: f64,
pub level_2618: f64,
}
#[napi(js_name = "FibProjection")]
pub struct FibProjectionNode {
inner: wc::FibProjection,
}
#[napi]
impl FibProjectionNode {
#[napi(constructor)]
pub fn new() -> Self {
Self {
inner: wc::FibProjection::new(),
}
}
#[napi]
pub fn update(&mut self, high: f64, low: f64) -> napi::Result<Option<FibProjectionValue>> {
Ok(self
.inner
.update(swing_cnd(high, low)?)
.map(|o| FibProjectionValue {
level_618: o.level_618,
level_1000: o.level_1000,
level_1618: o.level_1618,
level_2618: o.level_2618,
}))
}
#[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.level_618;
out[i * 4 + 1] = o.level_1000;
out[i * 4 + 2] = o.level_1618;
out[i * 4 + 3] = o.level_2618;
}
}
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
}
}
#[napi(object)]
pub struct AutoFibValue {
pub level_0: f64,
pub level_236: f64,
pub level_382: f64,
pub level_500: f64,
pub level_618: f64,
pub level_786: f64,
pub level_1000: f64,
}
#[napi(js_name = "AutoFib")]
pub struct AutoFibNode {
inner: wc::AutoFib,
}
#[napi]
impl AutoFibNode {
#[napi(constructor)]
pub fn new() -> Self {
Self {
inner: wc::AutoFib::new(),
}
}
#[napi]
pub fn update(&mut self, high: f64, low: f64) -> napi::Result<Option<AutoFibValue>> {
Ok(self
.inner
.update(swing_cnd(high, low)?)
.map(|o| AutoFibValue {
level_0: o.level_0,
level_236: o.level_236,
level_382: o.level_382,
level_500: o.level_500,
level_618: o.level_618,
level_786: o.level_786,
level_1000: o.level_1000,
}))
}
#[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 * 7];
for i in 0..n {
if let Some(o) = self.inner.update(swing_cnd(high[i], low[i])?) {
out[i * 7] = o.level_0;
out[i * 7 + 1] = o.level_236;
out[i * 7 + 2] = o.level_382;
out[i * 7 + 3] = o.level_500;
out[i * 7 + 4] = o.level_618;
out[i * 7 + 5] = o.level_786;
out[i * 7 + 6] = o.level_1000;
}
}
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
}
}
#[napi(object)]
pub struct GoldenPocketValue {
pub low: f64,
pub mid: f64,
pub high: f64,
}
#[napi(js_name = "GoldenPocket")]
pub struct GoldenPocketNode {
inner: wc::GoldenPocket,
}
#[napi]
impl GoldenPocketNode {
#[napi(constructor)]
pub fn new() -> Self {
Self {
inner: wc::GoldenPocket::new(),
}
}
#[napi]
pub fn update(&mut self, high: f64, low: f64) -> napi::Result<Option<GoldenPocketValue>> {
Ok(self
.inner
.update(swing_cnd(high, low)?)
.map(|o| GoldenPocketValue {
low: o.low,
mid: o.mid,
high: o.high,
}))
}
#[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.low;
out[i * 3 + 1] = o.mid;
out[i * 3 + 2] = o.high;
}
}
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
}
}
#[napi(object)]
pub struct FibConfluenceValue {
pub price: f64,
pub strength: f64,
}
#[napi(js_name = "FibConfluence")]
pub struct FibConfluenceNode {
inner: wc::FibConfluence,
}
#[napi]
impl FibConfluenceNode {
#[napi(constructor)]
pub fn new() -> Self {
Self {
inner: wc::FibConfluence::new(),
}
}
#[napi]
pub fn update(&mut self, high: f64, low: f64) -> napi::Result<Option<FibConfluenceValue>> {
Ok(self
.inner
.update(swing_cnd(high, low)?)
.map(|o| FibConfluenceValue {
price: o.price,
strength: o.strength,
}))
}
#[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.price;
out[i * 2 + 1] = o.strength;
}
}
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 FibRetracementNode {
fn default() -> Self {
Self::new()
}
}
impl Default for FibExtensionNode {
fn default() -> Self {
Self::new()
}
}
impl Default for FibProjectionNode {
fn default() -> Self {
Self::new()
}
}
impl Default for AutoFibNode {
fn default() -> Self {
Self::new()
}
}
impl Default for GoldenPocketNode {
fn default() -> Self {
Self::new()
}
}
impl Default for FibConfluenceNode {
fn default() -> Self {
Self::new()
}
}
+14
View File
@@ -339,6 +339,13 @@ from ._wickra import (
Butterfly,
Gartley,
Abcd,
# Fibonacci
FibConfluence,
GoldenPocket,
AutoFib,
FibProjection,
FibExtension,
FibRetracement,
# Microstructure: order book
OrderBookImbalanceTop1,
OrderBookImbalanceTopN,
@@ -734,6 +741,13 @@ __all__ = [
"Butterfly",
"Gartley",
"Abcd",
# Fibonacci
"FibConfluence",
"GoldenPocket",
"AutoFib",
"FibProjection",
"FibExtension",
"FibRetracement",
# Microstructure: order book
"OrderBookImbalanceTop1",
"OrderBookImbalanceTopN",
+454
View File
@@ -49,6 +49,8 @@ const NON_CONTIGUOUS: &str = "array must be C-contiguous; pass np.ascontiguousar
/// `(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`.
type FibExtLevels = (f64, f64, f64, f64, f64);
/// `(pp, r1, r2, s1, s2)` pivot levels returned by Woodie pivots.
type WoodieLevels = (f64, f64, f64, f64, f64);
/// `(tenkan, kijun, senkou_a, senkou_b, chikou)` Ichimoku lines, each optional during warmup.
@@ -17846,6 +17848,451 @@ impl PyOvernightIntradayReturn {
}
}
// ============================== Fibonacci ==============================
/// Build a candle for the swing-based Fibonacci tools from a `high`/`low` pair.
/// Only the high and low drive the swing tracker, so open and close are pinned
/// to the midpoint to keep the OHLC invariants valid.
fn swing_candle(high: f64, low: f64) -> Result<wc::Candle, wc::Error> {
let mid = f64::midpoint(high, low);
wc::Candle::new(mid, high, low, mid, 0.0, 0)
}
#[pyclass(
name = "FibRetracement",
module = "wickra._wickra",
skip_from_py_object
)]
#[derive(Clone)]
struct PyFibRetracement {
inner: wc::FibRetracement,
}
#[pymethods]
impl PyFibRetracement {
#[new]
fn new() -> Self {
Self {
inner: wc::FibRetracement::new(),
}
}
/// Returns `(level_0, …, level_1000)` (seven levels) or None during warmup.
fn update(&mut self, candle: &Bound<'_, PyAny>) -> PyResult<Option<PivotLevels>> {
let c = extract_candle(candle)?;
Ok(self.inner.update(c).map(|o| {
(
o.level_0,
o.level_236,
o.level_382,
o.level_500,
o.level_618,
o.level_786,
o.level_1000,
)
}))
}
/// Batch over numpy columns high, low. Returns shape `(n, 7)`.
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 * 7];
for i in 0..n {
if let Some(o) = self
.inner
.update(swing_candle(h[i], l[i]).map_err(map_err)?)
{
out[i * 7] = o.level_0;
out[i * 7 + 1] = o.level_236;
out[i * 7 + 2] = o.level_382;
out[i * 7 + 3] = o.level_500;
out[i * 7 + 4] = o.level_618;
out[i * 7 + 5] = o.level_786;
out[i * 7 + 6] = o.level_1000;
}
}
Ok(numpy::ndarray::Array2::from_shape_vec((n, 7), 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 {
"FibRetracement()".to_string()
}
}
#[pyclass(name = "FibExtension", module = "wickra._wickra", skip_from_py_object)]
#[derive(Clone)]
struct PyFibExtension {
inner: wc::FibExtension,
}
#[pymethods]
impl PyFibExtension {
#[new]
fn new() -> Self {
Self {
inner: wc::FibExtension::new(),
}
}
/// Returns `(level_1272, level_1414, level_1618, level_2000, level_2618)` or None.
fn update(&mut self, candle: &Bound<'_, PyAny>) -> PyResult<Option<FibExtLevels>> {
let c = extract_candle(candle)?;
Ok(self.inner.update(c).map(|o| {
(
o.level_1272,
o.level_1414,
o.level_1618,
o.level_2000,
o.level_2618,
)
}))
}
/// Batch over numpy columns high, low. Returns shape `(n, 5)`.
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 * 5];
for i in 0..n {
if let Some(o) = self
.inner
.update(swing_candle(h[i], l[i]).map_err(map_err)?)
{
out[i * 5] = o.level_1272;
out[i * 5 + 1] = o.level_1414;
out[i * 5 + 2] = o.level_1618;
out[i * 5 + 3] = o.level_2000;
out[i * 5 + 4] = o.level_2618;
}
}
Ok(numpy::ndarray::Array2::from_shape_vec((n, 5), 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 {
"FibExtension()".to_string()
}
}
#[pyclass(name = "FibProjection", module = "wickra._wickra", skip_from_py_object)]
#[derive(Clone)]
struct PyFibProjection {
inner: wc::FibProjection,
}
#[pymethods]
impl PyFibProjection {
#[new]
fn new() -> Self {
Self {
inner: wc::FibProjection::new(),
}
}
/// Returns `(level_618, level_1000, level_1618, level_2618)` or None during warmup.
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.level_618, o.level_1000, o.level_1618, o.level_2618)))
}
/// 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.level_618;
out[i * 4 + 1] = o.level_1000;
out[i * 4 + 2] = o.level_1618;
out[i * 4 + 3] = o.level_2618;
}
}
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 {
"FibProjection()".to_string()
}
}
#[pyclass(name = "AutoFib", module = "wickra._wickra", skip_from_py_object)]
#[derive(Clone)]
struct PyAutoFib {
inner: wc::AutoFib,
}
#[pymethods]
impl PyAutoFib {
#[new]
fn new() -> Self {
Self {
inner: wc::AutoFib::new(),
}
}
/// Returns `(level_0, …, level_1000)` for the dominant leg, or None during warmup.
fn update(&mut self, candle: &Bound<'_, PyAny>) -> PyResult<Option<PivotLevels>> {
let c = extract_candle(candle)?;
Ok(self.inner.update(c).map(|o| {
(
o.level_0,
o.level_236,
o.level_382,
o.level_500,
o.level_618,
o.level_786,
o.level_1000,
)
}))
}
/// Batch over numpy columns high, low. Returns shape `(n, 7)`.
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 * 7];
for i in 0..n {
if let Some(o) = self
.inner
.update(swing_candle(h[i], l[i]).map_err(map_err)?)
{
out[i * 7] = o.level_0;
out[i * 7 + 1] = o.level_236;
out[i * 7 + 2] = o.level_382;
out[i * 7 + 3] = o.level_500;
out[i * 7 + 4] = o.level_618;
out[i * 7 + 5] = o.level_786;
out[i * 7 + 6] = o.level_1000;
}
}
Ok(numpy::ndarray::Array2::from_shape_vec((n, 7), 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 {
"AutoFib()".to_string()
}
}
#[pyclass(name = "GoldenPocket", module = "wickra._wickra", skip_from_py_object)]
#[derive(Clone)]
struct PyGoldenPocket {
inner: wc::GoldenPocket,
}
#[pymethods]
impl PyGoldenPocket {
#[new]
fn new() -> Self {
Self {
inner: wc::GoldenPocket::new(),
}
}
/// Returns `(low, mid, high)` of the golden-pocket band, or None during warmup.
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.low, o.mid, o.high)))
}
/// 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.low;
out[i * 3 + 1] = o.mid;
out[i * 3 + 2] = o.high;
}
}
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 {
"GoldenPocket()".to_string()
}
}
#[pyclass(name = "FibConfluence", module = "wickra._wickra", skip_from_py_object)]
#[derive(Clone)]
struct PyFibConfluence {
inner: wc::FibConfluence,
}
#[pymethods]
impl PyFibConfluence {
#[new]
fn new() -> Self {
Self {
inner: wc::FibConfluence::new(),
}
}
/// Returns `(price, strength)` of the densest cluster, 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.price, o.strength)))
}
/// 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.price;
out[i * 2 + 1] = o.strength;
}
}
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 {
"FibConfluence()".to_string()
}
}
#[pymodule]
#[allow(clippy::too_many_lines)]
fn _wickra(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> {
@@ -18228,5 +18675,12 @@ fn _wickra(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_class::<PyShark>()?;
m.add_class::<PyCypher>()?;
m.add_class::<PyThreeDrives>()?;
// Fibonacci.
m.add_class::<PyFibRetracement>()?;
m.add_class::<PyFibExtension>()?;
m.add_class::<PyFibProjection>()?;
m.add_class::<PyAutoFib>()?;
m.add_class::<PyGoldenPocket>()?;
m.add_class::<PyFibConfluence>()?;
Ok(())
}
@@ -860,6 +860,36 @@ def test_candle_scalar_streaming_matches_batch(name, ohlcv):
# --- Candle-input, multi-output indicators --------------------------------
MULTI = {
"FibRetracement": (
lambda: ta.FibRetracement(),
lambda ind, h, l, c, v: ind.batch(h, l),
7,
),
"FibExtension": (
lambda: ta.FibExtension(),
lambda ind, h, l, c, v: ind.batch(h, l),
5,
),
"FibProjection": (
lambda: ta.FibProjection(),
lambda ind, h, l, c, v: ind.batch(h, l),
4,
),
"AutoFib": (
lambda: ta.AutoFib(),
lambda ind, h, l, c, v: ind.batch(h, l),
7,
),
"GoldenPocket": (
lambda: ta.GoldenPocket(),
lambda ind, h, l, c, v: ind.batch(h, l),
3,
),
"FibConfluence": (
lambda: ta.FibConfluence(),
lambda ind, h, l, c, v: ind.batch(h, l),
2,
),
"Vortex": (
lambda: ta.Vortex(14),
lambda ind, h, l, c, v: ind.batch(h, l, c),
@@ -2578,6 +2608,50 @@ def test_three_drives_reference():
assert t.update((109.08, 136.0, 109.08, 109.08, 1.0, 4)) == pytest.approx(0.0)
assert t.update((122.4, 134.64, 122.4, 122.4, 1.0, 5)) == pytest.approx(-1.0)
def test_fib_retracement_reference():
t = ta.FibRetracement()
assert t.update((199.8, 200.0, 199.8, 199.8, 1.0, 0)) is None
assert t.update((100.0, 198.0, 100.0, 100.0, 1.0, 1)) is None
assert t.update((101.0, 110.0, 101.0, 101.0, 1.0, 2)) == pytest.approx((100.0, 123.6, 138.2, 150.0, 161.8, 178.6, 200.0))
def test_fib_extension_reference():
t = ta.FibExtension()
assert t.update((199.8, 200.0, 199.8, 199.8, 1.0, 0)) is None
assert t.update((100.0, 198.0, 100.0, 100.0, 1.0, 1)) is None
assert t.update((101.0, 110.0, 101.0, 101.0, 1.0, 2)) == pytest.approx((72.8, 58.6, 38.2, 0.0, -61.8))
def test_fib_projection_reference():
t = ta.FibProjection()
assert t.update((199.8, 200.0, 199.8, 199.8, 1.0, 0)) is None
assert t.update((160.0, 198.0, 160.0, 160.0, 1.0, 1)) is None
assert t.update((161.6, 190.0, 161.6, 161.6, 1.0, 2)) is None
assert t.update((171.0, 188.1, 171.0, 171.0, 1.0, 3)) == pytest.approx((165.28, 150.0, 125.28, 85.28))
def test_auto_fib_reference():
t = ta.AutoFib()
assert t.update((199.8, 200.0, 199.8, 199.8, 1.0, 0)) is None
assert t.update((100.0, 198.0, 100.0, 100.0, 1.0, 1)) is None
assert t.update((101.0, 110.0, 101.0, 101.0, 1.0, 2)) == pytest.approx((100.0, 123.6, 138.2, 150.0, 161.8, 178.6, 200.0))
def test_golden_pocket_reference():
t = ta.GoldenPocket()
assert t.update((199.8, 200.0, 199.8, 199.8, 1.0, 0)) is None
assert t.update((100.0, 198.0, 100.0, 100.0, 1.0, 1)) is None
assert t.update((101.0, 110.0, 101.0, 101.0, 1.0, 2)) == pytest.approx((161.8, 163.4, 165.0))
def test_fib_confluence_reference():
t = ta.FibConfluence()
assert t.update((199.8, 200.0, 199.8, 199.8, 1.0, 0)) is None
assert t.update((100.0, 198.0, 100.0, 100.0, 1.0, 1)) is None
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))
# --- Lifecycle ------------------------------------------------------------
+388
View File
@@ -10629,3 +10629,391 @@ impl WasmOvernightIntradayReturn {
self.inner.warmup_period()
}
}
// ============================== Fibonacci ==============================
/// Candle for the swing-based Fibonacci tools: only high/low drive the tracker,
/// so open/close are pinned to the midpoint.
fn swing_make_candle(high: f64, low: f64) -> Result<wc::Candle, JsError> {
make_candle(high, low, f64::midpoint(high, low), 0.0)
}
#[wasm_bindgen(js_name = FibRetracement)]
pub struct WasmFibRetracement {
inner: wc::FibRetracement,
}
impl Default for WasmFibRetracement {
fn default() -> Self {
Self::new()
}
}
#[wasm_bindgen(js_class = FibRetracement)]
impl WasmFibRetracement {
#[wasm_bindgen(constructor)]
pub fn new() -> WasmFibRetracement {
Self {
inner: wc::FibRetracement::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, &"level0".into(), &o.level_0.into()).ok();
Reflect::set(&obj, &"level236".into(), &o.level_236.into()).ok();
Reflect::set(&obj, &"level382".into(), &o.level_382.into()).ok();
Reflect::set(&obj, &"level500".into(), &o.level_500.into()).ok();
Reflect::set(&obj, &"level618".into(), &o.level_618.into()).ok();
Reflect::set(&obj, &"level786".into(), &o.level_786.into()).ok();
Reflect::set(&obj, &"level1000".into(), &o.level_1000.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 * 7];
for i in 0..n {
if let Some(o) = self.inner.update(swing_make_candle(high[i], low[i])?) {
out[i * 7] = o.level_0;
out[i * 7 + 1] = o.level_236;
out[i * 7 + 2] = o.level_382;
out[i * 7 + 3] = o.level_500;
out[i * 7 + 4] = o.level_618;
out[i * 7 + 5] = o.level_786;
out[i * 7 + 6] = o.level_1000;
}
}
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 = FibExtension)]
pub struct WasmFibExtension {
inner: wc::FibExtension,
}
impl Default for WasmFibExtension {
fn default() -> Self {
Self::new()
}
}
#[wasm_bindgen(js_class = FibExtension)]
impl WasmFibExtension {
#[wasm_bindgen(constructor)]
pub fn new() -> WasmFibExtension {
Self {
inner: wc::FibExtension::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, &"level1272".into(), &o.level_1272.into()).ok();
Reflect::set(&obj, &"level1414".into(), &o.level_1414.into()).ok();
Reflect::set(&obj, &"level1618".into(), &o.level_1618.into()).ok();
Reflect::set(&obj, &"level2000".into(), &o.level_2000.into()).ok();
Reflect::set(&obj, &"level2618".into(), &o.level_2618.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 * 5];
for i in 0..n {
if let Some(o) = self.inner.update(swing_make_candle(high[i], low[i])?) {
out[i * 5] = o.level_1272;
out[i * 5 + 1] = o.level_1414;
out[i * 5 + 2] = o.level_1618;
out[i * 5 + 3] = o.level_2000;
out[i * 5 + 4] = o.level_2618;
}
}
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 = FibProjection)]
pub struct WasmFibProjection {
inner: wc::FibProjection,
}
impl Default for WasmFibProjection {
fn default() -> Self {
Self::new()
}
}
#[wasm_bindgen(js_class = FibProjection)]
impl WasmFibProjection {
#[wasm_bindgen(constructor)]
pub fn new() -> WasmFibProjection {
Self {
inner: wc::FibProjection::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, &"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();
Reflect::set(&obj, &"level2618".into(), &o.level_2618.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.level_618;
out[i * 4 + 1] = o.level_1000;
out[i * 4 + 2] = o.level_1618;
out[i * 4 + 3] = o.level_2618;
}
}
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 = AutoFib)]
pub struct WasmAutoFib {
inner: wc::AutoFib,
}
impl Default for WasmAutoFib {
fn default() -> Self {
Self::new()
}
}
#[wasm_bindgen(js_class = AutoFib)]
impl WasmAutoFib {
#[wasm_bindgen(constructor)]
pub fn new() -> WasmAutoFib {
Self {
inner: wc::AutoFib::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, &"level0".into(), &o.level_0.into()).ok();
Reflect::set(&obj, &"level236".into(), &o.level_236.into()).ok();
Reflect::set(&obj, &"level382".into(), &o.level_382.into()).ok();
Reflect::set(&obj, &"level500".into(), &o.level_500.into()).ok();
Reflect::set(&obj, &"level618".into(), &o.level_618.into()).ok();
Reflect::set(&obj, &"level786".into(), &o.level_786.into()).ok();
Reflect::set(&obj, &"level1000".into(), &o.level_1000.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 * 7];
for i in 0..n {
if let Some(o) = self.inner.update(swing_make_candle(high[i], low[i])?) {
out[i * 7] = o.level_0;
out[i * 7 + 1] = o.level_236;
out[i * 7 + 2] = o.level_382;
out[i * 7 + 3] = o.level_500;
out[i * 7 + 4] = o.level_618;
out[i * 7 + 5] = o.level_786;
out[i * 7 + 6] = o.level_1000;
}
}
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 = GoldenPocket)]
pub struct WasmGoldenPocket {
inner: wc::GoldenPocket,
}
impl Default for WasmGoldenPocket {
fn default() -> Self {
Self::new()
}
}
#[wasm_bindgen(js_class = GoldenPocket)]
impl WasmGoldenPocket {
#[wasm_bindgen(constructor)]
pub fn new() -> WasmGoldenPocket {
Self {
inner: wc::GoldenPocket::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, &"low".into(), &o.low.into()).ok();
Reflect::set(&obj, &"mid".into(), &o.mid.into()).ok();
Reflect::set(&obj, &"high".into(), &o.high.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.low;
out[i * 3 + 1] = o.mid;
out[i * 3 + 2] = o.high;
}
}
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 = FibConfluence)]
pub struct WasmFibConfluence {
inner: wc::FibConfluence,
}
impl Default for WasmFibConfluence {
fn default() -> Self {
Self::new()
}
}
#[wasm_bindgen(js_class = FibConfluence)]
impl WasmFibConfluence {
#[wasm_bindgen(constructor)]
pub fn new() -> WasmFibConfluence {
Self {
inner: wc::FibConfluence::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, &"price".into(), &o.price.into()).ok();
Reflect::set(&obj, &"strength".into(), &o.strength.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.price;
out[i * 2 + 1] = o.strength;
}
}
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()
}
}