Skip to content

블로그

한국전력, 저평가 매력과 하락 추세의 충돌

한국전력 쇼츠 첫 장면

분석일 2026-08-19 · 뉴스 확인일 2026-08-19 · 최신 유효 종가 30850원

원문 종목분석: 한국전력 AI 종목분석

한국전력 종목 개요 장면

한국전력은 국내 전력의 송전·배전과 판매를 담당하는 지배적 전력 공기업입니다. 전기요금과 연료비, 송전망 투자 집행이 실적과 현금흐름의 핵심 변수입니다.

한국전력 종합 판단 장면

흑자 전환과 낮은 밸류에이션은 긍정적이지만, 기술적 하락과 기관·외국인 매도가 부담입니다.

팩터점수핵심 해석
기술25하락 추세와 과매도 근접
재무70흑자 전환과 현금흐름 개선
밸류에이션85PBR 0.41배, PER 2.32배
수급35기관·외국인 매도 우위
뉴스40송전망 지연 리스크
안정성55국민연금 보유 확대

한국전력 뉴스 장면

2026-08-19 · 송전망 지연, 하루 15억 원 손실 우려: 송전망 구축이 늦어지면 LNG 대체 발전 비용과 산업시설 가동 차질이 발생할 수 있다는 분석이 나왔습니다.

2026-08-19 · 전력망 투자와 인허가: 주민 협의와 지중화, 인허가 효율화가 전력 인프라 확충 속도를 좌우합니다.

한국전력 기술적 분석 장면

8월 19일 종가는 30,850원으로 전일보다 5.37% 하락했습니다. 5일·20일·60일 이동평균선 아래에서 RSI 38.34, MACD 음수로 하락 압력이 높습니다.

한국전력 재무·밸류에이션 장면

2025년 EPS는 13,311원으로 흑자 전환했고 ROE는 17.67%입니다. AI 분석 기준 PER은 2.32배, PBR은 0.41배로 저평가 구간입니다.

한국전력 수급 분석 장면

8월 19일 개인 순매수가 유입됐지만 기관·외국인 매도와 대비됩니다. 단기 추세를 누르는 수급입니다. 중장기 지지 기반으로 관찰할 수 있습니다.

보수적 대응: 3만400원 지지와 기관·외국인 수급 개선을 확인하기 전까지 중립 관점을 유지합니다.

한국전력 최종 의견 장면

중립

데이터 기준: 분석일 2026-08-19 · 뉴스 확인일 2026-08-19 · 최신 유효 종가 30850원. 한국거래소·기업 공시·퀀티랩 AI 분석

본 콘텐츠는 정보 제공을 위한 참고 자료이며 특정 금융상품의 매수·매도를 권유하지 않습니다.

파이썬으로 IMF에서 각국의 GDP 수집하기 (v.2025)

이번 글에서는 파이썬을 사용하여 IMF(국제통화기금)에서 WEO(World Economic Outlook) 데이터베이스를 통해 세계 경제 데이터를 수집하고 GDP(국내총생산) 데이터를 조회하고 시각화하는 방법을 소개합니다.

WEO 데이터베이스에는 각국의 GDP, 인플레이션, 실업률 등 다양한 경제 지표가 포함되어 있습니다. 여기서는 GDP 데이터를 중심으로 다룹니다.

다음 내용을 다룹니다:

  • WEO 데이터를 SDMX 3.0 API를 통해 수집하는 방법
  • GDP 데이터 조회 및 시각화 방법

먼저 필요한 파이썬 라이브러리를 설치합니다. pandas, requests, matplotlib 라이브러리가 필요합니다.

Terminal window
pip install pandas requests matplotlib

IMF의 WEO 데이터는 SDMX 3.0 API를 통해 접근할 수 있습니다. 다음은 파이썬을 사용하여 GDP 데이터를 수집하는 예제 코드입니다.

import json
import requests
def get_weo_data():
# Dataset
# - Dataset name: World Economic Outlook (WEO)
# - ID: WEO
# - Agency: IMF.RES
# - Version: 9.0.0
"IMF.RES,WEO"
# Data Query
# https://api.imf.org/external/sdmx/3.0/data/{context}/{agencyID}/{resourceID}/{version}/{key}[?c][&updatedAfter][&firstNObservations][&lastNObservations][&dimensionAtObservation][&attributes][&measures][&includeHistory][&asOf]
url = "https://api.imf.org/external/sdmx/3.0/data/dataflow/IMF.RES/WEO/9.0.0/*"
res = requests.get(url)
data = json.loads(res.text)
return data

get_weo_data 함수를 호출하여 WEO 데이터를 수집할 수 있습니다.

import json
with open("weo_data.json", "w") as fout:
data = get_weo_data()
json.dump(data, fout)

수집한 데이터는 SDMX 3.0 형식으로 제공되서 파싱이 필요합니다.

데이터를 살펴보면 dataSets 키 아래에 여러 국가와 지표가 포함되어 있습니다. 그런데 키 값이 복잡하므로, 이를 해석하기 위해서는 structures 키 아래의 정보를 참고해야 합니다.

다음은 데이터를 파싱하여 판다스 데이터프레임으로 변환하는 예제 코드입니다.

output = []
for data_set in data_sets:
structure = structures[data_set["structure"]]
series_dims = structure["dimensions"]["series"]
obs_dims = structure["dimensions"]["observation"]
for series_key, series_content in data_set["series"].items():
# series dimensions 파싱
dim_indices = series_key.split(":")
series_info = {}
for i, dim_index in enumerate(dim_indices):
dim_id = series_dims[i]["id"]
dim_value_id = series_dims[i]["values"][int(dim_index)]["id"]
series_info[dim_id] = dim_value_id
# series attributes 파싱
attributes_info = {}
for attr_value, struct_attr in zip(series_content.get("attributes", []),
structure["attributes"]["series"]):
attr_id = struct_attr["id"]
# coded attribute는 index로 제공 => value id로 변환
if isinstance(attr_value, int):
attr_id_value = struct_attr["values"][attr_value]["id"]
else:
attr_id_value = attr_value # uncoded string
attributes_info[attr_id] = attr_id_value
# observations 처리
if "observations" not in series_content:
continue
for obs_key, obs_value in series_content["observations"].items():
obs_idx = int(obs_key)
obs_dim_value = obs_dims[0]["values"][obs_idx]["value"]
value = obs_value[0]
row = {
**series_info,
**attributes_info,
"TIME_PERIOD": obs_dim_value,
"VALUE": value
}
output.append(row)
# pandas DF 생성
import pandas as pd
df = pd.DataFrame(output)
df
.dataframe tbody tr th {
vertical-align: top;
}
.dataframe thead th {
text-align: right;
}
COUNTRY INDICATOR FREQUENCY SCALE DECIMALS_DISPLAYED OVERLAP COUNTRY_UPDATE_DATE TIME_PERIOD VALUE
0 ABW BCA A 9 3 OL 9/19/2025 1999 -435000000
1 ABW BCA A 9 3 OL 9/19/2025 2000 213000000
2 ABW BCA A 9 3 OL 9/19/2025 2001 310000000
3 ABW BCA A 9 3 OL 9/19/2025 2002 -333000000
4 ABW BCA A 9 3 OL 9/19/2025 2003 -167000000
... ... ... ... ... ... ... ... ... ...
354235 ZWE TXG_RPCH A 0 3 OL 9/5/2025 2026 -0.814
354236 ZWE TXG_RPCH A 0 3 OL 9/5/2025 2027 4.939
354237 ZWE TXG_RPCH A 0 3 OL 9/5/2025 2028 4.226
354238 ZWE TXG_RPCH A 0 3 OL 9/5/2025 2029 3.698
354239 ZWE TXG_RPCH A 0 3 OL 9/5/2025 2030 3.604

354240 rows × 9 columns

