콘텐츠로 이동
4 posts with the tag “수급”

기술적 분석 7장: OBV 거래량 신호의 선행성

OBV와 거래량 분석

8장에서 거래량 돌파의 성과를 보기 전에, 거래량을 단순히 급증한 하루의 크기로만 볼 필요가 있는지 확인합니다. 가격이 오르는데 거래량이 줄어드는 경우와 가격 상승에 거래량이 함께 증가하는 경우는 같은 상승으로 보기 어렵습니다. OBV는 상승한 날의 거래량을 더하고 하락한 날의 거래량을 빼서 가격과 거래량의 방향을 하나의 시계열로 만듭니다.

direction = df.groupby("code")["close"].diff().apply(np.sign)
df["obv"] = (direction * df["volume"]).groupby(df["code"]).cumsum()
df["obv_ma20"] = df.groupby("code")["obv"].transform(
lambda x: x.rolling(20).mean()
)
df["obv_confirmation"] = (
(df["close"] > df["ma20"]) &
(df["obv"] > df["obv_ma20"])
)

이번 분석에서는 가격이 MA20 위에 있고 OBV도 OBV20 위로 올라온 첫날을 확인 신호로 정의했습니다.

검증 구간에서 OBV 확인 신호는 2,653회 발생했습니다.

신호발생 횟수20일 양수 비율평균 수익률중앙값
OBV 확인2,653회53.8%+6.02%+2.03%
거래량 돌파2,726회53.4%+9.75%+2.51%

OBV 확인은 거래량 돌파보다 평균수익률과 중앙값은 낮았지만, 양수 비율은 53.8%로 거래량 돌파의 53.4%보다 소폭 높았습니다. RSI 회복과 비교하면 평균과 중앙값이 모두 높았습니다. 즉 OBV가 거래량 돌파를 대체하는 더 강한 신호라기보다, 가격 추세가 거래량의 누적 방향과도 맞는지 확인하는 역할에 가깝습니다. 그래도 양수 비율이 53.8%에 그쳤으므로 거래량이 증가했다는 사실만으로 향후 상승을 확정할 수는 없습니다.

이번 결과에서 OBV의 역할은 단독 매수 신호보다 가격 신호의 신뢰도 확인에 가깝습니다. 급격한 거래량 돌파는 큰 평균수익률을 만들었고, OBV 확인은 비슷한 양수 비율을 조금 더 안정적으로 보완했습니다. 둘을 함께 사용하면 정보가 추가되는지, 같은 거래량 정보를 중복해서 쓰는지는 복합 신호에서 다시 확인해야 합니다.

  • 가격이 MA20 위인데 OBV가 하락하면 상승의 내부가 약할 수 있습니다.
  • 가격이 횡보하는 동안 OBV가 상승하면 매집 가능성을 살펴볼 수 있습니다.
  • 급등일 거래량만으로 OBV가 높아졌는지, 여러 날 누적되어 높아졌는지 구분해야 합니다.

다음 장에서는 평균수익률이 가장 높았던 거래량 돌파를 20일 고점 돌파라는 명확한 조건으로 분해해, 높은 평균이 얼마나 넓은 사례에서 나왔는지 확인합니다.

결과는 신호별 결과 CSV의 OBV 확인 행에서 확인할 수 있습니다.

외국인 순매수 종목을 따라 사도 될까? 실제 데이터로 확인한 수급의 함정

외국인 수급과 주가 흐름을 분석하는 연구 공간

외국인 순매수 상위 종목은 많은 투자자가 참고하는 대표적인 수급 지표입니다. 하지만 외국인이 많이 샀다는 사실만으로 다음 상승을 기대해도 될까요? 순매수는 강력한 정보일 수 있지만, 매수 기간·거래대금·다른 투자자 수급·이미 진행된 주가 상승을 함께 보지 않으면 신호를 잘못 해석하기 쉽습니다.

이 글에서는 퀀티랩이 수집한 일별 투자자 매매와 주가 데이터를 이용해 2026년 8월 7일부터 9월 4일까지 최근 20거래일을 분석했습니다. 분석 대상은 해당 기간 데이터가 15거래일 이상 존재하는 종목 중 외국인 순매수 금액이 가장 큰 5개 종목입니다.

최근 20거래일 외국인 순매수와 같은 기간 주가 수익률

투자자별 매매 금액은 순매수 기준으로 집계했습니다. 금액은 억 원 단위이며, 주가 수익률은 분석 구간의 첫 종가와 마지막 종가를 비교한 단순 수익률입니다. 최대낙폭은 같은 구간 안에서 종가 기준 누적수익률이 고점 대비 얼마나 하락했는지를 계산한 값입니다.

