st-comp 0.0.164 → 0.0.166

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (145) hide show
  1. package/components.d.ts +3 -0
  2. package/es/ChartLayout.cjs +1 -1
  3. package/es/ChartLayout.js +3 -3
  4. package/es/Dialog.cjs +1 -1
  5. package/es/Dialog.js +16 -14
  6. package/es/FactorWarning.cjs +1 -1
  7. package/es/FactorWarning.js +492 -614
  8. package/es/Kline.cjs +1 -1
  9. package/es/Kline.js +16 -16
  10. package/es/KlineBasic.cjs +1 -1
  11. package/es/KlineBasic.js +477 -766
  12. package/es/KlineConfig.cjs +1 -0
  13. package/es/KlineConfig.js +703 -0
  14. package/es/KlineNew.cjs +1 -1
  15. package/es/KlineNew.js +14 -14
  16. package/es/KlinePlus.cjs +6 -0
  17. package/es/KlinePlus.js +1212 -0
  18. package/es/Pagination.cjs +1 -1
  19. package/es/Pagination.js +21 -20
  20. package/es/PasswordPrompt.cjs +1 -1
  21. package/es/PasswordPrompt.js +3 -3
  22. package/es/Table.cjs +1 -1
  23. package/es/Table.js +100 -279
  24. package/es/User.cjs +1 -1
  25. package/es/User.js +41 -39
  26. package/es/VarSelectDialog.cjs +1 -1
  27. package/es/VarSelectDialog.js +23 -22
  28. package/es/VarietySearch.cjs +1 -1
  29. package/es/VarietySearch.js +34 -32
  30. package/es/VirtualTable.cjs +5 -5
  31. package/es/VirtualTable.js +1018 -1056
  32. package/es/{_initCloneObject-47814efa.cjs → _initCloneObject-9ccbb113.cjs} +1 -1
  33. package/es/{_initCloneObject-c939f3af.js → _initCloneObject-a9305c1e.js} +3 -3
  34. package/es/base-4bcd2756.js +304 -0
  35. package/es/base-bdc10baa.cjs +5 -0
  36. package/es/{castArray-a78ae889.cjs → castArray-37eb9980.cjs} +1 -1
  37. package/es/{castArray-2440268f.js → castArray-681d750b.js} +1 -1
  38. package/es/{config-provider-722dca36.js → config-provider-93846ae3.js} +9 -9
  39. package/es/config-provider-ec6b09df.cjs +1 -0
  40. package/es/dayjs.min-19fb125f.cjs +1 -0
  41. package/es/dayjs.min-54a4e095.js +296 -0
  42. package/es/{debounce-82e23c4b.js → debounce-51046b06.js} +1 -1
  43. package/es/{debounce-cf26af24.cjs → debounce-9e323221.cjs} +1 -1
  44. package/es/{dropdown-6f8379f0.js → dropdown-65e74de8.js} +6 -6
  45. package/es/{dropdown-790d31ea.cjs → dropdown-b2dab5bb.cjs} +1 -1
  46. package/es/{el-autocomplete-75762ef9.js → el-autocomplete-5064d466.js} +9 -9
  47. package/es/{el-autocomplete-84de99ba.cjs → el-autocomplete-814993af.cjs} +1 -1
  48. package/es/el-button-960edd49.cjs +1 -0
  49. package/es/{el-button-2e878325.js → el-button-d39472d3.js} +37 -36
  50. package/es/{el-checkbox-group-45247bbf.cjs → el-checkbox-group-020b49f5.cjs} +1 -1
  51. package/es/{el-checkbox-group-5e4f32f0.js → el-checkbox-group-779f353a.js} +2 -2
  52. package/es/el-dialog-cd484c1f.cjs +1 -0
  53. package/es/el-dialog-f45ec2a1.js +267 -0
  54. package/es/el-divider-40c50ce7.cjs +1 -0
  55. package/es/el-divider-7cf13678.js +45 -0
  56. package/es/{el-empty-2ece1134.cjs → el-empty-627ec09e.cjs} +1 -1
  57. package/es/{el-empty-7f8acbdc.js → el-empty-64b2ea8a.js} +2 -2
  58. package/es/{el-form-item-45ba08ff.js → el-form-item-b31e7a98.js} +8 -8
  59. package/es/{el-form-item-47e255d4.cjs → el-form-item-e1c3104b.cjs} +2 -2
  60. package/es/{el-input-1323afd2.js → el-input-42315ac4.js} +48 -48
  61. package/es/{el-input-cb340042.cjs → el-input-5163bda3.cjs} +1 -1
  62. package/es/{el-input-number-0f194f3f.cjs → el-input-number-eb54cde4.cjs} +1 -1
  63. package/es/{el-input-number-991f8bfc.js → el-input-number-ebd64e22.js} +19 -19
  64. package/es/el-loading-15a5501c.cjs +1 -0
  65. package/es/el-loading-5d29f26d.js +187 -0
  66. package/es/{el-menu-item-4c9eb7ad.cjs → el-menu-item-ac89e804.cjs} +1 -1
  67. package/es/{el-menu-item-dbbcd01a.js → el-menu-item-d62e0a3f.js} +32 -32
  68. package/es/el-message-ad87b9dd.cjs +1 -0
  69. package/es/{el-message-03e6f342.js → el-message-bd1d2c82.js} +17 -17
  70. package/es/el-overlay-690b50aa.js +269 -0
  71. package/es/el-overlay-e5d318c3.cjs +1 -0
  72. package/es/{el-popconfirm-8372cd13.js → el-popconfirm-88bbd066.js} +13 -13
  73. package/es/{el-popconfirm-86503f8a.cjs → el-popconfirm-a497b279.cjs} +1 -1
  74. package/es/{el-popper-7adbf1c6.cjs → el-popper-14a9d493.cjs} +1 -1
  75. package/es/{el-popper-b5315f5b.js → el-popper-85365d5b.js} +63 -63
  76. package/es/{el-scrollbar-ac525239.cjs → el-scrollbar-8bf765f2.cjs} +1 -1
  77. package/es/{el-scrollbar-4dc1493a.js → el-scrollbar-bcf29780.js} +21 -21
  78. package/es/el-segmented-84707138.js +136 -0
  79. package/es/el-segmented-bccd9f2a.cjs +1 -0
  80. package/es/{el-select-31f8ac8a.cjs → el-select-1d2309a1.cjs} +1 -1
  81. package/es/{el-select-a4ad7c8b.js → el-select-d3bce4d1.js} +13 -13
  82. package/es/{el-table-column-2e7132b3.js → el-table-column-9691b26d.js} +19 -18
  83. package/es/el-table-column-be9a76bb.cjs +14 -0
  84. package/es/{el-tag-a79385c6.js → el-tag-6036f51c.js} +39 -39
  85. package/es/{el-tag-9732314f.cjs → el-tag-8d49d4d2.cjs} +1 -1
  86. package/es/{index-375e8de4.cjs → index-02ca0096.cjs} +1 -1
  87. package/es/{index-afc88a4b.cjs → index-19fa1fcf.cjs} +1 -1
  88. package/es/{index-733cd68a.js → index-21fb27eb.js} +17 -17
  89. package/es/{index-e5fe37a4.js → index-3425f2f0.js} +10 -10
  90. package/es/{index-1d85ba4b.cjs → index-5b81f4da.cjs} +1 -1
  91. package/es/index-8d3c5e96.cjs +3 -0
  92. package/es/{index-97d24ab7.js → index-8ee89b40.js} +16 -16
  93. package/es/{index-9e6e8d03.cjs → index-a99a5307.cjs} +1 -1
  94. package/es/{index-58521b9e.cjs → index-b90c746b.cjs} +1 -1
  95. package/es/index-c45ac024.js +14 -0
  96. package/es/{index-f5b9b211.js → index-cb8157f2.js} +2 -2
  97. package/es/{index-e6a36e09.js → index-d3c37134.js} +7 -7
  98. package/es/{index-65b4113e.cjs → index-d94bae37.cjs} +1 -1
  99. package/es/{index-2d9c78d4.cjs → index-e348d5e8.cjs} +1 -1
  100. package/es/{index-57a7323a.js → index-ef7c4392.js} +11 -11
  101. package/es/{index-e3353eeb.js → index-fbe85773.js} +13 -13
  102. package/es/{raf-f46caea0.cjs → raf-07cb6953.cjs} +1 -1
  103. package/es/{raf-c817c3f7.js → raf-36faa519.js} +1 -1
  104. package/es/{scroll-4fe7a76f.js → scroll-0277d145.js} +1 -1
  105. package/es/{scroll-339681d4.cjs → scroll-4769a65d.cjs} +1 -1
  106. package/es/style.css +1 -1
  107. package/es/{use-form-common-props-04fb0752.js → use-form-common-props-54c31983.js} +40 -40
  108. package/es/{use-form-common-props-74a96df2.cjs → use-form-common-props-592797e2.cjs} +1 -1
  109. package/es/use-global-config-584b62f1.cjs +1 -0
  110. package/es/use-global-config-946f61a4.js +73 -0
  111. package/es/{vnode-b15eb11b.cjs → vnode-1b827c99.cjs} +1 -1
  112. package/es/{vnode-0fd3003d.js → vnode-dfc92f20.js} +1 -1
  113. package/es/{zh-cn-cac8283b.js → zh-cn-5066f5e9.js} +2 -2
  114. package/es/{zh-cn-36f34912.cjs → zh-cn-b24a7dc9.cjs} +1 -1
  115. package/lib/bundle.js +1 -1
  116. package/lib/bundle.umd.cjs +217 -212
  117. package/lib/{index-5f63dc2e.js → index-80531492.js} +39042 -37182
  118. package/lib/{python-5918f7d5.js → python-20a1a5fc.js} +1 -1
  119. package/lib/style.css +1 -1
  120. package/package.json +1 -1
  121. package/packages/KlineConfig/config.js +39 -0
  122. package/packages/KlineConfig/index.ts +8 -0
  123. package/packages/KlineConfig/index.vue +315 -0
  124. package/packages/KlinePlus/components/SliderChart.vue +218 -0
  125. package/packages/KlinePlus/components/Tips.vue +36 -0
  126. package/packages/KlinePlus/images/buy.svg +1 -0
  127. package/packages/KlinePlus/images/sell.svg +1 -0
  128. package/packages/KlinePlus/images/t.svg +1 -0
  129. package/packages/KlinePlus/index.ts +8 -0
  130. package/packages/KlinePlus/index.vue +880 -0
  131. package/packages/KlinePlus/utils.js +705 -0
  132. package/packages/index.ts +4 -0
  133. package/src/pages/KlineConfig/index.vue +20 -0
  134. package/src/pages/KlinePlus/index.vue +180 -0
  135. package/src/router/routes.ts +10 -0
  136. package/es/base-0ca7c43a.cjs +0 -5
  137. package/es/base-b56cf478.js +0 -303
  138. package/es/config-provider-cfbb9cd8.cjs +0 -1
  139. package/es/el-button-62966947.cjs +0 -1
  140. package/es/el-message-d5ed7a4f.cjs +0 -1
  141. package/es/el-overlay-41994f67.cjs +0 -1
  142. package/es/el-overlay-47b665a0.js +0 -521
  143. package/es/el-table-column-bdc46568.cjs +0 -14
  144. package/es/index-0baeaec7.js +0 -82
  145. package/es/index-2f958e56.cjs +0 -3
