@363045841yyt/klinechart-core 0.8.1-alpha.3 → 0.8.1

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 (130) hide show
  1. package/dist/controllers/createChartController.d.ts.map +1 -1
  2. package/dist/controllers/createChartController.js +30 -4
  3. package/dist/controllers/createChartController.js.map +1 -1
  4. package/dist/controllers/types.d.ts +9 -2
  5. package/dist/controllers/types.d.ts.map +1 -1
  6. package/dist/data-fetchers/baostock.js +3 -3
  7. package/dist/data-fetchers/baostock.js.map +1 -1
  8. package/dist/data-fetchers/dataBuffer.d.ts +6 -1
  9. package/dist/data-fetchers/dataBuffer.d.ts.map +1 -1
  10. package/dist/data-fetchers/dataBuffer.js +88 -47
  11. package/dist/data-fetchers/dataBuffer.js.map +1 -1
  12. package/dist/data-fetchers/index.d.ts +1 -0
  13. package/dist/data-fetchers/index.d.ts.map +1 -1
  14. package/dist/data-fetchers/index.js +1 -0
  15. package/dist/data-fetchers/index.js.map +1 -1
  16. package/dist/data-fetchers/router.d.ts.map +1 -1
  17. package/dist/data-fetchers/router.js +3 -0
  18. package/dist/data-fetchers/router.js.map +1 -1
  19. package/dist/data-fetchers/tradingview.d.ts +3 -0
  20. package/dist/data-fetchers/tradingview.d.ts.map +1 -0
  21. package/dist/data-fetchers/tradingview.js +45 -0
  22. package/dist/data-fetchers/tradingview.js.map +1 -0
  23. package/dist/engine/chart.d.ts +34 -351
  24. package/dist/engine/chart.d.ts.map +1 -1
  25. package/dist/engine/chart.js +246 -1716
  26. package/dist/engine/chart.js.map +1 -1
  27. package/dist/engine/chartContext.d.ts +24 -0
  28. package/dist/engine/chartContext.d.ts.map +1 -0
  29. package/dist/engine/chartContext.js +19 -0
  30. package/dist/engine/chartContext.js.map +1 -0
  31. package/dist/engine/chartTypes.d.ts +77 -0
  32. package/dist/engine/chartTypes.d.ts.map +1 -0
  33. package/dist/engine/chartTypes.js +2 -0
  34. package/dist/engine/chartTypes.js.map +1 -0
  35. package/dist/engine/controller/interaction.d.ts +1 -0
  36. package/dist/engine/controller/interaction.d.ts.map +1 -1
  37. package/dist/engine/controller/interaction.js +9 -2
  38. package/dist/engine/controller/interaction.js.map +1 -1
  39. package/dist/engine/data/chartDataManager.d.ts +102 -0
  40. package/dist/engine/data/chartDataManager.d.ts.map +1 -0
  41. package/dist/engine/data/chartDataManager.js +590 -0
  42. package/dist/engine/data/chartDataManager.js.map +1 -0
  43. package/dist/engine/indicators/chartIndicatorManager.d.ts +102 -0
  44. package/dist/engine/indicators/chartIndicatorManager.d.ts.map +1 -0
  45. package/dist/engine/indicators/chartIndicatorManager.js +437 -0
  46. package/dist/engine/indicators/chartIndicatorManager.js.map +1 -0
  47. package/dist/engine/layout/chartPaneLayout.d.ts +53 -0
  48. package/dist/engine/layout/chartPaneLayout.d.ts.map +1 -0
  49. package/dist/engine/layout/chartPaneLayout.js +388 -0
  50. package/dist/engine/layout/chartPaneLayout.js.map +1 -0
  51. package/dist/engine/render/chartRenderer.d.ts +86 -0
  52. package/dist/engine/render/chartRenderer.d.ts.map +1 -0
  53. package/dist/engine/render/chartRenderer.js +438 -0
  54. package/dist/engine/render/chartRenderer.js.map +1 -0
  55. package/dist/engine/renderers/Indicator/mainIndicatorLegend.d.ts.map +1 -1
  56. package/dist/engine/renderers/Indicator/mainIndicatorLegend.js +73 -7
  57. package/dist/engine/renderers/Indicator/mainIndicatorLegend.js.map +1 -1
  58. package/dist/engine/renderers/comparisonLine.d.ts.map +1 -1
  59. package/dist/engine/renderers/comparisonLine.js +25 -11
  60. package/dist/engine/renderers/comparisonLine.js.map +1 -1
  61. package/dist/engine/subPaneManager.d.ts +27 -6
  62. package/dist/engine/subPaneManager.d.ts.map +1 -1
  63. package/dist/engine/subPaneManager.js +54 -56
  64. package/dist/engine/subPaneManager.js.map +1 -1
  65. package/dist/engine/utils/chartZoomController.d.ts +33 -0
  66. package/dist/engine/utils/chartZoomController.d.ts.map +1 -0
  67. package/dist/engine/utils/chartZoomController.js +66 -0
  68. package/dist/engine/utils/chartZoomController.js.map +1 -0
  69. package/dist/engine/viewport/chartViewportManager.d.ts +72 -0
  70. package/dist/engine/viewport/chartViewportManager.d.ts.map +1 -0
  71. package/dist/engine/viewport/chartViewportManager.js +249 -0
  72. package/dist/engine/viewport/chartViewportManager.js.map +1 -0
  73. package/dist/engine/viewport/viewport.js +1 -1
  74. package/dist/engine/viewport/viewport.js.map +1 -1
  75. package/dist/plugin/types.d.ts +1 -0
  76. package/dist/plugin/types.d.ts.map +1 -1
  77. package/dist/plugin/types.js.map +1 -1
  78. package/dist/tokens/theme-china.d.ts.map +1 -1
  79. package/dist/tokens/theme-china.js +0 -4
  80. package/dist/tokens/theme-china.js.map +1 -1
  81. package/dist/tokens/theme-dark.d.ts.map +1 -1
  82. package/dist/tokens/theme-dark.js +0 -4
  83. package/dist/tokens/theme-dark.js.map +1 -1
  84. package/dist/tokens/theme-light.d.ts.map +1 -1
  85. package/dist/tokens/theme-light.js +1 -5
  86. package/dist/tokens/theme-light.js.map +1 -1
  87. package/dist/tokens/types.d.ts +0 -4
  88. package/dist/tokens/types.d.ts.map +1 -1
  89. package/dist/types/price.d.ts +2 -0
  90. package/dist/types/price.d.ts.map +1 -1
  91. package/dist/types/price.js.map +1 -1
  92. package/dist/version.d.ts +1 -1
  93. package/dist/version.d.ts.map +1 -1
  94. package/dist/version.js +1 -1
  95. package/dist/version.js.map +1 -1
  96. package/package.json +1 -1
  97. package/src/controllers/createChartController.ts +49 -13
  98. package/src/controllers/types.ts +9 -2
  99. package/src/data-fetchers/__tests__/dataBuffer.test.ts +77 -0
  100. package/src/data-fetchers/baostock.ts +3 -3
  101. package/src/data-fetchers/dataBuffer.ts +70 -22
  102. package/src/data-fetchers/index.ts +1 -0
  103. package/src/data-fetchers/router.ts +3 -0
  104. package/src/data-fetchers/tradingview.ts +48 -0
  105. package/src/engine/__tests__/subPaneManager.test.ts +154 -0
  106. package/src/engine/chart.ts +260 -2103
  107. package/src/engine/chartContext.ts +34 -0
  108. package/src/engine/chartTypes.ts +88 -0
  109. package/src/engine/controller/__tests__/interaction.dpr.test.ts +1 -0
  110. package/src/engine/controller/interaction.ts +10 -2
  111. package/src/engine/data/chartDataManager.ts +691 -0
  112. package/src/engine/indicators/__tests__/chartIndicatorManager.test.ts +103 -0
  113. package/src/engine/indicators/chartIndicatorManager.ts +566 -0
  114. package/src/engine/layout/chartPaneLayout.ts +474 -0
  115. package/src/engine/render/chartRenderer.ts +579 -0
  116. package/src/engine/renderers/Indicator/mainIndicatorLegend.ts +99 -13
  117. package/src/engine/renderers/comparisonLine.ts +25 -11
  118. package/src/engine/subPaneManager.ts +75 -59
  119. package/src/engine/utils/chartZoomController.ts +104 -0
  120. package/src/engine/viewport/chartViewportManager.ts +310 -0
  121. package/src/engine/viewport/viewport.ts +1 -1
  122. package/src/plugin/types.ts +1 -0
  123. package/src/tokens/__tests__/__snapshots__/baseline.test.ts.snap +1 -9
  124. package/src/tokens/theme-china.ts +0 -4
  125. package/src/tokens/theme-dark.ts +0 -4
  126. package/src/tokens/theme-light.ts +2 -6
  127. package/src/tokens/types.ts +0 -4
  128. package/src/types/price.ts +2 -0
  129. package/src/version.ts +1 -1
  130. package/src/engine/chart.d.ts +0 -619
