返回 Web3 笔记
Day 103

Day 103:订单簿与市场数据架构

L1/L2/L3数据层级、WebSocket实时推送、K线OHLCV计算与存储、TradingView集成、DeFi链上数据特殊性

2026-04-20
交易订单簿市场数据WebSocketK线TradingViewDay103

核心概念

什么是市场数据?

一句话定义:市场数据(Market Data)是交易所产生的所有与价格、订单、成交相关的信息流。它是交易者做决策的"眼睛",也是算法交易的"输入源"。

类比理解:如果交易所是一个菜市场,市场数据就是每个摊位上贴的"今日价格牌"+"排队人数"+"最近成交记录"。你看着这些信息决定去哪个摊位买菜。

市场数据的三个层级

市场数据层级体系:
═══════════════════════════════════════

L1 数据(Level 1)— 最佳报价
├── 内容:Best Bid (最高买价) + Best Ask (最低卖价)
├── 信息量:最少
├── 频率:高(每次变动推送)
├── 用途:基础交易决策、散户行情显示
├── 带宽:~1KB/s per symbol
└── 示例:
    BTC/USDT: Bid $60,050 × 2.5  |  Ask $60,055 × 1.8

L2 数据(Level 2)— 订单簿深度
├── 内容:多个价格档位的挂单量
├── 信息量:中等
├── 频率:中(通常推送前5-20档)
├── 用途:判断支撑/阻力、流动性分析
├── 带宽:~10KB/s per symbol
└── 示例:
    Ask $60,100 × 0.5
    Ask $60,080 × 1.2
    Ask $60,055 × 1.8  ← Best Ask
    ─────────────────
    Bid $60,050 × 2.5  ← Best Bid
    Bid $60,030 × 3.1
    Bid $60,000 × 8.4

L3 数据(Level 3)— 完整订单流
├── 内容:每一笔订单的新增/修改/取消
├── 信息量:最多
├── 频率:最高(每个订单事件)
├── 用途:HFT、做市策略、微观结构研究
├── 带宽:~100KB-1MB/s per symbol
└── 示例:
    10:00:01.001 | ADD    | Buy  2.5 BTC @ $60,050 | OrderID: #12345
    10:00:01.002 | CANCEL | OrderID: #12340
    10:00:01.003 | TRADE  | 1.0 BTC @ $60,055 | Buyer:#12345 Seller:#12341
    10:00:01.004 | MODIFY | OrderID: #12342 | NewQty: 3.0

知识点详解

知识点 1:订单簿深度可视化与分析

订单簿的完整视觉表示:
═══════════════════════════════════════

价格 ($)    │  卖出量   │  累计卖出
─────────────┼──────────┼──────────
60,200      │   0.3    │   15.8
60,150      │   1.5    │   15.5
60,100      │   2.0    │   14.0
60,080      │   3.2    │   12.0     ← 卖方堆积(阻力)
60,055      │   1.8    │    8.8     ← Best Ask
════════════╪══════════╪══════════
60,050      │   2.5    │    2.5     ← Best Bid
60,030      │   3.1    │    5.6
60,000      │   8.4    │   14.0     ← 买方堆积(支撑)
59,950      │   2.0    │   16.0
59,900      │   1.2    │   17.2

Spread = $60,055 - $60,050 = $5 (0.008%)
Mid Price = ($60,055 + $60,050) / 2 = $60,052.5

深度图(Depth Chart):
  累计量
  20│
    │     ╲                  ╱
  15│      ╲    卖方        ╱
    │       ╲             ╱
  10│        ╲          ╱    买方
    │         ╲       ╱
   5│          ╲    ╱
    │           ╲╱
   0│────────────────────────
    59,800   60,000   60,200  价格

订单簿的关键指标:
├── Spread(买卖价差):流动性衡量
├── Depth(深度):大单承受能力
├── Imbalance(不平衡):方向指示
│   Imbalance = (BidQty - AskQty) / (BidQty + AskQty)
│   > 0:买方强 → 价格可能上涨
│   < 0:卖方强 → 价格可能下跌
└── Resilience(弹性):被吃后恢复速度

知识点 2:WebSocket 实时推送架构

