在开发公司交易所项目的过程中,页面中需要集成专业的K线图功能。经过对多个类似网站的调研和参考,最终决定采用TradingView这一专业的股票交易所类图表库。结合之前的相关项目经验和TradingView的详细文档,我们成功实现了K线图的开发工作。现将整个开发过程进行整理,记录下心得体会,以便分享给大家作为参考。
一:Tradingview介绍
Tradingview 是一个价格图表和分析软件,提供免费和付费选项,由一群交易员和软件开发商在 2011 年 9 月推出。投资者可以通过 Tradingview 查看各种不同金融市场和资产类别的价格图表,包括股票、货币对、债券、期货以及加密货币。除此之外,投资者还可以通过该平台查看多个交易品种,比如股指期货、欧美货币对、黄金、原油、比特币等等。
TradingView 可以说是全球在网页 K 线图上最专业的网站了,凡是在网页上提供 K 线图的,大部分使用的都是 TradingView 的技术,比如说火币、币安……
简而言之,这是一个图表插件,刨除外观 UI 的设置,它的功能就是:获得数据——数据可视化——响应用户操作——获得数据——数据可视化——……
有兴趣可以看下在线demo
二:申请图表
1. 申请核心图表库
Tradingview 图表库是开源免费的,GitHub 上有官网 demo 可以下载。
该图表库支持多种语言及框架(如 Vue/React/Angular 等),其实下载了 demo 并不能直接运行,其中缺少关键的核心库(charting-library),这个需要到官网申请获得,申请步骤比较麻烦,需要下载它的一份协议,签名盖章之后扫描上传上去,然后填写一堆表单(邮箱公司地址等等),如果填写没问题的话,会在一两天之内回复你的邮箱,是 github 的链接(已授权过的,不然会报 404)。
获取 github 授权之后,就可以将核心库(charting-library)下载到本地了。
2. 参考文档
因为开发文档写的可能不怎么友好,在此罗列一下自己的一些参考文档及一些实现的 demo
文档
这是个很不错的文档,作者很用心,文档也很详尽,只是小白可能看着有点绕。
3. 运行demo
我图了一个方便,直接在github上下载了别人写好的demo
前面写到的下载好demo之后第一个坑就来了
node-sass!!!
这东西真的很坑,我不是专业的前端并不知道怎么解决兼容问题于是我果断
npm uninstall node-sass
npm install sass
小样,拿捏不了你,既然咱们不合适,那就拜拜
在我不断的祈祷中,嘿嘿,跑起来了,Perfect
等等!!!TypeScript,天塌了,我只是一个普普通通的java后端啊,为啥要这么对我,抽支烟冷静下,区区ts就能拦住我?我可是java后端,java才是世界上最好的语言!
三:改造Demo
重振java雄风,我辈义不容辞
我们先创建一个Vue2项目,这里就不赘述了
还记得前文中的那个文件吗?需要到官网申请的
+/charting_library
+ /static
- charting_library.min.js
- charting_library.min.d.ts
- datafeed-api.d.ts
+ /datafeeds
+ /udf
- index.html
- mobile_black.html
- mobile_white.html
- test.html
最重要的其实是charting_library这个文件夹
1:我们将这个文件夹复制到我们的项目中,我这里选择了复制到public下
2:在index.html下引入这个js
<script type="text/javascript" src='<%= BASE_URL %>custom_scripts/chart_main/charting_library.min.js'></script>
3:创建TradingView组件
话不多说直接贴代码,话不多说,都哥们直接用
<template>
<div>
<div id="chart_container"></div>
</div>
</template>
<script>
import axios from 'axios';
export default {
name: "TradingViewChart",
props: {
coin: {
type: String,
required: true
}
},
data() {
return {
chart: null,
dataonRealtimeCallback: null,
yuanshiArray:[]
};
},
created() {
},
mounted() {
this.getList();
this.initView();
},
methods: {
getList() {
this.yuanshiArray = [];
axios.get('获取历史数据的接口')
.then(response => {
let list = response.data
list.forEach((item, index) => {
let datata = {
time: item.time*1000,
close: item.close,
open: item.open,
high: item.high,
low: item.low,
volume: Number(item.volume)
};
this.yuanshiArray.push(datata);
});
});
},
createData() {
setInterval(() => {
axios.get('最新数据接口')
.then(response => {
let datata = {
time: response.data.time*1000,
close: response.data.close,
open: response.data.open,
high: response.data.high,
low: response.data.low,
volume: Number(response.data.volume)
};
this.dataonRealtimeCallback(datata);
})
}, 1000);
},
initView() {
let Tdata = this.createFeed();
this.chart = new TradingView.widget({
fullscreen: false,
autosize: true,
symbol: this.coin,
container_id: "chart_container",
datafeed: Tdata,
library_path: "/custom_scripts/chart_main/",
locale: "zh",
debug: false,
// loading_screen:{ backgroundColor: "#00ff00",foregroundColor: "#000000", }, //todo:do it
interval: '1',
style: '1',
// timeframe:'',//todo: na koncu
toolbar_bg: "#20334d",
// saved_data: this.savedData,
allow_symbol_change: true,
time_frames: [{
text: "1y",
resolution: "1W"
},
{
text: "6m",
resolution: "3D"
},
{
text: "3m",
resolution: "1D"
},
{
text: "1m",
resolution: "1D"
},
{
text: "1w",
resolution: "30"
},
{
text: "3d",
resolution: "30"
},
{
text: "1d",
resolution: "30"
},
{
text: "6h",
resolution: "15"
},
{
text: "1h",
resolution: "1"
}
],
drawings_access: {
type: 'black',
// tools: [{name: "Regression Trend"}]//todo: moje
tools: [{
name: "Trend Line",
grayed: true
}, {
name: "Trend Angle",
grayed: true
}] //todo: bb
},
disabled_features: [
"header_symbol_search",
"header_interval_dialog_button",
"show_interval_dialog_on_key_press",
"symbol_search_hot_key",
"study_dialog_search_control",
"display_market_status",
"header_compare",
"edit_buttons_in_legend",
"symbol_info",
"border_around_the_chart",
"main_series_scale_menu",
"star_some_intervals_by_default",
"datasource_copypaste",
"right_bar_stays_on_scroll",
"context_menus",
"go_to_date",
"compare_symbol",
"border_around_the_chart",
"timezone_menu",
"control_bar", //todo: przetestowac
"edit_buttons_in_legend", //todo: przetestowac
"remove_library_container_border",
],
enabled_features: [
"dont_show_boolean_study_arguments",
"use_localstorage_for_settings",
"remove_library_container_border",
"save_chart_properties_to_local_storage",
"side_toolbar_in_fullscreen_mode",
"hide_last_na_study_output",
"constraint_dialogs_movement", //todo: nie do końca jestem pewien
],
studies_overrides: {
"volume.volume.color.0": "#fe4761",
"volume.volume.color.1": "#3fcfb4",
"volume.volume.transparency": 75,
},
overrides: {
"symbolWatermarkProperties.color": "rgba(0,0,0, 0)",
"paneProperties.background": "#20334d",
"paneProperties.vertGridProperties.color": "#344568",
"paneProperties.horzGridProperties.color": "#344568",
"paneProperties.crossHairProperties.color": "#58637a",
"paneProperties.crossHairProperties.style": 2,
"mainSeriesProperties.style": 9,
"mainSeriesProperties.showCountdown": false,
"scalesProperties.showSeriesLastValue": true,
"mainSeriesProperties.visible": false,
"mainSeriesProperties.showPriceLine": false,
"mainSeriesProperties.priceLineWidth": 1,
"mainSeriesProperties.lockScale": false,
"mainSeriesProperties.minTick": "default",
"mainSeriesProperties.extendedHours": false,
"volumePaneSize": "tiny",
editorFontsList: ["Lato", "Arial", "Verdana", "Courier New", "Times New Roman"],
"paneProperties.topMargin": 5,
"paneProperties.bottomMargin": 5,
"paneProperties.leftAxisProperties.autoScale": true,
"paneProperties.leftAxisProperties.autoScaleDisabled": false,
"paneProperties.leftAxisProperties.percentage": false,
"paneProperties.leftAxisProperties.percentageDisabled": false,
"paneProperties.leftAxisProperties.log": false,
"paneProperties.leftAxisProperties.logDisabled": false,
"paneProperties.leftAxisProperties.alignLabels": true,
// "paneProperties.legendProperties.showStudyArguments": true,
"paneProperties.legendProperties.showStudyTitles": true,
"paneProperties.legendProperties.showStudyValues": true,
"paneProperties.legendProperties.showSeriesTitle": true,
"paneProperties.legendProperties.showSeriesOHLC": true,
"scalesProperties.showLeftScale": false,
"scalesProperties.showRightScale": true,
"scalesProperties.backgroundColor": "#20334d",
"scalesProperties.lineColor": "#46587b",
"scalesProperties.textColor": "#8f98ad",
"scalesProperties.scaleSeriesOnly": false,
"mainSeriesProperties.priceAxisProperties.autoScale": true,
"mainSeriesProperties.priceAxisProperties.autoScaleDisabled": false,
"mainSeriesProperties.priceAxisProperties.percentage": false,
"mainSeriesProperties.priceAxisProperties.percentageDisabled": false,
"mainSeriesProperties.priceAxisProperties.log": false,
"mainSeriesProperties.priceAxisProperties.logDisabled": false,
"mainSeriesProperties.candleStyle.upColor": "#3fcfb4",
"mainSeriesProperties.candleStyle.downColor": "#fe4761",
"mainSeriesProperties.candleStyle.drawWick": true,
"mainSeriesProperties.candleStyle.drawBorder": true,
"mainSeriesProperties.candleStyle.borderColor": "#3fcfb4",
"mainSeriesProperties.candleStyle.borderUpColor": "#3fcfb4",
"mainSeriesProperties.candleStyle.borderDownColor": "#fe4761",
"mainSeriesProperties.candleStyle.wickColor": "#737375",
"mainSeriesProperties.candleStyle.wickUpColor": "#3fcfb4",
"mainSeriesProperties.candleStyle.wickDownColor": "#fe4761",
"mainSeriesProperties.candleStyle.barColorsOnPrevClose": false,
"mainSeriesProperties.hollowCandleStyle.upColor": "#3fcfb4",
"mainSeriesProperties.hollowCandleStyle.downColor": "#fe4761",
"mainSeriesProperties.hollowCandleStyle.drawWick": true,
"mainSeriesProperties.hollowCandleStyle.drawBorder": true,
"mainSeriesProperties.hollowCandleStyle.borderColor": "#3fcfb4",
"mainSeriesProperties.hollowCandleStyle.borderUpColor": "#3fcfb4",
"mainSeriesProperties.hollowCandleStyle.borderDownColor": "#fe4761",
"mainSeriesProperties.hollowCandleStyle.wickColor": "#737375",
"mainSeriesProperties.hollowCandleStyle.wickUpColor": "#3fcfb4",
"mainSeriesProperties.hollowCandleStyle.wickDownColor": "#fe4761",
"mainSeriesProperties.haStyle.upColor": "#3fcfb4",
"mainSeriesProperties.haStyle.downColor": "#fe4761",
"mainSeriesProperties.haStyle.drawWick": true,
"mainSeriesProperties.haStyle.drawBorder": true,
"mainSeriesProperties.haStyle.borderColor": "#3fcfb4",
"mainSeriesProperties.haStyle.borderUpColor": "#3fcfb4",
"mainSeriesProperties.haStyle.borderDownColor": "#fe4761",
"mainSeriesProperties.haStyle.wickColor": "#737375",
"mainSeriesProperties.haStyle.wickUpColor": "#3fcfb4",
"mainSeriesProperties.haStyle.wickDownColor": "#fe4761",
"mainSeriesProperties.haStyle.barColorsOnPrevClose": false,
"mainSeriesProperties.barStyle.upColor": "#3fcfb4",
"mainSeriesProperties.barStyle.downColor": "#fe4761",
"mainSeriesProperties.barStyle.barColorsOnPrevClose": false,
"mainSeriesProperties.barStyle.dontDrawOpen": false,
"mainSeriesProperties.lineStyle.color": "#0cbef3",
"mainSeriesProperties.lineStyle.linestyle": 0,
"mainSeriesProperties.lineStyle.linewidth": 1,
"mainSeriesProperties.lineStyle.priceSource": "close",
"mainSeriesProperties.areaStyle.color1": "#0cbef3",
"mainSeriesProperties.areaStyle.color2": "#0098c4",
"mainSeriesProperties.areaStyle.linecolor": "#0cbef3",
"mainSeriesProperties.areaStyle.linestyle": 0,
"mainSeriesProperties.areaStyle.linewidth": 1,
"mainSeriesProperties.areaStyle.priceSource": "close",
"mainSeriesProperties.areaStyle.transparency": 80
},
custom_css_url: 'chart.css'
})
},
// 创建k线配置
// 创建k线配置
createFeed() {
let that = this
let Datafeed = {}
Datafeed.Container = function(updateFrequency) {
this._configuration = {
supports_search: false,
supports_group_request: false,
supported_resolutions: [ //支持的周期数组
'1',
'5',
'15',
'60',
'120',
'1D',
'1W'
],
supports_marks: true, //来标识您的 datafeed 是否支持在K线上显示标记。
supports_timescale_marks: true, //标识您的 datafeed 是否支持时间刻度标记。
exchanges: ['myExchange1'] //交易所对象数组
}
}
// onReady在图表Widget初始化之后立即调用,此方法可以设置图表库支持的图表配置
Datafeed.Container.prototype.onReady = function(callback) {
let that = this
if (this._configuration) {
setTimeout(function() {
callback(that._configuration)
}, 0)
} else {
this.on('configuration_ready', function() {
callback(that._configuration)
})
}
}
// 通过商品名称解析商品信息(SymbolInfo),可以在此配置单个商品
Datafeed.Container.prototype.resolveSymbol = function(
symbolName,
onSymbolResolvedCallback,
onResolveErrorCallback
) {
Promise.resolve().then(() => {
onSymbolResolvedCallback({
name: that.coin,
ticker: symbolName, //商品体系中此商品的唯一标识符
description: '', //商品说明
session: '24x7', //商品交易时间
timezone: 'Asia/Shanghai', // 这个商品的交易所时区
pricescale: 1000, // 价格精度
minmov: 100, //最小波动
minmov2: 0,
// 'exchange-traded': 'myExchange2',
// 'exchange-listed': productName,
has_intraday: true, // 显示商品是否具有日内(分钟)历史数据
intraday_multipliers: ['1', '5', '15', '15', '60', '120'], //日内周期(分钟单位)的数组
has_weekly_and_monthly: true, // 显示商品是否具有以W和M为单位的历史数据
has_daily: true, //显示商品是否具有以日为单位的历史数据
// has_empty_bars: true,
force_session_rebuild: true, //是否会随着当前交易而过滤K柱
has_no_volume: false, //表示商品是否拥有成交量数据。
regular_session: '24x7'
})
})
}
// 从我们的API源获取图表数据并将其交给TradingView。
Datafeed.Container.prototype.getBars = async function(
symbolInfo, // 商品信息对象
resolution, //(string (周期)
rangeStartDate, // unix 时间戳, 最左边请求的K线时间
rangeEndDate, // unix 时间戳, 最右边请求的K线时间
onDataCallback, // 历史数据的回调函数。每次请求只应被调用一次。
onErrorCallback, // 错误的回调函数。
firstDataRequest //布尔值,以标识是否第一次调用此商品/周期的历史记录。
) {
that.localresolution = resolution
if (firstDataRequest) {
console.log(symbolInfo.name);
console.log(resolution)
let bars = that.yuanshiArray;
if (bars.length) {
onDataCallback(bars)
} else {
onDataCallback([], {
noData: true
})
// onErrorCallback([], { noData: true })
}
} else {
onDataCallback([], {
noData: true
})
// onErrorCallback([], { noData: true })
}
}
// 订阅K线数据。图表库将调用onRealtimeCallback方法以更新实时数据。
Datafeed.Container.prototype.subscribeBars = function(
symbolInfo, // ObjectsymbolInfo对象
resolution, // StringK线周期
onRealtimeCallback, // Function将我们更新的K线传递给此回调以更新图表
listenerGUID, // String此交易对的唯一ID和表示订阅的分辨率,生成规则:ticker+'_'+周期
onResetCacheNeededCallback // Function调用次回调让图表再次请求历史K线数据
) {
that.callbacks = []
that.callbacks.push(onRealtimeCallback)
that.dataonRealtimeCallback = onRealtimeCallback
that.createData()
// 更改线型
//that.chart.activeChart().setChartType(1);
}
// 取消订阅K线数据
Datafeed.Container.prototype.unsubscribeBars = function(listenerGUID) {}
return new Datafeed.Container()
}
}
};
</script>
<style scoped lang="scss">
#chart_container {
height: calc(100vh - 100px);
}
</style>