@@ -0,0 +1,705 @@
1
+ import dayjs from "dayjs";
2
+ import { stMath } from "st-func";
3
+
4
+ const { round } = stMath;
5
+
6
+ // 获取K线展示根数上限配置
7
+ export const loadKlineConfig = {
8
+ // 单屏展示条目上限
9
+ minValueSpan: 5,
10
+ maxValueSpan: 2000,
11
+ // 加载更多: 触发阈值
12
+ loadCheckCount: 500,
13
+ // 加载更多: 载入条目
14
+ loadAddCount: 2000,
15
+ };
16
+
17
+ // 获取副图配置
18
+ const getSubBarStyle = (data, index) => {
19
+ const itemData = data[index];
20
+ const preItemData = index === 0 ? data[index] : data[index - 1];
21
+ if (itemData[0] === itemData[1]) {
22
+ return itemData[0] >= preItemData[1]
23
+ ? {
24
+ color: "transparent",
25
+ borderColor: "#FF0000",
26
+ }
27
+ : {
28
+ color: "#00FFFF",
29
+ };
30
+ } else {
31
+ return itemData[1] > itemData[0]
32
+ ? {
33
+ color: "transparent",
34
+ borderColor: "#FF0000",
35
+ }
36
+ : {
37
+ color: "#00FFFF",
38
+ };
39
+ }
40
+ };
41
+ export const getSubOptions = (klineData, startValue, endValue) => {
42
+ const series = klineData.subIndicator.map((item) => {
43
+ if (item.series === "bar") {
44
+ return {
45
+ name: "subMain",
46
+ xAxisIndex: 0,
47
+ yAxisIndex: 1,
48
+ type: "bar",
49
+ silent: true,
50
+ symbol: "none",
51
+ data: item.data.map((val, index) => {
52
+ if (item.seriesColor === "kline") {
53
+ return {
54
+ value: val,
55
+ itemStyle: getSubBarStyle(klineData.data, index),
56
+ };
57
+ } else if (item.seriesColor === "value") {
58
+ return {
59
+ value: val,
60
+ itemStyle: {
61
+ color: val >= 0 ? "#FF0000" : "#00FFFF",
62
+ },
63
+ };
64
+ } else {
65
+ return {
66
+ value: val,
67
+ itemStyle: {
68
+ color: item.seriesColor,
69
+ },
70
+ };
71
+ }
72
+ }),
73
+ };
74
+ } else if (item.series === "line") {
75
+ return {
76
+ xAxisIndex: 0,
77
+ yAxisIndex: item.yAxis === "right" ? 2 : 1,
78
+ name: item.key,
79
+ type: "line",
80
+ silent: true,
81
+ symbol: "none",
82
+ data: item.data,
83
+ lineStyle: {
84
+ width: 1,
85
+ },
86
+ itemStyle: {
87
+ color: item.color,
88
+ },
89
+ };
90
+ }
91
+ });
92
+ return {
93
+ animation: false,
94
+ grid: {
95
+ top: "10px",
96
+ left: "60px",
97
+ right: "20px",
98
+ bottom: "10px",
99
+ },
100
+ dataZoom: [
101
+ {
102
+ type: "inside",
103
+ startValue,
104
+ endValue,
105
+ maxValueSpan: loadKlineConfig.maxValueSpan,
106
+ },
107
+ ],
108
+ tooltip: {
109
+ show: false,
110
+ },
111
+ axisPointer: {
112
+ show: true,
113
+ triggerTooltip: false,
114
+ label: {
115
+ rich: {},
116
+ formatter: (data) => {
117
+ const { axisDimension, value } = data;
118
+ if (axisDimension === "x") {
119
+ return null;
120
+ } else if (data.axisIndex === 1) {
121
+ return String(round(value));
122
+ }
123
+ },
124
+ },
125
+ },
126
+ xAxis: {
127
+ type: "category",
128
+ data: klineData.time,
129
+ axisLine: {
130
+ show: true,
131
+ },
132
+ splitLine: {
133
+ show: false,
134
+ },
135
+ axisLabel: {
136
+ show: false,
137
+ },
138
+ },
139
+ yAxis: [
140
+ {
141
+ position: "right",
142
+ },
143
+ {
144
+ position: "left",
145
+ min: klineData.subIndicator[0]?.leftYAxisRange === "cover" ? (value) => value.min : null,
146
+ max: klineData.subIndicator[0]?.leftYAxisRange === "cover" ? (value) => value.max : null,
147
+ splitNumber: 1,
148
+ axisLine: {
149
+ show: true,
150
+ },
151
+ splitLine: {
152
+ show: true,
153
+ lineStyle: {
154
+ type: "dotted",
155
+ color: "#333",
156
+ },
157
+ },
158
+ },
159
+ {
160
+ position: "right",
161
+ min: klineData.subIndicator[0]?.rightYAxisRange === "cover" ? (value) => value.min : null,
162
+ max: klineData.subIndicator[0]?.rightYAxisRange === "cover" ? (value) => value.max : null,
163
+ splitNumber: 1,
164
+ axisLine: {
165
+ show: false,
166
+ },
167
+ splitLine: {
168
+ show: false,
169
+ },
170
+ axisLabel: {
171
+ show: false,
172
+ },
173
+ },
174
+ ],
175
+ series,
176
+ };
177
+ };
178
+
179
+ /**
180
+ * @description: 二分查找法
181
+ * @param {Array}: 值数组
182
+ * @param {any}: 值
183
+ * @param {string}: 查找模式
184
+ * - "strict": 精确查找(默认)
185
+ * - "gte": 查找第一个大于等于目标值的元素
186
+ * @returns {Number}: 索引 (-1表示未找到)
187
+ */
188
+ export const binarySearch = (arr, target, type = "strict") => {
189
+ let left = 0;
190
+ let right = arr.length - 1;
191
+ let result = -1; // 仅用于 gte 模式
192
+
193
+ while (left <= right) {
194
+ let mid = Math.floor((left + right) / 2);
195
+ if (type === "strict") {
196
+ // 精确查找模式
197
+ if (arr[mid] === target) {
198
+ return mid;
199
+ } else if (arr[mid] < target) {
200
+ // 左半区
201
+ left = mid + 1;
202
+ } else {
203
+ // 右半区
204
+ right = mid - 1;
205
+ }
206
+ }
207
+ // 大于等于模式
208
+ else if (type === "gte") {
209
+ if (arr[mid] >= target) {
210
+ result = mid; // 记录可能的结果
211
+ right = mid - 1; // 继续向左尝试找到更小的匹配
212
+ } else {
213
+ left = mid + 1;
214
+ }
215
+ }
216
+ }
217
+
218
+ return type === "strict" ? -1 : result;
219
+ };
220
+
221
+ // K线数据合并
222
+ export const mergeklineData = (leftData, rightData) => {
223
+ return {
224
+ data: [...leftData.data, ...rightData.data.slice(1)],
225
+ mainIndicator: leftData.mainIndicator.map((item, index) => ({
226
+ ...item,
227
+ data: [...item.data, ...rightData.mainIndicator[index].data.slice(1)],
228
+ })),
229
+ subIndicator: leftData.subIndicator.map((item, index) => ({
230
+ ...item,
231
+ data: [...item.data, ...rightData.subIndicator[index].data.slice(1)],
232
+ })),
233
+ time: [...leftData.time, ...rightData.time.slice(1)],
234
+ };
235
+ };
236
+
237
+ // 将时间标准化为K线时间(精准计算)
238
+ export const normalizeToKlineTime = (klineTimeArray, time, cycle) => {
239
+ // 5, 6, 7, 8特殊处理
240
+ switch (cycle) {
241
+ /**
242
+ * @description: 60min
243
+ * 按照最新的20根K线时分秒做匹配
244
+ * 不进行步阶计算的原因是无法确认首根K线的时间, 例如真实时间 09:35:00, 该品种首根可能是 10:00:00 也可能是 10:30:00
245
+ */
246
+ case "5": {
247
+ let HmsArray = [
248
+ ...new Set(
249
+ klineTimeArray.slice(-20).map((item) => {
250
+ const date = new Date(item);
251
+ const hour = String(date.getHours()).padStart(2, "0");
252
+ const minute = String(date.getMinutes()).padStart(2, "0");
253
+ const second = String(date.getSeconds()).padStart(2, "0");
254
+ return `${hour}:${minute}:${second}`;
255
+ })
256
+ ),
257
+ ];
258
+ if (HmsArray.includes("00:00:00")) {
259
+ HmsArray.splice(HmsArray.indexOf("00:00:00"), 1);
260
+ HmsArray.push("24:00:00");
261
+ }
262
+ HmsArray = HmsArray.sort((a, b) => new Date(`2000-01-30 ${a}`).getTime() - new Date(`2000-01-30 ${b}`).getTime());
263
+
264
+ const date = new Date(time);
265
+ const year = date.getFullYear();
266
+ const month = String(date.getMonth() + 1).padStart(2, "0");
267
+ const day = String(date.getDate()).padStart(2, "0");
268
+ const hour = String(date.getHours()).padStart(2, "0");
269
+ const minute = String(date.getMinutes()).padStart(2, "0");
270
+ const second = String(date.getSeconds()).padStart(2, "0");
271
+
272
+ // 该类为数据中台补全的数据, 不管它
273
+ if (`${hour}:${minute}:${second}` === "00:00:00") {
274
+ return `${year}-${month}-${day} 01:00:00`;
275
+ }
276
+ // 进行处理
277
+ else {
278
+ let Hms = null;
279
+ for (let index = 0; index < HmsArray.length; index++) {
280
+ const item = HmsArray[index].split(":");
281
+ if (Number(`${item[0]}${item[1]}${item[2]}`) >= Number(`${hour}${minute}${second}`)) {
282
+ Hms = HmsArray[index];
283
+ break;
284
+ }
285
+ }
286
+ if (Hms === "24:00:00") {
287
+ const date = new Date(time);
288
+ date.setDate(date.getDate() + 1);
289
+ const year = date.getFullYear();
290
+ const month = String(date.getMonth() + 1).padStart(2, "0");
291
+ const day = String(date.getDate()).padStart(2, "0");
292
+ return `${year}-${month}-${day} 00:00:00`;
293
+ }
294
+ return `${year}-${month}-${day} ${Hms}`;
295
+ }
296
+ }
297
+ /**
298
+ * @description: 1d
299
+ * 因为考虑到期货的操作, 并且日线不像周月线那样直接统一成周五,或月末, 如果直接+1天会面临可能存在的非交易日的情况
300
+ * 所以针对 [21:00:00 ~ 04:00:00)的数据, 都要进行匹配的算法, 即去klineTimeArray找到当前time的下一个日期
301
+ */
302
+ case "6": {
303
+ const date = new Date(time);
304
+ const hours = date.getHours();
305
+ // 常规处理的 [04:00:00 ~ 21:00:00) => 直接格式化
306
+ if (hours >= 4 && hours < 21) {
307
+ const year = date.getFullYear();
308
+ const month = String(date.getMonth() + 1).padStart(2, "0");
309
+ const day = String(date.getDate()).padStart(2, "0");
310
+ return `${year}-${month}-${day} 09:00:00`;
311
+ }
312
+ // 特殊处理的 [21:00:00 ~ 04:00:00) => 匹配算法
313
+ else {
314
+ const currentDate = new Date(time);
315
+ // 如果是21:00:00之后, 需要将日期+1天
316
+ if (currentDate.getHours() >= 21) {
317
+ currentDate.setDate(currentDate.getDate() + 1);
318
+ }
319
+ const year = currentDate.getFullYear();
320
+ const month = String(currentDate.getMonth() + 1).padStart(2, "0");
321
+ const day = String(currentDate.getDate()).padStart(2, "0");
322
+ const currentDateStr = `${year}-${month}-${day} 09:00:00`;
323
+
324
+ // 在klineTimeArray中找到当前日期或之后的第一个交易日 (二分优化)
325
+ const idx = binarySearch(klineTimeArray, currentDateStr, "gte");
326
+ return idx === -1 ? currentDateStr : klineTimeArray[idx];
327
+ }
328
+ }
329
+ /**
330
+ * @description: 1w
331
+ * 周五 || 下周五
332
+ */
333
+ case "7": {
334
+ const date = new Date(time);
335
+ const startOfWeek = new Date(date);
336
+ startOfWeek.setDate(date.getDate() - date.getDay());
337
+ startOfWeek.setHours(0, 0, 0, 0);
338
+ const nowWeek5 = new Date(startOfWeek);
339
+ nowWeek5.setDate(startOfWeek.getDate() + 5);
340
+ nowWeek5.setHours(23, 59, 59, 0);
341
+ const nextWeek5 = new Date(startOfWeek);
342
+ nextWeek5.setDate(startOfWeek.getDate() + 12);
343
+ nextWeek5.setHours(9, 0, 0, 0);
344
+ const result = date <= nowWeek5 ? nowWeek5 : nextWeek5;
345
+ const year = result.getFullYear();
346
+ const month = String(result.getMonth() + 1).padStart(2, "0");
347
+ const day = String(result.getDate()).padStart(2, "0");
348
+ return `${year}-${month}-${day} 09:00:00`;
349
+ }
350
+ /**
351
+ * @description: 1mon
352
+ * 本月最后一天
353
+ */
354
+ case "8": {
355
+ const date = new Date(time);
356
+ const nextMonth = new Date(date.getFullYear(), date.getMonth() + 1, 1);
357
+ const endOfMonth = new Date(nextMonth);
358
+ endOfMonth.setDate(nextMonth.getDate() - 1);
359
+ endOfMonth.setHours(9, 0, 0, 0);
360
+ const year = endOfMonth.getFullYear();
361
+ const month = String(endOfMonth.getMonth() + 1).padStart(2, "0");
362
+ const day = String(endOfMonth.getDate()).padStart(2, "0");
363
+ return `${year}-${month}-${day} 09:00:00`;
364
+ }
365
+ }
366
+ // 1, 2, 3, 4步阶计算
367
+ const date = new Date(time);
368
+ const year = date.getFullYear();
369
+ const month = date.getMonth(); // 0-based
370
+ const day = date.getDate();
371
+ const hour = date.getHours();
372
+ const minute = date.getMinutes();
373
+ const second = date.getSeconds();
374
+ const computeSmallCycle = (periodMinutes) => {
375
+ let targetMinute = null;
376
+ // 计算是否需要进位
377
+ const currentPeriod = Math.floor(minute / periodMinutes);
378
+ if (minute % periodMinutes === 0 && second === 0) {
379
+ // 正好在周期边界上且秒为0,不调整
380
+ targetMinute = minute;
381
+ } else {
382
+ // 计算下一个周期点
383
+ targetMinute = (currentPeriod + 1) * periodMinutes;
384
+ }
385
+
386
+ // 处理小时, 分钟的进位
387
+ const minuteCarry = Math.floor(targetMinute / 60);
388
+ const adjustedHour = hour + minuteCarry;
389
+ const adjustedMinute = targetMinute % 60;
390
+
391
+ // 创建新日期(自动处理日/月/年进位)
392
+ const resultDate = new Date(year, month, day, adjustedHour, adjustedMinute, 0);
393
+
394
+ // 格式化输出
395
+ const formatPart = (num) => String(num).padStart(2, "0");
396
+ const formattedYear = resultDate.getFullYear();
397
+ const formattedMonth = formatPart(resultDate.getMonth() + 1);
398
+ const formattedDay = formatPart(resultDate.getDate());
399
+ const formattedHour = formatPart(resultDate.getHours());
400
+ const formattedMinute = formatPart(resultDate.getMinutes());
401
+
402
+ return `${formattedYear}-${formattedMonth}-${formattedDay} ${formattedHour}:${formattedMinute}:00`;
403
+ };
404
+ switch (cycle) {
405
+ case "1":
406
+ return computeSmallCycle(1); // 1m周期
407
+ case "2":
408
+ return computeSmallCycle(5); // 5m周期
409
+ case "3":
410
+ return computeSmallCycle(15); // 15m周期
411
+ case "4":
412
+ return computeSmallCycle(30); // 30m周期
413
+ default:
414
+ throw new Error(`Unsupported cycle: ${time} ${cycle}`);
415
+ }
416
+ };
417
+
418
+ // 将时间标准化为K线时间范围(匹配算法)
419
+ export const normalizeToKlineTimeByMatch = (klineTimeArray, timeRange, cycle) => {
420
+ let klineStart = null;
421
+ let klineEnd = null;
422
+ const [start, end] = timeRange;
423
+ switch (cycle) {
424
+ // 日线
425
+ case "6": {
426
+ klineStart = klineTimeArray.find((t) => new Date(dayjs(t).format("YYYY-MM-DD 23:59:59")).getTime() >= new Date(start).getTime());
427
+ klineEnd = klineTimeArray.findLast((t) => new Date(dayjs(t).format("YYYY-MM-DD 00:00:00")).getTime() <= new Date(end).getTime());
428
+ break;
429
+ }
430
+ // 周线
431
+ case "7": {
432
+ const format5 = dayjs(end).endOf("week").add(1, "day").format("YYYY-MM-DD 23:59:59");
433
+ klineStart = klineTimeArray.find((t) => dayjs(t).endOf("week").add(1, "day").format("YYYY-MM-DD") === dayjs(start).endOf("week").add(1, "day").format("YYYY-MM-DD"));
434
+ klineEnd = klineTimeArray.findLast((t) => new Date(t).getTime() <= new Date(format5).getTime());
435
+ break;
436
+ }
437
+ // 月线 (仅判断是否同属本月即可)
438
+ case "8": {
439
+ klineStart = klineTimeArray.find((t) => dayjs(t).format("YYYY-MM") === dayjs(start).format("YYYY-MM"));
440
+ klineEnd = klineTimeArray.findLast((t) => dayjs(t).format("YYYY-MM") === dayjs(end).format("YYYY-MM"));
441
+ break;
442
+ }
443
+ // 常规匹配
444
+ default: {
445
+ klineStart = klineTimeArray.find((t) => new Date(t).getTime() >= new Date(start).getTime());
446
+ klineEnd = klineTimeArray.findLast((t) => new Date(t).getTime() <= new Date(end).getTime());
447
+ }
448
+ }
449
+ return [klineStart, klineEnd]
450
+ };
451
+
452
+ // 统一处理markPoint标记点的偏移量
453
+ export const handleMarkPointOffset = (markPointData) => {
454
+ const data = [];
455
+ markPointData.reduce((result, item) => {
456
+ // 1. 找到当前标记的位置
457
+ const key = `${item.coord[0]}+${item.label.position}`;
458
+ const baseOffset = item.label.position === "top" ? -16 : 16;
459
+ // 2. 找到先前处于相同位置 [key] 标记的数量
460
+ const sameNum = result.get(key);
461
+ if (sameNum) {
462
+ result.set(key, sameNum + 1);
463
+ item.symbolOffset[1] = baseOffset * (sameNum + 1);
464
+ } else {
465
+ result.set(key, 1);
466
+ item.symbolOffset[1] = item.label.position === "top" ? -7 : 7;
467
+ }
468
+ data.push(item);
469
+ return result;
470
+ }, new Map());
471
+ return data;
472
+ };
473
+
474
+ // 生成图表配置所需数据: 成交点位, 成交点位连线
475
+ export const handleMarkPointTradeLog = (tradeLog, cycle, sellBuy, klineTimeArray, klineDataArray) => {
476
+ // 获取交易类型
477
+ const handleTradeType = (data, type) => {
478
+ const { direction = "", tradeAction = "", tradeType = "" } = data;
479
+ const key = tradeType ?? direction + tradeAction;
480
+ switch (type) {
481
+ // 买卖
482
+ case 0: {
483
+ const keyMap = new Map([
484
+ ["开多", "买"],
485
+ ["平多", "卖"],
486
+ ["开空", "卖"],
487
+ ["平空", "买"],
488
+ ]);
489
+ return keyMap.get(key);
490
+ }
491
+ // 开平
492
+ case 1: {
493
+ return tradeType ?? direction + tradeAction;
494
+ }
495
+ }
496
+ };
497
+ // 交易点: 买卖配置数据
498
+ const handleSellBuyConfig = (data, baseLineData, klineTimeAry, klineDataAry) => {
499
+ return data.reduce((result, next) => {
500
+ const tradeType = handleTradeType(next, 0);
501
+ // 找寻同一根K线上是否有其它点位类型
502
+ const timeTradeList = data.filter((item) => item.klineTime === next.klineTime);
503
+ const timeTradeTypes = [...new Set(timeTradeList.map((item) => handleTradeType(item, 0)))];
504
+ let symbol = null;
505
+ if (timeTradeTypes.length > 1) {
506
+ symbol = "image://" + new URL("./images/t.svg", import.meta.url).href;
507
+ } else {
508
+ symbol = "image://" + new URL(`./images/${tradeType === "买" ? "buy" : "sell"}.svg`, import.meta.url).href;
509
+ }
510
+ // 获取对应K线索引, 图标Y值永远为: 高, 连线起始Y值为: 低, 末端Y值为: 高
511
+ const klineIndex = binarySearch(klineTimeAry, next.klineTime);
512
+ const yAxisValue = klineDataAry[klineIndex]?.[3];
513
+ baseLineData.forEach((item) => {
514
+ const rangeIndex = tradeType === "买" ? 0 : 1;
515
+ if (item.range[rangeIndex] === next.klineTime) {
516
+ item.rangeValue[rangeIndex] = klineDataAry[klineIndex]?.[tradeType === "买" ? 2 : 3];
517
+ }
518
+ });
519
+ result.push({
520
+ symbol,
521
+ symbolSize: 25,
522
+ symbolRotate: 0,
523
+ symbolOffset: [0, 0],
524
+ label: {
525
+ show: false,
526
+ position: "top",
527
+ },
528
+ coord: [next.klineTime, yAxisValue],
529
+ silent: true,
530
+ animation: false,
531
+ customData: {
532
+ tradeType,
533
+ amount: next.amount, // 手数
534
+ part: next.part, // 份数
535
+ profitAndLoss: next.profitAndLoss, // 盈亏
536
+ openPriceAll: next.openPriceAll, // 开仓价
537
+ closePriceAll: next.closePriceAll, // 收仓价
538
+ },
539
+ });
540
+ return result;
541
+ }, []);
542
+ };
543
+ // 交易点: 开平配置数据
544
+ const handleOpenCloseConfig = (data, baseLineData, klineTimeAry, klineDataAry) => {
545
+ return data.reduce((result, next, index) => {
546
+ // 1.交易类型
547
+ const tradeType = handleTradeType(next, 1);
548
+ // 2.图标位置 [开在K线上方,平在K线下方]
549
+ const position = ["开多", "开空"].includes(tradeType) ? "top" : "bottom";
550
+ // 3.获取对应Y轴值
551
+ const klineIndex = binarySearch(klineTimeAry, next.klineTime);
552
+ const yAxisValue = position === "top" ? klineDataAry[klineIndex]?.[3] : klineDataAry[klineIndex]?.[2];
553
+ // 4.将Y轴值存到连线的数据中, 提供给连线配置使用
554
+ baseLineData.forEach((item) => {
555
+ const rangeTime = position === "top" ? item.range[0] : item.range[1];
556
+ if (rangeTime === next.klineTime) item.rangeValue[position === "top" ? 0 : 1] = yAxisValue;
557
+ });
558
+ result.push({
559
+ symbol: "triangle",
560
+ symbolSize: [10, 12],
561
+ symbolRotate: position === "top" ? 180 : 0,
562
+ symbolOffset: [0, 0],
563
+ label: {
564
+ show: true,
565
+ position,
566
+ color: "#fff",
567
+ formatter: `${tradeType} ${position === "top" ? "+" : "-"} ${next.amount}手 ${next.part ? `(${next.part}份)` : ""}`,
568
+ },
569
+ itemStyle: { color: position === "top" ? "#FF0000" : "#389e0d" },
570
+ coord: [next.klineTime, yAxisValue],
571
+ silent: true,
572
+ animation: false,
573
+ customData: {
574
+ tradeType,
575
+ amount: next.amount, // 手数
576
+ part: next.part, // 份数
577
+ profitAndLoss: next.profitAndLoss, // 盈亏
578
+ openPriceAll: next.openPriceAll, // 开仓价
579
+ closePriceAll: next.closePriceAll, // 收仓价
580
+ },
581
+ });
582
+ return result;
583
+ }, []);
584
+ };
585
+ // 交易点: 生成基础点位, 连线数据
586
+ const handleTradePointLineData = (tradeLog, cycle, klineTimeArray) => {
587
+ /**
588
+ * @description: 点位, 连线
589
+ * 1.生成点位, 连线基础数据
590
+ * 2.点位处理: 合并同一根K线上的数据, 整体按照时间排序
591
+ * 3.连线处理: 过滤相同范围的连线
592
+ */
593
+ const { basePointData, baseLineData } = tradeLog.reduce(
594
+ (result, item) => {
595
+ // 开仓点位的数据
596
+ const startPoint = {
597
+ pointTime: item.openTime, // 交易点位的真实时间
598
+ klineTime: normalizeToKlineTime(klineTimeArray, item.openTime, cycle), // 交易点位的K线时间
599
+
600
+ tradeAction: "开", // 交易行为
601
+ tradeDirection: item.tradeDirection ? "空" : "多", // 交易方向 1:空, 0:多
602
+ tradeType: `开${item.tradeDirection ? "空" : "多"}`,
603
+
604
+ part: null, // 份数,
605
+ amount: item.tradeVolume, // 手数
606
+ profitAndLoss: item.profitAndLoss, // 盈亏
607
+ openPriceAll: item.openPrice * item.tradeVolume, // 开仓价格
608
+ closePriceAll: item.closePrice * item.tradeVolume, // 平仓价格
609
+ };
610
+ // 平仓点位的数据
611
+ const endPoint = {
612
+ pointTime: item.closeTime, // 交易点位的真实时间
613
+ klineTime: normalizeToKlineTime(klineTimeArray, item.closeTime, cycle), // 交易点位的K线时间
614
+
615
+ tradeAction: "平", // 交易行为
616
+ tradeDirection: item.tradeDirection ? "空" : "多", // 交易方向 1:空, 0:多
617
+ tradeType: `平${item.tradeDirection ? "空" : "多"}`,
618
+
619
+ part: null, // 份数,
620
+ amount: item.tradeVolume, // 手数
621
+ profitAndLoss: item.profitAndLoss, // 盈亏
622
+ openPriceAll: item.openPrice * item.tradeVolume, // 开仓价格
623
+ closePriceAll: item.closePrice * item.tradeVolume, // 平仓价格
624
+ };
625
+ result.basePointData.push(startPoint, endPoint);
626
+ result.baseLineData.push({
627
+ range: [startPoint.klineTime, endPoint.klineTime],
628
+ rangeValue: [null, null],
629
+ color: endPoint.profitAndLoss > 0 ? "#FF0000" : "#389e0d",
630
+ });
631
+ return result;
632
+ },
633
+ {
634
+ basePointData: [], // 点位数据
635
+ baseLineData: [], // 连线数据
636
+ }
637
+ );
638
+ const basePointDataMergeMap = basePointData.reduce((result, item) => {
639
+ const newItem = JSON.parse(JSON.stringify(item));
640
+ const key = newItem.klineTime + newItem.tradeType;
641
+ // 判断这个节点是否已存在数据
642
+ if (result.has(key)) {
643
+ // 已存在,说明节点重复,进行数据合并
644
+ const oldItem = result.get(key);
645
+ newItem.amount += oldItem.amount;
646
+ newItem.part += oldItem.part;
647
+ newItem.profitAndLoss += oldItem.profitAndLoss;
648
+ newItem.openPriceAll += oldItem.openPriceAll;
649
+ newItem.closePriceAll += oldItem.closePriceAll;
650
+ }
651
+ result.set(key, newItem);
652
+ return result;
653
+ }, new Map([]));
654
+ const pointData = [...basePointDataMergeMap.values()].sort((a, b) => new Date(a.klineTime) - new Date(b.klineTime));
655
+ const lineData = baseLineData.filter(({ range }) => range[0] !== range[1]);
656
+ return { pointData, lineData };
657
+ };
658
+ const { pointData, lineData } = handleTradePointLineData(tradeLog, cycle, klineTimeArray);
659
+ const handler = sellBuy === 0 ? handleSellBuyConfig : handleOpenCloseConfig;
660
+ return {
661
+ tradePointData: handler(pointData, lineData, klineTimeArray, klineDataArray),
662
+ tradeLineData: lineData.map(({ range, rangeValue, color }) => {
663
+ return [
664
+ {
665
+ symbol: "none",
666
+ coord: [range[0], rangeValue[0]],
667
+ lineStyle: { color, width: 2, type: "solid" },
668
+ silent: true,
669
+ animation: false,
670
+ },
671
+ {
672
+ symbol: "none",
673
+ coord: [range[1], rangeValue[1]],
674
+ silent: true,
675
+ animation: false,
676
+ },
677
+ ];
678
+ }),
679
+ };
680
+ };
681
+
682
+ // 生成图表配置所需数据: 净值曲线
683
+ export const handleNetPositionLine = (netPositionData, cycle) => {
684
+ let netPositionLineData = [];
685
+ // 分钟线: 时间格式化
686
+ if (Number(cycle) <= 5) {
687
+ netPositionLineData = netPositionData.map(({ tradeDate, netPositionValue }) => {
688
+ return [dayjs(tradeDate).format("YYYY-MM-DD HH:mm:ss"), netPositionValue];
689
+ });
690
+ }
691
+ // 日线: 取每天最后一条净值数据, 且时分秒转换成固定09:00:00
692
+ else if (Number(cycle) === 6) {
693
+ const netPositionMap = netPositionData.reduce((result, item) => {
694
+ const { tradeDate, netPositionValue } = item;
695
+ result.set(dayjs(tradeDate).format("YYYY-MM-DD 09:00:00"), netPositionValue);
696
+ return result;
697
+ }, new Map());
698
+ netPositionLineData = Array.from(netPositionMap, ([key, value]) => [key, value]);
699
+ }
700
+ // 其它: 不展示
701
+ else {
702
+ netPositionLineData = [];
703
+ }
704
+ return { netPositionLineData };
705
+ };