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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (130) hide show
  1. package/dist/controllers/createChartController.d.ts.map +1 -1
  2. package/dist/controllers/createChartController.js +30 -4
  3. package/dist/controllers/createChartController.js.map +1 -1
  4. package/dist/controllers/types.d.ts +9 -2
  5. package/dist/controllers/types.d.ts.map +1 -1
  6. package/dist/data-fetchers/baostock.js +3 -3
  7. package/dist/data-fetchers/baostock.js.map +1 -1
  8. package/dist/data-fetchers/dataBuffer.d.ts +6 -1
  9. package/dist/data-fetchers/dataBuffer.d.ts.map +1 -1
  10. package/dist/data-fetchers/dataBuffer.js +88 -47
  11. package/dist/data-fetchers/dataBuffer.js.map +1 -1
  12. package/dist/data-fetchers/index.d.ts +1 -0
  13. package/dist/data-fetchers/index.d.ts.map +1 -1
  14. package/dist/data-fetchers/index.js +1 -0
  15. package/dist/data-fetchers/index.js.map +1 -1
  16. package/dist/data-fetchers/router.d.ts.map +1 -1
  17. package/dist/data-fetchers/router.js +3 -0
  18. package/dist/data-fetchers/router.js.map +1 -1
  19. package/dist/data-fetchers/tradingview.d.ts +3 -0
  20. package/dist/data-fetchers/tradingview.d.ts.map +1 -0
  21. package/dist/data-fetchers/tradingview.js +45 -0
  22. package/dist/data-fetchers/tradingview.js.map +1 -0
  23. package/dist/engine/chart.d.ts +34 -351
  24. package/dist/engine/chart.d.ts.map +1 -1
  25. package/dist/engine/chart.js +246 -1716
  26. package/dist/engine/chart.js.map +1 -1
  27. package/dist/engine/chartContext.d.ts +24 -0
  28. package/dist/engine/chartContext.d.ts.map +1 -0
  29. package/dist/engine/chartContext.js +19 -0
  30. package/dist/engine/chartContext.js.map +1 -0
  31. package/dist/engine/chartTypes.d.ts +77 -0
  32. package/dist/engine/chartTypes.d.ts.map +1 -0
  33. package/dist/engine/chartTypes.js +2 -0
  34. package/dist/engine/chartTypes.js.map +1 -0
  35. package/dist/engine/controller/interaction.d.ts +1 -0
  36. package/dist/engine/controller/interaction.d.ts.map +1 -1
  37. package/dist/engine/controller/interaction.js +9 -2
  38. package/dist/engine/controller/interaction.js.map +1 -1
  39. package/dist/engine/data/chartDataManager.d.ts +102 -0
  40. package/dist/engine/data/chartDataManager.d.ts.map +1 -0
  41. package/dist/engine/data/chartDataManager.js +590 -0
  42. package/dist/engine/data/chartDataManager.js.map +1 -0
  43. package/dist/engine/indicators/chartIndicatorManager.d.ts +102 -0
  44. package/dist/engine/indicators/chartIndicatorManager.d.ts.map +1 -0
  45. package/dist/engine/indicators/chartIndicatorManager.js +437 -0
  46. package/dist/engine/indicators/chartIndicatorManager.js.map +1 -0
  47. package/dist/engine/layout/chartPaneLayout.d.ts +53 -0
  48. package/dist/engine/layout/chartPaneLayout.d.ts.map +1 -0
  49. package/dist/engine/layout/chartPaneLayout.js +388 -0
  50. package/dist/engine/layout/chartPaneLayout.js.map +1 -0
  51. package/dist/engine/render/chartRenderer.d.ts +86 -0
  52. package/dist/engine/render/chartRenderer.d.ts.map +1 -0
  53. package/dist/engine/render/chartRenderer.js +438 -0
  54. package/dist/engine/render/chartRenderer.js.map +1 -0
  55. package/dist/engine/renderers/Indicator/mainIndicatorLegend.d.ts.map +1 -1
  56. package/dist/engine/renderers/Indicator/mainIndicatorLegend.js +73 -7
  57. package/dist/engine/renderers/Indicator/mainIndicatorLegend.js.map +1 -1
  58. package/dist/engine/renderers/comparisonLine.d.ts.map +1 -1
  59. package/dist/engine/renderers/comparisonLine.js +25 -11
  60. package/dist/engine/renderers/comparisonLine.js.map +1 -1
  61. package/dist/engine/subPaneManager.d.ts +27 -6
  62. package/dist/engine/subPaneManager.d.ts.map +1 -1
  63. package/dist/engine/subPaneManager.js +54 -56
  64. package/dist/engine/subPaneManager.js.map +1 -1
  65. package/dist/engine/utils/chartZoomController.d.ts +33 -0
  66. package/dist/engine/utils/chartZoomController.d.ts.map +1 -0
  67. package/dist/engine/utils/chartZoomController.js +66 -0
  68. package/dist/engine/utils/chartZoomController.js.map +1 -0
  69. package/dist/engine/viewport/chartViewportManager.d.ts +72 -0
  70. package/dist/engine/viewport/chartViewportManager.d.ts.map +1 -0
  71. package/dist/engine/viewport/chartViewportManager.js +249 -0
  72. package/dist/engine/viewport/chartViewportManager.js.map +1 -0
  73. package/dist/engine/viewport/viewport.js +1 -1
  74. package/dist/engine/viewport/viewport.js.map +1 -1
  75. package/dist/plugin/types.d.ts +1 -0
  76. package/dist/plugin/types.d.ts.map +1 -1
  77. package/dist/plugin/types.js.map +1 -1
  78. package/dist/tokens/theme-china.d.ts.map +1 -1
  79. package/dist/tokens/theme-china.js +0 -4
  80. package/dist/tokens/theme-china.js.map +1 -1
  81. package/dist/tokens/theme-dark.d.ts.map +1 -1
  82. package/dist/tokens/theme-dark.js +0 -4
  83. package/dist/tokens/theme-dark.js.map +1 -1
  84. package/dist/tokens/theme-light.d.ts.map +1 -1
  85. package/dist/tokens/theme-light.js +1 -5
  86. package/dist/tokens/theme-light.js.map +1 -1
  87. package/dist/tokens/types.d.ts +0 -4
  88. package/dist/tokens/types.d.ts.map +1 -1
  89. package/dist/types/price.d.ts +2 -0
  90. package/dist/types/price.d.ts.map +1 -1
  91. package/dist/types/price.js.map +1 -1
  92. package/dist/version.d.ts +1 -1
  93. package/dist/version.d.ts.map +1 -1
  94. package/dist/version.js +1 -1
  95. package/dist/version.js.map +1 -1
  96. package/package.json +1 -1
  97. package/src/controllers/createChartController.ts +49 -13
  98. package/src/controllers/types.ts +9 -2
  99. package/src/data-fetchers/__tests__/dataBuffer.test.ts +77 -0
  100. package/src/data-fetchers/baostock.ts +3 -3
  101. package/src/data-fetchers/dataBuffer.ts +70 -22
  102. package/src/data-fetchers/index.ts +1 -0
  103. package/src/data-fetchers/router.ts +3 -0
  104. package/src/data-fetchers/tradingview.ts +48 -0
  105. package/src/engine/__tests__/subPaneManager.test.ts +154 -0
  106. package/src/engine/chart.ts +260 -2103
  107. package/src/engine/chartContext.ts +34 -0
  108. package/src/engine/chartTypes.ts +88 -0
  109. package/src/engine/controller/__tests__/interaction.dpr.test.ts +1 -0
  110. package/src/engine/controller/interaction.ts +10 -2
  111. package/src/engine/data/chartDataManager.ts +691 -0
  112. package/src/engine/indicators/__tests__/chartIndicatorManager.test.ts +103 -0
  113. package/src/engine/indicators/chartIndicatorManager.ts +566 -0
  114. package/src/engine/layout/chartPaneLayout.ts +474 -0
  115. package/src/engine/render/chartRenderer.ts +579 -0
  116. package/src/engine/renderers/Indicator/mainIndicatorLegend.ts +99 -13
  117. package/src/engine/renderers/comparisonLine.ts +25 -11
  118. package/src/engine/subPaneManager.ts +75 -59
  119. package/src/engine/utils/chartZoomController.ts +104 -0
  120. package/src/engine/viewport/chartViewportManager.ts +310 -0
  121. package/src/engine/viewport/viewport.ts +1 -1
  122. package/src/plugin/types.ts +1 -0
  123. package/src/tokens/__tests__/__snapshots__/baseline.test.ts.snap +1 -9
  124. package/src/tokens/theme-china.ts +0 -4
  125. package/src/tokens/theme-dark.ts +0 -4
  126. package/src/tokens/theme-light.ts +2 -6
  127. package/src/tokens/types.ts +0 -4
  128. package/src/types/price.ts +2 -0
  129. package/src/version.ts +1 -1
  130. package/src/engine/chart.d.ts +0 -619
