@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.
- package/dist/controllers/createChartController.d.ts.map +1 -1
- package/dist/controllers/createChartController.js +30 -4
- package/dist/controllers/createChartController.js.map +1 -1
- package/dist/controllers/types.d.ts +9 -2
- 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 +6 -1
- package/dist/data-fetchers/dataBuffer.d.ts.map +1 -1
- package/dist/data-fetchers/dataBuffer.js +88 -47
- package/dist/data-fetchers/dataBuffer.js.map +1 -1
- package/dist/data-fetchers/index.d.ts +1 -0
- package/dist/data-fetchers/index.d.ts.map +1 -1
- package/dist/data-fetchers/index.js +1 -0
- package/dist/data-fetchers/index.js.map +1 -1
- package/dist/data-fetchers/router.d.ts.map +1 -1
- package/dist/data-fetchers/router.js +3 -0
- package/dist/data-fetchers/router.js.map +1 -1
- package/dist/data-fetchers/tradingview.d.ts +3 -0
- package/dist/data-fetchers/tradingview.d.ts.map +1 -0
- package/dist/data-fetchers/tradingview.js +45 -0
- package/dist/data-fetchers/tradingview.js.map +1 -0
- package/dist/engine/chart.d.ts +34 -351
- package/dist/engine/chart.d.ts.map +1 -1
- package/dist/engine/chart.js +246 -1716
- 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/controller/interaction.d.ts +1 -0
- package/dist/engine/controller/interaction.d.ts.map +1 -1
- package/dist/engine/controller/interaction.js +9 -2
- package/dist/engine/controller/interaction.js.map +1 -1
- 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/engine/viewport/viewport.js +1 -1
- package/dist/engine/viewport/viewport.js.map +1 -1
- 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 +49 -13
- package/src/controllers/types.ts +9 -2
- package/src/data-fetchers/__tests__/dataBuffer.test.ts +77 -0
- package/src/data-fetchers/baostock.ts +3 -3
- package/src/data-fetchers/dataBuffer.ts +70 -22
- package/src/data-fetchers/index.ts +1 -0
- package/src/data-fetchers/router.ts +3 -0
- package/src/data-fetchers/tradingview.ts +48 -0
- package/src/engine/__tests__/subPaneManager.test.ts +154 -0
- package/src/engine/chart.ts +260 -2103
- package/src/engine/chartContext.ts +34 -0
- package/src/engine/chartTypes.ts +88 -0
- package/src/engine/controller/__tests__/interaction.dpr.test.ts +1 -0
- package/src/engine/controller/interaction.ts +10 -2
- 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/engine/viewport/viewport.ts +1 -1
- 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 -619
|
@@ -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`.
|
|
@@ -115,6 +114,7 @@ const DEFAULT_INDICATOR_CATALOG: ReadonlyArray<IndicatorDefinition> = [
|
|
|
115
114
|
|
|
116
115
|
interface MountedDom {
|
|
117
116
|
container: HTMLDivElement
|
|
117
|
+
scrollContent?: HTMLDivElement
|
|
118
118
|
canvasLayer: HTMLDivElement
|
|
119
119
|
rightAxisLayer: HTMLDivElement
|
|
120
120
|
xAxisCanvas: HTMLCanvasElement
|
|
@@ -129,7 +129,6 @@ function mapViewportState(vp: LegacyViewportState): ChartViewport {
|
|
|
129
129
|
dpr: vp.dpr,
|
|
130
130
|
visibleFrom: vp.visibleFrom,
|
|
131
131
|
visibleTo: vp.visibleTo,
|
|
132
|
-
desiredScrollLeft: vp.desiredScrollLeft,
|
|
133
132
|
kWidth: vp.kWidth,
|
|
134
133
|
kGap: vp.kGap,
|
|
135
134
|
}
|
|
@@ -247,6 +246,7 @@ function buildDom(container: HTMLElement): MountedDom {
|
|
|
247
246
|
canvasLayer.style.position = 'sticky'
|
|
248
247
|
canvasLayer.style.top = '0'
|
|
249
248
|
canvasLayer.style.left = '0'
|
|
249
|
+
canvasLayer.style.zIndex = '1'
|
|
250
250
|
|
|
251
251
|
const xAxisCanvas = ownerDoc.createElement('canvas')
|
|
252
252
|
xAxisCanvas.className = 'klc-x-axis-canvas'
|
|
@@ -274,7 +274,7 @@ function buildDom(container: HTMLElement): MountedDom {
|
|
|
274
274
|
}
|
|
275
275
|
}
|
|
276
276
|
|
|
277
|
-
return { container: chartContainer, canvasLayer, rightAxisLayer, xAxisCanvas, cleanup }
|
|
277
|
+
return { container: chartContainer, scrollContent, canvasLayer, rightAxisLayer, xAxisCanvas, cleanup }
|
|
278
278
|
}
|
|
279
279
|
|
|
280
280
|
// ---------------------------------------------------------------------------
|
|
@@ -319,6 +319,7 @@ export function createChartController(opts: ChartMountOptions): ChartController
|
|
|
319
319
|
const chart = new Chart(
|
|
320
320
|
{
|
|
321
321
|
container: mounted.container,
|
|
322
|
+
scrollContent: mounted.scrollContent,
|
|
322
323
|
canvasLayer: mounted.canvasLayer,
|
|
323
324
|
rightAxisLayer: mounted.rightAxisLayer,
|
|
324
325
|
xAxisCanvas: mounted.xAxisCanvas,
|
|
@@ -348,7 +349,6 @@ export function createChartController(opts: ChartMountOptions): ChartController
|
|
|
348
349
|
dpr: currentDpr,
|
|
349
350
|
visibleFrom: 0,
|
|
350
351
|
visibleTo: 0,
|
|
351
|
-
desiredScrollLeft: undefined,
|
|
352
352
|
kWidth: currentKWidth,
|
|
353
353
|
kGap: currentKGap,
|
|
354
354
|
})
|
|
@@ -370,6 +370,8 @@ export function createChartController(opts: ChartMountOptions): ChartController
|
|
|
370
370
|
Readonly<Record<string, number>>
|
|
371
371
|
>({})
|
|
372
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)
|
|
373
375
|
|
|
374
376
|
// -------------------------------------------------------------------
|
|
375
377
|
// Apply initial render state + seed data
|
|
@@ -476,6 +478,20 @@ export function createChartController(opts: ChartMountOptions): ChartController
|
|
|
476
478
|
),
|
|
477
479
|
)
|
|
478
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
|
+
|
|
479
495
|
// -------------------------------------------------------------------
|
|
480
496
|
// Lifecycle guard
|
|
481
497
|
// -------------------------------------------------------------------
|
|
@@ -500,6 +516,16 @@ export function createChartController(opts: ChartMountOptions): ChartController
|
|
|
500
516
|
chart.setSymbols(next)
|
|
501
517
|
}
|
|
502
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
|
+
|
|
503
529
|
function setDataFetcher(fetcher: DataFetcher | null): void {
|
|
504
530
|
if (disposed) return
|
|
505
531
|
chart.setDataFetcher(fetcher)
|
|
@@ -607,6 +633,11 @@ export function createChartController(opts: ChartMountOptions): ChartController
|
|
|
607
633
|
return chart.getContentWidth()
|
|
608
634
|
}
|
|
609
635
|
|
|
636
|
+
function scrollToRight(): void {
|
|
637
|
+
if (disposed) return
|
|
638
|
+
chart.scrollToRight()
|
|
639
|
+
}
|
|
640
|
+
|
|
610
641
|
function getIndicatorTitle(instanceId: string): string | undefined {
|
|
611
642
|
if (disposed) return undefined
|
|
612
643
|
const instances = chart.indicators.peek()
|
|
@@ -777,8 +808,12 @@ export function createChartController(opts: ChartMountOptions): ChartController
|
|
|
777
808
|
paneRatios,
|
|
778
809
|
paneLayout,
|
|
779
810
|
interactionState,
|
|
811
|
+
comparisonColors,
|
|
812
|
+
comparisonLoading,
|
|
780
813
|
catalog: DEFAULT_INDICATOR_CATALOG,
|
|
781
814
|
setSymbols,
|
|
815
|
+
addComparisonSymbol,
|
|
816
|
+
removeComparisonSymbol,
|
|
782
817
|
setDataFetcher,
|
|
783
818
|
setData,
|
|
784
819
|
appendData,
|
|
@@ -801,6 +836,7 @@ export function createChartController(opts: ChartMountOptions): ChartController
|
|
|
801
836
|
setTooltipAnchorPositioning,
|
|
802
837
|
getIndicatorTitle,
|
|
803
838
|
getContentWidth,
|
|
839
|
+
scrollToRight,
|
|
804
840
|
setDrawingTool,
|
|
805
841
|
clearDrawings,
|
|
806
842
|
removeDrawing,
|
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.
|
|
@@ -21,7 +21,6 @@ export interface ChartViewport {
|
|
|
21
21
|
dpr: number
|
|
22
22
|
visibleFrom: number
|
|
23
23
|
visibleTo: number
|
|
24
|
-
desiredScrollLeft: number | undefined
|
|
25
24
|
kWidth: number
|
|
26
25
|
kGap: number
|
|
27
26
|
}
|
|
@@ -72,6 +71,7 @@ export interface KLineData {
|
|
|
72
71
|
changePercent?: number
|
|
73
72
|
changeAmount?: number
|
|
74
73
|
turnoverRate?: number
|
|
74
|
+
date?: string
|
|
75
75
|
}
|
|
76
76
|
|
|
77
77
|
export type { PaneSpec }
|
|
@@ -98,6 +98,7 @@ export type DataFetcher = (
|
|
|
98
98
|
endDate: string
|
|
99
99
|
period: string
|
|
100
100
|
adjust: string
|
|
101
|
+
exchange?: string
|
|
101
102
|
},
|
|
102
103
|
) => Promise<ReadonlyArray<KLineData>>
|
|
103
104
|
|
|
@@ -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
|
|
@@ -303,6 +308,8 @@ export interface ChartController extends DrawingChartAdapter {
|
|
|
303
308
|
getIndicatorTitle(instanceId: string): string | undefined
|
|
304
309
|
/** total scrollable content width (replaces direct computeContentWidth imports) */
|
|
305
310
|
getContentWidth(): number
|
|
311
|
+
/** scroll to the rightmost position (latest data) */
|
|
312
|
+
scrollToRight(): void
|
|
306
313
|
|
|
307
314
|
// ---- Settings ----
|
|
308
315
|
updateSettingsFacade(settings: Record<string, unknown>): void
|
|
@@ -284,4 +284,81 @@ describe('DataBuffer', () => {
|
|
|
284
284
|
|
|
285
285
|
expect(prependCalls).toHaveLength(0)
|
|
286
286
|
})
|
|
287
|
+
|
|
288
|
+
it('ensureRange skips when same boundary was already attempted (empty fetch)', async () => {
|
|
289
|
+
const now = Date.now()
|
|
290
|
+
const oneYearAgo = now - 365 * MS_PER_DAY
|
|
291
|
+
const initialData = [makeKLine(oneYearAgo), makeKLine(now)]
|
|
292
|
+
|
|
293
|
+
let fetchCount = 0
|
|
294
|
+
const fetcher: DataFetcher = async () => {
|
|
295
|
+
fetchCount++
|
|
296
|
+
if (fetchCount === 1) return initialData
|
|
297
|
+
return []
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
buffer.setFetcher(fetcher)
|
|
301
|
+
buffer.setSymbol(defaultSpec)
|
|
302
|
+
|
|
303
|
+
await vi.waitFor(() => {
|
|
304
|
+
expect(buffer.loading()).toBe(false)
|
|
305
|
+
})
|
|
306
|
+
|
|
307
|
+
expect(fetchCount).toBe(1)
|
|
308
|
+
|
|
309
|
+
buffer.ensureRange(oneYearAgo - 30 * MS_PER_DAY, oneYearAgo)
|
|
310
|
+
|
|
311
|
+
await vi.waitFor(() => {
|
|
312
|
+
expect(buffer.loading()).toBe(false)
|
|
313
|
+
})
|
|
314
|
+
|
|
315
|
+
expect(fetchCount).toBe(2)
|
|
316
|
+
|
|
317
|
+
buffer.ensureRange(oneYearAgo - 60 * MS_PER_DAY, oneYearAgo)
|
|
318
|
+
|
|
319
|
+
await new Promise((r) => setTimeout(r, 50))
|
|
320
|
+
|
|
321
|
+
expect(fetchCount).toBe(2)
|
|
322
|
+
})
|
|
323
|
+
|
|
324
|
+
it('ensureRange allows retry when earliestTs moves after successful load', async () => {
|
|
325
|
+
const now = Date.now()
|
|
326
|
+
const oneYearAgo = now - 365 * MS_PER_DAY
|
|
327
|
+
const initialData = [makeKLine(oneYearAgo), makeKLine(now)]
|
|
328
|
+
|
|
329
|
+
let fetchCount = 0
|
|
330
|
+
const fetcher: DataFetcher = async () => {
|
|
331
|
+
fetchCount++
|
|
332
|
+
if (fetchCount === 1) return initialData
|
|
333
|
+
if (fetchCount === 2) return [makeKLine(oneYearAgo - 90 * MS_PER_DAY)]
|
|
334
|
+
return []
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
buffer.setFetcher(fetcher)
|
|
338
|
+
buffer.setSymbol(defaultSpec)
|
|
339
|
+
|
|
340
|
+
await vi.waitFor(() => {
|
|
341
|
+
expect(buffer.loading()).toBe(false)
|
|
342
|
+
})
|
|
343
|
+
|
|
344
|
+
expect(fetchCount).toBe(1)
|
|
345
|
+
|
|
346
|
+
buffer.ensureRange(oneYearAgo - 30 * MS_PER_DAY, oneYearAgo)
|
|
347
|
+
|
|
348
|
+
await vi.waitFor(() => {
|
|
349
|
+
expect(buffer.loading()).toBe(false)
|
|
350
|
+
})
|
|
351
|
+
|
|
352
|
+
expect(fetchCount).toBe(2)
|
|
353
|
+
expect(buffer.loadedWindow!.earliestTs).toBe(oneYearAgo - 90 * MS_PER_DAY)
|
|
354
|
+
|
|
355
|
+
const newEarliest = oneYearAgo - 90 * MS_PER_DAY
|
|
356
|
+
buffer.ensureRange(newEarliest - 30 * MS_PER_DAY, newEarliest)
|
|
357
|
+
|
|
358
|
+
await vi.waitFor(() => {
|
|
359
|
+
expect(buffer.loading()).toBe(false)
|
|
360
|
+
})
|
|
361
|
+
|
|
362
|
+
expect(fetchCount).toBe(3)
|
|
363
|
+
})
|
|
287
364
|
})
|
|
@@ -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,10 +40,12 @@ 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
|
|
45
47
|
private _disposed = false
|
|
48
|
+
private _attemptedBoundaries: Set<number> = new Set()
|
|
46
49
|
|
|
47
50
|
onPrepend: ((count: number) => void) | null = null
|
|
48
51
|
|
|
@@ -59,6 +62,10 @@ export class DataBuffer {
|
|
|
59
62
|
return this._loadingSignal
|
|
60
63
|
}
|
|
61
64
|
|
|
65
|
+
get currentSpec(): SymbolSpec | null {
|
|
66
|
+
return this._currentSpec
|
|
67
|
+
}
|
|
68
|
+
|
|
62
69
|
get loadedWindow(): DataWindow | null {
|
|
63
70
|
return this._loadedWindow
|
|
64
71
|
}
|
|
@@ -67,16 +74,25 @@ export class DataBuffer {
|
|
|
67
74
|
this._fetcher = fetcher
|
|
68
75
|
}
|
|
69
76
|
|
|
70
|
-
|
|
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 {
|
|
71
82
|
this._currentSpec = spec
|
|
72
83
|
this._data = []
|
|
73
84
|
this._loadedWindow = null
|
|
85
|
+
this._attemptedBoundaries.clear()
|
|
74
86
|
this._dataSignal.set([])
|
|
75
|
-
|
|
87
|
+
if (initialStartTs !== undefined) {
|
|
88
|
+
this.loadInitialRange(initialStartTs, Date.now())
|
|
89
|
+
} else {
|
|
90
|
+
this.loadInitial()
|
|
91
|
+
}
|
|
76
92
|
}
|
|
77
93
|
|
|
78
94
|
ensureRange(requestStartTs: number, _requestEndTs: number): void {
|
|
79
|
-
if (this._disposed || !this._fetcher || !this._currentSpec) return
|
|
95
|
+
if (this._disposed || (!this._requestFetch && !this._fetcher) || !this._currentSpec) return
|
|
80
96
|
if (!this._loadedWindow) return
|
|
81
97
|
|
|
82
98
|
if (requestStartTs >= this._loadedWindow.earliestTs) return
|
|
@@ -86,11 +102,14 @@ export class DataBuffer {
|
|
|
86
102
|
|
|
87
103
|
if (incrementalEnd <= incrementalStart) return
|
|
88
104
|
|
|
105
|
+
if (this._attemptedBoundaries.has(incrementalEnd)) return
|
|
106
|
+
|
|
107
|
+
this._attemptedBoundaries.add(incrementalEnd)
|
|
89
108
|
this.fetchRange(incrementalStart, incrementalEnd)
|
|
90
109
|
}
|
|
91
110
|
|
|
92
111
|
private loadInitial(): void {
|
|
93
|
-
if (!this._fetcher || !this._currentSpec || this._disposed) return
|
|
112
|
+
if ((!this._requestFetch && !this._fetcher) || !this._currentSpec || this._disposed) return
|
|
94
113
|
|
|
95
114
|
const now = Date.now()
|
|
96
115
|
const startDate = now - INITIAL_LOAD_DAYS * MS_PER_DAY
|
|
@@ -99,13 +118,18 @@ export class DataBuffer {
|
|
|
99
118
|
this.fetchRange(startDate, endDate)
|
|
100
119
|
}
|
|
101
120
|
|
|
102
|
-
private
|
|
103
|
-
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
|
|
104
128
|
|
|
105
129
|
if (this._pendingFetch) {
|
|
106
130
|
this._pendingFetch = this._pendingFetch.then(() => {
|
|
107
131
|
if (this._disposed) return
|
|
108
|
-
this.fetchRange(startTs, endTs)
|
|
132
|
+
return this.fetchRange(startTs, endTs, retryCount)
|
|
109
133
|
})
|
|
110
134
|
return
|
|
111
135
|
}
|
|
@@ -115,14 +139,18 @@ export class DataBuffer {
|
|
|
115
139
|
|
|
116
140
|
this._loadingSignal.set(true)
|
|
117
141
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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) => {
|
|
126
154
|
if (this._disposed) return
|
|
127
155
|
|
|
128
156
|
const oldLength = this._data.length
|
|
@@ -155,16 +183,35 @@ export class DataBuffer {
|
|
|
155
183
|
}
|
|
156
184
|
}
|
|
157
185
|
})
|
|
158
|
-
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const attempt = (count: number): Promise<void> => {
|
|
189
|
+
return doFetch().catch((err) => {
|
|
159
190
|
if (this._disposed) return
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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
|
+
})
|
|
166
202
|
}
|
|
203
|
+
|
|
204
|
+
console.error(`[DataBuffer] fetch failed after ${FETCH_MAX_RETRIES + 1} attempts:`, err)
|
|
205
|
+
this._attemptedBoundaries.delete(endTs)
|
|
167
206
|
})
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
this._pendingFetch = attempt(retryCount).finally(() => {
|
|
210
|
+
this._pendingFetch = null
|
|
211
|
+
if (!this._disposed) {
|
|
212
|
+
this._loadingSignal.set(false)
|
|
213
|
+
}
|
|
214
|
+
})
|
|
168
215
|
}
|
|
169
216
|
|
|
170
217
|
dispose(): void {
|
|
@@ -172,5 +219,6 @@ export class DataBuffer {
|
|
|
172
219
|
this._pendingFetch = null
|
|
173
220
|
this._data = []
|
|
174
221
|
this._loadedWindow = null
|
|
222
|
+
this._attemptedBoundaries.clear()
|
|
175
223
|
}
|
|
176
224
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export { thousandMockDataFetcher } from './thousand-mock'
|
|
2
2
|
export { hundredMockDataFetcher } from './hundred-mock'
|
|
3
3
|
export { baostockDataFetcher } from './baostock'
|
|
4
|
+
export { tradingviewDataFetcher } from './tradingview'
|
|
4
5
|
export { routerDataFetcher } from './router'
|
|
5
6
|
export { DataBuffer } from './dataBuffer'
|
|
6
7
|
export type { DataWindow } from './dataBuffer'
|
|
@@ -2,11 +2,14 @@ import type { DataFetcher } from '../controllers/types'
|
|
|
2
2
|
import { baostockDataFetcher } from './baostock'
|
|
3
3
|
import { hundredMockDataFetcher } from './hundred-mock'
|
|
4
4
|
import { thousandMockDataFetcher } from './thousand-mock'
|
|
5
|
+
import { tradingviewDataFetcher } from './tradingview'
|
|
5
6
|
|
|
6
7
|
export const routerDataFetcher: DataFetcher = (source, config) => {
|
|
7
8
|
switch (source) {
|
|
8
9
|
case 'baostock':
|
|
9
10
|
return baostockDataFetcher(source, config)
|
|
11
|
+
case 'tradingview':
|
|
12
|
+
return tradingviewDataFetcher(source, config)
|
|
10
13
|
case 'mock-100':
|
|
11
14
|
return hundredMockDataFetcher(source, config)
|
|
12
15
|
case 'mock-10000':
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { DataFetcher, KLineData } from '../controllers/types'
|
|
2
|
+
|
|
3
|
+
const PERIOD_TO_TIMEFRAME: Record<string, string> = {
|
|
4
|
+
daily: '1d',
|
|
5
|
+
weekly: '1w',
|
|
6
|
+
monthly: '1M',
|
|
7
|
+
'5min': '5m',
|
|
8
|
+
'15min': '15m',
|
|
9
|
+
'30min': '30m',
|
|
10
|
+
'60min': '60m',
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const tradingviewDataFetcher: DataFetcher = async (source, config) => {
|
|
14
|
+
const baseUrl = source === 'tradingview' ? 'http://localhost:8000' : ''
|
|
15
|
+
const timeframe = PERIOD_TO_TIMEFRAME[config.period] ?? '1d'
|
|
16
|
+
const startDate = config.startDate.split('T')[0]
|
|
17
|
+
const endDate = config.endDate.split('T')[0]
|
|
18
|
+
|
|
19
|
+
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}`
|
|
21
|
+
try {
|
|
22
|
+
const res = await fetch(url)
|
|
23
|
+
if (!res.ok) {
|
|
24
|
+
throw new Error(`[tradingview] fetch failed: ${res.status} ${res.statusText}`)
|
|
25
|
+
}
|
|
26
|
+
const json = await res.json()
|
|
27
|
+
if (!json.success) {
|
|
28
|
+
throw new Error(`[tradingview] API error: ${json.error_msg}`)
|
|
29
|
+
}
|
|
30
|
+
if (json.warning) {
|
|
31
|
+
console.warn(`[tradingview] ${json.warning}`)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return (json.data ?? []).map((item: Record<string, unknown>) => ({
|
|
35
|
+
timestamp: item.ts_open as number,
|
|
36
|
+
date: item.date as string,
|
|
37
|
+
open: item.open as number,
|
|
38
|
+
high: item.high as number,
|
|
39
|
+
low: item.low as number,
|
|
40
|
+
close: item.close as number,
|
|
41
|
+
volume: (item.volume as number) ?? 0,
|
|
42
|
+
stockCode: config.symbol,
|
|
43
|
+
})) as KLineData[]
|
|
44
|
+
} catch (err) {
|
|
45
|
+
console.warn('[tradingview] network error:', err)
|
|
46
|
+
throw err
|
|
47
|
+
}
|
|
48
|
+
}
|