<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://quant-note.com/feed.xml" rel="self" type="application/atom+xml" /><link href="https://quant-note.com/" rel="alternate" type="text/html" /><updated>2026-09-05T21:23:52+09:00</updated><id>https://quant-note.com/feed.xml</id><title type="html">퀀트 노트</title><subtitle>퀀트 투자, 백테스트, 파이썬 데이터 분석을 기록하는 블로그입니다.</subtitle><author><name>퀀트 노트</name></author><entry><title type="html">퀀트 투자 입문 ⑩ — 생존 편향, 사라진 종목들이 만드는 착시</title><link href="https://quant-note.com/posts/survivorship-bias/" rel="alternate" type="text/html" title="퀀트 투자 입문 ⑩ — 생존 편향, 사라진 종목들이 만드는 착시" /><published>2026-09-05T00:00:00+09:00</published><updated>2026-09-05T00:00:00+09:00</updated><id>https://quant-note.com/posts/survivorship-bias</id><content type="html" xml:base="https://quant-note.com/posts/survivorship-bias/"><![CDATA[<p>지난 글에서 “지금의 지수 구성종목으로 과거를 백테스트하면 안 된다”고 했습니다. 그 문제의 뿌리가 오늘 다룰 <strong>생존 편향(survivorship bias)</strong>입니다. 백테스트를 망가뜨리는 편향 중 가장 조용하고, 무료 데이터로 작업하는 개인 투자자가 가장 피하기 어려운 것이기도 합니다.</p>

<h2 id="돌아온-폭격기의-총알-자국">돌아온 폭격기의 총알 자국</h2>

<p>생존 편향을 설명할 때 빠지지 않는 이야기가 있습니다. 2차 세계대전 때 미군은 귀환한 폭격기의 총알 자국을 조사해 장갑을 보강할 위치를 정하려 했습니다. 날개와 동체에 자국이 집중되어 있었으니 거기를 보강하자는 것이 자연스러운 결론이었죠. 통계학자 에이브러햄 왈드는 반대로 답했습니다. <strong>자국이 없는 곳, 즉 엔진과 조종석을 보강해야 한다</strong>고요. 그곳에 맞은 비행기는 돌아오지 못했기 때문에 데이터에 없었던 겁니다.</p>

<p>백테스트에도 똑같은 일이 벌어집니다. <strong>지금 데이터에 남아 있는 종목은 돌아온 폭격기</strong>입니다. 상장폐지되어 사라진 종목은 데이터에 없고, 그래서 우리는 엔진에 맞은 비행기가 얼마나 많았는지 모릅니다.</p>

<h2 id="백테스트에서-생기는-방식">백테스트에서 생기는 방식</h2>

<p>가장 흔한 경로는 이렇습니다. “지금 상장된 전 종목 목록”을 받아서, 그 종목들의 10년 치 가격을 내려받고, 그 위에서 전략을 돌립니다. 지난 글에서 다룬 FinanceDataReader의 <code class="language-plaintext highlighter-rouge">StockListing('KRX')</code>가 정확히 이런 목록을 줍니다. 편리하지만, 이 목록에는 <strong>지난 10년 사이 상장폐지된 종목이 한 개도 없습니다.</strong></p>

<p>그 결과 백테스트의 유니버스는 “10년을 살아남은 종목들”로만 구성됩니다. 부도난 회사, 감사의견 거절로 퇴출된 회사, 다른 회사에 흡수된 회사는 모두 빠져 있습니다. 10년 전 시점에서 보면, 앞으로 망하지 않을 종목만 골라서 투자한 셈입니다. 성과가 좋을 수밖에 없습니다.</p>

<p>특히 심하게 왜곡되는 전략이 있습니다.</p>

<ul>
  <li><strong>밸류 전략</strong> — PBR이 낮은 종목은 “싸서 좋은 회사”와 “망해가는 회사”가 섞여 있습니다. 후자가 데이터에서 사라지면 밸류 전략은 실제보다 훨씬 좋아 보입니다.</li>
  <li><strong>소형주 전략</strong> — 상장폐지는 소형주에서 압도적으로 많이 일어납니다. 소형주 유니버스일수록 생존자 편향이 큽니다.</li>
  <li><strong>역발상·평균회귀 전략</strong> — “많이 떨어진 종목을 산다”는 규칙은 상장폐지 직전 종목을 정확히 골라냅니다. 그 종목들이 데이터에 없으면 폭락 후 반등한 종목만 남습니다.</li>
</ul>

<p>반대로 대형주·ETF·지수를 다루는 전략은 상대적으로 안전합니다. 지금까지 이 시리즈가 코스피 지수 하나로 백테스트를 해온 이유 중 하나이기도 합니다.</p>

<h2 id="왜곡의-크기">왜곡의 크기</h2>

<p>정확한 수치는 시장과 기간, 전략마다 다릅니다. 다만 학계 연구에서 생존 편향을 보정하기 전후의 성과 차이가 <strong>연 1%p를 넘는 경우가 흔히 보고되고</strong>, 소형주나 밸류 전략에서는 그보다 훨씬 큰 차이가 나기도 합니다. 연 1%p는 작아 보이지만, 20년 복리로는 최종 자산의 20% 이상 차이입니다. “이 전략이 시장을 연 1.5%p 이겼다”는 백테스트 결과가 생존 편향 하나로 통째로 사라질 수 있다는 뜻입니다.</p>

<h2 id="무료-데이터로-할-수-있는-것">무료 데이터로 할 수 있는 것</h2>

<p>완벽한 해결책은 <strong>각 시점의 상장 종목 목록과, 상장폐지된 종목의 가격 데이터</strong>를 모두 갖추는 것입니다. 이런 데이터는 유료이거나 구축에 손이 많이 갑니다. 무료 데이터로 작업할 때 현실적인 대응은 세 단계입니다.</p>

<h3 id="1단계-시점별-상장-목록-만들기">1단계: 시점별 상장 목록 만들기</h3>

<p>거래소 정보 시스템에서는 상장폐지 종목 목록과 상장일·폐지일을 조회할 수 있습니다. 이걸로 “그 시점에 상장되어 있던 종목”을 재구성합니다.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">pandas</span> <span class="k">as</span> <span class="n">pd</span>

<span class="c1"># listing: 종목별 상장일·상장폐지일 (폐지되지 않았으면 NaT)
# columns: code, listed_date, delisted_date
</span><span class="k">def</span> <span class="nf">universe_at</span><span class="p">(</span><span class="n">listing</span><span class="p">,</span> <span class="n">date</span><span class="p">):</span>
    <span class="s">"""date 시점에 상장되어 있던 종목 코드 목록"""</span>
    <span class="n">date</span> <span class="o">=</span> <span class="n">pd</span><span class="p">.</span><span class="n">Timestamp</span><span class="p">(</span><span class="n">date</span><span class="p">)</span>
    <span class="n">alive</span> <span class="o">=</span> <span class="p">(</span><span class="n">listing</span><span class="p">[</span><span class="s">'listed_date'</span><span class="p">]</span> <span class="o">&lt;=</span> <span class="n">date</span><span class="p">)</span> <span class="o">&amp;</span> <span class="p">(</span>
        <span class="n">listing</span><span class="p">[</span><span class="s">'delisted_date'</span><span class="p">].</span><span class="n">isna</span><span class="p">()</span> <span class="o">|</span> <span class="p">(</span><span class="n">listing</span><span class="p">[</span><span class="s">'delisted_date'</span><span class="p">]</span> <span class="o">&gt;</span> <span class="n">date</span><span class="p">)</span>
    <span class="p">)</span>
    <span class="k">return</span> <span class="n">listing</span><span class="p">.</span><span class="n">loc</span><span class="p">[</span><span class="n">alive</span><span class="p">,</span> <span class="s">'code'</span><span class="p">].</span><span class="n">tolist</span><span class="p">()</span>
</code></pre></div></div>

<p>이 함수를 리밸런싱 시점마다 호출해서 유니버스를 정하면, 적어도 “그때 존재하지 않았던 종목”을 사는 실수는 막을 수 있습니다. 문제는 폐지된 종목의 <strong>가격 데이터</strong>가 여전히 없다는 것인데, 그건 다음 단계에서 다룹니다.</p>

<h3 id="2단계-스트레스-테스트로-영향-가늠하기">2단계: 스트레스 테스트로 영향 가늠하기</h3>

<p>폐지 종목 가격을 구할 수 없다면, <strong>“만약 있었다면”을 가정해서 성과가 얼마나 나빠지는지</strong> 확인합니다. 매년 일정 비율의 종목이 큰 손실과 함께 사라진다고 가정하고 백테스트를 다시 돌리는 방식입니다.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">numpy</span> <span class="k">as</span> <span class="n">np</span>
<span class="kn">import</span> <span class="nn">pandas</span> <span class="k">as</span> <span class="n">pd</span>

<span class="k">def</span> <span class="nf">survivorship_stress</span><span class="p">(</span><span class="n">returns_df</span><span class="p">,</span> <span class="n">delist_rate_annual</span><span class="o">=</span><span class="mf">0.03</span><span class="p">,</span>
                        <span class="n">delist_loss</span><span class="o">=-</span><span class="mf">0.6</span><span class="p">,</span> <span class="n">seed</span><span class="o">=</span><span class="mi">0</span><span class="p">):</span>
    <span class="s">"""
    returns_df: 종목별 월간 수익률 (columns = 종목)
    매월 일부 종목을 무작위로 '상장폐지'시켜 그 달에 delist_loss를 부여하고
    이후 데이터는 제거한다. 생존 편향이 있었다면 성과가 얼마나 부풀려졌는지 가늠하는 용도.
    """</span>
    <span class="n">rng</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">random</span><span class="p">.</span><span class="n">default_rng</span><span class="p">(</span><span class="n">seed</span><span class="p">)</span>
    <span class="n">r</span> <span class="o">=</span> <span class="n">returns_df</span><span class="p">.</span><span class="n">copy</span><span class="p">()</span>
    <span class="n">monthly_rate</span> <span class="o">=</span> <span class="n">delist_rate_annual</span> <span class="o">/</span> <span class="mi">12</span>
    <span class="n">alive</span> <span class="o">=</span> <span class="n">pd</span><span class="p">.</span><span class="n">Series</span><span class="p">(</span><span class="bp">True</span><span class="p">,</span> <span class="n">index</span><span class="o">=</span><span class="n">r</span><span class="p">.</span><span class="n">columns</span><span class="p">)</span>

    <span class="k">for</span> <span class="n">i</span><span class="p">,</span> <span class="n">dt</span> <span class="ow">in</span> <span class="nb">enumerate</span><span class="p">(</span><span class="n">r</span><span class="p">.</span><span class="n">index</span><span class="p">):</span>
        <span class="n">candidates</span> <span class="o">=</span> <span class="n">alive</span><span class="p">[</span><span class="n">alive</span><span class="p">].</span><span class="n">index</span>
        <span class="n">n_delist</span> <span class="o">=</span> <span class="n">rng</span><span class="p">.</span><span class="n">binomial</span><span class="p">(</span><span class="nb">len</span><span class="p">(</span><span class="n">candidates</span><span class="p">),</span> <span class="n">monthly_rate</span><span class="p">)</span>
        <span class="k">if</span> <span class="n">n_delist</span> <span class="o">==</span> <span class="mi">0</span><span class="p">:</span>
            <span class="k">continue</span>
        <span class="n">victims</span> <span class="o">=</span> <span class="n">rng</span><span class="p">.</span><span class="n">choice</span><span class="p">(</span><span class="n">candidates</span><span class="p">,</span> <span class="n">size</span><span class="o">=</span><span class="n">n_delist</span><span class="p">,</span> <span class="n">replace</span><span class="o">=</span><span class="bp">False</span><span class="p">)</span>
        <span class="n">r</span><span class="p">.</span><span class="n">loc</span><span class="p">[</span><span class="n">dt</span><span class="p">,</span> <span class="n">victims</span><span class="p">]</span> <span class="o">=</span> <span class="n">delist_loss</span>
        <span class="n">r</span><span class="p">.</span><span class="n">loc</span><span class="p">[</span><span class="n">r</span><span class="p">.</span><span class="n">index</span><span class="p">[</span><span class="n">i</span> <span class="o">+</span> <span class="mi">1</span><span class="p">:],</span> <span class="n">victims</span><span class="p">]</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">nan</span>
        <span class="n">alive</span><span class="p">[</span><span class="n">victims</span><span class="p">]</span> <span class="o">=</span> <span class="bp">False</span>

    <span class="k">return</span> <span class="n">r</span>

<span class="c1"># 동일가중 포트폴리오로 비교 (NaN은 자동 제외 = 생존 종목에 재투자)
</span><span class="n">eq_original</span> <span class="o">=</span> <span class="n">returns_df</span><span class="p">.</span><span class="n">mean</span><span class="p">(</span><span class="n">axis</span><span class="o">=</span><span class="mi">1</span><span class="p">)</span>
<span class="n">eq_stressed</span> <span class="o">=</span> <span class="n">survivorship_stress</span><span class="p">(</span><span class="n">returns_df</span><span class="p">).</span><span class="n">mean</span><span class="p">(</span><span class="n">axis</span><span class="o">=</span><span class="mi">1</span><span class="p">)</span>

<span class="n">cum</span> <span class="o">=</span> <span class="n">pd</span><span class="p">.</span><span class="n">DataFrame</span><span class="p">({</span>
    <span class="s">'Survivors only'</span><span class="p">:</span> <span class="p">(</span><span class="mi">1</span> <span class="o">+</span> <span class="n">eq_original</span><span class="p">).</span><span class="n">cumprod</span><span class="p">(),</span>
    <span class="s">'With delistings'</span><span class="p">:</span> <span class="p">(</span><span class="mi">1</span> <span class="o">+</span> <span class="n">eq_stressed</span><span class="p">).</span><span class="n">cumprod</span><span class="p">(),</span>
<span class="p">})</span>
<span class="k">print</span><span class="p">(</span><span class="n">cum</span><span class="p">.</span><span class="n">iloc</span><span class="p">[</span><span class="o">-</span><span class="mi">1</span><span class="p">].</span><span class="nb">round</span><span class="p">(</span><span class="mi">3</span><span class="p">))</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">delist_rate_annual</code>(연간 상장폐지율)과 <code class="language-plaintext highlighter-rouge">delist_loss</code>(폐지 시 손실률)는 가정치입니다. 거래소 통계로 실제 폐지율을 확인해서 넣으면 더 현실적인 추정이 됩니다. 이 방법은 정확한 답을 주지는 못하지만, <strong>“이 전략은 생존 편향에 얼마나 민감한가”</strong>라는 질문에는 답을 줍니다. 두 곡선의 차이가 크면 그 전략은 실전에서 크게 실망시킬 가능성이 높습니다.</p>

<h3 id="3단계-편향이-작은-유니버스를-쓰기">3단계: 편향이 작은 유니버스를 쓰기</h3>

<p>가장 실용적인 방법입니다. 시가총액 상위 종목, 지수, ETF처럼 <strong>상장폐지가 드문 대상</strong>으로 전략을 설계하면 생존 편향의 영향이 구조적으로 작아집니다. 소형주 전략을 무료 데이터로 검증하려는 시도는, 정직하게 말하면 신뢰할 만한 결과를 얻기 어렵습니다.</p>

<h2 id="다른-얼굴들">다른 얼굴들</h2>

<p>생존 편향은 종목에만 있는 것이 아닙니다.</p>

<ul>
  <li><strong>펀드 성과</strong> — “국내 주식형 펀드의 10년 평균 수익률”에는 중간에 청산된 펀드가 빠져 있습니다. 성과가 나쁜 펀드가 주로 청산되므로 평균이 부풀려집니다.</li>
  <li><strong>전략 자체</strong> — 블로그나 책에 소개되는 전략은 살아남은 전략입니다. 실패한 전략은 아무도 글로 쓰지 않습니다. 이 블로그의 “삽질 기록” 카테고리를 만든 이유가 이것입니다.</li>
  <li><strong>투자 대가의 조언</strong> — 성공한 투자자의 습관을 따라 하는 것도 같은 문제를 안고 있습니다. 같은 습관으로 실패한 사람들은 인터뷰 대상이 되지 않습니다.</li>
</ul>

<h2 id="정리">정리</h2>

<ul>
  <li>생존 편향은 데이터에서 사라진 종목을 무시해서 성과가 부풀려지는 현상이며, 지금 상장 목록으로 과거를 백테스트하면 반드시 발생한다.</li>
  <li>밸류·소형주·역발상 전략에서 특히 심하고, 대형주·지수·ETF 전략에서는 상대적으로 작다.</li>
  <li>무료 데이터로는 시점별 상장 목록 재구성, 상장폐지 스트레스 테스트, 편향이 작은 유니버스 선택으로 대응한다.</li>
  <li>종목뿐 아니라 펀드 성과, 소개되는 전략, 성공담에도 같은 편향이 있다.</li>
</ul>

<p>이것으로 백테스트의 3대 함정 — 미래 참조, 과최적화, 생존 편향 — 을 모두 다뤘습니다. 다음 글부터는 방향을 바꿔서 <strong>자산 배분</strong>으로 넘어갑니다. 주식과 채권을 섞으면 왜 수익률 대비 위험이 줄어드는지, 그 원리를 데이터로 확인하는 것부터 시작하겠습니다.</p>

<hr />

<p><em>이 글은 학습과 정보 공유 목적으로 작성되었으며, 특정 종목이나 상품에 대한 투자 권유가 아닙니다. 투자의 판단과 책임은 투자자 본인에게 있습니다.</em></p>]]></content><author><name>퀀트 노트</name></author><category term="퀀트투자" /><category term="백테스트" /><category term="생존편향" /><category term="데이터" /><summary type="html"><![CDATA[지난 글에서 “지금의 지수 구성종목으로 과거를 백테스트하면 안 된다”고 했습니다. 그 문제의 뿌리가 오늘 다룰 생존 편향(survivorship bias)입니다. 백테스트를 망가뜨리는 편향 중 가장 조용하고, 무료 데이터로 작업하는 개인 투자자가 가장 피하기 어려운 것이기도 합니다.]]></summary></entry><entry><title type="html">퀀트 투자 입문 ⑨ — 미래 참조 편향의 여러 얼굴</title><link href="https://quant-note.com/posts/look-ahead-bias-faces/" rel="alternate" type="text/html" title="퀀트 투자 입문 ⑨ — 미래 참조 편향의 여러 얼굴" /><published>2026-09-04T00:00:00+09:00</published><updated>2026-09-04T00:00:00+09:00</updated><id>https://quant-note.com/posts/look-ahead-bias-faces</id><content type="html" xml:base="https://quant-note.com/posts/look-ahead-bias-faces/"><![CDATA[<p>백테스트의 제1원칙은 “각 시점의 판단에는 그 시점까지 알 수 있었던 정보만 쓴다”였습니다. 이 원칙을 어기는 것이 <strong>미래 참조 편향(look-ahead bias)</strong>입니다. 지금까지는 <code class="language-plaintext highlighter-rouge">shift(1)</code>을 빼먹는 경우만 이야기했는데, 실제로 미래 정보가 새어 들어오는 경로는 훨씬 다양하고 대부분 알아채기 어렵습니다. 오늘은 그 얼굴들을 하나씩 뜯어봅니다.</p>

<h2 id="얼굴-1-신호와-수익률의-시점이-어긋남">얼굴 1: 신호와 수익률의 시점이 어긋남</h2>

<p>가장 기본적인 형태이고, 앞에서 다뤘으니 짧게 복습만 합니다. 이달 말 종가로 계산한 신호를 이달 수익률에 적용하면, 이미 끝난 달에 그 결과를 알고 투자한 셈이 됩니다.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">signal</span> <span class="o">=</span> <span class="p">(</span><span class="n">monthly</span><span class="p">.</span><span class="n">pct_change</span><span class="p">(</span><span class="mi">12</span><span class="p">)</span> <span class="o">&gt;</span> <span class="mi">0</span><span class="p">).</span><span class="n">astype</span><span class="p">(</span><span class="nb">int</span><span class="p">)</span>
<span class="n">position</span> <span class="o">=</span> <span class="n">signal</span><span class="p">.</span><span class="n">shift</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span>     <span class="c1"># 이 줄이 없으면 시간여행
</span><span class="n">strategy</span> <span class="o">=</span> <span class="n">position</span> <span class="o">*</span> <span class="n">monthly</span><span class="p">.</span><span class="n">pct_change</span><span class="p">()</span>
</code></pre></div></div>

<p>이건 눈에 잘 띕니다. 문제는 지금부터 나오는 것들입니다.</p>

<h2 id="얼굴-2-재무제표는-결산일에-공개되지-않는다">얼굴 2: 재무제표는 결산일에 공개되지 않는다</h2>

