본문 바로가기
AyoProject/Ayotera-Trade

[AT][Error] 키움증권 OpenAPI 호출 제한

by 청양호박이 2021. 2. 18.

이번에는 현재 키움증권의 OpenAPI의 호출에 대한 제한에 대해서 알아보고자 합니다. 키움증권에서는 OpenAPI에 대한 과도한 Transaction의 방지를 위해서 아래와 같이 KOA Studio에 조회제한에 대한 제약사항이 명기되어 있습니다.

 

  • 주요1 - 1초당 5회로 제한
  • 주요2 - 분당, 시간당 유동적 조회제한

 

[조회처리(조회요청)]
OpenAPI가 제공하는 데이터중에서 원하는 데이터를 서버에 요청해서 가져오는 것을 말하는데 TR(Transaction)단위로 처리됩니다.
TR이란 서버와 데이터를 주고받을때 정의한 약속된 규약이며 입력부분(Input)과 데이터를 수신하는 출력부분(Output)을 가지고 있습니다.
입력부분은 요청하는 데이터에 따라 입력갯수(입력항목)가 달라지며 출력부분은 보통 데이터갯수(출력항목)가 여러개로 구성됩니다.

출력부분은 출력항목이 한번씩만 전달되는 싱글데이터와 복수로 전달되는 멀티데이터가 있고 TR에 따라 싱글데이터(또는 멀티데이터)만
있거나 둘다 있는 경우도 있습니다.
OpenAPI가 제공하는 TR은 KOA Studio의 TR목록 탭에서 찾아볼 수 있고 각 TR별로 조회도 가능합니다.
OPT10070 : 당일주요거래원요청 - 싱글데이터
OPT10081 : 주식일봉차트조회요청 - 싱글 + 멀티데이터

[계좌비밀번호 설정]
잔고나 주문가능금액,수량등 계좌관련 조회나 주문전에 미리 계좌비밀번호를 설정해야 오류 알림창과 -301 오류코드없이 사용하실수 있습니다.
계좌비밀번호 설정은 계좌비밀번호 입력창에서만 가능하며 이 입력창은 메뉴나 함수로 출력하실수 있습니다.
메뉴이용 - 로그인후 트레이 메뉴(모니터 오른쪽 하단)에서 "계좌비밀번호 저장"선택
함수이용 - 로그인후 OpenAPI.KOA_Functions(_T("ShowAccountWindow"), _T(""))호출

[조회제한]
OpenAPI 조회는 1초당 5회로 제한되며 복수종목 조회와 조건검색 조회 횟수가 합산됩니다.
가령 1초 동안 시세조회2회 관심종목 1회 조건검색 2회 순서로 조회를 했다면 모두 합쳐서 5회이므로 모두 조회성공하겠지만 
조건검색을 3회 조회하면 맨 마지막 조건검색 조회는 실패하게 됩니다.

[조회제한 강화]
기존 1초당 5회 조회제한외에 분당, 시간당 유동적 조회제한이 2017년 4월 6일 17:00이후 반영되었습니다.
(OpenAPI 게시판 조회제한 관련 공지내용을 참고하세요.)

그래서, 대규모 Transaction이 예상되는 부분에 대해서 테스트를 진행해 보았습니다. 테스트는 OpenAPI를 통한 Transaction 중에서 [opt10001 : 주식기본정보요청]에 대해서 진행했습니다. 아무래도 전체 주식종목에 대한 기본정보 요청이 있을 수 있어서 대략 3천건 이상의 조회가 한번에 이루어 져야 하기 때문입니다. 

 

그럼 해당 Transaction을 호출하는 코드를 보겠습니다.

 

[opt10001 : 주식기본정보요청]

table_insert_cnt = 0
sql_dml = '''
INSERT INTO stocks_basic_info(stocks_id, stocks_roe, stocks_pbr, stocks_marketcap, create_date)
VALUES (%s, %s, %s, %s, NOW())
ON DUPLICATE KEY UPDATE update_yn='Y', update_data=NOW()
'''
for i in sList:
  # opt10001 TR요청
  kd.opt10001_res = {}
  kd.SetInputValue("종목코드", i[0])
  kd.CommRqData("AT_opt10001", "opt10001", "0", "0101")

  sql_data = (kd.opt10001_res["종목명"],kd.opt10001_res["ROE"],kd.opt10001_res["PBR"],kd.opt10001_res["시가총액"])

  try:
  mariadb_cur.execute(sql_dml, sql_data)
  table_insert_cnt += 1
  except mariadb.Error as error:
  print(error)

  print(table_insert_cnt)

mariadb_conn.commit()

[결과]

3066
1
2
3
4
5

총 3,066건에 대한 Transaction에 대해서 5건만 수행이되고 정지합니다. 그럼 1초에 5회제한을 위해서 time.sleep을 주면서 테스트를 해보겠습니다.

 

 