순위종목외국인 순매수매수 우위 일수주가 수익률최대낙폭
1우리금융지주(316140)4,004억9일-4.6%-9.3%
2한미반도체(042700)1,840억15일+19.6%-8.7%
3실리콘투(257720)1,791억11일+22.1%-11.4%
4한국금융지주(071050)1,737억19일+0.4%-10.2%
5파마리서치(214450)1,685억17일+2.1%-17.4%

결과는 단순합니다. 외국인 순매수 상위 종목이라고 해서 모두 같은 방향으로 움직이지 않았습니다. 우리금융지주는 외국인 순매수 금액이 가장 컸지만 주가는 오히려 4.6% 하락했습니다. 반면 한미반도체와 실리콘투는 외국인 매수와 주가 상승이 함께 나타났습니다. 한국금융지주와 파마리서치는 외국인 매수 우위 일수가 많았지만 주가 상승폭은 제한적이었습니다.

순매수 금액이 큰 것과 매수가 지속되는 것은 다르다

섹션 제목: “순매수 금액이 큰 것과 매수가 지속되는 것은 다르다”

우리금융지주의 외국인 순매수는 4,004억 원으로 다른 종목보다 크게 나타났습니다. 그러나 9개 거래일만 매수 우위였고, 9월 3일 하루 외국인 순매수가 약 4,317억 원으로 기록되었습니다. 하루의 대규모 거래가 누적값을 크게 끌어올린 사례입니다.

이런 경우 누적 순매수만 보면 외국인이 꾸준히 매수했다고 착각할 수 있습니다. 따라서 다음 세 지표를 함께 확인해야 합니다.

  • 순매수 누적 금액
  • 순매수 우위 일수와 연속성
  • 최대 단일 거래일 순매수 금액이 전체 누적값에서 차지하는 비중

한 번의 블록성 거래나 지수 편입·편출 관련 거래가 전체 신호를 왜곡할 수 있기 때문입니다. 누적 금액이 크더라도 며칠에 걸쳐 반복된 매수인지, 특정 하루에 집중된 매수인지에 따라 해석은 달라져야 합니다.

다른 투자자의 매매 방향도 함께 봐야 한다

섹션 제목: “다른 투자자의 매매 방향도 함께 봐야 한다”

이번 구간에서 한미반도체는 외국인 순매수 1,840억 원과 함께 주가가 19.6% 상승했습니다. 하지만 기관은 약 2,303억 원 순매도했고 개인은 약 455억 원 순매수했습니다. 외국인 매수가 주가를 지지했지만, 기관의 반대 매매도 동시에 나타난 것입니다.

실리콘투는 외국인 순매수 1,791억 원, 주가 상승률 22.1%를 기록했지만 기관과 개인은 각각 약 645억 원, 1,119억 원을 순매도했습니다. 이런 패턴은 외국인 수급이 가격을 끌어올린 사례로 볼 수 있지만, 외국인 매수세가 약해지면 매수 주체가 빠르게 사라질 위험도 있습니다.

반대로 한국금융지주는 외국인 순매수 우위 일수가 19일로 가장 안정적이었지만, 기관은 약 63억 원, 개인은 약 1,653억 원 순매도했습니다. 그럼에도 주가 수익률은 0.4%에 그쳤습니다. 지속적인 외국인 매수가 있었지만 다른 매도 물량을 상쇄하는 데 대부분 사용되었을 가능성이 있습니다.

거래대금 대비 순매수 비율은 신호의 강도를 보여준다

섹션 제목: “거래대금 대비 순매수 비율은 신호의 강도를 보여준다”

절대 금액만 비교하면 대형주가 항상 상위에 오릅니다. 그래서 평균 거래대금 대비 외국인 누적 순매수 규모도 확인했습니다.

종목평균 일 거래대금외국인 순매수 / 20일 거래대금
우리금융지주약 899억 원22.3%
한미반도체약 1,182억 원7.8%
실리콘투약 971억 원9.2%
한국금융지주약 398억 원21.8%
파마리서치약 697억 원12.1%

우리금융지주와 한국금융지주는 평균 거래대금 대비 외국인 순매수 비중이 상대적으로 컸습니다. 그러나 우리금융지주의 주가가 하락했다는 점에서, 비율이 높다는 것만으로 상승을 보장할 수 없다는 사실도 확인됩니다. 매수 가격대와 매도 압력, 거래가 발생한 시점이 함께 중요합니다.

외국인 순매수를 따라가기 위한 조건

섹션 제목: “외국인 순매수를 따라가기 위한 조건”