<p>밸류 팩터를 백테스트한다고 합시다. “매년 말 PBR이 낮은 종목을 산다”는 규칙입니다. 여기서 PBR의 분모인 순자산은 <strong>12월 31일 결산 재무제표</strong>에서 나옵니다. 그런데 그 재무제표가 실제로 공개되는 시점은 언제일까요? 대개 이듬해 2~3월입니다. 감사보고서 확정까지 고려하면 3월 말이 흔합니다.</p>

<p>즉 “12월 31일에 12월 결산 PBR로 종목을 고른다”는 것은, <strong>석 달 뒤에야 알 수 있는 숫자를 미리 알고 투자하는 것</strong>입니다. 이 오류는 밸류·퀄리티처럼 재무 데이터를 쓰는 전략에서 거의 예외 없이 발생하고, 성과를 상당히 부풀립니다. 실적이 좋게 나올 회사를 발표 전에 미리 사는 것과 같기 때문입니다.</p>

<p>해결책은 재무 데이터에 <strong>공시 시차를 반영해서 “사용 가능 시점”을 따로 두는 것</strong>입니다.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">pandas</span> <span class="k">as</span> <span class="n">pd</span>

<span class="c1"># fin: 결산 기준일을 인덱스로 하는 분기 재무 데이터 (예: PBR)
# 보수적으로 결산일 + 90일부터 사용 가능하다고 가정
</span><span class="n">fin</span> <span class="o">=</span> <span class="n">fin</span><span class="p">.</span><span class="n">copy</span><span class="p">()</span>
<span class="n">fin</span><span class="p">[</span><span class="s">'available_from'</span><span class="p">]</span> <span class="o">=</span> <span class="n">fin</span><span class="p">.</span><span class="n">index</span> <span class="o">+</span> <span class="n">pd</span><span class="p">.</span><span class="n">DateOffset</span><span class="p">(</span><span class="n">days</span><span class="o">=</span><span class="mi">90</span><span class="p">)</span>

<span class="c1"># 각 매매 시점에 "그때까지 공개된 가장 최신 재무 데이터"를 붙인다
</span><span class="n">prices_df</span> <span class="o">=</span> <span class="n">monthly</span><span class="p">.</span><span class="n">rename</span><span class="p">(</span><span class="s">'close'</span><span class="p">).</span><span class="n">reset_index</span><span class="p">().</span><span class="n">rename</span><span class="p">(</span><span class="n">columns</span><span class="o">=</span><span class="p">{</span><span class="s">'index'</span><span class="p">:</span> <span class="s">'date'</span><span class="p">})</span>
<span class="n">fin_df</span> <span class="o">=</span> <span class="n">fin</span><span class="p">.</span><span class="n">sort_values</span><span class="p">(</span><span class="s">'available_from'</span><span class="p">)</span>

<span class="n">merged</span> <span class="o">=</span> <span class="n">pd</span><span class="p">.</span><span class="n">merge_asof</span><span class="p">(</span>
    <span class="n">prices_df</span><span class="p">.</span><span class="n">sort_values</span><span class="p">(</span><span class="s">'date'</span><span class="p">),</span>
    <span class="n">fin_df</span><span class="p">,</span>
    <span class="n">left_on</span><span class="o">=</span><span class="s">'date'</span><span class="p">,</span>
    <span class="n">right_on</span><span class="o">=</span><span class="s">'available_from'</span><span class="p">,</span>
    <span class="n">direction</span><span class="o">=</span><span class="s">'backward'</span>      <span class="c1"># 과거 방향으로만 찾는다 = 미래 참조 차단
</span><span class="p">)</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">pd.merge_asof</code>에 <code class="language-plaintext highlighter-rouge">direction='backward'</code>를 주는 것이 핵심입니다. 각 매매 시점보다 <strong>이전에</strong> 공개된 데이터만 가져옵니다. 90일이라는 숫자는 보수적인 가정치이고, 실제 공시 일정을 확인해서 조정하면 됩니다. 공시일 데이터가 있다면 그것을 <code class="language-plaintext highlighter-rouge">available_from</code>으로 쓰는 것이 가장 정확합니다.</p>

<h2 id="얼굴-3-오늘의-지수-구성종목으로-과거를-본다">얼굴 3: 오늘의 지수 구성종목으로 과거를 본다</h2>

<p>“코스피200 종목 중에서 모멘텀 상위를 산다”는 전략을 만들 때, 흔히 <strong>지금 시점의 코스피200 구성종목 목록</strong>을 받아서 10년 전부터 백테스트합니다. 문제는 10년 전에는 그 목록이 달랐다는 것입니다.</p>

<p>지금 코스피200에 들어 있는 종목은 지난 10년간 살아남았고 성장했기 때문에 들어 있는 것입니다. 10년 전 시점에서 “앞으로 지수에 편입될 종목들”만 골라 투자한 셈이니, 성과가 좋을 수밖에 없습니다. 이건 미래 참조 편향이면서 동시에 다음 글에서 다룰 <strong>생존 편향</strong>과 겹치는 영역입니다.</p>

<p>해결책은 <strong>각 시점의 구성종목 목록(point-in-time universe)</strong>을 쓰는 것인데, 이 데이터는 무료로 구하기 어렵습니다. 현실적인 차선책은 두 가지입니다. 첫째, 지수 대신 “그 시점에 상장되어 있던 종목 중 시가총액 상위 N개”처럼 <strong>그 시점 데이터만으로 재구성할 수 있는 유니버스</strong>를 쓰는 것. 둘째, 그것도 어렵다면 결과를 볼 때 “유니버스 편향으로 성과가 부풀려져 있다”는 것을 명시하고 보수적으로 해석하는 것입니다.</p>

<h2 id="얼굴-4-수정주가는-미래를-알고-있다">얼굴 4: 수정주가는 미래를 알고 있다</h2>

<p>수정주가(adjusted price)는 액면분할, 배당 등을 과거 가격에 소급 반영한 것입니다. 수익률 계산에는 이게 맞습니다. 그런데 수정주가에는 미묘한 함정이 있습니다.</p>

<p>2018년 삼성전자가 50:1 액면분할을 했을 때, 수정주가 기준으로 2017년 가격은 약 5만 원대로 표시됩니다. 하지만 2017년에 실제로 거래된 가격은 250만 원대였습니다. “주가가 10만 원 이하인 종목만 산다” 같은 <strong>가격 수준에 기반한 규칙</strong>을 수정주가로 백테스트하면, 2017년의 삼성전자를 살 수 있었던 것으로 잘못 계산됩니다. 그 시점에는 분할이 있을지 아무도 몰랐는데 말이죠.</p>

<p>원칙은 이렇습니다. <strong>수익률 계산은 수정주가로, 가격 수준·거래 단위 판단은 당시 실제 가격으로.</strong> 둘을 구분해서 보관해야 합니다.</p>

<h2 id="얼굴-5-전체-기간-통계로-정규화하기">얼굴 5: 전체 기간 통계로 정규화하기</h2>

<p>이건 정말 자주, 그리고 조용히 일어나는 실수입니다. 지표를 표준화할 때 <strong>전체 기간의 평균과 표준편차</strong>를 쓰는 경우입니다.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># 잘못된 방법: 전체 기간 평균·표준편차 (2026년 데이터가 2010년 판단에 영향)
</span><span class="n">z_wrong</span> <span class="o">=</span> <span class="p">(</span><span class="n">x</span> <span class="o">-</span> <span class="n">x</span><span class="p">.</span><span class="n">mean</span><span class="p">())</span> <span class="o">/</span> <span class="n">x</span><span class="p">.</span><span class="n">std</span><span class="p">()</span>

<span class="c1"># 올바른 방법: 각 시점까지의 데이터만 사용
</span><span class="n">z_right</span> <span class="o">=</span> <span class="p">(</span><span class="n">x</span> <span class="o">-</span> <span class="n">x</span><span class="p">.</span><span class="n">expanding</span><span class="p">().</span><span class="n">mean</span><span class="p">())</span> <span class="o">/</span> <span class="n">x</span><span class="p">.</span><span class="n">expanding</span><span class="p">().</span><span class="n">std</span><span class="p">()</span>

<span class="c1"># 또는 최근 N개 기간만 사용 (롤링)
</span><span class="n">z_rolling</span> <span class="o">=</span> <span class="p">(</span><span class="n">x</span> <span class="o">-</span> <span class="n">x</span><span class="p">.</span><span class="n">rolling</span><span class="p">(</span><span class="mi">36</span><span class="p">).</span><span class="n">mean</span><span class="p">())</span> <span class="o">/</span> <span class="n">x</span><span class="p">.</span><span class="n">rolling</span><span class="p">(</span><span class="mi">36</span><span class="p">).</span><span class="n">std</span><span class="p">()</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">x.mean()</code>은 2010년부터 2026년까지의 평균입니다. 2010년의 신호를 이 값으로 정규화하면, 2010년에는 몰랐던 이후 16년의 평균 수준을 알고 판단하는 것이 됩니다. 이 실수는 “평균 대비 얼마나 높은가”를 쓰는 모든 전략 — 밸류, 저변동성, 평균회귀 — 에 숨어 있을 수 있습니다. 비슷한 예로 머신러닝 전처리에서 전체 데이터로 스케일러를 학습시키는 것도 같은 오류입니다.</p>

<h2 id="점검-체크리스트">점검 체크리스트</h2>

<p>백테스트 코드를 완성했을 때 이 다섯 가지를 스스로에게 묻습니다.</p>

<ol>
  <li>신호 계산에 쓰인 마지막 데이터 시점이 <strong>매매 시점보다 앞</strong>인가? (<code class="language-plaintext highlighter-rouge">shift</code> 확인)</li>
  <li>재무·거시 데이터에 <strong>공시 시차</strong>를 반영했는가? (<code class="language-plaintext highlighter-rouge">merge_asof</code>, <code class="language-plaintext highlighter-rouge">direction='backward'</code>)</li>
  <li>유니버스(종목 목록)가 <strong>그 시점의 목록</strong>인가, 지금 목록인가?</li>
  <li>가격 수준 규칙에 수정주가를 쓰고 있지는 않은가?</li>
  <li>평균·표준편차·분위수 계산에 <strong>미래 데이터가 섞여 있지</strong> 않은가? (<code class="language-plaintext highlighter-rouge">expanding</code>, <code class="language-plaintext highlighter-rouge">rolling</code>)</li>
</ol>

<p>이 중 하나라도 “잘 모르겠다”면 백테스트 성과에 물음표를 붙여야 합니다.</p>

<h2 id="정리">정리</h2>

<ul>
  <li>미래 참조 편향은 <code class="language-plaintext highlighter-rouge">shift</code> 누락 외에도 재무제표 공시 시차, 현재 지수 구성종목 사용, 수정주가로 가격 수준 판단, 전체 기간 통계 사용 등 여러 경로로 스며든다.</li>
  <li>재무 데이터는 <code class="language-plaintext highlighter-rouge">merge_asof(direction='backward')</code>로 공시 시점 이후에만 사용한다.</li>
  <li>정규화는 <code class="language-plaintext highlighter-rouge">expanding</code> 또는 <code class="language-plaintext highlighter-rouge">rolling</code>으로 각 시점까지의 데이터만 쓴다.</li>
  <li>눈에 띄지 않는 경로일수록 성과를 크게 부풀리므로, 체크리스트로 매번 점검한다.</li>
</ul>

<p>다음 글에서는 얼굴 3에서 잠깐 언급한 <strong>생존 편향(survivorship bias)</strong>을 따로 다룹니다. 상장폐지되어 데이터에서 사라진 종목들이 백테스트를 어떻게 왜곡하는지, 그리고 무료 데이터로 이 문제를 얼마나 완화할 수 있는지 살펴보겠습니다.</p>

<hr />

<p><em>이 글은 학습과 정보 공유 목적으로 작성되었으며, 특정 종목이나 상품에 대한 투자 권유가 아닙니다. 투자의 판단과 책임은 투자자 본인에게 있습니다.</em></p>]]></content><author><name>퀀트 노트</name></author><category term="퀀트투자" /><category term="백테스트" /><category term="미래참조편향" /><category term="데이터" /><summary type="html"><![CDATA[백테스트의 제1원칙은 “각 시점의 판단에는 그 시점까지 알 수 있었던 정보만 쓴다”였습니다. 이 원칙을 어기는 것이 미래 참조 편향(look-ahead bias)입니다. 지금까지는 shift(1)을 빼먹는 경우만 이야기했는데, 실제로 미래 정보가 새어 들어오는 경로는 훨씬 다양하고 대부분 알아채기 어렵습니다. 오늘은 그 얼굴들을 하나씩 뜯어봅니다.]]></summary></entry><entry><title type="html">퀀트 투자 입문 ⑧ — 위험 지표 완전 정복: 변동성, MDD, 샤프비율, 칼마비율</title><link href="https://quant-note.com/posts/risk-metrics-explained/" rel="alternate" type="text/html" title="퀀트 투자 입문 ⑧ — 위험 지표 완전 정복: 변동성, MDD, 샤프비율, 칼마비율" /><published>2026-09-03T00:00:00+09:00</published><updated>2026-09-03T00:00:00+09:00</updated><id>https://quant-note.com/posts/risk-metrics-explained</id><content type="html" xml:base="https://quant-note.com/posts/risk-metrics-explained/"><![CDATA[<p>지금까지 전략 성과를 볼 때 CAGR과 MDD 두 가지만 썼습니다. 오늘은 이 도구함을 제대로 채웁니다. 수익률 하나만 보고 전략을 고르는 것은 자동차를 최고 속도만 보고 고르는 것과 같습니다. 브레이크와 안전벨트를 함께 봐야 합니다.</p>

<h2 id="변동성--얼마나-출렁이는가">변동성 — 얼마나 출렁이는가</h2>

<p>변동성(volatility)은 수익률의 <strong>표준편차</strong>입니다. 평균에서 얼마나 흩어져 있는지를 재는 값이고, 관례적으로 연 단위로 환산해서 씁니다.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">numpy</span> <span class="k">as</span> <span class="n">np</span>

<span class="c1"># 월별 수익률이라면 12, 일별이라면 252를 곱한다
</span><span class="n">vol_annual</span> <span class="o">=</span> <span class="n">monthly_returns</span><span class="p">.</span><span class="n">std</span><span class="p">()</span> <span class="o">*</span> <span class="n">np</span><span class="p">.</span><span class="n">sqrt</span><span class="p">(</span><span class="mi">12</span><span class="p">)</span>
</code></pre></div></div>

<p>여기서 <code class="language-plaintext highlighter-rouge">np.sqrt</code>가 붙는 이유를 짚고 갑니다. 분산(표준편차의 제곱)은 기간에 비례해 커지므로, 표준편차는 <strong>기간의 제곱근</strong>에 비례합니다. 그래서 월별 변동성을 연율화할 때는 12가 아니라 √12를 곱합니다. 이 규칙을 모르면 변동성을 3.5배 과대평가하게 됩니다.</p>

<p>변동성의 한계도 분명합니다. <strong>위로 튀는 것과 아래로 빠지는 것을 똑같이 위험으로 취급</strong>한다는 점입니다. 갑자기 +20% 오른 달과 -20% 빠진 달이 변동성에는 동일하게 기여합니다. 투자자에게 이 둘은 전혀 같지 않죠. 이 문제를 보완한 지표가 뒤에 나올 소르티노 비율입니다.</p>

<h2 id="mdd와-회복-기간--얼마나-아팠고-얼마나-오래-아팠는가">MDD와 회복 기간 — 얼마나 아팠고, 얼마나 오래 아팠는가</h2>

<p>MDD(최대 낙폭)는 앞에서 다뤘습니다. 전 고점 대비 가장 깊이 떨어진 폭입니다. 여기에 하나를 더 봐야 합니다. <strong>회복 기간(time to recovery)</strong>, 즉 전 고점을 되찾는 데 걸린 시간입니다.</p>

<p>-30%까지 빠졌다가 6개월 만에 회복한 전략과, -25%까지 빠졌지만 4년간 회복하지 못한 전략이 있다면, 후자가 훨씬 견디기 어렵습니다. 사람이 전략을 포기하는 것은 낙폭의 깊이보다 <strong>부진이 지속되는 시간</strong> 때문인 경우가 많습니다.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">drawdown_stats</span><span class="p">(</span><span class="n">cum</span><span class="p">):</span>
    <span class="s">"""낙폭 시계열, MDD, 최장 침체 기간(개월)을 반환"""</span>
    <span class="n">peak</span> <span class="o">=</span> <span class="n">cum</span><span class="p">.</span><span class="n">cummax</span><span class="p">()</span>
    <span class="n">dd</span> <span class="o">=</span> <span class="n">cum</span> <span class="o">/</span> <span class="n">peak</span> <span class="o">-</span> <span class="mi">1</span>

    <span class="c1"># 고점을 회복하지 못한 구간의 연속 길이 중 최댓값
</span>    <span class="n">under</span> <span class="o">=</span> <span class="n">dd</span> <span class="o">&lt;</span> <span class="mi">0</span>
    <span class="n">longest</span><span class="p">,</span> <span class="n">run</span> <span class="o">=</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span>
    <span class="k">for</span> <span class="n">flag</span> <span class="ow">in</span> <span class="n">under</span><span class="p">:</span>
        <span class="n">run</span> <span class="o">=</span> <span class="n">run</span> <span class="o">+</span> <span class="mi">1</span> <span class="k">if</span> <span class="n">flag</span> <span class="k">else</span> <span class="mi">0</span>
        <span class="n">longest</span> <span class="o">=</span> <span class="nb">max</span><span class="p">(</span><span class="n">longest</span><span class="p">,</span> <span class="n">run</span><span class="p">)</span>

    <span class="k">return</span> <span class="n">dd</span><span class="p">,</span> <span class="n">dd</span><span class="p">.</span><span class="nb">min</span><span class="p">(),</span> <span class="n">longest</span>
</code></pre></div></div>

<h2 id="샤프비율--위험-한-단위당-얼마를-벌었나">샤프비율 — 위험 한 단위당 얼마를 벌었나</h2>

<p>샤프비율(Sharpe ratio)은 가장 널리 쓰이는 위험조정 수익률 지표입니다.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>샤프비율 = (전략 수익률 - 무위험 수익률) / 변동성
</code></pre></div></div>

<p>“위험을 1만큼 감수해서 초과수익을 얼마나 얻었나”를 재는 값입니다. 연 10%를 변동성 20%로 번 전략보다, 연 8%를 변동성 10%로 번 전략이 더 효율적이라는 판단을 가능하게 해줍니다.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">sharpe</span><span class="p">(</span><span class="n">returns</span><span class="p">,</span> <span class="n">rf_annual</span><span class="o">=</span><span class="mf">0.03</span><span class="p">,</span> <span class="n">periods</span><span class="o">=</span><span class="mi">12</span><span class="p">):</span>
    <span class="s">"""rf_annual: 연 무위험수익률 가정치. 실제 값으로 교체할 것"""</span>
    <span class="n">rf_period</span> <span class="o">=</span> <span class="p">(</span><span class="mi">1</span> <span class="o">+</span> <span class="n">rf_annual</span><span class="p">)</span> <span class="o">**</span> <span class="p">(</span><span class="mi">1</span> <span class="o">/</span> <span class="n">periods</span><span class="p">)</span> <span class="o">-</span> <span class="mi">1</span>
    <span class="n">excess</span> <span class="o">=</span> <span class="n">returns</span> <span class="o">-</span> <span class="n">rf_period</span>
    <span class="k">if</span> <span class="n">excess</span><span class="p">.</span><span class="n">std</span><span class="p">()</span> <span class="o">==</span> <span class="mi">0</span><span class="p">:</span>
        <span class="k">return</span> <span class="nb">float</span><span class="p">(</span><span class="s">'nan'</span><span class="p">)</span>
    <span class="k">return</span> <span class="n">excess</span><span class="p">.</span><span class="n">mean</span><span class="p">()</span> <span class="o">/</span> <span class="n">excess</span><span class="p">.</span><span class="n">std</span><span class="p">()</span> <span class="o">*</span> <span class="n">np</span><span class="p">.</span><span class="n">sqrt</span><span class="p">(</span><span class="n">periods</span><span class="p">)</span>
</code></pre></div></div>

<p>해석의 대략적인 기준은 이렇습니다. 다만 자산군과 기간에 따라 통용되는 수준이 다르므로 절대적인 잣대로 쓰면 안 됩니다.</p>

<table>
  <thead>
    <tr>
      <th>샤프비율</th>
      <th>통상적인 평가</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>1 미만</td>
      <td>평범</td>
    </tr>
    <tr>
      <td>1 ~ 2</td>
      <td>양호</td>
    </tr>
    <tr>
      <td>2 이상</td>
      <td>매우 우수 — 다만 백테스트에서 나왔다면 오류를 먼저 의심</td>
    </tr>
  </tbody>
</table>

<p>마지막 줄이 중요합니다. 개인이 만든 백테스트에서 샤프 3이 나왔다면 축하할 일이 아니라 미래 참조나 비용 누락을 찾아야 할 일입니다.</p>

