https://blog.sakuradon.com/index.php/archives/203/
JSApi介绍
这是啥? 一套JS方法(以实现指定的公共接口)。
我该怎么使用它?: 您应该创建一个JS对象,它将以某种方式接收数据,并响应图表库的请求。
UDF是按照官方文档的规则来进行数据传输,但如果使用JSApi的话,你能使用任何你能使用的方式传输数据。得到的数据主要通过一个JS对象来获取并通过回调函数传递给TradingView插件。
也就是说,你需要创建一个JS对象,该对象拥有文档中规定的方法。插件会在适当的时候调用这个JS对象的方法,并传入对应参数,一般最后一个参数为回调函数,我们一般需要通过自己的方式获取数据,处理数据,然后按规定的格式以参数的格式传入给回调函数。
简单例子
举个简单的例子
const config = {
supports_search : true,
supports_group_request : false,
supported_resolutions : ["1", "5", "15", "30", "60", "1D", "1W"],
supports_marks : false,
supports_time : true,
exchanges : {
value : "",
name : "All Exchanges",
desc : ""
}
}
var datafeed = {
onReady: cb => {
console.log("=====onReady running");
setTimeout(() => cb(config), 0);
},
}
Copy
datafeed为一个JS对象,拥有onReady方法,当我们把datafeed传递给TradingView后,插件会在启动时调用该JS对象的onReady方法,该方法拥有一个回调函数,而我们则需要通过一些方式获取到基本配置信息,然后将其传递给回调函数cb,这里我们是直接const了一个对象config,还可以通过ajax等方式从服务器获取数据,但要注意处理一下数据,让其变成文档规定的格式。
必要的方法
除了上面提到的onReady
方法,还必须要有resolveSymbol
,getBars
以及subscribeBars
这几个方法。onReady
用来配置插件的一些属性。
resolveSymbol
用来解析某个商品(股票/虚拟币),如下所示
resolveSymbol: (symbolName, onSymbolResolvedCallback, onResolveErrorCallback) => {
var symbol_stub = {
name: symbolName,
description: "",
has_intraday: true,
has_no_volume: false,
minmov: 1,
minmov2: 2,
pricescale: 100,
session: "24x7",
supported_resolutions: ["1", "5", "15", "30", "60", "1D", "1W"],
ticker: symbolName,
timezone: "Asia/Shanghai",
type: "stock"
}
setTimeout(() => onSymbolResolvedCallback(symbol_stub), 0);
},
Copy
同onReady
一样,也是将配置作为参数传递给回调函数onSymbolResolvedCallback
,不过插件在调用该方法时,会传入symbolName
这个参数,该参数为字符串,代表着商品名。我在这里直接将其作为name
了,也可以通过该参数,向服务器请求对应商品的配置数据。除此之外,他还有一个onResolveErrorCallback
,这个回调函数用于处理获取数据失败时的情况。
getBars
和subscribeBars
都用于从服务器获取K线数据,不过getBars
用于获取一段时间内的所有K线数据,而subscribeBars
用于实时获取最新的K线数据并替换当前最新数据,下面详细讲解。
使用websocket实现实时更新K线
websocket后台采用php编写,先贴出后台代码
//获取区间内K线数据
public function GetKlineHistory($market, $from, $to, $resolution)
{
$resolution_old = $resolution;
$allResolutions = array(
'1' => 1,
'5' => 5,
'15' => 15,
'30' => 30,
'60' => 60,
'1D' => 1440,
'1W' => 10080,
);
$resolution = key_exists($resolution, $allResolutions) ? $allResolutions[$resolution] : 1;
$begin_time = 1532275200;
$to = floor(($to - $begin_time) / $resolution / 60) * $resolution * 60 + $begin_time;
$from = floor(($from - $begin_time) / $resolution / 60 + 1) * $resolution * 60 + $begin_time;
$sql = "SELECT * FROM qq3479015851_trade_json WHERE market='$market' AND `type`=$resolution AND addtime>$from AND addtime<=$to ORDER BY addtime";
$tradeJson = Db::querySql($sql);
$json_data = array();
foreach ($tradeJson as $k => $v) {
$json_data[] = json_decode($v['data'], true);
}
$data = array();
foreach ($json_data as $k => $v) {
$data[$k] = array();
$data[$k]['t'] = $v[0];
$data[$k]['o'] = floatval($v[2]);
$data[$k]['c'] = floatval($v[5]);
$data[$k]['h'] = floatval($v[3]);
$data[$k]['l'] = floatval($v[4]);
$data[$k]['v'] = $v[1];
$data[$k]['s'] = 'ok';
}
if ($this->GetKlineUpdata($market)[$resolution_old])
$data[] = $this->GetKlineUpdata($market)[$resolution_old];
return $data;
}
//更新K线数据
/**
* @param $market
* @param $resolution string 分辨率名称
* @return array
*/
public function GetKlineUpdata($market, $resolution = null)
{
$result = array();
$begin_time = 1532275200;//开始记录的时间
$time = time();//当前时间
$resolutions = [
'1' => 1,
'5' => 5,
'15' => 15,
'30' => 30,
'60' => 60,
'1D' => 1440,
'1W' => 10080,
];
if ($resolution != null && isset($resolutions[$resolution])) {
$resolutions = [$resolution => $resolutions[$resolution]];
}
foreach ($resolutions as $resolutionName => $resolutionScale) {
$result[$resolutionName] = array();
$sql_time = $time - $resolutionScale * 60 * 5;
$last_json_time = Db::querySql("SELECT MAX(addtime) FROM qq3479015851_trade_json WHERE market='$market' AND `type`=$resolutionScale AND addtime>$sql_time")[0];
$last_json_time = intval($last_json_time[0]);
$t = $last_json_time + floor(($time - $last_json_time) / ($resolutionScale * 60)) * $resolutionScale * 60;
$t_end = $t + $resolutionScale * 60;
$open_time = 0;//开盘时间
$close_time = 0;//闭盘时间
$open = 0;//开盘价格
$close = 0;//闭盘价格
$highest = 0;//最高价
$lowest = 0;//最低价
$volume = 0;//总量
$status = 0;//数据状态
if (!array_key_exists($market, $this->Kline)) {
$this->Kline[$market] = [];
}
if (!array_key_exists($resolutionName, $this->Kline[$market])) {
$this->Kline[$market][$resolutionName] = [
'id' => null,
't' => null,
'data' => []
];
}
if (!$this->Kline[$market][$resolutionName]['t']) {
$this->Kline[$market][$resolutionName]['t'] = $t;
}
if ($this->Kline[$market][$resolutionName]['t'] != $t) {
$t_old = $this->Kline[$market][$resolutionName]['t'];
$this->Kline[$market][$resolutionName]['t'] = $t;
if (!$this->Kline[$market][$resolutionName]['data']) {
$trade_info_sql = Db::querySql("SELECT * FROM qq3479015851_trade_json WHERE market='$market' AND `type`=$resolutionScale AND addtime=$last_json_time");
if ($trade_info_sql) {
$trade_info = $trade_info_sql[0];
$trade_data = json_decode($trade_info['data']);
$data = array(
't' => $t_old,
'o' => floatval($trade_data[5]),
'c' => floatval($trade_data[5]),
'h' => floatval($trade_data[5]),
'l' => floatval($trade_data[5]),
'v' => 0,
's' => 1
);
} else {
$data = array(
't' => $t_old,
'o' => 0,
'c' => 0,
'h' => 0,
'l' => 0,
'v' => 0,
's' => 1
);
}
} else {
$data = array();
}
$this->Kline[$market][$resolutionName]['data'] = array();
} else {
// var_dump($this->Kline[$market][$k]);
$id = $this->Kline[$market][$resolutionName]['id'] ? $this->Kline[$market][$resolutionName]['id'] : 0;
$trade_info = Db::querySql("SELECT * FROM qq3479015851_trade_log WHERE market='$market' AND `id`>$id AND addtime>=$t AND addtime<$t_end");
if (!empty($trade_info)) {
foreach ($trade_info as $tf) {
if ($open_time == 0) {
$open_time = $tf['addtime'];
$open = $tf['price'];
} else
if ($open_time > $tf['addtime']) {
$open_time = $tf['addtime'];
$open = $tf['price'];
}
if ($close_time == 0) {
$close_time = $tf['addtime'];
$close = $tf['price'];
} else
if ($close_time < $tf['addtime']) {
$close_time = $tf['addtime'];
$close = $tf['price'];
}
if ($highest == 0)
$highest = $tf['price'];
else
if ($highest < $tf['price'])
$highest = $tf['price'];
if ($lowest == 0)
$lowest = $tf['price'];
else
if ($lowest > $tf['price'])
$lowest = $tf['price'];
$volume += $tf['num'];
$status = 1;
$this->Kline[$market][$resolutionName]['id'] = $tf['id'];
}
$data = array(
't' => $t,
'o' => floatval($open),
'c' => floatval($close),
'h' => floatval($highest),
'l' => floatval($lowest),
'v' => $volume,
's' => $status
);
if ($this->Kline[$market][$resolutionName]['data']) {
$data_old = $this->Kline[$market][$resolutionName]['data'];
$data['o'] = $data_old['o'] ? $data_old['o'] : $data['o'];
if ($data['h'] > $data_old['h'])
$data_old['h'] = $data['h'];
else
$data['h'] = $data_old['h'];
if ($data['l'] < $data_old['l'])
$data_old['l'] = $data['l'];
else
$data['l'] = $data_old['l'];
$data['v'] += $data_old['v'];
}
$this->Kline[$market][$resolutionName]['data'] = $data;
} else {
$data = $this->Kline[$market][$resolutionName]['data'];
}
}
if ($resolutionName == '1')
if (!$data) {
// var_dump($this->Kline[$market][$k]);
// echo "t: " . $t;
}
$result[$resolutionName] = $data;
}
return $result;
}
Copy
后台很简单,总共就GetKlineHistory
和GetKlineUpdata
两个方法,前者对应getBars
,用于获取区间内K线数据,后者则对应subscribeBars
,用于更新K线数据。
接下来是前台websocket部分
socket = new WebSocket(url);
socket.onopen = () => {
}
socket.onmessage = msg => {
console.log(msg);
data = JSON.parse(msg.data);
if (data.type == 'server')
window[data.method](data.data);
}
socket.onclose = () => {
console.log("断开链接");
}
socket.onerror = () => {
};
function KlineHistory(data) {
}
function KlineUpdata(data) {
}
Copy
该websocket还涉及到其他的业务,代码没有贴出来,需要注意的是KlineHistory
和KlineUpdata
这两个函数,这是两个空函数,在后面会用到。
下面是JS对象里的getBars
和subscribeBars
方法
getBars: (symbolInfo, resolution, from, to, onHistoryCallback, onErrorCallback, firstDataRequest) => {
timer = setInterval(() => {
if (window.parent.socket.readyState == 1){
window.parent.websocketSend({
type: 'client',
method: 'KlineHistory',
data: {
market: symbolInfo.name,
from: from,
to: to,
resolution: resolution
}
});
clearInterval(timer);
}else{
}
}, 100);
window.parent.KlineHistory = data =>{
let bars = [];
if (data){
data.forEach(e => {
var _bar = {
time: e.t * 1000,
close: e.c,
open: e.o,
high: e.h,
low: e.l,
volume: e.v
}
bars.push(_bar);
});
meta = {
noData: false
}
}else{
meta = {
noData: true
}
}
setTimeout(() => onHistoryCallback(bars, meta));
}
},
subscribeBars: (symbolInfo, resolution, onRealtimeCallback, subscribeUID, onResetCacheNeededCallback) => {
window.parent.KlineUpdata = data => {
if (!data.s)
return;
var bar = {
time: data.t * 1000,
close: data.c,
open: data.o,
high: data.h,
low: data.l,
volume: data.v
}
setTimeout(() => onRealtimeCallback(bar));
}
},
Copy
这里首先要注意的是,TradingView这个插件最后会运行在一个iframe
标签里面的,也就是说,如果你想让插件和你的其他业务逻辑公用一个websocket,那么你需要使用window.parent
来获取父级窗口,再获取父级窗口的websocket对象。
插件在加载完毕后,首先会执行getBars
获取历史的K线数据。这时候虽然插件已经加载完毕,但是websocket不一定已经连接上了,所以我在这里使用了setInterval
,socket.readyState
为websocket的连接状态,如果已经连接完成,那么就通过websocket向后台发送请求,并清除定时器。
发送了数据请求怎么接收数据呢?这里我们通过改变window.parent.KlineHistory
这个函数的方式,将onHistoryCallback
这个回调函数传递给了父级KlineHistory
函数,使其能够调用,这样在websocket收到数据之后就能执行onHistoryCallback
。
subscribeBars
用于发起订阅信息,会在插件加载完毕后调用且仅调用一次,用同样改变父级函数的方法,将onRealtimeCallback
传给父级的函数。
https://b.aitrade.ga/books/tradingview/book/Widget-Constructor.html
https://b.aitrade.ga/books/proficient-tradingview/book/03-Http-Example-JP.html