← 기사 목록으로
July 2, 2026
5분 소요

프레임워크 세금: 백테스트 라이브러리가 순진한 pandas 루프보다 느릴 때

프레임워크 세금: 백테스트 라이브러리가 순진한 pandas 루프보다 느릴 때
#알고트레이딩
#백테스트
#성능
#파라미터 탐색
#벡터화
#벤치마크
Part 2 of 10 · Collection
High-Performance Backtest Engines

"환상 없는 백테스트" 시리즈의 일부.

대부분의 알고트레이딩 프로젝트의 밑바닥에는 편안한 가정이 하나 있다. 성숙하고 별점 높은 백테스트 라이브러리는 빠르다 는 가정이다. 수년간의 기여가 있고, 진짜 이벤트 루프가 있고, 브로커 모델이 있고, 수수료 체계가 있다. 분명 당신이 직접 짰을 법한 어설픈 pandas 루프보다는 나을 것이다. 그래서 당신은 그것에 손을 뻗어 전략을 연결하고 파라미터 스위프 — 수천 개의 구성, 밤새 도는 작업 — 를 시작한다. 아침에 돌아오면 그것은 여전히 돌고 있다.

우리는 8개의 백테스트 엔진을 완전히 동일한 파라미터 스위프로 벤치마크했고, 탐색 도구를 고르는 방식을 바꿔야 할 무언가를 발견했다. 가장 인기 있는 오픈소스 이벤트 기반 프레임워크 두 개 — backtraderbt — 는 스위프를 우리가 버릴 셈으로 기준선으로 짠 순진한 pandas 루프보다 느리게 실행했다. 약간 느린 게 아니다. backtrader 는 pandas 기준선의 대략 2.5배의 시간이 걸렸고, bt 는 약 4.7배가 걸렸다. 한편 같은 작업을 하는 벡터화/컴파일 엔진은 bt 보다 약 13,000배 빠르게 실행했다.

이것이 프레임워크 세금이다. 인기 있는 백테스터는 한 번의 정직한 실행 — 하나의 전략, 하나의 데이터셋, 신중한 체결, 브로커답게 행동하는 브로커 — 을 위해 만들어졌다. 그것은 최종 검증이나 실거래 일치 확인에 당신이 원하는 바로 그것이다. 하지만 알고 연구가 실제로 시간을 쏟는 것 — 같은 전략을 노브만 살짝 다르게 해서 만 번 돌리는 것 — 에는 정확히 잘못된 도구다. 이 글은 그 세금을 측정하고, 그 메커니즘을 설명하며, "진짜" 백테스트 프레임워크가 잘못된 선택이 되는 시점에 대한 판단 규칙을 제시한다.

여기 나오는 모든 숫자는 완전히 동일하고 패리티가 고정된 워크로드 위에서, 하나의 재현 가능한 하네스(benchmarks/bench_oss_engines.py, 커밋 250dbb5)로부터 나온 것이다. 우리가 직접 엔진을 돌리지 않은 경우에는 그렇다고 밝히고, 수치를 지어내는 대신 별도의 정직성 섹션에 넣는다.

한 번의 실행 대 만 번의 실행

같은 전략 연산이 세 갈래로 갈라지는 경로를 따라 흘러내려간다: 붉은 주황색의 이벤트 기반 트랙은 모든 바 정류장에서 멈춰야 하고, 호박색의 가중치/리밸런스는 알고 노드의 트리가 되며, 단 하나의 벡터화된 에메랄드-시안 광선은 전체 시계열을 한 번에 해결한다 — 스위프 속도에서 패러다임은 곧 운명이다

파라미터 탐색을 규정하는 사실은, 엔진 은 수천 번 돌지만 분석 은 단 한 번 일어난다는 것이다. 백테스트를 셋업하기 위해 지불하는 고정 비용 — 이벤트 루프 구축, 브로커 인스턴스화, 바당 객체 할당 — 이 무엇이든, 당신은 그것을 모든 조합마다 지불한다. 한 번의 실행에서는 보이지 않던 비용("6초쯤 누가 신경 써?")이 스위프 전체에서는 청구서 그 자체가 된다("6초 × 10,000 = 16.6시간").

