シストレどうですか

  Algorithmic Trading for Dummies

OANDA API 解説編 第9回 エラー発生時の処理

いままではレートや口座情報を取得する方法だけでしたので、取得に失敗した場合の処理は特になくてもなんとかなりましたがオーダーの作成や変更をする際にはやはりエラー発生時の処理がないことには何がおきるかわかりません。今回はそれについて考えてみたいと思います。


ステータスコードについて

とりあえずどんなステータスコードが返ってくるかは各エンドポイントの説明の部分に解説があり、よくあるエラーコードの説明についても記載されています。 正常の場合は、200または201(PostとPutの一部のみ)のようです。

f:id:jantzen:20200915054637p:plain
正常時ステータスコード

エラーの場合は、ここに解説がありますが、エラーの種類によって受け取るメッセージのフォーマットが違います。

銘柄が存在しない場合の例:

{"errorMessage":"Invalid Instrument ABC_JPY"}

取引番号が存在しない場合の例:

{'lastTransactionID': '828', 'errorMessage': 'The transaction ID specified does not exist', 'errorCode': 'NO_SUCH_TRANSACTION'}


エラーが発生した場合の処理方法いくつか


サンプルとしてテスト用にわざと間違えた銘柄情報をセットします。(ABC_JPY)

# 必要なモジュールの読み込み
import requests
  

# 口座情報の設定
API_Token = '********************************-********************************'
API_AccountID = '999-999-99999999-999'
  
# URLの設定 (デモ口座用非ストリーミングURL)
API_URL =  "https://api-fxpractice.oanda.com"
  
#銘柄情報
INSTRUMENT = "ABC_JPY"  👈誤った銘柄をセット!
  
# <取得用URLの変数の設定>
url = API_URL + "/v3/accounts/%s/pricing?instruments=%s" %(str(API_AccountID), INSTRUMENT)
  
# ヘッダー情報の変数の設定
headers = {
               "Authorization" : "Bearer " + API_Token
        }
  
try:

     # サーバーへの要求
        response = requests.get(url, headers=headers)
        print(response.json())
        
except Exception as e:
  
        print(e)
        #エラー処理の記述…
            
pass

これだけでは当然サーバーからエラーが戻ってきたステータスコードでエラー処理を行うかどうかは判断できませんが、とりあえず以下のデータを受け取ります。

{'errorMessage': 'Invalid Instrument ABC_JPY'}

というわけでここから考えられるエラー処理時の判断方法を考えてみます。 次のサンプルは、上のサンプルのtry~exceptの間の部分のみを表示しています。

  1. json()の値を使う

    受け取り値にはエラーの場合"errorMessage"キーが含まれますので、それを使ってエラーの判断をします。

    try:
      
            response = requests.get(url, headers=headers)
            if 'errorMessage' in reponse.json().keys():
                    print(response.json()['errorMessage'])
                    #エラー時の処理
            else:
                    pass
                    #正常時の処理
      
    except Exception as e:
      
            print(e)
            #その他エラー処理の記述…
    
    Invalid Instrument ABC_JPY 
  2. status_codeの値を使う

    使用するエンドポイントに応じたステータスコードを使って比較する。

    try:
      
            response = requests.get(url, headers=headers)
            if response.status_code != requests.codes['OK']: 👈['OK']="200"
                    print(response.status_code)
                    #エラー時の処理
            else:
                    pass
                    #正常時の処理
    
    200の時は、requests.codes['OK']、201の時は、requests.codes['CREATED']を使っても同様な結果を得られます。
    400
  3. okの値を使う

    もっと単純にokの中に理論値が入っているのでこれを使う

    try:
      
            response = requests.get(url, headers=headers)
            if not(response.ok):
                    print(response.ok)
                    #エラー時の処理
            else:
                    pass
                    #正常時の処理
    
    False
  4. raise_for_status()を使う

    except Exception内にエラーの処理を記述してしまうとサーバーからのレスポンス以外のエラーの場合うまくいかないので、 locals()(またはvars())でレスポンス用の変数が存在しているか確認するかrequests.HTTPErrorで処理をわけるようにします。

    try:
      
            response = requests.get(url, headers=headers)
            response.raise_for_status()
            ...
            #正常時の処理
      
    except Exception as e:
            if "response" in locals():
                   print(response.text)
            print(e)
            #その他エラー処理
    
    HTTPErrorの場合の例:
    except requests.HTTPError as e_HTTP:        
           print(response.text)
           print(e_HTTP)
           #エラー時の処理
      
    except Exception as e:
           print(e)
           #その他エラー処理
    
    {"errorMessage":"Invalid Instrument ABC_JPY"}
     400 Client Error: Bad Request for url: https://api-fxpractice.oanda.com/v3/accounts/999-999-99999999-999/pricing?instruments=ABC_JPY


