@363045841yyt/klinechart-core 0.8.1 → 0.8.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (138) hide show
  1. package/dist/controllers/createChartController.d.ts.map +1 -1
  2. package/dist/controllers/createChartController.js +31 -0
  3. package/dist/controllers/createChartController.js.map +1 -1
  4. package/dist/controllers/types.d.ts +16 -0
  5. package/dist/controllers/types.d.ts.map +1 -1
  6. package/dist/data-fetchers/baostock.d.ts +9 -2
  7. package/dist/data-fetchers/baostock.d.ts.map +1 -1
  8. package/dist/data-fetchers/baostock.js +78 -9
  9. package/dist/data-fetchers/baostock.js.map +1 -1
  10. package/dist/data-fetchers/dataBuffer.d.ts.map +1 -1
  11. package/dist/data-fetchers/dataBuffer.js +3 -0
  12. package/dist/data-fetchers/dataBuffer.js.map +1 -1
  13. package/dist/data-fetchers/fetcherDefinitionRegistry.d.ts +13 -0
  14. package/dist/data-fetchers/fetcherDefinitionRegistry.d.ts.map +1 -0
  15. package/dist/data-fetchers/fetcherDefinitionRegistry.js +36 -0
  16. package/dist/data-fetchers/fetcherDefinitionRegistry.js.map +1 -0
  17. package/dist/data-fetchers/gotdx.d.ts +10 -0
  18. package/dist/data-fetchers/gotdx.d.ts.map +1 -0
  19. package/dist/data-fetchers/gotdx.js +168 -0
  20. package/dist/data-fetchers/gotdx.js.map +1 -0
  21. package/dist/data-fetchers/hundred-mock.d.ts +9 -2
  22. package/dist/data-fetchers/hundred-mock.d.ts.map +1 -1
  23. package/dist/data-fetchers/hundred-mock.js +92 -7
  24. package/dist/data-fetchers/hundred-mock.js.map +1 -1
  25. package/dist/data-fetchers/index.d.ts +7 -4
  26. package/dist/data-fetchers/index.d.ts.map +1 -1
  27. package/dist/data-fetchers/index.js +6 -4
  28. package/dist/data-fetchers/index.js.map +1 -1
  29. package/dist/data-fetchers/router.d.ts.map +1 -1
  30. package/dist/data-fetchers/router.js +14 -15
  31. package/dist/data-fetchers/router.js.map +1 -1
  32. package/dist/data-fetchers/thousand-mock.d.ts +9 -2
  33. package/dist/data-fetchers/thousand-mock.d.ts.map +1 -1
  34. package/dist/data-fetchers/thousand-mock.js +88 -16
  35. package/dist/data-fetchers/thousand-mock.js.map +1 -1
  36. package/dist/data-fetchers/tradingview.d.ts +9 -2
  37. package/dist/data-fetchers/tradingview.d.ts.map +1 -1
  38. package/dist/data-fetchers/tradingview.js +75 -4
  39. package/dist/data-fetchers/tradingview.js.map +1 -1
  40. package/dist/data-fetchers/types.d.ts +21 -0
  41. package/dist/data-fetchers/types.d.ts.map +1 -0
  42. package/dist/data-fetchers/types.js +2 -0
  43. package/dist/data-fetchers/types.js.map +1 -0
  44. package/dist/engine/data/chartDataManager.d.ts +1 -0
  45. package/dist/engine/data/chartDataManager.d.ts.map +1 -1
  46. package/dist/engine/data/chartDataManager.js +3 -0
  47. package/dist/engine/data/chartDataManager.js.map +1 -1
  48. package/dist/engine/render/chartRenderer.d.ts.map +1 -1
  49. package/dist/engine/render/chartRenderer.js +2 -0
  50. package/dist/engine/render/chartRenderer.js.map +1 -1
  51. package/dist/engine/renderers/Indicator/ichimoku.d.ts.map +1 -1
  52. package/dist/engine/renderers/Indicator/ichimoku.js +8 -5
  53. package/dist/engine/renderers/Indicator/ichimoku.js.map +1 -1
  54. package/dist/engine/renderers/Indicator/mainIndicatorLegend.js +1 -1
  55. package/dist/engine/renderers/Indicator/mainIndicatorLegend.js.map +1 -1
  56. package/dist/engine/renderers/Indicator/sar.d.ts.map +1 -1
  57. package/dist/engine/renderers/Indicator/sar.js +3 -3
  58. package/dist/engine/renderers/Indicator/sar.js.map +1 -1
  59. package/dist/engine/renderers/Indicator/supertrend.d.ts.map +1 -1
  60. package/dist/engine/renderers/Indicator/supertrend.js +3 -3
  61. package/dist/engine/renderers/Indicator/supertrend.js.map +1 -1
  62. package/dist/engine/renderers/timeAxis.d.ts.map +1 -1
  63. package/dist/engine/renderers/timeAxis.js +1 -0
  64. package/dist/engine/renderers/timeAxis.js.map +1 -1
  65. package/dist/index.d.ts +2 -0
  66. package/dist/index.d.ts.map +1 -1
  67. package/dist/index.js +2 -0
  68. package/dist/index.js.map +1 -1
  69. package/dist/mcp/chartBridge.d.ts +47 -0
  70. package/dist/mcp/chartBridge.d.ts.map +1 -0
  71. package/dist/mcp/chartBridge.js +167 -0
  72. package/dist/mcp/chartBridge.js.map +1 -0
  73. package/dist/mcp/index.d.ts +3 -0
  74. package/dist/mcp/index.d.ts.map +1 -0
  75. package/dist/mcp/index.js +2 -0
  76. package/dist/mcp/index.js.map +1 -0
  77. package/dist/mcp/types.d.ts +17 -0
  78. package/dist/mcp/types.d.ts.map +1 -0
  79. package/dist/mcp/types.js +2 -0
  80. package/dist/mcp/types.js.map +1 -0
  81. package/dist/plugin/types.d.ts +2 -0
  82. package/dist/plugin/types.d.ts.map +1 -1
  83. package/dist/plugin/types.js.map +1 -1
  84. package/dist/semantic/index.d.ts +1 -1
  85. package/dist/semantic/index.d.ts.map +1 -1
  86. package/dist/semantic/index.js.map +1 -1
  87. package/dist/semantic/schema.json +1 -1
  88. package/dist/semantic/types.d.ts +2 -1
  89. package/dist/semantic/types.d.ts.map +1 -1
  90. package/dist/utils/dateFormat.d.ts +25 -0
  91. package/dist/utils/dateFormat.d.ts.map +1 -1
  92. package/dist/utils/dateFormat.js +78 -0
  93. package/dist/utils/dateFormat.js.map +1 -1
  94. package/dist/utils/kLineDraw/axis.d.ts +2 -0
  95. package/dist/utils/kLineDraw/axis.d.ts.map +1 -1
  96. package/dist/utils/kLineDraw/axis.js +11 -6
  97. package/dist/utils/kLineDraw/axis.js.map +1 -1
  98. package/dist/version.d.ts +1 -1
  99. package/dist/version.js +1 -1
  100. package/package.json +1 -1
  101. package/src/controllers/createChartController.ts +34 -0
  102. package/src/controllers/types.ts +9 -0
  103. package/src/data-fetchers/__tests__/dataBuffer.test.ts +5 -2
  104. package/src/data-fetchers/__tests__/fetcherRegistry.test.ts +192 -0
  105. package/src/data-fetchers/baostock.ts +54 -22
  106. package/src/data-fetchers/dataBuffer.ts +6 -0
  107. package/src/data-fetchers/fetcherDefinitionRegistry.ts +50 -0
  108. package/src/data-fetchers/gotdx.ts +160 -0
  109. package/src/data-fetchers/hundred-mock.ts +54 -7
  110. package/src/data-fetchers/index.ts +19 -4
  111. package/src/data-fetchers/router.ts +27 -15
  112. package/src/data-fetchers/thousand-mock.ts +49 -16
  113. package/src/data-fetchers/tradingview.ts +32 -6
  114. package/src/data-fetchers/types.ts +27 -0
  115. package/src/engine/data/chartDataManager.ts +4 -0
  116. package/src/engine/render/chartRenderer.ts +2 -0
  117. package/src/engine/renderers/Indicator/ichimoku.ts +10 -4
  118. package/src/engine/renderers/Indicator/mainIndicatorLegend.ts +1 -1
  119. package/src/engine/renderers/Indicator/sar.ts +3 -3
  120. package/src/engine/renderers/Indicator/supertrend.ts +3 -4
  121. package/src/engine/renderers/__tests__/boll.renderer.test.ts +1 -0
  122. package/src/engine/renderers/__tests__/ene.renderer.test.ts +1 -0
  123. package/src/engine/renderers/__tests__/expma.renderer.test.ts +1 -0
  124. package/src/engine/renderers/__tests__/ma.renderer.test.ts +1 -0
  125. package/src/engine/renderers/__tests__/mainIndicatorLegend.renderer.test.ts +1 -0
  126. package/src/engine/renderers/__tests__/yAxis.renderer.test.ts +1 -0
  127. package/src/engine/renderers/timeAxis.ts +1 -0
  128. package/src/index.ts +2 -0
  129. package/src/mcp/chartBridge.ts +220 -0
  130. package/src/mcp/index.ts +2 -0
  131. package/src/mcp/types.ts +19 -0
  132. package/src/plugin/types.ts +2 -0
  133. package/src/semantic/index.ts +1 -0
  134. package/src/semantic/schema.json +1 -1
  135. package/src/semantic/types.ts +3 -1
  136. package/src/utils/dateFormat.ts +85 -0
  137. package/src/utils/kLineDraw/axis.ts +13 -6
  138. package/src/version.ts +1 -1
