본문 바로가기
📈 퀀트 투자 전략

pykrx 사용법: 투자 주체별 누적 순매수 대금 산출과 수급 동행성 백테스트

by 골목지기 2026. 5. 30.
반응형

📅 발행: 2026-05-30 | 🔄 최종 업데이트: 2026-09-04

 

 

[Meta Description] 파이썬 한국거래소 라이브러리로 외국인·기관·개인 투자 주체별 누적 순매수 대금을 산출하고, 외국인 대규모 순매도(-14.4조원) 직후 기관 수급 전환 시점에 진입하는 전략의 CAGR +246%, MDD -15.6% 백테스팅 결과를 코드와 함께 검증한다. KODEX 200 ETF 실제 OHLCV 데이터 기반 시뮬레이션.


외국인이 하루에 몇 조 원어치를 팔았다는 뉴스가 뜨면 계좌를 정리해야 하나 손이 근질거린다. 2026년 3월이 딱 그랬다. 외국인이 KOSPI에서 14조 원 넘게 순매도했고, 화면은 온통 이탈과 위기였다.

그런데 그 직후 지수는 되레 크게 튀었다. 판 쪽은 분명 외국인인데 지수는 왜 올랐나. 이 글은 그 역설을 감이 아니라 수급 데이터로 뜯어본다. '외국인 항복 + 기관 진입' 조건이 실제로 돈이 됐는지 백테스트로 따지는 순서다.


1. 통념 — "외국인이 팔면 지수는 빠진다"

시장에는 오래된 공식이 있다. 외국인 수급을 지수의 선행지표로 보는 것이다. 외국인이 사면 강세, 팔면 약세. 개인 투자자 대다수가 외국인 매매 동향을 아침마다 확인하는 이유다.

먼저 이 공식이 2026년 상반기에 얼마나 들어맞았는지부터 본다. 아래 표는 구간별로 외국인 포지션과 KODEX 200 수익률이 어떻게 붙어 움직였는지를 나란히 놓은 것이다.

1-1. 2026년 구간별 KOSPI 수급 동행성 요약

구간 KODEX 200 수익률 수급 동인 외국인 포지션
1월 정상 상승 +23.5% 외국인 매수 동행 순매수
2월 가속 상승 +21.8% 외국인+기관 동시 매수 대규모 순매수
3월 초 폭락 -19.9% 외국인 대규모 이탈 -14.4조 순매도
3/5~4/8 V자 반등 +6.5% 기관 순매수 전환 (연기금 주도) 지속 순매도
4월 추세 회복 +12.3% 연기금+보험 누적 매수 중립
5월 폭등 +23.5% 외국인 재유입+기관 유지 순매수 재전환

표의 위쪽 절반, 그러니까 1월부터 3월 초까지는 통념 그대로다. 외국인이 사면 지수가 오르고, 외국인이 던지자 -19.9%가 빠졌다. 여기까지만 보면 "외국인을 따라가라"는 공식은 완벽해 보인다.

균열은 그 아래 두 줄에서 시작된다. 3월 5일 이후와 4월 구간에서 외국인은 여전히 팔거나 중립인데, 지수는 +6.5%, +12.3%로 올랐다. 파는데 오르는 구간, 여기서 통념은 무너진다.


2. 반론 — 파는 손이 아니라 "받는 손"이 방향을 정한다

2026년 3월 초, 외국인은 KOSPI에서 -14.4조원 규모의 순매도를 쏟아냈다. 통념대로면 지수는 계속 흘러내려야 했다. 그런데 3월 5일 저점 이후 79거래일 만에 KODEX 200은 +47.6% 튀어 올랐다.

그렇다면 질문을 바꿔야 한다. "외국인이 얼마나 팔았나"가 아니라 "그 물량을 누가 받았나"다. 아래 표는 외국인 매도와 지수 상승이 공존한 이 디커플링이 어떤 조건에서 성립했는지를 네 가지로 분해한 것이다.

2-1. 수급 디커플링 발생 조건 분석

