When Small Orders Move Big Markets

"Print. 100 shares. Exchange: EDGX. Condition: Dark."

At 10:47:03.182 AM on a Tuesday, 100 shares of a $47 stock exchanged hands. No headline captured it. No index rebalanced around it. Yet this single print — an odd-lot, dark-pool execution — quietly informed the NBBO calculation, influenced the closing auction imbalance, and became one data point in a pattern that a quant researcher spent three weeks hunting.

The problem is that most standard OHLCV data hides this story entirely. A 1-minute candle shows you that 847,200 shares changed hands between 10:47 and 10:48. It does not show you that 31% of those prints were dark-pool odd-lots, that the venue composition shifted mid-candle, or that the effective spread was 2.3 cents wider than the quoted spread would suggest.

This article tears open the tick-level layer. We will examine how sales conditions encode execution venue and dark-pool routing, how odd-lot prints distort aggregated K-line data, and how to implement production-grade filtering logic that separates the signal from the microstructure noise.

Important caveat before we begin: TickDB's trades endpoint does not cover US equities or A-shares. The concepts and code patterns discussed here apply directly to HK equity and crypto trades, where TickDB does provide tick-level data. We will flag US-equity-specific nuances where they differ from the HK/crypto implementation.


1. Understanding the Anatomy of a Trade Print

Before we can filter or identify anything, we need to understand what a trade print actually contains. A trade print — the raw output of a securities exchange — is not a single number. It is a structured record with multiple fields that together tell the story of how that transaction was executed.

1.1 Standard Trade Record Fields

Field Description Example
timestamp Time of execution (exchange time or UTC normalized) 1708520823182
symbol Ticker identifier AAPL.US
price Execution price 187.43
volume Number of shares/contracts executed 100
side Buyer-initiated or seller-initiated buy
exchange Venue code where execution occurred NASDAQ, EDGX, IEX
sales_condition Bitmask of execution qualifiers 0x01, 0x08, 0x10

The first five fields are self-explanatory. The sixth — sales_condition — is where the dark pool story lives.

1.2 Sales Condition Codes: The Fine Print of Execution

Sales conditions (also called trade condition codes or execution codes) are exchange-specific bitmasks that qualify how a trade was executed. They tell you whether the print was:

  • Regular (open market)
  • Dark pool / off-exchange
  • Odd-lot
  • Derived from a derivative (e.g., calculated from options)
  • Part of a closing auction
  • Part of an opening print
  • A price variation print
  • An extended-hours print

The Financial Industry Regulatory Authority (FINRA) maintains the master registry of Trade Condition Codes. For US equities, the most relevant codes include:

Code Hex Meaning Market
0x01 Regular Standard open-market execution All US exchanges
0x04 Odd-Lot Trade involving fewer than 100 shares All US exchanges
0x08 Derivatively Priced Price derived from another security All US exchanges
0x10 Dark Pool Executed off-exchange (ATS / dark pool) FINRA/OTC
0x20 Opening Print Part of the official opening auction Primary exchange
0x40 Closing Print Part of the official closing auction Primary exchange
0x80 Sold Last Trade reported late; closing print All US exchanges
0x100 Intermarket Sweep ISO sweep across venues All US exchanges

The critical insight: A single print can carry multiple conditions simultaneously. An order might be 0x14 — odd-lot AND dark pool. The bitwise OR of multiple conditions is common. Always parse sales conditions as bitmasks, never as simple integer equality checks.


2. Dark Pool Identification: Reading the Venue Layer

Dark pools are off-exchange trading venues — alternative trading systems (ATS) that match buyers and sellers without displaying orders on public consolidated tape. FINRA publishes a list of registered ATSs, but identifying a dark pool print in raw tick data requires parsing either the exchange code or the sales condition field.

2.1 Dark Pool Identification by Exchange Code

In US equity data, off-exchange prints are identifiable by their venue codes. The major off-exchange venues include:

Venue Code Venue Name Type
EDGX Cboe EDGA/EDGX Dark ATS
BATS Cboe BATS Dark ATS
LYX Cboe BYX ATS
FINRA FINRA/Nasdaq TRF Trade Reporting Facility
TFS TFS / Instinet ATS
DP Generic dark pool indicator Various