백테스트 엔진은 세 가지 패러다임으로 나뉘며, 그 패러다임이 스위프 성능의 운명을 결정한다:

  1. 이벤트 기반 — 엔진이 바를 하나씩 걸으며 이벤트를 방출하고, 당신의 next()/onBars() 콜백을 호출하며, 주문을 브로커 객체를 통해 라우팅한다. 이것이 backtrader, backtesting.py, PyAlgoTrade, zipline, nautilus_trader 의 아키텍처다. 실거래가 실제로 작동하는 방식을 그대로 반영하며, 바로 그 때문에 현실성 측면에서 신뢰받는다 — 그리고 바로 그 때문에 느리다: 바당 Python 오버헤드를 조합당 150,000번 지불한다.
  2. 가중치/리밸런스bt 가 여기에 속한다. 목표 가중치 행렬을 넘겨주면 당신이 지정한 날짜에 리밸런스한다. 바당 콜백은 없지만, 여전히 Python 안에서 평가되는 이벤트당 객체 그래프(알고의 트리, 거래 원장)가 있다.
  3. 벡터화 / 컴파일 — 전략 전체가 배열 연산(vectorbt), JIT 컴파일된 커널(numba), 또는 네이티브 코드(Rust, MLX GPU 커널)로 표현된다. 바당 Python 은 전혀 없다. 루프가 있다면, 그것은 머신 속도로 돈다.

이 글의 나머지는 그 분류법의 실증적 귀결이다. 우리는 하나의 워크로드를 만들었고, 모든 엔진이 증명 가능하게 같은 작업을 하도록 만든 뒤 시간을 쟀다.

워크로드: 하나의 전략, 여든 개의 노브, 모두에게 동일

벤치마크는 모든 엔진이 같은 일을 할 때에만 정직하다. 우리의 것은 의도적으로 평범하다 — 엔진 간에 다른 유일한 것이 엔진 자체인 워크로드다.

  • 데이터. 단일 합성 기하 브라운 운동 종가 시계열: 150,000 바, seed=42, 바당 변동성 sigma=0.0008, x0=30000. 결정론적이므로 누구나 비트 단위로 재현할 수 있다. 구조상 종가만 있다 — 전략이 종가 크로스이므로 OHLC 는 모든 구간에서 종가로 설정된다.
  • 전략. 헐 이동평균 크로스: 길이 L 의 HMA 를 더 빠른 3분의 1 변형(HMAHMA3)에 대해 교차시킨다. 항상 시장에 포지션을 두고, 각 크로스마다 롱/숏을 뒤집는다. 이것은 진짜의, 사소하지 않은 지표 — 두 개의 중첩된 가중 이동평균에 제곱근 윈도우 스무더를 더한 것 — 이며 장난감 SMA 가 아니어서, 바당 작업이 대표성을 띤다.
  • 스위프. 6..200 에 걸친 80 개의 HMA 길이. 그것은 직접 측정할 수 있을 만큼 작게 만든 "만 번의 실행"이다: 80개의 독립적인 조합, 각각이 150k 바에 걸친 완전한 백테스트다.
  • 비용. 왕복 수수료 0.09%, 사이드 수수료를 모델링하는 엔진에서는 사이드별로 나눈다. 동일 바 체결은 close[i] 에서 이뤄진다 — 바 i 의 신호는 그 바의 종가에 실행된다. 이것이 우리 프로덕션 엔진이 쓰는 관례다.

