本教程仅限于实现最基础功能,更详细精进的功能请绕道(ノ≧∀≦)ノ・‥…━━━★ ピキューン!

上一篇教程介绍了如何通过UDF实现数据的传输,这篇教程讲述如何通过JSApi实现websocket数据实时同步。
JSApi文档

##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);
    },
}

datafeed为一个JS对象,拥有onReady方法,当我们把datafeed传递给TradingView后,插件会在启动时调用该JS对象的onReady方法,该方法拥有一个回调函数,而我们则需要通过一些方式获取到基本配置信息,然后将其传递给回调函数cb,这里我们是直接const了一个对象config,还可以通过ajax等方式从服务器获取数据,但要注意处理一下数据,让其变成文档规定的格式。

##必要的方法##

除了上面提到的onReady方法,还必须要有resolveSymbolgetBars以及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);
},

onReady一样,也是将配置作为参数传递给回调函数onSymbolResolvedCallback,不过插件在调用该方法时,会传入symbolName这个参数,该参数为字符串,代表着商品名。我在这里直接将其作为name了,也可以通过该参数,向服务器请求对应商品的配置数据。除此之外,他还有一个onResolveErrorCallback,这个回调函数用于处理获取数据失败时的情况。

getBarssubscribeBars都用于从服务器获取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;
}

后台很简单,总共就GetKlineHistoryGetKlineUpdata两个方法,前者对应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) {

}

该websocket还涉及到其他的业务,代码没有贴出来,需要注意的是KlineHistoryKlineUpdata这两个函数,这是两个空函数,在后面会用到。

下面是JS对象里的getBarssubscribeBars方法

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));
	}
},

这里首先要注意的是,TradingView这个插件最后会运行在一个iframe标签里面的,也就是说,如果你想让插件和你的其他业务逻辑公用一个websocket,那么你需要使用window.parent来获取父级窗口,再获取父级窗口的websocket对象。

插件在加载完毕后,首先会执行getBars获取历史的K线数据。这时候虽然插件已经加载完毕,但是websocket不一定已经连接上了,所以我在这里使用了setIntervalsocket.readyState为websocket的连接状态,如果已经连接完成,那么就通过websocket向后台发送请求,并清除定时器。

发送了数据请求怎么接收数据呢?这里我们通过改变window.parent.KlineHistory这个函数的方式,将onHistoryCallback这个回调函数传递给了父级KlineHistory函数,使其能够调用,这样在websocket收到数据之后就能执行onHistoryCallback

subscribeBars用于发起订阅信息,会在插件加载完毕后调用且仅调用一次,用同样改变父级函数的方法,将onRealtimeCallback传给父级的函数。