シストレどうですか

  Algorithmic Trading for Dummies

OANDA API 解説編 第16回 oandapyv20を使ってみる

今まではpythonAPIの学習という事で、あえてラッパーを使わずにOANDA APIの使い方についていろいろ学習をしてきましたが、基本も一通り理解したという事で今回はoandapyv20というラッパーを使ってOANDAのサーバーにアクセスしてみたいと思います。


oandapyv20を試してみた。

oandapyv20に関するマニュアルは以下のリンクから:

oandapyv20のドキュメンテーション(英語版)
The oandapyV20 REST-V20 API wrapper documentation — OANDA REST V20 API Wrapper 0.6.3 documentation

oandapyv20のソースコード等(英語版)
GitHub - hootnot/oanda-api-v20: OANDA REST-V20 API wrapper. Easy access to OANDA's REST v20 API with oandapyV20 package. Checkout the Jupyter notebooks!

導入

まずはoandapyv20の導入が必要になりますが、私自身はAnacondaを使っていますので、pipを使わない導入方法を探してみましたがそんなものはないようです。
pipを使ってインストールをするとAnacondaがおかしくなる場合があるらしいので念のため仮想環境を作成しそこに" pip install oandapyv20"で導入してみました。

<参考>condaとpipの違い
onoz000.hatenablog.com

サンプル

oandapyv20の使い方に言及しているブログ等の記事は他にもいろいろありますので、以下のようなサンプルプログラムを作成して動きを確認してみました。

#外部モジュールの定義
import json
from oandapyV20 import API
from oandapyV20.exceptions import V20Error
    
#口座情報 (自分で取得した情報と置き換えてください。)
API_TOKEN = '~'
API_ID = '~'
  
#通貨ペア
INSTRUMENT = "USD_JPY"
  
api = API(API_TOKEN)
  
try:

        #最新レートの取得💡
        import oandapyV20.endpoints.pricing as pricing
        params = {"instruments" : INSTRUMENT}
        request = pricing.PricingInfo(accountID = API_ID, params = params)
        response_body = api.request(request)
        print("最新レート")
        print(json.dumps(response_body, indent=2))
  
        #ろうそく足の取得💡
        import oandapyV20.endpoints.instruments as instruments
        params = {
                "price" : "B",
                "granularity" : "M5",
                "count" : 10,
                "smooth" : False,
                }
        request = instruments.InstrumentsCandles(instrument=INSTRUMENT, params = params)
        response_body = api.request(request)
        print("ろうそく足")
        print(json.dumps(response_body, indent=2))

  
        #口座情報(通貨ペア別)の取得💡
        import oandapyV20.endpoints.accounts as accounts
        params = {"instruments" : INSTRUMENT}
        request = accounts.AccountInstruments(API_ID, params=params)
        response_body = api.request(request)
        print("口座情報(通貨ペア別)")
        print(json.dumps(response_body, indent=2))

  
        #スプレッド💡
        import oandapyV20.endpoints.forexlabs as labs
        params = {"instrument" : INSTRUMENT, "period" : 3600}
        request = labs.Spreads(params = params)
        response_body = api.request(request)
        print("スプレッド")
        print(json.dumps(response_body, indent=2))
    
        #ろうそく足の取得拡張版💡
        from oandapyV20.contrib.factories import InstrumentsCandlesFactory as icf
        import pandas as pd
          
        params = {
                "granularity" : "H1",
                "count" : 5000,
                "from" : "2010-01-01T00:00:00Z",
                "to" : "2020-12-31T00:00:00Z"              
                }
  
        filename_json = "C:/tmp/candle.json"
        filename_csv = "C:/tmp/candle.csv"
          
        df = pd.DataFrame(index=[])
        for r in icf(instrument=INSTRUMENT, params=params):
                api.request(r)
                record = pd.json_normalize(r.response.get('candles'))
                df = df.append(record, ignore_index=True)    #データフレームへの変換
    
        #ファイル書き込み  
        df.to_json(filename_json, orient='records')
        df.to_csv(filename_csv, sep=",")
  
        #ファイル読み込み
        with open(filename_json, 'r') as IN:
                data=IN.read()
        data = json.loads(data)
        i = len(data)
        print("ろうそく足拡張")
        print("配列の要素数: %s" %i)
        print("頭: %s" %data[0]['time'])
        print("お尻: %s" %data[i-1]['time'])   
  
#エラー時の処理💡
except V20Error as e:
        print('V20Error!=>:\n %s' %e)
  
except Exception as e:
        print('Error!=>\n%s' %e)

解説

今回はわかりやすい(?)ようにそれぞれのエンドポイントを呼び出す前に必要なものをimportしています。 API関数に関しては、初期値はデモ口座用("practice")ですので、本番口座の場合は、”api = API(API_TOKEN, "live")”のように引数を追加します。

