일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 | 31 |
- 서보모터
- 시간적 일관성
- 인공지능
- PYTHON
- LLM
- ubuntu
- 뉴럴링크
- 오픈AI
- 생성형 AI
- tts
- 멀티모달
- AI 기술
- 아두이노
- 확산 모델
- ChatGPT
- 딥마인드
- 오블완
- javascript
- LORA
- 트랜스포머
- 메타
- ControlNet
- 티스토리챌린지
- 일론 머스크
- 우분투
- 이미지 편집
- 가상환경
- AI
- TRANSFORMER
- OpenAI
- Today
- Total
AI 탐구노트
[주식] KRX 300 맵 만들어보기 본문
지난 글에서 PyKrx 패키지를 이용해서 데이터를 수집해 봤습니다.
이걸 가지고 finviz 사이트에서 제공하는 S&P 500 Map과 같은 것을 만들어 볼 생각입니다.
증시맵 (또는 지수맵)이란
증시맵이란 업종, 종목, 시가총액 비중 등 실시간 및 추세 정보를 직관적으로 시각화해서 보여주는 지도입니다. 이런 서비스를 제공하는 곳으로 대표적인 곳은 finviz와 TradingView 같은 곳이 있죠.
국내에서도 kospd.com 이라는 곳에서 KRX 300 지수 맵을 구현해서 서비스를 제공하기도 합니다.
아래는 앞서 언급한 곳들에서 제공하는 사이트들입니다.
있는데 왜 만들어?
이미 잘 제공되고 있는 서비스가 있는데 왜 굳이 만들어 보려는건지...
솔직히 저도 궁금했습니다. 재미있어서 하는건데 이유를 대려하니 그래서 억지로 핑계를 찾아봤죠. ^^;
'코드가 공개되어 있으면 안 해 봤을텐데... 없더라구요.'
그래, 시작해보자
우선 지수맵을 만들기 위해 뭐가 필요한지 확인해 봤습니다.
필요한 데이터 모으기
이런 지수맵은 해당 인덱스를 구성하는 종목들의 시가총액, 종목구분(카테고리?), 주가, 등락률 등의 데이터를 이용합니다.
이 데이터들 각각은 맵 구성에서 다음과 같은 역할을 합니다.
- 시가총액 : 전체에서 차지하는 비중을 알려줍니다. 사각박스의 크기로 나타나죠
- 종목구분 : 해당 종목이 속한 업종으로 분류됩니다. 같은 것들은 함께 모여있겠죠.
- 주가 : 주가변동과 주가는 마우스가 올려지면 툴팁처럼 제공됩니다.
- 등락률 : 전일 대비 변화폭을 보여줍니다. 화면 상에 표시되고 색상으로도 보여집니다. (미국은 한국과 반대...)
지난 번 작업에서 수집했던 데이터에 대부분의 정보는 다 있습니다. 다만, 종목구분을 하기 위한 sector 값이 없더군요.
시장코드라고 해서 krx300을 임의로 넣었었는데 사실 이것은 코스피, 코스닥 등에서 업종 구분을 하기 위해 사용하는 'sector'하고는 달라 이 데이터를 찾아서 추가하는 작업이 추가로 필요했습니다.
데이터, 다시 생각해 보기
그런데...
지난 번 데이터 수집할 때의 과정을 생각해봤고 뭔가 간단하지 않은 방식이었단 느낌이 들었죠.
종목명, 당일 주가 정보, 시가총액 등이 각각 다른 PyKrx 함수를 이용해서 수집해 와서 이를 합치는 과정이 있어야 했기 때문이죠.
거기에 또 추가로 업종명이 있어야 하는 거고... 흠...
다른 좀 더 간단한 방법을 찾아봤고 그 하나가 KRX 정보데이터시스템을 이용하는 것이었습니다.
업종분류를 찾다보니 이쪽으로 어쩔 수 없이 가게 되었기 때문이죠.
'주식 > 안내 > 업종분류 현황' 메뉴에서 KOSPI와 KOSDAQ 각각의 종목별 정보를 CSV 파일로 다운받을 수 있었습니다.
여기에 제가 원하던 종목코드, 종목명, 시장구분, 업종명, 종가, 등락률, 시가총액 등 주가맵을 만들기 위한 정보가 다 있었습니다~!
일단 KOSPI와 KOSDAQ 에 관한 2개의 CSV 파일을 다운받습니다.
저장할 때 날짜 별로 구분하기 위해 파일명은 각각 kospi_{날짜}.csv, kosdaq_{날짜}.csv 형태로 합니다.
데이터 가공하기
이제 받은 데이터를 주식맵에서 사용하기 위한 형태로 가공하면 되겠군요.
데이터 가공 절차는 다음과 같이 진행됩니다.
- 업종 정보 파일 (kospi, kosdaq)을 읽어서 합칩니다.
국내 기관들은 UTF-8 인코딩이 아닌 EUC-KR 인코딩을 이용하는 경우가 아직도 많으므로 파일을 읽을 때 고려해야 합니다. - KRX300 ticker(종목코드) 목록을 가져와 여기에 있는 것만 앞서의 데이터에서 추려냅니다.
- d3 treemap 차트에서 필요로 하는 json 데이터 형식으로 가공해서 파일로 저장합니다.
d3 Treemap에서는 name, value 값만 사용하는데 저는 종가, 등락률 등의 정보도 필요해서 일단 넣었습니다.
위 내용이 반영된 코드는 다음과 같습니다. 날짜는 9/20로 설정했습니다. (금요일)
from datetime import datetime, timedelta
import pandas as pd
from pykrx import stock
import json
date = '20240920'
# kospi, kosdaq 주가정보 파일 : 사이트에서 1회성 수작업
# URL : http://data.krx.co.kr/contents/MDC/MDI/mdiLoader/index.cmd?menuId=MDC0201020506
# KOSPI, KOSDAQ 회사 정보, 당일 주가 정보 읽기 [종목코드/종목명/시장구분/업종명/종가/대비/등락률/시가총액]
df_kospi = pd.read_csv(f'kospi_{date}.csv', encoding='EUC-KR',dtype={'종목코드': str})
df_kosdaq = pd.read_csv(f'kosdaq_{date}.csv', encoding='EUC-KR',dtype={'종목코드': str})
df_all = pd.concat([df_kospi,df_kosdaq])
# KRX300 ticker list
tickers = stock.get_index_portfolio_deposit_file('5300')
# KRX300에 있는 것만 추려내서 dataframe 저장
df_krx300 = df_all[df_all['종목코드'].isin(tickers)]
# d3 차트 컴포넌트에서 요구하는 json 데이터 구조로 변환
# 업종명으로 그룹화하여 JSON 구조로 변환
result = {
"name": "KRX 300",
"children": []
}
# 업종명으로 그룹화
grouped = df_krx300.groupby('업종명')
for group_name, group_data in grouped:
# 각 업종명 그룹에 해당하는 종목 정보 리스트 생성
children = []
for _, row in group_data.iterrows():
children.append({
"name": row['종목명'],
"value": row['시가총액'],
"ticker": row['종목코드'],
"close": row['종가'],
"percent_change": row['등락률']
})
# 최종 JSON에 추가
result['children'].append({
"name": group_name,
"children": children
})
# JSON 파일로 저장 (저장이 필요할 경우)
with open('krx_300.json', 'w', encoding='utf-8') as f:
json.dump(result, f, ensure_ascii=False, indent=4)
파일 내용 확인하기
저장된 Json 파일을 열어보니 다음과 같이 나오니 예상대로 된 것 같습니다.
{
"name": "KRX 300",
"children": [
{
"name": "건설업",
"children": [
{
"name": "DL이앤씨",
"value": 1151135284250,
"ticker": "375500",
"close": 29750,
"percent_change": -4.34
},
{
"name": "GS건설",
"value": 1639741348400,
"ticker": "006360",
"close": 19160,
"percent_change": 0.42
},
{
"name": "HDC현대산업개발",
"value": 1637797150500,
"ticker": "294870",
"close": 24850,
"percent_change": 2.9
},
{
"name": "대우건설",
"value": 1645865646480,
"ticker": "047040",
"close": 3960,
"percent_change": -1.37
},
{
"name": "한전KPS",
"value": 1827000000000,
"ticker": "051600",
"close": 40600,
"percent_change": -5.91
},
{
"name": "현대건설",
"value": 3446460926750,
"ticker": "000720",
"close": 30950,
"percent_change": -0.64
}
]
},
{
"name": "금속",
"children": [
{
...
차트 그리기
데이터가 준비됐으니 이제 차트를 그려봐야겠네요.
앞서 언급한 것처럼 차트 컴포넌트는 d3 TreeMap을 이용할 예정입니다.
기본으로는 sector별로 모아주고, name과 value 값을 박스 안에 그려주는 기능만 제공됩니다.
여기에 추가적으로 필요한 다음 기능들이 제공되도록 코드를 수정합니다.
- 주가 등락률에 따른 박스 색상 변경
- 박스 내 마우스가 위치하면 툴팁으로 정보 표시
- 박스 내 표시 텍스트 내용 변경 (value -> 등락률)
- 박스 내 표시 텍스트 크기를 박스 크기에 연동해서 변경
하나씩 해 보도록 하죠.
주가 등락률에 따라 박스의 색상 변경
-30% ~ 30% 까지 변동폭을 가지므로 이에 맞춰 색상 스케일을 사용합니다.
Finviz의 경우, -3%~3%, KOSPD의 경우, -5%~5% 스케일을 보여주고 있는데... 흠... 이건 왜 그런 건지 모르겠습니다.
색상 스케일은 기본은 min, 0, max로 3단계로 나뉘는데 이를 단계 수를 늘여서 좀 더 자연스럽게 변경할 수 있을 것 같긴 합니다.
일단은 6 구간으로 나눠봤습니다.
// 색상 스케일 (첨부된 이미지에서의 색상 코드 사용)
const colorScale = d3.scaleLinear()
.domain([-30, -20, -10, 0, 10, 20, 30]) // 단계별 percent_change 설정
.range(["#ff4500", "#ff6347", "#fa8072", "#2f4f4f", "#32cd32", "#7fff00", "#adff2f"]); // 첨부된 색상
박스 내 마우스가 오면 툴팁으로 정보 표시
박스 내에 마우스가 오게 되면 툴팁에 종가, 등락률을 출력하도록 합니다.
.on("mouseover", function(event, d) {
tooltip.style("visibility", "visible")
.html(`<strong>${d.data.name}</strong><br>종가: ${d.data.close}<br>등락률: ${d.data.percent_change}%`)
.style("left", (event.pageX + 10) + "px")
.style("top", (event.pageY - 20) + "px");
})
박스 내 표시 텍스트 내용 변경 (value -> 등락률) 및 텍스트 크기 조절
박스 내의 텍스트는 원래는 name (종목명), value (시가총액)이 표시되는데 value 대신에 등락률 (percent_change)로 변경합니다.
추가적으로 종목명과 등락률을 2줄로 표시하고 가운데 정렬을 하도록 설정합니다.
그리고, 텍스트의 크기를 박스의 크기에 맞춰 변경되고 글자들이 박스에서 벗어나지 않도록 설정합니다.
// 텍스트 추가: 2줄로 표시 (name + percent_change)
node.append("text")
.attr("x", d => (d.x1 - d.x0) / 2)
.attr("y", d => (d.y1 - d.y0) / 2 - 10) // 첫 번째 줄 (name)
.attr("dy", ".35em")
.attr("text-anchor", "middle")
.style("font-size", function(d) {
const boxHeight = d.y1 - d.y0;
const boxWidth = d.x1 - d.x0;
const textLength = d.data.name.length;
const percentLength = `${d.data.percent_change}%`.length;
const maxTextLength = Math.max(textLength, percentLength);
return Math.min(20, boxWidth / maxTextLength, boxHeight / 4) + "px"; // 텍스트 크기 조정
적용 결과 확인
흠... 이 정도면 괜찮게 나온 것 같긴 합니다.
물론 디테일에서 부족한 부분들이 보이긴 하지만, 그건 상용 서비스할 때나 필요한 것이고...
만들어진 것을 확인하려는 차원에서는 이 정도면 만족합니다. ^^
향후에는 종가가 아닌 현재주가로 해서, 전일 종가 대비 등락률을 계산, 시가총액도 계산해서 실시간 정보를 제공할 수도 있겠죠.
최종 코드 (index.html)
최종 코드는 다음과 같습니다.
<!DOCTYPE html>
<meta charset="utf-8">
<style>
body {
font-family: "Arial", sans-serif;
}
.node {
border: solid 1px white;
box-sizing: border-box;
font-size: 10px;
padding: 4px;
overflow: hidden;
text-align: center;
}
.tooltip {
position: absolute;
text-align: center;
width: auto;
height: auto;
padding: 8px;
font: 12px sans-serif;
background: lightgray;
border: 1px solid #999;
border-radius: 5px;
pointer-events: none;
visibility: hidden;
}
text {
fill: white; /* 텍스트 색상을 흰색으로 설정 */
font-weight: bold; /* 텍스트를 bold로 설정 */
}
</style>
<body>
<script src="https://d3js.org/d3.v6.min.js"></script>
<script>
// Treemap 차트 생성 코드 시작
// 데이터 파일 로드
fetch('krx_300.json')
.then(response => response.json())
.then(data => {
const width = 1200;
const height = 800;
// 툴팁 생성
const tooltip = d3.select("body").append("div")
.attr("class", "tooltip");
// 색상 스케일 (첨부된 이미지에서의 색상 코드 사용)
const colorScale = d3.scaleLinear()
.domain([-30, -20, -10, 0, 10, 20, 30]) // 단계별 percent_change 설정
.range(["#ff4500", "#ff6347", "#fa8072", "#2f4f4f", "#32cd32", "#7fff00", "#adff2f"]); // 첨부된 색상
const treemap = d3.treemap()
.size([width, height])
.padding(1)
.round(true);
const root = d3.hierarchy(data)
.eachBefore(d => { d.data.id = (d.parent ? d.parent.data.id + "." : "") + d.data.name; })
.sum(d => d.value)
.sort((a, b) => b.value - a.value);
treemap(root);
const svg = d3.create("svg")
.attr("width", width)
.attr("height", height)
.style("font", "10px sans-serif");
const node = svg.selectAll("g")
.data(root.leaves())
.join("g")
.attr("transform", d => `translate(${d.x0},${d.y0})`)
.on("mouseover", function(event, d) {
tooltip.style("visibility", "visible")
.html(`<strong>${d.data.name}</strong><br>종가: ${d.data.close}<br>등락률: ${d.data.percent_change}%`)
.style("left", (event.pageX + 10) + "px")
.style("top", (event.pageY - 20) + "px");
})
.on("mousemove", function(event) {
tooltip.style("left", (event.pageX + 10) + "px")
.style("top", (event.pageY - 20) + "px");
})
.on("mouseout", function() {
tooltip.style("visibility", "hidden");
});
node.append("rect")
.attr("id", d => d.data.id)
.attr("fill", d => colorScale(d.data.percent_change))
.attr("width", d => d.x1 - d.x0)
.attr("height", d => d.y1 - d.y0);
// 텍스트 추가: 2줄로 표시 (name + percent_change)
node.append("text")
.attr("x", d => (d.x1 - d.x0) / 2)
.attr("y", d => (d.y1 - d.y0) / 2 - 10) // 첫 번째 줄 (name)
.attr("dy", ".35em")
.attr("text-anchor", "middle")
.style("font-size", function(d) {
const boxHeight = d.y1 - d.y0;
const boxWidth = d.x1 - d.x0;
const textLength = d.data.name.length;
const percentLength = `${d.data.percent_change}%`.length;
const maxTextLength = Math.max(textLength, percentLength);
return Math.min(20, boxWidth / maxTextLength, boxHeight / 4) + "px"; // 텍스트 크기 조정
})
.text(d => d.data.name);
node.append("text")
.attr("x", d => (d.x1 - d.x0) / 2)
.attr("y", d => (d.y1 - d.y0) / 2 + 10) // 두 번째 줄 (percent_change)
.attr("dy", ".35em")
.attr("text-anchor", "middle")
.style("font-size", function(d) {
const boxHeight = d.y1 - d.y0;
const boxWidth = d.x1 - d.x0;
const textLength = `${d.data.percent_change}%`.length;
return Math.min(15, boxWidth / textLength, boxHeight / 4) + "px"; // 텍스트 크기 조정
})
.text(d => `${d.data.percent_change}%`);
document.body.appendChild(svg.node());
})
.catch(error => {
console.error('Error fetching the JSON data:', error);
});
</script>
</body>
앞서 생성된 krx_300.json 파일과 동일한 폴더에 넣어두고 해당 폴더 내에서 다음을 실행합니다.
그리고나서 브라우저에서 http://localhost:8080을 열어 확인하면 됩니다.
$ python3 -m http.server 8080
이번 글은 여기까지 하겠습니다. 다음에 또 다른 재미난 것을 만들어 봐야 겠습니다.
'DIY 테스트' 카테고리의 다른 글
Python을 이용한 파워포인트 다루기 (2) | 2024.09.24 |
---|---|
Fish-Speech를 이용한 음성 생성 테스트 (1) | 2024.09.23 |
주가 정보 가져오기 (4) | 2024.09.21 |
PyKrx 패키지 제공 함수 테스트 (2) | 2024.09.21 |
SAM2 (Segment Anything 2) 돌려 보기 (0) | 2024.09.20 |