위 코드에서는 다음과 같은 작업을 수행합니다:

  • data_sets와 structures를 추출하여 각각의 시리즈와 관측치를 파싱합니다.
  • series_key를 ”:“로 분리하여 각 차원의 값을 추출합니다.
  • series_content의 attributes를 구조체의 attributes와 매핑하여 속성 값을 추출합니다.
  • observations를 순회하며 각 관측치의 시간과 값을 추출하여 최종적으로 판다스 데이터프레임으로 변환합니다.

이제 df 데이터프레임에는 각국의 GDP 데이터가 포함되어 있습니다. 이를 활용하여 다양한 분석과 시각화를 수행할 수 있습니다.

특정 데이터를 조회하기 위해서 국가 코드, 지표 코드 등을 알아야 합니다. 예를 들어, 미국은 “USA”, 한국은 “KOR”입니다. 주요 GDP 지표는 다음과 같습니다:

  • 실질 GDP (현재 가격): NGDP_R
  • 실질 GDP (전년 대비 증감률): NGDP_RPCH
  • GDP 갭 (실질 GDP - 잠재 GDP): NGAP_NPGDP

다음 링크에서 각국의 GDP 데이터를 직접 조회할 수도 있습니다: https://data.imf.org/en/Data-Explorer?datasetUrn=IMF.RES:WEO(9.0.0)

조회한 테이블 좌측의 ℹ️ 아이콘을 클릭하면 Timeseries code를 확인할 수 있는데, 여기서 국가 코드와 지표 코드를 알 수 있습니다. 예를 들어, Timeseries code: KOR.NGDP_RPCH.A는 한국(KOR)의 실질 GDP 성장률(NGDP_RPCH)을 의미합니다. A는 연간 데이터를 나타냅니다.

alt text

다음은 미국과 한국의 실질 GDP와 실질 GDP 성장률을 시각화하는 예제 코드입니다.

import matplotlib.pyplot as plt
# NGDP_R: 실질 GDP (현재 가격)
# NGDP_RPCH: 실질 GDP (전년 대비 증감률)
# NGAP_NPGDP: GDP 갭 (실질 GDP - 잠재 GDP)
fig, axes = plt.subplots(nrows=2, ncols=3, figsize=(12, 6))
# 미국
us_data = df[(df["COUNTRY"] == "USA") & (df["INDICATOR"] == "NGDP_R")].copy()
us_data["VALUE"] = pd.to_numeric(us_data["VALUE"])
us_data.plot(x="TIME_PERIOD", y="VALUE", title="US Real GDP Over Time", kind="line", ax=axes[0, 0])
us_data = df[(df["COUNTRY"] == "USA") & (df["INDICATOR"] == "NGDP_RPCH")].copy()
us_data["VALUE"] = pd.to_numeric(us_data["VALUE"])
us_data.plot(x="TIME_PERIOD", y="VALUE", title="US Real GDP Growth Rate Over Time", kind="line", ax=axes[0, 1])
us_data = df[(df["COUNTRY"] == "USA") & (df["INDICATOR"] == "NGAP_NPGDP")].copy()
us_data["VALUE"] = pd.to_numeric(us_data["VALUE"])
us_data.plot(x="TIME_PERIOD", y="VALUE", title="US GDP Gap Over Time", kind="line", ax=axes[0, 2])
# 한국
kr_data = df[(df["COUNTRY"] == "KOR") & (df["INDICATOR"] == "NGDP_R")].copy()
kr_data["VALUE"] = pd.to_numeric(kr_data["VALUE"])
kr_data.plot(x="TIME_PERIOD", y="VALUE", title="Korea Real GDP Over Time", kind="line", ax=axes[1, 0])
kr_data = df[(df["COUNTRY"] == "KOR") & (df["INDICATOR"] == "NGDP_RPCH")].copy()
kr_data["VALUE"] = pd.to_numeric(kr_data["VALUE"])
kr_data.plot(x="TIME_PERIOD", y="VALUE", title="Korea Real GDP Growth Rate Over Time", kind="line", ax=axes[1, 1])
kr_data = df[(df["COUNTRY"] == "KOR") & (df["INDICATOR"] == "NGAP_NPGDP")].copy()
kr_data["VALUE"] = pd.to_numeric(kr_data["VALUE"])
kr_data.plot(x="TIME_PERIOD", y="VALUE", title="Korea GDP Gap Over Time", kind="line", ax=axes[1, 2])
plt.tight_layout()

alt text

양국의 실질 GDP는 꾸준히 증가하는 추세를 보이며, 실질 GDP 성장률은 경기 변동에 따라 등락을 반복하는 모습을 확인할 수 있습니다.

GDP 갭 역시 경제 상황에 따라 변화하는 모습을 시각적으로 파악할 수 있습니다. 잠재 GDP는 경제가 완전 고용 상태에서 생산할 수 있는 최대 생산량을 나타내는데, GDP 갭이 양수일 경우 실제 GDP가 잠재 GDP를 초과하는 상태를 의미하며, 음수일 경우 그 반대를 의미합니다. GDP 갭이 양수로 크면 인플레이션 압력이 증가할 수 있고, 음수로 크면 경기 침체의 신호일 수 있습니다.

이와 같이 파이썬을 활용하여 IMF WEO 데이터베이스에서 각국의 GDP 데이터를 수집하고 분석할 수 있습니다. 미국, 한국 외에도 다양한 국가의 GDP 데이터를 동일한 방법으로 수집하여 비교 분석할 수 있습니다.

물가 지수와 코스피의 상관관계 분석

이번 포스트에서는 한국의 소비자/생산자 물가 지수와 코스피 사이의 상관관계를 분석합니다. 이 포스트에서 사용하는 데이터는 퀀티랩 네이버 카페의 금융 데이터 게시판에서 다운받을 수 있습니다.

먼저 다운받은 데이터들을 불러옵니다.

import pandas as pd
df_cpi = pd.read_csv('./cpi_20220215.csv', header=0)
df_ppi = pd.read_csv('./ppi_20220215.csv', header=0)
df_kospi = pd.read_csv('./kospi_20220218.csv', header=0)

이 세 데이터를 하나의 DataFrame으로 합치겠습니다. 코스피는 일별이고 CPI 및 PPI 데이터는 월별 데이터 입니다.

df = df_kospi.merge(df_cpi, how='left', on='month').fillna(method='ffill').dropna(subset=['cpi'])
df = df.merge(df_ppi, how='left', on='month').fillna(method='ffill').dropna(subset=['ppi'])
df['dt'] = pd.to_datetime(df['date'], format='%Y%m%d')
df['cpi'] = df['cpi'].astype(float)
df['ppi'] = df['ppi'].astype(float)
df = df.reset_index(drop=True)

월별 데이터를 일별 데이터로 month 열을 기준으로 left join 했습니다. 여기서 cpi, ppi 열의 데이터 형식이 문자열로 되어 있어서 float로 캐스팅 했습니다.

이제 코스피와 CPI, 코스피와 PPI 차트를 출력해 보려고 합니다. 두 데이터의 값의 범위가 크게 차이나기 때문에 y 축을 2개 사용합니다.

다음과 같이 두 시계열을 별도의 y축으로 그리는 함수인 get_multchart()를 작성했습니다.

def get_multchart(xticklabels, y1, y2, label1='', label2='', w=500, h=300, sizing_mode='stretch_width'):
x = np.arange(len(xticklabels))
xticks = np.linspace(0, len(xticklabels)-1, 10, endpoint=True, dtype=int).tolist()
p = figure(plot_width=w, plot_height=h, tools='', y_axis_location='left')
p.xaxis.ticker = xticks
p.xaxis.major_label_overrides = {_x: util.format_date(str(xticklabels[_x])) for _x in xticks}
p.xaxis.major_label_orientation = -np.pi / 4
p.yaxis.major_label_text_color = 'red'
p.yaxis.formatter = NumeralTickFormatter(format='0,0')
p.y_range = Range1d(y1.min() * 0.95, y1.max() * 1.05)
p.extra_y_ranges = {
'y2_range': Range1d(y2.min() * 0.95, y2.max() * 1.05)
}
y2_axis = LinearAxis(y_range_name='y2_range')
y2_axis.major_label_text_color = 'blue'
y2_axis.formatter = NumeralTickFormatter(format='0,0')
p.add_layout(y2_axis, 'right')
p.line(x, y1, line_color='red', legend_label=label1)
p.line(x, y2, line_color='blue', legend_label=label2, y_range_name='y2_range')
configure_legend(p)
grid = gridplot([[p]], toolbar_location=None, sizing_mode=sizing_mode)
return grid

