A large institution wants to buy 500,000 shares of a mid-cap stock. Placing the full order at once would move the market by 2–3%, alerting HFT firms and other participants within milliseconds. Instead, the institution breaks the order into small, timed increments — each one indistinguishable from normal retail flow — until the full position is accumulated. This is the iceberg order, and its ghost is written in the order book.

For quantitative researchers and algorithmic traders, the ability to detect these ghost orders in real time is not an academic exercise. It is a direct signal of institutional intent, a predictor of short-term price pressure, and a source of alpha that most retail participants never see.

This article dissects the mechanics of iceberg order inference: how they interact with the limit order book, what statistical signatures they leave behind, and how to build a production-grade detector using real-time depth data.


The Microstructure of Hidden Liquidity

Why Institutions Hide

An iceberg order is a type of reserve order that displays only a fraction of its true size to the market. When you submit an order for 500,000 shares with a visible "tip" of 10,000 shares, the order book shows only the 10,000. The remaining 490,000 sits in a dark pool or matching engine, executing in small slices as the visible portion is consumed.

The motivation is threefold:

  1. Market impact minimization: A single 500,000-share market order would move price immediately. Iceberg orders amortize that impact across time.
  2. Information concealment: Other participants — especially latency-arbitrage algorithms — cannot see the full order size until it is substantially filled.
  3. Execution quality improvement: By slicing the order and using passive limit orders, the institution captures the spread rather than paying it.

The Order Book Signature

When an iceberg order is active, the order book exhibits a specific pattern that distinguishes it from normal limit order activity:

Property Normal Limit Order Iceberg Order Tip
Size persistence Decreases as price moves away Remains constant regardless of time elapsed
Size replenishment New orders arrive with varied sizes Fixed-size increment appears after tip is filled
Price clustering Distributed across price levels Concentrated at a single level
Time pattern Random inter-arrival times Regular or semi-regular replenishment cycles

The key signal is size invariance under time pressure. A normal large limit order at a given price level will have its visible size reduced as other participants trade against it. An iceberg order's visible size stays constant — and when the visible portion is consumed, a fresh chunk of identical size appears at the same price level, instantly.

Quantifying the Anomaly

The most direct metric for detecting this behavior is the order size persistence ratio (OSPR):

OSPR = (Visible size at time T) / (Visible size at time T−Δ)

For a normal limit order under moderate pressure, OSPR decreases as trades consume liquidity. For an iceberg order, OSPR stays close to 1.0 across short time windows — until the hidden portion is exhausted, at which point it drops to 0 and the order disappears.

A more robust detection signal uses size replenishment detection — monitoring for identical-size increments appearing at the same price level within a short time window:

def detect_size_replenishment(order_updates, window_ms=500, size_tolerance_pct=0.05):
    """
    Detects potential iceberg order replenishment patterns.
    
    Args:
        order_updates: List of (timestamp, price_level, size) tuples
        window_ms: Time window for detecting replenishment
        size_tolerance_pct: Acceptable size variance (5% default)
    
    Returns:
        List of detected replenishment events with metadata
    """
    import bisect
    
    replenishment_events = []
    price_levels = {}  # price_level -> list of (timestamp, size)
    
    for timestamp, price_level, size in order_updates:
        if price_level not in price_levels:
            price_levels[price_level] = []
        
        events = price_levels[price_level]
        events.append((timestamp, size))
        
        # Check for replenishment within the time window
        cutoff_time = timestamp - window_ms
        recent_events = [
            (ts, sz) for ts, sz in events 
            if ts >= cutoff_time and ts < timestamp
        ]
        
        if len(recent_events) >= 2:
            sizes = [sz for _, sz in recent_events]
            avg_size = sum(sizes) / len(sizes)
            
            # Check if sizes are within tolerance of each other
            if all(abs(sz - avg_size) / avg_size < size_tolerance_pct for sz in sizes):
                replenishment_events.append({
                    'price_level': price_level,
                    'replenishment_count': len(recent_events),
                    'avg_size': avg_size,
                    'first_seen': recent_events[0][0],
                    'last_seen': recent_events[-1][0],
                    'time_span_ms': recent_events[-1][0] - recent_events[0][0]
                })
    
    return replenishment_events

