<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>MediaMaak 최신 글</title>
    <link>https://mediamaak.com/</link>
    <description>데이터·기술·경제를 읽는 MediaMaak의 최신 가이드와 공개 기록</description>
    <language>ko-KR</language>
    <lastBuildDate>Wed, 28 Oct 2026 18:52:03 +0900</lastBuildDate>
    <atom:link href="https://mediamaak.com/rss.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>Windows에서 Python venv 만들기</title>
      <link>https://mediamaak.com/books/auto-trading/python-venv/</link>
      <guid isPermaLink="true">https://mediamaak.com/books/auto-trading/python-venv/</guid>
      <pubDate>Tue, 20 Oct 2026 00:00:00 +0900</pubDate>
      <description>Windows에서 Python 실행 파일을 정하고 프로젝트 전용 venv를 만듭니다. Python과 pip 경로 확인, 런처·활성화 오류 대응과 패키지 점검을 안내합니다.</description>
      <content:encoded><![CDATA[<article class="pk-article" data-content-id="/books/auto-trading/python-venv/" data-content-type="wiki-chapter" data-published-at="2026-10-20T00:00:00+09:00" data-updated-at="2026-10-28T18:52:03+09:00">
      <header class="pk-hero">
        <p class="pk-eyebrow">2 / 12 · AUTO TRADING WIKI</p>
        <h1>Windows에서 Python venv 만들기</h1>
        <p class="pk-summary">Windows에서 Python 실행 파일을 정하고 프로젝트 전용 venv를 만듭니다. Python과 pip 경로 확인, 런처·활성화 오류 대응과 패키지 점검을 안내합니다.</p>
        <div class="pk-meta"><dl><dt>발행</dt><dd><time datetime="2026-10-20T00:00:00+09:00">2026-10-20</time></dd><dt>수정</dt><dd><time datetime="2026-10-28T18:52:03+09:00">2026-10-28</time></dd><dt>문서 검토</dt><dd>2026-10-28</dd><dt>본문 버전</dt><dd>1.2.0-web.20261028</dd></dl></div>
      </header>
      <div class="book-reading-content">
        <div class="pk-body-column">
          <div class="pk-prose"><p>프로젝트 폴더가 준비되었다면 그 안에서 코드를 실행할 Python 환경을 만든다. 이 장의 결과는 프로젝트 전용 <code>.venv</code>, 설치할 패키지 목록, 실제 실행 경로를 확인하는 점검 기록이다. API 접속과 데이터베이스 연결은 다음 장에서 진행한다.</p>
<p>예시의 <code>C:\Users\minjun\auto-trading-lab</code>은 가상 경로다. 자신의 프로젝트 위치로 바꾸고, PowerShell 명령은 그 폴더에서 실행한다.</p>
<h2 id="quick-start">내 환경에서 시작할 곳 찾기</h2>
<p>Windows에서 Python 가상환경을 만들려면 <strong>사용할 Python 실행 파일을 정한 뒤, 프로젝트 폴더에 <code>.venv</code>를 만들고 그 실행 파일로 패키지를 설치</strong>한다. <code>(.venv)</code> 표시만으로 완료를 판단하지 않고 실제 경로를 확인한다.</p>
<ul>
<li><strong>Python 설치 여부나 경로를 모른다면:</strong> <a href="#check-installation">설치 상태 확인</a>에서 사용할 실행 파일과 버전을 확인한다.</li>
<li><strong>Python은 있지만 <code>.venv</code>가 없다면:</strong> <a href="#create-venv">가상환경 만들기</a>에서 프로젝트 안의 <code>.venv\Scripts\python.exe</code>를 만든다.</li>
<li><strong><code>.venv</code>는 있지만 패키지를 못 찾는다면:</strong> <a href="#verify-interpreter">실행 경로 점검</a>으로 Python과 pip가 같은 환경을 가리키는지 확인한다.</li>
<li><strong>실행 정책 때문에 활성화가 막힌다면:</strong> <a href="#activation">활성화와 직접 실행</a>에서 프로젝트 Python을 직접 지정하는 방법을 확인한다.</li>
</ul>
<p>완료 후에는 <a href="#smoke-check">외부 연결 없는 import 확인</a>을 거쳐 <a href="https://mediamaak.com/books/auto-trading/kis-api/">한국투자증권 API 인증과 조회</a>로 이동한다.</p>
<h2 id="check-installation">1. 설치 전에 현재 상태 확인하기</h2>
<p>먼저 이미 설치된 Python과 편집기를 확인한다. 여러 Python을 구분하지 않고 설치를 반복하면 패키지를 넣은 환경과 프로그램을 실행하는 환경이 달라질 수 있다.</p>
<div class="sourceCode" id="cb1"><pre class="sourceCode powershell"><code class="sourceCode powershell"><span id="cb1-1"><a href="#cb1-1" aria-hidden="true" tabindex="-1"></a><span class="fu">Get-Location</span></span>
<span id="cb1-2"><a href="#cb1-2" aria-hidden="true" tabindex="-1"></a><span class="fu">Get-Command</span> python<span class="op">,</span> py<span class="op">,</span> code <span class="op">-</span>ErrorAction SilentlyContinue</span>
<span id="cb1-3"><a href="#cb1-3" aria-hidden="true" tabindex="-1"></a>python <span class="op">--</span>version</span>
<span id="cb1-4"><a href="#cb1-4" aria-hidden="true" tabindex="-1"></a>py <span class="op">--</span>version</span></code></pre></div>
<p>명령을 찾지 못했다는 결과는 해당 명령이 현재 터미널에서 보이지 않는다는 뜻이다. Python 자체가 없는 경우도 있고, 설치 직후라 터미널의 경로 정보가 갱신되지 않은 경우도 있다. 설치를 마쳤다면 PowerShell을 새로 열고 확인한다.</p>
<p>Codex에는 다음처럼 요청한다.</p>
<pre class="text"><code>현재 auto-trading-lab 프로젝트의 Windows 실행 환경을 확인해줘.
Python 실행 파일의 위치와 버전, py 명령, VS Code 설치 여부를 조사해줘.
설치하거나 설정을 바꾸지 말고, 이미 있는 Python 중 이 프로젝트에서
사용할 후보와 확인하지 못한 항목을 구분해줘.</code></pre>
<p>정상 결과는 버전 번호 하나가 아니라 <strong>어떤 실행 파일을 사용할지</strong>를 설명할 수 있는 상태다. VS Code는 파일을 읽기 위한 선택 도구이며 Python 실행에 필수는 아니다.</p>
<h2 id="choose-python">2. 지원 중인 Python 버전 정하기</h2>
<p>Python 공식 문서는 버전의 상태를 기능 개발, 사전 공개, 버그 수정, 보안 수정 등으로 나눈다. 이 책에서는 <code>Python LTS</code>라는 별도 배포판을 가정하지 않는다. 2026년 10월 28일 확인한 지원표에서 3.14와 3.15는 버그 수정 단계다. 이 실습은 <strong>Python 3.14 계열</strong>을 기준으로 하되, 설치 시점의 패키지 호환성과 공식 지원 상태를 함께 확인한다. <a href="https://devguide.python.org/versions/">Python 공식 버전 지원표</a></p>
<p>설치는 <a href="https://docs.python.org/3/using/windows.html">Python 공식 Windows 안내</a>를 따른다. Windows의 Python 설치 관리자와 기존 런처는 제공 명령이 다를 수 있으므로, 현재 설치 방식의 안내를 확인한다. 공식 다운로드 페이지에서 안정 버전을 선택하는 방법도 사용할 수 있다.</p>
<p>이 장에서는 기존 Python을 제거하거나 전역 기본값을 바꿀 필요가 없다. 3.14를 사용할 수 있는 환경이라면 다음처럼 명시적으로 확인한다.</p>
<div class="sourceCode" id="cb3"><pre class="sourceCode powershell"><code class="sourceCode powershell"><span id="cb3-1"><a href="#cb3-1" aria-hidden="true" tabindex="-1"></a>py <span class="op">-</span><span class="dv">3.14</span> <span class="op">--</span>version</span>
<span id="cb3-2"><a href="#cb3-2" aria-hidden="true" tabindex="-1"></a>py <span class="op">-</span><span class="dv">3.14</span> <span class="op">-</span>c <span class="st">&quot;import sys; print(sys.executable)&quot;</span></span></code></pre></div>
<p><code>3.14.x</code>와 해당 Python의 경로가 표시되면 다음 단계로 간다. <code>py</code>를 사용할 수 없다면 설치된 <code>python.exe</code>의 전체 경로로 같은 명령을 실행한다. 예를 들어 확인한 경로가 <code>C:\Python314\python.exe</code>라면 PowerShell에서는 <code>&amp; 'C:\Python314\python.exe' --version</code>처럼 호출한다. 이 경로는 형식 예시이며 자신의 PC에 같은 폴더가 있다고 가정하지 않는다. 지원이 끝난 버전이나 시험 버전을 우연히 선택한 상태에서는 패키지를 설치하기 전에 실행 파일부터 정한다.</p>
<h2 id="create-venv">3. 프로젝트 전용 가상환경 만들기</h2>
<p><code>venv</code>는 프로젝트마다 패키지를 따로 설치하는 표준 도구다. 코드와 설정은 프로젝트에 두고, 실행 파일과 설치 패키지는 <code>.venv</code>에 둔다. <code>.venv</code>는 다른 컴퓨터에 복사하기보다 설치 목록으로 다시 만든다. <a href="https://docs.python.org/3/library/venv.html">Python 공식 venv 설명</a></p>
<div class="sourceCode" id="cb4"><pre class="sourceCode powershell"><code class="sourceCode powershell"><span id="cb4-1"><a href="#cb4-1" aria-hidden="true" tabindex="-1"></a><span class="fu">Set-Location</span> <span class="st">&#39;C:\Users\minjun\auto-trading-lab&#39;</span></span>
<span id="cb4-2"><a href="#cb4-2" aria-hidden="true" tabindex="-1"></a>py <span class="op">-</span><span class="dv">3.14</span> <span class="op">-</span>m venv <span class="op">.</span><span class="fu">venv</span></span>
<span id="cb4-3"><a href="#cb4-3" aria-hidden="true" tabindex="-1"></a><span class="fu">Test-Path</span> <span class="st">&#39;.\.venv\Scripts\python.exe&#39;</span></span></code></pre></div>
<p>마지막 결과가 <code>True</code>이면 실행 파일이 생성된 것이다. 이미 <code>.venv</code>가 있다면 덮어 만들기 전에 그 환경의 버전과 설치 목록을 확인한다. 프로젝트 소스는 <code>.venv</code> 안에 작성하지 않는다.</p>
<p>이후 명령은 활성화 상태와 관계없이 프로젝트 Python을 직접 지정한다.</p>
<div class="sourceCode" id="cb5"><pre class="sourceCode powershell"><code class="sourceCode powershell"><span id="cb5-1"><a href="#cb5-1" aria-hidden="true" tabindex="-1"></a><span class="op">.</span>\<span class="op">.</span><span class="fu">venv</span>\Scripts\python<span class="op">.</span><span class="fu">exe</span> <span class="op">--</span>version</span>
<span id="cb5-2"><a href="#cb5-2" aria-hidden="true" tabindex="-1"></a><span class="op">.</span>\<span class="op">.</span><span class="fu">venv</span>\Scripts\python<span class="op">.</span><span class="fu">exe</span> <span class="op">-</span>c <span class="st">&quot;import sys; print(sys.executable); print(sys.prefix != sys.base_prefix)&quot;</span></span>
<span id="cb5-3"><a href="#cb5-3" aria-hidden="true" tabindex="-1"></a><span class="op">.</span>\<span class="op">.</span><span class="fu">venv</span>\Scripts\python<span class="op">.</span><span class="fu">exe</span> <span class="op">-</span>m pip <span class="op">--</span>version</span></code></pre></div>
<p>예상 결과는 <code>.venv\Scripts\python.exe</code> 경로, 가상환경 여부 <code>True</code>, <code>.venv</code> 아래의 pip 설치 경로다. 이 세 가지가 일치하면 전역 Python과 구분할 수 있다.</p>
<h3 id="verify-interpreter">Python과 pip의 실행 경로가 다를 때</h3>
<p><code>pip install</code>은 성공했는데 <code>ModuleNotFoundError</code>가 나오면, 패키지를 설치한 Python과 코드를 실행한 Python부터 비교한다. 프로젝트 루트에서 다음 세 줄을 실행한다.</p>
<div class="sourceCode" id="cb6"><pre class="sourceCode powershell"><code class="sourceCode powershell"><span id="cb6-1"><a href="#cb6-1" aria-hidden="true" tabindex="-1"></a><span class="fu">Get-Location</span></span>
<span id="cb6-2"><a href="#cb6-2" aria-hidden="true" tabindex="-1"></a><span class="op">.</span>\<span class="op">.</span><span class="fu">venv</span>\Scripts\python<span class="op">.</span><span class="fu">exe</span> <span class="op">-</span>c <span class="st">&quot;import sys; print(sys.executable)&quot;</span></span>
<span id="cb6-3"><a href="#cb6-3" aria-hidden="true" tabindex="-1"></a><span class="op">.</span>\<span class="op">.</span><span class="fu">venv</span>\Scripts\python<span class="op">.</span><span class="fu">exe</span> <span class="op">-</span>m pip <span class="op">--</span>version</span></code></pre></div>
<p>Python 경로가 현재 프로젝트의 <code>.venv\Scripts\python.exe</code>이고 pip 경로도 같은 <code>.venv</code> 안이면 환경이 일치한다. 실행 파일을 찾지 못하면 현재 폴더와 <code>.venv</code> 생성 여부를 먼저 확인한다. 이 두 경로가 확인되기 전에는 전역 Python에 패키지를 반복 설치하지 않는다.</p>
<p><code>py</code> 명령이 실패해도 기존 <code>.venv\Scripts\python.exe</code>가 정상 실행된다면 그 경로로 계속 사용할 수 있다. 런처가 찾는 설치 목록과 프로젝트 가상환경의 실행 가능 여부는 따로 확인한다.</p>
<h2 id="activation">4. 활성화와 직접 실행 구분하기</h2>
<p>매번 전체 경로를 입력하기 번거롭다면 현재 터미널에서 활성화할 수 있다.</p>
<div class="sourceCode" id="cb7"><pre class="sourceCode powershell"><code class="sourceCode powershell"><span id="cb7-1"><a href="#cb7-1" aria-hidden="true" tabindex="-1"></a><span class="op">.</span>\<span class="op">.</span><span class="fu">venv</span>\Scripts\Activate<span class="op">.</span><span class="fu">ps1</span></span>
<span id="cb7-2"><a href="#cb7-2" aria-hidden="true" tabindex="-1"></a>python <span class="op">-</span>c <span class="st">&quot;import sys; print(sys.executable)&quot;</span></span></code></pre></div>
<p>터미널 앞에 <code>(.venv)</code>가 나타나더라도 출력된 실행 파일을 확인한다. 활성화는 현재 터미널이 Python을 찾는 순서를 바꾸는 편의 기능이다. 새 터미널이나 작업 스케줄러에는 그 상태가 자동으로 이어지지 않는다.</p>
<p>실행 정책 때문에 <code>Activate.ps1</code>이 차단되면 이 장의 직접 경로 명령으로 계속 진행할 수 있다. 활성화를 위해 컴퓨터 전체의 실행 정책을 바꿀 필요는 없다. 조직에서 관리하는 PC라면 현재 정책과 관리 규칙을 먼저 확인한다. <a href="https://learn.microsoft.com/powershell/module/microsoft.powershell.core/about/about_execution_policies">PowerShell 실행 정책 설명</a></p>
<p>활성화한 터미널에서 원래 상태로 돌아갈 때는 <code>deactivate</code>를 실행한다. 프로젝트 작업이 끝나도 <code>.venv</code> 폴더 자체는 남아 있다.</p>
<h2 id="dependencies">5. 필요한 패키지와 설치 목록 준비하기</h2>
<p>이 책은 HTTP 조회에 <code>requests</code>, 표 데이터 처리에 <code>pandas</code>, 설정 파일 읽기에 <code>python-dotenv</code>, MySQL 연결에 <code>mysql-connector-python</code>을 사용한다. 처음에는 이 네 가지로 시작한다. 실시간 웹소켓이나 차트 라이브러리는 해당 기능이 필요해질 때 추가한다.</p>
<p>프로젝트 루트의 <code>requirements.in</code>에 다음 내용을 저장한다. 이 파일은 직접 선택한 의존성 목록이다.</p>
<pre class="text"><code>requests
pandas
python-dotenv
mysql-connector-python</code></pre>
<p>프로젝트 가상환경에 설치한다. 기존 프로젝트에 검증된 <code>requirements.txt</code>가 있다면 새 목록을 만들기보다 그 파일을 우선 사용한다.</p>
<div class="sourceCode" id="cb9"><pre class="sourceCode powershell"><code class="sourceCode powershell"><span id="cb9-1"><a href="#cb9-1" aria-hidden="true" tabindex="-1"></a><span class="op">.</span>\<span class="op">.</span><span class="fu">venv</span>\Scripts\python<span class="op">.</span><span class="fu">exe</span> <span class="op">-</span>m pip install <span class="op">--</span>upgrade pip</span>
<span id="cb9-2"><a href="#cb9-2" aria-hidden="true" tabindex="-1"></a><span class="op">.</span>\<span class="op">.</span><span class="fu">venv</span>\Scripts\python<span class="op">.</span><span class="fu">exe</span> <span class="op">-</span>m pip install <span class="op">-</span>r requirements<span class="op">.</span><span class="fu">in</span></span>
<span id="cb9-3"><a href="#cb9-3" aria-hidden="true" tabindex="-1"></a><span class="op">.</span>\<span class="op">.</span><span class="fu">venv</span>\Scripts\python<span class="op">.</span><span class="fu">exe</span> <span class="op">-</span>m pip check</span></code></pre></div>
<p>정상 결과는 의존성 충돌 없이 설치가 끝나는 것이다. <code>pip check</code>가 통과한 뒤 설치된 정확한 버전을 기록한다. PowerShell 5.1에서도 텍스트 인코딩이 명확하도록 다음처럼 저장한다.</p>
<div class="sourceCode" id="cb10"><pre class="sourceCode powershell"><code class="sourceCode powershell"><span id="cb10-1"><a href="#cb10-1" aria-hidden="true" tabindex="-1"></a><span class="op">.</span>\<span class="op">.</span><span class="fu">venv</span>\Scripts\python<span class="op">.</span><span class="fu">exe</span> <span class="op">-</span>m pip freeze <span class="op">|</span> <span class="fu">Set-Content</span> <span class="op">-</span>Encoding UTF8 requirements<span class="op">.</span><span class="fu">txt</span></span>
<span id="cb10-2"><a href="#cb10-2" aria-hidden="true" tabindex="-1"></a><span class="fu">Get-Content</span> <span class="op">-</span>Encoding UTF8 requirements<span class="op">.</span><span class="fu">txt</span></span></code></pre></div>
<p>다른 컴퓨터에서는 같은 Python 계열로 <code>.venv</code>를 새로 만든 뒤 <code>-m pip install -r requirements.txt</code>로 설치한다. 운영체제와 Python 버전도 함께 기록해야 재현 범위를 이해할 수 있다. <a href="https://packaging.python.org/en/latest/guides/installing-using-pip-and-virtual-environments/">Python Packaging 공식 설치 안내</a></p>
<h2 id="smoke-check">6. 외부 연결 없이 최소 실행 확인하기</h2>
<p>패키지가 설치됐는지 확인하는 명령은 외부 API에 접속하지 않는다.</p>
<div class="sourceCode" id="cb11"><pre class="sourceCode powershell"><code class="sourceCode powershell"><span id="cb11-1"><a href="#cb11-1" aria-hidden="true" tabindex="-1"></a><span class="op">.</span>\<span class="op">.</span><span class="fu">venv</span>\Scripts\python<span class="op">.</span><span class="fu">exe</span> <span class="op">-</span>c <span class="st">&quot;import requests, pandas, dotenv, mysql.connector; print(&#39;imports OK&#39;)&quot;</span></span></code></pre></div>
<p><code>imports OK</code>가 출력되면 이번 장의 import 확인이 끝난다. 패키지 이름 <code>python-dotenv</code>와 Python에서 사용하는 모듈 이름 <code>dotenv</code>가 다른 점에 주의한다.</p>
<p>프로젝트에 이미 <code>src</code>의 Python 파일이 있다면 문법도 확인할 수 있다.</p>
<div class="sourceCode" id="cb12"><pre class="sourceCode powershell"><code class="sourceCode powershell"><span id="cb12-1"><a href="#cb12-1" aria-hidden="true" tabindex="-1"></a><span class="op">.</span>\<span class="op">.</span><span class="fu">venv</span>\Scripts\python<span class="op">.</span><span class="fu">exe</span> <span class="op">-</span>m compileall <span class="op">-</span>q src</span></code></pre></div>
<p>출력이 없고 종료 코드가 0이면 문법 검사에서 오류가 발견되지 않은 것이다. 이 검사는 API 인증, SQL 실행, 계산식의 정확성까지 증명하지 않는다. 각 단계의 실제 동작은 해당 장에서 별도로 확인한다.</p>
<p>Git을 사용한다면 <code>.gitignore</code>에 <code>.venv/</code>, <code>config/.env</code>, <code>config/*token*</code>, <code>logs/</code>를 포함한다. 공유할 결과 파일에 개인 정보가 없는지도 확인한다. 이 제외 목록은 나중에 만드는 파일의 비밀값 검토를 대신하지 않는다.</p>
<h2 id="troubleshooting">7. 오류를 실행 경로부터 좁히기</h2>
<table>
<colgroup>
<col style="width: 33%" />
<col style="width: 33%" />
<col style="width: 33%" />
</colgroup>
<thead>
<tr>
<th>증상</th>
<th>먼저 확인할 것</th>
<th>다음 조치</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>ModuleNotFoundError</code></td>
<td>실행 파일과 pip 경로가 같은 <code>.venv</code>인가</td>
<td><code>.venv</code> Python으로 설치 명령을 다시 실행한다</td>
</tr>
<tr>
<td><code>py -3.14</code>가 실패함</td>
<td>해당 버전이 설치되어 있는가</td>
<td>공식 설치 안내에서 설치된 런타임을 확인한다</td>
</tr>
<tr>
<td><code>Activate.ps1</code> 차단</td>
<td>스크립트 실행 정책 때문인가</td>
<td>활성화 없이 <code>.venv</code> Python 경로를 사용한다</td>
</tr>
<tr>
<td>설치 중 인증서·프록시 오류</td>
<td>네트워크와 조직 프록시 설정</td>
<td>오류를 보존하고 승인된 네트워크 설정을 확인한다</td>
</tr>
<tr>
<td>폴더를 옮긴 뒤 실행 실패</td>
<td><code>.venv</code>가 이전 위치에 의존하는가</td>
<td>새 위치에서 설치 목록으로 환경을 다시 만든다</td>
</tr>
</tbody>
</table>
<p>설치 오류를 해결하려고 TLS 검증을 끄거나 임의의 패키지 파일을 내려받지 않는다. 오류의 원인이 Python 버전, 네트워크, 패키지 호환성 중 어디에 있는지 나누어 확인한다.</p>
<h2 id="completion">8. 완료 기록 남기기</h2>
<p><code>docs/environment-check.md</code>에는 Python 버전, 실행 파일 경로, 패키지 설치 목록, <code>pip check</code>와 import 확인 결과를 적는다. 실제로 실행하지 않은 명령은 “미실행”으로 표시한다.</p>
<p>다음 네 가지가 맞으면 3장의 인증 준비로 넘어간다.</p>
<ul>
<li>프로젝트 전용 <code>.venv</code> 실행 파일이 확인된다.</li>
<li>필요한 패키지를 같은 환경에서 import할 수 있다.</li>
<li><code>requirements.txt</code>에 설치 버전이 기록되어 있다.</li>
<li>비밀 설정과 <code>.venv</code>를 공유 대상에서 제외했다.</li>
</ul>
<p>이 장의 명령은 환경 준비 예시다. 문서 자체가 독자의 컴퓨터에 Python이나 패키지를 설치하지 않으며, 설치 성공 여부는 자신의 실행 결과로 확인한다.</p>
</div>
          <section class="pk-related" aria-labelledby="related-heading"><p class="pk-eyebrow">FROM CONCEPT TO PRACTICE</p><h2 id="related-heading">구현 기록과 함께 읽기</h2><ul><li><a href="https://mediamaak.com/books/auto-trading/project-setup/">Codex 프로젝트 폴더 설정과 경로 확인</a></li></ul></section>
          <nav class="pk-pager" aria-label="이전 장과 다음 장"><a href="https://mediamaak.com/books/auto-trading/project-setup/">이전 글: Codex 프로젝트 폴더 설정과 경로 확인</a><a href="https://mediamaak.com/books/auto-trading/kis-api/">다음 글: 한국투자증권 API 인증과 현재가 조회</a></nav>
          <div class="pk-share"><div><p class="pk-share-title">학습의 다음 단계를 남겨 두세요</p><p>공유 링크를 복사해 이어서 읽을 수 있습니다.</p></div><div><button type="button" data-share-content>공유 링크 복사</button><span role="status" data-share-status></span></div></div>
        </div>
      </div>
    </article>]]></content:encoded>
    </item>
    <item>
      <title>한국투자증권 API 인증과 현재가 조회</title>
      <link>https://mediamaak.com/books/auto-trading/kis-api/</link>
      <guid isPermaLink="true">https://mediamaak.com/books/auto-trading/kis-api/</guid>
      <pubDate>Tue, 20 Oct 2026 00:00:00 +0900</pubDate>
      <description>한국투자증권 Open API의 서버·키 환경을 맞추고 인증과 현재가 한 건 조회를 확인합니다. 준비물, 실행 순서, 정상 출력과 오류 대응을 안내합니다.</description>
      <content:encoded><![CDATA[<article class="pk-article" data-content-id="/books/auto-trading/kis-api/" data-content-type="wiki-chapter" data-published-at="2026-10-20T00:00:00+09:00" data-updated-at="2026-10-28T18:52:03+09:00">
      <header class="pk-hero">
        <p class="pk-eyebrow">3 / 12 · AUTO TRADING WIKI</p>
        <h1>한국투자증권 API 인증과 현재가 조회</h1>
        <p class="pk-summary">한국투자증권 Open API의 서버·키 환경을 맞추고 인증과 현재가 한 건 조회를 확인합니다. 준비물, 실행 순서, 정상 출력과 오류 대응을 안내합니다.</p>
        <div class="pk-meta"><dl><dt>발행</dt><dd><time datetime="2026-10-20T00:00:00+09:00">2026-10-20</time></dd><dt>수정</dt><dd><time datetime="2026-10-28T18:52:03+09:00">2026-10-28</time></dd><dt>문서 검토</dt><dd>2026-10-28</dd><dt>본문 버전</dt><dd>1.2.0-web.20261028</dd></dl></div>
      </header>
      <div class="book-reading-content">
        <div class="pk-body-column">
          <div class="pk-prose"><p>한국투자증권 Open API와 연결할 때 첫 결과는 주문 체결이 아니라 <strong>인증 성공과 시세 한 건의 조회</strong>다. 이 장에서는 개인 설정을 코드에서 분리하고, 인증과 정해진 시세 조회만 수행하는 클라이언트를 만든다.</p>
<p>앞 장의 <code>.venv</code>와 패키지가 준비되어 있어야 한다. 계좌 개설, Open API 서비스 신청, 모의투자 이용 조건은 한국투자증권의 현재 안내에서 확인한다. 문서에 나온 키·계좌 정보는 모두 자리표시자이며 실제 값을 채팅이나 Git에 붙여 넣지 않는다.</p>
<h2 id="quick-start">인증과 현재가 조회까지 가는 순서</h2>
<p>처음 연결한다면 아래 세 단계를 차례로 진행한다. 이미 설정을 마쳤다면 <a href="#read-only-api-test">한 종목 조회</a>부터 실행하고, 실패한 단계가 있으면 <a href="#completion">오류 대응</a>으로 이동한다.</p>
<ol type="1">
<li><strong>프로젝트 Python을 확인한다.</strong> <a href="https://mediamaak.com/books/auto-trading/python-venv/#smoke-check">앞 장의 환경 점검</a>으로 같은 <code>.venv</code>에서 <code>requests</code>와 <code>dotenv</code>를 import할 수 있는지 확인한다.</li>
<li><strong>조회 도구를 작성한다.</strong> <a href="#secret-config">개인 설정</a>과 <a href="#readonly-client">인증·조회 코드</a>를 만든다. 선택한 서버와 키의 환경이 같고 문법 검사에 통과하면 다음 단계로 간다.</li>
<li><strong>현재가 한 건을 확인한다.</strong> 프로젝트 루트에서 4절의 명령으로 <code>src.check_kis</code>를 실행한다. 환경·종목코드·양의 가격과 <code>quotation OK</code>가 표시되면 이번 장의 조회 확인이 끝난다.</li>
</ol>
<p>이 장에서 만들 파일은 <code>config/example.env</code>, 개인용 <code>config/.env</code>, <code>src/kis_readonly.py</code>, <code>src/check_kis.py</code>다. 먼저 아래 본문을 따라 파일을 작성한 뒤 4절의 실행 명령을 사용한다. <code>.venv</code>가 없다면 <a href="https://mediamaak.com/books/auto-trading/python-venv/">Python 환경 준비</a>로 돌아간다.</p>
<h2 id="environment-and-mode">1. 서버 환경과 프로그램 실행 모드 구분하기</h2>
<p>한국투자증권은 실전과 모의투자 환경을 구분한다. 공식 예제도 각 환경의 앱 키와 앱 시크릿을 따로 선택한다. 여기서는 서버 선택을 <code>KIS_ENV</code>, 프로젝트의 동작 범위를 <code>RUN_MODE</code>로 나눈다. <a href="https://github.com/koreainvestment/open-trading-api">한국투자증권 공식 저장소</a></p>
<table>
<colgroup>
<col style="width: 33%" />
<col style="width: 33%" />
<col style="width: 33%" />
</colgroup>
<thead>
<tr>
<th>설정</th>
<th>이번 장의 값</th>
<th>의미</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>KIS_ENV</code></td>
<td><code>mock</code></td>
<td>증권사 모의투자 서버와 그 서버의 인증 정보를 사용한다</td>
</tr>
<tr>
<td><code>KIS_ENV</code></td>
<td><code>live</code></td>
<td>증권사 실전 서버와 그 서버의 인증 정보를 사용한다</td>
</tr>
<tr>
<td><code>RUN_MODE</code></td>
<td><code>read_only</code></td>
<td>프로젝트가 인증과 조회만 수행한다</td>
</tr>
</tbody>
</table>
<p><code>mock</code>은 증권사 서버 환경이다. 뒤에서 만드는 <code>paper</code>는 우리 프로그램이 가상 주문을 장부에 기록하는 모드다. 둘은 같은 개념이 아니다. 모의투자 서버를 사용하더라도 주문 API를 호출하면 증권사 모의 주문이 발생할 수 있으므로, 이번 클라이언트에는 주문 메서드를 만들지 않는다.</p>
<p>실전 서버로 시세를 조회할 수 있다는 사실과 실전 주문을 허용한다는 것은 별개다. 이번 장의 예제는 서버 선택과 무관하게 인증 POST와 지정한 시세 GET만 제공한다.</p>
<h2 id="secret-config">2. 개인 설정 파일 만들기</h2>
<p>공유용 <code>config/example.env</code>에는 다음 항목만 둔다.</p>
<pre class="dotenv"><code>RUN_MODE=read_only
KIS_ENV=mock
KIS_APP_KEY=REPLACE_WITH_YOUR_KEY
KIS_APP_SECRET=REPLACE_WITH_YOUR_SECRET</code></pre>
<p>이 파일을 로컬의 <code>config/.env</code>로 복사하고, 자신이 발급받은 <strong>해당 환경의</strong> 값을 입력한다. 현재가 조회에는 계좌번호를 보내지 않으므로 이 장에서 계좌 정보를 추가할 필요가 없다.</p>
<div class="sourceCode" id="cb2"><pre class="sourceCode powershell"><code class="sourceCode powershell"><span id="cb2-1"><a href="#cb2-1" aria-hidden="true" tabindex="-1"></a><span class="cf">if</span> <span class="op">(-not</span> <span class="op">(</span><span class="fu">Test-Path</span> <span class="op">-</span>LiteralPath <span class="st">&#39;config/.env&#39;</span><span class="op">))</span> <span class="op">{</span></span>
<span id="cb2-2"><a href="#cb2-2" aria-hidden="true" tabindex="-1"></a>    <span class="fu">Copy-Item</span> <span class="op">-</span>LiteralPath <span class="st">&#39;config/example.env&#39;</span> <span class="op">-</span>Destination <span class="st">&#39;config/.env&#39;</span></span>
<span id="cb2-3"><a href="#cb2-3" aria-hidden="true" tabindex="-1"></a><span class="op">}</span></span></code></pre></div>
<p>이미 <code>config/.env</code>가 있다면 복사로 덮어쓰지 말고 필요한 항목만 보완한다. 이 책은 <code>dotenv_values</code>로 지정 파일을 직접 읽는다. 운영체제 환경변수가 우연히 같은 이름으로 존재해도 자동으로 섞지 않는다. <a href="https://bbc2.github.io/python-dotenv/">python-dotenv 공식 문서</a></p>
<p>Git의 제외 대상에 <code>config/.env</code>가 있는지 확인한다. 앱 시크릿을 잘못 공개했다면 파일에서 지우는 것만으로 끝내지 말고 발급처에서 폐기·재발급 절차를 진행한다. 토큰도 인증 수단이므로 화면이나 로그에 원문을 출력하지 않는다.</p>
<h2 id="readonly-client">3. 인증과 시세 조회 코드 작성하기</h2>
<p><code>src/kis_readonly.py</code>에 다음 코드를 저장한다. 종목코드는 ASCII 숫자와 영문 대문자를 조합한 6자리 문자열로 받는다. <code>005930</code>의 앞자리 0을 보존하고 <code>0126Z0</code> 같은 영문 포함 코드도 허용한다. 소문자나 다른 문자 체계의 숫자를 임의 변환하지 않는다. 예제는 한 프로세스 안에서 발급받은 토큰을 재사용한다. 호출할 때마다 새 토큰을 요청하지 않도록 클라이언트도 한 번 만들어 재사용한다.</p>
<p>인증 경로와 서버 주소는 <a href="https://github.com/koreainvestment/open-trading-api/blob/main/examples_user/kis_auth.py">공식 인증 예제</a>를 확인했다. 토큰의 발급 제한·만료 정책은 바뀔 수 있으므로 반복 운영에 들어가기 전에는 공식 안내에 맞춰 만료 시각 관리와 안전한 캐시를 추가한다.</p>
<div class="sourceCode" id="cb3"><pre class="sourceCode python"><code class="sourceCode python"><span id="cb3-1"><a href="#cb3-1" aria-hidden="true" tabindex="-1"></a><span class="im">from</span> pathlib <span class="im">import</span> Path</span>
<span id="cb3-2"><a href="#cb3-2" aria-hidden="true" tabindex="-1"></a><span class="im">import</span> re</span>
<span id="cb3-3"><a href="#cb3-3" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-4"><a href="#cb3-4" aria-hidden="true" tabindex="-1"></a><span class="im">import</span> requests</span>
<span id="cb3-5"><a href="#cb3-5" aria-hidden="true" tabindex="-1"></a><span class="im">from</span> dotenv <span class="im">import</span> dotenv_values</span>
<span id="cb3-6"><a href="#cb3-6" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-7"><a href="#cb3-7" aria-hidden="true" tabindex="-1"></a>ROOT <span class="op">=</span> Path(<span class="va">__file__</span>).resolve().parents[<span class="dv">1</span>]</span>
<span id="cb3-8"><a href="#cb3-8" aria-hidden="true" tabindex="-1"></a>SERVERS <span class="op">=</span> {</span>
<span id="cb3-9"><a href="#cb3-9" aria-hidden="true" tabindex="-1"></a>    <span class="st">&quot;mock&quot;</span>: <span class="st">&quot;https://openapivts.koreainvestment.com:29443&quot;</span>,</span>
<span id="cb3-10"><a href="#cb3-10" aria-hidden="true" tabindex="-1"></a>    <span class="st">&quot;live&quot;</span>: <span class="st">&quot;https://openapi.koreainvestment.com:9443&quot;</span>,</span>
<span id="cb3-11"><a href="#cb3-11" aria-hidden="true" tabindex="-1"></a>}</span>
<span id="cb3-12"><a href="#cb3-12" aria-hidden="true" tabindex="-1"></a>QUOTATIONS <span class="op">=</span> {</span>
<span id="cb3-13"><a href="#cb3-13" aria-hidden="true" tabindex="-1"></a>    <span class="st">&quot;price&quot;</span>: (</span>
<span id="cb3-14"><a href="#cb3-14" aria-hidden="true" tabindex="-1"></a>        <span class="st">&quot;/uapi/domestic-stock/v1/quotations/inquire-price&quot;</span>,</span>
<span id="cb3-15"><a href="#cb3-15" aria-hidden="true" tabindex="-1"></a>        <span class="st">&quot;FHKST01010100&quot;</span>,</span>
<span id="cb3-16"><a href="#cb3-16" aria-hidden="true" tabindex="-1"></a>    ),</span>
<span id="cb3-17"><a href="#cb3-17" aria-hidden="true" tabindex="-1"></a>    <span class="st">&quot;daily&quot;</span>: (</span>
<span id="cb3-18"><a href="#cb3-18" aria-hidden="true" tabindex="-1"></a>        <span class="st">&quot;/uapi/domestic-stock/v1/quotations/inquire-daily-itemchartprice&quot;</span>,</span>
<span id="cb3-19"><a href="#cb3-19" aria-hidden="true" tabindex="-1"></a>        <span class="st">&quot;FHKST03010100&quot;</span>,</span>
<span id="cb3-20"><a href="#cb3-20" aria-hidden="true" tabindex="-1"></a>    ),</span>
<span id="cb3-21"><a href="#cb3-21" aria-hidden="true" tabindex="-1"></a>}</span>
<span id="cb3-22"><a href="#cb3-22" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-23"><a href="#cb3-23" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-24"><a href="#cb3-24" aria-hidden="true" tabindex="-1"></a><span class="kw">def</span> load_config():</span>
<span id="cb3-25"><a href="#cb3-25" aria-hidden="true" tabindex="-1"></a>    path <span class="op">=</span> ROOT <span class="op">/</span> <span class="st">&quot;config&quot;</span> <span class="op">/</span> <span class="st">&quot;.env&quot;</span></span>
<span id="cb3-26"><a href="#cb3-26" aria-hidden="true" tabindex="-1"></a>    <span class="cf">if</span> <span class="kw">not</span> path.is_file():</span>
<span id="cb3-27"><a href="#cb3-27" aria-hidden="true" tabindex="-1"></a>        <span class="cf">raise</span> <span class="pp">FileNotFoundError</span>(<span class="st">&quot;config/.env is missing&quot;</span>)</span>
<span id="cb3-28"><a href="#cb3-28" aria-hidden="true" tabindex="-1"></a>    <span class="cf">return</span> <span class="bu">dict</span>(dotenv_values(path, encoding<span class="op">=</span><span class="st">&quot;utf-8-sig&quot;</span>))</span>
<span id="cb3-29"><a href="#cb3-29" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-30"><a href="#cb3-30" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-31"><a href="#cb3-31" aria-hidden="true" tabindex="-1"></a><span class="kw">class</span> ReadOnlyKis:</span>
<span id="cb3-32"><a href="#cb3-32" aria-hidden="true" tabindex="-1"></a>    <span class="kw">def</span> <span class="fu">__init__</span>(<span class="va">self</span>, config):</span>
<span id="cb3-33"><a href="#cb3-33" aria-hidden="true" tabindex="-1"></a>        mode <span class="op">=</span> config.get(<span class="st">&quot;RUN_MODE&quot;</span>, <span class="st">&quot;read_only&quot;</span>)</span>
<span id="cb3-34"><a href="#cb3-34" aria-hidden="true" tabindex="-1"></a>        <span class="cf">if</span> mode <span class="kw">not</span> <span class="kw">in</span> {<span class="st">&quot;read_only&quot;</span>, <span class="st">&quot;paper&quot;</span>}:</span>
<span id="cb3-35"><a href="#cb3-35" aria-hidden="true" tabindex="-1"></a>            <span class="cf">raise</span> <span class="pp">ValueError</span>(<span class="st">&quot;This client is for read_only or local paper mode&quot;</span>)</span>
<span id="cb3-36"><a href="#cb3-36" aria-hidden="true" tabindex="-1"></a>        env <span class="op">=</span> config.get(<span class="st">&quot;KIS_ENV&quot;</span>, <span class="st">&quot;mock&quot;</span>)</span>
<span id="cb3-37"><a href="#cb3-37" aria-hidden="true" tabindex="-1"></a>        <span class="cf">if</span> env <span class="kw">not</span> <span class="kw">in</span> SERVERS:</span>
<span id="cb3-38"><a href="#cb3-38" aria-hidden="true" tabindex="-1"></a>            <span class="cf">raise</span> <span class="pp">ValueError</span>(<span class="st">&quot;KIS_ENV must be mock or live&quot;</span>)</span>
<span id="cb3-39"><a href="#cb3-39" aria-hidden="true" tabindex="-1"></a>        <span class="va">self</span>.base_url <span class="op">=</span> SERVERS[env]</span>
<span id="cb3-40"><a href="#cb3-40" aria-hidden="true" tabindex="-1"></a>        <span class="va">self</span>.key <span class="op">=</span> config.get(<span class="st">&quot;KIS_APP_KEY&quot;</span>) <span class="kw">or</span> <span class="st">&quot;&quot;</span></span>
<span id="cb3-41"><a href="#cb3-41" aria-hidden="true" tabindex="-1"></a>        <span class="va">self</span>.secret <span class="op">=</span> config.get(<span class="st">&quot;KIS_APP_SECRET&quot;</span>) <span class="kw">or</span> <span class="st">&quot;&quot;</span></span>
<span id="cb3-42"><a href="#cb3-42" aria-hidden="true" tabindex="-1"></a>        <span class="cf">if</span> <span class="bu">any</span>(<span class="kw">not</span> v <span class="kw">or</span> v.startswith(<span class="st">&quot;REPLACE_&quot;</span>) <span class="cf">for</span> v <span class="kw">in</span> (<span class="va">self</span>.key, <span class="va">self</span>.secret)):</span>
<span id="cb3-43"><a href="#cb3-43" aria-hidden="true" tabindex="-1"></a>            <span class="cf">raise</span> <span class="pp">ValueError</span>(<span class="st">&quot;KIS credentials are not configured&quot;</span>)</span>
<span id="cb3-44"><a href="#cb3-44" aria-hidden="true" tabindex="-1"></a>        <span class="va">self</span>.session <span class="op">=</span> requests.Session()</span>
<span id="cb3-45"><a href="#cb3-45" aria-hidden="true" tabindex="-1"></a>        <span class="va">self</span>.token <span class="op">=</span> <span class="va">None</span></span>
<span id="cb3-46"><a href="#cb3-46" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-47"><a href="#cb3-47" aria-hidden="true" tabindex="-1"></a>    <span class="at">@staticmethod</span></span>
<span id="cb3-48"><a href="#cb3-48" aria-hidden="true" tabindex="-1"></a>    <span class="kw">def</span> _json(response):</span>
<span id="cb3-49"><a href="#cb3-49" aria-hidden="true" tabindex="-1"></a>        <span class="cf">if</span> response.status_code <span class="op">!=</span> <span class="dv">200</span>:</span>
<span id="cb3-50"><a href="#cb3-50" aria-hidden="true" tabindex="-1"></a>            <span class="cf">raise</span> <span class="pp">RuntimeError</span>(<span class="ss">f&quot;KIS HTTP error: </span><span class="sc">{</span>response<span class="sc">.</span>status_code<span class="sc">}</span><span class="ss">&quot;</span>)</span>
<span id="cb3-51"><a href="#cb3-51" aria-hidden="true" tabindex="-1"></a>        <span class="cf">try</span>:</span>
<span id="cb3-52"><a href="#cb3-52" aria-hidden="true" tabindex="-1"></a>            body <span class="op">=</span> response.json()</span>
<span id="cb3-53"><a href="#cb3-53" aria-hidden="true" tabindex="-1"></a>        <span class="cf">except</span> <span class="pp">ValueError</span> <span class="im">as</span> exc:</span>
<span id="cb3-54"><a href="#cb3-54" aria-hidden="true" tabindex="-1"></a>            <span class="cf">raise</span> <span class="pp">RuntimeError</span>(<span class="st">&quot;KIS returned non-JSON content&quot;</span>) <span class="im">from</span> exc</span>
<span id="cb3-55"><a href="#cb3-55" aria-hidden="true" tabindex="-1"></a>        <span class="cf">if</span> <span class="kw">not</span> <span class="bu">isinstance</span>(body, <span class="bu">dict</span>):</span>
<span id="cb3-56"><a href="#cb3-56" aria-hidden="true" tabindex="-1"></a>            <span class="cf">raise</span> <span class="pp">RuntimeError</span>(<span class="st">&quot;KIS returned an unexpected JSON structure&quot;</span>)</span>
<span id="cb3-57"><a href="#cb3-57" aria-hidden="true" tabindex="-1"></a>        <span class="cf">return</span> body</span>
<span id="cb3-58"><a href="#cb3-58" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-59"><a href="#cb3-59" aria-hidden="true" tabindex="-1"></a>    <span class="kw">def</span> authenticate(<span class="va">self</span>):</span>
<span id="cb3-60"><a href="#cb3-60" aria-hidden="true" tabindex="-1"></a>        <span class="cf">if</span> <span class="va">self</span>.token:</span>
<span id="cb3-61"><a href="#cb3-61" aria-hidden="true" tabindex="-1"></a>            <span class="cf">return</span></span>
<span id="cb3-62"><a href="#cb3-62" aria-hidden="true" tabindex="-1"></a>        response <span class="op">=</span> <span class="va">self</span>.session.post(</span>
<span id="cb3-63"><a href="#cb3-63" aria-hidden="true" tabindex="-1"></a>            <span class="va">self</span>.base_url <span class="op">+</span> <span class="st">&quot;/oauth2/tokenP&quot;</span>,</span>
<span id="cb3-64"><a href="#cb3-64" aria-hidden="true" tabindex="-1"></a>            json<span class="op">=</span>{</span>
<span id="cb3-65"><a href="#cb3-65" aria-hidden="true" tabindex="-1"></a>                <span class="st">&quot;grant_type&quot;</span>: <span class="st">&quot;client_credentials&quot;</span>,</span>
<span id="cb3-66"><a href="#cb3-66" aria-hidden="true" tabindex="-1"></a>                <span class="st">&quot;appkey&quot;</span>: <span class="va">self</span>.key,</span>
<span id="cb3-67"><a href="#cb3-67" aria-hidden="true" tabindex="-1"></a>                <span class="st">&quot;appsecret&quot;</span>: <span class="va">self</span>.secret,</span>
<span id="cb3-68"><a href="#cb3-68" aria-hidden="true" tabindex="-1"></a>            },</span>
<span id="cb3-69"><a href="#cb3-69" aria-hidden="true" tabindex="-1"></a>            timeout<span class="op">=</span>(<span class="dv">5</span>, <span class="dv">20</span>),</span>
<span id="cb3-70"><a href="#cb3-70" aria-hidden="true" tabindex="-1"></a>        )</span>
<span id="cb3-71"><a href="#cb3-71" aria-hidden="true" tabindex="-1"></a>        body <span class="op">=</span> <span class="va">self</span>._json(response)</span>
<span id="cb3-72"><a href="#cb3-72" aria-hidden="true" tabindex="-1"></a>        token <span class="op">=</span> body.get(<span class="st">&quot;access_token&quot;</span>)</span>
<span id="cb3-73"><a href="#cb3-73" aria-hidden="true" tabindex="-1"></a>        <span class="cf">if</span> <span class="kw">not</span> <span class="bu">isinstance</span>(token, <span class="bu">str</span>) <span class="kw">or</span> <span class="kw">not</span> token:</span>
<span id="cb3-74"><a href="#cb3-74" aria-hidden="true" tabindex="-1"></a>            <span class="cf">raise</span> <span class="pp">RuntimeError</span>(<span class="st">&quot;KIS authentication failed; check the API portal&quot;</span>)</span>
<span id="cb3-75"><a href="#cb3-75" aria-hidden="true" tabindex="-1"></a>        <span class="va">self</span>.token <span class="op">=</span> token</span>
<span id="cb3-76"><a href="#cb3-76" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-77"><a href="#cb3-77" aria-hidden="true" tabindex="-1"></a>    <span class="kw">def</span> quotations(<span class="va">self</span>, kind, params):</span>
<span id="cb3-78"><a href="#cb3-78" aria-hidden="true" tabindex="-1"></a>        <span class="cf">if</span> kind <span class="kw">not</span> <span class="kw">in</span> QUOTATIONS:</span>
<span id="cb3-79"><a href="#cb3-79" aria-hidden="true" tabindex="-1"></a>            <span class="cf">raise</span> <span class="pp">ValueError</span>(<span class="st">&quot;Unsupported quotation kind&quot;</span>)</span>
<span id="cb3-80"><a href="#cb3-80" aria-hidden="true" tabindex="-1"></a>        <span class="va">self</span>.authenticate()</span>
<span id="cb3-81"><a href="#cb3-81" aria-hidden="true" tabindex="-1"></a>        path, tr_id <span class="op">=</span> QUOTATIONS[kind]</span>
<span id="cb3-82"><a href="#cb3-82" aria-hidden="true" tabindex="-1"></a>        response <span class="op">=</span> <span class="va">self</span>.session.get(</span>
<span id="cb3-83"><a href="#cb3-83" aria-hidden="true" tabindex="-1"></a>            <span class="va">self</span>.base_url <span class="op">+</span> path,</span>
<span id="cb3-84"><a href="#cb3-84" aria-hidden="true" tabindex="-1"></a>            headers<span class="op">=</span>{</span>
<span id="cb3-85"><a href="#cb3-85" aria-hidden="true" tabindex="-1"></a>                <span class="st">&quot;authorization&quot;</span>: <span class="ss">f&quot;Bearer </span><span class="sc">{</span><span class="va">self</span><span class="sc">.</span>token<span class="sc">}</span><span class="ss">&quot;</span>,</span>
<span id="cb3-86"><a href="#cb3-86" aria-hidden="true" tabindex="-1"></a>                <span class="st">&quot;appkey&quot;</span>: <span class="va">self</span>.key,</span>
<span id="cb3-87"><a href="#cb3-87" aria-hidden="true" tabindex="-1"></a>                <span class="st">&quot;appsecret&quot;</span>: <span class="va">self</span>.secret,</span>
<span id="cb3-88"><a href="#cb3-88" aria-hidden="true" tabindex="-1"></a>                <span class="st">&quot;tr_id&quot;</span>: tr_id,</span>
<span id="cb3-89"><a href="#cb3-89" aria-hidden="true" tabindex="-1"></a>                <span class="st">&quot;custtype&quot;</span>: <span class="st">&quot;P&quot;</span>,</span>
<span id="cb3-90"><a href="#cb3-90" aria-hidden="true" tabindex="-1"></a>            },</span>
<span id="cb3-91"><a href="#cb3-91" aria-hidden="true" tabindex="-1"></a>            params<span class="op">=</span>params,</span>
<span id="cb3-92"><a href="#cb3-92" aria-hidden="true" tabindex="-1"></a>            timeout<span class="op">=</span>(<span class="dv">5</span>, <span class="dv">20</span>),</span>
<span id="cb3-93"><a href="#cb3-93" aria-hidden="true" tabindex="-1"></a>        )</span>
<span id="cb3-94"><a href="#cb3-94" aria-hidden="true" tabindex="-1"></a>        body <span class="op">=</span> <span class="va">self</span>._json(response)</span>
<span id="cb3-95"><a href="#cb3-95" aria-hidden="true" tabindex="-1"></a>        <span class="cf">if</span> body.get(<span class="st">&quot;rt_cd&quot;</span>) <span class="op">!=</span> <span class="st">&quot;0&quot;</span>:</span>
<span id="cb3-96"><a href="#cb3-96" aria-hidden="true" tabindex="-1"></a>            code <span class="op">=</span> <span class="bu">str</span>(body.get(<span class="st">&quot;msg_cd&quot;</span>, <span class="st">&quot;unknown&quot;</span>))[:<span class="dv">40</span>]</span>
<span id="cb3-97"><a href="#cb3-97" aria-hidden="true" tabindex="-1"></a>            <span class="cf">raise</span> <span class="pp">RuntimeError</span>(<span class="ss">f&quot;KIS quotation rejected: </span><span class="sc">{</span>code<span class="sc">}</span><span class="ss">&quot;</span>)</span>
<span id="cb3-98"><a href="#cb3-98" aria-hidden="true" tabindex="-1"></a>        <span class="cf">return</span> body</span>
<span id="cb3-99"><a href="#cb3-99" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-100"><a href="#cb3-100" aria-hidden="true" tabindex="-1"></a>    <span class="kw">def</span> current_price(<span class="va">self</span>, symbol):</span>
<span id="cb3-101"><a href="#cb3-101" aria-hidden="true" tabindex="-1"></a>        <span class="cf">if</span> <span class="kw">not</span> re.fullmatch(<span class="vs">r&quot;</span><span class="pp">[A-Z0-9]</span><span class="op">{6}</span><span class="vs">&quot;</span>, symbol):</span>
<span id="cb3-102"><a href="#cb3-102" aria-hidden="true" tabindex="-1"></a>            <span class="cf">raise</span> <span class="pp">ValueError</span>(<span class="st">&quot;A six-character uppercase ASCII stock symbol is required&quot;</span>)</span>
<span id="cb3-103"><a href="#cb3-103" aria-hidden="true" tabindex="-1"></a>        body <span class="op">=</span> <span class="va">self</span>.quotations(<span class="st">&quot;price&quot;</span>, {</span>
<span id="cb3-104"><a href="#cb3-104" aria-hidden="true" tabindex="-1"></a>            <span class="st">&quot;FID_COND_MRKT_DIV_CODE&quot;</span>: <span class="st">&quot;J&quot;</span>,</span>
<span id="cb3-105"><a href="#cb3-105" aria-hidden="true" tabindex="-1"></a>            <span class="st">&quot;FID_INPUT_ISCD&quot;</span>: symbol,</span>
<span id="cb3-106"><a href="#cb3-106" aria-hidden="true" tabindex="-1"></a>        })</span>
<span id="cb3-107"><a href="#cb3-107" aria-hidden="true" tabindex="-1"></a>        output <span class="op">=</span> body.get(<span class="st">&quot;output&quot;</span>)</span>
<span id="cb3-108"><a href="#cb3-108" aria-hidden="true" tabindex="-1"></a>        <span class="cf">if</span> <span class="kw">not</span> <span class="bu">isinstance</span>(output, <span class="bu">dict</span>):</span>
<span id="cb3-109"><a href="#cb3-109" aria-hidden="true" tabindex="-1"></a>            <span class="cf">raise</span> <span class="pp">RuntimeError</span>(<span class="st">&quot;Price output is missing&quot;</span>)</span>
<span id="cb3-110"><a href="#cb3-110" aria-hidden="true" tabindex="-1"></a>        price <span class="op">=</span> output.get(<span class="st">&quot;stck_prpr&quot;</span>)</span>
<span id="cb3-111"><a href="#cb3-111" aria-hidden="true" tabindex="-1"></a>        <span class="cf">if</span> <span class="kw">not</span> <span class="bu">isinstance</span>(price, <span class="bu">str</span>) <span class="kw">or</span> <span class="kw">not</span> re.fullmatch(<span class="vs">r&quot;</span><span class="pp">[0-9]</span><span class="op">+</span><span class="vs">&quot;</span>, price) <span class="kw">or</span> <span class="bu">int</span>(price) <span class="op">&lt;=</span> <span class="dv">0</span>:</span>
<span id="cb3-112"><a href="#cb3-112" aria-hidden="true" tabindex="-1"></a>            <span class="cf">raise</span> <span class="pp">RuntimeError</span>(<span class="st">&quot;Price must be a positive integer&quot;</span>)</span>
<span id="cb3-113"><a href="#cb3-113" aria-hidden="true" tabindex="-1"></a>        <span class="cf">return</span> output</span>
<span id="cb3-114"><a href="#cb3-114" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-115"><a href="#cb3-115" aria-hidden="true" tabindex="-1"></a>    <span class="kw">def</span> close(<span class="va">self</span>):</span>
<span id="cb3-116"><a href="#cb3-116" aria-hidden="true" tabindex="-1"></a>        <span class="va">self</span>.session.close()</span></code></pre></div>
<p><code>RUN_MODE=paper</code> 허용은 뒤의 로컬 페이퍼 실행에서 같은 시세 조회기를 재사용하기 위한 것이다. 이 허용이 주문 기능을 추가하지는 않는다. URL을 외부 입력으로 받지 않고 <code>QUOTATIONS</code>의 두 경로만 선택하므로 코드의 동작 범위를 읽고 확인할 수 있다.</p>
<p>현재가 경로·거래 ID·필수 파라미터는 <a href="https://github.com/koreainvestment/open-trading-api/blob/main/examples_llm/domestic_stock/inquire_price/inquire_price.py">공식 현재가 조회 예제</a>를 기준으로 했다. 이 예제의 <code>J</code>는 KRX 시장 구분이다. 다른 시장 구분을 사용할 때는 데이터의 시장 기준까지 함께 바꿔야 한다.</p>
<h2 id="read-only-api-test">4. 한 종목만 조회하기</h2>
<p>공식 예제에서 사용하는 <code>005930</code>은 종목코드 형식을 확인하기 위한 입력이다. 특정 종목에 대한 투자 제안이 아니다. <code>src/check_kis.py</code>를 다음처럼 작성한다.</p>
<div class="sourceCode" id="cb4"><pre class="sourceCode python"><code class="sourceCode python"><span id="cb4-1"><a href="#cb4-1" aria-hidden="true" tabindex="-1"></a><span class="im">from</span> src.kis_readonly <span class="im">import</span> ReadOnlyKis, load_config</span>
<span id="cb4-2"><a href="#cb4-2" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb4-3"><a href="#cb4-3" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb4-4"><a href="#cb4-4" aria-hidden="true" tabindex="-1"></a><span class="kw">def</span> main():</span>
<span id="cb4-5"><a href="#cb4-5" aria-hidden="true" tabindex="-1"></a>    config <span class="op">=</span> load_config()</span>
<span id="cb4-6"><a href="#cb4-6" aria-hidden="true" tabindex="-1"></a>    client <span class="op">=</span> ReadOnlyKis(config)</span>
<span id="cb4-7"><a href="#cb4-7" aria-hidden="true" tabindex="-1"></a>    <span class="cf">try</span>:</span>
<span id="cb4-8"><a href="#cb4-8" aria-hidden="true" tabindex="-1"></a>        output <span class="op">=</span> client.current_price(<span class="st">&quot;005930&quot;</span>)</span>
<span id="cb4-9"><a href="#cb4-9" aria-hidden="true" tabindex="-1"></a>        price <span class="op">=</span> <span class="bu">int</span>(output[<span class="st">&quot;stck_prpr&quot;</span>])</span>
<span id="cb4-10"><a href="#cb4-10" aria-hidden="true" tabindex="-1"></a>        <span class="bu">print</span>(<span class="ss">f&quot;env=</span><span class="sc">{</span>config<span class="sc">.</span>get(<span class="st">&#39;KIS_ENV&#39;</span>, <span class="st">&#39;mock&#39;</span>)<span class="sc">}</span><span class="ss"> symbol=005930 price=</span><span class="sc">{</span>price<span class="sc">}</span><span class="ss">&quot;</span>)</span>
<span id="cb4-11"><a href="#cb4-11" aria-hidden="true" tabindex="-1"></a>        <span class="bu">print</span>(<span class="st">&quot;quotation OK; credentials and token omitted&quot;</span>)</span>
<span id="cb4-12"><a href="#cb4-12" aria-hidden="true" tabindex="-1"></a>    <span class="cf">finally</span>:</span>
<span id="cb4-13"><a href="#cb4-13" aria-hidden="true" tabindex="-1"></a>        client.close()</span>
<span id="cb4-14"><a href="#cb4-14" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb4-15"><a href="#cb4-15" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb4-16"><a href="#cb4-16" aria-hidden="true" tabindex="-1"></a><span class="cf">if</span> <span class="va">__name__</span> <span class="op">==</span> <span class="st">&quot;__main__&quot;</span>:</span>
<span id="cb4-17"><a href="#cb4-17" aria-hidden="true" tabindex="-1"></a>    main()</span></code></pre></div>
<p>프로젝트 루트에서 먼저 문법을 확인하고, 설정을 준비한 뒤 조회를 실행한다.</p>
<div class="sourceCode" id="cb5"><pre class="sourceCode powershell"><code class="sourceCode powershell"><span id="cb5-1"><a href="#cb5-1" aria-hidden="true" tabindex="-1"></a><span class="op">.</span>\<span class="op">.</span><span class="fu">venv</span>\Scripts\python<span class="op">.</span><span class="fu">exe</span> <span class="op">-</span>m py_compile src<span class="op">/</span>kis_readonly<span class="op">.</span><span class="fu">py</span> src<span class="op">/</span>check_kis<span class="op">.</span><span class="fu">py</span></span>
<span id="cb5-2"><a href="#cb5-2" aria-hidden="true" tabindex="-1"></a><span class="op">.</span>\<span class="op">.</span><span class="fu">venv</span>\Scripts\python<span class="op">.</span><span class="fu">exe</span> <span class="op">-</span>m src<span class="op">.</span><span class="fu">check_kis</span></span></code></pre></div>
<p>실제 출력의 가격은 조회 시점에 따라 달라진다. 성공 기준은 양의 가격이 파싱되고, 환경과 종목코드가 표시되며, 키·토큰·요청 헤더가 출력되지 않는 것이다. HTTP 응답이 200이어도 업무 결과 코드가 실패일 수 있으므로 코드에서 <code>rt_cd</code>를 별도로 검사한다.</p>
<p>출력 형식은 다음과 같다. <code>&lt;조회한 양의 정수&gt;</code>는 실제로 표시될 가격을 설명하는 자리표시자이며 실행 결과나 현재 시세가 아니다.</p>
<pre class="text"><code>env=mock symbol=005930 price=&lt;조회한 양의 정수&gt;
quotation OK; credentials and token omitted</code></pre>
<p>문법 검사만 통과한 경우에는 인증 성공으로 기록하지 않는다. <code>quotation OK</code>가 표시되지 않으면 실패한 단계와 오류 코드를 확인하고, 토큰이나 전체 응답을 복사해 공유하지 않는다.</p>
<p>이 장에서는 조회 실패를 자동으로 무한 재시도하지 않는다. 네트워크 타임아웃은 요청이 끝나지 않은 상황, 인증 실패는 자격 증명 문제, 업무 오류는 서버가 요청을 거절한 상황으로 구분한다. Requests의 타임아웃과 예외 의미는 <a href="https://requests.readthedocs.io/en/latest/user/quickstart/">공식 Quickstart</a>에서 확인할 수 있다.</p>
<h2 id="codex-request">5. Codex에 구현과 검증 범위 전달하기</h2>
<pre class="text"><code>auto-trading-lab에 한국투자증권 조회 클라이언트를 만들어줘.
config/.env를 직접 읽고 KIS_ENV=mock|live와 RUN_MODE를 구분해줘.
인증 POST, 현재가 GET, 기간별 일봉 GET만 허용하고 임의 URL 입력은 받지 마.
키·시크릿·토큰·전체 헤더와 전체 응답은 로그에 출력하지 마.
공식 예제에서 현재 파라미터와 거래 ID를 확인해 출처를 기록해줘.
먼저 가짜 응답으로 성공, HTTP 오류, rt_cd 실패, 빈 output을 검사해줘.
실제 접속을 실행했는지, 문법·가짜 응답만 검사했는지 구분해서 보고해줘.
주문 함수, 잔고 변경, 증권사 모의 주문도 이번 작업에 추가하지 마.</code></pre>
<p>가짜 응답 검사는 실제 키 없이도 오류 처리와 파싱을 확인하는 방법이다. 인증이 실제로 성공했다는 증거는 자신의 계정으로 실행한 조회 결과여야 한다.</p>
<h2 id="completion">6. 오류 대응과 완료 기준</h2>
<table>
<colgroup>
<col style="width: 50%" />
<col style="width: 50%" />
</colgroup>
<thead>
<tr>
<th>증상</th>
<th>확인 순서</th>
</tr>
</thead>
<tbody>
<tr>
<td>설정 파일을 찾지 못함</td>
<td><code>config/.env</code>의 위치와 <code>.env.txt</code>로 저장되지 않았는지 확인한다</td>
</tr>
<tr>
<td>인증 실패</td>
<td>서비스 신청 상태 → <code>KIS_ENV</code> → 같은 환경의 앱 키·시크릿 순서로 본다</td>
</tr>
<tr>
<td>HTTP·업무 오류</td>
<td>시각·환경·요청 종류·오류 코드만 남기고 공식 오류 안내를 확인한다</td>
</tr>
<tr>
<td>잦은 토큰 요청 거절</td>
<td>실행을 멈추고 토큰 발급 정책과 프로세스 재시작 빈도를 확인한다</td>
</tr>
<tr>
<td>조회는 성공했지만 값이 비어 있음</td>
<td>종목코드·시장 구분·해당 환경의 지원 API를 확인한다</td>
</tr>
</tbody>
</table>
<p>실패 원인을 확인하기 전에 모의 설정을 실전으로 바꾸거나 요청 수를 늘리지 않는다. 조회 가능 범위와 한도는 <a href="https://apiportal.koreainvestment.com/">한국투자증권 API 포털</a>의 현재 안내를 따른다.</p>
<p>완료 기록에는 사용 환경, 시세 조회 종류, 결과 코드, 확인 일시를 남긴다. 키와 토큰을 기록하지 않고도 인증과 조회가 어디까지 성공했는지 설명할 수 있어야 한다. 다음 장에서는 같은 클라이언트로 일봉 데이터를 수집한다.</p>
<p>시세 한 건을 읽었다면 <a href="https://mediamaak.com/books/auto-trading/kospi200-data/">코스피200 데이터 수집</a>으로 이어간다. 여러 프로그램이 인증을 반복하는 문제가 생겼을 때는 <a href="https://mediamaak.com/devlog/2026/10/17/kis-token-cache/">토큰을 여러 실행기에서 공유한 개발 사례</a>를 참고한다. 첫 조회를 마치기 전에 토큰 공유 구조부터 추가할 필요는 없다.</p>
</div>
          <section class="pk-related" aria-labelledby="related-heading"><p class="pk-eyebrow">FROM CONCEPT TO PRACTICE</p><h2 id="related-heading">구현 기록과 함께 읽기</h2><ul><li><a href="https://mediamaak.com/books/auto-trading/python-venv/">Windows에서 Python venv 만들기</a></li><li><a href="https://mediamaak.com/devlog/2026/10/17/kis-token-cache/">한국투자증권 API 토큰을 여러 실행기에서 안전하게 공유하기</a></li></ul></section>
          <nav class="pk-pager" aria-label="이전 장과 다음 장"><a href="https://mediamaak.com/books/auto-trading/python-venv/">이전 글: Windows에서 Python venv 만들기</a><a href="https://mediamaak.com/books/auto-trading/kospi200-data/">다음 글: 코스피200 데이터 수집하기</a></nav>
          <div class="pk-share"><div><p class="pk-share-title">학습의 다음 단계를 남겨 두세요</p><p>공유 링크를 복사해 이어서 읽을 수 있습니다.</p></div><div><button type="button" data-share-content>공유 링크 복사</button><span role="status" data-share-status></span></div></div>
        </div>
      </div>
    </article>]]></content:encoded>
    </item>
    <item>
      <title>주간 읽기 01: 자동매매를 시작하는 순서</title>
      <link>https://mediamaak.com/devlog/2026/10/26/weekly-reading-01/</link>
      <guid isPermaLink="true">https://mediamaak.com/devlog/2026/10/26/weekly-reading-01/</guid>
      <pubDate>Mon, 26 Oct 2026 00:00:00 +0900</pubDate>
      <description>프로젝트 폴더, API 토큰 공유, 과거 시뮬레이션의 확인 기준을 연결해 지금 읽을 글과 다음 단계를 고릅니다.</description>
      <content:encoded><![CDATA[<article class="pk-article" data-content-id="/devlog/2026/10/26/weekly-reading-01/" data-content-type="devlog" data-published-at="2026-10-26T00:00:00+09:00" data-updated-at="2026-10-26T00:00:00+09:00">
      <header class="pk-hero">
        <p class="pk-eyebrow">주간 읽기 · WEEKLY READING</p>
        <h1>주간 읽기 01: 자동매매를 시작하는 순서</h1>
        <p class="pk-summary">프로젝트 폴더, API 토큰 공유, 과거 시뮬레이션의 확인 기준을 연결해 지금 읽을 글과 다음 단계를 고릅니다.</p>
        <div class="pk-meta"><dl><dt>발행</dt><dd><time datetime="2026-10-26T00:00:00+09:00">2026-10-26</time></dd><dt>수정</dt><dd><time datetime="2026-10-26T00:00:00+09:00">2026-10-26</time></dd><dt>분류</dt><dd>읽기 안내</dd></dl></div>
      </header>
      <div class="pk-reading-layout">
        <aside class="pk-toc" aria-label="이 글의 목차">
          <details class="pk-toc-details">
            <summary class="pk-toc-summary">이 글에서 다루는 내용</summary>
            <div class="pk-toc-content"><nav aria-label="본문 목차"><ol><li class="pk-toc-level-2"><a href="#지금-막힌-지점에서-시작하세요">지금 막힌 지점에서 시작하세요</a></li><li class="pk-toc-level-2"><a href="#첫-단계는-실행-위치를-분명히-하는-것입니다">첫 단계는 실행 위치를 분명히 하는 것입니다</a></li><li class="pk-toc-level-2"><a href="#실행기가-늘면-토큰의-주인과-재사용-조건을-확인하세요">실행기가 늘면 토큰의 주인과 재사용 조건을 확인하세요</a></li><li class="pk-toc-level-2"><a href="#백테스트는-한-건의-거래를-다시-설명할-수-있어야-합니다">백테스트는 한 건의 거래를 다시 설명할 수 있어야 합니다</a></li><li class="pk-toc-level-2"><a href="#이번-읽기에서-작은-확인-기록을-남겨-보세요">이번 읽기에서 작은 확인 기록을 남겨 보세요</a></li></ol></nav></div>
          </details>
        </aside>
        <div class="pk-body-column">
          <div class="pk-prose"><p>자동매매를 처음 만들 때는 <strong>작업 폴더를 확인하고, 인증의 범위를 나누고, 과거 결과의 계산 조건을 읽는 순서</strong>로 시작해 보세요. 문제가 생겼을 때 확인할 위치와 다음에 읽을 글을 함께 정리했습니다.</p>
<p>2026년 10월 26일호, 첫 읽기 안내입니다. 기존 공개 글을 학습 순서에 맞춰 연결했으며, 각 구현과 예제의 확인 날짜·적용 범위는 연결된 본문에서 확인할 수 있습니다.</p>
<h2 id="지금-막힌-지점에서-시작하세요">지금 막힌 지점에서 시작하세요</h2>
<table>
<colgroup>
<col style="width: 33%" />
<col style="width: 33%" />
<col style="width: 33%" />
</colgroup>
<thead>
<tr>
<th>지금 확인하려는 것</th>
<th>먼저 읽을 글</th>
<th>읽고 확인할 결과</th>
</tr>
</thead>
<tbody>
<tr>
<td>Codex가 어느 폴더에서 작업하는지 모르겠어요</td>
<td><a href="https://mediamaak.com/books/auto-trading/project-setup/">프로젝트 폴더 지정하기</a></td>
<td>현재 위치와 문서·설정·로그·결과 폴더를 구분할 수 있음</td>
</tr>
<tr>
<td>실행기가 여럿인데 인증을 각각 요청하고 있어요</td>
<td><a href="https://mediamaak.com/devlog/2026/10/17/kis-token-cache/">여러 실행기의 API 토큰 공유</a></td>
<td>캐시를 재사용하는 조건과 새 인증이 필요한 조건을 구분할 수 있음</td>
</tr>
<tr>
<td>시뮬레이션 결과는 보이는데 무엇을 믿어야 할지 모르겠어요</td>
<td><a href="https://mediamaak.com/guides/kospi200-backtest/">KOSPI200 백테스트 읽는 순서</a></td>
<td>기간·거래 기록·비용·보유분의 계산 범위를 확인할 수 있음</td>
</tr>
</tbody>
</table>
<h2 id="첫-단계는-실행-위치를-분명히-하는-것입니다">첫 단계는 실행 위치를 분명히 하는 것입니다</h2>
<p>작업 폴더가 다르면 같은 명령도 다른 설정과 파일을 읽을 수 있습니다. 먼저 <a href="https://mediamaak.com/books/auto-trading/project-setup/">wiki 1장</a>에서 현재 위치와 작업 범위를 확인하세요. 문서, 설정, 로그와 결과를 나누면 파일이 만들어진 위치와 오류를 찾아볼 위치도 분명해집니다.</p>
<p>다음은 <a href="https://mediamaak.com/books/auto-trading/python-venv/">프로젝트 전용 Python 가상환경</a>, 그다음은 <a href="https://mediamaak.com/books/auto-trading/kis-api/">한국투자증권 Open API 조회</a>입니다. 각 단계에서는 명령을 복사하는 데서 멈추지 말고, 실제로 사용한 경로와 확인된 결과를 한 줄씩 남겨 두세요.</p>
<p>이미 실행 환경을 준비했다면 <a href="https://mediamaak.com/auto-trading-wiki.html">책 목차</a>에서 필요한 장을 고를 수 있습니다. 데이터 수집과 저장, CCI 계산, 과거 시뮬레이션은 앞 단계에서 확인한 입력을 다음 단계로 넘기는 흐름입니다.</p>
<h2 id="실행기가-늘면-토큰의-주인과-재사용-조건을-확인하세요">실행기가 늘면 토큰의 주인과 재사용 조건을 확인하세요</h2>
<p><a href="https://mediamaak.com/devlog/2026/10/17/kis-token-cache/">토큰 공유 개발 노트</a>는 같은 로컬 파일 시스템을 공유하는 실행기들의 인증 준비를 다룹니다. 설명된 구현은 캐시 형식, 로컬 기준 발급 날짜, 계좌 식별값, 인증 정보 식별값을 비교합니다. 파일이 있다는 사실만으로 토큰을 재사용하지 않습니다.</p>
<p>여러 실행기가 동시에 시작할 때는 잠금을 얻은 뒤 캐시를 다시 확인하는 순서도 살펴보세요. 기다리는 동안 다른 실행기가 준비한 값을 사용할 수 있기 때문입니다. 본문의 합성 예제에서 날짜나 인증 정보 표식을 하나 바꾸어, 어느 조건에서 재사용이 중단되는지 확인할 수 있습니다.</p>
<p>이 글의 적용 범위는 로컬 공유 캐시입니다. 여러 서버에 걸친 분산 잠금으로 확대해 해석하지 마세요. 읽기 요청의 인증 재시도와 주문 요청의 재시도도 별개입니다. 주문 응답이 불확실할 때는 인증 캐시만 보고 같은 주문을 다시 보내는 판단을 내려서는 안 됩니다.</p>
<h2 id="백테스트는-한-건의-거래를-다시-설명할-수-있어야-합니다">백테스트는 한 건의 거래를 다시 설명할 수 있어야 합니다</h2>
<p><a href="https://mediamaak.com/guides/kospi200-backtest/">백테스트 안내</a>에서는 결과를 만든 기간과 거래 기록을 먼저 확인하세요. 저장된 과거 시뮬레이션과 현재 시세, 종목별 결과와 공개 전략 기록은 각각 계산 범위가 다를 수 있습니다.</p>
<p>한 건을 골라 다음 질문에 답해 보세요.</p>
<ol type="1">
<li>어떤 기간의 데이터로, 어느 시점에 신호를 계산했나요?</li>
<li>그 신호를 계산한 뒤 이용할 수 있는 가격으로 진입했나요?</li>
<li>비용과 미청산 보유분은 어디까지 반영했나요?</li>
<li>완료하지 못했거나 제외한 신호에는 설명이 남아 있나요?</li>
</ol>
<p>계산 흐름까지 직접 확인하려면 <a href="https://mediamaak.com/books/auto-trading/simulation/">wiki 7장의 과거 시뮬레이션</a>으로 이어가세요. 이 장은 신호 뒤의 거래 세션, 비용을 나눈 손익, 완료·제외·미완료 기록을 함께 다룹니다. 확인 결과는 사용한 데이터와 가정의 범위 안에서 읽어야 합니다.</p>
<h2 id="이번-읽기에서-작은-확인-기록을-남겨-보세요">이번 읽기에서 작은 확인 기록을 남겨 보세요</h2>
<table>
<thead>
<tr>
<th>점검</th>
<th>적어 둘 내용</th>
</tr>
</thead>
<tbody>
<tr>
<td>작업 위치</td>
<td>프로젝트 루트와 결과를 찾아볼 폴더</td>
</tr>
<tr>
<td>인증 준비</td>
<td>캐시를 사용할 조건과 사용을 중단할 조건</td>
</tr>
<tr>
<td>결과 해석</td>
<td>데이터 기간, 진입·청산 기준, 비용과 미완료 처리</td>
</tr>
</tbody>
</table>
<p>다음 양식을 자신의 작업 메모에 복사해 사용하세요. 인증 값 대신 직접 확인한 결과만 적어 두면 됩니다.</p>
<pre class="text"><code>작업 위치: [프로젝트 루트와 결과 폴더]
자료 기간: [원본 데이터와 거래 기록의 시작·끝]
비용 처리: [반영 / 미반영 / 확인 필요]
다음 문서: [읽을 글 제목과 공개 링크]
확인한 내용: [직접 대조한 한 가지]
남은 질문: [다음에 확인할 한 가지]</code></pre>
<p>설명할 수 없는 항목이 남으면 그 부분을 다루는 글로 돌아가세요. 결과 숫자 하나를 더 모으는 것보다, 같은 입력과 조건으로 무엇을 확인했는지 남기는 편이 다음 문제를 좁히는 데 도움이 됩니다.</p>
<p>처음 시작한다면 <strong><a href="https://mediamaak.com/books/auto-trading/project-setup/" data-analytics-role="next">프로젝트 폴더 확인부터 이어 읽기</a></strong>를 선택하세요. 이미 결과를 보고 있다면 <strong><a href="https://mediamaak.com/guides/kospi200-backtest/" data-analytics-role="related">백테스트 확인 항목부터 점검하기</a></strong>로 이동하면 됩니다. 다음 주간 읽기와 새 개발 노트는 <a href="https://mediamaak.com/rss.xml">MediaMaak 최신 글 RSS</a>에서 확인할 수 있습니다.</p>
</div>
          <section class="pk-related" aria-labelledby="related-heading"><p class="pk-eyebrow">KEEP READING</p><h2 id="related-heading">개념을 이어서 살펴보세요</h2><ul><li><a href="https://mediamaak.com/books/auto-trading/project-setup/">Codex 프로젝트 폴더 설정과 경로 확인</a></li><li><a href="https://mediamaak.com/devlog/2026/10/17/kis-token-cache/">한국투자증권 API 토큰을 여러 실행기에서 안전하게 공유하기</a></li><li><a href="https://mediamaak.com/books/auto-trading/simulation/">CCI 신호를 과거 데이터로 시뮬레이션하기</a></li></ul></section>
          <nav class="pk-pager" aria-label="이전 글과 다음 글"><a href="https://mediamaak.com/devlog/2026/10/17/kis-token-cache/">이전 글: 한국투자증권 API 토큰을 여러 실행기에서 안전하게 공유하기</a></nav>
          <div class="pk-share"><div><p class="pk-share-title">다시 읽고 싶은 기록이라면</p><p>공유 링크를 복사해 보관할 수 있습니다.</p></div><div><button type="button" data-share-content>공유 링크 복사</button><span role="status" data-share-status></span></div></div>
        </div>
      </div>
    </article>]]></content:encoded>
    </item>
    <item>
      <title>Codex 프로젝트 폴더 설정과 경로 확인</title>
      <link>https://mediamaak.com/books/auto-trading/project-setup/</link>
      <guid isPermaLink="true">https://mediamaak.com/books/auto-trading/project-setup/</guid>
      <pubDate>Sat, 17 Oct 2026 16:46:00 +0900</pubDate>
      <description>Windows에서 Codex 작업 폴더를 연결하고 실제 경로와 기본 구조를 확인합니다. 폴더가 다를 때의 해결 방법과 다음 Python venv 준비를 안내합니다.</description>
      <content:encoded><![CDATA[<article class="pk-article" data-content-id="/books/auto-trading/project-setup/" data-content-type="wiki-chapter" data-published-at="2026-10-17T16:46:00+09:00" data-updated-at="2026-10-26T00:00:00+09:00">
      <header class="pk-hero">
        <p class="pk-eyebrow">1 / 12 · AUTO TRADING WIKI</p>
        <h1>Codex 프로젝트 폴더 설정과 경로 확인</h1>
        <p class="pk-summary">Windows에서 Codex 작업 폴더를 연결하고 실제 경로와 기본 구조를 확인합니다. 폴더가 다를 때의 해결 방법과 다음 Python venv 준비를 안내합니다.</p>
        <div class="pk-meta"><dl><dt>발행</dt><dd><time datetime="2026-10-17T16:46:00+09:00">2026-10-17</time></dd><dt>수정</dt><dd><time datetime="2026-10-26T00:00:00+09:00">2026-10-26</time></dd><dt>문서 검토</dt><dd>2026-10-26</dd><dt>본문 버전</dt><dd>1.2.0-web.20261026</dd></dl></div>
      </header>
      <div class="book-reading-content">
        <div class="pk-body-column">
          <div class="pk-prose"><p>Codex 프로젝트 폴더 설정은 <strong>어느 로컬 폴더의 파일을 읽고 만들지 정하고, 실제 작업 경로를 확인하는 과정</strong>이다. Windows에서 전용 폴더를 연결한 뒤 경로와 파일 목록을 대조하면 된다. 이 장은 그 확인부터 기본 폴더 구조 만들기까지 안내한다.</p>
<p>이 책의 새 요청문에서는 프로젝트 이름을 <code>auto-trading-lab</code>으로 통일한다. 이 장의 화면에 보이는 <code>kospi200_auto_trading</code>도 같은 역할의 작업 폴더다. 이미 정한 폴더가 있다면 이름을 바꾸지 않고 실제 경로를 그대로 사용한다.</p>
<p>이 글은 《Codex로 시작하는 자동매매 시스템 만들기》 1장의 웹 편집본이다. Windows 로컬 프로젝트를 기준으로 설명한다. 예시 경로의 <code>minjun</code>은 가상 사용자명이므로 자신의 Windows 사용자명으로 바꾼다.</p>
<h2 id="quick-start">Codex 작업 폴더, 먼저 이렇게 확인하세요</h2>
<p>설치와 로그인이 끝났다면 아래 세 단계부터 진행한다. 앱 준비가 필요하면 뒤의 <a href="#why-codex">설치 안내</a>부터 읽는다.</p>
<ol type="1">
<li>Windows에서 자동매매 전용 폴더를 만들고 로컬 프로젝트에 연결한다. 예시는 <code>C:\Users\minjun\auto-trading-lab</code>이며 이미 쓰는 폴더가 있으면 그 실제 경로를 사용한다.</li>
<li>여러 폴더가 연결돼 있다면 사용할 폴더를 가리켜 <strong>Make primary</strong>(기본 폴더)로 정한다. 연결 폴더를 추가하거나 바꾸려면 프로젝트의 <strong>Edit project</strong>를 연다. 메뉴가 다르면 <a href="https://learn.chatgpt.com/docs/projects#use-local-projects-for-folders-and-codebases">공식 프로젝트 안내</a>를 확인한다.</li>
<li>Codex에 아래 요청을 보내 <strong>기대한 경로와 보고된 경로가 같은지</strong> 확인한다. 다른 위치라면 파일을 만들기 전에 <a href="#folder-troubleshooting">경로 문제 해결</a>로 돌아간다.</li>
</ol>
<pre class="text"><code>현재 작업 폴더의 절대 경로와 바로 아래 파일·폴더 목록을 확인해줘.
기대한 경로는 C:\Users\minjun\auto-trading-lab 이야.
아직 파일을 만들거나 수정하지 말고, 실제 경로가 다른지 알려줘.</code></pre>
<p><code>minjun</code>은 가상 사용자명이다. 자신의 사용자명과 프로젝트 경로로 바꾼다. 이 블록은 터미널 명령이 아니라 Codex에 보내는 요청문이다.</p>
<p>경로가 맞으면 <a href="#project-structure">기본 구조 만들기</a>로 이동한다. 이미 여섯 하위 폴더가 준비됐다면 <a href="#completion">완료 기준</a>을 확인한 뒤 <a href="https://mediamaak.com/books/auto-trading/python-venv/" data-analytics-role="next">2장: Python 전용 가상환경 준비하기</a>로 이어간다.</p>
<h2 id="before-start">준비물과 이번 장의 결과</h2>
<p>준비물은 Windows 컴퓨터, 인터넷 연결, ChatGPT 데스크톱 앱에서 Codex를 사용할 수 있는 계정이다. Python 설치와 API 계정 준비는 뒤의 장에서 다룬다.</p>
<p>완료 결과는 다음과 같다.</p>
<ul>
<li>사용자가 위치를 정한 프로젝트 전용 폴더</li>
<li>Codex가 보고한 현재 작업 경로와 폴더 목록</li>
<li><code>docs</code>, <code>scripts</code>, <code>src</code>, <code>config</code>, <code>logs</code>, <code>outputs</code> 기본 구조</li>
<li>파일 작업·민감정보·주문 기능에 대한 프로젝트 작업 규칙</li>
</ul>
<h2 id="why-codex">1. 파일과 터미널을 함께 다루는 개발 도구</h2>
<p>자동매매 시스템을 만들려면 Python 파일만 작성하는 것으로 끝나지 않는다. 설정 파일을 구분하고, 실행 스크립트를 만들고, 수집 결과와 오류 로그를 찾아야 한다. Codex는 프로젝트의 파일을 읽고 수정하며 명령을 실행하는 개발 작업에 사용할 수 있다. 이 책은 그 작업을 작은 단계로 나누어 진행한다.</p>
<p>처음부터 “자동매매 프로그램을 만들어줘”라고 요청하면 작업 범위가 너무 넓다. “현재 폴더를 확인해줘”, “폴더 구조만 제안해줘”, “아직 파일은 만들지 말고 계획만 설명해줘”처럼 결과가 분명한 요청부터 시작한다. 파일을 어디에 만들었는지, 어떤 명령을 실행했는지, 결과가 무엇인지 확인한 뒤 다음 단계로 넘어간다.</p>
<h3 id="현재-앱-이름과-설치-경로">현재 앱 이름과 설치 경로</h3>
<p>책 초안의 <strong>Codex Desktop</strong>은 당시 데스크톱 작업 환경을 가리키는 표현이다. 2026년 10월 26일 다시 확인한 공식 문서는 <strong>ChatGPT 데스크톱 앱에서 Codex를 선택하는 흐름</strong>으로 안내한다. 아래 순서로 준비한다.</p>
<ol type="1">
<li><a href="https://learn.chatgpt.com/docs/app">OpenAI 공식 데스크톱 앱 안내</a>에서 운영체제에 맞는 설치 안내를 연다.</li>
<li>앱을 실행하고 ChatGPT 계정으로 로그인한다.</li>
<li>새 작업에서 ChatGPT 선택 메뉴의 <strong>Codex</strong>를 선택한다.</li>
<li>프로젝트 또는 폴더를 열고 이번 작업이 사용할 위치를 확인한다.</li>
</ol>
<p>메뉴 이름이나 위치가 다르면 <a href="https://learn.chatgpt.com/docs/quickstart">공식 시작 안내</a>의 데스크톱 설정을 확인한다. 여기서 사용하는 것은 로컬 폴더의 파일을 다루는 작업이다.</p>
<h2 id="account-and-runtime">2. 시작 전에 확인할 비용과 실행 조건</h2>
<h3 id="컴퓨터를-계속-켜두어야-하나요">컴퓨터를 계속 켜두어야 하나요?</h3>
<p>이번 장은 컴퓨터에서 프로젝트 폴더를 만들고 상태를 확인하는 작업이다. 작업하는 동안 앱과 컴퓨터가 실행 중이어야 한다. 자동매매 프로그램을 장중에 반복 실행하는 환경은 별도로 정한다. 개인 PC, 미니 PC, 서버 등 어느 환경을 사용하든 전원·인터넷·스케줄러·로그·중단 조건을 함께 관리해야 한다.</p>
<p>이 책에서 만드는 자동매매 프로그램의 일일 실행 환경과 Codex를 사용하는 개발 환경을 구분한다. 먼저 로컬 폴더, 데이터 조회, 저장과 검증을 준비하고 운영 환경은 뒤에서 다룬다.</p>
<h3 id="유료-구독이-꼭-필요한가요">유료 구독이 꼭 필요한가요?</h3>
<p>사용 가능한 기능과 사용량은 로그인 방식, 계정의 플랜, 워크스페이스 설정과 제공 시점에 따라 달라진다. 현재 <a href="https://learn.chatgpt.com/docs/pricing">공식 가격·기능 안내</a>에는 무료 플랜의 일부 Codex 제공도 안내되어 있으며 적용 여부는 계정에서 확인해야 한다. 이 장을 위해 특정 유료 상품을 먼저 구입할 필요가 있다고 가정하지 않는다.</p>
<p>앱에서 Codex 작업을 시작할 수 있는지 먼저 확인한다. 개발량이 늘어난 뒤 현재 사용량과 필요한 기능을 보고 이용 방식을 결정한다. 이 글에는 바뀔 수 있는 가격이나 고정된 작업 횟수를 적지 않는다.</p>
<h3 id="토큰과-사용량은-어떻게-이해하면-되나요">토큰과 사용량은 어떻게 이해하면 되나요?</h3>
<p>Codex는 요청, 파일, 대화 기록, 도구 결과를 읽고 답변을 생성하면서 사용량을 쓴다. 여러 파일을 비교하거나 오류 수정을 반복하면 작업량이 커질 수 있다. 필요한 파일과 성공 기준을 함께 전달하면 결과를 확인하기 쉽다.</p>
<p>구독의 포함 사용량과 API 사용료는 서로 다른 기준이다. 잔여 사용량과 초기화 시점은 자신의 계정에 표시되는 정보를 확인한다. 자세한 기준은 <a href="https://learn.chatgpt.com/docs/pricing#where-can-i-see-my-current-usage-limits">공식 가격 안내의 사용량 설명</a>을 따른다.</p>
<h3 id="chatgpt-로그인과-api-키-로그인은-무엇이-다른가요">ChatGPT 로그인과 API 키 로그인은 무엇이 다른가요?</h3>
<p>공식 문서는 구독 접근을 위한 ChatGPT 로그인과 사용량 기반 접근을 위한 API 키 로그인을 구분한다. API 키로 사용하면 OpenAI Platform의 API 사용료가 적용되고 일부 기능의 제공 범위가 달라질 수 있다. 이 장은 ChatGPT 계정 로그인으로 설명하며 API 키를 발급하거나 문서에 입력하지 않는다. <a href="https://learn.chatgpt.com/docs/auth">OpenAI 공식 인증 안내</a></p>
<h3 id="codex가-자동매매-프로그램을-계속-운영하나요">Codex가 자동매매 프로그램을 계속 운영하나요?</h3>
<p>이 책에서는 Codex를 설계·구현·오류 확인을 돕는 개발 도구로 사용한다. 매일 실행되는 자동매매 프로그램은 사용자가 정한 실행 환경에서 동작하도록 구성한다. 실행 환경이 켜져 있는지, 로그가 남는지, 오류가 났을 때 멈추는지는 프로그램의 운영 항목으로 확인한다.</p>
<h2 id="current-folder">3. 현재 작업 위치부터 확인하기</h2>
<p>설치와 로그인이 끝났다면 현재 폴더를 먼저 확인한다. 아래 블록은 PowerShell 명령이 아니라 <strong>Codex에 입력할 요청문</strong>이다. 교육용 화면은 누르면 원본 크기로 열어 볼 수 있다.</p>
<pre class="text"><code>현재 Codex가 보고 있는 작업 폴더 위치를 알려줘.
파일 생성이나 수정은 하지 말고, 현재 경로만 확인해줘.</code></pre>
<p><a href="https://mediamaak.com/assets/public-knowledge/book/project-setup/fig-1-2-current-folder.png"><img src="https://mediamaak.com/assets/public-knowledge/book/project-setup/fig-1-2-current-folder.png" alt="현재 작업 경로만 확인하도록 요청하고 Codex가 경로를 보고하는 교육용 대화 예시" /></a></p>
<p><em>화면 예시 1-A. 교육용 재현 화면이다. 현재 앱의 실제 캡처가 아니며, 응답과 화면 구성은 앱 버전 및 프로젝트 상태에 따라 달라질 수 있다.</em></p>
<p>현재 폴더를 모른 채 작업을 시작하면 파일이 예상하지 못한 곳에 생길 수 있다. 다음 세 가지를 확인한다.</p>
<table>
<colgroup>
<col style="width: 33%" />
<col style="width: 33%" />
<col style="width: 33%" />
</colgroup>
<thead>
<tr>
<th>확인 항목</th>
<th>정상 기준</th>
<th>문제가 있을 때</th>
</tr>
</thead>
<tbody>
<tr>
<td>Codex 작업 시작</td>
<td>Codex에서 요청을 보낼 수 있다</td>
<td>로그인 상태와 계정의 사용 가능 범위를 확인한다</td>
</tr>
<tr>
<td>로컬 폴더 접근</td>
<td>선택한 프로젝트 폴더의 경로를 확인할 수 있다</td>
<td>프로젝트에 연결한 폴더와 접근 권한을 확인한다</td>
</tr>
<tr>
<td>현재 경로</td>
<td>사용자가 의도한 폴더가 표시된다</td>
<td>올바른 폴더를 연결하고 기본 작업 위치를 확인한다</td>
</tr>
</tbody>
</table>
<p>공식 문서에 따르면 로컬 프로젝트에는 여러 폴더를 연결할 수 있다. 새 작업의 기본 위치는 기본 폴더에 영향을 받으므로, 처음에는 자동매매 전용 폴더 하나로 시작한다. <a href="https://learn.chatgpt.com/docs/projects#use-local-projects-for-folders-and-codebases">공식 프로젝트·폴더 안내</a></p>
<h2 id="choose-folder">4. 전용 프로젝트 폴더의 위치 정하기</h2>
<p>이 책에서는 사용자가 Windows 파일 탐색기에서 직접 폴더를 만들고 Codex의 로컬 프로젝트로 여는 방식으로 진행한다. 먼저 위치를 정해두면 파일을 찾거나 백업할 때 기준이 된다.</p>
<p>Windows 사용자 폴더의 예시는 다음과 같다.</p>
<pre class="text"><code>C:\Users\사용자명\</code></pre>
<p>가상 사용자 <code>minjun</code>의 프로젝트 위치는 다음과 같다.</p>
<pre class="text"><code>C:\Users\minjun\auto-trading-lab</code></pre>
<p>사용자 폴더 아래의 전용 경로를 쓰면 시스템 프로그램 폴더와 구분하기 쉽다. <code>C:\Program Files</code>처럼 프로그램 설치를 위한 경로는 이번 프로젝트 위치로 사용하지 않는다. 프로젝트를 백업할 때도 폴더 하나를 기준으로 관리할 수 있다. 실제 비밀값이 들어 있는 설정과 로그는 공유용 사본에 포함하지 않는다.</p>
<p>폴더를 연 뒤 작업 범위를 다음처럼 전달한다. 요청문에 적는 경로도 자신의 실제 작업 폴더로 바꾼다.</p>
<pre class="text"><code>앞으로 모든 작업은 C:\Users\minjun\auto-trading-lab 폴더 안에서만 진행해줘.</code></pre>
<p><a href="https://mediamaak.com/assets/public-knowledge/book/project-setup/fig-1-3-project-scope.png"><img src="https://mediamaak.com/assets/public-knowledge/book/project-setup/fig-1-3-project-scope.png" alt="앞으로 작업할 위치를 프로젝트 폴더 내부로 지정하는 교육용 대화 예시" /></a></p>
<p><em>화면 예시 1-B. 프로젝트 범위를 전달하는 교육용 재현 화면이다. 요청문은 작업 목적을 설명하는 예시이며 앱의 폴더 권한 설정을 대신하지 않는다.</em></p>
<h2 id="open-project">5. 폴더를 만들고 Codex에서 확인하기</h2>
<ol type="1">
<li>파일 탐색기에서 자신의 Windows 사용자 폴더를 연다.</li>
<li><code>auto-trading-lab</code>이라는 새 폴더를 만든다.</li>
<li>앱에서 이 폴더를 로컬 프로젝트에 연결해 연다.</li>
<li>여러 폴더가 연결되어 있다면 이번 폴더가 기본 작업 위치인지 확인한다.</li>
</ol>
<p>현재 문서의 메뉴 안내와 앱이 다르면 <a href="https://learn.chatgpt.com/docs/projects">공식 프로젝트 문서</a>를 참고한다. Codex에 아래 확인을 요청한다.</p>
<pre class="text"><code>현재 작업 폴더가 C:\Users\minjun\auto-trading-lab 인지 확인해줘.
파일 생성이나 수정은 하지 말고, 현재 경로와 폴더 안의 파일 목록만 알려줘.</code></pre>
<p><a href="https://mediamaak.com/assets/public-knowledge/book/project-setup/fig-1-4-folder-verification.png"><img src="https://mediamaak.com/assets/public-knowledge/book/project-setup/fig-1-4-folder-verification.png" alt="프로젝트 경로와 폴더 안의 파일 목록을 함께 확인하는 교육용 대화 예시" /></a></p>
<p><em>화면 예시 1-C. 프로젝트 폴더와 파일 목록을 확인하는 교육용 재현 화면이다.</em></p>
<p>정상 결과는 사용자가 정한 경로와 실제 파일 목록이 함께 보고되는 것이다. 새 폴더라면 비어 있거나 사용자가 알고 있는 파일만 있어야 한다. 이미 파일이 있다면 삭제부터 하지 말고 어떤 자료인지 확인한다.</p>
<h3 id="프로젝트-위치-확인-흐름">프로젝트 위치 확인 흐름</h3>
<figure>
<img src="https://mediamaak.com/assets/public-knowledge/book/project-setup/project-folder-flow.svg" alt="설치와 로그인 후 경로를 확인하고, 경로가 다르면 전용 폴더를 열어 다시 확인하며, 파일 목록을 확인한 뒤 기본 구조를 제안받는 흐름" />
<figcaption aria-hidden="true">설치와 로그인 후 경로를 확인하고, 경로가 다르면 전용 폴더를 열어 다시 확인하며, 파일 목록을 확인한 뒤 기본 구조를 제안받는 흐름</figcaption>
</figure>
<p><em>그림 1-1. 원고의 Mermaid 흐름도를 웹용 SVG로 다시 그렸다. 원하는 경로가 아니면 전용 폴더를 열고 경로 확인으로 돌아간다.</em></p>
<p>도표를 읽기 어려운 경우에는 <strong>로그인 → 현재 경로 확인 → 올바른 전용 폴더 열기 → 파일 목록 확인 → 기본 폴더 구조 제안</strong> 순서로 진행하면 된다.</p>
<h2 id="project-structure">6. 기본 폴더 구조를 제안받고 만들기</h2>
<p>프로젝트 위치가 확인되었다면 필요한 기본 구조만 먼저 제안받는다.</p>
<pre class="text"><code>이 폴더는 한국투자증권 Open API를 이용해 코스피200 자동매매 시스템을 만들기 위한 프로젝트 폴더다.
아직 파일을 만들지 말고, 필요한 기본 폴더 구조만 제안해줘.
각 폴더의 역할도 함께 설명해줘.</code></pre>
<p><a href="https://mediamaak.com/assets/public-knowledge/book/project-setup/fig-1-5-folder-structure-proposal.png"><img src="https://mediamaak.com/assets/public-knowledge/book/project-setup/fig-1-5-folder-structure-proposal.png" alt="문서·실행 스크립트·소스·설정·로그·결과를 분리하는 폴더 구조 제안의 교육용 대화 예시" /></a></p>
<p><em>화면 예시 1-D. 폴더를 만들기 전에 구조를 검토하는 교육용 재현 화면이다.</em></p>
<p>초기 구조는 다음 정도면 충분하다.</p>
<table>
<thead>
<tr>
<th>폴더</th>
<th>용도</th>
<th>보관할 내용</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>docs/</code></td>
<td>문서</td>
<td>설계 메모와 오류 해결 기록</td>
</tr>
<tr>
<td><code>scripts/</code></td>
<td>실행 스크립트</td>
<td>PowerShell 실행 파일</td>
</tr>
<tr>
<td><code>src/</code></td>
<td>소스 코드</td>
<td>Python 코드와 계산 로직</td>
</tr>
<tr>
<td><code>config/</code></td>
<td>설정</td>
<td>설정 항목의 예시와 개인용 설정 파일</td>
</tr>
<tr>
<td><code>logs/</code></td>
<td>로그</td>
<td>실행 과정과 오류 기록</td>
</tr>
<tr>
<td><code>outputs/</code></td>
<td>결과</td>
<td>CSV, 리포트, 시뮬레이션 결과</td>
</tr>
</tbody>
</table>
<p>공개 예시 설정에는 필요한 항목 이름만 담고, 실제 인증정보는 개인용 설정으로 구분한다. 이번 장에서는 인증정보를 만들거나 입력하지 않는다.</p>
<p>제안한 구조가 적절하고 작업 위치도 맞다면 폴더 생성을 요청한다. 이 예시의 생성 대상은 위의 여섯 하위 폴더다.</p>
<pre class="text"><code>방금 제안한 구조대로 docs, scripts, src, config, logs, outputs 폴더를 생성해줘.
기존 파일은 덮어쓰지 말고, 생성 후 실제 폴더가 존재하는지 확인해줘.
새로 만든 폴더와 원래 있던 폴더를 구분해서 알려줘.</code></pre>
<p><a href="https://mediamaak.com/assets/public-knowledge/book/project-setup/fig-1-6-folder-structure-created.png"><img src="https://mediamaak.com/assets/public-knowledge/book/project-setup/fig-1-6-folder-structure-created.png" alt="여섯 하위 폴더를 만든 뒤 각 폴더의 존재 여부를 확인하는 교육용 대화 예시" /></a></p>
<p><em>화면 예시 1-E. 생성 뒤 존재 여부를 확인하는 교육용 재현 화면이다. 화면의 문구는 앞선 원고 예시를 바탕으로 했으며 실제 결과 보고 형식은 달라질 수 있다.</em></p>
<p>탐색기에서도 여섯 폴더가 프로젝트 폴더 바로 아래에 있는지 확인한다. 생성했다고 답한 것과 실제 경로가 일치하는지 확인하는 과정까지가 이 단계의 완료 기준이다.</p>
<h2 id="project-rules">7. 프로젝트 작업 규칙을 전달하기</h2>
<p>파일을 만들고 명령을 실행하는 작업에서는 범위와 완료 기준을 함께 전달해야 한다. 다음은 독자가 자신의 프로젝트에 맞춰 사용할 수 있는 <strong>요청문 예시</strong>다.</p>
<pre class="text"><code>앞으로 이 프로젝트에서는 다음 규칙을 지켜줘.

1. 모든 작업은 C:\Users\minjun\auto-trading-lab 폴더 안에서 진행해줘.
2. 작업 목적과 수정할 범위를 설명하고, 요청한 범위의 결과를 확인해줘.
3. 읽기 전용으로 확인해 달라고 한 단계에서는 파일을 수정하지 마.
4. App Key, App Secret, 계좌번호, 비밀번호는 공개 예시와 로그에 직접 쓰지 마.
5. 이번 학습 단계에서는 실전 주문 기능을 구현하거나 주문 API를 호출하지 마.
6. 명령을 실행할 때는 실행 목적과 예상 결과를 알려줘.
7. 작업 후에는 수정한 파일, 실행한 명령, 확인 결과를 정리해줘.</code></pre>
<p><a href="https://mediamaak.com/assets/public-knowledge/book/project-setup/fig-1-7-safety-rules.png"><img src="https://mediamaak.com/assets/public-knowledge/book/project-setup/fig-1-7-safety-rules.png" alt="작업 범위·민감정보 보호·실전 주문 보류를 전달하는 기존 원고의 교육용 대화 예시" /></a></p>
<p><em>화면 예시 1-F. 기존 원고에 포함된 교육용 재현 화면이다. 위 웹 요청문은 읽기 전용 확인과 승인된 파일 생성을 구분하도록 다듬었다. 화면에는 당시 원고의 확인 문구가 남아 있어 현재 요청문과 일부 다르다.</em></p>
<p>요청문으로 범위를 설명하는 것과 앱에서 허용한 접근 권한은 별개다. 프로젝트에 필요한 폴더만 연결하고 실제 실행 결과를 확인한다. 작업 규칙을 전달한 뒤 Codex가 작업 위치, 민감정보 처리, 이번 단계의 실행 범위와 결과 보고 방법을 이해했는지 확인한다.</p>
<h2 id="completion">8. 이 장의 완료 기준과 문제 해결</h2>
<table>
<thead>
<tr>
<th>항목</th>
<th>완료 기준</th>
</tr>
</thead>
<tbody>
<tr>
<td>앱과 계정</td>
<td>Codex 작업을 시작할 수 있다</td>
</tr>
<tr>
<td>작업 위치</td>
<td>사용자가 정한 프로젝트 폴더가 현재 위치로 보고된다</td>
</tr>
<tr>
<td>기본 구조</td>
<td>여섯 하위 폴더가 실제로 존재한다</td>
</tr>
<tr>
<td>작업 범위</td>
<td>읽기 전용 확인과 폴더 생성 요청이 구분되어 실행된다</td>
</tr>
<tr>
<td>설정과 주문</td>
<td>인증정보 입력과 주문 API 호출을 하지 않았다</td>
</tr>
<tr>
<td>결과 확인</td>
<td>만든 폴더와 실행 결과를 사용자가 확인할 수 있다</td>
</tr>
</tbody>
</table>
<h3 id="folder-troubleshooting">작업 경로가 다르거나 폴더가 보이지 않을 때</h3>
<table>
<colgroup>
<col style="width: 33%" />
<col style="width: 33%" />
<col style="width: 33%" />
</colgroup>
<thead>
<tr>
<th>보이는 증상</th>
<th>먼저 확인할 것</th>
<th>다음 행동</th>
</tr>
</thead>
<tbody>
<tr>
<td>다른 프로젝트의 경로가 나온다</td>
<td>현재 대화가 열린 프로젝트와 연결 폴더</td>
<td>올바른 폴더에서 새 Codex 대화를 시작하고 경로를 다시 확인한다</td>
</tr>
<tr>
<td>여러 폴더 중 다른 폴더에서 시작한다</td>
<td>프로젝트의 기본 폴더</td>
<td>프로젝트에서 사용할 폴더를 Make primary로 정하고 새 대화에서 확인한다</td>
</tr>
<tr>
<td>폴더에 접근하지 못한다</td>
<td>실제 폴더 존재 여부와 프로젝트에 연결한 범위</td>
<td>파일 탐색기에서 경로를 확인한 뒤 필요한 폴더만 연결한다</td>
</tr>
<tr>
<td>만든 파일을 찾을 수 없다</td>
<td>응답에 나온 절대 경로와 실제 파일 목록</td>
<td>두 경로를 대조하고 기존 파일을 지우거나 옮기기 전에 위치부터 확인한다</td>
</tr>
</tbody>
</table>
<p>요청문에 경로를 적는 것만으로 앱의 접근 권한이나 연결 폴더가 바뀌지는 않는다. 폴더 연결과 권한은 앱에서 확인하고, 현재 대화의 위치는 다시 조회한다.</p>
<p>위치가 헷갈리면 다음 요청으로 돌아간다.</p>
<pre class="text"><code>현재 Codex가 보고 있는 작업 폴더 위치를 알려줘.
파일 생성이나 수정은 하지 말고, 현재 경로와 폴더 안의 파일 목록만 확인해줘.</code></pre>
<h2 id="setup-faq">자주 막히는 질문</h2>
<h3 id="folder-name-faq">폴더 이름은 예시와 똑같아야 하나요?</h3>
<p>아니다. 이미 쓰는 전용 폴더가 있다면 이름을 바꿀 필요가 없다. 예제의 경로를 실제 위치로 바꾸고 요청·응답·파일 탐색기에 표시된 경로가 같은지 확인한다.</p>
<h3 id="primary-folder-faq">기본 폴더를 바꾼 뒤에는 바로 작업해도 되나요?</h3>
<p>새 대화가 시작하는 기본 위치와 현재 대화의 위치를 구분한다. 기본 폴더를 바꾼 뒤에도 파일을 만들기 전 경로를 확인한다. 기존 대화의 위치가 다르면 원하는 폴더에서 새 Codex 대화를 시작하는 편이 확인하기 쉽다.</p>
<h3 id="setup-prerequisites-faq">이 장에서 Python이나 증권사 API 키도 준비하나요?</h3>
<p>이 장의 완료 기준은 작업 위치와 기본 폴더 구조다. Python과 가상환경은 2장, 증권사 API 준비는 3장에서 진행한다. 아직 실행 코드가 없다는 이유로 이 단계에서 키나 계좌 정보를 입력할 필요는 없다.</p>
<h2 id="next-reading">지금 상태에 맞는 다음 문서</h2>
<ul>
<li><strong>폴더와 기본 구조를 확인했다면:</strong> <a href="https://mediamaak.com/books/auto-trading/python-venv/" data-analytics-role="next">2장: Python 실행 경로와 빈 venv 확인하기</a>. 다음 장에서는 이 프로젝트에서 사용할 Python 환경을 준비한다.</li>
<li><strong>결과를 읽는 순서부터 알고 싶다면:</strong> <a href="https://mediamaak.com/guides/kospi200-backtest/" data-analytics-role="related">KOSPI200 백테스트의 기간·청산 거래·비용 읽기</a>. 개발 환경 준비와 결과 해석을 구분해 볼 수 있다.</li>
<li><strong>전체 읽기 경로를 보관하려면:</strong> <a href="https://mediamaak.com/devlog/2026/10/26/weekly-reading-01/" data-analytics-role="related">주간 읽기 01: 자동매매를 시작하는 순서</a>. 준비·인증·검증 중 자신의 단계에 맞는 문서를 고를 수 있다.</li>
</ul>
<h2 id="sources">출처와 웹 편집 기록</h2>
<p>원고의 1장에 있던 작업 위치 확인, 폴더 구조, 오류 점검, 여섯 교육용 화면을 바탕으로 웹에서 읽을 수 있도록 편집했다. 프로젝트·시작·인증·사용량 공식 문서의 이번 확인일은 <strong>2026년 10월 26일</strong>이다. 앱 메뉴와 계정 안내는 아래 공식 문서를 대조했으며, 새 컴퓨터에서 설치부터 재현한 검증을 뜻하지 않는다.</p>
<ul>
<li><a href="https://learn.chatgpt.com/docs/app">OpenAI 공식 데스크톱 앱 안내</a>: 현재 앱 명칭과 시작 흐름.</li>
<li><a href="https://learn.chatgpt.com/docs/quickstart">OpenAI 공식 시작 안내</a>: 로그인, 폴더 선택, Codex 작업 시작.</li>
<li><a href="https://learn.chatgpt.com/docs/projects">OpenAI 공식 프로젝트 안내</a>: 로컬 폴더 연결과 기본 작업 위치.</li>
<li><a href="https://learn.chatgpt.com/docs/auth">OpenAI 공식 인증 안내</a>: ChatGPT 로그인과 API 키 로그인 구분.</li>
<li><a href="https://learn.chatgpt.com/docs/pricing">OpenAI 공식 가격·사용량 안내</a>: 계정별 기능과 이용 범위 확인.</li>
</ul>
<p>이번 웹 개정에서는 경로 확인 요청문을 앞에 두고, 폴더 문제 해결 표와 자주 묻는 질문, 단계별 다음 읽기를 추가했다. 기존의 앱 명칭 안내, 비용·로그인 구분, 여섯 교육용 화면과 흐름도, 완료 기준은 유지했다. 원본 출판 원고와 교육용 화면 파일은 수정하지 않았다.</p>
<p>이 책은 자동매매 시스템 개발을 설명하는 교육 자료다. 특정 금융상품의 매매를 권유하거나 수익을 보장하지 않는다.</p>
</div>
          <section class="pk-related" aria-labelledby="related-heading"><p class="pk-eyebrow">FROM CONCEPT TO PRACTICE</p><h2 id="related-heading">구현 기록과 함께 읽기</h2><ul><li><a href="https://mediamaak.com/devlog/2026/10/17/kis-token-cache/">한국투자증권 API 토큰을 여러 실행기에서 안전하게 공유하기</a></li><li><a href="https://mediamaak.com/devlog/2026/10/26/weekly-reading-01/">주간 읽기 01: 자동매매를 시작하는 순서</a></li></ul></section>
          <nav class="pk-pager" aria-label="이전 장과 다음 장"><a href="https://mediamaak.com/books/auto-trading/python-venv/">다음 글: Windows에서 Python venv 만들기</a></nav>
          <div class="pk-share"><div><p class="pk-share-title">학습의 다음 단계를 남겨 두세요</p><p>공유 링크를 복사해 이어서 읽을 수 있습니다.</p></div><div><button type="button" data-share-content>공유 링크 복사</button><span role="status" data-share-status></span></div></div>
        </div>
      </div>
    </article>]]></content:encoded>
    </item>
    <item>
      <title>KOSPI200 백테스트 보는 법</title>
      <link>https://mediamaak.com/guides/kospi200-backtest/</link>
      <guid isPermaLink="true">https://mediamaak.com/guides/kospi200-backtest/</guid>
      <pubDate>Wed, 07 Oct 2026 09:00:00 +0900</pubDate>
      <description>종목별 과거 시뮬레이션을 찾고 검증 일봉 기간, 실현손익, 자금·비용 조건과 매수·매도 기록을 확인하는 순서를 안내합니다.</description>
      <content:encoded><![CDATA[<article data-content-id="guides/kospi200-backtest" data-content-type="guide"><p class="eyebrow">MEDIAMAAK GUIDE</p><h1>KOSPI200 백테스트 보는 법</h1><p class="lead">종목 검색 → 검증 일봉 기간 → 자금·비용 조건 → 매수·매도 기록 순서로 읽으세요. 백테스트는 정해진 규칙을 과거 데이터에 적용한 가상 결과입니다. 실제 계좌의 체결 기록이나 미래 수익 예측과 구분해서 확인합니다.</p><p class="guide-meta"><a href="https://mediamaak.com/about.html">MediaMaak 편집팀</a> · 작성 <time datetime="2026-10-07">2026년 10월 7일</time> · 수정 <time datetime="2026-10-26">2026년 10월 26일</time></p>
<p><a class="button" href="https://mediamaak.com/kospi200-simulation.html" data-analytics-role="content">종목 시뮬레이션 열기</a></p>
<nav aria-label="백테스트를 읽는 순서"><ol><li><a href="#read-periods">종목을 찾고 세 가지 날짜 구분하기</a></li><li><a href="#read-scope">실현손익·수익률·비용의 범위 읽기</a></li><li><a href="#read-trades">차트와 매수·매도 기록 대조하기</a></li><li><a href="#compare-results">실제 전략 기록과 구분하기</a></li></ol></nav>
<figure class="guide-hero"><img src="https://mediamaak.com/assets/images/guides/kospi200-backtest.png" width="1200" height="675" alt="가격 차트의 매수와 매도 지점을 데이터 기간과 거래 기록으로 대조하는 그림" loading="eager" fetchpriority="high"><figcaption>매수·매도 가격만 보지 말고 계산 기간과 비용 조건, 거래 기록을 함께 대조합니다.</figcaption></figure>
<h2 id="read-periods">1. 종목을 찾고 세 가지 날짜를 구분하세요</h2>
<p><a href="https://mediamaak.com/kospi200-simulation.html" data-analytics-role="content">KOSPI200 종목 검색</a>에서 종목명이나 6자리 영숫자 코드를 입력하고 원하는 검색 제안을 선택합니다. 공개 검색은 로그인 없이 읽을 수 있으며, 저장된 종목별 결과를 불러옵니다.</p>
<ul><li><strong>검증 일봉 기간:</strong> 계산에 사용한 가격 자료의 시작일과 종료일입니다.</li><li><strong>거래 기간·최근 거래:</strong> 해당 결과에서 시뮬레이션 거래가 생성된 범위와 마지막 거래일입니다.</li><li><strong>결과 게시:</strong> 계산한 결과가 웹용 자료로 게시된 시각입니다. 이 날짜가 최근이어도 가격 자료가 그날까지 갱신됐다는 뜻은 아닙니다.</li></ul>
<p>거래 기간은 검증 일봉 기간보다 짧을 수 있습니다. 기간 끝까지 새 거래가 없었다는 사실과 자료가 오래됐다는 판단을 구분하세요. 검색 대상 목록도 모든 과거 날짜의 KOSPI200 구성 종목을 그대로 재현한다는 뜻은 아닙니다. 과거 구성종목의 편입·편출 시점 검증 범위는 데이터 품질 안내와 함께 확인합니다.</p>
<h2 id="read-scope">2. 실현손익과 수익률의 계산 범위를 읽으세요</h2>
<p><strong>누적 실현손익은 청산된 거래의 손익을 합산한 값입니다.</strong> 미청산 보유분의 평가손익까지 포함한 전체 계좌 성과와 같게 읽지 않습니다. 현재 검색 화면의 누적 수익률은 실현손익을 순입금액(총 입금−총 출금)으로 나눈 기준이며, 입출금 전후의 구간 수익률을 연결하는 시간가중수익률(TWR)과 다릅니다.</p>
<p>입출금·손익 자료가 빠졌거나 순입금액이 0 이하이면 수익률을 산출하지 않습니다. 화면의 <strong>산출 불가</strong>를 0% 성과로 해석하지 마세요.</p>
<ul><li><strong>자금 조건:</strong> 종목별 입금액과 자금 정책을 확인합니다. 모든 종목에 같은 금액을 배정했다고 가정하지 않습니다.</li><li><strong>비용 조건:</strong> 전략 조건의 수수료율을 읽습니다. 수수료율 하나가 표시돼 있다는 이유로 세금·슬리피지 등 모든 비용이 반영됐다고 해석하지 않습니다.</li><li><strong>위험 지표:</strong> 완료 거래 수·승률·손익비와 자산 최대 낙폭을 함께 봅니다. 실현손익 집계와 일별 평가자산 기준의 낙폭은 서로 다른 내용을 보여 줍니다.</li></ul>
<p>손익 금액이 큰 종목과 수익률이 높은 종목이 다를 수 있습니다. 기간·자금·비용 조건이 다른 결과를 숫자 크기만으로 순위 매기기보다, 먼저 같은 기준으로 비교할 수 있는지 확인하세요.</p>
<h2 id="read-trades">3. 차트와 매수·매도 기록을 대조하세요</h2>
<p>화면의 <strong>청산일별 실현손익</strong> 그래프에서 막대는 그날 청산한 거래의 손익, 선은 누적 실현손익입니다. <strong>매수·매도 이벤트</strong>는 사건이 발생한 순서이며 시장 가격축이나 연속된 주가 차트가 아닙니다.</p>
<p>이벤트와 거래 내역에서 진입일·청산일, 매수가·매도가, 실현손익·수익률, 보유 거래일과 청산 사유를 대조합니다. 표시된 가격은 저장된 시뮬레이션의 거래 가격이며 실제 계좌 체결가가 아닙니다. 개별 거래의 주식 수와 연속된 원시 가격 자료는 이 공개 화면에서 제공하지 않습니다.</p>
<p>수치가 예상과 다르면 화면 하단의 <strong>전략 조건</strong>과 <strong>데이터 품질</strong>을 먼저 확인합니다. 저장된 결과를 조회하는 도구이므로 화면에서 기간·전략·수수료를 바꾸어 즉시 재계산하는 방식은 아닙니다.</p>
<h2 id="compare-results">4. 실제 전략 기록은 별도로 확인하세요</h2>
<p><a href="https://mediamaak.com/kospi200/" data-analytics-role="related">KOSPI200 전략 기록</a>은 종목별 과거 시뮬레이션과 다른 자료입니다. 비교할 때는 계산 방식, 시작일·종료일, 비용과 보유분의 반영 범위를 맞춥니다. 본인 기록은 해당 서비스에서 Google 로그인 후 확인합니다.</p>
<p>과거 조건으로 계산한 결과만으로 앞으로 같은 가격에 거래되거나 같은 수익이 난다고 판단할 수 없습니다. 이 안내는 공개 도구의 읽는 순서를 설명하며 종목의 매수·매도를 권하지 않습니다.</p>
<h2 id="faq">자주 묻는 질문</h2>
<h3>수익률이 높으면 내 계좌도 같은 결과가 나오나요?</h3><p>같은 결과를 뜻하지 않습니다. 실제 체결 시점·가격, 자금 배분, 비용과 입출금이 다를 수 있습니다. 먼저 <a href="https://mediamaak.com/guides/backtest-vs-live/" data-analytics-role="related">백테스트와 실제 결과가 다른 이유</a>에서 비교할 조건을 확인하세요.</p>
<h3>오늘의 추천이나 실시간 시세를 보는 화면인가요?</h3><p>과거 시뮬레이션을 조회하는 화면입니다. 거래 가격과 결과 게시 시각을 실시간 시세로 읽지 않습니다. 오늘의 추천은 별도 화면이며, 추천 후보와 과거 시뮬레이션 결과도 구분해서 봅니다.</p>
<h3>완료 거래가 적거나 거래 기간이 짧으면 어떻게 읽나요?</h3><p>표본 수와 계산에 사용한 자료 범위를 함께 확인합니다. 작은 표본에서 나온 비율을 다른 종목이나 미래 기간에 그대로 적용하지 않습니다. 결과를 불러오지 못한 상태도 거래가 없거나 손익이 0이라는 뜻은 아닙니다.</p>
<h3>기간이나 전략 조건을 바꿔 직접 검증하고 싶어요</h3><p>공개 검색은 저장된 결과 조회용입니다. 직접 계산하는 흐름은 <a href="https://mediamaak.com/books/auto-trading/simulation/" data-analytics-role="related">위키 7장: CCI 신호를 과거 데이터로 시뮬레이션하기</a>에서 살펴볼 수 있습니다. 검색 화면의 결과와 위키의 학습용 예제를 같은 계산 결과로 간주하지 않습니다.</p>
<div class="guide-next" id="next-reading"><h2>이어서 확인할 내용</h2><p><a class="button" href="https://mediamaak.com/kospi200-simulation.html" data-analytics-role="content">종목별 기간과 거래 기록 확인하기</a></p><p><a href="https://mediamaak.com/guides/returns-with-cashflows/" rel="next" data-analytics-role="next">다음 글: 입출금이 있을 때 수익률 계산하기</a></p><p>수익률 숫자가 다르게 보이는 이유를 간단한 입금 예제로 확인합니다.</p></div>
<p class="guide-sources">참고: 미국 SEC 투자자 교육 <a href="https://www.investor.gov/introduction-investing/general-resources/news-alerts/alerts-bulletins/investor-bulletins-47">Investor Bulletin: Performance Claims</a>는 백테스트를 가상 성과로 구분하고 비용·계산 방법·비교 기준을 확인하도록 설명합니다. 이 글의 화면 안내는 MediaMaak 공개 검색의 표시 항목을 기준으로 작성했습니다.</p>
<div class="guide-share"><button class="secondary" type="button" data-share-content>공유 링크 복사</button><span role="status" data-share-status></span></div></article>]]></content:encoded>
    </item>
    <item>
      <title>CCI 신호를 과거 데이터로 시뮬레이션하기</title>
      <link>https://mediamaak.com/books/auto-trading/simulation/</link>
      <guid isPermaLink="true">https://mediamaak.com/books/auto-trading/simulation/</guid>
      <pubDate>Tue, 20 Oct 2026 00:00:00 +0900</pubDate>
      <description>신호 다음 거래일 진입과 고정된 청산 규칙으로 계산 흐름을 검증합니다.</description>
      <content:encoded><![CDATA[<article class="pk-article" data-content-id="/books/auto-trading/simulation/" data-content-type="wiki-chapter" data-published-at="2026-10-20T00:00:00+09:00" data-updated-at="2026-10-20T00:00:00+09:00">
      <header class="pk-hero">
        <p class="pk-eyebrow">7 / 12 · AUTO TRADING WIKI</p>
        <h1>CCI 신호를 과거 데이터로 시뮬레이션하기</h1>
        <p class="pk-summary">신호 다음 거래일 진입과 고정된 청산 규칙으로 계산 흐름을 검증합니다.</p>
        <div class="pk-meta"><dl><dt>발행</dt><dd><time datetime="2026-10-20T00:00:00+09:00">2026-10-20</time></dd><dt>수정</dt><dd><time datetime="2026-10-20T00:00:00+09:00">2026-10-20</time></dd><dt>문서 검토</dt><dd>2026-10-20</dd><dt>본문 버전</dt><dd>1.1.0-web.20261020</dd></dl></div>
      </header>
      <div class="book-reading-content">
        <div class="pk-body-column">
          <div class="pk-prose"><p>CCI 신호를 계산했다면 이제 그 신호를 과거 가격에 적용해 본다. 이번 장의 결과는 수익 인증서가 아니라, 같은 입력으로 같은 진입·청산 기록을 다시 만들 수 있는 시뮬레이션이다.</p>
<p>이 장의 경로·종목·숫자·로그는 교육용 가상 예시다. 작업 폴더는 <code>C:\Users\minjun\auto-trading-lab</code>이며, 가격 원본은 앞 장의 <code>daily_prices(symbol, trade_date, open, high, low, close, volume)</code>을 사용한다. 주문 API를 호출하지 않는다.</p>
<h2 id="simulation-purpose">1. 시뮬레이션의 질문부터 정하기</h2>
<p>“얼마나 벌었는가”를 보기 전에 “그 시점에 알 수 있던 정보만 사용했는가”를 확인한다. T일 종가로 계산한 CCI는 T일 장이 끝난 뒤 확정된다. 그 값을 보고 T일 시가에 매수한 것으로 계산하면 미래 정보를 사용하게 된다.</p>
<p>여기서는 CCI 기간을 20으로 고정하고, 전 거래일 CCI가 <code>-100</code> 이하이며 당일 CCI가 <code>-100</code>을 초과하는 <strong>상향 돌파</strong>만 예제 신호로 삼는다. CCI가 낮다는 사실 자체를 매수 신호로 취급하지 않는다. 이 기준은 계산을 설명하기 위한 학습용 규칙이며 종목 추천이나 검증된 투자 전략이 아니다.</p>
<p>시뮬레이션에서 확인할 질문은 네 가지다. 신호 이후의 가격을 정확히 연결했는가, 보유 기간을 일관되게 셌는가, 비용과 제외 사유를 남겼는가, 재실행해도 결과가 중복되지 않는가. 모든 질문에 기록으로 답할 수 있어야 다음 장으로 넘어간다.</p>
<h2 id="simulation-rules">2. 진입·청산 규칙을 한 문서로 고정하기</h2>
<p><code>config/simulation.json</code>에 다음과 같이 저장하도록 설계한다. 아래 비용은 실제 증권사 요율이나 현행 세율이 아니라 계산을 연습하기 위한 가정값이다.</p>
<div class="sourceCode" id="cb1"><pre class="sourceCode json"><code class="sourceCode json"><span id="cb1-1"><a href="#cb1-1" aria-hidden="true" tabindex="-1"></a><span class="fu">{</span></span>
<span id="cb1-2"><a href="#cb1-2" aria-hidden="true" tabindex="-1"></a>  <span class="dt">&quot;strategy_version&quot;</span><span class="fu">:</span> <span class="st">&quot;cci20_cross_up_m100_v1&quot;</span><span class="fu">,</span></span>
<span id="cb1-3"><a href="#cb1-3" aria-hidden="true" tabindex="-1"></a>  <span class="dt">&quot;cci_period&quot;</span><span class="fu">:</span> <span class="dv">20</span><span class="fu">,</span></span>
<span id="cb1-4"><a href="#cb1-4" aria-hidden="true" tabindex="-1"></a>  <span class="dt">&quot;signal&quot;</span><span class="fu">:</span> <span class="st">&quot;prev_cci &lt;= -100 and cci &gt; -100&quot;</span><span class="fu">,</span></span>
<span id="cb1-5"><a href="#cb1-5" aria-hidden="true" tabindex="-1"></a>  <span class="dt">&quot;entry&quot;</span><span class="fu">:</span> <span class="st">&quot;next_session_open&quot;</span><span class="fu">,</span></span>
<span id="cb1-6"><a href="#cb1-6" aria-hidden="true" tabindex="-1"></a>  <span class="dt">&quot;holding_sessions&quot;</span><span class="fu">:</span> <span class="dv">5</span><span class="fu">,</span></span>
<span id="cb1-7"><a href="#cb1-7" aria-hidden="true" tabindex="-1"></a>  <span class="dt">&quot;exit&quot;</span><span class="fu">:</span> <span class="st">&quot;fifth_session_close&quot;</span><span class="fu">,</span></span>
<span id="cb1-8"><a href="#cb1-8" aria-hidden="true" tabindex="-1"></a>  <span class="dt">&quot;same_symbol_overlap&quot;</span><span class="fu">:</span> <span class="st">&quot;skip&quot;</span><span class="fu">,</span></span>
<span id="cb1-9"><a href="#cb1-9" aria-hidden="true" tabindex="-1"></a>  <span class="dt">&quot;fee_bps_each_side&quot;</span><span class="fu">:</span> <span class="st">&quot;5&quot;</span><span class="fu">,</span></span>
<span id="cb1-10"><a href="#cb1-10" aria-hidden="true" tabindex="-1"></a>  <span class="dt">&quot;sell_tax_bps&quot;</span><span class="fu">:</span> <span class="st">&quot;20&quot;</span><span class="fu">,</span></span>
<span id="cb1-11"><a href="#cb1-11" aria-hidden="true" tabindex="-1"></a>  <span class="dt">&quot;slippage_bps_each_side&quot;</span><span class="fu">:</span> <span class="st">&quot;0&quot;</span><span class="fu">,</span></span>
<span id="cb1-12"><a href="#cb1-12" aria-hidden="true" tabindex="-1"></a>  <span class="dt">&quot;cost_model&quot;</span><span class="fu">:</span> <span class="st">&quot;educational-assumption-v1&quot;</span></span>
<span id="cb1-13"><a href="#cb1-13" aria-hidden="true" tabindex="-1"></a><span class="fu">}</span></span></code></pre></div>
<table>
<thead>
<tr>
<th>항목</th>
<th>이번 장의 처리</th>
</tr>
</thead>
<tbody>
<tr>
<td>신호 확정</td>
<td>T일 종가까지의 데이터로만 계산</td>
</tr>
<tr>
<td>진입</td>
<td>T 다음의 실제 거래 세션 시가</td>
</tr>
<tr>
<td>보유 일수</td>
<td>진입 세션을 1일째로 계산</td>
</tr>
<tr>
<td>청산</td>
<td>5번째 보유 세션 종가</td>
</tr>
<tr>
<td>중복 신호</td>
<td>같은 종목 보유 중이면 <code>OVERLAPPING_POSITION</code></td>
</tr>
<tr>
<td>기간 끝의 신호</td>
<td>필요한 가격이 없으면 미완료로 분리</td>
</tr>
</tbody>
</table>
<p>“5거래일 보유”를 <code>entry_index + 5</code>로 구현하면 이 문서의 규칙보다 하루 늦어진다. 진입일을 포함하므로 청산 위치는 <code>entry_index + 4</code>다. 주말이나 공휴일을 단순히 날짜에 더하지 않는다.</p>
<p>또한 다음 행이 있다는 사실만으로 다음 거래일이 확인되지는 않는다. 원본에서 하루가 빠졌다면 그다음 행은 T+2일 수 있다. 거래 캘린더와 종목의 거래 가능 상태를 함께 확인하고, 원인 불명 누락은 <code>MISSING_EXPECTED_SESSION</code>으로 제외한다.</p>
<h2 id="simulation-inputs">3. 데이터 묶음과 실행 식별자 보관하기</h2>
<p>같은 기간이라도 원본 가격이 수정되거나 대상 종목이 바뀌면 결과가 달라진다. 실행마다 <code>run_id</code>, 코드 버전, 설정 해시, 데이터 스냅샷 식별자, 대상 종목 목록의 기준일을 보관한다. 오늘의 구성 종목을 과거 전체 기간에 적용했다면 구성 종목의 생존 편향 가능성도 보고서에 쓴다.</p>
<pre class="text"><code>outputs/simulation/demo-20261020-01/
  manifest.json
  trades.csv
  skipped_signals.csv
  summary.json</code></pre>
<p><code>manifest.json</code>에는 비밀번호가 들어간 연결 문자열 대신 <code>dataset_id</code>, 조회 기간, 행 수, 정렬 기준을 남긴다. 날짜·종목순으로 정렬한 입력의 해시를 보관하면 재현에 도움이 된다. Python의 SHA-256 사용 방법은 <a href="https://docs.python.org/3/library/hashlib.html">공식 hashlib 문서</a>에 나와 있다.</p>
<p>데이터 조회는 <code>ORDER BY symbol, trade_date</code>로 고정한다. 수정주가와 원주가를 섞거나, 분할 전후 가격을 조정 없이 이어 붙이지 않는다. 가격 조정 정책을 확인할 수 없다면 그 구간을 별도로 표시하고 성과 해석을 보류한다.</p>
<p>앞 장의 CSV 입력은 <code>outputs/indicators/cci_20.csv</code>이며 지표 열은 <code>cci_20</code>, 신호 열은 <code>entry_signal</code>이다. DB에 저장했다면 <code>cci_daily_signals.cci_value</code>를 <code>cci_20</code>으로 읽고 <code>trade_date</code>를 신호 날짜로 연결한다. DB의 <code>period = 20</code>, <code>strategy_version = 'cci20_cross_up_m100_v1'</code> 조건으로 같은 계산 버전만 선택한다.</p>
<div class="sourceCode" id="cb3"><pre class="sourceCode sql"><code class="sourceCode sql"><span id="cb3-1"><a href="#cb3-1" aria-hidden="true" tabindex="-1"></a><span class="kw">SELECT</span> symbol, trade_date, cci_value <span class="kw">AS</span> cci_20, entry_signal</span>
<span id="cb3-2"><a href="#cb3-2" aria-hidden="true" tabindex="-1"></a><span class="kw">FROM</span> cci_daily_signals</span>
<span id="cb3-3"><a href="#cb3-3" aria-hidden="true" tabindex="-1"></a><span class="kw">WHERE</span> period <span class="op">=</span> <span class="dv">20</span> <span class="kw">AND</span> strategy_version <span class="op">=</span> <span class="st">&#39;cci20_cross_up_m100_v1&#39;</span></span>
<span id="cb3-4"><a href="#cb3-4" aria-hidden="true" tabindex="-1"></a><span class="kw">ORDER</span> <span class="kw">BY</span> symbol, trade_date;</span></code></pre></div>
<p>신호 후보는 <code>entry_signal = true</code>인 행이다. 가격과 연결할 때는 <code>(symbol, trade_date)</code>를 사용한다. CSV를 읽는 경우 불리언 열의 <code>True</code>·<code>False</code> 문자열을 엄격하게 해석하고, 문자열 <code>"False"</code>를 단순한 참값으로 변환하지 않는다. 이전 원고의 <code>watch</code> 신호와 이 장의 상향 돌파 신호를 섞지 않는다.</p>
<h2 id="simulation-example">4. 손으로 맞출 수 있는 작은 사례 만들기</h2>
<p>먼저 가상 종목 <code>DEMO_A</code> 한 개로 날짜 연결을 확인한다. 아래 날짜는 교육용 세션 목록이며 실제 거래일 캘린더를 대신하지 않는다.</p>
<table>
<thead>
<tr>
<th>이벤트</th>
<th>세션</th>
<th>값</th>
</tr>
</thead>
<tbody>
<tr>
<td>직전 CCI</td>
<td>2026-06-01</td>
<td>-120</td>
</tr>
<tr>
<td>신호 CCI</td>
<td>2026-06-02</td>
<td>-85</td>
</tr>
<tr>
<td>진입·보유 1일째</td>
<td>2026-06-03</td>
<td>시가 10,000</td>
</tr>
<tr>
<td>보유 2·3·4일째</td>
<td>06-04, 06-05, 06-08</td>
<td>중간 가격 사용</td>
</tr>
<tr>
<td>보유 5일째·청산</td>
<td>2026-06-09</td>
<td>종가 10,200</td>
</tr>
</tbody>
</table>
<p>10주를 샀다고 가정하면 매수 금액은 100,000, 매도 금액은 102,000이다. 비용 전 손익은 2,000이다. 예제 수수료는 매수 50, 매도 51이고, 가정 매도세금은 204이므로 비용 후 손익은 1,695다. 슬리피지는 이 사례에서 0으로 놓았다.</p>
<div class="sourceCode" id="cb4"><pre class="sourceCode python"><code class="sourceCode python"><span id="cb4-1"><a href="#cb4-1" aria-hidden="true" tabindex="-1"></a><span class="im">from</span> decimal <span class="im">import</span> Decimal</span>
<span id="cb4-2"><a href="#cb4-2" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb4-3"><a href="#cb4-3" aria-hidden="true" tabindex="-1"></a>qty <span class="op">=</span> Decimal(<span class="st">&quot;10&quot;</span>)</span>
<span id="cb4-4"><a href="#cb4-4" aria-hidden="true" tabindex="-1"></a>entry <span class="op">=</span> Decimal(<span class="st">&quot;10000&quot;</span>) <span class="op">*</span> qty</span>
<span id="cb4-5"><a href="#cb4-5" aria-hidden="true" tabindex="-1"></a>exit_value <span class="op">=</span> Decimal(<span class="st">&quot;10200&quot;</span>) <span class="op">*</span> qty</span>
<span id="cb4-6"><a href="#cb4-6" aria-hidden="true" tabindex="-1"></a>buy_fee <span class="op">=</span> entry <span class="op">*</span> Decimal(<span class="st">&quot;0.0005&quot;</span>)</span>
<span id="cb4-7"><a href="#cb4-7" aria-hidden="true" tabindex="-1"></a>sell_fee <span class="op">=</span> exit_value <span class="op">*</span> Decimal(<span class="st">&quot;0.0005&quot;</span>)</span>
<span id="cb4-8"><a href="#cb4-8" aria-hidden="true" tabindex="-1"></a>sell_tax <span class="op">=</span> exit_value <span class="op">*</span> Decimal(<span class="st">&quot;0.0020&quot;</span>)</span>
<span id="cb4-9"><a href="#cb4-9" aria-hidden="true" tabindex="-1"></a>net_pnl <span class="op">=</span> exit_value <span class="op">-</span> entry <span class="op">-</span> buy_fee <span class="op">-</span> sell_fee <span class="op">-</span> sell_tax</span>
<span id="cb4-10"><a href="#cb4-10" aria-hidden="true" tabindex="-1"></a><span class="bu">print</span>(net_pnl)  <span class="co"># 1695.0000</span></span></code></pre></div>
<p>금액에는 문자열로 만든 <code>Decimal</code>을 사용하고 최종 반올림 위치를 별도로 정한다. 이는 소수 계산 오차를 관리하기 위한 선택이다. <a href="https://docs.python.org/3/library/decimal.html">Python decimal 공식 문서</a>는 정밀도와 반올림 방법을 설명한다. 실제 비용 처리 규칙은 이 예제와 다를 수 있다.</p>
<h2 id="simulation-fill-limits">5. 일봉으로 알 수 없는 체결은 표시하기</h2>
<p>일봉의 시가에 원하는 수량을 모두 체결했다고 보는 것은 단순한 가정이다. 거래정지, 유동성 부족, 가격 제한, 주문 지연을 재현하지 못한다. <code>fill_model = daily_open_close_assumption</code>을 결과에 명시하고, 거래량이 0이거나 가격이 비정상인 날은 체결로 만들지 않는다.</p>
<p>이번 규칙은 정해진 날의 종가 청산이므로 장중 손절·익절을 사용하지 않는다. 나중에 고가가 익절가를 넘고 저가가 손절가 아래인 일봉을 만나도 어느 가격에 먼저 도달했는지는 알 수 없다. 그때는 분봉 등 더 세밀한 데이터가 필요하며, 임의로 유리한 청산 순서를 택하지 않는다. 보수적인 우선순위를 가정하더라도 <code>intraday_order_unknown</code>을 기록해야 한다.</p>
<p>비용 전 결과와 비용 후 결과를 함께 저장한다. 슬리피지 0, 5, 10bp처럼 다른 가정을 비교할 수 있지만 가장 좋아 보이는 가정만 본문에 남기지 않는다. 이는 비용 민감도 확인이지 실제 체결 검증이 아니다.</p>
<h2 id="simulation-storage">6. 완료와 제외를 같은 무게로 저장하기</h2>
<p><code>simulation_runs</code>는 실행 단위, <code>simulation_trades</code>는 신호별 결과 단위로 나눈다. 결과의 고유 기준은 <code>(run_id, symbol, signal_date)</code>이며, 새 실험은 새 <code>run_id</code>를 사용한다. 동일 실행의 재시도는 기존 결과와 비교한 뒤 같은 결과이면 건너뛴다. 다른 결과이면 덮어쓰지 않고 불일치로 중단한다.</p>
<table>
<thead>
<tr>
<th>결과 필드</th>
<th>기록할 내용</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>status</code></td>
<td><code>COMPLETED</code>, <code>SKIPPED</code>, <code>INCOMPLETE</code></td>
</tr>
<tr>
<td><code>signal_date</code>, <code>entry_date</code>, <code>exit_date</code></td>
<td>신호·진입·청산 세션</td>
</tr>
<tr>
<td><code>quantity</code>, <code>entry_price</code>, <code>exit_price</code></td>
<td>가상 수량과 가정 가격</td>
</tr>
<tr>
<td><code>gross_pnl</code>, <code>fees</code>, <code>tax</code>, <code>net_pnl</code></td>
<td>비용을 구분한 손익</td>
</tr>
<tr>
<td><code>skip_reason</code></td>
<td>누락, 중복 보유, 가격 이상 등의 사유</td>
</tr>
<tr>
<td><code>strategy_version</code>, <code>dataset_id</code></td>
<td>재현에 필요한 기준</td>
</tr>
</tbody>
</table>
<p>SQL 조회 예시는 다음과 같다. <code>simulation_trades</code>는 이번 장에서 새로 만드는 교육용 결과 테이블 이름이다.</p>
<div class="sourceCode" id="cb5"><pre class="sourceCode sql"><code class="sourceCode sql"><span id="cb5-1"><a href="#cb5-1" aria-hidden="true" tabindex="-1"></a><span class="kw">SELECT</span> status, <span class="fu">COALESCE</span>(skip_reason, <span class="st">&#39;NONE&#39;</span>) <span class="kw">AS</span> reason, <span class="fu">COUNT</span>(<span class="op">*</span>) <span class="kw">AS</span> cnt</span>
<span id="cb5-2"><a href="#cb5-2" aria-hidden="true" tabindex="-1"></a><span class="kw">FROM</span> simulation_trades</span>
<span id="cb5-3"><a href="#cb5-3" aria-hidden="true" tabindex="-1"></a><span class="kw">WHERE</span> run_id <span class="op">=</span> <span class="st">&#39;demo-20261020-01&#39;</span></span>
<span id="cb5-4"><a href="#cb5-4" aria-hidden="true" tabindex="-1"></a><span class="kw">GROUP</span> <span class="kw">BY</span> status, <span class="fu">COALESCE</span>(skip_reason, <span class="st">&#39;NONE&#39;</span>);</span></code></pre></div>
<p>예를 들어 신호 21건 중 완료 15건, 기간 끝 미완료 4건, 보유 중 제외 2건이면 합계가 21건이어야 한다. 완료 15건만 보고 신호 처리율이 100%라고 표시하지 않는다. 제외가 없는 결과보다, 모든 제외를 설명할 수 있는 결과가 검토에 유용하다.</p>
<h2 id="simulation-codex">7. Codex에 구현과 확인을 나누어 요청하기</h2>
<p>다음 요청문으로 파일 구조와 최소 재현 사례를 먼저 만든다. 실행 명령은 구현이 끝난 뒤 실제 파일 이름과 옵션을 확인하고 사용한다.</p>
<pre class="text"><code>auto-trading-lab에서 과거 데이터 시뮬레이션만 구현해줘.
daily_prices와 앞 장의 CCI 결과를 읽고 CCI20 -100 상향 돌파를 사용해.
T+1 거래 세션 시가 진입, 진입일 포함 5번째 세션 종가 청산으로 고정해.
비용은 config/simulation.json의 학습용 가정을 사용하고 비용 전후를 구분해.
중복 보유, 누락 세션, 기간 끝 미완료를 사유별로 보관해.
src/simulate.py, scripts/run_simulation.ps1와 가상 데이터 예제를 만들어줘.
설정·데이터·코드 버전과 run_id를 남기고 재실행 시 중복을 방지해.
실제 주문, 증권사 모의주문, 외부 주문 전송 코드는 만들지 마.
한 종목 수동 계산과 날짜 경계·미완료 사례를 확인한 뒤 결과를 보고해.</code></pre>
<p>정상 로그의 모양은 아래와 같다. 실제 실행 결과가 아니라 필요한 항목을 보여주는 예시다.</p>
<pre class="text"><code>run_id=demo-20261020-01 mode=simulation dataset=fixture-v1
signals=21 completed=15 skipped=2 incomplete=4
check=entry_after_signal result=PASS
check=duplicate_result result=PASS duplicates=0
state=COMPLETED external_order_calls=0</code></pre>
<h2 id="simulation-done">8. 완료 기준과 문제 해결</h2>
<p>완료 기준은 가상 사례의 날짜·비용이 손계산과 일치하고, 모든 신호가 완료·제외·미완료 중 하나에 속하며, 같은 실행을 다시 처리해도 행과 손익이 늘지 않는 것이다. 개별 거래 수익률의 평균은 자금 제약을 반영한 포트폴리오 수익률과 다르므로 별도로 표시한다.</p>
<table>
<thead>
<tr>
<th>증상</th>
<th>먼저 확인할 것</th>
</tr>
</thead>
<tbody>
<tr>
<td>진입일이 신호일과 같음</td>
<td>날짜 결합과 <code>next_session</code> 선택 조건</td>
</tr>
<tr>
<td>청산이 하루 늦음</td>
<td>진입일을 1일째로 셌는지</td>
</tr>
<tr>
<td>결과가 비정상적으로 좋음</td>
<td>미래 정보, 누락 종목, 비용·가격 조정 정책</td>
</tr>
<tr>
<td>결과 건수가 재실행마다 증가</td>
<td>실행 식별자와 고유 제약</td>
</tr>
<tr>
<td>완료율이 낮음</td>
<td>기간 끝 미완료와 실제 데이터 누락의 구분</td>
</tr>
</tbody>
</table>
<p>문제가 생기면 전략 기준을 바꾸기 전에 해당 신호 한 건의 원본 가격·CCI·날짜 연결·계산식을 함께 출력한다. 다음 장에서는 이 기록을 바탕으로 페이퍼 실행을 시작할 기술적 준비가 되었는지 점검한다.</p>
</div>
          <section class="pk-related" aria-labelledby="related-heading"><p class="pk-eyebrow">FROM CONCEPT TO PRACTICE</p><h2 id="related-heading">구현 기록과 함께 읽기</h2><ul><li><a href="https://mediamaak.com/books/auto-trading/cci-signals/">MySQL 데이터로 CCI 신호 계산하기</a></li></ul></section>
          <nav class="pk-pager" aria-label="이전 장과 다음 장"><a href="https://mediamaak.com/books/auto-trading/cci-signals/">이전 글: MySQL 데이터로 CCI 신호 계산하기</a><a href="https://mediamaak.com/books/auto-trading/paper-readiness/">다음 글: 페이퍼 주문을 시작하기 전 확인할 것</a></nav>
          <div class="pk-share"><div><p class="pk-share-title">학습의 다음 단계를 남겨 두세요</p><p>공유 링크를 복사해 이어서 읽을 수 있습니다.</p></div><div><button type="button" data-share-content>공유 링크 복사</button><span role="status" data-share-status></span></div></div>
        </div>
      </div>
    </article>]]></content:encoded>
    </item>
    <item>
      <title>반복 실행과 중단 조건 만들기</title>
      <link>https://mediamaak.com/books/auto-trading/repeat-stop/</link>
      <guid isPermaLink="true">https://mediamaak.com/books/auto-trading/repeat-stop/</guid>
      <pubDate>Tue, 20 Oct 2026 00:00:00 +0900</pubDate>
      <description>하루 실행 순서와 중복 방지·오류 차단·실행 감사 기준을 정합니다.</description>
      <content:encoded><![CDATA[<article class="pk-article" data-content-id="/books/auto-trading/repeat-stop/" data-content-type="wiki-chapter" data-published-at="2026-10-20T00:00:00+09:00" data-updated-at="2026-10-20T00:00:00+09:00">
      <header class="pk-hero">
        <p class="pk-eyebrow">10 / 12 · AUTO TRADING WIKI</p>
        <h1>반복 실행과 중단 조건 만들기</h1>
        <p class="pk-summary">하루 실행 순서와 중복 방지·오류 차단·실행 감사 기준을 정합니다.</p>
        <div class="pk-meta"><dl><dt>발행</dt><dd><time datetime="2026-10-20T00:00:00+09:00">2026-10-20</time></dd><dt>수정</dt><dd><time datetime="2026-10-20T00:00:00+09:00">2026-10-20</time></dd><dt>문서 검토</dt><dd>2026-10-20</dd><dt>본문 버전</dt><dd>1.1.0-web.20261020</dd></dl></div>
      </header>
      <div class="book-reading-content">
        <div class="pk-body-column">
          <div class="pk-prose"><p>한 번 성공한 페이퍼 실행과 매일 반복해도 안전한 실행은 다르다. 반복 실행에서는 같은 날 두 번 시작되거나, 체결 기록을 저장하는 중에 프로그램이 끝나거나, 데이터 수집이 늦어지는 일이 생긴다. 이번 장은 그 상황을 기록하고 멈추는 방법을 다룬다.</p>
<p>예제는 <code>auto-trading-lab</code>의 로컬 <code>paper</code> 원장만 사용한다. 스케줄러를 켜는 것보다 중복 방지·완료 판정·복구 근거를 먼저 준비한다. 모든 시간과 로그는 가상 예시다.</p>
<h2 id="repeat-state">1. 하루 실행을 단계별 상태로 남기기</h2>
<p>실행 시작 때 <code>run_id</code>를 만들고 <code>paper_runs</code>에 기록한다. 단계가 끝날 때마다 마지막 확정 지점을 저장한다. 프로그램이 살아 있다는 사실과 해당 거래일의 처리가 끝났다는 사실은 별개다.</p>
<pre class="text"><code>CREATED → VALIDATING → SIGNALS_READY → ORDERS_READY
        → SETTLING → RECONCILING → COMPLETED
어느 단계에서든 → BLOCKED 또는 FAILED
저장 상태 불명확 → RECOVERY_REQUIRED</code></pre>
<p><code>BLOCKED</code>는 데이터 지연이나 중단 파일처럼 정해진 조건에 걸린 상태다. <code>FAILED</code>는 예외로 정상 진행하지 못한 상태다. <code>RECOVERY_REQUIRED</code>는 DB 확정 여부를 다시 확인하기 전에는 처리 재개를 허용하지 않는 상태다. 이들을 모두 성공 종료 코드로 숨기지 않는다.</p>
<h2 id="repeat-session">2. 거래일과 실행 시간을 구분하기</h2>
<p><code>business_date</code>는 처리할 거래 세션, <code>started_at</code>은 실제 실행을 시작한 시각이다. 6월 3일 데이터를 6월 4일 새벽에 처리할 수 있으므로 두 값을 같은 날짜로 가정하지 않는다. 시각에는 시간대를 보관하고 화면에는 한국 시간 기준임을 표시한다.</p>
<p>반복 실행의 입력은 <code>daily_prices</code>의 데이터 확정 상태와 거래 캘린더다. 컴퓨터의 오늘 날짜나 매주 월요일부터 금요일이라는 조건만으로 다음 세션을 정하지 않는다. 공식 거래일과 종목의 거래 가능 여부가 확인되지 않으면 <code>CALENDAR_NOT_READY</code>로 멈춘다.</p>
<p>일봉 기반 로컬 재생은 이미 확정된 과거 세션을 처리하는 작업이다. 실제 장 시작 시점의 주문 실행과 다르다. T일 종가 신호는 T+1 가상 <code>OPEN</code> 이벤트와 연결하고, 보고서에 가격의 관측 시각과 처리 시각을 나누어 적는다.</p>
<h2 id="repeat-lock">3. 중복 실행과 중복 주문을 각각 막기</h2>
<p>스케줄러 설정에서 중복 실행을 제한해도 수동 실행이 겹칠 수 있다. 프로그램에도 계정·모드별 실행 잠금을 둔다. 동시에 두 실행이 시작되면 하나만 진행하고 나머지는 <code>ALREADY_RUNNING</code>으로 종료한다.</p>
<p>잠금은 실행을 겹치지 않게 하는 장치이며, 주문 중복을 영구적으로 막는 장치는 아니다. 주문에는 앞 장의 고유 의도키가 별도로 필요하다. 같은 <code>(account_id, strategy_version, symbol, signal_date, action)</code>은 다른 <code>run_id</code>에서 재처리해도 기존 주문을 찾아야 한다.</p>
<p>DB 연결이 끊기면 잠금도 사라질 수 있으므로 연결 상실 즉시 새 변경을 중단한다. 잠금을 다시 얻었다고 곧바로 주문 단계부터 반복하지 않는다. 먼저 이전 실행과 원장 상태를 대조한다. 계정 행 잠금과 원자적 변경은 <a href="https://dev.mysql.com/doc/refman/8.4/en/innodb-locking-reads.html">MySQL 공식 잠금 읽기 문서</a>를 따른다.</p>
<h2 id="repeat-stop-rules">4. 중단 조건은 설정으로 고정하기</h2>
<p>다음은 <code>config/paper_runtime.json</code>의 학습용 예시다. 실제 운영에서 사용할 지연 허용 시간은 데이터 출처와 수집 주기를 확인해 정해야 한다.</p>
<div class="sourceCode" id="cb2"><pre class="sourceCode json"><code class="sourceCode json"><span id="cb2-1"><a href="#cb2-1" aria-hidden="true" tabindex="-1"></a><span class="fu">{</span></span>
<span id="cb2-2"><a href="#cb2-2" aria-hidden="true" tabindex="-1"></a>  <span class="dt">&quot;mode&quot;</span><span class="fu">:</span> <span class="st">&quot;paper&quot;</span><span class="fu">,</span></span>
<span id="cb2-3"><a href="#cb2-3" aria-hidden="true" tabindex="-1"></a>  <span class="dt">&quot;stop_file&quot;</span><span class="fu">:</span> <span class="st">&quot;config/STOP_PAPER&quot;</span><span class="fu">,</span></span>
<span id="cb2-4"><a href="#cb2-4" aria-hidden="true" tabindex="-1"></a>  <span class="dt">&quot;max_consecutive_failures&quot;</span><span class="fu">:</span> <span class="dv">2</span><span class="fu">,</span></span>
<span id="cb2-5"><a href="#cb2-5" aria-hidden="true" tabindex="-1"></a>  <span class="dt">&quot;stop_on_unexplained_price_gap&quot;</span><span class="fu">:</span> <span class="kw">true</span><span class="fu">,</span></span>
<span id="cb2-6"><a href="#cb2-6" aria-hidden="true" tabindex="-1"></a>  <span class="dt">&quot;stop_on_reconciliation_difference&quot;</span><span class="fu">:</span> <span class="kw">true</span><span class="fu">,</span></span>
<span id="cb2-7"><a href="#cb2-7" aria-hidden="true" tabindex="-1"></a>  <span class="dt">&quot;stop_on_intent_conflict&quot;</span><span class="fu">:</span> <span class="kw">true</span><span class="fu">,</span></span>
<span id="cb2-8"><a href="#cb2-8" aria-hidden="true" tabindex="-1"></a>  <span class="dt">&quot;allow_external_order_calls&quot;</span><span class="fu">:</span> <span class="kw">false</span><span class="fu">,</span></span>
<span id="cb2-9"><a href="#cb2-9" aria-hidden="true" tabindex="-1"></a>  <span class="dt">&quot;resume_requires_reconciliation&quot;</span><span class="fu">:</span> <span class="kw">true</span></span>
<span id="cb2-10"><a href="#cb2-10" aria-hidden="true" tabindex="-1"></a><span class="fu">}</span></span></code></pre></div>
<table>
<thead>
<tr>
<th>중단 사유</th>
<th>처리</th>
</tr>
</thead>
<tbody>
<tr>
<td>중단 파일 존재</td>
<td>새 의도·체결 생성 차단</td>
</tr>
<tr>
<td>데이터 미확정 또는 원인 불명 누락</td>
<td><code>BLOCKED/DATA_NOT_READY</code></td>
</tr>
<tr>
<td>현금·수량 대조 불일치</td>
<td><code>RECOVERY_REQUIRED</code></td>
</tr>
<tr>
<td>같은 의도키에 다른 내용</td>
<td><code>BLOCKED/INTENT_CONFLICT</code></td>
</tr>
<tr>
<td>외부 주문 기능이 연결됨</td>
<td>즉시 차단하고 설정·코드 검토</td>
</tr>
<tr>
<td>같은 기술 실패 2회 연속</td>
<td>자동 재시도 중단, 원인 기록</td>
</tr>
</tbody>
</table>
<p>중단은 원장 삭제나 가상 포지션 강제 청산을 뜻하지 않는다. 이미 확정한 기록을 보존하고 새 변경을 멈추는 기능이다. 다시 실행할 때는 멈춘 이유가 해소되었는지 확인한다.</p>
<h2 id="repeat-script">5. PowerShell 실행 스크립트의 종료 상태 전달하기</h2>
<p><code>scripts/run_paper.ps1</code>은 경로와 Python 실행 파일을 고정하고 실패 종료 코드를 호출자에게 전달한다. 다음 예시는 대응하는 <code>src.paper_runner</code> 모듈을 만든 뒤 사용할 실행 래퍼다.</p>
<div class="sourceCode" id="cb3"><pre class="sourceCode powershell"><code class="sourceCode powershell"><span id="cb3-1"><a href="#cb3-1" aria-hidden="true" tabindex="-1"></a><span class="va">$ErrorActionPreference</span> <span class="op">=</span> <span class="st">&#39;Stop&#39;</span></span>
<span id="cb3-2"><a href="#cb3-2" aria-hidden="true" tabindex="-1"></a><span class="va">$projectRoot</span> <span class="op">=</span> <span class="fu">Split-Path</span> <span class="op">-</span>Parent <span class="va">$PSScriptRoot</span></span>
<span id="cb3-3"><a href="#cb3-3" aria-hidden="true" tabindex="-1"></a><span class="fu">Set-Location</span> <span class="op">-</span>LiteralPath <span class="va">$projectRoot</span></span>
<span id="cb3-4"><a href="#cb3-4" aria-hidden="true" tabindex="-1"></a><span class="va">$pythonExe</span> <span class="op">=</span> <span class="fu">Join-Path</span> <span class="va">$projectRoot</span> <span class="st">&#39;.venv\Scripts\python.exe&#39;</span></span>
<span id="cb3-5"><a href="#cb3-5" aria-hidden="true" tabindex="-1"></a><span class="va">$stopPath</span> <span class="op">=</span> <span class="fu">Join-Path</span> <span class="va">$projectRoot</span> <span class="st">&#39;config\STOP_PAPER&#39;</span></span>
<span id="cb3-6"><a href="#cb3-6" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-7"><a href="#cb3-7" aria-hidden="true" tabindex="-1"></a><span class="cf">if</span> <span class="op">(</span><span class="fu">Test-Path</span> <span class="op">-</span>LiteralPath <span class="va">$stopPath</span><span class="op">)</span> <span class="op">{</span></span>
<span id="cb3-8"><a href="#cb3-8" aria-hidden="true" tabindex="-1"></a>    <span class="fu">Write-Output</span> <span class="st">&#39;state=BLOCKED reason=STOP_FILE&#39;</span></span>
<span id="cb3-9"><a href="#cb3-9" aria-hidden="true" tabindex="-1"></a>    <span class="cf">exit</span> <span class="dv">20</span></span>
<span id="cb3-10"><a href="#cb3-10" aria-hidden="true" tabindex="-1"></a><span class="op">}</span></span>
<span id="cb3-11"><a href="#cb3-11" aria-hidden="true" tabindex="-1"></a><span class="cf">if</span> <span class="op">(-not</span> <span class="op">(</span><span class="fu">Test-Path</span> <span class="op">-</span>LiteralPath <span class="va">$pythonExe</span><span class="op">))</span> <span class="op">{</span></span>
<span id="cb3-12"><a href="#cb3-12" aria-hidden="true" tabindex="-1"></a>    <span class="cf">throw</span> <span class="st">&#39;프로젝트 가상환경 Python을 찾지 못했습니다.&#39;</span></span>
<span id="cb3-13"><a href="#cb3-13" aria-hidden="true" tabindex="-1"></a><span class="op">}</span></span>
<span id="cb3-14"><a href="#cb3-14" aria-hidden="true" tabindex="-1"></a><span class="op">&amp;</span> <span class="va">$pythonExe</span> <span class="op">-</span>m src<span class="op">.</span><span class="fu">paper_runner</span> <span class="op">--</span>config <span class="st">&#39;config/paper_runtime.json&#39;</span></span>
<span id="cb3-15"><a href="#cb3-15" aria-hidden="true" tabindex="-1"></a><span class="va">$runnerExitCode</span> <span class="op">=</span> <span class="va">$LASTEXITCODE</span></span>
<span id="cb3-16"><a href="#cb3-16" aria-hidden="true" tabindex="-1"></a><span class="cf">exit</span> <span class="va">$runnerExitCode</span></span></code></pre></div>
<p>Python은 파일 로그를 UTF-8로 쓰고 <code>run_id</code>, <code>business_date</code>, <code>stage</code>, <code>state</code>, <code>reason</code>을 포함한다. 한글 로그는 <code>Get-Content -Encoding UTF8</code>으로 읽는다. PowerShell 버전별 기본 인코딩은 다를 수 있으므로 <a href="https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_character_encoding">Microsoft 공식 인코딩 문서</a>를 참고해 명시적으로 맞춘다.</p>
<div class="sourceCode" id="cb4"><pre class="sourceCode powershell"><code class="sourceCode powershell"><span id="cb4-1"><a href="#cb4-1" aria-hidden="true" tabindex="-1"></a><span class="fu">Get-Content</span> <span class="op">-</span>Encoding UTF8 <span class="st">&#39;.\logs\paper_runner.log&#39;</span> <span class="op">-</span>Tail <span class="dv">40</span></span></code></pre></div>
<h2 id="repeat-recovery-tests">6. 실패 지점 세 곳을 재현하기</h2>
<p>자동 반복을 등록하기 전에 가상 데이터로 실패를 주입한다. 실제 계좌나 외부 주문 서버는 필요 없다. 실패 시험은 복사한 테스트 DB에서 수행하고 정상 학습 원장을 지우지 않는다.</p>
<table>
<colgroup>
<col style="width: 33%" />
<col style="width: 33%" />
<col style="width: 33%" />
</colgroup>
<thead>
<tr>
<th>실패 지점</th>
<th>기대 저장 상태</th>
<th>다시 시작할 때</th>
</tr>
</thead>
<tbody>
<tr>
<td>주문 삽입 전</td>
<td>의도 없음</td>
<td>같은 키로 1회 생성</td>
</tr>
<tr>
<td>체결 트랜잭션 중, 확정 전</td>
<td>전체 변경 취소</td>
<td>미체결 주문만 재처리</td>
</tr>
<tr>
<td>DB 확정 후, 성공 로그 전</td>
<td>체결·원장 존재</td>
<td>기존 체결 확인 후 추가 정산 금지</td>
</tr>
</tbody>
</table>
<p>마지막 사례가 중요하다. 로그에 성공이 없다는 이유로 체결을 다시 반영하면 현금과 수량이 두 번 바뀐다. 복구의 기준은 영속 저장소의 주문·체결·원장이다. MySQL의 트랜잭션 경계는 <a href="https://dev.mysql.com/doc/refman/8.4/en/commit.html">공식 COMMIT·ROLLBACK 문서</a>를 참고한다.</p>
<p>DB에 접근할 수 없어 확정 여부를 모르면 <code>RECOVERY_REQUIRED</code>로 남긴다. 확인되지 않은 상태를 “실패했으니 아무 일도 없었다”로 가정하지 않는다. 로컬 시뮬레이터의 복구가 통과해도 실제 외부 주문의 응답 유실 문제가 해결된 것은 아니다.</p>
<h2 id="repeat-recovery-record">7. 복구 기록을 남기고 재개하기</h2>
<p>복구 기록에는 이전 <code>run_id</code>, 원인, 마지막 확정 단계, 대조한 주문·체결 건수, 변경 내용, 새 실행 식별자를 넣는다. 결과를 손으로 고쳤다면 수정 전후 값과 이유를 별도로 보관한다. 가능하면 원장 사건을 근거로 상태를 다시 계산한다.</p>
<pre class="text"><code>recovery_id=demo-recovery-01 previous_run=demo-paper-09
reason=PROCESS_EXIT_AFTER_COMMIT
orders=1 fills=1 cash_entries=1 duplicate_entries=0
cash_difference=0.00 quantity_difference=0
action=KEEP_COMMITTED_FILL next_run=demo-paper-10
decision=RESUME_LOCAL_PAPER</code></pre>
<p>중단 파일은 원인을 고친 뒤 담당자가 제거하는 절차로 둔다. 프로그램이 “몇 분 지났으니 괜찮다”고 자동 삭제하지 않는다. 재개 직전에도 설정 버전, 중단 사유 해소, 원장 대조를 확인한다.</p>
<h2 id="repeat-scheduler">8. 스케줄러는 마지막에 연결하기</h2>
<p>수동 실행과 실패 복구가 통과하면 Windows 작업 스케줄러에 등록한다. 실행 프로그램은 PowerShell, 인수는 프로젝트의 실행 스크립트 절대 경로로 지정하고, 작업 시작 위치도 프로젝트 폴더로 고정한다. 조건은 외부 데이터 확정 시간에 맞추어 정한다.</p>
<p>로그인 여부, 절전·전원 설정, 실행 계정의 폴더 권한이 결과에 영향을 줄 수 있다. 작업이 등록되었다는 사실보다 지정 시각에 실행 기록과 종료 코드가 남는지 확인한다. 명령줄 관리가 필요하면 <a href="https://learn.microsoft.com/en-us/windows/win32/taskschd/schtasks">Microsoft 공식 schtasks 문서</a>를 참고한다.</p>
<p>책의 예시를 읽었다고 자동 등록이 완료되는 것은 아니다. 이번 실습에서는 수동 실행 → 중복 시작 → 중단 파일 → 실패 복구 순서를 확인한 뒤 자신이 정한 관찰 일정에 연결한다.</p>
<h2 id="repeat-done">9. Codex 요청과 완료 기준</h2>
<pre class="text"><code>로컬 paper_runner의 반복 실행과 복구 절차를 구현해줘.
단계별 상태·종료 코드·계정 실행 잠금·영구 고유 의도키를 구분해.
중단 파일·데이터 미확정·잔고 불일치·의도 충돌에서 새 변경을 차단해.
주문 전, 체결 확정 전, 확정 후 로그 전의 실패를 가상 DB에서 재현해.
재시작 후 주문·체결·정산이 한 번씩만 남는지 확인해.
스케줄러 등록은 수동 검증 결과와 등록할 명령을 먼저 보고해.
외부 주문 기능은 추가하지 말고 운영 로그에서 비밀값을 제외해.</code></pre>
<p>완료 기준은 같은 거래일을 두 번 처리해도 주문과 자금이 늘지 않고, 중간 실패 후 마지막 확정 상태에서 재개하며, 중단 조건이 실제로 차단되는 것이다. 로그가 두 번 남았다는 사실과 주문이 두 번 발생했다는 사실도 구분해서 집계한다.</p>
<table>
<colgroup>
<col style="width: 50%" />
<col style="width: 50%" />
</colgroup>
<thead>
<tr>
<th>증상</th>
<th>먼저 확인할 것</th>
</tr>
</thead>
<tbody>
<tr>
<td>스케줄러에서는 Python을 못 찾음</td>
<td>가상환경 절대 경로와 실행 계정 권한</td>
</tr>
<tr>
<td>중단 파일이 있는데 새 주문 생성</td>
<td>실행 전과 변경 직전의 중단 검사</td>
</tr>
<tr>
<td>실패 뒤 예약현금이 계속 남음</td>
<td>주문 상태와 예약 해제의 트랜잭션 경계</td>
</tr>
<tr>
<td>재실행이 기존 체결을 다시 정산</td>
<td>영속 원장 대조와 체결 고유키</td>
</tr>
<tr>
<td>종료 기록이 없는데 작업이 끝남</td>
<td>시작 기록과 비정상 종료 감지</td>
</tr>
</tbody>
</table>
<p>다음 장에서는 페이퍼 성공을 실전 허가로 오해하지 않도록 별도의 차단 점검과 안전 기본값을 정리한다.</p>
</div>
          <section class="pk-related" aria-labelledby="related-heading"><p class="pk-eyebrow">FROM CONCEPT TO PRACTICE</p><h2 id="related-heading">구현 기록과 함께 읽기</h2><ul><li><a href="https://mediamaak.com/books/auto-trading/paper-orders/">페이퍼 주문과 가상 잔고 기록하기</a></li></ul></section>
          <nav class="pk-pager" aria-label="이전 장과 다음 장"><a href="https://mediamaak.com/books/auto-trading/paper-orders/">이전 글: 페이퍼 주문과 가상 잔고 기록하기</a><a href="https://mediamaak.com/books/auto-trading/live-safety/">다음 글: 주문 전 체크리스트와 안전장치</a></nav>
          <div class="pk-share"><div><p class="pk-share-title">학습의 다음 단계를 남겨 두세요</p><p>공유 링크를 복사해 이어서 읽을 수 있습니다.</p></div><div><button type="button" data-share-content>공유 링크 복사</button><span role="status" data-share-status></span></div></div>
        </div>
      </div>
    </article>]]></content:encoded>
    </item>
    <item>
      <title>페이퍼 주문을 시작하기 전 확인할 것</title>
      <link>https://mediamaak.com/books/auto-trading/paper-readiness/</link>
      <guid isPermaLink="true">https://mediamaak.com/books/auto-trading/paper-readiness/</guid>
      <pubDate>Tue, 20 Oct 2026 00:00:00 +0900</pubDate>
      <description>시뮬레이션의 누락·중복·미래 데이터 사용을 점검하고 진행 기준을 정합니다.</description>
      <content:encoded><![CDATA[<article class="pk-article" data-content-id="/books/auto-trading/paper-readiness/" data-content-type="wiki-chapter" data-published-at="2026-10-20T00:00:00+09:00" data-updated-at="2026-10-20T00:00:00+09:00">
      <header class="pk-hero">
        <p class="pk-eyebrow">8 / 12 · AUTO TRADING WIKI</p>
        <h1>페이퍼 주문을 시작하기 전 확인할 것</h1>
        <p class="pk-summary">시뮬레이션의 누락·중복·미래 데이터 사용을 점검하고 진행 기준을 정합니다.</p>
        <div class="pk-meta"><dl><dt>발행</dt><dd><time datetime="2026-10-20T00:00:00+09:00">2026-10-20</time></dd><dt>수정</dt><dd><time datetime="2026-10-20T00:00:00+09:00">2026-10-20</time></dd><dt>문서 검토</dt><dd>2026-10-20</dd><dt>본문 버전</dt><dd>1.1.0-web.20261020</dd></dl></div>
      </header>
      <div class="book-reading-content">
        <div class="pk-body-column">
          <div class="pk-prose"><p>과거 시뮬레이션이 끝나도 바로 반복 실행을 시작하지 않는다. 이번 장에서는 결과가 재현되는지, 데이터 문제가 숨겨져 있지 않은지, 가상 자금과 주문 상태를 기록할 준비가 되었는지 확인한다.</p>
<p>이 책에서 <code>paper</code>는 로컬 프로그램이 가상 주문·체결·잔고를 기록하는 학습 모드다. 한국투자증권의 모의투자 서버를 이용하는 방식과 구분한다. 이번 장에서도 외부 주문 요청을 보내지 않으며, 예시 숫자는 실제 투자 결과가 아니다.</p>
<h2 id="readiness-purpose">1. 성과 판단과 기술적 준비를 구분하기</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>CCI와 T+1 연결이 규칙에 맞는가</td>
<td>작은 사례로 대조</td>
</tr>
<tr>
<td>실행</td>
<td>중단과 재실행이 중복을 만들지 않는가</td>
<td>실패 주입으로 확인</td>
</tr>
<tr>
<td>전략</td>
<td>향후에도 수익을 낼 것인가</td>
<td>이 점검으로 결론 내리지 않음</td>
</tr>
</tbody>
</table>
<p>CCI20의 <code>-100</code> 상향 돌파, T+1 시가 진입, 진입일 포함 5번째 거래 세션 종가 청산이라는 예제 규칙을 유지한다. 준비 점검 중 결과를 좋게 만들려고 기간이나 청산 규칙을 바꾸면 비교 기준이 사라진다.</p>
<h2 id="readiness-evidence">2. 재현 자료를 하나의 묶음으로 만들기</h2>
<p>7장의 결과 폴더와 연결되는 <code>readiness_id</code>를 만든다. 원본 데이터 전체를 보고서에 복사할 필요는 없지만, 어떤 원본을 사용했는지 다시 찾을 수 있어야 한다.</p>
<pre class="text"><code>outputs/readiness/demo-review-01/
  input_manifest.json
  checks.csv
  summary.json
  failed_cases.csv</code></pre>
<p><code>input_manifest.json</code>에 시뮬레이션 <code>run_id</code>, <code>dataset_id</code>, 코드 버전, 설정 해시, 대상 종목의 기준일, 조회 시작·끝 날짜를 적는다. <code>checks.csv</code>에는 점검 항목별 <code>PASS</code>, <code>FAIL</code>, <code>NOT_EVALUATED</code>와 증거 파일 경로를 기록한다.</p>
<p>증거가 없는 항목은 <code>PASS</code>가 아니다. 아직 만들지 않은 복구 시험을 “향후 확인 예정”이라고 적었다면 <code>NOT_EVALUATED</code>로 남기고 다음 단계 허용을 보류한다. 로그가 없거나 파일을 읽을 수 없는 경우도 같은 방식으로 다룬다.</p>
<h2 id="readiness-expand">3. 다섯 종목에서 전체 대상으로 확장하기</h2>
<p>작은 사례가 통과하면 대상 목록을 확장하되 한 번에 계산 규칙까지 바꾸지 않는다. 처음 다섯 종목의 결과가 전체 실행에서도 동일한지 대조하고, 새로 추가된 종목의 가격 누락과 조정 정책을 확인한다.</p>
<p><code>daily_prices</code>에서 다음 쿼리는 키 중복을 찾아낸다. 기본 키가 이미 중복을 막더라도 수집 전 임시 데이터와 수입 과정에도 같은 검사를 적용한다.</p>
<div class="sourceCode" id="cb2"><pre class="sourceCode sql"><code class="sourceCode sql"><span id="cb2-1"><a href="#cb2-1" aria-hidden="true" tabindex="-1"></a><span class="kw">SELECT</span> symbol, trade_date, <span class="fu">COUNT</span>(<span class="op">*</span>) <span class="kw">AS</span> cnt</span>
<span id="cb2-2"><a href="#cb2-2" aria-hidden="true" tabindex="-1"></a><span class="kw">FROM</span> daily_prices</span>
<span id="cb2-3"><a href="#cb2-3" aria-hidden="true" tabindex="-1"></a><span class="kw">GROUP</span> <span class="kw">BY</span> symbol, trade_date</span>
<span id="cb2-4"><a href="#cb2-4" aria-hidden="true" tabindex="-1"></a><span class="kw">HAVING</span> <span class="fu">COUNT</span>(<span class="op">*</span>) <span class="op">&gt;</span> <span class="dv">1</span>;</span>
<span id="cb2-5"><a href="#cb2-5" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb2-6"><a href="#cb2-6" aria-hidden="true" tabindex="-1"></a><span class="kw">SELECT</span> symbol, trade_date, <span class="kw">open</span>, high, low, <span class="kw">close</span>, volume</span>
<span id="cb2-7"><a href="#cb2-7" aria-hidden="true" tabindex="-1"></a><span class="kw">FROM</span> daily_prices</span>
<span id="cb2-8"><a href="#cb2-8" aria-hidden="true" tabindex="-1"></a><span class="kw">WHERE</span> <span class="kw">open</span> <span class="op">&lt;=</span> <span class="dv">0</span> <span class="kw">OR</span> <span class="kw">close</span> <span class="op">&lt;=</span> <span class="dv">0</span> <span class="kw">OR</span> volume <span class="op">&lt;</span> <span class="dv">0</span></span>
<span id="cb2-9"><a href="#cb2-9" aria-hidden="true" tabindex="-1"></a>   <span class="kw">OR</span> high <span class="op">&lt;</span> low <span class="kw">OR</span> high <span class="op">&lt;</span> <span class="fu">GREATEST</span>(<span class="kw">open</span>, <span class="kw">close</span>)</span>
<span id="cb2-10"><a href="#cb2-10" aria-hidden="true" tabindex="-1"></a>   <span class="kw">OR</span> low <span class="op">&gt;</span> <span class="fu">LEAST</span>(<span class="kw">open</span>, <span class="kw">close</span>);</span></code></pre></div>
<p>이 쿼리가 0행이어도 데이터 검증이 끝난 것은 아니다. 아예 빠진 거래일은 행이 없어서 위 조건에 걸리지 않는다. 수집 대상·거래 캘린더·원본 조회 결과를 비교해 누락을 찾는다. 거래정지와 단순 수집 실패도 구분한다.</p>
<p>대상 목록을 “현재 코스피200”으로만 구했다면 과거 구성 종목 전체를 재현한 연구가 아니라 현재 목록에 대한 과거 계산이다. 이 제한을 보고서 첫 부분에 명시하고, 그 결과를 장기 성과 검증으로 표현하지 않는다.</p>
<h2 id="readiness-gates">4. 통과 조건을 점검 전에 작성하기</h2>
<p><code>config/paper_readiness.json</code>의 기준은 결과를 확인하기 전에 정한다. 다음 숫자는 교육용 기능 시험의 기준이며 투자 적합성을 나타내는 수치가 아니다.</p>
<div class="sourceCode" id="cb3"><pre class="sourceCode json"><code class="sourceCode json"><span id="cb3-1"><a href="#cb3-1" aria-hidden="true" tabindex="-1"></a><span class="fu">{</span></span>
<span id="cb3-2"><a href="#cb3-2" aria-hidden="true" tabindex="-1"></a>  <span class="dt">&quot;review_version&quot;</span><span class="fu">:</span> <span class="st">&quot;paper-gate-v1&quot;</span><span class="fu">,</span></span>
<span id="cb3-3"><a href="#cb3-3" aria-hidden="true" tabindex="-1"></a>  <span class="dt">&quot;required_fixture_cases&quot;</span><span class="fu">:</span> <span class="dv">8</span><span class="fu">,</span></span>
<span id="cb3-4"><a href="#cb3-4" aria-hidden="true" tabindex="-1"></a>  <span class="dt">&quot;max_duplicate_keys&quot;</span><span class="fu">:</span> <span class="dv">0</span><span class="fu">,</span></span>
<span id="cb3-5"><a href="#cb3-5" aria-hidden="true" tabindex="-1"></a>  <span class="dt">&quot;max_unexplained_missing_sessions&quot;</span><span class="fu">:</span> <span class="dv">0</span><span class="fu">,</span></span>
<span id="cb3-6"><a href="#cb3-6" aria-hidden="true" tabindex="-1"></a>  <span class="dt">&quot;max_reconciliation_difference&quot;</span><span class="fu">:</span> <span class="st">&quot;0.00&quot;</span><span class="fu">,</span></span>
<span id="cb3-7"><a href="#cb3-7" aria-hidden="true" tabindex="-1"></a>  <span class="dt">&quot;required_repeat_consistency&quot;</span><span class="fu">:</span> <span class="kw">true</span><span class="fu">,</span></span>
<span id="cb3-8"><a href="#cb3-8" aria-hidden="true" tabindex="-1"></a>  <span class="dt">&quot;required_failure_recovery&quot;</span><span class="fu">:</span> <span class="kw">true</span><span class="fu">,</span></span>
<span id="cb3-9"><a href="#cb3-9" aria-hidden="true" tabindex="-1"></a>  <span class="dt">&quot;require_all_checks_evaluated&quot;</span><span class="fu">:</span> <span class="kw">true</span><span class="fu">,</span></span>
<span id="cb3-10"><a href="#cb3-10" aria-hidden="true" tabindex="-1"></a>  <span class="dt">&quot;permit_mode&quot;</span><span class="fu">:</span> <span class="st">&quot;paper&quot;</span></span>
<span id="cb3-11"><a href="#cb3-11" aria-hidden="true" tabindex="-1"></a><span class="fu">}</span></span></code></pre></div>
<p>8개 사례는 상승·하락·비용 후 손실·신호 없음·데이터 부족·중복 신호·재실행·중간 실패로 구성한다. 각 사례의 입력과 기대 상태를 따로 저장한다. 이미 나온 결과를 그대로 기대값으로 복사하는 방식은 오류를 찾지 못한다.</p>
<p>비용 후 손실 사례에는 예를 들어 매수가와 매도가가 모두 10,000인 거래를 넣는다. 비용 전 손익은 0이지만 수수료·가정 세금을 빼면 순손익은 음수여야 한다. “가격이 같으면 항상 보합”이라고 분류하는 오류를 잡을 수 있다.</p>
<h2 id="readiness-summary">5. 요약표에 분모와 제외 사유를 함께 넣기</h2>
<p>다음은 가상 결과다. 숫자의 크기보다 같은 기준으로 집계했는지 살펴본다.</p>
<table>
<thead>
<tr>
<th>항목</th>
<th>예시</th>
<th>해석</th>
</tr>
</thead>
<tbody>
<tr>
<td>전체 신호</td>
<td>21건</td>
<td>모든 처리 결과의 분모</td>
</tr>
<tr>
<td>완료</td>
<td>15건</td>
<td>진입·청산 가격이 모두 존재</td>
</tr>
<tr>
<td>보유 중 제외</td>
<td>2건</td>
<td>중복 진입 금지 규칙 적용</td>
</tr>
<tr>
<td>기간 끝 미완료</td>
<td>4건</td>
<td>청산 가격 미확정</td>
</tr>
<tr>
<td>원인 불명 누락</td>
<td>0건</td>
<td>있다면 차단</td>
</tr>
<tr>
<td>중복 결과</td>
<td>0건</td>
<td>있다면 차단</td>
</tr>
<tr>
<td>원장 차이</td>
<td>0.00</td>
<td>현금·수량·체결 대조</td>
</tr>
</tbody>
</table>
<p>평균 손익과 승률에는 완료 건수라는 분모를 표시한다. 여러 종목의 거래 수익률 평균을 전체 자산 수익률로 쓰지 않는다. 최대 낙폭을 계산하려면 같은 시점별 현금, 보유 수량, 평가 가격을 사용한 자산 곡선이 별도로 필요하다.</p>
<p>현재 시뮬레이션이 종목별 독립 계산이라면 “동시에 모든 신호를 살 자금이 있었다”는 가정을 내포할 수 있다. 다음 장에서는 초기 가상 현금, 주문당 한도, 최대 보유 수, 현금 예약을 넣어 이 차이를 드러낸다.</p>
<h2 id="readiness-sensitivity">6. 비용과 기간에 대한 민감도 확인하기</h2>
<p>기준 실행을 보관한 채 비용만 바꾼 별도 실행을 만든다. 예를 들어 슬리피지 가정이 커질수록 순손익이 어떻게 달라지는지 확인한다. 요율은 소수로 읽는지 bp로 읽는지 함께 적는다. <code>5bp = 0.0005</code>이므로 <code>0.05</code>와 혼동하면 비용이 100배가 된다.</p>
<p>기간도 학습 구간과 검토 구간을 시간순으로 나눈다. 검토 구간 결과를 본 뒤 매번 규칙을 고쳐 다시 검토하면 그 구간도 개발에 사용한 셈이다. 어떤 기간을 언제 보았는지 변경 기록에 남긴다.</p>
<p>일봉에서 장중 손절·익절 순서를 알 수 없다는 제한은 그대로 유지된다. 민감도 표에 가정만 추가했다고 체결 품질이 입증되지는 않는다. 고정 청산 예제의 범위와 데이터 해상도를 보고서에 함께 쓴다.</p>
<h2 id="readiness-decision">7. 준비 판정을 기계가 읽을 수 있게 남기기</h2>
<p>단순히 “통과”라는 파일이 존재하는지만 검사하면 예전 결과가 새 설정에도 사용될 수 있다. 판정에는 입력의 버전과 허용 범위를 함께 넣고, 이후 실행이 달라지면 무효로 취급한다.</p>
<div class="sourceCode" id="cb4"><pre class="sourceCode json"><code class="sourceCode json"><span id="cb4-1"><a href="#cb4-1" aria-hidden="true" tabindex="-1"></a><span class="fu">{</span></span>
<span id="cb4-2"><a href="#cb4-2" aria-hidden="true" tabindex="-1"></a>  <span class="dt">&quot;readiness_id&quot;</span><span class="fu">:</span> <span class="st">&quot;demo-review-01&quot;</span><span class="fu">,</span></span>
<span id="cb4-3"><a href="#cb4-3" aria-hidden="true" tabindex="-1"></a>  <span class="dt">&quot;decision&quot;</span><span class="fu">:</span> <span class="st">&quot;READY_FOR_LOCAL_PAPER&quot;</span><span class="fu">,</span></span>
<span id="cb4-4"><a href="#cb4-4" aria-hidden="true" tabindex="-1"></a>  <span class="dt">&quot;strategy_version&quot;</span><span class="fu">:</span> <span class="st">&quot;cci20_cross_up_m100_v1&quot;</span><span class="fu">,</span></span>
<span id="cb4-5"><a href="#cb4-5" aria-hidden="true" tabindex="-1"></a>  <span class="dt">&quot;dataset_id&quot;</span><span class="fu">:</span> <span class="st">&quot;fixture-v1&quot;</span><span class="fu">,</span></span>
<span id="cb4-6"><a href="#cb4-6" aria-hidden="true" tabindex="-1"></a>  <span class="dt">&quot;config_hash&quot;</span><span class="fu">:</span> <span class="st">&quot;EXAMPLE_ONLY_REPLACE_WITH_COMPUTED_HASH&quot;</span><span class="fu">,</span></span>
<span id="cb4-7"><a href="#cb4-7" aria-hidden="true" tabindex="-1"></a>  <span class="dt">&quot;checks_passed&quot;</span><span class="fu">:</span> <span class="dv">8</span><span class="fu">,</span></span>
<span id="cb4-8"><a href="#cb4-8" aria-hidden="true" tabindex="-1"></a>  <span class="dt">&quot;checks_failed&quot;</span><span class="fu">:</span> <span class="dv">0</span><span class="fu">,</span></span>
<span id="cb4-9"><a href="#cb4-9" aria-hidden="true" tabindex="-1"></a>  <span class="dt">&quot;not_evaluated&quot;</span><span class="fu">:</span> <span class="dv">0</span><span class="fu">,</span></span>
<span id="cb4-10"><a href="#cb4-10" aria-hidden="true" tabindex="-1"></a>  <span class="dt">&quot;scope&quot;</span><span class="fu">:</span> <span class="st">&quot;local_paper_only&quot;</span><span class="fu">,</span></span>
<span id="cb4-11"><a href="#cb4-11" aria-hidden="true" tabindex="-1"></a>  <span class="dt">&quot;external_orders_permitted&quot;</span><span class="fu">:</span> <span class="kw">false</span></span>
<span id="cb4-12"><a href="#cb4-12" aria-hidden="true" tabindex="-1"></a><span class="fu">}</span></span></code></pre></div>
<p>이 예시 파일을 복사하는 것만으로 점검이 통과되지는 않는다. 실제 프로그램은 자신의 입력에서 해시를 계산하고 시험 결과로 판정을 생성해야 한다. 해시 계산의 기본 API는 <a href="https://docs.python.org/3/library/hashlib.html">Python 공식 hashlib 문서</a>를 참고한다.</p>
<h2 id="readiness-codex">8. Codex에 검토 범위를 분명하게 전달하기</h2>
<pre class="text"><code>7장의 시뮬레이션을 로컬 paper 실행으로 확장할 준비가 되었는지 검토해줘.
소스·설정·데이터 식별자를 연결한 checks.csv와 summary.json을 만들어줘.
8개 가상 사례의 기대값을 먼저 정하고 날짜·비용·중복·중간 실패를 확인해.
종목 전체 확장에서는 처음 5개 결과의 일관성과 누락 사유를 대조해.
성과 지표와 기술 통과 조건을 구분하고 미검사 항목을 PASS로 처리하지 마.
파일 이름이나 실행 옵션은 현재 프로젝트에 맞게 제안하고 근거를 적어줘.
투자 적합성 판단, 실전 주문, 증권사 모의주문 요청은 수행하지 마.</code></pre>
<p>검토 결과가 <code>BLOCKED</code>라면 모든 파일을 다시 만들지 않는다. 실패 항목과 관련된 가장 작은 입력을 재현하고, 수정 후 그 항목과 연관 검사를 다시 실행한다. 수정 전 결과도 남겨 원인을 비교한다.</p>
<h2 id="readiness-done">9. 완료 기준과 문제 해결</h2>
<p>이번 장을 마쳤다는 뜻은 준비 보고서가 존재하고 모든 기술 조건의 증거를 찾을 수 있다는 것이다. 보고서가 막힌 상태라면 다음 장을 읽을 수는 있지만 반복 실행을 허용하지 않는다. 기술 점검의 통과는 실전 거래 허가가 아니다.</p>
<table>
<colgroup>
<col style="width: 50%" />
<col style="width: 50%" />
</colgroup>
<thead>
<tr>
<th>증상</th>
<th>확인할 내용</th>
</tr>
</thead>
<tbody>
<tr>
<td>승률만 있고 완료 건수가 없음</td>
<td>집계의 분모와 미완료 제외 방식</td>
</tr>
<tr>
<td>전체 확장 후 기존 결과가 달라짐</td>
<td>대상별 계산 경계, 정렬, 데이터 버전</td>
</tr>
<tr>
<td>설정 변경 후에도 이전 통과 사용</td>
<td>설정 해시 대조와 판정 무효화</td>
</tr>
<tr>
<td>비용을 높여도 순손익이 그대로임</td>
<td>비용 계산이 보고서에 반영되는지</td>
</tr>
<tr>
<td>미검사 항목이 통과로 표시됨</td>
<td>기본 상태를 <code>NOT_EVALUATED</code>로 두었는지</td>
</tr>
</tbody>
</table>
<p>한국투자증권의 모의투자나 실전 API 사용 조건은 이 로컬 준비 보고서의 범위 밖이다. 해당 환경을 별도로 검토할 때는 <a href="https://apiportal.koreainvestment.com/intro">KIS Developers 공식 안내</a>의 API 문서와 현재 공지를 확인한다. 다음 장에서는 외부 주문 연결 없이 가상 주문 원장을 만든다.</p>
</div>
          <section class="pk-related" aria-labelledby="related-heading"><p class="pk-eyebrow">FROM CONCEPT TO PRACTICE</p><h2 id="related-heading">구현 기록과 함께 읽기</h2><ul><li><a href="https://mediamaak.com/books/auto-trading/simulation/">CCI 신호를 과거 데이터로 시뮬레이션하기</a></li></ul></section>
          <nav class="pk-pager" aria-label="이전 장과 다음 장"><a href="https://mediamaak.com/books/auto-trading/simulation/">이전 글: CCI 신호를 과거 데이터로 시뮬레이션하기</a><a href="https://mediamaak.com/books/auto-trading/paper-orders/">다음 글: 페이퍼 주문과 가상 잔고 기록하기</a></nav>
          <div class="pk-share"><div><p class="pk-share-title">학습의 다음 단계를 남겨 두세요</p><p>공유 링크를 복사해 이어서 읽을 수 있습니다.</p></div><div><button type="button" data-share-content>공유 링크 복사</button><span role="status" data-share-status></span></div></div>
        </div>
      </div>
    </article>]]></content:encoded>
    </item>
    <item>
      <title>페이퍼 주문과 가상 잔고 기록하기</title>
      <link>https://mediamaak.com/books/auto-trading/paper-orders/</link>
      <guid isPermaLink="true">https://mediamaak.com/books/auto-trading/paper-orders/</guid>
      <pubDate>Tue, 20 Oct 2026 00:00:00 +0900</pubDate>
      <description>실제 주문 전송 없이 주문·잔고·실행 결과를 가상으로 기록합니다.</description>
      <content:encoded><![CDATA[<article class="pk-article" data-content-id="/books/auto-trading/paper-orders/" data-content-type="wiki-chapter" data-published-at="2026-10-20T00:00:00+09:00" data-updated-at="2026-10-20T10:08:27+09:00">
      <header class="pk-hero">
        <p class="pk-eyebrow">9 / 12 · AUTO TRADING WIKI</p>
        <h1>페이퍼 주문과 가상 잔고 기록하기</h1>
        <p class="pk-summary">실제 주문 전송 없이 주문·잔고·실행 결과를 가상으로 기록합니다.</p>
        <div class="pk-meta"><dl><dt>발행</dt><dd><time datetime="2026-10-20T00:00:00+09:00">2026-10-20</time></dd><dt>수정</dt><dd><time datetime="2026-10-20T10:08:27+09:00">2026-10-20</time></dd><dt>문서 검토</dt><dd>2026-10-20</dd><dt>본문 버전</dt><dd>1.1.1-web.20261020</dd></dl></div>
      </header>
      <div class="book-reading-content">
        <div class="pk-body-column">
          <div class="pk-prose"><p>이번 장에서는 신호를 가상 주문으로 바꾸고, 가상 체결이 현금과 보유 수량에 미치는 영향을 기록한다. 시뮬레이션의 거래별 손익을 넘어 하나의 계정에서 자금이 실제로 모자랄 수 있다는 조건을 다룬다.</p>
<p>여기서 <code>paper</code>는 로컬 원장 시뮬레이션이다. 한국투자증권의 모의투자 서버에 접속하지 않으며 실제 계좌·주문 번호를 사용하지 않는다. 모든 코드와 금액은 <code>auto-trading-lab</code>의 학습 예시다.</p>
<h2 id="paper-events">1. 신호·주문·체결을 다른 사건으로 다루기</h2>
<p>신호는 조건이 만족되었다는 계산 결과다. 주문은 특정 수량을 특정 조건으로 처리해 달라는 의도다. 체결은 그 주문이 가격과 수량을 가지고 실행되었다는 결과다. 이 셋을 같은 테이블의 한 행으로 처리하면 미체결과 실패를 구분하기 어려워진다.</p>
<p>예를 들어 T일 CCI20이 <code>-100</code>을 상향 돌파해도 T일에 바로 가상 매수하지 않는다. T일 종가 확정 후 다음 거래 세션의 진입 의도를 만들고, T+1의 가상 <code>OPEN</code> 이벤트에서만 체결을 계산한다. 주문을 만들었다고 보유 수량부터 늘리지 않는다.</p>
<table>
<colgroup>
<col style="width: 33%" />
<col style="width: 33%" />
<col style="width: 33%" />
</colgroup>
<thead>
<tr>
<th>객체</th>
<th>예제 상태</th>
<th>잔고에 미치는 영향</th>
</tr>
</thead>
<tbody>
<tr>
<td>신호</td>
<td><code>DETECTED</code>, <code>SKIPPED</code></td>
<td>없음</td>
</tr>
<tr>
<td>주문</td>
<td><code>PENDING</code>, <code>FILLED</code>, <code>CANCELED</code>, <code>REJECTED</code></td>
<td>미체결 매수는 현금 예약</td>
</tr>
<tr>
<td>체결</td>
<td>가격·수량이 확정된 불변 기록</td>
<td>현금·수량을 변경</td>
</tr>
<tr>
<td>포지션</td>
<td>수량·평균 취득원가</td>
<td>체결 누적으로 계산</td>
</tr>
</tbody>
</table>
<p>이번 구현은 전량 체결 또는 전량 미체결의 단순 모델이다. 부분 체결, 정정·취소의 경합, 실제 주문 접수와 거래소 체결 차이는 재현하지 않는다. 보고서에 <code>partial_fill_supported = false</code>를 기록한다.</p>
<h2 id="paper-config">2. 가상 계정과 한도를 설정하기</h2>
<p>실제 계좌번호 대신 <code>PAPER_DEMO_01</code>을 사용한다. <code>config/paper.json</code>을 예제로 만들되, 실전 API 키가 들어 있는 설정을 함께 읽지 않도록 분리한다.</p>
<div class="sourceCode" id="cb1"><pre class="sourceCode json"><code class="sourceCode json"><span id="cb1-1"><a href="#cb1-1" aria-hidden="true" tabindex="-1"></a><span class="fu">{</span></span>
<span id="cb1-2"><a href="#cb1-2" aria-hidden="true" tabindex="-1"></a>  <span class="dt">&quot;mode&quot;</span><span class="fu">:</span> <span class="st">&quot;paper&quot;</span><span class="fu">,</span></span>
<span id="cb1-3"><a href="#cb1-3" aria-hidden="true" tabindex="-1"></a>  <span class="dt">&quot;account_id&quot;</span><span class="fu">:</span> <span class="st">&quot;PAPER_DEMO_01&quot;</span><span class="fu">,</span></span>
<span id="cb1-4"><a href="#cb1-4" aria-hidden="true" tabindex="-1"></a>  <span class="dt">&quot;initial_cash&quot;</span><span class="fu">:</span> <span class="st">&quot;1000000.00&quot;</span><span class="fu">,</span></span>
<span id="cb1-5"><a href="#cb1-5" aria-hidden="true" tabindex="-1"></a>  <span class="dt">&quot;max_order_notional&quot;</span><span class="fu">:</span> <span class="st">&quot;100000.00&quot;</span><span class="fu">,</span></span>
<span id="cb1-6"><a href="#cb1-6" aria-hidden="true" tabindex="-1"></a>  <span class="dt">&quot;max_open_positions&quot;</span><span class="fu">:</span> <span class="dv">3</span><span class="fu">,</span></span>
<span id="cb1-7"><a href="#cb1-7" aria-hidden="true" tabindex="-1"></a>  <span class="dt">&quot;same_symbol_overlap&quot;</span><span class="fu">:</span> <span class="st">&quot;skip&quot;</span><span class="fu">,</span></span>
<span id="cb1-8"><a href="#cb1-8" aria-hidden="true" tabindex="-1"></a>  <span class="dt">&quot;allow_short&quot;</span><span class="fu">:</span> <span class="kw">false</span><span class="fu">,</span></span>
<span id="cb1-9"><a href="#cb1-9" aria-hidden="true" tabindex="-1"></a>  <span class="dt">&quot;allow_leverage&quot;</span><span class="fu">:</span> <span class="kw">false</span><span class="fu">,</span></span>
<span id="cb1-10"><a href="#cb1-10" aria-hidden="true" tabindex="-1"></a>  <span class="dt">&quot;fill_model&quot;</span><span class="fu">:</span> <span class="st">&quot;fixture_next_open_and_fifth_close&quot;</span><span class="fu">,</span></span>
<span id="cb1-11"><a href="#cb1-11" aria-hidden="true" tabindex="-1"></a>  <span class="dt">&quot;external_order_transport&quot;</span><span class="fu">:</span> <span class="st">&quot;disabled&quot;</span><span class="fu">,</span></span>
<span id="cb1-12"><a href="#cb1-12" aria-hidden="true" tabindex="-1"></a>  <span class="dt">&quot;strategy_version&quot;</span><span class="fu">:</span> <span class="st">&quot;cci20_cross_up_m100_v1&quot;</span></span>
<span id="cb1-13"><a href="#cb1-13" aria-hidden="true" tabindex="-1"></a><span class="fu">}</span></span></code></pre></div>
<p>매수 수량은 거래 전 사용 가능 현금, 주문당 한도, 수수료와 가격 가정을 함께 고려해 정수로 계산한다. <code>initial_cash</code>는 실행마다 다시 지급하는 돈이 아니다. 계정을 최초 생성할 때 한 번 기록하고, 재시작은 저장된 현금과 원장을 읽는다.</p>
<p>7장의 <code>same_symbol_overlap = skip</code>을 유지한다. 같은 의도키의 재시도라면 먼저 기존 주문과 내용을 대조하고 새 주문을 만들지 않는다. 새 진입 의도는 계정 행 잠금 안에서 검사하며, 보유 수량이 양수이거나 <code>PENDING</code> 매수 주문이 있는 종목은 <code>SKIPPED/OVERLAPPING_POSITION</code>으로 제외한다.</p>
<p><code>max_open_positions</code>는 보유 종목과 <code>PENDING</code> 매수 종목의 합집합에 속한 고유 종목 수로 계산한다. 새 종목을 더했을 때 한도를 넘으면 의도를 생성하지 않는다. 취소·거절된 미체결 매수는 현금 예약과 미체결 슬롯을 해제하며, 보유 수량은 매도 체결 전까지 보유 슬롯에 남긴다. 이 검사와 주문 생성·예약은 같은 계정 잠금과 트랜잭션 안에서 처리한다.</p>
<p>동시에 여러 신호가 있으면 처리 순서도 고정한다. 예를 들어 <code>(signal_date, symbol)</code> 순서로 처리하면 재현하기 쉽지만 특정 종목 우선 효과가 생긴다. 이 정책을 문서에 남기고, 전략 성과로 오해하지 않는다.</p>
<h2 id="paper-ledger">3. 원장 구조와 고유키 정하기</h2>
<p>최소 저장 구조는 <code>paper_accounts</code>, <code>paper_orders</code>, <code>paper_fills</code>, <code>paper_cash_ledger</code>, <code>paper_positions</code>다. 각 기록은 <code>account_id</code>, <code>run_id</code>, <code>strategy_version</code>으로 연결한다. 금액은 SQL의 <code>DECIMAL</code>, 수량은 허용하는 단위에 맞는 타입으로 저장한다.</p>
<div class="sourceCode" id="cb2"><pre class="sourceCode sql"><code class="sourceCode sql"><span id="cb2-1"><a href="#cb2-1" aria-hidden="true" tabindex="-1"></a><span class="kw">CREATE</span> <span class="kw">TABLE</span> paper_orders (</span>
<span id="cb2-2"><a href="#cb2-2" aria-hidden="true" tabindex="-1"></a>  order_id <span class="dt">CHAR</span>(<span class="dv">36</span>) <span class="kw">PRIMARY</span> <span class="kw">KEY</span>,</span>
<span id="cb2-3"><a href="#cb2-3" aria-hidden="true" tabindex="-1"></a>  account_id <span class="dt">VARCHAR</span>(<span class="dv">40</span>) <span class="kw">NOT</span> <span class="kw">NULL</span>,</span>
<span id="cb2-4"><a href="#cb2-4" aria-hidden="true" tabindex="-1"></a>  run_id <span class="dt">VARCHAR</span>(<span class="dv">80</span>) <span class="kw">NOT</span> <span class="kw">NULL</span>,</span>
<span id="cb2-5"><a href="#cb2-5" aria-hidden="true" tabindex="-1"></a>  strategy_version <span class="dt">VARCHAR</span>(<span class="dv">40</span>) <span class="kw">NOT</span> <span class="kw">NULL</span>,</span>
<span id="cb2-6"><a href="#cb2-6" aria-hidden="true" tabindex="-1"></a>  client_order_key <span class="dt">VARCHAR</span>(<span class="dv">180</span>) <span class="kw">NOT</span> <span class="kw">NULL</span>,</span>
<span id="cb2-7"><a href="#cb2-7" aria-hidden="true" tabindex="-1"></a>  symbol <span class="dt">VARCHAR</span>(<span class="dv">20</span>) <span class="kw">NOT</span> <span class="kw">NULL</span>,</span>
<span id="cb2-8"><a href="#cb2-8" aria-hidden="true" tabindex="-1"></a>  side <span class="dt">VARCHAR</span>(<span class="dv">4</span>) <span class="kw">NOT</span> <span class="kw">NULL</span>,</span>
<span id="cb2-9"><a href="#cb2-9" aria-hidden="true" tabindex="-1"></a>  quantity <span class="dt">INT</span> <span class="kw">NOT</span> <span class="kw">NULL</span>,</span>
<span id="cb2-10"><a href="#cb2-10" aria-hidden="true" tabindex="-1"></a>  scheduled_session <span class="dt">DATE</span> <span class="kw">NOT</span> <span class="kw">NULL</span>,</span>
<span id="cb2-11"><a href="#cb2-11" aria-hidden="true" tabindex="-1"></a>  scheduled_event <span class="dt">VARCHAR</span>(<span class="dv">8</span>) <span class="kw">NOT</span> <span class="kw">NULL</span>,</span>
<span id="cb2-12"><a href="#cb2-12" aria-hidden="true" tabindex="-1"></a>  status <span class="dt">VARCHAR</span>(<span class="dv">20</span>) <span class="kw">NOT</span> <span class="kw">NULL</span>,</span>
<span id="cb2-13"><a href="#cb2-13" aria-hidden="true" tabindex="-1"></a>  reserved_cash <span class="dt">DECIMAL</span>(<span class="dv">18</span>,<span class="dv">2</span>) <span class="kw">NOT</span> <span class="kw">NULL</span> <span class="kw">DEFAULT</span> <span class="dv">0</span>,</span>
<span id="cb2-14"><a href="#cb2-14" aria-hidden="true" tabindex="-1"></a>  <span class="kw">UNIQUE</span> <span class="kw">KEY</span> uq_paper_intent (account_id, client_order_key)</span>
<span id="cb2-15"><a href="#cb2-15" aria-hidden="true" tabindex="-1"></a>) ENGINE<span class="op">=</span>InnoDB;</span></code></pre></div>
<p>교육용 키 예시는 <code>cci20_cross_up_m100_v1|DEMO_A|2026-06-02|ENTRY</code>다. 같은 실행을 재시도해도 이 의도 키는 바꾸지 않는다. 실행을 구별하는 <code>run_id</code>만 고유키로 삼으면 재시도할 때 같은 주문이 새 주문으로 생길 수 있다.</p>
<p><code>paper_orders.run_id</code>는 주문을 최초 생성한 실행을, <code>strategy_version</code>은 그때의 전략 버전을 나타낸다. 재시도·복구에서는 이 최초 생성 정보를 덮어쓰지 않는다. 이후 실행의 처리 시도는 기존 <code>order_id</code>를 참조하는 <code>paper_runs</code>와 실행 로그에 별도로 남긴다. 의도의 고유 제약은 <code>(account_id, client_order_key)</code>로 유지하며 <code>run_id</code>를 포함하지 않는다.</p>
<p>체결 테이블에는 <code>(order_id, fill_sequence)</code> 고유 제약을, 현금 원장에는 체결별 정산 사건의 고유 제약을 둔다. 종료 주문은 원래 진입 포지션 식별자와 연결해, 매일 “보유 중”이라는 이유로 청산 주문이 여러 개 생기지 않도록 한다.</p>
<h2 id="paper-reservation">4. 주문을 만들 때 현금을 예약하기</h2>
<p>예제에서 가상 현금이 1,000,000이고 10,000원 10주 매수와 수수료 50을 예상한다면 100,050을 예약한다. 총현금은 아직 1,000,000이지만 사용 가능 현금은 899,950이 된다. 다른 주문은 예약된 금액을 쓸 수 없다.</p>
<p>T일에는 T+1 시가를 알 수 없으므로 실제 구현의 예약액은 사전에 정한 가격 상한과 비용 여유를 이용해야 한다. T+1 가격으로 필요한 금액이 예약 한도를 넘으면 이번 단순 모델은 <code>REJECTED/RESERVATION_LIMIT</code>으로 처리하고 예약을 해제한다. 미래 시가를 T일에 미리 읽어 수량을 유리하게 정하지 않는다.</p>
<p>DB에서는 사용 가능 현금을 조회하고 주문을 삽입하고 예약을 갱신하는 과정을 한 트랜잭션으로 묶는다. 동시 실행 가능성이 있다면 계정 행을 <code>SELECT ... FOR UPDATE</code>로 잠그고 한도를 다시 확인한다. InnoDB 잠금 읽기의 동작은 <a href="https://dev.mysql.com/doc/refman/8.4/en/innodb-locking-reads.html">MySQL 공식 문서</a>를 참고한다.</p>
<p>고유키 충돌은 아무 오류나 무시하는 방식으로 처리하지 않는다. 기존 주문을 읽어 종목·방향·수량·예정 세션이 같은지 비교한다. 내용이 다르면 <code>INTENT_CONFLICT</code>로 중단한다. 같은 키에 다른 수량이 들어온 상황을 정상 재시도로 숨기지 않는다.</p>
<h2 id="paper-atomic-fill">5. 체결과 잔고 변경을 함께 확정하기</h2>
<p>가상 <code>OPEN</code> 이벤트에서 주문과 계정을 잠그고 상태가 <code>PENDING</code>인지 확인한다. 가격·거래량·예정 세션 조건이 맞으면 체결, 현금 차감, 수량 증가, 예약 해제, 주문 상태 변경을 하나의 트랜잭션으로 확정한다. 한 단계라도 실패하면 전체를 되돌린다.</p>
<pre class="text"><code>BEGIN
  계정·주문 행 잠금
  기존 체결과 주문 상태 확인
  예정 세션·가격·수량·예약 한도 확인
  paper_fills에 가상 체결 삽입
  paper_cash_ledger에 현금 차감 삽입
  계정 현금과 paper_positions 갱신
  예약 해제 및 주문을 FILLED로 변경
COMMIT</code></pre>
<p>실제 코드는 SQL 값을 문자열로 조합하지 않고 DB 드라이버의 파라미터 바인딩을 사용한다. 트랜잭션의 확정과 취소는 <a href="https://dev.mysql.com/doc/refman/8.4/en/commit.html">MySQL 공식 COMMIT·ROLLBACK 문서</a>를 기준으로 구현한다. 로그 파일 한 줄이 성공했다는 이유만으로 DB 확정을 대신 판단하지 않는다.</p>
<p>청산도 같은 원칙이다. 진입 세션을 포함해 5번째 세션의 가상 <code>CLOSE</code>에서 보유 수량을 넘지 않는 매도만 허용한다. 매수 예약·매수 체결·매도 체결의 사건이 연결되어야 현금과 수량을 복원할 수 있다.</p>
<h2 id="paper-reconcile">6. 가상 원장을 손으로 대조하기</h2>
<p>7장의 가상 거래와 같은 가격·비용을 사용하면 다음 결과가 나와야 한다. 초기 예제와 대조할 때만 매수 가격 10,000, 매도 가격 10,200, 수량 10, 슬리피지 0으로 고정한다.</p>
<table>
<thead>
<tr>
<th>시점</th>
<th>총현금</th>
<th>예약현금</th>
<th>보유 수량</th>
</tr>
</thead>
<tbody>
<tr>
<td>초기</td>
<td>1,000,000</td>
<td>0</td>
<td>0</td>
</tr>
<tr>
<td>매수 의도 생성</td>
<td>1,000,000</td>
<td>100,050</td>
<td>0</td>
</tr>
<tr>
<td>매수 체결</td>
<td>899,950</td>
<td>0</td>
<td>10</td>
</tr>
<tr>
<td>매도 체결</td>
<td>1,001,695</td>
<td>0</td>
<td>0</td>
</tr>
</tbody>
</table>
<p>매도 정산 유입은 102,000에서 가정 수수료 51과 가정 세금 204를 뺀 101,745다. 이 숫자가 현금 원장과 계정 현금에 모두 반영되어야 한다. 보유 중 자산 평가는 평가 가격과 평가 시각도 함께 표시하고 실현손익과 구분한다.</p>
<p>대조식은 <code>현재 현금 = 최초 입금 + 현금 원장 증감 합계</code>와 <code>현재 수량 = 매수 체결 수량 - 매도 체결 수량</code>이다. 반올림 정책까지 동일하게 적용했는데도 차이가 있으면 다음 실행을 막는다.</p>
<h2 id="paper-replay">7. 과거 재생과 앞으로의 관찰을 구분하기</h2>
<p>일봉만으로 학습할 때는 가상 시계를 두고 과거의 <code>CLOSE → 다음 OPEN → CLOSE</code> 이벤트를 순서대로 재생한다. T+1 시가를 이미 갖고 있어도 T일 의도 생성 함수에는 전달하지 않는다. 전체 데이터가 메모리에 있다는 이유로 미래 열을 읽지 않도록 입력 경계를 나눈다.</p>
<p>앞으로의 데이터를 매일 관찰하는 방식이라면 아직 오지 않은 시가나 종가를 체결로 기록할 수 없다. 일봉이 확정된 뒤 뒤늦게 체결을 재구성한다면 <code>replayed_after_close</code>와 원본 가격 확정 시각을 표시한다. 이는 실시간 시장에서 그 가격에 주문이 체결되었다는 뜻이 아니다.</p>
<p>장중 고가·저가를 이용한 손절·익절은 이 모델에 추가하지 않는다. 한 일봉에서 두 조건이 모두 충족될 때 선후관계를 알 수 없기 때문이다. 원장의 정확성과 시장 체결의 현실성은 별도 문제로 평가한다.</p>
<h2 id="paper-codex">8. Codex에 작은 원장 시험을 요청하기</h2>
<pre class="text"><code>외부 네트워크 주문 기능이 없는 로컬 paper 원장을 구현해줘.
config/paper.json을 읽고 계정 초기 입금은 한 번만 기록해.
T일 종가 신호로 PENDING 의도를 만들고 T+1 OPEN 이벤트에서만 가상 체결해.
진입일 포함 5번째 CLOSE 청산을 포지션 식별자와 연결해.
계정 잠금·고유 의도키·체결키·현금 예약·원자적 정산을 적용해.
같은 종목 보유·PENDING 매수의 중복 진입을 막고 보유·미체결 종목의 합집합으로 보유 한도를 검사해.
주문의 최초 run_id·strategy_version은 보존하고 재시도는 기존 order_id와 새 실행 로그로 연결해.
실제 계좌번호와 API 키를 읽지 말고 가상 fixture만 사용해.
매수 후 현금 899950, 청산 후 1001695가 되는 예제를 확인해.
같은 이벤트 재생·현금 부족·가격 누락·트랜잭션 중단도 확인하고 보고해.</code></pre>
<p>정상 로그는 <code>intent_created</code>, <code>fill_committed</code>, <code>reconciliation_passed</code>처럼 사건을 구분한다. 가상 체결 번호에는 <code>PAPER-</code> 접두사를 넣어 실제 주문 번호와 혼동하지 않게 한다. 민감한 설정 전체를 로그로 출력하지 않는다.</p>
<h2 id="paper-done">9. 완료 기준과 문제 해결</h2>
<p>완료 기준은 주문과 체결이 구분되고, 재실행해도 초기 자금·주문·체결이 늘지 않으며, 현금·수량 대조 차이가 0인 것이다. 가격이 없는 이벤트는 미체결로 남거나 사유와 함께 종료되어야 하며, 임의의 가격으로 채우지 않는다.</p>
<table>
<thead>
<tr>
<th>증상</th>
<th>먼저 확인할 것</th>
</tr>
</thead>
<tbody>
<tr>
<td>재시작마다 현금이 늘어남</td>
<td>초기 입금 사건의 고유키</td>
</tr>
<tr>
<td>주문만 만들었는데 수량이 증가</td>
<td>신호·주문·체결의 갱신 경계</td>
</tr>
<tr>
<td>현금 부족인데 여러 주문이 생성</td>
<td>계정 잠금과 예약액 대조</td>
</tr>
<tr>
<td>같은 체결이 두 번 반영</td>
<td>체결키와 정산 사건 고유키</td>
</tr>
<tr>
<td>가격 누락인데 FILLED 상태</td>
<td>가상 체결 전 데이터 검증</td>
</tr>
</tbody>
</table>
<p>다음 장에서는 이 원장을 하루씩 반복 실행하고, 중간 실패 후 어디까지 확정되었는지 확인하며 복구하는 절차를 만든다.</p>
</div>
          <section class="pk-related" aria-labelledby="related-heading"><p class="pk-eyebrow">FROM CONCEPT TO PRACTICE</p><h2 id="related-heading">구현 기록과 함께 읽기</h2><ul><li><a href="https://mediamaak.com/books/auto-trading/paper-readiness/">페이퍼 주문을 시작하기 전 확인할 것</a></li></ul></section>
          <nav class="pk-pager" aria-label="이전 장과 다음 장"><a href="https://mediamaak.com/books/auto-trading/paper-readiness/">이전 글: 페이퍼 주문을 시작하기 전 확인할 것</a><a href="https://mediamaak.com/books/auto-trading/repeat-stop/">다음 글: 반복 실행과 중단 조건 만들기</a></nav>
          <div class="pk-share"><div><p class="pk-share-title">학습의 다음 단계를 남겨 두세요</p><p>공유 링크를 복사해 이어서 읽을 수 있습니다.</p></div><div><button type="button" data-share-content>공유 링크 복사</button><span role="status" data-share-status></span></div></div>
        </div>
      </div>
    </article>]]></content:encoded>
    </item>
    <item>
      <title>운영 기록을 남기고 개선하기</title>
      <link>https://mediamaak.com/books/auto-trading/operations-review/</link>
      <guid isPermaLink="true">https://mediamaak.com/books/auto-trading/operations-review/</guid>
      <pubDate>Tue, 20 Oct 2026 00:00:00 +0900</pubDate>
      <description>기술 오류와 전략 결과를 구분하고 운영·전략 변경 기록을 남깁니다.</description>
      <content:encoded><![CDATA[<article class="pk-article" data-content-id="/books/auto-trading/operations-review/" data-content-type="wiki-chapter" data-published-at="2026-10-20T00:00:00+09:00" data-updated-at="2026-10-20T00:00:00+09:00">
      <header class="pk-hero">
        <p class="pk-eyebrow">12 / 12 · AUTO TRADING WIKI</p>
        <h1>운영 기록을 남기고 개선하기</h1>
        <p class="pk-summary">기술 오류와 전략 결과를 구분하고 운영·전략 변경 기록을 남깁니다.</p>
        <div class="pk-meta"><dl><dt>발행</dt><dd><time datetime="2026-10-20T00:00:00+09:00">2026-10-20</time></dd><dt>수정</dt><dd><time datetime="2026-10-20T00:00:00+09:00">2026-10-20</time></dd><dt>문서 검토</dt><dd>2026-10-20</dd><dt>본문 버전</dt><dd>1.1.0-web.20261020</dd></dl></div>
      </header>
      <div class="book-reading-content">
        <div class="pk-body-column">
          <div class="pk-prose"><p>자동매매 학습 프로젝트는 코드가 한 번 실행되었다고 끝나지 않는다. 데이터를 제때 읽었는지, 신호가 없었던 것인지 계산이 실패한 것인지, 현금·수량 기록이 맞는지 매일 구분해야 한다. 이번 장에서는 그 판단을 다음 실행과 연결하는 운영 기록을 만든다.</p>
<p>이 책의 운영 대상은 외부 주문이 없는 <code>auto-trading-lab</code>의 로컬 paper 환경이다. 아래 기록과 손익은 모두 가상 예시다. 운영 검토의 목적은 수익을 약속하거나 매매 결정을 대신하는 것이 아니라 실행 상태와 변경 근거를 보존하는 것이다.</p>
<h2 id="operations-purpose">1. 실행 결과와 운영 판단을 연결하기</h2>
<p><code>paper_runs</code>에는 프로그램이 실제로 한 일을 기록하고, <code>operation_notes</code>에는 사람이 그 결과를 어떻게 판단했는지 적는다. “오늘 이상 없음”이라는 메모만 남기면 어떤 데이터와 원장을 봤는지 알 수 없다. 실행 식별자와 증거 경로를 함께 기록한다.</p>
<table>
<thead>
<tr>
<th>기록</th>
<th>예시 내용</th>
</tr>
</thead>
<tbody>
<tr>
<td>실행 사실</td>
<td>데이터 기준일, 신호 수, 의도·체결 수, 상태</td>
</tr>
<tr>
<td>정합성 사실</td>
<td>현금 차이, 수량 차이, 미처리 주문 수</td>
</tr>
<tr>
<td>운영 판단</td>
<td>정상 관찰 지속, 차단 유지, 원인 조사</td>
</tr>
<tr>
<td>다음 조치</td>
<td>담당자, 기한, 재점검 기준</td>
</tr>
</tbody>
</table>
<p>판단과 사실이 다르면 사실을 먼저 확인한다. 예를 들어 로그에 오류가 없더라도 해당 거래일의 실행 행이 없다면 “문제 없음”이 아니라 “미실행 또는 기록 유실 의심”이다.</p>
<h2 id="operations-daily">2. 하루 점검 순서를 고정하기</h2>
<p>데이터 → 계산 → 의도 → 체결 → 원장 → 오류 → 다음 조치 순서로 점검한다. 매일 같은 순서로 보면 상류 단계의 실패가 하류의 신호 없음으로 위장하는 것을 줄일 수 있다.</p>
<ol type="1">
<li>처리할 <code>business_date</code>와 데이터 확정 시각을 확인한다.</li>
<li>대상 종목 수, 원본 행 수, 누락·가격 이상을 확인한다.</li>
<li>CCI 계산 대상과 결측 사유, 상향 돌파 신호 수를 확인한다.</li>
<li>주문 의도·제외 사유와 기존 의도키 충돌을 확인한다.</li>
<li>가상 체결과 예약현금, 보유 수량을 대조한다.</li>
<li>실행 상태와 오류·중단·복구 기록을 확인한다.</li>
<li>다음 실행을 허용할지 판단하고 근거를 적는다.</li>
</ol>
<p>신호 0건은 정상일 수 있다. 하지만 계산 대상 0건, 수집 실패 200건, 신호 0건을 같은 정상 결과로 표시하면 안 된다. 기대 대상 수와 실제 처리 수를 함께 보고한다.</p>
<h2 id="operations-notes">3. 운영 메모의 최소 구조 만들기</h2>
<p><code>operation_notes</code>에는 다음 필드를 둔다. 사실을 보정해야 할 때는 기존 메모를 지우지 않고 새 메모에서 이전 기록을 참조한다.</p>
<div class="sourceCode" id="cb1"><pre class="sourceCode sql"><code class="sourceCode sql"><span id="cb1-1"><a href="#cb1-1" aria-hidden="true" tabindex="-1"></a><span class="kw">CREATE</span> <span class="kw">TABLE</span> operation_notes (</span>
<span id="cb1-2"><a href="#cb1-2" aria-hidden="true" tabindex="-1"></a>  note_id <span class="dt">CHAR</span>(<span class="dv">36</span>) <span class="kw">PRIMARY</span> <span class="kw">KEY</span>,</span>
<span id="cb1-3"><a href="#cb1-3" aria-hidden="true" tabindex="-1"></a>  business_date <span class="dt">DATE</span> <span class="kw">NOT</span> <span class="kw">NULL</span>,</span>
<span id="cb1-4"><a href="#cb1-4" aria-hidden="true" tabindex="-1"></a>  run_id <span class="dt">VARCHAR</span>(<span class="dv">80</span>) <span class="kw">NOT</span> <span class="kw">NULL</span>,</span>
<span id="cb1-5"><a href="#cb1-5" aria-hidden="true" tabindex="-1"></a>  environment <span class="dt">VARCHAR</span>(<span class="dv">20</span>) <span class="kw">NOT</span> <span class="kw">NULL</span>,</span>
<span id="cb1-6"><a href="#cb1-6" aria-hidden="true" tabindex="-1"></a>  <span class="kw">category</span> <span class="dt">VARCHAR</span>(<span class="dv">40</span>) <span class="kw">NOT</span> <span class="kw">NULL</span>,</span>
<span id="cb1-7"><a href="#cb1-7" aria-hidden="true" tabindex="-1"></a>  decision <span class="dt">VARCHAR</span>(<span class="dv">40</span>) <span class="kw">NOT</span> <span class="kw">NULL</span>,</span>
<span id="cb1-8"><a href="#cb1-8" aria-hidden="true" tabindex="-1"></a>  evidence_path <span class="dt">VARCHAR</span>(<span class="dv">255</span>) <span class="kw">NOT</span> <span class="kw">NULL</span>,</span>
<span id="cb1-9"><a href="#cb1-9" aria-hidden="true" tabindex="-1"></a>  <span class="kw">summary</span> TEXT <span class="kw">NOT</span> <span class="kw">NULL</span>,</span>
<span id="cb1-10"><a href="#cb1-10" aria-hidden="true" tabindex="-1"></a>  follow_up TEXT,</span>
<span id="cb1-11"><a href="#cb1-11" aria-hidden="true" tabindex="-1"></a>  created_at <span class="dt">TIMESTAMP</span> <span class="kw">NOT</span> <span class="kw">NULL</span> <span class="kw">DEFAULT</span> <span class="fu">CURRENT_TIMESTAMP</span></span>
<span id="cb1-12"><a href="#cb1-12" aria-hidden="true" tabindex="-1"></a>) ENGINE<span class="op">=</span>InnoDB;</span></code></pre></div>
<p><code>environment</code>는 <code>local_paper</code>로 명시한다. 계좌번호나 토큰을 증거 경로와 메모에 넣지 않는다. 하루에 여러 실행이 있으면 각 실행과 일일 종합 메모를 연결해 재시도가 누락되지 않게 한다.</p>
<pre class="text"><code>business_date=2026-06-09 environment=local_paper
run_id=demo-paper-12 state=COMPLETED
targets=5 calculated=5 signals=0 pending_orders=0
cash_difference=0.00 quantity_difference=0
decision=CONTINUE_LOCAL_PAPER
summary=데이터 확정·계산·원장 대조 완료, 신규 신호 없음</code></pre>
<p>이 예시는 보고 형식이며 실제 프로젝트가 해당 날짜에 실행되었다는 증거가 아니다. 실제 보고서는 DB와 로그에서 읽은 값으로 생성한다.</p>
<h2 id="operations-classify">4. 기술 실패·운영 차단·전략 결과를 구분하기</h2>
<p>기술 실패는 프로그램이 정해진 처리를 수행하지 못한 경우다. 운영 차단은 미확정 데이터나 중단 파일처럼 미리 정한 조건에 따라 멈춘 경우다. 전략 결과는 프로그램이 정상 처리했지만 가상 손익이 좋거나 나빴던 경우다.</p>
<table>
<colgroup>
<col style="width: 33%" />
<col style="width: 33%" />
<col style="width: 33%" />
</colgroup>
<thead>
<tr>
<th>사례</th>
<th>분류</th>
<th>첫 조치</th>
</tr>
</thead>
<tbody>
<tr>
<td>DB 연결 오류</td>
<td><code>TECHNICAL_FAILURE</code></td>
<td>연결·권한·장애 기록 확인</td>
</tr>
<tr>
<td>데이터 미확정</td>
<td><code>EXPECTED_BLOCK</code></td>
<td>수집 상태와 다음 확인 시점 기록</td>
</tr>
<tr>
<td>현금 대조 차이</td>
<td><code>RECONCILIATION_FAILURE</code></td>
<td>다음 실행 차단, 원장 추적</td>
</tr>
<tr>
<td>비용 후 가상 손실</td>
<td><code>STRATEGY_OBSERVATION</code></td>
<td>규칙 유지하며 검토 기록</td>
</tr>
<tr>
<td>주문 기록 중복</td>
<td><code>DUPLICATE_INCIDENT</code></td>
<td>즉시 중단, 의도·체결키 분석</td>
</tr>
</tbody>
</table>
<p>손실을 모두 버그로 보거나 오류를 모두 전략 탓으로 돌리면 원인을 찾기 어렵다. 먼저 같은 입력의 수동 계산과 상태 전이를 확인하고, 처리 자체가 정확한 경우에 전략 검토 후보로 분류한다.</p>
<h2 id="operations-incident">5. 장애 기록은 원인보다 증거부터 적기</h2>
<p>장애 발생 시 첫 기록에는 관측 사실, 영향 범위, 마지막 정상 시각, 중단 상태를 적는다. 원인을 아직 모르면 <code>UNKNOWN</code>이라고 쓴다. “아마 네트워크 문제”라는 추정을 확정 원인처럼 기록하지 않는다.</p>
<pre class="text"><code>incident_id=demo-incident-03 severity=BLOCKING
observed=현금 원장 합계와 계정 현금 차이 50.00
affected_run=demo-paper-13 last_known_good=demo-paper-12
new_processing=BLOCKED root_cause=UNKNOWN
evidence=outputs/incidents/demo-incident-03/
next_action=체결별 수수료 정산 사건과 반올림 정책 대조</code></pre>
<p>기록을 맞추려고 원장 행을 삭제하거나 현금을 임의로 바꾸지 않는다. 불변 체결과 현금 사건으로 기대 상태를 다시 계산한다. 정정이 필요하면 수정 사유·원본 참조·정정 사건을 남기고 대조를 다시 수행한다. 새 상태가 맞았다는 근거를 복구 기록에 연결한다.</p>
<p>운영 로그를 읽을 때는 프로젝트의 UTF-8 로그 도구가 있으면 그것을 사용하고, PowerShell에서는 <code>Get-Content -Encoding UTF8</code>을 사용한다. 인코딩에 따른 표시 차이는 <a href="https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_character_encoding">Microsoft 공식 문서</a>에서 확인할 수 있다.</p>
<h2 id="operations-change">6. 전략 변경을 실험 단위로 관리하기</h2>
<p>손실 한 건을 본 직후 CCI 기준을 바꾸지 않는다. 바꾸고 싶은 이유를 먼저 가설로 적고, 현재 기준 실행을 보관한다. CCI 기간·돌파 수준·진입·청산·비용 중 무엇을 바꾸는지 한 번에 구분할 수 있어야 한다.</p>
<div class="sourceCode" id="cb4"><pre class="sourceCode json"><code class="sourceCode json"><span id="cb4-1"><a href="#cb4-1" aria-hidden="true" tabindex="-1"></a><span class="fu">{</span></span>
<span id="cb4-2"><a href="#cb4-2" aria-hidden="true" tabindex="-1"></a>  <span class="dt">&quot;change_id&quot;</span><span class="fu">:</span> <span class="st">&quot;demo-change-02&quot;</span><span class="fu">,</span></span>
<span id="cb4-3"><a href="#cb4-3" aria-hidden="true" tabindex="-1"></a>  <span class="dt">&quot;status&quot;</span><span class="fu">:</span> <span class="st">&quot;PROPOSED&quot;</span><span class="fu">,</span></span>
<span id="cb4-4"><a href="#cb4-4" aria-hidden="true" tabindex="-1"></a>  <span class="dt">&quot;baseline_version&quot;</span><span class="fu">:</span> <span class="st">&quot;cci20_cross_up_m100_v1&quot;</span><span class="fu">,</span></span>
<span id="cb4-5"><a href="#cb4-5" aria-hidden="true" tabindex="-1"></a>  <span class="dt">&quot;candidate_version&quot;</span><span class="fu">:</span> <span class="st">&quot;cci20_cross_up_m100_v2_experiment&quot;</span><span class="fu">,</span></span>
<span id="cb4-6"><a href="#cb4-6" aria-hidden="true" tabindex="-1"></a>  <span class="dt">&quot;hypothesis&quot;</span><span class="fu">:</span> <span class="st">&quot;가격 누락 처리 변경이 미완료 분류를 더 정확하게 만드는가&quot;</span><span class="fu">,</span></span>
<span id="cb4-7"><a href="#cb4-7" aria-hidden="true" tabindex="-1"></a>  <span class="dt">&quot;changed_component&quot;</span><span class="fu">:</span> <span class="st">&quot;missing_session_classification&quot;</span><span class="fu">,</span></span>
<span id="cb4-8"><a href="#cb4-8" aria-hidden="true" tabindex="-1"></a>  <span class="dt">&quot;evaluation_dataset&quot;</span><span class="fu">:</span> <span class="st">&quot;fixture-v2&quot;</span><span class="fu">,</span></span>
<span id="cb4-9"><a href="#cb4-9" aria-hidden="true" tabindex="-1"></a>  <span class="dt">&quot;paper_restart_required&quot;</span><span class="fu">:</span> <span class="kw">true</span><span class="fu">,</span></span>
<span id="cb4-10"><a href="#cb4-10" aria-hidden="true" tabindex="-1"></a>  <span class="dt">&quot;live_permission&quot;</span><span class="fu">:</span> <span class="kw">false</span></span>
<span id="cb4-11"><a href="#cb4-11" aria-hidden="true" tabindex="-1"></a><span class="fu">}</span></span></code></pre></div>
<p>이 예시는 수익 규칙 변경보다 데이터 처리 개선을 먼저 비교한다. 실제 전략 조건을 실험하더라도 가설, 평가 기간, 비용 가정, 제외 기준을 결과를 보기 전에 정한다. 여러 후보를 시도했다면 가장 좋은 하나뿐 아니라 시도한 후보 수와 탈락 결과도 남긴다.</p>
<p>변경 상태는 <code>PROPOSED → UNDER_REVIEW → SIMULATION_CHECKED → PAPER_CHECKED → ACCEPTED</code>로 관리한다. 실패나 보류는 <code>REJECTED</code>, <code>DEFERRED</code>로 남긴다. <code>ACCEPTED</code>는 이 책의 로컬 학습 기준에 반영했다는 뜻이며 실전 전송 허가가 아니다.</p>
<h2 id="operations-weekly">7. 주간 리뷰에서 처리 품질과 결과를 나누기</h2>
<p>주간 리뷰에는 예정 세션 수와 실제 실행 수, 차단 횟수와 사유, 오류 재발, 복구 시간, 미해결 항목을 먼저 적는다. 그다음 비용 후 가상 손익과 포지션 변화를 본다. 단순 수익률 표만으로 운영 상태를 판단하지 않는다.</p>
<table>
<thead>
<tr>
<th>지표</th>
<th>가상 예시</th>
<th>검토할 내용</th>
</tr>
</thead>
<tbody>
<tr>
<td>예정·완료 세션</td>
<td>5 / 4</td>
<td>나머지 1회는 데이터 지연 차단</td>
</tr>
<tr>
<td>설명된 차단</td>
<td>1회</td>
<td>원인과 재개 기록 존재</td>
</tr>
<tr>
<td>원장 불일치</td>
<td>0회</td>
<td>대조 실행 자체가 빠지지 않았는지</td>
</tr>
<tr>
<td>중복 주문·정산</td>
<td>0건</td>
<td>고유키 충돌 로그도 함께 확인</td>
</tr>
<tr>
<td>미해결 장애</td>
<td>0건</td>
<td>해결 근거와 회귀 확인</td>
</tr>
<tr>
<td>가상 손익</td>
<td>별도 표</td>
<td>비용·평가 시각·분모 표시</td>
</tr>
</tbody>
</table>
<p>4회 완료를 80% 성공이라고만 표현하면 정상적인 데이터 보호 차단과 기술 실패가 섞인다. 차단의 이유와 적절성을 함께 해석한다. 예시의 횟수는 운영 품질 기준을 설명할 뿐 실제 서비스 안정성을 입증하지 않는다.</p>
<h2 id="operations-backup">8. 백업은 복구 시험까지 수행하기</h2>
<p>보관 대상은 소스 버전, 비밀값 없는 설정 예제, DB 스키마, 원장 데이터, 실행 식별자와 검토 기록이다. 접근 권한이 필요한 원본 데이터와 비밀 설정은 별도 보호하고 공개 저장소나 위키에 올리지 않는다.</p>
<p>복구 시험은 빈 테스트 DB에서 수행한다. 백업을 복원한 뒤 원장 건수, 마지막 <code>run_id</code>, 현금·수량 대조, 미완료 주문과 예약액을 확인한다. 복구본을 실제 반복 스케줄러에 연결하지 않도록 환경 이름과 DB 이름을 구분한다.</p>
<p>기록의 해시를 비교하면 파일 변경을 확인하는 데 도움이 되지만, 해시가 같다는 이유로 데이터의 정확성까지 보장되지는 않는다. 해시 계산은 <a href="https://docs.python.org/3/library/hashlib.html">Python 공식 hashlib 문서</a>를 참고하고, 기능 대조와 함께 사용한다.</p>
<h2 id="operations-codex">9. Codex에 운영 증거를 요약하도록 요청하기</h2>
<pre class="text"><code>이번 주 local_paper 운영을 읽기 전용으로 점검해줘.
paper_runs·주문·체결·현금 원장·operation_notes를 run_id로 연결해.
예정 세션, 완료, 정상 차단, 기술 실패, 재시도, 복구를 구분해.
현금·수량 불일치와 미해결 장애를 먼저 보고하고 근거 경로를 적어줘.
가상 손익은 비용과 평가 시각을 표시하고 실제 성과처럼 표현하지 마.
전략 변경 로그 없이 달라진 설정이 있는지 확인해.
파일·DB 수정, 실제 주문, 외부 메시지 발송은 수행하지 마.</code></pre>
<p>읽기 전용 검토가 끝나면 해결할 항목 하나를 정해 별도 수정 작업으로 이어간다. 수정 시 기존 결과를 보존하고 관련 시뮬레이션·페이퍼·차단 시험을 다시 확인한다. 모르는 값을 추정해 보고서 빈칸을 채우지 않는다.</p>
<h2 id="operations-done">10. 완료 기준과 계속 유지할 원칙</h2>
<p>이 장의 완료 기준은 하루 메모가 실행·증거와 연결되고, 장애와 전략 관찰이 구분되며, 변경이 검토 단계를 거치고, 복구 시험 결과가 남는 것이다. 처음 한 번 작성한 운영 문서를 영구적으로 완료된 상태로 보지 않는다.</p>
<table>
<thead>
<tr>
<th>증상</th>
<th>먼저 확인할 것</th>
</tr>
</thead>
<tbody>
<tr>
<td>신호 0건이라 매일 정상 처리</td>
<td>계산 대상·완료 수·누락 행</td>
</tr>
<tr>
<td>전략 버전은 같은데 결과가 달라짐</td>
<td>데이터·설정·코드 해시와 변경 기록</td>
</tr>
<tr>
<td>장애를 해결했지만 재발</td>
<td>최소 재현 사례와 수정 후 검증</td>
</tr>
<tr>
<td>백업은 있는데 복구가 안 됨</td>
<td>버전·권한·스키마·복원 절차</td>
</tr>
<tr>
<td>가상 원장을 실제 성과로 소개</td>
<td>환경 이름·체결 가정·비용·자료 범위</td>
</tr>
</tbody>
</table>
<p>데이터를 확인하고, 규칙을 기록하고, 이상 상태에서 멈추고, 근거가 있을 때만 변경한다. 이 절차를 반복할 수 있는 로컬 학습 시스템이 이 책의 최종 산출물이다. 실제 시장 연결과 운영 판단은 별도의 검토 과제로 남긴다.</p>
</div>
          <section class="pk-related" aria-labelledby="related-heading"><p class="pk-eyebrow">FROM CONCEPT TO PRACTICE</p><h2 id="related-heading">구현 기록과 함께 읽기</h2><ul><li><a href="https://mediamaak.com/books/auto-trading/live-safety/">주문 전 체크리스트와 안전장치</a></li></ul></section>
          <nav class="pk-pager" aria-label="이전 장과 다음 장"><a href="https://mediamaak.com/books/auto-trading/live-safety/">이전 글: 주문 전 체크리스트와 안전장치</a></nav>
          <div class="pk-share"><div><p class="pk-share-title">학습의 다음 단계를 남겨 두세요</p><p>공유 링크를 복사해 이어서 읽을 수 있습니다.</p></div><div><button type="button" data-share-content>공유 링크 복사</button><span role="status" data-share-status></span></div></div>
        </div>
      </div>
    </article>]]></content:encoded>
    </item>
    <item>
      <title>수집한 데이터를 MySQL에 저장하기</title>
      <link>https://mediamaak.com/books/auto-trading/mysql-storage/</link>
      <guid isPermaLink="true">https://mediamaak.com/books/auto-trading/mysql-storage/</guid>
      <pubDate>Tue, 20 Oct 2026 00:00:00 +0900</pubDate>
      <description>수집 데이터의 저장 구조와 행 수·키·날짜 정합성을 확인합니다.</description>
      <content:encoded><![CDATA[<article class="pk-article" data-content-id="/books/auto-trading/mysql-storage/" data-content-type="wiki-chapter" data-published-at="2026-10-20T00:00:00+09:00" data-updated-at="2026-10-20T10:36:01+09:00">
      <header class="pk-hero">
        <p class="pk-eyebrow">5 / 12 · AUTO TRADING WIKI</p>
        <h1>수집한 데이터를 MySQL에 저장하기</h1>
        <p class="pk-summary">수집 데이터의 저장 구조와 행 수·키·날짜 정합성을 확인합니다.</p>
        <div class="pk-meta"><dl><dt>발행</dt><dd><time datetime="2026-10-20T00:00:00+09:00">2026-10-20</time></dd><dt>수정</dt><dd><time datetime="2026-10-20T10:36:01+09:00">2026-10-20</time></dd><dt>문서 검토</dt><dd>2026-10-20</dd><dt>본문 버전</dt><dd>1.1.1-web.20261020</dd></dl></div>
      </header>
      <div class="book-reading-content">
        <div class="pk-body-column">
          <div class="pk-prose"><p>검증된 CSV를 MySQL에 저장하면 종목과 기간을 조건으로 필요한 행을 다시 읽을 수 있다. 이 장에서는 종목마다 테이블을 만들지 않고 <strong><code>daily_prices</code> 하나에 모든 종목의 일봉</strong>을 모은다. 종목코드와 거래일을 기본 키로 지정해 같은 자료를 재실행해도 행이 계속 늘어나지 않게 한다.</p>
<p>MySQL 서버 설치와 사용자 관리는 독자의 실행 환경에 따라 다르다. 여기서는 별도의 학습용 MySQL 8.4 데이터베이스에 접속할 수 있다고 가정한다. 실제 서비스 DB에 예제를 그대로 실행하지 않고, 준비된 학습용 저장소에서 진행한다. <a href="https://dev.mysql.com/doc/refman/8.4/en/installing.html">MySQL 공식 설치 안내</a></p>
<h2 id="storage-model">1. 저장과 조회의 단위 정하기</h2>
<p>한 종목의 1일 일봉을 한 행으로 저장한다. 이렇게 하면 종목이 늘어도 테이블 이름을 동적으로 만들 필요가 없고, 다음 장에서 같은 SQL로 모든 종목을 읽을 수 있다.</p>
<table>
<colgroup>
<col style="width: 33%" />
<col style="width: 33%" />
<col style="width: 33%" />
</colgroup>
<thead>
<tr>
<th>컬럼</th>
<th>자료형</th>
<th>역할</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>symbol</code></td>
<td><code>CHAR(6)</code></td>
<td>앞자리 0과 영문 대문자를 보존하는 6자리 종목코드</td>
</tr>
<tr>
<td><code>trade_date</code></td>
<td><code>DATE</code></td>
<td>거래일</td>
</tr>
<tr>
<td><code>open</code>, <code>high</code>, <code>low</code>, <code>close</code></td>
<td><code>DECIMAL(18,4)</code></td>
<td>시가·고가·저가·종가</td>
</tr>
<tr>
<td><code>volume</code></td>
<td><code>BIGINT UNSIGNED</code></td>
<td>거래량</td>
</tr>
<tr>
<td><code>updated_at</code></td>
<td><code>TIMESTAMP</code></td>
<td>DB에 마지막으로 저장·변경한 시각</td>
</tr>
</tbody>
</table>
<p>가격에 <code>DECIMAL</code>을 사용하면 금액을 십진수 단위로 보관할 수 있다. 이번 CSV 예제는 원 단위 정수지만, SQL 구조는 소수 네 자리까지 허용한다. 이것이 지표 계산을 반드시 Decimal로 해야 한다는 뜻은 아니다. 금액 저장과 통계 계산의 숫자 정책은 목적에 맞게 구분한다. <a href="https://dev.mysql.com/doc/refman/8.4/en/fixed-point-types.html">MySQL 공식 고정소수점 설명</a></p>
<p>현재 파일의 수정주가와 다른 가격 체계를 같은 키에 섞으면 출처를 구분하기 어렵다. 학습용 <code>daily_prices</code>는 4장에서 정한 <strong>KRX 수정주가 일봉</strong>만 담는다. 다른 시장·원주가 자료는 별도 데이터셋으로 관리한다.</p>
<h2 id="schema">2. 학습용 데이터베이스와 테이블 준비하기</h2>
<p>DB를 관리하는 계정으로 <code>auto_trading_lab</code>을 만들고 아래 SQL을 실행한다. SQL은 <code>config/schema.sql</code>에 저장해 변경 이력을 남길 수 있다. <code>CREATE TABLE IF NOT EXISTS</code>는 기존 테이블의 구조를 자동으로 바꾸지 않으므로 이미 테이블이 있다면 <code>SHOW CREATE TABLE</code>로 확인한다.</p>
<div class="sourceCode" id="cb1"><pre class="sourceCode sql"><code class="sourceCode sql"><span id="cb1-1"><a href="#cb1-1" aria-hidden="true" tabindex="-1"></a><span class="kw">CREATE</span> <span class="kw">DATABASE</span> <span class="cf">IF</span> <span class="kw">NOT</span> <span class="kw">EXISTS</span> auto_trading_lab</span>
<span id="cb1-2"><a href="#cb1-2" aria-hidden="true" tabindex="-1"></a>  <span class="dt">CHARACTER</span> <span class="kw">SET</span> utf8mb4 COLLATE utf8mb4_0900_ai_ci;</span>
<span id="cb1-3"><a href="#cb1-3" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb1-4"><a href="#cb1-4" aria-hidden="true" tabindex="-1"></a><span class="kw">USE</span> auto_trading_lab;</span>
<span id="cb1-5"><a href="#cb1-5" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb1-6"><a href="#cb1-6" aria-hidden="true" tabindex="-1"></a><span class="kw">CREATE</span> <span class="kw">TABLE</span> <span class="cf">IF</span> <span class="kw">NOT</span> <span class="kw">EXISTS</span> universe_snapshots (</span>
<span id="cb1-7"><a href="#cb1-7" aria-hidden="true" tabindex="-1"></a>  snapshot_date <span class="dt">DATE</span> <span class="kw">NOT</span> <span class="kw">NULL</span>,</span>
<span id="cb1-8"><a href="#cb1-8" aria-hidden="true" tabindex="-1"></a>  symbol <span class="dt">CHAR</span>(<span class="dv">6</span>) <span class="kw">NOT</span> <span class="kw">NULL</span>,</span>
<span id="cb1-9"><a href="#cb1-9" aria-hidden="true" tabindex="-1"></a>  name <span class="dt">VARCHAR</span>(<span class="dv">100</span>) <span class="kw">NOT</span> <span class="kw">NULL</span>,</span>
<span id="cb1-10"><a href="#cb1-10" aria-hidden="true" tabindex="-1"></a>  <span class="kw">source</span> <span class="dt">VARCHAR</span>(<span class="dv">500</span>) <span class="kw">NOT</span> <span class="kw">NULL</span>,</span>
<span id="cb1-11"><a href="#cb1-11" aria-hidden="true" tabindex="-1"></a>  <span class="kw">PRIMARY</span> <span class="kw">KEY</span> (snapshot_date, symbol)</span>
<span id="cb1-12"><a href="#cb1-12" aria-hidden="true" tabindex="-1"></a>) ENGINE<span class="op">=</span>InnoDB;</span>
<span id="cb1-13"><a href="#cb1-13" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb1-14"><a href="#cb1-14" aria-hidden="true" tabindex="-1"></a><span class="kw">CREATE</span> <span class="kw">TABLE</span> <span class="cf">IF</span> <span class="kw">NOT</span> <span class="kw">EXISTS</span> daily_prices (</span>
<span id="cb1-15"><a href="#cb1-15" aria-hidden="true" tabindex="-1"></a>  symbol <span class="dt">CHAR</span>(<span class="dv">6</span>) <span class="kw">NOT</span> <span class="kw">NULL</span>,</span>
<span id="cb1-16"><a href="#cb1-16" aria-hidden="true" tabindex="-1"></a>  trade_date <span class="dt">DATE</span> <span class="kw">NOT</span> <span class="kw">NULL</span>,</span>
<span id="cb1-17"><a href="#cb1-17" aria-hidden="true" tabindex="-1"></a>  <span class="kw">open</span> <span class="dt">DECIMAL</span>(<span class="dv">18</span>,<span class="dv">4</span>) <span class="kw">NOT</span> <span class="kw">NULL</span>,</span>
<span id="cb1-18"><a href="#cb1-18" aria-hidden="true" tabindex="-1"></a>  high <span class="dt">DECIMAL</span>(<span class="dv">18</span>,<span class="dv">4</span>) <span class="kw">NOT</span> <span class="kw">NULL</span>,</span>
<span id="cb1-19"><a href="#cb1-19" aria-hidden="true" tabindex="-1"></a>  low <span class="dt">DECIMAL</span>(<span class="dv">18</span>,<span class="dv">4</span>) <span class="kw">NOT</span> <span class="kw">NULL</span>,</span>
<span id="cb1-20"><a href="#cb1-20" aria-hidden="true" tabindex="-1"></a>  <span class="kw">close</span> <span class="dt">DECIMAL</span>(<span class="dv">18</span>,<span class="dv">4</span>) <span class="kw">NOT</span> <span class="kw">NULL</span>,</span>
<span id="cb1-21"><a href="#cb1-21" aria-hidden="true" tabindex="-1"></a>  volume BIGINT UNSIGNED <span class="kw">NOT</span> <span class="kw">NULL</span>,</span>
<span id="cb1-22"><a href="#cb1-22" aria-hidden="true" tabindex="-1"></a>  updated_at <span class="dt">TIMESTAMP</span> <span class="kw">NOT</span> <span class="kw">NULL</span> <span class="kw">DEFAULT</span> <span class="fu">CURRENT_TIMESTAMP</span></span>
<span id="cb1-23"><a href="#cb1-23" aria-hidden="true" tabindex="-1"></a>    <span class="kw">ON</span> <span class="kw">UPDATE</span> <span class="fu">CURRENT_TIMESTAMP</span>,</span>
<span id="cb1-24"><a href="#cb1-24" aria-hidden="true" tabindex="-1"></a>  <span class="kw">PRIMARY</span> <span class="kw">KEY</span> (symbol, trade_date),</span>
<span id="cb1-25"><a href="#cb1-25" aria-hidden="true" tabindex="-1"></a>  <span class="kw">CONSTRAINT</span> chk_daily_prices_positive</span>
<span id="cb1-26"><a href="#cb1-26" aria-hidden="true" tabindex="-1"></a>    <span class="kw">CHECK</span> (<span class="kw">open</span> <span class="op">&gt;</span> <span class="dv">0</span> <span class="kw">AND</span> high <span class="op">&gt;</span> <span class="dv">0</span> <span class="kw">AND</span> low <span class="op">&gt;</span> <span class="dv">0</span> <span class="kw">AND</span> <span class="kw">close</span> <span class="op">&gt;</span> <span class="dv">0</span>),</span>
<span id="cb1-27"><a href="#cb1-27" aria-hidden="true" tabindex="-1"></a>  <span class="kw">CONSTRAINT</span> chk_daily_prices_range</span>
<span id="cb1-28"><a href="#cb1-28" aria-hidden="true" tabindex="-1"></a>    <span class="kw">CHECK</span> (high <span class="op">&gt;=</span> <span class="kw">open</span> <span class="kw">AND</span> high <span class="op">&gt;=</span> <span class="kw">close</span> <span class="kw">AND</span> high <span class="op">&gt;=</span> low</span>
<span id="cb1-29"><a href="#cb1-29" aria-hidden="true" tabindex="-1"></a>           <span class="kw">AND</span> low <span class="op">&lt;=</span> <span class="kw">open</span> <span class="kw">AND</span> low <span class="op">&lt;=</span> <span class="kw">close</span>)</span>
<span id="cb1-30"><a href="#cb1-30" aria-hidden="true" tabindex="-1"></a>) ENGINE<span class="op">=</span>InnoDB;</span></code></pre></div>
<p><code>universe_snapshots</code>는 종목 목록의 기준일을 보존하는 구조다. 이번 실습의 가격 적재 코드는 <code>daily_prices</code>만 다룬다. 구성 종목 목록을 적재할 때도 <code>(snapshot_date, symbol)</code>을 기준으로 검증·매개변수 바인딩을 적용한다.</p>
<p>DDL은 데이터 적재 트랜잭션과 분리해서 실행한다. MySQL에서는 일부 DDL 문이 암묵적 커밋을 일으킨다. 프로그램에서 테이블 생성과 가격 입력을 한 트랜잭션처럼 취급하지 않는다. <a href="https://dev.mysql.com/doc/refman/8.4/en/implicit-commit.html">MySQL 공식 암묵적 커밋 문서</a></p>
<h2 id="database-config">3. 접속 설정과 권한 구분하기</h2>
<p><code>config/example.env</code>와 개인용 <code>config/.env</code>에 다음 항목을 추가한다. 기존 KIS 설정을 지우거나 덮어쓰지 않는다.</p>
<pre class="dotenv"><code>MYSQL_HOST=127.0.0.1
MYSQL_PORT=3306
MYSQL_USER=lab_writer
MYSQL_PASSWORD=REPLACE_WITH_LOCAL_PASSWORD
MYSQL_DATABASE=auto_trading_lab</code></pre>
<p>서버 설치 때 쓰는 관리자 계정과 일봉을 적재하는 계정을 구분한다. 적재 계정에는 해당 학습용 테이블의 <code>SELECT</code>, <code>INSERT</code>, <code>UPDATE</code> 등 필요한 권한만 부여하고, 지표 조회용 계정은 읽기 권한으로 좁힐 수 있다. 원격 서버에 연결한다면 인증서 검증을 포함한 TLS 설정도 환경에 맞게 준비한다.</p>
<p>명령줄 클라이언트에서는 비밀번호를 명령 인수에 직접 쓰지 않고 프롬프트로 입력한다.</p>
<div class="sourceCode" id="cb3"><pre class="sourceCode powershell"><code class="sourceCode powershell"><span id="cb3-1"><a href="#cb3-1" aria-hidden="true" tabindex="-1"></a>mysql <span class="op">--</span>host<span class="op">=</span><span class="dv">127.0</span><span class="op">.</span><span class="dv">0.1</span> <span class="op">--</span>port<span class="op">=</span><span class="dv">3306</span> <span class="op">--</span>user<span class="op">=</span>lab_writer <span class="op">--</span>password auto_trading_lab</span></code></pre></div>
<p>접속한 뒤 <code>SELECT DATABASE(), VERSION();</code>와 <code>SHOW TABLES;</code>를 실행한다. 접속 성공과 테이블 쓰기 권한은 별도이므로 작은 CSV를 적재하는 단계에서 다시 확인한다.</p>
<h2 id="load-csv">4. CSV를 검사한 뒤 트랜잭션으로 저장하기</h2>
<p><code>src/load_prices.py</code>는 이미 수집 완료가 확인된 CSV 하나를 인수로 받는다. 폴더의 모든 CSV를 무조건 읽으면 이전 실패 실행의 파일이나 요약 파일까지 섞일 수 있다. 4장의 실행 요약에서 성공한 파일을 선택한다.</p>
<div class="sourceCode" id="cb4"><pre class="sourceCode python"><code class="sourceCode python"><span id="cb4-1"><a href="#cb4-1" aria-hidden="true" tabindex="-1"></a><span class="im">import</span> argparse</span>
<span id="cb4-2"><a href="#cb4-2" aria-hidden="true" tabindex="-1"></a><span class="im">from</span> decimal <span class="im">import</span> Decimal</span>
<span id="cb4-3"><a href="#cb4-3" aria-hidden="true" tabindex="-1"></a><span class="im">from</span> pathlib <span class="im">import</span> Path</span>
<span id="cb4-4"><a href="#cb4-4" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb4-5"><a href="#cb4-5" aria-hidden="true" tabindex="-1"></a><span class="im">import</span> mysql.connector</span>
<span id="cb4-6"><a href="#cb4-6" aria-hidden="true" tabindex="-1"></a><span class="im">import</span> pandas <span class="im">as</span> pd</span>
<span id="cb4-7"><a href="#cb4-7" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb4-8"><a href="#cb4-8" aria-hidden="true" tabindex="-1"></a><span class="im">from</span> src.kis_readonly <span class="im">import</span> load_config</span>
<span id="cb4-9"><a href="#cb4-9" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb4-10"><a href="#cb4-10" aria-hidden="true" tabindex="-1"></a>REQUIRED <span class="op">=</span> [<span class="st">&quot;symbol&quot;</span>, <span class="st">&quot;trade_date&quot;</span>, <span class="st">&quot;open&quot;</span>, <span class="st">&quot;high&quot;</span>, <span class="st">&quot;low&quot;</span>, <span class="st">&quot;close&quot;</span>, <span class="st">&quot;volume&quot;</span>]</span>
<span id="cb4-11"><a href="#cb4-11" aria-hidden="true" tabindex="-1"></a>SQL <span class="op">=</span> <span class="st">&quot;&quot;&quot;</span></span>
<span id="cb4-12"><a href="#cb4-12" aria-hidden="true" tabindex="-1"></a><span class="st">INSERT INTO daily_prices</span></span>
<span id="cb4-13"><a href="#cb4-13" aria-hidden="true" tabindex="-1"></a><span class="st">  (symbol, trade_date, open, high, low, close, volume)</span></span>
<span id="cb4-14"><a href="#cb4-14" aria-hidden="true" tabindex="-1"></a><span class="st">VALUES (</span><span class="sc">%s</span><span class="st">, </span><span class="sc">%s</span><span class="st">, </span><span class="sc">%s</span><span class="st">, </span><span class="sc">%s</span><span class="st">, </span><span class="sc">%s</span><span class="st">, </span><span class="sc">%s</span><span class="st">, </span><span class="sc">%s</span><span class="st">) AS incoming</span></span>
<span id="cb4-15"><a href="#cb4-15" aria-hidden="true" tabindex="-1"></a><span class="st">ON DUPLICATE KEY UPDATE</span></span>
<span id="cb4-16"><a href="#cb4-16" aria-hidden="true" tabindex="-1"></a><span class="st">  open = incoming.open, high = incoming.high, low = incoming.low,</span></span>
<span id="cb4-17"><a href="#cb4-17" aria-hidden="true" tabindex="-1"></a><span class="st">  close = incoming.close, volume = incoming.volume</span></span>
<span id="cb4-18"><a href="#cb4-18" aria-hidden="true" tabindex="-1"></a><span class="st">&quot;&quot;&quot;</span></span>
<span id="cb4-19"><a href="#cb4-19" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb4-20"><a href="#cb4-20" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb4-21"><a href="#cb4-21" aria-hidden="true" tabindex="-1"></a><span class="kw">def</span> read_rows(path):</span>
<span id="cb4-22"><a href="#cb4-22" aria-hidden="true" tabindex="-1"></a>    frame <span class="op">=</span> pd.read_csv(path, dtype<span class="op">=</span><span class="st">&quot;string&quot;</span>, encoding<span class="op">=</span><span class="st">&quot;utf-8-sig&quot;</span>)</span>
<span id="cb4-23"><a href="#cb4-23" aria-hidden="true" tabindex="-1"></a>    <span class="cf">if</span> <span class="bu">set</span>(REQUIRED) <span class="op">-</span> <span class="bu">set</span>(frame.columns) <span class="kw">or</span> frame.empty:</span>
<span id="cb4-24"><a href="#cb4-24" aria-hidden="true" tabindex="-1"></a>        <span class="cf">raise</span> <span class="pp">ValueError</span>(<span class="st">&quot;The CSV is empty or missing required columns&quot;</span>)</span>
<span id="cb4-25"><a href="#cb4-25" aria-hidden="true" tabindex="-1"></a>    <span class="cf">if</span> frame[REQUIRED].isna().<span class="bu">any</span>().<span class="bu">any</span>():</span>
<span id="cb4-26"><a href="#cb4-26" aria-hidden="true" tabindex="-1"></a>        <span class="cf">raise</span> <span class="pp">ValueError</span>(<span class="st">&quot;The CSV contains missing values&quot;</span>)</span>
<span id="cb4-27"><a href="#cb4-27" aria-hidden="true" tabindex="-1"></a>    <span class="cf">if</span> <span class="kw">not</span> frame.symbol.<span class="bu">str</span>.fullmatch(<span class="vs">r&quot;</span><span class="pp">[A-Z0-9]</span><span class="op">{6}</span><span class="vs">&quot;</span>).<span class="bu">all</span>():</span>
<span id="cb4-28"><a href="#cb4-28" aria-hidden="true" tabindex="-1"></a>        <span class="cf">raise</span> <span class="pp">ValueError</span>(<span class="st">&quot;Invalid stock symbol&quot;</span>)</span>
<span id="cb4-29"><a href="#cb4-29" aria-hidden="true" tabindex="-1"></a>    frame[<span class="st">&quot;trade_date&quot;</span>] <span class="op">=</span> pd.to_datetime(</span>
<span id="cb4-30"><a href="#cb4-30" aria-hidden="true" tabindex="-1"></a>        frame.trade_date, <span class="bu">format</span><span class="op">=</span><span class="st">&quot;%Y-%m-</span><span class="sc">%d</span><span class="st">&quot;</span>, errors<span class="op">=</span><span class="st">&quot;raise&quot;</span></span>
<span id="cb4-31"><a href="#cb4-31" aria-hidden="true" tabindex="-1"></a>    ).dt.date</span>
<span id="cb4-32"><a href="#cb4-32" aria-hidden="true" tabindex="-1"></a>    <span class="cf">if</span> frame.duplicated([<span class="st">&quot;symbol&quot;</span>, <span class="st">&quot;trade_date&quot;</span>]).<span class="bu">any</span>():</span>
<span id="cb4-33"><a href="#cb4-33" aria-hidden="true" tabindex="-1"></a>        <span class="cf">raise</span> <span class="pp">ValueError</span>(<span class="st">&quot;Duplicate symbol/date in the input&quot;</span>)</span>
<span id="cb4-34"><a href="#cb4-34" aria-hidden="true" tabindex="-1"></a>    rows <span class="op">=</span> []</span>
<span id="cb4-35"><a href="#cb4-35" aria-hidden="true" tabindex="-1"></a>    <span class="cf">for</span> record <span class="kw">in</span> frame[REQUIRED].itertuples(index<span class="op">=</span><span class="va">False</span>, name<span class="op">=</span><span class="va">None</span>):</span>
<span id="cb4-36"><a href="#cb4-36" aria-hidden="true" tabindex="-1"></a>        symbol, trade_date, <span class="op">*</span>numbers <span class="op">=</span> record</span>
<span id="cb4-37"><a href="#cb4-37" aria-hidden="true" tabindex="-1"></a>        prices <span class="op">=</span> [Decimal(value) <span class="cf">for</span> value <span class="kw">in</span> numbers[:<span class="dv">4</span>]]</span>
<span id="cb4-38"><a href="#cb4-38" aria-hidden="true" tabindex="-1"></a>        volume_value <span class="op">=</span> Decimal(numbers[<span class="dv">4</span>])</span>
<span id="cb4-39"><a href="#cb4-39" aria-hidden="true" tabindex="-1"></a>        <span class="cf">if</span> <span class="kw">not</span> <span class="bu">all</span>(p.is_finite() <span class="kw">and</span> p <span class="op">&gt;</span> <span class="dv">0</span> <span class="cf">for</span> p <span class="kw">in</span> prices):</span>
<span id="cb4-40"><a href="#cb4-40" aria-hidden="true" tabindex="-1"></a>            <span class="cf">raise</span> <span class="pp">ValueError</span>(<span class="st">&quot;Invalid price&quot;</span>)</span>
<span id="cb4-41"><a href="#cb4-41" aria-hidden="true" tabindex="-1"></a>        <span class="cf">if</span> <span class="bu">any</span>(p <span class="op">!=</span> p.quantize(Decimal(<span class="st">&quot;0.0001&quot;</span>)) <span class="cf">for</span> p <span class="kw">in</span> prices):</span>
<span id="cb4-42"><a href="#cb4-42" aria-hidden="true" tabindex="-1"></a>            <span class="cf">raise</span> <span class="pp">ValueError</span>(<span class="st">&quot;A price exceeds four decimal places&quot;</span>)</span>
<span id="cb4-43"><a href="#cb4-43" aria-hidden="true" tabindex="-1"></a>        <span class="cf">if</span> (<span class="kw">not</span> volume_value.is_finite() <span class="kw">or</span> volume_value <span class="op">&lt;</span> <span class="dv">0</span></span>
<span id="cb4-44"><a href="#cb4-44" aria-hidden="true" tabindex="-1"></a>                <span class="kw">or</span> volume_value <span class="op">!=</span> volume_value.to_integral_value()):</span>
<span id="cb4-45"><a href="#cb4-45" aria-hidden="true" tabindex="-1"></a>            <span class="cf">raise</span> <span class="pp">ValueError</span>(<span class="st">&quot;Invalid volume&quot;</span>)</span>
<span id="cb4-46"><a href="#cb4-46" aria-hidden="true" tabindex="-1"></a>        open_, high, low, close <span class="op">=</span> prices</span>
<span id="cb4-47"><a href="#cb4-47" aria-hidden="true" tabindex="-1"></a>        <span class="cf">if</span> high <span class="op">&lt;</span> <span class="bu">max</span>(open_, low, close) <span class="kw">or</span> low <span class="op">&gt;</span> <span class="bu">min</span>(open_, high, close):</span>
<span id="cb4-48"><a href="#cb4-48" aria-hidden="true" tabindex="-1"></a>            <span class="cf">raise</span> <span class="pp">ValueError</span>(<span class="st">&quot;Invalid OHLC range&quot;</span>)</span>
<span id="cb4-49"><a href="#cb4-49" aria-hidden="true" tabindex="-1"></a>        rows.append((symbol, trade_date, <span class="op">*</span>prices, <span class="bu">int</span>(volume_value)))</span>
<span id="cb4-50"><a href="#cb4-50" aria-hidden="true" tabindex="-1"></a>    <span class="cf">return</span> rows</span>
<span id="cb4-51"><a href="#cb4-51" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb4-52"><a href="#cb4-52" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb4-53"><a href="#cb4-53" aria-hidden="true" tabindex="-1"></a><span class="kw">def</span> main():</span>
<span id="cb4-54"><a href="#cb4-54" aria-hidden="true" tabindex="-1"></a>    parser <span class="op">=</span> argparse.ArgumentParser()</span>
<span id="cb4-55"><a href="#cb4-55" aria-hidden="true" tabindex="-1"></a>    parser.add_argument(<span class="st">&quot;csv&quot;</span>, <span class="bu">type</span><span class="op">=</span>Path)</span>
<span id="cb4-56"><a href="#cb4-56" aria-hidden="true" tabindex="-1"></a>    args <span class="op">=</span> parser.parse_args()</span>
<span id="cb4-57"><a href="#cb4-57" aria-hidden="true" tabindex="-1"></a>    rows <span class="op">=</span> read_rows(args.csv)</span>
<span id="cb4-58"><a href="#cb4-58" aria-hidden="true" tabindex="-1"></a>    config <span class="op">=</span> load_config()</span>
<span id="cb4-59"><a href="#cb4-59" aria-hidden="true" tabindex="-1"></a>    required <span class="op">=</span> [<span class="st">&quot;MYSQL_HOST&quot;</span>, <span class="st">&quot;MYSQL_USER&quot;</span>, <span class="st">&quot;MYSQL_PASSWORD&quot;</span>, <span class="st">&quot;MYSQL_DATABASE&quot;</span>]</span>
<span id="cb4-60"><a href="#cb4-60" aria-hidden="true" tabindex="-1"></a>    <span class="cf">if</span> <span class="bu">any</span>(<span class="kw">not</span> config.get(key) <span class="cf">for</span> key <span class="kw">in</span> required):</span>
<span id="cb4-61"><a href="#cb4-61" aria-hidden="true" tabindex="-1"></a>        <span class="cf">raise</span> <span class="pp">ValueError</span>(<span class="st">&quot;MySQL settings are incomplete&quot;</span>)</span>
<span id="cb4-62"><a href="#cb4-62" aria-hidden="true" tabindex="-1"></a>    connection <span class="op">=</span> mysql.connector.<span class="ex">connect</span>(</span>
<span id="cb4-63"><a href="#cb4-63" aria-hidden="true" tabindex="-1"></a>        host<span class="op">=</span>config[<span class="st">&quot;MYSQL_HOST&quot;</span>],</span>
<span id="cb4-64"><a href="#cb4-64" aria-hidden="true" tabindex="-1"></a>        port<span class="op">=</span><span class="bu">int</span>(config.get(<span class="st">&quot;MYSQL_PORT&quot;</span>, <span class="st">&quot;3306&quot;</span>)),</span>
<span id="cb4-65"><a href="#cb4-65" aria-hidden="true" tabindex="-1"></a>        user<span class="op">=</span>config[<span class="st">&quot;MYSQL_USER&quot;</span>],</span>
<span id="cb4-66"><a href="#cb4-66" aria-hidden="true" tabindex="-1"></a>        password<span class="op">=</span>config[<span class="st">&quot;MYSQL_PASSWORD&quot;</span>],</span>
<span id="cb4-67"><a href="#cb4-67" aria-hidden="true" tabindex="-1"></a>        database<span class="op">=</span>config[<span class="st">&quot;MYSQL_DATABASE&quot;</span>],</span>
<span id="cb4-68"><a href="#cb4-68" aria-hidden="true" tabindex="-1"></a>        charset<span class="op">=</span><span class="st">&quot;utf8mb4&quot;</span>,</span>
<span id="cb4-69"><a href="#cb4-69" aria-hidden="true" tabindex="-1"></a>        connection_timeout<span class="op">=</span><span class="dv">10</span>,</span>
<span id="cb4-70"><a href="#cb4-70" aria-hidden="true" tabindex="-1"></a>        autocommit<span class="op">=</span><span class="va">False</span>,</span>
<span id="cb4-71"><a href="#cb4-71" aria-hidden="true" tabindex="-1"></a>    )</span>
<span id="cb4-72"><a href="#cb4-72" aria-hidden="true" tabindex="-1"></a>    cursor <span class="op">=</span> <span class="va">None</span></span>
<span id="cb4-73"><a href="#cb4-73" aria-hidden="true" tabindex="-1"></a>    <span class="cf">try</span>:</span>
<span id="cb4-74"><a href="#cb4-74" aria-hidden="true" tabindex="-1"></a>        cursor <span class="op">=</span> connection.cursor()</span>
<span id="cb4-75"><a href="#cb4-75" aria-hidden="true" tabindex="-1"></a>        <span class="cf">for</span> row <span class="kw">in</span> rows:</span>
<span id="cb4-76"><a href="#cb4-76" aria-hidden="true" tabindex="-1"></a>            cursor.execute(SQL, row)</span>
<span id="cb4-77"><a href="#cb4-77" aria-hidden="true" tabindex="-1"></a>        connection.commit()</span>
<span id="cb4-78"><a href="#cb4-78" aria-hidden="true" tabindex="-1"></a>        <span class="bu">print</span>(<span class="ss">f&quot;committed input_rows=</span><span class="sc">{</span><span class="bu">len</span>(rows)<span class="sc">}</span><span class="ss"> file=</span><span class="sc">{</span>args<span class="sc">.</span>csv<span class="sc">.</span>name<span class="sc">}</span><span class="ss">&quot;</span>)</span>
<span id="cb4-79"><a href="#cb4-79" aria-hidden="true" tabindex="-1"></a>    <span class="cf">except</span> <span class="pp">Exception</span>:</span>
<span id="cb4-80"><a href="#cb4-80" aria-hidden="true" tabindex="-1"></a>        connection.rollback()</span>
<span id="cb4-81"><a href="#cb4-81" aria-hidden="true" tabindex="-1"></a>        <span class="cf">raise</span></span>
<span id="cb4-82"><a href="#cb4-82" aria-hidden="true" tabindex="-1"></a>    <span class="cf">finally</span>:</span>
<span id="cb4-83"><a href="#cb4-83" aria-hidden="true" tabindex="-1"></a>        <span class="cf">if</span> cursor <span class="kw">is</span> <span class="kw">not</span> <span class="va">None</span>:</span>
<span id="cb4-84"><a href="#cb4-84" aria-hidden="true" tabindex="-1"></a>            cursor.close()</span>
<span id="cb4-85"><a href="#cb4-85" aria-hidden="true" tabindex="-1"></a>        connection.close()</span>
<span id="cb4-86"><a href="#cb4-86" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb4-87"><a href="#cb4-87" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb4-88"><a href="#cb4-88" aria-hidden="true" tabindex="-1"></a><span class="cf">if</span> <span class="va">__name__</span> <span class="op">==</span> <span class="st">&quot;__main__&quot;</span>:</span>
<span id="cb4-89"><a href="#cb4-89" aria-hidden="true" tabindex="-1"></a>    main()</span></code></pre></div>
<p>SQL의 <code>%s</code>는 문자열 조합으로 채우는 자리가 아니라 Connector가 값을 바인딩하는 자리다. 종목코드나 날짜를 f-string으로 SQL에 붙이지 않는다. <a href="https://dev.mysql.com/doc/connector-python/en/connector-python-api-mysqlcursor-execute.html">Connector/Python 공식 execute 문서</a></p>
<p>기본 키가 이미 있으면 새 가격으로 갱신한다. 이 예제의 행 별칭 문법은 MySQL 8.4를 기준으로 하며, 같은 파일의 재실행 시 <strong>행 수가 중복 증가하지 않는 것</strong>이 목적이다. 수정주가가 변경되면 값은 갱신될 수 있으므로 원본 CSV와 적재 일시를 남긴다. <a href="https://dev.mysql.com/doc/refman/8.4/en/insert-on-duplicate.html">MySQL 공식 중복 키 갱신 문서</a></p>
<h2 id="verify-storage">5. 적재 후 다시 조회하기</h2>
<p>아래 파일명은 실제 수집 성공 파일명으로 바꾼다.</p>
<div class="sourceCode" id="cb5"><pre class="sourceCode powershell"><code class="sourceCode powershell"><span id="cb5-1"><a href="#cb5-1" aria-hidden="true" tabindex="-1"></a><span class="op">.</span>\<span class="op">.</span><span class="fu">venv</span>\Scripts\python<span class="op">.</span><span class="fu">exe</span> <span class="op">-</span>m py_compile src<span class="op">/</span>load_prices<span class="op">.</span><span class="fu">py</span></span>
<span id="cb5-2"><a href="#cb5-2" aria-hidden="true" tabindex="-1"></a><span class="op">.</span>\<span class="op">.</span><span class="fu">venv</span>\Scripts\python<span class="op">.</span><span class="fu">exe</span> <span class="op">-</span>m src<span class="op">.</span><span class="fu">load_prices</span> outputs<span class="op">/</span>daily<span class="op">/</span>005930_2026-01-01_2026-10-16<span class="op">.</span><span class="fu">csv</span></span></code></pre></div>
<p>출력의 <code>input_rows</code>는 커밋한 입력 건수다. 새로 추가된 DB 행 수와 같다고 해석하지 않는다. 중복 키 갱신이 포함되면 추가·변경·동일값 처리의 행 수 의미가 달라질 수 있다.</p>
<p>다음 SQL로 저장 상태를 다시 읽는다.</p>
<div class="sourceCode" id="cb6"><pre class="sourceCode sql"><code class="sourceCode sql"><span id="cb6-1"><a href="#cb6-1" aria-hidden="true" tabindex="-1"></a><span class="kw">SELECT</span> symbol, <span class="fu">COUNT</span>(<span class="op">*</span>) <span class="kw">AS</span> rows_count,</span>
<span id="cb6-2"><a href="#cb6-2" aria-hidden="true" tabindex="-1"></a>       <span class="fu">MIN</span>(trade_date) <span class="kw">AS</span> first_date, <span class="fu">MAX</span>(trade_date) <span class="kw">AS</span> last_date</span>
<span id="cb6-3"><a href="#cb6-3" aria-hidden="true" tabindex="-1"></a><span class="kw">FROM</span> daily_prices</span>
<span id="cb6-4"><a href="#cb6-4" aria-hidden="true" tabindex="-1"></a><span class="kw">GROUP</span> <span class="kw">BY</span> symbol</span>
<span id="cb6-5"><a href="#cb6-5" aria-hidden="true" tabindex="-1"></a><span class="kw">ORDER</span> <span class="kw">BY</span> symbol;</span>
<span id="cb6-6"><a href="#cb6-6" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb6-7"><a href="#cb6-7" aria-hidden="true" tabindex="-1"></a><span class="kw">SELECT</span> symbol, trade_date, <span class="kw">open</span>, high, low, <span class="kw">close</span>, volume</span>
<span id="cb6-8"><a href="#cb6-8" aria-hidden="true" tabindex="-1"></a><span class="kw">FROM</span> daily_prices</span>
<span id="cb6-9"><a href="#cb6-9" aria-hidden="true" tabindex="-1"></a><span class="kw">WHERE</span> symbol <span class="op">=</span> <span class="st">&#39;005930&#39;</span></span>
<span id="cb6-10"><a href="#cb6-10" aria-hidden="true" tabindex="-1"></a><span class="kw">ORDER</span> <span class="kw">BY</span> trade_date <span class="kw">DESC</span></span>
<span id="cb6-11"><a href="#cb6-11" aria-hidden="true" tabindex="-1"></a><span class="kw">LIMIT</span> <span class="dv">5</span>;</span></code></pre></div>
<p>같은 파일을 한 번 더 적재한 뒤 대상 종목의 행 수가 늘지 않는지 확인한다. 하나라도 실패하면 <code>rollback()</code>으로 해당 CSV의 트랜잭션을 되돌린다. 이미 성공한 다른 파일의 커밋까지 되돌리는 구조는 아니다. <a href="https://dev.mysql.com/doc/connector-python/en/connector-python-example-cursor-transaction.html">Connector/Python 공식 트랜잭션 예제</a></p>
<h2 id="codex-request">6. Codex에 저장 기능 요청하기</h2>
<pre class="text"><code>검증된 일봉 CSV를 auto_trading_lab.daily_prices에 적재해줘.
종목별 테이블을 만들지 말고 (symbol, trade_date)를 기본 키로 사용해줘.
테이블 생성 DDL과 데이터 적재 트랜잭션을 분리하고, 값은 매개변수로 바인딩해줘.
입력 검증을 먼저 끝낸 뒤 파일 단위로 commit/rollback해줘.
같은 CSV를 두 번 적재해 행 수가 중복 증가하지 않는지 검증해줘.
DB 접속이 준비되지 않았다면 CSV 검증·문법 검사까지만 실행했다고 명시해줘.
기존 테이블 삭제, 운영 DB 수정, 주문 기능 추가는 이 요청의 범위에 넣지 마.</code></pre>
<h2 id="completion">7. 오류 대응과 완료 기준</h2>
<table>
<colgroup>
<col style="width: 50%" />
<col style="width: 50%" />
</colgroup>
<thead>
<tr>
<th>오류</th>
<th>확인할 내용</th>
</tr>
</thead>
<tbody>
<tr>
<td>연결 거절·시간 초과</td>
<td>서버 실행 상태, 호스트, 포트, 방화벽</td>
</tr>
<tr>
<td><code>Access denied</code></td>
<td>사용자명·비밀번호와 접속 호스트별 계정 권한</td>
</tr>
<tr>
<td>DB·테이블을 찾지 못함</td>
<td><code>MYSQL_DATABASE</code>, 스키마 생성 결과, 현재 접속 DB</td>
</tr>
<tr>
<td>CHECK 제약 위반</td>
<td>CSV의 가격 범위와 수집 원본</td>
</tr>
<tr>
<td>SQL 문법 오류</td>
<td>서버가 MySQL 8.4인지, 다른 DB 제품·버전인지</td>
</tr>
<tr>
<td>재실행 시 행 증가</td>
<td>실제 기본 키와 코드가 사용하는 테이블 이름</td>
</tr>
</tbody>
</table>
<p>완료 기준은 <strong>입력 검증 → 커밋 → 다시 조회 → 동일 파일 재실행</strong>이 모두 설명되는 것이다. DB 연결을 실행하지 않았다면 저장 성공으로 기록하지 않는다. 다음 장에서는 <code>daily_prices</code>를 종목별 거래일 순서로 읽어 CCI를 계산한다.</p>
</div>
          <section class="pk-related" aria-labelledby="related-heading"><p class="pk-eyebrow">FROM CONCEPT TO PRACTICE</p><h2 id="related-heading">구현 기록과 함께 읽기</h2><ul><li><a href="https://mediamaak.com/books/auto-trading/kospi200-data/">코스피200 데이터 수집하기</a></li></ul></section>
          <nav class="pk-pager" aria-label="이전 장과 다음 장"><a href="https://mediamaak.com/books/auto-trading/kospi200-data/">이전 글: 코스피200 데이터 수집하기</a><a href="https://mediamaak.com/books/auto-trading/cci-signals/">다음 글: MySQL 데이터로 CCI 신호 계산하기</a></nav>
          <div class="pk-share"><div><p class="pk-share-title">학습의 다음 단계를 남겨 두세요</p><p>공유 링크를 복사해 이어서 읽을 수 있습니다.</p></div><div><button type="button" data-share-content>공유 링크 복사</button><span role="status" data-share-status></span></div></div>
        </div>
      </div>
    </article>]]></content:encoded>
    </item>
    <item>
      <title>주문 전 체크리스트와 안전장치</title>
      <link>https://mediamaak.com/books/auto-trading/live-safety/</link>
      <guid isPermaLink="true">https://mediamaak.com/books/auto-trading/live-safety/</guid>
      <pubDate>Tue, 20 Oct 2026 00:00:00 +0900</pubDate>
      <description>주문 요청을 차단하는 사전 점검과 상한·감사 기록의 역할을 설명합니다.</description>
      <content:encoded><![CDATA[<article class="pk-article" data-content-id="/books/auto-trading/live-safety/" data-content-type="wiki-chapter" data-published-at="2026-10-20T00:00:00+09:00" data-updated-at="2026-10-20T00:00:00+09:00">
      <header class="pk-hero">
        <p class="pk-eyebrow">11 / 12 · AUTO TRADING WIKI</p>
        <h1>주문 전 체크리스트와 안전장치</h1>
        <p class="pk-summary">주문 요청을 차단하는 사전 점검과 상한·감사 기록의 역할을 설명합니다.</p>
        <div class="pk-meta"><dl><dt>발행</dt><dd><time datetime="2026-10-20T00:00:00+09:00">2026-10-20</time></dd><dt>수정</dt><dd><time datetime="2026-10-20T00:00:00+09:00">2026-10-20</time></dd><dt>문서 검토</dt><dd>2026-10-20</dd><dt>본문 버전</dt><dd>1.1.0-web.20261020</dd></dl></div>
      </header>
      <div class="book-reading-content">
        <div class="pk-body-column">
          <div class="pk-prose"><p>페이퍼 원장이 정확해도 실제 시장에서 같은 가격으로 체결되거나 같은 방식으로 복구된다는 보장은 없다. 이번 장의 목표는 실전 주문을 켜는 것이 아니라, 필요한 검토가 빠지면 진행을 차단하는 구조와 확인 기록을 만드는 것이다.</p>
<p>이 책의 교육용 구현은 기본값이 <code>live_enabled = false</code>이며 외부 실전 주문 전송 코드를 제공하지 않는다. 모든 점검이 통과해도 로컬 안전 점검까지만 끝난다. 예제에는 실계좌 번호·주문 비밀번호·실전 주문 요청이 없다.</p>
<h2 id="live-environments">1. 실행 환경의 의미를 다시 확인하기</h2>
<p><code>simulation</code>, <code>paper</code>, 증권사 모의투자, 실전은 서로 다른 환경이다. 모드 문자열 하나를 바꾸어 같은 연결을 쓰는 구조를 피하고, 데이터 조회와 주문 권한도 분리한다.</p>
<table>
<colgroup>
<col style="width: 33%" />
<col style="width: 33%" />
<col style="width: 33%" />
</colgroup>
<thead>
<tr>
<th>환경</th>
<th>이번 책에서 하는 일</th>
<th>돈과 주문</th>
</tr>
</thead>
<tbody>
<tr>
<td>과거 시뮬레이션</td>
<td>확정 과거 가격으로 규칙 계산</td>
<td>외부 주문 없음</td>
</tr>
<tr>
<td>로컬 paper</td>
<td>가상 의도·체결·현금 원장</td>
<td>외부 주문 없음</td>
</tr>
<tr>
<td>증권사 모의투자</td>
<td>별도 API 환경과 조건 검토 필요</td>
<td>이번 실습에서 연결하지 않음</td>
</tr>
<tr>
<td>실전</td>
<td>별도 운영 설계·승인·검증 필요</td>
<td>이번 실습에서 전송 불가</td>
</tr>
</tbody>
</table>
<p>한국투자증권의 현재 API 문서와 공지는 <a href="https://apiportal.koreainvestment.com/intro">KIS Developers 공식 개발자센터</a>에서 확인한다. 모의투자 지원 범위, 인증·요청 규격, 호출 제한을 기억에 의존해 실전 코드에 복사하지 않는다. 이 책의 로컬 판정은 증권사의 서비스 승인이나 투자 적합성 검증을 대신하지 않는다.</p>
<h2 id="live-evidence">2. 실전 검토 전 필요한 증거를 정하기</h2>
<p>체크리스트는 “예” 표시를 모으는 문서가 아니다. 코드·설정·실행 기록과 연결된 증거 목록이다. 확인하지 않은 항목은 <code>NOT_EVALUATED</code>, 하나라도 실패한 항목은 <code>FAIL</code>로 두며 기본 상태를 통과로 만들지 않는다.</p>
<table>
<colgroup>
<col style="width: 33%" />
<col style="width: 33%" />
<col style="width: 33%" />
</colgroup>
<thead>
<tr>
<th>점검 영역</th>
<th>필요한 증거</th>
<th>차단 조건</th>
</tr>
</thead>
<tbody>
<tr>
<td>데이터</td>
<td>원본·캘린더·조정 정책·확정 시각</td>
<td>원인 불명 누락·미확정 가격</td>
</tr>
<tr>
<td>규칙</td>
<td>전략 버전·설정 해시·T+1 연결</td>
<td>설명과 코드 불일치</td>
</tr>
<tr>
<td>원장</td>
<td>현금·수량·주문·체결 대조</td>
<td>차이 또는 출처 불명 기록</td>
</tr>
<tr>
<td>반복 실행</td>
<td>중복·중단·재개 시험</td>
<td>중복 정산 또는 복구 미검사</td>
</tr>
<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>
<h2 id="live-defaults">3. 설정은 기본 차단으로 읽기</h2>
<p><code>config/safety.json</code> 예시는 다음과 같다. 값이 없거나 타입이 다르거나 모르는 모드가 들어오면 오류로 종료한다. 환경변수의 문자열 <code>"false"</code>를 Python의 참값으로 잘못 처리하지 않도록 JSON의 불리언을 엄격하게 검사한다.</p>
<div class="sourceCode" id="cb1"><pre class="sourceCode json"><code class="sourceCode json"><span id="cb1-1"><a href="#cb1-1" aria-hidden="true" tabindex="-1"></a><span class="fu">{</span></span>
<span id="cb1-2"><a href="#cb1-2" aria-hidden="true" tabindex="-1"></a>  <span class="dt">&quot;mode&quot;</span><span class="fu">:</span> <span class="st">&quot;paper&quot;</span><span class="fu">,</span></span>
<span id="cb1-3"><a href="#cb1-3" aria-hidden="true" tabindex="-1"></a>  <span class="dt">&quot;live_enabled&quot;</span><span class="fu">:</span> <span class="kw">false</span><span class="fu">,</span></span>
<span id="cb1-4"><a href="#cb1-4" aria-hidden="true" tabindex="-1"></a>  <span class="dt">&quot;order_transport&quot;</span><span class="fu">:</span> <span class="st">&quot;none&quot;</span><span class="fu">,</span></span>
<span id="cb1-5"><a href="#cb1-5" aria-hidden="true" tabindex="-1"></a>  <span class="dt">&quot;require_current_evidence&quot;</span><span class="fu">:</span> <span class="kw">true</span><span class="fu">,</span></span>
<span id="cb1-6"><a href="#cb1-6" aria-hidden="true" tabindex="-1"></a>  <span class="dt">&quot;block_on_unknown_state&quot;</span><span class="fu">:</span> <span class="kw">true</span><span class="fu">,</span></span>
<span id="cb1-7"><a href="#cb1-7" aria-hidden="true" tabindex="-1"></a>  <span class="dt">&quot;max_order_notional&quot;</span><span class="fu">:</span> <span class="st">&quot;100000.00&quot;</span><span class="fu">,</span></span>
<span id="cb1-8"><a href="#cb1-8" aria-hidden="true" tabindex="-1"></a>  <span class="dt">&quot;max_open_positions&quot;</span><span class="fu">:</span> <span class="dv">3</span><span class="fu">,</span></span>
<span id="cb1-9"><a href="#cb1-9" aria-hidden="true" tabindex="-1"></a>  <span class="dt">&quot;kill_switch_path&quot;</span><span class="fu">:</span> <span class="st">&quot;config/STOP_ALL&quot;</span></span>
<span id="cb1-10"><a href="#cb1-10" aria-hidden="true" tabindex="-1"></a><span class="fu">}</span></span></code></pre></div>
<p>한도는 의도 생성 전에 검사하고, 체결 또는 전송 직전에도 현재 상태로 다시 검사하는 항목이다. 다만 이 장의 한도는 로컬 가상 자금 시험에만 쓰인다. 한도를 작게 적었다고 실전 주문 전송을 안전하게 구현한 것이 아니다.</p>
<p><code>mode = live</code>를 오타로 쓴 상황, <code>live_enabled</code>를 문자열로 준 상황, 설정 파일을 읽지 못한 상황 모두 차단되어야 한다. 읽기 실패 시 “편의를 위해 기본 실전 설정 사용” 같은 대체 경로를 만들지 않는다.</p>
<h2 id="live-guard-code">4. 해제 기능 없는 교육용 차단 코드 만들기</h2>
<p>아래 코드는 모드 검증의 방향을 보여주는 최소 예제다. 실전 전송 함수나 주문 URL이 없으며, <code>live</code> 요청은 어떤 설정에서도 종료된다. 실제 서비스에 필요한 위험 통제 전체를 구현한 코드는 아니다.</p>
<div class="sourceCode" id="cb2"><pre class="sourceCode python"><code class="sourceCode python"><span id="cb2-1"><a href="#cb2-1" aria-hidden="true" tabindex="-1"></a><span class="kw">class</span> SafetyBlocked(<span class="pp">RuntimeError</span>):</span>
<span id="cb2-2"><a href="#cb2-2" aria-hidden="true" tabindex="-1"></a>    <span class="cf">pass</span></span>
<span id="cb2-3"><a href="#cb2-3" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb2-4"><a href="#cb2-4" aria-hidden="true" tabindex="-1"></a><span class="kw">def</span> validate_educational_mode(config):</span>
<span id="cb2-5"><a href="#cb2-5" aria-hidden="true" tabindex="-1"></a>    mode <span class="op">=</span> config.get(<span class="st">&quot;mode&quot;</span>)</span>
<span id="cb2-6"><a href="#cb2-6" aria-hidden="true" tabindex="-1"></a>    enabled <span class="op">=</span> config.get(<span class="st">&quot;live_enabled&quot;</span>)</span>
<span id="cb2-7"><a href="#cb2-7" aria-hidden="true" tabindex="-1"></a>    transport <span class="op">=</span> config.get(<span class="st">&quot;order_transport&quot;</span>)</span>
<span id="cb2-8"><a href="#cb2-8" aria-hidden="true" tabindex="-1"></a>    <span class="cf">if</span> <span class="bu">type</span>(enabled) <span class="kw">is</span> <span class="kw">not</span> <span class="bu">bool</span>:</span>
<span id="cb2-9"><a href="#cb2-9" aria-hidden="true" tabindex="-1"></a>        <span class="cf">raise</span> SafetyBlocked(<span class="st">&quot;live_enabled must be a JSON boolean&quot;</span>)</span>
<span id="cb2-10"><a href="#cb2-10" aria-hidden="true" tabindex="-1"></a>    <span class="cf">if</span> mode <span class="kw">not</span> <span class="kw">in</span> {<span class="st">&quot;simulation&quot;</span>, <span class="st">&quot;paper&quot;</span>, <span class="st">&quot;live&quot;</span>}:</span>
<span id="cb2-11"><a href="#cb2-11" aria-hidden="true" tabindex="-1"></a>        <span class="cf">raise</span> SafetyBlocked(<span class="st">&quot;unknown execution mode&quot;</span>)</span>
<span id="cb2-12"><a href="#cb2-12" aria-hidden="true" tabindex="-1"></a>    <span class="cf">if</span> mode <span class="op">==</span> <span class="st">&quot;live&quot;</span>:</span>
<span id="cb2-13"><a href="#cb2-13" aria-hidden="true" tabindex="-1"></a>        <span class="cf">raise</span> SafetyBlocked(<span class="st">&quot;live transport is not provided in this book&quot;</span>)</span>
<span id="cb2-14"><a href="#cb2-14" aria-hidden="true" tabindex="-1"></a>    <span class="cf">if</span> enabled <span class="kw">or</span> transport <span class="op">!=</span> <span class="st">&quot;none&quot;</span>:</span>
<span id="cb2-15"><a href="#cb2-15" aria-hidden="true" tabindex="-1"></a>        <span class="cf">raise</span> SafetyBlocked(<span class="st">&quot;external order configuration is forbidden&quot;</span>)</span>
<span id="cb2-16"><a href="#cb2-16" aria-hidden="true" tabindex="-1"></a>    <span class="cf">return</span> mode</span>
<span id="cb2-17"><a href="#cb2-17" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb2-18"><a href="#cb2-18" aria-hidden="true" tabindex="-1"></a><span class="kw">def</span> educational_order_entry(config, paper_engine, intent):</span>
<span id="cb2-19"><a href="#cb2-19" aria-hidden="true" tabindex="-1"></a>    mode <span class="op">=</span> validate_educational_mode(config)</span>
<span id="cb2-20"><a href="#cb2-20" aria-hidden="true" tabindex="-1"></a>    <span class="cf">if</span> mode <span class="op">!=</span> <span class="st">&quot;paper&quot;</span>:</span>
<span id="cb2-21"><a href="#cb2-21" aria-hidden="true" tabindex="-1"></a>        <span class="cf">raise</span> SafetyBlocked(<span class="st">&quot;paper order entry requires paper mode&quot;</span>)</span>
<span id="cb2-22"><a href="#cb2-22" aria-hidden="true" tabindex="-1"></a>    <span class="cf">return</span> paper_engine.record_local_intent(intent)</span></code></pre></div>
<p><code>paper_engine</code>에도 실제 HTTP 주문 호출을 넣지 않는다. 의존성을 로컬 원장 객체로 제한하고, 주문 연결 라이브러리를 가져오지 않는지 검사한다. 이름만 <code>paper_engine</code>인 실제 주문 클라이언트를 전달하면 설계 의도가 무너지므로 생성 위치와 의존성도 함께 검토한다.</p>
<p>검토 증거를 확인하는 절차는 이 함수 앞에 추가할 수 있다. 그러나 증거가 모두 <code>PASS</code>여도 이 예제의 <code>live</code> 차단은 그대로 유지한다. 이 장을 마친 뒤의 정상 상태는 <strong>실전 전송 기능 없음</strong>이다.</p>
<h2 id="live-audit">5. 통과 기록은 버전과 유효 범위를 묶기</h2>
<p><code>safety_checks</code>에는 개별 점검을, <code>safety_reviews</code>에는 전체 판단을 남긴다. 기록에는 <code>review_id</code>, 코드 버전, 설정 해시, 데이터 식별자, 생성 시각, 검토 범위, 증거 경로를 포함한다. 원래 기록을 덮어쓰지 않고 재검토를 새 기록으로 추가한다.</p>
<pre class="text"><code>review_id=demo-safety-01 scope=educational_local_paper
code_version=demo-v1 strategy_version=cci20_cross_up_m100_v1
check=ledger_reconciliation result=PASS evidence=reconcile-demo.json
check=restart_after_commit result=PASS evidence=recovery-demo.json
check=live_transport_available result=NOT_PROVIDED
decision=LOCAL_PAPER_ONLY external_orders_permitted=false</code></pre>
<p>설정이나 코드가 바뀌면 이전 검토의 해시와 달라진다. 그 경우 새 실행을 허용할지 다시 검토한다. 체크리스트를 한 번 작성하고 영구 허가증으로 사용하지 않는다. 오래된 증거의 허용 기간도 운영 문서에서 정하되 유효 기간만 지나지 않았다는 이유로 현재 상태 점검을 생략하지 않는다.</p>
<p>별도 실전 시스템을 설계한다면 실제 주문 접수·체결·잔고 조회·정정·취소·응답 유실의 정합성을 각각 검토해야 한다. 이번 원고에는 그 연결이 없으므로 관련 항목을 완료했다고 표시하지 않는다.</p>
<h2 id="live-unknown-state">6. 응답이 없을 때 재주문하지 않는 이유 이해하기</h2>
<p>로컬 원장에서는 같은 DB 트랜잭션으로 상태를 묶을 수 있었다. 외부 주문 시스템은 네트워크 너머에 있으므로 요청 결과를 받지 못했어도 주문이 접수되었을 가능성이 있다. 단순 재시도는 중복 주문을 만들 수 있다.</p>
<p>실전 연결을 별도로 개발할 때 필요한 상태는 <code>SUBMISSION_UNKNOWN</code>, 대조 절차, 재주문 금지 규칙이다. 실제 주문 내역과 체결 내역을 조회해 결과를 확인하기 전에는 “실패했으니 다시 보내기”를 하지 않아야 한다. 이번 교육용 코드에서는 외부 전송 자체를 막아 이 상황을 만들지 않는다.</p>
<p>가상 시험으로는 외부 서버를 호출하지 않고 “결과 불명” 사건을 주입할 수 있다. 기대 결과는 새 의도 생성 차단, 기존 상태 보존, 담당자 확인 요청 기록이다. 가상 시험이 실제 증권사 API의 동작을 검증한 것은 아니라고 보고서에 쓴다.</p>
<h2 id="live-stop">7. 중단 수단과 복구 수단을 함께 준비하기</h2>
<p>중단 파일 <code>config/STOP_ALL</code>은 새 원장 변경 전에 확인한다. 모니터링 화면에 중단 버튼이 있더라도 프로그램이 실제로 신호를 읽고 종료하는지 시험한다. 화면에 “중단”이라고 보이는 것과 진행 중 작업이 정지한 것은 다를 수 있다.</p>
<p>중단 시 로그에는 마지막 확정 단계, 미완료 의도 수, 예약현금, 마지막 대조 시각을 남긴다. 예제의 중단은 가상 포지션 청산을 의미하지 않는다. 유지된 포지션과 미처리 이벤트가 무엇인지 보고하고 이후 대조 절차를 거친다.</p>
<p>복구할 수 있는 백업과 재현 자료가 있어야 중단 뒤 상태를 확인할 수 있다. 비밀값을 제거한 코드·설정 예제·스키마·실행 증거를 보관하고, DB 복구는 별도 테스트 환경에서 시험한다. 백업 파일의 존재와 복구 성공을 구분한다.</p>
<h2 id="live-codex">8. Codex에는 차단 점검을 요청하기</h2>
<pre class="text"><code>교육용 자동매매 프로젝트의 안전 기본값과 차단 경로를 점검해줘.
실전 주문 전송 코드는 추가하거나 실행하지 마.
mode 누락, 오타, 문자열 false, live 모드, 외부 transport 설정을 모두 차단해.
paper 경로가 로컬 원장만 사용하며 실제 주문 클라이언트를 읽지 않는지 확인해.
중단 파일·오래된 증거·설정 해시 변경·대조 불일치를 가상 데이터로 시험해.
확인한 증거와 미검사 항목을 구분하고 결론은 LOCAL_PAPER_ONLY로 남겨.
계좌번호·토큰·비밀값을 읽거나 로그에 표시하지 마.</code></pre>
<p>학습용 시험에서 정상인 결과는 <code>mode=live → SafetyBlocked</code>다. 모든 체크리스트가 채워졌다는 이유로 전송 기능을 자동 생성하거나 실전 설정을 채우지 않는다. 별도 개발이 필요한 항목은 문서에 남긴다.</p>
<h2 id="live-done">9. 완료 기준과 문제 해결</h2>
<p>완료 기준은 기본 설정이 로컬 paper이고, 누락·오타·실전 요청이 차단되며, 점검 기록이 코드·설정 버전과 연결되는 것이다. 이번 장의 산출물은 체크리스트와 차단 시험 결과이며 실제 주문 성공 기록이 아니다.</p>
<table>
<colgroup>
<col style="width: 50%" />
<col style="width: 50%" />
</colgroup>
<thead>
<tr>
<th>증상</th>
<th>먼저 확인할 것</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>"false"</code>인데 활성화됨</td>
<td>문자열의 참값 변환과 엄격한 타입 검사</td>
</tr>
<tr>
<td>모드 오타가 paper로 대체됨</td>
<td>허용 목록과 실패 시 종료 처리</td>
</tr>
<tr>
<td>이전 통과가 변경 코드에도 적용됨</td>
<td>코드 버전·설정 해시 대조</td>
</tr>
<tr>
<td>중단 버튼 후에도 처리가 진행됨</td>
<td>변경 직전 중단 확인과 진행 단계 표시</td>
</tr>
<tr>
<td>종합 결과가 “실전 준비 완료”임</td>
<td>범위에 맞는 <code>LOCAL_PAPER_ONLY</code> 판정</td>
</tr>
</tbody>
</table>
<p>다음 장에서는 이 제한을 유지하면서 운영 기록과 변경 검토를 반복하는 방법을 정리한다.</p>
</div>
          <section class="pk-related" aria-labelledby="related-heading"><p class="pk-eyebrow">FROM CONCEPT TO PRACTICE</p><h2 id="related-heading">구현 기록과 함께 읽기</h2><ul><li><a href="https://mediamaak.com/books/auto-trading/repeat-stop/">반복 실행과 중단 조건 만들기</a></li></ul></section>
          <nav class="pk-pager" aria-label="이전 장과 다음 장"><a href="https://mediamaak.com/books/auto-trading/repeat-stop/">이전 글: 반복 실행과 중단 조건 만들기</a><a href="https://mediamaak.com/books/auto-trading/operations-review/">다음 글: 운영 기록을 남기고 개선하기</a></nav>
          <div class="pk-share"><div><p class="pk-share-title">학습의 다음 단계를 남겨 두세요</p><p>공유 링크를 복사해 이어서 읽을 수 있습니다.</p></div><div><button type="button" data-share-content>공유 링크 복사</button><span role="status" data-share-status></span></div></div>
        </div>
      </div>
    </article>]]></content:encoded>
    </item>
    <item>
      <title>코스피200 데이터 수집하기</title>
      <link>https://mediamaak.com/books/auto-trading/kospi200-data/</link>
      <guid isPermaLink="true">https://mediamaak.com/books/auto-trading/kospi200-data/</guid>
      <pubDate>Tue, 20 Oct 2026 00:00:00 +0900</pubDate>
      <description>종목 목록과 일봉 데이터를 수집하고 누락·중복·날짜를 점검합니다.</description>
      <content:encoded><![CDATA[<article class="pk-article" data-content-id="/books/auto-trading/kospi200-data/" data-content-type="wiki-chapter" data-published-at="2026-10-20T00:00:00+09:00" data-updated-at="2026-10-20T10:36:01+09:00">
      <header class="pk-hero">
        <p class="pk-eyebrow">4 / 12 · AUTO TRADING WIKI</p>
        <h1>코스피200 데이터 수집하기</h1>
        <p class="pk-summary">종목 목록과 일봉 데이터를 수집하고 누락·중복·날짜를 점검합니다.</p>
        <div class="pk-meta"><dl><dt>발행</dt><dd><time datetime="2026-10-20T00:00:00+09:00">2026-10-20</time></dd><dt>수정</dt><dd><time datetime="2026-10-20T10:36:01+09:00">2026-10-20</time></dd><dt>문서 검토</dt><dd>2026-10-20</dd><dt>본문 버전</dt><dd>1.1.1-web.20261020</dd></dl></div>
      </header>
      <div class="book-reading-content">
        <div class="pk-body-column">
          <div class="pk-prose"><p>시세 한 건을 조회했다면 다음 목표는 계산에 쓸 일봉 자료를 일정한 형식으로 모으는 것이다. 이 장에서는 <strong>대상 종목 목록 고정 → 기간별 시세 조회 → 검증 → CSV와 실행 요약 저장</strong> 순서로 진행한다. 먼저 실제 종목 1개, 다음 5개를 확인한 뒤 범위를 넓힌다.</p>
<p>이 책의 CSV와 다음 장의 SQL은 <code>symbol, trade_date, open, high, low, close, volume</code>이라는 공통 컬럼을 사용한다. 모든 날짜는 거래소의 거래일 기준이며, 일봉은 해당 거래일의 수집을 마친 데이터만 사용한다.</p>
<h2 id="universe-snapshot">1. 코스피200 목록의 기준일 보관하기</h2>
<p>코스피200은 분석 범위를 일정하게 정하기 위한 출발점이다. 구성 종목은 바뀔 수 있다. 현재 구성 종목만 골라 과거 성과를 계산하면 당시 제외되었거나 상장폐지된 종목을 놓치는 <strong>생존 편향</strong>이 생길 수 있다. 이 문제는 수집 성공 여부와 별도로 기록한다.</p>
<p><a href="https://data.krx.co.kr/contents/MDC/MAIN/main/index.cmd">KRX Data Marketplace</a>에서 지수의 구성 종목과 조회 기준일을 확인하고, 제공되는 내려받기 기능으로 원본을 보관한다. 화면의 메뉴와 다운로드 조건은 바뀔 수 있으므로 비공식 URL이나 로그인 우회 방식에 의존하지 않는다.</p>
<p>원본을 <code>outputs/universe/raw/</code>에 두고, 프로젝트가 읽는 <code>outputs/universe/kospi200.csv</code>를 다음 컬럼으로 정리한다.</p>
<table>
<colgroup>
<col style="width: 33%" />
<col style="width: 33%" />
<col style="width: 33%" />
</colgroup>
<thead>
<tr>
<th>컬럼</th>
<th>예시 형식</th>
<th>의미</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>symbol</code></td>
<td><code>005930</code>, <code>0126Z0</code>처럼 여섯 자리 문자열</td>
<td>ASCII 숫자·영문 대문자를 원본대로 보존한다</td>
</tr>
<tr>
<td><code>name</code></td>
<td>내려받은 종목명</td>
<td>표시용 이름이며 식별 키로 쓰지 않는다</td>
</tr>
<tr>
<td><code>snapshot_date</code></td>
<td><code>YYYY-MM-DD</code></td>
<td>구성 종목 목록의 조회 기준일</td>
</tr>
<tr>
<td><code>source</code></td>
<td>출처 URL 또는 원본 파일명</td>
<td>목록을 확인한 근거</td>
</tr>
</tbody>
</table>
<p>종목코드는 숫자만 있다고 가정하지 않는다. 예를 들어 <a href="https://www.samsungepisholdings.com/investment/investment01.do">삼성에피스홀딩스 공식 주식정보</a>는 <code>0126Z0</code>을 사용한다. 이 책은 <code>[A-Z0-9]{6}</code>을 공통 형식으로 사용하며, 소문자·Unicode 유사 문자는 거부한다.</p>
<p>종목 수가 예상과 다르면 단순히 200개에 맞추려고 행을 삭제하지 않는다. 조회한 지수·기준일·중복·빈 코드를 확인한다. 현재 목록만 확보한 상태라면 결과 문서에도 “현재 구성 종목을 과거에 적용한 탐색”이라고 명시한다.</p>
<h2 id="data-contract">2. 일봉 조회의 기준 정하기</h2>
<p>기간별 일봉은 고가·저가·종가뿐 아니라 다음 장에서 사용할 시가도 함께 저장한다. 거래량 0은 가능하지만 가격이 0인 행을 정상 거래일 가격으로 임의 보정하지 않는다. 거래 정지나 공급자 표시 방식인지 확인하고 별도 점검 대상으로 둔다.</p>
<p>이번 실습의 기준은 다음과 같다.</p>
<ul>
<li>시장 구분 <code>J</code>: KRX 기준 시세</li>
<li>기간 구분 <code>D</code>: 일봉</li>
<li>가격 구분 <code>0</code>: 수정주가</li>
<li>한 행의 식별자: <code>(symbol, trade_date)</code></li>
<li>계산 순서: 종목별 거래일 오름차순</li>
</ul>
<p>이 값과 응답 필드는 <a href="https://github.com/koreainvestment/open-trading-api/blob/main/examples_llm/domestic_stock/inquire_daily_itemchartprice/inquire_daily_itemchartprice.py">한국투자증권 공식 기간별시세 예제</a>를 기준으로 했다. 공식 예제는 한 번의 호출에서 최대 100건을 안내한다. 여기서는 달력 기준 80일 이하로 구간을 잘라 그 제한 안에서 요청한다. 긴 기간을 한 번 호출하고 전체를 받았다고 가정하지 않는다.</p>
<p>수정주가는 기업행사에 따라 과거 값이 바뀔 수 있다. 수집 일시, 가격 구분과 원본을 함께 기록한다. 일봉 지표 계산과 주문 수량·현금 정산의 가격 체계가 다르면 시뮬레이션도 달라지므로, 실제 체결을 재현하는 단계에서는 기업행사 처리까지 별도로 설계한다.</p>
<h2 id="normalize-prices">3. 응답을 공통 컬럼으로 정리하기</h2>
<p><code>src/collect_daily.py</code>에 다음 내용을 작성한다. 3장의 <code>ReadOnlyKis</code>를 그대로 사용한다. 공급자 필드와 프로젝트 필드를 바꾸는 부분을 별도 함수로 두면 가짜 응답으로도 검증할 수 있다.</p>
<div class="sourceCode" id="cb1"><pre class="sourceCode python"><code class="sourceCode python"><span id="cb1-1"><a href="#cb1-1" aria-hidden="true" tabindex="-1"></a><span class="im">import</span> argparse</span>
<span id="cb1-2"><a href="#cb1-2" aria-hidden="true" tabindex="-1"></a><span class="im">from</span> datetime <span class="im">import</span> date, timedelta</span>
<span id="cb1-3"><a href="#cb1-3" aria-hidden="true" tabindex="-1"></a><span class="im">from</span> pathlib <span class="im">import</span> Path</span>
<span id="cb1-4"><a href="#cb1-4" aria-hidden="true" tabindex="-1"></a><span class="im">import</span> time</span>
<span id="cb1-5"><a href="#cb1-5" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb1-6"><a href="#cb1-6" aria-hidden="true" tabindex="-1"></a><span class="im">import</span> pandas <span class="im">as</span> pd</span>
<span id="cb1-7"><a href="#cb1-7" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb1-8"><a href="#cb1-8" aria-hidden="true" tabindex="-1"></a><span class="im">from</span> src.kis_readonly <span class="im">import</span> ROOT, ReadOnlyKis, load_config</span>
<span id="cb1-9"><a href="#cb1-9" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb1-10"><a href="#cb1-10" aria-hidden="true" tabindex="-1"></a>FIELDS <span class="op">=</span> {</span>
<span id="cb1-11"><a href="#cb1-11" aria-hidden="true" tabindex="-1"></a>    <span class="st">&quot;stck_bsop_date&quot;</span>: <span class="st">&quot;trade_date&quot;</span>,</span>
<span id="cb1-12"><a href="#cb1-12" aria-hidden="true" tabindex="-1"></a>    <span class="st">&quot;stck_oprc&quot;</span>: <span class="st">&quot;open&quot;</span>,</span>
<span id="cb1-13"><a href="#cb1-13" aria-hidden="true" tabindex="-1"></a>    <span class="st">&quot;stck_hgpr&quot;</span>: <span class="st">&quot;high&quot;</span>,</span>
<span id="cb1-14"><a href="#cb1-14" aria-hidden="true" tabindex="-1"></a>    <span class="st">&quot;stck_lwpr&quot;</span>: <span class="st">&quot;low&quot;</span>,</span>
<span id="cb1-15"><a href="#cb1-15" aria-hidden="true" tabindex="-1"></a>    <span class="st">&quot;stck_clpr&quot;</span>: <span class="st">&quot;close&quot;</span>,</span>
<span id="cb1-16"><a href="#cb1-16" aria-hidden="true" tabindex="-1"></a>    <span class="st">&quot;acml_vol&quot;</span>: <span class="st">&quot;volume&quot;</span>,</span>
<span id="cb1-17"><a href="#cb1-17" aria-hidden="true" tabindex="-1"></a>}</span>
<span id="cb1-18"><a href="#cb1-18" aria-hidden="true" tabindex="-1"></a>PRICE_COLUMNS <span class="op">=</span> [<span class="st">&quot;open&quot;</span>, <span class="st">&quot;high&quot;</span>, <span class="st">&quot;low&quot;</span>, <span class="st">&quot;close&quot;</span>]</span>
<span id="cb1-19"><a href="#cb1-19" aria-hidden="true" tabindex="-1"></a>COLUMNS <span class="op">=</span> [<span class="st">&quot;symbol&quot;</span>, <span class="st">&quot;trade_date&quot;</span>, <span class="op">*</span>PRICE_COLUMNS, <span class="st">&quot;volume&quot;</span>]</span>
<span id="cb1-20"><a href="#cb1-20" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb1-21"><a href="#cb1-21" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb1-22"><a href="#cb1-22" aria-hidden="true" tabindex="-1"></a><span class="kw">def</span> normalize(symbol, records):</span>
<span id="cb1-23"><a href="#cb1-23" aria-hidden="true" tabindex="-1"></a>    <span class="cf">if</span> <span class="kw">not</span> records:</span>
<span id="cb1-24"><a href="#cb1-24" aria-hidden="true" tabindex="-1"></a>        <span class="cf">raise</span> <span class="pp">ValueError</span>(<span class="st">&quot;No daily records returned&quot;</span>)</span>
<span id="cb1-25"><a href="#cb1-25" aria-hidden="true" tabindex="-1"></a>    frame <span class="op">=</span> pd.DataFrame(records)</span>
<span id="cb1-26"><a href="#cb1-26" aria-hidden="true" tabindex="-1"></a>    missing <span class="op">=</span> <span class="bu">set</span>(FIELDS) <span class="op">-</span> <span class="bu">set</span>(frame.columns)</span>
<span id="cb1-27"><a href="#cb1-27" aria-hidden="true" tabindex="-1"></a>    <span class="cf">if</span> missing:</span>
<span id="cb1-28"><a href="#cb1-28" aria-hidden="true" tabindex="-1"></a>        <span class="cf">raise</span> <span class="pp">ValueError</span>(<span class="ss">f&quot;Missing daily fields: </span><span class="sc">{</span><span class="bu">sorted</span>(missing)<span class="sc">}</span><span class="ss">&quot;</span>)</span>
<span id="cb1-29"><a href="#cb1-29" aria-hidden="true" tabindex="-1"></a>    frame <span class="op">=</span> frame.loc[frame[<span class="st">&quot;stck_bsop_date&quot;</span>].fillna(<span class="st">&quot;&quot;</span>).ne(<span class="st">&quot;&quot;</span>), <span class="bu">list</span>(FIELDS)]</span>
<span id="cb1-30"><a href="#cb1-30" aria-hidden="true" tabindex="-1"></a>    frame <span class="op">=</span> frame.rename(columns<span class="op">=</span>FIELDS).copy()</span>
<span id="cb1-31"><a href="#cb1-31" aria-hidden="true" tabindex="-1"></a>    <span class="cf">if</span> frame.empty:</span>
<span id="cb1-32"><a href="#cb1-32" aria-hidden="true" tabindex="-1"></a>        <span class="cf">raise</span> <span class="pp">ValueError</span>(<span class="st">&quot;All returned dates are empty&quot;</span>)</span>
<span id="cb1-33"><a href="#cb1-33" aria-hidden="true" tabindex="-1"></a>    frame[<span class="st">&quot;trade_date&quot;</span>] <span class="op">=</span> pd.to_datetime(</span>
<span id="cb1-34"><a href="#cb1-34" aria-hidden="true" tabindex="-1"></a>        frame[<span class="st">&quot;trade_date&quot;</span>], <span class="bu">format</span><span class="op">=</span><span class="st">&quot;%Y%m</span><span class="sc">%d</span><span class="st">&quot;</span>, errors<span class="op">=</span><span class="st">&quot;raise&quot;</span></span>
<span id="cb1-35"><a href="#cb1-35" aria-hidden="true" tabindex="-1"></a>    ).dt.date</span>
<span id="cb1-36"><a href="#cb1-36" aria-hidden="true" tabindex="-1"></a>    <span class="cf">for</span> column <span class="kw">in</span> [<span class="op">*</span>PRICE_COLUMNS, <span class="st">&quot;volume&quot;</span>]:</span>
<span id="cb1-37"><a href="#cb1-37" aria-hidden="true" tabindex="-1"></a>        values <span class="op">=</span> pd.to_numeric(frame[column], errors<span class="op">=</span><span class="st">&quot;raise&quot;</span>)</span>
<span id="cb1-38"><a href="#cb1-38" aria-hidden="true" tabindex="-1"></a>        <span class="cf">if</span> values.isna().<span class="bu">any</span>() <span class="kw">or</span> values.mod(<span class="dv">1</span>).ne(<span class="dv">0</span>).<span class="bu">any</span>():</span>
<span id="cb1-39"><a href="#cb1-39" aria-hidden="true" tabindex="-1"></a>            <span class="cf">raise</span> <span class="pp">ValueError</span>(<span class="ss">f&quot;Non-integer value in </span><span class="sc">{</span>column<span class="sc">}</span><span class="ss">&quot;</span>)</span>
<span id="cb1-40"><a href="#cb1-40" aria-hidden="true" tabindex="-1"></a>        frame[column] <span class="op">=</span> values.astype(<span class="st">&quot;int64&quot;</span>)</span>
<span id="cb1-41"><a href="#cb1-41" aria-hidden="true" tabindex="-1"></a>    invalid <span class="op">=</span> (</span>
<span id="cb1-42"><a href="#cb1-42" aria-hidden="true" tabindex="-1"></a>        frame[PRICE_COLUMNS].le(<span class="dv">0</span>).<span class="bu">any</span>(axis<span class="op">=</span><span class="dv">1</span>)</span>
<span id="cb1-43"><a href="#cb1-43" aria-hidden="true" tabindex="-1"></a>        <span class="op">|</span> frame[<span class="st">&quot;volume&quot;</span>].lt(<span class="dv">0</span>)</span>
<span id="cb1-44"><a href="#cb1-44" aria-hidden="true" tabindex="-1"></a>        <span class="op">|</span> frame[<span class="st">&quot;high&quot;</span>].lt(frame[[<span class="st">&quot;open&quot;</span>, <span class="st">&quot;close&quot;</span>, <span class="st">&quot;low&quot;</span>]].<span class="bu">max</span>(axis<span class="op">=</span><span class="dv">1</span>))</span>
<span id="cb1-45"><a href="#cb1-45" aria-hidden="true" tabindex="-1"></a>        <span class="op">|</span> frame[<span class="st">&quot;low&quot;</span>].gt(frame[[<span class="st">&quot;open&quot;</span>, <span class="st">&quot;close&quot;</span>, <span class="st">&quot;high&quot;</span>]].<span class="bu">min</span>(axis<span class="op">=</span><span class="dv">1</span>))</span>
<span id="cb1-46"><a href="#cb1-46" aria-hidden="true" tabindex="-1"></a>    )</span>
<span id="cb1-47"><a href="#cb1-47" aria-hidden="true" tabindex="-1"></a>    <span class="cf">if</span> invalid.<span class="bu">any</span>():</span>
<span id="cb1-48"><a href="#cb1-48" aria-hidden="true" tabindex="-1"></a>        <span class="cf">raise</span> <span class="pp">ValueError</span>(<span class="ss">f&quot;Invalid OHLCV rows: </span><span class="sc">{</span><span class="bu">int</span>(invalid.<span class="bu">sum</span>())<span class="sc">}</span><span class="ss">&quot;</span>)</span>
<span id="cb1-49"><a href="#cb1-49" aria-hidden="true" tabindex="-1"></a>    frame[<span class="st">&quot;symbol&quot;</span>] <span class="op">=</span> symbol</span>
<span id="cb1-50"><a href="#cb1-50" aria-hidden="true" tabindex="-1"></a>    frame <span class="op">=</span> frame.drop_duplicates()</span>
<span id="cb1-51"><a href="#cb1-51" aria-hidden="true" tabindex="-1"></a>    <span class="cf">if</span> frame.duplicated([<span class="st">&quot;symbol&quot;</span>, <span class="st">&quot;trade_date&quot;</span>]).<span class="bu">any</span>():</span>
<span id="cb1-52"><a href="#cb1-52" aria-hidden="true" tabindex="-1"></a>        <span class="cf">raise</span> <span class="pp">ValueError</span>(<span class="st">&quot;Conflicting rows for the same symbol and date&quot;</span>)</span>
<span id="cb1-53"><a href="#cb1-53" aria-hidden="true" tabindex="-1"></a>    <span class="cf">return</span> frame[COLUMNS].sort_values([<span class="st">&quot;symbol&quot;</span>, <span class="st">&quot;trade_date&quot;</span>]).reset_index(drop<span class="op">=</span><span class="va">True</span>)</span>
<span id="cb1-54"><a href="#cb1-54" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb1-55"><a href="#cb1-55" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb1-56"><a href="#cb1-56" aria-hidden="true" tabindex="-1"></a><span class="kw">def</span> collect_symbol(client, symbol, start, end):</span>
<span id="cb1-57"><a href="#cb1-57" aria-hidden="true" tabindex="-1"></a>    records <span class="op">=</span> []</span>
<span id="cb1-58"><a href="#cb1-58" aria-hidden="true" tabindex="-1"></a>    cursor <span class="op">=</span> start</span>
<span id="cb1-59"><a href="#cb1-59" aria-hidden="true" tabindex="-1"></a>    <span class="cf">while</span> cursor <span class="op">&lt;=</span> end:</span>
<span id="cb1-60"><a href="#cb1-60" aria-hidden="true" tabindex="-1"></a>        window_end <span class="op">=</span> <span class="bu">min</span>(cursor <span class="op">+</span> timedelta(days<span class="op">=</span><span class="dv">79</span>), end)</span>
<span id="cb1-61"><a href="#cb1-61" aria-hidden="true" tabindex="-1"></a>        body <span class="op">=</span> client.quotations(<span class="st">&quot;daily&quot;</span>, {</span>
<span id="cb1-62"><a href="#cb1-62" aria-hidden="true" tabindex="-1"></a>            <span class="st">&quot;FID_COND_MRKT_DIV_CODE&quot;</span>: <span class="st">&quot;J&quot;</span>,</span>
<span id="cb1-63"><a href="#cb1-63" aria-hidden="true" tabindex="-1"></a>            <span class="st">&quot;FID_INPUT_ISCD&quot;</span>: symbol,</span>
<span id="cb1-64"><a href="#cb1-64" aria-hidden="true" tabindex="-1"></a>            <span class="st">&quot;FID_INPUT_DATE_1&quot;</span>: cursor.strftime(<span class="st">&quot;%Y%m</span><span class="sc">%d</span><span class="st">&quot;</span>),</span>
<span id="cb1-65"><a href="#cb1-65" aria-hidden="true" tabindex="-1"></a>            <span class="st">&quot;FID_INPUT_DATE_2&quot;</span>: window_end.strftime(<span class="st">&quot;%Y%m</span><span class="sc">%d</span><span class="st">&quot;</span>),</span>
<span id="cb1-66"><a href="#cb1-66" aria-hidden="true" tabindex="-1"></a>            <span class="st">&quot;FID_PERIOD_DIV_CODE&quot;</span>: <span class="st">&quot;D&quot;</span>,</span>
<span id="cb1-67"><a href="#cb1-67" aria-hidden="true" tabindex="-1"></a>            <span class="st">&quot;FID_ORG_ADJ_PRC&quot;</span>: <span class="st">&quot;0&quot;</span>,</span>
<span id="cb1-68"><a href="#cb1-68" aria-hidden="true" tabindex="-1"></a>        })</span>
<span id="cb1-69"><a href="#cb1-69" aria-hidden="true" tabindex="-1"></a>        output <span class="op">=</span> body.get(<span class="st">&quot;output2&quot;</span>)</span>
<span id="cb1-70"><a href="#cb1-70" aria-hidden="true" tabindex="-1"></a>        <span class="cf">if</span> <span class="kw">not</span> <span class="bu">isinstance</span>(output, <span class="bu">list</span>):</span>
<span id="cb1-71"><a href="#cb1-71" aria-hidden="true" tabindex="-1"></a>            <span class="cf">raise</span> <span class="pp">ValueError</span>(<span class="st">&quot;output2 must be a list&quot;</span>)</span>
<span id="cb1-72"><a href="#cb1-72" aria-hidden="true" tabindex="-1"></a>        records.extend(output)</span>
<span id="cb1-73"><a href="#cb1-73" aria-hidden="true" tabindex="-1"></a>        cursor <span class="op">=</span> window_end <span class="op">+</span> timedelta(days<span class="op">=</span><span class="dv">1</span>)</span>
<span id="cb1-74"><a href="#cb1-74" aria-hidden="true" tabindex="-1"></a>        time.sleep(<span class="fl">1.0</span>)</span>
<span id="cb1-75"><a href="#cb1-75" aria-hidden="true" tabindex="-1"></a>    frame <span class="op">=</span> normalize(symbol, records)</span>
<span id="cb1-76"><a href="#cb1-76" aria-hidden="true" tabindex="-1"></a>    <span class="cf">if</span> frame[<span class="st">&quot;trade_date&quot;</span>].lt(start).<span class="bu">any</span>() <span class="kw">or</span> frame[<span class="st">&quot;trade_date&quot;</span>].gt(end).<span class="bu">any</span>():</span>
<span id="cb1-77"><a href="#cb1-77" aria-hidden="true" tabindex="-1"></a>        <span class="cf">raise</span> <span class="pp">ValueError</span>(<span class="st">&quot;The response contains a date outside the requested range&quot;</span>)</span>
<span id="cb1-78"><a href="#cb1-78" aria-hidden="true" tabindex="-1"></a>    <span class="cf">return</span> frame</span>
<span id="cb1-79"><a href="#cb1-79" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb1-80"><a href="#cb1-80" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb1-81"><a href="#cb1-81" aria-hidden="true" tabindex="-1"></a><span class="kw">def</span> main():</span>
<span id="cb1-82"><a href="#cb1-82" aria-hidden="true" tabindex="-1"></a>    parser <span class="op">=</span> argparse.ArgumentParser()</span>
<span id="cb1-83"><a href="#cb1-83" aria-hidden="true" tabindex="-1"></a>    parser.add_argument(<span class="st">&quot;--start&quot;</span>, <span class="bu">type</span><span class="op">=</span>date.fromisoformat, required<span class="op">=</span><span class="va">True</span>)</span>
<span id="cb1-84"><a href="#cb1-84" aria-hidden="true" tabindex="-1"></a>    parser.add_argument(<span class="st">&quot;--end&quot;</span>, <span class="bu">type</span><span class="op">=</span>date.fromisoformat, required<span class="op">=</span><span class="va">True</span>)</span>
<span id="cb1-85"><a href="#cb1-85" aria-hidden="true" tabindex="-1"></a>    parser.add_argument(<span class="st">&quot;--limit&quot;</span>, <span class="bu">type</span><span class="op">=</span><span class="bu">int</span>, default<span class="op">=</span><span class="dv">1</span>)</span>
<span id="cb1-86"><a href="#cb1-86" aria-hidden="true" tabindex="-1"></a>    args <span class="op">=</span> parser.parse_args()</span>
<span id="cb1-87"><a href="#cb1-87" aria-hidden="true" tabindex="-1"></a>    <span class="cf">if</span> args.start <span class="op">&gt;</span> args.end <span class="kw">or</span> args.limit <span class="op">&lt;</span> <span class="dv">1</span>:</span>
<span id="cb1-88"><a href="#cb1-88" aria-hidden="true" tabindex="-1"></a>        parser.error(<span class="st">&quot;Check the date range and limit&quot;</span>)</span>
<span id="cb1-89"><a href="#cb1-89" aria-hidden="true" tabindex="-1"></a>    universe <span class="op">=</span> pd.read_csv(</span>
<span id="cb1-90"><a href="#cb1-90" aria-hidden="true" tabindex="-1"></a>        ROOT <span class="op">/</span> <span class="st">&quot;outputs/universe/kospi200.csv&quot;</span>, dtype<span class="op">=</span>{<span class="st">&quot;symbol&quot;</span>: <span class="st">&quot;string&quot;</span>}</span>
<span id="cb1-91"><a href="#cb1-91" aria-hidden="true" tabindex="-1"></a>    )</span>
<span id="cb1-92"><a href="#cb1-92" aria-hidden="true" tabindex="-1"></a>    symbols <span class="op">=</span> universe[<span class="st">&quot;symbol&quot;</span>]</span>
<span id="cb1-93"><a href="#cb1-93" aria-hidden="true" tabindex="-1"></a>    <span class="cf">if</span> symbols.isna().<span class="bu">any</span>() <span class="kw">or</span> <span class="kw">not</span> symbols.<span class="bu">str</span>.fullmatch(<span class="vs">r&quot;</span><span class="pp">[A-Z0-9]</span><span class="op">{6}</span><span class="vs">&quot;</span>).<span class="bu">all</span>():</span>
<span id="cb1-94"><a href="#cb1-94" aria-hidden="true" tabindex="-1"></a>        <span class="cf">raise</span> <span class="pp">ValueError</span>(<span class="st">&quot;Universe contains invalid symbols&quot;</span>)</span>
<span id="cb1-95"><a href="#cb1-95" aria-hidden="true" tabindex="-1"></a>    <span class="cf">if</span> symbols.duplicated().<span class="bu">any</span>():</span>
<span id="cb1-96"><a href="#cb1-96" aria-hidden="true" tabindex="-1"></a>        <span class="cf">raise</span> <span class="pp">ValueError</span>(<span class="st">&quot;Universe contains duplicate symbols&quot;</span>)</span>
<span id="cb1-97"><a href="#cb1-97" aria-hidden="true" tabindex="-1"></a>    target <span class="op">=</span> ROOT <span class="op">/</span> <span class="st">&quot;outputs/daily&quot;</span></span>
<span id="cb1-98"><a href="#cb1-98" aria-hidden="true" tabindex="-1"></a>    target.mkdir(parents<span class="op">=</span><span class="va">True</span>, exist_ok<span class="op">=</span><span class="va">True</span>)</span>
<span id="cb1-99"><a href="#cb1-99" aria-hidden="true" tabindex="-1"></a>    manifest <span class="op">=</span> []</span>
<span id="cb1-100"><a href="#cb1-100" aria-hidden="true" tabindex="-1"></a>    client <span class="op">=</span> ReadOnlyKis(load_config())</span>
<span id="cb1-101"><a href="#cb1-101" aria-hidden="true" tabindex="-1"></a>    <span class="cf">try</span>:</span>
<span id="cb1-102"><a href="#cb1-102" aria-hidden="true" tabindex="-1"></a>        <span class="cf">for</span> symbol <span class="kw">in</span> symbols.head(args.limit):</span>
<span id="cb1-103"><a href="#cb1-103" aria-hidden="true" tabindex="-1"></a>            <span class="cf">try</span>:</span>
<span id="cb1-104"><a href="#cb1-104" aria-hidden="true" tabindex="-1"></a>                frame <span class="op">=</span> collect_symbol(client, symbol, args.start, args.end)</span>
<span id="cb1-105"><a href="#cb1-105" aria-hidden="true" tabindex="-1"></a>                name <span class="op">=</span> <span class="ss">f&quot;</span><span class="sc">{</span>symbol<span class="sc">}</span><span class="ss">_</span><span class="sc">{</span>args<span class="sc">.</span>start<span class="sc">}</span><span class="ss">_</span><span class="sc">{</span>args<span class="sc">.</span>end<span class="sc">}</span><span class="ss">.csv&quot;</span></span>
<span id="cb1-106"><a href="#cb1-106" aria-hidden="true" tabindex="-1"></a>                frame.to_csv(target <span class="op">/</span> name, index<span class="op">=</span><span class="va">False</span>, encoding<span class="op">=</span><span class="st">&quot;utf-8-sig&quot;</span>)</span>
<span id="cb1-107"><a href="#cb1-107" aria-hidden="true" tabindex="-1"></a>                manifest.append({<span class="st">&quot;symbol&quot;</span>: symbol, <span class="st">&quot;status&quot;</span>: <span class="st">&quot;ok&quot;</span>, <span class="st">&quot;rows&quot;</span>: <span class="bu">len</span>(frame),</span>
<span id="cb1-108"><a href="#cb1-108" aria-hidden="true" tabindex="-1"></a>                                 <span class="st">&quot;first&quot;</span>: frame.trade_date.<span class="bu">min</span>(), <span class="st">&quot;last&quot;</span>: frame.trade_date.<span class="bu">max</span>()})</span>
<span id="cb1-109"><a href="#cb1-109" aria-hidden="true" tabindex="-1"></a>            <span class="cf">except</span> <span class="pp">Exception</span> <span class="im">as</span> exc:</span>
<span id="cb1-110"><a href="#cb1-110" aria-hidden="true" tabindex="-1"></a>                manifest.append({<span class="st">&quot;symbol&quot;</span>: symbol, <span class="st">&quot;status&quot;</span>: <span class="st">&quot;failed&quot;</span>, <span class="st">&quot;rows&quot;</span>: <span class="dv">0</span>,</span>
<span id="cb1-111"><a href="#cb1-111" aria-hidden="true" tabindex="-1"></a>                                 <span class="st">&quot;error_type&quot;</span>: <span class="bu">type</span>(exc).<span class="va">__name__</span>})</span>
<span id="cb1-112"><a href="#cb1-112" aria-hidden="true" tabindex="-1"></a>    <span class="cf">finally</span>:</span>
<span id="cb1-113"><a href="#cb1-113" aria-hidden="true" tabindex="-1"></a>        client.close()</span>
<span id="cb1-114"><a href="#cb1-114" aria-hidden="true" tabindex="-1"></a>    summary <span class="op">=</span> pd.DataFrame(manifest)</span>
<span id="cb1-115"><a href="#cb1-115" aria-hidden="true" tabindex="-1"></a>    summary.to_csv(target <span class="op">/</span> <span class="st">&quot;collection_manifest.csv&quot;</span>, index<span class="op">=</span><span class="va">False</span>, encoding<span class="op">=</span><span class="st">&quot;utf-8-sig&quot;</span>)</span>
<span id="cb1-116"><a href="#cb1-116" aria-hidden="true" tabindex="-1"></a>    <span class="bu">print</span>(summary.to_string(index<span class="op">=</span><span class="va">False</span>))</span>
<span id="cb1-117"><a href="#cb1-117" aria-hidden="true" tabindex="-1"></a>    <span class="cf">if</span> summary[<span class="st">&quot;status&quot;</span>].ne(<span class="st">&quot;ok&quot;</span>).<span class="bu">any</span>():</span>
<span id="cb1-118"><a href="#cb1-118" aria-hidden="true" tabindex="-1"></a>        <span class="cf">raise</span> <span class="pp">SystemExit</span>(<span class="dv">1</span>)</span>
<span id="cb1-119"><a href="#cb1-119" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb1-120"><a href="#cb1-120" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb1-121"><a href="#cb1-121" aria-hidden="true" tabindex="-1"></a><span class="cf">if</span> <span class="va">__name__</span> <span class="op">==</span> <span class="st">&quot;__main__&quot;</span>:</span>
<span id="cb1-122"><a href="#cb1-122" aria-hidden="true" tabindex="-1"></a>    main()</span></code></pre></div>
<p>CSV를 읽을 때 <code>symbol</code>을 문자열로 지정해야 <code>005930</code>의 앞자리 0이 유지된다. CSV 인코딩은 Windows에서 열기 편한 <code>utf-8-sig</code>로 맞췄다. <a href="https://pandas.pydata.org/docs/reference/api/pandas.read_csv.html">pandas 공식 read_csv 문서</a></p>
<h2 id="first-collection">4. 한 종목부터 수집하기</h2>
<p>다음은 이미 끝난 기간을 지정한 실행 예시다. 자신의 수집 목적에 맞게 날짜를 바꾸되 아직 끝나지 않은 당일 일봉은 완료 자료와 섞지 않는다.</p>
<div class="sourceCode" id="cb2"><pre class="sourceCode powershell"><code class="sourceCode powershell"><span id="cb2-1"><a href="#cb2-1" aria-hidden="true" tabindex="-1"></a><span class="op">.</span>\<span class="op">.</span><span class="fu">venv</span>\Scripts\python<span class="op">.</span><span class="fu">exe</span> <span class="op">-</span>m py_compile src<span class="op">/</span>collect_daily<span class="op">.</span><span class="fu">py</span></span>
<span id="cb2-2"><a href="#cb2-2" aria-hidden="true" tabindex="-1"></a><span class="op">.</span>\<span class="op">.</span><span class="fu">venv</span>\Scripts\python<span class="op">.</span><span class="fu">exe</span> <span class="op">-</span>m src<span class="op">.</span><span class="fu">collect_daily</span> <span class="op">--</span>start <span class="dv">2026</span><span class="op">-</span><span class="dv">01</span><span class="op">-</span><span class="dv">01</span> <span class="op">--</span>end <span class="dv">2026</span><span class="op">-</span><span class="dv">10</span><span class="op">-</span><span class="dv">16</span> <span class="op">--</span>limit <span class="dv">1</span></span></code></pre></div>
<p>성공하면 <code>outputs/daily/종목코드_시작일_종료일.csv</code>와 <code>collection_manifest.csv</code>가 생긴다. <code>rows</code>가 양수이고 <code>first</code>·<code>last</code>가 요청 범위 안에 있어야 한다. 시작일이 휴일이거나 상장일보다 빠르면 첫 거래일이 시작일보다 늦을 수 있다.</p>
<p>한 종목의 CSV를 열어 순서와 숫자를 확인한 뒤 <code>--limit 5</code>로 확장한다. 전체 종목을 수집할 때도 정상 종목 수, 실패 종목 수, 범위별 행 수를 확인한다. 코드의 1초 간격은 교육용 기본값이며 현재 호출 한도를 보장하는 값이 아니다. 서버 환경별 한도와 오류 안내는 <a href="https://apiportal.koreainvestment.com/">한국투자증권 API 포털</a>에서 확인한다.</p>
<p>응답이 빈 구간은 상장 전·휴일·API 이상 여부를 확인한다. 위 예제는 모든 구간이 합쳐진 최종 결과만 검사하므로 거래일 누락을 완전히 검출하지는 않는다. 거래소 영업일 달력과 대조하고 거래 정지·신규 상장 사유를 기록하는 검사를 운영 전에 추가한다.</p>
<h2 id="collection-quality">5. 부분 성공을 전체 성공으로 보지 않기</h2>
<p><code>collection_manifest.csv</code>의 실패 행은 해당 실행에서 완성된 CSV로 인정하지 않는다. 같은 파일명이 이전 실행에서 이미 존재할 수 있으므로 파일 존재 여부만으로 성공을 판단하지 않는다. 운영용으로 확장할 때는 실행 ID별 폴더와 완료 표시를 사용하고, 이번 실행에서 성공한 파일만 다음 단계가 읽도록 한다.</p>
<p>정상 데이터의 확인 순서는 <strong>요청 목록 → 성공 목록 → 파일 → 필수 컬럼 → 중복 → 가격 범위 → 거래일 누락</strong>이다. 휴일을 채우려고 전날 가격으로 새로운 일봉을 만들지 않는다. 원본 자료를 남겨야 누락 원인이 수집인지 공급자 데이터인지 다시 확인할 수 있다.</p>
<h2 id="codex-request">6. Codex에 수집 품질 검사 요청하기</h2>
<pre class="text"><code>auto-trading-lab의 코스피200 일봉 수집기를 점검해줘.
구성 종목 목록의 기준일과 원본 출처를 남기고 symbol은 ASCII 숫자·영문 대문자 6자리 문자열로 처리해줘.
공식 기간별시세 API의 건수 제한 안에서 기간을 분할해줘.
symbol/trade_date/open/high/low/close/volume 형식을 고정하고,
중복 충돌·음수 거래량·잘못된 OHLC·미완료 일봉을 검사해줘.
실패 종목이 있으면 전체 성공으로 보고하지 말고 실행 요약에 구분해줘.
가짜 응답으로 빈 결과, 역순 날짜, 중복 충돌을 검사하고 실제 API 실행 여부를 명시해줘.
이번 작업은 CSV까지만이며 DB 저장과 주문은 다음 단계로 남겨줘.</code></pre>
<h2 id="completion">7. 오류 대응과 완료 기준</h2>
<table>
<colgroup>
<col style="width: 50%" />
<col style="width: 50%" />
</colgroup>
<thead>
<tr>
<th>문제</th>
<th>확인과 대응</th>
</tr>
</thead>
<tbody>
<tr>
<td>종목코드가 다섯 자리로 보임</td>
<td>CSV 읽기에서 문자열 형식을 지정했는지 확인한다</td>
</tr>
<tr>
<td>긴 기간인데 약 100행만 있음</td>
<td>조회 기간 분할과 응답 제한을 확인한다</td>
</tr>
<tr>
<td>가격 0 또는 OHLC 모순</td>
<td>자동 보정하지 않고 원본과 거래 상태를 확인한다</td>
</tr>
<tr>
<td>중복 날짜의 가격이 다름</td>
<td>수정주가 구분·시장·수집 시점을 비교한다</td>
</tr>
<tr>
<td>일부 종목만 실패</td>
<td>오류 종류와 API 오류 코드를 별도로 확인하고 실패 대상을 다시 수집한다</td>
</tr>
</tbody>
</table>
<p>이번 장을 마치면 기준일이 있는 종목 목록, 검증된 일봉 CSV, 성공·실패 요약이 있어야 한다. 가짜 응답 검사를 통과했다는 것과 실제 전체 종목 수집을 마쳤다는 것은 서로 다른 검증 결과다. 다음 장에서는 <strong>이번 실행에서 검증된 CSV만</strong> MySQL에 저장한다.</p>
</div>
          <section class="pk-related" aria-labelledby="related-heading"><p class="pk-eyebrow">FROM CONCEPT TO PRACTICE</p><h2 id="related-heading">구현 기록과 함께 읽기</h2><ul><li><a href="https://mediamaak.com/books/auto-trading/kis-api/">한국투자증권 API 인증과 현재가 조회</a></li></ul></section>
          <nav class="pk-pager" aria-label="이전 장과 다음 장"><a href="https://mediamaak.com/books/auto-trading/kis-api/">이전 글: 한국투자증권 API 인증과 현재가 조회</a><a href="https://mediamaak.com/books/auto-trading/mysql-storage/">다음 글: 수집한 데이터를 MySQL에 저장하기</a></nav>
          <div class="pk-share"><div><p class="pk-share-title">학습의 다음 단계를 남겨 두세요</p><p>공유 링크를 복사해 이어서 읽을 수 있습니다.</p></div><div><button type="button" data-share-content>공유 링크 복사</button><span role="status" data-share-status></span></div></div>
        </div>
      </div>
    </article>]]></content:encoded>
    </item>
    <item>
      <title>MySQL 데이터로 CCI 신호 계산하기</title>
      <link>https://mediamaak.com/books/auto-trading/cci-signals/</link>
      <guid isPermaLink="true">https://mediamaak.com/books/auto-trading/cci-signals/</guid>
      <pubDate>Tue, 20 Oct 2026 00:00:00 +0900</pubDate>
      <description>가격 입력값과 계산 기간을 고정하고 계산 결과와 신호를 기록합니다.</description>
      <content:encoded><![CDATA[<article class="pk-article" data-content-id="/books/auto-trading/cci-signals/" data-content-type="wiki-chapter" data-published-at="2026-10-20T00:00:00+09:00" data-updated-at="2026-10-20T10:36:01+09:00">
      <header class="pk-hero">
        <p class="pk-eyebrow">6 / 12 · AUTO TRADING WIKI</p>
        <h1>MySQL 데이터로 CCI 신호 계산하기</h1>
        <p class="pk-summary">가격 입력값과 계산 기간을 고정하고 계산 결과와 신호를 기록합니다.</p>
        <div class="pk-meta"><dl><dt>발행</dt><dd><time datetime="2026-10-20T00:00:00+09:00">2026-10-20</time></dd><dt>수정</dt><dd><time datetime="2026-10-20T10:36:01+09:00">2026-10-20</time></dd><dt>문서 검토</dt><dd>2026-10-20</dd><dt>본문 버전</dt><dd>1.1.2-web.20261020</dd></dl></div>
      </header>
      <div class="book-reading-content">
        <div class="pk-body-column">
          <div class="pk-prose"><p>MySQL에 저장한 일봉을 이용해 CCI를 계산하고, 정해진 경계선을 넘는 날을 신호로 기록한다. 이번 장의 결과는 <strong>종목별 20거래일 CCI와 진입 후보 신호</strong>다. 이 신호를 주문으로 전환하지 않으며, 다음 장에서 과거 데이터로 동작과 한계를 확인한다.</p>
<p>예제 규칙은 <code>전일 CCI &lt;= -100</code>이고 <code>당일 CCI &gt; -100</code>일 때 신호를 한 번 기록하는 것이다. 이는 계산과 시뮬레이션을 연결하기 위한 학습 규칙이며, 특정 종목이나 매매 방법의 수익성을 추천하는 뜻이 아니다.</p>
<h2 id="cci-formula">1. CCI 계산식 이해하기</h2>
<p>CCI는 오늘의 대표가격이 최근 대표가격 평균에서 얼마나 떨어져 있는지를 평균절대편차로 나눈 지표다. 대표가격 <code>TP</code>는 고가·저가·종가의 평균이다. 기간을 20거래일로 정하면 오늘을 포함한 최근 20개 관측값을 한 창으로 계산한다.</p>
<pre class="text"><code>TP[t] = (high[t] + low[t] + close[t]) / 3
SMA[t] = 최근 20개 TP의 평균
MD[t] = 최근 20개 각각의 TP와 동일한 SMA[t] 간 절대 차이의 평균
CCI[t] = (TP[t] - SMA[t]) / (0.015 × MD[t])</code></pre>
<p><code>MD[t]</code>를 구할 때는 <strong>그 창 안의 모든 값에서 같은 창의 평균을 뺀다.</strong> 각 날짜의 별도 이동평균을 먼저 빼고 그 차이에 다시 이동평균을 적용하면 다른 계산이 된다. CCI의 대표가격·평균절대편차 공식은 <a href="https://www.fidelity.com/learning-center/trading-investing/technical-analysis/technical-indicator-guide/cci">Fidelity의 지표 설명</a>을 참고했다.</p>
<p>CCI는 -100과 100 사이에 제한된 지표가 아니다. 범위 밖으로 크게 움직일 수 있고 낮은 값이 계속 유지될 수도 있다. 따라서 <code>CCI &lt;= -100</code>이라는 상태와 <code>-100을 위로 통과했다</code>는 사건을 구분한다. 이번 규칙은 두 번째 사건만 기록한다.</p>
<h2 id="calculation-contract">2. 계산 전 데이터와 예외 기준 정하기</h2>
<p>5장의 <code>daily_prices</code>에서 <code>symbol, trade_date, open, high, low, close, volume</code>을 읽는다. 최소한 다음 조건을 통과해야 한다.</p>
<ul>
<li>종목코드는 ASCII 숫자·영문 대문자 6자리이며, 종목코드와 거래일 조합이 중복되지 않는다.</li>
<li>각 종목 안에서 거래일이 오름차순이다.</li>
<li>가격은 유한한 양수이고 OHLC의 범위가 맞는다.</li>
<li>종목이 바뀌면 이동 계산과 전일 비교도 새로 시작한다.</li>
<li>계산 시작일 전에 필요한 준비 기간을 함께 읽는다.</li>
</ul>
<p>처음 19개 관측값은 20개가 모이지 않았으므로 CCI가 없다. 20개가 모두 같은 대표가격이면 평균절대편차가 0이므로 역시 계산 불가로 둔다. 두 경우 모두 <code>NaN</code>으로 유지하며 DB에서는 <code>NULL</code>로 저장한다. 0으로 바꾸면 실제 중립값처럼 보일 수 있다.</p>
<p>신호에는 전일과 당일의 유효한 CCI가 모두 필요하다. 따라서 처음 유효한 CCI가 생긴 날은 신호를 만들지 않는다. 거래 정지나 누락으로 관측일 간격이 벌어진 경우, 이번 장의 계산은 <strong>존재하는 20개 거래 관측값</strong>을 사용한다. 데이터 품질 검사는 계산기 밖에서 별도로 유지한다.</p>
<h2 id="calculation-code">3. 종목별 CCI 계산 함수 작성하기</h2>
<p><code>src/calculate_cci.py</code>에 다음 코드를 작성한다. 이동 창의 <code>min_periods=period</code>를 지정해 준비 기간을 명확히 하고, 미래 값이 들어오는 중앙 정렬은 사용하지 않는다. 창과 최소 관측수의 의미는 <a href="https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.rolling.html">pandas 공식 rolling 문서</a>를 참고한다.</p>
<div class="sourceCode" id="cb2"><pre class="sourceCode python"><code class="sourceCode python"><span id="cb2-1"><a href="#cb2-1" aria-hidden="true" tabindex="-1"></a><span class="im">import</span> argparse</span>
<span id="cb2-2"><a href="#cb2-2" aria-hidden="true" tabindex="-1"></a><span class="im">import</span> math</span>
<span id="cb2-3"><a href="#cb2-3" aria-hidden="true" tabindex="-1"></a><span class="im">import</span> re</span>
<span id="cb2-4"><a href="#cb2-4" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb2-5"><a href="#cb2-5" aria-hidden="true" tabindex="-1"></a><span class="im">import</span> mysql.connector</span>
<span id="cb2-6"><a href="#cb2-6" aria-hidden="true" tabindex="-1"></a><span class="im">import</span> pandas <span class="im">as</span> pd</span>
<span id="cb2-7"><a href="#cb2-7" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb2-8"><a href="#cb2-8" aria-hidden="true" tabindex="-1"></a><span class="im">from</span> src.kis_readonly <span class="im">import</span> ROOT, load_config</span>
<span id="cb2-9"><a href="#cb2-9" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb2-10"><a href="#cb2-10" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb2-11"><a href="#cb2-11" aria-hidden="true" tabindex="-1"></a><span class="kw">def</span> cross_up(values, threshold<span class="op">=-</span><span class="fl">100.0</span>):</span>
<span id="cb2-12"><a href="#cb2-12" aria-hidden="true" tabindex="-1"></a>    previous <span class="op">=</span> values.shift(<span class="dv">1</span>)</span>
<span id="cb2-13"><a href="#cb2-13" aria-hidden="true" tabindex="-1"></a>    <span class="cf">return</span> (previous.le(threshold) <span class="op">&amp;</span> values.gt(threshold)).fillna(<span class="va">False</span>).astype(<span class="bu">bool</span>)</span>
<span id="cb2-14"><a href="#cb2-14" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb2-15"><a href="#cb2-15" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb2-16"><a href="#cb2-16" aria-hidden="true" tabindex="-1"></a><span class="kw">def</span> calculate_cci(frame, period<span class="op">=</span><span class="dv">20</span>):</span>
<span id="cb2-17"><a href="#cb2-17" aria-hidden="true" tabindex="-1"></a>    <span class="cf">if</span> period <span class="op">&lt;</span> <span class="dv">2</span>:</span>
<span id="cb2-18"><a href="#cb2-18" aria-hidden="true" tabindex="-1"></a>        <span class="cf">raise</span> <span class="pp">ValueError</span>(<span class="st">&quot;period must be at least 2&quot;</span>)</span>
<span id="cb2-19"><a href="#cb2-19" aria-hidden="true" tabindex="-1"></a>    required <span class="op">=</span> {<span class="st">&quot;symbol&quot;</span>, <span class="st">&quot;trade_date&quot;</span>, <span class="st">&quot;open&quot;</span>, <span class="st">&quot;high&quot;</span>, <span class="st">&quot;low&quot;</span>, <span class="st">&quot;close&quot;</span>}</span>
<span id="cb2-20"><a href="#cb2-20" aria-hidden="true" tabindex="-1"></a>    <span class="cf">if</span> required <span class="op">-</span> <span class="bu">set</span>(frame.columns) <span class="kw">or</span> frame.empty:</span>
<span id="cb2-21"><a href="#cb2-21" aria-hidden="true" tabindex="-1"></a>        <span class="cf">raise</span> <span class="pp">ValueError</span>(<span class="st">&quot;Prices are empty or missing required columns&quot;</span>)</span>
<span id="cb2-22"><a href="#cb2-22" aria-hidden="true" tabindex="-1"></a>    data <span class="op">=</span> frame.copy()</span>
<span id="cb2-23"><a href="#cb2-23" aria-hidden="true" tabindex="-1"></a>    <span class="cf">if</span> data[[<span class="st">&quot;symbol&quot;</span>, <span class="st">&quot;trade_date&quot;</span>]].isna().<span class="bu">any</span>().<span class="bu">any</span>():</span>
<span id="cb2-24"><a href="#cb2-24" aria-hidden="true" tabindex="-1"></a>        <span class="cf">raise</span> <span class="pp">ValueError</span>(<span class="st">&quot;Missing symbol or trade date&quot;</span>)</span>
<span id="cb2-25"><a href="#cb2-25" aria-hidden="true" tabindex="-1"></a>    data[<span class="st">&quot;trade_date&quot;</span>] <span class="op">=</span> pd.to_datetime(data.trade_date, errors<span class="op">=</span><span class="st">&quot;raise&quot;</span>)</span>
<span id="cb2-26"><a href="#cb2-26" aria-hidden="true" tabindex="-1"></a>    <span class="cf">if</span> data[<span class="st">&quot;trade_date&quot;</span>].isna().<span class="bu">any</span>():</span>
<span id="cb2-27"><a href="#cb2-27" aria-hidden="true" tabindex="-1"></a>        <span class="cf">raise</span> <span class="pp">ValueError</span>(<span class="st">&quot;Missing trade date&quot;</span>)</span>
<span id="cb2-28"><a href="#cb2-28" aria-hidden="true" tabindex="-1"></a>    <span class="cf">if</span> data.duplicated([<span class="st">&quot;symbol&quot;</span>, <span class="st">&quot;trade_date&quot;</span>]).<span class="bu">any</span>():</span>
<span id="cb2-29"><a href="#cb2-29" aria-hidden="true" tabindex="-1"></a>        <span class="cf">raise</span> <span class="pp">ValueError</span>(<span class="st">&quot;Duplicate symbol/date&quot;</span>)</span>
<span id="cb2-30"><a href="#cb2-30" aria-hidden="true" tabindex="-1"></a>    <span class="cf">for</span> column <span class="kw">in</span> [<span class="st">&quot;open&quot;</span>, <span class="st">&quot;high&quot;</span>, <span class="st">&quot;low&quot;</span>, <span class="st">&quot;close&quot;</span>]:</span>
<span id="cb2-31"><a href="#cb2-31" aria-hidden="true" tabindex="-1"></a>        data[column] <span class="op">=</span> pd.to_numeric(data[column], errors<span class="op">=</span><span class="st">&quot;raise&quot;</span>).astype(<span class="bu">float</span>)</span>
<span id="cb2-32"><a href="#cb2-32" aria-hidden="true" tabindex="-1"></a>        <span class="cf">if</span> <span class="kw">not</span> data[column].<span class="bu">map</span>(<span class="kw">lambda</span> x: math.isfinite(x) <span class="kw">and</span> x <span class="op">&gt;</span> <span class="dv">0</span>).<span class="bu">all</span>():</span>
<span id="cb2-33"><a href="#cb2-33" aria-hidden="true" tabindex="-1"></a>            <span class="cf">raise</span> <span class="pp">ValueError</span>(<span class="ss">f&quot;Invalid price in </span><span class="sc">{</span>column<span class="sc">}</span><span class="ss">&quot;</span>)</span>
<span id="cb2-34"><a href="#cb2-34" aria-hidden="true" tabindex="-1"></a>    invalid <span class="op">=</span> (</span>
<span id="cb2-35"><a href="#cb2-35" aria-hidden="true" tabindex="-1"></a>        data.high.lt(data[[<span class="st">&quot;open&quot;</span>, <span class="st">&quot;low&quot;</span>, <span class="st">&quot;close&quot;</span>]].<span class="bu">max</span>(axis<span class="op">=</span><span class="dv">1</span>))</span>
<span id="cb2-36"><a href="#cb2-36" aria-hidden="true" tabindex="-1"></a>        <span class="op">|</span> data.low.gt(data[[<span class="st">&quot;open&quot;</span>, <span class="st">&quot;high&quot;</span>, <span class="st">&quot;close&quot;</span>]].<span class="bu">min</span>(axis<span class="op">=</span><span class="dv">1</span>))</span>
<span id="cb2-37"><a href="#cb2-37" aria-hidden="true" tabindex="-1"></a>    )</span>
<span id="cb2-38"><a href="#cb2-38" aria-hidden="true" tabindex="-1"></a>    <span class="cf">if</span> invalid.<span class="bu">any</span>():</span>
<span id="cb2-39"><a href="#cb2-39" aria-hidden="true" tabindex="-1"></a>        <span class="cf">raise</span> <span class="pp">ValueError</span>(<span class="st">&quot;Invalid OHLC range&quot;</span>)</span>
<span id="cb2-40"><a href="#cb2-40" aria-hidden="true" tabindex="-1"></a>    data <span class="op">=</span> data.sort_values([<span class="st">&quot;symbol&quot;</span>, <span class="st">&quot;trade_date&quot;</span>]).reset_index(drop<span class="op">=</span><span class="va">True</span>)</span>
<span id="cb2-41"><a href="#cb2-41" aria-hidden="true" tabindex="-1"></a>    results <span class="op">=</span> []</span>
<span id="cb2-42"><a href="#cb2-42" aria-hidden="true" tabindex="-1"></a>    <span class="cf">for</span> symbol, group <span class="kw">in</span> data.groupby(<span class="st">&quot;symbol&quot;</span>, sort<span class="op">=</span><span class="va">False</span>):</span>
<span id="cb2-43"><a href="#cb2-43" aria-hidden="true" tabindex="-1"></a>        group <span class="op">=</span> group.copy()</span>
<span id="cb2-44"><a href="#cb2-44" aria-hidden="true" tabindex="-1"></a>        typical <span class="op">=</span> (group.high <span class="op">+</span> group.low <span class="op">+</span> group.close) <span class="op">/</span> <span class="fl">3.0</span></span>
<span id="cb2-45"><a href="#cb2-45" aria-hidden="true" tabindex="-1"></a>        window <span class="op">=</span> typical.rolling(window<span class="op">=</span>period, min_periods<span class="op">=</span>period)</span>
<span id="cb2-46"><a href="#cb2-46" aria-hidden="true" tabindex="-1"></a>        average <span class="op">=</span> window.mean()</span>
<span id="cb2-47"><a href="#cb2-47" aria-hidden="true" tabindex="-1"></a>        deviation <span class="op">=</span> window.<span class="bu">apply</span>(<span class="kw">lambda</span> values: <span class="bu">abs</span>(values <span class="op">-</span> values.mean()).mean(), raw<span class="op">=</span><span class="va">True</span>)</span>
<span id="cb2-48"><a href="#cb2-48" aria-hidden="true" tabindex="-1"></a>        denominator <span class="op">=</span> (<span class="fl">0.015</span> <span class="op">*</span> deviation).where(deviation.gt(<span class="dv">0</span>))</span>
<span id="cb2-49"><a href="#cb2-49" aria-hidden="true" tabindex="-1"></a>        cci <span class="op">=</span> (typical <span class="op">-</span> average) <span class="op">/</span> denominator</span>
<span id="cb2-50"><a href="#cb2-50" aria-hidden="true" tabindex="-1"></a>        group[<span class="st">&quot;typical_price&quot;</span>] <span class="op">=</span> typical</span>
<span id="cb2-51"><a href="#cb2-51" aria-hidden="true" tabindex="-1"></a>        group[<span class="st">&quot;mean_deviation&quot;</span>] <span class="op">=</span> deviation</span>
<span id="cb2-52"><a href="#cb2-52" aria-hidden="true" tabindex="-1"></a>        group[<span class="ss">f&quot;cci_</span><span class="sc">{</span>period<span class="sc">}</span><span class="ss">&quot;</span>] <span class="op">=</span> cci</span>
<span id="cb2-53"><a href="#cb2-53" aria-hidden="true" tabindex="-1"></a>        group[<span class="st">&quot;entry_signal&quot;</span>] <span class="op">=</span> cross_up(cci)</span>
<span id="cb2-54"><a href="#cb2-54" aria-hidden="true" tabindex="-1"></a>        results.append(group)</span>
<span id="cb2-55"><a href="#cb2-55" aria-hidden="true" tabindex="-1"></a>    <span class="cf">return</span> pd.concat(results, ignore_index<span class="op">=</span><span class="va">True</span>)</span>
<span id="cb2-56"><a href="#cb2-56" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb2-57"><a href="#cb2-57" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb2-58"><a href="#cb2-58" aria-hidden="true" tabindex="-1"></a><span class="kw">def</span> read_prices(symbols):</span>
<span id="cb2-59"><a href="#cb2-59" aria-hidden="true" tabindex="-1"></a>    config <span class="op">=</span> load_config()</span>
<span id="cb2-60"><a href="#cb2-60" aria-hidden="true" tabindex="-1"></a>    connection <span class="op">=</span> mysql.connector.<span class="ex">connect</span>(</span>
<span id="cb2-61"><a href="#cb2-61" aria-hidden="true" tabindex="-1"></a>        host<span class="op">=</span>config[<span class="st">&quot;MYSQL_HOST&quot;</span>],</span>
<span id="cb2-62"><a href="#cb2-62" aria-hidden="true" tabindex="-1"></a>        port<span class="op">=</span><span class="bu">int</span>(config.get(<span class="st">&quot;MYSQL_PORT&quot;</span>, <span class="st">&quot;3306&quot;</span>)),</span>
<span id="cb2-63"><a href="#cb2-63" aria-hidden="true" tabindex="-1"></a>        user<span class="op">=</span>config[<span class="st">&quot;MYSQL_USER&quot;</span>],</span>
<span id="cb2-64"><a href="#cb2-64" aria-hidden="true" tabindex="-1"></a>        password<span class="op">=</span>config[<span class="st">&quot;MYSQL_PASSWORD&quot;</span>],</span>
<span id="cb2-65"><a href="#cb2-65" aria-hidden="true" tabindex="-1"></a>        database<span class="op">=</span>config[<span class="st">&quot;MYSQL_DATABASE&quot;</span>],</span>
<span id="cb2-66"><a href="#cb2-66" aria-hidden="true" tabindex="-1"></a>        connection_timeout<span class="op">=</span><span class="dv">10</span>,</span>
<span id="cb2-67"><a href="#cb2-67" aria-hidden="true" tabindex="-1"></a>    )</span>
<span id="cb2-68"><a href="#cb2-68" aria-hidden="true" tabindex="-1"></a>    cursor <span class="op">=</span> <span class="va">None</span></span>
<span id="cb2-69"><a href="#cb2-69" aria-hidden="true" tabindex="-1"></a>    <span class="cf">try</span>:</span>
<span id="cb2-70"><a href="#cb2-70" aria-hidden="true" tabindex="-1"></a>        cursor <span class="op">=</span> connection.cursor(dictionary<span class="op">=</span><span class="va">True</span>)</span>
<span id="cb2-71"><a href="#cb2-71" aria-hidden="true" tabindex="-1"></a>        marks <span class="op">=</span> <span class="st">&quot;,&quot;</span>.join([<span class="st">&quot;</span><span class="sc">%s</span><span class="st">&quot;</span>] <span class="op">*</span> <span class="bu">len</span>(symbols))</span>
<span id="cb2-72"><a href="#cb2-72" aria-hidden="true" tabindex="-1"></a>        sql <span class="op">=</span> (</span>
<span id="cb2-73"><a href="#cb2-73" aria-hidden="true" tabindex="-1"></a>            <span class="st">&quot;SELECT symbol, trade_date, open, high, low, close, volume &quot;</span></span>
<span id="cb2-74"><a href="#cb2-74" aria-hidden="true" tabindex="-1"></a>            <span class="ss">f&quot;FROM daily_prices WHERE symbol IN (</span><span class="sc">{</span>marks<span class="sc">}</span><span class="ss">) &quot;</span></span>
<span id="cb2-75"><a href="#cb2-75" aria-hidden="true" tabindex="-1"></a>            <span class="st">&quot;ORDER BY symbol, trade_date&quot;</span></span>
<span id="cb2-76"><a href="#cb2-76" aria-hidden="true" tabindex="-1"></a>        )</span>
<span id="cb2-77"><a href="#cb2-77" aria-hidden="true" tabindex="-1"></a>        cursor.execute(sql, <span class="bu">tuple</span>(symbols))</span>
<span id="cb2-78"><a href="#cb2-78" aria-hidden="true" tabindex="-1"></a>        <span class="cf">return</span> pd.DataFrame(cursor.fetchall())</span>
<span id="cb2-79"><a href="#cb2-79" aria-hidden="true" tabindex="-1"></a>    <span class="cf">finally</span>:</span>
<span id="cb2-80"><a href="#cb2-80" aria-hidden="true" tabindex="-1"></a>        <span class="cf">if</span> cursor <span class="kw">is</span> <span class="kw">not</span> <span class="va">None</span>:</span>
<span id="cb2-81"><a href="#cb2-81" aria-hidden="true" tabindex="-1"></a>            cursor.close()</span>
<span id="cb2-82"><a href="#cb2-82" aria-hidden="true" tabindex="-1"></a>        connection.close()</span>
<span id="cb2-83"><a href="#cb2-83" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb2-84"><a href="#cb2-84" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb2-85"><a href="#cb2-85" aria-hidden="true" tabindex="-1"></a><span class="kw">def</span> main():</span>
<span id="cb2-86"><a href="#cb2-86" aria-hidden="true" tabindex="-1"></a>    parser <span class="op">=</span> argparse.ArgumentParser()</span>
<span id="cb2-87"><a href="#cb2-87" aria-hidden="true" tabindex="-1"></a>    parser.add_argument(<span class="st">&quot;--symbols&quot;</span>, nargs<span class="op">=</span><span class="st">&quot;+&quot;</span>, required<span class="op">=</span><span class="va">True</span>)</span>
<span id="cb2-88"><a href="#cb2-88" aria-hidden="true" tabindex="-1"></a>    args <span class="op">=</span> parser.parse_args()</span>
<span id="cb2-89"><a href="#cb2-89" aria-hidden="true" tabindex="-1"></a>    <span class="cf">if</span> <span class="bu">any</span>(<span class="kw">not</span> re.fullmatch(<span class="vs">r&quot;</span><span class="pp">[A-Z0-9]</span><span class="op">{6}</span><span class="vs">&quot;</span>, s) <span class="cf">for</span> s <span class="kw">in</span> args.symbols):</span>
<span id="cb2-90"><a href="#cb2-90" aria-hidden="true" tabindex="-1"></a>        parser.error(<span class="st">&quot;Use six-character uppercase ASCII stock symbols&quot;</span>)</span>
<span id="cb2-91"><a href="#cb2-91" aria-hidden="true" tabindex="-1"></a>    frame <span class="op">=</span> read_prices(args.symbols)</span>
<span id="cb2-92"><a href="#cb2-92" aria-hidden="true" tabindex="-1"></a>    <span class="cf">if</span> frame.empty <span class="kw">or</span> <span class="bu">set</span>(args.symbols) <span class="op">-</span> <span class="bu">set</span>(frame[<span class="st">&quot;symbol&quot;</span>]):</span>
<span id="cb2-93"><a href="#cb2-93" aria-hidden="true" tabindex="-1"></a>        <span class="cf">raise</span> <span class="pp">ValueError</span>(<span class="st">&quot;One or more requested symbols have no stored prices&quot;</span>)</span>
<span id="cb2-94"><a href="#cb2-94" aria-hidden="true" tabindex="-1"></a>    result <span class="op">=</span> calculate_cci(frame, period<span class="op">=</span><span class="dv">20</span>)</span>
<span id="cb2-95"><a href="#cb2-95" aria-hidden="true" tabindex="-1"></a>    target <span class="op">=</span> ROOT <span class="op">/</span> <span class="st">&quot;outputs/indicators&quot;</span></span>
<span id="cb2-96"><a href="#cb2-96" aria-hidden="true" tabindex="-1"></a>    target.mkdir(parents<span class="op">=</span><span class="va">True</span>, exist_ok<span class="op">=</span><span class="va">True</span>)</span>
<span id="cb2-97"><a href="#cb2-97" aria-hidden="true" tabindex="-1"></a>    result.to_csv(target <span class="op">/</span> <span class="st">&quot;cci_20.csv&quot;</span>, index<span class="op">=</span><span class="va">False</span>, encoding<span class="op">=</span><span class="st">&quot;utf-8-sig&quot;</span>)</span>
<span id="cb2-98"><a href="#cb2-98" aria-hidden="true" tabindex="-1"></a>    summary <span class="op">=</span> result.groupby(<span class="st">&quot;symbol&quot;</span>).agg(</span>
<span id="cb2-99"><a href="#cb2-99" aria-hidden="true" tabindex="-1"></a>        rows<span class="op">=</span>(<span class="st">&quot;trade_date&quot;</span>, <span class="st">&quot;size&quot;</span>),</span>
<span id="cb2-100"><a href="#cb2-100" aria-hidden="true" tabindex="-1"></a>        valid_cci<span class="op">=</span>(<span class="st">&quot;cci_20&quot;</span>, <span class="st">&quot;count&quot;</span>),</span>
<span id="cb2-101"><a href="#cb2-101" aria-hidden="true" tabindex="-1"></a>        signals<span class="op">=</span>(<span class="st">&quot;entry_signal&quot;</span>, <span class="st">&quot;sum&quot;</span>),</span>
<span id="cb2-102"><a href="#cb2-102" aria-hidden="true" tabindex="-1"></a>    )</span>
<span id="cb2-103"><a href="#cb2-103" aria-hidden="true" tabindex="-1"></a>    <span class="bu">print</span>(summary.to_string())</span>
<span id="cb2-104"><a href="#cb2-104" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb2-105"><a href="#cb2-105" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb2-106"><a href="#cb2-106" aria-hidden="true" tabindex="-1"></a><span class="cf">if</span> <span class="va">__name__</span> <span class="op">==</span> <span class="st">&quot;__main__&quot;</span>:</span>
<span id="cb2-107"><a href="#cb2-107" aria-hidden="true" tabindex="-1"></a>    main()</span></code></pre></div>
<p><code>read_prices</code>는 SQL의 자리표시자 개수만 만들고 종목코드 값은 별도로 바인딩한다. CCI 계산 중 다른 종목의 직전 행을 읽지 않도록 그룹 안에서 <code>shift(1)</code>을 수행한다. 이 예제의 집계와 정렬은 <a href="https://pandas.pydata.org/docs/user_guide/groupby.html">pandas 공식 GroupBy 안내</a>의 데이터 분리 원리를 따른다.</p>
<h2 id="synthetic-check">4. 인공 데이터로 계산과 신호 경계 확인하기</h2>
<p>실제 주가를 보기 전에 손으로 답을 구할 수 있는 자료를 사용한다. 아래 코드는 <code>scripts/check_cci.py</code>로 저장하고 프로젝트 루트에서 모듈로 실행한다. 가짜 가격은 특정 종목의 실제 수익률 자료가 아니다.</p>
<div class="sourceCode" id="cb3"><pre class="sourceCode python"><code class="sourceCode python"><span id="cb3-1"><a href="#cb3-1" aria-hidden="true" tabindex="-1"></a><span class="im">import</span> pandas <span class="im">as</span> pd</span>
<span id="cb3-2"><a href="#cb3-2" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-3"><a href="#cb3-3" aria-hidden="true" tabindex="-1"></a><span class="im">from</span> src.calculate_cci <span class="im">import</span> calculate_cci, cross_up</span>
<span id="cb3-4"><a href="#cb3-4" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-5"><a href="#cb3-5" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-6"><a href="#cb3-6" aria-hidden="true" tabindex="-1"></a><span class="kw">def</span> prices(symbol, values):</span>
<span id="cb3-7"><a href="#cb3-7" aria-hidden="true" tabindex="-1"></a>    <span class="cf">return</span> pd.DataFrame({</span>
<span id="cb3-8"><a href="#cb3-8" aria-hidden="true" tabindex="-1"></a>        <span class="st">&quot;symbol&quot;</span>: symbol,</span>
<span id="cb3-9"><a href="#cb3-9" aria-hidden="true" tabindex="-1"></a>        <span class="st">&quot;trade_date&quot;</span>: pd.date_range(<span class="st">&quot;2026-01-01&quot;</span>, periods<span class="op">=</span><span class="bu">len</span>(values), freq<span class="op">=</span><span class="st">&quot;D&quot;</span>),</span>
<span id="cb3-10"><a href="#cb3-10" aria-hidden="true" tabindex="-1"></a>        <span class="st">&quot;open&quot;</span>: values, <span class="st">&quot;high&quot;</span>: values, <span class="st">&quot;low&quot;</span>: values, <span class="st">&quot;close&quot;</span>: values,</span>
<span id="cb3-11"><a href="#cb3-11" aria-hidden="true" tabindex="-1"></a>    })</span>
<span id="cb3-12"><a href="#cb3-12" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-13"><a href="#cb3-13" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-14"><a href="#cb3-14" aria-hidden="true" tabindex="-1"></a>rising <span class="op">=</span> calculate_cci(prices(<span class="st">&quot;000001&quot;</span>, <span class="bu">list</span>(<span class="bu">range</span>(<span class="dv">1</span>, <span class="dv">21</span>))))</span>
<span id="cb3-15"><a href="#cb3-15" aria-hidden="true" tabindex="-1"></a><span class="cf">assert</span> rising.cci_20.iloc[:<span class="dv">19</span>].isna().<span class="bu">all</span>()</span>
<span id="cb3-16"><a href="#cb3-16" aria-hidden="true" tabindex="-1"></a><span class="cf">assert</span> <span class="bu">abs</span>(rising.cci_20.iloc[<span class="dv">19</span>] <span class="op">-</span> <span class="fl">126.6666666667</span>) <span class="op">&lt;</span> <span class="fl">1e-8</span></span>
<span id="cb3-17"><a href="#cb3-17" aria-hidden="true" tabindex="-1"></a><span class="cf">assert</span> <span class="kw">not</span> rising.entry_signal.<span class="bu">any</span>()</span>
<span id="cb3-18"><a href="#cb3-18" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-19"><a href="#cb3-19" aria-hidden="true" tabindex="-1"></a>flat <span class="op">=</span> calculate_cci(prices(<span class="st">&quot;000002&quot;</span>, [<span class="dv">10</span>] <span class="op">*</span> <span class="dv">25</span>))</span>
<span id="cb3-20"><a href="#cb3-20" aria-hidden="true" tabindex="-1"></a><span class="cf">assert</span> flat.cci_20.isna().<span class="bu">all</span>()</span>
<span id="cb3-21"><a href="#cb3-21" aria-hidden="true" tabindex="-1"></a><span class="cf">assert</span> <span class="kw">not</span> flat.entry_signal.<span class="bu">any</span>()</span>
<span id="cb3-22"><a href="#cb3-22" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-23"><a href="#cb3-23" aria-hidden="true" tabindex="-1"></a>mixed <span class="op">=</span> calculate_cci(pd.concat([</span>
<span id="cb3-24"><a href="#cb3-24" aria-hidden="true" tabindex="-1"></a>    prices(<span class="st">&quot;000001&quot;</span>, <span class="bu">list</span>(<span class="bu">range</span>(<span class="dv">1</span>, <span class="dv">26</span>))),</span>
<span id="cb3-25"><a href="#cb3-25" aria-hidden="true" tabindex="-1"></a>    prices(<span class="st">&quot;000002&quot;</span>, [<span class="dv">10</span>] <span class="op">*</span> <span class="dv">25</span>),</span>
<span id="cb3-26"><a href="#cb3-26" aria-hidden="true" tabindex="-1"></a>]).sample(frac<span class="op">=</span><span class="dv">1</span>, random_state<span class="op">=</span><span class="dv">7</span>))</span>
<span id="cb3-27"><a href="#cb3-27" aria-hidden="true" tabindex="-1"></a><span class="cf">assert</span> mixed.groupby(<span class="st">&quot;symbol&quot;</span>).cci_20.count().to_dict() <span class="op">==</span> {<span class="st">&quot;000001&quot;</span>: <span class="dv">6</span>, <span class="st">&quot;000002&quot;</span>: <span class="dv">0</span>}</span>
<span id="cb3-28"><a href="#cb3-28" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-29"><a href="#cb3-29" aria-hidden="true" tabindex="-1"></a>values <span class="op">=</span> pd.Series([<span class="bu">float</span>(<span class="st">&quot;nan&quot;</span>), <span class="op">-</span><span class="dv">120</span>, <span class="op">-</span><span class="dv">100</span>, <span class="op">-</span><span class="dv">99</span>, <span class="op">-</span><span class="dv">80</span>, <span class="op">-</span><span class="dv">110</span>, <span class="op">-</span><span class="dv">90</span>])</span>
<span id="cb3-30"><a href="#cb3-30" aria-hidden="true" tabindex="-1"></a><span class="cf">assert</span> cross_up(values).tolist() <span class="op">==</span> [<span class="va">False</span>, <span class="va">False</span>, <span class="va">False</span>, <span class="va">True</span>, <span class="va">False</span>, <span class="va">False</span>, <span class="va">True</span>]</span>
<span id="cb3-31"><a href="#cb3-31" aria-hidden="true" tabindex="-1"></a><span class="bu">print</span>(<span class="st">&quot;synthetic CCI checks OK&quot;</span>)</span></code></pre></div>
<div class="sourceCode" id="cb4"><pre class="sourceCode powershell"><code class="sourceCode powershell"><span id="cb4-1"><a href="#cb4-1" aria-hidden="true" tabindex="-1"></a><span class="op">.</span>\<span class="op">.</span><span class="fu">venv</span>\Scripts\python<span class="op">.</span><span class="fu">exe</span> <span class="op">-</span>m scripts<span class="op">.</span><span class="fu">check_cci</span></span></code></pre></div>
<p>첫 예제는 평균 10.5, 평균절대편차 5, 마지막 대표가격 20이므로 CCI가 약 126.6667이다. 두 번째는 0으로 나누는 경우를, 세 번째는 정렬과 종목 간 분리를 확인한다. 마지막 예제에서 정확히 -100에 도착한 날은 신호가 없고 -100보다 커진 날에만 신호가 발생한다.</p>
<h2 id="database-run">5. 실제 DB 자료로 결과 확인하기</h2>
<p>수집·저장이 끝난 종목 하나부터 실행한다. 실제 보유 종목이나 매수 대상으로 선정하는 절차는 아니다.</p>
<div class="sourceCode" id="cb5"><pre class="sourceCode powershell"><code class="sourceCode powershell"><span id="cb5-1"><a href="#cb5-1" aria-hidden="true" tabindex="-1"></a><span class="op">.</span>\<span class="op">.</span><span class="fu">venv</span>\Scripts\python<span class="op">.</span><span class="fu">exe</span> <span class="op">-</span>m src<span class="op">.</span><span class="fu">calculate_cci</span> <span class="op">--</span>symbols <span class="dv">005930</span></span></code></pre></div>
<p><code>outputs/indicators/cci_20.csv</code>를 열어 앞부분의 결측값, 날짜 순서, 대표가격과 평균절대편차를 확인한다. <code>rows</code>는 읽은 일봉 수, <code>valid_cci</code>는 계산 가능한 행 수, <code>signals</code>는 경계 통과 횟수다. 신호가 0건이어도 규칙에 맞는 데이터라면 정상 결과다.</p>
<p>특정 검토 기간만 계산할 때는 그 기간 이전 자료를 충분히 읽고, 계산을 마친 다음 보고할 날짜를 자른다. 처음부터 검토 시작일 이후 행만 읽으면 준비 기간이 잘려 앞부분의 신호가 달라진다. 마지막 일봉은 거래일 종료 후 확정된 자료인지도 확인한다.</p>
<h2 id="signal-storage">6. 계산 결과의 저장 키와 버전 정하기</h2>
<p>CSV로 계산을 확인한 뒤 DB에 결과를 보관할 수 있다. 다음 스키마는 지표 값과 신호 규칙의 버전을 함께 남기는 예시다.</p>
<div class="sourceCode" id="cb6"><pre class="sourceCode sql"><code class="sourceCode sql"><span id="cb6-1"><a href="#cb6-1" aria-hidden="true" tabindex="-1"></a><span class="kw">CREATE</span> <span class="kw">TABLE</span> <span class="cf">IF</span> <span class="kw">NOT</span> <span class="kw">EXISTS</span> cci_daily_signals (</span>
<span id="cb6-2"><a href="#cb6-2" aria-hidden="true" tabindex="-1"></a>  symbol <span class="dt">CHAR</span>(<span class="dv">6</span>) <span class="kw">NOT</span> <span class="kw">NULL</span>,</span>
<span id="cb6-3"><a href="#cb6-3" aria-hidden="true" tabindex="-1"></a>  trade_date <span class="dt">DATE</span> <span class="kw">NOT</span> <span class="kw">NULL</span>,</span>
<span id="cb6-4"><a href="#cb6-4" aria-hidden="true" tabindex="-1"></a>  period <span class="dt">SMALLINT</span> UNSIGNED <span class="kw">NOT</span> <span class="kw">NULL</span>,</span>
<span id="cb6-5"><a href="#cb6-5" aria-hidden="true" tabindex="-1"></a>  strategy_version <span class="dt">VARCHAR</span>(<span class="dv">40</span>) <span class="kw">NOT</span> <span class="kw">NULL</span>,</span>
<span id="cb6-6"><a href="#cb6-6" aria-hidden="true" tabindex="-1"></a>  cci_value <span class="dt">DOUBLE</span> <span class="kw">NULL</span>,</span>
<span id="cb6-7"><a href="#cb6-7" aria-hidden="true" tabindex="-1"></a>  entry_signal <span class="dt">BOOLEAN</span> <span class="kw">NOT</span> <span class="kw">NULL</span>,</span>
<span id="cb6-8"><a href="#cb6-8" aria-hidden="true" tabindex="-1"></a>  calculated_at <span class="dt">TIMESTAMP</span> <span class="kw">NOT</span> <span class="kw">NULL</span> <span class="kw">DEFAULT</span> <span class="fu">CURRENT_TIMESTAMP</span>,</span>
<span id="cb6-9"><a href="#cb6-9" aria-hidden="true" tabindex="-1"></a>  <span class="kw">PRIMARY</span> <span class="kw">KEY</span> (symbol, trade_date, period, strategy_version)</span>
<span id="cb6-10"><a href="#cb6-10" aria-hidden="true" tabindex="-1"></a>) ENGINE<span class="op">=</span>InnoDB;</span></code></pre></div>
<p>이번 버전 이름은 <code>cci20_cross_up_m100_v1</code>로 정한다. <code>cci_20</code>을 <code>cci_value</code>에 저장하고, <code>NaN</code>은 <code>None</code>으로 변환해 SQL의 <code>NULL</code>로 바인딩한다. 같은 키를 재계산하면 갱신하고, 공식이나 신호 규칙이 바뀌면 버전 이름을 바꾼다. 적재의 트랜잭션과 중복 처리 방식은 5장과 같다.</p>
<p>값을 NULL로 저장하는 것과 값이 0인 것은 다르다. DB를 다시 읽을 때도 결측 의미를 보존한다. <a href="https://dev.mysql.com/doc/refman/8.4/en/working-with-null.html">MySQL 공식 NULL 설명</a></p>
<h2 id="codex-request">7. Codex에 계산 검증 요청하기</h2>
<pre class="text"><code>daily_prices를 읽어 20거래일 CCI와 -100 상향 돌파 신호를 계산해줘.
MD는 같은 창의 평균에 대한 평균절대편차로 계산해줘.
첫 19개 행과 MD=0은 결측, 전일 값이 없으면 신호 False로 유지해줘.
종목별 정렬·그룹 분리·중복 검사를 하고 미래 값을 사용하지 마.
손계산 가능한 상승 자료, 일정한 가격, 섞인 두 종목, 경계값 사례를 검사해줘.
신호는 전일 &lt;= -100 이면서 당일 &gt; -100일 때만 True로 기록해줘.
결과 CSV와 요약을 만들고, 실제 DB 접속 여부와 합성 검사 결과를 구분해줘.
주문이나 성과 주장은 추가하지 말고 계산 근거와 남은 검증 범위를 보고해줘.</code></pre>
<h2 id="completion">8. 오류 대응과 다음 장의 연결</h2>
<table>
<colgroup>
<col style="width: 50%" />
<col style="width: 50%" />
</colgroup>
<thead>
<tr>
<th>문제</th>
<th>확인할 내용</th>
</tr>
</thead>
<tbody>
<tr>
<td>처음부터 값이 나옴</td>
<td>최소 관측수가 20인지, 결측을 0으로 채우지 않았는지</td>
</tr>
<tr>
<td>값이 계속 무한대임</td>
<td>MD=0을 분모에서 제외했는지</td>
</tr>
<tr>
<td>종목 전환 때 신호 발생</td>
<td>전일 비교를 종목별로 했는지</td>
</tr>
<tr>
<td>차트 서비스와 값이 다름</td>
<td>수정주가·시장·거래일·기간·MD 공식·반올림 시점을 비교한다</td>
</tr>
<tr>
<td>예상보다 신호가 적음</td>
<td>상태를 세는지 경계 통과 사건을 세는지 구분한다</td>
</tr>
</tbody>
</table>
<p>완료 기준은 계산식, 준비 기간, MD=0 처리, 상향 돌파 규칙을 코드와 결과로 설명할 수 있는 상태다. 신호가 발생한 날의 종가를 이미 알고 계산했으므로 그 종가에 즉시 체결된 것으로 가정하지 않는다. 다음 장에서는 <strong>T일 신호 → 다음 거래일 시가</strong>라는 시간 순서를 고정해 시뮬레이션한다.</p>
</div>
          <section class="pk-related" aria-labelledby="related-heading"><p class="pk-eyebrow">FROM CONCEPT TO PRACTICE</p><h2 id="related-heading">구현 기록과 함께 읽기</h2><ul><li><a href="https://mediamaak.com/books/auto-trading/mysql-storage/">수집한 데이터를 MySQL에 저장하기</a></li></ul></section>
          <nav class="pk-pager" aria-label="이전 장과 다음 장"><a href="https://mediamaak.com/books/auto-trading/mysql-storage/">이전 글: 수집한 데이터를 MySQL에 저장하기</a><a href="https://mediamaak.com/books/auto-trading/simulation/">다음 글: CCI 신호를 과거 데이터로 시뮬레이션하기</a></nav>
          <div class="pk-share"><div><p class="pk-share-title">학습의 다음 단계를 남겨 두세요</p><p>공유 링크를 복사해 이어서 읽을 수 있습니다.</p></div><div><button type="button" data-share-content>공유 링크 복사</button><span role="status" data-share-status></span></div></div>
        </div>
      </div>
    </article>]]></content:encoded>
    </item>
    <item>
      <title>한국투자증권 API 토큰을 여러 실행기에서 안전하게 공유하기</title>
      <link>https://mediamaak.com/devlog/2026/10/17/kis-token-cache/</link>
      <guid isPermaLink="true">https://mediamaak.com/devlog/2026/10/17/kis-token-cache/</guid>
      <pubDate>Sat, 17 Oct 2026 12:00:00 +0900</pubDate>
      <description>계좌별 토큰 캐시와 오전 워밍업을 공유하면서 잘못된 계좌의 토큰 재사용과 불필요한 재발급을 막은 구현 기록입니다.</description>
      <content:encoded><![CDATA[<article class="pk-article" data-content-id="/devlog/2026/10/17/kis-token-cache/" data-content-type="devlog" data-published-at="2026-10-17T12:00:00+09:00" data-updated-at="2026-10-17T12:00:00+09:00">
      <header class="pk-hero">
        <p class="pk-eyebrow">ENGINE DEVELOPMENT NOTES</p>
        <h1>한국투자증권 API 토큰을 여러 실행기에서 안전하게 공유하기</h1>
        <p class="pk-summary">계좌별 토큰 캐시와 오전 워밍업을 공유하면서 잘못된 계좌의 토큰 재사용과 불필요한 재발급을 막은 구현 기록입니다.</p>
        <div class="pk-meta"><dl><dt>발행</dt><dd><time datetime="2026-10-17T12:00:00+09:00">2026-10-17</time></dd><dt>수정</dt><dd><time datetime="2026-10-17T12:00:00+09:00">2026-10-17</time></dd><dt>구현 상태</dt><dd>구현 확인</dd></dl></div>
      </header>
      <div class="pk-reading-layout">
        <aside class="pk-toc" aria-label="이 글의 목차">
          <details class="pk-toc-details">
            <summary class="pk-toc-summary">이 글에서 다루는 내용</summary>
            <div class="pk-toc-content"><nav aria-label="본문 목차"><ol><li class="pk-toc-level-2"><a href="#여러-실행기가-같은-인증을-사용할-때-생기는-문제">여러 실행기가 같은 인증을 사용할 때 생기는 문제</a></li><li class="pk-toc-level-2"><a href="#캐시를-재사용하는-네-가지-조건">캐시를 재사용하는 네 가지 조건</a></li><li class="pk-toc-level-2"><a href="#동시에-실행되어도-발급은-한-곳에서">동시에 실행되어도 발급은 한 곳에서</a></li><li class="pk-toc-level-2"><a href="#실제-계좌-없이-재사용-조건-살펴보기">실제 계좌 없이 재사용 조건 살펴보기</a></li><li class="pk-toc-level-2"><a href="#유효한-날짜라도-인증이-거부될-수-있습니다">유효한 날짜라도 인증이 거부될 수 있습니다</a></li><li class="pk-toc-level-2"><a href="#어떤-검증이-코드에-포함되어-있나요">어떤 검증이 코드에 포함되어 있나요?</a></li><li class="pk-toc-level-2"><a href="#적용-범위와-다음에-확인할-점">적용 범위와 다음에 확인할 점</a></li><li class="pk-toc-level-2"><a href="#공식-참고-자료">공식 참고 자료</a></li></ol></nav></div>
          </details>
        </aside>
        <div class="pk-body-column">
          <div class="pk-prose"><h2 id="여러-실행기가-같은-인증을-사용할-때-생기는-문제">여러 실행기가 같은 인증을 사용할 때 생기는 문제</h2>
<p>자동매매 시스템에는 주문 실행기만 있는 것이 아닙니다. 시세 수집기와 상태 확인 작업도 인증이 필요합니다. 각각 토큰을 따로 관리하면 같은 계좌의 토큰을 중복 발급하거나, 인증 정보를 교체한 뒤에도 이전 토큰을 읽을 수 있습니다. 다른 계좌의 캐시를 잘못 사용하는 문제도 막아야 합니다.</p>
<p>MediaMaak 엔진은 이 문제를 물리 계좌별 공유 캐시와 사전 워밍업으로 다룹니다. 오전에 준비한 토큰을 이후 실행기가 재사용하되, 파일이 있다는 이유만으로 유효하다고 판단하지 않습니다. 이 글은 2026년 10월 17일에 확인한 구현을 설명합니다. 최초 구현 날짜나 실제 장애 발생 날짜를 확정한 기록은 아닙니다.</p>
<h2 id="캐시를-재사용하는-네-가지-조건">캐시를 재사용하는 네 가지 조건</h2>
<p>현재 구현은 캐시 형식 버전, 발급 날짜, 계좌 식별값, 인증 정보 식별값을 모두 비교합니다. 한 조건이라도 다르면 캐시를 재사용하지 않습니다.</p>
<table>
<colgroup>
<col style="width: 33%" />
<col style="width: 33%" />
<col style="width: 33%" />
</colgroup>
<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>계좌 fingerprint</td>
<td>다른 물리 계좌의 캐시를 섞지 않기 위해</td>
<td>기존 값을 사용하지 않음</td>
</tr>
<tr>
<td>인증 정보 fingerprint</td>
<td>API 키 또는 시크릿 교체를 감지하기 위해</td>
<td>이전 토큰을 사용하지 않음</td>
</tr>
</tbody>
</table>
<p>fingerprint는 원본을 직접 파일 이름에 넣지 않고 비교하기 위한 해시 식별값입니다. 계좌 식별값은 계좌 범위를, 인증 정보 식별값은 접속 환경과 해당 계좌의 인증 정보 범위를 구분합니다. 인증 정보를 바꾸더라도 같은 물리 계좌의 캐시 위치를 유지하면서 내용을 교체합니다.</p>
<p>이 식별값이 캐시 내용을 암호화하지는 않습니다. 캐시에는 인증 토큰이 저장되므로 접근 권한이 제한된 운영 저장소에서 관리해야 합니다. 소스 저장소, 게시글, 진단 출력에는 실제 값을 넣지 않습니다.</p>
<h2 id="동시에-실행되어도-발급은-한-곳에서">동시에 실행되어도 발급은 한 곳에서</h2>
<p>캐시가 없을 때 여러 실행기가 동시에 인증을 요청할 수 있습니다. 엔진은 계좌별 파일 잠금을 획득한 실행기만 발급 절차에 들어가게 합니다. 잠금을 얻은 뒤에는 캐시를 다시 읽습니다. 기다리는 동안 다른 실행기가 토큰을 저장했다면 새 발급 없이 그 결과를 사용합니다.</p>
<p>처리 순서는 다음과 같습니다.</p>
<ol type="1">
<li>현재 계좌의 캐시를 확인합니다.</li>
<li>재사용할 값이 없으면 계좌별 잠금을 얻습니다.</li>
<li>잠금 안에서 캐시를 다시 확인합니다.</li>
<li>여전히 사용할 값이 없을 때만 인증 API를 호출합니다.</li>
<li>임시 파일을 완성한 뒤 캐시 파일을 교체합니다.</li>
<li>잠금을 해제합니다.</li>
</ol>
<p>파일 교체는 다른 실행기가 쓰다 만 JSON을 읽을 가능성을 줄입니다. 잠금을 제한 시간 안에 얻지 못하면 다른 실행기와 경쟁해서 토큰을 발급하지 않고 실패로 종료합니다.</p>
<p>오전 워밍업은 활성화된 실계좌 KRX 프로필을 순서대로 처리합니다. 실패한 계좌만 정해진 횟수 안에서 재시도하고, 성공한 계좌까지 처음부터 다시 발급하지 않습니다. 워밍업은 인증 API를 사용할 수 있지만 매매 상태를 읽거나 주문을 제출하는 작업은 하지 않습니다.</p>
<h2 id="실제-계좌-없이-재사용-조건-살펴보기">실제 계좌 없이 재사용 조건 살펴보기</h2>
<p>다음 코드는 비교 규칙만 보여 주는 합성 예제입니다. 계좌·인증 정보 식별값도 설명용 표식이며, 실제 해시나 토큰은 없습니다. 네트워크 연결이나 파일 저장을 수행하지 않습니다.</p>
<div class="sourceCode" id="cb1"><pre class="sourceCode python"><code class="sourceCode python"><span id="cb1-1"><a href="#cb1-1" aria-hidden="true" tabindex="-1"></a><span class="kw">def</span> can_reuse(cache, <span class="op">*</span>, day, account_mark, credential_mark):</span>
<span id="cb1-2"><a href="#cb1-2" aria-hidden="true" tabindex="-1"></a>    <span class="cf">return</span> (</span>
<span id="cb1-3"><a href="#cb1-3" aria-hidden="true" tabindex="-1"></a>        cache.get(<span class="st">&quot;schema_version&quot;</span>) <span class="op">==</span> <span class="dv">1</span></span>
<span id="cb1-4"><a href="#cb1-4" aria-hidden="true" tabindex="-1"></a>        <span class="kw">and</span> cache.get(<span class="st">&quot;issued_on&quot;</span>) <span class="op">==</span> day</span>
<span id="cb1-5"><a href="#cb1-5" aria-hidden="true" tabindex="-1"></a>        <span class="kw">and</span> cache.get(<span class="st">&quot;account_mark&quot;</span>) <span class="op">==</span> account_mark</span>
<span id="cb1-6"><a href="#cb1-6" aria-hidden="true" tabindex="-1"></a>        <span class="kw">and</span> cache.get(<span class="st">&quot;credential_mark&quot;</span>) <span class="op">==</span> credential_mark</span>
<span id="cb1-7"><a href="#cb1-7" aria-hidden="true" tabindex="-1"></a>    )</span>
<span id="cb1-8"><a href="#cb1-8" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb1-9"><a href="#cb1-9" aria-hidden="true" tabindex="-1"></a>demo_cache <span class="op">=</span> {</span>
<span id="cb1-10"><a href="#cb1-10" aria-hidden="true" tabindex="-1"></a>    <span class="st">&quot;schema_version&quot;</span>: <span class="dv">1</span>,</span>
<span id="cb1-11"><a href="#cb1-11" aria-hidden="true" tabindex="-1"></a>    <span class="st">&quot;issued_on&quot;</span>: <span class="st">&quot;2026-10-17&quot;</span>,</span>
<span id="cb1-12"><a href="#cb1-12" aria-hidden="true" tabindex="-1"></a>    <span class="st">&quot;account_mark&quot;</span>: <span class="st">&quot;synthetic-account-a&quot;</span>,</span>
<span id="cb1-13"><a href="#cb1-13" aria-hidden="true" tabindex="-1"></a>    <span class="st">&quot;credential_mark&quot;</span>: <span class="st">&quot;synthetic-key-v1&quot;</span>,</span>
<span id="cb1-14"><a href="#cb1-14" aria-hidden="true" tabindex="-1"></a>}</span>
<span id="cb1-15"><a href="#cb1-15" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb1-16"><a href="#cb1-16" aria-hidden="true" tabindex="-1"></a><span class="cf">assert</span> can_reuse(</span>
<span id="cb1-17"><a href="#cb1-17" aria-hidden="true" tabindex="-1"></a>    demo_cache,</span>
<span id="cb1-18"><a href="#cb1-18" aria-hidden="true" tabindex="-1"></a>    day<span class="op">=</span><span class="st">&quot;2026-10-17&quot;</span>,</span>
<span id="cb1-19"><a href="#cb1-19" aria-hidden="true" tabindex="-1"></a>    account_mark<span class="op">=</span><span class="st">&quot;synthetic-account-a&quot;</span>,</span>
<span id="cb1-20"><a href="#cb1-20" aria-hidden="true" tabindex="-1"></a>    credential_mark<span class="op">=</span><span class="st">&quot;synthetic-key-v1&quot;</span>,</span>
<span id="cb1-21"><a href="#cb1-21" aria-hidden="true" tabindex="-1"></a>)</span>
<span id="cb1-22"><a href="#cb1-22" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb1-23"><a href="#cb1-23" aria-hidden="true" tabindex="-1"></a><span class="co"># 인증 정보를 교체하면 같은 계좌여도 재사용하지 않습니다.</span></span>
<span id="cb1-24"><a href="#cb1-24" aria-hidden="true" tabindex="-1"></a><span class="cf">assert</span> <span class="kw">not</span> can_reuse(</span>
<span id="cb1-25"><a href="#cb1-25" aria-hidden="true" tabindex="-1"></a>    demo_cache,</span>
<span id="cb1-26"><a href="#cb1-26" aria-hidden="true" tabindex="-1"></a>    day<span class="op">=</span><span class="st">&quot;2026-10-17&quot;</span>,</span>
<span id="cb1-27"><a href="#cb1-27" aria-hidden="true" tabindex="-1"></a>    account_mark<span class="op">=</span><span class="st">&quot;synthetic-account-a&quot;</span>,</span>
<span id="cb1-28"><a href="#cb1-28" aria-hidden="true" tabindex="-1"></a>    credential_mark<span class="op">=</span><span class="st">&quot;synthetic-key-v2&quot;</span>,</span>
<span id="cb1-29"><a href="#cb1-29" aria-hidden="true" tabindex="-1"></a>)</span></code></pre></div>
<p>이 예제는 실제 엔진의 캐시 형식이나 인증 클라이언트를 복사한 코드가 아닙니다. 날짜 또는 계좌 표식을 바꾸어 어느 조건에서 재사용이 차단되는지 확인하는 학습용 코드입니다.</p>
<h2 id="유효한-날짜라도-인증이-거부될-수-있습니다">유효한 날짜라도 인증이 거부될 수 있습니다</h2>
<p>같은 날짜의 캐시라도 토큰이 취소되었을 수 있습니다. 현재 클라이언트는 읽기 요청인 GET에서 HTTP 401 또는 403을 받으면 해당 토큰을 무효화하고 인증을 한 번 다시 시도합니다. 캐시를 지울 때에는 계좌·인증 정보 식별값과 토큰 자체가 모두 일치하는지도 확인합니다. 다른 실행기가 이미 저장한 새 토큰을 이전 실패 처리에서 삭제하지 않기 위해서입니다.</p>
<p>이 인증 재시도는 주문 POST에 적용하지 않습니다. 주문 응답이 불확실할 때 다시 요청하면 중복 주문이 될 수 있기 때문입니다. 주문을 재시도할지 여부는 인증 캐시와 별개로 주문 접수 이력과 실행 조건을 확인하는 절차에서 결정해야 합니다.</p>
<h2 id="어떤-검증이-코드에-포함되어-있나요">어떤 검증이 코드에 포함되어 있나요?</h2>
<p>현재 엔진에는 실제 증권사 연결 대신 가짜 응답과 임시 저장소를 사용하는 오프라인 테스트가 포함되어 있습니다.</p>
<table>
<colgroup>
<col style="width: 50%" />
<col style="width: 50%" />
</colgroup>
<thead>
<tr>
<th>검증 시나리오</th>
<th>확인하는 계약</th>
</tr>
</thead>
<tbody>
<tr>
<td>API 시크릿 교체</td>
<td>같은 계좌의 캐시 위치를 유지하되 이전 토큰을 재사용하지 않음</td>
</tr>
<tr>
<td>서로 다른 계좌</td>
<td>인증 요청과 주문 요청의 계좌 범위가 섞이지 않음</td>
</tr>
<tr>
<td>워밍업 실행</td>
<td>매매 상태를 읽지 않고 주문 제출도 하지 않음</td>
</tr>
<tr>
<td>계좌 하나의 워밍업 실패</td>
<td>실패한 계좌만 재시도함</td>
</tr>
<tr>
<td>진단 출력</td>
<td>인증 키·시크릿·토큰으로 식별한 필드를 가림</td>
</tr>
<tr>
<td>캐시 토큰의 GET 인증 거부</td>
<td>읽기 요청을 한 번 다시 시도함</td>
</tr>
</tbody>
</table>
<p>이 글에서는 해당 소스와 테스트의 검증 항목을 확인했습니다. 새로 실계좌 인증을 요청하거나 주문을 실행하지 않았으며, 이 글의 작성만으로 전체 엔진 테스트의 최신 통과 결과를 주장하지 않습니다.</p>
<h2 id="적용-범위와-다음에-확인할-점">적용 범위와 다음에 확인할 점</h2>
<p>이 구현은 같은 운영 환경의 로컬 파일 시스템을 공유하는 실행기를 대상으로 합니다. 여러 서버 사이의 분산 잠금이나 암호화된 비밀 관리 서비스를 제공하는 구현은 아닙니다.</p>
<p>또한 현재 캐시 재사용 조건은 로컬 날짜를 기준으로 합니다. 토큰 응답의 남은 유효 시간을 계산한 결과와 동일하지 않습니다. 운영 환경의 시간대와 시계를 확인해야 하고, 캐시가 같은 날짜라는 사실만으로 인증 성공을 보장할 수 없습니다.</p>
<p>진단 출력의 필드 가리기도 모든 종류의 비밀정보 유출을 자동으로 막는 보장은 아닙니다. 새 로그 필드를 추가할 때에는 출력 내용을 별도로 검토해야 합니다. 인증 준비, 시세 조회, 주문 제출의 경계를 유지하는 것이 이 설계의 핵심입니다.</p>
<h2 id="공식-참고-자료">공식 참고 자료</h2>
<p>한국투자증권은 Appkey와 App secret으로 토큰을 발급받아 REST API를 호출하는 흐름을 안내합니다. API의 현재 사용 조건과 요청 형식은 <a href="https://apiportal.koreainvestment.com/intro">KIS Developers</a>에서 확인할 수 있습니다.</p>
<p><a href="https://github.com/koreainvestment/open-trading-api">한국투자증권 공식 Open API 예제 저장소</a>도 함께 참고할 수 있습니다. 위의 캐시 공유·파일 잠금·워밍업 규칙은 MediaMaak의 구현 선택이며, 공식 API의 필수 규칙으로 해석하지 않습니다.</p>
</div>
          <section class="pk-related" aria-labelledby="related-heading"><p class="pk-eyebrow">KEEP READING</p><h2 id="related-heading">개념을 이어서 살펴보세요</h2><ul><li><a href="https://mediamaak.com/books/auto-trading/kis-api/">한국투자증권 API 인증과 현재가 조회</a></li></ul></section>
          <nav class="pk-pager" aria-label="이전 글과 다음 글"><a href="https://mediamaak.com/devlog/2026/10/26/weekly-reading-01/">다음 글: 주간 읽기 01: 자동매매를 시작하는 순서</a></nav>
          <div class="pk-share"><div><p class="pk-share-title">다시 읽고 싶은 기록이라면</p><p>공유 링크를 복사해 보관할 수 있습니다.</p></div><div><button type="button" data-share-content>공유 링크 복사</button><span role="status" data-share-status></span></div></div>
        </div>
      </div>
    </article>]]></content:encoded>
    </item>
    <item>
      <title>시간가중수익률(TWR)과 단순 수익률의 차이</title>
      <link>https://mediamaak.com/guides/time-weighted-vs-simple-return/</link>
      <guid isPermaLink="true">https://mediamaak.com/guides/time-weighted-vs-simple-return/</guid>
      <pubDate>Fri, 16 Oct 2026 09:00:00 +0900</pubDate>
      <description>입출금이 있는 계좌에서 시간가중수익률과 단순 수익률, 손익 금액이 어떻게 달라지는지 예시로 설명합니다.</description>
      <content:encoded><![CDATA[<article data-content-id="guides/time-weighted-vs-simple-return"><p class="eyebrow">MEDIAMAAK GUIDE</p><h1>시간가중수익률(TWR)과 단순 수익률의 차이</h1><p class="lead">입출금이 있는 계좌에서 시간가중수익률과 단순 수익률, 손익 금액이 어떻게 달라지는지 예시로 설명합니다.</p><p class="guide-meta"><a href="https://mediamaak.com/about.html">MediaMaak 편집팀</a> · 작성·수정 <time datetime="2026-10-16">2026년 10월 16일</time></p>
<figure class="guide-hero"><img src="https://mediamaak.com/assets/images/guides/time-weighted-vs-simple-return.png" width="1200" height="675" alt="입금 시점을 기준으로 구간 수익률을 연결하는 시간가중수익률과 단순 수익률 비교 그림" loading="eager" fetchpriority="high"><figcaption>현금 흐름을 구간 경계로 나누면 투자 성과와 입출금 효과를 분리할 수 있습니다.</figcaption></figure>
<h2>단순 수익률은 입출금이 없을 때 가장 명확합니다</h2><p>시작 평가금액이 100만원이고 입출금 없이 110만원이 되었다면 단순 수익률은 <strong>(110만원 ÷ 100만원) − 1 = 10%</strong>입니다. 계산 기간 중 외부 입출금이 생기면 잔액 변화와 투자 성과가 섞입니다.</p>
<h2>TWR은 외부 현금흐름의 영향을 분리합니다</h2><p>시간가중수익률(TWR)은 입출금이 발생할 때마다 기간을 나누고 각 구간의 성장률을 연결합니다. 운용자가 통제하기 어려운 입금 규모와 시점의 영향을 줄여 전략 자체의 성과를 비교할 때 유용합니다.</p>
<div class="guide-table"><table><caption>수익률 지표의 용도</caption><thead><tr><th scope="col">지표</th><th scope="col">계산 관점</th><th scope="col">주요 용도</th></tr></thead><tbody><tr><td>단순 수익률</td><td>시작값과 종료값 비교</td><td>입출금이 없는 단일 기간</td></tr><tr><td>TWR</td><td>입출금 전후 구간 수익률 연결</td><td>전략·운용 성과 비교</td></tr><tr><td>금액 손익</td><td>실제 늘거나 줄어든 금액</td><td>계좌 결과 확인</td></tr></tbody></table></div>
<h2>수익률과 손익 금액을 함께 보세요</h2><p>TWR이 높아도 투자금이 적었던 구간에서 수익이 났다면 전체 손익 금액은 작을 수 있습니다. 반대로 큰 금액을 투자한 뒤 손실이 발생하면 TWR과 체감 결과가 다를 수 있습니다. 수익률, 손익 금액, 입출금 내역과 평가금액을 함께 확인해야 합니다.</p>
<h2>비교할 때 계산 기준을 맞추세요</h2><p>전략과 KOSPI200 지수를 비교할 때 시작일·종료일, 배당 포함 여부, 비용 반영 범위와 평가 시각을 맞춥니다. 이름이 같은 수익률이라도 계산 규칙이 다르면 숫자를 직접 비교하기 어렵습니다.</p>
<div class="guide-next"><h2>입출금 예제를 단계별로 확인하세요</h2><p><a class="button" href="https://mediamaak.com/guides/returns-with-cashflows/">입출금 반영 계산 예제</a></p><p><a href="https://mediamaak.com/kospi200/">공개 전략 기록</a> · <a href="https://mediamaak.com/guides/kospi200-selection-criteria/">추천 기준 확인 방법</a></p></div>
<div class="guide-share"><button class="secondary" type="button" data-share-content>공유 링크 복사</button><span role="status" data-share-status></span></div></article>]]></content:encoded>
    </item>
    <item>
      <title>KOSPI200 종목 추천 기준, 무엇을 확인해야 할까요?</title>
      <link>https://mediamaak.com/guides/kospi200-selection-criteria/</link>
      <guid isPermaLink="true">https://mediamaak.com/guides/kospi200-selection-criteria/</guid>
      <pubDate>Fri, 16 Oct 2026 09:00:00 +0900</pubDate>
      <description>추천 종목을 볼 때 계산 시각, 선정 조건, 표본 수와 과거 목표 도달 기록을 함께 확인하는 방법을 설명합니다.</description>
      <content:encoded><![CDATA[<article data-content-id="guides/kospi200-selection-criteria"><p class="eyebrow">MEDIAMAAK GUIDE</p><h1>KOSPI200 종목 추천 기준, 무엇을 확인해야 할까요?</h1><p class="lead">추천 종목을 볼 때 계산 시각, 선정 조건, 표본 수와 과거 목표 도달 기록을 함께 확인하는 방법을 설명합니다.</p><p class="guide-meta"><a href="https://mediamaak.com/about.html">MediaMaak 편집팀</a> · 작성·수정 <time datetime="2026-10-16">2026년 10월 16일</time></p>
<figure class="guide-hero"><img src="https://mediamaak.com/assets/images/guides/kospi200-selection-criteria.png" width="1200" height="675" alt="계산 시각, 조건, 표본 수, 목표 도달 기록을 순서대로 점검하는 선정 기준 그림" loading="eager" fetchpriority="high"><figcaption>한 개의 수익률보다 계산 시각·조건·표본·도달 기록을 먼저 봅니다.</figcaption></figure>
<h2>추천 종목보다 먼저 기준 시각을 확인하세요</h2><p>같은 종목이라도 가격과 거래량은 계속 달라집니다. 추천 화면의 계산 시각, 사용한 가격 기준과 대상 종목군을 먼저 확인해야 결과를 같은 조건에서 다시 읽을 수 있습니다. 장중 계산과 장 마감 후 계산은 서로 다른 자료입니다.</p>
<h2>선정 조건과 표본 수를 함께 보세요</h2><p>도달률 하나만으로 판단하지 말고 어떤 조건으로 과거 사례를 찾았는지, 비슷한 사례가 몇 건이었는지 확인합니다. 표본이 적으면 높은 비율도 쉽게 흔들릴 수 있습니다. MediaMaak은 추천 결과에서 유사 조건 도달률과 표본을 함께 보여 줍니다.</p>
<h2>목표 도달과 투자 수익을 구분하세요</h2><p>정해진 기간 안에 특정 가격에 닿았다는 사실은 실제 매매 수익과 같지 않습니다. 매수 가능 가격, 체결 시점, 비용, 손절 기준과 보유 비중이 달라지면 계좌 결과도 달라집니다. 추천 결과는 후보를 좁히는 자료로 읽고 실제 성과는 별도의 기록에서 확인하세요.</p>
<h2>과거 결과를 같은 규칙으로 다시 확인하세요</h2><ul><li>추천 당시 공개된 시각과 가격을 바꾸지 않았는지</li><li>목표 도달·미도달·관찰중 기준이 일관적인지</li><li>성공 사례뿐 아니라 전체 추천 이력이 남아 있는지</li><li>시뮬레이션과 실제 운용 기록을 구분했는지</li></ul>
<div class="guide-next"><h2>오늘 결과와 과거 기록을 함께 보세요</h2><p><a class="button" href="https://mediamaak.com/kospi200/recommendations/">오늘의 추천 보기</a></p><p><a href="https://mediamaak.com/kospi200/recommendations/history/">과거 추천 결과</a> · <a href="https://mediamaak.com/guides/backtest-vs-live/">다음 글: 백테스트와 실제 결과가 다른 이유</a></p></div>
<div class="guide-share"><button class="secondary" type="button" data-share-content>공유 링크 복사</button><span role="status" data-share-status></span></div></article>]]></content:encoded>
    </item>
    <item>
      <title>백테스트와 실제 투자 결과가 다른 이유</title>
      <link>https://mediamaak.com/guides/backtest-vs-live/</link>
      <guid isPermaLink="true">https://mediamaak.com/guides/backtest-vs-live/</guid>
      <pubDate>Fri, 16 Oct 2026 09:00:00 +0900</pubDate>
      <description>백테스트의 가정과 실제 체결·비용·유동성·데이터 시점 차이를 확인해 성과를 과장 없이 읽는 방법을 정리합니다.</description>
      <content:encoded><![CDATA[<article data-content-id="guides/backtest-vs-live"><p class="eyebrow">MEDIAMAAK GUIDE</p><h1>백테스트와 실제 투자 결과가 다른 이유</h1><p class="lead">백테스트의 가정과 실제 체결·비용·유동성·데이터 시점 차이를 확인해 성과를 과장 없이 읽는 방법을 정리합니다.</p><p class="guide-meta"><a href="https://mediamaak.com/about.html">MediaMaak 편집팀</a> · 작성·수정 <time datetime="2026-10-16">2026년 10월 16일</time></p>
<figure class="guide-hero"><img src="https://mediamaak.com/assets/images/guides/backtest-vs-live.png" width="1200" height="675" alt="백테스트의 매끄러운 가정 곡선과 실제 운용의 체결·비용 변동을 비교한 그림" loading="eager" fetchpriority="high"><figcaption>가격, 비용, 체결, 데이터 시점의 차이를 함께 확인해야 합니다.</figcaption></figure>
<h2>백테스트 가격은 실제 체결 가격이 아닐 수 있습니다</h2><p>백테스트는 정해진 시점의 시가·종가 또는 규칙으로 만든 가격을 사용합니다. 실제 주문에는 호가 차이, 슬리피지, 주문 지연과 미체결이 생길 수 있습니다. 거래 비용과 세금이 빠진 결과라면 실제 수익은 더 낮아질 수 있습니다.</p>
<h2>과거에 알 수 없던 정보를 사용하지 않았는지 확인하세요</h2><p>당시에는 공개되지 않았던 재무자료나 나중에 확정된 종목 구성을 과거 계산에 사용하면 결과가 부풀려집니다. 신호를 계산한 시점과 데이터가 실제로 이용 가능했던 시점을 구분하고, 현재 살아남은 종목만으로 과거를 계산하지 않았는지도 살펴야 합니다.</p>
<h2>운용 규모와 유동성이 결과를 바꿉니다</h2><p>소액 시뮬레이션에서 가능한 거래가 큰 금액에서는 같은 가격에 체결되지 않을 수 있습니다. 종목별 거래대금, 주문 비중, 동시 보유 수와 최대 투자 비중을 확인하세요. 여러 신호가 같은 날 발생했을 때 자금이 어떻게 배분되는지도 중요합니다.</p>
<h2>검증 구간과 실제 운용 구간을 분리하세요</h2><p>전략을 만든 기간에서만 성과가 좋다면 새로운 시장에서도 같은 결과가 이어진다고 보기 어렵습니다. 조건을 정한 구간과 별도의 검증 구간을 나누고, 실제 운용에서는 규칙 변경 이력을 날짜와 함께 남기는 편이 좋습니다.</p>
<div class="guide-table"><table><caption>백테스트와 실제 운용 비교</caption><thead><tr><th scope="col">확인 항목</th><th scope="col">백테스트</th><th scope="col">실제 운용</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><tr><td>규칙</td><td>과거 전체에 동일 적용</td><td>운영 중 변경과 장애 기록 필요</td></tr></tbody></table></div>
<div class="guide-next"><h2>저장된 거래 기록을 직접 확인하세요</h2><p><a class="button" href="https://mediamaak.com/kospi200-simulation.html">종목별 백테스트 검색</a></p><p><a href="https://mediamaak.com/guides/kospi200-backtest/">백테스트 결과 읽는 순서</a> · <a href="https://mediamaak.com/guides/time-weighted-vs-simple-return/">다음 글: TWR과 단순 수익률 차이</a></p></div>
<div class="guide-share"><button class="secondary" type="button" data-share-content>공유 링크 복사</button><span role="status" data-share-status></span></div></article>]]></content:encoded>
    </item>
    <item>
      <title>입출금이 있으면 수익률은 어떻게 계산할까요?</title>
      <link>https://mediamaak.com/guides/returns-with-cashflows/</link>
      <guid isPermaLink="true">https://mediamaak.com/guides/returns-with-cashflows/</guid>
      <pubDate>Wed, 07 Oct 2026 09:00:00 +0900</pubDate>
      <description>계좌에 넣은 돈과 투자로 늘어난 돈을 구분하고, 시간가중수익률(TWR)을 간단한 숫자 예시로 이해합니다.</description>
      <content:encoded><![CDATA[<article data-content-id="guides/returns-with-cashflows"><p class="eyebrow">MEDIAMAAK GUIDE</p><h1>입출금이 있으면 수익률은 어떻게 계산할까요?</h1><p class="lead">계좌에 넣은 돈과 투자로 늘어난 돈을 구분하고, 시간가중수익률(TWR)을 간단한 숫자 예시로 이해합니다.</p><p class="guide-meta"><a href="https://mediamaak.com/about.html">MediaMaak 편집팀</a> · 작성·수정 <time datetime="2026-10-16">2026년 10월 16일</time></p>
<figure class="guide-hero"><img src="https://mediamaak.com/assets/images/guides/returns-with-cashflows.png" width="1200" height="675" alt="시작 자금과 추가 입금, 투자 성과를 분리해 최종 잔액을 계산하는 흐름 그림" loading="eager" fetchpriority="high"><figcaption>잔액 증가를 입출금과 투자 손익으로 분리해야 수익률을 올바르게 읽을 수 있습니다.</figcaption></figure>
<h2>입금으로 늘어난 잔액은 투자 수익이 아닙니다</h2><p>100만원으로 시작한 계좌에 100만원을 더 넣으면 잔액은 200만원이 됩니다. 가격 변화가 없었다면 투자 수익은 0원입니다. 시작 잔액과 마지막 잔액만 비교하면 추가 입금을 투자 성과로 오해할 수 있습니다.</p>
<h2>입출금이 없는 구간부터 계산합니다</h2><p>시간가중수익률(TWR)은 외부 입출금을 기준으로 구간을 나누고 각 구간의 수익률을 연결해 계산합니다. 아래 예시는 비용과 입금 시점의 가격 변동이 없고, 입금 직전의 평가금액을 알고 있다고 가정한 설명용 숫자입니다.</p>
<div class="guide-table"><table><caption>추가 입금이 있는 두 구간의 예시</caption><thead><tr><th scope="col">단계</th><th scope="col">계좌 평가금액</th><th scope="col">구간 수익률</th></tr></thead><tbody><tr><td>시작</td><td>100만원</td><td>—</td></tr><tr><td>첫 구간 종료, 입금 직전</td><td>110만원</td><td>10%</td></tr><tr><td>100만원 추가 입금 직후</td><td>210만원</td><td>입금 자체는 수익 아님</td></tr><tr><td>두 번째 구간 종료</td><td>231만원</td><td>10%</td></tr></tbody></table></div>
<p>전체 TWR은 <strong>(1 + 0.10) × (1 + 0.10) − 1 = 21%</strong>입니다. 구간별 성장 배수를 곱하므로 10%와 10%를 단순히 더한 20%와 다릅니다. 마지막 잔액 231만원에서 시작금액과 추가 입금 200만원을 빼면 손익은 31만원이지만, 이를 시작 원금으로 나눈 31%는 같은 TWR 지표가 아닙니다.</p>
<h2>실제 기록에서는 시점과 평가 자료가 중요합니다</h2><p>입출금 시점의 평가금액을 알 수 없으면 일별 평가와 입출금 시점 가정 등을 사용한 추정 방식이 필요할 수 있습니다. TWR, 추정 TWR, Modified Dietz처럼 화면에 표시된 계산 방식을 확인하세요. 시작일·종료일, 평가 기준과 비용 반영 범위가 다르면 수익률도 달라집니다.</p>
<h2>KOSPI200과 비교할 때는 기간을 맞추세요</h2><p>전략과 지수의 시작일·종료일을 동일하게 맞추고 두 시계열의 기준점과 누락 자료를 확인합니다. 지수의 배당 포함 여부와 전략의 비용 반영 여부도 함께 살펴야 비교 범위를 이해할 수 있습니다.</p>
<p class="guide-sources">계산 개념 참고: <a href="https://www.gipsstandards.org/standards/gips-standards-for-firms/gips-standards-handbook-for-firms/">CFA Institute, GIPS Standards Handbook — Calculation Methodology</a>. 이 글의 숫자는 개념 설명을 위한 예시이며, 사이트의 GIPS 준수나 인증을 의미하지 않습니다.</p>
<div class="guide-next"><h2>같은 기간의 기록을 확인하세요</h2><p><a class="button" href="https://mediamaak.com/kospi200/">공개 전략 기록 보기</a></p><p><a href="https://mediamaak.com/kospi200/app/#application">Google 로그인 후 이용 신청</a> · <a href="https://mediamaak.com/guides/kospi200-backtest/">종목 시뮬레이션 읽는 법</a></p></div><div class="guide-share"><button class="secondary" type="button" data-share-content>공유 링크 복사</button><span role="status" data-share-status></span></div></article>]]></content:encoded>
    </item>
    <item>
      <title>주가가 오르면 소비도 늘까요?</title>
      <link>https://mediamaak.com/board/bok/2026-10-stock-wealth-effect.html</link>
      <guid isPermaLink="true">https://mediamaak.com/board/bok/2026-10-stock-wealth-effect.html</guid>
      <pubDate>Sat, 05 Sep 2026 09:00:00 +0900</pubDate>
      <description>한국은행 연구를 바탕으로 주식시장과 기업가치에 관한 질문을 쉽게 풀어 읽습니다. 원문 출처와 해석의 범위를 함께 확인하세요.</description>
      <content:encoded><![CDATA[<article data-content-id="board/bok/2026-10-stock-wealth-effect"><header class="board-heading article-heading"><h1>주가가 오르면 소비도 늘까요?</h1><dl class="article-meta"><div><dt>글 작성</dt><dd><time datetime="2026-09-05">2026-09-05</time></dd></div><div><dt>원문 등록</dt><dd><time datetime="2026-05-07">2026-05-07</time></dd></div><div><dt>요약·가공</dt><dd>MediaMaak</dd></div></dl></header><div data-reader-deck data-mode="reading">
<section class="reader-slide layout-cover" id="question" aria-labelledby="question-title"><p class="slide-label">01 · 연구를 읽는 질문</p><h2 id="question-title">주가가 오르면 소비도 늘까요?</h2><figure class="visual-panel visual-cover" aria-labelledby="question-caption"><div class="visual-composition"><div class="visual-item"><h3>주식 자본이득</h3><p>보유한 주식 가치가 오른 부분</p></div><div class="visual-item"><h3>소비</h3><p>생활에 실제로 쓰는 돈</p></div></div><figcaption id="question-caption">주식 가치 변화가 가계 소비와 자산배분에 어떻게 이어지는지 살핀 연구입니다.</figcaption></figure><div class="slide-body"><p>이 글은 특정 주식의 가격을 예측하는 글이 아니라, 가계 전체에서 관찰된 관계를 쉽게 설명합니다.</p></div><p class="source-note">근거: 한국은행 상세 요약 · <a href="#source">출처 확인</a></p></section>
<section class="reader-slide layout-flow" id="terms" aria-labelledby="terms-title"><p class="slide-label">02 · 먼저, 뜻부터</p><h2 id="terms-title">자산효과는 자산 가치 변화가 소비에 미치는 영향입니다.</h2><figure class="visual-panel visual-flow" aria-labelledby="terms-caption"><div class="visual-composition"><div class="flow-row"><h3>주가 상승</h3><div class="flow-route"><strong>주식 가치</strong><div class="flow-transfers"><span class="flow-transfer">영향 →</span></div><strong>소비 여력</strong></div></div><div class="flow-row"><h3>자산 배분</h3><div class="flow-route"><strong>자본이득</strong><div class="flow-transfers"><span class="flow-transfer">이동 →</span></div><strong>소비·다른 자산</strong></div></div></div><figcaption id="terms-caption">자산효과가 있다는 말은 모든 사람이 같은 만큼 소비를 늘린다는 뜻은 아닙니다.</figcaption></figure><div class="slide-body"><p>연구는 가계금융복지조사 가구패널의 소비·주식자산 자료와 거시 자료를 함께 활용했습니다.</p></div><p class="source-note">근거: 한국은행 상세 요약 · <a href="#source">출처 확인</a></p></section>
<section class="reader-slide layout-metrics" id="estimate" aria-labelledby="estimate-title"><p class="slide-label">03 · 연구의 추정</p><h2 id="estimate-title">소비로 이어지는 비율은 상대적으로 작게 나타났습니다.</h2><figure class="visual-panel visual-metrics" aria-labelledby="estimate-caption"><div class="visual-composition"><div class="visual-item"><h3>약 1.3%</h3><p>연구가 추정한 자본이득의 소비 활용 비율</p></div><div class="visual-item"><h3>3~4%</h3><p>공개 요약에서 든 미국·유럽 주요국 비교 범위</p></div><div class="visual-item"><h3>2012~2024</h3><p>공개 요약이 설명한 소비·주식자산 자료 범위</p></div></div><figcaption id="estimate-caption">수치는 연구의 추정·비교이며 개인의 소비를 예측하는 값은 아닙니다.</figcaption></figure><div class="slide-body"><p>공개 요약은 주가 상승에 따른 자본이득 가운데 약 1.3%가 소비 재원으로 활용된 것으로 추정했습니다.</p></div><p class="source-note">근거: 한국은행 상세 요약 · <a href="#source">출처 확인</a></p></section>
<section class="reader-slide layout-relationship" id="reasons" aria-labelledby="reasons-title"><p class="slide-label">04 · 왜 작게 나타났나요?</p><h2 id="reasons-title">주식 보유의 폭과 자본이득의 다음 목적지가 함께 작용했습니다.</h2><figure class="visual-panel visual-relationship" aria-labelledby="reasons-caption"><div class="visual-composition"><div class="visual-item"><h3>주식 보유 기반</h3><p>주식 자산이 일부 가계에 집중된 구조</p></div><span class="relationship-link">관련</span><div class="visual-item"><h3>소비 반응</h3><p>주가 상승을 소비로 연결하는 정도</p></div></div><figcaption id="reasons-caption">연구는 구조적 요인과 가계의 자산배분 행태를 함께 설명합니다.</figcaption></figure><div class="slide-body"><p>연구는 자본이득이 부동산으로 먼저 이동하는 경향도 소비 여력 확대를 제한한 요인으로 제시했습니다.</p></div><p class="source-note">근거: 한국은행 상세 요약 · <a href="#source">출처 확인</a></p></section>
<section class="reader-slide layout-comparison" id="change" aria-labelledby="change-title"><p class="slide-label">05 · 최근 변화와 위험</p><h2 id="change-title">참여층이 넓어지는 변화와 하락 위험을 함께 봐야 합니다.</h2><figure class="visual-panel visual-comparison" aria-labelledby="change-caption"><div class="visual-composition"><div class="visual-item"><h3>변화 가능성</h3><p>주식 보유와 참여층이 넓어지면 자산효과가 커질 수 있음</p></div><div class="visual-item"><h3>주의할 위험</h3><p>주가 하락과 빚 부담이 함께 커질 수 있음</p></div></div><figcaption id="change-caption">두 항목은 미래를 단정하는 예측이 아니라 연구가 제시한 조건과 주의점입니다.</figcaption></figure><div class="slide-body"><p>레버리지는 빌린 돈을 더해 투자 규모를 키우는 방식이며, 손실과 빚 부담도 함께 커질 수 있습니다.</p></div><p class="source-note">근거: 한국은행 상세 요약 · <a href="#source">출처 확인</a></p></section>
<section class="reader-slide layout-takeaway" id="limits" aria-labelledby="limits-title"><p class="slide-label">06 · 어디까지 알 수 있나요?</p><h2 id="limits-title">이 연구가 내일의 주가나 소비를 알려 주지는 않습니다.</h2><figure class="visual-panel visual-takeaway" aria-labelledby="limits-caption"><div class="visual-composition"><div class="visual-item"><h3>살펴본 것</h3><p>주식 자본이득·소비·자산배분 사이의 과거 관계</p></div><div class="visual-item"><h3>알 수 없는 것</h3><p>특정 종목의 가격과 개인별 소비 변화</p></div></div><figcaption id="limits-caption">역자산효과는 자산 가격 하락이 소비 감소로 이어질 수 있는 현상입니다.</figcaption></figure><div class="slide-body"><p>이 글은 종목의 매수·매도를 권하지 않습니다.</p></div><p class="source-note">근거: 한국은행 상세 요약 · <a href="#source">출처 확인</a></p></section></div>
<section class="source-block" id="source" aria-labelledby="sourceTitle"><h2 id="sourceTitle">출처와 읽을 때의 주의점</h2><p>BOK 이슈노트 제2026-10호 · 우리나라 주식 자산효과에 대한 평가<br>담당부서: 거시분석팀 · 원문 등록일: 2026-05-07</p><p><a href="https://www.bok.or.kr/portal/bbs/P0002353/view.do?menuNo=200433&amp;nttId=10097868">한국은행 원문 안내에서 연구 보기 →</a></p><p>한국은행 공식 상세 페이지의 공개 요약을 바탕으로 MediaMaak가 쉽게 요약·가공했습니다. 이 글은 한국은행의 공식 전망이나 투자 조언이 아닙니다.</p></section><div class="guide-share"><button class="sidebar-parent" type="button" data-share-content>공유 링크 복사</button><span role="status" data-share-status></span></div><p><a href="https://mediamaak.com/guides/">KOSPI200와 수익률 기초 안내 →</a></p></article>]]></content:encoded>
    </item>
  </channel>
</rss>