이 함수를 이용해서 두 차트를 그립니다. 먼저 코스피와 CPI, 다음으로 코스피와 PPI를 그립니다.

from bokeh.io import output_notebook, show
output_notebook()
show(get_multchart(df['dt'], df['close'], df['cpi'], label1='KOSPI', label2='CPI'))
show(get_multchart(df['dt'], df['close'], df['ppi'], label1='KOSPI', label2='PPI'))

cpi

ppi

여기서 CPI, PPI는 전월 대비 % 값입니다. CPI와 코스피를 봤을 때 2001년 부근에서는 서로 역관계처럼 보이다가 2007년 부근에서는 코스피가 CPI를 선행하는 것처럼도 보입니다. PPI도 구간에 따라서 상관성이 다르게 보입니다. 즉 물가 지수와 코스피만으로 서로가 밀접한 상관성이 있다고 말할 수는 없어 보입니다. 이 부부은 2008년 금융위기로 주식 폭락과 물가

실제로 피어슨 상관계수를 구해 보면 구간에 따라서 매우 큰 차이가 생기는 것을 볼 수 있습니다.

from scipy.stats import pearsonr
_df = df[df['date'] >= '20150101']
print(pearsonr(_df['close'], _df['cpi']))
print(pearsonr(_df['close'], _df['ppi']))
(0.3184625030996628, 1.1693520291646116e-42)
(0.7737092632552787, 0.0)

그렇다고 상관성이 아주 없다고 볼 수도 없습니다. 주식 딥러닝 모델을 구축할 때 학습데이터의 피처로 충분히 활용할 만하다고 보입니다.

구글 검색 트렌드와 코스피 상관관계 분석

이번 포스트에서는 “kospi” 검색어에 대한 구글 검색 트렌드와 실제 코스피 지수를 한 차트에 그려보고 상관관계 분석을 해보겠습니다. 여기서 사용하는 데이터는 퀀티랩 네이버 카페에 올려 놓았습니다. 카페에 가입하시면 다운 받으실 수 있습니다.

위 데이터를 다운 받은 후 먼저 아래와 같이 구글 검색 트렌드 데이터를 불러옵니다. 이 데이터는 주간 단위의 데이터이므로 4년치라도 행의 수는 많지 않습니다.

import pandas as pd
df = pd.read_csv('./google_trend_kospi_2018_2021.csv', header=1)
df.columns = ['date', 'interest']
df['date'] = pd.to_datetime(df['date'], format="%Y-%m-%d")
df
indexdateinterest
02018-01-0726
12018-01-1430
22018-01-2128
32018-01-2829
42018-02-0433
2032021-11-2853
2042021-12-0556
2052021-12-1255
2062021-12-1960
2072021-12-2652

이 데이터를 다음처럼 차트로 그려볼 수 있습니다. 차트를 그릴 때 다양한 라이브러리를 활용할 수 있지만 여기서는 bokeh 라이브러리를 사용했습니다.

from math import pi
from bokeh.io import output_notebook, show
from bokeh.plotting import figure, gridplot
from bokeh.models import DatetimeTickFormatter
output_notebook()
grid = []
p = figure(title='Google Trend (kospi)', plot_width=500, plot_height=200, tools="crosshair")
p.line(df['date'], df['interest'], line_color='black')
p.xaxis.formatter = DatetimeTickFormatter(months=["%Y-%m-%d"])
p.xaxis.major_label_orientation = pi/4
grid.append([p])
p = gridplot(grid, toolbar_location=None)
show(p)

1

이제 코스피 종가 데이터를 불러옵니다. 위 구글 트렌드 데이터와 같은 y축으로 그리기 위해서 종가 값을 0~100 사이로 조정하여 pos 컬럼에 추가했습니다.

df_kospi = pd.read_csv('./kospi_2018_2021.csv', converters={'date': str})
df_kospi['date'] = pd.to_datetime(df_kospi['date'], format="%Y%m%d")
df_kospi['pos'] = ((df_kospi['close'] - df_kospi['close'].min()) / (df_kospi['close'].max() - df_kospi['close'].min())) * 100
df_kospi
indexdateclosepos
02018-01-022479.64990255.316438
12018-01-032486.35009855.679087
22018-01-042466.45996154.602531
32018-01-052497.52002056.283661
42018-01-082513.28002957.136674
9812021-12-243012.42993284.153237
9822021-12-272999.55004983.456111
9832021-12-283020.23999084.575958
9842021-12-292993.29003983.117288
9852021-12-302977.64990282.270763

이제 코스피 종가 데이터도 차트에 추가로 그려봅니다.

from math import pi
from bokeh.plotting import figure, show
from bokeh.models import DatetimeTickFormatter
from bokeh.models import Legend
grid = []
p = figure(title='Google Trend (kospi) and KOSPI Position', plot_width=800, plot_height=200, tools="crosshair")
a1 = p.line(df['date'], df['interest'], line_color='black')
a2 = p.line(df_kospi['date'], df_kospi['pos'], line_color='red')
p.xaxis.formatter = DatetimeTickFormatter(months=["%Y-%m-%d"])
p.xaxis.major_label_orientation = pi/4
legend = Legend(items=[
('G. Trend', [a1]),
('KOSPI Pos.', [a2]),
], location="center")
p.add_layout(legend, 'right')
grid.append([p])
p = gridplot(grid, toolbar_location=None)
show(p)

2

눈으로 보기에는 “kospi” 검색어가 급증할 때 코스피 지수가 폭락한 경우가 많았음이 보입니다. 공포 심리에 의해 뉴스 등을 찾아보기 위해 검색이 급증하고 실제로 코스피 지수도 요동치는 경우가 있습니다. 물론 상관성은 높지 않아 보이지만 검색 급증이 있으면 하나의 작은 위험 신호로 고려하는 것도 나쁘지 않을 것입니다. 조심해서 나쁠 것은 없습니다.

다음과 같이 상관 계수를 뽑아 보면 그렇게 상관성은 높게 나오지 않습니다. 시계열 전체에서의 상관성 보다는 검색 급증 시그널 정도만 잘 포착하는 것이 의미있어 보입니다.

from scipy.stats import pearsonr
_df = df.merge(df_kospi, how='outer', on='date').sort_values(by='date')
_df = _df.ffill()
_df = _df.bfill()
pearsonr(_df['pos'], _df['interest'])
(0.14169129281665557, 8.844934622779502e-07)

원자재 지수와 주가의 상관관계 분석

이번 포스트에서는 대표적인 원자재 지수인 S&P GSCI와 주식 종목 주가와의 상관관계를 분석해봅니다. 다음 코드 블록은 종목코드 데이터, 원자재 지수 데이터를 불러오는 부분입니다. 데이터는 퀀티랩 네이버 카페금융데이터 메뉴에서 공유드립니다.

이번 포스트에서는 대표적인 원자재 지수인 S&P GSCI와 주식 종목 주가와의 상관관계를 분석해봅니다. 다음 코드 블록은 종목코드 데이터, 원자재 지수 데이터를 불러오는 부분입니다. 데이터는 퀀티랩 네이버 카페금융데이터 메뉴에서 공유드립니다.

# 종목코드 데이터 준비
import json
with open('../../../data/stockcodes_20220128.json', encoding='utf-8') as f:
stockcodes = json.load(f)
stockcodes