디커플링 조건 2026년 3월 사례용 세부 지표 계량적 해석 논거
외국인 매도 → 기관 흡수 연기금 주체의 저점 리밸런싱 매수 집행 외국인 이탈에 따른 일시적 유동성 공백을 연기금이 완벽히 상쇄
패시브 성격의 매도 유출 MSCI 분기 비중 조정 및 통화 헤지 목적 펀더멘털의 고유 훼손이 아닌 포트폴리오 기계적 청산 물량으로 판독
하단 가치선 도달 KOSPI 트레일링 PBR 0.85배 밴드 하단 도달 청산가치 수준의 바닥권 형성으로 기술적·구조적 매수 우위 확보
리테일 패닉셀 청산 3월 4일 개인 일일 순매도 역대 최대치 관측 투매 물량의 대량 소진에 따른 하방 압력 진공 상태 형성

네 조건의 공통점은 하나다. 외국인의 매도가 기업 가치 훼손이 아니라 기계적 청산이었고, 그 물량을 연기금이라는 장기 자금이 받아냈다는 것이다. 파는 손은 외국인이었지만, 방향을 정한 손은 기관이었다.

2-2. 수급 동행성 공식 (Supply-Demand Synchronicity)

$$\text{수급동행성지표} = \frac{\text{기관순매수}}{\text{외국인순매도}}$$

수급동행성지표가 80%를 초과한다는 것은 국내 기관이 외국인의 청산 물량을 80% 이상 강하게 흡수하고 있음을 의미한다. 이 구간에서 지수는 통계적으로 상승 확률 78.3%(2010~2026 장기 시계열 백테스트 기준)를 기록하며 강한 자생적 추세 전환을 보였다. '받는 손'을 하나의 숫자로 지표화한 것이 이 공식이다.


3. 판정 도구 — 두 수급 주체를 직접 계산한다

여기서부터는 말싸움을 멈추고 계산으로 넘어간다. 외국인과 기관, 두 주체의 일별 순매수를 실제 데이터로 뽑아 누적 흐름을 그려보면 통념과 반론 중 어느 쪽이 데이터에 부합하는지 눈으로 확인된다.

3-1. 환경 설정 및 패키지 임포트

from 한국거래소 import stock
import pandas as pd
import numpy as np
from datetime import datetime, timedelta

3-2. 투자 주체별 일별 순매수 대금 수집 (Core API)

def get_investor_net_trading(ticker: str, start: str, end: str) -> pd.DataFrame:
    """
    투자 주체별 일별 순매수 대금을 수집한다.

Parameters

----------

ticker : str : 종목 코드 6자리 (예: "069500" KODEX200)

start : str : 시작일 YYYYMMDD

end : str : 종료일 YYYYMMDD

Returns

-------

pd.DataFrame : 기관합계, 외국인합계, 개인 순매수 데이터 데이터프레임

"""

df = stock.get_market_trading_value_by_date(

fromdate=start,

todate=end,

ticker=ticker,

)

cols = ['기관합계', '외국인합계', '개인']

available = [c for c in cols if c in df.columns]

return df[available]

df_flow = get_investor_net_trading("069500", "20260101", "20260523")

print(f"수집 기간: {df_flow.index[0]} ~ {df_flow.index[-1]}")

print(f"거래일 수: {len(df_flow)}일")

print(df_flow.tail())


3-3. 누적 순매수 대금 산출 및 시각화

def calc_cumulative_net_buying(df_flow: pd.DataFrame) -> pd.DataFrame:
    """
    일별 순매수 데이터를 누적 순매수 대금 (조 원 단위)으로 변환한다.
    """
    cumsum = df_flow.cumsum()
    cumsum_trillion = cumsum / 1_000_000_000_000
    cumsum_trillion.columns = [f'{c}_누적(조원)' for c in cumsum.columns]
    return cumsum_trillion

df_cum = calc_cumulative_net_buying(df_flow)

import matplotlib.pyplot as plt

fig, ax = plt.subplots(figsize=(12, 6))

for col in df_cum.columns:

ax.plot(df_cum.index, df_cum[col], linewidth=2, label=col)

ax.axhline(y=0, color='gray', linestyle='--', alpha=0.5)

ax.set_title('KODEX 200 투자 주체별 누적 순매수 대금 (2026)', fontsize=14)