조합당 타이머는 정확히 두 가지를 감싼다: numpy 로 하는 HMA 사전 계산과 엔진 실행이다. 진짜로 일회성인 셋업(데이터 로드, 바 객체 구축)은 타이머 밖에 있다. 워밍업 실행이 한 번 있고, 그다음 best-of-N 반복이 있으며 — 이벤트 기반 엔진은 80개의 완전한 조합이 수 분에서 한 시간 넘게 걸릴 만큼 느리기 때문에 — 우리는 그리드의 균일한 샘플을 재고 선형으로 외삽한다. 조합은 독립적이므로 선형 외삽은 기댓값 상 정확하다. 같은 관례가 pandas 기준선에도 적용되므로, 그로 인해 유리해지는 엔진은 없다.

패리티: 모두가 같은 작업을 한다는 것의 증명

순진한 엔진 비교가 빠지는 함정이 여기 있다: "빠른" 엔진은 그저 더 적은 일을 하고 있는 것일 수 있다. backtrader 가 2,700건의 거래를 기장하는데 당신의 벡터화 엔진이 40건을 기장한다면, 그 벡터화 엔진은 빠른 게 아니다 — 틀린 것이고, 비교는 무의미하다.

그래서 우리는 거래 수 패리티 검사로 비교를 고정한다. L=104 에서 numpy 레퍼런스는 정확히 2,707건의 청산된 거래를 생성한다. 모든 엔진은 이것을 ±1 의 허용 범위 안에서 재현해야 하며, 그렇지 않으면 실행은 work-parity FAILED 어서션으로 중단된다. 이 허용 범위가 존재하는 이유는 엔진들이 기장 관례에 대해 의견이 갈리기 때문 — 마지막 열린 포지션을 강제 청산해서 세는지, 최초 진입을 "거래"로 보는지 — 이지 거래 자체에 대해서가 아니다:

엔진 L=104 에서 보고된 거래 수 관례
numpy 레퍼런스 2707 청산된 왕복 거래
backtesting.py 2708 +1: 마지막 포지션을 종료 시점에 강제 청산
backtrader 2707 마지막 열린 포지션은 세지 않음
bt 2708 +1: 최초 진입을 거래로 셈
PyAlgoTrade 2708 +1: 최초 진입을 체결로 셈

모든 엔진이 2707 ± 1 에 안착한다. 속도 차이가 무엇으로 드러나든, 그것은 한 엔진이 조용히 작업을 건너뛴 결과물이 아니다. 이것이 바로 이벤트 기반 프레임워크와 GPU 커널을 같은 표에 올려놓고 진심으로 말할 수 있게 하는 규율이다.

결과

여기 표 전체가 있으며, 가장 빠른 것부터 가장 느린 것 순으로 정렬했다. combos/s 는 처리량이고, 마지막 열은 완전한 80조합 스위프에 걸리는 시간이다. 기준선 행은 M0 — 순진한 pandas 엔진, 바 위를 도는 for 루프에 스칼라 기장을 붙인, 오후 한나절에 짜서 버릴 법한 그것이다. 그것보다 느린 것은 모두 굵게 표시되어 있다.

엔진 combos/s 패러다임 완전한 80조합 스위프
MLX GPU 커널 779 벡터화 (Apple GPU) 0.10 s
네이티브 Rust ~350 컴파일 0.23 s
mp + numba 246 컴파일 JIT + 멀티프로세스 0.33 s
vectorbt 56.9 벡터화 (numpy/numba) 1.4 s
numba (싱글 코어) 39.7 컴파일 JIT 2.0 s
backtesting.py 1.42 이벤트 기반 56 s
PyAlgoTrade 0.51 이벤트 기반 2.6 min
M0 — 순진한 pandas + 루프 0.28 스칼라 기준선 4.8 min
backtrader 0.11 이벤트 기반 12.7 min
bt 0.06 가중치 / 리밸런스 22.5 min

표를 위에서 아래로 읽으면 패러다임이 저절로 정렬된다: 상위 다섯은 모두 벡터화 아니면 컴파일이고, 하위 다섯은 모두 이벤트 기반이거나 객체 그래프 기반이다 — 그리고 순진한 pandas 루프는 성숙하고 인기 있는 프레임워크 두 개의 위에 앉아 있다. 위에서 아래까지의 폭은 네 자릿수다. 정확히 같은 2,707 거래 워크로드에서 MLX 커널은 스위프를 10분의 1초에 끝내지만, bt 는 22분 반이 필요하다. 그것은 대략 13,000배의 차이다.

