대신증권 크레온(Creon) HTS 브리지 서버 만들기 (Flask 편)

2020-04-17 • rltrader대신증권, creon, 크레온, hts, 서버 • 9 min read

증권사 API 장단점 비교 포스트에서 키움증권, 대신증권 크레온, 이베스트투자증권의 HTS API를 비교했었습니다. HTS API를 Windows 환경에서만 사용할 수 있는 단점이 있는데, 이 이슈를 해결하기 위한 방법으로 브리지(Bridge) 서버를 두는 것을 다뤘었습니다.

이번 포스트에서는 대신증권 크레온(Creon) HTS API의 브리지 서버를 개발하는 방법에 대해서 다룹니다.

구조

브리지 서버 구조

HTS API를 사용하고자 하는 클라이언트(Client)가 RESTful 방식으로 브리지 서버에 데이터를 요청하고 받습니다.

브리지 서버는 받은 요청을 처리하기 위해서 크레온 HTS API를 호출합니다. 크레온 HTS API는 내부적으로 대신증권 서버와 통신하여 데이터를 전달할 것입니다.

브리지 서버는 심플한 웹 프레임워크인 Flask를 사용하여 구성했습니다.

브리지 서버 환경

기본 개발 환경

크레온 API 사용을 위한 환경 설치

  • conda install -c anaconda pywin32
  • pip install pywinauto

기능 명세

  • HTS 연결(Connection) 관리
  • 종목 상태 획득
  • 종목 특징 획득
  • 종목 차트 획득
  • 주식시장 차트 획득
  • 종목 공매도 추이 획득
  • ...

Creon API Wrapping Module 개발

creon 모듈은 크레온 API를 호출하여 원하는 데이터를 얻기 위한 파이썬 모듈입니다. 이 모듈은 Creon 클래스에 필요한 기능들을 구현합니다.

다음 코드조각은 이 모듈의 임포트 부분과 Creon 클래스 생성자를 보여줍니다.

import os
import time

import win32com.client
from pywinauto import application

import constants
import util


class Creon:
    def __init__(self):
        self.obj_CpUtil_CpCodeMgr = win32com.client.Dispatch('CpUtil.CpCodeMgr')
        self.obj_CpUtil_CpCybos = win32com.client.Dispatch('CpUtil.CpCybos')
        self.obj_CpSysDib_StockChart = win32com.client.Dispatch('CpSysDib.StockChart')
        self.obj_CpTrade_CpTdUtil = win32com.client.Dispatch('CpTrade.CpTdUtil')
        self.obj_CpSysDib_MarketEye = win32com.client.Dispatch('CpSysDib.MarketEye')
        self.obj_CpUtil_CpCybos = win32com.client.Dispatch('CpUtil.CpCybos')
        self.obj_CpSysDib_CpSvr7238 = win32com.client.Dispatch('CpSysDib.CpSvr7238')

크레온 API는 COM 방식으로 통신하기 때문에 win32com, pywinauto 모듈을 사용합니다.

종목 상태, 종목 특징, 종목 차트, 주식시장 차트, 종목 공매도 추이 등의 데이터를 획득하기 위해 관련된 크레온 COM 모듈들을 연결합니다. 각 크레온 COM 모듈들의 상세 정보는 이 페이지에서 확인할 수 있습니다.

다음 코드조각은 크레온 HTS 연결 관리 기능들을 보여줍니다.

    def connect(self, id_, pwd, pwdcert, trycnt=300):
        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
                )
            )

        cnt = 0
        while not self.connected():
            if cnt > trycnt:
                return False
            time.sleep(1)
            cnt += 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()
            return True
        return False

    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')

먼저 HTS 연결을 위해 connect() 함수를 호출하면 됩니다. 이 함수에 인자로 크레온 HTS ID, 비밀번호를 넣어줘야하고 필요한 경우 인증서 비밀번호를 입력할 수 있습니다.

연결까지는 수 분이 걸릴 수 있습니다. 이 함수는 연결이 완료되면 True를 리턴하고 오랫동안 연결이 완료되지 않으면 일정 시간 이후에 False를 리턴합니다.

connected() 함수는 크레온 HTS에 연결되었는지 여부를 반환합니다.

