콘텐츠로 이동
16 posts in the category “Data”

업비트에서 실시간 가격 및 호가 데이터 받아오기 (코드 포함)

업비트 WebSocket 실시간 데이터 대표 이미지

암호화폐 트레이딩 봇이나 데이터 분석 시스템을 개발할 때 가장 중요한 것 중 하나는 실시간 시장 데이터를 빠르고 안정적으로 수집하는 것입니다. 이번 포스트에서는 업비트(Upbit) 거래소의 WebSocket API를 활용하여 실시간으로 가격과 호가 데이터를 받아오는 방법을 다룹니다.

업비트는 REST API와 WebSocket API 두 가지 방식을 제공합니다. 실시간 데이터 수집에는 WebSocket이 훨씬 효율적입니다.

REST API의 단점:

  • 주기적으로 요청(polling)해야 하므로 네트워크 오버헤드가 큽니다
  • API 호출 제한(rate limit)에 걸리기 쉽습니다
  • 실시간성이 떨어집니다

WebSocket의 장점:

  • 한 번 연결하면 서버가 데이터를 실시간으로 푸시합니다
  • 네트워크 효율성이 높고 지연시간이 적습니다
  • 여러 종목을 동시에 구독 가능합니다
Terminal window
pip install websocket-client keyring keyrings.alt
  • websocket-client: WebSocket 클라이언트 라이브러리
  • keyring: API 키를 안전하게 관리하기 위한 라이브러리
  • keyrings.alt: keyring의 파일 기반 백엔드

이번 예제 코드는 다음과 같은 구조로 설계되었습니다:

  1. WebSocket 기본 클래스: 스레드 기반으로 동작하며 재사용 가능한 WebSocket 연결 관리
  2. TickerWebSocket: 실시간 가격 정보 수신용
  3. OrderbookWebSocket: 실시간 호가 정보 수신용
  4. 비동기 처리: asyncio를 활용한 데이터 처리 루프
class WebSocket(threading.Thread):
def __init__(self, subscribe_type: str, codes: list):
super().__init__(daemon=True) # 데몬 스레드로 설정
self.ws = websocket.WebSocketApp(...)

daemon=True로 설정하면 메인 프로그램이 종료될 때 함께 종료됩니다.

업비트 WebSocket API는 다음 형식의 구독 메시지를 요구합니다:

subscribe_message = [
{"ticket": str(uuid.uuid4())}, # 고유 식별자
{"type": "ticker", "codes": ["KRW-BTC", "KRW-ETH"]}, # 구독 정보
]
  • ticket: 요청을 구분하는 고유 ID (UUID 사용 권장)
  • type: 데이터 타입 (ticker, orderbook, trade 등)
  • codes: 구독할 마켓 코드 리스트
self.ws.run_forever(ping_interval=30, ping_timeout=10, reconnect=2)
  • ping_interval=30: 30초마다 ping 메시지 전송
  • ping_timeout=10: ping 응답 타임아웃 10초
  • reconnect=2: 연결 끊김 시 2초 후 자동 재연결
async def process(self):
count = 0
last_time = 0
while True:
if self.last_data:
self.process_data() # 데이터 처리 로직
count += 1
await asyncio.sleep(1 - (time.time() - last_time))
last_time = time.time()

매 초마다 최신 데이터를 처리하면서도 정확한 시간 간격을 유지합니다.

공개 데이터(가격, 호가)는 API 키 없이도 사용 가능하지만, 개인 거래 정보에는 필요합니다:

import keyring
from keyrings.alt.file import PlaintextKeyring
keyring.set_keyring(PlaintextKeyring())
keyring.set_password("upbit", "access_key", "your_access_key")
keyring.set_password("upbit", "secret_key", "your_secret_key")
Terminal window
python your_script.py

실행하면 다음과 같은 출력을 볼 수 있습니다:

WebSocket created: ticker ['KRW-ETH']
WebSocket created: orderbook ['KRW-ETH']
Starting WebSocket thread: ticker ['KRW-ETH']
Starting WebSocket thread: orderbook ['KRW-ETH']
Inserted ticker data at timestamp 1707393600000 | Count: 1
Inserted orderbook data at timestamp 1707393600000 | Count: 1

이 코드를 기반으로 다음과 같은 기능을 추가할 수 있습니다:

  1. 데이터베이스 저장: process_data() 메서드에서 MongoDB, PostgreSQL 등에 저장
  2. 실시간 알림: 특정 가격 도달 시 알림 전송
  3. 차트 시각화: Plotly, Matplotlib 등으로 실시간 차트 업데이트
  4. 트레이딩 봇: 가격 변동에 따른 자동 매매 로직 구현
  5. ML 모델 학습: 실시간 데이터를 feature로 활용
