最初はPython
で作業していたのだが、言語特性上pandas
なりnumpy
なりのデータセットでデータを取り扱わないと恐ろしく遅い。最終的にはNode
でツール化することを検討していたので、分析もJavaScript
でやってみようと思った。
ijavascript
Jupyter Notebook
のJavaScript
用カーネル
インストール方法
Anaconda
で環境構築していたので、公式サイトに記載されている通り、Anaconda prompt
で以下を実行するだけでOKだった。
conda install nodejs
npm install -g ijavascript
ijsinstall
可視化方法
使ってみたところ、データ操作は問題ないのだが、データの可視化方法がわからず困った。
公式サイトを見ると、Notebook
のOutput
エリアに、任意のHTMLを埋め込む仕組みがあった。
$$.html("<div style='background-color:olive;width:50px;height:50px'></div>");
こんな感じで記載すると、記載したHTMLがOutput
エリアに描画される。
可視化ライブラリハ状況に応じていろいろなものを使いたくなることが想定されるため、CDNで提供されているものが使える方法を検討し、以下のような感じで実装した。
- Webサイトでグラフを描画するのと同じコードでHTMLを記載しグラフを描画する
- ライブラリの読み込みやDOM要素を
Notebook
から独立させるためiframe内にHTMLを記載する - シリアライズした描画データを上記HTMLに埋め込むことで、
Notebook
上のデータをHTMLへ連携する
Chart.js
共通関数
Chart.js
を使うならこんな感じ
function chartjs(config, width='100%', height=500) {
var html = `<iframe width='${width}' height='${height}' srcdoc="
<head><script src='https://cdn.jsdelivr.net/npm/chart.js@2.8.0'></script></head>
<body>
<canvas id='canvas'></canvas>
<script>
window.onload = function() {
var ctx = document.getElementById('canvas').getContext('2d');
window.myLine = new Chart(ctx, ${JSON.stringify(config).replace(/"/gi,'\'')});
}
</script>
</body>
">`;
$$.html(html)
}
グラフ描画
config
はChart.jsのサンプルそのまま。これを先程作ったchartjs
関数に渡している。これだけでOutput
エリアにグラフを描画することができる。
var config = {
type: 'bar',
data: {
labels: ['Red', 'Blue', 'Yellow', 'Green', 'Purple', 'Orange'],
datasets: [{
label: '# of Votes',
data: [12, 19, 3, 5, 2, 3],
backgroundColor: [
'rgba(255, 99, 132, 0.2)',
'rgba(54, 162, 235, 0.2)',
'rgba(255, 206, 86, 0.2)',
'rgba(75, 192, 192, 0.2)',
'rgba(153, 102, 255, 0.2)',
'rgba(255, 159, 64, 0.2)'
],
borderColor: [
'rgba(255, 99, 132, 1)',
'rgba(54, 162, 235, 1)',
'rgba(255, 206, 86, 1)',
'rgba(75, 192, 192, 1)',
'rgba(153, 102, 255, 1)',
'rgba(255, 159, 64, 1)'
],
borderWidth: 1
}]
},
options: {
scales: {
yAxes: [{
ticks: {
beginAtZero: true
}
}]
}
}
}
chartjs(config);
Google Charts
少し応用して、bitFlyerの取引データを、Google Chartsを使ってローソク足チャートとして表示する。
グラフのオプションはJavaScriptでローソク足チャートの作成を参考にさせていただきました。
メイン処理
bitflyer APIでの取引情報取得は1回で最大500件までしか取得できない。500件程度では延べ時間にして1分程度にしかならないので、任意のデータ量まで溜めてから可視化するよう、規定数まで取引データが溜まっていなければfetchApi
を再帰呼び出しするようにしている。(下記サンプルでは3000件)
規定数まで溜まっていれば、データ加工→パラメータ生成→グラフ描画を行う。
var fetch = require("node-fetch");
function fetchApi(obj=[], query='') {
fetch(`https://api.bitflyer.com/v1/executions?product_code=FX_BTC_JPY&count=500${query}`).then(function (response) {
return response.json();
}).then(function(tmp) {
json = obj.concat(tmp);
console.log(json.length);
if (json.length < 3000) {
setTimeout(function(){ fetchApi(json, `&before=${tmp[tmp.length-1]['id']}`) }, 500);
} else {
const ranges = createRanges(json, 60*1000*0.1)
const params = createParams(ranges)
googleCharts(params);
}
});
}
fetchApi();
パッケージ追加方法
Anaconda prompt
で、-D
オプションを付与してnpm install
を実行すればよい。[参考]
npm i -D node-fetch
データ加工
取引データをレンジ(=一定の時間=span
)単位にまとめて、ローソク足チャート描画用の属性情報を生成する。
function createRanges(obj, span) {
const offset = Math.floor(Date.parse(obj[obj.length-1].exec_date)/span);
// 取引情報をレンジIDをキーに集約
let ranges = [];
obj.forEach(o => {
o.timestamp = Date.parse(o.exec_date);
var rangeId = Math.floor(o.timestamp/span) - offset;
if (ranges[rangeId]) {
ranges[rangeId]['executions'].push(o);
} else {
ranges[rangeId] = {'id':rangeId, 'date':dateFormat(o.exec_date,'hh:MM:ss'),'executions':[o]}
}
});
// 各レンジに属性情報を付与(レンジ単位計算)
ranges.forEach(r => {
r.executions.sort(function(a, b) {
if (a.timestamp > b.timestamp) {
return 1;
} else {
return -1;
}
})
var priceList = r.executions.map(x => x.price)
r['open'] = priceList[1];
r['close'] = priceList.slice(-1)[0];
r['high'] = Math.max(...priceList);
r['low'] = Math.min(...priceList);
r['volume'] = r.executions.map(x => x.size).reduce((a,b) => a + b);
})
// 各レンジに属性情報を付与(レンジ跨ぎ計算)
closeList = ranges.map(x => x.close);
ranges.forEach((r, idx, arr) => {
r['avg05'] = avg(closeList, idx, 5);
r['avg25'] = avg(closeList, idx, 25);
r['avg50'] = avg(closeList, idx, 50);
})
return ranges;
}
日付フォーマット
function dateFormat(dateStr, format) {
const date = new Date(Date.parse(dateStr));
format = format.replace(/yyyy/, date.getFullYear());
format = format.replace(/mm/, ('0'+date.getMonth() + 1).slice(-2));
format = format.replace(/dd/, ('0'+date.getDate()).slice(-2));
format = format.replace(/hh/, date.getHours());
format = format.replace(/MM/, ('0'+date.getMinutes()).slice(-2));
format = format.replace(/ss/, ('0'+date.getSeconds()).slice(-2));
return format;
}
移動平均計算
function avg(arr, idx, num) {
if (idx - num >= 0) {
const arrParts = arr.slice(idx-num+1, idx+1)
return arrParts.reduce((a,b) => a+b, 0) / num;
} else {
return;
}
}
パラメータ生成
Google Charts
用のパラメータ生成。data
とoption
はAPIリファレンスに準拠、chartType
とcolumns
はgoogleCharts
関数の汎用性を高めるために用意したもの。
function createParams(ranges) {
const commonOption = {
colors: [ '#003A76' ],
legend: { position: 'none' },
bar: { groupWidth: '100%' },
width: Math.max(ranges.length*10, 800),
hAxis: { direction: 1 },
vAxis: { viewWindowMode: 'maximized' }
}
const candleData = ranges.map(r => [r.date, r.low, r.open, r.close, r.high, r.avg05, r.avg25, r.avg50])
const candleOption = { ...commonOption, ...{
chartArea: { left: 80, top: 10, right: 80, bottom: 10 },
height: 400,
lineWidth: 2,
curveType: 'function',
seriesType: 'candlesticks',
series:
{ '1': { type: 'line', color: 'green' },
'2': { type: 'line', color: 'red' },
'3': { type: 'line', color: 'orange' } }
}};
const volumeData = ranges.map(r => [r.date, r.volume])
const volumeOption = { ...commonOption, ...{
chartArea: { left: 80, top: 10, right: 80, bottom: 80 },
height: 200,
}};
return [
{data:candleData, option:candleOption, chartType:'ComboChart', columns:['string'].concat(new Array(7).fill('number'))},
{data:volumeData, option:volumeOption, chartType:'ColumnChart', columns:['string', 'number']}
];
}
グラフ描画
Google Charts
を使ってグラフを生成する。任意の数のグラフを描画できるようにするため、描画エリアとなるdiv要素も動的に生成するようにした。
function googleCharts(params) {
const width = params[0].option.width + 30;
const height = params.map(p => p.option.height).reduce((a,b) => a+b) + 30;
params.map((p,i) => p.divId = `${p.chartType}${i}`);
const divs = params.map((p,i) => `<div id='${p.divId}'></div>`).join('');
const callers = params.map(p => `drawChart(${JSON.stringify(p).replace(/"/gi,'\'')});`).join('');
const html = `<iframe width="${width}" height="${height}" srcdoc="
<head><script type='text/javascript' src='https://www.gstatic.com/charts/loader.js'></script></head>
<body>
${divs}
<script>
var timeout;
google.charts.load('current', {'packages': ['corechart'] });
window.onload = function(){
timeout = setInterval(function () {
if (google.visualization != undefined) {
${callers}
clearInterval(timeout);
}
}, 300);
}
function drawChart(param) {
var chart = new google.visualization[param.chartType](document.getElementById(param.divId));
var chartData = new google.visualization.DataTable();
param.columns.forEach(c => chartData.addColumn(c));
chartData.addRows(param.data);
chart.draw(chartData, param.option);
}
</script>
</body>
">`;
$$.html(html);
}
グラフ描画(モジュール化)
exportsオブジェクトの関数として定義することで、ノートブックを越えて使えるモジュールにすることもできる。
GoogleChartDrawer.js
exports.html = function (params) {
var width = params[0].option.width + 30
var height = params.map(p => p.option.height).reduce((a,b) => a+b) + 30
params.map((p,i) => p.divId = `${p.chartType}${i}`)
var divs = params.map((p,i) => `<div id='${p.divId}'></div>`).join('')
var callers = params.map(p => `drawChart(${JSON.stringify(p).replace(/"/gi,'\'')});`).join('')
return `<iframe width="${width}" height="${height}" srcdoc="
<head><script type='text/javascript' src='https://www.gstatic.com/charts/loader.js'></script></head>
<body>
${divs}
<script>
var timeout;
google.charts.load('current', {'packages': ['corechart'] });
window.onload = function(){
timeout = setInterval(function () {
if (google.visualization != undefined) {
${callers}
clearInterval(timeout);
}
}, 300);
}
function drawChart(param) {
var chart = new google.visualization[param.chartType](document.getElementById(param.divId));
var chartData = new google.visualization.DataTable();
param.columns.forEach(c => chartData.addColumn(c));
chartData.addRows(param.data);
chart.draw(chartData, param.option);
}
</script>
</body>
">`;
}
exports.draw = function (params) {
if ($$) {
$$.html(exports.html(params))
}
}
呼び出し方
var google = require('./GoogleChartDrawer')
google.draw(params)