OANDAから為替などのヒストリカルデータを取得する

# 前置き

[oanda](https://www.oanda.com/lang/ja/) は練習用口座を登録をすれば無料でAPIを試すことができます。
その中にヒストリカルデータの取得やtickのストリーミング配信もあり面白いです。
今回は機械学習でよく使うのでヒストリカルデータの取得だけを目的にします。
2002年5月分から最新のデータまで根こそぎもらいます。
※注意、2002年あたりのデータは取引が少なく結構まばらでした。

#[OANDA REST API](http://developer.oanda.com/rest-live-v20/introduction/)
古いv1と新しいv20がありますが日本の口座ではv1しか対応していません。
またinstruments(銘柄？）も日本の口座だと少ないです。
というわけで海外の練習用口座からv20でヒストリカルデータをもらいます。
私が登録したのは5年以上前ですが[登録](https://www.oanda.com/forex-trading/platform/fxtrade-demo-platform)では特に書類などは必要ありませんでした。
巷のフリーサービスと同じように登録できるはずです。
APIkeyだけ発行すればOKです。

APIまんまだと以下の様な感じ

```text
curl -X GET "https://api-fxtrade.oanda.com/v1/instruments?accountId=12345&instruments=AUD_CAD%2CAUD_CHF"
```
wrapper位作ってくれればいいのにと思うのですが、なんかサードパーティーまかせです。

# 今まで使っていたもの
いままでは以前に作った[nodejsのラッパー](https://github.com/sakiyama/oanda)を使っていたのですが、機械学習ではpythonばっかりやっているのでpythonで作ります。
nodejsのものもヒストリカルデータの取得だけであれば以下の様に取得できるはずです。

```js
var oanda = require('./oanda/')({
	key: '99999999999999999999999999999999-99999999999999999999999999999999'
	type: 'practice' // 'real' or 'sandbox'
});

oanda.api.accounts(function(err,accounts){
	var account = accounts[0];

	account.candles({
		instrument : "USD_JPY",
		count : 5000,// APIの仕様で5000マックスです。
	},function(err,candles){
		console.log(err, candles);
	});
});
```
またnodejsのは日本口座用にv1で作ってあります。

#環境
python 3.5.2
jupyter notebook
oanda rest api v20
windows 10

もし入ってない場合は以下を。

```text
pip3 install requests pandas
```

#実装
作りが中途半場ですので、データだけほしい人は中は見なくていいと思います。
ヒストリカルデータの取得とそれに必要なアカウント情報、銘柄情報だけ取得できるよう実装しました。

```python
import requests, re,os
import pandas as pd
class TypeBase():
    def __init__(self,dictonary):
        for key in dictonary :
            target = self._converter(dictonary[key])
            setattr(self, key, target)
    def _converter(self,target):
        if type(target) is str:
            if re.match("^\d+?\.\d+?$", target):
                target = float(target)
            elif re.match("^\d+$", target):
                target = int(target)
        return target
class Instrument(TypeBase):
    def __init__(self,dictonary):
        super().__init__(dictonary)
class Host():
    def __init__(self,rest,stream):
        self.rest = rest 
        self.stream = stream 
        for name in ['rest','stream']:
            setattr(self, name, getattr(self, name) + '/v3/')
class Request():
    def __init__(self,key,type):
        if type is 'sandbox':
            self.host = Host('http://api-sandbox.oanda.com','http://stream-sandbox.oanda.com' )
        elif type is 'practice':
            self.host = Host('https://api-fxpractice.oanda.com','https://stream-fxpractice.oanda.com' )
        elif type is 'real':
            self.host = Host('https://api-fxtrade.oanda.com','https://stream-fxtrade.oanda.com' )
        else :
            raise Exception('unknown type')
        self.headers = {
            "Authorization" : 'Bearer ' + key,
            "X-Accept-Datetime-Format" : 'UNIX'
        }
    def get(self,url,params={}):
        response = requests.get(self.host.rest+url,headers=self.headers,params=params)
        json = response.json()
        err = None
        if 'errorMessage' in json:
            err = json['errorMessage']
            return err,None
        return None,json
        
class API():
    def __init__(self,key,type,id=None):
        self.request = Request(key,type)
        if id:
            self.account_id = id
            return;
        err,accounts = self.accounts()
        if err:
            raise Exception(err)
        using = None
        print('============== ACCOUNTS =================')
        for account in accounts:
            if using : 
                print(account['id'])
            else:
                using = account['id']
                print(account['id'] + ' << using this account.')
        self.account_id = using
    def _get(self,url,name,clas=None,params={}):
        err, data = self.request.get(url,params)
        if data and name in data:
            data = data[name]
            if clas :
                instanced = []
                for single in data:
                    instanced.append(clas(single))
                data = instanced
        return err , data
    def accounts(self):
        return self._get('accounts','accounts')
    def instruments(self,account_id=None):
        if account_id is None:
            account_id = self.account_id
        return self._get(
            'accounts/' + account_id + '/instruments',
            'instruments',
            Instrument)
    def candles(self,params):
        instrument = params['instrument']
        params.pop('instrument',None)
        if 'count' not in params:
            params['count'] = 5000
        err, candles = self._get(
            'instruments/'+instrument+'/candles',
            'candles',
            params=params)
        if candles is None:
            return err,candles
        if len(candles) == 0:
            return 'no more data',None
        temp = []
        for candle in candles:
            if 'mid' in candle:
                for key, value in {'o':'open','h':'high','l':'low','c':'close'}.items():
                    candle[value] = candle['mid'][key]
                candle.pop('mid',None)
                candle.pop('complete',None)
                temp.append(candle)
        candles = pd.DataFrame(temp)
        candles.time = pd.to_datetime(candles.time)
        candles.time = candles.time.dt.tz_localize('UTC')
        candles = candles.set_index('time')
        candles = candles.reindex_axis(['open','high','low','close','volume'], axis=1)
        for name in ['open','high','low','close']:
            candles[name] = candles[name].astype('float')
        return err, candles
class OANDA():
    def __init__(self, key= None,type='practice',id=None):
        if key is None:
            raise Exception('need api key')
        self.api = API(key,type,id)
```

#使い方

インスタンス作成

```py
oanda = OANDA(
　　# required 自分で取得したものを入れてください。
    key='99999999999999999999999999999999-99999999999999999999999999999999', 
    # default 'practice' あとは'real','sandbox'
    type='practice', 
    # default None 指定がない場合取得できた最初のアカウントを使用します。
    id='999-999-999999-999') 
```

アカウント確認

```py
err, accounts = oanda.api.accounts()
accounts　#[{'id': '999-999-99999-999', 'tags': []}]
```

銘柄確認

```py
err, instruments = oanda.api.instruments()
for instrument in instruments:
    print(instrument.name + '[' + instrument.type + ']')
```
```text
XAU_JPY[METAL]
TWIX_USD[CFD]
XAU_CAD[METAL]
CAD_CHF[CURRENCY]
NZD_CHF[CURRENCY]
EUR_GBP[CURRENCY]
EUR_JPY[CURRENCY]
SG30_SGD[CFD]
USD_CZK[CURRENCY]
GBP_NZD[CURRENCY]
NZD_HKD[CURRENCY]
SGD_JPY[CURRENCY]
USD_NOK[CURRENCY]
EUR_DKK[CURRENCY]
EUR_PLN[CURRENCY]
XAG_CHF[METAL]
FR40_EUR[CFD]
EUR_AUD[CURRENCY]
TRY_JPY[CURRENCY]
XAU_XAG[METAL]
AUD_HKD[CURRENCY]
US30_USD[CFD]
AUD_NZD[CURRENCY]
EUR_HKD[CURRENCY]
USD_HKD[CURRENCY]
USD_DKK[CURRENCY]
XCU_USD[CFD]
GBP_PLN[CURRENCY]
EUR_NZD[CURRENCY]
XPT_USD[METAL]
NZD_JPY[CURRENCY]
EUR_CAD[CURRENCY]
XAG_NZD[METAL]
AUD_USD[CURRENCY]
SOYBN_USD[CFD]
XAU_CHF[METAL]
AUD_CAD[CURRENCY]
USB02Y_USD[CFD]
US2000_USD[CFD]
GBP_SGD[CURRENCY]
USD_SEK[CURRENCY]
CAD_SGD[CURRENCY]
USD_CHF[CURRENCY]
XAG_EUR[METAL]
XAG_SGD[METAL]
EUR_HUF[CURRENCY]
USD_CAD[CURRENCY]
HKD_JPY[CURRENCY]
XAU_GBP[METAL]
WTICO_USD[CFD]
XAG_USD[METAL]
XAG_JPY[METAL]
XAU_NZD[METAL]
XAG_AUD[METAL]
NATGAS_USD[CFD]
NZD_USD[CURRENCY]
USD_JPY[CURRENCY]
EUR_TRY[CURRENCY]
XAU_USD[METAL]
NAS100_USD[CFD]
CHF_ZAR[CURRENCY]
GBP_HKD[CURRENCY]
ZAR_JPY[CURRENCY]
CHF_JPY[CURRENCY]
EUR_SEK[CURRENCY]
USD_SGD[CURRENCY]
UK100_GBP[CFD]
USD_THB[CURRENCY]
GBP_CHF[CURRENCY]
AU200_AUD[CFD]
SPX500_USD[CFD]
EUR_SGD[CURRENCY]
USD_INR[CURRENCY]
UK10YB_GBP[CFD]
DE10YB_EUR[CFD]
AUD_CHF[CURRENCY]
NZD_CAD[CURRENCY]
CHF_HKD[CURRENCY]
SGD_HKD[CURRENCY]
HK33_HKD[CFD]
XAG_CAD[METAL]
XPD_USD[METAL]
IN50_USD[CFD]
XAG_GBP[METAL]
CAD_JPY[CURRENCY]
NZD_SGD[CURRENCY]
CAD_HKD[CURRENCY]
XAU_AUD[METAL]
EUR_NOK[CURRENCY]
USD_SAR[CURRENCY]
GBP_CAD[CURRENCY]
USD_HUF[CURRENCY]
GBP_AUD[CURRENCY]
USD_PLN[CURRENCY]
GBP_USD[CURRENCY]
USD_MXN[CURRENCY]
XAU_HKD[METAL]
XAG_HKD[METAL]
AUD_JPY[CURRENCY]
DE30_EUR[CFD]
USB05Y_USD[CFD]
EUR_CHF[CURRENCY]
AUD_SGD[CURRENCY]
BCO_USD[CFD]
XAU_EUR[METAL]
XAU_SGD[METAL]
SUGAR_USD[CFD]
EUR_CZK[CURRENCY]
GBP_ZAR[CURRENCY]
WHEAT_USD[CFD]
EUR_USD[CURRENCY]
EUR_ZAR[CURRENCY]
CORN_USD[CFD]
USD_ZAR[CURRENCY]
CN50_USD[CFD]
SGD_CHF[CURRENCY]
EU50_EUR[CFD]
USD_CNH[CURRENCY]
GBP_JPY[CURRENCY]
USD_TRY[CURRENCY]
USB30Y_USD[CFD]
JP225_USD[CFD]
NL25_EUR[CFD]
USB10Y_USD[CFD]
```
米国債とかもちゃんとありますね。

```py
for property in vars(instruments[0]):
    print(property, ":", getattr(instruments[0],property))
```

中身はこんな感じ

```text
tradeUnitsPrecision : 0
maximumPositionSize : 0
displayName : Gold/JPY
pipLocation : 1
marginRate : 0.05
minimumTrailingStopDistance : 50
maximumOrderUnits : 50000
type : METAL
name : XAU_JPY
maximumTrailingStopDistance : 100000
minimumTradeSize : 1
displayPrecision : 0
```


ヒストリカルデータの取得

```py
err,candles = oanda.api.candles({
            'instrument' : 'USD_JPY',
            'granularity' : 'M10'
        })
```
priceは'M'(midpoint candles)のみ対応,countのデフォルトは5000にしました。
他は[OANDA](http://developer.oanda.com/rest-live-v20/instrument-ep/)の仕様にのっとってます。

## 補足[足の種類]

|value|description|
|:-----------------|:------------------|
S5	|5 second candlesticks, minute alignment									|
S10	|10 second candlesticks, minute alignment                 |
S15	|15 second candlesticks, minute alignment                 |
S30	|30 second candlesticks, minute alignment                 |
M1	|1 minute candlesticks, minute alignment                  |
M2	|2 minute candlesticks, hour alignment                    |
M4	|4 minute candlesticks, hour alignment                    |
M5	|5 minute candlesticks, hour alignment                    |
M10	|10 minute candlesticks, hour alignment                   |
M15	|15 minute candlesticks, hour alignment                   |
M30	|30 minute candlesticks, hour alignment                   |
H1	|1 hour candlesticks, hour alignment                      |
H2	|2 hour candlesticks, day alignment                       |
H3	|3 hour candlesticks, day alignment                       |
H4	|4 hour candlesticks, day alignment                       |
H6	|6 hour candlesticks, day alignment                       |
H8	|8 hour candlesticks, day alignment                       |
H12	|12 hour candlesticks, day alignment                      |
D		|1 day candlesticks, day alignment                        |
W		|1 week candlesticks, aligned to start of week            |
M		|1 month candlesticks, aligned to first day of the month  |

#いっぱい取得する
とりあえずUSD_JPY,GBP_USDでD,H1,M30,M15,M10,M5をもらいます。
一回の取得で5000足分しかとれないのでくっつけていきます。
ファイルに書かないで一度全部メモリにもってしまっているので気を付けてください。
M5で50MB位あります。(古いほうからとればよかった、、。)
ファイルは"./data/"+instrument+"/"+ granularity + ".csv"に保存していっています。


```py
def getHistorical(instrument,granularity):
    before = None;
    count = 1
    while(True):
        params = {
            'instrument' : instrument,
            'granularity' : granularity
        };
        if before is not None:
            params['to'] = before.iloc[0].name.timestamp()
        err,candles = oanda.api.candles(params)
        if err is not None:
            print(err)
            break;
        count += 1
        print("getting " + str(count), end="\r")
        if before is None:
            before = candles
        else:
            before = candles.append(before,verify_integrity=True)
    directory = "./data/"+instrument+"/"
    if not os.path.exists(directory):
        os.makedirs(directory)
    before.to_csv(directory + granularity + ".csv", date_format='%Y-%m-%d %H:%M:%S')
```

```py
import time
instruments = ['USD_JPY','GBP_USD']
granularities = ['D','H1','M30','M15','M10','M5']
for instrument in instruments:
    for granularity in granularities:
        print('start',instrument,granularity)
        start = time.time()
        getHistorical(instrument,granularity)
        print('end',instrument,granularity,time.time() - start)
```

```text
start USD_JPY D
no more data
end USD_JPY D 4.406407117843628
start USD_JPY H1
no more data
end USD_JPY H1 50.374043464660645
start USD_JPY M30
no more data
end USD_JPY M30 103.10180807113647
start USD_JPY M15
・
・
・
・
```

あとはDBにいれるなりなんなりいじってください。
これで強化学習シタイ！