@363045841yyt/klinechart-core 0.7.3 → 0.7.5

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 (247) hide show
  1. package/README.md +201 -201
  2. package/README.zh-CN.md +201 -201
  3. package/dist/controllers/index.d.ts +1 -0
  4. package/dist/controllers/index.d.ts.map +1 -1
  5. package/dist/controllers/index.js +1 -0
  6. package/dist/controllers/index.js.map +1 -1
  7. package/dist/engine/chart.d.ts +11 -19
  8. package/dist/engine/chart.d.ts.map +1 -1
  9. package/dist/engine/chart.js +92 -109
  10. package/dist/engine/chart.js.map +1 -1
  11. package/dist/engine/renderers/Indicator/indicatorData.d.ts +1 -0
  12. package/dist/engine/renderers/Indicator/indicatorData.d.ts.map +1 -1
  13. package/dist/engine/renderers/Indicator/indicatorData.js +1 -1
  14. package/dist/engine/renderers/Indicator/indicatorData.js.map +1 -1
  15. package/dist/engine/renderers/webgl/candleSurface.js +47 -47
  16. package/dist/engine/subPaneManager.d.ts +4 -0
  17. package/dist/engine/subPaneManager.d.ts.map +1 -1
  18. package/dist/engine/subPaneManager.js +13 -0
  19. package/dist/engine/subPaneManager.js.map +1 -1
  20. package/dist/version.d.ts +1 -1
  21. package/dist/version.d.ts.map +1 -1
  22. package/dist/version.js +1 -2
  23. package/dist/version.js.map +1 -1
  24. package/package.json +129 -122
  25. package/src/__tests__/signal.test.ts +124 -124
  26. package/src/config/chartSettings.ts +66 -66
  27. package/src/controllers/__tests__/drawing.test.ts +214 -214
  28. package/src/controllers/__tests__/indicatorSelector.test.ts +481 -481
  29. package/src/controllers/__tests__/toolbar.test.ts +225 -225
  30. package/src/controllers/createChartController.ts +665 -665
  31. package/src/controllers/createDrawingController.ts +96 -96
  32. package/src/controllers/createIndicatorSelectorController.ts +307 -307
  33. package/src/controllers/createToolbarController.ts +146 -146
  34. package/src/controllers/index.ts +20 -19
  35. package/src/controllers/types.ts +284 -284
  36. package/src/engine/__tests__/chart.dpr.test.ts +401 -401
  37. package/src/engine/__tests__/paneRenderer.resize.test.ts +92 -92
  38. package/src/engine/chart-store.ts +121 -121
  39. package/src/engine/chart.d.ts +617 -617
  40. package/src/engine/chart.ts +2803 -2815
  41. package/src/engine/controller/__tests__/interaction.dpr.test.ts +259 -259
  42. package/src/engine/controller/interaction.ts +722 -722
  43. package/src/engine/controller/markerInteraction.ts +130 -130
  44. package/src/engine/controller/pinchTracker.ts +82 -82
  45. package/src/engine/controller/tooltipPosition.ts +48 -48
  46. package/src/engine/draw/__tests__/pixelAlign.spec.ts +176 -176
  47. package/src/engine/draw/pixelAlign.ts +259 -259
  48. package/src/engine/drawing/index.ts +655 -655
  49. package/src/engine/drawing/interaction.ts +842 -842
  50. package/src/engine/drawing/plugin.ts +343 -343
  51. package/src/engine/indicators/__tests__/__fixtures__/golden/atr.json +38 -38
  52. package/src/engine/indicators/__tests__/__fixtures__/golden/dema.json +14 -14
  53. package/src/engine/indicators/__tests__/__fixtures__/golden/hma.json +14 -14
  54. package/src/engine/indicators/__tests__/__fixtures__/golden/index.ts +55 -55
  55. package/src/engine/indicators/__tests__/__fixtures__/golden/kama.json +14 -14
  56. package/src/engine/indicators/__tests__/__fixtures__/golden/tema.json +14 -14
  57. package/src/engine/indicators/__tests__/__fixtures__/golden/wma.json +40 -40
  58. package/src/engine/indicators/__tests__/__fixtures__/synthetic.ts +65 -65
  59. package/src/engine/indicators/__tests__/_propertyAssertions.ts +76 -76
  60. package/src/engine/indicators/__tests__/atr.test.ts +153 -153
  61. package/src/engine/indicators/__tests__/calculators.test.ts +614 -614
  62. package/src/engine/indicators/__tests__/cmf-mfi.test.ts +100 -100
  63. package/src/engine/indicators/__tests__/dema.test.ts +73 -73
  64. package/src/engine/indicators/__tests__/donchian.test.ts +70 -70
  65. package/src/engine/indicators/__tests__/hma.test.ts +73 -73
  66. package/src/engine/indicators/__tests__/ichimoku.test.ts +105 -105
  67. package/src/engine/indicators/__tests__/kama.test.ts +80 -80
  68. package/src/engine/indicators/__tests__/keltner.test.ts +65 -65
  69. package/src/engine/indicators/__tests__/pivot-fib.test.ts +110 -110
  70. package/src/engine/indicators/__tests__/roc.test.ts +68 -68
  71. package/src/engine/indicators/__tests__/sar.test.ts +86 -86
  72. package/src/engine/indicators/__tests__/scheduler.test.ts +831 -831
  73. package/src/engine/indicators/__tests__/soa.test.ts +533 -533
  74. package/src/engine/indicators/__tests__/structure.test.ts +110 -110
  75. package/src/engine/indicators/__tests__/supertrend.test.ts +65 -65
  76. package/src/engine/indicators/__tests__/tema.test.ts +68 -68
  77. package/src/engine/indicators/__tests__/trix.test.ts +70 -70
  78. package/src/engine/indicators/__tests__/volatility.test.ts +117 -117
  79. package/src/engine/indicators/__tests__/volume.test.ts +115 -115
  80. package/src/engine/indicators/__tests__/volumeProfile.test.ts +74 -74
  81. package/src/engine/indicators/__tests__/vwap.test.ts +69 -69
  82. package/src/engine/indicators/__tests__/wma.test.ts +112 -112
  83. package/src/engine/indicators/__tests__/zones.test.ts +95 -95
  84. package/src/engine/indicators/atrState.ts +27 -27
  85. package/src/engine/indicators/bollState.ts +51 -51
  86. package/src/engine/indicators/calculators.ts +2593 -2593
  87. package/src/engine/indicators/cciState.ts +25 -25
  88. package/src/engine/indicators/chaikinVolState.ts +32 -32
  89. package/src/engine/indicators/cmfState.ts +27 -27
  90. package/src/engine/indicators/demaState.ts +27 -27
  91. package/src/engine/indicators/donchianState.ts +43 -43
  92. package/src/engine/indicators/eneState.ts +43 -43
  93. package/src/engine/indicators/expmaState.ts +43 -43
  94. package/src/engine/indicators/fastkState.ts +25 -25
  95. package/src/engine/indicators/fibState.ts +41 -41
  96. package/src/engine/indicators/hmaState.ts +27 -27
  97. package/src/engine/indicators/hvState.ts +28 -28
  98. package/src/engine/indicators/ichimokuState.ts +70 -70
  99. package/src/engine/indicators/indicator.worker.ts +169 -169
  100. package/src/engine/indicators/indicatorDefinitionRegistry.ts +62 -62
  101. package/src/engine/indicators/indicatorMetadata.ts +110 -110
  102. package/src/engine/indicators/indicatorRegistry.ts +106 -106
  103. package/src/engine/indicators/indicatorRuntime.ts +1548 -1548
  104. package/src/engine/indicators/kamaState.ts +34 -34
  105. package/src/engine/indicators/keltnerState.ts +49 -49
  106. package/src/engine/indicators/kstState.ts +42 -42
  107. package/src/engine/indicators/maState.ts +36 -36
  108. package/src/engine/indicators/macdState.ts +76 -76
  109. package/src/engine/indicators/mfiState.ts +27 -27
  110. package/src/engine/indicators/momState.ts +25 -25
  111. package/src/engine/indicators/obvState.ts +25 -25
  112. package/src/engine/indicators/parkinsonState.ts +28 -28
  113. package/src/engine/indicators/pivotState.ts +51 -51
  114. package/src/engine/indicators/pvtState.ts +25 -25
  115. package/src/engine/indicators/rocState.ts +27 -27
  116. package/src/engine/indicators/rsiState.ts +65 -65
  117. package/src/engine/indicators/sarState.ts +41 -41
  118. package/src/engine/indicators/scheduler.ts +1205 -1205
  119. package/src/engine/indicators/soa.ts +352 -352
  120. package/src/engine/indicators/stateComposer.ts +1262 -1262
  121. package/src/engine/indicators/stochState.ts +26 -26
  122. package/src/engine/indicators/structureState.ts +69 -69
  123. package/src/engine/indicators/supertrendState.ts +37 -37
  124. package/src/engine/indicators/temaState.ts +27 -27
  125. package/src/engine/indicators/trixState.ts +35 -35
  126. package/src/engine/indicators/vmaState.ts +27 -27
  127. package/src/engine/indicators/volumeProfileState.ts +63 -63
  128. package/src/engine/indicators/vwapState.ts +29 -29
  129. package/src/engine/indicators/wmaState.ts +27 -27
  130. package/src/engine/indicators/wmsrState.ts +25 -25
  131. package/src/engine/indicators/workerProtocol.ts +613 -613
  132. package/src/engine/indicators/zonesState.ts +47 -47
  133. package/src/engine/layout/pane.ts +161 -161
  134. package/src/engine/marker/registry.ts +265 -265
  135. package/src/engine/paneRenderer.ts +169 -169
  136. package/src/engine/renderers/Indicator/atr.ts +237 -237
  137. package/src/engine/renderers/Indicator/boll.ts +317 -317
  138. package/src/engine/renderers/Indicator/cci.ts +275 -275
  139. package/src/engine/renderers/Indicator/chaikinVol.ts +138 -138
  140. package/src/engine/renderers/Indicator/cmf.ts +137 -137
  141. package/src/engine/renderers/Indicator/dema.ts +136 -136
  142. package/src/engine/renderers/Indicator/donchian.ts +137 -137
  143. package/src/engine/renderers/Indicator/ene.ts +271 -271
  144. package/src/engine/renderers/Indicator/expma.ts +197 -197
  145. package/src/engine/renderers/Indicator/fastk.ts +316 -316
  146. package/src/engine/renderers/Indicator/fib.ts +141 -141
  147. package/src/engine/renderers/Indicator/hma.ts +136 -136
  148. package/src/engine/renderers/Indicator/hv.ts +124 -124
  149. package/src/engine/renderers/Indicator/ichimoku.ts +181 -181
  150. package/src/engine/renderers/Indicator/index.ts +241 -241
  151. package/src/engine/renderers/Indicator/indicatorData.ts +650 -650
  152. package/src/engine/renderers/Indicator/kama.ts +136 -136
  153. package/src/engine/renderers/Indicator/keltner.ts +137 -137
  154. package/src/engine/renderers/Indicator/kst.ts +302 -302
  155. package/src/engine/renderers/Indicator/ma.ts +200 -200
  156. package/src/engine/renderers/Indicator/macd.ts +477 -477
  157. package/src/engine/renderers/Indicator/macdLegend.ts +141 -141
  158. package/src/engine/renderers/Indicator/mainIndicatorLegend.ts +272 -272
  159. package/src/engine/renderers/Indicator/mfi.ts +142 -142
  160. package/src/engine/renderers/Indicator/mom.ts +311 -311
  161. package/src/engine/renderers/Indicator/obv.ts +123 -123
  162. package/src/engine/renderers/Indicator/parkinson.ts +124 -124
  163. package/src/engine/renderers/Indicator/pivot.ts +131 -131
  164. package/src/engine/renderers/Indicator/pvt.ts +123 -123
  165. package/src/engine/renderers/Indicator/roc.ts +143 -143
  166. package/src/engine/renderers/Indicator/rsi.ts +390 -390
  167. package/src/engine/renderers/Indicator/sar.ts +113 -113
  168. package/src/engine/renderers/Indicator/scale/atr_scale.ts +19 -19
  169. package/src/engine/renderers/Indicator/scale/cci_scale.ts +19 -19
  170. package/src/engine/renderers/Indicator/scale/fastk_scale.ts +19 -19
  171. package/src/engine/renderers/Indicator/scale/indicator_scale.ts +204 -204
  172. package/src/engine/renderers/Indicator/scale/kst_scale.ts +19 -19
  173. package/src/engine/renderers/Indicator/scale/macd_scale.ts +22 -22
  174. package/src/engine/renderers/Indicator/scale/mom_scale.ts +19 -19
  175. package/src/engine/renderers/Indicator/scale/rsi_scale.ts +19 -19
  176. package/src/engine/renderers/Indicator/scale/stoch_scale.ts +19 -19
  177. package/src/engine/renderers/Indicator/scale/volume_scale.ts +26 -26
  178. package/src/engine/renderers/Indicator/scale/wmsr_scale.ts +19 -19
  179. package/src/engine/renderers/Indicator/stoch.ts +359 -359
  180. package/src/engine/renderers/Indicator/structure.ts +126 -126
  181. package/src/engine/renderers/Indicator/subPaneConfig.ts +265 -265
  182. package/src/engine/renderers/Indicator/supertrend.ts +115 -115
  183. package/src/engine/renderers/Indicator/tema.ts +136 -136
  184. package/src/engine/renderers/Indicator/trix.ts +158 -158
  185. package/src/engine/renderers/Indicator/vma.ts +124 -124
  186. package/src/engine/renderers/Indicator/volumeProfile.ts +125 -125
  187. package/src/engine/renderers/Indicator/vwap.ts +123 -123
  188. package/src/engine/renderers/Indicator/wma.ts +136 -136
  189. package/src/engine/renderers/Indicator/wmsr.ts +328 -328
  190. package/src/engine/renderers/Indicator/zones.ts +104 -104
  191. package/src/engine/renderers/__tests__/boll.renderer.test.ts +314 -314
  192. package/src/engine/renderers/__tests__/ene.renderer.test.ts +305 -305
  193. package/src/engine/renderers/__tests__/expma.renderer.test.ts +279 -279
  194. package/src/engine/renderers/__tests__/ma.renderer.test.ts +426 -426
  195. package/src/engine/renderers/__tests__/mainIndicatorLegend.renderer.test.ts +502 -502
  196. package/src/engine/renderers/__tests__/yAxis.renderer.test.ts +173 -173
  197. package/src/engine/renderers/candle.ts +459 -459
  198. package/src/engine/renderers/crosshair.ts +69 -69
  199. package/src/engine/renderers/customMarkers.ts +162 -162
  200. package/src/engine/renderers/extremaMarkers.ts +246 -246
  201. package/src/engine/renderers/gridLines.ts +90 -90
  202. package/src/engine/renderers/lastPrice.ts +97 -97
  203. package/src/engine/renderers/paneTitle.ts +136 -136
  204. package/src/engine/renderers/subVolume.ts +236 -236
  205. package/src/engine/renderers/timeAxis.ts +121 -121
  206. package/src/engine/renderers/webgl/candleSurface.ts +955 -955
  207. package/src/engine/renderers/webgl/sharedWebGLSurface.ts +146 -146
  208. package/src/engine/renderers/yAxis.ts +105 -105
  209. package/src/engine/scale/__tests__/logFormula.spec.ts +148 -148
  210. package/src/engine/scale/logFormula.ts +130 -130
  211. package/src/engine/scale/price.ts +39 -39
  212. package/src/engine/scale/priceScale.ts +264 -264
  213. package/src/engine/subPaneManager.ts +442 -427
  214. package/src/engine/theme/colors.ts +642 -642
  215. package/src/engine/theme/fonts.ts +20 -20
  216. package/src/engine/utils/klineConfig.ts +49 -49
  217. package/src/engine/utils/tickCount.ts +11 -11
  218. package/src/engine/utils/tickPosition.ts +214 -214
  219. package/src/engine/utils/zoom.ts +83 -83
  220. package/src/engine/viewport/viewport.ts +67 -67
  221. package/src/index.ts +3 -3
  222. package/src/plugin/ConfigManager.ts +93 -93
  223. package/src/plugin/EventBus.ts +77 -77
  224. package/src/plugin/HookSystem.ts +106 -106
  225. package/src/plugin/PluginHost.ts +243 -243
  226. package/src/plugin/PluginRegistry.ts +92 -92
  227. package/src/plugin/StateStore.ts +73 -73
  228. package/src/plugin/index.ts +19 -19
  229. package/src/plugin/rendererPluginManager.ts +368 -368
  230. package/src/plugin/stateKeys.ts +8 -8
  231. package/src/plugin/types.ts +526 -526
  232. package/src/reactivity/index.ts +2 -2
  233. package/src/reactivity/signal.ts +119 -119
  234. package/src/semantic/controller.ts +251 -251
  235. package/src/semantic/drawShape.ts +260 -260
  236. package/src/semantic/index.ts +28 -28
  237. package/src/semantic/schema.json +256 -256
  238. package/src/semantic/types.ts +251 -251
  239. package/src/semantic/validator.ts +349 -349
  240. package/src/types/kLine.ts +13 -13
  241. package/src/types/price.ts +56 -56
  242. package/src/types/volumePrice.ts +33 -33
  243. package/src/utils/dateFormat.ts +208 -208
  244. package/src/utils/kLineDraw/axis.ts +562 -562
  245. package/src/utils/priceToY.ts +34 -34
  246. package/src/utils/volumePrice.ts +202 -202
  247. package/src/version.ts +1 -1