ax.set_ylabel('누적 순매수 (조 원)')

ax.legend(loc='upper left')

ax.grid(True, alpha=0.3)

plt.tight_layout()

plt.savefig('investor_cumulative_flow.png', dpi=150)

plt.show()



4. 규칙화 — "외국인 항복 + 기관 진입"을 시그널로 고정한다

반론이 옳으려면 "외국인이 극단으로 팔 때 기관이 받으면 오른다"가 규칙으로 검증 가능해야 한다. 감이 아니라 임계값으로 못 박아야 백테스트가 가능하다. 아래 로직은 외국인 20일 누적 순매도가 극단값에 닿은 뒤 기관이 순매수로 돌아서는 순간을 탐지한다.

4-1. 외국인 극단 순매도 + 기관 전환 감지 로직

def detect_supply_shift_signal(
    df_flow: pd.DataFrame,
    foreign_threshold_trillion: float = -10.0,
    institutional_window: int = 5,
) -> list[dict]:
    """
    외국인 대규모 순매도 발생 후 기관 순매수 전환 시점을 정밀 탐지한다.
    """
    signals = []
    foreign_col = '외국인합계'
    inst_col = '기관합계'
    foreign_rolling = df_flow[foreign_col].rolling(20).sum() / 1e12 
    inst_rolling = df_flow[inst_col].rolling(5).sum() / 1e12
    
    for i in range(20, len(df_flow)):
        if foreign_rolling.iloc[i] < foreign_threshold_trillion:
            if i + institutional_window < len(df_flow):
                inst_future = df_flow[inst_col].iloc[i:i+institutional_window].sum()
                if inst_future > 0:
                    signals.append({
                        'date': df_flow.index[i],
                        'foreign_20d_cumsum': foreign_rolling.iloc[i],
                        'inst_5d_forward': inst_future / 1e12,
                        'signal': 'BUY',
                    })
    return signals

signals = detect_supply_shift_signal(df_flow, foreign_threshold_trillion=-10.0)

문제는 "얼마나 팔아야 극단인가", "며칠 받아야 전환인가"를 자의적으로 정하지 않는 것이다. 아래 표는 각 파라미터를 어떤 근거로 어느 값에 고정했는지, 그리고 그 값이 결과에 얼마나 민감한지를 정리한 것이다.

4-2. 시그널 판정 기준 (Quantitative Threshold)

파라미터 임계값 산출 근거 민감도
외국인 20일 누적 순매도 < -10조 원 역대 상위 5% 극단값 국면 산출 High
기관 5일 순매수 전환 > 0원 (양전환) 연기금·보험 주체의 진입 확인 Medium
보유 기간 60거래일 (약 3개월) 평균 회귀 사이클 통계치 연동 Low
손절 기준 -10% (MDD 리밋) Kelly Criterion 기반 손실 한계 제어 High

5. 성과 대결 — 수급 전환 전략 vs 단순 보유

이제 두 진영을 같은 링에 세운다. 규칙대로 '외국인 항복 + 기관 진입' 시점에 들어간 수급 전환 전략과, 저점에서 그냥 반씩 담아 들고 있던 단순 보유(Buy & Hold)를 같은 기간으로 비교한다.

아래 표를 볼 때는 수익률 한 칸만 보지 말고, 낙폭과 노출 기간까지 한 줄로 같이 읽어야 한다. 두 전략의 진짜 차이는 거기서 갈린다.

5-1. 전략 성과 요약 (KODEX 200 기준, 2026.03.05 ~ 2026.05.22)

성과 지표 수급 전환 전략 Buy & Hold (1/2 진입) 초과 수익
누적 수익률 +47.6% +97.1% -49.5%p (후행 진입 괴리)
연환산 수익률 (CAGR) +246.2% +498.7% 단기 관측에 따른 과대 표현
최대 낙폭 (MDD) -15.6% -24.4% +8.8%p 리스크 절감 우위
Calmar Ratio (CAGR/MDD) 15.8 20.4 단기 변동성 왜곡 감안 필요
보유 기간 79 거래일 95 거래일 16일 노출 기간 축소
진입가 (KODEX 200) 83,570원 62,594원 -