표 한가운데의 스캔들

표 한가운데의 역설: 수수한 회청색의 순진한 for 루프 주자가, 붉은 주황색으로 빛나는 화려하고 기능이 잔뜩 실린 이벤트 기반 프레임워크 기계 두 대보다 먼저 결승선을 넘고 있으며, 그 기계들은 브로커 모델과 애널라이저 스택에 무겁게 짓눌린 채 여전히 뒤에서 애쓰고 있다 — 동일한 2,707 거래 워크로드에서 겸손한 기준선이 성숙한 프레임워크들을 이기고 있다

눈길을 끄는 숫자는 양극단에 있지만, 교훈적인 결과는 한가운데에 있다: backtrader (0.11 combos/s) 와 bt (0.06 combos/s) 는 둘 다 순진한 pandas 기준선 (0.28 combos/s) 보다 느리다.

이것은 곱씹어 볼 가치가 있다. M0 는 영리한 엔진이 아니다. DataFrame 에 인덱스로 접근하고, 포지션과 현금을 평범한 스칼라로 추적하며, 거래를 리스트에 덧붙이는 Python for 루프다 — 명백히 나빠서 이길 만한 대상을 두려고 포함한, 의도적으로 최적화하지 않은 "대조군"이다. pandas 의 행별 접근은 악명 높게 느리고, 우리는 그것에 기댔다. 그런데도 생태계에서 가장 많이 추천되는 백테스트 라이브러리 두 개가 그것에 진다: backtrader 는 2.5배, bt 는 4.7배로.

이것을 정직하게 유지하는 미묘한 점: 모든 이벤트 기반 엔진이 pandas 보다 느린 것은 아니다. backtesting.py (1.42 combos/s) 는 기준선을 5배 앞선다. 왜냐하면 그것은 바당 객체 생성을 최소로 유지하는, 군살 없는 numpy 기반 이벤트 루프이기 때문이다. PyAlgoTrade (0.51) 도 기준선을 근소하게 앞선다. 그러니 "이벤트 기반"이 자동으로 사형 선고는 아니다 — 하지만 바당 기계 장치가 무거울수록 더 나빠지고, 여기서 backtrader 와 bt 가 가장 무거운 기계 장치를 지고 있다. 패러다임이 천장을 정하고, 구현이 그 천장 아래 어디에 착지할지를 결정한다.

요점은 이것들이 나쁜 라이브러리라는 게 아니다. backtrader 의 브로커 모델과 bt 의 알고-트리 설계는 당신에게 정확성과 표현력 을 사주려고 존재한다 — 현실적인 주문 처리, 포트폴리오 리밸런싱, 애널라이저. 그 기능들에는 런타임 비용이 있고, 그 비용은 한 번 돌릴 때는 보이지 않는다. 스위프 전체에서는 그것이 이야기의 전부다.

이벤트 기반 엔진은 왜 세금을 내는가

바당 프레임워크 세금이 부과되는 지점: 단일 바 반복이 그 값비싼 구성 요소들로 폭발한다 — 구체화된 바 객체, 콜백 프레임, 주문을 라우팅하는 브로커, 덧붙여지는 원장 — 각각이 동전을 긁어가는 붉은 주황색으로 빛나는 요금소이며, 1,200만 개의 동일한 바 반복으로 이뤄진 아득히 멀어지는 거대한 복도를 따라 반복된다