@@ -1,533 +1,533 @@
1
- import { describe, it, expect, beforeAll } from 'vitest'
2
- import type { KLineData } from '@/types/price'
3
- import {
4
- SharedKLineBuffer,
5
- getClosesView,
6
- getHighsLowsViews,
7
- getOHLCViews,
8
- type KLineSoALayout,
9
- } from '../soa'
10
- import {
11
- calcBOLLData,
12
- calcBOLLDataSoA,
13
- calcEXPMAData,
14
- calcEXPMADataSoA,
15
- calcENEData,
16
- calcENEDataSoA,
17
- calcMAData,
18
- calcMADataSoA,
19
- calcRSIData,
20
- calcRSIDataSoA,
21
- calcCCIData,
22
- calcCCIDataSoA,
23
- calcSTOCHData,
24
- calcSTOCHDataSoA,
25
- calcMOMData,
26
- calcMOMDataSoA,
27
- calcWMSRData,
28
- calcWMSRDataSoA,
29
- calcKSTData,
30
- calcKSTDataSoA,
31
- calcFASTKData,
32
- calcFASTKDataSoA,
33
- calcMACDData,
34
- calcMACDDataSoA,
35
- } from '../calculators'
36
-
37
- /**
38
- * 生成测试用的 K线数据
39
- */
40
- function generateTestData(length: number): KLineData[] {
41
- const data: KLineData[] = []
42
- let price = 100
43
- const now = Date.now()
44
-
45
- for (let i = 0; i < length; i++) {
46
- const change = (Math.random() - 0.5) * 5
47
- const open = price
48
- const close = price + change
49
- const high = Math.max(open, close) + Math.random() * 2
50
- const low = Math.min(open, close) - Math.random() * 2
51
-
52
- data.push({
53
- timestamp: now + i * 60000, // 每分钟
54
- open,
55
- high,
56
- low,
57
- close,
58
- volume: Math.floor(Math.random() * 1000000),
59
- turnover: Math.floor(Math.random() * 100000000),
60
- })
61
-
62
- price = close
63
- }
64
-
65
- return data
66
- }
67
-
68
- /**
69
- * 比较两个数字数组,考虑浮点精度误差
70
- */
71
- function compareNumberArrays(
72
- a: (number | undefined)[],
73
- b: (number | undefined)[],
74
- epsilon: number = 1e-10
75
- ): boolean {
76
- if (a.length !== b.length) return false
77
-
78
- for (let i = 0; i < a.length; i++) {
79
- const va = a[i]
80
- const vb = b[i]
81
-
82
- if (va === undefined && vb === undefined) continue
83
- if (va === undefined || vb === undefined) return false
84
- if (Math.abs(va - vb) > epsilon) return false
85
- }
86
-
87
- return true
88
- }
89
-
90
- describe('SharedKLineBuffer', () => {
91
- const testDataLength = 100
92
- let testData: KLineData[]
93
-
94
- beforeAll(() => {
95
- testData = generateTestData(testDataLength)
96
- })
97
-
98
- describe('detectSupport', () => {
99
- it('应该返回布尔值', () => {
100
- const result = SharedKLineBuffer.detectSupport()
101
- expect(typeof result).toBe('boolean')
102
- })
103
- })
104
-
105
- describe('fromKLineData', () => {
106
- it('应该正确转换 KLineData[] 到 SoA 布局', () => {
107
- const layout = SharedKLineBuffer.fromKLineData(testData)
108
-
109
- expect(layout.length).toBe(testDataLength)
110
- expect(layout.timestamps.length).toBe(testDataLength)
111
- expect(layout.opens.length).toBe(testDataLength)
112
- expect(layout.highs.length).toBe(testDataLength)
113
- expect(layout.lows.length).toBe(testDataLength)
114
- expect(layout.closes.length).toBe(testDataLength)
115
- expect(layout.volumes.length).toBe(testDataLength)
116
- expect(layout.turnovers.length).toBe(testDataLength)
117
- })
118
-
119
- it('应该正确填充数据', () => {
120
- const layout = SharedKLineBuffer.fromKLineData(testData)
121
-
122
- for (let i = 0; i < testDataLength; i++) {
123
- const original = testData[i]!
124
- expect(layout.timestamps[i]).toBe(original.timestamp)
125
- expect(layout.opens[i]).toBe(original.open)
126
- expect(layout.highs[i]).toBe(original.high)
127
- expect(layout.lows[i]).toBe(original.low)
128
- expect(layout.closes[i]).toBe(original.close)
129
- expect(layout.volumes[i]).toBe(original.volume ?? 0)
130
- expect(layout.turnovers[i]).toBe(original.turnover ?? 0)
131
- }
132
- })
133
-
134
- it('应该正确检测 hasVolume 和 hasTurnover', () => {
135
- const dataWithVolume = generateTestData(10)
136
- const layoutWithVolume = SharedKLineBuffer.fromKLineData(dataWithVolume)
137
- expect(layoutWithVolume.hasVolume).toBe(true)
138
- expect(layoutWithVolume.hasTurnover).toBe(true)
139
-
140
- const dataWithoutVolume: KLineData[] = dataWithVolume.map(d => ({
141
- timestamp: d.timestamp,
142
- open: d.open,
143
- high: d.high,
144
- low: d.low,
145
- close: d.close,
146
- }))
147
- const layoutWithoutVolume = SharedKLineBuffer.fromKLineData(dataWithoutVolume)
148
- expect(layoutWithoutVolume.hasVolume).toBe(false)
149
- expect(layoutWithoutVolume.hasTurnover).toBe(false)
150
- })
151
-
152
- it('应该正确计算缓冲区大小', () => {
153
- const layout = SharedKLineBuffer.fromKLineData(testData)
154
- const expectedByteLength = testDataLength * 8 * 7 // 7 列 x 8 字节
155
- expect(layout.buffer.byteLength).toBe(expectedByteLength)
156
- })
157
-
158
- it('isShared 应该与 detectSupport 一致', () => {
159
- const layout = SharedKLineBuffer.fromKLineData(testData, true)
160
- expect(layout.isShared).toBe(SharedKLineBuffer.detectSupport())
161
-
162
- const layoutArrayBuffer = SharedKLineBuffer.fromKLineData(testData, false)
163
- expect(layoutArrayBuffer.isShared).toBe(false)
164
- })
165
- })
166
-
167
- describe('toKLineData', () => {
168
- it('应该正确将 SoA 转换回 AoS', () => {
169
- const layout = SharedKLineBuffer.fromKLineData(testData)
170
- const restored = SharedKLineBuffer.toKLineData(layout)
171
-
172
- expect(restored.length).toBe(testData.length)
173
-
174
- for (let i = 0; i < testData.length; i++) {
175
- const original = testData[i]!
176
- const recovered = restored[i]!
177
-
178
- expect(recovered.timestamp).toBe(original.timestamp)
179
- expect(recovered.open).toBe(original.open)
180
- expect(recovered.high).toBe(original.high)
181
- expect(recovered.low).toBe(original.low)
182
- expect(recovered.close).toBe(original.close)
183
- expect(recovered.volume).toBe(original.volume)
184
- expect(recovered.turnover).toBe(original.turnover)
185
- }
186
- })
187
-
188
- it('应该正确处理缺失的 volume 和 turnover', () => {
189
- const dataWithoutVolume: KLineData[] = testData.slice(0, 10).map(d => ({
190
- timestamp: d.timestamp,
191
- open: d.open,
192
- high: d.high,
193
- low: d.low,
194
- close: d.close,
195
- }))
196
- const layout = SharedKLineBuffer.fromKLineData(dataWithoutVolume)
197
- const restored = SharedKLineBuffer.toKLineData(layout)
198
-
199
- for (const item of restored) {
200
- expect(item.volume).toBeUndefined()
201
- expect(item.turnover).toBeUndefined()
202
- }
203
- })
204
- })
205
-
206
- describe('roundtrip conversion', () => {
207
- it('AoS -> SoA -> AoS 应该保持数据一致性', () => {
208
- const layout = SharedKLineBuffer.fromKLineData(testData)
209
- const restored = SharedKLineBuffer.toKLineData(layout)
210
-
211
- expect(restored).toEqual(testData)
212
- })
213
- })
214
-
215
- describe('updateExisting', () => {
216
- it('相同长度数据应该复用缓冲区', () => {
217
- const layout = SharedKLineBuffer.fromKLineData(testData)
218
- const originalBuffer = layout.buffer
219
-
220
- // 修改数据但保持相同长度
221
- const newData = generateTestData(testDataLength)
222
- const updated = SharedKLineBuffer.updateExisting(layout, newData)
223
-
224
- expect(updated.buffer).toBe(originalBuffer)
225
- expect(updated.length).toBe(testDataLength)
226
- })
227
-
228
- it('不同长度数据应该创建新缓冲区', () => {
229
- const layout = SharedKLineBuffer.fromKLineData(testData)
230
- const originalBuffer = layout.buffer
231
-
232
- const newData = generateTestData(testDataLength + 10)
233
- const updated = SharedKLineBuffer.updateExisting(layout, newData)
234
-
235
- expect(updated.buffer).not.toBe(originalBuffer)
236
- expect(updated.length).toBe(testDataLength + 10)
237
- })
238
-
239
- it('应该正确更新数据内容', () => {
240
- const layout = SharedKLineBuffer.fromKLineData(testData)
241
- const newData = generateTestData(testDataLength)
242
-
243
- SharedKLineBuffer.updateExisting(layout, newData)
244
-
245
- for (let i = 0; i < testDataLength; i++) {
246
- expect(layout.timestamps[i]).toBe(newData[i]!.timestamp)
247
- expect(layout.closes[i]).toBe(newData[i]!.close)
248
- }
249
- })
250
- })
251
-
252
- describe('createSubview', () => {
253
- it('应该创建正确的子视图', () => {
254
- const layout = SharedKLineBuffer.fromKLineData(testData)
255
- const subview = SharedKLineBuffer.createSubview(layout, 10, 30)
256
-
257
- expect(subview.length).toBe(20)
258
- expect(subview.buffer).toBe(layout.buffer)
259
- })
260
-
261
- it('子视图应该共享同一缓冲区', () => {
262
- const layout = SharedKLineBuffer.fromKLineData(testData)
263
- const subview = SharedKLineBuffer.createSubview(layout, 10, 30)
264
-
265
- // 修改子视图应该影响原视图
266
- subview.closes[0] = 999.99
267
- expect(layout.closes[10]).toBe(999.99)
268
- })
269
-
270
- it('越界范围应该自动截断', () => {
271
- const layout = SharedKLineBuffer.fromKLineData(testData)
272
- const subview = SharedKLineBuffer.createSubview(layout, testDataLength - 5, testDataLength + 10)
273
-
274
- expect(subview.length).toBe(5)
275
- })
276
-
277
- it('无效范围应该抛出错误', () => {
278
- const layout = SharedKLineBuffer.fromKLineData(testData)
279
- expect(() => SharedKLineBuffer.createSubview(layout, 50, 50)).toThrow()
280
- expect(() => SharedKLineBuffer.createSubview(layout, 50, 40)).toThrow()
281
- })
282
- })
283
-
284
- describe('getBufferInfo', () => {
285
- it('应该返回正确的缓冲区信息', () => {
286
- const layout = SharedKLineBuffer.fromKLineData(testData)
287
- const info = SharedKLineBuffer.getBufferInfo(layout)
288
-
289
- expect(info.byteLength).toBe(layout.buffer.byteLength)
290
- expect(info.isShared).toBe(layout.isShared)
291
- expect(info.length).toBe(layout.length)
292
- expect(info.columns).toHaveLength(7)
293
- })
294
- })
295
-
296
- describe('helper functions', () => {
297
- it('getClosesView 应该返回 closes 数组', () => {
298
- const layout = SharedKLineBuffer.fromKLineData(testData)
299
- const closes = getClosesView(layout)
300
- expect(closes).toBe(layout.closes)
301
- })
302
-
303
- it('getHighsLowsViews 应该返回 highs 和 lows', () => {
304
- const layout = SharedKLineBuffer.fromKLineData(testData)
305
- const { highs, lows } = getHighsLowsViews(layout)
306
- expect(highs).toBe(layout.highs)
307
- expect(lows).toBe(layout.lows)
308
- })
309
-
310
- it('getOHLCViews 应该返回全部价格视图', () => {
311
- const layout = SharedKLineBuffer.fromKLineData(testData)
312
- const views = getOHLCViews(layout)
313
- expect(views.opens).toBe(layout.opens)
314
- expect(views.highs).toBe(layout.highs)
315
- expect(views.lows).toBe(layout.lows)
316
- expect(views.closes).toBe(layout.closes)
317
- })
318
- })
319
- })
320
-
321
- describe('SoA Calculator Wrappers', () => {
322
- const testDataLength = 200
323
- let testData: KLineData[]
324
- let layout: KLineSoALayout
325
-
326
- beforeAll(() => {
327
- testData = generateTestData(testDataLength)
328
- layout = SharedKLineBuffer.fromKLineData(testData)
329
- })
330
-
331
- describe('calcBOLLDataSoA', () => {
332
- it('SoA 结果应该与 AoS 结果一致', () => {
333
- const aosResult = calcBOLLData(testData, 20, 2)
334
- const soaResult = calcBOLLDataSoA(layout, 20, 2)
335
-
336
- expect(soaResult.length).toBe(aosResult.length)
337
-
338
- for (let i = 0; i < aosResult.length; i++) {
339
- const a = aosResult[i]
340
- const s = soaResult[i]
341
-
342
- if (a === undefined) {
343
- expect(s).toBeUndefined()
344
- } else {
345
- expect(s).toBeDefined()
346
- expect(s!.upper).toBeCloseTo(a.upper, 10)
347
- expect(s!.middle).toBeCloseTo(a.middle, 10)
348
- expect(s!.lower).toBeCloseTo(a.lower, 10)
349
- }
350
- }
351
- })
352
- })
353
-
354
- describe('calcEXPMADataSoA', () => {
355
- it('SoA 结果应该与 AoS 结果一致', () => {
356
- const aosResult = calcEXPMAData(testData, 12, 50)
357
- const soaResult = calcEXPMADataSoA(layout, 12, 50)
358
-
359
- for (let i = 0; i < aosResult.length; i++) {
360
- const a = aosResult[i]
361
- const s = soaResult[i]
362
-
363
- if (a === undefined) {
364
- expect(s).toBeUndefined()
365
- } else {
366
- expect(s!.fast).toBeCloseTo(a.fast, 10)
367
- expect(s!.slow).toBeCloseTo(a.slow, 10)
368
- }
369
- }
370
- })
371
- })
372
-
373
- describe('calcENEDataSoA', () => {
374
- it('SoA 结果应该与 AoS 结果一致', () => {
375
- const aosResult = calcENEData(testData, 10, 11)
376
- const soaResult = calcENEDataSoA(layout, 10, 11)
377
-
378
- for (let i = 0; i < aosResult.length; i++) {
379
- const a = aosResult[i]
380
- const s = soaResult[i]
381
-
382
- if (a === undefined) {
383
- expect(s).toBeUndefined()
384
- } else {
385
- expect(s!.upper).toBeCloseTo(a.upper, 10)
386
- expect(s!.middle).toBeCloseTo(a.middle, 10)
387
- expect(s!.lower).toBeCloseTo(a.lower, 10)
388
- }
389
- }
390
- })
391
- })
392
-
393
- describe('calcMADataSoA', () => {
394
- it('SoA 结果应该与 AoS 结果一致', () => {
395
- const aosResult = calcMAData(testData, 20)
396
- const soaResult = calcMADataSoA(layout, 20)
397
-
398
- expect(compareNumberArrays(aosResult, soaResult)).toBe(true)
399
- })
400
- })
401
-
402
- describe('calcRSIDataSoA', () => {
403
- it('SoA 结果应该与 AoS 结果一致', () => {
404
- const aosResult = calcRSIData(testData, 14)
405
- const soaResult = calcRSIDataSoA(layout, 14)
406
-
407
- expect(compareNumberArrays(aosResult, soaResult)).toBe(true)
408
- })
409
- })
410
-
411
- describe('calcCCIDataSoA', () => {
412
- it('SoA 结果应该与 AoS 结果一致', () => {
413
- const aosResult = calcCCIData(testData, 14)
414
- const soaResult = calcCCIDataSoA(layout, 14)
415
-
416
- expect(compareNumberArrays(aosResult, soaResult)).toBe(true)
417
- })
418
- })
419
-
420
- describe('calcSTOCHDataSoA', () => {
421
- it('SoA 结果应该与 AoS 结果一致', () => {
422
- const aosResult = calcSTOCHData(testData, 9, 3)
423
- const soaResult = calcSTOCHDataSoA(layout, 9, 3)
424
-
425
- for (let i = 0; i < aosResult.length; i++) {
426
- const a = aosResult[i]
427
- const s = soaResult[i]
428
-
429
- if (a === undefined) {
430
- expect(s).toBeUndefined()
431
- } else {
432
- expect(s!.k).toBeCloseTo(a.k, 10)
433
- expect(s!.d).toBeCloseTo(a.d, 10)
434
- }
435
- }
436
- })
437
- })
438
-
439
- describe('calcMOMDataSoA', () => {
440
- it('SoA 结果应该与 AoS 结果一致', () => {
441
- const aosResult = calcMOMData(testData, 10)
442
- const soaResult = calcMOMDataSoA(layout, 10)
443
-
444
- expect(compareNumberArrays(aosResult, soaResult)).toBe(true)
445
- })
446
- })
447
-
448
- describe('calcWMSRDataSoA', () => {
449
- it('SoA 结果应该与 AoS 结果一致', () => {
450
- const aosResult = calcWMSRData(testData, 14)
451
- const soaResult = calcWMSRDataSoA(layout, 14)
452
-
453
- expect(compareNumberArrays(aosResult, soaResult)).toBe(true)
454
- })
455
- })
456
-
457
- describe('calcKSTDataSoA', () => {
458
- it('SoA 结果应该与 AoS 结果一致', () => {
459
- const aosResult = calcKSTData(testData, 10, 15, 20, 30, 9)
460
- const soaResult = calcKSTDataSoA(layout, 10, 15, 20, 30, 9)
461
-
462
- for (let i = 0; i < aosResult.length; i++) {
463
- const a = aosResult[i]
464
- const s = soaResult[i]
465
-
466
- if (a === undefined) {
467
- expect(s).toBeUndefined()
468
- } else {
469
- expect(s!.kst).toBeCloseTo(a.kst, 10)
470
- expect(s!.signal).toBeCloseTo(a.signal, 10)
471
- }
472
- }
473
- })
474
- })
475
-
476
- describe('calcFASTKDataSoA', () => {
477
- it('SoA 结果应该与 AoS 结果一致', () => {
478
- const aosResult = calcFASTKData(testData, 9)
479
- const soaResult = calcFASTKDataSoA(layout, 9)
480
-
481
- expect(compareNumberArrays(aosResult, soaResult)).toBe(true)
482
- })
483
- })
484
-
485
- describe('calcMACDDataSoA', () => {
486
- it('SoA 结果应该与 AoS 结果一致', () => {
487
- const aosResult = calcMACDData(testData, 12, 26, 9)
488
- const soaResult = calcMACDDataSoA(layout, 12, 26, 9)
489
-
490
- for (let i = 0; i < aosResult.length; i++) {
491
- const a = aosResult[i]
492
- const s = soaResult[i]
493
-
494
- if (a === undefined) {
495
- expect(s).toBeUndefined()
496
- } else {
497
- expect(s!.dif).toBeCloseTo(a.dif, 10)
498
- expect(s!.dea).toBeCloseTo(a.dea, 10)
499
- expect(s!.macd).toBeCloseTo(a.macd, 10)
500
- }
501
- }
502
- })
503
- })
504
- })
505
-
506
- describe('SoA Performance', () => {
507
- it('大容量数据转换应该高效', () => {
508
- const largeData = generateTestData(10000)
509
-
510
- const start = performance.now()
511
- const layout = SharedKLineBuffer.fromKLineData(largeData)
512
- const layoutTime = performance.now() - start
513
-
514
- const start2 = performance.now()
515
- const restored = SharedKLineBuffer.toKLineData(layout)
516
- const restoreTime = performance.now() - start2
517
-
518
- // 转换应该在合理时间内完成(10K 数据 < 100ms)
519
- expect(layoutTime).toBeLessThan(100)
520
- expect(restoreTime).toBeLessThan(100)
521
- expect(restored.length).toBe(10000)
522
- })
523
-
524
- it('SoA 计算器应该与 AoS 产生相同结果', () => {
525
- const data = generateTestData(1000)
526
- const layout = SharedKLineBuffer.fromKLineData(data)
527
-
528
- const aosResult = calcBOLLData(data, 20, 2)
529
- const soaResult = calcBOLLDataSoA(layout, 20, 2)
530
-
531
- expect(soaResult.length).toBe(aosResult.length)
532
- })
533
- })
1
+ import { describe, it, expect, beforeAll } from 'vitest'
2
+ import type { KLineData } from '@/types/price'
3
+ import {
4
+ SharedKLineBuffer,
5
+ getClosesView,
6
+ getHighsLowsViews,
7
+ getOHLCViews,
8
+ type KLineSoALayout,
9
+ } from '../soa'
10
+ import {
11
+ calcBOLLData,
12
+ calcBOLLDataSoA,
13
+ calcEXPMAData,
14
+ calcEXPMADataSoA,
15
+ calcENEData,
16
+ calcENEDataSoA,
17
+ calcMAData,
18
+ calcMADataSoA,
19
+ calcRSIData,
20
+ calcRSIDataSoA,
21
+ calcCCIData,
22
+ calcCCIDataSoA,
23
+ calcSTOCHData,
24
+ calcSTOCHDataSoA,
25
+ calcMOMData,
26
+ calcMOMDataSoA,
27
+ calcWMSRData,
28
+ calcWMSRDataSoA,
29
+ calcKSTData,
30
+ calcKSTDataSoA,
31
+ calcFASTKData,
32
+ calcFASTKDataSoA,
33
+ calcMACDData,
34
+ calcMACDDataSoA,
35
+ } from '../calculators'
36
+
37
+ /**
38
+ * 生成测试用的 K线数据
39
+ */
40
+ function generateTestData(length: number): KLineData[] {
41
+ const data: KLineData[] = []
42
+ let price = 100
43
+ const now = Date.now()
44
+
45
+ for (let i = 0; i < length; i++) {
46
+ const change = (Math.random() - 0.5) * 5
47
+ const open = price
48
+ const close = price + change
49
+ const high = Math.max(open, close) + Math.random() * 2
50
+ const low = Math.min(open, close) - Math.random() * 2
51
+
52
+ data.push({
53
+ timestamp: now + i * 60000, // 每分钟
54
+ open,
55
+ high,
56
+ low,
57
+ close,
58
+ volume: Math.floor(Math.random() * 1000000),
59
+ turnover: Math.floor(Math.random() * 100000000),
60
+ })
61
+
62
+ price = close
63
+ }
64
+
65
+ return data
66
+ }
67
+
68
+ /**
69
+ * 比较两个数字数组,考虑浮点精度误差
70
+ */
71
+ function compareNumberArrays(
72
+ a: (number | undefined)[],
73
+ b: (number | undefined)[],
74
+ epsilon: number = 1e-10
75
+ ): boolean {
76
+ if (a.length !== b.length) return false
77
+
78
+ for (let i = 0; i < a.length; i++) {
79
+ const va = a[i]
80
+ const vb = b[i]
81
+
82
+ if (va === undefined && vb === undefined) continue
83
+ if (va === undefined || vb === undefined) return false
84
+ if (Math.abs(va - vb) > epsilon) return false
85
+ }
86
+
87
+ return true
88
+ }
89
+
90
+ describe('SharedKLineBuffer', () => {
91
+ const testDataLength = 100
92
+ let testData: KLineData[]
93
+
94
+ beforeAll(() => {
95
+ testData = generateTestData(testDataLength)
96
+ })
97
+
98
+ describe('detectSupport', () => {
99
+ it('应该返回布尔值', () => {
100
+ const result = SharedKLineBuffer.detectSupport()
101
+ expect(typeof result).toBe('boolean')
102
+ })
103
+ })
104
+
105
+ describe('fromKLineData', () => {
106
+ it('应该正确转换 KLineData[] 到 SoA 布局', () => {
107
+ const layout = SharedKLineBuffer.fromKLineData(testData)
108
+
109
+ expect(layout.length).toBe(testDataLength)
110
+ expect(layout.timestamps.length).toBe(testDataLength)
111
+ expect(layout.opens.length).toBe(testDataLength)
112
+ expect(layout.highs.length).toBe(testDataLength)
113
+ expect(layout.lows.length).toBe(testDataLength)
114
+ expect(layout.closes.length).toBe(testDataLength)
115
+ expect(layout.volumes.length).toBe(testDataLength)
116
+ expect(layout.turnovers.length).toBe(testDataLength)
117
+ })
118
+
119
+ it('应该正确填充数据', () => {
120
+ const layout = SharedKLineBuffer.fromKLineData(testData)
121
+
122
+ for (let i = 0; i < testDataLength; i++) {
123
+ const original = testData[i]!
124
+ expect(layout.timestamps[i]).toBe(original.timestamp)
125
+ expect(layout.opens[i]).toBe(original.open)
126
+ expect(layout.highs[i]).toBe(original.high)
127
+ expect(layout.lows[i]).toBe(original.low)
128
+ expect(layout.closes[i]).toBe(original.close)
129
+ expect(layout.volumes[i]).toBe(original.volume ?? 0)
130
+ expect(layout.turnovers[i]).toBe(original.turnover ?? 0)
131
+ }
132
+ })
133
+
134
+ it('应该正确检测 hasVolume 和 hasTurnover', () => {
135
+ const dataWithVolume = generateTestData(10)
136
+ const layoutWithVolume = SharedKLineBuffer.fromKLineData(dataWithVolume)
137
+ expect(layoutWithVolume.hasVolume).toBe(true)
138
+ expect(layoutWithVolume.hasTurnover).toBe(true)
139
+
140
+ const dataWithoutVolume: KLineData[] = dataWithVolume.map(d => ({
141
+ timestamp: d.timestamp,
142
+ open: d.open,
143
+ high: d.high,
144
+ low: d.low,
145
+ close: d.close,
146
+ }))
147
+ const layoutWithoutVolume = SharedKLineBuffer.fromKLineData(dataWithoutVolume)
148
+ expect(layoutWithoutVolume.hasVolume).toBe(false)
149
+ expect(layoutWithoutVolume.hasTurnover).toBe(false)
150
+ })
151
+
152
+ it('应该正确计算缓冲区大小', () => {
153
+ const layout = SharedKLineBuffer.fromKLineData(testData)
154
+ const expectedByteLength = testDataLength * 8 * 7 // 7 列 x 8 字节
155
+ expect(layout.buffer.byteLength).toBe(expectedByteLength)
156
+ })
157
+
158
+ it('isShared 应该与 detectSupport 一致', () => {
159
+ const layout = SharedKLineBuffer.fromKLineData(testData, true)
160
+ expect(layout.isShared).toBe(SharedKLineBuffer.detectSupport())
161
+
162
+ const layoutArrayBuffer = SharedKLineBuffer.fromKLineData(testData, false)
163
+ expect(layoutArrayBuffer.isShared).toBe(false)
164
+ })
165
+ })
166
+
167
+ describe('toKLineData', () => {
168
+ it('应该正确将 SoA 转换回 AoS', () => {
169
+ const layout = SharedKLineBuffer.fromKLineData(testData)
170
+ const restored = SharedKLineBuffer.toKLineData(layout)
171
+
172
+ expect(restored.length).toBe(testData.length)
173
+
174
+ for (let i = 0; i < testData.length; i++) {
175
+ const original = testData[i]!
176
+ const recovered = restored[i]!
177
+
178
+ expect(recovered.timestamp).toBe(original.timestamp)
179
+ expect(recovered.open).toBe(original.open)
180
+ expect(recovered.high).toBe(original.high)
181
+ expect(recovered.low).toBe(original.low)
182
+ expect(recovered.close).toBe(original.close)
183
+ expect(recovered.volume).toBe(original.volume)
184
+ expect(recovered.turnover).toBe(original.turnover)
185
+ }
186
+ })
187
+
188
+ it('应该正确处理缺失的 volume 和 turnover', () => {
189
+ const dataWithoutVolume: KLineData[] = testData.slice(0, 10).map(d => ({
190
+ timestamp: d.timestamp,
191
+ open: d.open,
192
+ high: d.high,
193
+ low: d.low,
194
+ close: d.close,
195
+ }))
196
+ const layout = SharedKLineBuffer.fromKLineData(dataWithoutVolume)
197
+ const restored = SharedKLineBuffer.toKLineData(layout)
198
+
199
+ for (const item of restored) {
200
+ expect(item.volume).toBeUndefined()
201
+ expect(item.turnover).toBeUndefined()
202
+ }
203
+ })
204
+ })
205
+
206
+ describe('roundtrip conversion', () => {
207
+ it('AoS -> SoA -> AoS 应该保持数据一致性', () => {
208
+ const layout = SharedKLineBuffer.fromKLineData(testData)
209
+ const restored = SharedKLineBuffer.toKLineData(layout)
210
+
211
+ expect(restored).toEqual(testData)
212
+ })
213
+ })
214
+
215
+ describe('updateExisting', () => {
216
+ it('相同长度数据应该复用缓冲区', () => {
217
+ const layout = SharedKLineBuffer.fromKLineData(testData)
218
+ const originalBuffer = layout.buffer
219
+
220
+ // 修改数据但保持相同长度
221
+ const newData = generateTestData(testDataLength)
222
+ const updated = SharedKLineBuffer.updateExisting(layout, newData)
223
+
224
+ expect(updated.buffer).toBe(originalBuffer)
225
+ expect(updated.length).toBe(testDataLength)
226
+ })
227
+
228
+ it('不同长度数据应该创建新缓冲区', () => {
229
+ const layout = SharedKLineBuffer.fromKLineData(testData)
230
+ const originalBuffer = layout.buffer
231
+
232
+ const newData = generateTestData(testDataLength + 10)
233
+ const updated = SharedKLineBuffer.updateExisting(layout, newData)
234
+
235
+ expect(updated.buffer).not.toBe(originalBuffer)
236
+ expect(updated.length).toBe(testDataLength + 10)
237
+ })
238
+
239
+ it('应该正确更新数据内容', () => {
240
+ const layout = SharedKLineBuffer.fromKLineData(testData)
241
+ const newData = generateTestData(testDataLength)
242
+
243
+ SharedKLineBuffer.updateExisting(layout, newData)
244
+
245
+ for (let i = 0; i < testDataLength; i++) {
246
+ expect(layout.timestamps[i]).toBe(newData[i]!.timestamp)
247
+ expect(layout.closes[i]).toBe(newData[i]!.close)
248
+ }
249
+ })
250
+ })
251
+
252
+ describe('createSubview', () => {
253
+ it('应该创建正确的子视图', () => {
254
+ const layout = SharedKLineBuffer.fromKLineData(testData)
255
+ const subview = SharedKLineBuffer.createSubview(layout, 10, 30)
256
+
257
+ expect(subview.length).toBe(20)
258
+ expect(subview.buffer).toBe(layout.buffer)
259
+ })
260
+
261
+ it('子视图应该共享同一缓冲区', () => {
262
+ const layout = SharedKLineBuffer.fromKLineData(testData)
263
+ const subview = SharedKLineBuffer.createSubview(layout, 10, 30)
264
+
265
+ // 修改子视图应该影响原视图
266
+ subview.closes[0] = 999.99
267
+ expect(layout.closes[10]).toBe(999.99)
268
+ })
269
+
270
+ it('越界范围应该自动截断', () => {
271
+ const layout = SharedKLineBuffer.fromKLineData(testData)
272
+ const subview = SharedKLineBuffer.createSubview(layout, testDataLength - 5, testDataLength + 10)
273
+
274
+ expect(subview.length).toBe(5)
275
+ })
276
+
277
+ it('无效范围应该抛出错误', () => {
278
+ const layout = SharedKLineBuffer.fromKLineData(testData)
279
+ expect(() => SharedKLineBuffer.createSubview(layout, 50, 50)).toThrow()
280
+ expect(() => SharedKLineBuffer.createSubview(layout, 50, 40)).toThrow()
281
+ })
282
+ })
283
+
284
+ describe('getBufferInfo', () => {
285
+ it('应该返回正确的缓冲区信息', () => {
286
+ const layout = SharedKLineBuffer.fromKLineData(testData)
287
+ const info = SharedKLineBuffer.getBufferInfo(layout)
288
+
289
+ expect(info.byteLength).toBe(layout.buffer.byteLength)
290
+ expect(info.isShared).toBe(layout.isShared)
291
+ expect(info.length).toBe(layout.length)
292
+ expect(info.columns).toHaveLength(7)
293
+ })
294
+ })
295
+
296
+ describe('helper functions', () => {
297
+ it('getClosesView 应该返回 closes 数组', () => {
298
+ const layout = SharedKLineBuffer.fromKLineData(testData)
299
+ const closes = getClosesView(layout)
300
+ expect(closes).toBe(layout.closes)
301
+ })
302
+
303
+ it('getHighsLowsViews 应该返回 highs 和 lows', () => {
304
+ const layout = SharedKLineBuffer.fromKLineData(testData)
305
+ const { highs, lows } = getHighsLowsViews(layout)
306
+ expect(highs).toBe(layout.highs)
307
+ expect(lows).toBe(layout.lows)
308
+ })
309
+
310
+ it('getOHLCViews 应该返回全部价格视图', () => {
311
+ const layout = SharedKLineBuffer.fromKLineData(testData)
312
+ const views = getOHLCViews(layout)
313
+ expect(views.opens).toBe(layout.opens)
314
+ expect(views.highs).toBe(layout.highs)
315
+ expect(views.lows).toBe(layout.lows)
316
+ expect(views.closes).toBe(layout.closes)
317
+ })
318
+ })
319
+ })
320
+
321
+ describe('SoA Calculator Wrappers', () => {
322
+ const testDataLength = 200
323
+ let testData: KLineData[]
324
+ let layout: KLineSoALayout
325
+
326
+ beforeAll(() => {
327
+ testData = generateTestData(testDataLength)
328
+ layout = SharedKLineBuffer.fromKLineData(testData)
329
+ })
330
+
331
+ describe('calcBOLLDataSoA', () => {
332
+ it('SoA 结果应该与 AoS 结果一致', () => {
333
+ const aosResult = calcBOLLData(testData, 20, 2)
334
+ const soaResult = calcBOLLDataSoA(layout, 20, 2)
335
+
336
+ expect(soaResult.length).toBe(aosResult.length)
337
+
338
+ for (let i = 0; i < aosResult.length; i++) {
339
+ const a = aosResult[i]
340
+ const s = soaResult[i]
341
+
342
+ if (a === undefined) {
343
+ expect(s).toBeUndefined()
344
+ } else {
345
+ expect(s).toBeDefined()
346
+ expect(s!.upper).toBeCloseTo(a.upper, 10)
347
+ expect(s!.middle).toBeCloseTo(a.middle, 10)
348
+ expect(s!.lower).toBeCloseTo(a.lower, 10)
349
+ }
350
+ }
351
+ })
352
+ })
353
+
354
+ describe('calcEXPMADataSoA', () => {
355
+ it('SoA 结果应该与 AoS 结果一致', () => {
356
+ const aosResult = calcEXPMAData(testData, 12, 50)
357
+ const soaResult = calcEXPMADataSoA(layout, 12, 50)
358
+
359
+ for (let i = 0; i < aosResult.length; i++) {
360
+ const a = aosResult[i]
361
+ const s = soaResult[i]
362
+
363
+ if (a === undefined) {
364
+ expect(s).toBeUndefined()
365
+ } else {
366
+ expect(s!.fast).toBeCloseTo(a.fast, 10)
367
+ expect(s!.slow).toBeCloseTo(a.slow, 10)
368
+ }
369
+ }
370
+ })
371
+ })
372
+
373
+ describe('calcENEDataSoA', () => {
374
+ it('SoA 结果应该与 AoS 结果一致', () => {
375
+ const aosResult = calcENEData(testData, 10, 11)
376
+ const soaResult = calcENEDataSoA(layout, 10, 11)
377
+
378
+ for (let i = 0; i < aosResult.length; i++) {
379
+ const a = aosResult[i]
380
+ const s = soaResult[i]
381
+
382
+ if (a === undefined) {
383
+ expect(s).toBeUndefined()
384
+ } else {
385
+ expect(s!.upper).toBeCloseTo(a.upper, 10)
386
+ expect(s!.middle).toBeCloseTo(a.middle, 10)
387
+ expect(s!.lower).toBeCloseTo(a.lower, 10)
388
+ }
389
+ }
390
+ })
391
+ })
392
+
393
+ describe('calcMADataSoA', () => {
394
+ it('SoA 结果应该与 AoS 结果一致', () => {
395
+ const aosResult = calcMAData(testData, 20)
396
+ const soaResult = calcMADataSoA(layout, 20)
397
+
398
+ expect(compareNumberArrays(aosResult, soaResult)).toBe(true)
399
+ })
400
+ })
401
+
402
+ describe('calcRSIDataSoA', () => {
403
+ it('SoA 结果应该与 AoS 结果一致', () => {
404
+ const aosResult = calcRSIData(testData, 14)
405
+ const soaResult = calcRSIDataSoA(layout, 14)
406
+
407
+ expect(compareNumberArrays(aosResult, soaResult)).toBe(true)
408
+ })
409
+ })
410
+
411
+ describe('calcCCIDataSoA', () => {
412
+ it('SoA 结果应该与 AoS 结果一致', () => {
413
+ const aosResult = calcCCIData(testData, 14)
414
+ const soaResult = calcCCIDataSoA(layout, 14)
415
+
416
+ expect(compareNumberArrays(aosResult, soaResult)).toBe(true)
417
+ })
418
+ })
419
+
420
+ describe('calcSTOCHDataSoA', () => {
421
+ it('SoA 结果应该与 AoS 结果一致', () => {
422
+ const aosResult = calcSTOCHData(testData, 9, 3)
423
+ const soaResult = calcSTOCHDataSoA(layout, 9, 3)
424
+
425
+ for (let i = 0; i < aosResult.length; i++) {
426
+ const a = aosResult[i]
427
+ const s = soaResult[i]
428
+
429
+ if (a === undefined) {
430
+ expect(s).toBeUndefined()
431
+ } else {
432
+ expect(s!.k).toBeCloseTo(a.k, 10)
433
+ expect(s!.d).toBeCloseTo(a.d, 10)
434
+ }
435
+ }
436
+ })
437
+ })
438
+
439
+ describe('calcMOMDataSoA', () => {
440
+ it('SoA 结果应该与 AoS 结果一致', () => {
441
+ const aosResult = calcMOMData(testData, 10)
442
+ const soaResult = calcMOMDataSoA(layout, 10)
443
+
444
+ expect(compareNumberArrays(aosResult, soaResult)).toBe(true)
445
+ })
446
+ })
447
+
448
+ describe('calcWMSRDataSoA', () => {
449
+ it('SoA 结果应该与 AoS 结果一致', () => {
450
+ const aosResult = calcWMSRData(testData, 14)
451
+ const soaResult = calcWMSRDataSoA(layout, 14)
452
+
453
+ expect(compareNumberArrays(aosResult, soaResult)).toBe(true)
454
+ })
455
+ })
456
+
457
+ describe('calcKSTDataSoA', () => {
458
+ it('SoA 结果应该与 AoS 结果一致', () => {
459
+ const aosResult = calcKSTData(testData, 10, 15, 20, 30, 9)
460
+ const soaResult = calcKSTDataSoA(layout, 10, 15, 20, 30, 9)
461
+
462
+ for (let i = 0; i < aosResult.length; i++) {
463
+ const a = aosResult[i]
464
+ const s = soaResult[i]
465
+
466
+ if (a === undefined) {
467
+ expect(s).toBeUndefined()
468
+ } else {
469
+ expect(s!.kst).toBeCloseTo(a.kst, 10)
470
+ expect(s!.signal).toBeCloseTo(a.signal, 10)
471
+ }
472
+ }
473
+ })
474
+ })
475
+
476
+ describe('calcFASTKDataSoA', () => {
477
+ it('SoA 结果应该与 AoS 结果一致', () => {
478
+ const aosResult = calcFASTKData(testData, 9)
479
+ const soaResult = calcFASTKDataSoA(layout, 9)
480
+
481
+ expect(compareNumberArrays(aosResult, soaResult)).toBe(true)
482
+ })
483
+ })
484
+
485
+ describe('calcMACDDataSoA', () => {
486
+ it('SoA 结果应该与 AoS 结果一致', () => {
487
+ const aosResult = calcMACDData(testData, 12, 26, 9)
488
+ const soaResult = calcMACDDataSoA(layout, 12, 26, 9)
489
+
490
+ for (let i = 0; i < aosResult.length; i++) {
491
+ const a = aosResult[i]
492
+ const s = soaResult[i]
493
+
494
+ if (a === undefined) {
495
+ expect(s).toBeUndefined()
496
+ } else {
497
+ expect(s!.dif).toBeCloseTo(a.dif, 10)
498
+ expect(s!.dea).toBeCloseTo(a.dea, 10)
499
+ expect(s!.macd).toBeCloseTo(a.macd, 10)
500
+ }
501
+ }
502
+ })
503
+ })
504
+ })
505
+
506
+ describe('SoA Performance', () => {
507
+ it('大容量数据转换应该高效', () => {
508
+ const largeData = generateTestData(10000)
509
+
510
+ const start = performance.now()
511
+ const layout = SharedKLineBuffer.fromKLineData(largeData)
512
+ const layoutTime = performance.now() - start
513
+
514
+ const start2 = performance.now()
515
+ const restored = SharedKLineBuffer.toKLineData(layout)
516
+ const restoreTime = performance.now() - start2
517
+
518
+ // 转换应该在合理时间内完成(10K 数据 < 100ms)
519
+ expect(layoutTime).toBeLessThan(100)
520
+ expect(restoreTime).toBeLessThan(100)
521
+ expect(restored.length).toBe(10000)
522
+ })
523
+
524
+ it('SoA 计算器应该与 AoS 产生相同结果', () => {
525
+ const data = generateTestData(1000)
526
+ const layout = SharedKLineBuffer.fromKLineData(data)
527
+
528
+ const aosResult = calcBOLLData(data, 20, 2)
529
+ const soaResult = calcBOLLDataSoA(layout, 20, 2)
530
+
531
+ expect(soaResult.length).toBe(aosResult.length)
532
+ })
533
+ })