API 参考

顶层类的公开接口:Chart、ChartWidget、ChartWidgetGrid 和 ChartGrid。

● 演示对比、价差与比值模拟行情数据,在你的浏览器中生成

Chart

无界面渲染器。界面由你自己实现;订阅事件;以命令式方式修改状态。

构造

new Chart(host: HTMLElement, options?: ChartOptions)

数据

方法用途
setData(data)替换整个序列。
appendBar(bar)追加一根新K线;如已启用则自动滚动。
appendBars(bars)批量追加;指标只重新计算一次。
updateLastBar(bar)修改当前正在形成的K线。
updateLastBarFromTick(tick)把一笔 tick 合并进最后一根K线。
getData()读取原始 OHLC 序列。

K 线时间为纪元毫秒或秒,整个序列使用同一单位。图表从最新一根 K 线读取一次单位:barTimeUnit(data) 返回 1(毫秒,1e11 及以上)或 1000(秒)。因此毫秒序列中 2001 年以前的日期也保持正确,即使这些值小于 1e12。请让一个序列只用一种单位;图表返回的时间(事件、getData())使用该单位。

图表类型与主题

方法用途
setChartType(type)18 种类型之一——参见 图表类型。
setTheme(name | theme)在内置主题之间切换,或设置自己的主题。getTheme() 按设置时的样子返回。
applyOverrides(patch, { layer })按键设置图表任意部分的样式('grid.vertical.visible'、'series.candlestick.upColor'…);null 移除一个键。见样式。
setOverrides(overrides, { layer }) · resetOverrides(keys?, { layer })一次替换整个层;移除指定的键(或全部)。
getOverrides({ layer }) · getStyle() · getStyleValue(key)某一层的覆盖;解析后的外观;某个键最终的值。
setTimeframe(tf)切换当前周期;重新连接实时数据流。

渲染器

方法用途
renderer 选项'canvas'(默认)、'webgl' 或 'auto'(仅在硬件 GPU 上用 WebGL)。
setRenderer(mode)运行时切换。返回当前实际使用的渲染器:没有 WebGL 2 时为 'canvas'。
getRenderer()'canvas' 或 'webgl'。

GPU 绘制哪些内容以及测量数字:性能 → WebGL 渲染器。

指标

方法用途
addIndicator(id, params?, position?)添加叠加指标或副图指标。返回实例 id。
updateIndicator(instanceId, params)修改一个运行中的指标。
removeIndicator(instanceId)删除并销毁。
updateIndicatorStyle(instanceId, style)颜色、线宽、不透明度,以及每条线的线型和显示与否(plots: { signal: { lineStyle: 'dashed' } })。
setIndicatorDefaults(id, style | null)从现在起同类指标的初始样式。
setPaneStyle(instanceId, { background, separator } | null) · getPaneStyle(instanceId)面板自己的外观,随其指标一起保存。

坐标轴与缩放

价格轴(右侧区域)和时间轴(底部区域)可直接用指针操作, 手势与交易员熟悉的一致:

手势效果
上下拖动价格轴压缩 / 拉伸纵向价格范围(会关闭自动缩放)。
左右拖动时间轴放大 / 缩小时间轴。
双击价格轴重新启用自动缩放。
双击时间轴让全部数据适应视口。

时区。时间轴标签和十字光标的时间标签默认跟随浏览器本地时区; 可以在设置面板中切换为固定的 UTC 偏移(或切回本地),也可以直接调用:

chart.setTimezoneOffset(-300)  // EST (UTC-5), in minutes
chart.setTimezoneOffset(330)   // IST (UTC+5:30)
chart.setTimezoneOffset(null)  // back to browser-local

同样的效果也可以通过代码实现:

chart.setAutoScale(false)  // freeze the current price range
chart.setLogScale(true)    // switch to logarithmic price scale
chart.setInvertScale(true) // upside down (Alt+I in the widget)
chart.fitContent()         // zoom out to all data
chart.scrollToEnd()
chart.setVisibleRangePreset('3M')       // 1D 5D 1M 3M 6M YTD 1Y 5Y All
chart.setMinBarUnit(0.05)               // narrower bars, so 'All' fits a long series (0.25 by default)
chart.goToTime(Date.UTC(2025, 0, 1))    // centre that bar (Alt+G in the widget)

长区间。每根 K 线的位置不窄于 0.25 px,因此超过这一限度的区间(笔记本上 26 年的日线)只显示最新的部分。在组件中,区间栏上的长区间会切换到 K 线能够放下的周期,每根至少 2 px:5Y 日线切换为周线,All 切换为月线;短区间则回到你选择的周期,绝不更细。它只会切换到你的数据源提供的周期(或现有 K 线能聚合到的周期)。features: { 'statusBar.rangeTimeframe': false } 保持周期不变。直接使用 Chart 时,minBarUnit(选项,或 setMinBarUnit)允许最窄 0.02 px,按像素列绘制。

价格坐标模式。除常规和对数外,坐标轴还可以以第一根可见K线为基准 重新计算标签——percentage 显示百分比涨跌,indexedTo100 把基准设为 100。常规、百分比和以 100 为基准三种模式共用同一套线性几何,只有标签不同。 可在图表设置面板中设置,也可以直接调用:

chart.setScaleMode('percentage')   // axis labels: +12.34% from first visible bar
chart.setScaleMode('indexedTo100') // first visible bar reads as 100
chart.setScaleMode('logarithmic')
chart.getScaleMode()

价格与时间格式