In practice, the most reliable approach is to maintain a curated list of dark pool venue codes and match against it:

# Dark pool and off-exchange venue identifiers
# Source: FINRA ATS list + exchange publications
DARK_POOL_VENUES = {
    "EDGX", "EDGA", "BATS", "BYX", "Y",       # Cboe dark
    "FINRA", "TFS", "DP", "CHI",               # TRF and ATS
    "NSDQ",  # Some Nasdaq dark variants
}

def is_dark_pool_print(exchange_code: str) -> bool:
    """
    Identify whether a trade was executed in a dark pool or off-exchange.
    
    Returns True for off-exchange venues (ATS, dark pools, TRFs).
    Returns False for lit exchanges (NYSE, Nasdaq, Arca, etc.).
    """
    if not exchange_code:
        return False
    return exchange_code.upper() in DARK_POOL_VENUES


def get_venue_type(exchange_code: str) -> str:
    """
    Classify venue into: 'lit', 'dark', 'auction', 'unknown'.
    """
    LIT_EXCHANGES = {"NYSE", "NASDAQ", "ARCX", "IEX", "PHLX", "CBOE"}
    AUCTION_VENUES = {"NYSE", "NASDAQ"}  # Auctions reported on primary

    code_upper = exchange_code.upper()
    if code_upper in DARK_POOL_VENUES:
        return "dark"
    elif code_upper in LIT_EXCHANGES:
        return "lit"
    elif code_upper in AUCTION_VENUES:
        return "auction"
    else:
        return "unknown"

2.2 Dark Pool Identification by Sales Condition Bitmask

