index.vue 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356
  1. <template>
  2. <echart-base :options="[options]" :empty="isEmpty" v-model:loading="loading"></echart-base>
  3. </template>
  4. <script lang="ts">
  5. import { defineComponent, PropType, ref, watch, watchEffect } from 'vue';
  6. import { QueryHistoryDatasRsp, QueryQuoteDayRsp, QueryHistoryDatas, CycleType } from '@/services/go/quote/interface';
  7. import { QueryHistoryDatas as queryHistoryDatas } from '@/services/go/quote';
  8. import { debounce } from '@/utils/time';
  9. import EchartBase from '../echart-base/index.vue';
  10. import { handleEchart, Source } from './setup';
  11. import moment from 'moment';
  12. export default defineComponent({
  13. name: 'EchartKline',
  14. components: {
  15. EchartBase,
  16. },
  17. props: {
  18. // 实时行情数据
  19. quoteData: {
  20. type: Object as PropType<QueryQuoteDayRsp>,
  21. required: true,
  22. },
  23. // 周期类型
  24. cycleType: {
  25. type: Number as PropType<CycleType>,
  26. required: true,
  27. },
  28. // 指标类型
  29. seriesType: {
  30. type: String,
  31. default: 'MACD',
  32. },
  33. },
  34. setup(props) {
  35. const loading = ref(true);
  36. const isEmpty = ref(false);
  37. const historyIndexs: number[] = []; // 行情历史数据中所有非补充数据的索引位置(用于计算均线)
  38. const { chartData, options, updateOptions, initOptions } = handleEchart();
  39. // 处理图表数据
  40. const handleData = (rawData: QueryHistoryDatasRsp[]): void => {
  41. const { source } = chartData.value;
  42. source.length = 0;
  43. historyIndexs.length = 0;
  44. rawData.forEach((item, index) => {
  45. const { o, c, h, l, ts, tv } = item;
  46. source.push({
  47. date: moment(ts).format('YYYY-MM-DD HH:mm:ss'),
  48. open: o,
  49. close: c,
  50. lowest: l,
  51. highest: h,
  52. ma5: '-',
  53. ma10: '-',
  54. ma15: '-',
  55. vol: tv,
  56. macd: '-',
  57. dif: '-',
  58. dea: '-',
  59. k: '-',
  60. d: '-',
  61. j: '-',
  62. cci: '-',
  63. });
  64. if (!item.f) historyIndexs.push(index); // 排除补充数据
  65. });
  66. calcMA('ma5', 5);
  67. calcMA('ma10', 10);
  68. calcMA('ma15', 15);
  69. calcMACD();
  70. calcKDJ();
  71. clacCCI();
  72. };
  73. // 计算平均线
  74. const calcMA = (key: keyof Source, count: number) => {
  75. const { source } = chartData.value;
  76. let result: Source[keyof Source] = '-';
  77. if (source.length >= count) {
  78. // 均线起始位置
  79. const startIndex = historyIndexs[count - 1];
  80. for (let i = 0; i < source.length; i++) {
  81. if (startIndex !== undefined && i > startIndex) {
  82. const j = historyIndexs.findIndex((val) => val === i);
  83. // 判断是否补充数据
  84. if (j === -1) {
  85. result = source[i - 1][key]; // 取上个平均值
  86. } else {
  87. // 向后取MA数
  88. const maIndexs = historyIndexs.slice(j - (count - 1), j + 1);
  89. // 计算总价
  90. const total = maIndexs.reduce((sum, val) => sum + source[val].close, 0);
  91. // 计算均线
  92. result = (total / count).toFixed(2);
  93. }
  94. }
  95. (<typeof result>source[i][key]) = result;
  96. }
  97. }
  98. };
  99. // 计算EMA
  100. const calcEMA = (close: number[], n: number) => {
  101. const ema: number[] = [],
  102. a = 2 / (n + 1); // 平滑系数
  103. for (let i = 0; i < close.length; i++) {
  104. if (i === 0) {
  105. //第一个EMA(n)是前n个收盘价代数平均
  106. const result = close.slice(0, n).reduce((sum, val) => sum + val, 0) / n;
  107. ema.push(result);
  108. } else {
  109. // EMA(n) = α × Close + (1 - α) × EMA(n - 1)
  110. const result = a * close[i] + (1 - a) * ema[i - 1];
  111. ema.push(result);
  112. }
  113. }
  114. return ema;
  115. };
  116. // 计算DEA
  117. const calcDEA = (dif: number[]) => {
  118. return calcEMA(dif, 9);
  119. };
  120. // 计算DIF
  121. const calcDIF = (close: number[]) => {
  122. const dif: number[] = [],
  123. emaShort = calcEMA(close, 12),
  124. emaLong = calcEMA(close, 26);
  125. for (let i = 0; i < close.length; i++) {
  126. const result = emaShort[i] - emaLong[i];
  127. dif.push(result);
  128. }
  129. return dif;
  130. };
  131. // 计算MACD
  132. const calcMACD = () => {
  133. const { source } = chartData.value,
  134. close = source.map((item) => item.close),
  135. dif = calcDIF(close),
  136. dea = calcDEA(dif);
  137. for (let i = 0; i < source.length; i++) {
  138. source[i].dif = dif[i].toFixed(2);
  139. source[i].dea = dea[i].toFixed(2);
  140. source[i].macd = ((dif[i] - dea[i]) * 2).toFixed(2);
  141. }
  142. };
  143. // 计算KDJ
  144. const calcKDJ = () => {
  145. const { source } = chartData.value;
  146. for (let i = 0; i < source.length; i++) {
  147. const item = source[i];
  148. if (i < 8) {
  149. item.k = '-';
  150. item.d = '-';
  151. item.j = '-';
  152. } else {
  153. let rsv = 50; // 如果最低价等于最高价,RSV默认值为50
  154. if (item.lowest !== item.highest) {
  155. const n9 = source.slice(i - 8, i + 1).map((item) => item.close), // 取前9个收盘价
  156. max = Math.max(...n9),
  157. min = Math.min(...n9);
  158. // 计算RSV
  159. rsv = ((item.close - min) / (max - min)) * 100;
  160. }
  161. const yestK = Number(source[i - 1].k); // 取前一日K值
  162. const yestD = Number(source[i - 1].d); // 取前一日D值
  163. if (isNaN(yestK) || isNaN(yestD)) {
  164. // 如果前一日的K值或D值不存在则默认值为50
  165. item.k = '50';
  166. item.d = '50';
  167. item.j = '50';
  168. } else {
  169. const k = (2 / 3) * yestK + (1 / 3) * rsv,
  170. d = (2 / 3) * yestD + (1 / 3) * yestK,
  171. j = 3 * k - 2 * d;
  172. item.k = k.toFixed(2);
  173. item.d = d.toFixed(2);
  174. item.j = j.toFixed(2);
  175. }
  176. }
  177. }
  178. };
  179. // 计算CCI
  180. const clacCCI = () => {
  181. const { source } = chartData.value;
  182. for (let i = 0; i < source.length; i++) {
  183. const item = source[i];
  184. if (i < 13) {
  185. item.cci = '-';
  186. } else {
  187. const tp = (item.close + item.lowest + item.highest) / 3, // (收盘价 + 最低价 + 最高价) / 3
  188. n14 = source.slice(i - 13, i + 1), // 取前14条数据
  189. ma = n14.reduce((sum, e) => sum + (e.close + e.lowest + e.highest) / 3, 0) / 14, // 计算前14条数据的(TP)价总和÷N
  190. md = n14.reduce((sum, e) => sum + Math.abs(ma - (e.close + e.lowest + e.highest) / 3), 0) / 14, // 计算前14条数据的(MA-TP)价总和÷N
  191. result = (tp - ma) / md / 0.015;
  192. item.cci = result.toFixed(2);
  193. }
  194. }
  195. };
  196. // 更新图表K线数据
  197. const updateChartData = () => {
  198. const { source } = chartData.value,
  199. lastIndex = source.length - 1, // 历史行情最后索引位置
  200. lastTime = moment(source[lastIndex].date), // 历史行情最后时间
  201. newTime = moment(props.quoteData.lasttime), // 实时行情最新时间
  202. newPrice = props.quoteData.last; // 实时行情最新价
  203. let cycleMilliseconds = 60 * 1000; // 周期毫秒数
  204. switch (props.cycleType) {
  205. case CycleType.minutes5:
  206. cycleMilliseconds *= 5;
  207. break;
  208. case CycleType.minutes30:
  209. cycleMilliseconds *= 30;
  210. break;
  211. case CycleType.minutes60:
  212. cycleMilliseconds *= 60;
  213. break;
  214. case CycleType.hours2:
  215. cycleMilliseconds *= 2 * 60;
  216. break;
  217. case CycleType.Hours4:
  218. cycleMilliseconds *= 4 * 60;
  219. break;
  220. case CycleType.days:
  221. cycleMilliseconds *= 24 * 60;
  222. break;
  223. }
  224. const diffTime = newTime.valueOf() - lastTime.valueOf(); // 计算时间差
  225. if (diffTime > cycleMilliseconds * 2) {
  226. // 时间间隔超过两个周期,重新请求历史数据
  227. } else {
  228. // 判断时间差是否大于周期时间
  229. if (diffTime > cycleMilliseconds) {
  230. lastTime.add(cycleMilliseconds, 'ms');
  231. // 添加历史行情
  232. source.push({
  233. date: lastTime.format('YYYY-MM-DD HH:mm:ss'),
  234. open: newPrice,
  235. close: newPrice,
  236. lowest: newPrice,
  237. highest: newPrice,
  238. ma5: '-',
  239. ma10: '-',
  240. ma15: '-',
  241. vol: 0,
  242. macd: '-',
  243. dif: '-',
  244. dea: '-',
  245. k: '-',
  246. d: '-',
  247. j: '-',
  248. cci: '-',
  249. });
  250. historyIndexs.push(lastIndex + 1); // 添加历史行情索引
  251. } else {
  252. const lastData = source[lastIndex];
  253. if (lastData.lowest > newPrice) {
  254. lastData.lowest = newPrice; //更新最低价
  255. }
  256. if (lastData.highest < newPrice) {
  257. lastData.highest = newPrice; //更新最高价
  258. }
  259. lastData.close = newPrice; //更新收盘价
  260. }
  261. calcMA('ma5', 5);
  262. calcMA('ma10', 10);
  263. calcMA('ma15', 15);
  264. calcMACD();
  265. calcKDJ();
  266. clacCCI();
  267. // 延迟图表更新,减少卡顿
  268. debounce(() => {
  269. updateOptions(props.seriesType);
  270. }, 1000);
  271. }
  272. };
  273. // 监听行情最新价推送
  274. watch(
  275. () => props.quoteData.last,
  276. () => {
  277. if (!loading.value) {
  278. updateChartData();
  279. }
  280. }
  281. );
  282. // 监听指标类型
  283. watch(
  284. () => props.seriesType,
  285. (val) => {
  286. if (!loading.value) {
  287. updateOptions(val);
  288. }
  289. }
  290. );
  291. // 监听周期选择变化
  292. watchEffect(() => {
  293. loading.value = true;
  294. const params: QueryHistoryDatas = {
  295. cycleType: props.cycleType,
  296. goodsCode: props.quoteData.goodscode.toUpperCase(),
  297. count: 1440,
  298. };
  299. // 查询K线数据
  300. queryHistoryDatas(params)
  301. .then((res) => {
  302. if (res.length) {
  303. isEmpty.value = false;
  304. // 日期升序排序
  305. const kdata = res.sort((a, b) => moment(a.ts).valueOf() - moment(b.ts).valueOf());
  306. handleData(kdata);
  307. } else {
  308. isEmpty.value = true;
  309. }
  310. initOptions(props.seriesType);
  311. })
  312. .catch(() => {
  313. isEmpty.value = true;
  314. })
  315. .finally(() => {
  316. loading.value = false;
  317. });
  318. });
  319. return {
  320. loading,
  321. isEmpty,
  322. options,
  323. };
  324. },
  325. });
  326. </script>