Order Book Change Rate Analysis

The Fundamental Detection Framework

Beyond individual order detection, the most powerful iceberg inference comes from analyzing the rate of change in order book state across multiple snapshots.

When an iceberg order is present, the order book exhibits a characteristic behavior at the relevant price level: the displayed quantity remains anomalously stable while surrounding price levels fluctuate normally. This creates a "frozen" zone in the order book that is statistically inconsistent with natural market maker behavior.

The detection algorithm works in three stages:

Stage 1: Baseline establishment
Collect order book snapshots over a 30–60 second window to establish the natural variance in size at each price level.

Stage 2: Anomaly scoring
For each new snapshot, compute a deviation score for each price level based on how far the current size deviates from the expected range.

Stage 3: Temporal consistency check
A single anomalous reading is noise. An anomaly that persists across multiple consecutive snapshots at the same price level is a signal.

Building the Detection Pipeline

The following production-grade code implements a real-time iceberg order detection system. It uses the TickDB WebSocket depth stream to maintain a rolling window of order book snapshots and applies the statistical detection framework described above.

import json
import time
import random
import os
import math
from collections import deque
from dataclasses import dataclass
from typing import Optional, Dict, List
import threading

# ⚠️ For production HFT workloads, use aiohttp/asyncio instead of threading
# This synchronous implementation is suitable for signals operating at 1–10 Hz


@dataclass
class OrderBookSnapshot:
    """Represents a point-in-time snapshot of the order book."""
    timestamp: int  # Unix milliseconds
    bids: List[tuple]  # [(price, size), ...]
    asks: List[tuple]  # [(price, size), ...]


@dataclass
class PriceLevelStats:
    """Running statistics for a single price level."""
    sizes: deque
    mean: float
    std: float
    anomaly_count: int