@@ -1,30 +1,63 @@
1
- import type { DataFetcher, KLineData } from '../controllers/types'
1
+ import type { KLineData } from '../controllers/types'
2
+ import { DataFetcher } from './fetcherDefinitionRegistry'
3
+ import type { FetchConfig } from './types'
2
4
 
3
- export const thousandMockDataFetcher: DataFetcher = async (_source, _config) => {
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()
7
12
  const dayMs = 24 * 60 * 60 * 1000
8
- let lastClose = 3000
9
- for (let i = 0; i < 10000; i++) {
13
+ const totalDays = 10000
14
+
15
+ const basePrice = 3000
16
+ const meanReversionStrength = 0.0005
17
+ const volatility = 0.02
18
+
19
+ const rawWalk: number[] = [basePrice]
20
+ for (let i = 1; i < totalDays; i++) {
21
+ const prev = rawWalk[i - 1]!
22
+ const reversion = meanReversionStrength * (basePrice - prev)
23
+ const change = (Math.random() - 0.5) * 2 * volatility * prev + reversion
24
+ rawWalk.push(prev + change)
25
+ }
26
+
27
+ const finalOffset = rawWalk[totalDays - 1]! - basePrice
28
+ for (let i = 0; i < totalDays; i++) {
29
+ const bridge = finalOffset * (i / (totalDays - 1))
30
+ const close = Math.round((rawWalk[i]! - bridge) * 100) / 100
31
+
10
32
  const timestamp = startTime + i * dayMs
11
- const volatility = 0.02
12
- const trend = 0.0001
13
- const change = (Math.random() - 0.5) * 2 * volatility + trend
14
- const open = lastClose
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)
33
+ const open = i === 0 ? basePrice : data[i - 1]!.close
34
+
35
+ const high = Math.round(Math.max(open, close) * (1 + Math.random() * 0.01) * 100) / 100
36
+ const low = Math.round(Math.min(open, close) * (1 - Math.random() * 0.01) * 100) / 100
18
37
  const volume = Math.floor(1000000 + Math.random() * 5000000)
19
38
  data.push({
20
39
  timestamp,
21
- open: parseFloat(open.toFixed(2)),
22
- high: parseFloat(high.toFixed(2)),
23
- low: parseFloat(low.toFixed(2)),
24
- close: parseFloat(close.toFixed(2)),
40
+ open,
41
+ high,
42
+ low,
43
+ close,
25
44
  volume,
26
45
  })
27
- lastClose = close
28
46
  }
47
+
29
48
  return data
30
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 { DataFetcher, KLineData } from '../controllers/types'
1
+ import type { KLineData } from '../controllers/types'
2
+ import { DataFetcher } from './fetcherDefinitionRegistry'
3
+ import type { FetchConfig } from './types'
2
4
 
3
5
  const PERIOD_TO_TIMEFRAME: Record<string, string> = {
4
6
  daily: '1d',
@@ -10,14 +12,25 @@ const PERIOD_TO_TIMEFRAME: Record<string, string> = {
10
12
  '60min': '60m',
11
13
  }
12
14
 
13
- export const tradingviewDataFetcher: DataFetcher = async (source, config) => {
14
- const baseUrl = source === 'tradingview' ? 'http://localhost:8000' : ''
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
-
30
+ const tvAdjust = ADJUST_TO_TV[config.adjust]
19
31
  const exchangeQ = config.exchange ? `&exchange=${config.exchange}` : ''
20
- const url = `${baseUrl}/api/tradingview/kdata?symbol=${config.symbol}&timeframe=${timeframe}&start_date=${startDate}&end_date=${endDate}${exchangeQ}`
32
+ const adjustQ = tvAdjust ? `&adjust=${tvAdjust}` : ''
33
+ const url = `${BASE_URL}/api/tradingview/kdata?symbol=${config.symbol}&timeframe=${timeframe}&start_date=${startDate}&end_date=${endDate}${exchangeQ}${adjustQ}`
21
34
  try {
22
35
  const res = await fetch(url)
23
36
  if (!res.ok) {
@@ -30,7 +43,6 @@ export const tradingviewDataFetcher: DataFetcher = async (source, config) => {
30
43
  if (json.warning) {
31
44
  console.warn(`[tradingview] ${json.warning}`)
32
45
  }
33
-
34
46
  return (json.data ?? []).map((item: Record<string, unknown>) => ({
35
47
  timestamp: item.ts_open as number,
36
48
  date: item.date as string,
@@ -46,3 +58,17 @@ export const tradingviewDataFetcher: DataFetcher = async (source, config) => {
46
58
  throw err
47
59
  }
48
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
+ }
@@ -177,6 +177,10 @@ export class ChartDataManager {
177
177
  return this._symbolsSignal
178
178
  }
179
179
 
180
+ get currentPeriod(): string {
181
+ return this._dataBuffer.currentSpec?.period ?? 'daily'
182
+ }
183
+
180
184
  getInternalData(): KLineData[] {
181
185
  return this._internalData
182
186
  }
@@ -401,6 +401,7 @@ export class ChartRenderer {
401
401
  overlayCtx: overlayCtx ?? undefined,
402
402
  pane: wrapPaneInfo(pane),
403
403
  data: dataManager.getInternalData(),
404
+ period: dataManager.currentPeriod,
404
405
  comparisonData: dataManager.getComparisonData(),
405
406
  comparisonSymbols: dataManager.getComparisonSpecs(),
406
407
  comparisonColors: dataManager.getComparisonColors(),
@@ -496,6 +497,7 @@ export class ChartRenderer {
496
497
  },
497
498
  priceRange: { maxPrice: 0, minPrice: 0 },
498
499
  },
500
+ period: this.deps.getDataManager().currentPeriod,
499
501
  data: this.deps.getDataManager().getInternalData(),
500
502
  range,
501
503
  scrollLeft: vp.scrollLeft,
@@ -1,5 +1,6 @@
1
1
  import type { RendererPluginWithHost, RenderContext, PluginHost } from '../../../plugin'
2
2
  import { RENDERER_PRIORITY } from '../../../plugin'
3
+ import { resolveThemeColors } from '../../../tokens'
3
4
  import type { KLineData } from '../../../types/price'
4
5
  import type { IchimokuRenderState } from '../../indicators/ichimokuState'
5
6
  import { createIchimokuStateKey, EMPTY_ICHIMOKU_STATE } from '../../indicators/ichimokuState'
@@ -14,8 +15,6 @@ const KIJUN_COLOR = '#2563eb'
14
15
  const SPAN_A_COLOR = '#16a34a'
15
16
  const SPAN_B_COLOR = '#dc2626'
16
17
  const CHIKOU_COLOR = '#7c3aed'
17
- const CLOUD_BULL = 'rgba(34, 197, 94, 0.15)'
18
- const CLOUD_BEAR = 'rgba(239, 68, 68, 0.15)'
19
18
 
20
19
  type Point = { x: number; y: number }
21
20
 
@@ -58,6 +57,7 @@ export function createIchimokuRendererPlugin(options: IchimokuRendererOptions =
58
57
 
59
58
  draw(context: RenderContext) {
60
59
  const { ctx, pane, range, scrollLeft, kLineCenters, lineWebGLSurface } = context
60
+ const colors = resolveThemeColors(context.theme, context.isAsiaMarket, context.colorPresetSettings)
61
61
  const stateKey = resolveKey()
62
62
  if (!stateKey) return
63
63
  const state = pluginHost?.getSharedState<IchimokuRenderState>(stateKey)
@@ -94,7 +94,7 @@ export function createIchimokuRendererPlugin(options: IchimokuRendererOptions =
94
94
  if (params.showCloud && cloudSegs.length >= 2) {
95
95
  ctx.save()
96
96
  ctx.translate(-scrollLeft, 0)
97
- fillCloud(ctx, cloudSegs)
97
+ fillCloud(ctx, cloudSegs, colors.candleUpBody, colors.candleDownBody)
98
98
  ctx.restore()
99
99
  }
100
100
 
@@ -153,11 +153,16 @@ function drawLine(ctx: CanvasRenderingContext2D, pts: Point[], color: string): v
153
153
  function fillCloud(
154
154
  ctx: CanvasRenderingContext2D,
155
155
  segs: { x: number; ya: number; yb: number; bull: boolean }[],
156
+ bullColor: string,
157
+ bearColor: string,
158
+ alpha = 0.15,
156
159
  ): void {
160
+ ctx.save()
161
+ ctx.globalAlpha = alpha
157
162
  for (let i = 0; i < segs.length - 1; i++) {
158
163
  const a = segs[i]!
159
164
  const b = segs[i + 1]!
160
- ctx.fillStyle = a.bull ? CLOUD_BULL : CLOUD_BEAR
165
+ ctx.fillStyle = a.bull ? bullColor : bearColor
161
166
  ctx.beginPath()
162
167
  ctx.moveTo(a.x, a.ya)
163
168
  ctx.lineTo(b.x, b.ya)
@@ -166,6 +171,7 @@ function fillCloud(
166
171
  ctx.closePath()
167
172
  ctx.fill()
168
173
  }
174
+ ctx.restore()
169
175
  }
170
176
 
171
177
  export function getIchimokuTitleInfo(
@@ -218,7 +218,7 @@ function pushComparisonLegendRows(
218
218
  const sign = pct > 0 ? '+' : ''
219
219
  const pctText = `${sign}${pct.toFixed(2)}%`
220
220
  overlayCtx.fillStyle = pct > 0 ? colors.candleUpBody : pct < 0 ? colors.candleDownBody : colors.text.primary
221
- overlayCtx.fillText(pctText, x, y + 1)
221
+ overlayCtx.fillText(pctText, x, y)
222
222
  },
223
223
  })
224
224
  }
@@ -1,5 +1,6 @@
1
1
  import type { RendererPluginWithHost, RenderContext, PluginHost } from '../../../plugin'
2
2
  import { RENDERER_PRIORITY } from '../../../plugin'
3
+ import { resolveThemeColors } from '../../../tokens'
3
4
  import type { KLineData } from '../../../types/price'
4
5
  import type { SARRenderState } from '../../indicators/sarState'
5
6
  import { createSARStateKey, EMPTY_SAR_STATE } from '../../indicators/sarState'
@@ -9,8 +10,6 @@ import type { IndicatorScheduler, SARSchedulerConfig } from '../../indicators/sc
9
10
  import { calcSARData } from '../../indicators/calculators'
10
11
  import { createValuePointVisibleStateComposer } from '../../indicators/visibleStateComposers'
11
12
 
12
- const SAR_UP_COLOR = '#22c55e'
13
- const SAR_DOWN_COLOR = '#ef4444'
14
13
  const DOT_RADIUS = 1.5
15
14
  const TAU = Math.PI * 2
16
15
 
@@ -59,6 +58,7 @@ export function createSARRendererPlugin(options: SARRendererOptions = {}): Rende
59
58
 
60
59
  draw(context: RenderContext) {
61
60
  const { ctx, pane, range, scrollLeft, kLineCenters } = context
61
+ const colors = resolveThemeColors(context.theme, context.isAsiaMarket, context.colorPresetSettings)
62
62
 
63
63
  const stateKey = resolveKey()
64
64
  if (!stateKey) return
@@ -77,7 +77,7 @@ export function createSARRendererPlugin(options: SARRendererOptions = {}): Rende
77
77
  const centerX = kLineCenters[i - range.start]
78
78
  if (centerX === undefined) continue
79
79
  const y = pane.yAxis.priceToY(point.value)
80
- ctx.fillStyle = point.trend === 'up' ? SAR_UP_COLOR : SAR_DOWN_COLOR
80
+ ctx.fillStyle = point.trend === 'up' ? colors.candleUpBody : colors.candleDownBody
81
81
  ctx.beginPath()
82
82
  ctx.arc(centerX, y, DOT_RADIUS, 0, TAU)
83
83
  ctx.fill()
@@ -1,5 +1,6 @@
1
1
  import type { RendererPluginWithHost, RenderContext, PluginHost } from '../../../plugin'
2
2
  import { RENDERER_PRIORITY } from '../../../plugin'
3
+ import { resolveThemeColors } from '../../../tokens'
3
4
  import type { KLineData } from '../../../types/price'
4
5
  import type { SuperTrendRenderState } from '../../indicators/supertrendState'
5
6
  import { createSuperTrendStateKey, EMPTY_SUPERTREND_STATE } from '../../indicators/supertrendState'
@@ -9,9 +10,6 @@ import type { IndicatorScheduler, SuperTrendSchedulerConfig } from '../../indica
9
10
  import { calcSuperTrendData } from '../../indicators/calculators'
10
11
  import { createValuePointVisibleStateComposer } from '../../indicators/visibleStateComposers'
11
12
 
12
- const ST_UP_COLOR = '#22c55e'
13
- const ST_DOWN_COLOR = '#ef4444'
14
-
15
13
  export interface SuperTrendRendererOptions {
16
14
  paneId?: string
17
15
  }
@@ -51,6 +49,7 @@ export function createSuperTrendRendererPlugin(options: SuperTrendRendererOption
51
49
 
52
50
  draw(context: RenderContext) {
53
51
  const { ctx, pane, range, scrollLeft, kLineCenters } = context
52
+ const colors = resolveThemeColors(context.theme, context.isAsiaMarket, context.colorPresetSettings)
54
53
  const stateKey = resolveKey()
55
54
  if (!stateKey) return
56
55
  const state = pluginHost?.getSharedState<SuperTrendRenderState>(stateKey)
@@ -77,7 +76,7 @@ export function createSuperTrendRendererPlugin(options: SuperTrendRendererOption
77
76
  const y = pane.yAxis.priceToY(point.value)
78
77
 
79
78
  if (prevX !== null && prevTrend === point.trend) {
80
- ctx.strokeStyle = point.trend === 'up' ? ST_UP_COLOR : ST_DOWN_COLOR
79
+ ctx.strokeStyle = point.trend === 'up' ? colors.candleUpBody : colors.candleDownBody
81
80
  ctx.beginPath()
82
81
  ctx.moveTo(prevX, prevY!)
83
82
  ctx.lineTo(centerX, y)
@@ -118,6 +118,7 @@ function createMockRenderContext(
118
118
  scrollLeft: 0,
119
119
  pane: mockPane,
120
120
  kLineCenters: Array.from({ length: 100 }, (_, i) => i * 10 + 5),
121
+ period: 'daily',
121
122
  ...overrides,
122
123
  } as RenderContext
123
124
  }
@@ -106,6 +106,7 @@ function createMockRenderContext(
106
106
  scrollLeft: 0,
107
107
  pane: mockPane,
108
108
  kLineCenters: Array.from({ length: 100 }, (_, i) => i * 10 + 5),
109
+ period: 'daily',
109
110
  ...overrides,
110
111
  } as RenderContext
111
112
  }
@@ -105,6 +105,7 @@ function createMockRenderContext(
105
105
  scrollLeft: 0,
106
106
  pane: mockPane,
107
107
  kLineCenters: Array.from({ length: 100 }, (_, i) => i * 10 + 5),
108
+ period: 'daily',
108
109
  ...overrides,
109
110
  } as RenderContext
110
111
  }
@@ -104,6 +104,7 @@ function createMockRenderContext(
104
104
  scrollLeft: 0,
105
105
  pane: mockPane,
106
106
  kLineCenters: Array.from({ length: 10 }, (_, i) => i * 10 + 5),
107
+ period: 'daily',
107
108
  ...overrides,
108
109
  } as RenderContext
109
110
  }
@@ -168,6 +168,7 @@ function createMockRenderContext(
168
168
  scrollLeft: 0,
169
169
  pane: mockPane,
170
170
  kLineCenters: Array.from({ length: 100 }, (_, i) => i * 10 + 5),
171
+ period: 'daily',
171
172
  ...overrides,
172
173
  } as RenderContext
173
174
  }
@@ -76,6 +76,7 @@ function createContext(overrides: Partial<RenderContext> = {}): RenderContext {
76
76
  kBarRects: [] as { x: number; width: number }[],
77
77
  yAxisLabels: [],
78
78
  xAxisLabels: [],
79
+ period: 'daily',
79
80
  theme: 'light',
80
81
  ...overrides,
81
82
  }
@@ -56,6 +56,7 @@ export function createTimeAxisRendererPlugin(options: {
56
56
  lineColor: colors.border.dark,
57
57
  drawTopBorder: false,
58
58
  drawBottomBorder: false,
59
+ period: context.period,
59
60
  }, context.theme, context.isAsiaMarket, context.colorPresetSettings)
60
61
 
61
62
  // 绘制来自 xAxisRanges 的时间范围带(先于标签绘制)
package/src/index.ts CHANGED
@@ -1,4 +1,6 @@
1
1
  export * from './reactivity'
2
2
  export * from './controllers'
3
+ export * from './mcp'
3
4
  export { VERSION } from './version'
4
5
  export * from './tokens'
6
+ export { formatTimestamp } from './utils/dateFormat'
@@ -0,0 +1,220 @@
1
+ import type { ToolCall, ToolResult, ControllerDescription, ToolCallHandler } from './types'
2
+
3
+ export interface ChartBridgeOptions {
4
+ wsUrl: string
5
+ onToolCall: ToolCallHandler
6
+ sessionId?: string
7
+ autoReconnect?: boolean
8
+ reconnectDelay?: number
9
+ heartbeatInterval?: number
10
+ wsImpl?: new (url: string) => WebSocket
11
+ }
12
+
13
+ export type ChartBridgeEvent =
14
+ | 'connected'
15
+ | 'disconnected'
16
+ | 'error'
17
+ | 'stateChanged'
18
+
19
+ type MessageHandler = (...args: unknown[]) => void
20
+
21
+ export class ChartBridge {
22
+ readonly sessionId: string
23
+ private readonly autoReconnect: boolean
24
+ private readonly reconnectDelay: number
25
+ private readonly heartbeatInterval: number
26
+ private readonly onToolCall: ToolCallHandler
27
+
28
+ private readonly wsImpl: new (url: string) => WebSocket
29
+ private ws: WebSocket | null = null
30
+ private reconnectTimer: ReturnType<typeof setTimeout> | null = null
31
+ private heartbeatTimer: ReturnType<typeof setInterval> | null = null
32
+ private destroyed = false
33
+
34
+ private listeners = new Map<ChartBridgeEvent, Set<MessageHandler>>()
35
+
36
+ onConnected?: () => void
37
+ onDisconnected?: () => void
38
+ onError?: (err: Error) => void
39
+ onStateChange?: () => void
40
+
41
+ constructor(options: ChartBridgeOptions) {
42
+ this.sessionId = options.sessionId ?? crypto.randomUUID()
43
+ this.autoReconnect = options.autoReconnect ?? true
44
+ this.reconnectDelay = options.reconnectDelay ?? 3000
45
+ this.heartbeatInterval = options.heartbeatInterval ?? 30_000
46
+ this.onToolCall = options.onToolCall
47
+ this.wsImpl = options.wsImpl ?? WebSocket
48
+ this.wsUrl = options.wsUrl
49
+ }
50
+
51
+ private wsUrl: string
52
+
53
+ async connect(): Promise<void> {
54
+ if (this.destroyed) return
55
+ this.disconnect()
56
+
57
+ return new Promise((resolve, reject) => {
58
+ try {
59
+ const ws = new this.wsImpl(this.wsUrl)
60
+
61
+ ws.onopen = () => {
62
+ this.ws = ws
63
+ console.info(
64
+ `[ChartBridge] WS opened → sending register (sessionId=${this.sessionId})`,
65
+ )
66
+ ws.send(JSON.stringify({ type: 'register', sessionId: this.sessionId }))
67
+ this.startHeartbeat()
68
+ this.onConnected?.()
69
+ this.emit('connected')
70
+ resolve()
71
+ }
72
+
73
+ ws.onmessage = (event: MessageEvent) => {
74
+ let msg: Record<string, unknown>
75
+ try {
76
+ msg = JSON.parse(event.data as string)
77
+ } catch {
78
+ return
79
+ }
80
+ this.handleMessage(msg)
81
+ }
82
+
83
+ ws.onclose = () => {
84
+ console.warn(
85
+ `[ChartBridge] WS closed, autoReconnect=${this.autoReconnect}`,
86
+ )
87
+ this.ws = null
88
+ this.stopHeartbeat()
89
+ this.onDisconnected?.()
90
+ this.emit('disconnected')
91
+ if (this.autoReconnect && !this.destroyed) {
92
+ this.scheduleReconnect()
93
+ }
94
+ }
95
+
96
+ ws.onerror = () => {
97
+ console.error(`[ChartBridge] WS error — connection failed`)
98
+ const err = new Error('WebSocket connection failed')
99
+ this.onError?.(err)
100
+ this.emit('error', err)
101
+ reject(err)
102
+ }
103
+ } catch (err) {
104
+ reject(err)
105
+ }
106
+ })
107
+ }
108
+
109
+ disconnect(): void {
110
+ this.ws?.close()
111
+ this.ws = null
112
+ this.cancelReconnect()
113
+ this.stopHeartbeat()
114
+ }
115
+
116
+ destroy(): void {
117
+ this.destroyed = true
118
+ this.disconnect()
119
+ this.listeners.clear()
120
+ }
121
+
122
+ private handleMessage(msg: Record<string, unknown>): void {
123
+ switch (msg.type) {
124
+ case 'registered':
125
+ break
126
+
127
+ case 'tool:call': {
128
+ const call = msg.call as ToolCall
129
+ const requestId = msg.requestId as string
130
+ this.dispatchToolCall(requestId, call)
131
+ break
132
+ }
133
+
134
+ case 'ping': {
135
+ this.ws?.send(JSON.stringify({ type: 'pong' }))
136
+ break
137
+ }
138
+ }
139
+ }
140
+
141
+ private async dispatchToolCall(requestId: string, call: ToolCall): Promise<void> {
142
+ const result = await this.onToolCall(call)
143
+ this.sendResult(requestId, result)
144
+
145
+ if (this.onStateChange) {
146
+ this.onStateChange()
147
+ }
148
+ this.emit('stateChanged')
149
+ }
150
+
151
+ private sendResult(requestId: string, result: ToolResult): void {
152
+ this.ws?.send(
153
+ JSON.stringify({
154
+ type: 'tool:result',
155
+ requestId,
156
+ result,
157
+ }),
158
+ )
159
+ }
160
+
161
+ sendStateUpdate(
162
+ descriptions: Record<string, ControllerDescription>,
163
+ ): void {
164
+ this.ws?.send(
165
+ JSON.stringify({
166
+ type: 'state:update',
167
+ sessionId: this.sessionId,
168
+ descriptions,
169
+ }),
170
+ )
171
+ }
172
+
173
+ private startHeartbeat(): void {
174
+ this.stopHeartbeat()
175
+ this.heartbeatTimer = setInterval(() => {
176
+ if (this.ws?.readyState === 1) {
177
+ this.ws.send(JSON.stringify({ type: 'ping' }))
178
+ }
179
+ }, this.heartbeatInterval)
180
+ }
181
+
182
+ private stopHeartbeat(): void {
183
+ if (this.heartbeatTimer !== null) {
184
+ clearInterval(this.heartbeatTimer)
185
+ this.heartbeatTimer = null
186
+ }
187
+ }
188
+
189
+ private scheduleReconnect(): void {
190
+ this.cancelReconnect()
191
+ this.reconnectTimer = setTimeout(() => {
192
+ if (!this.destroyed) {
193
+ this.connect()
194
+ }
195
+ }, this.reconnectDelay)
196
+ }
197
+
198
+ private cancelReconnect(): void {
199
+ if (this.reconnectTimer !== null) {
200
+ clearTimeout(this.reconnectTimer)
201
+ this.reconnectTimer = null
202
+ }
203
+ }
204
+
205
+ private emit(event: ChartBridgeEvent, ...args: unknown[]): void {
206
+ this.listeners.get(event)?.forEach((fn) => fn(...args))
207
+ }
208
+
209
+ on(event: ChartBridgeEvent, handler: MessageHandler): () => void {
210
+ if (!this.listeners.has(event)) {
211
+ this.listeners.set(event, new Set())
212
+ }
213
+ this.listeners.get(event)!.add(handler)
214
+ return () => this.listeners.get(event)?.delete(handler)
215
+ }
216
+
217
+ off(event: ChartBridgeEvent, handler: MessageHandler): void {
218
+ this.listeners.get(event)?.delete(handler)
219
+ }
220
+ }
@@ -0,0 +1,2 @@
1
+ export { ChartBridge, type ChartBridgeOptions, type ChartBridgeEvent } from './chartBridge'
2
+ export type { ToolCall, ToolResult, ControllerDescription, ToolCallHandler } from './types'
@@ -0,0 +1,19 @@
1
+ export interface ToolCall {
2
+ name: string
3
+ input: Record<string, unknown>
4
+ }
5
+
6
+ export interface ToolResult {
7
+ success: boolean
8
+ error?: string
9
+ data?: unknown
10
+ }
11
+
12
+ export type ToolCallHandler = (call: ToolCall) => ToolResult | Promise<ToolResult>
13
+
14
+ export interface ControllerDescription {
15
+ controllerId: string
16
+ summary: string
17
+ facts: Readonly<Record<string, string | number | boolean | null>>
18
+ warnings?: ReadonlyArray<string>
19
+ }
@@ -273,6 +273,8 @@ export interface RenderContext {
273
273
  ctx: CanvasRenderingContext2D
274
274
  pane: PaneInfo
275
275
  data: unknown[]
276
+ /** K线级别,如 'daily'、'5min'、'15min' */
277
+ period: string
276
278
  comparisonData?: ReadonlyMap<string, ReadonlyArray<KLineData>>
277
279
  comparisonSymbols?: ReadonlyArray<import('../controllers/types').SymbolSpec>
278
280
  comparisonColors?: ReadonlyMap<string, string>
@@ -1,5 +1,6 @@
1
1
  export type {
2
2
  SemanticChartConfig,
3
+ AdjustType,
3
4
  DataConfig,
4
5
  IndicatorsConfig,
5
6
  MainIndicatorConfig,