市场数据推送架构设计:
═══════════════════════════════════════

  ┌─────────────┐
  │  撮合引擎    │  ← 产生原始市场数据
  └──────┬──────┘
         │ 内部消息总线(如 Kafka/Aeron)
         ↓
  ┌─────────────┐
  │  数据聚合器   │  ← 汇聚、去重、格式化
  │  ├── L1聚合   │
  │  ├── L2聚合   │
  │  ├── K线计算   │
  │  └── 指标计算   │
  └──────┬──────┘
         │
         ↓
  ┌─────────────────────────────────┐
  │       WebSocket Gateway          │  ← 面向用户
  │  ┌──────────────────────────┐   │
  │  │  Channel Manager         │   │
  │  │  ├── orderbook.BTC-USDT │   │
  │  │  ├── trades.BTC-USDT    │   │
  │  │  ├── ticker.BTC-USDT    │   │
  │  │  └── kline.BTC-USDT.1m  │   │
  │  └──────────────────────────┘   │
  │  ┌──────────────────────────┐   │
  │  │  Connection Manager      │   │
  │  │  ├── 10万+ 并发连接      │   │
  │  │  ├── 心跳检测            │   │
  │  │  └── 自动重连            │   │
  │  └──────────────────────────┘   │
  └──────────────┬──────────────────┘
                 │ WebSocket
                 ↓
  ┌──────┐ ┌──────┐ ┌──────┐
  │ 用户1 │ │ 用户2 │ │ 用户N │
  └──────┘ └──────┘ └──────┘

关键设计决策:
  ├── 推送模式:增量更新 vs 全量快照
  │   └── 增量:只推送变化(省带宽)
  │   └── 全量:定期推送完整订单簿(防丢失)
  │   └── 最佳实践:初始全量 + 后续增量
  │
  ├── 频率控制:
  │   └── L1:每次变化推送(高频)
  │   └── L2:100ms聚合推送(平衡)
  │   └── K线:每秒推送
  │
  ├── 压缩:
  │   └── 消息压缩(gzip/deflate)
  │   └── 二进制协议(Protobuf/FlatBuffers)
  │   └── 差量编码(只传变化的字段)
  │
  └── 扩展性:
      └── 水平扩展WebSocket服务器
      └── 按交易对分片
      └── CDN加速(Cloudflare WS)

WebSocket 消息格式示例

// 订阅请求
{
  "method": "SUBSCRIBE",
  "params": ["orderbook.BTC-USDT.L2", "trades.BTC-USDT"],
  "id": 1
}

// 订单簿快照(初始)
{
  "channel": "orderbook.BTC-USDT.L2",
  "type": "snapshot",
  "data": {
    "bids": [[60050, 2.5], [60030, 3.1], [60000, 8.4]],
    "asks": [[60055, 1.8], [60080, 3.2], [60100, 2.0]],
    "sequence": 1000001,
    "timestamp": 1713600000000
  }
}

// 订单簿增量更新
{
  "channel": "orderbook.BTC-USDT.L2",
  "type": "update",
  "data": {
    "bids": [[60050, 3.0]],   // 更新60050档位的量
    "asks": [[60055, 0]],      // 量=0表示删除该档位
    "sequence": 1000002
  }
}

// 成交推送
{
  "channel": "trades.BTC-USDT",
  "data": {
    "price": 60055,
    "qty": 0.5,
    "side": "buy",        // 主动方
    "timestamp": 1713600001234,
    "trade_id": "T123456"
  }
}

知识点 3:K线(OHLCV)计算与存储

K线数据结构:
═══════════════════════════════════════

OHLCV = Open, High, Low, Close, Volume

一根K线代表一个时间周期内的价格运动:

  │          High ($60,200)
  │            │
  │     ┌──────┤
  │     │      │  ← 实体(Open和Close之间)
  │     │ Close│  = $60,150
  │     │      │
  │  ───┼──────┤  Open = $60,050
  │     │      │
  │     └──────┤
  │            │
  │          Low ($59,900)

  Volume = 该周期内总成交量

常见K线周期:
├── 1m, 3m, 5m, 15m, 30m(分钟级)
├── 1h, 2h, 4h, 6h, 12h(小时级)
├── 1d, 3d, 1w(日/周级)
└── 1M(月级)

K线的实时计算

K线实时计算逻辑:
═══════════════════════════════════════

// 伪代码
class KlineBuilder {
  currentCandle: {
    open: number,
    high: number,
    low: number,
    close: number,
    volume: number,
    startTime: number
  }

  function onTrade(price, quantity, timestamp):
    // 检查是否需要开新K线
    if timestamp >= currentCandle.startTime + INTERVAL:
      // 保存当前K线
      saveCandle(currentCandle)
      // 开始新K线
      currentCandle = {
        open: price,
        high: price,
        low: price,
        close: price,
        volume: quantity,
        startTime: getIntervalStart(timestamp)
      }
    else:
      // 更新当前K线
      currentCandle.high = max(currentCandle.high, price)
      currentCandle.low = min(currentCandle.low, price)
      currentCandle.close = price
      currentCandle.volume += quantity
}