class IcebergDetector:
    """
    Real-time iceberg order detection from order book depth data.
    Uses rolling window statistics to identify anomalously stable price levels.
    """
    
    def __init__(
        self,
        symbol: str,
        window_size: int = 30,
        anomaly_threshold: float = 2.5,
        persistence_required: int = 5,
        tick_size: float = 0.01
    ):
        self.symbol = symbol
        self.window_size = window_size  # Number of snapshots in rolling window
        self.anomaly_threshold = anomaly_threshold  # Z-score threshold for anomaly flag
        self.persistence_required = persistence_required  # Consecutive anomalies to confirm signal
        self.tick_size = tick_size
        
        # Rolling window of snapshots per side (bid/ask)
        self.bid_snapshots = deque(maxlen=window_size)
        self.ask_snapshots = deque(maxlen=window_size)
        
        # Price level statistics
        self.bid_stats: Dict[float, PriceLevelStats] = {}
        self.ask_stats: Dict[float, PriceLevelStats] = {}
        
        # Anomaly persistence tracking
        self.bid_anomaly_count: Dict[float, int] = {}
        self.ask_anomaly_count: Dict[float, int] = {}
        
        # Detected iceberg signals
        self.active_signals: List[Dict] = []
        self.lock = threading.Lock()
        
        # Connection state
        self.ws = None
        self.running = False
        self.reconnect_delay = 1.0
        self.max_reconnect_delay = 60.0
    
    def _compute_stats(self, sizes: List[float]) -> tuple:
        """Compute mean and standard deviation of a list of sizes."""
        if len(sizes) < 3:
            return 0.0, float('inf')  # No variance data yet
        
        mean = sum(sizes) / len(sizes)
        variance = sum((s - mean) ** 2 for s in sizes) / len(sizes)
        std = math.sqrt(variance) if variance > 0 else 0.0
        return mean, std
    
    def _update_price_level_stats(
        self,
        price: float,
        size: float,
        side: str
    ):
        """Update rolling statistics for a price level."""
        if side == 'bid':
            stats_dict = self.bid_stats
            anomaly_count_dict = self.bid_anomaly_count
            snapshots = self.bid_snapshots
        else:
            stats_dict = self.ask_stats
            anomaly_count_dict = self.ask_anomaly_count
            snapshots = self.ask_snapshots
        
        if price not in stats_dict:
            stats_dict[price] = PriceLevelStats(
                sizes=deque(maxlen=self.window_size),
                mean=0.0,
                std=0.0,
                anomaly_count=0
            )
        
        stats = stats_dict[price]
        stats.sizes.append(size)
        stats.mean, stats.std = self._compute_stats(list(stats.sizes))
        
        # Check for anomaly (size is anomalously stable — low variance)
        if len(stats.sizes) >= 5:
            # Calculate coefficient of variation (CV = std / mean)
            # Low CV indicates anomalously stable size
            if stats.mean > 0:
                cv = stats.std / stats.mean
                
                # An iceberg order tip shows high stability (low CV)
                # and larger-than-average size
                if cv < 0.1:  # Size varies less than 10%
                    anomaly_count_dict[price] = anomaly_count_dict.get(price, 0) + 1
                else:
                    anomaly_count_dict[price] = 0
    
    def process_snapshot(self, snapshot: OrderBookSnapshot):
        """Process a new order book snapshot and update detection state."""
        with self.lock:
            # Process bid side
            for price, size in snapshot.bids:
                self._update_price_level_stats(price, size, 'bid')
            
            # Process ask side
            for price, size in snapshot.asks:
                self._update_price_level_stats(price, size, 'ask')
            
            # Check for confirmed iceberg signals
            self._check_signals()
    
    def _check_signals(self):
        """Check for price levels that have persisted anomalous behavior."""
        new_signals = []
        
        for price, count in self.bid_anomaly_count.items():
            if count >= self.persistence_required:
                stats = self.bid_stats[price]
                new_signals.append({
                    'side': 'bid',
                    'price': price,
                    'inferred_visible_tip': stats.mean,
                    'anomaly_persistence': count,
                    'coefficient_of_variation': stats.std / stats.mean if stats.mean > 0 else 0
                })
        
        for price, count in self.ask_anomaly_count.items():
            if count >= self.persistence_required:
                stats = self.ask_stats[price]
                new_signals.append({
                    'side': 'ask',
                    'price': price,
                    'inferred_visible_tip': stats.mean,
                    'anomaly_persistence': count,
                    'coefficient_of_variation': stats.std / stats.mean if stats.mean > 0 else 0
                })
        
        self.active_signals = new_signals
    
    def get_active_signals(self) -> List[Dict]:
        """Return current iceberg signals (thread-safe)."""
        with self.lock:
            return list(self.active_signals)
    
    def _on_depth_message(self, message: dict):
        """Handle incoming depth update message."""
        try:
            # Parse TickDB depth message format
            # Message structure: {"code": 0, "data": {"ts": ..., "bids": [[p, s], ...], "asks": [[p, s], ...]}}
            if message.get('code') != 0:
                return
            
            data = message.get('data', {})
            snapshot = OrderBookSnapshot(
                timestamp=data.get('ts', int(time.time() * 1000)),
                bids=data.get('bids', []),
                asks=data.get('asks', [])
            )
            
            self.process_snapshot(snapshot)
            
        except Exception as e:
            print(f"Error processing depth message: {e}")
    
    def start(self, api_key: str):
        """
        Start the iceberg detector with WebSocket connection to TickDB.
        
        Args:
            api_key: TickDB API key (loaded from TICKDB_API_KEY env var)
        """
        import websocket
        
        ws_url = f"wss://api.tickdb.ai/ws/depth?symbol={self.symbol}&api_key={api_key}"
        
        def on_message(ws, message):
            self._on_depth_message(json.loads(message))
        
        def on_error(ws, error):
            print(f"WebSocket error: {error}")
        
        def on_close(ws):
            print("WebSocket connection closed")
            self.running = False
            # Reconnection logic handled in on_open
        
        def on_open(ws):
            print(f"Connected to TickDB depth stream for {self.symbol}")
            self.running = True
            self.ws = ws
            
            # Heartbeat: ping every 20 seconds
            def heartbeat():
                while self.running:
                    time.sleep(20)
                    if self.running and self.ws:
                        try:
                            self.ws.send(json.dumps({"cmd": "ping"}))
                        except Exception:
                            break
            
            heartbeat_thread = threading.Thread(target=heartbeat, daemon=True)
            heartbeat_thread.start()
        
        # Reconnection wrapper
        while True:
            try:
                ws = websocket.WebSocketApp(
                    ws_url,
                    on_message=on_message,
                    on_error=on_error,
                    on_close=on_close,
                    on_open=on_open
                )
                
                # Exponential backoff with jitter
                delay = self.reconnect_delay * (2 ** 0)  # Start at 1 second
                delay = min(delay, self.max_reconnect_delay)
                jitter = random.uniform(0, delay * 0.1)
                
                ws.run(ping_timeout=30, ping_interval=20)
                
            except Exception as e:
                print(f"Connection failed: {e}. Reconnecting in {delay + jitter:.1f}s...")
                time.sleep(delay + jitter)
                delay = min(delay * 2, self.max_reconnect_delay)


