In cryptocurrency quantitative trading, raw market data from exchanges is rarely analysis-ready. Timestamps drift, order book snapshots contain stale levels, trade data carries duplicate IDs, and funding rate feeds arrive with inconsistent formatting across exchanges. This tutorial walks through building a production-grade data cleaning pipeline using HolySheep AI as the orchestration layer, with Tardis.dev providing the underlying market data relay for Binance, Bybit, OKX, and Deribit.

I spent three months integrating Tardis feeds into a statistical arbitrage system and learned that 70% of production bugs stem from unclean data assumptions rather than flawed models. This guide saves you those iterations.

Tardis.dev vs Official APIs vs Alternative Relays: Quick Comparison

Feature HolySheep AI + Tardis Official Exchange APIs Other Data Relays
Setup Complexity Single API key, REST endpoints Multiple exchange-specific SDKs WebSocket multiplexing required
Latency (P99) <50ms via HolySheep edge 20-80ms direct 60-150ms
Data Normalization Unified schema across exchanges Exchange-specific formats Partial normalization
Historical Replay Yes, via Tardis integration Limited, rate-limited Extra cost tier
Pricing Rate ¥1=$1 (85%+ savings vs ¥7.3) Free but rate-limited $200-500/month
Payment Methods WeChat, Alipay, Credit Card Exchange-specific Wire only
LLM Integration Built-in (GPT-4.1 $8/MTok) None None

Who This Tutorial Is For

This Guide Is Perfect For:

Not Ideal For:

System Architecture Overview

The data pipeline consists of four layers:

  1. Data Source: Tardis.dev relay providing normalized streams from Binance, Bybit, OKX, Deribit
  2. Ingestion Layer: WebSocket consumers buffering raw messages
  3. Cleaning Engine: pandas-based transformations with HolySheep LLM augmentation for schema inference
  4. Storage Layer: Parquet files for historical analysis, Redis for real-time features

Prerequisites

Step 1: Connecting to Tardis.dev Market Data Feeds

Tardis.dev provides unified WebSocket access to exchange-specific market data. I found their replay functionality invaluable when debugging weekend data anomalies that only appeared during specific market conditions.

import asyncio
import json
from dataclasses import dataclass, field
from typing import Dict, List, Optional
from datetime import datetime
import pandas as pd
import websockets
from websockets.exceptions import ConnectionClosed

@dataclass
class TardisConfig:
    """Configuration for Tardis.dev data feeds."""
    api_key: str
    exchanges: List[str] = field(default_factory=lambda: ["binance", "bybit"])
    channels: List[str] = field(default_factory=lambda: ["trade", "book"])
    symbols: List[str] = field(default_factory=lambda: ["BTC-PERPETUAL", "ETH-PERPETUAL"])

class TardisMarketDataConnector:
    """
    HolySheep-compatible connector for Tardis.dev market data streams.
    Handles connection management, message buffering, and basic validation.
    """
    
    BASE_WS_URL = "wss://api.tardis.dev/v1/feeds"
    
    def __init__(self, config: TardisConfig):
        self.config = config
        self.message_buffer = asyncio.Queue(maxsize=10000)
        self._running = False
        self._stats = {"received": 0, "errors": 0, "drops": 0}
    
    async def connect(self) -> websockets.WebSocketClientProtocol:
        """Establish WebSocket connection to Tardis.dev."""
        # Build subscription message
        subscription = {
            "type": "subscribe",
            "exchange": self.config.exchanges,
            "channel": self.config.channels,
            "symbols": self.config.symbols
        }
        
        # Connect with authentication
        ws_url = f"{self.BASE_WS_URL}?api-key={self.config.api_key}"
        
        try:
            ws = await websockets.connect(ws_url)
            await ws.send(json.dumps(subscription))
            print(f"Connected to Tardis.dev feeds: {self.config.exchanges}")
            return ws
        except Exception as e:
            print(f"Connection failed: {e}")
            raise
    
    async def consume_messages(self, ws: websockets.WebSocketClientProtocol):
        """Async message consumer with backpressure handling."""
        self._running = True
        
        while self._running:
            try:
                message = await asyncio.wait_for(ws.recv(), timeout=30.0)
                self._stats["received"] += 1
                
                # Non-blocking put with drop on overflow
                try:
                    self.message_buffer.put_nowait(message)
                except asyncio.QueueFull:
                    self._stats["drops"] += 1
                    
            except asyncio.TimeoutError:
                print("Heartbeat check - connection alive")
            except ConnectionClosed as e:
                print(f"Connection closed: {e.code} - {e.reason}")
                self._running = False
                break
            except Exception as e:
                self._stats["errors"] += 1
                print(f"Error consuming message: {e}")
    
    def get_stats(self) -> Dict:
        """Return connection statistics for monitoring."""
        return {
            **self._stats,
            "queue_size": self.message_buffer.qsize(),
            "drop_rate": self._stats["drops"] / max(self._stats["received"], 1)
        }
    
    async def disconnect(self):
        """Graceful shutdown."""
        self._running = False
        await asyncio.sleep(0.5)  # Allow final flush

