NestBook / quant-trading

전통적인 투자 관리 프로세스와 퀀트 투자 프로세스의 핵심적인 차이 하나는, 퀀트 투자 전략을 과거에 어떻게 작동했을지 백테스팅해볼 수 있다는 것이다. 완전한 역사적 성과 데이터와 함께 전략이 상세하게 기술되어 있더라도, 직접 백테스트해볼 필요가 있다. 이 작업은 여러 목적에 부합한다. 최소한, 이 연구의 재현은 전략을 완전히 이해하고 트레이딩 시스템으로 정확하게 구현했는지 확인하게 해준다. 의학이나 과학 연구에서처럼, 다른 사람의 결과를 재현하는 것은 원래 연구가 이 과정을 괴롭히는 일반적인 오류를 범하지 않았는지도 확인해준다. 그러나 단순한 실사를 넘어서, 직접 백테스트를 해보면 원래 전략의 변형을 실험해볼 수 있어 전략을 다듬고 개선할 수 있다.

이 챕터에서는 백테스팅에 사용할 수 있는 일반적인 플랫폼, 백테스팅에 유용한 역사적 데이터의 다양한 출처, 백테스트가 제공해야 하는 최소한의 표준 성과 측정 지표들, 피해야 할 일반적인 함정들, 그리고 전략의 간단한 개선 방법을 설명한다. 원칙과 기법을 설명하기 위해 완전히 개발된 백테스팅 예시들도 몇 가지 제시할 것이다.


일반적인 백테스팅 플랫폼

백테스팅을 위해 설계된 상용 플랫폼이 수십만 달러짜리를 포함해 수없이 많다. 이 책의 스타트업 중심 기조에 맞춰, 저자가 익숙하고 경제적으로 구입할 수 있으며 널리 사용되는 것들부터 시작한다.

Excel

리테일이든 기관이든 트레이더에게 가장 기본적이고 가장 일반적인 도구다. Visual Basic 매크로를 작성할 수 있다면 그 능력을 더욱 확장할 수 있다. Excel의 장점은 “보이는 것이 실제다(What you see is what you get, WYSIWYG)“라는 점이다. 데이터와 프로그램이 한 곳에 있어서 숨겨진 것이 없다. 또한 나중에 설명할 “선행 편향(look-ahead bias)“이라는 일반적인 백테스팅 함정이 Excel에서는 발생하기 어렵다(매크로를 사용하지 않는 한—매크로를 쓰면 WYSIWYG가 아니게 된다). 스프레드시트에서 날짜와 각종 데이터 열 및 신호를 쉽게 정렬할 수 있기 때문이다. Excel의 또 다른 장점은 백테스팅과 실거래 주문 생성을 같은 스프레드시트에서 할 수 있어, 프로그래밍 작업의 중복을 없앤다는 것이다. Excel의 주요 단점은 비교적 단순한 모델만 백테스트할 수 있다는 것이다. 하지만 앞 챕터에서 설명했듯이, 단순한 모델이 종종 최선이다!

MATLAB

MATLAB®(www.mathworks.com)은 대형 기관의 퀀트 애널리스트와 트레이더들이 가장 많이 사용하는 백테스팅 플랫폼 중 하나다. 많은 수의 주식을 포함하는 전략을 테스트하는 데 이상적이다. (1,500개 종목이 포함된 전략을 Excel로 백테스팅하는 것을 상상해보라—가능하지만 꽤 고통스럽다.) 수많은 고급 통계 및 수학 모듈이 내장되어 있어, 트레이딩 알고리즘이 일부 정교하지만 일반적인 수학 개념을 포함할 때 트레이더들이 바퀴를 다시 발명할 필요가 없다. (좋은 예가 주성분 분석(principal component analysis)이다—통계적 차익거래 트레이딩의 팩터 모델에 자주 사용되는데, 다른 프로그래밍 언어로 구현하기가 번거롭다. 예시 7.4 참조.) 인터넷에서 다운로드 가능한 서드파티 프리웨어도 많아서 퀀트 트레이딩 목적에 매우 유용하다(예시 7.2에서 사용된 공적분 패키지가 그 예다). 마지막으로, MATLAB은 재무 데이터가 담긴 웹페이지를 가져와 유용한 형태로 파싱하는 이른바 웹 스크래핑에도 매우 유용하다. 예시 3.1이 이를 어떻게 하는지 보여준다.

플랫폼이 겉으로는 정교해 보이지만, 실제로는 배우기 매우 쉽고(적어도 기본 사용 수준에서는) 이 언어를 사용해서 완전한 백테스트 프로그램을 작성하는 것도 매우 빠르다. MATLAB의 주요 단점은 라이선스 취득 비용이 1,000달러를 넘어 상대적으로 비싸다는 것이다. 그러나 시장에는 MATLAB과 매우 유사한 코드를 작성하고 사용할 수 있는 여러 클론이 있다:

이 클론들은 수백 달러에 불과하거나 완전히 무료일 수 있다. 당연히 클론이 비쌀수록 MATLAB으로 작성된 프로그램과 더 호환된다. (물론, 모든 프로그램을 직접 작성하고 서드파티 코드를 사용하지 않을 생각이라면 호환성은 문제가 되지 않는다. 하지만 그렇다면 이 언어의 주요 장점 중 하나를 포기하는 것이다.) MATLAB의 또 다른 단점은 백테스팅에는 매우 유용하지만 실행 플랫폼으로 사용하기에는 번거롭다는 것이다. 그래서 전략을 백테스트한 후에는 다른 언어로 별도의 실행 시스템을 구축해야 하는 경우가 많다. 이런 단점에도 불구하고 MATLAB은 퀀트 트레이딩 커뮤니티에서 광범위하게 사용되고 있다. 이 책의 모든 백테스팅 예시에 MATLAB 코드를 포함할 것이며, 부록에서 MATLAB 언어 자체에 대한 간략한 개요도 제공한다.


예시 3.1: MATLAB으로 웹 페이지 스크래핑해서 금융 데이터 가져오기

MATLAB은 수치 계산뿐만 아니라 텍스트 파싱에도 유용하다. 다음은 MATLAB을 사용해서 야후 파이낸스에서 주식의 역사적 가격 정보를 가져오는 예시다:

clear; % 이전에 정의된 변수 지우기

symbol='IBM'; % 관심 종목
% 웹페이지 가져오기
historicalPriceFile = ...
    urlread(['http://finance.yahoo.com/q/hp?s=', symbol]);