多周期K线的优化:
  ├── 从1分钟K线聚合生成更大周期
  │   5m = aggregate(5个1m)
  │   1h = aggregate(60个1m)
  │   1d = aggregate(1440个1m)
  ├── 避免重复计算
  └── 内存中维护所有活跃周期的当前K线

K线存储方案

K线数据存储选型:
═══════════════════════════════════════

方案1:时序数据库(推荐)
  ├── InfluxDB
  │   └── 写入快、压缩好、查询DSL强
  ├── TimescaleDB(PostgreSQL扩展)
  │   └── SQL兼容、成熟稳定
  ├── QuestDB
  │   └── 极致写入性能、SQL兼容
  └── ClickHouse
      └── OLAP强、压缩率极高

方案2:Redis + 持久化
  ├── 最近K线放Redis(热数据)
  ├── 历史K线存时序DB(冷数据)
  └── 适合:超低延迟访问需求

存储量估算:
  假设:1000个交易对 × 9个周期

  1分钟K线:
  ├── 每根:48字节(OHLCV + timestamp)
  ├── 每天:1440根 × 48B = 69KB/交易对
  ├── 1000对/天:69MB
  └── 1年:~25GB

  全部周期:
  └── 1年总量:~30GB(压缩后~5GB)

知识点 4:TradingView 集成

TradingView 集成架构:
═══════════════════════════════════════

TradingView 提供两种集成方式:

方式1:UDF(Universal Data Feed)
  ├── 最简单的HTTP API方式
  ├── TradingView请求你的API获取数据
  ├── 接口规范:
  │   GET /config          → 支持的功能
  │   GET /symbol_info     → 交易对信息
  │   GET /symbols?symbol= → 单个交易对
  │   GET /history?symbol=&from=&to=&resolution=  → K线数据
  │   GET /time            → 服务器时间
  └── 适合:简单集成、数据量不大

方式2:JS API(推荐)
  ├── 通过JavaScript接口提供数据
  ├── 支持实时推送(WebSocket)
  ├── 接口定义:
  │   onReady(callback)
  │   searchSymbols(input, symbolType, exchange, onResult)
  │   resolveSymbol(symbolName, onResolve, onError)
  │   getBars(symbolInfo, resolution, from, to, onResult, onError)
  │   subscribeBars(symbolInfo, resolution, onTick, listenerGUID)
  │   unsubscribeBars(listenerGUID)
  └── 适合:专业交易平台、需要实时数据

集成架构图:
  ┌────────────────────────────────────┐
  │        TradingView Widget          │
  │  ┌──────────────────────────┐     │
  │  │  Charting Library         │     │
  │  │  ├── 图表渲染             │     │
  │  │  ├── 技术指标             │     │
  │  │  ├── 画图工具             │     │
  │  │  └── 交易面板             │     │
  │  └───────────┬──────────────┘     │
  │              │ JS API / UDF       │
  │  ┌───────────┴──────────────┐     │
  │  │  Datafeed Adapter        │     │  ← 你需要实现的
  │  │  ├── HTTP: 历史K线        │     │
  │  │  └── WS: 实时更新         │     │
  │  └──────────────────────────┘     │
  └──────────────┬─────────────────────┘
                 │
           [你的后端API]
           ├── K线历史数据
           ├── 实时价格推送
           └── 交易对配置

知识点 5:DeFi 链上数据的特殊性

DeFi 市场数据 vs CeFi 市场数据:
═══════════════════════════════════════

差异1:数据来源
  CeFi:交易所内部系统直接产生
  DeFi:从区块链事件日志中提取
  ├── Swap事件 → 成交数据
  ├── Sync事件 → 价格更新
  ├── Mint/Burn事件 → 流动性变化
  └── 需要自己索引和解析

差异2:更新频率
  CeFi:毫秒级连续更新
  DeFi:出块时间间隔(2s-12s)
  ├── Ethereum:12秒
  ├── Solana:400ms
  ├── Arbitrum:250ms
  └── K线可能有"空洞"(该周期内无交易)

差异3:订单簿
  CeFi:传统CLOB(中心化限价订单簿)
  DeFi-AMM:无订单簿,流动性曲线即"虚拟订单簿"
  ├── Uniswap v2:x*y=k → 无限深度但滑点递增
  ├── Uniswap v3:集中流动性 → 类似订单簿
  └── 从流动性分布计算"虚拟订单簿深度"