메커니즘은 신비롭지 않다. 이벤트 기반 백테스트는 바당 대략 이런 일을 한다:

  1. 시계를 전진시키고, 다음 바를 데이터 피드에서 잘라내어 객체(Bar, Line, 딕셔너리)로 구체화한다.
  2. 사용자 코드(next(), onBars())로 콜백을 발화한다. 이것은 자기 프레임을 가진 Python 함수 호출이다.
  3. 콜백 안에서, 브로커/포지션 상태를 — 역시 메서드 호출과 속성 조회를 통해 — 질의한다.
  4. 주문이 생성되면 그것을 브로커를 통해 라우팅한다: 검증하고, 마진/현금을 확인하고, 체결을 스케줄링하고, 포트폴리오 객체를 변경하고, 거래 원장에 덧붙인다.
  5. 애널라이저, 옵저버, 그리고 프레임워크가 유지하는 온갖 기장을 갱신한다.

이제 150,000 바를 곱하고, 다시 80조합을 곱하라: 스위프당 1,200만 번의 바 반복, 각각이 한 줌의 Python 수준 객체 할당과 동적 디스패치다. Python 의 연산당 오버헤드 — 속성 조회나 작은 할당에 수십에서 수백 나노초 — 는 한 번이면 사소하지만 1,200만 번이면 파멸적이다. bt 의 경우는 같은 질병의 변종이다: 바마다가 아니라 거래일에만 리밸런스하긴 해도, 각 리밸런스가 알고 객체의 트리를 평가하고 pandas 기반 포트폴리오 원장을 건드리며, 그것이 조합당 2,707번, 그것이 80번이다.

순진한 pandas 루프가 backtrader 와 bt 를 이기는 이유는 무딘 이유에서다: 바당 더 적은 일을 하기 때문이다. 브로커, 이벤트 객체, 애널라이저 스택, 주문 라우팅 상태 기계를 건너뛴다. pandas 의 못생긴 행별 세금은 내지만, 그 하나의 못생긴 세금조차 프레임워크의 단정하고 기능 완비된 이벤트당 1객체 세금보다는 싸다. 백테스트를 "포지션 × 다음 수익 − 수수료"까지 벗겨내면, 프레임워크가 바당 하는 일의 대부분은 탐색 중에 당신이 쓰지도 않는 오버헤드다.

그리고 이것이 함정이다: 그 오버헤드가 당신이 그 프레임워크를 고른 이유 다. 당신은 현실적인 브로커를 원했다. 애널라이저를 원했다. 최종 검증 때는 그 전부를 원한다. 하지만 반대편으로 스칼라 목적값 하나만 읽어내는 10,000조합 탐색에서, 당신은 트랙을 도는 데 리무진 요금을 내고 있다.

표의 반대쪽 끝: 벡터화와 컴파일

표의 상단은 바당 Python 을 통째로 삭제하면 무슨 일이 벌어지는지를 보여준다.

  • vectorbt (56.9 combos/s) 는 전략 전체를 numpy/numba 배열 연산으로 표현한다. Python 안에 바 루프가 없다 — 신호, 포지션, PnL 이 모두 배열 수준이다. 스위프를 1.4초에 실행한다(bt 의 22.5분에 비해): 동일한 작업에서 약 950배 빠르다. (vectorbt 의 설계는 vectorbt 개요와 더 넓은 pandas 대 polars 비교에서 더 깊이 다룬다.)
  • numba (39.7) 는 형태를 바꾸지 않은 채 JIT 로 네이티브 코드로 컴파일된 pandas 루프다. M0 와 같은 알고리즘이지만, @njit 이 0.28 combos/s 를 39.7 로 바꾼다 — 데코레이터 하나로 약 140배 속도 향상이다. 왜냐하면 스칼라 루프를 지배하던 인터프리터 오버헤드가 그냥 증발하기 때문이다.
  • mp + numba (246) 는 컴파일된 커널을 CPU 코어들에 걸쳐 실행한다. 조합은 병렬화가 민망할 정도로 쉽다 — 각각이 독립적이다 — 그래서 멀티프로세싱이 JIT 위에서 거의 선형으로 확장된다.
  • 네이티브 Rust (~350) 는 Python 접착제의 마지막 조각을 제거한다: 스위프 전체가 네이티브다.
  • MLX GPU (779) 는 스위프를 Apple 실리콘 GPU 커널에 매핑한다. 80조합이 80개의 병렬 산술 레인이 된다. 엔터 키를 떼기도 전에 스위프가 끝난다.