def run_detector():
    """Entry point for iceberg detection demo."""
    api_key = os.environ.get("TICKDB_API_KEY")
    if not api_key:
        raise ValueError("TICKDB_API_KEY environment variable not set")
    
    # Detect iceberg orders on Apple (US equities L1 depth available)
    detector = IcebergDetector(
        symbol="AAPL.US",
        window_size=30,
        anomaly_threshold=2.5,
        persistence_required=5
    )
    
    print(f"Starting iceberg detector for AAPL.US")
    print("Monitoring for stable order sizes that may indicate iceberg orders...")
    
    # Start detection in background thread
    detector_thread = threading.Thread(
        target=detector.start,
        args=(api_key,),
        daemon=True
    )
    detector_thread.start()
    
    # Main loop: poll for signals every 2 seconds
    try:
        while True:
            time.sleep(2)
            signals = detector.get_active_signals()
            
            if signals:
                print(f"\n[{int(time.time())}] Potential iceberg detected:")
                for sig in signals:
                    print(f"  Side: {sig['side'].upper()}")
                    print(f"  Price: ${sig['price']:.2f}")
                    print(f"  Inferred visible tip: {sig['inferred_visible_tip']:.0f} shares")
                    print(f"  CV (lower = more stable): {sig['coefficient_of_variation']:.4f}")
                    print()
    
    except KeyboardInterrupt:
        print("\nShutting down iceberg detector...")
        detector.running = False


if __name__ == "__main__":
    run_detector()

Derived Metrics: Buy/Sell Pressure Ratio

The Ratio as an Iceberg Indicator

Once iceberg orders are detected, the buy/sell pressure ratio provides the actionable trading signal. This metric aggregates the depth on each side of the book, weighted by how anomalously stable each price level appears.

A pressure ratio significantly above 1.0 on the bid side, combined with detected iceberg signals on the bids, suggests that a large buyer is accumulating shares passively. Conversely, a low pressure ratio with iceberg signals on the asks indicates passive distribution.