价格可以按你自己的方式显示,也可以像债券及其期货报价那样以一个点的分数显示 (101'16 表示 101 又 16/32)。该格式作用于价格坐标上所有显示价格的地方:坐标轴、十字光标、 最新价标签、图例、提示框、委托、提醒和画线标签;坐标轴刻度落在整分数上,roundPrice 也按整分数取整。指标窗格保留各自的数字格式。时间同样可以按你的方式显示;格式化函数会得知当前标签的类型。

new Chart(host, { priceFormat: { denominator: 32 } })                    // 101'16
chart.setPriceFormat({ denominator: 32, subDenominator: 2 })            // 101'165: 16½ 32nds
chart.setPriceFormat((p) => ' 

坐标轴如何选择标签

时间轴为开启最大日历步长的 K 线标注:一年(粗体)、一个月、一天、一个整点,彼此至少相隔 80 px。因此在 5Y 时显示 2022 · Jul · 2023,一个月时 Oct · 8 · 15,一天之内 12:00 · 18:00。标签属于 K 线而非屏幕:平移时随 K 线移动,前面加载更多历史时保持原位。最早一根 K 线左侧没有标签。月份名称遵循 setNumberLocale,网格线与标签位置一致。

价格轴按其高度放置尽可能多的标签,彼此至少相隔 30 px,并落在市场的最小变动价位上。对数坐标下步长随价格增大,因此底部与顶部一样有标签;百分比或指数坐标下,标签是从基准起算的整百分比或整指数点。指标窗格也按自身高度确定标签数量。

市场:最小变动价位、涨跌停与交易时段

setMarket 把交易所的规则交给图表:K线颜色、小数位数、最小变动价位(可随价格变化)、 当日涨停价和跌停价,以及每手股数。roundPrice 和 getPriceStep 遵循价格所在区间的最小变动价位,因此从图表菜单得到的价格都是有效价格;组件的下单窗口 也会按最小变动价位、当日涨跌停价和整手检查订单。预设还带有交易所的交易时段和时区,供 getCurrentSession 和你自己的代码使用;图表的交易时段着色和延长交易时段则来自 setSymbolInfo。内置预设覆盖截至 2025 年 5 月的越南交易所规则:HOSE(按价格每档 10、50 或 100 越南盾,±7%)、其 ETF 和备兑权证(10 越南盾)、HNX(±10%,设有收盘后交易时段)、UPCoM(±15%) 以及 VN30 指数期货(每档 0.1 点,±7%,8:45 开盘);股票每手 100 股,绿涨红跌,紫色为涨停价、青色为跌停价、 黄色为参考价。预设中的价格以千越南盾为单位,与行情报价板一致;scaleMarketPrices(preset, 1000) 则按越南盾读取价格。

import {
  MARKET_HOSE, scaleMarketPrices, priceStepAt, roundToPriceStep, getCurrentSession,
  vn30fContracts, vn30fLastTradingDay,
} from '@tradecanvas/chart'

chart.setMarket(MARKET_HOSE)                 // 或用 scaleMarketPrices(MARKET_HOSE, 1000) 处理 25350 这样的价格
chart.setPriceLimits(25.35)                  // 参考价:前一交易日的收盘价
chart.getPriceLimits()                       // { ceiling: 27.1, floor: 23.6, reference: 25.35 }
chart.roundPrice(25.37)                      // 25.35,最小变动 50 越南盾
chart.getPriceStep(9.5)                      // 低于 10,000 越南盾时为 0.01

// 单独使用这些规则
priceStepAt(25.35, MARKET_HOSE)              // 0.05
roundToPriceStep(10.486, MARKET_HOSE, 'down') // 10.45
getCurrentSession(MARKET_HOSE.sessions, Date.now(), MARKET_HOSE.timeZone) // { name: 'ATC', type: 'preClose', … },仅按时钟判断

// VN30 期货:四个挂牌合约及其最后交易日
vn30fContracts(Date.now())                   // [{ symbol: 'VN30F2611', lastTradingDay: '2026-11-19', … }, …]
vn30fLastTradingDay(2026, 12, (day) => holidays.has(day)) // 第三个星期四,遇节假日则提前

自定义市场是一个 MarketConfig:priceStep(按价格区间分档时用 priceSteps: [{ from, step }])、priceLimits、 volumeUnit(每手数量)、sessions、timeZone 和 colorScheme。品种自身的 minTick 优先于市场的最小变动价位。交易所会不时调整规则; 在依赖某个预设之前,请先对照你所在交易所的现行规则进行核实。

品种对比

可以在价格坐标上显示另一个品种的涨跌幅(addCompareSymbol),也可以把它的价格放在独立坐标或独立窗格中, 或者显示图表收盘价与它的价差或比值——后几种都是指标(compareSymbol、spread), 和其他指标一样有图例、数值标签和提醒,并会保存在布局中。另一个品种的K线按时间与图表的K线对齐。 图表会请求它需要的K线;切换周期后请重新提供。correlation(按收盘价)和 correlationlog (按逐根K线收益率)以同样方式读取另一个品种,取值在 −1 到 1 之间。

chart.addCompareSymbol('eth', 'ETHUSDT', ethBars, '#7c4dff')   // percent change, on this scale
chart.addIndicator('compareSymbol', { symbol: 'ETHUSDT' }, 'bottom', { scale: 'left' })  // own scale
chart.addIndicator('spread', { symbol: 'ETHUSDT', mode: 'ratio' })                      // own pane
chart.addIndicator('correlation', { symbol: 'ETHUSDT', length: 20 })                    // −1 to 1, own pane

chart.on('symbolSeriesRequest', async ({ payload }) =>
  chart.setSymbolSeries(payload.symbol, await adapter.fetchHistory(payload.symbol, '1h', 1000)))
chart.getRequiredSymbols()                           // what to fetch again on a new interval
chart.setPaneScale(spreadId, { percent: true })     // a pane in percent of its first value

在 ChartWidget 中,对象树的对比按钮会先询问品种,再询问对比方式:涨跌幅、独立坐标、独立副图、价差、比值,或按价格或收益率计算的相关性。

导出数据

以 CSV 或 JSON 导出K线,每条指标线各占一列,列名与图例中的名称一致。ChartWidget 的图表右键菜单中有 导出数据 (CSV)。

chart.exportAllData('csv', 'btc-1h.csv')                 // every bar loaded
chart.exportVisibleData('json', undefined, { indicators: false })
const text = chart.getExportText('csv', { range: 'visible' })
const { bars, columns } = chart.getExportData()          // columns: { name, values }[]

成交量分布(Volume Profile)

在可见范围内按价格分组统计成交量的水平直方图。默认关闭—— 可通过代码或组件的设置面板开启:

chart.setVolumeProfileVisible(true)
chart.setVolumeProfileConfig({
  buckets: 48,        // resolution of the histogram
  widthRatio: 0.18,   // % of chart width
  opacity: 0.32,
  highlightPoC: true, // mark the highest-volume bucket
})

波段标记(枢轴点)

用小三角形标出分形波段高点 / 低点(▼ 位于已确认的枢轴高点上方,▲ 位于枢轴低点下方)。 强度参数决定两侧各需要有多少根更低的K线。可在设置面板中切换,或者:

chart.setPivotMarkersVisible(true)
chart.setPivotMarkersConfig({ left: 5, right: 5, showLabels: true })

// market-structure labels (HH / HL / LH / LL) instead of price
chart.setPivotMarkersConfig({ structureLabels: true })

// pure detection + classification are exported
import { findPivots, classifyPivots } from '@tradecanvas/core'
const pivots = findPivots(bars, 5, 5)        // [{ index, price, type }]
const structure = classifyPivots(pivots)     // adds label: 'HH'|'LH'|'HL'|'LL'

交易时段着色(常规交易时段)

把常规交易时段以外的K线(盘前 / 盘后或隔夜休市)调暗,让常规交易时段更加醒目。 默认使用美股常规交易时段(纽约时间 09:30–16:00,已考虑夏令时); 可以用“一天中的分钟数”加上市场时区来配置时间窗口。 如果某个品种的数据源提供了交易时段信息,会自动设置。

chart.setSessionShadingVisible(true)
chart.setSessionShadingConfig({
  startMinute: 9 * 60 + 30,     // 09:30
  endMinute: 16 * 60,           // 16:00 (end-exclusive; end < start wraps midnight)
  timeZone: 'America/New_York', // or tzOffsetMinutes: -300 for a fixed offset
})

延长交易时段。关闭后,品种常规交易时段(按其 timezone 计算的 SymbolInfo.sessions)以外的K线会离开图表;它们会被暂存起来,实时K线和历史分页也是如此, 重新开启后即恢复。日线及更长周期的K线保持不变。

chart.setSymbolInfo({ symbol: 'AAPL', timezone: 'America/New_York', sessions: [{ start: '09:30', end: '16:00' }] })
chart.setExtendedHours(false)     // or new Chart(host, { extendedHours: false })
chart.isExtendedHoursVisible()

前一周期高低点(PDH / PDL / PDC)

将前一日(或前一周)的最高价、最低价、收盘价以及当前周期的开盘价绘制为带标签的水平线—— 这正是日内交易者关注的支撑 / 阻力位。可在设置面板中切换,也可以直接调用:

chart.setPeriodLevelsVisible(true)
chart.setPeriodLevelsPeriod('week')   // 'day' (PDH/PDL/PDC) | 'week' (PWH/PWL/PWC)

// pure computation is exported
import { computePeriodLevels } from '@tradecanvas/core'
const levels = computePeriodLevels(bars, 'day')  // [{ id, label, price }]

市场轮廓(TPO)

一种“价格停留时间”直方图:每根K线会为其价格范围触及的每个价格档位贡献一个 TPO, 由此显示控制点(Point of Control,成交最密集的价格)和价值区(约 70% 的 TPO)。 它与成交量分布不同——按时间而非成交量加权——并固定在左侧,因此两者可以同时显示。 默认关闭;可在设置面板中切换,也可以直接调用:

chart.setMarketProfileVisible(true)
chart.setMarketProfileConfig({
  buckets: 48,
  widthRatio: 0.18,
  opacity: 0.32,
  valueAreaPct: 0.7,  // fraction of TPOs in the value area
  highlightPoC: true, // dashed line at the point of control
})

// split into one mini-profile per calendar-day session
chart.setMarketProfileConfig({ splitBySession: true })

// classic TPO letters per session (when zoomed in enough to be legible)
chart.setMarketProfileConfig({ splitBySession: true, letters: true })

// pure computation is exported too
import { computeMarketProfile, computeSessionProfiles } from '@tradecanvas/core'
const profile = computeMarketProfile(bars, priceMin, priceMax, { buckets: 48 })
const sessions = computeSessionProfiles(bars, priceMin, priceMax)  // per-day TPO

触控与移动端

手势操作
单指拖动(图表区域)平移并移动十字光标
双指捏合以中点为中心缩放
长按(约 500 ms)在K线上固定 OHLC 提示(相当于移动端的 Alt+点击)
在价格轴 / 时间轴区域内单指拖动缩放对应的坐标轴
轻点点击:click、barClick、信号标记;点在绘图上则选中它
双击轻点双击:在坐标轴上则重置该轴;在绘图上则打开其设置
在绘图上单指拖动移动它,或拖动所选绘图的控制点改变形状
选中绘图工具时:点、点——或按下、拖动、抬起放置各点;按下、拖动再抬起,一次画出两点线
在绘图上长按打开它的菜单(相当于右键)
在提醒线、订单线、括号单线或窗格分隔线上单指拖动拖动它

手指的触及范围比鼠标大:到线约 16 px,到控制点约 22 px,鼠标为 8 px。在可滚动的页面上,pageScroll: true 会把竖直滑动和普通鼠标滚轮留给页面:图表随横向滑动平移,用双指或 Ctrl + 滚轮(触控板捏合)缩放,而按在绘图、线和坐标轴上的操作仍归图表。chart.setPageScroll(on) 可在运行时切换。

当视口宽度小于 640 px 时,弹窗(设置、快捷键列表、命令面板、代码搜索) 会自动切换为底部抽屉样式,带拖动手柄,并适配安全区域的内边距。

测量工具

按住 Shift 在图表上拖动,即可测量两点之间的K线数 × 价差—— 浮层会显示价格 Δ(绝对值 + 百分比)、K线数量和时间跨度。 松开鼠标后浮层立即消失,不会保存到状态中。

事件

所有事件都通过 ChartEventMap 提供类型:

chart.on('orderPlace', e => /* OrderPlacePayload */)
chart.on('orderModify', e => /* OrderModifyPayload */)
chart.on('signalMarkerAdd', e => /* { marker } */)
chart.on('tradeZoneAdd', e => /* { zone } */)
chart.on('dataUpdate', e => /* { length } */)
chart.on('ordersChange', e => /* { orders } */)
chart.on('positionsChange', e => /* { positions } */)
chart.on('executionFill', e => /* { side, price, quantity, reason, pnl } */)
chart.on('chartContextMenu', e => /* { area, x, y, price, time } */)
chart.on('stateChange', () => /* 画线、指标、提醒、图表类型或主题可能已变化 */)
chart.on('paneChange', e => /* { instanceId, change: 'collapsed' | 'maximized' | 'order' } */)
chart.on('chartTypeChange', e => /* { type, previous } */)
chart.on('symbolChange', e => /* { symbol, previous } */)
chart.on('timeframeChange', e => /* { timeframe, previous } */)
chart.on('historyChange', e => /* { canUndo, canRedo } */)
chart.on('drawingSelect', e => /* { ids, primary } */)
chart.on('rendererChange', e => /* { renderer: 'canvas' | 'webgl', reason?: 'unsupported' | 'contextLost' | 'contextRestored' | 'limit' } */)

为你自己的改动提供撤销

recordUndo 把你的一次改动放进图表的撤销历史,和画线、指标放在一起:Ctrl/Cmd+Z 调用 undo, 重做调用 redo。针对同一 subject、时间上相近的改动算作一步。ChartWidget 就是这样记录它的设置和图表类型的。

const before = panel.color
panel.color = 'red'
chart.recordUndo({ subject: 'panel-color', undo: () => (panel.color = before), redo: () => (panel.color = 'red') })

键盘与屏幕阅读器

获得焦点的图表可以用方向键滚动(按住 Shift 一次十根K线),用 ↑/↓ 或 +/− 缩放,用 Home 和 End 跳到开头和末尾。 对屏幕阅读器而言,它是一个带摘要(品种、类型、周期、最新价)的应用程序;按键移动视图后,它会说出屏幕上显示的内容, 逗号和句号键则逐根朗读K线。a11y.labels 把这些文字换成你的语言;a11y: false 则不加这部分。 scrollBars(n) 和 selectDrawing(id) 用代码完成按键和点击所做的事。

new Chart(host, { a11y: { labels: { role: 'gráfico', summary: '{what}. Último precio {close}.' } } })

来自指针的价格可以用 chart.roundPrice(price) 对齐到市场的价格刻度: 取品种 minTick 的整数倍,未设置时按品种的价格精度取整。ChartWidget 的菜单、下单窗口和画线设置的 Coordinates 标签页都会这样做。 chart.getPriceStep() 返回这个步长本身。

ChartWidget

为 Chart 包上一套完整界面。同一个实例可通过 widget.chart 访问。

import { ChartWidget } from '@tradecanvas/chart/widget'

const widget = new ChartWidget(host, {
  symbol: 'BTCUSDT',
  timeframe: '5m',
  theme: 'dark',
  adapter: new BinanceAdapter(),
  historyLimit: 500,
  trading: true,
  features: { 'toolbar.replay': false },   // 组件的开关:见“定制”
  onReady: (chart) => { /* ... */ },
})

widget.chart.setData(...)
widget.destroy()

组件快捷键

快捷键操作
Ctrl / ⌘ + K命令面板(指标、图表类型、画线……)
Ctrl / ⌘ + P代码搜索——在已配置的品种列表中模糊查找
Ctrl / ⌘ + S保存布局(首次保存时会询问名称)
0–9输入周期(5、15m、1h、1D)后按 Enter(设置 intervalTyping: false 可关闭)
Alt + T / H / J / V / C / F趋势线、水平线、水平射线、垂直线、十字线、斐波那契回撤
?显示快捷键列表
Alt + 点击图表在鼠标所在K线上固定 OHLC 提示(同时显示与实时十字光标的差值)
Esc取消固定提示 / 取消当前画线
点击工具栏中的品种打开代码搜索弹窗
点击工具栏中的播放按钮打开K线回放进度条(播放 / 单步 / 跳转 / 速度)

运行时可用 widget.setSymbols(['BTCUSDT', 'ETHUSDT', …]) 更新可搜索的品种列表。

数据窗口

一个浮动的读数面板,显示鼠标所在K线的精确 O/H/L/C/V、K线涨跌,以及每个已添加指标的数值—— 移动十字光标时实时更新。可从命令面板中切换(Ctrl/⌘ K → “显示/隐藏数据窗口”)。

可分享视图(深层链接)

把整个视图——品种、周期、图表类型、价格坐标、指标(含参数)和画线——编码为一个紧凑的、 可安全放入 URL 的字符串,用于深层链接。设置 shareUrl: true 后, 组件会在加载时恢复 #tcw=… 哈希,命令面板中的“分享视图”操作 会把链接复制到剪贴板。

const widget = new ChartWidget(host, { shareUrl: true })

const token = widget.exportState()      // portable string
await widget.importState(token)         // restore a view
await widget.copyShareLink()            // copy "<url>#tcw=<token>"

命名布局

工具栏上的布局按钮会以一个名称保存图表:品种、周期、价格坐标、图表类型、指标、画线和提醒 (不包括主题,主题由查看者自己决定)。可从该按钮的菜单中打开、重命名和删除布局; 当前打开的布局会在变化时自动保存,按 Ctrl/⌘ S 也可以保存。 除非你提供一个 storage,布局都保存在当前浏览器的 localStorage 中。 storage 由四个方法组成,每个方法都可以返回 promise。

import { ChartWidget, type LayoutStorage } from '@tradecanvas/chart/widget'

const server: LayoutStorage = {
  list: () => api.get('/layouts'),              // [{ id, name, symbol, timeframe, updatedAt }]
  load: (id) => api.get(`/layouts/${id}`),     // { ...summary, content } 或 null
  save: (layout) => api.put(`/layouts/${layout.id}`, layout),
  remove: (id) => api.delete(`/layouts/${id}`),
}

const widget = new ChartWidget(host, {
  layouts: { storage: server, autoSave: true, openLast: true },  // 设为 false 则不启用
})

const layouts = widget.getLayoutSession()!
await layouts.saveAs('Swing BTC')
await layouts.open(id)
layouts.current()          // { id, name, … } 或 null
layouts.setAutoSave(false)

// 只取内容本身,可以存放在任何地方
const json = widget.getLayoutContent()
await widget.applyLayoutContent(json)

内置两种存储:localStorageLayouts(prefix) 和 memoryLayouts()。 读取已保存的内容时会做防御性校验:无法解析的布局会被拒绝,而不会只应用一半。 每个布局都会记录自己的 kind('chart' 或 'grid'), 因此图表组件和网格可以共用一个存储,各自只列出自己的布局。保存、打开和自动保存会逐个依次执行, 因此保存绝不会写入在它之后打开的布局。

按品种保存的布局

另外,还可以自动把每个品种的指标组合、画线、提醒和图表类型 持久化到 localStorage:

new ChartWidget(host, {
  symbol: 'BTCUSDT',
  symbols: ['BTCUSDT', 'ETHUSDT', 'SOLUSDT'],
  adapter: new BinanceAdapter(),
  persistLayouts: true,  // or { keyPrefix: 'myapp:', debounceMs: 2000 }
})

// Reset a single symbol's layout
widget.clearSavedLayout('BTCUSDT')

切换品种和销毁组件时都会立即写入布局,因此用户离开页面时不会丢失任何内容。

拖放导入数据

把 CSV 或 JSON 文件拖到图表上即可立即载入。默认启用—— 设置 dragDropImport: false 可关闭。解析器支持常见的列布局 (time, open, high, low, close, volume)、 ISO 8601 时间戳以及 Unix 秒 / 毫秒。

// Programmatic use
import { parseOHLCV } from '@tradecanvas/chart'

const { data, rowCount, skipped } = parseOHLCV(csvText)
chart.setData(data)

周期重采样

用 widget.setData() 传入你最细粒度的序列,工具栏的周期按钮会在客户端 进行聚合——一份数据驱动所有周期,无需重新请求。只要没有连接实时适配器就会生效; 设置 resampleTimeframes: false 可关闭。周线默认以周一为起点 (weekStartsOn: 0 表示周日)。

const widget = new ChartWidget(host, {
  symbol: 'BTCUSDT',
  timeframe: '1h',
  timeframes: ['5m', '15m', '1h', '4h', '1d', '1w'],
})
widget.setData(oneMinuteBars)   // base series; clicking 4h/1d/1w resamples it

// Or use the pure function directly
import { resampleOHLCV, inferTimeframeMs } from '@tradecanvas/chart'

const hourly = resampleOHLCV(oneMinuteBars, '1h')   // OHLC merged, volume summed
const fourHour = resampleOHLCV(oneMinuteBars, '4h', { weekStartsOn: 1 })

分组遵循日历规则:日内和日线周期对齐 UTC 纪元边界,周线对齐所配置的一周起始日, 月 / 季度 / 年对齐日历边界。输入的K线永远不会被修改。

自选列表

右侧面板中有多个品种列表,每一行显示最新价、涨跌幅 % 和走势图。列表可在面板菜单中切换、新建、重命名和删除; 品种可从代码搜索(+)添加,可移除,也可通过拖动或 Alt+↑/↓ 重新排序。各行的报价来自适配器的 subscribeQuotes、你自己的报价源,或你推送的数据。

const widget = new ChartWidget(host, {
  symbol: 'BTCUSDT',
  adapter: new BinanceAdapter(),               // its quotes fill the rows
  watchlist: {
    lists: [
      { id: 'majors', name: 'Majors', symbols: ['BTCUSDT', 'ETHUSDT'] },
      { id: 'alts', name: 'Alts', symbols: ['SOLUSDT', 'ADAUSDT'] },
    ],
    persist: true,                             // kept in this browser
    onChange: (lists, active) => save(lists),  // or keep them yourself
  },
})

widget.addToWatchlist('BNBUSDT', 'alts')
widget.setActiveWatchlist('alts')
widget.setQuotes([{ symbol: 'AAPL', last: 190.2, prevClose: 188.1 }])   // from your own feed
widget.getQuote('AAPL')

watchlist: true 仍和以前一样显示一个由 symbols 组成的列表;setWatchlistEntry 仍可推送某一行的价格、涨跌和走势图。

品种信息

从工具栏的 ⓘ 按钮(或命令面板)打开的面板:品种的名称、最新价和涨跌、市场状态及距下次开盘或收盘的倒计时、 当日开盘价、波动区间、成交量、昨收、买价和卖价、最小变动价位、货币、时区和交易时段,以及新闻。市场休市时,状态栏会显示出来。

new ChartWidget(host, {
  symbol: 'AAPL',
  news: (symbol, limit) => api.headlines(symbol, limit),   // or the adapter's fetchNews
})
widget.toggleSymbolInfo(true)

导航、撤销与文字方向

图表上的按钮——缩小和放大、向前和向后滚动(按住会持续滚动)、重置——在鼠标位于图表上时显示 (navigation: false 可去掉它们)。设置的改动和图表类型的切换可以用 Ctrl/Cmd+Z 撤销,与画线和指标一样。 在阿拉伯语、希伯来语、波斯语或乌尔都语下,组件会左右镜像(dir: 'auto';或 'rtl' / 'ltr'); 图表中的时间仍从左向右推进。

收藏的画线工具

把常用的画线工具固定到侧边栏顶部的收藏栏。 右键点击任意工具(在分组弹出菜单或收藏栏中)即可固定或取消固定; 该设置会持久化到 localStorage。可用 drawingFavorites 预设初始收藏:

new ChartWidget(host, {
  drawingFavorites: ['trendLine', 'horizontalLine', 'fibRetracement', 'rectangle'],
})

画线样式与模板

画线侧边栏上的调色板按钮会打开样式浮层——为下一条画线(以及当前选中的画线)选择颜色、 线宽和线型,还可以保存带名称的模板到 localStorage,一键复用。 对应的代码调用:

chart.setDrawingStyle({ color: '#e8505b', lineWidth: 2, lineStyle: 'dashed' })
chart.getDrawingStyle()
chart.setSelectedDrawingStyle({ color: '#1fa874' })  // restyle the selected drawing

对象树

工具栏中的图层按钮会打开对象树面板,列出所有已添加的指标和画线。 指标可以删除;每条画线都可以单独显示 / 隐藏、锁定 / 解锁、设置和删除, 分组则连同其中的画线一起列出。 默认启用——设置 objectTree: false 可关闭。画线控制对应以下调用:

chart.getDrawings()                 // DrawingState[] (id, type, visible, locked)
chart.setDrawingVisible(id, false)  // hide a single drawing
chart.setDrawingLocked(id, true)    // lock it from edits
chart.removeDrawing(id)
chart.groupDrawings(ids, 'Weekly levels')  // 一起隐藏、锁定和选中
chart.renameDrawingGroup(groupId, 'Old highs')
chart.getActiveIndicators()         // active indicator instances
chart.updateIndicator(instanceId, { period: 50 })  // re-tune params live
chart.removeIndicator(instanceId)

画线设置与菜单

双击一条画线,或在对象树中点击它的齿轮按钮,即可打开它的设置:样式、工具自身的设置 (斐波那契级别、延伸、标签……)以及按图表时区显示的各个点。右键点击画线可打开它的菜单: 设置、基于其线条的提醒、顺序、分组、锁定、隐藏、创建副本和删除。侧边栏里还有橡皮擦、 缩放工具,以及可关闭、弱、强三档的磁吸。底层 API 参见 画线工具。

每行指标上的齿轮按钮会打开设置对话框,它会读取指标的参数 (数值、开关、颜色),并通过 updateIndicator 实时应用修改—— 修改周期或颜色时无需先删除再重新添加。

对象树中的对比区域可以把其他品种叠加为归一化的折线。 连接实时适配器时,点击 + 按钮会打开品种选择器,通过 adapter.fetchHistory 获取该品种的历史数据,并以百分比模式添加(这样价格量级不同的品种可以共用一个坐标轴)。 切换周期时,对比数据会自动重新获取。对应的代码调用:

widget.addCompareSymbol('ETHUSDT')   // fetches + overlays (needs an adapter)

// or drive the chart directly with your own data
chart.addCompareSymbol('ETHUSDT', 'ETH', ethBars, '#627eea')
chart.setCompareMode('percent')      // 'percent' | 'absolute'
chart.removeCompareSymbol('ETHUSDT')

价格提醒

除了价格水平,提醒还可以把一条线与另一条线比较(价格穿越移动平均线、MACD 穿越其信号线), 在若干根K线内(2 到 500 根)涨跌达到一定百分比时触发,只看已收盘的K线(影线刺破后又收回不会触发), 以及到期失效。组件的提醒面板支持以上全部功能;在代码中,它们是 addAlert 的最后一个参数。 每次有新价格(来自 setCurrentPrice 或已连接的数据源)时,提醒都会与其监控的线一起检查:

const ema = chart.addIndicator('ema', { period: 50 })
const rsi = chart.addIndicator('rsi')
chart.addAlert(NaN, 'crossingUp', 'above the 50 EMA', 'price', undefined, { target: `${ema}:value` })
chart.addAlert(NaN, 'movesUp', 'pump', 'price', undefined, { percent: 5, bars: 12 })
chart.addAlert(70, 'greaterThan', 'RSI closed above 70', `${rsi}:value`, 'RSI', { onBarClose: true })
chart.addAlert(64_000, 'crossing', 'today only', 'price', undefined, { expiresAt: Date.now() + 86_400_000 })

chart.on('alertExpired', (e) => e.payload)   // it reached its time without firing
chart.on('alertTriggered', (e) => e.payload)  // { id, condition, channel, target?, percent?, bars?, … }

工具栏中的铃铛按钮会打开浮动面板,用于添加、查看和删除价格提醒; 提醒触发时会弹出通知。提醒线也可以拖动—— 在图表上抓住它并滑动即可修改价格(移动提醒会重新激活它)。默认启用—— 设置 alerts: false 可关闭。也可以通过 Chart API 和带类型的提醒事件以代码方式控制:

// Add from code (condition: 'crossing' | 'crossingUp' | 'crossingDown'
//                          | 'greaterThan' | 'lessThan')
const id = chart.addAlert(64200, 'crossingUp', 'breakout')
chart.removeAlert(id)
chart.getAlerts()      // PriceAlert[]
chart.saveAlerts('tcw:alerts:BTCUSDT')   // localStorage persistence
chart.loadAlerts('tcw:alerts:BTCUSDT')

// React to triggers
chart.on('alertTriggered', (e) => {
  console.log('hit', e.payload.price, e.payload.message)
})
// also: 'alertAdd' / 'alertRemove' / 'alertUpdate' (fired on drag)

// Indicator alerts: bind to an indicator line via channel '<instanceId>:<key>'.
// In the widget, the alerts panel's source dropdown lists every active line.
const ema = chart.addIndicator('rsi')
chart.addAlert(70, 'crossingUp', 'RSI overbought', `${ema}:rsi`, 'RSI')

提醒触发时可选择播放声音和 / 或发送桌面通知(两者默认都关闭)。 sound: true 播放内置提示音;也可以传入 URL 使用自定义声音。 desktop: true 使用 Notification API,首次使用时会请求权限。

new ChartWidget(host, {
  alertNotifications: { sound: true, desktop: true },
})

功能开关

112 个开关,全部默认开启,直到被关闭:不带点的名称是一整项功能,在它出现的每个地方关闭;带点的名称是一个位置。开关可在组件运行时切换。完整列表及对应的旧选项见定制。

new ChartWidget(host, { features: { 'toolbar.replay': false, 'sidebar.patterns': false, hotkeys: false } })   // 一个按钮、一组绘图工具、所有快捷键

widget.setFeatures({ sidebar: false, statusBar: false })
widget.isFeatureOn('sidebar')    // false
widget.getFeatures()             // 所有开关,开或关

自定义按钮和菜单项

向工具栏添加按钮(内置图标或你自己的元素、文字、开关),并向图表的右键菜单添加菜单项, 它们会排在组件自带的项目之后。

const news = widget.addToolbarButton({
  id: 'news',
  label: 'News',
  icon: 'bell',            // 或一个 <svg> 元素;或 text: 'News'
  side: 'right',           // 'left' 会与图表控件放在一起
  toggle: true,
  onClick: () => news?.setActive(togglePanel()),
})
news?.setText('3')
news?.remove()

new ChartWidget(host, {
  chartMenuItems: ({ area, price, time }) => area === 'plot' && price !== undefined
    ? [{ label: `Copy ${price.toFixed(2)}`, icon: 'check', onSelect: () => copy(price) }]
    : [],
})

另外还有工具栏上的菜单按钮、绘图侧栏上的按钮、状态栏上的项目、各个栏和图表上方属于你的位置,以及绘图或指标菜单末尾的菜单项(见定制):

widget.addToolbarDropdown({ id: 'scans', label: 'Scans', icon: 'layers', items: () => [...] })   // 每次打开时重新获取
widget.addSidebarButton({ id: 'ruler', label: 'Ruler', icon: 'ruler', toggle: true, onClick })
widget.addStatusBarItem({ id: 'latency', text: '12 ms', label: 'Latency' })
widget.getSlot('chart')          // 图表上方的一层
widget.addHotkey({ keys: 'Alt+N', label: 'New note', onPress })   // 你自己的快捷键,按键相同时取代组件自带的

new ChartWidget(host, {
  drawingMenuItems: ({ id, type }) => [{ label: 'Share', onSelect: () => share(id) }],
  indicatorMenuItems: ({ instanceId, indicatorId }) => [{ label: 'Explain', onSelect: () => explain(indicatorId) }],
})

ChartWidgetGrid

多个图表组件并排显示,每个都有自己的品种、周期、指标和画线。上方的工具条用于选择排列方式、 联动各个图表,并把整个网格保存为一个命名布局。最后点击的图表就是当前图表(带边框)。

import { ChartWidgetGrid } from '@tradecanvas/chart/widget'

const grid = new ChartWidgetGrid(host, {
  layout: '2x2',                                   // '1x1' '1x2' '2x1' '2x2' '1x3' '3x1' '2x3' '3x2'
  widget: { timeframe: '1h' },                    // 作用于每个图表
  adapter: () => new BinanceAdapter(),            // 每个图表一个:一个适配器只维持一条数据流
  cells: [{ symbol: 'BTCUSDT' }, { symbol: 'ETHUSDT' }, { symbol: 'SOLUSDT' }, { symbol: 'BNBUSDT' }],
  sync: { crosshair: true, time: false, symbol: false, interval: false, drawings: false, replay: false },
})

grid.setLayout('1x2')
grid.setSync({ interval: true })   // 让其他图表与当前图表保持一致
grid.applyOverrides({ 'grid.vertical.visible': false })   // 所有图表的外观,以及之后添加的图表
grid.setFeatures({ 'toolbar.replay': false })               // 所有图表的开关,以及之后添加的图表
grid.getActiveWidget().getChart()
grid.getLayoutSession()?.saveAs('Majors')

// 每个图表创建时(初始时,以及网格变大时)
new ChartWidgetGrid(host, {
  onChartAdd: (widget, index) => widget.getChart().addIndicator('ema', { period: 21 }),
})

十字光标同步会在每个图表上显示指针所在的时间;时间同步会让其他图表随正在操作的图表一起滚动和缩放; 画线会复制到显示同一品种的图表上(开启时会把这些图表的画线合并到一起,不会丢失任何画线); 回放同步会把其他图表回放到与正在回放的图表相同的时间(K线比回放步长更长的图表会显示包含该时间的整根K线)。 网格缩小时被移除的图表会被收起并保留在已保存的布局中,网格再次变大时会原样恢复; 全新的图表在品种和周期处于同步状态时,会使用当前图表的品种和周期打开。

ChartGrid

由无界面图表(不带工具栏)组成的同步联动多图表布局;如需完整界面,请参见 ChartWidgetGrid。

import { ChartGrid } from '@tradecanvas/chart'

const grid = new ChartGrid(host, { layout: '2x2', theme: 'dark' })
// 每个图表一个适配器:一个适配器只维持一条数据流
await grid.connectAll(() => new BinanceAdapter(), ['BTCUSDT','ETHUSDT','SOLUSDT','BNBUSDT'], '5m')
grid.setLayout('1x2')

布局:'1x1'、'1x2'、'2x1'、'2x2'、'1x3'、'3x1'、'2x3'、'3x2'。

+ p.toFixed(2))
chart.setPriceFormat(null) // decimals again chart.setTimeFormatter((time, { kind, timeZone }) => // kind: 'year', 'month', 'day' (the step a label starts), 'time' (within a day), 'crosshair' new Intl.DateTimeFormat('en-GB', { timeZone: timeZone ?? undefined, hour: '2-digit', minute: '2-digit' }).format(time))

坐标轴如何选择标签

时间轴为开启最大日历步长的 K 线标注:一年(粗体)、一个月、一天、一个整点,彼此至少相隔 80 px。因此在 5Y 时显示 2022 · Jul · 2023,一个月时 Oct · 8 · 15,一天之内 12:00 · 18:00。标签属于 K 线而非屏幕:平移时随 K 线移动,前面加载更多历史时保持原位。最早一根 K 线左侧没有标签。月份名称遵循 setNumberLocale,网格线与标签位置一致。

价格轴按其高度放置尽可能多的标签,彼此至少相隔 30 px,并落在市场的最小变动价位上。对数坐标下步长随价格增大,因此底部与顶部一样有标签;百分比或指数坐标下,标签是从基准起算的整百分比或整指数点。指标窗格也按自身高度确定标签数量。

市场:最小变动价位、涨跌停与交易时段

setMarket 把交易所的规则交给图表:K线颜色、小数位数、最小变动价位(可随价格变化)、 当日涨停价和跌停价,以及每手股数。roundPrice 和 getPriceStep 遵循价格所在区间的最小变动价位,因此从图表菜单得到的价格都是有效价格;组件的下单窗口 也会按最小变动价位、当日涨跌停价和整手检查订单。预设还带有交易所的交易时段和时区,供 getCurrentSession 和你自己的代码使用;图表的交易时段着色和延长交易时段则来自 setSymbolInfo。内置预设覆盖截至 2025 年 5 月的越南交易所规则:HOSE(按价格每档 10、50 或 100 越南盾,±7%)、其 ETF 和备兑权证(10 越南盾)、HNX(±10%,设有收盘后交易时段)、UPCoM(±15%) 以及 VN30 指数期货(每档 0.1 点,±7%,8:45 开盘);股票每手 100 股,绿涨红跌,紫色为涨停价、青色为跌停价、 黄色为参考价。预设中的价格以千越南盾为单位,与行情报价板一致;scaleMarketPrices(preset, 1000) 则按越南盾读取价格。

import {
  MARKET_HOSE, scaleMarketPrices, priceStepAt, roundToPriceStep, getCurrentSession,
  vn30fContracts, vn30fLastTradingDay,
} from '@tradecanvas/chart'

chart.setMarket(MARKET_HOSE)                 // 或用 scaleMarketPrices(MARKET_HOSE, 1000) 处理 25350 这样的价格
chart.setPriceLimits(25.35)                  // 参考价:前一交易日的收盘价
chart.getPriceLimits()                       // { ceiling: 27.1, floor: 23.6, reference: 25.35 }
chart.roundPrice(25.37)                      // 25.35,最小变动 50 越南盾
chart.getPriceStep(9.5)                      // 低于 10,000 越南盾时为 0.01

// 单独使用这些规则
priceStepAt(25.35, MARKET_HOSE)              // 0.05
roundToPriceStep(10.486, MARKET_HOSE, 'down') // 10.45
getCurrentSession(MARKET_HOSE.sessions, Date.now(), MARKET_HOSE.timeZone) // { name: 'ATC', type: 'preClose', … },仅按时钟判断

// VN30 期货:四个挂牌合约及其最后交易日
vn30fContracts(Date.now())                   // [{ symbol: 'VN30F2611', lastTradingDay: '2026-11-19', … }, …]
vn30fLastTradingDay(2026, 12, (day) => holidays.has(day)) // 第三个星期四,遇节假日则提前

自定义市场是一个 MarketConfig:priceStep(按价格区间分档时用 priceSteps: [{ from, step }])、priceLimits、 volumeUnit(每手数量)、sessions、timeZone 和 colorScheme。品种自身的 minTick 优先于市场的最小变动价位。交易所会不时调整规则; 在依赖某个预设之前,请先对照你所在交易所的现行规则进行核实。

品种对比

可以在价格坐标上显示另一个品种的涨跌幅(addCompareSymbol),也可以把它的价格放在独立坐标或独立窗格中, 或者显示图表收盘价与它的价差或比值——后几种都是指标(compareSymbol、spread), 和其他指标一样有图例、数值标签和提醒,并会保存在布局中。另一个品种的K线按时间与图表的K线对齐。 图表会请求它需要的K线;切换周期后请重新提供。correlation(按收盘价)和 correlationlog (按逐根K线收益率)以同样方式读取另一个品种,取值在 −1 到 1 之间。

chart.addCompareSymbol('eth', 'ETHUSDT', ethBars, '#7c4dff')   // percent change, on this scale
chart.addIndicator('compareSymbol', { symbol: 'ETHUSDT' }, 'bottom', { scale: 'left' })  // own scale
chart.addIndicator('spread', { symbol: 'ETHUSDT', mode: 'ratio' })                      // own pane
chart.addIndicator('correlation', { symbol: 'ETHUSDT', length: 20 })                    // −1 to 1, own pane

chart.on('symbolSeriesRequest', async ({ payload }) =>
  chart.setSymbolSeries(payload.symbol, await adapter.fetchHistory(payload.symbol, '1h', 1000)))
chart.getRequiredSymbols()                           // what to fetch again on a new interval
chart.setPaneScale(spreadId, { percent: true })     // a pane in percent of its first value

在 ChartWidget 中,对象树的对比按钮会先询问品种,再询问对比方式:涨跌幅、独立坐标、独立副图、价差、比值,或按价格或收益率计算的相关性。

导出数据

以 CSV 或 JSON 导出K线,每条指标线各占一列,列名与图例中的名称一致。ChartWidget 的图表右键菜单中有 导出数据 (CSV)。

chart.exportAllData('csv', 'btc-1h.csv')                 // every bar loaded
chart.exportVisibleData('json', undefined, { indicators: false })
const text = chart.getExportText('csv', { range: 'visible' })
const { bars, columns } = chart.getExportData()          // columns: { name, values }[]

成交量分布(Volume Profile)

在可见范围内按价格分组统计成交量的水平直方图。默认关闭—— 可通过代码或组件的设置面板开启:

chart.setVolumeProfileVisible(true)
chart.setVolumeProfileConfig({
  buckets: 48,        // resolution of the histogram
  widthRatio: 0.18,   // % of chart width
  opacity: 0.32,
  highlightPoC: true, // mark the highest-volume bucket
})

波段标记(枢轴点)

用小三角形标出分形波段高点 / 低点(▼ 位于已确认的枢轴高点上方,▲ 位于枢轴低点下方)。 强度参数决定两侧各需要有多少根更低的K线。可在设置面板中切换,或者:

chart.setPivotMarkersVisible(true)
chart.setPivotMarkersConfig({ left: 5, right: 5, showLabels: true })

// market-structure labels (HH / HL / LH / LL) instead of price
chart.setPivotMarkersConfig({ structureLabels: true })

// pure detection + classification are exported
import { findPivots, classifyPivots } from '@tradecanvas/core'
const pivots = findPivots(bars, 5, 5)        // [{ index, price, type }]
const structure = classifyPivots(pivots)     // adds label: 'HH'|'LH'|'HL'|'LL'

交易时段着色(常规交易时段)

把常规交易时段以外的K线(盘前 / 盘后或隔夜休市)调暗,让常规交易时段更加醒目。 默认使用美股常规交易时段(纽约时间 09:30–16:00,已考虑夏令时); 可以用“一天中的分钟数”加上市场时区来配置时间窗口。 如果某个品种的数据源提供了交易时段信息,会自动设置。

chart.setSessionShadingVisible(true)
chart.setSessionShadingConfig({
  startMinute: 9 * 60 + 30,     // 09:30
  endMinute: 16 * 60,           // 16:00 (end-exclusive; end < start wraps midnight)
  timeZone: 'America/New_York', // or tzOffsetMinutes: -300 for a fixed offset
})

延长交易时段。关闭后,品种常规交易时段(按其 timezone 计算的 SymbolInfo.sessions)以外的K线会离开图表;它们会被暂存起来,实时K线和历史分页也是如此, 重新开启后即恢复。日线及更长周期的K线保持不变。

chart.setSymbolInfo({ symbol: 'AAPL', timezone: 'America/New_York', sessions: [{ start: '09:30', end: '16:00' }] })
chart.setExtendedHours(false)     // or new Chart(host, { extendedHours: false })
chart.isExtendedHoursVisible()

前一周期高低点(PDH / PDL / PDC)

将前一日(或前一周)的最高价、最低价、收盘价以及当前周期的开盘价绘制为带标签的水平线—— 这正是日内交易者关注的支撑 / 阻力位。可在设置面板中切换,也可以直接调用:

chart.setPeriodLevelsVisible(true)
chart.setPeriodLevelsPeriod('week')   // 'day' (PDH/PDL/PDC) | 'week' (PWH/PWL/PWC)

// pure computation is exported
import { computePeriodLevels } from '@tradecanvas/core'
const levels = computePeriodLevels(bars, 'day')  // [{ id, label, price }]

市场轮廓(TPO)

一种“价格停留时间”直方图:每根K线会为其价格范围触及的每个价格档位贡献一个 TPO, 由此显示控制点(Point of Control,成交最密集的价格)和价值区(约 70% 的 TPO)。 它与成交量分布不同——按时间而非成交量加权——并固定在左侧,因此两者可以同时显示。 默认关闭;可在设置面板中切换,也可以直接调用:

chart.setMarketProfileVisible(true)
chart.setMarketProfileConfig({
  buckets: 48,
  widthRatio: 0.18,
  opacity: 0.32,
  valueAreaPct: 0.7,  // fraction of TPOs in the value area
  highlightPoC: true, // dashed line at the point of control
})

// split into one mini-profile per calendar-day session
chart.setMarketProfileConfig({ splitBySession: true })

// classic TPO letters per session (when zoomed in enough to be legible)
chart.setMarketProfileConfig({ splitBySession: true, letters: true })

// pure computation is exported too
import { computeMarketProfile, computeSessionProfiles } from '@tradecanvas/core'
const profile = computeMarketProfile(bars, priceMin, priceMax, { buckets: 48 })
const sessions = computeSessionProfiles(bars, priceMin, priceMax)  // per-day TPO

触控与移动端

手势操作
单指拖动(图表区域)平移并移动十字光标
双指捏合以中点为中心缩放
长按(约 500 ms)在K线上固定 OHLC 提示(相当于移动端的 Alt+点击)
在价格轴 / 时间轴区域内单指拖动缩放对应的坐标轴
轻点点击:click、barClick、信号标记;点在绘图上则选中它
双击轻点双击:在坐标轴上则重置该轴;在绘图上则打开其设置
在绘图上单指拖动移动它,或拖动所选绘图的控制点改变形状
选中绘图工具时:点、点——或按下、拖动、抬起放置各点;按下、拖动再抬起,一次画出两点线
在绘图上长按打开它的菜单(相当于右键)
在提醒线、订单线、括号单线或窗格分隔线上单指拖动拖动它

手指的触及范围比鼠标大:到线约 16 px,到控制点约 22 px,鼠标为 8 px。在可滚动的页面上,pageScroll: true 会把竖直滑动和普通鼠标滚轮留给页面:图表随横向滑动平移,用双指或 Ctrl + 滚轮(触控板捏合)缩放,而按在绘图、线和坐标轴上的操作仍归图表。chart.setPageScroll(on) 可在运行时切换。

当视口宽度小于 640 px 时,弹窗(设置、快捷键列表、命令面板、代码搜索) 会自动切换为底部抽屉样式,带拖动手柄,并适配安全区域的内边距。

测量工具

按住 Shift 在图表上拖动,即可测量两点之间的K线数 × 价差—— 浮层会显示价格 Δ(绝对值 + 百分比)、K线数量和时间跨度。 松开鼠标后浮层立即消失,不会保存到状态中。

事件

所有事件都通过 ChartEventMap 提供类型:

chart.on('orderPlace', e => /* OrderPlacePayload */)
chart.on('orderModify', e => /* OrderModifyPayload */)
chart.on('signalMarkerAdd', e => /* { marker } */)
chart.on('tradeZoneAdd', e => /* { zone } */)
chart.on('dataUpdate', e => /* { length } */)
chart.on('ordersChange', e => /* { orders } */)
chart.on('positionsChange', e => /* { positions } */)
chart.on('executionFill', e => /* { side, price, quantity, reason, pnl } */)
chart.on('chartContextMenu', e => /* { area, x, y, price, time } */)
chart.on('stateChange', () => /* 画线、指标、提醒、图表类型或主题可能已变化 */)
chart.on('paneChange', e => /* { instanceId, change: 'collapsed' | 'maximized' | 'order' } */)
chart.on('chartTypeChange', e => /* { type, previous } */)
chart.on('symbolChange', e => /* { symbol, previous } */)
chart.on('timeframeChange', e => /* { timeframe, previous } */)
chart.on('historyChange', e => /* { canUndo, canRedo } */)
chart.on('drawingSelect', e => /* { ids, primary } */)
chart.on('rendererChange', e => /* { renderer: 'canvas' | 'webgl', reason?: 'unsupported' | 'contextLost' | 'contextRestored' | 'limit' } */)

为你自己的改动提供撤销

recordUndo 把你的一次改动放进图表的撤销历史,和画线、指标放在一起:Ctrl/Cmd+Z 调用 undo, 重做调用 redo。针对同一 subject、时间上相近的改动算作一步。ChartWidget 就是这样记录它的设置和图表类型的。

const before = panel.color
panel.color = 'red'
chart.recordUndo({ subject: 'panel-color', undo: () => (panel.color = before), redo: () => (panel.color = 'red') })

键盘与屏幕阅读器

获得焦点的图表可以用方向键滚动(按住 Shift 一次十根K线),用 ↑/↓ 或 +/− 缩放,用 Home 和 End 跳到开头和末尾。 对屏幕阅读器而言,它是一个带摘要(品种、类型、周期、最新价)的应用程序;按键移动视图后,它会说出屏幕上显示的内容, 逗号和句号键则逐根朗读K线。a11y.labels 把这些文字换成你的语言;a11y: false 则不加这部分。 scrollBars(n) 和 selectDrawing(id) 用代码完成按键和点击所做的事。

new Chart(host, { a11y: { labels: { role: 'gráfico', summary: '{what}. Último precio {close}.' } } })

来自指针的价格可以用 chart.roundPrice(price) 对齐到市场的价格刻度: 取品种 minTick 的整数倍,未设置时按品种的价格精度取整。ChartWidget 的菜单、下单窗口和画线设置的 Coordinates 标签页都会这样做。 chart.getPriceStep() 返回这个步长本身。

ChartWidget

为 Chart 包上一套完整界面。同一个实例可通过 widget.chart 访问。

import { ChartWidget } from '@tradecanvas/chart/widget'

const widget = new ChartWidget(host, {
  symbol: 'BTCUSDT',
  timeframe: '5m',
  theme: 'dark',
  adapter: new BinanceAdapter(),
  historyLimit: 500,
  trading: true,
  features: { 'toolbar.replay': false },   // 组件的开关:见“定制”
  onReady: (chart) => { /* ... */ },
})

widget.chart.setData(...)
widget.destroy()

组件快捷键

快捷键操作
Ctrl / ⌘ + K命令面板(指标、图表类型、画线……)
Ctrl / ⌘ + P代码搜索——在已配置的品种列表中模糊查找
Ctrl / ⌘ + S保存布局(首次保存时会询问名称)
0–9输入周期(5、15m、1h、1D)后按 Enter(设置 intervalTyping: false 可关闭)
Alt + T / H / J / V / C / F趋势线、水平线、水平射线、垂直线、十字线、斐波那契回撤
?显示快捷键列表
Alt + 点击图表在鼠标所在K线上固定 OHLC 提示(同时显示与实时十字光标的差值)
Esc取消固定提示 / 取消当前画线
点击工具栏中的品种打开代码搜索弹窗
点击工具栏中的播放按钮打开K线回放进度条(播放 / 单步 / 跳转 / 速度)

运行时可用 widget.setSymbols(['BTCUSDT', 'ETHUSDT', …]) 更新可搜索的品种列表。

数据窗口

一个浮动的读数面板,显示鼠标所在K线的精确 O/H/L/C/V、K线涨跌,以及每个已添加指标的数值—— 移动十字光标时实时更新。可从命令面板中切换(Ctrl/⌘ K → “显示/隐藏数据窗口”)。

可分享视图(深层链接)

把整个视图——品种、周期、图表类型、价格坐标、指标(含参数)和画线——编码为一个紧凑的、 可安全放入 URL 的字符串,用于深层链接。设置 shareUrl: true 后, 组件会在加载时恢复 #tcw=… 哈希,命令面板中的“分享视图”操作 会把链接复制到剪贴板。

const widget = new ChartWidget(host, { shareUrl: true })

const token = widget.exportState()      // portable string
await widget.importState(token)         // restore a view
await widget.copyShareLink()            // copy "<url>#tcw=<token>"

命名布局

工具栏上的布局按钮会以一个名称保存图表:品种、周期、价格坐标、图表类型、指标、画线和提醒 (不包括主题,主题由查看者自己决定)。可从该按钮的菜单中打开、重命名和删除布局; 当前打开的布局会在变化时自动保存,按 Ctrl/⌘ S 也可以保存。 除非你提供一个 storage,布局都保存在当前浏览器的 localStorage 中。 storage 由四个方法组成,每个方法都可以返回 promise。

import { ChartWidget, type LayoutStorage } from '@tradecanvas/chart/widget'

const server: LayoutStorage = {
  list: () => api.get('/layouts'),              // [{ id, name, symbol, timeframe, updatedAt }]
  load: (id) => api.get(`/layouts/${id}`),     // { ...summary, content } 或 null
  save: (layout) => api.put(`/layouts/${layout.id}`, layout),
  remove: (id) => api.delete(`/layouts/${id}`),
}

const widget = new ChartWidget(host, {
  layouts: { storage: server, autoSave: true, openLast: true },  // 设为 false 则不启用
})

const layouts = widget.getLayoutSession()!
await layouts.saveAs('Swing BTC')
await layouts.open(id)
layouts.current()          // { id, name, … } 或 null
layouts.setAutoSave(false)

// 只取内容本身,可以存放在任何地方
const json = widget.getLayoutContent()
await widget.applyLayoutContent(json)

内置两种存储:localStorageLayouts(prefix) 和 memoryLayouts()。 读取已保存的内容时会做防御性校验:无法解析的布局会被拒绝,而不会只应用一半。 每个布局都会记录自己的 kind('chart' 或 'grid'), 因此图表组件和网格可以共用一个存储,各自只列出自己的布局。保存、打开和自动保存会逐个依次执行, 因此保存绝不会写入在它之后打开的布局。

按品种保存的布局

另外,还可以自动把每个品种的指标组合、画线、提醒和图表类型 持久化到 localStorage:

new ChartWidget(host, {
  symbol: 'BTCUSDT',
  symbols: ['BTCUSDT', 'ETHUSDT', 'SOLUSDT'],
  adapter: new BinanceAdapter(),
  persistLayouts: true,  // or { keyPrefix: 'myapp:', debounceMs: 2000 }
})

// Reset a single symbol's layout
widget.clearSavedLayout('BTCUSDT')

切换品种和销毁组件时都会立即写入布局,因此用户离开页面时不会丢失任何内容。

拖放导入数据

把 CSV 或 JSON 文件拖到图表上即可立即载入。默认启用—— 设置 dragDropImport: false 可关闭。解析器支持常见的列布局 (time, open, high, low, close, volume)、 ISO 8601 时间戳以及 Unix 秒 / 毫秒。

// Programmatic use
import { parseOHLCV } from '@tradecanvas/chart'

const { data, rowCount, skipped } = parseOHLCV(csvText)
chart.setData(data)

周期重采样

用 widget.setData() 传入你最细粒度的序列,工具栏的周期按钮会在客户端 进行聚合——一份数据驱动所有周期,无需重新请求。只要没有连接实时适配器就会生效; 设置 resampleTimeframes: false 可关闭。周线默认以周一为起点 (weekStartsOn: 0 表示周日)。

const widget = new ChartWidget(host, {
  symbol: 'BTCUSDT',
  timeframe: '1h',
  timeframes: ['5m', '15m', '1h', '4h', '1d', '1w'],
})
widget.setData(oneMinuteBars)   // base series; clicking 4h/1d/1w resamples it

// Or use the pure function directly
import { resampleOHLCV, inferTimeframeMs } from '@tradecanvas/chart'

const hourly = resampleOHLCV(oneMinuteBars, '1h')   // OHLC merged, volume summed
const fourHour = resampleOHLCV(oneMinuteBars, '4h', { weekStartsOn: 1 })

分组遵循日历规则:日内和日线周期对齐 UTC 纪元边界,周线对齐所配置的一周起始日, 月 / 季度 / 年对齐日历边界。输入的K线永远不会被修改。

自选列表

右侧面板中有多个品种列表,每一行显示最新价、涨跌幅 % 和走势图。列表可在面板菜单中切换、新建、重命名和删除; 品种可从代码搜索(+)添加,可移除,也可通过拖动或 Alt+↑/↓ 重新排序。各行的报价来自适配器的 subscribeQuotes、你自己的报价源,或你推送的数据。

const widget = new ChartWidget(host, {
  symbol: 'BTCUSDT',
  adapter: new BinanceAdapter(),               // its quotes fill the rows
  watchlist: {
    lists: [
      { id: 'majors', name: 'Majors', symbols: ['BTCUSDT', 'ETHUSDT'] },
      { id: 'alts', name: 'Alts', symbols: ['SOLUSDT', 'ADAUSDT'] },
    ],
    persist: true,                             // kept in this browser
    onChange: (lists, active) => save(lists),  // or keep them yourself
  },
})

widget.addToWatchlist('BNBUSDT', 'alts')
widget.setActiveWatchlist('alts')
widget.setQuotes([{ symbol: 'AAPL', last: 190.2, prevClose: 188.1 }])   // from your own feed
widget.getQuote('AAPL')

watchlist: true 仍和以前一样显示一个由 symbols 组成的列表;setWatchlistEntry 仍可推送某一行的价格、涨跌和走势图。

品种信息

从工具栏的 ⓘ 按钮(或命令面板)打开的面板:品种的名称、最新价和涨跌、市场状态及距下次开盘或收盘的倒计时、 当日开盘价、波动区间、成交量、昨收、买价和卖价、最小变动价位、货币、时区和交易时段,以及新闻。市场休市时,状态栏会显示出来。

new ChartWidget(host, {
  symbol: 'AAPL',
  news: (symbol, limit) => api.headlines(symbol, limit),   // or the adapter's fetchNews
})
widget.toggleSymbolInfo(true)

导航、撤销与文字方向

图表上的按钮——缩小和放大、向前和向后滚动(按住会持续滚动)、重置——在鼠标位于图表上时显示 (navigation: false 可去掉它们)。设置的改动和图表类型的切换可以用 Ctrl/Cmd+Z 撤销,与画线和指标一样。 在阿拉伯语、希伯来语、波斯语或乌尔都语下,组件会左右镜像(dir: 'auto';或 'rtl' / 'ltr'); 图表中的时间仍从左向右推进。

收藏的画线工具

把常用的画线工具固定到侧边栏顶部的收藏栏。 右键点击任意工具(在分组弹出菜单或收藏栏中)即可固定或取消固定; 该设置会持久化到 localStorage。可用 drawingFavorites 预设初始收藏:

new ChartWidget(host, {
  drawingFavorites: ['trendLine', 'horizontalLine', 'fibRetracement', 'rectangle'],
})

画线样式与模板

画线侧边栏上的调色板按钮会打开样式浮层——为下一条画线(以及当前选中的画线)选择颜色、 线宽和线型,还可以保存带名称的模板到 localStorage,一键复用。 对应的代码调用:

chart.setDrawingStyle({ color: '#e8505b', lineWidth: 2, lineStyle: 'dashed' })
chart.getDrawingStyle()
chart.setSelectedDrawingStyle({ color: '#1fa874' })  // restyle the selected drawing

对象树

工具栏中的图层按钮会打开对象树面板,列出所有已添加的指标和画线。 指标可以删除;每条画线都可以单独显示 / 隐藏、锁定 / 解锁、设置和删除, 分组则连同其中的画线一起列出。 默认启用——设置 objectTree: false 可关闭。画线控制对应以下调用:

chart.getDrawings()                 // DrawingState[] (id, type, visible, locked)
chart.setDrawingVisible(id, false)  // hide a single drawing
chart.setDrawingLocked(id, true)    // lock it from edits
chart.removeDrawing(id)
chart.groupDrawings(ids, 'Weekly levels')  // 一起隐藏、锁定和选中
chart.renameDrawingGroup(groupId, 'Old highs')
chart.getActiveIndicators()         // active indicator instances
chart.updateIndicator(instanceId, { period: 50 })  // re-tune params live
chart.removeIndicator(instanceId)

画线设置与菜单

双击一条画线,或在对象树中点击它的齿轮按钮,即可打开它的设置:样式、工具自身的设置 (斐波那契级别、延伸、标签……)以及按图表时区显示的各个点。右键点击画线可打开它的菜单: 设置、基于其线条的提醒、顺序、分组、锁定、隐藏、创建副本和删除。侧边栏里还有橡皮擦、 缩放工具,以及可关闭、弱、强三档的磁吸。底层 API 参见 画线工具。

每行指标上的齿轮按钮会打开设置对话框,它会读取指标的参数 (数值、开关、颜色),并通过 updateIndicator 实时应用修改—— 修改周期或颜色时无需先删除再重新添加。

对象树中的对比区域可以把其他品种叠加为归一化的折线。 连接实时适配器时,点击 + 按钮会打开品种选择器,通过 adapter.fetchHistory 获取该品种的历史数据,并以百分比模式添加(这样价格量级不同的品种可以共用一个坐标轴)。 切换周期时,对比数据会自动重新获取。对应的代码调用:

widget.addCompareSymbol('ETHUSDT')   // fetches + overlays (needs an adapter)

// or drive the chart directly with your own data
chart.addCompareSymbol('ETHUSDT', 'ETH', ethBars, '#627eea')
chart.setCompareMode('percent')      // 'percent' | 'absolute'
chart.removeCompareSymbol('ETHUSDT')

价格提醒

除了价格水平,提醒还可以把一条线与另一条线比较(价格穿越移动平均线、MACD 穿越其信号线), 在若干根K线内(2 到 500 根)涨跌达到一定百分比时触发,只看已收盘的K线(影线刺破后又收回不会触发), 以及到期失效。组件的提醒面板支持以上全部功能;在代码中,它们是 addAlert 的最后一个参数。 每次有新价格(来自 setCurrentPrice 或已连接的数据源)时,提醒都会与其监控的线一起检查:

const ema = chart.addIndicator('ema', { period: 50 })
const rsi = chart.addIndicator('rsi')
chart.addAlert(NaN, 'crossingUp', 'above the 50 EMA', 'price', undefined, { target: `${ema}:value` })
chart.addAlert(NaN, 'movesUp', 'pump', 'price', undefined, { percent: 5, bars: 12 })
chart.addAlert(70, 'greaterThan', 'RSI closed above 70', `${rsi}:value`, 'RSI', { onBarClose: true })
chart.addAlert(64_000, 'crossing', 'today only', 'price', undefined, { expiresAt: Date.now() + 86_400_000 })

chart.on('alertExpired', (e) => e.payload)   // it reached its time without firing
chart.on('alertTriggered', (e) => e.payload)  // { id, condition, channel, target?, percent?, bars?, … }

工具栏中的铃铛按钮会打开浮动面板,用于添加、查看和删除价格提醒; 提醒触发时会弹出通知。提醒线也可以拖动—— 在图表上抓住它并滑动即可修改价格(移动提醒会重新激活它)。默认启用—— 设置 alerts: false 可关闭。也可以通过 Chart API 和带类型的提醒事件以代码方式控制:

// Add from code (condition: 'crossing' | 'crossingUp' | 'crossingDown'
//                          | 'greaterThan' | 'lessThan')
const id = chart.addAlert(64200, 'crossingUp', 'breakout')
chart.removeAlert(id)
chart.getAlerts()      // PriceAlert[]
chart.saveAlerts('tcw:alerts:BTCUSDT')   // localStorage persistence
chart.loadAlerts('tcw:alerts:BTCUSDT')

// React to triggers
chart.on('alertTriggered', (e) => {
  console.log('hit', e.payload.price, e.payload.message)
})
// also: 'alertAdd' / 'alertRemove' / 'alertUpdate' (fired on drag)

// Indicator alerts: bind to an indicator line via channel '<instanceId>:<key>'.
// In the widget, the alerts panel's source dropdown lists every active line.
const ema = chart.addIndicator('rsi')
chart.addAlert(70, 'crossingUp', 'RSI overbought', `${ema}:rsi`, 'RSI')

提醒触发时可选择播放声音和 / 或发送桌面通知(两者默认都关闭)。 sound: true 播放内置提示音;也可以传入 URL 使用自定义声音。 desktop: true 使用 Notification API,首次使用时会请求权限。

new ChartWidget(host, {
  alertNotifications: { sound: true, desktop: true },
})

功能开关

112 个开关,全部默认开启,直到被关闭:不带点的名称是一整项功能,在它出现的每个地方关闭;带点的名称是一个位置。开关可在组件运行时切换。完整列表及对应的旧选项见定制。

new ChartWidget(host, { features: { 'toolbar.replay': false, 'sidebar.patterns': false, hotkeys: false } })   // 一个按钮、一组绘图工具、所有快捷键

widget.setFeatures({ sidebar: false, statusBar: false })
widget.isFeatureOn('sidebar')    // false
widget.getFeatures()             // 所有开关,开或关

自定义按钮和菜单项

向工具栏添加按钮(内置图标或你自己的元素、文字、开关),并向图表的右键菜单添加菜单项, 它们会排在组件自带的项目之后。

const news = widget.addToolbarButton({
  id: 'news',
  label: 'News',
  icon: 'bell',            // 或一个 <svg> 元素;或 text: 'News'
  side: 'right',           // 'left' 会与图表控件放在一起
  toggle: true,
  onClick: () => news?.setActive(togglePanel()),
})
news?.setText('3')
news?.remove()

new ChartWidget(host, {
  chartMenuItems: ({ area, price, time }) => area === 'plot' && price !== undefined
    ? [{ label: `Copy ${price.toFixed(2)}`, icon: 'check', onSelect: () => copy(price) }]
    : [],
})

另外还有工具栏上的菜单按钮、绘图侧栏上的按钮、状态栏上的项目、各个栏和图表上方属于你的位置,以及绘图或指标菜单末尾的菜单项(见定制):

widget.addToolbarDropdown({ id: 'scans', label: 'Scans', icon: 'layers', items: () => [...] })   // 每次打开时重新获取
widget.addSidebarButton({ id: 'ruler', label: 'Ruler', icon: 'ruler', toggle: true, onClick })
widget.addStatusBarItem({ id: 'latency', text: '12 ms', label: 'Latency' })
widget.getSlot('chart')          // 图表上方的一层
widget.addHotkey({ keys: 'Alt+N', label: 'New note', onPress })   // 你自己的快捷键,按键相同时取代组件自带的

new ChartWidget(host, {
  drawingMenuItems: ({ id, type }) => [{ label: 'Share', onSelect: () => share(id) }],
  indicatorMenuItems: ({ instanceId, indicatorId }) => [{ label: 'Explain', onSelect: () => explain(indicatorId) }],
})

ChartWidgetGrid

多个图表组件并排显示,每个都有自己的品种、周期、指标和画线。上方的工具条用于选择排列方式、 联动各个图表,并把整个网格保存为一个命名布局。最后点击的图表就是当前图表(带边框)。

import { ChartWidgetGrid } from '@tradecanvas/chart/widget'

const grid = new ChartWidgetGrid(host, {
  layout: '2x2',                                   // '1x1' '1x2' '2x1' '2x2' '1x3' '3x1' '2x3' '3x2'
  widget: { timeframe: '1h' },                    // 作用于每个图表
  adapter: () => new BinanceAdapter(),            // 每个图表一个:一个适配器只维持一条数据流
  cells: [{ symbol: 'BTCUSDT' }, { symbol: 'ETHUSDT' }, { symbol: 'SOLUSDT' }, { symbol: 'BNBUSDT' }],
  sync: { crosshair: true, time: false, symbol: false, interval: false, drawings: false, replay: false },
})

grid.setLayout('1x2')
grid.setSync({ interval: true })   // 让其他图表与当前图表保持一致
grid.applyOverrides({ 'grid.vertical.visible': false })   // 所有图表的外观,以及之后添加的图表
grid.setFeatures({ 'toolbar.replay': false })               // 所有图表的开关,以及之后添加的图表
grid.getActiveWidget().getChart()
grid.getLayoutSession()?.saveAs('Majors')

// 每个图表创建时(初始时,以及网格变大时)
new ChartWidgetGrid(host, {
  onChartAdd: (widget, index) => widget.getChart().addIndicator('ema', { period: 21 }),
})

十字光标同步会在每个图表上显示指针所在的时间;时间同步会让其他图表随正在操作的图表一起滚动和缩放; 画线会复制到显示同一品种的图表上(开启时会把这些图表的画线合并到一起,不会丢失任何画线); 回放同步会把其他图表回放到与正在回放的图表相同的时间(K线比回放步长更长的图表会显示包含该时间的整根K线)。 网格缩小时被移除的图表会被收起并保留在已保存的布局中,网格再次变大时会原样恢复; 全新的图表在品种和周期处于同步状态时,会使用当前图表的品种和周期打开。

ChartGrid

由无界面图表(不带工具栏)组成的同步联动多图表布局;如需完整界面,请参见 ChartWidgetGrid。

import { ChartGrid } from '@tradecanvas/chart'

const grid = new ChartGrid(host, { layout: '2x2', theme: 'dark' })
// 每个图表一个适配器:一个适配器只维持一条数据流
await grid.connectAll(() => new BinanceAdapter(), ['BTCUSDT','ETHUSDT','SOLUSDT','BNBUSDT'], '5m')
grid.setLayout('1x2')

布局:'1x1'、'1x2'、'2x1'、'2x2'、'1x3'、'3x1'、'2x3'、'3x2'。