disconnect()는 HTS의 연결을 해제하는 함수입니다. 완전한 HTS 프로세스 종료를 위해서는 추가로 kill_client() 함수를 호출해야 합니다.

다음 코드조각은 크레온 API의 요청제한 횟수를 지키기 위한 함수입니다.

    def avoid_reqlimitwarning(self):
        remainTime = self.obj_CpUtil_CpCybos.LimitRequestRemainTime
        remainCount = self.obj_CpUtil_CpCybos.GetLimitRemainCount(1)  # 시세 제한
        if remainCount <= 3:
            time.sleep(remainTime / 1000)

연속된 크레온 API 호출 사이에서 이 함수를 호출하여 요청제한 경고 문구가 뜨는 것을 방지할 수 있습니다.

다음 코드조각은 코스피, 코스닥 등의 주식 시장의 종목코드들을 획득하는 함수를 보여줍니다.

    def get_stockcodes(self, code):
        if code == constants.MARKET_CODE_KOSPI:
            code = 1
        elif code == constants.MARKET_CODE_KOSDAQ:
            code = 2
        res = self.obj_CpUtil_CpCodeMgr.GetStockListByMarket(code)
        return res

여기서 인자 code는 주식 시장 코드로 코스피, 코스닥 코드는 각각 1, 2 입니다.

결과는 종목 코드 리스트로 받습니다. 결과 중에서 일반적인 종목코드는 A로 시작하고 ETN은 Q로 시작합니다.

종목의 상태를 확인할 수 있습니다. 관리 종목인지, 거래정지 중인지 등을 확인할 수 있습니다.

    def get_stockstatus(self, code):
        if not code.startswith('A'):
            code = 'A' + code
        return {
            'control': self.obj_CpUtil_CpCodeMgr.GetStockControlKind(code),
            'supervision': self.obj_CpUtil_CpCodeMgr.GetStockSupervisionKind(code),
            'status': self.obj_CpUtil_CpCodeMgr.GetStockStatusKind(code),
        }

control, supervision, status의 값을 다음과 같이 가질 수 있습니다.

  • control
    • 0: 정상
    • 1: 주의
    • 2: 경고
    • 3: 위험예고
    • 4: 위험
  • supervision
    • 0: 일반종목
    • 1: 관리
  • status
    • 0: 정상
    • 1: 거래정지
    • 2: 거래중단

세 값이 0이 아니면 투자시 유의해야 할 종목으로 볼 수 있습니다.