<p><strong>샤프비율의 함정</strong>도 알아둬야 합니다. 분모가 변동성이므로, 위로 크게 튄 달이 있으면 오히려 샤프비율이 나빠집니다. 또 수익률 분포가 정규분포에 가깝다는 가정을 깔고 있어서, 평소엔 잔잔하다가 가끔 크게 터지는 전략(옵션 매도 같은)은 실제 위험보다 좋아 보이게 됩니다.</p>

<h2 id="소르티노와-칼마--다른-각도에서-보기">소르티노와 칼마 — 다른 각도에서 보기</h2>

<p><strong>소르티노 비율(Sortino ratio)</strong>은 샤프비율의 분모를 “전체 변동성” 대신 <strong>하락 변동성</strong>으로 바꾼 것입니다. 손실 구간의 출렁임만 위험으로 셉니다.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">sortino</span><span class="p">(</span><span class="n">returns</span><span class="p">,</span> <span class="n">rf_annual</span><span class="o">=</span><span class="mf">0.03</span><span class="p">,</span> <span class="n">periods</span><span class="o">=</span><span class="mi">12</span><span class="p">):</span>
    <span class="n">rf_period</span> <span class="o">=</span> <span class="p">(</span><span class="mi">1</span> <span class="o">+</span> <span class="n">rf_annual</span><span class="p">)</span> <span class="o">**</span> <span class="p">(</span><span class="mi">1</span> <span class="o">/</span> <span class="n">periods</span><span class="p">)</span> <span class="o">-</span> <span class="mi">1</span>
    <span class="n">excess</span> <span class="o">=</span> <span class="n">returns</span> <span class="o">-</span> <span class="n">rf_period</span>
    <span class="n">downside</span> <span class="o">=</span> <span class="n">excess</span><span class="p">[</span><span class="n">excess</span> <span class="o">&lt;</span> <span class="mi">0</span><span class="p">].</span><span class="n">std</span><span class="p">()</span>
    <span class="k">if</span> <span class="n">downside</span> <span class="o">==</span> <span class="mi">0</span> <span class="ow">or</span> <span class="n">np</span><span class="p">.</span><span class="n">isnan</span><span class="p">(</span><span class="n">downside</span><span class="p">):</span>
        <span class="k">return</span> <span class="nb">float</span><span class="p">(</span><span class="s">'nan'</span><span class="p">)</span>
    <span class="k">return</span> <span class="n">excess</span><span class="p">.</span><span class="n">mean</span><span class="p">()</span> <span class="o">/</span> <span class="n">downside</span> <span class="o">*</span> <span class="n">np</span><span class="p">.</span><span class="n">sqrt</span><span class="p">(</span><span class="n">periods</span><span class="p">)</span>
</code></pre></div></div>

<p><strong>칼마 비율(Calmar ratio)</strong>은 훨씬 직관적입니다. <strong>CAGR을 MDD의 절댓값으로 나눈 값</strong>으로, “내가 견뎌야 했던 최악의 고통 대비 얼마를 벌었나”를 뜻합니다. 칼마 0.5라면 최대 낙폭의 절반만큼을 매년 벌었다는 의미입니다.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">calmar</span><span class="p">(</span><span class="n">cagr</span><span class="p">,</span> <span class="n">mdd</span><span class="p">):</span>
    <span class="k">return</span> <span class="n">cagr</span> <span class="o">/</span> <span class="nb">abs</span><span class="p">(</span><span class="n">mdd</span><span class="p">)</span> <span class="k">if</span> <span class="n">mdd</span> <span class="o">!=</span> <span class="mi">0</span> <span class="k">else</span> <span class="nb">float</span><span class="p">(</span><span class="s">'nan'</span><span class="p">)</span>
</code></pre></div></div>

<p>실전 감각으로는 칼마 비율이 가장 와닿는 지표라고 생각합니다. 샤프비율은 통계적으로 정교하지만, 실제로 계좌를 들고 버티는 사람에게는 “얼마나 깊이 파였나” 대비 수익이 더 피부에 닿습니다.</p>

<h2 id="한-번에-계산하는-성과-요약-함수">한 번에 계산하는 성과 요약 함수</h2>

<p>지금까지의 지표를 하나로 묶습니다. 앞으로 백테스트를 만들 때마다 이 함수를 재사용하면 됩니다.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">pandas</span> <span class="k">as</span> <span class="n">pd</span>
<span class="kn">import</span> <span class="nn">numpy</span> <span class="k">as</span> <span class="n">np</span>

<span class="k">def</span> <span class="nf">performance_summary</span><span class="p">(</span><span class="n">returns</span><span class="p">,</span> <span class="n">rf_annual</span><span class="o">=</span><span class="mf">0.03</span><span class="p">,</span> <span class="n">periods</span><span class="o">=</span><span class="mi">12</span><span class="p">):</span>
    <span class="n">r</span> <span class="o">=</span> <span class="n">returns</span><span class="p">.</span><span class="n">dropna</span><span class="p">()</span>
    <span class="n">cum</span> <span class="o">=</span> <span class="p">(</span><span class="mi">1</span> <span class="o">+</span> <span class="n">r</span><span class="p">).</span><span class="n">cumprod</span><span class="p">()</span>
    <span class="n">n_years</span> <span class="o">=</span> <span class="nb">len</span><span class="p">(</span><span class="n">r</span><span class="p">)</span> <span class="o">/</span> <span class="n">periods</span>

    <span class="n">cagr</span> <span class="o">=</span> <span class="n">cum</span><span class="p">.</span><span class="n">iloc</span><span class="p">[</span><span class="o">-</span><span class="mi">1</span><span class="p">]</span> <span class="o">**</span> <span class="p">(</span><span class="mi">1</span> <span class="o">/</span> <span class="n">n_years</span><span class="p">)</span> <span class="o">-</span> <span class="mi">1</span>
    <span class="n">vol</span> <span class="o">=</span> <span class="n">r</span><span class="p">.</span><span class="n">std</span><span class="p">()</span> <span class="o">*</span> <span class="n">np</span><span class="p">.</span><span class="n">sqrt</span><span class="p">(</span><span class="n">periods</span><span class="p">)</span>
    <span class="n">dd</span><span class="p">,</span> <span class="n">mdd</span><span class="p">,</span> <span class="n">longest</span> <span class="o">=</span> <span class="n">drawdown_stats</span><span class="p">(</span><span class="n">cum</span><span class="p">)</span>

    <span class="k">return</span> <span class="n">pd</span><span class="p">.</span><span class="n">Series</span><span class="p">({</span>
        <span class="s">'CAGR'</span><span class="p">:</span> <span class="n">cagr</span><span class="p">,</span>
        <span class="s">'Volatility'</span><span class="p">:</span> <span class="n">vol</span><span class="p">,</span>
        <span class="s">'MDD'</span><span class="p">:</span> <span class="n">mdd</span><span class="p">,</span>
        <span class="s">'Sharpe'</span><span class="p">:</span> <span class="n">sharpe</span><span class="p">(</span><span class="n">r</span><span class="p">,</span> <span class="n">rf_annual</span><span class="p">,</span> <span class="n">periods</span><span class="p">),</span>
        <span class="s">'Sortino'</span><span class="p">:</span> <span class="n">sortino</span><span class="p">(</span><span class="n">r</span><span class="p">,</span> <span class="n">rf_annual</span><span class="p">,</span> <span class="n">periods</span><span class="p">),</span>
        <span class="s">'Calmar'</span><span class="p">:</span> <span class="n">cagr</span> <span class="o">/</span> <span class="nb">abs</span><span class="p">(</span><span class="n">mdd</span><span class="p">)</span> <span class="k">if</span> <span class="n">mdd</span> <span class="o">!=</span> <span class="mi">0</span> <span class="k">else</span> <span class="n">np</span><span class="p">.</span><span class="n">nan</span><span class="p">,</span>
        <span class="s">'Longest DD (기간)'</span><span class="p">:</span> <span class="n">longest</span><span class="p">,</span>
    <span class="p">})</span>

<span class="c1"># 사용 예: 지난 글의 모멘텀 전략과 buy &amp; hold 비교
# summary = pd.DataFrame({
#     'Buy &amp; Hold': performance_summary(returns),
#     'Momentum':   performance_summary(strategy_returns),
# })
# print(summary.round(3))
</span></code></pre></div></div>

<p>무위험 수익률(<code class="language-plaintext highlighter-rouge">rf_annual</code>)은 시기에 따라 달라지므로 기본값 3%는 어디까지나 가정치입니다. 비교하려는 기간의 실제 단기 국채나 예금 금리 수준으로 바꿔 넣으셔야 합니다.</p>

<h2 id="실제-결과로-읽어보기">실제 결과로 읽어보기</h2>

<p>위 함수를 지금까지의 코스피 절대 모멘텀 백테스트(2005년 1월~2026년 8월, 비용 0.3% 반영, 무위험수익률 3% 가정)에 적용한 결과입니다.</p>

<table>
  <thead>
    <tr>
      <th> </th>
      <th>Buy &amp; Hold</th>
      <th>모멘텀 (비용 반영)</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>CAGR</td>
      <td>9.66%</td>
      <td>5.37%</td>
    </tr>
    <tr>
      <td>연 변동성</td>
      <td>22.5%</td>
      <td>18.2%</td>
    </tr>
    <tr>
      <td>MDD</td>
      <td>-48.5%</td>
      <td>-31.5%</td>
    </tr>
    <tr>
      <td>샤프비율</td>
      <td>0.39</td>
      <td>0.21</td>
    </tr>
    <tr>
      <td>소르티노 비율</td>
      <td>0.59</td>
      <td>0.36</td>
    </tr>
    <tr>
      <td><strong>칼마 비율</strong></td>
      <td><strong>0.199</strong></td>
      <td><strong>0.171</strong></td>
    </tr>
    <tr>
      <td>최장 침체 기간</td>
      <td>71개월</td>
      <td>115개월</td>
    </tr>
  </tbody>
</table>

<p>이 표가 “지표를 여러 개 봐야 한다”는 말의 뜻을 그대로 보여줍니다.</p>

<p><strong>샤프비율로 보면</strong> 모멘텀은 명백한 패자입니다(0.21 vs 0.39). 변동성을 줄이긴 했지만 수익률을 그보다 훨씬 많이 잃었기 때문입니다.</p>

<p><strong>칼마 비율로 보면</strong> 거의 비슷합니다(0.171 vs 0.199). 최대 낙폭을 -48.5%에서 -31.5%로 줄인 것이 수익률 손실을 상당히 상쇄합니다. “최악의 순간에 얼마나 아팠나” 대비로는 두 전략이 비슷한 효율이라는 뜻입니다. 2008년을 -48%로 겪을 자신이 없는 사람에게는 이 지표가 더 중요할 수 있습니다.</p>

<p><strong>최장 침체 기간으로 보면</strong> 모멘텀이 오히려 나쁩니다(115개월 vs 71개월). 낙폭은 얕지만 물속에 훨씬 오래 있었습니다. 현금으로 대피한 동안 시장이 회복해 버리면, 모멘텀 전략의 계좌는 전 고점을 되찾지 못한 채 시간만 흐릅니다. 이 숫자는 CAGR·MDD·샤프 어디에도 나타나지 않습니다.</p>

<p>같은 백테스트가 어떤 지표로는 “비슷한 효율”, 어떤 지표로는 “명백한 패자”, 어떤 지표로는 “더 나쁨”으로 읽힙니다. 지표 하나를 골라 결론을 내리면 그 순간 나머지가 말하는 것을 놓칩니다.</p>

<h2 id="지표를-읽을-때의-원칙">지표를 읽을 때의 원칙</h2>

<ul>
  <li><strong>한 지표만 보지 않습니다.</strong> CAGR만 보면 위험을 놓치고, 샤프만 보면 낙폭의 깊이를 놓칩니다. 최소한 CAGR, MDD, 샤프 세 개는 함께 봅니다.</li>
  <li><strong>비교 대상과 나란히 놓습니다.</strong> 샤프 0.8이 좋은지 나쁜지는 같은 기간 buy &amp; hold의 샤프가 얼마였는지를 봐야 압니다.</li>
  <li><strong>너무 좋은 숫자는 의심합니다.</strong> 앞 글의 교훈이 여기서도 그대로 적용됩니다.</li>
  <li><strong>가정치를 명시합니다.</strong> 무위험 수익률, 연율화 기준(12/252), 거래 비용을 무엇으로 놓았는지 적어두지 않으면 나중에 자기 결과도 재현하지 못합니다.</li>
</ul>

<h2 id="정리">정리</h2>

<ul>
  <li>변동성은 √기간으로 연율화하며, 상승과 하락을 구분하지 못한다는 한계가 있다.</li>
  <li>MDD는 깊이를, 회복 기간은 지속 시간을 재며 둘 다 봐야 한다.</li>
  <li>샤프는 위험 한 단위당 초과수익이고, 소르티노는 하락 위험만, 칼마는 최대 낙폭 대비 수익을 본다.</li>
  <li>지표는 항상 여러 개를 벤치마크와 나란히 놓고 읽는다.</li>
</ul>

<p>다음 글에서는 백테스트를 조용히 망가뜨리는 <strong>미래 참조 편향(look-ahead bias)</strong>을 본격적으로 해부합니다. <code class="language-plaintext highlighter-rouge">shift(1)</code>을 빼먹는 것 말고도 미래 정보가 새어 들어오는 경로가 여럿 있는데, 재무제표 발표 시차와 지수 편입 시점처럼 알아채기 어려운 사례를 중심으로 다루겠습니다.</p>

<hr />

<p><em>이 글은 학습과 정보 공유 목적으로 작성되었으며, 특정 종목이나 상품에 대한 투자 권유가 아닙니다. 투자의 판단과 책임은 투자자 본인에게 있습니다.</em></p>]]></content><author><name>퀀트 노트</name></author><category term="퀀트투자" /><category term="위험지표" /><category term="샤프비율" /><category term="MDD" /><summary type="html"><![CDATA[지금까지 전략 성과를 볼 때 CAGR과 MDD 두 가지만 썼습니다. 오늘은 이 도구함을 제대로 채웁니다. 수익률 하나만 보고 전략을 고르는 것은 자동차를 최고 속도만 보고 고르는 것과 같습니다. 브레이크와 안전벨트를 함께 봐야 합니다.]]></summary></entry><entry><title type="html">퀀트 투자 입문 ⑦ — 과최적화, 가장 유혹적인 함정</title><link href="https://quant-note.com/posts/overfitting-in-backtesting/" rel="alternate" type="text/html" title="퀀트 투자 입문 ⑦ — 과최적화, 가장 유혹적인 함정" /><published>2026-09-02T00:00:00+09:00</published><updated>2026-09-02T00:00:00+09:00</updated><id>https://quant-note.com/posts/overfitting-in-backtesting</id><content type="html" xml:base="https://quant-note.com/posts/overfitting-in-backtesting/"><![CDATA[<p>지난 글 마지막에 고백한 것이 있습니다. 절대 모멘텀 백테스트에서 모멘텀 기간을 12개월로 정한 것은 <strong>제가 그냥 정한 것</strong>이었습니다. 그러면 6개월, 9개월, 3개월도 다 돌려보고 제일 성과가 좋은 것을 고르면 되지 않을까요? 오늘은 그 생각이 왜 위험한지, 그리고 대신 무엇을 해야 하는지 다룹니다.</p>

<h2 id="과최적화란-무엇인가">과최적화란 무엇인가</h2>

<p>과최적화(overfitting)는 <strong>모형이 데이터의 진짜 패턴이 아니라 그 데이터에만 있는 우연까지 외워버리는 것</strong>입니다.</p>

<p>시험공부에 비유하면 명확합니다. 기출문제를 이해하며 푼 학생과, 기출문제의 답 순서를 통째로 외운 학생이 있습니다. 기출문제로 시험을 보면 후자가 만점입니다. 하지만 새 문제를 주면 후자는 무너집니다. 백테스트에서 파라미터를 이리저리 바꿔가며 최고 성과를 찾는 행위가 바로 답 순서를 외우는 쪽입니다.</p>

<p>문제는 이 행위가 <strong>너무나 자연스럽고 재미있다</strong>는 것입니다. 숫자 하나 바꿨더니 CAGR이 1%p 올라가는 경험을 하면 멈추기 어렵습니다. 그래서 과최적화는 실력이 늘수록 오히려 더 위험해지는 함정입니다.</p>

<h2 id="실험-파라미터를-전부-돌려보기">실험: 파라미터를 전부 돌려보기</h2>

<p>말로만 하면 와닿지 않으니 직접 돌려봅니다. 모멘텀 기간을 1개월부터 24개월까지 바꿔가며 성과를 전부 계산합니다.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">FinanceDataReader</span> <span class="k">as</span> <span class="n">fdr</span>
<span class="kn">import</span> <span class="nn">pandas</span> <span class="k">as</span> <span class="n">pd</span>

<span class="n">kospi</span> <span class="o">=</span> <span class="n">fdr</span><span class="p">.</span><span class="n">DataReader</span><span class="p">(</span><span class="s">'KS11'</span><span class="p">,</span> <span class="s">'2005-01-01'</span><span class="p">)[</span><span class="s">'Close'</span><span class="p">]</span>
<span class="n">monthly</span> <span class="o">=</span> <span class="n">kospi</span><span class="p">.</span><span class="n">resample</span><span class="p">(</span><span class="s">'ME'</span><span class="p">).</span><span class="n">last</span><span class="p">()</span>
<span class="n">returns</span> <span class="o">=</span> <span class="n">monthly</span><span class="p">.</span><span class="n">pct_change</span><span class="p">()</span>

<span class="k">def</span> <span class="nf">backtest</span><span class="p">(</span><span class="n">lookback</span><span class="p">,</span> <span class="n">start</span><span class="o">=</span><span class="bp">None</span><span class="p">,</span> <span class="n">end</span><span class="o">=</span><span class="bp">None</span><span class="p">,</span> <span class="n">cost</span><span class="o">=</span><span class="mf">0.003</span><span class="p">):</span>
    <span class="s">"""절대 모멘텀 전략의 연평균 복리 수익률(CAGR)을 반환"""</span>
    <span class="n">signal</span> <span class="o">=</span> <span class="p">(</span><span class="n">monthly</span><span class="p">.</span><span class="n">pct_change</span><span class="p">(</span><span class="n">lookback</span><span class="p">)</span> <span class="o">&gt;</span> <span class="mi">0</span><span class="p">).</span><span class="n">astype</span><span class="p">(</span><span class="nb">int</span><span class="p">)</span>
    <span class="n">position</span> <span class="o">=</span> <span class="n">signal</span><span class="p">.</span><span class="n">shift</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span>                 <span class="c1"># 미래 참조 방지
</span>    <span class="n">trades</span> <span class="o">=</span> <span class="n">position</span><span class="p">.</span><span class="n">diff</span><span class="p">().</span><span class="nb">abs</span><span class="p">().</span><span class="n">fillna</span><span class="p">(</span><span class="mi">0</span><span class="p">)</span>   <span class="c1"># 매매가 일어난 시점
</span>    <span class="n">net</span> <span class="o">=</span> <span class="p">(</span><span class="n">position</span> <span class="o">*</span> <span class="n">returns</span> <span class="o">-</span> <span class="n">trades</span> <span class="o">*</span> <span class="n">cost</span><span class="p">).</span><span class="n">dropna</span><span class="p">()</span>

    <span class="k">if</span> <span class="n">start</span> <span class="ow">is</span> <span class="ow">not</span> <span class="bp">None</span><span class="p">:</span>
        <span class="n">net</span> <span class="o">=</span> <span class="n">net</span><span class="p">[</span><span class="n">net</span><span class="p">.</span><span class="n">index</span> <span class="o">&gt;=</span> <span class="n">start</span><span class="p">]</span>
    <span class="k">if</span> <span class="n">end</span> <span class="ow">is</span> <span class="ow">not</span> <span class="bp">None</span><span class="p">:</span>
        <span class="n">net</span> <span class="o">=</span> <span class="n">net</span><span class="p">[</span><span class="n">net</span><span class="p">.</span><span class="n">index</span> <span class="o">&lt;=</span> <span class="n">end</span><span class="p">]</span>
    <span class="k">if</span> <span class="nb">len</span><span class="p">(</span><span class="n">net</span><span class="p">)</span> <span class="o">==</span> <span class="mi">0</span><span class="p">:</span>
        <span class="k">return</span> <span class="nb">float</span><span class="p">(</span><span class="s">'nan'</span><span class="p">)</span>

    <span class="n">cum</span> <span class="o">=</span> <span class="p">(</span><span class="mi">1</span> <span class="o">+</span> <span class="n">net</span><span class="p">).</span><span class="n">cumprod</span><span class="p">()</span>
    <span class="k">return</span> <span class="n">cum</span><span class="p">.</span><span class="n">iloc</span><span class="p">[</span><span class="o">-</span><span class="mi">1</span><span class="p">]</span> <span class="o">**</span> <span class="p">(</span><span class="mi">12</span> <span class="o">/</span> <span class="nb">len</span><span class="p">(</span><span class="n">net</span><span class="p">))</span> <span class="o">-</span> <span class="mi">1</span>