def compute_pressure_ratio(
    snapshot: OrderBookSnapshot,
    iceberg_signals: List[Dict],
    top_n_levels: int = 5,
    iceberg_weight: float = 1.5
) -> dict:
    """
    Compute buy/sell pressure ratio with iceberg signal weighting.
    
    Args:
        snapshot: Current order book snapshot
        iceberg_signals: List of detected iceberg signals from IcebergDetector
        top_n_levels: Number of price levels to include
        iceberg_weight: Multiplier applied to price levels with detected iceberg orders
    
    Returns:
        Dictionary with pressure ratio and component breakdown
    """
    # Extract iceberg price levels
    iceberg_bids = {sig['price'] for sig in iceberg_signals if sig['side'] == 'bid'}
    iceberg_asks = {sig['price'] for sig in iceberg_signals if sig['side'] == 'ask'}
    
    # Compute weighted bid depth
    bid_depth = 0.0
    for price, size in snapshot.bids[:top_n_levels]:
        weight = iceberg_weight if price in iceberg_bids else 1.0
        bid_depth += size * weight
    
    # Compute weighted ask depth
    ask_depth = 0.0
    for price, size in snapshot.asks[:top_n_levels]:
        weight = iceberg_weight if price in iceberg_asks else 1.0
        ask_depth += size * weight
    
    # Avoid division by zero
    if ask_depth == 0:
        ask_depth = 0.001
    
    pressure_ratio = bid_depth / ask_depth
    
    return {
        'pressure_ratio': pressure_ratio,
        'weighted_bid_depth': bid_depth,
        'weighted_ask_depth': ask_depth,
        'raw_bid_depth': sum(size for _, size in snapshot.bids[:top_n_levels]),
        'raw_ask_depth': sum(size for _, size in snapshot.asks[:top_n_levels]),
        'iceberg_bid_signals': len(iceberg_bids),
        'iceberg_ask_signals': len(iceberg_asks),
        'net_iceberg_bias': len(iceberg_bids) - len(iceberg_asks)
    }

Practical Deployment Considerations

Data Source Requirements

Iceberg detection requires access to high-frequency order book depth updates. The requirements vary by market:

Market Depth Channel Update Frequency Notes
US Equities L1 (best bid/ask) Sub-second TickDB supports L1 for US equities
HK Equities L1–L10 Up to 100ms Multi-level depth enables more robust detection
Crypto L1–L10 Sub-second Most granular data available

For the detection algorithm to function reliably, the data source must provide:

  • Sufficient update frequency: At least 1 update per second per price level
  • Consistent timestamp precision: Millisecond or better resolution
  • Complete size information: Full visible quantity at each price level

False Positive Management

Iceberg detection is inherently probabilistic. Several market conditions produce signals that resemble iceberg orders but have different causes:

  1. Market maker hedging: Large dealers maintaining passive quotes may display stable size at key levels.
  2. Index rebalancing: At known rebalancing times, institutional algorithms accumulate in predictable patterns.
  3. Thin markets: Low-volume stocks exhibit naturally higher variance in order sizes.
  4. Algorithm timing: Many algorithms use fixed-time slicing, creating periodic replenishment patterns.

The production system should implement a confidence scoring mechanism that weights these factors:

def compute_iceberg_confidence(
    signal: Dict,
    market_context: Dict,
    time_of_day: float  # Fraction of trading day (0.0 to 1.0)
) -> float:
    """
    Compute confidence score (0.0 to 1.0) for an iceberg detection signal.
    
    Args:
        signal: Single iceberg signal from IcebergDetector
        market_context: Market-wide metrics (volume, spread, volatility)
        time_of_day: Position in trading day (0.0 = open, 1.0 = close)
    
    Returns:
        Confidence score
    """
    score = 0.5  # Base score
    
    # Strong signal: very low coefficient of variation
    cv = signal.get('coefficient_of_variation', 1.0)
    if cv < 0.05:
        score += 0.25
    elif cv < 0.10:
        score += 0.15
    elif cv < 0.20:
        score += 0.05
    
    # Strong signal: high persistence
    persistence = signal.get('anomaly_persistence', 0)
    if persistence >= 10:
        score += 0.15
    elif persistence >= 5:
        score += 0.10
    
    # Context: market is active (reduces false positive from thin markets)
    avg_spread_bps = market_context.get('avg_spread_bps', 100)
    if 5 < avg_spread_bps < 50:
        score += 0.10  # Normal liquidity
    elif avg_spread_bps >= 50:
        score -= 0.15  # Wide spread = thin market, lower confidence
    
    # Context: near known rebalancing times (reduce confidence)
    # 15 minutes before/after major index closes
    if 0.88 < time_of_day < 0.92 or 0.93 < time_of_day < 0.97:
        score -= 0.20
    
    return max(0.0, min(1.0, score))