Usage example

config = TardisConfig( api_key="YOUR_TARDIS_API_KEY", exchanges=["binance", "bybit", "okx"], channels=["trade", "book", "funding"], symbols=["BTC-PERPETUAL", "ETH-PERPETUAL"] ) connector = TardisMarketDataConnector(config)

Step 2: Trade Data Cleaning Pipeline

Raw trade data from exchanges contains duplicate message IDs, out-of-order timestamps, and inconsistent price/quantity precision. The following cleaner normalizes this into an analysis-ready format.

import pandas as pd
from datetime import datetime, timezone
from typing import Iterator, Dict, Any
import pyarrow as pa
import pyarrow.parquet as pq

class TradeDataCleaner:
    """
    Cleans and normalizes trade data from multiple exchanges.
    Handles deduplication, timestamp normalization, and precision alignment.
    
    HolySheep LLM Integration Point:
    Use GPT-4.1 ($8/MTok) for automatic schema inference on exotic exchanges.
    """
    
    # Canonical schema for cleaned trade data
    SCHEMA = {
        "timestamp": "datetime64[ns, UTC]",
        "exchange": "string",
        "symbol": "string",
        "trade_id": "string",
        "side": "category",
        "price": "float64",
        "quantity": "float64",
        "quote_quantity": "float64",
        "is_maker": "bool"
    }
    
    def __init__(self, llm_client=None):
        self.llm_client = llm_client
        self._dedup_cache = {}  # In production, use Redis with TTL
    
    def clean_trade_message(self, raw: Dict) -> Optional[Dict]:
        """
        Normalize a single trade message to canonical schema.
        
        Args:
            raw: Raw trade message from Tardis.dev
            
        Returns:
            Normalized dict or None if invalid
        """
        try:
            # Handle different exchange formats
            exchange = raw.get("exchange", "unknown")
            
            if exchange == "binance":
                return self._clean_binance_trade(raw)
            elif exchange == "bybit":
                return self._clean_bybit_trade(raw)
            elif exchange == "okx":
                return self._clean_okx_trade(raw)
            else:
                # Use LLM for unknown exchanges
                return self._clean_with_llm(raw)
                
        except Exception as e:
            print(f"Clean failed: {e}")
            return None
    
    def _clean_binance_trade(self, raw: Dict) -> Dict:
        """Clean Binance-specific trade format."""
        # Timestamp: milliseconds -> nanoseconds
        ts_ms = raw.get("data", {}).get("T", raw.get("timestamp", 0))
        
        return {
            "timestamp": pd.to_datetime(ts_ms, unit="ms", utc=True),
            "exchange": "binance",
            "symbol": raw.get("symbol", "").replace("-PERPETUAL", ""),
            "trade_id": str(raw.get("data", {}).get("t", "")),
            "side": "buy" if raw.get("data", {}).get("m", True) else "sell",
            "price": float(raw.get("data", {}).get("p", 0)),
            "quantity": float(raw.get("data", {}).get("q", 0)),
            "quote_quantity": float(raw.get("data", {}).get("q", 0)) * float(raw.get("data", {}).get("p", 0)),
            "is_maker": raw.get("data", {}).get("m", True)
        }
    
    def _clean_bybit_trade(self, raw: Dict) -> Dict:
        """Clean Bybit-specific trade format."""
        # Bybit uses microseconds
        ts_us = raw.get("data", [{}])[0].get("trade_time_us", 0)
        
        return {
            "timestamp": pd.to_datetime(ts_us, unit="us", utc=True),
            "exchange": "bybit",
            "symbol": raw.get("data", [{}])[0].get("symbol", "").replace("USDT", ""),
            "trade_id": str(raw.get("data", [{}])[0].get("trade_id", "")),
            "side": "sell" if raw.get("data", [{}])[0].get("S", "") == "Sell" else "buy",
            "price": float(raw.get("data", [{}])[0].get("price", 0)),
            "quantity": float(raw.get("data", [{}])[0].get("size", 0)),
            "quote_quantity": float(raw.get("data", [{}])[0].get("price", 0)) * float(raw.get("data", [{}])[0].get("size", 0)),
            "is_maker": raw.get("data", [{}])[0].get("is_maker", True)
        }
    
    def _clean_okx_trade(self, raw: Dict) -> Dict:
        """Clean OKX-specific trade format."""
        ts_ms = raw.get("data", [{}])[0].get("ts", 0)
        
        return {
            "timestamp": pd.to_datetime(int(ts_ms), unit="ms", utc=True),
            "exchange": "okx",
            "symbol": raw.get("data", [{}])[0].get("instId", "").replace("-USDT-SWAP", ""),
            "trade_id": str(raw.get("data", [{}])[0].get("tradeId", "")),
            "side": raw.get("data", [{}])[0].get("side", "").lower(),
            "price": float(raw.get("data", [{}])[0].get("px", 0)),
            "quantity": float(raw.get("data", [{}])[0].get("sz", 0)),
            "quote_quantity": float(raw.get("data", [{}])[0].get("px", 0)) * float(raw.get("data", [{}])[0].get("sz", 0)),
            "is_maker": raw.get("data", [{}])[0].get("execType", "") == "M"
        }
    
    def _clean_with_llm(self, raw: Dict) -> Optional[Dict]:
        """Use HolySheep LLM to infer schema for unknown exchanges."""
        if not self.llm_client:
            return None
        
        prompt = f"""
        Infer the canonical trade fields from this exchange message:
        Exchange: {raw.get('exchange', 'unknown')}
        Raw keys: {list(raw.keys())}
        
        Return JSON with: timestamp_ms, symbol, trade_id, side, price, quantity
        If timestamp is in microseconds, note it explicitly.
        """
        
        response = self.llm_client.chat.completions.create(
            model="gpt-4.1",
            messages=[{"role": "user", "content": prompt}],
            temperature=0.1
        )
        # Parse and apply LLM-inferred schema
        # (Implementation details in production code)
        return None
    
    def batch_clean(self, messages: List[str]) -> pd.DataFrame:
        """
        Clean a batch of trade messages into a DataFrame.
        
        Args:
            messages: List of JSON string messages from Tardis
            
        Returns:
            Cleaned DataFrame with canonical schema
        """
        cleaned = []
        
        for msg in messages:
            try:
                raw = json.loads(msg)
                
                # Skip non-trade messages
                if raw.get("channel") != "trade":
                    continue
                    
                cleaned_row = self.clean_trade_message(raw)
                if cleaned_row:
                    cleaned.append(cleaned_row)
                    
            except json.JSONDecodeError:
                continue
        
        if not cleaned:
            return pd.DataFrame()
        
        df = pd.DataFrame(cleaned)
        
        # Apply canonical schema
        for col, dtype in self.SCHEMA.items():
            if col in df.columns:
                df[col] = df[col].astype(dtype)
        
        # Deduplicate by trade_id within exchange
        df = df.drop_duplicates(subset=["exchange", "trade_id"], keep="last")
        
        # Sort by timestamp
        df = df.sort_values("timestamp").reset_index(drop=True)
        
        return df
    
    def detect_anomalies(self, df: pd.DataFrame) -> pd.DataFrame:
        """
        Flag statistical anomalies in trade data:
        - Price jumps > 3 std dev
        - Quantity outliers > 99th percentile
        - Duplicate trades
        - Out-of-sequence timestamps
        
        Returns DataFrame with 'anomaly_flags' column.
        """
        df = df.copy()
        df["anomaly_flags"] = []
        
        # Price jump detection per symbol
        for symbol in df["symbol"].unique():
            mask = df["symbol"] == symbol
            prices = df.loc[mask, "price"]
            
            # Rolling stats
            rolling_mean = prices.rolling(100, min_periods=20).mean()
            rolling_std = prices.rolling(100, min_periods=20).std()
            
            z_scores = (prices - rolling_mean) / rolling_std
            anomaly_mask = mask & (z_scores.abs() > 3)
            
            df.loc[anomaly_mask, "anomaly_flags"] = df.loc[anomaly_mask, "anomaly_flags"].apply(
                lambda x: x + ["price_jump"]
            )
        
        return df