<span class="n">scores</span> <span class="o">=</span> <span class="n">pd</span><span class="p">.</span><span class="n">Series</span><span class="p">({</span><span class="n">lb</span><span class="p">:</span> <span class="n">backtest</span><span class="p">(</span><span class="n">lb</span><span class="p">)</span> <span class="k">for</span> <span class="n">lb</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mi">25</span><span class="p">)})</span>
<span class="k">print</span><span class="p">(</span><span class="n">scores</span><span class="p">.</span><span class="nb">round</span><span class="p">(</span><span class="mi">4</span><span class="p">))</span>
<span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"</span><span class="se">\n</span><span class="s">최고 성과: </span><span class="si">{</span><span class="n">scores</span><span class="p">.</span><span class="n">idxmax</span><span class="p">()</span><span class="si">}</span><span class="s">개월 (CAGR </span><span class="si">{</span><span class="n">scores</span><span class="p">.</span><span class="nb">max</span><span class="p">()</span><span class="si">:</span><span class="p">.</span><span class="mi">2</span><span class="o">%</span><span class="si">}</span><span class="s">)"</span><span class="p">)</span>

<span class="n">scores</span><span class="p">.</span><span class="n">plot</span><span class="p">(</span><span class="n">kind</span><span class="o">=</span><span class="s">'bar'</span><span class="p">,</span> <span class="n">figsize</span><span class="o">=</span><span class="p">(</span><span class="mi">12</span><span class="p">,</span> <span class="mi">4</span><span class="p">),</span> <span class="n">title</span><span class="o">=</span><span class="s">'Lookback vs CAGR'</span><span class="p">)</span>
</code></pre></div></div>

<p>돌려보면 성과가 가장 좋은 개월 수가 하나 나옵니다. 여기서 “찾았다!”고 생각하는 순간이 위험한 순간입니다.</p>

<p>실제 코스피 데이터(2005년 1월~2026년 8월, 비용 0.3% 반영)로 돌린 결과는 이렇습니다.</p>

<p><img src="/assets/images/backtests/03_lookback_sweep.png" alt="파라미터별 CAGR" /></p>

<table>
  <thead>
    <tr>
      <th>개월</th>
      <th>CAGR</th>
      <th>개월</th>
      <th>CAGR</th>
      <th>개월</th>
      <th>CAGR</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>1</td>
      <td>1.7%</td>
      <td>9</td>
      <td>6.9%</td>
      <td>17</td>
      <td>5.7%</td>
    </tr>
    <tr>
      <td>2</td>
      <td>4.1%</td>
      <td>10</td>
      <td>7.7%</td>
      <td>18</td>
      <td>5.1%</td>
    </tr>
    <tr>
      <td><strong>3</strong></td>
      <td><strong>9.5%</strong></td>
      <td>11</td>
      <td>5.3%</td>
      <td>19</td>
      <td>3.1%</td>
    </tr>
    <tr>
      <td>4</td>
      <td>6.6%</td>
      <td>12</td>
      <td>5.4%</td>
      <td>20</td>
      <td>3.6%</td>
    </tr>
    <tr>
      <td>5</td>
      <td>8.4%</td>
      <td>13</td>
      <td>5.8%</td>
      <td>21</td>
      <td>2.6%</td>
    </tr>
    <tr>
      <td>6</td>
      <td>6.5%</td>
      <td>14</td>
      <td>5.1%</td>
      <td>22</td>
      <td>1.6%</td>
    </tr>
    <tr>
      <td>7</td>
      <td>6.8%</td>
      <td>15</td>
      <td>6.1%</td>
      <td>23</td>
      <td>1.8%</td>
    </tr>
    <tr>
      <td>8</td>
      <td>6.1%</td>
      <td>16</td>
      <td>5.4%</td>
      <td>24</td>
      <td>3.7%</td>
    </tr>
  </tbody>
</table>

<p>최고는 <strong>3개월(9.5%)</strong>입니다. 지난 글에서 임의로 골랐던 12개월(5.4%)보다 4%p 높습니다. 그런데 막대그래프를 보세요. 3개월 바로 양옆인 2개월은 4.1%, 4개월은 6.6%입니다. <strong>3개월만 뾰족하게 솟아 있습니다.</strong> 이게 아래에서 말할 “절벽 위의 최고점”의 실제 모습입니다. 2개월이었으면 성과가 반토막이었을 규칙을, 미래에도 정확히 3개월이 최적일 거라 믿고 채택할 수 있을까요.</p>

<h2 id="왜-최고-성과를-고르면-안-되는가">왜 최고 성과를 고르면 안 되는가</h2>

<p>이유는 세 가지입니다.</p>

<p><strong>첫째, 24번 시도했다는 사실 자체가 결과를 왜곡합니다.</strong> 동전 24개를 던져서 그중 가장 앞면이 많이 나온 동전을 고르면, 그 동전은 앞면이 잘 나오는 동전처럼 보입니다. 하지만 그건 동전의 성질이 아니라 24번 골랐다는 사실의 결과입니다. 파라미터 탐색도 똑같습니다. 여러 번 시도한 뒤 최고를 고르면, 그 최고 성과에는 <strong>실력과 운이 섞여</strong> 있고 운의 몫은 미래에 재현되지 않습니다.</p>

<p><strong>둘째, 최고점은 대개 절벽 위에 있습니다.</strong> 위 막대그래프를 보세요. 최고 성과를 낸 개월 수의 <strong>양옆</strong>은 어떤가요? 옆 값들도 비슷하게 좋다면 그 구간은 믿을 만합니다. 반면 최고점만 뾰족하게 솟아 있고 한 칸 옆은 형편없다면, 그 봉우리는 데이터의 우연일 가능성이 큽니다. 미래에 그 정확한 값이 계속 최고일 이유가 없습니다.</p>

<p><strong>셋째, 고르는 행위 자체가 미래 정보를 쓰는 것입니다.</strong> 2005~2026년 전체 데이터를 보고 “17개월이 최고였다”고 정하는 것은, 2005년의 나에게는 불가능한 판단입니다. 이것도 넓은 의미의 미래 참조입니다.</p>

<h2 id="실제로-얼마나-무너지는지-확인하기">실제로 얼마나 무너지는지 확인하기</h2>

<p>데이터를 반으로 갈라서, 앞부분에서 고른 최적값이 뒷부분에서도 통하는지 봅니다. 이것이 <strong>홀드아웃(hold-out) 검증</strong>의 가장 단순한 형태입니다.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">MID</span> <span class="o">=</span> <span class="s">'2015-12-31'</span>

<span class="c1"># 전반부(설계 구간)에서 최적 파라미터 찾기
</span><span class="n">in_sample</span> <span class="o">=</span> <span class="n">pd</span><span class="p">.</span><span class="n">Series</span><span class="p">({</span><span class="n">lb</span><span class="p">:</span> <span class="n">backtest</span><span class="p">(</span><span class="n">lb</span><span class="p">,</span> <span class="n">end</span><span class="o">=</span><span class="n">MID</span><span class="p">)</span> <span class="k">for</span> <span class="n">lb</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mi">25</span><span class="p">)})</span>
<span class="n">best</span> <span class="o">=</span> <span class="n">in_sample</span><span class="p">.</span><span class="n">idxmax</span><span class="p">()</span>
<span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"전반부 최적: </span><span class="si">{</span><span class="n">best</span><span class="si">}</span><span class="s">개월, CAGR </span><span class="si">{</span><span class="n">in_sample</span><span class="p">.</span><span class="nb">max</span><span class="p">()</span><span class="si">:</span><span class="p">.</span><span class="mi">2</span><span class="o">%</span><span class="si">}</span><span class="s">"</span><span class="p">)</span>

<span class="c1"># 그 파라미터를 후반부(검증 구간)에 그대로 적용
</span><span class="n">oos</span> <span class="o">=</span> <span class="n">backtest</span><span class="p">(</span><span class="n">best</span><span class="p">,</span> <span class="n">start</span><span class="o">=</span><span class="n">MID</span><span class="p">)</span>
<span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"후반부 실제 성과: </span><span class="si">{</span><span class="n">oos</span><span class="si">:</span><span class="p">.</span><span class="mi">2</span><span class="o">%</span><span class="si">}</span><span class="s">"</span><span class="p">)</span>

<span class="c1"># 비교: 전반부에서 '평범했던' 12개월은 후반부에서 어땠나
</span><span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"12개월 후반부 성과: </span><span class="si">{</span><span class="n">backtest</span><span class="p">(</span><span class="mi">12</span><span class="p">,</span> <span class="n">start</span><span class="o">=</span><span class="n">MID</span><span class="p">)</span><span class="si">:</span><span class="p">.</span><span class="mi">2</span><span class="o">%</span><span class="si">}</span><span class="s">"</span><span class="p">)</span>
</code></pre></div></div>

<p>실제 결과는 이렇습니다.</p>

<table>
  <thead>
    <tr>
      <th> </th>
      <th>CAGR</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>전반부(2005~2015) 최적 파라미터</td>
      <td><strong>3개월</strong>, 7.17%</td>
    </tr>
    <tr>
      <td>그 3개월을 후반부(2016~2026)에 적용</td>
      <td>11.65%</td>
    </tr>
    <tr>
      <td>비교: 12개월의 후반부 성과</td>
      <td>9.22%</td>
    </tr>
    <tr>
      <td>비교: Buy &amp; Hold 후반부 성과</td>
      <td>12.39%</td>
    </tr>
  </tbody>
</table>

<p>여기서 정직해져야 합니다. 이 실험에서는 <strong>전반부 최적값이 후반부에서 무너지지 않았습니다.</strong> 3개월은 후반부에서도 12개월보다 나았습니다. “과최적화된 파라미터는 반드시 붕괴한다”는 교과서적 결말이 나오지 않은 겁니다.</p>

<p>그래서 이 결과를 어떻게 읽어야 할까요. 세 가지입니다.</p>

<p>첫째, 홀드아웃을 <strong>한 번</strong> 한 것은 표본이 하나인 실험입니다. 갈라지는 지점을 2013년이나 2018년으로 바꾸면 결론이 달라질 수 있습니다. 한 번의 검증 통과가 “이 파라미터는 진짜다”의 증명이 되지는 않습니다.</p>

<p>둘째, 살아남았다고 해서 뾰족한 봉우리가 고원이 되는 것은 아닙니다. 3개월은 여전히 양옆이 절벽인 값이고, 3개월 모멘텀은 학계에서도 단기 반전 효과와 뒤섞이는 불안정한 구간으로 알려져 있습니다. 후반부에 운이 따랐을 가능성을 배제할 수 없습니다.</p>

<p>셋째, 그리고 가장 중요한 것. 최적 파라미터든 아니든, <strong>후반부에는 어떤 개월 수도 Buy &amp; Hold(12.39%)를 이기지 못했습니다.</strong> 파라미터를 아무리 다듬어도 전략 자체의 한계는 넘지 못한 겁니다. 파라미터 탐색에 시간을 쓰기 전에 “이 전략이 비교 대상보다 나은가”를 먼저 물어야 한다는 것을 이 숫자가 보여줍니다.</p>

<p>이 실험은 반드시 한 번 직접 해보시기를 권합니다. 남의 백테스트를 볼 때의 태도가 달라집니다.</p>

<h2 id="과최적화를-피하는-다섯-가지-습관">과최적화를 피하는 다섯 가지 습관</h2>

<ol>
  <li><strong>가설을 먼저, 검증을 나중에</strong> — 데이터를 보기 전에 “왜 이게 통할 것인가”를 문장으로 적습니다. 모멘텀은 정보의 느린 반영 때문에 통한다는 설명이 먼저 있고, 검증은 그다음입니다. 설명 없이 데이터에서 찾아낸 규칙은 대부분 우연입니다.</li>
  <li><strong>파라미터 개수를 줄입니다</strong> — 조정할 수 있는 손잡이가 많을수록 과최적화는 쉬워집니다. 손잡이 하나짜리 단순한 전략이 손잡이 다섯 개짜리보다 대개 더 오래 살아남습니다.</li>
  <li><strong>최고점이 아니라 고원을 고릅니다</strong> — 인접한 파라미터들도 함께 괜찮은 넓은 구간의 가운데 값을 씁니다. 뾰족한 봉우리는 피합니다.</li>
  <li><strong>검증 데이터를 아껴 씁니다</strong> — 홀드아웃 구간을 정했으면 거기서 성과를 확인한 뒤 다시 전반부로 돌아가 규칙을 고치면 안 됩니다. 그 순간 홀드아웃은 오염되어 더 이상 검증 구간이 아닙니다.</li>
  <li><strong>시도 횟수를 세어 둡니다</strong> — 몇 개의 조합을 시험했는지 기록합니다. 100개를 돌려서 찾은 최고 성과는, 3개를 돌려서 찾은 최고 성과보다 훨씬 더 의심해야 합니다.</li>
</ol>

<h2 id="정리">정리</h2>

<ul>
  <li>과최적화는 데이터의 우연까지 외워버리는 것이고, 파라미터를 뒤져 최고를 고르는 순간 시작된다.</li>
  <li>여러 번 시도한 뒤의 최고 성과에는 운이 섞여 있으며, 그 운은 미래에 재현되지 않는다.</li>
  <li>최고점이 뾰족한지 고원인지 확인하고, 데이터를 갈라 홀드아웃 검증을 해본다.</li>
  <li>가설을 먼저 세우고, 파라미터를 줄이고, 시도 횟수를 기록하는 습관이 최선의 방어다.</li>
</ul>

<p>다음 글에서는 지금까지 CAGR과 MDD만으로 봐 온 성과 평가를 확장합니다. <strong>변동성, 샤프비율, 칼마비율</strong>이 각각 무엇을 재는 지표이고, 어떤 상황에서 어떤 지표가 오해를 부르는지 정리하겠습니다.</p>

<hr />

<p><em>이 글은 학습과 정보 공유 목적으로 작성되었으며, 특정 종목이나 상품에 대한 투자 권유가 아닙니다. 투자의 판단과 책임은 투자자 본인에게 있습니다.</em></p>]]></content><author><name>퀀트 노트</name></author><category term="퀀트투자" /><category term="백테스트" /><category term="과최적화" /><category term="검증" /><summary type="html"><![CDATA[지난 글 마지막에 고백한 것이 있습니다. 절대 모멘텀 백테스트에서 모멘텀 기간을 12개월로 정한 것은 제가 그냥 정한 것이었습니다. 그러면 6개월, 9개월, 3개월도 다 돌려보고 제일 성과가 좋은 것을 고르면 되지 않을까요? 오늘은 그 생각이 왜 위험한지, 그리고 대신 무엇을 해야 하는지 다룹니다.]]></summary></entry><entry><title type="html">퀀트 투자 입문 ⑥ — 거래 비용과 슬리피지, 백테스트에 현실 넣기</title><link href="https://quant-note.com/posts/trading-costs-and-slippage/" rel="alternate" type="text/html" title="퀀트 투자 입문 ⑥ — 거래 비용과 슬리피지, 백테스트에 현실 넣기" /><published>2026-09-01T00:00:00+09:00</published><updated>2026-09-01T00:00:00+09:00</updated><id>https://quant-note.com/posts/trading-costs-and-slippage</id><content type="html" xml:base="https://quant-note.com/posts/trading-costs-and-slippage/"><![CDATA[<p>지난 글에서 절대 모멘텀 백테스트를 만들면서, 이 코드에 빠진 것 네 가지를 적어뒀습니다. 오늘은 그중 첫 번째인 <strong>거래 비용</strong>을 채워 넣습니다. 백테스트에서 비용을 빼먹는 것은 초보만의 실수가 아닙니다. 논문에서도 비용을 반영하면 사라지는 전략이 수두룩합니다.</p>

<h2 id="거래-비용은-세-겹이다">거래 비용은 세 겹이다</h2>

<p>“수수료 얼마 안 되는데”라고 생각하기 쉽지만, 실제로 계좌에서 빠져나가는 돈은 세 종류입니다.</p>

<table>
  <thead>
    <tr>
      <th>구분</th>
      <th>성격</th>
      <th>특징</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>위탁수수료</td>
      <td>증권사에 내는 돈</td>
      <td>증권사·계좌 조건에 따라 다름. 협상 가능</td>
    </tr>
    <tr>
      <td>세금</td>
      <td>매도 시 부과</td>
      <td>요율과 대상이 제도 변경에 따라 바뀜</td>
    </tr>
    <tr>
      <td>슬리피지</td>
      <td>시장에 내는 보이지 않는 돈</td>
      <td>명세서에 안 찍히지만 가장 클 수 있음</td>
    </tr>
  </tbody>
</table>

<p>앞의 둘은 명세서에 숫자로 찍히니 확인하기 쉽습니다. 문제는 세 번째입니다.</p>

<p>주의할 점 하나. 수수료율과 세율은 증권사와 제도에 따라 다르고 시간이 지나면 바뀝니다. 아래 코드에서 쓰는 숫자는 <strong>설명을 위한 가정치</strong>이며, 실제로 쓰실 때는 본인 계좌의 수수료율과 현재 시행 중인 세율을 직접 확인해서 넣으셔야 합니다. 백테스트의 정확도는 이런 확인에서 나옵니다.</p>

<h2 id="슬리피지-명세서에-없는-비용">슬리피지: 명세서에 없는 비용</h2>

<p>슬리피지(slippage)는 <strong>내가 기대한 가격과 실제 체결된 가격의 차이</strong>입니다. 원인은 크게 둘입니다.</p>

<p>첫째, <strong>호가 스프레드</strong>입니다. 매수 호가와 매도 호가 사이에는 항상 틈이 있습니다. 사자마자 팔면 그 틈만큼 손해입니다. 유동성이 풍부한 대형주나 ETF는 이 틈이 얇지만, 거래가 뜸한 소형주는 한 호가만 밀려도 0.5% 이상 벌어질 수 있습니다.</p>

<p>둘째, <strong>시장 충격(market impact)</strong>입니다. 내 주문 자체가 가격을 밀어 올립니다. 100만 원어치 사는 것과 10억 원어치 사는 것은 다릅니다. 개인 투자자 규모에서는 대형주라면 무시할 만하지만, 소형주를 다루거나 종가에 몰아서 주문하는 전략이라면 무시할 수 없습니다.</p>

<p>백테스트는 보통 “종가에 정확히 체결됐다”고 가정합니다. 현실에서 종가에 정확히 사는 것은 불가능합니다. 이 간극이 슬리피지입니다.</p>

<h2 id="비용을-코드에-넣기">비용을 코드에 넣기</h2>

<p>지난 글의 절대 모멘텀 백테스트에 비용을 붙여 봅니다. 핵심은 <strong>매매가 실제로 일어난 시점을 찾아내는 것</strong>입니다.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">FinanceDataReader</span> <span class="k">as</span> <span class="n">fdr</span>
<span class="kn">import</span> <span class="nn">pandas</span> <span class="k">as</span> <span class="n">pd</span>

<span class="c1"># 지난 글과 동일한 세팅
</span><span class="n">kospi</span> <span class="o">=</span> <span class="n">fdr</span><span class="p">.</span><span class="n">DataReader</span><span class="p">(</span><span class="s">'KS11'</span><span class="p">,</span> <span class="s">'2005-01-01'</span><span class="p">)[</span><span class="s">'Close'</span><span class="p">]</span>
<span class="n">monthly</span> <span class="o">=</span> <span class="n">kospi</span><span class="p">.</span><span class="n">resample</span><span class="p">(</span><span class="s">'ME'</span><span class="p">).</span><span class="n">last</span><span class="p">()</span>
<span class="n">returns</span> <span class="o">=</span> <span class="n">monthly</span><span class="p">.</span><span class="n">pct_change</span><span class="p">()</span>

<span class="n">signal</span> <span class="o">=</span> <span class="p">(</span><span class="n">monthly</span><span class="p">.</span><span class="n">pct_change</span><span class="p">(</span><span class="mi">12</span><span class="p">)</span> <span class="o">&gt;</span> <span class="mi">0</span><span class="p">).</span><span class="n">astype</span><span class="p">(</span><span class="nb">int</span><span class="p">)</span>
<span class="n">position</span> <span class="o">=</span> <span class="n">signal</span><span class="p">.</span><span class="n">shift</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span>          <span class="c1"># 미래 참조 방지
</span>
<span class="c1"># 포지션이 바뀐 달 = 매매가 일어난 달
</span><span class="n">trades</span> <span class="o">=</span> <span class="n">position</span><span class="p">.</span><span class="n">diff</span><span class="p">().</span><span class="nb">abs</span><span class="p">().</span><span class="n">fillna</span><span class="p">(</span><span class="mi">0</span><span class="p">)</span>