For data providers that include sales condition codes (such as TickDB's trades endpoint for HK equities), dark pool prints are explicitly flagged:

# FINRA / US Equity Sales Condition Bit Definitions
# See: https://www.finra.org/finra-data/otc/otcpr
TRADE_CONDITION_MASK = {
    0x01: "Regular",
    0x04: "OddLot",
    0x08: "DerivativelyPriced",
    0x10: "DarkPool",
    0x20: "OpeningPrint",
    0x40: "ClosingPrint",
    0x80: "SoldLast",
    0x100: "IntermarketSweep",
}

def parse_sales_conditions(condition_code: int) -> list[str]:
    """
    Decode a sales condition bitmask into a list of active conditions.
    
    Example:
        parse_sales_conditions(0x14) -> ['OddLot', 'DarkPool']
    """
    active = []
    for mask, name in TRADE_CONDITION_MASK.items():
        if condition_code & mask:
            active.append(name)
    return active


def is_dark_pool_condition(condition_code: int) -> bool:
    """
    Check if the DarkPool bit is set in the sales condition code.
    """
    return bool(condition_code & 0x10)


def is_odd_lot(condition_code: int) -> bool:
    """
    Check if the OddLot bit is set.
    """
    return bool(condition_code & 0x04)

2.3 Combined Dark Pool Detection

For production systems that have access to both exchange codes and sales conditions, combine both checks for maximum reliability:

from dataclasses import dataclass
from typing import Optional


@dataclass
class TradeRecord:
    timestamp: int          # Unix milliseconds
    symbol: str
    price: float
    volume: int
    side: str               # 'buy' or 'sell'
    exchange: str
    condition_code: int     # Sales condition bitmask


def classify_trade_print(trade: TradeRecord) -> dict:
    """
    Full classification of a trade print across venue and condition axes.
    Returns a dict with classification flags for downstream filtering.
    """
    conditions = parse_sales_conditions(trade.condition_code)
    
    return {
        "is_dark_pool_venue": is_dark_pool_print(trade.exchange),
        "is_dark_pool_condition": is_dark_pool_condition(trade.condition_code),
        "is_odd_lot": is_odd_lot(trade.condition_code),
        "is_auction": "OpeningPrint" in conditions or "ClosingPrint" in conditions,
        "is_derivative": "DerivativelyPriced" in conditions,
        "is_regular": conditions == ["Regular"],
        "conditions": conditions,
        "venue_type": get_venue_type(trade.exchange),
    }

3. Odd-Lot Prints: When 99 Shares Tell a Different Story

An odd-lot is a trade involving fewer than 100 shares. In the US equity market, the round-lot standard is 100 shares, and odd-lot trades historically received different regulatory treatment — they were exempt from certain quoting rules and did not contribute to the National Best Bid and Offer (NBBO) calculation in the same way.

This changed with the adoption of Rule 610(c) of Regulation NMS in 2020, which required odd-lot quotes to be included in the consolidated quotation system. However, odd-lot prints — actual executions for fewer than 100 shares — still behave differently from round-lot prints in measurable ways.

3.1 Why Odd-Lots Matter for Data Quality

Odd-lot prints introduce systematic distortions into aggregated K-line data:

Distortion type Mechanism Impact
Price noise Odd-lots frequently execute at prices between the bid and ask (sub-penny pricing in some venues) OHLC candles may include prices not available to other participants
Volume fragmentation 50 separate 100-share prints vs. 50 separate odd-lot prints produce identical volume but different candle characteristics Candle construction algorithms that weight by print count are biased
Bid-ask spread inflation Odd-lots executing at mid-point do not widen the spread but do affect volume-weighted metrics VWAP and TWAP calculations are skewed
Dark pool interaction Dark pool odd-lots frequently represent algorithmic internalization The print does not reflect genuine two-sided market interest

3.2 Odd-Lot Filtering Strategy

Whether you should filter odd-lots depends on your use case:

Use case Filter odd-lots? Rationale
Liquidity analysis No Odd-lots ARE liquidity. Excluding them understates real trading activity.
Spread estimation No Odd-lots contribute to true effective spread.
Algorithmic execution backtesting Partial Filter dark pool odd-lots; keep lit odd-lots.
Signal generation (momentum, mean-reversion) Yes Odd-lots from internalization introduce microstructure noise unrelated to price discovery.
K-line aggregation for charting Conditional Include odd-lots from lit venues; exclude dark pool odd-lots.
from enum import Enum
from typing import Callable


class OddLotFilterMode(Enum):
    KEEP_ALL = "keep_all"
    DROP_DARK_ODD_LOTS = "drop_dark_odd_lots"
    DROP_ALL_ODD_LOTS = "drop_all_odd_lots"
    KEEP_ONLY_ODD_LOTS = "keep_only_odd_lots"


def filter_odd_lots(
    trades: list[TradeRecord],
    mode: OddLotFilterMode = OddLotFilterMode.DROP_DARK_ODD_LOTS,
) -> list[TradeRecord]:
    """
    Filter trade records based on odd-lot and venue classification.
    
    Modes:
        KEEP_ALL:           No filtering — all prints included
        DROP_DARK_ODD_LOTS: Exclude prints that are both odd-lot AND dark pool
        DROP_ALL_ODD_LOTS:  Exclude all odd-lot prints regardless of venue
        KEEP_ONLY_ODD_LOTS: Inverse — keep only odd-lot prints (useful for microstructure research)
    """
    filtered = []

    for trade in trades:
        classification = classify_trade_print(trade)
        is_odd = classification["is_odd_lot"]
        is_dark = classification["is_dark_pool_venue"] or classification["is_dark_pool_condition"]

        if mode == OddLotFilterMode.KEEP_ALL:
            filtered.append(trade)
        elif mode == OddLotFilterMode.DROP_DARK_ODD_LOTS:
            if not (is_odd and is_dark):
                filtered.append(trade)
        elif mode == OddLotFilterMode.DROP_ALL_ODD_LOTS:
            if not is_odd:
                filtered.append(trade)
        elif mode == OddLotFilterMode.KEEP_ONLY_ODD_LOTS:
            if is_odd:
                filtered.append(trade)

    return filtered

4. Impact on Aggregated K-Line Data

This is where the practical consequences become visible. When you aggregate tick-level trade data into OHLCV candles, the inclusion or exclusion of dark pool and odd-lot prints measurably changes the output.

4.1 The Candle Construction Problem

Consider a 1-minute candle for a stock trading at $100. Over the interval 10:47:00 to 10:48:00, the following trades occur:

Time Price Volume Venue Condition
10:47:03 100.01 100 NYSE Regular
10:47:15 100.02 50 EDGX (dark) OddLot + DarkPool
10:47:28 100.02 500 NYSE Regular
10:47:41 99.99 25 BATS (dark) OddLot + DarkPool
10:47:55 100.03 800 IEX Regular
10:48:00 100.05 200 NYSE Regular

Without filtering: High = $100.05, Low = $99.99, Close = $100.05, Volume = 1,675 shares

With dark pool odd-lot filtering: High = $100.05, Low = $100.01, Close = $100.05, Volume = 1,600 shares

The difference is subtle in this example — a 4-cent low range and 75 shares of volume. But over thousands of candles and high-frequency dark pool activity, these differences accumulate into systematic biases that affect strategy backtests.

4.2 K-Line Aggregation with Conditional Filtering

from collections import defaultdict
from datetime import datetime


def build_kline_from_trades(
    trades: list[TradeRecord],
    interval_seconds: int = 60,
    filter_mode: OddLotFilterMode = OddLotFilterMode.DROP_DARK_ODD_LOTS,
) -> list[dict]:
    """
    Aggregate tick-level trades into OHLCV candles with configurable filtering.
    
    Args:
        trades: List of TradeRecord objects
        interval_seconds: Candle interval in seconds (60 = 1-minute, 300 = 5-minute)
        filter_mode: Odd-lot and venue filtering mode
    
    Returns:
        List of candle dictionaries: {'timestamp', 'open', 'high', 'low', 'close', 'volume', 'print_count'}
    """
    # Apply filtering before aggregation
    filtered_trades = filter_odd_lots(trades, mode=filter_mode)

    # Group trades by interval bucket
    buckets = defaultdict(list)
    for trade in filtered_trades:
        bucket_ts = (trade.timestamp // (interval_seconds * 1000)) * (interval_seconds * 1000)
        buckets[bucket_ts].append(trade)

    candles = []
    for ts in sorted(buckets.keys()):
        bucket_trades = buckets[ts]
        
        prices = [t.price for t in bucket_trades]
        volume = sum(t.volume for t in bucket_trades)

        candle = {
            "timestamp": ts,
            "open": prices[0],
            "high": max(prices),
            "low": min(prices),
            "close": prices[-1],
            "volume": volume,
            "print_count": len(bucket_trades),
        }
        candles.append(candle)

    return candles

4.3 Measuring Filter Impact Quantitatively

Before committing to a filtering strategy, measure its actual impact on your data:

def compare_filter_strategies(trades: list[TradeRecord]) -> dict:
    """
    Compare candle output across all four filter modes.
    Returns metrics showing how each strategy affects the aggregated data.
    """
    results = {}
    
    for mode in OddLotFilterMode:
        filtered = filter_odd_lots(trades, mode=mode)
        candles = build_kline_from_trades(trades, interval_seconds=60, filter_mode=mode)
        
        total_volume = sum(c["volume"] for c in candles)
        avg_candle_range = sum(c["high"] - c["low"] for c in candles) / len(candles) if candles else 0
        
        results[mode.value] = {
            "trade_count": len(filtered),
            "candle_count": len(candles),
            "total_volume": total_volume,
            "avg_candle_range_bps": (avg_candle_range / (sum(c["close"] for c in candles) / len(candles))) * 10000 if candles else 0,
        }
    
    return results

5. Fetching Tick Data from TickDB

For HK equities and crypto assets, TickDB provides the trades endpoint with sales condition data. The following production-grade code demonstrates fetching, classifying, and filtering tick data with full engineering standards.

import os
import time
import random
import logging
from typing import Optional
import requests

logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
logger = logging.getLogger(__name__)

# ─── Configuration ────────────────────────────────────────────────────────────
TICKDB_API_KEY = os.environ.get("TICKDB_API_KEY")
if not TICKDB_API_KEY:
    raise EnvironmentError("TICKDB_API_KEY environment variable is not set")

BASE_URL = "https://api.tickdb.ai"
HEADERS = {"X-API-Key": TICKDB_API_KEY}

# ─── Error Handling ───────────────────────────────────────────────────────────
def handle_api_error(response_data: dict, status_code: int) -> None:
    """Standard TickDB error handler with retry guidance."""
    code = response_data.get("code", 0)
    if code == 0:
        return  # Success
    
    error_messages = {
        1001: "Invalid API key — verify TICKDB_API_KEY",
        1002: "Missing API key — verify TICKDB_API_KEY",
        2002: "Symbol not found — check /v1/symbols/available",
        3001: "Rate limit exceeded — respect Retry-After header",
    }
    
    msg = error_messages.get(code, response_data.get("message", "Unknown error"))
    raise RuntimeError(f"TickDB error {code}: {msg} (HTTP {status_code})")


def fetch_trades_with_backoff(
    symbol: str,
    start_time: int,
    end_time: int,
    max_retries: int = 5,
) -> list[dict]:
    """
    Fetch tick-level trade data from TickDB with exponential backoff and jitter.
    
    # ⚠️ Engineering notes:
    # - Trades endpoint supports HK equities and crypto; NOT US equities or A-shares
    # - For US equity OHLCV, use the /kline endpoint instead
    # - High-frequency polling will trigger rate limits; batch requests by time window
    """
    url = f"{BASE_URL}/v1/market/trades"
    params = {
        "symbol": symbol,
        "start": start_time,
        "end": end_time,
    }

    for attempt in range(max_retries):
        try:
            response = requests.get(
                url,
                headers=HEADERS,
                params=params,
                timeout=(3.05, 15),
            )

            if response.status_code == 200:
                data = response.json()
                if data.get("code") == 3001:
                    retry_after = int(response.headers.get("Retry-After", 5))
                    logger.warning(f"Rate limited. Retrying after {retry_after}s...")
                    time.sleep(retry_after)
                    continue
                
                handle_api_error(data, response.status_code)
                return data.get("data", {}).get("trades", [])
            
            elif response.status_code == 429:
                wait = int(response.headers.get("Retry-After", 60))
                logger.warning(f"HTTP 429. Backing off for {wait}s...")
                time.sleep(wait)
                continue
            
            else:
                response.raise_for_status()

        except requests.exceptions.Timeout:
            logger.warning(f"Timeout on attempt {attempt + 1}. Retrying...")
        
        except requests.exceptions.RequestException as e:
            logger.error(f"Request failed: {e}")
            if attempt == max_retries - 1:
                raise
        
        # Exponential backoff with jitter
        base_delay = 1.0
        delay = min(base_delay * (2 ** attempt), 30.0)
        jitter = random.uniform(0, delay * 0.1)
        time.sleep(delay + jitter)
    
    raise RuntimeError(f"Failed to fetch trades after {max_retries} attempts")


# ─── Example Usage ─────────────────────────────────────────────────────────────
if __name__ == "__main__":
    # Fetch HK equity tick data for a 5-minute window
    # Timestamps in Unix milliseconds
    end_ts = int(time.time() * 1000)
    start_ts = end_ts - (5 * 60 * 1000)  # 5 minutes ago

    symbol = "0700.HK"  # Tencent Holdings

    logger.info(f"Fetching trades for {symbol} from {start_ts} to {end_ts}")
    
    trades_raw = fetch_trades_with_backoff(symbol, start_ts, end_ts)
    
    # Convert to TradeRecord objects
    trade_records = [
        TradeRecord(
            timestamp=t["time"],
            symbol=t["symbol"],
            price=t["price"],
            volume=t["volume"],
            side="buy" if t.get("side") == 1 else "sell",
            exchange=t.get("exchange", "UNKNOWN"),
            condition_code=t.get("condition", 0),
        )
        for t in trades_raw
    ]

    logger.info(f"Fetched {len(trade_records)} trades")

    # Classify and analyze
    for trade in trade_records[:5]:
        classification = classify_trade_print(trade)
        print(f"Price: {trade.price} | Vol: {trade.volume} | "
              f"Conditions: {classification['conditions']} | "
              f"Venue: {classification['venue_type']}")

    # Compare filter strategies
    comparison = compare_filter_strategies(trade_records)
    for mode, metrics in comparison.items():
        print(f"{mode}: {metrics['trade_count']} trades, "
              f"{metrics['candle_count']} candles, "
              f"total volume {metrics['total_volume']}")

6. Practical Deployment Recommendations

The filtering and classification logic above is a foundation. Production deployment requires decisions tailored to your specific use case.

6.1 Filter Strategy by Use Case

Strategy When to use Code configuration
Aggressive (DROP_ALL_ODD_LOTS) Momentum/mean-reversion signals where microstructure noise dominates filter_mode=OddLotFilterMode.DROP_ALL_ODD_LOTS
Moderate (DROP_DARK_ODD_LOTS) General-purpose backtesting; most common choice filter_mode=OddLotFilterMode.DROP_DARK_ODD_LOTS
Permissive (KEEP_ALL) Liquidity research, effective spread analysis, market impact studies filter_mode=OddLotFilterMode.KEEP_ALL
Inverse (KEEP_ONLY_ODD_LOTS) Internalization rate analysis, dark pool routing studies filter_mode=OddLotFilterMode.KEEP_ONLY_ODD_LOTS

6.2 Data Source Selection

Data need Recommended TickDB endpoint Coverage
US equity OHLCV (historical backtest) /v1/market/kline 10+ years, cleaned and aligned
US equity current candle /v1/market/kline/latest Real-time
HK equity tick-level trades /v1/market/trades Full condition codes
Crypto tick-level trades /v1/market/trades Full condition codes
Order book depth /v1/market/depth US: L1 / HK & Crypto: L1–L10

6.3 Backtest Implications

If you are building a strategy that will be deployed live, your backtest data filtering strategy must match your live data pipeline. A common failure mode:

  1. Backtester uses unfiltered OHLCV candles (includes dark pool odd-lots)
  2. Live execution engine receives only lit-market data
  3. Backtest performance exceeds live performance by 2–4% annually

Always align your backtest filtering with your live data source. If you are trading on lit exchange fills, filter your historical data the same way.


7. Summary: What Every Quant Should Know About Dark Pools and Odd-Lots

Three principles should guide every quant engineer's approach to dark pool and odd-lot data:

First, sales condition codes are a first-class data field, not metadata. Parse them as bitmasks. The interaction between OddLot and DarkPool flags is the most common source of microstructure noise in retail-accessible tick data. If your data provider does not expose sales conditions, treat their tick data as a curated subset — not a complete picture of market activity.

Second, filtering decisions are strategy-specific, not universal. There is no correct answer to "should I filter dark pool prints?" The answer is "it depends on what your strategy is trying to capture." Liquidity analysis requires odd-lots. Momentum signals are corrupted by them. Build your filtering logic as a parameterized function, not a hardcoded assumption.

Third, the K-line you see is not the K-line the market experienced. Every OHLCV candle is a filtered, aggregated view of underlying prints. The dark pool and odd-lot content of that candle determines whether it reflects genuine price discovery or internalization-driven noise. Before you trust a candle for a backtest, ask what was filtered — and whether that matches what you will trade.


Next Steps

If you want to explore HK equity tick data with full condition code support, sign up at tickdb.ai — the free tier provides access to the trades endpoint with complete sales condition data for HK equities and major crypto pairs.

If you need 10+ years of US equity historical OHLCV data for cross-cycle backtesting, explore the /v1/market/kline endpoint on Professional plans.

If you're building a live trading system, ensure your tick data pipeline logs both filtered and unfiltered trade counts per candle — this diagnostic will save you hours of backtest-vs-live discrepancy debugging.

If you're using AI coding tools, install the tickdb-market-data SKILL in your AI tool's marketplace to get TickDB API integration directly in your development environment.


This article does not constitute investment advice. Market microstructure phenomena such as dark pool routing and odd-lot execution vary by security, venue, and time period. Backtesting results do not guarantee future performance.