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

수정주가와 원주가의 차이: 금융 데이터 분석과 백테스트의 필수 기초

수정주가와 원주가 개념 시각화

과거 주가 차트를 열어보았을 때, 2018년 이전 삼성전자의 주가가 5만 원대로 표시되어 의아했던 적이 있으신가요? 당시 신문 기사에서는 1주에 250만 원이 훌쩍 넘는 대표적인 ‘황제주’로 불렸는데 말입니다.

금융 데이터 분석이나 퀀트 백테스트를 처음 시작할 때 가장 흔하게 마주치는 혼란 중 하나가 바로 원주가(수정 전 주가, Unadjusted / Raw Price)와 수정주가(Adjusted Stock Price)의 구분입니다. 겉보기에는 단순한 가격 표기 방식의 차이처럼 보이지만, 이를 제대로 이해하지 못하고 백테스트나 분석 모델에 원주가를 그대로 넣으면 하루아침에 -80% 폭락이 발생한 것으로 인식되거나 모델의 가중치가 통째로 왜곡되는 치명적인 오류가 발생합니다.

이번 글에서는 원주가와 수정주가의 개념, 자본 변동 이벤트별 보정 메커니즘, 그리고 데이터 분석 및 백테스트 실무에서 반드시 확인해야 할 주의점을 정리합니다.


1. 원주가(Unadjusted Price)란 무엇인가?

섹션 제목: “1. 원주가(Unadjusted Price)란 무엇인가?”

원주가(수정 전 주가)는 해당 거래일 당시 한국거래소(KRX) 등 시장에서 실제로 체결되고 거래장에 공식 기록되었던 액면 그대로의 가격(Nominal / Raw Price)을 뜻합니다.

  • 실제 거래 체결 증명: 매매 당일 투자자가 지불한 실제 금액과 계좌 잔고 증명의 기준입니다.
  • 역사적 시가총액 계산: 과거 특정 시점의 기업 규모를 측정할 때 당시의 실제 상장주식수와 원주가를 곱해 계산합니다. 시가총액원주가실제발행주식수
  • 호가 단위(Tick Size) 및 최소 주문 금액 파악: 당시 시장 참여자가 직면했던 실제 호가 단위와 진입 장벽을 복원할 때 필요합니다.

원주가의 한계: 시계열의 인위적 단절

섹션 제목: “원주가의 한계: 시계열의 인위적 단절”

원주가만으로 장기 시계열을 분석하면 심각한 문제가 발생합니다. 기업이 액면분할, 액면병합, 무상증자, 유상증자, 감자 등 자본 변동을 단행할 때마다 주가 시계열에 급격한 갭(Gap)과 단절이 발생하기 때문입니다.

예를 들어 주가가 250만 원이던 주식이 50:1 액면분할을 하면 다음 날 아침 주가는 5만 원으로 시작합니다. 회사의 본질 가치나 주주의 자산 총액은 변함이 없는데, 원주가 시계열만 보면 하루 만에 주가가 98% 폭락한 것처럼 보이게 됩니다.


2. 수정주가(Adjusted Stock Price)란 무엇인가?

섹션 제목: “2. 수정주가(Adjusted Stock Price)란 무엇인가?”

수정주가는 액면분할, 증자, 배당 등 주주 가치의 실질적 변화가 없는 자본 변동 이벤트로 인해 발생하는 주가의 불연속성을 제거하기 위해, 과거 주가를 현재 가치 기준에 맞춰 소급 조정한 가격입니다.

수정주가가 반드시 필요한 이유