import asyncio
import uuid
import websocket
import threading
import json
import time
from keyrings.alt.file import PlaintextKeyring
import keyring
keyring.set_keyring(PlaintextKeyring())
# codes
# https://docs.upbit.com/kr/reference/list-trading-pairs
BITCOIN = "KRW-BTC"
ETHEREUM = "KRW-ETH"
access_key = keyring.get_password("upbit", "access_key")
secret_key = keyring.get_password("upbit", "secret_key")
class WebSocket(threading.Thread):
def __init__(self, subscribe_type: str, codes: list):
super().__init__(daemon=True)
self.ws = websocket.WebSocketApp(
"wss://api.upbit.com/websocket/v1",
on_open=self.on_open,
on_message=self.on_message,
on_error=self.on_error,
on_close=self.on_close
)
self.last_data = None
self.subscribe_type = subscribe_type
self.codes = codes
def on_open(self, ws):
assert self.subscribe_type != "", "subscribe_type must be set before running the WebSocket"
assert self.codes, "codes must be set before running the WebSocket"
print(f"WebSocket created: {self.subscribe_type} {self.codes}")
subscribe_message = [
{"ticket": str(uuid.uuid4())},
{"type": self.subscribe_type, "codes": self.codes},
]
ws.send(json.dumps(subscribe_message))
def on_message(self, ws, message):
data = json.loads(message)
self.last_data = data
def on_error(self, ws, err):
print(f"Error in WebSocket {self.subscribe_type} {self.codes}:", err)
def on_close(self):
print(f"WebSocket closed: {self.subscribe_type} {self.codes}")
def run(self):
print(f"Starting WebSocket thread: {self.subscribe_type} {self.codes}")
self.ws.run_forever(ping_interval=30, ping_timeout=10, reconnect=2)
async def process(self):
count = 0
last_time = 0
while True:
if self.last_data:
self.process_data()
count += 1
print(f"\rInserted {self.subscribe_type} data at timestamp {self.last_data['timestamp']} | Count: {count}", end="", flush=True)
await asyncio.sleep(1 - (time.time() - last_time))
last_time = time.time()
def process_data(self):
pass
class TickerWebSocket(WebSocket):
def __init__(self):
super().__init__(subscribe_type="ticker", codes=[ETHEREUM])
def process_data(self):
# do something
pass
class OrderbookWebSocket(WebSocket):
def __init__(self):
super().__init__(subscribe_type="orderbook", codes=[ETHEREUM])
def process_data(self):
# do something
pass
async def main():
ticker_ws = TickerWebSocket()
orderbook_ws = OrderbookWebSocket()
ticker_ws.start()
orderbook_ws.start()
ticker_task = asyncio.create_task(ticker_ws.process())
orderbook_task = asyncio.create_task(orderbook_ws.process())
await asyncio.gather(ticker_task, orderbook_task)
if __name__ == "__main__":
asyncio.run(main())
  1. 연결 안정성: 네트워크 상태에 따라 연결이 끊길 수 있으므로 재연결 로직이 중요합니다.
  2. 메모리 관리: last_data를 계속 덮어쓰므로 메모리 누수는 없지만, 데이터를 저장하는 경우 메모리 관리에 주의해야 합니다.
  3. 에러 처리: 실제 운영 환경에서는 더 상세한 에러 처리와 로깅이 필요합니다.

이번 포스트에서는 업비트 WebSocket API를 활용한 실시간 데이터 수집 방법을 알아보았습니다.

핵심 포인트:

  • WebSocket은 실시간 데이터 수집에 효율적입니다
  • 스레드와 asyncio를 조합하여 안정적인 데이터 처리가 가능합니다
  • 자동 재연결 기능으로 연결 안정성을 확보할 수 있습니다

이 코드를 기반으로 여러분만의 트레이딩 시스템이나 분석 도구를 만들어보세요!

파이썬으로 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 데이터를 동일한 방법으로 수집하여 비교 분석할 수 있습니다.

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

이번 포스트에서는 크레온 플러스 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 정보데이터시스템에서 다음 두 메뉴를 확인해 주세요.

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

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

섹션 제목: “개별종목 공매도 거래 데이터 수집하기”

먼저 개별종목 공매도 거래 데이터를 받아오겠습니다. 크롬이나 웨일 브라우저에서 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

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

섹션 제목: “개별종목 공매도 잔고 데이터 수집하기”

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

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

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

섹션 제목: “공매도 거래와 공매도 잔고 데이터 합치기”

이제 두 데이터를 합쳐서 확인해 보겠습니다. 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

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

대신증권 크레온 API로 매수/매도 주문 넣고 확인하기

이번 포스트에서는 대신증권 크레온 API로 주식 주문을 내고 실시간으로 체결을 확인하고 주문 내역을 조회하는 방법을 다룹니다.

먼저 매수 또는 매도 주문을 내는 방법을 알아보겠습니다. 주문은 크레온 API의 CpTrade.CpTd0311 모듈을 사용하면 됩니다.

다음과 같이 order() 함수를 선언하고, action, code, amount를 매수/매도 구분, 주문할 종목 코드, 주식 수로 각각 입력 받도록 했습니다. 물론 더 다양한 입력을 받을 수 있게 이 함수를 수정할 수 있습니다.

import win32com.client
class Creon:
def __init__(self):
self.orderevent_handler = None
def order(self, action, code, amount):
if not code.startswith('A'):
code = 'A' + code
account_no, account_gflags = self.init_trade()
self.obj_CpTrade_CpTd0311.SetInputValue(0, action) # 1: 매도, 2: 매수
self.obj_CpTrade_CpTd0311.SetInputValue(1, account_no) # 계좌번호
self.obj_CpTrade_CpTd0311.SetInputValue(2, account_gflags[0]) # 상품구분
self.obj_CpTrade_CpTd0311.SetInputValue(3, code) # 종목코드
self.obj_CpTrade_CpTd0311.SetInputValue(4, amount) # 매수수량
self.obj_CpTrade_CpTd0311.SetInputValue(8, '03') # 시장가
result = self.obj_CpTrade_CpTd0311.BlockRequest()
if result != 0:
print('order request failed.', file=sys.stderr)
status = self.obj_CpTrade_CpTd0311.GetDibStatus()
msg = self.obj_CpTrade_CpTd0311.GetDibMsg1()
if status != 0:
print('order failed. {}'.format(msg), file=sys.stderr)
def buy(self, code, amount):
return self.order('2', code, amount)
def sell(self, code, amount):
return self.order('1', code, amount)

매수할 때는 buy() 함수를 호출해서 action을 2로 지정하고, 매도할 때는 sell() 함수를 호출해서 action을 1로 지정하도록 했습니다.

여기서는 시장가로 매수/매도를 하도록 했는데 지정가, 최유리지정가, 최우선지정가 등으로 변경할 수 있습니다. 이 부분은 CpTrade.CpTd0311 문서에서 SetInputValue의 8 - (string) 주문호가구분코드 항목을 함고하시기 바랍니다.