외국인 순매수를 매매 신호로 활용하려면 최소한 다음 순서를 권합니다.

  1. 최근 20일과 60일의 누적 순매수 방향이 같은지 확인합니다.
  2. 순매수 우위 일수와 최대 단일 거래일 비중을 확인합니다.
  3. 외국인 순매수가 평균 거래대금의 어느 정도인지 계산합니다.
  4. 기관과 개인이 같은 방향인지, 반대 방향인지 구분합니다.
  5. 주가가 이미 단기 급등했는지와 최대낙폭을 확인합니다.
  6. 공매도 거래와 공매도 잔고가 매수 신호를 상쇄하지 않는지 살펴봅니다.

간단한 필터로 표현하면 다음과 같습니다.

signal = (
(foreign_net_20 > 0)
& (foreign_buy_days_20 >= 12)
& (foreign_net_20 / turnover_20 >= 0.05)
& (return_20 < 0.20)
)

이 조건은 매수 추천 공식이 아니라 수급의 지속성과 과열 여부를 함께 보려는 분석 예시입니다. 임계값은 시장과 투자 기간에 따라 다시 검증해야 합니다.

최근 20거래일 데이터에서 외국인 순매수와 주가 상승이 동시에 나타난 종목은 한미반도체와 실리콘투였습니다. 그러나 두 종목 모두 구간 중 최대낙폭이 각각 8.7%, 11.4% 발생했습니다. 수급이 좋았던 종목도 중간 변동성을 피할 수 없었다는 뜻입니다.

우리금융지주는 가장 큰 외국인 순매수 금액에도 주가가 하락했고, 한국금융지주는 외국인 매수 우위 일수가 가장 많았지만 거의 제자리였습니다. 따라서 외국인 수급은 매수 버튼을 누르게 하는 단독 신호라기보다, 주가·거래대금·다른 투자자 수급과 함께 사용하는 확인 신호에 가깝습니다.

외국인이 순매수한 종목을 따라 사는 전략은 단순하고 직관적이지만, 누적 금액만 보면 중요한 정보를 잃게 됩니다. 실제 분석에서는 매수의 지속성, 거래대금 대비 규모, 다른 투자자의 반대 매매, 이미 발생한 수익률과 최대낙폭을 함께 봐야 합니다.

이번 사례의 핵심은 외국인 순매수 상위라는 순위와 이후 주가 성과가 일치하지 않았다는 것입니다. 순매수는 관심 종목을 좁히는 유용한 출발점이지만, 최종 판단을 위해서는 수급이 가격을 실제로 지지하고 있는지, 그리고 그 흐름이 며칠의 이벤트가 아닌지 검증해야 합니다.

분석 기준일은 2026년 9월 4일이며, 데이터 집계 방식과 거래비용에 따라 결과가 달라질 수 있습니다. 이 글은 투자 판단을 위한 참고 자료이며 특정 종목의 매수·매도를 권유하지 않습니다.

대신증권 크레온에서 투자자별 매매동향 수집하기

이번 포스트에서는 크레온 플러스 API를 이용하여 개인, 기관, 외국인의 매매동향 데이터를 수집하는 방법을 다루겠습니다.

퀀티랩 SysTrader 패키지를 다운받아 설치하시면 다음과 같이 쉽게 기능을 사용할 수 있습니다.

from quantylab.systrader.creon import Creon
c = Creon()
data = c.get_investorbuysell('005930', n=60)

지금부터는 SysTrader 설치 없이 위 기능을 직접 구현해 보실 수 있도록 풀어서 설명하겠습니다.

퀀티랩 YouTube 채널의 이 영상을 보셔도 됩니다.

투자자별 매매동향 데이터는 크레온의 매매입체분석 CpSysDib.CpSvr7254 모듈을 사용해서 가져올 수 있습니다.

크레온 플러스 API 사용을 위해서는 win32com 패키지가 필요한데 pip install pywinauto로 설치할 수 있습니다.

먼저 win32com.client를 임포트하여 CpSysDib.CpSvr7254 모듈을 Dispatch 합니다.

import win32com.client
obj = win32com.client.Dispatch('CpSysDib.CpSvr7254')

이제 파라미터를 입력해야 합니다. 문서를 보면 다양한 파라미터들이 있는데 여기서는 하나의 예시를 들겠습니다. 이 문서에서 확인하시어 아래 예제처럼 원하시는 값으로 입력해 주시면 됩니다.

