공매도 데이터 분석하기
이번 포스트에서는 파이썬으로 공매도 데이터를 분석해 봅니다. 공매도 데이터 수집에 대해서는 이 포스트를 확인해 주세요.
공매도 거래, 공매도 잔고, 종목 정보 데이터를 가져옵니다. 이 부분은 개인적으로 사용하는 데이터베이스를 활용하므로 퀀티랩 카페에서 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()
name | code | date | avg_price | avg_price_ratio | close | diff | diffratio | short_amount | short_ratio | short_volume | volume | bal_rto | bal_amt | bal_qty |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
LX홀딩스 | 383800 | 20210601 | 11078 | -78 | 11000 | 50 | 0.46 | 26214 | 0.012591000000000001 | 23662 | 1879288 | 0.0 | 34639000.0 | 3149.0 |
DB금융투자 | 016610 | 20210601 | 7215 | 5 | 7220 | 50 | 0.7 | 412 | 0.003169 | 571 | 180173 | 0.0 | 0.0 | 0.0 |
CJ제일제당 | 097950 | 20210601 | 484327 | 1173 | 485500 | -3000 | -0.61 | 122535 | 0.0437 | 2530 | 57895 | 0.0029 | 21517360000.0 | 44320.0 |
CJ씨푸드 | 011150 | 20210601 | 4819 | 1 | 4820 | 105 | 2.23 | 22 | 3.8e-05 | 45 | 1188132 | 0.0083 | 1435482760.0 | 297818.0 |
BGF리테일 | 282330 | 20210601 | 183995 | 5 | 184000 | 2500 | 1.38 | 64950 | 0.089838 | 3530 | 39293 | 0.0007000000000000001 | 2272032000.0 | 12348.0 |
... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
엠씨넥스 | 097520 | 20220114 | 56312 | -12 | 56300 | 3800 | 7.24 | 141 | 1.9e-05 | 25 | 1321720 | 0.0029 | 2909752900.0 | 51683.0 |
더블유게임즈 | 192080 | 20220114 | 53966 | 234 | 54200 | -400 | -0.73 | 47851 | 0.10421200000000001 | 8867 | 85086 | 0.0139 | 13883763600.0 | 256158.0 |
PI첨단소재 | 178920 | 20220114 | 52734 | 366 | 53100 | 1000 | 1.92 | 34556 | 0.044195000000000005 | 6553 | 148275 | 0.0086 | 13446300600.0 | 253226.0 |
포스코케미칼 | 003670 | 20220114 | 129380 | 620 | 130000 | -2000 | -1.52 | 94033 | 0.043436 | 7268 | 167325 | 0.0235 | 236509260000.0 | 1819302.0 |
LG이노텍 | 011070 | 20220114 | 381022 | 3478 | 384500 | 32000 | 9.08 | 3552349 | 0.05383 | 93232 | 1731987 | 0.005600000000000001 | 50517917000.0 | 131386.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
name | code | date | avg_price | avg_price_ratio | close | diff | diffratio | short_amount | short_ratio | short_volume | volume | bal_rto | bal_amt | bal_qty | bal_qty_diff | repay_volume | repay_ratio | short_ratio_ma5 | bal_rto_ma5 | repay_ratio_ma5 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
BGF리테일 | 282330 | 20220114 | 145501 | -1501 | 144000 | -2000 | -1.37 | 188743 | 0.31445 | 12972 | 41253 | 0.0013 | 3342384000.0 | 23211.0 | 4353.0 | 8619.0 | 0.20893025961748238 | 0.24250639999999998 | 0.0013000000000000002 | 0.2374309360178176 |
세방전지 | 004490 | 20220114 | 69346 | -146 | 69200 | -1800 | -2.54 | 22322 | 0.094635 | 3219 | 34015 | 0.0191 | 18506225200.0 | 267431.0 | -6377.0 | 9596.0 | 0.2821108334558283 | 0.1048104 | 0.02026 | 0.22422523200232863 |
호텔신라 | 008770 | 20220114 | 77304 | 96 | 77400 | -1100 | -1.4 | 594543 | 0.222531 | 76910 | 345615 | 0.0717 | 217719311400.0 | 2812911.0 | 83286.0 | 0.0 | 0.0 | 0.271113 | 0.06978000000000001 | 0.2142213088295697 |
금호석유화학 | 011780 | 20220114 | 174115 | -115 | 174000 | 2500 | 1.46 | 293854 | 0.075005 | 16877 | 225011 | 0.05 | 263459838000.0 | 1514137.0 | -44184.0 | 61061.0 | 0.2713689552955189 | 0.0773396 | 0.0519 | 0.20765174156183894 |
효성첨단소재 | 298050 | 20220114 | 532588 | 412 | 533000 | -8000 | -1.48 | 314067 | 0.159077 | 5897 | 37070 | 0.0147 | 35143888000.0 | 65936.0 | 15093.0 | 0.0 | 0.0 | 0.1540592 | 0.01316 | 0.18448044554531187 |
... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
콤텍시스템 | 031820 | 20220114 | 957 | 0 | 957 | -16 | -1.64 | 1 | 9e-06 | 8 | 937184 | 0.0011 | 123620475.0 | 129175.0 | 0.0 | 8.0 | 8.536210605388056e-06 | 4.259999999999999e-05 | 0.0010999999999999998 | 4.258825517099485e-05 |
TCC스틸 | 002710 | 20220114 | 12950 | 0 | 12950 | 800 | 6.58 | 3 | 0.0 | 2 | 5084832 | 0.0 | 0.0 | 0.0 | 0.0 | 2.0 | 3.933266625131371e-07 | 3.48e-05 | 0.0 | 3.473958840739652e-05 |
미래산업 | 025560 | 20220114 | 13650 | 100 | 13750 | -350 | -2.48 | 1 | 2.5e-05 | 1 | 39706 | 0.0 | 0.0 | 0.0 | 0.0 | 1.0 | 2.518511056263537e-05 | 2.46e-05 | 0.0 | 2.4705969543888606e-05 |
이스타코 | 015020 | 20220114 | 3105 | 0 | 3105 | -110 | -3.42 | 1 | 5.999999999999999e-06 | 4 | 721686 | 0.0 | 0.0 | 0.0 | 0.0 | 4.0 | 5.542576688476706e-06 | 2.3000000000000024e-05 | 0.0 | 2.2816870909533658e-05 |
오리엔트바이오 | 002630 | 20220114 | 1315 | 5 | 1320 | 20 | 1.54 | 0 | 0.0 | 1 | 2479455 | 0.0013 | 198262680.0 | 150199.0 | 0.0 | 1.0 | 4.033144380519106e-07 | 5.200000000000005e-06 | 0.0013 | 5.125393445035716e-06 |
이 종목들 중에서 공매도 상환율이 높은 상위 5개 종목을 살펴보겠습니다.
BGF리테일
최근 공매도 거래가 높은데 공매도 잔고는 크게 늘지 않고 유지되고 있습니다. 즉, 공매도 상환율도 높은 상황입니다. 공매도자들의 판단이 엇갈리고 있다고 보이는 지점입니다.
세방전지
공매도 거래는 꾸준히 있는데 공매도 잔고가 떨어지고 있습니다.
호텔신라
11월에 어닝쇼크로 공매도 잔고가 급격하게 늘었습니다. 이후 공매도 거래는 많은데 공매도 잔고는 조금씩 오르는 모습입니다.
금호석유화학
공매도 잔고가 꾸준히 늘다가 최근 하락 전환하는 모습입니다.
효성첨단소재
최근 공매도 거래가 상당한 상황입니다.