이렇게 매수 또는 매도 주문을 넣고 체결 되었을 때 다음과 같이 이벤트를 받을 수 있습니다.

class Creon:
def subscribe_orderevent(self, cb):
obj = win32com.client.Dispatch('Dscbo1.CpConclusion')
handler = win32com.client.WithEvents(obj, OrderEventHandler)
handler.set_attrs(obj, cb)
self.orderevent_handler = obj
obj.Subscribe()
def unsubscribe_orderevent(self):
if self.orderevent_handler is not None:
self.orderevent_handler.Unsubscribe()
self.orderevent_handler = None
class OrderEventHandler(EventHandler):
def OnReceived(self):
item = {
'계좌명': self.obj.GetHeaderValue(1),
'name': self.obj.GetHeaderValue(2),
'체결수량': self.obj.GetHeaderValue(3),
'체결가격': self.obj.GetHeaderValue(4),
'주문번호': self.obj.GetHeaderValue(5),
'원주문번호': self.obj.GetHeaderValue(6),
'계좌번호': self.obj.GetHeaderValue(7),
'상품관리구분코드': self.obj.GetHeaderValue(8),
'종목코드': self.obj.GetHeaderValue(9),
'매매구분코드': self.obj.GetHeaderValue(12),
'체결구분코드': self.obj.GetHeaderValue(14),
'체결구분코드': self.obj.GetHeaderValue(14),
'체결구분코드': self.obj.GetHeaderValue(14),
'현금신용대용구분코드': self.obj.GetHeaderValue(17),
}
self.cb(item)

subscribe_orderevent() 함수를 호출해서 체결 이벤트를 실시간으로 받아올 수 있습니다. 매수/매도 주문이 체결되면 지정된 콜백 함수 cb로 체결 정보를 전송합니다. 이를 위해서 OrderEventHandler 클래스를 선언했고, 이 클래스의 OnReceived() 함수에서 체결 정보를 cb에 전송합니다.

다음과 같이 지금까지 주문한 내역을 확인할 수도 있습니다.

class Creon:
def __init__(self, account_no=''):
self.obj_CpTrade_CpTd5341 = win32com.client.Dispatch('CpTrade.CpTd5341')
def get_trade_history(self):
account_no, account_gflags = self.init_trade()
self.obj_CpTrade_CpTd5341.SetInputValue(0, account_no)
self.obj_CpTrade_CpTd5341.SetInputValue(1, account_gflags[0]) # 상품구분
_fields = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 16, 17, 18, 19, 22, 24]
_keys = [
'상품관리구분코드', '주문번호', '원주문번호', '종목코드', '종목이름',
'주문내용', '주문호가구분코드내용', '주문수량', '주문단가', '총체결수량',
'체결수량', '체결단가', '확인수량', '정정취소구분내용 ', '거부사유내용',
'채권매수일', '거래세과세구분내용', '현금신용대용구분내용', '주문입력매체코드내용',
'정정취소가능수량', '매매구분',
]
result = self.request(self.obj_CpTrade_CpTd5341, dict(zip(_fields, _keys)), cntidx=6)
return result

주문 이후에 get_trade_history() 함수를 호출하면 다음과 같이 매수/매도 주문에 대한 정보들을 받아올 수 있습니다. 여기서는 삼성전자를 10주씩 매수->매도->매수한 다음 get_trade_history() 함수를 호출한 결과 입니다.

{'data': [{'상품관리구분코드': '10', '주문번호': 6095, '원주문번호': 0, '종목코드': 'A005930', '종목이름': '삼성전자', '주문내용': '정규장현금매수', '주문호가구분코드내용': '시장가', '주문수량': 10, '주문단가': 0, '총체결수량': 10, '체결수량': 10, '체결단가': 82430, '확인수량': 0, '정정취소구분내용 ': '정상주문', '거부사유내용': '', '채권매수일': '', '거래세과세구분내용': '', '현금신용대용구분내용': '', '주문입력매체코드내용': 'C8', '정정취소가능수량': 0, '매매구분': '매수'}, {'상품관리구분코드': '10', '주문번호': 6099, '원주문번호': 0, '종목코드': 'A005930', '종목이름': '삼성전자', '주문내용': '정규장현금매도', '주문호가구분코드내용': '시장가', '주문수량': 10, '주문단가': 0, '총체결수량': 10, '체결수량': 10, '체결단가': 82500, '확인수량': 0, '정정취소구분내용 ': '정상주문', '거부사유내용': '', '채권매수일': '', '거래세과세구분내용': '', '현금신용대용구분내용': '', '주문입력매체코드내용': 'C8', '정정취소가능수량': 0, '매매구분': '매도'}, {'상품관리구분코드': '10', '주문번호': 6121, '원주문번호': 0, '종목코드': 'A005930', '종목이름': '삼성전자', '주문내용': '정규장현금매수', '주문호가구분코드내용': '시장가', '주문수량': 10, '주문단가': 0, '총체결수량': 10, '체결수량': 10, '체결단가': 82500, '확인수량': 0, '정정취소구분내용 ': '정상주문', '거부사유내용': '', '채권매수일': '', '거래세과세구분내용': '', '현금신용대용구분내용': '', '주문입력매체코드내용': 'C8', '정정취소가능수량': 0, '매매구분': '매수'}]}

퀀티랩 systrader 프로젝트에서 전반적인 시스템 트레이딩 코드를 공개하고 있습니다. 앞으로 시스템 트레이딩에 필요한 주요 기능들을 퀀티랩 Github systrader 리포지토리에서 오픈소스로 개발해 나갈 예정입니다.

