@363045841yyt/klinechart-core 0.7.9 → 0.7.10

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 (234) hide show
  1. package/dist/engine/chart.d.ts.map +1 -1
  2. package/dist/engine/chart.js +39 -229
  3. package/dist/engine/chart.js.map +1 -1
  4. package/dist/engine/indicators/indicatorDefinitionRegistry.d.ts +10 -3
  5. package/dist/engine/indicators/indicatorDefinitionRegistry.d.ts.map +1 -1
  6. package/dist/engine/indicators/indicatorDefinitionRegistry.js +29 -2
  7. package/dist/engine/indicators/indicatorDefinitionRegistry.js.map +1 -1
  8. package/dist/engine/indicators/indicatorMetadata.d.ts +79 -2
  9. package/dist/engine/indicators/indicatorMetadata.d.ts.map +1 -1
  10. package/dist/engine/indicators/indicatorMetadata.js.map +1 -1
  11. package/dist/engine/indicators/indicatorRegistry.d.ts +5 -0
  12. package/dist/engine/indicators/indicatorRegistry.d.ts.map +1 -1
  13. package/dist/engine/indicators/indicatorRegistry.js +49 -5
  14. package/dist/engine/indicators/indicatorRegistry.js.map +1 -1
  15. package/dist/engine/indicators/registerBuiltins.d.ts +42 -0
  16. package/dist/engine/indicators/registerBuiltins.d.ts.map +1 -0
  17. package/dist/engine/indicators/registerBuiltins.js +46 -0
  18. package/dist/engine/indicators/registerBuiltins.js.map +1 -0
  19. package/dist/engine/indicators/scheduler.d.ts +2 -2
  20. package/dist/engine/indicators/scheduler.d.ts.map +1 -1
  21. package/dist/engine/indicators/scheduler.js +41 -11
  22. package/dist/engine/indicators/scheduler.js.map +1 -1
  23. package/dist/engine/indicators/stateComposer.d.ts +6 -4
  24. package/dist/engine/indicators/stateComposer.d.ts.map +1 -1
  25. package/dist/engine/indicators/stateComposer.js +80 -984
  26. package/dist/engine/indicators/stateComposer.js.map +1 -1
  27. package/dist/engine/indicators/visibleStateComposers.d.ts +145 -0
  28. package/dist/engine/indicators/visibleStateComposers.d.ts.map +1 -0
  29. package/dist/engine/indicators/visibleStateComposers.js +469 -0
  30. package/dist/engine/indicators/visibleStateComposers.js.map +1 -0
  31. package/dist/engine/renderers/Indicator/atr.d.ts.map +1 -1
  32. package/dist/engine/renderers/Indicator/atr.js +8 -0
  33. package/dist/engine/renderers/Indicator/atr.js.map +1 -1
  34. package/dist/engine/renderers/Indicator/boll.d.ts.map +1 -1
  35. package/dist/engine/renderers/Indicator/boll.js +47 -0
  36. package/dist/engine/renderers/Indicator/boll.js.map +1 -1
  37. package/dist/engine/renderers/Indicator/cci.d.ts.map +1 -1
  38. package/dist/engine/renderers/Indicator/cci.js +8 -1
  39. package/dist/engine/renderers/Indicator/cci.js.map +1 -1
  40. package/dist/engine/renderers/Indicator/chaikinVol.d.ts.map +1 -1
  41. package/dist/engine/renderers/Indicator/chaikinVol.js +7 -1
  42. package/dist/engine/renderers/Indicator/chaikinVol.js.map +1 -1
  43. package/dist/engine/renderers/Indicator/cmf.d.ts.map +1 -1
  44. package/dist/engine/renderers/Indicator/cmf.js +7 -1
  45. package/dist/engine/renderers/Indicator/cmf.js.map +1 -1
  46. package/dist/engine/renderers/Indicator/dema.d.ts.map +1 -1
  47. package/dist/engine/renderers/Indicator/dema.js +8 -1
  48. package/dist/engine/renderers/Indicator/dema.js.map +1 -1
  49. package/dist/engine/renderers/Indicator/donchian.d.ts.map +1 -1
  50. package/dist/engine/renderers/Indicator/donchian.js +8 -1
  51. package/dist/engine/renderers/Indicator/donchian.js.map +1 -1
  52. package/dist/engine/renderers/Indicator/ene.d.ts.map +1 -1
  53. package/dist/engine/renderers/Indicator/ene.js +45 -0
  54. package/dist/engine/renderers/Indicator/ene.js.map +1 -1
  55. package/dist/engine/renderers/Indicator/expma.d.ts.map +1 -1
  56. package/dist/engine/renderers/Indicator/expma.js +45 -0
  57. package/dist/engine/renderers/Indicator/expma.js.map +1 -1
  58. package/dist/engine/renderers/Indicator/fastk.d.ts.map +1 -1
  59. package/dist/engine/renderers/Indicator/fastk.js +8 -1
  60. package/dist/engine/renderers/Indicator/fastk.js.map +1 -1
  61. package/dist/engine/renderers/Indicator/fib.d.ts.map +1 -1
  62. package/dist/engine/renderers/Indicator/fib.js +8 -1
  63. package/dist/engine/renderers/Indicator/fib.js.map +1 -1
  64. package/dist/engine/renderers/Indicator/hma.d.ts.map +1 -1
  65. package/dist/engine/renderers/Indicator/hma.js +8 -1
  66. package/dist/engine/renderers/Indicator/hma.js.map +1 -1
  67. package/dist/engine/renderers/Indicator/hv.d.ts.map +1 -1
  68. package/dist/engine/renderers/Indicator/hv.js +7 -0
  69. package/dist/engine/renderers/Indicator/hv.js.map +1 -1
  70. package/dist/engine/renderers/Indicator/ichimoku.d.ts.map +1 -1
  71. package/dist/engine/renderers/Indicator/ichimoku.js +8 -1
  72. package/dist/engine/renderers/Indicator/ichimoku.js.map +1 -1
  73. package/dist/engine/renderers/Indicator/index.d.ts +7 -2
  74. package/dist/engine/renderers/Indicator/index.d.ts.map +1 -1
  75. package/dist/engine/renderers/Indicator/index.js +2 -114
  76. package/dist/engine/renderers/Indicator/index.js.map +1 -1
  77. package/dist/engine/renderers/Indicator/kama.d.ts.map +1 -1
  78. package/dist/engine/renderers/Indicator/kama.js +8 -1
  79. package/dist/engine/renderers/Indicator/kama.js.map +1 -1
  80. package/dist/engine/renderers/Indicator/keltner.d.ts.map +1 -1
  81. package/dist/engine/renderers/Indicator/keltner.js +8 -1
  82. package/dist/engine/renderers/Indicator/keltner.js.map +1 -1
  83. package/dist/engine/renderers/Indicator/kst.d.ts.map +1 -1
  84. package/dist/engine/renderers/Indicator/kst.js +8 -0
  85. package/dist/engine/renderers/Indicator/kst.js.map +1 -1
  86. package/dist/engine/renderers/Indicator/ma.d.ts.map +1 -1
  87. package/dist/engine/renderers/Indicator/ma.js +63 -0
  88. package/dist/engine/renderers/Indicator/ma.js.map +1 -1
  89. package/dist/engine/renderers/Indicator/macd.d.ts.map +1 -1
  90. package/dist/engine/renderers/Indicator/macd.js +8 -1
  91. package/dist/engine/renderers/Indicator/macd.js.map +1 -1
  92. package/dist/engine/renderers/Indicator/mfi.d.ts.map +1 -1
  93. package/dist/engine/renderers/Indicator/mfi.js +7 -1
  94. package/dist/engine/renderers/Indicator/mfi.js.map +1 -1
  95. package/dist/engine/renderers/Indicator/mom.d.ts.map +1 -1
  96. package/dist/engine/renderers/Indicator/mom.js +8 -0
  97. package/dist/engine/renderers/Indicator/mom.js.map +1 -1
  98. package/dist/engine/renderers/Indicator/obv.d.ts.map +1 -1
  99. package/dist/engine/renderers/Indicator/obv.js +7 -1
  100. package/dist/engine/renderers/Indicator/obv.js.map +1 -1
  101. package/dist/engine/renderers/Indicator/parkinson.d.ts.map +1 -1
  102. package/dist/engine/renderers/Indicator/parkinson.js +7 -0
  103. package/dist/engine/renderers/Indicator/parkinson.js.map +1 -1
  104. package/dist/engine/renderers/Indicator/pivot.d.ts.map +1 -1
  105. package/dist/engine/renderers/Indicator/pivot.js +8 -1
  106. package/dist/engine/renderers/Indicator/pivot.js.map +1 -1
  107. package/dist/engine/renderers/Indicator/pvt.d.ts.map +1 -1
  108. package/dist/engine/renderers/Indicator/pvt.js +7 -1
  109. package/dist/engine/renderers/Indicator/pvt.js.map +1 -1
  110. package/dist/engine/renderers/Indicator/roc.d.ts.map +1 -1
  111. package/dist/engine/renderers/Indicator/roc.js +7 -1
  112. package/dist/engine/renderers/Indicator/roc.js.map +1 -1
  113. package/dist/engine/renderers/Indicator/rsi.d.ts.map +1 -1
  114. package/dist/engine/renderers/Indicator/rsi.js +8 -1
  115. package/dist/engine/renderers/Indicator/rsi.js.map +1 -1
  116. package/dist/engine/renderers/Indicator/sar.d.ts.map +1 -1
  117. package/dist/engine/renderers/Indicator/sar.js +8 -1
  118. package/dist/engine/renderers/Indicator/sar.js.map +1 -1
  119. package/dist/engine/renderers/Indicator/stoch.d.ts.map +1 -1
  120. package/dist/engine/renderers/Indicator/stoch.js +8 -1
  121. package/dist/engine/renderers/Indicator/stoch.js.map +1 -1
  122. package/dist/engine/renderers/Indicator/structure.d.ts.map +1 -1
  123. package/dist/engine/renderers/Indicator/structure.js +8 -1
  124. package/dist/engine/renderers/Indicator/structure.js.map +1 -1
  125. package/dist/engine/renderers/Indicator/subPaneConfig.d.ts +1 -1
  126. package/dist/engine/renderers/Indicator/subPaneConfig.d.ts.map +1 -1
  127. package/dist/engine/renderers/Indicator/subPaneConfig.js.map +1 -1
  128. package/dist/engine/renderers/Indicator/supertrend.d.ts.map +1 -1
  129. package/dist/engine/renderers/Indicator/supertrend.js +8 -1
  130. package/dist/engine/renderers/Indicator/supertrend.js.map +1 -1
  131. package/dist/engine/renderers/Indicator/tema.d.ts.map +1 -1
  132. package/dist/engine/renderers/Indicator/tema.js +8 -1
  133. package/dist/engine/renderers/Indicator/tema.js.map +1 -1
  134. package/dist/engine/renderers/Indicator/trix.d.ts.map +1 -1
  135. package/dist/engine/renderers/Indicator/trix.js +7 -0
  136. package/dist/engine/renderers/Indicator/trix.js.map +1 -1
  137. package/dist/engine/renderers/Indicator/vma.d.ts.map +1 -1
  138. package/dist/engine/renderers/Indicator/vma.js +7 -0
  139. package/dist/engine/renderers/Indicator/vma.js.map +1 -1
  140. package/dist/engine/renderers/Indicator/volumeProfile.d.ts.map +1 -1
  141. package/dist/engine/renderers/Indicator/volumeProfile.js +7 -1
  142. package/dist/engine/renderers/Indicator/volumeProfile.js.map +1 -1
  143. package/dist/engine/renderers/Indicator/vwap.d.ts.map +1 -1
  144. package/dist/engine/renderers/Indicator/vwap.js +7 -1
  145. package/dist/engine/renderers/Indicator/vwap.js.map +1 -1
  146. package/dist/engine/renderers/Indicator/wma.d.ts.map +1 -1
  147. package/dist/engine/renderers/Indicator/wma.js +8 -1
  148. package/dist/engine/renderers/Indicator/wma.js.map +1 -1
  149. package/dist/engine/renderers/Indicator/wmsr.d.ts.map +1 -1
  150. package/dist/engine/renderers/Indicator/wmsr.js +8 -1
  151. package/dist/engine/renderers/Indicator/wmsr.js.map +1 -1
  152. package/dist/engine/renderers/Indicator/zones.d.ts.map +1 -1
  153. package/dist/engine/renderers/Indicator/zones.js +8 -1
  154. package/dist/engine/renderers/Indicator/zones.js.map +1 -1
  155. package/dist/engine/renderers/extremaMarkers.d.ts.map +1 -1
  156. package/dist/engine/renderers/extremaMarkers.js +0 -22
  157. package/dist/engine/renderers/extremaMarkers.js.map +1 -1
  158. package/dist/engine/renderers/subVolume.d.ts.map +1 -1
  159. package/dist/engine/renderers/subVolume.js +3 -0
  160. package/dist/engine/renderers/subVolume.js.map +1 -1
  161. package/dist/engine/subPaneManager.d.ts +1 -0
  162. package/dist/engine/subPaneManager.d.ts.map +1 -1
  163. package/dist/engine/subPaneManager.js +30 -237
  164. package/dist/engine/subPaneManager.js.map +1 -1
  165. package/dist/semantic/controller.d.ts +9 -4
  166. package/dist/semantic/controller.d.ts.map +1 -1
  167. package/dist/semantic/controller.js +57 -53
  168. package/dist/semantic/controller.js.map +1 -1
  169. package/dist/version.d.ts +1 -1
  170. package/dist/version.d.ts.map +1 -1
  171. package/dist/version.js +1 -1
  172. package/dist/version.js.map +1 -1
  173. package/package.json +1 -1
  174. package/src/engine/chart.ts +49 -229
  175. package/src/engine/indicators/__tests__/indicatorDefinitionRegistry.test.ts +59 -0
  176. package/src/engine/indicators/__tests__/indicatorRegistry.test.ts +62 -0
  177. package/src/engine/indicators/__tests__/registerBuiltins.test.ts +380 -0
  178. package/src/engine/indicators/__tests__/scheduler.test.ts +62 -32
  179. package/src/engine/indicators/__tests__/stateComposer.test.ts +444 -0
  180. package/src/engine/indicators/indicatorDefinitionRegistry.ts +49 -4
  181. package/src/engine/indicators/indicatorMetadata.ts +107 -2
  182. package/src/engine/indicators/indicatorRegistry.ts +54 -5
  183. package/src/engine/indicators/registerBuiltins.ts +47 -0
  184. package/src/engine/indicators/scheduler.ts +61 -13
  185. package/src/engine/indicators/stateComposer.ts +110 -1040
  186. package/src/engine/indicators/visibleStateComposers.ts +734 -0
  187. package/src/engine/renderers/Indicator/__tests__/createSubIndicatorRenderer.test.ts +61 -0
  188. package/src/engine/renderers/Indicator/atr.ts +9 -1
  189. package/src/engine/renderers/Indicator/boll.ts +53 -1
  190. package/src/engine/renderers/Indicator/cci.ts +9 -2
  191. package/src/engine/renderers/Indicator/chaikinVol.ts +8 -2
  192. package/src/engine/renderers/Indicator/cmf.ts +8 -2
  193. package/src/engine/renderers/Indicator/dema.ts +9 -2
  194. package/src/engine/renderers/Indicator/donchian.ts +9 -2
  195. package/src/engine/renderers/Indicator/ene.ts +51 -1
  196. package/src/engine/renderers/Indicator/expma.ts +51 -1
  197. package/src/engine/renderers/Indicator/fastk.ts +9 -2
  198. package/src/engine/renderers/Indicator/fib.ts +9 -2
  199. package/src/engine/renderers/Indicator/hma.ts +9 -2
  200. package/src/engine/renderers/Indicator/hv.ts +8 -1
  201. package/src/engine/renderers/Indicator/ichimoku.ts +9 -2
  202. package/src/engine/renderers/Indicator/index.ts +9 -121
  203. package/src/engine/renderers/Indicator/kama.ts +9 -2
  204. package/src/engine/renderers/Indicator/keltner.ts +9 -2
  205. package/src/engine/renderers/Indicator/kst.ts +9 -1
  206. package/src/engine/renderers/Indicator/ma.ts +69 -0
  207. package/src/engine/renderers/Indicator/macd.ts +9 -2
  208. package/src/engine/renderers/Indicator/mfi.ts +8 -2
  209. package/src/engine/renderers/Indicator/mom.ts +9 -1
  210. package/src/engine/renderers/Indicator/obv.ts +8 -2
  211. package/src/engine/renderers/Indicator/parkinson.ts +8 -1
  212. package/src/engine/renderers/Indicator/pivot.ts +9 -2
  213. package/src/engine/renderers/Indicator/pvt.ts +8 -2
  214. package/src/engine/renderers/Indicator/roc.ts +8 -2
  215. package/src/engine/renderers/Indicator/rsi.ts +9 -2
  216. package/src/engine/renderers/Indicator/sar.ts +9 -2
  217. package/src/engine/renderers/Indicator/stoch.ts +9 -2
  218. package/src/engine/renderers/Indicator/structure.ts +9 -2
  219. package/src/engine/renderers/Indicator/subPaneConfig.ts +1 -1
  220. package/src/engine/renderers/Indicator/supertrend.ts +9 -2
  221. package/src/engine/renderers/Indicator/tema.ts +9 -2
  222. package/src/engine/renderers/Indicator/trix.ts +8 -1
  223. package/src/engine/renderers/Indicator/vma.ts +8 -1
  224. package/src/engine/renderers/Indicator/volumeProfile.ts +8 -2
  225. package/src/engine/renderers/Indicator/vwap.ts +8 -2
  226. package/src/engine/renderers/Indicator/wma.ts +9 -2
  227. package/src/engine/renderers/Indicator/wmsr.ts +9 -2
  228. package/src/engine/renderers/Indicator/zones.ts +9 -2
  229. package/src/engine/renderers/extremaMarkers.ts +0 -24
  230. package/src/engine/renderers/subVolume.ts +3 -0
  231. package/src/engine/subPaneManager.ts +34 -274
  232. package/src/semantic/__tests__/controller.test.ts +84 -0
  233. package/src/semantic/controller.ts +69 -65
  234. package/src/version.ts +1 -1
