728x90
반응형

결과화면
ApexChart.js
import { useEffect, useState, useRef } from "react";
import axios from "axios";
import Chart from "react-apexcharts";
import MaCalc from "../utils/MaCalc";
const MAX_CANDLES = 100;
function ApexChart() {
const [candles, setCandles] = useState([]);
const wsRef = useRef(null);
/* =========================
* 1️⃣ 초기 캔들 로딩 (REST API)
* ========================= */
const loadInitialCandles = async () => {
try {
const res = await axios.get("http://localhost:8000/chart");
const sorted = res.data.sort(
(a, b) =>
new Date(a.candle_date_time_kst) - new Date(b.candle_date_time_kst)
);
const formatted = sorted.map(item => ({
timestamp: new Date(item.candle_date_time_kst).getTime(),
open: item.opening_price,
high: item.high_price,
low: item.low_price,
close: item.trade_price,
}));
setCandles(formatted.slice(-MAX_CANDLES));
} catch (err) {
console.error("초기 캔들 로딩 실패", err);
}
};
/* =========================
* 2️⃣ WebSocket 실시간 업데이트
* ========================= */
const connectWs = () => {
const ws = new WebSocket("ws://localhost:8000/ws/charts");
wsRef.current = ws;
ws.onopen = () => console.log("WS 연결됨");
ws.onmessage = (event) => {
try {
const t = JSON.parse(event.data);
const price = t.trade_price;
const ts = t.trade_timestamp;
setCandles(prev => {
if (prev.length === 0) return prev;
const list = [...prev];
const last = { ...list[list.length - 1] };
const lastMin = Math.floor(last.timestamp / 60000);
const nowMin = Math.floor(ts / 60000);
/* 새 분 시작 → 새 캔들 */
if (nowMin !== lastMin) {
if (list.length >= MAX_CANDLES) list.shift(); // 이전 캔들 삭제
list.push({
timestamp: ts,
open: price,
high: price,
low: price,
close: price,
});
return list;
}
/* 같은 분 → 기존 캔들 업데이트 */
last.high = Math.max(last.high, price);
last.low = Math.min(last.low, price);
last.close = price;
list[list.length - 1] = last;
return list;
});
} catch (err) {
console.error("WS JSON 파싱 오류:", err);
}
};
ws.onclose = () => {
console.log("WS 종료 → 재연결");
setTimeout(connectWs, 2000);
};
};
/* =========================
* 3️⃣ 컴포넌트 최초 실행
* ========================= */
useEffect(() => {
loadInitialCandles();
connectWs();
return () => {
if (wsRef.current) wsRef.current.close();
};
}, []);
if (candles.length === 0) return <div>Loading...</div>;
/* =========================
* 4️⃣ 시리즈용 데이터 변환
* ========================= */
const seriesData = candles.map(c => ({
x: new Date(c.timestamp),
y: [c.open, c.high, c.low, c.close],
}));
/* =========================
* 5️⃣ 이동평균
* ========================= */
const makeMa = (n) =>
MaCalc(candles.map(c => ({ trade_price: c.close })), n)
.map((v, i) =>
v == null ? null : { x: seriesData[i].x, y: v }
)
.filter(Boolean);
/* =========================
* 6️⃣ 볼린저밴드 계산
* ========================= */
const makeBollinger = (n = 20, k = 2) => {
if (candles.length < n) return { upper: [], lower: [], middle: [] };
const closes = candles.map(c => c.close);
const middle = MaCalc(candles.map(c => ({ trade_price: c.close })), n);
const upper = [];
const lower = [];
const middleSeries = [];
for (let i = 0; i < closes.length; i++) {
const x = seriesData[i].x;
if (i < n - 1 || middle[i] == null) {
continue; // 아직 볼린저 계산 불가 → skip
}
const window = closes.slice(i - n + 1, i + 1);
const mean = middle[i];
const std = Math.sqrt(window.reduce((acc, v) => acc + (v - mean) ** 2, 0) / n);
upper.push({ x, y: mean + k * std });
lower.push({ x, y: mean - k * std });
middleSeries.push({ x, y: mean });
}
return { upper, lower, middle: middleSeries };
};
const { upper: bbUpper, lower: bbLower, middle: bbMiddle } = makeBollinger(20, 2);
/* =========================
* 7️⃣ ApexCharts 시리즈
* ========================= */
const series = [
{ name: "캔들", type: "candlestick", data: seriesData },
{ name: "MA5", type: "line", data: makeMa(5) },
{ name: "MA10", type: "line", data: makeMa(10) },
{ name: "MA20", type: "line", data: makeMa(20) },
{ name: "MA60", type: "line", data: makeMa(60) },
{ name: "BB 상단", type: "line", data: bbUpper, color: "#ff1c1cff" },
{ name: "BB 중간", type: "line", data: bbMiddle, color: "#00aa00" },
{ name: "BB 하단", type: "line", data: bbLower, color: "#0000ff" },
];
/* =========================
* 8️⃣ 차트 옵션
* ========================= */
const options = {
chart: {
type: "line",
height: 450,
animations: { enabled: false },
},
xaxis: { type: "datetime" },
yaxis: {
labels: {
formatter: (value) => value == null ? "" : (value / 100000000).toFixed(3) + "억",
},
},
stroke: { width: [1, 2, 2, 2, 2, 1, 1, 1] },
tooltip: {
shared: true,
y: {
formatter: (value) => value == null ? "" : (value / 100000000).toFixed(2) + "억",
},
},
};
return <Chart options={options} series={series} height={450} />;
}
export default ApexChart;
① 과매수 / 과매도 판단
- 가격이 상단선 위로 치솟으면 과매수
- 가격이 하단선 아래로 떨어지면 과매도
② 변동성 확인
- 밴드 폭이 넓으면 변동성 ↑, 좁으면 변동성 ↓
- 밴드가 좁아지는 구간 → 이후 급격한 변동 가능성
③ 추세 추정
- 가격이 상단 밴드를 타고 상승 → 강한 상승 추세
- 가격이 하단 밴드를 타고 하락 → 강한 하락 추세
④ 트레이딩 전략 예시
- 반등 전략: 밴드 하단에서 매수 → 중앙선까지 상승
- 돌파 전략: 밴드 수축 → 상단 밴드 돌파 → 매수
볼린저 밴드의 중앙선 기준은 20일 이동평균을 가장 흔히 사용한다.
왜냐, 20일은 한 달(거래일 기준 약 한 달) 정도를 반영한다. 가격 평균이 과도하게 단기적이거나 장기적이지 않다. 또한, 표준편차의 2배로, 가격의 95퍼 정도가 밴드 안에서 들어오는 통계적 이유이다.
1️⃣ 함수 정의
const makeBollinger = (n = 20, k = 2) => { ... }
- n = 20 → 기간. 보통 20일(혹은 20분 등) 이동평균을 기준으로 볼린저밴드를 계산한다.
- k = 2 → 표준편차 배수. 상단 밴드 = 평균 + 2 * 표준편차, 하단 밴드 = 평균 - 2 * 표준편차.
- 이 함수는 candles 배열을 바탕으로 볼린저밴드 상단, 하단, 중간값을 계산해서 리턴한다.
2️⃣ 초기 체크
if (candles.length < n) return { upper: [], lower: [], middle: [] };
- 캔들 데이터가 n개보다 적으면 볼린저밴드 계산 불가
- 안전하게 빈 배열 반환
3️⃣ 데이터 준비
const closes = candles.map(c => c.close);
const middle = MaCalc(candles.map(c => ({ trade_price: c.close })), n);
- closes → 모든 캔들의 종가만 뽑아서 배열로 준비
- middle → 중간 이동평균(MA) 계산.
- MaCalc는 이전에 만든 함수로, n기간 동안 종가의 이동평균을 계산한다.
- 볼린저밴드의 중앙선이 이 값이다.
4️⃣ 결과 배열 준비
const upper = [];
const lower = [];
const middleSeries = [];
- 계산된 각 캔들별 볼린저 상단, 하단, 중앙 데이터를 저장할 배열이다.
- ApexCharts는 {x, y} 형태로 받으므로 이 배열에 {x: 시간, y: 값} 형태로 넣는다.
5️⃣ 메인 루프 (볼린저 계산)
for (let i = 0; i < closes.length; i++) {
const x = seriesData[i].x;
if (i < n - 1 || middle[i] == null) {
continue; // 아직 볼린저 계산 불가 → skip
}
const window = closes.slice(i - n + 1, i + 1);
const mean = middle[i];
const std = Math.sqrt(window.reduce((acc, v) => acc + (v - mean) ** 2, 0) / n);
upper.push({ x, y: mean + k * std });
lower.push({ x, y: mean - k * std });
middleSeries.push({ x, y: mean });
}
- x = seriesData[i].x
- 각 캔들의 시간 정보(Date)를 가져옵니다.
- ApexCharts 시리즈는 {x: Date, y: 값} 형태여야 하므로 필요합니다.
- if (i < n - 1 || middle[i] == null) continue;
- n개 미만이면 이동평균 계산 불가 → skip
- middle[i]가 null이면 계산 불가 → skip
- const window = closes.slice(i - n + 1, i + 1);
- 현재 캔들을 포함한 n개 구간 종가
- 표준편차 계산에 필요
- const mean = middle[i];
- 중앙선(중간 이동평균)
- 표준편차 계산:
const std = Math.sqrt(window.reduce((acc, v) => acc + (v - mean) ** 2, 0) / n);
- n기간 동안 각 종가가 평균에서 얼마나 떨어져 있는지 계산
- Math.sqrt → 제곱근 → 표준편차
6. 볼린저밴드 상/하단 계산:
upper.push({ x, y: mean + k * std });
lower.push({ x, y: mean - k * std });
middleSeries.push({ x, y: mean });
- 상단선 = 중앙선 + k * 표준편차
- 하단선 = 중앙선 - k * 표준편차
- 중앙선 = 중간 이동평균
6️⃣ 최종 반환
return { upper, lower, middle: middleSeries };
- 계산된 상단, 하단, 중앙선 배열을 객체로 반환
- ApexCharts에서 각각 시리즈로 사용할 수 있음
728x90
반응형
'✨ python > FastAPI' 카테고리의 다른 글
| [FastAPI] asyncio 사용하여 볼린저밴드 차트 구현하기 (0) | 2026.01.06 |
|---|---|
| [FastAPI, React] 골든크로스, 데드크로스 ( Telegram Bot) (1) | 2025.12.21 |
| [FastAPI, React] Telegram Bot 연동하기 (0) | 2025.12.21 |
| [FastAPI, Recat] 이전 데이터 + 실시간 데이터(WebSocket) 합하기 (3) (0) | 2025.12.14 |
| [FastAPI, Recat] 이전 데이터 + 실시간 데이터(WebSocket) 합하기 (2) (0) | 2025.12.12 |
댓글