<span class="c1"># 편도 비용 가정치 (수수료 + 세금 + 슬리피지). 반드시 본인 조건으로 교체할 것
</span><span class="n">COST</span> <span class="o">=</span> <span class="mf">0.003</span>   <span class="c1"># 0.3%
</span>
<span class="n">gross</span> <span class="o">=</span> <span class="n">position</span> <span class="o">*</span> <span class="n">returns</span>
<span class="n">net</span> <span class="o">=</span> <span class="n">gross</span> <span class="o">-</span> <span class="n">trades</span> <span class="o">*</span> <span class="n">COST</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">position.diff().abs()</code>가 이 코드의 핵심입니다. 포지션이 0에서 1로(매수) 또는 1에서 0으로(매도) 바뀐 달에만 1이 되고, 그대로 유지된 달에는 0입니다. 즉 <strong>실제로 주문을 낸 달에만 비용을 뺍니다.</strong> 매달 무조건 비용을 빼면 과도하게 보수적인 백테스트가 되고, 아예 안 빼면 낙관적인 백테스트가 됩니다.</p>

<h2 id="얼마나-차이가-나는지-직접-보기">얼마나 차이가 나는지 직접 보기</h2>

<p>비용 전후를 나란히 놓고 비교합니다.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">result</span> <span class="o">=</span> <span class="n">pd</span><span class="p">.</span><span class="n">DataFrame</span><span class="p">({</span>
    <span class="s">'Buy &amp; Hold'</span><span class="p">:</span> <span class="n">returns</span><span class="p">,</span>
    <span class="s">'Momentum (gross)'</span><span class="p">:</span> <span class="n">gross</span><span class="p">,</span>
    <span class="s">'Momentum (net)'</span><span class="p">:</span> <span class="n">net</span>
<span class="p">}).</span><span class="n">dropna</span><span class="p">()</span>

<span class="n">cum</span> <span class="o">=</span> <span class="p">(</span><span class="mi">1</span> <span class="o">+</span> <span class="n">result</span><span class="p">).</span><span class="n">cumprod</span><span class="p">()</span>
<span class="n">n_years</span> <span class="o">=</span> <span class="nb">len</span><span class="p">(</span><span class="n">result</span><span class="p">)</span> <span class="o">/</span> <span class="mi">12</span>
<span class="n">cagr</span> <span class="o">=</span> <span class="n">cum</span><span class="p">.</span><span class="n">iloc</span><span class="p">[</span><span class="o">-</span><span class="mi">1</span><span class="p">]</span> <span class="o">**</span> <span class="p">(</span><span class="mi">1</span> <span class="o">/</span> <span class="n">n_years</span><span class="p">)</span> <span class="o">-</span> <span class="mi">1</span>

<span class="c1"># 연평균 매매 횟수 (회전율)
</span><span class="n">n_trades</span> <span class="o">=</span> <span class="n">trades</span><span class="p">.</span><span class="n">loc</span><span class="p">[</span><span class="n">result</span><span class="p">.</span><span class="n">index</span><span class="p">].</span><span class="nb">sum</span><span class="p">()</span>
<span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"총 매매 횟수: </span><span class="si">{</span><span class="n">n_trades</span><span class="si">:</span><span class="p">.</span><span class="mi">0</span><span class="n">f</span><span class="si">}</span><span class="s">회, 연평균: </span><span class="si">{</span><span class="n">n_trades</span> <span class="o">/</span> <span class="n">n_years</span><span class="si">:</span><span class="p">.</span><span class="mi">1</span><span class="n">f</span><span class="si">}</span><span class="s">회"</span><span class="p">)</span>
<span class="k">print</span><span class="p">(</span><span class="n">cagr</span><span class="p">.</span><span class="nb">round</span><span class="p">(</span><span class="mi">4</span><span class="p">))</span>

<span class="n">cum</span><span class="p">.</span><span class="n">plot</span><span class="p">(</span><span class="n">figsize</span><span class="o">=</span><span class="p">(</span><span class="mi">12</span><span class="p">,</span> <span class="mi">5</span><span class="p">),</span> <span class="n">title</span><span class="o">=</span><span class="s">'Trading Cost Impact'</span><span class="p">)</span>
</code></pre></div></div>

<p>여기서 봐야 할 숫자가 <strong>연평균 매매 횟수</strong>입니다. 비용이 성과를 갉아먹는 정도는 대략 이렇게 계산됩니다.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>연간 비용 부담 ≈ 연평균 매매 횟수 × 편도 비용
</code></pre></div></div>

<p>연 4회 매매에 편도 0.3%면 연 1.2%입니다. CAGR이 8%인 전략이라면 성과의 15%가 비용으로 사라지는 셈입니다. 매달 종목을 갈아치우는 상대 모멘텀 전략이라면 연 24회(매수 12 + 매도 12)까지 갈 수 있고, 그러면 연 7% 이상이 비용입니다. <strong>이 지점에서 죽는 전략이 정말 많습니다.</strong></p>

<h2 id="실제-결과">실제 결과</h2>

<p>같은 코스피 데이터(2005년 1월~2026년 8월)로 비용 전후를 비교한 결과입니다.</p>

<table>
  <thead>
    <tr>
      <th> </th>
      <th>CAGR</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Buy &amp; Hold</td>
      <td>9.66%</td>
    </tr>
    <tr>
      <td>모멘텀 (비용 전)</td>
      <td>5.91%</td>
    </tr>
    <tr>
      <td>모멘텀 (편도 0.3% 반영)</td>
      <td>5.37%</td>
    </tr>
  </tbody>
</table>

<p>총 매매 횟수는 21년 반 동안 <strong>37회, 연평균 1.71회</strong>였습니다. 비용으로 깎인 성과는 연 0.54%p인데, 앞에서 말한 어림셈 “연평균 매매 횟수 × 편도 비용 = 1.71 × 0.3% = 0.51%p”와 거의 일치합니다. 어림셈이 쓸 만하다는 뜻입니다.</p>

<p>절대 모멘텀은 매매가 드문 전략이라 비용 타격이 작은 편입니다. 그래도 5.91%에서 5.37%로, 원래도 Buy &amp; Hold에 뒤지던 성과가 더 뒤집니다. 매달 종목을 갈아치우는 전략이었다면 연 20회 넘는 매매에 연 6%p 이상이 비용으로 사라졌을 겁니다. 비용을 빼먹은 백테스트가 왜 위험한지, 숫자로 확인한 셈입니다.</p>

<h2 id="비용을-줄이는-방향">비용을 줄이는 방향</h2>

<p>비용을 알고 나면 전략 설계가 달라집니다.</p>

<ul>
  <li><strong>매매를 줄인다</strong> — 신호가 애매할 때 무조건 갈아타지 않고 완충 구간을 두는 방법이 있습니다. 예를 들어 “수익률 &gt; 0”이 아니라 “수익률 &gt; +1%면 매수, &lt; -1%면 매도, 그 사이면 유지”로 바꾸면 휩쏘 구간의 잦은 매매가 줄어듭니다.</li>
  <li><strong>유동성 있는 자산을 쓴다</strong> — 거래대금이 큰 ETF나 대형주는 슬리피지가 작습니다.</li>
  <li><strong>리밸런싱 주기를 늘린다</strong> — 월 1회를 분기 1회로 바꾸면 비용이 3분의 1이 됩니다. 대신 신호 반응이 느려집니다. 이 맞교환을 데이터로 비교해 보는 것 자체가 좋은 백테스트 연습입니다.</li>
  <li><strong>비용을 보수적으로 잡는다</strong> — 실제보다 조금 크게 잡아서도 살아남는 전략만 채택합니다. 비용을 낙관적으로 잡아 통과시킨 전략은 실전에서 반드시 실망시킵니다.</li>
</ul>

<h2 id="정리">정리</h2>

<ul>
  <li>거래 비용은 수수료, 세금, 슬리피지 세 겹이고, 명세서에 안 찍히는 슬리피지가 종종 가장 크다.</li>
  <li>비용은 <code class="language-plaintext highlighter-rouge">position.diff().abs()</code>로 <strong>실제 매매가 일어난 시점</strong>에만 부과해야 한다.</li>
  <li>성과 훼손 정도는 대략 “연평균 매매 횟수 × 편도 비용”이며, 회전율이 높은 전략일수록 치명적이다.</li>
  <li>수수료율·세율은 조건과 제도에 따라 다르므로 반드시 본인 기준으로 확인해서 넣는다.</li>
</ul>

<p>다음 글에서는 두 번째 빠진 조각, <strong>과최적화(오버피팅)</strong>를 다룹니다. 지난 글에서 모멘텀 기간을 12개월로 정한 것은 제 임의였습니다. 6개월, 9개월, 3개월을 다 돌려보고 제일 좋은 것을 고르면 어떻게 될까요? 그게 왜 위험한지 같은 코드로 직접 확인해 보겠습니다.</p>

<hr />

<p><em>이 글은 학습과 정보 공유 목적으로 작성되었으며, 특정 종목이나 상품에 대한 투자 권유가 아닙니다. 투자의 판단과 책임은 투자자 본인에게 있습니다.</em></p>]]></content><author><name>퀀트 노트</name></author><category term="퀀트투자" /><category term="백테스트" /><category term="거래비용" /><category term="슬리피지" /><summary type="html"><![CDATA[지난 글에서 절대 모멘텀 백테스트를 만들면서, 이 코드에 빠진 것 네 가지를 적어뒀습니다. 오늘은 그중 첫 번째인 거래 비용을 채워 넣습니다. 백테스트에서 비용을 빼먹는 것은 초보만의 실수가 아닙니다. 논문에서도 비용을 반영하면 사라지는 전략이 수두룩합니다.]]></summary></entry><entry><title type="html">퀀트 투자 입문 ⑤ — 첫 백테스트: 절대 모멘텀을 코드 30줄로</title><link href="https://quant-note.com/posts/first-backtest-absolute-momentum/" rel="alternate" type="text/html" title="퀀트 투자 입문 ⑤ — 첫 백테스트: 절대 모멘텀을 코드 30줄로" /><published>2026-08-31T00:00:00+09:00</published><updated>2026-08-31T00:00:00+09:00</updated><id>https://quant-note.com/posts/first-backtest-absolute-momentum</id><content type="html" xml:base="https://quant-note.com/posts/first-backtest-absolute-momentum/"><![CDATA[<p>앞의 네 글에서 준비를 마쳤습니다. 데이터를 받는 법, 수익률을 계산하는 법, 백테스트가 무엇인지, 모멘텀이 무엇인지. 이제 그것들을 하나로 합쳐 <strong>실제로 돌아가는 백테스트</strong>를 만듭니다. 코스피 지수 하나에 절대 모멘텀 규칙을 적용하고, 그냥 들고 있기(buy &amp; hold)와 비교합니다.</p>

<h2 id="검증할-규칙">검증할 규칙</h2>

<p>가장 단순한 형태로 정합니다.</p>

<blockquote>
  <p><strong>매월 말, 코스피의 최근 12개월 수익률이 플러스면 다음 달 한 달간 코스피를 보유한다. 마이너스면 현금으로 대피한다(수익률 0).</strong></p>
</blockquote>

<p>백테스트 4단계 중 1단계(규칙 정하기)가 이걸로 끝났습니다. 애매한 단어가 하나도 없다는 점을 확인해 보세요. “12개월 수익률 &gt; 0”은 누가 계산해도 같은 답이 나옵니다.</p>

<h2 id="2단계-데이터-준비">2단계: 데이터 준비</h2>

<p>월 단위 규칙이므로 월말 종가만 있으면 됩니다.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">FinanceDataReader</span> <span class="k">as</span> <span class="n">fdr</span>
<span class="kn">import</span> <span class="nn">pandas</span> <span class="k">as</span> <span class="n">pd</span>

<span class="c1"># 코스피 지수 일별 종가 → 월말 종가
</span><span class="n">kospi</span> <span class="o">=</span> <span class="n">fdr</span><span class="p">.</span><span class="n">DataReader</span><span class="p">(</span><span class="s">'KS11'</span><span class="p">,</span> <span class="s">'2005-01-01'</span><span class="p">)[</span><span class="s">'Close'</span><span class="p">]</span>
<span class="n">monthly</span> <span class="o">=</span> <span class="n">kospi</span><span class="p">.</span><span class="n">resample</span><span class="p">(</span><span class="s">'ME'</span><span class="p">).</span><span class="n">last</span><span class="p">()</span>

<span class="k">print</span><span class="p">(</span><span class="n">monthly</span><span class="p">.</span><span class="n">tail</span><span class="p">())</span>
<span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"월 데이터 개수: </span><span class="si">{</span><span class="nb">len</span><span class="p">(</span><span class="n">monthly</span><span class="p">)</span><span class="si">}</span><span class="s">"</span><span class="p">)</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">resample('ME')</code>는 월말(Month End) 기준으로 묶으라는 뜻이고, <code class="language-plaintext highlighter-rouge">.last()</code>로 그달의 마지막 값을 취합니다. 2005년부터 잡은 이유는 2008년 금융위기와 2020년 코로나 폭락을 모두 포함시키기 위해서입니다. 앞 글에서 말한 “최소한 큰 하락장을 한 번 이상 포함하라”는 조건입니다.</p>

<h2 id="3단계-규칙을-기계적으로-적용">3단계: 규칙을 기계적으로 적용</h2>

<p>여기가 백테스트의 심장입니다. 코드는 다섯 줄이지만 한 줄 한 줄이 중요합니다.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># 월별 수익률
</span><span class="n">returns</span> <span class="o">=</span> <span class="n">monthly</span><span class="p">.</span><span class="n">pct_change</span><span class="p">()</span>

<span class="c1"># 신호: 최근 12개월 수익률이 플러스인가?
</span><span class="n">momentum</span> <span class="o">=</span> <span class="n">monthly</span><span class="p">.</span><span class="n">pct_change</span><span class="p">(</span><span class="mi">12</span><span class="p">)</span>
<span class="n">signal</span> <span class="o">=</span> <span class="p">(</span><span class="n">momentum</span> <span class="o">&gt;</span> <span class="mi">0</span><span class="p">).</span><span class="n">astype</span><span class="p">(</span><span class="nb">int</span><span class="p">)</span>   <span class="c1"># 1 = 보유, 0 = 현금
</span>
<span class="c1"># 핵심: 이번 달 말에 관측한 신호는 '다음 달'에 적용된다
</span><span class="n">position</span> <span class="o">=</span> <span class="n">signal</span><span class="p">.</span><span class="n">shift</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span>

<span class="c1"># 전략의 월별 수익률
</span><span class="n">strategy_returns</span> <span class="o">=</span> <span class="n">position</span> <span class="o">*</span> <span class="n">returns</span>
</code></pre></div></div>

<h3 id="shift1이-없으면-백테스트는-무효입니다"><code class="language-plaintext highlighter-rouge">shift(1)</code>이 없으면 백테스트는 무효입니다</h3>

<p><code class="language-plaintext highlighter-rouge">position = signal.shift(1)</code> 이 한 줄이 이 코드 전체에서 가장 중요합니다.</p>

<p><code class="language-plaintext highlighter-rouge">signal</code>은 <strong>그달 말 종가를 봐야</strong> 계산됩니다. 8월 31일 종가를 알아야 8월의 12개월 모멘텀을 알 수 있죠. 그런데 <code class="language-plaintext highlighter-rouge">returns</code>의 8월 값은 <strong>8월 한 달 동안의 수익률</strong>입니다. 이 둘을 그냥 곱하면 어떻게 될까요? “8월이 끝난 뒤에 알게 된 정보로, 이미 지나간 8월에 투자한다”는 시간여행이 됩니다.</p>

<p><code class="language-plaintext highlighter-rouge">shift(1)</code>은 신호를 한 칸 뒤로 미뤄서 “8월 말에 본 신호 → 9월 수익률에 적용”으로 맞춰줍니다. 이것이 앞 글에서 말한 <strong>미래 참조 편향(look-ahead bias)</strong>을 막는 장치입니다. 이 한 줄을 빼면 백테스트 성과가 마법처럼 좋아지는데, 그건 실력이 아니라 버그입니다.</p>

<p>시험 삼아 <code class="language-plaintext highlighter-rouge">shift(1)</code>을 지우고 돌려보세요. 성과가 얼마나 부풀려지는지 직접 보면 이 개념을 평생 잊지 않게 됩니다.</p>

<h2 id="4단계-성과-평가">4단계: 성과 평가</h2>

<p>이제 buy &amp; hold와 나란히 놓고 비교합니다.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">result</span> <span class="o">=</span> <span class="n">pd</span><span class="p">.</span><span class="n">DataFrame</span><span class="p">({</span>
    <span class="s">'Buy &amp; Hold'</span><span class="p">:</span> <span class="n">returns</span><span class="p">,</span>
    <span class="s">'Momentum'</span><span class="p">:</span> <span class="n">strategy_returns</span>
<span class="p">}).</span><span class="n">dropna</span><span class="p">()</span>

<span class="n">cum</span> <span class="o">=</span> <span class="p">(</span><span class="mi">1</span> <span class="o">+</span> <span class="n">result</span><span class="p">).</span><span class="n">cumprod</span><span class="p">()</span>

<span class="c1"># 연평균 복리 수익률 (CAGR)
</span><span class="n">n_years</span> <span class="o">=</span> <span class="nb">len</span><span class="p">(</span><span class="n">result</span><span class="p">)</span> <span class="o">/</span> <span class="mi">12</span>
<span class="n">cagr</span> <span class="o">=</span> <span class="n">cum</span><span class="p">.</span><span class="n">iloc</span><span class="p">[</span><span class="o">-</span><span class="mi">1</span><span class="p">]</span> <span class="o">**</span> <span class="p">(</span><span class="mi">1</span> <span class="o">/</span> <span class="n">n_years</span><span class="p">)</span> <span class="o">-</span> <span class="mi">1</span>

<span class="c1"># 최대 낙폭 (MDD): 고점 대비 얼마나 깊이 떨어졌나
</span><span class="k">def</span> <span class="nf">max_drawdown</span><span class="p">(</span><span class="n">cum_series</span><span class="p">):</span>
    <span class="n">peak</span> <span class="o">=</span> <span class="n">cum_series</span><span class="p">.</span><span class="n">cummax</span><span class="p">()</span>
    <span class="k">return</span> <span class="p">((</span><span class="n">cum_series</span> <span class="o">/</span> <span class="n">peak</span><span class="p">)</span> <span class="o">-</span> <span class="mi">1</span><span class="p">).</span><span class="nb">min</span><span class="p">()</span>

<span class="n">mdd</span> <span class="o">=</span> <span class="n">cum</span><span class="p">.</span><span class="nb">apply</span><span class="p">(</span><span class="n">max_drawdown</span><span class="p">)</span>

<span class="k">print</span><span class="p">(</span><span class="n">pd</span><span class="p">.</span><span class="n">DataFrame</span><span class="p">({</span><span class="s">'CAGR'</span><span class="p">:</span> <span class="n">cagr</span><span class="p">,</span> <span class="s">'MDD'</span><span class="p">:</span> <span class="n">mdd</span><span class="p">}).</span><span class="nb">round</span><span class="p">(</span><span class="mi">4</span><span class="p">))</span>
<span class="n">cum</span><span class="p">.</span><span class="n">plot</span><span class="p">(</span><span class="n">figsize</span><span class="o">=</span><span class="p">(</span><span class="mi">12</span><span class="p">,</span> <span class="mi">5</span><span class="p">),</span> <span class="n">title</span><span class="o">=</span><span class="s">'Absolute Momentum vs Buy &amp; Hold (KOSPI)'</span><span class="p">)</span>
</code></pre></div></div>

<p>여기서 처음 등장한 지표가 <strong>MDD(Maximum Drawdown, 최대 낙폭)</strong>입니다. 전 고점 대비 가장 깊이 떨어졌던 폭으로, “이 전략을 들고 있었다면 최악의 순간에 계좌가 얼마나 녹아 있었나”를 뜻합니다. 수익률만큼, 어쩌면 수익률보다 중요한 숫자입니다. CAGR이 아무리 좋아도 MDD가 -60%인 전략은 실제로는 대부분의 사람이 중간에 포기하기 때문입니다.</p>

<h2 id="실제로-돌려본-결과">실제로 돌려본 결과</h2>

<p>위 코드를 실제 코스피 지수 월말 종가(2005년 1월~2026년 8월, 260개월, FinanceDataReader)로 돌린 결과입니다. 비용은 아직 반영하지 않았습니다.</p>

<table>
  <thead>
    <tr>
      <th> </th>
      <th>Buy &amp; Hold</th>
      <th>절대 모멘텀(12개월)</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>CAGR</td>
      <td>9.66%</td>
      <td>5.91%</td>
    </tr>
    <tr>
      <td>연 변동성</td>
      <td>22.5%</td>
      <td>18.2%</td>
    </tr>
    <tr>
      <td>MDD</td>
      <td>-48.5%</td>
      <td>-30.5%</td>
    </tr>
    <tr>
      <td>최장 침체 기간</td>
      <td>71개월</td>
      <td>115개월</td>
    </tr>
  </tbody>
