← 기사 목록으로
May 26, 2026
5분 소요

DeepLOB: 지정가 주문장에 적용한 딥러닝

DeepLOB: 지정가 주문장에 적용한 딥러닝
#deep-learning
#order-book
#LOB
#microstructure
#HFT
#market-making
📖
Part 5 of 6 · Collection
Order Book & Market Microstructure

지정가 주문장(limit order book)은 현대 전자 시장의 핵심 데이터 구조다. 모든 매수 호가, 모든 매도 호가, 모든 취소 — 이 모든 것이 LOB 안에 담겨 있다. 수십 년 동안 퀀트 연구자들은 이 데이터로부터 특징을 직접 손으로 설계해 왔다. 주문 불균형 비율, 가중 중간 가격, 큐 위치 신호 같은 것들이다. DeepLOB는 다른 길을 택했다. 하이브리드 CNN-LSTM 아키텍처를 사용해 원시 주문장 스냅숏으로부터 이러한 특징을 직접 학습하는 것이다.

2019년 옥스퍼드-맨 연구소(Oxford-Man Institute)의 Zhang, Zohren, Roberts가 발표한 DeepLOB는, 원시 주문장 데이터로 종단 간(end-to-end) 학습한 단일 심층 네트워크가 중간 가격 움직임 예측에서 강력한 고전적 기준 모델과 경쟁하거나 그보다 우수하다는 것을 보였다. 다만 근거에 대해서는 정확하게 짚고 넘어가야 한다. 대부분의 논문이 인용하는 벤치마크는 FI-2010 데이터셋이지만, 저자들 스스로가 이를 불충분하다고 평하며 "너무 짧고, 다운샘플링되었으며, 유동성이 낮은 시장에서 가져온 것"이라고 말한다. 그리고 그들은 더 강력한 결과로서 런던 증권거래소(LSE) 데이터에 대한 훨씬 더 큰 두 번째 평가를 제시한다. 이 LSE 연구는 학습용 다섯 종목과 시험용 다섯 종목을 대상으로 대략 1년에 걸쳐 1억 개 이상 수준의 표본을 다루며, 전이 학습(transfer learning)을 입증한다. 즉, 한 종목 집합으로 학습한 모델이 본 적 없는 종목에 대해서도 여전히 예측을 해낸다는 것이다. 이 논문의 진짜 핵심은 FI-2010 리더보드가 아니라 바로 이 전이 결과다. 이 글에서는 아키텍처, 수학, 실제 수치, 그리고 동작하는 PyTorch 재구현을 해부한다.

데이터 구조로서의 지정가 주문장

지정가 주문장의 등각 투영 시각화: 중간 가격을 중심으로 매수·매도 거래량 레벨이 쌓여 깊이 텐서를 이루는 모습

지정가 주문장은 두 개의 정렬된 목록 — 매수 호가(buy orders)와 매도 호가(sell orders) — 를 각각 가격 레벨별로 정리해 유지한다. 각 레벨에서 주문장은 가격과 대기 중인 주문의 총 거래량을 기록한다.

레벨이 LL개인 주문장에서, 시점 tt의 단일 스냅숏은 4L4L개의 값으로 이루어진 벡터다.

xt=[p1a,v1a,p1b,v1b,p2a,v2a,p2b,v2b,,pLa,vLa,pLb,vLb]\mathbf{x}_t = [p_1^{a}, v_1^{a}, p_1^{b}, v_1^{b}, p_2^{a}, v_2^{a}, p_2^{b}, v_2^{b}, \ldots, p_L^{a}, v_L^{a}, p_L^{b}, v_L^{b}]

여기서 piap_i^{a}viav_i^{a}는 레벨 ii의 매도 가격과 거래량이고, pibp_i^{b}, vibv_i^{b}는 그에 대응하는 매수 측 값이다. 레벨 1은 최우선 매수 호가와 최우선 매도 호가(BBO)를 담으며, 둘의 차이가 매수-매도 스프레드다.

FI-2010 벤치마크 데이터셋에서는 L=10L = 10이므로 스냅숏당 40개의 특징이 된다. FI-2010은 2010년 6월 1일부터 14일까지 연속된 10거래일 동안 NASDAQ Nordic에 상장된 핀란드 주식 다섯 종목(Kesko, Outokumpu, Sampo, Rautaruukki, Wartsila)에서 나온 약 400만 건의 원시 지정가 주문 메시지로 구축된다. 결정적으로, DeepLOB는 400만 개의 스냅숏으로 학습하지 않는다. 벤치마크는 이 메시지들을 다운샘플링(10개 이벤트마다)하여 394,337개 표본의 정규화된 표현으로 만든다. 모델이 실제로 보는 것은 이 다운샘플링되고 정규화된 형태이며, 이것이 저자들이 FI-2010을 최종 결론으로 취급하지 말라고 경고하는 이유 중 하나다.

중간 가격과 그 움직임

시점 tt의 중간 가격은 최우선 매수 호가와 매도 호가의 평균이다.

ptmid=p1a(t)+p1b(t)2p_t^{mid} = \frac{p_1^{a}(t) + p_1^{b}(t)}{2}