SysTrader의 주요 TODO 태스크는 다음과 같습니다.

  • 주요 시스템 트레이딩 기능 구현
  • 딥러닝 모델 기반 자동 매매
  • PyQt5 기반의 GUI 개발
  • 매매 현황 리포팅

관심 있으신 모든 분들께서 이 프로젝트에 참여해 주시면 감사하겠습니다. SysTrader는 모든 개미들을 위한 견고한 시스템 트레이딩 솔루션이 되는 것을 목표로 합니다.

대신증권 크레온 API로 실시간 주가 데이터 받기

이번 포스트에서는 대신증권 크레온 API를 이용한 실시간 주가를 획득하는 방법을 다룹니다. 여느 때와 같이 언어는 파이썬으로 진행합니다.

크레온 API는 문서화가 비교적 잘되어 있고 사용성이 좋습니다. [증권사 API 장단점 비교][/htsapi.html] 포스트에서 주요 증권사 API의 장단점을 비교해 놓았으니 참고하시기 바랍니다.

실시간 주가를 얻어오기 위한 모듈은 Dscbo1.StockCur이며 이에 대한 문서는 여기에서 확인할 수 있습니다.

실시간 데이터는 Publish/Subscribe 방식으로 데이터를 획득합니다. 크레온에서 실시간 주가 데이터를 발행(Publish)하고 사용자는 실시간 주가를 원하는 종목을 구독(Subscribe)합니다. 사용자는 크레온이 발행한 데이터를 콜백(callback)을 통해 받습니다.

그럼 파이썬으로 실시간 주가 데이터를 구독하는 함수와 데이터를 획득하는 콜백 함수를 구현해 보겠습니다.

import win32com.client
class Creon:
def __init__(self):
self.stockcur_handlers = {} # 주식/업종/ELW시세 subscribe event handlers
def subscribe_stockcur(self, code, cb):
if not code.startswith('A'):
code = 'A' + code
if code in self.stockcur_handlers:
return
obj = win32com.client.Dispatch('DsCbo1.StockCur')
obj.SetInputValue(0, code)
handler = win32com.client.WithEvents(obj, StockCurEventHandler)
handler.set_attrs(obj, cb)
self.stockcur_handlers[code] = obj
obj.Subscribe()
def unsubscribe_stockcur(self, code=None):
lst_code = []
if code is not None:
if not code.startswith('A'):
code = 'A' + code
if code not in self.stockcur_handlers:
return
lst_code.append(code)
else:
lst_code = list(self.stockcur_handlers.keys()).copy()
for code in lst_code:
obj = self.stockcur_handlers[code]
obj.Unsubscribe()
del self.stockcur_handlers[code]
class StockCurEventHandler:
def set_attrs(self, obj, cb):
self.obj = obj
self.cb = cb
def OnReceived(self):
item = {
'code': self.obj.GetHeaderValue(0),
'name': self.obj.GetHeaderValue(1),
'diffratio': self.obj.GetHeaderValue(2),
'timestamp': self.obj.GetHeaderValue(3),
'price_open': self.obj.GetHeaderValue(4),
'price_high': self.obj.GetHeaderValue(5),
'price_low': self.obj.GetHeaderValue(6),
'bid_sell': self.obj.GetHeaderValue(7),
'bid_buy': self.obj.GetHeaderValue(8),
'cum_volume': self.obj.GetHeaderValue(9), # 주, 거래소지수: 천주
'cum_trans': self.obj.GetHeaderValue(10),
'price': self.obj.GetHeaderValue(13),
'contract_type': self.obj.GetHeaderValue(14),
'cum_sellamount': self.obj.GetHeaderValue(15),
'buy_sellamount': self.obj.GetHeaderValue(16),
'contract_amount': self.obj.GetHeaderValue(17),
'second': self.obj.GetHeaderValue(18),
'price_type': chr(self.obj.GetHeaderValue(19)), # 1: 동시호가시간 예상체결가, 2: 장중 체결가
'market_flag': chr(self.obj.GetHeaderValue(20)), # '1': 장전예상체결, '2': 장중, '4': 장후시간외, '5': 장후예상체결
'premarket_volume': self.obj.GetHeaderValue(21),
'diffsign': chr(self.obj.GetHeaderValue(22)),
'LP보유수량':self.obj.GetHeaderValue(23),
'LP보유수량대비':self.obj.GetHeaderValue(24),
'LP보유율':self.obj.GetHeaderValue(25),
'체결상태(호가방식)':self.obj.GetHeaderValue(26),
'누적매도체결수량(호가방식)':self.obj.GetHeaderValue(27),
'누적매수체결수량(호가방식)':self.obj.GetHeaderValue(28),
}
self.cb(item)

이렇게 Creon 클래스에 실시간 주가를 구독하는 함수인 subscribe_stockcur와 구독을 취소하는 unsubscribe_stockcur 함수를 구현했습니다. subscribe_stockcur 함수에서 발행되는 주가 데이터를 처리하기 위한 StockCurEventHandler 클래스를 이어서 정의하였습니다.

subscribe_stockcur 함수에서 구독하는 종목마다 StockCurEventHandler를 할당해 주었습니다. 크레온이 주식 데이터를 발행하면 StockCurEventHandler의 OnReceived 함수가 호출됩니다.

여기서 필요한 데이터를 획득할 수 있습니다. 여기서는 이렇게 획득한 데이터를 StockCurEventHandler 객체를 생성할 때 인자로 넘겨준 cb 함수로 전달하고 있습니다.

다음과 같이 종목명과 가격을 출력하는 간단한 cb 함수를 선언하고 NAVER와 미래에셋증권 두 종목의 실시간 주가를 구독해 보았습니다.

from quantylab.systrader.creon import Creon
c = Creon()
def cb(item):
print(item['name'], item['price'])
c.subscribe_stockcur('035420', cb)
c.subscribe_stockcur('006800', cb)
# ...
c.unsubscribe_stockcur()

