@363045841yyt/klinechart-core 0.8.1 → 0.8.3

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 (138) hide show
  1. package/dist/controllers/createChartController.d.ts.map +1 -1
  2. package/dist/controllers/createChartController.js +31 -0
  3. package/dist/controllers/createChartController.js.map +1 -1
  4. package/dist/controllers/types.d.ts +16 -0
  5. package/dist/controllers/types.d.ts.map +1 -1
  6. package/dist/data-fetchers/baostock.d.ts +9 -2
  7. package/dist/data-fetchers/baostock.d.ts.map +1 -1
  8. package/dist/data-fetchers/baostock.js +78 -9
  9. package/dist/data-fetchers/baostock.js.map +1 -1
  10. package/dist/data-fetchers/dataBuffer.d.ts.map +1 -1
  11. package/dist/data-fetchers/dataBuffer.js +3 -0
  12. package/dist/data-fetchers/dataBuffer.js.map +1 -1
  13. package/dist/data-fetchers/fetcherDefinitionRegistry.d.ts +13 -0
  14. package/dist/data-fetchers/fetcherDefinitionRegistry.d.ts.map +1 -0
  15. package/dist/data-fetchers/fetcherDefinitionRegistry.js +36 -0
  16. package/dist/data-fetchers/fetcherDefinitionRegistry.js.map +1 -0
  17. package/dist/data-fetchers/gotdx.d.ts +10 -0
  18. package/dist/data-fetchers/gotdx.d.ts.map +1 -0
  19. package/dist/data-fetchers/gotdx.js +168 -0
  20. package/dist/data-fetchers/gotdx.js.map +1 -0
  21. package/dist/data-fetchers/hundred-mock.d.ts +9 -2
  22. package/dist/data-fetchers/hundred-mock.d.ts.map +1 -1
  23. package/dist/data-fetchers/hundred-mock.js +92 -7
  24. package/dist/data-fetchers/hundred-mock.js.map +1 -1
  25. package/dist/data-fetchers/index.d.ts +7 -4
  26. package/dist/data-fetchers/index.d.ts.map +1 -1
  27. package/dist/data-fetchers/index.js +6 -4
  28. package/dist/data-fetchers/index.js.map +1 -1
  29. package/dist/data-fetchers/router.d.ts.map +1 -1
  30. package/dist/data-fetchers/router.js +14 -15
  31. package/dist/data-fetchers/router.js.map +1 -1
  32. package/dist/data-fetchers/thousand-mock.d.ts +9 -2
  33. package/dist/data-fetchers/thousand-mock.d.ts.map +1 -1
  34. package/dist/data-fetchers/thousand-mock.js +88 -16
  35. package/dist/data-fetchers/thousand-mock.js.map +1 -1
  36. package/dist/data-fetchers/tradingview.d.ts +9 -2
  37. package/dist/data-fetchers/tradingview.d.ts.map +1 -1
  38. package/dist/data-fetchers/tradingview.js +75 -4
  39. package/dist/data-fetchers/tradingview.js.map +1 -1
  40. package/dist/data-fetchers/types.d.ts +21 -0
  41. package/dist/data-fetchers/types.d.ts.map +1 -0
  42. package/dist/data-fetchers/types.js +2 -0
  43. package/dist/data-fetchers/types.js.map +1 -0
  44. package/dist/engine/data/chartDataManager.d.ts +1 -0
  45. package/dist/engine/data/chartDataManager.d.ts.map +1 -1
  46. package/dist/engine/data/chartDataManager.js +3 -0
  47. package/dist/engine/data/chartDataManager.js.map +1 -1
  48. package/dist/engine/render/chartRenderer.d.ts.map +1 -1
  49. package/dist/engine/render/chartRenderer.js +2 -0
  50. package/dist/engine/render/chartRenderer.js.map +1 -1
  51. package/dist/engine/renderers/Indicator/ichimoku.d.ts.map +1 -1
  52. package/dist/engine/renderers/Indicator/ichimoku.js +8 -5
  53. package/dist/engine/renderers/Indicator/ichimoku.js.map +1 -1
  54. package/dist/engine/renderers/Indicator/mainIndicatorLegend.js +1 -1
  55. package/dist/engine/renderers/Indicator/mainIndicatorLegend.js.map +1 -1
  56. package/dist/engine/renderers/Indicator/sar.d.ts.map +1 -1
  57. package/dist/engine/renderers/Indicator/sar.js +3 -3
  58. package/dist/engine/renderers/Indicator/sar.js.map +1 -1
  59. package/dist/engine/renderers/Indicator/supertrend.d.ts.map +1 -1
  60. package/dist/engine/renderers/Indicator/supertrend.js +3 -3
  61. package/dist/engine/renderers/Indicator/supertrend.js.map +1 -1
  62. package/dist/engine/renderers/timeAxis.d.ts.map +1 -1
  63. package/dist/engine/renderers/timeAxis.js +1 -0
  64. package/dist/engine/renderers/timeAxis.js.map +1 -1
  65. package/dist/index.d.ts +2 -0
  66. package/dist/index.d.ts.map +1 -1
  67. package/dist/index.js +2 -0
  68. package/dist/index.js.map +1 -1
  69. package/dist/mcp/chartBridge.d.ts +47 -0
  70. package/dist/mcp/chartBridge.d.ts.map +1 -0
  71. package/dist/mcp/chartBridge.js +167 -0
  72. package/dist/mcp/chartBridge.js.map +1 -0
  73. package/dist/mcp/index.d.ts +3 -0
  74. package/dist/mcp/index.d.ts.map +1 -0
  75. package/dist/mcp/index.js +2 -0
  76. package/dist/mcp/index.js.map +1 -0
  77. package/dist/mcp/types.d.ts +17 -0
  78. package/dist/mcp/types.d.ts.map +1 -0
  79. package/dist/mcp/types.js +2 -0
  80. package/dist/mcp/types.js.map +1 -0
  81. package/dist/plugin/types.d.ts +2 -0
  82. package/dist/plugin/types.d.ts.map +1 -1
  83. package/dist/plugin/types.js.map +1 -1
  84. package/dist/semantic/index.d.ts +1 -1
  85. package/dist/semantic/index.d.ts.map +1 -1
  86. package/dist/semantic/index.js.map +1 -1
  87. package/dist/semantic/schema.json +1 -1
  88. package/dist/semantic/types.d.ts +2 -1
  89. package/dist/semantic/types.d.ts.map +1 -1
  90. package/dist/utils/dateFormat.d.ts +25 -0
  91. package/dist/utils/dateFormat.d.ts.map +1 -1
  92. package/dist/utils/dateFormat.js +78 -0
  93. package/dist/utils/dateFormat.js.map +1 -1
  94. package/dist/utils/kLineDraw/axis.d.ts +2 -0
  95. package/dist/utils/kLineDraw/axis.d.ts.map +1 -1
  96. package/dist/utils/kLineDraw/axis.js +11 -6
  97. package/dist/utils/kLineDraw/axis.js.map +1 -1
  98. package/dist/version.d.ts +1 -1
  99. package/dist/version.js +1 -1
  100. package/package.json +1 -1
  101. package/src/controllers/createChartController.ts +34 -0
  102. package/src/controllers/types.ts +9 -0
  103. package/src/data-fetchers/__tests__/dataBuffer.test.ts +5 -2
  104. package/src/data-fetchers/__tests__/fetcherRegistry.test.ts +192 -0
  105. package/src/data-fetchers/baostock.ts +54 -22
  106. package/src/data-fetchers/dataBuffer.ts +6 -0
  107. package/src/data-fetchers/fetcherDefinitionRegistry.ts +50 -0
  108. package/src/data-fetchers/gotdx.ts +160 -0
  109. package/src/data-fetchers/hundred-mock.ts +54 -7
  110. package/src/data-fetchers/index.ts +19 -4
  111. package/src/data-fetchers/router.ts +27 -15
  112. package/src/data-fetchers/thousand-mock.ts +49 -16
  113. package/src/data-fetchers/tradingview.ts +32 -6
  114. package/src/data-fetchers/types.ts +27 -0
  115. package/src/engine/data/chartDataManager.ts +4 -0
  116. package/src/engine/render/chartRenderer.ts +2 -0
  117. package/src/engine/renderers/Indicator/ichimoku.ts +10 -4
  118. package/src/engine/renderers/Indicator/mainIndicatorLegend.ts +1 -1
  119. package/src/engine/renderers/Indicator/sar.ts +3 -3
  120. package/src/engine/renderers/Indicator/supertrend.ts +3 -4
  121. package/src/engine/renderers/__tests__/boll.renderer.test.ts +1 -0
  122. package/src/engine/renderers/__tests__/ene.renderer.test.ts +1 -0
  123. package/src/engine/renderers/__tests__/expma.renderer.test.ts +1 -0
  124. package/src/engine/renderers/__tests__/ma.renderer.test.ts +1 -0
  125. package/src/engine/renderers/__tests__/mainIndicatorLegend.renderer.test.ts +1 -0
  126. package/src/engine/renderers/__tests__/yAxis.renderer.test.ts +1 -0
  127. package/src/engine/renderers/timeAxis.ts +1 -0
  128. package/src/index.ts +2 -0
  129. package/src/mcp/chartBridge.ts +220 -0
  130. package/src/mcp/index.ts +2 -0
  131. package/src/mcp/types.ts +19 -0
  132. package/src/plugin/types.ts +2 -0
  133. package/src/semantic/index.ts +1 -0
  134. package/src/semantic/schema.json +1 -1
  135. package/src/semantic/types.ts +3 -1
  136. package/src/utils/dateFormat.ts +85 -0
  137. package/src/utils/kLineDraw/axis.ts +13 -6
  138. package/src/version.ts +1 -1