정확히 짚어둘 만한 것이 두 가지다. 첫째, numba 는 언어보다 패러다임이 더 중요함을 증명한다. M0 와 numba 는 같은 알고리즘 을 돌린다 — 차이는 순전히 하나는 바당 해석되는 Python 이고 다른 하나는 컴파일된 것이라는 점뿐이다. 그것이 통제된 A/B 에서의 프레임워크 세금 전부다: 내부 루프에서 인터프리터를 삭제하는 데 약 140배. 둘째, numba (39.7) 에서 mp+numba (246), MLX (779) 로의 도약은 더 이상 엔진에 관한 것이 전혀 아니다 — 그것은 오케스트레이션과 하드웨어에 관한 것이다. 바당 세금이 사라지고 나면, 속도는 얼마나 많은 조합을 병렬로, 어떤 실리콘 위에서 돌리느냐의 문제가 된다. 우리는 그 전체 진행을 백테스트 엔진 속도 사다리에서 밟고, 마지막 1마일이 프로세스/직렬화 비용에 지배되는 이유를 IPC 세금 글에서 다룬다.

우리가 측정하지 않은 것(그리고 그것을 밝히는 이유)

벤치마크의 신뢰성은 그것이 무엇을 지어내기를 거부하는가에 산다. 우리는 8개의 엔진을 패리티 아래에서 끝에서 끝까지 돌렸다. 잘 알려진 몇몇 프레임워크에는 숫자를 붙이지 않았다. 측정하지 않은 수치를 외삽하느니 그것들을 정직하게 열거하는 편을 택한다:

  • zipline / zipline-reloaded — 이벤트 기반, Quantopian 혈통. 무거운 셋업(완전한 거래 캘린더와 데이터 번들)이 있어 사과 대 사과 식 조합당 타이밍 측정을 까다롭게 만든다. 아키텍처상 backtrader 와 함께 이벤트 기반 진영에 속하며, 표의 그 끝 근처에 오리라 예상하지만 증명하지는 못했다.
  • nautilus_trader — Rust/Cython 코어를 가진 이벤트 기반으로, 실거래 일치를 위해 명시적으로 설계되었다. 코어가 컴파일되어 있어 Python 세금을 완전히 내지는 않을 가능성이 가장 높은 이벤트 기반 엔진이다 — 우리가 아직 돌려보지 않은, 진정으로 흥미로운 측정이다.
  • QuantConnect Lean — C# 기반으로, 완전히 다른 런타임이다. Python 하네스에서는 직접 비교되지 않는다.
  • Jesse — 이벤트 기반, 암호화폐 중심. 그 설계는 별도의 노트에서 리뷰했지만 여기서 벤치마크하지는 않았다.
  • QSTrader — 이벤트 기반, 포트폴리오 지향. 같은 패러다임의 단서가 적용된다.
  • fastquant — 시도했다. 하지만 우리 환경에서 설치/API 가 깨져 있어 숫자가 없다. 우리는 그것을 추측하지 않을 것이다.

우리가 실제로 보고하는 숫자에 관한 정직한 단서 두 가지. vectorbt, numba, mp+numba, 네이티브 Rust, MLX 수치는 동일한 워크로드 위에서 우리 자신의 엔진 사다리로부터 나온 것이지, 네 개의 이벤트 기반 행을 낳은 OSS 하네스로부터 나온 것이 아니다 — 같은 워크로드지만 다른 측정 장비이며, 네이티브 Rust 수치는 대략적인 ~350 이지 정밀한 측정이 아니다. 그리고 절대 combos/s 는 하드웨어에 종속적이다. 옮겨가는 것은 순서와 비율 이며, 그것들은 충분히 커서(위에서 아래까지 13,000배, pandas 대 프레임워크 역전에서 2.5~4.7배) 어떤 합리적인 하드웨어 차이도 그것들을 뒤집지 못한다.