그러면 이런식으로 종목명과 주가가 출력되는 것을 볼 수 있습니다.

NAVER 389500
미래에셋증권 9990
미래에셋증권 10000
NAVER 390000
NAVER 389500
NAVER 389500
미래에셋증권 9990
NAVER 389500
NAVER 389500
미래에셋증권 9990
미래에셋증권 9990
미래에셋증권 9990
미래에셋증권 10000
미래에셋증권 9990
NAVER 390000
NAVER 389500
NAVER 389500
미래에셋증권 9990

퀀티랩 systrader 프로젝트에서 실시간 주가 확인 외에도 다양한 크레온 API 기능을 연결하고 있습니다. 퀀티랩 Github systrader 리포지토리에서 전체 코드를 확인할 수 있습니다. 문의는 이 리포지토리 이슈로 또는 네이버 카페로 주세요.

파이썬으로 DART에서 재무제표 수집하기

이번 포스트에서는 전자공시시스템(DART)에서 제공하는 재무제표를 파이썬으로 다운로드하고 파싱하는 방법을 다룹니다.

DART에서 재무제표를 얻어오는 방법은 다양한데 여기서는 두 가지 방법을 다룹니다.

첫 번째로 단일회사 주요계정 API를 활용한 방법입니다.

DART Open API를 사용하려면 인증키가 필요합니다. 인증키 획득 방법은 이 포스트에서 확인하세요.

다음은 단일회사 주요계정 API를 호출하여 재무제표 정보를 획득하는 함수 입니다. 인증키, 회사 고유번호, 사업연도, 보고서 코드를 입력하여 재무제표를 받아올 수 있습니다.

def get_fs(crtfc_key, corp_code, bsns_year, reprt_code):
# 단일회사 주요계정
# crtfc_key API 인증키 STRING(40) Y 발급받은 인증키(40자리)
# corp_code 고유번호 STRING(8) Y 공시대상회사의 고유번호(8자리)
# ※ 개발가이드 > 공시정보 > 고유번호 API조회 가능
# bsns_year 사업연도 STRING(1) Y 사업연도(4자리)
# ※ 2015년 이후 부터 정보제공
# reprt_code 보고서 코드 STRING(5) Y 1분기보고서 : 11013
# 반기보고서 : 11012
# 3분기보고서 : 11014
# 사업보고서 : 11011
api = 'https://opendart.fss.or.kr/api/fnlttSinglAcnt.json'
params = {
'crtfc_key': crtfc_key,
'corp_code': corp_code,
'bsns_year': bsns_year,
}
data = []
reprt_name = REPRT_NAMES.get(reprt_code, "")
params['reprt_code'] = reprt_code
res = self._request(api, params=params)
if res is None:
return None
if res.status_code != 200:
return None
item = json.loads(res.text)
if item.get('status', '') != '000':
return None
for _item in item.get('list'):
_item['corp_code'] = corp_code
_item['reprt_code'] = reprt_code
_item['reprt_name'] = reprt_name
data.append(_item)
return data

get_fs(crtfc_key, '00266961', 2015, '11011')와 같이 호출하면 다음과 같은 결과를 받아올 수 있습니다.

[{'account_nm': '자산총계', 'bfefrmtrm_amount': '9,881,190,909,324', 'bfefrmtrm_dt': '2018.12.31 현재', 'bfefrmtrm_nm': '제 20 기', 'bsns_year': '2020', 'corp_code': '00266961', 'frmtrm_amount': '12,299,527,120,786', 'frmtrm_dt': '2019.12.31 현재', 'frmtrm_nm': '제 21 기', ...}, {'account_nm': '부채총계', 'bfefrmtrm_amount': '3,932,050,396,031', 'bfefrmtrm_dt': '2018.12.31 현재', 'bfefrmtrm_nm': '제 20 기', 'bsns_year': '2020', 'corp_code': '00266961', 'frmtrm_amount': '5,795,601,052,206', 'frmtrm_dt': '2019.12.31 현재', 'frmtrm_nm': '제 21 기', ...}, ...]

DART Open API 재무정보 일괄 다운로드

섹션 제목: “DART Open API 재무정보 일괄 다운로드”

두 번째는 재무정보 일괄 다운로드로 한꺼번에 전체 기업의 재무제표 데이터를 받아오는 방법입니다. Open DART 사이트에서 공시정보 활용마당 / 재무정보 일괄다운로드를 클릭하면 다음과 같은 테이블을 볼 수 있습니다.

재무정보 일괄다운로드

여기서 다운로드 하면 해당 연도와 분기의 네 종류의 재무제표를 압축파일로 다운 받을 수 있습니다. 여기서는 다운로드, 압축해제를 포함하여 파이썬에서 재무제표를 읽어오는 방법을 다룹니다.

다음은 Selenium으로 다운로드 버튼을 클릭하여 파일을 다운로드하고 압축을 해제하는 코드입니다.

options = webdriver.ChromeOptions()
options.add_experimental_option("prefs", {
"download.default_directory": self.job_base,
"download.prompt_for_download": False,
"download.directory_upgrade": True,
"safebrowsing.enabled": True
})
options.add_argument("disable-infobars")
options.add_argument("--disable-extensions")
browser = webdriver.Chrome(executable_path=os.path.join(settings.QUANTYLAB_BASE, 'drivers/chromedriver'), chrome_options=options)
try:
browser.get('https://opendart.fss.or.kr/disclosureinfo/fnltt/dwld/main.do')
elem = browser.find_element_by_css_selector('table.tb01')
for elem_a in elem.find_elements_by_link_text('다운로드'):
filename = re.findall(r'.*\(.*\'(.+)\'\).*', elem_a.get_attribute('onclick'))[0]
# 파일 다운로드
elem_a.click()
time.sleep(10)
# 압축파일 로드
filepath = os.path.join(base, filename)
z = zipfile.ZipFile(filepath)
for info in z.infolist():
# 한글 파일명 인코딩 수정
_filename = info.filename.encode('cp437').decode('euc-kr')
_filepath = os.path.join(base, _filename)
if os.path.exists(_filepath):
continue
info.filename = _filename
# 압축해제
z.extract(info, base)
z.close()
except Exception as e:
print(str(e))
finally:
browser.quit()