다음 코드조각은 주식 종목의 다양한 특징들을 가져오는 함수를 보여줍니다.

    def get_stockfeatures(self, code):
        if not code.startswith('A'):
            code = 'A' + code
        stock = {
            'name': self.obj_CpUtil_CpCodeMgr.CodeToName(code),
            'marginrate': self.obj_CpUtil_CpCodeMgr.GetStockMarginRate(code),
            'unit': self.obj_CpUtil_CpCodeMgr.GetStockMemeMin(code),
            'industry': self.obj_CpUtil_CpCodeMgr.GetStockIndustryCode(code),
            'market': self.obj_CpUtil_CpCodeMgr.GetStockMarketKind(code),
            'control': self.obj_CpUtil_CpCodeMgr.GetStockControlKind(code),
            'supervision': self.obj_CpUtil_CpCodeMgr.GetStockSupervisionKind(code),
            'status': self.obj_CpUtil_CpCodeMgr.GetStockStatusKind(code),
            'capital': self.obj_CpUtil_CpCodeMgr.GetStockCapital(code),
            'fiscalmonth': self.obj_CpUtil_CpCodeMgr.GetStockFiscalMonth(code),
            'groupcode': self.obj_CpUtil_CpCodeMgr.GetStockGroupCode(code),
            'kospi200kind': self.obj_CpUtil_CpCodeMgr.GetStockKospi200Kind(code),
            'section': self.obj_CpUtil_CpCodeMgr.GetStockSectionKind(code),
            'off': self.obj_CpUtil_CpCodeMgr.GetStockLacKind(code),
            'listeddate': self.obj_CpUtil_CpCodeMgr.GetStockListedDate(code),
            'maxprice': self.obj_CpUtil_CpCodeMgr.GetStockMaxPrice(code),
            'minprice': self.obj_CpUtil_CpCodeMgr.GetStockMinPrice(code),
            'ydopen': self.obj_CpUtil_CpCodeMgr.GetStockYdOpenPrice(code),
            'ydhigh': self.obj_CpUtil_CpCodeMgr.GetStockYdHighPrice(code),
            'ydlow': self.obj_CpUtil_CpCodeMgr.GetStockYdLowPrice(code),
            'ydclose': self.obj_CpUtil_CpCodeMgr.GetStockYdClosePrice(code),
            'creditenabled': self.obj_CpUtil_CpCodeMgr.IsStockCreditEnable(code),
            'parpricechangetype': self.obj_CpUtil_CpCodeMgr.GetStockParPriceChageType(code),
            'spac': self.obj_CpUtil_CpCodeMgr.IsSPAC(code),
            'biglisting': self.obj_CpUtil_CpCodeMgr.IsBigListingStock(code),
            'groupname': self.obj_CpUtil_CpCodeMgr.GetGroupName(code),
            'industryname': self.obj_CpUtil_CpCodeMgr.GetIndustryName(code),
            'membername': self.obj_CpUtil_CpCodeMgr.GetMemberName(code),
        }

        _fields = [67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 116, 118, 120, 123, 124, 125, 127, 156]
        _keys = ['PER', '시간외매수잔량', '시간외매도잔량', 'EPS', '자본금', '액면가', '배당률', '배당수익률', '부채비율', '유보율', '자기자본이익률', '매출액증가율', '경상이익증가율', '순이익증가율', '투자심리', 'VR', '5일회전율', '4일종가합', '9일종가합', '매출액', '경상이익', '당기순이익', 'BPS', '영업이익증가율', '영업이익', '매출액영업이익률', '매출액경상이익률', '이자보상비율', '분기BPS', '분기매출액증가율', '분기영업이액증가율', '분기경상이익증가율', '분기순이익증가율', '분기매출액', '분기영업이익', '분기경상이익', '분기당기순이익', '분개매출액영업이익률', '분기매출액경상이익률', '분기ROE', '분기이자보상비율', '분기유보율', '분기부채비율', '프로그램순매수', '당일외국인순매수', '당일기관순매수', 'SPS', 'CFPS', 'EBITDA', '공매도수량', '당일개인순매수']
        self.obj_CpSysDib_MarketEye.SetInputValue(0, _fields)
        self.obj_CpSysDib_MarketEye.SetInputValue(1, 'A'+code)
        self.obj_CpSysDib_MarketEye.BlockRequest()

        cnt_field = self.obj_CpSysDib_MarketEye.GetHeaderValue(0)
        if cnt_field > 0:
            for i in range(cnt_field):
                stock[_keys[i]] = self.obj_CpSysDib_MarketEye.GetDataValue(i, 0)
        return stock

여기서 주식 종목의 상태 뿐만 아니라 EPS, BPS 등의 기본적 자질들을 가져옵니다.

