Order Flow & Tape Reading
Level 5 — Mastery (Cross-Domain: Quantitative Modelling, Statistics & Coding) Time Limit: 2 hours 30 minutes Total Marks: 60
Instructions: Answer ALL three questions. Show full derivations. Where code is requested, write clean, runnable pseudocode/Python. State all assumptions.
Question 1 — Cumulative Delta, Absorption & a Statistical Detector (22 marks)
A futures tape over one session is discretised into trades. Trade has signed volume , where is size and indicates aggressor side ( = buy-lifts-ask, = sell-hits-bid).
(a) Define per-bar delta and cumulative delta . Prove that if price trends up over a window while is flat or falling, the configuration is bearish delta divergence. State the microstructural interpretation in terms of absorption. (5)
(b) Over a 5-bar window you observe:
| Bar | Buy vol | Sell vol | Close price |
|---|---|---|---|
| 1 | 1200 | 800 | 100.0 |
| 2 | 1500 | 900 | 100.3 |
| 3 | 1100 | 1400 | 100.4 |
| 4 | 900 | 1700 | 100.5 |
| 5 | 700 | 1900 | 100.5 |
Compute and for each bar. Identify whether delta divergence is present and classify the likely event (absorption vs exhaustion). (7)
(c) Model each bar's delta as under "normal" flow. Design a z-score based absorption detector: trigger when price makes a new high but cumulative delta z-score falls below threshold . Given the data in (b), estimate from bars 1–5 and compute the z-score of the bar-5 delta. Using , state whether the detector fires. (6)
(d) Write a vectorised function absorption_signal(buy, sell, price) returning boolean array of triggers. (4)
Question 2 — Volume Profile, POC & Value Area Construction (20 marks)
A session's traded volume by price level is:
| Price | 101 | 102 | 103 | 104 | 105 | 106 | 107 |
|---|---|---|---|---|---|---|---|
| Vol | 400 | 900 | 1500 | 2600 | 1800 | 700 | 300 |
(a) Define and compute the Point of Control (POC). (3)
(b) The Value Area (VA) contains 70% of total volume, grown outward from POC by the standard "compare-the-pair" TPO/volume algorithm: at each step add whichever adjacent side (single price above vs single price below the current VA) has greater volume, until 70% is reached. Determine VAH and VAL. Show every comparison step and running cumulative percentage. (9)
(c) Prove that the greedy pair-comparison algorithm does not in general maximise the volume captured for a fixed number of price levels, by constructing a counterexample profile. (5)
(d) Give the time complexity of the VA algorithm for price levels and justify. (3)
Question 3 — Spoofing Detection & Institutional Footprint (18 marks)
The Level-2 book streams events where type {add, cancel, trade}.
(a) Formally define spoofing and layering. Give two quantitative signatures distinguishing spoofing from legitimate resting liquidity. (4)
(b) Define a cancel-to-fill ratio per level over a rolling window. A trader posts and cancels the following on the bid side of a level within 3 seconds:
- Adds 500, cancels 480, filled 20 (repeated pattern).
Compute and argue with reference to a chosen legal threshold why this is suspicious. (4)
(c) An institution executes a large order via iceberg slicing. Observed prints at the ask over 10 seconds: repeated 100-lot trades at 103.00 with the displayed ask size reloading to 100 after each hit. Model the true hidden size given observed reloads and a per-slice display of . Explain how footprint/delta confirms accumulation, and how to estimate from tape when execution stops. (5)
(d) Write pseudocode detect_iceberg(trades, book) that flags a level where displayed size repeatedly reloads while aggressive prints continue. (5)
Answer keyMark scheme & solutions
Question 1
(a) (5 marks)
- is net aggressive buying minus selling in bar . (telescoping running sum). (1)
- Rising price means aggressive buyers pushing up. Normally rising rising (buyers dominate). (1)
- Divergence: but . Formally sign sign over the window. (1)
- Interpretation: price rises despite net selling delta ⇒ a large passive limit seller is absorbing the aggressive buying at the ask; buyers cannot move price efficiently. This is bearish because when buyers exhaust, the resting seller dominates → reversal down. (2)
(b) (7 marks)
Buy − Sell:
| Bar | Price | ||
|---|---|---|---|
| 1 | +400 | 400 | 100.0 |
| 2 | +600 | 1000 | 100.3 |
| 3 | −300 | 700 | 100.4 |
| 4 | −800 | −100 | 100.5 |
| 5 | −1200 | −1300 | 100.5 |
(1 mark per correct / pairing, ~5)
- Price rises 100.0 → 100.5 (monotone up / flat), while peaks at bar 2 (1000) then falls to −1300. Clear bearish delta divergence. (1)
- Classification: rising price with heavily negative delta = passive buyers/absorbing... precisely: aggressive sellers increasing yet price won't drop → absorption of selling by a passive buyer initially, but bar 5 price stalls (100.5=100.5) with huge negative delta → transitioning to exhaustion of the up-move; net read = bearish absorption at highs. (1)
(c) (6 marks)
Deltas: .
- . (2)
- Variance (population): deviations , squares , sum , , . (2)
- Bar-5 delta z-score: . (1)
- Price at bar 5 = new high (100.5, matching bar 4's high, ≥ prior). ⇒ detector FIRES (absorption/bearish flag). (1)
(d) (4 marks)
import numpy as np
def absorption_signal(buy, sell, price, z_star=1.0):
delta = buy - sell
C = np.cumsum(delta)
mu, sd = delta.mean(), delta.std() # population std
z = (delta - mu) / sd
new_high = price >= np.maximum.accumulate(price)
return new_high & (z < -z_star)(1 delta/cumsum, 1 z-score, 1 new-high logic, 1 combine)
Question 2
(a) (3 marks) POC = price level with the highest traded volume = 104 (2600). Definition 1 mark, identification 2.
(b) (9 marks)
Total volume . 70% target . (1)
Start at POC 104: cum (31.7%). VA . (1)
Compare pair rule (one above vs one below current VA edges):
| Step | Above(price/vol) | Below(price/vol) | Add | Cum vol | Cum % |
|---|---|---|---|---|---|
| 1 | 105/1800 | 103/1500 | 105 | 4400 | 53.7% |
| 2 | 106/700 | 103/1500 | 103 | 5900 | 71.95% ✓ |
(2 marks for correct comparisons, 2 for cumulative)
- Step 2 crosses 70% → stop. VA levels = {103,104,105}. (1)
- VAH = 105, VAL = 103, POC = 104. (1)
(marks: total 1, start 1, table steps 4, stop logic 1, final answer 2 = 9)
(c) (5 marks)
Greedy adds the larger single adjacent side, but optimal capture for fixed level-count is the max-sum contiguous window of length containing POC (or any window).
Counterexample profile (volumes): [10, 1, 100, 1, 10] with POC at index 2 (=100).
- To capture 3 levels greedily from POC: compare left(1) vs right(1) — tie/either, say add left → {1,100}, then next compare left(10) vs right(1) → add left → window {10,1,100}, sum = 111.
- But choosing the 3-level window starting elsewhere, e.g.
[100,1,10]= 111 same; however consider[10, 90, 100, 5, 80], POC=100(idx2), pick 3 levels:- Greedy: left(90) vs right(5) → add 90 → {90,100}; next left(10) vs right(5) → add 10 → sum 200.
- Optimal contiguous 3-window: {100,5,80} = 185, or {90,100,5}=195, or {10,90,100}=200. Here greedy = optimal — so refine.
- Cleaner counterexample forcing suboptimality when the algorithm must be contiguous through POC and one side has a "wall then void":
[200, 5, 100(POC), 6, 190], capture 3 levels:- Greedy: left(5) vs right(6) → right → {100,6}; next left(5) vs right(190) → right → {100,6,190} sum=296.
- Alternative 3-window through POC {200,5,100} = 305 > 296. Greedy is suboptimal. ∎
(3 marks valid counterexample with correct greedy trace, 2 marks showing better alternative). Full credit for any construction where a large "far" level is blocked behind a small near level.
(d) (3 marks) : POC found in one pass ; VA expansion adds ≤ levels, each step is pointer comparison → total . Justify: constant work per level, each level added once. (3)
Question 3
(a) (4 marks)
- Spoofing: placing large non-bona-fide orders with intent to cancel before execution, to create a false impression of supply/demand and move price/induce others to trade. (1)
- Layering: multiple spoof orders at several price levels on one side to exaggerate depth. (1)
- Signatures: (i) very high cancel-to-fill / order-to-trade ratio; (ii) large size that vanishes as price approaches it / is placed away from touch and pulled; opposite-side execution coincides. (2 signatures, 2 marks)
(b) (4 marks) . (2)
- Legal/exchange thresholds for order-to-trade or cancel ratios are typically low (e.g. flag when order-to-trade ≫ ~ a few, many venues investigate ratios >~ 20–50). with sub-3-second lifetimes and repetition ⇒ pattern of intent-to-cancel = suspicious spoofing. Argument must cite: high ratio + short lifetime + repetition + one-sided. (2)
(c) (5 marks)
- Iceberg: displayed reloads times; observed executed volume . Hidden order true size ; best estimate when reloads stop after full reloads (plus final partial). (2)
- Footprint/delta confirmation: repeated prints hitting the ask with the ask reloading = aggressive buyers absorbed by passive... here institution is the passive reloader on ask (distributing) OR aggressive buyer? Convention: reloading ask that keeps getting lifted = passive seller distributing; if prints are buyer-initiated (at ask) and price doesn't rise → absorption by hidden seller ⇒ cumulative delta rises while price flat = bearish. If institution is the aggressor lifting reloaded offers, delta rises with price = accumulation. Must state direction consistent with tape. (2)
- Estimate : count reloads until display no longer reloads / price breaks; \hat Q = q\cdot(\text{#reloads}). (1)
(d) (5 marks)
def detect_iceberg(trades, book, level, min_reloads=4, window=10):
# trades: list of (t, side, price, qty)
# book: function book(t, price) -> displayed size at level
reloads = 0
last_size = book(trades[0].t, level)
aggressive_volume = 0
t0 = trades[0].t
for tr in trades:
if tr.t - t0 > window:
break
if tr.price == level:
aggressive_volume += tr.qty
cur = book(tr.t, level)
# displayed size drops on hit then reloads back up
if cur >= last_size and last_size < book(tr.t - eps, level):
reloads += 1
last_size = cur
return reloads >= min_reloads and aggressive_volume > 0Marks: iteration/window 1, reload detection logic 2, threshold flag 1, aggressive-volume confirmation 1.
[
{"claim":"Bar deltas and cumulative delta correct; C5=-1300",
"code":"buy=[1200,1500,1100,900,700]; sell=[800,900,1400,1700,1900]; delta=[b-s for b,s in zip(buy,sell)]; C=[]; s=0\nfor d in delta:\n s+=d; C.append(s)\nresult = (delta==[400,600,-300,-800,-1200]) and (C==[400,1000,700,-100,-1300])"},
{"claim":"mu=-260 and bar5 z-score approx -1.371, detector fires at z*=1.0",
"code":"import statistics as st\ndelta=[400,600,-300,-800,-1200]; mu=sum(delta)/5\nsd=(sum((d-mu)**2 for d in delta)/5)**0.5\nz5=(delta[-1]-mu)/sd\nresult = abs(mu-(-260))<1e-9 and abs(z5-(-1.371))<0.01 and z5 < -1.0"},
{"claim":"Volume profile POC=104, total=8200, VA {103,104,105} crosses 70% at 71.95%",
"code":"prices=[101,102,103,104,105,106,107]; vol=[400,900,1500,2600,1800,700,300]\ntotal=sum(vol); poc_i=vol.index(max(vol)); cum=vol[poc_i]\nlo=poc_i; hi=poc_i; steps=[]\nwhile cum/total < 0.70:\n above = vol[hi+1] if hi+1<len(vol) else -1\n below = vol[lo-1] if lo-1>=0 else -1\n if above>=below: hi+