@@ -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
@@ -153,6 +153,12 @@ export class DataBuffer {
153
153
  return fetchPromise.then((incoming) => {
154
154
  if (this._disposed) return
155
155
 
156
+ if (incoming.length === 0) {
157
+ throw new Error(
158
+ `[DataBuffer] empty data for ${spec.symbol} ${formatDate(startTs)}~${formatDate(endTs)}`,
159
+ )
160
+ }
161
+
156
162
  const oldLength = this._data.length
157
163
  const oldEarliestTs = oldLength > 0 ? this._data[0]!.timestamp : null
158
164
  const merged = mergeSortedData(this._data, [...incoming])
@@ -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
+ }
@@ -0,0 +1,160 @@
1
+ import type { KLineData } from '../controllers/types'
2
+ import { DataFetcher } from './fetcherDefinitionRegistry'
3
+ import type { FetchConfig } from './types'
4
+
5
+ const PERIOD_TO_CATEGORY: Record<string, number> = {
6
+ '1min': 8,
7
+ '5min': 0,
8
+ '15min': 1,
9
+ '30min': 2,
10
+ '60min': 3,
11
+ daily: 4,
12
+ weekly: 5,
13
+ monthly: 6,
14
+ quarterly: 10,
15
+ yearly: 11,
16
+ }
17
+
18
+ const ADJUST_MAP: Record<string, number> = {
19
+ none: 0,
20
+ qfq: 1,
21
+ hfq: 2,
22
+ splits: 0,
23
+ }
24
+
25
+ const EXCHANGE_EX_CATEGORY: Record<string, number> = {
26
+ US: 74,
27
+ HK: 71,
28
+ SG: 78,
29
+ DE: 73,
30
+ }
31
+
32
+ const BASE_URL = 'http://127.0.0.1:8080'
33
+
34
+ interface SecurityBar {
35
+ Last: number
36
+ Open: number
37
+ Close: number
38
+ High: number
39
+ Low: number
40
+ Vol: number
41
+ Amount: number
42
+ Turnover: number
43
+ RisePrice: number
44
+ RiseRate: number
45
+ Year: number
46
+ Month: number
47
+ Day: number
48
+ Hour: number
49
+ Minute: number
50
+ DateTime: string
51
+ UpCount: number
52
+ DownCount: number
53
+ }
54
+
55
+ interface ExKLineItem {
56
+ DateTime: string
57
+ Open: number
58
+ High: number
59
+ Low: number
60
+ Close: number
61
+ Amount: number
62
+ Vol: number
63
+ }
64
+
65
+ function mapBar(item: SecurityBar, code: string): KLineData {
66
+ const ts = new Date(item.DateTime).getTime()
67
+ return {
68
+ timestamp: ts,
69
+ date: item.DateTime.split('T')[0],
70
+ open: item.Open,
71
+ high: item.High,
72
+ low: item.Low,
73
+ close: item.Close,
74
+ volume: item.Vol,
75
+ turnover: item.Amount,
76
+ turnoverRate: item.Turnover,
77
+ changeAmount: item.RisePrice,
78
+ changePercent: item.RiseRate,
79
+ stockCode: code,
80
+ }
81
+ }
82
+
83
+ function mapExItem(item: ExKLineItem, code: string): KLineData {
84
+ const ts = new Date(item.DateTime).getTime()
85
+ return {
86
+ timestamp: ts,
87
+ date: item.DateTime.split('T')[0],
88
+ open: item.Open,
89
+ high: item.High,
90
+ low: item.Low,
91
+ close: item.Close,
92
+ volume: item.Vol,
93
+ turnover: item.Amount,
94
+ stockCode: code,
95
+ }
96
+ }
97
+
98
+ async function fetchGotdx(
99
+ _source: string,
100
+ config: FetchConfig,
101
+ ): Promise<ReadonlyArray<KLineData>> {
102
+ if (config.exchange && config.exchange in EXCHANGE_EX_CATEGORY) {
103
+ const category = EXCHANGE_EX_CATEGORY[config.exchange]
104
+ const period = PERIOD_TO_CATEGORY[config.period] ?? 4
105
+ const body = {
106
+ category,
107
+ code: config.symbol,
108
+ period,
109
+ start_date: config.startDate,
110
+ end_date: config.endDate,
111
+ times: 1,
112
+ }
113
+ const res = await fetch(`${BASE_URL}/api/ex/kline-by-date`, {
114
+ method: 'POST',
115
+ headers: { 'Content-Type': 'application/json' },
116
+ body: JSON.stringify(body),
117
+ })
118
+ if (!res.ok) throw new Error(`[gotdx] ex/kline-by-date failed: ${res.status} ${res.statusText}`)
119
+ const list: ExKLineItem[] = await res.json()
120
+ return list.map((item) => mapExItem(item, config.symbol))
121
+ }
122
+
123
+ const market = config.symbol.startsWith('6') || config.symbol.startsWith('9') ? 1 : 0
124
+ const category = PERIOD_TO_CATEGORY[config.period] ?? 4
125
+ const adjust = ADJUST_MAP[config.adjust] ?? 0
126
+ const body = {
127
+ market,
128
+ code: config.symbol,
129
+ category,
130
+ start_date: config.startDate,
131
+ end_date: config.endDate,
132
+ times: 1,
133
+ adjust,
134
+ }
135
+ const res = await fetch(`${BASE_URL}/api/stock/kline-by-date`, {
136
+ method: 'POST',
137
+ headers: { 'Content-Type': 'application/json' },
138
+ body: JSON.stringify(body),
139
+ })
140
+ if (!res.ok) throw new Error(`[gotdx] stock/kline-by-date failed: ${res.status} ${res.statusText}`)
141
+ const list: SecurityBar[] = await res.json()
142
+ return list.map((item) => mapBar(item, config.symbol))
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,18 +1,51 @@
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()
7
12
  const dayMs = 86400000
8
13
  const totalDays = Math.floor((end - start) / dayMs) + 1
14
+ if (totalDays <= 0) return []
15
+
16
+ const basePrice = 12.5
9
17
  const data: KLineData[] = []
10
- let price = 10 + Math.random() * 5
18
+
19
+ if (totalDays === 1) {
20
+ data.push({
21
+ timestamp: start,
22
+ open: basePrice,
23
+ high: basePrice,
24
+ low: basePrice,
25
+ close: basePrice,
26
+ volume: Math.round(Math.random() * 10000000 + 1000000),
27
+ })
28
+ return data
29
+ }
30
+
31
+ const meanReversionStrength = 0.005
32
+
33
+ const rawWalk: number[] = [basePrice]
34
+ for (let i = 1; i < totalDays; i++) {
35
+ const prev = rawWalk[i - 1]!
36
+ const reversion = meanReversionStrength * (basePrice - prev)
37
+ const change = (Math.random() - 0.48) * prev * 0.06 + reversion
38
+ rawWalk.push(prev + change)
39
+ }
40
+
41
+ const finalOffset = rawWalk[totalDays - 1]! - basePrice
11
42
  for (let i = 0; i < totalDays; i++) {
43
+ const bridge = finalOffset * (i / (totalDays - 1))
44
+ const close = Math.round((rawWalk[i]! - bridge) * 100) / 100
45
+
12
46
  const ts = start + i * dayMs
13
- const change = (Math.random() - 0.48) * price * 0.06
14
- const open = price
15
- const close = Math.round((open + change) * 100) / 100
47
+ const open = i === 0 ? basePrice : data[i - 1]!.close
48
+
16
49
  const high = Math.round(Math.max(open, close) * (1 + Math.random() * 0.03) * 100) / 100
17
50
  const low = Math.round(Math.min(open, close) * (1 - Math.random() * 0.03) * 100) / 100
18
51
  const volume = Math.round(Math.random() * 10000000 + 1000000)
@@ -25,7 +58,21 @@ export const hundredMockDataFetcher: DataFetcher = async (_source, config) => {
25
58
  volume,
26
59
  turnover: Math.round((volume * (open + close)) / 2),
27
60
  })
28
- price = close
29
61
  }
62
+
30
63
  return data
31
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,7 +1,22 @@
1
- export { thousandMockDataFetcher } from './thousand-mock'
2
- export { hundredMockDataFetcher } from './hundred-mock'
3
- export { baostockDataFetcher } from './baostock'
4
- export { tradingviewDataFetcher } from './tradingview'
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'
5
6
  export { routerDataFetcher } from './router'
6
7
  export { DataBuffer } from './dataBuffer'
7
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,20 +1,32 @@
1
1
  import type { DataFetcher } from '../controllers/types'
2
- import { baostockDataFetcher } from './baostock'
3
- import { hundredMockDataFetcher } from './hundred-mock'
4
- import { thousandMockDataFetcher } from './thousand-mock'
5
- import { tradingviewDataFetcher } from './tradingview'
2
+ import { getRegisteredFetcher, fetcherSupportsPeriod } from './fetcherDefinitionRegistry'
3
+
4
+ const FALLBACK_SOURCE = 'baostock'
6
5
 
7
6
  export const routerDataFetcher: DataFetcher = (source, config) => {
8
- switch (source) {
9
- case 'baostock':
10
- return baostockDataFetcher(source, config)
11
- case 'tradingview':
12
- return tradingviewDataFetcher(source, config)
13
- case 'mock-100':
14
- return hundredMockDataFetcher(source, config)
15
- case 'mock-10000':
16
- return thousandMockDataFetcher(source, config)
17
- default:
18
- 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
+ )
19
29
  }
30
+
31
+ return def.fetcher(source, config)
20
32
  }