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

560 lines
25 KiB
Python

import asyncio
import websockets
import json
import time
from multiprocessing import Process, Queue, Manager
import json
import time
import requests
import asyncio
import websockets
import urllib3
import os
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
# 웹소켓 접속키 발급
def get_approval(key, secret):
# url = https://openapivts.koreainvestment.com:29443' # 모의투자계좌
url = 'https://openapi.koreainvestment.com:9443' # 실전투자계좌
headers = {"content-type": "application/json"}
body = {"grant_type": "client_credentials",
"appkey": key,
"secretkey": secret}
PATH = "oauth2/Approval"
URL = f"{url}/{PATH}"
time.sleep(0.05)
res = requests.post(URL, headers=headers, data=json.dumps(body))
approval_key = res.json()["approval_key"]
return approval_key
# 국내주식호가 출력라이브러리
def stockhoka(data):
""" 넘겨받는데이터가 정상인지 확인
print("stockhoka[%s]"%(data))
"""
recvvalue = data.split('^') # 수신데이터를 split '^'
"""
print("유가증권 단축 종목코드 [" + recvvalue[0] + "]")
print("영업시간 [" + recvvalue[1] + "]" + "시간구분코드 [" + recvvalue[2] + "]")
print("======================================")
print("매도호가10 [%s] 잔량10 [%s]" % (recvvalue[12], recvvalue[32]))
print("매도호가09 [%s] 잔량09 [%s]" % (recvvalue[11], recvvalue[31]))
print("매도호가08 [%s] 잔량08 [%s]" % (recvvalue[10], recvvalue[30]))
print("매도호가07 [%s] 잔량07 [%s]" % (recvvalue[9], recvvalue[29]))
print("매도호가06 [%s] 잔량06 [%s]" % (recvvalue[8], recvvalue[28]))
print("매도호가05 [%s] 잔량05 [%s]" % (recvvalue[7], recvvalue[27]))
print("매도호가04 [%s] 잔량04 [%s]" % (recvvalue[6], recvvalue[26]))
print("매도호가03 [%s] 잔량03 [%s]" % (recvvalue[5], recvvalue[25]))
print("매도호가02 [%s] 잔량02 [%s]" % (recvvalue[4], recvvalue[24]))
print("매도호가01 [%s] 잔량01 [%s]" % (recvvalue[3], recvvalue[23]))
print("--------------------------------------")
print("매수호가01 [%s] 잔량01 [%s]" % (recvvalue[13], recvvalue[33]))
print("매수호가02 [%s] 잔량02 [%s]" % (recvvalue[14], recvvalue[34]))
print("매수호가03 [%s] 잔량03 [%s]" % (recvvalue[15], recvvalue[35]))
print("매수호가04 [%s] 잔량04 [%s]" % (recvvalue[16], recvvalue[36]))
print("매수호가05 [%s] 잔량05 [%s]" % (recvvalue[17], recvvalue[37]))
print("매수호가06 [%s] 잔량06 [%s]" % (recvvalue[18], recvvalue[38]))
print("매수호가07 [%s] 잔량07 [%s]" % (recvvalue[19], recvvalue[39]))
print("매수호가08 [%s] 잔량08 [%s]" % (recvvalue[20], recvvalue[40]))
print("매수호가09 [%s] 잔량09 [%s]" % (recvvalue[21], recvvalue[41]))
print("매수호가10 [%s] 잔량10 [%s]" % (recvvalue[22], recvvalue[42]))
print("======================================")
print("총매도호가 잔량 [%s]" % (recvvalue[43]))
print("총매도호가 잔량 증감 [%s]" % (recvvalue[54]))
print("총매수호가 잔량 [%s]" % (recvvalue[44]))
print("총매수호가 잔량 증감 [%s]" % (recvvalue[55]))
print("시간외 총매도호가 잔량 [%s]" % (recvvalue[45]))
print("시간외 총매수호가 증감 [%s]" % (recvvalue[46]))
print("시간외 총매도호가 잔량 [%s]" % (recvvalue[56]))
print("시간외 총매수호가 증감 [%s]" % (recvvalue[57]))
print("예상 체결가 [%s]" % (recvvalue[47]))
print("예상 체결량 [%s]" % (recvvalue[48]))
print("예상 거래량 [%s]" % (recvvalue[49]))
print("예상체결 대비 [%s]" % (recvvalue[50]))
print("부호 [%s]" % (recvvalue[51]))
print("예상체결 전일대비율 [%s]" % (recvvalue[52]))
print("누적거래량 [%s]" % (recvvalue[53]))
print("주식매매 구분코드 [%s]" % (recvvalue[58]))
"""
# 국내주식체결처리 출력라이브러리
def stockspurchase(data_cnt, data):
print("============================================")
menulist = "유가증권단축종목코드|주식체결시간|주식현재가|전일대비부호|전일대비|전일대비율|가중평균주식가격|주식시가|주식최고가|주식최저가|매도호가1|매수호가1|체결거래량|누적거래량|누적거래대금|매도체결건수|매수체결건수|순매수체결건수|체결강도|총매도수량|총매수수량|체결구분|매수비율|전일거래량대비등락율|시가시간|시가대비구분|시가대비|최고가시간|고가대비구분|고가대비|최저가시간|저가대비구분|저가대비|영업일자|신장운영구분코드|거래정지여부|매도호가잔량|매수호가잔량|총매도호가잔량|총매수호가잔량|거래량회전율|전일동시간누적거래량|전일동시간누적거래량비율|시간구분코드|임의종료구분코드|정적VI발동기준가"
menustr = menulist.split('|')
pValue = data.split('^')
i = 0
for cnt in range(data_cnt): # 넘겨받은 체결데이터 개수만큼 print 한다
## print("### [%d / %d]" % (cnt + 1, data_cnt))
for menu in menustr:
## print("%-13s[%s]" % (menu, pValue[i]))
i += 1
# 국내주식체결통보 출력라이브러리
def stocksigningnotice_domestic(data, key, iv):
# AES256 처리 단계
aes_dec_str = aes_cbc_base64_dec(key, iv, data)
pValue = aes_dec_str.split('^')
if pValue[13] == '2': # 체결통보
## print("#### 국내주식 체결 통보 ####")
menulist = "고객ID|계좌번호|주문번호|원주문번호|매도매수구분|정정구분|주문종류|주문조건|주식단축종목코드|체결수량|체결단가|주식체결시간|거부여부|체결여부|접수여부|지점번호|주문수량|계좌명|호가조건가격|주문거래소구분|실시간체결창표시여부|필러|신용구분|신용대출일자|체결종목명40|주문가격"
menustr1 = menulist.split('|')
else:
## print("#### 국내주식 주문·정정·취소·거부 접수 통보 ####")
menulist = "고객ID|계좌번호|주문번호|원주문번호|매도매수구분|정정구분|주문종류|주문조건|주식단축종목코드|주문수량|주문가격|주식체결시간|거부여부|체결여부|접수여부|지점번호|주문수량|계좌명|호가조건가격|주문거래소구분|실시간체결창표시여부|필러|신용구분|신용대출일자|체결종목명40|체결단가"
menustr1 = menulist.split('|')
i = 0
for menu in menustr1:
print("%s [%s]" % (menu, pValue[i]))
i += 1
def data_processor_worker(worker_id, data_queue, stock_code):
"""
데이터 처리 전용 워커 프로세스
Args:
worker_id: 워커 식별 번호
data_queue: 메인 프로세스로부터 데이터를 받을 큐
stock_code: 담당 종목코드
"""
import sys
process_id = os.getpid()
print(f"\n{'#'*80}")
print(f"[Processor-{worker_id}] 시작: PID={process_id}, 담당종목={stock_code}")
print(f"{'#'*80}\n")
processed_count = 0
message = None # 에러 추적용
error_count = 0
try:
while True:
try:
# 큐에서 데이터 가져오기 (타임아웃 1초)
message = data_queue.get(timeout=1.0)
if message is None: # 종료 신호
print(f"[Processor-{worker_id}] [PID:{process_id}] 종료 신호 수신")
break
# 메시지 유효성 검사
if not isinstance(message, dict):
print(f"[Processor-{worker_id}] [PID:{process_id}] 잘못된 메시지 타입: {type(message)}, 내용: {message}")
continue
msg_type = message.get('type')
data = message.get('data')
source_stock = message.get('stock_code')
if not msg_type:
print(f"[Processor-{worker_id}] [PID:{process_id}] 메시지 타입 없음: {message}")
continue
if not source_stock:
print(f"[Processor-{worker_id}] [PID:{process_id}] 종목코드 없음: {message}")
continue
# 자신이 담당하는 종목의 데이터만 처리
if source_stock != stock_code:
continue
processed_count += 1
if msg_type == 'hoka':
print(f"[Processor-{worker_id}] [PID:{process_id}] [종목:{stock_code}] "
f"호가 데이터 처리 중... (처리건수: {processed_count})")
stockhoka(data)
elif msg_type == 'chegyeol':
data_cnt = message.get('data_cnt', 1)
print(f"[Processor-{worker_id}] [PID:{process_id}] [종목:{stock_code}] "
f"체결 데이터 처리 중... (체결건수: {data_cnt}, 총처리: {processed_count})")
stockspurchase(data_cnt, data)
elif msg_type == 'signing_notice':
aes_key = message.get('aes_key')
aes_iv = message.get('aes_iv')
print(f"[Processor-{worker_id}] [PID:{process_id}] [종목:{stock_code}] "
f"체결통보 처리 중... (처리건수: {processed_count})")
stocksigningnotice(data, aes_key, aes_iv)
except Exception as e:
error_count += 1
error_msg = str(e)
# Queue.Empty는 정상 동작
if "Empty" in str(type(e).__name__):
continue
# 기타 에러는 상세 출력
import traceback
error_details = traceback.format_exc()
print(f"\n{'!'*80}")
print(f"[Processor-{worker_id}] [PID:{process_id}] 🔴 처리 에러 발생 (에러 #{error_count})")
print(f" 에러 타입: {type(e).__name__}")
print(f" 에러 메시지: '{error_msg}' (길이: {len(error_msg)})")
print(f" 에러 repr: {repr(e)}")
if message:
print(f" 문제 메시지 타입: {type(message)}")
try:
print(f" 문제 메시지 내용: {message}")
except:
print(f" 문제 메시지 내용: [출력 불가]")
else:
print(f" 문제 메시지: None")
print(f"\n 상세 스택 트레이스:")
print(error_details)
print(f"{'!'*80}\n")
# 에러가 너무 많으면 종료
if error_count > 100:
print(f"[Processor-{worker_id}] [PID:{process_id}] 에러 과다 발생으로 종료")
break
continue
except KeyboardInterrupt:
print(f"[Processor-{worker_id}] [PID:{process_id}] 인터럽트로 종료")
except Exception as e:
print(f"[Processor-{worker_id}] [PID:{process_id}] 치명적 에러: {e}")
import traceback
traceback.print_exc()
print(f"[Processor-{worker_id}] [PID:{process_id}] 종료. 총 처리건수: {processed_count}, 에러: {error_count}")
def parse_stock_code_from_data(raw_data, trid):
"""
실시간 데이터에서 종목코드 추출
Args:
raw_data: 파이프로 구분된 실시간 데이터
trid: TR ID (H0STCNT0, H0STASP0 등)
Returns:
종목코드 (6자리)
"""
try:
# 디버깅: 처음 3번만 전체 데이터 출력
if not hasattr(parse_stock_code_from_data, 'debug_count'):
parse_stock_code_from_data.debug_count = 0
if parse_stock_code_from_data.debug_count < 3:
print(f"\n[DEBUG parse_stock_code] TR_ID: {trid}")
print(f"[DEBUG parse_stock_code] Raw data: {raw_data[:200]}")
parse_stock_code_from_data.debug_count += 1
# 한국투자증권 실시간 데이터는 ^ 구분자로 필드가 나뉨
fields = raw_data.split('^')
if parse_stock_code_from_data.debug_count <= 3:
print(f"[DEBUG parse_stock_code] 필드 개수: {len(fields)}")
print(f"[DEBUG parse_stock_code] 첫 5개 필드: {fields[:5]}\n")
if len(fields) > 0:
# 첫 번째 필드가 보통 종목코드
stock_code = fields[0].strip()
# 종목코드 검증 (6자리 숫자)
if stock_code.isdigit() and len(stock_code) == 6:
return stock_code
# 파싱 실패 시 None 반환
return None
except Exception as e:
print(f"[parse_stock_code] 에러: {e}, data: {raw_data[:100]}")
return None
async def websocket_receiver(url, approval_key, stock_codes, data_queues, custtype='P'):
"""
단일 웹소켓 세션으로 여러 종목 데이터 수신
수신한 데이터를 각 프로세스의 큐에 분배
Args:
url: 웹소켓 URL
approval_key: 승인 키
stock_codes: 구독할 종목 리스트
data_queues: 각 워커의 데이터 큐 딕셔너리 {종목코드: Queue}
custtype: 고객 타입
"""
main_pid = os.getpid()
aes_key = None
aes_iv = None
retry_count = 0
max_retries = 5
while retry_count < max_retries:
try:
print(f"\n{'='*80}")
print(f"[WebSocket Main] PID: {main_pid}")
print(f"[WebSocket Main] 연결 시도... URL: {url}")
print(f"[WebSocket Main] 구독 종목: {', '.join(stock_codes)}")
print(f"{'='*80}\n")
async with websockets.connect(url, ping_interval=None) as websocket:
# 웹소켓 세션 정보
local_address = websocket.local_address
remote_address = websocket.remote_address
print(f"\n{'*'*80}")
print(f"[WebSocket Main] 세션 정보")
print(f" - 메인 프로세스 PID: {main_pid}")
print(f" - 로컬 주소: {local_address[0]}:{local_address[1]}")
print(f" - 원격 주소: {remote_address[0]}:{remote_address[1]}")
print(f" - 웹소켓 객체 ID: {id(websocket)}")
print(f" - 구독 종목 수: {len(stock_codes)}")
print(f"{'*'*80}\n")
# 각 종목에 대해 구독 요청 전송
for i, stock_code in enumerate(stock_codes):
await asyncio.sleep(0.5) # 요청 간격
senddata = {
"header": {
"approval_key": approval_key,
"custtype": custtype,
"tr_type": "1",
"content-type": "utf-8"
},
"body": {
"input": {
"tr_id": "H0STCNT0", # 주식체결
"tr_key": stock_code
}
}
}
senddata_str = json.dumps(senddata, ensure_ascii=False)
print(f"[WebSocket Main] [{i+1}/{len(stock_codes)}] 구독 요청: {stock_code}")
await websocket.send(senddata_str)
retry_count = 0 # 연결 성공 시 재시도 카운트 초기화
subscribe_count = 0
# 데이터 수신 및 분배 루프
print(f"\n[WebSocket Main] 데이터 수신 대기 중...\n")
while True:
try:
data = await asyncio.wait_for(websocket.recv(), timeout=30.0)
except asyncio.TimeoutError:
print(f"[WebSocket Main] 타임아웃 - 연결 유지 중...")
continue
# 실시간 데이터 처리 (0 또는 1로 시작)
if data[0] in ('0', '1'):
recvstr = data.split('|')
if len(recvstr) < 4:
print(f"[WebSocket Main] Invalid data: {data}")
continue
trid0 = recvstr[1]
raw_data = recvstr[3]
# 실제 데이터에서 종목코드 추출
target_stock = parse_stock_code_from_data(raw_data, trid0)
# 종목코드 추출 실패 또는 구독하지 않은 종목
if not target_stock:
print(f"[WebSocket Main] ⚠️ 종목코드 추출 실패: {raw_data[:50]}...")
continue
if target_stock not in data_queues:
print(f"[WebSocket Main] ⚠️ 구독하지 않은 종목: {target_stock}")
continue
if data[0] == '0':
# 주식호가
if trid0 == "H0STASP0":
message = {
'type': 'hoka',
'data': raw_data,
'stock_code': target_stock
}
data_queues[target_stock].put(message)
print(f"[WebSocket Main] 호가 데이터 → Processor (종목: {target_stock})")
# 주식체결
elif trid0 == "H0STCNT0":
data_cnt = int(recvstr[2])
message = {
'type': 'chegyeol',
'data': raw_data,
'data_cnt': data_cnt,
'stock_code': target_stock
}
data_queues[target_stock].put(message)
print(f"[WebSocket Main] 체결 데이터 → Processor (종목: {target_stock}, 건수: {data_cnt})")
elif data[0] == '1':
# 주식체결 통보
if trid0 in ("K0STCNI0", "K0STCNI9", "H0STCNI0", "H0STCNI9"):
if aes_key and aes_iv:
message = {
'type': 'signing_notice',
'data': raw_data,
'aes_key': aes_key,
'aes_iv': aes_iv,
'stock_code': target_stock
}
data_queues[target_stock].put(message)
print(f"[WebSocket Main] 체결통보 → Processor (종목: {target_stock})")
# JSON 메시지 처리
else:
try:
jsonObject = json.loads(data)
trid = jsonObject["header"]["tr_id"]
# PINGPONG 처리
if trid == "PINGPONG":
print(f"[WebSocket Main] RECV [PINGPONG]")
await websocket.send(data)
print(f"[WebSocket Main] SEND [PINGPONG]")
# 일반 응답 처리
else:
rt_cd = jsonObject["body"]["rt_cd"]
tr_key = jsonObject["header"]["tr_key"]
msg = jsonObject["body"].get("msg1", "")
if rt_cd == '1': # 에러
print(f"[WebSocket Main] ❌ ERROR [종목:{tr_key}] MSG [{msg}]")
elif rt_cd == '0': # 정상
print(f"[WebSocket Main] ✓ SUCCESS [종목:{tr_key}] MSG [{msg}]")
if "SUBSCRIBE SUCCESS" in msg or "SUCCESS" in msg:
subscribe_count += 1
print(f"[WebSocket Main] 🎉 구독 완료! ({subscribe_count}/{len(stock_codes)})")
# AES 키 저장
if trid in ("H0STCNI0", "H0STCNI9"):
aes_key = jsonObject["body"]["output"]["key"]
aes_iv = jsonObject["body"]["output"]["iv"]
print(f"[WebSocket Main] AES KEY 저장: {aes_key[:20]}...")
except json.JSONDecodeError as e:
print(f"[WebSocket Main] JSON error: {e}")
except KeyError as e:
print(f"[WebSocket Main] Key error: {e}")
except websockets.exceptions.ConnectionClosed as e:
print(f"[WebSocket Main] 연결 종료: {e}")
retry_count += 1
await asyncio.sleep(2 ** retry_count)
except Exception as e:
print(f"[WebSocket Main] Exception: {e}")
import traceback
traceback.print_exc()
retry_count += 1
await asyncio.sleep(2 ** retry_count)
print(f"[WebSocket Main] 최대 재시도 횟수 초과")
def main():
"""메인 함수"""
# API 키 설정
g_appkey = '앱키 입력해주세요'
g_appsecret = '앱시크릿키 입력해주세요'
custtype = 'P' # 개인
url = 'ws://ops.koreainvestment.com:21000' # 실전투자계좌
# url = 'ws://ops.koreainvestment.com:31000' # 모의투자계좌
# 모니터링할 종목 리스트
stock_codes = [
'005930', # 삼성전자
'000660', # SK하이닉스
'035420', # NAVER
'005380', # 현대차
'051910', # LG화학
]
print(f"\n{'='*80}")
print(f"메인 프로세스 PID: {os.getpid()}")
print(f"구독 종목: {', '.join(stock_codes)}")
print(f"{'='*80}\n")
try:
# 1. Approval key 발급
print("=== Approval Key 발급 중 ===")
approval_key = get_approval(g_appkey, g_appsecret)
print(f"Approval Key: {approval_key}\n")
# 2. 각 종목별 데이터 큐 생성 (Manager 사용)
manager = Manager()
data_queues = {}
for stock_code in stock_codes:
data_queues[stock_code] = manager.Queue(maxsize=1000)
# 3. 데이터 처리 워커 프로세스 생성
processes = []
for i, stock_code in enumerate(stock_codes):
worker_id = i + 1
p = Process(
target=data_processor_worker,
args=(worker_id, data_queues[stock_code], stock_code)
)
p.start()
processes.append(p)
print(f"✓ Processor-{worker_id} 시작: PID={p.pid}, 종목={stock_code}")
time.sleep(0.2)
print(f"\n=== 모든 프로세서 시작 완료 ({len(processes)}개) ===\n")
# 4. 웹소켓 수신 시작 (메인 프로세스에서 실행)
print("=== 웹소켓 연결 시작 ===\n")
asyncio.run(websocket_receiver(url, approval_key, stock_codes, data_queues, custtype))
except KeyboardInterrupt:
print("\n\n=== 프로그램 종료 중 ===")
# 모든 워커에 종료 신호 전송
for stock_code in stock_codes:
try:
data_queues[stock_code].put(None)
except:
pass
# 프로세스 종료 대기
for p in processes:
p.join(timeout=2)
if p.is_alive():
p.terminate()
print("모든 프로세서 종료 완료")
except Exception as e:
print(f"메인 프로세스 에러: {e}")
import traceback
traceback.print_exc()
for p in processes:
p.terminate()
if __name__ == "__main__":
main()