Post

(OMS 2) 중요한 숫자 관리는 정수로

OMS, 매칭 엔진 등 매매 시스템에서 가격(price)이나 금액(amount)을 float/double 대신 정수(integer)로 변환하여 관리하는 이유를 정리합니다.

(OMS 2) 중요한 숫자 관리는 정수로

이전 글: 인트로

이 글은 매매 시스템 시리즈의 글입니다.

다음 글: 시세 수신

가장 기본적인 숫자 관리 얘기부터 해보겠습니다. I/O, 계좌, 오더 북 등 OMS 내부의 숫자는 정수로 관리합니다. 메모리 얼라인먼트와 함께 성능에 영향을 주는 기본적인 설계 결정입니다. 이유를 살펴보겠습니다.

부동소수점의 정밀도 문제

이진수로 표현 불가능한 십진 소수

IEEE 754 부동소수점은 이진수(base-2) 기반이라 십진 소수를 정확히 표현할 수 없습니다.

1
2
>>> 0.1 + 0.2
0.30000000000000004  # 0.3이 아님

0.1은 이진수로 0.0001100110011001100110011...처럼 무한 순환소수가 됩니다. 64비트 double도 결국 근사값일 뿐입니다.

누적 오차

수백만 건의 거래를 처리하면 작은 오차가 누적되어 실제 금액과 계산 금액이 불일치할 수 있습니다. 금융 시스템에서는 1원의 오차도 감사 실패로 이어질 수 있거든요.

실제 사례:

  • 독일 소매은행에서 부동소수점으로 복리 이자를 계산했다가 5년간 오차가 누적되었습니다. 일부 고객은 수백 유로를 초과 납부했고 일부는 미납 상태가 되었습니다. 은행은 12M 유로 규모의 정정 작업과 규제 벌금을 부담했습니다.
  • LSE(런던증권거래소)에서 고빈도 매매 알고리즘이 빠른 가격 계산을 하는 과정에서 부동소수점 오차가 누적되어 수천 건의 잘못된 거래가 발생했습니다.

비교 연산의 신뢰성 (핵심)

매매 시스템에서 가장 치명적인 문제입니다. 부동소수점 비교는 본질적으로 불안합니다.

같은 값인데 다르다고 판정

1
2
3
4
5
6
7
8
>>> a = 0.15 + 0.15
>>> a
0.30000000000000004
>>> b = 0.1 + 0.2
>>> b
0.3
>>> a == b
False

같은 논리적 결과를 다른 경로로 계산하면 미세하게 다른 값이 나옵니다.

프로그래밍 언어에 따라 float인 경우 key 값으로 사용 불가

Rust에서 f64Hash trait을 구현하지 않아 HashMap의 키로 사용할 수 없습니다.

가격 조회 실패

가격과 수량을 Vec<(f64, i64)> 등으로 관리한다고 해봅시다. 아주 비효율적이지만 일단 가능은 합니다. 이 경우에도 가격 조회 자체가 실패할 수 있습니다.

1
2
3
4
5
6
7
let order_book: Vec<(f64, i64)> = vec![(100.05, 100)];

// 나중에 같은 가격으로 조회
let lookup_price = 99.05 + 1.0;  // 논리적으로 100.05
let found = order_book.iter().find(|(p, _)| *p == lookup_price);  // None!

// 이유: 100.05와 (99.05 + 1.0)의 비트 표현이 다름

잔고 확인 실패

1
2
3
4
5
6
7
8
9
let mut balance: f64 = 1000.0;
balance -= 333.33;
balance -= 333.33;
balance -= 333.34;

if balance == 0.0 {
    close_account();  // 실행 안 됨!
}
// balance는 0.0이 아니라 -0.000000000000057 같은 값

Epsilon 비교의 함정

1
2
3
4
// 흔한 해결책
fn equal(a: f64, b: f64) -> bool {
    (a - b).abs() < 0.0001  // epsilon
}

이 방식도 문제가 있습니다:

  1. 값이 작을 때: 0.00001과 0.00002는 다른 값인데 같다고 판정
  2. 값이 클 때: 1000000.0과 1000000.0001은 같은 값인데 다르다고 판정
  3. epsilon 선택: 어떤 값이 적절한지 상황마다 다름

정수를 사용하면

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 명확한 비교
let price_ticks: i64 = 10_005;  // 100.05를 tick 단위로

if price_ticks == other_price_ticks { ... }  // 항상 정확

// HashMap 키로 사용해도 안전
let mut order_book: HashMap<i64, Vec<Order>> = HashMap::new();
order_book.insert(10005, orders);
let found = order_book.get(&10005);  // 항상 찾음

// 잔고 확인
let mut balance_cents: i64 = 100000;  // $1000.00
balance_cents -= 33333;
balance_cents -= 33333;
balance_cents -= 33334;
if balance_cents == 0 { ... }  // 정확히 동작

결론

매매 시스템에서 정수를 씁시다!

그럼 언제 float을 쓸까요?

  • 리스크 계산, greeks, featuring 등 분석 영역에서만 사용합니다

정수를 사용하면 비트 패킹으로 주문 ID 최적화처럼 공간 효율도 높일 수 있습니다. 시세 수신에서 실제로 거래소 데이터를 정수로 파싱하는 방법을 다룹니다.


참고 자료


다음 글: 시세 수신

This post is licensed under CC BY 4.0 by the author.