이렇게 압축해제까지 하면 2020_1분기보고서_04_현금흐름표_20210211.txt과 같은 파일들이 생성되어 있을 것입니다.

이 텍스트 파일은 다음과 같이 pandas로 쉽게 DataFrame으로 읽어올 수 있습니다. 다만 sep과 encoding을 다음과 같이 적절하게 정해주는 것이 중요합니다.

df = pd.read_csv(filepath, sep='\t', encoding='cp949')

여기서 당기, 당기 1분기, 당기 반기 등의 다양한 컬럼이 생기는데 저는 이들을 당기로 통합하여 정리했습니다.

다음은 포괄손익계산서의 일부 항목을 나타낸 것입니다.

항목명결산기준일보고서종류당기
영업수익2018-12-31사업보고서5,586,904,533,355
영업이익(손실)2018-12-31사업보고서942,532,561,543
당기순이익(손실)2018-12-31사업보고서627,901,873,332

그런데 재무제표를 볼 때 항목의 흐름을 보는 것이 중요합니다. 예를 들어 영업 이익이 꾸준히 증가하고 있는지를 보고 싶은 것입니다.

그래서 동일 항목에 대해서 결산기준일 순서대로 DataFrame을 재구성하고자 합니다. 이를 위해 DataFrame의 pivot 함수를 사용합니다.

df_pivot = df.pivot(values='value', index='항목명', columns='결산기준일')

동일 항목이지만 항목명이 다르게 적혀있는 경우가 있어서 이를 맞춰주는 처리를 했습니다.

그럼 다음과 같이 동일 항목의 값을 분기 순서대로 볼 수 있습니다.

항목명2015-12-312016-03-312016-06-302016-09-302016-12-312017-03-312017-06-302017-09-302017-12-312018-03-312018-06-302018-09-302018-12-312019-03-312019-06-302019-09-302019-12-312020-03-312020-06-302020-09-30
매출액3,251,157,100,000937,280,220,152987,281,364,2951,013,075,223,7704,022,629,619,9821,082,247,281,1961,129,631,366,8471,200,676,275,2204,678,468,928,0321,309,059,864,2481,363,615,953,3321,397,713,834,6745,586,904,533,3551,510,861,770,2821,630,275,004,3951,664,815,000,0786,593,400,065,2441,732,064,462,0971,902,467,957,4911,360,779,374,259
영업이익762,203,849,000256,827,536,675272,663,961,847282,298,051,5251,102,040,475,792290,797,289,304285,213,522,180312,084,238,4441,179,187,806,331256,980,563,870250,583,189,279221,718,402,565942,532,561,543206,244,504,954128,334,709,480202,086,039,366710,070,173,513221,495,078,328230,597,573,115291,726,798,004
당기순이익516,986,279,000165,036,146,535213,223,506,226198,000,157,034759,072,951,187210,876,050,870171,420,229,303215,798,264,368770,101,670,207153,751,654,038281,758,577,70168,386,763,908627,901,873,33287,585,903,29027,756,852,97585,268,053,088396,821,062,465134,874,581,86590,682,203,810235,342,965,699

파이썬으로 KRX에서 전종목 기본 정보 얻어오기

이번 포스트에서는 코스피, 코스닥, 코넥스의 상장 종목들을의 기본 정보를 파이썬으로 획득하는 방법을 다룹니다. 한국거래소(KRX)에서 운영하는 기업공시채널 KIND라는 서비스가 있는데 여기서 정보를 얻어와 보겠습니다.

네이버 웨일이나 구글 크롬으로 접속하시길 추천드립니다.

먼저 KIND에 접속합니다.

1

위 그림의 하단에 빨간 박스 표시한 상장법인상세정보 메뉴를 클릭합니다.

그럼 다음과 같은 화면이 나오는데 여기서 우측에 있는 상장법인목록 메뉴를 클릭합니다.

2

그럼 상장법인목록을 확인할 수 있는 페이지가 뜨는데 여기서 EXCEL 버튼을 누르면 전체 상장 법인 목록을 다운받을 수 있습니다.

3

상장법인 목록 파이썬으로 가져오기

섹션 제목: “상장법인 목록 파이썬으로 가져오기”

이 파일을 파이썬으로 바로 다운 받을 수도 있습니다. 다양한 방법이 있겠지만 여기서는 pandas의 read_html 함수를 사용하겠습니다.

이제 개발자 도구를 열어봅니다. 웨일의 경우 다음과 같은 방법으로 개발자 도구를 열 수 있습니다.

  • 우측 상단의 ፧ 버튼을 클릭하고 기타 / 고급기능 / 개발자 도구를 클릭
  • Ctrl + Shift + I
  • F12

그럼 다음과 같이 개발자 도구가 나타납니다. (브라우저 하단에 뜰 수도 있습니다.)

4

여기서 Network 탭으로 이동합니다. 이동한 다음 상장법인목록 페이지의 EXCEL 버튼을 클릭합니다.

그럼 개발자 도구의 Network 탭에 다음과 같이 요청 정보가 표시됩니다.

5

여기서 빨간색 박스로 표시한 Request URL을 확인하고 아래로 쭉 내려서 Form Data를 확인합니다.

6

이제 이 정보를 가지고 요청 코드를 작성합니다.

