AllTick
加密货币量化策略的做市策略:如何根据订单簿不平衡度动态调整买卖报价?
量化交易加密货币交易做市交易订单簿失衡Python 量化交易

加密货币量化策略的做市策略:如何根据订单簿不平衡度动态调整买卖报价?

了解量化交易者如何利用 Python 和订单簿失衡(OBI)动态调整加密货币做市报价、管理波动率与库存,并降低逆向选择风险。

AllTick阅读 2 分钟

对于量化交易者而言,做市是一项需要根据市场环境不断调整的定价工作。报价过宽,可能错失有利的成交机会;报价过于激进,则可能面临逆向选择风险,即订单刚刚成交,市场价格就朝着不利于自身持仓的方向移动。

在加密货币市场中,交易全天候进行,订单簿流动性也可能快速变化。订单簿失衡(Order Book Imbalance,OBI) 可以衡量买卖双方可见流动性的分布情况,并为系统化调整报价提供依据。

本文将介绍如何计算 OBI、如何将其转化为动态报价,以及如何使用 Python 处理实时加密货币订单簿数据。

为什么订单簿失衡对加密货币做市很重要

传统做市策略通常围绕参考价格设置买入报价和卖出报价。然而,即使中间价相同,买卖双方的挂单流动性不同,也可能意味着截然不同的交易环境。

例如,买单一侧的挂单量相对较大,可能表明市场上存在更多可见的买入意愿。相反,如果卖单一侧的挂单量更大,则可能意味着卖方流动性更充足。

OBI 通过衡量选定订单簿档位中买卖双方的成交量分布差异,量化这种失衡状态。它无法确定性地预测价格走势,但可以为量化交易者调整报价提供额外的参考信号。

如何计算订单簿失衡

一种常见的多档位 OBI 计算公式如下:

text
OBI = (V_bid - V_ask) / (V_bid + V_ask)

其中:

  • (V_{bid}) 表示所选档位内所有买单的总挂单量。
  • (V_{ask}) 表示相同档位范围内所有卖单的总挂单量。
    当买卖双方的总挂单量大于零时,OBI 的取值范围为 -1 到 +1。
OBI 数值含义潜在的报价调整方式
大于零买单侧可见挂单量更大将报价中心向上调整
小于零卖单侧可见挂单量更大将报价中心向下调整
接近零买卖双方挂单量相对均衡让报价中心保持在接近中间价的位置

例如,如果选定的买单档位包含 800 个单位的挂单量,而卖单档位包含 400 个单位,则 OBI 约为 0.33。

这说明可见流动性存在不平衡,但并不意味着买方的实际交易压力已经得到确认。限价订单可能随时被撤销或补充,因此,OBI 应被视为多个信号之一,而不是独立使用的交易规则。

如何利用 OBI 实现动态报价

静态做市模型通常围绕市场中间价设置报价:

text
Mid = (BestBid + BestAsk) / 2

其中,BestBid 是当前最高买价,BestAsk 是当前最低卖价。

为了将 OBI 纳入定价过程,可以对报价中心进行调整:

text
Center = Mid + κ × OBI

随后,根据报价中心计算买入和卖出报价:

text
Bid = Center - h
text
Ask = Center + h

其中,\(h\) 表示目标半价差,\(\kappa\) 则用于控制 OBI 对报价中心的影响程度。由于 OBI 本身是无量纲数值,因此,\(\kappa\) 必须使用价格单位表示。

当 OBI 为正时,买入和卖出报价都会向上移动;当 OBI 为负时,两侧报价都会向下移动。这种方式能够调整报价所依据的参考价格,而不一定改变买卖价差本身。

在实际运行的做市策略中,半价差和调整系数还应考虑交易成本、库存风险、市场波动率、最小价格变动单位(Tick Size)以及执行风险。相关参数需要通过测试进行校准,不能假设它们在所有市场环境下都有效。

使用 Python 处理实时订单簿数据

要计算多档位 OBI,策略需要获取买卖双方的挂单价格和挂单量,而不仅仅是最新成交价。

AllTick 提供用于获取实时订单簿更新的 WebSocket 接口。其深度订阅文档介绍了订阅协议,以及下方代码使用的 bids 和 asks 字段。

以下示例订阅 BTCUSDT 的五档订单簿数据,计算 OBI,并输出用于演示的报价调整结果。该示例不会提交真实交易订单。

text
import json
import os
import time
import uuid
import threading
from urllib.parse import quote

import websocket

API_KEY = os.environ.get("ALLTICK_API_KEY")
if not API_KEY:
    raise RuntimeError("Set the ALLTICK_API_KEY environment variable")

WS_URL = (
    "wss://quote.alltick.co/quote-b-ws-api?token="
    + quote(API_KEY, safe="")
)