이벤트 기반 엔진을 옹호하며

제 역할을 하는 이벤트 기반 엔진: 살아남은 하나의 전략에 대한, 단 한 번의 꼼꼼하고 고충실도인 검증 실행. 현실적인 체결을 모델링하고 마진과 주문 수명 주기를 존중하는, 정밀한 에메랄드-시안 계기로 그려져 있다 — 탐색 중에는 순전한 낭비였던 바로 그 바당 기계 장치가, 여기서는 값진 장인 정신으로 드러난다. 배치 전의, 신중한 한 번의 총연습이다

이것을 "이벤트 기반 백테스터는 나쁘다"로 읽기는 쉬울 것이다. 그것은 잘못된 교훈이고 부당하기도 하다.

이벤트 기반 엔진은 다른 일을 위해 만들어졌고, 그리고 그 일을 잘한다. 바당 브로커, 주문 수명 주기, 체결 로직, 애널라이저 — 그 기계 장치는 백테스트를 실거래에 최대한 가깝게 닮게 만들려고 존재한다. 당신의 목표가 곧 배치하려는 전략의 단 한 번의 믿을 만한 총연습일 때, 당신은 엔진이 모든 체결에 땀 흘리고, 부분 체결을 모델링하고, 마진을 존중하며, 당신이 얻을 수 없었을 가격에 거래하도록 두기를 거부하기를 원한다. 그 충실도가 바로 제품이다. 그 런타임 비용은 현실성의 대가이며, 한 번의 실행에서 그 대가는 무시할 만하다.

실패는 엔진이 아니다. 그것을 잘못된 단계에서 쓰는 것이다. 연구에는 정반대의 요구를 가진 두 개의 별개 단계가 있다:

  • 탐색 은 처리량을 원한다. 당신은 풍경을 탐사하고 있고, 그 대부분은 쓰레기이며, 두 번 볼 가치가 있는 소수를 찾으려면 수천 개의 점을 평가해야 한다. 점당 충실도는 거의 중요하지 않다 — 당신은 순위를 매기고 있지 배치하는 게 아니다. 여기서 프레임워크 세금은 순전한 낭비다.
  • 검증 은 충실도를 원한다. 당신은 한 줌의 후보를 가지고 있고, 그것들이 현실적인 실행, 수수료, 슬리피지, 그리고 종이상의 수익을 부풀리는 선행 편향의 함정을 견뎌내는지를 최대한 정밀하게 알아야 한다. 여기서 이벤트 기반 엔진이 그 비용값을 한다.

프레임워크 세금이 벌하는 실수는 검증 엔진에서 탐색 을 돌리는 것이다 — 99%를 버릴 풍경을 탐사하는 데 리무진 요금을 내는 것.

판단 규칙

실용적인 요점은 하나의 판단으로 압축된다:

탐색은 벡터화/컴파일 엔진에서 하라. 살아남은 것은 이벤트 기반 엔진에서 검증하라.

구체적으로:

  1. 스위프를 위한 빠른 커널을 만들거나 빌려와라. 기성품을 원하면 vectorbt. 전략이 깔끔하게 벡터화되지 않으면 numba 로 컴파일한 루프(@njit 데코레이터 하나만으로 여기서 약 140배를 벌었다). 파라미터 공간 전체를 그것에 통과시켜라.
  2. 큰 스위프를 backtrader, bt, zipline, 또는 어떤 무거운 이벤트 기반 프레임워크에서도 절대 돌리지 마라. 그중 하나에서의 스위프가 당신의 병목이라면, 해결책은 더 큰 머신이 아니다 — 잘못된 엔진인 것이다. 순진한 pandas 루프조차 그중 둘은 이길 것이다.
  3. 짧은 후보 목록을 이벤트 기반 엔진으로 승격시켜 충실도 검사를 하라. 한 줌의 생존자를 데려다 현실적인 엔진에서 다시 돌려라. 거기서 브로커 모델과 체결 로직이, 빠른 커널이 추상화해 버린 문제들을 드러낼 수 있다.
  4. 두 엔진 사이에 패리티를 강제하라. 빠른 엔진과 충실도 엔진은 고정된 구성에서 거래 수와 PnL 에 대해 일치해야 한다(L=104 에서의 ±1 거래 검사). 그렇지 않으면 탐색과 검증은 서로 다른 전략을 측정하고 있고, 파이프라인 전체가 거짓말이다.