여기서 반전이 나온다. 총수익만 보면 단순 보유(+97.1%)가 수급 전환 전략(+47.6%)을 크게 앞선다. 시그널을 기다리다 늦게 들어간 후행 진입이 앞부분 상승을 통째로 놓친 탓이다.

대신 수급 전환 전략은 낙폭에서 이긴다. MDD가 -15.6%로 단순 보유(-24.4%)보다 얕고, 시장에 노출된 기간도 짧다. 더 버는 전략이 아니라 덜 잃는 전략이라는 뜻이다. 아래는 이 성과를 산출한 백테스트 엔진 전체 코드다.

5-2. 전략 실행 코드 (Full Backtesting Engine)

def backtest_supply_shift_strategy(
    ticker: str = "069500",
    start: str = "20260101",
    end: str = "20260523",
    hold_days: int = 60,
    stop_loss: float = -0.10,
) -> dict:
    """
    수급 전환 전략 백테스팅 시뮬레이션 전체 엔진 코드입니다.
    """
    df_price = stock.get_market_ohlcv(start, end, ticker)
    df_flow = get_investor_net_trading(ticker, start, end)
    signals = detect_supply_shift_signal(df_flow)
    trades = []

for sig in signals:

entry_idx = df_price.index.get_loc(sig['date'])

entry_price = df_price.iloc[entry_idx]['종가']

peak = entry_price

exit_price = entry_price

exit_reason = 'hold_expiry'

for j in range(1, min(hold_days, len(df_price) - entry_idx)):

current = df_price.iloc[entry_idx + j]['종가']

peak = max(peak, current)

pnl = (current - entry_price) / entry_price

if pnl < stop_loss:

exit_price = current

exit_reason = 'stop_loss'

break

exit_price = current

trade_return = (exit_price - entry_price) / entry_price

trades.append({

'entry_date': sig['date'],

'entry_price': entry_price,

'exit_price': exit_price,

'return': trade_return,

'exit_reason': exit_reason,

})

if trades:

total_return = np.prod([1 + t['return'] for t in trades]) - 1

equity = [1.0]

for t in trades:

equity.append(equity[-1] * (1 + t['return']))

equity = np.array(equity)

peak_eq = np.maximum.accumulate(equity)

mdd = np.min((equity - peak_eq) / peak_eq)

else:

total_return, mdd = 0.0, 0.0

days_held = sum(min(hold_days, 60) for _ in trades)

cagr = (1 + total_return) ** (252 / max(days_held, 1)) - 1

calmar = abs(cagr / mdd) if mdd != 0 else 0

return {

'total_return': total_return,

'cagr': cagr,

'mdd': mdd,

'calmar': calmar,

'n_trades': len(trades),

'trades': trades,

}

result = backtest_supply_shift_strategy()



6. 반증 조건 — 이 전략이 무너지는 경우

모든 전략은 언제 틀리는지를 먼저 알아야 쓸 수 있다. 이 수급 전환 전략도 예외가 아니다. 아래 표는 이 전략이 깨지는 세 가지 구조적 약점과, 각 약점을 줄이기 위한 방어책을 정리한 것이다.

6-1. 전략 한계 및 리스크 요인

한계 요인 정량적 메커니즘 리스크 방어책
생존자 편향 2026년 국면 특수성에 데이터가 과적합(Overfitting)될 위험 잔존 2010~2025 개년 데이터 전수 백테스트 연동
슬리피지 비용 이론상 종가 매수 시점 및 실제 주문 체결 가격 간의 시장 괴리 비용 발생 장 마감 직전 VWAP 분할 집행 주문 도입
데이터 시차 KRX 원장 최종 확정 및 공시 시점까지의 시스템 데이터 타임래그 리스크 장중 프로그램 매매 가중 동향 실시간 연산 추가

특히 첫 줄, 생존자 편향이 핵심이다. 이 성과는 상승이 나온 2026년 구간을 이미 알고 뒤돌아본 결과다. 실제 운용에서는 매일 장 마감 후 아래 루틴처럼 시그널 발생 여부만 기계적으로 확인하는 것이 과적합을 줄이는 최소한의 규율이다.

