Backtesting Big Trades

Do large aggressive trades predict short-term price direction?

We tested big trades in crude oil (CL) and the E-mini S&P 500 (ES) from August 31 – September 4, 2026.

CL:
50+ contracts · 430 trades
ES:
120+ contracts · 525 trades

Moves are in each instrument's ticks: +1 means one tick up, −1 means one tick down. n is the number of trades.

What happens after a big trade?

Average price moves after big trades in CL and ES
SideMarketn10s30s1m5m
BuysCL220-1.1-0.9-0.8+1.4
ES258+0.4-0.5-0.5+1.0
SellsCL210+0.5+1.3+1.0+2.3
ES267+2.2+1.0+0.1+1.3

Following the aggressor did not give a consistent directional signal.

Does range location matter?

We grouped trades by their position in the preceding 30 minute range.

Buys

Buys: average price moves by range location in CL and ES
LocationMarketn10s30s1m5m
Top 20%CL123-0.7-0.4-0.1+4.3
ES1000.0-2.4-3.1-1.1
Middle 60%CL68-1.9-2.4-3.1-3.4
ES125+0.2+0.4+1.1+3.4
Bottom 20%CL29-0.8+0.3+1.4+0.3
ES33+2.3+2.2+1.3-2.0

Sells

Sells: average price moves by range location in CL and ES
LocationMarketn10s30s1m5m
Top 20%CL39-0.2+1.0-0.1+4.0
ES59+1.9-2.7-4.6+1.1
Middle 60%CL90+0.2+1.5+1.3-2.8
ES148+2.1+1.7+1.5+1.6
Bottom 20%CL81+1.3+1.3+1.2+0.8
ES60+2.7+2.7+1.5+0.9

No clear pattern. Large buys do have a tendency to occur at the top of a range.

Does size matter?

We grouped trades by contract size in each market. Q1 contains the smallest qualifying trades; Q4 the largest.

Buys

buys: trade size quartiles and average price moves in ticks for CL and ES
QuartileMarketContract rangen10s30s1m5m
Q1CL50–5455-1.1+0.4+0.5+3.0
ES120–13765-0.2-1.7-1.3+0.2
Q2CL55–7455-0.6-1.4-1.9-0.7
ES138–16564+0.6-0.7-1.6-0.4
Q3CL75–10157-1.3-1.5-0.6+1.5
ES166–24065+1.6+0.6+0.3+0.9
Q4CL102–61753-1.4-1.2-1.4+1.8
ES241–134464-0.40.0+0.5+3.2

Sells

sells: trade size quartiles and average price moves in ticks for CL and ES
QuartileMarketContract rangen10s30s1m5m
Q1CL50–5454-0.1+0.8+0.8-1.3
ES120–13770+1.8+0.1-0.7+0.1
Q2CL55–7451+1.3+2.1+2.4+4.9
ES138–16564+2.5+1.8+0.1+1.6
Q3CL75–10157+0.1+1.0+0.9+3.6
ES166–24066+1.3-0.3-1.0-1.3
Q4CL102–61748+0.9+1.5-0.2+1.9
ES241–134467+3.1+2.3+2.1+4.9

Larger trades did not consistently produce stronger follow-through. ES's largest buys were followed by higher prices at 5 minutes, but so were its largest sells.

Does liquidity matter?

We split buys and sells in each market into quartiles by average contracts per level swept. Q1 is the thinnest liquidity; Q4 is the most concentrated.

Buys

buys: liquidity quartiles and average price moves in ticks for CL and ES
QuartileMarketAvg contracts / levelAvg levels swept10s30s1m5m
Q1CL11.47.3-2.6-2.1-1.2-4.9
ES36.36.1-1.6-3.0-2.8-2.5
Q2CL16.05.3-1.6-2.0-3.0-0.3
ES59.73.2+0.3-1.2-0.5+2.6
Q3CL37.03.4-0.9-0.6-0.7+4.3
ES97.92.5+0.9+0.70.0+1.7
Q4CL121.11.1+0.8+1.1+1.6+6.6
ES199.31.3+2.0+1.6+1.3+2.1

Sells

sells: liquidity quartiles and average price moves in ticks for CL and ES
QuartileMarketAvg contracts / levelAvg levels swept10s30s1m5m
Q1CL11.75.9+2.3+4.0+4.2+6.5
ES38.47.4+1.5+1.3+0.8+1.7
Q2CL15.65.1+0.4+0.6+0.6+2.5
ES69.12.9+3.4+1.8+1.7+1.2
Q3CL27.74.2+0.5+0.6-0.5-0.8
ES118.31.9+1.7-0.3-1.3+1.2
Q4CL104.11.0-1.10.0-0.3+0.9
ES219.71.2+2.2+1.1-0.7+1.1

Concentrated buys were followed by higher prices in both markets; concentrated sells did not lead to lower prices at 5 minutes.

