@363045841yyt/klinechart-core 0.7.3 → 0.7.5-alpha.2

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 (231) hide show
  1. package/README.md +201 -201
  2. package/README.zh-CN.md +201 -201
  3. package/dist/engine/renderers/webgl/candleSurface.js +47 -47
  4. package/dist/version.d.ts +1 -1
  5. package/dist/version.d.ts.map +1 -1
  6. package/dist/version.js +1 -2
  7. package/dist/version.js.map +1 -1
  8. package/package.json +129 -122
  9. package/src/__tests__/signal.test.ts +124 -124
  10. package/src/config/chartSettings.ts +66 -66
  11. package/src/controllers/__tests__/drawing.test.ts +214 -214
  12. package/src/controllers/__tests__/indicatorSelector.test.ts +481 -481
  13. package/src/controllers/__tests__/toolbar.test.ts +225 -225
  14. package/src/controllers/createChartController.ts +665 -665
  15. package/src/controllers/createDrawingController.ts +96 -96
  16. package/src/controllers/createIndicatorSelectorController.ts +307 -307
  17. package/src/controllers/createToolbarController.ts +146 -146
  18. package/src/controllers/index.ts +19 -19
  19. package/src/controllers/types.ts +284 -284
  20. package/src/engine/__tests__/chart.dpr.test.ts +401 -401
  21. package/src/engine/__tests__/paneRenderer.resize.test.ts +92 -92
  22. package/src/engine/chart-store.ts +121 -121
  23. package/src/engine/chart.d.ts +617 -617
  24. package/src/engine/chart.ts +2815 -2815
  25. package/src/engine/controller/__tests__/interaction.dpr.test.ts +259 -259
  26. package/src/engine/controller/interaction.ts +722 -722
  27. package/src/engine/controller/markerInteraction.ts +130 -130
  28. package/src/engine/controller/pinchTracker.ts +82 -82
  29. package/src/engine/controller/tooltipPosition.ts +48 -48
  30. package/src/engine/draw/__tests__/pixelAlign.spec.ts +176 -176
  31. package/src/engine/draw/pixelAlign.ts +259 -259
  32. package/src/engine/drawing/index.ts +655 -655
  33. package/src/engine/drawing/interaction.ts +842 -842
  34. package/src/engine/drawing/plugin.ts +343 -343
  35. package/src/engine/indicators/__tests__/__fixtures__/golden/atr.json +38 -38
  36. package/src/engine/indicators/__tests__/__fixtures__/golden/dema.json +14 -14
  37. package/src/engine/indicators/__tests__/__fixtures__/golden/hma.json +14 -14
  38. package/src/engine/indicators/__tests__/__fixtures__/golden/index.ts +55 -55
  39. package/src/engine/indicators/__tests__/__fixtures__/golden/kama.json +14 -14
  40. package/src/engine/indicators/__tests__/__fixtures__/golden/tema.json +14 -14
  41. package/src/engine/indicators/__tests__/__fixtures__/golden/wma.json +40 -40
  42. package/src/engine/indicators/__tests__/__fixtures__/synthetic.ts +65 -65
  43. package/src/engine/indicators/__tests__/_propertyAssertions.ts +76 -76
  44. package/src/engine/indicators/__tests__/atr.test.ts +153 -153
  45. package/src/engine/indicators/__tests__/calculators.test.ts +614 -614
  46. package/src/engine/indicators/__tests__/cmf-mfi.test.ts +100 -100
  47. package/src/engine/indicators/__tests__/dema.test.ts +73 -73
  48. package/src/engine/indicators/__tests__/donchian.test.ts +70 -70
  49. package/src/engine/indicators/__tests__/hma.test.ts +73 -73
  50. package/src/engine/indicators/__tests__/ichimoku.test.ts +105 -105
  51. package/src/engine/indicators/__tests__/kama.test.ts +80 -80
  52. package/src/engine/indicators/__tests__/keltner.test.ts +65 -65
  53. package/src/engine/indicators/__tests__/pivot-fib.test.ts +110 -110
  54. package/src/engine/indicators/__tests__/roc.test.ts +68 -68
  55. package/src/engine/indicators/__tests__/sar.test.ts +86 -86
  56. package/src/engine/indicators/__tests__/scheduler.test.ts +831 -831
  57. package/src/engine/indicators/__tests__/soa.test.ts +533 -533
  58. package/src/engine/indicators/__tests__/structure.test.ts +110 -110
  59. package/src/engine/indicators/__tests__/supertrend.test.ts +65 -65
  60. package/src/engine/indicators/__tests__/tema.test.ts +68 -68
  61. package/src/engine/indicators/__tests__/trix.test.ts +70 -70
  62. package/src/engine/indicators/__tests__/volatility.test.ts +117 -117
  63. package/src/engine/indicators/__tests__/volume.test.ts +115 -115
  64. package/src/engine/indicators/__tests__/volumeProfile.test.ts +74 -74
  65. package/src/engine/indicators/__tests__/vwap.test.ts +69 -69
  66. package/src/engine/indicators/__tests__/wma.test.ts +112 -112
  67. package/src/engine/indicators/__tests__/zones.test.ts +95 -95
  68. package/src/engine/indicators/atrState.ts +27 -27
  69. package/src/engine/indicators/bollState.ts +51 -51
  70. package/src/engine/indicators/calculators.ts +2593 -2593
  71. package/src/engine/indicators/cciState.ts +25 -25
  72. package/src/engine/indicators/chaikinVolState.ts +32 -32
  73. package/src/engine/indicators/cmfState.ts +27 -27
  74. package/src/engine/indicators/demaState.ts +27 -27
  75. package/src/engine/indicators/donchianState.ts +43 -43
  76. package/src/engine/indicators/eneState.ts +43 -43
  77. package/src/engine/indicators/expmaState.ts +43 -43
  78. package/src/engine/indicators/fastkState.ts +25 -25
  79. package/src/engine/indicators/fibState.ts +41 -41
  80. package/src/engine/indicators/hmaState.ts +27 -27
  81. package/src/engine/indicators/hvState.ts +28 -28
  82. package/src/engine/indicators/ichimokuState.ts +70 -70
  83. package/src/engine/indicators/indicator.worker.ts +169 -169
  84. package/src/engine/indicators/indicatorDefinitionRegistry.ts +62 -62
  85. package/src/engine/indicators/indicatorMetadata.ts +110 -110
  86. package/src/engine/indicators/indicatorRegistry.ts +106 -106
  87. package/src/engine/indicators/indicatorRuntime.ts +1548 -1548
  88. package/src/engine/indicators/kamaState.ts +34 -34
  89. package/src/engine/indicators/keltnerState.ts +49 -49
  90. package/src/engine/indicators/kstState.ts +42 -42
  91. package/src/engine/indicators/maState.ts +36 -36
  92. package/src/engine/indicators/macdState.ts +76 -76
  93. package/src/engine/indicators/mfiState.ts +27 -27
  94. package/src/engine/indicators/momState.ts +25 -25
  95. package/src/engine/indicators/obvState.ts +25 -25
  96. package/src/engine/indicators/parkinsonState.ts +28 -28
  97. package/src/engine/indicators/pivotState.ts +51 -51
  98. package/src/engine/indicators/pvtState.ts +25 -25
  99. package/src/engine/indicators/rocState.ts +27 -27
  100. package/src/engine/indicators/rsiState.ts +65 -65
  101. package/src/engine/indicators/sarState.ts +41 -41
  102. package/src/engine/indicators/scheduler.ts +1205 -1205
  103. package/src/engine/indicators/soa.ts +352 -352
  104. package/src/engine/indicators/stateComposer.ts +1262 -1262
  105. package/src/engine/indicators/stochState.ts +26 -26
  106. package/src/engine/indicators/structureState.ts +69 -69
  107. package/src/engine/indicators/supertrendState.ts +37 -37
  108. package/src/engine/indicators/temaState.ts +27 -27
  109. package/src/engine/indicators/trixState.ts +35 -35
  110. package/src/engine/indicators/vmaState.ts +27 -27
  111. package/src/engine/indicators/volumeProfileState.ts +63 -63
  112. package/src/engine/indicators/vwapState.ts +29 -29
  113. package/src/engine/indicators/wmaState.ts +27 -27
  114. package/src/engine/indicators/wmsrState.ts +25 -25
  115. package/src/engine/indicators/workerProtocol.ts +613 -613
  116. package/src/engine/indicators/zonesState.ts +47 -47
  117. package/src/engine/layout/pane.ts +161 -161
  118. package/src/engine/marker/registry.ts +265 -265
  119. package/src/engine/paneRenderer.ts +169 -169
  120. package/src/engine/renderers/Indicator/atr.ts +237 -237
  121. package/src/engine/renderers/Indicator/boll.ts +317 -317
  122. package/src/engine/renderers/Indicator/cci.ts +275 -275
  123. package/src/engine/renderers/Indicator/chaikinVol.ts +138 -138
  124. package/src/engine/renderers/Indicator/cmf.ts +137 -137
  125. package/src/engine/renderers/Indicator/dema.ts +136 -136
  126. package/src/engine/renderers/Indicator/donchian.ts +137 -137
  127. package/src/engine/renderers/Indicator/ene.ts +271 -271
  128. package/src/engine/renderers/Indicator/expma.ts +197 -197
  129. package/src/engine/renderers/Indicator/fastk.ts +316 -316
  130. package/src/engine/renderers/Indicator/fib.ts +141 -141
  131. package/src/engine/renderers/Indicator/hma.ts +136 -136
  132. package/src/engine/renderers/Indicator/hv.ts +124 -124
  133. package/src/engine/renderers/Indicator/ichimoku.ts +181 -181
  134. package/src/engine/renderers/Indicator/index.ts +241 -241
  135. package/src/engine/renderers/Indicator/indicatorData.ts +650 -650
  136. package/src/engine/renderers/Indicator/kama.ts +136 -136
  137. package/src/engine/renderers/Indicator/keltner.ts +137 -137
  138. package/src/engine/renderers/Indicator/kst.ts +302 -302
  139. package/src/engine/renderers/Indicator/ma.ts +200 -200
  140. package/src/engine/renderers/Indicator/macd.ts +477 -477
  141. package/src/engine/renderers/Indicator/macdLegend.ts +141 -141
  142. package/src/engine/renderers/Indicator/mainIndicatorLegend.ts +272 -272
  143. package/src/engine/renderers/Indicator/mfi.ts +142 -142
  144. package/src/engine/renderers/Indicator/mom.ts +311 -311
  145. package/src/engine/renderers/Indicator/obv.ts +123 -123
  146. package/src/engine/renderers/Indicator/parkinson.ts +124 -124
  147. package/src/engine/renderers/Indicator/pivot.ts +131 -131
  148. package/src/engine/renderers/Indicator/pvt.ts +123 -123
  149. package/src/engine/renderers/Indicator/roc.ts +143 -143
  150. package/src/engine/renderers/Indicator/rsi.ts +390 -390
  151. package/src/engine/renderers/Indicator/sar.ts +113 -113
  152. package/src/engine/renderers/Indicator/scale/atr_scale.ts +19 -19
  153. package/src/engine/renderers/Indicator/scale/cci_scale.ts +19 -19
  154. package/src/engine/renderers/Indicator/scale/fastk_scale.ts +19 -19
  155. package/src/engine/renderers/Indicator/scale/indicator_scale.ts +204 -204
  156. package/src/engine/renderers/Indicator/scale/kst_scale.ts +19 -19
  157. package/src/engine/renderers/Indicator/scale/macd_scale.ts +22 -22
  158. package/src/engine/renderers/Indicator/scale/mom_scale.ts +19 -19
  159. package/src/engine/renderers/Indicator/scale/rsi_scale.ts +19 -19
  160. package/src/engine/renderers/Indicator/scale/stoch_scale.ts +19 -19
  161. package/src/engine/renderers/Indicator/scale/volume_scale.ts +26 -26
  162. package/src/engine/renderers/Indicator/scale/wmsr_scale.ts +19 -19
  163. package/src/engine/renderers/Indicator/stoch.ts +359 -359
  164. package/src/engine/renderers/Indicator/structure.ts +126 -126
  165. package/src/engine/renderers/Indicator/subPaneConfig.ts +265 -265
  166. package/src/engine/renderers/Indicator/supertrend.ts +115 -115
  167. package/src/engine/renderers/Indicator/tema.ts +136 -136
  168. package/src/engine/renderers/Indicator/trix.ts +158 -158
  169. package/src/engine/renderers/Indicator/vma.ts +124 -124
  170. package/src/engine/renderers/Indicator/volumeProfile.ts +125 -125
  171. package/src/engine/renderers/Indicator/vwap.ts +123 -123
  172. package/src/engine/renderers/Indicator/wma.ts +136 -136
  173. package/src/engine/renderers/Indicator/wmsr.ts +328 -328
  174. package/src/engine/renderers/Indicator/zones.ts +104 -104
  175. package/src/engine/renderers/__tests__/boll.renderer.test.ts +314 -314
  176. package/src/engine/renderers/__tests__/ene.renderer.test.ts +305 -305
  177. package/src/engine/renderers/__tests__/expma.renderer.test.ts +279 -279
  178. package/src/engine/renderers/__tests__/ma.renderer.test.ts +426 -426
  179. package/src/engine/renderers/__tests__/mainIndicatorLegend.renderer.test.ts +502 -502
  180. package/src/engine/renderers/__tests__/yAxis.renderer.test.ts +173 -173
  181. package/src/engine/renderers/candle.ts +459 -459
  182. package/src/engine/renderers/crosshair.ts +69 -69
  183. package/src/engine/renderers/customMarkers.ts +162 -162
  184. package/src/engine/renderers/extremaMarkers.ts +246 -246
  185. package/src/engine/renderers/gridLines.ts +90 -90
  186. package/src/engine/renderers/lastPrice.ts +97 -97
  187. package/src/engine/renderers/paneTitle.ts +136 -136
  188. package/src/engine/renderers/subVolume.ts +236 -236
  189. package/src/engine/renderers/timeAxis.ts +121 -121
  190. package/src/engine/renderers/webgl/candleSurface.ts +955 -955
  191. package/src/engine/renderers/webgl/sharedWebGLSurface.ts +146 -146
  192. package/src/engine/renderers/yAxis.ts +105 -105
  193. package/src/engine/scale/__tests__/logFormula.spec.ts +148 -148
  194. package/src/engine/scale/logFormula.ts +130 -130
  195. package/src/engine/scale/price.ts +39 -39
  196. package/src/engine/scale/priceScale.ts +264 -264
  197. package/src/engine/subPaneManager.ts +427 -427
  198. package/src/engine/theme/colors.ts +642 -642
  199. package/src/engine/theme/fonts.ts +20 -20
  200. package/src/engine/utils/klineConfig.ts +49 -49
  201. package/src/engine/utils/tickCount.ts +11 -11
  202. package/src/engine/utils/tickPosition.ts +214 -214
  203. package/src/engine/utils/zoom.ts +83 -83
  204. package/src/engine/viewport/viewport.ts +67 -67
  205. package/src/index.ts +3 -3
  206. package/src/plugin/ConfigManager.ts +93 -93
  207. package/src/plugin/EventBus.ts +77 -77
  208. package/src/plugin/HookSystem.ts +106 -106
  209. package/src/plugin/PluginHost.ts +243 -243
  210. package/src/plugin/PluginRegistry.ts +92 -92
  211. package/src/plugin/StateStore.ts +73 -73
  212. package/src/plugin/index.ts +19 -19
  213. package/src/plugin/rendererPluginManager.ts +368 -368
  214. package/src/plugin/stateKeys.ts +8 -8
  215. package/src/plugin/types.ts +526 -526
  216. package/src/reactivity/index.ts +2 -2
  217. package/src/reactivity/signal.ts +119 -119
  218. package/src/semantic/controller.ts +251 -251
  219. package/src/semantic/drawShape.ts +260 -260
  220. package/src/semantic/index.ts +28 -28
  221. package/src/semantic/schema.json +256 -256
  222. package/src/semantic/types.ts +251 -251
  223. package/src/semantic/validator.ts +349 -349
  224. package/src/types/kLine.ts +13 -13
  225. package/src/types/price.ts +56 -56
  226. package/src/types/volumePrice.ts +33 -33
  227. package/src/utils/dateFormat.ts +208 -208
  228. package/src/utils/kLineDraw/axis.ts +562 -562
  229. package/src/utils/priceToY.ts +34 -34
  230. package/src/utils/volumePrice.ts +202 -202
  231. package/src/version.ts +1 -1