最新レートの取得

最新のレートの取得方法ですが、ラッパーを使用してもしなくても渡す引数は変わりませんが、ラッパーありのほうがシンプルに記述できますね。

        #最新レートの取得💡
        import oandapyV20.endpoints.pricing as pricing
        params = {"instruments" : INSTRUMENT}
        request = pricing.PricingInfo(accountID = API_ID, params = params)
        response_body = api.request(request)
        print("最新レート")
        print(json.dumps(response_body, indent=2))

引数の解説はこちらも参考にして下さい。 jantzen.hatenablog.com

ろうそく足の取得

こちらも特に説明の必要はなさそうです。

        #ろうそく足の取得💡
        import oandapyV20.endpoints.instruments as instruments
        params = {
                "price" : "B",
                "granularity" : "M5",
                "count" : 10,
                "smooth" : False,
                }
        request = instruments.InstrumentsCandles(instrument=INSTRUMENT, params = params)
        response_body = api.request(request)
        print("ろうそく足")
        print(json.dumps(response_body, indent=2))

引数の解説はこちらも参考にして下さい。 jantzen.hatenablog.com

口座情報(通貨ペア別)の取得

やはり特にありません。

        #口座情報(通貨ペア別)の取得💡
        import oandapyV20.endpoints.accounts as accounts
        params = {"instruments" : INSTRUMENT}
        request = accounts.AccountInstruments(API_ID, params=params)
        response_body = api.request(request)
        print("口座情報(通貨ペア別)")
        print(json.dumps(response_body, indent=2))

引数の解説はこちらも参考にして下さい。
jantzen.hatenablog.com

FOREX LABS ENDPOINT

oandapyV20.endpointsnoの中には"forexlabs"と呼ばれるエンドポイントもあるようです。ONADA APIのサイトを調べたらどうやら古いバージョンの方からもいくつかのデータが取り出せるようです。
(OANDAのマニュアルはこちら👉 Forex Labs)

という訳ですので、試しにスプレッド情報を取得してみました。

        #スプレッド💡
        import oandapyV20.endpoints.forexlabs as labs
        params = {"instrument" : INSTRUMENT, "period" : 3600}
        request = labs.Spreads(params = params)
        response_body = api.request(request)
        print("スプレッド")
        print(json.dumps(response_body, indent=2))

"period"引数には秒単位(例では3600秒=1時間)で取得したい時間幅を指定します。
これを実行すると、以下のようなデータを受け取ります。

スプレッド
{
  "avg": [
    [
      1613595600,
      1.27633
    ],
    [
      1613596500,
      1.27522
    ]
  ],
  "min": [
    [
      1613595600,
      1
    ],
    [
      1613596500,
      1
    ]
  ],
  "max": [
    [
      1613595600,
      1.4
    ],
    [
      1613596500,
      1.4
    ]
  ]
}

受け取ったデータの内容は、平均値・最小値・最大値につきそれぞれの時間ごとに表示されているようです。
時間はUNIX時間みたいですので変換すると1613595600=2021/02/18 06:00:00、1613596500=2021/02/18 06:15:00 (日本時間)になりますので、どうやら15分おきの結果が表示されるようですが、1時間を指定したのに2個しかでてこないのはどうしてでしょうか?せめて3個あってもいいような気はしますが・・・
長い期間でも試してみましたが15分間隔は変わらないようです。

