@363045841yyt/klinechart-core 0.8.2 → 0.8.4

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 (115) hide show
  1. package/dist/controllers/createChartController.d.ts +1 -1
  2. package/dist/controllers/createChartController.d.ts.map +1 -1
  3. package/dist/controllers/createChartController.js +34 -1
  4. package/dist/controllers/createChartController.js.map +1 -1
  5. package/dist/controllers/index.d.ts +1 -0
  6. package/dist/controllers/index.d.ts.map +1 -1
  7. package/dist/controllers/index.js +1 -0
  8. package/dist/controllers/index.js.map +1 -1
  9. package/dist/controllers/types.d.ts +17 -1
  10. package/dist/controllers/types.d.ts.map +1 -1
  11. package/dist/data-fetchers/baostock.d.ts +9 -2
  12. package/dist/data-fetchers/baostock.d.ts.map +1 -1
  13. package/dist/data-fetchers/baostock.js +78 -9
  14. package/dist/data-fetchers/baostock.js.map +1 -1
  15. package/dist/data-fetchers/fetcherDefinitionRegistry.d.ts +13 -0
  16. package/dist/data-fetchers/fetcherDefinitionRegistry.d.ts.map +1 -0
  17. package/dist/data-fetchers/fetcherDefinitionRegistry.js +36 -0
  18. package/dist/data-fetchers/fetcherDefinitionRegistry.js.map +1 -0
  19. package/dist/data-fetchers/gotdx.d.ts +9 -2
  20. package/dist/data-fetchers/gotdx.d.ts.map +1 -1
  21. package/dist/data-fetchers/gotdx.js +72 -5
  22. package/dist/data-fetchers/gotdx.js.map +1 -1
  23. package/dist/data-fetchers/hundred-mock.d.ts +9 -2
  24. package/dist/data-fetchers/hundred-mock.d.ts.map +1 -1
  25. package/dist/data-fetchers/hundred-mock.js +66 -4
  26. package/dist/data-fetchers/hundred-mock.js.map +1 -1
  27. package/dist/data-fetchers/index.d.ts +7 -5
  28. package/dist/data-fetchers/index.d.ts.map +1 -1
  29. package/dist/data-fetchers/index.js +6 -5
  30. package/dist/data-fetchers/index.js.map +1 -1
  31. package/dist/data-fetchers/router.d.ts.map +1 -1
  32. package/dist/data-fetchers/router.js +14 -18
  33. package/dist/data-fetchers/router.js.map +1 -1
  34. package/dist/data-fetchers/thousand-mock.d.ts +9 -2
  35. package/dist/data-fetchers/thousand-mock.d.ts.map +1 -1
  36. package/dist/data-fetchers/thousand-mock.js +66 -4
  37. package/dist/data-fetchers/thousand-mock.js.map +1 -1
  38. package/dist/data-fetchers/tradingview.d.ts +9 -2
  39. package/dist/data-fetchers/tradingview.d.ts.map +1 -1
  40. package/dist/data-fetchers/tradingview.js +73 -9
  41. package/dist/data-fetchers/tradingview.js.map +1 -1
  42. package/dist/data-fetchers/types.d.ts +21 -0
  43. package/dist/data-fetchers/types.d.ts.map +1 -0
  44. package/dist/data-fetchers/types.js +2 -0
  45. package/dist/data-fetchers/types.js.map +1 -0
  46. package/dist/engine/indicators/registerBuiltins.d.ts +2 -40
  47. package/dist/engine/indicators/registerBuiltins.d.ts.map +1 -1
  48. package/dist/engine/indicators/registerBuiltins.js +55 -42
  49. package/dist/engine/indicators/registerBuiltins.js.map +1 -1
  50. package/dist/engine/renderers/Indicator/ichimoku.d.ts.map +1 -1
  51. package/dist/engine/renderers/Indicator/ichimoku.js +8 -5
  52. package/dist/engine/renderers/Indicator/ichimoku.js.map +1 -1
  53. package/dist/engine/renderers/Indicator/index.d.ts +0 -39
  54. package/dist/engine/renderers/Indicator/index.d.ts.map +1 -1
  55. package/dist/engine/renderers/Indicator/index.js +0 -78
  56. package/dist/engine/renderers/Indicator/index.js.map +1 -1
  57. package/dist/engine/renderers/Indicator/indicatorCatalog.d.ts +1 -3
  58. package/dist/engine/renderers/Indicator/indicatorCatalog.d.ts.map +1 -1
  59. package/dist/engine/renderers/Indicator/indicatorCatalog.js +27 -17
  60. package/dist/engine/renderers/Indicator/indicatorCatalog.js.map +1 -1
  61. package/dist/engine/renderers/Indicator/sar.d.ts.map +1 -1
  62. package/dist/engine/renderers/Indicator/sar.js +3 -3
  63. package/dist/engine/renderers/Indicator/sar.js.map +1 -1
  64. package/dist/engine/renderers/Indicator/supertrend.d.ts.map +1 -1
  65. package/dist/engine/renderers/Indicator/supertrend.js +3 -3
  66. package/dist/engine/renderers/Indicator/supertrend.js.map +1 -1
  67. package/dist/index.d.ts +1 -0
  68. package/dist/index.d.ts.map +1 -1
  69. package/dist/index.js +1 -0
  70. package/dist/index.js.map +1 -1
  71. package/dist/mcp/chartBridge.d.ts +47 -0
  72. package/dist/mcp/chartBridge.d.ts.map +1 -0
  73. package/dist/mcp/chartBridge.js +167 -0
  74. package/dist/mcp/chartBridge.js.map +1 -0
  75. package/dist/mcp/index.d.ts +3 -0
  76. package/dist/mcp/index.d.ts.map +1 -0
  77. package/dist/mcp/index.js +2 -0
  78. package/dist/mcp/index.js.map +1 -0
  79. package/dist/mcp/types.d.ts +17 -0
  80. package/dist/mcp/types.d.ts.map +1 -0
  81. package/dist/mcp/types.js +2 -0
  82. package/dist/mcp/types.js.map +1 -0
  83. package/dist/version.d.ts +1 -1
  84. package/dist/version.js +1 -1
  85. package/package.json +1 -1
  86. package/src/controllers/createChartController.ts +38 -1
  87. package/src/controllers/index.ts +1 -0
  88. package/src/controllers/types.ts +10 -1
  89. package/src/data-fetchers/__tests__/fetcherRegistry.test.ts +192 -0
  90. package/src/data-fetchers/baostock.ts +54 -22
  91. package/src/data-fetchers/fetcherDefinitionRegistry.ts +50 -0
  92. package/src/data-fetchers/gotdx.ts +28 -6
  93. package/src/data-fetchers/hundred-mock.ts +21 -4
  94. package/src/data-fetchers/index.ts +19 -5
  95. package/src/data-fetchers/router.ts +27 -18
  96. package/src/data-fetchers/thousand-mock.ts +21 -4
  97. package/src/data-fetchers/tradingview.ts +30 -11
  98. package/src/data-fetchers/types.ts +27 -0
  99. package/src/engine/__tests__/chart.dpr.test.ts +6 -1
  100. package/src/engine/indicators/__tests__/chartIndicatorManager.test.ts +6 -1
  101. package/src/engine/indicators/__tests__/registerBuiltins.test.ts +6 -2
  102. package/src/engine/indicators/__tests__/scheduler.test.ts +8 -3
  103. package/src/engine/indicators/__tests__/stateComposer.test.ts +6 -2
  104. package/src/engine/indicators/registerBuiltins.ts +58 -42
  105. package/src/engine/renderers/Indicator/__tests__/createSubIndicatorRenderer.test.ts +6 -2
  106. package/src/engine/renderers/Indicator/ichimoku.ts +10 -4
  107. package/src/engine/renderers/Indicator/index.ts +0 -92
  108. package/src/engine/renderers/Indicator/indicatorCatalog.ts +28 -19
  109. package/src/engine/renderers/Indicator/sar.ts +3 -3
  110. package/src/engine/renderers/Indicator/supertrend.ts +3 -4
  111. package/src/index.ts +1 -0
  112. package/src/mcp/chartBridge.ts +220 -0
  113. package/src/mcp/index.ts +2 -0
  114. package/src/mcp/types.ts +19 -0
  115. package/src/version.ts +1 -1