이것은 탐색과 검증이 정반대의 비용 프로파일을 가질 때마다 등장하는, 같은 2단 속도 아키텍처다. 그리고 이것이 우리 자신의 스택이 파라미터 탐색을 위해 빠른 벡터화/컴파일 경로를 유지하고, 최종 목적 함수 평가고원 검사를 위해 무거운 기계 장치를 예약해 두는 이유다.

핵심 정리

  1. 인기 있음 ≠ 스위프에서 빠름. 하나의 동일하고 패리티가 고정된 워크로드(150k 바, 80개의 HMA 크로스 조합, 2,707 거래)에서 backtrader (0.11 combos/s) 와 bt (0.06) 는 둘 다 순진한 pandas 루프 (0.28) 보다 느리게 실행했다. 성숙하고 별점 높은 프레임워크가 자동으로 빠른 선택은 아니다.
  2. 프레임워크 세금은 바당이고, 스위프가 그것을 곱한다. 스위프당 1,200만 번의 바 반복, 각각이 이벤트 객체, 콜백, 브로커 왕복을 나른다. 한 번의 실행에서는 보이지 않던 비용이 만 번에 걸쳐서는 청구서 전부다.
  3. 패러다임이 천장을 정한다. 벡터화/컴파일 엔진(vectorbt 56.9, numba 39.7, mp+numba 246, 네이티브 Rust ~350, MLX 779)은 이벤트 기반 엔진을 두 자릿수에서 네 자릿수만큼 — 위에서 아래까지 최대 약 13,000배 — 이긴다. 같은 알고리즘이, 단지 JIT 컴파일되었을 뿐인데 140배 빨라졌다.
  4. 모든 이벤트 기반 엔진이 동등하지는 않다. backtesting.py (1.42) 와 PyAlgoTrade (0.51) 는 그래도 순진한 기준선을 이긴다. 세금은 바당 기계 장치가 얼마나 무거운지에 따라 확장된다. 천장 아래 어디에 착지할지는 구현이 결정한다.
  5. 두 엔진, 두 단계. 탐색은 벡터화/컴파일 커널에서, 살아남은 것은 현실적인 이벤트 기반 엔진에서 검증하라. 둘이 같은 전략을 측정하도록 둘 사이에 거래 수/PnL 패리티를 강제하라.
  6. 무엇을 측정했는지에 정직하라. 우리는 8개의 엔진을 패리티 아래에서 벤치마크했고, 측정하지 않은 것들(zipline, nautilus_trader, Lean, Jesse, QSTrader, 그리고 돌아가지 않은 fastquant 설치)을 수치를 지어내는 대신 열거했다.

불편한 요약: 파라미터 스위프가 당신의 병목이라면, 문제는 아마 당신의 머신도 아니고 당신의 전략도 아니다. 문제는 당신이 단 한 번의 정직한 실행을 위해 만들어진 엔진에서 탐색을 돌리고 있다는 것 — 그리고 창피해서 차마 남겨두지 못했던 그 pandas 루프가 오히려 더 빨랐으리라는 것이다.

완전한 하네스, 패리티 어서션, 그리고 엔진별 원시 JSON 결과는 커밋 250dbb5benchmarks/bench_oss_engines.pybenchmarks/results_oss/ 에 있다. 사다리의 컴파일/GPU 쪽에 대해서는 백테스트 엔진 속도 사다리IPC 세금 분석을 참고하라.

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 트레이딩 통찰력, 시장 분석 및 플랫폼 업데이트를 받아보세요.

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