【PHP】暗号通貨のローソク足チャートをsvgで出力する

暗号通貨のローソク足チャートをブラウザ上に表示しようという話の3回目です。言語は PHP を用います。前回までの記事は以下。

【PHP】暗号通貨のローソク足を取得する方法をはじめから

【PHP】curlで為替情報を取得してアメリカドルを日本円に変換する

ローソク足チャート

今回作りたいものは上のようなシンプルなチャートです。

ローソク足の情報や,米ドル円の為替情報の取得のやり方については以前の記事を参照してください。

次に,コードの説明に進みます。

最新のローソク足情報の出力

date_default_timezone_set('Asia/Tokyo');
$unixtime = $candlestick["result"][3600][999][0];
echo date('<p>Y/m/d H時 ', $unixtime);
echo "始値", number_format($candlestick["result"][3600][999][1] * $usdjpy);
echo " 高値", number_format($candlestick["result"][3600][999][2] * $usdjpy);
echo " 安値", number_format($candlestick["result"][3600][999][3] * $usdjpy);
echo " 終値", number_format($candlestick["result"][3600][999][4] * $usdjpy);
echo "</p>";

チャートの上に最新のローソク足の情報を出力します。

$candlestick["result"][3600] には1時間足のデータが 1000 個含まれており,最新のデータは配列の末尾である $candlestick["result"][3600][999] に格納されています。

$unixtime = $candlestick["result"][3600][999][0];
echo date('<p>Y/m/d H時 ', $unixtime);

配列から取り出したUNIX 時間を $unixtime に格納し,画面に出力します。時間は1時間足の終了時刻なので,例えば現在9時30分なら,10時と表示されます。

echo "始値", number_format($candlestick["result"][3600][999][1] * $usdjpy);
echo " 高値", number_format($candlestick["result"][3600][999][2] * $usdjpy);
echo " 安値", number_format($candlestick["result"][3600][999][3] * $usdjpy);
echo " 終値", number_format($candlestick["result"][3600][999][4] * $usdjpy);

ローソク足の価格を表示します。number_format() は数値の出力形式を指定するもので,特に指定がなければ,3 桁区切りの整数として出力します。また,価格のデータは米ドルなので,為替レート $usdjpy をかけて日本円に変換しています。

描画領域を用意する

チャートの出力に進みます。

$width = 800;
$height = 400;
$margin = 10;
$padding = 10;

描画領域を指定します。$width は幅,$height は 高さ,$margin は描画領域と枠線の間隔,$padding は枠線とローソク足の間隔を意味します。間隔は上下左右すべて同じです。

これによって,実際にローソク足が描画される領域は少し小さくなり,幅 760,高さ 360 の領域の中に描画されることになります。これらの値を変更することで,画面に出力するグラフの大きさを変えることができます。

echo "<svg xmlns='http://www.w3.org/2000/svg'";
echo "viewBox='0 0 ", $width, " ", $height,"' width='", $width, "' height='", $height, "'>";
echo "<rect x='", $margin, "' y='", $margin,
    "' width='", $width - ($margin * 2),
    "' height='", $height - ($margin * 2),
    "' stroke='black' stroke-width='1' fill='none'/>";

SVG 領域を作ります。

echo "viewBox='0 0 ", $width, " ", $height,"' width='", $width, "' height='", $height, "'>";

この部分がコードが少し読み取りにくいかもしれません。通常,viewBox などの値は文字列として指定するのですが,それらを上で作った変数に置き換えるようにしています。したがって上の文は

echo "viewBox='0 0 400 800', width='400', height='800'>

と書いているのと同じことです。

縦軸方向の範囲を求める

$start = 905; //配列の何番目から描画するか
$prices = array();
for($i=0;$i<95;$i++) {
    $high = $candlestick["result"][3600][$start + $i][2] * $usdjpy; //高値
    $low = $candlestick["result"][3600][$start + $i][3] * $usdjpy; //安値
    $open = $candlestick["result"][3600][$start + $i][1] * $usdjpy; //始値
    $close = $candlestick["result"][3600][$start + $i][4] * $usdjpy; //終値
    array_push($prices, $high, $low, $open, $close);
}
$max = max($prices);
$min = min($prices);
$scale = ($height - (($margin + $padding) * 2))/($max-$min);

実際にチャートを描画するためには,縦軸方向の範囲を決めなければなりません。たとえば,グラフの下端を 300 万円,上端を 400 万円にするなど,範囲を絞り込んで表示しないと見づらいグラフになってしまいます。

そこで,出力したいデータからグラフの上端と下端を求めてみます。

今回は 1000 個のデータのうち,最新のものを 95 個表示します。配列としては [905][999] の範囲です。これらに格納されている価格の数値から,最大値と最小値を求めてグラフの上端と下端にします。