[[‘082740’, ‘HSD엔진’], [‘001390’, ‘KG케미칼’], [‘011070’, ‘LG이노텍’], [‘002360’, ‘SH에너지화학’], [‘001740’, ‘SK네트웍스’], [‘071970’, ‘STX중공업’], [‘024070’, ‘WISCOM’], [‘011420’, ‘갤럭시아에스엠’], [‘267290’, ‘경동도시가스’], [‘002240’, ‘고려제강’], [‘001290’, ‘상상인증권’], [‘014530’, ‘극동유화’], [‘214330’, ‘금호에이치티’], [‘013700’, ‘까뮤이앤씨’], [‘090350’, ‘노루페인트’], [‘005250’, ‘녹십자홀딩스’], [‘004440’, ‘삼일씨엔에스’], [‘014160’, ‘대영포장’], [‘003090’, ‘대웅’], [‘003220’, ‘대원제약’], [‘002880’, ‘대유에이텍’], [‘001620’, ‘케이비아이동국실업’], [‘023450’, ‘동남합성’], [‘004140’, ‘동방’], [‘000640’, ‘동아쏘시오홀딩스’], [‘225330’, ‘씨엠에스에듀’], [‘101240’, ‘씨큐브’], [‘047920’, ‘HLB제약’], [‘013990’, ‘아가방컴퍼니’], [‘052710’, ‘아모텍’], …]

# 원자재 데이터 준비
import pandas as pd
df_gsci = pd.read_csv('../../../data/gsci_20220128.csv', converters={'date': str})
df_gsci
dateclosediffdiffratiohighlowopenvolume
20150102414.9583-3.177835-0.0076421.5643412.5496418.12250
20150105404.6091-10.332034-0.0249414.9583403.7641414.95830
20150106397.9523-6.676373-0.0165406.4719396.1576404.60910
20150107396.6795-1.273449-0.0032400.6534392.2534397.95230
20150108397.10100.4363310.0011399.5170393.1882397.22570
20220121608.8819-2.568090-0.0042611.4234599.3347611.42340
20220124601.5777-7.306612-0.0120612.6561595.1512608.88190
20220125611.03789.4450070.0157611.3097601.1267601.57770
20220126619.54848.4936610.0139621.5063608.4988611.03780
20220127616.7431-2.787889-0.0045623.2471613.9215619.54840

다음은 종목 일봉 차트와 원자재 지수의 상관관계를 구합니다. 여기서 사용한 상관분석 방법은 피어슨 상관분석 입니다. 피어슨은 상관계수가 -1에서 1 사이의 값으로, 1에 가까울수록 정관계, -1에 가까울수록 역관계임을 의미합니다.

# 종목 데이터 준비
# 종목과 원자재 상관관계 저장
from scipy.stats import pearsonr
from tqdm import tqdm
df_corr = pd.DataFrame()
for code, name in tqdm(stockcodes):
data = {'code': code, 'name': name}
df_stockcandles_day = pd.read_csv(
f'../../../data/stockcandles_day_20220128/stockcandles_day_{code}_{name}_20220128.csv',
converters={'date': str}
)
start_date = df_stockcandles_day.iloc[0]['date']
end_date = df_stockcandles_day.iloc[-1]['date']
_df_gsci = df_gsci[(df_gsci['date'] >= start_date) & (df_gsci['date'] <= end_date)]
_df = pd.merge(df_stockcandles_day, _df_gsci, how='left', on='date', suffixes=['_stock', '_commodity'])
_df = _df.dropna()
if len(_df) < 20:
continue
corr, p = pearsonr(_df['close_stock'], _df['close_commodity'])
if p <= 0.05:
data['gsci'] = corr
else:
data['gsci'] = 0
df_corr = df_corr.append(data, ignore_index=True)
columns = df_corr.columns.tolist()
columns.remove('code')
columns.remove('name')
columns = ['code', 'name'] + columns
df_corr = df_corr[columns]
df_corr
codenamegsci
082740HSD엔진0.697773
001390KG케미칼0.771450
011070LG이노텍0.650439
002360SH에너지화학-0.084560
001740SK네트웍스-0.138996
298870케이티비네트워크-0.954783
400840하이제7호스팩0.000000
397880교보11호스팩0.000000
396770엔에이치스팩22호-0.438219
400560하나금융20호스팩0.000000

이렇게 종목 일봉 차트와 GSCI의 상관계수를 구하고 다음과 같이 후처리합니다.

# NaN 제거
df_corr2 = df_corr.dropna().copy()
# 데이터 저장
df_corr2.to_csv('data/corr.csv', index=False)
# 데이터 불러오기
str_columns = ['code', 'name']
df_corr2 = pd.read_csv('data/corr.csv', converters={k: lambda x: str(x) for k in str_columns})

먼저 NaN을 제거하고 데이터를 저장합니다. 이 데이터도 퀀티랩 네이버 카페에 올려 놓겠습니다.

다음과 같이 스팩과 리츠 종목은 제거하고 상관관계가 높은 종목을 확인합니다.

# 스팩, 리츠 제외
df_corr3 = df_corr2[~(df_corr2['name'].str.endswith('스팩') | df_corr2['name'].str.endswith('리츠'))]

먼저 정관계로 상관관계가 높은 종목을 랭킹하여 가시화합니다.

# 상관관계 높은 종목 가시화
from quantylab.common import viz
for idx, row in df_corr3.sort_values(by='gsci', ascending=False).head(n=5).iterrows():
df_stockcandles_day = pd.read_csv(
'../../../data/stockcandles_day_20220128/stockcandles_day_{}_{}_20220128.csv'.format(row['code'], row['name']),
converters={'date': str}
)
start_date = df_stockcandles_day.iloc[0]['date']
end_date = df_stockcandles_day.iloc[-1]['date']
_df_gsci = df_gsci[(df_gsci['date'] >= start_date) & (df_gsci['date'] <= end_date)]
_df = pd.merge(df_stockcandles_day, _df_gsci, how='left', on='date', suffixes=['_stock', '_commodity'])
_df = _df.dropna().copy().reset_index(drop=True)
print('{} {} {}'.format(row['name'], row['max_commodity'], row['max_corr']))
viz.show_notebook(viz.get_multchart(_df['date'], _df['close_stock'], _df['close_commodity'], label1=row['name'], label2=row['max_commodity']))

1 2 3 4 5

주로 소형 제조업 위주로 랭킹되는 것을 볼 수 있습니다.

다음으로 역관계로 상관관계가 높은 종목을 랭킹하여 가시화합니다.

# 역 상관관계 높은 종목 가시화
from quantylab.common import viz
for idx, row in df_corr3.sort_values(by='gsci', ascending=True).head(n=5).iterrows():
df_stockcandles_day = pd.read_csv(
'../../../data/stockcandles_day_20220128/stockcandles_day_{}_{}_20220128.csv'.format(row['code'], row['name']),
converters={'date': str}
)
start_date = df_stockcandles_day.iloc[0]['date']
end_date = df_stockcandles_day.iloc[-1]['date']
_df_gsci = df_gsci[(df_gsci['date'] >= start_date) & (df_gsci['date'] <= end_date)]
_df = pd.merge(df_stockcandles_day, _df_gsci, how='left', on='date', suffixes=['_stock', '_commodity'])
_df = _df.dropna().copy().reset_index(drop=True)
print('{} {} {}'.format(row['name'], row['max_commodity'], row['max_corr']))
viz.show_notebook(viz.get_multchart(_df['date'], _df['close_stock'], _df['close_commodity'], label1=row['name'], label2=row['max_commodity']))

6 7 8 9 10

주로 연구 위주의 중소형주가 상위랭킹되는 것을 볼 수 있습니다.

데이터 받아가셔서 다양하게 분석해 보시기 바랍니다.

공매도 데이터 분석하기

이번 포스트에서는 파이썬으로 공매도 데이터를 분석해 봅니다. 공매도 데이터 수집에 대해서는 이 포스트를 확인해 주세요.

공매도 거래, 공매도 잔고, 종목 정보 데이터를 가져옵니다. 이 부분은 개인적으로 사용하는 데이터베이스를 활용하므로 퀀티랩 카페에서 CSV 파일을 다운받아 사용하시기 바랍니다.