</table>

<p><img src="/assets/images/backtests/01_cumulative.png" alt="절대 모멘텀 vs Buy &amp; Hold 누적 수익률" /></p>

<p>솔직히 말하면 예상보다 모멘텀 쪽이 훨씬 초라합니다. 연 수익률에서 3.7%p를 내줬고, 21년 반 동안 누적으로는 Buy &amp; Hold가 약 7.4배, 모멘텀은 약 3.5배입니다. 절반도 안 됩니다.</p>

<p>대신 약속한 것은 지켰습니다. 2008년 금융위기에서 Buy &amp; Hold가 -48.5%까지 빠질 때 모멘텀은 -30.5%에서 멈췄습니다. 변동성도 4%p 낮습니다. “큰 하락장을 피한다”는 목적 자체는 달성한 겁니다.</p>

<p>그런데 표에서 가장 놀란 숫자는 <strong>최장 침체 기간</strong>입니다. 모멘텀이 115개월, 거의 10년입니다. Buy &amp; Hold의 71개월보다 훨씬 깁니다. 낙폭이 얕은 전략이 왜 물속에 더 오래 있었을까요? 아래 낙폭 그래프를 보면 이유가 보입니다.</p>

<p><img src="/assets/images/backtests/02_drawdown.png" alt="낙폭 비교" /></p>

<p>모멘텀의 낙폭 선에 <strong>수평으로 멈춘 구간</strong>들이 있습니다. 현금으로 대피해 있는 기간입니다. 그 사이 시장은 반등해서 Buy &amp; Hold의 낙폭은 회복되는데, 모멘텀은 현금에 앉아 있으니 낙폭이 그 자리에 얼어 있습니다. 2011년부터 2017년까지 코스피가 박스권에서 오르내리던 시기에 모멘텀은 샀다 팔았다를 반복하며 계속 물속에 있었습니다. 이것이 앞 글에서 말한 <strong>휩쏘</strong>의 실제 모습입니다.</p>

<p>그리고 그래프 오른쪽 끝, 2024년 이후의 급등 구간에서 격차가 결정적으로 벌어집니다. 모멘텀도 상승에 올라타긴 했지만(1.3배→3.9배) 한 박자 늦게 진입한 대가가 컸습니다. 모멘텀의 구조적 약점인 “급격한 방향 전환에 늦다”가 그대로 나타났습니다.</p>

<p>이 결과는 코스피라는 하나의 시장, 21년이라는 하나의 표본에서 나온 것입니다. 다른 시장이나 다른 기간이라면 다를 수 있습니다. 다만 “절대 모멘텀은 수익을 올려주는 전략”이 아니라 <strong>“수익의 일부를 내주고 최악의 낙폭을 사는 보험”</strong>이라는 성격은 분명하게 드러났다고 봅니다. 그 보험료가 연 3.7%p라면 비싼지 싼지는 사람마다 답이 다를 겁니다.</p>

<h2 id="결과를-읽는-법">결과를 읽는 법</h2>

<p>위 결과를 볼 때 던져야 할 질문을 정리합니다.</p>

<ul>
  <li><strong>CAGR이 buy &amp; hold보다 높은가?</strong> 낮아도 실패가 아닙니다. 다음 질문을 함께 봐야 합니다.</li>
  <li><strong>MDD가 얼마나 줄었는가?</strong> 절대 모멘텀의 존재 이유는 수익 극대화가 아니라 <strong>큰 하락장 회피</strong>입니다. CAGR이 비슷한데 MDD가 절반이면 훌륭한 결과입니다.</li>
  <li><strong>누적 수익률 곡선의 모양은?</strong> 2008년과 2020년 구간에서 두 선이 어떻게 갈라지는지 보세요. 모멘텀 전략이 폭락을 피했다면 그 지점에서 선이 벌어집니다.</li>
  <li><strong>반대로 어디서 손해를 봤는가?</strong> 급반등 구간(2009년, 2020년 4~5월)에서는 현금에 앉아 있느라 상승을 놓쳤을 겁니다. 앞 글에서 말한 모멘텀의 구조적 약점이 실제 데이터에서 어떻게 나타나는지 확인하는 순간입니다.</li>
</ul>

<h2 id="이-백테스트에-아직-없는-것들">이 백테스트에 아직 없는 것들</h2>

<p>축하하지 말고 의심하라고 했었죠. 지금 이 30줄에는 현실이 여럿 빠져 있습니다.</p>

<ol>
  <li><strong>거래 비용</strong> — 매수·매도 수수료와 세금, 슬리피지가 전혀 없습니다. 신호가 자주 바뀌는 구간에서는 이 비용이 성과를 상당히 갉아먹습니다.</li>
  <li><strong>현금의 수익률</strong> — 대피했을 때 수익률을 0으로 뒀지만, 실제로는 예금이나 단기 채권에 넣어 이자를 받습니다. 이건 오히려 성과를 과소평가한 부분입니다.</li>
  <li><strong>지수는 살 수 없다</strong> — 코스피 지수 자체는 매매할 수 없고, 실제로는 ETF를 삽니다. ETF는 운용보수가 있고 지수를 완벽히 따라가지도 않습니다.</li>
  <li><strong>파라미터 12개월은 어디서 왔나</strong> — 제가 그냥 정했습니다. 6개월, 9개월로 바꾸면 결과가 달라지고, “가장 성과 좋은 개월 수”를 찾아 헤매는 순간 과최적화가 시작됩니다.</li>
</ol>

<p>특히 4번은 다음 단계로 넘어가기 전에 반드시 짚어야 할 주제입니다.</p>

<h2 id="정리">정리</h2>

<ul>
  <li>절대 모멘텀 백테스트의 뼈대는 <code class="language-plaintext highlighter-rouge">pct_change</code> → 신호 → <strong><code class="language-plaintext highlighter-rouge">shift(1)</code></strong> → 곱하기, 네 단계다.</li>
  <li><code class="language-plaintext highlighter-rouge">shift(1)</code>은 미래 참조를 막는 장치이고, 이 한 줄이 백테스트의 유효성을 결정한다.</li>
  <li>성과는 CAGR만이 아니라 MDD와 곡선의 모양까지 함께, 반드시 buy &amp; hold와 비교해서 읽는다.</li>
  <li>첫 백테스트에는 거래 비용, 파라미터 선택 문제 등 현실이 빠져 있다.</li>
</ul>

<p>다음 글에서는 이 빠진 것들을 하나씩 채워 넣습니다. <strong>거래 비용을 반영하면 성과가 얼마나 달라지는지</strong>, 그리고 <strong>파라미터를 바꿔가며 최고 성과를 찾는 것이 왜 위험한지</strong>를 같은 코드로 직접 확인해 보겠습니다.</p>

<hr />

<p><em>이 글은 학습과 정보 공유 목적으로 작성되었으며, 특정 종목이나 상품에 대한 투자 권유가 아닙니다. 투자의 판단과 책임은 투자자 본인에게 있습니다.</em></p>]]></content><author><name>퀀트 노트</name></author><category term="퀀트투자" /><category term="백테스트" /><category term="모멘텀" /><category term="pandas" /><summary type="html"><![CDATA[앞의 네 글에서 준비를 마쳤습니다. 데이터를 받는 법, 수익률을 계산하는 법, 백테스트가 무엇인지, 모멘텀이 무엇인지. 이제 그것들을 하나로 합쳐 실제로 돌아가는 백테스트를 만듭니다. 코스피 지수 하나에 절대 모멘텀 규칙을 적용하고, 그냥 들고 있기(buy &amp; hold)와 비교합니다.]]></summary></entry><entry><title type="html">퀀트 투자 입문 ③ — 백테스트란 무엇인가</title><link href="https://quant-note.com/posts/what-is-backtesting/" rel="alternate" type="text/html" title="퀀트 투자 입문 ③ — 백테스트란 무엇인가" /><published>2026-08-31T00:00:00+09:00</published><updated>2026-08-31T00:00:00+09:00</updated><id>https://quant-note.com/posts/what-is-backtesting</id><content type="html" xml:base="https://quant-note.com/posts/what-is-backtesting/"><![CDATA[<p>지난 글에서 수익률 계산을 배웠으니 이제 백테스트를 만들 차례인데, 그 전에 백테스트가 정확히 무엇인지부터 제대로 짚고 갑니다. 이번 글은 코드가 없는 개념 편입니다. 다음 글들에서 코드를 쓸 때 “지금 뭘 하고 있는 건지” 헷갈리지 않기 위한 준비입니다.</p>

<h2 id="백테스트란-무엇인가">백테스트란 무엇인가</h2>

<p>백테스트(backtest)는 한 문장으로 이렇게 정의할 수 있습니다.</p>

<blockquote>
  <p><strong>“이 규칙대로 과거에 투자했다면 어떻게 됐을까?”를 데이터로 재현해 보는 것.</strong></p>
</blockquote>

<p>예를 들어 “매달 말에 최근 6개월 수익률이 플러스면 주식을 사고, 마이너스면 현금으로 도망간다”는 규칙이 있다고 합시다. 이 규칙이 좋은지 나쁜지 어떻게 알 수 있을까요? 지금부터 실제 돈으로 10년 실험해 볼 수는 없습니다. 대신 <strong>과거 10년 치 주가 데이터에 이 규칙을 그대로 적용해서, 매달 규칙이 시키는 대로 사고팔았다고 가정하고 계좌 잔고를 계산</strong>해 보는 겁니다. 이것이 백테스트입니다.</p>

<p>새 비행기를 실제로 띄우기 전에 시뮬레이터로 검증하는 것과 비슷합니다. 시뮬레이션을 통과했다고 실전에서 무조건 안전한 것은 아니지만, 시뮬레이션에서 추락하는 설계를 실전에 내보내지 않을 수는 있습니다.</p>

<h2 id="왜-백테스트가-필요한가">왜 백테스트가 필요한가</h2>

<p>백테스트 없이 투자 아이디어를 평가하는 방법은 사실상 두 가지뿐입니다. 남의 말을 믿거나, 실제 돈으로 실험하거나. 둘 다 비쌉니다. 백테스트는 세 번째 길을 열어줍니다.</p>

<ol>
  <li><strong>가설을 걸러내는 필터</strong> — “골든크로스가 나면 사면 된다더라” 같은 속설을 몇 시간 만에 데이터로 확인할 수 있습니다. 과거 20년 동안도 통하지 않았던 규칙이라면 굳이 내 돈으로 다시 확인할 필요가 없습니다.</li>
  <li><strong>전략의 성격 파악</strong> — 수익률만이 아니라 “이 전략은 최악의 순간에 얼마나 깨지는가(낙폭)”, “몇 년씩 부진한 구간이 있는가”를 미리 봅니다. 실전에서 규칙을 버리게 되는 것은 대부분 수익이 낮아서가 아니라, 부진한 구간을 견딜 마음의 준비가 없어서입니다.</li>
  <li><strong>비교의 기준</strong> — 아이디어 A와 B 중 무엇이 나은지, 같은 데이터 위에서 공정하게 비교할 수 있습니다.</li>
</ol>

<h2 id="백테스트의-4단계">백테스트의 4단계</h2>

<p>어떤 백테스트든 구조는 같습니다. 나중에 코드로 만들 때도 이 4단계가 그대로 함수 네 개가 됩니다.</p>

<h3 id="1단계-규칙-정하기">1단계: 규칙 정하기</h3>

<p>무엇을, 언제 사고, 언제 팔지를 <strong>사람의 판단이 끼어들 틈 없이</strong> 명확하게 적습니다. “좋아 보이면 산다”는 규칙이 아닙니다. “최근 6개월 수익률 &gt; 0이면 산다”는 규칙입니다. 판별 기준은 간단합니다. 그 규칙을 프로그래밍을 모르는 사람에게 줘도, 엑셀만 가지고 똑같은 매매 목록을 만들어낼 수 있어야 합니다. 애매한 단어(“과열”, “저평가”, “추세가 살아 있으면”)가 하나라도 들어 있으면 아직 규칙이 아닙니다.</p>

<h3 id="2단계-과거-데이터-준비">2단계: 과거 데이터 준비</h3>

<p>주가 데이터를 구합니다(이전에 다룬 FinanceDataReader가 이 단계의 도구입니다). 여기서 데이터의 품질이 백테스트 전체의 품질을 결정합니다. 수정주가인지, 배당이 반영됐는지, 상장폐지 종목이 빠져 있지 않은지 — 데이터 편에서 언급한 함정들이 전부 이 단계에서 스며듭니다.</p>

<h3 id="3단계-규칙을-기계적으로-적용">3단계: 규칙을 기계적으로 적용</h3>

<p>데이터의 처음부터 끝까지 <strong>시간 순서대로</strong> 진행하면서, 각 시점에 규칙이 시키는 매매를 기록합니다. 여기서 백테스트의 제1원칙이 나옵니다.</p>

<blockquote>
  <p><strong>각 시점의 판단에는 그 시점까지 알 수 있었던 정보만 사용한다.</strong></p>
</blockquote>

<p>1월 말의 매매 판단에 2월 데이터가 조금이라도 끼어들면 그 백테스트는 무효입니다. 당연한 말 같지만, 코드로 구현하다 보면 놀랄 만큼 자주 어기게 됩니다. <code class="language-plaintext highlighter-rouge">shift()</code> 한 줄을 빼먹어서 “오늘 종가를 보고 오늘 종가에 산다”는 불가능한 매매를 시뮬레이션한다든가, 그해 연말에야 발표되는 재무제표를 연초부터 알고 있었다고 가정한다든가. 이런 실수를 <strong>미래 참조 편향(look-ahead bias)</strong>이라고 부르고, 백테스트 성과를 가짜로 부풀리는 주범입니다.</p>

<h3 id="4단계-성과-평가">4단계: 성과 평가</h3>

<p>그 결과로 만들어진 수익률 흐름을 누적 수익률, CAGR 같은 지표로 평가합니다. 그리고 반드시 <strong>비교 대상(벤치마크)</strong>을 세웁니다. 보통은 “같은 자산을 그냥 사서 들고만 있기(buy &amp; hold)”가 기준입니다. 어떤 전략이 10년간 연 7%를 냈다는 사실 자체는 아무 의미가 없습니다. 같은 기간 그냥 들고 있었으면 연 9%였다면 그 전략은 수고비만 든 실패작이고, 연 3%에 폭락장을 두 번 피했다면 이야기가 달라집니다.</p>

<h2 id="좋은-백테스트를-가르는-질문들">좋은 백테스트를 가르는 질문들</h2>

<p>같은 4단계를 밟아도 백테스트의 질은 천차만별입니다. 결과를 볼 때 스스로에게 던져야 할 질문들입니다.</p>

<ul>
  <li><strong>거래 비용을 뺐는가?</strong> — 수수료, 세금, 그리고 내 주문 자체가 가격을 밀어 올리는 슬리피지. 월 1회 리밸런싱 전략도 왕복 0.3~0.5%씩 10년이면 성과가 크게 달라집니다.</li>
  <li><strong>충분히 긴 기간인가?</strong> — 상승장 3년만 잘라서 테스트한 전략은 아무것도 증명하지 못합니다. 최소한 큰 하락장(2008, 2020 같은)을 한 번 이상 포함해야 전략의 맷집을 볼 수 있습니다.</li>
  <li><strong>규칙을 몇 번이나 고쳤는가?</strong> — 결과를 보고 규칙을 고치고, 다시 결과를 보고 또 고치고… 를 반복하면 그 규칙은 미래에 통할 규칙이 아니라 <strong>그 과거 데이터에만 꼭 맞는 규칙</strong>이 됩니다. 이것이 과최적화(overfitting)이고, 백테스트의 가장 유혹적인 함정입니다.</li>
  <li><strong>너무 좋지 않은가?</strong> — 연 30%씩 꾸준히 버는 백테스트가 나왔다면, 축하할 일이 아니라 버그를 찾아야 할 일입니다. 경험칙상 거의 항상 미래 참조나 데이터 오류가 숨어 있습니다.</li>
</ul>

<h2 id="백테스트가-해줄-수-있는-것과-없는-것">백테스트가 해줄 수 있는 것과 없는 것</h2>

<p>분명히 해둡시다.</p>

<ul>
  <li>해줄 수 있는 것: <strong>과거에도 통하지 않았던 전략을 걸러내는 것.</strong> 그리고 전략의 성격 — 낙폭, 부진 구간, 벤치마크 대비 우열 — 을 미리 보여주는 것.</li>
  <li>해줄 수 없는 것: <strong>미래 수익 보장.</strong> 시장의 구조는 변하고, 과거에 통했던 규칙이 미래에도 통한다는 보장은 어디에도 없습니다.</li>
</ul>

<p>그래서 백테스트는 “돈 버는 기계를 찾는 도구”가 아니라 <strong>“틀린 아이디어를 빨리, 싸게 버리는 도구”</strong>라고 생각하는 쪽이 안전합니다. 이 관점의 차이가 과최적화의 유혹에서 스스로를 지켜줍니다.</p>

<h2 id="정리">정리</h2>

<ul>
  <li>백테스트는 “이 규칙대로 과거에 투자했다면?”을 데이터로 재현하는 시뮬레이션이다.</li>
  <li>규칙 정하기 → 데이터 준비 → 기계적 적용 → 성과 평가의 4단계이며, 각 시점에는 그 시점까지의 정보만 써야 한다(미래 참조 금지).</li>
  <li>성과는 반드시 벤치마크와 비교하고, 거래 비용·기간·과최적화 여부를 따져 물어야 한다.</li>
  <li>백테스트는 미래를 보장하는 도구가 아니라 틀린 아이디어를 싸게 버리는 도구다.</li>
</ul>

<p>다음 글에서는 우리가 처음으로 백테스트할 아이디어인 <strong>모멘텀</strong>이 무엇인지 다룹니다. 왜 “오르던 주식이 더 오른다”는 직관에 반하는 현상이 수십 년째 관찰되는지, 그리고 모멘텀에도 종류가 있다는 것까지요.</p>

<hr />

<p><em>이 글은 학습과 정보 공유 목적으로 작성되었으며, 특정 종목이나 상품에 대한 투자 권유가 아닙니다. 투자의 판단과 책임은 투자자 본인에게 있습니다.</em></p>]]></content><author><name>퀀트 노트</name></author><category term="퀀트투자" /><category term="백테스트" /><category term="입문" /><summary type="html"><![CDATA[지난 글에서 수익률 계산을 배웠으니 이제 백테스트를 만들 차례인데, 그 전에 백테스트가 정확히 무엇인지부터 제대로 짚고 갑니다. 이번 글은 코드가 없는 개념 편입니다. 다음 글들에서 코드를 쓸 때 “지금 뭘 하고 있는 건지” 헷갈리지 않기 위한 준비입니다.]]></summary></entry><entry><title type="html">퀀트 투자 입문 ④ — 모멘텀이란 무엇인가</title><link href="https://quant-note.com/posts/what-is-momentum/" rel="alternate" type="text/html" title="퀀트 투자 입문 ④ — 모멘텀이란 무엇인가" /><published>2026-08-31T00:00:00+09:00</published><updated>2026-08-31T00:00:00+09:00</updated><id>https://quant-note.com/posts/what-is-momentum</id><content type="html" xml:base="https://quant-note.com/posts/what-is-momentum/"><![CDATA[<p>백테스트가 무엇인지 알았으니, 이제 처음으로 검증해 볼 아이디어를 골라야 합니다. 이 시리즈는 <strong>모멘텀(momentum)</strong>으로 시작합니다. 규칙이 가장 단순하면서도 학계에서 가장 많이 검증된 현상 중 하나이기 때문입니다. 이번 글에서는 모멘텀이 무엇이고, 왜 생기며, 어떤 종류가 있고, 어디가 약점인지를 정리합니다. 역시 코드 없는 개념 편입니다.</p>

<h2 id="모멘텀-오르던-것이-더-오른다">모멘텀: 오르던 것이 더 오른다</h2>

<p>모멘텀은 물리학 용어로 “운동량”, 쉽게 말해 관성입니다. 굴러가는 공이 계속 굴러가려는 성질처럼, 주가에도 비슷한 경향이 관찰됩니다.</p>

<blockquote>
  <p><strong>최근에 오르던 자산은 당분간 더 오르고, 떨어지던 자산은 당분간 더 떨어지는 경향.</strong></p>
</blockquote>

<p>여기서 “최근”은 보통 <strong>3~12개월</strong>을 말합니다. 이 기간이 중요합니다. 너무 짧으면(며칠~몇 주) 오히려 반대 현상(단기 반전)이 나타나고, 너무 길면(3~5년) 이번엔 장기 반전 — 몇 년간 많이 오른 주식이 이후 부진한 현상 — 이 나타난다는 연구들이 있습니다. 모멘텀은 “무조건 추세를 따르라”가 아니라 <strong>특정 시간 척도에서 관찰되는 현상</strong>입니다.</p>