Big buys and sells that hit thin liquidity tended to reverse. The separation was clearer in CL: ES sell quartiles all finished with similar positive 5 minute moves.

Can we trade it?

We turned the thin-liquditiy pattern into trading rules, exiting after 5 minutes with no stop or target. Both results include commission.

Trading results for CL and ES, including commission
MarketRound tripsWins / lossesWin rateNet PnL
CL9053 / 3758.9%+$1,636.40
ES4917 / 3234.7%-$73.70

While ES finished negative, each round trip is $3.80 in commission - without including commissions this would be profitable ($112.50). This could be worth pursuing.

Try and modify the algo yourself

Paste this into Algos for CL or ES. This version adds a 7-tick stop and limits trading to 09:30–15:55 New York time.

// Fades big trades that sweep thin liquidity.
// https://marketbyorder.com/docs/api/algos

interface SymbolParams {
  minSize: number
  thinThreshold: number
}
const SYMBOL_PARAMS: Record<string, SymbolParams> = {
  ES: { minSize: 120, thinThreshold: 45 },
  CL: { minSize: 50, thinThreshold: 15 },
}
const DEFAULT_PARAMS = SYMBOL_PARAMS.ES

function paramsFor(sym: string): SymbolParams {
  const key = Object.keys(SYMBOL_PARAMS).find(k => sym.toUpperCase().startsWith(k))
  if (!key) {
    print(`Thin Liquidity Fade: no tuned params for symbol "${sym}", falling back to ES defaults`)
    return DEFAULT_PARAMS
  }
  return SYMBOL_PARAMS[key]
}

const { minSize: MIN_SIZE, thinThreshold: THIN_THRESHOLD } = paramsFor(symbol)
const QTY = 1
const HOLD_MS = 5 * 60_000
const SL_TICKS = 7

const RTH_START = 9 * 60 + 30 // minutes after midnight, New York time
const RTH_END = 16 * 60 // minutes after midnight, New York time
const FLATTEN_BUFFER_MIN = 5

const exchangeTime = new Intl.DateTimeFormat('en-US', {
  timeZone: 'America/New_York',
  hourCycle: 'h23',
  weekday: 'short',
  hour: 'numeric',
  minute: 'numeric',
})

const sessionByMinute = new Map<number, { clock: number; weekday: string }>()
const sessionInfo = (ms: number) => {
  const minute = Math.floor(ms / 60000)
  const cached = sessionByMinute.get(minute)
  if (cached) return cached
  const parts = exchangeTime.formatToParts(ms)
  const get = (type: string) => parts.find((part) => part.type === type)?.value ?? ''
  const info = { clock: Number(get('hour')) * 60 + Number(get('minute')), weekday: get('weekday') }
  sessionByMinute.set(minute, info)
  return info
}

const isRth = (ms: number) => {
  const { clock, weekday } = sessionInfo(ms)
  return weekday !== 'Sat' && weekday !== 'Sun' && clock >= RTH_START && clock < RTH_END
}

const canEnter = (ms: number) => {
  const { clock, weekday } = sessionInfo(ms)
  return weekday !== 'Sat' && weekday !== 'Sun' && clock >= RTH_START && clock < RTH_END - FLATTEN_BUFFER_MIN
}

let exitTime: number | null = null

onUpdate(({ time, bid, ask, trades }) => {
  const pos = orders.position()

  if (pos.side !== "flat" && !canEnter(time)) {
    orders.flatten()
    exitTime = null
    return
  }
  if (orders.working().length > 0 && !canEnter(time)) {
    orders.cancelAll()
  }

  if (pos.side !== "flat" && exitTime !== null && time >= exitTime) {
    orders.flatten()
    exitTime = null
    return
  }

  if (pos.side !== "flat" || orders.working().length > 0 || !canEnter(time)) return

  for (const trade of trades) {
    if (trade.side === "none" || trade.size < MIN_SIZE) continue
    const levels = trade.levels.length
    const cpl = trade.size / levels
    if (cpl > THIN_THRESHOLD) continue

    if (trade.side === "buy") {
      const sl = bid + SL_TICKS * tickSize
      orders.sell(QTY, { sl })
      print(`FADE SELL: big BUY ${trade.size} swept ${levels} levels (cpl=${cpl.toFixed(1)}) - thin liquidity, sl=${sl.toFixed(2)}`)
    } else {
      const sl = ask - SL_TICKS * tickSize
      orders.buy(QTY, { sl })
      print(`FADE BUY: big SELL ${trade.size} swept ${levels} levels (cpl=${cpl.toFixed(1)}) - thin liquidity, sl=${sl.toFixed(2)}`)
    }
    exitTime = time + HOLD_MS
    break
  }
})

Or explore the prints with the Big Trades indicator .

Replay the tape and check these trades yourself.

Start practicing

See plans and pricing