import pandas as pd
from quantylab.common.db.mongodb import MongoDBClient
coll = MongoDBClient.get_coll('short')
cursor = coll.find({'date': {'$gte': '20210601', '$lte': '20220114'}}, {'_id': 0, 'dt_update': 0, 'date_update': 0}, sort=[('date', -1)])
df_short = pd.DataFrame(list(cursor))
coll = MongoDBClient.get_coll('shortbalance')
cursor = coll.find({'date': {'$gte': '20210601', '$lte': '20220114'}}, {'_id': 0, 'dt_update': 0, 'date_update': 0}, sort=[('date', -1)])
df_shortbalance = pd.DataFrame(list(cursor))
coll = MongoDBClient.get_coll('stockcodes')
cursor = coll.find({}, {'_id': 0, 'code': 1, 'name': 1})
df_stockcodes = pd.DataFrame(list(cursor))

위에서 준비한 세 DataFrame을 합칩니다. 이 합친 데이터를 CSV 파일로 저장해 둡니다. 이 CSV 데이터를 카페에 올려 놓았습니다. 카페 가입하셔서 다운받으시고 질문도 해주시면 감사하겠습니다.

df = pd.merge(df_short, df_shortbalance[['code', 'date', 'bal_rto', 'bal_amt', 'bal_qty']], on=['code', 'date'], how='left')
df = pd.merge(df, df_stockcodes, on='code', how='left')
df = df[['name'] + df.columns.tolisZt()[:-1]]
df.to_csv('20220113_analysis_short.csv', index=False)

CSV 파일을 다운받으셨다면 다음 부분부터 직접 해보실 수 있습니다. 다음 코드는 CSV 파일을 불러오는 부분입니다.

import pandas as pd
str_columns = ['code', 'date']
df = pd.read_csv('20220113_analysis_short.csv', converters={k: lambda x: str(x) for k in str_columns})

code, date를 숫자로 읽는 것을 방지하기 위해서 converters 인자를 넣어줍니다. 그리고 다음 코드에서처럼 date로 정렬하고 % 단위를 떼고 NaN 값이 포함된 행을 버립니다.

df = df.sort_values(by='date')
# % 단위 떼기
df['short_ratio'] /= 100
df['bal_rto'] /= 100
df = df.dropna().copy()
namecodedateavg_priceavg_price_ratioclosediffdiffratioshort_amountshort_ratioshort_volumevolumebal_rtobal_amtbal_qty
LX홀딩스3838002021060111078-7811000500.46262140.0125910000000000012366218792880.034639000.03149.0
DB금융투자01661020210601721557220500.74120.0031695711801730.00.00.0
CJ제일제당097950202106014843271173485500-3000-0.611225350.04372530578950.002921517360000.044320.0
CJ씨푸드011150202106014819148201052.23223.8e-054511881320.00831435482760.0297818.0
BGF리테일28233020210601183995518400025001.38649500.0898383530392930.00070000000000000012272032000.012348.0
엠씨넥스0975202022011456312-125630038007.241411.9e-052513217200.00292909752900.051683.0
더블유게임즈192080202201145396623454200-400-0.73478510.104212000000000018867850860.013913883763600.0256158.0
PI첨단소재17892020220114527343665310010001.92345560.04419500000000000565531482750.008613446300600.0253226.0
포스코케미칼00367020220114129380620130000-2000-1.52940330.04343672681673250.0235236509260000.01819302.0
LG이노텍011070202201143810223478384500320009.0835523490.053839323217319870.00560000000000000150517917000.0131386.0

이 데이터에서 종목별로 상환 규모를 확인하고 공매도 거래량, 공매도 잔고, 상환 규모의 5일 이동평균을 구합니다. 이 과정은 몇 분 정도 걸릴 수 있습니다. 그래서 tqdm으로 진행율을 확인합니다.

from tqdm import tqdm
for k, g in tqdm(df.groupby('code')):
# 상환 규모 확인
# 당일 short_volume - (당일 bal_qty - 전일 bal_qty)
df.loc[df['code'] == k, 'bal_qty_diff'] = g['bal_qty'].diff()
df.loc[df['code'] == k, 'repay_volume'] = (df.loc[df['code'] == k, 'short_volume'] - df.loc[df['code'] == k, 'bal_qty_diff']).clip(0)
df.loc[df['code'] == k, 'repay_ratio'] = df.loc[df['code'] == k, 'repay_volume'] / df.loc[df['code'] == k, 'volume']
# 이동평균
df.loc[df['code'] == k, 'short_ratio_ma5'] = g['short_ratio'].fillna(0).rolling(5).mean()
df.loc[df['code'] == k, 'bal_rto_ma5'] = g['bal_rto'].fillna(0).rolling(5).mean()
df.loc[df['code'] == k, 'repay_ratio_ma5'] = df.loc[df['code'] == k, 'repay_ratio'] .fillna(0).rolling(5).mean()

이렇게 구한 repay_ratio_ma5로 내림차순 정렬을 해보면 다음과 같은 결과를 얻을 수 있습니다.

_df = df[df['date'] == '20220114'].sort_values(by='repay_ratio_ma5', ascending=False)
_df
namecodedateavg_priceavg_price_ratioclosediffdiffratioshort_amountshort_ratioshort_volumevolumebal_rtobal_amtbal_qtybal_qty_diffrepay_volumerepay_ratioshort_ratio_ma5bal_rto_ma5repay_ratio_ma5
BGF리테일28233020220114145501-1501144000-2000-1.371887430.3144512972412530.00133342384000.023211.04353.08619.00.208930259617482380.242506399999999980.00130000000000000020.2374309360178176
세방전지0044902022011469346-14669200-1800-2.54223220.0946353219340150.019118506225200.0267431.0-6377.09596.00.28211083345582830.10481040.020260.22422523200232863
호텔신라00877020220114773049677400-1100-1.45945430.222531769103456150.0717217719311400.02812911.083286.00.00.00.2711130.069780000000000010.2142213088295697
금호석유화학01178020220114174115-11517400025001.462938540.075005168772250110.05263459838000.01514137.0-44184.061061.00.27136895529551890.07733960.05190.20765174156183894
효성첨단소재29805020220114532588412533000-8000-1.483140670.1590775897370700.014735143888000.065936.015093.00.00.00.15405920.013160.18448044554531187
콤텍시스템031820202201149570957-16-1.6419e-0689371840.0011123620475.0129175.00.08.08.536210605388056e-064.259999999999999e-050.00109999999999999984.258825517099485e-05
TCC스틸00271020220114129500129508006.5830.0250848320.00.00.00.02.03.933266625131371e-073.48e-050.03.473958840739652e-05
미래산업025560202201141365010013750-350-2.4812.5e-051397060.00.00.00.01.02.518511056263537e-052.46e-050.02.4705969543888606e-05
이스타코01502020220114310503105-110-3.4215.999999999999999e-0647216860.00.00.00.04.05.542576688476706e-062.3000000000000024e-050.02.2816870909533658e-05
오리엔트바이오00263020220114131551320201.5400.0124794550.0013198262680.0150199.00.01.04.033144380519106e-075.200000000000005e-060.00135.125393445035716e-06

이 종목들 중에서 공매도 상환율이 높은 상위 5개 종목을 살펴보겠습니다.

1

최근 공매도 거래가 높은데 공매도 잔고는 크게 늘지 않고 유지되고 있습니다. 즉, 공매도 상환율도 높은 상황입니다. 공매도자들의 판단이 엇갈리고 있다고 보이는 지점입니다.

2

공매도 거래는 꾸준히 있는데 공매도 잔고가 떨어지고 있습니다.

3

11월에 어닝쇼크로 공매도 잔고가 급격하게 늘었습니다. 이후 공매도 거래는 많은데 공매도 잔고는 조금씩 오르는 모습입니다.

4

공매도 잔고가 꾸준히 늘다가 최근 하락 전환하는 모습입니다.

5

최근 공매도 거래가 상당한 상황입니다.

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

이번 포스트에서는 크레온 플러스 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

KRX에서 공매도 데이터 수집하기

이번 포스트에서는 KRX에서 개별종목 공매도 거래개별종목 공매도 잔고 데이터를 파이썬으로 수집하는 방법을 다룹니다.