DeepLOB는 미래 kk개 이벤트 구간에 걸친 중간 가격 움직임의 방향을 예측한다. 레이블은 평활화된 수익률이지만, 문헌에는 두 가지 평활화 관례가 있으며 둘은 서로 호환되지 않는다는 점을 알아 두어야 한다.

  • 미래 평균 대 현재 가격(Tsantekidis et al.): 다음 kk개 이벤트에 걸친 평균 중간 가격을 현재의 원시 중간 가격과 비교한다.
  • 미래 평균 대 과거 평균(Ntakaris et al., 표준적인 FI-2010 레이블이자 DeepLOB가 LSE 데이터에 사용한 것): 기준값과 목표값 모두 평활화된다.
lt(k)=m+(t)m(t)m(t),m(t)=1ki=0k1ptimid,m+(t)=1ki=1kpt+imidl_t(k) = \frac{m_+(t) - m_-(t)}{m_-(t)}, \qquad m_-(t) = \frac{1}{k}\sum_{i=0}^{k-1} p_{t-i}^{mid}, \quad m_+(t) = \frac{1}{k}\sum_{i=1}^{k} p_{t+i}^{mid}

기준값을 평활화하는 것(미래만이 아니라)은 단일 틱 변동으로 인한 레이블 잡음을 실질적으로 줄여 주며, 이것이 과거/미래 평균 형태가 선호되는 이유다. 어느 쪽을 쓰든, 연속적인 수익률은 고정된 임계값 ±α\pm\alpha를 사용해 세 가지 클래스 — 상승(up), 하락(down), 정체(stationary) — 로 이산화된다.

