@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.
- package/dist/controllers/createChartController.d.ts.map +1 -1
- package/dist/controllers/createChartController.js +31 -0
- package/dist/controllers/createChartController.js.map +1 -1
- package/dist/controllers/types.d.ts +16 -0
- package/dist/controllers/types.d.ts.map +1 -1
- package/dist/data-fetchers/baostock.d.ts +9 -2
- package/dist/data-fetchers/baostock.d.ts.map +1 -1
- package/dist/data-fetchers/baostock.js +78 -9
- package/dist/data-fetchers/baostock.js.map +1 -1
- package/dist/data-fetchers/dataBuffer.d.ts.map +1 -1
- package/dist/data-fetchers/dataBuffer.js +3 -0
- package/dist/data-fetchers/dataBuffer.js.map +1 -1
- package/dist/data-fetchers/fetcherDefinitionRegistry.d.ts +13 -0
- package/dist/data-fetchers/fetcherDefinitionRegistry.d.ts.map +1 -0
- package/dist/data-fetchers/fetcherDefinitionRegistry.js +36 -0
- package/dist/data-fetchers/fetcherDefinitionRegistry.js.map +1 -0
- package/dist/data-fetchers/gotdx.d.ts +10 -0
- package/dist/data-fetchers/gotdx.d.ts.map +1 -0
- package/dist/data-fetchers/gotdx.js +168 -0
- package/dist/data-fetchers/gotdx.js.map +1 -0
- package/dist/data-fetchers/hundred-mock.d.ts +9 -2
- package/dist/data-fetchers/hundred-mock.d.ts.map +1 -1
- package/dist/data-fetchers/hundred-mock.js +92 -7
- package/dist/data-fetchers/hundred-mock.js.map +1 -1
- package/dist/data-fetchers/index.d.ts +7 -4
- package/dist/data-fetchers/index.d.ts.map +1 -1
- package/dist/data-fetchers/index.js +6 -4
- package/dist/data-fetchers/index.js.map +1 -1
- package/dist/data-fetchers/router.d.ts.map +1 -1
- package/dist/data-fetchers/router.js +14 -15
- package/dist/data-fetchers/router.js.map +1 -1
- package/dist/data-fetchers/thousand-mock.d.ts +9 -2
- package/dist/data-fetchers/thousand-mock.d.ts.map +1 -1
- package/dist/data-fetchers/thousand-mock.js +88 -16
- package/dist/data-fetchers/thousand-mock.js.map +1 -1
- package/dist/data-fetchers/tradingview.d.ts +9 -2
- package/dist/data-fetchers/tradingview.d.ts.map +1 -1
- package/dist/data-fetchers/tradingview.js +75 -4
- package/dist/data-fetchers/tradingview.js.map +1 -1
- package/dist/data-fetchers/types.d.ts +21 -0
- package/dist/data-fetchers/types.d.ts.map +1 -0
- package/dist/data-fetchers/types.js +2 -0
- package/dist/data-fetchers/types.js.map +1 -0
- package/dist/engine/data/chartDataManager.d.ts +1 -0
- package/dist/engine/data/chartDataManager.d.ts.map +1 -1
- package/dist/engine/data/chartDataManager.js +3 -0
- package/dist/engine/data/chartDataManager.js.map +1 -1
- package/dist/engine/render/chartRenderer.d.ts.map +1 -1
- package/dist/engine/render/chartRenderer.js +2 -0
- package/dist/engine/render/chartRenderer.js.map +1 -1
- package/dist/engine/renderers/Indicator/ichimoku.d.ts.map +1 -1
- package/dist/engine/renderers/Indicator/ichimoku.js +8 -5
- package/dist/engine/renderers/Indicator/ichimoku.js.map +1 -1
- package/dist/engine/renderers/Indicator/mainIndicatorLegend.js +1 -1
- package/dist/engine/renderers/Indicator/mainIndicatorLegend.js.map +1 -1
- package/dist/engine/renderers/Indicator/sar.d.ts.map +1 -1
- package/dist/engine/renderers/Indicator/sar.js +3 -3
- package/dist/engine/renderers/Indicator/sar.js.map +1 -1
- package/dist/engine/renderers/Indicator/supertrend.d.ts.map +1 -1
- package/dist/engine/renderers/Indicator/supertrend.js +3 -3
- package/dist/engine/renderers/Indicator/supertrend.js.map +1 -1
- package/dist/engine/renderers/timeAxis.d.ts.map +1 -1
- package/dist/engine/renderers/timeAxis.js +1 -0
- package/dist/engine/renderers/timeAxis.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/mcp/chartBridge.d.ts +47 -0
- package/dist/mcp/chartBridge.d.ts.map +1 -0
- package/dist/mcp/chartBridge.js +167 -0
- package/dist/mcp/chartBridge.js.map +1 -0
- package/dist/mcp/index.d.ts +3 -0
- package/dist/mcp/index.d.ts.map +1 -0
- package/dist/mcp/index.js +2 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/mcp/types.d.ts +17 -0
- package/dist/mcp/types.d.ts.map +1 -0
- package/dist/mcp/types.js +2 -0
- package/dist/mcp/types.js.map +1 -0
- package/dist/plugin/types.d.ts +2 -0
- package/dist/plugin/types.d.ts.map +1 -1
- package/dist/plugin/types.js.map +1 -1
- package/dist/semantic/index.d.ts +1 -1
- package/dist/semantic/index.d.ts.map +1 -1
- package/dist/semantic/index.js.map +1 -1
- package/dist/semantic/schema.json +1 -1
- package/dist/semantic/types.d.ts +2 -1
- package/dist/semantic/types.d.ts.map +1 -1
- package/dist/utils/dateFormat.d.ts +25 -0
- package/dist/utils/dateFormat.d.ts.map +1 -1
- package/dist/utils/dateFormat.js +78 -0
- package/dist/utils/dateFormat.js.map +1 -1
- package/dist/utils/kLineDraw/axis.d.ts +2 -0
- package/dist/utils/kLineDraw/axis.d.ts.map +1 -1
- package/dist/utils/kLineDraw/axis.js +11 -6
- package/dist/utils/kLineDraw/axis.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/src/controllers/createChartController.ts +34 -0
- package/src/controllers/types.ts +9 -0
- package/src/data-fetchers/__tests__/dataBuffer.test.ts +5 -2
- package/src/data-fetchers/__tests__/fetcherRegistry.test.ts +192 -0
- package/src/data-fetchers/baostock.ts +54 -22
- package/src/data-fetchers/dataBuffer.ts +6 -0
- package/src/data-fetchers/fetcherDefinitionRegistry.ts +50 -0
- package/src/data-fetchers/gotdx.ts +160 -0
- package/src/data-fetchers/hundred-mock.ts +54 -7
- package/src/data-fetchers/index.ts +19 -4
- package/src/data-fetchers/router.ts +27 -15
- package/src/data-fetchers/thousand-mock.ts +49 -16
- package/src/data-fetchers/tradingview.ts +32 -6
- package/src/data-fetchers/types.ts +27 -0
- package/src/engine/data/chartDataManager.ts +4 -0
- package/src/engine/render/chartRenderer.ts +2 -0
- package/src/engine/renderers/Indicator/ichimoku.ts +10 -4
- package/src/engine/renderers/Indicator/mainIndicatorLegend.ts +1 -1
- package/src/engine/renderers/Indicator/sar.ts +3 -3
- package/src/engine/renderers/Indicator/supertrend.ts +3 -4
- package/src/engine/renderers/__tests__/boll.renderer.test.ts +1 -0
- package/src/engine/renderers/__tests__/ene.renderer.test.ts +1 -0
- package/src/engine/renderers/__tests__/expma.renderer.test.ts +1 -0
- package/src/engine/renderers/__tests__/ma.renderer.test.ts +1 -0
- package/src/engine/renderers/__tests__/mainIndicatorLegend.renderer.test.ts +1 -0
- package/src/engine/renderers/__tests__/yAxis.renderer.test.ts +1 -0
- package/src/engine/renderers/timeAxis.ts +1 -0
- package/src/index.ts +2 -0
- package/src/mcp/chartBridge.ts +220 -0
- package/src/mcp/index.ts +2 -0
- package/src/mcp/types.ts +19 -0
- package/src/plugin/types.ts +2 -0
- package/src/semantic/index.ts +1 -0
- package/src/semantic/schema.json +1 -1
- package/src/semantic/types.ts +3 -1
- package/src/utils/dateFormat.ts +85 -0
- package/src/utils/kLineDraw/axis.ts +13 -6
- 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 {
|
|
1
|
+
import type { KLineData } from '../controllers/types'
|
|
2
|
+
import { DataFetcher } from './fetcherDefinitionRegistry'
|
|
3
|
+
import type { FetchConfig } from './types'
|
|
2
4
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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 {
|
|
1
|
+
import type { KLineData } from '../controllers/types'
|
|
2
|
+
import { DataFetcher } from './fetcherDefinitionRegistry'
|
|
3
|
+
import type { FetchConfig } from './types'
|
|
2
4
|
|
|
3
|
-
|
|
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
|
-
|
|
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
|
|
14
|
-
|
|
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 {
|
|
2
|
-
export {
|
|
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 {
|
|
3
|
-
|
|
4
|
-
|
|
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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
}
|