필요한 파이썬 라이브러리는 다음과 같습니다.

  • pip install requests
  • pip install numpy
  • pip install pandas

먼저 KRX 정보데이터시스템에서 다음 두 메뉴를 확인해 주세요.

  • 통계 / 공매도 통계 / 공매도 거래 / 개별종목 공매도 거래
  • 통계 / 공매도 통계 / 공매도 잔고 / 개별종목 공매도 잔고

개별종목 공매도 거래 데이터 수집하기

Section titled “개별종목 공매도 거래 데이터 수집하기”

먼저 개별종목 공매도 거래 데이터를 받아오겠습니다. 크롬이나 웨일 브라우저에서 F12 키를 눌러 개발자 도구를 열어줍니다. 그리고 다음과 같이 Network 탭에서 Fetch/XHR 하위 탭을 확인합니다. 그리고 조회 버튼을 다시 누르면 아래에 getJsonData.cmd 내역이 뜨고 여기서 요청 정보를 확인할 수 있습니다.

1

여기서 Request URL, Request Headers, Form Data를 확인합니다. 다음과 같이 확인한 정보로 요청 준비를 합니다.

url = 'http://data.krx.co.kr/comm/bldAttendant/getJsonData.cmd'
headers = {
'Accept': 'application/json, text/javascript, */*; q=0.01',
'Accept-Encoding': 'gzip, deflate',
'Accept-Language': 'ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7',
'Connection': 'keep-alive',
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
'Host': 'data.krx.co.kr',
'Origin': 'http://data.krx.co.kr',
'Referer': 'http://data.krx.co.kr/contents/MDC/MDI/mdiLoader/index.cmd?menuId=MDC0203',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.118 Whale/2.11.126.23 Safari/537.36',
'X-Requested-With': 'XMLHttpRequest',
}
data = [
('bld', 'dbms/MDC/STAT/srt/MDCSTAT30101'),
('searchType', '1'),
('mktId', 'STK'),
('secugrpId', 'STMFRTSCIFDRFS'),
('secugrpId', 'SRSW'),
('secugrpId', 'BC'),
('inqCond', 'STMFRTSCIFDRFSSRSWBC'),
('trdDd', '20211105'),
('share', '1'),
('money', '1'),
('csvxls_isNo', 'false'),
]

이렇게 준비한 url, headers, data로 requests 요청을 보냅니다. Request Method를 보면 POST를 확인할 수 있습니다.

import requests
res = requests.post(url, headers=headers, data=data)
res
Output: <Response [200]>

Status Code가 200으로 요청이 성공했음을 확인했습니다. 다음으로 받아온 데이터를 DataFrame으로 저장합니다.

import json
short_list = json.loads(res.text)['OutBlock_1']
import pandas as pd
df = pd.DataFrame(short_list)
df.columns = ['종목코드', '종목명', '증권구분', '공매도거래량', '공매도거래량업틱룰', '공매도거래량업틱룰예외', '거래량', '거래량비중', '공매도거래대금', '공매도거래대금업틱룰', '공매도거래대금업틱룰예외', '거래대금', '거래대금비중']
df
index종목코드종목명증권구분공매도거래량공매도거래량업틱룰공매도거래량업틱룰예외거래량거래량비중공매도거래대금공매도거래대금업틱룰공매도거래대금업틱룰예외거래대금거래대금비중
0095570AJ네트웍스주권00059,7390.00000335,609,2800.00
1006840AK홀딩스주권00020,2250.00000508,366,0000.00
2027410BGF주권147,2900147,2908,276,9041.78959,076,1300959,076,13054,013,149,7701.78
3282330BGF리테일주권1,4801,4027822,4896.58244,731,000231,828,50012,902,5003,734,658,0006.55
4138930BNK금융지주주권54,96254,96202,228,7702.47463,644,520463,644,520018,819,972,0802.46
1028069260휴켐스주권72,49872,0734251,137,3256.371,919,680,5501,908,522,05011,158,50030,290,522,8506.34
1029000540흥국화재주권00083,2810.00000318,332,1400.00
1030000547흥국화재2우B주권0001920.000005,323,0000.00
1031000545흥국화재우주권0009880.000008,514,7700.00
1032003280흥아해운주권0001,114,0590.000003,560,429,5450.00

거래량비중으로 내림차순 정렬을 해보면 공매도 비중이 높은 종목 순위를 확인할 수 있습니다.

df.sort_values('거래량비중', ascending=False)
index종목코드종목명증권구분공매도거래량공매도거래량업틱룰공매도거래량업틱룰예외거래량거래량비중공매도거래대금공매도거래대금업틱룰공매도거래대금업틱룰예외거래대금거래대금비중
51294870HDC현대산업개발주권54,93554,9350562,4339.771,353,057,7501,353,057,750013,880,230,4009.75
1004008770호텔신라주권66,04056,3889,652684,4589.655,269,414,8004,501,247,500768,167,30054,557,366,5009.66
800039490키움증권주권4,5614,07149047,2999.64484,180,000432,296,50051,883,5005,018,881,5009.65
813003240태광산업주권676706979.6166,460,00066,460,0000692,149,0009.60
940180640한진칼주권5,5874,96162658,8329.50315,127,600279,921,10035,206,5003,317,193,8009.50
433007210벽산주권2023,489,8850.008,06008,06014,442,649,3350.00
434002760보락주권000230,6080.00000490,745,8650.00
436000890보해양조주권0001,282,0730.000001,233,822,7750.00
438001270부국증권주권0006,8280.00000166,036,9500.00
1032003280흥아해운주권0001,114,0590.000003,560,429,5450.00

그런데 조금 이상합니다. 거래량비중이 생각보다 적고 9.XX 값이 상위에 랭크됩니다. 이는 데이터 형식이 문자열(str)로 되어 있을 때 자주 겪는 문제입니다. 다음과 같이 거래량비중 컬럼의 데이터 형식을 float로 변경합니다.

df['거래량비중'] = df['거래량비중'].astype(float)

다시 정렬 결과를 확인하면 다음과 같이 제대로된 랭킹이 된 것을 확인할 수 있습니다.

df.sort_values('거래량비중', ascending=False)
index종목코드종목명증권구분공매도거래량공매도거래량업틱룰공매도거래량업틱룰예외거래량거래량비중공매도거래대금공매도거래대금업틱룰공매도거래대금업틱룰예외거래대금거래대금비중
539068270셀트리온주권150,827133,03017,797488,77930.8631,363,423,00027,715,592,5003,647,830,500101,280,970,50030.97
1024081660휠라홀딩스주권161,370160,878492665,89024.235,958,633,4505,940,402,90018,230,55024,692,808,60024.13
673214320이노션주권6,9946,85613830,81022.70403,284,700395,322,1007,962,6001,776,867,00022.70
382011170롯데케미칼주권45,95634,17611,780206,11622.3010,110,596,0007,520,623,0002,589,973,00045,414,691,50022.26
240002350넥센타이어주권27,71225,1762,536125,20422.13208,348,430189,322,06019,026,370941,065,64022.14
433007210벽산주권2023,489,8850.008,06008,06014,442,649,3350.00
434002760보락주권000230,6080.00000490,745,8650.00
436000890보해양조주권0001,282,0730.000001,233,822,7750.00
438001270부국증권주권0006,8280.00000166,036,9500.00
1032003280흥아해운주권0001,114,0590.000003,560,429,5450.00

개별종목 공매도 잔고 데이터 수집하기

Section titled “개별종목 공매도 잔고 데이터 수집하기”

이제 개별종목 공매도 잔고 데이터를 받아 보겠습니다. 비슷하게 요청 준비를 합니다.