다음 코드조각은 주식 시장이나 종목의 차트데이터를 가져오는 함수를 보여줍니다.

    def get_chart(self, code, target='A', unit='D', n=None, date_from=None, date_to=None):
        _fields = []
        _keys = []
        if unit == 'm':
            _fields = [0, 1, 2, 3, 4, 5, 6, 8, 9, 37]
            _keys = ['date', 'time', 'open', 'high', 'low', 'close', 'diff', 'volume', 'price', 'diffsign']
        else:
            _fields = [0, 2, 3, 4, 5, 6, 8, 9, 37]
            _keys = ['date', 'open', 'high', 'low', 'close', 'diff', 'volume', 'price', 'diffsign']

        if date_to is None:
            date_to = util.get_str_today()

        self.obj_CpSysDib_StockChart.SetInputValue(0, target+code) # 주식코드: A, 업종코드: U
        if n is not None:
            self.obj_CpSysDib_StockChart.SetInputValue(1, ord('2'))  # 0: ?, 1: 기간, 2: 개수
            self.obj_CpSysDib_StockChart.SetInputValue(4, n)  # 요청 개수
        if date_from is not None or date_to is not None:
            if date_from is not None and date_to is not None:
                self.obj_CpSysDib_StockChart.SetInputValue(1, ord('1'))  # 0: ?, 1: 기간, 2: 개수
            if date_from is not None:
                self.obj_CpSysDib_StockChart.SetInputValue(3, date_from)  # 시작일
            if date_to is not None:
                self.obj_CpSysDib_StockChart.SetInputValue(2, date_to)  # 종료일
        self.obj_CpSysDib_StockChart.SetInputValue(5, _fields)  # 필드
        self.obj_CpSysDib_StockChart.SetInputValue(6, ord(unit))
        self.obj_CpSysDib_StockChart.SetInputValue(9, ord('1')) # 0: 무수정주가, 1: 수정주가

        def req(prev_result):
            self.obj_CpSysDib_StockChart.BlockRequest()

            status = self.obj_CpSysDib_StockChart.GetDibStatus()
            msg = self.obj_CpSysDib_StockChart.GetDibMsg1()
            if status != 0:
                return None

            cnt = self.obj_CpSysDib_StockChart.GetHeaderValue(3)
            list_item = []
            for i in range(cnt):
                dict_item = {k: self.obj_CpSysDib_StockChart.GetDataValue(j, cnt-1-i) for j, k in enumerate(_keys)}

                # type conversion
                dict_item['diffsign'] = chr(dict_item['diffsign'])
                for k in ['open', 'high', 'low', 'close', 'diff']:
                    dict_item[k] = float(dict_item[k])
                for k in ['volume', 'price']:
                    dict_item[k] = int(dict_item[k])

                # additional fields
                dict_item['diffratio'] = (dict_item['diff'] / (dict_item['close'] - dict_item['diff'])) * 100
                list_item.append(dict_item)
            return list_item

        # 연속조회 처리
        result = req([])
        while self.obj_CpSysDib_StockChart.Continue:
            self.avoid_reqlimitwarning()
            _list_item = req(result)
            if len(_list_item) > 0:
                result = _list_item + result
                if n is not None and n <= len(result):
                    break
            else:
                break
        return result

차트데이터는 확인하고자 하는 데이터 기간이 길면 연속조회를 수행해야 할 수 있습니다. 연속조회는 한 번의 요청에 가져오는 데이터 양이 제한되기 때문에 여러번의 같은 요청으로 데이터를 이어서 가져오는 방식입니다.

다음 코드조각은 종목의 공매도 추이 데이터를 가져오는 함수를 보여줍니다.

    def get_shortstockselling(self, code, n=None):
        """
        종목별공매도추이
        """
        if not self.connected():
            return None
        _fields = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
        _keys = ['date', 'close', 'diff', 'diffratio', 'volume', 'short_volume', 'short_ratio', 'short_amount', 'avg_price', 'avg_price_ratio']

        self.obj_CpSysDib_CpSvr7238.SetInputValue(0, 'A'+code) 

        def req(prev_result):
            self.obj_CpSysDib_CpSvr7238.BlockRequest()

            status = self.obj_CpSysDib_CpSvr7238.GetDibStatus()
            msg = self.obj_CpSysDib_CpSvr7238.GetDibMsg1()
            if status != 0:
                return None

            cnt = self.obj_CpSysDib_CpSvr7238.GetHeaderValue(0)
            list_item = []
            for i in range(cnt):
                dict_item = {k: self.obj_CpSysDib_CpSvr7238.GetDataValue(j, cnt-1-i) for j, k in enumerate(_keys)}
                list_item.append(dict_item)
            return list_item

        # 연속조회 처리
        result = req([])
        while self.obj_CpSysDib_CpSvr7238.Continue:
            self.avoid_reqlimitwarning()
            _list_item = req(result)
            if len(_list_item) > 0:
                result = _list_item + result
                if n is not None and n <= len(result):
                    break
            else:
                break
        return result

차트 데이터를 가져오는 함수와 마찬가지로 획득하고자 하는 데이터의 크기에 따라서 연속조회로 요청을 처리합니다.

브리지 서버 개발

이제 creon.py를 외부와 연결해 줄 브리지 서버 bridge.py의 소스코드를 살펴보겠습니다.

다음 코드조각은 브리지 서버에서 필요한 모듈 임포트 부분을 보여줍니다.

from flask import Flask, request, jsonify
from creon import Creon
import constants


app = Flask(__name__)
c = Creon()

여기서 브리지 서버는 Flask를 사용했습니다. 그리고 creon.pyCreon 클래스 객체를 준비합니다.

