Goで仮想通貨の価格情報をまるっとダウンロード

みなさま仮想通貨で遊んでいますでしょうか？８月１日にビットコインの分裂がおこりその後どうなるかと心配していましたが、1ビットコイン30万円だったところが今では45万円まで上がりその力強さにため息が出るほどです。

# 取得するデータの形式

さて、仮想通貨で遊ぶにしてもまずはデータが必用だと思います。そこで世界各地にある18の取引所から約450種類の価格情報をまるっと取得します。情報は3種類あり以下のようになものになります。

+ 最新の価格(ticker)
+ 板情報(orderbook, board, depth)
+ 直近の取引情報(executions, transactions, orders, trades)

()内は主な英語表記で、取引所によって名称が変わります。

# コードと実行

コードは以下になります。urls は長いので省略しました。[こちら](https://github.com/akiraak/dl_vcurrency/blob/a034ca3cdeb6d287e3899c5e610a80ecdf842bb0/main.go)から全ての urls を確認できます。

```go
package main

import (
	"errors"
	"fmt"
	"io/ioutil"
	"net/http"
	"os"
	"time"
)

var urls = []string{
	/*	bitflyer
		API Doc: https://lightning.bitflyer.jp/docs?lang=ja
	*/
	"https://api.bitflyer.jp/v1/ticker?product_code=BTC_JPY",
	"https://api.bitflyer.jp/v1/board?product_code=BTC_JPY",
	"https://api.bitflyer.jp/v1/executions?product_code=BTC_JPY",
	...省略...
}

func getUrlContent(url string) ([]byte, error) {
	resp, err := http.Get(url)
	if err != nil {
		return []byte{}, err
	}
	if resp.StatusCode != http.StatusOK {
		errorString := fmt.Sprintf("Not StatusOK. Code:%d URL:%s", resp.StatusCode, url)
		return []byte{}, errors.New(errorString)
	}
	defer resp.Body.Close()
	body, err := ioutil.ReadAll(resp.Body)
	return body, err
}

func saveFile(data []byte, id int, date string) error {
	idStr := fmt.Sprintf("%04d", id)
	dir := fmt.Sprintf("contents/%s", idStr)
	os.MkdirAll(dir, os.FileMode(0700))
	path := fmt.Sprintf("%s/%s-%s.json", dir, idStr, date)
	err := ioutil.WriteFile(path, data, os.FileMode(0600))
	return err
}

func main() {
	now := time.Now().UTC()
	fmt.Println(now)
	date := now.Format("20060102-150405")

	errors := make(chan error)
	threads := make(chan struct{}, 64)

	for i, url := range urls {
		id := i + 1
		//fmt.Printf("%04d: %s\n", id, url)
		go func(id int, url string, date string) {
			threads <- struct{}{}
			defer func() { <-threads }()

			content, err := getUrlContent(url)
			if err == nil {
				errors <- saveFile(content, id, date)
			} else {
				errors <- err
			}
		}(id, url, date)
	}

	successes := 0
	failures := 0
	for range urls {
		error := <-errors
		if error == nil {
			successes++
		} else {
			failures++
			fmt.Println(error)
		}
	}
	fmt.Printf("Total:%d Successes:%d Failures:%d\n", len(urls), successes, failures)
}
```
実行すると以下のような表示になります。

```shell-session
$ time go run main.go 
2017-08-26 23:10:13.61065158 +0000 UTC
Not StatusOK. Code:503 URL:https://api.gemini.com/v1/book/ethusd
Not StatusOK. Code:503 URL:https://api.gemini.com/v1/trades/btcusd
Not StatusOK. Code:503 URL:https://api.gemini.com/v1/trades/ethusd
Not StatusOK. Code:503 URL:https://api.gemini.com/v1/book/ethbtc
Not StatusOK. Code:503 URL:https://api.gemini.com/v1/pubticker/ethbtc
Not StatusOK. Code:503 URL:https://api.gemini.com/v1/pubticker/btcusd
Not StatusOK. Code:503 URL:https://api.gemini.com/v1/book/btcusd
Not StatusOK. Code:503 URL:https://api.gemini.com/v1/trades/ethbtc
Not StatusOK. Code:503 URL:https://api.gemini.com/v1/pubticker/ethusd
Total:1432 Successes:1423 Failures:9

real	0m28.344s
user	0m47.757s
sys	0m1.744s
```

Gemini はメンテナンスを行っているようでエラーが返ってきいます。

取得されたデータは contents ディレクトリの下に保存されます。１回の実行で44MBほどのファイルが作成されるので、連続で実行する場合はディスク容量に気をつけてください。

データは json 形式になりますが、中身は取引所によって異なります。URL一覧に各取引所のAPIドキュメントのURLを書いておいたので詳細はそちらを参照してください。（例：[BitflyerのAPIドキュメント](https://github.com/akiraak/dl_vcurrency/blob/a034ca3cdeb6d287e3899c5e610a80ecdf842bb0/main.go#L14)）

# データ例

Bitflyer BTC_JPY 最新の価格
https://api.bitflyer.jp/v1/ticker?product_code=BTC_JPY

```json
{
    "volume_by_product": 5838.66856754,
    "best_bid": 475249.0,
    "best_bid_size": 0.00535929,
    "timestamp": "2017-08-27T01:12:25.453",
    "total_ask_depth": 2042.62278512,
    "best_ask": 475337.0,
    "tick_id": 1830163,
    "volume": 80681.66841929,
    "ltp": 475327.0,
    "best_ask_size": 0.01072098,
    "total_bid_depth": 4729.51753672,
    "product_code": "BTC_JPY"
}
```

Bitflyer BTC_JPY 板情報
https://api.bitflyer.jp/v1/board?product_code=BTC_JPY

```json
{
    "mid_price": 475214.0,
    "bids": [
        {
            "price": 474999.0,
            "size": 0.80008001
        },
        {
            "price": 474998.0,
            "size": 0.29055574
        },
        {
            "price": 474844.0,
            "size": 0.38432848
        },
        {
            "price": 474843.0,
            "size": 0.29055574
        },
        ...省略...
    ],
    "asks": [
        {
            "price": 475430.0,
            "size": 0.37093269
        },
        {
            "price": 475544.0,
            "size": 0.09567152
        },
        {
            "price": 475545.0,
            "size": 0.3
        },
        ...省略...
    ]
}
```

Bitflyer BTC_JPY 直近の取引情報
https://api.bitflyer.jp/v1/executions?product_code=BTC_JPY

```json
[
    {
        "price": 475601.0,
        "exec_date": "2017-08-27T01:17:50.777",
        "side": "BUY",
        "id": 42928920,
        "sell_child_order_acceptance_id": "JRF20170827-011721-034840",
        "buy_child_order_acceptance_id": "JRF20170827-011738-383150",
        "size": 0.083
    },
    {
        "price": 475585.0,
        "exec_date": "2017-08-27T01:17:50.777",
        "side": "BUY",
        "id": 42928919,
        "sell_child_order_acceptance_id": "JRF20170827-011727-518742",
        "buy_child_order_acceptance_id": "JRF20170827-011738-383150",
        "size": 0.02734978
    },
    {
        "price": 475585.0,
        "exec_date": "2017-08-27T01:17:48.853",
        "side": "BUY",
        "id": 42928910,
        "sell_child_order_acceptance_id": "JRF20170827-011727-518742",
        "buy_child_order_acceptance_id": "JRF20170827-011737-272731",
        "size": 0.00199958
    },
    ...省略...
]
```

# 並列処理の補足説明

```go
	threads := make(chan struct{}, 64)

	for i, url := range urls {
		id := i + 1
		//fmt.Printf("%04d: %s\n", id, url)
		go func(id int, url string, date string) {
			threads <- struct{}{}
			defer func() { <-threads }()

			content, err := getUrlContent(url)
			if err == nil {
				errors <- saveFile(content, id, date)
			} else {
				errors <- err
			}
		}(id, url, date)
	}
```

Go の並列処理ではチャンネルという値を入れるバッファのサイズを指定することで、同時に実行される処理の数を制限することができます。今回の処理ではURLが1400以上あるので、これらを全て同時に取得しようとするとOSの制限に引っかかりエラーが発生します。

そこでチャンネルのサイズを `threads := make(chan struct{}, 64)` として、同時に実行できる数を64に制限します。この `threads` は配列のようなものであり、64個のデータが入るとそれ以降のアクセスが止まります。

`threads <- struct{}{}` で配列にデータを入れます。これが64回行われた時点でこれ以降の処理が止まります。しかし処理の最後で `<-threads` としてバッファからデータを取り出すことでバッファに空きができて新たな処理が始まります。

# おわり

データが手に入りましたので、あんなことやこんなことや人に言えないことをして遊びましょう！！！