url = 'http://data.krx.co.kr/comm/bldAttendant/getJsonData.cmd'
headers = {
'Accept': 'application/json, text/javascript, */*; q=0.01',
'Accept-Language': 'ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7',
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
'Origin': 'http://data.krx.co.kr',
'Referer': 'http://data.krx.co.kr/contents/MDC/MDI/mdiLoader/index.cmd?menuId=MDC0201',
}
data = [
('bld', 'dbms/MDC/STAT/srt/MDCSTAT30501'),
('searchType', '1'),
('mktTpCd', '1'),
('trdDd', '20211102'),
('share', '1'),
('money', '1'),
('csvxls_isNo', 'false'),
]
res = requests.post(url, headers=headers, data=data)
res
Output: <Response [200]>

다음과 같이 받아온 데이터를 DataFrame으로 변환합니다.

df_shortbalance = pd.DataFrame(json.loads(res.text)['OutBlock_1'])
df_shortbalance
indexISU_CDISU_ABBRVBAL_QTYLIST_SHRSBAL_AMTMKTCAPBAL_RTO
0095570AJ네트웍스34,25546,822,295190,115,250259,863,737,2500.07
1006840AK홀딩스4,59313,247,561121,484,850350,397,988,4500.03
2027410BGF48,44095,716,791279,014,400551,328,716,1600.05
3282330BGF리테일5,01617,283,906825,132,0002,843,202,537,0000.03
4138930BNK금융지주524,715325,935,2464,601,750,5502,858,452,107,4200.16
934069260휴켐스953,65840,878,58826,320,960,8001,128,249,028,8002.33
935000540흥국화재33,30164,242,645129,540,890249,903,889,0500.05
936000547흥국화재2우B0153,60004,331,520,0000.00
937000545흥국화재우0768,00006,673,920,0000.00
938003280흥아해운84,187240,424,899311,070,965888,370,001,8050.04
df_shortbalance.columns = ['종목코드', '종목명', '공매도잔고수량', '상장주식수', '공매도잔고금액', '시가총액', '비중']
df_shortbalance
index종목코드종목명공매도잔고수량상장주식수공매도잔고금액시가총액비중
0095570AJ네트웍스34,25546,822,295190,115,250259,863,737,2500.07
1006840AK홀딩스4,59313,247,561121,484,850350,397,988,4500.03
2027410BGF48,44095,716,791279,014,400551,328,716,1600.05
3282330BGF리테일5,01617,283,906825,132,0002,843,202,537,0000.03
4138930BNK금융지주524,715325,935,2464,601,750,5502,858,452,107,4200.16
934069260휴켐스953,65840,878,58826,320,960,8001,128,249,028,8002.33
935000540흥국화재33,30164,242,645129,540,890249,903,889,0500.05
936000547흥국화재2우B0153,60004,331,520,0000.00
937000545흥국화재우0768,00006,673,920,0000.00
938003280흥아해운84,187240,424,899311,070,965888,370,001,8050.04

이번에는 비중 컬럼으로 내림차순 정렬을 해봅니다. 마찬가지로 문자열 정렬 문제가 있을 수 있으므로 이 컬럼을 float으로 변환하고 정렬합니다.

df_shortbalance['비중'] = df_shortbalance['비중'].astype(float)
df_shortbalance.sort_values('비중', ascending=False)
index종목코드종목명공매도잔고수량상장주식수공매도잔고금액시가총액비중
78034220LG디스플레이28,935,291357,815,700568,578,468,1507,031,078,505,0008.09
339032350롯데관광개발4,726,86769,275,66294,300,996,6501,382,049,456,9006.82
910008770호텔신라2,462,55739,248,121198,974,605,6003,171,248,176,8006.27
46011200HMM20,968,085405,392,487574,525,529,00011,107,754,143,8005.17
523019170신풍제약2,499,52752,984,990128,225,735,1002,718,129,987,0004.72
313030720동원수산04,653,805045,886,517,3000.00
315014825동원시스템즈우0265,35808,809,885,6000.00
644000230일동홀딩스011,540,4000156,949,440,0000.00
316163560동일고무벨트013,900,0000116,760,000,0000.00
261006340대원전선071,016,9670142,033,934,0000.00

공매도 거래와 공매도 잔고 데이터 합치기

Section titled “공매도 거래와 공매도 잔고 데이터 합치기”

이제 두 데이터를 합쳐서 확인해 보겠습니다. df는 공매도 거래 데이터이고 df_shortbalance는 공매도 잔고 데이터입니다. 이들 둘을 pandas merge() 메소드로 합치겠습니다.

_df = df.merge(df_shortbalance, how='outer', on='종목코드', suffixes=('', '_dup'))

이 데이터로 다양한 분석을 해볼 수 있겠습니다. 여기서는 공매도 잔고는 많으면서 공매도 거래는 적은 순으로 정렬해 봅니다.

_df['score'] = _df['비중'] / (_df['거래량비중'] + 1)
_df.sort_values('score', ascending=False)
index종목코드종목명증권구분공매도거래량공매도거래량업틱룰공매도거래량업틱룰예외거래량거래량비중공매도거래대금공매도거래대금업틱룰공매도거래대금업틱룰예외거래대금거래대금비중종목명_dup공매도잔고수량상장주식수공매도잔고금액시가총액비중score
97034220LG디스플레이주권4,4202,2902,1301,949,1940.2389,668,95046,585,15043,083,80039,626,663,8000.23LG디스플레이28,935,291357,815,700568,578,468,1507,031,078,505,0008.096.577236
620097520엠씨넥스주권3003083,6760.041,333,05001,333,0503,713,085,8500.04엠씨넥스435,40917,963,28219,332,159,600797,569,720,8002.422.326923
370032350롯데관광개발주권17,56716,928639882,3321.99324,702,700312,941,65011,761,05016,281,919,2501.99롯데관광개발4,726,86769,275,66294,300,996,6501,382,049,456,9006.822.280936
699006490인스코비주권67506751,384,4220.052,285,32002,285,3204,702,224,2550.05인스코비1,824,640109,848,1006,541,334,400393,805,438,5001.661.580952
578003620쌍용차주권00000.0000000.00쌍용차2,180,965149,840,0026,041,273,050415,056,805,5401.461.460000
98375701B93현대유퍼스트부동산25호 A수익증권00000.0000000.00NaNNaNNaNNaNNaNNaNNaN
98475702B93현대유퍼스트부동산25호 C-i수익증권00000.0000000.00NaNNaNNaNNaNNaNNaNNaN
98575701BA7현대유퍼스트부동산30호[파생형] A수익증권00000.0000000.00NaNNaNNaNNaNNaNNaNNaN
98675702BA7현대유퍼스트부동산30호[파생형] C-i수익증권00000.0000000.00NaNNaNNaNNaN

공매도 잔고가 많다는 것은 다시 상환해야 할 물량이 많다는 것이고 동시에 공매도 거래 비중이 적다면 주가가 바닥에 가까워 졌다고 추측해 볼 여지가 있습니다. 주식 거래에서 확실한 것은 없습니다. 다양한 금융데이터 분석을 통해 성공적인 투자에 한걸음씩 다가가는 것이 중요하다고 봅니다.

퀀티랩 종목분석 기능 소개 (feat. 삼성전자 종목분석)

이번 포스트에서는 퀀티랩 종목분석에서 제공하는 데이터들을 소개하고자 합니다.

현재 퀀티랩 종목분석에서는 회사 기본정보, 퀀티랩 모델로 판단한 종목랭킹 추이, 일봉차트, MACD, RSI, 재무제표, PER, PBR, ROE, 공매도, 투자 주체별 수급 분석, 일봉수급점수를 제공합니다.

삼성전자 종목분석 페이지를 예로 각 항목을 살펴보겠습니다.

우선 삼성전자의 기본정보를 보여줍니다.

기본정보

코스피인지 코스닥인지, 업종과 주요제품은 무엇인지 등을 보여줍니다.

QL 모델(퀀티랩 중장기 모델)의 종목랭킹 변화를 확인할 수 있습니다. 8월초 주가 급락에 따라 순위가 하락했다가 최근 다시 상위권으로 회복한 모습입니다.

퀀티랩모델

