Detecting choppy markets with volume imbalance
How to use buy/sell volume imbalance as a chop filter, with the exact threshold logic from poly5m-v4 and poly15m-v6.
What chop costs you
A choppy market is one where price oscillates without direction. In a directional strategy, entering during chop means paying taker fees on a trade that has roughly coin-flip odds. Over 20 such trades at 1.5% fee per side, you have spent 30% of position size with no edge.
The goal of a chop filter is not to win every trade -- it is to sit out the trades where your signal has no predictive value.
Volume imbalance defined
Volume imbalance measures whether buyers or sellers are more aggressive on an exchange order book within a recent window. The canonical Binance calculation uses the aggTrade stream:
imbalance = (buyVolume - sellVolume) / (buyVolume + sellVolume)
The result is in [-1, +1]. +0.7 means 85% of the volume was buy-initiated. -0.7 means 85% was sell-initiated. Near zero means balanced -- neither side is dominating.
Collecting aggTrade data via WebSocket
const { WebSocket } = require('ws')
let buyVol = 0
let sellVol = 0
function startImbalanceTracker(symbol = 'BTCUSDT', windowMs = 60_000) {
const ws = new WebSocket(`wss://stream.binance.com:9443/ws/${symbol.toLowerCase()}@aggTrade`)
ws.on('message', (raw) => {
const trade = JSON.parse(raw)
const qty = parseFloat(trade.q)
if (trade.m) {
// maker was the buyer → this is a sell-initiated trade
sellVol += qty
} else {
buyVol += qty
}
})
// Reset window periodically
setInterval(() => {
buyVol = 0
sellVol = 0
}, windowMs)
return {
getImbalance() {
const total = buyVol + sellVol
if (total === 0) return 0
return (buyVol - sellVol) / total
},
}
}Note: trade.m being true means the trade matched against a resting buy (maker), making the aggressive side the seller.
The skip threshold
Our bots skip a trade when |imbalance| < threshold. The threshold is set so that directional signals below it are likely noise. In poly5m-v4 the default is 0.15: if neither buyers nor sellers are clearly dominant, skip.
const IMBALANCE_THRESHOLD = 0.15
function shouldSkipChop(imbalance) {
return Math.abs(imbalance) < IMBALANCE_THRESHOLD
}Higher thresholds (0.25-0.35) make the bot more selective but reduce trade frequency. During backtesting on BTC 5-minute data, thresholds in the 0.15-0.20 range gave the best risk-adjusted returns.
Combining imbalance with price momentum
Volume imbalance alone can give false signals on large single trades. Combining it with a price momentum condition reduces noise:
function hasDirectionalSignal(
priceChangePct,
imbalance,
priceThreshold = 0.0005,
imbalanceThreshold = 0.15,
) {
const strongBuy = priceChangePct > priceThreshold && imbalance > imbalanceThreshold
const strongSell = priceChangePct < -priceThreshold && imbalance < -imbalanceThreshold
return { strongBuy, strongSell, isChop: !strongBuy && !strongSell }
}This requires both price movement AND volume confirmation. If only one is present, the filter treats it as chop and skips.
ATR as a secondary chop indicator
Average True Range (ATR) measures recent volatility. Very low ATR suggests the asset is flat and unlikely to provide the required move before expiry. Poly5m-v4 uses a 14-period ATR on 1-minute Binance candles as a secondary gate:
function atr(highs, lows, closes, period = 14) {
const trs = highs.map((h, i) => {
if (i === 0) return h - lows[i]
return Math.max(h - lows[i], Math.abs(h - closes[i - 1]), Math.abs(lows[i] - closes[i - 1]))
})
// Simple EMA-style ATR
let atrVal = trs.slice(0, period).reduce((a, b) => a + b, 0) / period
for (let i = period; i < trs.length; i++) {
atrVal = (atrVal * (period - 1) + trs[i]) / period
}
return atrVal
}Skip if currentATR < MIN_ATR_USD, where MIN_ATR_USD is set to the minimum move needed to cover fees and hit the target profit margin.
Logging skips
Always log chop skips. If you are skipping 90% of candles, either the threshold is too high or the market is genuinely directionless and not worth trading at all. Review the skip ratio weekly.
if (shouldSkipChop(imbalance)) {
console.log(`[CHOP] imbalance=${imbalance.toFixed(3)}, skipping`)
return
}Summary
- Volume imbalance = (buyVol - sellVol) / totalVol, calculated from Binance aggTrade stream.
- Skip when
|imbalance| < 0.15(tune based on your backtest). - Combine with price momentum: require both for a directional signal.
- Use ATR as a secondary gate to skip flat market periods entirely.
- Log all chop skips and review the ratio periodically.
Related bots
poly5m-v4 — Split-window momentum BTC scalper
Buy-both-sides Gabagool on 5-min BTC. Picks up cheap YES + NO at $0.49 or less, locks profit when pair stays below $0.98. CLOB v2 / pUSD, polls books every 2s from 10s to 260s.
poly15m-v6 — Enhanced orderbook signal BTC scalper
Polymarket book + Chainlink RTDS + CVD + OFI + chop filter + trained meta-model. Needs 3 of 8 confirmation layers before FOK taker at $0.52. Quarter-Kelly with drawdown scaling, holds to resolution.