1. 테스트 (time.sleep 0.2초)


1초에 5회 제한이 생기기 때문에 time.sleep을 0.2초 추가하여, API를 호출해 보겠습니다. 

 

[opt10001 : 주식기본정보요청]

for i in sList:
  # opt10001 TR요청
  kd.opt10001_res = {}
  kd.SetInputValue("종목코드", i[0])
  kd.CommRqData("AT_opt10001", "opt10001", "0", "0101")

  sql_data = (kd.opt10001_res["종목명"],kd.opt10001_res["ROE"],kd.opt10001_res["PBR"],kd.opt10001_res["시가총액"])

  try:
  mariadb_cur.execute(sql_dml, sql_data)
  table_insert_cnt += 1
  except mariadb.Error as error:
  print(error)

  time.sleep(0.2) ----------------------------> 추가
  print(table_insert_cnt)

mariadb_conn.commit()

다음과 같이 time.sleep(0.2)를 추가해 줍니다. 그렇게 하면 결과는,

아래와 같이, 100건의 Transaction이 정상적으로 수행되었지만 결국 과도한 요청으로 제한이 걸리게 됩니다. 그렇다면... 위에서 알아본 [주요2 - 분당, 시간당 유동적 조회제한]이 걸린 것으로 보입니다.

 

키움증권 OpenAPI에 대한 고객문의 게시판을 통해서 답을 구해보고자 했지만... 결국은 아래와 같은 대답이 일반적이였습니다.

서버부하방지 제한은 1초당 5회 조회횟수 제한과는 다르게 서버상태에따라 유동적일 수 있어서
1초당 5회 제한처럼 명확한 가이드를 드리기 어려운점 양해 부탁드립니다.
가능한 경우 TR서비스를 매번 요청하는 대신 실시간으로 수신되는 데이터를 이용해주시고
말씀하신 0.5초 보다 더 큰 여유를 두어 구현하시기 바랍니다.

아무래도 최적의 방법을 찾아봐야겠습니다.

 

 

2. 테스트 (time.sleep 1분추가, 50건 마다)


우선 기본적으로 1초에 5건에 대해서는 지켜주고, 매 50건 마다 1분에 해당하는 sleep을 추가시켜 보겠습니다. 이를 반영한 코드는 아래와 같습니다. 소요시간을 확인하기 위해서 Transaction 수행전 시간을 찍고, 매 Transaction마다 해당 시간을 찍어주도록 하겠습니다. 왜냐하면... 종료 후에 한번만 찍어주고자 한다면, TR초과로 더이상 진행이 되지 않아 특정 상황에서 얼마만큼 시간이 흘렀는지 시간이 나오지 않기 때문입니다.

 

[opt10001 : 주식기본정보요청]

for i in sList:
    # opt10001 TR요청
    kd.opt10001_res = {}
    kd.SetInputValue("종목코드", i[0])
    kd.CommRqData("AT_opt10001", "opt10001", "0", "0101")

    sql_data = (kd.opt10001_res["종목명"],kd.opt10001_res["ROE"],kd.opt10001_res["PBR"],kd.opt10001_res["시가총액"])

    try:
    mariadb_cur.execute(sql_dml, sql_data)
    table_insert_cnt += 1
    except mariadb.Error as error:
    print(error)

    print(table_insert_cnt, ", ", datetime.datetime.now())

    time.sleep(0.2)
    if table_insert_cnt % 50 == 0:
        time.sleep(60)

mariadb_conn.commit()

자 그러면 얼마나 진행이 될까요?? 아마... 시간이 좀 걸릴 것 입니다. 왜냐하면, 50건마다 1분씩 쉬기 때문에 500건이 진행된다고 했을때... 이미 10분이나 휴식을 가졌기 때문입니다.

 

결국은 아래와 같이 1000번째 시도를 할때, 과도한 요청으로 정지를 당하게 되었습니다. 우리는 무려 약 3천건의 데이터에 대해서 요청을 해야하기 때문에 다른 방법을 찾아봐야 할 것 같습니다.  그럼 얼마의 시간에 요청에 대해서 거부를 한 걸까요??

 

start :  2021-02-18 08:10:17.571000
block :  2021-02-18 08:33:09.478000

 

약 23분 사이에 1,000건에 대해서 요청을 하면 과도한 요청으로 갈음하는 것 같습니다. 서버에 따라서 유동적이라고 하기 때문에... 추측하는 방법밖에 없을 것으로 보입니다.

 

자 그렇다면... 아무래도 사람이 코딩을 하는 것 일테니... 30분에 1,000건으로 제약이 걸린 것 아닐까 하는 생각으로 코드를 또 변경해 보도록 하겠습니다. 

 

 

 

3. 테스트 (time.sleep 30분에 1000건이 처리되도록 조절)


