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:
@@ -6331,6 +6331,134 @@ wasm_candle_pattern!(WasmSpinningTop, wc::SpinningTop, SpinningTop);
|
||||
wasm_candle_pattern!(WasmThreeInside, wc::ThreeInside, ThreeInside);
|
||||
wasm_candle_pattern!(WasmThreeOutside, wc::ThreeOutside, ThreeOutside);
|
||||
|
||||
// ============================== Microstructure: Order Book ==============================
|
||||
//
|
||||
// Order-book indicators consume a depth snapshot rather than OHLCV. Each
|
||||
// `update(bidPx, bidSz, askPx, askSz)` takes four equal-length typed arrays for
|
||||
// one snapshot (bids best-first = descending price, asks best-first = ascending
|
||||
// price) — the streaming model that fits a live browser book feed. Batch over a
|
||||
// ragged depth history is provided by the Python and Node bindings.
|
||||
|
||||
fn build_order_book(
|
||||
bid_px: &[f64],
|
||||
bid_sz: &[f64],
|
||||
ask_px: &[f64],
|
||||
ask_sz: &[f64],
|
||||
) -> Result<wc::OrderBook, JsError> {
|
||||
if bid_px.len() != bid_sz.len() || ask_px.len() != ask_sz.len() {
|
||||
return Err(JsError::new(
|
||||
"bid/ask price and size arrays must be equal length",
|
||||
));
|
||||
}
|
||||
let bids = bid_px
|
||||
.iter()
|
||||
.zip(bid_sz)
|
||||
.map(|(&p, &s)| wc::Level::new_unchecked(p, s))
|
||||
.collect();
|
||||
let asks = ask_px
|
||||
.iter()
|
||||
.zip(ask_sz)
|
||||
.map(|(&p, &s)| wc::Level::new_unchecked(p, s))
|
||||
.collect();
|
||||
wc::OrderBook::new(bids, asks).map_err(map_err)
|
||||
}
|
||||
|
||||
macro_rules! wasm_ob_indicator {
|
||||
($wasm:ident, $inner:ty, $js:ident) => {
|
||||
#[wasm_bindgen(js_name = $js)]
|
||||
pub struct $wasm {
|
||||
inner: $inner,
|
||||
}
|
||||
|
||||
impl Default for $wasm {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_class = $js)]
|
||||
impl $wasm {
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new() -> $wasm {
|
||||
Self {
|
||||
inner: <$inner>::new(),
|
||||
}
|
||||
}
|
||||
pub fn update(
|
||||
&mut self,
|
||||
bid_px: &[f64],
|
||||
bid_sz: &[f64],
|
||||
ask_px: &[f64],
|
||||
ask_sz: &[f64],
|
||||
) -> Result<Option<f64>, JsError> {
|
||||
let book = build_order_book(bid_px, bid_sz, ask_px, ask_sz)?;
|
||||
Ok(self.inner.update(book))
|
||||
}
|
||||
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()
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
wasm_ob_indicator!(
|
||||
WasmOrderBookImbalanceTop1,
|
||||
wc::OrderBookImbalanceTop1,
|
||||
OrderBookImbalanceTop1
|
||||
);
|
||||
wasm_ob_indicator!(
|
||||
WasmOrderBookImbalanceFull,
|
||||
wc::OrderBookImbalanceFull,
|
||||
OrderBookImbalanceFull
|
||||
);
|
||||
wasm_ob_indicator!(WasmMicroprice, wc::Microprice, Microprice);
|
||||
wasm_ob_indicator!(WasmQuotedSpread, wc::QuotedSpread, QuotedSpread);
|
||||
|
||||
// Top-N imbalance carries a `levels` parameter, so it is hand-written.
|
||||
#[wasm_bindgen(js_name = OrderBookImbalanceTopN)]
|
||||
pub struct WasmOrderBookImbalanceTopN {
|
||||
inner: wc::OrderBookImbalanceTopN,
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_class = OrderBookImbalanceTopN)]
|
||||
impl WasmOrderBookImbalanceTopN {
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(levels: usize) -> Result<WasmOrderBookImbalanceTopN, JsError> {
|
||||
Ok(Self {
|
||||
inner: wc::OrderBookImbalanceTopN::new(levels).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
pub fn update(
|
||||
&mut self,
|
||||
bid_px: &[f64],
|
||||
bid_sz: &[f64],
|
||||
ask_px: &[f64],
|
||||
ask_sz: &[f64],
|
||||
) -> Result<Option<f64>, JsError> {
|
||||
let book = build_order_book(bid_px, bid_sz, ask_px, ask_sz)?;
|
||||
Ok(self.inner.update(book))
|
||||
}
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
Reference in New Issue
Block a user