obj.SetInputValue(0, 'A005930') # 종목코드
obj.SetInputValue(1, 6) # 기간선택구분 - 6:일별
obj.SetInputValue(4, ord('0')) # 매매비중구분 - '0':순매수
obj.SetInputValue(5, 0) # 투자자 - 0:전체
obj.SetInputValue(6, ord('1')) # 데이터 구분 - '1':순매수량

이렇게 삼성전자에 대해 투자자들의 일별 매매동향을 가져오도록 파라미터를 채웠습니다. 이제 요청을 다음과 같이 보내면 됩니다.

obj.BlockRequest()

이제 요청 결과를 가져올 수 있는데, 먼저 결과 데이터 개수를 확인해 보겠습니다. 이건 GetHeaderValue(type) 메소드로 가져올 수 있는데 type은 모듈마다 달라서 문서를 확인해야 합니다. 이 모듈에서 결과의 데이터 개수에 대한 type 값은 1 입니다.

cnt = obj.GetHeaderValue(1)

아마 cnt는 17개로 나올 것입니다. 이건 이 모듈이 한번 조회할 때 최대 17개까지만 반환해주기 때문입니다. 더 많은 과거 데이터를 원할 경우 연속조회 하면 됩니다. 이건 뒤에서 확인하겠습니다.

먼저 현재 요청 결과를 dict로 구성해 보겠습니다. 결과는 GetDataValue(type, index) 메소드로 가져옵니다. type은 문서를 확인하면 되고 index는 17개 아이템에 대한 인덱스 입니다.

이렇게 결과를 dict 형태로 만들 수 있습니다.

keys = [
'일자',
'개인',
'외국인',
'기관계',
'금융투자',
'보험',
'투신',
'은행',
'기타금융',
'연기금 등',
'기타법인',
'기타외인',
'사모펀드',
'국가지자체',
'종가',
'대비',
'대비율',
'거래량',
'확정치여부',
]
data = []
for i in range(cnt):
item = {k: obj.GetDataValue(j, i) for j, k in enumerate(keys)}
data.append(item)

이제 연속조회로 더 많은 데이터를 가져와 보겠습니다.

if obj.Continue:
obj.BlockRequest()

그리고 위와 똑같이 dict 형태로 만들어서 data에 추가하면 됩니다.

cnt = obj.GetHeaderValue(1)
for i in range(cnt):
item = {k: obj.GetDataValue(j, i) for j, k in enumerate(keys)}
data.append(item)

이 데이터를 pandas DataFrame으로 변환하여 출력해보면 다음처럼 테이블로 결과를 볼 수 있습니다.

import pandas as pd
df = pd.DataFrame(data)
index일자개인외국인기관계금융투자보험투신은행기타금융연기금 등기타법인기타외인사모펀드국가지자체종가대비대비율거래량확정치여부
020211122-868900257667213033799187991920352738060-363952738785824-82117-2940131865207490037005.202723983649
120211119-74207831397043282728320212079-23332-3478-24859179418-87214002979707120010001.421195472849
2202111181314434-738137-576566-103191-3195-1092701-456299-54135682-2955070200-500-0.711014495749
32021111712345371519353-2816912-255789427969122759105367-114917-49138857393562991192070700-600-0.841102760649
420211116-7530521025989-202007148871-4899212214846961-116-551871-73698276880992071300-100-0.141091923949
520211115-3174454976207230138417081032737563674-65602705369771-86615-165221363160714008001.131242071049
620211112-241132238422820469492015464-10560196819-761405-152781-16190-3665-16370706007001.001008745049
7202111112266214-2179760-11669865562167711-1330622502-234-389250279062338-319986069900-300-0.431148912249
820211110-7227931435001-724133-268435-14154326100-5246-475829902429016921070200-300-0.43942200949
920211109-454505-72553911933681695062-18889-3561601624-478674-10796-252829861070500-100-0.141115958949
1020211108714776-628399-8916823323923127389127130103-3844128551936-72670706004000.571112198149
11202111052087811-1174569-905068-452144158763280842288-682-633973-119573783-7404070200-400-0.571266774349
1220211104-1743969-8076419470301874492201770-161783000-196-190245-119787-2510743870706002000.281181898749
13202111033748000-1032459-2789497-2145003-97809-883930004247-300074722391717-245019070400-1100-1.541277042849
1420211102-5644694204362436210763253414-151433150916223348113959-2851-1715525364907150016002.291681257049
1520211101-1367521-16517363003169282767865072387742-7314-753871338627021543440699001000.141150372949
16202110294050479-1812320-2365662-149683166131124596182425201-11034701116081589520469069800-900-1.271701615149
1720211028899791-243288512817449896017371446463-53112738200349253867-25175211740707006000.862064490249
18202110273080455-2124585-1005822-622428-28520-47963894-436-3544583414315809922070100-1000-1.411029531649
1920211026-1247398-55156318121231501865-364378012723675796060-1642232601318360711009001.281052825249
2020211025582794-15994469884621110489178388699-1410547-18252230985-279534821070200-200-0.281002962149
2120211022-644269-12853876240412152682445-40105-682-167-45215012542-2139377950704002000.28839544849
2220211021203876109754-197734242202-125548-304780-13079-275758-1165426464927070200-100-0.141089109449
2320211020556532256311-829527-125516-131592-86437200-558-48659386648020969070300-300-0.421015163849
2420211019-504814-431790910091142909816262-618301361167-43252733963-7450-424400706004000.57950799149
25202110187718901303614-2117009-1564134-42320-17082-329833-4771963058410921-167810702001000.141348639149
2620211015822742-1376104456288626433-604589964521339242-3124008423812836814870701007001.011805161249
27202110142011163-378717316966621993635-9347676957-2010524-2364636270416644-544950694006000.871952064149
28202110132224620-527184527255962539887-1001663387116584-7544200553117679862222909068800-200-0.292417201549
292021101212155071-11004329-1421202-907809-230050-17815515394-609911239924871321747-71990069000-2500-3.503100148449
30202110082612855319900-2923031-1565044-88549428053341-8642-1269657-10175451-37285071500-100-0.141404328749
31202110071266151-266150613913581821608-9018269692-16895770-407085-25076504-67560716003000.421368353249
32202110061374960-1070439-342467244117-249351025501669924-59975941544-3598-67033071300-900-1.251895696249
33202110051783237-302558412559111173732-574062289-47544-9339104473-188895325-21960072200-1000-1.372401392149

