Pressure before the price moves
A price rarely moves by itself; it moves because orders arrive and remove liquidity. Looking only at trades misses much of the story, since most of the action is orders being placed and cancelled at the top of the book. Order-flow imbalance (OFI) summarises every update of the best bid and ask into one signed number: positive when buyers are adding or sellers are retreating, negative in the opposite case. Over short horizons it predicts the next price changes remarkably well.
def book_imbalance(bid_size, ask_size):
total = bid_size + ask_size
if total == 0:
return 0.0
return (bid_size - ask_size) / total
print(book_imbalance(450, 200)) # 0.3846 more size on the bid: upward pressure
print(book_imbalance(100, 400)) # -0.6 sellers dominate
print(book_imbalance(0, 0)) # 0.0 an empty top does not divide by zeroOne update, one number
Compare two successive quotes. On the bid: if the bid price rises or stays, its new size is added; if the bid price falls or stays, its old size is removed (when the price is unchanged both apply, so the change in size counts). On the ask the signs are reversed: a lower or equal ask adds selling pressure (- new ask size), a higher or equal ask removes the old one (+ old ask size). The four terms are (b >= pb) * bs - (b <= pb) * pbs - (a <= pa) * as + (a >= pa) * pas, where each comparison is 1 or 0.
# The order-flow imbalance of one quote update, tried on four changes of the top of the book
def ofi_event(prev, cur):
pb, pbs, pa, pas_ = prev
b, bs, a, as_ = cur
return (b >= pb) * bs - (b <= pb) * pbs - (a <= pa) * as_ + (a >= pa) * pas_
q0 = (99.5, 300, 100.0, 200)
print(ofi_event(q0, (99.5, 450, 100.0, 200))) # 150 more size on the same bid
print(ofi_event(q0, (99.6, 100, 100.0, 200))) # 100 a higher bid: the whole new size counts
print(ofi_event(q0, (99.4, 300, 100.0, 200))) # -300 the bid price fell: the old size is gone
print(ofi_event(q0, (99.5, 300, 100.1, 200))) # 200 the ask retreated: less selling pressureA series, and what it explains
Applying the rule to every consecutive pair of quotes gives an OFI series. Over a short window, such as one second, sum the events and compare with the change of the mid price: the relation is close to linear, d_mid = slope * OFI. The slope says how much price each unit of flow moves, and it shrinks when the book is deep. Fit it as a least-squares line through the origin: sum(ofi * dmid) / sum(ofi ** 2).
# A series of events from consecutive quotes, set beside the change of the mid price
import numpy as np
def ofi_event(prev, cur):
pb, pbs, pa, pas_ = prev
b, bs, a, as_ = cur
return (b >= pb) * bs - (b <= pb) * pbs - (a <= pa) * as_ + (a >= pa) * pas_
quotes = [(99.5, 300, 100.0, 200), (99.5, 450, 100.0, 200), (99.6, 100, 100.0, 150), (99.6, 100, 100.1, 400)]
events = np.array([ofi_event(prev, cur) for prev, cur in zip(quotes, quotes[1:])], dtype=float)
print(events) # [150. 150. 150.]
mid = np.array([(q[0] + q[2]) / 2 for q in quotes])
dmid = np.diff(mid)
print(np.round(dmid, 3)) # the mid moved in the direction of the flow# Recover the price impact of the flow with a slope through the origin
import numpy as np
def impact_slope(ofi, dmid):
return float(np.sum(ofi * dmid) / np.sum(ofi ** 2))
rng = np.random.default_rng(1)
flow = rng.normal(0, 200, size=500) # order-flow imbalance per second
price_change = 0.00005 * flow + rng.normal(0, 0.004, size=500) # the mid moves with the flow, plus noise
print(round(impact_slope(flow, price_change), 6)) # 5e-05, the true impact per unit of flow- Book imbalance
(bid - ask) / (bid + ask)lies between -1 and +1; guard the empty case. - OFI of an update:
(b >= pb) * bs - (b <= pb) * pbs - (a <= pa) * as + (a >= pa) * pas; a series is this for every consecutive pair. - Mid-price changes are close to linear in OFI:
slope = sum(ofi * dmid) / sum(ofi**2); it is a short-horizon signal, most useful for execution.
Next up: asynchronous code with asyncio, handling many data streams without waiting on each one.
Lab · what's graded
- Compare the two sides with
book_imbalance25 pts - Score one update with
ofi_event25 pts - Stream the pressure with
ofi_series25 pts - Price the flow with
impact_slope25 pts