섹션 제목: “수정주가가 반드시 필요한 이유”
  1. 연속적인 수익률 계산: 주가 수익률은 연속된 두 거래일의 가격 비율로 정의됩니다. 수정주가를 사용해야 자본 변동 이벤트 발생일에도 왜곡 없는 정상적인 일일 수익률()을 얻을 수 있습니다.
  2. 기술적 지표의 유효성 유지: 20일 이동평균선, 볼린저 밴드, RSI, MACD 등 대부분의 기술적 분석 지표는 과거 가격 데이터의 연속성을 전제로 합니다. 원주가를 사용하면 50:1 액면분할이 발생한 날 이후 한 달 동안 이동평균선이 완전히 무너집니다.
  3. 머신러닝·통계 모델의 정상성(Stationarity): 시계열 예측 모델에 가격 데이터를 입력할 때 불필요한 충격(Outlier)을 배제하고 연속적인 패턴을 학습시킬 수 있습니다.

수정 방식의 표준: 후방 수정(Backward Adjustment)

섹션 제목: “수정 방식의 표준: 후방 수정(Backward Adjustment)”

금융 데이터 플랫폼(네이버 증권, Yahoo Finance, 인베스팅닷컴 등)과 증권사 HTS는 대부분 후방 수정(Backward Adjustment)을 표준으로 사용합니다. 즉, 현재 시점의 주가를 그대로 두고, 과거의 주가들을 분할·증자 비율에 맞춰 하향 조정(할인)하는 방식입니다.

과거 250만 원이던 삼성전자 주가가 오늘날 5만 원대로 표시되는 이유가 바로 현재 유통되는 1주 가치에 맞춰 과거 주가를 50분의 1로 환산했기 때문입니다.


3. 시각 자료로 보는 원주가와 수정주가의 차이

섹션 제목: “3. 시각 자료로 보는 원주가와 수정주가의 차이”

실제 주식 시장에서 액면분할이 발생했을 때 원주가와 수정주가가 어떻게 다르게 나타나는지 아래 차트를 통해 한눈에 비교할 수 있습니다.

원주가 vs 수정주가 시계열 및 수익률 비교

상단 그래프를 보면, 거래일 50일 차에 1:5 액면분할이 발생했을 때 원주가(붉은 점선)는 하루 만에 25만 원 수준에서 5만 원 수준으로 수직 낙하합니다. 반면 수정주가(하늘색 실선)는 분할 이전의 가격을 모두 1/5로 소급 적용하여 매끄럽고 연속적인 상승 흐름을 온전히 보존합니다.

하단 일일 수익률 그래프를 보면 차이는 더 극명합니다. 원주가로 계산한 일일 수익률은 분할 당일 -80% 폭락이라는 치명적인 아티팩트(Artifact)를 만들어내며, 이는 백테스트 알고리즘을 즉시 망가뜨리는 원인이 됩니다.


4. 자본 변동 이벤트별 수정 메커니즘

섹션 제목: “4. 자본 변동 이벤트별 수정 메커니즘”

수정주가가 계산되는 대표적인 이벤트와 보정 원리는 다음과 같습니다.

  • 액면분할 (1:N): 주식 액면가를 쪼개 유통 주식 수를 늘립니다. 분할 비율 만큼 과거 주가를 모두 로 나눕니다.
  • 액면병합 (N:1): 여러 주식을 하나로 합쳐 주가를 높입니다. 과거 주가를 모두 배로 곱합니다.

회사의 잉여금을 자본금으로 전입하여 주주들에게 대가 없이 신주를 발행합니다. 예를 들어 1주당 1주의 신주를 배정(100% 무상증자)하면 권리락일 주가는 50% 수준으로 조정되므로, 권리락 이전의 과거 주가를 0.5배로 보정합니다.

신주를 발행해 자금을 조달할 때 통상 기존 주가보다 할인된 가격(할인율 10~30%)으로 발행합니다. 이때 발생하는 권리락 기준가와 직전 종가의 비율을 바탕으로 수정계수(Adjustment Factor)를 산출하여 과거 주가에 곱해줍니다.

배당락일에는 배당금 지급 가치만큼 주가가 이론적으로 하락합니다.

  • 가격 수정주가 (Price Return Adjusted): 배당락은 보정하지 않고 분할/증자만 보정합니다. 일반적인 주가 차트에서 흔히 보는 형태입니다.
  • 총수익 수정주가 (Total Return Adjusted): 배당금을 당일 종가로 전액 재투자했다고 가정한 수정주가입니다. ETF의 분배금 재투자 성과나 장기 복리 백테스트에서 사용됩니다.