スプレッド以外にもいろいろあるようなので、他にもカレンダー(”Calendar")を試してみました。"period"にマイナスの数値を与えると未来のイベントも出力するので、指標発表時にはトレードしないとか、指標が事前予想と大きくずれた時はトレードするとか等で使えるかもしれませんね。

oandapyV20.contrib

oandapyV20.contribの中にはろうそく足の本数を1回の上限(5,000本)を超えて取得できる関数が用意されています。
一発呼び出せば全部のデータが取得できるようにはなっていないようですが、ヒストリカルなデータを使って解析やバックテストを行うこともあると思いましたのでファイル(JSON形式とCSV型式)に書き出す方法を試してみました。

        #ろうそく足の取得拡張版💡
        from oandapyV20.contrib.factories import InstrumentsCandlesFactory as icf
        import pandas as pd
          
        params = {
                "granularity" : "H1",
                "count" : 5000,
                "from" : "2010-01-01T00:00:00Z",
                "to" : "2020-12-31T00:00:00Z"              
                }

        filename_json = "C:/tmp/candle.json"
        filename_csv = "C:/tmp/candle.csv"
          
        df = pd.DataFrame(index=[])
        for r in icf(instrument=INSTRUMENT, params=params):
                api.request(r)
                record = pd.json_normalize(r.response.get('candles'))
                df = df.append(record, ignore_index=True)    #データフレームへの変換
    
        #ファイル書き込み  
        df.to_json(filename_json, orient='records')
        df.to_csv(filename_csv, sep=",")
        
        #ファイル読み込み
        with open(filename_json, 'r') as IN:
                data=IN.read()
        data = json.loads(data)
        i = len(data)
        print("ろうそく足拡張")
        print("配列の要素数: %s" %i)
        print("頭: %s" %data[0]['time'])
        print("お尻: %s" %data[i-1]['time'])    

取得する件数が多いので、いったんファイルに書き出したのちそれを再度読み込んで取得した内容がきちんと書かれているかどうか検証してみました。
最終的には上のサンプルに書かれている方法にしましたが、初めはoandapyv20のドキュメンテーションの事例を参考に作ってみたところ、 一筋縄ではいかないことに気が付きました。 InstrumentsCandlesFactory — OANDA REST V20 API Wrapper 0.6.3 documentation

当初"count"引数が5,000以内であれば、oandapyv20のドキュメンテーションのサンプルの通り問題なく動くことを確認しました。

ところが、"count"引数に5,000より大きい数字を指定した場合エラーになってしまいます。これはもとのOANDAのAPI側で5,000が最大値なのでそのまま"count"引数の値が渡っているためエラーが発生していると考えられます。
という訳で"from", "to"引数を使って5,000本以上取得する期間を使って試してみました。
ファイルへの書き込みは問題なくできるのですが、今度はそれをファイルをオープン後JSON型式で展開しようとしたところでまたまたエラーが発生しました。
書き込んだファイルの中身を調べてみると、なんとJSON型式のデータが複数存在している形で書かれています。
オリジナルのOANDAのAPIでは5,000本までしか取得できないので、それ以上のデータを取得する場合は配列が追加(Append)される形でファイル書かれるようです。そのつなぎ目の部分が"]["になっており複数のJOSNファイルが混在するような形で書かれています。 (こんな感じ👉[ , , , , , ,][ , , , , , ,][ , , , , , ,].....)

その為書き出したファイルを単純に"json.load"で読み込んでも2番目の固まりの部分を読み込むときにエラーが発生します。さらに"count"引数にあたえる数値によっては、空の配列"[]"が追加されたりと不安定な書き込みを行うのでこの方法はあきらめました。

最後は直接ファイルに書き出す方法ではなくpandasのデータフレームに変換後ファイルに書き出すように変更したところ、読み込み処理でも問題なく結果が表示されるようになりました。

そもそも最大値(5,000件)を超えるろうそく足を取得する目的で作成されているので、"count"引数は5,000で指定しておき、"from","to"引数の期間でろうそく足を取得すると覚えておけば大丈夫でしょう。
ただ、ドキュメンテーションのサンプルをみてもわかるようにもともとループさせないとデータを取得しないといけないし、編集の問題もあるし、そんなに便利な機能ではなかったようです・・・。

GitHub - hootnot/oanda-api-v20 at contrib-factories のサンプルように"candles"キーの値を丸ごとファイルに書き出さずに、その中から指定して必要な部分だけ書き出せばもう少しシンプルみたいなのでこちらのほうを参考にすればよかったです。

エラーの処理

APIで発生したエラーの処理はラッパーで拾ってくれるので、try~exceptにしておけば、"from oandapyV20.exceptions import V20Error"がなくても"except Exception"にその内容が渡ってきます。
"from oandapyV20.exceptions import V20Error"をインポートしておけば、その他のエラーと分けて、"except V20Error"でエラー時の処理をわけて記述できます。

from oandapyV20.exceptions import V20Error
#💡エラー時の処理💡
except V20Error as e:
        print('V20Error!=>:\n %s' %e)
  
except Exception as e:
        print('Error!=>\n%s' %e)


まとめ

データの取得方法を中心にoandapyv20の使い方を試してみました。
oandapyv20w使えば、エラー時の余計な処理の記述を省くこともできますので、よりシンプルなコーディングができそうです。 また、詳しい人が作成してくれている(?)し、メンテナンスもされているようですので安心感もあります。

反面、公式なドキュメンテーションをいろいろ読んでみましたが、渡す引数の内容や戻り値に関する説明はやはり乏しい(OANDAのマニュアル参照的な・・・)ので、 結局はOANDAのマニュアルも参考にしなくてはならず、そのOANDAのマニュアルもわかりずらいので、そのすべてを理解できるような人であればあえてラッパーを使う必要もないかも。と思いました。

プログラミング指向でない場合は、無駄な時間を省くために利用できるものは利用したほうがいいですし、ラッパーなしでのプログラミングを理解できていれば、将来的にOANDA以外のFXや仮想通貨取引でラッパーを使わないボットを作成する場合でもいろいろ応用がきくと思います。 いずれにせよ自分の自動売買トレードのスタイルに合わせて使い分ければいいってことなんですけどね。(と玉虫色な結びにしてみました😓)