|
@@ -0,0 +1,397 @@
|
|
|
|
|
+import { reactive, toRefs, computed } from 'vue'
|
|
|
|
|
+import { queryHistoryDatas } from '@/services/api/quote'
|
|
|
|
|
+import { ChartCycleType } from '@/constants/chart'
|
|
|
|
|
+import { EchartsDataset, Candlestick, MACD } from '@/hooks/echarts/candlestick/types'
|
|
|
|
|
+import moment from 'moment'
|
|
|
|
|
+
|
|
|
|
|
+export function useDataset() {
|
|
|
|
|
+ const dataset: EchartsDataset = {
|
|
|
|
|
+ invalid: [],
|
|
|
|
|
+ decimal: 0,
|
|
|
|
|
+ cycleType: ChartCycleType.Minutes,
|
|
|
|
|
+ candlestick: {
|
|
|
|
|
+ dimensions: ['date', 'open', 'close', 'lowest', 'highest', 'ma5', 'ma10', 'ma15'],
|
|
|
|
|
+ source: [],
|
|
|
|
|
+ },
|
|
|
|
|
+ macd: {
|
|
|
|
|
+ dimensions: ['date', 'macd', 'dif', 'dea'],
|
|
|
|
|
+ source: [],
|
|
|
|
|
+ },
|
|
|
|
|
+ vol: {
|
|
|
|
|
+ dimensions: ['date', 'vol'],
|
|
|
|
|
+ source: [],
|
|
|
|
|
+ },
|
|
|
|
|
+ kdj: {
|
|
|
|
|
+ dimensions: ['date', 'k', 'd', 'j'],
|
|
|
|
|
+ source: [],
|
|
|
|
|
+ },
|
|
|
|
|
+ cci: {
|
|
|
|
|
+ dimensions: ['date', 'cci'],
|
|
|
|
|
+ source: [],
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 清空数据
|
|
|
|
|
+ const clearData = () => {
|
|
|
|
|
+ dataset.invalid = [];
|
|
|
|
|
+ dataset.candlestick.source = [];
|
|
|
|
|
+ dataset.macd.source = [];
|
|
|
|
|
+ dataset.vol.source = [];
|
|
|
|
|
+ dataset.kdj.source = [];
|
|
|
|
|
+ dataset.cci.source = [];
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 处理行情数据
|
|
|
|
|
+ const handleData = (rawData: Model.HistoryDatasRsp[]) => {
|
|
|
|
|
+ for (let i = 0; i < rawData.length; i++) {
|
|
|
|
|
+ const { o, c, h, l, ts: date, f, tv } = rawData[i];
|
|
|
|
|
+ if (f) dataset.invalid.push(i); // 添加补充数据的索引位置
|
|
|
|
|
+
|
|
|
|
|
+ dataset.candlestick.source.push({
|
|
|
|
|
+ date,
|
|
|
|
|
+ open: o,
|
|
|
|
|
+ close: c,
|
|
|
|
|
+ lowest: l,
|
|
|
|
|
+ highest: h,
|
|
|
|
|
+ ma5: '-',
|
|
|
|
|
+ ma10: '-',
|
|
|
|
|
+ ma15: '-',
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ dataset.macd.source.push({
|
|
|
|
|
+ date,
|
|
|
|
|
+ ema12: 0,
|
|
|
|
|
+ ema26: 0,
|
|
|
|
|
+ dif: 0,
|
|
|
|
|
+ dea: 0,
|
|
|
|
|
+ macd: 0,
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ dataset.vol.source.push({
|
|
|
|
|
+ date,
|
|
|
|
|
+ vol: tv,
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ dataset.kdj.source.push({
|
|
|
|
|
+ date,
|
|
|
|
|
+ k: '-',
|
|
|
|
|
+ d: '-',
|
|
|
|
|
+ j: '-'
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ dataset.cci.source.push({
|
|
|
|
|
+ date,
|
|
|
|
|
+ cci: '-',
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
+ // 计算各种指标
|
|
|
|
|
+ calcIndicator()
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 计算MA
|
|
|
|
|
+ const calcMA = (count: 5 | 10 | 15, startIndex = 0) => {
|
|
|
|
|
+ const key: keyof Candlestick = count === 5 ? 'ma5' : count === 10 ? 'ma10' : 'ma15';
|
|
|
|
|
+ const candlestickSource = dataset.candlestick.source;
|
|
|
|
|
+
|
|
|
|
|
+ for (let i = startIndex; i < candlestickSource.length; i++) {
|
|
|
|
|
+ // 判断是否补充数据
|
|
|
|
|
+ if (dataset.invalid.includes(i)) {
|
|
|
|
|
+ const value = i > 0 ? candlestickSource[i - 1][key] : '-'; // 如果存在,取上一条记录的MA值
|
|
|
|
|
+ candlestickSource[i][key] = value;
|
|
|
|
|
+ } else {
|
|
|
|
|
+ let n = 0;
|
|
|
|
|
+ const tmpList: Candlestick[] = [];
|
|
|
|
|
+
|
|
|
|
|
+ for (let j = i; j >= 0; j--) {
|
|
|
|
|
+ if (dataset.invalid.includes(j)) continue; // 如果是补充数据,跳过本次循环
|
|
|
|
|
+ if (n === count) break; // 如果 n 等于计数,结束循环
|
|
|
|
|
+ tmpList.push(candlestickSource[j]);
|
|
|
|
|
+ n++;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (n === count) {
|
|
|
|
|
+ // 计算总价(收盘价总和)
|
|
|
|
|
+ const total = tmpList.reduce((res, e) => res + e.close, 0);
|
|
|
|
|
+ // 计算均价
|
|
|
|
|
+ candlestickSource[i][key] = (total / count).toFixed(2);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ candlestickSource[i][key] = '-';
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 计算EMA
|
|
|
|
|
+ const calcEMA = (count: 12 | 26, startIndex = 0) => {
|
|
|
|
|
+ const key: keyof MACD = count === 12 ? 'ema12' : 'ema26';
|
|
|
|
|
+ const macdSource = dataset.macd.source;
|
|
|
|
|
+ const candlestickSource = dataset.candlestick.source;
|
|
|
|
|
+ const a = 2 / (count + 1); // 平滑系数
|
|
|
|
|
+
|
|
|
|
|
+ for (let i = startIndex; i < candlestickSource.length; i++) {
|
|
|
|
|
+ const close = candlestickSource[i].close; // 收盘价
|
|
|
|
|
+
|
|
|
|
|
+ if (i === 0) {
|
|
|
|
|
+ macdSource[i][key] = close; // 初始EMA取收盘价
|
|
|
|
|
+ } else {
|
|
|
|
|
+ const prevEMA = macdSource[i - 1][key]; // 昨日EMA
|
|
|
|
|
+ const value = a * close + (1 - a) * Number(prevEMA);
|
|
|
|
|
+
|
|
|
|
|
+ macdSource[i][key] = value;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 计算MACD
|
|
|
|
|
+ const calcMACD = (startIndex = 0) => {
|
|
|
|
|
+ // 先计算EMA
|
|
|
|
|
+ calcEMA(12, startIndex); // EMA(12) = 2 ÷ 13 * 今日收盘价(12) + 11 ÷ 13 * 昨日EMA(12)
|
|
|
|
|
+ calcEMA(26, startIndex);
|
|
|
|
|
+
|
|
|
|
|
+ const macdSource = dataset.macd.source;
|
|
|
|
|
+
|
|
|
|
|
+ for (let i = startIndex; i < macdSource.length; i++) {
|
|
|
|
|
+ const { ema12, ema26 } = macdSource[i];
|
|
|
|
|
+ const prevDEA = i > 0 ? macdSource[i - 1].dea : 0; // 昨日DEA
|
|
|
|
|
+
|
|
|
|
|
+ const dif = ema12 - ema26; // DIF = 今日EMA(12) - 今日EMA(26)
|
|
|
|
|
+ const dea = prevDEA * 8 / 10 + dif * 2 / 10; // DEA = 昨日DEA × 8 ÷ 10 + 今日DIF × 2 ÷ 10
|
|
|
|
|
+ const macd = (dif - dea) * 2; // MACD = (DIFF - DEA) × 2
|
|
|
|
|
+
|
|
|
|
|
+ macdSource[i].dif = Number(dif.toFixed(2));
|
|
|
|
|
+ macdSource[i].dea = Number(dea.toFixed(2));
|
|
|
|
|
+ macdSource[i].macd = Number(macd.toFixed(2));
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 计算KDJ
|
|
|
|
|
+ const calcKDJ = (startIndex = 0) => {
|
|
|
|
|
+ const count = 9; // 以9日周期计算
|
|
|
|
|
+ const candlestickSource = dataset.candlestick.source;
|
|
|
|
|
+ const kdjSource = dataset.kdj.source;
|
|
|
|
|
+
|
|
|
|
|
+ for (let i = startIndex; i < candlestickSource.length; i++) {
|
|
|
|
|
+ let n = 0;
|
|
|
|
|
+ const tmpList: Candlestick[] = [];
|
|
|
|
|
+
|
|
|
|
|
+ for (let j = i; j >= 0; j--) {
|
|
|
|
|
+ if (n === count) break; // 如果 n 等于计数,结束循环
|
|
|
|
|
+ tmpList.push(candlestickSource[j]);
|
|
|
|
|
+ n++;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (n === count) {
|
|
|
|
|
+ const close = candlestickSource[i].close, // 收盘价
|
|
|
|
|
+ n9 = tmpList.map((item) => item.close),
|
|
|
|
|
+ h9 = Math.max(...n9), // 9天内最高价
|
|
|
|
|
+ l9 = Math.min(...n9); // 9天内最低价
|
|
|
|
|
+
|
|
|
|
|
+ const rsv = (close - l9) / (h9 - l9) * 100, // RSV = (Ct-L9) ÷ (H9-L9) * 100
|
|
|
|
|
+ prevK = Number(kdjSource[i - 1].k), // 昨日K值
|
|
|
|
|
+ prevD = Number(kdjSource[i - 1].d); // 昨日D值
|
|
|
|
|
+
|
|
|
|
|
+ const kValue = isNaN(prevK) ? 50 : prevK; // 若无昨日K值,则可用50来代替
|
|
|
|
|
+ const dValue = isNaN(prevD) ? 50 : prevD; // 若无昨日D值,则可用50来代替
|
|
|
|
|
+
|
|
|
|
|
+ const k = (2 / 3) * kValue + (1 / 3) * rsv, // K = 2 ÷ 3 × 昨日K值 + 1 ÷ 3 × RSV
|
|
|
|
|
+ d = (2 / 3) * dValue + (1 / 3) * k, // D = 2 ÷ 3 × 昨日D值 + 1 ÷ 3 × K值
|
|
|
|
|
+ j = 3 * k - 2 * d; // J = 3 * K值 - 2 * D值
|
|
|
|
|
+
|
|
|
|
|
+ kdjSource[i].k = k.toFixed(2);
|
|
|
|
|
+ kdjSource[i].d = d.toFixed(2);
|
|
|
|
|
+ kdjSource[i].j = j.toFixed(2);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 计算CCI
|
|
|
|
|
+ const calcCCI = (startIndex = 0) => {
|
|
|
|
|
+ const count = 14; // 以14日周期计算
|
|
|
|
|
+ const candlestickSource = dataset.candlestick.source;
|
|
|
|
|
+ const cciSource = dataset.cci.source;
|
|
|
|
|
+
|
|
|
|
|
+ for (let i = startIndex; i < candlestickSource.length; i++) {
|
|
|
|
|
+ let n = 0;
|
|
|
|
|
+ const tmpList: Candlestick[] = [];
|
|
|
|
|
+
|
|
|
|
|
+ for (let j = i; j >= 0; j--) {
|
|
|
|
|
+ if (n === count) break; // 如果 n 等于计数,结束循环
|
|
|
|
|
+ tmpList.push(candlestickSource[j]);
|
|
|
|
|
+ n++;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (n === count) {
|
|
|
|
|
+ const calcTP = (item: Candlestick) => (item.highest + item.lowest + item.close) / 3; // TP = (最高价 + 最低价 + 收盘价) ÷ 3
|
|
|
|
|
+ const tp = calcTP(candlestickSource[i]),
|
|
|
|
|
+ ma = tmpList.reduce((res, e) => res + calcTP(e), 0) / count, // MA = 近N日TP的累计之和 ÷ N
|
|
|
|
|
+ md = tmpList.reduce((res, e) => res + Math.abs(calcTP(e) - ma), 0) / count; // MD = 近N日TP的绝对值的累计之和 ÷ N
|
|
|
|
|
+
|
|
|
|
|
+ const cci = (tp - ma) / (md * 0.015);
|
|
|
|
|
+ cciSource[i].cci = cci.toFixed(2);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 计算各项指标
|
|
|
|
|
+ const calcIndicator = (startIndex = 0) => {
|
|
|
|
|
+ calcMA(5, startIndex);
|
|
|
|
|
+ calcMA(10, startIndex);
|
|
|
|
|
+ calcMA(15, startIndex);
|
|
|
|
|
+ calcMACD(startIndex);
|
|
|
|
|
+ calcKDJ(startIndex);
|
|
|
|
|
+ calcCCI(startIndex);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return {
|
|
|
|
|
+ dataset,
|
|
|
|
|
+ handleData,
|
|
|
|
|
+ clearData,
|
|
|
|
|
+ calcIndicator,
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+export function useCandlestick(goodsCode: string) {
|
|
|
|
|
+ const { dataset, handleData, clearData } = useDataset()
|
|
|
|
|
+
|
|
|
|
|
+ const state = reactive({
|
|
|
|
|
+ loading: false,
|
|
|
|
|
+ dataIndex: -1
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ // 当前选中的数据项
|
|
|
|
|
+ const selectedItem = computed(() => {
|
|
|
|
|
+ if (state.dataIndex > -1 && dataset.candlestick.source.length > state.dataIndex) {
|
|
|
|
|
+ const { open, close, highest, lowest, ma5, ma10, ma15 } = dataset.candlestick.source[state.dataIndex]
|
|
|
|
|
+ const { macd, dif, dea } = dataset.macd.source[state.dataIndex]
|
|
|
|
|
+ const { vol } = dataset.vol.source[state.dataIndex]
|
|
|
|
|
+ const { k, d, j } = dataset.kdj.source[state.dataIndex]
|
|
|
|
|
+ const { cci } = dataset.cci.source[state.dataIndex]
|
|
|
|
|
+ return { open, close, highest, lowest, ma5, ma10, ma15, macd, dif, dea, vol, k, d, j, cci }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ return { open: '--', close: '--', highest: '--', lowest: '--', ma5: '--', ma10: '--', ma15: '--', macd: '--', dif: '--', dea: '--', vol: '--', k: '--', d: '--', j: '--', cci: '--' }
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ // 获取历史行情
|
|
|
|
|
+ const getHistoryData = async () => {
|
|
|
|
|
+ try {
|
|
|
|
|
+ clearData()
|
|
|
|
|
+ state.dataIndex = -1
|
|
|
|
|
+ state.loading = true
|
|
|
|
|
+ const res = await queryHistoryDatas({
|
|
|
|
|
+ data: {
|
|
|
|
|
+ goodsCode,
|
|
|
|
|
+ cycleType: dataset.cycleType,
|
|
|
|
|
+ count: 1440,
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+ // 日期升序排序
|
|
|
|
|
+ const data = res.data.sort((a, b) => moment(a.ts).valueOf() - moment(b.ts).valueOf())
|
|
|
|
|
+ state.dataIndex = data.length - 1
|
|
|
|
|
+ handleData(data)
|
|
|
|
|
+ return dataset
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ state.loading = false
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ let onLoadData = getHistoryData()
|
|
|
|
|
+
|
|
|
|
|
+ const initData = (cycleType: ChartCycleType) => {
|
|
|
|
|
+ dataset.cycleType = cycleType
|
|
|
|
|
+ onLoadData = getHistoryData()
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 格式化日期
|
|
|
|
|
+ const formatDate = (value: string) => {
|
|
|
|
|
+ switch (dataset.cycleType) {
|
|
|
|
|
+ case ChartCycleType.Day: {
|
|
|
|
|
+ return moment(value).format('YYYY-MM-DD')
|
|
|
|
|
+ }
|
|
|
|
|
+ case ChartCycleType.Hours2:
|
|
|
|
|
+ case ChartCycleType.Hours4: {
|
|
|
|
|
+ return moment(value).format('YYYY-MM-DD HH:00:00')
|
|
|
|
|
+ }
|
|
|
|
|
+ default: {
|
|
|
|
|
+ return moment(value).format('YYYY-MM-DD HH:mm:00')
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const updateData = (q: Partial<Model.QuoteDayRsp>) => {
|
|
|
|
|
+ const { last, lasttime } = q
|
|
|
|
|
+ if (last && lasttime) {
|
|
|
|
|
+ const { candlestick, macd, vol, kdj, cci } = dataset
|
|
|
|
|
+ const lastIndex = candlestick.source.length - 1 // 历史数据最后索引位置
|
|
|
|
|
+
|
|
|
|
|
+ const oldTime = lastIndex === -1 ? moment(lasttime) : moment(candlestick.source[lastIndex].date) // 历史行情最后时间
|
|
|
|
|
+ const diffTime = moment(lasttime).valueOf() - oldTime.valueOf() // 计算时间差
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+ // 判断时间差是否大于周期时间
|
|
|
|
|
+ if (lastIndex === -1 || diffTime > 60 * 1000) {
|
|
|
|
|
+ const newtime = formatDate(lasttime)
|
|
|
|
|
+ // 新增K线数据
|
|
|
|
|
+ candlestick.source.push({
|
|
|
|
|
+ date: newtime,
|
|
|
|
|
+ open: last,
|
|
|
|
|
+ close: last,
|
|
|
|
|
+ lowest: last,
|
|
|
|
|
+ highest: last,
|
|
|
|
|
+ ma5: '-',
|
|
|
|
|
+ ma10: '-',
|
|
|
|
|
+ ma15: '-',
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ // 新增MACD数据
|
|
|
|
|
+ macd.source.push({
|
|
|
|
|
+ date: newtime,
|
|
|
|
|
+ ema12: 0,
|
|
|
|
|
+ ema26: 0,
|
|
|
|
|
+ dif: 0,
|
|
|
|
|
+ dea: 0,
|
|
|
|
|
+ macd: 0,
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ // 新增VOL数据
|
|
|
|
|
+ vol.source.push({
|
|
|
|
|
+ date: newtime,
|
|
|
|
|
+ vol: 0,
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ // 新增KDJ数据
|
|
|
|
|
+ kdj.source.push({
|
|
|
|
|
+ date: newtime,
|
|
|
|
|
+ k: '-',
|
|
|
|
|
+ d: '-',
|
|
|
|
|
+ j: '-',
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ // 新增CCI数据
|
|
|
|
|
+ cci.source.push({
|
|
|
|
|
+ date: newtime,
|
|
|
|
|
+ cci: '-',
|
|
|
|
|
+ })
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // 更新列表中最后一条记录的数据
|
|
|
|
|
+ const record = candlestick.source[lastIndex]
|
|
|
|
|
+ if (record.lowest > last) {
|
|
|
|
|
+ record.lowest = last // 更新最低价
|
|
|
|
|
+ }
|
|
|
|
|
+ if (record.highest < last) {
|
|
|
|
|
+ record.highest = last // 更新最高价
|
|
|
|
|
+ }
|
|
|
|
|
+ record.close = last // 更新收盘价
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return dataset
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return {
|
|
|
|
|
+ ...toRefs(state),
|
|
|
|
|
+ onLoadData,
|
|
|
|
|
+ selectedItem,
|
|
|
|
|
+ initData,
|
|
|
|
|
+ updateData
|
|
|
|
|
+ }
|
|
|
|
|
+}
|