@363045841yyt/klinechart-core 0.8.4 → 0.8.6
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/config/chartSettings.d.ts +1 -1
- package/dist/config/chartSettings.d.ts.map +1 -1
- package/dist/config/chartSettings.js +8 -4
- package/dist/config/chartSettings.js.map +1 -1
- package/dist/controllers/createChartController.d.ts.map +1 -1
- package/dist/controllers/createChartController.js +11 -1
- package/dist/controllers/createChartController.js.map +1 -1
- package/dist/controllers/types.d.ts +2 -0
- package/dist/controllers/types.d.ts.map +1 -1
- package/dist/data-fetchers/dataBuffer.d.ts.map +1 -1
- package/dist/data-fetchers/dataBuffer.js +1 -4
- package/dist/data-fetchers/dataBuffer.js.map +1 -1
- package/dist/data-fetchers/gotdx.d.ts.map +1 -1
- package/dist/data-fetchers/gotdx.js +1 -0
- package/dist/data-fetchers/gotdx.js.map +1 -1
- package/dist/engine/controller/interaction.d.ts +6 -0
- package/dist/engine/controller/interaction.d.ts.map +1 -1
- package/dist/engine/controller/interaction.js +51 -8
- package/dist/engine/controller/interaction.js.map +1 -1
- package/dist/engine/data/chartDataManager.js +1 -1
- package/dist/engine/data/chartDataManager.js.map +1 -1
- package/dist/engine/indicators/calculators.d.ts.map +1 -1
- package/dist/engine/indicators/calculators.js +20 -3
- package/dist/engine/indicators/calculators.js.map +1 -1
- package/dist/engine/indicators/ichimokuState.d.ts +2 -0
- package/dist/engine/indicators/ichimokuState.d.ts.map +1 -1
- package/dist/engine/indicators/ichimokuState.js.map +1 -1
- package/dist/engine/indicators/visibleStateComposers.d.ts +14 -0
- package/dist/engine/indicators/visibleStateComposers.d.ts.map +1 -1
- package/dist/engine/indicators/visibleStateComposers.js +34 -0
- package/dist/engine/indicators/visibleStateComposers.js.map +1 -1
- package/dist/engine/renderers/Indicator/ichimoku.d.ts.map +1 -1
- package/dist/engine/renderers/Indicator/ichimoku.js +25 -3
- package/dist/engine/renderers/Indicator/ichimoku.js.map +1 -1
- package/dist/engine/renderers/Indicator/mainIndicatorLegend.d.ts.map +1 -1
- package/dist/engine/renderers/Indicator/mainIndicatorLegend.js +92 -0
- package/dist/engine/renderers/Indicator/mainIndicatorLegend.js.map +1 -1
- package/dist/engine/renderers/crosshair.js +1 -1
- package/dist/engine/renderers/crosshair.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 +3 -0
- package/dist/mcp/chartBridge.d.ts.map +1 -1
- package/dist/mcp/chartBridge.js +15 -2
- package/dist/mcp/chartBridge.js.map +1 -1
- package/dist/tokens/theme-dark.js +1 -1
- package/dist/utils/kLineDraw/axis.js +1 -1
- package/dist/utils/kLineDraw/axis.js.map +1 -1
- package/dist/utils/uuid.d.ts +2 -0
- package/dist/utils/uuid.d.ts.map +1 -0
- package/dist/utils/uuid.js +10 -0
- package/dist/utils/uuid.js.map +1 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +3 -2
- package/src/config/chartSettings.ts +14 -10
- package/src/controllers/createChartController.ts +10 -1
- package/src/controllers/types.ts +2 -0
- package/src/data-fetchers/__tests__/dataBuffer.test.ts +1 -1
- package/src/data-fetchers/dataBuffer.ts +1 -4
- package/src/data-fetchers/gotdx.ts +2 -0
- package/src/engine/controller/interaction.ts +56 -9
- package/src/engine/data/chartDataManager.ts +1 -1
- package/src/engine/indicators/__tests__/ichimoku.test.ts +3 -3
- package/src/engine/indicators/calculators.ts +22 -3
- package/src/engine/indicators/ichimokuState.ts +2 -0
- package/src/engine/indicators/visibleStateComposers.ts +51 -0
- package/src/engine/renderers/Indicator/ichimoku.ts +23 -3
- package/src/engine/renderers/Indicator/mainIndicatorLegend.ts +102 -0
- package/src/engine/renderers/crosshair.ts +1 -1
- package/src/index.ts +1 -0
- package/src/mcp/chartBridge.ts +20 -2
- package/src/tokens/__tests__/__snapshots__/baseline.test.ts.snap +1 -1
- package/src/tokens/theme-dark.ts +1 -1
- package/src/utils/kLineDraw/axis.ts +1 -1
- package/src/utils/uuid.ts +8 -0
- package/src/version.ts +1 -1
|
@@ -190,7 +190,7 @@ describe('DataBuffer', () => {
|
|
|
190
190
|
})
|
|
191
191
|
|
|
192
192
|
buffer.ensureRange(oneYearAgo - 30 * MS_PER_DAY, oneYearAgo)
|
|
193
|
-
buffer.ensureRange(oneYearAgo -
|
|
193
|
+
buffer.ensureRange(oneYearAgo - 120 * MS_PER_DAY, oneYearAgo)
|
|
194
194
|
|
|
195
195
|
await vi.waitFor(() => {
|
|
196
196
|
expect(buffer.loading()).toBe(false)
|
|
@@ -97,15 +97,12 @@ export class DataBuffer {
|
|
|
97
97
|
|
|
98
98
|
if (requestStartTs >= this._loadedWindow.earliestTs) return
|
|
99
99
|
|
|
100
|
-
const incrementalStart = requestStartTs - INCREMENTAL_LOAD_DAYS * MS_PER_DAY
|
|
101
100
|
const incrementalEnd = this._loadedWindow.earliestTs
|
|
102
101
|
|
|
103
|
-
if (incrementalEnd <= incrementalStart) return
|
|
104
|
-
|
|
105
102
|
if (this._attemptedBoundaries.has(incrementalEnd)) return
|
|
106
103
|
|
|
107
104
|
this._attemptedBoundaries.add(incrementalEnd)
|
|
108
|
-
this.fetchRange(
|
|
105
|
+
this.fetchRange(requestStartTs, incrementalEnd)
|
|
109
106
|
}
|
|
110
107
|
|
|
111
108
|
private loadInitial(): void {
|
|
@@ -42,6 +42,7 @@ interface SecurityBar {
|
|
|
42
42
|
Turnover: number
|
|
43
43
|
RisePrice: number
|
|
44
44
|
RiseRate: number
|
|
45
|
+
Amplitude: number
|
|
45
46
|
Year: number
|
|
46
47
|
Month: number
|
|
47
48
|
Day: number
|
|
@@ -76,6 +77,7 @@ function mapBar(item: SecurityBar, code: string): KLineData {
|
|
|
76
77
|
turnoverRate: item.Turnover,
|
|
77
78
|
changeAmount: item.RisePrice,
|
|
78
79
|
changePercent: item.RiseRate,
|
|
80
|
+
amplitude: item.Amplitude,
|
|
79
81
|
stockCode: code,
|
|
80
82
|
}
|
|
81
83
|
}
|
|
@@ -36,7 +36,7 @@ export interface InteractionSnapshot {
|
|
|
36
36
|
export class InteractionController {
|
|
37
37
|
private chart: Chart
|
|
38
38
|
private isDragging = false
|
|
39
|
-
private dragMode: 'none' | 'pan' | 'resize-separator' | 'scale-price' = 'none'
|
|
39
|
+
private dragMode: 'none' | 'pan' | 'resize-separator' | 'scale-price' | 'explore' = 'none'
|
|
40
40
|
private dragStartX = 0
|
|
41
41
|
private scrollStartX = 0
|
|
42
42
|
|
|
@@ -62,6 +62,13 @@ export class InteractionController {
|
|
|
62
62
|
/** [触屏]:触摸会话标记,避免触摸触发的模拟 mouse 事件干扰 */
|
|
63
63
|
private isTouchSession = false
|
|
64
64
|
|
|
65
|
+
/** 触屏探索模式:true=长按出十字线不滚动,false=直接滚动 */
|
|
66
|
+
private exploreMode = true
|
|
67
|
+
/** 触屏按下时的时间戳/位置(用于 tap 检测) */
|
|
68
|
+
private touchStartTime = 0
|
|
69
|
+
private touchStartX = 0
|
|
70
|
+
private touchStartY = 0
|
|
71
|
+
|
|
65
72
|
private pinchTracker = new PinchTracker()
|
|
66
73
|
|
|
67
74
|
/** 十字线位置 */
|
|
@@ -217,14 +224,22 @@ export class InteractionController {
|
|
|
217
224
|
|
|
218
225
|
const pane = this.getPaneByY(mouseY)
|
|
219
226
|
this.isDragging = true
|
|
220
|
-
this.
|
|
221
|
-
this.
|
|
227
|
+
this.touchStartTime = Date.now()
|
|
228
|
+
this.touchStartX = e.clientX
|
|
229
|
+
this.touchStartY = e.clientY
|
|
230
|
+
this.dragMode = this.isTouchSession && this.exploreMode ? 'explore' : 'pan'
|
|
231
|
+
if (this.dragMode === 'explore') {
|
|
232
|
+
this.updatePlotHoverFromPoint(e.clientX, e.clientY)
|
|
233
|
+
}
|
|
222
234
|
this.dragStartX = e.clientX
|
|
223
235
|
this.dragStartY = e.clientY
|
|
224
236
|
this.scrollStartX = this.chart.getCachedScrollLeft()
|
|
225
237
|
this.activePaneIdOnDrag = pane?.id || null
|
|
226
238
|
|
|
227
239
|
this.chart.scheduleDraw()
|
|
240
|
+
if (this.dragMode === 'explore') {
|
|
241
|
+
this.notifyInteractionChange()
|
|
242
|
+
}
|
|
228
243
|
}
|
|
229
244
|
|
|
230
245
|
|
|
@@ -250,14 +265,37 @@ export class InteractionController {
|
|
|
250
265
|
|
|
251
266
|
if (e.isPrimary === false) return
|
|
252
267
|
const wasPanning = this.dragMode === 'pan'
|
|
268
|
+
const wasExploring = this.dragMode === 'explore'
|
|
269
|
+
|
|
270
|
+
if (wasExploring && this.isTouchSession) {
|
|
271
|
+
const elapsed = Date.now() - this.touchStartTime
|
|
272
|
+
const dx = e.clientX - this.touchStartX
|
|
273
|
+
const dy = e.clientY - this.touchStartY
|
|
274
|
+
if (elapsed < 200 && Math.abs(dx) < 5 && Math.abs(dy) < 5) {
|
|
275
|
+
// Quick tap → dismiss crosshair, switch to scroll mode
|
|
276
|
+
this.exploreMode = false
|
|
277
|
+
this.clearHover()
|
|
278
|
+
this.chart.scheduleDraw()
|
|
279
|
+
this.notifyInteractionChange()
|
|
280
|
+
} else {
|
|
281
|
+
// Long press or drag → keep crosshair
|
|
282
|
+
this.exploreMode = true
|
|
283
|
+
this.updatePlotHoverFromPoint(e.clientX, e.clientY)
|
|
284
|
+
this.chart.scheduleDraw()
|
|
285
|
+
this.notifyInteractionChange()
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (wasPanning) {
|
|
290
|
+
this.exploreMode = true
|
|
291
|
+
this.chart.checkVisibleRangeGap()
|
|
292
|
+
}
|
|
293
|
+
|
|
253
294
|
this.isDragging = false
|
|
254
295
|
this.dragMode = 'none'
|
|
255
296
|
this.activePaneIdOnDrag = null
|
|
256
297
|
this.activeSeparatorUpperPaneId = null
|
|
257
298
|
this.notifyInteractionChange()
|
|
258
|
-
if (wasPanning) {
|
|
259
|
-
this.chart.checkVisibleRangeGap()
|
|
260
|
-
}
|
|
261
299
|
}
|
|
262
300
|
|
|
263
301
|
/**
|
|
@@ -273,10 +311,12 @@ export class InteractionController {
|
|
|
273
311
|
this.dragMode = 'none'
|
|
274
312
|
this.activePaneIdOnDrag = null
|
|
275
313
|
this.clearSeparatorState()
|
|
314
|
+
if (!this.isTouchSession) {
|
|
315
|
+
this.clearHover()
|
|
316
|
+
this.chart.scheduleDraw()
|
|
317
|
+
this.notifyInteractionChange()
|
|
318
|
+
}
|
|
276
319
|
this.isTouchSession = false
|
|
277
|
-
this.clearHover()
|
|
278
|
-
this.chart.scheduleDraw()
|
|
279
|
-
this.notifyInteractionChange()
|
|
280
320
|
}
|
|
281
321
|
|
|
282
322
|
/** 处理滚动事件 */
|
|
@@ -326,6 +366,13 @@ export class InteractionController {
|
|
|
326
366
|
return
|
|
327
367
|
}
|
|
328
368
|
|
|
369
|
+
if (this.dragMode === 'explore') {
|
|
370
|
+
this.updatePlotHoverFromPoint(e.clientX, e.clientY)
|
|
371
|
+
this.chart.scheduleDraw()
|
|
372
|
+
this.notifyInteractionChange()
|
|
373
|
+
return
|
|
374
|
+
}
|
|
375
|
+
|
|
329
376
|
if (this.dragMode === 'pan') {
|
|
330
377
|
const deltaX = this.dragStartX - e.clientX
|
|
331
378
|
this.applyPanScroll(container, this.scrollStartX + deltaX)
|
|
@@ -348,7 +348,7 @@ export class ChartDataManager {
|
|
|
348
348
|
let firstVisibleTs: number | undefined
|
|
349
349
|
|
|
350
350
|
if (range.start < 0 && this._dataFetcher) {
|
|
351
|
-
const earlierThanEarliest = window.earliestTs -
|
|
351
|
+
const earlierThanEarliest = window.earliestTs - 365 * MS_PER_DAY
|
|
352
352
|
this._dataBuffer.ensureRange(earlierThanEarliest, window.earliestTs)
|
|
353
353
|
firstVisibleTs = this._internalData[0]?.timestamp
|
|
354
354
|
} else if (range.start < this._internalData.length) {
|
|
@@ -15,10 +15,10 @@ describe('calcIchimokuData', () => {
|
|
|
15
15
|
|
|
16
16
|
it('on constantPrice (H=L=100) all lines collapse to 100', () => {
|
|
17
17
|
// Only constantPrice (30 bars) is shorter than spanBPeriod=52, so use small periods for this test
|
|
18
|
-
const
|
|
19
|
-
|
|
18
|
+
const displacement = 5
|
|
19
|
+
const out = calcIchimokuData(constantPrice, 5, 10, 15, displacement)
|
|
20
20
|
const valid = out.filter((p): p is NonNullable<typeof p> => p !== undefined)
|
|
21
|
-
expect(valid.length).toBe(constantPrice.length)
|
|
21
|
+
expect(valid.length).toBe(constantPrice.length + displacement)
|
|
22
22
|
// After warm-up, tenkan and kijun should both be 100
|
|
23
23
|
for (let t = 15; t < out.length - 5; t++) {
|
|
24
24
|
const p = out[t]!
|
|
@@ -1559,7 +1559,7 @@ export function calcDonchianDataSoA(
|
|
|
1559
1559
|
// spanA(t) = (tenkan(t-displacement) + kijun(t-displacement)) / 2 ← 前置 displacement
|
|
1560
1560
|
// spanB(t) = 用 spanBPeriod 计算后再前置 displacement
|
|
1561
1561
|
// chikou(t) = close(t+displacement) ← 后置 displacement
|
|
1562
|
-
//
|
|
1562
|
+
// 输出长度 = data.length + displacement,末尾 displacement 根为未来云(仅 spanA/spanB)
|
|
1563
1563
|
// ============================================================================
|
|
1564
1564
|
|
|
1565
1565
|
export interface IchimokuPoint {
|
|
@@ -1600,8 +1600,11 @@ export function calcIchimokuData(
|
|
|
1600
1600
|
displacement: number,
|
|
1601
1601
|
): (IchimokuPoint | undefined)[] {
|
|
1602
1602
|
const n = data.length
|
|
1603
|
-
const
|
|
1604
|
-
|
|
1603
|
+
const totalLen = n + displacement
|
|
1604
|
+
const result: (IchimokuPoint | undefined)[] = new Array(totalLen).fill(undefined)
|
|
1605
|
+
if (n === 0 || tenkanPeriod <= 0 || kijunPeriod <= 0 || spanBPeriod <= 0) {
|
|
1606
|
+
return result.slice(0, n)
|
|
1607
|
+
}
|
|
1605
1608
|
|
|
1606
1609
|
const tenkan = _rollingMidline(data, tenkanPeriod)
|
|
1607
1610
|
const kijun = _rollingMidline(data, kijunPeriod)
|
|
@@ -1631,6 +1634,22 @@ export function calcIchimokuData(
|
|
|
1631
1634
|
result[t] = point
|
|
1632
1635
|
}
|
|
1633
1636
|
|
|
1637
|
+
// 未来云:在 data 末尾延伸 displacement 根,仅含 spanA/spanB
|
|
1638
|
+
for (let f = 0; f < displacement; f++) {
|
|
1639
|
+
const t = n + f
|
|
1640
|
+
const src = t - displacement
|
|
1641
|
+
const point: IchimokuPoint = {}
|
|
1642
|
+
if (src >= 0 && src < n) {
|
|
1643
|
+
if (tenkan[src] !== undefined && kijun[src] !== undefined) {
|
|
1644
|
+
point.spanA = (tenkan[src]! + kijun[src]!) / 2
|
|
1645
|
+
}
|
|
1646
|
+
if (spanBSource[src] !== undefined) {
|
|
1647
|
+
point.spanB = spanBSource[src]
|
|
1648
|
+
}
|
|
1649
|
+
}
|
|
1650
|
+
result[t] = point
|
|
1651
|
+
}
|
|
1652
|
+
|
|
1634
1653
|
return result
|
|
1635
1654
|
}
|
|
1636
1655
|
|
|
@@ -10,6 +10,8 @@ import { createIndicatorStateKey } from '../../plugin/stateKeys'
|
|
|
10
10
|
* - chikou (迟行线) = close[t+displacement],后置位移
|
|
11
11
|
*
|
|
12
12
|
* 任一字段都可能 undefined(数据不足或位移外)。
|
|
13
|
+
*
|
|
14
|
+
* series 长度 = data.length + displacement,末尾 displacement 槽位为未来云(仅 spanA/spanB)。
|
|
13
15
|
*/
|
|
14
16
|
export interface IchimokuPoint {
|
|
15
17
|
tenkan?: number
|
|
@@ -542,6 +542,57 @@ export function createValuePointVisibleStateComposer<T extends object>(
|
|
|
542
542
|
}
|
|
543
543
|
}
|
|
544
544
|
|
|
545
|
+
/**
|
|
546
|
+
* 一目均衡表专用的 visible state composer。
|
|
547
|
+
* 与 createValuePointVisibleStateComposer 的区别:
|
|
548
|
+
* - 将 visibleRange 向后扩展 displacement 根,以确保未来云的极值计入 valueMin/valueMax
|
|
549
|
+
*/
|
|
550
|
+
export function createIchimokuVisibleStateComposer<T extends object>(
|
|
551
|
+
bundleKey: string,
|
|
552
|
+
emptyState: {
|
|
553
|
+
timestamp: number
|
|
554
|
+
series: (T | undefined)[]
|
|
555
|
+
params: unknown
|
|
556
|
+
valueMin: number
|
|
557
|
+
valueMax: number
|
|
558
|
+
visibleMin: number
|
|
559
|
+
visibleMax: number
|
|
560
|
+
},
|
|
561
|
+
fields: readonly (keyof T)[],
|
|
562
|
+
): IndicatorVisibleStateComposer {
|
|
563
|
+
return ({ bundle, visibleRange, timestamp, active }) => {
|
|
564
|
+
const source = getPointArraySeriesBundle<T>(bundle, bundleKey)
|
|
565
|
+
if (!active) {
|
|
566
|
+
return {
|
|
567
|
+
...emptyState,
|
|
568
|
+
timestamp,
|
|
569
|
+
series: source.series,
|
|
570
|
+
params: source.params,
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
const displacement = (source.params as Record<string, unknown>)?.displacement as number ?? 26
|
|
575
|
+
const extendedRange = {
|
|
576
|
+
start: visibleRange.start,
|
|
577
|
+
end: Math.min(visibleRange.end + displacement, source.series.length),
|
|
578
|
+
}
|
|
579
|
+
const extremes = calcPointArrayExtremes(source.series, fields, extendedRange)
|
|
580
|
+
const bounds = computeMAFamilyBounds(
|
|
581
|
+
Number.isFinite(extremes.min) && Number.isFinite(extremes.max) ? extremes : null,
|
|
582
|
+
emptyState,
|
|
583
|
+
)
|
|
584
|
+
return {
|
|
585
|
+
timestamp,
|
|
586
|
+
series: source.series,
|
|
587
|
+
params: source.params,
|
|
588
|
+
valueMin: bounds.valueMin,
|
|
589
|
+
valueMax: bounds.valueMax,
|
|
590
|
+
visibleMin: extremes.min,
|
|
591
|
+
visibleMax: extremes.max,
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
545
596
|
export function createBandVisibleStateComposer<T extends object>(
|
|
546
597
|
bundleKey: string,
|
|
547
598
|
emptyState: {
|
|
@@ -8,7 +8,8 @@ import { Indicator } from '../../indicators/indicatorDefinitionRegistry'
|
|
|
8
8
|
import { resolveStateKey, type TitleInfo, type TitleValueItem, type GetTitleInfoFn } from '../../indicators/indicatorMetadata'
|
|
9
9
|
import type { IndicatorScheduler, IchimokuSchedulerConfig } from '../../indicators/scheduler'
|
|
10
10
|
import { calcIchimokuData } from '../../indicators/calculators'
|
|
11
|
-
import {
|
|
11
|
+
import { createIchimokuVisibleStateComposer } from '../../indicators/visibleStateComposers'
|
|
12
|
+
import { getPhysicalKLineConfig } from '../../utils/klineConfig'
|
|
12
13
|
|
|
13
14
|
const TENKAN_COLOR = '#dc2626'
|
|
14
15
|
const KIJUN_COLOR = '#2563eb'
|
|
@@ -50,7 +51,7 @@ export function createIchimokuRendererPlugin(options: IchimokuRendererOptions =
|
|
|
50
51
|
description: '一目均衡表渲染器(WebGL 线 + Canvas2D 云图)',
|
|
51
52
|
debugName: 'Ichimoku',
|
|
52
53
|
paneId,
|
|
53
|
-
priority: RENDERER_PRIORITY.
|
|
54
|
+
priority: RENDERER_PRIORITY.INDICATOR,
|
|
54
55
|
|
|
55
56
|
onInstall(host: PluginHost) { pluginHost = host },
|
|
56
57
|
getDeclaredNamespaces() { const key = resolveKey(); return key ? [key] : [] },
|
|
@@ -90,6 +91,25 @@ export function createIchimokuRendererPlugin(options: IchimokuRendererOptions =
|
|
|
90
91
|
}
|
|
91
92
|
}
|
|
92
93
|
|
|
94
|
+
// 未来云:在数据末尾延伸 displacement 根 spanA/spanB 线及云段
|
|
95
|
+
const dataLen = (context.data as unknown[]).length
|
|
96
|
+
if (dataLen < series.length) {
|
|
97
|
+
const physConfig = getPhysicalKLineConfig(context.kWidth, context.kGap, context.dpr)
|
|
98
|
+
const futureEnd = Math.min(dataLen + params.displacement, series.length)
|
|
99
|
+
for (let i = dataLen; i < futureEnd; i++) {
|
|
100
|
+
const p = series[i]
|
|
101
|
+
if (!p) continue
|
|
102
|
+
const leftPx = physConfig.startXPx + i * physConfig.unitPx
|
|
103
|
+
const wickXPx = leftPx + (physConfig.kWidthPx - 1) / 2
|
|
104
|
+
const centerX = wickXPx / context.dpr
|
|
105
|
+
if (params.showSpanA && p.spanA !== undefined) spanAPts.push({ x: centerX, y: toY(p.spanA) })
|
|
106
|
+
if (params.showSpanB && p.spanB !== undefined) spanBPts.push({ x: centerX, y: toY(p.spanB) })
|
|
107
|
+
if (params.showCloud && p.spanA !== undefined && p.spanB !== undefined) {
|
|
108
|
+
cloudSegs.push({ x: centerX, ya: toY(p.spanA), yb: toY(p.spanB), bull: p.spanA > p.spanB })
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
93
113
|
// Cloud fill (Canvas2D only)
|
|
94
114
|
if (params.showCloud && cloudSegs.length >= 2) {
|
|
95
115
|
ctx.save()
|
|
@@ -209,7 +229,7 @@ export function getIchimokuTitleInfo(
|
|
|
209
229
|
allowMainPane: true,
|
|
210
230
|
mainPane: { rendererName: 'ichimoku_main', toActiveConfig: (params, active) => ({ ...params, showTenkan: active, showKijun: active, showSpanA: active, showSpanB: active, showChikou: active, showCloud: active }) },
|
|
211
231
|
scale: { indicatorKey: 'ichimoku', label: 'Ichimoku', decimals: 2 },
|
|
212
|
-
visibleState: { compose:
|
|
232
|
+
visibleState: { compose: createIchimokuVisibleStateComposer('ichimoku', EMPTY_ICHIMOKU_STATE, ['tenkan', 'kijun', 'spanA', 'spanB', 'chikou']) },
|
|
213
233
|
runtime: { defaultConfig:{tenkanPeriod:9,kijunPeriod:26,spanBPeriod:52,displacement:26,showTenkan:true,showKijun:true,showSpanA:true,showSpanB:true,showCloud:true,showChikou:true}, computeKey:'calcIchimokuData', compute:(data,c)=>calcIchimokuData(data,c.tenkanPeriod,c.kijunPeriod,c.spanBPeriod,c.displacement) },
|
|
214
234
|
})
|
|
215
235
|
class IchimokuDefinition {
|
|
@@ -81,6 +81,102 @@ export function createMainIndicatorLegendRendererPlugin(options: {
|
|
|
81
81
|
const targetIndex = crosshairIndex ?? Math.min(range.end - 1, klineData.length - 1)
|
|
82
82
|
const rows: Array<{ draw: (rowIndex: number) => void }> = []
|
|
83
83
|
|
|
84
|
+
if (typeof crosshairIndex === 'number') {
|
|
85
|
+
const k = klineData[targetIndex]
|
|
86
|
+
if (k) {
|
|
87
|
+
const isUp = k.close >= k.open
|
|
88
|
+
const volText = typeof k.volume === 'number' ? formatVolumeShort(k.volume) : null
|
|
89
|
+
const upColor = isUp ? colors.candleUpBody : colors.candleDownBody
|
|
90
|
+
|
|
91
|
+
if (context.paneWidth >= 400) {
|
|
92
|
+
rows.push({
|
|
93
|
+
draw: (rowIndex: number) => {
|
|
94
|
+
let x = legendX
|
|
95
|
+
const y = config.yPaddingPx / 2 + legendYOffset + rowIndex * lineHeight
|
|
96
|
+
|
|
97
|
+
overlayCtx.fillStyle = colors.text.primary
|
|
98
|
+
overlayCtx.fillText('O ', x, y)
|
|
99
|
+
x += measureTextWidth(overlayCtx, 'O ')
|
|
100
|
+
overlayCtx.fillStyle = upColor
|
|
101
|
+
overlayCtx.fillText(k.open.toFixed(2), x, y)
|
|
102
|
+
x += measureTextWidth(overlayCtx, k.open.toFixed(2)) + gap
|
|
103
|
+
|
|
104
|
+
overlayCtx.fillStyle = colors.text.primary
|
|
105
|
+
overlayCtx.fillText('H ', x, y)
|
|
106
|
+
x += measureTextWidth(overlayCtx, 'H ')
|
|
107
|
+
overlayCtx.fillText(k.high.toFixed(2), x, y)
|
|
108
|
+
x += measureTextWidth(overlayCtx, k.high.toFixed(2)) + gap
|
|
109
|
+
|
|
110
|
+
overlayCtx.fillText('L ', x, y)
|
|
111
|
+
x += measureTextWidth(overlayCtx, 'L ')
|
|
112
|
+
overlayCtx.fillText(k.low.toFixed(2), x, y)
|
|
113
|
+
x += measureTextWidth(overlayCtx, k.low.toFixed(2)) + gap
|
|
114
|
+
|
|
115
|
+
overlayCtx.fillStyle = colors.text.primary
|
|
116
|
+
overlayCtx.fillText('C ', x, y)
|
|
117
|
+
x += measureTextWidth(overlayCtx, 'C ')
|
|
118
|
+
overlayCtx.fillStyle = upColor
|
|
119
|
+
overlayCtx.fillText(k.close.toFixed(2), x, y)
|
|
120
|
+
x += measureTextWidth(overlayCtx, k.close.toFixed(2)) + gap
|
|
121
|
+
|
|
122
|
+
if (volText) {
|
|
123
|
+
overlayCtx.fillStyle = colors.text.tertiary
|
|
124
|
+
overlayCtx.fillText('Vol ', x, y)
|
|
125
|
+
x += measureTextWidth(overlayCtx, 'Vol ')
|
|
126
|
+
overlayCtx.fillStyle = colors.text.primary
|
|
127
|
+
overlayCtx.fillText(volText, x, y)
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
})
|
|
131
|
+
} else {
|
|
132
|
+
rows.push({
|
|
133
|
+
draw: (rowIndex: number) => {
|
|
134
|
+
let x = legendX
|
|
135
|
+
const y = config.yPaddingPx / 2 + legendYOffset + rowIndex * lineHeight
|
|
136
|
+
|
|
137
|
+
overlayCtx.fillStyle = colors.text.primary
|
|
138
|
+
overlayCtx.fillText('O ', x, y)
|
|
139
|
+
x += measureTextWidth(overlayCtx, 'O ')
|
|
140
|
+
overlayCtx.fillStyle = upColor
|
|
141
|
+
overlayCtx.fillText(k.open.toFixed(2), x, y)
|
|
142
|
+
x += measureTextWidth(overlayCtx, k.open.toFixed(2)) + gap
|
|
143
|
+
|
|
144
|
+
overlayCtx.fillStyle = colors.text.primary
|
|
145
|
+
overlayCtx.fillText('H ', x, y)
|
|
146
|
+
x += measureTextWidth(overlayCtx, 'H ')
|
|
147
|
+
overlayCtx.fillText(k.high.toFixed(2), x, y)
|
|
148
|
+
x += measureTextWidth(overlayCtx, k.high.toFixed(2)) + gap
|
|
149
|
+
|
|
150
|
+
overlayCtx.fillText('L ', x, y)
|
|
151
|
+
x += measureTextWidth(overlayCtx, 'L ')
|
|
152
|
+
overlayCtx.fillText(k.low.toFixed(2), x, y)
|
|
153
|
+
},
|
|
154
|
+
})
|
|
155
|
+
rows.push({
|
|
156
|
+
draw: (rowIndex: number) => {
|
|
157
|
+
let x = legendX
|
|
158
|
+
const y = config.yPaddingPx / 2 + legendYOffset + rowIndex * lineHeight
|
|
159
|
+
|
|
160
|
+
overlayCtx.fillStyle = colors.text.primary
|
|
161
|
+
overlayCtx.fillText('C ', x, y)
|
|
162
|
+
x += measureTextWidth(overlayCtx, 'C ')
|
|
163
|
+
overlayCtx.fillStyle = upColor
|
|
164
|
+
overlayCtx.fillText(k.close.toFixed(2), x, y)
|
|
165
|
+
x += measureTextWidth(overlayCtx, k.close.toFixed(2)) + gap
|
|
166
|
+
|
|
167
|
+
if (volText) {
|
|
168
|
+
overlayCtx.fillStyle = colors.text.tertiary
|
|
169
|
+
overlayCtx.fillText('Vol ', x, y)
|
|
170
|
+
x += measureTextWidth(overlayCtx, 'Vol ')
|
|
171
|
+
overlayCtx.fillStyle = colors.text.primary
|
|
172
|
+
overlayCtx.fillText(volText, x, y)
|
|
173
|
+
}
|
|
174
|
+
},
|
|
175
|
+
})
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
84
180
|
const scheduler = pluginHost && typeof pluginHost.getService === 'function'
|
|
85
181
|
? pluginHost.getService<IndicatorScheduler>('indicatorScheduler')
|
|
86
182
|
: undefined
|
|
@@ -237,3 +333,9 @@ function findBaselineByTimestamp(data: ReadonlyArray<KLineData>, timestamp: numb
|
|
|
237
333
|
}
|
|
238
334
|
return null
|
|
239
335
|
}
|
|
336
|
+
|
|
337
|
+
function formatVolumeShort(v: number): string {
|
|
338
|
+
if (v >= 1e8) return (v / 1e8).toFixed(2) + '亿'
|
|
339
|
+
if (v >= 1e4) return (v / 1e4).toFixed(2) + '万'
|
|
340
|
+
return v.toFixed(2)
|
|
341
|
+
}
|
|
@@ -30,7 +30,7 @@ export function createCrosshairRendererPlugin(options: {
|
|
|
30
30
|
const colors = resolveThemeColors(context.theme, context.isAsiaMarket, context.colorPresetSettings)
|
|
31
31
|
const state = options.getCrosshairState()
|
|
32
32
|
|
|
33
|
-
if (
|
|
33
|
+
if (!state.pos) return
|
|
34
34
|
|
|
35
35
|
const { x } = state.pos
|
|
36
36
|
const isActive = pane.id === state.activePaneId
|
package/src/index.ts
CHANGED
package/src/mcp/chartBridge.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ToolCall, ToolResult, ControllerDescription, ToolCallHandler } from './types'
|
|
2
|
+
import { generateUUID } from '../utils/uuid'
|
|
2
3
|
|
|
3
4
|
export interface ChartBridgeOptions {
|
|
4
5
|
wsUrl: string
|
|
@@ -6,6 +7,7 @@ export interface ChartBridgeOptions {
|
|
|
6
7
|
sessionId?: string
|
|
7
8
|
autoReconnect?: boolean
|
|
8
9
|
reconnectDelay?: number
|
|
10
|
+
maxReconnectDelay?: number
|
|
9
11
|
heartbeatInterval?: number
|
|
10
12
|
wsImpl?: new (url: string) => WebSocket
|
|
11
13
|
}
|
|
@@ -22,6 +24,7 @@ export class ChartBridge {
|
|
|
22
24
|
readonly sessionId: string
|
|
23
25
|
private readonly autoReconnect: boolean
|
|
24
26
|
private readonly reconnectDelay: number
|
|
27
|
+
private readonly maxReconnectDelay: number
|
|
25
28
|
private readonly heartbeatInterval: number
|
|
26
29
|
private readonly onToolCall: ToolCallHandler
|
|
27
30
|
|
|
@@ -29,6 +32,7 @@ export class ChartBridge {
|
|
|
29
32
|
private ws: WebSocket | null = null
|
|
30
33
|
private reconnectTimer: ReturnType<typeof setTimeout> | null = null
|
|
31
34
|
private heartbeatTimer: ReturnType<typeof setInterval> | null = null
|
|
35
|
+
private _reconnectAttempt = 0
|
|
32
36
|
private destroyed = false
|
|
33
37
|
|
|
34
38
|
private listeners = new Map<ChartBridgeEvent, Set<MessageHandler>>()
|
|
@@ -39,9 +43,10 @@ export class ChartBridge {
|
|
|
39
43
|
onStateChange?: () => void
|
|
40
44
|
|
|
41
45
|
constructor(options: ChartBridgeOptions) {
|
|
42
|
-
this.sessionId = options.sessionId ??
|
|
46
|
+
this.sessionId = options.sessionId ?? generateUUID()
|
|
43
47
|
this.autoReconnect = options.autoReconnect ?? true
|
|
44
48
|
this.reconnectDelay = options.reconnectDelay ?? 3000
|
|
49
|
+
this.maxReconnectDelay = options.maxReconnectDelay ?? 30_000
|
|
45
50
|
this.heartbeatInterval = options.heartbeatInterval ?? 30_000
|
|
46
51
|
this.onToolCall = options.onToolCall
|
|
47
52
|
this.wsImpl = options.wsImpl ?? WebSocket
|
|
@@ -59,6 +64,7 @@ export class ChartBridge {
|
|
|
59
64
|
const ws = new this.wsImpl(this.wsUrl)
|
|
60
65
|
|
|
61
66
|
ws.onopen = () => {
|
|
67
|
+
this._reconnectAttempt = 0
|
|
62
68
|
this.ws = ws
|
|
63
69
|
console.info(
|
|
64
70
|
`[ChartBridge] WS opened → sending register (sessionId=${this.sessionId})`,
|
|
@@ -115,6 +121,7 @@ export class ChartBridge {
|
|
|
115
121
|
|
|
116
122
|
destroy(): void {
|
|
117
123
|
this.destroyed = true
|
|
124
|
+
this._reconnectAttempt = 0
|
|
118
125
|
this.disconnect()
|
|
119
126
|
this.listeners.clear()
|
|
120
127
|
}
|
|
@@ -188,11 +195,22 @@ export class ChartBridge {
|
|
|
188
195
|
|
|
189
196
|
private scheduleReconnect(): void {
|
|
190
197
|
this.cancelReconnect()
|
|
198
|
+
const base = this.reconnectDelay
|
|
199
|
+
const attempt = this._reconnectAttempt
|
|
200
|
+
const exponential = Math.min(base * Math.pow(2, attempt), this.maxReconnectDelay)
|
|
201
|
+
const jitter = 0.5 + Math.random() * 0.5
|
|
202
|
+
const delay = Math.round(exponential * jitter)
|
|
203
|
+
|
|
204
|
+
this._reconnectAttempt = attempt + 1
|
|
205
|
+
console.info(
|
|
206
|
+
`[ChartBridge] reconnect scheduled in ${delay}ms (attempt ${attempt + 1})`,
|
|
207
|
+
)
|
|
208
|
+
|
|
191
209
|
this.reconnectTimer = setTimeout(() => {
|
|
192
210
|
if (!this.destroyed) {
|
|
193
211
|
this.connect()
|
|
194
212
|
}
|
|
195
|
-
},
|
|
213
|
+
}, delay)
|
|
196
214
|
}
|
|
197
215
|
|
|
198
216
|
private cancelReconnect(): void {
|
|
@@ -64,7 +64,7 @@ exports[`theme baseline — dark > CSS declaration block (snapshot) 1`] = `
|
|
|
64
64
|
--klc-color-tag-bg-transparent: transparent;
|
|
65
65
|
--klc-color-tag-bg-active: #1890ff;
|
|
66
66
|
--klc-color-tag-bg-active-hover: #40a9ff;
|
|
67
|
-
--klc-color-tag-bg-hover: #
|
|
67
|
+
--klc-color-tag-bg-hover: #262C36;
|
|
68
68
|
--klc-color-border-dark: rgba(255, 255, 255, 0.15);
|
|
69
69
|
--klc-color-border-medium: rgba(255, 255, 255, 0.12);
|
|
70
70
|
--klc-color-border-light: rgba(255, 255, 255, 0.08);
|
package/src/tokens/theme-dark.ts
CHANGED
|
@@ -505,7 +505,7 @@ export function drawAxisPriceLabel(ctx: CanvasRenderingContext2D, opts: AxisPric
|
|
|
505
505
|
|
|
506
506
|
const centerX = x + width / 2
|
|
507
507
|
ctx.fillStyle = textColor
|
|
508
|
-
ctx.fillText(priceText, roundToPhysicalPixel(centerX, dpr),
|
|
508
|
+
ctx.fillText(priceText, roundToPhysicalPixel(centerX, dpr), roundToPhysicalPixel(yy, dpr) + 1)
|
|
509
509
|
|
|
510
510
|
ctx.restore()
|
|
511
511
|
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export function generateUUID(): string {
|
|
2
|
+
if (typeof crypto.randomUUID === 'function') return crypto.randomUUID()
|
|
3
|
+
const bytes = crypto.getRandomValues(new Uint8Array(16))
|
|
4
|
+
bytes[6] = (bytes[6]! & 0x0f) | 0x40
|
|
5
|
+
bytes[8] = (bytes[8]! & 0x3f) | 0x80
|
|
6
|
+
const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, '0'))
|
|
7
|
+
return `${hex.slice(0, 4).join('')}-${hex.slice(4, 6).join('')}-${hex.slice(6, 8).join('')}-${hex.slice(8, 10).join('')}-${hex.slice(10).join('')}`
|
|
8
|
+
}
|
package/src/version.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION = "0.8.
|
|
1
|
+
export const VERSION = "0.8.6"
|