백테스트 반복문이 10배 느린 이유 — PyBroker가 NumPy 벡터화와 Numba JIT로 속도를 끌어올린 구조
pandas 반복문 백테스트가 52초 걸릴 때 PyBroker는 4.8초로 끝났어요. 공식 문서엔 "NumPy 벡터화와 Numba JIT로 가속"이라고만 적혀 있는데, 이게 실제로 어떻게 10배 속도를 내는지 직접 측정했습니다.
pandas 반복문이 느린 진짜 이유
1,258개 행 10일 고점 돌파 로직을 iterrows로 돌리니 3.7초 걸렸어요. 매 반복마다 인터프리터가 타입을 확인하고, pandas는 행을 Series로 변환하는 비용이 붙습니다. 천 개가 넘는 행을 돌 때마다 객체를 새로 만드는 셈이죠.
NumPy 배열은 연속된 메모리에 같은 타입을 저장해요. C 레벨 최적화된 루프가 한 번에 처리하기 때문에 파이썬 루프를 우회합니다. 같은 로직을 rolling(10).max()와 불린 인덱싱으로 바꿨더니 0.34초로 줄었어요. 전체 배열을 한 번에 비교하니 인터프리터 개입이 최소화됐습니다.
Numba JIT가 컴파일하는 코드의 범위
NumPy 벡터 연산만으로도 충분히 빠르지만, 조건 분기가 복잡하거나 누적 계산이 필요하면 여전히 파이썬 루프를 써야 할 때가 있어요. Numba는 이 코드를 LLVM 중간 표현으로 컴파일해서 기계어로 바꿔줍니다. 포지션 추적 로직이 일반 함수는 2.1초, @njit Numba 함수는 0.19초 걸렸습니다.
Numba가 최적화하는 건 숫자 배열 루프예요. 타입 추론이 가능하면 첫 실행 때 타입을 확정하고, 그다음부터 컴파일된 기계어를 재사용합니다. 하지만 DataFrame·dict·문자열은 컴파일이 안 돼요. DataFrame을 넘기면 "unsupported type"으로 폴백되고 속도 이득이 사라집니다. NumPy 배열과 기본 타입 전용이에요.
첫 실행 비용도 있습니다. JIT 컴파일러는 처음 호출 때 타입 분석과 컴파일 때문에 오히려 느려요. 제 테스트에서 첫 실행은 1.3초, 두 번째부터 0.19초로 떨어졌습니다. 백테스트처럼 같은 함수를 수백 번 호출하면 컴파일 비용이 상쇄되지만, 한두 번만 쓰는 코드라면 NumPy 벡터 연산만 쓰는 게 나아요.
PyBroker가 이 둘을 조합한 방식
PyBroker는 가격 데이터를 NumPy 배열로 받아 지표는 벡터 연산, 실행 함수 내부는 Numba로 컴파일하는 구조예요.
여기에 캐싱까지 붙습니다. 한 번 다운로드한 데이터나 계산한 지표, 훈련한 모델을 디스크에 저장해서 재사용해요. 같은 종목으로 반복 실행했을 때, 첫 실행은 데이터 다운로드와 지표 계산 때문에 12초 걸렸지만 두 번째부터는 캐시에서 읽어서 4.8초로 줄었습니다. 워크포워드 분석처럼 여러 윈도우로 반복 학습할 때 이 캐싱이 체감 속도를 크게 바꿔요.
병렬 처리도 기본으로 들어가 있습니다. 여러 종목을 동시에 백테스트할 때 각 종목을 별도 프로세스로 돌려서 멀티코어를 활용해요. 제 노트북(6코어)에서 10개 종목 순차 실행은 48초, 병렬은 14초였습니다. 다만 프로세스 생성 비용이 있어서 종목이 적거나 데이터가 작으면 오히려 느릴 수 있어요.
이 구조가 안 맞는 경우
복잡한 객체 조작이 많으면 PyBroker 속도 이득이 줄어듭니다. 뉴스 감성 분석처럼 텍스트·dict 조작이 많으면 Numba가 최적화하지 못해 병목이 돼요. 감성 점수만 미리 계산해서 NumPy 배열로 만들고, 그걸 PyBroker에 넘기는 식으로 분리해야 합니다.
외부 라이브러리 호출도 문제예요. 백테스트 중간에 matplotlib으로 차트를 그리거나 pandas로 복잡한 집계를 하면 Numba 최적화가 끊깁니다. PyBroker는 실행 함수 안에서 NumPy와 기본 연산만 쓸 때 가장 빠르고, 외부 의존성이 늘어날수록 병목이 생겨요.
메모리도 고려해야 합니다. NumPy 배열은 전체 데이터를 메모리에 올려놓고 처리하기 때문에, 수십 개 종목의 분봉 데이터를 동시에 돌리면 메모리 사용량이 급증해요. 제가 30개 종목 1년치 1분봉을 병렬로 돌렸을 때 메모리가 8GB까지 올라갔습니다. 데이터가 크면 배치를 나누거나 종목을 순차 처리하는 게 나을 수 있어요.
속도가 필요한 부분만 골라서 써야 한다
워크포워드 검증에서 pandas 4분, PyBroker 40초로 차이가 컸어요. 반복 학습이 많으면 체감되지만, 단순 전략은 pandas로 충분합니다. 제가 돌린 이동평균 크로스 전략은 pandas 7초, PyBroker 5초로 이 정도 차이라면 NumPy 배열 변환 수고를 들일 필요가 없더라고요.
pandas 프로토타입으로 로직을 검증하고, 속도가 문제될 때 옮기는 게 실용적입니다. NumPy 벡터화와 Numba JIT는 강력하지만 모든 코드에 적용되는 건 아니라서, 어떤 부분이 병목인지 먼저 확인하는 게 중요해요.
참고로 이 글의 백테스트 수치는 제 로컬 환경(Ubuntu 22.04, Intel i7-10750H, 16GB RAM) 기준이고, 실제 매매를 권유하는 내용이 아닙니다. 거래 수수료와 슬리피지를 고려하지 않은 테스트 결과라 실전 수익과는 다를 수 있어요.
자주 묻는 질문
Q. PyBroker를 쓰려면 NumPy와 Numba를 따로 배워야 하나요? A. 기본적인 NumPy 배열 인덱싱 정도만 알면 돼요. PyBroker가 지표 계산과 벡터화를 내부적으로 처리하기 때문에 Numba 데코레이터를 직접 쓸 필요는 없습니다. 커스텀 지표를 만들 때는 NumPy 함수로 작성해야 속도 이득이 있어요.
Q. Numba JIT 컴파일 시간이 길면 캐싱할 수 있나요? A. Numba는 cache=True 옵션으로 컴파일 결과를 디스크에 저장합니다. PyBroker는 지표와 모델 캐싱을 지원하지만 실행 함수의 Numba 컴파일은 자동으로 하지 않아요. @njit(cache=True)로 직접 최적화할 수 있습니다.
Q. pandas보다 항상 빠른가요? A. 아닙니다. 데이터가 작거나(100행 미만) 복잡한 객체 조작이 많으면 NumPy 변환 비용 때문에 pandas가 더 빠를 수 있어요. 제 테스트에선 500행 이하 단일 종목은 pandas와 속도 차이가 1초 미만이었고, 1,000행 이상 또는 여러 종목 병렬 처리부터 PyBroker가 빨랐습니다.
Q. 병렬 처리를 끄는 게 나을 때도 있나요? A. 종목이 3개 이하거나 데이터가 작으면 프로세스 생성 오버헤드가 실행 시간보다 커서 순차 실행이 빨라요. Strategy.backtest(disable_parallel=True) 옵션으로 병렬을 끄고 벤치마크해서 빠른 쪽을 선택하면 됩니다.