분봉 수급 점수 계산하기

여기서 구하고자 하는 피쳐는 분봉 수급 점수입니다.

분봉 수급 점수: 1분동안 체결이 많이 이루어진 분봉들 중에서 매수세가 강한 분봉들의 비율로 [-1, 1]의 범위를 가짐

아래와 같은 1분봉차트가 있습니다. 분봉들이 아주 많아서 사람이 직접 점수를 계산하기는 어려울 수 있습니다.

분봉동시호가제외

분봉차트 CSV 파일

이를 파이썬으로 계산해 보겠습니다.

python3, numpy, pandas, matplotlib, mpl_finance 등을 필요로 합니다.

mpl_finance는 https://github.com/matplotlib/mpl_finance 에서 받을 수 있습니다.

우선 필요한 라이브러리들 부터 임포트합니다.

import numpy as np
import pandas as pd
import matplotlib as mpl
import matplotlib.pyplot as plt
import mpl_finance
if __name__ == '__main__':
pass

이제 아래 코드로 main을 채웁니다.

df_chart = pd.read_csv('data/000660_m_in.csv') # 다운받은 데이터의 경로로 변경하십시오
# 고려할 분봉 수 결정
# df_chart = df_chart.iloc[-60*6*5:] # 약 5일치 분봉
# 동시호가 거래 없애기
df_chart['체결일'] = (df_chart['체결시간'] / 1000000).astype(int)
df_chart['체결시분'] = df_chart['체결시간'].mod(1000000).astype(int)
df_chart = df_chart[df_chart['체결시분'] < 153000]
# 거래량이 평균+표준편자*2 보다 많았던 분봉들만 남깁니다
df_chart = df_chart[df_chart['거래량'] > df_chart['거래량'].mean() + df_chart['거래량'].std() * 2]
# [수요 거래대금 + 공급 거래대금]을 구합니다
supdemand = ((df_chart['현재가'] - df_chart['시가']).abs() * df_chart['거래량']).sum()
# [수요 거래대금 - 공급 거래대금]을 구합니다
demand = ((df_chart['현재가'] - df_chart['시가']) * df_chart['거래량']).sum()
# [(수요 거래대금 - 공급 거래대금) / (수요 거래대금 + 공급 거래대금)]을 구합니다
score_demand = float(demand) / supdemand

여기서 score_demand가 분봉 수급 점수입니다. 이 점수가 1에 가까울수록 수요 거래대금이 컸다는 의미이고 -1에 가까울수록 공급 거래대금이 많았다는 의미입니다.

위의 1분봉 차트에 대해서 분봉 수급 점수는 0.1252 입니다. 양수이므로 수요 거래대금이 더 많았다는 뜻입니다.

물론 이 지표만으로 투자를 결정할 수는 없습니다. 앞으로 다양한 지표들을 개발하고 데이터를 기반으로 이유있는 투자를 하고자 합니다.