3. 行情数据接入:交易所API对接与数据清洗

行情数据接入,说白了就是量化做市系统的「眼睛」。眼睛要是瞎了,再牛逼的策略也白搭。我见过太多团队,策略模型跑得飞起,结果行情延迟高了50毫秒,直接亏到姥姥家。

今天咱们就聊聊,怎么把这双「眼睛」练得又亮又稳。

3.1 交易所API对接:REST vs WebSocket

交易所给咱们提供了两种接口:REST和WebSocket。这俩有啥区别?我直接说人话:

  • REST:你问一句,它答一句。像发微信消息,一问一答。
  • WebSocket:它一直跟你连着,有数据就主动推给你。像打电话,实时通话。

你想想看,做市系统需要实时盯盘,肯定得用WebSocket啊。但REST也不是完全没用,我后面会讲。

3.1.1 REST接口:适合做什么?

REST接口虽然慢,但胜在简单可靠。我个人习惯用它来做三件事:

  • 获取历史数据:比如拉取过去24小时的K线数据,用于策略回测
  • 查询账户信息:余额、持仓这些不常变的数据
  • 下单与撤单:虽然WebSocket也能下单,但REST更稳定,不容易丢单

这里有个坑,我曾经踩过:REST接口有频率限制。大部分交易所限制每秒1-10次请求。你要是写个死循环去轮询,分分钟被封IP。

⚠️ 注意: REST接口的限频策略因交易所而异。建议在代码里实现一个「令牌桶」算法,控制请求速率。别问我怎么知道的,问就是被封过。

3.1.2 WebSocket:实时行情的命脉

WebSocket是做市系统的核心。它建立一条长连接,交易所一旦有新的行情数据,就立刻推给你。延迟通常在1-10毫秒级别。

但WebSocket也有麻烦事:

  • 断线重连:网络波动导致连接断开,你得自动重连
  • 心跳检测:长时间没收到数据,要主动发ping包确认连接是否存活
  • 数据乱序:多个数据包同时到达,顺序可能错乱

我建议的架构是这样的:

// WebSocket连接管理器伪代码
class WebSocketManager {
    constructor(url) {
        this.url = url;
        this.ws = null;
        this.reconnectInterval = 1000; // 初始重连间隔1秒
        this.maxReconnectInterval = 30000; // 最大重连间隔30秒
    }

    connect() {
        this.ws = new WebSocket(this.url);
        this.ws.onopen = () => {
            console.log('连接成功');
            this.reconnectInterval = 1000; // 重置重连间隔
            this.startHeartbeat();
        };
        this.ws.onmessage = (event) => {
            this.handleMessage(event.data);
        };
        this.ws.onclose = () => {
            console.log('连接断开,准备重连');
            this.reconnect();
        };
        this.ws.onerror = (error) => {
            console.error('连接错误', error);
            this.ws.close();
        };
    }

    reconnect() {
        setTimeout(() => {
            console.log(`尝试重连,间隔${this.reconnectInterval}ms`);
            this.connect();
            this.reconnectInterval = Math.min(
                this.reconnectInterval * 2,
                this.maxReconnectInterval
            );
        }, this.reconnectInterval);
    }

    startHeartbeat() {
        setInterval(() => {
            if (this.ws.readyState === WebSocket.OPEN) {
                this.ws.send('ping');
            }
        }, 30000); // 每30秒发一次心跳
    }
}

嗯,这里要注意:重连间隔要指数退避。第一次断开等1秒,第二次等2秒,第三次等4秒...最多等30秒。这样既不会把交易所服务器打爆,也能尽快恢复连接。

3.2 行情数据格式标准化

不同交易所的行情数据格式五花八门。有的叫「bid」,有的叫「buy」,有的叫「bids」。你总不能为每个交易所写一套解析逻辑吧?

所以,我们需要一个统一的数据模型

3.2.1 Level1 vs Level2 行情

先搞清楚这两个概念:

类型 包含内容 更新频率 用途
Level1 最新价、最高价、最低价、成交量、涨跌幅 每秒几次 简单监控、普通交易
Level2 完整买卖盘口(通常前50档)、逐笔成交 毫秒级 做市策略、高频交易

做市系统必须用Level2数据。为什么?因为你要看到盘口的深度和厚度,才能判断流动性够不够。Level1数据太粗糙,根本不够用。

3.2.2 统一数据模型设计

我个人习惯这样设计:

// 统一行情数据模型
class UnifiedMarketData {
    constructor() {
        this.exchange = '';      // 交易所名称
        this.symbol = '';       // 交易对,如 BTC/USDT
        this.timestamp = 0;     // 时间戳(毫秒)
        this.bids = [];         // 买单队列 [[价格, 数量], ...]
        this.asks = [];         // 卖单队列 [[价格, 数量], ...]
        this.lastPrice = 0;     // 最新成交价
        this.lastVolume = 0;    // 最新成交量
        this.volume24h = 0;     // 24小时成交量
    }
}

// 交易所适配器接口
class ExchangeAdapter {
    constructor(exchangeName) {
        this.exchangeName = exchangeName;
    }

    // 将交易所原始数据转换为统一格式
    normalize(rawData) {
        throw new Error('子类必须实现normalize方法');
    }
}