5. 원주가 vs 수정주가 핵심 비교

섹션 제목: “5. 원주가 vs 수정주가 핵심 비교”
비교 항목원주가 (Unadjusted Price)수정주가 (Adjusted Price)
정의거래 당일 시장에서 실제 체결된 명목 가격자본 변동 이벤트를 소급 보정한 연속 가격
시계열 연속성단절 발생 (분할·증자 시 급격한 갭)완벽한 연속성 유지
수익률 계산자본 변동일 왜곡 발생 (사용 부적합)장기 시계열 및 연속 수익률 계산에 필수
기술적 지표 산출이동평균선, RSI 등 지표 왜곡정상적인 지표 계산 가능
실제 체결가 일치 여부일치 (당시 거래 영수증과 동일)불일치 (소급 할인된 가상 가격)
주요 활용처체결 확인, 잔고 평가, 역사적 시가총액 산출백테스트, 퀀트 모델링, 차트 분석, 머신러닝

6. 실무 데이터 분석에서 흔히 저지르는 3대 함정

섹션 제목: “6. 실무 데이터 분석에서 흔히 저지르는 3대 함정”

데이터 파이프라인을 구축하거나 퀀트 백테스트 코드를 작성할 때 자주 마주치는 실무 함정들을 짚어보겠습니다.

함정 1: 주가는 수정주가를 쓰면서, 거래량(Volume)은 원데이터를 쓰는 경우

섹션 제목: “함정 1: 주가는 수정주가를 쓰면서, 거래량(Volume)은 원데이터를 쓰는 경우”

가장 흔하면서도 알아채기 힘든 실수입니다. 50:1 액면분할이 일어나면 주가는 50분의 1로 내려가지만, 시장에서 거래되는 주식 수(거래량)는 대략 50배로 늘어납니다.

주가만 수정주가로 바꾸고 거래량을 그대로 두면, 분할 이전의 거래량은 극도로 적고 분할 이후의 거래량만 폭증한 것처럼 왜곡됩니다. 이 상태로 거래량 이동평균이나 OBV(On Balance Volume), 유동성 필터를 계산하면 과거 구간의 거래량 신호가 전부 무용지물이 됩니다.

해결책:

  • 거래량도 주가와 반대 비율로 보정한 **수정거래량(Adjusted Volume = 원거래량 분할비율)**을 사용합니다.
  • 또는 주식 수의 영향을 받지 않는 **거래대금(원주가거래량수정주가수정거래량)**을 유동성 지표의 기준으로 사용합니다.

함정 2: 과거 특정 시점의 주문 수량 계산 오류

섹션 제목: “함정 2: 과거 특정 시점의 주문 수량 계산 오류”

백테스트 시뮬레이션에서 2017년 1월에 보유 자산 1,000만 원으로 삼성전자를 매수하는 로직을 짰다고 가정해 보겠습니다. 만약 수정주가(당시 기준 약 36,000원)로 매수 가능 주식 수를 계산하면 약 277주를 매수한 것으로 계산됩니다.

하지만 2017년 당시 실제 삼성전자 원주가는 약 180만 원이었습니다. 실제로는 5주밖에 살 수 없었고 나머지 현금은 잔액으로 남아 있었어야 합니다. 고가 대형주를 다룰 때 수정주가만으로 매수 수량을 계산하면 소수점 단위 매매나 최소 투자 단위 왜곡이 생길 수 있습니다.

함정 3: 미래 자본 변동으로 과거 데이터가 갱신되는 특성 (Data Versioning)

섹션 제목: “함정 3: 미래 자본 변동으로 과거 데이터가 갱신되는 특성 (Data Versioning)”

