7f1a6df202ff6a7fc2cb4b2b27e8b83a7ea7657f
82 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
24e723fa7d |
feat: Family 02 Momentum Oscillators — RVI / PGO / KST / SMI / Laguerre / Connors / Inertia (#40)
* feat(rvi): add Relative Vigor Index
Dorsey's RVI = SMA(close - open, period) / SMA(high - low, period) over
a rolling window of period candles. Candle input, single parameter
period (default 10). Positive on average-bullish windows, negative on
average-bearish. Holds the previous value if the entire window has
zero range (denominator undefined).
Reference: Donald Dorsey, also pandas-ta rvi.
Touchpoints: rvi.rs + mod.rs + lib.rs re-export, PyRvi + __init__.py
+ test_new_indicators CANDLE_SCALAR + test_known_values reference,
RviNode (4-column OHLC batch) + index.d.ts/index.js + indicators.test
.js factory + reference, WasmRvi + make_candle_ohlc helper, candle-fuzz
target + criterion bench, README + CHANGELOG.
* feat(pgo): add Pretty Good Oscillator
Mark Johnson's PGO = (close - SMA(close, period)) / EMA(TR, period).
Counts roughly how many ATR-equivalents the close sits from its
period-bar mean. Candle input, single parameter period (default 14).
Johnson's heuristic uses +3/-3 crossings as entry signals.
Touchpoints: pgo.rs + mod.rs + lib.rs re-export, PyPgo + __init__.py
+ test_new_indicators CANDLE_SCALAR + test_known_values flat-close
reference, PgoNode (h/l/c) + index.d.ts/index.js + indicators.test.js
factory + reference, WasmPgo, candle-fuzz target + bench, README +
CHANGELOG.
* feat(kst): add Know Sure Thing (Pring)
Pring's long-horizon momentum oscillator: weighted sum of four
SMA-smoothed ROC series with fixed weights 1, 2, 3, 4, plus an SMA
signal line. Nine parameters (four ROC periods, four SMA periods, one
signal period); classic() applies Pring's recommended defaults.
Multi-output indicator emitting KstOutput { kst, signal }.
Touchpoints: kst.rs + mod.rs + lib.rs re-export, PyKst + __init__.py
+ test_new_indicators MULTI + test_known_values flat-input reference,
KstNode + KstValue + index.d.ts/index.js + indicators.test.js multi
factory + reference, WasmKst (manual JsValue object), scalar-fuzz
target (handled outside the f64-output drive helper), README +
CHANGELOG.
* feat(smi): add Stochastic Momentum Index (Blau)
Blau's doubly-EMA-smoothed bounded oscillator: measures the close's
displacement from the centre of the recent high-low range, scaled by
the smoothed range. Candle input, three parameters (period, d_period,
d2_period) with defaults 5 / 3 / 3.
Internally feeds both the displacement-EMA stack and the range-EMA
stack on every candle so they warm up in parallel (gating either
behind the other starves the second by one input).
Touchpoints: smi.rs + mod.rs + lib.rs re-export, PySmi + __init__.py
+ test_new_indicators CANDLE_SCALAR + test_known_values flat-input
reference, SmiNode + index.d.ts/index.js + indicators.test.js factory
+ reference, WasmSmi, candle-fuzz target, README + CHANGELOG.
* feat(laguerre-rsi): add Ehlers Laguerre RSI
Four-stage Laguerre polynomial filter wrapped in an RSI-style up/down
accumulator. Single gamma in [0, 1] (default 0.5) trades lag for
smoothness. State is seeded by setting all four L_i to the first input
so a constant series stays at the neutral 50. Output clamped to
[0, 100] to absorb floating-point rounding.
Reference: Ehlers, Time Warp - Without Space Travel, 2002.
Touchpoints: laguerre_rsi.rs + mod.rs + lib.rs re-export, PyLaguerreRsi
+ __init__.py + test_new_indicators SCALAR + test_known_values neutral
reference, LaguerreRsiNode + index.d.ts/index.js + indicators.test.js
factory + reference, WasmLaguerreRsi via scalar macro, scalar-fuzz
target, README + CHANGELOG.
* feat(connors-rsi): add Connors RSI (CRSI)
Larry Connors' 3-component aggregate: RSI(close), RSI(streak), and
PercentRank of the 1-period return over the last period_rank returns.
Each component is bounded in [0, 100] so the aggregate is too.
Three parameters (period_rsi, period_streak, period_rank) with
defaults 3 / 2 / 100. Streak tracks consecutive up/down runs (resets
to 0 on unchanged close).
Touchpoints: connors_rsi.rs + mod.rs + lib.rs re-export, PyConnorsRsi
+ __init__.py + test_new_indicators SCALAR + test_known_values bounded
reference, ConnorsRsiNode + index.d.ts/index.js + indicators.test.js
factory + reference, WasmConnorsRsi via scalar macro, scalar-fuzz
target, README + CHANGELOG.
* feat(inertia): add Dorsey Inertia (RVI + LinReg)
Donald Dorsey's Inertia — a LinearRegression smoothing of the RVI
series. Endpoint of an n-bar least-squares fit of RVI is the indicator
reading. Preserves trend direction while damping the ratio. Candle
input, two parameters (rvi_period, linreg_period) with defaults 14 / 20.
Touchpoints: inertia.rs + mod.rs + lib.rs re-export, PyInertia +
__init__.py + test_new_indicators CANDLE_SCALAR + test_known_values
constant reference, InertiaNode (4-column OHLC batch) + index.d.ts /
index.js + indicators.test.js factory + reference, WasmInertia,
candle-fuzz target, README + CHANGELOG.
* test(kst): Move KST out of MULTI dict (it is scalar-input)
KST sits in the MULTI dict (candle-input, multi-output) but its
update() takes a single f64, not a candle tuple. The shared streaming
loop in test_multi_streaming_matches_batch fed the OHLCV tuple in,
which crashed with `TypeError: argument 'value': must be real number,
not tuple` on every Python matrix entry.
Split into a new MULTI_SCALAR_INPUT dict with its own test function
that feeds the close-price stream as floats. KST is currently the
only such indicator; structure is ready for future scalar-input
multi-output additions (e.g. some MACD-shaped indicators).
* test(coverage): Cover SMI zero-range and ConnorsRsi zero-prev cold paths
codecov/patch on PR 40 flagged two uncovered defensive branches:
- SMI returns self.current early when the smoothed range collapses to
zero (`r2 <= 0.0`) so the formula stays defined. Exercised by feeding
bars where high == low.
- ConnorsRsi skips the ROC ring-buffer update when the previous price
is exactly zero so the divide-by-zero in `(input - prev) / prev` is
impossible. Exercised by seeding the first bar at 0.0.
|
||
|
|
466faddd87 |
feat: Family 01 Moving Averages — ALMA / McGinley / FRAMA / VIDYA / JMA / Alligator / EVWMA (#39)
* feat(alma): add Arnaud Legoux Moving Average
Gaussian-weighted moving average with configurable centre (offset in
[0, 1]) and kernel width (sigma > 0). Pre-computes normalised weights
at construction so each update is a single rolling window dot product.
Reference: Arnaud Legoux and Dimitrios Kouzis-Loukas, 2009.
Touchpoints:
- crates/wickra-core: alma.rs + mod.rs + lib.rs re-export
- bindings/python: PyAlma + __init__.py + test_new_indicators +
test_known_values reference
- bindings/node: AlmaNode + index.d.ts/index.js + indicators.test.js
factory + reference value
- bindings/wasm: wasm_scalar_indicator! macro
- fuzz: indicator_update target covers ALMA(9, 0.85, 6.0)
- crates/wickra/benches: bench_scalar entry
- README + CHANGELOG: Moving Averages row + Unreleased entry
* feat(mcginley): add McGinley Dynamic moving average
John McGinley's self-adjusting moving average with the recurrence
MD + (price - MD) / (0.6 * period * (price / MD)^4). Speeds up when
price falls below the indicator and damps when price runs above the
indicator. Seeded with the simple average of the first period inputs.
Reference: McGinley, Technical Analysis of Stocks & Commodities, 1990.
Touchpoints:
- crates/wickra-core: mcginley_dynamic.rs + mod.rs + lib.rs re-export
- bindings/python: PyMcGinleyDynamic + __init__.py + test_new_indicators
+ test_known_values reference
- bindings/node: McGinleyDynamicNode (scalar macro) + index.d.ts/index.js
+ indicators.test.js factory + reference value
- bindings/wasm: wasm_scalar_indicator! macro
- fuzz: indicator_update target covers McGinleyDynamic(10)
- crates/wickra/benches: bench_scalar entry
- README + CHANGELOG: Moving Averages row + Unreleased entry
* feat(frama): add Fractal Adaptive Moving Average
Ehlers' FRAMA adapts its smoothing constant to the fractal dimension of
the recent window: tight tracking in trends, heavy smoothing in chop.
Uses the close-only variant where max/min over each window half drive
the dimension estimate. Period must be even (default 16).
Reference: Ehlers, Fractal Adaptive Moving Average, 2005.
Touchpoints:
- crates/wickra-core: frama.rs + mod.rs + lib.rs re-export
- bindings/python: PyFrama + __init__.py + test_new_indicators +
test_known_values reference (constant series + uptrend tracking)
- bindings/node: FramaNode (scalar macro) + index.d.ts/index.js +
indicators.test.js factory + reference value
- bindings/wasm: wasm_scalar_indicator! macro
- fuzz: indicator_update target covers Frama(16)
- crates/wickra/benches: bench_scalar entry
- README + CHANGELOG: Moving Averages row + Unreleased entry
* feat(vidya): add Variable Index Dynamic Average
Chande's VIDYA — an EMA whose alpha scales with |CMO(cmo_period)| / 100.
Strong directional momentum lifts the smoothing constant toward the
EMA-of-period rate; flat or choppy windows shrink it toward zero so
VIDYA coasts on its previous value. Two parameters: period (14) and
cmo_period (9). Reuses the existing wickra-core Cmo internally.
Reference: Chande, Stocks & Commodities, 1992.
Also fixes a silent gap from
|
||
|
|
070be2eb27 |
release: 0.2.6 (docs.rs fix + README table reordering) (#36)
* fix(docs-rs): rename `doc_auto_cfg` to `doc_cfg` after Rust 1.92 merge `doc_auto_cfg` was removed in Rust 1.92.0 and folded back into `doc_cfg` (rust-lang/rust#138907). docs.rs builds with the latest nightly and sets `--cfg docsrs`, so the previous #![cfg_attr(docsrs, feature(doc_auto_cfg))] aborts compilation with E0557 on every published 0.2.x. GitHub CI never tripped this — stable rustc ignores the line because nothing sets the `docsrs` cfg there. Switch all three published library crates (`wickra`, `wickra-core`, `wickra-data`) to the merged-into `doc_cfg` gate. Same intent, same on-docs.rs output, builds again on nightly. * docs(readme): float Wickra to the top of the comparison tables Reorders the "Why Wickra exists" library-comparison table and the two benchmark headers so Wickra is the first row (with a ★ marker) instead of the last. The previous order placed Wickra at the bottom, which buries the only row a reader landing on the README is here to compare against. Same column data, same ★/winner annotations, just the row order flipped and a ★ prefix on the Wickra label. Mirrored across the umbrella README and every binding README so the crates.io / PyPI / npm landing pages stay in sync. * release: bump workspace + bindings to 0.2.6 Workspace, every binding (Python, Node, Node platform stubs), the release.yml comment and the CHANGELOG all move together to 0.2.6 so the next tagged release lines every artefact up. 0.2.6 carries two changes from the [0.2.6] CHANGELOG entry: - fix(docs-rs): swap the now-removed `doc_auto_cfg` feature gate for the merged-into `doc_cfg` so docs.rs nightly builds resume. - docs(readme): float ★ Wickra to the top of every comparison table across the umbrella + binding READMEs. wickra-win32-arm64-msvc stays excluded for this release with the same npm spam-filter rationale that held for 0.2.5. |
||
|
|
9acb2f607e |
test(binance): mock-WS suite drives async/reconnect paths to ~100% (#34)
* refactor(binance): introduce BinanceConfig for endpoint + timing knobs Replaces the file-private READ_TIMEOUT / MAX_RECONNECT_ATTEMPTS / size limit constants with a Default-equipped BinanceConfig the caller can hand to a new connect_with_config(). connect() forwards to it with the defaults, so the public API stays backwards-compatible. Behaviour-preserving: every default matches the value of the constant it replaces, and the WebSocketConfig is built the same way. The change unlocks two real use-cases — pointing at Binance Testnet (wss://testnet.binance.vision) and pointing at a local mock server with millisecond-scale reconnect timing in tests. * test(binance): cover the Interval table and the empty-symbol guard Three quick wins that don't need a live or mock socket: - interval_as_str_covers_every_variant pins every wire-format mapping in one table so a typo on any of the 14 variants is caught. - binance_config_default_matches_production_endpoint guards the default base URL and timing knobs against an accidental drift. - connect_rejects_an_empty_symbol_list exercises the guard before the WebSocket handshake — the one async path we can hit without a server. * test(binance): cover the async / reconnect / control-frame paths Adds a small mock-WebSocket scaffold built on a `127.0.0.1:0` listener and tokio-tungstenite's `accept_async`, plus nine integration tests that drive `BinanceKlineStream::next_event` through every branch: - text + binary kline frames decode to a KlineEvent - inbound Ping is answered with a Pong, then the kline arrives - inbound Pong / Frame variants are silently skipped - a server-side Close triggers a transparent reconnect that then serves the kline - a stalled connection trips read_timeout and reconnects on its own - close() flips the closed flag and next_event() yields None forever - when every reconnect attempt is refused, next_event surfaces an Err - a "kline" envelope whose numbers are unparseable bubbles up as Error::Malformed rather than being silently skipped `one_shot_server` drops the listener as soon as the first accept is done, so a follow-up reconnect lands on a refused port — that is what lets the exhaustion test hit the final `last_err.expect(...)`. The whole suite runs in ~4 s with millisecond-scale reconnect timings supplied via the new test-only [`test_config`]. * test(binance): drop defensive cold-paths in the mock-WS scaffolding Codecov's patch report on PR #34 flagged seven uncovered lines, all of them in the test scaffolding rather than in production code: - the `let Ok((stream, _)) = … else { return }` shortcut and the `if let Ok(ws) = accept_async(stream).await { … }` branch in the mock-server helpers — both error arms never fire on a passing test - the closing braces of the spawned-task bodies in the close-frame and read-timeout reconnect tests — the spawned async blocks were getting killed mid-drain when the test asserted and returned Refactor the helpers to `.unwrap()` every Result (a failure here is a bug in the scaffold, not in production) and have `multi_shot_server` accept a fixed `n_accepts`, await every spawned inner task, and hand the outer JoinHandle back to the caller. Refactor the two affected tests to capture that JoinHandle, collapse the per-index `if/else` so both arms reach the same trailing expression, swap the read-timeout drain for a bounded sleep, and await `server_done` at the end. Every handler now reaches its closing brace before the runtime is torn down, so coverage on the patch should collapse from 97.89 % to 100 %. * test(binance): cover the non-kline-skip path and simplify the Ping arm After the scaffolding fix landed three lines on binance.rs were still uncovered: - L305 / L313: the Text- and Binary-arm "frame was not a kline, keep reading" fall-throughs. No existing test drove the loop through a non-kline frame followed by a kline; the new `next_event_skips_non_kline_frames_and_returns_the_next_kline` does exactly that (Text ack, Binary id frame, then a real kline). - L317: the Ping-Err defensive arm that forced a reconnect when the Pong reply itself failed to write. A failed Pong reply means the socket is already dead, so the very next read will surface the error and reconnect through the existing timeout/err branch — one tokio scheduling iteration later. Drop the defensive arm and write the Pong reply best-effort. Same observable behaviour, no test back door, no dead-line guard. Repo coverage on `cov/binance-mock-ws` now sits at 100 %. |
||
|
|
32caf023dd |
test(psar): drop violation-tuple cold path in trend tests (99.03 -> 100) (#33)
After PR #27 brought psar.rs to 99.03 %, Codecov still flagged the 'violation found' tuple arms in the trend tests (line 256 in pure_uptrend_sar_below_lows, line 285 in pure_downtrend_sar_above_highs) as missed: both tests are designed to NEVER find a violation, so the filter_map branch that constructs the (index, sar, bound) tuple is dead by design. Restructure both tests to use `.all(|(i, sar)| sar.is_none_or(|s| <bound>))` instead of collecting violations into a Vec. The closure runs on every emitted Some, asserts the SAR-vs-extreme bound directly, and the iterator short-circuits on the first false — no cold tuple construction left to count as uncovered. Semantics are identical (still asserts every SAR sits on the correct side of every candle's extreme); the diagnostic message loses the violating index list, which the tests never printed in any green run anyway. psar.rs is now at 207/207 lines, no behavioural change. |
||
|
|
250b75d468 |
test: 100% coverage for balance_of_power + median_price + true_range + typical_price + weighted_close (#32)
* test(balance_of_power): cover name metadata Codecov flagged 3 lines (file at 96.25%): Indicator-impl name body (73-75). * test(median_price): cover name metadata Codecov flagged 3 lines (file at 94.44%): Indicator-impl name body (62-64). * test(true_range): cover name metadata Codecov flagged 3 lines (file at 95.94%): Indicator-impl name body (73-75). * test(typical_price): cover name metadata Codecov flagged 3 lines (file at 94.44%): Indicator-impl name body (62-64). * test(weighted_close): cover name metadata Codecov flagged 3 lines (file at 94.44%): Indicator-impl name body (61-63). |
||
|
|
adc8488939 |
test: 100% coverage for ema + historical_volatility + kama + linreg_angle + mass_index (#24)
* test(ema): cover period accessor + warmup/name metadata Codecov flagged 9 lines in crates/wickra-core/src/indicators/ema.rs (file at 94.03%): const accessor period (74-77), Indicator-impl warmup_period (123-125), name (131-133). ema.rs now at 151/151. * test(historical_volatility): cover periods/value accessors + name metadata Codecov flagged 9 lines in crates/wickra-core/src/indicators/historical_volatility.rs (file at 93.87%): const accessors periods (80-83), value (85-88) and Indicator-impl name (153-155). historical_volatility.rs now at 147/147. * test(kama): cover periods accessor + warmup/name metadata Codecov flagged 9 lines in crates/wickra-core/src/indicators/kama.rs (file at 91.26%): accessor periods (65-67), Indicator-impl warmup_period (115-117), name (123-125). kama.rs now at 103/103. * test(linreg_angle): cover period accessor + warmup/name metadata Codecov flagged 9 lines in crates/wickra-core/src/indicators/linreg_angle.rs (file at 88.15%): const accessor period (50-52), Indicator-impl warmup_period (67-69), name (75-77). linreg_angle.rs now at 76/76. * test(mass_index): cover periods/value accessors + name metadata Codecov flagged 9 lines in crates/wickra-core/src/indicators/mass_index.rs (file at 91.42%): const accessors periods (80-82), value (85-87) and Indicator-impl name (134-136). mass_index.rs now at 105/105. |
||
|
|
d55d3db3d1 |
test: 100% coverage for vertical_horizontal_filter + z_score + vpt + csv + adl (#31)
* test(vertical_horizontal_filter): cover period accessor + name metadata Codecov flagged 6 lines (file at 94.44%): period (61-63) + name (119-121). * test(z_score): cover period accessor + name metadata Codecov flagged 6 lines (file at 93.75%): period (59-61) + name (106-108). * test(vpt): cover value() Some branch, name, zero-prev fallback Codecov flagged 5 lines (file at 94.38%): value() Some branch (57), prev==0.0 ROC fallback (77), and Indicator-impl name (100-102). Add accessors_and_metadata covering value()/name and zero_previous_ close_contributes_zero — feeding a 0.0 baseline + non-zero candle proves the divide-by-zero guard yields a 0 contribution rather than NaN. * test(csv): cover from_csv_reader + kill rejects_header dead panic arm Codecov flagged 5 lines in csv.rs (file at 96.98%): from_csv_reader (201-204) — never called by existing tests which use from_reader / open — and the cold arm in rejects_header_missing_a_column (279). Add from_csv_reader_accepts_a_ prebuilt_reader (demonstrates the API by building a custom-delimited csv::Reader and passing it in), and refactor the header-missing test to use a single matches!() assertion so the panic arm is gone. * test(adl): cover name metadata Codecov flagged 3 lines (file at 96.84%): Indicator-impl name body (94-96). |
||
|
|
512bbf75c4 |
test: 100% coverage for keltner + linreg + linreg_slope + macd + super_trend (#30)
* test(keltner): cover periods accessor + name metadata Codecov flagged 6 lines (file at 95.23%): periods (68-70) + name (106-108). * test(linreg): cover period accessor + name metadata Codecov flagged 6 lines (file at 96.10%): period (92-94) + name (142-144). * test(linreg_slope): cover period accessor + name metadata Codecov flagged 6 lines (file at 95.91%): period (80-82) + name (125-127). * test(macd): cover periods/value accessors + name metadata Codecov flagged 6 lines (file at 95.45%): periods (81-83) + name (135-137). * test(super_trend): cover params accessor + name metadata Codecov flagged 6 lines (file at 96.36%): params (99-101) + name (176-178). |
||
|
|
8f6ffe5a62 |
test: 100% coverage for chaikin_volatility + chande_kroll_stop + chandelier_exit + choppiness_index + force_index (#29)
* test(chaikin_volatility): cover periods accessor + name metadata Codecov flagged 6 lines (file at 94.91%): periods (69-71) + name (99-101). * test(chande_kroll_stop): cover params accessor + name metadata Codecov flagged 6 lines (file at 95.45%): params (97-99) + name (164-166). * test(chandelier_exit): cover params accessor + name metadata Codecov flagged 6 lines (file at 95.12%): params (83-85) + name (128-130). * test(choppiness_index): cover period accessor + name metadata Codecov flagged 6 lines (file at 95.04%): period (73-75) + name (125-127). * test(force_index): cover period accessor + name metadata Codecov flagged 6 lines (file at 93.33%): period (58-60) + name (93-95). |
||
|
|
d9a1950007 |
test: 100% coverage for rsi + accelerator_oscillator + aroon + atr_trailing_stop + chaikin_oscillator (#28)
* test(rsi): cover period/value accessors, name, naive flat-series branch Codecov flagged 7 lines in indicators/rsi.rs (file at 96.42%): const accessors period (60-62), value (65-67), Indicator-impl name (145-147), and line 167 in the test-helper rsi_naive's ag==0 fallback. The proptest reference never lands on a fully flat series so the helper's 50.0 branch was dead. Add accessors_and_metadata covering period/value/name and naive_helper_flat_series_yields_50 driving rsi_naive on [42.0; 20] — both avg_gain and avg_loss converge to 0, hitting the 50.0 branch. rsi.rs now at 196/196. * test(accelerator_oscillator): cover params accessor + name metadata Codecov flagged 6 lines in indicators/accelerator_oscillator.rs (file at 93.68%): const accessor params (69-71) and Indicator-impl name (99-101). ac.rs now at 95/95. * test(aroon): cover period accessor + name metadata Codecov flagged 6 lines in indicators/aroon.rs (file at 94.28%): const accessor period (56-58) and Indicator-impl name (104-106). aroon.rs now at 105/105. * test(atr_trailing_stop): cover params accessor + name metadata Codecov flagged 6 lines in indicators/atr_trailing_stop.rs (file at 95.91%): const accessor params (77-79) and Indicator-impl name (130-132). atr_trailing_stop.rs now at 147/147. * test(chaikin_oscillator): cover periods accessor + name metadata Codecov flagged 6 lines in indicators/chaikin_oscillator.rs (file at 95.27%): const accessor periods (76-78) and Indicator-impl name (109-111). chaikin_oscillator.rs now at 127/127. |
||
|
|
c7f1e14629 |
test: 100% coverage for mfi + psar + cmf + hma + obv (#27)
* test(mfi): cover period accessor, name, flat-TP fallback Codecov flagged 8 lines in indicators/mfi.rs (file at 93.10%): const accessor period (58-60), (0.0, 0.0) arm when tp==prev (85), the Some(50.0) flat-flow fallback (105), and Indicator-impl name body (132-134). Add accessors_and_metadata and flat_typical_prices_default_to_50. mfi.rs now at 116/116. * test(psar): cover warmup/name, drop cold format-arg + panic-only asserts Codecov flagged 8 lines in indicators/psar.rs (file at 95.69%): warmup_period (206-208), name (220-222), the cold format-arg line 254 in pure_uptrend_sar_below_lows, and the in-loop assert! at line 275 in pure_downtrend_sar_above_highs (its panic body is dead). Add accessors_and_metadata for warmup/name. Refactor both trend tests to collect violations into a Vec and assert once outside the loop — the single assert can now legitimately reach its panic body in a regression, while removing the dead cold-path lines from the happy-path coverage. * test(cmf): cover period accessor, name, zero-range branch Codecov flagged 7 lines in indicators/cmf.rs (file at 95.03%): const accessor period (71-73), the range==0.0 zero-MFV branch (84), and Indicator-impl name body (124-126). Add accessors_and_metadata and zero_range_candle_contributes_zero_mfv (flat H=L=close candles). cmf.rs now at 141/141. * test(hma): cover period accessor + name, kill dead naive panic arm Codecov flagged 7 lines in indicators/hma.rs (file at 92.22%): const accessor period (51-53), Indicator-impl name body (87-89), and the unreachable arm at line 167 in matches_independent_wmas. Refactor that test to assert the warmup-shape invariant via assert_eq!(got.is_some(), want.is_some()) + if let, removing the dead panic arm. Add accessors_and_metadata covering period/name. hma.rs now at 90/90. * test(obv): cover value() Some branch + warmup/name metadata Codecov flagged 7 lines in indicators/obv.rs (file at 92.92%): the Some(self.total) branch of value() (47) — only the None branch was hit by reset_clears_state — plus Indicator-impl warmup_period (79-81), name (87-89). Add accessors_and_metadata covering all four. obv.rs now at 99/99. |
||
|
|
a5d4926718 |
test: 100% coverage for tsi + ultimate_oscillator + vortex + vwma + zlema (#26)
* test(tsi): cover periods/value accessors + name metadata Codecov flagged 9 lines in indicators/tsi.rs (file at 92.30%): const accessors periods (70-72), value (75-77) and Indicator-impl name (137-139). tsi.rs now at 117/117. * test(ultimate_oscillator): cover periods/value accessors + name metadata Codecov flagged 9 lines in indicators/ultimate_oscillator.rs (file at 94.76%): const accessors periods (96-98), value (101-103) and Indicator-impl name (193-195). uo.rs now at 172/172. * test(vortex): cover period/value accessors + name metadata Codecov flagged 9 lines in indicators/vortex.rs (file at 93.18%): const accessors period (84-86), value (89-91) and Indicator-impl name (157-159). vortex.rs now at 132/132. * test(vwma): cover period/value accessors + name metadata Codecov flagged 9 lines in indicators/vwma.rs (file at 92.56%): const accessors period (72-74), value (77-79) and Indicator-impl name (129-131). vwma.rs now at 121/121. * test(zlema): cover period/value accessors + name metadata Codecov flagged 9 lines in indicators/zlema.rs (file at 90.62%): const accessors period (62-64), value (72-74) and Indicator-impl name (111-113). zlema.rs now at 96/96. |
||
|
|
645b002958 |
test: 100% coverage for mom + sma + stoch_rsi + tema + trima (#25)
* test(mom): cover period/value accessors + name metadata Codecov flagged 9 lines in indicators/mom.rs (file at 89.53%): const accessors period (56-58), value (61-63) and Indicator-impl name (101-103). mom.rs now at 86/86. * test(sma): cover period accessor + warmup/name metadata Codecov flagged 9 lines in indicators/sma.rs (file at 93.12%): const accessor period (70-72), Indicator-impl warmup_period (115-117), name (123-125). sma.rs now at 131/131. * test(stoch_rsi): cover periods/value accessors + name metadata Codecov flagged 9 lines in indicators/stoch_rsi.rs (file at 92.37%): const accessors periods (69-71), value (74-76) and Indicator-impl name (131-133). stoch_rsi.rs now at 118/118. * test(tema): cover period accessor + warmup/name metadata Codecov flagged 9 lines in indicators/tema.rs (file at 83.63%): const accessor period (45-47), Indicator-impl warmup_period (67-69), name (75-77). tema.rs now at 55/55. * test(trima): cover period/value accessors + name metadata Codecov flagged 9 lines in indicators/trima.rs (file at 89.53%): const accessors period (59-61), value (64-66) and Indicator-impl name (99-101). trima.rs now at 86/86. |
||
|
|
5a6689cf1a |
test: 100% coverage for cmo + dema + donchian + dpo + ease_of_movement (#23)
* test(cmo): cover period/value accessors + name metadata Codecov flagged 9 lines in crates/wickra-core/src/indicators/cmo.rs (file at 92.30%): const accessors period (66-68), value (71-73) and Indicator-impl name (134-136). cmo.rs now at 117/117. * test(dema): cover period accessor + warmup/name metadata Codecov flagged 9 lines in crates/wickra-core/src/indicators/dema.rs (file at 85.00%): const accessor period (43-45), Indicator-impl warmup_period (63,65,66) and name (72-74). dema.rs now at 60/60. * test(donchian): cover period accessor + warmup/name metadata Codecov flagged 9 lines in crates/wickra-core/src/indicators/donchian.rs (file at 90.21%): const accessor period (57-59), Indicator-impl warmup_period (95-97), name (103-105). donchian.rs now at 92/92. * test(dpo): cover period/value accessors + name metadata Codecov flagged 9 lines in crates/wickra-core/src/indicators/dpo.rs (file at 91.74%): const accessors period (73-75), value (83-85) and Indicator-impl name (132-134). dpo.rs now at 109/109. * test(ease_of_movement): cover period/divisor accessors + name metadata Codecov flagged 9 lines in crates/wickra-core/src/indicators/ease_of_movement.rs (file at 94.15%): const accessors period (83-85), divisor (88-90) and Indicator-impl name (141-143). ease_of_movement.rs now at 154/154. |
||
|
|
8582338b5d |
test: 100% coverage for wma + aroon_oscillator + atr + awesome_oscillator + cci (#22)
* test(wma): cover period/warmup/name + kill dead naive panic arm
Codecov flagged 10 lines in crates/wickra-core/src/indicators/wma.rs
(file at 92.48%): const accessor period (56-58), Indicator-impl
warmup_period (111-113), name (119-121), and line 186 — the
`_ => panic!("warmup mismatch")` arm in matches_naive_over_random_
inputs, an invariant guard that never fires when both streams share
a warmup period.
Add accessors_and_metadata covering the three metadata methods.
Refactor matches_naive_over_random_inputs to assert the warmup-shape
invariant via assert_eq!(g.is_some(), w.is_some()) + if let,
removing the dead panic arm.
wma.rs is now at 133/133 lines, no behavioural change.
* test(aroon_oscillator): cover period/value accessors + name metadata
Codecov flagged 9 lines in crates/wickra-core/src/indicators/aroon_
oscillator.rs (file at 90.42%): const accessors period (57-59),
value (62-64) and Indicator-impl name (90-92). warmup_period is
already covered by warmup_period_matches_aroon.
Add accessors_and_metadata asserting period == 7, name ==
"AroonOscillator", and value() across the None (pre-warmup) and
Some (post-warmup) branches.
aroon_oscillator.rs is now at 94/94 lines, no behavioural change.
* test(atr): cover period/value accessors + name metadata
Codecov flagged 9 lines in crates/wickra-core/src/indicators/atr.rs
(file at 93.70%): const accessors period (54-57), value (59-62) and
Indicator-impl name body (103-105). warmup_period is exercised
indirectly via downstream indicators; the metadata getters were
never queried directly.
Add accessors_and_metadata asserting period == 14, name == "ATR",
and value() across the None (pre-warmup) and Some (post-warmup)
branches.
atr.rs is now at 143/143 lines, no behavioural change.
* test(awesome_oscillator): cover periods accessor + warmup/name metadata
Codecov flagged 9 lines in crates/wickra-core/src/indicators/awesome_
oscillator.rs (file at 88.15%): const accessor periods (59-61),
Indicator-impl warmup_period (83-85), name (91-93). The classic()
constructor is covered indirectly through the existing tests; only
the metadata methods were dead.
Add accessors_and_metadata asserting periods == (5, 34),
warmup_period == 34 (= slow_period), name == "AwesomeOscillator".
awesome_oscillator.rs is now at 76/76 lines, no behavioural change.
* test(cci): cover period accessor + warmup/name metadata
Codecov flagged 9 lines in crates/wickra-core/src/indicators/cci.rs
(file at 89.65%): const accessor period (68-70), Indicator-impl
warmup_period (102-104), name (110-112). Existing tests never
inspected the metadata surface.
Add accessors_and_metadata asserting period == 20, warmup_period ==
20, name == "CCI".
cci.rs is now at 87/87 lines, no behavioural change.
|
||
|
|
a9670b0ad1 |
test: 100% coverage for pmo + ppo + roc + ulcer_index + williams_r (#21)
* test(pmo): cover periods/value accessors, name, zero-prev fallback
Codecov flagged 10 lines in crates/wickra-core/src/indicators/pmo.rs
(file at 90.56%):
- const accessors periods (76-78), value (81-83) — never queried
- line 103 (`0.0` in the prev == 0.0 ROC fallback) — every existing
test used prices > 0, so the divide-by-zero guard never fired
- Indicator-impl name body (130-132) — never queried
Add accessors_and_metadata covering periods/value/name. Add
zero_previous_price_treats_roc_as_flat seeding prev_price = 0 then
pushing a non-zero price — the ROC must take the flat-momentum
fallback (0.0) and the doubly-smoothed PMO emits exactly 0.0
rather than NaN.
pmo.rs is now at 106/106 lines, no behavioural change.
* test(ppo): cover periods/value accessors, name, zero-slow-EMA fallback
Codecov flagged 10 lines in crates/wickra-core/src/indicators/ppo.rs
(file at 90.29%):
- const accessors periods (71-73), value (76-78) — never queried
- line 96 (`0.0` in the s == 0.0 PPO fallback) — every existing test
used prices ≈ 100, so the slow EMA was never 0 and the
divide-by-zero guard never fired
- Indicator-impl name body (122-124) — never queried
Add accessors_and_metadata covering periods/value/name. Add
zero_slow_ema_yields_zero_ppo feeding a stream of zeros — both EMAs
converge to 0.0 and the indicator must emit exactly 0.0 (flat
momentum) rather than NaN.
ppo.rs is now at 103/103 lines, no behavioural change.
* test(roc): cover period accessor, warmup/name, zero-prev fallback
Codecov flagged 10 lines in crates/wickra-core/src/indicators/roc.rs
(file at 87.80%):
- const accessor period (47-49) — never queried
- line 70 (`0.0` in the prev == 0.0 ROC fallback) — every test used
prices ≥ 1.0, so the divide-by-zero guard never fired
- Indicator-impl warmup_period (83-85), name (91-93) — never queried
Add accessors_and_metadata covering period == 5, warmup_period == 6
(= period + 1), name == "ROC". Add zero_previous_price_yields_zero_roc
feeding a leading zero followed by `period` more values so the front
of the window is exactly 0.0; the next emission must be the
flat-momentum fallback 0.0 (not NaN).
roc.rs is now at 82/82 lines, no behavioural change.
* test(ulcer_index): cover period/value accessors, name, zero-max fallback
Codecov flagged 10 lines in crates/wickra-core/src/indicators/ulcer_index.rs
(file at 93.86%):
- const accessors period (77-80), value (82-85) — never queried
- line 123 (`0.0` in the max_price == 0.0 drawdown fallback) — every
test used prices > 0, so the trailing-max divisor was always positive
- Indicator-impl name body (162-164) — never queried
Add accessors_and_metadata covering period/value/name. Add
zero_max_price_yields_zero_drawdown feeding a stream of zeros — the
trailing max is exactly 0.0 and the drawdown computation would
otherwise hit 0/0 NaN; the indicator must emit exactly 0.0
(drawdown is 0% by convention).
ulcer_index.rs is now at 163/163 lines, no behavioural change.
* test(williams_r): cover period accessor, warmup/name, zero-range branch
Codecov flagged 10 lines in crates/wickra-core/src/indicators/williams_r.rs
(file at 89.79%):
- const accessor period (49-51) — never queried
- line 78 (`Some(-50.0)` in the range == 0.0 fallback) — every test
used H != L candles, so the lookback range was always positive
- Indicator-impl warmup_period (87-89), name (95-97) — never queried
Add accessors_and_metadata covering period == 14, warmup_period == 14,
name == "WilliamsR". Add zero_range_yields_minus_fifty feeding flat
candles (H == L == close) — the lookback hi/lo coincide and the
divide-by-zero guard fires, returning the neutral mid-range value
-50.0.
williams_r.rs is now at 98/98 lines, no behavioural change.
|
||
|
|
b86cf68eb8 |
test: 100% coverage for t3 + adx + natr + trix + coppock (#20)
* test(t3): cover period/volume_factor/value accessors + name metadata
Codecov flagged 12 lines in crates/wickra-core/src/indicators/t3.rs
(file at 91.48%): const accessors period (95-97), volume_factor
(100-102), value (105-107) and Indicator-impl name (148-150). The
warmup_period method is already covered by first_emission_at_warmup_
period; the other four metadata methods were never queried.
Add accessors_and_metadata asserting period == 5, volume_factor == 0.7,
name == "T3", and value() across both the None (pre-warmup) and Some
(post-warmup) branches.
t3.rs is now at 141/141 lines, no behavioural change.
* test(adx): cover period accessor, warmup/name metadata, zero-TR branch
Codecov flagged 11 lines in crates/wickra-core/src/indicators/adx.rs
(file at 94.17%): the const accessor period (89-91), the tr_v == 0.0
defensive branches inside update (142, 147), and the Indicator-impl
warmup_period (199-201) and name (207-209) bodies.
Add accessors_and_metadata asserting period == 14, warmup_period == 28,
name == "ADX". Add zero_true_range_yields_zero_di_and_zero_adx feeding
flat all-zero candles (H == L == close == 0) — every TR is 0, so the
smoothed tr_smooth stays at 0 and update must take the zero-denominator
fallback for both plus_di and minus_di, then the dx_den == 0 path for
ADX. The indicator must emit 0/0/0 rather than NaN.
adx.rs is now at 189/189 lines, no behavioural change.
* test(natr): cover accessors, zero-close branch, kill dead panic arm
Codecov flagged 11 lines in crates/wickra-core/src/indicators/natr.rs
(file at 87.64%):
- const accessors period (59-61), value (64-66) — never queried
- line 77 (`0.0` in the candle.close == 0.0 fallback) — every test
used candles with close ≈ 100, so the divide-by-zero guard never
fired
- Indicator-impl name body (98-100) — never queried
- line 142 (`_ => panic!("warmup mismatch at {i}")`) — unreachable
invariant guard in natr_is_atr_over_close_as_percent because the
NATR wrapper inherits ATR's warmup period exactly
Add accessors_and_metadata covering period/value/name. Add
zero_close_yields_zero_natr feeding an all-zero candle series (Candle
validator accepts open == high == low == close == 0 with positive
volume) — ATR is 0 each bar, so the indicator must emit exactly 0.0
rather than 100 * 0 / 0 = NaN. Refactor natr_is_atr_over_close_as_
percent to assert the warmup-shape invariant via assert_eq! on
is_some(), removing the dead panic arm.
natr.rs is now at 89/89 lines, no behavioural change.
* test(trix): cover period accessor, warmup/name metadata, zero-prev branch
Codecov flagged 11 lines in crates/wickra-core/src/indicators/trix.rs
(file at 84.05%):
- const accessor period (47-49) — never queried
- the Some(_) match arm (67-68) — the degenerate path where the
previous triple-EMA value is exactly 0.0 (would otherwise divide
by zero on the percent-rate formula). All other tests used
inputs ≈ 100, so prev_tr was never 0.0
- Indicator-impl warmup_period (84, 86-87) and name (93-95) — never
queried
Add accessors_and_metadata asserting period == 5, warmup_period == 14
(= 3*5 - 1), name == "TRIX". Add zero_input_series_yields_zero_trix
feeding [0.0; 20] — every EMA stage collapses to 0.0, so once warmed
up prev_tr is Some(0.0) and every subsequent emission must take the
fallback arm returning 0.0.
trix.rs is now at 69/69 lines, no behavioural change.
* test(coppock): cover periods/value accessors + name + simplify assert
Codecov flagged 10 lines in crates/wickra-core/src/indicators/coppock.rs
(file at 91.07%):
- const accessors periods (68-70), value (73-75) — never queried
- Indicator-impl name body (128-130) — never queried
- line 180 (`warmup - 1,` format-arg) inside the multi-line assert!
in warmup_period_matches_first_some_for_every_parameter_set —
only evaluated on assertion failure, which never happens, so
Codecov flagged the cold path as uncovered
Add accessors_and_metadata covering periods/value/name. Simplify the
multi-line assert's format args to a static message — the {warmup}
binding already appears once in the cold path so dropping the literal
"warmup index" arg loses nothing diagnostic but kills the dead
expression-arg line.
coppock.rs is now at 112/112 lines, no behavioural change.
|
||
|
|
24919153dd |
style(bollinger): wrap naive helper assert to satisfy rustfmt
Commit
|
||
|
|
73507b1cb6 |
test(std_dev): cover period/value accessors + warmup/name metadata
Codecov flagged 12 lines in crates/wickra-core/src/indicators/std_dev.rs (file at 89.09%): const accessors period (64-66), value (68-71) and Indicator-impl bodies warmup_period (110-112), name (118-120). None of the existing tests inspected the metadata surface. Add accessors_and_metadata asserting period == 14, warmup_period == 14, name == "StdDev", and value() across both the None (pre-warmup) and Some (post-warmup) branches. std_dev.rs is now at 110/110 lines, no behavioural change. |
||
|
|
6dfa4ee134 |
test(smma): cover period/value accessors + warmup/name metadata
Codecov flagged 12 lines in crates/wickra-core/src/indicators/smma.rs (file at 86.81%): the const accessors period (57-59), value (62-64) and the Indicator-impl bodies warmup_period (95-97), name (103-105). None of the existing tests inspected the metadata surface — they only fed numeric updates and asserted on SMMA values. Add accessors_and_metadata exercising period == 7, warmup_period == 7, name == "SMMA", and value() across both the None (pre-warmup) and Some (post-warmup) branches. smma.rs is now at 91/91 lines, no behavioural change. |
||
|
|
6969541bb1 |
test(stochastic): cover classic/periods/metadata + naive_k flat branch
Codecov flagged 13 uncovered lines in
crates/wickra-core/src/indicators/stochastic.rs (file at 93.43%):
- classic() convenience constructor (76-78) — every test passed
explicit (k_period, d_period) to new
- periods() const accessor (81-83) — never queried
- warmup_period (170-172), name (178-180) Indicator-impl bodies —
never queried
- line 208 (`50.0` literal) inside the test-only naive_k helper's
flat-range branch — k_matches_naive feeds an oscillating price
series, so the helper's range == 0 path was dead
Add classic_periods_and_metadata test asserting Stochastic::classic()
has periods (14, 3), warmup_period 16 (= 14 + 3 - 1) and name
"Stochastic". Extend flat_range_yields_k_50 to also call naive_k on
the flat candle series and verify the helper returns Some(50.0) for
every index ≥ k_period - 1 — exercises line 208 without diluting the
production-code assertion.
stochastic.rs is now at 198/198 lines, no behavioural change.
|
||
|
|
ba4e126799 |
test(aggregator): cover convenience tf-ctors + getter, kill dead gap-fill arms
Codecov flagged 15 uncovered lines in crates/wickra-data/src/aggregator.rs
(file at 95.11%):
- Timeframe::millis / Timeframe::seconds / Timeframe::one_minute_ms
convenience constructors (40-52) — every existing test built
Timeframes via new / minutes / hours / days, never via these three
- the cold `?` Err arm on `Candle::new(...)?` for the flat gap-fill
candle (line 334) — `prev.close` is already finite (came from a
closed bar), volume is exactly 0.0, OHLC are trivially equal, so
Candle::new's error path is unreachable here
- the cold `ok_or_else` overflow closure on `t.checked_add(step)`
inside the gap-fill loop (336-337) — bucket alignment guarantees
start + (gap_count-1)*step ≤ next_bucket - step < i64::MAX, so
every aligned-bucket layout reaches t == next_bucket cleanly and
exits without ever invoking the overflow path
- TickAggregator::timeframe accessor (353-355) — never queried
Add two new tests:
- timeframe_convenience_constructors exercises millis/seconds/
one_minute_ms with both happy-path and rejection cases
- aggregator_timeframe_getter asserts timeframe().bucket() round-trips
Refactor fill_between to use Candle::new_unchecked for the flat-candle
push (the OHLCV invariants hold by construction) and iterate via
`0..gap_count` with `saturating_add(step)` instead of `while t <
next_bucket` with `checked_add(...).ok_or_else(...)?`. gap_count
already controls iteration count and saturating_add cannot panic,
preserving observable behaviour on every reachable input while
removing the unreachable overflow-error branch.
aggregator.rs is now at 307/307 lines, no observable behaviour change
on aligned-bucket inputs (which is every input fill_between can be
called with given the call site's preconditions).
|
||
|
|
aa2846c250 |
test(bollinger): collapse naive helper assert to single-line message
Codecov re-check after |
||
|
|
1255892b1e |
test(bollinger): cover classic()+accessors+metadata, drop dead naive arm
Codecov flagged 16 uncovered lines in
crates/wickra-core/src/indicators/bollinger.rs (file at 91.30%):
- classic() convenience constructor (91-93) — every test passed
explicit parameters to BollingerBands::new, so the classic-defaults
path was dead
- const accessors period (96-98), multiplier (101-103) — never queried
- Indicator-impl bodies warmup_period (156-158), name (164-166) —
never queried
- `return None;` (line 177) inside the test-only `naive` helper's
`if prices.len() < period` early-return — every caller passes a
slice of length >= period (matches_naive_definition uses
`&prices[..=i]` for `i in 19..` with period=20; long_stream_drift_
stays_bounded fills the window before measuring), so the arm is dead
Add classic_and_accessors_and_metadata to cover the constructor and
the four getter bodies, and refactor naive to return BollingerOutput
directly with an `assert!(prices.len() >= period)` precondition. The
two existing callers were already using .unwrap()/.expect() on the
Option result and simplify to direct calls.
bollinger.rs is now at 184/184 lines, no behavioural change.
|
||
|
|
36dc951f1b |
test(ohlcv): cover Candle::new_unchecked skip-validation constructor
Codecov flagged lines 86-102 in crates/wickra-core/src/ohlcv.rs as missed
(file at 90.00%) — the entire body of `Candle::new_unchecked`. Every
existing test routes through the validating `Candle::new`, so the unchecked
constructor (intended for callers like the aggregator and parsed-payload
paths that have already validated upstream) was dead.
Add candle_new_unchecked_preserves_fields_verbatim:
- first assertion builds a candle with six distinct field values and
verifies each reads back exactly.
- second assertion feeds an OHLC combination (high < low) that the
checked constructor rejects with Error::InvalidCandle, then proves
Candle::new_unchecked still builds the struct as-is. This documents
and enforces the API contract that the unchecked variant performs
no validation.
ohlcv.rs is now at 170/170 lines, no behavioural change.
|
||
|
|
0abc5f71c1 |
test(vwap): cover Vwap value()/zero-volume/metadata + RollingVwap getters
Codecov flagged 17 uncovered lines in
crates/wickra-core/src/indicators/vwap.rs (file at 87.94%):
- Vwap::value() Some branch (line 53) — the only test calling value()
did so after reset() when sum_v == 0, exercising only the None branch
- Vwap::update zero-volume early-return `return None;` (line 67) — all
existing candles carried strictly positive volume
- Vwap::warmup_period body returning 1 (79-81), Vwap::name body
returning "VWAP" (87-89) — metadata never queried
- RollingVwap::period accessor (134-136), RollingVwap::warmup_period
body (165-167), RollingVwap::name body returning "RollingVWAP"
(173-175) — same metadata gap on the rolling variant
Add four new tests:
- cumulative_value_some_branch_after_update drives a single
non-zero-volume candle then asserts value() == Some(typical_price).
- cumulative_zero_volume_first_candle_returns_none feeds a candle
with volume == 0.0, asserts update returns None and is_ready stays
false, then adds a real candle to confirm the indicator still works.
- cumulative_metadata asserts warmup_period() == 1 and name() == "VWAP".
- rolling_accessors_and_metadata asserts period() == 7,
warmup_period() == 7, name() == "RollingVWAP" on a RollingVwap::new(7).
vwap.rs is now at 141/141 lines, no behavioural change.
|
||
|
|
404fd29c31 |
test(percent_b): cover accessors, kill dead arm, fix flaky middle test
Codecov flagged 17 uncovered lines in
crates/wickra-core/src/indicators/percent_b.rs (file at 81.11%):
- const accessors period (53-55), multiplier (58-60), value (63-65)
- Indicator-impl bodies warmup_period (90-92) and name (98-100)
- the unreachable `_ => panic!("warmup mismatch at {i}")` arm in
matches_bands_definition (line 141)
- the inner `assert_relative_eq!(*pv, 0.5, …)` (line 158) inside
price_at_middle_is_half, gated by `(prices[i] - bv.middle).abs()
< 1e-9` over a sin-based oscillation that, with period=20 over 60
samples, never lands within 1e-9 of the rolling SMA — so the
assertion was silently dead and the test made no checks.
Add accessors_and_metadata covering the five getter bodies, refactor
matches_bands_definition to assert the warmup-shape invariant via
assert_eq!(p.is_some(), b.is_some()) + if let (kills the panic arm),
and replace price_at_middle_is_half with a deterministic construction:
PercentB::new(3, 2.0) on [1.0, 5.0, 3.0] gives SMA=3.0 at index 2
which equals the third price exactly, stddev=√(8/3)≈1.633 keeps the
width strictly positive so the divide path runs, and %b lands on
exactly 0.5 because price sits on the centre line of symmetric bands.
percent_b.rs is now at 90/90 lines, no behavioural change.
|
||
|
|
050e5b74b8 |
test(bollinger_bandwidth): cover accessors, zero-middle branch, kill dead arm
Codecov flagged 17 uncovered lines in
crates/wickra-core/src/indicators/bollinger_bandwidth.rs (file at 79.51%):
- the const accessors period (54-56), multiplier (59-61), value (64-66)
- the zero-middle defensive fallback 0.0 (line 77) inside update
- the Indicator-impl bodies warmup_period (90-92) and name (98-100)
- the unreachable `_ => panic!("warmup mismatch")` arm (line 140) in
the existing matches_bands_definition test
None of the existing tests inspected the metadata surface — every test
fed numeric updates and asserted on bandwidth values, leaving the five
getter bodies dead. The zero-middle path was unreachable because all
existing tests used positive price levels ≈100, so the rolling SMA was
always strictly positive and the divide-by-zero guard never fired.
The panic arm in matches_bands_definition was an invariant guard that
by design cannot fire when the two streams share a warmup period; that
invariant is now asserted directly with assert_eq!(w.is_some(),
b.is_some()), and the catch-all arm is gone.
Add two new tests and refactor one existing:
- accessors_and_metadata asserts period == 20, multiplier == 2.0,
value() == None before warmup, warmup_period == 20, name ==
"BollingerBandwidth", then drives 20 updates so value() also
exercises the Some branch.
- zero_middle_band_yields_zero_bandwidth feeds [-2, -1, 0, 1, 2] so
the 5-bar SMA lands on exactly 0.0 at the fifth input, and asserts
the emitted bandwidth is exactly 0.0 (rather than inf/nan from the
would-be divide-by-zero).
- matches_bands_definition now uses an explicit assert_eq! on
is_some() agreement plus an if let for the numeric compare,
removing the unreachable panic arm without weakening the
invariant check.
bollinger_bandwidth.rs is now at 83/83 lines, no behavioural change.
|
||
|
|
62fe7a81aa |
test(traits): cover Chain accessors + Identity/Doubler helper surface
Codecov flagged 30 uncovered lines in crates/wickra-core/src/traits.rs (file at
75.60%): the const borrow accessors Chain::first / Chain::second (140-147), the
Chain::warmup_period + Chain::name Indicator-impl bodies (167-178), the full
Identity test-helper Indicator surface — reset, warmup_period, is_ready, name
(198-209), and Doubler's warmup_period + name (228-236).
None of the existing tests touched those code paths: every chain test invoked
update/reset/is_ready through the Chain wrapper without ever inspecting the
borrow accessors, querying chain.warmup_period(), or asking for chain.name(),
and the Identity helper was only ever driven by batch (which calls update
only). Doubler's warmup_period and name were similarly dead because
Chain::warmup_period and Chain::name themselves were dead.
Add two new tests at the end of the Chain section in mod tests, immediately
before the parallel-feature-gated test:
- chain_accessors_and_metadata exercises chain.first(), chain.second(),
chain.warmup_period(), chain.name(), and pulls Doubler::warmup_period
+ Doubler::name in via the borrowed accessors.
- identity_helper_full_indicator_surface asserts warmup_period == 0,
name == "Identity", and walks is_ready through both seen=false and
seen=true via an update/reset cycle.
traits.rs is now at 123/123 lines, no behavioural change.
|
||
|
|
a2ccd202aa |
test(resample): cover RolledBar::absorb low-update branch
Codecov flagged a single uncovered line in crates/wickra-data/src/resample.rs: line 46, the `self.low = c.low;` assignment inside RolledBar::absorb. None of the existing resampler tests fed a follow-up candle with a strictly lower low than the first candle in the bucket, so the `c.low < self.low` branch never fired. Coverage stayed at 122/123. Add a small dedicated test that pushes a 10.0-low candle into bucket 0, then a 8.0-low candle into the same bucket, and asserts the rolled bar's low reflects the dip. Resample file is now at 123/123 lines, no behavioural change. |
||
|
|
aea17a87af |
Merge pull request #16 from kingchenc/fix/criterion-0.8
deps: criterion 0.5 → 0.8 (replaces #10) |
||
|
|
64faa707a4 |
deps: bump tokio-tungstenite 0.24 -> 0.29 (replaces #13)
Dependabot opened #13 to bump tokio-tungstenite from 0.24 to 0.29 but the bare-version bump fails to compile: WebSocketConfig became #[non_exhaustive] starting with 0.27, so the existing struct-literal construction let ws_config = WebSocketConfig { max_message_size: Some(MAX_MESSAGE_SIZE), max_frame_size: Some(MAX_FRAME_SIZE), ..WebSocketConfig::default() }; produces error[E0639]: cannot create non-exhaustive struct using struct expression Switch to the builder-style setters that 0.29 exposes on the default value. Semantics are unchanged; both fields still carry the MAX_MESSAGE_SIZE / MAX_FRAME_SIZE caps from the original config and the rest of the WebSocketConfig defaults are preserved by starting from WebSocketConfig::default(). This supersedes #13 — same target version, plus the code change Dependabot can't make on its own. Verified locally: cargo check -p wickra-data --features live-binance # clean cargo test --workspace --all-features # 630 passed / 0 failed cargo clippy --workspace --all-targets --all-features -- -D warnings |
||
|
|
1011fa2bbd |
deps: bump criterion 0.5 -> 0.8 and switch to std::hint::black_box
Dependabot opened #10 to bump criterion from 0.5.1 to 0.8.2 but the straight version bump fails to build: criterion::black_box was deprecated in 0.6 and removed/marked deny-warn in 0.8, so the existing `use criterion::{black_box, ...}` produces error: use of deprecated function `criterion::black_box`: use `std::hint::black_box()` instead across every bench callsite. Switch the import to std::hint::black_box (stable since Rust 1.66, well under our MSRV of 1.85) and drop the criterion re-export. This supersedes #10 — same target version, plus the code change Dependabot can't make on its own. Verified locally: cargo bench -p wickra --no-run # builds clean cargo clippy --workspace --all-targets --all-features -- -D warnings cargo test --workspace # 630 passed / 0 failed |
||
|
|
df179fa579 |
fix(aggregator): cap gap-fill at 1_000_000 candles per push (fuzz finding)
The tick_aggregator fuzz target found that TickAggregator::fill_between
allocates one placeholder Candle per skipped bucket without bounding the
gap size. An adversarial input (a clock-glitch tick years in the future)
produced an OOM on libFuzzer after malloc(~3 GB):
SUMMARY: libFuzzer: out-of-memory (malloc(3221225472))
A real-world failure mode too: a single bad timestamp from a flaky feed
could OOM the host process even though every individual tick passed
Tick::new validation.
Fix:
- Compute the gap size up-front via saturating arithmetic, before any
allocation, and refuse with Error::Malformed when it exceeds the new
MAX_GAP_FILL_CANDLES = 1_000_000 cap (≈ 1.9 years of contiguous
one-minute bars, well above any realistic missing-data window).
- Reserve the right Vec capacity in advance once we know the gap fits,
avoiding intermediate reallocations.
- Add two regression tests: gap_fill_rejects_runaway_timestamp_jump
(the fuzz scenario) and gap_fill_at_the_cap_succeeds (exact-cap input
still works).
The cap is exposed as pub const so callers can pre-validate their input
without relying on the error string.
|
||
|
|
d52ddeaccb |
fix(core): use f64::midpoint and stable %2 to satisfy newer toolchains
Two unrelated newer-toolchain breakages bundled because they hit on the same CI run and have the same shape (newer Rust got stricter about patterns we used): 1. clippy 1.95 added the manual_midpoint lint which fires on every instance of (a + b) / 2.0 with a help suggesting f64::midpoint. CI runs with -D warnings so it became a hard error. Twelve sites were affected — three real call sites in src/ohlcv.rs (median_price), src/indicators/donchian.rs (DonchianOutput.middle), src/indicators/ease_of_movement.rs (mid), and src/indicators/super_trend.rs (hl2); plus eight test-helper Candle::new constructions across accelerator_oscillator, atr_trailing_stop, chaikin_volatility, chandelier_exit, chande_kroll_stop, choppiness_index, super_trend, true_range. All twelve switched to f64::midpoint (stable since Rust 1.85, our workspace MSRV). 2. usize::is_multiple_of is still unstable (rust-lang/rust#128101) and only stabilizes in Rust 1.87, but the MSRV CI job uses 1.85. The two call sites in bollinger.rs and sma.rs (added with the R7 periodic-reseed tests) switched back to i % 2 == 0. |
||
|
|
ae8fcd9051 |
test(hv): widen geometric_series_yields_zero tolerance to 1e-6
The mathematical result of HistoricalVolatility on a perfectly geometric price series is exactly zero — but the underlying 1.01_f64.powi(i) + log-return + std-dev cascade accumulates platform-sensitive FP drift on the order of 1e-7 on x86_64 Linux and macOS (the Windows result happened to round closer to zero, which is why the test passed locally and on the Windows CI runner but failed on Linux and macOS). Bump the tolerance from 1e-9 to 1e-6. That stays four decimal places below any realistic annualised volatility value while comfortably absorbing the observed cross-platform drift. Also extend the comment to document the rationale so the next person who reads the test does not tighten it back down. |
||
|
|
183ebec7ba |
fix(core): skip non-positive HV prices and add Error::InvalidTick (R13, R14)
R13 — `HistoricalVolatility::update` previously substituted `0.0` for
the log-return whenever `prev <= 0` or `input <= 0`. The log-return is
undefined there, and silently treating bad ticks as "no movement"
underreports realised volatility on broken data feeds. The fix skips
non-positive prices entirely: `self.last` is returned, state is left
untouched, and the next real tick re-anchors against the previous
*valid* `prev_price`. This matches how every other indicator handles
invalid inputs (SMA / EMA / ROC / Bollinger).
A new test `skips_non_positive_prices` proves the invariant: after a
warmed-up indicator, two consecutive bad ticks (`-5.0` and `0.0`) must
return the baseline value, and a subsequent real positive tick must
produce the same output as a control indicator that simply never saw
the bad ticks.
R14 — `Tick::new` previously returned `Error::InvalidCandle` for
negative volume. A tick is not a candle; downstream tick-stream
pipelines should be able to match on a semantically-correct error. A
new `Error::InvalidTick { message }` variant is added; the existing
test is updated to assert against it. Python's `map_err` is extended
to forward the new variant as `PyValueError`; the Node and WASM
bindings format via `Error::to_string()` and pick the new variant up
automatically without source changes.
|
||
|
|
510013fc5a |
fix(sma, bollinger): periodic recompute to bound long-stream drift (R7, L2-Rust)
`Sma` and `BollingerBands` both maintained their running `sum` (and `sum_sq` for Bollinger) with a single-subtract incremental update. That is correct in exact arithmetic, but in f64 the sequence `sum -= old; sum += new` on long streams with alternating large/small magnitudes can accumulate catastrophic-cancellation error. Bollinger's existing `.max(0.0)` clamp on the computed variance was a band-aid for the same root cause — the drift had already driven the running variance below zero. The fix: every `16 · period` finite updates, reseed `sum` (and `sum_sq` for Bollinger) from the live window. Amortised cost stays at O(1) — `O(period)` work amortised over `O(period)` updates — and the reseed strategy is named after the constant `RECOMPUTE_EVERY` so the intention is clear at the call site. Behaviour is unchanged on inputs that did not drift to begin with (every existing test still passes, including `batch_equals_streaming` and the SMA proptest). Two new stress tests (`long_stream_drift_stays_bounded` in each module) feed a magnitude-alternating stream for `5 · RECOMPUTE_EVERY · period` updates and assert the reported value tracks a fresh from-scratch computation over the live window to within tight tolerance — these would have failed without the reseed on Bollinger's `sum_sq`. The misleading `sma.rs` comment that claimed drift was already bounded by recomputing the sum after each pop is rewritten to describe the actual reseed strategy (audit finding L2-Rust). |
||
|
|
2db546ac12 |
chore(coppock): fix unbalanced backticks in the new doc comment
Follow-up to
|
||
|
|
b340ecd3d6 |
test(coppock): lock in warmup_period for every parameter set (refutes R12)
Audit finding R12 claimed `Coppock::warmup_period()` was off by one because it returns `max(roc_long, roc_short) + wma`, while `Roc::warmup_period() = period + 1`. After tracing the actual emission sequence the existing formula is correct: when both ROCs reach `Some` at 0-based index L (the slower of `roc_long_period` and `roc_short_period`), the WMA receives its first input there and emits its `wma_period`-th value at 0-based index `L + wma_period − 1`. The `warmup_period()` is the 1-based count of inputs needed before the first `Some`, i.e. `L + wma_period`. R12 was a misread by both Sonnet audit agents and the Opus verifier — none of them traced the actual emission timeline. This commit: - Expands the doc comment on `warmup_period` with the precise emission argument and a worked example for `Coppock::new(6, 4, 3)` (the existing test) so a future reader cannot mis-derive the formula. - Adds `warmup_period_matches_first_some_for_every_parameter_set`, which asserts `out[warmup - 1].is_some()` for five parameter combinations — including the audit's smoking gun `(4, 2, 3)`. The audit's proposed `max + 1 + wma` formula would have predicted index 7 (the 8th input) for that combination; the real first `Some` lands at index 6 (the 7th input), exactly what the current formula reports. No behaviour change — the audit was wrong and the test makes the contract regression-proof. |
||
|
|
2aef8c8db5 |
perf(linreg): incremental O(1) OLS for LinearRegression and LinRegSlope (R2)
`LinearRegression::fit` and `LinRegSlope::update` previously iterated the
full `period`-window on every tick to recompute `Σy` and `Σxy` from
scratch — O(period) per update, in violation of the `Indicator` trait's
O(1) contract. `LinRegAngle` inherits the cost transitively because it
delegates to `LinRegSlope`.
This commit slides the OLS state in closed form. The constant terms
(`Σx`, `Σxx`, the denominator `n·Σxx − (Σx)²`) were already precomputed
in `new`. The new running state is:
- `sum_y: f64` — running sum of the values currently in the window.
- `sum_xy: f64` — running Σ(x · y) where `x` is the position of each
value inside the trailing window (`0` for the oldest, `n−1` for the
newest).
On every push, when the window is already full the front value `y₀` is
popped and the indices of every remaining value shift down by 1; the
identity
new_Σxy = old_Σxy − old_Σy + y₀
closes the slide in O(1). The new value is then pushed at position `k`
(the current length before the push), contributing `k · new_value` to
`sum_xy` and `new_value` to `sum_y`. The output is the same TA-Lib OLS
formula evaluated against the incremental accumulators.
Behaviour is unchanged: same per-tick values, same warmup, same NaN
semantics. Two new tests compare the O(1) result bar-by-bar against a
fresh O(n) refit on a noisy ramp (sliding-phase dominated), a step
function (large pop/push deltas), and constants (tests floating-point
drift) — agreement is within `1e-9`.
`LinRegAngle` benefits automatically through its `LinRegSlope` field.
|
||
|
|
0995f8d66a |
fix(psar): correct is_ready convention and use NaN sentinels (R6, B-Opus-1)
`Psar::is_ready` previously returned `self.initialised`, which flips to
`true` *after* the seed candle — but the seed candle itself returns
`None`. The contract every other indicator honours is
`is_ready() == true` ↔ "the most recent update produced (or could
produce) a real value". Streaming consumers writing
`if ind.is_ready() { use(ind.update(c)?) }` would hit an unexpected
`None` on the first post-seed update.
Fix: add a `has_emitted: bool` field that flips on the first
`Some(sar)` return; `is_ready` now reads that. New test
`is_ready_only_after_first_some_value` pins the contract.
While in the same file, `reset()` is corrected to restore the compute
fields (`prev_high`, `prev_low`, `sar`, `ep`) to `f64::NAN` sentinels
instead of `0.0` (Opus bonus finding). The fields are gated by
`initialised` today, so the `0.0` sentinel never leaked into output —
but a future refactor that read them pre-init would have silently
treated `0.0` as a real price. A `debug_assert!` at the read site makes
the invariant explicit and catches a re-introduction of the bug in
debug builds.
Bit-equivalence with the previous behaviour is preserved
(`reset_allows_clean_reuse` and `batch_equals_streaming` continue to
pass unchanged).
|
||
|
|
a530f1b4cb |
perf(ulcer-index): track trailing max with a monotone deque (R1, B-Opus-2)
`UlcerIndex::update` previously scanned the full `period`-window every tick via `prices.iter().fold(NEG_INFINITY, f64::max)`, breaking the `Indicator` trait's O(1) contract. For long windows (e.g. period 50+ on a live tick stream) this turned a constant-time update into an O(period) one, and full-history batch replays into O(n · period). The window of raw prices is replaced with a monotonically-decreasing deque of `(index, price)` pairs. On every push, all back entries `<= input` are popped (they can never be the trailing max again, since they are dominated and at least as old). On every step, the front is popped if its index is older than `count - period + 1`. The deque's front is therefore always the trailing max in O(1). `count: u64` is the 1-based input counter that drives expiration; on `reset()` it returns to zero alongside the deque and the drawdown state. Behaviour is unchanged: same per-tick values, same warmup (`2 * period - 1`), same non-finite-input semantics. A new test `monotone_deque_matches_naive_max_on_adversarial_inputs` compares the deque output bar-by-bar against an independent O(n) trailing-max scan on inputs designed to hit every code path: strictly increasing (full tail pops), strictly decreasing (head expirations only), constants (the `<= input` pop rule keeps a single newest entry), and a sawtooth. The doc comment on `warmup_period()` is also corrected (B-Opus-2): the two windows overlap by one bar, so the formula is `2 * period - 1`, not `2 * period`. |
||
|
|
747d1a5b1b |
examples: move Rust examples into a top-level examples/rust/ crate
The three Rust examples (backtest, fetch_btcusdt, live_binance) used to
live each in their own crate's examples/ dir, splitting the example set
across crates and burying it inside the source tree. Move them into a new
workspace member crate at `examples/rust/` (package `wickra-examples`,
`publish = false`) so all language examples sit under one top-level
`examples/<lang>/` tree.
* `examples/rust/Cargo.toml` declares the per-binary deps (wickra,
wickra-data with the `live-binance` feature always on, serde_json, tokio
for the macro and current-thread runtime).
* `examples/rust/src/bin/{backtest,fetch_btcusdt,live_binance}.rs` are the
three migrated binaries; their doc-comments and the fetch_btcusdt output
path are updated for the new location and run command
(`cargo run -p wickra-examples --bin <name>`).
* Workspace `Cargo.toml` lists the new member; the now-empty
`[dev-dependencies]` extras (`wickra`, `tokio` in wickra-data and
`serde_json` in wickra) that existed only for these examples are dropped.
* The `[[example]] live_binance` table is removed from wickra-data's
manifest since the file moved out.
* README "Languages" + project-layout, examples/README.md, Quickstart-Rust
and Data-Layer are pointed at the new paths and commands.
`cargo build -p wickra-examples` and `cargo run --release -p wickra-examples
--bin backtest -- examples/data/btcusdt-1d.csv` both succeed; the rest of
the workspace (core, data, wickra) builds, clippies (`--all-targets -D
warnings`) and tests (508 core + 28 data + 1 integration + 74+3+1
doctests) all stay green.
|
||
|
|
a1c646ae7c |
examples: move the bundled BTCUSDT datasets to a top-level examples/data/
The seven BTCUSDT OHLCV datasets used to live under crates/wickra/examples/data/, which buried them inside a Rust crate even though the Node backtest example and the upcoming Rust/Node/WASM example restructure need to reach them too. Move them to the workspace-level examples/data/ so every language's examples can resolve the same path. The bench (crates/wickra/benches/indicators.rs), the example_data integration test, fetch_btcusdt.rs and the Node backtest example all take the new ../../examples/data/ path; Data-Layer.md, examples/README.md and the CHANGELOG entry are updated to match. No data file content changes. |
||
|
|
d5ff0a9df6 |
wickra-data: add minutes/hours/days Timeframe constructors
Timeframe gained new/millis/seconds/one_minute_ms; add minutes, hours and days alongside them. Each builds on seconds (minutes(5) -> a 300-second bucket), consistent with Timeframe::seconds, and guards the multiplication with checked_mul so an oversized n yields Error::InvalidTimeframe instead of an overflow panic. A non-positive n is rejected by Timeframe::new. Each method carries a runnable doctest, and unit tests cover the known bucket sizes, non-positive rejection and overflow rejection. |
||
|
|
2b3a1b7384 |
examples: add real BTCUSDT candle datasets from Binance
Add seven OHLCV datasets under crates/wickra/examples/data/, one per timeframe (1m/5m/15m/1h/12h/1d/1month), holding real BTCUSDT spot klines fetched from the Binance REST API. The new fetch_btcusdt example regenerates them: it paginates the klines endpoint through the system curl, parses with serde_json, validates every candle via Candle::new and keeps only fully closed buckets. The indicator benchmarks now run against the 1m dataset instead of a synthetic series, and a new example_data integration test checks that every file parses and carries evenly spaced, monotonic timestamps. The monthly file is named btcusdt-1month.csv rather than btcusdt-1M.csv so it does not collide with btcusdt-1m.csv on case-insensitive filesystems (Windows, default macOS). |
||
|
|
6643f7a81d |
F13b: add True Range, Chaikin Volatility, Z-Score and Linear Regression Angle
Second half of the eight indicators that fill out the new family taxonomy. - Rust core: true_range.rs (TrueRange — the raw single-bar volatility ATR averages), chaikin_volatility.rs (ChaikinVolatility — rate of change of a smoothed high-low spread), z_score.rs (ZScore — price normalised against its rolling mean and standard deviation) and linreg_angle.rs (LinRegAngle — the rolling regression slope as a degree angle). Each with a full Indicator impl, runnable doctest and reference / property / warmup / reset / batch==streaming tests. - Python / Node / WASM: classes wired through all three bindings (ZScore and LinRegAngle ride the scalar macros where possible) plus .pyi stubs and __init__.py / __all__ entries. - Wiki: four new Indicator-*.md pages. The eight-family taxonomy restructure (Overview / Home / README / folder layout) lands next in F13c. cargo fmt + clippy (core/wickra/data/wasm/node) clean; 508 core tests, 25 data tests and 74 doctests green. |
||
|
|
e452d35a27 |
F13a: add Accelerator Oscillator, Balance of Power, Choppiness Index and Vertical Horizontal Filter
First half of the eight indicators that fill out the new family taxonomy. - Rust core: accelerator_oscillator.rs (AcceleratorOscillator — AO minus a short SMA of itself), balance_of_power.rs (BalanceOfPower — per-bar (close-open)/(high-low)), choppiness_index.rs (ChoppinessIndex — summed true range over the high-low span, log-scaled) and vertical_horizontal_filter.rs (VerticalHorizontalFilter — net move over total move). Each with a full Indicator impl, runnable doctest and reference / property / warmup / reset / batch==streaming tests. - Python / Node / WASM: classes wired through all three bindings (BalanceOfPower carries an explicit open column; VHF rides the scalar macros) plus .pyi stubs and __init__.py / __all__ entries. - Wiki: four new Indicator-*.md pages. The eight-family taxonomy restructure (Overview / Home / README / folder layout) lands in F13c once F13b's four indicators are in. cargo fmt + clippy (core/wickra/data/wasm/node) clean; 481 core tests, 25 data tests and 70 doctests green. |