エラー時の処理が離れたところに記述できるraise_for_status()を使用する方がすっきりした感じがするので、個人的にはこれが好きです。



リトライする場合の方法

最後にサーバーからのエラーによっては再試行を行いたい場合の処理について考えてみたいと思います。

タイムアウトなど再試行すれば接続できる可能性のあるエラーの場合は一定時間後に再度接続する仕組みを考えてみたいと思います。 下のサンプルの例としては、初期値でリトライ回数とリトライまでの待ち時間を定義した関数を作成し、そこからサーバーに接続する流れにしています。 リトライさせたいステータスは配列に格納しておき、受け取ったコードと比較します。
それに該当する場合、またはサーバーへ接続できない場合のみ再度request.get命令を呼び出します。
また再試行回数を超えた場合は最後に発生したエラーを返す仕組みになっています。

# 必要なモジュールの読み込み
import requests
import time
  
#リトライ用の関数--------------------------------
def request_get_retry(url, headers, retry_counts = 3, sleep_duration = 3):

        #リトライさせたいステータスコードの指定
        RETRY_CODES = [408, 500, 502, 503, 504, 522, 524]
                
        for i in range(retry_counts + 1):
                try:
                        res = requests.get(url, headers=headers)
                        res.raise_for_status()
                        return res.json()      👈成功した時はその値を戻す
  
                except requests.HTTPError as e_HTTP:
                        Error_message = e_HTTP
                        if int(res.status_code) in RETRY_CO DES:
                                if retry_counts - i > 0:
                                        print("リトライあと%d回" %(retry_counts - i))
                                        time.sleep(sleep_duration)
                        else:
                                print("リトライせずに終了")
                                break

                except requests.ConnectionError as ec:                
                        Error_message = ec
                        if retry_counts - i > 0:
                                print("リトライあと%d回" %(retry_counts - i))
                                time.sleep(sleep_duration)

                except Exception as e:
                                Error_message = e
                                print("リトライせずに終了")
                                break

                                                       
        raise Exception(Error_message)  👈リトライ回数を超えた時にエラーの内容を上位に返す
  
#メイン--------------------------------
API_AccountID = '999-999-99999999-999'
API_URL =  "https://api-fxpractice.oanda.com"
INSTRUMENT = "USD_JPY"    
url = API_URL + "/v3/accounts/%s/pricing?instruments=%s" %(str(API_AccountID), INSTRUMENT)
headers = {
               "Authorization" : "Bearer " + API_Token
        }
  
try:
     
        response = requests.get(url, headers=headers)
        print(response)
        
except Exception as e:
  
        print(e)
        #エラー処理の記述…
            
pass

再現テストが難しいので、ネットワークを切断した状態で動かすと再試行後以下のエラーが表示されました。

リトライあと3回
リトライあと2回
リトライあと1回
HTTPSConnectionPool(host='api-fxpractice.oanda.com', port=443): Max retries exceeded with url: /v3/accounts/999-999-99999999-999/pricing?instruments=USD_JPY (Caused by NewConnectionError('<urllib3.connection.HTTPSConnection object at 0x000001B5B46CBC40>: Failed to establish a new connection: [Errno 11001] getaddrinfo failed'))


リトライするステータスコード、再試行回数および待機時間などは、そのシステムの設計思想や実際動かしてみて発生しうる状態によって変わってきますのでさらなる検証が必要になります。


まとめ

実際にすべてのステータスコードに対してテストするのは難しいので、200, 201以外のコードを受け取った時は早急に停止するまたは管理者に連絡できるような仕組みにしておくのが無難そうですね。
私の場合は、実験的にデモ口座でいろいろ学習用に検証しているだけですので、リアル口座で動かす場合はあくまでも自己責任でお願いいたします😱