Order Flow & Tape Reading
Level: 3 — Production (from-scratch derivations, code-from-memory, explain-out-loud) Time limit: 45 minutes Total marks: 60
Instructions: Show all working. Where code is requested, pseudocode or Python is acceptable but must be logically complete. Use for any math.
Question 1 — Delta & Cumulative Delta from the Tape (12 marks)
You are given a raw time-and-sales feed. Each print has: price, size, and aggressor ("buy" if it lifted the ask, "sell" if it hit the bid).
(a) From memory, state the definition of bar delta and cumulative delta in terms of aggressor-classified volume. (3 marks)
(b) Write a function compute_cumulative_delta(trades) that returns a list of cumulative delta values, one per trade. (5 marks)
(c) Given the tape below, compute the bar delta and the final cumulative delta. (4 marks)
| # | price | size | aggressor |
|---|---|---|---|
| 1 | 100.0 | 40 | buy |
| 2 | 100.0 | 25 | sell |
| 3 | 100.1 | 60 | buy |
| 4 | 100.0 | 30 | sell |
| 5 | 100.1 | 15 | sell |
Question 2 — Point of Control & Value Area (14 marks)
A session's volume-by-price profile is:
| Price | Volume |
|---|---|
| 101.4 | 120 |
| 101.3 | 300 |
| 101.2 | 500 |
| 101.1 | 700 |
| 101.0 | 400 |
| 100.9 | 250 |
| 100.8 | 130 |
(a) Define the POC and identify it here. (2 marks)
(b) From scratch, derive the 70% Value Area. State the standard algorithm (POC-outward, pairwise comparison), then apply it. Report VAH, VAL and the total volume inside the value area. (8 marks)
(c) Explain out loud (2–4 sentences) why the value area matters for a trader deciding to fade or follow a breakout. (4 marks)
Question 3 — Absorption vs Exhaustion (10 marks)
(a) Contrast absorption and exhaustion in the order book / footprint. For each, state (i) what you see on the tape, (ii) what delta does, (iii) the expected price outcome. (6 marks)
(b) You observe: price grinds up into a level, delta is strongly positive on each bar, but price stops advancing and closes flat while heavy volume trades. Classify this and justify. (4 marks)
Question 4 — Spoofing & Layering Detection (12 marks)
(a) Define spoofing and layering, and state why each is illegal. (4 marks)
(b) From memory, write pseudocode for a heuristic flag_spoof(order_events) that flags a resting order as suspicious spoofing. Use at least: order size relative to average, distance from touch, and cancel-before-fill within a short lifetime. (8 marks)
Question 5 — Reading Level-2 & Institutional Footprints (12 marks)
(a) Given a Level-2 snapshot, define market depth, imbalance, and how an iceberg order reveals itself. (4 marks)
(b) Compute the top-3-level bid/ask imbalance ratio for the book below and interpret it. (4 marks)
| Bid Size | Bid | Ask | Ask Size |
|---|---|---|---|
| 500 | 99.98 | 99.99 | 120 |
| 800 | 99.97 | 100.00 | 150 |
| 650 | 99.96 | 100.01 | 200 |
(c) Explain how you would distinguish a genuine large institutional buyer from a spoofer using the tape + book together (2–4 sentences). (4 marks)
Answer keyMark scheme & solutions
Question 1 (12 marks)
(a) (3 marks)
- Bar delta within the bar. (2)
- Cumulative delta = running total of bar/trade deltas across the session; it never resets intrabar. (1)
(b) (5 marks)
def compute_cumulative_delta(trades):
cum = 0
out = []
for t in trades:
signed = t['size'] if t['aggressor'] == 'buy' else -t['size']
cum += signed # accumulate
out.append(cum)
return outMarks: signed volume via aggressor (2), running accumulator (2), returns per-trade list (1).
(c) (4 marks) Signed volumes: . Bar delta . (2) Cumulative path: . Final cumulative delta . (2)
Question 2 (14 marks)
(a) (2 marks) POC = price level with the greatest traded volume. Here max volume = 700 at 101.1 → POC = 101.1. (1 def + 1 identify)
(b) (8 marks) Total volume . Target . (1)
Algorithm: start at POC; repeatedly compare the sum of the two rows above the current top vs the two rows below the current bottom; add whichever pair is larger; continue until accumulated volume . (2)
Apply (start band = {101.1} = 700):
- Above pair (101.2, 101.3) = ; Below pair (101.0, 100.9) = . Above larger → add. Total . Band now 101.0? no — band = [101.1..101.3]. (1)
- Next above pair (101.4, —) = (only one row left, +0) = 120; Below pair (101.0, 100.9) = 650. Below larger → add. Total . (2)
Value area now spans 100.9 → 101.3.
- VAL = 100.9, VAH = 101.3. (1.5)
- Volume inside VA . (0.5)
(Accept stopping exactly when threshold crossed; the crossing row 100.9 is included by convention.)
(c) (4 marks)
- Value area = where ~70% of business was done → "fair value" / balance region. (1)
- Price accepted outside VA (holds beyond VAH/VAL) signals a genuine breakout → follow. (1.5)
- Price rejected back inside VA signals a failed auction/false break → fade back toward POC. (1.5)
Question 3 (10 marks)
(a) (6 marks — 3 each)
- Absorption: (i) heavy aggressive volume hitting a level but price barely moves — a large passive limit order soaks it up. (ii) Delta is large (same direction as aggression) yet price flat/diverges from delta. (iii) Reversal against the aggressor as they get exhausted / trapped.
- Exhaustion: (i) climactic volume/large prints at the extreme, then aggression dries up. (ii) Delta spikes then collapses / momentum fades. (iii) Trend stalls and reverses as the last participants are used up.
(b) (4 marks)
- Strong positive delta but price fails to advance = classic delta/price divergence → absorption. (2)
- Justification: aggressive buyers are trading heavily but a passive seller is absorbing all of it at the level; price can't rise, signalling sellers control the level and a reversal down is likely. (2)
Question 4 (12 marks)
(a) (4 marks)
- Spoofing: placing a large order with intent to cancel before execution, to create false impression of supply/demand and move price. (1.5)
- Layering: placing multiple orders at several price levels on one side to build a fake wall / stacked deception. (1.5)
- Illegal because it's manipulative intent to deceive the market (fraud), no genuine intent to trade. (1)
(b) (8 marks)
def flag_spoof(order_events, book_stats):
flags = []
for o in order_events:
big = o.size > 5 * book_stats.avg_order_size # unusually large
far = abs(o.price - book_stats.touch) <= NEAR_TICKS # near enough to spook
life = o.cancel_time - o.place_time
canceled_fast = (o.status == 'canceled') and (life < SHORT_MS)
never_filled = (o.filled == 0)
if big and far and canceled_fast and never_filled:
flags.append(o.id)
return flagsMarks: size-vs-average (2), distance-from-touch (2), short lifetime + cancel-before-fill (3), returns flagged IDs (1). Accept a repeat-count / layering extension for full credit.
Question 5 (12 marks)
(a) (4 marks)
- Market depth: the resting bid/ask volume at each price level away from the best (top of book). (1.5)
- Imbalance: ratio/difference of aggregated bid vs ask size indicating buy/sell pressure. (1.5)
- Iceberg: displayed size is small but keeps replenishing after fills — repeated refills at one price reveal hidden size. (1)
(b) (4 marks)
- Bid total (top 3) . (1)
- Ask total (top 3) . (1)
- Ratio . (1)
- Interpretation: bids heavily outweigh asks (~4:1) → bullish depth imbalance, upward pressure — but note thin size could itself be spoofed. (1)
(c) (4 marks)
- Genuine institution: passive order gets filled/refilled (iceberg), tape shows real absorption, order stays through pressure. (2)
- Spoofer: large size but cancels before fill, pulls when price approaches, no prints match the displayed size. Cross-check book display against actual executed tape. (2)
[
{"claim":"Q1 bar delta = 30 and final cumulative delta = 30","code":"vols=[40,-25,60,-30,-15]\ndelta=sum(vols)\ncum=0\npath=[]\nfor v in vols:\n cum+=v\n path.append(cum)\nresult = (delta==30 and path[-1]==30)"},
{"claim":"Q2 total volume 2400 and 70% target 1680","code":"vols=[120,300,500,700,400,250,130]\ntotal=sum(vols)\nresult = (total==2400 and abs(0.7*total-1680)<1e-9)"},
{"claim":"Q2 value area volume = 2150 (100.9 to 101.3 inclusive)","code":"va=[300,500,700,400,250]\nresult = (sum(va)==2150 and sum(va)>=1680)"},
{"claim":"Q5 top-3 imbalance ratio approx 4.15","code":"bid=500+800+650\nask=120+150+200\nratio=bid/ask\nresult = (bid==1950 and ask==470 and abs(ratio-4.148936)<1e-4)"}
]