for($i=0;$i<95;$i++) {
    $high = $candlestick["result"][3600][$start + $i][2] * $usdjpy; //高値
    $low = $candlestick["result"][3600][$start + $i][3] * $usdjpy; //安値
    $open = $candlestick["result"][3600][$start + $i][1] * $usdjpy; //始値
    $close = $candlestick["result"][3600][$start + $i][4] * $usdjpy; //終値
    array_push($prices, $high, $low, $open, $close);
}

for 文で繰り返し処理を行い,配列の [905][999]の範囲で値を取り出していきます。

    array_push($prices, $high, $low, $open, $close);

array_push() は配列に要素を加えます。価格のデータは高値,安値,始値,終値の 4 つに分かれているので,これらを区別せずに 1 つの配列の中に並べていきます。その結果,配列 $prices は高値,安値,始値,終値,高値,安値,始値,終値,・・・と価格が連続して並ぶことになります。

こうして,出力する予定であるすべてのデータから最大と最小を求め,それを縦軸方向の範囲とします。

$max = max($prices);
$min = min($prices);

max()min() を用いて配列の最大値と最小値を求めます。

$scale = ($height - (($margin + $padding) * 2))/($max-$min);

縮尺を求めます。例えば,下端が 300 万円,上端が 400 万円だったとすると,上端と下端の差は 100 万です。画面上ではこれを縦幅 360 として描画するので,100 万が 360 になるように縮尺を変える必要があります。上の式は,上端 400 万,下端 300 万の場合で言えば

( 400 – (( 10 + 10 ) * 2 )) / ( 4000000 – 3000000 )

= 360 / 1000000

ということになります。これが縮尺です。たとえば地図の縮尺で「2万5千分の1の地図」などがありますが,この場合は「100万分の360」という縮尺を用いているのです。

あとで,価格に $scale を掛け算すると 0 から 360 の間で縦方向の位置を求めることができます。

補足:SVG には縮尺を調節する transform という機能がありますが,ここではなるべく原則に立ち返るという立場からこの機能を使わずに説明しています。

チャートの描画

for($i=0;$i<95;$i++) {
    $high = $candlestick["result"][3600][$start + $i][2] * $usdjpy; //高値
    $low = $candlestick["result"][3600][$start + $i][3] * $usdjpy; //安値
    $open = $candlestick["result"][3600][$start + $i][1] * $usdjpy; //始値
    $close = $candlestick["result"][3600][$start + $i][4] * $usdjpy; //終値
    $x = $i * 8 + $margin + $padding;   //x座標
    $y1 = ($max - $high) * $scale + $margin + $padding; //高値のy座標
    $y2 = ($max - $low) * $scale + $margin + $padding; //安値のy座標
    echo "<line x1='", $x, "' y1='", $y1, "' x2='", $x,
        "' y2='", $y2, "' stroke='black' stroke-width='1'/>";
    $y1 = ($max - $open) * $scale + $margin + $padding; //始値のy座標
    $y2 = ($max - $close) * $scale + $margin + $padding; //終値のy座標
    echo "<line x1='", $x, "' y1='", $y1, "' x2='", $x,
        "' y2='", $y2, "' stroke='black' stroke-width='5'/>";
}

チャートを描画していきます。ここでは,描画領域に収まる 95 本のローソク足を描画していきます。よって,for 文を用いてローソク足を描画する作業を 95 回繰り返します。

    $high = $candlestick["result"][3600][$start + $i][2] * $usdjpy; //高値
    $low = $candlestick["result"][3600][$start + $i][3] * $usdjpy; //安値
    $open = $candlestick["result"][3600][$start + $i][1] * $usdjpy; //始値
    $close = $candlestick["result"][3600][$start + $i][4] * $usdjpy; //終値

まず,1000 個のデータの中から最新のものについて抽出します。

    $x = $i * 8 + $margin + $padding;   //x座標

ローソク足の x 座標を $x に格納します。ローソク足は 8px ごとに出力します。

    $y1 = ($max - $high) * $scale + $margin + $padding; //高値のy座標
    $y2 = ($max - $low) * $scale + $margin + $padding; //安値のy座標

高値と安値の座標をそれぞれ $y1$y2 に格納します。SVG の座標は上から下に向かって増加します。つまり,一般的な数学のグラフとは逆向きです。一番上の y 座標が 0 で,一番下の y 座標が 360 です。

そこで,$max - $high とすることで,上端からの位置を求めることができます。

    echo "<line x1='", $x, "' y1='", $y1, "' x2='", $x,
        "' y2='", $y2, "' stroke='black' stroke-width='1'/>";