30분에 1000건에 대해서 조절하기 위해서는, time.sleep을 그에 상응하게 주면 됩니다. 이제는 그냥 전체 초를 건수로 나누기로 하겠습니다. 

 

30분 = 1800초 이고, 1000건이기 때문에 단순하게 나눠주면... time.sleep은 1.8초로 수행하면됩니다. 혹시나 너무 딱 떨어 질 경우, 애매한 상황을 피하기 위해서 최종적으로 time.sleep(1.9)로 진행해 보겠습니다. 

 

[opt10001 : 주식기본정보요청]

for i in sList:
    # opt10001 TR요청
    kd.opt10001_res = {}
    kd.SetInputValue("종목코드", i[0])
    kd.CommRqData("AT_opt10001", "opt10001", "0", "0101")

    sql_data = (kd.opt10001_res["종목명"],kd.opt10001_res["ROE"],kd.opt10001_res["PBR"],kd.opt10001_res["시가총액"])

    try:
    mariadb_cur.execute(sql_dml, sql_data)
    table_insert_cnt += 1
    except mariadb.Error as error:
    print(error)

    print(table_insert_cnt, ", ", datetime.datetime.now())

    time.sleep(1.9)

mariadb_conn.commit()

자 과연 결과는 어떻게 나왔을까요??

 

start :  2021-02-18 08:43:22.324000
block :  2021-02-18 09:15:32.255000

 

약 32분 사이에... 1,000건에 대해서 요청을 해도 역시나 과도한 요청으로 갈음하는 것 같습니다. 최후의 보루로 1시간 즉 60분에 1,000건으로 제약이 걸린 것 아닐까 하는 생각으로 코드를 또 변경해 보도록 하겠습니다. 

 

 

 

4. 테스트 (time.sleep 60분에 1000건이 처리되도록 조절)


60분에 1000건에 대해서 조절하기 위해서는, 60분 = 3600초 이고, 1000건이기 때문에 단순하게 나눠주면... time.sleep은 3.6초로 수행하면됩니다. 혹시나 너무 딱 떨어 질 경우, 애매한 상황을 피하기 위해서 최종적으로 time.sleep(3.6)로 진행해 보겠습니다. 

 

[opt10001 : 주식기본정보요청]

for i in sList:
    # opt10001 TR요청
    kd.opt10001_res = {}
    kd.SetInputValue("종목코드", i[0])
    kd.CommRqData("AT_opt10001", "opt10001", "0", "0101")

    sql_data = (kd.opt10001_res["종목명"],kd.opt10001_res["ROE"],kd.opt10001_res["PBR"],kd.opt10001_res["시가총액"])

    try:
    mariadb_cur.execute(sql_dml, sql_data)
    table_insert_cnt += 1
    except mariadb.Error as error:
    print(error)

    print(table_insert_cnt, ", ", datetime.datetime.now())

    time.sleep(3.6)

mariadb_conn.commit()

흑... 이번에는 좀 되었으면 좋겠네요...

2021-02-18
3065
start :  2021-02-18 10:57:55.848000
1 ,  2021-02-18 10:57:55.883000
2 ,  2021-02-18 10:57:59.515000
3 ,  2021-02-18 10:58:03.139000
...
...
...
997 ,  2021-02-18 11:58:12.345000
998 ,  2021-02-18 11:58:15.976000
999 ,  2021-02-18 11:58:19.606000
1000 ,  2021-02-18 11:58:23.237000
1001 ,  2021-02-18 11:58:26.867000
1002 ,  2021-02-18 11:58:30.498000
...
...
...
1997 ,  2021-02-18 12:58:44.143000
1998 ,  2021-02-18 12:58:47.773000
1999 ,  2021-02-18 12:58:51.406000
2000 ,  2021-02-18 12:58:55.036000
2001 ,  2021-02-18 12:58:58.672000
2002 ,  2021-02-18 12:59:02.303000
...
...
...
3064 ,  2021-02-18 14:03:19.499000
3065 ,  2021-02-18 14:03:23.130000
end :  2021-02-18 14:03:26.736000

억 결과가 나왔습니다. 1시간에 1,000건 요청하는 로직으로는 정상적으로 완료 되었습니다. 다만, 소요시간이... 3시간6분정도 걸렸습니다. 

DB에도 정상적으로 등록이 되었습니다.

 

 

0. 결과정리


테스트 해본결과 로직을 구현할때 정리해보면 아래와 같습니다. 여러분도 개발 시 참고해주시면 될 듯 해요.

 

  • TR 5건 이하 - time.sleep없이 그냥 진행가능
  • TR 100건 이하 - time.sleep(0.2)를 추가하여 진행가능
  • TR 1000건 이하 - time.sleep(1.8)을 추가하여 진행가능
  • TR 1000건 초과 - 1시간 1000건 제약으로 time.sleep(3.6)을 추가하여 진행가능

 

- Ayotera Lab -

댓글