Microstructure¶

How the market is trading underneath the price — liquidity, the cost of crossing the spread, whether order flow looks informed, and how fast a series reverts. These are standard on institutional desks and largely absent from retail TA libraries, because they need bar-level volume classification, autocovariance of returns, or scaling-law fits rather than a simple rolling window. All are validated against real BTCUSDT data.
Spread & transaction cost (from prices alone)¶
Recover the effective bid-ask spread without quote data:
roll_spread— Roll (1984), from the negative autocovariance that bid-ask bounce induces in price changes.corwin_schultz_spread— the modern high-low estimator; more robust on OHLC bars than Roll.effective_spread— distance of the trade price from the mid.
Price impact & liquidity¶
How much price moves per unit of order flow — thinner market, steeper impact:
kyle_lambda— Kyle (1985): the workhorse linear price-impact measure for sizing orders against liquidity.hasbrouck_lambda— impact in log-price / √dollar-volume space, matching the empirical square-root impact law.
Order-flow toxicity & trade direction¶
vpin— Volume-Synchronised Probability of Informed Trading: order-flow imbalance in volume time, the canonical flow-toxicity warning (it spiked ahead of the 2010 Flash Crash).lee_ready_trade_sign— classifies each trade as buy- or sell-initiated (the input to signed-flow measures).
Regime & mean-reversion diagnostics¶
Decide whether to run momentum or mean-reversion, and how fast reversion is:
hurst_exponent— full R/S Hurst: persistent (>0.5) vs. mean-reverting (<0.5). (For a fast multi-scale version, seequant.hurst_ribbon.)half_life— how many bars a mean-reverting series takes to close half the gap to its mean (from an OU fit). The "how fast does it revert?" number for stat-arb.variance_ratio— Lo-MacKinlay test: is this a random walk (=1), trending (>1), or mean-reverting (<1)?shannon_entropy/approximate_entropy— distributional complexity and pattern predictability; complementary to Hurst (they don't care about direction, only structure).
These describe conditions, not signals
Microstructure tools are for classifying the environment — is it liquid, is flow toxic, is it trending or reverting — and gating strategy choice, rather than firing buy/sell signals directly.
polars_ta.microstructure
¶
Market microstructure and order-flow features used on professional/quant desks.
These are largely absent from retail TA libraries (they need bar-level volume classification, autocovariance of returns, or scaling-law fits rather than a simple rolling window), but are standard tools for liquidity analysis, informed-trading detection, and regime classification on institutional desks.
roll_spread
¶
roll_spread(close: str | Expr, window: int = 20) -> Expr
Roll (1984) implied bid-ask spread from serial covariance of price changes.
Estimates the effective spread purely from trade prices, using the fact that bid-ask bounce induces negative first-order autocovariance in price changes: spread = 2 * sqrt(-cov(delta_p_t, delta_p_t-1)) when that covariance is negative (as microstructure theory predicts); the window is reported as null when the covariance is non-negative, since the model's premise doesn't hold there.
Source code in polars_ta/microstructure.py
89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 | |
kyle_lambda
¶
kyle_lambda(close: str | Expr, volume: str | Expr, window: int = 20) -> Expr
Kyle's (1985) lambda: price impact per unit of signed order flow.
Regresses price changes on signed volume (sign of the price change used as a trade-direction proxy, i.e. a tick rule) within a rolling window. A steeper slope means the market absorbs less volume per unit of price move — thinner, less liquid conditions. This is the workhorse price-impact measure for execution/market-making desks sizing orders against liquidity.
Source code in polars_ta/microstructure.py
108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 | |
hasbrouck_lambda
¶
hasbrouck_lambda(close: str | Expr, volume: str | Expr, window: int = 20) -> Expr
Hasbrouck's (1991) lambda: price impact regressed in log-price / sqrt(dollar-volume) space rather than Kyle's raw price/volume space.
Using sqrt(signed dollar volume) makes the impact measure robust to the typical square-root law of market impact, which is closer to what execution desks actually observe versus Kyle's linear assumption.
Source code in polars_ta/microstructure.py
129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 | |
effective_spread
¶
effective_spread(close: str | Expr, mid_price: str | Expr | None = None) -> Expr
Effective spread proxy: 2 * |close - mid|, in the same units as price.
When no explicit mid-price/quote data is available (the common case for bar data), the previous close is used as a proxy for the prevailing mid, which is the standard fallback in the academic microstructure literature when only trade prices are observable.
Source code in polars_ta/microstructure.py
152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 | |
lee_ready_trade_sign
¶
lee_ready_trade_sign(close: str | Expr, mid_price: str | Expr | None = None) -> Expr
Lee-Ready trade-side classification: +1 buy-initiated, -1 sell-initiated, 0 unclassifiable, per bar/trade.
The full Lee & Ready (1991) algorithm classifies by the quote test
(trade price vs. the prevailing bid-ask midpoint) first, falling back to
the tick test (trade price vs. the previous trade price) only when the
trade is exactly at the midpoint. Bar data has no quotes, so when
mid_price isn't supplied the quote test is skipped entirely (comparing
close against its own previous value as "mid" would make the quote test
degenerate into the tick test) and classification falls straight to the
tick test — the standard reduction used when only trade prices are
observable. A genuine tie (flat price) is unclassifiable (0), never
guessed.
Source code in polars_ta/microstructure.py
170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 | |
vpin
¶
vpin(close: str | Expr, volume: str | Expr, bucket_size: int, window: int = 50) -> Expr
Volume-Synchronized Probability of Informed Trading (Easley, Lopez de Prado & O'Hara, 2012).
Bars are aggregated into equal-sized volume buckets (not time bars),
each bucket's volume is split into buy/sell using bulk volume
classification (a z-scored price-change CDF, standard for bar data
without tick-level trade direction), and VPIN is the rolling mean of
|buy - sell| / total volume across the last window buckets. High VPIN
signals order-flow imbalance consistent with informed trading — the
canonical warning signal ahead of liquidity-driven crashes (e.g. the
2010 Flash Crash), used on institutional desks for flow-toxicity
monitoring rather than directional signal generation.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
bucket_size
|
int
|
total volume per synchronized bucket (must match the
typical scale of |
required |
window
|
int
|
number of trailing buckets averaged into the VPIN estimate. |
50
|
Source code in polars_ta/microstructure.py
205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 | |
hurst_exponent
¶
hurst_exponent(close: str | Expr, window: int = 100) -> Expr
Rolling Hurst exponent via rescaled-range (R/S) analysis.
H < 0.5 indicates a mean-reverting regime, H = 0.5 a random walk, and H > 0.5 a trending/persistent regime. Quant desks use this to switch strategy family (momentum vs mean-reversion) rather than as a trade signal by itself.
Implemented with a single map_batches pass and a sliding-window view
(no per-row Python rolling_map): each window's R/S regression is
computed over NumPy-reshaped chunks, so cost is dominated by vectorized
array ops rather than an O(window) Python loop per row.
Source code in polars_ta/microstructure.py
330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 | |
variance_ratio
¶
variance_ratio(close: str | Expr, window: int = 20, lag: int = 2) -> Expr
Lo-MacKinlay (1988) variance ratio test statistic, rolled over window.
VR(lag) = Var(lag-period return) / (lag * Var(1-period return)). Under the random-walk null hypothesis VR = 1; VR > 1 indicates positive serial correlation (trending), VR < 1 indicates mean reversion. Used by quant desks to test whether a random-walk assumption (and therefore standard options-pricing / risk models built on it) actually holds for an instrument over a given regime.
Source code in polars_ta/microstructure.py
363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 | |
corwin_schultz_spread
¶
corwin_schultz_spread(high: str | Expr, low: str | Expr, window: int = 20) -> Expr
Corwin-Schultz (2012) high-low bid-ask spread estimator.
Recovers the effective spread from two consecutive daily high-low ranges
only (no volume, no quotes). The insight is that the high-low range
reflects both the true variance (which scales with the time interval) and
the spread (which does not), so combining a single-bar range with a
two-bar range separates the two. This is the modern successor to Roll's
estimator and is far more robust on OHLC bar data; negative per-bar
estimates (which the model treats as zero spread) are floored at zero and
the result is averaged over window bars.
Source code in polars_ta/microstructure.py
385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 | |
half_life
¶
half_life(close: str | Expr, window: int = 60) -> Expr
Half-life of mean reversion from a rolling Ornstein-Uhlenbeck fit.
Regresses the change in price on the lagged price level within each
window (an AR(1) / discretized OU fit): dP_t = a + b * P_{t-1}. When
b < 0 the series is mean-reverting and the half-life — the expected
number of bars to close half the gap to the mean — is -ln(2) / ln(1+b).
Non-mean-reverting windows (b >= 0) are reported as null. This is the
workhorse "how fast does it revert" number on stat-arb desks, pairing
naturally with :func:variance_ratio and :func:hurst_exponent.
Source code in polars_ta/microstructure.py
421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 | |
shannon_entropy
¶
shannon_entropy(close: str | Expr, window: int = 50, n_bins: int = 10) -> Expr
Rolling Shannon entropy (normalized to [0, 1]) of the binned distribution of log returns within each window.
A value near 1 means returns within the window are spread roughly uniformly across bins — high "surprise"/complexity, consistent with a noisy or regime-shifting market. A value near 0 means returns cluster into a few bins — low complexity, consistent with a persistent trend or a tightly range-bound market. Unlike the Hurst exponent (which measures directional persistence), entropy measures distributional concentration and doesn't care about sign or serial correlation, so the two are complementary regime signals rather than redundant ones.
A window with fewer non-null returns than n_bins can't populate every
bin meaningfully and reports null rather than a misleadingly low entropy.
Source code in polars_ta/microstructure.py
458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 | |
approximate_entropy
¶
approximate_entropy(close: str | Expr, window: int = 30, m: int = 2, r_frac: float = 0.2) -> Expr
Rolling approximate entropy (ApEn) of log returns within each window.
Measures how predictable consecutive m-length patterns are: low ApEn
means the series repeats similar short patterns (more regular/
predictable), high ApEn means patterns rarely recur (more random). r
(the similarity tolerance) is set as a fraction (r_frac, Pincus'
convention is ~0.2) of the window's own standard deviation, so it
self-scales with local volatility rather than needing an absolute
threshold tuned per instrument.
Cost warning: this is O(window^2) per row via map_batches (see
:func:_approximate_entropy_from_window) — the classic algorithm has no
faster exact form. Keep window in the tens, not hundreds, on large
frames; this is meant as a slow-moving regime gate, not a per-bar signal
computed over a huge lookback.
Source code in polars_ta/microstructure.py
526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 | |