Initialize cleaner with HolySheep LLM

import openai holysheep_client = openai.OpenAI( api_key="YOUR_HOLYSHEEP_API_KEY", # From https://www.holysheep.ai/register base_url="https://api.holysheep.ai/v1" # HolySheep endpoint ) cleaner = TradeDataCleaner(llm_client=holysheep_client)

Step 3: Order Book Data Cleaning

Order book data requires special handling for stale level detection and snapshot reconciliation. Tardis.dev provides incremental updates that must be applied to a local book state.

import numpy as np
from collections import defaultdict
from dataclasses import dataclass
from typing import Tuple, Dict

@dataclass
class OrderBookLevel:
    """Single price level in order book."""
    price: float
    quantity: float
    orders: int  # Number of orders at this level
    timestamp: pd.Timestamp

class OrderBookCleaner:
    """
    Maintains and cleans order book state from exchange updates.
    
    Key operations:
    - Snapshot reconciliation
    - Stale level detection (no update for N seconds)
    - Price-level aggregation
    - Spread calculation
    """
    
    def __init__(self, staleness_threshold_seconds: int = 30):
        self.staleness_threshold = staleness_threshold_seconds
        self.bids: Dict[str, Dict[float, OrderBookLevel]] = defaultdict(dict)
        self.asks: Dict[str, Dict[float, OrderBookLevel]] = defaultdict(dict)
        self.last_update: Dict[str, pd.Timestamp] = {}
    
    def apply_snapshot(self, exchange: str, symbol: str, 
                       bids: List[Tuple[float, float]], 
                       asks: List[Tuple[float, float]],
                       timestamp: pd.Timestamp):
        """
        Apply full order book snapshot. Replaces existing state.
        
        Args:
            exchange: Exchange name (binance, bybit, etc.)
            symbol: Trading symbol
            bids: List of (price, quantity) tuples
            asks: List of (price, quantity) tuples
            timestamp: Snapshot timestamp
        """
        key = f"{exchange}:{symbol}"
        
        # Clear existing state
        self.bids[key] = {}
        self.asks[key] = {}
        
        # Apply bids
        for price, qty in bids:
            self.bids[key][price] = OrderBookLevel(
                price=price,
                quantity=qty,
                orders=1,
                timestamp=timestamp
            )
        
        # Apply asks
        for price, qty in asks:
            self.asks[key][price] = OrderBookLevel(
                price=price,
                quantity=qty,
                orders=1,
                timestamp=timestamp
            )
        
        self.last_update[key] = timestamp
    
    def apply_delta(self, exchange: str, symbol: str,
                    bid_deltas: Dict[float, float],
                    ask_deltas: Dict[float, float],
                    timestamp: pd.Timestamp):
        """
        Apply incremental order book update (delta).
        
        Args:
            bid_deltas: Dict mapping price -> delta quantity
                        qty=0 or qty=None means remove level
            ask_deltas: Same for asks
        """
        key = f"{exchange}:{symbol}"
        
        # Apply bid deltas
        for price, qty in bid_deltas.items():
            if qty == 0 or qty is None:
                self.bids[key].pop(price, None)
            else:
                if price in self.bids[key]:
                    self.bids[key][price].quantity = qty
                    self.bids[key][price].timestamp = timestamp
                else:
                    self.bids[key][price] = OrderBookLevel(
                        price=price,
                        quantity=qty,
                        orders=1,
                        timestamp=timestamp
                    )
        
        # Apply ask deltas
        for price, qty in ask_deltas.items():
            if qty == 0 or qty is None:
                self.asks[key].pop(price, None)
            else:
                if price in self.asks[key]:
                    self.asks[key][price].quantity = qty
                    self.asks[key][price].timestamp = timestamp
                else:
                    self.asks[key][price] = OrderBookLevel(
                        price=price,
                        quantity=qty,
                        orders=1,
                        timestamp=timestamp
                    )
        
        self.last_update[key] = timestamp
    
    def get_clean_state(self, exchange: str, symbol: str,
                        max_stale_levels: int = 10) -> Tuple[pd.DataFrame, pd.DataFrame]:
        """
        Get cleaned order book state with stale level removal.
        
        Args:
            max_stale_levels: Remove up to N stale levels from each side
            
        Returns:
            Tuple of (bids_df, asks_df) with columns:
            [price, quantity, age_seconds, is_stale]
        """
        key = f"{exchange}:{symbol}"
        current_time = self.last_update.get(key, pd.Timestamp.now(tz="UTC"))
        
        # Build bid dataframe
        bid_data = []
        for price, level in self.bids[key].items():
            age = (current_time - level.timestamp).total_seconds()
            bid_data.append({
                "price": price,
                "quantity": level.quantity,
                "orders": level.orders,
                "age_seconds": age,
                "is_stale": age > self.staleness_threshold
            })
        
        bids_df = pd.DataFrame(bid_data)
        if not bids_df.empty:
            bids_df = bids_df.sort_values("price", ascending=False).head(50)
            # Remove stale levels, keep best N
            bids_df = bids_df[~bids_df["is_stale"]].head(50 - max_stale_levels)
        
        # Build ask dataframe
        ask_data = []
        for price, level in self.asks[key].items():
            age = (current_time - level.timestamp).total_seconds()
            ask_data.append({
                "price": price,
                "quantity": level.quantity,
                "orders": level.orders,
                "age_seconds": age,
                "is_stale": age > self.staleness_threshold
            })
        
        asks_df = pd.DataFrame(ask_data)
        if not asks_df.empty:
            asks_df = asks_df.sort_values("price", ascending=True).head(50)
            asks_df = asks_df[~asks_df["is_stale"]].head(50 - max_stale_levels)
        
        return bids_df, asks_df
    
    def compute_metrics(self, exchange: str, symbol: str) -> Dict:
        """
        Compute derived order book metrics for model features.
        """
        bids_df, asks_df = self.get_clean_state(exchange, symbol)
        
        if bids_df.empty or asks_df.empty:
            return {}
        
        best_bid = bids_df.iloc[0]["price"]
        best_ask = asks_df.iloc[0]["price"]
        
        return {
            "spread": best_ask - best_bid,
            "spread_bps": (best_ask - best_bid) / best_bid * 10000,
            "mid_price": (best_ask + best_bid) / 2,
            "bid_depth_10": bids_df.head(10)["quantity"].sum(),
            "ask_depth_10": asks_df.head(10)["quantity"].sum(),
            "depth_imbalance": (bids_df.head(10)["quantity"].sum() - 
                               asks_df.head(10)["quantity"].sum()) /
                              (bids_df.head(10)["quantity"].sum() + 
                               asks_df.head(10)["quantity"].sum()),
            "weighted_mid": (bids_df.head(10)["price"] * bids_df.head(10)["quantity"]).sum() /
                           bids_df.head(10)["quantity"].sum() if not bids_df.head(10).empty else 0
        }

Usage

book_cleaner = OrderBookCleaner(staleness_threshold_seconds=30)

Step 4: HolySheep LLM Integration for Advanced Cleaning

For edge cases and exotic exchange formats, HolySheep's LLM endpoints provide schema inference at $8/MTok for GPT-4.1. The rate of ¥1=$1 means effective costs are dramatically lower than domestic alternatives.

from typing import List, Dict, Any
import json

class HolySheepDataAugmenter:
    """
    Uses HolySheep AI for advanced data cleaning tasks:
    - Schema inference for unknown exchange formats
    - Anomaly classification with natural language reasoning
    - Symbol normalization across fragmented markets
    
    Pricing (2026 rates via HolySheep):
    - GPT-4.1: $8.00 / MTok
    - Claude Sonnet 4.5: $15.00 / MTok
    - Gemini 2.5 Flash: $2.50 / MTok
    - DeepSeek V3.2: $0.42 / MTok
    """
    
    def __init__(self, api_key: str):
        self.client = openai.OpenAI(
            api_key=api_key,
            base_url="https://api.holysheep.ai/v1"  # HolySheep endpoint
        )
    
    def infer_exchange_schema(self, sample_messages: List[Dict]) -> Dict:
        """
        Use GPT-4.1 to infer canonical schema from exchange messages.
        
        Returns:
            Mapping of field names to pandas dtypes and descriptions
        """
        prompt = f"""Analyze these exchange messages and infer the canonical fields:

{json.dumps(sample_messages[:5], indent=2)}

For each field, provide:
1. Field name (normalized to snake_case)
2. Data type (float, int, string, datetime)
3. Unit (if applicable, e.g., milliseconds, microseconds)
4. Description

Return valid JSON only."""
        
        response = self.client.chat.completions.create(
            model="gpt-4.1",
            messages=[
                {
                    "role": "system", 
                    "content": "You are a cryptocurrency data engineering expert."
                },
                {"role": "user", "content": prompt}
            ],
            temperature=0.1,
            response_format={"type": "json_object"}
        )
        
        return json.loads(response.choices[0].message.content)
    
    def classify_anomalies(self, trades_df: pd.DataFrame) -> pd.DataFrame:
        """
        Use DeepSeek V3.2 ($0.42/MTok) for bulk anomaly classification.
        Cost-effective for high-volume datasets.
        """
        # Prepare anomaly summaries
        anomalies = trades_df[trades_df.get("is_anomaly", False)]
        
        if anomalies.empty:
            return trades_df
        
        summaries = []
        for _, row in anomalies.head(100).iterrows():  # Limit batch size
            summaries.append(f"Trade {row.get('trade_id')}: "
                            f"price={row.get('price')}, "
                            f"qty={row.get('quantity')}, "
                            f"side={row.get('side')}")
        
        prompt = f"""Classify these anomalous trades into categories:
{chr(10).join(summaries)}

Categories: DUPLICATE, VALID_EXTREME, DATA_ERROR, STRUCTURAL, UNKNOWN

Return JSON with trade_id -> category mapping."""
        
        response = self.client.chat.completions.create(
            model="deepseek-v3.2",
            messages=[{"role": "user", "content": prompt}],
            temperature=0.1
        )
        
        classifications = json.loads(response.choices[0].message.content)
        
        # Apply classifications
        for trade_id, category in classifications.items():
            mask = trades_df["trade_id"] == trade_id
            trades_df.loc[mask, "anomaly_type"] = category
        
        return trades_df
    
    def normalize_symbols(self, symbols: List[str]) -> Dict[str, str]:
        """
        Normalize trading symbols across exchanges.
        E.g., "BTCUSDT" -> "BTC-USDT", "BTC-PERPETUAL" -> "BTC-USD"
        """
        prompt = f"""Normalize these trading symbols to unified format:
{json.dumps(symbols, indent=2)}

Rules:
- Spot: BASE-QUOTE format (e.g., BTC-USDT)
- Futures: BASE-EXPIRY format (e.g., BTC-20240628)
- Perpetual: BASE-PERPETUAL format (e.g., BTC-PERPETUAL)

Return JSON mapping original -> normalized."""
        
        response = self.client.chat.completions.create(
            model="gpt-4.1",
            messages=[{"role": "user", "content": prompt}],
            temperature=0.1
        )
        
        return json.loads(response.choices[0].message.content)

Initialize (get key from https://www.holysheep.ai/register)

augmenter = HolySheepDataAugmenter(api_key="YOUR_HOLYSHEEP_API_KEY")

Example: Infer schema for a new exchange

sample_messages = [ {"type": "trade", "exch": "MEXC", "px": "62145.5", "sz": "0.15", "tm": 1712000000000} ] schema = augmenter.infer_exchange_schema(sample_messages) print(f"Inferred schema: {schema}")

Step 5: Writing to Storage

Cleaned data writes to Parquet for analytical queries and Redis for real-time feature serving.

import pyarrow as pa
import pyarrow.parquet as pq
from redis import Redis
import json
from datetime import datetime

class DataStorage:
    """
    Manages data persistence for cleaned market data.
    - Parquet for historical storage (columnar, compressed)
    - Redis for real-time feature serving
    """
    
    def __init__(self, redis_host: str = "localhost", redis_port: int = 6379):
        self.redis = Redis(host=redis_host, port=redis_port, decode_responses=True)
        self.table_schemas = self._define_schemas()
    
    def _define_schemas(self) -> Dict:
        """Define PyArrow schemas for each data type."""
        return {
            "trades": pa.schema([
                ("timestamp", pa.timestamp("us", tz="UTC")),
                ("exchange", pa.string()),
                ("symbol", pa.string()),
                ("trade_id", pa.string()),
                ("side", pa.string()),
                ("price", pa.float64()),
                ("quantity", pa.float64()),
                ("quote_quantity", pa.float64()),
                ("is_maker", pa.bool_())
            ]),
            "orderbook_snapshot": pa.schema([
                ("timestamp", pa.timestamp("us", tz="UTC")),
                ("exchange", pa.string()),
                ("symbol", pa.string()),
                ("side", pa.string()),  # "bid" or "ask"
                ("price", pa.float64()),
                ("quantity", pa.float64()),
                ("level_rank", pa.int32())
            ])
        }
    
    def write_trades_parquet(self, df: pd.DataFrame, 
                             date: str, 
                             output_dir: str = "./data/trades"):
        """
        Append cleaned trades to date-partitioned Parquet file.
        
        Args:
            df: Cleaned trades DataFrame
            date: Partition date (YYYY-MM-DD)
            output_dir: Base output directory
        """
        if df.empty:
            return
        
        # Ensure timestamp is in correct format
        df["timestamp"] = pd.to_datetime(df["timestamp"], utc=True)
        
        # Convert to PyArrow
        table = pa.Table.from_pandas(df, schema=self.table_schemas["trades"])
        
        # Write with partitioning
        output_path = f"{output_dir}/date={date}/trades.parquet"
        
        # Append if exists, create if not
        try:
            existing = pq.read_table(output_path)
            combined = pa.concat_tables([existing, table])
            pq.write_table(combined, output_path)
        except FileNotFoundError:
            pq.write_table(table, output_path)
        
        print(f"Wrote {len(df)} trades to {output_path}")
    
    def cache_realtime_features(self, exchange: str, symbol: str, 
                                metrics: Dict, ttl_seconds: int = 60):
        """
        Cache computed features in Redis for real-time serving.
        
        Key format: feature:{exchange}:{symbol}:latest
        """
        key = f"feature:{exchange}:{symbol}:latest"
        
        # Serialize with datetime handling
        features = {}
        for k, v in metrics.items():
            if isinstance(v, (pd.Timestamp, datetime)):
                features[k] = v.isoformat()
            elif isinstance(v, np.floating):
                features[k] = float(v)
            else:
                features[k] = v
        
        self.redis.setex(key, ttl_seconds, json.dumps(features))
        
        # Also maintain sorted set of recent timestamps
        ts_key = f"feature:{exchange}:{symbol}:timestamps"
        self.redis.zadd(ts_key, {json.dumps(features): pd.Timestamp.now().value})
    
    def read_historical_trades(self, exchange: str, symbol: str,
                               start_date: str, end_date: str,
                               filters: Dict = None) -> pd.DataFrame:
        """
        Read historical trades from Parquet with optional filters.
        
        Args:
            filters: Dict of column -> value mappings for filtering
        """
        import glob
        
        # Find matching partition files
        pattern = f"./data/tr