yt={1if lt(k)>α0if lt(k)α1if lt(k)<αy_t = \begin{cases} 1 & \text{if } l_t(k) > \alpha \\ 0 & \text{if } |l_t(k)| \leq \alpha \\ -1 & \text{if } l_t(k) < -\alpha \end{cases}

FI-2010 데이터셋은 다섯 가지 예측 구간 k{10,20,30,50,100}k \in \{10, 20, 30, 50, 100\} 이벤트에 대한 레이블을 제공한다.

전통적인 LOB 특징

딥러닝 이전에 연구자들은 손으로 설계한 특징에 의존했다. 이들을 이해하는 것은 필수적인데, DeepLOB가 그중 많은 것을 의도적으로 닮도록 구성되었기 때문이다 — 그 아키텍처는 정확히 똑같이 재현하지는 않더라도 이러한 양들에 의해 동기를 부여받았다.

주문장 불균형 (OBI)

가장 널리 쓰이는 미시구조 신호다. 주문장 최상단에서는 다음과 같다.

OBI1=v1bv1av1b+v1a\text{OBI}_1 = \frac{v_1^{b} - v_1^{a}}{v_1^{b} + v_1^{a}}

매도 거래량 대비 매수 거래량이 많을수록 상승 압력을 시사한다. 다중 레벨 불균형은 LL개 레벨에 걸쳐 집계한다.

OBIL=i=1Lvibi=1Lviai=1Lvib+i=1Lvia\text{OBI}_L = \frac{\sum_{i=1}^{L} v_i^{b} - \sum_{i=1}^{L} v_i^{a}}{\sum_{i=1}^{L} v_i^{b} + \sum_{i=1}^{L} v_i^{a}}

거래량 가중 중간 가격

표준 중간 가격은 양쪽을 동등하게 가중한다. 거래량 가중 중간 가격(가중 중간 가격, 또는 VAMP라고도 한다)은 주문장에서 더 무거운 쪽으로 기운다. 최상단 불균형을 I=v1b/(v1b+v1a)I = v_1^{b}/(v_1^{b}+v_1^{a})로 쓰면, 이는 두 가격의 불균형 가중 평균이다.

pwmid=Ip1a+(1I)p1b=v1bp1a+v1ap1bv1b+v1ap^{wmid} = I\, p_1^{a} + (1 - I)\, p_1^{b} = \frac{v_1^{b}\, p_1^{a} + v_1^{a}\, p_1^{b}}{v_1^{b} + v_1^{a}}

교차 가중에 주목하라. 매도 가격은 매수 거래량으로 가중되고, 그 반대도 마찬가지다. 매수 거래량이 우세할 때(I1I \to 1), 가중 중간 가격은 매도 측으로 이동한다 — 불균형한 주문장이 더 무거운 쪽으로 움직이며 해소되리라는 기대를 반영하는 것이다.

명명에 관해 한 가지 주의가 필요하다. 이 정적인 거래량 가중 중간 가격은 Stoikov의 마이크로프라이스(microprice)가 아니다. Stoikov의 마이크로프라이스(2018)는 스프레드와 불균형이 어떻게 진화하는지에 대한 재귀적 보정으로부터 얻어지는 마팅게일 보정 추정량 Mt+g(I,S)M_t + g(I, S)다 — 그의 요점은 순진한 가중 중간 가격이 편향되어 있으며 마팅게일이 아니라는 것이다. 둘은 서로 다른 두 개의 대상이다. 여기서 우리는 가중 중간 가격을 마이크로프라이스의 대용물로서가 아니라 그 단순성 때문에 사용한다.

깊이 불균형

BBO 너머의 주문장 형태를 측정한다.

DIi=vibviavib+via,i=1,,L\text{DI}_i = \frac{v_i^{b} - v_i^{a}}{v_i^{b} + v_i^{a}}, \quad i = 1, \ldots, L

스프레드와 상대 스프레드

st=p1a(t)p1b(t),strel=stptmids_t = p_1^{a}(t) - p_1^{b}(t), \qquad s_t^{rel} = \frac{s_t}{p_t^{mid}}

이러한 특징들은 강력하지만 한계가 있다. 설계하려면 도메인 전문 지식이 필요하고, 복잡한 비선형 상호작용을 포착하지 못하며, 연속된 스냅숏 사이의 시간적 동역학을 무시한다. DeepLOB는 이 세 가지 한계를 모두 해결한다.

DeepLOB 아키텍처

DeepLOB 아키텍처: 원시 주문장 텐서가 합성곱 계층, 인셉션 모듈, LSTM을 거쳐 흐르는 모습

이 아키텍처는 각각 뚜렷한 역할을 가진 세 개의 블록으로 이루어진다.

  1. 합성곱 블록(convolutional block) — 주문장 스냅숏에서 공간적 특징을 추출한다
  2. 인셉션 모듈(inception module) — 다중 스케일의 시간적 패턴을 포착한다
  3. LSTM 블록 — 시간에 걸친 순차적 의존성을 모델링한다

네트워크의 입력은 형태가 (T,L×4)(T, L \times 4)인 텐서다. 여기서 TT는 룩백 윈도(논문에서는 100개 타임스텝)이고, 양쪽의 10개 가격·거래량 레벨에 대해 L×4=40L \times 4 = 40이다.

블록 1: 합성곱 특징 추출

첫 합성곱 계층들은 특징 차원을 따라 작동하여 각 레벨의 가격과 거래량 사이의 상호작용을 학습한다.

계층 1: 각 가격-거래량 쌍에 적용되는 필터 크기 1×21 \times 2, 스트라이드 (1,2)(1, 2)의 합성곱. 이는 각 (pi,vi)(p_i, v_i) 쌍을 하나의 특징으로 요약한다 — 각 레벨에서 "가격-거래량 상호작용"을 계산하는 것과 유사하다. 너비는 40에서 20으로 줄어든다.

계층 2: 같은 레벨에서 짝지어진 매수-매도 쌍에 걸쳐 작동하는 1×21 \times 2, 스트라이드 (1,2)(1, 2) 합성곱. 이는 레벨별 스프레드와 불균형 정보를 포착한다. 너비는 20에서 10으로 줄어든다.

계층 3: 10개 레벨 전체에 걸쳐 통합하여 깊이 차원을 너비 1로 접는 1×101 \times 10 합성곱. 이는 전체 깊이 프로파일을 부호화하는, 타임스텝당 하나의 집계된 특징을 만들어 낸다.

1×21 \times 2 / 1×101 \times 10 단계 뒤에는 시간 축을 따라 두 개의 (4,1)(4, 1) 합성곱이 이어지며, 각각 LeakyReLU 활성화와 배치 정규화를 거친다. 주목할 점은, padding=(1, 0)을 적용한 이 (4,1)(4, 1) 합성곱은 시간 차원을 보존하지 않는다는 것이다. 각 합성곱은 시간 차원을 한 스텝씩 줄인다(Tout=Tin1T_{\text{out}} = T_{\text{in}} - 1). 블록 1-3에 걸친 여섯 개의 그러한 계층을 지나며, 시간 축은 인셉션 모듈 이전에 100에서 94로 줄어든다 — 아래 코드의 형태 주석이 이를 명시한다.

이 설계는 큰 공간 필터를 의도적으로 피한다. 1×21 \times 2 필터는 LOB 데이터의 자연스러운 짝짓기 구조 — 가격과 거래량, 매수와 매도 — 를 존중한다. 이러한 귀납적 편향(inductive bias)이 핵심이다. 3×33 \times 3 필터는 인접한 레벨과 측면을 금융적으로 아무 의미가 없는 방식으로 섞어 버릴 것이다.

블록 2: 인셉션 모듈

합성곱 블록이 40개의 특징을 압축된 공간 표현(너비 1)으로 줄인 뒤, 인셉션 모듈은 시간 차원을 따라 작동하여 여러 시간 스케일의 패턴을 동시에 포착한다.

DeepLOB 인셉션 모듈은 각각 32개 채널을 만들어 내는 네 개의 병렬 경로를 가지며, 모두 연결(concatenate)된다.

  • 1×11 \times 1 병목 \to 3×13 \times 1 시간 합성곱: 단기 시간 패턴(3 타임스텝)
  • 1×11 \times 1 병목 \to 5×15 \times 1 시간 합성곱: 중기 시간 패턴(5 타임스텝)
  • 3×13 \times 1 최대 풀링 \to 1×11 \times 1 합성곱: 평활화·다운샘플링된 관점을 유지하는 풀링 경로
  • (1×11 \times 1 축소는 시간 합성곱 이전의 시점별(point-in-time) 병목 역할을 한다)

각 시간 경로는 대칭 패딩을 사용하므로 인셉션 블록 내부에서는 시간 차원이 유지된다(위의 합성곱 블록과 달리). 출력은 채널 차원을 따라 연결되어, 네트워크가 다중 스케일의 시간 특징에 한 번에 접근할 수 있게 한다.

이는 GoogLeNet 인셉션 아키텍처에서 영감을 받았지만, 2D 이미지가 아니라 1D 시간 데이터에 맞게 변형한 것이다. 핵심 통찰은 LOB 동역학이 여러 시간 스케일에서 작동한다는 것이다. 틱 단위 잡음, 단기 모멘텀, 더 긴 구간의 평균 회귀가 모두 공존한다.

아래의 PyTorch 재구현은 코드를 읽기 쉽게 유지하기 위해 단순화된 세 경로 변형(두 개의 시간 합성곱과 1×11\times1 경로)을 제시하며, 최대 풀링 경로는 생략한다. 이는 원래 모듈이 아니라 논문에서 벗어난 부분이므로 명시적으로 짚어 둔다.

블록 3: LSTM 시퀀스 모델링

인셉션 모듈의 출력은 전체 TT 길이 시퀀스를 처리하는 LSTM 계층으로 들어간다. LSTM은 합성곱만으로는 놓칠 장기 시간 의존성 — 체제 변화, 시간대 효과, 변화하는 시장 상황 — 을 포착한다.

LSTM의 최종 은닉 상태는 소프트맥스 활성화를 가진 완전 연결 계층을 거쳐 세 가지 클래스(상승, 하락, 정체)에 대한 확률 분포를 만들어 낸다.

손실 함수

모델은 범주형 교차 엔트로피(categorical cross-entropy)로 학습된다.

L=1Ni=1Nc=13yi,clog(y^i,c)\mathcal{L} = -\frac{1}{N} \sum_{i=1}^{N} \sum_{c=1}^{3} y_{i,c} \log(\hat{y}_{i,c})

여기서 yi,cy_{i,c}는 원-핫 인코딩된 참 레이블이고 y^i,c\hat{y}_{i,c}는 클래스 cc에 대한 예측 확률이다.

PyTorch 재구현

원래 DeepLOB는 TensorFlow 백엔드 위에서 Keras로 구축되었으며, 저자들의 저장소(zcakhaa)는 이후 PyTorch 포팅을 추가했다. 아래 코드는 공식 구현이 아니라 우리 자신의 설명용 PyTorch 재구현이다. 이는 두 가지 방식으로 논문에서 벗어나며, 코드 내에 표시해 두었다. 인셉션의 최대 풀링 경로를 생략하고(네 개 대신 세 개의 경로), 채널 수를 키웠다. 논문은 합성곱 블록 전체에서 16개 필터인셉션 경로당 32개 필터(LSTM 입력 3×32=963 \times 32 = 96)를 사용하여, 약 6만 개 파라미터의 작은 모델을 만든다. 아래 코드 조각은 32개 합성곱 / 64개 인셉션 필터(LSTM 입력 192)를 사용한다 — 내부적으로 일관되고 실행 가능하지만 원본보다 무겁다. 논문과 일치시키려면 합성곱 채널을 16으로, 인셉션 채널을 32로, 그리고 input_size=96으로 설정하라.

import torch
import torch.nn as nn


class DeepLOB(nn.Module):
    """
    DeepLOB-style CNN-Inception-LSTM for limit order books.
    Reimplementation inspired by Zhang, Zohren, Roberts (2019),
    IEEE Transactions on Signal Processing, arXiv:1808.03668.

    NOTE: this is an illustrative reimplementation, NOT the official model.
    Differences from the paper:
      - inception has 3 paths here vs 4 in the paper (max-pool path omitted);
      - channel counts are scaled up (paper: 16 conv / 32 inception, input 96).

    Input shape: (batch_size, 1, T, 40)
        T = lookback window (e.g. 100)
        40 = 10 levels x 4 features (ask_price, ask_vol, bid_price, bid_vol)
    """

    def __init__(self, num_classes: int = 3, T: int = 100):
        super().__init__()
        self.num_classes = num_classes

        self.conv1 = nn.Sequential(
            nn.Conv2d(1, 32, kernel_size=(1, 2), stride=(1, 2)),
            nn.LeakyReLU(negative_slope=0.01),
            nn.BatchNorm2d(32),
            nn.Conv2d(32, 32, kernel_size=(4, 1), padding=(1, 0)),  # time -1
            nn.LeakyReLU(negative_slope=0.01),
            nn.BatchNorm2d(32),
            nn.Conv2d(32, 32, kernel_size=(4, 1), padding=(1, 0)),  # time -1
            nn.LeakyReLU(negative_slope=0.01),
            nn.BatchNorm2d(32),
        )

        self.conv2 = nn.Sequential(
            nn.Conv2d(32, 32, kernel_size=(1, 2), stride=(1, 2)),
            nn.LeakyReLU(negative_slope=0.01),
            nn.BatchNorm2d(32),
            nn.Conv2d(32, 32, kernel_size=(4, 1), padding=(1, 0)),  # time -1
            nn.LeakyReLU(negative_slope=0.01),
            nn.BatchNorm2d(32),
            nn.Conv2d(32, 32, kernel_size=(4, 1), padding=(1, 0)),  # time -1
            nn.LeakyReLU(negative_slope=0.01),
            nn.BatchNorm2d(32),
        )

        self.conv3 = nn.Sequential(
            nn.Conv2d(32, 32, kernel_size=(1, 10)),
            nn.LeakyReLU(negative_slope=0.01),
            nn.BatchNorm2d(32),
            nn.Conv2d(32, 32, kernel_size=(4, 1), padding=(1, 0)),  # time -1
            nn.LeakyReLU(negative_slope=0.01),
            nn.BatchNorm2d(32),
            nn.Conv2d(32, 32, kernel_size=(4, 1), padding=(1, 0)),  # time -1
            nn.LeakyReLU(negative_slope=0.01),
            nn.BatchNorm2d(32),
        )

        self.inp1 = nn.Sequential(
            nn.Conv2d(32, 64, kernel_size=(1, 1), padding=(0, 0)),
            nn.LeakyReLU(negative_slope=0.01),
            nn.BatchNorm2d(64),
        )
        self.inp2 = nn.Sequential(
            nn.Conv2d(32, 64, kernel_size=(1, 1), padding=(0, 0)),
            nn.LeakyReLU(negative_slope=0.01),
            nn.BatchNorm2d(64),
            nn.Conv2d(64, 64, kernel_size=(3, 1), padding=(1, 0)),
            nn.LeakyReLU(negative_slope=0.01),
            nn.BatchNorm2d(64),
        )
        self.inp3 = nn.Sequential(
            nn.Conv2d(32, 64, kernel_size=(1, 1), padding=(0, 0)),
            nn.LeakyReLU(negative_slope=0.01),
            nn.BatchNorm2d(64),
            nn.Conv2d(64, 64, kernel_size=(5, 1), padding=(2, 0)),
            nn.LeakyReLU(negative_slope=0.01),
            nn.BatchNorm2d(64),
        )

        self.lstm = nn.LSTM(
            input_size=192,  # 64 * 3 paths from the simplified inception
            hidden_size=64,
            num_layers=1,
            batch_first=True,
        )

        self.fc = nn.Linear(64, num_classes)

    def forward(self, x: torch.Tensor) -> torch.Tensor:
        """
        Args:
            x: Tensor of shape (batch, 1, T, 40)
        Returns:
            Tensor of shape (batch, num_classes)
        """
        h = self.conv1(x)   # (batch, 32, T-2, 20)
        h = self.conv2(h)   # (batch, 32, T-4, 10)
        h = self.conv3(h)   # (batch, 32, T-6, 1)   e.g. T=100 -> 94

        h_inp1 = self.inp1(h)  # (batch, 64, T-6, 1)
        h_inp2 = self.inp2(h)  # (batch, 64, T-6, 1)
        h_inp3 = self.inp3(h)  # (batch, 64, T-6, 1)
        h = torch.cat([h_inp1, h_inp2, h_inp3], dim=1)  # (batch, 192, T-6, 1)

        h = h.squeeze(-1)          # (batch, 192, T-6)
        h = h.permute(0, 2, 1)     # (batch, T-6, 192)

        h, _ = self.lstm(h)
        h = h[:, -1, :]  # take last hidden state

        out = self.fc(h)
        return out

데이터 전처리

FI-2010 데이터셋은 z-점수 정규화된 상태로 제공된다. 실시간 주문장 데이터의 경우, 비슷하게 정규화해야 한다.

import numpy as np
from torch.utils.data import Dataset


class LOBDataset(Dataset):
    """Dataset for limit order book snapshots."""

    def __init__(
        self,
        data: np.ndarray,
        labels: np.ndarray,
        T: int = 100,
    ):
        """
        Args:
            data: shape (num_snapshots, 40), raw LOB features
            labels: shape (num_snapshots,), class labels {0, 1, 2}
            T: lookback window length
        """
        self.data = data
        self.labels = labels
        self.T = T

        self.mean = data[:len(data) // 2].mean(axis=0)
        self.std = data[:len(data) // 2].std(axis=0)
        self.std[self.std == 0] = 1.0
        self.data = (self.data - self.mean) / self.std

    def __len__(self) -> int:
        return len(self.data) - self.T

    def __getitem__(self, idx: int):
        x = self.data[idx : idx + self.T].reshape(1, self.T, 40)
        y = self.labels[idx + self.T - 1]
        return torch.tensor(x, dtype=torch.float32), torch.tensor(
            y, dtype=torch.long
        )

학습 루프

def train_deeplob(
    model: DeepLOB,
    train_loader,
    val_loader,
    epochs: int = 50,
    lr: float = 0.01,
    device: str = "cuda",
):
    model = model.to(device)
    criterion = nn.CrossEntropyLoss()
    optimizer = torch.optim.Adam(model.parameters(), lr=lr, eps=1)

    best_val_f1 = 0.0
    for epoch in range(epochs):
        model.train()
        train_loss = 0.0
        for x_batch, y_batch in train_loader:
            x_batch = x_batch.to(device)
            y_batch = y_batch.to(device)

            optimizer.zero_grad()
            logits = model(x_batch)
            loss = criterion(logits, y_batch)
            loss.backward()
            optimizer.step()
            train_loss += loss.item()

        model.eval()
        all_preds, all_labels = [], []
        with torch.no_grad():
            for x_val, y_val in val_loader:
                x_val = x_val.to(device)
                preds = model(x_val).argmax(dim=1).cpu()
                all_preds.extend(preds.numpy())
                all_labels.extend(y_val.numpy())

        from sklearn.metrics import f1_score
        val_f1 = f1_score(all_labels, all_preds, average="weighted")

        print(
            f"Epoch {epoch+1}/{epochs} | "
            f"Train Loss: {train_loss/len(train_loader):.4f} | "
            f"Val F1: {val_f1:.4f}"
        )

        if val_f1 > best_val_f1:
            best_val_f1 = val_f1
            torch.save(model.state_dict(), "deeplob_best.pt")

원래 논문의 핵심 학습 하이퍼파라미터:

  • 옵티마이저: ϵ=1\epsilon = 1인 Adam(이례적으로 큰 값으로 — 잡음이 많은 금융 데이터에서 업데이트를 안정화하는 데 도움이 된다)
  • 학습률: 0.01
  • 배치 크기: 32
  • 룩백 윈도: T=100T = 100 타임스텝

우리는 검증에서 정확도가 아니라 가중 F1을 추적하는데, 세 클래스가 불균형하기 때문이다 — 그리고 다음 절에서 보듯이, F1은 원래 논문이 바로 그 이유로 강조하는 지표다.

DeepLOB가 포착하도록 설계된 것

DeepLOB가 학습하는 미시구조 신호: 주문 흐름 불균형, 큐 동역학, 거래량 가중 중간 가격 압력

DeepLOB의 매력적인 한 가지 측면은, 그 블록들이 위의 손으로 설계한 특징들에 의해 얼마나 밀접하게 동기를 부여받았는지다. 여기서 우리는 신중해야 한다. 논문은 이들을 학습된 네트워크의 입증된 속성이 아니라 설계 직관으로 제시한다. 논문은 필터 시각화, 활성화-불균형 상관관계, 또는 LSTM이 무엇에 주의를 기울이는지에 대한 분석을 발표하지 않는다. 따라서 다음 내용은 측정된 발견이 아니라 아키텍처적 의도로 읽어야 한다.

첫 번째 합성곱 블록. 1×21 \times 2 필터는 각 레벨에서 가격과 거래량을 짝짓는다. 구조적으로 이는 거래량 가중 가격을 형성하는 것과 같은 연산이다 — 논문은 첫 계층의 동기를, 그 특징 맵이 마이크로프라이스 유사 양을 형성한다는 점에서 찾는다. 네트워크는 그 계열의 무언가를 복원하도록 만들어졌다. 우리는 그것이 어떤 특정 추정량을 증명 가능하게 재발견한다고 주장하지 않는다.

두 번째 합성곱 블록. 매수-매도 쌍에 걸쳐 작동함으로써, 이 필터들은 여러 레벨에서 불균형 유사 특징을 학습하기에 적합한 위치에 놓인다 — 이 역시 입증된 상관관계가 아니라 구성에 의한 것이다.

인셉션 모듈. 다중 스케일 시간 합성곱은 서로 다른 주파수의 모멘텀을 포착하려는 것이다. 1×11\times1 경로는 시점별 구조에, 3×13\times1 경로는 단기 추세에, 5×15\times1 경로는 약간 더 긴 추세에, 그리고 (완전한 모듈에서는) 최대 풀링 경로는 평활화된 관점에 반응한다.

LSTM 계층. 순환 구성 요소는 합성곱이 표현할 수 없는 장기 의존성 — 체제 전환과 시간대 효과 — 에 대한 기억을 더한다. 우리는 더 강한 주장(예컨대 LSTM이 변동성 높은 체제에서 최근 데이터를 재가중하는 "적응적 주의(adaptive attention)"를 구현한다는 것)은 피한다. 논문은 이를 보여 주지 않는다.

FI-2010에서의 성능

원래 논문은 두 가지 프로토콜을 평가한다. Setup 1은 데이터셋의 초기 정규화 분할을 사용하며, Setup 2는 이후 대부분의 연구가 비교 대상으로 삼는 딥러닝 설정이다. FI-2010은 클래스 불균형이 있으므로, 논문은 정확도가 아니라 F1을 강조하며, 비신경망 기준 모델(SVM, MLP)에 대해서는 정밀도/재현율 기반 수치를 보고한다. 다음은 실제 Setup 2 수치다(F1, %).

구간 (k) SVM MLP CNN-I LSTM DeepLOB
10 35.88 48.27 55.21 66.33 83.40
20 72.82
50 80.35

k=10k = 10에서, DeepLOB는 83.40 F1과 함께 84.47% 정확도도 보고한다. Setup 2는 k{10,20,50}k \in \{10, 20, 50\}만 보고하며 — 여기에는 k=100k = 100 행이 없다. (Setup 1은 k{10,50,100}k \in \{10, 50, 100\}를 보고하며, DeepLOB F1은 각각 77.66 / 74.96 / 76.58이다.) 위의 대시는 이 프로토콜에서 해당 구간에 대해 논문이 표로 제시하지 않은 기준 모델 칸을 나타낸다.

몇 가지 관찰이 두드러진다.

  1. DeepLOB는 단기 구간에서 큰 차이로 기준 모델을 이긴다. k=10k = 10에서 그 F1(83.40)은 LSTM(66.33), CNN-I(55.21), MLP(48.27), SVM(35.88)을 훨씬 웃돈다. 질적 순서 — 일반 CNN, MLP, SVM을 넘어서는 심층 순환-합성곱 모델 — 가 견고한 결론이다.
  2. 구간에 따른 추세는 단조롭지 않으며, "길수록 쉽다"가 아니다. Setup 2 전반에서, kk10205010 \to 20 \to 50으로 갈 때 DeepLOB의 F1은 83.4072.8280.3583.40 \to 72.82 \to 80.35로 움직인다 — 깔끔한 상승 행진이 아니라 k=20k=20이 가장 어려운 U자형 패턴이다. 어떤 구간이 "더 쉬운지"는 설정, 임계값 α\alpha, 레이블 평활화 방식에 크게 좌우된다. 우리는 이 수치들에서 보편적 법칙을 읽어 내서는 안 된다.
  3. DeepLOB가 모든 칸에서 모든 고전적 방법을 압도하지는 않는다. C(TABL) 같은 강력한 기준 모델에 대해서는 일부 구간에서 차이가 좁혀지므로, "어디서나 모든 고전적 접근을 능가한다"는 과장이다. 방어 가능한 주장은, 경쟁력 있음에서 최고 수준이며 단기 구간에서는 분명히 최고라는 것이다.

그리고 다시 한번: 논문 자신의 견해는 FI-2010이 이 질문을 매듭짓기에는 너무 작고 너무 많이 다운샘플링되었다는 것이다. 그 더 강력한 근거는 LSE 연구로, 여기서 DeepLOB는 한 종목 집합으로 학습하고도 따로 떼어 둔(held-out) 종목에 대해 여전히 예측을 한다 — FI-2010으로는 보일 수 없는 전이 학습 결과다.

LOBFrame과 재현 위기

표본 내(in-sample)와 표본 외(out-of-sample) 곡선이 갈라지며 주문장 딥러닝의 재현 위기를 보여 주는 그래프

LOBFrame 벤치마크 프레임워크(Briola, Bartolucci, Aste, 2024)는 이러한 결과에 대해 냉정하게 균형을 잡아 준다. DeepLOB의 아키텍처는 견실하지만, 이 프레임워크는 몇 가지 중요한 단서를 부각한다.

미시구조 의존성. 모델 성능은 서로 다른 미시구조적 특성을 가진 종목에 따라 크게 달라진다. 유동성이 높고 틱이 큰 종목은 유동성이 낮고 틱이 작은 종목보다 예측하기 쉽다. 한 종목에서 강해 보이는 모델이 다른 종목에서는 상당히 저하될 수 있다.

예측 대 수익. 높은 분류 정확도가 자동으로 거래 수익으로 이어지지는 않는다. LOBFrame은 예측된 움직임이 매수-매도 스프레드를 넘어설 만큼 충분히 큰지와 결부된 지표를 강조한다. 모델은 스프레드를 극복하기에 충분히 자주, 그리고 충분히 큰 움직임에서 맞아야 한다 — 스프레드가 좁은 종목의 경우 그 기준이 높다.

레이블 민감도. 임계값 α\alpha와 구간 kk의 선택은 보고된 정확도와 예측의 실용적 가치 모두에 극적으로 영향을 준다. 너무 좁은 레이블은 잡음이 많은 목표를 만들고, 너무 넓은 레이블은 사소하게 "정확하지만" 쓸모없는 예측을 만든다.

DeepLOB를 넘어서: 현재의 지형

원래의 DeepLOB 모델을 공전하는 현대 주문장 예측 접근법들의 성좌

DeepLOB가 발표된 이후 몇 가지 확장이 등장했다.

지정가 주문장을 위한 트랜스포머(Wallbridge, 2020). 이후 문헌에서 흔히 "TransLOB"로 느슨하게 불리지만, 모델 자체는 인과적/팽창(dilated) 합성곱 특징 추출기 뒤에 마스킹된 다중 헤드 자기 주의(masked multi-head self-attention)가 이어지는 것이다 — 단순히 DeepLOB의 LSTM을 트랜스포머로 바꾼 것이 아니며, 인셉션+합성곱 스택을 재사용하지 않는다. 이는 당시 기준으로 FI-2010의 새로운 최첨단(state of the art)을 보고했다.

암호화폐 도메인 변형. 여러 연구 그룹이 DeepLOB 스타일의 아키텍처를 암호화폐 시장에 적용하는데, 이곳의 주문장 동역학은 주식과 상당히 다르다 — 더 넓은 스프레드, 24시간 연중무휴 거래, 그리고 모두 재학습을 요구하는 다른 참가자 구성이 그렇다.

주의 보강 변형. 한 갈래의 연구는 인셉션 블록과 LSTM 블록 사이에 다중 헤드 주의를 삽입하여, 네트워크가 현재 예측에 가장 정보가 많은 레벨이나 타임스텝에 집중할 수 있게 한다.

LOB-Bench(Nagy, Frey, Li, Sarkar, Vyetrenko, Zohren, Calinescu, Foerster, 2025). 이 벤치마크는 관심을 예측에서 생성으로 돌려, 주문장 데이터의 생성 모델이 얼마나 현실적인지를 채점한다 — 방향성 예측보다는 백테스팅과 에이전트 학습에 관련된다.

운영 환경을 위한 실무적 고려 사항

라이브 트레이딩 시스템에 DeepLOB 스타일의 모델을 배포하는 것을 고려한다면, 몇 가지 엔지니어링 문제가 발생한다.

지연 시간(latency)

모델은 트레이딩 시스템의 지연 시간 예산 안에서 예측을 만들어 내야 한다. 밀리초 미만의 지연 시간으로 작동하는 HFT 시스템의 경우, 최적화된 PyTorch 추론조차 너무 느릴 수 있다. 선택지로는 다음이 있다.

  • TensorRT 최적화를 동반한 ONNX 내보내기
  • INT8로의 양자화
  • 가장 지연 시간에 민감한 응용을 위한 FPGA 배포

데이터 파이프라인

모델은 고정된 주기로 정규화되고 정렬된 주문장 스냅숏을 기대한다. 실제로 LOB 업데이트는 비동기적으로 도착한다. 다음이 필요하다.

  • 현재 주문장 상태를 유지하는 스냅숏 재구성 엔진
  • (T,40)(T, 40) 입력 텐서를 만들어 내는 고정 주기 샘플러
  • 롤링 통계를 사용한 온라인 정규화

특징 안정성

학습 데이터로 계산한 z-점수 정규화 파라미터는 시간이 지나면서 표류한다. 가격이 바뀌고, 변동성 체제가 이동하며, 시장 구조가 진화한다. 운영 시스템에는 다음이 필요하다.

  • 롤링 정규화 윈도(예: 매일 통계 재계산)
  • 모델 재학습을 촉발하는 체제 탐지
  • 입력 분포 이동(distribution shift) 모니터링

미시구조에 대한 과적합

LOB 패턴은 거래소별, 종목별로 다르다. NASDAQ 주식으로 학습한 모델은 재학습 없이는 Binance BTC/USDT에서 작동하지 않는다. 같은 거래소 안에서도 모델은 다음에 과적합될 수 있다.

  • 틱 크기 체제(틱이 큰 종목 대 틱이 작은 종목)
  • 시간대 패턴(개장 동시호가, 점심시간 소강, 종가 결정 크로스)
  • 시간이 지나면서 변하는 마켓 메이커의 행동

결론

DeepLOB는 시장 미시구조에 대한 깔끔하고 잘 동기 부여된 딥러닝 응용을 대표한다. 그 세 블록 아키텍처 — 공간 특징을 위한 CNN, 다중 스케일 시간 패턴을 위한 인셉션, 순차 의존성을 위한 LSTM — 는 지정가 주문장 데이터의 구조에 자연스럽게 대응된다.

핵심 통찰은 단지 심층 모델이 손으로 설계한 특징과 맞먹거나 그를 이길 수 있다는 것이 아니라, 아키텍처의 귀납적 편향 — 1×21 \times 2 필터, 인셉션 병렬성, 순환 기억 — 이 주문장이 어떻게 작동하는지에 대한 진정한 도메인 지식을 부호화한다는 것이다. 이는 금융 데이터에 무작정 던진 일반적인 심층 네트워크가 아니다. 지정가 주문장의 특정한 기하 구조를 중심으로 설계된 아키텍처다. 그리고 그 가장 설득력 있는 근거는 저자들 자신이 신뢰하지 않는 FI-2010 리더보드가 아니라, 1년 치 LSE 데이터에 대한 전이 학습 결과다.

실무자에게 DeepLOB는 강력한 기준 모델이자 유용한 구성 요소다. 연구자에게는, 단지 모델 규모만이 아니라 사려 깊은 아키텍처 설계가 구조화된 금융 데이터에서 성능을 끌어낸다는 것을 보여 준다. 다만 실제 수치를 인용하고, 레이블 방식을 주시하며, 분류 정확도를 거래 수익에는 한참 못 미치는 것으로 취급해야 한다는 점을 잊지 말라.


참고 문헌

  1. Zhang, Z., Zohren, S., & Roberts, S. (2019). DeepLOB: Deep Convolutional Neural Networks for Limit Order Books. IEEE Transactions on Signal Processing, 67(11), 3001-3012. arXiv:1808.03668

  2. Ntakaris, A., Magris, M., Kanniainen, J., Gabbouj, M., & Iosifidis, A. (2018). Benchmark Dataset for Mid-Price Forecasting of Limit Order Book Data with Machine Learning Methods. Journal of Forecasting, 37(8), 852-866. arXiv:1705.03233

  3. Briola, A., Bartolucci, S., & Aste, T. (2024). Deep Limit Order Book Forecasting (LOBFrame). arXiv:2403.09267

  4. Wallbridge, J. (2020). Transformers for Limit Order Books. arXiv:2003.00130

  5. Cont, R., Kukanov, A., & Stoikov, S. (2014). The Price Impact of Order Book Events. Journal of Financial Econometrics, 12(1), 47-88.

  6. Stoikov, S. (2018). The Micro-Price: A High-Frequency Estimator of Future Prices. Quantitative Finance, 18(12), 1959-1966.

  7. Nagy, P., Frey, S., Li, K., Sarkar, B., Vyetrenko, S., Zohren, S., Calinescu, A., & Foerster, J. (2025). LOB-Bench: Benchmarking Generative AI for Finance — an Application to Limit Order Book Data. ICML 2025. arXiv:2502.09172

  8. DeepLOB code (original in Keras/TensorFlow; the repository also provides a PyTorch port): github.com/zcakhaa/DeepLOB-Deep-Convolutional-Neural-Networks-for-Limit-Order-Books

blog.disclaimer

Authors

Eugen Soloviov
Eugen Soloviov

Trading-systems engineer

Trading-systems engineer building bots since 2017: cross-exchange arbitrage (connected up to 30 venues), cointegration-based pairs arbitrage across spot and futures, scalping, news and sentiment-driven strategies, trend algorithms, and portfolio management and balancing algorithms. Also builds sub-millisecond order execution, big-data warehouses, backtesting engines, AI agents, and trading interfaces (incl. open-source profitmaker.cc). Stack: JS/TS, Python, Rust/Zig/Go, DevOps, backend, frontend, architecture.

Newsletter

시장에서 앞서 나가세요

뉴스레터를 구독하여 독점적인 AI 트레이딩 통찰력, 시장 분석 및 플랫폼 업데이트를 받아보세요.

귀하의 개인정보를 존중합니다. 언제든지 구독을 취소할 수 있습니다.