차트 정의 완료

This commit is contained in:
2025-08-24 16:23:27 +09:00
parent 44f70df8c2
commit 04131e7506
11 changed files with 537 additions and 126 deletions

View File

@@ -10,104 +10,178 @@ import { ref } from 'vue';
*/
import LWChart from './components/LWChart.vue';
import {getCandleList} from '/@src/utils/api'
import {calculateMACD, calculateEMA, calculateBollingerBands} from "./utils/meter.js";
/**
* Generates sample data for the lightweight chart
* @param {Boolean} ohlc Whether generated dat should include open, high, low, and close values
* @returns {Array} sample data
*/
const socket = new WebSocket("ws://127.0.0.1:8765");
const sk = new WebSocket("wss://fx-ws.gateio.ws/v4/ws/usdt")
let oldLow = 9999999999;
let oldHigh = 0;
let lock = false
let socket;
const lazyLock = ref(false)
const contract = ref('BTC_USDT')
const time = ref('15m')
const chartOptions = ref({
layout: {
textColor: 'black',
background: { type: 'solid', color: 'white' },
panes: {
separatorColor: '#FF0000',
},
},
height: 800,
height: 1300,
timeScale: {
timeVisible: true, // 분/초까지 표시
secondsVisible: false // 초 숨기고 싶으면 false
},
});
const candleTick = ref([]);
const seriesOptions = ref({
const candleTick = ref([])
const ema = ref([])
const macd = ref({})
const bb = ref({})
const selectCoins = [
{ title: 'BTC_USDT'},
{ title: 'ETH_USDT'},
{ title: 'SOL_USDT'},
{ title: 'XRP_USDT'},
{ title: 'LINK_USDT'},
]
const selectTimes = [
{ title: '5m'},
{ title: '15m'},
{ title: '30m'},
{ title: '1h'},
{ title: '4h'},
{ title: '8h'},
{ title: '1d'},
{ title: '7d'},
]
const line_color = [
'#AD81FF',
'#FFBAB5',
'#6BFF74',
'#FFFC7B',
'#27FFE6',
'#225EFF'
]
const emaLineOptions = ref({
lineWidth: 2,
color: '#AD81FF',
title: 'EMA',
});
const candleOptions = ref({
upColor: '#26a69a',
downColor: '#ef5350',
borderVisible: false,
wickUpColor: '#26a69a',
wickDownColor: '#ef5350',
});
const macdLineOptions = ref({
lineWidth: 2,
color: '#FFBAB5'
});
const macdSignalOptions = ref({
lineWidth: 2,
color: '#AD81FF'
});
const macdHistogramOptions = ref({
lineWidth: 2,
color: '#27FFE6'
});
const upperBbOptions = ref({
color: '#006AFF', // 주황색 계열
lineStyle: 0, // 점선 스타일 (선택)
lineWidth: 2,
title: 'UpperBB',
})
const middleBbOptions = ref({
color: '#006AFF', // 주황색 계열
lineStyle: 1, // 점선 스타일 (선택)
lineWidth: 0,
title: 'MiddleBB(SMA)',
})
const lowerBbOptions = ref({
color: '#006AFF', // 주황색 계열
lineStyle: 0, // 점선 스타일 (선택)
lineWidth: 2,
title: 'LowerBB',
})
const chartType = ref('candlestick');
const lwChart = ref();
onMounted(() => {
getCandleList('BTC_USDT', '1m').then(data => {
candleTick.value = data
})
tradeSocket()
candleSocket('1m')
init(contract.value, time.value)
})
const tradeSocket = () => {
sk.addEventListener('open', (event) => {
sk.send(JSON.stringify({"time" : curToUnix(), "channel" : "futures.trades",
"event": "subscribe", "payload" : [contract.value]}))
const init = (cont, ti) => {
socket = new WebSocket("ws://127.0.0.1:8765");
const dt = new Date()
getCandleList(cont, ti, Math.round(dt.getTime()/1000)).then(data => {
ema.value = calculateEMA(data, 20)
macd.value = calculateMACD(data)
bb.value = calculateBollingerBands(data)
candleTick.value = data
console.log(bb.value)
})
sk.addEventListener('message', (message) => {
const getDa = JSON.parse(message.data)
if (getDa.event === 'update') {
// console.log(getDa.result[0].price)
candleSocket(cont, ti)
}
if(candleTick.value.length === 0) return
const distroy = (cont) => {
candleTick.value = []
}
const lastCandle = candleTick.value[candleTick.value.length - 1]
const lazyLoad = async (ti) => {
lazyLock.value = true
getCandleList(contract.value, time.value, ti).then(data => {
candleTick.value = [...data, ...candleTick.value]
ema.value = calculateEMA(data, 20)
macd.value = calculateMACD(data)
bb.value = calculateBollingerBands(data)
setTimeout(() => {lazyLock.value = false}, 3000)
const price = parseFloat(getDa.result[0].price);
lastCandle.close = price
if(oldLow > candleTick.value[candleTick.value.length - 1].close) {
lastCandle.low = price
oldLow = price;
}
if(oldHigh < candleTick.value[candleTick.value.length - 1].open) {
lastCandle.high = price
oldHigh = price;
}
candleTick.value[candleTick.value.length - 1] = lastCandle
candleTick.value = [...candleTick.value]
}
})
}
const candleSocket = (time) => {
const candleSocket = (cont, ti) => {
socket.addEventListener('open', (event) => {
socket.send(JSON.stringify({"type":"subscribe", "channel": contract.value, "time": time}))
socket.send(JSON.stringify({"type":"subscribe", "channel": cont, "time": ti}))
})
socket.addEventListener('message', (message) => {
lock = true
const getDa = JSON.parse(message.data)
const msg = JSON.parse(getDa.msg)
const tick = msg.data[0]
const msg = JSON.parse(message.data)
const item = JSON.parse(msg.msg)
const ticks = candleTick.value
if(tick.time !== ticks[ticks.length - 1].time) {
// ticks[ticks.length - 1] = tick
// candleTick.value = [...ticks]
// } else {
const ticks = candleTick.value
ticks.push(tick)
if(item.t === ticks[ticks.length - 1].t) {
ticks[ticks.length - 1] = item
candleTick.value = [...ticks]
} else {
ticks.push(item)
candleTick.value = [...ticks]
}
ema.value = calculateEMA(ticks, 20)
macd.value = calculateMACD(ticks)
bb.value = calculateBollingerBands(ticks)
lazyLock.value = false
})
}
@@ -120,14 +194,6 @@ const curToUnix = (() => {
})
function randomShade() {
return Math.round(Math.random() * 255);
}
const randomColor = (alpha = 1) => {
return `rgba(${randomShade()}, ${randomShade()}, ${randomShade()}, ${alpha})`;
};
const colorsTypeMap = {
area: [
['topColor', 0.4],
@@ -158,64 +224,82 @@ const colorsTypeMap = {
line: [['color', 1]],
};
// Set a random colour for the series as an example of how
// to apply new options to series. A similar appraoch will work on the
// option properties.
const changeColors = () => {
const options = {};
const colorsToSet = colorsTypeMap[chartType.value];
colorsToSet.forEach((c) => {
options[c[0]] = randomColor(c[1]);
});
seriesOptions.value = options;
};
watch(contract, newVal => {
distroy()
console.log(newVal);
init(newVal, time.value)
})
const changeData = () => {
const candlestickTypeData = ['candlestick', 'bar'].includes(chartType.value);
const newData = generateSampleData(candlestickTypeData);
data.value = newData;
if (chartType.value === 'baseline') {
const average =
newData.reduce((s, c) => {
return s + c.value;
}, 0) / newData.length;
seriesOptions.value = { baseValue: { type: 'price', price: average, format: 'yyyy-MM-dd HH:mm' } };
}
};
const changeType = () => {
const types = [
'line',
'area',
'baseline',
'histogram',
'candlestick',
'bar',
].filter((t) => t !== chartType.value);
const randIndex = Math.round(Math.random() * (types.length - 1));
chartType.value = types[randIndex];
changeData();
// call a method on the component.
lwChart.value.fitContent();
};
watch(time, newVal => {
distroy()
console.log(newVal);
init(contract.value, newVal)
})
</script>
<template>
<div>
<div class="chart-container">
<div class="container">
<div class="d-flex justify-content-center">
<v-menu>
<template v-slot:activator="{ props }">
<v-btn color="grey-lighten-5" min-width="100" size="small" v-bind="props">{{contract}}</v-btn>
</template>
<v-list v-model="contract">
<v-list-item
v-for="(item, idx) in selectCoins"
:key="idx"
:value="item.title"
@click="() => {
socket.send(JSON.stringify({'type':'unsubscribe', 'channel': contract, 'time': time}))
contract = item.title
}"
>
<v-list-item-title>{{ item.title }}</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
<v-menu>
<template v-slot:activator="{ props }">
<v-btn color="grey-lighten-5" min-width="50" size="small" v-bind="props">{{time}}</v-btn>
</template>
<v-list v-model="time">
<v-list-item
v-for="(item, idx) in selectTimes"
:key="idx"
:value="item.title"
@click="() => {
socket.send(JSON.stringify({'type':'unsubscribe', 'channel': contract, 'time': time}))
time = item.title
}"
>
<v-list-item-title>{{ item.title }}</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
</div>
<div class="chart-container" v-if="candleTick !== undefined">
<LWChart
:type="chartType"
:data="candleTick"
:ema="ema"
:macd="macd"
:bb="bb"
:autosize="true"
:chart-options="chartOptions"
:series-options="seriesOptions"
:ema-options="emaLineOptions"
:candle-options="candleOptions"
:macd-line-options="macdLineOptions"
:macd-signal-options="macdSignalOptions"
:macd-histogram-options="macdHistogramOptions"
:lower-bb-options="lowerBbOptions"
:middle-bb-options="middleBbOptions"
:upper-bb-options="upperBbOptions"
:lazyLock="lazyLock"
@lazyLoad="lazyLoad"
ref="lwChart"
/>
</div>
<button type="button" @click="changeColors">Set Random Colors</button>
<button type="button" @click="changeType">Change Chart Type</button>
<button type="button" @click="changeData">Change Data</button>
</div>
</template>
<style scoped>

View File

@@ -26,6 +26,16 @@ const props = defineProps({
type: Array,
required: true,
},
ema: {
type: Array,
required: true,
},
macd: {
type: Object,
},
bb: {
type: Object,
},
autosize: {
default: true,
type: Boolean,
@@ -33,7 +43,28 @@ const props = defineProps({
chartOptions: {
type: Object,
},
seriesOptions: {
emaOptions: {
type: Object,
},
candleOptions: {
type: Object,
},
macdLineOptions: {
type: Object,
},
macdSignalOptions: {
type: Object,
},
macdHistogramOptions: {
type: Object,
},
upperBbOptions: {
type: Object,
},
middleBbOptions: {
type: Object,
},
lowerBbOptions: {
type: Object,
},
timeScaleOptions: {
@@ -42,8 +73,14 @@ const props = defineProps({
priceScaleOptions: {
type: Object,
},
lazyLock: {
type: Boolean,
default: false,
},
});
const emits = defineEmits(['lazyLoad'])
function getChartSeriesDefinition(type) {
switch (type.toLowerCase()) {
case 'line':
@@ -64,9 +101,20 @@ function getChartSeriesDefinition(type) {
// Lightweight Charts™ instances are stored as normal JS variables
// If you need to use a ref then it is recommended that you use `shallowRef` instead
let series;
let emaLine;
let candlestick;
let chart;
let macdLine;
let macdSignalLine;
let macdHistogram;
let mainPane;
let macdPane;
let upperBb;
let middleBb;
let lowerBb;
const chartContainer = ref();
const fitContent = () => {
@@ -78,7 +126,7 @@ const getChart = () => {
return chart;
};
defineExpose({ fitContent, getChart });
defineExpose({fitContent, getChart});
// Auto resizes the chart when the browser window is resized.
const resizeHandler = () => {
@@ -89,15 +137,56 @@ const resizeHandler = () => {
// Creates the chart series and sets the data.
const addSeriesAndData = props => {
const seriesDefinition = getChartSeriesDefinition(props.type);
series = chart.addSeries(seriesDefinition, props.seriesOptions);
series.setData(props.data);
const candleStickDefinition = getChartSeriesDefinition(props.type);
candlestick = chart.addSeries(candleStickDefinition, props.candleOptions);
let oldT = 0;
let list = []
props.data.forEach(item => {
if (item.t !== oldT) {
list.push({
time: Number(item.t),
open: Number(item.o),
high: Number(item.h),
low: Number(item.l),
close: Number(item.c),
volume: Number(item.v),
})
}
oldT = item.t
})
candlestick.setData(list);
const seriesDefinition = getChartSeriesDefinition('line')
emaLine = chart.addSeries(seriesDefinition, props.emaOptions)
emaLine.setData(props.ema)
console.log(Object.keys(props.macd).length)
macdLine = macdPane.addSeries(seriesDefinition, props.macdLineOptions)
macdSignalLine = macdPane.addSeries(seriesDefinition, props.macdSignalOptions)
macdHistogram = macdPane.addSeries(getChartSeriesDefinition('histogram'), props.macdHistogramOptions)
if(Object.keys(props.macd).length) {
macdLine.setData(props.macd.m)
macdHistogram.setData(props.macd.h)
macdSignalLine.setData(props.macd.s)
}
upperBb = chart.addSeries(seriesDefinition, props.upperBbOptions);
middleBb = chart.addSeries(seriesDefinition, props.middleBbOptions);
lowerBb = chart.addSeries(seriesDefinition, props.lowerBbOptions);
if(Object.keys(props.macd).length) {
upperBb.setData(props.bb.u);
middleBb.setData(props.bb.m);
lowerBb.setData(props.bb.l);
}
// console.log(3)
};
onMounted(() => {
// Create the Lightweight Charts Instance using the container ref.
chart = createChart(chartContainer.value, props.chartOptions);
console.log(props)
macdPane = chart.addPane();
macdPane.setStretchFactor(0.5)
addSeriesAndData(props);
if (props.priceScaleOptions) {
@@ -113,15 +202,28 @@ onMounted(() => {
if (props.autosize) {
window.addEventListener('resize', resizeHandler);
}
chart.timeScale().subscribeVisibleLogicalRangeChange(onVisibleLogicalRangeChanged);
});
function onVisibleLogicalRangeChanged(newVisibleLogicalRange) {
const barsInfo = candlestick.barsInLogicalRange(newVisibleLogicalRange);
// if there less than 50 bars to the left of the visible area
if (barsInfo !== null && barsInfo.barsBefore < 500 && !props.lazyLock) {
// try to load additional historical data and prepend it to the series data
emits('lazyLoad', props.data[0].t - 5000);
}
}
onUnmounted(() => {
if (chart) {
chart.remove();
chart = null;
}
if (series) {
series = null;
if (candlestick) {
candlestick = null;
}
window.removeEventListener('resize', resizeHandler);
});
@@ -151,8 +253,8 @@ watch(
watch(
() => props.type,
newType => {
if (series && chart) {
chart.removeSeries(series);
if (candlestick && chart) {
chart.removeSeries(candlestick);
}
addSeriesAndData(props);
}
@@ -161,8 +263,53 @@ watch(
watch(
() => props.data,
newData => {
if (!series) return;
series.setData(newData);
if (!candlestick) return;
let oldT = 0;
let list = []
newData.forEach(item => {
if (item.t !== oldT) {
list.push({
time: item.t,
open: Number(item.o),
high: Number(item.h),
low: Number(item.l),
close: Number(item.c),
volume: Number(item.v),
})
}
oldT = item.t
})
candlestick.setData(list);
}
);
watch(
() => props.ema,
newData => {
if (!emaLine) return;
emaLine.setData(newData);
}
);
watch(
() => props.macd,
newData => {
if (!macdLine || !macdHistogram || !macdSignalLine) return;
macdLine.setData(newData.m);
macdHistogram.setData(newData.h);
macdSignalLine.setData(newData.s);
}
);
watch(
() => props.bb,
newData => {
if (!upperBb || !middleBb || !lowerBb) return;
upperBb.setData(newData.u);
middleBb.setData(newData.m);
lowerBb.setData(newData.l);
}
);
@@ -175,10 +322,42 @@ watch(
);
watch(
() => props.seriesOptions,
() => props.candleOptions,
newOptions => {
if (!series) return;
series.applyOptions(newOptions);
if (!candlestick) return;
candlestick.applyOptions(newOptions);
}
);
watch(
() => props.emaOptions,
newOptions => {
if (!emaLine) return;
emaLine.applyOptions(newOptions);
}
);
watch(
() => props.macdLineOptions,
newOptions => {
if (!macdLine) return;
macdLine.applyOptions(newOptions);
}
);
watch(
() => props.macdSignalOptions,
newOptions => {
if (!macdSignalLine) return;
macdSignalLine.applyOptions(newOptions);
}
);
watch(
() => props.macdHistogramOptions,
newOptions => {
if (!macdHistogram) return;
macdHistogram.applyOptions(newOptions);
}
);

View File

@@ -1,5 +1,10 @@
import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
import vuetify from './plugins/vuetify'
createApp(App).mount('#app')
const app = createApp(App)
app.use(vuetify)
app.mount('#app')

10
src/plugins/vuetify.ts Normal file
View File

@@ -0,0 +1,10 @@
import '@mdi/font/css/materialdesignicons.css'
import 'vuetify/styles'
import {createVuetify} from "vuetify/framework";
import * as components from 'vuetify/components'
import * as directives from 'vuetify/directives'
export default createVuetify({
components,
directives
})

View File

@@ -1,9 +1,9 @@
import {useAxios} from "@vueuse/integrations/useAxios";
export const getCandleList = (contact:string, time: string) => {
export const getCandleList = (contact:string, time: string, timestamp: number) => {
return new Promise(resolve => {
useAxios(`/api/candle/${contact}/${time}`).then((res) => {
resolve(res.data.value.data)
useAxios(`/api/candle/${contact}/${time}/${timestamp}`).then((res) => {
resolve(res.data.value)
})
})

76
src/utils/meter.ts Normal file
View File

@@ -0,0 +1,76 @@
function calculateBollingerBands(data: any, period = 20, stdDevMultiplier = 2) {
// prices = prices.reverse()
const u = [];
const m = [];
const l = [];
for (let i = 0; i < data.length; i++) {
if (i < period - 1) {
} else {
const slice = data.slice(i - period + 1, i + 1);
const sum = slice.reduce((a: any, b: any) => a + Number(b.c), 0);
const mean = sum / slice.length;
const squaredDiffs = slice.map((price: any) => (Number(price.c) - mean) ** 2);
const variance = squaredDiffs.reduce((a:any, b:any) => a + b, 0) / slice.length;
const stdDev = Math.sqrt(variance);
if(mean) {
m.push({'time':data[i].t, value: mean});
u.push({'time':data[i].t, value: mean + stdDev * stdDevMultiplier});
l.push({'time':data[i].t, value: mean - stdDev * stdDevMultiplier});
}
}
}
return { u, m, l };
}
function calculateEMA(data:any, period:any) {
const ema = [];
const k = 2 / (period + 1);
let emaValue = Number(data[0].c ?? data[0].value); // 첫 값은 단순 평균 or 첫 close
for (let i = 0; i < data.length; i++) {
if (i === 0) {
ema.push({time: data[i].t ?? data[i].time, value: emaValue});
} else {
emaValue = Number(data[i].c ?? data[i].value) * k + emaValue * (1 - k);
ema.push({time: data[i].t ?? data[i].time, value: emaValue});
}
}
return ema;
}
function calculateMACD(data: any, fastPeriod = 12, slowPeriod = 26, signalPeriod = 9) {
// 1. EMA 계산 함수 필요 (이전에 정의한 EMA 사용)
const ema12: any = calculateEMA(data, fastPeriod);
const ema26: any = calculateEMA(data, slowPeriod);
// 2. MACD Line = EMA12 - EMA26
const m = [];
for (let i = 0; i < data.length; i++) {
if (ema12[i] == null || ema26[i] == null) {
m.push({});
} else {
m.push({time: data[i].t, value: ema12[i].value - ema26[i].value});
}
}
// 3. Signal Line = MACD Line의 9일 EMA
const s = calculateEMA(m, signalPeriod);
// 4. Histogram
const h = [];
for (let i = 0; i < m.length; i++) {
if (m[i] == null || s[i] == null) {
h.push({});
} else {
h.push({time: data[i].t, value: m[i].value - s[i].value, color: (m[i].value - s[i].value) > 0 ? '#26a69a' : '#ef5350',});
}
}
return {
m, s, h
};
}
export { calculateBollingerBands, calculateEMA, calculateMACD };