Compare commits

...

4 Commits

Author SHA1 Message Date
kingchenc 5862401958 release: bump 0.7.0 -> 0.7.1 (#215)
Version bump 0.7.0 -> 0.7.1 for the B16 Derivatives batch (493 indicators).
2026-06-08 03:35:31 +02:00
kingchenc ff5a047078 feat(derivatives): leverage, OI/volume, perpetual premium, funding APR, OI momentum (B16) (#214)
## B16 Derivatives — five new indicators (488 → 493)

All consume a `DerivativesTick` and emit `f64`:

| Indicator | Reads | Formula |
|-----------|-------|---------|
| `EstimatedLeverageRatio` | open_interest, long_size, short_size | `OI / (long + short)` |
| `OiToVolumeRatio` | open_interest, taker_buy_volume, taker_sell_volume | `OI / (buy + sell)` |
| `PerpetualPremiumIndex` | mark_price, index_price | `(mark − index) / index` |
| `FundingImpliedApr` | funding_rate | `rate × intervals_per_year` |
| `OpenInterestMomentum` | open_interest | `100 · (OI_t − OI_{t−period}) / OI_{t−period}` |

### Wiring
- Core structs + full unit tests (incl. zero-denominator branches).
- Hand-written Python/Node/WASM tick bindings; two new tick helpers (`deriv_oi_long_short`, `deriv_oi_taker`).
- Fuzz drives in `indicator_update_derivatives.rs`; dedicated reference + streaming-vs-batch tests (Python + Node).
- README counter + `docs/README.md` + `FAMILIES` assert bumped to 493.

### Verify (local, all green)
- `cargo test -p wickra-core --lib`: 4028 · `--doc`: 443
- clippy workspace: clean
- node: 563 · pytest: 928
2026-06-08 03:33:59 +02:00
kingchenc dc415a77fd release: bump 0.6.9 -> 0.7.0 (#213)
Version bump 0.6.9 -> 0.7.0 for the B15 Microstructure batch (488 indicators).
2026-06-08 03:10:16 +02:00
kingchenc e385734275 feat(microstructure): trade-sign autocorrelation, PIN, Hasbrouck information share (B15) (#212)
## B15 Microstructure — three new indicators (485 → 488)

| Indicator | Input | Output | Notes |
|-----------|-------|--------|-------|
| `TradeSignAutocorrelation` | `Trade` | `f64` ∈ [-1,1] | lag-1 autocorrelation of the signed aggressor (order-flow persistence) |
| `Pin` | `Trade` | `f64` ∈ [0,1] | probability of informed trading from rolling buy/sell imbalance (EKOP single-window estimator); `name()` = `"PIN"` |
| `HasbrouckInformationShare` | `(f64, f64)` | `f64` ∈ [0,1] | variance-ratio proxy for each venue's share of price discovery |

### Wiring
- Core structs + full unit tests (every branch).
- Hand-written Python/Node/WASM bindings for the two `Trade`-input indicators (precedent `TradeImbalance`); `node_pair_indicator!` / `wasm_pair_indicator!` macro bindings + hand Python pyclass for the pairwise Hasbrouck (precedent `RollingCorrelation`).
- Fuzz drives added to `indicator_update_trade.rs` and `indicator_update_pair.rs`.
- Dedicated Python + Node streaming-vs-batch and reference tests; Hasbrouck in the `PAIR` registry.
- README counter (3 spots) + `docs/README.md` + `FAMILIES` assert bumped to 488.

### Verify (all green, local)
- `cargo test -p wickra-core --lib`: 3991 passed
- `cargo test -p wickra-core --doc`: 438 passed
- `cargo clippy --workspace --all-targets --all-features -- -D warnings`: clean
- node: 561 passed · pytest: 926 passed
2026-06-08 03:07:50 +02:00
36 changed files with 3184 additions and 102 deletions
+15 -1
View File
@@ -7,6 +7,18 @@ 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`).
@@ -1377,7 +1389,9 @@ 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.9...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
Generated
+8 -8
View File
@@ -1944,7 +1944,7 @@ dependencies = [
[[package]]
name = "wickra"
version = "0.6.9"
version = "0.7.1"
dependencies = [
"approx",
"criterion",
@@ -1955,7 +1955,7 @@ dependencies = [
[[package]]
name = "wickra-bench"
version = "0.6.9"
version = "0.7.1"
dependencies = [
"criterion",
"kand",
@@ -1967,7 +1967,7 @@ dependencies = [
[[package]]
name = "wickra-core"
version = "0.6.9"
version = "0.7.1"
dependencies = [
"approx",
"proptest",
@@ -1977,7 +1977,7 @@ dependencies = [
[[package]]
name = "wickra-data"
version = "0.6.9"
version = "0.7.1"
dependencies = [
"approx",
"csv",
@@ -1994,7 +1994,7 @@ dependencies = [
[[package]]
name = "wickra-examples"
version = "0.6.9"
version = "0.7.1"
dependencies = [
"serde_json",
"tokio",
@@ -2004,7 +2004,7 @@ dependencies = [
[[package]]
name = "wickra-node"
version = "0.6.9"
version = "0.7.1"
dependencies = [
"napi",
"napi-build",
@@ -2014,7 +2014,7 @@ dependencies = [
[[package]]
name = "wickra-python"
version = "0.6.9"
version = "0.7.1"
dependencies = [
"numpy",
"pyo3",
@@ -2023,7 +2023,7 @@ dependencies = [
[[package]]
name = "wickra-wasm"
version = "0.6.9"
version = "0.7.1"
dependencies = [
"console_error_panic_hook",
"js-sys",
+2 -2
View File
@@ -13,7 +13,7 @@ members = [
exclude = ["fuzz"]
[workspace.package]
version = "0.6.9"
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.9" }
wickra-core = { path = "crates/wickra-core", version = "0.7.1" }
thiserror = "2"
rayon = "1.10"
+9 -9
View File
@@ -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=485" 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>
[![CI](https://github.com/wickra-lib/wickra/actions/workflows/ci.yml/badge.svg)](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 485 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.** 485 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,7 +77,7 @@ times to get there.
- **Correct by construction, not by hope.** Every `update` validates its input,
runs a real warmup, and returns an `Option` so a single bad tick can't silently
poison state. `batch == streaming` is **bit-exact, fuzzed and 100 %-line-covered
for all 485 indicators**.
for all 493 indicators**.
- **Orders of magnitude faster where it counts.** In streaming Wickra is **1156×**
faster than the only other incremental peer and **thousands of times** faster
than recompute-on-every-tick libraries. On batch it wins several rows outright
@@ -95,7 +95,7 @@ Every other library forces one of those compromises. Wickra doesn't:
| Library | Install | Streaming | Languages | Indicators | Active |
|------------------|-------------|-------------|-----------------------------|-----------:|--------|
| **★&nbsp;Wickra**| **clean** | **yes, O(1)** | **Python · Node · WASM · Rust** | **485** | **yes** |
| **★&nbsp;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 |
@@ -128,7 +128,7 @@ Full tables (Rust + Python, streaming + batch) and how to reproduce them live in
## Indicators
485 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).
@@ -153,8 +153,8 @@ warmup) at [docs.wickra.org](https://docs.wickra.org/Indicators-Overview).
| 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 485 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)
+62 -1
View File
@@ -667,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)) {
@@ -1270,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);
@@ -1279,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', () => {
@@ -1432,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.
+76
View File
@@ -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)`
@@ -4333,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)
@@ -4513,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()
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "wickra-darwin-arm64",
"version": "0.6.9",
"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 -1
View File
@@ -1,6 +1,6 @@
{
"name": "wickra-darwin-x64",
"version": "0.6.9",
"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.9",
"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 -1
View File
@@ -1,6 +1,6 @@
{
"name": "wickra-linux-x64-gnu",
"version": "0.6.9",
"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.9",
"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.9",
"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": [
+20 -20
View File
@@ -1,12 +1,12 @@
{
"name": "wickra",
"version": "0.6.9",
"version": "0.7.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "wickra",
"version": "0.6.9",
"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.9",
"wickra-darwin-x64": "0.6.9",
"wickra-linux-arm64-gnu": "0.6.9",
"wickra-linux-x64-gnu": "0.6.9",
"wickra-win32-arm64-msvc": "0.6.9",
"wickra-win32-x64-msvc": "0.6.9"
"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.9",
"resolved": "https://registry.npmjs.org/wickra-darwin-arm64/-/wickra-darwin-arm64-0.6.9.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.9",
"resolved": "https://registry.npmjs.org/wickra-darwin-x64/-/wickra-darwin-x64-0.6.9.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.9",
"resolved": "https://registry.npmjs.org/wickra-linux-arm64-gnu/-/wickra-linux-arm64-gnu-0.6.9.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.9",
"resolved": "https://registry.npmjs.org/wickra-linux-x64-gnu/-/wickra-linux-x64-gnu-0.6.9.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.9",
"resolved": "https://registry.npmjs.org/wickra-win32-arm64-msvc/-/wickra-win32-arm64-msvc-0.6.9.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.9",
"resolved": "https://registry.npmjs.org/wickra-win32-x64-msvc/-/wickra-win32-x64-msvc-0.6.9.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"
+7 -7
View File
@@ -1,6 +1,6 @@
{
"name": "wickra",
"version": "0.6.9",
"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.9",
"wickra-linux-arm64-gnu": "0.6.9",
"wickra-darwin-x64": "0.6.9",
"wickra-darwin-arm64": "0.6.9",
"wickra-win32-x64-msvc": "0.6.9",
"wickra-win32-arm64-msvc": "0.6.9"
"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",
+433
View File
@@ -884,6 +884,11 @@ node_pair_indicator!(
"BetaNeutralSpread",
wc::BetaNeutralSpread
);
node_pair_indicator!(
HasbrouckInformationShareNode,
"HasbrouckInformationShare",
wc::HasbrouckInformationShare
);
// ============================== PairSpreadZScore ==============================
@@ -13739,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 {
@@ -14343,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,
@@ -15058,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
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "maturin"
[project]
name = "wickra"
version = "0.6.9"
version = "0.7.1"
description = "Streaming-first technical indicators: incremental, fast, install-free."
readme = "README.md"
license = "MIT OR Apache-2.0"
+16
View File
@@ -464,6 +464,8 @@ from ._wickra import (
QuotedSpread,
DepthSlope,
# Microstructure: trade flow
Pin,
TradeSignAutocorrelation,
RollMeasure,
AmihudIlliquidity,
Vpin,
@@ -471,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,
@@ -979,6 +987,8 @@ __all__ = [
"QuotedSpread",
"DepthSlope",
# Microstructure: trade flow
"Pin",
"TradeSignAutocorrelation",
"RollMeasure",
"AmihudIlliquidity",
"Vpin",
@@ -986,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",
+518
View File
@@ -16980,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(
@@ -18652,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(
@@ -19250,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,
@@ -19970,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
@@ -24743,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>()?;
@@ -24833,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>()?;
@@ -24856,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>()?;
@@ -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)),
@@ -3323,6 +3324,13 @@ def test_tower_top_bottom_reference():
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 ------------------------------------------------------------
@@ -3663,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()
@@ -3674,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)
@@ -4044,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 ------------------------------------------------------
+298
View File
@@ -563,6 +563,11 @@ wasm_pair_indicator!(
"BetaNeutralSpread",
wc::BetaNeutralSpread
);
wasm_pair_indicator!(
WasmHasbrouckInformationShare,
"HasbrouckInformationShare",
wc::HasbrouckInformationShare
);
// ---------- PairSpreadZScore (two params) ----------
@@ -9279,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 {
@@ -9868,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,
@@ -10191,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)]
@@ -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,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,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_{t1}, ry_t = y_t y_{t1} (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);
}
}
+25 -1
View File
@@ -131,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;
@@ -156,6 +157,7 @@ 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;
@@ -174,6 +176,7 @@ 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;
@@ -277,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;
@@ -294,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;
@@ -423,6 +430,7 @@ 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;
@@ -616,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;
@@ -641,6 +650,7 @@ 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;
@@ -659,6 +669,7 @@ 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;
@@ -762,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;
@@ -779,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;
@@ -908,6 +923,7 @@ pub use time_of_day_return_profile::{TimeOfDayReturnProfile, TimeOfDayReturnProf
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;
@@ -1452,6 +1468,9 @@ pub const FAMILIES: &[(&str, &[&str])] = &[
"Vpin",
"AmihudIlliquidity",
"RollMeasure",
"TradeSignAutocorrelation",
"Pin",
"HasbrouckInformationShare",
],
),
(
@@ -1469,6 +1488,11 @@ pub const FAMILIES: &[(&str, &[&str])] = &[
"LiquidationFeatures",
"TermStructureBasis",
"CalendarSpread",
"EstimatedLeverageRatio",
"OiToVolumeRatio",
"PerpetualPremiumIndex",
"FundingImpliedApr",
"OpenInterestMomentum",
],
),
(
@@ -1615,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, 485, "FAMILIES total drifted from indicator count");
assert_eq!(total, 493, "FAMILIES total drifted from indicator count");
}
}
@@ -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_{tperiod}) / OI_{tperiod}
/// ```
///
/// 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);
}
}
+223
View File
@@ -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,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_{t1} ) ∈ [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);
}
}
+33 -30
View File
@@ -81,17 +81,18 @@ pub use indicators::{
DoubleTopBottom, DownsideGapThreeMethods, Dpo, DragonflyDoji, DrawdownDuration, DumplingTop,
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, FryPanBottom, FundingBasis, FundingRate,
FundingRateMean, FundingRateZScore, GainLossRatio, GapSideBySideWhite, Garch11,
GarmanKlassVolatility, Gartley, GatorOscillator, GatorOscillatorOutput, GeneralizedDema,
GeometricMa, GoldenPocket, GoldenPocketOutput, GrangerCausality, GravestoneDoji, Hammer,
HangingMan, Harami, HaramiCross, HeadAndShoulders, HeikinAshi, HeikinAshiOscillator,
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, HaramiCross,
HasbrouckInformationShare, HeadAndShoulders, HeikinAshi, HeikinAshiOscillator,
HeikinAshiOutput, HiLoActivator, HighLowIndex, HighLowRange, HighWave, HighpassFilter, Hikkake,
HikkakeModified, HilbertDominantCycle, HistoricalVolatility, Hma, HoltWinters, HomingPigeon,
HtDcPhase, HtPhasor, HtPhasorOutput, HtTrendMode, HurstChannel, HurstChannelOutput,
@@ -112,11 +113,12 @@ pub use indicators::{
MedianPrice, Mfi, Microprice, MidPoint, MidPrice, MinusDi, MinusDm, ModifiedMaStop,
ModifiedMaStopOutput, Mom, MorningDojiStar, MorningEveningStar, MurreyMathLines,
MurreyMathLinesOutput, Natr, NewHighsNewLows, NewPriceLines, 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,
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,
@@ -140,20 +142,21 @@ pub use indicators::{
TdSequentialOutput, TdSetup, TdTrap, Tema, TermStructureBasis, ThreeDrives, ThreeInside,
ThreeLineBreak, ThreeLineStrike, ThreeOutside, ThreeSoldiersOrCrows, ThreeStarsInSouth,
Thrusting, TickIndex, Tii, TimeBasedStop, TimeOfDayReturnProfile, TimeOfDayReturnProfileOutput,
TowerTopBottom, TpoProfile, TpoProfileOutput, TradeImbalance, 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, ZeroLagMacdOutput, ZigZag, ZigZagOutput, Zlema, FAMILIES, T3,
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,
ZeroLagMacdOutput, ZigZag, ZigZagOutput, Zlema, FAMILIES, T3,
};
// `FootprintLevel` is a row element of `FootprintOutput`, re-exported on its own
// line so the indicator-count tooling (which scans the braced block above and
+1 -1
View File
@@ -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 **485 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 &
+7 -7
View File
@@ -17,7 +17,7 @@
},
"../../bindings/node": {
"name": "wickra",
"version": "0.6.9",
"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.9",
"wickra-darwin-x64": "0.6.9",
"wickra-linux-arm64-gnu": "0.6.9",
"wickra-linux-x64-gnu": "0.6.9",
"wickra-win32-arm64-msvc": "0.6.9",
"wickra-win32-x64-msvc": "0.6.9"
"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": {
@@ -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();
+2 -1
View File
@@ -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`).
+3 -1
View File
@@ -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.