@@ -1,2593 +1,2593 @@
1
- import type { KLineData } from '../../types/price'
2
-
3
- /**
4
- * MA 周期配置标志
5
- */
6
- export type MAFlags = {
7
- ma5?: boolean
8
- ma10?: boolean
9
- ma20?: boolean
10
- ma30?: boolean
11
- ma60?: boolean
12
- }
13
-
14
- /**
15
- * 默认 MA 周期列表
16
- */
17
- export const DEFAULT_MA_PERIODS = [5, 10, 20, 30, 60] as const
18
-
19
- // ============================================================================
20
- // BOLL 布林带
21
- // ============================================================================
22
-
23
- /**
24
- * BOLL 数据点
25
- */
26
- export interface BOLLPoint {
27
- upper: number
28
- middle: number
29
- lower: number
30
- }
31
-
32
- /**
33
- * 默认 BOLL 参数
34
- */
35
- export const DEFAULT_BOLL_PERIOD = 20
36
- export const DEFAULT_BOLL_MULTIPLIER = 2
37
-
38
- /**
39
- * 计算 BOLL 数据(使用滑动窗口优化)
40
- * @param data K线数据数组
41
- * @param period 周期(默认20)
42
- * @param multiplier 标准差倍数(默认2)
43
- * @returns 每个索引对应的BOLL值,前 period-1 个为 undefined
44
- */
45
- export function calcBOLLData(
46
- data: KLineData[],
47
- period: number,
48
- multiplier: number
49
- ): BOLLPoint[] {
50
- const result: BOLLPoint[] = new Array(data.length)
51
-
52
- if (data.length < period) return result
53
-
54
- // 使用滑动窗口计算,避免重复求和
55
- let sum = 0
56
- const window: number[] = []
57
-
58
- // 初始化第一个窗口
59
- for (let i = 0; i < period; i++) {
60
- const item = data[i]
61
- if (!item) return result
62
- const close = item.close
63
- window.push(close)
64
- sum += close
65
- }
66
-
67
- // 计算每个点的 BOLL
68
- for (let i = period - 1; i < data.length; i++) {
69
- const item = data[i]
70
- if (!item) continue
71
-
72
- // 更新窗口求和
73
- if (i >= period) {
74
- const oldVal = window.shift()
75
- if (oldVal !== undefined) sum -= oldVal
76
- const close = item.close
77
- window.push(close)
78
- sum += close
79
- }
80
-
81
- const ma = sum / period
82
-
83
- // 计算标准差
84
- let variance = 0
85
- for (let j = 0; j < period; j++) {
86
- const wVal = window[j]
87
- if (wVal !== undefined) {
88
- variance += Math.pow(wVal - ma, 2)
89
- }
90
- }
91
- const stdDev = Math.sqrt(variance / period)
92
-
93
- result[i] = {
94
- upper: ma + multiplier * stdDev,
95
- middle: ma,
96
- lower: ma - multiplier * stdDev,
97
- }
98
- }
99
-
100
- return result
101
- }
102
-
103
- // ============================================================================
104
- // EXPMA 指数平滑移动平均线
105
- // ============================================================================
106
-
107
- /**
108
- * EXPMA 数据点
109
- */
110
- export interface EXPMAPoint {
111
- fast: number
112
- slow: number
113
- }
114
-
115
- /**
116
- * 默认 EXPMA 参数
117
- */
118
- export const DEFAULT_EXPMA_FAST_PERIOD = 12
119
- export const DEFAULT_EXPMA_SLOW_PERIOD = 50
120
-
121
- /**
122
- * 计算 EXPMA 数据
123
- * 公式:EXPMA(i) = C(i) × K + EXPMA(i-1) × (1-K),K = 2/(N+1)
124
- * @param data K线数据数组
125
- * @param fastPeriod 快线周期(默认12)
126
- * @param slowPeriod 慢线周期(默认50)
127
- * @returns 每个索引对应的EXPMA值(从 index 0 开始有值)
128
- */
129
- export function calcEXPMAData(
130
- data: KLineData[],
131
- fastPeriod: number,
132
- slowPeriod: number
133
- ): EXPMAPoint[] {
134
- const result: EXPMAPoint[] = new Array(data.length)
135
-
136
- if (data.length === 0) return result
137
-
138
- const fastK = 2 / (fastPeriod + 1)
139
- const slowK = 2 / (slowPeriod + 1)
140
-
141
- // 第一个点的 EXPMA 等于第一天的收盘价
142
- const firstClose = data[0]!.close
143
- let fastEMA = firstClose
144
- let slowEMA = firstClose
145
-
146
- result[0] = { fast: fastEMA, slow: slowEMA }
147
-
148
- for (let i = 1; i < data.length; i++) {
149
- const close = data[i]!.close
150
- fastEMA = close * fastK + fastEMA * (1 - fastK)
151
- slowEMA = close * slowK + slowEMA * (1 - slowK)
152
- result[i] = { fast: fastEMA, slow: slowEMA }
153
- }
154
-
155
- return result
156
- }
157
-
158
- // ============================================================================
159
- // ENE 轨道线
160
- // ============================================================================
161
-
162
- /**
163
- * ENE 数据点
164
- */
165
- export interface ENEPoint {
166
- upper: number
167
- middle: number
168
- lower: number
169
- }
170
-
171
- /**
172
- * 默认 ENE 参数
173
- */
174
- export const DEFAULT_ENE_PERIOD = 10
175
- export const DEFAULT_ENE_DEVIATION = 11
176
-
177
- /**
178
- * 计算 ENE 数据
179
- * 中轨 = MA(close, N)
180
- * 上轨 = 中轨 × (1 + M/100)
181
- * 下轨 = 中轨 × (1 - M/100)
182
- * @param data K线数据数组
183
- * @param period 周期(默认10)
184
- * @param deviation 偏离率百分比(默认11)
185
- * @returns 每个索引对应的ENE值,前 period-1 个为 undefined
186
- */
187
- export function calcENEData(
188
- data: KLineData[],
189
- period: number,
190
- deviation: number
191
- ): ENEPoint[] {
192
- const result: ENEPoint[] = new Array(data.length)
193
-
194
- if (data.length < period) return result
195
-
196
- // 使用滑动窗口计算 MA
197
- let sum = 0
198
-
199
- // 初始化第一个窗口
200
- for (let i = 0; i < period; i++) {
201
- const item = data[i]
202
- if (!item) return result
203
- sum += item.close
204
- }
205
-
206
- // 第一个有效点
207
- const firstMA = sum / period
208
- const firstDeviation = deviation / 100
209
- result[period - 1] = {
210
- upper: firstMA * (1 + firstDeviation),
211
- middle: firstMA,
212
- lower: firstMA * (1 - firstDeviation),
213
- }
214
-
215
- // 滑动计算后续点
216
- for (let i = period; i < data.length; i++) {
217
- const prevItem = data[i - period]
218
- const currItem = data[i]
219
- if (!prevItem || !currItem) continue
220
-
221
- sum = sum - prevItem.close + currItem.close
222
- const ma = sum / period
223
- const dev = deviation / 100
224
-
225
- result[i] = {
226
- upper: ma * (1 + dev),
227
- middle: ma,
228
- lower: ma * (1 - dev),
229
- }
230
- }
231
-
232
- return result
233
- }
234
-
235
- /**
236
- * 计算指定周期的 MA 数据(使用滑动窗口优化,O(n) 复杂度)
237
- * @param data K线数据数组
238
- * @param period MA周期
239
- * @returns 每个索引对应的MA值,前 period-1 个为 undefined
240
- */
241
- export function calcMAData(data: KLineData[], period: number): (number | undefined)[] {
242
- const result: (number | undefined)[] = new Array(data.length)
243
-
244
- if (data.length < period) return result
245
-
246
- // 滑动窗口求和
247
- let sum = 0
248
-
249
- // 初始化第一个窗口
250
- for (let i = 0; i < period; i++) {
251
- const item = data[i]
252
- if (!item) return result
253
- sum += item.close
254
- }
255
-
256
- // 第一个有效点
257
- result[period - 1] = sum / period
258
-
259
- // 滑动计算后续点
260
- for (let i = period; i < data.length; i++) {
261
- const prevItem = data[i - period]
262
- const currItem = data[i]
263
- if (!prevItem || !currItem) continue
264
-
265
- sum = sum - prevItem.close + currItem.close
266
- result[i] = sum / period
267
- }
268
-
269
- return result
270
- }
271
-
272
- // ============================================================================
273
- // RSI 相对强弱指标
274
- // ============================================================================
275
-
276
- /**
277
- * 默认 RSI 参数
278
- */
279
- export const DEFAULT_RSI_PERIOD1 = 6
280
- export const DEFAULT_RSI_PERIOD2 = 12
281
- export const DEFAULT_RSI_PERIOD3 = 24
282
- export const DEFAULT_RSI_PERIODS = [6, 12, 24] as const
283
-
284
- /**
285
- * 计算 RSI 数据
286
- * RSI = 100 - 100 / (1 + RS)
287
- * RS = 平均上涨幅度 / 平均下跌幅度
288
- * @param data K线数据数组
289
- * @param period RSI周期
290
- * @returns 每个索引对应的RSI值,前 period+1 个为 undefined(需要 period+1 个数据点计算初始平均)
291
- */
292
- export function calcRSIData(data: KLineData[], period: number): (number | undefined)[] {
293
- const result: (number | undefined)[] = new Array(data.length)
294
-
295
- if (data.length < period + 1) return result
296
-
297
- // 计算价格变化
298
- const changes: number[] = []
299
- for (let i = 1; i < data.length; i++) {
300
- changes.push(data[i]!.close - data[i - 1]!.close)
301
- }
302
-
303
- // 初始化:计算前 period 天的平均涨跌
304
- let sumGain = 0
305
- let sumLoss = 0
306
-
307
- for (let i = 0; i < period; i++) {
308
- const change = changes[i]
309
- if (change !== undefined) {
310
- if (change > 0) sumGain += change
311
- else sumLoss += Math.abs(change)
312
- }
313
- }
314
-
315
- // 第一个 RSI 值
316
- let avgGain = sumGain / period
317
- let avgLoss = sumLoss / period
318
-
319
- if (avgLoss === 0) {
320
- result[period] = 100
321
- } else {
322
- const rs = avgGain / avgLoss
323
- result[period] = 100 - 100 / (1 + rs)
324
- }
325
-
326
- // 后续使用平滑计算(Wilder's smoothing)
327
- for (let i = period; i < changes.length; i++) {
328
- const change = changes[i]
329
- if (change === undefined) continue
330
-
331
- if (change > 0) {
332
- avgGain = (avgGain * (period - 1) + change) / period
333
- avgLoss = (avgLoss * (period - 1)) / period
334
- } else {
335
- avgGain = (avgGain * (period - 1)) / period
336
- avgLoss = (avgLoss * (period - 1) + Math.abs(change)) / period
337
- }
338
-
339
- if (avgLoss === 0) {
340
- result[i + 1] = 100
341
- } else {
342
- const rs = avgGain / avgLoss
343
- result[i + 1] = 100 - 100 / (1 + rs)
344
- }
345
- }
346
-
347
- return result
348
- }
349
-
350
- // ============================================================================
351
- // CCI 顺势指标
352
- // ============================================================================
353
-
354
- export const DEFAULT_CCI_PERIOD = 14
355
-
356
- export function calcCCIData(data: KLineData[], period: number): (number | undefined)[] {
357
- const result: (number | undefined)[] = new Array(data.length)
358
-
359
- if (data.length < period) return result
360
-
361
- // 计算 TP (Typical Price) = (H + L + C) / 3
362
- const tpValues: number[] = []
363
- for (const item of data) {
364
- tpValues.push((item.high + item.low + item.close) / 3)
365
- }
366
-
367
- // 计算 TP 的 SMA
368
- let sum = 0
369
- for (let i = 0; i < period; i++) {
370
- sum += tpValues[i]!
371
- }
372
-
373
- for (let i = period - 1; i < data.length; i++) {
374
- if (i >= period) {
375
- sum = sum - tpValues[i - period]! + tpValues[i]!
376
- }
377
- const sma = sum / period
378
-
379
- // 计算平均绝对偏差
380
- let meanDeviation = 0
381
- for (let j = 0; j < period; j++) {
382
- meanDeviation += Math.abs(tpValues[i - j]! - sma)
383
- }
384
- meanDeviation /= period
385
-
386
- if (meanDeviation === 0) {
387
- result[i] = 0
388
- } else {
389
- result[i] = (tpValues[i]! - sma) / (0.015 * meanDeviation)
390
- }
391
- }
392
-
393
- return result
394
- }
395
-
396
- // ============================================================================
397
- // STOCH 随机指标
398
- // ============================================================================
399
-
400
- export const DEFAULT_STOCH_N = 9
401
- export const DEFAULT_STOCH_M = 3
402
-
403
- export interface STOCHPoint {
404
- k: number
405
- d: number
406
- }
407
-
408
- export function calcSTOCHData(data: KLineData[], n: number, m: number): STOCHPoint[] {
409
- const result: STOCHPoint[] = new Array(data.length)
410
-
411
- if (data.length < n) return result
412
-
413
- // 计算 RSV 和 K
414
- const kValues: (number | undefined)[] = new Array(data.length)
415
-
416
- for (let i = n - 1; i < data.length; i++) {
417
- let highest = -Infinity
418
- let lowest = Infinity
419
-
420
- for (let j = 0; j < n; j++) {
421
- const item = data[i - j]
422
- if (!item) continue
423
- highest = Math.max(highest, item.high)
424
- lowest = Math.min(lowest, item.low)
425
- }
426
-
427
- const close = data[i]!.close
428
- if (highest === lowest) {
429
- kValues[i] = 50
430
- } else {
431
- kValues[i] = ((close - lowest) / (highest - lowest)) * 100
432
- }
433
- }
434
-
435
- // 计算 D (K 的 M 日移动平均)
436
- for (let i = n - 1 + m - 1; i < data.length; i++) {
437
- const k = kValues[i]
438
- if (k === undefined) continue
439
-
440
- let sum = 0
441
- let validCount = 0
442
- for (let j = 0; j < m; j++) {
443
- const kv = kValues[i - j]
444
- if (kv !== undefined) {
445
- sum += kv
446
- validCount++
447
- }
448
- }
449
-
450
- if (validCount === m) {
451
- result[i] = { k, d: sum / m }
452
- }
453
- }
454
-
455
- return result
456
- }
457
-
458
- // ============================================================================
459
- // MOM 动量指标
460
- // ============================================================================
461
-
462
- export const DEFAULT_MOM_PERIOD = 10
463
-
464
- export function calcMOMData(data: KLineData[], period: number): (number | undefined)[] {
465
- const result: (number | undefined)[] = new Array(data.length)
466
-
467
- if (data.length < period + 1) return result
468
-
469
- for (let i = period; i < data.length; i++) {
470
- const currentClose = data[i]?.close
471
- const prevClose = data[i - period]?.close
472
-
473
- if (currentClose !== undefined && prevClose !== undefined) {
474
- result[i] = currentClose - prevClose
475
- }
476
- }
477
-
478
- return result
479
- }
480
-
481
- // ============================================================================
482
- // WMSR 威廉指标
483
- // ============================================================================
484
-
485
- export const DEFAULT_WMSR_PERIOD = 14
486
-
487
- export function calcWMSRData(data: KLineData[], period: number): (number | undefined)[] {
488
- const result: (number | undefined)[] = new Array(data.length)
489
-
490
- if (data.length < period) return result
491
-
492
- for (let i = period - 1; i < data.length; i++) {
493
- let highest = -Infinity
494
- let lowest = Infinity
495
-
496
- for (let j = 0; j < period; j++) {
497
- const item = data[i - j]
498
- if (!item) continue
499
- highest = Math.max(highest, item.high)
500
- lowest = Math.min(lowest, item.low)
501
- }
502
-
503
- const close = data[i]!.close
504
- if (highest === lowest) {
505
- result[i] = -50
506
- } else {
507
- result[i] = ((highest - close) / (highest - lowest)) * -100
508
- }
509
- }
510
-
511
- return result
512
- }
513
-
514
- // ============================================================================
515
- // KST 确知指标
516
- // ============================================================================
517
-
518
- export const DEFAULT_KST_ROC1 = 10
519
- export const DEFAULT_KST_ROC2 = 15
520
- export const DEFAULT_KST_ROC3 = 20
521
- export const DEFAULT_KST_ROC4 = 30
522
- export const DEFAULT_KST_SIGNAL = 9
523
-
524
- export interface KSTPoint {
525
- kst: number
526
- signal: number
527
- }
528
-
529
- function calcROCInternal(data: KLineData[], period: number): (number | undefined)[] {
530
- const result: (number | undefined)[] = new Array(data.length)
531
-
532
- if (data.length < period + 1) return result
533
-
534
- for (let i = period; i < data.length; i++) {
535
- const currentClose = data[i]?.close
536
- const prevClose = data[i - period]?.close
537
-
538
- if (currentClose !== undefined && prevClose !== undefined && prevClose !== 0) {
539
- result[i] = ((currentClose - prevClose) / prevClose) * 100
540
- }
541
- }
542
-
543
- return result
544
- }
545
-
546
- function calcSMAInternal(data: (number | undefined)[], period: number): (number | undefined)[] {
547
- const result: (number | undefined)[] = new Array(data.length)
548
-
549
- let sum = 0
550
- let count = 0
551
-
552
- for (let i = 0; i < data.length; i++) {
553
- const val = data[i]
554
-
555
- if (val !== undefined) {
556
- sum += val
557
- count++
558
-
559
- if (count > period) {
560
- const oldVal = data[i - period]
561
- if (oldVal !== undefined) {
562
- sum -= oldVal
563
- count--
564
- }
565
- }
566
-
567
- if (count === period) {
568
- result[i] = sum / period
569
- }
570
- }
571
- }
572
-
573
- return result
574
- }
575
-
576
- export function calcKSTData(
577
- data: KLineData[],
578
- roc1: number,
579
- roc2: number,
580
- roc3: number,
581
- roc4: number,
582
- signalPeriod: number
583
- ): KSTPoint[] {
584
- const result: KSTPoint[] = new Array(data.length)
585
-
586
- const roc1Data = calcROCInternal(data, roc1)
587
- const roc2Data = calcROCInternal(data, roc2)
588
- const roc3Data = calcROCInternal(data, roc3)
589
- const roc4Data = calcROCInternal(data, roc4)
590
-
591
- const sma1 = calcSMAInternal(roc1Data, 10)
592
- const sma2 = calcSMAInternal(roc2Data, 10)
593
- const sma3 = calcSMAInternal(roc3Data, 10)
594
- const sma4 = calcSMAInternal(roc4Data, 15)
595
-
596
- const kstValues: (number | undefined)[] = new Array(data.length)
597
-
598
- for (let i = 0; i < data.length; i++) {
599
- const v1 = sma1[i]
600
- const v2 = sma2[i]
601
- const v3 = sma3[i]
602
- const v4 = sma4[i]
603
-
604
- if (v1 !== undefined && v2 !== undefined && v3 !== undefined && v4 !== undefined) {
605
- kstValues[i] = v1 * 1 + v2 * 2 + v3 * 3 + v4 * 4
606
- }
607
- }
608
-
609
- const signalData = calcSMAInternal(kstValues, signalPeriod)
610
-
611
- for (let i = 0; i < data.length; i++) {
612
- const kst = kstValues[i]
613
- const signal = signalData[i]
614
-
615
- if (kst !== undefined && signal !== undefined) {
616
- result[i] = { kst, signal }
617
- }
618
- }
619
-
620
- return result
621
- }
622
-
623
- // ============================================================================
624
- // FASTK 快速随机指标
625
- // ============================================================================
626
-
627
- export const DEFAULT_FASTK_PERIOD = 9
628
-
629
- export function calcFASTKData(data: KLineData[], period: number): (number | undefined)[] {
630
- const result: (number | undefined)[] = new Array(data.length)
631
-
632
- if (data.length < period) return result
633
-
634
- for (let i = period - 1; i < data.length; i++) {
635
- let highest = -Infinity
636
- let lowest = Infinity
637
-
638
- for (let j = 0; j < period; j++) {
639
- const item = data[i - j]
640
- if (!item) continue
641
- highest = Math.max(highest, item.high)
642
- lowest = Math.min(lowest, item.low)
643
- }
644
-
645
- const close = data[i]!.close
646
- if (highest === lowest) {
647
- result[i] = 50
648
- } else {
649
- result[i] = ((close - lowest) / (highest - lowest)) * 100
650
- }
651
- }
652
-
653
- return result
654
- }
655
-
656
- // ============================================================================
657
- // MACD 指数平滑异同移动平均线
658
- // ============================================================================
659
-
660
- /**
661
- * MACD 数据点
662
- */
663
- export interface MACDPoint {
664
- /** DIF 线值 */
665
- dif: number
666
- /** DEA 线值 */
667
- dea: number
668
- /** MACD 柱状图值 */
669
- macd: number
670
- }
671
-
672
- /**
673
- * 默认 MACD 参数
674
- */
675
- export const DEFAULT_MACD_FAST_PERIOD = 12
676
- export const DEFAULT_MACD_SLOW_PERIOD = 26
677
- export const DEFAULT_MACD_SIGNAL_PERIOD = 9
678
-
679
- /**
680
- * 计算 EMA(指数移动平均)值
681
- * EMA(today) = close × K + EMA(yesterday) × (1 - K)
682
- * K = 2 / (period + 1)
683
- * @param data K线数据数组
684
- * @param period 周期
685
- * @returns EMA 值数组,第一个值使用第一个收盘价
686
- */
687
- export function calcEMA(data: KLineData[], period: number): number[] {
688
- const result: number[] = new Array(data.length)
689
- const k = 2 / (period + 1)
690
-
691
- if (data.length === 0) return result
692
-
693
- // 第一个 EMA 值使用第一个收盘价
694
- result[0] = data[0]!.close
695
-
696
- for (let i = 1; i < data.length; i++) {
697
- const item = data[i]
698
- if (!item) continue
699
- result[i] = item.close * k + result[i - 1]! * (1 - k)
700
- }
701
-
702
- return result
703
- }
704
-
705
- /**
706
- * 基于数值数组计算 EMA
707
- * @param values 数值数组(可能包含 undefined)
708
- * @param period 周期
709
- * @returns EMA 值数组
710
- */
711
- export function calcEMAFromArray(values: (number | undefined)[], period: number): (number | undefined)[] {
712
- const result: (number | undefined)[] = new Array(values.length)
713
- const k = 2 / (period + 1)
714
-
715
- const firstValid = values.findIndex(v => v !== undefined)
716
- if (firstValid === -1) return result
717
-
718
- result[firstValid] = values[firstValid]
719
-
720
- for (let i = firstValid + 1; i < values.length; i++) {
721
- const val = values[i]
722
- const prev = result[i - 1]
723
- if (val === undefined || prev === undefined) continue
724
- result[i] = val * k + prev * (1 - k)
725
- }
726
-
727
- return result
728
- }
729
-
730
- /**
731
- * 计算 MACD 数据
732
- * DIF = EMA(close, fastPeriod) - EMA(close, slowPeriod)
733
- * DEA = EMA(DIF, signalPeriod)
734
- * MACD = (DIF - DEA) × 2
735
- * @param data K线数据数组
736
- * @param fastPeriod 快线周期(默认12)
737
- * @param slowPeriod 慢线周期(默认26)
738
- * @param signalPeriod 信号线周期(默认9)
739
- * @returns MACD 数据点数组,前 slowPeriod-1 个可能为 undefined
740
- */
741
- export function calcMACDData(
742
- data: KLineData[],
743
- fastPeriod: number,
744
- slowPeriod: number,
745
- signalPeriod: number
746
- ): MACDPoint[] {
747
- const result: MACDPoint[] = new Array(data.length)
748
-
749
- if (data.length < slowPeriod) return result
750
-
751
- // 计算 EMA12 和 EMA26
752
- const emaFast = calcEMA(data, fastPeriod)
753
- const emaSlow = calcEMA(data, slowPeriod)
754
-
755
- // 计算 DIF
756
- const dif: (number | undefined)[] = new Array(data.length)
757
- for (let i = 0; i < data.length; i++) {
758
- const fast = emaFast[i]
759
- const slow = emaSlow[i]
760
- if (fast !== undefined && slow !== undefined) {
761
- dif[i] = fast - slow
762
- }
763
- }
764
-
765
- // 计算 DEA(DIF 的 signalPeriod 日 EMA)
766
- const dea = calcEMAFromArray(dif, signalPeriod)
767
-
768
- // 计算 MACD 柱
769
- for (let i = 0; i < data.length; i++) {
770
- const d = dif[i]
771
- const e = dea[i]
772
- if (d !== undefined && e !== undefined) {
773
- result[i] = {
774
- dif: d,
775
- dea: e,
776
- macd: (d - e) * 2,
777
- }
778
- }
779
- }
780
-
781
- return result
782
- }
783
-
784
- // ============================================================================
785
- // SoA (Structure of Arrays) 包装函数
786
- // 用于验证 SoA 数据层与原始 AoS 计算的一致性
787
- // ============================================================================
788
-
789
- import type { KLineSoALayout } from './soa'
790
- import { SharedKLineBuffer } from './soa'
791
-
792
- /**
793
- * 从 SoA 布局计算 BOLL 数据(验证用包装函数)
794
- * @param layout SoA 布局
795
- * @param period 周期
796
- * @param multiplier 标准差倍数
797
- * @returns BOLL 数据点数组
798
- */
799
- export function calcBOLLDataSoA(
800
- layout: KLineSoALayout,
801
- period: number,
802
- multiplier: number
803
- ): BOLLPoint[] {
804
- const data = SharedKLineBuffer.toKLineData(layout)
805
- return calcBOLLData(data, period, multiplier)
806
- }
807
-
808
- /**
809
- * 从 SoA 布局计算 EXPMA 数据(验证用包装函数)
810
- * @param layout SoA 布局
811
- * @param fastPeriod 快线周期
812
- * @param slowPeriod 慢线周期
813
- * @returns EXPMA 数据点数组
814
- */
815
- export function calcEXPMADataSoA(
816
- layout: KLineSoALayout,
817
- fastPeriod: number,
818
- slowPeriod: number
819
- ): EXPMAPoint[] {
820
- const data = SharedKLineBuffer.toKLineData(layout)
821
- return calcEXPMAData(data, fastPeriod, slowPeriod)
822
- }
823
-
824
- /**
825
- * 从 SoA 布局计算 ENE 数据(验证用包装函数)
826
- * @param layout SoA 布局
827
- * @param period 周期
828
- * @param deviation 偏离率百分比
829
- * @returns ENE 数据点数组
830
- */
831
- export function calcENEDataSoA(
832
- layout: KLineSoALayout,
833
- period: number,
834
- deviation: number
835
- ): ENEPoint[] {
836
- const data = SharedKLineBuffer.toKLineData(layout)
837
- return calcENEData(data, period, deviation)
838
- }
839
-
840
- /**
841
- * 从 SoA 布局计算 MA 数据(验证用包装函数)
842
- * @param layout SoA 布局
843
- * @param period MA周期
844
- * @returns MA 值数组
845
- */
846
- export function calcMADataSoA(
847
- layout: KLineSoALayout,
848
- period: number
849
- ): (number | undefined)[] {
850
- const data = SharedKLineBuffer.toKLineData(layout)
851
- return calcMAData(data, period)
852
- }
853
-
854
- /**
855
- * 从 SoA 布局计算 RSI 数据(验证用包装函数)
856
- * @param layout SoA 布局
857
- * @param period RSI周期
858
- * @returns RSI 值数组
859
- */
860
- export function calcRSIDataSoA(
861
- layout: KLineSoALayout,
862
- period: number
863
- ): (number | undefined)[] {
864
- const data = SharedKLineBuffer.toKLineData(layout)
865
- return calcRSIData(data, period)
866
- }
867
-
868
- /**
869
- * 从 SoA 布局计算 CCI 数据(验证用包装函数)
870
- * @param layout SoA 布局
871
- * @param period 周期
872
- * @returns CCI 值数组
873
- */
874
- export function calcCCIDataSoA(
875
- layout: KLineSoALayout,
876
- period: number
877
- ): (number | undefined)[] {
878
- const data = SharedKLineBuffer.toKLineData(layout)
879
- return calcCCIData(data, period)
880
- }
881
-
882
- /**
883
- * 从 SoA 布局计算 STOCH 数据(验证用包装函数)
884
- * @param layout SoA 布局
885
- * @param n RSV周期
886
- * @param m K的M日移动平均周期
887
- * @returns STOCH 数据点数组
888
- */
889
- export function calcSTOCHDataSoA(
890
- layout: KLineSoALayout,
891
- n: number,
892
- m: number
893
- ): STOCHPoint[] {
894
- const data = SharedKLineBuffer.toKLineData(layout)
895
- return calcSTOCHData(data, n, m)
896
- }
897
-
898
- /**
899
- * 从 SoA 布局计算 MOM 数据(验证用包装函数)
900
- * @param layout SoA 布局
901
- * @param period 周期
902
- * @returns MOM 值数组
903
- */
904
- export function calcMOMDataSoA(
905
- layout: KLineSoALayout,
906
- period: number
907
- ): (number | undefined)[] {
908
- const data = SharedKLineBuffer.toKLineData(layout)
909
- return calcMOMData(data, period)
910
- }
911
-
912
- /**
913
- * 从 SoA 布局计算 WMSR 数据(验证用包装函数)
914
- * @param layout SoA 布局
915
- * @param period 周期
916
- * @returns WMSR 值数组
917
- */
918
- export function calcWMSRDataSoA(
919
- layout: KLineSoALayout,
920
- period: number
921
- ): (number | undefined)[] {
922
- const data = SharedKLineBuffer.toKLineData(layout)
923
- return calcWMSRData(data, period)
924
- }
925
-
926
- /**
927
- * 从 SoA 布局计算 KST 数据(验证用包装函数)
928
- * @param layout SoA 布局
929
- * @param roc1 第一个ROC周期
930
- * @param roc2 第二个ROC周期
931
- * @param roc3 第三个ROC周期
932
- * @param roc4 第四个ROC周期
933
- * @param signalPeriod 信号线周期
934
- * @returns KST 数据点数组
935
- */
936
- export function calcKSTDataSoA(
937
- layout: KLineSoALayout,
938
- roc1: number,
939
- roc2: number,
940
- roc3: number,
941
- roc4: number,
942
- signalPeriod: number
943
- ): KSTPoint[] {
944
- const data = SharedKLineBuffer.toKLineData(layout)
945
- return calcKSTData(data, roc1, roc2, roc3, roc4, signalPeriod)
946
- }
947
-
948
- /**
949
- * 从 SoA 布局计算 FASTK 数据(验证用包装函数)
950
- * @param layout SoA 布局
951
- * @param period 周期
952
- * @returns FASTK 值数组
953
- */
954
- export function calcFASTKDataSoA(
955
- layout: KLineSoALayout,
956
- period: number
957
- ): (number | undefined)[] {
958
- const data = SharedKLineBuffer.toKLineData(layout)
959
- return calcFASTKData(data, period)
960
- }
961
-
962
- /**
963
- * 从 SoA 布局计算 MACD 数据(验证用包装函数)
964
- * @param layout SoA 布局
965
- * @param fastPeriod 快线周期
966
- * @param slowPeriod 慢线周期
967
- * @param signalPeriod 信号线周期
968
- * @returns MACD 数据点数组
969
- */
970
- export function calcMACDDataSoA(
971
- layout: KLineSoALayout,
972
- fastPeriod: number,
973
- slowPeriod: number,
974
- signalPeriod: number
975
- ): MACDPoint[] {
976
- const data = SharedKLineBuffer.toKLineData(layout)
977
- return calcMACDData(data, fastPeriod, slowPeriod, signalPeriod)
978
- }
979
-
980
- // ============================================================================
981
- // ATR — Wilder's Average True Range
982
- // ============================================================================
983
-
984
- export const DEFAULT_ATR_PERIOD = 14
985
-
986
- /**
987
- * 计算 Wilder ATR。
988
- * TR(0) = H(0) - L(0)
989
- * TR(t) = max(H(t) - L(t), |H(t) - C(t-1)|, |L(t) - C(t-1)|)
990
- * ATR(period-1) = mean(TR[0..period-1])
991
- * ATR(t) = ((period-1) * ATR(t-1) + TR(t)) / period for t >= period
992
- *
993
- * @param data K 线数组
994
- * @param period 周期,需 >= 1;若 <= 0 或 data.length < period,返回全 undefined
995
- */
996
- export function calcATRData(data: KLineData[], period: number): (number | undefined)[] {
997
- const n = data.length
998
- const result: (number | undefined)[] = new Array(n).fill(undefined)
999
- if (n === 0 || period <= 0) return result
1000
-
1001
- if (period === 1) {
1002
- const first = data[0]!
1003
- result[0] = first.high - first.low
1004
- let prevClose = first.close
1005
- for (let i = 1; i < n; i++) {
1006
- const cur = data[i]!
1007
- const tr = Math.max(
1008
- cur.high - cur.low,
1009
- Math.abs(cur.high - prevClose),
1010
- Math.abs(cur.low - prevClose),
1011
- )
1012
- result[i] = tr
1013
- prevClose = cur.close
1014
- }
1015
- return result
1016
- }
1017
-
1018
- if (n < period) return result
1019
-
1020
- const first = data[0]!
1021
- let sumTR = first.high - first.low
1022
- let prevClose = first.close
1023
-
1024
- for (let i = 1; i < period; i++) {
1025
- const cur = data[i]!
1026
- sumTR += Math.max(
1027
- cur.high - cur.low,
1028
- Math.abs(cur.high - prevClose),
1029
- Math.abs(cur.low - prevClose),
1030
- )
1031
- prevClose = cur.close
1032
- }
1033
-
1034
- let atr = sumTR / period
1035
- result[period - 1] = atr
1036
-
1037
- const periodMinusOne = period - 1
1038
- for (let i = period; i < n; i++) {
1039
- const cur = data[i]!
1040
- const tr = Math.max(
1041
- cur.high - cur.low,
1042
- Math.abs(cur.high - prevClose),
1043
- Math.abs(cur.low - prevClose),
1044
- )
1045
- atr = (periodMinusOne * atr + tr) / period
1046
- result[i] = atr
1047
- prevClose = cur.close
1048
- }
1049
-
1050
- return result
1051
- }
1052
-
1053
- /**
1054
- * 从 SoA 布局计算 ATR(包装函数,对齐其他指标的 SoA 入口)
1055
- */
1056
- export function calcATRDataSoA(
1057
- layout: KLineSoALayout,
1058
- period: number,
1059
- ): (number | undefined)[] {
1060
- const data = SharedKLineBuffer.toKLineData(layout)
1061
- return calcATRData(data, period)
1062
- }
1063
-
1064
- // ============================================================================
1065
- // WMA — Weighted Moving Average (linear weights)
1066
- // 权重: w_i = i (i=1..period),分母 = period*(period+1)/2
1067
- // 滞后 = (period-1)/3 (相比 SMA 更快响应)
1068
- // ============================================================================
1069
-
1070
- export const DEFAULT_WMA_PERIOD = 9
1071
-
1072
- function _computeWMAOnNumbers(values: (number | undefined)[], period: number): (number | undefined)[] {
1073
- const n = values.length
1074
- const result: (number | undefined)[] = new Array(n).fill(undefined)
1075
- if (n === 0 || period <= 0 || n < period) return result
1076
-
1077
- const denom = (period * (period + 1)) / 2
1078
-
1079
- for (let t = period - 1; t < n; t++) {
1080
- let sw = 0
1081
- let valid = true
1082
- for (let k = 0; k < period; k++) {
1083
- const v = values[t - period + 1 + k]
1084
- if (v === undefined) {
1085
- valid = false
1086
- break
1087
- }
1088
- sw += (k + 1) * v
1089
- }
1090
- if (valid) result[t] = sw / denom
1091
- }
1092
- return result
1093
- }
1094
-
1095
- export function calcWMAData(data: KLineData[], period: number): (number | undefined)[] {
1096
- if (data.length === 0 || period <= 0) {
1097
- return new Array(data.length).fill(undefined)
1098
- }
1099
- const closes = new Array<number | undefined>(data.length)
1100
- for (let i = 0; i < data.length; i++) closes[i] = data[i]!.close
1101
- return _computeWMAOnNumbers(closes, period)
1102
- }
1103
-
1104
- export function calcWMADataSoA(layout: KLineSoALayout, period: number): (number | undefined)[] {
1105
- const data = SharedKLineBuffer.toKLineData(layout)
1106
- return calcWMAData(data, period)
1107
- }
1108
-
1109
- // ============================================================================
1110
- // EMA helper(DEMA / TEMA 复用,沿用 EXPMA 的 first-close seed 习惯)
1111
- // alpha = 2 / (period + 1)
1112
- // ============================================================================
1113
-
1114
- function _computeEMASeries(values: (number | undefined)[], period: number): (number | undefined)[] {
1115
- const n = values.length
1116
- const result: (number | undefined)[] = new Array(n).fill(undefined)
1117
- if (n === 0 || period <= 0) return result
1118
-
1119
- const alpha = 2 / (period + 1)
1120
-
1121
- let i = 0
1122
- while (i < n && values[i] === undefined) i++
1123
- if (i >= n) return result
1124
-
1125
- let ema = values[i]!
1126
- result[i] = ema
1127
- for (let t = i + 1; t < n; t++) {
1128
- const v = values[t]
1129
- if (v === undefined) continue
1130
- ema = v * alpha + ema * (1 - alpha)
1131
- result[t] = ema
1132
- }
1133
- return result
1134
- }
1135
-
1136
- // ============================================================================
1137
- // DEMA — Double Exponential Moving Average
1138
- // 公式: DEMA(t) = 2*EMA(t) - EMA(EMA)(t)
1139
- // 性质: 对线性输入零滞后(稳态),warmup ~ 2*(period-1)
1140
- // ============================================================================
1141
-
1142
- export const DEFAULT_DEMA_PERIOD = 20
1143
-
1144
- export function calcDEMAData(data: KLineData[], period: number): (number | undefined)[] {
1145
- const n = data.length
1146
- const result: (number | undefined)[] = new Array(n).fill(undefined)
1147
- if (n === 0 || period <= 0) return result
1148
-
1149
- const closes = new Array<number | undefined>(n)
1150
- for (let i = 0; i < n; i++) closes[i] = data[i]!.close
1151
-
1152
- const ema1 = _computeEMASeries(closes, period)
1153
- const ema2 = _computeEMASeries(ema1, period)
1154
-
1155
- for (let i = 0; i < n; i++) {
1156
- const e1 = ema1[i]
1157
- const e2 = ema2[i]
1158
- if (e1 === undefined || e2 === undefined) continue
1159
- result[i] = 2 * e1 - e2
1160
- }
1161
- return result
1162
- }
1163
-
1164
- export function calcDEMADataSoA(layout: KLineSoALayout, period: number): (number | undefined)[] {
1165
- const data = SharedKLineBuffer.toKLineData(layout)
1166
- return calcDEMAData(data, period)
1167
- }
1168
-
1169
- // ============================================================================
1170
- // TEMA — Triple Exponential Moving Average
1171
- // 公式: TEMA(t) = 3*EMA(t) - 3*EMA(EMA)(t) + EMA(EMA(EMA))(t)
1172
- // 性质: 对二次多项式输入零滞后(稳态),warmup ~ 3*(period-1)
1173
- // ============================================================================
1174
-
1175
- export const DEFAULT_TEMA_PERIOD = 20
1176
-
1177
- export function calcTEMAData(data: KLineData[], period: number): (number | undefined)[] {
1178
- const n = data.length
1179
- const result: (number | undefined)[] = new Array(n).fill(undefined)
1180
- if (n === 0 || period <= 0) return result
1181
-
1182
- const closes = new Array<number | undefined>(n)
1183
- for (let i = 0; i < n; i++) closes[i] = data[i]!.close
1184
-
1185
- const ema1 = _computeEMASeries(closes, period)
1186
- const ema2 = _computeEMASeries(ema1, period)
1187
- const ema3 = _computeEMASeries(ema2, period)
1188
-
1189
- for (let i = 0; i < n; i++) {
1190
- const e1 = ema1[i]
1191
- const e2 = ema2[i]
1192
- const e3 = ema3[i]
1193
- if (e1 === undefined || e2 === undefined || e3 === undefined) continue
1194
- result[i] = 3 * e1 - 3 * e2 + e3
1195
- }
1196
- return result
1197
- }
1198
-
1199
- export function calcTEMADataSoA(layout: KLineSoALayout, period: number): (number | undefined)[] {
1200
- const data = SharedKLineBuffer.toKLineData(layout)
1201
- return calcTEMAData(data, period)
1202
- }
1203
-
1204
- // ============================================================================
1205
- // HMA — Hull Moving Average
1206
- // 公式: HMA(n) = WMA( 2*WMA(close, n/2) - WMA(close, n), sqrt(n) )
1207
- // 性质: 平滑性高于 WMA,滞后远低于同期 SMA
1208
- // warmup ≈ period - 1 + round(sqrt(period)) - 1
1209
- // ============================================================================
1210
-
1211
- export const DEFAULT_HMA_PERIOD = 9
1212
-
1213
- export function calcHMAData(data: KLineData[], period: number): (number | undefined)[] {
1214
- const n = data.length
1215
- const result: (number | undefined)[] = new Array(n).fill(undefined)
1216
- if (n === 0 || period <= 0) return result
1217
-
1218
- const closes = new Array<number | undefined>(n)
1219
- for (let i = 0; i < n; i++) closes[i] = data[i]!.close
1220
-
1221
- const halfPeriod = Math.max(1, Math.floor(period / 2))
1222
- const sqrtPeriod = Math.max(1, Math.round(Math.sqrt(period)))
1223
-
1224
- const wmaHalf = _computeWMAOnNumbers(closes, halfPeriod)
1225
- const wmaFull = _computeWMAOnNumbers(closes, period)
1226
-
1227
- const raw: (number | undefined)[] = new Array(n).fill(undefined)
1228
- for (let i = 0; i < n; i++) {
1229
- const h = wmaHalf[i]
1230
- const f = wmaFull[i]
1231
- if (h === undefined || f === undefined) continue
1232
- raw[i] = 2 * h - f
1233
- }
1234
- return _computeWMAOnNumbers(raw, sqrtPeriod)
1235
- }
1236
-
1237
- export function calcHMADataSoA(layout: KLineSoALayout, period: number): (number | undefined)[] {
1238
- const data = SharedKLineBuffer.toKLineData(layout)
1239
- return calcHMAData(data, period)
1240
- }
1241
-
1242
- // ============================================================================
1243
- // KAMA — Kaufman's Adaptive Moving Average
1244
- // 自适应:在趋势强时跟得紧(接近 fast EMA),在震荡时跟得慢(接近 slow EMA)。
1245
- // ER (efficiency ratio) = |close[t] - close[t-n]| / sum(|close[i] - close[i-1]|, i=t-n+1..t)
1246
- // SC = (ER * (2/(fast+1) - 2/(slow+1)) + 2/(slow+1))^2
1247
- // KAMA(t) = KAMA(t-1) + SC * (close[t] - KAMA(t-1))
1248
- // 种子 KAMA(n-1) = close[n-1](或 SMA(n);这里采用 close 种子以保持与项目内 EMA 系列一致)
1249
- // ============================================================================
1250
-
1251
- export const DEFAULT_KAMA_PERIOD = 10
1252
- export const DEFAULT_KAMA_FAST_PERIOD = 2
1253
- export const DEFAULT_KAMA_SLOW_PERIOD = 30
1254
-
1255
- export function calcKAMAData(
1256
- data: KLineData[],
1257
- period: number,
1258
- fastPeriod: number,
1259
- slowPeriod: number,
1260
- ): (number | undefined)[] {
1261
- const n = data.length
1262
- const result: (number | undefined)[] = new Array(n).fill(undefined)
1263
- if (n === 0 || period <= 0 || fastPeriod <= 0 || slowPeriod <= 0 || n <= period) return result
1264
-
1265
- const fastSC = 2 / (fastPeriod + 1)
1266
- const slowSC = 2 / (slowPeriod + 1)
1267
- const scRange = fastSC - slowSC
1268
-
1269
- // 维护滚动求和:sum(|close[i] - close[i-1]|, i=t-period+1..t)
1270
- let volSum = 0
1271
- for (let i = 1; i <= period; i++) {
1272
- volSum += Math.abs(data[i]!.close - data[i - 1]!.close)
1273
- }
1274
-
1275
- let kama = data[period - 1]!.close
1276
- result[period - 1] = kama
1277
-
1278
- for (let t = period; t < n; t++) {
1279
- const close = data[t]!.close
1280
- const closeNPeriodsAgo = data[t - period]!.close
1281
- const direction = Math.abs(close - closeNPeriodsAgo)
1282
-
1283
- const er = volSum > 0 ? direction / volSum : 0
1284
- const sc = (er * scRange + slowSC) ** 2
1285
-
1286
- kama = kama + sc * (close - kama)
1287
- result[t] = kama
1288
-
1289
- // 滚动 volSum:减去最旧的 |close[t-period+1] - close[t-period]|,加上最新的 |close[t+1] - close[t]|
1290
- if (t < n - 1) {
1291
- volSum -= Math.abs(data[t - period + 1]!.close - data[t - period]!.close)
1292
- volSum += Math.abs(data[t + 1]!.close - data[t]!.close)
1293
- }
1294
- }
1295
-
1296
- return result
1297
- }
1298
-
1299
- export function calcKAMADataSoA(
1300
- layout: KLineSoALayout,
1301
- period: number,
1302
- fastPeriod: number,
1303
- slowPeriod: number,
1304
- ): (number | undefined)[] {
1305
- const data = SharedKLineBuffer.toKLineData(layout)
1306
- return calcKAMAData(data, period, fastPeriod, slowPeriod)
1307
- }
1308
-
1309
- // ============================================================================
1310
- // SAR — Parabolic Stop and Reverse
1311
- // 经典 Wilder 公式:SAR(t+1) = SAR(t) + AF * (EP - SAR(t)),AF 在每次创出新极端时 +step(上限 maxStep)
1312
- // 趋势翻转条件:上升趋势中 SAR 越过 low(或反之)
1313
- // 种子:从 bar[1] 起,初始 trend=up,SAR=low[0],EP=high[0],AF=step
1314
- // 返回每根 K 线对应的 SAR 点(带方向)
1315
- // ============================================================================
1316
-
1317
- export interface SARPoint {
1318
- value: number
1319
- trend: 'up' | 'down'
1320
- }
1321
-
1322
- export const DEFAULT_SAR_STEP = 0.02
1323
- export const DEFAULT_SAR_MAX_STEP = 0.2
1324
-
1325
- export function calcSARData(
1326
- data: KLineData[],
1327
- step: number,
1328
- maxStep: number,
1329
- ): (SARPoint | undefined)[] {
1330
- const n = data.length
1331
- const result: (SARPoint | undefined)[] = new Array(n).fill(undefined)
1332
- if (n < 2 || step <= 0 || maxStep <= 0) return result
1333
-
1334
- let trend: 'up' | 'down' = data[1]!.close >= data[0]!.close ? 'up' : 'down'
1335
- let sar = trend === 'up' ? data[0]!.low : data[0]!.high
1336
- let ep = trend === 'up' ? data[0]!.high : data[0]!.low
1337
- let af = step
1338
-
1339
- result[0] = { value: sar, trend }
1340
-
1341
- for (let t = 1; t < n; t++) {
1342
- const bar = data[t]!
1343
- // 先按当前趋势推进 SAR
1344
- sar = sar + af * (ep - sar)
1345
-
1346
- // 边界约束:SAR 不能穿透前两根 K 线的极端
1347
- if (trend === 'up') {
1348
- const cap1 = data[t - 1]!.low
1349
- const cap2 = t >= 2 ? data[t - 2]!.low : cap1
1350
- sar = Math.min(sar, cap1, cap2)
1351
- } else {
1352
- const cap1 = data[t - 1]!.high
1353
- const cap2 = t >= 2 ? data[t - 2]!.high : cap1
1354
- sar = Math.max(sar, cap1, cap2)
1355
- }
1356
-
1357
- // 检测翻转
1358
- if (trend === 'up' && bar.low < sar) {
1359
- trend = 'down'
1360
- sar = ep
1361
- ep = bar.low
1362
- af = step
1363
- } else if (trend === 'down' && bar.high > sar) {
1364
- trend = 'up'
1365
- sar = ep
1366
- ep = bar.high
1367
- af = step
1368
- } else {
1369
- // 同趋势:更新 EP / AF
1370
- if (trend === 'up' && bar.high > ep) {
1371
- ep = bar.high
1372
- af = Math.min(af + step, maxStep)
1373
- } else if (trend === 'down' && bar.low < ep) {
1374
- ep = bar.low
1375
- af = Math.min(af + step, maxStep)
1376
- }
1377
- }
1378
-
1379
- result[t] = { value: sar, trend }
1380
- }
1381
-
1382
- return result
1383
- }
1384
-
1385
- export function calcSARDataSoA(
1386
- layout: KLineSoALayout,
1387
- step: number,
1388
- maxStep: number,
1389
- ): (SARPoint | undefined)[] {
1390
- const data = SharedKLineBuffer.toKLineData(layout)
1391
- return calcSARData(data, step, maxStep)
1392
- }
1393
-
1394
- // ============================================================================
1395
- // SuperTrend — ATR-based trend-following stop/band
1396
- // ============================================================================
1397
-
1398
- export interface SuperTrendPoint {
1399
- value: number
1400
- trend: 'up' | 'down'
1401
- }
1402
-
1403
- export const DEFAULT_SUPERTREND_ATR_PERIOD = 10
1404
- export const DEFAULT_SUPERTREND_MULTIPLIER = 3
1405
-
1406
- export function calcSuperTrendData(
1407
- data: KLineData[],
1408
- atrPeriod: number,
1409
- multiplier: number,
1410
- ): (SuperTrendPoint | undefined)[] {
1411
- const n = data.length
1412
- const result: (SuperTrendPoint | undefined)[] = new Array(n).fill(undefined)
1413
- if (n === 0 || atrPeriod <= 0 || multiplier <= 0) return result
1414
-
1415
- const atr = calcATRData(data, atrPeriod)
1416
-
1417
- let trend: 'up' | 'down' = 'up'
1418
- let prevUpper = Infinity
1419
- let prevLower = -Infinity
1420
-
1421
- for (let t = 0; t < n; t++) {
1422
- const bar = data[t]!
1423
- const a = atr[t]
1424
- if (a === undefined) continue
1425
-
1426
- const hl2 = (bar.high + bar.low) / 2
1427
- const upperBasic = hl2 + multiplier * a
1428
- const lowerBasic = hl2 - multiplier * a
1429
-
1430
- // Smoothing: keep the previous band unless price has broken through it
1431
- const prevClose = t > 0 ? data[t - 1]!.close : bar.close
1432
- const upper = (upperBasic < prevUpper || prevClose > prevUpper) ? upperBasic : prevUpper
1433
- const lower = (lowerBasic > prevLower || prevClose < prevLower) ? lowerBasic : prevLower
1434
-
1435
- // Trend update
1436
- if (trend === 'up' && bar.close < lower) {
1437
- trend = 'down'
1438
- } else if (trend === 'down' && bar.close > upper) {
1439
- trend = 'up'
1440
- }
1441
-
1442
- result[t] = { value: trend === 'up' ? lower : upper, trend }
1443
-
1444
- prevUpper = upper
1445
- prevLower = lower
1446
- }
1447
-
1448
- return result
1449
- }
1450
-
1451
- export function calcSuperTrendDataSoA(
1452
- layout: KLineSoALayout,
1453
- atrPeriod: number,
1454
- multiplier: number,
1455
- ): (SuperTrendPoint | undefined)[] {
1456
- const data = SharedKLineBuffer.toKLineData(layout)
1457
- return calcSuperTrendData(data, atrPeriod, multiplier)
1458
- }
1459
-
1460
- // ============================================================================
1461
- // Keltner Channel — EMA ± multiplier × ATR
1462
- // ============================================================================
1463
-
1464
- export interface KeltnerPoint {
1465
- upper: number
1466
- middle: number
1467
- lower: number
1468
- }
1469
-
1470
- export const DEFAULT_KELTNER_EMA_PERIOD = 20
1471
- export const DEFAULT_KELTNER_ATR_PERIOD = 10
1472
- export const DEFAULT_KELTNER_MULTIPLIER = 2
1473
-
1474
- export function calcKeltnerData(
1475
- data: KLineData[],
1476
- emaPeriod: number,
1477
- atrPeriod: number,
1478
- multiplier: number,
1479
- ): (KeltnerPoint | undefined)[] {
1480
- const n = data.length
1481
- const result: (KeltnerPoint | undefined)[] = new Array(n).fill(undefined)
1482
- if (n === 0 || emaPeriod <= 0 || atrPeriod <= 0) return result
1483
-
1484
- const closes = new Array<number | undefined>(n)
1485
- for (let i = 0; i < n; i++) closes[i] = data[i]!.close
1486
-
1487
- const ema = _computeEMASeries(closes, emaPeriod)
1488
- const atr = calcATRData(data, atrPeriod)
1489
-
1490
- for (let t = 0; t < n; t++) {
1491
- const m = ema[t]
1492
- const a = atr[t]
1493
- if (m === undefined || a === undefined) continue
1494
- result[t] = {
1495
- upper: m + multiplier * a,
1496
- middle: m,
1497
- lower: m - multiplier * a,
1498
- }
1499
- }
1500
- return result
1501
- }
1502
-
1503
- export function calcKeltnerDataSoA(
1504
- layout: KLineSoALayout,
1505
- emaPeriod: number,
1506
- atrPeriod: number,
1507
- multiplier: number,
1508
- ): (KeltnerPoint | undefined)[] {
1509
- const data = SharedKLineBuffer.toKLineData(layout)
1510
- return calcKeltnerData(data, emaPeriod, atrPeriod, multiplier)
1511
- }
1512
-
1513
- // ============================================================================
1514
- // Donchian Channel — rolling max(high) / min(low) over period
1515
- // ============================================================================
1516
-
1517
- export interface DonchianPoint {
1518
- upper: number
1519
- middle: number
1520
- lower: number
1521
- }
1522
-
1523
- export const DEFAULT_DONCHIAN_PERIOD = 20
1524
-
1525
- export function calcDonchianData(
1526
- data: KLineData[],
1527
- period: number,
1528
- ): (DonchianPoint | undefined)[] {
1529
- const n = data.length
1530
- const result: (DonchianPoint | undefined)[] = new Array(n).fill(undefined)
1531
- if (n === 0 || period <= 0 || n < period) return result
1532
-
1533
- for (let t = period - 1; t < n; t++) {
1534
- let hi = -Infinity
1535
- let lo = Infinity
1536
- for (let k = 0; k < period; k++) {
1537
- const bar = data[t - k]!
1538
- if (bar.high > hi) hi = bar.high
1539
- if (bar.low < lo) lo = bar.low
1540
- }
1541
- result[t] = { upper: hi, middle: (hi + lo) / 2, lower: lo }
1542
- }
1543
- return result
1544
- }
1545
-
1546
- export function calcDonchianDataSoA(
1547
- layout: KLineSoALayout,
1548
- period: number,
1549
- ): (DonchianPoint | undefined)[] {
1550
- const data = SharedKLineBuffer.toKLineData(layout)
1551
- return calcDonchianData(data, period)
1552
- }
1553
-
1554
- // ============================================================================
1555
- // Ichimoku Kinko Hyo — 一目均衡表
1556
- // 5 线 + 云图(spanA/B 前置位移构成):
1557
- // tenkan(t) = (max(high[t-tenkanPeriod+1..t]) + min(low[t-tenkanPeriod+1..t])) / 2
1558
- // kijun(t) = 同公式但用 kijunPeriod
1559
- // spanA(t) = (tenkan(t-displacement) + kijun(t-displacement)) / 2 ← 前置 displacement
1560
- // spanB(t) = 用 spanBPeriod 计算后再前置 displacement
1561
- // chikou(t) = close(t+displacement) ← 后置 displacement
1562
- // 注:不做未来云的延伸(输出长度 = data.length;最后 displacement 根没 spanA/B;前 displacement 根没 chikou)
1563
- // ============================================================================
1564
-
1565
- export interface IchimokuPoint {
1566
- tenkan?: number
1567
- kijun?: number
1568
- spanA?: number
1569
- spanB?: number
1570
- chikou?: number
1571
- }
1572
-
1573
- export const DEFAULT_ICHIMOKU_TENKAN = 9
1574
- export const DEFAULT_ICHIMOKU_KIJUN = 26
1575
- export const DEFAULT_ICHIMOKU_SPAN_B = 52
1576
- export const DEFAULT_ICHIMOKU_DISPLACEMENT = 26
1577
-
1578
- function _rollingMidline(data: KLineData[], period: number): (number | undefined)[] {
1579
- const n = data.length
1580
- const result: (number | undefined)[] = new Array(n).fill(undefined)
1581
- if (n < period || period <= 0) return result
1582
- for (let t = period - 1; t < n; t++) {
1583
- let hi = -Infinity
1584
- let lo = Infinity
1585
- for (let k = 0; k < period; k++) {
1586
- const bar = data[t - k]!
1587
- if (bar.high > hi) hi = bar.high
1588
- if (bar.low < lo) lo = bar.low
1589
- }
1590
- result[t] = (hi + lo) / 2
1591
- }
1592
- return result
1593
- }
1594
-
1595
- export function calcIchimokuData(
1596
- data: KLineData[],
1597
- tenkanPeriod: number,
1598
- kijunPeriod: number,
1599
- spanBPeriod: number,
1600
- displacement: number,
1601
- ): (IchimokuPoint | undefined)[] {
1602
- const n = data.length
1603
- const result: (IchimokuPoint | undefined)[] = new Array(n).fill(undefined)
1604
- if (n === 0 || tenkanPeriod <= 0 || kijunPeriod <= 0 || spanBPeriod <= 0) return result
1605
-
1606
- const tenkan = _rollingMidline(data, tenkanPeriod)
1607
- const kijun = _rollingMidline(data, kijunPeriod)
1608
- const spanBSource = _rollingMidline(data, spanBPeriod)
1609
-
1610
- for (let t = 0; t < n; t++) {
1611
- const point: IchimokuPoint = {}
1612
- if (tenkan[t] !== undefined) point.tenkan = tenkan[t]
1613
- if (kijun[t] !== undefined) point.kijun = kijun[t]
1614
-
1615
- // spanA / spanB 由 displacement 根之前的值填到当前槽位
1616
- const src = t - displacement
1617
- if (src >= 0) {
1618
- if (tenkan[src] !== undefined && kijun[src] !== undefined) {
1619
- point.spanA = (tenkan[src]! + kijun[src]!) / 2
1620
- }
1621
- if (spanBSource[src] !== undefined) {
1622
- point.spanB = spanBSource[src]
1623
- }
1624
- }
1625
-
1626
- // chikou:当前 close 后置 displacement 根(即存到 t - displacement 槽位上 close[t])
1627
- // 这里改成:存当前槽位的 chikou = close[t + displacement],需 future 数据;不可用时 undefined
1628
- const future = t + displacement
1629
- if (future < n) point.chikou = data[future]!.close
1630
-
1631
- result[t] = point
1632
- }
1633
-
1634
- return result
1635
- }
1636
-
1637
- export function calcIchimokuDataSoA(
1638
- layout: KLineSoALayout,
1639
- tenkanPeriod: number,
1640
- kijunPeriod: number,
1641
- spanBPeriod: number,
1642
- displacement: number,
1643
- ): (IchimokuPoint | undefined)[] {
1644
- const data = SharedKLineBuffer.toKLineData(layout)
1645
- return calcIchimokuData(data, tenkanPeriod, kijunPeriod, spanBPeriod, displacement)
1646
- }
1647
-
1648
- // ============================================================================
1649
- // ROC — Rate of Change
1650
- // ROC(t) = (close[t] - close[t-period]) / close[t-period] * 100
1651
- // ============================================================================
1652
-
1653
- export const DEFAULT_ROC_PERIOD = 12
1654
-
1655
- export function calcROCData(data: KLineData[], period: number): (number | undefined)[] {
1656
- const n = data.length
1657
- const result: (number | undefined)[] = new Array(n).fill(undefined)
1658
- if (n === 0 || period <= 0) return result
1659
- for (let t = period; t < n; t++) {
1660
- const prev = data[t - period]!.close
1661
- if (prev === 0) continue
1662
- result[t] = (data[t]!.close - prev) / prev * 100
1663
- }
1664
- return result
1665
- }
1666
-
1667
- export function calcROCDataSoA(layout: KLineSoALayout, period: number): (number | undefined)[] {
1668
- const data = SharedKLineBuffer.toKLineData(layout)
1669
- return calcROCData(data, period)
1670
- }
1671
-
1672
- // ============================================================================
1673
- // TRIX — Triple Exponential Smoothing Oscillator
1674
- // EMA3 = EMA(EMA(EMA(close, p), p), p)
1675
- // TRIX(t) = (EMA3[t] - EMA3[t-1]) / EMA3[t-1] * 100
1676
- // Signal(t) = EMA(TRIX, signalPeriod) —— 配合金叉/死叉
1677
- // ============================================================================
1678
-
1679
- export interface TRIXResult {
1680
- series: (number | undefined)[]
1681
- signalSeries: (number | undefined)[]
1682
- }
1683
-
1684
- export const DEFAULT_TRIX_PERIOD = 15
1685
- export const DEFAULT_TRIX_SIGNAL_PERIOD = 9
1686
-
1687
- export function calcTRIXData(
1688
- data: KLineData[],
1689
- period: number,
1690
- signalPeriod: number,
1691
- ): TRIXResult {
1692
- const n = data.length
1693
- const series: (number | undefined)[] = new Array(n).fill(undefined)
1694
- const signalSeries: (number | undefined)[] = new Array(n).fill(undefined)
1695
- if (n === 0 || period <= 0) return { series, signalSeries }
1696
-
1697
- const closes = new Array<number | undefined>(n)
1698
- for (let i = 0; i < n; i++) closes[i] = data[i]!.close
1699
-
1700
- const ema1 = _computeEMASeries(closes, period)
1701
- const ema2 = _computeEMASeries(ema1, period)
1702
- const ema3 = _computeEMASeries(ema2, period)
1703
-
1704
- for (let t = 1; t < n; t++) {
1705
- const cur = ema3[t]
1706
- const prev = ema3[t - 1]
1707
- if (cur === undefined || prev === undefined || prev === 0) continue
1708
- series[t] = (cur - prev) / prev * 100
1709
- }
1710
-
1711
- if (signalPeriod > 0) {
1712
- const smoothed = _computeEMASeries(series, signalPeriod)
1713
- for (let i = 0; i < n; i++) signalSeries[i] = smoothed[i]
1714
- }
1715
-
1716
- return { series, signalSeries }
1717
- }
1718
-
1719
- export function calcTRIXDataSoA(
1720
- layout: KLineSoALayout,
1721
- period: number,
1722
- signalPeriod: number,
1723
- ): TRIXResult {
1724
- const data = SharedKLineBuffer.toKLineData(layout)
1725
- return calcTRIXData(data, period, signalPeriod)
1726
- }
1727
-
1728
- // ============================================================================
1729
- // HV — Historical Volatility (close-to-close log returns)
1730
- // HV(t) = stdDev(log(close[i]/close[i-1]), i=t-period+1..t) * sqrt(annualization)
1731
- // 输出年化波动率(百分比形式 × 100)
1732
- // ============================================================================
1733
-
1734
- export const DEFAULT_HV_PERIOD = 20
1735
- export const DEFAULT_HV_ANNUALIZATION = 252
1736
-
1737
- export function calcHVData(
1738
- data: KLineData[],
1739
- period: number,
1740
- annualizationFactor: number,
1741
- ): (number | undefined)[] {
1742
- const n = data.length
1743
- const result: (number | undefined)[] = new Array(n).fill(undefined)
1744
- if (n < 2 || period <= 0 || annualizationFactor <= 0) return result
1745
-
1746
- const logReturns: number[] = new Array(n)
1747
- logReturns[0] = 0
1748
- for (let t = 1; t < n; t++) {
1749
- const prev = data[t - 1]!.close
1750
- const cur = data[t]!.close
1751
- logReturns[t] = (prev > 0 && cur > 0) ? Math.log(cur / prev) : 0
1752
- }
1753
-
1754
- const annScale = Math.sqrt(annualizationFactor)
1755
- for (let t = period; t < n; t++) {
1756
- let sum = 0
1757
- for (let k = 1; k <= period; k++) sum += logReturns[t - period + k]!
1758
- const mean = sum / period
1759
- let varSum = 0
1760
- for (let k = 1; k <= period; k++) {
1761
- const diff = logReturns[t - period + k]! - mean
1762
- varSum += diff * diff
1763
- }
1764
- const std = Math.sqrt(varSum / (period - 1 > 0 ? period - 1 : 1))
1765
- result[t] = std * annScale * 100
1766
- }
1767
-
1768
- return result
1769
- }
1770
-
1771
- export function calcHVDataSoA(
1772
- layout: KLineSoALayout,
1773
- period: number,
1774
- annualizationFactor: number,
1775
- ): (number | undefined)[] {
1776
- const data = SharedKLineBuffer.toKLineData(layout)
1777
- return calcHVData(data, period, annualizationFactor)
1778
- }
1779
-
1780
- // ============================================================================
1781
- // Parkinson Volatility — high-low range volatility
1782
- // PV(t) = sqrt( (1/(4*ln(2))) * mean(ln(high[i]/low[i])^2) * annualization ) * 100
1783
- // ============================================================================
1784
-
1785
- export const DEFAULT_PARKINSON_PERIOD = 20
1786
- export const DEFAULT_PARKINSON_ANNUALIZATION = 252
1787
-
1788
- export function calcParkinsonData(
1789
- data: KLineData[],
1790
- period: number,
1791
- annualizationFactor: number,
1792
- ): (number | undefined)[] {
1793
- const n = data.length
1794
- const result: (number | undefined)[] = new Array(n).fill(undefined)
1795
- if (n === 0 || period <= 0 || annualizationFactor <= 0 || n < period) return result
1796
-
1797
- const factor = 1 / (4 * Math.log(2))
1798
- const annScale = Math.sqrt(annualizationFactor)
1799
-
1800
- const hlLogSq: number[] = new Array(n)
1801
- for (let i = 0; i < n; i++) {
1802
- const bar = data[i]!
1803
- if (bar.high > 0 && bar.low > 0) {
1804
- const ln = Math.log(bar.high / bar.low)
1805
- hlLogSq[i] = ln * ln
1806
- } else {
1807
- hlLogSq[i] = 0
1808
- }
1809
- }
1810
-
1811
- for (let t = period - 1; t < n; t++) {
1812
- let sum = 0
1813
- for (let k = 0; k < period; k++) sum += hlLogSq[t - k]!
1814
- const mean = sum / period
1815
- result[t] = Math.sqrt(factor * mean) * annScale * 100
1816
- }
1817
-
1818
- return result
1819
- }
1820
-
1821
- export function calcParkinsonDataSoA(
1822
- layout: KLineSoALayout,
1823
- period: number,
1824
- annualizationFactor: number,
1825
- ): (number | undefined)[] {
1826
- const data = SharedKLineBuffer.toKLineData(layout)
1827
- return calcParkinsonData(data, period, annualizationFactor)
1828
- }
1829
-
1830
- // ============================================================================
1831
- // Chaikin Volatility — EMA(high-low) 的 ROC
1832
- // ChaikinVol(t) = (EMA(H-L, p)[t] - EMA(H-L, p)[t-rocPeriod]) / EMA(H-L, p)[t-rocPeriod] * 100
1833
- // ============================================================================
1834
-
1835
- export const DEFAULT_CHAIKIN_VOL_EMA_PERIOD = 10
1836
- export const DEFAULT_CHAIKIN_VOL_ROC_PERIOD = 10
1837
-
1838
- export function calcChaikinVolData(
1839
- data: KLineData[],
1840
- emaPeriod: number,
1841
- rocPeriod: number,
1842
- ): (number | undefined)[] {
1843
- const n = data.length
1844
- const result: (number | undefined)[] = new Array(n).fill(undefined)
1845
- if (n === 0 || emaPeriod <= 0 || rocPeriod <= 0) return result
1846
-
1847
- const hl: (number | undefined)[] = new Array(n)
1848
- for (let i = 0; i < n; i++) hl[i] = data[i]!.high - data[i]!.low
1849
-
1850
- const emaSeries = _computeEMASeries(hl, emaPeriod)
1851
-
1852
- for (let t = rocPeriod; t < n; t++) {
1853
- const cur = emaSeries[t]
1854
- const prev = emaSeries[t - rocPeriod]
1855
- if (cur === undefined || prev === undefined || prev === 0) continue
1856
- result[t] = (cur - prev) / prev * 100
1857
- }
1858
-
1859
- return result
1860
- }
1861
-
1862
- export function calcChaikinVolDataSoA(
1863
- layout: KLineSoALayout,
1864
- emaPeriod: number,
1865
- rocPeriod: number,
1866
- ): (number | undefined)[] {
1867
- const data = SharedKLineBuffer.toKLineData(layout)
1868
- return calcChaikinVolData(data, emaPeriod, rocPeriod)
1869
- }
1870
-
1871
- // ============================================================================
1872
- // VMA — Volume Moving Average (SMA of volume)
1873
- // ============================================================================
1874
-
1875
- export const DEFAULT_VMA_PERIOD = 5
1876
-
1877
- export function calcVMAData(data: KLineData[], period: number): (number | undefined)[] {
1878
- const n = data.length
1879
- const result: (number | undefined)[] = new Array(n).fill(undefined)
1880
- if (n === 0 || period <= 0 || n < period) return result
1881
- let sum = 0
1882
- for (let i = 0; i < period; i++) sum += data[i]!.volume ?? 0
1883
- result[period - 1] = sum / period
1884
- for (let t = period; t < n; t++) {
1885
- sum += (data[t]!.volume ?? 0) - (data[t - period]!.volume ?? 0)
1886
- result[t] = sum / period
1887
- }
1888
- return result
1889
- }
1890
-
1891
- export function calcVMADataSoA(layout: KLineSoALayout, period: number): (number | undefined)[] {
1892
- const data = SharedKLineBuffer.toKLineData(layout)
1893
- return calcVMAData(data, period)
1894
- }
1895
-
1896
- // ============================================================================
1897
- // OBV — On Balance Volume (cumulative)
1898
- // close[t] > close[t-1] → OBV += volume[t]
1899
- // close[t] < close[t-1] → OBV -= volume[t]
1900
- // else → OBV unchanged
1901
- // ============================================================================
1902
-
1903
- export function calcOBVData(data: KLineData[]): (number | undefined)[] {
1904
- const n = data.length
1905
- const result: (number | undefined)[] = new Array(n).fill(undefined)
1906
- if (n === 0) return result
1907
- let obv = 0
1908
- result[0] = 0
1909
- for (let t = 1; t < n; t++) {
1910
- const cur = data[t]!
1911
- const prev = data[t - 1]!
1912
- if (cur.close > prev.close) obv += cur.volume ?? 0
1913
- else if (cur.close < prev.close) obv -= cur.volume ?? 0
1914
- result[t] = obv
1915
- }
1916
- return result
1917
- }
1918
-
1919
- export function calcOBVDataSoA(layout: KLineSoALayout): (number | undefined)[] {
1920
- const data = SharedKLineBuffer.toKLineData(layout)
1921
- return calcOBVData(data)
1922
- }
1923
-
1924
- // ============================================================================
1925
- // PVT — Price Volume Trend (cumulative)
1926
- // PVT(t) = PVT(t-1) + ((close[t] - close[t-1]) / close[t-1]) * volume[t]
1927
- // ============================================================================
1928
-
1929
- export function calcPVTData(data: KLineData[]): (number | undefined)[] {
1930
- const n = data.length
1931
- const result: (number | undefined)[] = new Array(n).fill(undefined)
1932
- if (n === 0) return result
1933
- let pvt = 0
1934
- result[0] = 0
1935
- for (let t = 1; t < n; t++) {
1936
- const prevClose = data[t - 1]!.close
1937
- if (prevClose === 0) {
1938
- result[t] = pvt
1939
- continue
1940
- }
1941
- pvt += ((data[t]!.close - prevClose) / prevClose) * (data[t]!.volume ?? 0)
1942
- result[t] = pvt
1943
- }
1944
- return result
1945
- }
1946
-
1947
- export function calcPVTDataSoA(layout: KLineSoALayout): (number | undefined)[] {
1948
- const data = SharedKLineBuffer.toKLineData(layout)
1949
- return calcPVTData(data)
1950
- }
1951
-
1952
- // ============================================================================
1953
- // VWAP — Volume-Weighted Average Price
1954
- // VWAP(t) = sum_{i in session} TP(i) * V(i) / sum_{i in session} V(i)
1955
- // where TP(i) = (H+L+C)/3 (typical price)
1956
- // Session reset: if sessionResetGapMs > 0, reset cumulative sums when the gap
1957
- // between consecutive bar timestamps exceeds this value (e.g., overnight)
1958
- // ============================================================================
1959
-
1960
- export const DEFAULT_VWAP_SESSION_GAP_MS = 0
1961
-
1962
- export function calcVWAPData(
1963
- data: KLineData[],
1964
- sessionResetGapMs: number,
1965
- ): (number | undefined)[] {
1966
- const n = data.length
1967
- const result: (number | undefined)[] = new Array(n).fill(undefined)
1968
- if (n === 0) return result
1969
-
1970
- let cumPV = 0
1971
- let cumV = 0
1972
- let prevTs = data[0]!.timestamp
1973
-
1974
- for (let t = 0; t < n; t++) {
1975
- const bar = data[t]!
1976
- if (sessionResetGapMs > 0 && t > 0 && bar.timestamp - prevTs > sessionResetGapMs) {
1977
- cumPV = 0
1978
- cumV = 0
1979
- }
1980
- const tp = (bar.high + bar.low + bar.close) / 3
1981
- cumPV += tp * (bar.volume ?? 0)
1982
- cumV += bar.volume ?? 0
1983
- result[t] = cumV > 0 ? cumPV / cumV : tp
1984
- prevTs = bar.timestamp
1985
- }
1986
-
1987
- return result
1988
- }
1989
-
1990
- export function calcVWAPDataSoA(
1991
- layout: KLineSoALayout,
1992
- sessionResetGapMs: number,
1993
- ): (number | undefined)[] {
1994
- const data = SharedKLineBuffer.toKLineData(layout)
1995
- return calcVWAPData(data, sessionResetGapMs)
1996
- }
1997
-
1998
- // ============================================================================
1999
- // CMF — Chaikin Money Flow
2000
- // MFM = ((C-L) - (H-C)) / (H-L) ∈ [-1, 1]
2001
- // MFV = MFM * Volume
2002
- // CMF(t) = sum(MFV[t-period+1..t]) / sum(Volume[t-period+1..t]) ∈ [-1, 1]
2003
- // ============================================================================
2004
-
2005
- export const DEFAULT_CMF_PERIOD = 20
2006
-
2007
- export function calcCMFData(data: KLineData[], period: number): (number | undefined)[] {
2008
- const n = data.length
2009
- const result: (number | undefined)[] = new Array(n).fill(undefined)
2010
- if (n === 0 || period <= 0 || n < period) return result
2011
-
2012
- const mfv: number[] = new Array(n)
2013
- for (let i = 0; i < n; i++) {
2014
- const bar = data[i]!
2015
- const range = bar.high - bar.low
2016
- const mfm = range > 0 ? ((bar.close - bar.low) - (bar.high - bar.close)) / range : 0
2017
- mfv[i] = mfm * (bar.volume ?? 0)
2018
- }
2019
-
2020
- let sumMFV = 0
2021
- let sumV = 0
2022
- for (let i = 0; i < period; i++) {
2023
- sumMFV += mfv[i]!
2024
- sumV += data[i]!.volume ?? 0
2025
- }
2026
- result[period - 1] = sumV > 0 ? sumMFV / sumV : 0
2027
-
2028
- for (let t = period; t < n; t++) {
2029
- sumMFV += mfv[t]! - mfv[t - period]!
2030
- sumV += (data[t]!.volume ?? 0) - (data[t - period]!.volume ?? 0)
2031
- result[t] = sumV > 0 ? sumMFV / sumV : 0
2032
- }
2033
- return result
2034
- }
2035
-
2036
- export function calcCMFDataSoA(layout: KLineSoALayout, period: number): (number | undefined)[] {
2037
- const data = SharedKLineBuffer.toKLineData(layout)
2038
- return calcCMFData(data, period)
2039
- }
2040
-
2041
- // ============================================================================
2042
- // MFI — Money Flow Index
2043
- // TP = (H+L+C)/3, RMF = TP * Volume
2044
- // PMF = sum of RMF where TP > TP[-1]; NMF = sum where TP < TP[-1]
2045
- // MFR = PMF / NMF; MFI = 100 - 100 / (1 + MFR) ∈ [0, 100]
2046
- // ============================================================================
2047
-
2048
- export const DEFAULT_MFI_PERIOD = 14
2049
-
2050
- export function calcMFIData(data: KLineData[], period: number): (number | undefined)[] {
2051
- const n = data.length
2052
- const result: (number | undefined)[] = new Array(n).fill(undefined)
2053
- if (n < period + 1 || period <= 0) return result
2054
-
2055
- const tp: number[] = new Array(n)
2056
- for (let i = 0; i < n; i++) tp[i] = (data[i]!.high + data[i]!.low + data[i]!.close) / 3
2057
-
2058
- // Pre-classified positive/negative money flow per bar
2059
- const pmfArr: number[] = new Array(n)
2060
- const nmfArr: number[] = new Array(n)
2061
- pmfArr[0] = 0
2062
- nmfArr[0] = 0
2063
- for (let i = 1; i < n; i++) {
2064
- const rmf = tp[i]! * (data[i]!.volume ?? 0)
2065
- if (tp[i]! > tp[i - 1]!) {
2066
- pmfArr[i] = rmf
2067
- nmfArr[i] = 0
2068
- } else if (tp[i]! < tp[i - 1]!) {
2069
- pmfArr[i] = 0
2070
- nmfArr[i] = rmf
2071
- } else {
2072
- pmfArr[i] = 0
2073
- nmfArr[i] = 0
2074
- }
2075
- }
2076
-
2077
- let pSum = 0
2078
- let nSum = 0
2079
- for (let i = 1; i <= period; i++) {
2080
- pSum += pmfArr[i]!
2081
- nSum += nmfArr[i]!
2082
- }
2083
- result[period] = nSum > 0 ? 100 - 100 / (1 + pSum / nSum) : 100
2084
-
2085
- for (let t = period + 1; t < n; t++) {
2086
- pSum += pmfArr[t]! - pmfArr[t - period]!
2087
- nSum += nmfArr[t]! - nmfArr[t - period]!
2088
- result[t] = nSum > 0 ? 100 - 100 / (1 + pSum / nSum) : 100
2089
- }
2090
- return result
2091
- }
2092
-
2093
- export function calcMFIDataSoA(layout: KLineSoALayout, period: number): (number | undefined)[] {
2094
- const data = SharedKLineBuffer.toKLineData(layout)
2095
- return calcMFIData(data, period)
2096
- }
2097
-
2098
- // ============================================================================
2099
- // Pivot Points — Classic floor-trader pivots from prior bar's HLC
2100
- // PP = (H + L + C) / 3
2101
- // R1 = 2·PP - L; S1 = 2·PP - H
2102
- // R2 = PP + (H - L); S2 = PP - (H - L)
2103
- // R3 = H + 2·(PP - L); S3 = L - 2·(H - PP)
2104
- // Each bar t (t >= 1) shows pivots derived from bar[t-1]'s HLC.
2105
- // ============================================================================
2106
-
2107
- export interface PivotPoint {
2108
- pp: number
2109
- r1: number
2110
- r2: number
2111
- r3: number
2112
- s1: number
2113
- s2: number
2114
- s3: number
2115
- }
2116
-
2117
- export function calcPivotData(data: KLineData[]): (PivotPoint | undefined)[] {
2118
- const n = data.length
2119
- const result: (PivotPoint | undefined)[] = new Array(n).fill(undefined)
2120
- if (n < 2) return result
2121
- for (let t = 1; t < n; t++) {
2122
- const p = data[t - 1]!
2123
- const pp = (p.high + p.low + p.close) / 3
2124
- const range = p.high - p.low
2125
- result[t] = {
2126
- pp,
2127
- r1: 2 * pp - p.low,
2128
- s1: 2 * pp - p.high,
2129
- r2: pp + range,
2130
- s2: pp - range,
2131
- r3: p.high + 2 * (pp - p.low),
2132
- s3: p.low - 2 * (p.high - pp),
2133
- }
2134
- }
2135
- return result
2136
- }
2137
-
2138
- export function calcPivotDataSoA(layout: KLineSoALayout): (PivotPoint | undefined)[] {
2139
- const data = SharedKLineBuffer.toKLineData(layout)
2140
- return calcPivotData(data)
2141
- }
2142
-
2143
- // ============================================================================
2144
- // Fibonacci Retracement — anchored to rolling-window high/low
2145
- // Window = last `period` bars. High = max(high), Low = min(low).
2146
- // Direction = 'up' if the most recent extreme is the high (price moved up);
2147
- // 'down' otherwise. Retracement levels computed accordingly.
2148
- // ============================================================================
2149
-
2150
- export interface FibPoint {
2151
- high: number
2152
- low: number
2153
- direction: 'up' | 'down'
2154
- level236: number
2155
- level382: number
2156
- level500: number
2157
- level618: number
2158
- level786: number
2159
- }
2160
-
2161
- export const DEFAULT_FIB_PERIOD = 50
2162
-
2163
- export function calcFibData(data: KLineData[], period: number): (FibPoint | undefined)[] {
2164
- const n = data.length
2165
- const result: (FibPoint | undefined)[] = new Array(n).fill(undefined)
2166
- if (n === 0 || period <= 0 || n < period) return result
2167
-
2168
- for (let t = period - 1; t < n; t++) {
2169
- let hi = -Infinity
2170
- let lo = Infinity
2171
- let hiIdx = t
2172
- let loIdx = t
2173
- for (let k = 0; k < period; k++) {
2174
- const bar = data[t - k]!
2175
- if (bar.high > hi) { hi = bar.high; hiIdx = t - k }
2176
- if (bar.low < lo) { lo = bar.low; loIdx = t - k }
2177
- }
2178
- const direction: 'up' | 'down' = hiIdx >= loIdx ? 'up' : 'down'
2179
- const range = hi - lo
2180
- // For uptrend retracements: 0% at high, 100% at low; price retraces FROM high TOWARD low
2181
- // For downtrend: 0% at low, 100% at high
2182
- const level = (frac: number) =>
2183
- direction === 'up' ? hi - range * frac : lo + range * frac
2184
- result[t] = {
2185
- high: hi,
2186
- low: lo,
2187
- direction,
2188
- level236: level(0.236),
2189
- level382: level(0.382),
2190
- level500: level(0.5),
2191
- level618: level(0.618),
2192
- level786: level(0.786),
2193
- }
2194
- }
2195
- return result
2196
- }
2197
-
2198
- export function calcFibDataSoA(layout: KLineSoALayout, period: number): (FibPoint | undefined)[] {
2199
- const data = SharedKLineBuffer.toKLineData(layout)
2200
- return calcFibData(data, period)
2201
- }
2202
-
2203
- // ============================================================================
2204
- // SMC Structure — Swing detection + BOS / CHOCH events
2205
- // Window-based swing fractal (default left=right=2 = Bill Williams fractal).
2206
- // Swing confirmed when right-window bars have closed after the extremum.
2207
- // Trend state machine: HH+HL = up, LL+LH = down, mixed = range.
2208
- // BOS = continuation break (close > last HH in up trend, or < last LL in down trend).
2209
- // CHOCH = reversal break (against current trend).
2210
- // ============================================================================
2211
-
2212
- export interface SwingPoint {
2213
- index: number
2214
- price: number
2215
- kind: 'high' | 'low'
2216
- label: 'HH' | 'HL' | 'LH' | 'LL'
2217
- confirmed: boolean
2218
- }
2219
-
2220
- export type StructureEventKind = 'BOS' | 'CHOCH'
2221
-
2222
- export interface StructureEvent {
2223
- kind: StructureEventKind
2224
- index: number
2225
- triggerPrice: number
2226
- brokenLevel: number
2227
- brokenSwingIndex: number
2228
- direction: 'up' | 'down'
2229
- }
2230
-
2231
- export interface StructureSnapshot {
2232
- swings: SwingPoint[]
2233
- events: StructureEvent[]
2234
- trend: 'up' | 'down' | 'range'
2235
- }
2236
-
2237
- export const DEFAULT_STRUCTURE_LEFT = 2
2238
- export const DEFAULT_STRUCTURE_RIGHT = 2
2239
-
2240
- export function calcStructureData(
2241
- data: KLineData[],
2242
- leftWindow: number,
2243
- rightWindow: number,
2244
- breakoutSource: 'close' | 'wick',
2245
- ): StructureSnapshot {
2246
- const n = data.length
2247
- if (n === 0 || leftWindow < 0 || rightWindow < 0) {
2248
- return { swings: [], events: [], trend: 'range' }
2249
- }
2250
-
2251
- const rawSwings: { index: number; price: number; kind: 'high' | 'low'; confirmed: boolean }[] = []
2252
- for (let i = 0; i < n; i++) {
2253
- const bar = data[i]!
2254
- if (isExtremum(data, i, leftWindow, rightWindow, 'high')) {
2255
- rawSwings.push({ index: i, price: bar.high, kind: 'high', confirmed: i + rightWindow < n })
2256
- }
2257
- if (isExtremum(data, i, leftWindow, rightWindow, 'low')) {
2258
- rawSwings.push({ index: i, price: bar.low, kind: 'low', confirmed: i + rightWindow < n })
2259
- }
2260
- }
2261
- rawSwings.sort((a, b) => a.index - b.index)
2262
-
2263
- const swings: SwingPoint[] = []
2264
- let lastHigh: { index: number; price: number } | null = null
2265
- let lastLow: { index: number; price: number } | null = null
2266
- for (const s of rawSwings) {
2267
- let label: 'HH' | 'HL' | 'LH' | 'LL'
2268
- if (s.kind === 'high') {
2269
- label = lastHigh && s.price > lastHigh.price ? 'HH' : 'LH'
2270
- lastHigh = { index: s.index, price: s.price }
2271
- } else {
2272
- label = lastLow && s.price > lastLow.price ? 'HL' : 'LL'
2273
- lastLow = { index: s.index, price: s.price }
2274
- }
2275
- swings.push({ ...s, label })
2276
- }
2277
-
2278
- const events: StructureEvent[] = []
2279
- let trend: 'up' | 'down' | 'range' = 'range'
2280
- let lastSwingHigh: { index: number; price: number } | null = null
2281
- let lastSwingLow: { index: number; price: number } | null = null
2282
- const confirmedSwings = swings.filter((s) => s.confirmed)
2283
- let swingCursor = 0
2284
-
2285
- for (let t = 0; t < n; t++) {
2286
- while (swingCursor < confirmedSwings.length && confirmedSwings[swingCursor]!.index + rightWindow <= t) {
2287
- const s = confirmedSwings[swingCursor]!
2288
- if (s.kind === 'high') lastSwingHigh = { index: s.index, price: s.price }
2289
- else lastSwingLow = { index: s.index, price: s.price }
2290
- swingCursor++
2291
- }
2292
-
2293
- const bar = data[t]!
2294
- const upBreakPrice = breakoutSource === 'close' ? bar.close : bar.high
2295
- const downBreakPrice = breakoutSource === 'close' ? bar.close : bar.low
2296
-
2297
- if (lastSwingHigh && upBreakPrice > lastSwingHigh.price) {
2298
- const kind: StructureEventKind = trend === 'down' ? 'CHOCH' : 'BOS'
2299
- events.push({
2300
- kind,
2301
- index: t,
2302
- triggerPrice: upBreakPrice,
2303
- brokenLevel: lastSwingHigh.price,
2304
- brokenSwingIndex: lastSwingHigh.index,
2305
- direction: 'up',
2306
- })
2307
- trend = 'up'
2308
- lastSwingHigh = null
2309
- } else if (lastSwingLow && downBreakPrice < lastSwingLow.price) {
2310
- const kind: StructureEventKind = trend === 'up' ? 'CHOCH' : 'BOS'
2311
- events.push({
2312
- kind,
2313
- index: t,
2314
- triggerPrice: downBreakPrice,
2315
- brokenLevel: lastSwingLow.price,
2316
- brokenSwingIndex: lastSwingLow.index,
2317
- direction: 'down',
2318
- })
2319
- trend = 'down'
2320
- lastSwingLow = null
2321
- }
2322
- }
2323
-
2324
- return { swings, events, trend }
2325
- }
2326
-
2327
- function isExtremum(
2328
- data: KLineData[],
2329
- i: number,
2330
- left: number,
2331
- right: number,
2332
- kind: 'high' | 'low',
2333
- ): boolean {
2334
- const n = data.length
2335
- if (i < left || i + right >= n) return false
2336
- const center = kind === 'high' ? data[i]!.high : data[i]!.low
2337
- for (let k = 1; k <= left; k++) {
2338
- const v = kind === 'high' ? data[i - k]!.high : data[i - k]!.low
2339
- if (kind === 'high' ? v >= center : v <= center) return false
2340
- }
2341
- for (let k = 1; k <= right; k++) {
2342
- const v = kind === 'high' ? data[i + k]!.high : data[i + k]!.low
2343
- if (kind === 'high' ? v >= center : v <= center) return false
2344
- }
2345
- return true
2346
- }
2347
-
2348
- export function calcStructureDataSoA(
2349
- layout: KLineSoALayout,
2350
- leftWindow: number,
2351
- rightWindow: number,
2352
- breakoutSource: 'close' | 'wick',
2353
- ): StructureSnapshot {
2354
- const data = SharedKLineBuffer.toKLineData(layout)
2355
- return calcStructureData(data, leftWindow, rightWindow, breakoutSource)
2356
- }
2357
-
2358
- // ============================================================================
2359
- // SMC Zones — FVG (Fair Value Gap) + Order Blocks
2360
- // FVG (3-bar pattern):
2361
- // Bullish FVG: bar[t-2].high < bar[t].low → gap zone [bar[t-2].high, bar[t].low] anchored at bar[t-1]
2362
- // Bearish FVG: bar[t-2].low > bar[t].high → gap zone [bar[t].high, bar[t-2].low] anchored at bar[t-1]
2363
- // Zone is "filled" (endIndex set) when price re-enters it.
2364
- // Order Blocks:
2365
- // Computed in conjunction with BOS events from calcStructureData.
2366
- // Bullish OB = last bearish candle (close < open) within obLookback bars before an upward BOS.
2367
- // Bearish OB = last bullish candle (close > open) within obLookback bars before a downward BOS.
2368
- // Mitigated (endIndex set) when price returns into the candle's range.
2369
- // ============================================================================
2370
-
2371
- export type ZoneKind = 'FVG_BULL' | 'FVG_BEAR' | 'OB_BULL' | 'OB_BEAR'
2372
-
2373
- export interface Zone {
2374
- kind: ZoneKind
2375
- startIndex: number
2376
- endIndex?: number
2377
- high: number
2378
- low: number
2379
- }
2380
-
2381
- export const DEFAULT_ZONES_OB_LOOKBACK = 5
2382
-
2383
- export function calcZonesData(
2384
- data: KLineData[],
2385
- obLookback: number,
2386
- structureLeftWindow: number,
2387
- structureRightWindow: number,
2388
- breakoutSource: 'close' | 'wick',
2389
- ): Zone[] {
2390
- const n = data.length
2391
- if (n < 3) return []
2392
- const zones: Zone[] = []
2393
-
2394
- // 1. Detect FVGs
2395
- for (let t = 2; t < n; t++) {
2396
- const a = data[t - 2]!
2397
- const c = data[t]!
2398
- // Bullish FVG: a.high < c.low → gap
2399
- if (a.high < c.low) {
2400
- zones.push({
2401
- kind: 'FVG_BULL',
2402
- startIndex: t - 1,
2403
- high: c.low,
2404
- low: a.high,
2405
- })
2406
- }
2407
- // Bearish FVG: a.low > c.high → gap
2408
- if (a.low > c.high) {
2409
- zones.push({
2410
- kind: 'FVG_BEAR',
2411
- startIndex: t - 1,
2412
- high: a.low,
2413
- low: c.high,
2414
- })
2415
- }
2416
- }
2417
-
2418
- // 2. Detect Order Blocks using structure BOS events
2419
- const struct = calcStructureData(data, structureLeftWindow, structureRightWindow, breakoutSource)
2420
- for (const ev of struct.events) {
2421
- if (ev.kind !== 'BOS') continue
2422
- // Look back obLookback bars for the OB candle
2423
- const start = Math.max(0, ev.index - obLookback)
2424
- if (ev.direction === 'up') {
2425
- // Bullish OB: latest bearish candle (close < open) in [start, ev.index)
2426
- for (let k = ev.index - 1; k >= start; k--) {
2427
- const bar = data[k]!
2428
- if (bar.close < bar.open) {
2429
- zones.push({ kind: 'OB_BULL', startIndex: k, high: bar.high, low: bar.low })
2430
- break
2431
- }
2432
- }
2433
- } else {
2434
- // Bearish OB: latest bullish candle (close > open) in [start, ev.index)
2435
- for (let k = ev.index - 1; k >= start; k--) {
2436
- const bar = data[k]!
2437
- if (bar.close > bar.open) {
2438
- zones.push({ kind: 'OB_BEAR', startIndex: k, high: bar.high, low: bar.low })
2439
- break
2440
- }
2441
- }
2442
- }
2443
- }
2444
-
2445
- // 3. Mark zones as filled when price re-enters their range
2446
- for (const zone of zones) {
2447
- for (let t = zone.startIndex + 1; t < n; t++) {
2448
- const bar = data[t]!
2449
- // Zone is touched if the bar overlaps the zone's [low, high]
2450
- if (bar.low <= zone.high && bar.high >= zone.low) {
2451
- zone.endIndex = t
2452
- break
2453
- }
2454
- }
2455
- }
2456
-
2457
- return zones
2458
- }
2459
-
2460
- export function calcZonesDataSoA(
2461
- layout: KLineSoALayout,
2462
- obLookback: number,
2463
- structureLeftWindow: number,
2464
- structureRightWindow: number,
2465
- breakoutSource: 'close' | 'wick',
2466
- ): Zone[] {
2467
- const data = SharedKLineBuffer.toKLineData(layout)
2468
- return calcZonesData(data, obLookback, structureLeftWindow, structureRightWindow, breakoutSource)
2469
- }
2470
-
2471
- // ============================================================================
2472
- // Volume Profile — price-bin volume distribution
2473
- // For each bar, volume is distributed uniformly across [low, high] into bins.
2474
- // Outputs: bins[], POC (max-volume bin center), VAH/VAL (value area boundaries).
2475
- // Value area = contiguous bins around POC summing to valueAreaPercent of total V.
2476
- // ============================================================================
2477
-
2478
- export interface VolumeProfileBin {
2479
- priceLow: number
2480
- priceHigh: number
2481
- volume: number
2482
- }
2483
-
2484
- export interface VolumeProfileResult {
2485
- bins: VolumeProfileBin[]
2486
- poc: number
2487
- vah: number
2488
- val: number
2489
- totalVolume: number
2490
- }
2491
-
2492
- export const DEFAULT_VP_BINS = 24
2493
- export const DEFAULT_VP_LOOKBACK = 0
2494
- export const DEFAULT_VP_VALUE_AREA = 0.7
2495
-
2496
- export function calcVolumeProfileData(
2497
- data: KLineData[],
2498
- bins: number,
2499
- lookback: number,
2500
- valueAreaPercent: number,
2501
- ): VolumeProfileResult {
2502
- const n = data.length
2503
- if (n === 0 || bins <= 0) {
2504
- return { bins: [], poc: 0, vah: 0, val: 0, totalVolume: 0 }
2505
- }
2506
-
2507
- const startIdx = lookback > 0 ? Math.max(0, n - lookback) : 0
2508
- let priceMin = Infinity
2509
- let priceMax = -Infinity
2510
- for (let i = startIdx; i < n; i++) {
2511
- const bar = data[i]!
2512
- if (bar.low < priceMin) priceMin = bar.low
2513
- if (bar.high > priceMax) priceMax = bar.high
2514
- }
2515
- if (!Number.isFinite(priceMin) || !Number.isFinite(priceMax) || priceMax <= priceMin) {
2516
- return { bins: [], poc: priceMin, vah: priceMin, val: priceMin, totalVolume: 0 }
2517
- }
2518
-
2519
- const binWidth = (priceMax - priceMin) / bins
2520
- const binVolumes: number[] = new Array(bins).fill(0)
2521
-
2522
- // Distribute each bar's volume uniformly across the bins its [low, high] covers
2523
- for (let i = startIdx; i < n; i++) {
2524
- const bar = data[i]!
2525
- const barRange = bar.high - bar.low
2526
- if (barRange <= 0) {
2527
- const binIdx = Math.min(bins - 1, Math.max(0, Math.floor((bar.close - priceMin) / binWidth)))
2528
- binVolumes[binIdx]! += bar.volume ?? 0
2529
- continue
2530
- }
2531
- const volPerPrice = (bar.volume ?? 0) / barRange
2532
- const startBin = Math.max(0, Math.floor((bar.low - priceMin) / binWidth))
2533
- const endBin = Math.min(bins - 1, Math.floor((bar.high - priceMin) / binWidth))
2534
- for (let b = startBin; b <= endBin; b++) {
2535
- const binLow = priceMin + b * binWidth
2536
- const binHigh = binLow + binWidth
2537
- const overlapLow = Math.max(bar.low, binLow)
2538
- const overlapHigh = Math.min(bar.high, binHigh)
2539
- const overlap = overlapHigh - overlapLow
2540
- if (overlap > 0) {
2541
- binVolumes[b]! += overlap * volPerPrice
2542
- }
2543
- }
2544
- }
2545
-
2546
- const binsArr: VolumeProfileBin[] = binVolumes.map((v, b) => ({
2547
- priceLow: priceMin + b * binWidth,
2548
- priceHigh: priceMin + (b + 1) * binWidth,
2549
- volume: v,
2550
- }))
2551
-
2552
- // POC = max-volume bin center
2553
- let pocBinIdx = 0
2554
- for (let b = 1; b < bins; b++) {
2555
- if (binVolumes[b]! > binVolumes[pocBinIdx]!) pocBinIdx = b
2556
- }
2557
- const poc = (binsArr[pocBinIdx]!.priceLow + binsArr[pocBinIdx]!.priceHigh) / 2
2558
-
2559
- const totalVolume = binVolumes.reduce((a, b) => a + b, 0)
2560
-
2561
- // Value Area: expand outward from POC until cumulative volume >= valueAreaPercent of total
2562
- const target = totalVolume * valueAreaPercent
2563
- let acc = binVolumes[pocBinIdx]!
2564
- let lo = pocBinIdx
2565
- let hi = pocBinIdx
2566
- while (acc < target && (lo > 0 || hi < bins - 1)) {
2567
- const loCand = lo > 0 ? binVolumes[lo - 1]! : -Infinity
2568
- const hiCand = hi < bins - 1 ? binVolumes[hi + 1]! : -Infinity
2569
- if (loCand >= hiCand && lo > 0) {
2570
- lo--
2571
- acc += binVolumes[lo]!
2572
- } else if (hi < bins - 1) {
2573
- hi++
2574
- acc += binVolumes[hi]!
2575
- } else {
2576
- break
2577
- }
2578
- }
2579
- const val = binsArr[lo]!.priceLow
2580
- const vah = binsArr[hi]!.priceHigh
2581
-
2582
- return { bins: binsArr, poc, vah, val, totalVolume }
2583
- }
2584
-
2585
- export function calcVolumeProfileDataSoA(
2586
- layout: KLineSoALayout,
2587
- bins: number,
2588
- lookback: number,
2589
- valueAreaPercent: number,
2590
- ): VolumeProfileResult {
2591
- const data = SharedKLineBuffer.toKLineData(layout)
2592
- return calcVolumeProfileData(data, bins, lookback, valueAreaPercent)
2593
- }
1
+ import type { KLineData } from '../../types/price'
2
+
3
+ /**
4
+ * MA 周期配置标志
5
+ */
6
+ export type MAFlags = {
7
+ ma5?: boolean
8
+ ma10?: boolean
9
+ ma20?: boolean
10
+ ma30?: boolean
11
+ ma60?: boolean
12
+ }
13
+
14
+ /**
15
+ * 默认 MA 周期列表
16
+ */
17
+ export const DEFAULT_MA_PERIODS = [5, 10, 20, 30, 60] as const
18
+
19
+ // ============================================================================
20
+ // BOLL 布林带
21
+ // ============================================================================
22
+
23
+ /**
24
+ * BOLL 数据点
25
+ */
26
+ export interface BOLLPoint {
27
+ upper: number
28
+ middle: number
29
+ lower: number
30
+ }
31
+
32
+ /**
33
+ * 默认 BOLL 参数
34
+ */
35
+ export const DEFAULT_BOLL_PERIOD = 20
36
+ export const DEFAULT_BOLL_MULTIPLIER = 2
37
+
38
+ /**
39
+ * 计算 BOLL 数据(使用滑动窗口优化)
40
+ * @param data K线数据数组
41
+ * @param period 周期(默认20)
42
+ * @param multiplier 标准差倍数(默认2)
43
+ * @returns 每个索引对应的BOLL值,前 period-1 个为 undefined
44
+ */
45
+ export function calcBOLLData(
46
+ data: KLineData[],
47
+ period: number,
48
+ multiplier: number
49
+ ): BOLLPoint[] {
50
+ const result: BOLLPoint[] = new Array(data.length)
51
+
52
+ if (data.length < period) return result
53
+
54
+ // 使用滑动窗口计算,避免重复求和
55
+ let sum = 0
56
+ const window: number[] = []
57
+
58
+ // 初始化第一个窗口
59
+ for (let i = 0; i < period; i++) {
60
+ const item = data[i]
61
+ if (!item) return result
62
+ const close = item.close
63
+ window.push(close)
64
+ sum += close
65
+ }
66
+
67
+ // 计算每个点的 BOLL
68
+ for (let i = period - 1; i < data.length; i++) {
69
+ const item = data[i]
70
+ if (!item) continue
71
+
72
+ // 更新窗口求和
73
+ if (i >= period) {
74
+ const oldVal = window.shift()
75
+ if (oldVal !== undefined) sum -= oldVal
76
+ const close = item.close
77
+ window.push(close)
78
+ sum += close
79
+ }
80
+
81
+ const ma = sum / period
82
+
83
+ // 计算标准差
84
+ let variance = 0
85
+ for (let j = 0; j < period; j++) {
86
+ const wVal = window[j]
87
+ if (wVal !== undefined) {
88
+ variance += Math.pow(wVal - ma, 2)
89
+ }
90
+ }
91
+ const stdDev = Math.sqrt(variance / period)
92
+
93
+ result[i] = {
94
+ upper: ma + multiplier * stdDev,
95
+ middle: ma,
96
+ lower: ma - multiplier * stdDev,
97
+ }
98
+ }
99
+
100
+ return result
101
+ }
102
+
103
+ // ============================================================================
104
+ // EXPMA 指数平滑移动平均线
105
+ // ============================================================================
106
+
107
+ /**
108
+ * EXPMA 数据点
109
+ */
110
+ export interface EXPMAPoint {
111
+ fast: number
112
+ slow: number
113
+ }
114
+
115
+ /**
116
+ * 默认 EXPMA 参数
117
+ */
118
+ export const DEFAULT_EXPMA_FAST_PERIOD = 12
119
+ export const DEFAULT_EXPMA_SLOW_PERIOD = 50
120
+
121
+ /**
122
+ * 计算 EXPMA 数据
123
+ * 公式:EXPMA(i) = C(i) × K + EXPMA(i-1) × (1-K),K = 2/(N+1)
124
+ * @param data K线数据数组
125
+ * @param fastPeriod 快线周期(默认12)
126
+ * @param slowPeriod 慢线周期(默认50)
127
+ * @returns 每个索引对应的EXPMA值(从 index 0 开始有值)
128
+ */
129
+ export function calcEXPMAData(
130
+ data: KLineData[],
131
+ fastPeriod: number,
132
+ slowPeriod: number
133
+ ): EXPMAPoint[] {
134
+ const result: EXPMAPoint[] = new Array(data.length)
135
+
136
+ if (data.length === 0) return result
137
+
138
+ const fastK = 2 / (fastPeriod + 1)
139
+ const slowK = 2 / (slowPeriod + 1)
140
+
141
+ // 第一个点的 EXPMA 等于第一天的收盘价
142
+ const firstClose = data[0]!.close
143
+ let fastEMA = firstClose
144
+ let slowEMA = firstClose
145
+
146
+ result[0] = { fast: fastEMA, slow: slowEMA }
147
+
148
+ for (let i = 1; i < data.length; i++) {
149
+ const close = data[i]!.close
150
+ fastEMA = close * fastK + fastEMA * (1 - fastK)
151
+ slowEMA = close * slowK + slowEMA * (1 - slowK)
152
+ result[i] = { fast: fastEMA, slow: slowEMA }
153
+ }
154
+
155
+ return result
156
+ }
157
+
158
+ // ============================================================================
159
+ // ENE 轨道线
160
+ // ============================================================================
161
+
162
+ /**
163
+ * ENE 数据点
164
+ */
165
+ export interface ENEPoint {
166
+ upper: number
167
+ middle: number
168
+ lower: number
169
+ }
170
+
171
+ /**
172
+ * 默认 ENE 参数
173
+ */
174
+ export const DEFAULT_ENE_PERIOD = 10
175
+ export const DEFAULT_ENE_DEVIATION = 11
176
+
177
+ /**
178
+ * 计算 ENE 数据
179
+ * 中轨 = MA(close, N)
180
+ * 上轨 = 中轨 × (1 + M/100)
181
+ * 下轨 = 中轨 × (1 - M/100)
182
+ * @param data K线数据数组
183
+ * @param period 周期(默认10)
184
+ * @param deviation 偏离率百分比(默认11)
185
+ * @returns 每个索引对应的ENE值,前 period-1 个为 undefined
186
+ */
187
+ export function calcENEData(
188
+ data: KLineData[],
189
+ period: number,
190
+ deviation: number
191
+ ): ENEPoint[] {
192
+ const result: ENEPoint[] = new Array(data.length)
193
+
194
+ if (data.length < period) return result
195
+
196
+ // 使用滑动窗口计算 MA
197
+ let sum = 0
198
+
199
+ // 初始化第一个窗口
200
+ for (let i = 0; i < period; i++) {
201
+ const item = data[i]
202
+ if (!item) return result
203
+ sum += item.close
204
+ }
205
+
206
+ // 第一个有效点
207
+ const firstMA = sum / period
208
+ const firstDeviation = deviation / 100
209
+ result[period - 1] = {
210
+ upper: firstMA * (1 + firstDeviation),
211
+ middle: firstMA,
212
+ lower: firstMA * (1 - firstDeviation),
213
+ }
214
+
215
+ // 滑动计算后续点
216
+ for (let i = period; i < data.length; i++) {
217
+ const prevItem = data[i - period]
218
+ const currItem = data[i]
219
+ if (!prevItem || !currItem) continue
220
+
221
+ sum = sum - prevItem.close + currItem.close
222
+ const ma = sum / period
223
+ const dev = deviation / 100
224
+
225
+ result[i] = {
226
+ upper: ma * (1 + dev),
227
+ middle: ma,
228
+ lower: ma * (1 - dev),
229
+ }
230
+ }
231
+
232
+ return result
233
+ }
234
+
235
+ /**
236
+ * 计算指定周期的 MA 数据(使用滑动窗口优化,O(n) 复杂度)
237
+ * @param data K线数据数组
238
+ * @param period MA周期
239
+ * @returns 每个索引对应的MA值,前 period-1 个为 undefined
240
+ */
241
+ export function calcMAData(data: KLineData[], period: number): (number | undefined)[] {
242
+ const result: (number | undefined)[] = new Array(data.length)
243
+
244
+ if (data.length < period) return result
245
+
246
+ // 滑动窗口求和
247
+ let sum = 0
248
+
249
+ // 初始化第一个窗口
250
+ for (let i = 0; i < period; i++) {
251
+ const item = data[i]
252
+ if (!item) return result
253
+ sum += item.close
254
+ }
255
+
256
+ // 第一个有效点
257
+ result[period - 1] = sum / period
258
+
259
+ // 滑动计算后续点
260
+ for (let i = period; i < data.length; i++) {
261
+ const prevItem = data[i - period]
262
+ const currItem = data[i]
263
+ if (!prevItem || !currItem) continue
264
+
265
+ sum = sum - prevItem.close + currItem.close
266
+ result[i] = sum / period
267
+ }
268
+
269
+ return result
270
+ }
271
+
272
+ // ============================================================================
273
+ // RSI 相对强弱指标
274
+ // ============================================================================
275
+
276
+ /**
277
+ * 默认 RSI 参数
278
+ */
279
+ export const DEFAULT_RSI_PERIOD1 = 6
280
+ export const DEFAULT_RSI_PERIOD2 = 12
281
+ export const DEFAULT_RSI_PERIOD3 = 24
282
+ export const DEFAULT_RSI_PERIODS = [6, 12, 24] as const
283
+
284
+ /**
285
+ * 计算 RSI 数据
286
+ * RSI = 100 - 100 / (1 + RS)
287
+ * RS = 平均上涨幅度 / 平均下跌幅度
288
+ * @param data K线数据数组
289
+ * @param period RSI周期
290
+ * @returns 每个索引对应的RSI值,前 period+1 个为 undefined(需要 period+1 个数据点计算初始平均)
291
+ */
292
+ export function calcRSIData(data: KLineData[], period: number): (number | undefined)[] {
293
+ const result: (number | undefined)[] = new Array(data.length)
294
+
295
+ if (data.length < period + 1) return result
296
+
297
+ // 计算价格变化
298
+ const changes: number[] = []
299
+ for (let i = 1; i < data.length; i++) {
300
+ changes.push(data[i]!.close - data[i - 1]!.close)
301
+ }
302
+
303
+ // 初始化:计算前 period 天的平均涨跌
304
+ let sumGain = 0
305
+ let sumLoss = 0
306
+
307
+ for (let i = 0; i < period; i++) {
308
+ const change = changes[i]
309
+ if (change !== undefined) {
310
+ if (change > 0) sumGain += change
311
+ else sumLoss += Math.abs(change)
312
+ }
313
+ }
314
+
315
+ // 第一个 RSI 值
316
+ let avgGain = sumGain / period
317
+ let avgLoss = sumLoss / period
318
+
319
+ if (avgLoss === 0) {
320
+ result[period] = 100
321
+ } else {
322
+ const rs = avgGain / avgLoss
323
+ result[period] = 100 - 100 / (1 + rs)
324
+ }
325
+
326
+ // 后续使用平滑计算(Wilder's smoothing)
327
+ for (let i = period; i < changes.length; i++) {
328
+ const change = changes[i]
329
+ if (change === undefined) continue
330
+
331
+ if (change > 0) {
332
+ avgGain = (avgGain * (period - 1) + change) / period
333
+ avgLoss = (avgLoss * (period - 1)) / period
334
+ } else {
335
+ avgGain = (avgGain * (period - 1)) / period
336
+ avgLoss = (avgLoss * (period - 1) + Math.abs(change)) / period
337
+ }
338
+
339
+ if (avgLoss === 0) {
340
+ result[i + 1] = 100
341
+ } else {
342
+ const rs = avgGain / avgLoss
343
+ result[i + 1] = 100 - 100 / (1 + rs)
344
+ }
345
+ }
346
+
347
+ return result
348
+ }
349
+
350
+ // ============================================================================
351
+ // CCI 顺势指标
352
+ // ============================================================================
353
+
354
+ export const DEFAULT_CCI_PERIOD = 14
355
+
356
+ export function calcCCIData(data: KLineData[], period: number): (number | undefined)[] {
357
+ const result: (number | undefined)[] = new Array(data.length)
358
+
359
+ if (data.length < period) return result
360
+
361
+ // 计算 TP (Typical Price) = (H + L + C) / 3
362
+ const tpValues: number[] = []
363
+ for (const item of data) {
364
+ tpValues.push((item.high + item.low + item.close) / 3)
365
+ }
366
+
367
+ // 计算 TP 的 SMA
368
+ let sum = 0
369
+ for (let i = 0; i < period; i++) {
370
+ sum += tpValues[i]!
371
+ }
372
+
373
+ for (let i = period - 1; i < data.length; i++) {
374
+ if (i >= period) {
375
+ sum = sum - tpValues[i - period]! + tpValues[i]!
376
+ }
377
+ const sma = sum / period
378
+
379
+ // 计算平均绝对偏差
380
+ let meanDeviation = 0
381
+ for (let j = 0; j < period; j++) {
382
+ meanDeviation += Math.abs(tpValues[i - j]! - sma)
383
+ }
384
+ meanDeviation /= period
385
+
386
+ if (meanDeviation === 0) {
387
+ result[i] = 0
388
+ } else {
389
+ result[i] = (tpValues[i]! - sma) / (0.015 * meanDeviation)
390
+ }
391
+ }
392
+
393
+ return result
394
+ }
395
+
396
+ // ============================================================================
397
+ // STOCH 随机指标
398
+ // ============================================================================
399
+
400
+ export const DEFAULT_STOCH_N = 9
401
+ export const DEFAULT_STOCH_M = 3
402
+
403
+ export interface STOCHPoint {
404
+ k: number
405
+ d: number
406
+ }
407
+
408
+ export function calcSTOCHData(data: KLineData[], n: number, m: number): STOCHPoint[] {
409
+ const result: STOCHPoint[] = new Array(data.length)
410
+
411
+ if (data.length < n) return result
412
+
413
+ // 计算 RSV 和 K
414
+ const kValues: (number | undefined)[] = new Array(data.length)
415
+
416
+ for (let i = n - 1; i < data.length; i++) {
417
+ let highest = -Infinity
418
+ let lowest = Infinity
419
+
420
+ for (let j = 0; j < n; j++) {
421
+ const item = data[i - j]
422
+ if (!item) continue
423
+ highest = Math.max(highest, item.high)
424
+ lowest = Math.min(lowest, item.low)
425
+ }
426
+
427
+ const close = data[i]!.close
428
+ if (highest === lowest) {
429
+ kValues[i] = 50
430
+ } else {
431
+ kValues[i] = ((close - lowest) / (highest - lowest)) * 100
432
+ }
433
+ }
434
+
435
+ // 计算 D (K 的 M 日移动平均)
436
+ for (let i = n - 1 + m - 1; i < data.length; i++) {
437
+ const k = kValues[i]
438
+ if (k === undefined) continue
439
+
440
+ let sum = 0
441
+ let validCount = 0
442
+ for (let j = 0; j < m; j++) {
443
+ const kv = kValues[i - j]
444
+ if (kv !== undefined) {
445
+ sum += kv
446
+ validCount++
447
+ }
448
+ }
449
+
450
+ if (validCount === m) {
451
+ result[i] = { k, d: sum / m }
452
+ }
453
+ }
454
+
455
+ return result
456
+ }
457
+
458
+ // ============================================================================
459
+ // MOM 动量指标
460
+ // ============================================================================
461
+
462
+ export const DEFAULT_MOM_PERIOD = 10
463
+
464
+ export function calcMOMData(data: KLineData[], period: number): (number | undefined)[] {
465
+ const result: (number | undefined)[] = new Array(data.length)
466
+
467
+ if (data.length < period + 1) return result
468
+
469
+ for (let i = period; i < data.length; i++) {
470
+ const currentClose = data[i]?.close
471
+ const prevClose = data[i - period]?.close
472
+
473
+ if (currentClose !== undefined && prevClose !== undefined) {
474
+ result[i] = currentClose - prevClose
475
+ }
476
+ }
477
+
478
+ return result
479
+ }
480
+
481
+ // ============================================================================
482
+ // WMSR 威廉指标
483
+ // ============================================================================
484
+
485
+ export const DEFAULT_WMSR_PERIOD = 14
486
+
487
+ export function calcWMSRData(data: KLineData[], period: number): (number | undefined)[] {
488
+ const result: (number | undefined)[] = new Array(data.length)
489
+
490
+ if (data.length < period) return result
491
+
492
+ for (let i = period - 1; i < data.length; i++) {
493
+ let highest = -Infinity
494
+ let lowest = Infinity
495
+
496
+ for (let j = 0; j < period; j++) {
497
+ const item = data[i - j]
498
+ if (!item) continue
499
+ highest = Math.max(highest, item.high)
500
+ lowest = Math.min(lowest, item.low)
501
+ }
502
+
503
+ const close = data[i]!.close
504
+ if (highest === lowest) {
505
+ result[i] = -50
506
+ } else {
507
+ result[i] = ((highest - close) / (highest - lowest)) * -100
508
+ }
509
+ }
510
+
511
+ return result
512
+ }
513
+
514
+ // ============================================================================
515
+ // KST 确知指标
516
+ // ============================================================================
517
+
518
+ export const DEFAULT_KST_ROC1 = 10
519
+ export const DEFAULT_KST_ROC2 = 15
520
+ export const DEFAULT_KST_ROC3 = 20
521
+ export const DEFAULT_KST_ROC4 = 30
522
+ export const DEFAULT_KST_SIGNAL = 9
523
+
524
+ export interface KSTPoint {
525
+ kst: number
526
+ signal: number
527
+ }
528
+
529
+ function calcROCInternal(data: KLineData[], period: number): (number | undefined)[] {
530
+ const result: (number | undefined)[] = new Array(data.length)
531
+
532
+ if (data.length < period + 1) return result
533
+
534
+ for (let i = period; i < data.length; i++) {
535
+ const currentClose = data[i]?.close
536
+ const prevClose = data[i - period]?.close
537
+
538
+ if (currentClose !== undefined && prevClose !== undefined && prevClose !== 0) {
539
+ result[i] = ((currentClose - prevClose) / prevClose) * 100
540
+ }
541
+ }
542
+
543
+ return result
544
+ }
545
+
546
+ function calcSMAInternal(data: (number | undefined)[], period: number): (number | undefined)[] {
547
+ const result: (number | undefined)[] = new Array(data.length)
548
+
549
+ let sum = 0
550
+ let count = 0
551
+
552
+ for (let i = 0; i < data.length; i++) {
553
+ const val = data[i]
554
+
555
+ if (val !== undefined) {
556
+ sum += val
557
+ count++
558
+
559
+ if (count > period) {
560
+ const oldVal = data[i - period]
561
+ if (oldVal !== undefined) {
562
+ sum -= oldVal
563
+ count--
564
+ }
565
+ }
566
+
567
+ if (count === period) {
568
+ result[i] = sum / period
569
+ }
570
+ }
571
+ }
572
+
573
+ return result
574
+ }
575
+
576
+ export function calcKSTData(
577
+ data: KLineData[],
578
+ roc1: number,
579
+ roc2: number,
580
+ roc3: number,
581
+ roc4: number,
582
+ signalPeriod: number
583
+ ): KSTPoint[] {
584
+ const result: KSTPoint[] = new Array(data.length)
585
+
586
+ const roc1Data = calcROCInternal(data, roc1)
587
+ const roc2Data = calcROCInternal(data, roc2)
588
+ const roc3Data = calcROCInternal(data, roc3)
589
+ const roc4Data = calcROCInternal(data, roc4)
590
+
591
+ const sma1 = calcSMAInternal(roc1Data, 10)
592
+ const sma2 = calcSMAInternal(roc2Data, 10)
593
+ const sma3 = calcSMAInternal(roc3Data, 10)
594
+ const sma4 = calcSMAInternal(roc4Data, 15)
595
+
596
+ const kstValues: (number | undefined)[] = new Array(data.length)
597
+
598
+ for (let i = 0; i < data.length; i++) {
599
+ const v1 = sma1[i]
600
+ const v2 = sma2[i]
601
+ const v3 = sma3[i]
602
+ const v4 = sma4[i]
603
+
604
+ if (v1 !== undefined && v2 !== undefined && v3 !== undefined && v4 !== undefined) {
605
+ kstValues[i] = v1 * 1 + v2 * 2 + v3 * 3 + v4 * 4
606
+ }
607
+ }
608
+
609
+ const signalData = calcSMAInternal(kstValues, signalPeriod)
610
+
611
+ for (let i = 0; i < data.length; i++) {
612
+ const kst = kstValues[i]
613
+ const signal = signalData[i]
614
+
615
+ if (kst !== undefined && signal !== undefined) {
616
+ result[i] = { kst, signal }
617
+ }
618
+ }
619
+
620
+ return result
621
+ }
622
+
623
+ // ============================================================================
624
+ // FASTK 快速随机指标
625
+ // ============================================================================
626
+
627
+ export const DEFAULT_FASTK_PERIOD = 9
628
+
629
+ export function calcFASTKData(data: KLineData[], period: number): (number | undefined)[] {
630
+ const result: (number | undefined)[] = new Array(data.length)
631
+
632
+ if (data.length < period) return result
633
+
634
+ for (let i = period - 1; i < data.length; i++) {
635
+ let highest = -Infinity
636
+ let lowest = Infinity
637
+
638
+ for (let j = 0; j < period; j++) {
639
+ const item = data[i - j]
640
+ if (!item) continue
641
+ highest = Math.max(highest, item.high)
642
+ lowest = Math.min(lowest, item.low)
643
+ }
644
+
645
+ const close = data[i]!.close
646
+ if (highest === lowest) {
647
+ result[i] = 50
648
+ } else {
649
+ result[i] = ((close - lowest) / (highest - lowest)) * 100
650
+ }
651
+ }
652
+
653
+ return result
654
+ }
655
+
656
+ // ============================================================================
657
+ // MACD 指数平滑异同移动平均线
658
+ // ============================================================================
659
+
660
+ /**
661
+ * MACD 数据点
662
+ */
663
+ export interface MACDPoint {
664
+ /** DIF 线值 */
665
+ dif: number
666
+ /** DEA 线值 */
667
+ dea: number
668
+ /** MACD 柱状图值 */
669
+ macd: number
670
+ }
671
+
672
+ /**
673
+ * 默认 MACD 参数
674
+ */
675
+ export const DEFAULT_MACD_FAST_PERIOD = 12
676
+ export const DEFAULT_MACD_SLOW_PERIOD = 26
677
+ export const DEFAULT_MACD_SIGNAL_PERIOD = 9
678
+
679
+ /**
680
+ * 计算 EMA(指数移动平均)值
681
+ * EMA(today) = close × K + EMA(yesterday) × (1 - K)
682
+ * K = 2 / (period + 1)
683
+ * @param data K线数据数组
684
+ * @param period 周期
685
+ * @returns EMA 值数组,第一个值使用第一个收盘价
686
+ */
687
+ export function calcEMA(data: KLineData[], period: number): number[] {
688
+ const result: number[] = new Array(data.length)
689
+ const k = 2 / (period + 1)
690
+
691
+ if (data.length === 0) return result
692
+
693
+ // 第一个 EMA 值使用第一个收盘价
694
+ result[0] = data[0]!.close
695
+
696
+ for (let i = 1; i < data.length; i++) {
697
+ const item = data[i]
698
+ if (!item) continue
699
+ result[i] = item.close * k + result[i - 1]! * (1 - k)
700
+ }
701
+
702
+ return result
703
+ }
704
+
705
+ /**
706
+ * 基于数值数组计算 EMA
707
+ * @param values 数值数组(可能包含 undefined)
708
+ * @param period 周期
709
+ * @returns EMA 值数组
710
+ */
711
+ export function calcEMAFromArray(values: (number | undefined)[], period: number): (number | undefined)[] {
712
+ const result: (number | undefined)[] = new Array(values.length)
713
+ const k = 2 / (period + 1)
714
+
715
+ const firstValid = values.findIndex(v => v !== undefined)
716
+ if (firstValid === -1) return result
717
+
718
+ result[firstValid] = values[firstValid]
719
+
720
+ for (let i = firstValid + 1; i < values.length; i++) {
721
+ const val = values[i]
722
+ const prev = result[i - 1]
723
+ if (val === undefined || prev === undefined) continue
724
+ result[i] = val * k + prev * (1 - k)
725
+ }
726
+
727
+ return result
728
+ }
729
+
730
+ /**
731
+ * 计算 MACD 数据
732
+ * DIF = EMA(close, fastPeriod) - EMA(close, slowPeriod)
733
+ * DEA = EMA(DIF, signalPeriod)
734
+ * MACD = (DIF - DEA) × 2
735
+ * @param data K线数据数组
736
+ * @param fastPeriod 快线周期(默认12)
737
+ * @param slowPeriod 慢线周期(默认26)
738
+ * @param signalPeriod 信号线周期(默认9)
739
+ * @returns MACD 数据点数组,前 slowPeriod-1 个可能为 undefined
740
+ */
741
+ export function calcMACDData(
742
+ data: KLineData[],
743
+ fastPeriod: number,
744
+ slowPeriod: number,
745
+ signalPeriod: number
746
+ ): MACDPoint[] {
747
+ const result: MACDPoint[] = new Array(data.length)
748
+
749
+ if (data.length < slowPeriod) return result
750
+
751
+ // 计算 EMA12 和 EMA26
752
+ const emaFast = calcEMA(data, fastPeriod)
753
+ const emaSlow = calcEMA(data, slowPeriod)
754
+
755
+ // 计算 DIF
756
+ const dif: (number | undefined)[] = new Array(data.length)
757
+ for (let i = 0; i < data.length; i++) {
758
+ const fast = emaFast[i]
759
+ const slow = emaSlow[i]
760
+ if (fast !== undefined && slow !== undefined) {
761
+ dif[i] = fast - slow
762
+ }
763
+ }
764
+
765
+ // 计算 DEA(DIF 的 signalPeriod 日 EMA)
766
+ const dea = calcEMAFromArray(dif, signalPeriod)
767
+
768
+ // 计算 MACD 柱
769
+ for (let i = 0; i < data.length; i++) {
770
+ const d = dif[i]
771
+ const e = dea[i]
772
+ if (d !== undefined && e !== undefined) {
773
+ result[i] = {
774
+ dif: d,
775
+ dea: e,
776
+ macd: (d - e) * 2,
777
+ }
778
+ }
779
+ }
780
+
781
+ return result
782
+ }
783
+
784
+ // ============================================================================
785
+ // SoA (Structure of Arrays) 包装函数
786
+ // 用于验证 SoA 数据层与原始 AoS 计算的一致性
787
+ // ============================================================================
788
+
789
+ import type { KLineSoALayout } from './soa'
790
+ import { SharedKLineBuffer } from './soa'
791
+
792
+ /**
793
+ * 从 SoA 布局计算 BOLL 数据(验证用包装函数)
794
+ * @param layout SoA 布局
795
+ * @param period 周期
796
+ * @param multiplier 标准差倍数
797
+ * @returns BOLL 数据点数组
798
+ */
799
+ export function calcBOLLDataSoA(
800
+ layout: KLineSoALayout,
801
+ period: number,
802
+ multiplier: number
803
+ ): BOLLPoint[] {
804
+ const data = SharedKLineBuffer.toKLineData(layout)
805
+ return calcBOLLData(data, period, multiplier)
806
+ }
807
+
808
+ /**
809
+ * 从 SoA 布局计算 EXPMA 数据(验证用包装函数)
810
+ * @param layout SoA 布局
811
+ * @param fastPeriod 快线周期
812
+ * @param slowPeriod 慢线周期
813
+ * @returns EXPMA 数据点数组
814
+ */
815
+ export function calcEXPMADataSoA(
816
+ layout: KLineSoALayout,
817
+ fastPeriod: number,
818
+ slowPeriod: number
819
+ ): EXPMAPoint[] {
820
+ const data = SharedKLineBuffer.toKLineData(layout)
821
+ return calcEXPMAData(data, fastPeriod, slowPeriod)
822
+ }
823
+
824
+ /**
825
+ * 从 SoA 布局计算 ENE 数据(验证用包装函数)
826
+ * @param layout SoA 布局
827
+ * @param period 周期
828
+ * @param deviation 偏离率百分比
829
+ * @returns ENE 数据点数组
830
+ */
831
+ export function calcENEDataSoA(
832
+ layout: KLineSoALayout,
833
+ period: number,
834
+ deviation: number
835
+ ): ENEPoint[] {
836
+ const data = SharedKLineBuffer.toKLineData(layout)
837
+ return calcENEData(data, period, deviation)
838
+ }
839
+
840
+ /**
841
+ * 从 SoA 布局计算 MA 数据(验证用包装函数)
842
+ * @param layout SoA 布局
843
+ * @param period MA周期
844
+ * @returns MA 值数组
845
+ */
846
+ export function calcMADataSoA(
847
+ layout: KLineSoALayout,
848
+ period: number
849
+ ): (number | undefined)[] {
850
+ const data = SharedKLineBuffer.toKLineData(layout)
851
+ return calcMAData(data, period)
852
+ }
853
+
854
+ /**
855
+ * 从 SoA 布局计算 RSI 数据(验证用包装函数)
856
+ * @param layout SoA 布局
857
+ * @param period RSI周期
858
+ * @returns RSI 值数组
859
+ */
860
+ export function calcRSIDataSoA(
861
+ layout: KLineSoALayout,
862
+ period: number
863
+ ): (number | undefined)[] {
864
+ const data = SharedKLineBuffer.toKLineData(layout)
865
+ return calcRSIData(data, period)
866
+ }
867
+
868
+ /**
869
+ * 从 SoA 布局计算 CCI 数据(验证用包装函数)
870
+ * @param layout SoA 布局
871
+ * @param period 周期
872
+ * @returns CCI 值数组
873
+ */
874
+ export function calcCCIDataSoA(
875
+ layout: KLineSoALayout,
876
+ period: number
877
+ ): (number | undefined)[] {
878
+ const data = SharedKLineBuffer.toKLineData(layout)
879
+ return calcCCIData(data, period)
880
+ }
881
+
882
+ /**
883
+ * 从 SoA 布局计算 STOCH 数据(验证用包装函数)
884
+ * @param layout SoA 布局
885
+ * @param n RSV周期
886
+ * @param m K的M日移动平均周期
887
+ * @returns STOCH 数据点数组
888
+ */
889
+ export function calcSTOCHDataSoA(
890
+ layout: KLineSoALayout,
891
+ n: number,
892
+ m: number
893
+ ): STOCHPoint[] {
894
+ const data = SharedKLineBuffer.toKLineData(layout)
895
+ return calcSTOCHData(data, n, m)
896
+ }
897
+
898
+ /**
899
+ * 从 SoA 布局计算 MOM 数据(验证用包装函数)
900
+ * @param layout SoA 布局
901
+ * @param period 周期
902
+ * @returns MOM 值数组
903
+ */
904
+ export function calcMOMDataSoA(
905
+ layout: KLineSoALayout,
906
+ period: number
907
+ ): (number | undefined)[] {
908
+ const data = SharedKLineBuffer.toKLineData(layout)
909
+ return calcMOMData(data, period)
910
+ }
911
+
912
+ /**
913
+ * 从 SoA 布局计算 WMSR 数据(验证用包装函数)
914
+ * @param layout SoA 布局
915
+ * @param period 周期
916
+ * @returns WMSR 值数组
917
+ */
918
+ export function calcWMSRDataSoA(
919
+ layout: KLineSoALayout,
920
+ period: number
921
+ ): (number | undefined)[] {
922
+ const data = SharedKLineBuffer.toKLineData(layout)
923
+ return calcWMSRData(data, period)
924
+ }
925
+
926
+ /**
927
+ * 从 SoA 布局计算 KST 数据(验证用包装函数)
928
+ * @param layout SoA 布局
929
+ * @param roc1 第一个ROC周期
930
+ * @param roc2 第二个ROC周期
931
+ * @param roc3 第三个ROC周期
932
+ * @param roc4 第四个ROC周期
933
+ * @param signalPeriod 信号线周期
934
+ * @returns KST 数据点数组
935
+ */
936
+ export function calcKSTDataSoA(
937
+ layout: KLineSoALayout,
938
+ roc1: number,
939
+ roc2: number,
940
+ roc3: number,
941
+ roc4: number,
942
+ signalPeriod: number
943
+ ): KSTPoint[] {
944
+ const data = SharedKLineBuffer.toKLineData(layout)
945
+ return calcKSTData(data, roc1, roc2, roc3, roc4, signalPeriod)
946
+ }
947
+
948
+ /**
949
+ * 从 SoA 布局计算 FASTK 数据(验证用包装函数)
950
+ * @param layout SoA 布局
951
+ * @param period 周期
952
+ * @returns FASTK 值数组
953
+ */
954
+ export function calcFASTKDataSoA(
955
+ layout: KLineSoALayout,
956
+ period: number
957
+ ): (number | undefined)[] {
958
+ const data = SharedKLineBuffer.toKLineData(layout)
959
+ return calcFASTKData(data, period)
960
+ }
961
+
962
+ /**
963
+ * 从 SoA 布局计算 MACD 数据(验证用包装函数)
964
+ * @param layout SoA 布局
965
+ * @param fastPeriod 快线周期
966
+ * @param slowPeriod 慢线周期
967
+ * @param signalPeriod 信号线周期
968
+ * @returns MACD 数据点数组
969
+ */
970
+ export function calcMACDDataSoA(
971
+ layout: KLineSoALayout,
972
+ fastPeriod: number,
973
+ slowPeriod: number,
974
+ signalPeriod: number
975
+ ): MACDPoint[] {
976
+ const data = SharedKLineBuffer.toKLineData(layout)
977
+ return calcMACDData(data, fastPeriod, slowPeriod, signalPeriod)
978
+ }
979
+
980
+ // ============================================================================
981
+ // ATR — Wilder's Average True Range
982
+ // ============================================================================
983
+
984
+ export const DEFAULT_ATR_PERIOD = 14
985
+
986
+ /**
987
+ * 计算 Wilder ATR。
988
+ * TR(0) = H(0) - L(0)
989
+ * TR(t) = max(H(t) - L(t), |H(t) - C(t-1)|, |L(t) - C(t-1)|)
990
+ * ATR(period-1) = mean(TR[0..period-1])
991
+ * ATR(t) = ((period-1) * ATR(t-1) + TR(t)) / period for t >= period
992
+ *
993
+ * @param data K 线数组
994
+ * @param period 周期,需 >= 1;若 <= 0 或 data.length < period,返回全 undefined
995
+ */
996
+ export function calcATRData(data: KLineData[], period: number): (number | undefined)[] {
997
+ const n = data.length
998
+ const result: (number | undefined)[] = new Array(n).fill(undefined)
999
+ if (n === 0 || period <= 0) return result
1000
+
1001
+ if (period === 1) {
1002
+ const first = data[0]!
1003
+ result[0] = first.high - first.low
1004
+ let prevClose = first.close
1005
+ for (let i = 1; i < n; i++) {
1006
+ const cur = data[i]!
1007
+ const tr = Math.max(
1008
+ cur.high - cur.low,
1009
+ Math.abs(cur.high - prevClose),
1010
+ Math.abs(cur.low - prevClose),
1011
+ )
1012
+ result[i] = tr
1013
+ prevClose = cur.close
1014
+ }
1015
+ return result
1016
+ }
1017
+
1018
+ if (n < period) return result
1019
+
1020
+ const first = data[0]!
1021
+ let sumTR = first.high - first.low
1022
+ let prevClose = first.close
1023
+
1024
+ for (let i = 1; i < period; i++) {
1025
+ const cur = data[i]!
1026
+ sumTR += Math.max(
1027
+ cur.high - cur.low,
1028
+ Math.abs(cur.high - prevClose),
1029
+ Math.abs(cur.low - prevClose),
1030
+ )
1031
+ prevClose = cur.close
1032
+ }
1033
+
1034
+ let atr = sumTR / period
1035
+ result[period - 1] = atr
1036
+
1037
+ const periodMinusOne = period - 1
1038
+ for (let i = period; i < n; i++) {
1039
+ const cur = data[i]!
1040
+ const tr = Math.max(
1041
+ cur.high - cur.low,
1042
+ Math.abs(cur.high - prevClose),
1043
+ Math.abs(cur.low - prevClose),
1044
+ )
1045
+ atr = (periodMinusOne * atr + tr) / period
1046
+ result[i] = atr
1047
+ prevClose = cur.close
1048
+ }
1049
+
1050
+ return result
1051
+ }
1052
+
1053
+ /**
1054
+ * 从 SoA 布局计算 ATR(包装函数,对齐其他指标的 SoA 入口)
1055
+ */
1056
+ export function calcATRDataSoA(
1057
+ layout: KLineSoALayout,
1058
+ period: number,
1059
+ ): (number | undefined)[] {
1060
+ const data = SharedKLineBuffer.toKLineData(layout)
1061
+ return calcATRData(data, period)
1062
+ }
1063
+
1064
+ // ============================================================================
1065
+ // WMA — Weighted Moving Average (linear weights)
1066
+ // 权重: w_i = i (i=1..period),分母 = period*(period+1)/2
1067
+ // 滞后 = (period-1)/3 (相比 SMA 更快响应)
1068
+ // ============================================================================
1069
+
1070
+ export const DEFAULT_WMA_PERIOD = 9
1071
+
1072
+ function _computeWMAOnNumbers(values: (number | undefined)[], period: number): (number | undefined)[] {
1073
+ const n = values.length
1074
+ const result: (number | undefined)[] = new Array(n).fill(undefined)
1075
+ if (n === 0 || period <= 0 || n < period) return result
1076
+
1077
+ const denom = (period * (period + 1)) / 2
1078
+
1079
+ for (let t = period - 1; t < n; t++) {
1080
+ let sw = 0
1081
+ let valid = true
1082
+ for (let k = 0; k < period; k++) {
1083
+ const v = values[t - period + 1 + k]
1084
+ if (v === undefined) {
1085
+ valid = false
1086
+ break
1087
+ }
1088
+ sw += (k + 1) * v
1089
+ }
1090
+ if (valid) result[t] = sw / denom
1091
+ }
1092
+ return result
1093
+ }
1094
+
1095
+ export function calcWMAData(data: KLineData[], period: number): (number | undefined)[] {
1096
+ if (data.length === 0 || period <= 0) {
1097
+ return new Array(data.length).fill(undefined)
1098
+ }
1099
+ const closes = new Array<number | undefined>(data.length)
1100
+ for (let i = 0; i < data.length; i++) closes[i] = data[i]!.close
1101
+ return _computeWMAOnNumbers(closes, period)
1102
+ }
1103
+
1104
+ export function calcWMADataSoA(layout: KLineSoALayout, period: number): (number | undefined)[] {
1105
+ const data = SharedKLineBuffer.toKLineData(layout)
1106
+ return calcWMAData(data, period)
1107
+ }
1108
+
1109
+ // ============================================================================
1110
+ // EMA helper(DEMA / TEMA 复用,沿用 EXPMA 的 first-close seed 习惯)
1111
+ // alpha = 2 / (period + 1)
1112
+ // ============================================================================
1113
+
1114
+ function _computeEMASeries(values: (number | undefined)[], period: number): (number | undefined)[] {
1115
+ const n = values.length
1116
+ const result: (number | undefined)[] = new Array(n).fill(undefined)
1117
+ if (n === 0 || period <= 0) return result
1118
+
1119
+ const alpha = 2 / (period + 1)
1120
+
1121
+ let i = 0
1122
+ while (i < n && values[i] === undefined) i++
1123
+ if (i >= n) return result
1124
+
1125
+ let ema = values[i]!
1126
+ result[i] = ema
1127
+ for (let t = i + 1; t < n; t++) {
1128
+ const v = values[t]
1129
+ if (v === undefined) continue
1130
+ ema = v * alpha + ema * (1 - alpha)
1131
+ result[t] = ema
1132
+ }
1133
+ return result
1134
+ }
1135
+
1136
+ // ============================================================================
1137
+ // DEMA — Double Exponential Moving Average
1138
+ // 公式: DEMA(t) = 2*EMA(t) - EMA(EMA)(t)
1139
+ // 性质: 对线性输入零滞后(稳态),warmup ~ 2*(period-1)
1140
+ // ============================================================================
1141
+
1142
+ export const DEFAULT_DEMA_PERIOD = 20
1143
+
1144
+ export function calcDEMAData(data: KLineData[], period: number): (number | undefined)[] {
1145
+ const n = data.length
1146
+ const result: (number | undefined)[] = new Array(n).fill(undefined)
1147
+ if (n === 0 || period <= 0) return result
1148
+
1149
+ const closes = new Array<number | undefined>(n)
1150
+ for (let i = 0; i < n; i++) closes[i] = data[i]!.close
1151
+
1152
+ const ema1 = _computeEMASeries(closes, period)
1153
+ const ema2 = _computeEMASeries(ema1, period)
1154
+
1155
+ for (let i = 0; i < n; i++) {
1156
+ const e1 = ema1[i]
1157
+ const e2 = ema2[i]
1158
+ if (e1 === undefined || e2 === undefined) continue
1159
+ result[i] = 2 * e1 - e2
1160
+ }
1161
+ return result
1162
+ }
1163
+
1164
+ export function calcDEMADataSoA(layout: KLineSoALayout, period: number): (number | undefined)[] {
1165
+ const data = SharedKLineBuffer.toKLineData(layout)
1166
+ return calcDEMAData(data, period)
1167
+ }
1168
+
1169
+ // ============================================================================
1170
+ // TEMA — Triple Exponential Moving Average
1171
+ // 公式: TEMA(t) = 3*EMA(t) - 3*EMA(EMA)(t) + EMA(EMA(EMA))(t)
1172
+ // 性质: 对二次多项式输入零滞后(稳态),warmup ~ 3*(period-1)
1173
+ // ============================================================================
1174
+
1175
+ export const DEFAULT_TEMA_PERIOD = 20
1176
+
1177
+ export function calcTEMAData(data: KLineData[], period: number): (number | undefined)[] {
1178
+ const n = data.length
1179
+ const result: (number | undefined)[] = new Array(n).fill(undefined)
1180
+ if (n === 0 || period <= 0) return result
1181
+
1182
+ const closes = new Array<number | undefined>(n)
1183
+ for (let i = 0; i < n; i++) closes[i] = data[i]!.close
1184
+
1185
+ const ema1 = _computeEMASeries(closes, period)
1186
+ const ema2 = _computeEMASeries(ema1, period)
1187
+ const ema3 = _computeEMASeries(ema2, period)
1188
+
1189
+ for (let i = 0; i < n; i++) {
1190
+ const e1 = ema1[i]
1191
+ const e2 = ema2[i]
1192
+ const e3 = ema3[i]
1193
+ if (e1 === undefined || e2 === undefined || e3 === undefined) continue
1194
+ result[i] = 3 * e1 - 3 * e2 + e3
1195
+ }
1196
+ return result
1197
+ }
1198
+
1199
+ export function calcTEMADataSoA(layout: KLineSoALayout, period: number): (number | undefined)[] {
1200
+ const data = SharedKLineBuffer.toKLineData(layout)
1201
+ return calcTEMAData(data, period)
1202
+ }
1203
+
1204
+ // ============================================================================
1205
+ // HMA — Hull Moving Average
1206
+ // 公式: HMA(n) = WMA( 2*WMA(close, n/2) - WMA(close, n), sqrt(n) )
1207
+ // 性质: 平滑性高于 WMA,滞后远低于同期 SMA
1208
+ // warmup ≈ period - 1 + round(sqrt(period)) - 1
1209
+ // ============================================================================
1210
+
1211
+ export const DEFAULT_HMA_PERIOD = 9
1212
+
1213
+ export function calcHMAData(data: KLineData[], period: number): (number | undefined)[] {
1214
+ const n = data.length
1215
+ const result: (number | undefined)[] = new Array(n).fill(undefined)
1216
+ if (n === 0 || period <= 0) return result
1217
+
1218
+ const closes = new Array<number | undefined>(n)
1219
+ for (let i = 0; i < n; i++) closes[i] = data[i]!.close
1220
+
1221
+ const halfPeriod = Math.max(1, Math.floor(period / 2))
1222
+ const sqrtPeriod = Math.max(1, Math.round(Math.sqrt(period)))
1223
+
1224
+ const wmaHalf = _computeWMAOnNumbers(closes, halfPeriod)
1225
+ const wmaFull = _computeWMAOnNumbers(closes, period)
1226
+
1227
+ const raw: (number | undefined)[] = new Array(n).fill(undefined)
1228
+ for (let i = 0; i < n; i++) {
1229
+ const h = wmaHalf[i]
1230
+ const f = wmaFull[i]
1231
+ if (h === undefined || f === undefined) continue
1232
+ raw[i] = 2 * h - f
1233
+ }
1234
+ return _computeWMAOnNumbers(raw, sqrtPeriod)
1235
+ }
1236
+
1237
+ export function calcHMADataSoA(layout: KLineSoALayout, period: number): (number | undefined)[] {
1238
+ const data = SharedKLineBuffer.toKLineData(layout)
1239
+ return calcHMAData(data, period)
1240
+ }
1241
+
1242
+ // ============================================================================
1243
+ // KAMA — Kaufman's Adaptive Moving Average
1244
+ // 自适应:在趋势强时跟得紧(接近 fast EMA),在震荡时跟得慢(接近 slow EMA)。
1245
+ // ER (efficiency ratio) = |close[t] - close[t-n]| / sum(|close[i] - close[i-1]|, i=t-n+1..t)
1246
+ // SC = (ER * (2/(fast+1) - 2/(slow+1)) + 2/(slow+1))^2
1247
+ // KAMA(t) = KAMA(t-1) + SC * (close[t] - KAMA(t-1))
1248
+ // 种子 KAMA(n-1) = close[n-1](或 SMA(n);这里采用 close 种子以保持与项目内 EMA 系列一致)
1249
+ // ============================================================================
1250
+
1251
+ export const DEFAULT_KAMA_PERIOD = 10
1252
+ export const DEFAULT_KAMA_FAST_PERIOD = 2
1253
+ export const DEFAULT_KAMA_SLOW_PERIOD = 30
1254
+
1255
+ export function calcKAMAData(
1256
+ data: KLineData[],
1257
+ period: number,
1258
+ fastPeriod: number,
1259
+ slowPeriod: number,
1260
+ ): (number | undefined)[] {
1261
+ const n = data.length
1262
+ const result: (number | undefined)[] = new Array(n).fill(undefined)
1263
+ if (n === 0 || period <= 0 || fastPeriod <= 0 || slowPeriod <= 0 || n <= period) return result
1264
+
1265
+ const fastSC = 2 / (fastPeriod + 1)
1266
+ const slowSC = 2 / (slowPeriod + 1)
1267
+ const scRange = fastSC - slowSC
1268
+
1269
+ // 维护滚动求和:sum(|close[i] - close[i-1]|, i=t-period+1..t)
1270
+ let volSum = 0
1271
+ for (let i = 1; i <= period; i++) {
1272
+ volSum += Math.abs(data[i]!.close - data[i - 1]!.close)
1273
+ }
1274
+
1275
+ let kama = data[period - 1]!.close
1276
+ result[period - 1] = kama
1277
+
1278
+ for (let t = period; t < n; t++) {
1279
+ const close = data[t]!.close
1280
+ const closeNPeriodsAgo = data[t - period]!.close
1281
+ const direction = Math.abs(close - closeNPeriodsAgo)
1282
+
1283
+ const er = volSum > 0 ? direction / volSum : 0
1284
+ const sc = (er * scRange + slowSC) ** 2
1285
+
1286
+ kama = kama + sc * (close - kama)
1287
+ result[t] = kama
1288
+
1289
+ // 滚动 volSum:减去最旧的 |close[t-period+1] - close[t-period]|,加上最新的 |close[t+1] - close[t]|
1290
+ if (t < n - 1) {
1291
+ volSum -= Math.abs(data[t - period + 1]!.close - data[t - period]!.close)
1292
+ volSum += Math.abs(data[t + 1]!.close - data[t]!.close)
1293
+ }
1294
+ }
1295
+
1296
+ return result
1297
+ }
1298
+
1299
+ export function calcKAMADataSoA(
1300
+ layout: KLineSoALayout,
1301
+ period: number,
1302
+ fastPeriod: number,
1303
+ slowPeriod: number,
1304
+ ): (number | undefined)[] {
1305
+ const data = SharedKLineBuffer.toKLineData(layout)
1306
+ return calcKAMAData(data, period, fastPeriod, slowPeriod)
1307
+ }
1308
+
1309
+ // ============================================================================
1310
+ // SAR — Parabolic Stop and Reverse
1311
+ // 经典 Wilder 公式:SAR(t+1) = SAR(t) + AF * (EP - SAR(t)),AF 在每次创出新极端时 +step(上限 maxStep)
1312
+ // 趋势翻转条件:上升趋势中 SAR 越过 low(或反之)
1313
+ // 种子:从 bar[1] 起,初始 trend=up,SAR=low[0],EP=high[0],AF=step
1314
+ // 返回每根 K 线对应的 SAR 点(带方向)
1315
+ // ============================================================================
1316
+
1317
+ export interface SARPoint {
1318
+ value: number
1319
+ trend: 'up' | 'down'
1320
+ }
1321
+
1322
+ export const DEFAULT_SAR_STEP = 0.02
1323
+ export const DEFAULT_SAR_MAX_STEP = 0.2
1324
+
1325
+ export function calcSARData(
1326
+ data: KLineData[],
1327
+ step: number,
1328
+ maxStep: number,
1329
+ ): (SARPoint | undefined)[] {
1330
+ const n = data.length
1331
+ const result: (SARPoint | undefined)[] = new Array(n).fill(undefined)
1332
+ if (n < 2 || step <= 0 || maxStep <= 0) return result
1333
+
1334
+ let trend: 'up' | 'down' = data[1]!.close >= data[0]!.close ? 'up' : 'down'
1335
+ let sar = trend === 'up' ? data[0]!.low : data[0]!.high
1336
+ let ep = trend === 'up' ? data[0]!.high : data[0]!.low
1337
+ let af = step
1338
+
1339
+ result[0] = { value: sar, trend }
1340
+
1341
+ for (let t = 1; t < n; t++) {
1342
+ const bar = data[t]!
1343
+ // 先按当前趋势推进 SAR
1344
+ sar = sar + af * (ep - sar)
1345
+
1346
+ // 边界约束:SAR 不能穿透前两根 K 线的极端
1347
+ if (trend === 'up') {
1348
+ const cap1 = data[t - 1]!.low
1349
+ const cap2 = t >= 2 ? data[t - 2]!.low : cap1
1350
+ sar = Math.min(sar, cap1, cap2)
1351
+ } else {
1352
+ const cap1 = data[t - 1]!.high
1353
+ const cap2 = t >= 2 ? data[t - 2]!.high : cap1
1354
+ sar = Math.max(sar, cap1, cap2)
1355
+ }
1356
+
1357
+ // 检测翻转
1358
+ if (trend === 'up' && bar.low < sar) {
1359
+ trend = 'down'
1360
+ sar = ep
1361
+ ep = bar.low
1362
+ af = step
1363
+ } else if (trend === 'down' && bar.high > sar) {
1364
+ trend = 'up'
1365
+ sar = ep
1366
+ ep = bar.high
1367
+ af = step
1368
+ } else {
1369
+ // 同趋势:更新 EP / AF
1370
+ if (trend === 'up' && bar.high > ep) {
1371
+ ep = bar.high
1372
+ af = Math.min(af + step, maxStep)
1373
+ } else if (trend === 'down' && bar.low < ep) {
1374
+ ep = bar.low
1375
+ af = Math.min(af + step, maxStep)
1376
+ }
1377
+ }
1378
+
1379
+ result[t] = { value: sar, trend }
1380
+ }
1381
+
1382
+ return result
1383
+ }
1384
+
1385
+ export function calcSARDataSoA(
1386
+ layout: KLineSoALayout,
1387
+ step: number,
1388
+ maxStep: number,
1389
+ ): (SARPoint | undefined)[] {
1390
+ const data = SharedKLineBuffer.toKLineData(layout)
1391
+ return calcSARData(data, step, maxStep)
1392
+ }
1393
+
1394
+ // ============================================================================
1395
+ // SuperTrend — ATR-based trend-following stop/band
1396
+ // ============================================================================
1397
+
1398
+ export interface SuperTrendPoint {
1399
+ value: number
1400
+ trend: 'up' | 'down'
1401
+ }
1402
+
1403
+ export const DEFAULT_SUPERTREND_ATR_PERIOD = 10
1404
+ export const DEFAULT_SUPERTREND_MULTIPLIER = 3
1405
+
1406
+ export function calcSuperTrendData(
1407
+ data: KLineData[],
1408
+ atrPeriod: number,
1409
+ multiplier: number,
1410
+ ): (SuperTrendPoint | undefined)[] {
1411
+ const n = data.length
1412
+ const result: (SuperTrendPoint | undefined)[] = new Array(n).fill(undefined)
1413
+ if (n === 0 || atrPeriod <= 0 || multiplier <= 0) return result
1414
+
1415
+ const atr = calcATRData(data, atrPeriod)
1416
+
1417
+ let trend: 'up' | 'down' = 'up'
1418
+ let prevUpper = Infinity
1419
+ let prevLower = -Infinity
1420
+
1421
+ for (let t = 0; t < n; t++) {
1422
+ const bar = data[t]!
1423
+ const a = atr[t]
1424
+ if (a === undefined) continue
1425
+
1426
+ const hl2 = (bar.high + bar.low) / 2
1427
+ const upperBasic = hl2 + multiplier * a
1428
+ const lowerBasic = hl2 - multiplier * a
1429
+
1430
+ // Smoothing: keep the previous band unless price has broken through it
1431
+ const prevClose = t > 0 ? data[t - 1]!.close : bar.close
1432
+ const upper = (upperBasic < prevUpper || prevClose > prevUpper) ? upperBasic : prevUpper
1433
+ const lower = (lowerBasic > prevLower || prevClose < prevLower) ? lowerBasic : prevLower
1434
+
1435
+ // Trend update
1436
+ if (trend === 'up' && bar.close < lower) {
1437
+ trend = 'down'
1438
+ } else if (trend === 'down' && bar.close > upper) {
1439
+ trend = 'up'
1440
+ }
1441
+
1442
+ result[t] = { value: trend === 'up' ? lower : upper, trend }
1443
+
1444
+ prevUpper = upper
1445
+ prevLower = lower
1446
+ }
1447
+
1448
+ return result
1449
+ }
1450
+
1451
+ export function calcSuperTrendDataSoA(
1452
+ layout: KLineSoALayout,
1453
+ atrPeriod: number,
1454
+ multiplier: number,
1455
+ ): (SuperTrendPoint | undefined)[] {
1456
+ const data = SharedKLineBuffer.toKLineData(layout)
1457
+ return calcSuperTrendData(data, atrPeriod, multiplier)
1458
+ }
1459
+
1460
+ // ============================================================================
1461
+ // Keltner Channel — EMA ± multiplier × ATR
1462
+ // ============================================================================
1463
+
1464
+ export interface KeltnerPoint {
1465
+ upper: number
1466
+ middle: number
1467
+ lower: number
1468
+ }
1469
+
1470
+ export const DEFAULT_KELTNER_EMA_PERIOD = 20
1471
+ export const DEFAULT_KELTNER_ATR_PERIOD = 10
1472
+ export const DEFAULT_KELTNER_MULTIPLIER = 2
1473
+
1474
+ export function calcKeltnerData(
1475
+ data: KLineData[],
1476
+ emaPeriod: number,
1477
+ atrPeriod: number,
1478
+ multiplier: number,
1479
+ ): (KeltnerPoint | undefined)[] {
1480
+ const n = data.length
1481
+ const result: (KeltnerPoint | undefined)[] = new Array(n).fill(undefined)
1482
+ if (n === 0 || emaPeriod <= 0 || atrPeriod <= 0) return result
1483
+
1484
+ const closes = new Array<number | undefined>(n)
1485
+ for (let i = 0; i < n; i++) closes[i] = data[i]!.close
1486
+
1487
+ const ema = _computeEMASeries(closes, emaPeriod)
1488
+ const atr = calcATRData(data, atrPeriod)
1489
+
1490
+ for (let t = 0; t < n; t++) {
1491
+ const m = ema[t]
1492
+ const a = atr[t]
1493
+ if (m === undefined || a === undefined) continue
1494
+ result[t] = {
1495
+ upper: m + multiplier * a,
1496
+ middle: m,
1497
+ lower: m - multiplier * a,
1498
+ }
1499
+ }
1500
+ return result
1501
+ }
1502
+
1503
+ export function calcKeltnerDataSoA(
1504
+ layout: KLineSoALayout,
1505
+ emaPeriod: number,
1506
+ atrPeriod: number,
1507
+ multiplier: number,
1508
+ ): (KeltnerPoint | undefined)[] {
1509
+ const data = SharedKLineBuffer.toKLineData(layout)
1510
+ return calcKeltnerData(data, emaPeriod, atrPeriod, multiplier)
1511
+ }
1512
+
1513
+ // ============================================================================
1514
+ // Donchian Channel — rolling max(high) / min(low) over period
1515
+ // ============================================================================
1516
+
1517
+ export interface DonchianPoint {
1518
+ upper: number
1519
+ middle: number
1520
+ lower: number
1521
+ }
1522
+
1523
+ export const DEFAULT_DONCHIAN_PERIOD = 20
1524
+
1525
+ export function calcDonchianData(
1526
+ data: KLineData[],
1527
+ period: number,
1528
+ ): (DonchianPoint | undefined)[] {
1529
+ const n = data.length
1530
+ const result: (DonchianPoint | undefined)[] = new Array(n).fill(undefined)
1531
+ if (n === 0 || period <= 0 || n < period) return result
1532
+
1533
+ for (let t = period - 1; t < n; t++) {
1534
+ let hi = -Infinity
1535
+ let lo = Infinity
1536
+ for (let k = 0; k < period; k++) {
1537
+ const bar = data[t - k]!
1538
+ if (bar.high > hi) hi = bar.high
1539
+ if (bar.low < lo) lo = bar.low
1540
+ }
1541
+ result[t] = { upper: hi, middle: (hi + lo) / 2, lower: lo }
1542
+ }
1543
+ return result
1544
+ }
1545
+
1546
+ export function calcDonchianDataSoA(
1547
+ layout: KLineSoALayout,
1548
+ period: number,
1549
+ ): (DonchianPoint | undefined)[] {
1550
+ const data = SharedKLineBuffer.toKLineData(layout)
1551
+ return calcDonchianData(data, period)
1552
+ }
1553
+
1554
+ // ============================================================================
1555
+ // Ichimoku Kinko Hyo — 一目均衡表
1556
+ // 5 线 + 云图(spanA/B 前置位移构成):
1557
+ // tenkan(t) = (max(high[t-tenkanPeriod+1..t]) + min(low[t-tenkanPeriod+1..t])) / 2
1558
+ // kijun(t) = 同公式但用 kijunPeriod
1559
+ // spanA(t) = (tenkan(t-displacement) + kijun(t-displacement)) / 2 ← 前置 displacement
1560
+ // spanB(t) = 用 spanBPeriod 计算后再前置 displacement
1561
+ // chikou(t) = close(t+displacement) ← 后置 displacement
1562
+ // 注:不做未来云的延伸(输出长度 = data.length;最后 displacement 根没 spanA/B;前 displacement 根没 chikou)
1563
+ // ============================================================================
1564
+
1565
+ export interface IchimokuPoint {
1566
+ tenkan?: number
1567
+ kijun?: number
1568
+ spanA?: number
1569
+ spanB?: number
1570
+ chikou?: number
1571
+ }
1572
+
1573
+ export const DEFAULT_ICHIMOKU_TENKAN = 9
1574
+ export const DEFAULT_ICHIMOKU_KIJUN = 26
1575
+ export const DEFAULT_ICHIMOKU_SPAN_B = 52
1576
+ export const DEFAULT_ICHIMOKU_DISPLACEMENT = 26
1577
+
1578
+ function _rollingMidline(data: KLineData[], period: number): (number | undefined)[] {
1579
+ const n = data.length
1580
+ const result: (number | undefined)[] = new Array(n).fill(undefined)
1581
+ if (n < period || period <= 0) return result
1582
+ for (let t = period - 1; t < n; t++) {
1583
+ let hi = -Infinity
1584
+ let lo = Infinity
1585
+ for (let k = 0; k < period; k++) {
1586
+ const bar = data[t - k]!
1587
+ if (bar.high > hi) hi = bar.high
1588
+ if (bar.low < lo) lo = bar.low
1589
+ }
1590
+ result[t] = (hi + lo) / 2
1591
+ }
1592
+ return result
1593
+ }
1594
+
1595
+ export function calcIchimokuData(
1596
+ data: KLineData[],
1597
+ tenkanPeriod: number,
1598
+ kijunPeriod: number,
1599
+ spanBPeriod: number,
1600
+ displacement: number,
1601
+ ): (IchimokuPoint | undefined)[] {
1602
+ const n = data.length
1603
+ const result: (IchimokuPoint | undefined)[] = new Array(n).fill(undefined)
1604
+ if (n === 0 || tenkanPeriod <= 0 || kijunPeriod <= 0 || spanBPeriod <= 0) return result
1605
+
1606
+ const tenkan = _rollingMidline(data, tenkanPeriod)
1607
+ const kijun = _rollingMidline(data, kijunPeriod)
1608
+ const spanBSource = _rollingMidline(data, spanBPeriod)
1609
+
1610
+ for (let t = 0; t < n; t++) {
1611
+ const point: IchimokuPoint = {}
1612
+ if (tenkan[t] !== undefined) point.tenkan = tenkan[t]
1613
+ if (kijun[t] !== undefined) point.kijun = kijun[t]
1614
+
1615
+ // spanA / spanB 由 displacement 根之前的值填到当前槽位
1616
+ const src = t - displacement
1617
+ if (src >= 0) {
1618
+ if (tenkan[src] !== undefined && kijun[src] !== undefined) {
1619
+ point.spanA = (tenkan[src]! + kijun[src]!) / 2
1620
+ }
1621
+ if (spanBSource[src] !== undefined) {
1622
+ point.spanB = spanBSource[src]
1623
+ }
1624
+ }
1625
+
1626
+ // chikou:当前 close 后置 displacement 根(即存到 t - displacement 槽位上 close[t])
1627
+ // 这里改成:存当前槽位的 chikou = close[t + displacement],需 future 数据;不可用时 undefined
1628
+ const future = t + displacement
1629
+ if (future < n) point.chikou = data[future]!.close
1630
+
1631
+ result[t] = point
1632
+ }
1633
+
1634
+ return result
1635
+ }
1636
+
1637
+ export function calcIchimokuDataSoA(
1638
+ layout: KLineSoALayout,
1639
+ tenkanPeriod: number,
1640
+ kijunPeriod: number,
1641
+ spanBPeriod: number,
1642
+ displacement: number,
1643
+ ): (IchimokuPoint | undefined)[] {
1644
+ const data = SharedKLineBuffer.toKLineData(layout)
1645
+ return calcIchimokuData(data, tenkanPeriod, kijunPeriod, spanBPeriod, displacement)
1646
+ }
1647
+
1648
+ // ============================================================================
1649
+ // ROC — Rate of Change
1650
+ // ROC(t) = (close[t] - close[t-period]) / close[t-period] * 100
1651
+ // ============================================================================
1652
+
1653
+ export const DEFAULT_ROC_PERIOD = 12
1654
+
1655
+ export function calcROCData(data: KLineData[], period: number): (number | undefined)[] {
1656
+ const n = data.length
1657
+ const result: (number | undefined)[] = new Array(n).fill(undefined)
1658
+ if (n === 0 || period <= 0) return result
1659
+ for (let t = period; t < n; t++) {
1660
+ const prev = data[t - period]!.close
1661
+ if (prev === 0) continue
1662
+ result[t] = (data[t]!.close - prev) / prev * 100
1663
+ }
1664
+ return result
1665
+ }
1666
+
1667
+ export function calcROCDataSoA(layout: KLineSoALayout, period: number): (number | undefined)[] {
1668
+ const data = SharedKLineBuffer.toKLineData(layout)
1669
+ return calcROCData(data, period)
1670
+ }
1671
+
1672
+ // ============================================================================
1673
+ // TRIX — Triple Exponential Smoothing Oscillator
1674
+ // EMA3 = EMA(EMA(EMA(close, p), p), p)
1675
+ // TRIX(t) = (EMA3[t] - EMA3[t-1]) / EMA3[t-1] * 100
1676
+ // Signal(t) = EMA(TRIX, signalPeriod) —— 配合金叉/死叉
1677
+ // ============================================================================
1678
+
1679
+ export interface TRIXResult {
1680
+ series: (number | undefined)[]
1681
+ signalSeries: (number | undefined)[]
1682
+ }
1683
+
1684
+ export const DEFAULT_TRIX_PERIOD = 15
1685
+ export const DEFAULT_TRIX_SIGNAL_PERIOD = 9
1686
+
1687
+ export function calcTRIXData(
1688
+ data: KLineData[],
1689
+ period: number,
1690
+ signalPeriod: number,
1691
+ ): TRIXResult {
1692
+ const n = data.length
1693
+ const series: (number | undefined)[] = new Array(n).fill(undefined)
1694
+ const signalSeries: (number | undefined)[] = new Array(n).fill(undefined)
1695
+ if (n === 0 || period <= 0) return { series, signalSeries }
1696
+
1697
+ const closes = new Array<number | undefined>(n)
1698
+ for (let i = 0; i < n; i++) closes[i] = data[i]!.close
1699
+
1700
+ const ema1 = _computeEMASeries(closes, period)
1701
+ const ema2 = _computeEMASeries(ema1, period)
1702
+ const ema3 = _computeEMASeries(ema2, period)
1703
+
1704
+ for (let t = 1; t < n; t++) {
1705
+ const cur = ema3[t]
1706
+ const prev = ema3[t - 1]
1707
+ if (cur === undefined || prev === undefined || prev === 0) continue
1708
+ series[t] = (cur - prev) / prev * 100
1709
+ }
1710
+
1711
+ if (signalPeriod > 0) {
1712
+ const smoothed = _computeEMASeries(series, signalPeriod)
1713
+ for (let i = 0; i < n; i++) signalSeries[i] = smoothed[i]
1714
+ }
1715
+
1716
+ return { series, signalSeries }
1717
+ }
1718
+
1719
+ export function calcTRIXDataSoA(
1720
+ layout: KLineSoALayout,
1721
+ period: number,
1722
+ signalPeriod: number,
1723
+ ): TRIXResult {
1724
+ const data = SharedKLineBuffer.toKLineData(layout)
1725
+ return calcTRIXData(data, period, signalPeriod)
1726
+ }
1727
+
1728
+ // ============================================================================
1729
+ // HV — Historical Volatility (close-to-close log returns)
1730
+ // HV(t) = stdDev(log(close[i]/close[i-1]), i=t-period+1..t) * sqrt(annualization)
1731
+ // 输出年化波动率(百分比形式 × 100)
1732
+ // ============================================================================
1733
+
1734
+ export const DEFAULT_HV_PERIOD = 20
1735
+ export const DEFAULT_HV_ANNUALIZATION = 252
1736
+
1737
+ export function calcHVData(
1738
+ data: KLineData[],
1739
+ period: number,
1740
+ annualizationFactor: number,
1741
+ ): (number | undefined)[] {
1742
+ const n = data.length
1743
+ const result: (number | undefined)[] = new Array(n).fill(undefined)
1744
+ if (n < 2 || period <= 0 || annualizationFactor <= 0) return result
1745
+
1746
+ const logReturns: number[] = new Array(n)
1747
+ logReturns[0] = 0
1748
+ for (let t = 1; t < n; t++) {
1749
+ const prev = data[t - 1]!.close
1750
+ const cur = data[t]!.close
1751
+ logReturns[t] = (prev > 0 && cur > 0) ? Math.log(cur / prev) : 0
1752
+ }
1753
+
1754
+ const annScale = Math.sqrt(annualizationFactor)
1755
+ for (let t = period; t < n; t++) {
1756
+ let sum = 0
1757
+ for (let k = 1; k <= period; k++) sum += logReturns[t - period + k]!
1758
+ const mean = sum / period
1759
+ let varSum = 0
1760
+ for (let k = 1; k <= period; k++) {
1761
+ const diff = logReturns[t - period + k]! - mean
1762
+ varSum += diff * diff
1763
+ }
1764
+ const std = Math.sqrt(varSum / (period - 1 > 0 ? period - 1 : 1))
1765
+ result[t] = std * annScale * 100
1766
+ }
1767
+
1768
+ return result
1769
+ }
1770
+
1771
+ export function calcHVDataSoA(
1772
+ layout: KLineSoALayout,
1773
+ period: number,
1774
+ annualizationFactor: number,
1775
+ ): (number | undefined)[] {
1776
+ const data = SharedKLineBuffer.toKLineData(layout)
1777
+ return calcHVData(data, period, annualizationFactor)
1778
+ }
1779
+
1780
+ // ============================================================================
1781
+ // Parkinson Volatility — high-low range volatility
1782
+ // PV(t) = sqrt( (1/(4*ln(2))) * mean(ln(high[i]/low[i])^2) * annualization ) * 100
1783
+ // ============================================================================
1784
+
1785
+ export const DEFAULT_PARKINSON_PERIOD = 20
1786
+ export const DEFAULT_PARKINSON_ANNUALIZATION = 252
1787
+
1788
+ export function calcParkinsonData(
1789
+ data: KLineData[],
1790
+ period: number,
1791
+ annualizationFactor: number,
1792
+ ): (number | undefined)[] {
1793
+ const n = data.length
1794
+ const result: (number | undefined)[] = new Array(n).fill(undefined)
1795
+ if (n === 0 || period <= 0 || annualizationFactor <= 0 || n < period) return result
1796
+
1797
+ const factor = 1 / (4 * Math.log(2))
1798
+ const annScale = Math.sqrt(annualizationFactor)
1799
+
1800
+ const hlLogSq: number[] = new Array(n)
1801
+ for (let i = 0; i < n; i++) {
1802
+ const bar = data[i]!
1803
+ if (bar.high > 0 && bar.low > 0) {
1804
+ const ln = Math.log(bar.high / bar.low)
1805
+ hlLogSq[i] = ln * ln
1806
+ } else {
1807
+ hlLogSq[i] = 0
1808
+ }
1809
+ }
1810
+
1811
+ for (let t = period - 1; t < n; t++) {
1812
+ let sum = 0
1813
+ for (let k = 0; k < period; k++) sum += hlLogSq[t - k]!
1814
+ const mean = sum / period
1815
+ result[t] = Math.sqrt(factor * mean) * annScale * 100
1816
+ }
1817
+
1818
+ return result
1819
+ }
1820
+
1821
+ export function calcParkinsonDataSoA(
1822
+ layout: KLineSoALayout,
1823
+ period: number,
1824
+ annualizationFactor: number,
1825
+ ): (number | undefined)[] {
1826
+ const data = SharedKLineBuffer.toKLineData(layout)
1827
+ return calcParkinsonData(data, period, annualizationFactor)
1828
+ }
1829
+
1830
+ // ============================================================================
1831
+ // Chaikin Volatility — EMA(high-low) 的 ROC
1832
+ // ChaikinVol(t) = (EMA(H-L, p)[t] - EMA(H-L, p)[t-rocPeriod]) / EMA(H-L, p)[t-rocPeriod] * 100
1833
+ // ============================================================================
1834
+
1835
+ export const DEFAULT_CHAIKIN_VOL_EMA_PERIOD = 10
1836
+ export const DEFAULT_CHAIKIN_VOL_ROC_PERIOD = 10
1837
+
1838
+ export function calcChaikinVolData(
1839
+ data: KLineData[],
1840
+ emaPeriod: number,
1841
+ rocPeriod: number,
1842
+ ): (number | undefined)[] {
1843
+ const n = data.length
1844
+ const result: (number | undefined)[] = new Array(n).fill(undefined)
1845
+ if (n === 0 || emaPeriod <= 0 || rocPeriod <= 0) return result
1846
+
1847
+ const hl: (number | undefined)[] = new Array(n)
1848
+ for (let i = 0; i < n; i++) hl[i] = data[i]!.high - data[i]!.low
1849
+
1850
+ const emaSeries = _computeEMASeries(hl, emaPeriod)
1851
+
1852
+ for (let t = rocPeriod; t < n; t++) {
1853
+ const cur = emaSeries[t]
1854
+ const prev = emaSeries[t - rocPeriod]
1855
+ if (cur === undefined || prev === undefined || prev === 0) continue
1856
+ result[t] = (cur - prev) / prev * 100
1857
+ }
1858
+
1859
+ return result
1860
+ }
1861
+
1862
+ export function calcChaikinVolDataSoA(
1863
+ layout: KLineSoALayout,
1864
+ emaPeriod: number,
1865
+ rocPeriod: number,
1866
+ ): (number | undefined)[] {
1867
+ const data = SharedKLineBuffer.toKLineData(layout)
1868
+ return calcChaikinVolData(data, emaPeriod, rocPeriod)
1869
+ }
1870
+
1871
+ // ============================================================================
1872
+ // VMA — Volume Moving Average (SMA of volume)
1873
+ // ============================================================================
1874
+
1875
+ export const DEFAULT_VMA_PERIOD = 5
1876
+
1877
+ export function calcVMAData(data: KLineData[], period: number): (number | undefined)[] {
1878
+ const n = data.length
1879
+ const result: (number | undefined)[] = new Array(n).fill(undefined)
1880
+ if (n === 0 || period <= 0 || n < period) return result
1881
+ let sum = 0
1882
+ for (let i = 0; i < period; i++) sum += data[i]!.volume ?? 0
1883
+ result[period - 1] = sum / period
1884
+ for (let t = period; t < n; t++) {
1885
+ sum += (data[t]!.volume ?? 0) - (data[t - period]!.volume ?? 0)
1886
+ result[t] = sum / period
1887
+ }
1888
+ return result
1889
+ }
1890
+
1891
+ export function calcVMADataSoA(layout: KLineSoALayout, period: number): (number | undefined)[] {
1892
+ const data = SharedKLineBuffer.toKLineData(layout)
1893
+ return calcVMAData(data, period)
1894
+ }
1895
+
1896
+ // ============================================================================
1897
+ // OBV — On Balance Volume (cumulative)
1898
+ // close[t] > close[t-1] → OBV += volume[t]
1899
+ // close[t] < close[t-1] → OBV -= volume[t]
1900
+ // else → OBV unchanged
1901
+ // ============================================================================
1902
+
1903
+ export function calcOBVData(data: KLineData[]): (number | undefined)[] {
1904
+ const n = data.length
1905
+ const result: (number | undefined)[] = new Array(n).fill(undefined)
1906
+ if (n === 0) return result
1907
+ let obv = 0
1908
+ result[0] = 0
1909
+ for (let t = 1; t < n; t++) {
1910
+ const cur = data[t]!
1911
+ const prev = data[t - 1]!
1912
+ if (cur.close > prev.close) obv += cur.volume ?? 0
1913
+ else if (cur.close < prev.close) obv -= cur.volume ?? 0
1914
+ result[t] = obv
1915
+ }
1916
+ return result
1917
+ }
1918
+
1919
+ export function calcOBVDataSoA(layout: KLineSoALayout): (number | undefined)[] {
1920
+ const data = SharedKLineBuffer.toKLineData(layout)
1921
+ return calcOBVData(data)
1922
+ }
1923
+
1924
+ // ============================================================================
1925
+ // PVT — Price Volume Trend (cumulative)
1926
+ // PVT(t) = PVT(t-1) + ((close[t] - close[t-1]) / close[t-1]) * volume[t]
1927
+ // ============================================================================
1928
+
1929
+ export function calcPVTData(data: KLineData[]): (number | undefined)[] {
1930
+ const n = data.length
1931
+ const result: (number | undefined)[] = new Array(n).fill(undefined)
1932
+ if (n === 0) return result
1933
+ let pvt = 0
1934
+ result[0] = 0
1935
+ for (let t = 1; t < n; t++) {
1936
+ const prevClose = data[t - 1]!.close
1937
+ if (prevClose === 0) {
1938
+ result[t] = pvt
1939
+ continue
1940
+ }
1941
+ pvt += ((data[t]!.close - prevClose) / prevClose) * (data[t]!.volume ?? 0)
1942
+ result[t] = pvt
1943
+ }
1944
+ return result
1945
+ }
1946
+
1947
+ export function calcPVTDataSoA(layout: KLineSoALayout): (number | undefined)[] {
1948
+ const data = SharedKLineBuffer.toKLineData(layout)
1949
+ return calcPVTData(data)
1950
+ }
1951
+
1952
+ // ============================================================================
1953
+ // VWAP — Volume-Weighted Average Price
1954
+ // VWAP(t) = sum_{i in session} TP(i) * V(i) / sum_{i in session} V(i)
1955
+ // where TP(i) = (H+L+C)/3 (typical price)
1956
+ // Session reset: if sessionResetGapMs > 0, reset cumulative sums when the gap
1957
+ // between consecutive bar timestamps exceeds this value (e.g., overnight)
1958
+ // ============================================================================
1959
+
1960
+ export const DEFAULT_VWAP_SESSION_GAP_MS = 0
1961
+
1962
+ export function calcVWAPData(
1963
+ data: KLineData[],
1964
+ sessionResetGapMs: number,
1965
+ ): (number | undefined)[] {
1966
+ const n = data.length
1967
+ const result: (number | undefined)[] = new Array(n).fill(undefined)
1968
+ if (n === 0) return result
1969
+
1970
+ let cumPV = 0
1971
+ let cumV = 0
1972
+ let prevTs = data[0]!.timestamp
1973
+
1974
+ for (let t = 0; t < n; t++) {
1975
+ const bar = data[t]!
1976
+ if (sessionResetGapMs > 0 && t > 0 && bar.timestamp - prevTs > sessionResetGapMs) {
1977
+ cumPV = 0
1978
+ cumV = 0
1979
+ }
1980
+ const tp = (bar.high + bar.low + bar.close) / 3
1981
+ cumPV += tp * (bar.volume ?? 0)
1982
+ cumV += bar.volume ?? 0
1983
+ result[t] = cumV > 0 ? cumPV / cumV : tp
1984
+ prevTs = bar.timestamp
1985
+ }
1986
+
1987
+ return result
1988
+ }
1989
+
1990
+ export function calcVWAPDataSoA(
1991
+ layout: KLineSoALayout,
1992
+ sessionResetGapMs: number,
1993
+ ): (number | undefined)[] {
1994
+ const data = SharedKLineBuffer.toKLineData(layout)
1995
+ return calcVWAPData(data, sessionResetGapMs)
1996
+ }
1997
+
1998
+ // ============================================================================
1999
+ // CMF — Chaikin Money Flow
2000
+ // MFM = ((C-L) - (H-C)) / (H-L) ∈ [-1, 1]
2001
+ // MFV = MFM * Volume
2002
+ // CMF(t) = sum(MFV[t-period+1..t]) / sum(Volume[t-period+1..t]) ∈ [-1, 1]
2003
+ // ============================================================================
2004
+
2005
+ export const DEFAULT_CMF_PERIOD = 20
2006
+
2007
+ export function calcCMFData(data: KLineData[], period: number): (number | undefined)[] {
2008
+ const n = data.length
2009
+ const result: (number | undefined)[] = new Array(n).fill(undefined)
2010
+ if (n === 0 || period <= 0 || n < period) return result
2011
+
2012
+ const mfv: number[] = new Array(n)
2013
+ for (let i = 0; i < n; i++) {
2014
+ const bar = data[i]!
2015
+ const range = bar.high - bar.low
2016
+ const mfm = range > 0 ? ((bar.close - bar.low) - (bar.high - bar.close)) / range : 0
2017
+ mfv[i] = mfm * (bar.volume ?? 0)
2018
+ }
2019
+
2020
+ let sumMFV = 0
2021
+ let sumV = 0
2022
+ for (let i = 0; i < period; i++) {
2023
+ sumMFV += mfv[i]!
2024
+ sumV += data[i]!.volume ?? 0
2025
+ }
2026
+ result[period - 1] = sumV > 0 ? sumMFV / sumV : 0
2027
+
2028
+ for (let t = period; t < n; t++) {
2029
+ sumMFV += mfv[t]! - mfv[t - period]!
2030
+ sumV += (data[t]!.volume ?? 0) - (data[t - period]!.volume ?? 0)
2031
+ result[t] = sumV > 0 ? sumMFV / sumV : 0
2032
+ }
2033
+ return result
2034
+ }
2035
+
2036
+ export function calcCMFDataSoA(layout: KLineSoALayout, period: number): (number | undefined)[] {
2037
+ const data = SharedKLineBuffer.toKLineData(layout)
2038
+ return calcCMFData(data, period)
2039
+ }
2040
+
2041
+ // ============================================================================
2042
+ // MFI — Money Flow Index
2043
+ // TP = (H+L+C)/3, RMF = TP * Volume
2044
+ // PMF = sum of RMF where TP > TP[-1]; NMF = sum where TP < TP[-1]
2045
+ // MFR = PMF / NMF; MFI = 100 - 100 / (1 + MFR) ∈ [0, 100]
2046
+ // ============================================================================
2047
+
2048
+ export const DEFAULT_MFI_PERIOD = 14
2049
+
2050
+ export function calcMFIData(data: KLineData[], period: number): (number | undefined)[] {
2051
+ const n = data.length
2052
+ const result: (number | undefined)[] = new Array(n).fill(undefined)
2053
+ if (n < period + 1 || period <= 0) return result
2054
+
2055
+ const tp: number[] = new Array(n)
2056
+ for (let i = 0; i < n; i++) tp[i] = (data[i]!.high + data[i]!.low + data[i]!.close) / 3
2057
+
2058
+ // Pre-classified positive/negative money flow per bar
2059
+ const pmfArr: number[] = new Array(n)
2060
+ const nmfArr: number[] = new Array(n)
2061
+ pmfArr[0] = 0
2062
+ nmfArr[0] = 0
2063
+ for (let i = 1; i < n; i++) {
2064
+ const rmf = tp[i]! * (data[i]!.volume ?? 0)
2065
+ if (tp[i]! > tp[i - 1]!) {
2066
+ pmfArr[i] = rmf
2067
+ nmfArr[i] = 0
2068
+ } else if (tp[i]! < tp[i - 1]!) {
2069
+ pmfArr[i] = 0
2070
+ nmfArr[i] = rmf
2071
+ } else {
2072
+ pmfArr[i] = 0
2073
+ nmfArr[i] = 0
2074
+ }
2075
+ }
2076
+
2077
+ let pSum = 0
2078
+ let nSum = 0
2079
+ for (let i = 1; i <= period; i++) {
2080
+ pSum += pmfArr[i]!
2081
+ nSum += nmfArr[i]!
2082
+ }
2083
+ result[period] = nSum > 0 ? 100 - 100 / (1 + pSum / nSum) : 100
2084
+
2085
+ for (let t = period + 1; t < n; t++) {
2086
+ pSum += pmfArr[t]! - pmfArr[t - period]!
2087
+ nSum += nmfArr[t]! - nmfArr[t - period]!
2088
+ result[t] = nSum > 0 ? 100 - 100 / (1 + pSum / nSum) : 100
2089
+ }
2090
+ return result
2091
+ }
2092
+
2093
+ export function calcMFIDataSoA(layout: KLineSoALayout, period: number): (number | undefined)[] {
2094
+ const data = SharedKLineBuffer.toKLineData(layout)
2095
+ return calcMFIData(data, period)
2096
+ }
2097
+
2098
+ // ============================================================================
2099
+ // Pivot Points — Classic floor-trader pivots from prior bar's HLC
2100
+ // PP = (H + L + C) / 3
2101
+ // R1 = 2·PP - L; S1 = 2·PP - H
2102
+ // R2 = PP + (H - L); S2 = PP - (H - L)
2103
+ // R3 = H + 2·(PP - L); S3 = L - 2·(H - PP)
2104
+ // Each bar t (t >= 1) shows pivots derived from bar[t-1]'s HLC.
2105
+ // ============================================================================
2106
+
2107
+ export interface PivotPoint {
2108
+ pp: number
2109
+ r1: number
2110
+ r2: number
2111
+ r3: number
2112
+ s1: number
2113
+ s2: number
2114
+ s3: number
2115
+ }
2116
+
2117
+ export function calcPivotData(data: KLineData[]): (PivotPoint | undefined)[] {
2118
+ const n = data.length
2119
+ const result: (PivotPoint | undefined)[] = new Array(n).fill(undefined)
2120
+ if (n < 2) return result
2121
+ for (let t = 1; t < n; t++) {
2122
+ const p = data[t - 1]!
2123
+ const pp = (p.high + p.low + p.close) / 3
2124
+ const range = p.high - p.low
2125
+ result[t] = {
2126
+ pp,
2127
+ r1: 2 * pp - p.low,
2128
+ s1: 2 * pp - p.high,
2129
+ r2: pp + range,
2130
+ s2: pp - range,
2131
+ r3: p.high + 2 * (pp - p.low),
2132
+ s3: p.low - 2 * (p.high - pp),
2133
+ }
2134
+ }
2135
+ return result
2136
+ }
2137
+
2138
+ export function calcPivotDataSoA(layout: KLineSoALayout): (PivotPoint | undefined)[] {
2139
+ const data = SharedKLineBuffer.toKLineData(layout)
2140
+ return calcPivotData(data)
2141
+ }
2142
+
2143
+ // ============================================================================
2144
+ // Fibonacci Retracement — anchored to rolling-window high/low
2145
+ // Window = last `period` bars. High = max(high), Low = min(low).
2146
+ // Direction = 'up' if the most recent extreme is the high (price moved up);
2147
+ // 'down' otherwise. Retracement levels computed accordingly.
2148
+ // ============================================================================
2149
+
2150
+ export interface FibPoint {
2151
+ high: number
2152
+ low: number
2153
+ direction: 'up' | 'down'
2154
+ level236: number
2155
+ level382: number
2156
+ level500: number
2157
+ level618: number
2158
+ level786: number
2159
+ }
2160
+
2161
+ export const DEFAULT_FIB_PERIOD = 50
2162
+
2163
+ export function calcFibData(data: KLineData[], period: number): (FibPoint | undefined)[] {
2164
+ const n = data.length
2165
+ const result: (FibPoint | undefined)[] = new Array(n).fill(undefined)
2166
+ if (n === 0 || period <= 0 || n < period) return result
2167
+
2168
+ for (let t = period - 1; t < n; t++) {
2169
+ let hi = -Infinity
2170
+ let lo = Infinity
2171
+ let hiIdx = t
2172
+ let loIdx = t
2173
+ for (let k = 0; k < period; k++) {
2174
+ const bar = data[t - k]!
2175
+ if (bar.high > hi) { hi = bar.high; hiIdx = t - k }
2176
+ if (bar.low < lo) { lo = bar.low; loIdx = t - k }
2177
+ }
2178
+ const direction: 'up' | 'down' = hiIdx >= loIdx ? 'up' : 'down'
2179
+ const range = hi - lo
2180
+ // For uptrend retracements: 0% at high, 100% at low; price retraces FROM high TOWARD low
2181
+ // For downtrend: 0% at low, 100% at high
2182
+ const level = (frac: number) =>
2183
+ direction === 'up' ? hi - range * frac : lo + range * frac
2184
+ result[t] = {
2185
+ high: hi,
2186
+ low: lo,
2187
+ direction,
2188
+ level236: level(0.236),
2189
+ level382: level(0.382),
2190
+ level500: level(0.5),
2191
+ level618: level(0.618),
2192
+ level786: level(0.786),
2193
+ }
2194
+ }
2195
+ return result
2196
+ }
2197
+
2198
+ export function calcFibDataSoA(layout: KLineSoALayout, period: number): (FibPoint | undefined)[] {
2199
+ const data = SharedKLineBuffer.toKLineData(layout)
2200
+ return calcFibData(data, period)
2201
+ }
2202
+
2203
+ // ============================================================================
2204
+ // SMC Structure — Swing detection + BOS / CHOCH events
2205
+ // Window-based swing fractal (default left=right=2 = Bill Williams fractal).
2206
+ // Swing confirmed when right-window bars have closed after the extremum.
2207
+ // Trend state machine: HH+HL = up, LL+LH = down, mixed = range.
2208
+ // BOS = continuation break (close > last HH in up trend, or < last LL in down trend).
2209
+ // CHOCH = reversal break (against current trend).
2210
+ // ============================================================================
2211
+
2212
+ export interface SwingPoint {
2213
+ index: number
2214
+ price: number
2215
+ kind: 'high' | 'low'
2216
+ label: 'HH' | 'HL' | 'LH' | 'LL'
2217
+ confirmed: boolean
2218
+ }
2219
+
2220
+ export type StructureEventKind = 'BOS' | 'CHOCH'
2221
+
2222
+ export interface StructureEvent {
2223
+ kind: StructureEventKind
2224
+ index: number
2225
+ triggerPrice: number
2226
+ brokenLevel: number
2227
+ brokenSwingIndex: number
2228
+ direction: 'up' | 'down'
2229
+ }
2230
+
2231
+ export interface StructureSnapshot {
2232
+ swings: SwingPoint[]
2233
+ events: StructureEvent[]
2234
+ trend: 'up' | 'down' | 'range'
2235
+ }
2236
+
2237
+ export const DEFAULT_STRUCTURE_LEFT = 2
2238
+ export const DEFAULT_STRUCTURE_RIGHT = 2
2239
+
2240
+ export function calcStructureData(
2241
+ data: KLineData[],
2242
+ leftWindow: number,
2243
+ rightWindow: number,
2244
+ breakoutSource: 'close' | 'wick',
2245
+ ): StructureSnapshot {
2246
+ const n = data.length
2247
+ if (n === 0 || leftWindow < 0 || rightWindow < 0) {
2248
+ return { swings: [], events: [], trend: 'range' }
2249
+ }
2250
+
2251
+ const rawSwings: { index: number; price: number; kind: 'high' | 'low'; confirmed: boolean }[] = []
2252
+ for (let i = 0; i < n; i++) {
2253
+ const bar = data[i]!
2254
+ if (isExtremum(data, i, leftWindow, rightWindow, 'high')) {
2255
+ rawSwings.push({ index: i, price: bar.high, kind: 'high', confirmed: i + rightWindow < n })
2256
+ }
2257
+ if (isExtremum(data, i, leftWindow, rightWindow, 'low')) {
2258
+ rawSwings.push({ index: i, price: bar.low, kind: 'low', confirmed: i + rightWindow < n })
2259
+ }
2260
+ }
2261
+ rawSwings.sort((a, b) => a.index - b.index)
2262
+
2263
+ const swings: SwingPoint[] = []
2264
+ let lastHigh: { index: number; price: number } | null = null
2265
+ let lastLow: { index: number; price: number } | null = null
2266
+ for (const s of rawSwings) {
2267
+ let label: 'HH' | 'HL' | 'LH' | 'LL'
2268
+ if (s.kind === 'high') {
2269
+ label = lastHigh && s.price > lastHigh.price ? 'HH' : 'LH'
2270
+ lastHigh = { index: s.index, price: s.price }
2271
+ } else {
2272
+ label = lastLow && s.price > lastLow.price ? 'HL' : 'LL'
2273
+ lastLow = { index: s.index, price: s.price }
2274
+ }
2275
+ swings.push({ ...s, label })
2276
+ }
2277
+
2278
+ const events: StructureEvent[] = []
2279
+ let trend: 'up' | 'down' | 'range' = 'range'
2280
+ let lastSwingHigh: { index: number; price: number } | null = null
2281
+ let lastSwingLow: { index: number; price: number } | null = null
2282
+ const confirmedSwings = swings.filter((s) => s.confirmed)
2283
+ let swingCursor = 0
2284
+
2285
+ for (let t = 0; t < n; t++) {
2286
+ while (swingCursor < confirmedSwings.length && confirmedSwings[swingCursor]!.index + rightWindow <= t) {
2287
+ const s = confirmedSwings[swingCursor]!
2288
+ if (s.kind === 'high') lastSwingHigh = { index: s.index, price: s.price }
2289
+ else lastSwingLow = { index: s.index, price: s.price }
2290
+ swingCursor++
2291
+ }
2292
+
2293
+ const bar = data[t]!
2294
+ const upBreakPrice = breakoutSource === 'close' ? bar.close : bar.high
2295
+ const downBreakPrice = breakoutSource === 'close' ? bar.close : bar.low
2296
+
2297
+ if (lastSwingHigh && upBreakPrice > lastSwingHigh.price) {
2298
+ const kind: StructureEventKind = trend === 'down' ? 'CHOCH' : 'BOS'
2299
+ events.push({
2300
+ kind,
2301
+ index: t,
2302
+ triggerPrice: upBreakPrice,
2303
+ brokenLevel: lastSwingHigh.price,
2304
+ brokenSwingIndex: lastSwingHigh.index,
2305
+ direction: 'up',
2306
+ })
2307
+ trend = 'up'
2308
+ lastSwingHigh = null
2309
+ } else if (lastSwingLow && downBreakPrice < lastSwingLow.price) {
2310
+ const kind: StructureEventKind = trend === 'up' ? 'CHOCH' : 'BOS'
2311
+ events.push({
2312
+ kind,
2313
+ index: t,
2314
+ triggerPrice: downBreakPrice,
2315
+ brokenLevel: lastSwingLow.price,
2316
+ brokenSwingIndex: lastSwingLow.index,
2317
+ direction: 'down',
2318
+ })
2319
+ trend = 'down'
2320
+ lastSwingLow = null
2321
+ }
2322
+ }
2323
+
2324
+ return { swings, events, trend }
2325
+ }
2326
+
2327
+ function isExtremum(
2328
+ data: KLineData[],
2329
+ i: number,
2330
+ left: number,
2331
+ right: number,
2332
+ kind: 'high' | 'low',
2333
+ ): boolean {
2334
+ const n = data.length
2335
+ if (i < left || i + right >= n) return false
2336
+ const center = kind === 'high' ? data[i]!.high : data[i]!.low
2337
+ for (let k = 1; k <= left; k++) {
2338
+ const v = kind === 'high' ? data[i - k]!.high : data[i - k]!.low
2339
+ if (kind === 'high' ? v >= center : v <= center) return false
2340
+ }
2341
+ for (let k = 1; k <= right; k++) {
2342
+ const v = kind === 'high' ? data[i + k]!.high : data[i + k]!.low
2343
+ if (kind === 'high' ? v >= center : v <= center) return false
2344
+ }
2345
+ return true
2346
+ }
2347
+
2348
+ export function calcStructureDataSoA(
2349
+ layout: KLineSoALayout,
2350
+ leftWindow: number,
2351
+ rightWindow: number,
2352
+ breakoutSource: 'close' | 'wick',
2353
+ ): StructureSnapshot {
2354
+ const data = SharedKLineBuffer.toKLineData(layout)
2355
+ return calcStructureData(data, leftWindow, rightWindow, breakoutSource)
2356
+ }
2357
+
2358
+ // ============================================================================
2359
+ // SMC Zones — FVG (Fair Value Gap) + Order Blocks
2360
+ // FVG (3-bar pattern):
2361
+ // Bullish FVG: bar[t-2].high < bar[t].low → gap zone [bar[t-2].high, bar[t].low] anchored at bar[t-1]
2362
+ // Bearish FVG: bar[t-2].low > bar[t].high → gap zone [bar[t].high, bar[t-2].low] anchored at bar[t-1]
2363
+ // Zone is "filled" (endIndex set) when price re-enters it.
2364
+ // Order Blocks:
2365
+ // Computed in conjunction with BOS events from calcStructureData.
2366
+ // Bullish OB = last bearish candle (close < open) within obLookback bars before an upward BOS.
2367
+ // Bearish OB = last bullish candle (close > open) within obLookback bars before a downward BOS.
2368
+ // Mitigated (endIndex set) when price returns into the candle's range.
2369
+ // ============================================================================
2370
+
2371
+ export type ZoneKind = 'FVG_BULL' | 'FVG_BEAR' | 'OB_BULL' | 'OB_BEAR'
2372
+
2373
+ export interface Zone {
2374
+ kind: ZoneKind
2375
+ startIndex: number
2376
+ endIndex?: number
2377
+ high: number
2378
+ low: number
2379
+ }
2380
+
2381
+ export const DEFAULT_ZONES_OB_LOOKBACK = 5
2382
+
2383
+ export function calcZonesData(
2384
+ data: KLineData[],
2385
+ obLookback: number,
2386
+ structureLeftWindow: number,
2387
+ structureRightWindow: number,
2388
+ breakoutSource: 'close' | 'wick',
2389
+ ): Zone[] {
2390
+ const n = data.length
2391
+ if (n < 3) return []
2392
+ const zones: Zone[] = []
2393
+
2394
+ // 1. Detect FVGs
2395
+ for (let t = 2; t < n; t++) {
2396
+ const a = data[t - 2]!
2397
+ const c = data[t]!
2398
+ // Bullish FVG: a.high < c.low → gap
2399
+ if (a.high < c.low) {
2400
+ zones.push({
2401
+ kind: 'FVG_BULL',
2402
+ startIndex: t - 1,
2403
+ high: c.low,
2404
+ low: a.high,
2405
+ })
2406
+ }
2407
+ // Bearish FVG: a.low > c.high → gap
2408
+ if (a.low > c.high) {
2409
+ zones.push({
2410
+ kind: 'FVG_BEAR',
2411
+ startIndex: t - 1,
2412
+ high: a.low,
2413
+ low: c.high,
2414
+ })
2415
+ }
2416
+ }
2417
+
2418
+ // 2. Detect Order Blocks using structure BOS events
2419
+ const struct = calcStructureData(data, structureLeftWindow, structureRightWindow, breakoutSource)
2420
+ for (const ev of struct.events) {
2421
+ if (ev.kind !== 'BOS') continue
2422
+ // Look back obLookback bars for the OB candle
2423
+ const start = Math.max(0, ev.index - obLookback)
2424
+ if (ev.direction === 'up') {
2425
+ // Bullish OB: latest bearish candle (close < open) in [start, ev.index)
2426
+ for (let k = ev.index - 1; k >= start; k--) {
2427
+ const bar = data[k]!
2428
+ if (bar.close < bar.open) {
2429
+ zones.push({ kind: 'OB_BULL', startIndex: k, high: bar.high, low: bar.low })
2430
+ break
2431
+ }
2432
+ }
2433
+ } else {
2434
+ // Bearish OB: latest bullish candle (close > open) in [start, ev.index)
2435
+ for (let k = ev.index - 1; k >= start; k--) {
2436
+ const bar = data[k]!
2437
+ if (bar.close > bar.open) {
2438
+ zones.push({ kind: 'OB_BEAR', startIndex: k, high: bar.high, low: bar.low })
2439
+ break
2440
+ }
2441
+ }
2442
+ }
2443
+ }
2444
+
2445
+ // 3. Mark zones as filled when price re-enters their range
2446
+ for (const zone of zones) {
2447
+ for (let t = zone.startIndex + 1; t < n; t++) {
2448
+ const bar = data[t]!
2449
+ // Zone is touched if the bar overlaps the zone's [low, high]
2450
+ if (bar.low <= zone.high && bar.high >= zone.low) {
2451
+ zone.endIndex = t
2452
+ break
2453
+ }
2454
+ }
2455
+ }
2456
+
2457
+ return zones
2458
+ }
2459
+
2460
+ export function calcZonesDataSoA(
2461
+ layout: KLineSoALayout,
2462
+ obLookback: number,
2463
+ structureLeftWindow: number,
2464
+ structureRightWindow: number,
2465
+ breakoutSource: 'close' | 'wick',
2466
+ ): Zone[] {
2467
+ const data = SharedKLineBuffer.toKLineData(layout)
2468
+ return calcZonesData(data, obLookback, structureLeftWindow, structureRightWindow, breakoutSource)
2469
+ }
2470
+
2471
+ // ============================================================================
2472
+ // Volume Profile — price-bin volume distribution
2473
+ // For each bar, volume is distributed uniformly across [low, high] into bins.
2474
+ // Outputs: bins[], POC (max-volume bin center), VAH/VAL (value area boundaries).
2475
+ // Value area = contiguous bins around POC summing to valueAreaPercent of total V.
2476
+ // ============================================================================
2477
+
2478
+ export interface VolumeProfileBin {
2479
+ priceLow: number
2480
+ priceHigh: number
2481
+ volume: number
2482
+ }
2483
+
2484
+ export interface VolumeProfileResult {
2485
+ bins: VolumeProfileBin[]
2486
+ poc: number
2487
+ vah: number
2488
+ val: number
2489
+ totalVolume: number
2490
+ }
2491
+
2492
+ export const DEFAULT_VP_BINS = 24
2493
+ export const DEFAULT_VP_LOOKBACK = 0
2494
+ export const DEFAULT_VP_VALUE_AREA = 0.7
2495
+
2496
+ export function calcVolumeProfileData(
2497
+ data: KLineData[],
2498
+ bins: number,
2499
+ lookback: number,
2500
+ valueAreaPercent: number,
2501
+ ): VolumeProfileResult {
2502
+ const n = data.length
2503
+ if (n === 0 || bins <= 0) {
2504
+ return { bins: [], poc: 0, vah: 0, val: 0, totalVolume: 0 }
2505
+ }
2506
+
2507
+ const startIdx = lookback > 0 ? Math.max(0, n - lookback) : 0
2508
+ let priceMin = Infinity
2509
+ let priceMax = -Infinity
2510
+ for (let i = startIdx; i < n; i++) {
2511
+ const bar = data[i]!
2512
+ if (bar.low < priceMin) priceMin = bar.low
2513
+ if (bar.high > priceMax) priceMax = bar.high
2514
+ }
2515
+ if (!Number.isFinite(priceMin) || !Number.isFinite(priceMax) || priceMax <= priceMin) {
2516
+ return { bins: [], poc: priceMin, vah: priceMin, val: priceMin, totalVolume: 0 }
2517
+ }
2518
+
2519
+ const binWidth = (priceMax - priceMin) / bins
2520
+ const binVolumes: number[] = new Array(bins).fill(0)
2521
+
2522
+ // Distribute each bar's volume uniformly across the bins its [low, high] covers
2523
+ for (let i = startIdx; i < n; i++) {
2524
+ const bar = data[i]!
2525
+ const barRange = bar.high - bar.low
2526
+ if (barRange <= 0) {
2527
+ const binIdx = Math.min(bins - 1, Math.max(0, Math.floor((bar.close - priceMin) / binWidth)))
2528
+ binVolumes[binIdx]! += bar.volume ?? 0
2529
+ continue
2530
+ }
2531
+ const volPerPrice = (bar.volume ?? 0) / barRange
2532
+ const startBin = Math.max(0, Math.floor((bar.low - priceMin) / binWidth))
2533
+ const endBin = Math.min(bins - 1, Math.floor((bar.high - priceMin) / binWidth))
2534
+ for (let b = startBin; b <= endBin; b++) {
2535
+ const binLow = priceMin + b * binWidth
2536
+ const binHigh = binLow + binWidth
2537
+ const overlapLow = Math.max(bar.low, binLow)
2538
+ const overlapHigh = Math.min(bar.high, binHigh)
2539
+ const overlap = overlapHigh - overlapLow
2540
+ if (overlap > 0) {
2541
+ binVolumes[b]! += overlap * volPerPrice
2542
+ }
2543
+ }
2544
+ }
2545
+
2546
+ const binsArr: VolumeProfileBin[] = binVolumes.map((v, b) => ({
2547
+ priceLow: priceMin + b * binWidth,
2548
+ priceHigh: priceMin + (b + 1) * binWidth,
2549
+ volume: v,
2550
+ }))
2551
+
2552
+ // POC = max-volume bin center
2553
+ let pocBinIdx = 0
2554
+ for (let b = 1; b < bins; b++) {
2555
+ if (binVolumes[b]! > binVolumes[pocBinIdx]!) pocBinIdx = b
2556
+ }
2557
+ const poc = (binsArr[pocBinIdx]!.priceLow + binsArr[pocBinIdx]!.priceHigh) / 2
2558
+
2559
+ const totalVolume = binVolumes.reduce((a, b) => a + b, 0)
2560
+
2561
+ // Value Area: expand outward from POC until cumulative volume >= valueAreaPercent of total
2562
+ const target = totalVolume * valueAreaPercent
2563
+ let acc = binVolumes[pocBinIdx]!
2564
+ let lo = pocBinIdx
2565
+ let hi = pocBinIdx
2566
+ while (acc < target && (lo > 0 || hi < bins - 1)) {
2567
+ const loCand = lo > 0 ? binVolumes[lo - 1]! : -Infinity
2568
+ const hiCand = hi < bins - 1 ? binVolumes[hi + 1]! : -Infinity
2569
+ if (loCand >= hiCand && lo > 0) {
2570
+ lo--
2571
+ acc += binVolumes[lo]!
2572
+ } else if (hi < bins - 1) {
2573
+ hi++
2574
+ acc += binVolumes[hi]!
2575
+ } else {
2576
+ break
2577
+ }
2578
+ }
2579
+ const val = binsArr[lo]!.priceLow
2580
+ const vah = binsArr[hi]!.priceHigh
2581
+
2582
+ return { bins: binsArr, poc, vah, val, totalVolume }
2583
+ }
2584
+
2585
+ export function calcVolumeProfileDataSoA(
2586
+ layout: KLineSoALayout,
2587
+ bins: number,
2588
+ lookback: number,
2589
+ valueAreaPercent: number,
2590
+ ): VolumeProfileResult {
2591
+ const data = SharedKLineBuffer.toKLineData(layout)
2592
+ return calcVolumeProfileData(data, bins, lookback, valueAreaPercent)
2593
+ }