@363045841yyt/klinechart-core 0.8.1-alpha.4 → 0.8.2
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 +21 -1
- package/dist/controllers/createChartController.js.map +1 -1
- package/dist/controllers/types.d.ts +6 -1
- package/dist/controllers/types.d.ts.map +1 -1
- package/dist/data-fetchers/baostock.js +3 -3
- package/dist/data-fetchers/baostock.js.map +1 -1
- package/dist/data-fetchers/dataBuffer.d.ts +5 -1
- package/dist/data-fetchers/dataBuffer.d.ts.map +1 -1
- package/dist/data-fetchers/dataBuffer.js +85 -48
- package/dist/data-fetchers/dataBuffer.js.map +1 -1
- package/dist/data-fetchers/gotdx.d.ts +3 -0
- package/dist/data-fetchers/gotdx.d.ts.map +1 -0
- package/dist/data-fetchers/gotdx.js +101 -0
- package/dist/data-fetchers/gotdx.js.map +1 -0
- package/dist/data-fetchers/hundred-mock.d.ts.map +1 -1
- package/dist/data-fetchers/hundred-mock.js +28 -5
- package/dist/data-fetchers/hundred-mock.js.map +1 -1
- package/dist/data-fetchers/index.d.ts +1 -0
- package/dist/data-fetchers/index.d.ts.map +1 -1
- package/dist/data-fetchers/index.js +1 -0
- 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 +3 -0
- package/dist/data-fetchers/router.js.map +1 -1
- package/dist/data-fetchers/thousand-mock.d.ts.map +1 -1
- package/dist/data-fetchers/thousand-mock.js +24 -14
- package/dist/data-fetchers/thousand-mock.js.map +1 -1
- package/dist/data-fetchers/tradingview.d.ts.map +1 -1
- package/dist/data-fetchers/tradingview.js +12 -6
- package/dist/data-fetchers/tradingview.js.map +1 -1
- package/dist/engine/chart.d.ts +29 -367
- package/dist/engine/chart.d.ts.map +1 -1
- package/dist/engine/chart.js +239 -1842
- package/dist/engine/chart.js.map +1 -1
- package/dist/engine/chartContext.d.ts +24 -0
- package/dist/engine/chartContext.d.ts.map +1 -0
- package/dist/engine/chartContext.js +19 -0
- package/dist/engine/chartContext.js.map +1 -0
- package/dist/engine/chartTypes.d.ts +77 -0
- package/dist/engine/chartTypes.d.ts.map +1 -0
- package/dist/engine/chartTypes.js +2 -0
- package/dist/engine/chartTypes.js.map +1 -0
- package/dist/engine/data/chartDataManager.d.ts +103 -0
- package/dist/engine/data/chartDataManager.d.ts.map +1 -0
- package/dist/engine/data/chartDataManager.js +593 -0
- package/dist/engine/data/chartDataManager.js.map +1 -0
- package/dist/engine/indicators/chartIndicatorManager.d.ts +102 -0
- package/dist/engine/indicators/chartIndicatorManager.d.ts.map +1 -0
- package/dist/engine/indicators/chartIndicatorManager.js +437 -0
- package/dist/engine/indicators/chartIndicatorManager.js.map +1 -0
- package/dist/engine/layout/chartPaneLayout.d.ts +53 -0
- package/dist/engine/layout/chartPaneLayout.d.ts.map +1 -0
- package/dist/engine/layout/chartPaneLayout.js +388 -0
- package/dist/engine/layout/chartPaneLayout.js.map +1 -0
- package/dist/engine/render/chartRenderer.d.ts +86 -0
- package/dist/engine/render/chartRenderer.d.ts.map +1 -0
- package/dist/engine/render/chartRenderer.js +440 -0
- package/dist/engine/render/chartRenderer.js.map +1 -0
- package/dist/engine/renderers/Indicator/mainIndicatorLegend.d.ts.map +1 -1
- package/dist/engine/renderers/Indicator/mainIndicatorLegend.js +73 -7
- package/dist/engine/renderers/Indicator/mainIndicatorLegend.js.map +1 -1
- package/dist/engine/renderers/comparisonLine.d.ts.map +1 -1
- package/dist/engine/renderers/comparisonLine.js +25 -11
- package/dist/engine/renderers/comparisonLine.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/engine/subPaneManager.d.ts +27 -6
- package/dist/engine/subPaneManager.d.ts.map +1 -1
- package/dist/engine/subPaneManager.js +54 -56
- package/dist/engine/subPaneManager.js.map +1 -1
- package/dist/engine/utils/chartZoomController.d.ts +33 -0
- package/dist/engine/utils/chartZoomController.d.ts.map +1 -0
- package/dist/engine/utils/chartZoomController.js +66 -0
- package/dist/engine/utils/chartZoomController.js.map +1 -0
- package/dist/engine/viewport/chartViewportManager.d.ts +72 -0
- package/dist/engine/viewport/chartViewportManager.d.ts.map +1 -0
- package/dist/engine/viewport/chartViewportManager.js +249 -0
- package/dist/engine/viewport/chartViewportManager.js.map +1 -0
- 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/plugin/types.d.ts +3 -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/tokens/theme-china.d.ts.map +1 -1
- package/dist/tokens/theme-china.js +0 -4
- package/dist/tokens/theme-china.js.map +1 -1
- package/dist/tokens/theme-dark.d.ts.map +1 -1
- package/dist/tokens/theme-dark.js +0 -4
- package/dist/tokens/theme-dark.js.map +1 -1
- package/dist/tokens/theme-light.d.ts.map +1 -1
- package/dist/tokens/theme-light.js +1 -5
- package/dist/tokens/theme-light.js.map +1 -1
- package/dist/tokens/types.d.ts +0 -4
- package/dist/tokens/types.d.ts.map +1 -1
- package/dist/types/price.d.ts +2 -0
- package/dist/types/price.d.ts.map +1 -1
- package/dist/types/price.js.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.d.ts.map +1 -1
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -1
- package/package.json +1 -1
- package/src/controllers/createChartController.ts +39 -10
- package/src/controllers/types.ts +6 -1
- package/src/data-fetchers/__tests__/dataBuffer.test.ts +5 -2
- package/src/data-fetchers/baostock.ts +3 -3
- package/src/data-fetchers/dataBuffer.ts +70 -23
- package/src/data-fetchers/gotdx.ts +138 -0
- package/src/data-fetchers/hundred-mock.ts +35 -5
- package/src/data-fetchers/index.ts +1 -0
- package/src/data-fetchers/router.ts +3 -0
- package/src/data-fetchers/thousand-mock.ts +30 -14
- package/src/data-fetchers/tradingview.ts +12 -6
- package/src/engine/__tests__/subPaneManager.test.ts +154 -0
- package/src/engine/chart.ts +252 -2250
- package/src/engine/chartContext.ts +34 -0
- package/src/engine/chartTypes.ts +88 -0
- package/src/engine/data/chartDataManager.ts +695 -0
- package/src/engine/indicators/__tests__/chartIndicatorManager.test.ts +103 -0
- package/src/engine/indicators/chartIndicatorManager.ts +566 -0
- package/src/engine/layout/chartPaneLayout.ts +474 -0
- package/src/engine/render/chartRenderer.ts +581 -0
- package/src/engine/renderers/Indicator/mainIndicatorLegend.ts +99 -13
- 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/comparisonLine.ts +25 -11
- package/src/engine/renderers/timeAxis.ts +1 -0
- package/src/engine/subPaneManager.ts +75 -59
- package/src/engine/utils/chartZoomController.ts +104 -0
- package/src/engine/viewport/chartViewportManager.ts +310 -0
- package/src/index.ts +1 -0
- package/src/plugin/types.ts +3 -0
- package/src/semantic/index.ts +1 -0
- package/src/semantic/schema.json +1 -1
- package/src/semantic/types.ts +3 -1
- package/src/tokens/__tests__/__snapshots__/baseline.test.ts.snap +1 -9
- package/src/tokens/theme-china.ts +0 -4
- package/src/tokens/theme-dark.ts +0 -4
- package/src/tokens/theme-light.ts +2 -6
- package/src/tokens/types.ts +0 -4
- package/src/types/price.ts +2 -0
- package/src/utils/dateFormat.ts +85 -0
- package/src/utils/kLineDraw/axis.ts +13 -6
- package/src/version.ts +1 -1
- package/src/engine/chart.d.ts +0 -626
|
@@ -9,6 +9,7 @@ export interface DataWindow {
|
|
|
9
9
|
const MS_PER_DAY = 86_400_000
|
|
10
10
|
const INITIAL_LOAD_DAYS = 365
|
|
11
11
|
const INCREMENTAL_LOAD_DAYS = 90
|
|
12
|
+
const FETCH_MAX_RETRIES = 2
|
|
12
13
|
|
|
13
14
|
function formatDate(ts: number): string {
|
|
14
15
|
const d = new Date(ts)
|
|
@@ -39,6 +40,7 @@ export class DataBuffer {
|
|
|
39
40
|
private _dataSignal: Signal<ReadonlyArray<KLineData>>
|
|
40
41
|
private _loadingSignal: Signal<boolean>
|
|
41
42
|
private _fetcher: DataFetcher | null = null
|
|
43
|
+
private _requestFetch: ((spec: SymbolSpec, startTs: number, endTs: number) => Promise<ReadonlyArray<KLineData>>) | null = null
|
|
42
44
|
private _currentSpec: SymbolSpec | null = null
|
|
43
45
|
private _loadedWindow: DataWindow | null = null
|
|
44
46
|
private _pendingFetch: Promise<void> | null = null
|
|
@@ -60,6 +62,10 @@ export class DataBuffer {
|
|
|
60
62
|
return this._loadingSignal
|
|
61
63
|
}
|
|
62
64
|
|
|
65
|
+
get currentSpec(): SymbolSpec | null {
|
|
66
|
+
return this._currentSpec
|
|
67
|
+
}
|
|
68
|
+
|
|
63
69
|
get loadedWindow(): DataWindow | null {
|
|
64
70
|
return this._loadedWindow
|
|
65
71
|
}
|
|
@@ -68,17 +74,25 @@ export class DataBuffer {
|
|
|
68
74
|
this._fetcher = fetcher
|
|
69
75
|
}
|
|
70
76
|
|
|
71
|
-
|
|
77
|
+
setRequestFetch(fn: ((spec: SymbolSpec, startTs: number, endTs: number) => Promise<ReadonlyArray<KLineData>>) | null): void {
|
|
78
|
+
this._requestFetch = fn
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
setSymbol(spec: SymbolSpec, initialStartTs?: number): void {
|
|
72
82
|
this._currentSpec = spec
|
|
73
83
|
this._data = []
|
|
74
84
|
this._loadedWindow = null
|
|
75
85
|
this._attemptedBoundaries.clear()
|
|
76
86
|
this._dataSignal.set([])
|
|
77
|
-
|
|
87
|
+
if (initialStartTs !== undefined) {
|
|
88
|
+
this.loadInitialRange(initialStartTs, Date.now())
|
|
89
|
+
} else {
|
|
90
|
+
this.loadInitial()
|
|
91
|
+
}
|
|
78
92
|
}
|
|
79
93
|
|
|
80
94
|
ensureRange(requestStartTs: number, _requestEndTs: number): void {
|
|
81
|
-
if (this._disposed || !this._fetcher || !this._currentSpec) return
|
|
95
|
+
if (this._disposed || (!this._requestFetch && !this._fetcher) || !this._currentSpec) return
|
|
82
96
|
if (!this._loadedWindow) return
|
|
83
97
|
|
|
84
98
|
if (requestStartTs >= this._loadedWindow.earliestTs) return
|
|
@@ -95,7 +109,7 @@ export class DataBuffer {
|
|
|
95
109
|
}
|
|
96
110
|
|
|
97
111
|
private loadInitial(): void {
|
|
98
|
-
if (!this._fetcher || !this._currentSpec || this._disposed) return
|
|
112
|
+
if ((!this._requestFetch && !this._fetcher) || !this._currentSpec || this._disposed) return
|
|
99
113
|
|
|
100
114
|
const now = Date.now()
|
|
101
115
|
const startDate = now - INITIAL_LOAD_DAYS * MS_PER_DAY
|
|
@@ -104,13 +118,18 @@ export class DataBuffer {
|
|
|
104
118
|
this.fetchRange(startDate, endDate)
|
|
105
119
|
}
|
|
106
120
|
|
|
107
|
-
private
|
|
108
|
-
if (!this._fetcher || !this._currentSpec || this._disposed) return
|
|
121
|
+
private loadInitialRange(startTs: number, endTs: number): void {
|
|
122
|
+
if ((!this._requestFetch && !this._fetcher) || !this._currentSpec || this._disposed) return
|
|
123
|
+
this.fetchRange(startTs, endTs)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
private fetchRange(startTs: number, endTs: number, retryCount = 0): void {
|
|
127
|
+
if ((!this._requestFetch && !this._fetcher) || !this._currentSpec || this._disposed) return
|
|
109
128
|
|
|
110
129
|
if (this._pendingFetch) {
|
|
111
130
|
this._pendingFetch = this._pendingFetch.then(() => {
|
|
112
131
|
if (this._disposed) return
|
|
113
|
-
this.fetchRange(startTs, endTs)
|
|
132
|
+
return this.fetchRange(startTs, endTs, retryCount)
|
|
114
133
|
})
|
|
115
134
|
return
|
|
116
135
|
}
|
|
@@ -120,17 +139,26 @@ export class DataBuffer {
|
|
|
120
139
|
|
|
121
140
|
this._loadingSignal.set(true)
|
|
122
141
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
142
|
+
const doFetch = (): Promise<void> => {
|
|
143
|
+
const fetchPromise = this._requestFetch
|
|
144
|
+
? this._requestFetch(spec, startTs, endTs)
|
|
145
|
+
: (fetcher as NonNullable<DataFetcher>)(spec.source ?? 'baostock', {
|
|
146
|
+
symbol: spec.symbol,
|
|
147
|
+
startDate: formatDate(startTs),
|
|
148
|
+
endDate: formatDate(endTs),
|
|
149
|
+
period: spec.period ?? 'daily',
|
|
150
|
+
adjust: spec.adjust ?? 'none',
|
|
151
|
+
exchange: spec.exchange,
|
|
152
|
+
})
|
|
153
|
+
return fetchPromise.then((incoming) => {
|
|
132
154
|
if (this._disposed) return
|
|
133
155
|
|
|
156
|
+
if (incoming.length === 0) {
|
|
157
|
+
throw new Error(
|
|
158
|
+
`[DataBuffer] empty data for ${spec.symbol} ${formatDate(startTs)}~${formatDate(endTs)}`,
|
|
159
|
+
)
|
|
160
|
+
}
|
|
161
|
+
|
|
134
162
|
const oldLength = this._data.length
|
|
135
163
|
const oldEarliestTs = oldLength > 0 ? this._data[0]!.timestamp : null
|
|
136
164
|
const merged = mergeSortedData(this._data, [...incoming])
|
|
@@ -161,16 +189,35 @@ export class DataBuffer {
|
|
|
161
189
|
}
|
|
162
190
|
}
|
|
163
191
|
})
|
|
164
|
-
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const attempt = (count: number): Promise<void> => {
|
|
195
|
+
return doFetch().catch((err) => {
|
|
165
196
|
if (this._disposed) return
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
197
|
+
|
|
198
|
+
if (count < FETCH_MAX_RETRIES) {
|
|
199
|
+
const delay = Math.pow(2, count) * 1000
|
|
200
|
+
console.warn(
|
|
201
|
+
`[DataBuffer] fetch failed, retry ${count + 1}/${FETCH_MAX_RETRIES} in ${delay}ms:`,
|
|
202
|
+
err,
|
|
203
|
+
)
|
|
204
|
+
return new Promise<void>((resolve) => setTimeout(resolve, delay)).then(() => {
|
|
205
|
+
if (this._disposed) return
|
|
206
|
+
return attempt(count + 1)
|
|
207
|
+
})
|
|
172
208
|
}
|
|
209
|
+
|
|
210
|
+
console.error(`[DataBuffer] fetch failed after ${FETCH_MAX_RETRIES + 1} attempts:`, err)
|
|
211
|
+
this._attemptedBoundaries.delete(endTs)
|
|
173
212
|
})
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
this._pendingFetch = attempt(retryCount).finally(() => {
|
|
216
|
+
this._pendingFetch = null
|
|
217
|
+
if (!this._disposed) {
|
|
218
|
+
this._loadingSignal.set(false)
|
|
219
|
+
}
|
|
220
|
+
})
|
|
174
221
|
}
|
|
175
222
|
|
|
176
223
|
dispose(): void {
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import type { DataFetcher, KLineData } from '../controllers/types'
|
|
2
|
+
|
|
3
|
+
const PERIOD_TO_CATEGORY: Record<string, number> = {
|
|
4
|
+
'1min': 8,
|
|
5
|
+
'5min': 0,
|
|
6
|
+
'15min': 1,
|
|
7
|
+
'30min': 2,
|
|
8
|
+
'60min': 3,
|
|
9
|
+
daily: 4,
|
|
10
|
+
weekly: 5,
|
|
11
|
+
monthly: 6,
|
|
12
|
+
quarterly: 10,
|
|
13
|
+
yearly: 11,
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const ADJUST_MAP: Record<string, number> = {
|
|
17
|
+
none: 0,
|
|
18
|
+
qfq: 1,
|
|
19
|
+
hfq: 2,
|
|
20
|
+
splits: 0,
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const EXCHANGE_EX_CATEGORY: Record<string, number> = {
|
|
24
|
+
US: 74,
|
|
25
|
+
HK: 71,
|
|
26
|
+
SG: 78,
|
|
27
|
+
DE: 73,
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface SecurityBar {
|
|
31
|
+
Last: number
|
|
32
|
+
Open: number
|
|
33
|
+
Close: number
|
|
34
|
+
High: number
|
|
35
|
+
Low: number
|
|
36
|
+
Vol: number
|
|
37
|
+
Amount: number
|
|
38
|
+
Turnover: number
|
|
39
|
+
RisePrice: number
|
|
40
|
+
RiseRate: number
|
|
41
|
+
Year: number
|
|
42
|
+
Month: number
|
|
43
|
+
Day: number
|
|
44
|
+
Hour: number
|
|
45
|
+
Minute: number
|
|
46
|
+
DateTime: string
|
|
47
|
+
UpCount: number
|
|
48
|
+
DownCount: number
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
interface ExKLineItem {
|
|
52
|
+
DateTime: string
|
|
53
|
+
Open: number
|
|
54
|
+
High: number
|
|
55
|
+
Low: number
|
|
56
|
+
Close: number
|
|
57
|
+
Amount: number
|
|
58
|
+
Vol: number
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function mapBar(item: SecurityBar, code: string): KLineData {
|
|
62
|
+
const ts = new Date(item.DateTime).getTime()
|
|
63
|
+
return {
|
|
64
|
+
timestamp: ts,
|
|
65
|
+
date: item.DateTime.split('T')[0],
|
|
66
|
+
open: item.Open,
|
|
67
|
+
high: item.High,
|
|
68
|
+
low: item.Low,
|
|
69
|
+
close: item.Close,
|
|
70
|
+
volume: item.Vol,
|
|
71
|
+
turnover: item.Amount,
|
|
72
|
+
turnoverRate: item.Turnover,
|
|
73
|
+
changeAmount: item.RisePrice,
|
|
74
|
+
changePercent: item.RiseRate,
|
|
75
|
+
stockCode: code,
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function mapExItem(item: ExKLineItem, code: string): KLineData {
|
|
80
|
+
const ts = new Date(item.DateTime).getTime()
|
|
81
|
+
return {
|
|
82
|
+
timestamp: ts,
|
|
83
|
+
date: item.DateTime.split('T')[0],
|
|
84
|
+
open: item.Open,
|
|
85
|
+
high: item.High,
|
|
86
|
+
low: item.Low,
|
|
87
|
+
close: item.Close,
|
|
88
|
+
volume: item.Vol,
|
|
89
|
+
turnover: item.Amount,
|
|
90
|
+
stockCode: code,
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export const gotdxDataFetcher: DataFetcher = async (source, config) => {
|
|
95
|
+
const baseUrl = source === 'gotdx' ? 'http://127.0.0.1:8080' : ''
|
|
96
|
+
|
|
97
|
+
if (config.exchange && config.exchange in EXCHANGE_EX_CATEGORY) {
|
|
98
|
+
const category = EXCHANGE_EX_CATEGORY[config.exchange]
|
|
99
|
+
const period = PERIOD_TO_CATEGORY[config.period] ?? 4
|
|
100
|
+
const body = {
|
|
101
|
+
category,
|
|
102
|
+
code: config.symbol,
|
|
103
|
+
period,
|
|
104
|
+
start_date: config.startDate,
|
|
105
|
+
end_date: config.endDate,
|
|
106
|
+
times: 1,
|
|
107
|
+
}
|
|
108
|
+
const res = await fetch(`${baseUrl}/api/ex/kline-by-date`, {
|
|
109
|
+
method: 'POST',
|
|
110
|
+
headers: { 'Content-Type': 'application/json' },
|
|
111
|
+
body: JSON.stringify(body),
|
|
112
|
+
})
|
|
113
|
+
if (!res.ok) throw new Error(`[gotdx] ex/kline-by-date failed: ${res.status} ${res.statusText}`)
|
|
114
|
+
const list: ExKLineItem[] = await res.json()
|
|
115
|
+
return list.map((item) => mapExItem(item, config.symbol))
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const market = config.symbol.startsWith('6') || config.symbol.startsWith('9') ? 1 : 0
|
|
119
|
+
const category = PERIOD_TO_CATEGORY[config.period] ?? 4
|
|
120
|
+
const adjust = ADJUST_MAP[config.adjust] ?? 0
|
|
121
|
+
const body = {
|
|
122
|
+
market,
|
|
123
|
+
code: config.symbol,
|
|
124
|
+
category,
|
|
125
|
+
start_date: config.startDate,
|
|
126
|
+
end_date: config.endDate,
|
|
127
|
+
times: 1,
|
|
128
|
+
adjust,
|
|
129
|
+
}
|
|
130
|
+
const res = await fetch(`${baseUrl}/api/stock/kline-by-date`, {
|
|
131
|
+
method: 'POST',
|
|
132
|
+
headers: { 'Content-Type': 'application/json' },
|
|
133
|
+
body: JSON.stringify(body),
|
|
134
|
+
})
|
|
135
|
+
if (!res.ok) throw new Error(`[gotdx] stock/kline-by-date failed: ${res.status} ${res.statusText}`)
|
|
136
|
+
const list: SecurityBar[] = await res.json()
|
|
137
|
+
return list.map((item) => mapBar(item, config.symbol))
|
|
138
|
+
}
|
|
@@ -6,13 +6,43 @@ export const hundredMockDataFetcher: DataFetcher = async (_source, config) => {
|
|
|
6
6
|
const end = new Date(config.endDate).getTime()
|
|
7
7
|
const dayMs = 86400000
|
|
8
8
|
const totalDays = Math.floor((end - start) / dayMs) + 1
|
|
9
|
+
if (totalDays <= 0) return []
|
|
10
|
+
|
|
11
|
+
const basePrice = 12.5
|
|
9
12
|
const data: KLineData[] = []
|
|
10
|
-
|
|
13
|
+
|
|
14
|
+
if (totalDays === 1) {
|
|
15
|
+
data.push({
|
|
16
|
+
timestamp: start,
|
|
17
|
+
open: basePrice,
|
|
18
|
+
high: basePrice,
|
|
19
|
+
low: basePrice,
|
|
20
|
+
close: basePrice,
|
|
21
|
+
volume: Math.round(Math.random() * 10000000 + 1000000),
|
|
22
|
+
})
|
|
23
|
+
return data
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const meanReversionStrength = 0.005
|
|
27
|
+
|
|
28
|
+
// raw random walk with mean reversion (close prices before bridge)
|
|
29
|
+
const rawWalk: number[] = [basePrice]
|
|
30
|
+
for (let i = 1; i < totalDays; i++) {
|
|
31
|
+
const prev = rawWalk[i - 1]!
|
|
32
|
+
const reversion = meanReversionStrength * (basePrice - prev)
|
|
33
|
+
const change = (Math.random() - 0.48) * prev * 0.06 + reversion
|
|
34
|
+
rawWalk.push(prev + change)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Brownian bridge: subtract linear drift so last close = basePrice
|
|
38
|
+
const finalOffset = rawWalk[totalDays - 1]! - basePrice
|
|
11
39
|
for (let i = 0; i < totalDays; i++) {
|
|
40
|
+
const bridge = finalOffset * (i / (totalDays - 1))
|
|
41
|
+
const close = Math.round((rawWalk[i]! - bridge) * 100) / 100
|
|
42
|
+
|
|
12
43
|
const ts = start + i * dayMs
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
const close = Math.round((open + change) * 100) / 100
|
|
44
|
+
const open = i === 0 ? basePrice : data[i - 1]!.close
|
|
45
|
+
|
|
16
46
|
const high = Math.round(Math.max(open, close) * (1 + Math.random() * 0.03) * 100) / 100
|
|
17
47
|
const low = Math.round(Math.min(open, close) * (1 - Math.random() * 0.03) * 100) / 100
|
|
18
48
|
const volume = Math.round(Math.random() * 10000000 + 1000000)
|
|
@@ -25,7 +55,7 @@ export const hundredMockDataFetcher: DataFetcher = async (_source, config) => {
|
|
|
25
55
|
volume,
|
|
26
56
|
turnover: Math.round((volume * (open + close)) / 2),
|
|
27
57
|
})
|
|
28
|
-
price = close
|
|
29
58
|
}
|
|
59
|
+
|
|
30
60
|
return data
|
|
31
61
|
}
|
|
@@ -2,6 +2,7 @@ export { thousandMockDataFetcher } from './thousand-mock'
|
|
|
2
2
|
export { hundredMockDataFetcher } from './hundred-mock'
|
|
3
3
|
export { baostockDataFetcher } from './baostock'
|
|
4
4
|
export { tradingviewDataFetcher } from './tradingview'
|
|
5
|
+
export { gotdxDataFetcher } from './gotdx'
|
|
5
6
|
export { routerDataFetcher } from './router'
|
|
6
7
|
export { DataBuffer } from './dataBuffer'
|
|
7
8
|
export type { DataWindow } from './dataBuffer'
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { DataFetcher } from '../controllers/types'
|
|
2
2
|
import { baostockDataFetcher } from './baostock'
|
|
3
|
+
import { gotdxDataFetcher } from './gotdx'
|
|
3
4
|
import { hundredMockDataFetcher } from './hundred-mock'
|
|
4
5
|
import { thousandMockDataFetcher } from './thousand-mock'
|
|
5
6
|
import { tradingviewDataFetcher } from './tradingview'
|
|
@@ -8,6 +9,8 @@ export const routerDataFetcher: DataFetcher = (source, config) => {
|
|
|
8
9
|
switch (source) {
|
|
9
10
|
case 'baostock':
|
|
10
11
|
return baostockDataFetcher(source, config)
|
|
12
|
+
case 'gotdx':
|
|
13
|
+
return gotdxDataFetcher(source, config)
|
|
11
14
|
case 'tradingview':
|
|
12
15
|
return tradingviewDataFetcher(source, config)
|
|
13
16
|
case 'mock-100':
|
|
@@ -5,26 +5,42 @@ export const thousandMockDataFetcher: DataFetcher = async (_source, _config) =>
|
|
|
5
5
|
const data: KLineData[] = []
|
|
6
6
|
const startTime = new Date('2020-01-01').getTime()
|
|
7
7
|
const dayMs = 24 * 60 * 60 * 1000
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
const totalDays = 10000
|
|
9
|
+
|
|
10
|
+
const basePrice = 3000
|
|
11
|
+
const meanReversionStrength = 0.0005
|
|
12
|
+
const volatility = 0.02
|
|
13
|
+
|
|
14
|
+
// raw random walk with mean reversion (close prices before bridge)
|
|
15
|
+
const rawWalk: number[] = [basePrice]
|
|
16
|
+
for (let i = 1; i < totalDays; i++) {
|
|
17
|
+
const prev = rawWalk[i - 1]!
|
|
18
|
+
const reversion = meanReversionStrength * (basePrice - prev)
|
|
19
|
+
const change = (Math.random() - 0.5) * 2 * volatility * prev + reversion
|
|
20
|
+
rawWalk.push(prev + change)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Brownian bridge: subtract linear drift so last close = basePrice
|
|
24
|
+
const finalOffset = rawWalk[totalDays - 1]! - basePrice
|
|
25
|
+
for (let i = 0; i < totalDays; i++) {
|
|
26
|
+
const bridge = finalOffset * (i / (totalDays - 1))
|
|
27
|
+
const close = Math.round((rawWalk[i]! - bridge) * 100) / 100
|
|
28
|
+
|
|
10
29
|
const timestamp = startTime + i * dayMs
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
const
|
|
14
|
-
const
|
|
15
|
-
const close = open * (1 + change)
|
|
16
|
-
const high = Math.max(open, close) * (1 + Math.random() * 0.01)
|
|
17
|
-
const low = Math.min(open, close) * (1 - Math.random() * 0.01)
|
|
30
|
+
const open = i === 0 ? basePrice : data[i - 1]!.close
|
|
31
|
+
|
|
32
|
+
const high = Math.round(Math.max(open, close) * (1 + Math.random() * 0.01) * 100) / 100
|
|
33
|
+
const low = Math.round(Math.min(open, close) * (1 - Math.random() * 0.01) * 100) / 100
|
|
18
34
|
const volume = Math.floor(1000000 + Math.random() * 5000000)
|
|
19
35
|
data.push({
|
|
20
36
|
timestamp,
|
|
21
|
-
open
|
|
22
|
-
high
|
|
23
|
-
low
|
|
24
|
-
close
|
|
37
|
+
open,
|
|
38
|
+
high,
|
|
39
|
+
low,
|
|
40
|
+
close,
|
|
25
41
|
volume,
|
|
26
42
|
})
|
|
27
|
-
lastClose = close
|
|
28
43
|
}
|
|
44
|
+
|
|
29
45
|
return data
|
|
30
46
|
}
|
|
@@ -16,18 +16,23 @@ export const tradingviewDataFetcher: DataFetcher = async (source, config) => {
|
|
|
16
16
|
const startDate = config.startDate.split('T')[0]
|
|
17
17
|
const endDate = config.endDate.split('T')[0]
|
|
18
18
|
|
|
19
|
+
const ADJUST_TO_TV: Record<string, string | undefined> = {
|
|
20
|
+
qfq: 'dividends',
|
|
21
|
+
splits: 'splits',
|
|
22
|
+
none: 'none',
|
|
23
|
+
}
|
|
24
|
+
const tvAdjust = ADJUST_TO_TV[config.adjust]
|
|
19
25
|
const exchangeQ = config.exchange ? `&exchange=${config.exchange}` : ''
|
|
20
|
-
const
|
|
26
|
+
const adjustQ = tvAdjust ? `&adjust=${tvAdjust}` : ''
|
|
27
|
+
const url = `${baseUrl}/api/tradingview/kdata?symbol=${config.symbol}&timeframe=${timeframe}&start_date=${startDate}&end_date=${endDate}${exchangeQ}${adjustQ}`
|
|
21
28
|
try {
|
|
22
29
|
const res = await fetch(url)
|
|
23
30
|
if (!res.ok) {
|
|
24
|
-
|
|
25
|
-
return []
|
|
31
|
+
throw new Error(`[tradingview] fetch failed: ${res.status} ${res.statusText}`)
|
|
26
32
|
}
|
|
27
33
|
const json = await res.json()
|
|
28
34
|
if (!json.success) {
|
|
29
|
-
|
|
30
|
-
return []
|
|
35
|
+
throw new Error(`[tradingview] API error: ${json.error_msg}`)
|
|
31
36
|
}
|
|
32
37
|
if (json.warning) {
|
|
33
38
|
console.warn(`[tradingview] ${json.warning}`)
|
|
@@ -35,6 +40,7 @@ export const tradingviewDataFetcher: DataFetcher = async (source, config) => {
|
|
|
35
40
|
|
|
36
41
|
return (json.data ?? []).map((item: Record<string, unknown>) => ({
|
|
37
42
|
timestamp: item.ts_open as number,
|
|
43
|
+
date: item.date as string,
|
|
38
44
|
open: item.open as number,
|
|
39
45
|
high: item.high as number,
|
|
40
46
|
low: item.low as number,
|
|
@@ -44,6 +50,6 @@ export const tradingviewDataFetcher: DataFetcher = async (source, config) => {
|
|
|
44
50
|
})) as KLineData[]
|
|
45
51
|
} catch (err) {
|
|
46
52
|
console.warn('[tradingview] network error:', err)
|
|
47
|
-
|
|
53
|
+
throw err
|
|
48
54
|
}
|
|
49
55
|
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
2
|
+
import { SubPaneManager, type SubPaneContext } from '../subPaneManager'
|
|
3
|
+
import type { SubIndicatorType } from '../renderers/Indicator'
|
|
4
|
+
import type { IndicatorScheduler } from '../indicators/scheduler'
|
|
5
|
+
|
|
6
|
+
function createMockScheduler(): Partial<IndicatorScheduler> {
|
|
7
|
+
return {
|
|
8
|
+
getIndicatorMetadata: vi.fn((_id: string) => ({
|
|
9
|
+
rendererFactory: vi.fn(() => ({ name: 'custom_rsi_rsi_0' })),
|
|
10
|
+
updateConfig: vi.fn(),
|
|
11
|
+
scale: { indicatorKey: 'test', label: 'Test', decimals: 2 },
|
|
12
|
+
})),
|
|
13
|
+
onSubPaneChanged: vi.fn(),
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function createMockContext(): SubPaneContext {
|
|
18
|
+
const scheduler = createMockScheduler()
|
|
19
|
+
return {
|
|
20
|
+
getIndicatorScheduler: vi.fn(() => scheduler as unknown as IndicatorScheduler),
|
|
21
|
+
hasPane: vi.fn(() => false),
|
|
22
|
+
upsertPane: vi.fn(),
|
|
23
|
+
getRenderer: vi.fn(),
|
|
24
|
+
useRenderer: vi.fn(),
|
|
25
|
+
removeRenderer: vi.fn(),
|
|
26
|
+
removePaneDefinition: vi.fn(),
|
|
27
|
+
updateRendererConfig: vi.fn(),
|
|
28
|
+
getRightAxisWidth: vi.fn(() => 60),
|
|
29
|
+
getPriceLabelWidth: vi.fn(() => 60),
|
|
30
|
+
getYPaddingPx: vi.fn(() => 4),
|
|
31
|
+
getCrosshairPos: vi.fn(() => null),
|
|
32
|
+
getCrosshairPrice: vi.fn(() => null),
|
|
33
|
+
getActivePaneId: vi.fn(() => null),
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
describe('SubPaneManager', () => {
|
|
38
|
+
let manager: SubPaneManager
|
|
39
|
+
let ctx: SubPaneContext
|
|
40
|
+
|
|
41
|
+
beforeEach(() => {
|
|
42
|
+
manager = new SubPaneManager()
|
|
43
|
+
ctx = createMockContext()
|
|
44
|
+
vi.clearAllMocks()
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
describe('updateParams', () => {
|
|
48
|
+
it('should update paneTitle renderer config with new params and indicatorId', () => {
|
|
49
|
+
manager.create(ctx, 'RSI_0', 'RSI' as SubIndicatorType, {
|
|
50
|
+
period1: 6,
|
|
51
|
+
period2: 12,
|
|
52
|
+
period3: 24,
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
const entry = manager.getByPaneId('RSI_0')
|
|
56
|
+
expect(entry).toBeDefined()
|
|
57
|
+
vi.clearAllMocks()
|
|
58
|
+
|
|
59
|
+
const newParams = { period1: 10, period2: 20, period3: 30 }
|
|
60
|
+
manager.updateParams(ctx, 'RSI_0', newParams)
|
|
61
|
+
|
|
62
|
+
expect(ctx.updateRendererConfig).toHaveBeenCalledWith(
|
|
63
|
+
entry!.paneTitleRendererName,
|
|
64
|
+
{ params: newParams, indicatorId: 'RSI' },
|
|
65
|
+
)
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it('should update main indicator renderer config with new params', () => {
|
|
69
|
+
manager.create(ctx, 'RSI_0', 'RSI' as SubIndicatorType, {
|
|
70
|
+
period1: 6,
|
|
71
|
+
period2: 12,
|
|
72
|
+
period3: 24,
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
const entry = manager.getByPaneId('RSI_0')
|
|
76
|
+
expect(entry).toBeDefined()
|
|
77
|
+
vi.clearAllMocks()
|
|
78
|
+
|
|
79
|
+
const newParams = { period1: 10, period2: 20, period3: 30 }
|
|
80
|
+
manager.updateParams(ctx, 'RSI_0', newParams)
|
|
81
|
+
|
|
82
|
+
expect(ctx.updateRendererConfig).toHaveBeenCalledWith(
|
|
83
|
+
entry!.rendererName,
|
|
84
|
+
newParams,
|
|
85
|
+
)
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it('should update scheduler config via definition.updateConfig', () => {
|
|
89
|
+
const updateConfigSpy = vi.fn()
|
|
90
|
+
const customScheduler: Partial<IndicatorScheduler> = {
|
|
91
|
+
getIndicatorMetadata: vi.fn(() => ({
|
|
92
|
+
rendererFactory: vi.fn(() => ({ name: 'custom_rsi_rsi_0' })),
|
|
93
|
+
updateConfig: updateConfigSpy,
|
|
94
|
+
scale: { indicatorKey: 'test', label: 'Test', decimals: 2 },
|
|
95
|
+
})),
|
|
96
|
+
onSubPaneChanged: vi.fn(),
|
|
97
|
+
}
|
|
98
|
+
const customCtx: SubPaneContext = {
|
|
99
|
+
...ctx,
|
|
100
|
+
getIndicatorScheduler: vi.fn(() => customScheduler as unknown as IndicatorScheduler),
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
manager.create(customCtx, 'RSI_0', 'RSI' as SubIndicatorType, {
|
|
104
|
+
period1: 6,
|
|
105
|
+
period2: 12,
|
|
106
|
+
period3: 24,
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
const newParams = { period1: 10, period2: 20, period3: 30 }
|
|
110
|
+
manager.updateParams(customCtx, 'RSI_0', newParams)
|
|
111
|
+
|
|
112
|
+
expect(updateConfigSpy).toHaveBeenCalled()
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
it('should update entry params in the manager', () => {
|
|
116
|
+
manager.create(ctx, 'RSI_0', 'RSI' as SubIndicatorType, {
|
|
117
|
+
period1: 6,
|
|
118
|
+
period2: 12,
|
|
119
|
+
period3: 24,
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
const newParams = { period1: 10, period2: 20, period3: 30 }
|
|
123
|
+
manager.updateParams(ctx, 'RSI_0', newParams)
|
|
124
|
+
|
|
125
|
+
const entry = manager.getByPaneId('RSI_0')
|
|
126
|
+
expect(entry?.params).toEqual(newParams)
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
it('should fire entries signal on update', () => {
|
|
130
|
+
manager.create(ctx, 'RSI_0', 'RSI' as SubIndicatorType, {
|
|
131
|
+
period1: 6,
|
|
132
|
+
period2: 12,
|
|
133
|
+
period3: 24,
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
const listener = vi.fn()
|
|
137
|
+
manager.entriesSignal.subscribe(listener)
|
|
138
|
+
vi.clearAllMocks()
|
|
139
|
+
|
|
140
|
+
const newParams = { period1: 10, period2: 20, period3: 30 }
|
|
141
|
+
manager.updateParams(ctx, 'RSI_0', newParams)
|
|
142
|
+
|
|
143
|
+
expect(listener).toHaveBeenCalledTimes(1)
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
it('should silently skip when paneId does not exist', () => {
|
|
147
|
+
const newParams = { period1: 10, period2: 20, period3: 30 }
|
|
148
|
+
manager.updateParams(ctx, 'NONEXISTENT', newParams)
|
|
149
|
+
|
|
150
|
+
expect(ctx.updateRendererConfig).not.toHaveBeenCalled()
|
|
151
|
+
expect(ctx.getIndicatorScheduler().onSubPaneChanged).not.toHaveBeenCalled()
|
|
152
|
+
})
|
|
153
|
+
})
|
|
154
|
+
})
|