기술적분석에서는 일봉 차트와 이동평균선, 볼린저밴드를 함께 보여주고 그 아래에 MACD, RSI 차트도 보여줍니다. MACD 상으로는 주가 급락 이후 8월 중순쯤 MACD 신호를 돌파하면서 매수타이밍으로 판단할 수 있고 RSI 상으로도 비슷한 시점을 과냉 상태로 볼 수 있어서 매수를 고려해볼 수 있습니다. 다만 기술적분석만으로 투자에 임하는 것은 매우 위험함을 말씀드립니다.

기술적분석

기본적분석은 재무제표와 PER, PBR, ROE 지표를 주로 봅니다.

재무제표의 재무상태표, 손익계산서, 현금흐름표에서 주요 계정들을 뽑아서 다음과 같이 차트로 보여줍니다.

재무제표

삼성전자의 재무 상태는 매우 좋습니다. 300조에 육박하는 자본을 가지고 있다는 것에 놀라울 따름입니다. 당기순이익을 보면 성장이 약간 둔화된 듯 보여서 아쉬움은 있습니다. 현금흐름도 아주 안정적입니다. 재무제표 보는 법에 대해서는 이 포스트를 참고해 주세요.

수익성, 안정성, 성장성 지표로 주로 사용되는 PER, PBR, ROE를 차트로 보여줍니다.

지표

성장주들에 비한다면야 PER도 그렇게 높지 않고 물리적인 자산이 많은걸 감안해도 PBR은 상당히 낮아서 상승여력은 충분해 보입니다.

그리고 수급분석에서는 공매도 현황과 기관/외국인/개인들의 순매수/순매도량을 보여주고 일봉에서 계산한 수급 점수인 일봉수급점수를 보여줍니다.

다음은 공매도 현황을 보여줍니다. 공매도는 주식을 빌려서 파는 행위를 말합니다. 공매도자는 주가가 하락하면 수익이 생깁니다.

공매도

  • 공매도 잔고(위 차트)는 주식을 빌려서 판 후에 아직 갚지 않은 주식수 입니다.
  • 공매도량(아래 차트)은 빌려서 판 주식 수 입니다.

삼성전자의 경우 공매도가 많은 상황은 아닌 것으로 보입니다.

다음 차트는 투자 주체(기관/외국인/개인)들의 순매수/순매도량은 보여줍니다.

투자자

삼성전자를 외국인이 8월초에 대량 매도하고 그걸 개인이 대부분 받은 것을 볼 수 있습니다.

다음은 일봉수급점수 차트입니다.

일봉수급점수

일봉수급점수는 240일, 120일, 60일, 20일, 10일, 5일 동안의 거래 강도를 나타냅니다. 1에 가까울수록 매수세가, -1에 가까울수록 매도세가 강했음을 의미합니다.

삼성전자의 일봉수급점수는 매도세가 약간 강한 상태로 유지 중입니다.

그럼 여기까지 퀀티랩 종목분석 기능소개를 마치겠습니다.

앞으로도 유익한 데이터를 많이 생성해서 제공할 것이니 퀀티랩에 많은 관심 부탁드립니다.

퀀티랩 Long-Term Trading (QL) 모델 개선 작업

퀀티랩 주식투자 대표 모델인 Quantylab Long-Term Trading (QL) 모델을 업데이트 했습니다. 기존 모델에 비해 다양한 시장지표 및 종목지표를 추가하고 레이블링을 조정하는 작업이었습니다.

Feature 수는 412개로 326개 시장지표 자질과 86개 종목지표 자질로 구성했습니다. 각종 지표에 Moving Average 등을 적용하여 자질 수는 부풀려진 면이 있습니다.

시장지표는 코스피/코스닥 지수, 해외 주식 시장 지수, 환율, 채권, 금리, 원자재, GSCI, 달러 지수, BDI, 필라델피아 반도체 지수, 미국시장 변동성 지수, MSCI 지수 등을 포함합니다. 종목지표는 종목 주가, 거래량, 시가총액, Bollinger Band, MACD, RSI, 수급, PER, PBR, ROE, 배당수익률, 기관/외국인/개인 수급, 공매도 비율, 대차잔고액 등을 포함합니다. 이들 시장지표 및 종목지표 자질들을 모아서 표준화 (Standardization) 했습니다.

레이블링은 수익성, 변동성, 거래규모 지표를 만들어 조합했습니다. 수익성 지표는 중장기 기간에서 종가의 변곡점을 찾아서 현재 종가와 다음 변곡점과의 차이를 기대 수익률로 삼았습니다. 변동성 지표는 변곡점 사이에서의 표준편차로 구했습니다. 거래규모 지표는 종목의 거래대금에 log를 취해 사용했습니다.

이렇게 자질과 레이블링을 마치고 적당히 심층신경망 레이어를 구성하여 학습했습니다. 중간에 이런저런 실수로 시행착오를 겪었지만 다 생략하고 결과를 바로 공유하려 합니다.

학습에는 2015~2019년 기간내의 데이터를 사용했습니다. 이렇게 학습한 모델로 2020년부터 현재까지 일단위로 종목 점수를 구하여 매수/매도를 판단해서 종가매매 백테스팅을 수행했습니다. 즉, 학습과 백테스팅 데이터가 겹쳐지지 않게하여 의미있는 백테스팅이 되도록 했습니다.

백테스팅

백테스팅 결과 2020년 부터 2021-09-17 까지의 평가 수익률은 +73.07%로 동기간 코스피 대비 30% 정도 높았습니다. QL 모델로 매수/매도를 판단한 내역은 다음과 같습니다.

날짜매매종목명단가수량수익률평가총액
2021-08-19매도현대자동차206,50058+20.41%173,831,949
2021-05-06매수LG전자151,50066177,219,388
2021-05-06매수SK케미칼256,00039177,220,888
2021-05-06매수LG화학938,00010177,222,386
2021-05-06매도포스코395,50051+103.87%177,223,793
2021-03-23매도현대모비스286,50049+40.79%172,520,635
2021-03-23매수셀트리온292,50034172,517,645
2021-03-23매수삼성전기185,00054172,519,136
2021-01-13매도삼성SDI754,00026+100.80%178,728,737
2021-01-08매도삼성전기193,000112+116.37%179,837,188
2020-11-16매도NAVER278,00055+53.17%148,783,070
2020-09-24매수SK텔레콤230,50043136,101,988
2020-09-24매수현대자동차171,50058136,103,475
2020-09-24매수케이티앤지80,400124136,104,967
2020-09-24매도셀트리온245,50261+51.06%136,106,463
2020-08-03매도LG화학635,00031+102.23%137,605,846
2020-07-24매수현대모비스203,50049131,218,600
2020-07-24매수포스코194,00051131,215,651
2020-07-24매수삼성SDI375,50026131,217,135
2020-07-24매도엔씨소프트802,00017+41.95%131,220,096
2020-06-29매도SK케미칼134,500163+120.49%128,438,310
2020-03-23매수삼성전기89,20011281,295,074
2020-03-23매도케이티앤지63,600108-31.24%81,296,573
2020-03-17매수SK케미칼61,00016385,736,761
2020-03-17매도현대자동차80,10084-32.12%85,738,252
2020-03-16매도삼성SDI253,00043+10.48%86,948,811
2020-03-16매도포스코162,00042-31.36%86,977,640
2020-03-16매수삼성전자48,90020486,945,828
2020-03-16매수셀트리온162,5226186,947,324
2020-03-13매도현대모비스171,50040-30.71%91,169,271
2020-01-03매수이마트122,5008199,845,350
2020-01-03매수NAVER181,5005599,846,838
2020-01-03매수케이티앤지92,50010899,848,336
2020-01-03매수삼성SDI229,0004399,849,834
2020-01-03매수엔씨소프트565,0001799,851,311
2020-01-02매수LG화학314,0003199,994,081
2020-01-02매수현대모비스247,5004099,995,541
2020-01-02매수현대자동차118,0008499,997,026
2020-01-02매수LG생활건강1,266,000799,992,752
2020-01-02매수포스코236,0004299,998,513

퀀티랩에서 매일 올라오는 QL 모델의 매매내역과 보유종목을 확인해 보세요.