def get_stocks(market=None):
market_type = ''
if market == 'kospi':
market_type = '&marketType=stockMkt'
elif market == 'kosdaq':
market_type = '&marketType=kosdaqMkt'
elif market == 'konex':
market_type = '&marketType=konexMkt'
url = 'http://kind.krx.co.kr/corpgeneral/corpList.do?currentPageSize=5000&pageIndex=1&method=download&searchType=13{market_type}'.format(market_type=market_type)
list_df_stocks = pd.read_html(url, header=0, converters={'종목코드': lambda x: str(x)})
df_stocks = list_df_stocks[0]
return df_stocks

read_html 함수를 호출할 때 첫 번째 행을 헤더로 인식하도록 headers=0을 넣어줬고 종목코드를 숫자로 인식하지 않도록 converters 인자를 주었습니다.

이렇게 코스피, 코스닥, 코넥스 전체 또는 시장 별로 종목 목록을 받아올 수 있습니다.

받아온 데이터에는 다음과 같이 회사명, 종목코드, 업종, 주요제품, 상장일, 결산월, 대표자명, 홈페이지, 지역 정보가 포함됩니다.

회사명종목코드업종주요제품상장일결산월대표자명홈페이지지역
DSR1556601차 비철금속 제조업합섬섬유로프2013-05-1512월홍석빈http://www.dsr.com부산광역시
GS글로벌001250상품 종합 도매업수출입업(시멘트,철강금속,전기전자,섬유,기계화학),상품중개,광업,채석업/하수처리 서비스/부동산 임대1976-06-2612월김태형http://www.gsgcorp.com서울특별시
HDC현대산업개발294870건물 건설업외주주택, 자체공사, 일반건축, 토목 등2018-06-1212월권순호, 정경구http://www.hdc-dvp.com서울특별시
KG케미칼001390기초 화학물질 제조업콘크리트혼화제, 비료, 친환경농자재, 수처리제1989-08-2512월곽정현, 김재익http://www.kgchem.co.kr울산광역시
LG이노텍011070전자부품 제조업기타 전자부품 제조업2008-07-2412월정철동http://www.lginnotek.co.kr서울특별시

파이썬으로 DART OpenAPI 사용하기

이번 포스트에서는 금융감독원에서 운영하는 전자공시시스템(DART)의 OpenAPI를 사용하는 방법을 다룹니다. DART OpenAPI를 통해 기업의 재무제표 등 주식 투자에 도움이 되는 다양한 정보를 얻을 수 있습니다.

우선 DART 사이트 http://dart.fss.or.kr/에 접속합니다. 그리고 상단의 작은 메뉴에서 오픈API 메뉴를 찾아서 클릭합니다.

DART 메뉴

인증키 신청/관리 메뉴로 가서 인증키 신청을 진행하고 로그인 하면 됩니다. 인증키를 신청할 때 사용자를 개인과 기관으로 구분하는데 기관으로 신청 시 입력할 사항들이 늘어나는 반면 공시목록 및 기업개황 조회를 한도제한 없이 사용할 수 있다고 합니다.

인증키 신청/관리

개발가이드를 보면 DART OpenAPI로 조회할 수 있는 데이터 종류들을 살펴볼 수 있습니다. 다음은 DART에서 제공하는 API들의 개발가이드 링크입니다.

여기서는 간단히 예로 DART OpenAPI 사용법을 알아보겠습니다.

DART API는 HTTP GET Method로 데이터 요청을 보냅니다. API마다 지원하는 출력 포멧(Zip 파일, JSON, XML)이 다릅니다. 대부분 JSON과 XML을 지원하고 요청 URL 마지막이 .json, .xml으로 출력포멧에 따라 다릅니다. 여기서는 JSON 포멧 요청 우선적으로 사용하겠습니다.

먼저 고유번호 API로 공시대상회사의 8자리 고유번호를 획득해 보겠습니다.

고유번호 API는 출력포멧이 Zip 파일입니다. 파이썬으로 다음과 같이 데이터를 확인할 수 있습니다.

여기서는 requests로 HTTP 요청을 수행합니다. API 파라미터로 입력해야 하는 crtfc_key는 위에서 발급했던 인증키를 입력하면 됩니다. 데이터가 binary 형태이기 때문에 ByteIO로 읽어주고 Zip 파일로 생성합니다.

import requests
from io import BytesIO
import zipfile
import xmltodict
api = 'https://opendart.fss.or.kr/api/corpCode.xml'
res = requests.get(api, params={'crtfc_key': crtfc_key})
data_xml = zipfile.ZipFile(BytesIO(res.content))

이렇게 하면 공시대상 기업의 고유번호 데이터를 Zip 압축 포멧으로 받을 수 있습니다. Zip 파일에 담겨있는 파일은 리스트는 다음과 같이 확인할 수 있습니다.

z.namelist()
output: ['CORPCODE.xml']

여기서는 편의상 다음과 같이 XML 파일을 dict로 변환했습니다.

import xmltodict
import json
data_xml = z.read('CORPCODE.xml').decode('utf-8')
data_odict = xmltodict.parse(data_xml)
data_dict = json.loads(json.dumps(data_odict))
data = data_dict.get('result', {}).get('list')

이를 위해 xmltodict 라이브러리를 사용했습니다. xmltodict로 XML을 파싱하고 나면 OrderedDict 형식의 결과를 얻을 수 있습니다. 이를 편의상 dict 형식으로 변환하기 위해 JSON dumps/loads 과정을 거쳤습니다.

data를 확인해 보면 공시대상 기업들의 고유번호와 주식 종목코드를 확인할 수 있습니다.

