From f5059ed0ce886f53478c0e5f652bc7b1185d8d8e Mon Sep 17 00:00:00 2001 From: bangae1 Date: Sun, 24 Aug 2025 22:00:11 +0900 Subject: [PATCH] =?UTF-8?q?rsi=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.vue | 79 +++++++++++++------- src/components/LWChart.vue | 27 ++++++- src/utils/api.ts | 4 +- src/utils/cro.ts | 145 ++++++++++++++++++++++++++++++++++++ src/utils/meter.ts | 29 +++++++- src/utils/sig.ts | 146 +++++++++++++++++++++++++++++++++++++ types/imports.d.ts | 4 + vite.config.ts | 4 +- 8 files changed, 404 insertions(+), 34 deletions(-) create mode 100644 src/utils/cro.ts create mode 100644 src/utils/sig.ts diff --git a/src/App.vue b/src/App.vue index e23739e..c6b8c4b 100644 --- a/src/App.vue +++ b/src/App.vue @@ -10,7 +10,9 @@ import { ref } from 'vue'; */ import LWChart from './components/LWChart.vue'; import {getCandleList} from '/@src/utils/api' -import {calculateMACD, calculateEMA, calculateBollingerBands} from "./utils/meter.js"; +import {calculateMACD, calculateEMA, calculateBollingerBands, calculateRSI} from "./utils/meter.js"; +import {entrySignals} from "./utils/cro.js"; +import {swingSignal} from "./utils/sig.js"; /** * Generates sample data for the lightweight chart * @param {Boolean} ohlc Whether generated dat should include open, high, low, and close values @@ -21,7 +23,8 @@ const lazyLock = ref(false) const positionData = ref('') const contract = ref('BTC_USDT') -const time = ref('15m') +/* Interval : "10s", "1m", "5m", "15m", "30m", "1h", "4h", "8h", "1d", "7d"*/ +const time = ref('1h') const chartOptions = ref({ layout: { textColor: 'black', @@ -42,6 +45,7 @@ const candleTick = ref([]) const ema = ref([]) const macd = ref({}) const bb = ref({}) +const rsi = ref({}) const selectCoins = [ { title: 'BTC_USDT'}, @@ -87,17 +91,20 @@ const candleOptions = ref({ const macdLineOptions = ref({ lineWidth: 2, - color: '#FFBAB5' + color: '#FFBAB5', + title: 'MACD', }); const macdSignalOptions = ref({ lineWidth: 2, - color: '#AD81FF' + color: '#AD81FF', + title: 'SIGNAL', }); const macdHistogramOptions = ref({ lineWidth: 2, - color: '#27FFE6' + color: '#27FFE6', + title: 'HISTOGRAM', }); const upperBbOptions = ref({ @@ -121,6 +128,12 @@ const lowerBbOptions = ref({ title: 'LowerBB', }) +const rsiLineOptions = ref({ + lineWidth: 2, + color: '#AD81FF', + title: 'RSI', +}); + const chartType = ref('candlestick'); const lwChart = ref(); @@ -130,14 +143,16 @@ onMounted(() => { }) const init = (cont, ti) => { - socket = new WebSocket("ws://127.0.0.1:8765"); + socket = new WebSocket("wss://fx-ws.gateio.ws/v4/ws/usdt"); 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) + rsi.value = calculateRSI(data) + // console.log(swingSignal(data)) + // console.table(entrySignals(data)) candleTick.value = data - console.log(bb.value) }) candleSocket(cont, ti) @@ -145,6 +160,7 @@ const init = (cont, ti) => { const distroy = (cont) => { candleTick.value = [] + socket.close() } const lazyLoad = async (ti) => { @@ -154,6 +170,7 @@ const lazyLoad = async (ti) => { ema.value = calculateEMA(data, 20) macd.value = calculateMACD(data) bb.value = calculateBollingerBands(data) + rsi.value = calculateRSI(data) setTimeout(() => {lazyLock.value = false}, 3000) }) @@ -161,26 +178,34 @@ const lazyLoad = async (ti) => { const candleSocket = (cont, ti) => { socket.addEventListener('open', (event) => { - socket.send(JSON.stringify({"type":"subscribe", "channel": cont, "time": ti})) + // socket.send(JSON.stringify({"type":"subscribe", "channel": cont, "time": ti})) + console.log(cont, ti) + socket.send(JSON.stringify({"time" : curToUnix(), "channel" : "futures.candlesticks","event": "subscribe", "payload" : [ti, cont]})) }) socket.addEventListener('message', (message) => { const msg = JSON.parse(message.data) - const item = JSON.parse(msg.msg) + if(msg.event === 'update') { + const item = msg.result[0] - const ticks = candleTick.value - if(item.t === ticks[ticks.length - 1].t) { - ticks[ticks.length - 1] = item - candleTick.value = [...ticks] - } else { - ticks.push(item) - candleTick.value = [...ticks] + const ticks = candleTick.value + 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) + rsi.value = calculateRSI(ticks) + // console.table(entrySignals(ticks)) + console.log(swingSignal(ticks)) + lazyLock.value = false } - ema.value = calculateEMA(ticks, 20) - macd.value = calculateMACD(ticks) - bb.value = calculateBollingerBands(ticks) - lazyLock.value = false }) } @@ -233,23 +258,21 @@ const colorsTypeMap = { const mouseMove = (data) => { positionData.value = ` -O${data.open} -C${data.close} -L${data.low} -H${data.high} -T${epochToString(data.time*1000)} +O${data == null ? 0 : data.open} +C${data == null ? 0 : data.close} +L${data == null ? 0 : data.low} +H${data == null ? 0 : data.high} +T${data == null ? 0 : epochToString(data.time*1000)} `; } watch(contract, newVal => { distroy() - console.log(newVal); init(newVal, time.value) }) watch(time, newVal => { distroy() - console.log(newVal); init(contract.value, newVal) }) @@ -303,6 +326,7 @@ watch(time, newVal => { :ema="ema" :macd="macd" :bb="bb" + :rsi="rsi" :autosize="true" :chart-options="chartOptions" :ema-options="emaLineOptions" @@ -313,6 +337,7 @@ watch(time, newVal => { :lower-bb-options="lowerBbOptions" :middle-bb-options="middleBbOptions" :upper-bb-options="upperBbOptions" + :rsi-line-options="rsiLineOptions" :lazyLock="lazyLock" @lazyLoad="lazyLoad" @mouseMove="mouseMove" diff --git a/src/components/LWChart.vue b/src/components/LWChart.vue index b976de6..b9b7fa6 100644 --- a/src/components/LWChart.vue +++ b/src/components/LWChart.vue @@ -36,6 +36,9 @@ const props = defineProps({ bb: { type: Object, }, + rsi: { + type: Object, + }, autosize: { default: true, type: Boolean, @@ -58,6 +61,9 @@ const props = defineProps({ macdHistogramOptions: { type: Object, }, + rsiLineOptions: { + type: Object, + }, upperBbOptions: { type: Object, }, @@ -108,13 +114,15 @@ let chart; let macdLine; let macdSignalLine; let macdHistogram; -let mainPane; let macdPane; +let rsiPane; let upperBb; let middleBb; let lowerBb; +let rsiLine; + const chartContainer = ref(); const fitContent = () => { @@ -160,7 +168,6 @@ const addSeriesAndData = props => { 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) @@ -178,6 +185,11 @@ const addSeriesAndData = props => { middleBb.setData(props.bb.m); lowerBb.setData(props.bb.l); } + + rsiLine = rsiPane.addSeries(seriesDefinition, props.rsiLineOptions) + if(Object.keys(props.macd).length) { + rsiLine.setData(props.rsi) + } // console.log(3) }; @@ -186,6 +198,8 @@ onMounted(() => { chart = createChart(chartContainer.value, props.chartOptions); macdPane = chart.addPane(); macdPane.setStretchFactor(0.5) + rsiPane = chart.addPane(); + rsiPane.setStretchFactor(0.4) addSeriesAndData(props); @@ -325,6 +339,15 @@ watch( } ); + +watch( + () => props.rsi, + newData => { + if (!rsiLine) return; + rsiLine.setData(props.rsi); + } +); + watch( () => props.chartOptions, newOptions => { diff --git a/src/utils/api.ts b/src/utils/api.ts index 8b6e9ae..1da5b8e 100644 --- a/src/utils/api.ts +++ b/src/utils/api.ts @@ -1,8 +1,8 @@ import {useAxios} from "@vueuse/integrations/useAxios"; -export const getCandleList = (contact:string, time: string, timestamp: number) => { +export const getCandleList = (contract:string, time: string, timestamp: number) => { return new Promise(resolve => { - useAxios(`/api/candle/${contact}/${time}/${timestamp}`).then((res) => { + useAxios(`/api/v4/futures/usdt/candlesticks?contract=${contract}&interval=${time}&to=${timestamp}&limit=1500`).then((res) => { resolve(res.data.value) }) }) diff --git a/src/utils/cro.ts b/src/utils/cro.ts new file mode 100644 index 0000000..66e234d --- /dev/null +++ b/src/utils/cro.ts @@ -0,0 +1,145 @@ + +function mean(arr: any) { + return arr.reduce((a: number, b: any) => a + b, 0) / arr.length; +} + +function sum(arr: any) { + return arr.reduce((a: number, b: any) => a + b, 0) +} + +function atr(data:any, period: number) { + const trs = []; + for (let i = 1; i < data.length; i++) { + const prev = data[i - 1]; + const cur = data[i]; + const tr = Math.max( + cur.h - cur.l, + Math.abs(cur.h - prev.c), + Math.abs(cur.l - prev.c) + ); + trs.push(tr); + } + const atrArr = []; + for (let i = period - 1; i < trs.length; i++) { + const slice = trs.slice(i - period + 1, i + 1); + atrArr.push(mean(slice)); + } + return atrArr; +} + +function vwap(data:any, period: number) { + // 일반 VWAP = ∑(typical * volume) / ∑volume + const vwaps = []; + for (let i = period - 1; i < data.length; i++) { + const slice = data.slice(i - period + 1, i + 1); + let num = 0, + den = 0; + slice.forEach((c: any) => { + const typical = (Number(c.h) + Number(c.l) + Number(c.c)) / 3; + num += typical * Number(c.v); + den += Number(c.v); + }); + vwaps.push(num / den); + } + return vwaps; +} + + +function rsi(data: any, period: number) { + const gains = [], + losses = []; + for (let i = 1; i < data.length; i++) { + const diff = Number(data[i].c) - Number(data[i - 1].c); + gains.push(diff > 0 ? diff : 0); + losses.push(diff < 0 ? -diff : 0); + } + const rsiArr = []; + for (let i = period; i < gains.length; i++) { + const avgGain = mean(gains.slice(i - period, i)); + const avgLoss = mean(losses.slice(i - period, i)); + const rs = avgLoss === 0 ? 100 : avgGain / avgLoss; + rsiArr.push(100 - 100 / (1 + rs)); + } + return rsiArr; +} + +function bb(data: any, period: number, mult = 2) { + const up = [], + mid = [], + low = []; + for (let i = period - 1; i < data.length; i++) { + const slice = data.slice(i - period + 1, i + 1).map((x: any) => Number(x.c)); + const m = mean(slice); + const stdev = + Math.sqrt(sum(slice.map((p: any) => Math.pow(p - m, 2))) / period) || 0; + mid.push(m); + up.push(m + mult * stdev); + low.push(m - mult * stdev); + } + return { upper: up, middle: mid, lower: low }; +} + + +function calculateCRO(data: any) { + const n = data.length; + const vw = vwap(data, 20); + const mom = []; + for (let i = 20; i < n; i++) { + mom.push(data[i].c / data[i - 20].c - 1); + } + const divergences = []; + for (let i = 0; i < mom.length; i++) { + const idx20 = 19 + i; + const v = Math.abs(mom[i]) - Math.abs((vw[i] - data[idx20].c) / data[idx20].c); + divergences.push(v); + } + const atr14 = atr(data, 14); + const atr63 = atr(data, 63); + const vsr = []; + for (let i = 0; i < atr14.length; i++) { + const a63 = atr63[i] || 1; + vsr.push(atr14[i] / a63); + } + // offset 맞추기 + const offset = 63 - 1; + const cro = []; + for (let i = offset; i < n; i++) { + const di = i - offset; + const d = divergences[di] || 0; + const v = vsr[di - (63 - 14)] || 1; + let val = 0; + if (d >= 0.015 && v >= 1.2) val = 1; + if (d <= -0.015 && v >= 1.2) val = -1; + cro.push({ t: data[i].t, cro: val, divergence: d, vsr: v }); + } + return cro; +} + +function entrySignals(data: any) { + const cro = calculateCRO(data); + const rsi14 = rsi(data, 14); + const bbands = bb(data, 20, 2); + + const signals = []; + const croOffset = data.length - cro.length; + const rsiOffset = data.length - rsi14.length; + const bbOffset = data.length - bbands.lower.length; + for (let i = 0; i < cro.length; i++) { + const idx = croOffset + i; + const cur = data[idx]; + const r = rsi14[i - (croOffset - rsiOffset)]; + const bbl = bbands.lower[i - (croOffset - bbOffset)]; + const bbu = bbands.upper[i - (croOffset - bbOffset)]; + + if (cro[i].cro === 1 && r <= 35 && cur.c <= bbl) { + signals.push({ t: cro[i].t, side: 'LONG', price: cur.c }); + } + if (cro[i].cro === -1 && r >= 65 && cur.c >= bbu) { + signals.push({ t: cro[i].t, side: 'SHORT', price: cur.c }); + } + } + + return signals; +} + +export {entrySignals} \ No newline at end of file diff --git a/src/utils/meter.ts b/src/utils/meter.ts index 048d478..3b18e8c 100644 --- a/src/utils/meter.ts +++ b/src/utils/meter.ts @@ -24,6 +24,33 @@ function calculateBollingerBands(data: any, period = 20, stdDevMultiplier = 2) { return { u, m, l }; } + +function calculateRSI(data: any, period = 14) { + function mean(arr: any) { + return arr.reduce((a: number, b: any) => a + b, 0) / arr.length; + } + + const gains = [], + losses = []; + for (let i = 1; i < data.length; i++) { + const diff = Number(data[i].c) - Number(data[i - 1].c); + gains.push(diff > 0 ? diff : 0); + losses.push(diff < 0 ? -diff : 0); + } + const rsiArr = []; + for (let i = 0; i < gains.length; i++) { + const avgGain = mean(gains.slice(i - period, i)); + const avgLoss = mean(losses.slice(i - period, i)); + const rs = avgLoss === 0 ? 100 : avgGain / avgLoss; + if(isNaN(rs)) { + rsiArr.push({time: data[i].t ?? data[i].time, value: 0}); + } else { + rsiArr.push({time: data[i].t ?? data[i].time, value: 100 - 100 / (1 + rs)}); + } + } + return rsiArr; +} + function calculateEMA(data:any, period:any) { const ema = []; const k = 2 / (period + 1); @@ -73,4 +100,4 @@ function calculateMACD(data: any, fastPeriod = 12, slowPeriod = 26, signalPeriod }; } -export { calculateBollingerBands, calculateEMA, calculateMACD }; \ No newline at end of file +export { calculateBollingerBands, calculateEMA, calculateMACD, calculateRSI }; \ No newline at end of file diff --git a/src/utils/sig.ts b/src/utils/sig.ts new file mode 100644 index 0000000..64541fa --- /dev/null +++ b/src/utils/sig.ts @@ -0,0 +1,146 @@ + +// ---------- 헬퍼 ---------- +const mean = (arr: any) => arr.reduce((a:number, b:number) => a + b, 0) / arr.length; +const sum = (arr: any) => arr.reduce((a:number, b:number) => a + b, 0); + +// ---------- 기술지표 ---------- +const atr = (data: any, period: number) => { + const trs = []; + for (let i = 1; i < data.length; i++) { + const prev = data[i-1], cur = data[i]; + const tr = Math.max(Number(cur.h)-Number(cur.l), Math.abs(Number(cur.h)-Number(prev.c)), Math.abs(Number(cur.l)-Number(prev.c))); + trs.push(tr); + } + const res = []; + for (let i=period-1;i { + const gains=[], losses=[]; + for(let i=1;i0?d:0); losses.push(d<0?-d:0); + } + const res=[]; + for(let i=period;i { + const up=[], mid=[], low=[]; + for(let i=period-1;iNumber(x.c)); + const m=mean(slice); + const stdev=Math.sqrt(sum(slice.map((p: number)=>(p-m)**2))/period); + mid.push(m); up.push(m+mult*stdev); low.push(m-mult*stdev); + } + return {upper:up, middle:mid, lower:low}; +}; + +const macd = (data: any) => { + const fast = 12, slow = 26, sig = 9; + + // 1) EMA 헬퍼 + const ema = (arr: any, len: number) => { + const k = 2 / (len + 1); + const out = [arr[0]]; + for (let i = 1; i < arr.length; i++) { + out.push(arr[i] * k + out[i - 1] * (1 - k)); + } + return out; + }; + + // 2) MACD-Signal-Histogram + const closes = data.map((x: any) => Number(x.c)); + const emaFast = ema(closes, fast); + const emaSlow = ema(closes, slow); + + // 두 EMA 길이 맞추기 (slow 기준으로 잘라내기) + const alignedFast = emaFast.slice(slow - 1); + const alignedSlow = emaSlow.slice(slow - 1); + + const macdLine = alignedFast.map((v, i) => v - alignedSlow[i]); + + const signalLine = ema(macdLine, sig); + const hist = macdLine.slice(sig - 1) + .map((v, i) => v - signalLine[i]); + + return { macdLine, signalLine, hist }; +}; + +// ---------- 시그널 ---------- +// const swingSignal = (data: any) => { +// const macdHist = macd(data).hist; +// const rsi14 = rsi(data, 14); +// const bbands = bb(data, 20, 2); +// +// const offsetMacd = data.length - macdHist.length; +// const offsetRsi = data.length - rsi14.length; +// const offsetBb = data.length - bbands.lower.length; +// +// const latest = data.length-1; +// const hist = macdHist[latest-offsetMacd]; +// const prev = macdHist[latest-offsetMacd-1]; +// const rsiVal = rsi14[latest-offsetRsi]; +// const close = Number(data[latest].c); +// const bbl = bbands.lower[latest-offsetBb]; +// const bbu = bbands.upper[latest-offsetBb]; +// console.log(prev, hist, rsiVal, close, bbl) +// if(prev<0 && hist>=0 && rsiVal<=35 && close<=bbl) return 'LONG'; +// if(prev>0 && hist<=0 && rsiVal>=65 && close>=bbu) return 'SHORT'; +// return 'HOLD'; +// }; + +const swingSignal = (data: any) => { + // 1) 기존 지표 + const macdHist = macd(data).hist; + const rsi14 = rsi(data, 14); + const bbands = bb(data, 20, 2); + + // 2) ATR 변동성 필터 + const atr14 = atr(data, 14); + const atr63 = atr(data, 63); + // const offsetAtr = data.length - atr14.length; + const currentATR14 = atr14[atr14.length - 1]; + const currentATR63 = atr63[atr63.length - 1] || 1; + + // 변동성 급등 조건 + const volatilityBreak = currentATR14 > currentATR63 * 1.2; + + // 3) 기존 offset 계산 + const offsetMacd = data.length - macdHist.length; + const offsetRsi = data.length - rsi14.length; + const offsetBb = data.length - bbands.lower.length; + + const latest = data.length - 1; + const hist = macdHist[latest - offsetMacd]; + const prev = macdHist[latest - offsetMacd - 1]; + const rsiVal = rsi14[latest - offsetRsi]; + const close = Number(data[latest].c); + const bbl = bbands.lower[latest - offsetBb]; + const bbu = bbands.upper[latest - offsetBb]; + + console.log(volatilityBreak, prev, hist, rsiVal, close, bbl) + // 4) 최종 조건 + if (volatilityBreak && + prev < 0 && hist >= 0 && + rsiVal <= 35 && close <= bbl) + return 'LONG'; + + if (volatilityBreak && + prev > 0 && hist <= 0 && + rsiVal >= 65 && close >= bbu) + return 'SHORT'; + + return 'HOLD'; +}; + +export {swingSignal} \ No newline at end of file diff --git a/types/imports.d.ts b/types/imports.d.ts index 021ce62..d3a5c74 100644 --- a/types/imports.d.ts +++ b/types/imports.d.ts @@ -10,8 +10,10 @@ declare global { const asyncComputed: typeof import('@vueuse/core')['asyncComputed'] const autoResetRef: typeof import('@vueuse/core')['autoResetRef'] const calculateBollingerBands: typeof import('../src/utils/meter')['calculateBollingerBands'] + const calculateCRO: typeof import('../src/utils/meter')['calculateCRO'] const calculateEMA: typeof import('../src/utils/meter')['calculateEMA'] const calculateMACD: typeof import('../src/utils/meter')['calculateMACD'] + const calculateRSI: typeof import('../src/utils/meter')['calculateRSI'] const computed: typeof import('vue')['computed'] const computedAsync: typeof import('@vueuse/core')['computedAsync'] const computedEager: typeof import('@vueuse/core')['computedEager'] @@ -36,6 +38,7 @@ declare global { const defineComponent: typeof import('vue')['defineComponent'] const eagerComputed: typeof import('@vueuse/core')['eagerComputed'] const effectScope: typeof import('vue')['effectScope'] + const entrySignals: typeof import('../src/utils/cro')['entrySignals'] const extendRef: typeof import('@vueuse/core')['extendRef'] const getCandleList: typeof import('../src/utils/api')['getCandleList'] const getCurrentInstance: typeof import('vue')['getCurrentInstance'] @@ -95,6 +98,7 @@ declare global { const shallowReactive: typeof import('vue')['shallowReactive'] const shallowReadonly: typeof import('vue')['shallowReadonly'] const shallowRef: typeof import('vue')['shallowRef'] + const swingSignal: typeof import('../src/utils/sig')['swingSignal'] const syncRef: typeof import('@vueuse/core')['syncRef'] const syncRefs: typeof import('@vueuse/core')['syncRefs'] const templateRef: typeof import('@vueuse/core')['templateRef'] diff --git a/vite.config.ts b/vite.config.ts index abe8529..2c3081c 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -44,8 +44,8 @@ export default defineConfig(({ isSsrBuild }) => ({ host: '0.0.0.0', port: 3000, proxy: { - '/api': { - target: 'http://localhost:7010', + '/api/v4': { + target: 'https://fx-api.gateio.ws', changeOrigin: true, // rewrite: (path) => path.replace(/^\/api/, ''), },