@@ -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
- Chart,
36
- type ChartOptions,
37
- type ViewportState as LegacyViewportState,
38
- type IndicatorInstance as LegacyIndicatorInstance,
39
- type SubPaneInfo as LegacySubPaneInfo,
40
- type DrawingObject as LegacyDrawingObject,
41
- type DrawingToolType as LegacyDrawingToolType,
42
- type InteractionSnapshot as LegacyInteractionSnapshot,
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,
@@ -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/chart'
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
- console.warn(`[baostock] fetch failed: ${res.status} ${res.statusText}`)
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
- return []
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
- 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 {
71
82
  this._currentSpec = spec
72
83
  this._data = []
73
84
  this._loadedWindow = null
85
+ this._attemptedBoundaries.clear()
74
86
  this._dataSignal.set([])
75
- this.loadInitial()
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 fetchRange(startTs: number, endTs: number): void {
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
- this._pendingFetch = fetcher(spec.source ?? 'baostock', {
119
- symbol: spec.symbol,
120
- startDate: formatDate(startTs),
121
- endDate: formatDate(endTs),
122
- period: spec.period ?? 'daily',
123
- adjust: spec.adjust ?? 'none',
124
- })
125
- .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) => {
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
- .catch((err) => {
186
+ }
187
+
188
+ const attempt = (count: number): Promise<void> => {
189
+ return doFetch().catch((err) => {
159
190
  if (this._disposed) return
160
- console.error('[DataBuffer] fetch failed:', err)
161
- })
162
- .finally(() => {
163
- this._pendingFetch = null
164
- if (!this._disposed) {
165
- this._loadingSignal.set(false)
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
+ }