@@ -0,0 +1,380 @@
1
+ import { describe, expect, it, vi } from 'vitest'
2
+ import { getRegisteredIndicatorDefinition } from '../indicatorDefinitionRegistry'
3
+ import { getBuiltinIndicatorDefinitions } from '../registerBuiltins'
4
+
5
+ describe('builtin indicator registration', () => {
6
+ it('loads all builtin indicator definitions through decorators', () => {
7
+ const definitions = getBuiltinIndicatorDefinitions()
8
+
9
+ expect(definitions).toHaveLength(40)
10
+ expect(definitions.map((definition) => definition.name)).toEqual(
11
+ expect.arrayContaining([
12
+ 'ma',
13
+ 'boll',
14
+ 'rsi',
15
+ 'macd',
16
+ 'volume',
17
+ 'volumeProfile',
18
+ 'zones',
19
+ ]),
20
+ )
21
+ })
22
+
23
+ it('allows builtin definitions to be queried by display name', () => {
24
+ expect(getRegisteredIndicatorDefinition('RSI')?.name).toBe('rsi')
25
+ expect(getRegisteredIndicatorDefinition('MACD')?.name).toBe('macd')
26
+ expect(getRegisteredIndicatorDefinition('VOL')?.name).toBe('volume')
27
+ })
28
+
29
+ it('registers metadata config updaters for stage 4A indicators', () => {
30
+ expect(getRegisteredIndicatorDefinition('RSI')?.updateConfig).toBeTypeOf('function')
31
+ expect(getRegisteredIndicatorDefinition('MACD')?.updateConfig).toBeTypeOf('function')
32
+ expect(getRegisteredIndicatorDefinition('VOL')?.updateConfig).toBeTypeOf('function')
33
+ expect(getRegisteredIndicatorDefinition('VOLUME_PROFILE')?.updateConfig).toBeTypeOf('function')
34
+ })
35
+
36
+ it('registers metadata config updaters for stage 4B indicators', () => {
37
+ const expectedIndicators = [
38
+ 'CCI', 'STOCH', 'MOM', 'WMSR', 'KST', 'FASTK', 'ATR',
39
+ 'WMA', 'DEMA', 'TEMA', 'HMA', 'KAMA', 'SAR',
40
+ 'SUPERTREND', 'KELTNER', 'DONCHIAN', 'ICHIMOKU',
41
+ 'ROC', 'TRIX', 'HV', 'PARKINSON', 'CHAIKIN_VOL',
42
+ 'VMA', 'OBV', 'PVT', 'VWAP', 'CMF', 'MFI',
43
+ 'PIVOT', 'FIB', 'STRUCTURE', 'ZONES',
44
+ ]
45
+ for (const id of expectedIndicators) {
46
+ expect(getRegisteredIndicatorDefinition(id)?.updateConfig).toBeTypeOf('function')
47
+ }
48
+ })
49
+
50
+ it('routes stage 4A metadata config updates to scheduler methods', () => {
51
+ const scheduler = {
52
+ updateRSIConfig: vi.fn(),
53
+ updateMACDConfig: vi.fn(),
54
+ updateVolumeProfileConfig: vi.fn(),
55
+ }
56
+
57
+ getRegisteredIndicatorDefinition('RSI')?.updateConfig?.(scheduler, { period1: 7 }, 'RSI_0')
58
+ getRegisteredIndicatorDefinition('MACD')?.updateConfig?.(scheduler, { fastPeriod: 8 }, 'MACD_0')
59
+ getRegisteredIndicatorDefinition('VOLUME_PROFILE')?.updateConfig?.(scheduler, { bins: 32 }, 'VP_0')
60
+ getRegisteredIndicatorDefinition('VOL')?.updateConfig?.(scheduler, {}, 'VOL_0')
61
+
62
+ expect(scheduler.updateRSIConfig).toHaveBeenCalledWith({ period1: 7 }, 'RSI_0')
63
+ expect(scheduler.updateMACDConfig).toHaveBeenCalledWith({ fastPeriod: 8 }, 'MACD_0')
64
+ expect(scheduler.updateVolumeProfileConfig).toHaveBeenCalledWith({ bins: 32 }, 'VP_0')
65
+ })
66
+
67
+ it('routes stage 4B metadata config updates to scheduler methods', () => {
68
+ const scheduler = {
69
+ updateCCIConfig: vi.fn(),
70
+ updateATRConfig: vi.fn(),
71
+ updateChaikinVolConfig: vi.fn(),
72
+ updateZonesConfig: vi.fn(),
73
+ }
74
+
75
+ getRegisteredIndicatorDefinition('CCI')?.updateConfig?.(scheduler, { period: 14 }, 'CCI_0')
76
+ getRegisteredIndicatorDefinition('ATR')?.updateConfig?.(scheduler, { period: 10 }, 'ATR_0')
77
+ getRegisteredIndicatorDefinition('CHAIKIN_VOL')?.updateConfig?.(scheduler, { emaPeriod: 10 }, 'CV_0')
78
+ getRegisteredIndicatorDefinition('ZONES')?.updateConfig?.(scheduler, { showFVG: true }, 'Z_0')
79
+
80
+ expect(scheduler.updateCCIConfig).toHaveBeenCalledWith({ period: 14 }, 'CCI_0')
81
+ expect(scheduler.updateATRConfig).toHaveBeenCalledWith({ period: 10 }, 'ATR_0')
82
+ expect(scheduler.updateChaikinVolConfig).toHaveBeenCalledWith({ emaPeriod: 10 }, 'CV_0')
83
+ expect(scheduler.updateZonesConfig).toHaveBeenCalledWith({ showFVG: true }, 'Z_0')
84
+ })
85
+
86
+ it('registers dedicated scale renderer factories for stage 5A indicators', () => {
87
+ const expectedIndicators = [
88
+ 'VOL', 'MACD', 'RSI', 'CCI', 'STOCH', 'MOM', 'WMSR', 'KST', 'FASTK', 'ATR',
89
+ ]
90
+
91
+ for (const id of expectedIndicators) {
92
+ expect(getRegisteredIndicatorDefinition(id)?.scaleRendererFactory).toBeTypeOf('function')
93
+ }
94
+ })
95
+
96
+ it('creates scale renderers through stage 5A metadata factories', () => {
97
+ const rsiScaleRenderer = getRegisteredIndicatorDefinition('RSI')?.scaleRendererFactory?.({
98
+ indicatorId: 'RSI',
99
+ paneId: 'RSI_0',
100
+ axisWidth: 80,
101
+ yPaddingPx: 4,
102
+ getCrosshair: () => null,
103
+ })
104
+ const volumeScaleRenderer = getRegisteredIndicatorDefinition('VOL')?.scaleRendererFactory?.({
105
+ indicatorId: 'VOLUME',
106
+ paneId: 'VOLUME_0',
107
+ axisWidth: 80,
108
+ yPaddingPx: 4,
109
+ getCrosshair: () => null,
110
+ })
111
+
112
+ expect(rsiScaleRenderer?.name).toBe('rsiScale_RSI_0')
113
+ expect(volumeScaleRenderer?.name).toBe('volumeScale_VOLUME_0')
114
+ })
115
+
116
+ it('registers generic scale metadata for stage 5B indicators', () => {
117
+ const expected: Record<string, { indicatorKey: string; label: string; decimals: number }> = {
118
+ WMA: { indicatorKey: 'wma', label: 'WMA', decimals: 2 },
119
+ DEMA: { indicatorKey: 'dema', label: 'DEMA', decimals: 2 },
120
+ TEMA: { indicatorKey: 'tema', label: 'TEMA', decimals: 2 },
121
+ HMA: { indicatorKey: 'hma', label: 'HMA', decimals: 2 },
122
+ KAMA: { indicatorKey: 'kama', label: 'KAMA', decimals: 2 },
123
+ SAR: { indicatorKey: 'sar', label: 'SAR', decimals: 4 },
124
+ SUPERTREND: { indicatorKey: 'supertrend', label: 'SuperTrend', decimals: 2 },
125
+ KELTNER: { indicatorKey: 'keltner', label: 'Keltner', decimals: 2 },
126
+ DONCHIAN: { indicatorKey: 'donchian', label: 'Donchian', decimals: 2 },
127
+ ICHIMOKU: { indicatorKey: 'ichimoku', label: 'Ichimoku', decimals: 2 },
128
+ ROC: { indicatorKey: 'roc', label: 'ROC', decimals: 2 },
129
+ TRIX: { indicatorKey: 'trix', label: 'TRIX', decimals: 6 },
130
+ HV: { indicatorKey: 'hv', label: 'HV', decimals: 2 },
131
+ PARKINSON: { indicatorKey: 'parkinson', label: 'Parkinson', decimals: 2 },
132
+ CHAIKIN_VOL: { indicatorKey: 'chaikinVol', label: 'ChaikinVol', decimals: 2 },
133
+ VMA: { indicatorKey: 'vma', label: 'VMA', decimals: 0 },
134
+ OBV: { indicatorKey: 'obv', label: 'OBV', decimals: 0 },
135
+ PVT: { indicatorKey: 'pvt', label: 'PVT', decimals: 0 },
136
+ VWAP: { indicatorKey: 'vwap', label: 'VWAP', decimals: 2 },
137
+ CMF: { indicatorKey: 'cmf', label: 'CMF', decimals: 4 },
138
+ MFI: { indicatorKey: 'mfi', label: 'MFI', decimals: 2 },
139
+ PIVOT: { indicatorKey: 'pivot', label: 'Pivot', decimals: 2 },
140
+ FIB: { indicatorKey: 'fib', label: 'Fib', decimals: 4 },
141
+ STRUCTURE: { indicatorKey: 'structure', label: 'Structure', decimals: 2 },
142
+ ZONES: { indicatorKey: 'zones', label: 'Zones', decimals: 2 },
143
+ VOLUME_PROFILE: { indicatorKey: 'volumeProfile', label: 'VP', decimals: 0 },
144
+ }
145
+
146
+ for (const [id, scale] of Object.entries(expected)) {
147
+ expect(getRegisteredIndicatorDefinition(id)?.scale).toEqual(scale)
148
+ }
149
+ })
150
+
151
+ it('creates generic scale renderers through stage 5B metadata', () => {
152
+ const definition = getRegisteredIndicatorDefinition('WMA')
153
+ expect(definition?.scale).toBeDefined()
154
+ })
155
+
156
+ it('registers main pane renderer metadata for stage 6A-1 indicators', () => {
157
+ const expected: Record<string, string> = {
158
+ MA: 'ma',
159
+ BOLL: 'boll',
160
+ EXPMA: 'expma',
161
+ ENE: 'ene',
162
+ }
163
+
164
+ for (const [id, rendererName] of Object.entries(expected)) {
165
+ expect(getRegisteredIndicatorDefinition(id)?.mainPane?.rendererName).toBe(rendererName)
166
+ }
167
+ })
168
+
169
+ it('creates main pane renderers through stage 6A-1 metadata factories', () => {
170
+ const maRenderer = getRegisteredIndicatorDefinition('MA')?.rendererFactory({ paneId: 'main', indicatorId: 'MA' })
171
+ const bollRenderer = getRegisteredIndicatorDefinition('BOLL')?.rendererFactory({ paneId: 'main', indicatorId: 'BOLL' })
172
+
173
+ expect(maRenderer?.name).toBe('ma')
174
+ expect(bollRenderer?.name).toBe('boll')
175
+ })
176
+
177
+ it('registers main pane renderer metadata for stage 6A-2 indicators', () => {
178
+ const expected: Record<string, string> = {
179
+ WMA: 'wma_main',
180
+ DEMA: 'dema_main',
181
+ TEMA: 'tema_main',
182
+ HMA: 'hma_main',
183
+ KAMA: 'kama_main',
184
+ SAR: 'sar_main',
185
+ SUPERTREND: 'supertrend_main',
186
+ KELTNER: 'keltner_main',
187
+ DONCHIAN: 'donchian_main',
188
+ ICHIMOKU: 'ichimoku_main',
189
+ PIVOT: 'pivot_main',
190
+ FIB: 'fib_main',
191
+ STRUCTURE: 'structure_main',
192
+ ZONES: 'zones_main',
193
+ }
194
+
195
+ for (const [id, rendererName] of Object.entries(expected)) {
196
+ expect(getRegisteredIndicatorDefinition(id)?.mainPane?.rendererName).toBe(rendererName)
197
+ }
198
+ })
199
+
200
+ it('creates overlay main pane renderers through stage 6A-2 metadata factories', () => {
201
+ const wma = getRegisteredIndicatorDefinition('WMA')?.rendererFactory({ paneId: 'main', indicatorId: 'WMA' })
202
+ const zones = getRegisteredIndicatorDefinition('ZONES')?.rendererFactory({ paneId: 'main', indicatorId: 'ZONES' })
203
+
204
+ expect(wma?.name).toBe('wma_main')
205
+ expect(zones?.name).toBe('zones_main')
206
+ })
207
+
208
+ it('registers overlay main config metadata for stage 6B-2 indicators', () => {
209
+ const ids = [
210
+ 'WMA', 'DEMA', 'TEMA', 'HMA', 'KAMA',
211
+ 'SAR', 'SUPERTREND', 'KELTNER', 'DONCHIAN',
212
+ 'ICHIMOKU', 'PIVOT', 'FIB', 'STRUCTURE', 'ZONES',
213
+ ]
214
+
215
+ for (const id of ids) {
216
+ expect(getRegisteredIndicatorDefinition(id)?.mainPane?.toActiveConfig).toBeTypeOf('function')
217
+ }
218
+ })
219
+
220
+ it('builds overlay main active configs through stage 6B-2 metadata', () => {
221
+ expect(getRegisteredIndicatorDefinition('WMA')?.mainPane?.toActiveConfig?.({ period: 10 }, false)).toEqual({
222
+ period: 10, showWMA: false,
223
+ })
224
+
225
+ expect(getRegisteredIndicatorDefinition('KELTNER')?.mainPane?.toActiveConfig?.({ emaPeriod: 20 }, false)).toEqual({
226
+ emaPeriod: 20, showUpper: false, showMiddle: false, showLower: false,
227
+ })
228
+
229
+ expect(getRegisteredIndicatorDefinition('ICHIMOKU')?.mainPane?.toActiveConfig?.({}, false)).toEqual({
230
+ showTenkan: false, showKijun: false, showSpanA: false,
231
+ showSpanB: false, showChikou: false, showCloud: false,
232
+ })
233
+
234
+ expect(getRegisteredIndicatorDefinition('ZONES')?.mainPane?.toActiveConfig?.({}, false)).toEqual({
235
+ showFVG: false, showOB: false, showFilledZones: false,
236
+ })
237
+ })
238
+
239
+ it('registers base main config metadata for stage 6B-1 indicators', () => {
240
+ for (const id of ['MA', 'BOLL', 'EXPMA', 'ENE']) {
241
+ const definition = getRegisteredIndicatorDefinition(id)
242
+ expect(definition?.updateConfig).toBeTypeOf('function')
243
+ expect(definition?.mainPane?.toActiveConfig).toBeTypeOf('function')
244
+ }
245
+ })
246
+
247
+ it('builds base main active configs through stage 6B-1 metadata', () => {
248
+ expect(getRegisteredIndicatorDefinition('MA')?.mainPane?.toActiveConfig?.({}, false)).toEqual({
249
+ ma5: false,
250
+ ma10: false,
251
+ ma20: false,
252
+ ma30: false,
253
+ ma60: false,
254
+ })
255
+ expect(getRegisteredIndicatorDefinition('BOLL')?.mainPane?.toActiveConfig?.({ period: 20 }, false)).toEqual({
256
+ period: 20,
257
+ showUpper: false,
258
+ showMiddle: false,
259
+ showLower: false,
260
+ showBand: false,
261
+ })
262
+ expect(getRegisteredIndicatorDefinition('EXPMA')?.mainPane?.toActiveConfig?.({}, false)).toBeNull()
263
+ expect(getRegisteredIndicatorDefinition('ENE')?.mainPane?.toActiveConfig?.({}, false)).toBeNull()
264
+ })
265
+
266
+ it('routes base main metadata config updates to scheduler methods', () => {
267
+ const scheduler = {
268
+ updateMAConfig: vi.fn(),
269
+ updateBOLLConfig: vi.fn(),
270
+ updateEXPMAConfig: vi.fn(),
271
+ updateENEConfig: vi.fn(),
272
+ }
273
+
274
+ getRegisteredIndicatorDefinition('MA')?.updateConfig?.(scheduler, { ma5: true }, 'main')
275
+ getRegisteredIndicatorDefinition('BOLL')?.updateConfig?.(scheduler, { period: 20 }, 'main')
276
+ getRegisteredIndicatorDefinition('EXPMA')?.updateConfig?.(scheduler, { fastPeriod: 12 }, 'main')
277
+ getRegisteredIndicatorDefinition('ENE')?.updateConfig?.(scheduler, { period: 10 }, 'main')
278
+
279
+ expect(scheduler.updateMAConfig).toHaveBeenCalledWith({ ma5: true })
280
+ expect(scheduler.updateBOLLConfig).toHaveBeenCalledWith({ period: 20 })
281
+ expect(scheduler.updateEXPMAConfig).toHaveBeenCalledWith({ fastPeriod: 12 })
282
+ expect(scheduler.updateENEConfig).toHaveBeenCalledWith({ period: 10 })
283
+ })
284
+
285
+ it('registers semantic apply metadata for stage 7A main indicators', () => {
286
+ for (const id of ['MA', 'BOLL', 'EXPMA', 'ENE']) {
287
+ expect(getRegisteredIndicatorDefinition(id)?.semantic?.apply).toBeTypeOf('function')
288
+ }
289
+ })
290
+
291
+ it('routes semantic main indicator configs through stage 7A metadata', () => {
292
+ const chart = { updateRendererConfig: vi.fn() }
293
+
294
+ getRegisteredIndicatorDefinition('MA')?.semantic?.apply?.(chart, {
295
+ type: 'MA',
296
+ enabled: true,
297
+ params: { periods: [5, 20, 99] },
298
+ })
299
+ getRegisteredIndicatorDefinition('BOLL')?.semantic?.apply?.(chart, {
300
+ type: 'BOLL',
301
+ enabled: true,
302
+ params: { period: 21, multiplier: 2.5 },
303
+ })
304
+ getRegisteredIndicatorDefinition('EXPMA')?.semantic?.apply?.(chart, {
305
+ type: 'EXPMA',
306
+ enabled: true,
307
+ params: {},
308
+ })
309
+ getRegisteredIndicatorDefinition('ENE')?.semantic?.apply?.(chart, {
310
+ type: 'ENE',
311
+ enabled: true,
312
+ })
313
+
314
+ expect(chart.updateRendererConfig).toHaveBeenCalledWith('ma', { ma5: true, ma20: true })
315
+ expect(chart.updateRendererConfig).toHaveBeenCalledWith('boll', { period: 21, multiplier: 2.5 })
316
+ expect(chart.updateRendererConfig).toHaveBeenCalledWith('expma', { fastPeriod: 12, slowPeriod: 50 })
317
+ expect(chart.updateRendererConfig).toHaveBeenCalledWith('ene', { period: 10, deviation: 11 })
318
+ })
319
+
320
+ it('registers main pane price range metadata for stage 8A indicators', () => {
321
+ for (const id of ['MA', 'BOLL', 'EXPMA', 'ENE']) {
322
+ expect(getRegisteredIndicatorDefinition(id)?.mainPane?.computePriceRange).toBeTypeOf('function')
323
+ }
324
+ })
325
+
326
+ it('registers main pane render state composer metadata for stage 8B indicators', () => {
327
+ for (const id of ['MA', 'BOLL', 'EXPMA', 'ENE']) {
328
+ expect(getRegisteredIndicatorDefinition(id)?.mainPane?.composeRenderState).toBeTypeOf('function')
329
+ }
330
+ })
331
+
332
+ it('registers visible state composer metadata for stage 8C-A indicators', () => {
333
+ for (const id of ['WMA', 'DEMA', 'TEMA', 'HMA', 'KAMA', 'ROC', 'ChaikinVol', 'OBV', 'PVT', 'VWAP']) {
334
+ expect(getRegisteredIndicatorDefinition(id)?.visibleState?.compose).toBeTypeOf('function')
335
+ }
336
+ })
337
+
338
+ it('registers visible state composer metadata for stage 8C-B indicators', () => {
339
+ for (const id of ['RSI', 'STOCH', 'FASTK', 'MFI', 'WMSR', 'CMF']) {
340
+ expect(getRegisteredIndicatorDefinition(id)?.visibleState?.compose).toBeTypeOf('function')
341
+ }
342
+ })
343
+
344
+ it('registers visible state composer metadata for stage 8C-C indicators', () => {
345
+ for (const id of ['MOM', 'KST', 'ATR', 'HV', 'PARKINSON', 'VMA', 'TRIX']) {
346
+ expect(getRegisteredIndicatorDefinition(id)?.visibleState?.compose).toBeTypeOf('function')
347
+ }
348
+ })
349
+
350
+ it('registers visible state composer metadata for stage 8C-D1 MACD indicator', () => {
351
+ expect(getRegisteredIndicatorDefinition('MACD')?.visibleState?.compose).toBeTypeOf('function')
352
+ })
353
+
354
+ it('registers visible state composer metadata for stage 8C-D2 overlay indicators', () => {
355
+ for (const id of ['SAR', 'SUPERTREND', 'KELTNER', 'DONCHIAN', 'ICHIMOKU', 'PIVOT', 'FIB']) {
356
+ expect(getRegisteredIndicatorDefinition(id)?.visibleState?.compose).toBeTypeOf('function')
357
+ }
358
+ })
359
+
360
+ it('registers visible state composer metadata for stage 8C-D2-B structure indicators', () => {
361
+ for (const id of ['STRUCTURE', 'ZONES', 'VOLUME_PROFILE']) {
362
+ expect(getRegisteredIndicatorDefinition(id)?.visibleState?.compose).toBeTypeOf('function')
363
+ }
364
+ })
365
+
366
+ it('registers visible state composer metadata for all composed visible sub indicators', () => {
367
+ const ids = [
368
+ 'RSI', 'CCI', 'STOCH', 'MOM', 'WMSR', 'FASTK', 'MACD',
369
+ 'ATR', 'WMA', 'DEMA', 'TEMA', 'HMA', 'KAMA',
370
+ 'SAR', 'SUPERTREND', 'KELTNER', 'DONCHIAN', 'ICHIMOKU',
371
+ 'ROC', 'TRIX', 'HV', 'PARKINSON', 'CHAIKIN_VOL',
372
+ 'VMA', 'OBV', 'PVT', 'VWAP', 'CMF', 'MFI',
373
+ 'KST', 'PIVOT', 'FIB', 'STRUCTURE', 'ZONES', 'VOLUME_PROFILE',
374
+ ]
375
+
376
+ for (const id of ids) {
377
+ expect(getRegisteredIndicatorDefinition(id)?.visibleState?.compose).toBeTypeOf('function')
378
+ }
379
+ })
380
+ })
@@ -1,5 +1,6 @@
1
1
  import { describe, it, expect, vi, beforeEach } from 'vitest'
