@363045841yyt/klinechart-core 0.8.1-alpha.4 → 0.8.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 (167) hide show
  1. package/dist/controllers/createChartController.d.ts.map +1 -1
  2. package/dist/controllers/createChartController.js +21 -1
  3. package/dist/controllers/createChartController.js.map +1 -1
  4. package/dist/controllers/types.d.ts +6 -1
  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 +5 -1
  9. package/dist/data-fetchers/dataBuffer.d.ts.map +1 -1
  10. package/dist/data-fetchers/dataBuffer.js +85 -48
  11. package/dist/data-fetchers/dataBuffer.js.map +1 -1
  12. package/dist/data-fetchers/gotdx.d.ts +3 -0
  13. package/dist/data-fetchers/gotdx.d.ts.map +1 -0
  14. package/dist/data-fetchers/gotdx.js +101 -0
  15. package/dist/data-fetchers/gotdx.js.map +1 -0
  16. package/dist/data-fetchers/hundred-mock.d.ts.map +1 -1
  17. package/dist/data-fetchers/hundred-mock.js +28 -5
  18. package/dist/data-fetchers/hundred-mock.js.map +1 -1
  19. package/dist/data-fetchers/index.d.ts +1 -0
  20. package/dist/data-fetchers/index.d.ts.map +1 -1
  21. package/dist/data-fetchers/index.js +1 -0
  22. package/dist/data-fetchers/index.js.map +1 -1
  23. package/dist/data-fetchers/router.d.ts.map +1 -1
  24. package/dist/data-fetchers/router.js +3 -0
  25. package/dist/data-fetchers/router.js.map +1 -1
  26. package/dist/data-fetchers/thousand-mock.d.ts.map +1 -1
  27. package/dist/data-fetchers/thousand-mock.js +24 -14
  28. package/dist/data-fetchers/thousand-mock.js.map +1 -1
  29. package/dist/data-fetchers/tradingview.d.ts.map +1 -1
  30. package/dist/data-fetchers/tradingview.js +12 -6
  31. package/dist/data-fetchers/tradingview.js.map +1 -1
  32. package/dist/engine/chart.d.ts +29 -367
  33. package/dist/engine/chart.d.ts.map +1 -1
  34. package/dist/engine/chart.js +239 -1842
  35. package/dist/engine/chart.js.map +1 -1
  36. package/dist/engine/chartContext.d.ts +24 -0
  37. package/dist/engine/chartContext.d.ts.map +1 -0
  38. package/dist/engine/chartContext.js +19 -0
  39. package/dist/engine/chartContext.js.map +1 -0
  40. package/dist/engine/chartTypes.d.ts +77 -0
  41. package/dist/engine/chartTypes.d.ts.map +1 -0
  42. package/dist/engine/chartTypes.js +2 -0
  43. package/dist/engine/chartTypes.js.map +1 -0
  44. package/dist/engine/data/chartDataManager.d.ts +103 -0
  45. package/dist/engine/data/chartDataManager.d.ts.map +1 -0
  46. package/dist/engine/data/chartDataManager.js +593 -0
  47. package/dist/engine/data/chartDataManager.js.map +1 -0
  48. package/dist/engine/indicators/chartIndicatorManager.d.ts +102 -0
  49. package/dist/engine/indicators/chartIndicatorManager.d.ts.map +1 -0
  50. package/dist/engine/indicators/chartIndicatorManager.js +437 -0
  51. package/dist/engine/indicators/chartIndicatorManager.js.map +1 -0
  52. package/dist/engine/layout/chartPaneLayout.d.ts +53 -0
  53. package/dist/engine/layout/chartPaneLayout.d.ts.map +1 -0
  54. package/dist/engine/layout/chartPaneLayout.js +388 -0
  55. package/dist/engine/layout/chartPaneLayout.js.map +1 -0
  56. package/dist/engine/render/chartRenderer.d.ts +86 -0
  57. package/dist/engine/render/chartRenderer.d.ts.map +1 -0
  58. package/dist/engine/render/chartRenderer.js +440 -0
  59. package/dist/engine/render/chartRenderer.js.map +1 -0
  60. package/dist/engine/renderers/Indicator/mainIndicatorLegend.d.ts.map +1 -1
  61. package/dist/engine/renderers/Indicator/mainIndicatorLegend.js +73 -7
  62. package/dist/engine/renderers/Indicator/mainIndicatorLegend.js.map +1 -1
  63. package/dist/engine/renderers/comparisonLine.d.ts.map +1 -1
  64. package/dist/engine/renderers/comparisonLine.js +25 -11
  65. package/dist/engine/renderers/comparisonLine.js.map +1 -1
  66. package/dist/engine/renderers/timeAxis.d.ts.map +1 -1
  67. package/dist/engine/renderers/timeAxis.js +1 -0
  68. package/dist/engine/renderers/timeAxis.js.map +1 -1
  69. package/dist/engine/subPaneManager.d.ts +27 -6
  70. package/dist/engine/subPaneManager.d.ts.map +1 -1
  71. package/dist/engine/subPaneManager.js +54 -56
  72. package/dist/engine/subPaneManager.js.map +1 -1
  73. package/dist/engine/utils/chartZoomController.d.ts +33 -0
  74. package/dist/engine/utils/chartZoomController.d.ts.map +1 -0
  75. package/dist/engine/utils/chartZoomController.js +66 -0
  76. package/dist/engine/utils/chartZoomController.js.map +1 -0
  77. package/dist/engine/viewport/chartViewportManager.d.ts +72 -0
  78. package/dist/engine/viewport/chartViewportManager.d.ts.map +1 -0
  79. package/dist/engine/viewport/chartViewportManager.js +249 -0
  80. package/dist/engine/viewport/chartViewportManager.js.map +1 -0
  81. package/dist/index.d.ts +1 -0
  82. package/dist/index.d.ts.map +1 -1
  83. package/dist/index.js +1 -0
  84. package/dist/index.js.map +1 -1
  85. package/dist/plugin/types.d.ts +3 -0
  86. package/dist/plugin/types.d.ts.map +1 -1
  87. package/dist/plugin/types.js.map +1 -1
  88. package/dist/semantic/index.d.ts +1 -1
  89. package/dist/semantic/index.d.ts.map +1 -1
  90. package/dist/semantic/index.js.map +1 -1
  91. package/dist/semantic/schema.json +1 -1
  92. package/dist/semantic/types.d.ts +2 -1
  93. package/dist/semantic/types.d.ts.map +1 -1
  94. package/dist/tokens/theme-china.d.ts.map +1 -1
  95. package/dist/tokens/theme-china.js +0 -4
  96. package/dist/tokens/theme-china.js.map +1 -1
  97. package/dist/tokens/theme-dark.d.ts.map +1 -1
  98. package/dist/tokens/theme-dark.js +0 -4
  99. package/dist/tokens/theme-dark.js.map +1 -1
  100. package/dist/tokens/theme-light.d.ts.map +1 -1
  101. package/dist/tokens/theme-light.js +1 -5
  102. package/dist/tokens/theme-light.js.map +1 -1
  103. package/dist/tokens/types.d.ts +0 -4
  104. package/dist/tokens/types.d.ts.map +1 -1
  105. package/dist/types/price.d.ts +2 -0
  106. package/dist/types/price.d.ts.map +1 -1
  107. package/dist/types/price.js.map +1 -1
  108. package/dist/utils/dateFormat.d.ts +25 -0
  109. package/dist/utils/dateFormat.d.ts.map +1 -1
  110. package/dist/utils/dateFormat.js +78 -0
  111. package/dist/utils/dateFormat.js.map +1 -1
  112. package/dist/utils/kLineDraw/axis.d.ts +2 -0
  113. package/dist/utils/kLineDraw/axis.d.ts.map +1 -1
  114. package/dist/utils/kLineDraw/axis.js +11 -6
  115. package/dist/utils/kLineDraw/axis.js.map +1 -1
  116. package/dist/version.d.ts +1 -1
  117. package/dist/version.d.ts.map +1 -1
  118. package/dist/version.js +1 -1
  119. package/dist/version.js.map +1 -1
  120. package/package.json +1 -1
  121. package/src/controllers/createChartController.ts +39 -10
  122. package/src/controllers/types.ts +6 -1
  123. package/src/data-fetchers/__tests__/dataBuffer.test.ts +5 -2
  124. package/src/data-fetchers/baostock.ts +3 -3
  125. package/src/data-fetchers/dataBuffer.ts +70 -23
  126. package/src/data-fetchers/gotdx.ts +138 -0
  127. package/src/data-fetchers/hundred-mock.ts +35 -5
  128. package/src/data-fetchers/index.ts +1 -0
  129. package/src/data-fetchers/router.ts +3 -0
  130. package/src/data-fetchers/thousand-mock.ts +30 -14
  131. package/src/data-fetchers/tradingview.ts +12 -6
  132. package/src/engine/__tests__/subPaneManager.test.ts +154 -0
  133. package/src/engine/chart.ts +252 -2250
  134. package/src/engine/chartContext.ts +34 -0
  135. package/src/engine/chartTypes.ts +88 -0
  136. package/src/engine/data/chartDataManager.ts +695 -0
  137. package/src/engine/indicators/__tests__/chartIndicatorManager.test.ts +103 -0
  138. package/src/engine/indicators/chartIndicatorManager.ts +566 -0
  139. package/src/engine/layout/chartPaneLayout.ts +474 -0
  140. package/src/engine/render/chartRenderer.ts +581 -0
  141. package/src/engine/renderers/Indicator/mainIndicatorLegend.ts +99 -13
  142. package/src/engine/renderers/__tests__/boll.renderer.test.ts +1 -0
  143. package/src/engine/renderers/__tests__/ene.renderer.test.ts +1 -0
  144. package/src/engine/renderers/__tests__/expma.renderer.test.ts +1 -0
  145. package/src/engine/renderers/__tests__/ma.renderer.test.ts +1 -0
  146. package/src/engine/renderers/__tests__/mainIndicatorLegend.renderer.test.ts +1 -0
  147. package/src/engine/renderers/__tests__/yAxis.renderer.test.ts +1 -0
  148. package/src/engine/renderers/comparisonLine.ts +25 -11
  149. package/src/engine/renderers/timeAxis.ts +1 -0
  150. package/src/engine/subPaneManager.ts +75 -59
  151. package/src/engine/utils/chartZoomController.ts +104 -0
  152. package/src/engine/viewport/chartViewportManager.ts +310 -0
  153. package/src/index.ts +1 -0
  154. package/src/plugin/types.ts +3 -0
  155. package/src/semantic/index.ts +1 -0
  156. package/src/semantic/schema.json +1 -1
  157. package/src/semantic/types.ts +3 -1
  158. package/src/tokens/__tests__/__snapshots__/baseline.test.ts.snap +1 -9
  159. package/src/tokens/theme-china.ts +0 -4
  160. package/src/tokens/theme-dark.ts +0 -4
  161. package/src/tokens/theme-light.ts +2 -6
  162. package/src/tokens/types.ts +0 -4
  163. package/src/types/price.ts +2 -0
  164. package/src/utils/dateFormat.ts +85 -0
  165. package/src/utils/kLineDraw/axis.ts +13 -6
  166. package/src/version.ts +1 -1
  167. package/src/engine/chart.d.ts +0 -626
