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
This commit is contained in:
@@ -6,6 +6,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
- **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
|
## [0.7.0] - 2026-06-08
|
||||||
- **Hasbrouck Information Share** — variance-ratio proxy for each venue's share of price discovery (Hasbrouck information share) (`HasbrouckInformationShare`).
|
- **Hasbrouck Information Share** — variance-ratio proxy for each venue's share of price discovery (Hasbrouck information share) (`HasbrouckInformationShare`).
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://wickra.org"><img src="https://raw.githubusercontent.com/wickra-lib/.github/main/profile/wickra-banner.webp?v=488" 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>
|
</p>
|
||||||
|
|
||||||
[](https://github.com/wickra-lib/wickra/actions/workflows/ci.yml)
|
[](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),
|
[Node](https://docs.wickra.org/Quickstart-Node),
|
||||||
[WASM](https://docs.wickra.org/Quickstart-WASM).
|
[WASM](https://docs.wickra.org/Quickstart-WASM).
|
||||||
- **Indicators** — a per-indicator deep dive (formula, parameters, warmup) for
|
- **Indicators** — a per-indicator deep dive (formula, parameters, warmup) for
|
||||||
every one of the 488 indicators; start at the
|
every one of the 493 indicators; start at the
|
||||||
[indicators overview](https://docs.wickra.org/Indicators-Overview).
|
[indicators overview](https://docs.wickra.org/Indicators-Overview).
|
||||||
- **Reference** — [warmup periods](https://docs.wickra.org/Warmup-Periods),
|
- **Reference** — [warmup periods](https://docs.wickra.org/Warmup-Periods),
|
||||||
[streaming vs batch](https://docs.wickra.org/Streaming-vs-Batch),
|
[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
|
a full batch library, and without making you reimplement your indicators four
|
||||||
times to get there.
|
times to get there.
|
||||||
|
|
||||||
- **The biggest streaming-native catalogue, period.** 488 indicators across 24
|
- **The biggest streaming-native catalogue, period.** 493 indicators across 24
|
||||||
families — candlesticks, harmonic & chart patterns, market profile, market
|
families — candlesticks, harmonic & chart patterns, market profile, market
|
||||||
breadth, Renko/Kagi/Point&Figure bars, Ehlers DSP cycles, risk/performance
|
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
|
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,
|
- **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
|
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
|
poison state. `batch == streaming` is **bit-exact, fuzzed and 100 %-line-covered
|
||||||
for all 488 indicators**.
|
for all 493 indicators**.
|
||||||
- **Orders of magnitude faster where it counts.** In streaming Wickra is **11–56×**
|
- **Orders of magnitude faster where it counts.** In streaming Wickra is **11–56×**
|
||||||
faster than the only other incremental peer and **thousands of times** faster
|
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
|
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 |
|
| Library | Install | Streaming | Languages | Indicators | Active |
|
||||||
|------------------|-------------|-------------|-----------------------------|-----------:|--------|
|
|------------------|-------------|-------------|-----------------------------|-----------:|--------|
|
||||||
| **★ Wickra**| **clean** | **yes, O(1)** | **Python · Node · WASM · Rust** | **488** | **yes** |
|
| **★ Wickra**| **clean** | **yes, O(1)** | **Python · Node · WASM · Rust** | **493** | **yes** |
|
||||||
| kand | clean | yes | Python · WASM · Rust | ~60 | yes |
|
| kand | clean | yes | Python · WASM · Rust | ~60 | yes |
|
||||||
| ta-rs | clean | yes | Rust only | ~30 | stale |
|
| ta-rs | clean | yes | Rust only | ~30 | stale |
|
||||||
| yata | clean | partial | Rust only | ~35 | yes |
|
| 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
|
## Indicators
|
||||||
|
|
||||||
488 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
|
`batch == streaming` equivalence test, reference-value tests, and reset
|
||||||
semantics tests. Each has a per-indicator deep dive (formula, parameters,
|
semantics tests. Each has a per-indicator deep dive (formula, parameters,
|
||||||
warmup) at [docs.wickra.org](https://docs.wickra.org/Indicators-Overview).
|
warmup) at [docs.wickra.org](https://docs.wickra.org/Indicators-Overview).
|
||||||
@@ -154,7 +154,7 @@ warmup) at [docs.wickra.org](https://docs.wickra.org/Indicators-Overview).
|
|||||||
| Harmonic Patterns | AB=CD, Gartley, Butterfly, Bat, Crab, Shark, Cypher, Three Drives |
|
| 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 |
|
| 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, Trade-Sign Autocorrelation, Hasbrouck Information Share |
|
| 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 |
|
| 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 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 |
|
| 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) |
|
| 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/
|
wickra/
|
||||||
├── crates/
|
├── crates/
|
||||||
│ ├── wickra-core/ core engine + all 488 indicators
|
│ ├── wickra-core/ core engine + all 493 indicators
|
||||||
│ ├── wickra/ top-level facade crate (publishes on crates.io) + benches/
|
│ ├── wickra/ top-level facade crate (publishes on crates.io) + benches/
|
||||||
│ ├── wickra-data/ CSV reader, tick aggregator, live exchange feeds
|
│ ├── wickra-data/ CSV reader, tick aggregator, live exchange feeds
|
||||||
│ └── wickra-bench/ internal cross-library benchmark harness (not published)
|
│ └── wickra-bench/ internal cross-library benchmark harness (not published)
|
||||||
|
|||||||
@@ -1443,6 +1443,56 @@ test('derivatives reject bad input', () => {
|
|||||||
assert.throws(() => new wickra.FundingBasis().update(100, 0));
|
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', () => {
|
test('market breadth: AdvanceDecline reference values', () => {
|
||||||
// A breadth tick is the universe as parallel arrays; the sign of `change`
|
// A breadth tick is the universe as parallel arrays; the sign of `change`
|
||||||
// classifies each symbol as advancing / declining / unchanged.
|
// classifies each symbol as advancing / declining / unchanged.
|
||||||
|
|||||||
Vendored
+45
@@ -4544,6 +4544,51 @@ export declare class CalendarSpread {
|
|||||||
isReady(): boolean
|
isReady(): boolean
|
||||||
warmupPeriod(): number
|
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 type AdvanceDeclineNode = AdvanceDecline
|
||||||
export declare class AdvanceDecline {
|
export declare class AdvanceDecline {
|
||||||
constructor()
|
constructor()
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -14450,6 +14450,50 @@ fn deriv_taker(taker_buy_volume: f64, taker_sell_volume: f64) -> napi::Result<wc
|
|||||||
.map_err(map_err)
|
.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(
|
fn deriv_liquidation(
|
||||||
long_liquidation: f64,
|
long_liquidation: f64,
|
||||||
short_liquidation: f64,
|
short_liquidation: f64,
|
||||||
@@ -15165,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) ----------
|
// ---------- Market Breadth (CrossSection input) ----------
|
||||||
//
|
//
|
||||||
// A breadth tick is the per-symbol state of the whole universe, passed as four
|
// A breadth tick is the per-symbol state of the whole universe, passed as four
|
||||||
|
|||||||
@@ -480,6 +480,11 @@ from ._wickra import (
|
|||||||
# Microstructure: footprint
|
# Microstructure: footprint
|
||||||
Footprint,
|
Footprint,
|
||||||
# Derivatives
|
# Derivatives
|
||||||
|
OpenInterestMomentum,
|
||||||
|
FundingImpliedApr,
|
||||||
|
PerpetualPremiumIndex,
|
||||||
|
OiToVolumeRatio,
|
||||||
|
EstimatedLeverageRatio,
|
||||||
FundingRate,
|
FundingRate,
|
||||||
FundingRateMean,
|
FundingRateMean,
|
||||||
FundingRateZScore,
|
FundingRateZScore,
|
||||||
@@ -998,6 +1003,11 @@ __all__ = [
|
|||||||
# Microstructure: footprint
|
# Microstructure: footprint
|
||||||
"Footprint",
|
"Footprint",
|
||||||
# Derivatives
|
# Derivatives
|
||||||
|
"OpenInterestMomentum",
|
||||||
|
"FundingImpliedApr",
|
||||||
|
"PerpetualPremiumIndex",
|
||||||
|
"OiToVolumeRatio",
|
||||||
|
"EstimatedLeverageRatio",
|
||||||
"FundingRate",
|
"FundingRate",
|
||||||
"FundingRateMean",
|
"FundingRateMean",
|
||||||
"FundingRateZScore",
|
"FundingRateZScore",
|
||||||
|
|||||||
@@ -19420,6 +19420,50 @@ fn deriv_taker(taker_buy_volume: f64, taker_sell_volume: f64) -> PyResult<wc::De
|
|||||||
.map_err(map_err)
|
.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(
|
fn deriv_liquidation(
|
||||||
long_liquidation: f64,
|
long_liquidation: f64,
|
||||||
short_liquidation: f64,
|
short_liquidation: f64,
|
||||||
@@ -20140,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 ==============================
|
||||||
//
|
//
|
||||||
// Market-breadth indicators consume a `CrossSection`: one tick carrying the
|
// Market-breadth indicators consume a `CrossSection`: one tick carrying the
|
||||||
@@ -25029,6 +25369,11 @@ fn _wickra(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> {
|
|||||||
m.add_class::<PyLiquidationFeatures>()?;
|
m.add_class::<PyLiquidationFeatures>()?;
|
||||||
m.add_class::<PyTermStructureBasis>()?;
|
m.add_class::<PyTermStructureBasis>()?;
|
||||||
m.add_class::<PyCalendarSpread>()?;
|
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::<PyAdvanceDecline>()?;
|
||||||
m.add_class::<PyAdvanceDeclineRatio>()?;
|
m.add_class::<PyAdvanceDeclineRatio>()?;
|
||||||
m.add_class::<PyAdVolumeLine>()?;
|
m.add_class::<PyAdVolumeLine>()?;
|
||||||
|
|||||||
@@ -4082,6 +4082,71 @@ def test_basis_indicators_streaming_equals_batch():
|
|||||||
assert _eq_nan(batch, streamed)
|
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 ------------------------------------------------------
|
# --- Alt-Chart Bars ------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -9933,6 +9933,50 @@ fn deriv_taker(
|
|||||||
.map_err(map_err)
|
.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(
|
fn deriv_liquidation(
|
||||||
long_liquidation: f64,
|
long_liquidation: f64,
|
||||||
short_liquidation: f64,
|
short_liquidation: f64,
|
||||||
@@ -10256,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 ----------
|
// ---------- Heikin-Ashi Oscillator ----------
|
||||||
|
|
||||||
#[wasm_bindgen(js_name = HeikinAshiOscillator)]
|
#[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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -131,6 +131,7 @@ mod ema;
|
|||||||
mod empirical_mode_decomposition;
|
mod empirical_mode_decomposition;
|
||||||
mod engulfing;
|
mod engulfing;
|
||||||
mod equivolume;
|
mod equivolume;
|
||||||
|
mod estimated_leverage_ratio;
|
||||||
mod even_better_sinewave;
|
mod even_better_sinewave;
|
||||||
mod evening_doji_star;
|
mod evening_doji_star;
|
||||||
mod evwma;
|
mod evwma;
|
||||||
@@ -156,6 +157,7 @@ mod fractal_chaos_bands;
|
|||||||
mod frama;
|
mod frama;
|
||||||
mod fry_pan_bottom;
|
mod fry_pan_bottom;
|
||||||
mod funding_basis;
|
mod funding_basis;
|
||||||
|
mod funding_implied_apr;
|
||||||
mod funding_rate;
|
mod funding_rate;
|
||||||
mod funding_rate_mean;
|
mod funding_rate_mean;
|
||||||
mod funding_rate_zscore;
|
mod funding_rate_zscore;
|
||||||
@@ -278,9 +280,11 @@ mod ob_imbalance_topn;
|
|||||||
mod obv;
|
mod obv;
|
||||||
mod oi_delta;
|
mod oi_delta;
|
||||||
mod oi_price_divergence;
|
mod oi_price_divergence;
|
||||||
|
mod oi_to_volume_ratio;
|
||||||
mod oi_weighted;
|
mod oi_weighted;
|
||||||
mod omega_ratio;
|
mod omega_ratio;
|
||||||
mod on_neck;
|
mod on_neck;
|
||||||
|
mod open_interest_momentum;
|
||||||
mod opening_marubozu;
|
mod opening_marubozu;
|
||||||
mod opening_range;
|
mod opening_range;
|
||||||
mod order_flow_imbalance;
|
mod order_flow_imbalance;
|
||||||
@@ -295,6 +299,7 @@ mod pearson_correlation;
|
|||||||
mod percent_above_ma;
|
mod percent_above_ma;
|
||||||
mod percent_b;
|
mod percent_b;
|
||||||
mod percentage_trailing_stop;
|
mod percentage_trailing_stop;
|
||||||
|
mod perpetual_premium_index;
|
||||||
mod pgo;
|
mod pgo;
|
||||||
mod piercing_dark_cloud;
|
mod piercing_dark_cloud;
|
||||||
mod pin;
|
mod pin;
|
||||||
@@ -619,6 +624,7 @@ pub use ema::Ema;
|
|||||||
pub use empirical_mode_decomposition::EmpiricalModeDecomposition;
|
pub use empirical_mode_decomposition::EmpiricalModeDecomposition;
|
||||||
pub use engulfing::Engulfing;
|
pub use engulfing::Engulfing;
|
||||||
pub use equivolume::{Equivolume, EquivolumeOutput};
|
pub use equivolume::{Equivolume, EquivolumeOutput};
|
||||||
|
pub use estimated_leverage_ratio::EstimatedLeverageRatio;
|
||||||
pub use even_better_sinewave::EvenBetterSinewave;
|
pub use even_better_sinewave::EvenBetterSinewave;
|
||||||
pub use evening_doji_star::EveningDojiStar;
|
pub use evening_doji_star::EveningDojiStar;
|
||||||
pub use evwma::Evwma;
|
pub use evwma::Evwma;
|
||||||
@@ -644,6 +650,7 @@ pub use fractal_chaos_bands::{FractalChaosBands, FractalChaosBandsOutput};
|
|||||||
pub use frama::Frama;
|
pub use frama::Frama;
|
||||||
pub use fry_pan_bottom::FryPanBottom;
|
pub use fry_pan_bottom::FryPanBottom;
|
||||||
pub use funding_basis::FundingBasis;
|
pub use funding_basis::FundingBasis;
|
||||||
|
pub use funding_implied_apr::FundingImpliedApr;
|
||||||
pub use funding_rate::FundingRate;
|
pub use funding_rate::FundingRate;
|
||||||
pub use funding_rate_mean::FundingRateMean;
|
pub use funding_rate_mean::FundingRateMean;
|
||||||
pub use funding_rate_zscore::FundingRateZScore;
|
pub use funding_rate_zscore::FundingRateZScore;
|
||||||
@@ -766,9 +773,11 @@ pub use ob_imbalance_topn::OrderBookImbalanceTopN;
|
|||||||
pub use obv::Obv;
|
pub use obv::Obv;
|
||||||
pub use oi_delta::OpenInterestDelta;
|
pub use oi_delta::OpenInterestDelta;
|
||||||
pub use oi_price_divergence::OIPriceDivergence;
|
pub use oi_price_divergence::OIPriceDivergence;
|
||||||
|
pub use oi_to_volume_ratio::OiToVolumeRatio;
|
||||||
pub use oi_weighted::OIWeighted;
|
pub use oi_weighted::OIWeighted;
|
||||||
pub use omega_ratio::OmegaRatio;
|
pub use omega_ratio::OmegaRatio;
|
||||||
pub use on_neck::OnNeck;
|
pub use on_neck::OnNeck;
|
||||||
|
pub use open_interest_momentum::OpenInterestMomentum;
|
||||||
pub use opening_marubozu::OpeningMarubozu;
|
pub use opening_marubozu::OpeningMarubozu;
|
||||||
pub use opening_range::{OpeningRange, OpeningRangeOutput};
|
pub use opening_range::{OpeningRange, OpeningRangeOutput};
|
||||||
pub use order_flow_imbalance::OrderFlowImbalance;
|
pub use order_flow_imbalance::OrderFlowImbalance;
|
||||||
@@ -783,6 +792,7 @@ pub use pearson_correlation::PearsonCorrelation;
|
|||||||
pub use percent_above_ma::PercentAboveMa;
|
pub use percent_above_ma::PercentAboveMa;
|
||||||
pub use percent_b::PercentB;
|
pub use percent_b::PercentB;
|
||||||
pub use percentage_trailing_stop::PercentageTrailingStop;
|
pub use percentage_trailing_stop::PercentageTrailingStop;
|
||||||
|
pub use perpetual_premium_index::PerpetualPremiumIndex;
|
||||||
pub use pgo::Pgo;
|
pub use pgo::Pgo;
|
||||||
pub use piercing_dark_cloud::PiercingDarkCloud;
|
pub use piercing_dark_cloud::PiercingDarkCloud;
|
||||||
pub use pin::Pin;
|
pub use pin::Pin;
|
||||||
@@ -1478,6 +1488,11 @@ pub const FAMILIES: &[(&str, &[&str])] = &[
|
|||||||
"LiquidationFeatures",
|
"LiquidationFeatures",
|
||||||
"TermStructureBasis",
|
"TermStructureBasis",
|
||||||
"CalendarSpread",
|
"CalendarSpread",
|
||||||
|
"EstimatedLeverageRatio",
|
||||||
|
"OiToVolumeRatio",
|
||||||
|
"PerpetualPremiumIndex",
|
||||||
|
"FundingImpliedApr",
|
||||||
|
"OpenInterestMomentum",
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
@@ -1624,6 +1639,6 @@ mod family_tests {
|
|||||||
// the actual indicator count is the early-warning signal that an
|
// the actual indicator count is the early-warning signal that an
|
||||||
// indicator was added without being assigned a family.
|
// indicator was added without being assigned a family.
|
||||||
let total: usize = FAMILIES.iter().map(|(_, ns)| ns.len()).sum();
|
let total: usize = FAMILIES.iter().map(|(_, ns)| ns.len()).sum();
|
||||||
assert_eq!(total, 488, "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_{t−period}) / OI_{t−period}
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// Where [`OIDelta`](crate::OIDelta) reports the single-tick change in open
|
||||||
|
/// interest, OI Momentum measures the trend in positioning over a window: positive
|
||||||
|
/// values mean open interest is expanding (new money entering — a position build
|
||||||
|
/// that fuels the prevailing move), negative values mean it is contracting
|
||||||
|
/// (positions being closed — deleveraging or short-covering). Read alongside price:
|
||||||
|
/// rising OI with rising price is a strong new-long trend, while rising price with
|
||||||
|
/// falling OI is a short-covering rally on borrowed time.
|
||||||
|
///
|
||||||
|
/// The output is a percentage and may be negative. A zero base open interest
|
||||||
|
/// `period` ticks ago reports `0` rather than dividing by zero. The first value
|
||||||
|
/// lands after `period + 1` inputs. Each `update` is O(1).
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use wickra_core::{DerivativesTick, Indicator, OpenInterestMomentum};
|
||||||
|
///
|
||||||
|
/// let mut indicator = OpenInterestMomentum::new(5).unwrap();
|
||||||
|
/// let mut last = None;
|
||||||
|
/// for i in 0..20 {
|
||||||
|
/// let oi = 1_000.0 + f64::from(i) * 100.0;
|
||||||
|
/// let tick = DerivativesTick::new(0.0, 100.0, 100.0, 100.0, oi, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0).unwrap();
|
||||||
|
/// last = indicator.update(tick);
|
||||||
|
/// }
|
||||||
|
/// assert!(last.unwrap() > 0.0); // expanding OI
|
||||||
|
/// ```
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct OpenInterestMomentum {
|
||||||
|
period: usize,
|
||||||
|
window: VecDeque<f64>,
|
||||||
|
last: Option<f64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OpenInterestMomentum {
|
||||||
|
/// Construct an OI Momentum over a `period`-tick lookback.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns [`Error::PeriodZero`] if `period == 0`.
|
||||||
|
pub fn new(period: usize) -> Result<Self> {
|
||||||
|
if period == 0 {
|
||||||
|
return Err(Error::PeriodZero);
|
||||||
|
}
|
||||||
|
Ok(Self {
|
||||||
|
period,
|
||||||
|
window: VecDeque::with_capacity(period + 1),
|
||||||
|
last: None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Configured lookback period.
|
||||||
|
pub const fn period(&self) -> usize {
|
||||||
|
self.period
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Current value if available.
|
||||||
|
pub const fn value(&self) -> Option<f64> {
|
||||||
|
self.last
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Indicator for OpenInterestMomentum {
|
||||||
|
type Input = DerivativesTick;
|
||||||
|
type Output = f64;
|
||||||
|
|
||||||
|
fn update(&mut self, tick: DerivativesTick) -> Option<f64> {
|
||||||
|
if self.window.len() == self.period + 1 {
|
||||||
|
self.window.pop_front();
|
||||||
|
}
|
||||||
|
self.window.push_back(tick.open_interest);
|
||||||
|
if self.window.len() < self.period + 1 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let base = *self.window.front().expect("non-empty");
|
||||||
|
let current = tick.open_interest;
|
||||||
|
let oim = if base > 0.0 {
|
||||||
|
100.0 * (current - base) / base
|
||||||
|
} else {
|
||||||
|
0.0
|
||||||
|
};
|
||||||
|
self.last = Some(oim);
|
||||||
|
Some(oim)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reset(&mut self) {
|
||||||
|
self.window.clear();
|
||||||
|
self.last = None;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn warmup_period(&self) -> usize {
|
||||||
|
self.period + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_ready(&self) -> bool {
|
||||||
|
self.last.is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
"OpenInterestMomentum"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::traits::BatchExt;
|
||||||
|
use approx::assert_relative_eq;
|
||||||
|
|
||||||
|
fn tick(oi: f64) -> DerivativesTick {
|
||||||
|
DerivativesTick::new_unchecked(
|
||||||
|
0.0, 100.0, 100.0, 100.0, oi, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rejects_zero_period() {
|
||||||
|
assert!(matches!(
|
||||||
|
OpenInterestMomentum::new(0),
|
||||||
|
Err(Error::PeriodZero)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn accessors_and_metadata() {
|
||||||
|
let o = OpenInterestMomentum::new(5).unwrap();
|
||||||
|
assert_eq!(o.period(), 5);
|
||||||
|
assert_eq!(o.warmup_period(), 6);
|
||||||
|
assert_eq!(o.name(), "OpenInterestMomentum");
|
||||||
|
assert!(!o.is_ready());
|
||||||
|
assert_eq!(o.value(), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn first_emission_at_warmup_period() {
|
||||||
|
let mut o = OpenInterestMomentum::new(3).unwrap();
|
||||||
|
let ticks: Vec<DerivativesTick> = (0..6)
|
||||||
|
.map(|i| tick(1_000.0 + f64::from(i) * 100.0))
|
||||||
|
.collect();
|
||||||
|
let out = o.batch(&ticks);
|
||||||
|
for v in out.iter().take(3) {
|
||||||
|
assert!(v.is_none());
|
||||||
|
}
|
||||||
|
assert!(out[3].is_some());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn reference_value() {
|
||||||
|
// period 2: OI 1000 -> 1200 over the window -> +20%.
|
||||||
|
let mut o = OpenInterestMomentum::new(2).unwrap();
|
||||||
|
let out = o.batch(&[tick(1_000.0), tick(1_100.0), tick(1_200.0)]);
|
||||||
|
assert_relative_eq!(out[2].unwrap(), 20.0, epsilon = 1e-9);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn expanding_oi_is_positive() {
|
||||||
|
let mut o = OpenInterestMomentum::new(5).unwrap();
|
||||||
|
let ticks: Vec<DerivativesTick> = (0..20)
|
||||||
|
.map(|i| tick(1_000.0 + f64::from(i) * 100.0))
|
||||||
|
.collect();
|
||||||
|
let last = o.batch(&ticks).into_iter().flatten().last().unwrap();
|
||||||
|
assert!(last > 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn contracting_oi_is_negative() {
|
||||||
|
let mut o = OpenInterestMomentum::new(5).unwrap();
|
||||||
|
let ticks: Vec<DerivativesTick> = (0..20)
|
||||||
|
.map(|i| tick(3_000.0 - f64::from(i) * 100.0))
|
||||||
|
.collect();
|
||||||
|
let last = o.batch(&ticks).into_iter().flatten().last().unwrap();
|
||||||
|
assert!(last < 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn zero_base_is_zero() {
|
||||||
|
let mut o = OpenInterestMomentum::new(2).unwrap();
|
||||||
|
let out = o.batch(&[tick(0.0), tick(100.0), tick(200.0)]);
|
||||||
|
assert_relative_eq!(out[2].unwrap(), 0.0, epsilon = 1e-12);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn reset_clears_state() {
|
||||||
|
let mut o = OpenInterestMomentum::new(3).unwrap();
|
||||||
|
o.batch(
|
||||||
|
&(0..10)
|
||||||
|
.map(|i| tick(1_000.0 + f64::from(i) * 50.0))
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
);
|
||||||
|
assert!(o.is_ready());
|
||||||
|
o.reset();
|
||||||
|
assert!(!o.is_ready());
|
||||||
|
assert_eq!(o.value(), None);
|
||||||
|
assert_eq!(o.update(tick(1_000.0)), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn batch_equals_streaming() {
|
||||||
|
let ticks: Vec<DerivativesTick> = (0..80)
|
||||||
|
.map(|i| tick(1_000.0 + (f64::from(i) * 0.25).sin() * 300.0))
|
||||||
|
.collect();
|
||||||
|
let batch = OpenInterestMomentum::new(10).unwrap().batch(&ticks);
|
||||||
|
let mut b = OpenInterestMomentum::new(10).unwrap();
|
||||||
|
let streamed: Vec<_> = ticks.iter().map(|x| b.update(*x)).collect();
|
||||||
|
assert_eq!(batch, streamed);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
//! Perpetual Premium Index — the perp mark price relative to spot.
|
||||||
|
|
||||||
|
use crate::derivatives::DerivativesTick;
|
||||||
|
use crate::traits::Indicator;
|
||||||
|
|
||||||
|
/// Perpetual Premium Index — the perpetual's mark price relative to the spot index
|
||||||
|
/// it tracks, as a fraction.
|
||||||
|
///
|
||||||
|
/// ```text
|
||||||
|
/// premium = (mark_price − index_price) / index_price
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// A perpetual swap is pegged to spot by the funding mechanism, but it can still
|
||||||
|
/// trade at a premium (above spot) or discount (below). A positive premium signals
|
||||||
|
/// net long demand willing to pay up to hold the perp — bullish positioning, and
|
||||||
|
/// the proximate driver of positive funding; a negative premium signals the
|
||||||
|
/// reverse. Sustained extremes flag crowded positioning ripe for a funding-driven
|
||||||
|
/// mean reversion.
|
||||||
|
///
|
||||||
|
/// The output is centred on zero and dimensionless (a fraction; multiply by `100`
|
||||||
|
/// for percent). `index_price` is validated strictly positive on the tick, so the
|
||||||
|
/// division is always defined. It is stateless — each tick yields one value (no
|
||||||
|
/// warmup). Each `update` is O(1).
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use wickra_core::{DerivativesTick, Indicator, PerpetualPremiumIndex};
|
||||||
|
///
|
||||||
|
/// let mut indicator = PerpetualPremiumIndex::new();
|
||||||
|
/// // Mark 101 vs index 100 -> +1% premium.
|
||||||
|
/// let tick = DerivativesTick::new(0.0, 101.0, 100.0, 101.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0).unwrap();
|
||||||
|
/// let premium = indicator.update(tick).unwrap();
|
||||||
|
/// assert!((premium - 0.01).abs() < 1e-12);
|
||||||
|
/// ```
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
pub struct PerpetualPremiumIndex {
|
||||||
|
ready: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PerpetualPremiumIndex {
|
||||||
|
/// Construct a new Perpetual Premium Index. The indicator is parameter-free.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn new() -> Self {
|
||||||
|
Self { ready: false }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Indicator for PerpetualPremiumIndex {
|
||||||
|
type Input = DerivativesTick;
|
||||||
|
type Output = f64;
|
||||||
|
|
||||||
|
fn update(&mut self, tick: DerivativesTick) -> Option<f64> {
|
||||||
|
let premium = (tick.mark_price - tick.index_price) / tick.index_price;
|
||||||
|
self.ready = true;
|
||||||
|
Some(premium)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reset(&mut self) {
|
||||||
|
self.ready = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn warmup_period(&self) -> usize {
|
||||||
|
1
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_ready(&self) -> bool {
|
||||||
|
self.ready
|
||||||
|
}
|
||||||
|
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
"PerpetualPremiumIndex"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::traits::BatchExt;
|
||||||
|
use approx::assert_relative_eq;
|
||||||
|
|
||||||
|
fn tick(mark: f64, index: f64) -> DerivativesTick {
|
||||||
|
DerivativesTick::new_unchecked(0.0, mark, index, mark, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn accessors_and_metadata() {
|
||||||
|
let p = PerpetualPremiumIndex::new();
|
||||||
|
assert_eq!(p.warmup_period(), 1);
|
||||||
|
assert_eq!(p.name(), "PerpetualPremiumIndex");
|
||||||
|
assert!(!p.is_ready());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn premium_reference_value() {
|
||||||
|
let mut p = PerpetualPremiumIndex::new();
|
||||||
|
assert_relative_eq!(p.update(tick(101.0, 100.0)).unwrap(), 0.01, epsilon = 1e-12);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn discount_is_negative() {
|
||||||
|
let mut p = PerpetualPremiumIndex::new();
|
||||||
|
assert!(p.update(tick(99.0, 100.0)).unwrap() < 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn at_par_is_zero() {
|
||||||
|
let mut p = PerpetualPremiumIndex::new();
|
||||||
|
assert_relative_eq!(p.update(tick(100.0, 100.0)).unwrap(), 0.0, epsilon = 1e-12);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ready_after_first_update() {
|
||||||
|
let mut p = PerpetualPremiumIndex::new();
|
||||||
|
assert!(!p.is_ready());
|
||||||
|
p.update(tick(100.0, 100.0));
|
||||||
|
assert!(p.is_ready());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn reset_clears_state() {
|
||||||
|
let mut p = PerpetualPremiumIndex::new();
|
||||||
|
p.update(tick(101.0, 100.0));
|
||||||
|
assert!(p.is_ready());
|
||||||
|
p.reset();
|
||||||
|
assert!(!p.is_ready());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn batch_equals_streaming() {
|
||||||
|
let ticks: Vec<DerivativesTick> = (0..40)
|
||||||
|
.map(|i| tick(100.0 + (f64::from(i) * 0.3).sin(), 100.0))
|
||||||
|
.collect();
|
||||||
|
let batch = PerpetualPremiumIndex::new().batch(&ticks);
|
||||||
|
let mut b = PerpetualPremiumIndex::new();
|
||||||
|
let streamed: Vec<_> = ticks.iter().map(|x| b.update(*x)).collect();
|
||||||
|
assert_eq!(batch, streamed);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -81,22 +81,23 @@ pub use indicators::{
|
|||||||
DoubleTopBottom, DownsideGapThreeMethods, Dpo, DragonflyDoji, DrawdownDuration, DumplingTop,
|
DoubleTopBottom, DownsideGapThreeMethods, Dpo, DragonflyDoji, DrawdownDuration, DumplingTop,
|
||||||
Dx, DynamicMomentumIndex, EaseOfMovement, EffectiveSpread, EhlersStochastic, Ehma,
|
Dx, DynamicMomentumIndex, EaseOfMovement, EffectiveSpread, EhlersStochastic, Ehma,
|
||||||
ElderImpulse, ElderRay, ElderRayOutput, ElderSafeZone, ElderSafeZoneOutput, Ema,
|
ElderImpulse, ElderRay, ElderRayOutput, ElderSafeZone, ElderSafeZoneOutput, Ema,
|
||||||
EmpiricalModeDecomposition, Engulfing, Equivolume, EquivolumeOutput, EvenBetterSinewave,
|
EmpiricalModeDecomposition, Engulfing, Equivolume, EquivolumeOutput, EstimatedLeverageRatio,
|
||||||
EveningDojiStar, Evwma, EwmaVolatility, Expectancy, FallingThreeMethods, Fama, FibArcs,
|
EvenBetterSinewave, EveningDojiStar, Evwma, EwmaVolatility, Expectancy, FallingThreeMethods,
|
||||||
FibArcsOutput, FibChannel, FibChannelOutput, FibConfluence, FibConfluenceOutput, FibExtension,
|
Fama, FibArcs, FibArcsOutput, FibChannel, FibChannelOutput, FibConfluence, FibConfluenceOutput,
|
||||||
FibExtensionOutput, FibFan, FibFanOutput, FibProjection, FibProjectionOutput, FibRetracement,
|
FibExtension, FibExtensionOutput, FibFan, FibFanOutput, FibProjection, FibProjectionOutput,
|
||||||
FibRetracementOutput, FibTimeZones, FibTimeZonesOutput, FibonacciPivots, FibonacciPivotsOutput,
|
FibRetracement, FibRetracementOutput, FibTimeZones, FibTimeZonesOutput, FibonacciPivots,
|
||||||
FisherRsi, FisherTransform, FlagPennant, Footprint, FootprintOutput, ForceIndex,
|
FibonacciPivotsOutput, FisherRsi, FisherTransform, FlagPennant, Footprint, FootprintOutput,
|
||||||
FractalChaosBands, FractalChaosBandsOutput, Frama, FryPanBottom, FundingBasis, FundingRate,
|
ForceIndex, FractalChaosBands, FractalChaosBandsOutput, Frama, FryPanBottom, FundingBasis,
|
||||||
FundingRateMean, FundingRateZScore, GainLossRatio, GapSideBySideWhite, Garch11,
|
FundingImpliedApr, FundingRate, FundingRateMean, FundingRateZScore, GainLossRatio,
|
||||||
GarmanKlassVolatility, Gartley, GatorOscillator, GatorOscillatorOutput, GeneralizedDema,
|
GapSideBySideWhite, Garch11, GarmanKlassVolatility, Gartley, GatorOscillator,
|
||||||
GeometricMa, GoldenPocket, GoldenPocketOutput, GrangerCausality, GravestoneDoji, Hammer,
|
GatorOscillatorOutput, GeneralizedDema, GeometricMa, GoldenPocket, GoldenPocketOutput,
|
||||||
HangingMan, Harami, HaramiCross, HasbrouckInformationShare, HeadAndShoulders, HeikinAshi,
|
GrangerCausality, GravestoneDoji, Hammer, HangingMan, Harami, HaramiCross,
|
||||||
HeikinAshiOscillator, HeikinAshiOutput, HiLoActivator, HighLowIndex, HighLowRange, HighWave,
|
HasbrouckInformationShare, HeadAndShoulders, HeikinAshi, HeikinAshiOscillator,
|
||||||
HighpassFilter, Hikkake, HikkakeModified, HilbertDominantCycle, HistoricalVolatility, Hma,
|
HeikinAshiOutput, HiLoActivator, HighLowIndex, HighLowRange, HighWave, HighpassFilter, Hikkake,
|
||||||
HoltWinters, HomingPigeon, HtDcPhase, HtPhasor, HtPhasorOutput, HtTrendMode, HurstChannel,
|
HikkakeModified, HilbertDominantCycle, HistoricalVolatility, Hma, HoltWinters, HomingPigeon,
|
||||||
HurstChannelOutput, HurstExponent, Ichimoku, IchimokuOutput, IdenticalThreeCrows, InNeck,
|
HtDcPhase, HtPhasor, HtPhasorOutput, HtTrendMode, HurstChannel, HurstChannelOutput,
|
||||||
Inertia, InformationRatio, InitialBalance, InitialBalanceOutput, InstantaneousTrendline,
|
HurstExponent, Ichimoku, IchimokuOutput, IdenticalThreeCrows, InNeck, Inertia,
|
||||||
|
InformationRatio, InitialBalance, InitialBalanceOutput, InstantaneousTrendline,
|
||||||
IntradayIntensity, IntradayMomentumIndex, IntradayVolatilityProfile,
|
IntradayIntensity, IntradayMomentumIndex, IntradayVolatilityProfile,
|
||||||
IntradayVolatilityProfileOutput, InverseFisherTransform, InvertedHammer, JarqueBera, Jma,
|
IntradayVolatilityProfileOutput, InverseFisherTransform, InvertedHammer, JarqueBera, Jma,
|
||||||
JumpIndicator, KagiBars, KalmanHedgeRatio, KalmanHedgeRatioOutput, Kama, KaseDevStop,
|
JumpIndicator, KagiBars, KalmanHedgeRatio, KalmanHedgeRatioOutput, Kama, KaseDevStop,
|
||||||
@@ -112,12 +113,13 @@ pub use indicators::{
|
|||||||
MedianPrice, Mfi, Microprice, MidPoint, MidPrice, MinusDi, MinusDm, ModifiedMaStop,
|
MedianPrice, Mfi, Microprice, MidPoint, MidPrice, MinusDi, MinusDm, ModifiedMaStop,
|
||||||
ModifiedMaStopOutput, Mom, MorningDojiStar, MorningEveningStar, MurreyMathLines,
|
ModifiedMaStopOutput, Mom, MorningDojiStar, MorningEveningStar, MurreyMathLines,
|
||||||
MurreyMathLinesOutput, Natr, NewHighsNewLows, NewPriceLines, Nrtr, NrtrOutput, Nvi,
|
MurreyMathLinesOutput, Natr, NewHighsNewLows, NewPriceLines, Nrtr, NrtrOutput, Nvi,
|
||||||
OIPriceDivergence, OIWeighted, Obv, OmegaRatio, OnNeck, OpenInterestDelta, OpeningMarubozu,
|
OIPriceDivergence, OIWeighted, Obv, OiToVolumeRatio, OmegaRatio, OnNeck, OpenInterestDelta,
|
||||||
OpeningRange, OpeningRangeOutput, OrderBookImbalanceFull, OrderBookImbalanceTop1,
|
OpenInterestMomentum, OpeningMarubozu, OpeningRange, OpeningRangeOutput,
|
||||||
OrderBookImbalanceTopN, OrderFlowImbalance, OuHalfLife, OvernightGap, OvernightIntradayReturn,
|
OrderBookImbalanceFull, OrderBookImbalanceTop1, OrderBookImbalanceTopN, OrderFlowImbalance,
|
||||||
OvernightIntradayReturnOutput, PainIndex, PairSpreadZScore, PairwiseBeta, ParkinsonVolatility,
|
OuHalfLife, OvernightGap, OvernightIntradayReturn, OvernightIntradayReturnOutput, PainIndex,
|
||||||
PearsonCorrelation, PercentAboveMa, PercentB, PercentageTrailingStop, Pgo, PiercingDarkCloud,
|
PairSpreadZScore, PairwiseBeta, ParkinsonVolatility, PearsonCorrelation, PercentAboveMa,
|
||||||
Pin, PivotReversal, PlusDi, PlusDm, Pmo, PointAndFigureBars, PolarizedFractalEfficiency, Ppo,
|
PercentB, PercentageTrailingStop, PerpetualPremiumIndex, Pgo, PiercingDarkCloud, Pin,
|
||||||
|
PivotReversal, PlusDi, PlusDm, Pmo, PointAndFigureBars, PolarizedFractalEfficiency, Ppo,
|
||||||
PpoHistogram, ProfitFactor, ProjectionBands, ProjectionBandsOutput, ProjectionOscillator, Psar,
|
PpoHistogram, ProfitFactor, ProjectionBands, ProjectionBandsOutput, ProjectionOscillator, Psar,
|
||||||
Pvi, Qqe, QqeOutput, Qstick, QuartileBands, QuartileBandsOutput, QuotedSpread, RSquared,
|
Pvi, Qqe, QqeOutput, Qstick, QuartileBands, QuartileBandsOutput, QuotedSpread, RSquared,
|
||||||
RealizedSpread, RealizedVolatility, RecoveryFactor, RectangleRange, Reflex, RegimeLabel,
|
RealizedSpread, RealizedVolatility, RecoveryFactor, RectangleRange, Reflex, RegimeLabel,
|
||||||
|
|||||||
+1
-1
@@ -8,7 +8,7 @@ That includes:
|
|||||||
[Python](https://docs.wickra.org/Quickstart-Python),
|
[Python](https://docs.wickra.org/Quickstart-Python),
|
||||||
[Node](https://docs.wickra.org/Quickstart-Node), and
|
[Node](https://docs.wickra.org/Quickstart-Node), and
|
||||||
[WASM](https://docs.wickra.org/Quickstart-WASM).
|
[WASM](https://docs.wickra.org/Quickstart-WASM).
|
||||||
- A per-indicator deep dive for every one of the **488 indicators** across
|
- A per-indicator deep dive for every one of the **493 indicators** across
|
||||||
the sixteen families (Moving Averages, Momentum Oscillators, Trend &
|
the sixteen families (Moving Averages, Momentum Oscillators, Trend &
|
||||||
Directional, Price Oscillators, Volatility & Bands, Bands & Channels,
|
Directional, Price Oscillators, Volatility & Bands, Bands & Channels,
|
||||||
Trailing Stops, Volume, Price Statistics, Ehlers / Cycle DSP, Pivots &
|
Trailing Stops, Volume, Price Statistics, Ehlers / Cycle DSP, Pivots &
|
||||||
|
|||||||
@@ -10,11 +10,7 @@
|
|||||||
//! never panic, streaming or batched.
|
//! never panic, streaming or batched.
|
||||||
|
|
||||||
use libfuzzer_sys::fuzz_target;
|
use libfuzzer_sys::fuzz_target;
|
||||||
use wickra_core::{
|
use wickra_core::{BatchExt, CalendarSpread, DerivativesTick, EstimatedLeverageRatio, FundingBasis, FundingImpliedApr, FundingRate, FundingRateMean, FundingRateZScore, Indicator, LiquidationFeatures, LongShortRatio, OIPriceDivergence, OIWeighted, OiToVolumeRatio, OpenInterestDelta, OpenInterestMomentum, PerpetualPremiumIndex, TakerBuySellRatio, TermStructureBasis};
|
||||||
BatchExt, CalendarSpread, DerivativesTick, FundingBasis, FundingRate, FundingRateMean,
|
|
||||||
FundingRateZScore, Indicator, LiquidationFeatures, LongShortRatio, OIPriceDivergence,
|
|
||||||
OIWeighted, OpenInterestDelta, TakerBuySellRatio, TermStructureBasis,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[inline(never)]
|
#[inline(never)]
|
||||||
fn drive<I>(make: impl Fn() -> I, ticks: &[DerivativesTick])
|
fn drive<I>(make: impl Fn() -> I, ticks: &[DerivativesTick])
|
||||||
@@ -53,6 +49,11 @@ fuzz_target!(|data: &[u8]| {
|
|||||||
drive(TakerBuySellRatio::new, &ticks);
|
drive(TakerBuySellRatio::new, &ticks);
|
||||||
drive(TermStructureBasis::new, &ticks);
|
drive(TermStructureBasis::new, &ticks);
|
||||||
drive(CalendarSpread::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.
|
// LiquidationFeatures emits a struct, not an f64, so drive it directly.
|
||||||
let mut liq = LiquidationFeatures::new();
|
let mut liq = LiquidationFeatures::new();
|
||||||
|
|||||||
Reference in New Issue
Block a user