@@ -227,6 +227,15 @@ export interface ChartMountOptions {
227
227
  priceLabelWidth?: number
228
228
  minKWidth?: number
229
229
  maxKWidth?: number
230
+
231
+ // MCP / AI runtime bridge
232
+ mcp?: {
233
+ wsUrl?: string
234
+ onToolCall?: (call: { name: string; input: Record<string, unknown> }) =>
235
+ | { success: boolean; error?: string; data?: unknown }
236
+ | Promise<{ success: boolean; error?: string; data?: unknown }>
237
+ autoReconnect?: boolean
238
+ }
230
239
  }
231
240
 
232
241
  export interface ChartController extends DrawingChartAdapter {
@@ -325,7 +334,7 @@ export interface ChartController extends DrawingChartAdapter {
325
334
  * Implementation lives in packages/core/src/controllers/createChartController.ts
326
335
  * (Phase 1 deliverable). It wires the existing Chart engine in src/core/chart.ts.
327
336
  */
328
- export type ChartControllerFactory = (opts: ChartMountOptions) => ChartController
337
+ export type ChartControllerFactory = (opts: ChartMountOptions) => Promise<ChartController>
329
338
 
330
339
  // ---------------------------------------------------------------------------
331
340
  // Legacy type aliases (deprecated — kept for internal sub-controller tests)
@@ -0,0 +1,192 @@
1
+ import { describe, expect, it, vi, beforeEach } from 'vitest'
2
+ import {
3
+ DataFetcher,
4
+ getRegisteredFetcher,
5
+ fetcherSupportsPeriod,
6
+ clearRegisteredFetchersForTest,
7
+ } from '../fetcherDefinitionRegistry'
8
+ import { routerDataFetcher } from '../router'
9
+ import type { DataFetcherFn } from '../types'
10
+ import type { KLineData } from '../../controllers/types'
11
+
12
+ const mockFetch = vi.fn<() => Promise<ReadonlyArray<KLineData>>>()
13
+
14
+ const fetchFn: DataFetcherFn = async () => mockFetch()
15
+
16
+ const defaultConfig = {
17
+ symbol: '000001',
18
+ startDate: '2024-01-01',
19
+ endDate: '2024-01-31',
20
+ period: 'daily',
21
+ adjust: 'none',
22
+ }
23
+
24
+ describe('DataFetcher registry', () => {
25
+ beforeEach(() => {
26
+ clearRegisteredFetchersForTest()
27
+ })
28
+
29
+ it('collects decorated fetcher definition with metadata', () => {
30
+ @DataFetcher({
31
+ name: 'test',
32
+ displayName: 'Test',
33
+ version: '1.0.0',
34
+ capabilities: ['daily', 'weekly'],
35
+ })
36
+ class TestFetcher {
37
+ static fetcher = fetchFn
38
+ }
39
+ void TestFetcher
40
+
41
+ const def = getRegisteredFetcher('test')
42
+ expect(def).toBeDefined()
43
+ expect(def!.name).toBe('test')
44
+ expect(def!.displayName).toBe('Test')
45
+ expect(def!.version).toBe('1.0.0')
46
+ expect(def!.capabilities).toEqual(['daily', 'weekly'])
47
+ expect(def!.fetcher).toBe(fetchFn)
48
+ })
49
+
50
+ it('fetcherSupportsPeriod returns true for exact match', () => {
51
+ @DataFetcher({ name: 'test', displayName: 'Test', capabilities: ['daily', 'weekly'] })
52
+ class TestFetcher { static fetcher = fetchFn }
53
+ void TestFetcher
54
+
55
+ expect(fetcherSupportsPeriod('test', 'daily')).toBe(true)
56
+ expect(fetcherSupportsPeriod('test', 'weekly')).toBe(true)
57
+ })
58
+
59
+ it('fetcherSupportsPeriod returns false for unsupported period', () => {
60
+ @DataFetcher({ name: 'test', displayName: 'Test', capabilities: ['weekly'] })
61
+ class TestFetcher { static fetcher = fetchFn }
62
+ void TestFetcher
63
+
64
+ expect(fetcherSupportsPeriod('test', 'daily')).toBe(false)
65
+ expect(fetcherSupportsPeriod('test', '5min')).toBe(false)
66
+ })
67
+
68
+ it('fetcherSupportsPeriod accepts wildcard * for any period', () => {
69
+ @DataFetcher({ name: 'test', displayName: 'Test', capabilities: ['*'] })
70
+ class TestFetcher { static fetcher = fetchFn }
71
+ void TestFetcher
72
+
73
+ expect(fetcherSupportsPeriod('test', 'daily')).toBe(true)
74
+ expect(fetcherSupportsPeriod('test', '5min')).toBe(true)
75
+ expect(fetcherSupportsPeriod('test', 'quarterly')).toBe(true)
76
+ })
77
+
78
+ it('fetcherSupportsPeriod returns false for empty capabilities', () => {
79
+ @DataFetcher({ name: 'test', displayName: 'Test', capabilities: [] })
80
+ class TestFetcher { static fetcher = fetchFn }
81
+ void TestFetcher
82
+
83
+ expect(fetcherSupportsPeriod('test', 'daily')).toBe(false)
84
+ })
85
+
86
+ it('fetcherSupportsPeriod returns false when capabilities not set', () => {
87
+ @DataFetcher({ name: 'test', displayName: 'Test' })
88
+ class TestFetcher { static fetcher = fetchFn }
89
+ void TestFetcher
90
+
91
+ expect(fetcherSupportsPeriod('test', 'daily')).toBe(false)
92
+ })
93
+
94
+ it('fetcherSupportsPeriod returns false for unknown source', () => {
95
+ expect(fetcherSupportsPeriod('nonexistent', 'daily')).toBe(false)
96
+ })
97
+
98
+ it('clearRegisteredFetchersForTest removes all definitions', () => {
99
+ @DataFetcher({ name: 'test', displayName: 'Test', capabilities: ['daily'] })
100
+ class TestFetcher { static fetcher = fetchFn }
101
+ void TestFetcher
102
+
103
+ expect(getRegisteredFetcher('test')).toBeDefined()
104
+ clearRegisteredFetchersForTest()
105
+ expect(getRegisteredFetcher('test')).toBeUndefined()
106
+ })
107
+ })
108
+
109
+ describe('routerDataFetcher capability check', () => {
110
+ beforeEach(() => {
111
+ clearRegisteredFetchersForTest()
112
+ mockFetch.mockReset()
113
+ })
114
+
115
+ it('passes through request when period is supported', async () => {
116
+ const data: KLineData[] = [{ timestamp: 1000, open: 100, high: 110, low: 90, close: 105, volume: 1000 }]
117
+ mockFetch.mockResolvedValue(data)
118
+
119
+ @DataFetcher({ name: 'test', displayName: 'Test', capabilities: ['daily'] })
120
+ class TestFetcher { static fetcher = fetchFn }
121
+ void TestFetcher
122
+
123
+ const result = await routerDataFetcher('test', defaultConfig)
124
+ expect(result).toBe(data)
125
+ expect(mockFetch).toHaveBeenCalledTimes(1)
126
+ })
127
+
128
+ it('passes through request when capabilities are wildcard', async () => {
129
+ const data: KLineData[] = [{ timestamp: 1000, open: 100, high: 110, low: 90, close: 105, volume: 1000 }]
130
+ mockFetch.mockResolvedValue(data)
131
+
132
+ @DataFetcher({ name: 'test', displayName: 'Test', capabilities: ['*'] })
133
+ class TestFetcher { static fetcher = fetchFn }
134
+ void TestFetcher
135
+
136
+ const result = await routerDataFetcher('test', { ...defaultConfig, period: '5min' })
137
+ expect(result).toBe(data)
138
+ })
139
+
140
+ it('throws when period is not in capabilities', async () => {
141
+ @DataFetcher({ name: 'test', displayName: 'Test', capabilities: ['weekly'] })
142
+ class TestFetcher { static fetcher = fetchFn }
143
+ void TestFetcher
144
+
145
+ await expect(
146
+ routerDataFetcher('test', defaultConfig),
147
+ ).rejects.toThrow(/does not support period/)
148
+ })
149
+
150
+ it('throws with error message listing supported capabilities', async () => {
151
+ @DataFetcher({ name: 'test', displayName: 'Test', capabilities: ['weekly', 'monthly'] })
152
+ class TestFetcher { static fetcher = fetchFn }
153
+ void TestFetcher
154
+
155
+ await expect(
156
+ routerDataFetcher('test', { ...defaultConfig, period: '5min' }),
157
+ ).rejects.toThrow(/weekly, monthly/)
158
+ })
159
+
160
+ it('throws when capabilities is empty array', async () => {
161
+ @DataFetcher({ name: 'test', displayName: 'Test', capabilities: [] })
162
+ class TestFetcher { static fetcher = fetchFn }
163
+ void TestFetcher
164
+
165
+ await expect(
166
+ routerDataFetcher('test', defaultConfig),
167
+ ).rejects.toThrow(/does not support period/)
168
+ })
169
+
170
+ it('throws when capabilities is not set', async () => {
171
+ @DataFetcher({ name: 'test', displayName: 'Test' })
172
+ class TestFetcher { static fetcher = fetchFn }
173
+ void TestFetcher
174
+
175
+ await expect(
176
+ routerDataFetcher('test', defaultConfig),
177
+ ).rejects.toThrow(/does not support period/)
178
+ })
179
+
180
+ it('falls back to registered baostock for unknown source', async () => {
181
+ const data: KLineData[] = [{ timestamp: 2000, open: 200, high: 210, low: 190, close: 205, volume: 2000 }]
182
+ const baostockFn = vi.fn<DataFetcherFn>().mockResolvedValue(data)
183
+
184
+ @DataFetcher({ name: 'baostock', displayName: 'BaoStock', capabilities: ['*'] })
185
+ class BaoStockStub { static fetcher = baostockFn }
186
+ void BaoStockStub
187
+
188
+ const result = await routerDataFetcher('nonexistent', defaultConfig)
189
+ expect(result).toBe(data)
190
+ expect(baostockFn).toHaveBeenCalledWith('nonexistent', expect.objectContaining({ period: 'daily' }))
191
+ })
192
+ })
@@ -1,34 +1,66 @@
1
- import type { DataFetcher, KLineData } from '../controllers/types'
1
+ import type { KLineData } from '../controllers/types'
2
+ import { DataFetcher } from './fetcherDefinitionRegistry'
3
+ import type { FetchConfig } from './types'
2
4
 
3
- export const baostockDataFetcher: DataFetcher = async (source, config) => {
4
- console.log(`[baostock] fetching ${config.symbol} ${config.period} ${config.startDate}~${config.endDate}`)
5
- const baseUrl = source === 'baostock' ? 'http://localhost:8000' : ''
6
- const adjustMap: Record<string, string> = { qfq: '2', hfq: '1', none: '3' }
7
- const periodMap: Record<string, string> = { daily: 'd', weekly: 'w', monthly: 'm', '5min': '5', '15min': '15', '30min': '30', '60min': '60' }
8
- const adjustflag = adjustMap[config.adjust] ?? '3'
9
- const url = `${baseUrl}/api/stock/kdata?stock_code=${config.symbol}&start_date=${config.startDate}&end_date=${config.endDate}&frequency=${periodMap[config.period] ?? 'd'}&adjustflag=${adjustflag}`
5
+ const ADJUST_MAP: Record<string, string> = { qfq: '2', hfq: '1', none: '3' }
6
+
7
+ const PERIOD_MAP: Record<string, string> = {
8
+ daily: 'd',
9
+ weekly: 'w',
10
+ monthly: 'm',
11
+ '5min': '5',
12
+ '15min': '15',
13
+ '30min': '30',
14
+ '60min': '60',
15
+ }
16
+
17
+ const BASE_URL = 'http://localhost:8000'
18
+
19
+ async function fetchBaoStock(
20
+ _source: string,
21
+ config: FetchConfig,
22
+ ): Promise<ReadonlyArray<KLineData>> {
23
+ console.log(
24
+ `[baostock] fetching ${config.symbol} ${config.period} ${config.startDate}~${config.endDate}`,
25
+ )
26
+ const url = `${BASE_URL}/api/stock/kdata?stock_code=${config.symbol}&start_date=${config.startDate}&end_date=${config.endDate}&frequency=${PERIOD_MAP[config.period] ?? 'd'}&adjustflag=${ADJUST_MAP[config.adjust] ?? '3'}`
10
27
  try {
11
28
  const res = await fetch(url)
12
- console.log(res)
13
29
  if (!res.ok) {
14
30
  throw new Error(`[baostock] fetch failed: ${res.status} ${res.statusText}`)
15
31
  }
16
32
  const json = await res.json()
17
- console.log(json)
18
- return (json.data ?? json).map((item: Record<string, unknown>) => ({
19
- timestamp: new Date(item.date as string).getTime(),
20
- date: item.date as string,
21
- open: Number(item.open),
22
- high: Number(item.high),
23
- low: Number(item.low),
24
- close: Number(item.close),
25
- volume: Number(item.volume),
26
- turnover: Number(item.amount ?? 0),
27
- turnoverRate: item.turn === '' ? 0 : Number(item.turn),
28
- stockCode: String(item.code ?? config.symbol),
29
- })) as KLineData[]
33
+ return (json.data ?? json).map(
34
+ (item: Record<string, unknown>) =>
35
+ ({
36
+ timestamp: new Date(item.date as string).getTime(),
37
+ date: item.date as string,
38
+ open: Number(item.open),
39
+ high: Number(item.high),
40
+ low: Number(item.low),
41
+ close: Number(item.close),
42
+ volume: Number(item.volume),
43
+ turnover: Number(item.amount ?? 0),
44
+ turnoverRate: item.turn === '' ? 0 : Number(item.turn),
45
+ stockCode: String(item.code ?? config.symbol),
46
+ }) as KLineData,
47
+ )
30
48
  } catch (err) {
31
49
  console.warn('[baostock] network error:', err)
32
50
  throw err
33
51
  }
34
52
  }
53
+
54
+ @DataFetcher({
55
+ name: 'baostock',
56
+ displayName: 'BaoStock',
57
+ description: 'BaoStock data source via local proxy',
58
+ version: '1.0.0',
59
+ capabilities: ['daily', 'weekly', 'monthly', '5min', '15min', '30min', '60min'],
60
+ })
61
+ export class BaoStockFetcher {
62
+ static fetcher = fetchBaoStock
63
+ }
64
+
65
+ /** @deprecated Use `BaoStockFetcher.fetcher` directly or rely on routerDataFetcher. */
66
+ export const baostockDataFetcher = fetchBaoStock
@@ -0,0 +1,50 @@
1
+ import type { DataFetcherDefinitionConfig, DataFetcherDefinition, DataFetcherFn } from './types'
2
+
3
+ type DataFetcherClass = {
4
+ new(...args: never[]): unknown
5
+ fetcher: DataFetcherFn
6
+ }
7
+
8
+ const definitions = new Map<string, DataFetcherDefinition>()
9
+
10
+ export function DataFetcher(config: DataFetcherDefinitionConfig) {
11
+ return function <T extends DataFetcherClass>(value: T, context: ClassDecoratorContext<T>): T {
12
+ context.addInitializer(function (this: T) {
13
+ if (typeof this.fetcher !== 'function') {
14
+ throw new Error(
15
+ `[DataFetcher] '${config.name}' definition must expose static fetcher`,
16
+ )
17
+ }
18
+ definitions.set(config.name, {
19
+ ...config,
20
+ fetcher: this.fetcher,
21
+ })
22
+ })
23
+ return value
24
+ }
25
+ }
26
+
27
+ export function getRegisteredFetcher(
28
+ name: string,
29
+ ): DataFetcherDefinition | undefined {
30
+ return definitions.get(name)
31
+ }
32
+
33
+ export function getRegisteredFetchers(): DataFetcherDefinition[] {
34
+ return Array.from(definitions.values())
35
+ }
36
+
37
+ export function fetcherHasCapability(name: string, capability: string): boolean {
38
+ return definitions.get(name)?.capabilities?.includes(capability) ?? false
39
+ }
40
+
41
+ export function fetcherSupportsPeriod(name: string, period: string): boolean {
42
+ const def = definitions.get(name)
43
+ if (!def) return false
44
+ if (!def.capabilities || def.capabilities.length === 0) return false
45
+ return def.capabilities.includes('*') || def.capabilities.includes(period)
46
+ }
47
+
48
+ export function clearRegisteredFetchersForTest(): void {
49
+ definitions.clear()
50
+ }
@@ -1,4 +1,6 @@
1
- import type { DataFetcher, KLineData } from '../controllers/types'
1
+ import type { KLineData } from '../controllers/types'
2
+ import { DataFetcher } from './fetcherDefinitionRegistry'
3
+ import type { FetchConfig } from './types'
2
4
 
3
5
  const PERIOD_TO_CATEGORY: Record<string, number> = {
4
6
  '1min': 8,
@@ -27,6 +29,8 @@ const EXCHANGE_EX_CATEGORY: Record<string, number> = {
27
29
  DE: 73,
28
30
  }
29
31
 
32
+ const BASE_URL = 'http://127.0.0.1:8080'
33
+
30
34
  interface SecurityBar {
31
35
  Last: number
32
36
  Open: number
@@ -91,9 +95,10 @@ function mapExItem(item: ExKLineItem, code: string): KLineData {
91
95
  }
92
96
  }
93
97
 
94
- export const gotdxDataFetcher: DataFetcher = async (source, config) => {
95
- const baseUrl = source === 'gotdx' ? 'http://127.0.0.1:8080' : ''
96
-
98
+ async function fetchGotdx(
99
+ _source: string,
100
+ config: FetchConfig,
101
+ ): Promise<ReadonlyArray<KLineData>> {
97
102
  if (config.exchange && config.exchange in EXCHANGE_EX_CATEGORY) {
98
103
  const category = EXCHANGE_EX_CATEGORY[config.exchange]
99
104
  const period = PERIOD_TO_CATEGORY[config.period] ?? 4
@@ -105,7 +110,7 @@ export const gotdxDataFetcher: DataFetcher = async (source, config) => {
105
110
  end_date: config.endDate,
106
111
  times: 1,
107
112
  }
108
- const res = await fetch(`${baseUrl}/api/ex/kline-by-date`, {
113
+ const res = await fetch(`${BASE_URL}/api/ex/kline-by-date`, {
109
114
  method: 'POST',
110
115
  headers: { 'Content-Type': 'application/json' },
111
116
  body: JSON.stringify(body),
@@ -127,7 +132,7 @@ export const gotdxDataFetcher: DataFetcher = async (source, config) => {
127
132
  times: 1,
128
133
  adjust,
129
134
  }
130
- const res = await fetch(`${baseUrl}/api/stock/kline-by-date`, {
135
+ const res = await fetch(`${BASE_URL}/api/stock/kline-by-date`, {
131
136
  method: 'POST',
132
137
  headers: { 'Content-Type': 'application/json' },
133
138
  body: JSON.stringify(body),
@@ -136,3 +141,20 @@ export const gotdxDataFetcher: DataFetcher = async (source, config) => {
136
141
  const list: SecurityBar[] = await res.json()
137
142
  return list.map((item) => mapBar(item, config.symbol))
138
143
  }
144
+
145
+ @DataFetcher({
146
+ name: 'gotdx',
147
+ displayName: 'GOTDX',
148
+ description: 'TDX data source via local proxy',
149
+ version: '1.0.0',
150
+ capabilities: [
151
+ '1min', '5min', '15min', '30min', '60min',
152
+ 'daily', 'weekly', 'monthly', 'quarterly', 'yearly',
153
+ ],
154
+ })
155
+ export class GotdxFetcher {
156
+ static fetcher = fetchGotdx
157
+ }
158
+
159
+ /** @deprecated Use `GotdxFetcher.fetcher` directly or rely on routerDataFetcher. */
160
+ export const gotdxDataFetcher = fetchGotdx
@@ -1,6 +1,11 @@
1
- import type { DataFetcher, KLineData } from '../controllers/types'
1
+ import type { KLineData } from '../controllers/types'
2
+ import { DataFetcher } from './fetcherDefinitionRegistry'
3
+ import type { FetchConfig } from './types'
2
4
 
3
- export const hundredMockDataFetcher: DataFetcher = async (_source, config) => {
5
+ async function fetchHundredMock(
6
+ _source: string,
7
+ config: FetchConfig,
8
+ ): Promise<ReadonlyArray<KLineData>> {
4
9
  console.log(`[hundred-mock] generating ${config.symbol} ${config.period}`)
5
10
  const start = new Date(config.startDate).getTime()
6
11
  const end = new Date(config.endDate).getTime()
@@ -25,7 +30,6 @@ export const hundredMockDataFetcher: DataFetcher = async (_source, config) => {
25
30
 
26
31
  const meanReversionStrength = 0.005
27
32
 
28
- // raw random walk with mean reversion (close prices before bridge)
29
33
  const rawWalk: number[] = [basePrice]
30
34
  for (let i = 1; i < totalDays; i++) {
31
35
  const prev = rawWalk[i - 1]!
@@ -34,7 +38,6 @@ export const hundredMockDataFetcher: DataFetcher = async (_source, config) => {
34
38
  rawWalk.push(prev + change)
35
39
  }
36
40
 
37
- // Brownian bridge: subtract linear drift so last close = basePrice
38
41
  const finalOffset = rawWalk[totalDays - 1]! - basePrice
39
42
  for (let i = 0; i < totalDays; i++) {
40
43
  const bridge = finalOffset * (i / (totalDays - 1))
@@ -59,3 +62,17 @@ export const hundredMockDataFetcher: DataFetcher = async (_source, config) => {
59
62
 
60
63
  return data
61
64
  }
65
+
66
+ @DataFetcher({
67
+ name: 'mock-100',
68
+ displayName: 'Mock 100',
69
+ description: 'Generates ~100 random K-line bars with Brownian bridge',
70
+ version: '1.0.0',
71
+ capabilities: ['*'],
72
+ })
73
+ export class HundredMockFetcher {
74
+ static fetcher = fetchHundredMock
75
+ }
76
+
77
+ /** @deprecated Use `HundredMockFetcher.fetcher` directly or rely on routerDataFetcher. */
78
+ export const hundredMockDataFetcher = fetchHundredMock
@@ -1,8 +1,22 @@
1
- export { thousandMockDataFetcher } from './thousand-mock'
2
- export { hundredMockDataFetcher } from './hundred-mock'
3
- export { baostockDataFetcher } from './baostock'
4
- export { tradingviewDataFetcher } from './tradingview'
5
- export { gotdxDataFetcher } from './gotdx'
1
+ export { HundredMockFetcher, hundredMockDataFetcher } from './hundred-mock'
2
+ export { ThousandMockFetcher, thousandMockDataFetcher } from './thousand-mock'
3
+ export { BaoStockFetcher, baostockDataFetcher } from './baostock'
4
+ export { TradingviewFetcher, tradingviewDataFetcher } from './tradingview'
5
+ export { GotdxFetcher, gotdxDataFetcher } from './gotdx'
6
6
  export { routerDataFetcher } from './router'
7
7
  export { DataBuffer } from './dataBuffer'
8
8
  export type { DataWindow } from './dataBuffer'
9
+ export {
10
+ DataFetcher,
11
+ getRegisteredFetcher,
12
+ getRegisteredFetchers,
13
+ fetcherHasCapability,
14
+ fetcherSupportsPeriod,
15
+ clearRegisteredFetchersForTest,
16
+ } from './fetcherDefinitionRegistry'
17
+ export type {
18
+ FetchConfig,
19
+ DataFetcherDefinitionConfig,
20
+ DataFetcherDefinition,
21
+ DataFetcherFn,
22
+ } from './types'
@@ -1,23 +1,32 @@
1
1
  import type { DataFetcher } from '../controllers/types'
2
- import { baostockDataFetcher } from './baostock'
3
- import { gotdxDataFetcher } from './gotdx'
4
- import { hundredMockDataFetcher } from './hundred-mock'
5
- import { thousandMockDataFetcher } from './thousand-mock'
6
- import { tradingviewDataFetcher } from './tradingview'
2
+ import { getRegisteredFetcher, fetcherSupportsPeriod } from './fetcherDefinitionRegistry'
3
+
4
+ const FALLBACK_SOURCE = 'baostock'
7
5
 
8
6
  export const routerDataFetcher: DataFetcher = (source, config) => {
9
- switch (source) {
10
- case 'baostock':
11
- return baostockDataFetcher(source, config)
12
- case 'gotdx':
13
- return gotdxDataFetcher(source, config)
14
- case 'tradingview':
15
- return tradingviewDataFetcher(source, config)
16
- case 'mock-100':
17
- return hundredMockDataFetcher(source, config)
18
- case 'mock-10000':
19
- return thousandMockDataFetcher(source, config)
20
- default:
21
- return hundredMockDataFetcher(source, config)
7
+ const def = getRegisteredFetcher(source)
8
+ if (!def) {
9
+ console.warn(
10
+ `[DataFetcher] unknown source "${source}", falling back to "${FALLBACK_SOURCE}"`,
11
+ )
12
+ const fallback = getRegisteredFetcher(FALLBACK_SOURCE)
13
+ if (!fallback) {
14
+ return Promise.reject(
15
+ new Error(
16
+ `[DataFetcher] no fetcher registered for "${source}" and no fallback available`,
17
+ ),
18
+ )
19
+ }
20
+ return fallback.fetcher(source, config)
21
+ }
22
+
23
+ if (!fetcherSupportsPeriod(source, config.period)) {
24
+ return Promise.reject(
25
+ new Error(
26
+ `[DataFetcher] "${source}" does not support period "${config.period}". Supported: ${def.capabilities?.join(', ') ?? 'none'}`,
27
+ ),
28
+ )
22
29
  }
30
+
31
+ return def.fetcher(source, config)
23
32
  }
@@ -1,6 +1,11 @@
1
- import type { DataFetcher, KLineData } from '../controllers/types'
1
+ import type { KLineData } from '../controllers/types'
2
+ import { DataFetcher } from './fetcherDefinitionRegistry'
3
+ import type { FetchConfig } from './types'
2
4
 
3
- export const thousandMockDataFetcher: DataFetcher = async (_source, _config) => {
5
+ async function fetchThousandMock(
6
+ _source: string,
7
+ _config: FetchConfig,
8
+ ): Promise<ReadonlyArray<KLineData>> {
4
9
  console.log('[thousand-mock] generating 10k K-lines')
5
10
  const data: KLineData[] = []
6
11
  const startTime = new Date('2020-01-01').getTime()
@@ -11,7 +16,6 @@ export const thousandMockDataFetcher: DataFetcher = async (_source, _config) =>
11
16
  const meanReversionStrength = 0.0005
12
17
  const volatility = 0.02
13
18
 
14
- // raw random walk with mean reversion (close prices before bridge)
15
19
  const rawWalk: number[] = [basePrice]
16
20
  for (let i = 1; i < totalDays; i++) {
17
21
  const prev = rawWalk[i - 1]!
@@ -20,7 +24,6 @@ export const thousandMockDataFetcher: DataFetcher = async (_source, _config) =>
20
24
  rawWalk.push(prev + change)
21
25
  }
22
26
 
23
- // Brownian bridge: subtract linear drift so last close = basePrice
24
27
  const finalOffset = rawWalk[totalDays - 1]! - basePrice
25
28
  for (let i = 0; i < totalDays; i++) {
26
29
  const bridge = finalOffset * (i / (totalDays - 1))
@@ -44,3 +47,17 @@ export const thousandMockDataFetcher: DataFetcher = async (_source, _config) =>
44
47
 
45
48
  return data
46
49
  }
50
+
51
+ @DataFetcher({
52
+ name: 'mock-10000',
53
+ displayName: 'Mock 10000',
54
+ description: 'Generates ~10,000 random K-line bars with Brownian bridge',
55
+ version: '1.0.0',
56
+ capabilities: ['*'],
57
+ })
58
+ export class ThousandMockFetcher {
59
+ static fetcher = fetchThousandMock
60
+ }
61
+
62
+ /** @deprecated Use `ThousandMockFetcher.fetcher` directly or rely on routerDataFetcher. */
63
+ export const thousandMockDataFetcher = fetchThousandMock
@@ -1,4 +1,6 @@
1
- import type { DataFetcher, KLineData } from '../controllers/types'
1
+ import type { KLineData } from '../controllers/types'
2
+ import { DataFetcher } from './fetcherDefinitionRegistry'
3
+ import type { FetchConfig } from './types'
2
4
 
3
5
  const PERIOD_TO_TIMEFRAME: Record<string, string> = {
4
6
  daily: '1d',
@@ -10,21 +12,25 @@ const PERIOD_TO_TIMEFRAME: Record<string, string> = {
10
12
  '60min': '60m',
11
13
  }
12
14
 
13
- export const tradingviewDataFetcher: DataFetcher = async (source, config) => {
14
- const baseUrl = source === 'tradingview' ? 'http://localhost:8000' : ''
15
+ const ADJUST_TO_TV: Record<string, string | undefined> = {
16
+ qfq: 'dividends',
17
+ splits: 'splits',
18
+ none: 'none',
19
+ }
20
+
21
+ const BASE_URL = 'http://localhost:8000'
22
+
23
+ async function fetchTradingview(
24
+ _source: string,
25
+ config: FetchConfig,
26
+ ): Promise<ReadonlyArray<KLineData>> {
15
27
  const timeframe = PERIOD_TO_TIMEFRAME[config.period] ?? '1d'
16
28
  const startDate = config.startDate.split('T')[0]
17
29
  const endDate = config.endDate.split('T')[0]
18
-
19
- const ADJUST_TO_TV: Record<string, string | undefined> = {
20
- qfq: 'dividends',
21
- splits: 'splits',
22
- none: 'none',
23
- }
24
30
  const tvAdjust = ADJUST_TO_TV[config.adjust]
25
31
  const exchangeQ = config.exchange ? `&exchange=${config.exchange}` : ''
26
32
  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}`
33
+ const url = `${BASE_URL}/api/tradingview/kdata?symbol=${config.symbol}&timeframe=${timeframe}&start_date=${startDate}&end_date=${endDate}${exchangeQ}${adjustQ}`
28
34
  try {
29
35
  const res = await fetch(url)
30
36
  if (!res.ok) {
@@ -37,7 +43,6 @@ export const tradingviewDataFetcher: DataFetcher = async (source, config) => {
37
43
  if (json.warning) {
38
44
  console.warn(`[tradingview] ${json.warning}`)
39
45
  }
40
-
41
46
  return (json.data ?? []).map((item: Record<string, unknown>) => ({
42
47
  timestamp: item.ts_open as number,
43
48
  date: item.date as string,
@@ -53,3 +58,17 @@ export const tradingviewDataFetcher: DataFetcher = async (source, config) => {
53
58
  throw err
54
59
  }
55
60
  }
61
+
62
+ @DataFetcher({
63
+ name: 'tradingview',
64
+ displayName: 'TradingView',
65
+ description: 'TradingView-style data source via local proxy',
66
+ version: '1.0.0',
67
+ capabilities: ['daily', 'weekly', 'monthly', '5min', '15min', '30min', '60min'],
68
+ })
69
+ export class TradingviewFetcher {
70
+ static fetcher = fetchTradingview
71
+ }
72
+
73
+ /** @deprecated Use `TradingviewFetcher.fetcher` directly or rely on routerDataFetcher. */
74
+ export const tradingviewDataFetcher = fetchTradingview