app은 Flask 서버의 WSGI(Web Server Gateway Interface) 입니다.

먼저 크레온 HTS를 연결하기 위한 인터페이스를 살펴보겠습니다.

@app.route('/connection', methods=['GET', 'POST', 'DELETE'])
def handle_connect():
    c = Creon()
    if request.method == 'GET':
        # check connection status
        return jsonify(c.connected())
    elif request.method == 'POST':
        # make connection
        data = request.get_json()
        _id = data['id']
        _pwd = data['pwd']
        _pwdcert = data['pwdcert']
        return jsonify(c.connect(_id, _pwd, _pwdcert))
    elif request.method == 'DELETE':
        # disconnect
        res = c.disconnect()
        c.kill_client()
        return jsonify(res)

/connection 인터페이스는 GET, POST, DELETE 방식을 지원합니다. GET으로 크레온 HTS와의 연결 상태를 확인하고 POST로 크레온 HTS와 새로운 연결을 생성하고 DELETE로 기존 연결을 끊습니다.

다음 코드조각은 주식 시장에 속한 종목 코드를 획득하는 인터페이스입니다.

@app.route('/stockcodes', methods=['GET'])
def handle_stockcodes():
    c = Creon()
    c.avoid_reqlimitwarning()
    market = request.args.get('market')
    if market == 'kospi':
        return jsonify(c.get_stockcodes(constants.MARKET_CODE_KOSPI))
    elif market == 'kosdaq':
        return jsonify(c.get_stockcodes(constants.MARKET_CODE_KOSDAQ))
    else:
        return '"market" should be one of "kospi" and "kosdaq".', 400

/stockcodes 인터페이스는 market인자로 kospi 또는 kosdaq를 입력받아 이 주식 시장에 포함된 종목코드를 리스트로 반환합니다. 이 때 Creon 객체로 요청 제한을 피하기 위해 avoid_reqlimitwarning() 함수를 호출하고 get_stockcodes() 함수로 종목코드를 획득합니다. 그러면 다음과 같이 종목코드 리스트를 JSON Array 형태로 반환합니다.

[
  "A000020",
  "A000040",
  "A000050",
  "A000060",
  "A000070",
  ...
]

다음 코드조각은 종목의 상태를 확인하는 인터페이스를 보여줍니다.

@app.route('/stockstatus', methods=['GET'])
def handle_stockstatus():
    c = Creon()
    c.avoid_reqlimitwarning()
    stockcode = request.args.get('code')
    if not stockcode:
        return '', 400
    status = c.get_stockstatus(stockcode)
    return jsonify(status)

/stockstatus 인터페이스는 code 인자로 종목코드를 입력으로 받아서 해당 종목의 상태를 JSON Object 형태로 반환합니다.

{
  "control": 0,
  "status": 0,
  "supervision": 0
}

다음 코드조각은 종목의 차트 데이터를 획득하는 인터페이스를 보여줍니다.

@app.route('/stockcandles', methods=['GET'])
def handle_stockcandles():
    c = Creon()
    c.avoid_reqlimitwarning()
    stockcode = request.args.get('code')
    n = request.args.get('n')
    date_from = request.args.get('date_from')
    date_to = request.args.get('date_to')
    if not (n or date_from):
        return 'Need to provide "n" or "date_from" argument.', 400
    stockcandles = c.get_chart(stockcode, target='A', unit='D', n=n, date_from=date_from, date_to=date_to)
    return jsonify(stockcandles)

/stockcandles 인터페이스는 인자로 code, n, date_from, date_to를 받아서 차트 데이터를 조회합니다. 여기서 code는 필수로 입력해야 하고 n 또는 date_from을 입력해야 합니다. n만 입력할 경우 가장 최근 n개의 차트 데이터를 조회하고 date_from을 입력할 경우 해당 날짜부터 가장 최근 날짜까지 데이터를 조회합니다. date_to를 옵션으로 입력하여 차트 데이터의 마지막 날짜를 지정할 수 있습니다.

Creon 클래스의 get_chart() 함수에 적절한 인자를 줘서 종목의 차트 데이터를 획득하고 반환합니다.

차트 데이터의 형태는 다음과 같습니다.

