feat: order-book microstructure indicators (part 1 of 4) (#112)

* feat(core): add microstructure input types (OrderBook, Trade, TradeQuote)

New non-OHLCV value types for the order-book / trade-flow indicator family:
Level, OrderBook (sorted, uncrossed depth snapshot), Side, Trade (with
aggressor side), and TradeQuote (trade paired with prevailing mid). Each has a
validating constructor plus a new_unchecked hot-path constructor, with full
unit coverage. Adds InvalidOrderBook / InvalidTrade error variants.

* feat(core): add 5 order-book microstructure indicators

OrderBookImbalanceTop1/TopN/Full (signed depth imbalance), Microprice
(size-weighted fair value), and QuotedSpread (top-of-book spread in bps). All
consume the OrderBook snapshot type, emit f64, are stateless and ready after
the first snapshot, with full unit coverage. Registers a new Microstructure
family in the taxonomy.

* feat(bindings): expose order-book microstructure indicators

Python, Node, and WASM bindings for OrderBookImbalanceTop1/TopN/Full,
Microprice and QuotedSpread. Each takes a depth snapshot via four equal-length
(bid_px, bid_sz, ask_px, ask_sz) arrays. Python and Node expose a batch over a
list of snapshots; WASM exposes per-snapshot update (the streaming model that
fits a browser book feed). Regenerates node index.d.ts/.js and registers the
new InvalidOrderBook/InvalidTrade arms in the Python error mapping.

* test(bindings,fuzz): cover order-book microstructure indicators

Python: smoke, reference values, streaming-vs-batch, lifecycle/repr and input
validation (mismatched lengths, crossed book, misordered levels, zero levels)
for all five order-book indicators. Node: reference values, streaming-vs-batch,
and rejection cases. Adds an indicator_update_orderbook fuzz target driving
every order-book indicator over arbitrary (incl. degenerate) snapshots.

* bench(microstructure): synthetic order-book benchmarks

Add a bench_orderbook_input harness and synthesise a five-level book around
each candle close (no order-book dataset ships with the repo). Benches the
cheapest (top-of-book imbalance) and most-expensive (full-depth imbalance) plus
microprice, matching the curated cheapest/expensive-per-family approach.

* docs: add Microstructure family + bump indicator counter to 224

README gains the Microstructure family row (order-book imbalance, microprice,
quoted spread) and the indicator counter goes 219 -> 224 across seventeen
families; CHANGELOG records the new order-book indicators and value types.
This commit is contained in:
kingchenc
2026-06-01 16:06:22 +02:00
committed by GitHub
parent 498b74a5ae
commit 2be21df803
27 changed files with 2189 additions and 21 deletions
+7
View File
@@ -52,6 +52,13 @@ test = false
doc = false
bench = false
[[bin]]
name = "indicator_update_orderbook"
path = "fuzz_targets/indicator_update_orderbook.rs"
test = false
doc = false
bench = false
[[bin]]
name = "tick_aggregator"
path = "fuzz_targets/tick_aggregator.rs"
@@ -0,0 +1,54 @@
#![no_main]
//! Fuzz order-book `Indicator<Input = OrderBook>` implementations with
//! arbitrary depth snapshots.
//!
//! Each iteration consumes a byte stream, interprets it as a sequence of
//! `f64` values (8 bytes each), packs consecutive values into `(price, size)`
//! levels, and groups levels into order-book snapshots. Books are built with
//! `OrderBook::new_unchecked` so the fuzzer can explore degenerate shapes
//! (empty sides, crossed books, non-finite prices, negative sizes) that the
//! validating constructor would reject — the indicators must never panic on
//! any of them, streaming or batched.
use libfuzzer_sys::fuzz_target;
use wickra_core::{
BatchExt, Indicator, Level, Microprice, OrderBook, OrderBookImbalanceFull,
OrderBookImbalanceTop1, OrderBookImbalanceTopN, QuotedSpread,
};
#[inline(never)]
fn drive<I>(make: impl Fn() -> I, books: &[OrderBook])
where
I: Indicator<Input = OrderBook, Output = f64> + BatchExt,
{
let mut streaming = make();
for book in books {
let _ = streaming.update(book.clone());
}
let _ = make().batch(books);
}
fuzz_target!(|data: &[u8]| {
let floats: Vec<f64> = data
.chunks_exact(8)
.map(|c| f64::from_le_bytes(c.try_into().expect("8 bytes")))
.collect();
let levels: Vec<Level> = floats
.chunks_exact(2)
.map(|c| Level::new_unchecked(c[0], c[1]))
.collect();
// Group levels into snapshots of up to four levels (split into bids / asks).
let books: Vec<OrderBook> = levels
.chunks(4)
.map(|chunk| {
let half = chunk.len() / 2;
OrderBook::new_unchecked(chunk[..half].to_vec(), chunk[half..].to_vec())
})
.collect();
drive(OrderBookImbalanceTop1::new, &books);
drive(|| OrderBookImbalanceTopN::new(3).unwrap(), &books);
drive(OrderBookImbalanceFull::new, &books);
drive(Microprice::new, &books);
drive(QuotedSpread::new, &books);
});