差异4:MEV影响
  CeFi:成交顺序由撮合引擎决定(FIFO)
  DeFi:成交顺序由区块构建者决定
  ├── 三明治攻击改变实际成交价
  ├── 前置运行(Frontrunning)
  └── 影响K线的"真实性"

差异5:多池碎片化
  CeFi:一个交易对一个订单簿
  DeFi:同一交易对可能在多个池子
  ├── ETH/USDC 在 Uniswap v3 0.05%池
  ├── ETH/USDC 在 Uniswap v3 0.3%池
  ├── ETH/USDC 在 Curve
  └── 需要聚合所有池子的数据

DeFi市场数据解析架构:
  ┌─────────────┐
  │ 区块链节点   │
  │ (RPC/WS)    │
  └──────┬──────┘
         ↓
  ┌─────────────┐
  │ 事件索引器   │  ← The Graph / 自建索引
  │ ├── Swap    │
  │ ├── Sync    │
  │ └── events  │
  └──────┬──────┘
         ↓
  ┌─────────────┐
  │ 数据聚合    │  ← 多池聚合、价格计算
  │ ├── 价格    │
  │ ├── 成交量  │
  │ └── 流动性  │
  └──────┬──────┘
         ↓
  ┌─────────────┐
  │ K线/行情API │  ← 标准化输出
  └─────────────┘

面试题解析

如何设计支持百万用户的实时市场数据系统?

简短回答(30秒版本)

分层架构 + 扇出模式。撮合引擎产生原始数据→通过消息总线(Kafka)分发→数据聚合服务计算各种视图(L1/L2/K线)→WebSocket网关层水平扩展支撑百万连接→按交易对分片、按数据类型分频道、增量推送减少带宽。

详细回答(2分钟版本)

百万用户实时市场数据系统设计:
═══════════════════════════════════════

容量估算:
├── 100万用户同时在线
├── 平均每用户订阅5个频道
├── 每个频道每秒10条消息
├── 每条消息~200字节
├── 总推送:100万 × 5 × 10 × 200B = 10GB/s
└── 这个量级需要精心设计

架构分4层:

Layer 1:数据生产(撮合引擎 → 消息总线)
├── Kafka/Aeron 作为中间件
├── 按交易对分区(Partition)
├── 保证同一交易对的消息有序
└── 吞吐:100万消息/秒

Layer 2:数据处理(聚合 + 计算)
├── L1/L2/L3数据聚合
├── K线实时计算(多周期)
├── 指标计算(VWAP/MA等)
└── 可以水平扩展(按交易对分片)

Layer 3:WebSocket Gateway(面向用户)
├── 每台服务器支撑5-10万连接
├── 需要10-20台服务器
├── 负载均衡:按用户ID一致性哈希
├── 订阅管理:高效的频道→连接映射
└── 消息广播:同一频道的消息只处理一次

Layer 4:客户端优化
├── 增量更新(只推变化)
├── 客户端节流(前端限制刷新率)
├── 离屏标签页暂停推送
└── 自动降级(网络差时切换低频模式)

追问准备

  • Q:如何处理WebSocket断连重连?A:客户端记录最后收到的sequence number,重连后请求该sequence之后的增量+一次全量快照。服务端保持短时间的消息回放缓冲。
  • Q:如何保证消息不丢失?A:在关键路径上使用sequence number保序;客户端检测到gap时主动请求重传或快照。对于行情数据,允许少量丢失(最终一致即可)。

产品经理视角

PM在市场数据产品中的关键决策:
═══════════════════════════════════════

1. 数据层级与收费策略
   ├── 免费:L1 + 基础K线
   ├── 专业版:L2深度 + 更多周期
   ├── 机构版:L3订单流 + 原始数据
   └── 这是交易所的重要收入来源

2. 延迟与用户体验
   ├── 散户:100ms延迟可接受
   ├── 专业交易者:<10ms
   ├── HFT:<1ms(需要Co-location)
   └── 不同用户群的需求截然不同

3. 图表功能优先级
   ├── P0:K线、深度图、成交列表
   ├── P1:技术指标(MA/MACD/RSI)
   ├── P2:画图工具、多图表布局
   └── P3:策略回测、自定义指标

4. DeFi特殊考虑
   ├── 多池聚合价格 vs 单池价格
   ├── MEV影响的标注
   ├── 流动性深度展示
   └── Gas成本在交易面板中的集成

明日预告

Day 104:CeFi-DeFi 桥接架构 — 混合交易系统设计 — 性能与去中心化的平衡术

  • 三种混合模式详解
  • 信任假设与活性风险分析
  • 强制包含机制(L1逃生舱)
  • KYC Hooks 合规层设计
  • App-chain vs Rollup 的取舍