[
  {
    "close": 17300.0,
    "date": 20200323,
    "diff": 0.0,
    "diffratio": 0.0,
    "diffsign": "3",
    "high": 18100.0,
    "low": 16100.0,
    "open": 16400.0,
    "price": 62973000000,
    "volume": 3685846
  },
  {
    "close": 18650.0,
    "date": 20200324,
    "diff": 1350.0,
    "diffratio": 7.803468208092486,
    "diffsign": "2",
    "high": 18650.0,
    "low": 17500.0,
    "open": 17700.0,
    "price": 64865000000,
    "volume": 3570950
  },
  ...
]

다음은 주식 시장의 차트 데이터를 획득하는 인터페이스 입니다.

@app.route('/marketcandles', methods=['GET'])
def handle_marketcandles():
    c = Creon()
    c.avoid_reqlimitwarning()
    marketcode = request.args.get('code')
    n = request.args.get('n')
    date_from = request.args.get('date_from')
    date_to = request.args.get('date_to')
    if marketcode == 'kospi':
        marketcode = '001'
    elif marketcode == 'kosdaq':
        marketcode = '201'
    elif marketcode == 'kospi200':
        marketcode = '180'
    else:
        return [], 400
    if not (n or date_from):
        return '', 400
    marketcandles = c.get_chart(marketcode, target='U', unit='D', n=n, date_from=date_from, date_to=date_to)
    return jsonify(marketcandles)

/marketcandles 인터페이스는 codekospi 또는 kosdaq를 받아서 해당 지수의 차트 데이터를 조회합니다. 이외의 입력 인자는 /stockcandles 인터페이스와 같습니다.

획득한 차트 데이터를 다음과 같은 형태로 반환합니다.

[
  {
    "close": 1482.4599609375,
    "date": 20200323,
    "diff": -83.69000244140625,
    "diffratio": -5.3436774509669815,
    "diffsign": "0",
    "high": 1516.75,
    "low": 1458.4100341796875,
    "open": 1474.449951171875,
    "price": 9645271000000,
    "volume": 647528300
  },
  {
    "close": 1609.969970703125,
    "date": 20200324,
    "diff": 127.51000213623047,
    "diffratio": 8.601244204893801,
    "diffsign": "0",
    "high": 1609.969970703125,
    "low": 1508.6800537109375,
    "open": 1523.68994140625,
    "price": 10481447000000,
    "volume": 679288400
  },
  ...
]

다음 코드조각은 주식 종목의 다양한 자질들을 획득하는 인터페이스를 보여줍니다.

@app.route('/stockfeatures', methods=['GET'])
def handle_stockfeatures():
    c = Creon()
    c.avoid_reqlimitwarning()
    stockcode = request.args.get('code')
    if not stockcode:
        return '', 400
    stockfeatures = c.get_stockfeatures(stockcode)
    return jsonify(stockfeatures)

/stockfeatures 인터페이스는 종목 코드를 code 인자로 받아서 해당 종목의 다양한 자질들을 조회합니다.

이 인터페이스는 종목의 자질을 JSON Object 형태로 반환합니다.