@@ -0,0 +1,103 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest'
2
+ import { ChartIndicatorManager, type IndicatorDependencies } from '../chartIndicatorManager'
3
+ import { createPluginHost } from '../../../plugin/PluginHost'
4
+ import { createSignal } from '../../../reactivity/signal'
5
+ import type { VisibleRange } from '../../layout/pane'
6
+ import { UpdateLevel } from '../../layout/pane'
7
+
8
+ function createMockDeps() {
9
+ const rendererMap = new Map<string, any>()
10
+ const paneRatiosSignal = createSignal<Readonly<Record<string, number>>>({})
11
+
12
+ return {
13
+ rendererMap,
14
+ getOption: () => ({
15
+ rightAxisWidth: 60,
16
+ priceLabelWidth: 60,
17
+ yPaddingPx: 4,
18
+ paneGap: 1,
19
+ defaultPaneMinHeightPx: 40,
20
+ panes: [],
21
+ bottomAxisHeight: 20,
22
+ kWidth: 8,
23
+ kGap: 2,
24
+ minKWidth: 4,
25
+ maxKWidth: 16,
26
+ }),
27
+ getPluginHost: () => createPluginHost(),
28
+ getRenderer: vi.fn((name: string) => rendererMap.get(name)),
29
+ useRenderer: vi.fn((plugin: any, _config?: any) => {
30
+ if (plugin?.name) rendererMap.set(plugin.name, plugin)
31
+ }),
32
+ removeRenderer: vi.fn((name: string) => {
33
+ rendererMap.delete(name)
34
+ }),
35
+ updateRendererConfig: vi.fn(),
36
+ setRendererEnabled: vi.fn(),
37
+ hasPane: vi.fn(() => false),
38
+ upsertPane: vi.fn(),
39
+ removePaneDefinition: vi.fn(),
40
+ getPaneSpecs: vi.fn(() => []),
41
+ getPaneRatiosSignal: () => paneRatiosSignal,
42
+ getInternalPaneRatios: vi.fn(() => new Map()),
43
+ setInternalPaneRatio: vi.fn(),
44
+ deleteInternalPaneRatio: vi.fn(),
45
+ applyPaneLayoutSpecs: vi.fn(),
46
+ getLastVisibleRange: vi.fn(() => ({ start: 0, end: 0 }) as VisibleRange),
47
+ getCrosshairPos: vi.fn(() => null),
48
+ getCrosshairPrice: vi.fn(() => null),
49
+ getActivePaneId: vi.fn(() => null),
50
+ scheduleDraw: vi.fn(),
51
+ setPendingIndicatorDataUpdate: vi.fn(),
52
+ } as IndicatorDependencies & { rendererMap: Map<string, any> }
53
+ }
54
+
55
+ describe('ChartIndicatorManager', () => {
56
+ let manager: ChartIndicatorManager
57
+ let deps: ReturnType<typeof createMockDeps>
58
+
59
+ beforeEach(() => {
60
+ deps = createMockDeps()
61
+ manager = new ChartIndicatorManager(deps)
62
+ vi.clearAllMocks()
63
+ })
64
+
65
+ describe('updateMainIndicatorParams', () => {
66
+ it('should call renderer setConfig with merged params', () => {
67
+ manager.enableMainIndicator('MA')
68
+
69
+ const maRenderer = deps.rendererMap.get('ma')
70
+ const setConfigSpy = vi.spyOn(maRenderer, 'setConfig')
71
+
72
+ manager.updateMainIndicatorParams('MA', { ma5: false })
73
+
74
+ expect(setConfigSpy).toHaveBeenCalledTimes(1)
75
+ expect(setConfigSpy).toHaveBeenCalledWith({ ma5: false, ma10: true, ma20: true, ma30: true, ma60: true })
76
+ })
77
+
78
+ it('should merge params instead of replacing', () => {
79
+ manager.enableMainIndicator('MA')
80
+
81
+ manager.updateMainIndicatorParams('MA', { ma5: false })
82
+
83
+ const params = manager.getMainIndicatorParams('MA')
84
+ expect(params).toEqual({ ma5: false, ma10: true, ma20: true, ma30: true, ma60: true })
85
+ })
86
+
87
+ it('should schedule a redraw after params update', () => {
88
+ manager.enableMainIndicator('MA')
89
+ vi.clearAllMocks()
90
+
91
+ manager.updateMainIndicatorParams('MA', { ma5: false })
92
+
93
+ expect(deps.scheduleDraw).toHaveBeenCalledTimes(1)
94
+ })
95
+
96
+ it('should be no-op when indicator is not active', () => {
97
+ manager.updateMainIndicatorParams('MA', { ma5: false })
98
+
99
+ expect(deps.scheduleDraw).not.toHaveBeenCalled()
100
+ expect(manager.getMainIndicatorParams('MA')).toBeNull()
101
+ })
102
+ })
103
+ })
@@ -0,0 +1,566 @@
1
+ import type { KLineData } from '../../types/price'
2
+ import { createSignal, computed, type Signal, type Computed } from '../../reactivity/signal'
3
+ import type { IndicatorInstance, SubPaneInfo, PaneSpec, ChartOptions } from '../chartTypes'
4
+ import type { VisibleRange } from '../layout/pane'
5
+ import { UpdateLevel } from '../layout/pane'
6
+ import { IndicatorScheduler } from './scheduler'
7
+ import { getBuiltinIndicatorDefinitions } from './registerBuiltins'
8
+ import { getRegisteredIndicatorDefinitions } from './indicatorDefinitionRegistry'
9
+ import { SubPaneManager, type SubPaneEntry, type SubPaneContext } from '../subPaneManager'
10
+ import { createSubIndicatorRenderer, type SubIndicatorType } from '../renderers/Indicator'
11
+ import { createMainIndicatorLegendRendererPlugin } from '../renderers/Indicator/mainIndicatorLegend'
12
+ import type {
13
+ PluginHostImpl,
14
+ RendererPlugin,
15
+ RendererPluginWithHost,
16
+ } from '../../plugin'
17
+
18
+ type ResolvedChartOptions = Omit<ChartOptions, 'kWidth' | 'kGap'> & {
19
+ kWidth: number
20
+ kGap: number
21
+ }
22
+
23
+ /** 主图指标条目,存在 = 激活 */
24
+ interface MainIndicatorEntry {
25
+ params: Record<string, number | boolean | string>
26
+ }
27
+
28
+ export interface IndicatorDependencies {
29
+ getOption: () => ResolvedChartOptions
30
+ getPluginHost: () => PluginHostImpl
31
+ getRenderer: <T extends RendererPlugin = RendererPlugin>(name: string) => T | undefined
32
+ useRenderer: (plugin: RendererPlugin | RendererPluginWithHost, config?: Record<string, unknown>) => void
33
+ removeRenderer: (name: string) => void
34
+ updateRendererConfig: (name: string, config: Record<string, unknown>) => void
35
+ setRendererEnabled: (name: string, enabled: boolean) => void
36
+ hasPane: (paneId: string) => boolean
37
+ upsertPane: (def: PaneSpec) => void
38
+ removePaneDefinition: (paneId: string) => void
39
+ getPaneSpecs: () => PaneSpec[]
40
+ getPaneRatiosSignal: () => Signal<Readonly<Record<string, number>>>
41
+ getInternalPaneRatios: () => Map<string, number>
42
+ setInternalPaneRatio: (paneId: string, ratio: number) => void
43
+ deleteInternalPaneRatio: (paneId: string) => void
44
+ applyPaneLayoutSpecs: (specs: PaneSpec[]) => void
45
+ getLastVisibleRange: () => VisibleRange
46
+ getCrosshairPos: () => { x: number; y: number } | null
47
+ getCrosshairPrice: () => number | null
48
+ getActivePaneId: () => string | null
49
+ scheduleDraw: (level?: UpdateLevel) => void
50
+ setPendingIndicatorDataUpdate: (v: boolean) => void
51
+ }
52
+
53
+ export class ChartIndicatorManager {
54
+ private deps: IndicatorDependencies
55
+ private indicatorScheduler: IndicatorScheduler
56
+ private subPaneManager: SubPaneManager
57
+ private _mainIndicatorsSignal: Signal<Map<string, MainIndicatorEntry>>
58
+ private _indicatorsComputed: Computed<ReadonlyArray<IndicatorInstance>>
59
+ private _subPanesComputed: Computed<ReadonlyArray<SubPaneInfo>>
60
+ private subPaneCtx: SubPaneContext
61
+
62
+ /** 主图指标默认参数(从注册表中懒加载) */
63
+ private static _defaultMainParamsCache: Record<string, Record<string, number | boolean | string>> | null = null
64
+
65
+ private static get DEFAULT_MAIN_PARAMS(): Record<string, Record<string, number | boolean | string>> {
66
+ if (ChartIndicatorManager._defaultMainParamsCache === null) {
67
+ ChartIndicatorManager._defaultMainParamsCache = {}
68
+ for (const def of getRegisteredIndicatorDefinitions()) {
69
+ if (def.category === 'main') {
70
+ ChartIndicatorManager._defaultMainParamsCache[def.displayName.toUpperCase()] = (def.runtime?.defaultConfig ?? {}) as Record<string, number | boolean | string>
71
+ }
72
+ }
73
+ }
74
+ return ChartIndicatorManager._defaultMainParamsCache
75
+ }
76
+
77
+ /** 可启用的主图指标白名单(从注册表中懒加载) */
78
+ private static _enableMainIndicatorsCache: string[] | null = null
79
+
80
+ private static get ENABLE_MAIN_INDICATORS(): string[] {
81
+ if (ChartIndicatorManager._enableMainIndicatorsCache === null) {
82
+ ChartIndicatorManager._enableMainIndicatorsCache = getRegisteredIndicatorDefinitions()
83
+ .filter(d => d.category === 'main')
84
+ .map(d => d.displayName.toUpperCase())
85
+ }
86
+ return ChartIndicatorManager._enableMainIndicatorsCache
87
+ }
88
+
89
+ /** 副图渲染器名称前缀(保留向后兼容) */
90
+ static readonly SUB_PANE_PREFIX = 'sub_'
91
+
92
+ constructor(deps: IndicatorDependencies) {
93
+ this.deps = deps
94
+
95
+ // 初始化指标调度器
96
+ this.indicatorScheduler = new IndicatorScheduler()
97
+ this.indicatorScheduler.setPluginHost(deps.getPluginHost())
98
+ for (const definition of getBuiltinIndicatorDefinitions()) {
99
+ this.indicatorScheduler.registerIndicator(definition)
100
+ }
101
+ this.indicatorScheduler.setInvalidateCallback(() => {
102
+ deps.setPendingIndicatorDataUpdate(false)
103
+ deps.scheduleDraw()
104
+ })
105
+
106
+ // 初始化副图管理器
107
+ this.subPaneManager = new SubPaneManager()
108
+ this.subPaneCtx = this.createSubPaneContext()
109
+
110
+ // 注册副图活跃列表提供者
111
+ this.indicatorScheduler.setActiveSubPaneProvider(
112
+ () => this.subPaneManager.getPaneIds(),
113
+ )
114
+
115
+ // 初始化主图指标信号
116
+ this._mainIndicatorsSignal = createSignal<Map<string, MainIndicatorEntry>>(new Map())
117
+
118
+ // 派生信号
119
+ const mainSignal = this._mainIndicatorsSignal
120
+ const subPaneManager = this.subPaneManager
121
+ this._indicatorsComputed = computed<ReadonlyArray<IndicatorInstance>>(() => {
122
+ const mainIndicators: IndicatorInstance[] = [...mainSignal().entries()].map(([id, entry]) => ({
123
+ id,
124
+ definitionId: id,
125
+ label: id,
126
+ name: id,
127
+ role: 'main' as const,
128
+ params: { ...entry.params },
129
+ }))
130
+
131
+ const subIndicators: IndicatorInstance[] = subPaneManager.entriesSignal().map(entry => ({
132
+ id: entry.paneId,
133
+ definitionId: entry.indicatorId,
134
+ label: entry.indicatorId,
135
+ name: entry.indicatorId,
136
+ role: 'sub' as const,
137
+ paneId: entry.paneId,
138
+ params: { ...entry.params },
139
+ }))
140
+
141
+ return [...mainIndicators, ...subIndicators]
142
+ })
143
+ this._subPanesComputed = computed<ReadonlyArray<SubPaneInfo>>(() => {
144
+ const ratios = deps.getPaneRatiosSignal()()
145
+ return subPaneManager.entriesSignal().map(entry => ({
146
+ paneId: entry.paneId,
147
+ indicatorId: entry.indicatorId,
148
+ params: { ...entry.params },
149
+ ratio: ratios[entry.paneId] ?? 1,
150
+ }))
151
+ })
152
+
153
+ // dev: 主副图状态变更日志
154
+ if ((import.meta as any).env?.MODE !== 'production') {
155
+ this._indicatorsComputed.subscribe(() => {
156
+ const instances = this._indicatorsComputed.peek()
157
+ console.log('[Chart] indicators signal changed:', instances)
158
+ })
159
+ this._subPanesComputed.subscribe(() => {
160
+ const subPanes = this._subPanesComputed.peek()
161
+ console.log('[Chart] subPanes signal changed:', subPanes)
162
+ })
163
+ }
164
+ }
165
+
166
+ get indicatorSchedulerAccessor(): IndicatorScheduler {
167
+ return this.indicatorScheduler
168
+ }
169
+
170
+ get subPaneManagerAccessor(): SubPaneManager {
171
+ return this.subPaneManager
172
+ }
173
+
174
+ get mainIndicatorsSignalPeek(): Map<string, MainIndicatorEntry> {
175
+ return this._mainIndicatorsSignal.peek()
176
+ }
177
+
178
+ get indicatorsComputed(): Computed<ReadonlyArray<IndicatorInstance>> {
179
+ return this._indicatorsComputed
180
+ }
181
+
182
+ get subPanesComputed(): Computed<ReadonlyArray<SubPaneInfo>> {
183
+ return this._subPanesComputed
184
+ }
185
+
186
+ // ========== SubPaneContext factory ==========
187
+
188
+ private createSubPaneContext(): SubPaneContext {
189
+ const deps = this.deps
190
+ const self = this
191
+ return {
192
+ getIndicatorScheduler: () => self.indicatorScheduler,
193
+ hasPane: (paneId) => deps.hasPane(paneId),
194
+ upsertPane: (def) => deps.upsertPane(def),
195
+ getRenderer: <T extends RendererPlugin = RendererPlugin>(name: string) => deps.getRenderer<T>(name),
196
+ useRenderer: (plugin, config) => deps.useRenderer(plugin, config),
197
+ removeRenderer: (name) => deps.removeRenderer(name),
198
+ removePaneDefinition: (paneId) => deps.removePaneDefinition(paneId),
199
+ updateRendererConfig: (name, config) => deps.updateRendererConfig(name, config),
200
+ getRightAxisWidth: () => deps.getOption().rightAxisWidth,
201
+ getPriceLabelWidth: () => deps.getOption().priceLabelWidth ?? 60,
202
+ getYPaddingPx: () => deps.getOption().yPaddingPx,
203
+ getCrosshairPos: () => deps.getCrosshairPos(),
204
+ getCrosshairPrice: () => deps.getCrosshairPrice(),
205
+ getActivePaneId: () => deps.getActivePaneId(),
206
+ }
207
+ }
208
+
209
+ // ========== 主图指标 API ==========
210
+
211
+ enableMainIndicator(indicatorId: string, params?: Record<string, number | boolean | string>): boolean {
212
+ const id = indicatorId.toUpperCase()
213
+ if (!ChartIndicatorManager.ENABLE_MAIN_INDICATORS.includes(id)) {
214
+ console.warn(`[Chart] 未知的主图指标: ${indicatorId}`)
215
+ return false
216
+ }
217
+
218
+ const map = this._mainIndicatorsSignal.peek()
219
+ const existing = map.get(id)
220
+
221
+ if (existing) {
222
+ if (params) {
223
+ const next = new Map(map)
224
+ next.set(id, { params: { ...existing.params, ...params } })
225
+ this._mainIndicatorsSignal.set(next)
226
+ this.updateIndicatorSchedulerConfig(id)
227
+ }
228
+ return true
229
+ }
230
+
231
+ const defaults = ChartIndicatorManager.DEFAULT_MAIN_PARAMS[id] ?? {}
232
+ const merged = params ? { ...defaults, ...params } : defaults
233
+ const next = new Map(map)
234
+ next.set(id, { params: merged })
235
+ this._mainIndicatorsSignal.set(next)
236
+
237
+ this.enableMainIndicatorRenderer(id)
238
+
239
+ this.updateIndicatorSchedulerConfig(id)
240
+
241
+ this.indicatorScheduler.updateVisibleRange(this.deps.getLastVisibleRange())
242
+
243
+ this.deps.scheduleDraw()
244
+ return true
245
+ }
246
+
247
+ disableMainIndicator(indicatorId: string): boolean {
248
+ const id = indicatorId.toUpperCase()
249
+ const map = this._mainIndicatorsSignal.peek()
250
+ if (!map.has(id)) return false
251
+
252
+ const next = new Map(map)
253
+ next.delete(id)
254
+ this._mainIndicatorsSignal.set(next)
255
+
256
+ this.disableMainIndicatorRenderer(id)
257
+
258
+ this.updateIndicatorSchedulerConfig(id)
259
+
260
+ this.deps.scheduleDraw()
261
+ return true
262
+ }
263
+
264
+ toggleMainIndicator(indicatorId: string, enabled: boolean): void {
265
+ if (enabled) {
266
+ this.enableMainIndicator(indicatorId)
267
+ } else {
268
+ this.disableMainIndicator(indicatorId)
269
+ }
270
+ }
271
+
272
+ getActiveMainIndicators(): string[] {
273
+ return [...this._mainIndicatorsSignal.peek().keys()]
274
+ }
275
+
276
+ isMainIndicatorActive(indicatorId: string): boolean {
277
+ return this._mainIndicatorsSignal.peek().has(indicatorId.toUpperCase())
278
+ }
279
+
280
+ updateMainIndicatorParams(indicatorId: string, params: Record<string, number | boolean | string>): void {
281
+ const id = indicatorId.toUpperCase()
282
+ const map = this._mainIndicatorsSignal.peek()
283
+ const entry = map.get(id)
284
+ if (!entry) return
285
+
286
+ const merged = { ...entry.params, ...params }
287
+ const next = new Map(map)
288
+ next.set(id, { params: merged })
289
+ this._mainIndicatorsSignal.set(next)
290
+
291
+ const rendererName = id.toLowerCase()
292
+ const renderer = this.deps.getRenderer(rendererName)
293
+ if (renderer && (renderer as any).setConfig) {
294
+ ;(renderer as any).setConfig(merged)
295
+ }
296
+
297
+ this.updateIndicatorSchedulerConfig(id)
298
+ this.deps.scheduleDraw()
299
+ }
300
+
301
+ getMainIndicatorParams(indicatorId: string): Record<string, number | boolean | string> | null {
302
+ return this._mainIndicatorsSignal.peek().get(indicatorId.toUpperCase())?.params ?? null
303
+ }
304
+
305
+ clearMainIndicators(): void {
306
+ const map = this._mainIndicatorsSignal.peek()
307
+ for (const id of map.keys()) {
308
+ this.disableMainIndicatorRenderer(id)
309
+ }
310
+ this._mainIndicatorsSignal.set(new Map())
311
+ this.deps.scheduleDraw()
312
+ }
313
+
314
+ private enableMainIndicatorRenderer(indicatorId: string): void {
315
+ const definition = this.indicatorScheduler.getIndicatorMetadata(indicatorId)
316
+ const mainPane = definition?.mainPane
317
+ if (!definition || !mainPane) return
318
+
319
+ if (!this.deps.getRenderer(mainPane.rendererName)) {
320
+ this.deps.useRenderer(definition.rendererFactory({ paneId: 'main', indicatorId }))
321
+ }
322
+
323
+ this.deps.setRendererEnabled(mainPane.rendererName, true)
324
+
325
+ if (!this.deps.getRenderer('mainIndicatorLegend')) {
326
+ this.deps.useRenderer(createMainIndicatorLegendRendererPlugin({ yPaddingPx: this.deps.getOption().yPaddingPx }))
327
+ }
328
+ }
329
+
330
+ private disableMainIndicatorRenderer(indicatorId: string): void {
331
+ const rendererName = this.indicatorScheduler.getIndicatorMetadata(indicatorId)?.mainPane?.rendererName
332
+ if (rendererName) {
333
+ this.deps.setRendererEnabled(rendererName, false)
334
+ }
335
+ }
336
+
337
+ private updateIndicatorSchedulerConfig(indicatorId: string): void {
338
+ const entry = this._mainIndicatorsSignal.peek().get(indicatorId)
339
+ const isActive = entry !== undefined
340
+ const params = entry?.params ?? {}
341
+
342
+ const definition = this.indicatorScheduler.getIndicatorMetadata(indicatorId)
343
+ const toActiveConfig = definition?.mainPane?.toActiveConfig
344
+ if (!definition?.updateConfig || !toActiveConfig) return
345
+
346
+ const config = toActiveConfig(params, isActive)
347
+ if (config !== null) {
348
+ definition.updateConfig(this.indicatorScheduler, config, 'main')
349
+ }
350
+ }
351
+
352
+ /**
353
+ * @deprecated 使用 enableMainIndicator/disableMainIndicator 替代
354
+ */
355
+ setActiveMainIndicators(indicators: string[]): void {
356
+ const newSet = new Set(indicators.map(i => i.toUpperCase()))
357
+ const currentSet = new Set(this._mainIndicatorsSignal.peek().keys())
358
+
359
+ for (const id of currentSet) {
360
+ if (!newSet.has(id)) {
361
+ this.disableMainIndicator(id)
362
+ }
363
+ }
364
+
365
+ for (const id of newSet) {
366
+ if (!currentSet.has(id)) {
367
+ this.enableMainIndicator(id)
368
+ }
369
+ }
370
+ }
371
+
372
+ // ========== 副图管理 API ==========
373
+
374
+ bindIndicatorToPane(paneId: string, indicatorId: SubIndicatorType, params?: Record<string, number | boolean | string>): void {
375
+ if (!this.deps.hasPane(paneId)) {
376
+ this.deps.upsertPane({ id: paneId, ratio: 1, visible: true, role: 'indicator' })
377
+ }
378
+
379
+ const definition = this.indicatorScheduler.getIndicatorMetadata(indicatorId)
380
+ if (!definition) {
381
+ throw new Error(`[Chart] Unknown indicator: ${indicatorId}`)
382
+ }
383
+ const renderer = createSubIndicatorRenderer({ indicatorId, paneId, definition, params })
384
+ const rendererName = renderer.name
385
+ const existing = this.deps.getRenderer(rendererName)
386
+ if (existing) {
387
+ if (params) this.deps.updateRendererConfig(rendererName, params)
388
+ return
389
+ }
390
+
391
+ this.deps.useRenderer(renderer, params)
392
+ }
393
+
394
+ createSubPane(paneId: string, indicatorId: SubIndicatorType, params?: Record<string, number | boolean | string>): boolean {
395
+ const paneSpecs = this.deps.getPaneSpecs()
396
+ const visibleSpecs = paneSpecs.filter((pane) => pane.visible !== false)
397
+ const pricePanes = visibleSpecs.filter((pane) => pane.role === 'price')
398
+ const indicatorPanes = visibleSpecs.filter((pane) => pane.role === 'indicator')
399
+
400
+ if (pricePanes.length === 1) {
401
+ const pricePane = pricePanes[0]
402
+ if (pricePane) {
403
+ this.deps.setInternalPaneRatio(pricePane.id, 3)
404
+ }
405
+ for (const pane of indicatorPanes) {
406
+ this.deps.setInternalPaneRatio(pane.id, 1)
407
+ }
408
+ this.deps.setInternalPaneRatio(paneId, 1)
409
+ } else {
410
+ this.deps.setInternalPaneRatio(paneId, 1)
411
+ }
412
+
413
+ this.deps.upsertPane({ id: paneId, ratio: this.deps.getInternalPaneRatios().get(paneId) ?? 1, visible: true, role: 'indicator' })
414
+
415
+ const success = this.subPaneManager.create(this.subPaneCtx, paneId, indicatorId, params ?? this.getDefaultSubPaneParams(indicatorId))
416
+ return success
417
+ }
418
+
419
+ removeSubPane(paneId: string): void {
420
+ this.subPaneManager.remove(this.subPaneCtx, paneId)
421
+ }
422
+
423
+ replaceSubPaneIndicator(paneId: string, newIndicatorId: SubIndicatorType, params?: Record<string, number | boolean | string>): void {
424
+ this.subPaneManager.replaceIndicator(this.subPaneCtx, paneId, newIndicatorId, params ?? this.getDefaultSubPaneParams(newIndicatorId))
425
+ }
426
+
427
+ updateSubPaneParams(paneId: string, params: Record<string, unknown>): void {
428
+ this.subPaneManager.updateParams(this.subPaneCtx, paneId, params)
429
+ }
430
+
431
+ clearSubPanes(): void {
432
+ const subPaneIds = this.subPaneManager.getPaneIds()
433
+
434
+ if (subPaneIds.length === 0) return
435
+
436
+ this.subPaneManager.clear(this.subPaneCtx)
437
+
438
+ for (const paneId of subPaneIds) {
439
+ this.deps.deleteInternalPaneRatio(paneId)
440
+ }
441
+
442
+ this.deps.applyPaneLayoutSpecs(this.deps.getPaneSpecs().filter((spec) => !subPaneIds.includes(spec.id)))
443
+ }
444
+
445
+ /**
446
+ * @deprecated 使用 getSubPaneEntries 获取完整信息
447
+ */
448
+ getSubPaneIndicators(): SubIndicatorType[] {
449
+ return this.subPaneManager.getAll().map((entry) => entry.indicatorId)
450
+ }
451
+
452
+ getSubPaneEntries(): SubPaneEntry[] {
453
+ return this.subPaneManager.getAll()
454
+ }
455
+
456
+ getSubPaneEntry(paneId: string): SubPaneEntry | undefined {
457
+ return this.subPaneManager.getByPaneId(paneId)
458
+ }
459
+
460
+ private getDefaultSubPaneParams(indicatorId: SubIndicatorType): Record<string, unknown> {
461
+ const defaults: Record<string, Record<string, unknown>> = {
462
+ VOLUME: {},
463
+ MACD: { fastPeriod: 12, slowPeriod: 26, signalPeriod: 9 },
464
+ RSI: { period1: 6, period2: 12, period3: 24 },
465
+ CCI: { period: 14, showCCI: true },
466
+ STOCH: { n: 9, m: 3, showK: true, showD: true },
467
+ MOM: { period: 10, showMOM: true },
468
+ WMSR: { period: 14, showWMSR: true },
469
+ KST: { roc1: 10, roc2: 15, roc3: 20, roc4: 30, signalPeriod: 9, showKST: true, showSignal: true },
470
+ FASTK: { period: 9, showFASTK: true },
471
+ ATR: { period: 14, showATR: true },
472
+ WMA: { period: 10, showWMA: true },
473
+ DEMA: { period: 14, showDEMA: true },
474
+ TEMA: { period: 14, showTEMA: true },
475
+ HMA: { period: 14, showHMA: true },
476
+ KAMA: { period: 10, fastPeriod: 2, slowPeriod: 30, showKAMA: true },
477
+ SAR: { step: 0.02, maxStep: 0.2, showSAR: true },
478
+ SUPERTREND: { atrPeriod: 10, multiplier: 3, showSuperTrend: true },
479
+ KELTNER: { emaPeriod: 20, atrPeriod: 10, multiplier: 2, showUpper: true, showMiddle: true, showLower: true },
480
+ DONCHIAN: { period: 20, showUpper: true, showMiddle: true, showLower: true },
481
+ ICHIMOKU: { tenkanPeriod: 9, kijunPeriod: 26, spanBPeriod: 52, displacement: 26, showTenkan: true, showKijun: true, showSpanA: true, showSpanB: true, showChikou: true, showCloud: true },
482
+ ROC: { period: 12, showROC: true },
483
+ TRIX: { period: 15, signalPeriod: 9, showTRIX: true, showSignal: true },
484
+ HV: { period: 20, annualizationFactor: 252, showHV: true },
485
+ PARKINSON: { period: 20, annualizationFactor: 252, showParkinson: true },
486
+ CHAIKIN_VOL: { emaPeriod: 10, rocPeriod: 10, showChaikinVol: true },
487
+ VMA: { period: 5, showVMA: true },
488
+ OBV: { showOBV: true },
489
+ PVT: { showPVT: true },
490
+ VWAP: { sessionResetGapMs: 0, showVWAP: true },
491
+ CMF: { period: 20, showCMF: true },
492
+ MFI: { period: 14, showMFI: true },
493
+ PIVOT: { showPP: true, showR1: true, showR2: true, showR3: false, showS1: true, showS2: true, showS3: false },
494
+ FIB: { period: 50, showLevels: true },
495
+ STRUCTURE: { leftWindow: 2, rightWindow: 2, breakoutSource: 'close', showSwingLabels: true, showBOS: true, showCHOCH: true, showProvisional: false },
496
+ ZONES: { showFVG: true, showOB: true, showFilledZones: true, obLookback: 5 },
497
+ VOLUME_PROFILE: { bins: 24, lookback: 0, valueAreaPercent: 0.7, showVolumeProfile: true },
498
+ }
499
+ return { ...(defaults[indicatorId] ?? {}) }
500
+ }
501
+
502
+ // ========== 高层指标 API ==========
503
+
504
+ addIndicator(
505
+ definitionId: string,
506
+ role: 'main' | 'sub',
507
+ params?: Record<string, unknown>,
508
+ ): string | null {
509
+ if (role === 'main') {
510
+ const success = this.enableMainIndicator(definitionId, params as Record<string, number | boolean | string>)
511
+ if (!success) return null
512
+ return definitionId.toUpperCase()
513
+ } else {
514
+ const paneId = `${definitionId.toUpperCase()}_${Date.now()}`
515
+ const success = this.createSubPane(
516
+ paneId,
517
+ definitionId as SubIndicatorType,
518
+ params as Record<string, number | boolean | string>,
519
+ )
520
+ if (!success) return null
521
+ return paneId
522
+ }
523
+ }
524
+
525
+ removeIndicator(instanceId: string): boolean {
526
+ const id = instanceId.toUpperCase()
527
+
528
+ if (this._mainIndicatorsSignal.peek().has(id)) {
529
+ return this.disableMainIndicator(instanceId)
530
+ }
531
+
532
+ const subPaneEntry = this.getSubPaneEntry(instanceId)
533
+ if (subPaneEntry) {
534
+ this.removeSubPane(instanceId)
535
+ return true
536
+ }
537
+
538
+ return false
539
+ }
540
+
541
+ updateIndicatorParams(instanceId: string, params: Record<string, unknown>): boolean {
542
+ const id = instanceId.toUpperCase()
543
+
544
+ if (this._mainIndicatorsSignal.peek().has(id)) {
545
+ this.updateMainIndicatorParams(instanceId, params as Record<string, number | boolean | string>)
546
+ return true
547
+ }
548
+
549
+ const subPaneEntry = this.getSubPaneEntry(instanceId)
550
+ if (subPaneEntry) {
551
+ this.updateSubPaneParams(instanceId, params)
552
+ return true
553
+ }
554
+
555
+ return false
556
+ }
557
+
558
+ reorderIndicators(orderedInstanceIds: string[]): boolean {
559
+ console.warn('[Chart] reorderIndicators not fully implemented yet')
560
+ return false
561
+ }
562
+
563
+ destroy(): void {
564
+ this.indicatorScheduler.destroy()
565
+ }
566
+ }