<h2 id="직관과-반대-아닌가">직관과 반대 아닌가?</h2>

<p>“오른 주식은 비싸졌으니 이제 떨어질 차례 아닌가?” — 자연스러운 생각이고, 소위 ‘물타기’와 ‘차익 실현’이 몸에 밴 투자자일수록 그렇게 느낍니다. 그런데 데이터는 반대를 말해 왔습니다.</p>

<p>이 현상을 학계에 각인시킨 것이 1993년 Jegadeesh와 Titman의 연구입니다. 미국 주식을 최근 3~12개월 수익률로 줄 세워서, 상위 종목을 사고 하위 종목을 공매도하는 포트폴리오가 이후 3~12개월 동안 유의미한 초과 수익을 냈다는 내용입니다. 이후 수십 년간 다른 나라 주식, 채권, 원자재, 환율에서도 같은 패턴이 반복 확인됐고, 2012년 Moskowitz 등의 연구는 자산 자신의 과거 수익률만 보는 시계열 모멘텀(뒤에 나올 절대 모멘텀과 같은 계열)이 58개 자산에서 작동했다고 보고했습니다. 팩터 투자를 다룬 글에서 모멘텀을 4대 팩터에 넣었던 이유가 이것입니다.</p>

<h2 id="왜-이런-현상이-생길까">왜 이런 현상이 생길까</h2>

<p>시장이 효율적이라면 이런 공짜 점심은 없어야 합니다. 모멘텀이 존재하는 이유로 제시되는 설명은 크게 이렇습니다.</p>

<ol>
  <li><strong>정보의 느린 반영(underreaction)</strong> — 좋은 뉴스가 나와도 가격은 한 번에 적정 수준까지 뛰지 않고 서서히 반영됩니다. 애널리스트가 실적 전망을 찔끔찔끔 올리고, 투자자들이 뒤늦게 알아차리는 과정에서 상승이 이어집니다.</li>
  <li><strong>추격 매수(herding)</strong> — 오르는 주식이 뉴스와 수익률 상위 목록에 노출되면서 새 매수자를 끌어들이고, 그 매수가 다시 가격을 밀어 올립니다.</li>
  <li><strong>처분 효과(disposition effect)</strong> — 투자자들은 이익 난 주식은 서둘러 팔고 손실 난 주식은 붙들고 버팁니다. 이 행동이 오르는 주식의 상승을 더디게(그래서 더 오래) 만들고, 떨어지는 주식의 하락도 질질 끌리게 만듭니다.</li>
</ol>

<p>공통점은 셋 다 <strong>사람의 행동 편향</strong>에서 나온다는 것입니다. 그래서 모멘텀이 논문으로 공개된 지 30년이 지나도 완전히 사라지지 않았다는 해석이 있습니다. 사람의 본성은 논문을 읽어도 잘 바뀌지 않으니까요.</p>

<h2 id="상대-모멘텀과-절대-모멘텀">상대 모멘텀과 절대 모멘텀</h2>

<p>모멘텀 전략은 묻는 질문에 따라 두 종류로 나뉩니다.</p>

<table>
  <thead>
    <tr>
      <th>종류</th>
      <th>질문</th>
      <th>예시 규칙</th>
      <th>목적</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>상대 모멘텀 (cross-sectional)</td>
      <td>여러 자산 중 <strong>어느 것이</strong> 제일 잘 나가나?</td>
      <td>최근 6개월 수익률 상위 30종목 매수, 매월 교체</td>
      <td>더 나은 자산 고르기</td>
    </tr>
    <tr>
      <td>절대 모멘텀 (time-series)</td>
      <td>이 자산이 <strong>오르는 중인가 아닌가?</strong></td>
      <td>코스피의 최근 6개월 수익률 &gt; 0이면 보유, 아니면 현금</td>
      <td>하락장 회피</td>
    </tr>
  </tbody>
</table>

<p>둘은 성격이 다릅니다. <strong>상대 모멘텀</strong>은 시장 안에서 승자를 고르는 것이라 시장 전체가 폭락하면 같이 폭락합니다(“제일 덜 빠진 종목”을 들고 있을 뿐). <strong>절대 모멘텀</strong>은 시장 자체에서 도망칠 수 있어서 큰 하락장을 피하는 데 강점이 있는 대신, 시장이 오를 때 현금에 앉아 있는 기회비용이 생깁니다. 이 둘을 결합한 것이 Gary Antonacci가 정리한 <strong>듀얼 모멘텀</strong>인데, 시리즈 후반에 따로 다루겠습니다.</p>

<p>다음 글에서 코드로 만들 것은 둘 중 더 단순한 <strong>절대 모멘텀</strong>입니다. 자산 하나(코스피 지수)와 규칙 하나(“N개월 수익률이 플러스인가?”)만 있으면 되기 때문입니다.</p>

<h2 id="모멘텀의-약점">모멘텀의 약점</h2>

<p>어떤 전략이든 약점이 어디인지 아는 것까지가 검증입니다. 모멘텀의 약점은 구조적으로 분명합니다.</p>

<ul>
  <li><strong>급반전에 약하다</strong> — 모멘텀은 과거를 보고 판단하므로, 시장이 방향을 급격히 바꾸는 순간에는 항상 한 박자 늦습니다. 대표 사례가 2009년 봄의 “모멘텀 크래시”입니다. 금융위기 폭락 직후 시장이 급반등했는데, 모멘텀 전략은 여전히 “떨어지던 것들”을 피하고 있다가(혹은 공매도하고 있다가) 그 급반등을 정통으로 얻어맞았습니다.</li>
  <li><strong>횡보장에 약하다</strong> — 방향 없이 오르내리는 시장에서는 “올랐으니 사고 → 떨어져서 팔고 → 다시 올라서 사고”를 반복하며 거래 비용만 쌓입니다. 이를 휩쏘(whipsaw)라고 부릅니다.</li>
  <li><strong>거래가 잦다</strong> — 특히 상대 모멘텀은 매달 종목을 갈아치우므로 거래 비용에 민감합니다. 백테스트에서 비용을 빼먹으면 성과가 가장 크게 부풀려지는 전략군이기도 합니다.</li>
</ul>

<p>그래서 모멘텀을 실제로 쓰는 사람들은 단독으로 쓰기보다 자산 배분의 한 부품으로, 또는 밸류처럼 성격이 반대인 팩터와 섞어서 씁니다. 이것도 뒤에서 데이터로 확인해 보겠습니다.</p>

<h2 id="정리">정리</h2>

<ul>
  <li>모멘텀은 “최근 3~12개월 오르던 자산이 당분간 더 오르는 경향”이고, 30년 넘게 여러 자산군에서 반복 확인된 현상이다.</li>
  <li>원인으로는 정보의 느린 반영, 추격 매수, 처분 효과 같은 행동 편향이 지목된다.</li>
  <li>여러 자산 중 승자를 고르면 상대 모멘텀, 한 자산의 보유/현금을 결정하면 절대 모멘텀이다.</li>
  <li>약점도 뚜렷하다: 급반전, 횡보장, 거래 비용. 약점을 아는 것까지가 공부다.</li>
</ul>

<p>다음 글에서는 드디어 코드입니다. 코스피 지수 하나로 <strong>절대 모멘텀 전략의 백테스트</strong>를 파이썬 30줄로 만들고, 그냥 들고 있기(buy &amp; hold)와 비교해 봅니다.</p>

<hr />

<p><em>이 글은 학습과 정보 공유 목적으로 작성되었으며, 특정 종목이나 상품에 대한 투자 권유가 아닙니다. 투자의 판단과 책임은 투자자 본인에게 있습니다.</em></p>]]></content><author><name>퀀트 노트</name></author><category term="퀀트투자" /><category term="모멘텀" /><category term="팩터투자" /><category term="입문" /><summary type="html"><![CDATA[백테스트가 무엇인지 알았으니, 이제 처음으로 검증해 볼 아이디어를 골라야 합니다. 이 시리즈는 모멘텀(momentum)으로 시작합니다. 규칙이 가장 단순하면서도 학계에서 가장 많이 검증된 현상 중 하나이기 때문입니다. 이번 글에서는 모멘텀이 무엇이고, 왜 생기며, 어떤 종류가 있고, 어디가 약점인지를 정리합니다. 역시 코드 없는 개념 편입니다.]]></summary></entry><entry><title type="html">퀀트 투자 입문 ② — 수익률 계산, 제대로 알고 시작하기</title><link href="https://quant-note.com/posts/return-calculation-basics/" rel="alternate" type="text/html" title="퀀트 투자 입문 ② — 수익률 계산, 제대로 알고 시작하기" /><published>2026-08-30T00:00:00+09:00</published><updated>2026-08-30T00:00:00+09:00</updated><id>https://quant-note.com/posts/return-calculation-basics</id><content type="html" xml:base="https://quant-note.com/posts/return-calculation-basics/"><![CDATA[<p>백테스트의 모든 결과물은 결국 수익률 계산 위에 서 있습니다. 그런데 수익률은 생각보다 헷갈리는 개념입니다. “10% 오르고 10% 떨어지면 본전”이라고 생각했다면 이번 글이 도움이 될 겁니다(정답: 1% 손실입니다). 이번 글에서는 단순 수익률, 로그 수익률, 누적 수익률, 연평균 수익률(CAGR)을 pandas 코드와 함께 정리합니다.</p>

<h2 id="단순-수익률-simple-return">단순 수익률 (Simple Return)</h2>

<p>가장 기본이 되는 수익률입니다. 어제 10,000원이던 주식이 오늘 10,500원이 되었다면:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>수익률 = (10,500 - 10,000) / 10,000 = 0.05 = 5%
</code></pre></div></div>

<p>pandas에서는 <code class="language-plaintext highlighter-rouge">pct_change()</code> 한 줄입니다.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">FinanceDataReader</span> <span class="k">as</span> <span class="n">fdr</span>

<span class="n">df</span> <span class="o">=</span> <span class="n">fdr</span><span class="p">.</span><span class="n">DataReader</span><span class="p">(</span><span class="s">'005930'</span><span class="p">,</span> <span class="s">'2023-01-01'</span><span class="p">)</span>  <span class="c1"># 삼성전자
</span><span class="n">returns</span> <span class="o">=</span> <span class="n">df</span><span class="p">[</span><span class="s">'Close'</span><span class="p">].</span><span class="n">pct_change</span><span class="p">().</span><span class="n">dropna</span><span class="p">()</span>
<span class="k">print</span><span class="p">(</span><span class="n">returns</span><span class="p">.</span><span class="n">head</span><span class="p">())</span>
</code></pre></div></div>

<p>첫 행은 비교할 전일 가격이 없어서 <code class="language-plaintext highlighter-rouge">NaN</code>이 되므로 <code class="language-plaintext highlighter-rouge">dropna()</code>로 제거합니다.</p>

<h2 id="수익률은-더하면-안-된다">수익률은 더하면 안 된다</h2>

<p>여기서 초보 시절 누구나 한 번은 빠지는 함정이 나옵니다. <strong>단순 수익률은 기간에 걸쳐 더할 수 없습니다.</strong></p>

<ul>
  <li>첫날 +10%, 둘째 날 -10%: 합산하면 0%로 보이지만</li>
  <li>실제로는 1.10 × 0.90 = 0.99, 즉 <strong>-1%</strong>입니다.</li>
</ul>

<p>수익률은 더하는 것이 아니라 <strong>(1 + 수익률)을 곱하는 것</strong>입니다. 이걸 복리(compounding)라고 부릅니다. 하락이 무서운 이유도 여기 있습니다. -50%가 나면 +50%로는 본전이 안 되고 +100%가 필요합니다.</p>

<h2 id="누적-수익률-cumulative-return">누적 수익률 (Cumulative Return)</h2>

<p>그래서 여러 기간의 성과는 곱셈으로 누적합니다.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">cum_returns</span> <span class="o">=</span> <span class="p">(</span><span class="mi">1</span> <span class="o">+</span> <span class="n">returns</span><span class="p">).</span><span class="n">cumprod</span><span class="p">()</span> <span class="o">-</span> <span class="mi">1</span>
<span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"기간 전체 수익률: </span><span class="si">{</span><span class="n">cum_returns</span><span class="p">.</span><span class="n">iloc</span><span class="p">[</span><span class="o">-</span><span class="mi">1</span><span class="p">]</span><span class="si">:</span><span class="p">.</span><span class="mi">2</span><span class="o">%</span><span class="si">}</span><span class="s">"</span><span class="p">)</span>

<span class="n">cum_returns</span><span class="p">.</span><span class="n">plot</span><span class="p">(</span><span class="n">figsize</span><span class="o">=</span><span class="p">(</span><span class="mi">12</span><span class="p">,</span> <span class="mi">5</span><span class="p">),</span> <span class="n">title</span><span class="o">=</span><span class="s">'Cumulative Return'</span><span class="p">)</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">(1 + returns).cumprod()</code>는 “1원을 투자했으면 지금 얼마가 됐나”를 매일 계산한 것이고, 거기서 1을 빼면 수익률이 됩니다. 백테스트 성과 그래프는 대부분 이 곡선입니다.</p>

<h2 id="로그-수익률-log-return">로그 수익률 (Log Return)</h2>

<p>수익률을 곱셈이 아니라 덧셈으로 다루고 싶을 때 쓰는 것이 로그 수익률입니다.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">numpy</span> <span class="k">as</span> <span class="n">np</span>

<span class="n">log_returns</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">log</span><span class="p">(</span><span class="n">df</span><span class="p">[</span><span class="s">'Close'</span><span class="p">]</span> <span class="o">/</span> <span class="n">df</span><span class="p">[</span><span class="s">'Close'</span><span class="p">].</span><span class="n">shift</span><span class="p">(</span><span class="mi">1</span><span class="p">)).</span><span class="n">dropna</span><span class="p">()</span>

<span class="c1"># 로그 수익률은 그냥 더하면 누적이 된다
</span><span class="n">total_log</span> <span class="o">=</span> <span class="n">log_returns</span><span class="p">.</span><span class="nb">sum</span><span class="p">()</span>
<span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"단순 수익률로 환산: </span><span class="si">{</span><span class="n">np</span><span class="p">.</span><span class="n">exp</span><span class="p">(</span><span class="n">total_log</span><span class="p">)</span> <span class="o">-</span> <span class="mi">1</span><span class="si">:</span><span class="p">.</span><span class="mi">2</span><span class="o">%</span><span class="si">}</span><span class="s">"</span><span class="p">)</span>
</code></pre></div></div>

<p>로그 수익률의 합에 <code class="language-plaintext highlighter-rouge">exp</code>를 씌우면 누적 단순 수익률과 정확히 일치합니다. 통계 분석(평균, 분산, 정규성 검정)을 할 때는 로그 수익률이 다루기 편해서 학계 논문은 대부분 로그 수익률을 씁니다. 일상적인 백테스트에서는 단순 수익률의 <code class="language-plaintext highlighter-rouge">cumprod</code> 방식이면 충분하고, 둘을 섞어 쓰지만 않으면 됩니다.</p>

<h2 id="연평균-수익률-cagr">연평균 수익률, CAGR</h2>

<p>“3년 동안 40% 벌었다”는 말은 1년 기준으로 얼마인지 감이 안 옵니다. 그래서 기간이 다른 성과를 비교할 때는 <strong>CAGR(연평균 복리 수익률)</strong>로 환산합니다.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>CAGR = (최종 자산 / 초기 자산)^(1/연수) - 1
</code></pre></div></div>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">n_years</span> <span class="o">=</span> <span class="p">(</span><span class="n">df</span><span class="p">.</span><span class="n">index</span><span class="p">[</span><span class="o">-</span><span class="mi">1</span><span class="p">]</span> <span class="o">-</span> <span class="n">df</span><span class="p">.</span><span class="n">index</span><span class="p">[</span><span class="mi">0</span><span class="p">]).</span><span class="n">days</span> <span class="o">/</span> <span class="mf">365.25</span>
<span class="n">final_value</span> <span class="o">=</span> <span class="p">(</span><span class="mi">1</span> <span class="o">+</span> <span class="n">returns</span><span class="p">).</span><span class="n">prod</span><span class="p">()</span>
<span class="n">cagr</span> <span class="o">=</span> <span class="n">final_value</span> <span class="o">**</span> <span class="p">(</span><span class="mi">1</span> <span class="o">/</span> <span class="n">n_years</span><span class="p">)</span> <span class="o">-</span> <span class="mi">1</span>
<span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"CAGR: </span><span class="si">{</span><span class="n">cagr</span><span class="si">:</span><span class="p">.</span><span class="mi">2</span><span class="o">%</span><span class="si">}</span><span class="s">"</span><span class="p">)</span>
</code></pre></div></div>

<p>3년에 40%면 CAGR로 약 11.9%입니다. 어떤 전략의 성과든 CAGR로 환산해 보는 습관을 들이면 과장된 숫자에 속을 일이 줄어듭니다.</p>

<h2 id="산술평균의-함정-변동성-손실">산술평균의 함정: 변동성 손실</h2>

<p>“평균 수익률이 플러스면 돈을 번다”는 착각도 짚고 갑니다. 2년짜리 극단적인 예를 봅시다.</p>

<ul>
  <li>1년 차 +50%, 2년 차 -40%: 산술평균은 (50 - 40) / 2 = <strong>+5%</strong></li>
  <li>실제 계좌: 1.50 × 0.60 = 0.90, 즉 <strong>-10%</strong></li>
</ul>

<p>평균은 플러스인데 계좌는 마이너스입니다. 수익률이 출렁일수록 산술평균과 실제 복리 수익률의 격차가 커지는데, 이를 <strong>변동성 손실(volatility drag)</strong>이라고 부릅니다. 같은 평균 수익률이라면 덜 출렁이는 쪽의 실제 성과가 더 좋습니다. 저변동성 팩터나 자산 배분이 힘을 발휘하는 수학적 이유가 바로 이것입니다.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># 산술평균 vs 실제 복리(기하평균) 비교
</span><span class="n">arith_mean</span> <span class="o">=</span> <span class="n">returns</span><span class="p">.</span><span class="n">mean</span><span class="p">()</span> <span class="o">*</span> <span class="mi">252</span>            <span class="c1"># 연환산 산술평균
</span><span class="n">geo_mean</span> <span class="o">=</span> <span class="p">(</span><span class="mi">1</span> <span class="o">+</span> <span class="n">returns</span><span class="p">).</span><span class="n">prod</span><span class="p">()</span> <span class="o">**</span> <span class="p">(</span><span class="mi">252</span> <span class="o">/</span> <span class="nb">len</span><span class="p">(</span><span class="n">returns</span><span class="p">))</span> <span class="o">-</span> <span class="mi">1</span>  <span class="c1"># 연환산 복리
</span><span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"산술평균(연): </span><span class="si">{</span><span class="n">arith_mean</span><span class="si">:</span><span class="p">.</span><span class="mi">2</span><span class="o">%</span><span class="si">}</span><span class="s"> / 복리(연): </span><span class="si">{</span><span class="n">geo_mean</span><span class="si">:</span><span class="p">.</span><span class="mi">2</span><span class="o">%</span><span class="si">}</span><span class="s">"</span><span class="p">)</span>
</code></pre></div></div>

<p>둘의 차이가 클수록 그 자산은 출렁임이 심하다는 뜻입니다.</p>

<h2 id="기간-환산-일별월별연간">기간 환산: 일별·월별·연간</h2>

<p>수익률을 다른 기간 단위로 바꾸는 관례도 알아두면 편합니다. 1년은 보통 거래일 <strong>252일</strong>, <strong>12개월</strong>로 놓고 계산합니다.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># 일별 수익률 → 월별 수익률 (그 달의 복리 누적)
</span><span class="n">monthly_returns</span> <span class="o">=</span> <span class="p">(</span><span class="mi">1</span> <span class="o">+</span> <span class="n">returns</span><span class="p">).</span><span class="n">resample</span><span class="p">(</span><span class="s">'ME'</span><span class="p">).</span><span class="n">prod</span><span class="p">()</span> <span class="o">-</span> <span class="mi">1</span>

<span class="c1"># 월별 수익률 → 연환산 (12개월 복리)
</span><span class="n">annualized</span> <span class="o">=</span> <span class="p">(</span><span class="mi">1</span> <span class="o">+</span> <span class="n">monthly_returns</span><span class="p">.</span><span class="n">mean</span><span class="p">())</span> <span class="o">**</span> <span class="mi">12</span> <span class="o">-</span> <span class="mi">1</span>
</code></pre></div></div>

<p>주의: <code class="language-plaintext highlighter-rouge">returns.mean() * 252</code>처럼 단순히 곱하는 연환산은 근사치일 뿐이고(위에서 본 변동성 손실이 무시됨), 정확한 값은 복리로 계산해야 합니다. 관행상 통계 지표(변동성, 샤프비율)에는 곱셈 연환산을 쓰지만, <strong>성과 보고에는 복리 기준</strong>을 쓰는 것이 정직합니다.</p>