{
  "4일종가합": 81950,
  "5일회전율": 0.0,
  "9일종가합": 177550,
  "BPS": 105140,
  "CFPS": 20910,
  "EBITDA": 9852219,
  "EPS": 0,
  "PER": 0.0,
  "SPS": 92175,
  "VR": 0.0,
  "biglisting": 0,
  "capital": 1,
  "control": 0,
  "creditenabled": 1,
  "fiscalmonth": 12,
  "groupcode": 0,
  "groupname": "",
  "industry": "017",
  "industryname": "",
  "kospi200kind": 7,
  "listeddate": 19890810,
  "marginrate": 30,
  "market": 1,
  "maxprice": 26300,
  "membername": "",
  "minprice": 14200,
  "name": "한국전력",
  "off": 0,
  "parpricechangetype": 0,
  "section": 1,
  "spac": 0,
  "status": 0,
  "supervision": 0,
  "unit": 1,
  "ydclose": 20250,
  "ydhigh": 20850,
  "ydlow": 20150,
  "ydopen": 20700,
  "경상이익": -3265838000000,
  "경상이익증가율": 0.0,
  "공매도수량": 979,
  "당기순이익": -2263535000000,
  "당일개인순매수": -39676,
  "당일기관순매수": 84261,
  "당일외국인순매수": -54633,
  "매출액": 59172890,
  "매출액경상이익률": -5.519999980926514,
  "매출액영업이익률": -2.1600000858306885,
  "매출액증가율": -2.4000000953674316,
  "배당률": 15.0,
  "배당수익률": 3.8299999237060547,
  "부채비율": 186.8300018310547,
  "분개매출액영업이익률": -2.1600000858306885,
  "분기BPS": 105140,
  "분기ROE": -3.4200000762939453,
  "분기경상이익": -3265838000000,
  "분기경상이익증가율": 0.0,
  "분기당기순이익": -2263535000000,
  "분기매출액": 59172890,
  "분기매출액경상이익률": -5.519999980926514,
  "분기매출액증가율": -2.4000000953674316,
  "분기부채비율": 186.8300018310547,
  "분기순이익증가율": 0.0,
  "분기영업이액증가율": 0.0,
  "분기영업이익": -1276521000000,
  "분기유보율": 2002.81005859375,
  "분기이자보상비율": 0.0,
  "순이익증가율": 0.0,
  "시간외매도잔량": 0,
  "시간외매수잔량": 4217,
  "액면가": 5000,
  "영업이익": -1276521000000,
  "영업이익증가율": 0.0,
  "유보율": 2002.81005859375,
  "이자보상비율": 0.0,
  "자기자본이익률": -3.4200000762939453,
  "자본금": 3209820,
  "투자심리": 0.0,
  "프로그램순매수": 108287
}

다음 코드조각은 공매도 추이를 획득하는 인터페이스 입니다.

@app.route('/short', methods=['GET'])
def handle_short():
    c = Creon()
    c.avoid_reqlimitwarning()
    stockcode = request.args.get('code')
    n = request.args.get('n')
    if not stockcode:
        return '', 400
    stockfeatures = c.get_shortstockselling(stockcode, n=n)
    return jsonify(stockfeatures)

/short 인터페이스는 종목 코드를 code 인자로 받아서 이 종목의 최근 공매도 현황을 조회합니다. n 인자로 획득할 데이터 크기를 지정하면 연속조회로 n개 이상이 될때까지 데이터를 추가로 획득합니다.

다음과 같은 형태로 공매도 추이 데이터를 반환합니다.

[
  {
    "avg_price": 21189,
    "avg_price_ratio": 11,
    "close": 21200,
    "date": 20200414,
    "diff": 750,
    "diffratio": 3.67,
    "short_amount": 2466,
    "short_ratio": 0.0305,
    "short_volume": 1164,
    "volume": 3816326
  },
  {
    "avg_price": 20459,
    "avg_price_ratio": -9,
    "close": 20450,
    "date": 20200413,
    "diff": 400,
    "diffratio": 2.0,
    "short_amount": 3028,
    "short_ratio": 0.0263,
    "short_volume": 1480,
    "volume": 5633244
  },
  ...
]

마지막으로 main에서 브리지 서버를 실행하도록 합니다.

if __name__ == "__main__":
    app.run()

브리지 서버 실행

크레온 HTS API는 32비트 파이썬 환경에서 관리자 권한으로 연결해야 안정적으로 사용할 수 있습니다.

다음과 같은 systrader_flask.bat 스크립트로 서버를 실행할 수 있습니다.

call C:\Users\%username%\Anaconda3x86\Scripts\activate.bat
set FLASK_ENV=development
python C:\Users\%username%\systrader\quantylab\systrader\creon\bridge_flask.py

여기서 파이썬 32비트 환경을 사용하기 위해 32비트 아나콘다를 활성화 했습니다. 그리고 개발을 용이하게 하기 위해 FLASK_ENVdevelopment로 설정했습니다. 이렇게 하면 브리지 서버의 코드를 수정할 때 자동으로 서버가 재시작되고 에러 발생 시 디버깅이 용이하도록 상세한 호출 스택을 볼 수 있습니다.

그리고 이 스크립트의 바로가기를 만든 다음 다음과 같이 관리자 권한을 부여합니다.

바로가기 관리자 권한

바로가기의 속성에 들어가서 바로 가기 탭의 고급 버튼을 누르면 관리자 권한으로 실행 옵션을 선택할 수 있습니다.