| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356 |
- <template>
- <echart-base :options="[options]" :empty="isEmpty" v-model:loading="loading"></echart-base>
- </template>
- <script lang="ts">
- import { defineComponent, PropType, ref, watch, watchEffect } from 'vue';
- import { QueryHistoryDatasRsp, QueryQuoteDayRsp, QueryHistoryDatas, CycleType } from '@/services/go/quote/interface';
- import { QueryHistoryDatas as queryHistoryDatas } from '@/services/go/quote';
- import { debounce } from '@/utils/time';
- import EchartBase from '../echart-base/index.vue';
- import { handleEchart, Source } from './setup';
- import moment from 'moment';
- export default defineComponent({
- name: 'EchartKline',
- components: {
- EchartBase,
- },
- props: {
- // 实时行情数据
- quoteData: {
- type: Object as PropType<QueryQuoteDayRsp>,
- required: true,
- },
- // 周期类型
- cycleType: {
- type: Number as PropType<CycleType>,
- required: true,
- },
- // 指标类型
- seriesType: {
- type: String,
- default: 'MACD',
- },
- },
- setup(props) {
- const loading = ref(true);
- const isEmpty = ref(false);
- const historyIndexs: number[] = []; // 行情历史数据中所有非补充数据的索引位置(用于计算均线)
- const { chartData, options, updateOptions, initOptions } = handleEchart();
- // 处理图表数据
- const handleData = (rawData: QueryHistoryDatasRsp[]): void => {
- const { source } = chartData.value;
- source.length = 0;
- historyIndexs.length = 0;
- rawData.forEach((item, index) => {
- const { o, c, h, l, ts, tv } = item;
- source.push({
- date: moment(ts).format('YYYY-MM-DD HH:mm:ss'),
- open: o,
- close: c,
- lowest: l,
- highest: h,
- ma5: '-',
- ma10: '-',
- ma15: '-',
- vol: tv,
- macd: '-',
- dif: '-',
- dea: '-',
- k: '-',
- d: '-',
- j: '-',
- cci: '-',
- });
- if (!item.f) historyIndexs.push(index); // 排除补充数据
- });
- calcMA('ma5', 5);
- calcMA('ma10', 10);
- calcMA('ma15', 15);
- calcMACD();
- calcKDJ();
- clacCCI();
- };
- // 计算平均线
- const calcMA = (key: keyof Source, count: number) => {
- const { source } = chartData.value;
- let result: Source[keyof Source] = '-';
- if (source.length >= count) {
- // 均线起始位置
- const startIndex = historyIndexs[count - 1];
- for (let i = 0; i < source.length; i++) {
- if (startIndex !== undefined && i > startIndex) {
- const j = historyIndexs.findIndex((val) => val === i);
- // 判断是否补充数据
- if (j === -1) {
- result = source[i - 1][key]; // 取上个平均值
- } else {
- // 向后取MA数
- const maIndexs = historyIndexs.slice(j - (count - 1), j + 1);
- // 计算总价
- const total = maIndexs.reduce((sum, val) => sum + source[val].close, 0);
- // 计算均线
- result = (total / count).toFixed(2);
- }
- }
- (<typeof result>source[i][key]) = result;
- }
- }
- };
- // 计算EMA
- const calcEMA = (close: number[], n: number) => {
- const ema: number[] = [],
- a = 2 / (n + 1); // 平滑系数
- for (let i = 0; i < close.length; i++) {
- if (i === 0) {
- //第一个EMA(n)是前n个收盘价代数平均
- const result = close.slice(0, n).reduce((sum, val) => sum + val, 0) / n;
- ema.push(result);
- } else {
- // EMA(n) = α × Close + (1 - α) × EMA(n - 1)
- const result = a * close[i] + (1 - a) * ema[i - 1];
- ema.push(result);
- }
- }
- return ema;
- };
- // 计算DEA
- const calcDEA = (dif: number[]) => {
- return calcEMA(dif, 9);
- };
- // 计算DIF
- const calcDIF = (close: number[]) => {
- const dif: number[] = [],
- emaShort = calcEMA(close, 12),
- emaLong = calcEMA(close, 26);
- for (let i = 0; i < close.length; i++) {
- const result = emaShort[i] - emaLong[i];
- dif.push(result);
- }
- return dif;
- };
- // 计算MACD
- const calcMACD = () => {
- const { source } = chartData.value,
- close = source.map((item) => item.close),
- dif = calcDIF(close),
- dea = calcDEA(dif);
- for (let i = 0; i < source.length; i++) {
- source[i].dif = dif[i].toFixed(2);
- source[i].dea = dea[i].toFixed(2);
- source[i].macd = ((dif[i] - dea[i]) * 2).toFixed(2);
- }
- };
- // 计算KDJ
- const calcKDJ = () => {
- const { source } = chartData.value;
- for (let i = 0; i < source.length; i++) {
- const item = source[i];
- if (i < 8) {
- item.k = '-';
- item.d = '-';
- item.j = '-';
- } else {
- let rsv = 50; // 如果最低价等于最高价,RSV默认值为50
- if (item.lowest !== item.highest) {
- const n9 = source.slice(i - 8, i + 1).map((item) => item.close), // 取前9个收盘价
- max = Math.max(...n9),
- min = Math.min(...n9);
- // 计算RSV
- rsv = ((item.close - min) / (max - min)) * 100;
- }
- const yestK = Number(source[i - 1].k); // 取前一日K值
- const yestD = Number(source[i - 1].d); // 取前一日D值
- if (isNaN(yestK) || isNaN(yestD)) {
- // 如果前一日的K值或D值不存在则默认值为50
- item.k = '50';
- item.d = '50';
- item.j = '50';
- } else {
- const k = (2 / 3) * yestK + (1 / 3) * rsv,
- d = (2 / 3) * yestD + (1 / 3) * yestK,
- j = 3 * k - 2 * d;
- item.k = k.toFixed(2);
- item.d = d.toFixed(2);
- item.j = j.toFixed(2);
- }
- }
- }
- };
- // 计算CCI
- const clacCCI = () => {
- const { source } = chartData.value;
- for (let i = 0; i < source.length; i++) {
- const item = source[i];
- if (i < 13) {
- item.cci = '-';
- } else {
- const tp = (item.close + item.lowest + item.highest) / 3, // (收盘价 + 最低价 + 最高价) / 3
- n14 = source.slice(i - 13, i + 1), // 取前14条数据
- ma = n14.reduce((sum, e) => sum + (e.close + e.lowest + e.highest) / 3, 0) / 14, // 计算前14条数据的(TP)价总和÷N
- md = n14.reduce((sum, e) => sum + Math.abs(ma - (e.close + e.lowest + e.highest) / 3), 0) / 14, // 计算前14条数据的(MA-TP)价总和÷N
- result = (tp - ma) / md / 0.015;
- item.cci = result.toFixed(2);
- }
- }
- };
- // 更新图表K线数据
- const updateChartData = () => {
- const { source } = chartData.value,
- lastIndex = source.length - 1, // 历史行情最后索引位置
- lastTime = moment(source[lastIndex].date), // 历史行情最后时间
- newTime = moment(props.quoteData.lasttime), // 实时行情最新时间
- newPrice = props.quoteData.last; // 实时行情最新价
- let cycleMilliseconds = 60 * 1000; // 周期毫秒数
- switch (props.cycleType) {
- case CycleType.minutes5:
- cycleMilliseconds *= 5;
- break;
- case CycleType.minutes30:
- cycleMilliseconds *= 30;
- break;
- case CycleType.minutes60:
- cycleMilliseconds *= 60;
- break;
- case CycleType.hours2:
- cycleMilliseconds *= 2 * 60;
- break;
- case CycleType.Hours4:
- cycleMilliseconds *= 4 * 60;
- break;
- case CycleType.days:
- cycleMilliseconds *= 24 * 60;
- break;
- }
- const diffTime = newTime.valueOf() - lastTime.valueOf(); // 计算时间差
- if (diffTime > cycleMilliseconds * 2) {
- // 时间间隔超过两个周期,重新请求历史数据
- } else {
- // 判断时间差是否大于周期时间
- if (diffTime > cycleMilliseconds) {
- lastTime.add(cycleMilliseconds, 'ms');
- // 添加历史行情
- source.push({
- date: lastTime.format('YYYY-MM-DD HH:mm:ss'),
- open: newPrice,
- close: newPrice,
- lowest: newPrice,
- highest: newPrice,
- ma5: '-',
- ma10: '-',
- ma15: '-',
- vol: 0,
- macd: '-',
- dif: '-',
- dea: '-',
- k: '-',
- d: '-',
- j: '-',
- cci: '-',
- });
- historyIndexs.push(lastIndex + 1); // 添加历史行情索引
- } else {
- const lastData = source[lastIndex];
- if (lastData.lowest > newPrice) {
- lastData.lowest = newPrice; //更新最低价
- }
- if (lastData.highest < newPrice) {
- lastData.highest = newPrice; //更新最高价
- }
- lastData.close = newPrice; //更新收盘价
- }
- calcMA('ma5', 5);
- calcMA('ma10', 10);
- calcMA('ma15', 15);
- calcMACD();
- calcKDJ();
- clacCCI();
- // 延迟图表更新,减少卡顿
- debounce(() => {
- updateOptions(props.seriesType);
- }, 1000);
- }
- };
- // 监听行情最新价推送
- watch(
- () => props.quoteData.last,
- () => {
- if (!loading.value) {
- updateChartData();
- }
- }
- );
- // 监听指标类型
- watch(
- () => props.seriesType,
- (val) => {
- if (!loading.value) {
- updateOptions(val);
- }
- }
- );
- // 监听周期选择变化
- watchEffect(() => {
- loading.value = true;
- const params: QueryHistoryDatas = {
- cycleType: props.cycleType,
- goodsCode: props.quoteData.goodscode.toUpperCase(),
- count: 1440,
- };
- // 查询K线数据
- queryHistoryDatas(params)
- .then((res) => {
- if (res.length) {
- isEmpty.value = false;
- // 日期升序排序
- const kdata = res.sort((a, b) => moment(a.ts).valueOf() - moment(b.ts).valueOf());
- handleData(kdata);
- } else {
- isEmpty.value = true;
- }
- initOptions(props.seriesType);
- })
- .catch(() => {
- isEmpty.value = true;
- })
- .finally(() => {
- loading.value = false;
- });
- });
- return {
- loading,
- isEmpty,
- options,
- };
- },
- });
- </script>
|