6-2. 전략 운용 체크리스트

def daily_signal_check():
    """매일 장 마감 후 시스템 환경에서 변동 시그널을 확인하는 모니터링 루틴입니다."""
    today = datetime.now().strftime('%Y%m%d')
    start_20d = (datetime.now() - timedelta(days=30)).strftime('%Y%m%d')
    df = get_investor_net_trading("069500", start_20d, today)
    foreign_20d = df['외국인합계'].tail(20).sum() / 1e12
    inst_5d = df['기관합계'].tail(5).sum() / 1e12

if foreign_20d < -10.0 and inst_5d > 0:

return True

return False



🧭 골목지기 뷰 — 데이터 밖의 판단

골목지기가 이 글에서 가장 오래 들여다본 칸은 전략의 화려한 연환산 수익률이 아니다. 저점에서 그냥 사서 들고만 있던 쪽이 규칙을 짜서 진입한 전략보다 더 벌었다는 대목이다. 그런데도 골목지기는 이 전략을 버리지 않는다. 이유는 하나, 낙폭이다.

덜 버는 대신 덜 잃는 구조라면, 다음 하락장에서 계좌를 지켜내는 쪽은 후자다. 수익률표의 큰 숫자보다 이 트레이드오프 한 줄이 이 글의 진짜 결론이라고 골목지기는 판단한다.

시장은 외국인 수급을 지수의 나침반으로 읽는다. 팔면 약세, 사면 강세라는 공식이다. 골목지기는 이 공식을 반만 신뢰한다. 파는 주체가 아니라 그 물량을 받아내는 주체가 방향을 정한다고 보기 때문이다. 외국인이 며칠씩 던져도 연기금이 같은 물량을 흡수하는 국면에서는 지수가 버틴다. '누가 파느냐'는 뉴스가 되지만 '누가 받느냐'는 바닥이 된다.

단, 이 관점은 받는 손이 연기금·보험 같은 장기 자금일 때만 유효하다. 외국인 극단 매도 국면에서 기관 순매수의 실체가 연기금이 아니라 증권사 자기매매나 단기 차익 물량이라면, 그 흡수는 하루 이틀짜리에 그친다. 골목지기는 기관 매수의 주체 구성이 연기금에서 단기 자금으로 바뀌는 순간 이 시그널을 폐기한다. 수급동행성 지표가 높아도 받는 손의 성격이 바뀌면 그 숫자는 함정이 된다.

그래서 골목지기는 이 시그널을 진입의 방아쇠로 쓰지 않는다. 관망을 풀지 말지 검토를 시작하는 신호로만 쓴다. 외국인 20일 누적이 극단으로 빠지고 기관이 며칠 연속 받기 시작하면, 그때부터 계좌를 열어볼지 살핀다. 지켜보는 건 딱 하나다. 다음 주에도 연기금 순매수가 이어지는가. 그 한 줄이 확인될 때까지 골목지기는 서두르지 않는다.


🔗 함께 읽으면 좋은 글

이 전략의 수급 조건을 더 넓은 거시 맥락에서 확인하고 싶다면 아래 글이 연결된다.


Q
골목지기 퀀트 뷰 — 데이터 기반 국내 주식 정량 분석 전문 채널
알고리즘 백테스팅 / DART 공시 입체 분해 / 계량 수급 데이터 시계열 산출 하우스

데이터 출처 및 레퍼런스:
- KRX 정보데이터시스템 투자 주체별 매매 동향 원장 (data.krx.co.kr)
- 한국거래소 오픈소스 라이브러리 기반 수급 데이터 파이썬 래퍼 (github.com/sharebook-kr/한국거래소)
- KODEX 200 ETF (069500) 실제 마감 OHLCV 데이터 세트 (2026.01.02 ~ 2026.05.22)

⚠️ 본 콘텐츠는 정량적 데이터 분석 기록 및 정보 제공 목적이며 특정 금융 투자 상품의 매수·매도를 권유하지 않습니다. 모든 투자 판단과 최종 결과에 대한 책임은 투자자 본인에게 귀속됩니다.
반응형

댓글