QUOTE_SHIFT_FACTOR = 0.25


def send_heartbeat(ws):
    while ws.sock and ws.sock.connected:
        time.sleep(10)

        if not ws.sock or not ws.sock.connected:
            break

        try:
            ws.send(json.dumps({
                "cmd_id": 22000,
                "seq_id": int(time.time()),
                "trace": str(uuid.uuid4()),
                "data": {}
            }))
        except Exception:
            break


def on_open(ws):
    ws.send(json.dumps({
        "cmd_id": 22002,
        "seq_id": int(time.time()),
        "trace": str(uuid.uuid4()),
        "data": {
            "symbol_list": [
                {"code": "BTCUSDT", "depth_level": 5}
            ]
        }
    }))

    threading.Thread(
        target=send_heartbeat, args=(ws,), daemon=True
    ).start()


def on_message(ws, message):
    try:
        packet = json.loads(message)
    except json.JSONDecodeError:
        return

    if packet.get("cmd_id") != 22999:
        return

    book = packet.get("data") or {}
    bids = book.get("bids") or []
    asks = book.get("asks") or []

    if not bids or not asks:
        return

    try:
        bid_vol = sum(float(x.get("volume") or 0) for x in bids)
        ask_vol = sum(float(x.get("volume") or 0) for x in asks)
        best_bid = float(bids[0]["price"])
        best_ask = float(asks[0]["price"])
    except (TypeError, ValueError, KeyError):
        return

    total_vol = bid_vol + ask_vol
    if total_vol <= 0 or best_ask <= best_bid:
        return

    obi = (bid_vol - ask_vol) / total_vol
    mid = (best_bid + best_ask) / 2
    spread = best_ask - best_bid

    shift = QUOTE_SHIFT_FACTOR * spread * obi
    model_bid = mid - spread / 2 + shift
    model_ask = mid + spread / 2 + shift

    print(
        f"{book.get('code')} OBI={obi:.3f} "
        f"bid={model_bid:.2f} ask={model_ask:.2f}"
    )


def start():
    while True:
        ws = websocket.WebSocketApp(
            WS_URL,
            on_open=on_open,
            on_message=on_message,
            on_error=lambda ws, err: print(f"WebSocket error: {err}"),
        )
        ws.run_forever()
        time.sleep(3)


if __name__ == "__main__":
    start()

这段代码利用当前最优买卖价之间的价差来演示报价中心的移动。其中,报价调整系数仅用于示例,并非经过验证的交易参数。

生产环境中的系统还需要进一步处理订单提交、重连状态、数据过期、价格精度、库存限制以及特定交易场所的执行规则。

如何结合波动率和库存管理控制逆向选择风险

单独使用 OBI 可能产生大量噪声信号。较大的挂单可能在成交前消失,而突发的市场波动也可能使原本合理的报价迅速失效。

更稳健的加密货币做市策略可以结合以下三种控制机制:

  • 波动率过滤: 当短期波动率上升时,扩大报价价差,或降低报价对 OBI 的敏感程度。
  • 库存管理: 当累积持仓形成过大的方向性风险时,调整报价中心,以控制持仓敞口。
  • 信号持续性判断: 检查订单簿失衡是否在多个连续更新中持续存在,而不是对每次快照变化都立即作出反应。

这三种机制分别应对不同的风险。波动率过滤用于响应市场环境变化,库存管理用于限制持仓集中度,而信号持续性判断则有助于降低策略对短暂订单簿变化的敏感度。

部署前如何测试策略

OBI 为正,并不意味着提高报价就一定能够改善盈利能力。量化交易者需要验证这一信号是否有助于预测后续价格变化,以及是否能够改善实际成交结果。

建议记录以下数据:

  • 订单簿时间戳。
  • 选定档位的买卖价格与挂单量。
  • 计算得到的 OBI 数值。
  • 策略生成的买入和卖出报价。
  • 后续中间价的变化情况。

评估时,应重点关注订单成交率、成交后的不利价格变动、交易成本、库存敞口,以及策略在不同波动率环境下的净表现。

还需要注意一个重要的数据限制:根据 AllTick 的文档说明,其 WebSocket 深度订阅接口提供实时订单簿更新,而非历史订单簿数据。如果量化交易者需要开展可重复的历史 OBI 回测,就需要自行归档深度数据流,并保留时间戳及数据质量检查记录。

如需接入实时数据,可以查看 AllTick 加密货币 API,并确认当前账户是否支持所需的交易品种和订单簿深度。

实用的做市策略不会将订单簿失衡视为确定性的价格预测工具,而是利用 OBI 调整报价中心,同时结合波动率和库存风险控制,并通过实际成交数据验证这种调整是否真正创造了价值。

立即开始接入市场数据

几秒内生成免费 API 密钥,通过一个端点连接所有市场。