高値と安値を結ぶ直線を描画します。

    $y1 = ($max - $open) * $scale + $margin + $padding; //始値のy座標
    $y2 = ($max - $close) * $scale + $margin + $padding; //終値のy座標

今度は,始値と終値の座標を求めます。

    echo "<line x1='", $x, "' y1='", $y1, "' x2='", $x,
        "' y2='", $y2, "' stroke='black' stroke-width='5'/>";

同様に,始値と終値を結ぶ直線を描画します。stroke-width='5' とすることによって始値と終値を太い直線で結びます。

これで,チャートが描画されました。

まとめ

ここでは,curl で取得したローソク足のデータをもとに,SVG を利用してチャートを出力する方法を学びました。縮尺を調節することで出力すべきデータの範囲に基づいたグラフを描画できるようになりました。出力されたチャートはシンプルなものですが,ここからさらに情報を加えることでネットで一般的に見かけるようなカラフルなチャートを作成することができるようになります。

全体のコード

最後に全体のコードを示します。

<!DOCTYPE html>
<html lang="ja">
<head><meta charset="utf-8"></head>
<body>
<?php
//Bitcoinの1時間足を取得する
$url = "https://api.cryptowat.ch/markets/binance/btcusdt/ohlc?periods=3600";
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url); //URLを指定
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); //データを文字列で受け取る
$res = curl_exec($ch);
curl_close($ch);
$candlestick = json_decode($res, true);
//為替を取得する
$url = "https://www.gaitameonline.com/rateaj/getrate";
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$res = curl_exec($ch);
curl_close($ch);
$exchange = json_decode($res, true);
$usdjpy = $exchange["quotes"][20]["open"];
?>
<p>BTC/JPYローソク足(1時間足)-Binance</p>
<?php
date_default_timezone_set('Asia/Tokyo');
$unixtime = $candlestick["result"][3600][999][0];
echo date('<p>Y/m/d H時 ', $unixtime);
echo "始値", number_format($candlestick["result"][3600][999][1] * $usdjpy);
echo " 高値", number_format($candlestick["result"][3600][999][2] * $usdjpy);
echo " 安値", number_format($candlestick["result"][3600][999][3] * $usdjpy);
echo " 終値", number_format($candlestick["result"][3600][999][4] * $usdjpy);
echo "</p>";
//チャートの描画
$width = 800;
$height = 400;
$margin = 10;
$padding = 10;
echo "<svg xmlns='http://www.w3.org/2000/svg'";
echo "viewBox='0 0 ", $width, " ", $height,"' width='", $width, "' height='", $height, "'>";
echo "<rect x='", $margin, "' y='", $margin,
    "' width='", $width - ($margin * 2),
    "' height='", $height - ($margin * 2),
    "' stroke='black' stroke-width='1' fill='none'/>";
//y軸方向の範囲を求める
$start = 905; //配列の何番目から描画するか
$prices = array();
for($i=0;$i<95;$i++) {
    $high = $candlestick["result"][3600][$start + $i][2] * $usdjpy; //高値
    $low = $candlestick["result"][3600][$start + $i][3] * $usdjpy; //安値
    $open = $candlestick["result"][3600][$start + $i][1] * $usdjpy; //始値
    $close = $candlestick["result"][3600][$start + $i][4] * $usdjpy; //終値
    array_push($prices, $high, $low, $open, $close);
}
$max = max($prices);
$min = min($prices);
$scale = ($height - (($margin + $padding) * 2))/($max-$min);
//描画
for($i=0;$i<95;$i++) {
    $high = $candlestick["result"][3600][$start + $i][2] * $usdjpy; //高値
    $low = $candlestick["result"][3600][$start + $i][3] * $usdjpy; //安値
    $open = $candlestick["result"][3600][$start + $i][1] * $usdjpy; //始値
    $close = $candlestick["result"][3600][$start + $i][4] * $usdjpy; //終値
    $x = $i * 8 + $margin + $padding;   //x座標
    $y1 = ($max - $high) * $scale + $margin + $padding; //高値のy座標
    $y2 = ($max - $low) * $scale + $margin + $padding; //安値のy座標
    echo "<line x1='", $x, "' y1='", $y1, "' x2='", $x,
        "' y2='", $y2, "' stroke='black' stroke-width='1'/>";
    $y1 = ($max - $open) * $scale + $margin + $padding; //始値のy座標
    $y2 = ($max - $close) * $scale + $margin + $padding; //終値のy座標
    echo "<line x1='", $x, "' y1='", $y1, "' x2='", $x,
        "' y2='", $y2, "' stroke='black' stroke-width='5'/>";
}
echo "</svg>";
?>
</body>
</html>