📅 발행: 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일 누적이 극단으로 빠지고 기관이 며칠 연속 받기 시작하면, 그때부터 계좌를 열어볼지 살핀다. 지켜보는 건 딱 하나다. 다음 주에도 연기금 순매수가 이어지는가. 그 한 줄이 확인될 때까지 골목지기는 서두르지 않는다.
🔗 함께 읽으면 좋은 글
이 전략의 수급 조건을 더 넓은 거시 맥락에서 확인하고 싶다면 아래 글이 연결된다.
- [📊 글로벌 매크로 & 시황] 주간 시황 리포트 — 외국인 순매도 맥락의 거시경제 배경 분석
- [📚 퀀트·투자 백과사전] 이격도·샤프지수·PER/PBR/ROE 핵심 산식 가이드
- [🔬 기업 & 산업 인텔리전스] 수급 수혜 종목 피어 그룹 밸류에이션 비교 분석
알고리즘 백테스팅 / DART 공시 입체 분해 / 계량 수급 데이터 시계열 산출 하우스
데이터 출처 및 레퍼런스:
- KRX 정보데이터시스템 투자 주체별 매매 동향 원장 (data.krx.co.kr)
- 한국거래소 오픈소스 라이브러리 기반 수급 데이터 파이썬 래퍼 (github.com/sharebook-kr/한국거래소)
- KODEX 200 ETF (069500) 실제 마감 OHLCV 데이터 세트 (2026.01.02 ~ 2026.05.22)
'📈 퀀트 투자 전략' 카테고리의 다른 글
| VCP(변동성 수축 패턴) 완전 해설: 마크 미너비니 추세 돌파 매매 전략 통계 검증 (0) | 2026.06.23 |
|---|---|
| [퀀트전략] 시장 중립 전략(Market Neutral) 2026 완전 분석 — 롱숏·페어트레이딩·절대수익 구조 정량 점검 (0) | 2026.06.08 |
| MACD지표 (0) | 2026.05.29 |
| RSI지표 (0) | 2026.05.29 |
댓글