본문 바로가기
Algorithm Trading/ComDon 프로그램 개발이야기

7. 이베스트투자증권 OPEN API

by 컴돈AI 2024. 1. 22.

목차

    이베스트투자증권 OPEN API

    • 공식 홈페이지
    • 윈도우 COM, OCX, DLL 기반의 API를 제공하는 다른 증권사와 달리 이베스트투자증권이나 한국투자증권은 REST API를 제공합니다. 따라서 윈도우 환경이 아니더라도 개발할 수 있고, 보다 쉽게 진행할 수 있습니다.
      • 이베스트투자증권은 최근 2024년 1월 15일(월) OPEN API 내 조건검색 서비스를 추가하였습니다. 조건검색이 필요하기때문에 이베스트투자증권 OPEN API를 이용하기로 했습니다.
    • 해당 내용을 참고하여 손쉽게 OPEN API 신청할 수 있습니다. (APP KEY와 SECRET KEY 발급)
    • 초당 전송 건수를 확인하여 API 호출을 해야합니다.

    접근토큰(Access Token) 발급

    • 본인 인증하는 확인 절차로, 여기서 발급한 접근토큰을 이용하여 OPEN API를 사용합니다.
    • 한번 발급받은 접근토큰은 익일 07시까지 유효합니다. 만료되면 다시 동일한 방법으로 접근 토큰을 재발급하여 사용하면 됩니다. 
      • 즉, 접근토큰은 매일 변경됩니다.
    • REST API (header와 parameter)에 필요한 정보를 입력한 뒤 Method 방식으로 request 해줍니다.
      • 초당 전송 건수 제한
        • 제한 없음 
      • 기본정보
      • header / parameter
        • header = {"content-type":"application/x-www-form-urlencoded"}
          param = {"grant_type":"client_credentials", "appkey":APP_KEY, "appsecretkey":APP_SECRET,"scope":"oob"}
          • grant_type과 scope는 고정값입니다.
      • 전체 코드
        • import requests
          import json
          
          APP_KEY = ""
          APP_SECRET = ""
          
          BASE_URL = "https://openapi.ebestsec.co.kr:8080"
          PATH = "oauth2/token"
          URL = f"{BASE_URL}/{PATH}"
          
          header = {"content-type":"application/x-www-form-urlencoded"}
          param = {"grant_type":"client_credentials", "appkey":APP_KEY, "appsecretkey":APP_SECRET,"scope":"oob"}
          
          request = requests.post(URL, headers=header, params=param)
          ACCESS_TOKEN = request.json()["access_token"]

    실시간으로 조건검색에 있는 종목 가져오기(+ 조건검색에 있는 종목 가져오기)

    • 실시간으로 조건검색을 사용하기 위해서는 우선 조건검색을 생성한 뒤에 해당 조건검색을 서버에 저장을 해주어야 합니다.
      • 우선 테스트용으로 아주 간단하게 거래대금 상위 50 종목을 추려내는 조건검색을 만들도록 하겠습니다.
      • 좌측 상단에 전략관리 클릭 후 해당 전략을 PC에서 서버로 저장해 줍니다.
    • 조건검색이 서버에 저장이 되었다면 해당 조건검색의 실시간 키값을 알아야지 웹소켓 연결이 가능합니다. 우선 실시간 키 값을 알아내는 작업을 진행하도록 하겠습니다. 실시간 키를 알아내기 전에 해당 조건검색의 query_index를 알아야 합니다. (즉, 조건검색 -> query_index -> 실시간 키(sAlertNum) -> Websocket 연결)
      • 조건검색 -> query_index
        • 초당 전송 건수 제한
          • 1
        • 기본정보
        • header / body
          • header = {"content-type": "application/json; charset=utf-8",
                "authorization": f"Bearer {ACCESS_TOKEN}",
                "tr_cd":"t1866",
                "tr_cont":"N",
                "tr_cont_key":"",
                }
            
            body = {
                "t1866InBlock" : 
                {    
                    "user_id" : "본인 id",    
                    "gb" : "0",    
                    "group_name" : "",    
                    "cont" : "",    
                    "cont_key" : ""  
                }
            }
        • 전체코드
          • BASE_URL = "https://openapi.ebestsec.co.kr:8080"
            PATH = "stock/item-search"
            URL = f"{BASE_URL}/{PATH}"
            
            header = {"content-type": "application/json; charset=utf-8",
                "authorization": f"Bearer {ACCESS_TOKEN}",
                "tr_cd":"t1866",
                "tr_cont":"N",
                "tr_cont_key":"",
                }
            
            body = {
                "t1866InBlock" : 
                {    
                    "user_id" : "본인id입력",    
                    "gb" : "0",    
                    "group_name" : "",    
                    "cont" : "",    
                    "cont_key" : ""  
                }
            }
            
            request = requests.post(URL, headers=header, json=body)
            print(request.json())
             
        • 실행결과
          • {'t1866OutBlock': {'result_count': 1, 'cont': '', 'contkey': ''}, 't1866OutBlock1': [{'query_index': '본인id 0000', 'group_name': '나의전략', 'query_name': '거래대금상위'}], 'rsp_cd': '00000', 'rsp_msg': '조회성 
            공'}
          • query_index가 "본인 id 0000" 인 것을 확인할 수 있습니다. (참고로 다른 조건검색이 더 있을 경우 "본인 id 0001" 이런 식으로 계속해서 진행되어 나갑니다.)
      • query_index -> 실시간 키(sAlertNum)
        • 초당 전송 건수 제한
          • 1
        • 기본정보
        • header / body
          • header = {"content-type": "application/json; charset=utf-8",
                "authorization": f"Bearer {ACCESS_TOKEN}",
                "tr_cd":"t1860",
                "tr_cont":"N",
                "tr_cont_key":"",
                }
            
            body = {
                "t1860InBlock" : 
                {    
                    "sSysUserFlag" : "U",    
                    "sFlag" : "E",    
                    "sAlertNum" : "",    
                    "query_index" : "본인id 0000",    
                }
            }
        • 전체 코드
          • BASE_URL = "https://openapi.ebestsec.co.kr:8080"
            PATH = "stock/item-search"
            URL = f"{BASE_URL}/{PATH}"
            
            header = {"content-type": "application/json; charset=utf-8",
                "authorization": f"Bearer {ACCESS_TOKEN}",
                "tr_cd":"t1860",
                "tr_cont":"N",
                "tr_cont_key":"",
                }
            
            body = {
                "t1860InBlock" : 
                {    
                    "sSysUserFlag" : "U",    
                    "sFlag" : "E",    
                    "sAlertNum" : "",    
                    "query_index" : "본인id 0000",    
                }
            }
            
            request = requests.post(URL, headers=header, json=body)
            print(request.json())
            sAlertNum=request.json()['t1860OutBlock']['sAlertNum']
        • 실행결과
          • {
                "t1860OutBlock": {
                    "sSysUserFlag": "U",
                    "sFlag": "E",
                    "sResultFlag": "S",
                    "sTime": "172249",
                    "sAlertNum": "~~~~",
                    "Msg": "정상처리 되었습니다."
                },
                "rsp_cd": "00000",
                "rsp_msg": "조회 완료"
            }
             
          • sAlertNum값을 이용하여 웹소켓을 통한 실시간 등록을 진행할 것입니다.
      • (참고) Websocket 방식이 아닌 REST API 방식으로 해당 조건검색 종목을 호출해 오기 (단, 이렇게 진행할 경우 1초에 1번 API 호출 제한으로 인하여 완벽하게 실시간으로 조건검색 종목들을 받아올 수 없습니다.)
        • 초당 전송 건수 제한
          • 1
        • 기본정보
        • header / body
          • header = {"content-type": "application/json; charset=utf-8",
                "authorization": f"Bearer {ACCESS_TOKEN}",
                "tr_cd":"t1859",
                "tr_cont":"N",
                "tr_cont_key":"",
                }
            
            body = {
                "t1859InBlock" : 
                {     
                    "query_index" : "본인id 0000",    
                }
            }
             
        • 전체 코드
          • BASE_URL = "https://openapi.ebestsec.co.kr:8080"
            PATH = "stock/item-search"
            URL = f"{BASE_URL}/{PATH}"
            
            header = {"content-type": "application/json; charset=utf-8",
                "authorization": f"Bearer {ACCESS_TOKEN}",
                "tr_cd":"t1859",
                "tr_cont":"N",
                "tr_cont_key":"",
                }
            
            body = {
                "t1859InBlock" : 
                {     
                    "query_index" : "본인id 0000",    
                }
            }
            
            request = requests.post(URL, headers=header, json=body)
            print(request.json())
        • 실행 결과
          • {
                "t1859OutBlock": {
                    "result_count": 225,
                    "result_time": "171729",
                    "text": ""
                },
                "t1859OutBlock1": [
                    {
                        "shcode": "000250",
                        "hname": "삼천당제약",
                        "price": 68300,
                        "sign": "2",
                        "change": 1200,
                        "diff": "1.79",
                        "volume": 241418
                    },
                    ~중 략 ~
            	{
                        "shcode": "950140",
                        "hname": "잉글우드랩",
                        "price": 13110,
                        "sign": "2",
                        "change": 300,
                        "diff": "2.34",
                        "volume": 134628
                    }
                ],
                "rsp_cd": "00000",
                "rsp_msg": ""
            }
    • 이제 위 과정을 통해 얻어온 실시간 키(sAlertNum)를 이용하여 실시간으로 조건검색을 받아올 수 있도록 Websocket 등록하도록 하겠습니다.
      • 초당 전송 건수 제한
        • 없음(WEBSOCKET)
      • 기본정보
      • header / body
        • header = {"token": ACCESS_TOKEN,"tr_type": "3"}
          body = {"tr_cd": "AFR","tr_key": sAlertNum}
           
          • 실시간 키는 t1860 서버저장조건 실시간 검색에서 얻은 실시간 키(sAlertNum)입니다
      • 전체 코드
        • import websockets
          import asyncio
          
          BASE_URL = "wss://openapi.ebestsec.co.kr:9443" 
          PATH = "websocket"
          URL = f"{BASE_URL}/{PATH}"
          
          header = {"token": ACCESS_TOKEN,"tr_type": "3"}
          body = {"tr_cd": "AFR","tr_key": sAlertNum}
          
          async def connect():
          # 웹 소켓에 접속을 합니다.
              async with websockets.connect(URL) as websocket:
                  data_to_send = json.dumps({"header": header, "body": body}) # json -> str로 변경
                  await websocket.send(data_to_send)
          
                  while True:
                      data = await websocket.recv()
                      print(data)
          
          asyncio.run(connect())
      • 실행결과
        • {"header":{"tr_type":"3","tr_cd":"AFR","rsp_cd":"00000","rsp_msg":"정상처리되었습니다"},"body":null}
          {"header":{"tr_cd":"AFR","tr_key":"실시간키"},"body":{"gsJobFlag":"O","gsVolume":"18020","gsPrice":"7990","gsSign":"5","gshname":"케이엔더블유","gsChange":"-170","gsChgRate":"-2.08","gsCode":"105330"}}
          {"header":{"tr_cd":"AFR","tr_key":"실시간키"},"body":{"gsJobFlag":"O","gsVolume":"5958138","gsPrice":"7690","gsSign":"5","gshname":"린드먼아시아","gsChange":"-70","gsChgRate":"-1.03","gsCode":"277070"}}
          {"header":{"tr_cd":"AFR","tr_key":"실시간키"},"body":{"gsJobFlag":"O","gsVolume":"192878","gsPrice":"7400","gsSign":"5","gshname":"풍원정밀","gsChange":"-10","gsChgRate":"-0.13","gsCode":"371950"}}
          ...
          • 위와 같이 한번 정상처리되었습니다. 메시지가 뜨고 나서 이제 조건검색에 진입/재진입/이탈할 때마다 해당 정보가 print 되게 됩니다.
            • 진입(신규) : N  / 재진입 : R / 이탈 : O
        • 여기서 주의할 점은 이제 웹소켓을 이용한 조건검색(AFR)은 이미 조건에 해당하는 종목들은 검색이 되지 않고 추가로 진입되거나 재진입 또는 이탈되는 종목만 메시지를 통해서 알려주게 됩니다. 만약 현재 이미 조건에 해당하는 종목을 알고 싶다면 위에서 작성한 [(참고) Websocket 방식이 아닌 REST API 방식으로 해당 조건검색 종목을 호출해 오기(t1859)]로 호출해주어야 합니다.
          • 정확히 하기 위해서는 t1859로 한번 현재 조건검색에 선발된 종목들을 먼저 추려낸 뒤에 AFR을 웹소켓을 통해서 등록해 주면 됩니다.

    지정가 / 시장가 매수 / 매도하기

    • 지정가 / 시장가 설정은 body 부분에서 OrdprcPtnCode 부분을 수정해 주면 됩니다. 마찬가지로 매수 / 매도의 경우 BnsTpCode 부분을 설정해 주면 됩니다. (단, 지정가의 경우 반드시 OrdPrc(주문가)를 0으로 설정해주어야 합니다.)
      • 초당 전송 건수 제한
        • 10
      • 기본 정보
      • header / body
        • header = {"content-type": "application/json; charset=utf-8",
              "authorization": f"Bearer {ACCESS_TOKEN}",
              "tr_cd":"CSPAT00601",
              "tr_cont":"N",
              "tr_cont_key":"",
              }
          
          body = {
            "CSPAT00601InBlock1" : {
              "IsuNo" : "005930", # 종목코드 모의투자의경우 A005930 으로 넣어주기
              "OrdQty" : 2, # 수량
              "OrdPrc" : 60000.0, # 가격 / 시장가일 경우 0으로 반드시 입력
              "BnsTpCode" : "2", # 1 : 매도 / 2 : 매수
              "OrdprcPtnCode" : "00", # 00 : 지정가 / 03 : 시장가
              "MgntrnCode" : "000",
              "LoanDt" : "",
              "OrdCndiTpCode" : "0"
            }
          }
      • 전체 코드
        • BASE_URL = "https://openapi.ebestsec.co.kr:8080"
          PATH = "stock/order"
          URL = f"{BASE_URL}/{PATH}"
          
          header = {"content-type": "application/json; charset=utf-8",
              "authorization": f"Bearer {ACCESS_TOKEN}",
              "tr_cd":"CSPAT00601",
              "tr_cont":"N",
              "tr_cont_key":"",
              }
          
          body = {
            "CSPAT00601InBlock1" : {
              "IsuNo" : "005930", # 종목코드
              "OrdQty" : 2, # 수량
              "OrdPrc" : 60000.0, # 가격 / 시장가일 경우 0으로 반드시 입력
              "BnsTpCode" : "2", # 1 : 매도 / 2 : 매수
              "OrdprcPtnCode" : "00", # 00 : 지정가 / 03 : 시장가
              "MgntrnCode" : "000",
              "LoanDt" : "",
              "OrdCndiTpCode" : "0"
            }
          }
          
          request = requests.post(URL, headers=header, json=body)
          print(request.json())
          • 주문번호 : request.json()[' CSPAT00601OutBlock2']['OrdNo']

    주문 취소하기

      • 분할 매수를 위해 현재가보다 아래 가격에 주문을 넣어놓았습니다. 근데 만약 분할 매수 가격보다 목표가에 먼저 도달하게 된다면 해당 분할매수 주문은 취소해주어야 합니다. 
      • 주문 취소하기
        • 초당 전송 건수 제한
          • 3
        • 기본정보
        • header / body
          • header = {"content-type": "application/json; charset=utf-8",
                "authorization": f"Bearer {ACCESS_TOKEN}",
                "tr_cd":"CSPAT00801",
                "tr_cont":"N",
                "tr_cont_key":"",
                }
            
            body = {
                "CSPAT00801InBlock1" : 
                {    
                    "OrgOrdNo" : 3106, # 주문번호
                    "IsuNo" : "005930",    
                    "OrdQty" : 1,
                }
            }
            • OrdOrdNo 은 주문이나 정정 주문에서 결과에 대해 아래 방법으로 가져올 수 있습니다. 
              • 주문 : request.json()[' CSPAT00601OutBlock2']['OrdNo']
              • 정정 : request.json()[' CSPAT00701OutBlock2']['OrdNo']
        • 전체 코드
          • BASE_URL = "https://openapi.ebestsec.co.kr:8080"
            PATH = "stock/order"
            URL = f"{BASE_URL}/{PATH}"
            
            header = {"content-type": "application/json; charset=utf-8",
                "authorization": f"Bearer {ACCESS_TOKEN}",
                "tr_cd":"CSPAT00801",
                "tr_cont":"N",
                "tr_cont_key":"",
                }
            
            body = {
                "CSPAT00801InBlock1" : 
                {    
                    "OrgOrdNo" : 3106,
                    "IsuNo" : "005930",    
                    "OrdQty" : 1,
                }
            }
            
            request = requests.post(URL, headers=header, json=body)
            print(request.json())

    주문 정정하기

      • 가격이 상승하면 팔 가격인 목표가와 가격이 하락하면 추가로 매수할 추가매수가격이 있습니다. 이때 만약 추가매수가격에 먼저 도달하게 되면은 다시 평균단가를 계산해서 목표가를 다시 지정한 뒤에 주문을 정정해주어야 합니다.
      • 주문 정정하기
        • 초당 전송 건수 제한
          • 3
        • 기본정보
        • header / body
          • header = {"content-type": "application/json; charset=utf-8",
                "authorization": f"Bearer {ACCESS_TOKEN}",
                "tr_cd":"CSPAT00701",
                "tr_cont":"N",
                "tr_cont_key":"",
                }
            
            body = {
                "CSPAT00701InBlock1" : 
                {    
                    "OrgOrdNo" : 3076,
                    "IsuNo" : "005930",    
                    "OrdQty" : 1,    
                    "OrdPrc" : 74300,
                    "BnsTpCode" : "2" , # 1 : 매도 / 2 : 매수 
                    "OrdprcPtnCode" : "00", # 00 : 지정가 / 03 : 시장가
                    "OrdCndiTpCode" : "0" 
                }
            }
            • 대부분 주문하기와 비슷합니다. 단 OrgOrdNo을 지정해 줘야지 기존 주문을 변경할 수 있습니다.
            • OrdOrdNo 은 주문이나 정정 주문에서 결과에 대해 아래 방법으로 가져올 수 있습니다. 
              • 주문 : request.json()[' CSPAT00601OutBlock2']['OrdNo']
              • 정정 : request.json()[' CSPAT00701OutBlock2']['OrdNo']
        • 전체 코드
          • BASE_URL = "https://openapi.ebestsec.co.kr:8080"
            PATH = "stock/order"
            URL = f"{BASE_URL}/{PATH}"
            
            header = {"content-type": "application/json; charset=utf-8",
                "authorization": f"Bearer {ACCESS_TOKEN}",
                "tr_cd":"CSPAT00701",
                "tr_cont":"N",
                "tr_cont_key":"",
                }
            
            body = {
                "CSPAT00701InBlock1" : 
                {    
                    "OrgOrdNo" : 3076,
                    "IsuNo" : "005930",    
                    "OrdQty" : 1,    
                    "OrdPrc" : 74300,
                    "BnsTpCode" : "2" , # 1 : 매도 / 2 : 매수 
                    "OrdprcPtnCode" : "00", # 00 : 지정가 / 03 : 시장가
                    "OrdCndiTpCode" : "0" 
                }
            }
            
            request = requests.post(URL, headers=header, json=body)
            print(request.json())

    비동기코드

    • 비동기코드 필요한 이유
      • API의 초당 전송 건수 제한으로 인하여 비동기적으로 코드를 짜야하는가에 대한 생각이 들 수 있습니다. 그래서 직접 시간을 비교해 본 결과 다음과 같은 결과를 얻을 수 있었습니다. (현물주문으로 테스트를 진행했습니다. 초당 전송 건수 10건)
        • 동기적으로 코드를 작성하고 시간을 측정한 결과 한 주문을 처리하는데 약 0.05초 / 전체 주문을 처리하는데 약 0.5초가 소요되었습니다. 
        • 비동기적으로 코드를 작성하고 시간을 측정한 결과 한 주문을 처리하는데 약 0.07초 / 전체 주문을 처리하는데 약 0.1초가 소요되었습니다.
        • 비교를 해본 결과 API 초당 전송 건수 제한이 있다고 하더라도 5배 정도의 차이가 발생하였습니다. 
      • 시간만 놓고 봤을 때 단순히 API 호출만 한다고 하면은 그렇게까지 영향을 미칠 정도는 아닙니다. 하지만 이제 실시간으로 조건검색의 결과를 받아오는 과정(Websocket)의 경우 비동기적으로 계속해서 확인하는 작업이 필요합니다. 따라서 단순히 다른 코드들을 동기적으로 작성하게 되면, 해당 주문이 처리되는 동안 실시간으로 조건검색의 결과를 받아오는 과정이 멈추게 될 수도 있습니다. 
      • 이러한 이유들로 대부분의 코드를 비동기 코드로 작성해주어야 합니다.
    • 비동기 시장가 매수 코드
      • 아래 코드에서 URL, header, body 부분을 수정하면 손쉽게 사용이 가능합니다. 
      • import aiohttp
        import asyncio
        
        async def fetch(session, URL, header, body):
            async with session.post(URL, headers=header, json=body) as response:
                response_json = await response.json()  # JSON 응답을 받습니다.
                return response_json
                
        async def main():
            BASE_URL = "https://openapi.ebestsec.co.kr:8080"
            PATH = "stock/order"
            URL = f"{BASE_URL}/{PATH}"
        
            header = {"content-type": "application/json; charset=utf-8",
                "authorization": f"Bearer {ACCESS_TOKEN}",
                "tr_cd":"CSPAT00601",
                "tr_cont":"N",
                "tr_cont_key":"",
                }
        
            body = {
                "CSPAT00601InBlock1" : 
                {    
                    "IsuNo" : "005930",    
                    "OrdQty" : 1,    
                    "OrdPrc" : 0, # 시장가일 경우 0 으로 입력해줘야합니다.
                    "BnsTpCode" : "2" , # 1 : 매도 / 2 : 매수 
                    "OrdprcPtnCode" : "03", # 00 : 지정가 / 03 : 시장가
                    "MgntrnCode":"000",
                    "LoanDt" : "",
                    "OrdCndiTpCode" : "0" 
                }
            }
        
            async with aiohttp.ClientSession() as session:
                response = await fetch(session, URL, header, body)
                print(response)
        
        # 이벤트 루프 실행
        result = asyncio.run(main())
        print(result)