orderflow-metrics
v0.32.0
Published
Microstructure metrics in dependency-free TypeScript — OFI, VPIN, information-driven bars, market impact (square-root & Almgren-Chriss), markouts, implementation shortfall, high-low spread estimators (Corwin-Schultz, Abdi-Ranaldo), range-based volatility
Maintainers
Readme
orderflow-metrics
Microstructure order-flow metrics in dependency-free TypeScript and Python: Order Flow Imbalance (OFI), VPIN, information-driven bars, transaction-cost / price-impact metrics, trade-sign classification, limit-order-book reconstruction and execution scheduling. The npm package ships as compiled ESM with type declarations (Node ≥ 18) and has zero runtime dependencies; the TypeScript source is also vendorable directly (Node 22+ type-stripping, no build step).
Metrics
- Order flow & imbalance — Order Flow Imbalance · Imbalance · VPIN · Trade-sign classification · Order-flow entropy
- Bars & sampling — Information-driven bars
- Fair value & spreads — Fair value · Spread estimators (OHLC)
- Execution & impact — Execution cost & price impact · Market impact · Implementation shortfall · Execution scheduling
- Order book — Order book
- Volatility & risk — Volatility · Range-based volatility (OHLC) · Realized moments · Jumps & bipower variation · Realized semivariance
- Market efficiency — Market efficiency · Hurst exponent · Mean reversion (half-life & z-score)
- Liquidity — Liquidity
- Streaming — Online / streaming estimators
- Cross-asset — Realized covariance, correlation & beta
Runnable quickstarts live in examples/.
Install
npm install orderflow-metrics
# or vendor the src/ directory directly — it's tiny and dependency-freeUsage
import { ofi, depthImbalance, tradeImbalance } from "orderflow-metrics";
// Order Flow Imbalance across a stream of best-quote updates
const quotes = [
{ bidPrice: 100, bidSize: 5, askPrice: 101, askSize: 4 },
{ bidPrice: 100, bidSize: 8, askPrice: 101, askSize: 1 },
{ bidPrice: 100.5, bidSize: 2, askPrice: 101, askSize: 1 },
];
ofi(quotes); // 8 (net buy-side pressure)
depthImbalance(quotes[0]); // (5 - 4) / (5 + 4) ≈ 0.111
tradeImbalance([
{ price: 100, size: 2, side: "buy" },
{ price: 100, size: 1, side: "sell" },
]); // 0.333Examples
Runnable, dependency-free quickstarts that tour the library end-to-end on
deterministic synthetic data live in examples/:
node --experimental-strip-types examples/quickstart.ts # TypeScript
python examples/quickstart.py # PythonBoth use the same seeded data and print the same numbers — a quick check that the two ports agree.
Order Flow Imbalance
ofi implements the level-1 OFI of Cont, Kukanov & Stoikov (2014). For two
consecutive best-quote observations the event contribution is:
e_n = q_bid_n · 1{P_bid_n ≥ P_bid_{n-1}} − q_bid_{n-1} · 1{P_bid_n ≤ P_bid_{n-1}}
− q_ask_n · 1{P_ask_n ≤ P_ask_{n-1}} + q_ask_{n-1} · 1{P_ask_n ≥ P_ask_{n-1}}OFI over a window is the sum of e_n. Intuitively it counts size added to the
bid and removed from the ask (buy pressure) against the reverse. Empirically it
is a strong linear predictor of short-horizon price changes.
ofiContribution(prev, curr)— one transitionofiSeries(quotes)— per-step contributions (for bucketing / regression)ofi(quotes)— cumulative
Imbalance
depthImbalance(quote)—(bidSize − askSize) / (bidSize + askSize), in[-1, 1]tradeImbalance(trades)—(buyVol − sellVol) / (buyVol + sellVol), in[-1, 1]
VPIN
vpin implements Volume-Synchronized Probability of Informed Trading (Easley,
López de Prado & O'Hara, 2012). Trades are grouped into equal-volume buckets;
each bucket is split into buy/sell volume by Bulk Volume Classification (BVC)
from the standardized price change, and VPIN is the average absolute imbalance
across a rolling window.
import { bucketByVolume, vpin } from "orderflow-metrics";
const buckets = bucketByVolume(trades, 1_000); // equal-volume buckets
vpin(buckets, { window: 50 }); // flow toxicity in [0, 1]bucketByVolume(trades, bucketSize)— split a trade stream into equal-volume bucketsbvcBuyFraction(priceChange, sigma)— BVC buy fraction Φ(ΔP/σ)vpin(buckets, { window, sigma })— VPIN over the lastwindowbucketsstandardNormalCdf(z)— Φ, the standard normal CDF (Abramowitz & Stegun 7.1.26)
Execution cost & price impact
Transaction-cost analysis (TCA) building blocks (buys +1, sells −1):
import { effectiveSpread, realizedSpread, priceImpact, kyleLambda } from "orderflow-metrics";
effectiveSpread(101, 100, "buy"); // 2 — cost vs the midpoint
realizedSpread(101, 100.5, "buy"); // 1 — LP revenue after reversion
priceImpact(100, 100.5, "buy"); // 1 — permanent impact (effective − realized)
kyleLambda([ // price impact per unit signed flow
{ signedVolume: 2, priceChange: 1 },
{ signedVolume: -2, priceChange: -1 },
]); // 0.5effectiveSpread/effectiveHalfSpread— realized cost vs the quote midrealizedSpread— post-trade reversion componentpriceImpact— permanent impactkyleLambda— OLS impact slope of ΔP on signed volumerollSpread— Roll's (1984) spread from price-change autocovariance
Execution quality (vs the quote)
Measure a fill against the quote it faced — the SEC Rule 605 / TCA view:
import { quotedSpread, priceImprovement, effectiveToQuotedRatio } from "orderflow-metrics";
quotedSpread(99.98, 100.02); // 0.04 — width of the market
priceImprovement(100.01, 99.98, 100.02, "buy"); // 0.01 — filled inside the ask
effectiveToQuotedRatio(0.02, 0.04); // 0.5 — traded at half the quoted spreadquotedSpread/quotedHalfSpread— width of the marketpriceImprovement— how far inside the quote a fill landed (signed by side)effectiveToQuotedRatio— effective ÷ quoted; <1 = price improvement, >1 = walked the book
Fair value
import { weightedMid, relativeSpreadBps } from "orderflow-metrics";
weightedMid({ bidPrice: 100, bidSize: 9, askPrice: 101, askSize: 1 }); // ~100.9 — heavy bid pulls toward ask
relativeSpreadBps({ bidPrice: 99.99, bidSize: 1, askPrice: 100.01, askSize: 1 }); // 2 (bps)weightedMid— imbalance-weighted mid (a simple micro-price)mid— arithmetic midrelativeSpreadBps— quoted spread in basis points
Trade-sign classification
Public prints rarely say who was the aggressor. Infer it so OFI / imbalance / VPIN inputs can be signed (+1 buyer-initiated, −1 seller-initiated, 0 unknown):
import { tickRule, leeReady } from "orderflow-metrics";
tickRule([100, 101, 101, 100]); // [0, 1, 1, -1]
leeReady([{ price: 101, mid: 100 }, { price: 99, mid: 100 }]); // [1, -1]tickRule— sign from the change vs the previous price (zero ticks carry)leeReady— Lee-Ready (1991): quote rule, with the tick rule breaking ties
Liquidity
import { amihudIlliquidity } from "orderflow-metrics";
amihudIlliquidity([
{ ret: 0.02, volume: 100 },
{ ret: -0.01, volume: 50 },
]); // 0.0002 — price move per unit of volume; higher = thinneramihudIlliquidity— Amihud (2002): average |return| / volume across periods
Volatility
import { realizedVolatility, annualizedVolatility } from "orderflow-metrics";
realizedVolatility([0.03, 0.04]); // 0.05 — √(Σ rᵢ²)
annualizedVolatility(minuteReturns, 252 * 390); // scaled to a yearrealizedVariance— Σ rᵢ²realizedVolatility— √ of the realized varianceannualizedVolatility— √( mean(rᵢ²) · periodsPerYear )
Market efficiency
import { varianceRatio, autocorrelation } from "orderflow-metrics";
varianceRatio(returns, 2); // <1 mean-reverting · ~1 random walk · >1 trending
autocorrelation(returns, 1); // lag-1 return autocorrelationvarianceRatio— Lo-MacKinlay variance ratio over overlapping q-period returnsautocorrelation— lag-k autocorrelation of a return series
Order book
Reconstruct a limit order book from incremental level updates and read the usual top-of-book / depth signals:
import { OrderBook } from "orderflow-metrics";
const ob = new OrderBook();
ob.update("bid", 100, 5);
ob.update("ask", 101, 3);
ob.bestBid(); // { price: 100, size: 5 }
ob.mid(); // 100.5
ob.spread(); // 1
ob.imbalance(1); // 0.25 — top-of-book bid/ask size imbalance
ob.update("bid", 100, 0); // size 0 removes the levelupdate(side, price, size)·bestBid/bestAsk·mid·spreaddepth(side, n)— top n levels ·imbalance(n)— depth imbalance in[-1, 1]
Market-order simulation
Sweep the book with a market order and see the real fill — VWAP price, slippage and any unfilled size (read-only, the book isn't touched):
import { simulateMarketOrder } from "orderflow-metrics";
const r = simulateMarketOrder(ob, "buy", 4);
r.avgPrice; // volume-weighted fill price
r.slippageBps; // cost vs mid, in basis points
r.remainingSize; // > 0 if the book was too thinBook-depth liquidity
Read liquidity off a book snapshot — near-touch depth, how steeply the book
thickens away from mid, and the round-trip cost of a given size. Take plain
Level[] arrays sorted best-first (bids high→low, asks low→high):
import { depthWithin, orderBookSlope, costOfRoundTrip } from "orderflow-metrics";
const bids = [{ price: 99.95, size: 6 }, { price: 99.9, size: 10 }];
const asks = [{ price: 100.0, size: 5 }, { price: 100.05, size: 8 }];
depthWithin(bids, asks, 10); // { bidDepth, askDepth, total } within ±10 bps of mid
orderBookSlope(asks, 99.975); // cumulative size per unit of relative price move
costOfRoundTrip(bids, asks, 15); // { roundTripBps, avgBuyPrice, avgSellPrice, filledSize }depthWithin— resting size within ±bps of mid, split by sideorderBookSlope— (Σ size) / (relative distance to the outermost level)costOfRoundTrip— basis-point liquidity tax of buying then sellingsize
Execution scheduling
Split a parent order into child slices:
import { twap, pov } from "orderflow-metrics";
twap(100, 4); // [25, 25, 25, 25] — even time slices
pov(30, [100, 100, 100], 0.1); // [10, 10, 10] — 10% of each interval's volumetwap— time-weighted: even slices that sum exactly to the parent sizepov— percentage-of-volume: participate at a fixed fraction of each interval
Information-driven bars
Sampling trades on a fixed time grid oversamples quiet periods and produces non-IID returns. Sampling on activity instead — a bar every N ticks, N units of volume, or N units of traded value — gives bars with much better statistical properties (López de Prado, Advances in Financial ML, ch. 2). Build them first, then run the other metrics on the resulting series.
import { tickBars, volumeBars, dollarBars } from "orderflow-metrics";
const trades = [
{ price: 100, size: 3, side: "buy" },
{ price: 101, size: 4, side: "buy" },
{ price: 100, size: 2, side: "sell" },
{ price: 102, size: 5, side: "sell" },
];
tickBars(trades, 2); // one bar per 2 trades
volumeBars(trades, 5); // new bar each time cumulative size ≥ 5
dollarBars(trades, 500); // new bar each time cumulative price·size ≥ 500Each Bar carries open/high/low/close, volume, dollar (traded
value), vwap, ticks, and signed buyVolume / sellVolume (plus start /
end timestamps when the feed provides them). The trade that crosses the
threshold is included whole (never split), and a trailing partial bar is
dropped. Dollar bars are usually preferred — they are the most robust of the
three to changes in price level.
tickBars(trades, threshold)— a bar everythresholdtradesvolumeBars(trades, threshold)— a bar everythresholdunits of volumedollarBars(trades, threshold)— a bar everythresholdunits of traded value
Market impact
Pre-trade cost models and post-trade markouts:
import { squareRootImpact, almgrenChrissCost, markout } from "orderflow-metrics";
squareRootImpact(0.02, 1_000, 1_000_000); // Y·σ·√(Q/V) — empirical impact
almgrenChrissCost(10_000, 30, 1e-6, 2e-7); // { permanent, temporary, total }
markout("buy", 100, 100.5); // +0.5 — price moved with the tradesquareRootImpact— the empirical square-root law of impactlinearPermanentImpact/linearTemporaryImpact— Almgren-Chriss impact termsalmgrenChrissCost— expected TWAP cost, split into permanent vs temporarymarkout/averageMarkout— realized post-trade adverse-selection drift
Implementation shortfall
Execution-quality analytics against a decision / arrival benchmark:
import { implementationShortfall, arrivalSlippageBps } from "orderflow-metrics";
implementationShortfall("buy", 100, 100.5, 800, 1000, 101, 5);
// { execution: 400, opportunity: 200, fees: 5, total: 605 }
arrivalSlippageBps("buy", 100, 100.5); // 50 bps paid up vs arrivalimplementationShortfall— Perold's execution + opportunity + fees decompositionarrivalSlippageBps— signed slippage of the fill vs the arrival price
Spread estimators (from OHLC)
Recover the effective bid-ask spread when all you have is daily high, low, and close — no tick data required:
import { corwinSchultz, abdiRanaldo } from "orderflow-metrics";
const bars = [
{ high: 10.2, low: 9.8, close: 10.18 },
{ high: 10.25, low: 9.85, close: 9.88 },
{ high: 10.3, low: 9.9, close: 10.27 },
];
corwinSchultz(bars); // proportional spread from the two-day high-low range
abdiRanaldo(bars); // proportional spread from close vs high-low mid-rangecorwinSchultz— Corwin & Schultz (2012) high-low estimatorabdiRanaldo— Abdi & Ranaldo (2017) close/high/low estimator
Both return a proportional spread (a fraction of price); negative estimates are floored at 0.
Range-based volatility (from OHLC)
Estimate volatility from the open, high, low, and close — far more efficient than close-to-close when you have candles:
import {
parkinsonVolatility,
garmanKlassVolatility,
rogersSatchellVolatility,
yangZhangVolatility,
} from "orderflow-metrics";
const candles = [
{ open: 100, high: 105, low: 99, close: 102 },
{ open: 102, high: 106, low: 101, close: 104 },
{ open: 104, high: 104, low: 98, close: 99 },
];
parkinsonVolatility(candles); // high-low range
garmanKlassVolatility(candles); // adds open & close
rogersSatchellVolatility(candles); // drift-independent
yangZhangVolatility(candles); // + overnight jumps (needs >= 3 bars)parkinsonVolatility— Parkinson (1980), high-low rangegarmanKlassVolatility— Garman & Klass (1980), OHLCrogersSatchellVolatility— Rogers & Satchell (1991), drift-independentyangZhangVolatility— Yang & Zhang (2000), drift- and jump-robust
Each returns the volatility (standard deviation) per bar; multiply the variance by bars-per-year to annualize.
Hurst exponent
Detect long-memory — trending vs mean-reverting — from a return series via rescaled-range (R/S) analysis:
import { hurstExponent } from "orderflow-metrics";
hurstExponent(returns);
// ~0.5 random walk · >0.5 persistent/trending · <0.5 mean-reverting
// NaN if the series is too short (needs ~32+ points)hurstExponent— R/S Hurst estimate; a companion tovarianceRatioandautocorrelationfor gauging market efficiency
Mean reversion (half-life & z-score)
Quantify how fast a spread or pair residual reverts and how far from home it sits right now — the Ornstein–Uhlenbeck timescale a pairs / stat-arb strategy trades on:
import { meanReversionSpeed, halfLife, zScore } from "orderflow-metrics";
const spread = [10.0, 10.6, 10.1, 9.7, 10.2, 9.8, 10.3, 9.9];
meanReversionSpeed(spread); // κ ≈ 1.393 per step (>0 reverting · <0 trending)
halfLife(spread); // ≈ 0.498 steps to decay halfway (Infinity if κ ≤ 0)
zScore(spread); // ≈ -0.642 — latest point sits below the meanmeanReversionSpeed— OU reversion speedκ, the negated OLS slope of the changeΔyₜon the lagged levelyₜ₋₁halfLife—ln 2 / κ, the number of steps a deviation takes to revert halfway;Infinitywhen the series does not mean-revertzScore— the latest observation as a standardized deviation from the sample mean (population σ), the raw entry/exit signal
Operates on a level / spread series (not returns), complementing the
varianceRatio and hurstExponent regime diagnostics above.
Realized moments
Higher moments of the intraday return distribution (Amaya et al., 2015):
import { realizedSkewness, realizedKurtosis } from "orderflow-metrics";
realizedSkewness(returns); // √N · Σr³ / RV^1.5 — intraday asymmetry
realizedKurtosis(returns); // N · Σr⁴ / RV² — intraday tail heavinessrealizedSkewness— asymmetry of the intraday return distributionrealizedKurtosis— tail heaviness of the intraday return distribution
Both return 0 for an empty or zero-variance series.
Jumps & bipower variation
Split realized variance into its continuous (diffusive) part and its jump part (Barndorff-Nielsen & Shephard, 2004). Bipower variation is jump-robust because multiplying adjacent absolute returns damps a lone spike:
import { bipowerVariation, jumpVariation, relativeJumpVariation } from "orderflow-metrics";
bipowerVariation(returns); // (π/2)·Σ|rᵢ₋₁||rᵢ| — continuous variance
jumpVariation(returns); // max(RV − BV, 0) — variance from jumps
relativeJumpVariation(returns); // jump share of RV, in [0, 1]bipowerVariation— jump-robust estimate of continuous variancejumpVariation— the realized-variance contribution of discrete jumpsrelativeJumpVariation— that jump contribution as a fraction of RV
All three return 0 for fewer than two returns (and a jumpless series gives a jump variation of 0).
Realized semivariance
Realized variance treats an up-move and a down-move of equal size as identical risk. Realized semivariance splits it by the sign of each return, isolating downside ("bad") from upside ("good") volatility — Barndorff-Nielsen, Kinnebrock & Shephard (2010) and Patton & Shephard (2015):
import {
realizedSemivariance,
downsideVarianceRatio,
signedJumpVariation,
} from "orderflow-metrics";
realizedSemivariance(returns); // { upside: Σr²·1{r>0}, downside: Σr²·1{r<0} }
downsideVarianceRatio(returns); // RS⁻ / (RS⁺ + RS⁻), in [0, 1]
signedJumpVariation(returns); // RS⁺ − RS⁻ — keeps the direction of jump riskrealizedSemivariance— upside/downside split (their sum is realized variance)downsideVarianceRatio— the negative-return share of RV; > 0.5 is downside-heavysignedJumpVariation— RS⁺ − RS⁻; positive when upside dominates, negative when downside does
Zero returns contribute to neither half, and an empty series returns zeros.
Order-flow entropy
Shannon entropy of order flow measures how predictable a stream of trades or returns is. One-sided flow (nearly all buys, or a series that only ticks up) carries little surprise — low entropy — and is easier to anticipate; balanced, unpredictable flow sits at maximum entropy. Persistently low flow entropy is a signature of directional, potentially informed activity. Reported in bits:
import { shannonEntropy, normalizedEntropy, signEntropy } from "orderflow-metrics";
shannonEntropy([3, 1]); // 0.811… bits — H of a count/probability vector
normalizedEntropy([2, 1, 1]); // 0.946 — H / log₂(k), in [0, 1]
signEntropy(returns); // up/down balance of a series, in [0, 1] bitsshannonEntropy— H = −Σ pᵢ log₂ pᵢ over positive weights; two equal outcomes = 1 bitnormalizedEntropy— that entropy scaled by log₂(k) so distributions of different sizes comparesignEntropy— 1 bit is perfectly balanced two-sided flow, near 0 is one-sided and predictable
Zero and negative weights are ignored, and fewer than two live categories returns 0.
Online / streaming estimators
Batch metrics rescan the whole history on every tick. In a live pipeline you
want estimators that update in O(1) time and memory as each observation
streams in. These are stateful classes — push one value at a time and read the
current estimate — and they are numerically stable (Welford / West, not the
naive Σx² − (Σx)²/n form that loses precision when the mean dwarfs the variance):
import { Welford, Ewma, EwmaVariance, RollingWindow } from "orderflow-metrics";
const w = new Welford();
for (const r of returns) w.push(r);
w.mean; w.variance; w.std; // running, exact, updated in O(1)
const vol = new EwmaVariance(0.94); // RiskMetrics daily λ
for (const r of returns) vol.push(r);
vol.std; // current EWMA volatility
const win = new RollingWindow(20); // trailing 20-observation window
for (const r of returns) win.push(r);
win.mean; win.variance; // O(1) add + evict (West 1979)Welford— running mean & variance over all data (variancesample,populationVariance,std,count)Ewma— exponentially weighted moving average of a level;lambdain (0, 1) is the decayEwmaVariance— RiskMetrics-style EWMA variance/volatility; assumes ~zero-mean returnsRollingWindow— mean & variance over the lastsizevalues, with O(1) add/remove
Welford and RollingWindow are verified in the test suite to equal a batch
recomputation at every step; the EWMA classes seed on their first value.
Realized covariance, correlation & beta
Single-asset volatility says how much one instrument moved; risk lives in how instruments move together. Summing products of contemporaneous returns gives the model-free, high-frequency analogue of covariance, correlation, and beta:
import { realizedCovariance, realizedCorrelation, realizedBeta } from "orderflow-metrics";
realizedCovariance(x, y); // Σ xᵢyᵢ
realizedCorrelation(x, y); // Σxy / (√Σx²·√Σy²) — in [−1, 1]
realizedBeta(asset, market); // Σa·m / Σm² — sensitivity of asset to marketrealizedCovariance— Σ xᵢyᵢ (symmetric)realizedCorrelation— scale-free co-movement in [−1, 1]realizedBeta— an asset's realized covariance with a market over the market's realized variance
The two series are paired element-wise over their common length, so align them to the same sampling grid first; empty or zero-variance inputs return 0.
Realized semicovariance
Split realized covariance by the sign of each pair of returns — the cross-asset analogue of realized semivariance (Bollerslev, Li, Patton & Quaedvlieg 2020):
import { realizedSemicovariance } from "orderflow-metrics";
realizedSemicovariance(x, y);
// { positive, negative, mixed } — sum to realizedCovariance(x, y)positive— both up: Σ max(x,0)·max(y,0) (≥ 0)negative— both down: Σ min(x,0)·min(y,0) (≥ 0) — joint downside / crash covariancemixed— opposite signs (≤ 0); the three sum to the realized covariance
Python
A dependency-free Python port lives in python/ and ships the same
metrics (OFI, VPIN, information-driven bars, spreads, price impact, order-book
reconstruction). Install from PyPI:
pip install orderflow-metricsSee python/README.md for the Python API.
Tests
node --experimental-strip-types --testLicense
MIT © RATE LTD (TwoWayMind). See LICENSE.
Part of TwoWayMind's open microstructure tooling. Educational and technical material only — not investment advice.