@@ -9,6 +9,7 @@ export interface DataWindow {
9
9
  const MS_PER_DAY = 86_400_000
10
10
  const INITIAL_LOAD_DAYS = 365
11
11
  const INCREMENTAL_LOAD_DAYS = 90
12
+ const FETCH_MAX_RETRIES = 2
12
13
 
13
14
  function formatDate(ts: number): string {
14
15
  const d = new Date(ts)
@@ -39,6 +40,7 @@ export class DataBuffer {
39
40
  private _dataSignal: Signal<ReadonlyArray<KLineData>>
40
41
  private _loadingSignal: Signal<boolean>
41
42
  private _fetcher: DataFetcher | null = null
43
+ private _requestFetch: ((spec: SymbolSpec, startTs: number, endTs: number) => Promise<ReadonlyArray<KLineData>>) | null = null
42
44
  private _currentSpec: SymbolSpec | null = null
43
45
  private _loadedWindow: DataWindow | null = null
44
46
  private _pendingFetch: Promise<void> | null = null
@@ -60,6 +62,10 @@ export class DataBuffer {
60
62
  return this._loadingSignal
61
63
  }
62
64
 
65
+ get currentSpec(): SymbolSpec | null {
66
+ return this._currentSpec
67
+ }
68
+
63
69
  get loadedWindow(): DataWindow | null {
64
70
  return this._loadedWindow
65
71
  }
@@ -68,17 +74,25 @@ export class DataBuffer {
68
74
  this._fetcher = fetcher
69
75
  }
70
76
 
71
- setSymbol(spec: SymbolSpec): void {
77
+ setRequestFetch(fn: ((spec: SymbolSpec, startTs: number, endTs: number) => Promise<ReadonlyArray<KLineData>>) | null): void {
78
+ this._requestFetch = fn
79
+ }
80
+
81
+ setSymbol(spec: SymbolSpec, initialStartTs?: number): void {
72
82
  this._currentSpec = spec
73
83
  this._data = []
74
84
  this._loadedWindow = null
75
85
  this._attemptedBoundaries.clear()
76
86
  this._dataSignal.set([])
77
- this.loadInitial()
87
+ if (initialStartTs !== undefined) {
88
+ this.loadInitialRange(initialStartTs, Date.now())
89
+ } else {
90
+ this.loadInitial()
91
+ }
78
92
  }
79
93
 
80
94
  ensureRange(requestStartTs: number, _requestEndTs: number): void {
81
- if (this._disposed || !this._fetcher || !this._currentSpec) return
95
+ if (this._disposed || (!this._requestFetch && !this._fetcher) || !this._currentSpec) return
82
96
  if (!this._loadedWindow) return
83
97
 
84
98
  if (requestStartTs >= this._loadedWindow.earliestTs) return
@@ -95,7 +109,7 @@ export class DataBuffer {
95
109
  }
96
110
 
97
111
  private loadInitial(): void {
98
- if (!this._fetcher || !this._currentSpec || this._disposed) return
112
+ if ((!this._requestFetch && !this._fetcher) || !this._currentSpec || this._disposed) return
99
113
 
100
114
  const now = Date.now()
101
115
  const startDate = now - INITIAL_LOAD_DAYS * MS_PER_DAY
@@ -104,13 +118,18 @@ export class DataBuffer {
104
118
  this.fetchRange(startDate, endDate)
105
119
  }
106
120
 
107
- private fetchRange(startTs: number, endTs: number): void {
108
- if (!this._fetcher || !this._currentSpec || this._disposed) return
121
+ private loadInitialRange(startTs: number, endTs: number): void {
122
+ if ((!this._requestFetch && !this._fetcher) || !this._currentSpec || this._disposed) return
123
+ this.fetchRange(startTs, endTs)
124
+ }
125
+
126
+ private fetchRange(startTs: number, endTs: number, retryCount = 0): void {
127
+ if ((!this._requestFetch && !this._fetcher) || !this._currentSpec || this._disposed) return
109
128
 
110
129
  if (this._pendingFetch) {
111
130
  this._pendingFetch = this._pendingFetch.then(() => {
112
131
  if (this._disposed) return
113
- this.fetchRange(startTs, endTs)
132
+ return this.fetchRange(startTs, endTs, retryCount)
114
133
  })
115
134
  return
116
135
  }
@@ -120,17 +139,26 @@ export class DataBuffer {
120
139
 
121
140
  this._loadingSignal.set(true)
122
141
 
123
- this._pendingFetch = fetcher(spec.source ?? 'baostock', {
124
- symbol: spec.symbol,
125
- startDate: formatDate(startTs),
126
- endDate: formatDate(endTs),
127
- period: spec.period ?? 'daily',
128
- adjust: spec.adjust ?? 'none',
129
- exchange: spec.exchange,
130
- })
131
- .then((incoming) => {
142
+ const doFetch = (): Promise<void> => {
143
+ const fetchPromise = this._requestFetch
144
+ ? this._requestFetch(spec, startTs, endTs)
145
+ : (fetcher as NonNullable<DataFetcher>)(spec.source ?? 'baostock', {
146
+ symbol: spec.symbol,
147
+ startDate: formatDate(startTs),
148
+ endDate: formatDate(endTs),
149
+ period: spec.period ?? 'daily',
150
+ adjust: spec.adjust ?? 'none',
151
+ exchange: spec.exchange,
152
+ })
153
+ return fetchPromise.then((incoming) => {
132
154
  if (this._disposed) return
133
155
 
156
+ if (incoming.length === 0) {
157
+ throw new Error(
158
+ `[DataBuffer] empty data for ${spec.symbol} ${formatDate(startTs)}~${formatDate(endTs)}`,
159
+ )
160
+ }
161
+
134
162
  const oldLength = this._data.length
135
163
  const oldEarliestTs = oldLength > 0 ? this._data[0]!.timestamp : null
136
164
  const merged = mergeSortedData(this._data, [...incoming])
@@ -161,16 +189,35 @@ export class DataBuffer {
161
189
  }
162
190
  }
163
191
  })
164
- .catch((err) => {
192
+ }
193
+
194
+ const attempt = (count: number): Promise<void> => {
195
+ return doFetch().catch((err) => {
165
196
  if (this._disposed) return
166
- console.error('[DataBuffer] fetch failed:', err)
167
- })
168
- .finally(() => {
169
- this._pendingFetch = null
170
- if (!this._disposed) {
171
- this._loadingSignal.set(false)
197
+
198
+ if (count < FETCH_MAX_RETRIES) {
199
+ const delay = Math.pow(2, count) * 1000
200
+ console.warn(
201
+ `[DataBuffer] fetch failed, retry ${count + 1}/${FETCH_MAX_RETRIES} in ${delay}ms:`,
202
+ err,
203
+ )
204
+ return new Promise<void>((resolve) => setTimeout(resolve, delay)).then(() => {
205
+ if (this._disposed) return
206
+ return attempt(count + 1)
207
+ })
172
208
  }
209
+
210
+ console.error(`[DataBuffer] fetch failed after ${FETCH_MAX_RETRIES + 1} attempts:`, err)
211
+ this._attemptedBoundaries.delete(endTs)
173
212
  })
213
+ }
214
+
215
+ this._pendingFetch = attempt(retryCount).finally(() => {
216
+ this._pendingFetch = null
217
+ if (!this._disposed) {
218
+ this._loadingSignal.set(false)
219
+ }
220
+ })
174
221
  }
175
222
 
176
223
  dispose(): void {
@@ -0,0 +1,138 @@
1
+ import type { DataFetcher, KLineData } from '../controllers/types'
2
+
3
+ const PERIOD_TO_CATEGORY: Record<string, number> = {
4
+ '1min': 8,
5
+ '5min': 0,
6
+ '15min': 1,
7
+ '30min': 2,
8
+ '60min': 3,
9
+ daily: 4,
10
+ weekly: 5,
11
+ monthly: 6,
12
+ quarterly: 10,
13
+ yearly: 11,
14
+ }
15
+
16
+ const ADJUST_MAP: Record<string, number> = {
17
+ none: 0,
18
+ qfq: 1,
19
+ hfq: 2,
20
+ splits: 0,
21
+ }
22
+
23
+ const EXCHANGE_EX_CATEGORY: Record<string, number> = {
24
+ US: 74,
25
+ HK: 71,
26
+ SG: 78,
27
+ DE: 73,
28
+ }
29
+
30
+ interface SecurityBar {
31
+ Last: number
32
+ Open: number
33
+ Close: number
34
+ High: number
35
+ Low: number
36
+ Vol: number
37
+ Amount: number
38
+ Turnover: number
39
+ RisePrice: number
40
+ RiseRate: number
41
+ Year: number
42
+ Month: number
43
+ Day: number
44
+ Hour: number
45
+ Minute: number
46
+ DateTime: string
47
+ UpCount: number
48
+ DownCount: number
49
+ }
50
+
51
+ interface ExKLineItem {
52
+ DateTime: string
53
+ Open: number
54
+ High: number
55
+ Low: number
56
+ Close: number
57
+ Amount: number
58
+ Vol: number
59
+ }
60
+
61
+ function mapBar(item: SecurityBar, code: string): KLineData {
62
+ const ts = new Date(item.DateTime).getTime()
63
+ return {
64
+ timestamp: ts,
65
+ date: item.DateTime.split('T')[0],
66
+ open: item.Open,
67
+ high: item.High,
68
+ low: item.Low,
69
+ close: item.Close,
70
+ volume: item.Vol,
71
+ turnover: item.Amount,
72
+ turnoverRate: item.Turnover,
73
+ changeAmount: item.RisePrice,
74
+ changePercent: item.RiseRate,
75
+ stockCode: code,
76
+ }
77
+ }
78
+
79
+ function mapExItem(item: ExKLineItem, code: string): KLineData {
80
+ const ts = new Date(item.DateTime).getTime()
81
+ return {
82
+ timestamp: ts,
83
+ date: item.DateTime.split('T')[0],
84
+ open: item.Open,
85
+ high: item.High,
86
+ low: item.Low,
87
+ close: item.Close,
88
+ volume: item.Vol,
89
+ turnover: item.Amount,
90
+ stockCode: code,
91
+ }
92
+ }
93
+
94
+ export const gotdxDataFetcher: DataFetcher = async (source, config) => {
95
+ const baseUrl = source === 'gotdx' ? 'http://127.0.0.1:8080' : ''
96
+
97
+ if (config.exchange && config.exchange in EXCHANGE_EX_CATEGORY) {
98
+ const category = EXCHANGE_EX_CATEGORY[config.exchange]
99
+ const period = PERIOD_TO_CATEGORY[config.period] ?? 4
100
+ const body = {
101
+ category,
102
+ code: config.symbol,
103
+ period,
104
+ start_date: config.startDate,
105
+ end_date: config.endDate,
106
+ times: 1,
107
+ }
108
+ const res = await fetch(`${baseUrl}/api/ex/kline-by-date`, {
109
+ method: 'POST',
110
+ headers: { 'Content-Type': 'application/json' },
111
+ body: JSON.stringify(body),
112
+ })
113
+ if (!res.ok) throw new Error(`[gotdx] ex/kline-by-date failed: ${res.status} ${res.statusText}`)
114
+ const list: ExKLineItem[] = await res.json()
115
+ return list.map((item) => mapExItem(item, config.symbol))
116
+ }
117
+
118
+ const market = config.symbol.startsWith('6') || config.symbol.startsWith('9') ? 1 : 0
119
+ const category = PERIOD_TO_CATEGORY[config.period] ?? 4
120
+ const adjust = ADJUST_MAP[config.adjust] ?? 0
121
+ const body = {
122
+ market,
123
+ code: config.symbol,
124
+ category,
125
+ start_date: config.startDate,
126
+ end_date: config.endDate,
127
+ times: 1,
128
+ adjust,
129
+ }
130
+ const res = await fetch(`${baseUrl}/api/stock/kline-by-date`, {
131
+ method: 'POST',
132
+ headers: { 'Content-Type': 'application/json' },
133
+ body: JSON.stringify(body),
134
+ })
135
+ if (!res.ok) throw new Error(`[gotdx] stock/kline-by-date failed: ${res.status} ${res.statusText}`)
136
+ const list: SecurityBar[] = await res.json()
137
+ return list.map((item) => mapBar(item, config.symbol))
138
+ }
@@ -6,13 +6,43 @@ export const hundredMockDataFetcher: DataFetcher = async (_source, config) => {
6
6
  const end = new Date(config.endDate).getTime()
7
7
  const dayMs = 86400000
8
8
  const totalDays = Math.floor((end - start) / dayMs) + 1
9
+ if (totalDays <= 0) return []
10
+
11
+ const basePrice = 12.5
9
12
  const data: KLineData[] = []
10
- let price = 10 + Math.random() * 5
13
+
14
+ if (totalDays === 1) {
15
+ data.push({
16
+ timestamp: start,
17
+ open: basePrice,
18
+ high: basePrice,
19
+ low: basePrice,
20
+ close: basePrice,
21
+ volume: Math.round(Math.random() * 10000000 + 1000000),
22
+ })
23
+ return data
24
+ }
25
+
26
+ const meanReversionStrength = 0.005
27
+
28
+ // raw random walk with mean reversion (close prices before bridge)
29
+ const rawWalk: number[] = [basePrice]
30
+ for (let i = 1; i < totalDays; i++) {
31
+ const prev = rawWalk[i - 1]!
32
+ const reversion = meanReversionStrength * (basePrice - prev)
33
+ const change = (Math.random() - 0.48) * prev * 0.06 + reversion
34
+ rawWalk.push(prev + change)
35
+ }
36
+
37
+ // Brownian bridge: subtract linear drift so last close = basePrice
38
+ const finalOffset = rawWalk[totalDays - 1]! - basePrice
11
39
  for (let i = 0; i < totalDays; i++) {
40
+ const bridge = finalOffset * (i / (totalDays - 1))
41
+ const close = Math.round((rawWalk[i]! - bridge) * 100) / 100
42
+
12
43
  const ts = start + i * dayMs
13
- const change = (Math.random() - 0.48) * price * 0.06
14
- const open = price
15
- const close = Math.round((open + change) * 100) / 100
44
+ const open = i === 0 ? basePrice : data[i - 1]!.close
45
+
16
46
  const high = Math.round(Math.max(open, close) * (1 + Math.random() * 0.03) * 100) / 100
17
47
  const low = Math.round(Math.min(open, close) * (1 - Math.random() * 0.03) * 100) / 100
18
48
  const volume = Math.round(Math.random() * 10000000 + 1000000)
@@ -25,7 +55,7 @@ export const hundredMockDataFetcher: DataFetcher = async (_source, config) => {
25
55
  volume,
26
56
  turnover: Math.round((volume * (open + close)) / 2),
27
57
  })
28
- price = close
29
58
  }
59
+
30
60
  return data
31
61
  }
@@ -2,6 +2,7 @@ export { thousandMockDataFetcher } from './thousand-mock'
2
2
  export { hundredMockDataFetcher } from './hundred-mock'
3
3
  export { baostockDataFetcher } from './baostock'
4
4
  export { tradingviewDataFetcher } from './tradingview'
5
+ export { gotdxDataFetcher } from './gotdx'
5
6
  export { routerDataFetcher } from './router'
6
7
  export { DataBuffer } from './dataBuffer'
7
8
  export type { DataWindow } from './dataBuffer'
@@ -1,5 +1,6 @@
1
1
  import type { DataFetcher } from '../controllers/types'
2
2
  import { baostockDataFetcher } from './baostock'
3
+ import { gotdxDataFetcher } from './gotdx'
3
4
  import { hundredMockDataFetcher } from './hundred-mock'
4
5
  import { thousandMockDataFetcher } from './thousand-mock'
5
6
  import { tradingviewDataFetcher } from './tradingview'
@@ -8,6 +9,8 @@ export const routerDataFetcher: DataFetcher = (source, config) => {
8
9
  switch (source) {
9
10
  case 'baostock':
10
11
  return baostockDataFetcher(source, config)
12
+ case 'gotdx':
13
+ return gotdxDataFetcher(source, config)
11
14
  case 'tradingview':
12
15
  return tradingviewDataFetcher(source, config)
13
16
  case 'mock-100':
@@ -5,26 +5,42 @@ export const thousandMockDataFetcher: DataFetcher = async (_source, _config) =>
5
5
  const data: KLineData[] = []
6
6
  const startTime = new Date('2020-01-01').getTime()
7
7
  const dayMs = 24 * 60 * 60 * 1000
8
- let lastClose = 3000
9
- for (let i = 0; i < 10000; i++) {
8
+ const totalDays = 10000
9
+
10
+ const basePrice = 3000
11
+ const meanReversionStrength = 0.0005
12
+ const volatility = 0.02
13
+
14
+ // raw random walk with mean reversion (close prices before bridge)
15
+ const rawWalk: number[] = [basePrice]
16
+ for (let i = 1; i < totalDays; i++) {
17
+ const prev = rawWalk[i - 1]!
18
+ const reversion = meanReversionStrength * (basePrice - prev)
19
+ const change = (Math.random() - 0.5) * 2 * volatility * prev + reversion
20
+ rawWalk.push(prev + change)
21
+ }
22
+
23
+ // Brownian bridge: subtract linear drift so last close = basePrice
24
+ const finalOffset = rawWalk[totalDays - 1]! - basePrice
25
+ for (let i = 0; i < totalDays; i++) {
26
+ const bridge = finalOffset * (i / (totalDays - 1))
27
+ const close = Math.round((rawWalk[i]! - bridge) * 100) / 100
28
+
10
29
  const timestamp = startTime + i * dayMs
11
- const volatility = 0.02
12
- const trend = 0.0001
13
- const change = (Math.random() - 0.5) * 2 * volatility + trend
14
- const open = lastClose
15
- const close = open * (1 + change)
16
- const high = Math.max(open, close) * (1 + Math.random() * 0.01)
17
- const low = Math.min(open, close) * (1 - Math.random() * 0.01)
30
+ const open = i === 0 ? basePrice : data[i - 1]!.close
31
+
32
+ const high = Math.round(Math.max(open, close) * (1 + Math.random() * 0.01) * 100) / 100
33
+ const low = Math.round(Math.min(open, close) * (1 - Math.random() * 0.01) * 100) / 100
18
34
  const volume = Math.floor(1000000 + Math.random() * 5000000)
19
35
  data.push({
20
36
  timestamp,
21
- open: parseFloat(open.toFixed(2)),
22
- high: parseFloat(high.toFixed(2)),
23
- low: parseFloat(low.toFixed(2)),
24
- close: parseFloat(close.toFixed(2)),
37
+ open,
38
+ high,
39
+ low,
40
+ close,
25
41
  volume,
26
42
  })
27
- lastClose = close
28
43
  }
44
+
29
45
  return data
30
46
  }
@@ -16,18 +16,23 @@ export const tradingviewDataFetcher: DataFetcher = async (source, config) => {
16
16
  const startDate = config.startDate.split('T')[0]
17
17
  const endDate = config.endDate.split('T')[0]
18
18
 
19
+ const ADJUST_TO_TV: Record<string, string | undefined> = {
20
+ qfq: 'dividends',
21
+ splits: 'splits',
22
+ none: 'none',
23
+ }
24
+ const tvAdjust = ADJUST_TO_TV[config.adjust]
19
25
  const exchangeQ = config.exchange ? `&exchange=${config.exchange}` : ''
20
- const url = `${baseUrl}/api/tradingview/kdata?symbol=${config.symbol}&timeframe=${timeframe}&start_date=${startDate}&end_date=${endDate}${exchangeQ}`
26
+ const adjustQ = tvAdjust ? `&adjust=${tvAdjust}` : ''
27
+ const url = `${baseUrl}/api/tradingview/kdata?symbol=${config.symbol}&timeframe=${timeframe}&start_date=${startDate}&end_date=${endDate}${exchangeQ}${adjustQ}`
21
28
  try {
22
29
  const res = await fetch(url)
23
30
  if (!res.ok) {
24
- console.warn(`[tradingview] fetch failed: ${res.status} ${res.statusText}`)
25
- return []
31
+ throw new Error(`[tradingview] fetch failed: ${res.status} ${res.statusText}`)
26
32
  }
27
33
  const json = await res.json()
28
34
  if (!json.success) {
29
- console.warn(`[tradingview] API error: ${json.error_msg}`)
30
- return []
35
+ throw new Error(`[tradingview] API error: ${json.error_msg}`)
31
36
  }
32
37
  if (json.warning) {
33
38
  console.warn(`[tradingview] ${json.warning}`)
@@ -35,6 +40,7 @@ export const tradingviewDataFetcher: DataFetcher = async (source, config) => {
35
40
 
36
41
  return (json.data ?? []).map((item: Record<string, unknown>) => ({
37
42
  timestamp: item.ts_open as number,
43
+ date: item.date as string,
38
44
  open: item.open as number,
39
45
  high: item.high as number,
40
46
  low: item.low as number,
@@ -44,6 +50,6 @@ export const tradingviewDataFetcher: DataFetcher = async (source, config) => {
44
50
  })) as KLineData[]
45
51
  } catch (err) {
46
52
  console.warn('[tradingview] network error:', err)
47
- return []
53
+ throw err
48
54
  }
49
55
  }
@@ -0,0 +1,154 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest'
2
+ import { SubPaneManager, type SubPaneContext } from '../subPaneManager'
3
+ import type { SubIndicatorType } from '../renderers/Indicator'
4
+ import type { IndicatorScheduler } from '../indicators/scheduler'
5
+
6
+ function createMockScheduler(): Partial<IndicatorScheduler> {
7
+ return {
8
+ getIndicatorMetadata: vi.fn((_id: string) => ({
9
+ rendererFactory: vi.fn(() => ({ name: 'custom_rsi_rsi_0' })),
10
+ updateConfig: vi.fn(),
11
+ scale: { indicatorKey: 'test', label: 'Test', decimals: 2 },
12
+ })),
13
+ onSubPaneChanged: vi.fn(),
14
+ }
15
+ }
16
+
17
+ function createMockContext(): SubPaneContext {
18
+ const scheduler = createMockScheduler()
19
+ return {
20
+ getIndicatorScheduler: vi.fn(() => scheduler as unknown as IndicatorScheduler),
21
+ hasPane: vi.fn(() => false),
22
+ upsertPane: vi.fn(),
23
+ getRenderer: vi.fn(),
24
+ useRenderer: vi.fn(),
25
+ removeRenderer: vi.fn(),
26
+ removePaneDefinition: vi.fn(),
27
+ updateRendererConfig: vi.fn(),
28
+ getRightAxisWidth: vi.fn(() => 60),
29
+ getPriceLabelWidth: vi.fn(() => 60),
30
+ getYPaddingPx: vi.fn(() => 4),
31
+ getCrosshairPos: vi.fn(() => null),
32
+ getCrosshairPrice: vi.fn(() => null),
33
+ getActivePaneId: vi.fn(() => null),
34
+ }
35
+ }
36
+
37
+ describe('SubPaneManager', () => {
38
+ let manager: SubPaneManager
39
+ let ctx: SubPaneContext
40
+
41
+ beforeEach(() => {
42
+ manager = new SubPaneManager()
43
+ ctx = createMockContext()
44
+ vi.clearAllMocks()
45
+ })
46
+
47
+ describe('updateParams', () => {
48
+ it('should update paneTitle renderer config with new params and indicatorId', () => {
49
+ manager.create(ctx, 'RSI_0', 'RSI' as SubIndicatorType, {
50
+ period1: 6,
51
+ period2: 12,
52
+ period3: 24,
53
+ })
54
+
55
+ const entry = manager.getByPaneId('RSI_0')
56
+ expect(entry).toBeDefined()
57
+ vi.clearAllMocks()
58
+
59
+ const newParams = { period1: 10, period2: 20, period3: 30 }
60
+ manager.updateParams(ctx, 'RSI_0', newParams)
61
+
62
+ expect(ctx.updateRendererConfig).toHaveBeenCalledWith(
63
+ entry!.paneTitleRendererName,
64
+ { params: newParams, indicatorId: 'RSI' },
65
+ )
66
+ })
67
+
68
+ it('should update main indicator renderer config with new params', () => {
69
+ manager.create(ctx, 'RSI_0', 'RSI' as SubIndicatorType, {
70
+ period1: 6,
71
+ period2: 12,
72
+ period3: 24,
73
+ })
74
+
75
+ const entry = manager.getByPaneId('RSI_0')
76
+ expect(entry).toBeDefined()
77
+ vi.clearAllMocks()
78
+
79
+ const newParams = { period1: 10, period2: 20, period3: 30 }
80
+ manager.updateParams(ctx, 'RSI_0', newParams)
81
+
82
+ expect(ctx.updateRendererConfig).toHaveBeenCalledWith(
83
+ entry!.rendererName,
84
+ newParams,
85
+ )
86
+ })
87
+
88
+ it('should update scheduler config via definition.updateConfig', () => {
89
+ const updateConfigSpy = vi.fn()
90
+ const customScheduler: Partial<IndicatorScheduler> = {
91
+ getIndicatorMetadata: vi.fn(() => ({
92
+ rendererFactory: vi.fn(() => ({ name: 'custom_rsi_rsi_0' })),
93
+ updateConfig: updateConfigSpy,
94
+ scale: { indicatorKey: 'test', label: 'Test', decimals: 2 },
95
+ })),
96
+ onSubPaneChanged: vi.fn(),
97
+ }
98
+ const customCtx: SubPaneContext = {
99
+ ...ctx,
100
+ getIndicatorScheduler: vi.fn(() => customScheduler as unknown as IndicatorScheduler),
101
+ }
102
+
103
+ manager.create(customCtx, 'RSI_0', 'RSI' as SubIndicatorType, {
104
+ period1: 6,
105
+ period2: 12,
106
+ period3: 24,
107
+ })
108
+
109
+ const newParams = { period1: 10, period2: 20, period3: 30 }
110
+ manager.updateParams(customCtx, 'RSI_0', newParams)
111
+
112
+ expect(updateConfigSpy).toHaveBeenCalled()
113
+ })
114
+
115
+ it('should update entry params in the manager', () => {
116
+ manager.create(ctx, 'RSI_0', 'RSI' as SubIndicatorType, {
117
+ period1: 6,
118
+ period2: 12,
119
+ period3: 24,
120
+ })
121
+
122
+ const newParams = { period1: 10, period2: 20, period3: 30 }
123
+ manager.updateParams(ctx, 'RSI_0', newParams)
124
+
125
+ const entry = manager.getByPaneId('RSI_0')
126
+ expect(entry?.params).toEqual(newParams)
127
+ })
128
+
129
+ it('should fire entries signal on update', () => {
130
+ manager.create(ctx, 'RSI_0', 'RSI' as SubIndicatorType, {
131
+ period1: 6,
132
+ period2: 12,
133
+ period3: 24,
134
+ })
135
+
136
+ const listener = vi.fn()
137
+ manager.entriesSignal.subscribe(listener)
138
+ vi.clearAllMocks()
139
+
140
+ const newParams = { period1: 10, period2: 20, period3: 30 }
141
+ manager.updateParams(ctx, 'RSI_0', newParams)
142
+
143
+ expect(listener).toHaveBeenCalledTimes(1)
144
+ })
145
+
146
+ it('should silently skip when paneId does not exist', () => {
147
+ const newParams = { period1: 10, period2: 20, period3: 30 }
148
+ manager.updateParams(ctx, 'NONEXISTENT', newParams)
149
+
150
+ expect(ctx.updateRendererConfig).not.toHaveBeenCalled()
151
+ expect(ctx.getIndicatorScheduler().onSubPaneChanged).not.toHaveBeenCalled()
152
+ })
153
+ })
154
+ })