// 币安适配器示例
class BinanceAdapter extends ExchangeAdapter {
    normalize(rawData) {
        const data = new UnifiedMarketData();
        data.exchange = 'binance';
        data.symbol = rawData.s;
        data.timestamp = rawData.E;
        data.bids = rawData.b.map(item => [parseFloat(item[0]), parseFloat(item[1])]);
        data.asks = rawData.a.map(item => [parseFloat(item[0]), parseFloat(item[1])]);
        data.lastPrice = parseFloat(rawData.c);
        data.lastVolume = parseFloat(rawData.v);
        data.volume24h = parseFloat(rawData.V);
        return data;
    }
}

这样设计的好处是:新增一个交易所,只需要写一个适配器。核心策略完全不用改,直接消费统一格式的数据就行。

3.3 数据降噪与清洗

行情数据不是拿来就能用的。交易所推送的数据里,有很多「噪音」。比如:

  • 重复数据:同一个tick推了两次
  • 异常数据:价格突然跳变100倍(交易所bug或网络问题)
  • 过期数据:时间戳比当前时间还早

这些噪音如果不处理,策略会做出错误决策。我曾经遇到过:某交易所的WebSocket偶尔会推送一条历史数据,导致策略以为价格暴跌,直接清仓了...嗯,亏了不少钱。

3.3.1 数据去重

最简单的去重方法:维护一个序列号或时间戳缓存

class DataDeduplicator {
    constructor() {
        this.lastTimestamp = 0;
        this.lastSequence = 0;
    }

    isDuplicate(data) {
        // 如果时间戳和序列号都相同,说明是重复数据
        if (data.timestamp === this.lastTimestamp && 
            data.sequence === this.lastSequence) {
            return true;
        }
        this.lastTimestamp = data.timestamp;
        this.lastSequence = data.sequence;
        return false;
    }
}

3.3.2 异常值过滤

对于价格数据,我建议用滑动窗口标准差来检测异常:

class OutlierFilter {
    constructor(windowSize = 100, threshold = 3) {
        this.windowSize = windowSize;
        this.threshold = threshold;
        this.priceHistory = [];
    }

    isOutlier(price) {
        if (this.priceHistory.length < this.windowSize) {
            this.priceHistory.push(price);
            return false; // 数据不够,先不判断
        }

        // 计算均值和标准差
        const mean = this.priceHistory.reduce((a, b) => a + b, 0) / this.windowSize;
        const variance = this.priceHistory.reduce((sum, p) => sum + Math.pow(p - mean, 2), 0) / this.windowSize;
        const std = Math.sqrt(variance);

        // 如果价格偏离均值超过3个标准差,视为异常
        if (Math.abs(price - mean) > this.threshold * std) {
            return true;
        }

        // 更新滑动窗口
        this.priceHistory.shift();
        this.priceHistory.push(price);
        return false;
    }
}
💡 小技巧: 阈值设为3倍标准差,在正态分布下能覆盖99.7%的正常数据。但加密货币市场波动大,我一般设到5倍标准差,避免误杀。

3.3.3 数据对齐

不同交易所的数据到达时间不一样。比如币安的数据比OKX早到10毫秒。如果你直接比较两个交易所的价格,可能会得出错误的价差。

我的做法是:按时间戳对齐。把数据放进一个时间桶里,比如每100毫秒一个桶,同一个桶里的数据才进行比较。

class TimeAligner {
    constructor(bucketSizeMs = 100) {
        this.bucketSizeMs = bucketSizeMs;
        this.buckets = new Map();
    }

    addData(data) {
        const bucketKey = Math.floor(data.timestamp / this.bucketSizeMs);
        if (!this.buckets.has(bucketKey)) {
            this.buckets.set(bucketKey, []);
        }
        this.buckets.get(bucketKey).push(data);
    }

    getAlignedData() {
        // 返回上一个完整时间桶的数据
        const now = Date.now();
        const currentBucket = Math.floor(now / this.bucketSizeMs);
        const previousBucket = currentBucket - 1;
        
        const data = this.buckets.get(previousBucket);
        this.buckets.delete(previousBucket); // 清理旧数据
        return data || [];
    }
}

3.4 整体架构图

说了这么多,咱们画张图总结一下行情数据接入的整体流程:

行情数据接入架构图 交易所层 币安 WebSocket OKX WebSocket 火币 WebSocket ... 适配器层(数据标准化) BinanceAdapter OKXAdapter HuobiAdapter 数据清洗层 去重 异常过滤 时间对齐 统一行情数据(UnifiedMarketData)

这张图展示了从交易所原始数据到统一行情数据的完整链路。每一层都有明确的职责,出了问题也好排查。

📌 核心要点:
  • WebSocket负责实时数据,REST负责辅助功能
  • 适配器模式解决数据格式标准化问题
  • 数据清洗是策略稳定运行的保障,不能省

行情数据接入这块,说白了就是「接进来、洗干净、统一化」。做好了,你的做市系统就成功了一半。做不好,后面策略写得再漂亮也是白搭。

我个人建议,在开发初期就把数据接入模块做得足够健壮。别急着跑策略,先把数据质量搞上去。你想想看,如果连数据都是错的,策略怎么可能赚钱?


公众号:蓝海资料掘金营,微信deep3321