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/, ''),
},