Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5862401958 | |||
| ff5a047078 | |||
| dc415a77fd | |||
| e385734275 | |||
| 3a46b210bb | |||
| 943825d6a0 | |||
| d16df1e224 | |||
| 34c097aee2 |
@@ -53,6 +53,22 @@ jobs:
|
||||
continue-on-error: true # cache is an optimisation; never block on a stuck/slow restore
|
||||
timeout-minutes: 6
|
||||
|
||||
- name: Warm cargo registry (retry transient DNS/registry flakes)
|
||||
shell: bash
|
||||
# CARGO_NET_RETRY rides out short blips, but a longer runner DNS outage
|
||||
# outlasts cargo's rapid in-process retries: the crates.io index fetch
|
||||
# the first cargo step does ("Could not resolve host: index.crates.io")
|
||||
# then fails the whole job. Pre-fetch the dependency graph here with real
|
||||
# backoff so clippy/build/test resolve from the warmed local cache.
|
||||
run: |
|
||||
for attempt in 1 2 3 4 5; do
|
||||
if cargo fetch; then exit 0; fi
|
||||
echo "::warning::cargo fetch failed (attempt $attempt/5) — likely a registry/DNS flake; retrying in $((attempt * 20))s..."
|
||||
sleep $((attempt * 20))
|
||||
done
|
||||
echo "::error::cargo fetch still failing after 5 attempts"
|
||||
exit 1
|
||||
|
||||
- name: Format check
|
||||
run: cargo fmt --all -- --check
|
||||
|
||||
@@ -232,6 +248,19 @@ jobs:
|
||||
continue-on-error: true # cache is an optimisation; never block on a stuck/slow restore
|
||||
timeout-minutes: 6
|
||||
|
||||
- name: Warm cargo registry (retry transient DNS/registry flakes)
|
||||
shell: bash
|
||||
# See the rust job: pre-fetch with backoff so the clippy index update
|
||||
# can't fail the job on a transient "Could not resolve host" DNS blip.
|
||||
run: |
|
||||
for attempt in 1 2 3 4 5; do
|
||||
if cargo fetch; then exit 0; fi
|
||||
echo "::warning::cargo fetch failed (attempt $attempt/5) — likely a registry/DNS flake; retrying in $((attempt * 20))s..."
|
||||
sleep $((attempt * 20))
|
||||
done
|
||||
echo "::error::cargo fetch still failing after 5 attempts"
|
||||
exit 1
|
||||
|
||||
- name: Clippy (bindings, all targets)
|
||||
run: cargo clippy -p wickra-node -p wickra-python --all-targets -- -D warnings
|
||||
|
||||
@@ -272,6 +301,19 @@ jobs:
|
||||
continue-on-error: true # cache is an optimisation; never block on a stuck/slow restore
|
||||
timeout-minutes: 6
|
||||
|
||||
- name: Warm cargo registry (retry transient DNS/registry flakes)
|
||||
shell: bash
|
||||
# See the rust job: pre-fetch with backoff so the first cargo step can't
|
||||
# fail the job on a transient "Could not resolve host" DNS blip.
|
||||
run: |
|
||||
for attempt in 1 2 3 4 5; do
|
||||
if cargo fetch; then exit 0; fi
|
||||
echo "::warning::cargo fetch failed (attempt $attempt/5) — likely a registry/DNS flake; retrying in $((attempt * 20))s..."
|
||||
sleep $((attempt * 20))
|
||||
done
|
||||
echo "::error::cargo fetch still failing after 5 attempts"
|
||||
exit 1
|
||||
|
||||
- name: Build on MSRV
|
||||
run: cargo build ${{ matrix.packages }} --verbose
|
||||
|
||||
|
||||
+18
-18
@@ -26,15 +26,15 @@ was built to expose.
|
||||
|
||||
| Indicator | **★ Wickra** | talipp | TA-Lib (recompute) |
|
||||
|------------------|------------------:|------------------|-----------------------|
|
||||
| SMA(20) | **0.063 µs ★** | 0.59 µs (9×) | 204 µs (3 300×) |
|
||||
| EMA(20) | **0.060 µs ★** | 0.72 µs (12×) | 212 µs (3 500×) |
|
||||
| RSI(14) | **0.065 µs ★** | 1.06 µs (16×) | 230 µs (3 600×) |
|
||||
| MACD(12, 26, 9) | **0.078 µs ★** | 4.22 µs (54×) | 245 µs (3 100×) |
|
||||
| Bollinger(20, 2) | **0.088 µs ★** | 5.15 µs (58×) | 229 µs (2 600×) |
|
||||
| SMA(20) | **0.089 µs ★** | 0.96 µs (11×) | 422 µs (4 700×) |
|
||||
| EMA(20) | **0.111 µs ★** | 1.19 µs (11×) | 430 µs (3 900×) |
|
||||
| RSI(14) | **0.061 µs ★** | 0.95 µs (16×) | 298 µs (4 900×) |
|
||||
| MACD(12, 26, 9) | **0.079 µs ★** | 3.30 µs (42×) | 327 µs (4 100×) |
|
||||
| Bollinger(20, 2) | **0.089 µs ★** | 4.97 µs (56×) | 296 µs (3 300×) |
|
||||
|
||||
Against the only other incremental Python peer Wickra is **9–58× faster**;
|
||||
against the recompute-on-every-tick libraries it is **2 600–14 000× faster**
|
||||
(`finta` RSI hits 14 000×). tulipy / pandas-ta land in the same recompute band
|
||||
Against the only other incremental Python peer Wickra is **11–56× faster**;
|
||||
against the recompute-on-every-tick libraries it is **2 800–19 000× faster**
|
||||
(`finta` RSI hits 19 000×). tulipy / pandas-ta land in the same recompute band
|
||||
as TA-Lib.
|
||||
|
||||
**Rust — per-tick latency** (whole 50 000-bar series, lower = faster):
|
||||
@@ -63,17 +63,17 @@ field everywhere.
|
||||
|
||||
**Python** (20 000-bar pass, µs/op, lower = faster):
|
||||
|
||||
| Indicator | Wickra | TA-Lib | tulipy | pandas-ta |
|
||||
|------------------|---------:|-------:|-------:|----------:|
|
||||
| SMA(20) | 22.7 | **15.4** | 15.9 | 33.7 |
|
||||
| EMA(20) | 30.8 | **30.3** | 31.1 | 48.8 |
|
||||
| RSI(14) | 58.9 | 72.5 | **38.5** | 94.8 |
|
||||
| MACD(12, 26, 9) | 71.7 | 99.1 | **33.5** | 207.6 |
|
||||
| Bollinger(20, 2) | 84.9 | 65.7 | **32.3** | 336.4 |
|
||||
| ATR(14) | 52.0 | 79.4 | **31.9** | — |
|
||||
| Indicator | Wickra | TA-Lib | tulipy | pandas-ta | finta |
|
||||
|------------------|---------:|---------:|---------:|----------:|---------:|
|
||||
| SMA(20) | 22.2 | **15.6** | 15.9 | 32.7 | 290.1 |
|
||||
| EMA(20) | 30.5 | **30.4** | 30.9 | 46.7 | 198.5 |
|
||||
| RSI(14) | 52.3 | 72.0 | **34.2** | 88.8 | 812.3 |
|
||||
| MACD(12, 26, 9) | 129.8 | 111.1 | **38.4** | 286.8 | 716.7 |
|
||||
| Bollinger(20, 2) | 87.2 | 74.6 | **37.9** | 474.3 | 1255.5 |
|
||||
| ATR(14) | 74.7 | 87.3 | **35.5** | — | 3496.4 |
|
||||
|
||||
Wickra beats TA-Lib on RSI, MACD and ATR and the whole Python field on every
|
||||
row; tulipy's SIMD C stays ahead on the heavier indicators.
|
||||
Wickra beats pandas-ta and finta on every row and TA-Lib on RSI and ATR;
|
||||
tulipy's SIMD C (and TA-Lib on SMA/EMA) lead the remaining rows.
|
||||
|
||||
**Rust** (50 000-bar pass, µs, lower = faster). Only Wickra and `kand` expose a
|
||||
batch API; `ta-rs` and `yata` are streaming-only:
|
||||
|
||||
+24
-1
@@ -7,6 +7,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [0.7.1] - 2026-06-08
|
||||
- **Open-Interest Momentum** — rate-of-change of open interest over a rolling window (`OpenInterestMomentum`).
|
||||
- **Funding-Implied APR** — annualised funding rate (per-interval funding times intervals per year) (`FundingImpliedApr`).
|
||||
- **Perpetual Premium Index** — relative premium of the mark price over the index price (`PerpetualPremiumIndex`).
|
||||
- **OI-to-Volume Ratio** — open interest divided by taker volume (position turnover proxy) (`OiToVolumeRatio`).
|
||||
- **Estimated Leverage Ratio** — open interest divided by aggregate long+short position size (leverage proxy) (`EstimatedLeverageRatio`).
|
||||
|
||||
## [0.7.0] - 2026-06-08
|
||||
- **Hasbrouck Information Share** — variance-ratio proxy for each venue's share of price discovery (Hasbrouck information share) (`HasbrouckInformationShare`).
|
||||
- **PIN** — probability of informed trading from rolling buy/sell imbalance (EKOP single-window estimator) (`Pin`).
|
||||
- **Trade-Sign Autocorrelation** — lag-1 autocorrelation of the signed trade aggressor (order-flow persistence) (`TradeSignAutocorrelation`).
|
||||
|
||||
## [0.6.9] - 2026-06-08
|
||||
- **Tristar** — a three-doji star reversal: three consecutive dojis with the middle gapped above (bearish) or below (bullish) its neighbours (`Tristar`).
|
||||
- **Harami Cross** — a Harami whose second candle is a contained doji, a stronger reversal than a plain Harami (`HaramiCross`).
|
||||
- **Tower Top/Bottom** — a tall bar, a small pause bar, then a tall opposite bar marking a reversal (`TowerTopBottom`).
|
||||
- **Frying Pan Bottom** — a rounded (U-shaped) accumulation base over the lookback window, confirmed when price recovers above the rim (`FryPanBottom`).
|
||||
- **Dumpling Top** — a rounded (dome-shaped) distribution top over the lookback window, confirmed when price breaks below the start (`DumplingTop`).
|
||||
- **New Price Lines** — flags a run of N consecutive new closing highs (+1) or lows (-1), the eight/ten-new-price-lines exhaustion gauge (`NewPriceLines`).
|
||||
|
||||
## [0.6.8] - 2026-06-08
|
||||
- **Smoothed Heikin-Ashi** — a Heikin-Ashi candle computed from EMA-smoothed OHLC, damping noise into a cleaner trend candle (`SmoothedHeikinAshi`).
|
||||
- **Heikin-Ashi Oscillator** — the Heikin-Ashi candle body (`ha_close − ha_open`), optionally EMA-smoothed, as a zero-line oscillator (`HeikinAshiOscillator`).
|
||||
@@ -1369,7 +1389,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
optional Binance live feed.
|
||||
- Bindings for Python, Node.js, and WebAssembly.
|
||||
|
||||
[Unreleased]: https://github.com/wickra-lib/wickra/compare/v0.6.8...HEAD
|
||||
[Unreleased]: https://github.com/wickra-lib/wickra/compare/v0.7.1...HEAD
|
||||
[0.7.1]: https://github.com/wickra-lib/wickra/compare/v0.7.0...v0.7.1
|
||||
[0.7.0]: https://github.com/wickra-lib/wickra/compare/v0.6.9...v0.7.0
|
||||
[0.6.9]: https://github.com/wickra-lib/wickra/compare/v0.6.8...v0.6.9
|
||||
[0.6.8]: https://github.com/wickra-lib/wickra/compare/v0.6.7...v0.6.8
|
||||
[0.6.7]: https://github.com/wickra-lib/wickra/compare/v0.6.6...v0.6.7
|
||||
[0.6.6]: https://github.com/wickra-lib/wickra/compare/v0.6.5...v0.6.6
|
||||
|
||||
Generated
+8
-8
@@ -1944,7 +1944,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wickra"
|
||||
version = "0.6.8"
|
||||
version = "0.7.1"
|
||||
dependencies = [
|
||||
"approx",
|
||||
"criterion",
|
||||
@@ -1955,7 +1955,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wickra-bench"
|
||||
version = "0.6.8"
|
||||
version = "0.7.1"
|
||||
dependencies = [
|
||||
"criterion",
|
||||
"kand",
|
||||
@@ -1967,7 +1967,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wickra-core"
|
||||
version = "0.6.8"
|
||||
version = "0.7.1"
|
||||
dependencies = [
|
||||
"approx",
|
||||
"proptest",
|
||||
@@ -1977,7 +1977,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wickra-data"
|
||||
version = "0.6.8"
|
||||
version = "0.7.1"
|
||||
dependencies = [
|
||||
"approx",
|
||||
"csv",
|
||||
@@ -1994,7 +1994,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wickra-examples"
|
||||
version = "0.6.8"
|
||||
version = "0.7.1"
|
||||
dependencies = [
|
||||
"serde_json",
|
||||
"tokio",
|
||||
@@ -2004,7 +2004,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wickra-node"
|
||||
version = "0.6.8"
|
||||
version = "0.7.1"
|
||||
dependencies = [
|
||||
"napi",
|
||||
"napi-build",
|
||||
@@ -2014,7 +2014,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wickra-python"
|
||||
version = "0.6.8"
|
||||
version = "0.7.1"
|
||||
dependencies = [
|
||||
"numpy",
|
||||
"pyo3",
|
||||
@@ -2023,7 +2023,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wickra-wasm"
|
||||
version = "0.6.8"
|
||||
version = "0.7.1"
|
||||
dependencies = [
|
||||
"console_error_panic_hook",
|
||||
"js-sys",
|
||||
|
||||
+2
-2
@@ -13,7 +13,7 @@ members = [
|
||||
exclude = ["fuzz"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.6.8"
|
||||
version = "0.7.1"
|
||||
authors = ["kingchenc <support@wickra.org>"]
|
||||
edition = "2021"
|
||||
rust-version = "1.86"
|
||||
@@ -25,7 +25,7 @@ keywords = ["finance", "trading", "indicators", "technical-analysis", "ta"]
|
||||
categories = ["finance", "mathematics", "science"]
|
||||
|
||||
[workspace.dependencies]
|
||||
wickra-core = { path = "crates/wickra-core", version = "0.6.8" }
|
||||
wickra-core = { path = "crates/wickra-core", version = "0.7.1" }
|
||||
|
||||
thiserror = "2"
|
||||
rayon = "1.10"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<p align="center">
|
||||
<a href="https://wickra.org"><img src="https://raw.githubusercontent.com/wickra-lib/.github/main/profile/wickra-banner.webp?v=479" alt="Wickra — streaming-first technical indicators" width="100%"></a>
|
||||
<a href="https://wickra.org"><img src="https://raw.githubusercontent.com/wickra-lib/.github/main/profile/wickra-banner.webp?v=493" alt="Wickra — streaming-first technical indicators" width="100%"></a>
|
||||
</p>
|
||||
|
||||
[](https://github.com/wickra-lib/wickra/actions/workflows/ci.yml)
|
||||
@@ -48,7 +48,7 @@ Full documentation lives at **[docs.wickra.org](https://docs.wickra.org)**:
|
||||
[Node](https://docs.wickra.org/Quickstart-Node),
|
||||
[WASM](https://docs.wickra.org/Quickstart-WASM).
|
||||
- **Indicators** — a per-indicator deep dive (formula, parameters, warmup) for
|
||||
every one of the 479 indicators; start at the
|
||||
every one of the 493 indicators; start at the
|
||||
[indicators overview](https://docs.wickra.org/Indicators-Overview).
|
||||
- **Reference** — [warmup periods](https://docs.wickra.org/Warmup-Periods),
|
||||
[streaming vs batch](https://docs.wickra.org/Streaming-vs-Batch),
|
||||
@@ -66,7 +66,7 @@ an afterthought — **live, tick-by-tick data** — without giving up the breadt
|
||||
a full batch library, and without making you reimplement your indicators four
|
||||
times to get there.
|
||||
|
||||
- **The biggest streaming-native catalogue, period.** 479 indicators across 24
|
||||
- **The biggest streaming-native catalogue, period.** 493 indicators across 24
|
||||
families — candlesticks, harmonic & chart patterns, market profile, market
|
||||
breadth, Renko/Kagi/Point&Figure bars, Ehlers DSP cycles, risk/performance
|
||||
metrics — every single one updating in **O(1) per tick**. TA-Lib ships ~150 and
|
||||
@@ -77,8 +77,8 @@ times to get there.
|
||||
- **Correct by construction, not by hope.** Every `update` validates its input,
|
||||
runs a real warmup, and returns an `Option` so a single bad tick can't silently
|
||||
poison state. `batch == streaming` is **bit-exact, fuzzed and 100 %-line-covered
|
||||
for all 479 indicators**.
|
||||
- **Orders of magnitude faster where it counts.** In streaming Wickra is **9–58×**
|
||||
for all 493 indicators**.
|
||||
- **Orders of magnitude faster where it counts.** In streaming Wickra is **11–56×**
|
||||
faster than the only other incremental peer and **thousands of times** faster
|
||||
than recompute-on-every-tick libraries. On batch it wins several rows outright
|
||||
and trades the simple recurrences (SMA, EMA, MACD) for its guarantees — and
|
||||
@@ -95,7 +95,7 @@ Every other library forces one of those compromises. Wickra doesn't:
|
||||
|
||||
| Library | Install | Streaming | Languages | Indicators | Active |
|
||||
|------------------|-------------|-------------|-----------------------------|-----------:|--------|
|
||||
| **★ Wickra**| **clean** | **yes, O(1)** | **Python · Node · WASM · Rust** | **479** | **yes** |
|
||||
| **★ Wickra**| **clean** | **yes, O(1)** | **Python · Node · WASM · Rust** | **493** | **yes** |
|
||||
| kand | clean | yes | Python · WASM · Rust | ~60 | yes |
|
||||
| ta-rs | clean | yes | Rust only | ~30 | stale |
|
||||
| yata | clean | partial | Rust only | ~35 | yes |
|
||||
@@ -118,7 +118,7 @@ useful version of that itch is the one other people can build on too.
|
||||
## Benchmarks
|
||||
|
||||
Wickra updates every indicator in **O(1)** per tick. In **streaming** — the
|
||||
workload it is built for — it is **9–58× faster** than the only other incremental
|
||||
workload it is built for — it is **11–56× faster** than the only other incremental
|
||||
peer and **thousands of times** faster than recompute-on-every-tick libraries.
|
||||
**Batch** is competitive: it wins several rows outright and trades a few µs
|
||||
elsewhere for `None`-warmup, NaN-safety and bit-exact `batch == streaming`.
|
||||
@@ -128,7 +128,7 @@ Full tables (Rust + Python, streaming + batch) and how to reproduce them live in
|
||||
|
||||
## Indicators
|
||||
|
||||
479 streaming-first indicators across twenty-four families. Every one passes the
|
||||
493 streaming-first indicators across twenty-four families. Every one passes the
|
||||
`batch == streaming` equivalence test, reference-value tests, and reset
|
||||
semantics tests. Each has a per-indicator deep dive (formula, parameters,
|
||||
warmup) at [docs.wickra.org](https://docs.wickra.org/Indicators-Overview).
|
||||
@@ -149,12 +149,12 @@ warmup) at [docs.wickra.org](https://docs.wickra.org/Indicators-Overview).
|
||||
| DeMark | TD Setup, TD Sequential, TD DeMarker, TD REI, TD Pressure, TD Combo, TD Countdown, TD Lines, TD Range Projection, TD Differential, TD Open, TD Risk Level, TD Camouflage, TD Clop, TD Clopwin, TD Propulsion, TD Trap, TD D-Wave, TD Moving Averages |
|
||||
| Ichimoku & Charts | Ichimoku Kinko Hyo (Tenkan, Kijun, Senkou A/B, Chikou), Heikin-Ashi, Heikin-Ashi Oscillator, Three Line Break, Smoothed Heikin-Ashi, Equivolume, CandleVolume |
|
||||
| Alt-Chart Bars | Renko (box-size bricks), Kagi (reversal-amount lines), Point & Figure (X/O columns) |
|
||||
| Candlestick Patterns | Doji, Hammer, Inverted Hammer, Hanging Man, Shooting Star, Engulfing, Harami, Morning/Evening Star, Three White Soldiers/Black Crows, Piercing Line/Dark Cloud Cover, Marubozu, Tweezer, Spinning Top, Three Inside Up/Down, Three Outside Up/Down, Two Crows, Upside Gap Two Crows, Identical Three Crows, Three Line Strike, Three Stars in the South, Abandoned Baby, Advance Block, Belt-hold, Breakaway, Counterattack, Doji Star, Dragonfly Doji, Gravestone Doji, Long-Legged Doji, Rickshaw Man, Evening Doji Star, Morning Doji Star, Gap Side-by-Side White, High-Wave, Hikkake, Modified Hikkake, Homing Pigeon, On-Neck, In-Neck, Thrusting, Separating Lines, Kicking, Kicking by Length, Ladder Bottom, Mat Hold, Matching Low, Long Line, Short Line, Rising Three Methods, Falling Three Methods, Upside Gap Three Methods, Downside Gap Three Methods, Stalled Pattern, Stick Sandwich, Takuri, Closing Marubozu, Opening Marubozu, Tasuki Gap, Unique Three River, Concealing Baby Swallow |
|
||||
| Candlestick Patterns | Doji, Hammer, Inverted Hammer, Hanging Man, Shooting Star, Engulfing, Harami, Morning/Evening Star, Three White Soldiers/Black Crows, Piercing Line/Dark Cloud Cover, Marubozu, Tweezer, Spinning Top, Three Inside Up/Down, Three Outside Up/Down, Two Crows, Upside Gap Two Crows, Identical Three Crows, Three Line Strike, Three Stars in the South, Abandoned Baby, Advance Block, Belt-hold, Breakaway, Counterattack, Doji Star, Dragonfly Doji, Gravestone Doji, Long-Legged Doji, Rickshaw Man, Evening Doji Star, Morning Doji Star, Gap Side-by-Side White, High-Wave, Hikkake, Modified Hikkake, Homing Pigeon, On-Neck, In-Neck, Thrusting, Separating Lines, Kicking, Kicking by Length, Ladder Bottom, Mat Hold, Matching Low, Long Line, Short Line, Rising Three Methods, Falling Three Methods, Upside Gap Three Methods, Downside Gap Three Methods, Stalled Pattern, Stick Sandwich, Takuri, Closing Marubozu, Opening Marubozu, Tasuki Gap, Unique Three River, Concealing Baby Swallow, Tristar, Harami Cross, Tower Top/Bottom, Dumpling Top, New Price Lines, Frying Pan Bottom |
|
||||
| Chart Patterns | Double Top / Bottom, Triple Top / Bottom, Head and Shoulders, Triangle (asc/desc/sym), Wedge (rising/falling), Flag / Pennant, Rectangle / Range, Cup and Handle |
|
||||
| Harmonic Patterns | AB=CD, Gartley, Butterfly, Bat, Crab, Shark, Cypher, Three Drives |
|
||||
| Fibonacci | Fibonacci Retracement, Fibonacci Extension, Fibonacci Projection, Auto-Fibonacci, Golden Pocket, Fibonacci Confluence, Fibonacci Fan, Fibonacci Arcs, Fibonacci Channel, Fibonacci Time Zones |
|
||||
| Microstructure | Order-Book Imbalance (Top-1 / Top-N / Full), Microprice, Quoted Spread, Depth Slope, Signed Volume, Cumulative Volume Delta, Trade Imbalance, Effective Spread, Realized Spread, Kyle's Lambda, Footprint, Order Flow Imbalance, VPIN, Amihud Illiquidity, Roll Measure |
|
||||
| Derivatives | Funding Rate, Funding Rate Mean, Funding Rate Z-Score, Funding Basis, Open-Interest Delta, OI / Price Divergence, OI-Weighted Price, Long/Short Ratio, Taker Buy/Sell Ratio, Liquidation Features, Term-Structure Basis, Calendar Spread |
|
||||
| Microstructure | Order-Book Imbalance (Top-1 / Top-N / Full), Microprice, Quoted Spread, Depth Slope, Signed Volume, Cumulative Volume Delta, Trade Imbalance, Effective Spread, Realized Spread, Kyle's Lambda, Footprint, Order Flow Imbalance, VPIN, Amihud Illiquidity, Roll Measure, Trade-Sign Autocorrelation, Hasbrouck Information Share |
|
||||
| Derivatives | Funding Rate, Funding Rate Mean, Funding Rate Z-Score, Funding Basis, Open-Interest Delta, OI / Price Divergence, OI-Weighted Price, Long/Short Ratio, Taker Buy/Sell Ratio, Liquidation Features, Term-Structure Basis, Calendar Spread, Estimated Leverage Ratio, OI-to-Volume Ratio, Perpetual Premium Index, Funding-Implied APR, Open-Interest Momentum |
|
||||
| Market Profile | Value Area (POC / VAH / VAL), Volume Profile (histogram), TPO Profile, Initial Balance, Opening Range |
|
||||
| Market Breadth | Advance/Decline Line, Advance/Decline Ratio, Advance/Decline Volume Line, McClellan Oscillator, McClellan Summation Index, TRIN / Arms Index, Breadth Thrust, New Highs - New Lows, High-Low Index, Percent Above Moving Average, Up/Down Volume Ratio, Bullish Percent Index, Cumulative Volume Index, Absolute Breadth Index, TICK Index |
|
||||
| Risk / Performance | Sharpe Ratio, Sortino Ratio, Calmar Ratio, Omega Ratio, Max Drawdown, Average Drawdown, Drawdown Duration, Pain Index, Value at Risk, Conditional Value at Risk (CVaR), Profit Factor, Gain/Loss Ratio, Recovery Factor, Kelly Criterion, Treynor Ratio, Information Ratio, Alpha (Jensen) |
|
||||
@@ -237,7 +237,7 @@ A Python live-trading example using the public `websockets` package lives at
|
||||
```
|
||||
wickra/
|
||||
├── crates/
|
||||
│ ├── wickra-core/ core engine + all 479 indicators
|
||||
│ ├── wickra-core/ core engine + all 493 indicators
|
||||
│ ├── wickra/ top-level facade crate (publishes on crates.io) + benches/
|
||||
│ ├── wickra-data/ CSV reader, tick aggregator, live exchange feeds
|
||||
│ └── wickra-bench/ internal cross-library benchmark harness (not published)
|
||||
|
||||
@@ -386,6 +386,12 @@ const candleScalar = {
|
||||
TDDWave: { make: () => new wickra.TDDWave(2), step: (ind, i) => ind.update(high[i], low[i], close[i]), batch: (ind) => ind.batch(high, low, close) },
|
||||
HeikinAshiOscillator: { make: () => new wickra.HeikinAshiOscillator(5), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
|
||||
ThreeLineBreak: { make: () => new wickra.ThreeLineBreak(3), step: (ind, i) => ind.update(high[i], low[i], close[i]), batch: (ind) => ind.batch(high, low, close) },
|
||||
Tristar: { make: () => new wickra.Tristar(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
|
||||
HaramiCross: { make: () => new wickra.HaramiCross(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
|
||||
TowerTopBottom: { make: () => new wickra.TowerTopBottom(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
|
||||
DumplingTop: { make: () => new wickra.DumplingTop(9), step: (ind, i) => ind.update(high[i], low[i], close[i]), batch: (ind) => ind.batch(high, low, close) },
|
||||
NewPriceLines: { make: () => new wickra.NewPriceLines(5), step: (ind, i) => ind.update(high[i], low[i], close[i]), batch: (ind) => ind.batch(high, low, close) },
|
||||
FryPanBottom: { make: () => new wickra.FryPanBottom(9), step: (ind, i) => ind.update(high[i], low[i], close[i]), batch: (ind) => ind.batch(high, low, close) },
|
||||
};
|
||||
|
||||
for (const [name, d] of Object.entries(candleScalar)) {
|
||||
@@ -661,6 +667,7 @@ const pairFactories = {
|
||||
GrangerCausality: () => new wickra.GrangerCausality(60, 1),
|
||||
SpreadAr1Coefficient: () => new wickra.SpreadAr1Coefficient(40),
|
||||
KendallTau: () => new wickra.KendallTau(20),
|
||||
HasbrouckInformationShare: () => new wickra.HasbrouckInformationShare(2),
|
||||
};
|
||||
|
||||
for (const [name, make] of Object.entries(pairFactories)) {
|
||||
@@ -1264,7 +1271,7 @@ test('vpin / amihud / roll reference + streaming matches batch', () => {
|
||||
const price = Array.from({ length: n }, (_, i) => 100 + Math.sin(i * 0.25) * 4);
|
||||
const size = Array.from({ length: n }, (_, i) => 1 + (i % 5));
|
||||
const isBuy = Array.from({ length: n }, (_, i) => i % 2 === 0);
|
||||
for (const make of [() => new wickra.Vpin(8, 5), () => new wickra.AmihudIlliquidity(14), () => new wickra.RollMeasure(14)]) {
|
||||
for (const make of [() => new wickra.Vpin(8, 5), () => new wickra.AmihudIlliquidity(14), () => new wickra.RollMeasure(14), () => new wickra.TradeSignAutocorrelation(10), () => new wickra.Pin(10)]) {
|
||||
const batch = make().batch(price, size, isBuy);
|
||||
const streamer = make();
|
||||
assert.equal(batch.length, n);
|
||||
@@ -1273,6 +1280,16 @@ test('vpin / amihud / roll reference + streaming matches batch', () => {
|
||||
assert.ok((Number.isNaN(batch[i]) && s === null) || Math.abs(s - batch[i]) < 1e-9, `mismatch at ${i}`);
|
||||
}
|
||||
}
|
||||
// Trade-sign autocorrelation: alternating signs -> -1, all buys -> +1.
|
||||
let tsac = null;
|
||||
const tsacInd = new wickra.TradeSignAutocorrelation(10);
|
||||
for (let i = 0; i < 20; i++) tsac = tsacInd.update(100, 1, i % 2 === 0);
|
||||
assert.ok(Math.abs(tsac - -1.0) < 1e-12);
|
||||
// PIN: one-sided flow -> 1, balanced flow -> 0.
|
||||
let pin = null;
|
||||
const pinInd = new wickra.Pin(10);
|
||||
for (let i = 0; i < 20; i++) pin = pinInd.update(100, 1, true);
|
||||
assert.ok(Math.abs(pin - 1.0) < 1e-12);
|
||||
});
|
||||
|
||||
test('price-impact indicators reference values', () => {
|
||||
@@ -1426,6 +1443,56 @@ test('derivatives reject bad input', () => {
|
||||
assert.throws(() => new wickra.FundingBasis().update(100, 0));
|
||||
});
|
||||
|
||||
test('B16 derivatives reference values', () => {
|
||||
// Estimated leverage: oi / (long + short) = 200 / 100 = 2.
|
||||
assert.ok(Math.abs(new wickra.EstimatedLeverageRatio().update(200, 60, 40) - 2.0) < 1e-12);
|
||||
// OI-to-volume: oi / (buy + sell) = 100 / 50 = 2.
|
||||
assert.ok(Math.abs(new wickra.OiToVolumeRatio().update(100, 30, 20) - 2.0) < 1e-12);
|
||||
// Perpetual premium: (mark - index) / index = 0.5 / 100 = 0.005.
|
||||
assert.ok(Math.abs(new wickra.PerpetualPremiumIndex().update(100.5, 100.0) - 0.005) < 1e-12);
|
||||
// Funding-implied APR: rate * intervals = 0.0001 * 1095 = 0.1095.
|
||||
assert.ok(Math.abs(new wickra.FundingImpliedApr(1095).update(0.0001) - 0.1095) < 1e-12);
|
||||
// Open-interest momentum (period 2): warmup then ROC% = 100*(120 - 100)/100 = 20.
|
||||
const oim = new wickra.OpenInterestMomentum(2);
|
||||
assert.equal(oim.update(100), null);
|
||||
assert.equal(oim.update(110), null);
|
||||
assert.ok(Math.abs(oim.update(120) - 20.0) < 1e-12);
|
||||
});
|
||||
|
||||
test('B16 derivatives streaming matches batch', () => {
|
||||
const n = 30;
|
||||
const oi = Array.from({ length: n }, (_, i) => 1000 + 50 * Math.sin(i * 0.3));
|
||||
const longSz = Array.from({ length: n }, (_, i) => 600 + 20 * Math.cos(i * 0.2));
|
||||
const shortSz = Array.from({ length: n }, (_, i) => 400 + 15 * Math.sin(i * 0.4));
|
||||
const buy = Array.from({ length: n }, (_, i) => 300 + 10 * Math.sin(i * 0.5));
|
||||
const sell = Array.from({ length: n }, (_, i) => 250 + 12 * Math.cos(i * 0.35));
|
||||
const index = Array.from({ length: n }, (_, i) => 100 + Math.sin(i * 0.2));
|
||||
const mark = Array.from({ length: n }, (_, i) => index[i] + 0.05 * Math.cos(i * 0.3));
|
||||
const rate = Array.from({ length: n }, (_, i) => 0.0001 * Math.sin(i * 0.3));
|
||||
const cmp = (batch, s, i) =>
|
||||
assert.ok((s === null && Number.isNaN(batch[i])) || Math.abs(s - batch[i]) < 1e-12, `mismatch at ${i}`);
|
||||
|
||||
let b = new wickra.EstimatedLeverageRatio().batch(oi, longSz, shortSz);
|
||||
let st = new wickra.EstimatedLeverageRatio();
|
||||
for (let i = 0; i < n; i++) cmp(b, st.update(oi[i], longSz[i], shortSz[i]), i);
|
||||
|
||||
b = new wickra.OiToVolumeRatio().batch(oi, buy, sell);
|
||||
st = new wickra.OiToVolumeRatio();
|
||||
for (let i = 0; i < n; i++) cmp(b, st.update(oi[i], buy[i], sell[i]), i);
|
||||
|
||||
b = new wickra.PerpetualPremiumIndex().batch(mark, index);
|
||||
st = new wickra.PerpetualPremiumIndex();
|
||||
for (let i = 0; i < n; i++) cmp(b, st.update(mark[i], index[i]), i);
|
||||
|
||||
b = new wickra.FundingImpliedApr(1095).batch(rate);
|
||||
st = new wickra.FundingImpliedApr(1095);
|
||||
for (let i = 0; i < n; i++) cmp(b, st.update(rate[i]), i);
|
||||
|
||||
b = new wickra.OpenInterestMomentum(10).batch(oi);
|
||||
st = new wickra.OpenInterestMomentum(10);
|
||||
for (let i = 0; i < n; i++) cmp(b, st.update(oi[i]), i);
|
||||
});
|
||||
|
||||
test('market breadth: AdvanceDecline reference values', () => {
|
||||
// A breadth tick is the universe as parallel arrays; the sign of `change`
|
||||
// classifies each symbol as advancing / declining / unchanged.
|
||||
|
||||
Vendored
+130
@@ -1449,6 +1449,19 @@ export declare class BetaNeutralSpread {
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type HasbrouckInformationShareNode = HasbrouckInformationShare
|
||||
export declare class HasbrouckInformationShare {
|
||||
constructor(period: number)
|
||||
update(x: number, y: number): number | null
|
||||
/**
|
||||
* Batch over two equally-sized arrays. Returns a length-`n` array
|
||||
* with `NaN` for warmup positions.
|
||||
*/
|
||||
batch(x: Array<number>, y: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type PairSpreadZScoreNode = PairSpreadZScore
|
||||
/**
|
||||
* Pair spread z-score: two ctor params (`betaPeriod`, `zPeriod`), one `(a, b)`
|
||||
@@ -3421,6 +3434,33 @@ export declare class CandleVolume {
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type FryPanBottomNode = FryPanBottom
|
||||
export declare class FryPanBottom {
|
||||
constructor(period: number)
|
||||
update(high: number, low: number, close: number): number | null
|
||||
batch(high: Array<number>, low: Array<number>, close: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type DumplingTopNode = DumplingTop
|
||||
export declare class DumplingTop {
|
||||
constructor(period: number)
|
||||
update(high: number, low: number, close: number): number | null
|
||||
batch(high: Array<number>, low: Array<number>, close: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type NewPriceLinesNode = NewPriceLines
|
||||
export declare class NewPriceLines {
|
||||
constructor(count: number)
|
||||
update(high: number, low: number, close: number): number | null
|
||||
batch(high: Array<number>, low: Array<number>, close: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type ValueAreaNode = ValueArea
|
||||
export declare class ValueArea {
|
||||
constructor(period: number, binCount: number, valueAreaPct: number)
|
||||
@@ -4198,6 +4238,33 @@ export declare class TDTrap {
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type TristarNode = Tristar
|
||||
export declare class Tristar {
|
||||
constructor()
|
||||
update(open: number, high: number, low: number, close: number): number | null
|
||||
batch(open: Array<number>, high: Array<number>, low: Array<number>, close: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type HaramiCrossNode = HaramiCross
|
||||
export declare class HaramiCross {
|
||||
constructor()
|
||||
update(open: number, high: number, low: number, close: number): number | null
|
||||
batch(open: Array<number>, high: Array<number>, low: Array<number>, close: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type TowerTopBottomNode = TowerTopBottom
|
||||
export declare class TowerTopBottom {
|
||||
constructor()
|
||||
update(open: number, high: number, low: number, close: number): number | null
|
||||
batch(open: Array<number>, high: Array<number>, low: Array<number>, close: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type OrderBookImbalanceTop1Node = OrderBookImbalanceTop1
|
||||
export declare class OrderBookImbalanceTop1 {
|
||||
constructor()
|
||||
@@ -4279,6 +4346,24 @@ export declare class TradeImbalance {
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type TradeSignAutocorrelationNode = TradeSignAutocorrelation
|
||||
export declare class TradeSignAutocorrelation {
|
||||
constructor(period: number)
|
||||
update(price: number, size: number, isBuy: boolean): number | null
|
||||
batch(price: Array<number>, size: Array<number>, isBuy: Array<boolean>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type PinNode = Pin
|
||||
export declare class Pin {
|
||||
constructor(window: number)
|
||||
update(price: number, size: number, isBuy: boolean): number | null
|
||||
batch(price: Array<number>, size: Array<number>, isBuy: Array<boolean>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type OrderFlowImbalanceNode = OrderFlowImbalance
|
||||
export declare class OrderFlowImbalance {
|
||||
constructor(period: number)
|
||||
@@ -4459,6 +4544,51 @@ export declare class CalendarSpread {
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type EstimatedLeverageRatioNode = EstimatedLeverageRatio
|
||||
export declare class EstimatedLeverageRatio {
|
||||
constructor()
|
||||
update(openInterest: number, longSize: number, shortSize: number): number | null
|
||||
batch(openInterest: Array<number>, longSize: Array<number>, shortSize: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type OiToVolumeRatioNode = OiToVolumeRatio
|
||||
export declare class OiToVolumeRatio {
|
||||
constructor()
|
||||
update(openInterest: number, takerBuyVolume: number, takerSellVolume: number): number | null
|
||||
batch(openInterest: Array<number>, takerBuyVolume: Array<number>, takerSellVolume: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type PerpetualPremiumIndexNode = PerpetualPremiumIndex
|
||||
export declare class PerpetualPremiumIndex {
|
||||
constructor()
|
||||
update(markPrice: number, indexPrice: number): number | null
|
||||
batch(markPrice: Array<number>, indexPrice: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type FundingImpliedAprNode = FundingImpliedApr
|
||||
export declare class FundingImpliedApr {
|
||||
constructor(intervalsPerYear: number)
|
||||
update(fundingRate: number): number | null
|
||||
batch(fundingRate: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type OpenInterestMomentumNode = OpenInterestMomentum
|
||||
export declare class OpenInterestMomentum {
|
||||
constructor(period: number)
|
||||
update(openInterest: number): number | null
|
||||
batch(openInterest: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type AdvanceDeclineNode = AdvanceDecline
|
||||
export declare class AdvanceDecline {
|
||||
constructor()
|
||||
|
||||
+15
-1
File diff suppressed because one or more lines are too long
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "wickra-darwin-arm64",
|
||||
"version": "0.6.8",
|
||||
"version": "0.7.1",
|
||||
"description": "Native binding for wickra (macOS Apple Silicon). Installed automatically as an optional dependency of wickra on matching platforms.",
|
||||
"main": "wickra.darwin-arm64.node",
|
||||
"files": [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "wickra-darwin-x64",
|
||||
"version": "0.6.8",
|
||||
"version": "0.7.1",
|
||||
"description": "Native binding for wickra (macOS Intel). Installed automatically as an optional dependency of wickra on matching platforms.",
|
||||
"main": "wickra.darwin-x64.node",
|
||||
"files": [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "wickra-linux-arm64-gnu",
|
||||
"version": "0.6.8",
|
||||
"version": "0.7.1",
|
||||
"description": "Native binding for wickra (linux arm64 GNU). Installed automatically as an optional dependency of wickra on matching platforms.",
|
||||
"main": "wickra.linux-arm64-gnu.node",
|
||||
"files": [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "wickra-linux-x64-gnu",
|
||||
"version": "0.6.8",
|
||||
"version": "0.7.1",
|
||||
"description": "Native binding for wickra (linux x64 GNU). Installed automatically as an optional dependency of wickra on matching platforms.",
|
||||
"main": "wickra.linux-x64-gnu.node",
|
||||
"files": [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "wickra-win32-arm64-msvc",
|
||||
"version": "0.6.8",
|
||||
"version": "0.7.1",
|
||||
"description": "Native binding for wickra (Windows arm64 MSVC). Installed automatically as an optional dependency of wickra on matching platforms.",
|
||||
"main": "wickra.win32-arm64-msvc.node",
|
||||
"files": [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "wickra-win32-x64-msvc",
|
||||
"version": "0.6.8",
|
||||
"version": "0.7.1",
|
||||
"description": "Native binding for wickra (Windows x64 MSVC). Installed automatically as an optional dependency of wickra on matching platforms.",
|
||||
"main": "wickra.win32-x64-msvc.node",
|
||||
"files": [
|
||||
|
||||
Generated
+20
-20
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "wickra",
|
||||
"version": "0.6.8",
|
||||
"version": "0.7.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "wickra",
|
||||
"version": "0.6.8",
|
||||
"version": "0.7.1",
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"devDependencies": {
|
||||
"@napi-rs/cli": "^2.18.0"
|
||||
@@ -15,12 +15,12 @@
|
||||
"node": ">= 18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"wickra-darwin-arm64": "0.6.8",
|
||||
"wickra-darwin-x64": "0.6.8",
|
||||
"wickra-linux-arm64-gnu": "0.6.8",
|
||||
"wickra-linux-x64-gnu": "0.6.8",
|
||||
"wickra-win32-arm64-msvc": "0.6.8",
|
||||
"wickra-win32-x64-msvc": "0.6.8"
|
||||
"wickra-darwin-arm64": "0.7.1",
|
||||
"wickra-darwin-x64": "0.7.1",
|
||||
"wickra-linux-arm64-gnu": "0.7.1",
|
||||
"wickra-linux-x64-gnu": "0.7.1",
|
||||
"wickra-win32-arm64-msvc": "0.7.1",
|
||||
"wickra-win32-x64-msvc": "0.7.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/cli": {
|
||||
@@ -41,8 +41,8 @@
|
||||
}
|
||||
},
|
||||
"node_modules/wickra-darwin-arm64": {
|
||||
"version": "0.6.8",
|
||||
"resolved": "https://registry.npmjs.org/wickra-darwin-arm64/-/wickra-darwin-arm64-0.6.8.tgz",
|
||||
"version": "0.7.1",
|
||||
"resolved": "https://registry.npmjs.org/wickra-darwin-arm64/-/wickra-darwin-arm64-0.7.1.tgz",
|
||||
"integrity": "sha512-4eZiBR/yGUdr4nzhEUFy2i69XgNx64iI2ax/LPamsThgylC0KpHOZKK19QzJ2d9KbK4C8nMjME5FLuR+4GNEwQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
@@ -57,8 +57,8 @@
|
||||
}
|
||||
},
|
||||
"node_modules/wickra-darwin-x64": {
|
||||
"version": "0.6.8",
|
||||
"resolved": "https://registry.npmjs.org/wickra-darwin-x64/-/wickra-darwin-x64-0.6.8.tgz",
|
||||
"version": "0.7.1",
|
||||
"resolved": "https://registry.npmjs.org/wickra-darwin-x64/-/wickra-darwin-x64-0.7.1.tgz",
|
||||
"integrity": "sha512-6hf8zI3QPjTFp4zCpmgUwDvNtu6jHqNUHKD5e55POo0CgA52HkpyxSPtVm8TGTIZDI7kPjlbOdBM8CJ76mmXwA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
@@ -73,8 +73,8 @@
|
||||
}
|
||||
},
|
||||
"node_modules/wickra-linux-arm64-gnu": {
|
||||
"version": "0.6.8",
|
||||
"resolved": "https://registry.npmjs.org/wickra-linux-arm64-gnu/-/wickra-linux-arm64-gnu-0.6.8.tgz",
|
||||
"version": "0.7.1",
|
||||
"resolved": "https://registry.npmjs.org/wickra-linux-arm64-gnu/-/wickra-linux-arm64-gnu-0.7.1.tgz",
|
||||
"integrity": "sha512-kSe6y0xBMSiqdPLXNjwop5WZdHtvdBNKSEBCwZ4hFq33p4apW25/wrlzv9/oDuyD4kuPabJEhCCnFOplh58CUg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
@@ -89,8 +89,8 @@
|
||||
}
|
||||
},
|
||||
"node_modules/wickra-linux-x64-gnu": {
|
||||
"version": "0.6.8",
|
||||
"resolved": "https://registry.npmjs.org/wickra-linux-x64-gnu/-/wickra-linux-x64-gnu-0.6.8.tgz",
|
||||
"version": "0.7.1",
|
||||
"resolved": "https://registry.npmjs.org/wickra-linux-x64-gnu/-/wickra-linux-x64-gnu-0.7.1.tgz",
|
||||
"integrity": "sha512-tWBWS4qz7hxM4xnpFb59bhf6TaLwXq0Z3jEa/2l7r8PiHA94g8r8S53NRMiT+4yiL5hSWe/nUiC/YXdRrhEZ4g==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
@@ -105,8 +105,8 @@
|
||||
}
|
||||
},
|
||||
"node_modules/wickra-win32-arm64-msvc": {
|
||||
"version": "0.6.8",
|
||||
"resolved": "https://registry.npmjs.org/wickra-win32-arm64-msvc/-/wickra-win32-arm64-msvc-0.6.8.tgz",
|
||||
"version": "0.7.1",
|
||||
"resolved": "https://registry.npmjs.org/wickra-win32-arm64-msvc/-/wickra-win32-arm64-msvc-0.7.1.tgz",
|
||||
"integrity": "sha512-EXIckHxAtF75PUGDKRzXyqMe9ldP0JjSdu68WFN6iJfp+McYrGu6h40TEJlQ/oUEIoPqiZB/xhVyo/el5Lg7zw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
@@ -121,8 +121,8 @@
|
||||
}
|
||||
},
|
||||
"node_modules/wickra-win32-x64-msvc": {
|
||||
"version": "0.6.8",
|
||||
"resolved": "https://registry.npmjs.org/wickra-win32-x64-msvc/-/wickra-win32-x64-msvc-0.6.8.tgz",
|
||||
"version": "0.7.1",
|
||||
"resolved": "https://registry.npmjs.org/wickra-win32-x64-msvc/-/wickra-win32-x64-msvc-0.7.1.tgz",
|
||||
"integrity": "sha512-Yfsqq1Xwp6hdxMyLze411vNdo7BDwI6+lPSe7A9XdqyPecNDbtKwYLpsal2r8EHbNzqM+R8XnuRtUaEQS5VlUQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "wickra",
|
||||
"version": "0.6.8",
|
||||
"version": "0.7.1",
|
||||
"description": "Streaming-first technical indicators: incremental, fast, install-free. Node bindings powered by Rust.",
|
||||
"author": "kingchenc <support@wickra.org>",
|
||||
"main": "index.js",
|
||||
@@ -47,12 +47,12 @@
|
||||
"node": ">= 18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"wickra-linux-x64-gnu": "0.6.8",
|
||||
"wickra-linux-arm64-gnu": "0.6.8",
|
||||
"wickra-darwin-x64": "0.6.8",
|
||||
"wickra-darwin-arm64": "0.6.8",
|
||||
"wickra-win32-x64-msvc": "0.6.8",
|
||||
"wickra-win32-arm64-msvc": "0.6.8"
|
||||
"wickra-linux-x64-gnu": "0.7.1",
|
||||
"wickra-linux-arm64-gnu": "0.7.1",
|
||||
"wickra-darwin-x64": "0.7.1",
|
||||
"wickra-darwin-arm64": "0.7.1",
|
||||
"wickra-win32-x64-msvc": "0.7.1",
|
||||
"wickra-win32-arm64-msvc": "0.7.1"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "napi build --platform --release",
|
||||
|
||||
@@ -884,6 +884,11 @@ node_pair_indicator!(
|
||||
"BetaNeutralSpread",
|
||||
wc::BetaNeutralSpread
|
||||
);
|
||||
node_pair_indicator!(
|
||||
HasbrouckInformationShareNode,
|
||||
"HasbrouckInformationShare",
|
||||
wc::HasbrouckInformationShare
|
||||
);
|
||||
|
||||
// ============================== PairSpreadZScore ==============================
|
||||
|
||||
@@ -12618,6 +12623,171 @@ impl CandleVolumeNode {
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== Frying Pan Bottom ==============================
|
||||
|
||||
#[napi(js_name = "FryPanBottom")]
|
||||
pub struct FryPanBottomNode {
|
||||
inner: wc::FryPanBottom,
|
||||
}
|
||||
|
||||
#[napi]
|
||||
impl FryPanBottomNode {
|
||||
#[napi(constructor)]
|
||||
pub fn new(period: u32) -> napi::Result<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::FryPanBottom::new(period as usize).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
#[napi]
|
||||
pub fn update(&mut self, high: f64, low: f64, close: f64) -> napi::Result<Option<f64>> {
|
||||
Ok(self.inner.update(cnd(high, low, close, 0.0)?))
|
||||
}
|
||||
#[napi]
|
||||
pub fn batch(
|
||||
&mut self,
|
||||
high: Vec<f64>,
|
||||
low: Vec<f64>,
|
||||
close: Vec<f64>,
|
||||
) -> napi::Result<Vec<f64>> {
|
||||
if high.len() != low.len() || low.len() != close.len() {
|
||||
return Err(NapiError::from_reason(
|
||||
"high, low, close must be equal length".to_string(),
|
||||
));
|
||||
}
|
||||
let mut out = Vec::with_capacity(high.len());
|
||||
for i in 0..high.len() {
|
||||
out.push(
|
||||
self.inner
|
||||
.update(cnd(high[i], low[i], close[i], 0.0)?)
|
||||
.unwrap_or(f64::NAN),
|
||||
);
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== Dumpling Top ==============================
|
||||
|
||||
#[napi(js_name = "DumplingTop")]
|
||||
pub struct DumplingTopNode {
|
||||
inner: wc::DumplingTop,
|
||||
}
|
||||
|
||||
#[napi]
|
||||
impl DumplingTopNode {
|
||||
#[napi(constructor)]
|
||||
pub fn new(period: u32) -> napi::Result<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::DumplingTop::new(period as usize).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
#[napi]
|
||||
pub fn update(&mut self, high: f64, low: f64, close: f64) -> napi::Result<Option<f64>> {
|
||||
Ok(self.inner.update(cnd(high, low, close, 0.0)?))
|
||||
}
|
||||
#[napi]
|
||||
pub fn batch(
|
||||
&mut self,
|
||||
high: Vec<f64>,
|
||||
low: Vec<f64>,
|
||||
close: Vec<f64>,
|
||||
) -> napi::Result<Vec<f64>> {
|
||||
if high.len() != low.len() || low.len() != close.len() {
|
||||
return Err(NapiError::from_reason(
|
||||
"high, low, close must be equal length".to_string(),
|
||||
));
|
||||
}
|
||||
let mut out = Vec::with_capacity(high.len());
|
||||
for i in 0..high.len() {
|
||||
out.push(
|
||||
self.inner
|
||||
.update(cnd(high[i], low[i], close[i], 0.0)?)
|
||||
.unwrap_or(f64::NAN),
|
||||
);
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== New Price Lines ==============================
|
||||
|
||||
#[napi(js_name = "NewPriceLines")]
|
||||
pub struct NewPriceLinesNode {
|
||||
inner: wc::NewPriceLines,
|
||||
}
|
||||
|
||||
#[napi]
|
||||
impl NewPriceLinesNode {
|
||||
#[napi(constructor)]
|
||||
pub fn new(count: u32) -> napi::Result<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::NewPriceLines::new(count as usize).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
#[napi]
|
||||
pub fn update(&mut self, high: f64, low: f64, close: f64) -> napi::Result<Option<f64>> {
|
||||
Ok(self.inner.update(cnd(high, low, close, 0.0)?))
|
||||
}
|
||||
#[napi]
|
||||
pub fn batch(
|
||||
&mut self,
|
||||
high: Vec<f64>,
|
||||
low: Vec<f64>,
|
||||
close: Vec<f64>,
|
||||
) -> napi::Result<Vec<f64>> {
|
||||
if high.len() != low.len() || low.len() != close.len() {
|
||||
return Err(NapiError::from_reason(
|
||||
"high, low, close must be equal length".to_string(),
|
||||
));
|
||||
}
|
||||
let mut out = Vec::with_capacity(high.len());
|
||||
for i in 0..high.len() {
|
||||
out.push(
|
||||
self.inner
|
||||
.update(cnd(high[i], low[i], close[i], 0.0)?)
|
||||
.unwrap_or(f64::NAN),
|
||||
);
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== ValueArea ==============================
|
||||
|
||||
#[napi(object)]
|
||||
@@ -13271,6 +13441,9 @@ node_candle_pattern!(TdClopNode, wc::TdClop, "TDClop");
|
||||
node_candle_pattern!(TdClopwinNode, wc::TdClopwin, "TDClopwin");
|
||||
node_candle_pattern!(TdPropulsionNode, wc::TdPropulsion, "TDPropulsion");
|
||||
node_candle_pattern!(TdTrapNode, wc::TdTrap, "TDTrap");
|
||||
node_candle_pattern!(TristarNode, wc::Tristar, "Tristar");
|
||||
node_candle_pattern!(HaramiCrossNode, wc::HaramiCross, "HaramiCross");
|
||||
node_candle_pattern!(TowerTopBottomNode, wc::TowerTopBottom, "TowerTopBottom");
|
||||
|
||||
// ============================== Microstructure: Order Book ==============================
|
||||
//
|
||||
@@ -13571,6 +13744,108 @@ impl TradeImbalanceNode {
|
||||
}
|
||||
}
|
||||
|
||||
// Trade-sign autocorrelation carries a `period` parameter, so it is hand-written.
|
||||
#[napi(js_name = "TradeSignAutocorrelation")]
|
||||
pub struct TradeSignAutocorrelationNode {
|
||||
inner: wc::TradeSignAutocorrelation,
|
||||
}
|
||||
|
||||
#[napi]
|
||||
impl TradeSignAutocorrelationNode {
|
||||
#[napi(constructor)]
|
||||
pub fn new(period: u32) -> napi::Result<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::TradeSignAutocorrelation::new(period as usize).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
#[napi]
|
||||
pub fn update(&mut self, price: f64, size: f64, is_buy: bool) -> napi::Result<Option<f64>> {
|
||||
Ok(self.inner.update(build_trade(price, size, is_buy)?))
|
||||
}
|
||||
#[napi]
|
||||
pub fn batch(
|
||||
&mut self,
|
||||
price: Vec<f64>,
|
||||
size: Vec<f64>,
|
||||
is_buy: Vec<bool>,
|
||||
) -> napi::Result<Vec<f64>> {
|
||||
if price.len() != size.len() || size.len() != is_buy.len() {
|
||||
return Err(NapiError::from_reason(
|
||||
"price, size, is_buy must be equal length".to_string(),
|
||||
));
|
||||
}
|
||||
let mut out = Vec::with_capacity(price.len());
|
||||
for i in 0..price.len() {
|
||||
let trade = build_trade(price[i], size[i], is_buy[i])?;
|
||||
out.push(self.inner.update(trade).unwrap_or(f64::NAN));
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// PIN carries a `window` parameter, so it is hand-written.
|
||||
#[napi(js_name = "Pin")]
|
||||
pub struct PinNode {
|
||||
inner: wc::Pin,
|
||||
}
|
||||
|
||||
#[napi]
|
||||
impl PinNode {
|
||||
#[napi(constructor)]
|
||||
pub fn new(window: u32) -> napi::Result<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::Pin::new(window as usize).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
#[napi]
|
||||
pub fn update(&mut self, price: f64, size: f64, is_buy: bool) -> napi::Result<Option<f64>> {
|
||||
Ok(self.inner.update(build_trade(price, size, is_buy)?))
|
||||
}
|
||||
#[napi]
|
||||
pub fn batch(
|
||||
&mut self,
|
||||
price: Vec<f64>,
|
||||
size: Vec<f64>,
|
||||
is_buy: Vec<bool>,
|
||||
) -> napi::Result<Vec<f64>> {
|
||||
if price.len() != size.len() || size.len() != is_buy.len() {
|
||||
return Err(NapiError::from_reason(
|
||||
"price, size, is_buy must be equal length".to_string(),
|
||||
));
|
||||
}
|
||||
let mut out = Vec::with_capacity(price.len());
|
||||
for i in 0..price.len() {
|
||||
let trade = build_trade(price[i], size[i], is_buy[i])?;
|
||||
out.push(self.inner.update(trade).unwrap_or(f64::NAN));
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// Order Flow Imbalance: order-book input with a `period` parameter.
|
||||
#[napi(js_name = "OrderFlowImbalance")]
|
||||
pub struct OrderFlowImbalanceNode {
|
||||
@@ -14175,6 +14450,50 @@ fn deriv_taker(taker_buy_volume: f64, taker_sell_volume: f64) -> napi::Result<wc
|
||||
.map_err(map_err)
|
||||
}
|
||||
|
||||
fn deriv_oi_long_short(
|
||||
open_interest: f64,
|
||||
long_size: f64,
|
||||
short_size: f64,
|
||||
) -> napi::Result<wc::DerivativesTick> {
|
||||
wc::DerivativesTick::new(
|
||||
0.0,
|
||||
1.0,
|
||||
1.0,
|
||||
1.0,
|
||||
open_interest,
|
||||
long_size,
|
||||
short_size,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0,
|
||||
)
|
||||
.map_err(map_err)
|
||||
}
|
||||
|
||||
fn deriv_oi_taker(
|
||||
open_interest: f64,
|
||||
taker_buy_volume: f64,
|
||||
taker_sell_volume: f64,
|
||||
) -> napi::Result<wc::DerivativesTick> {
|
||||
wc::DerivativesTick::new(
|
||||
0.0,
|
||||
1.0,
|
||||
1.0,
|
||||
1.0,
|
||||
open_interest,
|
||||
0.0,
|
||||
0.0,
|
||||
taker_buy_volume,
|
||||
taker_sell_volume,
|
||||
0.0,
|
||||
0.0,
|
||||
0,
|
||||
)
|
||||
.map_err(map_err)
|
||||
}
|
||||
|
||||
fn deriv_liquidation(
|
||||
long_liquidation: f64,
|
||||
short_liquidation: f64,
|
||||
@@ -14890,6 +15209,288 @@ impl CalendarSpreadNode {
|
||||
}
|
||||
}
|
||||
|
||||
// Estimated leverage ratio: open interest over aggregate long+short size.
|
||||
#[napi(js_name = "EstimatedLeverageRatio")]
|
||||
pub struct EstimatedLeverageRatioNode {
|
||||
inner: wc::EstimatedLeverageRatio,
|
||||
}
|
||||
|
||||
impl Default for EstimatedLeverageRatioNode {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[napi]
|
||||
impl EstimatedLeverageRatioNode {
|
||||
#[napi(constructor)]
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
inner: wc::EstimatedLeverageRatio::new(),
|
||||
}
|
||||
}
|
||||
#[napi]
|
||||
pub fn update(
|
||||
&mut self,
|
||||
open_interest: f64,
|
||||
long_size: f64,
|
||||
short_size: f64,
|
||||
) -> napi::Result<Option<f64>> {
|
||||
Ok(self
|
||||
.inner
|
||||
.update(deriv_oi_long_short(open_interest, long_size, short_size)?))
|
||||
}
|
||||
#[napi]
|
||||
pub fn batch(
|
||||
&mut self,
|
||||
open_interest: Vec<f64>,
|
||||
long_size: Vec<f64>,
|
||||
short_size: Vec<f64>,
|
||||
) -> napi::Result<Vec<f64>> {
|
||||
if open_interest.len() != long_size.len() || long_size.len() != short_size.len() {
|
||||
return Err(NapiError::from_reason(
|
||||
"open_interest, long_size, short_size must be equal length".to_string(),
|
||||
));
|
||||
}
|
||||
let mut out = Vec::with_capacity(open_interest.len());
|
||||
for i in 0..open_interest.len() {
|
||||
out.push(
|
||||
self.inner
|
||||
.update(deriv_oi_long_short(
|
||||
open_interest[i],
|
||||
long_size[i],
|
||||
short_size[i],
|
||||
)?)
|
||||
.unwrap_or(f64::NAN),
|
||||
);
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// OI-to-volume ratio: open interest over taker buy+sell volume.
|
||||
#[napi(js_name = "OiToVolumeRatio")]
|
||||
pub struct OiToVolumeRatioNode {
|
||||
inner: wc::OiToVolumeRatio,
|
||||
}
|
||||
|
||||
impl Default for OiToVolumeRatioNode {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[napi]
|
||||
impl OiToVolumeRatioNode {
|
||||
#[napi(constructor)]
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
inner: wc::OiToVolumeRatio::new(),
|
||||
}
|
||||
}
|
||||
#[napi]
|
||||
pub fn update(
|
||||
&mut self,
|
||||
open_interest: f64,
|
||||
taker_buy_volume: f64,
|
||||
taker_sell_volume: f64,
|
||||
) -> napi::Result<Option<f64>> {
|
||||
Ok(self.inner.update(deriv_oi_taker(
|
||||
open_interest,
|
||||
taker_buy_volume,
|
||||
taker_sell_volume,
|
||||
)?))
|
||||
}
|
||||
#[napi]
|
||||
pub fn batch(
|
||||
&mut self,
|
||||
open_interest: Vec<f64>,
|
||||
taker_buy_volume: Vec<f64>,
|
||||
taker_sell_volume: Vec<f64>,
|
||||
) -> napi::Result<Vec<f64>> {
|
||||
if open_interest.len() != taker_buy_volume.len()
|
||||
|| taker_buy_volume.len() != taker_sell_volume.len()
|
||||
{
|
||||
return Err(NapiError::from_reason(
|
||||
"open_interest, taker_buy_volume, taker_sell_volume must be equal length"
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
let mut out = Vec::with_capacity(open_interest.len());
|
||||
for i in 0..open_interest.len() {
|
||||
out.push(
|
||||
self.inner
|
||||
.update(deriv_oi_taker(
|
||||
open_interest[i],
|
||||
taker_buy_volume[i],
|
||||
taker_sell_volume[i],
|
||||
)?)
|
||||
.unwrap_or(f64::NAN),
|
||||
);
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// Perpetual premium index: relative premium of mark over index price.
|
||||
#[napi(js_name = "PerpetualPremiumIndex")]
|
||||
pub struct PerpetualPremiumIndexNode {
|
||||
inner: wc::PerpetualPremiumIndex,
|
||||
}
|
||||
|
||||
impl Default for PerpetualPremiumIndexNode {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[napi]
|
||||
impl PerpetualPremiumIndexNode {
|
||||
#[napi(constructor)]
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
inner: wc::PerpetualPremiumIndex::new(),
|
||||
}
|
||||
}
|
||||
#[napi]
|
||||
pub fn update(&mut self, mark_price: f64, index_price: f64) -> napi::Result<Option<f64>> {
|
||||
Ok(self.inner.update(deriv_basis(mark_price, index_price)?))
|
||||
}
|
||||
#[napi]
|
||||
pub fn batch(&mut self, mark_price: Vec<f64>, index_price: Vec<f64>) -> napi::Result<Vec<f64>> {
|
||||
if mark_price.len() != index_price.len() {
|
||||
return Err(NapiError::from_reason(
|
||||
"mark_price and index_price must be equal length".to_string(),
|
||||
));
|
||||
}
|
||||
let mut out = Vec::with_capacity(mark_price.len());
|
||||
for i in 0..mark_price.len() {
|
||||
out.push(
|
||||
self.inner
|
||||
.update(deriv_basis(mark_price[i], index_price[i])?)
|
||||
.unwrap_or(f64::NAN),
|
||||
);
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// Funding-implied APR: per-interval funding annualised.
|
||||
#[napi(js_name = "FundingImpliedApr")]
|
||||
pub struct FundingImpliedAprNode {
|
||||
inner: wc::FundingImpliedApr,
|
||||
}
|
||||
|
||||
#[napi]
|
||||
impl FundingImpliedAprNode {
|
||||
#[napi(constructor)]
|
||||
pub fn new(intervals_per_year: f64) -> napi::Result<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::FundingImpliedApr::new(intervals_per_year).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
#[napi]
|
||||
pub fn update(&mut self, funding_rate: f64) -> napi::Result<Option<f64>> {
|
||||
Ok(self.inner.update(deriv_funding(funding_rate)?))
|
||||
}
|
||||
#[napi]
|
||||
pub fn batch(&mut self, funding_rate: Vec<f64>) -> napi::Result<Vec<f64>> {
|
||||
let mut out = Vec::with_capacity(funding_rate.len());
|
||||
for r in funding_rate {
|
||||
out.push(self.inner.update(deriv_funding(r)?).unwrap_or(f64::NAN));
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// Open-interest momentum: rate-of-change of open interest over a window.
|
||||
#[napi(js_name = "OpenInterestMomentum")]
|
||||
pub struct OpenInterestMomentumNode {
|
||||
inner: wc::OpenInterestMomentum,
|
||||
}
|
||||
|
||||
#[napi]
|
||||
impl OpenInterestMomentumNode {
|
||||
#[napi(constructor)]
|
||||
pub fn new(period: u32) -> napi::Result<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::OpenInterestMomentum::new(period as usize).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
#[napi]
|
||||
pub fn update(&mut self, open_interest: f64) -> napi::Result<Option<f64>> {
|
||||
Ok(self.inner.update(deriv_oi(open_interest)?))
|
||||
}
|
||||
#[napi]
|
||||
pub fn batch(&mut self, open_interest: Vec<f64>) -> napi::Result<Vec<f64>> {
|
||||
let mut out = Vec::with_capacity(open_interest.len());
|
||||
for oi in open_interest {
|
||||
out.push(self.inner.update(deriv_oi(oi)?).unwrap_or(f64::NAN));
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Market Breadth (CrossSection input) ----------
|
||||
//
|
||||
// A breadth tick is the per-symbol state of the whole universe, passed as four
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "maturin"
|
||||
|
||||
[project]
|
||||
name = "wickra"
|
||||
version = "0.6.8"
|
||||
version = "0.7.1"
|
||||
description = "Streaming-first technical indicators: incremental, fast, install-free."
|
||||
readme = "README.md"
|
||||
license = "MIT OR Apache-2.0"
|
||||
|
||||
@@ -360,6 +360,12 @@ from ._wickra import (
|
||||
KagiBars,
|
||||
PointAndFigureBars,
|
||||
# Candlestick patterns
|
||||
TowerTopBottom,
|
||||
HaramiCross,
|
||||
Tristar,
|
||||
FryPanBottom,
|
||||
DumplingTop,
|
||||
NewPriceLines,
|
||||
Doji,
|
||||
Hammer,
|
||||
InvertedHammer,
|
||||
@@ -458,6 +464,8 @@ from ._wickra import (
|
||||
QuotedSpread,
|
||||
DepthSlope,
|
||||
# Microstructure: trade flow
|
||||
Pin,
|
||||
TradeSignAutocorrelation,
|
||||
RollMeasure,
|
||||
AmihudIlliquidity,
|
||||
Vpin,
|
||||
@@ -465,12 +473,18 @@ from ._wickra import (
|
||||
CumulativeVolumeDelta,
|
||||
TradeImbalance,
|
||||
# Microstructure: price impact
|
||||
HasbrouckInformationShare,
|
||||
EffectiveSpread,
|
||||
RealizedSpread,
|
||||
KylesLambda,
|
||||
# Microstructure: footprint
|
||||
Footprint,
|
||||
# Derivatives
|
||||
OpenInterestMomentum,
|
||||
FundingImpliedApr,
|
||||
PerpetualPremiumIndex,
|
||||
OiToVolumeRatio,
|
||||
EstimatedLeverageRatio,
|
||||
FundingRate,
|
||||
FundingRateMean,
|
||||
FundingRateZScore,
|
||||
@@ -869,6 +883,12 @@ __all__ = [
|
||||
"KagiBars",
|
||||
"PointAndFigureBars",
|
||||
# Candlestick patterns
|
||||
"TowerTopBottom",
|
||||
"HaramiCross",
|
||||
"Tristar",
|
||||
"FryPanBottom",
|
||||
"DumplingTop",
|
||||
"NewPriceLines",
|
||||
"Doji",
|
||||
"Hammer",
|
||||
"InvertedHammer",
|
||||
@@ -967,6 +987,8 @@ __all__ = [
|
||||
"QuotedSpread",
|
||||
"DepthSlope",
|
||||
# Microstructure: trade flow
|
||||
"Pin",
|
||||
"TradeSignAutocorrelation",
|
||||
"RollMeasure",
|
||||
"AmihudIlliquidity",
|
||||
"Vpin",
|
||||
@@ -974,12 +996,18 @@ __all__ = [
|
||||
"CumulativeVolumeDelta",
|
||||
"TradeImbalance",
|
||||
# Microstructure: price impact
|
||||
"HasbrouckInformationShare",
|
||||
"EffectiveSpread",
|
||||
"RealizedSpread",
|
||||
"KylesLambda",
|
||||
# Microstructure: footprint
|
||||
"Footprint",
|
||||
# Derivatives
|
||||
"OpenInterestMomentum",
|
||||
"FundingImpliedApr",
|
||||
"PerpetualPremiumIndex",
|
||||
"OiToVolumeRatio",
|
||||
"EstimatedLeverageRatio",
|
||||
"FundingRate",
|
||||
"FundingRateMean",
|
||||
"FundingRateZScore",
|
||||
|
||||
@@ -15705,6 +15705,186 @@ impl PyCandleVolume {
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== Frying Pan Bottom ==============================
|
||||
|
||||
#[pyclass(name = "FryPanBottom", module = "wickra._wickra", skip_from_py_object)]
|
||||
#[derive(Clone)]
|
||||
struct PyFryPanBottom {
|
||||
inner: wc::FryPanBottom,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyFryPanBottom {
|
||||
#[new]
|
||||
#[pyo3(signature = (period=9))]
|
||||
fn new(period: usize) -> PyResult<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::FryPanBottom::new(period).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
fn update(&mut self, candle: &Bound<'_, PyAny>) -> PyResult<Option<f64>> {
|
||||
let c = extract_candle(candle)?;
|
||||
Ok(self.inner.update(c))
|
||||
}
|
||||
fn batch<'py>(
|
||||
&mut self,
|
||||
py: Python<'py>,
|
||||
high: PyReadonlyArray1<'py, f64>,
|
||||
low: PyReadonlyArray1<'py, f64>,
|
||||
close: PyReadonlyArray1<'py, f64>,
|
||||
) -> PyResult<Bound<'py, PyArray1<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))?;
|
||||
let c = close
|
||||
.as_slice()
|
||||
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||
if h.len() != l.len() || l.len() != c.len() {
|
||||
return Err(PyValueError::new_err(
|
||||
"high, low, close must be equal length",
|
||||
));
|
||||
}
|
||||
let mut out = Vec::with_capacity(h.len());
|
||||
for i in 0..h.len() {
|
||||
let candle = wc::Candle::new(c[i], h[i], l[i], c[i], 0.0, 0).map_err(map_err)?;
|
||||
out.push(self.inner.update(candle).unwrap_or(f64::NAN));
|
||||
}
|
||||
Ok(out.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()
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== Dumpling Top ==============================
|
||||
|
||||
#[pyclass(name = "DumplingTop", module = "wickra._wickra", skip_from_py_object)]
|
||||
#[derive(Clone)]
|
||||
struct PyDumplingTop {
|
||||
inner: wc::DumplingTop,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyDumplingTop {
|
||||
#[new]
|
||||
#[pyo3(signature = (period=9))]
|
||||
fn new(period: usize) -> PyResult<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::DumplingTop::new(period).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
fn update(&mut self, candle: &Bound<'_, PyAny>) -> PyResult<Option<f64>> {
|
||||
let c = extract_candle(candle)?;
|
||||
Ok(self.inner.update(c))
|
||||
}
|
||||
fn batch<'py>(
|
||||
&mut self,
|
||||
py: Python<'py>,
|
||||
high: PyReadonlyArray1<'py, f64>,
|
||||
low: PyReadonlyArray1<'py, f64>,
|
||||
close: PyReadonlyArray1<'py, f64>,
|
||||
) -> PyResult<Bound<'py, PyArray1<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))?;
|
||||
let c = close
|
||||
.as_slice()
|
||||
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||
if h.len() != l.len() || l.len() != c.len() {
|
||||
return Err(PyValueError::new_err(
|
||||
"high, low, close must be equal length",
|
||||
));
|
||||
}
|
||||
let mut out = Vec::with_capacity(h.len());
|
||||
for i in 0..h.len() {
|
||||
let candle = wc::Candle::new(c[i], h[i], l[i], c[i], 0.0, 0).map_err(map_err)?;
|
||||
out.push(self.inner.update(candle).unwrap_or(f64::NAN));
|
||||
}
|
||||
Ok(out.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()
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== New Price Lines ==============================
|
||||
|
||||
#[pyclass(name = "NewPriceLines", module = "wickra._wickra", skip_from_py_object)]
|
||||
#[derive(Clone)]
|
||||
struct PyNewPriceLines {
|
||||
inner: wc::NewPriceLines,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyNewPriceLines {
|
||||
#[new]
|
||||
#[pyo3(signature = (count=5))]
|
||||
fn new(count: usize) -> PyResult<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::NewPriceLines::new(count).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
fn update(&mut self, candle: &Bound<'_, PyAny>) -> PyResult<Option<f64>> {
|
||||
let c = extract_candle(candle)?;
|
||||
Ok(self.inner.update(c))
|
||||
}
|
||||
fn batch<'py>(
|
||||
&mut self,
|
||||
py: Python<'py>,
|
||||
high: PyReadonlyArray1<'py, f64>,
|
||||
low: PyReadonlyArray1<'py, f64>,
|
||||
close: PyReadonlyArray1<'py, f64>,
|
||||
) -> PyResult<Bound<'py, PyArray1<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))?;
|
||||
let c = close
|
||||
.as_slice()
|
||||
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||
if h.len() != l.len() || l.len() != c.len() {
|
||||
return Err(PyValueError::new_err(
|
||||
"high, low, close must be equal length",
|
||||
));
|
||||
}
|
||||
let mut out = Vec::with_capacity(h.len());
|
||||
for i in 0..h.len() {
|
||||
let candle = wc::Candle::new(c[i], h[i], l[i], c[i], 0.0, 0).map_err(map_err)?;
|
||||
out.push(self.inner.update(candle).unwrap_or(f64::NAN));
|
||||
}
|
||||
Ok(out.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()
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== CoefficientOfVariation ==============================
|
||||
|
||||
#[pyclass(
|
||||
@@ -16800,6 +16980,70 @@ impl PyRollingCorrelation {
|
||||
}
|
||||
}
|
||||
|
||||
// ========================= HasbrouckInformationShare =========================
|
||||
|
||||
#[pyclass(
|
||||
name = "HasbrouckInformationShare",
|
||||
module = "wickra._wickra",
|
||||
skip_from_py_object
|
||||
)]
|
||||
#[derive(Clone)]
|
||||
struct PyHasbrouckInformationShare {
|
||||
inner: wc::HasbrouckInformationShare,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyHasbrouckInformationShare {
|
||||
#[new]
|
||||
#[pyo3(signature = (period=20))]
|
||||
fn new(period: usize) -> PyResult<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::HasbrouckInformationShare::new(period).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
fn update(&mut self, a: f64, b: f64) -> Option<f64> {
|
||||
self.inner.update((a, b))
|
||||
}
|
||||
/// Batch over two equally-sized numpy arrays: `a` and `b`.
|
||||
fn batch<'py>(
|
||||
&mut self,
|
||||
py: Python<'py>,
|
||||
a: PyReadonlyArray1<'py, f64>,
|
||||
b: PyReadonlyArray1<'py, f64>,
|
||||
) -> PyResult<Bound<'py, PyArray1<f64>>> {
|
||||
let xs = a
|
||||
.as_slice()
|
||||
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||
let ys = b
|
||||
.as_slice()
|
||||
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||
if xs.len() != ys.len() {
|
||||
return Err(PyValueError::new_err("a and b must be equal length"));
|
||||
}
|
||||
let mut out = Vec::with_capacity(xs.len());
|
||||
for i in 0..xs.len() {
|
||||
out.push(self.inner.update((xs[i], ys[i])).unwrap_or(f64::NAN));
|
||||
}
|
||||
Ok(out.into_pyarray(py))
|
||||
}
|
||||
#[getter]
|
||||
fn period(&self) -> usize {
|
||||
self.inner.period()
|
||||
}
|
||||
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 {
|
||||
format!("HasbrouckInformationShare(period={})", self.inner.period())
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== RollingCovariance ==============================
|
||||
|
||||
#[pyclass(
|
||||
@@ -18181,6 +18425,9 @@ candle_pattern_no_param!(PyTdClop, wc::TdClop, "TDClop");
|
||||
candle_pattern_no_param!(PyTdClopwin, wc::TdClopwin, "TDClopwin");
|
||||
candle_pattern_no_param!(PyTdPropulsion, wc::TdPropulsion, "TDPropulsion");
|
||||
candle_pattern_no_param!(PyTdTrap, wc::TdTrap, "TDTrap");
|
||||
candle_pattern_no_param!(PyTristar, wc::Tristar, "Tristar");
|
||||
candle_pattern_no_param!(PyHaramiCross, wc::HaramiCross, "HaramiCross");
|
||||
candle_pattern_no_param!(PyTowerTopBottom, wc::TowerTopBottom, "TowerTopBottom");
|
||||
// ============================== Microstructure: Order Book ==============================
|
||||
//
|
||||
// Order-book indicators consume a depth snapshot rather than OHLCV. Streaming
|
||||
@@ -18469,6 +18716,112 @@ impl PyTradeImbalance {
|
||||
}
|
||||
}
|
||||
|
||||
// Trade-sign autocorrelation carries a `period` parameter, so it is hand-written.
|
||||
#[pyclass(
|
||||
name = "TradeSignAutocorrelation",
|
||||
module = "wickra._wickra",
|
||||
skip_from_py_object
|
||||
)]
|
||||
#[derive(Clone)]
|
||||
struct PyTradeSignAutocorrelation {
|
||||
inner: wc::TradeSignAutocorrelation,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyTradeSignAutocorrelation {
|
||||
#[new]
|
||||
fn new(period: usize) -> PyResult<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::TradeSignAutocorrelation::new(period).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
fn update(&mut self, price: f64, size: f64, is_buy: bool) -> PyResult<Option<f64>> {
|
||||
Ok(self.inner.update(build_trade(price, size, is_buy)?))
|
||||
}
|
||||
fn batch<'py>(
|
||||
&mut self,
|
||||
py: Python<'py>,
|
||||
price: Vec<f64>,
|
||||
size: Vec<f64>,
|
||||
is_buy: Vec<bool>,
|
||||
) -> PyResult<Bound<'py, PyArray1<f64>>> {
|
||||
if price.len() != size.len() || size.len() != is_buy.len() {
|
||||
return Err(PyValueError::new_err(
|
||||
"price, size, is_buy must be equal length",
|
||||
));
|
||||
}
|
||||
let mut out = Vec::with_capacity(price.len());
|
||||
for i in 0..price.len() {
|
||||
let trade = build_trade(price[i], size[i], is_buy[i])?;
|
||||
out.push(self.inner.update(trade).unwrap_or(f64::NAN));
|
||||
}
|
||||
Ok(out.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 {
|
||||
format!("TradeSignAutocorrelation(period={})", self.inner.period())
|
||||
}
|
||||
}
|
||||
|
||||
// PIN carries a `window` parameter, so it is hand-written.
|
||||
#[pyclass(name = "Pin", module = "wickra._wickra", skip_from_py_object)]
|
||||
#[derive(Clone)]
|
||||
struct PyPin {
|
||||
inner: wc::Pin,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyPin {
|
||||
#[new]
|
||||
fn new(window: usize) -> PyResult<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::Pin::new(window).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
fn update(&mut self, price: f64, size: f64, is_buy: bool) -> PyResult<Option<f64>> {
|
||||
Ok(self.inner.update(build_trade(price, size, is_buy)?))
|
||||
}
|
||||
fn batch<'py>(
|
||||
&mut self,
|
||||
py: Python<'py>,
|
||||
price: Vec<f64>,
|
||||
size: Vec<f64>,
|
||||
is_buy: Vec<bool>,
|
||||
) -> PyResult<Bound<'py, PyArray1<f64>>> {
|
||||
if price.len() != size.len() || size.len() != is_buy.len() {
|
||||
return Err(PyValueError::new_err(
|
||||
"price, size, is_buy must be equal length",
|
||||
));
|
||||
}
|
||||
let mut out = Vec::with_capacity(price.len());
|
||||
for i in 0..price.len() {
|
||||
let trade = build_trade(price[i], size[i], is_buy[i])?;
|
||||
out.push(self.inner.update(trade).unwrap_or(f64::NAN));
|
||||
}
|
||||
Ok(out.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 {
|
||||
format!("Pin(window={})", self.inner.window())
|
||||
}
|
||||
}
|
||||
|
||||
// Order Flow Imbalance carries a `period` parameter and an order-book input,
|
||||
// so it is hand-written.
|
||||
#[pyclass(
|
||||
@@ -19067,6 +19420,50 @@ fn deriv_taker(taker_buy_volume: f64, taker_sell_volume: f64) -> PyResult<wc::De
|
||||
.map_err(map_err)
|
||||
}
|
||||
|
||||
fn deriv_oi_long_short(
|
||||
open_interest: f64,
|
||||
long_size: f64,
|
||||
short_size: f64,
|
||||
) -> PyResult<wc::DerivativesTick> {
|
||||
wc::DerivativesTick::new(
|
||||
0.0,
|
||||
1.0,
|
||||
1.0,
|
||||
1.0,
|
||||
open_interest,
|
||||
long_size,
|
||||
short_size,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0,
|
||||
)
|
||||
.map_err(map_err)
|
||||
}
|
||||
|
||||
fn deriv_oi_taker(
|
||||
open_interest: f64,
|
||||
taker_buy_volume: f64,
|
||||
taker_sell_volume: f64,
|
||||
) -> PyResult<wc::DerivativesTick> {
|
||||
wc::DerivativesTick::new(
|
||||
0.0,
|
||||
1.0,
|
||||
1.0,
|
||||
1.0,
|
||||
open_interest,
|
||||
0.0,
|
||||
0.0,
|
||||
taker_buy_volume,
|
||||
taker_sell_volume,
|
||||
0.0,
|
||||
0.0,
|
||||
0,
|
||||
)
|
||||
.map_err(map_err)
|
||||
}
|
||||
|
||||
fn deriv_liquidation(
|
||||
long_liquidation: f64,
|
||||
short_liquidation: f64,
|
||||
@@ -19787,6 +20184,302 @@ impl PyCalendarSpread {
|
||||
}
|
||||
}
|
||||
|
||||
// Estimated leverage ratio: open interest over aggregate long+short size.
|
||||
#[pyclass(
|
||||
name = "EstimatedLeverageRatio",
|
||||
module = "wickra._wickra",
|
||||
skip_from_py_object
|
||||
)]
|
||||
#[derive(Clone)]
|
||||
struct PyEstimatedLeverageRatio {
|
||||
inner: wc::EstimatedLeverageRatio,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyEstimatedLeverageRatio {
|
||||
#[new]
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
inner: wc::EstimatedLeverageRatio::new(),
|
||||
}
|
||||
}
|
||||
fn update(
|
||||
&mut self,
|
||||
open_interest: f64,
|
||||
long_size: f64,
|
||||
short_size: f64,
|
||||
) -> PyResult<Option<f64>> {
|
||||
Ok(self
|
||||
.inner
|
||||
.update(deriv_oi_long_short(open_interest, long_size, short_size)?))
|
||||
}
|
||||
fn batch<'py>(
|
||||
&mut self,
|
||||
py: Python<'py>,
|
||||
open_interest: Vec<f64>,
|
||||
long_size: Vec<f64>,
|
||||
short_size: Vec<f64>,
|
||||
) -> PyResult<Bound<'py, PyArray1<f64>>> {
|
||||
if open_interest.len() != long_size.len() || long_size.len() != short_size.len() {
|
||||
return Err(PyValueError::new_err(
|
||||
"open_interest, long_size, short_size must be equal length",
|
||||
));
|
||||
}
|
||||
let mut out = Vec::with_capacity(open_interest.len());
|
||||
for i in 0..open_interest.len() {
|
||||
out.push(
|
||||
self.inner
|
||||
.update(deriv_oi_long_short(
|
||||
open_interest[i],
|
||||
long_size[i],
|
||||
short_size[i],
|
||||
)?)
|
||||
.unwrap_or(f64::NAN),
|
||||
);
|
||||
}
|
||||
Ok(out.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 {
|
||||
"EstimatedLeverageRatio()".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
// OI-to-volume ratio: open interest over taker buy+sell volume.
|
||||
#[pyclass(
|
||||
name = "OiToVolumeRatio",
|
||||
module = "wickra._wickra",
|
||||
skip_from_py_object
|
||||
)]
|
||||
#[derive(Clone)]
|
||||
struct PyOiToVolumeRatio {
|
||||
inner: wc::OiToVolumeRatio,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyOiToVolumeRatio {
|
||||
#[new]
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
inner: wc::OiToVolumeRatio::new(),
|
||||
}
|
||||
}
|
||||
fn update(
|
||||
&mut self,
|
||||
open_interest: f64,
|
||||
taker_buy_volume: f64,
|
||||
taker_sell_volume: f64,
|
||||
) -> PyResult<Option<f64>> {
|
||||
Ok(self.inner.update(deriv_oi_taker(
|
||||
open_interest,
|
||||
taker_buy_volume,
|
||||
taker_sell_volume,
|
||||
)?))
|
||||
}
|
||||
fn batch<'py>(
|
||||
&mut self,
|
||||
py: Python<'py>,
|
||||
open_interest: Vec<f64>,
|
||||
taker_buy_volume: Vec<f64>,
|
||||
taker_sell_volume: Vec<f64>,
|
||||
) -> PyResult<Bound<'py, PyArray1<f64>>> {
|
||||
if open_interest.len() != taker_buy_volume.len()
|
||||
|| taker_buy_volume.len() != taker_sell_volume.len()
|
||||
{
|
||||
return Err(PyValueError::new_err(
|
||||
"open_interest, taker_buy_volume, taker_sell_volume must be equal length",
|
||||
));
|
||||
}
|
||||
let mut out = Vec::with_capacity(open_interest.len());
|
||||
for i in 0..open_interest.len() {
|
||||
out.push(
|
||||
self.inner
|
||||
.update(deriv_oi_taker(
|
||||
open_interest[i],
|
||||
taker_buy_volume[i],
|
||||
taker_sell_volume[i],
|
||||
)?)
|
||||
.unwrap_or(f64::NAN),
|
||||
);
|
||||
}
|
||||
Ok(out.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 {
|
||||
"OiToVolumeRatio()".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
// Perpetual premium index: relative premium of mark over index price.
|
||||
#[pyclass(
|
||||
name = "PerpetualPremiumIndex",
|
||||
module = "wickra._wickra",
|
||||
skip_from_py_object
|
||||
)]
|
||||
#[derive(Clone)]
|
||||
struct PyPerpetualPremiumIndex {
|
||||
inner: wc::PerpetualPremiumIndex,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyPerpetualPremiumIndex {
|
||||
#[new]
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
inner: wc::PerpetualPremiumIndex::new(),
|
||||
}
|
||||
}
|
||||
fn update(&mut self, mark_price: f64, index_price: f64) -> PyResult<Option<f64>> {
|
||||
Ok(self.inner.update(deriv_basis(mark_price, index_price)?))
|
||||
}
|
||||
fn batch<'py>(
|
||||
&mut self,
|
||||
py: Python<'py>,
|
||||
mark_price: Vec<f64>,
|
||||
index_price: Vec<f64>,
|
||||
) -> PyResult<Bound<'py, PyArray1<f64>>> {
|
||||
if mark_price.len() != index_price.len() {
|
||||
return Err(PyValueError::new_err(
|
||||
"mark_price and index_price must be equal length",
|
||||
));
|
||||
}
|
||||
let mut out = Vec::with_capacity(mark_price.len());
|
||||
for i in 0..mark_price.len() {
|
||||
out.push(
|
||||
self.inner
|
||||
.update(deriv_basis(mark_price[i], index_price[i])?)
|
||||
.unwrap_or(f64::NAN),
|
||||
);
|
||||
}
|
||||
Ok(out.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 {
|
||||
"PerpetualPremiumIndex()".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
// Funding-implied APR: per-interval funding annualised.
|
||||
#[pyclass(
|
||||
name = "FundingImpliedApr",
|
||||
module = "wickra._wickra",
|
||||
skip_from_py_object
|
||||
)]
|
||||
#[derive(Clone)]
|
||||
struct PyFundingImpliedApr {
|
||||
inner: wc::FundingImpliedApr,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyFundingImpliedApr {
|
||||
#[new]
|
||||
fn new(intervals_per_year: f64) -> PyResult<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::FundingImpliedApr::new(intervals_per_year).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
fn update(&mut self, funding_rate: f64) -> PyResult<Option<f64>> {
|
||||
Ok(self.inner.update(deriv_funding(funding_rate)?))
|
||||
}
|
||||
fn batch<'py>(
|
||||
&mut self,
|
||||
py: Python<'py>,
|
||||
funding_rate: Vec<f64>,
|
||||
) -> PyResult<Bound<'py, PyArray1<f64>>> {
|
||||
let mut out = Vec::with_capacity(funding_rate.len());
|
||||
for r in funding_rate {
|
||||
out.push(self.inner.update(deriv_funding(r)?).unwrap_or(f64::NAN));
|
||||
}
|
||||
Ok(out.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 {
|
||||
format!(
|
||||
"FundingImpliedApr(intervals_per_year={})",
|
||||
self.inner.intervals_per_year()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Open-interest momentum: rate-of-change of open interest over a window.
|
||||
#[pyclass(
|
||||
name = "OpenInterestMomentum",
|
||||
module = "wickra._wickra",
|
||||
skip_from_py_object
|
||||
)]
|
||||
#[derive(Clone)]
|
||||
struct PyOpenInterestMomentum {
|
||||
inner: wc::OpenInterestMomentum,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyOpenInterestMomentum {
|
||||
#[new]
|
||||
fn new(period: usize) -> PyResult<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::OpenInterestMomentum::new(period).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
fn update(&mut self, open_interest: f64) -> PyResult<Option<f64>> {
|
||||
Ok(self.inner.update(deriv_oi(open_interest)?))
|
||||
}
|
||||
fn batch<'py>(
|
||||
&mut self,
|
||||
py: Python<'py>,
|
||||
open_interest: Vec<f64>,
|
||||
) -> PyResult<Bound<'py, PyArray1<f64>>> {
|
||||
let mut out = Vec::with_capacity(open_interest.len());
|
||||
for oi in open_interest {
|
||||
out.push(self.inner.update(deriv_oi(oi)?).unwrap_or(f64::NAN));
|
||||
}
|
||||
Ok(out.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 {
|
||||
format!("OpenInterestMomentum(period={})", self.inner.period())
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== Market Breadth ==============================
|
||||
//
|
||||
// Market-breadth indicators consume a `CrossSection`: one tick carrying the
|
||||
@@ -24560,6 +25253,7 @@ fn _wickra(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||
m.add_class::<PyCointegration>()?;
|
||||
m.add_class::<PyRelativeStrengthAB>()?;
|
||||
m.add_class::<PyRollingCorrelation>()?;
|
||||
m.add_class::<PyHasbrouckInformationShare>()?;
|
||||
m.add_class::<PyRollingCovariance>()?;
|
||||
m.add_class::<PyOuHalfLife>()?;
|
||||
m.add_class::<PySpreadHurst>()?;
|
||||
@@ -24650,6 +25344,8 @@ fn _wickra(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||
m.add_class::<PySignedVolume>()?;
|
||||
m.add_class::<PyCumulativeVolumeDelta>()?;
|
||||
m.add_class::<PyTradeImbalance>()?;
|
||||
m.add_class::<PyTradeSignAutocorrelation>()?;
|
||||
m.add_class::<PyPin>()?;
|
||||
m.add_class::<PyOrderFlowImbalance>()?;
|
||||
m.add_class::<PyVpin>()?;
|
||||
m.add_class::<PyAmihudIlliquidity>()?;
|
||||
@@ -24673,6 +25369,11 @@ fn _wickra(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||
m.add_class::<PyLiquidationFeatures>()?;
|
||||
m.add_class::<PyTermStructureBasis>()?;
|
||||
m.add_class::<PyCalendarSpread>()?;
|
||||
m.add_class::<PyEstimatedLeverageRatio>()?;
|
||||
m.add_class::<PyOiToVolumeRatio>()?;
|
||||
m.add_class::<PyPerpetualPremiumIndex>()?;
|
||||
m.add_class::<PyFundingImpliedApr>()?;
|
||||
m.add_class::<PyOpenInterestMomentum>()?;
|
||||
m.add_class::<PyAdvanceDecline>()?;
|
||||
m.add_class::<PyAdvanceDeclineRatio>()?;
|
||||
m.add_class::<PyAdVolumeLine>()?;
|
||||
@@ -24826,5 +25527,11 @@ fn _wickra(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||
m.add_class::<PyTdClopwin>()?;
|
||||
m.add_class::<PyTdPropulsion>()?;
|
||||
m.add_class::<PyTdTrap>()?;
|
||||
m.add_class::<PyTristar>()?;
|
||||
m.add_class::<PyHaramiCross>()?;
|
||||
m.add_class::<PyTowerTopBottom>()?;
|
||||
m.add_class::<PyFryPanBottom>()?;
|
||||
m.add_class::<PyDumplingTop>()?;
|
||||
m.add_class::<PyNewPriceLines>()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -217,6 +217,7 @@ def test_scalar_streaming_matches_batch(cls, args, sine_prices):
|
||||
# --- Two-series (asset, benchmark) indicators -----------------------------
|
||||
|
||||
PAIR = [
|
||||
(ta.HasbrouckInformationShare, (2,)),
|
||||
(ta.KendallTau, (20,)),
|
||||
(ta.SpreadAr1Coefficient, (40,)),
|
||||
(ta.GrangerCausality, (60, 1)),
|
||||
@@ -382,6 +383,30 @@ def test_relative_strength_streaming_matches_batch():
|
||||
# 6-tuple candle; the batch helper takes only the columns it needs.
|
||||
|
||||
CANDLE_SCALAR = {
|
||||
"FryPanBottom": (
|
||||
lambda: ta.FryPanBottom(9),
|
||||
lambda ind, h, l, c, v: ind.batch(h, l, c),
|
||||
),
|
||||
"NewPriceLines": (
|
||||
lambda: ta.NewPriceLines(5),
|
||||
lambda ind, h, l, c, v: ind.batch(h, l, c),
|
||||
),
|
||||
"DumplingTop": (
|
||||
lambda: ta.DumplingTop(9),
|
||||
lambda ind, h, l, c, v: ind.batch(h, l, c),
|
||||
),
|
||||
"TowerTopBottom": (
|
||||
lambda: ta.TowerTopBottom(),
|
||||
lambda ind, h, l, c, v: ind.batch(c, h, l, c),
|
||||
),
|
||||
"HaramiCross": (
|
||||
lambda: ta.HaramiCross(),
|
||||
lambda ind, h, l, c, v: ind.batch(c, h, l, c),
|
||||
),
|
||||
"Tristar": (
|
||||
lambda: ta.Tristar(),
|
||||
lambda ind, h, l, c, v: ind.batch(c, h, l, c),
|
||||
),
|
||||
"ThreeLineBreak": (
|
||||
lambda: ta.ThreeLineBreak(3),
|
||||
lambda ind, h, l, c, v: ind.batch(h, l, c),
|
||||
@@ -3278,6 +3303,34 @@ def test_equivolume_reference():
|
||||
def test_candle_volume_reference():
|
||||
t = ta.CandleVolume(20)
|
||||
|
||||
|
||||
def test_tristar_reference():
|
||||
t = ta.Tristar()
|
||||
assert t.update((100.0, 101.0, 99.0, 100.02, 1.0, 0)) == pytest.approx(0.0)
|
||||
assert t.update((105.0, 106.0, 104.0, 105.02, 1.0, 1)) == pytest.approx(0.0)
|
||||
assert t.update((100.0, 101.0, 99.0, 100.02, 1.0, 2)) == pytest.approx(-1.0)
|
||||
|
||||
|
||||
def test_harami_cross_reference():
|
||||
t = ta.HaramiCross()
|
||||
assert t.update((110.0, 110.2, 99.8, 100.0, 1.0, 0)) == pytest.approx(0.0)
|
||||
assert t.update((105.0, 106.0, 104.0, 105.02, 1.0, 1)) == pytest.approx(1.0)
|
||||
|
||||
|
||||
def test_tower_top_bottom_reference():
|
||||
t = ta.TowerTopBottom()
|
||||
assert t.update((100.0, 110.1, 99.9, 110.0, 1.0, 0)) == pytest.approx(0.0)
|
||||
assert t.update((105.0, 107.0, 103.0, 105.1, 1.0, 1)) == pytest.approx(0.0)
|
||||
assert t.update((110.0, 110.1, 99.9, 100.0, 1.0, 2)) == pytest.approx(-1.0)
|
||||
|
||||
|
||||
|
||||
def test_hasbrouck_information_share_reference():
|
||||
t = ta.HasbrouckInformationShare(2)
|
||||
assert t.update(7.0, 9.0) is None
|
||||
assert t.update(7.0, 9.0) is None
|
||||
assert t.update(7.0, 9.0) == pytest.approx(0.5)
|
||||
|
||||
# --- Lifecycle ------------------------------------------------------------
|
||||
|
||||
|
||||
@@ -3618,6 +3671,8 @@ def test_tradeflow_indicators_streaming_equals_batch():
|
||||
lambda: ta.Vpin(8.0, 5),
|
||||
lambda: ta.AmihudIlliquidity(14),
|
||||
lambda: ta.RollMeasure(14),
|
||||
lambda: ta.TradeSignAutocorrelation(10),
|
||||
lambda: ta.Pin(10),
|
||||
):
|
||||
batch = make().batch(price, size, is_buy)
|
||||
streamer = make()
|
||||
@@ -3629,6 +3684,34 @@ def test_tradeflow_indicators_streaming_equals_batch():
|
||||
assert _eq_nan(batch, streamed)
|
||||
|
||||
|
||||
def test_trade_sign_autocorrelation_reference():
|
||||
# Perfectly alternating aggressor signs -> lag-1 autocorrelation -1.
|
||||
t = ta.TradeSignAutocorrelation(10)
|
||||
last = None
|
||||
for i in range(20):
|
||||
last = t.update(100.0, 1.0, i % 2 == 0)
|
||||
assert last == pytest.approx(-1.0)
|
||||
# All buys -> perfectly persistent flow -> +1.
|
||||
t2 = ta.TradeSignAutocorrelation(10)
|
||||
for _ in range(20):
|
||||
last2 = t2.update(100.0, 1.0, True)
|
||||
assert last2 == pytest.approx(1.0)
|
||||
|
||||
|
||||
def test_pin_reference():
|
||||
# One-sided flow (all buys) -> maximally informed -> PIN 1.
|
||||
p = ta.Pin(10)
|
||||
last = None
|
||||
for _ in range(20):
|
||||
last = p.update(100.0, 1.0, True)
|
||||
assert last == pytest.approx(1.0)
|
||||
# Balanced flow -> uninformed -> PIN 0.
|
||||
p2 = ta.Pin(10)
|
||||
for i in range(20):
|
||||
last2 = p2.update(100.0, 1.0, i % 2 == 0)
|
||||
assert last2 == pytest.approx(0.0)
|
||||
|
||||
|
||||
def test_price_impact_indicators_streaming_equals_batch():
|
||||
n = 40
|
||||
mid = np.array([100.0 + 0.5 * math.sin(i * 0.4) for i in range(n)], dtype=np.float64)
|
||||
@@ -3999,6 +4082,71 @@ def test_basis_indicators_streaming_equals_batch():
|
||||
assert _eq_nan(batch, streamed)
|
||||
|
||||
|
||||
def test_b16_derivatives_reference():
|
||||
# Estimated leverage: oi / (long + short) = 200 / 100 = 2.
|
||||
assert ta.EstimatedLeverageRatio().update(200.0, 60.0, 40.0) == pytest.approx(2.0)
|
||||
# OI-to-volume: oi / (buy + sell) = 100 / 50 = 2.
|
||||
assert ta.OiToVolumeRatio().update(100.0, 30.0, 20.0) == pytest.approx(2.0)
|
||||
# Perpetual premium: (mark - index) / index = 0.5 / 100 = 0.005.
|
||||
assert ta.PerpetualPremiumIndex().update(100.5, 100.0) == pytest.approx(0.005)
|
||||
# Funding-implied APR: rate * intervals = 0.0001 * 1095 = 0.1095.
|
||||
assert ta.FundingImpliedApr(1095.0).update(0.0001) == pytest.approx(0.1095)
|
||||
# Open-interest momentum (period 2): warmup then ROC% = 100*(120-100)/100 = 20.
|
||||
oim = ta.OpenInterestMomentum(2)
|
||||
assert oim.update(100.0) is None
|
||||
assert oim.update(110.0) is None
|
||||
assert oim.update(120.0) == pytest.approx(20.0)
|
||||
|
||||
|
||||
def test_b16_derivatives_streaming_equals_batch():
|
||||
n = 40
|
||||
oi = np.array([1000.0 + 50.0 * math.sin(i * 0.3) for i in range(n)], dtype=np.float64)
|
||||
long_sz = np.array([600.0 + 20.0 * math.cos(i * 0.2) for i in range(n)], dtype=np.float64)
|
||||
short_sz = np.array([400.0 + 15.0 * math.sin(i * 0.4) for i in range(n)], dtype=np.float64)
|
||||
buy = np.array([300.0 + 10.0 * math.sin(i * 0.5) for i in range(n)], dtype=np.float64)
|
||||
sell = np.array([250.0 + 12.0 * math.cos(i * 0.35) for i in range(n)], dtype=np.float64)
|
||||
index = np.array([100.0 + math.sin(i * 0.2) for i in range(n)], dtype=np.float64)
|
||||
mark = np.array([index[i] + 0.05 * math.cos(i * 0.3) for i in range(n)], dtype=np.float64)
|
||||
rate = np.array([0.0001 * math.sin(i * 0.3) for i in range(n)], dtype=np.float64)
|
||||
|
||||
# EstimatedLeverageRatio; update(open_interest, long_size, short_size).
|
||||
batch = ta.EstimatedLeverageRatio().batch(oi, long_sz, short_sz)
|
||||
streamer = ta.EstimatedLeverageRatio()
|
||||
streamed = np.array(
|
||||
[streamer.update(oi[i], long_sz[i], short_sz[i]) for i in range(n)], dtype=np.float64
|
||||
)
|
||||
assert batch.shape == (n,)
|
||||
assert _eq_nan(batch, streamed)
|
||||
|
||||
# OiToVolumeRatio; update(open_interest, taker_buy_volume, taker_sell_volume).
|
||||
batch = ta.OiToVolumeRatio().batch(oi, buy, sell)
|
||||
streamer = ta.OiToVolumeRatio()
|
||||
streamed = np.array(
|
||||
[streamer.update(oi[i], buy[i], sell[i]) for i in range(n)], dtype=np.float64
|
||||
)
|
||||
assert _eq_nan(batch, streamed)
|
||||
|
||||
# PerpetualPremiumIndex; update(mark_price, index_price).
|
||||
batch = ta.PerpetualPremiumIndex().batch(mark, index)
|
||||
streamer = ta.PerpetualPremiumIndex()
|
||||
streamed = np.array(
|
||||
[streamer.update(mark[i], index[i]) for i in range(n)], dtype=np.float64
|
||||
)
|
||||
assert _eq_nan(batch, streamed)
|
||||
|
||||
# FundingImpliedApr; update(funding_rate).
|
||||
batch = ta.FundingImpliedApr(1095.0).batch(rate)
|
||||
streamer = ta.FundingImpliedApr(1095.0)
|
||||
streamed = np.array([streamer.update(rate[i]) for i in range(n)], dtype=np.float64)
|
||||
assert _eq_nan(batch, streamed)
|
||||
|
||||
# OpenInterestMomentum; update(open_interest).
|
||||
batch = ta.OpenInterestMomentum(10).batch(oi)
|
||||
streamer = ta.OpenInterestMomentum(10)
|
||||
streamed = np.array([streamer.update(oi[i]) for i in range(n)], dtype=np.float64)
|
||||
assert _eq_nan(batch, streamed)
|
||||
|
||||
|
||||
# --- Alt-Chart Bars ------------------------------------------------------
|
||||
|
||||
|
||||
|
||||
@@ -563,6 +563,11 @@ wasm_pair_indicator!(
|
||||
"BetaNeutralSpread",
|
||||
wc::BetaNeutralSpread
|
||||
);
|
||||
wasm_pair_indicator!(
|
||||
WasmHasbrouckInformationShare,
|
||||
"HasbrouckInformationShare",
|
||||
wc::HasbrouckInformationShare
|
||||
);
|
||||
|
||||
// ---------- PairSpreadZScore (two params) ----------
|
||||
|
||||
@@ -9050,6 +9055,9 @@ wasm_candle_pattern!(WasmTdClop, wc::TdClop, TDClop);
|
||||
wasm_candle_pattern!(WasmTdClopwin, wc::TdClopwin, TDClopwin);
|
||||
wasm_candle_pattern!(WasmTdPropulsion, wc::TdPropulsion, TDPropulsion);
|
||||
wasm_candle_pattern!(WasmTdTrap, wc::TdTrap, TDTrap);
|
||||
wasm_candle_pattern!(WasmTristar, wc::Tristar, Tristar);
|
||||
wasm_candle_pattern!(WasmHaramiCross, wc::HaramiCross, HaramiCross);
|
||||
wasm_candle_pattern!(WasmTowerTopBottom, wc::TowerTopBottom, TowerTopBottom);
|
||||
|
||||
// ============================== Microstructure: Order Book ==============================
|
||||
//
|
||||
@@ -9276,6 +9284,66 @@ impl WasmTradeImbalance {
|
||||
}
|
||||
}
|
||||
|
||||
// Trade-sign autocorrelation carries a `period` parameter, so it is hand-written.
|
||||
#[wasm_bindgen(js_name = TradeSignAutocorrelation)]
|
||||
pub struct WasmTradeSignAutocorrelation {
|
||||
inner: wc::TradeSignAutocorrelation,
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_class = TradeSignAutocorrelation)]
|
||||
impl WasmTradeSignAutocorrelation {
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(period: usize) -> Result<WasmTradeSignAutocorrelation, JsError> {
|
||||
Ok(Self {
|
||||
inner: wc::TradeSignAutocorrelation::new(period).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
pub fn update(&mut self, price: f64, size: f64, is_buy: bool) -> Result<Option<f64>, JsError> {
|
||||
Ok(self.inner.update(build_trade(price, size, is_buy)?))
|
||||
}
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
// PIN carries a `window` parameter, so it is hand-written.
|
||||
#[wasm_bindgen(js_name = Pin)]
|
||||
pub struct WasmPin {
|
||||
inner: wc::Pin,
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_class = Pin)]
|
||||
impl WasmPin {
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(window: usize) -> Result<WasmPin, JsError> {
|
||||
Ok(Self {
|
||||
inner: wc::Pin::new(window).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
pub fn update(&mut self, price: f64, size: f64, is_buy: bool) -> Result<Option<f64>, JsError> {
|
||||
Ok(self.inner.update(build_trade(price, size, is_buy)?))
|
||||
}
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
// Order Flow Imbalance: order-book input with a `period` parameter.
|
||||
#[wasm_bindgen(js_name = OrderFlowImbalance)]
|
||||
pub struct WasmOrderFlowImbalance {
|
||||
@@ -9865,6 +9933,50 @@ fn deriv_taker(
|
||||
.map_err(map_err)
|
||||
}
|
||||
|
||||
fn deriv_oi_long_short(
|
||||
open_interest: f64,
|
||||
long_size: f64,
|
||||
short_size: f64,
|
||||
) -> Result<wc::DerivativesTick, JsError> {
|
||||
wc::DerivativesTick::new(
|
||||
0.0,
|
||||
1.0,
|
||||
1.0,
|
||||
1.0,
|
||||
open_interest,
|
||||
long_size,
|
||||
short_size,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0,
|
||||
)
|
||||
.map_err(map_err)
|
||||
}
|
||||
|
||||
fn deriv_oi_taker(
|
||||
open_interest: f64,
|
||||
taker_buy_volume: f64,
|
||||
taker_sell_volume: f64,
|
||||
) -> Result<wc::DerivativesTick, JsError> {
|
||||
wc::DerivativesTick::new(
|
||||
0.0,
|
||||
1.0,
|
||||
1.0,
|
||||
1.0,
|
||||
open_interest,
|
||||
0.0,
|
||||
0.0,
|
||||
taker_buy_volume,
|
||||
taker_sell_volume,
|
||||
0.0,
|
||||
0.0,
|
||||
0,
|
||||
)
|
||||
.map_err(map_err)
|
||||
}
|
||||
|
||||
fn deriv_liquidation(
|
||||
long_liquidation: f64,
|
||||
short_liquidation: f64,
|
||||
@@ -10188,6 +10300,195 @@ impl WasmCalendarSpread {
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Estimated Leverage Ratio ----------
|
||||
|
||||
#[wasm_bindgen(js_name = EstimatedLeverageRatio)]
|
||||
pub struct WasmEstimatedLeverageRatio {
|
||||
inner: wc::EstimatedLeverageRatio,
|
||||
}
|
||||
|
||||
impl Default for WasmEstimatedLeverageRatio {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_class = EstimatedLeverageRatio)]
|
||||
impl WasmEstimatedLeverageRatio {
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new() -> WasmEstimatedLeverageRatio {
|
||||
Self {
|
||||
inner: wc::EstimatedLeverageRatio::new(),
|
||||
}
|
||||
}
|
||||
pub fn update(
|
||||
&mut self,
|
||||
open_interest: f64,
|
||||
long_size: f64,
|
||||
short_size: f64,
|
||||
) -> Result<Option<f64>, JsError> {
|
||||
Ok(self
|
||||
.inner
|
||||
.update(deriv_oi_long_short(open_interest, long_size, short_size)?))
|
||||
}
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- OI-to-Volume Ratio ----------
|
||||
|
||||
#[wasm_bindgen(js_name = OiToVolumeRatio)]
|
||||
pub struct WasmOiToVolumeRatio {
|
||||
inner: wc::OiToVolumeRatio,
|
||||
}
|
||||
|
||||
impl Default for WasmOiToVolumeRatio {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_class = OiToVolumeRatio)]
|
||||
impl WasmOiToVolumeRatio {
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new() -> WasmOiToVolumeRatio {
|
||||
Self {
|
||||
inner: wc::OiToVolumeRatio::new(),
|
||||
}
|
||||
}
|
||||
pub fn update(
|
||||
&mut self,
|
||||
open_interest: f64,
|
||||
taker_buy_volume: f64,
|
||||
taker_sell_volume: f64,
|
||||
) -> Result<Option<f64>, JsError> {
|
||||
Ok(self.inner.update(deriv_oi_taker(
|
||||
open_interest,
|
||||
taker_buy_volume,
|
||||
taker_sell_volume,
|
||||
)?))
|
||||
}
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Perpetual Premium Index ----------
|
||||
|
||||
#[wasm_bindgen(js_name = PerpetualPremiumIndex)]
|
||||
pub struct WasmPerpetualPremiumIndex {
|
||||
inner: wc::PerpetualPremiumIndex,
|
||||
}
|
||||
|
||||
impl Default for WasmPerpetualPremiumIndex {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_class = PerpetualPremiumIndex)]
|
||||
impl WasmPerpetualPremiumIndex {
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new() -> WasmPerpetualPremiumIndex {
|
||||
Self {
|
||||
inner: wc::PerpetualPremiumIndex::new(),
|
||||
}
|
||||
}
|
||||
pub fn update(&mut self, mark_price: f64, index_price: f64) -> Result<Option<f64>, JsError> {
|
||||
Ok(self.inner.update(deriv_basis(mark_price, index_price)?))
|
||||
}
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Funding-Implied APR ----------
|
||||
|
||||
#[wasm_bindgen(js_name = FundingImpliedApr)]
|
||||
pub struct WasmFundingImpliedApr {
|
||||
inner: wc::FundingImpliedApr,
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_class = FundingImpliedApr)]
|
||||
impl WasmFundingImpliedApr {
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(intervals_per_year: f64) -> Result<WasmFundingImpliedApr, JsError> {
|
||||
Ok(Self {
|
||||
inner: wc::FundingImpliedApr::new(intervals_per_year).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
pub fn update(&mut self, funding_rate: f64) -> Result<Option<f64>, JsError> {
|
||||
Ok(self.inner.update(deriv_funding(funding_rate)?))
|
||||
}
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Open-Interest Momentum ----------
|
||||
|
||||
#[wasm_bindgen(js_name = OpenInterestMomentum)]
|
||||
pub struct WasmOpenInterestMomentum {
|
||||
inner: wc::OpenInterestMomentum,
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_class = OpenInterestMomentum)]
|
||||
impl WasmOpenInterestMomentum {
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(period: usize) -> Result<WasmOpenInterestMomentum, JsError> {
|
||||
Ok(Self {
|
||||
inner: wc::OpenInterestMomentum::new(period).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
pub fn update(&mut self, open_interest: f64) -> Result<Option<f64>, JsError> {
|
||||
Ok(self.inner.update(deriv_oi(open_interest)?))
|
||||
}
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Heikin-Ashi Oscillator ----------
|
||||
|
||||
#[wasm_bindgen(js_name = HeikinAshiOscillator)]
|
||||
@@ -10488,6 +10789,150 @@ impl WasmCandleVolume {
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Frying Pan Bottom ----------
|
||||
|
||||
#[wasm_bindgen(js_name = FryPanBottom)]
|
||||
pub struct WasmFryPanBottom {
|
||||
inner: wc::FryPanBottom,
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_class = FryPanBottom)]
|
||||
impl WasmFryPanBottom {
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(period: usize) -> Result<WasmFryPanBottom, JsError> {
|
||||
Ok(Self {
|
||||
inner: wc::FryPanBottom::new(period).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
pub fn update(&mut self, high: f64, low: f64, close: f64) -> Result<Option<f64>, JsError> {
|
||||
let c = make_candle(high, low, close, 0.0)?;
|
||||
Ok(self.inner.update(c))
|
||||
}
|
||||
pub fn batch(
|
||||
&mut self,
|
||||
high: &[f64],
|
||||
low: &[f64],
|
||||
close: &[f64],
|
||||
) -> Result<Float64Array, JsError> {
|
||||
if high.len() != low.len() || low.len() != close.len() {
|
||||
return Err(JsError::new("high, low, close must be equal length"));
|
||||
}
|
||||
let mut out = Vec::with_capacity(high.len());
|
||||
for i in 0..high.len() {
|
||||
let c = make_candle(high[i], low[i], close[i], 0.0)?;
|
||||
out.push(self.inner.update(c).unwrap_or(f64::NAN));
|
||||
}
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Dumpling Top ----------
|
||||
|
||||
#[wasm_bindgen(js_name = DumplingTop)]
|
||||
pub struct WasmDumplingTop {
|
||||
inner: wc::DumplingTop,
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_class = DumplingTop)]
|
||||
impl WasmDumplingTop {
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(period: usize) -> Result<WasmDumplingTop, JsError> {
|
||||
Ok(Self {
|
||||
inner: wc::DumplingTop::new(period).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
pub fn update(&mut self, high: f64, low: f64, close: f64) -> Result<Option<f64>, JsError> {
|
||||
let c = make_candle(high, low, close, 0.0)?;
|
||||
Ok(self.inner.update(c))
|
||||
}
|
||||
pub fn batch(
|
||||
&mut self,
|
||||
high: &[f64],
|
||||
low: &[f64],
|
||||
close: &[f64],
|
||||
) -> Result<Float64Array, JsError> {
|
||||
if high.len() != low.len() || low.len() != close.len() {
|
||||
return Err(JsError::new("high, low, close must be equal length"));
|
||||
}
|
||||
let mut out = Vec::with_capacity(high.len());
|
||||
for i in 0..high.len() {
|
||||
let c = make_candle(high[i], low[i], close[i], 0.0)?;
|
||||
out.push(self.inner.update(c).unwrap_or(f64::NAN));
|
||||
}
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- New Price Lines ----------
|
||||
|
||||
#[wasm_bindgen(js_name = NewPriceLines)]
|
||||
pub struct WasmNewPriceLines {
|
||||
inner: wc::NewPriceLines,
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_class = NewPriceLines)]
|
||||
impl WasmNewPriceLines {
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(count: usize) -> Result<WasmNewPriceLines, JsError> {
|
||||
Ok(Self {
|
||||
inner: wc::NewPriceLines::new(count).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
pub fn update(&mut self, high: f64, low: f64, close: f64) -> Result<Option<f64>, JsError> {
|
||||
let c = make_candle(high, low, close, 0.0)?;
|
||||
Ok(self.inner.update(c))
|
||||
}
|
||||
pub fn batch(
|
||||
&mut self,
|
||||
high: &[f64],
|
||||
low: &[f64],
|
||||
close: &[f64],
|
||||
) -> Result<Float64Array, JsError> {
|
||||
if high.len() != low.len() || low.len() != close.len() {
|
||||
return Err(JsError::new("high, low, close must be equal length"));
|
||||
}
|
||||
let mut out = Vec::with_capacity(high.len());
|
||||
for i in 0..high.len() {
|
||||
let c = make_candle(high[i], low[i], close[i], 0.0)?;
|
||||
out.push(self.inner.update(c).unwrap_or(f64::NAN));
|
||||
}
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Market Breadth (CrossSection input) ----------
|
||||
//
|
||||
// A breadth tick is the per-symbol state of the whole universe, passed as four
|
||||
|
||||
@@ -0,0 +1,215 @@
|
||||
//! Dumpling Top — a rounded top (dome) confirmed by a breakdown.
|
||||
|
||||
use std::collections::VecDeque;
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
use crate::ohlcv::Candle;
|
||||
use crate::traits::Indicator;
|
||||
|
||||
/// Dumpling Top — the bearish mirror of the [`FryPanBottom`](crate::FryPanBottom):
|
||||
/// a gently rounded **top** (dome) across the window, confirmed by a close back
|
||||
/// below where it started.
|
||||
///
|
||||
/// ```text
|
||||
/// over the last `period` closes:
|
||||
/// the maximum close sits in the middle third of the window (the "dome")
|
||||
/// the latest close is below the first close (the breakdown)
|
||||
/// signal = −1 when both hold, else 0
|
||||
/// ```
|
||||
///
|
||||
/// The dumpling top is a distribution pattern: price rounds over at the top as
|
||||
/// buying fades, then rolls down through the level it rose from. Detection requires
|
||||
/// a *central* high (a symmetric dome, not a one-sided spike) and a close below the
|
||||
/// window's opening level. The output is `−1.0` (pattern) or `0.0`.
|
||||
///
|
||||
/// The first value lands after `period` inputs; each `update` scans the window in
|
||||
/// O(`period`).
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use wickra_core::{Candle, Indicator, DumplingTop};
|
||||
///
|
||||
/// let mut indicator = DumplingTop::new(9).unwrap();
|
||||
/// let closes = [100.0, 102.0, 104.0, 105.0, 104.0, 102.0, 99.0, 97.0, 95.0];
|
||||
/// let mut last = None;
|
||||
/// for &cl in &closes {
|
||||
/// let c = Candle::new(cl, cl + 0.5, cl - 0.5, cl, 1_000.0, 0).unwrap();
|
||||
/// last = indicator.update(c);
|
||||
/// }
|
||||
/// assert_eq!(last, Some(-1.0));
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DumplingTop {
|
||||
period: usize,
|
||||
closes: VecDeque<f64>,
|
||||
last: Option<f64>,
|
||||
}
|
||||
|
||||
impl DumplingTop {
|
||||
/// Construct a Dumpling Top over `period` bars.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`Error::InvalidPeriod`] if `period < 5`.
|
||||
pub fn new(period: usize) -> Result<Self> {
|
||||
if period < 5 {
|
||||
return Err(Error::InvalidPeriod {
|
||||
message: "dumpling top needs period >= 5",
|
||||
});
|
||||
}
|
||||
Ok(Self {
|
||||
period,
|
||||
closes: VecDeque::with_capacity(period),
|
||||
last: None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Configured window period.
|
||||
pub const fn period(&self) -> usize {
|
||||
self.period
|
||||
}
|
||||
|
||||
/// Current value if available.
|
||||
pub const fn value(&self) -> Option<f64> {
|
||||
self.last
|
||||
}
|
||||
}
|
||||
|
||||
impl Indicator for DumplingTop {
|
||||
type Input = Candle;
|
||||
type Output = f64;
|
||||
|
||||
fn update(&mut self, candle: Candle) -> Option<f64> {
|
||||
if self.closes.len() == self.period {
|
||||
self.closes.pop_front();
|
||||
}
|
||||
self.closes.push_back(candle.close);
|
||||
if self.closes.len() < self.period {
|
||||
return None;
|
||||
}
|
||||
let first = *self.closes.front().expect("non-empty");
|
||||
let last = *self.closes.back().expect("non-empty");
|
||||
let mut max_idx = 0;
|
||||
let mut max_val = f64::NEG_INFINITY;
|
||||
for (i, &v) in self.closes.iter().enumerate() {
|
||||
if v > max_val {
|
||||
max_val = v;
|
||||
max_idx = i;
|
||||
}
|
||||
}
|
||||
let lo = self.period / 4;
|
||||
let hi = self.period - self.period / 4;
|
||||
let dome = max_idx >= lo && max_idx < hi;
|
||||
let broke_down = last < first && last < max_val;
|
||||
let v = if dome && broke_down { -1.0 } else { 0.0 };
|
||||
self.last = Some(v);
|
||||
Some(v)
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.closes.clear();
|
||||
self.last = None;
|
||||
}
|
||||
|
||||
fn warmup_period(&self) -> usize {
|
||||
self.period
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.last.is_some()
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"DumplingTop"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::traits::BatchExt;
|
||||
|
||||
fn c(close: f64) -> Candle {
|
||||
Candle::new_unchecked(close, close + 0.5, close - 0.5, close, 1_000.0, 0)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_small_period() {
|
||||
assert!(matches!(
|
||||
DumplingTop::new(4),
|
||||
Err(Error::InvalidPeriod { .. })
|
||||
));
|
||||
assert!(DumplingTop::new(5).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accessors_and_metadata() {
|
||||
let d = DumplingTop::new(9).unwrap();
|
||||
assert_eq!(d.period(), 9);
|
||||
assert_eq!(d.warmup_period(), 9);
|
||||
assert_eq!(d.name(), "DumplingTop");
|
||||
assert!(!d.is_ready());
|
||||
assert_eq!(d.value(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn first_emission_at_warmup_period() {
|
||||
let mut d = DumplingTop::new(5).unwrap();
|
||||
let out = d.batch(&[c(100.0), c(101.0), c(102.0), c(101.0), c(99.0), c(98.0)]);
|
||||
for v in out.iter().take(4) {
|
||||
assert!(v.is_none());
|
||||
}
|
||||
assert!(out[4].is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rounded_top_then_breakdown_signals() {
|
||||
let mut d = DumplingTop::new(9).unwrap();
|
||||
let closes = [100.0, 102.0, 104.0, 105.0, 104.0, 102.0, 99.0, 97.0, 95.0];
|
||||
let candles: Vec<Candle> = closes.iter().map(|&x| c(x)).collect();
|
||||
let last = d.batch(&candles).into_iter().flatten().last().unwrap();
|
||||
assert_eq!(last, -1.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn one_sided_rise_is_zero() {
|
||||
let mut d = DumplingTop::new(9).unwrap();
|
||||
let candles: Vec<Candle> = (0..9).map(|i| c(100.0 + f64::from(i))).collect();
|
||||
let last = d.batch(&candles).into_iter().flatten().last().unwrap();
|
||||
assert_eq!(last, 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_breakdown_is_zero() {
|
||||
let mut d = DumplingTop::new(9).unwrap();
|
||||
let closes = [
|
||||
100.0, 102.0, 104.0, 105.0, 104.0, 103.0, 102.0, 101.0, 100.5,
|
||||
];
|
||||
let candles: Vec<Candle> = closes.iter().map(|&x| c(x)).collect();
|
||||
let last = d.batch(&candles).into_iter().flatten().last().unwrap();
|
||||
assert_eq!(last, 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut d = DumplingTop::new(5).unwrap();
|
||||
d.batch(&[c(100.0), c(101.0), c(102.0), c(101.0), c(99.0)]);
|
||||
assert!(d.is_ready());
|
||||
d.reset();
|
||||
assert!(!d.is_ready());
|
||||
assert_eq!(d.value(), None);
|
||||
assert_eq!(d.update(c(100.0)), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_equals_streaming() {
|
||||
let candles: Vec<Candle> = (0..60)
|
||||
.map(|i| c(100.0 + (f64::from(i) * 0.3).sin() * 5.0))
|
||||
.collect();
|
||||
let batch = DumplingTop::new(9).unwrap().batch(&candles);
|
||||
let mut b = DumplingTop::new(9).unwrap();
|
||||
let streamed: Vec<_> = candles.iter().map(|x| b.update(*x)).collect();
|
||||
assert_eq!(batch, streamed);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
//! Estimated Leverage Ratio — open interest per unit of aggregate position size.
|
||||
|
||||
use crate::derivatives::DerivativesTick;
|
||||
use crate::traits::Indicator;
|
||||
|
||||
/// Estimated Leverage Ratio (ELR) — open interest relative to the aggregate
|
||||
/// long+short position size, a proxy for how leveraged outstanding positions are.
|
||||
///
|
||||
/// ```text
|
||||
/// ELR = open_interest / (long_size + short_size)
|
||||
/// ```
|
||||
///
|
||||
/// The classic estimated leverage ratio compares open interest (the notional of
|
||||
/// outstanding contracts) to the capital backing it. With the size fields of a
|
||||
/// [`DerivativesTick`] standing in for the position base, the ratio rises when a
|
||||
/// given pool of positions controls more open interest — i.e. when the market is
|
||||
/// running hotter leverage. Spikes in ELR mark crowded, fragile conditions where a
|
||||
/// move can cascade into liquidations; a falling ELR marks deleveraging.
|
||||
///
|
||||
/// The ratio is non-negative; a tick with zero aggregate size reports `0` rather
|
||||
/// than dividing by zero. It is stateless — each tick yields one value (no warmup).
|
||||
/// Each `update` is O(1).
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use wickra_core::{DerivativesTick, Indicator, EstimatedLeverageRatio};
|
||||
///
|
||||
/// let mut indicator = EstimatedLeverageRatio::new();
|
||||
/// let tick = DerivativesTick::new(0.0001, 100.0, 100.0, 100.0, 1_000.0, 400.0, 600.0, 0.0, 0.0, 0.0, 0.0, 0).unwrap();
|
||||
/// let elr = indicator.update(tick).unwrap();
|
||||
/// assert!((elr - 1.0).abs() < 1e-12); // 1000 / (400 + 600)
|
||||
/// ```
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct EstimatedLeverageRatio {
|
||||
ready: bool,
|
||||
}
|
||||
|
||||
impl EstimatedLeverageRatio {
|
||||
/// Construct a new Estimated Leverage Ratio. The indicator is parameter-free.
|
||||
#[must_use]
|
||||
pub const fn new() -> Self {
|
||||
Self { ready: false }
|
||||
}
|
||||
}
|
||||
|
||||
impl Indicator for EstimatedLeverageRatio {
|
||||
type Input = DerivativesTick;
|
||||
type Output = f64;
|
||||
|
||||
fn update(&mut self, tick: DerivativesTick) -> Option<f64> {
|
||||
let base = tick.long_size + tick.short_size;
|
||||
let elr = if base > 0.0 {
|
||||
tick.open_interest / base
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
self.ready = true;
|
||||
Some(elr)
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.ready = false;
|
||||
}
|
||||
|
||||
fn warmup_period(&self) -> usize {
|
||||
1
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.ready
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"EstimatedLeverageRatio"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::traits::BatchExt;
|
||||
use approx::assert_relative_eq;
|
||||
|
||||
fn tick(oi: f64, long: f64, short: f64) -> DerivativesTick {
|
||||
DerivativesTick::new_unchecked(
|
||||
0.0, 100.0, 100.0, 100.0, oi, long, short, 0.0, 0.0, 0.0, 0.0, 0,
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accessors_and_metadata() {
|
||||
let e = EstimatedLeverageRatio::new();
|
||||
assert_eq!(e.warmup_period(), 1);
|
||||
assert_eq!(e.name(), "EstimatedLeverageRatio");
|
||||
assert!(!e.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ratio_reference_value() {
|
||||
let mut e = EstimatedLeverageRatio::new();
|
||||
// 1000 / (400 + 600) = 1.0.
|
||||
assert_relative_eq!(
|
||||
e.update(tick(1_000.0, 400.0, 600.0)).unwrap(),
|
||||
1.0,
|
||||
epsilon = 1e-12
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn higher_oi_raises_ratio() {
|
||||
let mut e = EstimatedLeverageRatio::new();
|
||||
let low = e.update(tick(1_000.0, 500.0, 500.0)).unwrap();
|
||||
let high = e.update(tick(3_000.0, 500.0, 500.0)).unwrap();
|
||||
assert!(high > low);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zero_base_is_zero() {
|
||||
let mut e = EstimatedLeverageRatio::new();
|
||||
assert_relative_eq!(
|
||||
e.update(tick(1_000.0, 0.0, 0.0)).unwrap(),
|
||||
0.0,
|
||||
epsilon = 1e-12
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ready_after_first_update() {
|
||||
let mut e = EstimatedLeverageRatio::new();
|
||||
assert!(!e.is_ready());
|
||||
e.update(tick(1_000.0, 500.0, 500.0));
|
||||
assert!(e.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut e = EstimatedLeverageRatio::new();
|
||||
e.update(tick(1_000.0, 500.0, 500.0));
|
||||
assert!(e.is_ready());
|
||||
e.reset();
|
||||
assert!(!e.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_equals_streaming() {
|
||||
let ticks: Vec<DerivativesTick> = (0..40)
|
||||
.map(|i| tick(1_000.0 + f64::from(i) * 10.0, 500.0, 500.0))
|
||||
.collect();
|
||||
let batch = EstimatedLeverageRatio::new().batch(&ticks);
|
||||
let mut b = EstimatedLeverageRatio::new();
|
||||
let streamed: Vec<_> = ticks.iter().map(|x| b.update(*x)).collect();
|
||||
assert_eq!(batch, streamed);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,218 @@
|
||||
//! Frying Pan Bottom — a rounded bottom (U) confirmed by recovery.
|
||||
|
||||
use std::collections::VecDeque;
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
use crate::ohlcv::Candle;
|
||||
use crate::traits::Indicator;
|
||||
|
||||
/// Frying Pan Bottom — a gently rounded bottom across the lookback window: prices
|
||||
/// decline, flatten near the centre, then recover above where they started.
|
||||
///
|
||||
/// ```text
|
||||
/// over the last `period` closes:
|
||||
/// the minimum close sits in the middle third of the window (the "bowl")
|
||||
/// the latest close is above the first close (the rim is recovered)
|
||||
/// signal = +1 when both hold, else 0
|
||||
/// ```
|
||||
///
|
||||
/// The frying pan is a bullish accumulation pattern: a saucer-shaped base where
|
||||
/// selling dries up, the curve flattens, and price lifts off the rim. Detecting it
|
||||
/// requires the low point to be central (a symmetric bowl, not a one-sided drop)
|
||||
/// and the close to have climbed back above the window's opening level, confirming
|
||||
/// the breakout from the base. The output is `+1.0` (pattern) or `0.0`.
|
||||
///
|
||||
/// The first value lands after `period` inputs; each `update` scans the window in
|
||||
/// O(`period`).
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use wickra_core::{Candle, Indicator, FryPanBottom};
|
||||
///
|
||||
/// let mut indicator = FryPanBottom::new(9).unwrap();
|
||||
/// // A U-shaped base then recovery.
|
||||
/// let closes = [100.0, 98.0, 96.0, 95.0, 96.0, 98.0, 101.0, 103.0, 105.0];
|
||||
/// let mut last = None;
|
||||
/// for &cl in &closes {
|
||||
/// let c = Candle::new(cl, cl + 0.5, cl - 0.5, cl, 1_000.0, 0).unwrap();
|
||||
/// last = indicator.update(c);
|
||||
/// }
|
||||
/// assert_eq!(last, Some(1.0));
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct FryPanBottom {
|
||||
period: usize,
|
||||
closes: VecDeque<f64>,
|
||||
last: Option<f64>,
|
||||
}
|
||||
|
||||
impl FryPanBottom {
|
||||
/// Construct a Frying Pan Bottom over `period` bars.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`Error::InvalidPeriod`] if `period < 5` (a bowl needs room for a
|
||||
/// central low between recovering sides).
|
||||
pub fn new(period: usize) -> Result<Self> {
|
||||
if period < 5 {
|
||||
return Err(Error::InvalidPeriod {
|
||||
message: "frying pan bottom needs period >= 5",
|
||||
});
|
||||
}
|
||||
Ok(Self {
|
||||
period,
|
||||
closes: VecDeque::with_capacity(period),
|
||||
last: None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Configured window period.
|
||||
pub const fn period(&self) -> usize {
|
||||
self.period
|
||||
}
|
||||
|
||||
/// Current value if available.
|
||||
pub const fn value(&self) -> Option<f64> {
|
||||
self.last
|
||||
}
|
||||
}
|
||||
|
||||
impl Indicator for FryPanBottom {
|
||||
type Input = Candle;
|
||||
type Output = f64;
|
||||
|
||||
fn update(&mut self, candle: Candle) -> Option<f64> {
|
||||
if self.closes.len() == self.period {
|
||||
self.closes.pop_front();
|
||||
}
|
||||
self.closes.push_back(candle.close);
|
||||
if self.closes.len() < self.period {
|
||||
return None;
|
||||
}
|
||||
let first = *self.closes.front().expect("non-empty");
|
||||
let last = *self.closes.back().expect("non-empty");
|
||||
// Index of the minimum close.
|
||||
let mut min_idx = 0;
|
||||
let mut min_val = f64::INFINITY;
|
||||
for (i, &v) in self.closes.iter().enumerate() {
|
||||
if v < min_val {
|
||||
min_val = v;
|
||||
min_idx = i;
|
||||
}
|
||||
}
|
||||
let lo = self.period / 4;
|
||||
let hi = self.period - self.period / 4;
|
||||
let bowl = min_idx >= lo && min_idx < hi;
|
||||
let recovered = last > first && last > min_val;
|
||||
let v = if bowl && recovered { 1.0 } else { 0.0 };
|
||||
self.last = Some(v);
|
||||
Some(v)
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.closes.clear();
|
||||
self.last = None;
|
||||
}
|
||||
|
||||
fn warmup_period(&self) -> usize {
|
||||
self.period
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.last.is_some()
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"FryPanBottom"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::traits::BatchExt;
|
||||
|
||||
fn c(close: f64) -> Candle {
|
||||
Candle::new_unchecked(close, close + 0.5, close - 0.5, close, 1_000.0, 0)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_small_period() {
|
||||
assert!(matches!(
|
||||
FryPanBottom::new(4),
|
||||
Err(Error::InvalidPeriod { .. })
|
||||
));
|
||||
assert!(FryPanBottom::new(5).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accessors_and_metadata() {
|
||||
let f = FryPanBottom::new(9).unwrap();
|
||||
assert_eq!(f.period(), 9);
|
||||
assert_eq!(f.warmup_period(), 9);
|
||||
assert_eq!(f.name(), "FryPanBottom");
|
||||
assert!(!f.is_ready());
|
||||
assert_eq!(f.value(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn first_emission_at_warmup_period() {
|
||||
let mut f = FryPanBottom::new(5).unwrap();
|
||||
let out = f.batch(&[c(100.0), c(99.0), c(98.0), c(99.0), c(101.0), c(102.0)]);
|
||||
for v in out.iter().take(4) {
|
||||
assert!(v.is_none());
|
||||
}
|
||||
assert!(out[4].is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rounded_bottom_then_recovery_signals() {
|
||||
let mut f = FryPanBottom::new(9).unwrap();
|
||||
let closes = [100.0, 98.0, 96.0, 95.0, 96.0, 98.0, 101.0, 103.0, 105.0];
|
||||
let candles: Vec<Candle> = closes.iter().map(|&x| c(x)).collect();
|
||||
let last = f.batch(&candles).into_iter().flatten().last().unwrap();
|
||||
assert_eq!(last, 1.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn one_sided_drop_is_zero() {
|
||||
// A straight decline (min at the end) is not a bowl.
|
||||
let mut f = FryPanBottom::new(9).unwrap();
|
||||
let candles: Vec<Candle> = (0..9).map(|i| c(100.0 - f64::from(i))).collect();
|
||||
let last = f.batch(&candles).into_iter().flatten().last().unwrap();
|
||||
assert_eq!(last, 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_recovery_is_zero() {
|
||||
// Bowl shape but the last close never climbs above the first.
|
||||
let mut f = FryPanBottom::new(9).unwrap();
|
||||
let closes = [100.0, 98.0, 96.0, 95.0, 96.0, 97.0, 98.0, 99.0, 99.5];
|
||||
let candles: Vec<Candle> = closes.iter().map(|&x| c(x)).collect();
|
||||
let last = f.batch(&candles).into_iter().flatten().last().unwrap();
|
||||
assert_eq!(last, 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut f = FryPanBottom::new(5).unwrap();
|
||||
f.batch(&[c(100.0), c(99.0), c(98.0), c(99.0), c(101.0)]);
|
||||
assert!(f.is_ready());
|
||||
f.reset();
|
||||
assert!(!f.is_ready());
|
||||
assert_eq!(f.value(), None);
|
||||
assert_eq!(f.update(c(100.0)), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_equals_streaming() {
|
||||
let candles: Vec<Candle> = (0..60)
|
||||
.map(|i| c(100.0 + (f64::from(i) * 0.3).sin() * 5.0))
|
||||
.collect();
|
||||
let batch = FryPanBottom::new(9).unwrap().batch(&candles);
|
||||
let mut b = FryPanBottom::new(9).unwrap();
|
||||
let streamed: Vec<_> = candles.iter().map(|x| b.update(*x)).collect();
|
||||
assert_eq!(batch, streamed);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
//! Funding-Implied APR — the per-interval funding rate annualised.
|
||||
|
||||
use crate::derivatives::DerivativesTick;
|
||||
use crate::error::{Error, Result};
|
||||
use crate::traits::Indicator;
|
||||
|
||||
/// Funding-Implied APR — the perpetual's per-interval funding rate scaled to an
|
||||
/// annualised rate.
|
||||
///
|
||||
/// ```text
|
||||
/// APR = funding_rate · intervals_per_year
|
||||
/// ```
|
||||
///
|
||||
/// Funding is paid in small per-interval amounts (commonly every 8 hours, i.e.
|
||||
/// `1095` intervals per year). Annualising it converts the headline funding number
|
||||
/// into the carry cost (or yield) of holding the position for a year, which is far
|
||||
/// easier to reason about and to compare against spot lending rates, basis trades,
|
||||
/// and other yields. A large positive APR means longs pay a steep carry to shorts
|
||||
/// (and vice versa) — the economic incentive behind cash-and-carry and
|
||||
/// funding-arbitrage strategies.
|
||||
///
|
||||
/// The output is a fraction (multiply by `100` for percent) and may be negative.
|
||||
/// It is stateless — each tick yields one value (no warmup). Each `update` is O(1).
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use wickra_core::{DerivativesTick, Indicator, FundingImpliedApr};
|
||||
///
|
||||
/// // 0.01% per 8h funding -> 0.0001 * 1095 ≈ 10.95% APR.
|
||||
/// let mut indicator = FundingImpliedApr::new(1095.0).unwrap();
|
||||
/// let tick = DerivativesTick::new(0.0001, 100.0, 100.0, 100.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0).unwrap();
|
||||
/// let apr = indicator.update(tick).unwrap();
|
||||
/// assert!((apr - 0.1095).abs() < 1e-9);
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct FundingImpliedApr {
|
||||
intervals_per_year: f64,
|
||||
ready: bool,
|
||||
}
|
||||
|
||||
impl FundingImpliedApr {
|
||||
/// Construct a Funding-Implied APR with the number of funding intervals per
|
||||
/// year (e.g. `1095` for 8-hour funding, `365` for daily).
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`Error::InvalidParameter`] if `intervals_per_year` is not finite
|
||||
/// and positive.
|
||||
pub fn new(intervals_per_year: f64) -> Result<Self> {
|
||||
if !intervals_per_year.is_finite() || intervals_per_year <= 0.0 {
|
||||
return Err(Error::InvalidParameter {
|
||||
message: "intervals_per_year must be finite and positive",
|
||||
});
|
||||
}
|
||||
Ok(Self {
|
||||
intervals_per_year,
|
||||
ready: false,
|
||||
})
|
||||
}
|
||||
|
||||
/// Configured intervals per year.
|
||||
pub const fn intervals_per_year(&self) -> f64 {
|
||||
self.intervals_per_year
|
||||
}
|
||||
}
|
||||
|
||||
impl Indicator for FundingImpliedApr {
|
||||
type Input = DerivativesTick;
|
||||
type Output = f64;
|
||||
|
||||
fn update(&mut self, tick: DerivativesTick) -> Option<f64> {
|
||||
self.ready = true;
|
||||
Some(tick.funding_rate * self.intervals_per_year)
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.ready = false;
|
||||
}
|
||||
|
||||
fn warmup_period(&self) -> usize {
|
||||
1
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.ready
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"FundingImpliedApr"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::traits::BatchExt;
|
||||
use approx::assert_relative_eq;
|
||||
|
||||
fn tick(funding: f64) -> DerivativesTick {
|
||||
DerivativesTick::new_unchecked(
|
||||
funding, 100.0, 100.0, 100.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0,
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_invalid_intervals() {
|
||||
assert!(matches!(
|
||||
FundingImpliedApr::new(0.0),
|
||||
Err(Error::InvalidParameter { .. })
|
||||
));
|
||||
assert!(matches!(
|
||||
FundingImpliedApr::new(-1.0),
|
||||
Err(Error::InvalidParameter { .. })
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accessors_and_metadata() {
|
||||
let f = FundingImpliedApr::new(1095.0).unwrap();
|
||||
assert_relative_eq!(f.intervals_per_year(), 1095.0, epsilon = 1e-12);
|
||||
assert_eq!(f.warmup_period(), 1);
|
||||
assert_eq!(f.name(), "FundingImpliedApr");
|
||||
assert!(!f.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn apr_reference_value() {
|
||||
let mut f = FundingImpliedApr::new(1095.0).unwrap();
|
||||
assert_relative_eq!(f.update(tick(0.0001)).unwrap(), 0.1095, epsilon = 1e-9);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn negative_funding_is_negative_apr() {
|
||||
let mut f = FundingImpliedApr::new(1095.0).unwrap();
|
||||
assert!(f.update(tick(-0.0001)).unwrap() < 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zero_funding_is_zero() {
|
||||
let mut f = FundingImpliedApr::new(365.0).unwrap();
|
||||
assert_relative_eq!(f.update(tick(0.0)).unwrap(), 0.0, epsilon = 1e-12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut f = FundingImpliedApr::new(1095.0).unwrap();
|
||||
f.update(tick(0.0001));
|
||||
assert!(f.is_ready());
|
||||
f.reset();
|
||||
assert!(!f.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_equals_streaming() {
|
||||
let ticks: Vec<DerivativesTick> = (0..40)
|
||||
.map(|i| tick(0.0001 * (f64::from(i) * 0.3).sin()))
|
||||
.collect();
|
||||
let batch = FundingImpliedApr::new(1095.0).unwrap().batch(&ticks);
|
||||
let mut b = FundingImpliedApr::new(1095.0).unwrap();
|
||||
let streamed: Vec<_> = ticks.iter().map(|x| b.update(*x)).collect();
|
||||
assert_eq!(batch, streamed);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
#![allow(clippy::doc_markdown)]
|
||||
|
||||
//! Harami Cross — a Harami whose second candle is a Doji.
|
||||
//!
|
||||
//! A Harami Cross is a stronger Harami: a large real body followed by a Doji whose
|
||||
//! body sits *within* the prior body. The Doji's total indecision after a strong
|
||||
//! move makes the reversal signal more potent than a plain Harami.
|
||||
//!
|
||||
//! - **Bullish** (`+1.0`): the prior candle is a large **bearish** body
|
||||
//! (`close < open`) and the current candle is a Doji whose open and close lie
|
||||
//! within the prior body.
|
||||
//! - **Bearish** (`-1.0`): the prior candle is a large **bullish** body and the
|
||||
//! current is a contained Doji.
|
||||
//! - Otherwise the output is `0.0`.
|
||||
//!
|
||||
//! A doji is a candle whose body is `<= 0.1 * range`. The two-bar lookback means
|
||||
//! the first value lands on the second candle.
|
||||
|
||||
use crate::ohlcv::Candle;
|
||||
use crate::traits::Indicator;
|
||||
|
||||
fn is_doji(candle: Candle) -> bool {
|
||||
let body = (candle.close - candle.open).abs();
|
||||
let range = candle.high - candle.low;
|
||||
range > 0.0 && body <= 0.1 * range
|
||||
}
|
||||
|
||||
/// Harami Cross — large-body-then-contained-doji reversal detector.
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct HaramiCross {
|
||||
prev: Option<Candle>,
|
||||
last_value: Option<f64>,
|
||||
}
|
||||
|
||||
impl HaramiCross {
|
||||
/// Construct a new `HaramiCross`.
|
||||
#[must_use]
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Latest emitted signal if available.
|
||||
pub const fn value(&self) -> Option<f64> {
|
||||
self.last_value
|
||||
}
|
||||
}
|
||||
|
||||
impl Indicator for HaramiCross {
|
||||
type Input = Candle;
|
||||
type Output = f64;
|
||||
|
||||
fn update(&mut self, candle: Candle) -> Option<f64> {
|
||||
let Some(prev) = self.prev else {
|
||||
self.prev = Some(candle);
|
||||
self.last_value = Some(0.0);
|
||||
return Some(0.0);
|
||||
};
|
||||
let prev_body_low = prev.open.min(prev.close);
|
||||
let prev_body_high = prev.open.max(prev.close);
|
||||
let prev_is_solid = !is_doji(prev);
|
||||
let curr_is_doji = is_doji(candle);
|
||||
let contained = candle.open >= prev_body_low
|
||||
&& candle.open <= prev_body_high
|
||||
&& candle.close >= prev_body_low
|
||||
&& candle.close <= prev_body_high;
|
||||
|
||||
let v = if prev_is_solid && curr_is_doji && contained {
|
||||
if prev.close < prev.open {
|
||||
1.0
|
||||
} else {
|
||||
-1.0
|
||||
}
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
self.prev = Some(candle);
|
||||
self.last_value = Some(v);
|
||||
Some(v)
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.prev = None;
|
||||
self.last_value = None;
|
||||
}
|
||||
|
||||
fn warmup_period(&self) -> usize {
|
||||
2
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.last_value.is_some()
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"HaramiCross"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::traits::BatchExt;
|
||||
|
||||
fn solid(open: f64, close: f64) -> Candle {
|
||||
Candle::new_unchecked(
|
||||
open,
|
||||
open.max(close) + 0.2,
|
||||
open.min(close) - 0.2,
|
||||
close,
|
||||
0.0,
|
||||
0,
|
||||
)
|
||||
}
|
||||
|
||||
fn doji(mid: f64) -> Candle {
|
||||
Candle::new_unchecked(mid, mid + 1.0, mid - 1.0, mid + 0.02, 0.0, 0)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accessors_and_metadata() {
|
||||
let h = HaramiCross::new();
|
||||
assert_eq!(h.warmup_period(), 2);
|
||||
assert_eq!(h.name(), "HaramiCross");
|
||||
assert!(!h.is_ready());
|
||||
assert_eq!(h.value(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn first_bar_seeds_without_signal() {
|
||||
let mut h = HaramiCross::new();
|
||||
assert_eq!(h.update(solid(110.0, 100.0)), Some(0.0));
|
||||
assert!(h.update(doji(105.0)).is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bullish_harami_cross() {
|
||||
// prior big bearish body [100, 110]; doji centred at 105 inside it -> +1.
|
||||
let mut h = HaramiCross::new();
|
||||
h.update(solid(110.0, 100.0));
|
||||
assert_eq!(h.update(doji(105.0)), Some(1.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bearish_harami_cross() {
|
||||
// prior big bullish body [100, 110]; doji inside -> -1.
|
||||
let mut h = HaramiCross::new();
|
||||
h.update(solid(100.0, 110.0));
|
||||
assert_eq!(h.update(doji(105.0)), Some(-1.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn doji_outside_body_is_zero() {
|
||||
let mut h = HaramiCross::new();
|
||||
h.update(solid(110.0, 100.0));
|
||||
// doji centred at 120, outside the prior body -> 0.
|
||||
assert_eq!(h.update(doji(120.0)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn non_doji_second_is_zero() {
|
||||
let mut h = HaramiCross::new();
|
||||
h.update(solid(110.0, 100.0));
|
||||
assert_eq!(h.update(solid(104.0, 106.0)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut h = HaramiCross::new();
|
||||
h.update(solid(110.0, 100.0));
|
||||
h.update(doji(105.0));
|
||||
assert!(h.is_ready());
|
||||
h.reset();
|
||||
assert!(!h.is_ready());
|
||||
assert_eq!(h.update(solid(110.0, 100.0)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_equals_streaming() {
|
||||
let candles: Vec<Candle> = (0..40)
|
||||
.map(|i| {
|
||||
if i % 2 == 0 {
|
||||
solid(110.0, 100.0)
|
||||
} else {
|
||||
doji(105.0)
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
let batch = HaramiCross::new().batch(&candles);
|
||||
let mut b = HaramiCross::new();
|
||||
let streamed: Vec<_> = candles.iter().map(|x| b.update(*x)).collect();
|
||||
assert_eq!(batch, streamed);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,251 @@
|
||||
//! Hasbrouck Information Share — each venue's contribution to price discovery.
|
||||
|
||||
use std::collections::VecDeque;
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
use crate::traits::Indicator;
|
||||
|
||||
/// Hasbrouck Information Share — the share of price-discovery attributable to the
|
||||
/// **first** of two synchronised price series (e.g. the same asset on two venues).
|
||||
///
|
||||
/// ```text
|
||||
/// rx_t = x_t − x_{t−1}, ry_t = y_t − y_{t−1} (one-step price changes)
|
||||
/// IS_x = var(rx) / ( var(rx) + var(ry) ) over the window, ∈ [0, 1]
|
||||
/// ```
|
||||
///
|
||||
/// When the same instrument trades on several venues, Joel Hasbrouck's information
|
||||
/// share measures how much each venue contributes to the common efficient price.
|
||||
/// The venue whose innovations carry more of the variance leads price discovery.
|
||||
/// This streaming form uses the **variance-ratio proxy**: the fraction of total
|
||||
/// return variance contributed by series `x`. A reading above `0.5` means venue
|
||||
/// `x` is the price leader; below `0.5`, the follower. (The full Hasbrouck measure
|
||||
/// estimates a vector error-correction model and reports an upper/lower bound from
|
||||
/// the Cholesky ordering; this proxy captures the leading idea without the VECM.)
|
||||
///
|
||||
/// The output is in `[0, 1]`; if both series are flat it reports the neutral `0.5`.
|
||||
/// The first value lands after `period + 1` inputs. Each `update` is O(1).
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use wickra_core::{Indicator, HasbrouckInformationShare};
|
||||
///
|
||||
/// let mut indicator = HasbrouckInformationShare::new(20).unwrap();
|
||||
/// let mut last = None;
|
||||
/// for i in 0..40 {
|
||||
/// // Venue x moves a lot, venue y barely moves -> x leads.
|
||||
/// let x = (f64::from(i) * 0.5).sin() * 10.0;
|
||||
/// let y = (f64::from(i) * 0.5).sin() * 1.0;
|
||||
/// last = indicator.update((x, y));
|
||||
/// }
|
||||
/// assert!(last.unwrap() > 0.8);
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct HasbrouckInformationShare {
|
||||
period: usize,
|
||||
prev: Option<(f64, f64)>,
|
||||
window: VecDeque<(f64, f64)>,
|
||||
sum_x: f64,
|
||||
sum_y: f64,
|
||||
sum_xx: f64,
|
||||
sum_yy: f64,
|
||||
}
|
||||
|
||||
impl HasbrouckInformationShare {
|
||||
/// Construct a Hasbrouck information share over `period` return pairs.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`Error::InvalidPeriod`] if `period < 2` (variance needs two
|
||||
/// returns).
|
||||
pub fn new(period: usize) -> Result<Self> {
|
||||
if period < 2 {
|
||||
return Err(Error::InvalidPeriod {
|
||||
message: "information share needs period >= 2",
|
||||
});
|
||||
}
|
||||
Ok(Self {
|
||||
period,
|
||||
prev: None,
|
||||
window: VecDeque::with_capacity(period),
|
||||
sum_x: 0.0,
|
||||
sum_y: 0.0,
|
||||
sum_xx: 0.0,
|
||||
sum_yy: 0.0,
|
||||
})
|
||||
}
|
||||
|
||||
/// Configured window of return pairs.
|
||||
pub const fn period(&self) -> usize {
|
||||
self.period
|
||||
}
|
||||
}
|
||||
|
||||
impl Indicator for HasbrouckInformationShare {
|
||||
type Input = (f64, f64);
|
||||
type Output = f64;
|
||||
|
||||
fn update(&mut self, input: (f64, f64)) -> Option<f64> {
|
||||
let (x, y) = input;
|
||||
let Some((px, py)) = self.prev else {
|
||||
self.prev = Some((x, y));
|
||||
return None;
|
||||
};
|
||||
self.prev = Some((x, y));
|
||||
let (rx, ry) = (x - px, y - py);
|
||||
if self.window.len() == self.period {
|
||||
let (ox, oy) = self.window.pop_front().expect("non-empty");
|
||||
self.sum_x -= ox;
|
||||
self.sum_y -= oy;
|
||||
self.sum_xx -= ox * ox;
|
||||
self.sum_yy -= oy * oy;
|
||||
}
|
||||
self.window.push_back((rx, ry));
|
||||
self.sum_x += rx;
|
||||
self.sum_y += ry;
|
||||
self.sum_xx += rx * rx;
|
||||
self.sum_yy += ry * ry;
|
||||
if self.window.len() < self.period {
|
||||
return None;
|
||||
}
|
||||
let n = self.period as f64;
|
||||
let var_x = (self.sum_xx / n - (self.sum_x / n).powi(2)).max(0.0);
|
||||
let var_y = (self.sum_yy / n - (self.sum_y / n).powi(2)).max(0.0);
|
||||
let total = var_x + var_y;
|
||||
Some(if total > 0.0 { var_x / total } else { 0.5 })
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.prev = None;
|
||||
self.window.clear();
|
||||
self.sum_x = 0.0;
|
||||
self.sum_y = 0.0;
|
||||
self.sum_xx = 0.0;
|
||||
self.sum_yy = 0.0;
|
||||
}
|
||||
|
||||
fn warmup_period(&self) -> usize {
|
||||
self.period + 1
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.window.len() == self.period
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"HasbrouckInformationShare"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::traits::BatchExt;
|
||||
use approx::assert_relative_eq;
|
||||
|
||||
#[test]
|
||||
fn rejects_period_below_two() {
|
||||
assert!(matches!(
|
||||
HasbrouckInformationShare::new(1),
|
||||
Err(Error::InvalidPeriod { .. })
|
||||
));
|
||||
assert!(HasbrouckInformationShare::new(2).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accessors_and_metadata() {
|
||||
let h = HasbrouckInformationShare::new(20).unwrap();
|
||||
assert_eq!(h.period(), 20);
|
||||
assert_eq!(h.warmup_period(), 21);
|
||||
assert_eq!(h.name(), "HasbrouckInformationShare");
|
||||
assert!(!h.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn warmup_needs_period_plus_one() {
|
||||
let mut h = HasbrouckInformationShare::new(3).unwrap();
|
||||
assert_eq!(h.update((1.0, 1.0)), None);
|
||||
assert_eq!(h.update((2.0, 2.0)), None);
|
||||
assert_eq!(h.update((3.0, 2.5)), None);
|
||||
assert!(h.update((4.0, 3.0)).is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn loud_venue_leads() {
|
||||
// x is far more volatile than y -> x holds nearly all the share.
|
||||
let pairs: Vec<(f64, f64)> = (0..40)
|
||||
.map(|i| {
|
||||
(
|
||||
(f64::from(i) * 0.5).sin() * 10.0,
|
||||
(f64::from(i) * 0.5).sin() * 1.0,
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
let last = HasbrouckInformationShare::new(20)
|
||||
.unwrap()
|
||||
.batch(&pairs)
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.last()
|
||||
.unwrap();
|
||||
assert!(last > 0.8, "the loud venue should lead, got {last}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn equal_venues_split_evenly() {
|
||||
// Independent but equal-variance moves -> share near 0.5.
|
||||
let pairs: Vec<(f64, f64)> = (0..200)
|
||||
.map(|i| {
|
||||
(
|
||||
(f64::from(i) * 0.5).sin() * 5.0,
|
||||
(f64::from(i) * 0.5).cos() * 5.0,
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
for v in HasbrouckInformationShare::new(40)
|
||||
.unwrap()
|
||||
.batch(&pairs)
|
||||
.into_iter()
|
||||
.flatten()
|
||||
{
|
||||
assert!((0.0..=1.0).contains(&v));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn flat_series_is_half() {
|
||||
let pairs: Vec<(f64, f64)> = (0..20).map(|_| (7.0, 9.0)).collect();
|
||||
let last = HasbrouckInformationShare::new(5)
|
||||
.unwrap()
|
||||
.batch(&pairs)
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.last()
|
||||
.unwrap();
|
||||
assert_relative_eq!(last, 0.5, epsilon = 1e-12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut h = HasbrouckInformationShare::new(4).unwrap();
|
||||
h.batch(&[(1.0, 1.0), (2.0, 2.0), (3.0, 3.0), (4.0, 4.0), (5.0, 5.0)]);
|
||||
assert!(h.is_ready());
|
||||
h.reset();
|
||||
assert!(!h.is_ready());
|
||||
assert_eq!(h.update((1.0, 1.0)), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_equals_streaming() {
|
||||
let pairs: Vec<(f64, f64)> = (0..120)
|
||||
.map(|i| {
|
||||
let t = f64::from(i);
|
||||
(t.sin() * 5.0, (t * 0.5).cos() * 3.0)
|
||||
})
|
||||
.collect();
|
||||
let batch = HasbrouckInformationShare::new(20).unwrap().batch(&pairs);
|
||||
let mut h = HasbrouckInformationShare::new(20).unwrap();
|
||||
let streamed: Vec<_> = pairs.iter().map(|p| h.update(*p)).collect();
|
||||
assert_eq!(batch, streamed);
|
||||
}
|
||||
}
|
||||
@@ -117,6 +117,7 @@ mod downside_gap_three_methods;
|
||||
mod dpo;
|
||||
mod dragonfly_doji;
|
||||
mod drawdown_duration;
|
||||
mod dumpling_top;
|
||||
mod dx;
|
||||
mod dynamic_momentum_index;
|
||||
mod ease_of_movement;
|
||||
@@ -130,6 +131,7 @@ mod ema;
|
||||
mod empirical_mode_decomposition;
|
||||
mod engulfing;
|
||||
mod equivolume;
|
||||
mod estimated_leverage_ratio;
|
||||
mod even_better_sinewave;
|
||||
mod evening_doji_star;
|
||||
mod evwma;
|
||||
@@ -153,7 +155,9 @@ mod footprint;
|
||||
mod force_index;
|
||||
mod fractal_chaos_bands;
|
||||
mod frama;
|
||||
mod fry_pan_bottom;
|
||||
mod funding_basis;
|
||||
mod funding_implied_apr;
|
||||
mod funding_rate;
|
||||
mod funding_rate_mean;
|
||||
mod funding_rate_zscore;
|
||||
@@ -171,6 +175,8 @@ mod gravestone_doji;
|
||||
mod hammer;
|
||||
mod hanging_man;
|
||||
mod harami;
|
||||
mod harami_cross;
|
||||
mod hasbrouck_information_share;
|
||||
mod head_and_shoulders;
|
||||
mod heikin_ashi;
|
||||
mod heikin_ashi_oscillator;
|
||||
@@ -265,6 +271,7 @@ mod morning_evening_star;
|
||||
mod murrey_math_lines;
|
||||
mod natr;
|
||||
mod new_highs_new_lows;
|
||||
mod new_price_lines;
|
||||
mod nrtr;
|
||||
mod nvi;
|
||||
mod ob_imbalance_full;
|
||||
@@ -273,9 +280,11 @@ mod ob_imbalance_topn;
|
||||
mod obv;
|
||||
mod oi_delta;
|
||||
mod oi_price_divergence;
|
||||
mod oi_to_volume_ratio;
|
||||
mod oi_weighted;
|
||||
mod omega_ratio;
|
||||
mod on_neck;
|
||||
mod open_interest_momentum;
|
||||
mod opening_marubozu;
|
||||
mod opening_range;
|
||||
mod order_flow_imbalance;
|
||||
@@ -290,8 +299,10 @@ mod pearson_correlation;
|
||||
mod percent_above_ma;
|
||||
mod percent_b;
|
||||
mod percentage_trailing_stop;
|
||||
mod perpetual_premium_index;
|
||||
mod pgo;
|
||||
mod piercing_dark_cloud;
|
||||
mod pin;
|
||||
mod pivot_reversal;
|
||||
mod plus_di;
|
||||
mod plus_dm;
|
||||
@@ -416,8 +427,10 @@ mod tick_index;
|
||||
mod tii;
|
||||
mod time_based_stop;
|
||||
mod time_of_day_return_profile;
|
||||
mod tower_top_bottom;
|
||||
mod tpo_profile;
|
||||
mod trade_imbalance;
|
||||
mod trade_sign_autocorrelation;
|
||||
mod trade_volume_index;
|
||||
mod trend_label;
|
||||
mod trend_strength_index;
|
||||
@@ -427,6 +440,7 @@ mod triangle;
|
||||
mod trima;
|
||||
mod trin;
|
||||
mod triple_top_bottom;
|
||||
mod tristar;
|
||||
mod trix;
|
||||
mod true_range;
|
||||
mod tsf;
|
||||
@@ -596,6 +610,7 @@ pub use downside_gap_three_methods::DownsideGapThreeMethods;
|
||||
pub use dpo::Dpo;
|
||||
pub use dragonfly_doji::DragonflyDoji;
|
||||
pub use drawdown_duration::DrawdownDuration;
|
||||
pub use dumpling_top::DumplingTop;
|
||||
pub use dx::Dx;
|
||||
pub use dynamic_momentum_index::DynamicMomentumIndex;
|
||||
pub use ease_of_movement::EaseOfMovement;
|
||||
@@ -609,6 +624,7 @@ pub use ema::Ema;
|
||||
pub use empirical_mode_decomposition::EmpiricalModeDecomposition;
|
||||
pub use engulfing::Engulfing;
|
||||
pub use equivolume::{Equivolume, EquivolumeOutput};
|
||||
pub use estimated_leverage_ratio::EstimatedLeverageRatio;
|
||||
pub use even_better_sinewave::EvenBetterSinewave;
|
||||
pub use evening_doji_star::EveningDojiStar;
|
||||
pub use evwma::Evwma;
|
||||
@@ -632,7 +648,9 @@ pub use footprint::{Footprint, FootprintLevel, FootprintOutput};
|
||||
pub use force_index::ForceIndex;
|
||||
pub use fractal_chaos_bands::{FractalChaosBands, FractalChaosBandsOutput};
|
||||
pub use frama::Frama;
|
||||
pub use fry_pan_bottom::FryPanBottom;
|
||||
pub use funding_basis::FundingBasis;
|
||||
pub use funding_implied_apr::FundingImpliedApr;
|
||||
pub use funding_rate::FundingRate;
|
||||
pub use funding_rate_mean::FundingRateMean;
|
||||
pub use funding_rate_zscore::FundingRateZScore;
|
||||
@@ -650,6 +668,8 @@ pub use gravestone_doji::GravestoneDoji;
|
||||
pub use hammer::Hammer;
|
||||
pub use hanging_man::HangingMan;
|
||||
pub use harami::Harami;
|
||||
pub use harami_cross::HaramiCross;
|
||||
pub use hasbrouck_information_share::HasbrouckInformationShare;
|
||||
pub use head_and_shoulders::HeadAndShoulders;
|
||||
pub use heikin_ashi::{HeikinAshi, HeikinAshiOutput};
|
||||
pub use heikin_ashi_oscillator::HeikinAshiOscillator;
|
||||
@@ -744,6 +764,7 @@ pub use morning_evening_star::MorningEveningStar;
|
||||
pub use murrey_math_lines::{MurreyMathLines, MurreyMathLinesOutput};
|
||||
pub use natr::Natr;
|
||||
pub use new_highs_new_lows::NewHighsNewLows;
|
||||
pub use new_price_lines::NewPriceLines;
|
||||
pub use nrtr::{Nrtr, NrtrOutput};
|
||||
pub use nvi::Nvi;
|
||||
pub use ob_imbalance_full::OrderBookImbalanceFull;
|
||||
@@ -752,9 +773,11 @@ pub use ob_imbalance_topn::OrderBookImbalanceTopN;
|
||||
pub use obv::Obv;
|
||||
pub use oi_delta::OpenInterestDelta;
|
||||
pub use oi_price_divergence::OIPriceDivergence;
|
||||
pub use oi_to_volume_ratio::OiToVolumeRatio;
|
||||
pub use oi_weighted::OIWeighted;
|
||||
pub use omega_ratio::OmegaRatio;
|
||||
pub use on_neck::OnNeck;
|
||||
pub use open_interest_momentum::OpenInterestMomentum;
|
||||
pub use opening_marubozu::OpeningMarubozu;
|
||||
pub use opening_range::{OpeningRange, OpeningRangeOutput};
|
||||
pub use order_flow_imbalance::OrderFlowImbalance;
|
||||
@@ -769,8 +792,10 @@ pub use pearson_correlation::PearsonCorrelation;
|
||||
pub use percent_above_ma::PercentAboveMa;
|
||||
pub use percent_b::PercentB;
|
||||
pub use percentage_trailing_stop::PercentageTrailingStop;
|
||||
pub use perpetual_premium_index::PerpetualPremiumIndex;
|
||||
pub use pgo::Pgo;
|
||||
pub use piercing_dark_cloud::PiercingDarkCloud;
|
||||
pub use pin::Pin;
|
||||
pub use pivot_reversal::PivotReversal;
|
||||
pub use plus_di::PlusDi;
|
||||
pub use plus_dm::PlusDm;
|
||||
@@ -895,8 +920,10 @@ pub use tick_index::TickIndex;
|
||||
pub use tii::Tii;
|
||||
pub use time_based_stop::TimeBasedStop;
|
||||
pub use time_of_day_return_profile::{TimeOfDayReturnProfile, TimeOfDayReturnProfileOutput};
|
||||
pub use tower_top_bottom::TowerTopBottom;
|
||||
pub use tpo_profile::{TpoProfile, TpoProfileOutput};
|
||||
pub use trade_imbalance::TradeImbalance;
|
||||
pub use trade_sign_autocorrelation::TradeSignAutocorrelation;
|
||||
pub use trade_volume_index::TradeVolumeIndex;
|
||||
pub use trend_label::TrendLabel;
|
||||
pub use trend_strength_index::TrendStrengthIndex;
|
||||
@@ -906,6 +933,7 @@ pub use triangle::Triangle;
|
||||
pub use trima::Trima;
|
||||
pub use trin::Trin;
|
||||
pub use triple_top_bottom::TripleTopBottom;
|
||||
pub use tristar::Tristar;
|
||||
pub use trix::Trix;
|
||||
pub use true_range::TrueRange;
|
||||
pub use tsf::Tsf;
|
||||
@@ -1412,6 +1440,12 @@ pub const FAMILIES: &[(&str, &[&str])] = &[
|
||||
"TasukiGap",
|
||||
"UniqueThreeRiver",
|
||||
"ConcealingBabySwallow",
|
||||
"Tristar",
|
||||
"HaramiCross",
|
||||
"TowerTopBottom",
|
||||
"FryPanBottom",
|
||||
"DumplingTop",
|
||||
"NewPriceLines",
|
||||
],
|
||||
),
|
||||
(
|
||||
@@ -1434,6 +1468,9 @@ pub const FAMILIES: &[(&str, &[&str])] = &[
|
||||
"Vpin",
|
||||
"AmihudIlliquidity",
|
||||
"RollMeasure",
|
||||
"TradeSignAutocorrelation",
|
||||
"Pin",
|
||||
"HasbrouckInformationShare",
|
||||
],
|
||||
),
|
||||
(
|
||||
@@ -1451,6 +1488,11 @@ pub const FAMILIES: &[(&str, &[&str])] = &[
|
||||
"LiquidationFeatures",
|
||||
"TermStructureBasis",
|
||||
"CalendarSpread",
|
||||
"EstimatedLeverageRatio",
|
||||
"OiToVolumeRatio",
|
||||
"PerpetualPremiumIndex",
|
||||
"FundingImpliedApr",
|
||||
"OpenInterestMomentum",
|
||||
],
|
||||
),
|
||||
(
|
||||
@@ -1597,6 +1639,6 @@ mod family_tests {
|
||||
// the actual indicator count is the early-warning signal that an
|
||||
// indicator was added without being assigned a family.
|
||||
let total: usize = FAMILIES.iter().map(|(_, ns)| ns.len()).sum();
|
||||
assert_eq!(total, 479, "FAMILIES total drifted from indicator count");
|
||||
assert_eq!(total, 493, "FAMILIES total drifted from indicator count");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,234 @@
|
||||
//! New Price Lines — the "eight/ten new price lines" exhaustion count.
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
use crate::ohlcv::Candle;
|
||||
use crate::traits::Indicator;
|
||||
|
||||
/// New Price Lines — the Japanese "shinne" (new-price) exhaustion count: when the
|
||||
/// close has made `count` consecutive new highs (or lows), the trend is considered
|
||||
/// stretched and ripe for a pause or reversal.
|
||||
///
|
||||
/// ```text
|
||||
/// consecutive higher closes form "new price lines" up
|
||||
/// consecutive lower closes form "new price lines" down
|
||||
/// signal = −1 once `count` consecutive higher closes (overbought / sell warning)
|
||||
/// signal = +1 once `count` consecutive lower closes (oversold / buy warning)
|
||||
/// signal = 0 otherwise
|
||||
/// ```
|
||||
///
|
||||
/// Traditional Japanese practice flags **eight** new price lines (and a stronger
|
||||
/// **ten** or twelve) as the point where a directional run becomes exhausted —
|
||||
/// the market has gone up (or down) so many bars in a row that a corrective pause
|
||||
/// is statistically due. The signal stays active for every bar the streak remains
|
||||
/// at or above `count`, and clears the moment a close breaks the streak.
|
||||
///
|
||||
/// The first value lands on the second bar (one prior close is needed). The
|
||||
/// output is `+1` / `0` / `−1`. Each `update` is O(1).
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use wickra_core::{Candle, Indicator, NewPriceLines};
|
||||
///
|
||||
/// let mut indicator = NewPriceLines::new(8).unwrap();
|
||||
/// let mut last = None;
|
||||
/// for i in 0..12 {
|
||||
/// let close = 100.0 + f64::from(i); // 11 consecutive higher closes
|
||||
/// let c = Candle::new(close, close, close, close, 1_000.0, 0).unwrap();
|
||||
/// last = indicator.update(c);
|
||||
/// }
|
||||
/// assert_eq!(last, Some(-1.0));
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct NewPriceLines {
|
||||
count: usize,
|
||||
prev_close: Option<f64>,
|
||||
consec_up: usize,
|
||||
consec_down: usize,
|
||||
last: Option<f64>,
|
||||
}
|
||||
|
||||
impl NewPriceLines {
|
||||
/// Construct a New Price Lines counter that fires at `count` consecutive new
|
||||
/// closes (classic `8`, stronger `10`/`12`).
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`Error::InvalidPeriod`] if `count < 2`.
|
||||
pub fn new(count: usize) -> Result<Self> {
|
||||
if count < 2 {
|
||||
return Err(Error::InvalidPeriod {
|
||||
message: "new price lines count must be >= 2",
|
||||
});
|
||||
}
|
||||
Ok(Self {
|
||||
count,
|
||||
prev_close: None,
|
||||
consec_up: 0,
|
||||
consec_down: 0,
|
||||
last: None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Configured count threshold.
|
||||
pub const fn count(&self) -> usize {
|
||||
self.count
|
||||
}
|
||||
|
||||
/// Current consecutive streak `(up, down)`.
|
||||
pub const fn streak(&self) -> (usize, usize) {
|
||||
(self.consec_up, self.consec_down)
|
||||
}
|
||||
|
||||
/// Current value if available.
|
||||
pub const fn value(&self) -> Option<f64> {
|
||||
self.last
|
||||
}
|
||||
}
|
||||
|
||||
impl Indicator for NewPriceLines {
|
||||
type Input = Candle;
|
||||
type Output = f64;
|
||||
|
||||
fn update(&mut self, candle: Candle) -> Option<f64> {
|
||||
let close = candle.close;
|
||||
let Some(prev) = self.prev_close else {
|
||||
self.prev_close = Some(close);
|
||||
return None;
|
||||
};
|
||||
if close > prev {
|
||||
self.consec_up += 1;
|
||||
self.consec_down = 0;
|
||||
} else if close < prev {
|
||||
self.consec_down += 1;
|
||||
self.consec_up = 0;
|
||||
} else {
|
||||
self.consec_up = 0;
|
||||
self.consec_down = 0;
|
||||
}
|
||||
self.prev_close = Some(close);
|
||||
|
||||
let v = if self.consec_up >= self.count {
|
||||
-1.0
|
||||
} else if self.consec_down >= self.count {
|
||||
1.0
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
self.last = Some(v);
|
||||
Some(v)
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.prev_close = None;
|
||||
self.consec_up = 0;
|
||||
self.consec_down = 0;
|
||||
self.last = None;
|
||||
}
|
||||
|
||||
fn warmup_period(&self) -> usize {
|
||||
2
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.last.is_some()
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"NewPriceLines"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::traits::BatchExt;
|
||||
|
||||
fn c(close: f64) -> Candle {
|
||||
Candle::new_unchecked(close, close, close, close, 1_000.0, 0)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_small_count() {
|
||||
assert!(matches!(
|
||||
NewPriceLines::new(1),
|
||||
Err(Error::InvalidPeriod { .. })
|
||||
));
|
||||
assert!(NewPriceLines::new(2).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accessors_and_metadata() {
|
||||
let n = NewPriceLines::new(8).unwrap();
|
||||
assert_eq!(n.count(), 8);
|
||||
assert_eq!(n.streak(), (0, 0));
|
||||
assert_eq!(n.warmup_period(), 2);
|
||||
assert_eq!(n.name(), "NewPriceLines");
|
||||
assert!(!n.is_ready());
|
||||
assert_eq!(n.value(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn first_bar_seeds_without_signal() {
|
||||
let mut n = NewPriceLines::new(3).unwrap();
|
||||
assert_eq!(n.update(c(100.0)), None);
|
||||
assert!(n.update(c(101.0)).is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eight_higher_closes_signal_sell() {
|
||||
let mut n = NewPriceLines::new(8).unwrap();
|
||||
// 11 consecutive higher closes -> by the 9th the count reaches 8 -> -1.
|
||||
let candles: Vec<Candle> = (0..12).map(|i| c(100.0 + f64::from(i))).collect();
|
||||
let last = n.batch(&candles).into_iter().flatten().last().unwrap();
|
||||
assert_eq!(last, -1.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eight_lower_closes_signal_buy() {
|
||||
let mut n = NewPriceLines::new(8).unwrap();
|
||||
let candles: Vec<Candle> = (0..12).map(|i| c(200.0 - f64::from(i))).collect();
|
||||
let last = n.batch(&candles).into_iter().flatten().last().unwrap();
|
||||
assert_eq!(last, 1.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn break_in_streak_clears_signal() {
|
||||
let mut n = NewPriceLines::new(3).unwrap();
|
||||
n.batch(&[c(100.0), c(101.0), c(102.0), c(103.0)]); // streak 3 -> -1
|
||||
assert_eq!(n.value(), Some(-1.0));
|
||||
// A lower close breaks the up streak.
|
||||
assert_eq!(n.update(c(102.0)), Some(0.0));
|
||||
assert_eq!(n.streak(), (0, 1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unchanged_close_resets_streak() {
|
||||
let mut n = NewPriceLines::new(3).unwrap();
|
||||
n.batch(&[c(100.0), c(101.0), c(102.0)]);
|
||||
assert_eq!(n.update(c(102.0)), Some(0.0)); // equal -> reset
|
||||
assert_eq!(n.streak(), (0, 0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut n = NewPriceLines::new(3).unwrap();
|
||||
n.batch(&[c(100.0), c(101.0), c(102.0), c(103.0)]);
|
||||
assert!(n.is_ready());
|
||||
n.reset();
|
||||
assert!(!n.is_ready());
|
||||
assert_eq!(n.value(), None);
|
||||
assert_eq!(n.streak(), (0, 0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_equals_streaming() {
|
||||
let candles: Vec<Candle> = (0..80)
|
||||
.map(|i| c(100.0 + (f64::from(i) * 0.25).sin() * 9.0))
|
||||
.collect();
|
||||
let batch = NewPriceLines::new(8).unwrap().batch(&candles);
|
||||
let mut b = NewPriceLines::new(8).unwrap();
|
||||
let streamed: Vec<_> = candles.iter().map(|x| b.update(*x)).collect();
|
||||
assert_eq!(batch, streamed);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
//! OI-to-Volume Ratio — open interest relative to traded volume.
|
||||
|
||||
use crate::derivatives::DerivativesTick;
|
||||
use crate::traits::Indicator;
|
||||
|
||||
/// OI-to-Volume Ratio — open interest divided by the tick's total taker volume, a
|
||||
/// measure of how much position is *held* versus *turned over*.
|
||||
///
|
||||
/// ```text
|
||||
/// OIVR = open_interest / (taker_buy_volume + taker_sell_volume)
|
||||
/// ```
|
||||
///
|
||||
/// A high ratio means open interest dwarfs the volume trading it — positions are
|
||||
/// being held, not churned (low participation, potential complacency or a coiling
|
||||
/// market). A low ratio means heavy volume relative to outstanding interest —
|
||||
/// active churn, often around breakouts or capitulation. Watching the ratio change
|
||||
/// distinguishes new-money trends (OI and volume both rising) from short-covering
|
||||
/// or position rolls.
|
||||
///
|
||||
/// The ratio is non-negative; a tick with zero taker volume reports `0` rather than
|
||||
/// dividing by zero. It is stateless — each tick yields one value (no warmup). Each
|
||||
/// `update` is O(1).
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use wickra_core::{DerivativesTick, Indicator, OiToVolumeRatio};
|
||||
///
|
||||
/// let mut indicator = OiToVolumeRatio::new();
|
||||
/// let tick = DerivativesTick::new(0.0, 100.0, 100.0, 100.0, 5_000.0, 0.0, 0.0, 400.0, 600.0, 0.0, 0.0, 0).unwrap();
|
||||
/// let oivr = indicator.update(tick).unwrap();
|
||||
/// assert!((oivr - 5.0).abs() < 1e-12); // 5000 / (400 + 600)
|
||||
/// ```
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct OiToVolumeRatio {
|
||||
ready: bool,
|
||||
}
|
||||
|
||||
impl OiToVolumeRatio {
|
||||
/// Construct a new OI-to-Volume Ratio. The indicator is parameter-free.
|
||||
#[must_use]
|
||||
pub const fn new() -> Self {
|
||||
Self { ready: false }
|
||||
}
|
||||
}
|
||||
|
||||
impl Indicator for OiToVolumeRatio {
|
||||
type Input = DerivativesTick;
|
||||
type Output = f64;
|
||||
|
||||
fn update(&mut self, tick: DerivativesTick) -> Option<f64> {
|
||||
let volume = tick.taker_buy_volume + tick.taker_sell_volume;
|
||||
let ratio = if volume > 0.0 {
|
||||
tick.open_interest / volume
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
self.ready = true;
|
||||
Some(ratio)
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.ready = false;
|
||||
}
|
||||
|
||||
fn warmup_period(&self) -> usize {
|
||||
1
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.ready
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"OiToVolumeRatio"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::traits::BatchExt;
|
||||
use approx::assert_relative_eq;
|
||||
|
||||
fn tick(oi: f64, buy: f64, sell: f64) -> DerivativesTick {
|
||||
DerivativesTick::new_unchecked(
|
||||
0.0, 100.0, 100.0, 100.0, oi, 0.0, 0.0, buy, sell, 0.0, 0.0, 0,
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accessors_and_metadata() {
|
||||
let o = OiToVolumeRatio::new();
|
||||
assert_eq!(o.warmup_period(), 1);
|
||||
assert_eq!(o.name(), "OiToVolumeRatio");
|
||||
assert!(!o.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ratio_reference_value() {
|
||||
let mut o = OiToVolumeRatio::new();
|
||||
assert_relative_eq!(
|
||||
o.update(tick(5_000.0, 400.0, 600.0)).unwrap(),
|
||||
5.0,
|
||||
epsilon = 1e-12
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn more_volume_lowers_ratio() {
|
||||
let mut o = OiToVolumeRatio::new();
|
||||
let held = o.update(tick(5_000.0, 100.0, 100.0)).unwrap();
|
||||
let churned = o.update(tick(5_000.0, 1_000.0, 1_000.0)).unwrap();
|
||||
assert!(churned < held);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zero_volume_is_zero() {
|
||||
let mut o = OiToVolumeRatio::new();
|
||||
assert_relative_eq!(
|
||||
o.update(tick(5_000.0, 0.0, 0.0)).unwrap(),
|
||||
0.0,
|
||||
epsilon = 1e-12
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ready_after_first_update() {
|
||||
let mut o = OiToVolumeRatio::new();
|
||||
assert!(!o.is_ready());
|
||||
o.update(tick(5_000.0, 100.0, 100.0));
|
||||
assert!(o.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut o = OiToVolumeRatio::new();
|
||||
o.update(tick(5_000.0, 100.0, 100.0));
|
||||
assert!(o.is_ready());
|
||||
o.reset();
|
||||
assert!(!o.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_equals_streaming() {
|
||||
let ticks: Vec<DerivativesTick> = (0..40)
|
||||
.map(|i| tick(5_000.0, 100.0 + f64::from(i), 100.0))
|
||||
.collect();
|
||||
let batch = OiToVolumeRatio::new().batch(&ticks);
|
||||
let mut b = OiToVolumeRatio::new();
|
||||
let streamed: Vec<_> = ticks.iter().map(|x| b.update(*x)).collect();
|
||||
assert_eq!(batch, streamed);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,221 @@
|
||||
//! Open-Interest Momentum — the rate of change of open interest over a lookback.
|
||||
|
||||
use std::collections::VecDeque;
|
||||
|
||||
use crate::derivatives::DerivativesTick;
|
||||
use crate::error::{Error, Result};
|
||||
use crate::traits::Indicator;
|
||||
|
||||
/// Open-Interest Momentum — the percentage rate of change of open interest over a
|
||||
/// `period`-tick lookback.
|
||||
///
|
||||
/// ```text
|
||||
/// OIM = 100 · (OI_t − OI_{t−period}) / OI_{t−period}
|
||||
/// ```
|
||||
///
|
||||
/// Where [`OIDelta`](crate::OIDelta) reports the single-tick change in open
|
||||
/// interest, OI Momentum measures the trend in positioning over a window: positive
|
||||
/// values mean open interest is expanding (new money entering — a position build
|
||||
/// that fuels the prevailing move), negative values mean it is contracting
|
||||
/// (positions being closed — deleveraging or short-covering). Read alongside price:
|
||||
/// rising OI with rising price is a strong new-long trend, while rising price with
|
||||
/// falling OI is a short-covering rally on borrowed time.
|
||||
///
|
||||
/// The output is a percentage and may be negative. A zero base open interest
|
||||
/// `period` ticks ago reports `0` rather than dividing by zero. The first value
|
||||
/// lands after `period + 1` inputs. Each `update` is O(1).
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use wickra_core::{DerivativesTick, Indicator, OpenInterestMomentum};
|
||||
///
|
||||
/// let mut indicator = OpenInterestMomentum::new(5).unwrap();
|
||||
/// let mut last = None;
|
||||
/// for i in 0..20 {
|
||||
/// let oi = 1_000.0 + f64::from(i) * 100.0;
|
||||
/// let tick = DerivativesTick::new(0.0, 100.0, 100.0, 100.0, oi, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0).unwrap();
|
||||
/// last = indicator.update(tick);
|
||||
/// }
|
||||
/// assert!(last.unwrap() > 0.0); // expanding OI
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct OpenInterestMomentum {
|
||||
period: usize,
|
||||
window: VecDeque<f64>,
|
||||
last: Option<f64>,
|
||||
}
|
||||
|
||||
impl OpenInterestMomentum {
|
||||
/// Construct an OI Momentum over a `period`-tick lookback.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`Error::PeriodZero`] if `period == 0`.
|
||||
pub fn new(period: usize) -> Result<Self> {
|
||||
if period == 0 {
|
||||
return Err(Error::PeriodZero);
|
||||
}
|
||||
Ok(Self {
|
||||
period,
|
||||
window: VecDeque::with_capacity(period + 1),
|
||||
last: None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Configured lookback period.
|
||||
pub const fn period(&self) -> usize {
|
||||
self.period
|
||||
}
|
||||
|
||||
/// Current value if available.
|
||||
pub const fn value(&self) -> Option<f64> {
|
||||
self.last
|
||||
}
|
||||
}
|
||||
|
||||
impl Indicator for OpenInterestMomentum {
|
||||
type Input = DerivativesTick;
|
||||
type Output = f64;
|
||||
|
||||
fn update(&mut self, tick: DerivativesTick) -> Option<f64> {
|
||||
if self.window.len() == self.period + 1 {
|
||||
self.window.pop_front();
|
||||
}
|
||||
self.window.push_back(tick.open_interest);
|
||||
if self.window.len() < self.period + 1 {
|
||||
return None;
|
||||
}
|
||||
let base = *self.window.front().expect("non-empty");
|
||||
let current = tick.open_interest;
|
||||
let oim = if base > 0.0 {
|
||||
100.0 * (current - base) / base
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
self.last = Some(oim);
|
||||
Some(oim)
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.window.clear();
|
||||
self.last = None;
|
||||
}
|
||||
|
||||
fn warmup_period(&self) -> usize {
|
||||
self.period + 1
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.last.is_some()
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"OpenInterestMomentum"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::traits::BatchExt;
|
||||
use approx::assert_relative_eq;
|
||||
|
||||
fn tick(oi: f64) -> DerivativesTick {
|
||||
DerivativesTick::new_unchecked(
|
||||
0.0, 100.0, 100.0, 100.0, oi, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0,
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_zero_period() {
|
||||
assert!(matches!(
|
||||
OpenInterestMomentum::new(0),
|
||||
Err(Error::PeriodZero)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accessors_and_metadata() {
|
||||
let o = OpenInterestMomentum::new(5).unwrap();
|
||||
assert_eq!(o.period(), 5);
|
||||
assert_eq!(o.warmup_period(), 6);
|
||||
assert_eq!(o.name(), "OpenInterestMomentum");
|
||||
assert!(!o.is_ready());
|
||||
assert_eq!(o.value(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn first_emission_at_warmup_period() {
|
||||
let mut o = OpenInterestMomentum::new(3).unwrap();
|
||||
let ticks: Vec<DerivativesTick> = (0..6)
|
||||
.map(|i| tick(1_000.0 + f64::from(i) * 100.0))
|
||||
.collect();
|
||||
let out = o.batch(&ticks);
|
||||
for v in out.iter().take(3) {
|
||||
assert!(v.is_none());
|
||||
}
|
||||
assert!(out[3].is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reference_value() {
|
||||
// period 2: OI 1000 -> 1200 over the window -> +20%.
|
||||
let mut o = OpenInterestMomentum::new(2).unwrap();
|
||||
let out = o.batch(&[tick(1_000.0), tick(1_100.0), tick(1_200.0)]);
|
||||
assert_relative_eq!(out[2].unwrap(), 20.0, epsilon = 1e-9);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn expanding_oi_is_positive() {
|
||||
let mut o = OpenInterestMomentum::new(5).unwrap();
|
||||
let ticks: Vec<DerivativesTick> = (0..20)
|
||||
.map(|i| tick(1_000.0 + f64::from(i) * 100.0))
|
||||
.collect();
|
||||
let last = o.batch(&ticks).into_iter().flatten().last().unwrap();
|
||||
assert!(last > 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn contracting_oi_is_negative() {
|
||||
let mut o = OpenInterestMomentum::new(5).unwrap();
|
||||
let ticks: Vec<DerivativesTick> = (0..20)
|
||||
.map(|i| tick(3_000.0 - f64::from(i) * 100.0))
|
||||
.collect();
|
||||
let last = o.batch(&ticks).into_iter().flatten().last().unwrap();
|
||||
assert!(last < 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zero_base_is_zero() {
|
||||
let mut o = OpenInterestMomentum::new(2).unwrap();
|
||||
let out = o.batch(&[tick(0.0), tick(100.0), tick(200.0)]);
|
||||
assert_relative_eq!(out[2].unwrap(), 0.0, epsilon = 1e-12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut o = OpenInterestMomentum::new(3).unwrap();
|
||||
o.batch(
|
||||
&(0..10)
|
||||
.map(|i| tick(1_000.0 + f64::from(i) * 50.0))
|
||||
.collect::<Vec<_>>(),
|
||||
);
|
||||
assert!(o.is_ready());
|
||||
o.reset();
|
||||
assert!(!o.is_ready());
|
||||
assert_eq!(o.value(), None);
|
||||
assert_eq!(o.update(tick(1_000.0)), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_equals_streaming() {
|
||||
let ticks: Vec<DerivativesTick> = (0..80)
|
||||
.map(|i| tick(1_000.0 + (f64::from(i) * 0.25).sin() * 300.0))
|
||||
.collect();
|
||||
let batch = OpenInterestMomentum::new(10).unwrap().batch(&ticks);
|
||||
let mut b = OpenInterestMomentum::new(10).unwrap();
|
||||
let streamed: Vec<_> = ticks.iter().map(|x| b.update(*x)).collect();
|
||||
assert_eq!(batch, streamed);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
//! Perpetual Premium Index — the perp mark price relative to spot.
|
||||
|
||||
use crate::derivatives::DerivativesTick;
|
||||
use crate::traits::Indicator;
|
||||
|
||||
/// Perpetual Premium Index — the perpetual's mark price relative to the spot index
|
||||
/// it tracks, as a fraction.
|
||||
///
|
||||
/// ```text
|
||||
/// premium = (mark_price − index_price) / index_price
|
||||
/// ```
|
||||
///
|
||||
/// A perpetual swap is pegged to spot by the funding mechanism, but it can still
|
||||
/// trade at a premium (above spot) or discount (below). A positive premium signals
|
||||
/// net long demand willing to pay up to hold the perp — bullish positioning, and
|
||||
/// the proximate driver of positive funding; a negative premium signals the
|
||||
/// reverse. Sustained extremes flag crowded positioning ripe for a funding-driven
|
||||
/// mean reversion.
|
||||
///
|
||||
/// The output is centred on zero and dimensionless (a fraction; multiply by `100`
|
||||
/// for percent). `index_price` is validated strictly positive on the tick, so the
|
||||
/// division is always defined. It is stateless — each tick yields one value (no
|
||||
/// warmup). Each `update` is O(1).
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use wickra_core::{DerivativesTick, Indicator, PerpetualPremiumIndex};
|
||||
///
|
||||
/// let mut indicator = PerpetualPremiumIndex::new();
|
||||
/// // Mark 101 vs index 100 -> +1% premium.
|
||||
/// let tick = DerivativesTick::new(0.0, 101.0, 100.0, 101.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0).unwrap();
|
||||
/// let premium = indicator.update(tick).unwrap();
|
||||
/// assert!((premium - 0.01).abs() < 1e-12);
|
||||
/// ```
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct PerpetualPremiumIndex {
|
||||
ready: bool,
|
||||
}
|
||||
|
||||
impl PerpetualPremiumIndex {
|
||||
/// Construct a new Perpetual Premium Index. The indicator is parameter-free.
|
||||
#[must_use]
|
||||
pub const fn new() -> Self {
|
||||
Self { ready: false }
|
||||
}
|
||||
}
|
||||
|
||||
impl Indicator for PerpetualPremiumIndex {
|
||||
type Input = DerivativesTick;
|
||||
type Output = f64;
|
||||
|
||||
fn update(&mut self, tick: DerivativesTick) -> Option<f64> {
|
||||
let premium = (tick.mark_price - tick.index_price) / tick.index_price;
|
||||
self.ready = true;
|
||||
Some(premium)
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.ready = false;
|
||||
}
|
||||
|
||||
fn warmup_period(&self) -> usize {
|
||||
1
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.ready
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"PerpetualPremiumIndex"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::traits::BatchExt;
|
||||
use approx::assert_relative_eq;
|
||||
|
||||
fn tick(mark: f64, index: f64) -> DerivativesTick {
|
||||
DerivativesTick::new_unchecked(0.0, mark, index, mark, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accessors_and_metadata() {
|
||||
let p = PerpetualPremiumIndex::new();
|
||||
assert_eq!(p.warmup_period(), 1);
|
||||
assert_eq!(p.name(), "PerpetualPremiumIndex");
|
||||
assert!(!p.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn premium_reference_value() {
|
||||
let mut p = PerpetualPremiumIndex::new();
|
||||
assert_relative_eq!(p.update(tick(101.0, 100.0)).unwrap(), 0.01, epsilon = 1e-12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn discount_is_negative() {
|
||||
let mut p = PerpetualPremiumIndex::new();
|
||||
assert!(p.update(tick(99.0, 100.0)).unwrap() < 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn at_par_is_zero() {
|
||||
let mut p = PerpetualPremiumIndex::new();
|
||||
assert_relative_eq!(p.update(tick(100.0, 100.0)).unwrap(), 0.0, epsilon = 1e-12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ready_after_first_update() {
|
||||
let mut p = PerpetualPremiumIndex::new();
|
||||
assert!(!p.is_ready());
|
||||
p.update(tick(100.0, 100.0));
|
||||
assert!(p.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut p = PerpetualPremiumIndex::new();
|
||||
p.update(tick(101.0, 100.0));
|
||||
assert!(p.is_ready());
|
||||
p.reset();
|
||||
assert!(!p.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_equals_streaming() {
|
||||
let ticks: Vec<DerivativesTick> = (0..40)
|
||||
.map(|i| tick(100.0 + (f64::from(i) * 0.3).sin(), 100.0))
|
||||
.collect();
|
||||
let batch = PerpetualPremiumIndex::new().batch(&ticks);
|
||||
let mut b = PerpetualPremiumIndex::new();
|
||||
let streamed: Vec<_> = ticks.iter().map(|x| b.update(*x)).collect();
|
||||
assert_eq!(batch, streamed);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
//! PIN — Probability of Informed Trading (single-window EKOP estimate).
|
||||
|
||||
use std::collections::VecDeque;
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
use crate::microstructure::Trade;
|
||||
use crate::traits::Indicator;
|
||||
|
||||
/// PIN — the **Probability of Informed Trading**, estimated from the buy/sell order
|
||||
/// imbalance over a rolling window of trades.
|
||||
///
|
||||
/// ```text
|
||||
/// over the last `window` trades: B = buys, S = sells (B + S = window)
|
||||
/// PIN ≈ |B − S| / (B + S) ∈ [0, 1]
|
||||
/// ```
|
||||
///
|
||||
/// The Easley-Kiefer-O'Hara-Paperman (EKOP) model splits order flow into an
|
||||
/// uninformed component (balanced buys and sells, rate `ε` per side) and an
|
||||
/// informed component that trades one-directionally when private information
|
||||
/// arrives (rate `μ`, probability `α`). The probability that any given trade is
|
||||
/// information-motivated is `PIN = αμ / (αμ + 2ε)`. Estimated over a single window,
|
||||
/// the informed flow shows up as the **net imbalance** `|B − S|` and the uninformed
|
||||
/// flow as the balanced remainder, giving the moment estimator above. A high PIN
|
||||
/// flags a one-sided, likely-informed market; a low PIN flags balanced, uninformed
|
||||
/// flow.
|
||||
///
|
||||
/// This is distinct from [`Vpin`](crate::Vpin), the volume-synchronised variant
|
||||
/// that buckets by volume and uses bulk-volume classification; here trades are
|
||||
/// counted in event time and classified by their tagged aggressor side. The full
|
||||
/// PIN is fit by maximum likelihood over many periods — this single-window
|
||||
/// estimator is the streaming moment approximation. The output is in `[0, 1]`; the
|
||||
/// first value lands after `window` trades.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use wickra_core::{Indicator, Pin, Side, Trade};
|
||||
///
|
||||
/// let mut indicator = Pin::new(20).unwrap();
|
||||
/// let mut last = None;
|
||||
/// for i in 0..40 {
|
||||
/// // All buys -> maximally one-sided -> PIN 1.
|
||||
/// last = indicator.update(Trade::new(100.0, 1.0, Side::Buy, i).unwrap());
|
||||
/// }
|
||||
/// assert!((last.unwrap() - 1.0).abs() < 1e-9);
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Pin {
|
||||
window: usize,
|
||||
sides: VecDeque<f64>,
|
||||
buy_count: usize,
|
||||
last: Option<f64>,
|
||||
}
|
||||
|
||||
impl Pin {
|
||||
/// Construct a PIN estimator over `window` trades.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`Error::PeriodZero`] if `window == 0`.
|
||||
pub fn new(window: usize) -> Result<Self> {
|
||||
if window == 0 {
|
||||
return Err(Error::PeriodZero);
|
||||
}
|
||||
Ok(Self {
|
||||
window,
|
||||
sides: VecDeque::with_capacity(window),
|
||||
buy_count: 0,
|
||||
last: None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Configured window of trades.
|
||||
pub const fn window(&self) -> usize {
|
||||
self.window
|
||||
}
|
||||
|
||||
/// Current value if available.
|
||||
pub const fn value(&self) -> Option<f64> {
|
||||
self.last
|
||||
}
|
||||
}
|
||||
|
||||
impl Indicator for Pin {
|
||||
type Input = Trade;
|
||||
type Output = f64;
|
||||
|
||||
fn update(&mut self, trade: Trade) -> Option<f64> {
|
||||
let is_buy = trade.side.sign() > 0.0;
|
||||
if self.sides.len() == self.window {
|
||||
let old = self.sides.pop_front().expect("non-empty");
|
||||
if old > 0.0 {
|
||||
self.buy_count -= 1;
|
||||
}
|
||||
}
|
||||
self.sides.push_back(if is_buy { 1.0 } else { 0.0 });
|
||||
if is_buy {
|
||||
self.buy_count += 1;
|
||||
}
|
||||
if self.sides.len() < self.window {
|
||||
return None;
|
||||
}
|
||||
// The window is full and `window >= 1` (zero is rejected at
|
||||
// construction), so the trade count is always positive — `|B - S| / N`
|
||||
// needs no zero guard.
|
||||
let buys = self.buy_count as f64;
|
||||
let sells = self.window as f64 - buys;
|
||||
let total = self.window as f64;
|
||||
let pin = (buys - sells).abs() / total;
|
||||
self.last = Some(pin);
|
||||
Some(pin)
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.sides.clear();
|
||||
self.buy_count = 0;
|
||||
self.last = None;
|
||||
}
|
||||
|
||||
fn warmup_period(&self) -> usize {
|
||||
self.window
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.last.is_some()
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"PIN"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::microstructure::Side;
|
||||
use crate::traits::BatchExt;
|
||||
use approx::assert_relative_eq;
|
||||
|
||||
fn buy() -> Trade {
|
||||
Trade::new_unchecked(100.0, 1.0, Side::Buy, 0)
|
||||
}
|
||||
|
||||
fn sell() -> Trade {
|
||||
Trade::new_unchecked(100.0, 1.0, Side::Sell, 0)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_zero_window() {
|
||||
assert!(matches!(Pin::new(0), Err(Error::PeriodZero)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accessors_and_metadata() {
|
||||
let p = Pin::new(20).unwrap();
|
||||
assert_eq!(p.window(), 20);
|
||||
assert_eq!(p.warmup_period(), 20);
|
||||
assert_eq!(p.name(), "PIN");
|
||||
assert!(!p.is_ready());
|
||||
assert_eq!(p.value(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn first_emission_at_warmup_period() {
|
||||
let mut p = Pin::new(4).unwrap();
|
||||
let out = p.batch(&[buy(), buy(), buy(), buy(), buy()]);
|
||||
for v in out.iter().take(3) {
|
||||
assert!(v.is_none());
|
||||
}
|
||||
assert!(out[3].is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn one_sided_flow_is_one() {
|
||||
let mut p = Pin::new(10).unwrap();
|
||||
let trades: Vec<Trade> = (0..20).map(|_| buy()).collect();
|
||||
let last = p.batch(&trades).into_iter().flatten().last().unwrap();
|
||||
assert_relative_eq!(last, 1.0, epsilon = 1e-12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn balanced_flow_is_zero() {
|
||||
let mut p = Pin::new(10).unwrap();
|
||||
let trades: Vec<Trade> = (0..20)
|
||||
.map(|i| if i % 2 == 0 { buy() } else { sell() })
|
||||
.collect();
|
||||
let last = p.batch(&trades).into_iter().flatten().last().unwrap();
|
||||
assert_relative_eq!(last, 0.0, epsilon = 1e-12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn output_in_range() {
|
||||
let mut p = Pin::new(16).unwrap();
|
||||
let trades: Vec<Trade> = (0..200)
|
||||
.map(|i| if (i * 5 % 13) < 8 { buy() } else { sell() })
|
||||
.collect();
|
||||
for v in p.batch(&trades).into_iter().flatten() {
|
||||
assert!((0.0..=1.0).contains(&v));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut p = Pin::new(4).unwrap();
|
||||
p.batch(&[buy(), buy(), sell(), buy()]);
|
||||
assert!(p.is_ready());
|
||||
p.reset();
|
||||
assert!(!p.is_ready());
|
||||
assert_eq!(p.value(), None);
|
||||
assert_eq!(p.update(buy()), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_equals_streaming() {
|
||||
let trades: Vec<Trade> = (0..120)
|
||||
.map(|i| if i % 3 == 0 { sell() } else { buy() })
|
||||
.collect();
|
||||
let batch = Pin::new(16).unwrap().batch(&trades);
|
||||
let mut b = Pin::new(16).unwrap();
|
||||
let streamed: Vec<_> = trades.iter().map(|x| b.update(*x)).collect();
|
||||
assert_eq!(batch, streamed);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,224 @@
|
||||
#![allow(clippy::doc_markdown)]
|
||||
|
||||
//! Tower Top / Tower Bottom — a tall bar, a pause, then a tall opposite bar.
|
||||
//!
|
||||
//! A Tower is a reversal where a strong directional bar is followed by a small
|
||||
//! "pause" bar and then a strong bar in the *opposite* direction, like two towers
|
||||
//! flanking a low wall. This is the compact three-bar form of the classic
|
||||
//! multi-bar Tower pattern.
|
||||
//!
|
||||
//! - **Tower Bottom** (`+1.0`): a tall **bearish** bar, a small-bodied bar, then a
|
||||
//! tall **bullish** bar.
|
||||
//! - **Tower Top** (`-1.0`): a tall **bullish** bar, a small-bodied bar, then a
|
||||
//! tall **bearish** bar.
|
||||
//! - Otherwise the output is `0.0`.
|
||||
//!
|
||||
//! "Tall" = body `>= 0.5 * range`; "small" = body `<= 0.3 * range`. The three-bar
|
||||
//! lookback means the first value lands on the third candle.
|
||||
|
||||
use crate::ohlcv::Candle;
|
||||
use crate::traits::Indicator;
|
||||
|
||||
fn body_fraction(candle: Candle) -> f64 {
|
||||
let range = candle.high - candle.low;
|
||||
if range > 0.0 {
|
||||
(candle.close - candle.open).abs() / range
|
||||
} else {
|
||||
0.0
|
||||
}
|
||||
}
|
||||
|
||||
fn is_tall(candle: Candle) -> bool {
|
||||
body_fraction(candle) >= 0.5
|
||||
}
|
||||
|
||||
fn is_small(candle: Candle) -> bool {
|
||||
body_fraction(candle) <= 0.3
|
||||
}
|
||||
|
||||
/// Tower Top / Bottom — three-bar reversal detector.
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct TowerTopBottom {
|
||||
c1: Option<Candle>,
|
||||
c2: Option<Candle>,
|
||||
last_value: Option<f64>,
|
||||
}
|
||||
|
||||
impl TowerTopBottom {
|
||||
/// Construct a new `TowerTopBottom`.
|
||||
#[must_use]
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Latest emitted signal if available.
|
||||
pub const fn value(&self) -> Option<f64> {
|
||||
self.last_value
|
||||
}
|
||||
}
|
||||
|
||||
impl Indicator for TowerTopBottom {
|
||||
type Input = Candle;
|
||||
type Output = f64;
|
||||
|
||||
fn update(&mut self, candle: Candle) -> Option<f64> {
|
||||
let (Some(first), Some(middle)) = (self.c1, self.c2) else {
|
||||
self.c1 = self.c2;
|
||||
self.c2 = Some(candle);
|
||||
self.last_value = Some(0.0);
|
||||
return Some(0.0);
|
||||
};
|
||||
let pause = is_small(middle);
|
||||
let first_tall = is_tall(first);
|
||||
let last_tall = is_tall(candle);
|
||||
let v = if pause && first_tall && last_tall {
|
||||
let first_up = first.close > first.open;
|
||||
let last_up = candle.close > candle.open;
|
||||
if !first_up && last_up {
|
||||
1.0
|
||||
} else if first_up && !last_up {
|
||||
-1.0
|
||||
} else {
|
||||
0.0
|
||||
}
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
self.c1 = self.c2;
|
||||
self.c2 = Some(candle);
|
||||
self.last_value = Some(v);
|
||||
Some(v)
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.c1 = None;
|
||||
self.c2 = None;
|
||||
self.last_value = None;
|
||||
}
|
||||
|
||||
fn warmup_period(&self) -> usize {
|
||||
3
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.last_value.is_some()
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"TowerTopBottom"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::traits::BatchExt;
|
||||
|
||||
/// A tall candle from `open` to `close` (body fills most of the range).
|
||||
fn tall(open: f64, close: f64) -> Candle {
|
||||
Candle::new_unchecked(
|
||||
open,
|
||||
open.max(close) + 0.1,
|
||||
open.min(close) - 0.1,
|
||||
close,
|
||||
0.0,
|
||||
0,
|
||||
)
|
||||
}
|
||||
|
||||
/// A small-bodied candle (long shadows, tiny body).
|
||||
fn small(mid: f64) -> Candle {
|
||||
Candle::new_unchecked(mid, mid + 2.0, mid - 2.0, mid + 0.1, 0.0, 0)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accessors_and_metadata() {
|
||||
let t = TowerTopBottom::new();
|
||||
assert_eq!(t.warmup_period(), 3);
|
||||
assert_eq!(t.name(), "TowerTopBottom");
|
||||
assert!(!t.is_ready());
|
||||
assert_eq!(t.value(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn first_two_bars_seed_without_signal() {
|
||||
let mut t = TowerTopBottom::new();
|
||||
assert_eq!(t.update(tall(100.0, 110.0)), Some(0.0));
|
||||
assert_eq!(t.update(small(105.0)), Some(0.0));
|
||||
assert!(t.update(tall(110.0, 100.0)).is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tower_top() {
|
||||
// tall bullish, small pause, tall bearish -> top -> -1.
|
||||
let mut t = TowerTopBottom::new();
|
||||
t.update(tall(100.0, 110.0));
|
||||
t.update(small(110.0));
|
||||
assert_eq!(t.update(tall(110.0, 100.0)), Some(-1.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tower_bottom() {
|
||||
let mut t = TowerTopBottom::new();
|
||||
t.update(tall(110.0, 100.0));
|
||||
t.update(small(100.0));
|
||||
assert_eq!(t.update(tall(100.0, 110.0)), Some(1.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn same_direction_is_zero() {
|
||||
let mut t = TowerTopBottom::new();
|
||||
t.update(tall(100.0, 110.0));
|
||||
t.update(small(110.0));
|
||||
// last bar also bullish -> not a tower -> 0.
|
||||
assert_eq!(t.update(tall(110.0, 120.0)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_pause_is_zero() {
|
||||
let mut t = TowerTopBottom::new();
|
||||
t.update(tall(100.0, 110.0));
|
||||
t.update(tall(110.0, 120.0)); // middle is tall, not a pause
|
||||
assert_eq!(t.update(tall(120.0, 110.0)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut t = TowerTopBottom::new();
|
||||
t.update(tall(100.0, 110.0));
|
||||
t.update(small(110.0));
|
||||
t.update(tall(110.0, 100.0));
|
||||
assert!(t.is_ready());
|
||||
t.reset();
|
||||
assert!(!t.is_ready());
|
||||
assert_eq!(t.update(tall(100.0, 110.0)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zero_range_bar_has_zero_body_fraction() {
|
||||
// A flat bar (high == low) exercises the zero-range body-fraction branch;
|
||||
// it counts as a small "pause" bar, so tall-flat-tall still reverses.
|
||||
fn flat(mid: f64) -> Candle {
|
||||
Candle::new_unchecked(mid, mid, mid, mid, 0.0, 0)
|
||||
}
|
||||
let mut t = TowerTopBottom::new();
|
||||
t.update(tall(100.0, 110.0));
|
||||
t.update(flat(110.0));
|
||||
assert_eq!(t.update(tall(110.0, 100.0)), Some(-1.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_equals_streaming() {
|
||||
let candles: Vec<Candle> = (0..30)
|
||||
.map(|i| match i % 3 {
|
||||
0 => tall(100.0, 110.0),
|
||||
1 => small(110.0),
|
||||
_ => tall(110.0, 100.0),
|
||||
})
|
||||
.collect();
|
||||
let batch = TowerTopBottom::new().batch(&candles);
|
||||
let mut b = TowerTopBottom::new();
|
||||
let streamed: Vec<_> = candles.iter().map(|x| b.update(*x)).collect();
|
||||
assert_eq!(batch, streamed);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,217 @@
|
||||
//! Trade-Sign Autocorrelation — lag-1 persistence of the trade-aggressor side.
|
||||
|
||||
use std::collections::VecDeque;
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
use crate::microstructure::Trade;
|
||||
use crate::traits::Indicator;
|
||||
|
||||
/// Trade-Sign Autocorrelation — the lag-1 autocorrelation of the **trade sign**
|
||||
/// (`+1` buy, `−1` sell), measuring how strongly signed order flow persists.
|
||||
///
|
||||
/// ```text
|
||||
/// s_t = +1 if the trade is a buy, −1 if a sell
|
||||
/// ρ1 = mean over the window of ( s_t · s_{t−1} ) ∈ [−1, +1]
|
||||
/// ```
|
||||
///
|
||||
/// In real markets trade signs are strongly **positively** autocorrelated: a buy
|
||||
/// tends to be followed by another buy (and a sell by a sell), because large
|
||||
/// parent orders are split into many child trades and because of order-splitting
|
||||
/// and herding. A high reading therefore indicates persistent directional pressure
|
||||
/// — a footprint of informed or algorithmic execution — while a reading near zero
|
||||
/// signals balanced, uninformed flow and a negative reading signals alternating
|
||||
/// (bid-ask bounce) flow.
|
||||
///
|
||||
/// The output is the mean product of consecutive signs, bounded in `[−1, +1]`. The
|
||||
/// first value lands after `period` trades. Each `update` is O(`period`).
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use wickra_core::{Indicator, Side, Trade, TradeSignAutocorrelation};
|
||||
///
|
||||
/// let mut indicator = TradeSignAutocorrelation::new(20).unwrap();
|
||||
/// let mut last = None;
|
||||
/// for i in 0..40 {
|
||||
/// let side = if i % 2 == 0 { Side::Buy } else { Side::Sell };
|
||||
/// last = indicator.update(Trade::new(100.0, 1.0, side, i).unwrap());
|
||||
/// }
|
||||
/// // Perfectly alternating signs -> autocorrelation -1.
|
||||
/// assert!((last.unwrap() + 1.0).abs() < 1e-9);
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TradeSignAutocorrelation {
|
||||
period: usize,
|
||||
signs: VecDeque<f64>,
|
||||
last: Option<f64>,
|
||||
}
|
||||
|
||||
impl TradeSignAutocorrelation {
|
||||
/// Construct a trade-sign autocorrelation over `period` trades.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`Error::InvalidPeriod`] if `period < 2` (a lag-1 product needs two
|
||||
/// trades).
|
||||
pub fn new(period: usize) -> Result<Self> {
|
||||
if period < 2 {
|
||||
return Err(Error::InvalidPeriod {
|
||||
message: "trade-sign autocorrelation needs period >= 2",
|
||||
});
|
||||
}
|
||||
Ok(Self {
|
||||
period,
|
||||
signs: VecDeque::with_capacity(period),
|
||||
last: None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Configured window of trades.
|
||||
pub const fn period(&self) -> usize {
|
||||
self.period
|
||||
}
|
||||
|
||||
/// Current value if available.
|
||||
pub const fn value(&self) -> Option<f64> {
|
||||
self.last
|
||||
}
|
||||
}
|
||||
|
||||
impl Indicator for TradeSignAutocorrelation {
|
||||
type Input = Trade;
|
||||
type Output = f64;
|
||||
|
||||
fn update(&mut self, trade: Trade) -> Option<f64> {
|
||||
if self.signs.len() == self.period {
|
||||
self.signs.pop_front();
|
||||
}
|
||||
self.signs.push_back(trade.side.sign());
|
||||
if self.signs.len() < self.period {
|
||||
return None;
|
||||
}
|
||||
let mut product_sum = 0.0;
|
||||
let mut prev: Option<f64> = None;
|
||||
for &s in &self.signs {
|
||||
if let Some(p) = prev {
|
||||
product_sum += s * p;
|
||||
}
|
||||
prev = Some(s);
|
||||
}
|
||||
let rho = product_sum / (self.period as f64 - 1.0);
|
||||
self.last = Some(rho);
|
||||
Some(rho)
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.signs.clear();
|
||||
self.last = None;
|
||||
}
|
||||
|
||||
fn warmup_period(&self) -> usize {
|
||||
self.period
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.last.is_some()
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"TradeSignAutocorrelation"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::microstructure::Side;
|
||||
use crate::traits::BatchExt;
|
||||
use approx::assert_relative_eq;
|
||||
|
||||
fn buy() -> Trade {
|
||||
Trade::new_unchecked(100.0, 1.0, Side::Buy, 0)
|
||||
}
|
||||
|
||||
fn sell() -> Trade {
|
||||
Trade::new_unchecked(100.0, 1.0, Side::Sell, 0)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_period_below_two() {
|
||||
assert!(matches!(
|
||||
TradeSignAutocorrelation::new(1),
|
||||
Err(Error::InvalidPeriod { .. })
|
||||
));
|
||||
assert!(TradeSignAutocorrelation::new(2).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accessors_and_metadata() {
|
||||
let t = TradeSignAutocorrelation::new(20).unwrap();
|
||||
assert_eq!(t.period(), 20);
|
||||
assert_eq!(t.warmup_period(), 20);
|
||||
assert_eq!(t.name(), "TradeSignAutocorrelation");
|
||||
assert!(!t.is_ready());
|
||||
assert_eq!(t.value(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn first_emission_at_warmup_period() {
|
||||
let mut t = TradeSignAutocorrelation::new(4).unwrap();
|
||||
let out = t.batch(&[buy(), buy(), buy(), buy(), buy()]);
|
||||
for v in out.iter().take(3) {
|
||||
assert!(v.is_none());
|
||||
}
|
||||
assert!(out[3].is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn persistent_flow_is_one() {
|
||||
let mut t = TradeSignAutocorrelation::new(10).unwrap();
|
||||
let trades: Vec<Trade> = (0..20).map(|_| buy()).collect();
|
||||
let last = t.batch(&trades).into_iter().flatten().last().unwrap();
|
||||
assert_relative_eq!(last, 1.0, epsilon = 1e-12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn alternating_flow_is_minus_one() {
|
||||
let mut t = TradeSignAutocorrelation::new(10).unwrap();
|
||||
let trades: Vec<Trade> = (0..20)
|
||||
.map(|i| if i % 2 == 0 { buy() } else { sell() })
|
||||
.collect();
|
||||
let last = t.batch(&trades).into_iter().flatten().last().unwrap();
|
||||
assert_relative_eq!(last, -1.0, epsilon = 1e-12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn output_in_range() {
|
||||
let mut t = TradeSignAutocorrelation::new(16).unwrap();
|
||||
let trades: Vec<Trade> = (0..200)
|
||||
.map(|i| if (i * 7 % 13) < 6 { buy() } else { sell() })
|
||||
.collect();
|
||||
for v in t.batch(&trades).into_iter().flatten() {
|
||||
assert!((-1.0..=1.0).contains(&v));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut t = TradeSignAutocorrelation::new(4).unwrap();
|
||||
t.batch(&[buy(), buy(), buy(), buy()]);
|
||||
assert!(t.is_ready());
|
||||
t.reset();
|
||||
assert!(!t.is_ready());
|
||||
assert_eq!(t.value(), None);
|
||||
assert_eq!(t.update(buy()), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_equals_streaming() {
|
||||
let trades: Vec<Trade> = (0..120)
|
||||
.map(|i| if i % 3 == 0 { sell() } else { buy() })
|
||||
.collect();
|
||||
let batch = TradeSignAutocorrelation::new(16).unwrap().batch(&trades);
|
||||
let mut b = TradeSignAutocorrelation::new(16).unwrap();
|
||||
let streamed: Vec<_> = trades.iter().map(|x| b.update(*x)).collect();
|
||||
assert_eq!(batch, streamed);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
#![allow(clippy::doc_markdown)]
|
||||
|
||||
//! Tristar — a three-doji reversal pattern.
|
||||
//!
|
||||
//! A Tristar is three consecutive Doji candles where the middle one gaps away
|
||||
//! from its neighbours, forming a star. A bearish Tristar (top) has the middle
|
||||
//! doji sitting above the other two; a bullish Tristar (bottom) has it below.
|
||||
//!
|
||||
//! - **Bullish** (`+1.0`): three dojis, the middle doji's body centre below both
|
||||
//! neighbours' body centres.
|
||||
//! - **Bearish** (`-1.0`): three dojis, the middle above both neighbours.
|
||||
//! - Otherwise the output is `0.0`.
|
||||
//!
|
||||
//! A doji is a candle whose body is `<= 0.1 * range`. The three-bar lookback means
|
||||
//! the first value lands on the third candle.
|
||||
|
||||
use crate::ohlcv::Candle;
|
||||
use crate::traits::Indicator;
|
||||
|
||||
/// Body-centre of a candle.
|
||||
fn body_mid(candle: Candle) -> f64 {
|
||||
f64::midpoint(candle.open, candle.close)
|
||||
}
|
||||
|
||||
/// Whether a candle is a doji (body small relative to range).
|
||||
fn is_doji(candle: Candle) -> bool {
|
||||
let body = (candle.close - candle.open).abs();
|
||||
let range = candle.high - candle.low;
|
||||
range > 0.0 && body <= 0.1 * range
|
||||
}
|
||||
|
||||
/// Tristar — three-doji star reversal detector.
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct Tristar {
|
||||
c1: Option<Candle>,
|
||||
c2: Option<Candle>,
|
||||
last_value: Option<f64>,
|
||||
}
|
||||
|
||||
impl Tristar {
|
||||
/// Construct a new `Tristar`.
|
||||
#[must_use]
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Latest emitted signal if available.
|
||||
pub const fn value(&self) -> Option<f64> {
|
||||
self.last_value
|
||||
}
|
||||
}
|
||||
|
||||
impl Indicator for Tristar {
|
||||
type Input = Candle;
|
||||
type Output = f64;
|
||||
|
||||
fn update(&mut self, candle: Candle) -> Option<f64> {
|
||||
let (Some(first), Some(middle)) = (self.c1, self.c2) else {
|
||||
self.c1 = self.c2;
|
||||
self.c2 = Some(candle);
|
||||
self.last_value = Some(0.0);
|
||||
return Some(0.0);
|
||||
};
|
||||
let v = if is_doji(first) && is_doji(middle) && is_doji(candle) {
|
||||
let mid = body_mid(middle);
|
||||
let n1 = body_mid(first);
|
||||
let n3 = body_mid(candle);
|
||||
if mid > n1 && mid > n3 {
|
||||
-1.0
|
||||
} else if mid < n1 && mid < n3 {
|
||||
1.0
|
||||
} else {
|
||||
0.0
|
||||
}
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
self.c1 = self.c2;
|
||||
self.c2 = Some(candle);
|
||||
self.last_value = Some(v);
|
||||
Some(v)
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.c1 = None;
|
||||
self.c2 = None;
|
||||
self.last_value = None;
|
||||
}
|
||||
|
||||
fn warmup_period(&self) -> usize {
|
||||
3
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.last_value.is_some()
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"Tristar"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::traits::BatchExt;
|
||||
|
||||
/// A doji centred at `mid` (tiny body, symmetric shadows).
|
||||
fn doji(mid: f64) -> Candle {
|
||||
Candle::new_unchecked(mid, mid + 1.0, mid - 1.0, mid + 0.02, 0.0, 0)
|
||||
}
|
||||
|
||||
/// A non-doji (big body).
|
||||
fn solid(open: f64, close: f64) -> Candle {
|
||||
Candle::new_unchecked(
|
||||
open,
|
||||
open.max(close) + 0.1,
|
||||
open.min(close) - 0.1,
|
||||
close,
|
||||
0.0,
|
||||
0,
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accessors_and_metadata() {
|
||||
let t = Tristar::new();
|
||||
assert_eq!(t.warmup_period(), 3);
|
||||
assert_eq!(t.name(), "Tristar");
|
||||
assert!(!t.is_ready());
|
||||
assert_eq!(t.value(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn first_two_bars_seed_without_signal() {
|
||||
let mut t = Tristar::new();
|
||||
assert_eq!(t.update(doji(100.0)), Some(0.0));
|
||||
assert_eq!(t.update(doji(100.0)), Some(0.0));
|
||||
assert!(t.update(doji(100.0)).is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bearish_tristar_top() {
|
||||
// middle doji centred above the two neighbours -> top -> -1.
|
||||
let mut t = Tristar::new();
|
||||
t.update(doji(100.0));
|
||||
t.update(doji(105.0)); // middle, highest
|
||||
assert_eq!(t.update(doji(100.0)), Some(-1.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bullish_tristar_bottom() {
|
||||
let mut t = Tristar::new();
|
||||
t.update(doji(100.0));
|
||||
t.update(doji(95.0)); // middle, lowest
|
||||
assert_eq!(t.update(doji(100.0)), Some(1.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn non_doji_is_zero() {
|
||||
let mut t = Tristar::new();
|
||||
t.update(doji(100.0));
|
||||
t.update(solid(100.0, 110.0)); // not a doji
|
||||
assert_eq!(t.update(doji(100.0)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut t = Tristar::new();
|
||||
t.update(doji(100.0));
|
||||
t.update(doji(105.0));
|
||||
t.update(doji(100.0));
|
||||
assert!(t.is_ready());
|
||||
t.reset();
|
||||
assert!(!t.is_ready());
|
||||
assert_eq!(t.update(doji(100.0)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_equals_streaming() {
|
||||
let candles: Vec<Candle> = (0..40)
|
||||
.map(|i| doji(100.0 + (f64::from(i) * 0.4).sin() * 5.0))
|
||||
.collect();
|
||||
let batch = Tristar::new().batch(&candles);
|
||||
let mut b = Tristar::new();
|
||||
let streamed: Vec<_> = candles.iter().map(|x| b.update(*x)).collect();
|
||||
assert_eq!(batch, streamed);
|
||||
}
|
||||
}
|
||||
@@ -78,24 +78,26 @@ pub use indicators::{
|
||||
DecyclerOscillator, Dema, DemandIndex, DemarkPivots, DemarkPivotsOutput, DepthSlope,
|
||||
DerivativeOscillator, DetrendedStdDev, DisparityIndex, DistanceSsd, Doji, DojiStar, Donchian,
|
||||
DonchianOutput, DonchianStop, DonchianStopOutput, DoubleBollinger, DoubleBollingerOutput,
|
||||
DoubleTopBottom, DownsideGapThreeMethods, Dpo, DragonflyDoji, DrawdownDuration, Dx,
|
||||
DynamicMomentumIndex, EaseOfMovement, EffectiveSpread, EhlersStochastic, Ehma, ElderImpulse,
|
||||
ElderRay, ElderRayOutput, ElderSafeZone, ElderSafeZoneOutput, Ema, EmpiricalModeDecomposition,
|
||||
Engulfing, Equivolume, EquivolumeOutput, EvenBetterSinewave, EveningDojiStar, Evwma,
|
||||
EwmaVolatility, Expectancy, FallingThreeMethods, Fama, FibArcs, FibArcsOutput, FibChannel,
|
||||
FibChannelOutput, FibConfluence, FibConfluenceOutput, FibExtension, FibExtensionOutput, FibFan,
|
||||
FibFanOutput, FibProjection, FibProjectionOutput, FibRetracement, FibRetracementOutput,
|
||||
FibTimeZones, FibTimeZonesOutput, FibonacciPivots, FibonacciPivotsOutput, FisherRsi,
|
||||
FisherTransform, FlagPennant, Footprint, FootprintOutput, ForceIndex, FractalChaosBands,
|
||||
FractalChaosBandsOutput, Frama, FundingBasis, FundingRate, FundingRateMean, FundingRateZScore,
|
||||
GainLossRatio, GapSideBySideWhite, Garch11, GarmanKlassVolatility, Gartley, GatorOscillator,
|
||||
DoubleTopBottom, DownsideGapThreeMethods, Dpo, DragonflyDoji, DrawdownDuration, DumplingTop,
|
||||
Dx, DynamicMomentumIndex, EaseOfMovement, EffectiveSpread, EhlersStochastic, Ehma,
|
||||
ElderImpulse, ElderRay, ElderRayOutput, ElderSafeZone, ElderSafeZoneOutput, Ema,
|
||||
EmpiricalModeDecomposition, Engulfing, Equivolume, EquivolumeOutput, EstimatedLeverageRatio,
|
||||
EvenBetterSinewave, EveningDojiStar, Evwma, EwmaVolatility, Expectancy, FallingThreeMethods,
|
||||
Fama, FibArcs, FibArcsOutput, FibChannel, FibChannelOutput, FibConfluence, FibConfluenceOutput,
|
||||
FibExtension, FibExtensionOutput, FibFan, FibFanOutput, FibProjection, FibProjectionOutput,
|
||||
FibRetracement, FibRetracementOutput, FibTimeZones, FibTimeZonesOutput, FibonacciPivots,
|
||||
FibonacciPivotsOutput, FisherRsi, FisherTransform, FlagPennant, Footprint, FootprintOutput,
|
||||
ForceIndex, FractalChaosBands, FractalChaosBandsOutput, Frama, FryPanBottom, FundingBasis,
|
||||
FundingImpliedApr, FundingRate, FundingRateMean, FundingRateZScore, GainLossRatio,
|
||||
GapSideBySideWhite, Garch11, GarmanKlassVolatility, Gartley, GatorOscillator,
|
||||
GatorOscillatorOutput, GeneralizedDema, GeometricMa, GoldenPocket, GoldenPocketOutput,
|
||||
GrangerCausality, GravestoneDoji, Hammer, HangingMan, Harami, HeadAndShoulders, HeikinAshi,
|
||||
HeikinAshiOscillator, HeikinAshiOutput, HiLoActivator, HighLowIndex, HighLowRange, HighWave,
|
||||
HighpassFilter, Hikkake, HikkakeModified, HilbertDominantCycle, HistoricalVolatility, Hma,
|
||||
HoltWinters, HomingPigeon, HtDcPhase, HtPhasor, HtPhasorOutput, HtTrendMode, HurstChannel,
|
||||
HurstChannelOutput, HurstExponent, Ichimoku, IchimokuOutput, IdenticalThreeCrows, InNeck,
|
||||
Inertia, InformationRatio, InitialBalance, InitialBalanceOutput, InstantaneousTrendline,
|
||||
GrangerCausality, GravestoneDoji, Hammer, HangingMan, Harami, HaramiCross,
|
||||
HasbrouckInformationShare, HeadAndShoulders, HeikinAshi, HeikinAshiOscillator,
|
||||
HeikinAshiOutput, HiLoActivator, HighLowIndex, HighLowRange, HighWave, HighpassFilter, Hikkake,
|
||||
HikkakeModified, HilbertDominantCycle, HistoricalVolatility, Hma, HoltWinters, HomingPigeon,
|
||||
HtDcPhase, HtPhasor, HtPhasorOutput, HtTrendMode, HurstChannel, HurstChannelOutput,
|
||||
HurstExponent, Ichimoku, IchimokuOutput, IdenticalThreeCrows, InNeck, Inertia,
|
||||
InformationRatio, InitialBalance, InitialBalanceOutput, InstantaneousTrendline,
|
||||
IntradayIntensity, IntradayMomentumIndex, IntradayVolatilityProfile,
|
||||
IntradayVolatilityProfileOutput, InverseFisherTransform, InvertedHammer, JarqueBera, Jma,
|
||||
JumpIndicator, KagiBars, KalmanHedgeRatio, KalmanHedgeRatioOutput, Kama, KaseDevStop,
|
||||
@@ -110,12 +112,13 @@ pub use indicators::{
|
||||
McGinleyDynamic, MedianAbsoluteDeviation, MedianChannel, MedianChannelOutput, MedianMa,
|
||||
MedianPrice, Mfi, Microprice, MidPoint, MidPrice, MinusDi, MinusDm, ModifiedMaStop,
|
||||
ModifiedMaStopOutput, Mom, MorningDojiStar, MorningEveningStar, MurreyMathLines,
|
||||
MurreyMathLinesOutput, Natr, NewHighsNewLows, Nrtr, NrtrOutput, Nvi, OIPriceDivergence,
|
||||
OIWeighted, Obv, OmegaRatio, OnNeck, OpenInterestDelta, OpeningMarubozu, OpeningRange,
|
||||
OpeningRangeOutput, OrderBookImbalanceFull, OrderBookImbalanceTop1, OrderBookImbalanceTopN,
|
||||
OrderFlowImbalance, OuHalfLife, OvernightGap, OvernightIntradayReturn,
|
||||
OvernightIntradayReturnOutput, PainIndex, PairSpreadZScore, PairwiseBeta, ParkinsonVolatility,
|
||||
PearsonCorrelation, PercentAboveMa, PercentB, PercentageTrailingStop, Pgo, PiercingDarkCloud,
|
||||
MurreyMathLinesOutput, Natr, NewHighsNewLows, NewPriceLines, Nrtr, NrtrOutput, Nvi,
|
||||
OIPriceDivergence, OIWeighted, Obv, OiToVolumeRatio, OmegaRatio, OnNeck, OpenInterestDelta,
|
||||
OpenInterestMomentum, OpeningMarubozu, OpeningRange, OpeningRangeOutput,
|
||||
OrderBookImbalanceFull, OrderBookImbalanceTop1, OrderBookImbalanceTopN, OrderFlowImbalance,
|
||||
OuHalfLife, OvernightGap, OvernightIntradayReturn, OvernightIntradayReturnOutput, PainIndex,
|
||||
PairSpreadZScore, PairwiseBeta, ParkinsonVolatility, PearsonCorrelation, PercentAboveMa,
|
||||
PercentB, PercentageTrailingStop, PerpetualPremiumIndex, Pgo, PiercingDarkCloud, Pin,
|
||||
PivotReversal, PlusDi, PlusDm, Pmo, PointAndFigureBars, PolarizedFractalEfficiency, Ppo,
|
||||
PpoHistogram, ProfitFactor, ProjectionBands, ProjectionBandsOutput, ProjectionOscillator, Psar,
|
||||
Pvi, Qqe, QqeOutput, Qstick, QuartileBands, QuartileBandsOutput, QuotedSpread, RSquared,
|
||||
@@ -139,16 +142,17 @@ pub use indicators::{
|
||||
TdSequentialOutput, TdSetup, TdTrap, Tema, TermStructureBasis, ThreeDrives, ThreeInside,
|
||||
ThreeLineBreak, ThreeLineStrike, ThreeOutside, ThreeSoldiersOrCrows, ThreeStarsInSouth,
|
||||
Thrusting, TickIndex, Tii, TimeBasedStop, TimeOfDayReturnProfile, TimeOfDayReturnProfileOutput,
|
||||
TpoProfile, TpoProfileOutput, TradeImbalance, TradeVolumeIndex, TrendLabel, TrendStrengthIndex,
|
||||
Trendflex, TreynorRatio, Triangle, Trima, Trin, TripleTopBottom, Trix, TrueRange, Tsf,
|
||||
TsfOscillator, Tsi, Tsv, TtmSqueeze, TtmSqueezeOutput, TtmTrend, TurnOfMonth, Tweezer,
|
||||
TwiggsMoneyFlow, TwoCrows, TypicalPrice, UlcerIndex, UltimateOscillator, UniqueThreeRiver,
|
||||
UniversalOscillator, UpDownVolumeRatio, UpsideGapThreeMethods, UpsideGapTwoCrows, ValueArea,
|
||||
ValueAreaOutput, ValueAtRisk, Variance, VarianceRatio, VerticalHorizontalFilter, Vidya,
|
||||
VolatilityCone, VolatilityConeOutput, VolatilityOfVolatility, VolatilityRatio, VoltyStop,
|
||||
VolumeByTimeProfile, VolumeByTimeProfileOutput, VolumeOscillator, VolumePriceTrend,
|
||||
VolumeProfile, VolumeProfileOutput, VolumeRsi, VolumeWeightedMacd, VolumeWeightedMacdOutput,
|
||||
VolumeWeightedSr, VolumeWeightedSrOutput, Vortex, VortexOutput, Vpin, Vwap, VwapStdDevBands,
|
||||
TowerTopBottom, TpoProfile, TpoProfileOutput, TradeImbalance, TradeSignAutocorrelation,
|
||||
TradeVolumeIndex, TrendLabel, TrendStrengthIndex, Trendflex, TreynorRatio, Triangle, Trima,
|
||||
Trin, TripleTopBottom, Tristar, Trix, TrueRange, Tsf, TsfOscillator, Tsi, Tsv, TtmSqueeze,
|
||||
TtmSqueezeOutput, TtmTrend, TurnOfMonth, Tweezer, TwiggsMoneyFlow, TwoCrows, TypicalPrice,
|
||||
UlcerIndex, UltimateOscillator, UniqueThreeRiver, UniversalOscillator, UpDownVolumeRatio,
|
||||
UpsideGapThreeMethods, UpsideGapTwoCrows, ValueArea, ValueAreaOutput, ValueAtRisk, Variance,
|
||||
VarianceRatio, VerticalHorizontalFilter, Vidya, VolatilityCone, VolatilityConeOutput,
|
||||
VolatilityOfVolatility, VolatilityRatio, VoltyStop, VolumeByTimeProfile,
|
||||
VolumeByTimeProfileOutput, VolumeOscillator, VolumePriceTrend, VolumeProfile,
|
||||
VolumeProfileOutput, VolumeRsi, VolumeWeightedMacd, VolumeWeightedMacdOutput, VolumeWeightedSr,
|
||||
VolumeWeightedSrOutput, Vortex, VortexOutput, Vpin, Vwap, VwapStdDevBands,
|
||||
VwapStdDevBandsOutput, Vwma, Vzo, Wad, WavePm, WaveTrend, WaveTrendOutput, Wedge,
|
||||
WeightedClose, WickRatio, WilliamsFractals, WilliamsFractalsOutput, WilliamsR, WinRate, Wma,
|
||||
WoodiePivots, WoodiePivotsOutput, YangZhangVolatility, YoyoExit, ZScore, ZeroLagMacd,
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@ That includes:
|
||||
[Python](https://docs.wickra.org/Quickstart-Python),
|
||||
[Node](https://docs.wickra.org/Quickstart-Node), and
|
||||
[WASM](https://docs.wickra.org/Quickstart-WASM).
|
||||
- A per-indicator deep dive for every one of the **479 indicators** across
|
||||
- A per-indicator deep dive for every one of the **493 indicators** across
|
||||
the sixteen families (Moving Averages, Momentum Oscillators, Trend &
|
||||
Directional, Price Oscillators, Volatility & Bands, Bands & Channels,
|
||||
Trailing Stops, Volume, Price Statistics, Ehlers / Cycle DSP, Pivots &
|
||||
|
||||
Generated
+7
-7
@@ -17,7 +17,7 @@
|
||||
},
|
||||
"../../bindings/node": {
|
||||
"name": "wickra",
|
||||
"version": "0.6.8",
|
||||
"version": "0.7.1",
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"devDependencies": {
|
||||
"@napi-rs/cli": "^2.18.0"
|
||||
@@ -26,12 +26,12 @@
|
||||
"node": ">= 18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"wickra-darwin-arm64": "0.6.8",
|
||||
"wickra-darwin-x64": "0.6.8",
|
||||
"wickra-linux-arm64-gnu": "0.6.8",
|
||||
"wickra-linux-x64-gnu": "0.6.8",
|
||||
"wickra-win32-arm64-msvc": "0.6.8",
|
||||
"wickra-win32-x64-msvc": "0.6.8"
|
||||
"wickra-darwin-arm64": "0.7.1",
|
||||
"wickra-darwin-x64": "0.7.1",
|
||||
"wickra-linux-arm64-gnu": "0.7.1",
|
||||
"wickra-linux-x64-gnu": "0.7.1",
|
||||
"wickra-win32-arm64-msvc": "0.7.1",
|
||||
"wickra-win32-x64-msvc": "0.7.1"
|
||||
}
|
||||
},
|
||||
"node_modules/wickra": {
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
//! WeightedClose.
|
||||
|
||||
use libfuzzer_sys::fuzz_target;
|
||||
use wickra_core::{AbandonedBaby, Abcd, AccelerationBands, AcceleratorOscillator, AdOscillator, AdaptiveCci, Adl, AdvanceBlock, Adx, Adxr, Alligator, AnchoredVwap, AndrewsPitchfork, Aroon, AroonOscillator, Atr, AtrBands, AtrRatchet, AtrTrailingStop, AutoFib, AverageDailyRange, AwesomeOscillator, AwesomeOscillatorHistogram, BalanceOfPower, Bat, BatchExt, BeltHold, BetterVolume, BodySizePct, Breakaway, Butterfly, Camarilla, Candle, CandleVolume, Cci, CentralPivotRange, ChaikinMoneyFlow, ChaikinOscillator, ChaikinVolatility, ChandeKrollStop, ChandelierExit, ChoppinessIndex, ClassicPivots, CloseVsOpen, ClosingMarubozu, ConcealingBabySwallow, Counterattack, Crab, CupAndHandle, Cypher, DayOfWeekProfile, DemandIndex, DemarkPivots, Doji, DojiStar, Donchian, DonchianStop, DoubleTopBottom, DownsideGapThreeMethods, DragonflyDoji, Dx, EaseOfMovement, ElderRay, ElderSafeZone, Engulfing, Equivolume, EveningDojiStar, Evwma, FallingThreeMethods, FibArcs, FibChannel, FibConfluence, FibExtension, FibFan, FibProjection, FibRetracement, FibTimeZones, FibonacciPivots, FlagPennant, ForceIndex, FractalChaosBands, GapSideBySideWhite, GarmanKlassVolatility, Gartley, GatorOscillator, GoldenPocket, GravestoneDoji, Hammer, HangingMan, Harami, HeadAndShoulders, HeikinAshi, HeikinAshiOscillator, HiLoActivator, HighLowRange, HighWave, Hikkake, HikkakeModified, HomingPigeon, HurstChannel, Ichimoku, IdenticalThreeCrows, InNeck, Indicator, Inertia, InitialBalance, IntradayIntensity, IntradayMomentumIndex, IntradayVolatilityProfile, InvertedHammer, KaseDevStop, KasePermissionStochastic, Keltner, Kicking, KickingByLength, Kvo, LadderBottom, LongLeggedDoji, LongLine, MarketFacilitationIndex, Marubozu, MassIndex, MatHold, MatchingLow, AvgPrice, MedianPrice, Mfi, MidPrice, MinusDi, MinusDm, ModifiedMaStop, MorningDojiStar, MorningEveningStar, MurreyMathLines, Natr, Nrtr, Nvi, Obv, OnNeck, OpeningMarubozu, OpeningRange, OvernightGap, OvernightIntradayReturn, ParkinsonVolatility, Pgo, PiercingDarkCloud, PivotReversal, PlusDi, PlusDm, ProjectionBands, ProjectionOscillator, Psar, Pvi, Qstick, RectangleRange, RickshawMan, RisingThreeMethods, RogersSatchellVolatility, RollingVwap, Rvi, Rwi, SarExt, SeasonalZScore, SeparatingLines, SessionHighLow, SessionRange, SessionVwap, Shark, ShootingStar, ShortLine, Smi, SmoothedHeikinAshi, SpinningTop, StalledPattern, StarcBands, StickSandwich, Stochastic, StochasticCci, SuperTrend, Takuri, TasukiGap, TdCamouflage, TdClop, TdClopwin, TdCombo, TdCountdown, TdDWave, TdDeMarker, TdDifferential, TdLines, TdMovingAverage, TdOpen, TdPressure, TdPropulsion, TdRangeProjection, TdRei, TdRiskLevel, TdSequential, TdSetup, TdTrap, ThreeDrives, ThreeInside, ThreeLineBreak, ThreeLineStrike, ThreeOutside, ThreeSoldiersOrCrows, ThreeStarsInSouth, Thrusting, TimeBasedStop, TimeOfDayReturnProfile, TpoProfile, TradeVolumeIndex, Triangle, TripleTopBottom, TrueRange, Tsv, TtmSqueeze, TtmTrend, TurnOfMonth, Tweezer, TwiggsMoneyFlow, TwoCrows, TypicalPrice, UltimateOscillator, UniqueThreeRiver, UpsideGapThreeMethods, UpsideGapTwoCrows, ValueArea, VolatilityCone, VolatilityRatio, VoltyStop, VolumeByTimeProfile, VolumeOscillator, VolumePriceTrend, VolumeProfile, VolumeRsi, VolumeWeightedMacd, VolumeWeightedSr, Vortex, Vwap, VwapStdDevBands, Vwma, Vzo, WaveTrend, Wedge, WeightedClose, WickRatio, Wad, WilliamsFractals, WilliamsR, WoodiePivots, YangZhangVolatility, YoyoExit, ZigZag};
|
||||
use wickra_core::{AbandonedBaby, Abcd, AccelerationBands, AcceleratorOscillator, AdOscillator, AdaptiveCci, Adl, AdvanceBlock, Adx, Adxr, Alligator, AnchoredVwap, AndrewsPitchfork, Aroon, AroonOscillator, Atr, AtrBands, AtrRatchet, AtrTrailingStop, AutoFib, AverageDailyRange, AwesomeOscillator, AwesomeOscillatorHistogram, BalanceOfPower, Bat, BatchExt, BeltHold, BetterVolume, BodySizePct, Breakaway, Butterfly, Camarilla, Candle, CandleVolume, Cci, CentralPivotRange, ChaikinMoneyFlow, ChaikinOscillator, ChaikinVolatility, ChandeKrollStop, ChandelierExit, ChoppinessIndex, ClassicPivots, CloseVsOpen, ClosingMarubozu, ConcealingBabySwallow, Counterattack, Crab, CupAndHandle, Cypher, DayOfWeekProfile, DemandIndex, DemarkPivots, Doji, DojiStar, Donchian, DonchianStop, DoubleTopBottom, DownsideGapThreeMethods, DragonflyDoji, DumplingTop, Dx, EaseOfMovement, ElderRay, ElderSafeZone, Engulfing, Equivolume, EveningDojiStar, Evwma, FallingThreeMethods, FibArcs, FibChannel, FibConfluence, FibExtension, FibFan, FibProjection, FibRetracement, FibTimeZones, FibonacciPivots, FlagPennant, ForceIndex, FractalChaosBands, FryPanBottom, GapSideBySideWhite, GarmanKlassVolatility, Gartley, GatorOscillator, GoldenPocket, GravestoneDoji, Hammer, HangingMan, Harami, HaramiCross, HeadAndShoulders, HeikinAshi, HeikinAshiOscillator, HiLoActivator, HighLowRange, HighWave, Hikkake, HikkakeModified, HomingPigeon, HurstChannel, Ichimoku, IdenticalThreeCrows, InNeck, Indicator, Inertia, InitialBalance, IntradayIntensity, IntradayMomentumIndex, IntradayVolatilityProfile, InvertedHammer, KaseDevStop, KasePermissionStochastic, Keltner, Kicking, KickingByLength, Kvo, LadderBottom, LongLeggedDoji, LongLine, MarketFacilitationIndex, Marubozu, MassIndex, MatHold, MatchingLow, AvgPrice, MedianPrice, Mfi, MidPrice, MinusDi, MinusDm, ModifiedMaStop, MorningDojiStar, MorningEveningStar, MurreyMathLines, Natr, NewPriceLines, Nrtr, Nvi, Obv, OnNeck, OpeningMarubozu, OpeningRange, OvernightGap, OvernightIntradayReturn, ParkinsonVolatility, Pgo, PiercingDarkCloud, PivotReversal, PlusDi, PlusDm, ProjectionBands, ProjectionOscillator, Psar, Pvi, Qstick, RectangleRange, RickshawMan, RisingThreeMethods, RogersSatchellVolatility, RollingVwap, Rvi, Rwi, SarExt, SeasonalZScore, SeparatingLines, SessionHighLow, SessionRange, SessionVwap, Shark, ShootingStar, ShortLine, Smi, SmoothedHeikinAshi, SpinningTop, StalledPattern, StarcBands, StickSandwich, Stochastic, StochasticCci, SuperTrend, Takuri, TasukiGap, TdCamouflage, TdClop, TdClopwin, TdCombo, TdCountdown, TdDWave, TdDeMarker, TdDifferential, TdLines, TdMovingAverage, TdOpen, TdPressure, TdPropulsion, TdRangeProjection, TdRei, TdRiskLevel, TdSequential, TdSetup, TdTrap, ThreeDrives, ThreeInside, ThreeLineBreak, ThreeLineStrike, ThreeOutside, ThreeSoldiersOrCrows, ThreeStarsInSouth, Thrusting, TimeBasedStop, TimeOfDayReturnProfile, TowerTopBottom, TpoProfile, TradeVolumeIndex, Triangle, TripleTopBottom, Tristar, TrueRange, Tsv, TtmSqueeze, TtmTrend, TurnOfMonth, Tweezer, TwiggsMoneyFlow, TwoCrows, TypicalPrice, UltimateOscillator, UniqueThreeRiver, UpsideGapThreeMethods, UpsideGapTwoCrows, ValueArea, VolatilityCone, VolatilityRatio, VoltyStop, VolumeByTimeProfile, VolumeOscillator, VolumePriceTrend, VolumeProfile, VolumeRsi, VolumeWeightedMacd, VolumeWeightedSr, Vortex, Vwap, VwapStdDevBands, Vwma, Vzo, WaveTrend, Wedge, WeightedClose, WickRatio, Wad, WilliamsFractals, WilliamsR, WoodiePivots, YangZhangVolatility, YoyoExit, ZigZag};
|
||||
|
||||
/// Convert a flat `f64` stream into a `Vec<Candle>` by chunking it into
|
||||
/// `[open, high, low, close, volume]` groups. Tuples that fail OHLCV
|
||||
@@ -315,6 +315,12 @@ fuzz_target!(|data: Vec<f64>| {
|
||||
}
|
||||
|
||||
// --- Candlestick Patterns (family 14) ---
|
||||
drive(TowerTopBottom::new, &candles);
|
||||
drive(HaramiCross::new, &candles);
|
||||
drive(Tristar::new, &candles);
|
||||
drive(|| FryPanBottom::new(9).unwrap(), &candles);
|
||||
drive(|| DumplingTop::new(9).unwrap(), &candles);
|
||||
drive(|| NewPriceLines::new(5).unwrap(), &candles);
|
||||
drive(TdTrap::new, &candles);
|
||||
drive(TdPropulsion::new, &candles);
|
||||
drive(TdClopwin::new, &candles);
|
||||
|
||||
@@ -10,11 +10,7 @@
|
||||
//! never panic, streaming or batched.
|
||||
|
||||
use libfuzzer_sys::fuzz_target;
|
||||
use wickra_core::{
|
||||
BatchExt, CalendarSpread, DerivativesTick, FundingBasis, FundingRate, FundingRateMean,
|
||||
FundingRateZScore, Indicator, LiquidationFeatures, LongShortRatio, OIPriceDivergence,
|
||||
OIWeighted, OpenInterestDelta, TakerBuySellRatio, TermStructureBasis,
|
||||
};
|
||||
use wickra_core::{BatchExt, CalendarSpread, DerivativesTick, EstimatedLeverageRatio, FundingBasis, FundingImpliedApr, FundingRate, FundingRateMean, FundingRateZScore, Indicator, LiquidationFeatures, LongShortRatio, OIPriceDivergence, OIWeighted, OiToVolumeRatio, OpenInterestDelta, OpenInterestMomentum, PerpetualPremiumIndex, TakerBuySellRatio, TermStructureBasis};
|
||||
|
||||
#[inline(never)]
|
||||
fn drive<I>(make: impl Fn() -> I, ticks: &[DerivativesTick])
|
||||
@@ -53,6 +49,11 @@ fuzz_target!(|data: &[u8]| {
|
||||
drive(TakerBuySellRatio::new, &ticks);
|
||||
drive(TermStructureBasis::new, &ticks);
|
||||
drive(CalendarSpread::new, &ticks);
|
||||
drive(EstimatedLeverageRatio::new, &ticks);
|
||||
drive(OiToVolumeRatio::new, &ticks);
|
||||
drive(PerpetualPremiumIndex::new, &ticks);
|
||||
drive(|| FundingImpliedApr::new(1095.0).unwrap(), &ticks);
|
||||
drive(|| OpenInterestMomentum::new(14).unwrap(), &ticks);
|
||||
|
||||
// LiquidationFeatures emits a struct, not an f64, so drive it directly.
|
||||
let mut liq = LiquidationFeatures::new();
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
//! panic.
|
||||
|
||||
use libfuzzer_sys::fuzz_target;
|
||||
use wickra_core::{Alpha, BatchExt, BetaNeutralSpread, Cointegration, DistanceSsd, GrangerCausality, Indicator, InformationRatio, KalmanHedgeRatio, KendallTau, LeadLagCrossCorrelation, OuHalfLife, PairSpreadZScore, PairwiseBeta, RelativeStrengthAB, RollingCorrelation, RollingCovariance, SpreadAr1Coefficient, SpreadBollingerBands, SpreadHurst, TreynorRatio, VarianceRatio};
|
||||
use wickra_core::{Alpha, BatchExt, BetaNeutralSpread, Cointegration, DistanceSsd, GrangerCausality, HasbrouckInformationShare, Indicator, InformationRatio, KalmanHedgeRatio, KendallTau, LeadLagCrossCorrelation, OuHalfLife, PairSpreadZScore, PairwiseBeta, RelativeStrengthAB, RollingCorrelation, RollingCovariance, SpreadAr1Coefficient, SpreadBollingerBands, SpreadHurst, TreynorRatio, VarianceRatio};
|
||||
|
||||
#[inline(never)]
|
||||
fn drive<I>(make: impl Fn() -> I, data: &[(f64, f64)])
|
||||
@@ -48,6 +48,7 @@ fuzz_target!(|data: &[u8]| {
|
||||
drive(|| GrangerCausality::new(60, 1).unwrap(), &pairs);
|
||||
drive(|| SpreadAr1Coefficient::new(40).unwrap(), &pairs);
|
||||
drive(|| KendallTau::new(20).unwrap(), &pairs);
|
||||
drive(|| HasbrouckInformationShare::new(2).unwrap(), &pairs);
|
||||
|
||||
// Struct-output pair indicator: drive update + batch directly (the generic
|
||||
// `drive` above only covers `Output = f64`).
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
//! would reject — the indicators must never panic, streaming or batched.
|
||||
|
||||
use libfuzzer_sys::fuzz_target;
|
||||
use wickra_core::{AmihudIlliquidity, BatchExt, CumulativeVolumeDelta, Footprint, Indicator, RollMeasure, Side, SignedVolume, Trade, TradeImbalance, Vpin};
|
||||
use wickra_core::{AmihudIlliquidity, BatchExt, CumulativeVolumeDelta, Footprint, Indicator, Pin, RollMeasure, Side, SignedVolume, Trade, TradeImbalance, TradeSignAutocorrelation, Vpin};
|
||||
|
||||
#[inline(never)]
|
||||
fn drive<I>(make: impl Fn() -> I, trades: &[Trade])
|
||||
@@ -43,6 +43,8 @@ fuzz_target!(|data: &[u8]| {
|
||||
drive(|| Vpin::new(8.0, 5).unwrap(), &trades);
|
||||
drive(|| AmihudIlliquidity::new(20).unwrap(), &trades);
|
||||
drive(|| RollMeasure::new(20).unwrap(), &trades);
|
||||
drive(|| TradeSignAutocorrelation::new(20).unwrap(), &trades);
|
||||
drive(|| Pin::new(20).unwrap(), &trades);
|
||||
|
||||
// Footprint emits a variable-length `FootprintOutput` rather than an `f64`,
|
||||
// so it is driven directly rather than through the scalar-output helper.
|
||||
|
||||
Reference in New Issue
Block a user