<h2 id="자주-하는-실수-정리">자주 하는 실수 정리</h2>

<ol>
  <li><strong>수익률을 더해서 누적하기</strong> — 반드시 (1+r)을 곱해야 합니다. 특히 손실 구간이 있으면 오차가 커집니다.</li>
  <li><strong>단순 수익률과 로그 수익률 섞어 쓰기</strong> — 한 백테스트 안에서는 하나로 통일해야 합니다.</li>
  <li><strong>기간이 다른 수익률을 그대로 비교하기</strong> — 6개월 수익률과 2년 수익률은 CAGR로 환산한 뒤 비교합니다.</li>
  <li><strong>평균 수익률로 성과를 말하기</strong> — 일별 수익률의 산술평균이 양수여도 누적 수익률은 마이너스일 수 있습니다(변동성 손실). 성과는 누적 수익률과 CAGR로 말해야 합니다.</li>
</ol>

<h2 id="정리">정리</h2>

<ul>
  <li>수익률의 누적은 덧셈이 아니라 (1+r)의 곱셈이다.</li>
  <li>통계 분석에는 로그 수익률, 성과 보고에는 단순 수익률 기반 누적 수익률과 CAGR를 쓴다.</li>
  <li>pandas에서는 <code class="language-plaintext highlighter-rouge">pct_change()</code>, <code class="language-plaintext highlighter-rouge">cumprod()</code>, <code class="language-plaintext highlighter-rouge">prod()</code> 세 가지면 기본 계산이 끝난다.</li>
</ul>

<p>다음 두 글은 코드를 잠시 내려놓고 개념을 정리합니다. 먼저 <strong>백테스트가 정확히 무엇인지</strong>, 그다음 <strong>모멘텀이 무엇인지</strong>를 다룬 뒤, 그 개념들을 실제 코드 30줄짜리 백테스트로 구현합니다. 여기서 계산한 누적 수익률 곡선이 전략 평가의 기준이 됩니다.</p>

<hr />

<p><em>이 글은 학습과 정보 공유 목적으로 작성되었으며, 특정 종목이나 상품에 대한 투자 권유가 아닙니다. 투자의 판단과 책임은 투자자 본인에게 있습니다.</em></p>]]></content><author><name>퀀트 노트</name></author><category term="퀀트투자" /><category term="수익률" /><category term="pandas" /><category term="입문" /><summary type="html"><![CDATA[백테스트의 모든 결과물은 결국 수익률 계산 위에 서 있습니다. 그런데 수익률은 생각보다 헷갈리는 개념입니다. “10% 오르고 10% 떨어지면 본전”이라고 생각했다면 이번 글이 도움이 될 겁니다(정답: 1% 손실입니다). 이번 글에서는 단순 수익률, 로그 수익률, 누적 수익률, 연평균 수익률(CAGR)을 pandas 코드와 함께 정리합니다.]]></summary></entry><entry><title type="html">파이썬으로 주가 데이터 받아오기 — FinanceDataReader 시작하기</title><link href="https://quant-note.com/posts/financedatareader-tutorial/" rel="alternate" type="text/html" title="파이썬으로 주가 데이터 받아오기 — FinanceDataReader 시작하기" /><published>2026-08-29T00:00:00+09:00</published><updated>2026-08-29T00:00:00+09:00</updated><id>https://quant-note.com/posts/financedatareader-tutorial</id><content type="html" xml:base="https://quant-note.com/posts/financedatareader-tutorial/"><![CDATA[<p>퀀트 투자의 시작은 데이터입니다. 이번 글에서는 무료 오픈소스 라이브러리인 <strong>FinanceDataReader</strong>로 국내외 주가 데이터를 받아오는 방법을 정리합니다.</p>

<h2 id="financedatareader란">FinanceDataReader란</h2>

<p><a href="https://github.com/FinanceData/FinanceDataReader">FinanceDataReader</a>는 한국 주식(KRX), 미국 주식, 환율, 지수 등의 데이터를 pandas DataFrame으로 받아주는 파이썬 라이브러리입니다. 회원가입이나 API 키 없이 바로 쓸 수 있어서 입문용으로 가장 무난합니다.</p>

<p>설치는 pip 한 줄이면 됩니다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>pip <span class="nb">install </span>finance-datareader
</code></pre></div></div>

<h2 id="종목-리스트-받기">종목 리스트 받기</h2>

<p>먼저 KRX 전체 상장 종목 목록을 받아봅니다.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">FinanceDataReader</span> <span class="k">as</span> <span class="n">fdr</span>

<span class="c1"># KRX 전체 상장 종목 (코스피 + 코스닥 + 코넥스)
</span><span class="n">krx</span> <span class="o">=</span> <span class="n">fdr</span><span class="p">.</span><span class="n">StockListing</span><span class="p">(</span><span class="s">'KRX'</span><span class="p">)</span>
<span class="k">print</span><span class="p">(</span><span class="n">krx</span><span class="p">.</span><span class="n">head</span><span class="p">())</span>
<span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"전체 종목 수: </span><span class="si">{</span><span class="nb">len</span><span class="p">(</span><span class="n">krx</span><span class="p">)</span><span class="si">}</span><span class="s">"</span><span class="p">)</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">Code</code>(종목코드), <code class="language-plaintext highlighter-rouge">Name</code>(종목명), <code class="language-plaintext highlighter-rouge">Market</code>(시장) 같은 컬럼이 들어 있습니다. 종목코드는 이후 주가를 조회할 때 사용합니다.</p>

<h2 id="개별-종목-주가-받기">개별 종목 주가 받기</h2>

<p>삼성전자(005930)의 일별 주가를 받아보겠습니다.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># 삼성전자, 2020년부터 현재까지
</span><span class="n">df</span> <span class="o">=</span> <span class="n">fdr</span><span class="p">.</span><span class="n">DataReader</span><span class="p">(</span><span class="s">'005930'</span><span class="p">,</span> <span class="s">'2020-01-01'</span><span class="p">)</span>
<span class="k">print</span><span class="p">(</span><span class="n">df</span><span class="p">.</span><span class="n">tail</span><span class="p">())</span>
</code></pre></div></div>

<p>결과는 날짜를 인덱스로 하는 DataFrame입니다.</p>

<table>
  <thead>
    <tr>
      <th>컬럼</th>
      <th>의미</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Open / High / Low / Close</td>
      <td>시가 / 고가 / 저가 / 종가</td>
    </tr>
    <tr>
      <td>Volume</td>
      <td>거래량</td>
    </tr>
    <tr>
      <td>Change</td>
      <td>전일 대비 등락률</td>
    </tr>
  </tbody>
</table>

<p>미국 주식이나 지수도 같은 방식입니다.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">spy</span> <span class="o">=</span> <span class="n">fdr</span><span class="p">.</span><span class="n">DataReader</span><span class="p">(</span><span class="s">'SPY'</span><span class="p">,</span> <span class="s">'2020-01-01'</span><span class="p">)</span>      <span class="c1"># S&amp;P 500 ETF
</span><span class="n">kospi</span> <span class="o">=</span> <span class="n">fdr</span><span class="p">.</span><span class="n">DataReader</span><span class="p">(</span><span class="s">'KS11'</span><span class="p">,</span> <span class="s">'2020-01-01'</span><span class="p">)</span>   <span class="c1"># 코스피 지수
</span><span class="n">usdkrw</span> <span class="o">=</span> <span class="n">fdr</span><span class="p">.</span><span class="n">DataReader</span><span class="p">(</span><span class="s">'USD/KRW'</span><span class="p">,</span> <span class="s">'2020-01-01'</span><span class="p">)</span>  <span class="c1"># 원달러 환율
</span></code></pre></div></div>

<p>기간을 지정하고 싶으면 종료일을 두 번째 인자로 넘기면 됩니다.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># 2020년 한 해만
</span><span class="n">df_2020</span> <span class="o">=</span> <span class="n">fdr</span><span class="p">.</span><span class="n">DataReader</span><span class="p">(</span><span class="s">'005930'</span><span class="p">,</span> <span class="s">'2020-01-01'</span><span class="p">,</span> <span class="s">'2020-12-31'</span><span class="p">)</span>
</code></pre></div></div>

<h2 id="월별-데이터로-바꾸기">월별 데이터로 바꾸기</h2>

<p>백테스트에서는 일별 데이터보다 월별 데이터를 쓰는 경우가 많습니다. 월 단위 리밸런싱 전략이 흔하기 때문입니다. pandas의 <code class="language-plaintext highlighter-rouge">resample</code>로 간단히 변환할 수 있습니다.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># 월말 종가만 추출
</span><span class="n">monthly</span> <span class="o">=</span> <span class="n">df</span><span class="p">[</span><span class="s">'Close'</span><span class="p">].</span><span class="n">resample</span><span class="p">(</span><span class="s">'ME'</span><span class="p">).</span><span class="n">last</span><span class="p">()</span>

<span class="c1"># 월별 수익률
</span><span class="n">monthly_returns</span> <span class="o">=</span> <span class="n">monthly</span><span class="p">.</span><span class="n">pct_change</span><span class="p">().</span><span class="n">dropna</span><span class="p">()</span>
<span class="k">print</span><span class="p">(</span><span class="n">monthly_returns</span><span class="p">.</span><span class="n">head</span><span class="p">())</span>
</code></pre></div></div>

<p>받은 데이터를 매번 다시 내려받지 않으려면 CSV로 저장해 두는 것이 좋습니다. 데이터 소스에 부담도 줄이고, 나중에 같은 데이터로 재현할 수도 있습니다.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">df</span><span class="p">.</span><span class="n">to_csv</span><span class="p">(</span><span class="s">'samsung_005930.csv'</span><span class="p">)</span>

<span class="c1"># 다시 불러올 때
</span><span class="kn">import</span> <span class="nn">pandas</span> <span class="k">as</span> <span class="n">pd</span>
<span class="n">df</span> <span class="o">=</span> <span class="n">pd</span><span class="p">.</span><span class="n">read_csv</span><span class="p">(</span><span class="s">'samsung_005930.csv'</span><span class="p">,</span> <span class="n">index_col</span><span class="o">=</span><span class="mi">0</span><span class="p">,</span> <span class="n">parse_dates</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>
</code></pre></div></div>

<h2 id="여러-종목을-한-번에-받아-합치기">여러 종목을 한 번에 받아 합치기</h2>

<p>백테스트를 하다 보면 결국 여러 종목의 종가를 한 표에 모으게 됩니다. 반복문으로 받아서 열로 합치는 것이 기본 패턴입니다.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">pandas</span> <span class="k">as</span> <span class="n">pd</span>
<span class="kn">import</span> <span class="nn">time</span>

<span class="n">tickers</span> <span class="o">=</span> <span class="p">{</span><span class="s">'005930'</span><span class="p">:</span> <span class="s">'삼성전자'</span><span class="p">,</span> <span class="s">'000660'</span><span class="p">:</span> <span class="s">'SK하이닉스'</span><span class="p">,</span> <span class="s">'035420'</span><span class="p">:</span> <span class="s">'NAVER'</span><span class="p">}</span>

<span class="n">prices</span> <span class="o">=</span> <span class="p">{}</span>
<span class="k">for</span> <span class="n">code</span><span class="p">,</span> <span class="n">name</span> <span class="ow">in</span> <span class="n">tickers</span><span class="p">.</span><span class="n">items</span><span class="p">():</span>
    <span class="n">prices</span><span class="p">[</span><span class="n">name</span><span class="p">]</span> <span class="o">=</span> <span class="n">fdr</span><span class="p">.</span><span class="n">DataReader</span><span class="p">(</span><span class="n">code</span><span class="p">,</span> <span class="s">'2022-01-01'</span><span class="p">)[</span><span class="s">'Close'</span><span class="p">]</span>
    <span class="n">time</span><span class="p">.</span><span class="n">sleep</span><span class="p">(</span><span class="mf">0.5</span><span class="p">)</span>  <span class="c1"># 데이터 소스에 부담을 주지 않도록 간격을 둔다
</span>
<span class="n">df_all</span> <span class="o">=</span> <span class="n">pd</span><span class="p">.</span><span class="n">DataFrame</span><span class="p">(</span><span class="n">prices</span><span class="p">)</span>
<span class="k">print</span><span class="p">(</span><span class="n">df_all</span><span class="p">.</span><span class="n">tail</span><span class="p">())</span>
</code></pre></div></div>

<p>이렇게 만든 표는 날짜 인덱스가 자동으로 정렬·정합되지만, 종목마다 거래정지 등으로 빠진 날짜가 다를 수 있으므로 <code class="language-plaintext highlighter-rouge">df_all.isna().sum()</code>으로 결측을 확인하는 습관이 필요합니다.</p>

<h2 id="받은-데이터를-믿기-전에-검증-습관-3가지">받은 데이터를 믿기 전에: 검증 습관 3가지</h2>

<p>다운로드가 성공했다고 데이터가 멀쩡하다는 보장은 없습니다. 저는 새 데이터를 받으면 반드시 이 세 가지를 봅니다.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># 1. 기간과 개수가 상식적인가 (1년이면 거래일 약 245~250개)
</span><span class="k">print</span><span class="p">(</span><span class="n">df</span><span class="p">.</span><span class="n">index</span><span class="p">.</span><span class="nb">min</span><span class="p">(),</span> <span class="n">df</span><span class="p">.</span><span class="n">index</span><span class="p">.</span><span class="nb">max</span><span class="p">(),</span> <span class="nb">len</span><span class="p">(</span><span class="n">df</span><span class="p">))</span>

<span class="c1"># 2. 값의 범위가 상식적인가 (0이나 음수 가격, 터무니없는 급등락)
</span><span class="k">print</span><span class="p">(</span><span class="n">df</span><span class="p">[</span><span class="s">'Close'</span><span class="p">].</span><span class="n">describe</span><span class="p">())</span>
<span class="k">print</span><span class="p">(</span><span class="n">df</span><span class="p">[</span><span class="s">'Close'</span><span class="p">].</span><span class="n">pct_change</span><span class="p">().</span><span class="nb">abs</span><span class="p">().</span><span class="n">nlargest</span><span class="p">(</span><span class="mi">5</span><span class="p">))</span>  <span class="c1"># 최대 등락일 확인
</span>
<span class="c1"># 3. 결측과 중복이 없는가
</span><span class="k">print</span><span class="p">(</span><span class="n">df</span><span class="p">.</span><span class="n">isna</span><span class="p">().</span><span class="nb">sum</span><span class="p">())</span>
<span class="k">print</span><span class="p">(</span><span class="n">df</span><span class="p">.</span><span class="n">index</span><span class="p">.</span><span class="n">duplicated</span><span class="p">().</span><span class="nb">sum</span><span class="p">())</span>
</code></pre></div></div>

<p>2번에서 하루 ±30% 같은 값이 나오면 상한가·하한가(국내는 ±30%)일 수도 있지만 액면분할이 수정 반영되지 않은 것일 수도 있습니다. 날짜를 뉴스와 대조해 확인해야 합니다. 이 10분이 백테스트 전체를 살립니다.</p>

<h2 id="간단한-차트-그리기">간단한 차트 그리기</h2>

<p>받은 데이터로 종가 차트를 그려봅니다.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">matplotlib.pyplot</span> <span class="k">as</span> <span class="n">plt</span>

<span class="n">df</span><span class="p">[</span><span class="s">'Close'</span><span class="p">].</span><span class="n">plot</span><span class="p">(</span><span class="n">figsize</span><span class="o">=</span><span class="p">(</span><span class="mi">12</span><span class="p">,</span> <span class="mi">5</span><span class="p">),</span> <span class="n">title</span><span class="o">=</span><span class="s">'Samsung Electronics (005930)'</span><span class="p">)</span>
<span class="n">plt</span><span class="p">.</span><span class="n">ylabel</span><span class="p">(</span><span class="s">'Price (KRW)'</span><span class="p">)</span>
<span class="n">plt</span><span class="p">.</span><span class="n">tight_layout</span><span class="p">()</span>
<span class="n">plt</span><span class="p">.</span><span class="n">show</span><span class="p">()</span>
</code></pre></div></div>

<p>누적 수익률로 바꿔서 보면 서로 다른 자산을 비교하기 좋습니다.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># 일별 수익률 → 누적 수익률
</span><span class="n">returns</span> <span class="o">=</span> <span class="n">df</span><span class="p">[</span><span class="s">'Close'</span><span class="p">].</span><span class="n">pct_change</span><span class="p">().</span><span class="n">fillna</span><span class="p">(</span><span class="mi">0</span><span class="p">)</span>
<span class="n">cum_returns</span> <span class="o">=</span> <span class="p">(</span><span class="mi">1</span> <span class="o">+</span> <span class="n">returns</span><span class="p">).</span><span class="n">cumprod</span><span class="p">()</span> <span class="o">-</span> <span class="mi">1</span>
<span class="n">cum_returns</span><span class="p">.</span><span class="n">plot</span><span class="p">(</span><span class="n">figsize</span><span class="o">=</span><span class="p">(</span><span class="mi">12</span><span class="p">,</span> <span class="mi">5</span><span class="p">),</span> <span class="n">title</span><span class="o">=</span><span class="s">'Cumulative Return'</span><span class="p">)</span>
<span class="n">plt</span><span class="p">.</span><span class="n">show</span><span class="p">()</span>
</code></pre></div></div>

<h2 id="주의할-점">주의할 점</h2>

<p>데이터를 받는 것 자체는 쉽지만, 백테스트에 쓰기 전에 알아야 할 함정이 있습니다.</p>

<ol>
  <li><strong>수정주가 여부</strong> — 액면분할이나 배당이 반영된 가격인지 확인해야 합니다. 삼성전자는 2018년에 50:1 액면분할을 했기 때문에, 수정되지 않은 가격으로 계산하면 수익률이 완전히 틀어집니다.</li>
  <li><strong>상장폐지 종목</strong> — 현재 상장된 종목 목록으로 과거를 백테스트하면, 그 사이 망한 회사들이 빠져서 성과가 부풀려집니다(생존 편향).</li>
  <li><strong>무료 데이터의 한계</strong> — 실전 운용 수준의 정합성이 필요하면 결국 KRX 정보데이터시스템이나 증권사 API 같은 원천 데이터와 교차 검증이 필요합니다.</li>
  <li><strong>결측일 처리</strong> — 휴장일, 거래정지일에는 데이터가 없습니다. 여러 종목을 비교할 때 날짜 인덱스를 맞추지 않으면(<code class="language-plaintext highlighter-rouge">pd.concat</code> 후 <code class="language-plaintext highlighter-rouge">dropna</code> 또는 <code class="language-plaintext highlighter-rouge">ffill</code>) 계산이 어긋납니다.</li>
</ol>

<p>이 함정들은 각각 따로 글을 쓸 만큼 중요한 주제라, 시리즈에서 하나씩 다루겠습니다.</p>

<h2 id="대안-라이브러리-pykrx">대안 라이브러리: pykrx</h2>

<p>국내 데이터에 한정하면 <a href="https://github.com/sharebook-kr/pykrx">pykrx</a>라는 선택지도 있습니다. KRX 정보데이터시스템을 직접 조회하는 방식이라 시가총액, PER/PBR 같은 투자지표, 공매도 잔고 등 FinanceDataReader에 없는 데이터를 받을 수 있습니다. 다만 요청이 잦으면 차단될 수 있어 대량 수집에는 주의가 필요합니다. 두 라이브러리는 용도가 달라서, 저는 주가는 FinanceDataReader, 재무·지표 데이터는 pykrx로 나눠 쓰고 있습니다. pykrx 사용법도 별도 글로 정리하겠습니다.</p>

<h2 id="정리">정리</h2>

<ul>
  <li>FinanceDataReader는 설치 후 바로 국내외 주가를 DataFrame으로 받을 수 있다.</li>
  <li><code class="language-plaintext highlighter-rouge">StockListing('KRX')</code>로 종목 목록, <code class="language-plaintext highlighter-rouge">DataReader(코드, 시작일)</code>로 개별 주가를 받는다.</li>
  <li>백테스트에 쓰기 전에 수정주가, 생존 편향 같은 데이터 품질 문제를 반드시 확인한다.</li>
</ul>

<p>다음 글에서는 이 데이터를 이용해 가장 단순한 전략인 <strong>모멘텀 전략의 백테스트</strong>를 만들어 보겠습니다.</p>

<hr />

<p><em>이 글은 학습과 정보 공유 목적으로 작성되었으며, 특정 종목이나 상품에 대한 투자 권유가 아닙니다. 투자의 판단과 책임은 투자자 본인에게 있습니다.</em></p>]]></content><author><name>퀀트 노트</name></author><category term="파이썬" /><category term="FinanceDataReader" /><category term="pandas" /><category term="데이터수집" /><summary type="html"><![CDATA[퀀트 투자의 시작은 데이터입니다. 이번 글에서는 무료 오픈소스 라이브러리인 FinanceDataReader로 국내외 주가 데이터를 받아오는 방법을 정리합니다.]]></summary></entry></feed>