for item in data:
if item['corp_name'] in ["삼성전자", "SK하이닉스", "NAVER"]:
print(item)
output:
{'corp_code': '00126380', 'corp_name': '삼성전자', 'stock_code': '005930', 'modify_date': '20191209'}
{'corp_code': '00266961', 'corp_name': 'NAVER', 'stock_code': '035420', 'modify_date': '20200512'}
{'corp_code': '00164779', 'corp_name': 'SK하이닉스', 'stock_code': '000660', 'modify_date': '20200512'}

DART API는 대부분 공시대학회사의 고유번호를 요청 파라미터로 입력해줘야 하므로 이 데이터를 잘 관리하고 있는 것이 좋습니다.

기업개황 API는 대표자명, 사업자등록번호 등의 기업의 기본 정보를 제공합니다. JSON 포멧 요청 URL은 https://opendart.fss.or.kr/api/company.json이고 다음 두 파라미터를 입력해야 합니다.

  • crtfc_key: 발급받은 인증키
  • corp_code: 공시대상회사 고유번호

crtfc_key에 발급받은 인증키를 입력하고 corp_code에 삼성전자의 기업번호인 00126380를 입력하면 다음과 같은 결과를 얻을 수 있습니다.

res = requests.get('https://opendart.fss.or.kr/api/company.json?crtfc_key=2ff3dd09bbe0e8140dc1941c86ed08b77597b5e5&corp_code=00126380')
item = json.loads(res.text)
print(item)
output:
{'status': '000',
'message': '정상',
'corp_code': '00126380',
'corp_name': '삼성전자(주)',
'corp_name_eng': 'SAMSUNG ELECTRONICS CO,.LTD',
'stock_name': '삼성전자',
'stock_code': '005930',
'ceo_nm': '김기남, 김현석, 고동진',
'corp_cls': 'Y',
'jurir_no': '1301110006246',
'bizr_no': '1248100998',
'adres': '경기도 수원시 영통구 삼성로 129 (매탄동)',
'hm_url': 'www.sec.co.kr',
'ir_url': '',
'phn_no': '031-200-1114',
'fax_no': '031-200-7538',
'induty_code': '264',
'est_dt': '19690113',
'acc_mt': '12'}

그 외의 API들도 이와 비슷한 방법으로 사용할 수 있으므로 이렇게 마무리하겠습니다. 질문이 있으시면 메일 등으로 문의주시면 됩니다.

  • dart-fss 라이브러리를 사용하면 DART에서 쉽게 재무제표를 추출할 수 있습니다.
  • Insomnia는 간편하게 HTTP API를 사용해 볼 수 있는 클라이언트 입니다.

파이썬으로 크레온 플러스(Creon Plus) 자동 로그인하기

시스템 트레이딩에 관심 있는 분들이라면 주식투자 자동화에도 관심이 있을 것입니다. 주식투자 자동화의 첫걸음은 증권사 HTS API 자동 로그인 입니다. 증권사 HTS마다 그 방법이 다르며 이번 포스트에서는 대신증권의 크레온 플러스를 파이썬에서 자동으로 로그인하는 방법을 다루겠습니다.

크레온 다운로드센터에서 CREON HTS를 다운받습니다. CREON HTS에 CREON Plus가 포함되어 있습니다. 설치 과정은 생략하겠습니다.

크레온 HTS를 실행하면 다음과 같은 로그인 창이 나타날 것입니다. 직접 크레온 HTS에 로그인하여 API를 사용할 때는 로그인 창 상단에 creon plus 버튼을 누르고 ID, 비밀번호, 공인인증 비밀번호(조회전용이 아닌 경우)를 입력하여 로그인 버튼을 누르면 됩니다.

크레온 로그인 창

이제 파이썬에서 크레온 HTS에 자동으로 로그인하기 위한 준비를 합니다.

파이썬 32bit 환경은 아나콘다를 설치 후에 32bit 환경을 설치하는 것을 권장합니다. 그 방법은 이 포스트를 참고 바랍니다.

커맨드 창에서 다음 pip 명령으로 pywinauto와 pywin32를 설치합니다.

Terminal window
pip install pywinauto
pip install pywin32

크레온 API를 감싸는(wrapping) 클래스를 만들겠습니다.

import win32com.client
from pywinauto import application
class Creon:
def __init__(self):
self.obj_CpUtil_CpCybos = win32com.client.Dispatch('CpUtil.CpCybos')
def kill_client(self):
os.system('taskkill /IM coStarter* /F /T')
os.system('taskkill /IM CpStart* /F /T')
os.system('taskkill /IM DibServer* /F /T')
os.system('wmic process where "name like \'%coStarter%\'" call terminate')
os.system('wmic process where "name like \'%CpStart%\'" call terminate')
os.system('wmic process where "name like \'%DibServer%\'" call terminate')
def connect(self, id_, pwd, pwdcert):
if not self.connected():
self.disconnect()
self.kill_client()
app = application.Application()
app.start(
'C:\CREON\STARTER\coStarter.exe /prj:cp /id:{id} /pwd:{pwd} /pwdcert:{pwdcert} /autostart'.format(
id=id_, pwd=pwd, pwdcert=pwdcert
)
)
while not self.connected():
time.sleep(1)
return True
def connected(self):
b_connected = self.obj_CpUtil_CpCybos.IsConnect
if b_connected == 0:
return False
return True
def disconnect(self):
if self.connected():
self.obj_CpUtil_CpCybos.PlusDisconnect()

Creon 클래스는 자동 로그인 기능을 위해 다음 함수들을 가집니다.

  • kill_client: 실행중인 크레온 HTS 프로그램을 종료합니다.
  • connect: 크레온 ID, 비밀번호, 공인인증서 비밀번호를 입력받아서 크레온 HTS에 로그인 합니다.
  • connected: 크레온 HTS에 연결되었는지 확인합니다.
  • disconnect: 크레온 HTS와의 연결을 해제합니다.

여기서 connect 함수를 호출하면 기존 크레온을 종료하고 로그인을 시도하게 되어 있습니다.