@363045841yyt/klinechart-core 0.7.13 → 0.8.1-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 (93) hide show
  1. package/dist/config/chartSettings.d.ts +0 -6
  2. package/dist/config/chartSettings.d.ts.map +1 -1
  3. package/dist/config/chartSettings.js +0 -1
  4. package/dist/config/chartSettings.js.map +1 -1
  5. package/dist/controllers/createChartController.d.ts.map +1 -1
  6. package/dist/controllers/createChartController.js +26 -0
  7. package/dist/controllers/createChartController.js.map +1 -1
  8. package/dist/controllers/index.d.ts +5 -3
  9. package/dist/controllers/index.d.ts.map +1 -1
  10. package/dist/controllers/index.js +3 -1
  11. package/dist/controllers/index.js.map +1 -1
  12. package/dist/controllers/types.d.ts +22 -0
  13. package/dist/controllers/types.d.ts.map +1 -1
  14. package/dist/data-fetchers/baostock.d.ts +3 -0
  15. package/dist/data-fetchers/baostock.d.ts.map +1 -0
  16. package/dist/data-fetchers/baostock.js +34 -0
  17. package/dist/data-fetchers/baostock.js.map +1 -0
  18. package/dist/data-fetchers/dataBuffer.d.ts +28 -0
  19. package/dist/data-fetchers/dataBuffer.d.ts.map +1 -0
  20. package/dist/data-fetchers/dataBuffer.js +150 -0
  21. package/dist/data-fetchers/dataBuffer.js.map +1 -0
  22. package/dist/data-fetchers/hundred-mock.d.ts +3 -0
  23. package/dist/data-fetchers/hundred-mock.d.ts.map +1 -0
  24. package/dist/data-fetchers/hundred-mock.js +30 -0
  25. package/dist/data-fetchers/hundred-mock.js.map +1 -0
  26. package/dist/data-fetchers/index.d.ts +7 -0
  27. package/dist/data-fetchers/index.d.ts.map +1 -0
  28. package/dist/data-fetchers/index.js +6 -0
  29. package/dist/data-fetchers/index.js.map +1 -0
  30. package/dist/data-fetchers/router.d.ts +3 -0
  31. package/dist/data-fetchers/router.d.ts.map +1 -0
  32. package/dist/data-fetchers/router.js +16 -0
  33. package/dist/data-fetchers/router.js.map +1 -0
  34. package/dist/data-fetchers/thousand-mock.d.ts +3 -0
  35. package/dist/data-fetchers/thousand-mock.d.ts.map +1 -0
  36. package/dist/data-fetchers/thousand-mock.js +29 -0
  37. package/dist/data-fetchers/thousand-mock.js.map +1 -0
  38. package/dist/engine/chart.d.ts +21 -0
  39. package/dist/engine/chart.d.ts.map +1 -1
  40. package/dist/engine/chart.js +113 -3
  41. package/dist/engine/chart.js.map +1 -1
  42. package/dist/engine/renderers/Indicator/{indicatorData.d.ts → indicatorCatalog.d.ts} +1 -1
  43. package/dist/engine/renderers/Indicator/indicatorCatalog.d.ts.map +1 -0
  44. package/dist/engine/renderers/Indicator/{indicatorData.js → indicatorCatalog.js} +94 -406
  45. package/dist/engine/renderers/Indicator/indicatorCatalog.js.map +1 -0
  46. package/dist/engine/renderers/Indicator/structure.js +1 -1
  47. package/dist/engine/renderers/Indicator/structure.js.map +1 -1
  48. package/dist/engine/renderers/Indicator/supertrend.js +1 -1
  49. package/dist/engine/renderers/Indicator/supertrend.js.map +1 -1
  50. package/dist/engine/renderers/paneTitle.d.ts.map +1 -1
  51. package/dist/engine/renderers/paneTitle.js +3 -2
  52. package/dist/engine/renderers/paneTitle.js.map +1 -1
  53. package/dist/engine/subPaneManager.d.ts.map +1 -1
  54. package/dist/engine/subPaneManager.js +2 -1
  55. package/dist/engine/subPaneManager.js.map +1 -1
  56. package/dist/semantic/controller.d.ts +3 -14
  57. package/dist/semantic/controller.d.ts.map +1 -1
  58. package/dist/semantic/controller.js +9 -43
  59. package/dist/semantic/controller.js.map +1 -1
  60. package/dist/semantic/index.d.ts +3 -2
  61. package/dist/semantic/index.d.ts.map +1 -1
  62. package/dist/semantic/index.js +1 -1
  63. package/dist/semantic/index.js.map +1 -1
  64. package/dist/version.d.ts +1 -1
  65. package/dist/version.d.ts.map +1 -1
  66. package/dist/version.js +1 -1
  67. package/dist/version.js.map +1 -1
  68. package/package.json +4 -4
  69. package/src/config/chartSettings.ts +0 -1
  70. package/src/controllers/__tests__/indicatorSelector.test.ts +1 -1
  71. package/src/controllers/createChartController.ts +35 -1
  72. package/src/controllers/index.ts +8 -2
  73. package/src/controllers/types.ts +31 -0
  74. package/src/data-fetchers/__tests__/dataBuffer.test.ts +287 -0
  75. package/src/data-fetchers/baostock.ts +34 -0
  76. package/src/data-fetchers/dataBuffer.ts +176 -0
  77. package/src/data-fetchers/hundred-mock.ts +31 -0
  78. package/src/data-fetchers/index.ts +6 -0
  79. package/src/data-fetchers/router.ts +17 -0
  80. package/src/data-fetchers/thousand-mock.ts +30 -0
  81. package/src/engine/chart.ts +128 -3
  82. package/src/engine/renderers/Indicator/indicatorCatalog.ts +346 -0
  83. package/src/engine/renderers/Indicator/structure.ts +1 -1
  84. package/src/engine/renderers/Indicator/supertrend.ts +1 -1
  85. package/src/engine/renderers/paneTitle.ts +3 -2
  86. package/src/engine/subPaneManager.ts +2 -1
  87. package/src/semantic/__tests__/controller.test.ts +19 -7
  88. package/src/semantic/controller.ts +9 -57
  89. package/src/semantic/index.ts +3 -2
  90. package/src/version.ts +1 -1
  91. package/dist/engine/renderers/Indicator/indicatorData.d.ts.map +0 -1
  92. package/dist/engine/renderers/Indicator/indicatorData.js.map +0 -1
  93. package/src/engine/renderers/Indicator/indicatorData.ts +0 -650
