@363045841yyt/klinechart-core 0.8.2 → 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/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 +9 -2
- package/dist/data-fetchers/gotdx.d.ts.map +1 -1
- package/dist/data-fetchers/gotdx.js +72 -5
- package/dist/data-fetchers/gotdx.js.map +1 -1
- 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 +66 -4
- package/dist/data-fetchers/hundred-mock.js.map +1 -1
- package/dist/data-fetchers/index.d.ts +7 -5
- package/dist/data-fetchers/index.d.ts.map +1 -1
- package/dist/data-fetchers/index.js +6 -5
- 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 -18
- 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 +66 -4
- 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 +73 -9
- 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/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/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/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -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/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__/fetcherRegistry.test.ts +192 -0
- package/src/data-fetchers/baostock.ts +54 -22
- package/src/data-fetchers/fetcherDefinitionRegistry.ts +50 -0
- package/src/data-fetchers/gotdx.ts +28 -6
- package/src/data-fetchers/hundred-mock.ts +21 -4
- package/src/data-fetchers/index.ts +19 -5
- package/src/data-fetchers/router.ts +27 -18
- package/src/data-fetchers/thousand-mock.ts +21 -4
- package/src/data-fetchers/tradingview.ts +30 -11
- package/src/data-fetchers/types.ts +27 -0
- package/src/engine/renderers/Indicator/ichimoku.ts +10 -4
- package/src/engine/renderers/Indicator/sar.ts +3 -3
- package/src/engine/renderers/Indicator/supertrend.ts +3 -4
- package/src/index.ts +1 -0
- package/src/mcp/chartBridge.ts +220 -0
- package/src/mcp/index.ts +2 -0
- package/src/mcp/types.ts +19 -0
- 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
|
|
@@ -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 {
|
|
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
|
-
|
|
95
|
-
|
|
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(`${
|
|
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(`${
|
|
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 {
|
|
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()
|
|
@@ -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 {
|
|
2
|
-
export {
|
|
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 {
|
|
3
|
-
|
|
4
|
-
|
|
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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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 {
|
|
1
|
+
import type { KLineData } from '../controllers/types'
|
|
2
|
+
import { DataFetcher } from './fetcherDefinitionRegistry'
|
|
3
|
+
import type { FetchConfig } from './types'
|
|
2
4
|
|
|
3
|
-
|
|
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 {
|
|
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
|
-
|
|
14
|
-
|
|
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 = `${
|
|
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
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { KLineData } from '../controllers/types'
|
|
2
|
+
|
|
3
|
+
export type FetchConfig = {
|
|
4
|
+
symbol: string
|
|
5
|
+
startDate: string
|
|
6
|
+
endDate: string
|
|
7
|
+
period: string
|
|
8
|
+
adjust: string
|
|
9
|
+
exchange?: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export type DataFetcherFn = (
|
|
13
|
+
source: string,
|
|
14
|
+
config: FetchConfig,
|
|
15
|
+
) => Promise<ReadonlyArray<KLineData>>
|
|
16
|
+
|
|
17
|
+
export interface DataFetcherDefinitionConfig {
|
|
18
|
+
name: string
|
|
19
|
+
displayName: string
|
|
20
|
+
description?: string
|
|
21
|
+
version?: string
|
|
22
|
+
capabilities?: string[]
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface DataFetcherDefinition extends DataFetcherDefinitionConfig {
|
|
26
|
+
fetcher: DataFetcherFn
|
|
27
|
+
}
|