오늘 어떤 종목이 무상증자나 액면분할을 실시하면, 오늘 기준으로 과거 10년 치 수정주가 데이터 전체가 새롭게 다시 계산(Rewrite)됩니다.

데이터베이스에 과거 수정주가를 한 번 적재해 두고 고정해 버리면(Frozen), 새로 들어오는 주가와 과거 주가 사이의 수정 비율이 어긋나게 됩니다. 반대로 데이터베이스를 관리할 때는 원주가 테이블과 자본변동(수정계수) 테이블을 분리하여 관리하는 것이 안전합니다.


7. Python 코드로 확인하는 수정주가 계산 예시

섹션 제목: “7. Python 코드로 확인하는 수정주가 계산 예시”

간단한 Pandas 코드로 액면분할 시 원주가와 수정주가의 수익률 차이를 확인할 수 있습니다.

import pandas as pd
import numpy as np
# 가상의 주가 데이터 생성 (5일 차에 1:2 액면분할 발생)
df = pd.DataFrame({
"date": pd.date_range("2026-10-01", periods=6),
"raw_close": [100000, 102000, 101000, 104000, 53000, 54000], # 5일차에 1:2 분할
"split_ratio": [1.0, 1.0, 1.0, 1.0, 2.0, 1.0] # 5일차에 2배 분할
})
# 1. 후방 수정계수(Cumulative Adjustment Factor) 계산
# 미래부터 역순으로 누적 곱을 계산
df["adj_factor"] = df["split_ratio"].iloc[::-1].cumprod().iloc[::-1]
# 2. 수정주가 계산: 원주가 / 수정계수 (최신 기준 통일)
# (5일차 이전 과거 주가를 2로 나누어 현재 수준으로 맞춤)
df["adj_close"] = df["raw_close"] / (df["adj_factor"] / df["adj_factor"].iloc[-1])
# 3. 일일 수익률 계산 비교
df["raw_return"] = df["raw_close"].pct_change()
df["adj_return"] = df["adj_close"].pct_change()
print(df[["date", "raw_close", "adj_close", "raw_return", "adj_return"]])
date raw_close adj_close raw_return adj_return
0 2026-10-01 100000 50000.0 NaN NaN
1 2026-10-02 102000 51000.0 0.020000 0.020000
2 2026-10-03 101000 50500.0 -0.009804 -0.009804
3 2026-10-04 104000 52000.0 0.029703 0.029703
4 2026-10-05 53000 53000.0 -0.490385 0.019231 <-- 원주가 -49%, 수정주가 +1.92%
5 2026-10-06 54000 54000.0 0.018868 0.018868

4일 차에서 5일 차로 넘어갈 때, 원주가는 -49% 손실로 기록되지만 수정주가는 정상적인 +1.92% 상승으로 올바르게 반영됩니다.


8. 독자를 위한 실전 체크포인트

섹션 제목: “8. 독자를 위한 실전 체크포인트”

금융 데이터 분석이나 백테스트 파이프라인을 운영할 때 아래 항목들을 반드시 체크해 보세요.

  • 전략 시뮬레이션 및 백테스트: 가격 지표와 수익률 계산에 수정주가를 사용하고 있는가?
  • 거래량 지표 계산: OBV, MFI 등 거래량 기반 지표 계산 시 수정거래량 또는 거래대금을 적용했는가?
  • 실제 포트폴리오 회계 및 주문 집행: 과거 체결 금액, 잔고 증명, 세금 계산 시 원주가와 실제 유통 주식수를 기준으로 검증했는가?
  • 데이터베이스 보관 구조: 원주가(Raw Price)와 자본변동 수정계수(Adjustment Factor)를 분리 보관하여 과거 데이터의 재계산 유연성을 확보했는가?

데이터의 기본이 되는 가격 체계를 정확히 이해하고 정제하는 것이야말로 신뢰할 수 있는 퀀트 전략의 가장 첫 번째 출발점입니다.

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

업비트 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를 사용해 볼 수 있는 클라이언트 입니다.