WebSocket 을 활용한 주가정보 실시간 조회 작업
대쉬보드에서 주식 종목 정보를 보여주는 부분이 존재하는데 주가정보는 전일 오후 3시 30분, 장이 마감되고 난 뒤에 일괄 업데이트 된 정보만을 보여주다보니 당일 실시간으로 변하고 있는 주가정보를 반영해주지 못하고 있었습니다.
20분 지연시세도 아닌 전일 종가라서 당일 변동에 따른 수익률의 변화를 실시간으로 반영하지 못하고 있는 부분으로 인해 이 부분을 개선해주기로 하였습니다.
주가정보는 서버에서 가져오게 하고, 뷰단에서는 수신된 주가정보를 업데이트 하도록 했습니다.
웹페이지 접속 시 웹소켓을 이용해 서버로 접속 요청 이후 접속 성공 시 주가정보를 내려받을 종목 코드 리스트를 넘겨주도록 합니다.
서버는 python 으로 작성되었으며 주가정보는 FinanceDataReader 를 통해 가져오도록 합니다. 키움이나 한투에서 제공해주는 API를 이용하면 실시간으로 주가변동에 대한 정보를 내려받는 함수를 제공해주는데 복잡해지는 부분을 감안해 FinanceDataReader 를 활용했습니다.
뷰페이지는 Spring Framework 로 구동되는 웹서버상에서 jsp 페이지 내에서 그려주고 있는데 예제는 간단히 HTML 로 구성해봅니다.
PYTHON 서버 코드
StockServer 객체는 웹소켓서버를 실행하고 클라이언트 접속 시 주가정보 요청을 위한 종목 코드 리스트를 받은 뒤 3초마다 해당 주가의 종가 정보를 조회 한 뒤 클라이언트로 내려주도록 설계되어 있습니다.
stock_server.py 파일
import random
import asyncio
import websockets
import FinanceDataReader as fdr
import json
class StockServer:
def __init__(self, host='localhost', port=8766):
self.host = host
self.port = port
print(f"StockServer started at {host}:{port}")
async def send_stock_prices(self, websocket, path):
await websocket.send(json.dumps({"message": "서버에 연결되었습니다."}))
try:
message = await websocket.recv()
# 조회할 주가 코드 리스트 내려받기
stock_list = json.loads(message)
await websocket.send(json.dumps({"message": "주가 정보 전송 시작"}))
# 주가 데이터를 3초마다 조회 후 전송
while True:
stock_data = self.get_stock_data(stock_list)
await websocket.send(json.dumps(stock_data)) # 주가 정보 전송
await asyncio.sleep(5) # 3초 대기
except Exception as e:
print(f"Error: {e}")
finally:
print("클라이언트 연결 종료")
def get_stock_data(self, stock_list):
stock_data = {}
for stock in stock_list:
try:
# 주가 데이터 가져오기
df = fdr.DataReader(stock)
# 주가 정보 변경 시 뷰페이지 상태 업데이트를 확인하기 위해
# 마지막 종가에 10원~100원 랜덤 값 더하기
stock_data[stock] = int(df['Close'].iloc[-1]) + random.randint(10, 100)
except Exception as e:
stock_data[stock] = str(e) # 오류 발생 시 오류 메시지 저장
return stock_data
async def start(self):
start_server = await websockets.serve(self.send_stock_prices, self.host, self.port)
await start_server.wait_closed()
main.py 에서는 메인 이벤트 루프를 통해 async def 로 선언되는 코루틴을 비동기적으로 실행시켜줍니다. 운영서버에서는 uvicorn 으로 실행되는 FastAPI 서버가 있어서 2개를 실행시켜 주도록 선언되어 있으나 예제에서는 해당 코드는 제거하고 웹소켓 단일 서버만 실행되도록 하였습니다.
main.py 파일
import stock_agent.StockServer as ss
import asyncio
async def run_stock_server():
stock_server = ss.StockServer()
await stock_server.start()
# uvicorn 을 통해 실행되는 서버
async def start_uvicorn():
...
if __name__ == "__main__":
# 새로운 이벤트 루프 생성
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
# 비동기적으로 실행
loop.run_until_complete(asyncio.gather(run_stock_server()))
뷰페이지 처리
뷰페이지 운영서버는 Spring 프레임워크 웹서버 베이스에 jsp로 화면을 그리고 있는데 예제는 심플하게 HTML5 websocket 으로 작성되므로 html 파일로 작성해줍니다.을 써서 작성해줍니다.
조회할 종목코드 리스트를 구성하는 로직은 예제에서 불필요해 기본적으로 3종목의 주가정보를 가져오도록 설정해두었습니다.
삼성전자(005930), SK하이닉스(0000660), 현대차(005380)
connect() 함수를 통해 localhost 8766 포트로 띄워둔 서버에 접속하게 됩니다. 서버 접속에 성공하면 '서버에 연결되었습니다.' 메시지를 출력하게 되며 서버는 종목코드 리스트를 수신할 준비 상태가 됩니다.
전송 버튼을 통해 종목코드 리스트를 전송하게 되면 3초마다 변경되어서 내려받는 주가정보를 확인하실 수 있습니다.
websocket.html 파일
<!DOCTYPE html>
<html>
<head>
<title>Stock Prices</title>
<style>
#errorMessages {
color: red;
margin-top: 10px;
}
</style>
<script>
let socket;
function connect() {
socket = new WebSocket('ws://localhost:8766');
socket.onopen = function () {
document.getElementById("stockPrices").innerHTML += "<br>서버에 연결되었습니다.<br>";
};
socket.onmessage = function (event) {
const stockPrices = JSON.parse(event.data);
document.getElementById("stockPrices").innerHTML += "<br>" + JSON.stringify(stockPrices) + "<br>";
};
socket.onerror = function (error) {
console.error("웹소켓 오류:", error);
displayErrorMessage("서버에 연결할 수 없습니다.");
};
socket.onclose = function (event) {
console.log("서버와의 연결이 종료되었습니다.", event);
displayErrorMessage("서버와의 연결이 끊어졌습니다.");
};
}
function requestStockPrices() {
//const stocks = document.getElementById("stockInput").value.split(',');
//const stockList = stocks.map(stock => stock.trim());
// 005930, 000660, 005380
const stockList = ['005930', '000660', '005380'];
var res = socket.send(JSON.stringify(stockList));
console.log(res);
}
function displayErrorMessage(message) {
const errorMessagesDiv = document.getElementById("errorMessages");
const currentTime = new Date().toLocaleTimeString();
errorMessagesDiv.innerHTML += `<p>[${currentTime}] ${message}</p>`;
}
</script>
</head>
<body onload="connect()">
<h1>Real-time Stock Prices</h1>
<input type="text" id="stockInput" placeholder="주식 코드 입력 (예: 005930, 000660)">
<button onclick="requestStockPrices()">조회</button>
<div id="stockPrices"></div>
<div id="errorMessages"></div>
</body>
</html>
websocket.html파일을 크롬에서 실행시켜 봅니다.
python 서버가 실행중이고 onload 이벤트에 connect() 함수가 실행되기 때문에 크롬에 페이지가 오픈되자마자 서버에 접속이 진행됩니다.
'조회' 버튼을 눌러서 주가를 수신할 종목리스트를 전송해주면 주가 정보가 수신이 됩니다.
3초마다 변경된 주가 정보가 화면에 출력이 되는데 현재는 단순히 뿌려주도록 처리되어 있습니다.
다음편에선 주가 정보 변경 시 변경된 걸 알아차릴 수 있도록 애니메이션을 살짝 넣은 버전으로 변경해보도록 하겠습니다.