Level 5 — MasteryOrder Flow & Tape Reading

Order Flow & Tape Reading

2 minutes60 marksprintable — key stays hidden on paper

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 NN trades. Trade ii has signed volume di=viϵid_i = v_i \cdot \epsilon_i, where vi>0v_i > 0 is size and ϵi{+1,1}\epsilon_i \in \{+1,-1\} indicates aggressor side (+1+1 = buy-lifts-ask, 1-1 = sell-hits-bid).

(a) Define per-bar delta Δk=ibar kdi\Delta_k = \sum_{i \in \text{bar } k} d_i and cumulative delta Ck=j=1kΔjC_k = \sum_{j=1}^{k} \Delta_j. Prove that if price PkP_k trends up over a window while CkC_k 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 kk 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 Δk\Delta_k and CkC_k 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 ΔkN(μ,σ2)\Delta_k \sim \mathcal{N}(\mu, \sigma^2) 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 z-z^*. Given the data in (b), estimate μ,σ\mu,\sigma from bars 1–5 and compute the z-score of the bar-5 delta. Using z=1.0z^* = 1.0, 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 MM price levels and justify. (3)


Question 3 — Spoofing Detection & Institutional Footprint (18 marks)

The Level-2 book streams events (t,side,price,qty,type)(t, \text{side}, \text{price}, \text{qty}, \text{type}) where type \in {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 R=cancelled qtyfilled qtyR = \frac{\text{cancelled qty}}{\text{filled qty}} 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 RR 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 QQ given nn observed reloads and a per-slice display of qq. Explain how footprint/delta confirms accumulation, and how to estimate QQ 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)

  • Δk=ikviϵi\Delta_k = \sum_{i\in k} v_i\epsilon_i is net aggressive buying minus selling in bar kk. Ck=jkΔjC_k=\sum_{j\le k}\Delta_j (telescoping running sum). (1)
  • Rising price means aggressive buyers pushing PP up. Normally rising PP \Rightarrow rising CC (buyers dominate). (1)
  • Divergence: dPdk>0\frac{dP}{dk}>0 but dCdk0\frac{dC}{dk}\le 0. Formally sign(ΔP)(\Delta P)\ne sign(ΔC)(\Delta C) 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)

Δk=\Delta_k= Buy − Sell:

Bar Δk\Delta_k CkC_k 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 Δ\Delta/CC pairing, ~5)

  • Price rises 100.0 → 100.5 (monotone up / flat), while CkC_k 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: [400,600,300,800,1200][400,600,-300,-800,-1200].

  • μ=400+60030080012005=13005=260\mu = \frac{400+600-300-800-1200}{5} = \frac{-1300}{5} = -260. (2)
  • Variance (population): deviations =[660,860,40,540,940]=[660,860,-40,-540,-940], squares =[435600,739600,1600,291600,883600]=[435600,739600,1600,291600,883600], sum =2352000=2352000, /5=470400/5=470400, σ=470400685.9\sigma=\sqrt{470400}\approx 685.9. (2)
  • Bar-5 delta z-score: z5=1200(260)685.9=940685.91.371z_5=\frac{-1200-(-260)}{685.9}=\frac{-940}{685.9}\approx -1.371. (1)
  • Price at bar 5 = new high (100.5, matching bar 4's high, ≥ prior). z5=1.37<z=1.0z_5=-1.37 < -z^* = -1.0detector 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 =400+900+1500+2600+1800+700+300=8200=400+900+1500+2600+1800+700+300 = 8200. 70% target =5740=5740. (1)

Start at POC 104: cum =2600=2600 (31.7%). VA ={104}=\{104\}. (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 LL is the max-sum contiguous window of length LL 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) O(M)O(M): POC found in one pass O(M)O(M); VA expansion adds ≤ M1M-1 levels, each step is O(1)O(1) pointer comparison → total O(M)O(M). 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) R=cancelledfilled=48020=24R=\frac{\text{cancelled}}{\text{filled}}=\frac{480}{20}=24. (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). R=24R=24 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 q=100q=100 reloads nn times; observed executed volume =nq=n\cdot q. Hidden order true size QnqQ \ge n\cdot q; best estimate when reloads stop after nn full reloads Q^nq\hat Q \approx n\cdot q (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 QQ: 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 > 0

Marks: 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+