한국투자증권 공식 백테스터 — 기본값 3가지 함정
0.0이라 슬리피지 모델 코드가 아예 생성되지 않고,
② 슬리피지를 켜도 호가 단위표가 2023년 개편 전 기준이라 세 가격대에서 틱이 현행보다 크며,
③ 샤프지수·CAGR이 워밍업 구간 때문에 부풀려집니다(README가 "60일 이상이면 실제의 1.5~2배"라고 직접 적고 있습니다).
세 가지 모두 공식 코드와 README 원문에서 확인한 것이고, 고치는 방법도 아래에 있습니다.
목차
- 저장소에 무엇이 들어왔나 —
backtester와strategy_builder - 함정 ① 슬리피지 기본값이 0이다
- 함정 ② 호가 단위표가 2023년 개편 전이다
- 함정 ③ 워밍업이 샤프지수와 CAGR을 부풀린다
- 데이터는 어디서 오나 —
FHKST03010100한 줄기 - Lean CSV로 바뀌는 순간 잃는 것 3가지
- 그래서 어떻게 쓰나 — 체크리스트
- 자주 묻는 질문
1. 저장소에 무엇이 들어왔나
한국투자증권 공식 저장소 koreainvestment/open-trading-api는 오랫동안
샘플 스크립트 모음이었습니다(그 구조는
한국투자증권 API 파이썬 예제 — 공식 깃허브 실행에 정리해 두었습니다).
지금은 그 위에 독립 애플리케이션 두 개가 더 들어 있습니다.
| 폴더 | 정체 | 구성 |
|---|---|---|
strategy_builder/ |
전략 설계 + 시그널 생성 + 모의/실전 주문 | 비주얼 빌더로 지표·조건·리스크를 짜고 .kis.yaml로 내보낸다 |
backtester/ |
백테스팅 엔진 | QuantConnect Lean(Docker) + FastAPI 백엔드(8002) + Next.js 프론트(3001) + MCP 서버(3846) |
핵심은 백테스트 엔진을 자체 구현하지 않고 QuantConnect Lean을 Docker 컨테이너로 직접 돌린다는 점입니다. 실행기 소스 첫 줄이 그렇게 말합니다.
# backtester/kis_backtest/lean/executor.py
"""Lean Docker 실행기
Docker 컨테이너로 Lean 백테스트 직접 실행. (Lean CLI 불필요)
"""
# Lean Docker 이미지
LEAN_IMAGE = "quantconnect/lean:latest"
kis_backtest는 백엔드 없이 파이썬 라이브러리로만 쓸 수도 있습니다.
입력 경로 세 갈래(파이썬 프리셋 · .kis.yaml 파일 · API 요청 JSON)가 전부
StrategySchema 하나로 수렴한 뒤 LeanCodeGenerator가 Lean용 코드를 만들어 냅니다.
from kis_backtest import LeanClient, STRATEGY_REGISTRY, LeanCodeGenerator
import kis_backtest.strategies.preset # 10종 전략 자동 등록
schema = STRATEGY_REGISTRY.build("sma_crossover", fast_period=20, slow_period=50)
generator = LeanCodeGenerator(schema)
code = generator.generate(
symbols=["005930"],
start_date="2024-01-01",
end_date="2024-12-31",
)
client = LeanClient()
result = await client.run_backtest(code) # Docker 필요
규모는 작지 않습니다 — 프리셋 전략 10종(sma_crossover,
momentum, week52_high, volatility_breakout 등),
지표 레지스트리 80개(sma·rsi·macd부터
supertrend·ichimoku·frama·vidya까지),
예제 스크립트 8개, 파라미터 최적화(Grid/Random Search), HTML 리포트.
먼저 정리해 둘 것. 이건 증권사가 파는 상품이 아니라 공식 저장소에 포함된 도구입니다. 아래 세 가지는 "버그"가 아니라 기본값을 그대로 믿었을 때 결과가 낙관적으로 나오는 지점입니다.
2. 함정 ① 슬리피지 기본값이 0이다
코드 생성 설정은 CodeGenConfig 한 곳에 모여 있습니다. 원문 그대로입니다.
# backtester/kis_backtest/codegen/generator.py
@dataclass
class CodeGenConfig:
"""코드 생성 설정"""
market: str = "krx" # krx, us
commission_rate: float = 0.00015 # 0.015%
tax_rate: float = 0.002 # 0.2% (KRX 매도세)
slippage: float = 0.0 # 슬리피지 (기본 0%)
initial_capital: float = 100_000_000 # 1억원
수수료와 세금은 들어갑니다. 생성되는 수수료 모델은 이렇게 생겼습니다 — 매수는 수수료만, 매도는 수수료 + 세금입니다.
class CustomFeeModel(FeeModel):
def GetOrderFee(self, parameters):
value = abs(parameters.Order.GetValue(parameters.Security))
if parameters.Order.Direction == OrderDirection.Buy:
fee = value * 0.00015
else:
fee = value * (0.00015 + 0.002)
return OrderFee(CashAmount(fee, "USD"))
세율 자체는 현행과 맞습니다. 2026년 기준 매도 부담은 코스피가
증권거래세 0.05% + 농어촌특별세 0.15% = 0.20%, 코스닥이 0.20%라
tax_rate = 0.002가 두 시장 모두에 들어맞습니다(세율은 개정되므로 적용 시점 기준은 공식 자료로 확인하십시오.
비용 항목 전체는 백테스트 거래비용 — 수수료·세금·슬리피지에 있습니다).
문제는 세 번째 줄입니다. 슬리피지 모델을 만드는 코드가 이렇게 시작합니다.
def _generate_slippage_model(self) -> str:
"""슬리피지 모델 생성 - KRX 호가 단위 기반 KRXSlippageModel"""
if self.config.slippage <= 0:
return ""
슬리피지가 0이면 모델이 "약하게" 적용되는 게 아니라, 클래스 자체가 생성되지 않습니다. 기본 설정으로 돌린 결과는 모든 체결이 종가에 정확히 이뤄졌다고 가정한 수치입니다. 호가가 얇은 종목, 시가총액 하위 종목, 변동성 돌파처럼 급등 구간에 진입하는 전략일수록 이 가정과 실제의 거리가 멉니다. 백테스트와 실거래가 왜 벌어지는지는 백테스트와 실전의 괴리에서 따로 다뤘습니다.
고치는 법은 간단합니다. CodeGenConfig(slippage=...)에 값을 넣어 모델을 켜면 됩니다.
다만 켠 순간 두 번째 함정이 나옵니다.
3. 함정 ② 호가 단위표가 2023년 개편 전이다
슬리피지 모델은 비율을 곱하는 데서 끝나지 않고, KRX 호가 가격단위(틱)에 맞춰 반올림합니다. 매수는 올림(ceiling), 매도는 내림(floor)으로 유효 틱에 붙입니다. 설계 자체는 성의 있습니다. 문제는 테이블 값입니다.
class KRXSlippageModel:
"""한국 주식 호가 단위 기반 슬리피지 모델
매수: (종가 + raw_slip) → 올림(ceiling) → 상위 유효 틱
매도: (종가 - raw_slip) → 내림(floor) → 하위 유효 틱
"""
def _tick(self, price):
if price < 1000: return 1
elif price < 5000: return 5
elif price < 10000: return 10
elif price < 50000: return 50
elif price < 100000: return 100
elif price < 500000: return 500
else: return 1000
한국거래소는 2023년 1월 25일 호가가격단위를 개편했습니다. 1,000~2,000원 구간이 5원 → 1원, 10,000~20,000원 구간이 50원 → 10원, 100,000~200,000원 구간이 500원 → 100원으로 세분화됐습니다. 위 코드의 경계값(1000·5000·10000·50000·100000·500000)은 개편 전 구간과 일치합니다.
| 가격대 | 코드의 틱 | 현행 KRX(2023-01-25~) | 차이 |
|---|---|---|---|
| 1,000 ~ 2,000원 | 5원 | 1원 | 5배 |
| 10,000 ~ 20,000원 | 50원 | 10원 | 5배 |
| 100,000 ~ 200,000원 | 500원 | 100원 | 5배 |
| 그 외 구간 | 일치 | ||
방향에 주의하십시오. 이 어긋남은 슬리피지를 실제보다 크게 만듭니다
— 매수 체결가를 필요 이상 위 틱으로 올려 붙이기 때문입니다.
함정 ①(슬리피지 0)과는 반대 방향이라 둘이 상쇄된다고 생각하면 곤란합니다.
기본값에서는 ①만 작동하고, 값을 넣는 순간 ②만 작동합니다.
특히 10,000~20,000원은 코스피·코스닥에서 종목 수가 두터운 가격대라,
이 구간을 다루는 전략이면 _tick()을 현행 기준으로 고쳐 쓰는 편이 맞습니다.
현재 호가단위는 한국거래소 공지가 기준입니다(규정은 바뀝니다).
4. 함정 ③ 워밍업이 샤프지수와 CAGR을 부풀린다
이건 숨겨진 것도 아닙니다. 공식 README가 직접 경고하고 있습니다. 그런데 결과 화면의 숫자만 보는 사람은 이 문단을 읽지 않습니다.
"전략에 워밍업(lookback) 파라미터가 있으면 초반 수십 거래일 동안 포트폴리오가 idle 상태(일간 수익률 = 0)입니다. 이 기간이 Sharpe 계산에 포함되면 수익률 표준편차가 인위적으로 낮아져 Sharpe가 실제보다 크게 표시될 수 있습니다."
| 워밍업 | README가 제시한 보정 기준 |
|---|---|
| 없음 | 표시값 그대로 |
| 30일 이하 | 소폭 과대계상 가능 |
| 60일 이상 | 실제의 1.5~2배 수준으로 부풀려질 수 있음 |
생성되는 Lean 코드에 self.SetWarmUp(warmup, Resolution.Daily)가 들어가므로 워밍업 구간은 실제로 존재하고,
성과 지표 계산에서 그 기간을 따로 빼지 않습니다. 일간 수익률이 0인 날이 60일 섞이면
분모(표준편차)가 내려가고 샤프지수는 그만큼 올라갑니다.
CAGR도 같은 뿌리에서 왜곡됩니다 — 연환산 분모가 첫 거래일이 아니라 백테스트 시작일이라,
거래가 절반 기간에만 일어났어도 전체 기간으로 나눕니다.
지표를 손으로 다시 계산하는 방법은
샤프지수·MDD 계산 — quantstats에 있습니다.
5. 데이터는 어디서 오나 — FHKST03010100 한 줄기
백테스트 데이터는 데이터 벤더가 아니라 KIS API 일봉 TR 하나에서 옵니다.
KISDataProvider._get_daily_bars()가 부르는 것은
/uapi/domestic-stock/v1/quotations/inquire-daily-itemchartprice,
tr_id = FHKST03010100이고, 이 TR의 함정은
KIS 일봉 API FHKST03010100에 정리해 두었습니다.
params = {
"FID_COND_MRKT_DIV_CODE": "J",
"FID_INPUT_ISCD": symbol,
"FID_INPUT_DATE_1": start_str,
"FID_INPUT_DATE_2": current_end,
"FID_PERIOD_DIV_CODE": "D",
"FID_ORG_ADJ_PRC": "0", # 수정주가
}
FID_ORG_ADJ_PRC = "0"— 0이 수정주가입니다. 액면분할·권리락이 반영된 시계열을 받습니다.- 100건 페이지네이션 —
len(data) < 100이면 종료하고, 아니면 마지막 날짜에서 하루를 빼 다시 요청합니다. - 분봉은 사실상 백테스트용이 아닙니다 —
_get_minute_bars()는 단일 날짜만 받고 최대 10페이지에서 끊깁니다. 분봉 수집 구조는 주식 분봉 데이터 KIS API에 따로 있습니다.
호출 한도 처리도 코드에 상수로 박혀 있습니다. EGW00201이 뜨면 61초를 자고 최대 3회까지 재시도합니다.
# EGW00201: 초당 API 호출 한도 초과
_RATE_LIMIT_CODE = "EGW00201"
_RATE_LIMIT_WAIT = 61 # 초 (KIS 한도 초기화 대기)
_RATE_LIMIT_MAX_RETRIES = 3
종목 수가 많은 유니버스를 처음 받을 때 체감상 오래 걸리는 이유가 여기 있습니다. 이 에러의 구조와 회피 설계는 KIS EGW00201 초당 호출 제한에 있습니다.
6. Lean CSV로 바뀌는 순간 잃는 것 3가지
받은 데이터는 DataConverter가 Lean이 읽는 CSV
(YYYYMMDD,open,high,low,close,volume, 헤더 없음)로 떨궈
.lean-workspace/data/equity/krx/daily/에 넣습니다. 이 변환에서 세 가지가 사라집니다.
① 가격이 정수로 반올림된다
if market_type == "krx":
# 한국 주식: 원 단위 (정수)
for col in required_cols:
df_out[col] = df_out[col].astype(float).round(0).astype(int)
국내 호가가 정수라 대개는 무해합니다. 다만 수정주가는 조정계수가 곱해진 값이라 과거 구간에서 소수가 생길 수 있고, 저가주일수록 반올림 오차의 상대 비중이 커집니다.
② 여러 종목을 넣으면 기간이 교집합으로 잘린다
get_date_range()는 종목별 시작일 중 가장 늦은 것,
종료일 중 가장 이른 것을 잡습니다. 즉 모든 종목이 공통으로 존재하는 구간만 남습니다.
2021년 상장 종목 하나를 유니버스에 넣으면 10년 백테스트가 조용히 4년으로 줄어듭니다.
에러도 경고도 없습니다. 결과 화면의 기간을 항상 확인하십시오.
③ map/factor 파일이 없다
Lean 설정에는 LocalDiskMapFileProvider·LocalDiskFactorFileProvider가 지정돼 있지만,
setup_lean_data.sh가 만드는 폴더는 market-hours·symbol-properties·
equity/krx/daily뿐입니다. 종목코드 변경·상장폐지 이력(map)과 조정계수(factor) 파일은 만들지 않습니다.
여기서 생존편향이 들어옵니다. 백테스트 유니버스는 지금 KIS API로 조회되는 종목으로만 구성됩니다. 그 사이 상장폐지된 종목은 애초에 후보에 오르지 않습니다. 이게 성과를 얼마나 밀어 올리는지는 생존편향 백테스트 — 10년간 사라진 266종목에서 실측으로 다뤘습니다. 도구를 바꾼다고 해결되는 문제가 아니라, 유니버스를 어떻게 만들었는지의 문제입니다.
재현성 관점에서 하나 더. 이미지가 quantconnect/lean:latest라
몇 달 뒤 같은 코드를 돌려도 엔진 버전이 다를 수 있습니다 — 결과를 남겨야 한다면 태그를 고정하십시오.
수수료가 CashAmount(fee, "USD")로 붙는 것도 기억해 둘 지점입니다.
단일 시장만 쓰면 산술은 일관되지만 국내와 해외를 섞는 순간 통화 처리를 따로 봐야 합니다.
7. 그래서 어떻게 쓰나
도구 자체는 쓸 만합니다. 엔진(Lean)은 검증된 오픈소스고, 데이터 경로가 KIS API 하나라 "실거래에 쓸 데이터로 백테스트한다"는 조건이 자동으로 맞춰집니다.
돌리기 전 체크리스트
- 슬리피지를 0이 아닌 값으로 넣는다. 넣지 않으면 종가 체결 가정이다.
- 슬리피지를 켰다면
_tick()테이블을 현행 KRX 호가단위로 고친다. - 워밍업이 있는 전략이면 샤프지수는 참고용으로만 보고, MDD와 자산 곡선의 기울기를 같이 본다.
- 자산 추이 차트에서 실제 첫 거래일을 확인한다. CAGR 분모가 그만큼 늘어나 있다.
- 멀티 종목이면 실제 백테스트 기간이 교집합으로 줄지 않았는지 본다.
- 승률 0%인데 수익이면 미청산 포지션이 크다는 뜻이다 — 매도 조건을 다시 본다.
- 상장폐지 종목이 빠진 유니버스라는 것을 전제하고 결과를 읽는다.
프로그램을 직접 짜서 돌릴지, 증권사 도구를 쓸지의 갈림길이라면 증권사 API 비교 — 키움·KIS·LS·대신에서 플랫폼 층을 먼저 정리하시는 편이 빠릅니다.
8. 자주 묻는 질문
한국투자증권이 공식 백테스터를 제공하나요?
공식 저장소 koreainvestment/open-trading-api 안에 backtester(QuantConnect Lean을
Docker로 돌리는 백테스팅 시스템)와 strategy_builder(전략을 비주얼로 설계해
.kis.yaml로 내보내는 도구)가 들어 있고, 두 도구는 이 포맷을 공유합니다.
다만 별도 상품이 아니라 저장소에 포함된 샘플 성격이므로,
구성과 기본값은 현재 저장소의 README와 소스로 확인하십시오.
KIS 백테스터의 기본 거래비용은 어떻게 설정돼 있나요?
CodeGenConfig의 기본값은 수수료 0.00015(0.015%), 세금 0.002(0.2%),
슬리피지 0.0, 초기자본 1억원입니다. 수수료는 매수·매도 양쪽에, 세금은 매도에만 붙습니다.
슬리피지 값이 0 이하이면 모델 코드 자체가 생성되지 않으므로, 기본 설정 결과는 종가 체결 가정입니다.
백테스터의 호가 단위표가 현재 KRX 규정과 같나요?
다릅니다. 한국거래소가 2023년 1월 25일 개편으로 1,000~2,000원을 1원, 10,000~20,000원을 10원,
100,000~200,000원을 100원으로 세분화했는데, 코드의 _tick()은 개편 전 구간을 씁니다.
세 구간에서 코드의 틱이 현행보다 5배 큽니다.
슬리피지를 켜서 쓸 계획이면 이 표를 직접 고쳐야 하고, 현재 규정은 한국거래소 공지로 확인하십시오.
샤프지수가 비정상적으로 높게 나오는 이유가 뭔가요?
공식 README가 직접 설명합니다. 워밍업(lookback)이 있으면 초반 수십 거래일 동안 일간 수익률이 0이라 표준편차가 인위적으로 낮아져 샤프가 크게 표시되고, README는 워밍업 60일 이상이면 실제의 1.5~2배까지 부풀려질 수 있다고 적고 있습니다. CAGR도 같은 이유로 왜곡됩니다 — 연환산 분모가 첫 거래일이 아니라 백테스트 시작일이기 때문입니다.
백테스트 승률이 0%인데 총 수익률이 플러스면 잘못된 건가요?
버그가 아니라 집계 기준 차이입니다. 승률은 청산된 포지션만 셉니다. 종료 시점까지 들고 있는 미청산 포지션의 평가이익은 총 수익률과 최종 자산에는 반영되지만 승률에서는 빠집니다. 승률이 낮은데 총 수익률이 높으면 미실현 이익 비중이 크다는 신호이고, 실전에서는 매도 전까지 이익이 확정되지 않으므로 매도 조건 설계를 같이 봐야 합니다.
마무리 — 엔진이 아니라 가정을 읽는다
공식 백테스터가 생긴 것은 진전입니다. 엔진은 QuantConnect Lean이고, 데이터는 실거래에 쓸 KIS API에서 오고, 소스가 전부 열려 있어 가정을 눈으로 확인할 수 있습니다. 이 글에서 짚은 세 가지도 소스를 열었기 때문에 확인된 것입니다. 반대로 말하면 결과를 읽는 일은 여전히 사람 몫이고, 슬리피지 0과 워밍업 구간은 둘 다 숫자를 좋은 쪽으로 밉니다. 어떤 백테스트도 과거 데이터에 대한 계산일 뿐 미래 성과를 보장하지 않습니다. 본문의 기본값·세율·호가단위는 확인 시점 기준이며, 적용 전에 저장소의 현재 소스와 한국거래소·국세청 공식 자료로 대조하십시오.
알고랩(퀀트웍스)은 자동매매 프로그램을 맞춤 제작하는 도구 제공 사업자이며, 투자자문업·투자일임업을 영위하지 않습니다. 이 글은 공개된 공식 저장소의 코드와 문서를 기술적으로 설명한 것으로, 특정 종목·전략의 매매를 권유하지 않습니다. 백테스트 수치는 과거 데이터에 대한 계산이며 미래 수익을 보장하지 않습니다. 세율·호가단위·API 스펙은 변경될 수 있으므로 공식 자료를 확인하십시오.