@@ -1,6 +1,8 @@
1
1
  import type { KLineData } from '../types/price'
2
2
  import type { ChartSettings } from '../config/chartSettings'
3
3
  import { createSignal, computed, type Signal, type Computed } from '../reactivity/signal'
4
+ import type { SymbolSpec, DataFetcher } from '../controllers/types'
5
+ import { DataBuffer } from '../data-fetchers/dataBuffer'
4
6
  import { getVisibleRange } from './viewport/viewport'
5
7
  import { Pane, type VisibleRange, UpdateLevel } from './layout/pane'
6
8
  import { InteractionController, type InteractionSnapshot } from './controller/interaction'
@@ -157,6 +159,9 @@ export class Chart {
157
159
  private dom: ChartDom
158
160
  private opt: ResolvedChartOptions
159
161
  private _internalData: KLineData[] = []
162
+ private _dataFetcher: DataFetcher | null = null
163
+ private _dataBuffer: DataBuffer = new DataBuffer()
164
+ private _dataBufferUnsub: (() => void) | null = null
160
165
 
161
166
  private raf: number | null = null
162
167
  private pendingUpdateLevel: UpdateLevel = UpdateLevel.All
@@ -742,7 +747,10 @@ export class Chart {
742
747
 
743
748
  // 2. 准备帧数据(视口 / 可见范围 / K 线坐标,优先走缓存)
744
749
  const frame = this.prepareFrameData(level)
745
- if (!frame) return
750
+ if (!frame) {
751
+ if (this._internalData.length === 0) this.clearAllCanvases()
752
+ return
753
+ }
746
754
 
747
755
  const { vp, range, kLinePositions, kLineCenters, kBarRects, kWidthPx, useCachedFrame } = frame
748
756
 
@@ -794,6 +802,7 @@ export class Chart {
794
802
  if (!useCachedFrame && (range.start !== this.lastVisibleRange.start || range.end !== this.lastVisibleRange.end)) {
795
803
  this.indicatorScheduler.updateVisibleRange(range)
796
804
  this.lastVisibleRange = range
805
+ this.checkVisibleRangeGap()
797
806
  }
798
807
 
799
808
  const kLinePositions = useCachedFrame
@@ -840,6 +849,24 @@ export class Chart {
840
849
  return { vp, range, kLinePositions, kLineCenters, kBarRects, kWidthPx, useCachedFrame }
841
850
  }
842
851
 
852
+ private clearAllCanvases() {
853
+ const vp = this.computeViewport()
854
+ if (!vp) return
855
+ for (const r of this.paneRenderers) {
856
+ const { mainCtx, overlayCtx, yAxisCtx } = r.getContexts()
857
+ const pane = r.getPane()
858
+ mainCtx?.clearRect(0, 0, vp.plotWidth + 1, pane.height + 2 / vp.dpr)
859
+ overlayCtx?.clearRect(0, 0, vp.plotWidth + 1, pane.height + 2 / vp.dpr)
860
+ yAxisCtx?.clearRect(0, 0, vp.plotWidth + 1, pane.height + 2 / vp.dpr)
861
+ }
862
+ const xCtx = this.xAxisCtx
863
+ if (xCtx) {
864
+ const xW = xCtx.canvas.width
865
+ const xH = xCtx.canvas.height
866
+ xCtx.clearRect(0, 0, xW, xH)
867
+ }
868
+ }
869
+
843
870
  private renderPanes(
844
871
  vp: Viewport,
845
872
  range: VisibleRange,
@@ -1654,6 +1681,9 @@ export class Chart {
1654
1681
  }
1655
1682
 
1656
1683
 
1684
+ private static readonly LEADING_SLOTS = 60
1685
+ private static readonly TRAILING_DRAWING_SLOTS = 24
1686
+
1657
1687
  /** 获取内容总宽度(用于外部 scroll-content 撑开 scrollWidth) */
1658
1688
  getContentWidth(): number {
1659
1689
  const dataLength = this._internalData.length
@@ -1662,9 +1692,8 @@ export class Chart {
1662
1692
  const kGap = this.opt.kGap
1663
1693
  const viewWidth = this._internalViewport?.plotWidth ?? 0
1664
1694
  const dpr = this.getEffectiveDpr()
1665
- const TRAILING_DRAWING_SLOTS = 24
1666
1695
  const { startXPx, unitPx } = getPhysicalKLineConfig(kWidth, kGap, dpr)
1667
- const dataPlotWidth = (startXPx + (dataLength + TRAILING_DRAWING_SLOTS) * unitPx) / dpr
1696
+ const dataPlotWidth = (startXPx + (Chart.LEADING_SLOTS + dataLength + Chart.TRAILING_DRAWING_SLOTS) * unitPx) / dpr
1668
1697
  return Math.max(dataPlotWidth, viewWidth)
1669
1698
  }
1670
1699
 
@@ -1725,6 +1754,12 @@ export class Chart {
1725
1754
  this.raf = null
1726
1755
  }
1727
1756
 
1757
+ if (this._dataBufferUnsub) {
1758
+ this._dataBufferUnsub()
1759
+ this._dataBufferUnsub = null
1760
+ }
1761
+ this._dataBuffer.dispose()
1762
+
1728
1763
  // 清理尺寸观察器
1729
1764
  this.resizeObserver?.disconnect()
1730
1765
  this.resizeObserver = undefined
@@ -2096,6 +2131,7 @@ export class Chart {
2096
2131
  })
2097
2132
 
2098
2133
  private _dataSignal = createSignal<ReadonlyArray<KLineData>>([])
2134
+ private _symbolsSignal = createSignal<ReadonlyArray<SymbolSpec>>([])
2099
2135
  private _themeSignal = createSignal<'light' | 'dark'>('light')
2100
2136
  private _drawingToolSignal = createSignal<DrawingToolType | null>(null)
2101
2137
  private _drawingsSignal = createSignal<ReadonlyArray<import('../plugin').DrawingObject>>([])
@@ -2160,6 +2196,11 @@ export class Chart {
2160
2196
  return this._dataSignal
2161
2197
  }
2162
2198
 
2199
+ /** 符号信号 */
2200
+ get symbols(): Signal<ReadonlyArray<SymbolSpec>> {
2201
+ return this._symbolsSignal
2202
+ }
2203
+
2163
2204
  /** 主题信号 */
2164
2205
  get theme(): Signal<'light' | 'dark'> {
2165
2206
  return this._themeSignal
@@ -2218,6 +2259,90 @@ export class Chart {
2218
2259
  this.setData(merged)
2219
2260
  }
2220
2261
 
2262
+ /**
2263
+ * 设置数据获取器适配器
2264
+ */
2265
+ setDataFetcher(fetcher: DataFetcher | null): void {
2266
+ this._dataFetcher = fetcher
2267
+ this._dataBuffer.setFetcher(fetcher)
2268
+ }
2269
+
2270
+ get dataBuffer(): DataBuffer {
2271
+ return this._dataBuffer
2272
+ }
2273
+
2274
+ checkVisibleRangeGap(): void {
2275
+ if (this._internalData.length === 0) return
2276
+ const window = this._dataBuffer.loadedWindow
2277
+ if (!window) return
2278
+ const range = this.lastVisibleRange
2279
+
2280
+ if (range.start <= 5 && this._dataFetcher) {
2281
+ const MS_PER_DAY = 86_400_000
2282
+ const earlierThanEarliest = window.earliestTs - 90 * MS_PER_DAY
2283
+ this._dataBuffer.ensureRange(earlierThanEarliest, window.earliestTs)
2284
+ return
2285
+ }
2286
+
2287
+ if (range.start >= this._internalData.length) return
2288
+ const firstVisibleTs = this._internalData[Math.max(0, range.start)]?.timestamp
2289
+ if (firstVisibleTs === undefined) return
2290
+ if (firstVisibleTs < window.earliestTs) {
2291
+ this._dataBuffer.ensureRange(firstVisibleTs, window.earliestTs)
2292
+ }
2293
+ }
2294
+
2295
+ /**
2296
+ * 设置当前符号并触发数据加载
2297
+ */
2298
+ setSymbols(specs: ReadonlyArray<SymbolSpec>): void {
2299
+ this._symbolsSignal.set(specs)
2300
+ if (specs.length === 0) return
2301
+ const spec = specs[0]!
2302
+ if (!this._dataFetcher) return
2303
+
2304
+ this._dataBuffer.setFetcher(this._dataFetcher)
2305
+
2306
+ this._dataBuffer.onPrepend = (count: number) => {
2307
+ const dpr = this.getEffectiveDpr()
2308
+ const { unitPx } = getPhysicalKLineConfig(this.opt.kWidth, this.opt.kGap, dpr)
2309
+ const compensation = (count * unitPx) / dpr
2310
+ const container = this.dom.container
2311
+ if (container) {
2312
+ container.scrollLeft += compensation
2313
+ this.cachedScrollLeft = container.scrollLeft
2314
+ }
2315
+ }
2316
+
2317
+ if (!this._dataBufferUnsub) {
2318
+ this._dataBufferUnsub = this._dataBuffer.data.subscribe(() => {
2319
+ const bufferData = this._dataBuffer.data.peek()
2320
+ this._internalData = [...bufferData]
2321
+ this._dataSignal.set([...this._internalData])
2322
+ this.interaction.reset()
2323
+ if (this.lastVisibleRange.start === 0 && this.lastVisibleRange.end === 0 && this._internalData.length > 0) {
2324
+ const plotWidth = this.observedSize.width > 0
2325
+ ? this.observedSize.width
2326
+ : Math.max(1, Math.round(this.dom.container?.clientWidth ?? 800))
2327
+ const dpr = this.getEffectiveDpr()
2328
+ const { start, end } = getVisibleRange(
2329
+ this.cachedScrollLeft,
2330
+ plotWidth,
2331
+ this.opt.kWidth,
2332
+ this.opt.kGap,
2333
+ this._internalData.length,
2334
+ dpr,
2335
+ )
2336
+ this.lastVisibleRange = { start, end }
2337
+ }
2338
+ this.indicatorScheduler.update(this._internalData, this.lastVisibleRange)
2339
+ this.scheduleDraw()
2340
+ })
2341
+ }
2342
+
2343
+ this._dataBuffer.setSymbol(spec)
2344
+ }
2345
+
2221
2346
  // ---------- Theme ----------
2222
2347
 
2223
2348
  /**
@@ -0,0 +1,346 @@
1
+ import { getRegisteredIndicatorDefinitions } from '../../indicators/indicatorDefinitionRegistry'
2
+
3
+ export interface ParamConfig {
4
+ key: string
5
+ label: string
6
+ type: 'number'
7
+ min?: number
8
+ max?: number
9
+ step?: number
10
+ default?: number
11
+ description?: string
12
+ }
13
+
14
+ export interface Indicator {
15
+ id: string
16
+ label: string
17
+ name: string
18
+ pane: 'main' | 'sub'
19
+ description?: string
20
+ params?: ParamConfig[]
21
+ }
22
+
23
+ function normalizeId(id: string): string {
24
+ return id.trim().toLowerCase().replace(/[^a-z0-9]/g, '')
25
+ }
26
+
27
+ // UI-only metadata that don't belong in the @Indicator decorator (rendering layer)
28
+ const uiMeta: Record<string, {
29
+ name: string
30
+ description: string
31
+ params?: ParamConfig[]
32
+ }> = {
33
+ ma: {
34
+ name: '均线',
35
+ description: '',
36
+ params: [],
37
+ },
38
+ volume: {
39
+ name: '成交量',
40
+ description:
41
+ '成交量反映市场活跃度,柱状图显示每根K线的交易量。上涨时柱子为红色,下跌时为绿色。',
42
+ },
43
+ boll: {
44
+ name: '布林带',
45
+ description:
46
+ '布林带由三条轨道线组成,用于判断价格的波动范围和趋势强度。价格触及上轨可能超买,触及下轨可能超卖。',
47
+ params: [
48
+ { key: 'period', label: '周期', type: 'number', min: 2, max: 100, step: 1, default: 20, description: '计算移动平均线的周期数,周期越长轨道越平滑' },
49
+ { key: 'multiplier', label: '倍数', type: 'number', min: 0.1, max: 5, step: 0.1, default: 2, description: '标准差倍数,决定轨道宽度,通常为 2' },
50
+ ],
51
+ },
52
+ expma: {
53
+ name: '指数平滑移动平均线',
54
+ description:
55
+ 'EXPMA 对近期价格给予更高权重,比普通 MA 更敏感。快线上穿慢线为金叉看涨,下穿为死叉看跌。',
56
+ params: [
57
+ { key: 'fastPeriod', label: '快线', type: 'number', min: 2, max: 100, step: 1, default: 12, description: '快线周期,对价格变化更敏感' },
58
+ { key: 'slowPeriod', label: '慢线', type: 'number', min: 2, max: 200, step: 1, default: 50, description: '慢线周期,用于判断趋势方向' },
59
+ ],
60
+ },
61
+ ene: {
62
+ name: '轨道线',
63
+ description:
64
+ 'ENE 轨道线由三条轨道组成,价格突破上轨可能超买,突破下轨可能超卖,适合判断震荡行情的买卖点。',
65
+ params: [
66
+ { key: 'period', label: '周期', type: 'number', min: 2, max: 100, step: 1, default: 10, description: '计算中轨的周期数' },
67
+ { key: 'deviation', label: '偏离率', type: 'number', min: 1, max: 30, step: 0.5, default: 11, description: '轨道偏离率百分比,决定轨道宽度' },
68
+ ],
69
+ },
70
+ macd: {
71
+ name: '指数平滑异同移动平均线',
72
+ description:
73
+ 'MACD 通过快慢均线的交叉判断趋势方向和动量。DIF 上穿 DEA 为金叉看涨,下穿为死叉看跌。',
74
+ params: [
75
+ { key: 'fastPeriod', label: '快线', type: 'number', min: 2, max: 50, step: 1, default: 12, description: '快线 EMA 周期,对价格变化更敏感' },
76
+ { key: 'slowPeriod', label: '慢线', type: 'number', min: 2, max: 100, step: 1, default: 26, description: '慢线 EMA 周期,用于计算 DIF' },
77
+ { key: 'signalPeriod', label: '信号', type: 'number', min: 2, max: 50, step: 1, default: 9, description: 'DEA 的 EMA 周期,用于生成买卖信号' },
78
+ ],
79
+ },
80
+ rsi: {
81
+ name: '相对强弱指标',
82
+ description: 'RSI 衡量价格变动的速度和幅度,判断超买超卖状态。RSI > 70 超买,RSI < 30 超卖。',
83
+ params: [
84
+ { key: 'period1', label: '周期 1', type: 'number', min: 2, max: 100, step: 1, default: 6, description: '第一条 RSI 周期,通常为 6(快线)' },
85
+ { key: 'period2', label: '周期 2', type: 'number', min: 2, max: 100, step: 1, default: 12, description: '第二条 RSI 周期,通常为 12(中线)' },
86
+ { key: 'period3', label: '周期 3', type: 'number', min: 2, max: 100, step: 1, default: 24, description: '第三条 RSI 周期,通常为 24(慢线)' },
87
+ ],
88
+ },
89
+ cci: {
90
+ name: '顺势指标',
91
+ description:
92
+ 'CCI 衡量价格与统计平均值的偏离程度。CCI > 100 超买,CCI < -100 超卖,适合捕捉趋势反转。',
93
+ params: [
94
+ { key: 'period', label: '周期', type: 'number', min: 2, max: 100, step: 1, default: 14, description: '计算周期,周期越短信号越灵敏' },
95
+ ],
96
+ },
97
+ stoch: {
98
+ name: '随机指标',
99
+ description:
100
+ 'KDJ 随机指标通过比较收盘价与价格区间判断超买超卖。K > 80 超买,K < 20 超卖,K 上穿 D 金叉。',
101
+ params: [
102
+ { key: 'n', label: 'K 周期', type: 'number', min: 2, max: 100, step: 1, default: 9, description: '计算 K 值的周期,统计 N 日内价格区间' },
103
+ { key: 'm', label: 'D 周期', type: 'number', min: 1, max: 50, step: 1, default: 3, description: 'D 值是 K 的 M 日移动平均,使信号更平滑' },
104
+ ],
105
+ },
106
+ mom: {
107
+ name: '动量指标',
108
+ description:
109
+ '动量指标衡量价格变化的速度,MOM > 0 表示上涨动能,MOM < 0 表示下跌动能。适合判断趋势强度。',
110
+ params: [
111
+ { key: 'period', label: '周期', type: 'number', min: 2, max: 100, step: 1, default: 10, description: '与多少日前价格比较,周期越短越灵敏' },
112
+ ],
113
+ },
114
+ wmsr: {
115
+ name: '威廉指标',
116
+ description: '威廉指标衡量超买超卖程度,范围为 -100 到 0。WMSR > -20 超买,WMSR < -80 超卖。',
117
+ params: [
118
+ { key: 'period', label: '周期', type: 'number', min: 2, max: 100, step: 1, default: 14, description: '回溯周期,统计周期内最高最低价' },
119
+ ],
120
+ },
121
+ kst: {
122
+ name: '确然指标',
123
+ description:
124
+ 'KST 综合多个 ROC 判断长期趋势,KST 上穿信号线看涨,下穿看跌。适合捕捉主要趋势转换。',
125
+ params: [
126
+ { key: 'roc1', label: 'ROC1', type: 'number', min: 2, max: 100, step: 1, default: 10, description: '短期变化率周期' },
127
+ { key: 'roc2', label: 'ROC2', type: 'number', min: 2, max: 100, step: 1, default: 15, description: '中短期变化率周期' },
128
+ { key: 'roc3', label: 'ROC3', type: 'number', min: 2, max: 100, step: 1, default: 20, description: '中长期变化率周期' },
129
+ { key: 'roc4', label: 'ROC4', type: 'number', min: 2, max: 100, step: 1, default: 30, description: '长期变化率周期' },
130
+ { key: 'signalPeriod', label: '信号', type: 'number', min: 2, max: 50, step: 1, default: 9, description: '信号线的 SMA 周期' },
131
+ ],
132
+ },
133
+ fastk: {
134
+ name: '快速随机指标',
135
+ description:
136
+ 'FASTK 是未经过平滑处理的随机指标,比普通 KDJ 更敏感,能更快捕捉价格转折点,但假信号也更多。',
137
+ params: [
138
+ { key: 'period', label: '周期', type: 'number', min: 2, max: 100, step: 1, default: 9, description: '计算周期,周期越短越敏感' },
139
+ ],
140
+ },
141
+ atr: {
142
+ name: '平均真实波幅',
143
+ description:
144
+ 'ATR(Average True Range)衡量市场波动性,值越大表示波动越剧烈。Wilder 平滑算法,常用于设置止损位和判断趋势强度。',
145
+ params: [
146
+ { key: 'period', label: '周期', type: 'number', min: 1, max: 100, step: 1, default: 14, description: 'ATR 计算周期,周期越长曲线越平滑' },
147
+ ],
148
+ },
149
+ wma: {
150
+ name: '加权移动平均',
151
+ description: 'WMA 对近期价格赋予更高权重,比普通 MA 反应更快,适用于短期趋势跟踪。',
152
+ params: [{ key: 'period', label: '周期', type: 'number', min: 2, max: 100, step: 1, default: 10, description: '计算周期' }],
153
+ },
154
+ dema: {
155
+ name: '双指数移动平均',
156
+ description: 'DEMA 通过双重平滑减少滞后,比传统 EMA 更贴近价格,适合快速趋势判断。',
157
+ params: [{ key: 'period', label: '周期', type: 'number', min: 2, max: 100, step: 1, default: 14, description: '计算周期' }],
158
+ },
159
+ tema: {
160
+ name: '三指数移动平均',
161
+ description: 'TEMA 三重平滑,滞后最小,响应最快,适合敏锐捕捉价格变化。',
162
+ params: [{ key: 'period', label: '周期', type: 'number', min: 2, max: 100, step: 1, default: 14, description: '计算周期' }],
163
+ },
164
+ hma: {
165
+ name: '赫尔移动平均',
166
+ description: 'HMA 通过加权移动平均和平方根计算,在平滑度和响应速度之间取得极佳平衡。',
167
+ params: [{ key: 'period', label: '周期', type: 'number', min: 2, max: 100, step: 1, default: 14, description: '计算周期' }],
168
+ },
169
+ kama: {
170
+ name: '考夫曼自适应移动平均',
171
+ description: 'KAMA 根据市场噪音自适应调整平滑度,趋势强时快速跟随,震荡时平滑过滤。',
172
+ params: [
173
+ { key: 'period', label: '周期', type: 'number', min: 2, max: 100, step: 1, default: 10, description: 'ER 计算周期' },
174
+ { key: 'fastPeriod', label: '快线', type: 'number', min: 2, max: 30, step: 1, default: 2, description: '最快平滑系数' },
175
+ { key: 'slowPeriod', label: '慢线', type: 'number', min: 2, max: 60, step: 1, default: 30, description: '最慢平滑系数' },
176
+ ],
177
+ },
178
+ sar: {
179
+ name: '抛物线转向',
180
+ description: 'SAR(Parabolic Stop and Reverse)通过抛物线轨迹跟踪趋势,提供动态止损和反转信号。',
181
+ params: [
182
+ { key: 'step', label: '步长', type: 'number', min: 0.001, max: 0.1, step: 0.001, default: 0.02, description: '加速度步长' },
183
+ { key: 'maxStep', label: '最大步长', type: 'number', min: 0.01, max: 0.5, step: 0.01, default: 0.2, description: '最大加速度上限' },
184
+ ],
185
+ },
186
+ supertrend: {
187
+ name: '超级趋势',
188
+ description: 'SuperTrend 基于 ATR 的通道突破系统,价格在通道上方为多头趋势,下方为空头趋势。',
189
+ params: [
190
+ { key: 'atrPeriod', label: 'ATR 周期', type: 'number', min: 2, max: 100, step: 1, default: 10, description: 'ATR 计算周期' },
191
+ { key: 'multiplier', label: '倍数', type: 'number', min: 0.5, max: 10, step: 0.5, default: 3, description: 'ATR 倍数' },
192
+ ],
193
+ },
194
+ keltner: {
195
+ name: '肯特纳通道',
196
+ description: 'Keltner Channel 以 EMA 为中轨,ATR 倍数确定通道宽度,适合判断突破与回归。',
197
+ params: [
198
+ { key: 'emaPeriod', label: 'EMA 周期', type: 'number', min: 2, max: 100, step: 1, default: 20, description: '中轨 EMA 周期' },
199
+ { key: 'atrPeriod', label: 'ATR 周期', type: 'number', min: 2, max: 100, step: 1, default: 10, description: 'ATR 计算周期' },
200
+ { key: 'multiplier', label: '倍数', type: 'number', min: 0.5, max: 10, step: 0.5, default: 2, description: 'ATR 倍数' },
201
+ ],
202
+ },
203
+ donchian: {
204
+ name: '唐奇安通道',
205
+ description: 'Donchian Channel 显示 N 日内最高价和最低价的通道,价格突破上下轨视为趋势信号。',
206
+ params: [{ key: 'period', label: '周期', type: 'number', min: 2, max: 200, step: 1, default: 20, description: '通道周期' }],
207
+ },
208
+ ichimoku: {
209
+ name: '一目均衡表',
210
+ description: 'Ichimoku Kinko Hyo(一目均衡表)综合显示支撑阻力、趋势方向和动量,云区变色提示趋势转换。',
211
+ params: [
212
+ { key: 'tenkanPeriod', label: '转折线', type: 'number', min: 2, max: 100, step: 1, default: 9, description: 'Tenkan-sen 周期' },
213
+ { key: 'kijunPeriod', label: '基准线', type: 'number', min: 2, max: 100, step: 1, default: 26, description: 'Kijun-sen 周期' },
214
+ { key: 'spanBPeriod', label: '领先线 B', type: 'number', min: 2, max: 200, step: 1, default: 52, description: 'Senkou Span B 周期' },
215
+ { key: 'displacement', label: '偏移', type: 'number', min: 1, max: 52, step: 1, default: 26, description: '偏移量' },
216
+ ],
217
+ },
218
+ roc: {
219
+ name: '变化率',
220
+ description: 'ROC 衡量当前价格与 N 日前价格的百分比变化,正值表示上涨动能,负值表示下跌动能。',
221
+ params: [{ key: 'period', label: '周期', type: 'number', min: 2, max: 100, step: 1, default: 12, description: 'ROC 周期' }],
222
+ },
223
+ trix: {
224
+ name: '三重指数平滑平均',
225
+ description: 'TRIX 三平滑后取 ROC,过滤短期波动,上穿信号线为买入信号,下穿为卖出信号。',
226
+ params: [
227
+ { key: 'period', label: '周期', type: 'number', min: 2, max: 100, step: 1, default: 15, description: 'TRIX 周期' },
228
+ { key: 'signalPeriod', label: '信号', type: 'number', min: 2, max: 50, step: 1, default: 9, description: '信号线周期' },
229
+ ],
230
+ },
231
+ hv: {
232
+ name: '历史波动率',
233
+ description: 'HV 衡量过去 N 日对数收益率的年化标准差,值越高表示市场波动越大。',
234
+ params: [
235
+ { key: 'period', label: '周期', type: 'number', min: 2, max: 100, step: 1, default: 20, description: '计算周期' },
236
+ { key: 'annualizationFactor', label: '年化因子', type: 'number', min: 1, max: 365, step: 1, default: 252, description: '年化天数' },
237
+ ],
238
+ },
239
+ parkinson: {
240
+ name: '帕金森波动率',
241
+ description: 'Parkinson 利用最高最低价估算波动率,比 HV 更高效,捕捉日内波动范围。',
242
+ params: [
243
+ { key: 'period', label: '周期', type: 'number', min: 2, max: 100, step: 1, default: 20, description: '计算周期' },
244
+ { key: 'annualizationFactor', label: '年化因子', type: 'number', min: 1, max: 365, step: 1, default: 252, description: '年化天数' },
245
+ ],
246
+ },
247
+ chaikin_vol: {
248
+ name: '蔡金波动率',
249
+ description: 'Chaikin Volatility 衡量价格区间的宽度变化,波动率扩张预示突破,收缩预示盘整。',
250
+ params: [
251
+ { key: 'emaPeriod', label: 'EMA 周期', type: 'number', min: 2, max: 100, step: 1, default: 10, description: '平滑周期' },
252
+ { key: 'rocPeriod', label: 'ROC 周期', type: 'number', min: 2, max: 100, step: 1, default: 10, description: '变化率周期' },
253
+ ],
254
+ },
255
+ vma: {
256
+ name: '成交量移动平均',
257
+ description: 'VMA 对成交量做移动平均,量能上升确认趋势,量能萎缩预示反转。',
258
+ params: [{ key: 'period', label: '周期', type: 'number', min: 2, max: 100, step: 1, default: 5, description: 'VMA 周期' }],
259
+ },
260
+ obv: {
261
+ name: '能量潮',
262
+ description: 'OBV 累积量能,价格上涨时加量,下跌时减量。OBV 趋势与价格背离常预示反转。',
263
+ },
264
+ pvt: {
265
+ name: '价量趋势',
266
+ description: 'PVT 将成交量按价格变化率加权累计,比 OBV 更精细,反映资金流入流出力度。',
267
+ },
268
+ vwap: {
269
+ name: '成交量加权均价',
270
+ description: 'VWAP(Volume-Weighted Average Price)以成交量加权的均价线,机构常用作日内交易基准。',
271
+ params: [{ key: 'sessionResetGapMs', label: '重置间隔', type: 'number', min: 0, max: 86400000, step: 3600000, default: 0, description: '0=不重置,>0=超过间隔毫秒重置' }],
272
+ },
273
+ cmf: {
274
+ name: '蔡金资金流',
275
+ description: 'CMF(Chaikin Money Flow)衡量资金流入流出强度,正值看涨,负值看跌。',
276
+ params: [{ key: 'period', label: '周期', type: 'number', min: 2, max: 100, step: 1, default: 20, description: 'CMF 周期' }],
277
+ },
278
+ mfi: {
279
+ name: '资金流量指数',
280
+ description: 'MFI(Money Flow Index)类似 RSI 但用量能加权,判断超买超卖。MFI > 80 超买,MFI < 20 超卖。',
281
+ params: [{ key: 'period', label: '周期', type: 'number', min: 2, max: 100, step: 1, default: 14, description: 'MFI 周期' }],
282
+ },
283
+ pivot: {
284
+ name: '枢轴点',
285
+ description: 'Pivot Points 根据前日最高/最低/收盘计算支撑阻力位,R1-R3 为阻力,S1-S3 为支撑。',
286
+ },
287
+ fib: {
288
+ name: '斐波那契',
289
+ description: 'Fibonacci 回调线标记关键回撤位(0.236/0.382/0.5/0.618/0.786),用于判断回调目标。',
290
+ params: [{ key: 'period', label: '周期', type: 'number', min: 5, max: 500, step: 1, default: 50, description: '计算周期' }],
291
+ },
292
+ structure: {
293
+ name: 'SMC 结构',
294
+ description: 'Swing / BOS / CHOCH 识别市场结构转换,突破结构(BOS)确认趋势,趋势反转(CHOCH)提示反转。',
295
+ params: [
296
+ { key: 'leftWindow', label: '左窗口', type: 'number', min: 1, max: 20, step: 1, default: 2, description: '摆点左侧 K 线数' },
297
+ { key: 'rightWindow', label: '右窗口', type: 'number', min: 1, max: 20, step: 1, default: 2, description: '摆点右侧 K 线数' },
298
+ ],
299
+ },
300
+ zones: {
301
+ name: 'SMC 区域',
302
+ description: 'FVG(公允价值缺口)和 Order Block(订单块)识别机构交易行为中的关键流动性区域。',
303
+ params: [
304
+ { key: 'obLookback', label: 'OB 回溯', type: 'number', min: 1, max: 50, step: 1, default: 5, description: 'Order Block 回溯窗口' },
305
+ ],
306
+ },
307
+ volume_profile: {
308
+ name: '成交量分布',
309
+ description: 'Volume Profile 显示各价位成交量分布,识别高量区域(价值区)和低量区域(缺口),POC 为成交量最大价位。',
310
+ params: [
311
+ { key: 'bins', label: '分箱数', type: 'number', min: 5, max: 100, step: 1, default: 24, description: '价格分箱数' },
312
+ { key: 'lookback', label: '回溯', type: 'number', min: 0, max: 500, step: 1, default: 0, description: '0=全部数据' },
313
+ { key: 'valueAreaPercent', label: '价值区%', type: 'number', min: 0.1, max: 1, step: 0.05, default: 0.7, description: '价值区占比' },
314
+ ],
315
+ },
316
+ }
317
+
318
+ function buildAllIndicators(): Indicator[] {
319
+ return getRegisteredIndicatorDefinitions().map((def) => {
320
+ const key = normalizeId(def.name)
321
+ const ui = uiMeta[key]
322
+ return {
323
+ id: def.displayName.toUpperCase(),
324
+ label: def.displayName,
325
+ name: ui?.name ?? def.displayName,
326
+ pane: def.category === 'main' || def.allowMainPane ? 'main' : 'sub',
327
+ description: ui?.description,
328
+ params: ui?.params,
329
+ }
330
+ })
331
+ }
332
+
333
+ export const allIndicators: Indicator[] = buildAllIndicators()
334
+
335
+ export const mainIndicators = allIndicators.filter((i) => i.pane === 'main')
336
+ export const subIndicators = allIndicators.filter((i) => i.pane === 'sub')
337
+
338
+ export function findIndicator(id: string): Indicator | undefined {
339
+ const norm = normalizeId(id)
340
+ return allIndicators.find((i) => normalizeId(i.id) === norm || normalizeId(i.label) === norm)
341
+ }
342
+
343
+ export function isSubIndicatorId(id: string): boolean {
344
+ const indicator = findIndicator(id)
345
+ return indicator?.pane === 'sub'
346
+ }
@@ -142,7 +142,7 @@ export function getStructureTitleInfo(
142
142
  @Indicator({
143
143
  name: 'structure',
144
144
  displayName: 'Structure',
145
- category: 'sub',
145
+ category: 'main',
146
146
  defaultPaneId: 'sub_Structure',
147
147
  allowMainPane: true,
148
148
  mainPane: { rendererName: 'structure_main', toActiveConfig: (params, active) => ({ ...params, showSwingLabels: active, showBOS: active, showCHOCH: active }) },
@@ -126,7 +126,7 @@ export function getSuperTrendTitleInfo(
126
126
  name: 'supertrend',
127
127
  displayName: 'SuperTrend',
128
128
  getTitleInfo: getSuperTrendTitleInfo,
129
- category: 'oscillator',
129
+ category: 'main',
130
130
  defaultPaneId: 'sub_SuperTrend',
131
131
  allowMainPane: true,
132
132
  mainPane: { rendererName: 'supertrend_main', toActiveConfig: (params, active) => ({ ...params, showSuperTrend: active }) },
@@ -123,7 +123,7 @@ export function createPaneTitleRendererPlugin(options: PaneTitleOptions): Render
123
123
  }
124
124
 
125
125
  if (titleInfo.values && titleInfo.values.length > 0) {
126
- y += 1
126
+ // y += 1
127
127
  for (const item of titleInfo.values) {
128
128
  const valueText = `${item.label} ${item.value.toFixed(3)}`
129
129
  overlayCtx.fillStyle = item.color
@@ -133,7 +133,8 @@ export function createPaneTitleRendererPlugin(options: PaneTitleOptions): Render
133
133
  }
134
134
  } else {
135
135
  overlayCtx.fillStyle = colors.text.primary
136
- overlayCtx.fillText(currentOptions.title, x, y)
136
+ const fallbackTitle = meta?.displayName ?? currentOptions.title
137
+ overlayCtx.fillText(fallbackTitle, x, y)
137
138
 
138
139
  if (currentOptions.description) {
139
140
  const titleWidth = measureTextWidth(overlayCtx, currentOptions.title)
@@ -4,6 +4,7 @@ import { createSignal, type Signal } from '../reactivity/signal'
4
4
  import { createSubIndicatorRenderer } from './renderers/Indicator'
5
5
  import { createPaneTitleRendererPlugin } from './renderers/paneTitle'
6
6
  import { createIndicatorScaleRendererPlugin } from './renderers/Indicator/scale/indicator_scale'
7
+ import { findIndicator } from './renderers/Indicator/indicatorCatalog'
7
8
 
8
9
  export interface SubPaneEntry {
9
10
  paneId: string
@@ -220,7 +221,7 @@ export class SubPaneManager {
220
221
 
221
222
  const renderer = createPaneTitleRendererPlugin({
222
223
  paneId,
223
- title: indicatorId,
224
+ title: findIndicator(indicatorId)?.label ?? indicatorId,
224
225
  indicatorId,
225
226
  params,
226
227
  })
@@ -1,5 +1,5 @@
1
1
  import { afterEach, describe, expect, it, vi } from 'vitest'
2
- import { SemanticChartController, __setDataFetcher, type SemanticChartAdapter } from '../controller'
2
+ import { SemanticChartController, type SemanticChartAdapter } from '../controller'
3
3
  import type { SemanticChartConfig } from '../types'
4
4
 
5
5
  function createConfig(indicators: SemanticChartConfig['indicators']): SemanticChartConfig {
@@ -20,6 +20,7 @@ function createConfig(indicators: SemanticChartConfig['indicators']): SemanticCh
20
20
 
21
21
  function createChartAdapter(): SemanticChartAdapter {
22
22
  return {
23
+ setSymbols: vi.fn(),
23
24
  updateData: vi.fn(),
24
25
  updateRendererConfig: vi.fn(),
25
26
  addIndicator: vi.fn((definitionId: string) => definitionId),
@@ -34,12 +35,7 @@ function createChartAdapter(): SemanticChartAdapter {
34
35
  }
35
36
 
36
37
  describe('SemanticChartController', () => {
37
- afterEach(() => {
38
- __setDataFetcher(null)
39
- })
40
-
41
38
  it('routes semantic sub indicators through registered definitions', async () => {
42
- __setDataFetcher(vi.fn(async () => []))
43
39
  const chart = createChartAdapter()
44
40
  const controller = new SemanticChartController(chart)
45
41
 
@@ -61,7 +57,6 @@ describe('SemanticChartController', () => {
61
57
  })
62
58
 
63
59
  it('routes semantic main indicators through chart main indicator API', async () => {
64
- __setDataFetcher(vi.fn(async () => []))
65
60
  const chart = createChartAdapter()
66
61
  const controller = new SemanticChartController(chart)
67
62
 
@@ -81,4 +76,21 @@ describe('SemanticChartController', () => {
81
76
  expect(chart.disableMainIndicator).not.toHaveBeenCalled()
82
77
  expect(chart.updateRendererConfig).not.toHaveBeenCalledWith('boll', expect.anything())
83
78
  })
79
+
80
+ it('calls setSymbols on the adapter when applying config', async () => {
81
+ const chart = createChartAdapter()
82
+ const controller = new SemanticChartController(chart)
83
+
84
+ await controller.applyConfig(createConfig({}))
85
+
86
+ expect(chart.setSymbols).toHaveBeenCalledWith([{
87
+ symbol: '600000',
88
+ exchange: 'SH',
89
+ period: 'daily',
90
+ adjust: 'qfq',
91
+ source: 'baostock',
92
+ startDate: '2025-01-01',
93
+ endDate: '2025-01-02',
94
+ }])
95
+ })
84
96
  })