Signal Interpretation and Trading Applications

What the Signal Tells You

When an iceberg order is detected on the bid side, several inferences follow:

Immediate implications:

  • A large buyer is accumulating shares at this price level
  • The visible liquidity is "fake" — it will replenish, keeping price from rising
  • Short-term upward pressure is likely limited despite buying interest

Medium-term implications:

  • Once the hidden portion is exhausted, the buying pressure will either move up the book or become visible as market orders
  • The total order size can be estimated from the replenishment rate and time-in-force
  • If the iceberg is detected during a downtrend, it may represent smart money accumulation

Risk considerations:

  • Detection latency matters: by the time the signal is confirmed, part of the order may be filled
  • Multiple algorithms may detect the same iceberg, causing front-running
  • Iceberg orders can be cancelled and re-submitted at different levels

Use Cases

  1. Alpha generation: Detect iceberg orders on the opposing side and trade with them (follow the large buyer/seller)
  2. Execution algorithm enhancement: Detect iceberg orders ahead of your own orders to avoid trading against them
  3. Market microstructure research: Track iceberg order frequency and persistence as a proxy for institutional activity
  4. Risk monitoring: Alert when iceberg orders appear in your portfolio holdings (suggesting large repositioning by other institutions)

Deployment Architecture

For production deployment, the iceberg detection system should be structured as a streaming pipeline:

TickDB WebSocket → Depth Stream → Message Queue → Detection Engine → Signal Storage → Trading System
                                    ↓
                             Alert System (Slack/Email)

The key architectural decisions:

Component Recommendation Rationale
Ingestion WebSocket with reconnection Real-time depth updates required
Queue Redis Streams or Kafka Decouples ingestion from processing; handles burst loads
Processing Stateless function with shared state store Allows horizontal scaling
State Redis sorted sets Efficient rolling window management per price level
Alerting Async webhook delivery Non-blocking signal emission

Limitations and Honest Caveats

Iceberg order detection has fundamental limitations that must be acknowledged:

Data resolution: TickDB's depth channel provides snapshots, not individual order events. True iceberg detection requires observing each order's lifecycle individually. Snapshot-based detection infers iceberg behavior from aggregate statistics, which is less precise.

Latency: The algorithm requires 5–30 consecutive anomalous snapshots to confirm a signal. At 1 Hz update frequency, this introduces 5–30 seconds of detection latency — during which the iceberg order may be substantially filled.

Adverse selection: Any detection algorithm that generates actionable signals will attract participants who trade on those signals. This degrades the signal quality over time as market makers adjust their quoting behavior.

Market-specific variation: Iceberg order prevalence varies significantly across markets. US equities see frequent iceberg orders on large-cap stocks. Thinly traded securities may exhibit iceberg-like behavior from thin order flow that is not institutionally driven.


Next Steps

If you're building a quantitative trading system, the iceberg detector above provides a production-ready foundation. Review the configuration parameters (window size, anomaly threshold, persistence count) against your specific symbol's characteristics.

If you want to backtest iceberg detection strategies, access 10+ years of historical US equity OHLCV data via TickDB's /v1/market/kline endpoint. Note that backtesting on historical kline data requires a different detection approach (looking for price impact patterns rather than real-time order book stability).

If you're integrating this into an algorithmic trading system:

  1. Deploy the detector as a separate service with a clean API interface
  2. Implement proper error handling and monitoring (SLA tracking, alert on connection failures)
  3. Test thoroughly in paper trading mode before live deployment
  4. Monitor false positive rates and adjust thresholds quarterly

If you use AI coding assistants, install the tickdb-market-data SKILL in your AI tool's marketplace for streamlined API integration and code generation.


This article does not constitute investment advice. Markets involve risk; past patterns do not guarantee future behavior. Iceberg order detection is a probabilistic signal, not a certainty.