Files
KisStock/한국투자증권(API)/legacy/Sample01/kis_ovrseafuopt_ws.py
2026-01-31 22:34:57 +09:00

509 lines
24 KiB
Python

# 국내주식 실시간 websocket sample
import websocket
import kis_auth as ka
import kis_ovrseafuopt as kb
import os
import json
import requests
import pandas as pd
import numpy as np
import time
import datetime
from io import StringIO
from threading import Thread
from collections import namedtuple, deque
try:
import websockets
except ImportError:
print("websocket-client 설치중입니다.")
os.system('python3 -m pip3 install websocket-client')
from enum import StrEnum
class KIS_WSReq(StrEnum):
CONTRACT = 'HDFFF020' # 해외선물옵션 실시간체결가
BID_ASK = 'HDFFF010' # 해외선물옵션 실시간호가
ORDERNOTICE = 'HDFFF1C0' # 실시간 해외선물옵션 주문내역발생통보
CCLDNOTICE = 'HDFFF2C0' # 실시간 해외선물옵션 체결내역발생통보
import talib as ta
class BasicPlan:
def __init__(self, stock_code, window=20):
self._stock_code = stock_code
self._queue = deque(maxlen=window)
self._prev_ma = None
def push(self, value):
self._queue.append(value)
ma = sum(self._queue) / len(self._queue)
diff = ma - self._prev_ma if self._prev_ma is not None else None
self._prev_ma = ma
print(f"{self._stock_code}****** value: {value}, MA: {ma}, diff: {diff}...")
class RSI_ST: # RSI(Relative Strength Index, 상대강도지수)라는 주가 지표 계산
def __init__(self, stock_code, window=21):
self._stock_code = stock_code
self._queue = deque(maxlen=window)
self.rsi_period = window
def eval(self):
# dftt = getStreamdDF(self._stock_code)
# print(self)
dftt = contract_sub_df.get(self._stock_code).copy()
dftt = dftt.set_index(['TICK_HOUR'])
dftt['LAST_PRICE'] = pd.to_numeric(dftt['LAST_PRICE'], errors='coerce').convert_dtypes()
np_closes = np.array(dftt['LAST_PRICE'], dtype=np.float64)
rsi = ta.RSI(np_closes, self.rsi_period)
last_rsi = rsi[-1]
if last_rsi < 30:
print(f"({self._stock_code})[BUY] ***RSI: {last_rsi}") # 통상적으로 RSI가 30 이하면 과매도 상태인 것으로 판단하고 시장이 과도하게 하락했음을 나타냄
elif last_rsi < 70 and last_rsi >= 30:
print(f"({self._stock_code})[N/A] ***RSI: {last_rsi}")
elif last_rsi >= 70:
print(f"({self._stock_code})[SELL] ***RSI: {last_rsi}") # 통상적으로 RSI가 70 이상이면 과매수 상태로 간주하고 시장이 과열되었을 가능성이 있음을 나타냄
else:
print(self._stock_code)
_today__ = datetime.date.today().strftime("%Y%m%d")
ka.auth()
__DEBUG__ = False # True
# 실시간 해외주식 계좌체결통보 복호화를 위한 부분 - start
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
from base64 import b64decode
# AES256 DECODE: Copied from KIS Developers Github sample code
def aes_cbc_base64_dec(key, iv, cipher_text):
"""
:param key: str type AES256 secret key value
:param iv: str type AES256 Initialize Vector
:param cipher_text: Base64 encoded AES256 str
:return: Base64-AES256 decodec str
"""
cipher = AES.new(key.encode('utf-8'), AES.MODE_CBC, iv.encode('utf-8'))
return bytes.decode(unpad(cipher.decrypt(b64decode(cipher_text)), AES.block_size))
# 실시간 해외주식 계좌체결통보 복호화를 위한 부분 - end
contract_sub_df = dict() # 실시간 해외주식 체결 결과를 종목별로 저장하기 위한 container
tr_plans = dict() # 실시간 해외주식 체결 값에 따라 무언가를 수행할 Class 를 저장하기 위한 container
excg_dict = {
'NYS' : 'NYSE', #미국뉴욕
'NAS' : 'NASD', #미국나스닥
'AMS' : 'AMEX', #미국아멕스
'TSE' : 'TKSE', #일본도쿄
'HKS' : 'SEHK', #홍콩
'SHS' : 'SHAA', #중국상해
'SZS' : 'SZAA', #중국심천
'HSX' : 'VNSE', #베트남호치민,
'HNX' : 'HASE', #베트남하노이
'BAY' : 'NYSE', #미국뉴욕(주간)
'BAQ' : 'NASD', #미국나스닥(주간),
'BAA' : 'AMEX' #미국아멕스(주간)
}
#reserved_cols = ['TICK_HOUR', 'STCK_PRPR', 'ACML_VOL'] # 실시간 해외주식 체결 중 사용할 수신시간, 현재가, 누적거래량 만 추출하기 위한 column 정의
reserved_cols = ['TICK_HOUR', 'LAST_PRICE', 'VOL'] # 실시간 해외선물옵션 체결 중 사용할 column 만 추출하기 위한 column 정의
# 해외선물옵션 실시간체결가 column header
contract_cols = ['SERIES_CD', # 종목코드 * 각 항목사이에는 구분자로 ^ 사용, 모든 데이터타입은 STRING으로 변환되어 PUSH 처리됨'
'BSNS_DATE', # 영업일자
'MRKT_OPEN_DATE', # 장개시일자
'MRKT_OPEN_TIME', # 장개시시각
'MRKT_CLOSE_DATE', # 장종료일자
'MRKT_CLOSE_TIME', # 장종료시각
'PREV_PRICE', # 전일종가 ※ 전일종가, 체결가격, 전일대비가, 시가, 고가, 저가 ※ FFCODE.MST(해외선물종목마스터 파일)의 SCALCDESZ(계산 소수점) 값 참고
'RECV_DATE', # 수신일자
'TICK_HOUR', # 수신시각 ※ 수신시각(RECV_TIME) = 실제 체결시각 ★ pandas time conversion 편의를 위해 이 필드만 이름을 통일한다 'KHMS' 한국시간
'ACTIVE_FLAG', # 본장_전산장구분
'LAST_PRICE', # 체결가격
'LAST_QNTT', # 체결수량
'PREV_DIFF_PRICE', # 전일대비가
'PREV_DIFF_RATE', # 등락률
'OPEN_PRICE', # 시가
'HIGH_PRICE', # 고가
'LOW_PRICE', # 저가
'VOL', # 누적거래량
'PREV_SIGN', # 전일대비부호
'QUOTSIGN', # 체결구분 ※ 2:매수체결 5:매도체결
'RECV_TIME2', # 수신시각2 만분의일초
'PSTTL_PRICE', # 전일정산가
'PSTTL_SIGN', # 전일정산가대비
'PSTTL_DIFF_PRICE', # 전일정산가대비가격
'PSTTL_DIFF_RATE'] # 전일정산가대비율
# 실시간 해외선물옵션호가 column eader
bid_ask_cols = ['SERIES_CD', # 종목코드 '각 항목사이에는 구분자로 ^ 사용,모든 데이터타입은 STRING으로 변환되어 PUSH 처리됨'
'RECV_DATE', # 수신일자
'TICK_HOUR', # 수신시각 ※ 수신시각(RECV_TIME) = 실제 체결시각 ★ pandas time conversion 편의를 위해 이 필드만 이름을 통일한다 'KHMS' 한국시간
'PREV_PRICE', # 전일종가 ※ 전일종가, 매수1호가~매도5호가 ※ FFCODE.MST(해외선물종목마스터 파일)의 SCALCDESZ(계산 소수점) 값 참고
'BID_QNTT_1', # 매수1수량
'BID_NUM_1', # 매수1번호
'BID_PRICE_1', # 매수1호가
'ASK_QNTT_1', # 매도1수량
'ASK_NUM_1', # 매도1번호
'ASK_PRICE_1', # 매도1호가
'BID_QNTT_2', # 매수2수량
'BID_NUM_2', # 매수2번호
'BID_PRICE_2', # 매수2호가
'ASK_QNTT_2', # 매도2수량
'ASK_NUM_2', # 매도2번호
'ASK_PRICE_2', # 매도2호가
'BID_QNTT_3', # 매수3수량
'BID_NUM_3', # 매수3번호
'BID_PRICE_3', # 매수3호가
'ASK_QNTT_3', # 매도3수량
'ASK_NUM_3', # 매도3번호
'ASK_PRICE_3', # 매도3호가
'BID_QNTT_4', # 매수4수량
'BID_NUM_4', # 매수4번호
'BID_PRICE_4', # 매수4호가
'ASK_QNTT_4', # 매도4수량
'ASK_NUM_4', # 매도4번호
'ASK_PRICE_4', # 매도4호가
'BID_QNTT_5', # 매수5수량
'BID_NUM_5', # 매수5번호
'BID_PRICE_5', # 매수5호가
'ASK_QNTT_5', # 매도5수량
'ASK_NUM_5', # 매도5번호
'ASK_PRICE_5', # 매도5호가
'STTL_PRICE'] # 전일정산가
# 실시간 계좌주문내역발생통보 column header
ordernotice_cols = ['USER_ID', # 유저ID 각 항목사이에는 구분자로 ^ 사용, 모든 데이터타입은 STRING으로 변환되어 PUSH 처리됨'
'ACCT_NO', # 계좌번호
'ORD_DT', # 주문일자
'ODNO', # 주문번호
'ORGN_ORD_DT', # 원주문일자
'ORGN_ODNO', # 원주문번호
'SERIES', # 종목명
'RVSE_CNCL_DVSN_CD',# 정정취소구분코드 해당없음 : 00 , 정정 : 01 , 취소 : 02
'SLL_BUY_DVSN_CD', # 매도매수구분코드 01 : 매도, 02 : 매수
'CPLX_ORD_DVSN_CD', # 복합주문구분코드 0 (HEDGE청산만 이용)
'PRCE_TP', # 가격구분코드', # 1:LIMIT, 2:MARKET, 3:STOP(STOP가격시 시장가)
'FM_EXCG_RCIT_DVSN_CD', # FM거래소접수구분코드 01:접수전, 02:응답, 03:거부
'ORD_QTY', # 주문수량
'FM_LMT_PRIC', # FMLIMIT가격
'FM_STOP_ORD_PRIC', # FMSTOP주문가격
'TOT_CCLD_QTY', # 총체결수량
'TOT_CCLD_UV', # 총체결단가
'ORD_REMQ', # 잔량
'FM_ORD_GRP_DT', # FM주문그룹일자 주문일자(ORD_DT)와 동일
'ORD_GRP_STNO', # 주문그룹번호
'ORD_DTL_DTIME', # 주문상세일시
'OPRT_DTL_DTIME', # 조작상세일시
'WORK_EMPL', # 주문자
'CRCY_CD', # 통화코드
'LQD_YN', # 청산여부(Y/N)
'LQD_LMT_PRIC', # 청산LIMIT가격
'LQD_STOP_PRIC', # 청산STOP가격
'TRD_COND', # 체결조건코드
'TERM_ORD_VALD_DTIME', # 기간주문유효상세일시
'SPEC_TP', # 계좌청산유형구분코드
'ECIS_RSVN_ORD_YN', # 행사예약주문여부
'FUOP_ITEM_DVSN_CD',# 선물옵션종목구분코드
'AUTO_ORD_DVSN_CD'] # 자동주문 전략구분
# 실시간 계좌체결내역발생통보 column header
ccldnotice_cols = ['USER_ID', # 유저ID '각 항목사이에는 구분자로 ^ 사용, 모든 데이터타입은 STRING으로 변환되어 PUSH 처리됨'
'ACCT_NO', # 계좌번호
'ORD_DT', # 주문일자
'ODNO', # 주문번호
'ORGN_ORD_DT', # 원주문일자
'ORGN_ODNO', # 원주문번호
'SERIES', # 종목명
'RVSE_CNCL_DVSN_CD',# 정정취소구분코드', # 해당없음 : 00 , 정정 : 01 , 취소 : 02
'SLL_BUY_DVSN_CD', # 매도매수구분코드', # 01 : 매도, 02 : 매수
'CPLX_ORD_DVSN_CD', # 복합주문구분코드', # 0 (HEDGE청산만 이용)
'PRCE_TP', # 가격구분코드
'FM_EXCG_RCIT_DVSN_CD', # FM거래소접수구분코드
'ORD_QTY', # 주문수량
'FM_LMT_PRIC', # FMLIMIT가격
'FM_STOP_ORD_PRIC', # FMSTOP주문가격
'TOT_CCLD_QTY', # 총체결수량', # 동일한 주문건에 대한 누적된 체결수량 (하나의 주문건에 여러건의 체결내역 발생)
'TOT_CCLD_UV', # 총체결단가
'ORD_REMQ', # 잔량
'FM_ORD_GRP_DT', # FM주문그룹일자
'ORD_GRP_STNO', # 주문그룹번호
'ORD_DTL_DTIME', # 주문상세일시
'OPRT_DTL_DTIME', # 조작상세일시
'WORK_EMPL', # 주문자
'CCLD_DT', # 체결일자
'CCNO', # 체결번호
'API_CCNO', # API 체결번호
'CCLD_QTY', # 체결수량', # 매 체결 단위 체결수량임 (여러건 체결내역 누적 체결수량인 총체결수량과 다름)
'FM_CCLD_PRIC', # FM체결가격
'CRCY_CD', # 통화코드
'TRST_FEE', # 위탁수수료
'ORD_MDIA_ONLINE_YN', # 주문매체온라인여부
'FM_CCLD_AMT', # FM체결금액
'FUOP_ITEM_DVSN_CD']# 선물옵션종목구분코드
# 웹소켓 접속키 발급
def get_approval():
url = ka.getTREnv().my_url
headers = {"content-type": "application/json"}
body = {"grant_type": "client_credentials",
"appkey": ka.getTREnv().my_app,
"secretkey": ka.getTREnv().my_sec}
PATH = "oauth2/Approval"
URL = f"{url}/{PATH}"
res = requests.post(URL, headers=headers, data=json.dumps(body))
approval_key = res.json()["approval_key"]
return approval_key
_connect_key = get_approval() # websocker 연결Key
_iv = None # for 복호화
_ekey = None # for 복호화
executed_df = pd.DataFrame(data=None, columns=contract_cols) # 체결통보 저장용 DF
# added_data 는 종목코드(실시간체결, 실시간호가) 또는 HTS_ID(체결통보)
def _build_message(app_key, tr_id, added_data, tr_type='1'):
_h = {
"approval_key": app_key,
"custtype": 'P',
"tr_type": tr_type,
"content-type": "utf-8"
}
_inp = {
"tr_id": tr_id,
"tr_key": added_data
}
_b = {
"input": _inp
}
_data = {
"header": _h,
"body": _b
}
d1 = json.dumps(_data)
return d1
# sub_data 는 종목코드(실시간체결, 실시간호가) 또는 HTS_ID(실시간 계좌체결발생통보)
def subscribe(ws, sub_type, app_key, sub_data): # 세션 종목코드(실시간체결, 실시간호가) 등록
ws.send(_build_message(app_key, sub_type, sub_data), websocket.ABNF.OPCODE_TEXT)
time.sleep(.1)
def unsubscribe(ws, sub_type, app_key, sub_data): # 세션 종목코드(실시간체결, 실시간호가) 등록해제
ws.send(_build_message(app_key, sub_type, sub_data, '2'), websocket.ABNF.OPCODE_TEXT)
time.sleep(.1)
# streaming data 를 이용해 주어진 bar 크기(예: 1분, 5분 등)의 OHLC(x분봉) 데이터프레임을 반환한다.
# 이때 streamign data 는 websocket client 가 시작한 다음부터 지금까지의 해당 종목의 가격 정보를 의미한다.
# ** 동시호가 시간은 OHLC data 가 모두 NA 가 된다.
def getStreamdDF(stock_code, bar_sz='1Min'):
df3 = contract_sub_df.get(stock_code).copy()
df3 = df3.set_index(['TICK_HOUR'])
df3['LAST_PRICE'] = pd.to_numeric(df3['LAST_PRICE'], errors='coerce').convert_dtypes()
df3 = df3['LAST_PRICE'].resample(bar_sz).ohlc()
return df3
# 수신데이터 파싱
def _dparse(data):
global executed_df
d1 = data.split("|")
dp_ = None
hcols = []
if len(d1) >= 4:
tr_id = d1[1]
if tr_id == KIS_WSReq.CONTRACT: # 실시간체결
hcols = contract_cols
elif tr_id == KIS_WSReq.BID_ASK: # 해외선물옵션 실시간호가
hcols = bid_ask_cols
elif tr_id == KIS_WSReq.ORDERNOTICE: # 주문내역발생통보
hcols = ordernotice_cols
elif tr_id == KIS_WSReq.CCLDNOTICE: # 체결내역발생통보
hcols = ccldnotice_cols
else:
pass
if tr_id in (KIS_WSReq.CONTRACT, KIS_WSReq.BID_ASK): # 실시간체결, 실시간호가
dp_ = pd.read_csv(StringIO(d1[3]), header=None, sep='^', names=hcols, dtype=object) # 수신데이터 parsing
print(dp_) # 실시간체결, 실시간호가 수신 데이터 파싱 결과 확인
dp_['TICK_HOUR'] = _today__ + dp_['TICK_HOUR'] # 수신시간
dp_['TICK_HOUR'] = pd.to_datetime(dp_['TICK_HOUR'], format='%Y%m%d%H%M%S', errors='coerce')
else: # 실시간 계좌체결발생통보는 암호화되어서 수신되므로 복호화 과정이 필요
dp_ = pd.read_csv(StringIO(aes_cbc_base64_dec(_ekey, _iv, d1[3])), header=None, sep='^', names=hcols, # 수신데이터 parsing 및 복호화
dtype=object)
print(dp_) # 실시간 계좌체결발생통보 수신 파싱 결과 확인
if __DEBUG__: print(f'***EXECUTED CCLDNOTICE [{dp_.to_string(header=False, index=False)}]')
if tr_id == KIS_WSReq.CONTRACT: # 실시간 체결
if __DEBUG__: print(dp_.to_string(header=False, index=False))
stock_code = dp_[dp_.columns[0]].values.tolist()[0]
df2_ = dp_[reserved_cols]
# dft_ = pd.concat([contract_sub_df.get(stock_code), df2_], axis=0, ignore_index=True)
# 선택된 열이 비어 있거나 모든 값이 NA인지 확인
selected_df = contract_sub_df.get(stock_code)
if selected_df is not None and not selected_df.dropna().empty:
dft_ = pd.concat([selected_df, df2_], axis=0, ignore_index=True)
else:
dft_ = df2_
contract_sub_df[stock_code] = dft_
######### 이 부분에서 로직을 적용한 후 매수/매도를 수행하면 될 듯!!
val1 = dp_['LAST_PRICE'].tolist()[0]
tr_plans[stock_code].push(float(val1)) # 이동평균값 활용
# tr_plans[stock_code].eval() # RSI(Relative Strength Index, 상대강도지수)라는 주가 지표 계산 활용
stock_df = dp_['SERIES_CD'].tolist()[0] # 종목코드
# [해외선물옵션] 주문/계좌 > 해외선물옵션 주문가능조회 (선물옵션구분fuop_dvsn)
# 선물옵션구분 00:전체 / 01:선물 / 02:옵션
rt_data = kb.get_overseasfuopt_inquire_psamount(itm_no=stock_df, dvsn="00", pric=0, ordyn="")
ord_qty = rt_data.loc[0, 'fm_new_ord_psbl_qty'] # 신규주문가능수량 총주문가능수량(fm_tot_ord_psbl_qty), 시장가총주문가능수량(fm_mkpr_tot_ord_psbl_qty)
print("[주문가능수량!] : " + ord_qty)
###########################################################
# [해외선물옵션] 주문/계좌 > 해외선물옵션주문 (종목번호<6자리 5자리> + 매수매도구분ord_dv + 가격구분dvsn + 주문수량qty + 주문가격limt_pric + 주문가격stop_pric)
# 매수매도구분ord_dv 01 : 매도, 02 : 매수 # 가격구분dvsn : 1.지정, 2. 시장, 3. STOP, 4 S/L
# 주문가격limt_pric : 지정가인 경우 가격 입력 * 시장가, STOP주문인 경우, 빈칸("") 입력
# 주문가격stop_pric : STOP 주문 가격 입력 * 시장가, 지정가인 경우, 빈칸("") 입력
# rt_data = kb.get_overseasfuopt_order(itm_no=stock_df, ord_dv="02", dvsn="1", qty=ord_qty, limt_pric=val1, stop_pric=0)
# print(rt_data.ORD_DT + "+" + rt_data.ODNO) # 주문일자+주문접수번호
print("매수/매도 조건 주문 : " + val1)
###########################################################
elif tr_id == KIS_WSReq.CCLDNOTICE: # 체결통보의 경우, 일단 executed_df 에만 저장해 둠
if __DEBUG__: print(dp_.to_string(header=False, index=False))
executed_df = pd.concat([executed_df, dp_], axis=0, ignore_index=True)
else:
pass
else:
print("Data length error...{data}")
def _get_sys_resp(data):
global _iv
global _ekey
isPingPong = False
isUnSub = False
isOk = False
tr_msg = None
tr_key = None
rdic = json.loads(data)
tr_id = rdic['header']['tr_id']
if tr_id != "PINGPONG": tr_key = rdic['header']['tr_key']
if rdic.get("body", None) is not None:
isOk = True if rdic["body"]["rt_cd"] == "0" else False
tr_msg = rdic["body"]["msg1"]
# 복호화를 위한 key 를 추출
if 'output' in rdic["body"]:
_iv = rdic["body"]["output"]["iv"]
_ekey = rdic["body"]["output"]["key"]
isUnSub = True if tr_msg[:5] == "UNSUB" else False
else:
isPingPong = True if tr_id == "PINGPONG" else False
nt2 = namedtuple('SysMsg', ['isOk', 'tr_id', 'tr_key', 'isUnSub', 'isPingPong'])
d = {
'isOk': isOk,
'tr_id': tr_id,
'tr_key': tr_key,
'isUnSub': isUnSub,
'isPingPong': isPingPong
}
return nt2(**d)
def on_data(ws, data, resp_type, data_continu):
# print(f"On data => {resp_type}, {data_continu}, {data}") #return only 1, True
pass
def on_message(ws, data):
if data[0] in ('0', '1'): # 실시간체결 or 실시간호가
_dparse(data)
else: # system message or PINGPONG
rsp = _get_sys_resp(data)
if rsp.isPingPong:
ws.send(data, websocket.ABNF.OPCODE_PING)
else:
if (not rsp.isUnSub and rsp.tr_id == KIS_WSReq.CONTRACT):
contract_sub_df[rsp.tr_key] = pd.DataFrame(columns=reserved_cols)
########################################################################
#### 이 부분에서 전략을 수행할 class 를 등록한다.
#### 실제 주문 실행은 _dparse 함수에서 처리
tr_plans[rsp.tr_key] = BasicPlan(rsp.tr_key) # 이동 평균선 계산 (웹소켓 프로그램 실행시 수집된 데이터만 반영)
# tr_plans[rsp.tr_key] = RSI_ST(rsp.tr_key) # RSI(Relative Strength Index, 상대강도지수)라는 주가 지표 계산
########################################################################
elif (rsp.isUnSub):
del (contract_sub_df[rsp.tr_key])
else:
print(rsp)
def on_error(ws, error):
print('error=', error)
def on_close(ws, status_code, close_msg):
print('on_close close_status_code=', status_code, " close_msg=", close_msg)
def on_open(ws):
# stocks 에는 40개까지만 가능
stocks = ('6EV24', '6EU24', 'ESU24', 'OESU24 C5450', 'ONQU24 C18900', 'OESU24 C6000') # 해외선물옵션
for scode in stocks:
subscribe(ws, KIS_WSReq.BID_ASK, _connect_key, scode) # 실시간 호가(미국)
subscribe(ws, KIS_WSReq.CONTRACT, _connect_key, scode) # 실시간 체결
# unsubscribe(ws, KIS_WSReq.CONTRACT, _connect_key, "RBAQAAPL") #실시간 체결 연결해제
# subscribe(ws, KIS_WSReq.CONTRACT, _connect_key, "RBAQAAPL") #실시간 체결 연결등록
# unsubscribe(ws, KIS_WSReq.BID_USA, _connect_key, "RBAQAAPL") #실시간 호가(미국) 연결해제
# subscribe(ws, KIS_WSReq.BID_USA, _connect_key, "RBAQAAPL") #실시간 호가(미국) 연결등록
# 실시간 계좌체결발생통보를 등록한다. 계좌체결발생통보 결과는 executed_df 에 저장된다.
#subscribe(ws, KIS_WSReq.ORDERNOTICE, _connect_key, "HTS ID 입력") # "HTS ID 입력 하세요" 계좌주문내역발생통보
subscribe(ws, KIS_WSReq.CCLDNOTICE, _connect_key, "HTS ID 입력") # "HTS ID 입력 하세요" 계좌체결내역발생통보
ws = websocket.WebSocketApp("ws://ops.koreainvestment.com:21000/tryitout",
on_open=on_open, on_message=on_message, on_error=on_error, on_data=on_data)
ws.run_forever() # 실시간 웹소켓 연결 작동