@363045841yyt/klinechart-core 0.7.3 → 0.7.5-alpha.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +201 -201
- package/README.zh-CN.md +201 -201
- package/dist/engine/renderers/webgl/candleSurface.js +47 -47
- package/dist/version.d.ts +1 -1
- package/dist/version.d.ts.map +1 -1
- package/dist/version.js +1 -2
- package/dist/version.js.map +1 -1
- package/package.json +129 -122
- package/src/__tests__/signal.test.ts +124 -124
- package/src/config/chartSettings.ts +66 -66
- package/src/controllers/__tests__/drawing.test.ts +214 -214
- package/src/controllers/__tests__/indicatorSelector.test.ts +481 -481
- package/src/controllers/__tests__/toolbar.test.ts +225 -225
- package/src/controllers/createChartController.ts +665 -665
- package/src/controllers/createDrawingController.ts +96 -96
- package/src/controllers/createIndicatorSelectorController.ts +307 -307
- package/src/controllers/createToolbarController.ts +146 -146
- package/src/controllers/index.ts +19 -19
- package/src/controllers/types.ts +284 -284
- package/src/engine/__tests__/chart.dpr.test.ts +401 -401
- package/src/engine/__tests__/paneRenderer.resize.test.ts +92 -92
- package/src/engine/chart-store.ts +121 -121
- package/src/engine/chart.d.ts +617 -617
- package/src/engine/chart.ts +2815 -2815
- package/src/engine/controller/__tests__/interaction.dpr.test.ts +259 -259
- package/src/engine/controller/interaction.ts +722 -722
- package/src/engine/controller/markerInteraction.ts +130 -130
- package/src/engine/controller/pinchTracker.ts +82 -82
- package/src/engine/controller/tooltipPosition.ts +48 -48
- package/src/engine/draw/__tests__/pixelAlign.spec.ts +176 -176
- package/src/engine/draw/pixelAlign.ts +259 -259
- package/src/engine/drawing/index.ts +655 -655
- package/src/engine/drawing/interaction.ts +842 -842
- package/src/engine/drawing/plugin.ts +343 -343
- package/src/engine/indicators/__tests__/__fixtures__/golden/atr.json +38 -38
- package/src/engine/indicators/__tests__/__fixtures__/golden/dema.json +14 -14
- package/src/engine/indicators/__tests__/__fixtures__/golden/hma.json +14 -14
- package/src/engine/indicators/__tests__/__fixtures__/golden/index.ts +55 -55
- package/src/engine/indicators/__tests__/__fixtures__/golden/kama.json +14 -14
- package/src/engine/indicators/__tests__/__fixtures__/golden/tema.json +14 -14
- package/src/engine/indicators/__tests__/__fixtures__/golden/wma.json +40 -40
- package/src/engine/indicators/__tests__/__fixtures__/synthetic.ts +65 -65
- package/src/engine/indicators/__tests__/_propertyAssertions.ts +76 -76
- package/src/engine/indicators/__tests__/atr.test.ts +153 -153
- package/src/engine/indicators/__tests__/calculators.test.ts +614 -614
- package/src/engine/indicators/__tests__/cmf-mfi.test.ts +100 -100
- package/src/engine/indicators/__tests__/dema.test.ts +73 -73
- package/src/engine/indicators/__tests__/donchian.test.ts +70 -70
- package/src/engine/indicators/__tests__/hma.test.ts +73 -73
- package/src/engine/indicators/__tests__/ichimoku.test.ts +105 -105
- package/src/engine/indicators/__tests__/kama.test.ts +80 -80
- package/src/engine/indicators/__tests__/keltner.test.ts +65 -65
- package/src/engine/indicators/__tests__/pivot-fib.test.ts +110 -110
- package/src/engine/indicators/__tests__/roc.test.ts +68 -68
- package/src/engine/indicators/__tests__/sar.test.ts +86 -86
- package/src/engine/indicators/__tests__/scheduler.test.ts +831 -831
- package/src/engine/indicators/__tests__/soa.test.ts +533 -533
- package/src/engine/indicators/__tests__/structure.test.ts +110 -110
- package/src/engine/indicators/__tests__/supertrend.test.ts +65 -65
- package/src/engine/indicators/__tests__/tema.test.ts +68 -68
- package/src/engine/indicators/__tests__/trix.test.ts +70 -70
- package/src/engine/indicators/__tests__/volatility.test.ts +117 -117
- package/src/engine/indicators/__tests__/volume.test.ts +115 -115
- package/src/engine/indicators/__tests__/volumeProfile.test.ts +74 -74
- package/src/engine/indicators/__tests__/vwap.test.ts +69 -69
- package/src/engine/indicators/__tests__/wma.test.ts +112 -112
- package/src/engine/indicators/__tests__/zones.test.ts +95 -95
- package/src/engine/indicators/atrState.ts +27 -27
- package/src/engine/indicators/bollState.ts +51 -51
- package/src/engine/indicators/calculators.ts +2593 -2593
- package/src/engine/indicators/cciState.ts +25 -25
- package/src/engine/indicators/chaikinVolState.ts +32 -32
- package/src/engine/indicators/cmfState.ts +27 -27
- package/src/engine/indicators/demaState.ts +27 -27
- package/src/engine/indicators/donchianState.ts +43 -43
- package/src/engine/indicators/eneState.ts +43 -43
- package/src/engine/indicators/expmaState.ts +43 -43
- package/src/engine/indicators/fastkState.ts +25 -25
- package/src/engine/indicators/fibState.ts +41 -41
- package/src/engine/indicators/hmaState.ts +27 -27
- package/src/engine/indicators/hvState.ts +28 -28
- package/src/engine/indicators/ichimokuState.ts +70 -70
- package/src/engine/indicators/indicator.worker.ts +169 -169
- package/src/engine/indicators/indicatorDefinitionRegistry.ts +62 -62
- package/src/engine/indicators/indicatorMetadata.ts +110 -110
- package/src/engine/indicators/indicatorRegistry.ts +106 -106
- package/src/engine/indicators/indicatorRuntime.ts +1548 -1548
- package/src/engine/indicators/kamaState.ts +34 -34
- package/src/engine/indicators/keltnerState.ts +49 -49
- package/src/engine/indicators/kstState.ts +42 -42
- package/src/engine/indicators/maState.ts +36 -36
- package/src/engine/indicators/macdState.ts +76 -76
- package/src/engine/indicators/mfiState.ts +27 -27
- package/src/engine/indicators/momState.ts +25 -25
- package/src/engine/indicators/obvState.ts +25 -25
- package/src/engine/indicators/parkinsonState.ts +28 -28
- package/src/engine/indicators/pivotState.ts +51 -51
- package/src/engine/indicators/pvtState.ts +25 -25
- package/src/engine/indicators/rocState.ts +27 -27
- package/src/engine/indicators/rsiState.ts +65 -65
- package/src/engine/indicators/sarState.ts +41 -41
- package/src/engine/indicators/scheduler.ts +1205 -1205
- package/src/engine/indicators/soa.ts +352 -352
- package/src/engine/indicators/stateComposer.ts +1262 -1262
- package/src/engine/indicators/stochState.ts +26 -26
- package/src/engine/indicators/structureState.ts +69 -69
- package/src/engine/indicators/supertrendState.ts +37 -37
- package/src/engine/indicators/temaState.ts +27 -27
- package/src/engine/indicators/trixState.ts +35 -35
- package/src/engine/indicators/vmaState.ts +27 -27
- package/src/engine/indicators/volumeProfileState.ts +63 -63
- package/src/engine/indicators/vwapState.ts +29 -29
- package/src/engine/indicators/wmaState.ts +27 -27
- package/src/engine/indicators/wmsrState.ts +25 -25
- package/src/engine/indicators/workerProtocol.ts +613 -613
- package/src/engine/indicators/zonesState.ts +47 -47
- package/src/engine/layout/pane.ts +161 -161
- package/src/engine/marker/registry.ts +265 -265
- package/src/engine/paneRenderer.ts +169 -169
- package/src/engine/renderers/Indicator/atr.ts +237 -237
- package/src/engine/renderers/Indicator/boll.ts +317 -317
- package/src/engine/renderers/Indicator/cci.ts +275 -275
- package/src/engine/renderers/Indicator/chaikinVol.ts +138 -138
- package/src/engine/renderers/Indicator/cmf.ts +137 -137
- package/src/engine/renderers/Indicator/dema.ts +136 -136
- package/src/engine/renderers/Indicator/donchian.ts +137 -137
- package/src/engine/renderers/Indicator/ene.ts +271 -271
- package/src/engine/renderers/Indicator/expma.ts +197 -197
- package/src/engine/renderers/Indicator/fastk.ts +316 -316
- package/src/engine/renderers/Indicator/fib.ts +141 -141
- package/src/engine/renderers/Indicator/hma.ts +136 -136
- package/src/engine/renderers/Indicator/hv.ts +124 -124
- package/src/engine/renderers/Indicator/ichimoku.ts +181 -181
- package/src/engine/renderers/Indicator/index.ts +241 -241
- package/src/engine/renderers/Indicator/indicatorData.ts +650 -650
- package/src/engine/renderers/Indicator/kama.ts +136 -136
- package/src/engine/renderers/Indicator/keltner.ts +137 -137
- package/src/engine/renderers/Indicator/kst.ts +302 -302
- package/src/engine/renderers/Indicator/ma.ts +200 -200
- package/src/engine/renderers/Indicator/macd.ts +477 -477
- package/src/engine/renderers/Indicator/macdLegend.ts +141 -141
- package/src/engine/renderers/Indicator/mainIndicatorLegend.ts +272 -272
- package/src/engine/renderers/Indicator/mfi.ts +142 -142
- package/src/engine/renderers/Indicator/mom.ts +311 -311
- package/src/engine/renderers/Indicator/obv.ts +123 -123
- package/src/engine/renderers/Indicator/parkinson.ts +124 -124
- package/src/engine/renderers/Indicator/pivot.ts +131 -131
- package/src/engine/renderers/Indicator/pvt.ts +123 -123
- package/src/engine/renderers/Indicator/roc.ts +143 -143
- package/src/engine/renderers/Indicator/rsi.ts +390 -390
- package/src/engine/renderers/Indicator/sar.ts +113 -113
- package/src/engine/renderers/Indicator/scale/atr_scale.ts +19 -19
- package/src/engine/renderers/Indicator/scale/cci_scale.ts +19 -19
- package/src/engine/renderers/Indicator/scale/fastk_scale.ts +19 -19
- package/src/engine/renderers/Indicator/scale/indicator_scale.ts +204 -204
- package/src/engine/renderers/Indicator/scale/kst_scale.ts +19 -19
- package/src/engine/renderers/Indicator/scale/macd_scale.ts +22 -22
- package/src/engine/renderers/Indicator/scale/mom_scale.ts +19 -19
- package/src/engine/renderers/Indicator/scale/rsi_scale.ts +19 -19
- package/src/engine/renderers/Indicator/scale/stoch_scale.ts +19 -19
- package/src/engine/renderers/Indicator/scale/volume_scale.ts +26 -26
- package/src/engine/renderers/Indicator/scale/wmsr_scale.ts +19 -19
- package/src/engine/renderers/Indicator/stoch.ts +359 -359
- package/src/engine/renderers/Indicator/structure.ts +126 -126
- package/src/engine/renderers/Indicator/subPaneConfig.ts +265 -265
- package/src/engine/renderers/Indicator/supertrend.ts +115 -115
- package/src/engine/renderers/Indicator/tema.ts +136 -136
- package/src/engine/renderers/Indicator/trix.ts +158 -158
- package/src/engine/renderers/Indicator/vma.ts +124 -124
- package/src/engine/renderers/Indicator/volumeProfile.ts +125 -125
- package/src/engine/renderers/Indicator/vwap.ts +123 -123
- package/src/engine/renderers/Indicator/wma.ts +136 -136
- package/src/engine/renderers/Indicator/wmsr.ts +328 -328
- package/src/engine/renderers/Indicator/zones.ts +104 -104
- package/src/engine/renderers/__tests__/boll.renderer.test.ts +314 -314
- package/src/engine/renderers/__tests__/ene.renderer.test.ts +305 -305
- package/src/engine/renderers/__tests__/expma.renderer.test.ts +279 -279
- package/src/engine/renderers/__tests__/ma.renderer.test.ts +426 -426
- package/src/engine/renderers/__tests__/mainIndicatorLegend.renderer.test.ts +502 -502
- package/src/engine/renderers/__tests__/yAxis.renderer.test.ts +173 -173
- package/src/engine/renderers/candle.ts +459 -459
- package/src/engine/renderers/crosshair.ts +69 -69
- package/src/engine/renderers/customMarkers.ts +162 -162
- package/src/engine/renderers/extremaMarkers.ts +246 -246
- package/src/engine/renderers/gridLines.ts +90 -90
- package/src/engine/renderers/lastPrice.ts +97 -97
- package/src/engine/renderers/paneTitle.ts +136 -136
- package/src/engine/renderers/subVolume.ts +236 -236
- package/src/engine/renderers/timeAxis.ts +121 -121
- package/src/engine/renderers/webgl/candleSurface.ts +955 -955
- package/src/engine/renderers/webgl/sharedWebGLSurface.ts +146 -146
- package/src/engine/renderers/yAxis.ts +105 -105
- package/src/engine/scale/__tests__/logFormula.spec.ts +148 -148
- package/src/engine/scale/logFormula.ts +130 -130
- package/src/engine/scale/price.ts +39 -39
- package/src/engine/scale/priceScale.ts +264 -264
- package/src/engine/subPaneManager.ts +427 -427
- package/src/engine/theme/colors.ts +642 -642
- package/src/engine/theme/fonts.ts +20 -20
- package/src/engine/utils/klineConfig.ts +49 -49
- package/src/engine/utils/tickCount.ts +11 -11
- package/src/engine/utils/tickPosition.ts +214 -214
- package/src/engine/utils/zoom.ts +83 -83
- package/src/engine/viewport/viewport.ts +67 -67
- package/src/index.ts +3 -3
- package/src/plugin/ConfigManager.ts +93 -93
- package/src/plugin/EventBus.ts +77 -77
- package/src/plugin/HookSystem.ts +106 -106
- package/src/plugin/PluginHost.ts +243 -243
- package/src/plugin/PluginRegistry.ts +92 -92
- package/src/plugin/StateStore.ts +73 -73
- package/src/plugin/index.ts +19 -19
- package/src/plugin/rendererPluginManager.ts +368 -368
- package/src/plugin/stateKeys.ts +8 -8
- package/src/plugin/types.ts +526 -526
- package/src/reactivity/index.ts +2 -2
- package/src/reactivity/signal.ts +119 -119
- package/src/semantic/controller.ts +251 -251
- package/src/semantic/drawShape.ts +260 -260
- package/src/semantic/index.ts +28 -28
- package/src/semantic/schema.json +256 -256
- package/src/semantic/types.ts +251 -251
- package/src/semantic/validator.ts +349 -349
- package/src/types/kLine.ts +13 -13
- package/src/types/price.ts +56 -56
- package/src/types/volumePrice.ts +33 -33
- package/src/utils/dateFormat.ts +208 -208
- package/src/utils/kLineDraw/axis.ts +562 -562
- package/src/utils/priceToY.ts +34 -34
- package/src/utils/volumePrice.ts +202 -202
- package/src/version.ts +1 -1
|
@@ -1,459 +1,459 @@
|
|
|
1
|
-
import type { RendererPlugin, RenderContext } from '../../plugin'
|
|
2
|
-
import { RENDERER_PRIORITY } from '../../plugin'
|
|
3
|
-
import type { KLineData } from '../../types/price'
|
|
4
|
-
import { getKLineTrend, type kLineTrend } from '../../types/kLine'
|
|
5
|
-
import { createAlignedKLineFromPx, createVerticalLineRect } from '../draw/pixelAlign'
|
|
6
|
-
import { getColors, type PriceColors, type VolumePriceColors } from '../theme/colors'
|
|
7
|
-
import { getPhysicalKLineConfig } from '../chart'
|
|
8
|
-
import { VolumePriceRelation } from '../../types/volumePrice'
|
|
9
|
-
import { analyzeVolumePriceRelationBatch, DEFAULT_VOLUME_PRICE_CONFIG } from '../../utils/volumePrice'
|
|
10
|
-
import type { MarkerManager } from '../marker/registry'
|
|
11
|
-
|
|
12
|
-
type CandleRenderData = {
|
|
13
|
-
i: number
|
|
14
|
-
aligned: ReturnType<typeof createAlignedKLineFromPx>
|
|
15
|
-
trend: kLineTrend
|
|
16
|
-
openY: number
|
|
17
|
-
closeY: number
|
|
18
|
-
highY: number
|
|
19
|
-
lowY: number
|
|
20
|
-
alignedHighY: number
|
|
21
|
-
alignedLowY: number
|
|
22
|
-
e: KLineData
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
type PreparedCandles = {
|
|
26
|
-
upKLines: CandleRenderData[]
|
|
27
|
-
downKLines: CandleRenderData[]
|
|
28
|
-
upBodyBuf: Float32Array
|
|
29
|
-
upBodyCount: number
|
|
30
|
-
downBodyBuf: Float32Array
|
|
31
|
-
downBodyCount: number
|
|
32
|
-
upWickBuf: Float32Array
|
|
33
|
-
upWickCount: number
|
|
34
|
-
downWickBuf: Float32Array
|
|
35
|
-
downWickCount: number
|
|
36
|
-
wickWidth: number
|
|
37
|
-
relations: VolumePriceRelation[] | null
|
|
38
|
-
showVolumePriceMarkers: boolean
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* 创建 K 线蜡烛图渲染器插件
|
|
43
|
-
*/
|
|
44
|
-
export function createCandleRenderer(): RendererPlugin {
|
|
45
|
-
return {
|
|
46
|
-
name: 'candle',
|
|
47
|
-
version: '1.0.0',
|
|
48
|
-
description: 'K线蜡烛图渲染器',
|
|
49
|
-
debugName: 'K线',
|
|
50
|
-
paneId: 'main',
|
|
51
|
-
priority: RENDERER_PRIORITY.MAIN,
|
|
52
|
-
|
|
53
|
-
draw(context: RenderContext) {
|
|
54
|
-
const { ctx, pane, data, range, scrollLeft, kWidth, kGap, dpr, kLinePositions, markerManager, settings } = context
|
|
55
|
-
const colors = getColors(context.theme)
|
|
56
|
-
const klineData = data as KLineData[]
|
|
57
|
-
if (!klineData.length) return
|
|
58
|
-
|
|
59
|
-
const prepared = prepareCandles({
|
|
60
|
-
pane,
|
|
61
|
-
data: klineData,
|
|
62
|
-
range,
|
|
63
|
-
kWidth,
|
|
64
|
-
kGap,
|
|
65
|
-
dpr,
|
|
66
|
-
kLinePositions,
|
|
67
|
-
settings,
|
|
68
|
-
})
|
|
69
|
-
|
|
70
|
-
const usedWebGL = drawCandlesWithWebGL(context, prepared, colors.PRICE)
|
|
71
|
-
if (!usedWebGL) {
|
|
72
|
-
drawCandlesWithCanvas2D(ctx, scrollLeft, prepared, colors.PRICE)
|
|
73
|
-
} else {
|
|
74
|
-
compositeWebGLToMainCanvas(ctx, context)
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
drawVolumePriceMarkers(context, prepared, markerManager as MarkerManager | undefined)
|
|
78
|
-
},
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function prepareCandles(args: {
|
|
83
|
-
pane: RenderContext['pane']
|
|
84
|
-
data: KLineData[]
|
|
85
|
-
range: { start: number; end: number }
|
|
86
|
-
kWidth: number
|
|
87
|
-
kGap: number
|
|
88
|
-
dpr: number
|
|
89
|
-
kLinePositions: number[]
|
|
90
|
-
settings?: RenderContext['settings']
|
|
91
|
-
}): PreparedCandles {
|
|
92
|
-
const { pane, data, range, kWidth, kGap, dpr, kLinePositions, settings } = args
|
|
93
|
-
const { kWidthPx } = getPhysicalKLineConfig(kWidth, kGap, dpr)
|
|
94
|
-
const showVolumePriceMarkers = settings?.showVolumePriceMarkers !== false
|
|
95
|
-
const relations = showVolumePriceMarkers
|
|
96
|
-
? analyzeVolumePriceRelationBatch(data, range.start, range.end, DEFAULT_VOLUME_PRICE_CONFIG)
|
|
97
|
-
: null
|
|
98
|
-
|
|
99
|
-
const upKLines: CandleRenderData[] = []
|
|
100
|
-
const downKLines: CandleRenderData[] = []
|
|
101
|
-
const maxRects = Math.max(1, range.end - range.start)
|
|
102
|
-
const upBodyBuf = new Float32Array(maxRects * 4)
|
|
103
|
-
const downBodyBuf = new Float32Array(maxRects * 4)
|
|
104
|
-
const upWickBuf = new Float32Array(maxRects * 2 * 4)
|
|
105
|
-
const downWickBuf = new Float32Array(maxRects * 2 * 4)
|
|
106
|
-
let upBodyCount = 0
|
|
107
|
-
let downBodyCount = 0
|
|
108
|
-
let upWickCount = 0
|
|
109
|
-
let downWickCount = 0
|
|
110
|
-
|
|
111
|
-
// 预取 displayRange,避免循环内每根 K 线 4 次 getDisplayRange()
|
|
112
|
-
const { maxPrice, minPrice } = pane.yAxis.getDisplayRange()
|
|
113
|
-
const paddingTop = pane.yAxis.getPaddingTop()
|
|
114
|
-
const paddingBottom = pane.yAxis.getPaddingBottom()
|
|
115
|
-
const viewHeight = Math.max(1, pane.height - paddingTop - paddingBottom)
|
|
116
|
-
const isLinear = pane.yAxis.getScaleType() !== 'log'
|
|
117
|
-
let fastPriceToY: (price: number) => number
|
|
118
|
-
if (isLinear) {
|
|
119
|
-
const priceRange = maxPrice - minPrice || 1
|
|
120
|
-
const scaleK = viewHeight / priceRange
|
|
121
|
-
const scaleB = paddingTop + viewHeight
|
|
122
|
-
fastPriceToY = (price: number) => scaleB - (price - minPrice) * scaleK
|
|
123
|
-
} else {
|
|
124
|
-
// 对数模式回退到标准方法
|
|
125
|
-
fastPriceToY = (price: number) => pane.yAxis.priceToY(price)
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
for (let i = range.start; i < range.end && i < data.length; i++) {
|
|
129
|
-
const e = data[i]
|
|
130
|
-
if (!e) continue
|
|
131
|
-
|
|
132
|
-
const openY = fastPriceToY(e.open)
|
|
133
|
-
const closeY = fastPriceToY(e.close)
|
|
134
|
-
const highY = fastPriceToY(e.high)
|
|
135
|
-
const lowY = fastPriceToY(e.low)
|
|
136
|
-
|
|
137
|
-
const leftLogical = kLinePositions[i - range.start]
|
|
138
|
-
if (leftLogical === undefined) continue
|
|
139
|
-
|
|
140
|
-
const alignY = (logical: number) => Math.round(logical * dpr) / dpr
|
|
141
|
-
const alignedOpenY = alignY(openY)
|
|
142
|
-
const alignedCloseY = alignY(closeY)
|
|
143
|
-
const alignedHighY = alignY(highY)
|
|
144
|
-
const alignedLowY = alignY(lowY)
|
|
145
|
-
const alignedRawRectY = Math.min(alignedOpenY, alignedCloseY)
|
|
146
|
-
const alignedRawRectH = Math.max(Math.abs(alignedOpenY - alignedCloseY), 1)
|
|
147
|
-
|
|
148
|
-
const roundedLeftPx = Math.round(leftLogical * dpr)
|
|
149
|
-
const aligned = createAlignedKLineFromPx(
|
|
150
|
-
roundedLeftPx,
|
|
151
|
-
alignedRawRectY,
|
|
152
|
-
kWidthPx,
|
|
153
|
-
alignedRawRectH,
|
|
154
|
-
dpr
|
|
155
|
-
)
|
|
156
|
-
|
|
157
|
-
const trend: kLineTrend = getKLineTrend(e)
|
|
158
|
-
const renderData: CandleRenderData = {
|
|
159
|
-
i,
|
|
160
|
-
aligned,
|
|
161
|
-
trend,
|
|
162
|
-
openY,
|
|
163
|
-
closeY,
|
|
164
|
-
highY,
|
|
165
|
-
lowY,
|
|
166
|
-
alignedHighY,
|
|
167
|
-
alignedLowY,
|
|
168
|
-
e,
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
const bodyRect = aligned.bodyRect
|
|
172
|
-
const targetKLines = trend === 'up' ? upKLines : downKLines
|
|
173
|
-
const isUp = trend === 'up'
|
|
174
|
-
|
|
175
|
-
targetKLines.push(renderData)
|
|
176
|
-
|
|
177
|
-
if (isUp) {
|
|
178
|
-
const off = upBodyCount++ * 4
|
|
179
|
-
upBodyBuf[off] = bodyRect.x
|
|
180
|
-
upBodyBuf[off + 1] = bodyRect.y
|
|
181
|
-
upBodyBuf[off + 2] = bodyRect.width
|
|
182
|
-
upBodyBuf[off + 3] = bodyRect.height
|
|
183
|
-
} else {
|
|
184
|
-
const off = downBodyCount++ * 4
|
|
185
|
-
downBodyBuf[off] = bodyRect.x
|
|
186
|
-
downBodyBuf[off + 1] = bodyRect.y
|
|
187
|
-
downBodyBuf[off + 2] = bodyRect.width
|
|
188
|
-
downBodyBuf[off + 3] = bodyRect.height
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
const bodyTop = bodyRect.y
|
|
192
|
-
const bodyBottom = bodyRect.y + bodyRect.height
|
|
193
|
-
const bodyHigh = Math.max(e.open, e.close)
|
|
194
|
-
const bodyLow = Math.min(e.open, e.close)
|
|
195
|
-
|
|
196
|
-
if (e.high > bodyHigh) {
|
|
197
|
-
const wick = createVerticalLineRect(aligned.wickRect.x, alignedHighY, bodyTop, dpr)
|
|
198
|
-
if (wick) {
|
|
199
|
-
const buf = isUp ? upWickBuf : downWickBuf
|
|
200
|
-
const idx = isUp ? upWickCount++ : downWickCount++
|
|
201
|
-
const off = idx * 4
|
|
202
|
-
buf[off] = wick.x
|
|
203
|
-
buf[off + 1] = wick.y
|
|
204
|
-
buf[off + 2] = wick.width
|
|
205
|
-
buf[off + 3] = wick.height
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
if (e.low < bodyLow) {
|
|
209
|
-
const wick = createVerticalLineRect(aligned.wickRect.x, bodyBottom, alignedLowY, dpr)
|
|
210
|
-
if (wick) {
|
|
211
|
-
const buf = isUp ? upWickBuf : downWickBuf
|
|
212
|
-
const idx = isUp ? upWickCount++ : downWickCount++
|
|
213
|
-
const off = idx * 4
|
|
214
|
-
buf[off] = wick.x
|
|
215
|
-
buf[off + 1] = wick.y
|
|
216
|
-
buf[off + 2] = wick.width
|
|
217
|
-
buf[off + 3] = wick.height
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
const wickWidth = upKLines[0]?.aligned.wickRect.width ?? downKLines[0]?.aligned.wickRect.width ?? 1
|
|
223
|
-
|
|
224
|
-
return {
|
|
225
|
-
upKLines,
|
|
226
|
-
downKLines,
|
|
227
|
-
upBodyBuf,
|
|
228
|
-
upBodyCount,
|
|
229
|
-
downBodyBuf,
|
|
230
|
-
downBodyCount,
|
|
231
|
-
upWickBuf,
|
|
232
|
-
upWickCount,
|
|
233
|
-
downWickBuf,
|
|
234
|
-
downWickCount,
|
|
235
|
-
wickWidth,
|
|
236
|
-
relations,
|
|
237
|
-
showVolumePriceMarkers,
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
function drawCandlesWithCanvas2D(ctx: CanvasRenderingContext2D, scrollLeft: number, prepared: PreparedCandles, priceColors: PriceColors): void {
|
|
242
|
-
ctx.save()
|
|
243
|
-
ctx.translate(-scrollLeft, 0)
|
|
244
|
-
|
|
245
|
-
ctx.fillStyle = priceColors.UP
|
|
246
|
-
for (let i = 0; i < prepared.upBodyCount; i++) {
|
|
247
|
-
const off = i * 4
|
|
248
|
-
ctx.fillRect(prepared.upBodyBuf[off], prepared.upBodyBuf[off + 1], prepared.upBodyBuf[off + 2], prepared.upBodyBuf[off + 3])
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
ctx.fillStyle = priceColors.DOWN
|
|
252
|
-
for (let i = 0; i < prepared.downBodyCount; i++) {
|
|
253
|
-
const off = i * 4
|
|
254
|
-
ctx.fillRect(prepared.downBodyBuf[off], prepared.downBodyBuf[off + 1], prepared.downBodyBuf[off + 2], prepared.downBodyBuf[off + 3])
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
ctx.fillStyle = priceColors.UP
|
|
258
|
-
for (let i = 0; i < prepared.upWickCount; i++) {
|
|
259
|
-
const off = i * 4
|
|
260
|
-
ctx.fillRect(prepared.upWickBuf[off], prepared.upWickBuf[off + 1], prepared.wickWidth, prepared.upWickBuf[off + 3])
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
ctx.fillStyle = priceColors.DOWN
|
|
264
|
-
for (let i = 0; i < prepared.downWickCount; i++) {
|
|
265
|
-
const off = i * 4
|
|
266
|
-
ctx.fillRect(prepared.downWickBuf[off], prepared.downWickBuf[off + 1], prepared.wickWidth, prepared.downWickBuf[off + 3])
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
ctx.restore()
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
function drawCandlesWithWebGL(context: RenderContext, prepared: PreparedCandles, priceColors: PriceColors): boolean {
|
|
273
|
-
if (context.settings?.enableWebGLRendering === false) return false
|
|
274
|
-
const surface = context.candleWebGLSurface
|
|
275
|
-
if (!surface || !surface.isAvailable()) return false
|
|
276
|
-
|
|
277
|
-
surface.clear()
|
|
278
|
-
|
|
279
|
-
const bodyUpOk = prepared.upBodyCount === 0 || surface.drawRectBuffer(prepared.upBodyBuf.subarray(0, prepared.upBodyCount * 4), prepared.upBodyCount, priceColors.UP, context.scrollLeft)
|
|
280
|
-
const bodyDownOk = prepared.downBodyCount === 0 || surface.drawRectBuffer(prepared.downBodyBuf.subarray(0, prepared.downBodyCount * 4), prepared.downBodyCount, priceColors.DOWN, context.scrollLeft)
|
|
281
|
-
const wickUpOk = prepared.upWickCount === 0 || surface.drawRectBuffer(prepared.upWickBuf.subarray(0, prepared.upWickCount * 4), prepared.upWickCount, priceColors.UP, context.scrollLeft)
|
|
282
|
-
const wickDownOk = prepared.downWickCount === 0 || surface.drawRectBuffer(prepared.downWickBuf.subarray(0, prepared.downWickCount * 4), prepared.downWickCount, priceColors.DOWN, context.scrollLeft)
|
|
283
|
-
|
|
284
|
-
return bodyUpOk && bodyDownOk && wickUpOk && wickDownOk
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
function compositeWebGLToMainCanvas(ctx: CanvasRenderingContext2D, context: RenderContext): void {
|
|
288
|
-
const surface = context.candleWebGLSurface
|
|
289
|
-
if (!surface) return
|
|
290
|
-
|
|
291
|
-
surface.compositeTo(ctx)
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
function drawVolumePriceMarkers(
|
|
295
|
-
context: RenderContext,
|
|
296
|
-
prepared: PreparedCandles,
|
|
297
|
-
markerManager: MarkerManager | undefined
|
|
298
|
-
): void {
|
|
299
|
-
const { ctx, range, kWidth, dpr } = context
|
|
300
|
-
const colors = getColors(context.theme)
|
|
301
|
-
if (!prepared.showVolumePriceMarkers || !markerManager || (context.zoomLevel ?? 1) < 2) {
|
|
302
|
-
return
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
ctx.save()
|
|
306
|
-
ctx.translate(-context.scrollLeft, 0)
|
|
307
|
-
|
|
308
|
-
for (const k of prepared.upKLines) {
|
|
309
|
-
const relation = prepared.relations?.[k.i - range.start]
|
|
310
|
-
if (relation !== undefined && relation !== VolumePriceRelation.OTHERS) {
|
|
311
|
-
const isRising = relation === VolumePriceRelation.RISE_WITH_VOLUME || relation === VolumePriceRelation.RISE_WITHOUT_VOLUME
|
|
312
|
-
const markerY = isRising ? k.alignedHighY - 15 : k.alignedLowY + 15
|
|
313
|
-
const posIndex = k.i - range.start
|
|
314
|
-
const markerX = context.kLineCenters[posIndex]!
|
|
315
|
-
drawVolumePriceMarker(ctx, markerX, markerY, relation, k.i, kWidth, 4, markerManager, dpr, colors.VOLUME_PRICE)
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
for (const k of prepared.downKLines) {
|
|
320
|
-
const relation = prepared.relations?.[k.i - range.start]
|
|
321
|
-
if (relation !== undefined && relation !== VolumePriceRelation.OTHERS) {
|
|
322
|
-
const isRising = relation === VolumePriceRelation.RISE_WITH_VOLUME || relation === VolumePriceRelation.RISE_WITHOUT_VOLUME
|
|
323
|
-
const markerY = isRising ? k.alignedHighY - 15 : k.alignedLowY + 15
|
|
324
|
-
const posIndex = k.i - range.start
|
|
325
|
-
const markerX = context.kLineCenters[posIndex]!
|
|
326
|
-
drawVolumePriceMarker(ctx, markerX, markerY, relation, k.i, kWidth, 4, markerManager, dpr, colors.VOLUME_PRICE)
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
ctx.restore()
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
/**
|
|
334
|
-
* 绘制量价关系标记
|
|
335
|
-
* 在K线图上标注量价关系标记符号
|
|
336
|
-
*
|
|
337
|
-
* @param ctx - Canvas绘图上下文
|
|
338
|
-
* @param x - 标记的x坐标(三角形水平中心)
|
|
339
|
-
* @param y - 标记的y坐标(三角形底边/顶点与K线的接触点)
|
|
340
|
-
* @param relation - 量价关系类型
|
|
341
|
-
* @param kWidth - K线宽度,作为三角形边长
|
|
342
|
-
* @param gap - 三角形与K线的间距,默认为4
|
|
343
|
-
* @param dpr - 设备像素比
|
|
344
|
-
*/
|
|
345
|
-
export function drawVolumePriceMarker(
|
|
346
|
-
ctx: CanvasRenderingContext2D,
|
|
347
|
-
x: number,
|
|
348
|
-
y: number,
|
|
349
|
-
relation: VolumePriceRelation,
|
|
350
|
-
kIndex: number,
|
|
351
|
-
kWidth: number,
|
|
352
|
-
gap: number = 4,
|
|
353
|
-
markerManager: MarkerManager,
|
|
354
|
-
dpr: number,
|
|
355
|
-
volumePriceColors: VolumePriceColors
|
|
356
|
-
): void {
|
|
357
|
-
const align = (v: number) => Math.round(v * dpr) / dpr
|
|
358
|
-
x = align(x)
|
|
359
|
-
y = align(y)
|
|
360
|
-
|
|
361
|
-
const sideLength = Math.min(kWidth, 20)
|
|
362
|
-
const height = sideLength * Math.sqrt(3) / 2
|
|
363
|
-
|
|
364
|
-
let color: string
|
|
365
|
-
let isUp: boolean
|
|
366
|
-
|
|
367
|
-
switch (relation) {
|
|
368
|
-
case VolumePriceRelation.RISE_WITH_VOLUME:
|
|
369
|
-
color = volumePriceColors.RISE_WITH
|
|
370
|
-
isUp = true
|
|
371
|
-
break
|
|
372
|
-
case VolumePriceRelation.RISE_WITHOUT_VOLUME:
|
|
373
|
-
color = volumePriceColors.RISE_WITHOUT
|
|
374
|
-
isUp = true
|
|
375
|
-
break
|
|
376
|
-
case VolumePriceRelation.FALL_WITH_VOLUME:
|
|
377
|
-
color = volumePriceColors.FALL_WITH
|
|
378
|
-
isUp = false
|
|
379
|
-
break
|
|
380
|
-
case VolumePriceRelation.FALL_WITHOUT_VOLUME:
|
|
381
|
-
color = volumePriceColors.FALL_WITHOUT
|
|
382
|
-
isUp = false
|
|
383
|
-
break
|
|
384
|
-
default:
|
|
385
|
-
return
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
ctx.save()
|
|
389
|
-
ctx.beginPath()
|
|
390
|
-
|
|
391
|
-
if (isUp) {
|
|
392
|
-
const baseY = align(y - gap)
|
|
393
|
-
const tipY = align(baseY - height)
|
|
394
|
-
|
|
395
|
-
ctx.moveTo(x, tipY)
|
|
396
|
-
ctx.lineTo(align(x - sideLength / 2), baseY)
|
|
397
|
-
ctx.lineTo(align(x + sideLength / 2), baseY)
|
|
398
|
-
} else {
|
|
399
|
-
const baseY = align(y + gap)
|
|
400
|
-
const tipY = align(baseY + height)
|
|
401
|
-
|
|
402
|
-
ctx.moveTo(x, tipY)
|
|
403
|
-
ctx.lineTo(align(x - sideLength / 2), baseY)
|
|
404
|
-
ctx.lineTo(align(x + sideLength / 2), baseY)
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
ctx.closePath()
|
|
408
|
-
|
|
409
|
-
ctx.fillStyle = color
|
|
410
|
-
ctx.fill()
|
|
411
|
-
|
|
412
|
-
ctx.restore()
|
|
413
|
-
|
|
414
|
-
let boundingX: number
|
|
415
|
-
let boundingY: number
|
|
416
|
-
|
|
417
|
-
if (isUp) {
|
|
418
|
-
const baseY = align(y - gap)
|
|
419
|
-
const tipY = align(baseY - height)
|
|
420
|
-
boundingX = align(x - sideLength / 2)
|
|
421
|
-
boundingY = tipY
|
|
422
|
-
} else {
|
|
423
|
-
const baseY = align(y + gap)
|
|
424
|
-
const tipY = align(baseY + height)
|
|
425
|
-
boundingX = align(x - sideLength / 2)
|
|
426
|
-
boundingY = baseY
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
let markerTypeKey: string
|
|
430
|
-
switch (relation) {
|
|
431
|
-
case VolumePriceRelation.RISE_WITH_VOLUME:
|
|
432
|
-
markerTypeKey = 'RISE_WITH_VOLUME'
|
|
433
|
-
break
|
|
434
|
-
case VolumePriceRelation.RISE_WITHOUT_VOLUME:
|
|
435
|
-
markerTypeKey = 'RISE_WITHOUT_VOLUME'
|
|
436
|
-
break
|
|
437
|
-
case VolumePriceRelation.FALL_WITH_VOLUME:
|
|
438
|
-
markerTypeKey = 'FALL_WITH_VOLUME'
|
|
439
|
-
break
|
|
440
|
-
case VolumePriceRelation.FALL_WITHOUT_VOLUME:
|
|
441
|
-
markerTypeKey = 'FALL_WITHOUT_VOLUME'
|
|
442
|
-
break
|
|
443
|
-
default:
|
|
444
|
-
return
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
const markerId = `mk_price-volume_${kIndex}`
|
|
448
|
-
markerManager.register({
|
|
449
|
-
id: markerId,
|
|
450
|
-
type: 'triangle',
|
|
451
|
-
markerType: markerTypeKey,
|
|
452
|
-
x: boundingX,
|
|
453
|
-
y: boundingY,
|
|
454
|
-
width: sideLength,
|
|
455
|
-
height: height,
|
|
456
|
-
dataIndex: kIndex,
|
|
457
|
-
metadata: { relation }
|
|
458
|
-
})
|
|
459
|
-
}
|
|
1
|
+
import type { RendererPlugin, RenderContext } from '../../plugin'
|
|
2
|
+
import { RENDERER_PRIORITY } from '../../plugin'
|
|
3
|
+
import type { KLineData } from '../../types/price'
|
|
4
|
+
import { getKLineTrend, type kLineTrend } from '../../types/kLine'
|
|
5
|
+
import { createAlignedKLineFromPx, createVerticalLineRect } from '../draw/pixelAlign'
|
|
6
|
+
import { getColors, type PriceColors, type VolumePriceColors } from '../theme/colors'
|
|
7
|
+
import { getPhysicalKLineConfig } from '../chart'
|
|
8
|
+
import { VolumePriceRelation } from '../../types/volumePrice'
|
|
9
|
+
import { analyzeVolumePriceRelationBatch, DEFAULT_VOLUME_PRICE_CONFIG } from '../../utils/volumePrice'
|
|
10
|
+
import type { MarkerManager } from '../marker/registry'
|
|
11
|
+
|
|
12
|
+
type CandleRenderData = {
|
|
13
|
+
i: number
|
|
14
|
+
aligned: ReturnType<typeof createAlignedKLineFromPx>
|
|
15
|
+
trend: kLineTrend
|
|
16
|
+
openY: number
|
|
17
|
+
closeY: number
|
|
18
|
+
highY: number
|
|
19
|
+
lowY: number
|
|
20
|
+
alignedHighY: number
|
|
21
|
+
alignedLowY: number
|
|
22
|
+
e: KLineData
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
type PreparedCandles = {
|
|
26
|
+
upKLines: CandleRenderData[]
|
|
27
|
+
downKLines: CandleRenderData[]
|
|
28
|
+
upBodyBuf: Float32Array
|
|
29
|
+
upBodyCount: number
|
|
30
|
+
downBodyBuf: Float32Array
|
|
31
|
+
downBodyCount: number
|
|
32
|
+
upWickBuf: Float32Array
|
|
33
|
+
upWickCount: number
|
|
34
|
+
downWickBuf: Float32Array
|
|
35
|
+
downWickCount: number
|
|
36
|
+
wickWidth: number
|
|
37
|
+
relations: VolumePriceRelation[] | null
|
|
38
|
+
showVolumePriceMarkers: boolean
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* 创建 K 线蜡烛图渲染器插件
|
|
43
|
+
*/
|
|
44
|
+
export function createCandleRenderer(): RendererPlugin {
|
|
45
|
+
return {
|
|
46
|
+
name: 'candle',
|
|
47
|
+
version: '1.0.0',
|
|
48
|
+
description: 'K线蜡烛图渲染器',
|
|
49
|
+
debugName: 'K线',
|
|
50
|
+
paneId: 'main',
|
|
51
|
+
priority: RENDERER_PRIORITY.MAIN,
|
|
52
|
+
|
|
53
|
+
draw(context: RenderContext) {
|
|
54
|
+
const { ctx, pane, data, range, scrollLeft, kWidth, kGap, dpr, kLinePositions, markerManager, settings } = context
|
|
55
|
+
const colors = getColors(context.theme)
|
|
56
|
+
const klineData = data as KLineData[]
|
|
57
|
+
if (!klineData.length) return
|
|
58
|
+
|
|
59
|
+
const prepared = prepareCandles({
|
|
60
|
+
pane,
|
|
61
|
+
data: klineData,
|
|
62
|
+
range,
|
|
63
|
+
kWidth,
|
|
64
|
+
kGap,
|
|
65
|
+
dpr,
|
|
66
|
+
kLinePositions,
|
|
67
|
+
settings,
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
const usedWebGL = drawCandlesWithWebGL(context, prepared, colors.PRICE)
|
|
71
|
+
if (!usedWebGL) {
|
|
72
|
+
drawCandlesWithCanvas2D(ctx, scrollLeft, prepared, colors.PRICE)
|
|
73
|
+
} else {
|
|
74
|
+
compositeWebGLToMainCanvas(ctx, context)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
drawVolumePriceMarkers(context, prepared, markerManager as MarkerManager | undefined)
|
|
78
|
+
},
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function prepareCandles(args: {
|
|
83
|
+
pane: RenderContext['pane']
|
|
84
|
+
data: KLineData[]
|
|
85
|
+
range: { start: number; end: number }
|
|
86
|
+
kWidth: number
|
|
87
|
+
kGap: number
|
|
88
|
+
dpr: number
|
|
89
|
+
kLinePositions: number[]
|
|
90
|
+
settings?: RenderContext['settings']
|
|
91
|
+
}): PreparedCandles {
|
|
92
|
+
const { pane, data, range, kWidth, kGap, dpr, kLinePositions, settings } = args
|
|
93
|
+
const { kWidthPx } = getPhysicalKLineConfig(kWidth, kGap, dpr)
|
|
94
|
+
const showVolumePriceMarkers = settings?.showVolumePriceMarkers !== false
|
|
95
|
+
const relations = showVolumePriceMarkers
|
|
96
|
+
? analyzeVolumePriceRelationBatch(data, range.start, range.end, DEFAULT_VOLUME_PRICE_CONFIG)
|
|
97
|
+
: null
|
|
98
|
+
|
|
99
|
+
const upKLines: CandleRenderData[] = []
|
|
100
|
+
const downKLines: CandleRenderData[] = []
|
|
101
|
+
const maxRects = Math.max(1, range.end - range.start)
|
|
102
|
+
const upBodyBuf = new Float32Array(maxRects * 4)
|
|
103
|
+
const downBodyBuf = new Float32Array(maxRects * 4)
|
|
104
|
+
const upWickBuf = new Float32Array(maxRects * 2 * 4)
|
|
105
|
+
const downWickBuf = new Float32Array(maxRects * 2 * 4)
|
|
106
|
+
let upBodyCount = 0
|
|
107
|
+
let downBodyCount = 0
|
|
108
|
+
let upWickCount = 0
|
|
109
|
+
let downWickCount = 0
|
|
110
|
+
|
|
111
|
+
// 预取 displayRange,避免循环内每根 K 线 4 次 getDisplayRange()
|
|
112
|
+
const { maxPrice, minPrice } = pane.yAxis.getDisplayRange()
|
|
113
|
+
const paddingTop = pane.yAxis.getPaddingTop()
|
|
114
|
+
const paddingBottom = pane.yAxis.getPaddingBottom()
|
|
115
|
+
const viewHeight = Math.max(1, pane.height - paddingTop - paddingBottom)
|
|
116
|
+
const isLinear = pane.yAxis.getScaleType() !== 'log'
|
|
117
|
+
let fastPriceToY: (price: number) => number
|
|
118
|
+
if (isLinear) {
|
|
119
|
+
const priceRange = maxPrice - minPrice || 1
|
|
120
|
+
const scaleK = viewHeight / priceRange
|
|
121
|
+
const scaleB = paddingTop + viewHeight
|
|
122
|
+
fastPriceToY = (price: number) => scaleB - (price - minPrice) * scaleK
|
|
123
|
+
} else {
|
|
124
|
+
// 对数模式回退到标准方法
|
|
125
|
+
fastPriceToY = (price: number) => pane.yAxis.priceToY(price)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
for (let i = range.start; i < range.end && i < data.length; i++) {
|
|
129
|
+
const e = data[i]
|
|
130
|
+
if (!e) continue
|
|
131
|
+
|
|
132
|
+
const openY = fastPriceToY(e.open)
|
|
133
|
+
const closeY = fastPriceToY(e.close)
|
|
134
|
+
const highY = fastPriceToY(e.high)
|
|
135
|
+
const lowY = fastPriceToY(e.low)
|
|
136
|
+
|
|
137
|
+
const leftLogical = kLinePositions[i - range.start]
|
|
138
|
+
if (leftLogical === undefined) continue
|
|
139
|
+
|
|
140
|
+
const alignY = (logical: number) => Math.round(logical * dpr) / dpr
|
|
141
|
+
const alignedOpenY = alignY(openY)
|
|
142
|
+
const alignedCloseY = alignY(closeY)
|
|
143
|
+
const alignedHighY = alignY(highY)
|
|
144
|
+
const alignedLowY = alignY(lowY)
|
|
145
|
+
const alignedRawRectY = Math.min(alignedOpenY, alignedCloseY)
|
|
146
|
+
const alignedRawRectH = Math.max(Math.abs(alignedOpenY - alignedCloseY), 1)
|
|
147
|
+
|
|
148
|
+
const roundedLeftPx = Math.round(leftLogical * dpr)
|
|
149
|
+
const aligned = createAlignedKLineFromPx(
|
|
150
|
+
roundedLeftPx,
|
|
151
|
+
alignedRawRectY,
|
|
152
|
+
kWidthPx,
|
|
153
|
+
alignedRawRectH,
|
|
154
|
+
dpr
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
const trend: kLineTrend = getKLineTrend(e)
|
|
158
|
+
const renderData: CandleRenderData = {
|
|
159
|
+
i,
|
|
160
|
+
aligned,
|
|
161
|
+
trend,
|
|
162
|
+
openY,
|
|
163
|
+
closeY,
|
|
164
|
+
highY,
|
|
165
|
+
lowY,
|
|
166
|
+
alignedHighY,
|
|
167
|
+
alignedLowY,
|
|
168
|
+
e,
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const bodyRect = aligned.bodyRect
|
|
172
|
+
const targetKLines = trend === 'up' ? upKLines : downKLines
|
|
173
|
+
const isUp = trend === 'up'
|
|
174
|
+
|
|
175
|
+
targetKLines.push(renderData)
|
|
176
|
+
|
|
177
|
+
if (isUp) {
|
|
178
|
+
const off = upBodyCount++ * 4
|
|
179
|
+
upBodyBuf[off] = bodyRect.x
|
|
180
|
+
upBodyBuf[off + 1] = bodyRect.y
|
|
181
|
+
upBodyBuf[off + 2] = bodyRect.width
|
|
182
|
+
upBodyBuf[off + 3] = bodyRect.height
|
|
183
|
+
} else {
|
|
184
|
+
const off = downBodyCount++ * 4
|
|
185
|
+
downBodyBuf[off] = bodyRect.x
|
|
186
|
+
downBodyBuf[off + 1] = bodyRect.y
|
|
187
|
+
downBodyBuf[off + 2] = bodyRect.width
|
|
188
|
+
downBodyBuf[off + 3] = bodyRect.height
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const bodyTop = bodyRect.y
|
|
192
|
+
const bodyBottom = bodyRect.y + bodyRect.height
|
|
193
|
+
const bodyHigh = Math.max(e.open, e.close)
|
|
194
|
+
const bodyLow = Math.min(e.open, e.close)
|
|
195
|
+
|
|
196
|
+
if (e.high > bodyHigh) {
|
|
197
|
+
const wick = createVerticalLineRect(aligned.wickRect.x, alignedHighY, bodyTop, dpr)
|
|
198
|
+
if (wick) {
|
|
199
|
+
const buf = isUp ? upWickBuf : downWickBuf
|
|
200
|
+
const idx = isUp ? upWickCount++ : downWickCount++
|
|
201
|
+
const off = idx * 4
|
|
202
|
+
buf[off] = wick.x
|
|
203
|
+
buf[off + 1] = wick.y
|
|
204
|
+
buf[off + 2] = wick.width
|
|
205
|
+
buf[off + 3] = wick.height
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
if (e.low < bodyLow) {
|
|
209
|
+
const wick = createVerticalLineRect(aligned.wickRect.x, bodyBottom, alignedLowY, dpr)
|
|
210
|
+
if (wick) {
|
|
211
|
+
const buf = isUp ? upWickBuf : downWickBuf
|
|
212
|
+
const idx = isUp ? upWickCount++ : downWickCount++
|
|
213
|
+
const off = idx * 4
|
|
214
|
+
buf[off] = wick.x
|
|
215
|
+
buf[off + 1] = wick.y
|
|
216
|
+
buf[off + 2] = wick.width
|
|
217
|
+
buf[off + 3] = wick.height
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const wickWidth = upKLines[0]?.aligned.wickRect.width ?? downKLines[0]?.aligned.wickRect.width ?? 1
|
|
223
|
+
|
|
224
|
+
return {
|
|
225
|
+
upKLines,
|
|
226
|
+
downKLines,
|
|
227
|
+
upBodyBuf,
|
|
228
|
+
upBodyCount,
|
|
229
|
+
downBodyBuf,
|
|
230
|
+
downBodyCount,
|
|
231
|
+
upWickBuf,
|
|
232
|
+
upWickCount,
|
|
233
|
+
downWickBuf,
|
|
234
|
+
downWickCount,
|
|
235
|
+
wickWidth,
|
|
236
|
+
relations,
|
|
237
|
+
showVolumePriceMarkers,
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function drawCandlesWithCanvas2D(ctx: CanvasRenderingContext2D, scrollLeft: number, prepared: PreparedCandles, priceColors: PriceColors): void {
|
|
242
|
+
ctx.save()
|
|
243
|
+
ctx.translate(-scrollLeft, 0)
|
|
244
|
+
|
|
245
|
+
ctx.fillStyle = priceColors.UP
|
|
246
|
+
for (let i = 0; i < prepared.upBodyCount; i++) {
|
|
247
|
+
const off = i * 4
|
|
248
|
+
ctx.fillRect(prepared.upBodyBuf[off], prepared.upBodyBuf[off + 1], prepared.upBodyBuf[off + 2], prepared.upBodyBuf[off + 3])
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
ctx.fillStyle = priceColors.DOWN
|
|
252
|
+
for (let i = 0; i < prepared.downBodyCount; i++) {
|
|
253
|
+
const off = i * 4
|
|
254
|
+
ctx.fillRect(prepared.downBodyBuf[off], prepared.downBodyBuf[off + 1], prepared.downBodyBuf[off + 2], prepared.downBodyBuf[off + 3])
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
ctx.fillStyle = priceColors.UP
|
|
258
|
+
for (let i = 0; i < prepared.upWickCount; i++) {
|
|
259
|
+
const off = i * 4
|
|
260
|
+
ctx.fillRect(prepared.upWickBuf[off], prepared.upWickBuf[off + 1], prepared.wickWidth, prepared.upWickBuf[off + 3])
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
ctx.fillStyle = priceColors.DOWN
|
|
264
|
+
for (let i = 0; i < prepared.downWickCount; i++) {
|
|
265
|
+
const off = i * 4
|
|
266
|
+
ctx.fillRect(prepared.downWickBuf[off], prepared.downWickBuf[off + 1], prepared.wickWidth, prepared.downWickBuf[off + 3])
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
ctx.restore()
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function drawCandlesWithWebGL(context: RenderContext, prepared: PreparedCandles, priceColors: PriceColors): boolean {
|
|
273
|
+
if (context.settings?.enableWebGLRendering === false) return false
|
|
274
|
+
const surface = context.candleWebGLSurface
|
|
275
|
+
if (!surface || !surface.isAvailable()) return false
|
|
276
|
+
|
|
277
|
+
surface.clear()
|
|
278
|
+
|
|
279
|
+
const bodyUpOk = prepared.upBodyCount === 0 || surface.drawRectBuffer(prepared.upBodyBuf.subarray(0, prepared.upBodyCount * 4), prepared.upBodyCount, priceColors.UP, context.scrollLeft)
|
|
280
|
+
const bodyDownOk = prepared.downBodyCount === 0 || surface.drawRectBuffer(prepared.downBodyBuf.subarray(0, prepared.downBodyCount * 4), prepared.downBodyCount, priceColors.DOWN, context.scrollLeft)
|
|
281
|
+
const wickUpOk = prepared.upWickCount === 0 || surface.drawRectBuffer(prepared.upWickBuf.subarray(0, prepared.upWickCount * 4), prepared.upWickCount, priceColors.UP, context.scrollLeft)
|
|
282
|
+
const wickDownOk = prepared.downWickCount === 0 || surface.drawRectBuffer(prepared.downWickBuf.subarray(0, prepared.downWickCount * 4), prepared.downWickCount, priceColors.DOWN, context.scrollLeft)
|
|
283
|
+
|
|
284
|
+
return bodyUpOk && bodyDownOk && wickUpOk && wickDownOk
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function compositeWebGLToMainCanvas(ctx: CanvasRenderingContext2D, context: RenderContext): void {
|
|
288
|
+
const surface = context.candleWebGLSurface
|
|
289
|
+
if (!surface) return
|
|
290
|
+
|
|
291
|
+
surface.compositeTo(ctx)
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function drawVolumePriceMarkers(
|
|
295
|
+
context: RenderContext,
|
|
296
|
+
prepared: PreparedCandles,
|
|
297
|
+
markerManager: MarkerManager | undefined
|
|
298
|
+
): void {
|
|
299
|
+
const { ctx, range, kWidth, dpr } = context
|
|
300
|
+
const colors = getColors(context.theme)
|
|
301
|
+
if (!prepared.showVolumePriceMarkers || !markerManager || (context.zoomLevel ?? 1) < 2) {
|
|
302
|
+
return
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
ctx.save()
|
|
306
|
+
ctx.translate(-context.scrollLeft, 0)
|
|
307
|
+
|
|
308
|
+
for (const k of prepared.upKLines) {
|
|
309
|
+
const relation = prepared.relations?.[k.i - range.start]
|
|
310
|
+
if (relation !== undefined && relation !== VolumePriceRelation.OTHERS) {
|
|
311
|
+
const isRising = relation === VolumePriceRelation.RISE_WITH_VOLUME || relation === VolumePriceRelation.RISE_WITHOUT_VOLUME
|
|
312
|
+
const markerY = isRising ? k.alignedHighY - 15 : k.alignedLowY + 15
|
|
313
|
+
const posIndex = k.i - range.start
|
|
314
|
+
const markerX = context.kLineCenters[posIndex]!
|
|
315
|
+
drawVolumePriceMarker(ctx, markerX, markerY, relation, k.i, kWidth, 4, markerManager, dpr, colors.VOLUME_PRICE)
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
for (const k of prepared.downKLines) {
|
|
320
|
+
const relation = prepared.relations?.[k.i - range.start]
|
|
321
|
+
if (relation !== undefined && relation !== VolumePriceRelation.OTHERS) {
|
|
322
|
+
const isRising = relation === VolumePriceRelation.RISE_WITH_VOLUME || relation === VolumePriceRelation.RISE_WITHOUT_VOLUME
|
|
323
|
+
const markerY = isRising ? k.alignedHighY - 15 : k.alignedLowY + 15
|
|
324
|
+
const posIndex = k.i - range.start
|
|
325
|
+
const markerX = context.kLineCenters[posIndex]!
|
|
326
|
+
drawVolumePriceMarker(ctx, markerX, markerY, relation, k.i, kWidth, 4, markerManager, dpr, colors.VOLUME_PRICE)
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
ctx.restore()
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* 绘制量价关系标记
|
|
335
|
+
* 在K线图上标注量价关系标记符号
|
|
336
|
+
*
|
|
337
|
+
* @param ctx - Canvas绘图上下文
|
|
338
|
+
* @param x - 标记的x坐标(三角形水平中心)
|
|
339
|
+
* @param y - 标记的y坐标(三角形底边/顶点与K线的接触点)
|
|
340
|
+
* @param relation - 量价关系类型
|
|
341
|
+
* @param kWidth - K线宽度,作为三角形边长
|
|
342
|
+
* @param gap - 三角形与K线的间距,默认为4
|
|
343
|
+
* @param dpr - 设备像素比
|
|
344
|
+
*/
|
|
345
|
+
export function drawVolumePriceMarker(
|
|
346
|
+
ctx: CanvasRenderingContext2D,
|
|
347
|
+
x: number,
|
|
348
|
+
y: number,
|
|
349
|
+
relation: VolumePriceRelation,
|
|
350
|
+
kIndex: number,
|
|
351
|
+
kWidth: number,
|
|
352
|
+
gap: number = 4,
|
|
353
|
+
markerManager: MarkerManager,
|
|
354
|
+
dpr: number,
|
|
355
|
+
volumePriceColors: VolumePriceColors
|
|
356
|
+
): void {
|
|
357
|
+
const align = (v: number) => Math.round(v * dpr) / dpr
|
|
358
|
+
x = align(x)
|
|
359
|
+
y = align(y)
|
|
360
|
+
|
|
361
|
+
const sideLength = Math.min(kWidth, 20)
|
|
362
|
+
const height = sideLength * Math.sqrt(3) / 2
|
|
363
|
+
|
|
364
|
+
let color: string
|
|
365
|
+
let isUp: boolean
|
|
366
|
+
|
|
367
|
+
switch (relation) {
|
|
368
|
+
case VolumePriceRelation.RISE_WITH_VOLUME:
|
|
369
|
+
color = volumePriceColors.RISE_WITH
|
|
370
|
+
isUp = true
|
|
371
|
+
break
|
|
372
|
+
case VolumePriceRelation.RISE_WITHOUT_VOLUME:
|
|
373
|
+
color = volumePriceColors.RISE_WITHOUT
|
|
374
|
+
isUp = true
|
|
375
|
+
break
|
|
376
|
+
case VolumePriceRelation.FALL_WITH_VOLUME:
|
|
377
|
+
color = volumePriceColors.FALL_WITH
|
|
378
|
+
isUp = false
|
|
379
|
+
break
|
|
380
|
+
case VolumePriceRelation.FALL_WITHOUT_VOLUME:
|
|
381
|
+
color = volumePriceColors.FALL_WITHOUT
|
|
382
|
+
isUp = false
|
|
383
|
+
break
|
|
384
|
+
default:
|
|
385
|
+
return
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
ctx.save()
|
|
389
|
+
ctx.beginPath()
|
|
390
|
+
|
|
391
|
+
if (isUp) {
|
|
392
|
+
const baseY = align(y - gap)
|
|
393
|
+
const tipY = align(baseY - height)
|
|
394
|
+
|
|
395
|
+
ctx.moveTo(x, tipY)
|
|
396
|
+
ctx.lineTo(align(x - sideLength / 2), baseY)
|
|
397
|
+
ctx.lineTo(align(x + sideLength / 2), baseY)
|
|
398
|
+
} else {
|
|
399
|
+
const baseY = align(y + gap)
|
|
400
|
+
const tipY = align(baseY + height)
|
|
401
|
+
|
|
402
|
+
ctx.moveTo(x, tipY)
|
|
403
|
+
ctx.lineTo(align(x - sideLength / 2), baseY)
|
|
404
|
+
ctx.lineTo(align(x + sideLength / 2), baseY)
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
ctx.closePath()
|
|
408
|
+
|
|
409
|
+
ctx.fillStyle = color
|
|
410
|
+
ctx.fill()
|
|
411
|
+
|
|
412
|
+
ctx.restore()
|
|
413
|
+
|
|
414
|
+
let boundingX: number
|
|
415
|
+
let boundingY: number
|
|
416
|
+
|
|
417
|
+
if (isUp) {
|
|
418
|
+
const baseY = align(y - gap)
|
|
419
|
+
const tipY = align(baseY - height)
|
|
420
|
+
boundingX = align(x - sideLength / 2)
|
|
421
|
+
boundingY = tipY
|
|
422
|
+
} else {
|
|
423
|
+
const baseY = align(y + gap)
|
|
424
|
+
const tipY = align(baseY + height)
|
|
425
|
+
boundingX = align(x - sideLength / 2)
|
|
426
|
+
boundingY = baseY
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
let markerTypeKey: string
|
|
430
|
+
switch (relation) {
|
|
431
|
+
case VolumePriceRelation.RISE_WITH_VOLUME:
|
|
432
|
+
markerTypeKey = 'RISE_WITH_VOLUME'
|
|
433
|
+
break
|
|
434
|
+
case VolumePriceRelation.RISE_WITHOUT_VOLUME:
|
|
435
|
+
markerTypeKey = 'RISE_WITHOUT_VOLUME'
|
|
436
|
+
break
|
|
437
|
+
case VolumePriceRelation.FALL_WITH_VOLUME:
|
|
438
|
+
markerTypeKey = 'FALL_WITH_VOLUME'
|
|
439
|
+
break
|
|
440
|
+
case VolumePriceRelation.FALL_WITHOUT_VOLUME:
|
|
441
|
+
markerTypeKey = 'FALL_WITHOUT_VOLUME'
|
|
442
|
+
break
|
|
443
|
+
default:
|
|
444
|
+
return
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
const markerId = `mk_price-volume_${kIndex}`
|
|
448
|
+
markerManager.register({
|
|
449
|
+
id: markerId,
|
|
450
|
+
type: 'triangle',
|
|
451
|
+
markerType: markerTypeKey,
|
|
452
|
+
x: boundingX,
|
|
453
|
+
y: boundingY,
|
|
454
|
+
width: sideLength,
|
|
455
|
+
height: height,
|
|
456
|
+
dataIndex: kIndex,
|
|
457
|
+
metadata: { relation }
|
|
458
|
+
})
|
|
459
|
+
}
|