% 날짜 필드를 셀 배열로 추출
dateField = ...
    regexp(historicalPriceFile, ...
    '<td class="yfnc_tabledata1" nowrap align="right">...
    ([\d\w-]+)</td>', 'tokens');

% 숫자 필드를 셀 배열로 추출
numField=regexp(historicalPriceFile, ...
    '<td class="yfnc_tabledata1" align="right">...
    ([\d\.,]+)</td>', 'tokens');

% 문자열 셀 배열로 변환
dates=[dateField{:}]';

% 문자열 셀 배열로 변환
numField=[numField{:}]';

% doubles 배열로 변환
op=str2double(numField (1:6:end)); % 시가
hi=str2double(numField (2:6:end)); % 고가
lo=str2double(numField (3:6:end)); % 저가
cl=str2double(numField (4:6:end)); % 종가
vol=str2double(numField (5:6:end)); % 거래량
adjCl=str2double(numField (6:6:end)); % 수정 종가

이 프로그램 파일은 example3_1.m으로 다운로드 가능하다. 이 웹 스크래핑 스크립트에는 한 가지 한계가 있다: 한 번에 하나의 웹 페이지만 가져올 수 있다. Yahoo!가 역사적 데이터를 여러 페이지에 걸쳐 표시하기 때문에, IBM의 전체 가격 히스토리를 가져오는 데는 실용적이지 않다. 그럼에도 MATLAB의 텍스트 처리 기능을 보여주는 간단한 예시다.


TradeStation

TradeStation(www.tradestation.com)은 많은 리테일 트레이더들에게 증권사로 익숙한데, 증권사 서버와 연결된 올인원 백테스팅 및 거래 실행 플랫폼을 제공한다. 이 설정의 주요 장점은:

  • 백테스팅에 필요한 대부분의 역사적 데이터가 바로 이용 가능하다. Excel이나 MATLAB을 쓸 경우에는 다른 곳에서 데이터를 다운로드해야 한다.
  • 전략 프로그램을 백테스트한 후 동일한 프로그램으로 바로 주문을 생성해서 증권사로 전송할 수 있다.

이 방식의 단점은 전략 소프트웨어를 작성하고 나면 TradeStation에 증권사로 묶이게 된다는 것과, TradeStation이 사용하는 독점 언어가 MATLAB만큼 유연하지 않으며 일부 트레이더들이 사용하는 좀 더 고급 통계 및 수학 함수를 포함하지 않는다는 것이다. 그럼에도 올인원 시스템의 편리함을 선호한다면 TradeStation은 좋은 선택일 수 있다.

저자는 실제 작업에서 TradeStation을 사용하지 않았으므로 TradeStation으로 백테스팅 예시를 포함하지 않는다.

고급 백테스팅 플랫폼

기관급 고급 백테스팅 플랫폼을 구입할 재정적 여유가 있다면, 다음은 일부 목록이다:

  • FactSet의 Alpha Testing
  • Clarifi의 ModelStation
  • Quantitative Analytics의 MarketQA
  • Barra의 Aegis System
  • Logical Information Machines (www.lim.com)
  • Alphacet의 Discovery (www.alphacet.com)

이 중 저자가 직접 경험한 것은 Logical Information Machines와 Alphacet Discovery뿐이다. Logical Information Machines는 선물 트레이딩 전략 테스트에 뛰어나지만, 10년 전 경험을 바탕으로 하면 주식 전략에는 약하다. Alphacet Discovery는 데이터 검색, 백테스팅, 머신러닝 알고리즘을 이용한 최적화, 자동화된 실행을 통합하는 새로운 제품이다. 선물, 주식, 통화를 포함한 다양한 시장에서의 백테스팅과 트레이딩에 상당히 강력하다. 예시 7.1은 Discovery 플랫폼을 사용한 백테스트 예시다.


역사적 데이터베이스 찾기와 활용

특정 유형의 역사적 데이터가 필요한 전략을 염두에 두고 있다면, 가장 먼저 할 일은 그 데이터 유형을 구글에서 검색하는 것이다. (예: “free historical intraday futures data”로 검색해보라.) 무료이거나 저비용인 역사적 데이터베이스가 매우 많다는 것에 놀랄 것이다. 아래 표 3.1에는 저자가 수년간 유용하게 활용한 데이터베이스들을 정리했다. 대부분 무료이거나 매우 저렴하다. Bloomberg, Dow Jones, FactSet, Thomson Reuters, Tick Data와 같은 고가 데이터 공급업체들은 의도적으로 제외했다. 이들은 상상할 수 있는 거의 모든 유형의 데이터를 판매하지만, 주로 더 확고히 자리 잡은 기관에 서비스하며 개인이나 스타트업 기관의 가격대에 맞지 않는다.

인터넷에서 데이터 소스를 찾는 것은 유망한 전략을 찾는 것보다도 쉽지만, 이 섹션에서 나중에 설명할 이런 데이터베이스들과 관련된 여러 문제점과 함정이 있다. 이 문제들은 주로 주식 및 ETF(상장지수펀드) 데이터에만 적용된다. 가장 중요한 것들은 다음과 같다.

표 3.1 백테스팅용 역사적 데이터베이스

출처장점단점
일별 주식 데이터
Finance.yahoo.com무료. 분할/배당 수정됨생존자 편향 있음. 한 번에 하나의 종목만 다운로드 가능
HQuotes.com저렴. Finance.yahoo.com과 동일 데이터. 소프트웨어로 여러 종목 다운로드 가능생존자 편향 있음. 분할은 수정되나 배당은 미수정
CSIdata.com저렴. Yahoo!와 Google의 역사적 데이터 소스. 소프트웨어로 여러 종목 다운로드 가능생존자 편향 있음
TrackData.com저렴. 분할/배당 수정됨. 소프트웨어로 여러 종목 다운로드 가능. 펀더멘털 데이터 이용 가능생존자 편향 있음
CRSP.com생존자 편향 없음비쌈. 월 1회 업데이트
일별 선물 데이터
Quotes-plus.com저렴. 소프트웨어로 여러 종목 다운로드 가능
CSIdata.com(위 참조)
일별 외환 데이터
Oanda.com무료
인트라데이 주식 데이터
HQuotes.com(위 참조)인트라데이 데이터는 히스토리가 짧음
인트라데이 선물 데이터
DTN.com매수·매도 데이터 히스토리가 NxCore 제품에 포함비쌈. 라이브 데이터피드 구독 필요
인트라데이 외환 데이터
GainCapital.com무료. 히스토리 길음

데이터는 분할·배당 수정이 되어 있는가?

주식이 N 대 1로 분할되었을 때(N은 보통 2이지만 0.5 같은 분수일 수도 있다. N이 1보다 작으면 *역분할(reverse split)*이라 한다), 기준일이 T인 경우 T 이전의 모든 가격을 1/N으로 곱해야 한다. 마찬가지로, 회사가 기준일 T에 주당 배당금 $d를 지급했을 때, T 이전의 모든 가격에 다음 배수를 곱해야 한다: (Close(T – 1) – d) / Close(T – 1). 여기서 Close(T – 1)은 T 전날의 종가다.

달러 금액인 $d를 차감하는 대신 배수를 적용해서 역사적 가격을 조정하는 이유는, 역사적 일별 수익률이 조정 전후에 동일하게 유지되도록 하기 위해서다. 이것이 야후 파이낸스가 역사적 데이터를 조정하는 방식이며, 가장 일반적인 방법이다. ($d를 차감하는 방식으로 조정하면, 가격의 역사적 일별 변화는 조정 전후에 동일하지만 일별 수익률은 동일하지 않다.) 역사적 데이터가 수정되지 않으면, 기준일 시장 시가에서 전날 종가 대비 가격 하락이 발생하며(일반적인 시장 변동과 별개로), 이는 잘못된 트레이딩 신호를 유발할 수 있다.

이미 분할·배당 수정된 역사적 데이터를 구하는 것을 권장한다. 그렇지 않으면 분할과 배당의 별도 역사적 데이터베이스(예: earnings.com)를 찾아 직접 조정을 적용해야 하는데, 이는 다소 지루하고 오류가 발생하기 쉬운 작업으로, 다음 예시에서 설명할 것이다.


예시 3.2: 분할과 배당 조정

여기서는 역사에서 분할과 배당이 모두 있었던 ETF인 IGE를 살펴본다. IGE는 2005년 6월 9일(기준일)에 2:1 분할이 있었다. 해당 날짜 전후의 비수정 가격을 살펴보자(야후 파이낸스에서 IGE의 역사적 가격을 Excel 스프레드시트로 다운로드할 수 있다):

날짜시가고가저가종가
6/10/200573.9874.0873.3174
6/9/200572.4573.7472.2373.74
6/8/2005144.13146.44143.75144.48
6/7/2005145146.07144.11144.11

이 분할로 인해 6/9/2005 이전 가격을 조정해야 한다. 간단하다: 여기서 N = 2이므로 그 가격들을 ½로 곱하기만 하면 된다. 다음 표는 조정된 가격을 보여준다:

날짜시가고가저가종가
6/10/200573.9874.0873.3174
6/9/200572.4573.7472.2373.74
6/8/200572.06573.2271.87572.24
6/7/200572.573.03572.05572.055

이제 눈치 빠른 독자는 여기서 조정된 종가가 야후 파이낸스 표에 표시된 조정 종가와 일치하지 않는다는 것을 알아챌 것이다. 그 이유는 6/9/2005 이후에도 배당이 지급되었기 때문에 야후는 그 배당들도 모두 조정했기 때문이다. 각 조정이 배수이므로 종합 조정 배수는 개별 배수 모두의 곱이다. 다음은 6/9/2005부터 2007년 11월까지의 배당과 전 거래일의 비수정 종가, 그리고 각각의 배수다:

기준일배당전일 종가배수
9/26/20070.177128.080.998618
6/29/20070.3119.440.997488
12/21/20060.322102.610.996862
9/27/20060.25891.530.997181
6/23/20060.3292.20.996529
3/27/20060.25394.790.997331
12/23/20050.23689.870.997374
9/26/20050.184890.997933
6/21/20050.21777.90.997214

배당에 대한 종합 배수는 단순히 0.998618 × 0.997488 × ··· × 0.997214 = 0.976773이다. 이 배수는 6/9/2005 이전 비수정 가격 모두에 적용되어야 한다. 배당과 분할을 합친 종합 배수는 0.976773 × 0.5 = 0.488386이며, 이는 6/9/2005 이전의 모든 비수정 가격에 적용되어야 한다. 이를 적용한 결과는 다음과 같다:

날짜시가고가저가종가
6/10/200572.2616372.3593171.607272.28117
6/9/200570.7671772.0272170.5522872.02721
6/8/200570.3911171.5192970.2055370.56205
6/7/200570.8160171.3385870.3813570.38135

이제 2007년 11월 1일경 야후 데이터와 비교해보자:

날짜시가고가저가종가거래량수정 종가
6/10/200573.9874.0873.317417930072.28
6/9/200572.4573.7472.2373.7485320072.03
6/8/2005144.13146.44143.75144.4810960070.56
6/7/2005145146.07144.11144.115800070.38

우리의 계산에서 조정된 종가와 야후의 조정 종가가 소수점 둘째 자리까지 반올림하면 동일하다는 것을 확인할 수 있다. 물론 이 글을 읽을 때쯤이면 IGE는 더 많은 배당을 지급했거나 추가 분할이 있었을 수 있어 야후 표가 위와 달라 보일 것이다. 지금의 야후 표와 동일한 조정 가격이 나오도록 해당 배당과 분할을 추가로 조정하는 것은 좋은 연습이 된다.


데이터는 생존자 편향에서 자유로운가?

이 문제는 이미 2장에서 다뤘다. 안타깝게도 생존자 편향이 없는 데이터베이스는 상당히 비싸서 스타트업에게 감당하기 어려울 수 있다. 이 문제를 해결하는 한 가지 방법은 미래의 백테스트를 위해 직접 포인트-인-타임(point-in-time) 데이터를 수집하기 시작하는 것이다. 매일 자신의 종목 우주에 속한 모든 주식의 가격을 파일에 저장해두면, 미래에 사용할 포인트-인-타임 또는 생존자 편향 없는 데이터베이스를 갖게 된다. 생존자 편향의 영향을 줄이는 또 다른 방법은 더 최근 데이터에서만 전략을 백테스트해서 누락된 주식이 너무 많아 결과가 왜곡되지 않도록 하는 것이다.


예시 3.3: 생존자 편향이 전략 성과를 인위적으로 부풀리는 방법

여기 “저가 주식 매수” 장난감 전략이 있다(경고: 이 장난감 전략은 재정 건강에 해롭다!). 시가총액 기준 1,000대 주식 우주에서, 연초에 종가가 가장 낮은 10개 주식을 선택해서 동등한 초기 자본으로 1년간 보유한다고 해보자. 생존자 편향이 없는 좋은 데이터베이스가 있었다면 어떤 주식을 선택했을지 살펴보자:

종목2001/1/2 종가2002/1/2 종가최종 가격
ETYS0.2188NaN0.125
MDM0.31250.490.49
INTW0.4063NaN0.11
FDHG0.5NaN0.33
OGNC0.6875NaN0.2
MPLX0.7188NaN0.8
GTS0.75NaN0.35
BUYX0.75NaN0.17
PSIX0.75NaN0.2188

MDM을 제외한 모든 종목이 2001/1/2와 2002/1/2 사이에 어느 시점에 상장 폐지되었다(닷컴 버블이 심각하게 터지던 시기였으니!). NaN은 2002/1/2 기준의 종가가 존재하지 않는 주식을 나타낸다. ‘최종 가격’ 열은 주식이 2002/1/2 이전에 마지막으로 거래된 가격을 나타낸다. 그 해 이 포트폴리오의 총 수익은 **–42%**였다.

이제 데이터베이스에 생존자 편향이 있어서 그 해 상장 폐지된 주식들을 실제로 모두 누락했다면 어떤 주식을 선택했을지 살펴보자. 그러면 다음 목록을 대신 선택했을 것이다:

종목2001/1/2 종가2002/1/2 종가
MDM0.31250.49
ENGA0.84380.44
NEOF0.87527.9
ENP0.8750.05
MVL0.95832.5
URBN1.01563.0688
FNV1.06250.81
APT1.1250.88
FLIR1.28139.475
RAZF1.34380.25

2002/1/2까지 “살아남은” 주식들만 선택한 것에 주목하라. 그 날 모두 종가가 있다. 이 포트폴리오의 총 수익은 **388%**였다!

이 예시에서 –42%는 트레이더가 이 전략을 따랐을 때 실제로 경험했을 수익이고, 388%는 데이터베이스의 생존자 편향으로 인한 허구의 수익이다.


전략이 고가·저가 데이터를 사용하는가?

거의 모든 일별 주식 데이터에서 고가와 저가는 시가와 종가보다 훨씬 노이즈가 많다. 이것이 의미하는 바는, 하루의 기록된 고가보다 낮은 가격에 지정가 매수 주문을 제출했더라도 체결되지 않을 수 있고, 매도 지정가 주문도 마찬가지라는 것이다. (이는 해당 고가에서 거래가 매우 소량이었거나, 주문이 라우팅되지 않은 시장에서 체결이 발생했기 때문일 수 있다. 때로는 고가 또는 저가가 필터링되지 않은 잘못 보고된 틱(tick) 때문이기도 하다.) 따라서 고가·저가 데이터에 의존하는 백테스트는 시가·종가에 의존하는 것보다 신뢰성이 낮다.

사실, 시가 주문(Market On Open, MOO)이나 종가 주문(Market On Close, MOC)조차도 데이터에 표시된 역사적 시가·종가로 체결되지 않을 수 있다. 이는 표시된 역사적 가격이 주요 거래소(예: 뉴욕증권거래소[NYSE])의 가격일 수도 있고, 모든 지역 거래소를 포함한 종합 가격일 수도 있기 때문이다. 주문이 어디로 라우팅되었느냐에 따라 데이터셋에 표시된 역사적 시가·종가와 다른 가격으로 체결될 수 있다. 그럼에도 시가·종가의 불일치는 보통 고가·저가 가격의 오류보다 백테스트 성과에 덜 영향을 미친다. 고가·저가는 거의 항상 백테스트 수익을 과대평가하기 때문이다.

데이터베이스에서 데이터를 가져온 후에는 간단한 오류 검사를 하는 것이 좋다. 가장 간단한 방법은 데이터를 기반으로 일별 수익률을 계산하는 것이다. 시가·고가·저가·종가가 있다면 전일 고가에서 당일 종가까지 등 다양한 일별 수익률 조합도 계산할 수 있다. 그런 다음 평균에서 4 표준편차 이상 떨어진 수익률이 있는 날들을 면밀히 살펴봐야 한다. 일반적으로 극단적인 수익률은 뉴스 발표와 함께 나타나거나, 시장 지수도 극단적인 수익률을 보인 날에 발생해야 한다. 그렇지 않다면 데이터를 의심해야 한다.


성과 측정

퀀트 트레이더들은 다양한 성과 측정 지표를 사용한다. 어떤 지표를 사용할지는 때로 개인적인 선호의 문제이지만, 서로 다른 전략과 트레이더들 간의 비교 편의성을 고려하면 샤프 비율과 낙폭이 가장 중요한 두 가지라고 주장하고 싶다. 투자자들이 가장 일반적으로 인용하는 지표인 평균 연환산 수익률을 포함하지 않은 것에 주목하라. 이 지표를 사용하면 수익률을 계산하는 데 어떤 분모를 사용했는지에 대해 여러 가지를 설명해야 한다. 예를 들어, 롱-숏 전략에서 자본의 한쪽 면만 분모로 사용했는가, 아니면 양쪽 면을 모두 사용했는가? 수익은 레버리지가 적용된 것인가(분모가 계좌 자기자본에 기반), 아니면 레버리지가 없는 것인가(분모가 포트폴리오의 시장 가치에 기반)? 자기자본이나 시장 가치가 매일 변한다면, 이동 평균을 분모로 사용하는가, 아니면 매일 또는 매월 말의 값을 사용하는가? 이러한 문제들의 대부분(전부는 아니지만)은 샤프 비율과 낙폭을 표준 성과 측정 지표로 인용함으로써 피할 수 있다.

샤프 비율, 최대 낙폭, 최대 낙폭 기간의 개념은 2장에서 소개했다. 여기서는 샤프 비율 계산과 관련된 여러 미묘한 점을 언급하고, Excel과 MATLAB에서 계산 예시를 제공한다.

샤프 비율을 계산할 때 경험 많은 포트폴리오 매니저들조차 종종 헷갈리는 미묘한 점이 하나 있다: 달러 중립 포트폴리오의 수익에서 무위험 수익률을 차감해야 하는가 하지 않아야 하는가? 답은 아니다(차감하지 않는다). 달러 중립 포트폴리오는 자기자금 조달(self-financing) 방식이다. 공매도로 얻은 현금이 롱 포지션 매수 비용을 충당하므로, 금융 비용(차입 이자율과 대출 이자율 사이의 스프레드)이 작아서 많은 백테스트 목적에서 무시할 수 있다. 한편, 유지해야 하는 증거금 잔액은 무위험 수익률 r_F에 가까운 이자를 받는다. 따라서 전략 수익(신용 이자 기여분을 제외한 포트폴리오 수익)을 R이라 하고 무위험 수익률을 r_F라 하면, 샤프 비율 계산에 사용되는 초과 수익은 R + r_Fr_F = R이다. 따라서 계산 전체에서 무위험 수익률을 무시하고 주식 포지션에서 발생하는 수익에만 집중해도 된다.

마찬가지로, 오버나이트 포지션을 보유하지 않는 롱 전용 데이트레이딩 전략이라면, 이 경우에도 금융 비용이 없으므로 전략 수익에서 무위험 수익률을 차감할 필요가 없다. 일반적으로 전략이 금융 비용을 발생시키는 경우에만 샤프 비율 계산 시 전략 수익에서 무위험 수익률을 차감해야 한다.

다양한 전략들 간의 비교를 더욱 쉽게 하기 위해 대부분의 트레이더들은 샤프 비율을 연환산한다. 평균 수익률을 연환산하는 방법은 대부분 알고 있다. 예를 들어, 월별 수익률을 사용해왔다면 연평균 수익률은 단순히 월평균 수익률의 12배다.

그러나 수익률의 표준편차를 연환산하는 것은 조금 더 까다롭다. 월별 수익률이 연속적으로 상관관계가 없다는 가정 하에(Sharpe, 1994), 연간 수익률의 표준편차는 월간 표준편차의 √12배다. 따라서 전반적으로 연환산 샤프 비율은 월간 샤프 비율의 √12배가 된다.

일반적으로, 특정 트레이딩 기간 T(월이든 일이든 시간이든)를 기반으로 수익의 평균과 표준편차를 계산하고, 이 수치들을 연환산하고 싶다면, 먼저 1년에 그런 트레이딩 기간이 몇 번 있는지(이를 N_T라 부른다) 파악해야 한다. 그러면:

$$\text{연환산 샤프 비율} = \sqrt{N_T} \times \text{기간 } T \text{ 기반 샤프 비율}$$


예를 들어, NYSE 시장 시간(오전 9:30–오후 4:00 ET)에만 포지션을 보유하는 전략에서 평균 시간당 수익이 R이고 시간당 수익의 표준편차가 s라면, 연환산 샤프 비율은 √1638 × R/s다. N_T = (252 거래일) × (하루 6.5시간) = 1,638이기 때문이다. (흔한 실수는 N_T를 252 × 24 = 6,048로 계산하는 것이다.)


예시 3.4: 롱 전용 vs 시장 중립 전략의 샤프 비율 계산

IGE에 대한 단순한 롱 전용 전략의 샤프 비율을 계산해보자: 2001년 11월 26일 종가에 매수해서 2007년 11월 14일 종가에 매도하는 전략이다. 이 기간 동안 평균 무위험 수익률은 연 4%라고 가정한다. Yahoo! Finance에서 날짜 범위를 지정해 일별 가격을 다운로드하고 Excel 파일(기본 콤마 구분 파일이 아닌)로 저장한다. IGE.xls라 부를 수 있다. 다음 단계는 Excel 또는 MATLAB으로 진행할 수 있다.

Excel 사용

  1. 다운로드된 파일에는 이미 A~G 열이 있어야 한다.
  2. 날짜의 오름차순으로 모든 열을 정렬한다(데이터-정렬 기능 사용, “선택 영역 확장” 버튼과 “오름차순” 및 “데이터에 머리글 행 있음” 버튼을 선택한다).
  3. H3 셀에 =(G3-G2)/G2를 입력한다. 이것이 일별 수익률이다.
  4. H3 셀 오른쪽 하단의 작은 검은 점을 더블클릭하면 H열 전체에 IGE의 일별 수익률이 채워진다.
  5. 명확성을 위해 헤더 셀 H1에 “Dailyret”을 입력할 수 있다.
  6. I3 셀에 =H3-0.04/252를 입력한다. 이것이 초과 일별 수익률로, 연 4% 무위험 수익률과 연간 252 거래일을 가정한다.
  7. I3 셀 오른쪽 하단의 작은 검은 점을 더블클릭하면 I열 전체에 초과 일별 수익률이 채워진다.
  8. 명확성을 위해 헤더 셀 I1에 “Excess Dailyret”을 입력한다.
  9. I1506 셀(다음 열의 마지막 행)에 =SQRT(252)*AVERAGE(I3:I1505)/STDEV(I3:I1505)를 입력한다.
  10. I1505 셀에 표시되는 값인 “0.789317538”이 이 매수-보유 전략의 샤프 비율이다.

완성된 스프레드시트는 epchan.com/book/example3_4.xls에서 다운로드할 수 있다.

MATLAB 사용

% 이전에 정의된 변수를 지운다.
clear;

% "IGE.xls" 스프레드시트를 MATLAB으로 읽어들인다.
[num, txt]=xlsread('IGE');

% 첫 번째 열(두 번째 행부터)에는 mm/dd/yyyy 형식의 거래일이 있다.
tday=txt(2:end, 1);

% 형식을 yyyymmdd로 변환한다.
tday=datestr(datenum(tday, 'mm/dd/yyyy'), 'yyyymmdd');
% 날짜 문자열을 먼저 셀 배열로, 그 다음 숫자 형식으로 변환한다.
tday=str2double(cellstr(tday));
% 마지막 열에는 수정 종가가 있다.
cls=num(:, end);
% tday를 오름차순으로 정렬한다.
[tday sortIndex]=sort(tday, 'ascend');
% cls를 날짜 오름차순으로 정렬한다.
cls=cls(sortIndex);
% 일별 수익률
dailyret=(cls(2:end)-cls(1:end-1))./cls(1:end-1);
% 연 4% 무위험 수익률과 연간 252 거래일을 가정한 초과 일별 수익률
excessRet=dailyret - 0.04/252;
% 출력값은 0.7893이어야 한다.
sharpeRatio=sqrt(252)*mean(excessRet)/std(excessRet)

이 MATLAB 코드는 epchan.com/book/example3_4.m에서도 다운로드할 수 있다.

이제 롱-숏 시장 중립 전략의 샤프 비율을 계산해보자. 사실, 위 매수-보유 전략의 매우 단순한 변형이다: IGE를 매수했을 때 헤지로서 S&P 500 예탁증서(SPY)를 동등한 달러 금액만큼 공매도하고, 두 포지션 모두 2007년 11월에 종료한다. Yahoo! Finance에서 SPY도 다운로드해서 SPY.xls로 저장한다. 위와 매우 유사한 단계를 Excel과 MATLAB 모두에서 수행할 수 있으며, 정확한 단계는 독자에게 연습으로 남긴다.

Excel 사용 (헤지 전략)

  1. SPY.xls의 열을 날짜 오름차순으로 정렬한다.
  2. SPY.xls의 G열(수정 종가)을 복사해서 IGE.xls의 J열에 붙여넣는다.
  3. J열이 A~I열과 같은 행 수를 가지는지 확인한다. 그렇지 않다면 Yahoo!에서 같은 날짜 범위를 다운로드했는지 확인한다.
  4. 위와 동일한 단계를 수행해서 K열에 일별 수익률을 계산한다.
  5. 명확성을 위해 K열 헤더에 “dailyretSPY”를 입력한다.
  6. L열에서 헤지 전략의 순 수익률을 H열과 K열의 차이를 2로 나눈 값으로 계산한다. (자본이 두 배가 되므로 2로 나눈다.)
  7. L1506 셀에서 이 헤지 전략의 샤프 비율을 계산한다. “0.783681”이 나와야 한다.

MATLAB 사용 (헤지 전략)

% 위 MATLAB 코드의 연속이라고 가정한다.

% 위와 같이 SPY.xls에서 데이터를 가져오는 코드를 삽입한다.
% SPY의 일별 수익률을 담은 배열을 "dailyretSPY"라 명명한다.

% 순 일별 수익률 (자본이 두 배이므로 2로 나눈다)
netRet=(dailyret - dailyretSPY)/2;

% 출력값은 0.7837이어야 한다.
sharpeRatio=sqrt(252)*mean(excessRet)/std(excessRet)

예시 3.5: 최대 낙폭과 최대 낙폭 기간 계산

위 롱-숏 시장 중립 예시에 이어서 최대 낙폭과 최대 낙폭 기간 계산을 설명한다. 이 계산의 첫 단계는 각 거래일 종가 기준의 “고점(high watermark)“을 계산하는 것이다. 이것은 그 시점까지의 전략 누적 복리 수익률이다. (누적 수익 곡선으로 고점과 낙폭을 계산하는 것은 자산 곡선을 사용하는 것과 동등하다. 자산은 초기 투자금에 1 + 누적 수익률을 곱한 것에 불과하기 때문이다.) 고점을 알면 낙폭, 최대 낙폭, 최대 낙폭 기간을 계산할 수 있다.

Excel 사용

  1. M3 셀에 =L3을 입력한다.
  2. M4 셀에 =(1+M3)*(1+L4)-1을 입력한다. 이것이 그 날까지 전략의 누적 복리 수익률이다. M열 전체를 이 누적 복리 수익률로 채우고 마지막 셀은 지운다. 이 열을 Cumret이라 명명한다.
  3. N3 셀에 =M3을 입력한다.
  4. N4 셀에 =MAX(N3, M4)를 입력한다. 이것이 그 날까지의 고점이다. N열 전체를 전략의 누적 고점으로 채우고 마지막 셀은 지운다. 이 열을 High watermark라 명명한다.
  5. O3 셀에 =(1+N3)/(1+M3)-1을 입력한다. 이것이 그 날 종가 기준 낙폭이다. O열 전체를 전략의 낙폭으로 채운다.
  6. O1506 셀에 =MAX(O3:O1505)를 입력한다. 이것이 전략의 최대 낙폭이다. 약 0.1053, 즉 최대 낙폭 10.53%가 나와야 한다.
  7. P3 셀에 =IF(O3=0, 0, P2+1)을 입력한다. 이것이 현재 낙폭 기간이다. P열 전체를 전략의 낙폭 기간으로 채우고 마지막 셀은 지운다.
  8. P1506 셀에 =MAX(P3:P1505)를 입력한다. 이것이 전략의 최대 낙폭 기간이다. 497, 즉 최대 낙폭 기간 497 거래일이 나와야 한다.

MATLAB 사용

% 위 MATLAB 코드의 연속이라고 가정한다.
% 누적 복리 수익률
cumret=cumprod(1+netRet)-1; plot(cumret);

[maxDrawdown maxDrawdownDuration]=...
    calculateMaxDD(cumret);

% 최대 낙폭. 출력값은 0.1053이어야 한다.
maxDrawdown
% 최대 낙폭 기간. 출력값은 497이어야 한다.
maxDrawdownDuration

위 코드에서 calculateMaxDD라는 함수를 호출한다. 해당 함수는 다음과 같다:

function [maxDD maxDDD]=calculateMaxDD(cumret)
% [maxDD maxDDD]=calculateMaxDD(cumret)
% 누적 복리 수익률을 기반으로 최대 낙폭과 최대 낙폭 기간을 계산한다.

% 고점을 0으로 초기화한다.
highwatermark=zeros(size(cumret));
% 낙폭을 0으로 초기화한다.
drawdown=zeros(size(cumret));
% 낙폭 기간을 0으로 초기화한다.
drawdownduration=zeros(size(cumret));
for t=2:length(cumret)
    highwatermark(t)=...
        max(highwatermark(t-1), cumret(t));
    % 각 날의 낙폭
    drawdown(t)=(1+highwatermark(t))/(1+cumret(t))-1;
    if (drawdown(t)==0)
        drawdownduration(t)=0;
    else
        drawdownduration(t)=drawdownduration(t-1)+1;
    end
end

maxDD=max(drawdown); % 최대 낙폭

% 최대 낙폭 기간
maxDDD=max(drawdownduration);

이 함수가 포함된 파일은 epchan.com/book/calculateMaxDD.m에서 다운로드할 수 있다. 그림 3.1의 누적 수익 그래프에서 최대 낙폭과 최대 낙폭 기간이 발생한 위치를 확인할 수 있다.

그림 3.1 — 예시 3.4의 최대 낙폭 및 최대 낙폭 기간

(누적 수익 곡선. 약 800일 부근에서 497일간의 최대 낙폭 기간이 나타나며, 약 1,200일 이후 구간에서 10.53% 최대 낙폭이 발생함.)


피해야 할 일반적인 백테스팅 함정

백테스팅은 그 시점에 이용 가능했던 역사적 정보를 바탕으로 역사적 거래를 생성하고, 이어서 그 거래들의 후속 성과를 파악하는 과정이다. 우리의 경우 컴퓨터 알고리즘을 사용해서 거래가 이루어졌기 때문에 이 과정은 쉬워 보이지만, 잘못될 수 있는 방법이 수없이 많다. 보통 잘못된 백테스트는 실제 거래에서 얻었을 것보다 더 좋은 역사적 성과를 산출한다. 백테스팅에 사용된 데이터의 생존자 편향이 어떻게 과대평가된 성과를 낳을 수 있는지는 이미 살펴봤다. 백테스트 프로그램이 작성되는 방식, 또는 더 근본적으로는 트레이딩 전략을 구성하는 방식과 관련된 다른 일반적인 함정들도 있다. 여기서 가장 일반적인 두 가지를 설명하고 피하는 방법을 알아본다.

선행 편향(Look-Ahead Bias)

이 오류는 거래가 이루어진 시점보다 앞선 시간에만 이용 가능했던 정보를 사용하는 상황을 말한다. 예를 들어, 트레이드 진입 규칙이 “당일 저가의 1% 이내에서 매수”라면 전략에 선행 편향을 도입한 것이다. 시장이 그날 마감될 때까지 당일 저가가 무엇인지 알 수 없기 때문이다. 또 다른 예: 두 가격 시계열의 선형 회귀 모델이 포함된다고 가정해보자. 전체 데이터셋에서 얻은 회귀 계수를 사용해서 일별 트레이딩 신호를 결정한다면, 선행 편향을 도입한 것이다.

선행 편향을 어떻게 피할 수 있을까? 신호를 계산하는 모든 기회에 지연된(lagged) 역사적 데이터를 사용하면 된다. 데이터 시계열을 지연시킨다는 것은 이동 평균, 고가·저가, 또는 거래량 같은 모든 수치를 이전 거래 기간의 종가까지의 데이터만을 기반으로 계산한다는 것을 의미한다. (물론 전략이 그 기간의 종가에서만 진입한다면 데이터를 지연시킬 필요가 없다.)

선행 편향은 MATLAB을 사용할 때보다 Excel이나 다른 WYSIWYG 프로그램을 사용할 때 피하기 더 쉽다. Excel에서는 모든 데이터 열을 쉽게 정렬해서 각 셀의 수식이 현재 행 위의 행들을 기반으로 계산되는지 확인할 수 있기 때문이다. Excel의 셀 하이라이팅 기능 덕분에 신호를 생성할 때 당일 데이터를 사용하는 경우가 시각적으로 명확하게 드러난다. (수식이 있는 셀을 더블클릭하면 그 수식이 참조하는 셀들이 하이라이트된다.) MATLAB을 사용할 때는 더 주의해야 하며 신호 생성에 사용되는 특정 시계열에 지연 함수를 실행하는 것을 기억해야 한다.

선행 편향 없는 백테스트 프로그램을 만들기 위해 모든 주의를 기울이더라도, 때로는 일부가 스며들 수 있다. 일부 선행 편향은 성격상 꽤 미묘하며, 특히 MATLAB을 사용할 때 피하기 쉽지 않다.

MATLAB 백테스트 프로그램의 최종 점검을 이 방법으로 하는 것이 가장 좋다: 모든 역사적 데이터를 사용해서 프로그램을 실행하고, 결과 포지션 파일(프로그램이 매일 생성하는 모든 권장 포지션을 담은 파일)을 파일 A에 저장한다. 이제 역사적 데이터를 잘라내어 가장 최근 부분(예: N일)을 제거한다. 원래 데이터의 마지막 날이 T라면, 잘라낸 데이터의 마지막 날은 T–N이어야 한다. N은 10일에서 100일이 될 수 있다. 이제 잘라낸 데이터를 사용해서 백테스트 프로그램을 다시 실행하고 결과 포지션을 새로운 파일 B에 저장한다. 두 파일 모두 같은 행 수(일수)와 마지막 날 T–N이 되도록 포지션 파일 A의 가장 최근 N개 행을 잘라낸다. 마지막으로, A와 B의 포지션이 동일한지 확인한다. 동일하지 않다면 백테스트 프로그램에 선행 편향이 있는 것으로, 찾아서 수정해야 한다. 불일치하는 포지션은 역사적 데이터의 잘라낸 부분(파일 A의 날짜 T–N 이후 부분)을 T–N 이전 날의 포지션 결정에 무의식적으로 사용하고 있다는 의미다. 이 다소 복잡한 절차는 예시 3.6의 끝부분에서 설명한다.

데이터 스누핑 편향(Data-Snooping Bias)

2장에서 데이터 스누핑 편향을 언급했다—역사적 데이터의 일시적 노이즈에 기반해서 모델 파라미터를 과최적화했기 때문에 백테스트 성과가 전략의 미래 성과에 비해 과대평가되는 위험이다. 데이터 스누핑 편향은 역사적 데이터의 예측 통계 모델 사업에서 만연하지만, 보유한 독립 데이터의 양이 제한적이기 때문에 금융에서 특히 심각하다. 고빈도 데이터는 풍부하게 공급되지만, 고빈도 모델에만 유용하다. 20세기 초까지 거슬러 올라가는 주식 시장 데이터가 있지만, 예측 모델 구축에 실제로 적합한 데이터는 최근 10년치뿐이다. 게다가 2장에서 논의한 것처럼, 레짐 변화는 불과 몇 년 된 데이터도 백테스팅 목적으로는 쓸모없게 만들 수 있다. 독립 데이터가 적을수록 트레이딩 모델에서 조정 가능한 파라미터 수도 줄여야 한다.

경험 법칙으로, 진입·청산 임계값, 보유 기간, 또는 이동 평균 계산의 룩백 기간 같은 수치를 포함해서 파라미터를 5개 이상 사용하지 않는다. 게다가, 모든 데이터 스누핑 편향이 파라미터 최적화에 기인하는 것은 아니다. 트레이딩 모델을 만들 때 내리는 수많은 선택들—시가에 진입할지 종가에 진입할지, 오버나이트 포지션을 보유할지, 대형주를 거래할지 중형주를 거래할지—이 동일한 데이터셋으로 반복 백테스팅되어 영향을 받을 수 있다. 이러한 정성적 결정들은 종종 백테스트 성과를 최적화하기 위해 이루어지지만, 앞으로도 최적이 아닐 수 있다. 데이터 기반 모델을 구축하는 한 데이터 스누핑 편향을 완전히 제거하는 것은 거의 불가능하다. 그러나 편향을 줄이는 방법들이 있다.

샘플 크기 — 데이터 스누핑 편향에 대한 가장 기본적인 안전장치는 최적화하려는 자유 파라미터 수에 비해 충분한 백테스트 데이터를 확보하는 것이다. 경험 법칙으로, 파라미터 최적화에 필요한 데이터 포인트 수는 모델의 자유 파라미터 수의 252배라고 가정한다. (이 비례 가정은 방대한 통계 문헌의 조사에 근거한 것이 아니라 순전히 경험에 기반한다.) 예를 들어, 파라미터가 3개인 일별 트레이딩 모델이 있다면 최소한 3년치 일별 가격 백테스트 데이터가 있어야 한다. 그러나 포지션을 매 분마다 업데이트하는 3-파라미터 트레이딩 모델이 있다면 최소 252/390년, 즉 약 7개월치의 분봉 백테스트 데이터가 있어야 한다. (7개월치의 분봉 데이터 포인트가 있더라도, 실질적으로는 약 7 × 21 = 147개의 데이터 포인트만 있는 것으로, 3-파라미터 모델 테스트에는 턱없이 부족하다.)

표본 외 테스트(Out-of-Sample Testing) — 역사적 데이터를 두 부분으로 나눈다. 두 번째(더 최근의) 부분을 표본 외 테스트를 위해 남겨둔다. 모델을 구축할 때 첫 번째 부분(훈련 세트라 함)에서 파라미터와 다른 정성적 결정들을 최적화하되, 결과 모델은 두 번째 부분(테스트 세트라 함)에서 테스트한다. (두 부분의 크기는 대략 같아야 하지만, 훈련 데이터가 부족하다면 테스트 데이터의 최소 3분의 1만큼의 훈련 데이터라도 있어야 한다. 훈련 세트의 최소 크기는 앞 섹션의 경험 법칙으로 결정된다.) 이상적으로는 백테스트 기간의 첫 번째 부분에서 최적 파라미터와 결정 세트가 두 번째 기간에서도 최적이어야 하지만, 이렇게 완벽한 경우는 드물다. 데이터의 두 번째 부분에서의 성과는 최소한 합리적이어야 한다. 그렇지 않다면 모델에 데이터 스누핑 편향이 내재되어 있는 것으로, 이를 해결하는 한 방법은 모델을 단순화하고 일부 파라미터를 제거하는 것이다.

더 엄격한(비록 계산 집약적이지만) 표본 외 테스트 방법은 파라미터의 이동 최적화(moving optimization)를 사용하는 것이다. 이 경우 파라미터 자체가 변화하는 역사적 데이터에 지속적으로 적응하며, 파라미터에 대한 데이터 스누핑 편향은 제거된다. (파라미터 없는 트레이딩 모델에 관한 사이드바 참조.)


사이드바: 파라미터 없는 트레이딩 모델

과거에 함께 일했던 한 포트폴리오 매니저는 자신의 트레이딩 모델에 “자유 파라미터가 없다”고 자랑스럽게 주장했다. 업계의 비밀주의 전통에 따라 더 이상 자신의 기법을 공개하지 않으려 했다.

최근 들어 자유 파라미터가 없는 트레이딩 모델이 무엇을 의미하는지 이해하기 시작했다. 트렌드 계산을 위한 룩백 기간이나 진입·청산 임계값 같은 것이 포함되지 않는다는 의미가 아니다. 그런 것은 불가능할 것이다. 그것은 단지 그런 모든 파라미터들이 이동 룩백 창(moving lookback window)에서 동적으로 최적화된다는 것을 의미한다. 이렇게 하면 트레이더가 “모델에 고정된 수익 한도가 있나요?”라고 물으면 솔직하게 대답할 수 있다: “아니요, 수익 한도는 입력 파라미터가 아닙니다. 모델 자체가 결정합니다.”

파라미터 없는 트레이딩 모델의 장점은 여러 입력 파라미터에 모델을 과적합(overfitting)하는 위험, 즉 이른바 “데이터 스누핑 편향”을 최소화한다는 것이다. 따라서 백테스트 성과가 실제 미래 성과에 훨씬 가까워야 한다.

(파라미터 최적화가 반드시 최상의 백테스트 성과를 주는 하나의 최적 파라미터 세트를 선택한다는 의미는 아니다. 다양한 파라미터 세트에 대한 일종의 평균값을 기반으로 트레이딩 결정을 내리는 것이 더 나을 때가 많다.)

이제 다음 주문이 오기 전에 이 모든 파라미터를 제때 최적화하는 것이 계산적으로 상당히 도전적이지만, 역사적 바(bar)마다 다차원 최적화를 수행해야 하는 백테스트에서 그렇게 하는 것은 더욱 어렵다. 결과적으로 저자는 7.1 예시에 설명된 레짐 전환 모델을 연구하기 전까지 파라미터 없는 모델을 거의 거래하지 않았다. 그 모델은 거의 파라미터가 없다(기술적 어려움이 아닌 시간 부족으로 최적화에서 몇 가지 파라미터를 뺐다). 이 경우 백테스트 파라미터 최적화를 몇 분 만에 수행할 수 있었던 것은 고급 백테스팅 플랫폼(Alphacet Discovery)을 사용했기 때문이다.


궁극적인 표본 외 테스트는 많은 트레이더들에게 익숙한 **모의 거래(paper trading)**다. 실제 보이지 않는 데이터로 모델을 실행하는 것이 진정으로 테스트하는 가장 신뢰할 수 있는 방법이다(실제로 거래하는 것을 제외하면). 모의 거래는 진정한 표본 외 테스트를 수행하게 해줄 뿐만 아니라, 종종 프로그램의 선행 편향 오류를 발견하게 해주고, 다양한 운영상의 문제들도 인식하게 해준다. 모의 거래는 5장에서 다룬다.

테스트하는 전략이 출판된 소스에서 온 것이고, 결과가 올바른지 확인하기 위해 백테스트를 수행하는 것이라면, 출판 시점부터 전략을 테스트한 시점까지의 전체 기간이 진정한 표본 외 기간이다. 출판된 모델의 파라미터를 표본 외 기간에 최적화하지 않는 한, 이 기간은 전략을 모의 거래하는 것과 마찬가지다.


예시 3.6: GLD와 GDX의 페어 트레이딩

이 예시는 데이터를 훈련 세트와 테스트 세트로 나누는 방법을 보여준다. GLD와 GDX의 페어 트레이딩 전략을 백테스트하고 훈련 세트에서 파라미터를 최적화한 뒤 테스트 세트에서의 효과를 살펴볼 것이다.

GLD vs GDX는 페어 트레이딩의 좋은 후보다. GLD는 금 현물 가격을 반영하고, GDX는 금광 주식 바스켓이기 때문이다. 두 가격이 연동해서 움직이는 것은 직관적으로 이해된다. 공적분 분석과의 연관성에 대해서는 블로그에서 광범위하게 다뤘다. 여기서는 훈련 세트에서의 공적분 분석은 7장으로 미루고, 대신 훈련 세트에서 GLD와 GDX 간의 헤지 비율을 결정하는 회귀 분석을 수행하고, 페어 트레이딩 전략의 진입·청산 임계값을 정의한다. 훈련 세트에서 이 임계값들을 최적화하면 테스트 세트의 성과가 어떻게 달라지는지 살펴볼 것이다. (이 프로그램은 epchan.com/book/example3_6.m으로 다운로드 가능하다. 데이터 파일은 GDX.xls와 GLD.xls로 제공된다. 이 프로그램은 시계열을 1 기간 지연시키는 lag1 함수를 사용한다. 이 함수도 epchan.com/book에서 제공된다. 선형 회귀를 위한 ols 함수도 사용하는데, 이것은 spatial-econometrics.com에서 다운로드할 수 있는 무료 패키지의 일부다.)

MATLAB 사용

% 이전에 정의된 변수를 지운다.
clear;
% "GLD.xls" 스프레드시트를 MATLAB으로 읽어들인다.
[num, txt]=xlsread('GLD');
% 첫 번째 열(두 번째 행부터)은 mm/dd/yyyy 형식의 거래일이다.
tday1=txt(2:end, 1);

% 형식을 yyyymmdd로 변환한다.
tday1=...
    datestr(datenum(tday1, 'mm/dd/yyyy'), 'yyyymmdd');
% 날짜 문자열을 먼저 셀 배열로, 그 다음 숫자 형식으로 변환한다.
tday1=str2double(cellstr(tday1));
% 마지막 열에는 수정 종가가 있다.
adjcls1=num(:, end);
% "GDX.xls" 스프레드시트를 MATLAB으로 읽어들인다.
[num, txt]=xlsread('GDX');
% 첫 번째 열(두 번째 행부터)은 mm/dd/yyyy 형식의 거래일이다.
tday2=txt(2:end, 1);
% 형식을 yyyymmdd로 변환한다.
tday2=...
    datestr(datenum(tday2, 'mm/dd/yyyy'), 'yyyymmdd');

% 날짜 문자열을 먼저 셀 배열로, 그 다음 숫자 형식으로 변환한다.
tday2=str2double(cellstr(tday2));
% 마지막 열에는 수정 종가가 있다.
adjcls2=num(:, end);

% 두 데이터셋의 교집합을 찾아 오름차순으로 정렬한다.
[tday, idx1, idx2]=intersect(tday1, tday2);
cl1=adjcls1(idx1);
cl2=adjcls2(idx2);

trainset=1:252; % 훈련 세트 인덱스 정의

% 테스트 세트 인덱스 정의
testset=trainset(end)+1:length(tday);
% 훈련 세트에서 헤지 비율 결정
% 회귀 함수 사용
results=ols(cl1(trainset), cl2(trainset));
hedgeRatio=results.beta;

% 스프레드 = GLD - hedgeRatio*GDX
spread=cl1-hedgeRatio*cl2;
plot(spread(trainset));

figure;
plot(spread(testset));
figure;

% 훈련 세트에서 스프레드의 평균
spreadMean=mean(spread(trainset));
% 훈련 세트에서 스프레드의 표준편차
spreadStd=std(spread(trainset));
% 스프레드의 z-점수
zscore=(spread - spreadMean)./spreadStd;
% 스프레드가 2 표준편차 아래로 떨어지면 스프레드 매수
longs=zscore<=-2;

% 스프레드가 2 표준편차 위로 올라가면 스프레드 공매도
shorts=zscore>=2;
% 스프레드 포지션이 평균의 1 표준편차 이내로 회귀하면 청산
exits=abs(zscore)<=1;
% 포지션 배열 초기화
positions=NaN(length(tday), 2);

% 숏 진입: GLD 공매도(-1), GDX 매수(+1)
positions(shorts, :)=repmat([-1 1], [length(find(shorts)) 1]);
% 롱 진입: GLD 매수(+1), GDX 공매도(-1)
positions(longs,  :)=repmat([1 -1], [length(find(longs)) 1]);
% 청산: 포지션을 0으로 설정
positions(exits,  :)=zeros(length(find(exits)), 2);
% 청산 신호 없으면 기존 포지션 유지
positions=fillMissingData(positions);
cl=[cl1 cl2]; % 두 가격 시계열 결합

dailyret=(cl - lag1(cl))./lag1(cl);

pnl=sum(lag1(positions).*dailyret, 2);

% 훈련 세트의 샤프 비율은 약 2.3이어야 한다
sharpeTrainset=...
    sqrt(252)*mean(pnl(trainset(2:end))). ...
     /std(pnl(trainset(2:end)))

% 테스트 세트의 샤프 비율은 약 1.5이어야 한다
sharpeTestset=sqrt(252)*mean(pnl(testset)). ...
    /std(pnl(testset))
plot(cumsum(pnl(testset)));
% 선행 편향 검사를 위해 포지션 파일 저장
save example3_6_positions positions;

lag1.m 파일:

function y=lag1(x)
% y=lag(x)
if (isnumeric(x))
    % 첫 번째 항목을 NaN으로 채운다
    y=[NaN(1,size(x,2));x(1:end-1, :)];
elseif (ischar(x))
    % 첫 번째 항목을 " "으로 채운다
    y=[repmat(" ",[1 size(x,2)]);x(1:end-1, :)];
else
    error('Can only be numeric or char array');
end

이 페어 트레이딩 전략은 훈련 세트와 테스트 세트 모두에서 우수한 샤프 비율을 보인다(각각 약 2.3, 1.5). 따라서 이 전략은 데이터 스누핑 편향이 없다고 볼 수 있다. 그러나 개선의 여지가 있다. 진입 임계값을 1 표준편차로, 청산 임계값을 0.5 표준편차로 변경하면 훈련 세트 샤프 비율은 2.9로, 테스트 세트 샤프 비율은 2.1로 증가한다. 분명히 이 파라미터 세트가 더 낫다.

그러나 훈련 세트에서 파라미터를 최적화하면 테스트 세트의 성과가 감소하는 경우도 많다. 이런 상황에서는 훈련 세트와 테스트 세트 모두에서 좋은(최선은 아닐 수 있지만) 성과를 내는 파라미터 세트를 선택해야 한다.

이 분석에 거래비용은 포함하지 않았다. 이 전략은 매우 빈번하게 거래하지 않기 때문에 거래비용이 샤프 비율에 큰 영향을 주지는 않는다.

마지막으로 성공을 확정 짓기 전에 한 가지를 더 확인해야 한다: 백테스트 프로그램의 선행 편향 검사다. cl2=adjcls2(idx2); 라인 다음에 아래 코드를 추가한다:

% 잘라낼 최근 거래일 수
cutoff=60;
% 마지막 cutoff일을 제거한다
tday(end-cutoff+1:end, :)=[];
cl1(end-cutoff+1:end,  :)=[];
cl2(end-cutoff+1:end,  :)=[];

그리고 이전 MATLAB 프로그램 맨 끝의 save example3_6_positions positions 라인을 다음 코드로 교체한다:

% 선행 편향 검사 2단계
oldoutput=load('example3_6_positions');
oldoutput.positions(end-cutoff+1:end, :)=[];

if (any(positions~=oldoutput.positions))
    fprintf(1, 'Program has look-forward-bias!\n');
end

이 새 프로그램을 “example3_6_1.m”으로 저장하고 실행한다. “Program has look-forward-bias!” 메시지가 출력되지 않는다. 이는 알고리즘이 선행 편향 테스트를 통과했음을 의미한다.


민감도 분석 (Sensitivity Analysis)

파라미터와 모델의 다양한 특성을 최적화하고 테스트 세트 성과가 여전히 합리적임을 확인한 후에는, 이 파라미터들을 변경하거나 모델 특성을 소폭 수정해서 훈련 세트와 테스트 세트 양쪽에서 성과가 어떻게 변하는지 살펴봐야 한다. 최적 파라미터 세트 이외의 어떤 파라미터 세트도 용납할 수 없을 만큼 성과가 급락한다면, 그 모델은 데이터 스누핑 편향에 시달리고 있을 가능성이 높다.

특히 시도해볼 중요한 모델 변형이 있다: 모델을 단순화하는 다양한 방법들이다. 거래 여부를 결정하기 위해 정말로 다섯 가지 조건이 필요한가? 조건들을 하나씩 제거해가면서 훈련 세트의 성과가 용납할 수 없는 수준으로 저하되는 시점을 확인해보라. 더 중요한 것은: 조건들을 제거할 때 테스트 세트의 성과도 함께 감소하는가? 일반적으로 테스트 세트에서 유의미한 성과 하락 없이 조건·제약·파라미터를 가능한 한 많이 제거해야 한다. 비록 그 과정에서 훈련 세트 성과가 다소 줄어들더라도 말이다. (단, 테스트 세트 성과를 개선하기 위해 조건이나 파라미터를 추가하거나 값을 조정해서는 안 된다. 그렇게 하면 테스트 세트를 훈련 세트처럼 사용하는 것이 되어 데이터 스누핑 편향을 모델에 재도입하는 셈이다.)

거래를 촉발하는 파라미터와 조건의 집합을 최소한으로 줄이고, 소폭 변화가 표본 외 성과를 크게 바꾸지 않는다는 것을 확인한 후에는, 서로 다른 파라미터 값과 조건 세트에 걸쳐 거래 자본을 분산 투입하는 것을 고려해야 한다. 파라미터에 걸친 이러한 평균화는 실제 거래 성과가 백테스트 결과에서 지나치게 벗어나지 않도록 하는 데 도움이 된다.


거래비용

어떤 백테스트 성과도 거래비용을 반영하지 않으면 현실적이지 않다. 2장에서 다양한 유형의 거래비용(수수료, 유동성 비용, 기회비용, 시장 충격, 슬리피지)을 논의했다. 거래비용 추가 전에 높은 샤프 비율을 가진 전략이 비용 추가 후에 매우 비수익적으로 변할 수 있다는 점에 놀라서는 안 된다. 예시 3.7에서 이를 보여준다.


예시 3.7: 거래비용 반영 전·후의 단순 평균 회귀 모델

MIT의 Amir Khandani와 Andrew Lo가 개발한 단순 평균 회귀 전략이다. 전략은 매우 단순하다: 전일 일별 수익률이 가장 나쁜 주식들을 매수하고, 전일 수익률이 가장 좋은 주식들을 공매도한다. 단순함에도 불구하고 이 전략은 1995년 이후 거래비용 무시 시 우수한 성과를 보였다(2006년 샤프 비율 4.47). 여기서의 목표는 2006년에 거래당 표준 5bp 거래비용을 적용하면 어떻게 될지 확인하는 것이다. (거래는 매수 또는 공매도를 의미하며 왕복 거래가 아니다.)

이 예시는 거래비용의 영향을 보여줄 뿐 아니라, 여러 종목을 동시에 거래하는 전형적인 통계적 차익거래 모델을 백테스트하는 MATLAB의 강점도 보여준다. 수백 개 종목에 걸친 전략을 수년간 백테스트하는 것은 Excel로는 너무 번거롭다. 생존자 편향 없는 데이터는 비싸기 때문에 여기서는 이 문제를 뒤로 미루고, 구한 어떤 성과 추정치도 실제 전략 성과의 상한선임을 유념한다.

주식 선택 전략을 백테스트할 때 첫 번째 질문은: 어떤 주식 유니버스를 사용할 것인가? 일반적인 출발점은 가장 유동성 높은 주식 집합인 S&P 500 유니버스다. HQuote Pro 소프트웨어로 2000년 1월 1일부터 현재까지의 데이터를 가져온 뒤 Date, Open, High, Low, Close, Volume 열만 선택해서 헤더 없이 “Export.txt”로 내보낸다. 아래 MATLAB 프로그램으로 이 데이터를 파싱해서 “SPX_20071123.mat” 바이너리 파일로 저장한다:

clear;

inputFile='Export.txt';
outputFile='SPX_20071123';

[mysym, mytday, myop, myhi, mylo, mycl, myvol]=...
    textread(inputFile, '%s %u %f %f %f %f %u', ...
    'delimiter', ',');

% 단일 파일에 여러 종목이 포함되어 있으므로
% 고유한 종목 집합을 찾아야 한다.
stocks=unique(mysym);
% 단일 파일에 여러 종목의 날짜가 반복되므로
% 고유한 날짜 집합을 찾아야 한다.
tday=unique(mytday);

op=NaN(length(tday), length(stocks));
hi=NaN(length(tday), length(stocks));
lo=NaN(length(tday), length(stocks));
cl=NaN(length(tday), length(stocks));
vol=NaN(length(tday), length(stocks));

for s=1:length(stocks)
    stk=stocks{s};
    % 현재 종목의 데이터 위치(인덱스)를 찾는다.
    idxA=strmatch(stk, mysym, 'exact');
    % 현재 날짜 세트의 데이터 위치를 찾는다.
    [foo, idxtA, idxtB]=intersect(mytday(idxA), tday);
    % 다운로드된 데이터에서 현재 종목의 가격을 추출한다.
    op(idxtB, s)=myop(idxA(idxtA));
    hi(idxtB, s)=myhi(idxA(idxtA));
    lo(idxtB, s)=mylo(idxA(idxtA));
    cl(idxtB, s)=mycl(idxA(idxtA));
    vol(idxtB, s)=myvol(idxA(idxtA));
end

save(outputFile, 'tday', 'stocks', 'op', 'hi', ...
    'lo', 'cl', 'vol');

다음으로, 이 데이터셋을 사용해서 거래비용 없는 평균 회귀 전략을 백테스트한다:

clear;

startDate=20060101;
endDate=20061231;

load('SPX_20071123', 'tday', 'stocks', 'cl');

% 일별 수익률
dailyret=(cl-lag1(cl))./lag1(cl);
% 동일가중 시장 지수 수익률
marketDailyret=smartmean(dailyret, 2);
% 종목의 가중치는 시장 지수 대비 음의 거리에 비례한다
weights=...
    -(dailyret-repmat(marketDailyret,[1 size(dailyret,2)]))./ ...
    repmat(smartsum(isfinite(cl), 2), ...
    [1 size(dailyret,2)]);

% 유효 가격이나 일별 수익률이 없는 종목은 제외한다
weights(~isfinite(cl) | ~isfinite(lag1(cl)))=0;
dailypnl=smartsum(lag1(weights).*dailyret, 2);

% 관심 날짜 범위 밖의 손익 제거
dailypnl(tday < startDate | tday > endDate) = [];
% 샤프 비율은 약 0.25이어야 한다
sharpe=...
    sqrt(252)*smartmean(dailypnl, 1)/smartstd(dailypnl, 1)

2006년 샤프 비율이 원저자들의 4.47이 아닌 0.25에 불과하다는 점에 주목하라. 이렇게 크게 낮은 성과의 이유는 S&P 500 대형주 유니버스를 사용하기 때문이다. 원 논문을 읽어보면 대부분의 수익이 소형주와 마이크로캡 주식에서 나온다는 것을 알 수 있다.

이 프로그램에서 smartsum, smartmean, smartstd 세 가지 새로운 함수를 사용했다. 이것들은 통상적인 sum, mean, std 함수와 유사하지만 데이터의 NaN 항목을 건너뛴다. 주식 가격 데이터는 종종 시작과 끝이 있어 이러한 함수들이 백테스팅에 매우 유용하다.

% smartsum.m
function y = smartsum(x, dim)
% NaN을 무시하고 dim 방향으로 합산한다.
hasData=isfinite(x);
x(~hasData)=0;
y=sum(x,dim);
y(all(~hasData, dim))=NaN;

% smartmean.m
function y = smartmean(x, dim)
% NaN을 무시하고 dim 방향으로 평균을 구한다.
hasData=isfinite(x);
x(~hasData)=0;
y=sum(x,dim)./sum(hasData, dim);
y(all(~hasData, dim))=NaN; % 모든 항목이 NaN이면 NaN 설정

% smartstd.m
function y = smartstd(x, dim)
% NaN과 Inf를 무시하고 dim 방향으로 표준편차를 구한다.
hasData=isfinite(x);
x(~hasData)=0;
y=std(x);
y(all(~hasData, dim))=NaN;

이제 모든 거래에 5bp 거래비용을 공제하면 어떻게 되는지 살펴보자:

% 거래비용이 공제된 일별 손익
onewaytcost=0.0005; % 5 베이시스 포인트 가정

% 관심 날짜 범위 밖의 가중치를 제거한다
weights(tday < startDate | tday > endDate, :) = [];
% 거래비용은 가중치가 변할 때만 발생한다
dailypnlminuscost=...
    dailypnl - smartsum(abs(weights-lag1(weights)), 2).*...
    onewaytcost;

% 샤프 비율은 약 -3.19이어야 한다
sharpeminuscost=...
    sqrt(252)*smartmean(dailypnlminuscost, 1)/...
    smartstd(dailypnlminuscost, 1)

이 전략은 이제 매우 비수익적이다!


전략 개선

전략이 첫 번째 시도에서 뛰어난 백테스트 성과를 내지 못한다면, 이를 개선하는 일반적인 방법들이 있다. 데이터 스누핑 편향을 도입하지 않고 파라미터를 최소화하면서 전략을 다듬는 것은 과학이라기보다는 예술에 가깝다. 지침 원칙은 파라미터 최적화와 동일하다: 훈련 세트의 성과를 개선하기 위해 전략에 가하는 어떤 변경이든, 테스트 세트의 성과도 함께 개선되어야 한다.

종종 트레이더들 사이에 꽤 알려져 있고 여전히 어느 정도 수익성이 있지만 수익이 줄어들고 있는 매우 단순한 전략들이 있다. 예를 들면 주식 페어 트레이딩이 그렇다. 수익이 감소하는 이유는 너무 많은 트레이더들이 이 차익거래 기회를 이용해 이익 마진을 점차 없애고 있기 때문이다. 그러나 기본 전략에 소폭의 변형을 도입해서 수익을 높이는 것이 종종 가능하다.

이러한 소폭 변형들은 기본 전략보다 훨씬 덜 알려져 있어 트레이더들에게 덜 이용되고 있다. 때로는 특정 주식이나 주식 그룹을 유니버스에서 제외하는 것을 포함한다. 예를 들어 트레이더들은 뉴스의 극적인 영향 때문에 기술적 트레이딩 프로그램에서 제약회사 주식을 제외하거나, 인수합병 진행 중인 주식을 제외하기도 한다. 또 다른 변형은 거래의 진입·청산 시점이나 빈도를 바꾸는 것이다. 또 다른 변형은 주식 유니버스의 선택과 관련된다: 예시 3.7에서 S&P 500 대형주에 적용하면 샤프 비율이 매우 낮지만 소형주에 적용하면 크게 좋아지는 전략을 보았다.

전략에 이러한 개선을 도입할 때, 근거 없는 시행착오보다는 기본 경제학이나 잘 연구된 시장 현상에 근거를 두는 것이 바람직하다. 그렇지 않으면 데이터 스누핑 편향이 도사린다.


예시 3.8: 기존 전략의 소폭 변형

위 예시 3.7에서 설명한 평균 회귀 전략을 개선해보자. 이 전략의 2006년 샤프 비율은 거래비용 전 0.25로 평범하고, 거래비용 후 –3.19로 매우 비수익적이었다. 여기서 바꿀 것은 딱 하나다: 종가 대신 시가에 포지션을 업데이트하는 것이다. MATLAB 코드에서 clop로 전부 교체하기만 하면 된다.

놀랍게도, 거래비용 전 샤프 비율은 4.43으로 크게 오르고, 거래비용 후에도 수익성 있는 0.78로 증가한다! S&P 400 미드캡과 S&P 600 스몰캡 유니버스에서 전략을 테스트해서 샤프 비율을 더 개선하는 것은 독자에게 연습으로 남긴다.


요약

백테스팅은 전략의 성과에 대한 현실적인 역사적 시뮬레이션을 수행하는 것이다. 전략의 미래 성과가 과거 성과를 닮을 것이라는 희망이 있지만, 이것은 결코 보장되지 않는다!

현실적인 역사적 백테스트를 만들고 미래 성과와 백테스트 성과의 괴리를 줄이는 데는 수많은 세부 사항이 관여된다. 이 챕터에서 논의한 핵심 사항들:

  • 데이터: 분할/배당 수정, 일별 고가/저가의 노이즈, 생존자 편향
  • 성과 측정: 연환산 샤프 비율과 최대 낙폭
  • 선행 편향: 과거 거래 결정에 얻을 수 없었던 미래 정보를 사용하는 것
  • 데이터 스누핑 편향: 너무 많은 파라미터로 역사적 데이터를 과적합하는 것, 그리고 이를 충분한 샘플 크기·표본 외 테스트·민감도 분석으로 피하는 방법
  • 거래비용: 성과에 대한 거래비용의 영향
  • 전략 개선: 성과를 최적화하기 위한 전략의 소폭 변형 방법

이 챕터를 읽고 예시와 연습문제들을 풀면서, Excel이나 MATLAB으로 역사적 데이터를 가져오고 전략을 백테스트하는 방법에 대한 실전 경험을 쌓았을 것이다.

전략 테스트를 시작할 때 시간과 기타 자원의 제약으로 이러한 함정들을 모두 피하는 것이 불가능할 수 있다. 이 경우 전략에 잠재력이 있는지 빠르게 파악하기 위해 일부 주의사항을 건너뛰어도 된다. 때로는 가장 철저하고 신중한 백테스트조차 몇 달간의 모의 거래나 실거래 후에야 명백해질 문제들을 드러내지 못한다. 모델이 실거래에 들어간 후에도 언제든지 이러한 문제들로 돌아올 수 있다.

합리적인 성과의 전략을 백테스트했다면, 이제 트레이딩 비즈니스 설정의 다음 단계를 밟을 준비가 된 것이다.