Order Flow Visualization Development (Footprint Chart)
When your trading system only sees OHLCV, you're blind to the battle inside each candle. Footprint Charts expose the microstructure: at every price level, you see exactly how many contracts were bought and sold. We've spent 10+ years building trading tools for funds and prop traders, and we know how to turn raw tick data into a clear footprint that gives you a real edge.
What Footprint Reveals That OHLCV Hides
A standard candle shows open, high, low, close, and total volume. A Footprint candle shows what happened at each price level: for example, at $42,050 there were 8.5 BTC of aggressive buys and 2.1 BTC of aggressive sells, while at $42,100 there were 3.2 buys and 12.4 sells. This is the actual footprint of market participants.
Key concepts: Ask volume (aggressive buys), Bid volume (aggressive sells), Delta (Ask - Bid), Imbalance (one side dominates by a threshold, typically 3×), and Point of Control (POC — the price level with maximum total volume).
How We Classify Trades for Accurate Footprint
The footprint is built from tick data — every individual trade. Each trade must be classified as aggressive buy or sell. We use the quote rule when bid/ask is available, and fall back to the tick rule. Exchanges often provide a direction flag directly (e.g., Binance's isBuyerMaker).
type Trade struct { Price decimal.Decimal Quantity decimal.Decimal Timestamp int64 IsBuy bool // true = aggressive buy (executed at ask) } // Classification by tick rule or quote rule type TradeClassifier struct { lastPrice decimal.Decimal lastBid decimal.Decimal lastAsk decimal.Decimal } // Quote rule: more accurate (requires bid/ask at trade time) func (tc *TradeClassifier) ClassifyByQuote(trade RawTrade) bool { midPrice := tc.lastBid.Add(tc.lastAsk).Div(decimal.New(2, 0)) return trade.Price.GreaterThanOrEqual(midPrice) // >= mid = buy } // Tick rule: fallback when bid/ask unavailable func (tc *TradeClassifier) ClassifyByTick(trade RawTrade) bool { if trade.Price.GreaterThan(tc.lastPrice) { return true // uptick = buy } if trade.Price.LessThan(tc.lastPrice) { return false // downtick = sell } // Zero tick — use previous classification return tc.lastWasBuy } Classification methods comparison
| Method | Accuracy | Required Data |
|---|---|---|
| Quote rule | High | Bid/ask at trade time |
| Tick rule | Medium | Price only |
| Exchange flag (isBuyerMaker) | High | Field from exchange |
Why Footprint Gives 10x More Information Than OHLCV
One OHLCV candle has 4 numbers. A Footprint candle contains dozens of numbers at each price level, revealing the market microstructure: who dominates at each tick. This is invaluable for algorithmic trading. Compare: OHLCV gives total volume; footprint gives volume distribution across prices.
Aggregating a Footprint Candle
type FootprintLevel struct { Price decimal.Decimal BidVol decimal.Decimal // aggressive sells AskVol decimal.Decimal // aggressive buys Delta decimal.Decimal // AskVol - BidVol } type FootprintCandle struct { Timestamp int64 Open decimal.Decimal High decimal.Decimal Low decimal.Decimal Close decimal.Decimal Volume decimal.Decimal Delta decimal.Decimal // total delta for candle Levels map[string]*FootprintLevel // price -> level data POC decimal.Decimal // level with max volume BuyPOC decimal.Decimal // level with max ask volume SellPOC decimal.Decimal // level with max bid volume } type FootprintBuilder struct { tickSize decimal.Decimal // price step for grouping (e.g., 10 USD for BTC) candles map[int64]*FootprintCandle // timestamp -> candle mu sync.Mutex } func (fb *FootprintBuilder) AddTrade(trade Trade, timeframe time.Duration) { fb.mu.Lock() defer fb.mu.Unlock() // Compute bucket for time timeframe bucket := (trade.Timestamp / int64(timeframe)) * int64(timeframe) candle := fb.getOrCreateCandle(bucket, trade.Price) // Group price by tick size priceBucket := trade.Price.Div(fb.tickSize).Floor().Mul(fb.tickSize) level := fb.getOrCreateLevel(candle, priceBucket) if trade.IsBuy { level.AskVol = level.AskVol.Add(trade.Quantity) } else { level.BidVol = level.BidVol.Add(trade.Quantity) } level.Delta = level.AskVol.Sub(level.BidVol) // Update OHLCV candle.Volume = candle.Volume.Add(trade.Quantity) candle.Delta = candle.Delta.Add(trade.IsBuyDelta(trade.Quantity)) if trade.Price.GreaterThan(candle.High) { candle.High = trade.Price } if trade.Price.LessThan(candle.Low) { candle.Low = trade.Price } candle.Close = trade.Price // Update POC candle.POC = fb.findPOC(candle) } func (fb *FootprintBuilder) findPOC(candle *FootprintCandle) decimal.Decimal { var maxVol decimal.Decimal var poc decimal.Decimal for price, level := range candle.Levels { total := level.AskVol.Add(level.BidVol) if total.GreaterThan(maxVol) { maxVol = total poc, _ = decimal.NewFromString(price) } } return poc } Detecting Imbalances: The Core Signal
Imbalance is the key footprint pattern. A level where ask volume is 3× bid volume is a "glass floor" (buyers dominated). A level where bid volume is 3× ask volume is a "glass ceiling".
type ImbalanceDetector struct { threshold decimal.Decimal // typically 300% (3x) } type Imbalance struct { Price decimal.Decimal Type string // "bid" or "ask" Ratio decimal.Decimal Volume decimal.Decimal } func (id *ImbalanceDetector) FindImbalances(candle *FootprintCandle) []Imbalance { var imbalances []Imbalance sortedLevels := candle.SortedLevels() // ascending by price for i, level := range sortedLevels { if i == 0 { continue } below := sortedLevels[i-1] // Compare ask of current level with bid of level below // "Stacked imbalance" — several consecutive if level.AskVol.IsPositive() && below.BidVol.IsPositive() { ratio := level.AskVol.Div(below.BidVol).Mul(decimal.New(100, 0)) if ratio.GreaterThan(id.threshold) { imbalances = append(imbalances, Imbalance{ Price: level.Price, Type: "ask", Ratio: ratio, Volume: level.AskVol, }) } } } return imbalances } Frontend Rendering: Canvas at Scale
Footprint is more complex than standard candles: each price level contains numbers. HTML Canvas is the only performant way to render hundreds of candles with full detail.
class FootprintRenderer { private canvas: HTMLCanvasElement; private ctx: CanvasRenderingContext2D; renderCandle(candle: FootprintCandle, x: number, candleWidth: number, priceToY: (price: number) => number) { const ctx = this.ctx; const levels = candle.getSortedLevels(); const levelHeight = Math.abs(priceToY(levels[0].price) - priceToY(levels[1]?.price || levels[0].price - candle.tickSize)); for (const level of levels) { const y = priceToY(level.price); const maxLevelVol = candle.maxLevelVolume; const askWidth = (level.askVol / maxLevelVol) * (candleWidth * 0.45); const bidWidth = (level.bidVol / maxLevelVol) * (candleWidth * 0.45); ctx.fillStyle = 'rgba(0, 177, 94, 0.3)'; ctx.fillRect(x + candleWidth/2, y, askWidth, levelHeight - 1); ctx.fillStyle = 'rgba(232, 66, 66, 0.3)'; ctx.fillRect(x + candleWidth/2 - bidWidth, y, bidWidth, levelHeight - 1); if (level.price === candle.poc) { ctx.strokeStyle = '#FFD700'; ctx.lineWidth = 1; ctx.strokeRect(x, y, candleWidth, levelHeight - 1); } if (levelHeight > 12) { ctx.fillStyle = '#6b7087'; ctx.font = `${Math.min(levelHeight - 2, 10)}px JetBrains Mono`; ctx.textAlign = 'left'; ctx.fillText(formatVol(level.bidVol), x + 2, y + levelHeight - 3); ctx.textAlign = 'right'; ctx.fillText(formatVol(level.askVol), x + candleWidth - 2, y + levelHeight - 3); } if (level.imbalanceType === 'ask') { ctx.fillStyle = 'rgba(0, 177, 94, 0.8)'; ctx.fillRect(x, y, 3, levelHeight); } else if (level.imbalanceType === 'bid') { ctx.fillStyle = 'rgba(232, 66, 66, 0.8)'; ctx.fillRect(x, y, 3, levelHeight); } } } renderDeltaBar(candle: FootprintCandle, x: number, candleWidth: number, baseY: number) { const ctx = this.ctx; const delta = candle.delta; const maxDelta = this.maxAbsDelta; const barWidth = Math.abs(delta / maxDelta) * (candleWidth / 2); const color = delta >= 0 ? '#00B15E' : '#E84242'; ctx.fillStyle = color; if (delta >= 0) { ctx.fillRect(x + candleWidth / 2, baseY, barWidth, 8); } else { ctx.fillRect(x + candleWidth / 2 - barWidth, baseY, barWidth, 8); } } } Delta Profile Within a Candle
Cumulative delta across internal bars shows the struggle between buyers and sellers:
function calculateCumulativeDelta(trades: Trade[], bucketSize: number): CumDeltaPoint[] { const points: CumDeltaPoint[] = []; let cumDelta = 0; for (const trade of trades) { cumDelta += trade.isBuy ? trade.quantity : -trade.quantity; points.push({ ts: trade.timestamp, price: trade.price, cumDelta }); } return points; } Data Storage and Performance
Footprint data is far bulkier than OHLCV. For BTC/USDT 1m with a $10 tick: ~15 levels per candle. That's 1,440 candles × 15 levels × 2 values = 43,200 records per day for a single timeframe. Optimal storage is TimescaleDB with compression, reducing size 5–20×. Our production systems handle 8,000 trades per second per core, enabling real-time rendering of multiple timeframes.
Detail: Choosing the tick size
Tick size determines the price step for grouping levels. For BTC/USDT we typically use $10; for altcoins, $0.01. The choice depends on volatility and liquidity.Our Process for Building a Footprint System
- Requirements analysis and data source identification (exchange APIs)
- Storage schema design and classifier architecture
- Core implementation: trade classifier, builder, imbalance detector
- Frontend: Canvas renderer with zoom and scroll
- Integration with real-time data via WebSocket
- Historical backtesting and validation
- Deployment with monitoring and alerting
What You Get
- Full source code with comments
- API and architecture documentation
- Usage examples with test data
- Repository access with CI/CD
- Team training session
- 3 months of post-delivery support
Timelines and Cost
Timelines depend on data volume and complexity. Estimates:
- Basic footprint chart for one pair: 2–3 months
- Full platform with multiple timeframes, alerts, and backtesting: 4–6 months
Cost is calculated individually. Our engineers have 10+ years of experience in trading system development, delivering 15+ projects for funds and prop traders. Contact us for a free project evaluation — we'll provide an estimate and a consultation.







