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.







