@363045841yyt/klinechart-core 0.8.1 → 0.8.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/controllers/createChartController.d.ts.map +1 -1
- package/dist/controllers/createChartController.js +31 -0
- package/dist/controllers/createChartController.js.map +1 -1
- package/dist/controllers/types.d.ts +16 -0
- package/dist/controllers/types.d.ts.map +1 -1
- package/dist/data-fetchers/baostock.d.ts +9 -2
- package/dist/data-fetchers/baostock.d.ts.map +1 -1
- package/dist/data-fetchers/baostock.js +78 -9
- package/dist/data-fetchers/baostock.js.map +1 -1
- package/dist/data-fetchers/dataBuffer.d.ts.map +1 -1
- package/dist/data-fetchers/dataBuffer.js +3 -0
- package/dist/data-fetchers/dataBuffer.js.map +1 -1
- package/dist/data-fetchers/fetcherDefinitionRegistry.d.ts +13 -0
- package/dist/data-fetchers/fetcherDefinitionRegistry.d.ts.map +1 -0
- package/dist/data-fetchers/fetcherDefinitionRegistry.js +36 -0
- package/dist/data-fetchers/fetcherDefinitionRegistry.js.map +1 -0
- package/dist/data-fetchers/gotdx.d.ts +10 -0
- package/dist/data-fetchers/gotdx.d.ts.map +1 -0
- package/dist/data-fetchers/gotdx.js +168 -0
- package/dist/data-fetchers/gotdx.js.map +1 -0
- package/dist/data-fetchers/hundred-mock.d.ts +9 -2
- package/dist/data-fetchers/hundred-mock.d.ts.map +1 -1
- package/dist/data-fetchers/hundred-mock.js +92 -7
- package/dist/data-fetchers/hundred-mock.js.map +1 -1
- package/dist/data-fetchers/index.d.ts +7 -4
- package/dist/data-fetchers/index.d.ts.map +1 -1
- package/dist/data-fetchers/index.js +6 -4
- package/dist/data-fetchers/index.js.map +1 -1
- package/dist/data-fetchers/router.d.ts.map +1 -1
- package/dist/data-fetchers/router.js +14 -15
- package/dist/data-fetchers/router.js.map +1 -1
- package/dist/data-fetchers/thousand-mock.d.ts +9 -2
- package/dist/data-fetchers/thousand-mock.d.ts.map +1 -1
- package/dist/data-fetchers/thousand-mock.js +88 -16
- package/dist/data-fetchers/thousand-mock.js.map +1 -1
- package/dist/data-fetchers/tradingview.d.ts +9 -2
- package/dist/data-fetchers/tradingview.d.ts.map +1 -1
- package/dist/data-fetchers/tradingview.js +75 -4
- package/dist/data-fetchers/tradingview.js.map +1 -1
- package/dist/data-fetchers/types.d.ts +21 -0
- package/dist/data-fetchers/types.d.ts.map +1 -0
- package/dist/data-fetchers/types.js +2 -0
- package/dist/data-fetchers/types.js.map +1 -0
- package/dist/engine/data/chartDataManager.d.ts +1 -0
- package/dist/engine/data/chartDataManager.d.ts.map +1 -1
- package/dist/engine/data/chartDataManager.js +3 -0
- package/dist/engine/data/chartDataManager.js.map +1 -1
- package/dist/engine/render/chartRenderer.d.ts.map +1 -1
- package/dist/engine/render/chartRenderer.js +2 -0
- package/dist/engine/render/chartRenderer.js.map +1 -1
- package/dist/engine/renderers/Indicator/ichimoku.d.ts.map +1 -1
- package/dist/engine/renderers/Indicator/ichimoku.js +8 -5
- package/dist/engine/renderers/Indicator/ichimoku.js.map +1 -1
- package/dist/engine/renderers/Indicator/mainIndicatorLegend.js +1 -1
- package/dist/engine/renderers/Indicator/mainIndicatorLegend.js.map +1 -1
- package/dist/engine/renderers/Indicator/sar.d.ts.map +1 -1
- package/dist/engine/renderers/Indicator/sar.js +3 -3
- package/dist/engine/renderers/Indicator/sar.js.map +1 -1
- package/dist/engine/renderers/Indicator/supertrend.d.ts.map +1 -1
- package/dist/engine/renderers/Indicator/supertrend.js +3 -3
- package/dist/engine/renderers/Indicator/supertrend.js.map +1 -1
- package/dist/engine/renderers/timeAxis.d.ts.map +1 -1
- package/dist/engine/renderers/timeAxis.js +1 -0
- package/dist/engine/renderers/timeAxis.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/mcp/chartBridge.d.ts +47 -0
- package/dist/mcp/chartBridge.d.ts.map +1 -0
- package/dist/mcp/chartBridge.js +167 -0
- package/dist/mcp/chartBridge.js.map +1 -0
- package/dist/mcp/index.d.ts +3 -0
- package/dist/mcp/index.d.ts.map +1 -0
- package/dist/mcp/index.js +2 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/mcp/types.d.ts +17 -0
- package/dist/mcp/types.d.ts.map +1 -0
- package/dist/mcp/types.js +2 -0
- package/dist/mcp/types.js.map +1 -0
- package/dist/plugin/types.d.ts +2 -0
- package/dist/plugin/types.d.ts.map +1 -1
- package/dist/plugin/types.js.map +1 -1
- package/dist/semantic/index.d.ts +1 -1
- package/dist/semantic/index.d.ts.map +1 -1
- package/dist/semantic/index.js.map +1 -1
- package/dist/semantic/schema.json +1 -1
- package/dist/semantic/types.d.ts +2 -1
- package/dist/semantic/types.d.ts.map +1 -1
- package/dist/utils/dateFormat.d.ts +25 -0
- package/dist/utils/dateFormat.d.ts.map +1 -1
- package/dist/utils/dateFormat.js +78 -0
- package/dist/utils/dateFormat.js.map +1 -1
- package/dist/utils/kLineDraw/axis.d.ts +2 -0
- package/dist/utils/kLineDraw/axis.d.ts.map +1 -1
- package/dist/utils/kLineDraw/axis.js +11 -6
- package/dist/utils/kLineDraw/axis.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/src/controllers/createChartController.ts +34 -0
- package/src/controllers/types.ts +9 -0
- package/src/data-fetchers/__tests__/dataBuffer.test.ts +5 -2
- package/src/data-fetchers/__tests__/fetcherRegistry.test.ts +192 -0
- package/src/data-fetchers/baostock.ts +54 -22
- package/src/data-fetchers/dataBuffer.ts +6 -0
- package/src/data-fetchers/fetcherDefinitionRegistry.ts +50 -0
- package/src/data-fetchers/gotdx.ts +160 -0
- package/src/data-fetchers/hundred-mock.ts +54 -7
- package/src/data-fetchers/index.ts +19 -4
- package/src/data-fetchers/router.ts +27 -15
- package/src/data-fetchers/thousand-mock.ts +49 -16
- package/src/data-fetchers/tradingview.ts +32 -6
- package/src/data-fetchers/types.ts +27 -0
- package/src/engine/data/chartDataManager.ts +4 -0
- package/src/engine/render/chartRenderer.ts +2 -0
- package/src/engine/renderers/Indicator/ichimoku.ts +10 -4
- package/src/engine/renderers/Indicator/mainIndicatorLegend.ts +1 -1
- package/src/engine/renderers/Indicator/sar.ts +3 -3
- package/src/engine/renderers/Indicator/supertrend.ts +3 -4
- package/src/engine/renderers/__tests__/boll.renderer.test.ts +1 -0
- package/src/engine/renderers/__tests__/ene.renderer.test.ts +1 -0
- package/src/engine/renderers/__tests__/expma.renderer.test.ts +1 -0
- package/src/engine/renderers/__tests__/ma.renderer.test.ts +1 -0
- package/src/engine/renderers/__tests__/mainIndicatorLegend.renderer.test.ts +1 -0
- package/src/engine/renderers/__tests__/yAxis.renderer.test.ts +1 -0
- package/src/engine/renderers/timeAxis.ts +1 -0
- package/src/index.ts +2 -0
- package/src/mcp/chartBridge.ts +220 -0
- package/src/mcp/index.ts +2 -0
- package/src/mcp/types.ts +19 -0
- package/src/plugin/types.ts +2 -0
- package/src/semantic/index.ts +1 -0
- package/src/semantic/schema.json +1 -1
- package/src/semantic/types.ts +3 -1
- package/src/utils/dateFormat.ts +85 -0
- package/src/utils/kLineDraw/axis.ts +13 -6
- package/src/version.ts +1 -1
|
@@ -1,30 +1,63 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { KLineData } from '../controllers/types'
|
|
2
|
+
import { DataFetcher } from './fetcherDefinitionRegistry'
|
|
3
|
+
import type { FetchConfig } from './types'
|
|
2
4
|
|
|
3
|
-
|
|
5
|
+
async function fetchThousandMock(
|
|
6
|
+
_source: string,
|
|
7
|
+
_config: FetchConfig,
|
|
8
|
+
): Promise<ReadonlyArray<KLineData>> {
|
|
4
9
|
console.log('[thousand-mock] generating 10k K-lines')
|
|
5
10
|
const data: KLineData[] = []
|
|
6
11
|
const startTime = new Date('2020-01-01').getTime()
|
|
7
12
|
const dayMs = 24 * 60 * 60 * 1000
|
|
8
|
-
|
|
9
|
-
|
|
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
|
|
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)
|
|
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
|
|
22
|
-
high
|
|
23
|
-
low
|
|
24
|
-
close
|
|
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 {
|
|
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
|
-
|
|
14
|
-
|
|
15
|
+
const ADJUST_TO_TV: Record<string, string | undefined> = {
|
|
16
|
+
qfq: 'dividends',
|
|
17
|
+
splits: 'splits',
|
|
18
|
+
none: 'none',
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const BASE_URL = 'http://localhost:8000'
|
|
22
|
+
|
|
23
|
+
async function fetchTradingview(
|
|
24
|
+
_source: string,
|
|
25
|
+
config: FetchConfig,
|
|
26
|
+
): Promise<ReadonlyArray<KLineData>> {
|
|
15
27
|
const timeframe = PERIOD_TO_TIMEFRAME[config.period] ?? '1d'
|
|
16
28
|
const startDate = config.startDate.split('T')[0]
|
|
17
29
|
const endDate = config.endDate.split('T')[0]
|
|
18
|
-
|
|
30
|
+
const tvAdjust = ADJUST_TO_TV[config.adjust]
|
|
19
31
|
const exchangeQ = config.exchange ? `&exchange=${config.exchange}` : ''
|
|
20
|
-
const
|
|
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 ?
|
|
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
|
|
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' ?
|
|
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' ?
|
|
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)
|
|
@@ -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
|
@@ -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
|
+
}
|
package/src/mcp/index.ts
ADDED
package/src/mcp/types.ts
ADDED
|
@@ -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
|
+
}
|
package/src/plugin/types.ts
CHANGED
|
@@ -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>
|