@363045841yyt/klinechart-core 0.8.1-alpha.4 → 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.
- package/dist/controllers/createChartController.d.ts.map +1 -1
- package/dist/controllers/createChartController.js +21 -1
- package/dist/controllers/createChartController.js.map +1 -1
- package/dist/controllers/types.d.ts +6 -1
- package/dist/controllers/types.d.ts.map +1 -1
- package/dist/data-fetchers/baostock.js +3 -3
- package/dist/data-fetchers/baostock.js.map +1 -1
- package/dist/data-fetchers/dataBuffer.d.ts +5 -1
- package/dist/data-fetchers/dataBuffer.d.ts.map +1 -1
- package/dist/data-fetchers/dataBuffer.js +82 -48
- package/dist/data-fetchers/dataBuffer.js.map +1 -1
- package/dist/data-fetchers/tradingview.d.ts.map +1 -1
- package/dist/data-fetchers/tradingview.js +4 -5
- package/dist/data-fetchers/tradingview.js.map +1 -1
- package/dist/engine/chart.d.ts +29 -367
- package/dist/engine/chart.d.ts.map +1 -1
- package/dist/engine/chart.js +239 -1842
- package/dist/engine/chart.js.map +1 -1
- package/dist/engine/chartContext.d.ts +24 -0
- package/dist/engine/chartContext.d.ts.map +1 -0
- package/dist/engine/chartContext.js +19 -0
- package/dist/engine/chartContext.js.map +1 -0
- package/dist/engine/chartTypes.d.ts +77 -0
- package/dist/engine/chartTypes.d.ts.map +1 -0
- package/dist/engine/chartTypes.js +2 -0
- package/dist/engine/chartTypes.js.map +1 -0
- package/dist/engine/data/chartDataManager.d.ts +102 -0
- package/dist/engine/data/chartDataManager.d.ts.map +1 -0
- package/dist/engine/data/chartDataManager.js +590 -0
- package/dist/engine/data/chartDataManager.js.map +1 -0
- package/dist/engine/indicators/chartIndicatorManager.d.ts +102 -0
- package/dist/engine/indicators/chartIndicatorManager.d.ts.map +1 -0
- package/dist/engine/indicators/chartIndicatorManager.js +437 -0
- package/dist/engine/indicators/chartIndicatorManager.js.map +1 -0
- package/dist/engine/layout/chartPaneLayout.d.ts +53 -0
- package/dist/engine/layout/chartPaneLayout.d.ts.map +1 -0
- package/dist/engine/layout/chartPaneLayout.js +388 -0
- package/dist/engine/layout/chartPaneLayout.js.map +1 -0
- package/dist/engine/render/chartRenderer.d.ts +86 -0
- package/dist/engine/render/chartRenderer.d.ts.map +1 -0
- package/dist/engine/render/chartRenderer.js +438 -0
- package/dist/engine/render/chartRenderer.js.map +1 -0
- package/dist/engine/renderers/Indicator/mainIndicatorLegend.d.ts.map +1 -1
- package/dist/engine/renderers/Indicator/mainIndicatorLegend.js +73 -7
- package/dist/engine/renderers/Indicator/mainIndicatorLegend.js.map +1 -1
- package/dist/engine/renderers/comparisonLine.d.ts.map +1 -1
- package/dist/engine/renderers/comparisonLine.js +25 -11
- package/dist/engine/renderers/comparisonLine.js.map +1 -1
- package/dist/engine/subPaneManager.d.ts +27 -6
- package/dist/engine/subPaneManager.d.ts.map +1 -1
- package/dist/engine/subPaneManager.js +54 -56
- package/dist/engine/subPaneManager.js.map +1 -1
- package/dist/engine/utils/chartZoomController.d.ts +33 -0
- package/dist/engine/utils/chartZoomController.d.ts.map +1 -0
- package/dist/engine/utils/chartZoomController.js +66 -0
- package/dist/engine/utils/chartZoomController.js.map +1 -0
- package/dist/engine/viewport/chartViewportManager.d.ts +72 -0
- package/dist/engine/viewport/chartViewportManager.d.ts.map +1 -0
- package/dist/engine/viewport/chartViewportManager.js +249 -0
- package/dist/engine/viewport/chartViewportManager.js.map +1 -0
- package/dist/plugin/types.d.ts +1 -0
- package/dist/plugin/types.d.ts.map +1 -1
- package/dist/plugin/types.js.map +1 -1
- package/dist/tokens/theme-china.d.ts.map +1 -1
- package/dist/tokens/theme-china.js +0 -4
- package/dist/tokens/theme-china.js.map +1 -1
- package/dist/tokens/theme-dark.d.ts.map +1 -1
- package/dist/tokens/theme-dark.js +0 -4
- package/dist/tokens/theme-dark.js.map +1 -1
- package/dist/tokens/theme-light.d.ts.map +1 -1
- package/dist/tokens/theme-light.js +1 -5
- package/dist/tokens/theme-light.js.map +1 -1
- package/dist/tokens/types.d.ts +0 -4
- package/dist/tokens/types.d.ts.map +1 -1
- package/dist/types/price.d.ts +2 -0
- package/dist/types/price.d.ts.map +1 -1
- package/dist/types/price.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.d.ts.map +1 -1
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -1
- package/package.json +1 -1
- package/src/controllers/createChartController.ts +39 -10
- package/src/controllers/types.ts +6 -1
- package/src/data-fetchers/baostock.ts +3 -3
- package/src/data-fetchers/dataBuffer.ts +64 -23
- package/src/data-fetchers/tradingview.ts +4 -5
- package/src/engine/__tests__/subPaneManager.test.ts +154 -0
- package/src/engine/chart.ts +252 -2250
- package/src/engine/chartContext.ts +34 -0
- package/src/engine/chartTypes.ts +88 -0
- package/src/engine/data/chartDataManager.ts +691 -0
- package/src/engine/indicators/__tests__/chartIndicatorManager.test.ts +103 -0
- package/src/engine/indicators/chartIndicatorManager.ts +566 -0
- package/src/engine/layout/chartPaneLayout.ts +474 -0
- package/src/engine/render/chartRenderer.ts +579 -0
- package/src/engine/renderers/Indicator/mainIndicatorLegend.ts +99 -13
- package/src/engine/renderers/comparisonLine.ts +25 -11
- package/src/engine/subPaneManager.ts +75 -59
- package/src/engine/utils/chartZoomController.ts +104 -0
- package/src/engine/viewport/chartViewportManager.ts +310 -0
- package/src/plugin/types.ts +1 -0
- package/src/tokens/__tests__/__snapshots__/baseline.test.ts.snap +1 -9
- package/src/tokens/theme-china.ts +0 -4
- package/src/tokens/theme-dark.ts +0 -4
- package/src/tokens/theme-light.ts +2 -6
- package/src/tokens/types.ts +0 -4
- package/src/types/price.ts +2 -0
- package/src/version.ts +1 -1
- package/src/engine/chart.d.ts +0 -626
|
@@ -31,16 +31,15 @@ import type {
|
|
|
31
31
|
DataFetcher,
|
|
32
32
|
} from './types'
|
|
33
33
|
import type { CustomMarkerEntity } from '../engine/marker/registry'
|
|
34
|
-
import {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
} from '../engine/chart'
|
|
34
|
+
import { Chart, type InteractionSnapshot as LegacyInteractionSnapshot } from '../engine/chart'
|
|
35
|
+
import type {
|
|
36
|
+
ChartOptions,
|
|
37
|
+
ViewportState as LegacyViewportState,
|
|
38
|
+
IndicatorInstance as LegacyIndicatorInstance,
|
|
39
|
+
SubPaneInfo as LegacySubPaneInfo,
|
|
40
|
+
DrawingObject as LegacyDrawingObject,
|
|
41
|
+
DrawingToolType as LegacyDrawingToolType,
|
|
42
|
+
} from '../engine/chartTypes'
|
|
44
43
|
import { zoomLevelToKWidth, kGapFromKWidth } from '../engine/utils/zoom'
|
|
45
44
|
|
|
46
45
|
// Plugin-backed drawings expose `kind` instead of legacy `type`.
|
|
@@ -371,6 +370,8 @@ export function createChartController(opts: ChartMountOptions): ChartController
|
|
|
371
370
|
Readonly<Record<string, number>>
|
|
372
371
|
>({})
|
|
373
372
|
const interactionState: Signal<InteractionSnapshot> = createSignal(INITIAL_INTERACTION)
|
|
373
|
+
const comparisonColors: Signal<ReadonlyMap<string, string>> = createSignal<ReadonlyMap<string, string>>(new Map())
|
|
374
|
+
const comparisonLoading: Signal<boolean> = createSignal(false)
|
|
374
375
|
|
|
375
376
|
// -------------------------------------------------------------------
|
|
376
377
|
// Apply initial render state + seed data
|
|
@@ -477,6 +478,20 @@ export function createChartController(opts: ChartMountOptions): ChartController
|
|
|
477
478
|
),
|
|
478
479
|
)
|
|
479
480
|
|
|
481
|
+
// comparisonColors
|
|
482
|
+
unsubs.push(
|
|
483
|
+
chart.comparisonColors.subscribe(() =>
|
|
484
|
+
comparisonColors.set(new Map(chart.comparisonColors.peek())),
|
|
485
|
+
),
|
|
486
|
+
)
|
|
487
|
+
|
|
488
|
+
// comparisonLoading
|
|
489
|
+
unsubs.push(
|
|
490
|
+
chart.comparisonLoading.subscribe(() =>
|
|
491
|
+
comparisonLoading.set(chart.comparisonLoading.peek()),
|
|
492
|
+
),
|
|
493
|
+
)
|
|
494
|
+
|
|
480
495
|
// -------------------------------------------------------------------
|
|
481
496
|
// Lifecycle guard
|
|
482
497
|
// -------------------------------------------------------------------
|
|
@@ -501,6 +516,16 @@ export function createChartController(opts: ChartMountOptions): ChartController
|
|
|
501
516
|
chart.setSymbols(next)
|
|
502
517
|
}
|
|
503
518
|
|
|
519
|
+
function addComparisonSymbol(spec: SymbolSpec): void {
|
|
520
|
+
if (disposed) return
|
|
521
|
+
chart.addComparisonSymbol(spec)
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
function removeComparisonSymbol(symbol: string): void {
|
|
525
|
+
if (disposed) return
|
|
526
|
+
chart.removeComparisonSymbol(symbol)
|
|
527
|
+
}
|
|
528
|
+
|
|
504
529
|
function setDataFetcher(fetcher: DataFetcher | null): void {
|
|
505
530
|
if (disposed) return
|
|
506
531
|
chart.setDataFetcher(fetcher)
|
|
@@ -783,8 +808,12 @@ export function createChartController(opts: ChartMountOptions): ChartController
|
|
|
783
808
|
paneRatios,
|
|
784
809
|
paneLayout,
|
|
785
810
|
interactionState,
|
|
811
|
+
comparisonColors,
|
|
812
|
+
comparisonLoading,
|
|
786
813
|
catalog: DEFAULT_INDICATOR_CATALOG,
|
|
787
814
|
setSymbols,
|
|
815
|
+
addComparisonSymbol,
|
|
816
|
+
removeComparisonSymbol,
|
|
788
817
|
setDataFetcher,
|
|
789
818
|
setData,
|
|
790
819
|
appendData,
|
package/src/controllers/types.ts
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
import type { Signal } from '../reactivity'
|
|
12
12
|
import type { CustomMarkerEntity, MarkerEntity } from '../engine/marker/registry'
|
|
13
|
-
import type { PaneSpec } from '../engine/
|
|
13
|
+
import type { PaneSpec } from '../engine/chartTypes'
|
|
14
14
|
|
|
15
15
|
// Controller-owned public surface. Legacy engine types may mirror these
|
|
16
16
|
// shapes internally, but adapters depend only on core-defined contracts.
|
|
@@ -71,6 +71,7 @@ export interface KLineData {
|
|
|
71
71
|
changePercent?: number
|
|
72
72
|
changeAmount?: number
|
|
73
73
|
turnoverRate?: number
|
|
74
|
+
date?: string
|
|
74
75
|
}
|
|
75
76
|
|
|
76
77
|
export type { PaneSpec }
|
|
@@ -242,12 +243,16 @@ export interface ChartController extends DrawingChartAdapter {
|
|
|
242
243
|
readonly paneRatios: Signal<Readonly<Record<string, number>>>
|
|
243
244
|
readonly paneLayout: Signal<ReadonlyArray<PaneSpec>>
|
|
244
245
|
readonly interactionState: Signal<InteractionSnapshot>
|
|
246
|
+
readonly comparisonColors: Signal<ReadonlyMap<string, string>>
|
|
247
|
+
readonly comparisonLoading: Signal<boolean>
|
|
245
248
|
|
|
246
249
|
// indicator catalog (static — adapters use for picker UI)
|
|
247
250
|
readonly catalog: ReadonlyArray<IndicatorDefinition>
|
|
248
251
|
|
|
249
252
|
// ---- Data ----
|
|
250
253
|
setSymbols(next: ReadonlyArray<SymbolSpec>): void
|
|
254
|
+
addComparisonSymbol(spec: SymbolSpec): void
|
|
255
|
+
removeComparisonSymbol(symbol: string): void
|
|
251
256
|
setDataFetcher(fetcher: DataFetcher | null): void
|
|
252
257
|
setData(next: ReadonlyArray<KLineData>): void
|
|
253
258
|
appendData(next: ReadonlyArray<KLineData>): void
|
|
@@ -11,13 +11,13 @@ const periodMap: Record<string, string> = { daily: 'd', weekly: 'w', monthly: 'm
|
|
|
11
11
|
const res = await fetch(url)
|
|
12
12
|
console.log(res)
|
|
13
13
|
if (!res.ok) {
|
|
14
|
-
|
|
15
|
-
return []
|
|
14
|
+
throw new Error(`[baostock] fetch failed: ${res.status} ${res.statusText}`)
|
|
16
15
|
}
|
|
17
16
|
const json = await res.json()
|
|
18
17
|
console.log(json)
|
|
19
18
|
return (json.data ?? json).map((item: Record<string, unknown>) => ({
|
|
20
19
|
timestamp: new Date(item.date as string).getTime(),
|
|
20
|
+
date: item.date as string,
|
|
21
21
|
open: Number(item.open),
|
|
22
22
|
high: Number(item.high),
|
|
23
23
|
low: Number(item.low),
|
|
@@ -29,6 +29,6 @@ const periodMap: Record<string, string> = { daily: 'd', weekly: 'w', monthly: 'm
|
|
|
29
29
|
})) as KLineData[]
|
|
30
30
|
} catch (err) {
|
|
31
31
|
console.warn('[baostock] network error:', err)
|
|
32
|
-
|
|
32
|
+
throw err
|
|
33
33
|
}
|
|
34
34
|
}
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
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,15 +139,18 @@ export class DataBuffer {
|
|
|
120
139
|
|
|
121
140
|
this._loadingSignal.set(true)
|
|
122
141
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
|
|
134
156
|
const oldLength = this._data.length
|
|
@@ -161,16 +183,35 @@ export class DataBuffer {
|
|
|
161
183
|
}
|
|
162
184
|
}
|
|
163
185
|
})
|
|
164
|
-
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const attempt = (count: number): Promise<void> => {
|
|
189
|
+
return doFetch().catch((err) => {
|
|
165
190
|
if (this._disposed) return
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
191
|
+
|
|
192
|
+
if (count < FETCH_MAX_RETRIES) {
|
|
193
|
+
const delay = Math.pow(2, count) * 1000
|
|
194
|
+
console.warn(
|
|
195
|
+
`[DataBuffer] fetch failed, retry ${count + 1}/${FETCH_MAX_RETRIES} in ${delay}ms:`,
|
|
196
|
+
err,
|
|
197
|
+
)
|
|
198
|
+
return new Promise<void>((resolve) => setTimeout(resolve, delay)).then(() => {
|
|
199
|
+
if (this._disposed) return
|
|
200
|
+
return attempt(count + 1)
|
|
201
|
+
})
|
|
172
202
|
}
|
|
203
|
+
|
|
204
|
+
console.error(`[DataBuffer] fetch failed after ${FETCH_MAX_RETRIES + 1} attempts:`, err)
|
|
205
|
+
this._attemptedBoundaries.delete(endTs)
|
|
173
206
|
})
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
this._pendingFetch = attempt(retryCount).finally(() => {
|
|
210
|
+
this._pendingFetch = null
|
|
211
|
+
if (!this._disposed) {
|
|
212
|
+
this._loadingSignal.set(false)
|
|
213
|
+
}
|
|
214
|
+
})
|
|
174
215
|
}
|
|
175
216
|
|
|
176
217
|
dispose(): void {
|
|
@@ -21,13 +21,11 @@ export const tradingviewDataFetcher: DataFetcher = async (source, config) => {
|
|
|
21
21
|
try {
|
|
22
22
|
const res = await fetch(url)
|
|
23
23
|
if (!res.ok) {
|
|
24
|
-
|
|
25
|
-
return []
|
|
24
|
+
throw new Error(`[tradingview] fetch failed: ${res.status} ${res.statusText}`)
|
|
26
25
|
}
|
|
27
26
|
const json = await res.json()
|
|
28
27
|
if (!json.success) {
|
|
29
|
-
|
|
30
|
-
return []
|
|
28
|
+
throw new Error(`[tradingview] API error: ${json.error_msg}`)
|
|
31
29
|
}
|
|
32
30
|
if (json.warning) {
|
|
33
31
|
console.warn(`[tradingview] ${json.warning}`)
|
|
@@ -35,6 +33,7 @@ export const tradingviewDataFetcher: DataFetcher = async (source, config) => {
|
|
|
35
33
|
|
|
36
34
|
return (json.data ?? []).map((item: Record<string, unknown>) => ({
|
|
37
35
|
timestamp: item.ts_open as number,
|
|
36
|
+
date: item.date as string,
|
|
38
37
|
open: item.open as number,
|
|
39
38
|
high: item.high as number,
|
|
40
39
|
low: item.low as number,
|
|
@@ -44,6 +43,6 @@ export const tradingviewDataFetcher: DataFetcher = async (source, config) => {
|
|
|
44
43
|
})) as KLineData[]
|
|
45
44
|
} catch (err) {
|
|
46
45
|
console.warn('[tradingview] network error:', err)
|
|
47
|
-
|
|
46
|
+
throw err
|
|
48
47
|
}
|
|
49
48
|
}
|
|
@@ -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
|
+
})
|