2
2
  import { IndicatorScheduler } from '../scheduler'
3
+ import { getBuiltinIndicatorDefinitions } from '../registerBuiltins'
3
4
  import { MA_STATE_KEY, EMPTY_MA_STATE, type MARenderState } from '../maState'
4
5
  import { BOLL_STATE_KEY, EMPTY_BOLL_STATE, type BOLLRenderState } from '../bollState'
5
6
  import { EXPMA_STATE_KEY, EMPTY_EXPMA_STATE, type EXPMARenderState } from '../expmaState'
@@ -15,40 +16,45 @@ function applyMainResult(key: string): NonNullable<IndicatorMetadata['applyResul
15
16
  }
16
17
  }
17
18
 
18
- function applyRSIResult(host: any, state: any, paneId: string): void {
19
- host.setSharedState(createRSIStateKey(paneId), state as any, 'indicator_scheduler')
19
+ const builtinDefinitionsByName = new Map(getBuiltinIndicatorDefinitions().map((definition) => [definition.name, definition]))
20
+
21
+ function getBuiltinTestIndicator(name: string): IndicatorMetadata {
22
+ const definition = builtinDefinitionsByName.get(name)
23
+ if (!definition) {
24
+ throw new Error(`Missing builtin test indicator: ${name}`)
25
+ }
26
+ return definition
20
27
  }
21
28
 
22
29
  function registerTestIndicators(scheduler: IndicatorScheduler): void {
23
30
  const indicators: Array<IndicatorMetadata> = [
24
- { name: 'ma', displayName: 'MA', category: 'main' as const, stateKey: MA_STATE_KEY, defaultPaneId: 'main', rendererFactory: vi.fn() as any, applyResult: applyMainResult(MA_STATE_KEY) },
25
- { name: 'boll', displayName: 'BOLL', category: 'main' as const, stateKey: BOLL_STATE_KEY, defaultPaneId: 'main', rendererFactory: vi.fn() as any, applyResult: applyMainResult(BOLL_STATE_KEY) },
26
- { name: 'expma', displayName: 'EXPMA', category: 'main' as const, stateKey: EXPMA_STATE_KEY, defaultPaneId: 'main', rendererFactory: vi.fn() as any, applyResult: applyMainResult(EXPMA_STATE_KEY) },
27
- { name: 'ene', displayName: 'ENE', category: 'main' as const, stateKey: ENE_STATE_KEY, defaultPaneId: 'main', rendererFactory: vi.fn() as any, applyResult: applyMainResult(ENE_STATE_KEY) },
28
- { name: 'rsi', displayName: 'RSI', category: 'sub' as const, stateKey: createRSIStateKey('sub_RSI'), defaultPaneId: 'sub_RSI', rendererFactory: vi.fn() as any, paneIdField: 'rsiPaneId' as any, applyResult: applyRSIResult },
29
- { name: 'macd', displayName: 'MACD', category: 'sub' as const, stateKey: 'indicator:macd:sub_MACD', defaultPaneId: 'sub_MACD', rendererFactory: vi.fn() as any, paneIdField: 'macdPaneId' as any, applyResult: applyMainResult('indicator:macd:sub_MACD') },
31
+ getBuiltinTestIndicator('ma'),
32
+ getBuiltinTestIndicator('boll'),
33
+ getBuiltinTestIndicator('expma'),
34
+ getBuiltinTestIndicator('ene'),
35
+ getBuiltinTestIndicator('rsi'),
36
+ getBuiltinTestIndicator('macd'),
30
37
  { name: 'volume', displayName: 'Volume', category: 'sub' as const, stateKey: 'indicator:volume:sub_Volume', defaultPaneId: 'sub_Volume', rendererFactory: vi.fn() as any, paneIdField: 'volumePaneId' as any, applyResult: applyMainResult('indicator:volume:sub_Volume') },
31
- { name: 'stoch', displayName: 'STOCH', category: 'sub' as const, stateKey: 'indicator:stoch:sub_STOCH', defaultPaneId: 'sub_STOCH', rendererFactory: vi.fn() as any, paneIdField: 'stochPaneId' as any, applyResult: applyMainResult('indicator:stoch:sub_STOCH') },
32
- { name: 'rsi2', displayName: 'RSI2', category: 'sub' as const, stateKey: createRSIStateKey('sub_RSI2'), defaultPaneId: 'sub_RSI2', rendererFactory: vi.fn() as any, paneIdField: 'rsi2PaneId' as any, applyResult: applyRSIResult },
33
- { name: 'wmsr', displayName: 'WMSR', category: 'sub' as const, stateKey: 'indicator:wmsr:sub_WMSR', defaultPaneId: 'sub_WMSR', rendererFactory: vi.fn() as any, paneIdField: 'wmsrPaneId' as any, applyResult: applyMainResult('indicator:wmsr:sub_WMSR') },
34
- { name: 'mom', displayName: 'MOM', category: 'sub' as const, stateKey: 'indicator:mom:sub_MOM', defaultPaneId: 'sub_MOM', rendererFactory: vi.fn() as any, paneIdField: 'momPaneId' as any, applyResult: applyMainResult('indicator:mom:sub_MOM') },
35
- { name: 'fastk', displayName: 'FASTK', category: 'sub' as const, stateKey: 'indicator:fastk:sub_FASTK', defaultPaneId: 'sub_FASTK', rendererFactory: vi.fn() as any, paneIdField: 'fastkPaneId' as any, applyResult: applyMainResult('indicator:fastk:sub_FASTK') },
36
- { name: 'cci', displayName: 'CCI', category: 'sub' as const, stateKey: 'indicator:cci:sub_CCI', defaultPaneId: 'sub_CCI', rendererFactory: vi.fn() as any, paneIdField: 'cciPaneId' as any, applyResult: applyMainResult('indicator:cci:sub_CCI') },
37
- { name: 'atr', displayName: 'ATR', category: 'sub' as const, stateKey: 'indicator:atr:sub_ATR', defaultPaneId: 'sub_ATR', rendererFactory: vi.fn() as any, paneIdField: 'atrPaneId' as any, applyResult: applyMainResult('indicator:atr:sub_ATR') },
38
- { name: 'kst', displayName: 'KST', category: 'sub' as const, stateKey: 'indicator:kst:sub_KST', defaultPaneId: 'sub_KST', rendererFactory: vi.fn() as any, paneIdField: 'kstPaneId' as any, applyResult: applyMainResult('indicator:kst:sub_KST') },
39
- { name: 'roc', displayName: 'ROC', category: 'sub' as const, stateKey: 'indicator:roc:sub_ROC', defaultPaneId: 'sub_ROC', rendererFactory: vi.fn() as any, paneIdField: 'rocPaneId' as any, applyResult: applyMainResult('indicator:roc:sub_ROC') },
40
- { name: 'trix', displayName: 'TRIX', category: 'sub' as const, stateKey: 'indicator:trix:sub_TRIX', defaultPaneId: 'sub_TRIX', rendererFactory: vi.fn() as any, paneIdField: 'trixPaneId' as any, applyResult: applyMainResult('indicator:trix:sub_TRIX') },
41
- { name: 'hv', displayName: 'HV', category: 'sub' as const, stateKey: 'indicator:hv:sub_HV', defaultPaneId: 'sub_HV', rendererFactory: vi.fn() as any, paneIdField: 'hvPaneId' as any, applyResult: applyMainResult('indicator:hv:sub_HV') },
42
- { name: 'parkinson', displayName: 'Parkinson', category: 'sub' as const, stateKey: 'indicator:parkinson:sub_Parkinson', defaultPaneId: 'sub_Parkinson', rendererFactory: vi.fn() as any, paneIdField: 'parkinsonPaneId' as any, applyResult: applyMainResult('indicator:parkinson:sub_Parkinson') },
43
- { name: 'chaikinVol', displayName: 'ChaikinVol', category: 'sub' as const, stateKey: 'indicator:chaikinVol:sub_ChaikinVol', defaultPaneId: 'sub_ChaikinVol', rendererFactory: vi.fn() as any, paneIdField: 'chaikinVolPaneId' as any, applyResult: applyMainResult('indicator:chaikinVol:sub_ChaikinVol') },
44
- { name: 'vma', displayName: 'VMA', category: 'sub' as const, stateKey: 'indicator:vma:sub_VMA', defaultPaneId: 'sub_VMA', rendererFactory: vi.fn() as any, paneIdField: 'vmaPaneId' as any, applyResult: applyMainResult('indicator:vma:sub_VMA') },
45
- { name: 'obv', displayName: 'OBV', category: 'sub' as const, stateKey: 'indicator:obv:sub_OBV', defaultPaneId: 'sub_OBV', rendererFactory: vi.fn() as any, paneIdField: 'obvPaneId' as any, applyResult: applyMainResult('indicator:obv:sub_OBV') },
46
- { name: 'pvt', displayName: 'PVT', category: 'sub' as const, stateKey: 'indicator:pvt:sub_PVT', defaultPaneId: 'sub_PVT', rendererFactory: vi.fn() as any, paneIdField: 'pvtPaneId' as any, applyResult: applyMainResult('indicator:pvt:sub_PVT') },
47
- { name: 'vwap', displayName: 'VWAP', category: 'sub' as const, stateKey: 'indicator:vwap:sub_VWAP', defaultPaneId: 'sub_VWAP', rendererFactory: vi.fn() as any, paneIdField: 'vwapPaneId' as any, applyResult: applyMainResult('indicator:vwap:sub_VWAP') },
48
- { name: 'cmf', displayName: 'CMF', category: 'sub' as const, stateKey: 'indicator:cmf:sub_CMF', defaultPaneId: 'sub_CMF', rendererFactory: vi.fn() as any, paneIdField: 'cmfPaneId' as any, applyResult: applyMainResult('indicator:cmf:sub_CMF') },
49
- { name: 'mfi', displayName: 'MFI', category: 'sub' as const, stateKey: 'indicator:mfi:sub_MFI', defaultPaneId: 'sub_MFI', rendererFactory: vi.fn() as any, paneIdField: 'mfiPaneId' as any, applyResult: applyMainResult('indicator:mfi:sub_MFI') },
50
- { name: 'sar', displayName: 'SAR', category: 'main' as const, stateKey: 'indicator:sar:main', defaultPaneId: 'main', rendererFactory: vi.fn() as any, applyResult: applyMainResult('indicator:sar:main') },
51
- { name: 'keltner', displayName: 'Keltner', category: 'main' as const, stateKey: 'indicator:keltner:main', defaultPaneId: 'main', rendererFactory: vi.fn() as any, applyResult: applyMainResult('indicator:keltner:main') },
38
+ getBuiltinTestIndicator('stoch'),
39
+ getBuiltinTestIndicator('wmsr'),
40
+ getBuiltinTestIndicator('mom'),
41
+ getBuiltinTestIndicator('fastk'),
42
+ getBuiltinTestIndicator('cci'),
43
+ getBuiltinTestIndicator('atr'),
44
+ getBuiltinTestIndicator('kst'),
45
+ getBuiltinTestIndicator('roc'),
46
+ getBuiltinTestIndicator('trix'),
47
+ getBuiltinTestIndicator('hv'),
48
+ getBuiltinTestIndicator('parkinson'),
49
+ getBuiltinTestIndicator('chaikinVol'),
50
+ getBuiltinTestIndicator('vma'),
51
+ getBuiltinTestIndicator('obv'),
52
+ getBuiltinTestIndicator('pvt'),
53
+ getBuiltinTestIndicator('vwap'),
54
+ getBuiltinTestIndicator('cmf'),
55
+ getBuiltinTestIndicator('mfi'),
56
+ getBuiltinTestIndicator('sar'),
57
+ getBuiltinTestIndicator('keltner'),
52
58
  ]
53
59
  for (const meta of indicators) {
54
60
  scheduler.registerIndicator(meta)
@@ -203,6 +209,28 @@ describe('IndicatorScheduler', () => {
203
209
  expect(state!.visibleMin).toBe(Infinity)
204
210
  expect(state!.visibleMax).toBe(-Infinity)
205
211
  })
212
+
213
+ it('should refresh main indicator visible extremes when visible range changes', () => {
214
+ const data = createTestData(100, 100)
215
+ const invalidate = vi.fn()
216
+ scheduler.setInvalidateCallback(invalidate)
217
+
218
+ scheduler.update(data, { start: 0, end: 0 })
219
+ invalidate.mockClear()
220
+ const initialState = getStateFromMockCalls<BOLLRenderState>(mockHost, BOLL_STATE_KEY)
221
+ expect(initialState).toBeDefined()
222
+ expect(initialState!.visibleMin).toBe(Infinity)
223
+ expect(initialState!.visibleMax).toBe(-Infinity)
224
+
225
+ scheduler.updateVisibleRange({ start: 30, end: 60 })
226
+
227
+ const updatedState = getStateFromMockCalls<BOLLRenderState>(mockHost, BOLL_STATE_KEY)
228
+ expect(updatedState).toBeDefined()
229
+ expect(updatedState!.visibleMin).toBeLessThan(updatedState!.visibleMax)
230
+ expect(Number.isFinite(updatedState!.visibleMin)).toBe(true)
231
+ expect(Number.isFinite(updatedState!.visibleMax)).toBe(true)
232
+ expect(invalidate).toHaveBeenCalledTimes(1)
233
+ })
206
234
  })
207
235
 
208
236
  describe('MA config update', () => {
@@ -279,9 +307,11 @@ describe('IndicatorScheduler', () => {
279
307
  // Update only viewport
280
308
  scheduler.updateVisibleRange({ start: 50, end: 60 })
281
309
 
282
- // updateVisibleStatesOnly writes the 20 sub-indicators that are both
283
- // registered and known to composeVisibleSubIndicatorStates.
284
- expect(mockHost.setSharedState).toHaveBeenCalledTimes(20)
310
+ const calls = vi.mocked(mockHost.setSharedState).mock.calls
311
+ expect(calls.some((call) => call[0] === MA_STATE_KEY)).toBe(false)
312
+ expect(calls.some((call) => call[0] === BOLL_STATE_KEY)).toBe(false)
313
+ expect(calls.some((call) => call[0] === EXPMA_STATE_KEY)).toBe(false)
314
+ expect(calls.some((call) => call[0] === ENE_STATE_KEY)).toBe(false)
285
315
 
286
316
  // Inspect a sub-indicator (RSI) since main indicators are not rewritten on viewport-only updates
287
317
  const rsiKey = createRSIStateKey('sub_RSI')